变种线段树 基础篇
权值线段树
线段树在这里作为前置知识,我们就不说了,而且权值线段树也不是核心内容,不会大篇幅讲。
首先,权值线段树在维护什么?维护的是桶。
然后,权值线段树有什么用?可以求一些序列的第 \(k\) 大之类的问题。
于是我们放个板子题。
第 k 小整数
简单题,直接看代码和注释就行,当然也可以使用线性的快速选择算法。
事实上,权值线段树左,右边界为 \([l,r]\) 的节点在这里维护的是区间 \([l,r]\) 内的数的个数(但是要去重)。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 300005
using namespace std;
int n,k,a[N],tr[N<<2];
void pushup(int u){
tr[u]=tr[u<<1]+tr[u<<1|1];
}
void build(int u,int l,int r){
if(l==r){
tr[u]=a[l];//大小为这个元素的桶
return;
}
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
int qry(int u,int l,int r,int k){
if(l==r)return l;
int mid=l+r>>1;
if(tr[u<<1]>=k)return qry(u<<1,l,mid,k);//如果左子树总个数更大,那么答案在左子树里
else return qry(u<<1|1,mid+1,r,k-tr[u<<1]);//否则在右子树里,但是我们需要减掉左子树的大小
}
signed main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
int x;
cin>>x;
if(a[x]==0)a[x]++;//这里多个看成一个,所以只统计一次
}
a[30000]++;//放一下哨兵
build(1,1,30000);
int res=qry(1,1,30000,k);
if(res==30000)cout<<"NO RESULT";
else cout<<res;
return 0;
}
线段树合并
基础概念没啥好说的,就是合并两颗线段树。所以直接看题。
Promotion Counting P
一道裸题。显然权值数量可以合并,所以我们对每个点建立一颗权值线段树,然后跑一遍 \(dfs\),在回溯的时候合并和求答案。
然后我们怎么求答案?显然地,我们查一下权值线段树中权值比他大的数的数量就行了。
直接看代码:
#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,p[N],a[N],res[N],rt[N];
int h[N],e[N],ne[N],idx,cnt;
struct node{
int l,r,sum;
}tr[N<<4];
void add(int a,int b){
e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}
void modify(int &u,int l,int r,int x){
if(l>r)return;
u=++cnt;//动态开点
if(l==r){
tr[u].sum++;
return;
}
int mid=l+r>>1;
if(x<=mid)modify(tr[u].l,l,mid,x);//如果在左区间里,插入到左子树
else modify(tr[u].r,mid+1,r,x);//否则插入到右子树
tr[u].sum=tr[tr[u].l].sum+tr[tr[u].r].sum;//数量就是两边加起来
}
int qry(int u,int l,int r,int x){
if(!u)return 0;
if(l>=x)return tr[u].sum;//如果区间内最小数已经不小于目标值,返回数量
int res=0;
int mid=l+r>>1;
if(mid>=x)res+=qry(tr[u].l,l,mid,x);//如果左子树里面的数可能比目标值大就递归
res+=qry(tr[u].r,mid+1,r,x);
return res;
}
int merge(int x,int y){
if(!x||!y)return x+y;//返回非空的一边
tr[x].l=merge(tr[x].l,tr[y].l);
tr[x].r=merge(tr[x].r,tr[y].r);//合并两棵树
tr[x].sum=tr[tr[x].l].sum+tr[tr[x].r].sum;//数量合并同上
return x;
}
void dfs(int u){
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
dfs(j);
rt[u]=merge(rt[u],rt[j]);//在dfs时合并
}
res[u]=qry(rt[u],1,n,a[u]+1);//查询比这个数大的数的数量
}
signed main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>p[i];
a[i]=p[i];
}
sort(p+1,p+n+1);
int len=unique(p+1,p+n+1)-p-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(p+1,p+len+1,a[i])-p;//离散化
}
memset(h,-1,sizeof h);
for(int i=2;i<=n;i++){
int x;
cin>>x;
add(x,i);
}
for(int i=1;i<=n;i++){
modify(rt[i],1,n,a[i]);//先把每个数开一颗权值线段树
}
dfs(1);
for(int i=1;i<=n;i++){
cout<<res[i]<<'\n';
}
return 0;
}
雨天的尾巴
首先要做这道题的话,需要会树上差分,下面就不讲了。
但是在说这个题之前,先讲点东西。比如先说一下线段树合并是怎么合并的:
-
如果有一棵树的 \(p\) 节点为空,返回另一棵树的 \(p\) 节点。
-
如果合并到了叶子节点,直接把这个点树 \(y\) 的东西加到树 \(x\) 上。
-
递归处理两棵子树。
-
用两棵子树的值更新当前节点,即上传操作。
-
返回这个点。
然后我们考虑对每个点建一棵权值线段树,然后用 \(mx_x\) 记录种类为 \(x\) 的粮食的数量,\(res_{rt}\) 记录第 \(rt\) 个房子中数量最多的粮食的种类编号。
对于每次操作,如果我们暴力修改每个点的权值线段树,显然时间上是不能接受的,所以在这里我们考虑使用树上差分进行修改,具体可以看下面的代码。
然后为了实现树上差分,需要预处理一下 \(lca\),这里用倍增实现。
最后我们的合并和上一题一样,都是边 \(dfs\) 边合并。然后直接看一下代码:
#include<bits/stdc++.h>
#define int long long
#define N 100005
#define M 200005
#define K 23
#define lim 100000
using namespace std;
int n,q,h[N],e[M],ne[M],idx;
int rt[N],cnt,fa[N][K],dep[N];
struct node{
int l,r,mx,res;
}tr[N<<7];
//mx是某一种粮食的数量
void add(int a,int b){
e[idx]=b;ne[idx]=h[a];h[a]=idx++;
}
void pushup(int u){
if(tr[tr[u].l].mx>=tr[tr[u].r].mx){//如果这一种的数量更多
tr[u].mx=tr[tr[u].l].mx;
tr[u].res=tr[tr[u].l].res;
}
else{
tr[u].mx=tr[tr[u].r].mx;
tr[u].res=tr[tr[u].r].res;
}
}
void dfs(int u,int f){
dep[u]=dep[f]+1;
fa[u][0]=f;//这里是lca预处理
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f)continue;
dfs(j,u);
}
}
void init(){
for(int j=1;j<K;j++){//还是lca预处理
for(int i=0;i<=n;i++){
fa[i][j]=fa[fa[i][j-1]][j-1];
}
}
}
int get_lca(int x,int y){
if(dep[x]<dep[y])swap(x,y);//求lca
for(int i=K-1;~i;i--){
if(dep[fa[x][i]]>=dep[y]){
x=fa[x][i];
}
}
if(x==y)return x;
for(int i=K-1;~i;i--){
if(fa[x][i]!=fa[y][i]){
x=fa[x][i];
y=fa[y][i];
}
}
return fa[x][0];
}
void modify(int &u,int l,int r,int p,int k){
if(!u)u=++cnt;//动态开点
if(l==r){
tr[u].mx+=k;//数量加上这个数,事实上是差分操作
tr[u].res=p;//数量最多的就是这种,因为叶子节点只有一种粮食
return;
}
int mid=l+r>>1;
if(p<=mid)modify(tr[u].l,l,mid,p,k);
else modify(tr[u].r,mid+1,r,p,k);
pushup(u);
}
int merge(int x,int y,int l,int r){
if(!x||!y){//这里其实返回的非空的一边
x+=y;
return x;
}
int u=++cnt;
if(l==r){
tr[u].mx=tr[x].mx+tr[y].mx;//这里是新开个点存储合并后的值
tr[u].res=l;//这里只有l一种粮食,所以最多的是l
return u;
}
int mid=l+r>>1;
tr[u].l=merge(tr[x].l,tr[y].l,l,mid);
tr[u].r=merge(tr[x].r,tr[y].r,mid+1,r);//然后合并左右子树
pushup(u);
return u;
}
void dfs2(int u,int f){
for(int i=h[u];~i;i=ne[i]){
int j=e[i];
if(j==f)continue;
dfs2(j,u);
rt[u]=merge(rt[u],rt[j],1,lim);//边dfs边合并
}
}
signed main(){
cin>>n>>q;
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
int a,b;
cin>>a>>b;
add(a,b);add(b,a);
}
add(0,1);add(1,0);
dfs(0,0);//这里必须是0,如果father传-1会越界
init();
while(q--){
int a,b,c,p;
cin>>a>>b>>c;
p=get_lca(a,b);
modify(rt[fa[p][0]],1,lim,c,-1);//做一下树上差分
modify(rt[p],1,lim,c,-1);
modify(rt[a],1,lim,c,1);
modify(rt[b],1,lim,c,1);
}
dfs2(0,0);
for(int i=1;i<=n;i++){
if(tr[rt[i]].mx==0)cout<<"0\n";//如果没有粮食
else cout<<tr[rt[i]].res<<'\n';
}
return 0;
}
ROT-Tree Rotations
先考虑一个很暴力的思路。我们看每个点的左子树和右子树交换和不交换所产生的逆序对数量,这样时间复杂度是 \(O(n^3)\) 的。
所以我们考虑有没有什么办法快速统计逆序对的数量。
我们可以在每个点建一棵权值线段树,记录的是其子树中的数的出现次数。
于是我们就把原来暴力访问子树改成了在权值线段树中查询,时间复杂度 \(O(n^2 \log n)\),还是不够优秀。
然后我们继续优化。可以考虑使用线段树合并,在合并的时候当前区间的左子区间的数一定小于右子区间的数。然后不交换的答案是:左子树的右子区间的数的数量乘上右子树的左子区间的数的数量;交换后的答案是:右子树的左子区间的数的数量乘上左子树的右子区间的数的数量,所以我们的贡献就是这俩东西的最小值,时间复杂度 \(O(n \log n)\),可以通过。
代码:
#include<bits/stdc++.h>
#define int long long
#define N 400005
using namespace std;
int n,tot,cnt,rt[N],cnt1,cnt2,res;
struct node{
int l,r,sum;
}tr[N<<6];
void pushup(int u){
tr[u].sum=tr[tr[u].l].sum+tr[tr[u].r].sum;
}
void modify(int &u,int l,int r,int p){
if(!u)u=++tot;
if(l==r){
tr[u].sum++;
return;
}
int mid=l+r>>1;
if(p<=mid)modify(tr[u].l,l,mid,p);
else modify(tr[u].r,mid+1,r,p);
pushup(u);
}
int merge(int x,int y,int l,int r){
if(!x||!y)return x+y;
if(l==r){
tr[x].sum+=tr[y].sum;
return x;
}
int mid=l+r>>1;
cnt1+=tr[tr[x].l].sum*tr[tr[y].r].sum;
cnt2+=tr[tr[x].r].sum*tr[tr[y].l].sum;
tr[x].l=merge(tr[x].l,tr[y].l,l,mid);
tr[x].r=merge(tr[x].r,tr[y].r,mid+1,r);
pushup(x);
return x;
}
int dfs(){
cnt++;
int x=cnt,v;
rt[x]=++tot;
cin>>v;
if(!v){
int l=dfs(),r=dfs();
cnt1=cnt2=0;
rt[x]=merge(rt[l],rt[r],1,n);
res+=min(cnt1,cnt2);
}
else modify(rt[x],1,n,v);
return x;
}
signed main(){
cin>>n;
dfs();
cout<<res;
return 0;
}
线段树分裂
线段树分裂也没有什么基础概念好说的,所以还是直接上题。
线段树分裂
我们先看一下如果已经会线段树分裂了应该怎么做这些操作,当然线段树分裂会在下面讲的。
首先我们把每个可重集看作一棵权值线段树。
-
操作 \(0\),考虑把这棵权值线段树分成三部分,比 \(x\) 小的,\(x,y\) 之间的和比 \(y\) 大的。然后把第一部分和第三部分合并。
-
操作 \(1\),相当于是线段树合并的基本操作,这里不说了。
-
操作 \(2\),直接在权值线段树里修改。
-
操作 \(3\),在权值线段树内查询,如果当前区间被查询区间包含则记录贡献。
-
操作 \(4\),在权值线段树内看一下比他小的数的数量,根据这个判断答案比他小,比他大还是就是自己。
然后我们就说完了每个操作怎么做,现在看一下线段树分裂是怎么分裂的。
对于操作 \(0\),每次我们新开一棵权值线段树,然后把需要分出去的部分弄到那棵新开的权值线段树上就行了。
大概就是,我们弄一个函数实现把树中存储权值比目标值大的数的部分分出去。然后如果这个点的值更大,我们就交换空树的这个部分和当前树的这个部分,再去分裂左子树;如果已经相等,那么把这个点换过去就不用再分裂左子树了;否则直接分裂右子树。最后更新一下这个点代表区间的数的数量,事实上就是上传操作。
然后就说完了,放一下代码:
#include<bits/stdc++.h>
#define int long long
#define N 200005
using namespace std;
int n,m,cnt,tot,a[N],rt[N];
struct node{
int l,r,sum;
}tr[N<<5];
void build(int &u,int l,int r){
if(!u)u=++tot;
if(l==r){
tr[u].sum=a[l];//存储出现次数
return;
}
int mid=l+r>>1;
build(tr[u].l,l,mid);
build(tr[u].r,mid+1,r);
tr[u].sum=tr[tr[u].l].sum+tr[tr[u].r].sum;
}
int merge(int x,int y){
if(!x||!y)return x+y;
tr[x].l=merge(tr[x].l,tr[y].l);
tr[x].r=merge(tr[x].r,tr[y].r);
tr[x].sum+=tr[y].sum;//权值线段树基本合并操作
return x;
}
void modify(int &u,int l,int r,int p,int x){
if(!u)u=++tot;
tr[u].sum+=x;//这里修改的值一定被当前区间包含,所以这个点要加上增加的数量
if(l==r)return;//到达叶子返回
int mid=l+r>>1;
if(p<=mid)modify(tr[u].l,l,mid,p,x);//判断被哪边包含
else modify(tr[u].r,mid+1,r,p,x);
}
int qry1(int u,int l,int r,int k){//第k小
if(l==r)return l;
int mid=l+r>>1;
if(k<=tr[tr[u].l].sum)return qry1(tr[u].l,l,mid,k);//在左子树
else return qry1(tr[u].r,mid+1,r,k-tr[tr[u].l].sum);//在右子树,但是是右子树内的第k-tr[tr[u].l].sum个
}
int qry2(int u,int l,int r,int L,int R){//区间内的数的个数
if(l>=L&&r<=R)return tr[u].sum;//被包含
int mid=l+r>>1;
int res=0;
if(L<=mid)res+=qry2(tr[u].l,l,mid,L,R);
if(R>mid)res+=qry2(tr[u].r,mid+1,r,L,R);//线段树基本判断
return res;
}
void split(int x,int &y,int l,int r,int v){
if(!x)return;
y=++tot;//这里应该直接覆盖掉,所以不能判断y节点是否为空
int mid=l+r>>1;
if(v<mid)swap(tr[x].r,tr[y].r),split(tr[x].l,tr[y].l,l,mid,v);
//等价于把这部分弄到空树上然后清空,然后再分裂左子树
else if(v==mid)swap(tr[x].r,tr[y].r);//和上一行相比不用分裂左子树
else split(tr[x].r,tr[y].r,mid+1,r,v);//分裂右子树,当前部分应保留
tr[x].sum=tr[tr[x].l].sum+tr[tr[x].r].sum;
tr[y].sum=tr[tr[y].l].sum+tr[tr[y].r].sum;//上传
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
cnt=1;
build(rt[1],1,n);//初始在编号1的树内
while(m--){
int op,a,b,c,tmp;
cin>>op>>a>>b;
if(op==0){
cin>>c;
split(rt[a],rt[++cnt],1,n,b-1);
split(rt[cnt],tmp,1,n,c);
rt[a]=merge(rt[a],tmp);//先分裂,再合并
}
else if(op==1){
rt[a]=merge(rt[a],rt[b]);//合并
}
else if(op==2){//下面这三个都是基础操作
cin>>c;
modify(rt[a],1,n,c,b);
}
else if(op==3){
cin>>c;
cout<<qry2(rt[a],1,n,b,c)<<'\n';
}
else{
if(tr[rt[a]].sum<b)cout<<"-1\n";
else cout<<qry1(rt[a],1,n,b)<<'\n';
}
}
return 0;
}