[USACO06DEC] Milk Patterns G

GitHub跳转原题关系图返回列表

二分模式长度,用序列双哈希统计固定长度子数组是否有出现至少 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 次。

用序列哈希处理:

  1. 预处理前缀哈希和 base 的幂;
  2. 对每个起点 iO(1)O(1) 取出区间 [i, i+len-1] 的哈希;
  3. 把这些哈希排序;
  4. 扫描排序后的数组,若某个相同哈希连续段长度至少为 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;
}

复杂度

每次检查排序 O(n)O(n) 个哈希,复杂度为 O(nlogn)O(n log n);二分 O(logn)O(log n) 次。

总时间复杂度为 O(nlog2n)O(n log^2 n),空间复杂度为 O(n)O(n)

总结

这道题的核心是“答案长度单调”。

二分长度后,再用哈希把连续子序列比较降到排序统计哈希值,从而快速判断某个长度是否可行。