变种线段树 基础篇

权值线段树

线段树在这里作为前置知识,我们就不说了,而且权值线段树也不是核心内容,不会大篇幅讲。

首先,权值线段树在维护什么?维护的是桶。

然后,权值线段树有什么用?可以求一些序列的第 \(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;
}
posted @ 2024-07-17 17:26  zxh923  阅读(6)  评论(0编辑  收藏  举报