主席树的一些习题

P3567 [POI2014]KUR-Couriers

题意

输出区间内出现次数严格大于区间长度的一半的数,存在输出任意,否则输出0。

思路

区间数出现次数具有可减性,考虑建立主席树,令 k = ( r − l + 1 ) 2 k=\dfrac{(r-l+1)}{2} k=2(rl+1),每次查询左子树元素个数是否大于 k k k,是就递归进入左子树,否则进入右子树,每次都需要提前判断当前区间元素个数是否大于 k k k,否则直接返回 0 0 0

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

空间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull; 
const int N=5e5+5,M=2e4+5,inf=0x3f3f3f3f,mod=1e9+7;
#define mst(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define fi first
#define se second
#define pb emplace_back
#define SZ(a) (int)a.size()
#define IOS ios::sync_with_stdio(false),cin.tie(0) 
void Print(int *a,int n){
	for(int i=1;i<n;i++)
		printf("%d ",a[i]);
	printf("%d\n",a[n]); 
}
int rt[N*20];
struct PST{
	#define lx a[x].l
	#define rx a[x].r
	struct node{
		int l,r,s;
	}a[N*20];
	int cnt;
	void upd(int &x,int pre,int l,int r,int v){
		x=++cnt,a[x]=a[pre],a[x].s++;
		if(l==r) return;
		int m=l+r>>1;
		if(v<=m) upd(lx,a[pre].l,l,m,v);
		else upd(rx,a[pre].r,m+1,r,v);
	}
	int que(int k1,int k2,int l,int r,int k){
		if(a[k2].s-a[k1].s<=k) return 0;
		if(l==r) return l;
		int m=l+r>>1;
		if(a[a[k2].l].s-a[a[k1].l].s>k) return que(a[k1].l,a[k2].l,l,m,k);
		else return que(a[k1].r,a[k2].r,m+1,r,k);
	}
}T;
int a[N],b[N];
void solve(){
	//think twice code once
	int n,m,q;scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),b[i]=a[i];
	sort(b+1,b+n+1);
	m=unique(b+1,b+n+1)-b-1;
	for(int i=1;i<=n;i++){
		int x=lower_bound(b+1,b+m+1,a[i])-b;
		T.upd(rt[i],rt[i-1],1,m,x);
	}
	while(q--){
		int l,r;scanf("%d%d",&l,&r);
		int k=(r-l+1)>>1;
		printf("%d\n",b[T.que(rt[l-1],rt[r],1,m,k)]);
	}
}
int main(){
	solve();
	return 0;
}
P3939 数颜色

题意

区间查询某个数出现次数,相连数交换。

思路

主席树,相邻数交换,第 x + 1 x+1 x+1版本不用修改,第 x x x版本先删去 a [ x ] a[x] a[x],然后再加上 a [ x + 1 ] a[x+1] a[x+1]即可。

code

// Problem: P3939 数颜色
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3939
// Memory Limit: 250 MB
// Time Limit: 1000 ms
// Date: 2021-04-04 12:50:36
// --------by Herio--------

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull; 
const int N=3e5+5,M=2e4+5,inf=0x3f3f3f3f,mod=1e9+7;
#define mst(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define fi first
#define se second
#define pb emplace_back
#define SZ(a) (int)a.size()
#define IOS ios::sync_with_stdio(false),cin.tie(0) 
void Print(int *a,int n){
	for(int i=1;i<n;i++)
		printf("%d ",a[i]);
	printf("%d\n",a[n]); 
}
int rt[N*80],ls[N*80],rs[N*80],s[N*80],cnt;
int a[N];
void upd(int &x,int y,int l,int r,int k,int v){
		x=++cnt,ls[x]=ls[y],rs[x]=rs[y],s[x]=s[y]+v;
		if(l==r) return;
		int m=l+r>>1;
		if(k<=m) upd(ls[x],ls[y],l,m,k,v);
		else upd(rs[x],rs[y],m+1,r,k,v);
}
int que(int x,int y,int l,int r,int k){
		if(l==r) return s[y]-s[x];
		int m=l+r>>1;
		if(k<=m) return que(ls[x],ls[y],l,m,k);
		else return que(rs[x],rs[y],m+1,r,k);
}
void solve(){
	//think twice code once
	int n,q;
	scanf("%d%d",&n,&q);
	int mx=3e5;
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		upd(rt[i],rt[i-1],1,mx,a[i],1);
	}
	while(q--){
		int op;
		scanf("%d",&op);
		if(op==1){
			int l,r,c;
			scanf("%d%d%d",&l,&r,&c);
			printf("%d\n",que(rt[l-1],rt[r],1,mx,c));
		}
		else {
			int x;scanf("%d",&x);
			upd(rt[x],rt[x],1,mx,a[x],-1);
			upd(rt[x],rt[x],1,mx,a[x+1],1);
			swap(a[x],a[x+1]);
		}
	}
	return;
}
int main(){
	solve();
	return 0;
}

2021ICPC昆明-M-Stone Games

题意

求区间最小不能表示的数,对于一个区间每个数至多使用一次。

思路

主席树,考虑一个问题,对于假设当前能表示的范围是: [ 1 , x ] [1,x] [1,x],我们求出所有数范围在 [ 1 , x + 1 ] [1,x+1] [1,x+1]的和 s u m sum sum,则当前能表示的范围是 [ 1 , s u m ] [1,sum] [1,sum]

为什么 [ 1 , x + 1 ] [1,x+1] [1,x+1]的右端点是 x + 1 x+1 x+1呢?

假设当前只加上一个数 a a a

则利用该 a a a,可表示的范围是: [ a + 1 , x + a ] [a+1,x+a] [a+1,x+a]

