1. 程式人生 > >LCA 最近公共祖先 tarjan離線 總結 結合3個例題

LCA 最近公共祖先 tarjan離線 總結 結合3個例題

在網上找了一些對tarjan演算法解釋較好的文章 並加入了自己的理解

LCA(Least Common Ancestor),顧名思義,是指在一棵樹中,距離兩個點最近的兩者的公共節點。也就是說,在兩個點通往根的道路上,肯定會有公共的節點,我們就是要求找到公共的節點中,深度儘量深的點。還可以表示成另一種說法,就是如果把樹看成是一個圖,這找到這兩個點中的最短距離。

     LCA演算法有線上演算法也有離線演算法,所謂的線上演算法就是實時性的,比方說,給你一個輸入,演算法就給出一個輸出,就像是http請求,請求網頁一樣。給一個實時的請求,就返回給你一個請求的網頁。而離線演算法則是要求一次性讀入所有的請求,然後在統一得處理。而在處理的過程中不一定是按照請求的輸入順序來處理的。說不定後輸入的請求在演算法的執行過程中是被先處理的。

     本文先介紹一個離線的演算法,就做tarjan演算法。這個演算法是基於並查集和DFS的。Dfs的作用呢,就是遞迴,一次對樹中的每一個節點進行處理。而並查集的作用就是當dfs每訪問完(注意,這裡是訪問完)到一個點的時候,就通過並查集將這個點,和它的子節點連結在一起構成一個集合,也就是將並查集中的pnt值都指向當前節點。這樣就把樹中的節點分成了若干個的集合,然後就是根據這些集合的情況來對輸入資料來進行處理。

     比方說當前訪問到的節點是u,等u處理完之後呢,ancestor[u]就構成了u的集合中的點與u點的LCA,而ancestor[fa[u]]就構成了,u的兄弟節點及其兄弟子樹的集合中點與u的LCA,而ancestor[fa[fa[u]]]就構成了u的父親節點的兄弟節點及其兄弟子樹的集合中的點與u的LCA。然後依次類推,這樣就構成了這個LCA的離線演算法。

以上來自 pursuit的專欄

首先,Tarjan演算法是一種離線演算法,也就是說,它要首先讀入所有的詢問(求一次LCA叫做一次詢問),然後並不一定按照原來的順序處理這些詢問。而打亂這個順序正是這個演算法的巧妙之處。看完下文,你便會發現,如果偏要按原來的順序處理詢問,Tarjan演算法將無法進行。

  Tarjan演算法是利用並查集來實現的。它按DFS的順序遍歷整棵樹。對於每個結點x,它進行以下幾步操作:
   * 計算當前結點的層號lv[x],並在並查集中建立僅包含x結點的集合,即root[x]:=x。
   * 依次處理與該結點關聯的詢問。
   * 遞迴處理x的所有孩子。
   * root[x]:=root[father[x]](對於根結點來說,它的父結點可以任選一個,反正這是最後一步操作了)。

  現在我們來觀察正在處理與x結點關聯的詢問時並查集的情況。由於一個結點處理完畢後,它就被歸到其父結點所在的集合,所以在已經處理過的結點中(包括 x本身),x結點本身構成了與x的LCA是x的集合,x結點的父結點及以x的所有已處理的兄弟結點為根的子樹構成了與x的LCA是father[x]的集合,x結點的父結點的父結點及以x的父結點的所有已處理的兄弟結點為根的子樹構成了與x的LCA是father[father[x]]的集合……(上面這幾句話如果看著彆扭,就分析一下句子成分,也可參照右面的圖)假設有一個詢問(x,y)(y是已處理的結點),在並查集中查到y所屬集合的根是z,那麼z 就是x和y的LCA,x到y的路徑長度就是lv[x]+lv[y]-lv[z]*2。累加所有經過的路徑長度就得到答案。  現在還有一個問題:上面提到的詢問(x,y)中,y是已處理過的結點。那麼,如果y尚未處理怎麼辦?其實很簡單,只要在詢問列表中加入兩個詢問(x, y)、(y,x),那麼就可以保證這兩個詢問有且僅有一個被處理了(暫時無法處理的那個就pass掉)。而形如(x,x)的詢問則根本不必儲存。  如果在並查集的實現中使用路徑壓縮等優化措施,一次查詢的複雜度將可以認為是常數級的,整個演算法也就是線性的了。

