最小生成树

  给定一个无向图, 是子图中任意两个顶点相互连通, 而且是一个树, 那么这是生成树(Spanning Tree); 倘若边上有权值, 那么使得边权和最小的生成树就是最小生成树(MST)!

  生活最小生成树的问题十分常见, 比如说要在几个城市之间修建铁路, 问如何修建铁路, 可以使得城市之间相互连通同时成本最小? 这就是一个MST 问题!

 

  最小生成树有着两种算法, 一个是 Kruskal 算法 以及 Prim 算法!

  最小生成树的 Prim 算法(点(集)出发) :是从某一个仅仅只有一个定点的树出发, 不断地添加最短边的算法,进而不断地扩大树的规模, 最终将所有的点都扩充进去。

  那么在此我们使用反证法来证明一下

    首先我们在算法运行过程中, 得到了一个半成品 T , 他是包含各 节点 X , 剩下的节点是 V \ X; 同时, 我们假设这两点点的集合当中, 有一个最小的连接边 是 e; 假设存在一个最小生成树, 包括了我们如今算法运行的半成品T,  下面有着两种情况, 

    1.   e 也是在这个最小生成树中, 那么算法成立;

    2.    那么 e 不在这个最小生成树当中, 我们不妨 把 e给添加进去, 因为这是一个最小生成树,所以他会形成一个回路, 那么由之前我们对 e 这条边的定义来看, 可以把那个回路边去掉, 添加上这个新的边, 最小生成树仍然成立;

  那么我们可以根据这个从一个节点, 不断地进行向外扩张, 最终得到了最小生成树;(这个类似于数学归纳法!!)

  算法中我们需要先确定一个起点, 然后再不断地进行扩张哦, 还需要不断地进行其他点到这个半成品树距离的更新。

  这个数组分为了 mincost 数组 和 cost 数组, 一个是进行更新距离, 一个是记录初始输入的边权。

 

 

  最小生成树的Kruskal算法 (边出发)

    该算法是按照边的权值顺序, 从大到小的进行排序, 如果不产生圈(重边)的话, 我们就把他放进去 , 否者下一个边;

    特点是使用了并查集, 高效的进行了点与点直接连通关系的维护;

  下面是两个算法的具体实施代码: 

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <cctype>
#include <algorithm>
#include <string>
#include <vector>
#include <queue>
#include <list>
#include <map>
#include <stack>
#include <set>
using namespace std;
const int MAX_V = 105;
const int MAX_E = 1000;
const int INF = 1e7;
int V, E;                   //点的数量, 边的数量
int cost[MAX_V][MAX_V];     //边权
int mincost[MAX_V];  //到现有生成树的距离
bool used[MAX_V];           //标记这个点是否被使用了
int par[MAX_V], ran[MAX_V]; //并查集的要素
struct node{
    int from, to, cost;     //边的三种属性
};
node edge[MAX_E];           //对边属性的存储
//Initial
void Initial(void){
    for(int i = 0; i < V; i++){
        ran[i] = 1, par[i] = i;
    }
}
//Find
int Find(int x){
    if(x == par[x]) return x;
    else return par[x] = Find(par[x]);
}
//Same
bool Same(int x, int y){
    return Find(x) == Find(y);
}
//Unite
void Unite(int x, int y){
    x = Find(x), y = Find(y);
    if(x == y)  return;
    else if(ran[x] < ran[y])    par[x] = y;
    else{
        par[y] = x;
        if(ran[x] == ran[y])    ran[x]++;
    }
}
// sort cmp;
int cmp(const void *a, const void *b){
    node *aa = (node *)a;
    node *bb = (node *)b;
    return aa->cost - bb->cost;
}

int main()
{
   /* cin >> V >>E;   //输入 vertax edge number!
    for(int i = 0; i < E; i++){
        int a, b, c;    scanf("%d%d%d", &a, &b, &c);
        cost[a][b] = cost[b][a] = c;
        edge[i].from = a, edge[i].to = b, edge[i].cost = c;
    }
    qsort(edge, E, sizeof(edge[0]), cmp);  int res = 0;
    printf("THE SHOW : \n");
    for(int i = 0; i < E; i++){
        printf("%d %d %d\n", edge[i].from, edge[i].to, edge[i].cost);
    }
    printf("END OF SHOW\n");
    // KRUSKAL 算法, 进行运算最小生成树
    Initial();
    for(int i = 0; i < E; i++){
        int a = edge[i].from, b = edge[i].to, c = edge[i].cost;
        if(Same(a, b))  continue;
        else Unite(a, b), res += c;
    }
    printf("THE MINCOST IS : %d\n", res);*/
    //prime 算法
    while(~scanf("%d", &V)){
        for(int i = 0; i < V; i++){
            for(int j = 0; j < V; j++)  scanf("%d", &cost[i][j]);
        }
        //数据初始化操作
        for(int i = 0; i < V; i++){
            used[i] = false;
            mincost[i] = INF;
        }
        mincost[0] = 0;         //这个没大要求, 只需要设置一个点作为最初的最小生成树就OK;
        int res = 0;
        while(true){
            int v = -1;
            for(int i = 0; i < V; i++){             //寻找最短距离点
                if(!used[i]&&(v == -1 || mincost[i] < mincost[v]))  v = i;
            }
            if(v == -1) break;                  //循环完毕;
            res += mincost[v];  used[v] = true;     //进行数的更新
            for(int i = 0; i < V; i++){
                mincost[i] = min(mincost[i], cost[v][i]);
            }
        }
       // printf("THE RESULT IS : ");
        printf("%d\n", res);
    }
    return 0;
}

/*

KRUSKAL DATA:
7 9
0 1 2
0 3 10
1 4 7
3 4 5
1 6 1
1 2 3
2 4 1
2 5 5
4 5 8

PRIM DATA :
4
0 4 9 21
4 0 8 17
9 8 0 16
21 17 16 0
*/

 

 

 

 

 

 

 

  注意事项:

  1.   mincost 数组的更新, 和我们之前提到的最短路数组的更新不同, mincost 数组的更新, 仅仅是 min( mincost [j], cost [i] [j] ) ;
  2.   注意边长的编号是从哪里开始的? 如果是 1 的话需要进行减减, 否则的话直接输入就可以了。
  3.        并查集一定要记得初始化!!! Initial   别只写函数, 不初始化

 

 

   上面的是目前网上主流的算法, 在这里,我们介绍其他两种不是很常见的方法:

  破圈法:

    我们知道, 上面的两种算法, 一个是 Prim 算法, 是从一个点为一棵树出发, 不断地更新树, 往里面加顶点, 最后得到最小生成树, 也可以说他是加点法;  Kruskal 的算法是不断地往里面加边,更新树, 最后得到最小生成树, 也可以说的上是加边法;  但是我们都可以看出, 这两种算法都不会形成圈, 也就是说, 他们都是避免圈的存在的前提下, 更新树的点、边;  然而我们这种算法破圈法, 思路是从一个完整的图进行出发, 不断地删去回路上面的最大边, 直到图中不存在回路, 算法完成, 这个思路就是我们所说的破圈法。

    

  Sollin算法:

  这个算法是一个十分古老的算法

posted @ 2019-09-01 10:08  lucky_light  阅读(561)  评论(0编辑  收藏  举报