线段树合并、树上启发式合并 简介

线段树合并

线段树合并可以解决这个问题:

有两棵动态开点线段树,每个节点维护的是一个数组中值域为 \([l,r]\) 的数个数。现在要将两个数组并起来,那么就需要将两棵线段树中的数据合并。做法是将两棵线段树对应位置的值相加

维护值域的线段树由于下标较大需要动态开点(类似于可持久化线段树的开点方式)。
代码实现:

int merge(int p,int q,int l,int r){
	if(!p)return q;if(!q)return p;
	if(l==r){t[p].cnt+=t[q].cnt;return p;}
	int mid=l+r>>1;
	t[p].ls=merge(t[p].ls,t[q].ls,l,mid),t[p].rs=merge(t[p].rs,t[q].rs,mid+1,r);
	pushup(p);
	return p;
}

补充:[示例]动态开点线段树的单点修改

void chg(int p,int v,int l,int r,int k){
	if(l==r){t[k].cnt+=v;return;}
	int mid=l+r>>1;
	if(p<=mid)chg(p,v,l,mid,t[k].ls?t[k].ls:t[k].ls=++tot);
	else chg(p,v,mid+1,r,t[k].rs?t[k].rs:t[k].rs=++tot);
	pushup(k);
}

【模板】CF600E Lomsat gelral

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
typedef long long ll;
struct node {
	int ls,rs,cnt;ll ans;
}t[N*20];
int n,tot,a[N];ll ans[N];
vector<int>G[N];
void pushup(int k){
	if(t[t[k].ls].cnt>t[t[k].rs].cnt)t[k].cnt=t[t[k].ls].cnt,t[k].ans=t[t[k].ls].ans;
	else if(t[t[k].ls].cnt<t[t[k].rs].cnt)t[k].cnt=t[t[k].rs].cnt,t[k].ans=t[t[k].rs].ans;
	else t[k].cnt=t[t[k].ls].cnt,t[k].ans=t[t[k].ls].ans+t[t[k].rs].ans;
}
void chg(int p,int v,int l,int r,int k){
	if(l==r){t[k].cnt+=v,t[k].ans=l;return;}
	int mid=l+r>>1;
	if(p<=mid)chg(p,v,l,mid,t[k].ls?t[k].ls:t[k].ls=++tot);
	else chg(p,v,mid+1,r,t[k].rs?t[k].rs:t[k].rs=++tot);
	pushup(k);
}
int merge(int p,int q,int l,int r){
	if(!p)return q;if(!q)return p;
	if(l==r){t[p].cnt+=t[q].cnt,t[p].ans=l;return p;}
	int mid=l+r>>1;
	t[p].ls=merge(t[p].ls,t[q].ls,l,mid),t[p].rs=merge(t[p].rs,t[q].rs,mid+1,r);
	pushup(p);
	return p;
}
void dfs(int x,int p){
	for(int i=0;i<G[x].size();i++){
		int y=G[x][i];
		if(y^p)dfs(y,x),merge(x,y,1,n);
	}
	ans[x]=t[x].ans;
}
int main(){
	scanf("%d",&n),tot=n;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),chg(a[i],1,1,n,i);
	for(int i=1,u,v;i<n;i++)scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
	dfs(1,0);
	for(int i=1;i<=n;i++)cout<<ans[i]<<' ';
}