上面內容來自NOCOW TarjanLCA的遞迴過程就是深度優先搜尋。 Tarjan作為離線off-line演算法,在程式開始前,需要將所有等待詢問的節點對提前儲存,然後程式從樹根開始執行TarjanLCA()。假如有如下一棵多叉樹

根據TarjanLCA的實現演算法可以看出,只有當某一棵子樹全部遍歷處理完成後,才將該子樹的根節點標記為黑色(初始化是白色),假設程式按上面的樹形結構進行遍歷,首先從節點1開始,然後遞迴處理根為2的子樹,當子樹2處理完畢後,節點2, 5, 6均為黑色;接著要回溯處理3子樹,首先被染黑的是節點7(因為節點7作為葉子不用深搜,直接處理),接著節點7就會檢視所有詢問(7, x)的節點對,假如存在(7, 5),因為節點5已經被染黑,所以就可以斷定(7, 5)的最近公共祖先就是find(5).ancestor,即節點1(因為2子樹處理完畢後,子樹2和節點1進行了union,find(5)返回了合併後的樹的根1,此時樹根的ancestor的值就是1)。    有人會問如果沒有(7, 5),而是有(5, 7)詢問對怎麼處理呢?我們可以在程式初始化的時候做個技巧,將詢問對(a, b)和(b, a)全部儲存,這樣就能保證完整性

下面這幅圖可以讓大家有個感性的認識   BY hnust_xiehonghao

赤裸裸的 題意 輸入cas 後  有cas組資料 輸入 n   再輸入n-1 條邊    之後輸入x y  問x y的最近公共祖先是什麼

#include<stdio.h>
#include<vector>
#include<string.h>
using namespace std;
#define Size 11111  //節點個數

vector<int> node[Size],que[Size];
int n,pare[Size],anse[Size],in[Size],rank[Size];

int vis[Size];
void init()
{
	int i;
	for(i=1;i<=n;i++)
	{
		node[i].clear();
		que[i].clear();
		rank[i]=1;
		pare[i]=i;/// 
	}
	memset(vis,0,sizeof(vis));
	memset(in,0,sizeof(in));
	memset(anse,0,sizeof(anse));
	 
}

int find(int nd)//並查集操作  不解釋
{
	return pare[nd]==nd?nd:pare[nd]=find(pare[nd]);
}
int Union(int nd1,int nd2)//並查集操作  不解釋
{
	int a=find(nd1);
	int b=find(nd2);
	if(a==b) return 0;
	else if(rank[a]<=rank[b])
	{
		pare[a]=b;
		rank[b]+=rank[a];
	}
	else 
	{
		pare[b]=a;
		rank[a]+=rank[b];
	}
	return 1;

}

void LCA(int root)
{
	int i,sz;
	anse[root]=root;//首先自成一個集合
	sz=node[root].size();
	for(i=0;i<sz;i++)
	{
		   LCA(node[root][i]);//遞迴子樹
		   Union(root,node[root][i]);//將子樹和root併到一塊 
		 anse[find(node[root][i])]=root;//修改子樹的祖先也指向root
	}
	vis[root]=1;
	sz=que[root].size();
	for(i=0;i<sz;i++)
	{
            if(vis[que[root][i]])
			{
				printf("%d\n",anse[find(que[root][i])]);///root和que[root][i]所表示的值的最近公共祖先
				return ;
			}
	}
     return ;
}

int main()
{
	int cas,i;
	scanf("%d",&cas);
	while(cas--)
	{
		int s,e;
		scanf("%d",&n);
		init();
		for(i=0;i<n-1;i++)
		{
			scanf("%d %d",&s,&e);
			if(s!=e)
			{
			    node[s].push_back(e);
			   // node[e].push_back(s);
				in[e]++;
			}
		}
		scanf("%d %d",&s,&e);
		que[s].push_back(e);
		que[e].push_back(s);
		for(i=1;i<=n;i++)  if(in[i]==0) break;//尋找根節點
	//	printf("root=%d\n",i);
		LCA(i);
	}
	return 0;
}


 之後來個加強版

