先来挖个大坑,这次要整个树链剖分,按照习惯,先推荐几波学习时观摩过的大佬博客
树链剖分详解 ChinHhh大佬的讲解,我觉得是我看过的里面最详细的了,而且带图好理解
树链剖分详解 自为风月马前卒大佬的讲解,分步骤讲解还有推荐例题
接下来就是我自己的领悟了。
学习树链剖分有几个先行条件,首先你需要知道树是什么,然后是学会线段树的基本操作,这个先挖个坑,之后复习到再填,然后最好对树形dp有点了解。再然后就是dfs序和lca了。
首先dfs序,我觉得这个还算简单了,就是每个节点进行dfs时的序号,像下图,当我们进行dfs时,从1开始所以1的dfs序是1,然后我们优先走左边的话,2的dfs序就是2,4的dfs序就是3,5的dfs序就是4,8的dfs序就是6,以此下去
我们用din记录到达该节点和dout记录离开该节点时的序号就是
这样从din到dout,某个节点和它的子树就是连续的编号了,线段树刚好就可以处理连续区间,简单的伪代码实现就是
void dfs(int u)u为当前节点
扫描二维码关注公众号,回复: 5674664 查看本文章{
din[u]=id++;
for(和u相连的每个节点v)
if(v不是u的父节点)
dfs(v);
dout[u]=id;
}
然后就是lca,lca的求法有好几种,我所知道的就有离线Tarjan,树上倍增,RMQ(当然树链剖分也可以),我这里谈谈最近对离线Tarjan和树上倍增的理解,RMQ还是先挖坑。。。
先是离线Tarjian,来个JVxie大佬博客最近公共祖先LCA(Tarjan算法)的思考和算法实现,还有zhouzhendong大佬的LCA算法解析-Tarjan&倍增&RMQ(其实你们百度lca前两个博客就是。。。)
LCA是最近公共祖先的意思,在上图的话像4和5的最近公共祖先就是2,而4和7的最近公共祖先是1,从某种意义上讲如果不怕超时的话,每次直接暴力搜索是可以找到每两个节点的最近公共祖先的,不过红红的TLE不好看,要想生活过得去,还是得看点AC的绿。
而Tarjan求lca是离线算法,也就是对于所有的询问需要全输入完处理之后再输出答案,而不是输入一组询问输出相应的答案,JVxie大佬的讲解中有很详细的模拟过程,我就不重复了,就简单讲讲自己的理解。
首先,我们需要保存所有询问,像建边一样我们记录下每个节点与其有询问关系的点,像上图的询问4和5的最近公共祖先的话,我们可以在4和5间类似建边一样加上一个关系,用我自己的代码来说就是(我是习惯链式前向星建边)
struct Side{
int v,c,ne;
}S[2*N],Q[2*M];建边:
void adds(int u,int v,int c)
{
S[sn].v=v;
S[sn].c=c;
S[sn].ne=heads[u];
heads[u]=sn++;
}建询问关系:
void addq(int u,int v,int id)
{
Q[qn].v=v;
Q[qn].c=id;
Q[qn].ne=headq[u];
headq[u]=qn++;
}边里面的c就代表边的权值,而询问关系里的c就代表它是第几组询问
然后就是dfs了,我们先对每个点深搜也就是对它的子树遍历一遍,并且用并查集把它的子节点归到它这里,然后当这个点的子树都遍历完后,我们就去遍历和它有询问关系的点,如果和它有询问关系的某个点已经被搜索过了的话,那么某个点当前并查集所归属到的那个点就是他们的最近公共祖先。
因为是按深搜的顺序遍历树,一个节点一开始是归属到它父节点的,如果它的兄弟节点或者它的父节点和它有询问关系的话,那么最近公共节点就是它的父节点,而如果是其他不跟它再同一个分支的节点跟它有询问关系的话,当它的父节点搜索完,它也会跟着它的父节点归属到它的爷爷节点,一直往上归属到两个节点的公共节点。光说太空洞,做题理解,引用大佬推荐的两道题
CODEVS 2370 小机房的树 传送门
中文题,大意就是树上两个节点的最低路径,也就是他们到他们的最近公共祖先的距离,直接上代码了
1 #include<cstdio> 2 const int N=52013,M=75118; 3 struct Side{ 4 int v,c,ne; 5 }S[2*N],Q[2*M]; 6 int sn,qn,heads[N],headq[N],fa[N],vis[N],dis[N],ans[M]; 7 void init(int n) 8 { 9 sn=qn=0; 10 for(int i=0;i<=n;i++) 11 { 12 fa[i]=i;//并查集,每个节点一开始自己是自己父亲 13 dis[i]=0; 14 vis[i]=0; 15 heads[i]=headq[i]=-1; 16 } 17 } 18 void adds(int u,int v,int c) 19 { 20 S[sn].v=v; 21 S[sn].c=c; 22 S[sn].ne=heads[u]; 23 heads[u]=sn++; 24 } 25 void addq(int u,int v,int id) 26 { 27 Q[qn].v=v; 28 Q[qn].c=id;//记录下它的第几个询问 29 Q[qn].ne=headq[u]; 30 headq[u]=qn++; 31 } 32 int gui(int x){ 33 return fa[x]==x ? x : fa[x]=gui(fa[x]); 34 } 35 void bing(int x,int y) 36 { 37 int gx=gui(x),gy=gui(y); 38 if(gx!=gy) 39 fa[gy]=gx; 40 }//并查集,我们老师说最简单的算法,嘤嘤嘤 41 void dfs(int u,int f,int len) 42 { 43 dis[u]=len; 44 for(int i=heads[u];i!=-1;i=S[i].ne) 45 { 46 int v=S[i].v; 47 if(v!=f) 48 { 49 dfs(v,u,len+S[i].c); 50 bing(u,v);//把子节点归属到当前节点 51 vis[v]=1;//标记当前节点以及访问过了 52 } 53 } 54 for(int i=headq[u];i!=-1;i=Q[i].ne) 55 {//处理每个和它有关的询问 56 int v=Q[i].v,id=Q[i].c; 57 if(vis[v])//当有关系的节点已经被访问过,处理该条询问 58 ans[id]=dis[u]+dis[v]-2*dis[gui(v)]; 59 } 60 } 61 int main() 62 { 63 int n,m,u,v,c; 64 scanf("%d",&n); 65 init(n); 66 for(int i=1;i<n;i++) 67 { 68 scanf("%d%d%d",&u,&v,&c); 69 adds(u,v,c); 70 adds(v,u,c); 71 } 72 scanf("%d",&m); 73 for(int i=0;i<m;i++) 74 { 75 scanf("%d%d",&u,&v); 76 addq(u,v,i); 77 addq(v,u,i); 78 //询问也要建双向的,因为不确定谁先访问到 79 } 80 dfs(0,-1,0); 81 for(int i=0;i<m;i++) 82 printf("%d\n",ans[i]); 83 return 0; 84 }
为什么前面说最后对树形dp有点理解呢,因为一般不会问最近公共祖先,而是问路径,所以我们需要记录下每个节点到根节点的距离,然后两个节点的距离就是它们到根节点的距离再减去两倍的它们最近公共祖先到根节点的距离(因为这段距离在两个节点到根节点的距离中重复了),比如上图,求4到5的距离的话,我们记录的是根节点,也就是1到4和1到5的距离,然后很明显,在1到4和1到5中都包含了1到2(4和5最近公共祖先)的距离,而我们要求4到5,是不需要1到2这段的距离的。
ZOJ 3195 Design the city 传送门
题目大意也是求树上的最短路径,不过是求三个节点的,也是先上代码
1 #include<cstdio> 2 const int N=50018,M=70018; 3 struct Side{ 4 int v,c,ne; 5 }S[2*N],Q[6*M]; 6 int sn,qn,heads[N],headq[N],fa[N],vis[N],dis[N],ans[M]; 7 void init(int n) 8 { 9 sn=qn=0; 10 for(int i=0;i<=n;i++) 11 { 12 fa[i]=i; 13 dis[i]=0; 14 vis[i]=0; 15 heads[i]=headq[i]=-1; 16 } 17 } 18 void adds(int u,int v,int c) 19 { 20 S[sn].v=v; 21 S[sn].c=c; 22 S[sn].ne=heads[u]; 23 heads[u]=sn++; 24 } 25 void addq(int u,int v,int id) 26 { 27 Q[qn].v=v; 28 Q[qn].c=id; 29 Q[qn].ne=headq[u]; 30 headq[u]=qn++; 31 } 32 int gui(int x){ 33 return fa[x]==x ? x : fa[x]=gui(fa[x]); 34 } 35 void bing(int x,int y) 36 { 37 int gx=gui(x),gy=gui(y); 38 if(gx!=gy) 39 fa[gy]=gx; 40 } 41 void dfs(int u,int f) 42 { 43 for(int i=heads[u];i!=-1;i=S[i].ne) 44 { 45 int v=S[i].v; 46 if(v!=f) 47 { 48 dis[v]=dis[u]+S[i].c; 49 dfs(v,u); 50 bing(u,v); 51 vis[v]=1; 52 } 53 } 54 for(int i=headq[u];i!=-1;i=Q[i].ne) 55 { 56 int v=Q[i].v,id=Q[i].c; 57 if(vis[v]) 58 ans[id]+=dis[u]+dis[v]-2*dis[gui(v)]; 59 } 60 } 61 int main() 62 { 63 int n,m,u,v,c,is=0; 64 while(~scanf("%d",&n)) 65 { 66 if(is) 67 printf("\n"); 68 is=1; 69 init(n); 70 for(int i=1;i<n;i++) 71 { 72 scanf("%d%d%d",&u,&v,&c); 73 adds(u,v,c); 74 adds(v,u,c); 75 } 76 scanf("%d",&m); 77 int x,y,z; 78 for(int i=0;i<m;i++) 79 { 80 ans[i]=0; 81 scanf("%d%d%d",&x,&y,&z); 82 addq(x,y,i);//每两个点间都要建个询问关系 83 addq(x,z,i); 84 addq(y,x,i); 85 addq(y,z,i); 86 addq(z,x,i); 87 addq(z,y,i); 88 } 89 dfs(0,-1); 90 for(int i=0;i<m;i++) 91 printf("%d\n",ans[i]/2); 92 } 93 return 0; 94 }
其实和上题基本都一样,就是在求三个节点时,我们要分别求出两两节点间的最短距离,然后把三个结果相加起来除2就是答案。因为像a,b,c三个节点,我们求出a到b,和b到c,以及a到c的距离,所需要求的距离就会重复了一遍,比如上图求3,4,5的距离,也就是要求3到1,1到2,2到4,2到5这些边,而我们求出3到4是,3到1,1到2,2到4,求出4到5是,4到2,2到5,求出3到5就是,3到1,1到2,2到5,可以看到我们需要求的边都刚好多走了一遍。所以只要用lcd求出两两的最短路,3个加起来除2就是3个节点间的最短路。
夜深了,树上倍增明天再更、