传送门:牛客
题目描述:
胡队长带领HA实验的战士们玩真人CS,真人CS的地图由一些据点组成,现在胡队长已经占领了n个据点,为了方
便,将他们编号为1-n,为了隐蔽,胡队长命令战士们在每个据点出挖一个坑,让战士们躲在坑里。由于需要在任意两
个点之间传递信息,两个坑之间必须挖出至少一条通路,而挖沟是一件很麻烦的差事,所以胡队长希望挖出数量尽可
能少的沟,使得任意两个据点之间有至少一条通路,顺便,尽可能的∑d[i][j]使最小(其中d[i][j]为据点i到j的距离)。
输入:
2 2
1 2 1
1 2 3
输出:
1
一道最小生成树的板题.接下来就就借此较为详细的介绍一下最小生成树经典的两种算法,prim和kruskal
首先介绍一下什么是最小生成树,就是给你一张图然后求出联通每一个点的边权值和最小的一张联通图,因为边权值和最小,所以显然最后是没有环的,所以也就变成了一棵树,这个求解的过程就叫做最小生成树.
首先介绍一下 k r u s k a l kruskal kruskal算法,该算法理解起来比较简单
首先该算法基于边进行操作,复杂度瓶颈在于排序,为 m l o g m mlogm mlogm,和点数无关,所以在稀疏图中比较优秀
算法思想:
首先我们的 k r u s k a l kruskal kruskal的算法思想就是将我们现有的边经过排序,然后每一次都取出权值最小的一条边,然后判断一下会不会因为加入这条边形成环,如果不会的话就加入这条边,直到我们的边数等于点数-1,构成一棵树
正确性解释
因为最小生成树是一张联通图,那么显然的,他是由多个联通分量通过边进行连接的.现在我们简化一下我们的问题,假设我们现在有两个联通分量,那么我想将这两个联通分量联通需要干什么??显然就是找出我们的两个联通分量之间最短的一条边是吧.那么假设我们现在有三个联通分量(两两不连通),现在我们想联通这三个分量,我们应该怎么办,显然先是求出两两之间最短的一条边(因为联通分量可能包含了多个点,那么此时可能有多条边可以构成联通),然后此时我们就有三条边了,此时我们肯定是找出三条边之间最短的两条边是吧.那么现在将我们的联通分量的数量推广到n个(注意单个点我们此时也算联通分量),那么此时我们就变成了如何联通n个点.那么显然的,我们应该找出两两联通量之间最短的边,然后选取最短的 n − 1 n-1 n−1条边即可
那么此时我们结合一下 k r u s k a l kruskal kruskal的实现过程,我们将边进行了一个排序,然后从小往大取边(并且这条边不会构成环,保证我们的边是不同的联通分量之间).这意味这什么呢,说明我们每一次取得边都是目前我们的所有联通量两两之间的最短边中的最短的边(此处十分重要,请理解后继续),那么此时选取的边显然是符合我们最短的 n − 1 n-1 n−1条边中的一条的,因为都是最短的了,显然符合最短的 n − 1 n-1 n−1条,所以正确性此时也就不难理解了
如果还是无法理解的话,我再讲详细一点,我们现在有n个点,那么显然我们现在选取了n个点之中最短的边,是符合我们的上述最短的n-1条边的情况的,那么此时我们有一个两个点的联通分量出现了,此时一共有n-1个联通分量,那么此时我们的下一条边既然是不允许我们的联通分量形成一个环,那就说明他只能用来联通两个联通分量,所以此时的话我们的这条边可以认作n-1个联通分量中最短的边(此时我们将问题化为联通n-1个联通分量,因为我们求出的是所有的边,所以此时边不变),那么此时最短的边,符合我们的n-2条最短边中的一条,然后逐渐递归下去,正确性也可以得到证明
下面是 k r u s k a l kruskal kruskal的实现代码:
//Kruskal
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <map>
#include <set>
#include <queue>
#include <string.h>
#include <stack>
#include <deque>
using namespace std;
typedef long long ll;
#define inf 0x3f3f3f3f
#define root 1,n,1
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
inline ll read() {
ll x=0,w=1;char ch=getchar();
for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;
for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
return x*w;
}
#define maxn 1000000
#define ll_maxn 0x3f3f3f3f3f3f3f3f
const double eps=1e-8;
int n,m;
struct Node{
int u,v,w;
}edge[maxn];
bool cmp(Node a,Node b) {
return a.w<b.w;
}
int fa[maxn];
int find(int a) {
if(a==fa[a]) return a;
return fa[a]=find(fa[a]);
}
int main() {
n=read();m=read();
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++) {
edge[i].u=read();edge[i].v=read();edge[i].w=read();
}
sort(edge+1,edge+1+m,cmp);
int cnt=0;int ans=0;
for(int i=1;i<=m;i++) {
int x=edge[i].u,y=edge[i].v;
if(find(x)==find(y)) continue;
cnt++;
fa[find(x)]=find(y);
ans+=edge[i].w;
if(cnt==n-1) {
break;
}
}
if(cnt!=n-1) printf("-1\n");
else cout<<ans<<endl;
return 0;
}
然后是 p r i m prim prim部分:
首先正经的 p r i m prim prim是根据点来的,复杂度为 n 2 n^2 n2,但是因为朴素版的 p r i m prim prim的复杂度有点高,并且是以存边进行的,实现起来不太舒服,所以博主接下来的 p r i m prim prim是"不正经版的",我存的是点,然后用的类似于dijkstra的堆优化的方式实现的,如果没见过可以参考一下,实现起来很舒服,但是此时复杂度时Mlogn,跟边有了一点点关系
算法思想:
我们随便找一个点作为我们的最小生成树的根节点,然后遍历我们的邻接点,将所有点和**将这个点加入我们的树的花费(注意这一点和dij不一样)**存入我们的堆中,然后我们的小根堆每次顶出点和和花费,然后继续更新,直到有n个点为止
正确性证明:
来一张洛谷上某大佬的一张图,便于解释.那么对于上面的这张图来说,我们直接拿B点证明一下,其他点的证明和B点是一样的,类比一下即可.
对于我们的B点,此时我们发现BG的长度是目前队列中距离最小的(队列中此时应有AF,BC,BI),那么此时按照prim算法就应该直接将BG这条边当做我们的最小生成树的一条边了.接下来证明一下,经过前面的 k r u s k a l kruskal kruskal的算法证明中,我们提到n个联通分量之间最短的n-1边显然就是我们的最小生成树的一条边,又因为此时BG是目前队列中距离最小的一条边,那么就是我们最短的边,不妨设此时树中的点的邻接点记为vn(vn显然也包括了我们的G点),注意此时的vn是目前我们的树所能联通的所有点,那么对于我们的G来说有两种情况,要么我们的G还是和B相连,要么我们的G和其他点形成一个联通分量然后靠其他点和树联通,那么此时又因为形成了一个联通分量,所以显然换用G连接更为优秀.所以无论如何都是G与树连接时最优的.
理解起来可能有点费劲,如果之前碰过堆优化的dijkstra应该好理解一点,建议自己对着例图模拟几遍
至此,证明结束
下面是具体的代码部分:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <map>
#include <set>
#include <queue>
#include <string.h>
#include <stack>
#include <deque>
using namespace std;
typedef long long ll;
#define inf 0x3f3f3f3f
#define root 1,n,1
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
inline ll read() {
ll x=0,w=1;char ch=getchar();
for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;
for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
return x*w;
}
#define maxn 1000000
#define ll_maxn 0x3f3f3f3f3f3f3f3f
const double eps=1e-8;
int n,m;
struct Node{
int v,w;
};
struct heapnode{
int u,d;
bool operator<(const heapnode &rhs) const {
return d>rhs.d;
}
};
vector<Node>edge[maxn];
int dis[maxn];int vis[maxn];
void prim(int S) {
memset(dis,0x3f,sizeof(dis));
priority_queue<heapnode>q;
q.push({S,0});
dis[S]=0;
int cnt=0;int ans=0;
while(!q.empty()&&cnt<n) {
heapnode f=q.top();q.pop();
int u=f.u;
if(vis[u]) continue;
cnt++;ans+=dis[u];
vis[u]=1;
for(int i=0;i<edge[u].size();i++) {
int v=edge[u][i].v;
if(dis[v]>edge[u][i].w) {
dis[v]=edge[u][i].w;
q.push({v,dis[v]});
}
}
}
if(cnt==n) printf("%d\n",ans);
else printf("-1\n");
return ;
}
int main() {
n=read();m=read();
for(int i=1;i<=m;i++) {
int u=read(),v=read(),w=read();
edge[u].push_back({v,w});
edge[v].push_back({u,w});
}
prim(1);
return 0;
}