【CodeForces-1446D2】Frequency Problem (Hard Version)
根号分治好题。
views
| comments
根号分治好题。
题意#
给出一个序列 ,求最长的子段,使得其中有至少两个出现次数最多的元素。
输出子段的长度,.
题解#
引理及证明#
众数:一个序列中出现次数最多的数,可以有多个。
首先有一个关于众数的结论:答案子段的众数集合,一定包含原序列的众数。
可以通过反证法来证明:
记 是原序列的众数,假设存在子段 是题目所求的最长子段,且子段的众数不包含 .
根据众数的定义,在 和 中一定还包含 。不妨设左侧包含 ,即 .
考虑将子段 向两侧扩展,把 包含进去,这样使子段长度变大,下面说明一定存在方案可以包含 .
把 扩展为 后,
- 若其众数的出现次数不变,则 即为更优答案。
- 若其众数的出现次数增加,那么我们继续扩展,使更多的 被包含进去。由于 是整个序列中出现次数最多的数,所以把 一个一个加进去,最后就变成了上面的情况,可以满足条件。
因此,我们总可以找到一个比 更优的答案,假设不成立。
所以结论成立。
思路#
先找到序列 的众数 ,若存在多个众数,则答案为 .
对答案中众数的出现次数进行根号分治,分别采用不同的方法处理。
设 为答案子段的众数的出现次数, 表示答案子段的众数, 为值 在序列 中的出现次数。
下面进行分类讨论。
的情况#
由于答案子段一定包含众数 ,且 ,所以 也满足 .
显然,满足这个条件的 最多有 个,考虑枚举 。
只需在 的复杂度内求出:以 作为众数的最大子段长度。
此时问题只与 这两个值有关,需要用到一个常见套路,让 的权值为 , 的权值为 ,其余为 .
只需在新序列中找:数值和为 的最长子段。
对新数列维护前缀和 ,子段 的数值和为 等价于:,只需记录每个 的最早出现位置即可。
// 1. 答案区间中众数的出现次数 > sqrt(n),枚举所有可行的数值check
for(int i = 1; i <= n; i++) if(cnt[i] >= Sqrt && i != mxval) {
memset(L, 0, sizeof(L)); L[0 + N] = 0;
for(int j = 1; j <= n; j++) {
s[j] = s[j - 1] + (a[j] == mxval ? 1 : (a[j] == i ? -1 : 0));
if(!L[s[j] + N] && s[j]) L[s[j] + N] = j; //注意这里下标可能为负,所以要都加上N
else ans = max(ans, j - L[s[j] + N]);
}
}
cpp注意 ,记录最早出现位置的数组,下标需要整体 .
的情况#
考虑枚举 ,只需在 的复杂度内求出:众数出现次数为 的最大子段。
这可以用双指针解决,细节有点多。
// 2. 答案区间中众数的出现次数 <= sqrt(n),枚举所有可行的答案,双指针判断是否合法
for(int C = 1; C <= Sqrt; C++) {
memset(cnt, 0, sizeof(cnt));
for(int l = 1, r = 1, C_cnt = 0; r <= n; r++) {
cnt[a[r]]++; if(cnt[a[r]] == C) C_cnt++;
while(l <= r && cnt[a[r]] > C) {
if(cnt[a[l]] == C) C_cnt--;
cnt[a[l++]]--;
}
if(C_cnt >= 2) ans = max(ans, r - l + 1);
}
}
cpp在上面的代码中, 代表出现次数为 的数的个数,动态维护桶。
代码#
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e5 + 10, N = 2e5;
int n, a[MAXN], ans;
int L[MAXN << 1], s[MAXN]; //情况一,L[i]为前缀和i第一次出现的下标,s[i]为前缀和
int cnt[MAXN]; //情况2,对于目前的尺取区间,cnt是对a[]的桶
int main() {
ios::sync_with_stdio(false);
cin >> n;
int Sqrt = (int)(sqrt(n));
for(int i = 1; i <= n; i++) cin >> a[i], cnt[a[i]]++;
int mxcnt = 0, mxval = -1;
for(int i = 1; i <= n; i++) {
if(cnt[a[i]] > mxcnt) mxcnt = cnt[a[i]], mxval = a[i];
else if(cnt[a[i]] == mxcnt && a[i] != mxval) mxval = n + 1;
}
if(mxval == n + 1) { cout << n << endl; return 0; } //若众数有俩,直接输出n
// 1. 答案区间中众数的出现次数 > sqrt(n),枚举所有可行的数值check
for(int i = 1; i <= n; i++) if(cnt[i] >= Sqrt && i != mxval) {
memset(L, 0, sizeof(L)); L[0 + N] = 0;
for(int j = 1; j <= n; j++) {
s[j] = s[j - 1] + (a[j] == mxval ? 1 : (a[j] == i ? -1 : 0));
if(!L[s[j] + N] && s[j]) L[s[j] + N] = j; //注意这里下标可能为负,所以要都加上N
else ans = max(ans, j - L[s[j] + N]);
}
}
// 2. 答案区间中众数的出现次数 <= sqrt(n),枚举所有可行的答案,双指针判断是否合法
for(int C = 1; C <= Sqrt; C++) {
memset(cnt, 0, sizeof(cnt));
for(int l = 1, r = 1, C_cnt = 0; r <= n; r++) {
cnt[a[r]]++; if(cnt[a[r]] == C) C_cnt++;
while(l <= r && cnt[a[r]] > C) {
if(cnt[a[l]] == C) C_cnt--;
cnt[a[l++]]--;
}
if(C_cnt >= 2) ans = max(ans, r - l + 1);
}
}
cout << ans << endl;
return 0;
}
cpp