【POJ1330】最近公共祖先(LCA):並查集+深搜

最近公共祖先(LCA)問題常見於各種面試題中,針對不同情況算法也不盡相同。

情況1:二叉樹是個二叉查找樹,且root和兩個節點的值(a, b)已知。

如果該二叉樹是二叉查找樹,那麼求解LCA十分簡單。

基本思想爲:從樹根開始,該節點的值爲t,如果t大於t1和t2,說明t1和t2都位於t的左側,所以它們的共同祖先必定在t的左子樹中,從t.left開始搜索;如果t小於t1和t2,說明t1和t2都位於t的右側,那麼從t.right開始搜索;如果t1<=t<= t2,說明t1和t2位於t的兩側(或t=t1,或t=t2),那麼該節點t爲公共祖先。

bstNode* LCA(bstNode* pNode, int value1, int value2)   
{   
    bstNode* pTemp = pNode;   
    while (pTemp)   
    {   
        if (pTemp->data>value1 && pTemp->data>value2)   
            pTemp = pTemp->pLeft;   
        else if(pTemp->data<value1 && pTemp->data<value2)   
            pTemp = pTemp->pRight;   
        else  
            return pTemp;   
    }   
    return NULL;   
}  

情況2:普通二叉樹,root未知,但是每個節點都有parent指針。

基本思想:分別從給定的兩個節點出發上溯到根節點,形成兩條相交的鏈表,問題轉化爲求這兩個相交鏈表的第一個交點,即傳統方法:求出linkedList A的長度lengthA, linkedList B的長度LengthB。然後讓長的那個鏈表走過abs(lengthA-lengthB)步之後,齊頭並進,就能解決了。

int getLength (bstNode* pNode)   
{      
    int length = 0;   
    bstNode* pTemp = pNode;   
    while (pTemp)   
    {   
        length ++ ;   
        pTemp = pTemp->pParent;   
    }   
    return length;   
}   
bstNode* LCAC(bstNode* pNode1, bstNode* pNode2)   
{   
    int length1 = getLength(pNode1);   
    int length2 = getLength(pNode2);   
       
    // skip the abs(length1-length2)   
    bstNode* pIter1 = NULL;   
    bstNode* pIter2 = NULL;   
    int k=0;   
    if (length1>=length2)   
    {   
        bstNode* pTemp = pNode1;   
        while (k++<length1-length2)   
        {   
            pTemp = pTemp->pParent;    
        }   
        pIter1 = pTemp;   
        pIter2 = pNode2;   
    }   
    else  
    {   
        bstNode* pTemp = pNode1;   
        while (k++<length2-length1)   
        {   
            pTemp = pTemp->pParent;    
        }   
        pIter1 = pNode1;   
        pIter2 = pTemp;   
    }   
       
    while (pIter1&&pIter2 && pIter1!= pIter2)   
    {   
        pIter1 = pIter1->pParent;   
        pIter2 = pIter2->pParent;   
    }   
    return pIter1;   
}  

情況3:也是最普通的情況,二叉樹是普通的二叉樹,節點只有left/right,沒有parent指針。

                                              10

                                          /       /
                                        6         14
                                      /  /       /   /
                                   4   8   12   16

                                   /  /

                                  3   5

 基本思想:記錄從根找到node1和node2的路徑,然後再把它們的路徑用類似的情況一來做分析,比如還是node1=3,node2=8這個case.我們肯定可以從根節點開始找到3這個節點,同時記錄下路徑3,4,6,10,類似的我們也可以找到8,6,10。我們把這樣的信息存儲到兩個vector裏面,把長的vector開始的多餘節點3扔掉,從相同剩餘長度開始比較,4!=8, 6==6,我們找到了我們的答案。

