码迷,mamicode.com
首页 > 其他好文 > 详细

最小生成树

时间:2021-03-17 15:05:25      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:中间   距离   clu   puts   注意   size   生成森林   return   ++   

最小生成树基础

定义

对于图 $ G = (V,E) $, 有 \(n\) 个点, \(m\) 条边, 由 \(V\) 中所有 \(n\) 个点和 \(E\)\(n-1\) 条边构成的一个连通子图(即一棵树),称为 \(G\) 的一个生成树, 边权值最小的为最小生成树.

求解方法:

  1. prim算法 \(O(n^2)\)
  2. kruskal算法 \(O(mlogn)\)

prim算法

一般用于稠密图:

#include <iostream>
#include <cstring>

using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 510;
int n,m;
int g[N][N];    //稠密图
int dist[N];    //表示某个结点到当前集合的最小距离(与dijkstra不同)
int st[N];      //是否在集合内

int prim()
{
    memset(dist, INF, sizeof dist);
    
    int res = 0;
    for (int i = 0; i < n; i ++ ){
        
        int t = -1; 
        for (int j = 1; j <= n; j ++ )          //寻找不在集合内,且到集合距离最小的结点
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
            
        st[t] = 1;      //进入集合
        
        if (i && dist[t] == INF)    return INF; //不存在生成树
        if (i)  res += dist[t];
            
        for (int j = 1; j <= n; j ++ )      //更新其它结点
            dist[j] = min(dist[j], g[t][j]);
    }
    return res;
}

int main()
{
    cin >> n >> m;
    
    memset(g, INF, sizeof g);
    
    for (int i = 1; i <= m; i ++ ){
        int a, b, v;
        cin >> a >> b >> v;
        g[a][b] = g[b][a] = min(g[a][b], v);    //无向图
    }
    
    int t = prim();
    
    if (t == INF)
        puts("impossible");
    else    
        cout << t << endl;
        
    return 0;
}

kruskal算法

一般用于稀疏图

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

const int N = 1e5 + 10;
const int M = 2 * N;
int n,m;

int f[N];       //并查集的操作

struct Edge{
    int a, b;
    int v;
}edge[M];

bool comp(Edge x, Edge y)       //自定义比较
{
    return x.v < y.v;
}

int find(int x)                 //寻找祖宗结点
{
    if (f[x] != x)  return f[x] = find(f[x]);
    return f[x];
}

int main()
{
    cin >> n >> m;
    
    for (int i = 1; i <= m; i ++ ){
        int a, b, v;
        cin >> a >> b >> v;
        edge[i] = {a, b, v};
    }
    
    sort(edge + 1, edge + m + 1, comp);
    
    for (int i = 1; i <= n; i ++ )      //并查集的初始化
        f[i] = i;
    
    int res = 0;        //记录距离之和
    int cnt = 0;        //存储结点数量
    for (int i = 1; i <= m; i ++ ){      //枚举每条边
        
        int a = edge[i].a;
        int b = edge[i].b;
        int v = edge[i].v;
        
        int fa = find(a);
        int fb = find(b);
        
        if (fa != fb){           //合并集合
            f[fa] = fb;
            res += v;
            cnt ++;
        }
    }
    if (cnt < n - 1)
        puts("impossible");
    else
        cout << res << endl;
    
    return 0;
    
}

简单应用

AcWing 1140. 最短网络

算法思路:

最小生成树的模板题, 输入矩阵形式, 采用prim算法

#include <iostream>
#include <cstring>

using namespace std;

const int N = 110;

int g[N][N];
int n;
int dist[N];
int st[N];

int prim()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    int res = 0;
    
    for (int i = 1; i <= n; i ++ ){
        int t = -1;
        
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        
        st[t] = 1;
        
        res += dist[t];
        
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], g[t][j]);
    }
    
    return res;
}

int main()
{
    cin >> n;
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            cin >> g[i][j];
        
    cout << prim() << endl;
    
    return 0;
}

AcWing 1141. 局域网

算法思路 :

kruakal算法求最小生成树, 注意每次当两个点需要删边时, 将结果加上权值.

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110, M = 220;

struct Edge{
    int a, b, v;
    
    bool operator < (const Edge &t)const 
    {
        return v < t.v;
    }
}edge[M];
int fa[N];
int n ,m;

int find(int x)
{
    if (fa[x] != x)
        fa[x] = find(fa[x]);
    else
        return x;
}


int main()
{
    cin >> n >> m;
    int res = 0;
    for (int i = 1; i <= n; i ++ )
        fa[i] = i;
    
    for (int i = 1; i <= m; i ++ ){
        int a, b, v;
        cin >> a >> b >> v;
        edge[i] = {a, b, v};
    }
    
    sort(edge + 1, edge + m + 1);
    
    for (int i = 1; i <= m; i ++ )
        cout << edge[i].v << endl;
    for (int i = 1; i <= m; i ++ ){
        int a = edge[i].a;
        int b = edge[i].b;
        int v = edge[i].v;
        
        int aa = find(a);
        int bb = find(b);
        
        if (aa != bb)   fa[aa] = bb;
        else
            res += v;
    }
    
    cout << res << endl;
    
    return 0;
}

