Kruscal 算法(C++)

Kruscal 算法(C++)

1. Kruscal 算法简介

Kruscal 最小生成树算法起始于一个空的图,并按照以下规则从边集中选择边。

不断重复地选择未被选中的边中权重最轻不会形成环的一条。

Kruscal 算法通过逐条增加边来构造最小生成树。在保证不出现环的同时,它总简单地选择当前所余的权重最轻的边。这是一个典型的贪心算法,即每次决策都对应于最明显的即时利益。

下面是《算法概论》一书中给出的伪代码。

2. 代码部分

下面将伪代码改写成 C++ 程序。

  1. 确定图的表示方式

    这里我选择建立一个结构体表示边的信息,u、v、w 分别表示边的 2 个端点和边权。

    struct Edge
    {
        int u, v, w;
    };
    Edge graph[MAXN];
    
  2. 编写 _find 和 _union

    有两种方法,分别是基于等级的合并路径压缩,这里先把它看做黑箱,接下来再详细阐述。

    但这两种方法都要使用 Rank 数组,下面先给出一些要用到的全局变量以及初始化 Rank 的 makeset 函数。

    int n, m;   					// n个结点,m条无向边
    int parent[MAXN], Rank[MAXN];	// 基于等级的合并
    int min_weight = 0;      		// 最小生成树的权值
    int cnt = 0;            		// 统计最小生成树的边数,判断是否连通
    
    void makeset(int x)
    {
        parent[x] = x;
        Rank[x] = 0;
    }
    
  3. Kruscal 核心代码——参照伪代码写出

    void kruskal()
    {
        for (int i = 0; i < n; ++i)
        {
            makeset(i); // 初始化
        }
    
        sort(graph, graph + m, [](const Edge &a, const Edge &b) -> bool
             { return a.w < b.w; });	// 这里使用C++11的lambda,也可以写一个cmp函数
        
        for (int i = 0; i < m; ++i)
        {
            int x = _find(graph[i].u), y = _find(graph[i].v);
            if (x != y) 				// 第i条边的两个端点u、v属于不同集合
            {
                min_weight += graph[i].w;
                _union(x, y);
                ++cnt;
            }
            //if (cnt == n-1) break;    // 选了n-1条边就结束了,也可以不加,因为剩下的边的顶点肯	
            							// 定都在最小生成树内了,接下来的循环中if总是判断为假。
        }
    }
    

3. 并查集的编写方式

3.1 基于等级的合并

存储集合的方法之一是采用有向树,树的结点对应集合中的元素,每个结点都包含一个父指针(使用parent数组实现)。父指针使得结点一级级相连并最终指向树的根,我们用树根元素来代表整个集合

与其他元素不同,树根的父指针指向该元素自身,所以我们使用 makeset 初始化时将每个元素的 parent 初始化为自身。对于节点的等级信息,我们将其解释为其下悬挂的子树的高度

void makeset(int x)
{
    parent[x] = x;
    Rank[x] = 0;
}

find 函数的实现很简单,只需要沿着节点的父指针找到树的根。_find 的执行时间和树的高度成正比

int _find(int x)
{
    while (x != parent[x])
        x = parent[x];
    return x;
}

合并两个树(集合)的过程也很简单,只需要将一个树的根(的父指针)指向另一个树的根。由于树的高度影响计算效率,我们应该尽可能使树的高度小一点。因此,我们选择让较低的树的根指向较高的树的根,这样一来,除非将要合并的树等高,否则不会使合并后的树总高度增加。

void _union(int x, int y)
{
    int rx = _find(x);
    int ry = _find(y);
    if (rx == ry)
        return;             // x和y的根相同,已经在同一个集合中

    if (Rank[rx] > Rank[ry])
    {
        parent[ry] = rx;    // 让较低的树根指向较高的树根
    }
    else
    {
        parent[rx] = ry;
        if (Rank[rx] == Rank[ry])		// 两个树等高的情形
            ++Rank[ry];
    }
}

下面是合并的具体过程。

3.2 路径压缩

由上文易知,find 操作的时间复杂度为 O(logh),h 为树高,采用路径压缩可以将总的代价分摊,由 O(logh) 下降到略微超过 O(1)。在每次 find 操作中,当沿着一系列的父指针找到树根后,我们可将这些父指针直接指向树根

int _find(int x)
{
	if (x != parent[x]) parent[x] = _find(parent[x]);
	return parent[x];
}

4. 测试

测试题目,洛谷 P3366 【模板】最小生成树

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

#define MAXN 200001

int n, m;                       // n个结点,m条无向边
int parent[MAXN], Rank[MAXN];
int min_weight = 0;             // 最小生成树的权值
int cnt = 0;                    // 统计最小生成树的边数,判断是否连通

struct Edge
{
    int u;
    int v;
    int w;
};
Edge graph[MAXN];

void makeset(int x)
{
    parent[x] = x;
    Rank[x] = 0;
}

int _find(int x)
{
    while (x != parent[x])
        x = parent[x];
    return x;
}

void _union(int x, int y)
{
    int rx = _find(x);
    int ry = _find(y);
    if (rx == ry)
        return;             // x和y的根相同,已经在同一个集合中

    if (Rank[rx] > Rank[ry])
    {
        parent[ry] = rx;    // 让较低的树根指向较高的树根
    }
    else
    {
        parent[rx] = ry;
        if (Rank[rx] == Rank[ry])       // 两个树等高的情形
            ++Rank[ry];
    }
}

void kruskal()
{
    for (int i = 0; i < n; ++i)
    {
        makeset(i); // 初始化
    }

    sort(graph, graph + m, [](const Edge &a, const Edge &b) -> bool
         { return a.w < b.w; });
    
    for (int i = 0; i < m; ++i)
    {
        int x = _find(graph[i].u), y = _find(graph[i].v);
        if (x != y)             // 第i条边的两个端点u、v属于不同集合
        {
            min_weight += graph[i].w;
            _union(x, y);
            ++cnt;
        }

        //if (cnt == n-1) break;        // 选了n-1条边就结束了,也可以不加,因为剩下的边的顶点肯	
        							    // 定都在最小生成树内了,接下来的循环中if总是判断为假
    }
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < m; ++i)
    {
        cin >> graph[i].u >> graph[i].v >> graph[i].w;
    }
    kruskal();
    if (cnt != n - 1)
        cout << "orz" << endl;
    else
        cout << min_weight << endl;
}
posted @ 2021-05-21 11:13  CoolGin  阅读(331)  评论(0编辑  收藏  举报