图论-----最小生成树
第一次写题解,大佬们勿喷。
传送门
最小生成树其实是最小权重生成树的简称。一个有n个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有n个结点,并且有保持图连通的最少的边。最小生成树可以用kruskal(克鲁斯卡尔)算法或Prim(普里姆)算法求出。---来自于搜狗百科
下面本蒟蒻一一为大家解说
Prim算法
Prim算法其实是一种贪心算法,其基本思想和dijkstra差不多,他最初将无向图中的所有顶点分成两个集合Va和Vb,Va表示已经连入最小生成树的点,Vb反之,最开始Va只有任意取得一个点,从Vb依次取点---其到Va的距离最小直到Vb为空,结束。
算法步骤
- 将一个图分为两部分,一部分归为点集U,一部分归为点集V,U的初始集合为{V1},V的初始集合为{ALL-V1}。
- 针对U开始找U中各节点的所有关联的边的权值最小的那个,然后将关联的节点Vi加入到U中,并且从V中删除(注意不能形成环)。
- 递归执行步骤2,直到V中的集合为空。
- U中所有节点构成的树就是最小生成树。
Prim写法
1 #include<bits/stdc++.h>//万能头文件
2 using namespace std;
3 #define inf 0x7fffffff;//0x7fffffff是long int 的最大值
4 int vst[5001];//用来标记当前的顶点i是否加入了最小生成树
5 int d[5001];//表示i到生成树中所有连边的最小值
6 int g[5001][5001];//用于存储邻接矩阵
7 int ans,n,m;
8 int i,j;
9 void read()//读取数据
10 {
11 int x,y,z;
12 cin>>n>>m;
13 for( i=1;i<=n;i++)
14 {
15 for( j=1;j<=n;j++)
16 {
17 g[i][j]=inf;//把邻接矩阵初始化,注,邻接矩阵虽然好打,但是一个二维数组的内存惊人,最近在清北学堂上课时无意中听到了链表,感觉怪好用,内存还小,有兴趣可以去百度一下。
18 }
19 }
20 for( i=1;i<=m;i++)
21 {
22 cin>>x>>y>>z;
23 if(g[x][y]>=z)//这一点巨坑,某两个点之间会反复读取,如果不去这些值的最小值就会全WA,这是本蒟蒻用血换来的教训。
24 g[x][y]=g[y][x]=z;
25 if(g[x][y]<z)
26 continue;//比赛中推荐用continue,我有一次就用break然后炸了。
27 }
28 }
29 void prim(int v0)//重点
30 {
31 int minn;//最小值
32 int k;
33 memset(vst,0,sizeof(vst));//把vst初始化,一定要写-----来自一个金牌大佬
34 for( i=1;i<=n;i++)
35 {
36 d[i]=inf;
37 }
38 d[v0]=0;
39 ans=0;
40 for( i=1;i<=n;i++)//选择n个点
41 {
42 minn=inf;
43 for( j=1;j<=n;j++)//选择最小边
44 if(vst[j]==0&&minn>d[j])
45 {
46 minn=d[j];
47 k=j;
48 }
49 vst[k]=1;//把vst标记为已用过
50 ans=ans+d[k];
51 for(j=1;j<=n;j++)//把d数组修改了
52 {
53 if(vst[j]==0&&d[j]>g[k][j])
54 d[j]=g[k][j];
55 }
56 }
57 }
58 int main()
59 {
60 read();
61 prim(1);
62 cout<<ans<<endl;
63 return 0;
64 }
65 //因为本题 经过骗分发现无orz,那句话纯属骗初学者。
Kruskal算法
Kruskal算法也是一个常用的算法,好处就是太好打了,一个并查集就没了(什么?),这导致本蒟蒻在比赛中就没打过Prim,后果嘛。。。很开心,TLE了2/5(看你们就迷上了Krusakl---大佬教育人的原话),所以稠密图 Prim > Kruskal,稀疏图 Kruskal > Prim。
算法步骤
- 把图中的所有边从小到大排好,至于你使用快排还是用SHELL排序或是什么大佬专属大法------只要排好就行
- 按边权大小一次选择,若当前边加入树中会出现环就舍弃当前边,反之就将其标记并sum++。
- 重复②直到生成树中有n-1条边,若跑完全图还是不到n-1条边,表示最小生成树不存在,就是真的要输出orz了
注释:
- 将边(E,F)加入R中。 边(E,F)的权值最小,因此将它加入到最小生成树结果R中。
- 将边(C,D)加入R中。 上一步操作之后,边(C,D)的权值最小,因此将它加入到最小生成树结果R中。
- 将边(D,E)加入R中。 上一步操作之后,边(D,E)的权值最小,因此将它加入到最小生成树结果R中。
- 将边(B,F)加入R中。上一步操作之后,边(C,E)的权值最小,但(C,E)会和已有的边构成回路;因此,跳过边(C,E)。同理,跳过边(C,F)。将边(B,F)加入到最小生成树结果R中。
- 第5步:将边(E,G)加入R中。 上一步操作之后,边(E,G)的权值最小,因此将它加入到最小生成树结果R中。
- 将边(A,B)加入R中。 上一步操作之后,边(F,G)的权值最小,但(F,G)会和已有的边构成回路;因此,跳过边(F,G)。同理,跳过边(B,C)。将边(A,B)加入到最小生成树结果R中。
Kruskal写法
1 #include<bits/stdc++.h>
2 using namespace std;
3 struct node
4 {
5 int x,y,z;
6 }a[5000001];//结构体
7 int n,m,ans=0,bj;
8 int prt[500001];
9 int cmp(const node &x,const node &y)//结构体快排,虽然还是要打,但已经比c语言省事多了
10 {
11 return x.z<y.z;
12 }
13 int find(int x)//并查集,基础的东西,一定要掌握,可以看看大佬的题解,会让你感叹这个世界的神奇,真的是清醒脱俗(手动滑稽)--->https://www.luogu.org/problemnew/solution/P3367
14 {
15 if(prt[x]==x)
16 return x;
17 else
18 return prt[x]=find(prt[x]);
19 }
20 void kruskal()//重点
21 {
22 int f1,f2,k,i;
23 k=0;
24 for(i=1;i<=n;i++)
25 prt[i]=i;//初始化
26 for(i=1;i<=m;i++)
27 {
28 f1=find(a[i].x);
29 f2=find(a[i].y);
30 if(f1!=f2)
31 {
32 ans=ans+a[i].z;
33 prt[f1]=f2;//合并两个不同的集合
34 k++;
35 if(k==n-1)
36 break;
37 }
38 }
39 if(k<n-1)//判断最小生成树是否存在
40 {
41 cout<<"orz"<<endl;[](https://www.cnblogs.com/ECJTUACM-873284962/p/7141078.html)
42 bj=0;
43 return ;
44 }
45 }
46 int main()
47 {
48 cin>>n>>m;
49 ans=0;
50 bj=1;
51 for(int i=1;i<=m;i++)
52 cin>>a[i].x>>a[i].y>>a[i].z;
53 sort(a+1,a+m+1,cmp);//结构体快排就是好用,但大佬说STL里除了快排其他的都慢。。。
54 kruskal();
55 if(bj)
56 cout<<ans<<endl;
57 return 0;
58 }
Kruskal和Prim的比较
-
方法上:
Kruskal在所有边中不断寻找最小的边,Prim在U和V两个集合之间寻找权值最小的连接,共同点是构造过程都不能形成环
-
时间上:
Prim适合稠密图,复杂度为O(n n),因此通常使用邻接矩阵储存,复杂度为O(e loge),而Kruskal多用邻接表,稠密图 Prim > Kruskal,稀疏图 Kruskal > Prim。
-
空间上:
Prim适合点少边多,Kruskal适合边多点少