【模板】雨天的尾巴

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
inline int read(){
	register char ch=getchar();register int x=0;
	while(ch<'0'||ch>'9')ch=getchar();
	while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return x;
}
struct node {
	int ls,rs,cnt,ans; node(){cnt=0;}
}t[N*72];
int n,m,tot,dep[N],fa[N][18],ans[N];
vector<int>G[N];
void dfs(int x,int p){
	dep[x]=dep[p]+1,fa[x][0]=p;
	for(int i=1;i<=17;i++)fa[x][i]=fa[fa[x][i-1]][i-1];
	for(int i=0;i<G[x].size();i++){
		int y=G[x][i];
		if(y^p)dfs(y,x);
	}
}
int glca(int u,int v){
	if(u==v)return u;
	if(dep[u]>dep[v])swap(u,v);
	for(int i=17;~i;i--)if(dep[fa[v][i]]>=dep[u])v=fa[v][i];
	if(u==v)return u;
	for(int i=17;~i;i--)if(fa[u][i]!=fa[v][i])u=fa[u][i],v=fa[v][i];
	return fa[u][0];
}
void pushup(int k){
	if(t[t[k].ls].cnt>t[t[k].rs].cnt)t[k].cnt=t[t[k].ls].cnt,t[k].ans=t[t[k].ls].ans;
	else if(t[t[k].ls].cnt<t[t[k].rs].cnt)t[k].cnt=t[t[k].rs].cnt,t[k].ans=t[t[k].rs].ans;
	else t[k].cnt=t[t[k].ls].cnt,t[k].ans=min(t[t[k].ls].ans,t[t[k].rs].ans);
}
void chg(int p,int v,int l,int r,int k){
	if(l==r){t[k].cnt+=v,t[k].ans=l;return;}
	int mid=l+r>>1;
	if(p<=mid)chg(p,v,l,mid,t[k].ls?t[k].ls:t[k].ls=++tot);
	else chg(p,v,mid+1,r,t[k].rs?t[k].rs:t[k].rs=++tot);
	pushup(k);
}
int merge(int p,int q,int l,int r){
	if(!p)return q;if(!q)return p;
	if(l==r){t[p].cnt+=t[q].cnt,t[p].ans=l;return p;}
	int mid=l+r>>1;
	t[p].ls=merge(t[p].ls,t[q].ls,l,mid),t[p].rs=merge(t[p].rs,t[q].rs,mid+1,r);
	pushup(p);
	return p;
}
void dfs2(int x,int p){
	for(int i=0;i<G[x].size();i++){
		int y=G[x][i];
		if(y^p)dfs2(y,x),merge(x,y,1,1e5);
	}
	ans[x]=t[x].ans;
}
int main(){
	n=read(),m=read();
	for(int i=1,u,v;i<n;i++)u=read(),v=read(),G[u].push_back(v),G[v].push_back(u);
	dfs(1,0);
	int x,y,z,lca;
	tot=n;
	while(m--){
		x=read(),y=read(),z=read(),lca=glca(x,y);
		chg(z,1,1,1e5,x);
		chg(z,1,1,1e5,y);
		chg(z,-1,1,1e5,lca);
		if(fa[lca][0])chg(z,-1,1,1e5,fa[lca][0]);
	}
	dfs2(1,0);
	for(int i=1;i<=n;i++)cout<<ans[i]<<'\n';
}

复杂度分析:合并两棵线段树复杂度是较小的那棵的点数 \(O(n_{\min}\log V)\),不难发现总复杂度就是 \(O(n\log V)\)\(V\) 为值域),空间复杂度也是 \(O(n\log V)\)

树上启发式合并

树上启发式合并的主要思想,结合上面 CF600E Lomsat gelral 来讲解。

暴力用 cnt 数组维护一个节点的子树中各颜色出现次数。那么对于每个节点,在1.让子节点都统计答案之后,都需要2.首先清空 cnt,然后3.遍历子树,将各节点颜色加入 cnt,并顺便统计答案(if(cnt[a[x]]>num)num=cnt[a[x]],ans=a[x];else if(cnt[a[x]]==num)ans+=a[x];

这样做的复杂度最坏是 \(O(n^2)\)(链)。

考虑最后一次统计的儿子是无需清空的,根据树链剖分思想可以处理出重儿子并最后处理重儿子而不清空它。接下来照旧做2.、3.(只做轻儿子)步骤。
可以证明,这样做的复杂度 \(O(n\log n)\)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
typedef long long ll;
int n,num,a[N],cnt[N],son[N],siz[N];ll res[N],ans;
vector<int>G[N];
void dfs1(int x,int p){
	siz[x]=1;
	for(int i=0;i<G[x].size();i++){
		int y=G[x][i];
		if(y^p){
			dfs1(y,x);siz[x]+=siz[y];if(siz[son[x]]<siz[y])son[x]=y;
		}
	}
}
void dfs(int x,int p,bool b){
	for(int i=0;i<G[x].size();i++){
		int y=G[x][i];
		if(y^p)if(y^son[x]){
			dfs(y,x,b);
			if(b)memset(cnt,0,sizeof(cnt)),num=0,ans=0;
		}
	}
	if(son[x])dfs(son[x],x,b);
	if(b)for(int i=0;i<G[x].size();i++){
		int y=G[x][i];
		if(y^p)if(y^son[x])dfs(y,x,0);
	}
	cnt[a[x]]++;
	if(cnt[a[x]]>num)num=cnt[a[x]],ans=a[x];else if(cnt[a[x]]==num)ans+=a[x];
	if(b)res[x]=ans;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1,u,v;i<n;i++)scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
	dfs1(1,0),dfs(1,0,1);
	for(int i=1;i<=n;i++)cout<<res[i]<<' ';
}
posted @ 2022-01-03 13:56  pengyule  阅读(138)  评论(0)    收藏  举报