把长度为 8 的子串和密码都转成 26 个字母的计数签名,再用滑动窗口统计每种签名出现次数。
OJ: luogu
题目 ID: P8630
难度:普及/提高-
标签:字符串滑动窗口哈希
日期: 2026-06-21 13:43
题意
给一段只含小写字母的文本,再给若干个长度为 8 的密码。
如果文本中某个长度为 8 的子串,经过重新排列后可以变成这个密码,就算匹配一次。
要求所有密码总共匹配了多少次。
思路
先看一个可以直接验证想法的朴素解:
cpp
// brute.cpp:直接枚举每个长度为 8 的子串并比较计数,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
string s;
int n;
bool same_count(const string &a, const string &b) {
int c1[26] = {0};
int c2[26] = {0};
for (int i = 0; i < 8; i++) {
c1[a[i] - 'a']++;
c2[b[i] - 'a']++;
}
for (int i = 0; i < 26; i++) {
if (c1[i] != c2[i]) {
return false;
}
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> s;
cin >> n;
long long ans = 0;
while (n--) {
string t;
cin >> t;
for (int i = 0; i + 7 < (int) s.size(); i++) {
string sub = s.substr(i, 8);
if (same_count(sub, t)) {
ans++;
}
}
}
cout << ans << '\n';
return 0;
}因为允许“任意排列后相同”,所以真正重要的不是字符顺序,而是:
- 每个字母分别出现了多少次
也就是说:
- 两个长度为
8的串互为排列 - 当且仅当它们 26 个字母的出现次数完全一样
于是可以把每个串都压成一个“计数签名”。
做法分两步:
- 先对原文本中所有长度为
8的子串做滑动窗口 - 把每个窗口的 26 维计数压成签名,统计它出现了多少次
之后每读一个密码:
- 统计它的 26 个字母出现次数
- 生成同样的签名
- 直接去表里查,这个签名出现了多少次
把这些次数累加起来就是答案。
因为窗口长度固定为 8,滑动窗口更新计数非常方便。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
string s;
int n;
// 把 26 个字母出现次数拼成一个字符串签名,方便放进 map。
string build_key(const int cnt[]) {
string key;
for (int i = 0; i < 26; i++) {
key += to_string(cnt[i]);
key += '#';
}
return key;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> s;
cin >> n;
map<string, int> mp;
int cnt[26] = {0};
for (int i = 0; i < (int) s.size(); i++) {
cnt[s[i] - 'a']++;
if (i >= 8) {
cnt[s[i - 8] - 'a']--;
}
if (i >= 7) {
mp[build_key(cnt)]++;
}
}
long long ans = 0;
while (n--) {
string t;
cin >> t;
int need[26] = {0};
for (int i = 0; i < 8; i++) {
need[t[i] - 'a']++;
}
ans += mp[build_key(need)];
}
cout << ans << '\n';
return 0;
}复杂度
设原串长度为 L,密码个数为 n。
滑动窗口一共处理 L 个字符,每次生成签名需要看 26 个字母,所以复杂度是
每个密码同样需要统计 26 个字母并查询一次,复杂度是
总复杂度可以看成
总结
这题的关键转换是:
- “排列后相同”不看顺序,只看计数
- 固定长度为
8,适合滑动窗口 - 文本的所有窗口签名先统计出来,再统一回答查询
本质上是“滑动窗口 + 计数签名”的字符串题。