首页 > 技术文章 > 例题 线段树合并

anyixing-fly 2020-07-05 14:43 原文

  1. 雨天的尾巴
    这道题应该算是很板子了,不过需要稍微思考一下,对于每次发放,如果模拟发放过程,那么每次发放的时间复杂度是\(O(n)\)的,这样显然会T,考虑如果每次只发放一种,用树上差分解决就可以,但是这个有很多种,所以给每个结点开一棵权值线段树就行,每个节点记录每种救济粮的数量,然后同样是利用差分的思想,只不过把差分数组改成了线段树的合并。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int lqs=1e5+10;
struct Edge{
	int to,nxt;
}e[lqs<<1];
int h[lqs],idx;
void Ins(int a,int b){
	e[++idx].to=b;e[idx].nxt=h[a];h[a]=idx;
}
int dep[lqs],fa[lqs],siz[lqs],son[lqs];
void dfs1(int u){
	siz[u]=1;
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa[u])continue;
		fa[v]=u;
		dep[v]=dep[u]+1;
		dfs1(v);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]])son[u]=v;
	}
}
int top[lqs];
void dfs2(int u,int tt){
	top[u]=tt;
	if(son[u])dfs2(son[u],tt);
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa[u]||v==son[u])continue;
		dfs2(v,v);
	}
}
int lca(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		x=fa[top[x]];
	}
	return dep[x]>dep[y]?y:x;
}
int cnt,lc[lqs*80],rc[lqs*80],val[lqs*80],ide[lqs*80];
void pushup(int rt){
	int ls=lc[rt],rs=rc[rt];
	if(val[ls]>=val[rs]){
		val[rt]=val[ls];
		ide[rt]=ide[ls];
	}else{
		val[rt]=val[rs];
		ide[rt]=ide[rs];
	}
}
void modify(int &rt,int l,int r,int pos,int w){
	if(!rt)rt=++cnt;
	if(l==r){
		val[rt]+=w;
		ide[rt]=l;
		return;
	}
	int mid=l+r>>1;
	if(pos<=mid)modify(lc[rt],l,mid,pos,w);
	else modify(rc[rt],mid+1,r,pos,w);
	pushup(rt);
}
int merge(int ls,int rs,int l,int r){
	if(!ls||!rs)return ls+rs;
	if(l==r){
		val[ls]+=val[rs];
		return ls;
	}
	int mid=l+r>>1;
	lc[ls]=merge(lc[ls],lc[rs],l,mid);
	rc[ls]=merge(rc[ls],rc[rs],mid+1,r);
	val[ls]+=val[rs];
	pushup(ls);
	return ls;
}
int ans[lqs];
int rt[lqs];
void dfs(int u){
	for(int i=h[u];i;i=e[i].nxt){
		int v=e[i].to;
		if(v==fa[u])continue;
		dfs(v);
		rt[u]=merge(rt[u],rt[v],1,lqs-10);
	}
	ans[u]=ide[rt[u]];
	if(val[rt[u]]==0)ans[u]=0;
}
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(ch<='9'&&ch>='0'){
		x=x*10+ch-'0';
		ch=getchar();
	}
	return x*f;
}
int main(){
	int n=read(),m=read();
	for(int i=1;i<n;i++){
		int a=read(),b=read();
		Ins(a,b);Ins(b,a);
	}
	dfs1(1);
	dfs2(1,1);
	for(int i=1;i<=m;i++){
		int x=read(),y=read(),z=read();
		modify(rt[x],1,lqs-10,z,1);
		modify(rt[y],1,lqs-10,z,1);
		int t=lca(x,y);
		modify(rt[t],1,lqs-10,z,-1);
		modify(rt[fa[t]],1,lqs-10,z,-1);
	}
	dfs(1);
	for(int i=1;i<=n;i++)
		printf("%d\n",ans[i]);
}

