• 最小生成树



    没有用的话qaq :
    Ummmm…图论的大部分知识本来早就有学过,只是一直没有写成博文来梳理,但既然上了qbxt DP图论就写一篇来总结下,主要是来听DP的,但…由于太菜的原因,DP听得天花乱坠QWQ


    一,最小生成树的定义

    一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。 ——百度百科


    二,最小生成树算法
    顾名思义,连通图图中求出图中最小生成树的算法叫做最小生成树算法,分为Prim和Kruskal两种
    1,Prim(普利姆算法)
    a.将所有点分为两个集合,一个集合代表已加入最小生成树点的集合S,另一个集合代表没有加入最小生成树的点的集合T
    b.计算T集合中的点到达S集合的最短距离,du =min<u,v>∈E,v∈S{wu,v}
    c.选取集合T中到达S集合中距离最小的一个点,加入S集合,并更新T集合中其它点到S集合的距离。
    d.重复上述过程,直到所有点都被选入了最小生成树,每次加入的边即是最小生成树的边

    正确性证明:
    Prim 是一个基于贪心的算法,可以采用归纳法和反证法证明其正确性。
    首先证明第一条选中的边e1 一定包含在某最优解方案中,如果最优解中不包含边e1,则加入e1 一定会出现环,且环上存在比e1 大的边,用e1 替换之答案更优,假设最优解包含前k 个选中的边,e1, e2, . . . , ek,则类似地可证明ek+1 存在于最优解中
    运用归纳法,Prim 算法得到的n − 1 条边构成最优解(摘自yousiki课件)。
    简单来说就是每次选择一条最短的边保证了最小生成树距离最短,并且不产生环。

    朴素算法的复杂度较劣,我们可以采取在选择最短边时可以采用堆优化(优先队列),将其复杂度优化为O((n+m)logn)。

    带堆优化代码

    inline void prim(int s)
    {
    	for(int i=1;i<=n;i++)
    	    dist[i]=INF,vis[i]=false;
    	heap.push(make_pair(0,s));
    	vis[s]=true;
    	while(!heap.empty())
    	{
    		int p=heap.top().second; 
    		if(!vis[p]) ans+=-1*heap.top().first;
    		heap.pop();
    		if(vis[p]) continue;
    		vis[p]=true;
    		for(int i=first[p];i;i=edge[i].nxt)
    		{
    			int e=edge[i].ed,d=edge[i].len;
    			if(dist[e]>dist[p]+d)
    			{
    				dist[e]=dist[p]+d;
    				heap.push(make_pair(-dist[e],e));
    			}
    		}
    	}
    }
    

    在堆优化后,prim的复杂度仍较高,一般不采用,但Prim在一类给定N个点(N的范围较大)没有给边,求从某点出发经过所有点后的最短距离的题目中占优,若要采用Kruskal算法就需要建成一个完全图,建图的时间复杂度为O(n^2),显然不优,Prim的优势在于,不需要建成一个完全图,可以边做边在更新集合T到集合S的距离时计算边的距离。


    2,Kruskal(克鲁斯卡尔算法)
    其主要采用了贪心的思想和并查集的维护,我们如果想在图中找出一个最小生成树的话,贪心的想,肯定要先选择最短的边(显然,因为最小生成树嘛),但就这样一条一条选择最短的边知道选到n-1一条为止吗,显然不对,因为我们选择的三条边可能形成环,有环显然不优,所以我们就用并查集维护一下,如果一条边的起点和终点的祖宗一样,即已经联通,我们就不能加,因为显然对于联通的两个点之间再加一条边就会形成环,否则,这条边就可以选择。

    不懂并查集戳这里
    算法复杂度 O(mlogm)

    代码实现比Prim要简单

    #include<cstdio>
    #include<iostream>
    #include<cstring>
    #include<algorithm>
    
    using namespace std;
    
    struct node
    {
    	int s,e,d;
    };
    node edge[2333];
    int ans,f[2333],n,m,cnt;
    
    inline bool cmp(node x,node y)
    {
    	return x.d<y.d;
    }
    
    inline int find(int k)
    {
    	if(f[k]==k) return f[k];
    	return f[k]=find(f[k]);
    }
    
    inline void kruskal()
    {
    	for(int i=1;i<=m;i++)
    	{
    		int f1=find(edge[i].s);
    		int f2=find(edge[i].e);
    		if(f1!=f2)
    		{
    			cnt++;
    			ans+=edge[i].d;
    			f[f1]=f2;
    		}
    		if(cnt==n-1) break;
    	}
    	return;
    }
    
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=m;++i)
    		scanf("%d%d%d",&edge[i].st,&edge[i].ed,&edge[i].d);
    	for(int i=1;i<=n;++i)
    	    f[i]=i;
    	sort(edge+1,edge+m+1,cmp);
    	kruskal();
    	printf("%d",ans);
    	return 0;
    }//看看就好,打了打而已,不保证能过编译Orz QwQ
    

    图论最小生成树算法中,我们经常采用Kruskal,因为它的效率较高


    三,最大生成树:反过来就好


    写在最后,Yousiki Orz Orz

  • 相关阅读:
    一二类斯特林数小结
    我的博客搬家了!
    常用编程思想与算法
    【spring专题】spring简介
    九、 Java程序初始化的顺序(二)
    八、 Java程序初始化的顺序(一)
    python的上下文管理
    七、 面向对象(二)
    六、 面向对象基础
    paramiko模块及ssh远程登陆
  • 原文地址:https://www.cnblogs.com/Hoyoak/p/11336566.html
Copyright © 2020-2023  润新知