最小生成树

本文详细介绍了Prim算法(适用于边多场景)和Kruskal算法(适用于点多场景),并通过实例演示了如何在城市通电、联络员和北极通讯网络等实际问题中运用。Prim通过迭代扩展连通块,而Kruskal则是合并最小边来构建树。文章还涉及了Prim的执行次数和负环处理技巧,以及Kruskal的连通块合并策略。
摘要由CSDN通过智能技术生成
最小生成树(无向图)
1.Prim          o(n^2)用于边多
2.kruskal       o(mlogm)用于点多

Prim

1140. 最短网络 - AcWing题库

每次找到离连通块最近的点t,如果不是第一个点且距离dis为INF,则不连通

如果不是第一个点,则将距离dis加入答案,然后用这个点t到别的点的距离去更新别的点的dis

注:Prim要执行n次,区别于Dijkstra执行 n-1次

int g[N][N];
int dis[N];
bool vis[N];
void init(){
	memset(dis,0x3f,sizeof(dis));
	memset(g,0x3f,sizeof(g));
} 
int prim(){
    dis[1]=0;
	int res=0;
	for(int i=0;i<n;i++){
		int t=-1;
		for(int j=1;j<=n;j++){
			if(!vis[j]&&(t==-1||dis[t]>dis[j]))
				t=j;
		}
    	vis[t]=true;
		if(i&&dis[t]==INF) return INF;//不是第一个点且与当前树不连通
		if(i) res+=dis[t];//不是第一个点  把这条边连入生成树中 
		//更新放在最后,避免负环的影响 
		for(int j=1;j<=n;j++) 
            dis[j]=min(dis[j],g[t][j]);//注意是到连通块的最小距离 
	}
	return res;
}

Kruskal

1141. 局域网 - AcWing题库

struct node{
	int u,v,w;
	bool operator<(const node& b)const{
		return w<b.w;
	}
}edge[N];
int kruskal(){
	sort(edge,edge+m);
	int res=0,cnt=0;
	for(int i=0;i<m;i++)
	{
		int a=edge[i].a,b=edge[i].b,w=edge[i].w;
		a=find(a),b=find(b);
		if(a!=b)//a和b不在同一个连通块中,则合并 
		{
			p[a]=b;
			res+=w;
			cnt++;
		}
	}
	if(cnt<n-1) return INF;
	return res;
}

 城市通电

 3728. 城市通电 - AcWing题库

        题意:若干个村庄,每个村庄可以通过建立发电站或从其他有电的村庄连线,每个村庄建立发电站以及连线的花费不同,求所有村庄都有电的最小花费、建立发电站的村庄、建立的电线

 fa[i]表示有电且可以连向i的村庄中,花费最小的村庄 

建立超级源点,建发电站相当于向源点连边

在Prim中,①如果点t是第一次被选,说明没有合适的点可以给该点连边,则在该点建电站;

②如果不是第一次被选,说明该点存在其他已经有电的点可以相连,fa[t]即为可以连的点中代价最小的 

用该点去更新其他点,如果更新了点j,则点t即为可以连到点j,且花费最小的点 

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=2e3+5;
int n;
PII node[N];
ll w[N];//建电站花费 
ll k[N];//建边花费 
vector<int> res1;
vector<PII> res2;
int fa[N];//fa[i] 可以连向点i的点中花费最小的 
ll dis[N];//i通电的最小花费 
bool vis[N];
int cal(int i,int j){
	int x=abs(node[i].first-node[j].first);
	int y=abs(node[i].second-node[j].second);
	return x+y;
}
ll prim(){
	ll res=0;
	memset(dis,0x3f,sizeof dis);
	//相当于建立一个超级源点
	//建发电站等于向源点连边 
	for(int i=1;i<=n;i++){
		dis[i]=w[i];
	}
	for(int i=0;i<n;i++){
		int t=-1;
		for(int j=1;j<=n;j++){
			if(!vis[j]&&(t==-1||dis[t]>dis[j])){
				t=j;
			}
		}
		res+=dis[t];
		vis[t]=true;
		if(!fa[t]){//如果该点是第一次被选,说明没有合适的点可以给该点连边,则在该点建电站 
			res1.push_back(t);
		}else{//不是第一次被选,说明该点存在其他已经有电的点可以相连,fa[t]即为可以连的点中代价最小的 
			res2.push_back({fa[t],t});
		}
		for(int j=1;j<=n;j++){//用该点去更新其他点,如果更新了点j,则点t即为可以连到点j,且花费最小的点 
			ll d=cal(t,j)*(k[t]+k[j]);
			if(dis[j]>d){
				dis[j]=d;
				fa[j]=t;
			}
		}
	}
	return res;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int x,y;cin>>x>>y;
		node[i]={x,y}; 
	}
	for(int i=1;i<=n;i++){
		cin>>w[i];
	}
	for(int i=1;i<=n;i++){
		cin>>k[i];
	}
	cout<<prim()<<endl;
	cout<<res1.size()<<endl;
	for(auto t:res1){
		cout<<t<<" ";
	}
	cout<<"\n";
	cout<<res2.size()<<endl;
	for(auto t:res2){
		cout<<t.first<<" "<<t.second<<"\n";
	}
}