思路:

  求出a和b的最近公共祖先,然後分4種情況討論

  ①. a和b有一個公共祖先c,則用 c時間戳-a的時間戳+1(1步可以直接從c到b)

  ②. a是b的祖先,則只用1步就可以到達b點

  ③. b是a的祖先,則用a的時間戳-b的時間戳

  ④. a和b是同一個點,則答案是0

#include<stdio.h>
#include<vector>
#include<string.h>
#include<map>
#include<math.h>
#include<string>
using namespace std;
#define Size 111111  //節點個數
struct Query
{
    int nd,id;
}temp;
struct out
{
    int s,e;
}out[Size];
vector<int> node[Size];
vector<struct Query>que[Size];
int n,m,pare[Size],ance[Size],in[Size],rank[Size],dis[Size],ans[Size],vis[Size];
map<string,int>mp;
void init()
{
	int i;
	for(i=1;i<=n;i++)
	{
		node[i].clear();
		que[i].clear();
		rank[i]=1;
		pare[i]=i;///
	}
	memset(vis,0,sizeof(vis));
	memset(in,0,sizeof(in));
	memset(ance,0,sizeof(ance));
	memset(dis,0,sizeof(dis));
	mp.clear();
}
int aabs(int aa)
{
     if(aa>0) return aa;
     else return -aa;
}
int find(int nd)//並查集操作  不解釋
{
	return pare[nd]==nd?nd:pare[nd]=find(pare[nd]);
}
int Union(int nd1,int nd2)//並查集操作  不解釋
{
	int a=find(nd1);
	int b=find(nd2);
	if(a==b) return 0;
	else if(rank[a]<=rank[b])
	{
		pare[a]=b;
		rank[b]+=rank[a];
	}
	else
	{
		pare[b]=a;
		rank[a]+=rank[b];
	}
	return 1;
}
void LCA(int root,int num)
{
	int i,sz;
	ance[root]=root;//首先自成一個集合
	dis[root]=num;
	sz=node[root].size();
	for(i=0;i<sz;i++)
	{
		   LCA(node[root][i],num+1);//遞迴子樹
		   Union(root,node[root][i]);//將子樹和root併到一塊
		 ance[find(node[root][i])]=root;//修改子樹的祖先也指向root
	}
	vis[root]=1;
	sz=que[root].size();
	for(i=0;i<sz;i++)
	{
	    int nd1,nd2,idx,ancestor;
	    nd1=root;nd2=que[root][i].nd;idx=que[root][i].id;
            if(vis[nd2])
			{
                  ans[idx]=ance[find(nd2)];
			}
	}
     return ;
}

int main()
{
	int cas,i;
	scanf("%d",&cas);
	while(cas--)
	{
		char  ss[100],ee[100];
		int s,e,cnt=1;
		scanf("%d %d",&n,&m);
		init();
		for(i=0;i<n-1;i++)
		{
			scanf("%s %s",ee,ss);
			if(mp.find(ss)==mp.end())
            {
                 s=cnt;mp[ss]=cnt++;
            }
            else s=mp[ss];
			if(mp.find(ee)==mp.end())
            {
                e=cnt;mp[ee]=cnt++;
            }
            else  e=mp[ee];
			if(s!=e)
			{
			    node[s].push_back(e);
				in[e]++;
			}
		}
		for(i=0;i<m;i++)
        {
		   scanf("%s %s",ss,ee);
		   s=mp[ss];e=mp[ee];
		   out[i].s=s;out[i].e=e;
		   temp.nd=e;temp.id=i;
		   que[s].push_back(temp);
		   temp.nd=s;temp.id=i;
		   que[e].push_back(temp);
        }
		for(i=1;i<=n;i++)  if(in[i]==0) break;//尋找根節點
		LCA(i,0);
		for(i=0;i<m;i++)
        {
            if(out[i].s==out[i].e)
                printf("0\n");
            else
                if(out[i].s==ans[i])
                      printf("1\n");
            else if(out[i].e==ans[i])
                printf("%d\n",dis[out[i].s]-dis[ans[i]]);
            else
            printf("%d\n",dis[out[i].s]-dis[ans[i]]+1);
        }
	}
	return 0;
}

 by hnust_xiehonghao

