最小生成树和最短路径问题的总结


这两个问题虽然没太大关系,但我是这两个一起学的(刚开始把这两个问题搞混了导致学岔了>﹏<) ,所以就把他们总结到一起了。

最小生成树

概念

最小生成树问题是指给定一个图,在图中选择n-1条边使n个点之间相互连通且边长和最小,例题如下。(注意以下两种算法只能应用于无向图中)。
在这里插入图片描述

Kruskal算法

Kruskal的思想是不断选取长度最短的边,最终所得即为最小。在选取这条边前我们需要判断选取这条边后是否会成环(因为要生成一棵树嘛),如果会则不选取这条边。而在这里我们就需要运用并查集来判断两个端点是否已经连通。至于判断该图是否连通的方法就是检测最后是否选取了n-1条边。所以大概思路为用一个vector数组来存边并按边的长度排序,然后遍历这个数组,当起点和终点不联通时选取这条边并连通这两个点,那么直接看代码吧,代码比较清晰。

struct node{
	ll u,v,val;//为起点,v为终点,val为边长
	bool operator < (node&a){//重载<便于sort排序
		return val<a.val;
	}
};
vector<node>v;储存边
ll n,m,x,y,z,f[maxn];

ll find(ll x){//并查集
	if(f[x]==x)return x;
	return f[x]=find(f[x]);
}
void kruskal(){
	ll sum=0,cnt=0;//cnt用来记录边的数目
	for(auto i:v){
		if(find(i.u)==find(i.v))continue;//当这两个点连通时不选取
		f[find(i.u)]=find(i.v);
		sum+=i.val;
		cnt++;
	}
	if(cnt<n-1){//选取边小于n-1时,该图不连通
		cout<<"orz";
		return;
	}
	cout<<sum;
}
int main(){
	cin>>n>>m;
	for(int i=0;i<m;i++){
		cin>>x>>y>>z;
		v.push_back({x,y,z});
	}
	for(int i=1;i<=n;i++){
		f[i]=i;//并查集的初始化
	}
	sort(v.begin(),v.end());//按边长排序
	kruskal();
	return 0;
}

算法复杂度为O(mlogm),m为边的数目,这个算法可以解决大部分情况下的问题,只要边不是特别多都能解决。但遇到那种边多点少的情况就要用到下面这个Prim算法了。

Prim算法

Prim也是一个基于贪心的算法,我们先假设存在集合a(已经连通的点的集合)b(待选取的点的集合),每次从b中选取距离连通集a最近的点加入集合a(注意这里是距离连通集最近而不是距离哪一个点距离最近)最终所得为最小生成树。这个算法的关键点就在这个距离连通集最近,我们用dis[i]表示i点到连通集最短距离为dis[i],每加入一个点后我们对其进行更新,通过遍历该数组得到应加入连通集的点。

ll n,m,a[maxn][maxn],vis[maxn],dis[maxn];//n为点数,m为边数
void prim(){
	memset(dis,0x3f,sizeof(dis));//将距离一开始初始化为INF
	dis[1]=0;//1为起点,将其设置为0
	ll sum=0;//记录总长度
	for(int i=0;i<n;i++){//循环n遍选取n个点
		ll id=-1;
		for(int j=1;i<=n;j++){//寻找距离连通集最近的点
			if(!vis[j]&&(id==-1||dis[id]>dis[j])){
				id=j;
			}
		}
		if(dis[id]==INF){//无点与该连通集连通
			cout<<"orz";
			return;
		}
		sum+=dis[id];
		vis[id]=1;//标记为已加入连通集
		for(int j=1;j<=n;j++){//加入点后遍历其可到达的点,更新这些点到连通集距离
			if(!vis[j])dis[j]=min(dis[j],a[id][j]);
		}
	}
	cout<<sum;
}

该算法时间复杂度为O(n^2),不过可以通过优先队列优化为O(nlogn),优化方式和Dijkstra一样,所以就放到最后说了。

最短路径

概念

最短路问题是求解两个点直接的最短距离。在这里和最小生成树做一下区分(因为我总是搞混淆),最小生成树是总长度最小,并不保证其中任意两点间距离最小,而最短路则是求解任意两点距离最小。所以这两个问题差别还是挺大的。而且下面介绍的这两种算法有向图无向图均适用,上文介绍的两个经典最小生成树的算法只适用于无向图。
在这里插入图片描述

Floyd算法

Floyd算法是经典的动态规划算法,通过三层循环不断尝试是否存在点k使i -> k -> j距离小于i -> j,所以状态转移方程就是

dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);

for(int k=1;k<=n;k++){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]);
		}
	}
]

(注意这里k这层循环需要放在最外面)
优点: 该算法可以直接求出任意两点间距离最小值,还可以处理负边,而Dijkstra只能求出其他点到一点的距离最小值,而且代码简洁易记忆。
缺点: 该算法时间复杂度为O(n^3),只能应用于特定题目,例如上述例题该算法就不能很好的解决。

