图论-最短路问题

 最短路旨在解决这样一种问题,在图中,需要知道从点i到点j的路径长度的最小值。

松弛操作:

在最短路中,我们需要做松弛操作,即当我们能够找到一个点k使得i->j能够通过k做中转变得更小,即满足i->j < i->k + k->j ,此时我们将i->j 更新为 i->k + k->j,此为松弛操作。

多源最短路

Folyd算法:

Folyd算法采用动态规划的算法,我们这样抽象问题:在考察n个点的情况下,求i->j的最短路径长度。当我们考察是否能够经过第k个点时使得i->j通过k这个中继变短,即i->j > i->k + k->j。算法原理是生成一系列中继矩阵dist^{[k]}[i][j],k表示考虑了1~k个点作为中继,那么自然有dist^{[n]}[i][j]为最终的i->j最短路径长度。但是不知道大家有没有个疑问,最短路本身的路径顺序并不是按照从1~n的呀,那为什么Foldy还是正确的呢?

假设i-j的最小路径上有两个中继点k1、k2,(k1<k2),现在假设i->j的最短路径是i->...->k2->...k1->....->j,且当前考虑的中继点是k2,即在i->j的这条最短路径上,所有的点大小都满足<=k2,那么我们将这条路径分割成i->k2与k2->j两端,由于路径上(不包括端点)的点都满足<k,因此他们的最短路径大小就是dist^{[k2-1]}[i][k2]dist^{[k2-1]}[k2][j],故算法正确。

核心代码:

void Foldy(int[][] dist,int n){
	for(int k=1;k<=n;++k){//考虑让每条关系通路都经过k 是否能变短 
		for(int i=1;i<=n;++i){
			for(int j=1;j<=n;++j){
				dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
			}
		}
	}
} 

void initFoldy(){
	int dist[410][410];
	memset(dist,0x3f,sizeof(dist));//0x3f3f3f3f + 0x3f3f3f3f < INT_MAX
	for(int i=1;i<=n;++i)dist[i][i]=0;
}

单源最短路

单源最短路是要求源点S到其余所有点的最短路径长度。

Bellman-Ford算法:

由图论的基本知识我们可以知道,在一个一共有n个点的连通图中,点x与源点S构成的最短路径上最多有n个点即n-1段(且这样的点只存在一个),即构成链式结构。因此,我们只需要对所有的边统统松弛n-1次,那么只要图本身不存在负环,则n-1后的松弛结果就是最短路。而且,根据松弛操作的更新规则,多余的松弛并不会影响最终结果。

当Bellman-Ford运行第n次时,如果有边还能被松弛,说明图中必定存在负环。

//单源最短路
//最短路的子路也是最短路 
//最短路上的节点数最多为n个 由n-1条边构成 最多松弛n-1次  不需要考虑每次松弛哪些边,直接松弛所有边 
void Bellman-Ford(int n,int m){//n:点个数 m:边条数 
	for(int i=1;i<n;++i){
		for(int j=1;j<=m;++j){
			dist[edge[j].to]]=min(dist[edge[j].to],dist[edge[j].from]+edge[j].w);
		}
	}
}

bool Bellman-Ford-judge(int n,int m){//判负环 n:点个数 m:边条数 第n次松弛时 有更新 则定有负环 
	for(int i=1;i<n;++i){
		for(int j=1;j<=m;++j){
			dist[edge[j].to]]=min(dist[edge[j].to],dist[edge[j].from]+edge[j].w);
		}
	}
	for(int j=1;j<=m;++j){
		if(dist[edge[j].from]+edge[j].w<dist[edge[j].to])return false;
	}
	return true;
}

SPFA算法:

