zoukankan      html  css  js  c++  java
  • 最近公共祖先算法LCA笔记(树上倍增法)

    Update:
    2019.7.15更新
    万分感谢[宁信]大佬,认认真真地审核了本文章,指出了超过五处错误捂脸,太尴尬了.
    万分感谢[宁信]大佬,认认真真地审核了本文章,指出了超过五处错误捂脸,太尴尬了.
    万分感谢[宁信]大佬,认认真真地审核了本文章,指出了超过五处错误捂脸,太尴尬了.
    重要事情说三遍!!!!!

    2019.7.16更新
    笔记再次完善,感谢[Ichinose]大佬提出的好问题,并且修改了代码部分的错误注释.
    笔记再次完善,感谢[Ichinose]大佬提出的好问题,并且修改了代码部分的错误注释.
    笔记再次完善,感谢[Ichinose]大佬提出的好问题,并且修改了代码部分的错误注释.
    重要事情说三遍

    2019.8.7更新
    感谢QQ大佬,wust-pyoxiao大佬指出问题。
    感谢QQ大佬,wust-pyoxiao大佬指出问题。
    感谢QQ大佬,wust-pyoxiao大佬指出问题。
    重要事情说三遍!!!!!

    更好的阅读体验,新博客开通,各位大佬收藏点赞评价好不好啊

    最近公共祖先

    概念定义

    对于节点(x,y)而言,如果说(z)节点既是(x)的祖先节点,也是(y)的祖先节点.

    那么我们认为(z)节点是((x,y))的公共祖先节点.

    公共祖先节点有很多,那么深度最大的节点,被称之为最近公共祖先.记为LCA(x,y)

    你也可以认为(LCA(x,y))节点,是所有公共祖先节点中,离着(x,y)两个点距离最近.

    最近公共祖先.png

    在这张图片上面,我们举几个例子.

    [LCA(2,4)=2 \\ LCA(6,7)=3 \\ LCA(5,6)=1 \\ LCA(2,6)=1 \\ ]

    树上倍增法

    初始思维

    树上倍增算法,是一个非常重要的算法,一般来说树上的问题,很多时候都会运用到树上倍增的算法思想.

    我们知道,暴力算法,是一步,一步,一步非常踏实的算法.

    但是我们知道,一步步走抵达终点太慢了,我们不得不学会连蹦带跳.

    每一次都比上一次多跳一倍的格子.

    我们发现,两个点的最近公共祖先,很多时候离他们很远.

    换一张升级版本的图片

    最近公共祖先2.png

    我们发现

    [Lca(10,13)=1 ]

    他们的公共祖先离他们比较远.

    我们可以分析一下.

    [dis(10,1)=3 ]

    这个式子的意思是.

    [节点10和节点1,他们之间的距离是3. ]

    也就是

    [dis(a,b)表示为节点a和节点b他们在树上的距离. ]

    假如说我们要用暴力方法的话.我们需要走三步,走一步要一格.

    但是如果我们连蹦带跳的话.

    第一跳,我们走格.

    第二跳,我们走格.

    我们惊奇地我们发现,我们只需要跳两次了.


    倍增思想

    既然如此的话,我们发现任意一个数字,都可以被划分成下面这个公式.

    [N=2^{p_1}+2^{p_2}+2^{p_3}+...+2^{p_k} ]

    这就是我们的二进制划分的思想,任何一个数字都可以被二进制划分.

    也可以这么理解,我们知道一个数有它的十进制表达,也有它的二进制表达.

    我们所谓的划分,就是将一个十进制数,转换为二进制表达.

    再举一个例子.

    [‭(3226)\_{10}=(‭110010011010‬)_{2} ]

    我们可以这么认为.

    [3226=2^1+2^3+2^4+2^7+2^{10}+2^{11} ]

    二进制表示下,计数位置从0开始.

    之前有富有学习经验的大佬说代码部分.

    	for(int k=lg[deep[x]]-1; k>=0; k--) //从大到小,枚举我们所需要的长度.2^(log(deep[x]))~1
    		if(fa[x][k]!=fa[y][k])//如果发现x,y节点还没有上升到最近公共祖先节点
    		{
    			x=fa[x][k];//当然要跳跃
    			y=fa[y][k];//当然要跳跃
    		}
    

    部分不利于初学者们理解.那么我们来认认真真地解析一下.

    1. 为什么要倒着循环,而且正着循环会出问题

    我们来看一组样例.多么的250

    [250=128+64+32+16+8+2 \\ 250=2+8+16+32+64+128 ]

    我们将他们放到树上.也就是节点a离着最近公共祖先有(250)个距离.

    假如说我们是顺着循环走的.我们必然走不到终点.

    我们刚开始,走了(2^0)格,我们发现满足条件,于是我们走了(2^0)个格子.

    我们然后,走了(2^1)格,我们发现也满足条件,((a,b))节点还是没有相遇,于是我们走了(2^1)个格子

    我们接着,走(2^2)格子,我们惊奇地发现,也是满足条件的,于是我们走了(2^2)个格子.

    不停地走啊走,我们永远都走不到终点.

    [1+2+4+8+16+32+64+128=255 \\ 250=1+2+4+8+16+32+64+64+32+16+8+2+1 \\ 但是如果顺着循环走 \ 1+2+4+8+16+32+64 quad 然后在128开始,就走不了一个格子了 ]

    因为抵达终点的路径,必须是二进制拆分下的路径.

    接下来我们分析一下为什么我们最后一遍循环完后,不是是Lca节点,而且Lca节点的儿子节点.

    if(fa[x][k]!=fa[y][k]) //如果不相遇
    	x=fa[x][k],y=fa[y][k];//我们才会跳跃.
    那么我们实际能跳跃到的节点们,其实也就是a节点到Lca节点的儿子节点这一段上面的节点.
    Lca节点,肯定fa[x][k]=fa[y][k].
    但是我们fa[x][k]!=fa[y][k],所以Lca节点不能抵达.
    但是Lca节点的儿子节点,肯定fa[x][k]!=fa[y][k].
    所以Lca节点的儿子节点,是我们能够跳跃的最大距离了.
    

    倍增数组

    既然如此的话,我们不妨设置

    [F[x][k]表示为x向上走(根节点)走2^k步,抵达的节点 \\ 若不存在该节点 F[x][k]=0 \\ F[x][0]表示为该节点的父节点 \ ]

    我们知道二的幂次,是具有一个数学性质的.

    [2^k=2^{k-1}+2^{k-1} quad 提取公因式\\ 2^k=2*2^{k-1} quad 提取底数2\\ 2^k=2^{k} quad 最终得到性质\\ ]

    或者你可以这么认为.

    [2^k=2^{k} quad 写出恒等式 \\ 2^k=2^{1}*2^{k-1} quad 指数分解一下 \\ 2^k=2^{k-1}+2^{k-1} quad 乘法变成加法 \\ ]

    我们将这个数学性质,带入到我们的倍增数组,就会发现一个转移方程.

    [f[x][k]=f[f[x][k-1]][k-1] \\ f[x][k-1]表示x向上爬2^{k-1}个节点 \\ 那么f[f[x][k-1]][k-1]表示为x向上爬2^{k-1}个节点,再向上爬2^{k-1}个节点 \\ x往上爬2^{k-1},然后再往上爬2^{k-1}个节点. \\ x+2^{k-1}+2^{k-1}=x+2^{k} \\ ]

    倍增数组就这么迅速地解决了!

    算法流程

    我们知道LCA(x,y)表示为两个节点的公共祖先.

    也就是我们知道节点(x),和节点(y)总会在一个节点相遇.

    也就是经过一系列跳跃过后的节点(x),和节点(y)深度必须是相同的.

    1. 节点x必须和节点y在同一深度
      根据这个条件,我们刚开始,显然深度更加深的节点(在下面的节点),跳跃到和另外一个节点(在上面的节点),一样的深度.

    [d[x]表示节点x的深度 \\ d[y]表示节点y的深度 \\ ]

    我们不妨认为,节点(x)深度更加深,是属于下面的节点.
    如果x在上面,我们就交换x,y即可,反正要使得.

    [d[x]>=d[y] ]

    1. 利用二进制划分,使得节点(x)向上调整到,和节点(y)的同一深度.
      也就是不停地尝试让节点(x)往上走k步.

    [k=2^{log_n},..,2^1,2^0 ]

    如果说我们发现,节点(x)往上走(k)步,还是在(y)下面.

    [x=F[x][k] quad 还没有抵达同一高度,我们还需要往上走 ]

    1. 如果说上调的过程中,发现(x=y),说明LCA找到了.
      往上面看图片,你可以认为是节点2,和节点4的情况.节点2是节点4的父亲节点.
    2. (x,y)节点他们的深度一致的时候,两个节点都向上跳跃同样高度,并且需要保证两个节点不相遇
      为什么要跳跃同一高度?
      之前我们就说了,两个节点必须保证同一高度.
      为什么要保证两个节点不相遇,题目不是要我们找到最近公共祖先吗?
      这是为了保证最近这个性质.
      我们发现满足两个节点不相遇的,深度最浅的两个节点.也就是在最近公共祖先节点下面,离最近公共祖先节点,最近的节点.
      就是最近公共祖先节点的两个儿子节点.
      那么这两个儿子节点,他们的父亲节点,就必然是最近公共祖先节点.
      怎么向上跳跃?其实和之前跳跃是一样的.
      也就是不停地尝试让节点(x,y)往上走k步.

    [假如说F[x][k]!=F[y][k] quad 也就是没有相遇 \\ x=F[x][k] quad 还没有抵达同一高度,我们还需要往上走 \\ y=F[y][k] quad 此时x,y节点还没有相遇.也需要往上走 \\ ]

    1. 然后最后我们输出(F[x][0]),也就是x节点的父节点,我们的最近公共祖先.

    代码解析

    #include <bits/stdc++.h>
    using namespace std;
    int n,m,s,x,y,tot=0;
    const int N=500005,M=1000005;//N存储节点总数,M存储边的总数
    int head[N],edge[M],Next[M];
    int deep[N],fa[N][22],lg[N];
    //deep[i]是i号节点的深度
    //lg是log数组
    void add(int x,int y)//链式前项星加边
    {
    	edge[++tot]=y;//存储节点
    	Next[tot]=head[x];//链表
    	head[x]=tot;//标记节点位置
    	return ;
    }
    void dfs(int x,int y)
    {
    	deep[x]=deep[y]+1;//x是y的儿子节点,所以要+1
    	fa[x][0]=y;//fa[x][0]表示x的父亲节点,而y是x的父亲节点.
    	for(int i=1; (1<<i)<=deep[x]; i++) //2^i<=deep[x]表示不能跳出去了,最多跳到根节点上面
    		fa[x][i]=fa[fa[x][i-1]][i-1];//状态转移 2^i=2^(i-1)+2^(i-1)
    	for(int i=head[x]; i; i=Next[i]) //遍历所有的出边
    		if(edge[i]!=y)//因为是无向图,所以要避免回到父亲节点上面去了
    			dfs(edge[i],x);//访问儿子节点,并且标记自己是父亲节点
    	return ;//返回
    }
    int LCA(int x,int y)
    {
    	if(deep[x]<deep[y])//强制要求x节点是在下方的节点
    		swap(x,y);//交换,维持性质
    	while(deep[x]>deep[y])//当我们还没有使得节点同样深度
    		x=fa[x][lg[deep[x]-deep[y]]-1];//往上面跳跃,deep[x]-deep[y]是高度差.-1是因为lg数组是Log值大1
    	if(x==y)//发现Lca(x,y)=y
    		return x;//返回吧,找到了...
    	for(int k=lg[deep[x]]-1; k>=0; k--) //从大到小,枚举我们所需要的长度.2^(log(deep[x]))~1
    		if(fa[x][k]!=fa[y][k])//如果发现x,y节点还没有上升到最近公共祖先节点
    		{
    			x=fa[x][k];//当然要跳跃
    			y=fa[y][k];//当然要跳跃
    		}
    	return fa[x][0];//必须返回x的父亲节点,也就是Lca(x,y)
    }
    int main()
    {
    	scanf("%d%d%d",&n,&m,&s);//n个节点,m次询问,s为根节点
    	for(int i=1; i<n; i++) //n-1条边
    	{
    		scanf("%d%d",&x,&y);//读入边
    		add(x,y);//建立边
    		add(y,x);//建立无向图
    	}
    	dfs(s,0);//从根节点,开始建立节点之间的跳跃关系,根节点的父亲节点没有,故选择0
    	for(int i=1; i<=n; i++)
    		lg[i]=lg[i-1]+(1<<lg[i-1]==i);//处理log数组的关系,lg[x]=log(x)+1,请记得最后使用要-1
    	for(int i=1; i<=m; i++)
    	{
    		scanf("%d%d",&x,&y);//读入需要查询的节点
    		printf("%d
    ",LCA(x,y));//输出查询的结果
    	}
    	return 0;
    }
    

    例题

    题目描述

    原题连接

    Y岛风景美丽宜人,气候温和,物产丰富。

    Y岛上有N个城市(编号(1,2,…,N)),有(N-1)条城市间的道路连接着它们。

    每一条道路都连接某两个城市。

    幸运的是,小可可通过这些道路可以走遍Y岛的所有城市。

    神奇的是,乘车经过每条道路所需要的费用都是一样的。

    小可可,小卡卡和小YY经常想聚会,每次聚会,他们都会选择一个城市,使得3个人到达这个城市的总费用最小。

    由于他们计划中还会有很多次聚会,每次都选择一个地点是很烦人的事情,所以他们决定把这件事情交给你来完成。

    他们会提供给你地图以及若干次聚会前他们所处的位置,希望你为他们的每一次聚会选择一个合适的地点。

    输入格式

    第一行两个正整数,(N)(M),分别表示城市个数和聚会次数。

    后面有(N-1)行,每行用两个正整数(A)(B)表示编号为(A)和编号为(B)的城市之间有一条路。

    再后面有(M)行,每行用三个正整数表示一次聚会的情况:小可可所在的城市编号,小卡卡所在的城市编号以及小YY所在的城市编号。

    输出格式

    一共有(M)行,每行两个数(Pos)(Cost),用一个空格隔开,表示第(i)次聚会的地点选择在编号为(Pos)的城市,总共的费用是经过(Cost)条道路所花费的费用。

    数据范围

    [N le 500000 \\ M le 500000 \\ ]

    输入样例:

    6 4
    1 2
    2 3
    2 4
    4 5
    5 6
    4 5 6
    6 3 1
    2 4 4
    6 6 6
    

    输出样例:

    5 2
    2 5
    4 1
    6 0
    

    解题报告

    题意理解

    不同于一般的LCA题目,这道题目是,在一棵(n-1)条边的树上,有三个节点,要你求出这个三个点抵达一个汇聚点最少代价.


    算法解析

    这道题目的核心点,就是它是由三个点构成的最短路.

    为什么,它同于一般的题目,难道不是让我们直接求出三个点的最近公共祖先?

    汇聚点为什么不是

    [Lca(Lca(a,b),Lca(a,c)) \\ 或者 \\ Lca(Lca(a,c),Lca(b,c)) \\ 以上选项二选一 ]

    如果你真的是这么想,脑海里面只有A,B选项,那么你应该庆幸,出题人比较良心丧心病狂留下的唯一良知,他给你提出了一个样例,告诉你为什么不是这样.

    因为文化课考试的时候,题目都是A,B,C或者再来一个D的单项选择题.

    聚会1.png

    (3)人分别在(4,5,6)三个节点上面.

    仔仔细细地观察一下,我们发现这道题目的汇聚点,应该是5,而不是4.

    1. 假如说我们按照楼上这个错误思路,我们的三点的最近公共祖先节点,应该是4.
    2. 但是最少花费,显然是在(5)号节点.

    我们的思路居然是错误的!!!

    它到底错误在了哪里.

    我们要分析一下,这道题目,为什么选择的是5,而不是4?

    选择(4),那么(1)号小朋友不需要行动.

    选择(5),那么(2,3)号小朋友汇聚在点(5)后,不需要行动.


    我们可以这么现实化这道题目.

    (2,3)号小朋友他们是互相的知己一对狗男女,所以说,他们想要在一起.发朋友圈,秀恩爱

    所以(2,3)号小朋友他们会先聚集在一起

    花费代价为

    [消耗距离=deep[b]+deep[c]-2 imes deep[Lca(b,c)] ]

    聚会.png

    此时我们面临两大选择.

    1. (1)号同学孤身一人走到2,3号同学相遇的地方.
    2. (2,3)号同学一起手拉手(1)号同学相遇.再秀一次恩爱,虐一下单身狗1号

    假如说(1)号同学,与(2,3)号同学相隔(L)个距离.

    我们将会发现,两大选择,会产生两大代价.

    然而显然一号选择是最好不过的了.
    同样的距离,一个人是一个人走,一个是两个人走,那么必然一个人走消耗卡路里更少.

    那么2,3号汇聚的话,他们会花费

    [消耗=deep[y]+deep[z]-2 imes deep[Lca(y,z)] ]

    之后一号过来找他们,距离消耗了:

    [deep[x]-deep[lca(x,lca(y,z))] ]

    这时候一号到了((x,y,z))公共祖先,然后一号朝(Lca(y,z))

    [deep[Lca(y,z)]-deep[Lca(x,Lca(y,z))] ]

    那么消耗总值就是

    [消耗总值=deep[y]+deep[z]-deep[Lca(y,z)]+deep[x]-2*deep[lca(x,Lca(y,z))] ]

    1. (1,2)先在一起
    2. (2,3)先在一起
    3. (1,3)先在一起

    代码解析

    #include <bits/stdc++.h>
    using namespace std;
    const int N=500000+200,M=500000*2+100;
    int n,m,s,lg[N],deep[N];
    struct Lca
    {
    	int head[M],Next[M],edge[M],tot,fa[N][22];
    	void init()
    	{
    		memset(head,0,sizeof(head));
    		tot=0;
    	}
    	void add_edge(int a,int b)
    	{
    		edge[++tot]=b;
    		Next[tot]=head[a];
    		head[a]=tot;
    		return ;
    	}
    	void dfs(int x,int y)
    	{
    		deep[x]=deep[y]+1;
    		fa[x][0]=y;
    		for(int i=1; (1<<i)<=deep[x]; i++)
    			fa[x][i]=fa[fa[x][i-1]][i-1];
    		for(int i=head[x]; i; i=Next[i])
    			if (edge[i]!=y)
    				dfs(edge[i],x);
    		return ;
    	}
    	int LCA(int x,int y)
    	{
    		if (deep[x]<deep[y])
    			swap(x,y);
    		while(deep[x]>deep[y])
    			x=fa[x][lg[deep[x]-deep[y]]-1];
    		if (x==y)
    			return x;
    		for(int k=lg[deep[x]]-1; k>=0; k--)
    			if (fa[x][k]!=fa[y][k])
    			{
    				x=fa[x][k];
    				y=fa[y][k];
    			}
    		return fa[x][0];
    	}
    } g1;
    int main()
    {
    	scanf("%d%d",&n,&m);
    	g1.init();
    	for(int i=1; i<n; i++)
    	{
    		int a,b;
    		scanf("%d%d",&a,&b);
    		g1.add_edge(a,b);
    		g1.add_edge(b,a);
    	}
    	g1.dfs(1,0);
    	for(int i=1; i<=n; i++)
    		lg[i]=lg[i-1]+(1<<lg[i-1]==i);
    	for(int i=1; i<=m; i++)
    	{
    		int x,y,z,c_x,c_y,c_z,dx,dy,dz;
    		scanf("%d%d%d",&x,&y,&z);
    		c_x=g1.LCA(x,y),dx=deep[x]+deep[y]-deep[c_x]+deep[z]-2*deep[g1.LCA(z,c_x)];
    		c_y=g1.LCA(y,z),dy=deep[y]+deep[z]-deep[c_y]+deep[x]-2*deep[g1.LCA(x,c_y)];
    		c_z=g1.LCA(x,z),dz=deep[x]+deep[z]-deep[c_z]+deep[y]-2*deep[g1.LCA(y,c_z)];
    		if(dx>dy) 
    		    dx=dy,c_x=c_y;
    		if(dx>dz) 
    		    dx=dz,c_x=c_z;
    		printf("%d %d
    ",c_x,dx);
    	}
    	return 0;
    }
    
  • 相关阅读:
    1.1 java变量及数据类型
    3. 软件测试的类型
    2.3 软件测试模型之 敏捷测试
    2.2 软件测试的手段
    2.1 软件测试的阶段
    1.1 软件测试基础概念
    浅谈内联元素inline
    微信内置浏览器清除缓存的方法
    我的package.json清单
    我的gulp.js清单
  • 原文地址:https://www.cnblogs.com/gzh-red/p/11180856.html
Copyright © 2011-2022 走看看