变种线段树 提高篇

可持久化线段树

注意,它的全称为可持久化权值线段树

例题 \(1\)可持久化线段树

首先我们考虑几个暴力:

对于每次询问,找出区间中的所有数,直接排序求第 \(k\) 小。这样做的时间复杂度为 \(O(nq\log n)\) 的。

对于每次询问,建出一棵权值线段树,然后权值线段树上二分查找即可。

发现瓶颈在于建树,如果忽略建树则复杂度为 \(O(q\log n)\) 的。

我们把每次插入一个数后看做一个历史版本,那么区间 \([l,r]\) 内的数就是 \(r\) 历史版本与 \(l-1\) 历史版本做差。这就是我们的询问。

考虑每次插入一个数改变了什么,改变了一条链上的信息,链最长不过树高,树高为 \(\log n\),所以每次插入一个数是 \(O(\log n)\) 的,建树总时间复杂度 \(O(n\log n)\)

查询的时候,因为做了前缀和,所以我们考虑左子树内的数的数量是否不小于当前的\(k\),如果是,递归左子树;否则递归右子树,同时 \(k\) 减去左子树内数的数量,查询最后会返回这个被离散化后的值,还原一下即可。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,m,a[N],b[N],rt[N];
struct dsgt{
	struct node{
		int l,r,v;
	}tr[N<<5];
	int cnt=0;
	int build(int L,int R){
		int u=++cnt;
		tr[u]={0,0,0};
		if(L<R){
			int mid=L+R>>1;
			tr[u].l=build(L,mid);//动态开点的建树方法 
			tr[u].r=build(mid+1,R);
		}
		return u;
	}
	int modify(int las,int L,int R,int x){
		int u=++cnt;
		tr[u]={tr[las].l,tr[las].r,tr[las].v+1};//继承上一个历史版本,x一定在改的东西代表的区间内 
		if(L<R){
			int mid=L+R>>1;
			if(x<=mid)tr[u].l=modify(tr[las].l,L,mid,x);//这里保证改的是一条链,同时所有区间包含x 
			else tr[u].r=modify(tr[las].r,mid+1,R,x);
		}
		return u;
	}
	int qry(int las,int now,int L,int R,int k){
		if(L==R)return L;//返回离散化后的值(排名) 
		int mid=L+R>>1;
		int num=tr[tr[now].l].v-tr[tr[las].l].v;
		if(num>=k)return qry(tr[las].l,tr[now].l,L,mid,k);//分讨在哪个子树内 
		else return qry(tr[las].r,tr[now].r,mid+1,R,k-num);
	}
	//可持久化线段树需要使用动态开点,否则空间会炸
	//注意全名叫可持久化“权值”线段树 
}dsgt;
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		b[i]=a[i];
	}
	sort(b+1,b+n+1);
	int cnt=unique(b+1,b+n+1)-b-1;//离散化 
	rt[0]=dsgt.build(1,cnt);//建出空版本 
	for(int i=1;i<=n;i++){
		int tmp=lower_bound(b+1,b+cnt+1,a[i])-b; 
		rt[i]=dsgt.modify(rt[i-1],1,cnt,tmp);//依次插入新数 
	}
	while(m--){
		int l,r,k;
		cin>>l>>r>>k;
		int res=dsgt.qry(rt[l-1],rt[r],1,cnt,k);//询问[l,r]相当于两个历史版本做差 
		cout<<b[res]<<'\n';//还原会最初的值 
	}
	return 0;
}

例题 \(2\)美味

我们考虑一个按位贪心。先分类讨论一下,这里假设当前最优的答案为 \(ans\)

  • \(b\) 的第 \(j\) 位为 \(0\):最好使 \(a\) 的第 \(j\) 位为 \(1\),故最优区间 \([ans+2^j,ans+2^{j+1}-1]\)

  • \(b\) 的第 \(j\) 位为 \(1\):最好使 \(a\) 的第 \(j\) 位为 \(0\),故最优区间 \([ans,ans+2^j-1]\)