Dijkstra算法

Dijksra算法是最短路中比较常用的算法(反观Prim却在最小生成树中比较少用),但其不能处理边权为负数的情况。思路大致为对于选定一点 i 遍历其所有路径,所得最短路径则为i到j的最短路径,即不存在k使i -> k -> j的距离小于i -> j,然后通过j这个点更新j可以到达的点与点i的距离来求出所有点到i的最小距离。至于为什么不能处理负边是因为会出现下图的情况Dijkstra算法得出最小距离为2,但因为存在负边,即存在k使i -> k -> j的距离小于i -> j,所以会出现错误,这个算法其实也是利用了只有正边这个条件来求最小距离。
在这里插入图片描述

#include<bits/stdc++.h>
#define ll long long
#define rep(i,a,b) for(int i=a;i<b;i++)
#define endl '\n'
#define mem(a) memset(a,0,sizeof(a))
#define y second
#define lowbit(x) x&-x
using namespace std;
const ll mod=1e9+7;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int maxn=5e3+5;
struct edge{
	ll to,val;
};//to为该点指向的下一个点,val为边长
ll cnt,vis[maxn],dis[maxn];//dis[i]记录每个点和要求的点最小距离,vis[i]标记该点是否被访问
ll n,m,k,x,y,z;
vector<edge>g[maxn];
void init(){//初始化
	mem(vis);
	memset(dis,0x3f,sizeof(dis));
}
void dijkstra(ll x,ll y){
	init();
	dis[x]=0;
	while(1){
		ll id=-1,minn=INF;
		rep(i,1,n+1){//找到距离起始点最近且未被访问的点
			if(!vis[i]&&dis[i]<minn){
				id=i;
				minn=dis[i];
			}
		}
		if(id==-1)break;//当无可更新点则跳出循环
		vis[id]=1;//标记为已访问
		for(auto i:g[id]){//更新每个点到起始点的最小距离
			if(!vis[i.to]&&dis[i.to]>i.val+dis[id]){
				dis[i.to]=i.val+dis[id];
			}
		}
	}
	if(dis[y]==INF)cout<<"-1"<<endl;//如果x与y不连通
	else cout<<dis[y]<<endl;
}
int main(){

	cin>>n>>m>>k;
	rep(i,0,m){
		cin>>x>>y>>z;
		g[x].push_back({y,z});//存图
	}
	while(k--){
		cin>>x>>y;
		dijkstra(x,y);
	}
	return 0;
}

Dijkstra算法的堆优化

Dijkstra算法的复杂度为O(n^2),这在很多题目中是远远不够的,所以必须对其进行优化。可以看到我们在寻找最小值的时候遍历了整个dis数组,做了很多无用功,所以这里我们用优先队列priority_queue,通过取栈顶元素快速获得最短的路径,时间复杂度为O(nlogn)。

#include<bits/stdc++.h>
#define ll long long
#define PLL pair<ll,ll>
#define rep(i,a,b) for(int i=a;i<b;i++)
#define endl '\n'
#define mem(a) memset(a,0,sizeof(a))
#define x first
#define y second
#define lowbit(x) x&-x
using namespace std;
const ll mod=1e9+7;
const ll INF=0x3f3f3f3f3f3f3f3f;
const int maxn=5e3+5;


struct edge{
	ll to,val;
};
ll cnt,vis[maxn],dis[maxn];
ll n,m,k,x,y,z;
vector<edge>g[maxn];
void init(){
	mem(vis);
	memset(dis,0x3f,sizeof(dis));
}
void dijkstra(ll x,ll y){
	init();
	priority_queue<pair<ll,ll>,vector<pair<ll,ll>>,greater<pair<ll,ll>> > q;
	//将最小值放在队首的优先队列
	q.push(make_pair(0,x));//起始点入队
	dis[x]=0;
	while(!q.empty()){
		PLL temp=q.top();//PLL即为pair<ll,ll>
		q.pop();
		ll id=temp.y;
		if(vis[id])continue;//如果该点已被访问则不做处理
		vis[id]=1;
		for(auto i:g[id]){
			if(!vis[i.to]&&dis[i.to]>dis[id]+i.val){
				dis[i.to]=dis[id]+i.val;//有点被松弛时将其入队
				q.push(PLL(dis[i.to],i.to));
			}
		}
	}
	if(dis[y]==INF)cout<<"-1"<<endl;
	else cout<<dis[y]<<endl;
}
int main(){

	cin>>n>>m>>k;
	rep(i,0,m){
		cin>>x>>y>>z;
		g[x].push_back({y,z});
	}
	while(k--){
		cin>>x>>y;
		dijkstra(x,y);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值