SPFA是以Bellman-Ford算法为基础的优化,它基于这样的考虑:每次我们基于一个源点S进行松弛的时候,只能把那些S能够到达的点(被S更新了的点)进行更新,也就是说,只有这些被更新的点用于更新,才有可能使得他们能够达到的点与源点S之间的路径长度变小。因此SPFA采用一个队列进行优化,每次只将可能引起更新的点放入队列,并且保证队列里不会有重复的点。SPFA的原理如下:设S->x的最短路径为S->p1->p2->...->x,那么在对S能到达的所有点进行松弛时,p1一定在其中,而使用p1松弛时,p2也一定在其中,如此就可以得到S->x的最短路径。

//SPFA
//只把可能导致更新的起始点入队列 
//存储结构采用链式前向星 
void SPFA(int s){
	queue<int> que;
	que.push(s);
	dist[s]=0;
	int p;
	while(!que.empty()){
		p=que.front();
		que.pop();
		inque[p]=0;//表示该点不在队列中
		for(int cur=head[p];cnt!=0;cnt=edge[cur].next){
			int to=edge[cnt].to;
			if(dist[to]<dist[p]+edge[cur].w){
				//pre[to]=p;
				dist[to]=dist[p]+edge[cur].w;
				if(!inque[to]){//发生更新的点不在队列中 
					inque[to]=1;
					que.push(to);
				}
			}
		} 
	}
}

//判负环  每次松弛后,如果一个点入队列n次,则必有负环 否则最多入队列n-1次 
bool SPFA-judge(int s){
	queue<int> que;
	que.push(s);
	dist[s]=0;
	count[s]=1;
	int p;
	while(!que.empty()){
		p=que.front();
		que.pop();
		inque[p]=0;//表示该点不在队列中
		for(int cur=head[p];cnt!=0;cnt=edge[cur].next){
			int to=edge[cnt].to;
			if(dist[to]<dist[p]+edge[cur].w){
				dist[to]=dist[p]+edge[cur].w;
				if(!inque[to]){//发生更新的点不在队列中 
					count[to]++;
					if(count[to]==n)return true;
					inque[to]=1;
					que.push(to);
				}
			}
		} 
	}
	return false;
}

事实上,SPFA虽然保证了队列在某个时刻不会有两个相同的点,但是不能保证一个点多次入队列,这个过程持续到这个点的dist值被更新为最短路径。而SPFA和Bellman-Ford判负环的方式如出一辙,因为最短路径最长可以有n-1段,因此一个点最多会入队列n-1次,那么只需要统计一下入队次数,(在有负环的情况下SPFA是不会终止的,需要手动跳出),一旦有一个点入队了n次,即存在负环。

P3371 【模板】单源最短路径(弱化版)

提交177.81k

通过51.26k

时间限制1.00s

内存限制125.00MB

题目描述

如题,给出一个有向图,请输出从某一点出发到所有点的最短路径长度。

输入格式

第一行包含三个整数 n,m,sn,m,s,分别表示点的个数、有向边的个数、出发点的编号。

接下来 mm 行每行包含三个整数 u,v,wu,v,w,表示一条 u \to vu→v 的,长度为 ww 的边。

输出格式

输出一行 nn 个整数,第 ii 个表示 ss 到第 ii 个点的最短路径,若不能到达则输出 2^{31}-1231−1。

输入输出样例

输入 

4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4

输出 

0 2 4 3
#include<bits/stdc++.h>
using namespace std;

struct Edge{
	int to,w,next;
};

int cnt,head[10010],dist[10010];
Edge edge[500010];

void add(int from,int to,int weight){
	edge[++cnt].to=to;
	edge[cnt].w=weight;
	edge[cnt].next=head[from];
	head[from]=cnt;
}

void SPFA(int S){
	memset(dist,0x3f,sizeof(dist));
	queue<int> que;
	que.push(S);
	inque[S]=true;
	dist[S]=0;
	int e,to,w;
	while(!que.empty()){
		S=que.front();
		que.pop();
		inque[S]=false;
		for(e=head[S];e!=0;e=edge[e].next){
			to=edge[e].to;w=edge[e].w;
			if(dist[to]>dist[S]+w){
				dist[to]=dist[S]+w;
				if(!inque[to]){
					que.push(to);
					inque[to]=true;
				}
			}
		}
	} 
}

