最短路

基本算法:

  1. \(dijkstra\)
    使用条件:无负权边
    每次取出 还未取出过的 \(dis\) 最小的节点更新其他节点
    正确性证明:因为是\(dis\)最小的节点,别的节点不可能有一条路径走到这个节点且\(dis\)更小(路径为正)

stl-pq默认是大根堆,用负号处理为小根堆

int n, m, s, tot ;
int head[N], ver[M], edge[M], nxt[M] ;
int dis[N] ;
bool vis[N] ;

priority_queue <pair<int, int> > q ; 

void add(int x, int y, int z) {
	ver[++tot] = y ;  edge[tot] = z ;
	nxt[tot] = head[x] ; head[x] = tot ; 
}

void dijkstra(int s) {
	memset(dis, 0x3f, sizeof(dis)) ;
	dis[s] = 0 ; q.push(make_pair(0, s)) ; 
	while (!q.empty()) {
		int x = q.top().second ; q.pop() ;
		if (vis[x]) continue ;
		vis[x] = true ;
		for (int i = head[x]; i; i = nxt[i]) {
			int y = ver[i], val = edge[i] ;
			if (dis[y] > dis[x] + val) {
				dis[y] = dis[x] + val ;
				q.push(make_pair(-dis[y], y)) ;
			}
		}
	}
}

int main() {
	scanf("%d%d%d", &n, &m, &s) ;
	for (int i = 1; i <= m; i++) {
		int x, y, z ; scanf("%d%d%d", &x, &y, &z) ; 
		add(x, y, z) ;
	}
	dijkstra(s) ;
	for (int i = 1; i <= n; i++) printf("%d ", dis[i]) ;
}
  1. \(spfa\) 队列优化的\(bellman-ford\)

\(bellman-ford\):扫描每条边,如果\(dis[y]>dis[x]+edge\) 就更新,直到结束
spfa:每次扫描在队列里的节点
为了避免重复入队,设置\(v\)数组表示是否在队列中

int n, m, s, tot ;
int ver[M], nxt[M], edge[M], head[N] ;
int dis[N], v[N] ;

queue <int> q ; 

void add(int x, int y, int z) {
	ver[++tot] = y ; edge[tot] = z ;
	nxt[tot] = head[x] ; head[x] = tot ;
}

void spfa(int s) {
	memset(dis, 0x3f, sizeof(dis)) ;
	dis[s] = 0 ; q.push(s) ;
	while (!q.empty()) {
		int x = q.front() ; q.pop() ;
		v[x] = 0 ;
		for (int i = head[x]; i; i = nxt[i]) {
			int y = ver[i], val = edge[i] ;
			if (dis[y] > dis[x] + val) {
				dis[y] = dis[x] + val ;
				if (!v[y]) v[y] = 1, q.push(y) ;
			}
		}
	}
}

int main() {
	scanf("%d%d%d", &n, &m, &s) ;
	for (int i = 1; i <= m; i++) {
		int x, y, z ; scanf("%d%d%d", &x, &y, &z) ;
		add(x, y, z) ;
	} 
	spfa(s) ;
	for (int i = 1; i <= n; i++) printf("%d ", dis[i]) ;
}
  1. floyd
    处理多元最短路
    \(f[k][i][j]\)表示只走过若干个编号\(\leqslant k\)的节点 \(i\)\(j\)的最短路
    \(f[k][i][j]=\min(f[k-1][i][j],f[k-1][i][k]+f[k-1][k][j])\)
    去掉\(k\)这一维,但要把\(k\)放在最前面枚举,因为是阶段
int n, m ;
int d[N][N] ;

int main() {
	scanf("%d%d", &n, &m) ;
	memset(d, 0x3f, sizeof(d)) ;
	for (int i = 1; i <= n; i++) d[i][i] = 0 ;
	for (int i = 1; i <= m; i++) {
		int x, y, z ; scanf("%d%d%d", &x, &y, &z) ;
		d[x][y] = min(d[x][y], z) ;
		d[y][x] = min(d[y][x], z) ; 
	}
	for (int k = 1; k <= n; k++)
	for (int i = 1; i <= n; i++) 
	for (int j = 1; j <= n; j++)
	d[i][j] = min(d[i][j], d[i][k] + d[k][j]) ;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) printf("%d ", d[i][j]) ;
		puts("") ;
	}
}

一些例题

Telephone Lines

link