#include <vector>   
bool nodePath (bstNode* pRoot, int value, std::vector<bstNode*>& path)   
{   
    if (pRoot==NULL) return false;   
    if (pRoot->data!=value)   
    {   
        if (nodePath(pRoot->pLeft,value,path))   
        {   
            path.push_back(pRoot);   
            return true;   
        }   
        else  
        {   
            if (nodePath(pRoot->pRight,value,path))   
            {   
                path.push_back(pRoot);   
                return true;   
            }   
            else  
                return false;   
        }   
    }   
    else  
    {   
        path.push_back(pRoot);   
        return true;   
    }   
}   
bstNode* LCAC(bstNode* pNode, int value1, int value2)   
{   
    std::vector<bstNode*> path1;   
    std::vector<bstNode*> path2;   
    bool find = false;   
    find |= nodePath(pNode, value1, path1);   
    find &= nodePath(pNode, value2, path2);   
    bstNode* pReturn=NULL;   
    if (find)   
    {   
        int minSize = path1.size()>path2.size()?path2.size():path1.size();   
        int it1 = path1.size()-minSize;   
        int it2 = path2.size()-minSize;   
        for (;it1<path1.size(),it2<path2.size();it1++,it2++)   
        {   
            if (path1[it1]==path2[it2])   
            {   
                pReturn = path1[it1];   
                break;   
            }   
        }   
    }   
    return pReturn;   
}  

下面說一下本文的題目,也就是POJ1330,用網上流行的LCA算法Tarjan求解(並查集+深搜)。

LCA是求最近公共祖先問題, tarjan的算法是離線算法,時間複雜度爲O(n+Q),n爲數據規模,Q爲詢問個數
其中用到並查集。關鍵是dfs的主循環比較重要。離線算法就是對每個查詢,都要求以下,此算法在lrj的黑書中簡單提起過,後邊還有O(n)-o(1)的算法,正在研究中。。。

分類,使每個結點都落到某個類中,到時候只要執行集合查詢,就可以知道結點的LCA了。
對於一個結點u,類別有 以u爲根的子樹、除類一以外的以f(u)爲根的子樹、除前兩類以外的以f(f(u))爲根的子樹、除前三類以外的以f(f(f(u)))爲根的子樹……
類一的LCA爲u,類二爲f(u),類三爲f(f(u)),類四爲f(f(f(u)))。這樣的分類看起來好像並不困難。但關鍵是查詢是二維的,並沒有一個確定的u。接下來就是這個算法的巧妙之處了。

利用遞歸的LCA過程。當lca(u)執行完畢後,以u爲根的子樹已經全部併爲了一個集合。而一個lca的內部實際上做了的事就是對其子結點,依 此調用lca.當v1(第一個子結點)被lca,正在處理v2的時候,以v1爲根的子樹+u同在一個集合裏,f(u)+編號比u小的u的兄弟的子樹 同在 一個集合裏,f(f(u)) + 編號比f(u)小的 f(u)的兄弟 的子樹 同在一個集合裏…… 而這些集合,對於v2的LCA都是不同的。因此只要 查詢x在哪一個集合裏,就能知道LCA(v2,x)

還有一種可能,x不在任何集合裏。當他是v2的兒子,v3,v4等子樹或編號比u大的u的兄弟的子樹(等等)時,就會發生這種情況。即還沒有被處 理。還沒有處理過的怎麼辦?把一個查詢(x1,x2)往查詢列表裏添加兩次,一次添加到x1的列表裏,一次添加到x2的列表裏,如果在做x1的時候發現 x2已經被處理了,那就接受這個詢問。(兩次中必定只有一次詢問被接受)

其他介紹:
首先,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)的詢問則根本不必存儲。   如果在並查集的實現中使用路徑壓縮等優化措施,一次查詢的複雜度將可以認爲是常數級的,整個算法也就是線性的了。

附僞代碼:
LCA(u)   
{   
     Make-Set(u)   
     ancestor[Find-Set(u)]=u   
     對於u的每一個孩子v   
     {   
         LCA(v)   
         Union(u)   
         ancestor[Find-Set(u)]=u   
     }   
     checked[u]=true  
     對於每個(u,v)屬於P   
     {   
         if checked[v]=true  
        then {   
             回答u和v的最近公共祖先爲 ancestor[Find-Set(v)]   
         }   
     }   
}
其中,makest就是建立一個集合,makeset(u )就是建立一個只含U的集合。
findset(u)是求跟U一個集合的一個代表,一般此集合用並查集表示,也就是當前樹的root節點。
union()就是把 V節點生成的子樹併入U中。
ancestor就是找跟節點,一直往上找,直至某節點的父節點是自己爲止。
這樣可能大家看不明白,最好的方法就是大家畫個樹,模擬一下,就會明白了,主要是那個dfs的尾部遞歸


 

