LCA(least common ancestors)最近公共祖先
指的就是對於一棵有根樹,若結點z既是x的祖先,也是y的祖先,那么z就是結點x和y的最近公共祖先。
定義到此。
那么怎么求LCA?
對於朴素思想,就是我要一步一步往上爬,一步一步走。先把結點x和y整到同一深度,然后再一次一個深度的往上查,直到祖先一樣才break(明顯是個while)
但是,一步一步實在是太慢了,所以不能腳踏實地地走
那么,考慮跳着走,
跳着走的條件就是要滿足一步步數盡可能多並且不跳過了。當然你跳過了在一步一步往下找也行啊QWQ
於是,在滿足這兩個條件的情況下,我們有了LCA算法:
一步跳2的n次方。
對於每一步跳躍,我們要預處理一個二維數組f(father)f[x][i],表示對於當前的x結點,往上跳2^i個祖先,特別的,像數學中的,f[x][0]就是x的一代祖先。於是我們要用一個dfs預處理來解決這個f數組。后面我們會提到;
處理完f數組,那就很簡單了。
輸入x和y,表示要求結點x和結點y的LCA,那么我們開始求LCA:
對於x和y在不同高度,我們先把他們拉到一個高度,同樣不能一步一步走,也要用到f數組。
這里我們要提前了解到一個定理:對於任意一個非零整數,我們都可以將他用2的次冪表示出來。也就是such as : 11=2^3+2^1+2^0。這倒也不用證明,就像每個數都可以用二進制表示的原理。
接着講:在把x和y弄到同一高度時,我們要先做的是設x的深度dep要比y的深度dep要大,如果dep[y]>dep[x],那么把他們交換。原因是如果不交換,那么我們需要判斷兩種情況,用兩個if以及幾乎相同的代碼(亂七八糟還占內存),對於非常懶的我們,自然不會這么干。
然后用一個判斷 if(dep[f[x][i]]>=dep[y])x=f[x][i];(此時x深度大於y),在這里用外層for循環來枚舉i,但是i一定要從大到小!。為什么?
安利一個大家都聽爛了故事:一個玻璃瓶,裝了幾塊石頭,滿到瓶口。滿了嗎?沒有。又裝了一些沙子,滿到瓶口。滿了嗎?沒有。最后又裝滿了水,滿到瓶口。終於滿了。
那么,類比於x和y的深度的距離,這個瓶子的容積也是同樣道理。從2的盡可能大的次冪去找,一旦能找到能接近他們的i,就更新dep[x],直到相等。類似於無限逼近,最后值相等的過程。如果i相等了,就說明他們的深度已經在同一層了。
與倍增到同一深度的過程相比,倍增找公共祖先也是類似的,但是有一點不同,就是你不知道什么時候找到他們的公共祖先,因此就沒有查找的上限,那么就讓他們盡可能接近。因此條件改成了這個:
if(f[x][i]!=f[y][i])
{
x=f[x][i];y=f[y][i];
}
在執行完這個語句后,我們成功讓x和y變成了某一節點z的左右兒子!
因為左右兒子是最接近但是又不相等的。
那么我們隨便取其中一個找爸爸,就找到了LCA。
AC代碼:
有關鏈式存圖不懂的的點這里。
#include<cstdio> #include<iostream> using namespace std; int n,m,s,num=0,head[1000001],dep[1000001],f[1000001][23]; int a1,a2; struct edg{ int next,to; }edge[1000001]; void edge_add(int u,int v)//鏈式前向星存圖 { num++; edge[num].next=head[u];edge[num].to=v;head[u]=num; edge[++num].next=head[v];edge[num].to=u;head[v]=num; } void dfs(int u,int father)//對應深搜預處理f數組 { dep[u]=dep[father]+1; for(int i=1;(1<<i)<=dep[u];i++) { f[u][i]=f[f[u][i-1]][i-1]; } for(int i=head[u];i;i=edge[i].next) { int v=edge[i].to; if(v==father)continue;//雙向圖需要判斷是不是父親節點 f[v][0]=u; dfs(v,u); } } int lca(int x,int y) { if(dep[x]<dep[y])swap(x,y); for(int i=20;i>=0;i--)//從大到小枚舉使x和y到了同一層 { if(dep[f[x][i]]>=dep[y])x=f[x][i]; if(x==y)return x; } for(int i=20;i>=0;i--)//從大到小枚舉 { if(f[x][i]!=f[y][i])//盡可能接近 { x=f[x][i];y=f[y][i]; } } return f[x][0];//隨便找一個**輸出 } int main(){ scanf("%d%d%d",&n,&m,&s); for(int i=1;i<n;i++) { scanf("%d",&a1);scanf("%d",&a2); edge_add(a1,a2);//鏈式存邊 } dfs(s,0); for(int i=1;i<=m;i++) { scanf("%d %d",&a1,&a2); printf("%d\n",lca(a1,a2));//求兩個節點的LCA } }
完結撒花✿ヽ(°▽°)ノ✿有疑問或者有可以改進講解的地方可以指出。