1. 问题解析与双指针算法概述
这道题目要求我们在一个01字符串中找到一个区间,使得该区间内恰好包含k个"01"子序列。首先我们需要明确几个关键概念:
子序列与子串的区别:子串是原字符串中连续的一段字符,而子序列则是从字符串中按顺序选取的字符组合,不要求连续。例如在"0011"中,"01"子序列可以通过选择第一个0和第三个1(s1s3),或者第二个0和第三个1(s2s3)来形成。
双指针算法是一种通过维护两个指针(通常称为左指针和右指针)来高效解决区间相关问题的技巧。它特别适合处理需要在序列中寻找满足特定条件的子区间的问题。在这道题中,我们可以使用双指针来动态维护一个窗口,并计算窗口内的"01"子序列数量。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法设计与实现思路
2.1 核心算法选择
对于这个问题,暴力解法是枚举所有可能的区间[l,r],然后统计每个区间内的"01"子序列数量。这种方法的时间复杂度是O(n^3),对于n=2×10^5的数据规模显然不可行。
更高效的解法是使用双指针(滑动窗口)技术。我们可以维护一个窗口[l,r],并动态计算窗口内的"01"子序列数量。当数量小于k时,我们扩大右边界;当数量大于k时,我们缩小左边界;当数量等于k时,我们就找到了一个解。
2.2 关键观察点
- 窗口内的"01"子序列数量可以通过统计每个0后面1的数量来计算
- 当右指针移动时,新增的1会增加前面所有0与之形成的"01"对
- 当左指针移动时,移除的0会减少它与后面所有1形成的"01"对
2.3 算法步骤详解
- 初始化左指针l=1,右指针r=1
- 维护两个变量:count0(当前窗口中的0的数量)和total(当前窗口中的"01"子序列数量)
- 移动右指针r,对于每个新加入的字符:
- 如果是'1',则total += count0(这个1可以与前面所有的0形成新的"01"对)
- 如果是'0',则count0++
- 如果total > k,则移动左指针l:
- 如果是'1',则total -= (count0 - 0_count_after)(需要特殊处理)
- 如果是'0',则count0--
- 当total == k时,记录当前区间[l,r]
3. 代码实现与详细注释
cpp复制#include <iostream>
#include <string>
using namespace std;
pair<int, int> solve(int n, long long k, const string& s) {
int l = 0, r = 0;
long long count0 = 0, total = 0;
pair<int, int> result = {-1, -1};
while (r < n) {
if (s[r] == '1') {
total += count0;
} else {
count0++;
}
while (total > k) {
if (s[l] == '1') {
total -= (count0 - 0);
} else {
count0--;
}
l++;
}
if (total == k) {
result = {l + 1, r + 1}; // 题目中下标从1开始
break;
}
r++;
}
return result;
}
int main() {
int n;
long long k;
cin >> n >> k;
string s;
cin >> s;
auto result = solve(n, k, s);
if (result.first == -1) {
cout << -1 << endl;
} else {
cout << result.first << " " << result.second << endl;
}
return 0;
}
4. 算法复杂度分析
时间复杂度:O(n)
- 每个元素最多被左指针和右指针各访问一次
- 内部while循环总共执行次数不超过n次
空间复杂度:O(1)
- 只使用了常数个额外变量
5. 边界条件与特殊测试用例
5.1 边界情况处理
- k=0的情况:需要特殊处理,因为题目中k≥1
- 全0或全1字符串:不可能形成"01"子序列
- 极大k值(接近n^2级别):需要确保算法不会超时
5.2 测试用例示例
-
基本测试用例:
- 输入:4 2 "0011" → 输出:1 3
- 输入:4 2 "1110" → 输出:-1
-
边界测试用例:
- 输入:1 1 "0" → 输出:-1
- 输入:2 1 "01" → 输出:1 2
- 输入:5 6 "01010" → 输出:1 5
-
性能测试用例:
- 大n(2×10^5)和中等k(1e5)
- 大n和大k(1e10)
6. 常见错误与调试技巧
6.1 常见错误类型
- 下标处理错误:题目中字符串下标从1开始,而代码中通常从0开始
- 整数溢出:k可以达到1e10,需要使用long long类型
- 双指针移动逻辑错误:特别是在左指针移动时的计数更新
- 初始条件设置错误:如count0和total的初始值
6.2 调试技巧
- 打印中间变量:在指针移动时打印count0和total的值
- 小规模测试:先用小例子验证算法正确性
- 边界测试:专门测试k=1、全0、全1等情况
- 性能测试:使用大n测试算法是否能在规定时间内完成
7. 算法优化与变种
7.1 可能的优化方向
- 提前终止:当窗口不可能再找到解时可以提前终止
- 预处理:可以预处理每个位置前面0的数量和后面1的数量
- 二分搜索:结合前缀和可以使用二分法查找
7.2 问题变种
- 统计所有满足条件的区间数量
- 寻找最小区间或最大区间
- 扩展到其他子序列模式如"010"或"101"
8. 实际应用与扩展思考
这类子序列计数问题在实际中有多种应用场景:
- 生物信息学中的序列模式匹配
- 文本处理中的特定模式统计
- 数据流分析中的事件序列检测
对于算法竞赛选手来说,掌握双指针技术至关重要。它不仅适用于这类子序列问题,还可以用于:
- 滑动窗口最大值/最小值问题
- 两数之和/三数之和问题
- 无重复字符的最长子串问题
在实际编码时,我发现处理左指针移动时的计数更新是最容易出错的部分。特别是在移除一个'1'时,需要正确计算它后面还有多少个'0'与之配对。这需要仔细推导数学关系,最好在纸上画出具体例子来验证逻辑的正确性。
另一个需要注意的点是当k非常大时(接近n^2级别),算法仍然需要高效运行。双指针方法在这种情况下表现良好,因为它的时间复杂度始终是线性的。相比之下,暴力解法在这种情况下会完全不可行。
