#5637. 神奇的字符串题解
神奇的字符串题解
好的,我们来一起详细分析这道关于字符串处理的算法题。我会先给出详细的思路分析,然后附上带有详细注释的参考代码,方便你直接复制给学生使用。
题目详细思路分析
1. 题目理解
我们有一个只包含 'A', 'B', 'C' 的字符串 S。我们的目标是通过一系列操作,将其转换成一个 k阶ABC字符串。
k阶ABC字符串的定义是:由 连续的k个A、连续的k个B、连续的k个C 按顺序构成的字符串。例如,AABBCC 是一个 2 阶 ABC 字符串。
我们可以进行三种操作:
- 操作1:删除字符串开头的一个字符。
- 操作2:删除字符串结尾的一个字符。
- 操作3:删除字符串中间的一个字符。
我们的任务是:
- 找到一种方法,使得操作完成后,
S变成一个 k 阶 ABC 字符串。 - 优化目标:在所有可行的方法中,让 操作3的使用次数尽可能少。
- 如果无法完成转换,则输出
-1。
2. 核心问题转化
这道题的关键在于理解操作3的最小化。
我们可以把最终的 k 阶 ABC 字符串看作是从原始字符串 S 中挑选出的一个子序列。这个子序列需要满足:
- 它包含至少
k个A,k个B,k个C。 - 这些
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. 算法思路
为了高效地找到这个最小范围,我们可以采用以下步骤:
-
预处理位置信息:
- 遍历字符串
S,将所有'A'的位置记录在数组a[]中。 - 将所有
'B'的位置记录在数组b[]中。 - 将所有
'C'的位置记录在数组c[]中。
- 遍历字符串
-
遍历所有可能的A组合:
- 我们需要选择
k个连续的A。在数组a[]中,这对应一个长度为k的窗口。 - 我们从第
i个A开始,窗口就包含了a[i], a[i+1], ..., a[i+k-1]。
- 我们需要选择
-
寻找匹配的B组合:
- 对于上一步选定的
k个A,我们需要找到k个连续的B,并且这k个B必须都出现在窗口中最后一个A(即a[i+k-1])的后面。 - 这可以通过在数组
b[]中使用二分查找来快速定位第一个大于a[i+k-1]的B的位置。 - 假设我们找到了这个位置是
xx,那么我们的B窗口就是b[xx], b[xx+1], ..., b[xx+k-1]。 - 如果找不到足够的
B,说明当前的A窗口不可行,直接跳过。
- 对于上一步选定的
-
寻找匹配的C组合:
- 同理,对于上一步选定的
k个B,我们需要找到k个连续的C,并且这k个C必须都出现在窗口中最后一个B(即b[xx+k-1])的后面。 - 同样使用二分查找在数组
c[]中定位。 - 如果找不到足够的
C,说明当前组合不可行,跳过。
- 同理,对于上一步选定的
-
计算并更新最小操作3次数:
- 如果我们成功找到了
A、B、C的窗口,那么这个组合定义的范围是从第一个A(a[i]) 到最后一个C(c[xx2+k-1])。 - 计算这个范围内需要删除的字符数
(end - start + 1) - 3*k,并更新全局最小值。
- 如果我们成功找到了
-
结果输出:
- 遍历完所有可能的
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) 的时间复杂度内高效地解决这个问题。