线段树复习
线段树复习
线段树合并
线段树合并是在权值线段数上操作的,类似于主席树,一般会在每一个点开一个权值线段树,然后统计答案的时候合并线段树。
\(1.雨天的尾巴\)
题意:首先村落里的一共有 \(n\) 座房屋,并形成一个树状结构。然后救济粮分 \(m\) 次发放,每次选择两个房屋 \((x,y)\)然后对于 \(x\) 到 \(y\) 的路径上(含 \(x\) 和 \(y\))每座房子里发放一袋 \(z\) 类型的救济粮。
然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。
Sol:每个节点建一个权值线段树,然后对于一次救济粮的发放,利用树上差分的思想,在\(u,v\)处\(+1\),在\(LCA(u,v)\)和\(fa[LCA(u,v)]\)处\(-1\),最后自低向上合并线段数计算答案即可。权值线段树一般会维护一个数的个数\(cnt\)量,在判断大小的时候基本上通过\(cnt\)去比较。
#include <bits/stdc++.h>
#define sc(x) scanf("%d",&x)
#define ll long long
#define ls rt<<1
#define rs rt<<1|1
#define endl '\n'
#define pb push_back
using namespace std;
const int N=1e5+10,M=5e5+10;
const int inf=1e5;
struct node
{
int l,r;
int cnt;
}tr[N*50];
int fa[N][21],dep[N],head[N],tot,idx,root[N],ans[N];
struct edges
{
int v,nxt;
}e[N*2];
void add(int u,int v)
{
e[tot]={v,head[u]},head[u]=tot++;
}
void dfs(int u,int f,int step)
{
dep[u]=step;
for(int i=1;i<=20;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=head[u];~i;i=e[i].nxt)
{
int v=e[i].v;
if(v==f) continue;
fa[v][0]=u;
dfs(v,u,step+1);
}
}
int LCA(int u,int v)
{
if(dep[u]<dep[v]) swap(u,v);
for(int i=20;i>=0;i--)
if(dep[fa[u][i]]>=dep[v]) u=fa[u][i];
if(u==v) return u;
for(int i=20;i>=0;i--)
{
if(fa[u][i]!=fa[v][i])
u=fa[u][i],v=fa[v][i];
}
return fa[u][0];
}
void update(int &rt,int l,int r,int p,int k)
{
if(!rt) rt=++idx;
if(l==r)
{
tr[rt].cnt+=k;
return;
}
int mid=l+r>>1;
if(p<=mid) update(tr[rt].l,l,mid,p,k);
else update(tr[rt].r,mid+1,r,p,k);
tr[rt].cnt=max(tr[tr[rt].l].cnt,tr[tr[rt].r].cnt);
}
int merge(int u ,int v) //把v合并到u上
{
if(!u||!v) return u|v;
tr[u].l=merge(tr[u].l,tr[v].l);
tr[u].r=merge(tr[u].r,tr[v].r);
if(!tr[u].l&&!tr[u].r) //如果是叶子节点,直接合并节点上的信息
{
tr[u].cnt+=tr[v].cnt;
return u;
}
tr[u].cnt=max(tr[tr[u].l].cnt,tr[tr[u].r].cnt);
return u;
}
int query(int rt,int l,int r)
{
if(l==r) return l;
int mid=l+r>>1;
if(tr[tr[rt].l].cnt>=tr[tr[rt].r].cnt) return query(tr[rt].l,l,mid);
else return query(tr[rt].r,mid+1,r);
}
void DFS(int u,int f)
{
for(int i=head[u];~i;i=e[i].nxt)
{
int v=e[i].v;
if(v==f) continue;
DFS(v,u);
root[u]=merge(root[u],root[v]);
}
if(tr[root[u]].cnt==0) ans[u]=0;
else ans[u]=query(root[u],1,inf);
}
int main()
{
memset(head,-1,sizeof head);
int n,m;
sc(n),sc(m);;
for(int i=1;i<n;i++)
{
int u,v;
sc(u),sc(v);
add(u,v),add(v,u);
}
dfs(1,0,1);
for(int i=1;i<=m;i++)
{
int x,y,z;
sc(x),sc(y),sc(z);
int lca=LCA(x,y);
//cout<<lca<<endl;
update(root[x],1,inf,z,1),update(root[y],1,inf,z,1);
update(root[lca],1,inf,z,-1),update(root[fa[lca][0]],1,inf,z,-1);
}
DFS(1,0);
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
return 0;
}
\(2.永无乡\)
题意:永无乡包含$ n$ 座岛,编号从 \(1\) 到 \(n\) ,每座岛都有自己的独一无二的重要度,按照重要度可以将这 \(n\) 座岛排名,名次用 \(1\) 到 \(n\) 来表示。某些岛之间由巨大的桥连接,通过桥可以从一个岛到达另一个岛。如果从岛 \(a\) 出发经过若干座(含 \(0\) 座)桥可以 到达岛 \(b\) ,则称岛 \(a\) 和岛 \(b\) 是连通的。
现在有两种操作:
B x y
表示在岛 \(x\) 与岛 \(y\) 之间修建一座新桥。
Q x k
表示询问当前与岛 \(x\) 连通的所有岛中第 \(k\) 重要的是哪座岛,即所有与岛 \(x\) 连通的岛中重要度排名第 \(k\) 小的岛是哪座,请你输出那个岛的编号。
Sol:每个岛建一个权值线段树,然后用并查集维护联通性,如果加入的边的两个岛不在一个集合中,合并权值线段树,查询就查询\(x\)所在连通块的权值线段树即可。
#include <bits/stdc++.h>
#define sc(x) scanf("%d",&x)
#define ll long long
#define ls rt<<1
#define rs rt<<1|1
#define endl '\n'
#define pb push_back
using namespace std;
const int N=1e5+10,M=5e5+10;
const int inf=1e5;
int w[N],rev[N],root[N],idx;
int fa[N];
int find(int x)
{
return x==fa[x]?x:fa[x]=find(fa[x]);
}
struct node
{
int l,r;
int cnt;
}tr[N*50];
void update(int &rt,int l,int r,int p)
{
if(!rt) rt=++idx;
tr[rt].cnt++;
if(l==r) return ;
int mid=l+r>>1;
if(p<=mid) update(tr[rt].l,l,mid,p);
else update(tr[rt].r,mid+1,r,p);
}
int merge(int u,int v)
{
if(!u||!v) return u|v;
tr[u].l=merge(tr[u].l,tr[v].l);
tr[u].r=merge(tr[u].r,tr[v].r);
if(!tr[u].l&&!tr[u].r)
{
tr[u].cnt+=tr[v].cnt;
return u;
}
tr[u].cnt=tr[tr[u].l].cnt+tr[tr[u].r].cnt;
return u;
}
int query(int rt,int l,int r,int k)
{
if(l==r) return l;
int mid=l+r>>1;
if(tr[tr[rt].l].cnt>=k) return query(tr[rt].l,l,mid,k);
else return query(tr[rt].r,mid+1,r,k-tr[tr[rt].l].cnt);
}
int main()
{
int n,m;
sc(n),sc(m);
for(int i=1;i<=n;i++) fa[i]=i,sc(w[i]),rev[w[i]]=i;
for(int i=1;i<=n;i++) update(root[i],1,n,w[i]);
for(int i=1;i<=m;i++)
{
int u,v;
sc(u),sc(v);
int x=find(u),y=find(v);
if(x!=y)
{
root[y]=merge(root[y],root[x]);
fa[x]=y;
}
}
int q;
sc(q);
while(q--)
{
char op;
int x,y;
cin>>op;
sc(x),sc(y);
if(op=='B')
{
int fx=find(x),fy=find(y);
if(fx!=fy)
{
merge(root[fy],root[fx]);
fa[fx]=fy;
}
}
else
{
x=find(x);
if(tr[root[x]].cnt<y) puts("-1");
else printf("%d\n",rev[query(root[x],1,n,y)]);
}
}
return 0;
}
线段树分裂
\(1.线段树分裂\)
题意:给出一个可重集 \(a\)(编号为 \(1\)),它支持以下操作:
0 p x y
:将可重集 \(p\) 中大于等于 \(x\) 且小于等于 \(y\) 的值放入一个新的可重集中(新可重集编号为从 \(2\) 开始的正整数,是上一次产生的新可重集的编号+1)。
1 p t
:将可重集\(t\) 中的数放入可重集 \(p\),且清空可重集 \(t\)(数据保证在此后的操作中不会出现可重集 \(t\))。
2 p x q
:在 p 这个可重集中加入 \(x\) 个数字 \(q\)。
3 p x y
:查询可重集 p 中大于等于 \(x\) 且小于等于 \(y\) 的值的个数。
4 p k
:查询在 \(p\) 这个可重集中第 \(k\) 小的数,不存在时输出 -1
。
Sol:操作0利用线段树分裂,把所在区间的信息新存在一个节点分裂出来然后清零原来的节点。操作1线段树合并。
#include <bits/stdc++.h>
#define sc(x) scanf("%d",&x)
#define ll long long
#define ls tr[rt].l
#define rs tr[rt].r
#define endl '\n'
#define pb push_back
using namespace std;
const int N=2e5+10;
int root[N],idx,w[N],cnt;
struct node
{
int l,r;
ll v;
}tr[N*100];
void pushup(int rt)
{
tr[rt].v=tr[ls].v+tr[rs].v;
}
void build(int &rt,int l,int r)
{
if(!rt) rt=++idx;
if(l==r)
{
tr[rt].v+=w[l];
return ;
}
int mid=l+r>>1;
build(tr[rt].l,l,mid);
build(tr[rt].r,mid+1,r);
pushup(rt);
}
void update(int &rt,int l,int r,int p,int x)
{
if(!rt) rt=++idx;
if(l==r)
{
tr[rt].v+=x;
return;
}
int mid=l+r>>1;
if(p<=mid) update(ls,l,mid,p,x);
else update(rs,mid+1,r,p,x);
pushup(rt);
}
int merge(int u,int v)
{
if(!u||!v) return u|v;
tr[u].l=merge(tr[u].l,tr[v].l);
tr[u].r=merge(tr[u].r,tr[v].r);
if(!tr[u].l&&!tr[u].r)
{
tr[u].v+=tr[v].v;
return u;
}
pushup(u);
return u;
}
int split(int p,int L,int R,int l,int r)
{
int rt=++idx;
if(l<=L&&R<=r)
{
tr[rt]=tr[p];
tr[p].l=tr[p].r=tr[p].v=0;
return rt;
}
int mid=L+R>>1;
if(l<=mid) ls=split(tr[p].l,L,mid,l,r);
if(r>mid) rs=split(tr[p].r, mid+1,R,l,r);
pushup(rt),pushup(p);
return rt;
}
ll query1(int rt,int L,int R,int l,int r)
{
if(l<=L&&R<=r) return tr[rt].v;
int mid=L+R>>1;
ll res=0;
if(l<=mid) res+=query1(ls,L,mid,l,r);
if(r>mid) res+=query1(rs,mid+1,R,l,r);
return res;
}
int query2(int rt,int l,int r,int k)
{
if(l==r) return l;
int mid=l+r>>1;
if(tr[ls].v>=k) return query2(ls,l,mid,k);
else return query2(rs,mid+1,r,k-tr[ls].v);
}
int main()
{
int n,m;
sc(n),sc(m);
for(int i=1;i<=n;i++) sc(w[i]);
build(root[++cnt],1,n);
while(m--)
{
int op;
sc(op);
if(op==0)
{
int p,x,y;
sc(p),sc(x),sc(y);
root[++cnt]=split(root[p],1,n,x,y);
}
else if(op==1)
{
int p,t;
sc(p),sc(t);
root[p]=merge(root[p],root[t]);
}
else if(op==2)
{
int p,x,q;
sc(p),sc(x),sc(q);
update(root[p],1,n,q,x);
}
else if(op==3)
{
int p,x,y;
sc(p),sc(x),sc(y);
printf("%lld\n",query1(root[p],1,n,x,y));
}
else
{
int p,k;
sc(p),sc(k);
if(tr[root[p]].v<k) puts("-1");
else printf("%d\n",query2(root[p],1,n,k));
}
}
return 0;
}
区间多次排序问题
\(1.ABC237G\)
题意:给定一个长度为\(N\)的排列\(P\)和一个整数\(X\),有\(Q\)次操作,每次操作有三个参数\((C_i,L_i,R_i)\),如果\(G_i=1\),那么就把\([L_i,R_i]\)区间内的数升序排序,反之降序排序,询问最后序列中等于\(X\)的数的位置。
Sol:这种问题一般是将序列中的数和\(X\)比较大小后赋值为\(0\)或\(1\)。在本题中建立两个线段树,第一个线段树中,如果\(P_i\)小于\(X\)赋值为\(0\),大于等于\(X\)赋值为\(1\);在第二个线段树中,如果\(P_i\)小于等于\(X\)赋值为\(0\),大于\(X\)赋值为\(1\)。这样每次操作都对两个线段树操作,最后两个线段树中不同的位置就是\(X\)的位置。具体做法就是区间修改,每次把一段区间修改为\(0\)或\(1\),记录区间中\(1\)的个数,涉及区间修改,要打tag。
#include <bits/stdc++.h>
#define ls rt<<1
#define rs rt<<1|1
using namespace std;
const int N=5e5+10;
struct Segment_Tree
{
int l,r;
int tag,sum;
}tr1[N*4],tr2[N*4];
int a[N];
void pushup(Segment_Tree tr[],int rt)
{
tr[rt].sum=tr[ls].sum+tr[rs].sum;
}
void build(Segment_Tree tr[],int rt,int l,int r,int x)
{
tr[rt].l=l,tr[rt].r=r,tr[rt].tag=-1;
if(l==r)
{
if(a[l]>=x) tr[rt].sum=1;
return;
}
int mid=l+r>>1;
build(tr,ls,l,mid,x);
build(tr,rs,mid+1,r,x);
pushup(tr,rt);
}
void pushdown(Segment_Tree tr[],int rt)
{
int &tag=tr[rt].tag;
if(tag!=-1)
{
tr[ls].sum=(tr[ls].r-tr[ls].l+1)*tag;
tr[rs].sum=(tr[rs].r-tr[rs].l+1)*tag;
tr[ls].tag=tr[rs].tag=tag;
tag=-1;
}
}
void update(Segment_Tree tr[],int rt,int l,int r,int x)
{
int L=tr[rt].l,R=tr[rt].r;
if(l<=L&&R<=r)
{
tr[rt].sum=(R-L+1)*x;
tr[rt].tag=x;
return;
}
pushdown(tr,rt);
int mid=L+R>>1;
if(l<=mid) update(tr,ls,l,r,x);
if(r>mid) update(tr,rs,l,r,x);
pushup(tr,rt);
}
int query(Segment_Tree tr[],int rt,int l,int r)
{
int L=tr[rt].l,R=tr[rt].r;
int res=0;
if(l<=L&&R<=r)
return tr[rt].sum;
pushdown(tr,rt);
int mid=L+R>>1;
if(l<=mid) res+=query(tr,ls,l,r);
if(r>mid) res+=query(tr,rs,l,r);
return res;
}
void sort(Segment_Tree tr[],int c,int l,int r)
{
int s=query(tr,1,l,r);
if(c==1)//降序
{
update(tr,1,l,r-s,0);
update(tr,1,r-s+1,r,1);
}else
{
update(tr,1,l,l+s-1,1);
update(tr,1,l+s,r,0);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n,q,x;
cin>>n>>q>>x;
for(int i=1;i<=n;i++) cin>>a[i];
build(tr1,1,1,n,x),build(tr2,1,1,n,x+1);
while(q--)
{
int c,l,r;
cin>>c>>l>>r;
sort(tr1,c,l,r),sort(tr2,c,l,r);
}
for(int i=1;i<=n;i++)
if(query(tr1,1,i,i)!=query(tr2,1,i,i))
{
cout<<i<<'\n';
return 0;
}
}
\(2.[HEOI2016/TJOI2016]排序\)
题意:在 \(2016\)年,佳媛姐姐喜欢上了数字序列。因而她经常研究关于序列的一些奇奇怪怪的问题,现在她在研究一个难题,需要你来帮助她。
这个难题是这样子的:给出一个 \(1\)到 \(n\)的排列,现在对这个排列序列进行 \(m\)次局部排序,排序分为两种:
0 l r
表示将区间 \([l,r]\) 的数字升序排序1 l r
表示将区间 \([l,r]\)的数字降序排序
注意,这里是对下标在区间 \([l,r]\)内的数排序。
最后询问第 \(q\) 位置上的数字。
Sol:
-
做法1:离线查询,二分答案。具体就是二分\(q\)位上的答案是什么,假设位\(mid\),那么现在问题就和上面那个题是一样的,但此时我们不需要开两个线段树,开一个和第一个线段树一样的,那么最后判断就是如果第\(p\)位为\(0\),说明\(mid\)就大了,否则\(mid\)就是小了。
#include <bits/stdc++.h> #define sc(x) scanf("%d",&x) #define ll long long #define ls rt<<1 #define rs rt<<1|1 #define endl '\n' #define pb push_back using namespace std; const int N=1e5+10; int n,m; int q; struct Q { int op,l,r; }p[N]; struct node { int l,r; int lazy,cnt; }tr[N*4]; int a[N]; void build(int rt,int l,int r,int x) { tr[rt]={l,r}; if(l==r) { tr[rt].cnt=a[l]>=x; tr[rt].lazy=0; return; } int mid=l+r>>1; build(ls,l,mid,x); build(rs,mid+1,r,x); tr[rt].cnt=tr[ls].cnt+tr[rs].cnt; tr[rt].lazy=0; } void pushdown(int rt) { if(tr[rt].lazy) { if(tr[rt].lazy==-1) { tr[ls].cnt=tr[rs].cnt=0; tr[ls].lazy=tr[rs].lazy=-1; tr[rt].lazy=0; } else { tr[ls].cnt=tr[ls].r-tr[ls].l+1; tr[rs].cnt=tr[rs].r-tr[rs].l+1; tr[ls].lazy=tr[rs].lazy=1; tr[rt].lazy=0; } } } void update(int rt,int l,int r,int ql,int qr,int k) { if(ql<=l&&r<=qr) { tr[rt].cnt=k*(r-l+1); tr[rt].lazy=k?1:-1; return; } if(ql>r||qr<l) return ; pushdown(rt); int mid=l+r>>1; if(ql<=mid) update(ls,l,mid,ql,qr,k); if(qr>mid) update(rs,mid+1,r,ql,qr,k); tr[rt].cnt=tr[ls].cnt+tr[rs].cnt; } int query(int rt,int l,int r,int ql,int qr) { if(ql<=l&&r<=qr) { return tr[rt].cnt; } pushdown(rt); int res=0; int mid=l+r>>1; if(ql<=mid) res+=query(ls,l,mid,ql,qr); if(qr>mid) res+=query(rs,mid+1,r,ql,qr); return res; } int find(int rt,int l,int r,int pos) { if(l==r) return tr[rt].cnt; pushdown(rt); int mid=l+r>>1; if(pos<=mid) return find(ls,l,mid,pos); else return find(rs,mid+1,r,pos); } int check(int x) { build(1,1,n,x); for(int i=1;i<=m;i++) { int L=p[i].l,R=p[i].r,op=p[i].op; int c1=query(1,1,n,L,R); if(op==0) { update(1,1,n,R-c1+1,R,1); update(1,1,n,L,R-c1,0); } else { update(1,1,n,L,L+c1-1,1); update(1,1,n,L+c1,R,0); } } return find(1,1,n,q); } int main() { sc(n),sc(m); for(int i=1;i<=n;i++) sc(a[i]); for(int i=1;i<=m;i++) sc(p[i].op),sc(p[i].l),sc(p[i].r); sc(q); int l=1,r=n; int ans=0; while(l<=r) { int mid=l+r>>1; if(check(mid)) ans=mid,l=mid+1; else r=mid-1; } cout<<ans<<endl; return 0; }
-
做法2:可以在线操作,同时可以查询出排序后的所有位置的数是什么。
太牛了,一点都不会,用到线段树分裂和合并。#include <bits/stdc++.h> #define lll __in128 #define pb push_back #define endl '\n' #define sc(x) scanf("%d",&x) #define scl(x) scanf("%lld",&x) #define fi first #define se second #define ls tr[rt].l #define rs tr[rt].r using namespace std; const int N=1e5+10; set<int>s; int root[N*50],idx,cz[N]; struct node { int l,r; int cnt; }tr[N*50]; void insert(int &rt,int l,int r,int p) { if(!rt) rt=++idx; tr[rt].cnt++; if(l==r) return ; int mid=l+r>>1; if(p<=mid) insert(ls,l,mid,p); else insert(rs,mid+1,r,p); } int merge(int u,int v) { if(!u||!v) return u|v; tr[u].cnt+=tr[v].cnt; tr[u].l=merge(tr[u].l,tr[v].l); tr[u].r=merge(tr[u].r,tr[v].r); return u; } void split(int &rt,int p,int k,int op) { if(tr[p].cnt==k) return; rt=++idx; tr[rt].cnt=tr[p].cnt-k; tr[p].cnt=k; if(op) { if(k<=tr[tr[p].r].cnt) split(tr[rt].r,tr[p].r,k,op),tr[rt].l=tr[p].l,tr[p].l=0; else split(tr[rt].l,tr[p].l,k-tr[tr[p].r].cnt,op); } else { if(k<=tr[tr[p].l].cnt) split(tr[rt].l,tr[p].l,k,op),tr[rt].r=tr[p].r,tr[p].r=0; else split(tr[rt].r,tr[p].r,k-tr[tr[p].l].cnt,op); } } auto Split(int p) { auto it=s.lower_bound(p); if(*it==p&&it!=s.end()) return it; --it; split(root[p],root[*it],p-*it,cz[p]=cz[*it]); return s.insert(p).first; } int query(int rt,int l,int r) { if(l==r) return l; int mid=l+r>>1; if(ls) return query(ls,l,mid); else return query(rs,mid+1,r); } int main() { int n,m; sc(n),sc(m); s.insert(n+1); for(int i=1;i<=n;i++) { int x; sc(x); s.insert(i); insert(root[i],0,n,x); } for(int i=1;i<=m;i++) { int op,l,r; sc(op),sc(l),sc(r); auto itl=Split(l),itr=Split(r+1); for(auto it=++itl;it!=itr;++it) root[l]=merge(root[l],root[*it]); cz[l]=op; s.erase(itl,itr); } int q; sc(q); Split(q),Split(q+1); cout<<query(root[q],0,n)<<endl; return 0; }
-