P8330 [ZJOI2022] 众数
P8330 [ZJOI2022] 众数
转化一下题意,求出将循环序列切成两段,每段区间众数出现次数之和的最大值。
一般 “出现次数” 都与 根号分治 挂钩,因为出现次数少的数可以直接考虑出现次数,出现次数多的数可以直接考虑每个数,这样就出现了根号。
将出现次数
考虑大数在外侧的情况。枚举每个大数
大数在内侧同理,只要先把所有
接下来我们只关心小数配对小数的情况。如果确定了某个小数
这样我们只需求出区间
普通地求
实际上还有一个性质没有用到,就是只需要众数出现次数
这样查询区间
更进一步地,考虑左端点
取
-
一种空间复杂度线性的做法:考虑从左往右扫描线枚举右端点
r ,时刻维护S_i 表示[i, r - 1] 的众数出现次数。跳过出现次数大于等于B 的a_r 。接下来讨论的a_r 的出现次数均小于B 。首先对于
a_r 的所有出现位置l ,用l 之前a_r 的出现次数,加上r 之后a_r 的出现次数,加上S_{l + 1} 更新a_r 的答案。全部更新完毕后再将
S_i 从[i, r - 1] 更新为[i, r] ,这部分可以再枚举a_r 的所有出现位置l ,用[l, r] 内a_r 的出现次数c 更新所有S_{1\sim l - 1} 。注意到S 的单调不增性,所以从l - 1 到1 枚举p ,一旦S_p\geq c 那么说明再往前更新也是无用的,可以 break 掉。由于每次
p 向前移动都会使S 的总和至少增加1 ,而S 的总和最终不超过nB ,所以总复杂度是线性根号。
#include <bits/stdc++.h>
using namespace std;
inline int read() {
int x = 0;
char s = getchar();
while(!isdigit(s)) s = getchar();
while(isdigit(s)) x = x * 10 + s - '0', s = getchar();
return x;
}
const int B = 200;
const int N = 2e5 + 5;
int n, a[N], d[N], ans[N], pointer[B][N];
vector <int> pos[N];
void solve() {
cin >> n;
for(int i = 1; i <= n; i++) pos[i].clear(), ans[i] = 0;
for(int i = 1; i <= n; i++) scanf("%d", &a[i]), d[i] = a[i];
sort(d + 1, d + n + 1);
for(int i = 1; i <= n; i++) pos[a[i] = lower_bound(d + 1, d + n + 1, a[i]) - d].push_back(i);
for(int i = 1; i <= n; i++) {
if(pos[i].size() < B) continue;
static int pre[N];
memset(pre, 0, sizeof(pre));
for(int it : pos[i]) pre[it] = 1;
for(int p = 1; p <= n; p++) pre[p] += pre[p - 1];
for(int j = 1; j <= n; j++) {
if(i == j) continue;
int mx = 1, cur = 1;
for(int ind = 1; ind < pos[j].size(); ind++) mx = max(mx, cur = max(1, cur - (pre[pos[j][ind]] - pre[pos[j][ind - 1]]) + 1));
ans[i] = max(ans[i], mx + (int) pos[i].size());
mx = cur = 0;
for(int ind = 1; ind < pos[j].size(); ind++) {
mx = max(mx, cur += pre[pos[j][ind]] - pre[pos[j][ind - 1]]);
cur = max(0, cur - 1);
}
ans[j] = max(ans[j], mx + (int) pos[j].size());
}
}
int limit = 0;
for(int i = 1; i <= n; i++) limit = max(limit, (int) pos[i].size());
limit = min(limit, B - 1);
for(int i = 1; i <= limit; i++) {
static int buc[N], mode;
memset(buc, mode = 0, sizeof(buc));
for(int l = 1, r = 0; l <= n; buc[a[l++]]--) {
while(buc[mode] < i) {
buc[a[r = r < n ? r + 1 : 1]]++;
if(buc[a[r]] > buc[mode]) mode = a[r];
}
pointer[i][l] = r;
}
}
for(int i = 1; i <= n; i++) {
if(pos[i].size() > limit) continue;
for(int j = 0; j < pos[i].size(); j++) { // 枚举左端点 l.
for(int k = 1; k <= limit; k++) if(pointer[k][1] < pos[i][j]) ans[i] = max(ans[i], (int) pos[i].size() - j + k); // 先处理一下 l = 1 的情况.
int pt = j, p = pos[i][j] + 1;
if(p > n) continue;
for(int k = 1; k <= limit; k++) { // 这里不同于题解的是, 我从小到大枚举出现次数, 用指针维护最小的合法右端点, 本质没有太大差别.
if(pointer[k][p] < p) break; // 如果要绕过去变成外侧就不行了.
while(pt < pos[i].size() && pos[i][pt] <= pointer[k][p]) pt++; // 出现次数增大使得右端点右移.
ans[i] = max(ans[i], j + k + 1 + (int) pos[i].size() - pt);
}
}
}
int mx = 0;
for(int i = 1; i <= n; i++) mx = max(mx, ans[i]);
cout << mx << endl;
for(int i = 1; i <= n; i++) if(ans[i] == mx) printf("%d\n", d[i]);
}
int main() {
int T;
cin >> T;
while(T--) solve();
return cerr << clock() << endl, 0;
}
/*
2022/5/8
start thinking at ??:??
高妙根号分治题.
先处理大块与大块和小块.
再处理小块之间的情况.
写一个 sqrt log 试试看.
又 T 又 WA, 我不理解.
start coding at 14:42
交换 N, B 两维跑得飞快啊.
实际上将 B 开小一点效率会比较高, 因为对于小数的处理用了巨大的二维数组, 实在是太慢了.
finish debugging at 16:21
*/