AcWing 1142. 繁忙的都市

算法思路 :

同样是模板题, 注意理解题意, 要求被改造的道路能够连通整个图(被选入生成树里的点)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 310, M = 8010;

struct Edge{
    int a, b, v;
    bool operator < (const Edge &t)const
    {
        return v < t.v;
    }
}edge[M];

int n,m;
int fa[N];
int sum;

int find(int x)
{
    if (fa[x] != x)
        fa[x] = find(fa[x]);
    else
        return x;
}


int main()
{
    cin >> n >> m;
    int res = 0;
    for (int i = 1; i <= m; i ++ ){
        int a, b, v;
        cin >> a >> b >> v;
        edge[i] = {a, b, v};
    }
    
    sort(edge + 1, edge + m + 1);
    
    for (int i = 1; i <= n; i ++ )
        fa[i] = i;
        
    for (int i = 1; i <= m; i ++ ){
        int a = edge[i].a;
        int b = edge[i].b;
        int v = edge[i].v;
        
        int aa = find(a);
        int bb = find(b);
        //cout << aa << ‘ ‘ << bb << endl;
        if (aa == bb)
            continue;
        else{
            sum ++;
            fa[aa] = bb;
            res = v;
            
        }
    }
    
    cout << sum << ‘ ‘ << res << endl;
    
    return 0;
    
}

AcWing 1143. 联络员

算法思路 :

图中含有必选边和非必选边, 对于必选边我们直接加入到生成树中, 然后再根据kruskal算法加入非必选边

  • kruskal算法求最小生成树可以从中间开始
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 2010, M = 10010;

struct Edge{
    int op, a, b, v;
    bool operator < (const Edge &t)const
    {
        if (op == t.op)
            return v < t.v;
        return op < t.op;
    }
}edge[M];
int fa[N];
int n, m;


int find(int x)
{
    if (fa[x] != x)
        fa[x] = find(fa[x]);
    else
        return x;
}

int main()
{
    cin >> n >> m;
    int res = 0;
    for (int i = 1; i <= n; i ++ )
        fa[i] = i;
        
    for (int i = 1; i <= m; i ++ ){
        int op, a, b, v;
        cin >> op >> a >> b >> v;
        edge[i] = {op, a, b, v};
    }
    
    sort(edge + 1, edge + m + 1);
    
    for (int i = 1; i <= m; i ++ ){
        int op = edge[i].op;
        int aa = find(edge[i].a);
        int bb = find(edge[i].b);
        int v = edge[i].v;
        
        if (op == 1){
            res += v;
            if (aa != bb)
                fa[aa] = bb;
        }else{
            if (aa == bb)
                continue;
            else{
                res += v;
                fa[aa] = bb;
            }
        }
    }

        
    cout << res << endl;
    
    return 0;
}

AcWing 1144. 连接格点

算法思路 :

原图中有 \(m * n\) 个点, 其中横向点之间、纵向点之间都有边, 根据kruskal算法,先加入纵向边(权值小),再加入横向边(权值大).
我们可以直接在加边时进行排序,避免sort().

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 2 * 1e6 + 10, M = 2 * 1e6 + 10;
int g[1010][1010];
struct Edge{
    int a, b, v;
    
    bool operator < (const Edge &t)const
    {
        return v < t.v;
    }
}edge[M];
int fa[N];
int n, m;

int find(int x)
{
    if (fa[x] != x)
        fa[x] = find(fa[x]);
    return fa[x];
}

int main()
{
    cin >> n >> m;
    for (int i = 1, t = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            g[i][j] = t ++;
    
    for (int i = 1; i <= n * m; i ++ )
        fa[i] = i;
    
    int cnt = 0;
    for (int i = 1; i <= m; i ++ )
        for (int j = 1; j < n; j ++ ){
            int a = g[j][i], b = g[j + 1][i];
            int v = 1;
            //cout << a << ‘ ‘ << b << endl;
            edge[cnt ++] = {a, b, v};
        }
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j < m; j ++ ){
            int a = g[i][j], b = g[i][j + 1];
            int v = 2;
            //cout << a << ‘ ‘ << b << endl;
            edge[cnt ++] = {a,b,v};
        }
    int x1, y1, x2, y2;
    while (~scanf("%d%d%d%d",&x1, &y1, &x2, &y2)){
        int a = g[x1][y1];
        int b = g[x2][y2];
        int aa = find(a);
        int bb = find(b);
        fa[aa] = bb;
    }

    int res = 0;
    
    for (int i = 0; i < cnt; i ++ ){
        int aa = find(edge[i].a);
        int bb = find(edge[i].b);
        int v = edge[i].v;
        if (aa == bb)
            continue;
        else{
            res += v;
            fa[aa] = bb;
        }
    }
    
    cout << res << endl;
    
    return 0;
    
}

拓展应用

