最小生成树及其拓展应用

1.最短网络(模板题)

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

using namespace std;

const int N = 110;

int n;
int w[N][N];
int dist[N];
bool st[N];

int prim()
{
    int res = 0;
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 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;

        res += dist[t];
        st[t] = true;

        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], w[t][j]);
    }

    return res;
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            cin >> w[i][j];

    cout << prim() << endl;

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/148031/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2.局域网(模板题)

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

using namespace std;

const int N = 110, M = 210;

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

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

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        cin >> a >> b >> w;
        e[i] = {a, b, w};
    }

    sort(e, e + m);

    int res = 0;
    for (int i = 0; i < m; i ++ )
    {
        int a = find(e[i].a), b = find(e[i].b), w = e[i].w;
        if (a != b) p[a] = b;
        else res += w;
    }

    cout << res << endl;

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/148059/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

3.繁忙的都市(模板题)

 Kruskal优点之一:最小图的最大边权在所有可成图的最大边权中一定是最小的

Kruskal 能够很好的应用此题,由于它的贪心性质,从小到大枚举所有边,所以一旦枚举完 m条边,最后加进去的那条边一定就是我们要求的边。

由此可见,由 Kruskal 算法求出的最小生成树,不但总权值和最小,而且最大边的边权一定是所有的生成树最大边中最小的。

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

using namespace std;

const int N = 310, M = 10010;

int n, m;

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

int p[N];

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

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        e[i] = {a, b, w};
    }

    sort(e, e + m);

    int res = 0;
    for (int i = 0; i < m; i ++ )
    {
        int a = find(e[i].a), b = find(e[i].b), w = e[i].w;
        if (a != b)
        {
            p[a] = b;
            res = w;
        }
    }

    cout << n - 1 << ' ' << res << endl;
    return 0;
}

作者:walkerㅤ
链接:https://www.acwing.com/solution/content/66046/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

二分法也可以(不建议)

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

using namespace std;

const int N = 310, M = 10010;

int n, m;
int p[N], siz[N];

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

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

bool check(int x)
{
    // 定义二分性质,这个区间中的每个点是一个边,判断这个边之前的所有边能否将所有点全部连通
    // 左边区间的边已经全部能够连通,找到左边区间的边界
    for (int i = 1; i <= n; i ++ ) 
    {
        p[i] = i;
        siz[i] = 1;
    }
    // 连边
    for (int i = 0; i <= x; i ++ )
    {   
        int a = find(e[i].a), b = find(e[i].b), w = e[i].w;
        if (a != b) p[a] = b, siz[b] += siz[a];
    }
    if (siz[find(p[1])] == n) return true;
    return false;
}

int main()
{
    cin >> n >> m;

    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        cin >> a >> b >> w;
        e[i] = {a, b, w};
    }

    sort(e, e + m);

    int l = 0, r = m - 1;

    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }

    cout << n - 1 << ' ' << e[l].w << endl;
    return 0;
}

作者:walkerㅤ
链接:https://www.acwing.com/solution/content/66046/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

4.联络员(模板题)

kruskal优点之一:可以在几个题中已固定连通块中选择建立最小边相连求出最小树

 
我们可以利用Kruskal算法,思考一下,既然第1类边是必须选择的,那我们在读入的时候就直接把所有第1类边的权值加到我们的总边权中,并将它们统计到判断图是否连通的并查集里(统计时不需要判断),然后照常做Kruskal就行。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 10010;
int n, m, p[N], res;

struct Edge 
{
    int u, v, w;
    bool operator <(const Edge& W) const {
        return w < W.w;
    }
} edge[N];

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

int Krus() 
{
    sort(edge + 1, edge + 1 + m);

    for (int i = 1; i <= m; i ++ ) 
    {
        int fu = find(edge[i].u), fv = find(edge[i].v);
        if (fu == fv) continue ;

        p[fu] = fv, res += edge[i].w;
    }

    return res;
}