int main(){
	int n,m,s,u,v,w,i;
	cin>>n>>m>>s;
	for(i=1;i<=m;++i){
		cin>>u>>v>>w;
		add(u,v,w);
	}
	SPFA(s);
	for(i=1;i<=n;++i){
		cout<<(dist[i]==0x3f3f3f3f?0x7fffffff:dist[i])<<" ";
	}
	return 0;
} 

Dijkstra算法

基于贪心的思想,每次将松弛得到的最短路径值的点作为新的出发点做新的松弛。

1.在没有负环的条件下,首先我们用源点作为出发点松弛所有边,此时每个点的dist值保存的是由S直接到它的距离,其中拥有最小dist值的点,其dist值就是它与源点的最短路径的长度,因为不存在负环,所以不可能通过其他点使得它与源点的最短路径更短。

2.我们用上一次的最小dist值的点S'进行松弛,并使得该点无法再被使用,而此时松弛过的所有dist值可以分为由S直接到达和经过S'到达,同理,由于不存在负环,所以该次最小dist值的点一定是最终答案。后面继续该过程直到点使用完。

void Djkstra(int S,int n){
	memset(dist,0x3f,sizeof(dist));
	dist[S]=0;
	int i,j,k,to,w,MinDist;
	for(i=1;i<=n;++i){
		MinDist=0x3f3f3f3f;
		for(j=1;j<=n;++j){
			if(!used[j]&&MinDist>dist[j]){
				MinDist=dist[j];
				k=j;
			}
		}
		used[k]=true;
		for(j=head[k];j!=0;j=edge[j].next){
			to=edge[j].to;w=edge[j].w;
			if(!used[to])dist[to]=min(dist[to],dist[k]+w);
		}
	}
}

朴素的DjkStra也可以过。。=-=

#include<bits/stdc++.h>
using namespace std;

struct Edge{
	int to,w,next;
};

int cnt,head[10010],dist[10010];
Edge edge[500010];

void add(int from,int to,int weight){
	edge[++cnt].to=to;
	edge[cnt].w=weight;
	edge[cnt].next=head[from];
	head[from]=cnt;
}

bool used[10010];

void Djkstra(int S,int n){
	memset(dist,0x3f,sizeof(dist));
	dist[S]=0;
	int i,j,k,to,w,MinDist;
	for(i=1;i<=n;++i){
		MinDist=0x3f3f3f3f;
		for(j=1;j<=n;++j){
			if(!used[j]&&MinDist>dist[j]){
				MinDist=dist[j];
				k=j;
			}
		}
		used[k]=true;
		for(j=head[k];j!=0;j=edge[j].next){
			to=edge[j].to;w=edge[j].w;
			if(!used[to])dist[to]=min(dist[to],dist[k]+w);
		}
	}
}

int main(){
	int n,m,s,u,v,w,i;
	cin>>n>>m>>s;
	for(i=1;i<=m;++i){
		cin>>u>>v>>w;
		add(u,v,w);
	}
	Djkstra(s,n);
	for(i=1;i<=n;++i){
		cout<<(dist[i]==0x3f3f3f3f?0x7fffffff:dist[i])<<" ";
	}
	return 0;
} 

事实上,朴素的Dijkstra算法需要O(n^2)的复杂度。但是由于它是取最小值,因此可以采用堆优化的方式,将复杂度压低至O(nlogn)。然而Dijikstra的队列优化将不可避免地让同一时刻里可能存在多个重复的点,而他们拥有不同的dist值(因为是在不同的松弛过程中得到的),但是由于我们每次取得是dist最小的点,因此当我们使用完这个点后,会将它标记为不可用,因此即便我们后面又一次拿到了dist值更大的相同点,但是由于已经使用过dist值更小的点进行更新,因此直接忽略就好了,不会对算法造成任何影响。