a + 1 ≤ x + 1 a+1 \le x+1 a+1x+1,都是满足 [ 1 , x + a ] [1,x+a] [1,x+a]的。

a ≤ x a\le x ax满足。

a = x + 1 a=x+1 a=x+1也满足,因为 a = x + 1 a=x+1 a=x+1可表示自身。

这样范围就是 [ 1 , x ] [ x + 1 , x + 1 ] , [ x + 2 , x + a ] = [ 1 , x + a ] [1,x][x+1,x+1],[x+2,x+a]=[1,x+a] [1,x][x+1,x+1],[x+2,x+a]=[1,x+a]

a > x + 1 a>x+1 a>x+1时 表示不了 x + 1 x+1 x+1。所以答案就是 x + 1 x+1 x+1了,否则令 x = x + a x=x+a x=x+a

然后利用主席树,进行区间求和 [ 1 , x ] [1,x] [1,x]。为什么要用主席树呢,因为要表示权值 [ 1 , 1 e 9 ] [1,1e9] [1,1e9]范围,动态开点避免爆内存。

注意到每次更新 x x x x x x至少增加一倍。所以复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)

code

// Problem: Stone Games
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/12548/M
// Memory Limit: 2097152 MB
// Time Limit: 8000 ms
// Date: 2021-04-04 14:56:37
// --------by Herio--------

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull; 
const int N=1e6+5,M=2e4+5,inf=0x3f3f3f3f,mod=1e9+7;
#define mst(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define fi first
#define se second
#define pb emplace_back
#define SZ(a) (int)a.size()
#define IOS ios::sync_with_stdio(false),cin.tie(0) 
void Print(int *a,int n){
	for(int i=1;i<n;i++)
		printf("%d ",a[i]);
	printf("%d\n",a[n]); 
}
int n,q,mx=1e9;
ll lans;
int ls[N*32],rs[N*32],rt[N*32],cnt;
ll s[N*32];
void upd(int x,int &y,int l,int r,int v){
	y=++cnt,ls[y]=ls[x],rs[y]=rs[x],s[y]=s[x]+v;
	if(l==r) return;
	int m=l+r>>1;
	if(v<=m) upd(ls[x],ls[y],l,m,v);
	else upd(rs[x],rs[y],m+1,r,v);
}
ll que(int x,int y,int l,int r,int ql,int qr){
	if(l>=ql&&r<=qr) return s[y]-s[x];
	if(l>r) return 0;
	ll ans=0;
	int m=l+r>>1;
	if(ql<=m) ans+=que(ls[x],ls[y],l,m,ql,qr);
	if(qr>m) ans+=que(rs[x],rs[y],m+1,r,ql,qr);
	return ans;  
}
void solve(){
	//think twice code once
	scanf("%d%d",&n,&q);
	for(int i=1,x;i<=n;i++){
		scanf("%d",&x);
		upd(rt[i-1],rt[i],1,mx,x);
	}
	while(q--){
		int l,r;
		scanf("%d%d",&l,&r);
		l=(l+lans)%n+1,r=(r+lans)%n+1;
		if(l>r) swap(l,r);
		ll ans=0;
		while(1){
			ll s=que(rt[l-1],rt[r],1,mx,1,min(1LL*mx,ans+1));
			if(s<=ans) break;
			ans=s;
		}
		lans=ans+1;
		printf("%lld\n",lans);
	}
}
int main(){
	solve();
	return 0;
}
P4137 Rmq Problem / mex

区间MEX问题

思路

主席树,需要记录一下每个数最后出现的位置,然后区间维护每个数最后出现的位置的最小值。最后查询的时候就查询位置小于 l l l的最小权值,注意到只有 n n n个数,所以答案不会大于 n n n,对于大于 n n n的数直接 r t [ i ] = r t [ i − 1 ] rt[i]=rt[i-1] rt[i]=rt[i1]即可,因为有 0 0 0存在,为了方便用权值线段树储存,我们就都先加1,范围就是 [ 1 , n + 1 ] [1,n+1] [1,n+1],然后最后答案再减1即可。

code

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull; 
const int N=2e5+5,M=2e4+5,inf=0x3f3f3f3f,mod=1e9+7;
#define mst(a,b) memset(a,b,sizeof a)
#define PII pair<int,int>
#define fi first
#define se second
#define pb emplace_back
#define SZ(a) (int)a.size()
#define IOS ios::sync_with_stdio(false),cin.tie(0) 
void Print(int *a,int n){
	for(int i=1;i<n;i++)
		printf("%d ",a[i]);
	printf("%d\n",a[n]); 
}
int cnt,ls[N*20],rs[N*20],s[N*20],rt[N*20];
void upd(int x,int &y,int l,int r,int p,int v){
	y=++cnt,ls[y]=ls[x],rs[y]=rs[x];
	if(l==r) {s[y]=v;return;}
	int m=l+r>>1;
	if(p<=m) upd(ls[x],ls[y],l,m,p,v);
	else upd(rs[x],rs[y],m+1,r,p,v);
	s[y]=min(s[ls[y]],s[rs[y]]);
}
int que(int x,int ql,int l,int r){
	if(l==r) return l;
	int m=l+r>>1;
	if(s[ls[x]]<ql) return que(ls[x],ql,l,m);
	else return que(rs[x],ql,m+1,r);
}
void solve(){
	//think twice code once
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1,x;i<=n;i++){
		scanf("%d",&x);
		x++;
		if(x>n) rt[i]=rt[i-1];
		else upd(rt[i-1],rt[i],1,n+1,x,i); 
	}
	while(m--){
		int l,r;scanf("%d%d",&l,&r);
		printf("%d\n",que(rt[r],l,1,n+1)-1);
	}
}
int main(){
	solve();
	return 0;
}