[kuangbin]专题六 最小生成树 题解+总结

kuangbin专题链接:https://vjudge.net/article/752

kuangbin专题十二 基础DP1 题解+总结:https://www.cnblogs.com/RioTian/p/13110438.html

最小生成算法 介绍模板

次最小生成树:介绍及模板

总结:

文章目录

1.Jungle Roads

原题链接:传送门

题意:

​ N个顶点的无向图,给你每条边的长度,要你求该图的最小生成树.其中每个点用大写字母A-Z表示.

思路:

​ 直接kruskal模板即可,转换输入格式.注意输入中的边没有重复边,所以无需判重.

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=30;
const int maxm=100+10;
 
struct Edge
{
    int u,v,dist;
    Edge(){}
    Edge(int u,int v,int d):u(u),v(v),dist(d){}
    bool operator<(const Edge &rhs)const
    {
        return dist<rhs.dist;
    }
};
 
struct Kruskal
{
    int n,m;
    Edge edges[maxm];
    int fa[maxn];
    int findset(int x){ return fa[x]==-1?x:fa[x]=findset(fa[x]); }
 
    void init(int n)
    {
        this->n=n;
        m=0;
        memset(fa,-1,sizeof(fa));
    }
 
    void AddEdge(int u,int v,int dist)
    {
        edges[m++]=Edge(u,v,dist);
    }
 
    int kruskal()
    {
        int sum=0,cnt=0;
        sort(edges,edges+m);
 
        for(int i=0;i<m;i++)
        {
            int u=edges[i].u, v=edges[i].v;
            if(findset(u) != findset(v))
            {
                sum +=edges[i].dist;
                fa[findset(u)] = findset(v);
                if(++cnt>=n-1) return sum;
            }
        }
        return -1;
    }
}KK;
 
int main()
{
    int n;
    while(scanf("%d",&n)==1&&n)
    {
        KK.init(n);
        for(int i=0;i<n-1;i++)
        {
            char s1[10],s2[10];
            int k,v,d;
            scanf("%s%d",s1,&k);
            while(k--)
            {
                scanf("%s%d",s2,&d);
                v=s2[0]-'A';
                KK.AddEdge(i,v,d);
            }
        }
        printf("%d\n",KK.kruskal());
    }
    return 0;
}

2.Networking

原题链接:传送门

思路:

  1. 完完全全的模板题,按照最小生成树的思想写代码即可。

prim 算法

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define ms(a,b) memset(a,b,sizeof a);
const int N = 55;
int n, m;
int g[N][N], d[N];
bool book[N];

int prime() {
    ms(d, 0x3f); ms(book, false);
    int ans = 0;
    //有点类似最短路的dijstra算法
    for (int i = 0; i < n; ++i) {
        int t = -1;
        for (int j = 1; j <= n; ++j) {
            if (!book[j] && (t == -1 || d[t] > d[j])) t = j;
        }
        book[t] = true;//加入集合
        if (i) ans += d[t];//代表不是第一个点	
        //改变其他点到该集合的距离 
        for (int j = 1; j <= n; ++j)d[j] = min(d[j], g[t][j]);
    }
    return ans;
}

int main() {
    //freopen("in.txt", "r", stdin);
    while (cin >> n && n) {
        cin >> m;
        //if (m == 0) { cout << 0 << endl; continue; }
        ms(g, 0x3f);
        int u, v, w;
        for (int i = 1; i <= m; ++i) {
            cin >> u >> v >> w;
            g[v][u] = g[u][v] = min(g[v][u], w);//重边,选择最小值
        }
        cout << prime() << endl;
    }
    return 0;
}

kruskal算法

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define ms(a,b) memset(a,b,sizeof a);
const int maxn = 200 + 55;
const int maxm = 1e5 + 5;
struct edge {
    int from, to;
    int cost;
}b[maxm];
int n, m;
int f[maxn];

bool cmp(edge a, edge b) {
    return a.cost < b.cost;
}

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

int kruskal() {
    int u, v;
    int ans = 0, total = 0;
    for (int i = 1; i <= m; ++i) {
        u = find(b[i].from);
        v = find(b[i].to);
        if (u != v) {
            ans += b[i].cost;
            f[u] = v;
            ++total;
            if (total == n - 1)
                return ans;
        }
    }
    return 0;
}

