变种线段树 提高篇
可持久化线段树
注意,它的全称为可持久化权值线段树。
例题 \(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;
}