求最近公共祖先(LCA)的各種算法
水一發題解。
我只是想存一下樹剖LCA的代碼......
以洛谷上的這個模板為例:P3379 【模板】最近公共祖先(LCA)
1.樸素LCA
就像做模擬題一樣,先dfs找到基本信息:每個節點的父親、深度。
把深的節點先往上跳。
深度相同了之後,一起往上跳。
最後跳到一起了就是LCA了。
預處理:O(n)
每次查詢:O(n)
2.倍增LCA
樸素LCA的一種優化。
一點一點跳,顯然太慢了。
如果要跳x次,可以把x轉換為二進制。
每一位都是1或0,也就是跳或者不跳。
在第i位,如果跳,就向上跳2(i-1)次。
至於跳或者不跳,判斷很簡單。
如果跳了之後還沒在一起,就跳。
預處理:算出每個點上跳2n
每次詢問:O(logn)
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 6 int n,m,s; 7 int hd[500005],nx[1000005],to[1000005],cnt; 8 9 void add(int af,int at) 10 { 11 to[++cnt]=at; 12 nx[cnt]=hd[af]; 13 hd[af]=cnt;倍增LCA14 } 15 16 int d[500005],f[500005][25]; 17 18 void pre(int p,int fa) 19 { 20 f[p][0]=fa; 21 d[p]=d[fa]+1; 22 for(int i=hd[p];i;i=nx[i]) 23 { 24 if(to[i]!=fa)pre(to[i],p); 25 } 26 } 27 28 int lca(int x,int y) 29 { 30 if(d[x]<d[y])swap(x,y); 31 for(int i=20;i>=0;i--) 32{ 33 if(d[f[x][i]]>=d[y])x=f[x][i]; 34 } 35 if(x==y)return x; 36 for(int i=20;i>=0;i--) 37 { 38 if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i]; 39 } 40 return f[x][0]; 41 } 42 43 int main() 44 { 45 scanf("%d%d%d",&n,&m,&s); 46 for(int i=1;i<n;i++) 47 { 48 int aa,bb; 49 scanf("%d%d",&aa,&bb); 50 add(aa,bb); 51 add(bb,aa); 52 } 53 pre(s,0); 54 for(int i=1;i<=20;i++) 55 { 56 for(int j=1;j<=n;j++) 57 { 58 f[j][i]=f[f[j][i-1]][i-1]; 59 } 60 } 61 for(int i=1;i<=m;i++) 62 { 63 int x,y; 64 scanf("%d%d",&x,&y); 65 printf("%d\n",lca(x,y)); 66 } 67 return 0; 68 }
3.歐拉序+RMQ
歐拉序,就是dfs時,無論是進入該點的子樹,還是從該點的子樹中出來,都記錄一遍這個點。這樣得到一個序列,就是歐拉序。
比如說點A為根,BCD為A的兒子的一顆簡單的樹,加上一個E作為C的兒子。
其歐拉序就是A B A C E C A D A
那麽,任取兩點,它們的LCA,就是歐拉序中,這兩個點之間深度最小的點。
如果一個點在歐拉序中出現了多次,任取一個位置就好。
區間深度最小點,用RMQ。O(nlogn)預處理後,每次詢問O(1)求出。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 6 int m,n,ecnt,root; 7 int head[500005],nx[1000005],to[1000005]; 8 int euler[1500005],eucnt,ps[1500005],high[1500005][25]; 9 int fa[500005],dep[500005]; 10 int log[1500005]; 11 12 int add(int af,int at) 13 { 14 to[++ecnt]=at; 15 nx[ecnt]=head[af]; 16 head[af]=ecnt; 17 } 18 19 void dfs(int pos,int fat) 20 { 21 dep[pos]=dep[fat]+1; 22 euler[++eucnt]=pos; 23 ps[pos]=eucnt; 24 fa[pos]=fat; 25 for(int i=head[pos];i;i=nx[i]) 26 { 27 if(to[i]!=fat) 28 { 29 dfs(to[i],pos); 30 euler[++eucnt]=pos; 31 } 32 } 33 } 34 35 void prelca() 36 { 37 for(int i=2;i<=3*n;i++)log[i]=log[i/2]+1; 38 for(int i=1;i<=eucnt;i++)high[i][0]=euler[i]; 39 for(int i=1;i<=27;i++) 40 { 41 for(int j=1;j+(1<<i)-1<=eucnt;j++) 42 { 43 if(dep[high[j][i-1]]>dep[high[j+(1<<(i-1))][i-1]]) 44 high[j][i]=high[j+(1<<(i-1))][i-1]; 45 else 46 high[j][i]=high[j][i-1]; 47 } 48 } 49 } 50 51 int lca(int x,int y) 52 { 53 int ll=ps[x]; 54 int rr=ps[y]; 55 if(ll>rr)int t=ll; ll=rr; rr=t; 56 int len=rr-ll+1; 57 if(dep[high[ll][log[len]]]>dep[high[rr-(1<<log[len])+1][log[len]]]) 58 return high[rr-(1<<log[len])+1][log[len]]; 59 else 60 return high[ll][log[len]]; 61 } 62 63 int main() 64 { 65 scanf("%d%d%d",&n,&m,&root); 66 for(int i=1;i<n;i++) 67 { 68 int a,b; 69 scanf("%d%d",&a,&b); 70 add(a,b); 71 add(b,a); 72 } 73 dfs(root,0); 74 prelca(); 75 for(int i=1;i<=m;i++) 76 { 77 int q,w; 78 scanf("%d%d",&q,&w); 79 printf("%d\n",lca(q,w)); 80 } 81 return 0; 82 }歐拉序+RMQ
4.樹鏈剖分
把樹分成輕鏈和重鏈。
先一遍dfs找到重兒子,即子樹最大的兒子。
每個點與重兒子的連邊組成重鏈。
第二遍dfs記錄每個點的tp值:所在重鏈的頂端。
如果在輕鏈上,tp就是它自己。
求LCA;類似倍增。
讓tp較深的點上跳,跳到fa[tp]。
最後tp[x]==tp[y]的時候,二者在同一重鏈上,LCA即為深度較淺的那個點。
預處理:O(n)
每次詢問:O(logn)
1 #include<cstdio> 2 3 int hd[500005],to[1000005],nx[1000005],cnt; 4 int hs[500005],tp[500005],f[500005],d[500005],sz[500005]; 5 6 int n,m,s; 7 8 void add(int af,int at) 9 { 10 to[++cnt]=at; 11 nx[cnt]=hd[af]; 12 hd[af]=cnt; 13 } 14 15 void dfs(int p,int fa) 16 { 17 f[p]=fa; 18 d[p]=d[fa]+1; 19 sz[p]=1; 20 for(int i=hd[p];i;i=nx[i]) 21 { 22 if(to[i]==fa)continue; 23 dfs(to[i],p); 24 sz[p]+=sz[to[i]]; 25 if(sz[to[i]]>sz[hs[p]])hs[p]=to[i]; 26 } 27 } 28 29 void findtp(int p) 30 { 31 if(p==hs[f[p]])tp[p]=tp[f[p]]; 32 else tp[p]=p; 33 for(int i=hd[p];i;i=nx[i]) 34 if(to[i]!=f[p])findtp(to[i]); 35 } 36 37 int lca(int a,int b) 38 { 39 while(tp[a]!=tp[b])d[tp[a]]>d[tp[b]]?a=f[tp[a]]:b=f[tp[b]]; 40 return d[a]<d[b]?a:b; 41 } 42 43 int main() 44 { 45 scanf("%d%d%d",&n,&m,&s); 46 for(int i=1;i<n;i++) 47 { 48 int x,y; 49 scanf("%d%d",&x,&y); 50 add(x,y); 51 add(y,x); 52 } 53 dfs(s,0); 54 findtp(s); 55 for(int i=1;i<=m;i++) 56 { 57 int a,b; 58 scanf("%d%d",&a,&b); 59 printf("%d\n",lca(a,b)); 60 } 61 return 0; 62 }樹鏈剖分
5.離線tarjan
(待填坑)
6.歐拉序+約束RMQ
洛谷上的玄學操作。應該是歐拉序+RMQ的優化。
把原歐拉序分塊,塊內預處理,塊間ST表。(我並不知道ST表是什麽......)
摘自洛谷題解:
分塊大小定為L=log(n)/2,這樣共分D=n/L塊,對這D個數(塊內最小值)做正常ST表,建表復雜度O(Dlog(D))=O((n/L)(log(n)-log(L))=O(n)
我們要保證每個步驟都是O(n)的,log(n)/2的塊正好消去了ST建表時的log
但在此之前,我們得處理出塊內的最小值,該怎麽做呢?一個正常想法就是枚舉每個數,一共是O(n)復雜度
但是,這樣做雖然留下了每塊的最小值以及其取到的位置,若考慮查詢塊的一個區間,而這個區間恰好取不到最小值,這時候只能暴力枚舉,就破壞了查詢O(1)了
至此我們仍沒有使用其±1的特殊性質,現在考慮一下。
塊內一共log(n)/2個數,由乘法原理可知,本質不同的塊有U=2^(log(n)/2)=n^(1/2)個,我們不妨處理出每個這種塊,復雜度Ulog(n)/2,這個函數增長是小於線性的,可以認為是O(n)
這樣,處理出每個塊內兩元素的大小關系,就可以用01唯一表示一個塊了,可以用二進制存下來,作為一個塊的特征,這一步復雜度O(n)
這樣有一個好處,即使查詢塊內一個區間,我們只需要提取這個區間對應的二進制數,就可以在預處理的數組中O(1)查詢了
(怎麽做呢?把這段二進制數提出來,移到最右邊,由於我們規定0表示小於,1表示大於,所以會貪心地選取前面的數,查表減去偏移量就可以了)
查詢時,類似分塊,邊角的塊直接查表,中間部分ST表查詢,查詢是O(1)的。
至此我們完成了O(n)建表,O(1)查詢的約束RMQ。
一般地,對於任何一個序列,可以在O(n)時間內建成一顆笛卡爾樹,把查詢該序列RMQ轉化為求笛卡爾樹LCA,就變成O(1)的了。
安利一下自己博客
找時間搞搞吧......
求最近公共祖先(LCA)的各種算法