//Dijkstra算法 队列优化
typedef pair<int,int> Polar;// dist、to  按照dist排序 

void Dijkstra(int s){
	dist[s]=0;
	priority_queue<Polar,vector<Polar>,greater<Polar> > que;
	que.push(make_pair(0,s));	
	int p,e,to;
	while(!que.empty()){
		p=que.top().second;
		if(vis[p])continue;//如果p点在之前已经被用于更新过 则这次的答案并不是最优值 不再用于更新 
		vis[p]=1;
		for(e=head[p];e!=0;e=edge[e].next){
			to=edge[e].to;
			if(dist[to]>dist[p]+edge[e].w){
				dist[to]=dist[p]+edge[e].w;//松弛虽然可能进行多次 但是最短的那条一定会被最先用于更新别人,更新别人后vis=1,其他结果就失效了 
				pre[to]=p;
				if(!vis[to])que.push(make_pair(dist[to],to));//没发生松弛则没必要放入队列  因为一开始出了源点 所有点的dist是INF  至少都会发生一次松弛 
			}
			
			
		}//也就是说 队列里会出现有不同dist却有相同to的情形,但是那个dist更小的to会被用来更新别的点,所以无需担心,而且dist[]数组的答案一定是较小的那个 
	}
} 

//路径打印 用pre数组指向父亲节点 原因:每一个节点的父亲都固定 但是每一个节点的儿子会有很多
void getPath(){
	if(edge[e].w+dist[p]<dist[to]){//被更新 
		pre[to]=p;
		dist[to]=edge[e].w+dist[p];
	}
} 

void printPath(int s,int v){
	while(v!=s){
		print("%d<-",v);
		v=pre[v];
	}
	print("%d",s);
}

用队列优化的DjkStra过一下洛谷的P3371。

#include<bits/stdc++.h>
using namespace std;

struct Edge{
	int to,w,next;
};

int cnt,head[10010],dist[10010];
bool inque[10010];
Edge edge[500010];
bool used[10010];

void add(int from,int to,int weight){
	edge[++cnt].to=to;
	edge[cnt].w=weight;
	edge[cnt].next=head[from];
	head[from]=cnt;
}

typedef pair<int,int> Dist; //<dist,i>

void Djkstra(int S,int n){
	memset(dist,0x3f,sizeof(dist));
	dist[S]=0;
	priority_queue<Dist,vector<Dist>,greater<Dist> > DistQue;//构造小根堆 
	DistQue.push(Dist{0,S});
	Dist Top;
	int distance,i,to,w;
	while(!DistQue.empty()){
		Top=DistQue.top();//取出一个拥有最小dist值的人 用他进行更新 并标记为已使用 
		DistQue.pop();
		distance=Top.first;
		S=Top.second;
		
		if(used[S])continue;//used为真 说明已经用于更新了 所以队列中存储的这个dist值在没有负环的情况下一定不会小于上次用于更新的值 
		
		used[S]=true;//标记为使用 
		
		for(i=head[S];i!=0;i=edge[i].next){
			to=edge[i].to;w=edge[i].w;
			if(dist[to]>distance+w){
				dist[to]=distance+w;
				DistQue.push(Dist{dist[to],to});
			}
		}
	}
}


int main(){
	int n,m,s,u,v,w,i;
	cin>>n>>m>>s;
	for(i=1;i<=m;++i){
		cin>>u>>v>>w;
		add(u,v,w);
	}
	Djkstra(s,n);
	for(i=1;i<=n;++i){
		cout<<(dist[i]==0x3f3f3f3f?0x7fffffff:dist[i])<<" ";
	}
	return 0;
} 

P4779 【模板】单源最短路径(标准版)

提交110.58k

通过36.43k

时间限制1.00s

内存限制125.00MB

题目描述

给定一个 n个点,m 条有向边的带非负权图,请你计算从 s出发,到每个点的距离。

数据保证你能从 ss 出发到任意点。

输入格式

