二分模式长度,用序列双哈希统计固定长度子数组是否有出现至少 K 次的模式。
OJ: luogu
题目 ID: P2852
难度:普及+/提高
标签:字符串哈希二分答案排序
日期: 2026-06-22 22:20
题意
给定一个长度为 N 的整数序列,求最长的连续子序列长度,使得某个相同模式至少出现 K 次。
出现位置可以重叠。
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:枚举长度并用 vector 序列精确计数,只适合小数据。
int n, k;
int a[105];
bool check_len(int len) {
map<vector<int>, int> cnt;
for (int i = 1; i + len - 1 <= n; i++) {
vector<int> seq;
for (int j = 0; j < len; j++) {
seq.push_back(a[i + j]);
}
cnt[seq]++;
if (cnt[seq] >= k) {
return true;
}
}
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int answer = 0;
for (int len = 1; len <= n; len++) {
if (check_len(len)) {
answer = len;
}
}
cout << answer << '\n';
return 0;
}直接枚举长度并用 vector 计数可以理解题意,但满数据太慢。
先观察单调性:如果长度 L 的某个模式出现至少 K 次,那么长度 L-1 的某个模式也至少出现 K 次。因此可以二分答案长度。
固定长度 len 后,问题变成:所有长度为 len 的连续子序列中,是否有某一种出现至少 K 次。
用序列哈希处理:
- 预处理前缀哈希和 base 的幂;
- 对每个起点
i,取出区间 [i, i+len-1]的哈希; - 把这些哈希排序;
- 扫描排序后的数组,若某个相同哈希连续段长度至少为
K,则len可行。
代码使用双 unsigned long long 哈希降低碰撞概率。因为原序列元素可能为 0,读入后先加一再参与哈希。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 20005;
struct HashValue {
unsigned long long h1, h2;
};
int n, k;
unsigned long long a[MAXN];
unsigned long long pow1_base[MAXN], pow2_base[MAXN];
unsigned long long pre1[MAXN], pre2[MAXN];
HashValue hashes[MAXN];
bool cmp_hash(const HashValue &a, const HashValue &b) {
if (a.h1 != b.h1) {
return a.h1 < b.h1;
}
return a.h2 < b.h2;
}
bool same_hash(const HashValue &a, const HashValue &b) {
return a.h1 == b.h1 && a.h2 == b.h2;
}
void read_input() {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i]++;
}
}
void build_hash() {
const unsigned long long base1 = 1000003;
const unsigned long long base2 = 1000033;
pow1_base[0] = 1;
pow2_base[0] = 1;
for (int i = 1; i <= n; i++) {
pow1_base[i] = pow1_base[i - 1] * base1;
pow2_base[i] = pow2_base[i - 1] * base2;
pre1[i] = pre1[i - 1] * base1 + a[i];
pre2[i] = pre2[i - 1] * base2 + a[i];
}
}
HashValue get_hash(int left, int right) {
HashValue res;
int len = right - left + 1;
res.h1 = pre1[right] - pre1[left - 1] * pow1_base[len];
res.h2 = pre2[right] - pre2[left - 1] * pow2_base[len];
return res;
}
bool check(int len) {
if (len == 0) {
return true;
}
int count_hash = 0;
for (int i = 1; i + len - 1 <= n; i++) {
count_hash++;
hashes[count_hash] = get_hash(i, i + len - 1);
}
sort(hashes + 1, hashes + count_hash + 1, cmp_hash);
int current = 1;
for (int i = 2; i <= count_hash; i++) {
if (same_hash(hashes[i], hashes[i - 1])) {
current++;
} else {
current = 1;
}
if (current >= k) {
return true;
}
}
return k <= 1;
}
void solve() {
build_hash();
int left = 0;
int right = n;
int answer = 0;
while (left <= right) {
int mid = (left + right) / 2;
if (check(mid)) {
answer = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
cout << answer << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
每次检查排序
总时间复杂度为
总结
这道题的核心是“答案长度单调”。
二分长度后,再用哈希把连续子序列比较降到排序统计哈希值,从而快速判断某个长度是否可行。