联络员

1143. 联络员 - AcWing题库

题意:给出的边中有若干条必选的,求最小生成树大小

思路:对于必选的,先加到生成树中,然后将可选的排序后继续最小生成树

	for(int i=1;i<=k;i++)
	{
		int q,a,b,d;
		cin>>q>>a>>b>>d;
		//可选的 
		if(q==2){
			ed[++j]={a,b,d};
		}//必选的 
		else{
			a=find(a),b=find(b);
			p[a]=b;
			res+=d;
		}
	} 
	sort(ed+1,ed+j+1,cmp);
	cout<<kruskal();

新的开始

1146. 新的开始 - AcWing题库

题意:n个点中,在若干个点建立供电站,连接所有点使得都通电,建立供电站和连接各点都需要一定的花费,求最小花费

思路:建立一个虚拟源点0,和每个点建立一条边,边权为在该点建立供电站的花费,然后求0~n的最小生成树

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

北极通讯网络

1145. 北极通讯网络 - AcWing题库

题意:n个村庄,每个村庄都能联系周围d米以内的村庄,最多给k个村庄建信号站,拥有信号站的村庄可以联系任何村庄,求所有村庄能建立通信网络的情况下,d的最小值

思路:将建立的生成树看作若干个连通块,最初为n个连通块,两两计算村庄之间距离,排序后进行kruskal,当连通块剩下k个时,可以在每个连通块内建立一个信号站,此时枚举到的边(连通块之间的最长边)即为d的最小值

注意:① 当信号站的数量大于等于村庄的数量,就在每个村庄建立信号站,d可以为0

           ② 当信号站的数量为0,此时d就等于生成树的最大边

double kruskal(){
	double maxn=0;
	int cnt=n;//n个连通块,到剩余k个连通块时结束
	if(n<=k) return 0;
	for(int i=1;i<=ed_sum;i++){
		int a=ed[i].a,b=ed[i].b;
		double d=ed[i].d;
		a=find(a),b=find(b);
		if(a!=b){
			cnt--;
			p[a]=b;
			maxn=max(maxn,d);
			if(cnt==k) return d;
		}
	}
	return maxn;
}

走廊泼水节

346. 走廊泼水节 - AcWing题库

题意:在n个点的图中建立最小生成树,再将图加边变为完全图,且保持最小生成树不变,求所加边的长度之和最小

思路:

          初始时先将每个点看成一个大小为1的连通块,在每循环到一条可以合并两个连通块的边e时,记e的边长为w,为了形成一个完全图,就要使得两个已经是完全图的连通块中的点有边,但是为了使最后的唯一最小生成树还是原来那棵而且,新增的边一定要大于w,所以最小为w+1

           在并查集中维护连通块中点的个数,如果连接点u和点v,则需要额外添加的边的数量为cnt[u]*cnt[v]-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;
        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];
            }
        }
        cout << res << endl;
    }
    return 0;
}

秘密的牛奶运输(次小生成树)

题意:求次小生成树的大小

思路:

        ① 求最小生成树,统计标记每条边是树边还是非树边,同时把最小生成树建出来
        ② 预处理任意两点间路径上的边权最大值和次大值
        ③ 依次枚举所有非树边尝试替换,求min(sum + w - dist[a][b]),满足w>dist[a][b]

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 510, M = 10010;
int n, m;
struct Edge
{
    int a, b, w;
    bool f;
    bool operator< (const Edge &t) const
    {
        return w < t.w;
    }
}edge[M];
int p[N];
int dist1[N][N], dist2[N][N];
int h[N], e[N * 2], w[N * 2], ne[N * 2], idx;
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)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
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()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        scanf("%d%d%d", &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 ++ )
        if (!edge[i].f)
        {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            LL t;
            if (w > dist1[a][b])
                t = sum + w - dist1[a][b];
            else
                t = sum + w - dist2[a][b];
            res = min(res, t);
        }
    printf("%lld\n", res);
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vic.GoodLuck

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值