最小生成树的题型汇通

咳咳...

我是看了b站的up主 讲编程的王西西 而总结的他的视频。当然也有我自己的题目的思考。

链接http://【【王西西coding】最小生成树原理与题目讲解】https://www.bilibili.com/video/BV1iS4y147uN?vd_source=d004092b8a889ec3e4c48d349193d64c

首先,先贴一下最小生成树的模板。

题目形如:给定n个点,现在有m个边,每个边耗费一定体力值,请问从起点走到终点最少耗费多少体力值?

prime:稠密图

Kruskal 算法:(通用性更强 )代码很好写,涉及到并查集:过程:

              初始状态一条边都不连,每个点属于自己的集合,按照边的权值从小到大排个序,然后按小到大遍历边,看边的起点和终点在不在同一个集合。如果不在就把他们连起来。(写模板的时候别忘了加个cnt)(代码省略部分过程,只是自己写的模板,仅供回忆思路)

struct EDGE{
  	int u,v,l;
  }edge[M];
  
  bool sorrt(struct EDGE i,struct EDGE j){
  	return i.l<j.l;
  }
  
  void find(int i){
  	if(fa[i]==i){
  		return i;
	  }
	fa[i]=find(fa[i]);
	return fa[i];
  }
  
  void unionn(int i,int j){
  	int fa_i=find(i);
  	int fa_j=find(j);
  	fa_i=fa[fa_j];
  }
 
  int main()
  {
  	  int ans=0,cnt++;
  	  cin>>n>>m;
	  for(int i=1;i<=m;i++){
	  	cin>>edge[i].u>>edge[i].v>>edge[i].l;
	  }
	  sort(edge+1,edge+m+1,sorrt);
  	  for(int i=1;i<=n;i++){
  		 fa[i]=i; 
  	  }
  	  for(int i=1;i<=m;i++){
  		 int fa_u=edge[i].u,fa_v=edge[i].v;
  		 if(fa_u!=fa_v){
  		  	fa[fa_u]=fa[v];
  		  	ans+=edge[i].l;
			cnt++;//若cnt==n-1,break即可;否则不是最小生成树;
	 	 }
  	  }
	  cout<<ans;
	  return 0;
  }

最小生成树用到的知识点:并查集图的存储排序

现在看看最小生成树的子题:

1.题目如下

给你云朵的个数 N,再给你 M 个关系,表示哪些云朵可以连在一起。现在小杉要把所有云朵连成 K 个棉花糖,一个棉花糖最少要用掉一朵云,小杉想知道他怎么连,花费的代价最小。

输入格式
第一行有三个数 N,M,K。接下来 M 行每行三个数 X,Y,L表示 X 云和 Y 云可以通过 L 的代价连在一起。

输出格式
对每组数据输出一行,仅有一个整数,表示最小的代价。

如果怎么连都连不出 K 个棉花糖,请输出 No Answer。

输入输出样例
输入 #1复制
3 1 2
1 2 1
输出 #1复制
1
说明/提示
对于 30% 的数据,1≤N≤100,1≤M≤10 ^3

对于 100%100% 的数据,1≤N≤10^3
,1≤M≤10 ^4
,1≤K≤10,1≤X,Y≤N,0≤L<10^4

翻译:

本题实质上意思是,如何控制集合的个数,使得最后一共连成k个集合?那么就抓住cnt的意义,本来一共有n个集合,每加入一个点入集合,cnt次后集合还有n-cnt个,则令n-cnt=k即可。

2.题目如下:

题目描述:

又到了一年一度的明明生日了,明明想要买B样东西,巧的是,这B样东西价格都是A元。但是,商店老板说最近有促销活动,也就是:如果你买了第I样东西,再买第J样,那么就可以只花K[I,J]元,更巧的是,K[I,J]竟然等于K[J,I]。现在明明想知道,他最少要花多少钱。

输入输出格式:

输入格式:

第一行两个整数,A,B。接下来B行,每行B个数,第I行第J个为K[I,J]。我们保证K[I,J]=K[J,I]并且K[I,I]=0。特别的,如果K[I,J]=0,那么表示这两样东西之间不会导致优惠。

