最短路

1|0基本算法:


  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 队列优化的bellmanford

bellmanford:扫描每条边,如果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]表示只走过若干个编号k的节点 ij的最短路
    f[k][i][j]=min(f[k1][i][j],f[k1][i][k]+f[k1][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("") ;
}
}

2|0一些例题


2|1Telephone 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
    判定1n的距离是否小于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) ;
}

2|2最优贸易(反图)


link

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

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

有个疑惑:点权的非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) ;
}

2|3道路和航线 (分层图+拓扑排序+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]) ;
}
}

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


link

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

使用floydij的最短路径

需要输出路径,根据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,用dijkstras到其他节点最短路
结束后把sdis变成inf,再跑一遍最短路
求到的dis[s]就是这个包含这个点的最小环(因为有向图不会出现1条路径走过去又走回来的情况)

时间复杂度比O(N3)floyd


__EOF__

作  者哈奇莱特
出  处https://www.cnblogs.com/lighthqg/p/17679370.html
关于博主:这个人很懒 什么也没有留下
版权声明:未获得本人同意请勿随意转载
声援博主:制作不易 点个赞吧

posted @   哈奇莱特  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
0
0
关注
跳至底部
点击右上角即可分享
微信分享提示