LCA 三種 解決方法講解 (附加例題)
LCA(Least Common Ancestors)
即最近公共祖先,是指這樣一個問題:在有根樹中,找出某兩個結點u和v最近的公共祖先(另一種說法,離樹根最遠的公共祖先)。
一、線上演算法ST演算法
所謂線上即輸入一個詢問,要立即返回答案,才可進行下一次詢問。
基礎:dp(即rmq)
時間複雜度O(nlogn+m+n)
步驟:
1.將樹看作一個無向圖,從根節點開始深搜,得到一個遍歷序列。
eg.
(1)深搜節點序列:1 3 1 2 5 7 5 6 5 2 4 2 1
(2)各點深度: 1 2 1 2 3 4 3 4 3 2 3 2 1
(3)第一次出現的下標: 1 4 2 11 5 8 6
2.在x~y區間中利用RMQ演算法找到深度最小返回其下標。
Eg.求4和6的最近公共祖先
通過上一步求解我們知道它們在深搜序列中出現在8~11,即6,5,2,4。
這時候用到RMQ演算法,維護一個dp陣列儲存其區間深度最小的下標,查詢時返回即可。例子中我們找到深度最小的數為2,返回其下標10。
例題:
給你一棵有根樹,要求你計算出m對結點的最近公共祖先。
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> #define N 200005 using namespace std; int tot,head[N],ver[2*N],r[2*N],first[N],dp[2*N][18]; int n,m; bool vis[N]; struct Node{ int to,next; }e[2*N]; void insert(int x,int y) { e[++tot].to=y; e[tot].next=head[x]; head[x]=tot; } void dfs(int u,int dep) { vis[u]=true;ver[++tot]=u;first[u]=tot;r[tot]=dep; for(int k=head[u];k!=-1;k=e[k].next) if(!vis[e[k].to]) { int v=e[k].to; dfs(v,dep+1); ver[++tot]=u;r[tot]=dep; } } void ST(int len) { for(int i=1;i<=len;i++) dp[i][0]=i; for(int j=1;(1<<j)<=len;j++) for(int i=1;i+(1<<j)-1<=len;i++) { int a=dp[i][j-1],b=dp[i+(1<<j-1)][j-1]; dp[i][j]=r[a]<=r[b]?a:b; } } int RMQ(int x,int y) { int k=trunc(log2(y-x+1)); int a=dp[x][k],b=dp[y-(1<<k)+1][k]; return r[a]<=r[b] ? a:b; } int LCA(int u,int v) { int x=first[u],y=first[v]; if(x>y)swap(x,y); if(x==y)return ver[x]; int res=RMQ(x,y); return ver[res]; } int main() { scanf("%d%d",&n,&m); memset(vis,false,sizeof(vis)); memset(head,-1,sizeof(head)); tot=0; int x,y,root; for(int i=1;i<n;i++) { scanf("%d%d",&x,&y); insert(x,y); vis[y]=1; } for(int i=1;i<=n;i++) if(!vis[i]){root=i;break;} memset(vis,false,sizeof(vis)); tot=0; dfs(root,0); ST(2*n-1); for(int i=1;i<=m;i++) { scanf("%d%d",&x,&y); printf("%d\n",LCA(x,y)); } }
二、倍增法求LCA(線上)
又稱作爬樹法。
時間複雜度:預處理O(nlogn),每次詢問O(logn)基礎:dp,求節點在樹中的深度。
步驟:
1.新增邊,並預處理p陣列。
void addedge(int x,int y)
{
e[++tot].v=y;
e[tot].next=head[x];
head[x]=tot;
}
p[i][j]表示i的2^j倍祖先。
p[i][j]=prt[i],j=0
p[p[i][j-1]][j-1],j>0
2.求每個點在樹中的深度d[i]。
3.對於每個詢問a,b
首先判斷d[a]<d[b],若小於則將a,b互換,即保證a的深度大於等於b。
將a 的深度不斷降低,調到與b相同的深度。
這時將a,b同時調整,直到兩個變數的父親相同,即當p[a][i]!=p[b][i],則a=p[a][i],b=p[b][i],i--。
最後p[a][0]或p[b][0]為答案。
eg.
對於上面的一棵樹,我們要詢問5和9的最近公共祖先
首先預處理出p[5][0]=3,p[5][1]=1;
p[9][0]=7;p[9][1]=4;
d[5]=3;d[9]=4;
然後將9調至與5同深度。
int k=trunc(log2(4));
for(int i=k;i>=0;i--)
if(d[a]-(1<<i)>=d[b])a=p[a][i];
9->7。
接下來將5,7同時向上調整,直到它們的父親相同,即變為3,4.。
輸出3或4的父親,即1。
例題:
給你一棵有根樹,要求你計算出m對結點的最近公共祖先。#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#define N 200005
using namespace std;
struct Node{
int v,next;
}e[N];
int n,m,tot;
int head[N],d[N];
bool vis[N];
int par[N][20];
void addedge(int x,int y)
{
e[++tot].v=y;
e[tot].next=head[x];
head[x]=tot;
}
void dfs(int u,int dep)
{
vis[u]=true;d[u]=dep;
for(int k=head[u];k;k=e[k].next)
if(!vis[e[k].v])
{
int v=e[k].v;
dfs(v,dep+1);
}
}
void prepare()
{
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i<=n;i++)
if(par[i][j-1]!=-1)
par[i][j]=par[par[i][j-1]][j-1];
}
int lca(int a,int b)
{
if(d[a]<d[b])swap(a,b);
int k=trunc(log2(d[a]));
for(int i=k;i>=0;i--)
if(d[a]-(1<<i)>=d[b])a=par[a][i];
if(a==b)return a;
for(int i=k;i>=0;i--)
{
if(par[a][i]!=-1&&par[a][i]!=par[b][i])
a=par[a][i],b=par[b][i];
}
return par[a][0];
}
int main()
{
scanf("%d%d",&n,&m);
tot=0;
memset(par,-1,sizeof(par));
memset(vis,0,sizeof(vis));
int x,y;
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
addedge(x,y);
par[y][0]=x;
vis[y]=1;
}
int root;
for(int i=1;i<=n;i++)
if(!vis[i]){root=i;break;}
memset(vis,0,sizeof(vis));
dfs(root,0);
prepare();
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
printf("%d\n",lca(x,y));
}
return 0;
}
例題:
給定一個包含n個節點的樹,節點編號為1..n。其中,節點1為樹根。
你的任務是給定這棵樹的兩個節點,快速計算出他們公共祖先的個數。
(第一行一個整數n(1≤n≤50,000),表示樹的節點個數。接下來的n行,第i行表示節點i的資訊。第i行第一個數字k,表示節點i擁有孩子的個數,接著k個數字,表示這個節點所擁有的孩子的編號。如果k=0,表示該節點是葉節點。注意,我們假定節點是節點本身的祖先。第n+2行是一個整數m(1≤m≤30,000),表示有m個查詢。接下去m行,每行兩個數字x,y,表示該查詢的兩個節點的編號。)
兩個節點的公共祖先的個數即它們的最近公共祖先的深度,因為顯然最近公共祖先以上都為兩節點的公共祖先。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#define N 50010
using namespace std;
int n,q,tot;
struct Node{
int u,v,next;
}e[N];
int head[N],d[N];
bool vis[N];
int p[N][20];
void addedge(int u,int v)
{
e[++tot].u=u;e[tot].v=v;
e[tot].next=head[u];
head[u]=tot;
}
void dfs(int x,int dep)
{
vis[x]=1;d[x]=dep;
for(int i=head[x];i!=-1;i=e[i].next)
if(!vis[e[i].v])
dfs(e[i].v,dep+1);
}
void prepare()
{
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i<=n;i++)
if(p[i][j-1]!=-1)
p[i][j]=p[p[i][j-1]][j-1];
}
int lca(int a,int b)
{
if(d[a]<d[b])swap(a,b);
int k=trunc(log2(d[a]));
for(int i=k;i>=0;i--)
if(d[a]-(1<<i)>=d[b])a=p[a][i];
if(a==b)return a;
for(int i=k;i>=0;i--)
{
if(p[a][i]!=-1&&p[a][i]!=p[b][i])
a=p[a][i],b=p[b][i];
}
return p[a][0];
}
int main()
{
scanf("%d",&n);
memset(head,-1,sizeof(head));
memset(p,-1,sizeof(p));
tot=0;
for(int i=1;i<=n;i++)
{
int k,v;
scanf("%d",&k);
for(int j=1;j<=k;j++)
{
scanf("%d",&v);
addedge(i,v);
p[v][0]=i;
}
}
memset(vis,false,sizeof(vis));
dfs(1,1);
scanf("%d",&q);
prepare();
for(int i=1;i<=q;i++)
{
int u,v;
scanf("%d%d",&u,&v);
printf("%d\n",d[lca(u,v)]);
}
return 0;
}
三、離線Tarjan求LCA
所謂離線是指在讀取完全部的詢問後再統一處理的演算法。
基礎:深度優先搜尋的思想,並查集
時間複雜度:O(n+q)
基於深度優先搜尋的框架,對於新搜尋到的一個結點,首先建立由這個結點構成的集合,再對當前結點的每一個子樹進行搜尋,每搜尋完一棵子樹,則可確定子樹內的LCA詢問都已解決。其他的LCA詢問的結果必然在這個子樹之外,這時把子樹所形成的集合與當前結點的集合合併,並將當前結點設為這個集合的祖先。之後繼續搜尋下一棵子樹,直到當前結點的所有子樹搜尋完。這時把當前結點也設為已被檢查過的,同時可以處理有關當前結點的LCA詢問,如果有一個從當前結點到結點v的詢問,且v已被檢查過,則由於進行的是深度優先搜尋,當前結點與v的最近公共祖先一定還沒有被檢查,而這個最近公共祖先的包含v的子樹一定已經搜尋過了,那麼這個最近公共祖先一定是v所在集合的祖先。
補充:上面提到的詢問(x,y)中,y是已處理過的結點。那麼,如果y尚未處理怎麼辦?其實很簡單,只要在詢問列表中加入兩個詢問(x, y)、(y,x),那麼就可以保證這兩個詢問有且僅有一個被處理了(暫時無法處理的那個就pass掉)。而形如(x,x)的詢問則根本不必儲存。
(1)讀入資料,建立樹結構,並記錄下詢問序列Q[],若有(u,v)的詢問,則(u,v)和(v,u)都要記錄。
(2)Tarjan(x)演算法
①建立集合,自己為自己的父親prt[x]=x;
②對當前節點x的每個兒子節點y進行深搜,並prt[y]=x;
③設定訪問標記mark[x]=1,查詢所有與x有關的回答,若另一點已經訪問了,則另一個點的祖先就是他們的最經公共祖先。
(3)輸出答案;
例題:
【USACO2004 FEB】距離查詢
讀入一棵無根樹,求樹上兩點的最短距離。
因為是無根樹,我們不妨設1為根,用d[i]表示點i到根的距離,求樹上兩點距離即求兩點的LCA,用d[a]+d[b]-2*d[lca(a,b)]即可算出答案。
#include<iostream>
#include<cstdio>
#include<cstring>
#define N 40010
#define M 10010
using namespace std;
int head[N],_head[N];
struct Node{
int u,v,w,next;
}e[2*N];
struct ask{
int u,v,lca,next;
}ea[2*M];
int dir[N],fa[N],ance[N];
bool vis[N];
void add_edge(int u,int v,int w,int &k)
{
e[k].u=u;e[k].v=v;e[k].w=w;
e[k].next=head[u];
head[u]=k++;
e[k].u=v;e[k].v=u;e[k].w=w;
e[k].next=head[v];
head[v]=k++;
}
void add_ask(int u,int v,int &k)
{
ea[k].u=u;ea[k].v=v;ea[k].lca=-1;
ea[k].next=_head[u];_head[u]=k++;
ea[k].u=v;ea[k].v=u;ea[k].lca=-1;
ea[k].next=_head[v];_head[v]=k++;
}
int find(int x)
{
return x==fa[x]?x:fa[x]=find(fa[x]);
}
void uni(int x,int y)
{
int a=find(x);
int b=find(y);
fa[b]=a;
}
void tarjan(int u)
{
vis[u]=true;
fa[u]=u;
for(int k=head[u];k!=-1;k=e[k].next)
if(!vis[e[k].v])
{
int v=e[k].v,w=e[k].w;
dir[v]=dir[u]+w;
tarjan(v);
uni(u,v);
}
for(int k=_head[u];k!=-1;k=ea[k].next)
if(vis[ea[k].v])
{
int v=ea[k].v;
ea[k].lca=ea[k^1].lca=find(v);
}
}
int main()
{
int n,q,m,tot;
char h;
scanf("%d%d",&n,&m);
memset(head,-1,sizeof(head));
memset(_head,-1,sizeof(_head));
tot=0;
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d %c",&u,&v,&w,&h);
add_edge(u,v,w,tot);
}
scanf("%d",&q);
tot=0;
for(int i=1;i<=q;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add_ask(u,v,tot);
}
memset(vis,0,sizeof(vis));
dir[1]=0;
tarjan(1);
for(int i=1;i<=q;i++)
{
int s=i*2-1,u=ea[s].u,v=ea[s].v,lca=ea[s].lca;
printf("%d\n",dir[u]+dir[v]-2*dir[lca]);
}
return 0;
}
相關推薦
LCA 三種 解決方法講解 (附加例題)
LCA(Least Common Ancestors) 即最近公共祖先,是指這樣一個問題:在有根樹中,找出某兩個結點u和v最近的公共祖先(另一種說法,離樹根最遠的公共祖先)。 一、線上演算法ST演算法 所謂線上即輸入一個詢問,要立即返回答案,才可進行下一次詢問。
Electron與jQuery中$符號沖突的三種解決方法
jquer obj define export tro conf jquery blog ack 在Electron工程中引用jQuery時,經常會出現以下錯誤: Uncaught ReferenceError: $ is not defined 解決的具體方
PHP no input file specified 三種解決方法
重新 cgi put 主機 tro robot 解析 進行 例如 一.IIS Noinput file specified (IIS上報的錯誤) 方法一:改PHP.ini中的doc_root行,打開ini文件註釋掉此行,然後重啟IIS 方法二: 請
js閉包中this的指向問題及三種解決方法
下面是一個問題,物件方法中定義的子函式,子函式執行時this指向哪裡? 三個問題: (1)以下程式碼中列印的this是個什麼物件? (2)這段程式碼能否實現使myNumber.value加1的功能? (3)在不放棄helper函式
OLE:物件的類沒有在註冊資料庫中註冊 問題的三種解決方法
我在網上下載了破解版的SAS9.3,用了一段時間之後,今天開啟就填出一個提示框: OLE:物件的類沒有在註冊資料庫中註冊 啟用該物件所需的應用程式不可用。是否用“轉換……”將其轉換為或啟用為另一型別
執行緒間操作無效: 從不是建立控制元件的執行緒訪問它的三種解決方法
今天遇到這個問題,百度了下,把解決的方法總結出來。 我們在ui執行緒建立的子執行緒操作ui控制元件時,系統提示錯誤詳細資訊為: 執行緒間操作無效: 從不是建立控制元件“XXX”的執行緒訪問它。 就我知道的有三種方法,先看一下msdn的介紹: 訪問 Windows 窗
mybatis返回map型別資料空值欄位不顯示(三種解決方法)
一、查詢sql新增每個欄位的判斷空 IFNULL(rate,'') as rate11 二、ResultType利用實體返回,不用map 三、springMVC+mybatis查詢資料,返回resultType=”map”時,如果資料為空的欄位,則該欄位省略不顯示,可以
js $ is not function 的三種解決方法
將格式改成如下形式: 一 、 jQuery(document).ready(function(){ jQuery(function () { //code }); 二、 jQuery(document).ready(function($){ $(
小程式請求豆瓣API報403的三種解決方法
微信小程式使用wx.request API請求豆瓣公開api的時候,會報一個403(Forbidden)的錯誤。 這是為什麼呢?是由於來自小程式的呼叫過多,豆瓣來自於小程式的呼叫被禁止。這裡收集以下三種方法解決此問題(設定代理): 1、使用 https://d
PHP刪除HTMl標籤的三種解決方法
直接取出想要取出的標記 複製程式碼程式碼如下: <?php //取出br標記 function strip($str) { $str=str_replace("<br>","",$str); //$str=htmlspecialchars($str); return
原生js三種選項卡效果(輪播)
col val 還在 log pla absolut 自動播放 div pac 第三種:定時輪播切換(我這邊定時是2s) <!DOCTYPE html> <html> <head> <meta charset="utf-8"
原生js三種選項卡效果(點擊)
eight void log utf 觸發 nts lin type position 第一種:選項卡單擊點擊切換 <!DOCTYPE html> <html> <head> <meta charset="utf-8" /&g
C++中類的三種繼承方式public(公有繼承)、protected(保護繼承)、private(私有繼承)之間的差別(附思維導圖)【轉】
(轉自:https://blog.csdn.net/coco56/article/details/80467975) 注:若不指明繼承方式,則預設是私有繼承。 一:對於公有繼承(public)方式: 基類的public和protected成員的訪問屬性在派生類中保持不變,但基類的p
讀取檔案內的資料(數字)並進行三種排序,1(快速排序)2(歸併排序)3(希爾排序)。
#include<iostream> #include<fstream> #include<stdlib.h> int n1=0; using namespace std; void Merge(int a[], i
spring bean的三種例項化方式 (xml方式)
1,類中的無參建構函式建立物件(最常用的方式) spring配置檔案 <bean id = "person" class="com.wjk.spring.test.beans.xml.
PYTHON中三種取整函式(// int round)的區別
>>> 5//3 1 >>> -5//3 -2 >>> int(5.3) 5 >>> int(5.6) 5 >>> round(5.3) 5 >>> round(5.6
duilib編譯錯誤解決方法整理 (含VS2013)
此文轉載,原文:http://blog.csdn.net/x356982611/article/details/30217473 @1:找不到Riched20.lib 用everything等軟體搜尋下磁碟,找到所在的目錄新增到vs的庫目錄即可,我得是C:\Prog
C++中的三種智慧指標分析(RAII思想)
智慧指標 首先我們在理解智慧指標之前我們先了解一下什麼是RAII思想。RAII(Resource Acquisition Is I
【轉】Mybatis傳多個參數(三種解決方案)
三種 方案 var nbsp myba rom name bsp 什麽 轉自: http://www.2cto.com/database/201409/338155.html 據我目前接觸到的傳多個參數的方案有三種。 第一種方案: DAO層的函數方法 Public
只查看ett.txt文件(共100行)內第25到35行的內容的八種解決方法
查找內容試題:只查看ett.txt文件(共100行)內第25到35行的內容解答:方法一:head -35 /data/ett.txt |tail -11方法二:sed -n ‘25,35p‘ /data/ett.txt方法三:grep -C5 30 /data/ett.txt方法四:grep -A10 25