概覽
相比於普里姆算法(Prim算法),克魯斯卡爾算法直接以邊為目標去構建最小生成樹。從按權值由小到大排好序的邊集合{E}中逐個尋找權值最小的邊來構建最小生成樹,只要構建時,不會形成環路即可保證當邊集合{E}中的邊都被嘗試了過后所形成的樹為最小生成樹。
定義
假設G=(V, {E})是連通網(即帶權連通圖),則令最小生成樹的初始狀態為只有N個頂點而無邊的非連通圖T=(V, {}),圖T中每個頂點自成一個連通分量。在圖G的邊集{E}中選擇權值最小的邊e,若e依附的頂點落在T中不同的連通分量上,則將e加入到T中,否則舍去e而選擇下一條權值最小的邊。以此類推,直至T中所有頂點都在同一連通分量上為止。
相關概念
連通:在無向圖G中,如果從頂點v到v’有路徑,則稱v和v’是連通的。
連通圖:如果對於圖G中任意兩個頂點vi、vj∈E,vi和vj都是連通的,則稱G是連通圖。
過程簡述
輸入:帶權連通圖(網)G的邊集E及頂點個數。(E已按權值的升序排序。)
初始:T=(V, {}), V是圖G的頂點集合且各頂點自成一個連通分量;表示邊的集合為空{}。
操作:重復以下操作,直到T中所有頂點都在同一個連通分量上。
- 依次取E中一條邊e(邊e必為未嘗試過的邊中權值最小的邊。因為{E}已按權值升序排序)。
- 將e的兩個頂點分別放入T的各連通分量集合V中以測試該頂點是否分別在不同連通分量中。
- 設存在一個方法/函數Find(V, vertex),從連通分量集合V的vertex頂點開始沿該連通分量查找,返回以vertex開始的連通分量的最后一個頂點(的下標)。令n=Find(V, e.Begin)和m=Find(V, e.End),若n≠m則e存在於T的不同連通分量中,故將e.End加入到以e.Begin開始的連通分量中去。
(注:e.Begin表示e的開始頂點;e.End表示e的結束頂點;雖然無向圖的邊不存在開始頂點或結束頂點,但是作為程序表示,也得有兩個值來表示邊的兩個頂點。)(為什么n≠m則兩個頂點分別位於不同的連通分量中?若v1、v4、v6位於同一個連通分量,v3、v7位於另一個連通分量。那么怎么表示這兩個連通分量呢?可以用一個數組來表示!數組的索引本身即是頂點v的下標,而v在數組中對應的存儲單元存有構成該連通分量的下一個頂點v’。用一個數組parent表示圖T的V。那么parent[1]=4,parent[4]=6,parent[6]=0,0表示該連通圖中已無別的頂點;parent[3]=7,parent[7]=0。對於邊(1, 6)(或(6, 1)),將1帶入parent數組,最終會沿着連通圖找到n=6;將6帶入parent數組,最終會沿着連通圖找到m=6。n=m所以這兩個頂點位於相同連通圖中。而對於邊(4, 7)(或(7, 4)),將4帶入parent數組,得到n=6;將7帶入parent數組,得到m=7。n≠m所以兩個頂點位於不同連通圖中。把點v7所處的連通圖放入v1、v4、v6構成的連通圖中:parent[6]=7(parent[n]=m)反過來點v4所處的連通圖放入v3、v7構成的連通圖中,即parent[7]=6也行,只采用兩者之一即可。)
- 直到T中所有頂點都在同一連通分量上為止。(E中的每一條邊都嘗試一遍即可。)
輸出:最小生成樹。
如何實現
輸入:用Edge類表示邊,其中Begin/End/Weight域分別表示邊的兩個頂點的下標及權重。Edge數組E表示邊集,N表示頂點個數。(E已按權值的升序排序。)
初始:用包含N個存儲單元的數組parent表示T=(V, {})的 V,即各頂點自成的連通分量。parent數組的下標i即為頂點的下標,i處存放的值parent[i]即為連通分量中下一個頂點的下標,parent[i]=0表示該連通分量已結束。將parent的各存儲單元初始化為0。
操作:重復以下操作,直到T中所有頂點都在同一個連通分量上。
- 依次取E中一條邊e。
- 將e.Begin和e.End帶入parent數組,找到連通分量中的最后一個頂點。n=Find(parent, e.Begin)和m=Find(parent, e.End),若n≠m則e存在於T的不同連通分量中,故將點e.End所處的連通圖加入到點e.Begin所處的連通圖中去,即parent[n]=m。(反過來parent[m]=n也行。)
- 直到E中的每一條邊都嘗試一遍即可。
輸出:最小生成樹。
如上面的這個圖G=(V, {E}),其中V={v0, v1, v2, v3, v4, v5, v6, v7, v8},E= {(v4, v7, 7), (v2, v8, 8), (v0, v1, 10), (v0, v5, 11), (v1, v8, 12), (v3, v7, 16), (v1, v6, 16), (v5, v6, 17), (v1, v2, 18), (v6, v7, 19), (v3, v4, 20), (v3, v8, 21), (v2, v3, 22), (v3,v6, 24), (v4, v5, 26)}
用一個邊集來表示該圖G,得上圖右邊的數組。
① 輸入:帶權連通圖G=(V, {E})的邊集合及頂點數目,求圖G的最小生成樹。
② 初始:T={V, {}},用數組parent=int[9]來表示V,parent數組記錄的是以索引i表示的頂點開始到parent[i]表示的頂點構成的連通圖。例如:(parent數組本身就含有兩個信息:索引和索引處的值,vertex數組是不存在的,只是為了輔助理解。)
非連通圖頭頂點下標vertex:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標parent:[ 0, 0, 8, 0, 7, 0, 0, 0, 0 ]
parent[2]=8,parent[8]=0即頂點v2、v8構成一個連通分量。
parent[4]=7,parent[7]=0即頂點v4、v7構成一個連通分量。
③ 操作:
- 上圖中,邊(4, 7, 7)權值最小,取該邊為e。
- 此時parent[4]=0,故n=4;parent[7]=0,故m=7;n≠m故parent[4]=7,將{v4}和{v7}這兩個連通圖合並為一個連通圖。執行后parent數組如下:
非連通圖頭頂點下標vertex:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標parent:[ 0, 0, 0, 0, 7, 0, 0, 0, 0 ]
解釋:parent[2]=0,即v2自成一個連通圖。parent[4]=7,parent[7]=0即v4所在連通圖中還有v7,v7接下來沒有別的頂點了,即v4、v7在同一個連通圖中。 - 從E中取下一條邊繼續上面1、2步驟的操作。
④輸出:
演示過程
(4,7) = 7
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 0, 0, 0, 0, 7, 0, 0, 0, 0 ]
(2,8) = 8
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 0, 0, 8, 0, 7, 0, 0, 0, 0 ]
(0,1) = 10
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 0, 8, 0, 7, 0, 0, 0, 0 ]
(0,5) = 11
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 0, 7, 0, 0, 0, 0 ]
(1,8) = 12
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 0, 7, 8, 0, 0, 0 ]
(3,7) = 16
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 7, 7, 8, 0, 0, 0 ]
(1,6) = 16
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 7, 7, 8, 0, 0, 6 ]
(6,7) = 19
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 7, 7, 8, 7, 0, 6 ]
運行結果
(4, 7) = 7
(2, 8) = 8
(0, 1) = 10
(0, 5) = 11
(1, 8) = 12
(3, 7) = 16
(1, 6) = 16
(6, 7) = 19
算法代碼
C#版
using System;
using System.Linq;
using System.Collections.Generic;
namespace Kruskal
{
class Program
{
static void Main(string[] args)
{
Edge[] edges = new Edge[] {
new Edge(){Begin = 4, End = 7, Weight = 7 },
new Edge(){Begin = 2, End = 8, Weight = 8 },
new Edge(){Begin = 0, End = 1, Weight = 10 },
new Edge(){Begin = 0, End = 5, Weight = 11 },
new Edge(){Begin = 1, End = 8, Weight = 12 },
new Edge(){Begin = 3, End = 7, Weight = 16 },
new Edge(){Begin = 1, End = 6, Weight = 16 },
new Edge(){Begin = 5, End = 6, Weight = 17 },
new Edge(){Begin = 1, End = 2, Weight = 18 },
new Edge(){Begin = 6, End = 7, Weight = 19 },
new Edge(){Begin = 3, End = 4, Weight = 20 },
new Edge(){Begin = 3, End = 8, Weight = 21 },
new Edge(){Begin = 2, End = 3, Weight = 22 },
new Edge(){Begin = 3, End = 6, Weight = 24 },
new Edge(){Begin = 4, End = 5, Weight = 26 },
};
int numberOfVertex = 9;
//Kruskal(edges, numberOfVertex);
Kruskal2(edges, numberOfVertex);
}
static void Kruskal(Edge[] edges, int numberOfVertex)
{
bool isDemonstrate = false; // (非必要代碼)
int[] vertex = new int[numberOfVertex]; // (非必要代碼)T連通圖的起始頂點。
int[] parent = new int[numberOfVertex]; // 若連通圖中存在環,那么從形成環的這條邊的
// 兩個頂點的任意頂點出發,都能沿着parent
// 數組找到相同的尾頂點下標。parent數組實際
// 存儲着一個或多個或多個連通圖。
if (isDemonstrate) // (非必要代碼)
{
for (int i = 0; i < numberOfVertex; i++)
{
vertex[i] = i;
}
}
for (int i = 0; i < numberOfVertex; i++) // 初始化路徑的各尾頂點下標。
{
parent[i] = 0;
}
edges.OrderBy(e => e.Weight); // 按權值的升序對邊集進行排序。
/** 從邊集中逐個取出邊,去測試這條邊是否會構
成環,不能構成環則將邊的尾頂點下標加入
parent數組中。*/
for (int i = 0; i < edges.Length; i++)
{
Edge edge = edges[i];
int n = Find(parent, edge.Begin),
m = Find(parent, edge.End);
if (n != m)
{
/** 若n與m不等,則此邊未與現有生成樹形成環路。
於是,將邊的尾頂點下標放入數組的下標為邊的
頭頂點的parent數組中。表示現在該尾頂點已經
在生成樹的集合中。*/
parent[n] = m; // 將邊的尾頂點下標放入數組parent。(兩者任選其一)
//parent[m] = n; // 將邊的頭頂點下標放入數組parent。(兩者任選其一)
string result = $"({edge.Begin}, {edge.End}) = {edge.Weight}";
Console.WriteLine(result); // 輸出邊。
if (isDemonstrate) // (非必要代碼)
{
Console.Write("非連通圖頭頂點下標vertex:");
PrintArray(vertex);
Console.Write("非連通圖尾頂點下標parent:"); // 查看parent數組。
PrintArray(parent);
}
}
}
}
static int Find(int[] parent, int vertex)
{
while (parent[vertex] > 0)
{
vertex = parent[vertex]; // 尋找路徑中下個頂點的下標。
}
return vertex;
}
static void PrintArray(int[] array)
{
Console.Write("[ ");
for (int i = 0; i < array.Length - 1; i++)
{ // 輸出數組的前面n-1個
Console.Write($"{ToInfinity(array[i])}, ");
}
if (array.Length > 0) // 輸出數組的最后1個
{
int n = array.Length - 1;
Console.Write($"{ToInfinity(array[n])}");
}
Console.WriteLine(" ]");
}
static string ToInfinity(int i) => i == int.MaxValue ? "∞" : i.ToString();
static void Kruskal2(Edge[] edges, int numberOfVertex)
{
var sets = new List<VertexSet>(); // 用於存放各連通分量的列表。
// 連通分量中頂點被放在一個頂點集合中。
for (int i = 0; i < numberOfVertex; i++) // 初始時,各頂點自成一個連通分量(頂點集合)。
{
sets.Add(new VertexSet(i));
}
edges.OrderBy(e => e.Weight); // 按權值的升序對邊集進行排序。
/** 從邊集中逐個取出邊,去測試這條邊是否會構
成環,不能構成環則將分別包含邊e的兩個頂點
的量連通分量(頂點集合)合並為tmp,然后從
連通分量列表中刪除這兩個連通分量,並將新合
成的連通分量tmp加入列表。*/
for (int i = 0; i < edges.Length; i++)
{
Edge edge = edges[i];
VertexSet n = Search(sets, edge.Begin),
m = Search(sets, edge.End);
if (n != m)
{
var tmp = n.Concat(m);
sets.Remove(n);
sets.Remove(m);
sets.Add(tmp);
string result = $"({edge.Begin}, {edge.End}) = {edge.Weight}";
Console.WriteLine(result); // 輸出邊。
}
}
//Console.WriteLine($"Number of Vertex Set: {sets.Count}");
}
static VertexSet Search(IList<VertexSet> s, int code)
{
return s.First(e => e.Has(code));
}
}
class Edge
{
public int Begin { get; set; }
public int End { get; set; }
public int Weight { get; set; }
}
class Vertex
{
public int Index { get; set; }
}
class VertexSet
{
public VertexSet() { }
public VertexSet(int index)
{
Add(new Vertex() { Index = index });
}
public HashSet<Vertex> Vertexes { get; } = new HashSet<Vertex>();
public Vertex Add(Vertex v)
{
Vertexes.Add(v);
return v;
}
public Vertex Remove(Vertex v)
{
if (Has(v))
{
return Vertexes.Remove(v) ? v : null;
}
return null;
}
public bool Has(Vertex v) => Has(v.Index);
public bool Has(int index) => Vertexes.Any(e => e.Index == index);
public VertexSet Concat(VertexSet second)
{
// 將當前頂點集合中的頂點和需要拼接的頂點集合中的頂點放入一個新的頂點集合vs中,
// 並返回該新的頂點集合vs。
VertexSet vs = new VertexSet();
for (int i = 0; i < Vertexes.Count; i++)
{
vs.Add(Vertexes.ElementAt(i));
}
for (int i = 0; i < second.Vertexes.Count; i++)
{
vs.Add(second.Vertexes.ElementAt(i));
}
return vs;
}
}
}
/**
(4,7) = 7
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 0, 0, 0, 0, 7, 0, 0, 0, 0 ]
(2,8) = 8
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 0, 0, 8, 0, 7, 0, 0, 0, 0 ]
(0,1) = 10
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 0, 8, 0, 7, 0, 0, 0, 0 ]
(0,5) = 11
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 0, 7, 0, 0, 0, 0 ]
(1,8) = 12
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 0, 7, 8, 0, 0, 0 ]
(3,7) = 16
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 7, 7, 8, 0, 0, 0 ]
(1,6) = 16
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 7, 7, 8, 0, 0, 6 ]
(6,7) = 19
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 7, 7, 8, 7, 0, 6 ]
*/
TypeScript版
class Edge {
Begin: number;
End: number;
Weight: number;
constructor(begin: number, end: number, weight: number) {
this.Begin = begin;
this.End = end;
this.Weight = weight;
}
}
function kruskal(edges: Edge[], numberOfVertex: number) {
let isDemonstrate: boolean = true; // (非必要代碼)
let vertex: number[] = []; // (非必要代碼)T連通圖的起始頂點。
let parent: number[] = []; /** 若連通圖中存在環,那么從形成環的這條邊的
兩個頂點的任意頂點出發,都能沿着parent
數組找到相同的尾頂點下標。parent數組實際
存儲着一個或多個或多個連通圖。*/
if (isDemonstrate) // (非必要代碼)
{
for (let i = 0; i < numberOfVertex; i++) {
vertex[i] = i;
}
}
for (let i = 0; i < numberOfVertex; i++) // 初始化路徑的各尾頂點下標。
{
parent[i] = 0;
}
edges.sort(e => e.Weight); // 按權值的升序對邊集進行排序。
/** 從邊集中逐個取出邊,去測試這條邊是否會構
成環,不能構成環則將邊的尾頂點下標加入
parent數組中。*/
for (let i = 0; i < edges.length; i++) {
let edge: Edge = edges[i];
let n: number = Find(parent, edge.Begin),
m: number = Find(parent, edge.End);
if (n != m) {
/** 若n與m不等,則此邊未與現有生成樹形成環路。
於是,將邊的尾頂點下標放入數組的下標為邊的
頭頂點的parent數組中。表示現在該尾頂點已經
在生成樹的集合中。*/
parent[n] = m; // 將邊的尾頂點下標放入數組parent。(兩者任選其一)
//parent[m] = n; // 將邊的頭頂點下標放入數組parent。(兩者任選其一)
let result: string = `(${edge.Begin}, ${edge.End}) = ${edge.Weight}`;
console.log(result); // 輸出邊。
if (isDemonstrate) // (非必要代碼)
{
console.log(`非連通圖頭頂點下標vertex:${printArray(vertex)}`);
// 查看parent數組。
console.log(`非連通圖尾頂點下標parent:${printArray(parent)}`);
}
}
}
}
function Find(parent: number[], vertex: number): number {
while (parent[vertex] > 0) {
vertex = parent[vertex]; // 尋找路徑中下個頂點的下標。
}
return vertex;
}
function printArray(array: number[]): string {
let str: string[] = [];
str.push("[ ");
for (let i = 0; i < array.length - 1; i++) // 輸出數組的前n-1個
{
str.push(`${toInfinity(array[i])}, `)
}
if (array.length > 0) // 輸出數組的最后1個
{
let n: number = array.length - 1;
str.push(`${toInfinity(array[n])}`);
}
str.push(" ]");
return str.join("");
}
function toInfinity(i: number) {
return i == Number.MAX_VALUE ? "∞" : i.toString();
}
function Main() {
let edges: Edge[] = [
new Edge(4, 7, 7),
new Edge(2, 8, 8),
new Edge(0, 1, 10),
new Edge(0, 5, 11),
new Edge(1, 8, 12),
new Edge(3, 7, 16),
new Edge(1, 6, 16),
new Edge(5, 6, 17),
new Edge(1, 2, 18),
new Edge(6, 7, 19),
new Edge(3, 4, 20),
new Edge(3, 8, 21),
new Edge(2, 3, 22),
new Edge(3, 6, 24),
new Edge(4, 5, 26),
];
let numberOfVertex: number = 9;
kruskal(edges, numberOfVertex);
}
Main();
/**
運行結果:
(4, 7) = 7
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 0, 0, 0, 0, 7, 0, 0, 0, 0 ]
(2, 8) = 8
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 0, 0, 8, 0, 7, 0, 0, 0, 0 ]
(0, 1) = 10
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 0, 8, 0, 7, 0, 0, 0, 0 ]
(0, 5) = 11
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 0, 7, 0, 0, 0, 0 ]
(1, 8) = 12
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 0, 7, 8, 0, 0, 0 ]
(3, 7) = 16
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 7, 7, 8, 0, 0, 0 ]
(1, 6) = 16
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 7, 7, 8, 0, 0, 6 ]
(6, 7) = 19
非連通圖頭頂點下標:[ 0, 1, 2, 3, 4, 5, 6, 7, 8 ]
非連通圖尾頂點下標:[ 1, 5, 8, 7, 7, 8, 7, 0, 6 ]
*/
復雜度
它的時間復雜度為O(eloge)(e為網中的邊數),所以,適合於求邊稀疏的網的最小生成樹。
參考資料:
《大話數據結構》 - 程傑 著 - 清華大學出版社
之前不會Markdown語法的角標(Subscript),所以分成了兩篇文章。這里將之前的合成整理為一篇。