#5637. 神奇的字符串题解

神奇的字符串题解

好的,我们来一起详细分析这道关于字符串处理的算法题。我会先给出详细的思路分析,然后附上带有详细注释的参考代码,方便你直接复制给学生使用。


题目详细思路分析

1. 题目理解

我们有一个只包含 'A', 'B', 'C' 的字符串 S。我们的目标是通过一系列操作,将其转换成一个 k阶ABC字符串

k阶ABC字符串的定义是:由 连续的k个A连续的k个B连续的k个C 按顺序构成的字符串。例如,AABBCC 是一个 2 阶 ABC 字符串。

我们可以进行三种操作:

  1. 操作1:删除字符串开头的一个字符。
  2. 操作2:删除字符串结尾的一个字符。
  3. 操作3:删除字符串中间的一个字符。

我们的任务是:

  • 找到一种方法,使得操作完成后,S 变成一个 k 阶 ABC 字符串。
  • 优化目标:在所有可行的方法中,让 操作3的使用次数尽可能少
  • 如果无法完成转换,则输出 -1

2. 核心问题转化

这道题的关键在于理解操作3的最小化

我们可以把最终的 k 阶 ABC 字符串看作是从原始字符串 S挑选出的一个子序列。这个子序列需要满足:

  • 它包含至少 kAkBkC
  • 这些 A 全部出现在 B 之前,这些 B 全部出现在 C 之前。

当我们确定了这 3*k 个字符在原字符串 S 中的位置后,我们就确定了一个最小的范围 [start, end],其中 start 是我们挑选的第一个 A 的位置,end 是我们挑选的最后一个 C 的位置。

在这个范围 [start, end] 内,除了我们挑选出的 3*k 个字符,剩下的所有字符都需要被删除。这些被删除的字符,就是通过操作3完成的。

因此,操作3的次数就等于:

(范围长度) - (我们需要的字符数)
= (end - start + 1) - (3 * k)

我们的目标就是找到所有可能的 [start, end] 范围,并选择其中 (end - start + 1) 最小的那个,从而使得操作3的次数最少。


3. 算法思路

为了高效地找到这个最小范围,我们可以采用以下步骤:

  1. 预处理位置信息

    • 遍历字符串 S,将所有 'A' 的位置记录在数组 a[] 中。
    • 将所有 'B' 的位置记录在数组 b[] 中。
    • 将所有 'C' 的位置记录在数组 c[] 中。
  2. 遍历所有可能的A组合

    • 我们需要选择 k 个连续的 A。在数组 a[] 中,这对应一个长度为 k 的窗口。
    • 我们从第 iA 开始,窗口就包含了 a[i], a[i+1], ..., a[i+k-1]
  3. 寻找匹配的B组合

    • 对于上一步选定的 kA,我们需要找到 k 个连续的 B,并且这 kB 必须都出现在窗口中最后一个 A(即 a[i+k-1])的后面。
    • 这可以通过在数组 b[] 中使用二分查找来快速定位第一个大于 a[i+k-1]B 的位置。
    • 假设我们找到了这个位置是 xx,那么我们的 B 窗口就是 b[xx], b[xx+1], ..., b[xx+k-1]
    • 如果找不到足够的 B,说明当前的 A 窗口不可行,直接跳过。
  4. 寻找匹配的C组合

    • 同理,对于上一步选定的 kB,我们需要找到 k 个连续的 C,并且这 kC 必须都出现在窗口中最后一个 B(即 b[xx+k-1])的后面。
    • 同样使用二分查找在数组 c[] 中定位。
    • 如果找不到足够的 C,说明当前组合不可行,跳过。
  5. 计算并更新最小操作3次数

    • 如果我们成功找到了 ABC 的窗口,那么这个组合定义的范围是从第一个 A (a[i]) 到最后一个 C (c[xx2+k-1])。
    • 计算这个范围内需要删除的字符数 (end - start + 1) - 3*k,并更新全局最小值。
  6. 结果输出

    • 遍历完所有可能的 A 窗口后,如果找到了可行解,就输出最小的操作3次数。
    • 如果没有找到任何可行解,则输出 -1

参考代码(带详细注释)

#include <bits/stdc++.h>
#define int long long
using namespace std;
int k;
// k阶ABC字符串的阶数
int a[1001010], b[1001010], c[1001010];
// 分别存储A、B、C在原字符串中的位置
int min_ops3 = INT_MAX;
// 最小操作3次数,初始化为无穷大
int numA, numB, numC;
// A、B、C的数量
string s; // 原始字符串
// 在数组b[]中,找到第一个大于x的元素的位置
int find_first_B(int x){
	int left = 1, right = numB, ans =-1;
	while (left <= right){
		int mid = (left + right) / 2;
		if (x < b[mid]){ // 当前B的位置在x之后
			ans = mid;
			// 记录可能的答案
			right = mid - 1;
			// 向左继续找更早的B
		}
		else {
			left = mid + 1;
			// 当前B的位置在x之前,向右找
		}
	}
	return ans;
	// 返回第一个大于x的B的位置
}
// 在数组c[]中,找到第一个大于x的元素的位置
int find_first_C(int x){
	int left = 1, right = numC, ans =-1;
	while (left <= right){
		int mid = (left + right) / 2;
		if (x < c[mid]){ // 当前C的位置在x之后
			ans = mid;
			// 记录可能的答案
			right = mid - 1;
			// 向左继续找更早的C
		}
		else {
			left = mid + 1;
			// 当前C的位置在x之前,向右找
		}
	}
	return ans;
	// 返回第一个大于x的C的位置
}
signed main(){
	cin >> k >> s;
	int n = s.size();
	s =  ' ' + s;
	// 为了让下标从1开始,方便计算
	// 1. 预处理位置
	for (int i = 1; i <= n; i++){
		if (s[i] ==  'A') a[++numA] = i;  // 记录A的位置
		else if (s[i] ==  'B') b[++numB] = i;  // 记录B的位置
		else c[++numC] = i;
		// 记录C的位置
	}
	// 2. 枚举所有可能的A区间
	for (int i = 1; i <= numA - k + 1; i++){
		int lastA = i + k - 1;
		// 当前A区间的最后一个A的位置
		int posA = a[lastA];
		// 最后一个A在原字符串中的位置
		// 3. 找匹配的B区间
		int startB = find_first_B(posA);  // 找到第一个在posA之后的B的位置
		if (startB ==-1 || startB + k - 1 > numB) continue;  // B不够
		int lastB = startB + k - 1;
		// 当前B区间的最后一个B的位置
		int posB = b[lastB];
		// 最后一个B在原字符串中的位置

		// 4. 找匹配的C区间
		int startC = find_first_C(posB);  // 找到第一个在posB之后的C的位置
		if (startC ==-1 || startC + k - 1 > numC) continue;  // C不够
		int lastC = startC + k - 1;
		// 当前C区间的最后一个C的位置
		int posC = c[lastC];
		// 最后一个C在原字符串中的位置

		// 5. 计算操作3次数
		int range_length = posC - a[i] + 1;
		// 区间长度
		int ops3 = range_length - 3 * k;
		// 操作3次数
		min_ops3 = min(min_ops3, ops3);  // 更新最小值
	}
	// 6. 输出结果
	if (min_ops3 == INT_MAX) cout <<-1;  // 无解
	else cout << min_ops3;
	// 输出最小操作3次数

	return 0;
}

总结

这道题的核心思想是将“最小化操作3次数”的问题,巧妙地转化为“寻找包含目标子序列的最短区间”的问题。通过预处理字符位置和使用二分查找,我们可以在 O(N log N) 的时间复杂度内高效地解决这个问题。