int main() {
    //freopen("in.txt", "r", stdin);
    int u, v, w;
    while (cin >> n && n) {
        cin >> m;
        for (int i = 0; i <= n; ++i)f[i] = i;
        for (int i = 1; i <= m; ++i) {
            cin >> b[i].from >> b[i].to >> b[i].cost;
        }
        sort(b + 1, b + 1 + m,cmp);
        cout << kruskal() << endl;
    }
    return 0;
}

3.Constructing Roads

原题链接:传送门

思路:

  1. 由于双向路,只需要记录一半的边就可以了。
  2. 常规最小生成树,只不过多了将部分点连接起来,可以用并查集完成这一份操作。提前读入m,连接已经修好的边。
  3. 即便连接起来点原本的祖先是自己,将点连通的时候也需要先find(u),find(v),这一点可参考并查集的连通操作。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn = 200 + 55;
const int maxm = 1e5 + 10;
struct edge {
	int from, to;
	int cost;
}b[maxm];
int f[maxn];
int n, m;
bool cmp(edge a, edge b) {
	return a.cost < b.cost;
}

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

int main() {
	//freopen("in.txt","r",stdin);
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
	int w, u, v;
	while (cin >> n && n) {
		int k = 0;
		for (int i = 0; i <= n; ++i)f[i] = i;
		for(int i = 1;i<= n;++i)
			for (int j = 1; j <= n; ++j) {
				cin >> w;
				if (j > i)
					b[++k].from = i, b[k].to = j, b[k].cost = w;
			}
		cin >> m;
		ll ans = 0;
		int total = 1;
		for (int i = 1; i <= m; ++i) {
			cin >> u >> v;
			u = find(u), v = find(v);
			if (u != v)
				f[u] = v, total++;
		}
		sort(b + 1, b + 1 + k, cmp);
		for (int i = 1; i <= k; ++i) {
			u = find(b[i].from), v = find(b[i].to);
			if (u != v)
				ans += b[i].cost, f[u] = v, ++total;
			if (total == n)break;
		}
		cout << ans << endl;
	}
}

4.Agri-Net

原题链接:传送门

思路:emmm,和上一道是一样的写法,读入的时候改改就可以了

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn = 200 + 55;
const int maxm = 1e5 + 10;
struct edge {
	int from, to;
	int cost;
}b[maxm];
int f[maxn];
int n, m;
bool cmp(edge a, edge b) {
	return a.cost < b.cost;
}

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

int main() {
	//freopen("in.txt","r",stdin);
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
	int w, u, v;
	while (cin >> n && n) {
		int k = 0;
		for (int i = 0; i <= n; ++i)f[i] = i;
		for (int i = 1; i <= n; ++i)
			for (int j = 1; j <= n; ++j) {
				cin >> w;
				if (j > i)
					b[++k].from = i, b[k].to = j, b[k].cost = w;
			}
		ll ans = 0;
		int total = 1;
		sort(b + 1, b + 1 + k, cmp);
		for (int i = 1; i <= k; ++i) {
			u = find(b[i].from), v = find(b[i].to);
			if (u != v)
				ans += b[i].cost, f[u] = v, ++total;
			if (total == n)break;
		}
		cout << ans << endl;
	}
}

5.The Unique MST

原题链接:传送门

题意: 判断最小生成树和次小生成树是否相等, 相等即输出Not Unique!

思路:次小生成树数就是第二小的生成树,仅比最小生成树大;求出次小生成树,判断是否和最小生成树相等,是则不唯一;

这里求次小生成树的方法有两个,一个是基本算法:依次枚举最小生成树的边并去掉,再求最小生成树,这些新求出的最小生成树的值中最小的那个就是次小生成树的值;

用kruskal先求出最小生成树,用used记录最小生成树的边,在不考虑used内任意一个边的情况下做kruskal,得到的 n 个最小生成树 (n的值可能不等于used存放的边数,因为可能存在无法构成最小生成树的情况),那么这n个最小生成树的值中最小的那个,就是次小生成树的权值和;时间复杂度是 V*ElogE(适合稀疏图);

