LCA(共通の祖先)
LCA、最低共通Ancetors、および最新の共通の祖先。
ウィキペディアの定義Baiduの:「木T Uのルートの2つのノードの場合、vは、最近の共通の祖先を 表すノードX、X、VのUを満たすためにある祖先。深さとは、できるだけ大X」
LCAとは何ですか?
何人かの友人のために、プレゼンテーションのBaiduの百科事典のスタイルは非常に友好的ではない、と我々は画像が実際にLCAであるかを説明するためにここにいます。
これは木である(残留私の手を許します)
Uの場合:ジャンクション6、V:ノード11は、定義によって、私たちは簡単に自分の先祖(親)V、Uを見つけることができます。(深いから浅いため)
U(ノード6):ノード3(深さ3)、ノード2(深さ2)、ノード1(深さ1)。
V(点11):ジャンクション9(4深さ)、ノード7(深さ3)、ノード2(深さ2)、ノード1(深さ1)。
LCAの定義に基づく「公共の共通の祖先」という言葉は、uが、Vはノード2、ノード1です。前記最深ノード2の深さは、U(ノード6)LCAのV(ノード11)です。
マップビューから、U / V接合部からの経路上の全ての点へのルート祖先U / V・ノードであり、ノードが2つのパスを交換する最初のLCAのuおよびvです。 。
任意の2点のLCAの間で取得する方法?
四大アルゴリズムがあります。
-
乗算
-
Tarjan
-
RMQ(ST +オイラー配列表)
-
ツリーチェーン分割
以上、基本的な、シンプルで理解しTarjanは難しいが、容易に理解あまり一般的RMQ、木は、下のセクション(後に更新されないことを特徴と乗算法ハト絶対にありません)。
今日は、LCAを求めてRMQ(STテーブル+オイラーシーケンス)を研究しています。
RMQ(ST +オイラー配列表)要求LCA
事前知識:
-
ST表(DP)
-
DFS
-
主演する元チェーン
概要:このメソッドは、クエリ所定の2点間のオイラー所与ツリーRMQ間隔によって決定テーブルSTの配列によって構成される所定の二時LCAを取得します。
オイラーシーケンス
定義:オイラー木のシーケンスは、DFSのシーケンスの木です。二つの形態がある:1、及び各ノードのうち、シーケンスに追加されます。彼らは達すると2は、各ノードは、彼がシーケンスを追加しました。
木やその他の問題のための最初の加算オイラーシーケンスは、私たちは話していない場合。後者は、2つのLCAの問題を見つけるために使用されます。
あるいは、この木はある(私の手残基を許します):
これらの木は、ツリーオイラー配列を取得することができ、DFSました。
実際のツリーパスアクセスパス
欧拉序列:1-> 2-> 7-> 8> 7> 10-> 13-> 10-> 12-> 10-> 7> 9-> 11-> 9-> 7> 2 - > 3-> 6-> 3-> 5-> 3-> 4-> 3-> 2-> 1
(原因DFSが開始異なる方向に、オイラー全配列順序は逆であってもよいことに留意されたいです)
自然:我々はオイラーシーケンスで見つけることができますが、それは、バックトラック横断を初めて表示され、バックのuノードにuは別のポイントvにUに木からすべてのサブツリーノードツリーをトラバースする必要が初めてですU-> Vツリーパス。
これら二つの点についてLCAは、困難は木のパス上のV> U-あり、いくつかのLCAを見つけるために、そして木はLCA最も浅いパスノードでなければなりません。これはオイラーの配列中に導入することができるVにUを指すようにツリー、ポイント粒子、及び
U及びVは、第オイラーシーケンスの最初の発生に存在するオイラーの最も浅い点の位置との間に形成された位置LCA部分配列です。
(eg.对于上图树中结点6与结点11,其在欧拉序列中形成区间为11->9->7->2->3->6,深度分别为5->4->3->2->3->4,LCA即为最浅点结点2(深度2)。)
P.S. 实际实现时注意两节点第一次出现位置的大小,可能需要交换顺序。
2.ST表
原理我们暂且不讲,不了解的同学可以先将它理解为一种快速查找给定区间最大/最小值(区间RMQ问题)的算法。
在求LCA的过程中,我们所需的ST表与普通ST表略微不同。因为我们在查找最小值时还需要查找此最小值对应的节点编号,以此直接求出LCA。
此问题的解决方法也比较简单,设定一个rec[ ]数组,使其在st表结构数组st[ ]更新时同步更新,查找时比较数组st[ ]得到某一结点深度最浅并返回此结点对应数组
rec[ ]中的节点编号。
至此,我们对此算法原理研究结束。
3.代码实现
上代码!
声明部分:
1 #include <iostream> 2 #include <cstdio>//标准输入输出 3 #include <cmath>//用于ST表中求解log 4 using namespace std; 5 int n,m,s,cnt,tot;// s:根节点,cnt:链式前向星,tot:总欧拉序列长度 6 int head[1000005];//链式前向星不解释 7 int depth[1000005];//记录当前结点深度 8 int num[1000005];//记录节点第一次出现位置 9 int rec[2000005][20];//查询数组 10 int st[2000005][20];//ST表结构数组 11 int euler[1000005];//欧拉序列数组 12 //int dp[1000005];求节点深度数组 13 //int wd[1000005];求某一深度树的宽度数组
链式前向星存边:
1 struct edge 2 { 3 int nxt; 4 int to; 5 //int dis;边权值//在本示例中默认边权为1 6 }e[4000005];//建议开4倍数组 7 void add(int x,int y/*,int d*/) 8 { 9 e[++cnt].nxt=head[x]; 10 //e[cnt].dis=d; 11 e[cnt].to=y; 12 head[x]=cnt; 13 }
DFS:
1 void dfs(int x,int dep)//x为当前节点,dep为当前节点深度 2 { 3 4 num[x]=++tot;//记录x节点第一次出现位置 5 depth[tot]=dep;//对应深度 6 euler[tot]=x;//记录序列 7 //dp[x]=max(dp[x],depth[tot]); //求某一结点深度 8 //cout<<"#访问节点:"<<x<<" depth数组:"<<depth[tot]<<endl; 9 for(int i=head[x];i;i=e[i].nxt)//遍历边 10 { 11 int p=e[i].to; 12 if(num[p]==0)//p节点如未出现 13 { 14 dfs(p,dep+1);//遍历 15 euler[++tot]=x;//回溯后记录序列 16 depth[tot]=dep;//记录对应深度 17 } 18 } 19 return ; 20 }
ST表求RMQ:
1 void RMQ(int N)//N:欧拉序列长度 2 { 3 for(int j=1;j<=(int)(log((double)N)/log(2.0));j++) 4 { 5 for(int i=1;i<=N;i++) 6 { 7 if(i+(1<<j)-1<=N) 8 if(st[i][j-1]<st[i+(1<<(j-1))][j-1])//同步更新rec[ ]数组 9 st[i][j]=st[i][j-1],rec[i][j]=rec[i][j-1]; 10 else 11 st[i][j]=st[i+(1<<(j-1))][j-1],rec[i][j]=rec[i+(1<<(j-1))][j-1]; 12 } 13 } 14 } 15 16 int search(int l,int r) 17 { 18 int k=(int)(log((double)(r-l+1))/log(2.0)); 19 if(st[l][k]<st[r-(1<<k)+1][k])//比较后返回rec[ ]数组对应节点编号 20 return rec[l][k]; 21 else 22 return rec[r-(1<<k)+1][k]; 23 }
主函数:
1 int main() 2 { 3 cin>>n>>m>>s; 4 for(int i=1;i<=n-1;i++)//读边 5 { 6 int a,b; 7 scanf("%d %d",&a,&b); 8 add(a,b);//无向图正反存边 9 add(b,a); 10 } 11 dfs(s,1);//开始遍历 12 for(int i=1;i<=tot;i++)//初始化 13 { 14 st[i][0]=depth[i],rec[i][0]=euler[i]; 15 } 16 RMQ(tot);//构建ST表 17 /*//接下来是不必要部分//当初死在了这里 18 int dcnt=0,maxx=0;//dcnt:数的最大深度,maxx:数的最大宽度 19 for(int i=1;i<=n;i++) 20 { 21 wd[dp[i]]++;//统计所有深度为dp[i]的节点求出当前深度树宽度 22 } 23 for(int i=1;i<=n;i++) 24 { 25 if(wd[i]==0) 26 { 27 break; 28 } 29 dcnt++;//统计最大深度//笨方法 30 } 31 for(int i=1;i<=wcnt+1;i++) 32 { 33 maxx=max(maxx,wd[i]);//求最大宽度 34 } 35 36 */ 37 for(int i=1;i<=m;i++)//查询部分 38 { 39 int l,r,fg=0;//l:节点u,r:节点v,fg:交换标志 40 scanf("%d %d",&l,&r); 41 if(num[l]>num[r])//交换 42 { 43 swap(num[l],num[r]); 44 fg=1;//交换后标记 45 } 46 printf("%d\n",search(num[l],num[r]));//查询并输出 47 if(fg==1)//交换回来!!!!!记得交换回来!!!!!//p.s.2019/7/29模拟赛爆0 r.i.p 48 swap(num[l],num[r]); 49 } 50 return 0; 51 }
结语
LCA问题较为常见,应至少掌握一种方法。
拓展:
此种思想也可用于求树(最大)宽度/深度,树上距离,三点LCA等问题。
相关题目
洛谷 P3379 【模板】最近公共祖先(LCA)
洛谷 P3884 [JLOI2009]二叉树问题
洛谷 P4281 [AHOI2008]紧急集合 / 聚会(三点LCA)
//暂时只想到这么多·········
Q.E.D.(大雾)
p.s.
由于是萌新第一次撰写题解,在语言及思路等方面定会有不足之处,请大家多多包涵,也欢迎各位大佬指正。