于是我们使用主席树快速查找这个区间内有没有数即可。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 500005
using namespace std;
int n,q,a[N],rt[N];
struct dsgt{
	struct node{
		int l,r,v;
	}tr[N<<5];
	int cnt=0;
	void modify(int las,int &now,int l,int r,int x){
		if(l>x||r<x)return;
		now=++cnt;
		tr[now]={tr[las].l,tr[las].r,tr[las].v+1};
		if(l==r)return;
		int mid=l+r>>1;
		modify(tr[las].l,tr[now].l,l,mid,x);
		modify(tr[las].r,tr[now].r,mid+1,r,x);
	}
	int qry(int las,int now,int l,int r,int L,int R){
		int num=tr[now].v-tr[las].v;
		int mid=l+r>>1;
		if(l>=L&&r<=R)return num;
		if(l>R||r<L||num==0)return 0;
		return qry(tr[las].l,tr[now].l,l,mid,L,R)+qry(tr[las].r,tr[now].r,mid+1,r,L,R);
	}
}dsgt;
signed main(){
	cin>>n>>q;
	int mx=0;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		mx=max(mx,a[i]);
	}
	for(int i=1;i<=n;i++){
		dsgt.modify(rt[i-1],rt[i],0,mx,a[i]);
	}
	while(q--){
		int b,x,l,r;
		cin>>b>>x>>l>>r;
		int res=0;
		for(int i=18;~i;i--){
			if(b>>i&1){
				if(dsgt.qry(rt[l-1],rt[r],0,mx,res-x,res-x+(1<<i)-1)==0)res+=1<<i;
			}
			else{
				if(dsgt.qry(rt[l-1],rt[r],0,mx,res-x+(1<<i),res-x+(1<<i+1)-1)!=0)res+=1<<i;
			}
		}
		cout<<(res^b)<<'\n';
	}
	return 0;
}

线段树分治

线段树分治是一种按照时间分治的技巧,适用于支持插入,不支持删除的数据结构的维护。我们可以使用线段树分治将其转化为撤销操作。

考虑如何撤销。使用一个栈记录修改操作,撤销时弹栈回退即可。

我们使用线段树维护时间轴,对于每个操作离线后得到它影响的时间段 \([l,r]\),然后在线段树上找到代表的区间被它完全包含的位置加入这个操作。

接下来我们对整棵线段树进行中序遍历。如果这一步是向儿子走的,则代表我们计入该子节点的操作带来的贡献;如果这一步是向父亲走的,则代表我们撤销当前节点的所有操作。

那么,对于时刻 \(t\) 的答案,就是从根走到代表区间 \([t,t]\) 的叶子节点的答案。

例题 \(1\)线段树分治

首先使用拓展域并查集判定二分图,这里要使用可撤销并查集,所以不能路径压缩,需要使用按秩合并。

代码:

#include<bits/stdc++.h>
#define int long long
#define N 100005
#define pii pair<int,int>
#define x first
#define y second
using namespace std;
int n,m,k,fa[N<<1],siz[N<<1],chk[N<<2];
vector<pii>tr[N<<2];
int find(int x){
	if(fa[x]!=x)return find(fa[x]);//不能使用路径压缩 
	return fa[x];
}
void modify(int u,int l,int r,int L,int R,pii v){
	if(l>=L&&r<=R){
		tr[u].push_back(v);//如果当前操作的时间完全包含这个点的时间,就加入这个操作 
		return;
	}
	int mid=l+r>>1;
	if(L<=mid)modify(u<<1,l,mid,L,R,v);
	if(R>mid)modify(u<<1|1,mid+1,r,L,R,v);
}
void undo(vector<pii>&x){
	for(auto it:x){
		siz[it.x]-=siz[it.y];//撤销 
		fa[it.y]=it.y;
	}
}
void qry(int x,int l,int r){
	vector<pii>s;
	if(chk[x]){//如果可能可行 
		for(auto it:tr[x]){
			int u=it.x,v=it.y,uu=u+n,vv=v+n;
			u=find(u);v=find(v);uu=find(uu);vv=find(vv);
			if(u==v){//如果在一部里还有边,就不是二分图 
				chk[x]=0;
				break;
			}
			if(siz[u]<siz[vv])swap(u,vv);
			if(siz[v]<siz[uu])swap(v,uu);//按秩合并 
			s.push_back({u,vv});
			s.push_back({v,uu});//用栈记录操作,用于可撤销并查集 
			siz[u]+=siz[vv];fa[vv]=u;
			siz[v]+=siz[uu];fa[uu]=v;
		}
	}
	if(l==r){//到达叶子 
		cout<<(chk[x]?"Yes\n":"No\n");
		undo(s);//撤销 
		return;
	}
	chk[x<<1]=chk[x<<1|1]=chk[x];//如果父亲可行,还有可能可行;否则一定不可行 
	int mid=l+r>>1;
	qry(x<<1,l,mid);
	qry(x<<1|1,mid+1,r);
	undo(s);//递归后撤销 
}
signed main(){
	cin>>n>>m>>k;
	chk[1]=1;//初始可行 
	for(int i=1;i<=n<<1;i++){
		fa[i]=i;
		siz[i]=1;
	}
	for(int i=1;i<=m;i++){
		int x,y,l,r;
		cin>>x>>y>>l>>r;
		if(l==r)continue;//同时出现和消失相当于没有出现 
		modify(1,0,k-1,l,r-1,{x,y});//因为r-1<k,所以只需要到k-1 
	}
	qry(1,0,k-1);//这里同理,到k-1即可 
	return 0;
}
posted @ 2024-09-06 15:48  zxh923  阅读(8)  评论(0编辑  收藏  举报