第二个:求出最小生成树中,任意两点之间的最大权值边,取出这些最大权值边中最小的那个值,那么次小生成树 = 最小生成树 的权值和+ 两点直接连接的权值 - 最小的两点之间的权值边(不考虑两点之间的权值);通俗的讲就是在最小生成树中再加一条边,那么这个树必然有环,在这个环中找除新加入的边以外的那条最大的权值边,如果用新边代替这条最大的权值边,那么得到的生成树一定比最小生成树的权值和大,在这些比最小生成树大的权值和中找最小的那个就是次小生成树的权值和;

在用prim求最小生成树的时候,用Max记录两点内的最大权值边,在求出最小生成树后,Max对应的就是两点边之间间接连接的最大权值边的值,用上面的公式就能求出次小生成树的值;时间复杂度 2nn (适合稠密图)

这里提示一下Max的求法(结合下面第二个代码),有点像DP;

这里的prim是《挑战程序设计》中的模板改过来的,代码的模板:把最小生成树的点放在一个集合X中,每加入一个点,就把不在这个集合X的所有点到这个新加入的点的距离做一次比较(距离cost初始化为 INF),如果比本身的cost大,那么就更新当前这个cost,并且用一个数组to来记录这个点指向的边,也就是 to[ i ] = j i到j的权值为cost[ i ] ,j为最小生成树内的点;而cost的实际意义就是当前点到最小生成树中的点中权值最小的值;下次再添加新点的时候就是找cost最小的那个;

最起初的时候,最小生成树只有一个点,那么Max[ i ][ i ] = 0 ( 自己到自己的最大权值肯定是0),当再加入一个点的时候,Max[ i ][ j ] 就是两点之间的权值;再加入一个点k,如果这个k的to是j,那么Max[ k ][ i ] = max(cost[ k ][ j ] ,Max[ j ][ i ]);每加入一个点的时候,就把新添加的点到集合X内所有的点的Max求出来,这样不管下次新加入的点到集合内其他的点有没有边,都能通过比较cost[ 当前点 ][ to ] 和 Max[ to ][ 集合X内任意点 ]的大小来求出Max[ 当前点 ][ 集合X内任意点 ];并不用担心我们需要知道的Max还没求出来,自己可以多在草稿纸上模拟几次,多添加几次点就知道,我们需要知道的Max总是提前求出来的了;感觉有点像DP的数据记忆;

//kruskal 实现的基本算法
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<algorithm>
using namespace std;

const int maxn = 150;
const int inf = 0x3f3f3f3f;

struct Edge {
    int u, v, w;
}edge[maxn * maxn];

int pre[maxn], Rank[maxn], V,M , res, ans, min_res;
bool ok, book[maxn * maxn];

bool cmp(Edge a, Edge b) { return a.w < b.w; }
int find(int x) { return pre[x] == x ? x : pre[x] = find(pre[x]); }

void merge(int x, int y) {
    x = find(x), y = find(y);
    if (x != y) {
        if (Rank[x] > Rank[y]) pre[x] = y;
        else {
            pre[y] = x;
            if (Rank[x] == Rank[y]) Rank[x]++;
        }
    }
}

void Kruskal(int x) {
    for (int i = 0; i <= V; ++i) pre[i] = i, Rank[i] = 0;
    int cnt = 0; res = 0;// 注意下这里res使用的是全局变量 (不过局部变量也没问题)
    for (int i = 1; i <= M; ++i) {
        if (x && i == x)continue;// 遇到 x 不是0的时候,表示x的值代表的是删除的边,做kruskal时不考虑就行 
        if (find(edge[i].u) != find(edge[i].v)) {
            merge(edge[i].u, edge[i].v);
            res += edge[i].w;
            cnt++;
            if (!x) book[i] = true; // x = 0的时候是求最小生成树的时候,记录对应的边 
        }
    }
    if (!x) ans = res;
    if (x && cnt + 1 == V && ans == res)ok = false;
}

int main() {
    int t; cin >> t;
    while (t--) {
        cin >> V >> M;
        for (int i = 1; i <= M; ++i)
            cin >> edge[i].u >> edge[i].v >> edge[i].w;
        memset(book, false, sizeof book);
        sort(edge + 1, edge + 1 + M, cmp);
        ok = true; Kruskal(0); //带入参数 0 ,只求最小生成树 
        for (int i = 1; i <= M; ++i) {
            if (book[i]) {
                Kruskal(i); // 带入的 i 是最小生成树边 
                if (!ok) break;
            }
        }
        if (ok) printf("%d\n", ans);
        else printf("Not Unique!\n");
    }
}
//prim算法 
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
 
