最短路专题(不完整)

参考文章

一、单源最短路

1. Dijkstra(堆优化)

DJ 的思想是每次用一个点更新与其相邻的所有点,更新后将该点标记(每个点只用一次),从用过的点连通分量出发,找一条连通分量与外界的连边,用该边的终点继续更新与其相连点到起点的距离。 O(n^2)
因为复杂度主要由边决定,适合稀疏图
不能处理负边权
利用堆优化,取出最短的一条O(nlogn),用其终点继续向外更新。

自定义比较操作符

模板

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring> 
using namespace std;
typedef pair<int,int> PII; // first 存距离   second 存编号 
const int maxn = 2e5+10;
int n, m;
int cnt = 0;
int dist[maxn];
bool st[maxn]; // 第 i 个点的最短路是否确定,是否需要更新 
int head[maxn];
struct Edge {
	int to, next, w;
}e[maxn];

void add(int u, int v, int w) {
	e[++cnt].next = head[u];
	e[cnt].to = v;
	e[cnt].w = w;
	head[u] = cnt;
}

void init() {
	memset(dist, 0x3f3f3f3f, sizeof(dist)); // 将所有距离初始化为正无穷 
	memset(head, 0, sizeof(head));
	memset(e, 0, sizeof(e));
	cnt = 0;
}

int dijkstra() {
	dist[1] = 0; // 第一个点到起点的距离
	
	priority_queue<PII, vector<PII>, greater<PII>> heap; // 小根堆 
	heap.push({0,1});
	
	while(heap.size()) { // 堆不空 
		PII t = heap.top();
		heap.pop();
		
		int ver = t.second, dis = t.first;
		if(st[ver]) continue; // 重边(访问过的)就不用再更新了,DJ思想就是贪心的访问每条最短边
		st[ver] =  true; // 标记 t 已经确定为最短路
		
		for(int i = head[ver]; i; i = e[i].next) {
			int to = e[i].to;
			if(dist[to] > dis + e[i].w) {
				dist[to] = dis + e[i].w;
				heap.push({dist[to], to});
			}
		} 
	} 
} 

int main() {
//	freopen("test.in", "r", stdin);
	while(~scanf("%d%d", &n, &m)) {
		init();
		for(int i = 1; i <= m; i++) {
			int u, v, w;
			scanf("%d%d%d", &u, &v, &w);
			add(u, v, w);
		}
		dijkstra();
		int to; // 目标点 
		scanf("%d", &to);
		if(dist[to] != 0x3f3f3f3f) {
			printf("%d\n", dist[n]); // 到达目标的最短距离。 DJ计算一个起点,多个终点 
		}
		else printf("无法到达!\n");
	}
	return 0;
}

例题

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

RE了好几次,总结一下RE原因

  1. 数组越界
  2. 站溢出
  3. 除0
  4. 函数格式(void int 是否有返回值)
    AC代码
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring> 
// RE:数组越界,站溢出,除0 , 函数格式(void int 是否有返回值) 
using namespace std;
typedef pair<int,int> PII; // first 存距离   second 存编号 
const int maxn = 5e5+10;
int n, m;
int cnt = 0;
int dist[10005];
bool st[maxn]; // 第 i 个点的最短路是否确定,是否需要更新 
int head[maxn];
struct Edge {
	int to, next, w;
}e[maxn];

void add(int u, int v, int w) {
	e[++cnt].next = head[u];
	e[cnt].to = v;
	e[cnt].w = w;
	head[u] = cnt;
}

void init() {
	for(int i = 0; i <= n; i++) dist[i] = (1<<31)-1; // 将所有距离初始化为正无穷 
	memset(head, 0, sizeof(head));
	memset(e, 0, sizeof(e));
	cnt = 0;
}