可以使\(k\)条边不被考虑的单源最短路问题

  1. 思路一:有后效性\(dp\)
    \(dp[x][t]\)表示到第\(x\)个节点,已经用掉\(t\)个机会的费用
    转移:
    使用机会:\(dp[x][t]->dp[y][t+1]\)
    不使用机会:\(dp[x][t]+edge->dp[y][t]\)
    因为有后效性(兜两圈),使用spfa解决
    时间复杂度\(O(NK)\)
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <cstring>
#include <queue>
using namespace std ;

typedef long long ll ;
const int N = 1010 ;
const int M = 20010 ;

queue <pair<int, int> > q ; 
int n, m, k, tot ;
int ver[M], edge[M], nxt[M], head[N] ;
int dp[N][N], v[N][N] ;

void add(int x, int y, int z) {
	ver[++tot] = y ; edge[tot] = z ;
	nxt[tot] = head[x] ; head[x] = tot ;
}

void spfa() {
	memset(dp, 0x3f, sizeof(dp)) ;
	for (int i = 0; i <= k; i++) dp[1][i] = 0 ;
	q.push(make_pair(1, 0)) ; // vertex + used
	while (!q.empty()) {
		int x = q.front().first, u = q.front().second ;
		q.pop() ;
		v[x][u] = 0 ;
		for (int i = head[x]; i; i = nxt[i]) {
			int y = ver[i], val = edge[i] ;
			if (dp[y][u] > max(val, dp[x][u])) {
				dp[y][u] = max(val, dp[x][u]) ;
				if (!v[y][u]) v[y][u] = 1, q.push(make_pair(y, u)) ;
			}
			if (u != k && dp[y][u + 1] > dp[x][u]) {
				dp[y][u + 1] = dp[x][u] ;
				if (!v[y][u + 1]) v[y][u + 1] = 1, q.push(make_pair(y, u + 1)) ;
			}
		}
	}
}

int main() {
	scanf("%d%d%d", &n, &m, &k) ;
	for (int i = 1; i <= m; i++) {
		int x, y, z ; scanf("%d%d%d", &x, &y, &z) ;
		add(x, y, z) ; add(y, x, z) ;
	}
	spfa() ; // 1->n
	int ans = 0x3f3f3f3f ;
	for (int i = 0; i <= k; i++) ans = min(ans, dp[n][i]) ;
	if (ans == 0x3f3f3f3f) printf("-1\n") ;
	else printf("%d\n", ans) ;
	
}

  1. 思路2:二分答案
    发现答案是单调的(用券不会使答案增加),使用二分法
    对于\(mid\),大于\(mid\)的边为\(1\),小于等于的为\(0\)
    判定\(1\)\(n\)的距离是否小于\(k\)即可

时间复杂度\(O(nlogMAXD)\)

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <cstring>
#include <queue> 
using namespace std ;

typedef long long ll ;
const int N = 1010 ;
const int M = 20010 ;

queue <int> q ; 
int n, m, k, tot ;
int ver[M], edge[M], edge_p[M], nxt[M], head[N] ;
int dis[N], vis[N] ;

void add(int x, int y, int z) {
	ver[++tot] = y ; edge_p[tot] = z ;
	nxt[tot] = head[x] ; head[x] = tot ;
}

void modify(int x) {
	for (int i = 1; i <= tot; i++) edge[i] = (edge_p[i] > x) ; 
}

void spfa() {
	memset(dis, 0x3f, sizeof(dis)) ;
	dis[1] = 0 ; q.push(1) ;
	while (!q.empty()) {
		int x = q.front() ; q.pop() ;
		vis[x] = 0 ;
		for (int i = head[x]; i; i = nxt[i]) {
			int y = ver[i], val = edge[i] ;
			if (dis[y] > dis[x] + val) {
				dis[y] = dis[x] + val ;
				if (!vis[y]) vis[y] = 1, q.push(y) ;
			}
		}
	}
}

int main() {
	scanf("%d%d%d", &n, &m, &k) ;
	int l = 0, r = 0 ;
	for (int i = 1; i <= m; i++) {
		int x, y, z ; scanf("%d%d%d", &x, &y, &z) ;
		add(x, y, z) ; add(y, x, z) ;
		r = max(r, z) ;
	}
	spfa() ;
	if (dis[n] == 0x3f3f3f3f) {
		puts("-1") ;
		return 0 ;
	}
	while (l < r) {
		int mid = (l + r) >> 1 ;
		modify(mid) ;
		spfa() ;
		if (dis[n] <= k) {
			r = mid ;
		} else {
			l = mid + 1 ;
		}
	}
	printf("%d\n", r) ;
}

最优贸易(反图)

link

这个题做法还是挺有意思的
从1到n上的路,因为只能交易一次,所以找一个最大一个最小相减是差价
直接遍历一次是不可以的,因为可能选到的最大值在最小值之前出现
于是考虑两遍,正+反
正序处理从\(1\)\(i\)的路径上的最小
反序处理从\(n\)\(i\)的路径上的最大,在反图上跑(把有向边翻转)