输出格式:

仅一行一个整数,为最小要花的钱数。

翻译:

首先这道题要学会把题目上的“图”翻译为树。而且两点是相互连通的(题目上说kij=kji)。其实不少题目涉及两点间相互关系的都可以试着往树上引导。不过这题还要再虚构一个0点,Kruskal即可。

3.题目https://www.luogu.org/problemnew/show/P3063

题目背景
奶牛爱干草

题目描述
Bessie 计划调查N (2 <= N <= 2,000)个农场的干草情况,它从1号农场出发。农场之间总共有M (1 <= M <= 10,000)条双向道路,所有道路的总长度不超过1,000,000,000。有些农场之间存在着多条道路,所有的农场之间都是连通的。

Bessie希望计算出该图中最小生成树中的最长边的长度。

翻译:

做法1:二分去做,看所有边长小于mid是否成立,不成立就l=mid+1;

做法2:但其实,这道题目的答案就等价于求最小生成树的最长边。因为每次二分都要扫一下这个图,当集合变为1时break看mid是否成立;还不如就扫一遍这个图,当集合为1时停止,看最长边;

在此贴一个求s与t点间“最长生成树”的最短边的代码:

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e6+7;
int n,m,k,s,t;
struct EDGE{
	int u,v,l;
}edge[N];
int fa[N];
int find(int i){
	if(fa[i]==i){
		return i;
	}
	fa[i]=find(fa[i]);
	return fa[i];
} 
bool check(struct EDGE i,struct EDGE j){
	return i.l>j.l;
}
int main()
{
	cin>>n>>m>>k>>s>>t;
	int z;
	for(int i=1;i<=m;i++){
		cin>>edge[i].u>>edge[i].v>>z;
		if(z==-1){
			edge[i].l=N;
		}else{
			edge[i].l=z;
		}
	}
	sort(edge+1,edge+m+1,check);
	int cnt=0,ans=N;
	for(int i=1;i<=n;i++){
		fa[i]=i;
	}
	for(int i=1;i<=m;i++){
		cnt++;
		int fa_u=find(edge[i].u),fa_v=find(edge[i].v);
		if(fa_u!=fa_v){
			fa[fa_u]=fa_v;
		}
		if(find(s)==find(t)){
			ans=min(k,edge[i].l);
			break;
		}
	}
	cout<<ans;
	return 0;
} 

4.题目描述

农民John 决定将水引入到他的n(1<=n<=300)个牧场。他准备通过挖若

干井,并在各块田中修筑水道来连通各块田地以供水。在第i 号田中挖一口井需要花费W_i(1<=W_i<=100,000)元。连接i 号田与j 号田需要P_ij (1 <= P_ij <= 100,000 , P_ji=P_ij)元。

请求出农民John 需要为连通整个牧场的每一块田地所需要的钱数。

突破点:

虚构另一个点(可以是a[0])来存自生件水#的情况。之前再最小生成树专题里讲解过类似的题目。

在此处再贴一下那个题目的代码吧,可以着重看一下虚构点的操作。

#include<iostream>
#include<algorithm>
using namespace std;
int n,ans,id;
struct NODE{
	int x,y,c;
}node[100010];
 
struct EDGE{
	int u,v,l;
}edge[1000100];
 
int c[1000010],k[1000010],w;
int fa[100010];
//并查集的算法 
int find(int i){
	if(fa[i]==i){
		return fa[i];
	}
	fa[i]=find(fa[i]);
	return fa[i];
}
void unionn(int i,int j){
	int fa_i=find(i);
	int fa_j=find(j);
	fa[fa_i]=fa_j;
}
//排序 (从小到大) 
bool check(struct EDGE i,struct EDGE j){
	return i.l<j.l;
}
int main()
{
	cin>>id;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>node[i].x>>node[i].y;
	}
	for(int i=1;i<=n;i++){
		cin>>c[i];
		edge[w++]=EDGE{0,i,c[i]};
	}
	for(int i=1;i<=n;i++){
		cin>>k[i];
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<i;j++){
			int u=1ll*(k[i]+k[j])*(abs(node[i].x-node[j].x)+abs(node[i].y-node[j].y));
			edge[w++]=EDGE{i,j,u};
		}
	}
	sort(edge,edge+w,check);
	for(int i=0;i<w;i++){
		cout<<edge[i].l<<endl;
	}
	for(int i=1;i<=n;i++){
		fa[i]=i;
	}
	for(int i=0;i<w;i++){
		int fa_u=find(edge[i].u),fa_v=find(edge[i].v);
		if(fa_u!=fa_v){
			unionn(edge[i].u,edge[i].v);
			ans+=edge[i].l;
		}
	}
	cout<<ans;
	return 0;
} 

