1. 程式人生 > >圖的生成樹和最小生成樹

圖的生成樹和最小生成樹

一、生成樹的概念

       

        在一個任意連通圖G中,如果取它的全部頂點和一部分邊構成一個子圖G',即:V(G')=V(G)和E(G')E(G)

        若同時滿足邊集E(G')中的所有邊既能夠使全部頂點連通而又不形成任何迴路,則稱子圖G'是原圖G的一棵生成樹

        下面簡單說明一下,在既能夠連通圖G中的全部n個頂點又沒有形成迴路的子圖G'(即生成樹)中,必定包含n-1條邊。要構造子圖G',首先從圖G中任取一個頂點加入G'中,此時G'中只有一個頂點,假定具有一個頂點的圖是連通的,以後每向G'中加入一個頂點,都要加入以該頂點為一個端點(終點),以已連通的頂點之中的任一個頂點為開始頂點的一條邊,這樣既連通了該頂點又不會產生迴路,進行n-1次後,就向G'中加入了n-1個頂點和n-1條邊,使得G'中的n個頂點既連通又不產生迴路。

       在圖G的一棵生成樹G'中,若再增加一條邊,就會出現一條迴路。這是因為此邊的兩個端點已連通,再加入此邊後,這兩個端點間有兩條路徑,因此就形成了一條迴路,子圖G'也就不再是生成樹了。同樣,若從生成樹G'中刪去一條邊,就使得G'變為非連通圖。這是因為此邊的兩個端點是靠此邊唯一連通的,刪除此邊後,必定使這兩個端點分屬於兩個連通分量中,使G'變成了具有兩個兩通分量的非連通圖。

        同一個連通圖可以有不同的生成樹。例如對於圖9-1(a),其餘3個子圖都是它的生成樹。在每棵生成樹中都包含8個頂點和7條邊,即n個頂點和n-1條邊,此時n等於原圖中的頂點數8,它們的差別只是邊的選取方法不同。

       在這3棵生成樹中,圖9-1(b)中的邊集是從圖9-1(a)中的頂點V0出發,利用深度優先搜尋遍歷的方法而得到的邊集,此圖是原圖的深度優先生成樹;圖9-1(c)中的邊集是從圖9-1(a)中的頂點V0出發,利用廣度優先搜尋遍歷的方法而得到的邊集,此圖是原圖的廣度優先生成樹;圖9-1(d)是原圖的任意一棵生成樹。當然圖9-1(a)的生成樹遠不止這3種,只要能連通所有頂點而又不產生迴路的任何子圖都是它的生成樹。


      


       對於一個連通網(即連通帶權圖,假定每條邊上的權值均為正實數)來說,生成樹不同,每棵樹的權(即樹中所有邊上的權值總和)也可能不同。圖9-2(a)就是一個連通網,圖9-2(b)、(c)和(d)是它的3棵生成樹,每棵樹的權各不相同。它們分別為57、53和38.具有權值最小的生成樹被稱為圖的最小生成樹

。通過後面將要介紹的構造最小生成樹的方法可知,圖9-2(d)是圖9-2(a)的最小生成樹。


 


       求圖的最小生成樹的方法(演算法)主要有兩個:一個是普里姆演算法;另一個是克魯斯卡爾演算法。下面分別進行討論。


二、普里姆演算法


       普里姆演算法的基本思路是:假設G=(V,E)是一個具有n個頂點的連通網,T=(U,TE)是G的最小生成樹,其中,U是T的頂點集,TE是T的邊集,U和TE的初值均為空集。演算法開始時,首先從V中任取一個頂點(假定取V0),將它併入U中,此時U={ V0},然後只要U是V的真子集(即UV),就從那些其中一個端點已在T中,另一個端點仍在T外的所有邊中,找一條最短(即權值最小)邊,假定為(i,j),其中ViU,Vj(V-U),並把該邊(i,j)和頂點j分別併入T的邊集TE和頂點集U,如此進行下去,每次往生成樹裡併入一個頂點和一條邊,直到n-1次後就把所有n個頂點都併入到生成樹T的頂點集中,此時U=V,TE中含有n-1條邊,T就是最後得到的最小生成樹。

       普里姆演算法的關鍵之處是:每次如何從生成樹T中到T外的所有邊中, 找到一條最短邊。例如,在第k次(1<=k<=n-1)前,生成樹T中已有k個頂點和k-1條邊,此時T中到T外的所有邊數為k(n-k),當然它包括兩頂點間沒有直接邊相連,其權值被看作常量MaxValue的邊在內,從如此多的邊中查詢最短邊,其時間複雜度為O(k(n-k)),顯然是很費事的。是否有一種好的方法能夠降低查詢最短邊的時間複雜度呢?回答是肯定的,它能夠使查詢最短邊的時間複雜度降低到O(n-k)。此方法是:假定在進行第k次前已經保留著從T中到T外每一個頂點(共n-k個頂點)的各一條邊,進行第k次時,首先從這n-k條最短邊中找出一條最短的邊,它就是從T中到T外的所有邊中的最短邊,假設為(i,j),此步需進行n-k次比較;然後把邊(i,j)和頂點j分別併入T中的邊集TE和頂點集U中,此時T外只有n-(k+1)個頂點,對於其中的每個頂點t,若(j,t)邊上的權值小於已保留的從原T中到頂點t的最短邊的權值,則用(j,t)修改之,使從T中到T外頂點t的最短邊為(j,t),否則原有最短邊保持不變,這樣,就把第k次後從T中到T外每一頂點t的各一條最短邊都保留下來了,為進行第k+1次運算做好了準備,此步需進行n-k-1次比較。所以,利用此方法求第k次的最短邊共需比較2(n-k)-1次,即時間複雜度為O(n-k)。

       例如,對於圖9-2(a),它的鄰接矩陣如圖9-3所示,假定從V0出發利用普里姆演算法構造最小生成樹T,在其過程中,每次(第0次為初始狀態)向T中併入一個頂點和一條邊後,頂點集U、邊集TE(每條邊的後面為該邊的權)以及從T中到T外每個頂點的各一條最短邊所構成的集合(假定用LW表示)的狀態如下:





第0次     U={ 0 }

              TE={  }

              LW={ (0,1)8,(0,2)∞,(0,3)5,(0,4)∞,(0,5)∞,(0,6)∞ }

第1次     U={ 0,3 }

              TE={(0,3)5  }

              LW={ (3,1)3,(0,2)∞,(0,4)∞,(3,5)7,(3,6)15 }

第2次     U={ 0 ,3,1}

              TE={ (0,3)5 ,(3,1)3}

              LW={ (1,2)12,(1,4)10,(3,5)7,(3,6)15 }

第3次     U={ 0,3,1,5 }

              TE={ (0,3)5 ,(3,1)3,(3,5)7 }

              LW={ (5,2)2,(5,4)9,(3,6)15 }

第4次     U={ 0,3,1,5,2 }

              TE={ (0,3)5 ,(3,1)3,(3,5)7,(5,2)2 }

              LW={ (2,4)6,(3,6)15 }

第5次     U={ 0,3,1,5,2,4 }

              TE={ (0,3)5 ,(3,1)3,(3,5)7,(5,2)2 ,(2,4)6 }

              LW={(3,6)15 }

第6次     U={ 0,3,1,5,2,4,6 }

              TE={ (0,3)5 ,(3,1)3,(3,5)7,(5,2)2 ,(2,4)6,(3,6)15 }

              LW={  }

        每次對應的圖形如圖9-4(b)至(h)所示,其中,粗實線表示新加入到TE集合中的邊,細實線表示已加入到TE集合中的邊,虛線表示LW集合中的邊,但權值我MaxValue的邊實際上是不存在的,所有沒畫出。




        圖9-4(h)就是最後得到的最小生成樹,它同圖9-2(d)是完全一樣的,所以圖9-4(h)是圖9-2(a)(重畫為 為圖9-4(a))的最小生成樹。

        通過以上分析可知,在構造圖的最小生成樹的過程中,在進行第k次(1<=k<=n-1)前,邊集TE中的邊數為k-1條,從T中到T外每一頂點的最短邊集LW中的邊數為n-空調,TE和LW中的邊數總和為n-1條。為了儲存這n-1條邊,設用具有n-1個元素的邊集樹組ed來儲存,其中ed的前k-1個元素(即ed[0]~ed[k-2])儲存生成樹的邊集TE中的邊,後n-k個元素(即ed[k-1]~ed[n-2])儲存LW中的邊。在進行第k次時,首先從下標為k-1到n-2的元素(即LW中的邊)中查找出權值最小的邊,假定為ed[m];接著把邊ed[k-1]與ed[m]對調,確保在第k次後ed的前k個元素儲存著TE中的邊,後n-k-1個元素儲存著LW中的邊;然後再修改LW中的有關邊,使得從T中到T外每一頂點的各一條最短邊被儲存下來。這樣經過n-1次運算後,CT中就儲存著最小生成樹中的全部n-1條邊。

       根據分析,假定採用鄰接矩陣作為圖的儲存結構,則求出圖的最小生成樹的普里姆演算法的具體描述為:

	public static void Prim(AdjacencyGraph gr,EdgeElement [] ed)
	{
		//利用普里姆演算法求出從頂點V0開始圖gr的最小生成樹,其邊集存入ed中
		if(gr.graphType()!=1)
		{
			System.out.println("gr不是一個連通網,不能求生成樹,退出執行!");
			System.exit(1);
		}
		int n=gr.vertices();                    //取出圖gr物件中的頂點個數的值賦給n
		int [][] a=gr.getArray();               //取出gr物件中鄰接矩陣的引用
		for(int i=0;i<n-1;i++)
		{
			ed[i]=new EdgeElement(0,i+1,a[0][i+1]);
		}
		for(int k=1;k<n;k++)
		{
			//進行n-1次迴圈,每次求出最小生成樹中的第k條邊
			//從ed[k-1]~ed[n-2](即LW邊集)中查詢最短邊ed[m]
			int min=gr.MaxValue;
			int j,m=k-1;                        //給min賦初值
			for(j=k-1;j<n-1;j++)
			{
				if(ed[j].weight<min)
				{
					min=ed[j].weight;
					m=j;
				}
			}
			//把最短邊對調到下標為k-1的元素位置
			EdgeElement temp=ed[k-1];
			ed[k-1]=ed[m];
			ed[m]=temp;
			//把新併入最小生成樹T中的頂點序號賦給j
			j=ed[k-1].endvex;
			//修改LW中的有關邊,使T外的每個頂點各保持一條目前最短的邊
			for(int i=k;i<n-1;i++)
			{
				int t=ed[i].endvex;
				int w=a[j][t];
				if(w<ed[i].weight)
				{
					ed[i].weight=w;
					ed[i].fromvex=j;
				}
			}
			
		}
	}

三、克魯斯卡演算法


         假設G=(V,E)是一個具有n個頂點的連通網,T=(U,TE)是G的最小生成樹,U的初值等於V,即包含G中全部頂點,TE的初值為空集,即不包含任何邊。克魯斯卡爾演算法的基本思路是:將圖G中的邊按權值從小到大的順序依次選取,若選取的一條邊使生成樹T不形成迴路 ,則把它併入生成樹的邊集TE中,保留作為T中的一條邊,若選取的一條邊使生成樹T形成迴路,則將其捨棄,如此進行下去,直到TE中包含n-1條邊為止,此時的T即為圖G的最小生成樹。

       現以圖9-5(a)為例來說明此演算法。設此圖是用邊集陣列表示的,且陣列中各邊是按權值從小到大的順序排列的,如圖9-5(d)所示。若元素沒有按序排列,則可通過呼叫排序演算法,使之成為有序。演算法要求按權值從小到大次序選取各邊轉換成按邊集陣列中下標次序選取各邊。當選取前3條邊時,均不產生迴路,應保留作為生成樹T中的邊,如圖9-5(b)所示;選取第4條邊(2,3)時,將與已保留的邊形成迴路,應捨去;接著保留(1,5)邊,捨去(3,5)邊;選取到(0,1)邊並保留後,保留的邊數已夠5條(即n-1條),此時必定將圖中全部6個頂點連通起來,並且沒有迴路,如圖9-5(c)所示,它就是圖9-5(a)的最小生成樹。




        實現克魯斯卡演算法的關鍵之處是:如何判斷欲加入T中的一條邊是否與生成樹中已保留的邊形成迴路。這可採用將各頂點劃分為不同集合的方法來解決,每個集合中的頂點表示一個無迴路的連通分量。演算法開始時,由於生成樹的頂點集等於圖G的頂點集,邊集為空,所以n個頂點分屬於n個集合,每個集合中只有一個頂點,表明頂點之間互不連通。例如對於圖9-5(a),其6個頂點集合為:

{0},{1},{2},{3},{4},{5}

當從邊集陣列中按次序選取一條邊時,若它的兩個端點分屬於不同的集合,則表明此邊連通了兩個不同的連通分量,因每個連通分量無迴路,所以連通後得到的連通分量仍不會產生迴路,此邊應保留作為生產樹的一條邊,同時把端點所在的兩個集合合併成一個,即成為一個連通分量;當選取的一條邊的兩個端點同屬於一個集合是,此邊應放棄,因同一個集合中的頂點使連通無迴路的,若再加入一條邊則必產生迴路。在上述例子中,當選取(0,4)4、(1,2)5、(1,3)8這3條邊後,頂點的集合則變成如下3個:

{0,4},{1,2,3},{5}

下一條邊(2,3)10的兩端點同屬於一個集合,故舍去,再下一條邊(1,5)12的兩端點屬於不同的集合,應保留,同時把兩個集合{1,2,3}和{5}合併成一個{1,2,3,5},以此類推,直到所有頂點同屬於一個集合,即進行了n-1次集合的合併,保留了n-1條生成樹的邊為止。

       為了用java語言描寫出克魯斯卡爾演算法,求出圖的最小生成樹,設定eg是具有EdgeElement元素型別的邊集陣列,並假定每條邊是按照權值從小到大的順序存放的;再設定ed也是一個具有EdgeElement元素型別的邊集陣列,用該陣列儲存依次所求得的最小生成樹中的每一條邊;還需要使用一個引數n,表示圖中的頂點數。另外,在演算法內部需要定義一個具有Set元素型別的集合陣列,假定用s表示,用它的每個元素表示對應的一個連通分量。

        根據以上分析,給出克魯斯卡爾演算法的具體描述如下:

	//克魯斯卡爾演算法
	public static void Kruskal(EdgeElement [] eg,EdgeElement [] ed,int n)
	{
		//利用克魯斯卡爾演算法求邊集陣列eg所表示圖的最下生成樹,結果存入ed中
		Set []s=new SequenceSet[n];              //定義集合陣列s,每個元素是一個集合物件
		for(int i=0;i<n;i++)                     //初始化s中的每個集合,並依次加入元素i
		{
			s[i]=new SequenceSet();
			s[i].add(i);                         //每個頂點分屬於不同集合
		}
		int k=1;                                 //k表示將得到的最小生成樹中的邊數,初值為1
		int d=0;                                 //d表示eg中待掃描邊元素的下標位置,初值為0
		while(k<n)                               //進行n-1次迴圈,每次得到的最小生成樹中的第k條邊
		{
			int m1=0,m2=0;                       //m1和m2記錄一條邊的兩個頂點所在的集合元素
			for(int i=0;i<n;i++)                 //求邊eg[d]的兩個頂點所在集合
			{
				if(s[i].contains(eg[d].fromvex)) 
				{
					m1=i;
				}
				if(s[i].contains(eg[d].endvex)) 
				{
					m2=i;
				}
			}
			if(m1!=m2)                           //若兩頂點屬於不同集合,則eg[d]是生成樹的一條邊
			{
				ed[k-1]=eg[d];                   //將邊eg[d]加入到邊集陣列ed中
				k++;
				s[m1]=s[m1].union(s[m2]);        //合併s[m1]和s[m2]集合到s[m1]中
				s[m2].clear();                   //將s[m2]置為一個空集
			}
			d++;                                 //d後移一個位置,以便掃描eg中的下一條邊
		}
	}