void dijkstra(int s) {
	 
	dist[s] = 0; // 第一个点到起点的距离
	priority_queue<PII, vector<PII>, greater<PII>> heap; // 小根堆 
	heap.push({0,s});
	
	while(heap.size()) { // 堆不空 
		PII t = heap.top();
		heap.pop();
		
		int ver = t.second, dis = t.first;
		if(st[ver]) continue; // 重边(访问过的)就不用再更新了,DJ思想就是贪心的访问每条最短边
		st[ver] =  true; // 标记 t 已经确定为最短路
		
		for(int i = head[ver]; i; i = e[i].next) {
			int to = e[i].to;
			if(dist[to] > dis + e[i].w) {
				dist[to] = dis + e[i].w;
				heap.push({dist[to], to});
			}
		} 
	} 
} 

int main() {
//	freopen("test.in", "r", stdin);
	int s;
	scanf("%d%d%d", &n, &m, &s);
	init();
	for(int i = 1; i <= m; i++) {
		int u, v, w;
		scanf("%d%d%d", &u, &v, &w);
		add(u, v, w);
	}
	dijkstra(s);
	for(int i = 1; i <= n; i++) {
		printf("%d", dist[i]); // 到达目标的最短距离。 DJ计算一个起点,多个终点 
		if(i == n) 	break;
		else printf(" ");
	}

	return 0;
}

Dijkstra 线段树优化 // 手抄了一遍还是没懂,等我强了再来

留坑

2. SPFA

先说一下Bellman-Ford 算法:
每次取一条边加入连通块,然后从该边的终点作为中转点更新整个图的最短路,最坏情况就是每次加入一条边都能更新整个图的最短路, 即 V*E
复杂度 O(VE)

SPFA(优化Bellman-Ford)是把一个点周围所有点放进队列来更新最短值,优化就在有导向性,只用更新与松弛成功的点相连的点

#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e5+10;
int head[maxn], cnt = 0, vis[maxn];
int dis[maxn];

struct Edge{
	int to, w, next;
}e[maxn];

void add(int u, int v, int w) {
	e[++cnt].next = head[u];
	e[cnt].to = v;
	e[cnt].w = w;
	head[u] = cnt;
}

void spfa(int s)  {
	memset(dis, 0x3f3f3f3f, sizeof(dis));
	dis[s] = 0; vis[s] = 1; 
	queue<int> Q;
	Q.push(s);
	int u, v;
	while(!Q.empty()) {
		u = Q.front(); Q.pop(); vis[u] = 0;
		for(int i = head[u]; i; i = e[i].next) {
			v = e[i].to;
			if(dis[u] + e[i].w < dis[v]) {
				dis[v] = dis[u] + e[i].w;
				if(!vis[v]) Q.push(v), vis[v] = 1; // 不在队列里就放进去更新别的点 
			}
		}
	}
}

void init() {
	memset(head, 0, sizeof(head));
	memset(vis, 0, sizeof(vis));
	memset(e, 0, sizeof(e));
	cnt = 0;
}

int main() {
//	freopen("test.in", "r", stdin);
	int n, m, s;
	while(scanf("%d%d%d", &n, &m, &s) == 3) {
		init();
		int u, v, w;
		for(int i = 1; i <= m; i++) scanf("%d%d%d", &u, &v, &w), add(u, v, w); // 有向图连一次边就好
		spfa(s);
		for(int i = 1; i <= n; i++) printf("%d ", dis[i]);
	}
	return 0;
}

二、全源最短路

Floyd

#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5+10;

int dis[maxn][maxn];

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

void init() {
	memset(dis, 0x3f, sizeof(dis));
}

int main() {
	freopen("test.in", "r", stdin);
	int n, m, s;
	while(~scanf("%d%d%d", &n, &m, &s)){
		init();
		int u, v, w;
		for(int i = 1; i <= m; i++) scanf("%d%d%d", &u, &v, &w), dis[u][v] = w;
		Floyd(n);
		for(int i = 1; i <= n; i++) printf("%d ", dis[s][i]);
	}
	return 0;
}

三、k短路

留坑

四、判环(SPFA)

用 spfa 判断正负环都可以利用某个点被松弛 n 次以上作为依据

判正环

判断正环的例题
E - Currency Exchange POJ - 1860

#include<iostream>
#include<cstdio>
#include<queue>
#include<vector> 
using namespace std;
int N, M, S;
double V;
struct edge {
	int to;
	double R, C;
};
vector<edge> G[105];

int vis[105], cnt[105];

