克鲁斯卡尔(Kruskal)算法

概览

相比于普里姆算法(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的邻接矩阵表示

如上面的这个图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构成一个连通分量。
③ 操作:

  1. 上图中,边(4, 7, 7)权值最小,取该边为e。
  2. 此时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在同一个连通图中。
  3. 从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),所以分成了两篇文章。这里将之前的合成整理为一篇。

posted @ 2021-05-22 21:16  kokiafan  阅读(1092)  评论(0编辑  收藏  举报