5.题目描述:

先贴个图吧,(累)下次写

https://blog.youkuaiyun.com/2301_81888203/article/details/141110355?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22141110355%22%2C%22source%22%3A%222301_81888203%22%7D

。。。

注:原视频链接:http://【【王西西coding】最小生成树原理与题目讲解】https://www.bilibili.com/video/BV1iS4y147uN?vd_source=d004092b8a889ec3e4c48d349193d64c

此贴供我复习treee使用...

6.         8.11日,今晚,很亮的月光...

            我又回来了!把我今天整的题目贴在这里哦

      链接:https://blog.youkuaiyun.com/2301_81888203/article/details/141110355

之前写的最小生成树很多都是给了3000个边(左右),但如果只给了点,且点的数量较多,就不能把所有边都列出来了,这样会超时。

先来看一道题目叭


 最小代价
时间限制: 1.000 Sec  内存限制: 128 MB
提交状态

题目描述
给定 N 个二维平面上的点(xi,yi),将两个点(a,b),(c,d)  连接起来的代价是min(|a-c|,|b-d|) 。求最少需要多少的代价可以使整幅图联通,即对于任意的两个点之间可以直接或间接地到达。

输入
第一行包括一个整数N。表示点的个数。
接下来的N行,每行两个整数(xi,yi),表示第i个点的坐标。

输出
输出一个整数,表示需要的最少代价。

样例输入 Copy
3
1 5
3 9
7 8
样例输出 Copy
3



这道题有10^5个点,如果每个边都算,就有将近10^9条边,毫无疑问不行。那么我们就要注意到,有些点之间是不可能有边的(当两点距离大),那么只要把点与其最附近的点连起边来就好。那么怎么找其附近的点呢?注意这条题目的距离计算方式,横纵坐标差决定距离,那么自然就是把横纵坐标排一下顺序,然后“一个挨着一个”的记录入边。

下面就看一下代码就理解啦:

#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
int n;
struct NODE{
    ll s;
	int id;
}nodex[100010],nodey[100010];
struct EDGE{
	int u,v;
	ll l;
}edge[100010];
bool sorrt(struct NODE i,struct NODE j){
	return i.s<j.s;
}
int fa[100010];
void init(int n){
	for(int i=1;i<=n;i++){
		fa[i]=i;
	}
}
int find(int i){
	if(fa[i]==i){
		return i;
	}
	fa[i]=find(fa[i]);
	return fa[i];
}
bool sorrt1(struct EDGE i,struct EDGE j){
	return i.l<j.l;
}
int t=0;
int main()
{
	cin>>n;
	init(n);
	for(int i=1;i<=n;i++){
		cin>>nodex[i].s>>nodey[i].s;
		nodex[i].id=nodey[i].id=i;
	}
	sort(nodex+1,nodex+n+1,sorrt);
	sort(nodey+1,nodey+n+1,sorrt);
	for(int i=2;i<=n;i++){
		edge[++t].u=nodex[i-1].id;
		edge[t].v=nodex[i].id;
		edge[t].l=nodex[i].s-nodex[i-1].s;
	}
	for(int i=2;i<=n;i++){
		edge[++t].u=nodey[i-1].id;
		edge[t].v=nodey[i].id;
		edge[t].l=nodey[i].s-nodey[i-1].s;
	}
	sort(edge+1,edge+1+t,sorrt1);
	ll cnt=0,ans=0;
	for(int i=1;i<=t;i++){
		int fa_u=find(edge[i].u),fa_v=find(edge[i].v);
		if(fa_u!=fa_v){
			fa[fa_u]=fa_v;
			cnt++;
			ans+=edge[i].l;
		}
		if(cnt==n-1){
			break;
		}
	}
	cout<<ans;
	return 0;
}

