线段树合并、树上启发式合并 简介
线段树合并
线段树合并可以解决这个问题:
有两棵动态开点线段树,每个节点维护的是一个数组中值域为 \([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);
}
点击查看代码
#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]<<' ';
}