变种线段树 提高篇
可持久化线段树
注意,它的全称为可持久化权值线段树。
例题 :可持久化线段树
首先我们考虑几个暴力:
对于每次询问,找出区间中的所有数,直接排序求第
对于每次询问,建出一棵权值线段树,然后权值线段树上二分查找即可。
发现瓶颈在于建树,如果忽略建树则复杂度为
我们把每次插入一个数后看做一个历史版本,那么区间
考虑每次插入一个数改变了什么,改变了一条链上的信息,链最长不过树高,树高为
查询的时候,因为做了前缀和,所以我们考虑左子树内的数的数量是否不小于当前的
代码:
#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;
}
例题 :美味
我们考虑一个按位贪心。先分类讨论一下,这里假设当前最优的答案为
-
的第 位为 :最好使 的第 位为 ,故最优区间 。 -
的第 位为 :最好使 的第 位为 ,故最优区间 。
于是我们使用主席树快速查找这个区间内有没有数即可。
代码:
#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;
}
线段树分治
线段树分治是一种按照时间分治的技巧,适用于支持插入,不支持删除的数据结构的维护。我们可以使用线段树分治将其转化为撤销操作。
考虑如何撤销。使用一个栈记录修改操作,撤销时弹栈回退即可。
我们使用线段树维护时间轴,对于每个操作离线后得到它影响的时间段
接下来我们对整棵线段树进行中序遍历。如果这一步是向儿子走的,则代表我们计入该子节点的操作带来的贡献;如果这一步是向父亲走的,则代表我们撤销当前节点的所有操作。
那么,对于时刻
例题 :线段树分治
首先使用拓展域并查集判定二分图,这里要使用可撤销并查集,所以不能路径压缩,需要使用按秩合并。
代码:
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】