using namespace std;
 
const int maxn = 150;
const int INF = 0x3f3f3f3f;
 
int V,G[maxn][maxn],vis[maxn],used[maxn][maxn],Max[maxn][maxn],cost[maxn],to[maxn];
 
void init () {
    for (int i = 1; i <= V; ++i) {
        for (int j = 1; j <= V; ++j) {
            if(i == j) G[i][j] = 0;
            else G[i][j] = INF;
        }
    }
}
 
void solve (int min_mst);
 
void prim () {
    memset(Max,0,sizeof(Max));  // 记录两点之间的最大权值
    memset(used,0,sizeof(used));  // 用来记录最小生成树的边
    memset(to,0,sizeof(to));  // to[i] 代表的是 i 对应的另一个点
    for (int i = 1; i <= V; ++i) {
        cost[i] = INF;    // cost[i] 是点i到已经访问过的点中的最小路径
        vis[i] = 0;
    }
    int mst = 0,v,cnt = 0; cost[1] = 0;
 
    while (1) {
        v = -1;
        for (int u = 1; u <= V; ++u) {
            if(!vis[u] && (v == -1 || cost[u] < cost[v])) v = u;
        }
        if(v == -1) break; vis[v] = 1; mst+=cost[v];
        if(to[v]) {
            used[v][to[v]] = used[to[v]][v] = 1;
            cnt++;
        }
 
        for (int u = 1; u <= V; ++u) {
            if(vis[u]) Max[u][v] = Max[v][u] = max(cost[v],Max[to[v]][u]);  // 更新cost的时候
            if(!vis[u] && G[u][v] != INF) {                             //遇到已经访问过的点就更新Max的值
                if(cost[u] > G[u][v]) {
                    cost[u] = G[u][v];
                    to[u] = v;
                }
            }
        }
    }
    if(cnt+1 != V) printf("Not Unique!\n");
    else solve (mst);
}
 
void solve (int min_mst) {
    int ans = INF;
    for (int u = 1; u < V; ++u) {
        for (int v = u+1; v <= V; ++v) {
            if(!used[u][v] && G[u][v] != INF)
                ans = min(ans,min_mst+G[u][v]-Max[u][v]);  // 枚举不属于最小生成树的边
        }
    }
    if(ans == min_mst) printf("Not Unique!\n");
    else printf("%d\n",min_mst);
}
 
int main(void)
{
    int t,u,v,w,edge;
    scanf("%d",&t);
        while (t--) {
            scanf("%d%d",&V,&edge);
            init (); // 邻接矩阵的初始化,尽量规范化,没有边的情况下为 INF,i = j的时候为0
            for (int i = 1; i <= edge; ++i) {
                scanf("%d%d%d",&u,&v,&w);
                G[u][v] = w;
                G[v][u] = w;
            }
            prim ();
        }
    return 0;
}

6.Highways

原题链接:传送门

思路:这道题的话因为是稠密图,用krus算法的话会超时,需要一定的剪枝

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cmath>
#define ll long long
#define R register int
#define inf 0x3f3f3f3f
using namespace std;
const int manx=1e4;
const int mamx=5e6+5000;
int n,m,u,v,w,k;
int x[manx],y[manx],f[manx];
struct node{
    int u,v,w;
}a[mamx];
bool cmp(node a,node b)
{
    return a.w<b.w;
}
int find(int x)
{
    if(f[x]==x) return x;
    else return f[x]=find(f[x]);
}
int main()
{
    scanf("%d",&n);
    k=0;
    for(int i=1;i<=n;i++){
        scanf("%d%d",&x[i],&y[i]);
        f[i]=i;
    }
    scanf("%d",&m);
    int total=1;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&u,&v);
        u=find(u),v=find(v);
        if(u!=v){ 
            f[u]=v;
            total++; //连通时直接计算最小生成树集合里面的点数
        }
    }
    for(int i=1;i<=n;i++)
        for(int j=i+1;j<=n;j++)
        {
            u=find(i),v=find(j);
            if(u==v) continue; //当边在最小生成树的集合里面跳过 
            w=(x[j]-x[i])*(x[j]-x[i])+(y[j]-y[i])*(y[j]-y[i]);
            a[++k].u=i,a[k].v=j,a[k].w=w;
        }
    sort(a+1,a+1+k,cmp);
    for(int i=1;i<=k;i++)
    {
        u=find(a[i].u),v=find(a[i].v);
        if(u==v) continue;
        f[u]=v;
        total++;
        cout<<a[i].u<<" "<<a[i].v<<endl;
        if(total==n) break;
    }
    return 0;
}

