单源有负权无负环求最短路径 SPFA 链式前向星实现

17 篇文章 0 订阅

SPFA两个作用

  • 求最短路径
  • 判断是否存在负环

原理:从起点开始加入队列,队首更新与其相邻的点,如果可以松弛就松弛,松弛之后如果这个点不在队列中,加入队列 如果一个点入队次数超过n,存在负环

求最短路

模板如下

#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
using namespace std;
const int N=1e5+10,M=5e5+10,INF=0x3f3f3f3f;
int n,m,s;//n个节点,m条边,s为源点 
bool vis[N];//判断是否在队列中,防止一个队列出现重复点 
int dis[N];//点i到源点最短距离 
struct node {//链式前向星 
	int to,next,w;
}edge[M];
int cnt,head[N];
void init() {
	cnt=0;
	memset(head,-1,sizeof(head));
	for(int i=1; i<=n; i++) {
		vis[i]=0;
		dis[i]=INF;
	}
}
void addEdge(int from,int to,int w) { // 起点,终点,边长 
	edge[cnt].to=to;   // 该边的终点
	edge[cnt].w=w;    // 权值
	edge[cnt].next=head[from]; //  (指向head[from]后,head[from]又指向了自己)
	head[from]=cnt++;  // 以from结点为起点的边在edge数组中存储的下标
}
void SPFA(int s) {//s为源点 
	queue<int>Q;
	Q.push(s);// 源点进队
	vis[s]=1;
	dis[s]=0;
	while(!Q.empty()) {
		int start=Q.front();
		Q.pop();
		vis[start]=0;  // 在SPFA中这儿需要改为0,因为每个节点需要重复进队
		for(int i=head[start]; ~i; i=edge[i].next) { //取出start结点在edge中的起始存储下标(当i=0,即edge[i].next为0,说明以start节点为起始点的边全部访问完)
			int w=edge[i].w;
			int end=edge[i].to;
			if(dis[end]>dis[start]+w) { // 松弛操作
				dis[end]=dis[start]+w;
				if(!vis[end]) { // 末端点不在队列 
					Q.push(end); //加入队列 
					vis[end]=1; 
				}
			}
		}
	}
}

模板题如下

https://www.acwing.com/problem/content/description/853/

#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
using namespace std;
const int N=1e5+10,M=5e5+10,INF=0x3f3f3f3f;
int n,m,s;//n个节点,m条边,s为源点 
bool vis[N];//判断是否在队列中,防止一个队列出现重复点 
int dis[N];//点i到源点最短距离 
struct node {//链式前向星 
	int to,next,w;
}edge[M];
int cnt,head[N];
void init() {
	cnt=0;
	memset(head,-1,sizeof(head));
	for(int i=1; i<=n; i++) {
		vis[i]=0;
		dis[i]=INF;
	}
}
void addEdge(int from,int to,int w) { // 起点,终点,边长 
	edge[cnt].to=to;   // 该边的终点
	edge[cnt].w=w;    // 权值
	edge[cnt].next=head[from]; //  (指向head[from]后,head[from]又指向了自己)
	head[from]=cnt++;  // 以from结点为起点的边在edge数组中存储的下标
}
void SPFA(int s) {//s为源点 
	queue<int>Q;
	Q.push(s);// 源点进队
	vis[s]=1;
	dis[s]=0;
	while(!Q.empty()) {
		int start=Q.front();
		Q.pop();
		vis[start]=0;  // 在SPFA中这儿需要改为0,因为每个节点需要重复进队
		for(int i=head[start]; ~i; i=edge[i].next) { //取出start结点在edge中的起始存储下标(当i=0,即edge[i].next为0,说明以start节点为起始点的边全部访问完)
			int w=edge[i].w;
			int end=edge[i].to;
			if(dis[end]>dis[start]+w) { // 松弛操作
				dis[end]=dis[start]+w;
				if(!vis[end]) { // 末端点不在队列 
					Q.push(end); //加入队列 
					vis[end]=1; 
				}
			}
		}
	}
}
int main() {
	while(scanf(" %d%d",&n,&m)!=EOF) {
		init();
		for(int i=1; i<=m; ++i) { // 输入m条边
			int from,to,w; // 起点 终点 权值
			scanf(" %d%d%d",&from,&to,&w);
			addEdge(from,to,w);  //若是无向图,反过来再加一次
		}
		SPFA(1);
		if(dis[n]!=INF)
			printf("%d\n",dis[n]);
		else
			printf("impossible\n");
	}
	return 0;
}

判断负环

很简单,开个cir数组记录加入队列的次数,spfa变成bool型函数。

涉及push的时候cir[i]++,cir[i]>=n就return 1,说明存在负环

模板