2.永无乡
这道题好像也是道水题。。。。
(最近我好像水的有点多)
不过要用到一个小技巧,就是查询第\(k\)小值,因为开的是权值线段树,所以左子树中的数一定大于右子树,注意这里说的数是岛的编号而不是树的值,在这里我们用树的\(val\)存子树中岛屿的个数,如果左子树的\(val\)大于等于\(k\),说明第\(k\)小值在左子树里,否则在右子树里,并且是右子树的第\(k-val_{left}\)小值。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int lqs=1e5+10;
int rk[lqs],cnt,rt[lqs],lc[lqs<<5],rc[lqs<<5],val[lqs<<5],f[lqs];
int find(int x){
	return x==f[x]?x:(f[x]=find(f[x]));
}
void update(int &rt,int l,int r,int w){
	rt=++cnt;
	val[rt]++;
	if(l==r)return;
	int mid=l+r>>1;
	if(w<=mid)update(lc[rt],l,mid,w);
	else update(rc[rt],mid+1,r,w);
}
int merge(int lt,int rt,int l,int r){
	if(!lt||!rt)return lt+rt;
	if(l==r){
		val[lt]+=val[rt];
		return lt;
	}
	int mid=l+r>>1;
	lc[lt]=merge(lc[lt],lc[rt],l,mid);
	rc[lt]=merge(rc[lt],rc[rt],mid+1,r);
	val[lt]+=val[rt];
	return lt;
}
int query(int rt,int l,int r,int pos){
	if(l==r)return l;
	int mid=l+r>>1;
	if(pos<=val[lc[rt]])return query(lc[rt],l,mid,pos);
	else return query(rc[rt],mid+1,r,pos-val[lc[rt]]);
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		int w;
		scanf("%d",&w);
		update(rt[i],1,n,w);
		f[i]=i;
		rk[w]=i;
	}
	for(int i=1;i<=m;i++){
		int a,b;
		scanf("%d%d",&a,&b);
		a=find(a);b=find(b);
		if(a==b)continue;
		f[b]=a;
		rt[a]=merge(rt[a],rt[b],1,n);
	}
	scanf("%d",&m);
	while(m--){
		int a,b;char s[20];
		scanf("%s%d%d",s,&a,&b);
		if(s[0]=='B'){
			if(find(a)==find(b))continue;
			rt[find(a)]=merge(rt[find(a)],rt[find(b)],1,n);
			f[find(b)]=find(a);		
		}else{
			a=find(a);
			if(val[rt[a]]<b)printf("-1\n");
			else printf("%d\n",rk[query(rt[a],1,n,b)]);
		}
	}
}

3.[POI2011]ROT-Tree Rotations
先看到前序遍历,可能会有点懵,但是再仔细看的话会发现,只有叶子节点有值,也就是说,前序遍历的深层意思就是,对于每棵子树而言,会先输出左子树然后再输出右子树中的值。
我们还可以再想,逆序对的情况可能会有以下三种

  • 在左子树里
  • 在右子树里
  • 跨越左右两个子树

我们能进行的操作只有交换左右子树,所以考虑这个操作所能造成的影响,发现它只能对情况三中的逆序对造成影响,情况一和二不会受到影响,所以可以从叶子节点开始向上操作。
现在考虑如何快速统计逆序对,不难想到对于每个节点开一棵权值线段树,注意由于开完的线段树内存可能会很大所以需要动态开点,然后每次将左子树和右子树合并覆盖到根节点上,至于逆序对,如果不交换左右子树,那就是左边那棵树的右半边乘上右边那棵树的的左半边的大小;交换的话则是左边那棵树的左半边乘上左边那棵树的的右半边的大小。
这么做为什么可以做到不重不漏的枚举所有的逆序对呢?上边已经提到,对根节点的操作不会影响左右子树的操作,对于这棵权值线段树也是一样,每次从底层开始回溯,依次用左子树去匹配右子树。

#include<cstdio>
#include<cstring>
#include<algorithm>
const int lqs=2e5+10;
int cnt,lc[lqs*20],rc[lqs*20],siz[lqs*20];
void update(int &rt,int l,int r,int val){
	if(!rt)rt=++cnt;
	if(l==r){
		siz[rt]++;
		return;
	}
	int mid=l+r>>1;
	if(val<=mid)update(lc[rt],l,mid,val);
	else update(rc[rt],mid+1,r,val);
	siz[rt]=siz[lc[rt]]+siz[rc[rt]];
}
long long tot1,tot2,ans;
int merge(int lt,int rt,int l,int r){
	if(!lt||!rt)return lt+rt;
	if(l==r){
		siz[lt]+=siz[rt];
		return lt;
	}
	tot1+=1ll*siz[lc[lt]]*siz[rc[rt]];
	tot2+=1ll*siz[lc[rt]]*siz[rc[lt]];
	int mid=l+r>>1;
	lc[lt]=merge(lc[lt],lc[rt],l,mid);
	rc[lt]=merge(rc[lt],rc[rt],mid+1,r);
	siz[lt]+=siz[rt];
	return lt;
}
int dfs(){
	int val,p=0;
	scanf("%d",&val);
	if(!val){
		int ll=dfs(),rr=dfs();
		tot1=tot2=0;
		p=merge(ll,rr,1,lqs);
		ans+=std::min(tot1,tot2);
	}else update(p,1,lqs,val);
	return p;
}
int main(){
	int n;
	scanf("%d",&n);
	dfs();
	printf("%lld\n",ans);
}

推荐阅读