double d[105];

bool spfa() {
	queue<int> Q;
	Q.push(S);
	vis[S] = 1;
	d[S] = V;
	while(Q.size()) {
		int u = Q.front(); Q.pop();
		vis[u] = 0; // 出队
		for(int i = 0; i < G[u].size(); i++) {
			edge e = G[u][i];
			double val = (d[u] - e.C) * e.R;
			if(val - d[e.to] > 1e-8) {
				d[e.to] = val;
				if(vis[e.to]) continue;
				Q.push(e.to);	// 每次更新完一个点,要把它周围的点加入队列继续往外更新其他点 
				vis[e.to] = 1;
				if(++cnt[e.to] > N) return 1;// 如果一个点入队 N 次以上,说明出现环了 
			}
		}
	}
	return 0;
} 

int main() {
//	freopen("test.in", "r", stdin);
	ios::sync_with_stdio(false); // 打消iostream中输入输出缓存,节省时间。
	cin.tie(0); cout.tie(0); // 可以通过tie(0)(0表示NULL)来解除cin与cout的绑定,进一步加快执行效率。
	cin >> N >> M >> S >> V;
	int a, b;
	double Rab, Cab, Rba, Cba;
	for(int i = 0; i < 105; i++) G[i].clear();
	for(int i = 0; i < M; i++) {
		cin >> a >> b >> Rab >> Cab >> Rba >> Cba;
		edge e;
		e.to = b, e.R = Rab, e.C = Cab;
		G[a].push_back(e);
		e.to = a, e.R = Rba, e.C = Cba;
		G[b].push_back(e);
	} 
	if(spfa()) { // 正环使钱变多 
		cout << "YES" << endl;
	}
	else cout << "NO" << endl;
	return 0;
}

判负环

利用 SPFA 由于每个点记录的是从起点出发到达当前点的最短路,只有当一个点松弛成功它才能作为一个中转点去松弛其他点,所以如果一个点如果被松弛 n 次,那么存在负环,否则一个点至多被松弛 n-1 次

负环例题
F - Wormholes POJ - 3259

只要出现一个负环,则沿这个负环不停走,一定能使时间不断前走,走一定次数以后一定能满足提前返回出发点

#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<vector>
const int inf = 1e9+7;
using namespace std;
const int maxn = 3e3+10;
int N, M, K;
int num[maxn]; // 记录每个点出现的次数 
int dis[600];

bool vis[600];
struct node {
	int to, w;
};

vector<node> G[maxn];

void init() {
	memset(vis, 0, sizeof(vis));
	memset(num, 0, sizeof(num)); 
	memset(dis, inf, sizeof(dis));
}

bool spfa(int s) {
	queue<int> Q;
	Q.push(s); vis[s] = 1;
	dis[s] = 0; num[s] = 1;
	
	while(!Q.empty()) {
		int u = Q.front(); Q.pop(); vis[u] = 0;
		for(int i = 0; i < G[u].size(); i++) {
			node e = G[u][i];
			if(dis[u] < inf && dis[e.to] > (dis[u] + e.w)) {
				dis[e.to] = dis[u] + e.w; // 在队列中只是不重复添加,但是需要更新值 
				if(vis[e.to]) continue;
				Q.push(e.to); vis[e.to] = 1;
				if(++num[e.to] > N) return 1; // 出现负环 
			}
		}
	}
	return 0; 
}

int main() {
//	freopen("test.in", "r", stdin);
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	int F; cin >> F;
	
	while(F--) {
		cin >> N >> M >> K;
		init();
		for(int i = 1; i <= N; i++) G[i].clear(); 
		int u, v, t;
		node e;
		for(int i = 1; i <= M; i++) {
			cin >> u >> v >> t;
			e.to = v; e.w = t;
			G[u].push_back(e);
			e.to = u; e.w = t;
			G[v].push_back(e);
		}
		for(int i = 1; i <= K; i++) {
			cin >> u >> v >> t;
			e.to = v; e.w = -t;
			G[u].push_back(e);
		}
		if(spfa(1)) cout << "YES" << endl;
		else cout << "NO" << endl;
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值