其实这题就是将横纵坐标的数组排序,就可以筛选边了。

24.9.12 晚 更新一题


走廊泼水节

题目描述

【简化版题意】给定一棵N个节点的树,要求增加若干条边,把这棵树扩充为完全图,并满足图的唯一最小生成树仍然是这棵树。求增加的边的权值总和最小是多少。
我们一共有N个OIER打算参加这个泼水节,同时很凑巧的是正好有N个水龙头(至于为什么,我不解释)。N个水龙头之间正好有N-1条小道,并且每个水龙头都可以经过小道到达其他水龙头(这是一棵树,你应该懂的..)。但是OIER门为了迎接中中的挑战,决定修建一些个道路(至于怎么修,秘密~),使得每个水龙头到每个水龙头之间都有一条直接的道路连接(也就是构成一个完全图呗~)。但是OIER门很懒得,并且记性也不好,他们只会去走那N-1条小道,并且希望所有水龙头之间修建的道路,都要大于两个水龙头之前连接的所有小道(小道当然要是最短的了)。所以神COW们,帮那些OIER们计算一下吧,修建的那些道路总长度最短是多少,毕竟修建道路是要破费的~~
 

输入

多组数据~
第一行t,表示有t组测试数据
对于每组数据
第一行N,表示水龙头的个数(当然也是OIER的个数);
2到N行,每行三个整数X,Y,Z;表示水龙头X和水龙头Y有一条长度为Z的小道

输出

对于每组数据,输出一个整数,表示修建的所有道路总长度的最短值。

样例输入 Copy
2
3
1 2 2
1 3 3
4
1 2 3
2 3 4
3 4 5
样例输出 Copy
4
17
提示

第一组数据,在2和3之间修建一条长度为4的道路,是这棵树变成一个完全图,且原来的树依然是这个图的唯一最小生成树.
每个测试点最多10组测试数据
50%n<=1500;
100%n<=6000
100%z<=100


这题其实也是最小生成树与最短路结合的变形题。如何保证修建完所有道路后,最小生成树仍然保持不变呢?我们可以模拟求最小生成树的过程:将所有边排序后,按序连接两个未连接的子集。注意:这时两个子集里的点是完全没有两个点相互连接的。所以要生成完全图,我们只需按这两个子区间连接所要的最短边连接所有边即可。假设子集u的元素个数为n1,子集v的元素个数为n2,则将这两个子集里面的点全部连接的边个数为n1*n2。

下面看一下代码吧。。。感觉我语言表述好差aaa -_-

#include<iostream>
#include<algorithm>
#define int long long
using namespace std;
 
int t;
int n;
struct EDGE{
    int u,v,l;
}edge[100010];
 
bool sorrt(struct EDGE i,struct EDGE j){
    return i.l<j.l;
}
 
int fa[1000010],sum[1000010];
void init(int n){
    for(int i=1;i<=n;i++){
        sum[i]=1;
        fa[i]=i;
    }
}
 
int find(int i){
    if(fa[i]==i){
        return i;
    }
    fa[i]=find(fa[i]);
    return fa[i];
}
 
int solve(int n){
    for(int i=1;i<=n-1;i++){
        cin>>edge[i].u>>edge[i].v>>edge[i].l;
    }
    sort(edge+1,edge+n,sorrt);
    init(n);
    int ans=0;
    for(int i=1;i<=n-1;i++){
        int fa_u=find(edge[i].u),fa_v=find(edge[i].v);
        if(fa_u!=fa_v){
            ans+=(sum[fa_u]*sum[fa_v]-1)*(edge[i].l+1);
            sum[fa_u]+=sum[fa_v];
            fa[fa_v]=fa_u;
        }
    }
    return ans;
}
 
signed main()
{
    cin>>t;
    while(t--){
        cin>>n;
        cout<<solve(n)<<endl;
    }
    return 0;
}

          

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值