第一行为三个正整数 n, m, sn,m,s。 第二行起 mm 行,每行三个非负整数 u_i, v_i, w_iui​,vi​,wi​,表示从 u_iui​ 到 v_ivi​ 有一条权值为 w_iwi​ 的有向边。

输出格式

输出一行 nn 个空格分隔的非负整数,表示 ss 到每个点的距离。

输入输出样例

输入

4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4

输出

0 2 4 3

和上面的题目同一个意思,但是这题的数据卡掉了SPFA,使用队列优化的DjkStra可以过。

#include<bits/stdc++.h>
using namespace std;

struct Edge{
	int to,w,next;
};

int cnt,head[100010],dist[100010];
Edge edge[200010];
bool used[100010];

void add(int from,int to,int weight){
	edge[++cnt].to=to;
	edge[cnt].w=weight;
	edge[cnt].next=head[from];
	head[from]=cnt;
}

typedef pair<int,int> Dist; //<dist,i>

void Djkstra(int S,int n){
	memset(dist,0x3f,sizeof(dist));
	dist[S]=0;
	priority_queue<Dist,vector<Dist>,greater<Dist> > DistQue;//构造小根堆 
	DistQue.push(Dist{0,S});
	Dist Top;
	int distance,i,to,w;
	while(!DistQue.empty()){
		Top=DistQue.top();//取出一个拥有最小dist值的人 用他进行更新 并标记为已使用 
		DistQue.pop();
		distance=Top.first;
		S=Top.second;
		
		if(used[S])continue;//used为真 说明已经用于更新了 所以队列中存储的这个dist值在没有负环的情况下一定不会小于上次用于更新的值 
		
		used[S]=true;//标记为使用 
		
		for(i=head[S];i!=0;i=edge[i].next){
			to=edge[i].to;w=edge[i].w;
			if(dist[to]>distance+w){
				dist[to]=distance+w;
				DistQue.push(Dist{dist[to],to});
			}
		}
	}
}

int main(){
	int n,m,s,u,v,w,i;
	cin>>n>>m>>s;
	for(i=1;i<=m;++i){
		cin>>u>>v>>w;
		add(u,v,w);
	}
	Djkstra(s,n);
	for(i=1;i<=n;++i){
		cout<<dist[i]<<" ";
	}
	return 0;
} 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]提供了使用Python的networkx库绘制网络图和计算最短加权路径的示例代码。该代码使用了一个包含顶点和边的列表,并使用add_nodes_from和add_weighted_edges_from方法将它们添加到图中。然后,使用nx.shortest_path_length方法计算了从顶点v1到顶点v11的最短加权路径长度为13。\[1\] 引用\[2\]提供了一个计算最短路径的Python程序示例。该程序使用了numpy和networkx库。首先,定义了一个包含顶点和边的列表,并使用add_nodes_from和add_weighted_edges_from方法将它们添加到图中。然后,使用nx.shortest_path_length方法计算了最短路径长度,并将结果存储在一个字典中。接下来,使用numpy创建了一个6x6的零矩阵,并使用两个嵌套的for循环将最短路径长度填充到矩阵中。最后,使用矩阵乘法计算了运力,并找到了最小运力和对应的位置。\[2\] 引用\[3\]提供了关于Dijkstra算法的一些背景信息。Dijkstra算法是一种寻找最短路径的算法,适用于所有权重大于等于0的情况。它可以用于解决从一个起始点到任意一个点的最短路问题。\[3\] 综上所述,如果你想在Python中计算图论中的最短路径,可以使用networkx库和Dijkstra算法。你可以根据引用\[1\]和引用\[2\]中的示例代码进行操作。 #### 引用[.reference_title] - *1* *3* [运筹学——图论与最短距离(Python实现)](https://blog.csdn.net/weixin_46039719/article/details/122521276)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [数学建模:图论模型 — 最短路模型示例 (Python 求解)](https://blog.csdn.net/qq_55851911/article/details/124776487)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值