#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
using namespace std;
const int N=1e5+10,M=5e5+10,INF=0x3f3f3f3f;
int n,m,s;//n个节点,m条边,s为源点
bool vis[N];//判断是否在队列中,防止一个队列出现重复点
int dis[N];//点i到源点最短距离
struct node {//链式前向星
	int to,next,w;
} edge[M];
int cnt,head[N];
int cir[N];//如果cir[i]>=n,存在负环
void init() {
	cnt=0;
	memset(head,-1,sizeof(head));
	for(int i=1; i<=n; i++) {
		vis[i]=0;
		dis[i]=INF;
		cir[i]=0;
	}
}
void addEdge(int from,int to,int w) { // 起点,终点,边长
	edge[cnt].to=to;   // 该边的终点
	edge[cnt].w=w;    // 权值
	edge[cnt].next=head[from]; //  (指向head[from]后,head[from]又指向了自己)
	head[from]=cnt++;  // 以from结点为起点的边在edge数组中存储的下标
}
bool SPFA(int s) {//s为源点
	queue<int>Q;
	Q.push(s);// 源点进队
	cir[s]++;
	vis[s]=1;
	dis[s]=0;
	while(!Q.empty()) {
		int start=Q.front();
		Q.pop();
		vis[start]=0;  // 在SPFA中这儿需要改为0,因为每个节点需要重复进队
		for(int i=head[start]; ~i; i=edge[i].next) { //取出start结点在edge中的起始存储下标(当i=0,即edge[i].next为0,说明以start节点为起始点的边全部访问完)
			int w=edge[i].w;
			int end=edge[i].to;
			if(dis[end]>dis[start]+w) { // 松弛操作
				dis[end]=dis[start]+w;
				if(!vis[end]) { // 末端点不在队列
					Q.push(end); //加入队列
					vis[end]=1;
					cir[end]++;
					if(cir[end]>=n)	return true;//存在负环
				}
			}
		}
	}
	return false;
}

和上面那个就几行不一样

例题

http://poj.org/problem?id=3259

#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
using namespace std;
const int N=1e5+10,M=5e5+10,INF=0x3f3f3f3f;
int n,m,s;//n个节点,m条边,s为源点
bool vis[N];//判断是否在队列中,防止一个队列出现重复点
int dis[N];//点i到源点最短距离
struct node {//链式前向星
	int to,next,w;
} edge[M];
int cnt,head[N];
int cir[N];//如果cir[i]>=n,存在负环
void init() {
	cnt=0;
	memset(head,-1,sizeof(head));
	for(int i=1; i<=n; i++) {
		vis[i]=0;
		dis[i]=INF;
		cir[i]=0;
	}
}
void addEdge(int from,int to,int w) { // 起点,终点,边长
	edge[cnt].to=to;   // 该边的终点
	edge[cnt].w=w;    // 权值
	edge[cnt].next=head[from]; //  (指向head[from]后,head[from]又指向了自己)
	head[from]=cnt++;  // 以from结点为起点的边在edge数组中存储的下标
}
bool SPFA(int s) {//s为源点
	queue<int>Q;
	Q.push(s);// 源点进队
	cir[s]++;
	vis[s]=1;
	dis[s]=0;
	while(!Q.empty()) {
		int start=Q.front();
		Q.pop();
		vis[start]=0;  // 在SPFA中这儿需要改为0,因为每个节点需要重复进队
		for(int i=head[start]; ~i; i=edge[i].next) { //取出start结点在edge中的起始存储下标(当i=0,即edge[i].next为0,说明以start节点为起始点的边全部访问完)
			int w=edge[i].w;
			int end=edge[i].to;
			if(dis[end]>dis[start]+w) { // 松弛操作
				dis[end]=dis[start]+w;
				if(!vis[end]) { // 末端点不在队列
					Q.push(end); //加入队列
					vis[end]=1;
					cir[end]++;
					if(cir[end]>=n)	return true;//存在负环
				}
			}
		}
	}
	return false;
}
int main() {
	int F;
	scanf("%d",&F);
	while(F--) {
		int q;
		scanf(" %d%d%d",&n,&m,&q);
		init();//初始化涉及参数n,所以必须放在输入n之后!!!!
		for(int i=1; i<=m; i++) {
			int from,to,w;
			scanf(" %d%d%d",&from,&to,&w);
			addEdge(from,to,w);
		addEdge(to,from,w);
		}
		for(int i=1; i<=q; i++) {
			int from,to,w;
			scanf(" %d%d%d",&from,&to,&w);
			w*=-1;
			addEdge(from,to,w);
		}
		bool t=SPFA(1);
		if(t)	printf("YES\n");
		else printf("NO\n");
	}
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值