1. 程式人生 > >LCA 三種 解決方法講解 (附加例題)

LCA 三種 解決方法講解 (附加例題)

  LCALeast Common Ancestors

即最近公共祖先,是指這樣一個問題:在有根樹中,找出某兩個結點uv最近的公共祖先(另一種說法,離樹根最遠的公共祖先)。

一、線上演算法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.46的最近公共祖先

通過上一步求解我們知道它們在深搜序列中出現在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為樹根。

你的任務是給定這棵樹的兩個節點,快速計算出他們公共祖先的個數。

(第一行一個整數n1n50,000),表示樹的節點個數。接下來的n行,第i行表示節點i的資訊。第i行第一個數字k,表示節點i擁有孩子的個數,接著k個數字,表示這個節點所擁有的孩子的編號。如果k=0,表示該節點是葉節點。注意,我們假定節點是節點本身的祖先。n+2行是一個整數m(1m30,000),表示有m個查詢。接下去m行,每行兩個數字xy,表示該查詢的兩個節點的編號。)

兩個節點的公共祖先的個數即它們的最近公共祖先的深度,因為顯然最近公共祖先以上都為兩節點的公共祖先。

#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