7.Arctic Network

原题链接:传送门

思路:先正常kru做,然后选择k条边免费,即求最小生成树第n-k大边,因为kru算法的边是排序好的,因此只要把第n-k条加入最小生成树的边输出就好。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cmath>
#define ll long long
#define R register int
#define inf 0x3f3f3f3f
using namespace std;
const int manx=1e3;
const int mamx=5e6+5000;
int n,m,u,v,w,k;
int x[manx],y[manx],f[manx];
struct node{
    int u,v;
    double w;
}a[mamx];
bool cmp(node a,node b)
{
    return a.w<b.w;
}
int find(int x)
{
    if(f[x]==x) return x;
    else return f[x]=find(f[x]);
}
int main()
{
    int t;
    cin>>t;
    while(t--){
        scanf("%d%d",&n,&m);
        k=0;
        for(int i=1;i<=m;i++){
            scanf("%d%d",&x[i],&y[i]);
            f[i]=i;
        }
        for(int i=1;i<=m;i++)
            for(int j=i+1;j<=m;j++)
            {
                double z=sqrt((x[i]-x[j])*(x[i]-x[j])*1.0+(y[i]-y[j])*(y[i]-y[j])*1.0);
                a[++k].u=i,a[k].v=j,a[k].w=z;
            }
        sort(a+1,a+1+k,cmp);
        int total=1;
        double ans=0.00;
        for(int i=1;i<=k;i++)
        {
            u=find(a[i].u),v=find(a[i].v);
            if(u==v) continue;
            f[u]=v;
            if(total==m-n){
                ans=a[i].w;
                break;
            }
            total++;
        }
        printf("%.2f\n",ans);
    }
    return 0;
}

8.Building a Space Station

原题链接:传送门

思路:在求两球之间的距离时,如果w<=0,则把这两个球之间的边加入最小生成树的集合中,其他没有什么坑点。

#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<cmath>
#define ll long long
#define R register int
#define inf 0x3f3f3f3f
using namespace std;
const int manx=1e2+5;
const int mamx=1e5+5;
int n,m,u,v,w,k;
double x[manx],y[manx],z[manx],r[manx];
int f[manx];
struct node{
    int u,v;
    double w;
}a[mamx];
bool cmp(node a,node b)
{
    return a.w<b.w;
}
int find(int x)
{
    if(f[x]==x) return x;
    else return f[x]=find(f[x]);
}
int main()
{
    while(cin>>n&&n){
        k=0;
        for(int i=1;i<=n;i++){
            scanf("%lf%lf%lf%lf",&x[i],&y[i],&z[i],&r[i]);
            f[i]=i;
        }
        int total=1;
        for(int i=1;i<=n;i++)
            for(int j=i+1;j<=n;j++)
            {
                double w=sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j])+(z[i]-z[j])*(z[i]-z[j]))-r[i]-r[j];
                if(w>0)a[++k].u=i,a[k].v=j,a[k].w=w;
                else{
                    u=find(i),v=find(j);
                    if(u==v) continue;
                    f[u]=v;
                    total++;
                }
            }
        sort(a+1,a+1+k,cmp);
        double ans=0.000;
        for(int i=1;i<=k;i++)
        {
            u=find(a[i].u),v=find(a[i].v);
            if(u==v) continue;
            f[u]=v;
            ans+=a[i].w;
            total++;
            if(total==n) break;
        }
        printf("%.3lf\n",ans);
    }
    return 0;
}

9.还是畅通工程

原题链接:传送门

思路:模板题。但是输入貌似很多,需要用scanf,不能用cin,不然会超时。

10.畅通工程再续

原题链接:传送门

思路:比普通的模板题多了x和y,需要把坐标化成距离,还有浮点的数据需要注意。

原文地址:https://www.cnblogs.com/RioTian/p/13380764.html