#include <vector>
#include <iostream>
using namespace std;

const int MAX=17;
int f[MAX];//每個節點所屬集合
int r[MAX];//r是rank(秩)合併
int indegree[MAX];//保存每個節點的入度
int visit[MAX];//只有0和1,表示節點是否已處理完畢
vector<int> tree[MAX], Qes[MAX];//數,待查詢的節點組合
int ancestor[MAX];//祖先集合

void init(int n)//初始化
{
	for(int i=1; i<=n; i++)
	{
		r[i]=1;//初始秩爲1
		f[i]=i;//每個節點的父節點初始爲自身
		indegree[i]=0;
		visit[i]=0;
		ancestor[i]=0;
		tree[i].clear();
		Qes[i].clear();
	}
}

int find(int n)//查找n所在集合,並壓縮路徑
{
	if(f[n]==n)
		return n;
	else
		f[n]=find(f[n]);
	return f[n];
}

int Union(int x, int y)//合併函數,若屬於同一分支則返回0,成功合併返回1
{
	int a=find(x);
	int b=find(y);
	if(a==b)
		return 0;
	else if(r[a]<r[b])
	{
		f[a]=b;
		r[b]+=r[a];
	}
	else
	{
		f[b]=a;
		r[a]+=r[b];
	}
	return 1;
}

void LCA(int u)//tarjan求最近公共祖先
{
	ancestor[u]=u;
	int size=tree[u].size();
	//一個一個子節點處理
	for(int i=0; i<size; i++)
	{
		LCA(tree[u][i]);
		Union(u, tree[u][i]);
		ancestor[find(u)]=u;
	}

	//處理完子節點,置visit[u]=1
	visit[u]=1;

	//求當前節點與有關的節點的最近公共祖先
	size=Qes[u].size();
	for(i=0; i<size; i++)
	{
		if(visit[Qes[u][i]]==1)//如果這個節點已處理過
		{
			cout<<ancestor[find(Qes[u][i])]<<endl;
			continue;
		}
	}
}

int main()
{
	int n=16;//樹的總節點
	init(n);
	int s, t;

	//構造樹
	tree[8].push_back(5); indegree[5]++;
	tree[8].push_back(4); indegree[4]++;
	tree[8].push_back(1); indegree[1]++;

	tree[5].push_back(9); indegree[9]++;

	tree[4].push_back(6); indegree[6]++;
	tree[4].push_back(10); indegree[10]++;

	tree[1].push_back(14); indegree[14]++;
	tree[1].push_back(13); indegree[13]++;

	tree[6].push_back(15); indegree[15]++;
	tree[6].push_back(7); indegree[7]++;

	tree[10].push_back(11); indegree[11]++;
	tree[10].push_back(16); indegree[16]++;
	tree[10].push_back(2); indegree[2]++;

	tree[16].push_back(3); indegree[3]++;
	tree[16].push_back(12); indegree[12]++;


	//輸入要查詢最近公共祖先的兩個節點
	cin>>s>>t;

	//如果s在t左邊,那麼在遍歷完s時還不能求得LCA,所以這裏相當於訪問兩次,在訪問t時即可求得結果
	Qes[s].push_back(t);
	Qes[t].push_back(s);

	for(int i=1; i<=n; i++)
	{
		//尋找根節點
		if(indegree[i]==0)//根節點的入度爲0
		{
			LCA(i);
			break;
		}
	}
	return 0;
}



 

 

感謝以下參考:

http://poj.org/problem?id=1330

http://apps.hi.baidu.com/share/detail/16279376

http://kmplayer.iteye.com/blog/604518

http://blog.csdn.net/lixiandejian/article/details/6661074

發佈了90 篇原創文章 · 獲贊 22 · 訪問量 41萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章