边分治讲解
前言:
边分治和点分治一样属于树分治的一部分,相比于点分治,边分治对于与度数相关的问题有着很大的优势,同时边分治也是解决树上最优化问题的一种重要的算法。
分治过程:
边分治的分治过程与点分治类似,同样每次分治时找到一条分治中心边使这条边两端的两个联通块中较大的一个尽量小。以分治中心边为界限,恰好将当前分治的联通块中的点分成了两部分,统计路径经过分治中心边的答案,然后将分治中心边断开,递归分治中心边两端的两个联通块。
代码实现:
找分治中心边
找分治中心边和找树的重心方法类似,同样记录子树大小,对于每条边取这条边的子节点子树大小size及联通块大小-size中较大的一个更新分治中心边。代码中sum为联通块大小。
inline void getroot(int x,int fa,int sum) { size[x]=1; for(int i=head[x];i;i=next[i]) { if(!vis[i>>1]&&to[i]!=fa) { getroot(to[i],x,sum); size[x]+=size[to[i]]; int mx_size=max(size[to[i]],sum-size[to[i]]); if(mx_size<num) { num=mx_size; root=i; } } } }
边分治
因为我们需要知道分治中心边的两端点,所以在开双向边时边的编号要从2开始,这样x与x^1就是一对双向边。代码中的calc函数是统计经过分治中心边的答案。
inline void partation(int x,int sum) { num=INF; getroot(x,0,sum); if(num==INF) { return ; } int now=root; vis[now>>1]=1; cnt=0; dfs2(x,0,to[now],1); dfs2(to[now],0,0,2); calc(); int sz=size[to[now]]; partation(to[now],sz); partation(x,sum-sz); }
多叉树转二叉树
边分治的时间复杂度同样是$O(nlogn)$,但会被菊花图卡成$O(n^2)$,所以我们要将多叉树转成二叉树,这样虽然会有一个二倍常数但可以保证时间复杂度是$O(nlogn)$。
多叉树转二叉树的方法有两种:
1、第一种方法是从1开始枚举每个点,对于一个点$x$,如果他有<=2个子节点,那么直接向子节点连边即可;否则新建两个点,将$x$连向这两个点,并将$x$的子节点按奇偶分类暂时归为这两个新建点的子节点。为了不影响原树深度等信息,我们将连向新建点的边权设为0。这样新建树因为每条原树边会被存$logn$次,所以空间复杂度是$O(nlogn)$,新建节点数$O(n)$。代码中m为总点数。
void rebuild() { tot=1; for(int i=1;i<=n;i++) { head[i]=0; } for(int i=1;i<=n;i++) { int len=q[i].size(); if(len<=2) { for(int j=0;j<len;j++) { add(i,q[i][j],(q[i][j]<=m)); add(q[i][j],i,(q[i][j]<=m)); } } else { int ls=++n; int rs=++n; v[ls]=v[rs]=v[i]; add(i,ls,0); add(ls,i,0); add(i,rs,0); add(rs,i,0); for(int j=0;j<len;j++) { if(j&1) { q[ls].push_back(q[i][j]); } else { q[rs].push_back(q[i][j]); } } } } }
2、第二种方法是dfs整棵树,对于原树每个点x,记录一个$last$(初始为x),每次将$last$连向一个子节点,并新建一个点$y$将$last$连向$y$,然后将$last$改为$y$。同样将连向新建点的边权设为0。因为每个原树边只被保存一次,所以空间复杂度是$O(n)$,新建点数是$O(n)$。代码中m为总点数。
inline void rebuild(int x,int fa) { int tmp=0; int last=0; int len=v[x].size(); for(int i=0;i<len;i++) { int to=v[x][i].first; int val=v[x][i].second; if(to==fa) { continue; } tmp++; if(tmp==1) { add(x,to,val); add(to,x,val); last=x; } else if(tmp==len-(x!=1)) { add(last,to,val); add(to,last,val); } else { m++; add(last,m,0); add(m,last,0); last=m; add(m,to,val); add(to,m,val); } } for(int i=0;i<len;i++) { if(v[x][i].first==fa) { continue; } rebuild(v[x][i].first,x); } }
边分治的性质:
1、如果我们在边分治找中心时以当前联通块在原树中深度最小的点为根,那么找到的分治中心边在原树中一定是一条从父节点$u$到子节点$v$的边,这条边将当前联通块分成了两部分,可以发现包含$v$的那部分一定是原树中的一棵子树,我们假设包含$u$的部分为$S$,包含$v$的部分为$T$,那么$S$中的任意一个点$x$与$T$中的任意一个点$y$的$lca$一定不在$T$中,这也就说明$x$与$T$中所有点的$lca$都相同,就是$lca(x,v)$。
2、边分治将每次的分治联通块中的点恰好分成了两部分,这就省去了像点分治那样单独处理以分治重心为路径端点的答案这一过程。
边分树:
与点分树类似,我们将每层分治中心边连向下一层的分治中心边所形成的树就是边分树,边分树是一棵二叉树,它可以类似线段树一样合并,这一部分内容暂时还不完整,待博主后续更新。
练习题:
BZOJ2870最长道路(边分治模板题)
CSTC2018暴力写挂(两棵树,第一棵树边分治转到第二棵树上建虚树DP)
WC2018通道(三棵树,码量较大)