「Day 8—最小生成树之Kruskal & Prim」
最小生成树
定义
什么是最小生成树呢?
好问题,通俗来说就是给你一个图( \(n\) 个节点),让你选 \(n-1\) 条边使其保持连通,但是又要保证所选的边的和最小。
P3366 【模板】最小生成树
思路(Kruskal)
我们应该怎么实现呢,不妨将要选的边排个序,从小到大选,诶,好想法,这也是一种贪心的思路,那我们该如何保证选这个边有意义呢?万一选了个没必要加的边呢?且必须保证不能有环。
想一想,如果我们什么边都没加入,开始的(\(n\) 个节点就相当于 \(n\) 棵树,每次加边就是对于两棵树的一次合并)。那么怎么能完成这个操作呢?诶,前几天学的并查集不就行吗?
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 800005;
const int MAXM = 4 * 1e7;
int n,m;
struct node{
int u,v,w;
}e[MAXN * 2];
int tot = 0,f[MAXN];
inline void add(int x,int y,int len){
e[++ tot] = {x,y,len};
}
inline int find(int x){
return (x == f[x]) ? x : f[x] = find(f[x]);
}
bool cmp(const node &x,const node &y){
return x.w < y.w;
}
inline int Krusal(){
int cnt = 0;
for(int i = 1;i <= n;i ++){
f[i] = i;
}
sort(e + 1,e + tot + 1,cmp);
int ans = 0;
for(int i = 1;i <= tot;i ++){
int x = find(e[i].u);
int y = find(e[i].v);
if(x != y){
ans += e[i].w;
f[x] = y;
cnt ++;
}
}
if(cnt != n - 1) return -1;
return ans;
}
int main(){
cin >> n >> m;
for(int i = 1;i <= m;i ++){
int x,y,z;
cin >> x >> y >> z;
add(x,y,z);
}
int sum = Krusal();
if(sum == -1){
cout << "orz\n";
}
else cout << sum << "\n";
return 0;
}
思路(Prim)
\(Prim\) 算法与 \(Dijkstra\) 算法类似,我们维护一个当前选择的点的集合,表示我们当前的生成树中有这些点。
接下来,每次找到距离当前点集最近的一个顶点,将这个顶点加入到当前点集中,并且将这个顶点与点集相连的边加入到生成树中。
代码
//不带优化版的Prim
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
const int maxn = 1001;
const int maxm = 10101;
const int inf = 0x3f3f3f3f;
struct Edge {
int to, next, len;
} edge[maxm * 2];
int h[maxn], tot = -1;
int n, m, s;
int d[maxn];
bool vis[maxn];
void addEdge(int x, int y, int len) {
edge[++tot] = {y, h[x], len};
h[x] = tot;
}
int prim() {
memset(d, 0x3f, sizeof(d));
d[s] = 0;
int sum = 0;
for (int i = 1; i <= n; i++) {
int mind = inf;
int v = 0;
for (int j = 1; j <= n; j++) {
if (!vis[j] && d[j] < mind) {
mind = d[j];
v = j;
}
}
if (mind == inf) {
break;
}
vis[v] = true;
sum += d[v];
for (int j = h[v]; j != -1; j = edge[j].next) {
int to = edge[j].to;
d[to] = min(d[to], edge[j].len);
}
}
return sum;
}
int main() {
cin >> n >> m >> s;
memset(h, -1, sizeof(h));
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
addEdge(u, v, w);
addEdge(v, u, w);
}
cout << prim() << endl;
return 0;
}
习题
P1547 [USACO05MAR] Out of Hay S
思路
板子,就没啥说的,代码就不放了。
P1546 [USACO3.1] 最短网络 Agri-Net
思路
这个题吧,也是板子。。。输入的时候注意一下就好。
P1195 口袋的天空
思路
首先这个题的本质是求连同 \(K\) 个块的所用最小代价。
只需要改 \(cnt\) 的判断即可,原来是 \(1\) 个连通块,现在是 \(K\) 个,即 \(cnt = n - k\),然后跑一遍即可。
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 800005;
const int MAXM = 4 * 1e7;
int n,m,k;
struct node{
int u,v,w;
}e[MAXN * 2];
int tot = 0,f[MAXN];
inline void add(int x,int y,int len){
e[++ tot] = {x,y,len};
}
inline int find(int x){
return (x == f[x]) ? x : f[x] = find(f[x]);
}
bool cmp(const node &x,const node &y){
return x.w < y.w;
}
inline int Krusal(){
int cnt = n - k;
for(int i = 1;i <= n;i ++){
f[i] = i;
}
sort(e + 1,e + tot + 1,cmp);
int ans = 0;
for(int i = 1;i <= tot;i ++){
if(!cnt) break;
int x = find(e[i].u);
int y = find(e[i].v);
if(x != y){
f[x] = y;
cnt --;
ans += e[i].w;
}
}
if(cnt) return -1;
return ans;
}
int main(){
cin >> n >> m >> k;
for(int i = 1;i <= m;i ++){
int u,v,w;
cin >> u >> v >> w;
add(u,v,w);
}
int sum = Krusal();
if(sum == -1){
cout << "No Answer\n";
}
else cout << sum << "\n";
return 0;
}
P1265 公路修建
思路
这个题一眼生成树,于是兴高采烈的交了一发 \(Kruskal\) 然后就挂了,问原因,答曰:\(TLE+MLE\).
那么这题怎么整呢,于是我们掏出了 \(Prim\) 算法,\(Prim\) 在稠密图中比 \(Kruskal\) 优,在稀疏图中比 \(Kruskal\) 劣。
那么就没有什么说的了,把坐标一存,用两点间距离公式(本质勾股定理)。
代码
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int MAXN = 5005;
const int MAXM = 2 * 1e5;
double dis(double x1,double y1,double x2,double y2){
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
double x[MAXM],y[MAXM];
int n, m, s;
double d[MAXN];
bool vis[MAXN];
double prim(){
d[1] = 0.0;
vis[1] = 1;
double ans = 0;
for(int i = 1; i <= n;i ++){
double mind = 1e9 * 1.0;
int v = 1;
for(int j = 1;j <= n;j ++){
if(!vis[j] && d[j] < mind){
mind = d[j];
v = j;
}
}
vis[v] = 1;
ans += d[v];
for(int j = 1;j <= n;j ++){
d[j] = min(d[j],dis(x[v],y[v],x[j],y[j]));
}
}
return ans;
}
int main(){
cin >> n;
for (int i = 1; i <= n; i++){
cin >> x[i] >> y[i];
d[i] = 1e9 * 1.0;
}
printf("%.2lf\n",prim());
return 0;
}
P1194 买礼物
思路
这个题还是很有思维难度的,我第一次还以为要用 \(dp\) 呢,但是看了眼标签,发现是最小生成树,于是我就开始研究怎么个生成树法,这个题的大意是买 \(B\) 件物品,怎么买便宜,有原价 \(A\) 和优惠价 \(K_i{,_j}\) 你可以进行选择。
翻译:你有 \(B\) 个节点,你要把他们连接起来保证边和最小,当然有部分的优惠为 \(0\) ,即没有优惠,或者优惠后比优惠前还贵的,需要注意。最后输出别忘了加上一个 \(A\)。问我为啥?答曰:第一个物品永远不会优惠。
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 800005;
const int MAXM = 4 * 1e7;
int A,B;
struct node{
int u,v,w;
}e[MAXN * 2];
int tot = 0,f[MAXN];
inline void add(int x,int y,int len){
e[++ tot] = {x,y,min(len,A)};
}
inline int find(int x){
return (x == f[x]) ? x : f[x] = find(f[x]);
}
bool cmp(const node &x,const node &y){
return x.w < y.w;
}
int cnt = 0,id = 0;
inline int Krusal(){
for(int i = 1;i <= B;i ++){
f[i] = i;
}
sort(e + 1,e + tot + 1,cmp);
int ans = 0;
for(int i = 1;i <= tot;i ++){
int x = find(e[i].u);
int y = find(e[i].v);
if(x != y){
ans += e[i].w;
f[x] = y;
cnt ++;
}
}
return ans;
}
int main(){
cin >> A >> B;
for(int i = 1;i <= B;i ++){
for(int j = 1;j <= B;j ++){
int w;
cin >> w;
if(!w){
add(i,j,A);
}
else add(i,j,w);
}
}
int sum = Krusal();
cout << sum + A << "\n";
return 0;
}
P1340 兽径管理
思路
首先最朴素的做法是每次加边都跑一遍,但是会炸,所以我们考虑这个时间的问题,如果都没有 \(n - 1\) 条边,那么肯定就不能联通,否则就跑 \(Kruskal\),但是不能瞎跑,跑的时候要判断在当前时间能走的边,不能走就不跑。
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int MAXN = 800005;
const int MAXM = 4 * 1e7;
int n,m;
struct node{
int u,v,w,t;
}e[MAXN * 2];
int tot = 0,f[MAXN];
inline void add(int x,int y,int len,int t){
e[++ tot] = {x,y,len,t};
}
inline int find(int x){
return (x == f[x]) ? x : f[x] = find(f[x]);
}
bool cmp(const node &x,const node &y){
return x.w < y.w;
}
inline void Krusal(int tm){
int ans = 0,cnt = 0;
for(int i = 1;i <= m;i ++){
//判断时间是否满足,不满足就不跑
if(e[i].t <= tm){
int x = find(e[i].u);
int y = find(e[i].v);
if(x != y){
ans += e[i].w;
f[x] = y;
cnt ++;
}
if(cnt == n - 1){
cout << ans << "\n";
return;
}
}
}
cout << "-1\n";
return;
}
int main(){
cin >> n >> m;
for(int i = 1;i <= m;i ++){
int u,v,w;
cin >> u >> v >> w;
add(u,v,w,i);
}
sort(e + 1,e + m + 1,cmp);
for(int i = 1;i <= m;i ++){
if(i < n - 1){
cout << "-1\n";
continue;
}
for(int j = 1;j <= n;j ++){
f[j] = j;
}
Krusal(i);
}
return 0;
}
本文来自一名初中牲,作者:To_Carpe_Diem