TonyYin's Blog

Back

根号分治好题。

题意#

给出一个序列 a1,a2,ana_1, a_2\cdots, a_n,求最长的子段,使得其中有至少两个出现次数最多的元素。

输出子段的长度,1n2×1051\leq n\leq 2\times 10^5.

题解#

引理及证明#

众数:一个序列中出现次数最多的数,可以有多个。

首先有一个关于众数的结论:答案子段的众数集合,一定包含原序列的众数。

可以通过反证法来证明:

xx 是原序列的众数,假设存在子段 al,al+1,,ara_l, a_{l+1}, \cdots, a_r 是题目所求的最长子段,且子段的众数不包含 xx.

根据众数的定义,在 a[1,l1]a_{[1, l-1]}a[r+1,n]a_{[r+1, n]} 中一定还包含 xx。不妨设左侧包含 xx,即 ap=x,p[1,l1]\exists a_p=x, p\in[1, l-1].

考虑将子段 a[l,r]a_{[l, r]} 向两侧扩展,把 apa_p 包含进去,这样使子段长度变大,下面说明一定存在方案可以包含 apa_p.

a[l,r]a_{[l, r]} 扩展为 a[p,r]a_{[p,r]} 后,

  • 若其众数的出现次数不变,则 a[p,r]a_{[p,r]} 即为更优答案。
  • 若其众数的出现次数增加,那么我们继续扩展,使更多的 xx 被包含进去。由于 xx 是整个序列中出现次数最多的数,所以把 xx 一个一个加进去,最后就变成了上面的情况,可以满足条件。

因此,我们总可以找到一个比 a[l,r]a_{[l,r]} 更优的答案,假设不成立

所以结论成立。

思路#

先找到序列 aa 的众数 xx,若存在多个众数,则答案为 nn.

对答案中众数的出现次数进行根号分治,分别采用不同的方法处理。

C\operatorname{C}答案子段的众数的出现次数x,yx,y 表示答案子段的众数,cnti\operatorname{cnt_i} 为值 ii 在序列 aa 中的出现次数。

下面进行分类讨论。

C>n\operatorname{C} > \sqrt{n} 的情况#

由于答案子段一定包含众数 xx,且 C>n\operatorname{C}>\sqrt{n},所以 yy 也满足 cnty>n\operatorname{cnt}_y>\sqrt{n}.

显然,满足这个条件的 yy 最多有 n\sqrt{n} 个,考虑枚举 yy

只需在 O(n)\mathcal{O}(n) 的复杂度内求出:以 (x,y)(x,y) 作为众数的最大子段长度

此时问题只与 x,yx,y 这两个值有关,需要用到一个常见套路,xx 的权值为 11yy 的权值为 1-1,其余为 00.

只需在新序列中找:数值和为 00 的最长子段。

对新数列维护前缀和 sis_i,子段 [l,r][l,r] 的数值和为 00 等价于:sr=sl1s_r=s_{l-1},只需记录每个 sis_i 的最早出现位置即可。

// 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

注意 si[n,n]s_i\in[-n, n],记录最早出现位置的数组,下标需要整体 ii+ni\leftarrow i+n.

Cn\operatorname{C} \leq \sqrt{n} 的情况#

考虑枚举 C\operatorname{C},只需在 O(n)\mathcal{O}(n) 的复杂度内求出:众数出现次数为 C\operatorname{C} 的最大子段

这可以用双指针解决,细节有点多

// 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

在上面的代码中,Ccnt\operatorname{C_{cnt}} 代表出现次数为 C\operatorname{C} 的数的个数,动态维护桶。

代码#

#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
【CodeForces-1446D2】Frequency Problem (Hard Version)
https://www.tonyyin.top/blog/oi-solution/cf1446d2
Author TonyYin
Published at December 21, 2021
Comment seems to stuck. Try to refresh?✨