日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

LCA总结

發布時間:2023/12/3 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 LCA总结 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

    • LCA介紹
    • 解決方法概括:
    • 倍增法:
    • Tarjan
    • 歐拉序
    • 樹剖解法:

看了很多關于LCA的文章,這里是一個總結

我們通過這個題洛谷P3379 【模板】最近公共祖先來講LCA

LCA介紹

lca是啥?最近公共祖先
就是:兩個點在這棵樹上距離最近的公共祖先節點
LCA(x,y)=z,z是x與y的最近公共祖先,(換句話說z的深度要盡可能大)
來看一個經典圖

LCA(4,5)=2
LCA(4,3)=1
LCA(2,1)=1

解決方法概括:

常用四種方法 :

  • 用倍增法求解,預處理復雜度是 O(nlogn) ,每次詢問的復雜度是 O(logn), 屬于在線解法。
  • 利用歐拉序轉化為RMQ問題,用ST表求解RMQ問題,預處理復雜度 O(n+nlogn),每次詢問的復雜度為 O(1),也是在線算法。
  • 采用Tarjan算法求解,復雜度 O(α(n)+Q),屬于離線算法。
  • 利用樹鏈剖分求解,復雜度預處理O(n),單次查詢 O(logn) ,屬于在線算法。
  • 倍增法:

    倍增:將兩個點調到一個高度之后不斷同時向上調到兩點重合,即為兩點的最近公共祖先
    所用到的函數: grand[x][i] ,這個數組表示標號為x節點向上跳2^i步的節點
    例如grand[5][0]=2(上圖), 節點5向上跳20次(1次)到達節點2
    grand[5][0]就是x的父節點
    grand[x][1]就是x的父親節點的父親節點,就是grand[grand[x][0]][0]
    這樣就能得到一個遞推式grand [ x ] [ i ] = grand [ grand [ x ] [ i-1 ] ] [ i-1 ]
    先讓x與y處于同一層,然后一起往上跳
    跳多少呢?
    比如dep[u]>dep[v]
    u要向上爬h=dep[u]-dep[v],才能和v相同深度
    將h進行二進制拆分,比如
    h=(15)10=(1111)2
    h=(5)10=(101)2
    從低位開始i=0,如果是這一位是1,就grand[u][i]
    任何調動次數都可以用2的指數冪之和來表示O(log n)
    h = 5 = 22 + 20
    h = 15 = 23 + 22 + 21 + 20
    dep[]表示節點的深度
    一開始跳log2dep[x]-dep[y],log我們可以打表預處理。

    lg[0]=-1; for(int i=1;i<=n;i++)lg[i]=lg[i>>1]+1;

    當兩點匯合時,就可以返回了
    查詢m組,總的復雜度應該是O(m log n)
    輸入:

    9 1 1 1 2 1 3 2 4 2 5 4 7 4 8 3 6 6 9 5 8 2 #include<bits/stdc++.h> #define maxn 500005 using namespace std; int n, m, root; int read() {int x = 0, f = 1, ch = getchar();while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();};while(isdigit(ch)) x = (x << 3) + (x << 1) + ch - '0', ch = getchar();return x * f; }struct edge {int to, nxt;edge() {}edge(int tt, int nn) {to = tt, nxt = nn;} }e[maxn << 1];int head[maxn], k = 0; void add(int u, int v) {e[k] = edge(v, head[u]);head[u] = k++; }int fa[maxn][25], dep[maxn], lg[maxn]; void dfs(int now, int fath)//初始化深度及祖祖輩輩 {dep[now] = dep[fath] + 1;fa[now][0] = fath;for(int i = 1; (1 << i) <= dep[now]; i++)fa[now][i] = fa[fa[now][i - 1]][i - 1];//前文的遞推式for(int i = head[now]; ~i; i = e[i].nxt)if(e[i].to != fath) dfs(e[i].to, now);//繼續往下遍歷 }int lca(int x, int y) {if(dep[x] < dep[y]) swap(x, y);//保證x的深度更大,跳xwhile(dep[x] > dep[y]) x = fa[x][lg[dep[x] - dep[y]]];if(x == y) return x;//特判for(int i = lg[dep[x]]; i >= 0; i--)//倍增一起往上跳if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];return fa[x][0]; }int main() {memset(head, -1, sizeof head);n = read(), m = read(), root = read();register int u, v;for(int i = 1; i < n; i++){u = read(), v = read();add(u, v);add(v, u);}dfs(root, 0);lg[0]=-1; for(int i=1;i<=n;i++)lg[i]=lg[i>>1]+1; // for(int i = 1; i <= n; i++) // lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);//log打表,后面那一坨是特判一下i是否進位了for(int i = 1; i <= m; i++){u = read(), v = read();printf("%d\n", lca(u, v));}return 0; }

    Tarjan

    倍增是在線算法
    Tarjan是離線算法

    Tarjan主要用到和并查集差不多的方法
    比如查詢7和5
    我們從根節點出發,1->2->4->7,發現另一點5還沒被查詢,然后回溯,再遍歷,7->4->8->4->2->5,遍歷到點5,我們發現另外一個點7已經訪問過,就到此結束。在這過程中我們用fa[]來表示父節點,就和并查集一樣,回溯時,7->4 , fa[7]=4 ;8->4 , fa[8]=4; 4->2,fa[4]=2; 5->2,fa[5]=2。
    這樣當發現另外一個點已經標記了,那么這個點的祖先一定是兩個點的lca(可以通過路徑壓縮)
    在訪問一個點時,我們會將與這點相關的一同詢問,所以tarjan是強制離線
    lca用于存結果,每個詢問都存了兩次(因為還要查詢另外一個點是否已經訪問過),最后輸出時 i * 2
    每組答案lca[i*2](i->m)
    大概時間復雜度為O(n+m)

    #include<bits/stdc++.h> #define maxn 500005 using namespace std; int n, m, root, lca[maxn << 1]; int read() {int x = 0, f = 1, ch = getchar();while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();return x * f; }struct edge {int to, nxt;edge(){}edge(int tt, int nn) {to = tt, nxt = nn;} }e[maxn << 1], qe[maxn << 1];int head[maxn], k = 0; void add(int u, int v) {e[k] = edge(v, head[u]);head[u] = k++; }int qhead[maxn], qk = 0; void qadd(int u, int v) {qe[qk] = edge(v, qhead[u]);qhead[u] = qk++; }int fa[maxn]; int get(int x) {return fa[x] == x? x : fa[x] = get(fa[x]);}//記得路徑壓縮!!bool vis[maxn]; void tarjan(int u) {register int v;vis[u] = 1;for(int i = head[u]; ~i; i = e[i].nxt)//先深優遍歷下去{v = e[i].to;if(vis[v]) continue;//vis過了,就說明是父親tarjan(v);fa[v] = u;//回溯時記錄}for(int i = qhead[u]; ~i; i = qe[i].nxt)//開始掃一遍關于u的所有詢問{v = qe[i].to;if(vis[v])//另一個點訪問過了,可以得出答案了{lca[i] = get(v);if(i & 1) lca[i - 1] = lca[i];//這里特殊處理是因為每個詢問存了兩次else lca[i + 1] = lca[i];}} }int main() {memset(head, -1, sizeof head);memset(qhead, -1, sizeof qhead);n = read(), m = read(), root = read();register int u, v;for(int i = 1; i < n; i++){u = read(), v = read();add(u, v);add(v, u);fa[i] = i;//順便初始化fa}fa[n] = n;for(int i = 1; i <= m; i++){u = read(), v = read();//存儲詢問qadd(u, v);qadd(v, u);}tarjan(root);//開始遍歷for(int i = 0; i < m; i++)printf("%d\n", lca[i << 1]);//每個詢問都存了兩次,所以要*2 }

    m較小時用倍增,較大時用Tarjan

    歐拉序

    歐拉序的定義
    樹在dfs過程中的節點訪問順序稱為歐拉序.
    那有人會問:dfs序和歐拉序啥區別?

    dfs序:是指將一棵樹被dfs時所經過的節點順序(不繞回原點)。
    歐拉序:就是從根結點出發,按dfs的順序在繞回原點所經過所有點的順序。


    歐拉序與dfs序不同地方在于,歐拉序中每個節點可以出現多次,比如進入一次退出一次,又比如每次回溯時記錄一次。

    因此兩個點的LCA,就是在該序列上兩個點第一次出現的區間內深度最小的那個點
    比如求D和E的LCA,D和E第一次出現的區間是DDCBE。這里面深度最小的就是點B,所以LCA(D,E)=B
    這樣就轉化為區間RMQ問題,所以可以用ST表。
    具體怎么做呢?
    先求出歐拉序和每個節點的深度,同時用start[]記錄每個節點第一次出現的位置
    LCA ( T , u , v ) = RMQ ( B , start ( u ) , start ( v ) )
    然后直接用ST表求RMQ就行
    ST表詳解

    所用到數組:

    a.歐拉序:圖的遍歷(幾種存儲結構寫法不太一樣)

    cnt:序列長度(每個元素一進一出共兩次,記得最大初始化為2*MAXN)
    oula[]:歐拉序列,記錄編號 dfs前記錄一次,dfs后(回溯)再記錄一次
    depth[]:每個編號的深度(也可以記錄每個下標的深度,見注釋)

    start[]:每個編號第一次出現的序列下標

    b.ST表

    minl[i][j] 記得第一層初始化為depth[]

    pos[][]最值下標,第一層初始化為i

    注意這里i是歐拉序列的下標,最終要的是編號(這里經常下標搞混!!)

    歐拉序列下標=start【編號】

    #pragma GCC optimize(3) #include <bits/stdc++.h> using namespace std; #define N 500010 vector<int>G[N]; int oula[N<<1],depth[N],start[N];//N*2 int cnt;int minl[N<<1][19],pos[N<<1][19],len[N<<1],vis[N],tmp; //開20以上就TLE,自閉 //struct node{int deep,order;}minl[N][19];int n,m,s,x,y;inline int read() {char ch='*';while(!isdigit(ch=getchar()));int num=ch-'0';while(isdigit(ch=getchar()))num=num*10+ch-'0';return num; }inline void dfs(int now,int fa,int deep){oula[++cnt]=now;//入 //depth[cnt]=deep;if(depth[now]==0)depth[now]=deep;if(start[now]==0)start[now]=cnt;int z=G[now].size();for(int i=0;i<z;i++){if(G[now][i]!=fa){dfs(G[now][i],now,deep+1);oula[++cnt]=now;//出 //depth[cnt]=deep;//index[now]=cnt;}} }inline void S_table(){for(int i=1;i<2*N;++i) len[i]=(1<<(tmp+1))==i?++tmp:tmp;for(int i=1;i<=cnt;i++) minl[i][0]= depth[oula[i]],pos[i][0]=i;//depth[i]//int l = log2((double)cnt);for (int j=1;(1<<j)<=cnt;j++){for (int i=1; i + (1 << (j-1) ) - 1 <=cnt;++i){//minl[i][j] = min(minl[i][j-1], minl[i + (1 << (j-1) )][j-1]);if(minl[i][j-1]<minl[i+(1<<(j-1))][j-1])minl[i][j]=minl[i][j-1],pos[i][j]=pos[i][j-1];else minl[i][j]=minl[i + (1 << (j-1) )][j-1],pos[i][j]=pos[i + (1 << (j-1) )][j-1];}} }inline int rmq(int l, int r){if(l>r) swap(l,r);int k=len[r-l+1];//int k = log2((double)(r-l+1));int mid=r-(1<<k)+1;if(minl[l][k]<=minl[mid][k])return pos[l][k];else return pos[mid][k]; } int main() {n=read();m=read();s=read(); for(int i=1;i<n;++i){x=read();y=read();G[x].push_back(y);G[y].push_back(x);}dfs(s,-1,1);//求歐拉序列S_table();//初始化st表for(int i=1;i<=m;++i){x=read();y=read();printf("%d\n",oula[rmq(start[x],start[y])]);}return 0; }

    樹剖解法:

    樹剖比Tarjan慢,但比倍增快

    樹剖詳講

    樹剖是把一棵樹按子樹大小分為鏈。樹剖基本操作中有一個是求x到y的路徑的邊權和,或者是所有邊權進行修改。我們可以用樹剖的思路來寫LCA。直接看點x和y是否在一條鏈上,不在則深度較大者跳到鏈頭的父親節點處,也就是跳出這條鏈;在則深度較淺者為LCA。
    樹剖一跳就是一條鏈,對于n極大的情況就相當于是倍增的再一優化。

    #include<bits/stdc++.h>//都是樹剖模板操作,就不做多解釋了。 #define maxn 500005 using namespace std; int n, m, root; int read() {int x = 0, f = 1, ch = getchar();while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();return x * f; } struct edge {int to, nxt;edge(){}edge(int tt, int nn){to = tt, nxt = nn;} }e[maxn << 1];int k = 0, head[maxn]; void add(int u, int v) {e[k] = edge(v, head[u]);head[u] = k++; }int fa[maxn], dep[maxn], size[maxn], son[maxn]; void dfs_getson(int u) {size[u] = 1;for(int i = head[u]; ~i; i = e[i].nxt){int v = e[i].to;if(v == fa[u]) continue;dep[v] = dep[u] + 1;fa[v] = u;dfs_getson(v);size[u] += size[v];if(size[v] > size[son[u]]) son[u] = v;} }int top[maxn]; void dfs_rewrite(int u, int tp) {top[u] = tp;if(son[u]) dfs_rewrite(son[u], tp);for(int i = head[u]; ~i; i = e[i].nxt){int v = e[i].to;if(v != fa[u] && v != son[u]) dfs_rewrite(v, v);} }void ask(int x, int y) {while(top[x] != top[y])//不同則跳{if(dep[top[x]] > dep[top[y]]) swap(x, y);y = fa[top[y]];}if(dep[x] > dep[y]) swap(x, y);printf("%d\n", x);//輸出深度較小者 }int main() {memset(head, -1, sizeof head);n = read(), m = read(), root = read();int u, v;for(int i = 1; i < n; i++){u = read(), v = read();add(u, v);add(v, u);}//樹剖初始化dfs_getson(root);dfs_rewrite(root, root);for(int i = 1; i <= m; i++){u = read(), v = read();ask(u, v);}return 0; }

    總結

    以上是生活随笔為你收集整理的LCA总结的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。