图论——最小生成树

Posted 牧空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图论——最小生成树相关的知识,希望对你有一定的参考价值。

Kruskal算法

原理

任何选择一些点属于集合A,剩余的点属于集合B,最小生成树必定存在一条权值最小的边,并且这条边的两个顶点分别属于集合A和集合B(即连通两个集合)

算法步骤

虽然在原理中是在两个点集中中边,但是通过点集去找边需要反复遍历某个点集中所有点的边,时间复杂度会很高,这里从边开始考虑,遍历所有边,自然也会涉及到所有的点,使用并查集可以判断边的两个点是否在同一个集合内

  1. 初始化所有顶点属于孤立的集合,即G为森林,每个节点为一棵树
  2. 按照边权递增顺序遍历所有边,若遍历到的边的两个顶点仍分属不同的集合(该边即为连通这两个集合的边中权值最小的那条边),则确定改变为最小生成树上的一条边,并将该边两个顶点分属的集合合并
  3. 遍历完所有边后,若原图连通,则被选取的边和所有顶点构成最小生成树;若原图不连通,最小生成树不存在

例题

OJ——继续畅通工程

描述
省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可)。现得到城镇道路统计表,表中列出了任意两城镇间修建道路的费用,以及该道路是否已经修通的状态。现请你编写程序,计算出全省畅通需要的最低成本。
输入描述:
测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( 1< N < 100 );随后的 N(N-1)/2 行对应村庄间道路的成本及修建状态,每行给4个正整数,分别是两个村庄的编号(从1编号到N),此两村庄间道路的成本,以及修建状态:1表示已建,0表示未建。 当N为0时输入结束。
输出描述:
每个测试用例的输出占一行,输出全省畅通需要的最低成本。

//来自王道机试
#include <iostream>
#include <cstdio>
#include <algorithm>

#define MAXN 100

using namespace std;

struct Edge
{
    int from;
    int to;
    int length;
    bool operator<(const Edge &e) const
    {
        return length < e.length;
    }
};

Edge edge[MAXN * MAXN]; //边集
int father[MAXN];
int height[MAXN];

void Initial(int n)
{
    for (int i = 0; i <= n; i++)
    {
        father[i] = i;
        height[i] = 0;
    }
}

int Find(int x)
{
    if (x != father[x])
    {
        father[x] = Find(father[x]);
    }
    return father[x];
}

void Union(int x, int y)
{
    x = Find(x);
    y = Find(y);
    if (x != y)
    {
        if (height[x] < height[y]) //x<-z y --> x<-z x<-y
        {
            father[x] = y;
        }
        else if (height[x] > height[y]) //x y<-z --> y<-z y<-x
        {
            father[y] = x;
        }
        else
        {
            father[y] = x;
            height[x]++;
        }
    }
    return;
}

int Kruskal(int n, int edgeNumber)
{
    Initial(n);
    sort(edge, edge + edgeNumber);
    int sum = 0;
    for (int i = 0; i < edgeNumber; i++)
    {
        Edge current = edge[i];
        if (Find(current.from) != Find(current.to))
        {
            Union(current.from, current.to);
            sum += current.length;
        }
    }
    return sum;
}

int main()
{
    int n;
    while (scanf("%d", &n) != EOF) //顶点数
    {
        if (n == 0)
            break;
        int edgeNumber = n * (n - 1) / 2; 
        for (int i = 0; i < edgeNumber; i++)
        {
            int status = 0;
            scanf("%d%d%d%d", &edge[i].from, &edge[i].to, &edge[i].length, &status);
            //已建成的路不可能拆掉,且必然会算在最短生成树中,所以将长度置为0,表示消耗为0
            if (status == 1) 
                edge[i].length = 0;     
        }
        int cost = Kruskal(n, edgeNumber);
        printf("%d\\n", cost);
    }
    return 0;
}

Prim算法

原理

贪心,持续寻找相邻可达的最近节点

算法步骤

  1. 用两个集合A{},B{}分别表示找到的点集,和未找到的点集;
  2. 我们以A中的点为起点a,在B中找一个点为终点b(可达的,不可达的距离为无穷大),这两个点构成的边(a,b)的权值是其余边中最小的,将找到的b置入集合A
  3. 重复上述步骤2,直至B中的点集为空,A中的点集为满

例题

OJ——还是畅通工程

描述
某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。
输入描述:
测试输入包含若干测试用例。每个测试用例的第1行给出村庄数目N ( < 100 );随后的N(N-1)/2行对应村庄间的距离,每行给出一对正整数,分别是两个村庄的编号,以及此两村庄间的距离。为简单起见,村庄从1到N编号。 当N为0时,输入结束,该用例不被处理。
输出描述:
对每个测试用例,在1行里输出最小的公路总长度。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#define MAXN 101

using namespace std;

/**
 * Prim算法
 * */

vector<vector<int>> Graph; //邻接矩阵


// n为节点个数
int Prim(int n)
{
    int lowCost[MAXN]; //lowCost[j] 存储节点j与当前已选节点集合相连的最小权值,如果lowCost[j]==0,说明j节点在已选择的点集中
    int closest[MAXN]; //closest[j] 存储lowCost[j]对应的连接点
    int newVertex = 0; //newVertex:最新加入集合的点
    int sum = 0;
    // 使用第一个节点作为起始节点
    for (int i = 0; i < n; i++)
    {
        closest[i] = 0;
        lowCost[i] = Graph[0][i];
    }
    // 找接下来的n-1个点
    for (int i = 1; i < n; i++)
    {
        // 在lowCost中寻找权重最小的边,并记录该节点
        int min = INT32_MAX, newVertex = -1;
        for (int j = 0; j < n; j++)
        {
            if (lowCost[j] != 0 && lowCost[j] < min)
            {
                min = lowCost[j];
                newVertex = j;
            }
        }
        // 将该点置入已选择的点集中,并累计边的权重
        lowCost[newVertex] = 0;
        sum += min;
        // 根据新点集更新lowCost,只要遍历index所连接的点并与已有的lowCost比较
        for (int j = 0; j < n; j++)
        {
            // 未选中的点且新加入的点到其距离小于已记录的最小值
            if (lowCost[j] != 0 && Graph[newVertex][j] < lowCost[j])
            {
                lowCost[j] = Graph[newVertex][j];
                closest[j] = newVertex;     //同时也更新点的连接对象
            }
        }
    }
    return sum;
}


int main()
{
    int n;
    while (scanf("%d", &n) != EOF) //顶点数
    {
        if (n == 0)
            break;
        int edgeNumber = n * (n - 1) / 2;
        Graph.clear();
        Graph.resize(n);
        for (int i = 0; i < n; i++)
            Graph[i].resize(n);

        for (int i = 0; i < edgeNumber; i++)
        {
            int x, y, len;
            scanf("%d%d%d", &x, &y, &len);
            Graph[x - 1][y - 1] = Graph[y - 1][x - 1] = len;
        }

        int cost = Prim(n);
        printf("%d\\n", cost);
    }
    return 0;
}

以上是关于图论——最小生成树的主要内容,如果未能解决你的问题,请参考以下文章

图论之最小生成树最小生成树专题

图论——迪杰斯特拉算法和最小生成树

图论 最小生成树

怎么求最小生成树 (离散数学 图论)

浅谈图论——最小生成树

图论讲解——最小生成树