int main() 
{
    scanf("%d%d", &n, &m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;

    for (int i = 1; i <= m; i ++ ) 
    {
        int P, u, v, w;
        scanf("%d%d%d%d", &P, &u, &v, &w);
        if (P == 1) res += w, p[find(u)] = find(v);//只加不判断
        edge[i] = {u, v, w};
    }

    printf("%d\n", Krus());

    return 0;
}

作者:StkOvflow
链接:https://www.acwing.com/solution/content/156920/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

5.连接格点(模板题)

矩阵求最小树

//注意代码上s是纵坐标,y是横坐标
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010, M = N * N, K = 2 * N * N;

int n, m, k;
int ids[N][N];
struct Edge
{
    int a, b, w;
}e[K];
int p[M];

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

void get_edges()
{
    int dx[2] = {1,0}, dy[2] = {0,1}, dw[2] = {1,2};
    for (int z = 0; z < 2; z ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
                for (int u = 0; u < 2; u ++ )
                    if (u % 2 == z)//先存最小边1,再存最大边2
                    {
                        int x = i + dx[u], y = j + dy[u], w = dw[u];
                        if (x && x <= n && y && y <= m)
                        {
                            int a = ids[i][j], b = ids[x][y];
                            e[k ++ ] = {a, b, w};
                        }
                    }
}

int main()
{
    cin >> n >> m;

    for (int i = 1, t = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++, t ++ )
            ids[i][j] = t;

    for (int i = 1; i <= n * m; i ++ ) p[i] = i;

    int x1, y1, x2, y2;
    while (cin >> x1 >> y1 >> x2 >> y2)
    {
        int a = ids[x1][y1], b = ids[x2][y2];
        p[find(a)] = find(b);
    }

    get_edges();

    int res = 0;
    for (int i = 0; i < k; i ++ )
    {
        int a = find(e[i].a), b = find(e[i].b), w = e[i].w;
        if (a != b)
        {
            p[a] = b;
            res += w;
        }
    }

    cout << res << endl;

    return 0;
}
// 作者:yxc
// 链接:https://www.acwing.com/activity/content/code/content/148150/
// 来源:AcWing
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

6.新的一天(建立虚拟源点)

这道题可以想象成有一个总电站,每个矿井连接此点的边权为在该点建立发电站的边权,因此就是求n+1个点的最小生成树。(注意是稠密图) 

#include<iostream>
#include<cstring>
using namespace std;
const int N=305;
int n,way[N][N],start,ans=0x3f3f3f3f,dist[N];
bool st[N];
void prim(){
    memset(dist,0x3f,sizeof dist);
    dist[start]=0;
    for(int i=0;i<n;i++){   //向集合里加n个点
        int now=-1;
        for(int j=1;j<=n;j++)
            if(!st[j]&&(now==-1||dist[now]>dist[j]))
                now=j;  //找距离集合最近的点
        st[now]=1;  //加入集合
        ans+=dist[now]; //更新答案
        for(int j=1;j<=n;j++)
            dist[j]=min(dist[j],way[now][j]);   //更新其它点到集合的距离
    }
}
int main(){
    scanf("%d",&n);
    memset(way,0x3f,sizeof way);
    for(int i=1;i<=n;i++){  //处理发电站
        int v;
        scanf("%d",&v);
        for(int j=1;j<=n;j++)
            way[j][i]=min(way[j][i],v);//单向边
        if(ans>v){  //更新起点
            start=i;
            ans=v;
        }
    }
    for(int i=1;i<=n;i++)   //处理电路
        for(int j=1;j<=n;j++){
            int w;
            scanf("%d",&w);
            way[i][j]=min(way[i][j],w);//双向边 
        }
    prim();
    printf("%d",ans);
    return 0;
}

作者:xiuzhiyuan
链接:https://www.acwing.com/solution/content/108462/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

7.通讯网络(求当连通块个数为x时的所成树的最大边权)和繁忙的都市类似,只不过连通块的取值不一样。

kruksal的优点之一

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

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 510, M = N * N / 2(N*N表示n个点互相连接,/2表示生成树时是单向边);

int n, k, m;
struct Edge
{
    int a, b;
    double w;
    bool operator< (const Edge &t) const
    {
        return w < t.w;
    }
}e[M];
PII q[M];//存储点的坐标
int p[N];

double get_dist(PII a, PII b)//求边权
{
    int dx = a.x - b.x;
    int dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
}

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

int main()
{
    cin >> n >> k;
    for (int i = 0; i < n; i ++ ) cin >> q[i].x >> q[i].y;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < i; j ++ )
            e[m ++ ] = {i, j, get_dist(q[i], q[j])};

    sort(e, e + m);
    for (int i = 0; i < n; i ++ ) p[i] = i;

    int cnt = n;
    double res = 0;
    for (int i = 0; i < m; i ++ )
    {
        if (cnt <= k) break;

        int a = find(e[i].a), b = find(e[i].b);
        double w = e[i].w;
        if (a != b)
        {
            p[a] = b;
            cnt -- ;
            res = w;
        }
    }

    printf("%.2lf\n", res);

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/151271/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

                                

8.走廊泼水节(由最小生成树建边成完全图)

 题目说明:题中给你一个最小树,需要你加边使其变成完全图,并且所加的边一定大于原图的边权使得所加边不会使题目所给树的边权是最小的

思路:

 在建边的时候,我们假设已经连成了一小个完全图,那么再遍历下一个不在完全图内的点时,我们只需要+(完全图内点的个数-1)*(题中给的此点与图内的一个点的边权+1)即可将此图连成完全图。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 6010;

struct Edge{
    int u , v , w;
    bool operator < (const Edge &W) const{
        return w < W.w;
    }
}edge[N];

int n;
int f[N];
int cnt[N];

int find(int x)
{
    return f[x] = (f[x] == x ? x : find(f[x]));
}

int main()
{
    int T;
    cin >> T;
    while(T--)
    {
        cin >> n;

        for(int i = 1 ; i <= n ; i++)   f[i] = i , cnt[i] = 1;//此时cnt表示一个点值有它自己相连,因此cnt都为1

        for(int i = 0 ; i < n - 1; i++)
        {
            int u , v , w;
            cin >> u >> v >> w;
            edge[i] = {u , v , w};
        }

        sort(edge , edge + n - 1);//从最小的边权开始遍历,因为边权越小,距离根的距离越近

        int res = 0;
        for(int i = 0 ; i < n - 1 ; i ++)
        {
            auto e = edge[i];
            int u = find(e.u) , v = find(e.v) , w = e.w;
            if(u != v)
            {
                res += (cnt[u] * cnt[v] - 1) * (w + 1);
                f[u] = v;//更新根节点
                cnt[v] += cnt[u];//更新此时v所连的点数
            }
        }

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

作者:Anoxia_3
链接:https://www.acwing.com/solution/content/15727/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

9.牛奶运输(求次短树)

 

 思考:树中任意两点所连成的边中,去掉一个长度中等的边(不是最大或次大),加上一个比这个边稍微大一点的边行不行?肯定是不行的,因为在生成最小树的时候,如果还有一个中等的且比最大或次大小的边时,我们肯定会把它加上,从而减少更大边权的加入,形成最小树,因此我们只需要比较次大或最大边即可。从而不考虑小边的情况。

将最小生成树称为为MST
次小生成树在最小生成树的邻集中。(最小生成树变一个边)

求最小生成树,统计标记每条边是否是树边;同时把最小生成树建立,权值之和为sum

预处理生成树中任意两点间的边权最大值dist1[a][b]和长度次大dist2[a][b] (树中两点路径唯一,dfs)

依次枚举所有非MST边t,边t连接a,b,权为w。显然a,b在MST中。
    尝试用t替换a-b的路径中最大的一条边A。t的权w >= A。(如果w < A,直接换边就能得到更小的生成树,矛盾了)
    如果w > A,替换后总权值是sum + w - dist1[a][b] 
    否则 w = A ,不能替换,会得到非严格次小生成树(权值和MST相等)
    w = A,w > 次大值B 替换后总权值是sum + w - dist2[a][b] 

    //以下是求非严格cixiao
 */
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 510, M = 10010;
typedef long long LL;

struct Edge{
    int a, b, w;
    bool f = false;
    bool operator < (const Edge & A) const{
        return w < A.w;
    }
}edge[M];

int h[N], e[N * 2], ne[N * 2], w[N * 2], idx; // 无向树 2 * N
int cnt;
int n, m;
int dist1[N][N], dist2[N][N];// 最小和次小
int p[N];
void add(int a,int b,int c){
    e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx ++;
}
int find(int x){
    return x == p[x] ? x : p[x] = find(p[x]);
}

//dfs无向图技巧:记录父节点防止回走
void dfs(int u, int fa, int maxd1, int maxd2, int d1[], int d2[]){
    d1[u] = maxd1, d2[u] = maxd2;
    for (int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if (j != fa){ //不往回搜
            int td1 = maxd1,  td2 = maxd2;
            if (w[i] > td1) td2 = td1, td1 = w[i];
            else if (w[i] < td1 && w[i] > td2) td2 = w[i];
            dfs(j, u, td1, td2, d1, d2);
        }
    }
}


int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    ;
    for(int i = 0; i < m; ++ i){
        int a, b, w;
        cin >> a >> b >> w;
        edge[i] = {a, b, w}; 
    }

    sort(edge, edge + m);
    for(int i = 1; i <= n; ++ i)p[i] = i;
    LL sum = 0;

    for(int i = 0; i < m; ++ i){
        int a = edge[i].a, b = edge[i].b, w = edge[i].w;
        int pa = find(a), pb = find(b); 
        if (pa != pb)
        {
            p[pa] = pb;
            sum += w;
            add(a, b, w), add(b, a, w);//! 合并集合,但加边是节点之间加边
            edge[i].f = true;
        }
    }

    for(int i = 1; i <= n; ++ i)dfs(i, -1, -1e9,-1e9, dist1[i],dist2[i]);// 生成树内搜索最长路

    LL res = 1e18;

    for(int i = 0; i < m; ++ i){ 
        int a = edge[i].a, b = edge[i].b, w = edge[i].w;

        if(!edge[i].f){// 遍历每条外部边尝试替换
            LL t = 1e18;
            if(w > dist1[a][b]){
                t = sum + w - dist1[a][b];
            }else if( w > dist2[a][b]){ // w不是大于就是等于dis1
                t = sum + w - dist2[a][b];
            }
            res = min(res, t);
        }
    }
    cout << res << endl;


    return 0;
}

作者:夜林
链接:https://www.acwing.com/solution/content/93261/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 18
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值