答案就是两者之差,这样可以保证不会出现最大值在最小值之前的情况

有个疑惑:点权的非dag能用dijkstra吗?因为是min可能有后效性,但实测ac
实证:确实是不可以的,但数据太水水过了 还是用spfa比较好

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <cstring>
using namespace std ;

typedef long long ll ;
const int N = 1000010 ;

int n, m ;
int dis1[N], dis2[N], a[N] ;
bool vis[N] ;
vector <int> g[N], h[N] ;
priority_queue <pair<int, int> > q ;

void dijkstra2() {
	memset(dis1, 0x3f, sizeof(dis1)) ;
	memset(vis, false, sizeof(vis)) ;
	dis1[1] = a[1] ; q.push(make_pair(-a[1], 1)) ;
	while (!q.empty()) {
		int x = q.top().second ; q.pop() ;
		vis[x] = true ;
		for (int i = 0; i < g[x].size(); i++) {
			int y = g[x][i] ;
			if (vis[y]) continue ;
			if (dis1[y] > min(dis1[x], a[y])) {
				dis1[y] = min(dis1[x], a[y]) ;
				q.push(make_pair(-dis1[y], y)) ;
			}
		}
	}
}

void dijkstra1() {
	memset(vis, false, sizeof(vis)) ;
	dis2[n] = a[n] ; q.push(make_pair(a[n], n)) ;
	while (!q.empty()) {
		int x = q.top().second ; q.pop() ;
		vis[x] = true ;
		for (int i = 0; i < h[x].size(); i++) {
			int y = h[x][i] ;
			if (vis[y]) continue ;
			if (dis2[y] < max(dis2[x], a[y])) {
				dis2[y] = max(dis2[x], a[y]) ;
				q.push(make_pair(dis2[y], y)) ;
			}
		}
	}
}


int main() {
	scanf("%d%d", &n, &m) ;
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]) ;
	for (int i = 1; i <= m; i++) {
		int x, y, z ; scanf("%d%d%d", &x, &y, &z) ;
		if (z == 1) g[x].push_back(y), h[y].push_back(x) ;
		else {
			g[x].push_back(y), g[y].push_back(x) ;
			h[x].push_back(y), h[y].push_back(x) ;
		} 
	}
	dijkstra1() ;
	dijkstra2() ;
	int ans = 0 ;
	for (int i = 1; i <= n; i++) ans = max(ans, dis2[i] - dis1[i]) ;
//	for (int i = 1; i <= n; i++) cout << dis1[i] << " " << dis2[i] << endl; 
	printf("%d\n", ans) ;
}

道路和航线 (分层图+拓扑排序+dij)

link

题目描述:道路双向(边权正),航线单向 (边权可负),不成环
不能直接\(dijkstra\)(有负边),也不能\(spfa\)(被卡)

这个题的性质挺特殊
对于双向道路,合并成一个连通块
单向的负边权连接这些连通块
联通块内部边权为正,可以dij
连通块之间是单向道路,是个\(dag\),可以拓扑排序
最终思路:

  1. 形成连通块\((dfs)\)
  2. \(s\)在的连通块和所有入度为\(0\)的连通块加入
  3. 按照拓扑排序的顺序遍历连通块,确保前面的连通块dij完成后再进行后面连通块的处理
  4. 连通块内部用dijkstra处理最短路
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <cstring>
#include <queue>
using namespace std ;

typedef long long ll ;
const int N = 25010 ;
const int inf = 2140000000 ;

int n, m1, m2, s, tot, max_de ;
int c[N], dis[N], vis[N], deg[N] ;
vector <pair<int, int> > rd[N], air[N] ;
vector <int> grp[N] ;
priority_queue <pair<int, int> > q ; 
queue <int> que ;

void dfs(int x) {
	c[x] = tot ; grp[tot].push_back(x) ;
	vis[x] = 1 ;
	for (int i = 0; i < rd[x].size(); i++) {
		int y = rd[x][i].first ;
		if (!vis[y]) dfs(y) ;
	}
}

