题意
给出一个序列 a a a,长度为 n ( 1 ≤ n ≤ 3 ⋅ 1 0 5 ) n(1\leq n \leq 3· 10^5) n(1≤n≤3⋅105)。给出 q ( 1 ≤ n ≤ 3 ⋅ 1 0 5 ) q(1\leq n \leq 3· 10^5) q(1≤n≤3⋅105) 个询问,每次选择一个区间 [ l , r ] [l,r] [l,r] ,问最少要将该区间拆分为几个子序列,使得每个序列中出现次数最多的数出现的次数不大于 ⌈ l e n 2 ⌉ \lceil \frac{len}{2}\rceil ⌈2len⌉,其中 l e n len len 为序列长度。
分析
假设序列长度为 a a a,出现次数最多的数出现的次数为 b b b,当 b > ⌈ a 2 ⌉ b>\lceil \frac{a}{2}\rceil b>⌈2a⌉,需要进行拆分。为了使原序列符合要求,每次应该选择出现次数最多的数拿出去,单独组成一个长度为 1 1 1 的序列。假设拿出了 x x x 个,有 b − x ≤ a − x + 1 2 b-x\leq \frac{a-x+1}{2} b−x≤2a−x+1,即 x ≥ 2 b − a − 1 x\geq 2b-a-1 x≥2b−a−1, x x x 取最小值,得到的序列个数的最小值为 x + 1 = 2 b − a x+1=2b-a x+1=2b−a。由此,问题转化为求出区间中出现次数最多的数出现的次数。可以利用主席树、线段树、随机数的方法(莫队不会QAQ)。
对于主席树,需要维护每个数出现的次数,因为最大值不能进行加减,但本题的特殊之处在于我们需要找出出现次数大于 ⌈ l e n 2 ⌉ \lceil \frac{len}{2}\rceil ⌈2len⌉ 的数的出现次数,通过判断区间中出现的数字的个数个目标的关系来判断。
对于线段树,每个点维护所表示区间中出现次数最大的数是哪个。需要提前预处理出每个数出现的位置,在区间合并时,只要判断两个儿子节点在父节点表示区间内谁出现次数更多,取大的即可。询问的时候记得放回出现次数。
随机数的方法,对于一段区间,出现次数大于 ⌈ l e n 2 ⌉ \lceil \frac{len}{2}\rceil ⌈2len⌉的数的个数一定占所有数的 1 2 \frac{1}{2} 21,每次随机选一个数,不是要求数的概率为 1 2 \frac{1}{2} 21,多选几次,使得 ( 1 2 ) k (\frac{1}{2})^k (21)k 尽量小,总有一次会选中。
代码
主席树代码:
#include <cstdio>
#include <algorithm>
#include <vector>
#include <iostream>
#define pb push_back
using namespace std;
const int N=3e5+5;
int a[N];
int tree[N*40],lson[N*40],rson[N*40];
int root[N],tol;
void update(int l,int r,int pos,int &rt,int last){
rt=++tol;
tree[rt]=tree[last]+1;
lson[rt]=lson[last];
rson[rt]=rson[last];
if(l==r) return;
int mid=(l+r)>>1;
if(pos<=mid) update(l,mid,pos,lson[rt],lson[last]);
else update(mid+1,r,pos,rson[rt],rson[last]);
}
int query(int l,int r,int u,int v,int val){
if(tree[v]-tree[u]<=val) return 0;
if(l==r) return tree[v]-tree[u];
int mid=(l+r)>>1;
//int num=tree[lson[v]]-tree[lson[u]];
if(tree[lson[v]]-tree[lson[u]]>val) return query(l,mid,lson[u],lson[v],val);
else if(tree[rson[v]]-tree[rson[u]]>val) return query(mid+1,r,rson[u],rson[v],val);
else return 0;
}
int main(){
int n,q;
scanf("%d%d",&n,&q);
tol=0;
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
update(1,n,x,root[i],root[i-1]);
}
while(q--){
int l,r;
scanf("%d%d",&l,&r);
int a=r-l+1;
int b=query(1,n,root[l-1],root[r],(a+1)/2);
printf("%d\n",max(1,2*b-a));
}
return 0;
}
线段树代码:
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <iostream>
#include <vector>
#define pb push_back
using namespace std;
const int N=3e5+5;
int a[N];
int tree[N<<2];//该点表示的区间内出现次数最多的数是哪个
vector<int>pos[N];
int cal(int x,int l,int r){
if(x==0) return 0;
return upper_bound(pos[x].begin(),pos[x].end(),r)-lower_bound(pos[x].begin(),pos[x].end(),l);
}
void pushup(int rt,int l,int r){
tree[rt]=cal(tree[rt<<1],l,r)>cal(tree[rt<<1|1],l,r)?tree[rt<<1]:tree[rt<<1|1];
}
void build(int l,int r,int rt){
if(l==r){
tree[rt]=a[l];
return;
}
int mid=(l+r)>>1;
build(l,mid,rt<<1);
build(mid+1,r,rt<<1|1);
pushup(rt,l,r);
}
int query(int l,int r,int L,int R,int rt){
if(L<=l&&r<=R) return cal(tree[rt],L,R);
int mid=(l+r)>>1,ans=0;
if(L<=mid) ans=max(ans,query(l,mid,L,R,rt<<1));
if(R>mid) ans=max(ans,query(mid+1,r,L,R,rt<<1|1));
return ans;
}
int main(){
int q,n;
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
pos[a[i]].pb(i);
}
build(1,n,1);
while(q--){
int l,r;
scanf("%d%d",&l,&r);
int x=r-l+1;
int y=query(1,n,l,r,1);
if(y>(x+1)/2) printf("%d\n",max(1,2*y-x));
else printf("1\n");
}
return 0;
}
随机数解法代码:
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cmath>
#include <random>
#include <chrono>
#define pb push_back
using namespace std;
const int N=3e5+5;
int a[N];
vector<int>pos[N];
int main(){
int n,q;
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
pos[a[i]].pb(i);
}
mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());//随机数种子
while(q--){
int l,r,num=0;
scanf("%d%d",&l,&r);
int len=r-l+1,maxn=0;
for(int i=1;i<=40;i++){
int x=a[uniform_int_distribution<int>(l,r)(rng)];
num=upper_bound(pos[x].begin(),pos[x].end(),r)-lower_bound(pos[x].begin(),pos[x].end(),l);
maxn=max(num,maxn);
}
printf("%d\n",max(1,2*maxn-len));
}
return 0;
}