咳咳,进入重难点的图论算法之一 : lca!(敲黑板)
题目: 洛谷 P3379
一、定义
LCA 指的是最近公共祖先(Least Connon Ancestors)。具体地,给定一棵有根树,若节点 z 既是 x 的祖先,也是节点 y 的祖先,则称 z 是 x , y 的公共祖先。在 x , y 的公共祖先中,深度最大的一个节点称为 x , y 的最近公共祖先,记为 LCA(x , y)。
我们来举个例子,如图所示:LCA(4 , 5)= 2,LCA(5 , 6)= 1,LCA(2 , 3)= 1。
二、如何求 LCA
我们考虑“暴力”要怎么实现找两点的 LCA。
举个例子,如图: 7 , 5 的 LCA 是 2。
先 DFS 一遍找出每个点的 DEP (深度)。然后先从深度大的 7 往上跳,跳到和 5 深度相同的点 4,发现还是没有到同一个点,那么 4、5 继续往上跳,直到跳到 2 位置,发现点一样了,那么 2 就是它们的 LCA 了。
三、如何优化这个方法
这里一共有两种方法:
1.离线 tarjan 求 LCA: 这里就贴一贴别人的博客吧
1 var 2 i,j,k,n,m,s,t:longint; 3 first,next,en,qfirst,qnext,qen,b,f,ans:array[0..2000001] of longint; 4 procedure add(k,x,y:longint); 5 begin 6 next[k]:=first[x]; 7 first[x]:=k; 8 en[k]:=y; 9 end; 10 procedure qadd(k,x,y:longint); 11 begin 12 qnext[k]:=qfirst[x]; 13 qfirst[x]:=k; 14 qen[k]:=y; 15 end; 16 function find(x:longint):longint; 17 begin 18 if f[x]=x then exit(x) else begin f[x]:=find(f[x]); exit(f[x]) end; 19 end; 20 procedure tarjan(x:longint); 21 var 22 j,k,t:longint; 23 begin 24 b[x]:=1; 25 t:=first[x]; 26 while t>0 do 27 begin 28 if b[en[t]]=0 then 29 begin 30 tarjan(en[t]); 31 j:=find(x); 32 k:=find(en[t]); 33 f[k]:=j; 34 end; 35 t:=next[t]; 36 end; 37 t:=qfirst[x]; 38 while t>0 do 39 begin 40 if b[qen[t]]=2 then 41 begin 42 ans[t]:=find(qen[t]); 43 if t mod 2=1 then ans[t+1]:=ans[t] 44 else ans[t-1]:=ans[t]; 45 end; 46 t:=qnext[t]; 47 end; 48 b[x]:=2; 49 end; 50 begin 51 readln(n,m,s); 52 for i:=1 to n-1 do 53 begin 54 readln(j,k); 55 add(i*2-1,j,k); 56 add(i*2,k,j); 57 end; 58 for i:=1 to m do 59 begin 60 readln(j,k); 61 qadd(i*2-1,j,k); 62 qadd(i*2,k,j); 63 end; 64 for i:=1 to n do 65 f[i]:=i; 66 tarjan(s); 67 for i:=1 to n do 68 writeln(ans[i*2]); 69 end.
2.倍增求 LCA:我们考虑这个方法慢在哪里,当然是对于每个点,一次往上跳一步,导致了效率低。那么如何优化呢?只要一次能向上跳多步,效率自然就高了。
树上倍增法
树上倍增法是一个很重要的算法。设 f [ x , k ] 表示 x 的 2 k 辈祖先,即从 x 向根节点走 2 k 步到达的节点。特别地,若该节点不存在,则令 f [ x , k ] = 0 。f [ x , 0 ] 就是 x 的父节点。
因为 x 向根节点走 2 k 步 ⇔ 向根走 2 k-1 步,再走 2 k-1 步(2 k = 2 k-1 + 2 k-1)。所以对于 k ∈ [ 1 , log n ],有 f [ x , k ] = f [ f [ x , k-1 ] , k-1 ]。
这类似于一个动态规划的过程,“阶段”就是节点的深度。因此,我们可以对树进行遍历,由此得到 f [ x , 0 ] ,再计算 f 数组的所有值。
以上部分是预处理,时间复杂度为 O(n log n)。之后可以多次对不同的 x , y 计算 LCA,每次询问的时间复杂度为 O(log n)。
然后基于 f 数组计算 LCA(x , y)分为以下几步:
① 设 dep [ x ] 表示 x 的深度。不妨设 dep [ x ] ≥ dep [ y ](否则,可交换 x , y)。
② 用二进制拆分思想,把 x 向上调整到与 y 同一深度。
具体来说,就是依次尝试从 x 向上走 k = 2 log n……2 1,2 0 步,若到达的节点比 y 深,则令 x = f [ x , k ] 。
③ 若此时 x = y,说明已经找到了 LCA,LCA 就等于 y 。
④ 若此时 x ≠ y,那么 x , y 就继续往上跳,用二进制拆分思想,把 x , y 同时向上调整,并保持深度一致且两者不相会。
具体来说,就是依次尝试把 x , y 同时向上走 k = 2 log n……2 1,2 0 步,若 f [ x , k ] ≠ f [ y , k ](即仍未相会),则令 x = f [ x , k ],y = f [ y , k ] 。
⑤ 此时 x , y 必定只差一步就相会了,所以它们的父节点 f [ x , 0 ](或 f [ y , 0 ]) 就是它们的 LCA !
【代码实现】
预处理:
void deal_first(int u,int father) { dep[u]=dep[father]+1; for (int i=0;i<=19;i++) f[u][i+1]=f[f[u][i]][i]; for (int e=first[u];e;e=next[e]) { int v=en[e]; if (v==father) continue; f[v][0]=u; // v 向上跳 20=1 就是 u deal_first(v,u); } }
查询 x , y 的 LCA:
int lca(int x,int y) { if (dep[x]<dep[y]) swap(x,y); //让 x 的深度较大 //我们用“暴力”的思想:先将 x,y 跳到一个深度,然后一起往上跳 for (int i=20;i>=0;i--) //一定要倒着 for { if (dep[f[x][i]]>=dep[y]) x=f[x][i]; //先跳到同一层 if (x==y) return x; } for (int i=20;i>=0;i--) //此时 x,y 已跳到同一层 if (f[x][i]!=f[y][i]) //如果 f[x][i]和 f[y][i] 不同才跳 { x=f[x][i]; y=f[y][i]; } return f[x][0];
// x,y 是深度最浅且不同的点,即 lca 的子节点 }
然后附完整的 Pascal 标程(以前写的,步骤排列不太一样,但总体思路是差不多的):
1 var 2 rmq:array[0..1000001,0..21] of longint; 3 first,next,en,one,b:array[0..2000001] of longint; 4 deep,a:array[0..10000001] of longint; 5 i,j,k,m,n,s,t,l,r:longint; 6 procedure add(k,x,y:longint); 7 begin 8 next[k]:=first[x]; 9 first[x]:=k; 10 en[k]:=y; 11 end; 12 procedure getrmq; 13 begin 14 for i:=1 to k do 15 rmq[i,0]:=i; 16 for j:=1 to 20 do 17 for i:=1 to k do 18 if i+(1<<j)-1<=k then 19 if deep[rmq[i,j-1]]<deep[rmq[i+(1<<(j-1)),j-1]] 20 then rmq[i,j]:=rmq[i,j-1] 21 else rmq[i,j]:=rmq[i+(1<<(j-1)),j-1]; 22 end; 23 function que:longint; 24 begin 25 t:=trunc(ln(r-l+1)/ln(2)); 26 if deep[rmq[l,t]]<deep[rmq[r-(1<<t)+1,t]] then exit(a[rmq[l,t]]) 27 else exit(a[rmq[r-(1<<t)+1,t]]); 28 end; 29 procedure dfs(x,y:longint); 30 var 31 t:longint; 32 begin 33 inc(k); 34 deep[k]:=y; 35 b[x]:=1; 36 a[k]:=x; 37 t:=first[x]; 38 while t>0 do 39 begin 40 if b[en[t]]=0 then 41 begin 42 dfs(en[t],y+1); 43 inc(k); 44 a[k]:=x; 45 deep[k]:=y; 46 end; 47 t:=next[t]; 48 end; 49 b[x]:=0; 50 end; 51 begin 52 readln(n,m,s); 53 for i:=1 to n-1 do 54 begin 55 readln(j,k); 56 add(i*2-1,j,k); 57 add(i*2,k,j); 58 end; 59 k:=0; 60 dfs(s,1); 61 for i:=1 to k do 62 if b[a[i]]=0 then 63 begin 64 one[a[i]]:=i; 65 b[a[i]]:=1; 66 end; 67 getrmq; 68 for i:=1 to m do 69 begin 70 readln(j,k); 71 l:=one[j]; 72 r:=one[k]; 73 if l>r then 74 begin 75 j:=l; 76 l:=r; 77 r:=j; 78 end; 79 writeln(que); 80 end; 81 end.
四、习题
1 . 洛谷 P3258:松鼠的新家(LCA+树上差分,当然也可以用树链剖分做)
1 #include<cstdio> 2 #include<algorithm> 3 #include<cmath> 4 5 using namespace std; 6 7 int n,x,y,tot,s,t; 8 int first[1000005],next[1000005],en[1000005]; 9 int ans[300005],dep[300005],f[300005][21],a[300005]; 10 11 void deal_first(int u,int father) 12 { 13 dep[u]=dep[father]+1; 14 for (int i=0;i<=19;i++) 15 f[u][i+1]=f[f[u][i]][i]; 16 for (int e=first[u];e;e=next[e]) 17 { 18 int v=en[e]; 19 if (v==father) continue; 20 f[v][0]=u; 21 deal_first(v,u); 22 } 23 } 24 25 int lca(int x,int y) 26 { 27 if (dep[x]<dep[y]) swap(x,y); 28 for (int i=20;i>=0;i--) 29 { 30 if (dep[f[x][i]]>=dep[y]) x=f[x][i]; 31 if (x==y) return x; 32 } 33 for (int i=20;i>=0;i--) 34 if (f[x][i]!=f[y][i]) 35 { 36 x=f[x][i]; 37 y=f[y][i]; 38 } 39 return f[x][0]; 40 } 41 42 void add(int x,int y) 43 { 44 next[++tot]=first[x]; 45 first[x]=tot; 46 en[tot]=y; 47 } 48 49 void updata(int u,int father) 50 { 51 for (int e=first[u];e;e=next[e]) 52 { 53 int v=en[e]; 54 if (v==father) continue; 55 updata(v,u); 56 ans[u]+=ans[v]; 57 } 58 } 59 60 int main() 61 { 62 scanf("%d ",&n); 63 for (int i=1;i<=n;i++) 64 scanf("%d",&a[i]); 65 for (int i=1;i<n;i++) 66 { 67 scanf("%d%d",&x,&y); 68 add(x,y); 69 add(y,x); 70 } 71 deal_first(1,0); 72 for (int i=1;i<n;i++) 73 { 74 int LCA=lca(a[i],a[i+1]); 75 ans[a[i]]++; 76 ans[a[i+1]]++; 77 ans[LCA]--; 78 ans[f[LCA][0]]--; 79 } 80 updata(1,0); 81 82 //每条路的终点会同时作为下条路的起点重复+1 83 //最后的终点不会重复,但题目说不要+1 84 //于是每条路的终点我们都加多了一次 85 //所以现在将每条路的终点都-1 86 for (int i=2;i<=n;i++) 87 ans[a[i]]--; 88 89 for (int i=1;i<=n;i++) 90 printf("%d ",ans[i]); 91 return 0; 92 }
以上内容部分借鉴于:《信息学奥赛一本通 · 提高篇(第一版)》