第三章 搜索与图论(二)

常见的最短路模型

源点: 起点
汇点: 终点
单源最短路: 从1个点到其他所有点的最短路, 比如求1号点到第n号点的最短路
多源汇最短路 : 起点和终点不确定
最 短 路 { 单 源 最 短 路 { 所 有 边 权 重 为 正 数 { 朴 素 D i j k s t r a   O ( n 2 )   比 较 适 合 稠 密 图 , 边 数 m = n 2 比 较 多 堆 优 化 版 D i j k s t r a   O ( m l o g n )   稀 疏 图 , m = n ≤ 1 0 5 , n 2 会 超 时 , 用 堆 优 化 存 在 负 权 边 { B e l l m a n _ F o r d   O ( n m )   n 是 点 数 , m 是 边 数 ( 经 过 不 超 过 k 条 边 , 只 能 用 此 方 法 )     S P F A   一 般 O ( m ) , 最 坏 O ( n m )                         多 源 汇 最 短 路   F l o y d 算 法   O ( n 3 )                                                                               最短路\left\{\begin{matrix} 单源最短路\left\{\begin{matrix} 所有边权重为正数\left\{\begin{matrix} 朴素Dijkstra  O(n^2)  比较适合稠密图, 边数m = n ^2比较多 \\ 堆优化版Dijkstra  O(mlogn)   稀疏图, m = n \leq 10^5, n^2会超时,用堆优化 \end{matrix}\right. \\ 存在负权边\left\{\begin{matrix} Bellman\_Ford O(nm) n是点数,m是边数(经过不超过k条边,只能用此方法)   \\ SPFA  一般O(m), 最坏O(nm)             \end{matrix}\right. \end{matrix}\right. \\ 多源汇最短路 Floyd算法 O(n^3)                                        \end{matrix}\right. { Dijkstra O(n2) ,m=n2Dijkstra O(mlogn) ,m=n105,n2,{ Bellman_Ford O(nm) n,m(k,)  SPFA O(m),O(nm)             Floyd O(n3)                                       

在这里插入图片描述
最短路 比较难的 是建图

各个算法的原理简介

dijkstra 原理是 贪心
floyd 基于动态规划
bellman_ford 基于离散数学的知识

Dijkstra 算法

用来解决 单源最短路问题

朴素版Dijkstra

s集合 : 当前已经确定最短距离的点

1.dist[1] = 0, dist[i] = INF;
2.for (int i = 0; i < n; i ++ ){
    
     
// 每次循环出队一个元素,可以确定一个点的最短距离
// 循环n次, 可以确定n个点的最短距离
	t <- 找到不在s中的最短距离的点
	s <- t (t加入到s集合中)
	用t更新其他所有点的距离(用t的出边去更新其他所有点的距离, 
	比如t->x, 看下1~x的距离能否用 1~t + t~x的距离更新
	): dist[x] > dist[t] + w 的话, dist[x] = dist[t] + w;
}	

在这里插入图片描述

例子

绿颜色表示确定了最短路的点
红颜色表示距离待定的点
初始状态:
在这里插入图片描述
第1次 发现2号点和3号点到1的距离可以更新
在这里插入图片描述
第二次寻找不在s中的最短距离的点, 2号点确定为绿色
在这里插入图片描述
用2来更新出边到1的距离, 可以将3号点的距离更新成3, 此时这轮迭代结束
在这里插入图片描述
下轮迭代只剩一个点, 可以直接确定.
在这里插入图片描述

时间复杂度O(n^2)

外层循环n次, 内层循环找不在s中的距离最近的点n次, 用t更新其他点n次.
总共n^2

AcWing 849. Dijkstra求最短路 I

分析

裸题
正权自环, 显然不会出现在最短路中,忽略
重边, 直接对g[a][b] = min(g[a][b], c);
找不在s中距离最短的点

int dijkstra(){
    
    
	for (int i = 0; i < n; i ++ ){
    
    
		int t = -1;
		for (int j = 1; j <= n; j ++ )
			if (!st[j] && (t == - 1 || dist[t] > dist[j])) t = j;
			// !st[j] 不在s中
			// t == -1表示第一次, dist[t] > dist[j] 找最小
		...
		st[t] =  true; // 标记成绿色
	}
}

代码

#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 10010;
int g[N][N];
int n, m;
bool st[N];
int dist[N];

int dijkstra(){
    
    
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    //st[1] = true; 一定不能加这句话.
    
    for (int i = 0; i < n; i ++ ){
    
    
        // 找不在s中最短距离的点    
        int t = -1;
        for (int j = 1; j <= n; j ++ ){
    
    
            if (!st[j] && (t == -1 || dist[t] > dist[j])) t = j;  // 注意 !st[j] && ( || )
        }
        st[t] = true;
        
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}
int main(){
    
    
    cin >> n >> m;
    
    memset(g, 0x3f, sizeof g);
    
    while (m -- ){
    
    
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = min(g[a][b], c);
    }
    
    int t = dijkstra();
    cout << t << endl;
    
    return 0;
}

堆优化版dijkstra

如果是个稀疏图, n = 1 0 5 n = 10^5 n=105, n 2 n^2 n2时间必爆

优化

找不在s中的点, 外部for循环n次, 找点n次,总共n^2s = t, 每次取1个点, 外部循环n次, 总共n 用t更新其他点的距离, 外部循环n次, 总共m`条边, 因此是m.

所以第一步可以用堆来找最小值使得
每次找最小值O(1).

第二步总共仍然是n

而第三步在堆中寻找元素时间复杂度O(logn), 总共修改m次, 所以总共 m l o g n mlogn mlogn
因此堆优化dijkstra时间复杂为O(mlogn)
在这里插入图片描述

实现堆的方式

手写

麻烦.

STL

STL不支持修改堆中元素操作, 堆中可能存在冗余.
比如存了dist[1] = 10, 又存了dist[1] = 15;
直接用st数组判重即可, 如果当前出队的元素, 已经确定为绿色,直接扔掉

AcWing 850. Dijkstra求最短路 II

分析

堆优化dijkstra 裸题

代码

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
typedef pair<int, int> PII;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
int dist[N];
bool st[N];

void add(int a, int b, int c){
    
    
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

int dijkstra(){
    
    
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({
    
    0, 1});

    while (heap.size()){
    
    
        auto t = heap.top(); heap.pop();
        int ver = t.second, distance = t.first;
        if (st[ver]) continue;
        st[ver] = true;
        
        for (int i = h[ver]; ~i; i = ne[i]){
    
    
            int j = e[i];
            if (dist[j] > dist[ver] + w[i]){
    
    
                dist[j] = dist[ver] + w[i];
                heap.push({
    
    dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];

}

int main(){
    
    
    scanf("%d%d", &n, &m);
    
    memset(h, -1, sizeof h);

    while (m -- ){
    
    
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    cout << dijkstra() << endl;

    return 0;
}

Bellman-Ford 算法

for循环n一定要对原来的距离备份,防止串连
  for 所有边 a, b, w.    a ⟶ w b a\stackrel{w}{\longrightarrow}b awb (从a到b的边,权重为w)(边的存储方式不一定写成邻接表, 可以是结构体)
    dist[b] = min(dist[b], dist[a] + w);

在这里插入图片描述

串联问题(备份)

在这里插入图片描述
比如截图中右上角的问题
边权重1-2 权重1, 2-3 权重1, 1-3权重3, 求1到3 最多经过1条边的最短路.
应该是3.
但是如果不备份
点 1 2 3
dist 0 ∞ \infty ∞ \infty
for 循环第1次
 第二层循环(假如先枚举1-2这条边)
2号点距离更新成1
点 1 2 3
dist 0 1 ∞ \infty

然后再枚举2-3(这时2到1的距离已经发生变化),因此3-1的距离就变成dist[2] + w[2,3] = 1 + 1 = 2

点 1 2 3
dist 0 1 2

虽然最外层只迭代了1次, 但更新过程中发生了串联, 将已经更新点,又拿来作为条件,更新后面的点.

如何不发生串联呢

保证更新的时候只用最外层循环for上一次的结果, 即:在最外层循环中+备份
在这里插入图片描述
使用上一次备份更新, 那么3的距离变成 ∞ + 1 \infty + 1 +1,不会变成2

负权回路

图中存在负权回路的话,最短路不一定存在
在这里插入图片描述

可以求出图中是否存在负权回路

最外层循环次数的意义, 比如迭代k次, 表示经过不超过k条边的最短距离,
如果第n次迭代的时候,又更新了某些边, 说明:存在一条最短路, 这条最短路上的边数 >= n.
n条边的话,意味这这条路径上有n + 1个点, 因为总的点数只有n,因此这条路径上必定有两个点相同,即:这条路径上必定存在环.
路径上存在环, 并且可以更新当前点的距离,那么一定是负环.
因此,第n迭代的时候,有更新的话,说明存在边数为n的最短路径,说明存在负环
一般找负环spfa,这里值是提及下,bellman-ford可以用来找负环.

时间复杂度O(nm)

两重循环,外层n次,内层循环所有边数m

有负权回路有些情况可以求得最短路

比如2号点的负权回路,不在最短路径上. 但是, spfa算法要求图中任何点,不能包含负环
在这里插入图片描述

AcWing 853. 有边数限制的最短路

分析

题目说了边权可能为负数,因此一定不能用dijkstra
题目要求最多经过k条边, 有负环也无所谓了

代码

if (dist[n] > 0x3f3f3f3f / 2 ) return -1;

在这里插入图片描述
(1号点当前不能到5号点)
(1号点当前也不能到n号点)
但是5号点能更新1号点到5号点的距离
dist[n] <= dist[5] + w[5, n]
n号点,最终可能到不了,但是比正无穷小一点点,也算到不了

代码

#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 100010 * 2 + 10;
struct Edge{
    
    
    int a, b, w;
}edge[M];

int n, m, k;
int dist[N], backup[N];

int bellman_ford(){
    
    
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0; // 注意初始化
    
    for (int i = 0; i < k; i ++ ){
    
    
        memcpy(backup, dist, sizeof dist);
        for (int j = 0; j < m; j ++ ){
    
    
            int a = edge[j].a, b = edge[j].b, c = edge[j].w;
            if (dist[b] > backup[a] + c) // 一定要是 backup[a] + c
                dist[b] = backup[a] + c;
        }
    }
    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}
int main(){
    
    
    cin >> n >> m >> k;
    
    for (int i = 0; i < m; i ++ ){
    
    
        scanf("%d%d%d", &edge[i].a, &edge[i].b, &edge[i].w);
    }
    
    int t = bellman_ford();
    
    if (t == -1) puts("impossible");
    else cout << t << endl;
    
    return 0;
}

SPFA 算法

只要没有负环,就可以用
spfa是对bellman_ford算法的优化

如果被spfa被出题人卡了,只能换堆优化版dikstra

在这里插入图片描述
因为每次进行dist[b] = min(dist[b], dist[a] + w)不一定会更新距离, spfa就是对这一步进行优化.
只有当dist[a]变小了,dist[b]才会变小.

优化方式

用宽搜进行优化
队列里存的就是能让dist[b]变小的节点,比如上图中的a

queue ⟵ \longleftarrow 1(起点)
while queue 不空
 1. t ⟵ \longleftarrow q.front(); q.pop();
 2. 更新下t的所有出边, t ⟶ w b t\stackrel{w}{\longrightarrow}b twb (t即上图中的a, 因为t变小,所以出边的距离b才会变小)
 queue ⟵ \longleftarrow b (如果队列中有b的话,就不用重复加入了)
 
基本思路: 更新过谁,让再拿更新过的点去更新出边, 只有我变小了,我的出边才会变小.

在这里插入图片描述

时间复杂度 最坏情况下 O ( n m ) O(nm) O(nm)

因为是对bellman_ford算法的改进, 最坏情况下 O ( n m ) O(nm) O(nm), 相当于队列优化没起任何作用

AcWing 851. spfa求最短路

分析

spfa代码与dijkstra相似, 直接copy过来,改下dijkstra函数即可

代码

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
int dist[N];
bool st[N];
typedef pair<int, int> PII;

void add(int a, int b, int c){
    
    
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

int spfa(){
    
    
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    queue<int> q;
    q.push(1);
    st[1] = true; // st数组表示当前点是否在队列中
    
    while (q.size()){
    
    
        auto t = q.front(); q.pop();
        st[t] = false;
        
        for (int i = h[t]; ~i; i = ne[i]){
    
    
            int j = e[i];
            if (dist[j] > dist[t] + w[i]){
    
    
                dist[j] = dist[t] + w[i];
                if (!st[j]){
    
     // 只有当j不在队列中才加入, 防止重复加入j
                    q.push(j);
                    st[j] = true;
                }
            }   
        }
    }
    
    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main(){
    
    
    cin >> n >> m;
    memset(h, -1, sizeof h);

    while (m -- ){
    
    
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    int t = spfa();
    if (t == -1) puts("impossible");
    else cout << t << endl;

    return 0;

}

SPFA求负环

dist[x]:当前1~x的最短距离
cnt[x]:当前最短路的边数
每次更新的时候
dist[x] = dist[t] + w[i];
cnt[x] = cnt[t] + 1; // 从1到t的边 + 1条边

如果cnt[x] >= n, 意味着当前最短路经过n条边, 那么一定有n + 1个点, 而图中最多才n个点, 一定有两个点相同,且出现在最短路径中, 比如i出现两次, 即 路径中存在环, 并且能更新距离, 所以是负环

在这里插入图片描述

在这里插入图片描述

AcWing 852. spfa判断负环

分析

直接将spfa算法,稍加改进即可.
可以删掉memset(dist, 0x3f, sizeof dist); dist[1] = 0, 因为求的不是距离的绝对值,而是距离之间的相对值.

队列开始的时候, 需要将所有点加入进来(不仅仅是1号点), 因为题目判断的是是否存在负环, 不是是否存在以1为起点的负环
加入

int spfa(){
    
    
	for (int i = 1; i <= n; i ++) {
    
    
		q.push(i);
		st[i] = true;
	}
	while (q.size()){
    
    
		cnt[j] = cnt[t] + 1;
		if (cnt[j] >= n) return true;
		.....
		}
	return false;
}

int main(){
    
    
	...
	if (spfa()) puts("YES");
	else puts("NO");
}

代码

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e6 + 10;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
int d[N];
bool st[N];// st数组表示当前的点是否在队列中,如果在队列中就不需要再入队了
int cnt[N];// 记录更新次数

void add(int a, int b, int c){
    
    
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}

int spfa(){
    
    


    queue<int> q;
    for (int i = 1; i <= n; i ++ ){
    
     // 加入所有点, 因为题目是判断所有点开始是否有负数环
        st[i] = true;
        q.push(i);
    }

    while (q.size()){
    
    
        int t = q.front(); q.pop();
        st[t] = false;//出队标记为false

        for (int i = h[t]; ~i; i = ne[i]){
    
    
            int j = e[i];
            if (d[j] > d[t] + w[i]){
    
    
                d[j] = d[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j]){
    
    
                    st[j] = true;
                    q.push(j);
                }
            }
        }
    }

    return false;

}

int main(){
    
    
    cin >> n >> m;
    memset(h, -1, sizeof h);

    while (m -- ){
    
    
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    int t = spfa();

    if (spfa()) puts("Yes");
    else puts("No");

    return 0;

}

Floyd 算法

用来求多源汇最短路

用邻接矩阵来存储d[i, j], 表示i 到 j的距离

	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]);

原理(动态规划)

k表示阶段 d[k, i, j]

d[k, i, j] = d[k - 1, i, k] + d[k - 1, k , j]; 
// 当只经过1 ~ k - 1这些点的时候, 加上第k个点,
// 那么就是先从i ~ k + k ~ j(i, j表示1 ~ k - 1中的点)
// 可以发现第1维 没什么用, 可以去掉
d[i, j] = d[i, k] + d[k, j];

时间复杂度O(n^3)

AcWing 854. Floyd求最短路

分析

裸题

代码

#include <iostream>
#include <cstring>
using namespace std;
const int N = 210, M = 200010, INF = 0x3f3f3f3f;

int dist[N][N];
int n, m, Q;

int floyd(){
    
    
    for (int k = 1; k <= n; 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]);
}

int main(){
    
    
    cin >> n >> m >> Q;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i != j) dist[i][j] = INF;
            else dist[i][j] = 0;
            
    for (int i = 0; i < m; i ++ ){
    
    
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        dist[a][b] = min(dist[a][b], c);
    }
    
    floyd();
    
    while (Q -- ){
    
    
        int a, b;
        cin >> a >> b;
        int t = dist[a][b];
        if (t > INF / 2) puts("impossible");
        else printf("%d\n", t);
    }
    
    return 0;
}

if(d[a][b] > INF / 2)

a 与 b不存在通路的情况下, INF会被更新, 可能会比INF小点.

初始化问题

因为多源汇问题, 题目会询问1到1的距离, 所以不能用memset(dist, 0x3f, sizeof dist);去初始化所有点的距离

猜你喜欢

转载自blog.csdn.net/esjiang/article/details/114165304