hdu   2874

http://acm.hdu.edu.cn/showproblem.php?pid=2874

題目大意: 給你一個n個節點m條邊的森林,再給定q個查詢,每次查詢森林裡兩個點的最近距離。n ,m <= 10000,q <= 100萬

本題和標準的LCA模板應用有了不小的區別   卻可以讓人更加透徹的看清LCA的思路  而且本題沒有必要去求出公共祖先

具體看程式碼

#include<stdio.h>
#include<string.h>
#include<vector>
using namespace std;
#define Size  11111
struct Edge
{
    int y,val;
}temp;
struct Query
{
    int y,id;
}mid;
int pare[Size],ance[Size],vis[Size],dis[Size],rank[Size],ans[1000000+100],n,m,c,tree[Size];
vector<struct Query>que[Size];
vector<struct Edge>node[Size];
void init()
{
    int i;
    for(i=0;i<=n;i++)
    {
        vis[i]=0;
        pare[i]=i;
        dis[i]=0;
        rank[i]=1;
        que[i].clear();
        node[i].clear();
    }
    memset(ans,-1,sizeof(ans));
}
int find(int x)
{
    return pare[x]==x?x:pare[x]=find(pare[x]);
}
/*
void Union(int x,int y)
{
x=find(x);
y=find(y);
if(x!=y)
{
if(rank[x]>rank[y])
{
rank[x]+=rank[y];
pare[y]=x;
}
else
{
rank[y]+=rank[x];
pare[x]=y;
}
}
}
*/
void LCA(int root,int d,int k)//k表示是以第k個點作為根的樹
{
    int i,sz,nd1,nd2;
    vis[root]=1; //已經遍歷過的點 要標記一下 不要
    tree[root]=k;dis[root]=d;
    // ance[root]=root;
    sz=node[root].size();
    for(i=0;i<sz;i++)
    {
        nd2=node[root][i].y;
        if(!vis[nd2])
        {
            LCA(nd2,d+node[root][i].val,k);
            // Union(node[root][i].y,root);//用帶rank的幷查集操作答案不對 不知道why
            int w=find(nd2),m=find(root);
            if(w!=m)
            {
               pare[w]=m;//這樣才對
            }
            //ance[find(node[root][i].y)]=root;
        }
    }
    sz=que[root].size();
    for(i=0;i<sz;i++)
    {
        nd1=root;
        nd2=que[root][i].y;
        if(vis[nd2]&&tree[nd1]==tree[nd2])//如果 nd1 nd2 的跟是同一個點 則是同一棵樹上的
        {
            ans[que[root][i].id]=dis[nd1]+dis[nd2]-2*dis[find(nd2)];
        }
    }
}
int main()
{
    int i,j,x,y,val;
    while(scanf("%d %d %d",&n,&m,&c)!=EOF)
    {
        init();
        for(i=0;i<m;i++)
        {
            scanf("%d %d %d",&x,&y,&val);
            if(x!=y)
            {
                temp.y=y;temp.val=val;
                node[x].push_back(temp);
                temp.y=x;
                node[y].push_back(temp);//路是2個方向都可以通行的
            }
        }
        for(i=0;i<c;i++)
        {
            scanf("%d %d",&x,&y);
            mid.id=i;
            mid.y=y;
            que[x].push_back(mid);
            mid.y=x;
            que[y].push_back(mid);
        }
        for(i=1;i<=n;i++)
        {
            LCA(i,0,i);//以每一個節點作為根節點去深度搜索  找出每個點作為根的所有最近公共祖先
        }
        for(i=0;i<c;i++)
        {
            if(ans[i]==-1)
                printf("Not connected\n");
            else
                printf("%d\n",ans[i]);
        }
    }
    return 0;
}
/*本題給的是一個森林 而不是一顆樹,由於在加入邊的時候,我們讓2個方向都能走 這樣就
形成了一個強連通的快,  對於這個快來說,不管從快上那點出發 都可以遍歷這個快上的所
有的點,且相對距離是一樣的*/


待續 。。。。。。