void dijkstra() {
	for (int i = 1; i <= n; i++) dis[i] = inf ;
	memset(vis, false, sizeof(vis)) ;
	dis[s] = 0 ;
	for (int i = 1; i <= tot; i++) if (!deg[i] or i == c[s]) que.push(i) ;
 	while (!que.empty()) {
 		int u = que.front() ; que.pop() ;
 		for (int i = 0; i < grp[u].size(); i++) 
		q.push(make_pair(-dis[grp[u][i]], grp[u][i])) ;
 		while (!q.empty()) {
	 		int x = q.top().second ; q.pop() ;
 			if (vis[x]) continue ;
 			vis[x] = true ;
 			for (int i = 0; i < rd[x].size(); i++) {
 				int y = rd[x][i].first, val = rd[x][i].second ;
				if (dis[y] > dis[x] + val) {
					dis[y] = dis[x] + val ;
					q.push(make_pair(-dis[y], y)) ;
			} 
			}
			for (int i = 0; i < air[x].size(); i++) {
				int y = air[x][i].first, val = air[x][i].second ;
				if (dis[y] > dis[x] + val) dis[y] = dis[x] + val ;
				deg[c[y]]-- ;
				if (deg[c[y]] == 0) que.push(c[y]) ;
			}
		}
	 }
}

int main() {
	// 道路双向(正),航线单向 (可负),不成环 
	scanf("%d%d%d%d", &n, &m1, &m2, &s) ; 
	for (int i = 1; i <= m1; i++) {
		int x, y, z ; scanf("%d%d%d", &x, &y, &z) ;
		rd[x].push_back(make_pair(y, z)) ;
		rd[y].push_back(make_pair(x, z)) ; 
	}
	for (int i = 1; i <= m2; i++) {
		int x, y, z ; scanf("%d%d%d", &x, &y, &z) ;
		air[x].push_back(make_pair(y, z)) ;
		max_de += min(z, 0) ;
	}
	for (int i = 1; i <= n; i++) 
	if (!vis[i]) {
		tot++ ;
		dfs(i) ;
	}
	for (int i = 1; i <= n; i++) 
	for (int j = 0; j < air[i].size(); j++) {
		deg[c[air[i][j].first]]++ ;
	}
	dijkstra() ;
	for (int i = 1; i <= n; i++) {
		if (dis[i] >= inf + max_de) puts("NO PATH") ;
		else printf("%d\n", dis[i]) ;
	}
}

Sightseeing Tree(无向图最小环问题)

link

无向图最小环问题使用\(floyd\)解决
\(i-------j--k--i\) 其中 \(i\)\(j\)之间可能比较长,但\(k\)\(i\)\(j\)一定是相邻的,确保是环而不是走了两遍一条边

使用\(floyd\)\(i\)\(j\)的最短路径

需要输出路径,根据floyd更新的规则,记录中转节点\(k\),结合两边进行递归,最后拼接\((A+k+B)\)即可

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std ;

typedef long long ll ;
const int N = 110 ;
const int inf = 0x3f3f3f3f ;

int n, m ;
int pos[N][N], d[N][N], a[N][N] ;
vector <int> path ; 
ll ans = inf  ;

void get_path(int x, int y) {
	if (pos[x][y] == 0) return ;
	get_path(x, pos[x][y]) ;
	path.push_back(pos[x][y]) ;
	get_path(pos[x][y], y) ; 
} 

int main() {
	scanf("%d%d", &n, &m) ;
	memset(d, 0x3f, sizeof(d)) ;
	memset(a, 0x3f, sizeof(a)) ;
	for (int i = 1; i <= m; i++) {
		int x, y, z ; scanf("%d%d%d", &x, &y, &z) ;
		a[x][y] = a[y][x] = min(a[y][x], z) ;
		d[x][y] = d[y][x] = min(d[y][x], z) ;
	} 
	for (int k = 1; k <= n; k++) {
		for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
		if (i != j && j != k && i != k && (ll) d[i][j] + a[j][k] + a[k][i] < ans) {
			ans = d[i][j] + a[j][k] + a[k][i] ;
			path.clear() ;
			path.push_back(i) ;
			get_path(i, j) ;
			path.push_back(j) ;
			path.push_back(k) ;
		}
		for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
		if (d[i][j] > d[i][k] + d[k][j]) {
			d[i][j] = d[i][k] + d[k][j] ;
			pos[i][j] = k ;
		} 
	}
	if (ans == inf) puts("No solution.") ;
	else
	for (int i = 0; i < (int)path.size(); i++) printf("%d ", path[i]) ;
	
}


值得一提的是有向图最小环问题
枚举启点\(s\),用\(dijkstra\)\(s\)到其他节点最短路
结束后把\(s\)\(dis\)变成\(\inf\),再跑一遍最短路
求到的dis[s]就是这个包含这个点的最小环(因为有向图不会出现1条路径走过去又走回来的情况)

时间复杂度比\(O(N^3)\)\(floyd\)

posted @ 2023-09-05 13:37  哈奇莱特  阅读(20)  评论(0)    收藏  举报