按右端点线性扫描,用变量 cnt 单调维护上界 j-K+1 内 c1 出现的次数,遇 c2 累加 cnt,O(n) 计数。
OJ: luogu
题目 ID: P9244
难度:普及-
标签:字符串双指针滑动窗口计数枚举优化
创建: 2026-10-02 15:07
更新: 2026-10-03 11:36
形式化题目
给定长度为
- 长度
; - 首字符
; - 尾字符
。
其中
把长度条件展开成对下标的不等式,题目等价于统计下标对
暴力解法
思路
照着定义做:枚举左端点
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-03 11:36
* update_at: 2026-10-03 11:36
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 直接按定义枚举所有子串:左端点 i、右端点 j,
// 只要长度 j - i + 1 >= K 且 s[i] == c1 且 s[j] == c2 就计数。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 5005;
int K;
int n;
char s[maxn];
char c1, c2;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> K;
cin >> (s + 1) >> c1 >> c2;
n = (int)strlen(s + 1);
ll ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++) {
if (j - i + 1 < K) {
continue; // 长度不够,不配使用简写
}
if (s[i] == c1 && s[j] == c2) {
ans++;
}
}
}
cout << ans << "\n";
return 0;
}复杂度
两层循环都是
瓶颈
瓶颈具体在哪里?看内层循环在做什么:对固定的 s[i] == c1。而这个检查对同一行的所有
反过来想:如果固定右端点
正解
关键观察
固定右端点
所以当前所有合法起点恰好是一个前缀
每个合法子串都有唯一的右端点
再看这个前缀怎么变化:
思路
设
- 先解锁:检查刚进入范围的位置
,若 s[j-K+1] == c1则cnt++; - 再统计:若
s[j] == c2,则以结尾的合法子串全部以这 cnt个位置为起点,ans += cnt;否则以结尾没有合法子串,不加。
第 1 步必须排在第 2 步之前:位置
循环可以从
用样例检验一遍。
| 新解锁的 |
解锁后 cnt |
ans |
|||
|---|---|---|---|---|---|
| 4 | 1 | a | 1 | b | 1 |
| 5 | 2 | b | 1 | a | 1 |
| 6 | 3 | a | 2 | b | 3 |
| 7 | 4 | b | 2 | d | 3 |
| 8 | 5 | a | 3 | b | 6 |
以 cnt = 2,说明合法起点有 S[1..6] = ababab)和 S[3..6] = abab);S[6] = b 命中 cnt = 3,对应 [abababdb]、ab[ababdb]、abab[abdb] 三个。最终
正确性说明。
- 计数语义:对
做归纳。循环开始前 ,上界为 ,集合为空, cnt = 0成立;进入第轮后,集合从 扩展为 ,新增的恰是位置 ,代码只对它做了一次判定,故 cnt恒等于定义值。 - 不重不漏:任意合法子串由它的右端点
唯一确定,它在第 轮被计入当且仅当 、 且 ,这正是合法性的定义。 - 两者合起来,
ans恰好等于答案。
溢出与边界提醒。
- 答案上界约为
, int装不下,ans必须用long long。 时不需要特判:起点与终点是两个独立判定,同一个位置可以既作起点又作终点;又因为 ,起点上界 ,起点永远在终点左边,不会出现“自己配自己”的非法计数。 不出现在 中时 cnt恒为 0,输出 0,符合预期。时循环只执行 一次,只解锁位置 1,同样正确。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-03 11:36
* update_at: 2026-10-03 11:36
*/
// P9244 [蓝桥杯 2023 省 B] 子串简写
// 正解:枚举右端点 j,用变量 cnt 维护“左侧所有满足 c1 且 j - i + 1 >= K 的起点 i 的个数”。
// 每当 j 右移一位,就新允许一个起点,把它加入 cnt。
// 若 S[j] == c2,则这 cnt 个起点各对应一个合法子串,累加进答案。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 5e5 + 5;
int K; // 子串长度下限:长度 >= K 才可以使用简写
int n; // 字符串长度
char s[maxn]; // 原字符串,下标从 1 开始
char c1, c2; // 要求的首字符与尾字符
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> K;
cin >> (s + 1) >> c1 >> c2;
n = (int)strlen(s + 1);
ll ans = 0;
ll cnt = 0; // 在当前右端点 j 下,左侧可作为起点的位置个数
// j = K 是长度刚好等于 K 的最小右端点;j < K 时任何子串都不够长
for (int j = K; j <= n; j++) {
// 右端点从 j-1 变成 j 时,起点 i = j - K + 1 的长度由 K-1 变成 K,
// 正好刚刚够长,于是它被“解锁”,加入可选起点集合
if (s[j - K + 1] == c1) {
cnt++;
}
// 现在 cnt 里恰好是 [1, j-K+1] 中所有值为 c1 的位置,且全部满足长度条件
if (s[j] == c2) {
ans += cnt;
}
}
cout << ans << "\n";
return 0;
}复杂度
时间:j 从
空间:除输入字符串 cnt、ans 两个变量,额外空间
总结
本题的推导链条是:
- 换枚举顺序:从“先定左端点、再扫右端点”变成“先定右端点、回头找左端点”;
- 发现结构:固定
后合法起点恰好是一个前缀,而这个前缀随 增大“只进不出”; - 用计数器替代扫描:只进不出的结构不需要滑动窗口的删除操作,一个变量就能维护统计量。
第一步是这类计数题最常用的角度切换——要让被重复计算的量变得可复用。第二步是识别“单调窗口”。第三步是本题比一般双指针更简单的地方:因为窗口只扩张,连左指针都不用维护。
实现上要留心两点:
- 先解锁、后统计,顺序反了会漏掉长度恰好等于
的那些子串; - 答案要用 64 位整数,最多约
。
(暴力解法与正解的输出已在 20 组随机数据上逐一比对一致。)