关于最小生成树的基本定理:

  1. 任意一棵最小生成树一定包含无向图中权值最小的边.
  2. 给定一张无向图 \(G = (V, E)\) , 从 \(E\) 中选出 \(k < n - 1\) 条边构成的 \(G\) 的一个生成森林, 若再从剩余的 \(m - k\) 条边中选 \(n - 1 - k\) 条添加到生成森林中, 使其成为 \(G\) 的生成树,并且选出的边的权值之和最小,则该生成树一定包含这 \(m - k\) 条边中连接生成森林的两个不连通的节点的权值最小的边.

AcWing 1146. 新的开始

算法思路 :

图论中常用的假设虚拟原点问题, 假设一个虚拟原点, 到点 \(i\) 的边的权值为在该点建立电站\(v_{i}\), 求一个包含所有点的最小生成树.

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 310;

int g[N][N];
int v[N];
int n;
int dist[N];
int st[N];

int prim()
{
    memset(dist, 0x3f, sizeof dist);
    dist[n + 1] = 0;
    int res = 0;
    for (int i = 0; i <= n; i ++ ){
        int t = -1;
        
        for (int j = 1; j <= n + 1; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        
        st[t] = 1;
        
        if (i)
            res += dist[t];
        
        for (int j = 1; j <= n + 1; j ++ )
            dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}


int main()
{
    cin >> n;
    
    for (int i = 1; i <= n; i ++ )
        cin >> v[i];
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            cin >> g[i][j];
    for (int i = 1; i < n + 1; i ++ )
        g[n + 1][i] = g[i][n + 1] = v[i];
    
    g[n + 1][n + 1] = 0;
    
    cout << prim() << endl;
    
    return 0;
}

AcWing 1145. 北极通讯网络

算法思路:

图中有 \(n\) 个点, 有 \(n*n\) 条路径. 假设起始状态都不连通, 即有 \(n\) 个连通块, 根据kruakal算法,每次找到一个边权值最小的点, 把该点加入到生成树中, 直到剩余 \(k\) 个连通块, 我们可以直接用卫星连通.

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;
typedef pair<int,int> PII;
const int N = 510, M = N * N;
int n, k;

struct Edge{
    int a, b;
    double v;
    
    bool operator < (const Edge &t)const
    {
        return v < t.v;
    }
}edge[M];
PII p[N];
int fa[N];

int find(int x)
{
    if (fa[x] != x)
        fa[x] = find(fa[x]);
    return fa[x];
}

double get_len(PII a, PII b)
{
    double dx = a.first - b.first;
    double dy = a.second - b.second;
    return sqrt(dx * dx + dy * dy);
}

int main()
{
    cin >> n >> k;
    for (int i = 1; i <= n; i ++ ){
        cin >> p[i].first >> p[i].second;
        fa[i] = i;
    }
    
    int cnt = 0;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ ){
            double v = get_len(p[i], p[j]);
            edge[cnt ++] = {i,j,v};
        }
    
    sort(edge, edge + cnt);
    
    
    double ans = 0;
    int block = n;
    for (int i = 0; i < cnt; i ++ ){
        int aa = find(edge[i].a);
        int bb = find(edge[i].b);
        double v = edge[i].v;
        if (block == k)
            break;
        ans = v;
        
        if (aa != bb){
            block --;
            fa[aa] = bb;
        }
        
        
    }
    
    printf("%.2f", ans);
    
    return 0;
}

AcWing 346. 走廊泼水节

算法思路:

完全图 : 完全图是一个简单的无向图,其中每对不同的顶点之间都恰连有一条边相连.

题目要求: 满足图的唯一最小生成树仍然是原树, 同时, 要求所加权值之和最小.
每次将两个连通块通过已有边连接时, 需要将两个连通块中的点都连一条边, 数量为 \(size[a] * size[b] - 1\) ,同时满足所加边权均为 \(v + 1\) (同时满足权值最小和生成树唯一).

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 6010;

struct Edge{
    int a, b, v;
    bool operator < (const Edge &t)const
    {
        return v < t.v;
    }
}e[N];
int fa[N];
int s[N];
int n;

int find(int x)
{
    if (x != fa[x])
        fa[x] = find(fa[x]);
    return fa[x];
}

int main()
{
    int t;
    cin >> t;
    while (t -- ){
        cin >> n;
        for (int i = 1; i < n; i ++ ){
            int a, b, v;
            cin >> a >> b >> v;
            e[i] = {a, b, v};
        }
        
        sort(e + 1, e + n);
        
        int res = 0;
        for (int i = 1; i <= n; i ++ ){
            fa[i] = i;
            s[i] = 1;
        }
        
        for (int i = 1; i < n; i ++ ){
            int a = find(e[i].a);
            int b = find(e[i].b);
            int v = e[i].v;
            
            if (a != b){
                res += (s[a] * s[b] - 1) * (v + 1); 
                s[b] += s[a];
                fa[a] = b;
            }
        }
        cout << res << endl;
    }
    
    return 0;
}

最小生成树

标签:中间   距离   clu   puts   注意   size   生成森林   return   ++   

原文地址:https://www.cnblogs.com/lhqwd/p/14546939.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!