题解:洛谷 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
【核心思想】
-
问题分析:给定 \(N\) 个整数序列,求至少出现 \(K\) 次的最长连续子序列长度(允许重叠)。本质上是二分答案 + 字符串哈希 / 子串计数问题:对长度进行二分,检查是否存在某个长度为 \(mid\) 的子序列出现至少 \(K\) 次。
-
算法选择:
- 二分答案:在 \([0, N]\) 范围内二分查找最大满足条件的长度 \(x\)
- 哈希表计数:将所有长度为 \(x\) 的连续子序列提取出来,用
map<string, int>统计出现次数
-
关键步骤:
- 读取数据:读入 \(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\)
-
时间/空间复杂度:
- 时间复杂度:\(O(\log N \cdot N^2 \cdot L)\),二分 \(O(\log N)\),每次
check提取子串 \(O(N \cdot L)\)(\(L\) 为平均子串长度) - 空间复杂度:\(O(N \cdot L)\),
map存储子串
- 时间复杂度:\(O(\log N \cdot N^2 \cdot L)\),二分 \(O(\log N)\),每次
-
二分 + 哈希表的核心思想:
- 单调性判定:若长度为 \(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
浙公网安备 33010602011771号