一.合并算法的引入.
我们先来看一道例题.
题目:BZOJ4719.
题目大意:给定一棵 n n n个点的树和树上每个节点的 w i w_i wi,现在给出 m m m对 s i s_i si和 t i t_i ti,表示从 s i s_i si到 t i t_i ti会有一个人沿树上的路径走过,并且这个人每秒移动到下一个点.现在每个人都在时刻 0 0 0走出,询问每一个点 i i i,请你输出第 w i w_i wi时刻有多少人会在点 i i i.
1 ≤ n , m ≤ 3 ∗ 1 0 5 1\leq n,m\leq 3*10^5 1≤n,m≤3∗105.
对你绝对没有看错我就是要用天天爱跑步这道NOIP史上第一难的题引入数据结构合并算法…
首先对于这道题的一些其他分析过程请参考BZOJ4719天天爱跑步题解.
现在我们考虑如何解决子树统计的问题.显然如果子树信息并不是具有可减性的信息的话,上面那篇题解的做法就不能用了.
如果是这样的话怎么办呢?有一个比较好的办法就是利用数据结构的合并.
具体来说,我们考虑对于对于树上每一个点对信息的贡献维护一个数据结构,然后遍历整棵树,当遍历完一个点的时候把这个点维护的信息直接合并到它的父亲上,最后在退出一个点之前统计贡献即可.
不过这个时候数据结构的合并效率就成了这个做法的复杂度瓶颈,所以接下来我们要介绍几种数据结构快速合并的方法.
二.启发式合并.
启发式合并是一类非常通用的数据结构合并技巧.
首先我们考虑一个数据结构(例如平衡树),先把所有元素塞进对应的数据结构中.每次合并两个数据结构的时候,把元素少的数据结构拆开,暴力插入的元素多的数据结构中,这种方法称为启发式合并.
若原来的总贡献数为 O ( n ) O(n) O(n),数据结构插入一个元素的时间复杂度为 O ( k ) O(k) O(k),把数据结构暴力拆开来的时间复杂度为 O ( n ) O(n) O(n),那么这样子做的时间复杂度是 O ( n k log n ) O(nk\log n) O(nklogn)的.
至于这个算法的时间复杂度怎么来的,当然是一个元素被合并一次,它所在的数据结构元素数就增大了至少一倍啊…
关于启发式合并的例子,比如并查集的按秩合并,本质上就是一种经典的启发式合并.
那么对于例题,我们维护两个map启发式合并即可.
时间复杂度 O ( m ( log n + log 2 m ) ) O(m(\log n+\log^2m)) O(m(logn+log2m)).
具体代码参考文末.
三.树上启发式合并dsu on tree.
上面的map启发式合并是 O ( m ( log n + log 2 m ) ) O(m(\log n+\log^2m)) O(m(logn+log2m))的,两个 log \log log的时间复杂度加上map的常数看起来并不优秀,这个复杂度是否可以优化呢?
考虑维护一个计数数组直接进行启发式合并,并记录一个数组表示哪些位置有值,那么就可以做到 O ( n log n ) O(n\log n) O(nlogn)的时间复杂度进行启发式合并了.
听上去好像没啥问题…等等空间复杂度是 O ( n 2 ) O(n^2) O(n2)的啊.
所以我们需要优化它的空间复杂度,考虑一下只记一个数组是否可行.
具体来说,我们先维护一个计数数组 c n t [ i ] cnt[i] cnt[i],然后遍历这棵树.每当我们遍历完点 k k k的所有儿子的时候,计算出当前点为根的子树对应的 c n t cnt cnt,这可以通过一遍暴力dfs整棵子树实现.为了不对它的祖先产生影响,统计完当前点的信息后应该将 c n t cnt cnt清空.
突然发现这个做法空间复杂度是降下来了,但是时间复杂度又变回了原来的 O ( n 2 ) O(n^2) O(n2).
但是我们仔细一想,这未免有些浪费,因为一个点遍历完它的所有儿子后,最后一个儿子对应的 c n t cnt cnt数组在当前点也需要用到,完全不需要清空.
为了让每次不清空的点数尽量多,显然需要最后一个遍历当前点子树点数最多的儿子,也就是它的重儿子.
而重儿子有一个性质——除了它之外的其它儿子的子树大小都只有当前点的最多一半.
考虑对于每一个点,它只有被清空一次才会有 O ( 1 ) O(1) O(1)的时间复杂度贡献.而我们将这个过程看做这个点向上被合并的过程,只有它是轻儿子的时候才会被暴力拆出来清空,此时它所在的那棵子树大小至少为原来的 2 2 2倍.
与上面启发式合并的时间复杂度证明类似的,它最多被清空 O ( log n ) O(\log n) O(logn)次,这个做法的总时间复杂度为 O ( n log n ) O(n\log n) O(nlogn).
对应到例题上,我们只需要维护两个计数数组dsu on tree即可.
时间复杂度 O ( n + m log n ) O(n+m\log n) O(n+mlogn).
具体代码参考文末.
四.线段树合并.
线段树合并不同于启发式合并,它并不是每一次把小的线段树暴力拆开插入大的线段树上,而是利用线段树的结构特性直接合并.
具体来说,我们先给每个点都建立一棵的动态开点权值线段树,并加入初始的贡献.然后设计一个返回合并后节点编号的合并函数 M e r g e ( k 0 , k 1 ) Merge(k0,k1) Merge(k0,k1)表示合并 k 0 k0 k0和 k 1 k1 k1为根的线段树.
对于 M e r g e ( k 0 , k 1 ) Merge(k0,k1) Merge(k0,k1),如果 k 0 k0 k0和 k 1 k1 k1中有一个空节点,那么直接返回那个非空节点;否则我们将 k 0 k0 k0和 k 1 k1 k1的信息合并到 k 0 k0 k0上,并分别调用 t r [ k 0 ] . s [ 0 ] = M e r g e ( t r [ k 0 ] . s [ 0 ] , t r [ k 1 ] . s [ 1 ] ) tr[k0].s[0]=Merge(tr[k0].s[0],tr[k1].s[1]) tr[k0].s[0]=Merge(tr[k0].s[0],tr[k1].s[1])和 t r [ k 1 ] . s [ 1 ] = M e r g e ( t r [ k 0 ] . s [ 1 ] , t r [ k 1 ] . s [ 1 ] ) tr[k1].s[1]=Merge(tr[k0].s[1],tr[k1].s[1]) tr[k1].s[1]=Merge(tr[k0].s[1],tr[k1].s[1]).
由于一开始线段树的节点个数是 O ( n log n ) O(n\log n) O(nlogn)的,而每次不合并的节点必然不被遍历到,被遍历到的节点必然会被合并,此时节点个数会被减少 1 1 1,而最后合并出来的线段树节点个数不可能是负数,也就是说此时被遍历到的节点数为 O ( n log n ) O(n\log n) O(nlogn),总时空复杂度也就是 O ( n log n ) O(n\log n) O(nlogn)的.
具体应用到例题上,我们需要先对每个点维护一个动态开点权值线段树,并将初始的贡献加入对应点上的权值线段树中,之后线段树合并即可.
时空复杂度 O ( n + m log n ) O(n+m\log n) O(n+mlogn).
具体代码参考文末.
不过由于这个算法存在线段树这种常数较大的数据结构,线段树合并相对于dsu on tree并不好用.
那么它有什么特别之处呢?它可以支持在线查询.
当然,我们上面的合并算法并不能直接支持在线查询,而是要在合并 k 0 , k 1 k0,k1 k0,k1的时候新建一个节点 k k k合并它们的信息并返回.显然这样做的时空复杂度仍然不变.
不过线段树合并仍然无法支持修改,因为当前点的线段树与它儿子的线段树仍然是有共用节点的.
五.例题代码.
map启发式合并:
#include
using namespace std;
#define Abigail inline void
typedef long long LL;
#define mapit map::iterator
const int N=300000;
int n,m;
struct side{
int y,next;
}e[N*2+9];
int lin[N+9],cs;
void Ins(int x,int y){e[++cs].y=y;e[cs].next=lin[x];lin[x]=cs;}
void Ins2(int x,int y){Ins(x,y);Ins(y,x);}
int w[N+9];
int st[N+9],td[N+9];
struct node{
int fa,son,dep,siz,top;
}d[N+9];
void Dfs_ord1(int k,int fa){
d[k].fa=fa;
d[k].dep=d[fa].dep+1;
d[k].siz=1;
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^fa){
Dfs_ord1(e[i].y,k);
d[k].siz+=d[e[i].y].siz;
if (d[e[i].y].siz>d[d[k].son].siz) d[k].son=e[i].y;
}
}
void Dfs_ord2(int k,int top){
d[k].top=top;
if (d[k].son) Dfs_ord2(d[k].son,top);
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_ord2(e[i].y,e[i].y);
}
int Query_lca(int x,int y){
for (;d[x].top^d[y].top;x=d[d[x].top].fa)
if (d[d[x].top].dep<d[d[y].top].dep) swap(x,y);
return d[x].dep<d[y].dep?x:y;
}
int id[2][N+9];
map<int,int>mp[2][N+9];
int Merge(int id,int x,int y){
if (mp[id][x].size()<mp[id][y].size()) swap(x,y);
for (mapit it=mp[id][y].begin();it!=mp[id][y].end();++it)
mp[id][x][it->first]+=it->second;
mp[id][y].clear();
return x;
}
int ans[N+9];
void Dfs_ans(int k){
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^d[k].fa){
Dfs_ans(e[i].y);
id[0][k]=Merge(0,id[0][k],id[0][e[i].y]);
id[1][k]=Merge(1,id[1][k],id[1][e[i].y]);
}
if (w[k]<=n) ans[k]=mp[0][id[0][k]][w[k]+d[k].dep]+mp[1][id[1][k]][w[k]-d[k].dep+n];
}
void Get_ans(){
for (int i=1;i<=n;++i) id[0][i]=id[1][i]=i;
for (int i=1;i<=m;++i){
int lca=Query_lca(st[i],td[i]),t0=d[st[i]].dep,t1=d[st[i]].dep-2*d[lca].dep+n;
++mp[0][st[i]][t0];
--mp[0][d[lca].fa][t0];
++mp[1][td[i]][t1];
--mp[1][lca][t1];
}
Dfs_ans(1);
}
Abigail into(){
scanf("%d%d",&n,&m);
for (int i=1;i<n;++i){
int x,y;
scanf("%d%d",&x,&y);
Ins2(x,y);
}
for (int i=1;i<=n;++i)
scanf("%d",&w[i]);
for (int i=1;i<=m;++i)
scanf("%d%d",&st[i],&td[i]);
}
Abigail work(){
Dfs_ord1(1,0);
Dfs_ord2(1,1);
Get_ans();
}
Abigail outo(){
for (int i=1;i<=n;++i)
printf("%d ",ans[i]);
puts("");
}
int main(){
into();
work();
outo();
return 0;
}
dsu on tree:
#include
using namespace std;
#define Abigail inline void
typedef long long LL;
const int N=300000;
int n,m;
struct side{
int y,next;
}e[N*2+9];
int lin[N+9],cs;
void Ins(int x,int y){e[++cs].y=y;e[cs].next=lin[x];lin[x]=cs;}
void Ins2(int x,int y){Ins(x,y);Ins(y,x);}
int w[N+9];
int st[N+9],td[N+9];
struct node{
int fa,son,dep,siz,top;
}d[N+9];
void Dfs_ord1(int k,int fa){
d[k].fa=fa;
d[k].dep=d[fa].dep+1;
d[k].siz=1;
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^fa){
Dfs_ord1(e[i].y,k);
d[k].siz+=d[e[i].y].siz;
if (d[e[i].y].siz>d[d[k].son].siz) d[k].son=e[i].y;
}
}
void Dfs_ord2(int k,int top){
d[k].top=top;
if (d[k].son) Dfs_ord2(d[k].son,top);
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_ord2(e[i].y,e[i].y);
}
int Query_lca(int x,int y){
for (;d[x].top^d[y].top;x=d[d[x].top].fa)
if (d[d[x].top].dep<d[d[y].top].dep) swap(x,y);
return d[x].dep<d[y].dep?x:y;
}
vector<int>a[2][N+9],v[2][N+9];
int Insert(int id,int k,int t,int x){a[id][k].push_back(t);v[id][k].push_back(x);}
struct Table{
int cnt[N*2+9],vis[N*2+9],p[N*2+9],cp;
void Clear(){for (;cp;--cp) cnt[p[cp]]=vis[p[cp]]=0;}
void Insert(int x,int v){if (!vis[x]) vis[x]=1,p[++cp]=x;cnt[x]+=v;}
void Erase(int x,int v){cnt[x]-=v;}
int Query(int x){return cnt[x];}
}tab[2];
int ans[N+9];
void Get_tab(int k){
int siz=a[0][k].size();
for (int i=0;i<siz;++i) tab[0].Insert(a[0][k][i],v[0][k][i]);
siz=a[1][k].size();
for (int i=0;i<siz;++i) tab[1].Insert(a[1][k][i],v[1][k][i]);
}
void Dfs_tab(int k){
Get_tab(k);
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^d[k].fa) Dfs_tab(e[i].y);
}
void Dfs_ans(int k){
tab[0].Clear();tab[1].Clear();
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_ans(e[i].y);
if (d[k].son) Dfs_ans(d[k].son);
Get_tab(k);
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_tab(e[i].y);
if (w[k]<=n) ans[k]=tab[0].Query(w[k]+d[k].dep)+tab[1].Query(w[k]-d[k].dep+n);
}
void Get_ans(){
for (int i=1;i<=m;++i){
int lca=Query_lca(st[i],td[i]),t0=d[st[i]].dep,t1=d[st[i]].dep-2*d[lca].dep+n;
Insert(0,st[i],t0,1);
Insert(0,d[lca].fa,t0,-1);
Insert(1,td[i],t1,1);
Insert(1,lca,t1,-1);
}
Dfs_ans(1);
}
Abigail into(){
scanf("%d%d",&n,&m);
for (int i=1;i<n;++i){
int x,y;
scanf("%d%d",&x,&y);
Ins2(x,y);
}
for (int i=1;i<=n;++i)
scanf("%d",&w[i]);
for (int i=1;i<=m;++i)
scanf("%d%d",&st[i],&td[i]);
}
Abigail work(){
Dfs_ord1(1,0);
Dfs_ord2(1,1);
Get_ans();
}
Abigail outo(){
for (int i=1;i<=n;++i)
printf("%d ",ans[i]);
puts("");
}
int main(){
into();
work();
outo();
return 0;
}
线段树合并:
#include
using namespace std;
#define Abigail inline void
typedef long long LL;
const int N=300000,C=80;
int n,m;
struct side{
int y,next;
}e[N*2+9];
int lin[N+9],cs;
void Ins(int x,int y){e[++cs].y=y;e[cs].next=lin[x];lin[x]=cs;}
void Ins2(int x,int y){Ins(x,y);Ins(y,x);}
int w[N+9];
int st[N+9],td[N+9];
struct node{
int fa,son,dep,siz,top;
}d[N+9];
void Dfs_ord1(int k,int fa){
d[k].fa=fa;
d[k].dep=d[fa].dep+1;
d[k].siz=1;
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^fa){
Dfs_ord1(e[i].y,k);
d[k].siz+=d[e[i].y].siz;
if (d[e[i].y].siz>d[d[k].son].siz) d[k].son=e[i].y;
}
}
void Dfs_ord2(int k,int top){
d[k].top=top;
if (d[k].son) Dfs_ord2(d[k].son,top);
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^d[k].fa&&e[i].y^d[k].son) Dfs_ord2(e[i].y,e[i].y);
}
int Query_lca(int x,int y){
for (;d[x].top^d[y].top;x=d[d[x].top].fa)
if (d[d[x].top].dep<d[d[y].top].dep) swap(x,y);
return d[x].dep<d[y].dep?x:y;
}
struct tree{
int v,s[2];
}tr[N*C+9];
int cn,rot[2][N+9];
void Change_add(int p,int v,int l,int r,int &k){
if (!k) k=++cn;
if (l==r) {tr[k].v+=v;return;}
int mid=l+r>>1;
p<=mid?Change_add(p,v,l,mid,tr[k].s[0]):Change_add(p,v,mid+1,r,tr[k].s[1]);
}
int Query(int p,int l,int r,int k){
if (!k) return 0;
if (l==r) return tr[k].v;
int mid=l+r>>1;
return p<=mid?Query(p,l,mid,tr[k].s[0]):Query(p,mid+1,r,tr[k].s[1]);
}
int Merge(int k0,int k1){
if (!k0||!k1) return k0+k1;
tr[k0].s[0]=Merge(tr[k0].s[0],tr[k1].s[0]);
tr[k0].s[1]=Merge(tr[k0].s[1],tr[k1].s[1]);
tr[k0].v+=tr[k1].v;
return k0;
}
int ans[N+9];
void Dfs_ans(int k){
for (int i=lin[k];i;i=e[i].next)
if (e[i].y^d[k].fa){
Dfs_ans(e[i].y);
rot[0][k]=Merge(rot[0][k],rot[0][e[i].y]);
rot[1][k]=Merge(rot[1][k],rot[1][e[i].y]);
}
if (w[k]<=n) ans[k]=Query(w[k]+d[k].dep,1,n<<1,rot[0][k])+Query(w[k]-d[k].dep+n,1,n<<1,rot[1][k]);
}
void Get_ans(){
for (int i=1;i<=m;++i){
int lca=Query_lca(st[i],td[i]),t0=d[st[i]].dep,t1=d[st[i]].dep-2*d[lca].dep+n;
Change_add(t0,1,1,n<<1,rot[0][st[i]]);
Change_add(t0,-1,1,n<<1,rot[0][d[lca].fa]);
Change_add(t1,1,1,n<<1,rot[1][td[i]]);
Change_add(t1,-1,1,n<<1,rot[1][lca]);
}
Dfs_ans(1);
}
Abigail into(){
scanf("%d%d",&n,&m);
for (int i=1;i<n;++i){
int x,y;
scanf("%d%d",&x,&y);
Ins2(x,y);
}
for (int i=1;i<=n;++i)
scanf("%d",&w[i]);
for (int i=1;i<=m;++i)
scanf("%d%d",&st[i],&td[i]);
}
Abigail work(){
Dfs_ord1(1,0);
Dfs_ord2(1,1);
Get_ans();
}
Abigail outo(){
for (int i=1;i<=n;++i)
printf("%d ",ans[i]);
puts("");
}
int main(){
into();
work();
outo();
return 0;
}