题解:洛谷 P2852 Milk Patterns

【题目来源】

洛谷:P2852 [USACO06DEC] Milk Patterns G - 洛谷

【题目描述】

农夫约翰注意到他的奶牛所产的牛奶质量每天都在变化。经过进一步调查,他发现虽然无法预测牛奶质量从一天到下一天的变化,但每天的牛奶质量中存在一些规律模式。

为了进行严格的研究,他发明了一种复杂的分类方案,其中每个牛奶样本被记录为一个介于 \(0\)\(1,000,000\) 之间的整数,并记录了一头奶牛在 \(N\ (1 \le N \le 20,000)\) 天内的数据。他希望找到一个最长的样本模式,该模式至少重复 \(K\ (2 \le K \le N)\) 次。这可能包括重叠的模式——例如,1 2 3 2 3 2 3 1 中的 2 3 2 3 重复了两次。

帮助农夫约翰找到样本序列中最长的重复子序列。保证至少有一个子序列重复至少 \(K\) 次。

【输入】

\(1\) 行:两个用空格分隔的整数:\(N\)\(K\)

\(2\) 行到第 \(N+1\) 行:\(N\) 个整数,每行一个,第 \(i\) 行表示第 \(i\) 天的牛奶质量。

【输出】

\(1\) 行:一个整数,表示至少出现 \(K\) 次的最长模式的长度。

【输入样例】

8 2
1
2
3
2
3
2
3
1

【输出样例】

4

【核心思想】

  1. 问题分析:给定 \(N\) 个整数序列,求至少出现 \(K\) 次的最长连续子序列长度(允许重叠)。本质上是二分答案 + 字符串哈希 / 子串计数问题:对长度进行二分,检查是否存在某个长度为 \(mid\) 的子序列出现至少 \(K\) 次。

  2. 算法选择

    • 二分答案:在 \([0, N]\) 范围内二分查找最大满足条件的长度 \(x\)
    • 哈希表计数:将所有长度为 \(x\) 的连续子序列提取出来,用 map<string, int> 统计出现次数
  3. 关键步骤

    • 读取数据:读入 \(N, K\)\(N\) 个整数,拼接成长字符串 \(s\)(下标从 \(1\) 开始)
    • 前缀和预处理\(sa[i]\) 表示前 \(i\) 个字符串的总字符数,用于快速定位子串位置
    • 二分查找
      • \(l = 0\)\(r = N\)
      • \(l < r\)\(mid = (l + r + 1) / 2\)
        • check(mid) 为真,\(l = mid\);否则 \(r = mid - 1\)
    • check(x) 函数
      • 遍历所有起始位置 \(i = 1\)\(N - x + 1\)
      • 计算子串 \(s[st\_pos \dots ed\_pos]\),用 map 统计频次
      • 返回最大频次是否 \(\geq K\)
    • 输出 \(l\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(\log N \cdot N^2 \cdot L)\),二分 \(O(\log N)\),每次 check 提取子串 \(O(N \cdot L)\)\(L\) 为平均子串长度)
    • 空间复杂度:\(O(N \cdot L)\)map 存储子串
  5. 二分 + 哈希表的核心思想

    • 单调性判定:若长度为 \(x\) 的子序列存在出现 \(K\) 次的,则长度 \(< x\) 的子序列也一定存在(子序列的任意前缀也是子序列),满足二分条件
    • 子串提取技巧:将每个整数转为字符串后拼接,利用前缀和数组快速定位任意连续 \(x\) 个整数拼接后的子串起止位置
    • 哈希表统计map<string, int> 自动去重并计数,\(O(\log M)\) 的插入和查询(\(M\) 为不同子串数)
    • 数据范围局限\(N \leq 20000\) 但子串长度可能很大,本题代码用字符串拼接方式在 \(N\) 较大时可能超时,更优解应使用后缀数组 + 高度数组滚动哈希
    • 适用于子串频次统计、模式匹配、二分判定类问题

【算法标签】

普及+ #哈希表

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 20005;

int n, k;
string s, a[N];
int sa[N];  // 前缀和数组,sa[i]表示前i个字符串拼接后的总字符数

// 检查是否存在长度为x的连续字符串组合出现至少k次
bool check(int x)
{
    // cout << "x " << x << endl;

    map<string, int> mp;
    for (int i = 1; i + x - 1 <= n; i++)
    {
        // 计算子串的起始和结束位置
        int st_pos = sa[i] - a[i].size() + 1;  // 第i个字符串的起始位置
        int ed_pos = sa[i + x - 1];            // 第(i+x-1)个字符串的结束位置
        // 提取子串
        string tmp = s.substr(st_pos, ed_pos - st_pos + 1);
        mp[tmp]++;  // 统计该子串出现的次数
    }
    int maxn = -1e9;
    for (auto t : mp)
    {
        maxn = max(maxn, t.second);  // 找到出现次数最多的子串
    }
    return maxn >= k;  // 判断是否有子串出现至少k次
}

int main()
{
    cin >> n >> k;
    s = " ";  // 在字符串前加一个空格,使索引从1开始
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        s += a[i];  // 将所有字符串拼接成一个长字符串
    }
    // 计算前缀和:sa[i]表示前i个字符串的总字符数
    for (int i = 1; i <= n; i++)
    {
        sa[i] = sa[i - 1] + a[i].size();
    }
    int l = 0, r = n;
    // 二分查找最大满足条件的x
    while (l < r)
    {
        int mid = (l + r + 1) / 2;
        if (check(mid))
        {
            l = mid;  // 满足条件,尝试更大的x
        }
        else
        {
            r = mid - 1;  // 不满足条件,减小x
        }
    }
    cout << l << endl;
    return 0;
}

【运行结果】

8 2
1
2
3
2
3
2
3
1
4
posted @ 2026-08-24 11:25  团爸讲算法  阅读(7)  评论(0)    收藏  举报