字符串哈希 - # CF271D Good Substrings
题目描述
你有一个由小写英文字母组成的字符串 \(s\)。其中一些英文字母是“好”的,剩下的则是“坏”的。
字符串 \(s\) 的一个子串 \(s[l...r]\)(\(1 \leq l \leq r \leq |s|\)),即 \(s = s_1s_2...s_{|s|}\)(其中 \(|s|\) 表示字符串 \(s\) 的长度),定义为 \(s_l s_{l+1}...s_r\)。
如果子串 \(s[l...r]\) 中的字母 \(s_l, s_{l+1}, ..., s_r\) 至多有 \(k\) 个是“坏”的(具体见样例解释),我们称该子串为“好子串”。
你的任务是计算字符串 \(s\) 中不同的好子串的数量。两个子串 \(s[x...y]\) 和 \(s[p...q]\) 被认为是不同的,当且仅当它们的内容不相同,即 \(s[x...y] \neq s[p...q]\)。
输入格式
第一行输入一个非空字符串 \(s\),由小写英文字母组成,长度不超过 \(1500\)。
第二行输入一个仅由字符“0”和“1”组成的字符串,长度恰为 \(26\)。如果这个字符串的第 \(i\) 个字符为“1”,则第 \(i\) 个英文字母是“好”的,否则是“坏”的。也就是说,这一行的第一个字符对应字母 \(a\),第二个对应 \(b\),以此类推。
第三行输入一个整数 \(k\)(\(0 \le k \le |s|\)),表示一个好子串中最多可以包含的“坏”字符数。
输出格式
输出一个整数,表示字符串 \(s\) 中不同的好子串的数量。
输入输出样例 #1
输入 #1
ababab
01000000000000000000000000
1
输出 #1
5
输入输出样例 #2
输入 #2
acbacbacaa
00000000000000000000000000
2
输出 #2
8
说明/提示
在第一个样例中,所有好子串有:"a"、"ab"、"b"、"ba"、"bab"。
在第二个样例中,所有好子串有:"a"、"aa"、"ac"、"b"、"ba"、"c"、"ca"、"cb"。
二、解题思路
1. 核心观察
字符串长度最大为 1600,所有子串总数最多为 \(\dfrac{1600\times 1601}{2} \approx 1.28\times 10^6\),暴力枚举所有子串完全可行。
2. 枚举子串方式
采用双循环枚举左右端点:
- 外层循环枚举子串左端点 \(l\);
- 内层循环从 \(l\) 开始不断向右扩展右端点 \(r\),逐个字符累加;
- 过程中统计当前子串内禁用字符数量 \(cnt\),一旦 \(cnt>k\),后续向右扩展的子串必然也不合法,直接
break剪枝。
3. 去重方案(单变量合并双哈希)
题目要求统计不同子串,需要对子串判重:
- 使用双哈希降低哈希碰撞概率:分别计算两组不同模数的多项式哈希;
- 代码中将两组哈希值做组合:\(hash = num1 \times MOD1 + num2\),把二元哈希对压缩为一个
long long整数; - 借助
set集合自动去重,最终集合的大小就是答案。
4. 哈希计算方式
采用滚动哈希(无需预处理前缀哈希数组与幂次数组):
固定左端点 \(l\),右端点 \(r\) 右移时,递推更新哈希值:
BASE 取 31,搭配两个大质数模数 \(10^9+7\)、\(10^9+9\),碰撞概率极低。
四、代码详解(附带完整注释)
#include <iostream>
#include <cstring>
#include <set>
using namespace std;
typedef long long LL;
const LL MOD1 = 1000000007;
const LL MOD2 = 1000000009;
const int BASE = 31;
bool F[30];
set<LL> st;
int main() {
char s[1600];
scanf("%s", s);
int len = strlen(s);
for (int i = 0; i < 26; ++i) {
char ch;
scanf(" %c", &ch);
if (ch == '0')
F[i] = true;
}
int k;
cin >> k;
// 枚举子串左端点 l
for (int l = 0; l < len; ++l) {
int cnt = 0; // 当前子串内禁用字符个数
LL num1 = 0, num2 = 0; // 两组滚动哈希
// 枚举子串右端点 r,从 l 向右扩展
for (int r = l; r < len; ++r) {
int idx = s[r] - 'a';
cnt += F[idx];
if (cnt > k) break;
// 滚动计算双哈希
num1 = (num1 * BASE + s[r]) % MOD1;
num2 = (num2 * BASE + s[r]) % MOD2;
// 合并两个哈希值,存入集合去重
st.insert(num1 * MOD1 + num2);
}
}
cout << st.size() << endl;
return 0;
}
五、复杂度分析
设字符串长度为 \(n\ (n\le 1600)\)
- 枚举子串:最坏情况 \(O(n^2)\),总运算量约 \(2.56\times 10^6\),效率极高;
set插入操作:单次复杂度 \(O(\log m)\),\(m\) 为不同子串数量;- 整体复杂度:\(\boldsymbol{O(n^2 \log n)}\),完全满足本题数据限制。
六、关键点补充
- 输入细节
scanf(" %c", &ch)前面加空格,作用是跳过缓冲区中的换行、空格,避免读到空字符。 - 哈希合并技巧
用 \(num1 \times MOD1 + num2\) 将两个long long哈希值合并为一个值存入set,替代set<pair<LL,LL>>,写法更简洁。 - 剪枝优化
当子串内禁用字符数 \(cnt>k\) 时直接break,避免无效枚举,在限制 \(k\) 较小时能大幅提速。 - 哈希选择
选用两组大质数模数 + 不同进制基数,是竞赛中规避哈希碰撞的标准做法。

浙公网安备 33010602011771号