最短路

  • 单源最短路
    • 所有边权都是正数
      • 朴素 Dijkstra算法 O(n^2) 遍历所有点
      • Dijkstra堆优化算法 O(mlogn) 遍历所有最短点并更新相连的所有边
    • 存在负权边
      • Bellman-Ford O(nm) 遍历所有边, 更新点. 可求限定边的最短路问题
      • SPFA 一般O(m) 最坏O(mn)
  • 多源汇最短路
    • Floyd算法 O(n^3)

源点: 起点

汇点: 重点

Dijkstra求最短路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int n
int g[N][N]; //邻接矩阵存储边
int dist[N]; //点n到源点的距离
bool st[N]; //是否遍历过

int dijkstra()
{
// 初始化所有点到源点的距离为无穷大,且第一个点到源点的距离为0(自己本身就是源点)
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;

// 所有的点都遍历一次
for (int i = 0; i < n - 1; i ++ )
{
// 寻找一个当前要操作的点t
int t = -1;
// 遍历所有的点,搜索一个没有更新过 且距离源点距离最小的点
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
// 此时t为没有更新过且距离最小的点

// 遍历所有的点, 判断源点直接到j的距离最短还是通过t到j的距离最短
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);

// 更新过t所以不再更新t的数据
st[t] = true;
}

// 判断字节
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
堆优化版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
typedef pair<int, int> PII;

const int N = 1e6 + 10;

int n, m;

int h[N], w[N], e[N], ne[N], idx; // 领接表存储

int dist[N]; // 当前点到源点的最短距离

bool st[N]; // 判断是否更新过

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 != -1; 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});
}
}
}

// 判断是否能到达最点n
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}

Bellman - Ford 算法

  • 核心操作是遍历所有的边, 刷新点的距离
  • 对于每一次松弛操作, 我们都要备份一次操作过的数组防止覆盖掉当前操作

a, b ,w a指向b的边w

1
2
3
4
5
6
7
循环n次   // 不超过k条边最短路的距离
{
for 所有边w // 松弛操作
{
dist[b] = min (dist[b] ,dist[a] + w)
}
}

算法执行结束后, 对于所有的边 dist[b] <= dist[a] + w 三角不等式

一定要备份数组, 不然会出现修改原dist数组的情况,导致结果混乱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void bellman_ford()
{
memset(dist, 0x3f, sizeof dist);

dist[1] = 0;
for (int i = 0; i < k; i ++ )
{
for (int j = 0; j < m; j ++ )
{
auto e = edges[j];
dist[e.b] = min(dist[e.b], dist[e.a] + e.c);
}
}
}

代码模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 10010;

struct Edge{
int a;
int b;
int c;
}w[N];

int n, m, k;

int dist[N];
int last[N];

int main()
{
cin >> n >> m >> k;
for(int i = 0 ; i < m; i ++)
{
cin >> w[i].a >> w[i].b >> w[i].c;
}

// bellman-ford 核心代码
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int j = 0 ; j < k; j++)
{
memcpy(last, dist, sizeof dist);
for(int i = 0 ; i < m ; i++)
{
int a = w[i].a, b = w[i].b, c = w[i].c;
dist[b] = min(dist[b], last[a] + c);
}
}

if(dist[n] > 0x3f3f3f3f / 2) cout << "impossible";
else cout << dist[n];

return 0;
}

spfa算法 (可判断负权环)

使用一个队列, 将更新后缩小的路径添加至队列, st数组表示某一点是否在队列中, 因为某一天有可能在遍历多个边之后, 路径缩短n次 ,我们只要求点在队列中存储一次即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int spfa()
{
// 初始化
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;

queue<int> q;
q.push(1);
st[1] = true; // st数组表示某点是否在队列中

while (q.size())
{
// 出队
int t = q.front();
q.pop();

st[t] = false;

// 遍历当前点的所有边
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
// 如果j点距离被缩小且不在队列中则入队
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}

return dist[n];
}

判断负权回路的方法

我们使用一个cnt数组记录当前走过的路径, 如果cnt > 经过的点, 则证明当前图中存在负权回路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int cnt[N];		//数组含义是当前点经过了多少个边


bool 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;

for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;

// 如果边数大于等于点数,则一定存在回路
if (cnt[j] >= n) return true;
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}

return false;
}
为什么不需要初始化dist数组?

如果只是为了判断负环, 如果负环存在的话dist的值会变为无穷小,所以dist初始值无论是多少都不会影响判断负环

多源汇最短路Floyd

解决多个源点到汇点的距离

1
2
3
4
5
6
7
8
9
10
11
12
13
for k
for i
for j
d[i][j] = min( d[i][j], d[i][k] + d[k][j]) //这里的顺序要注意
// ------------------------------------------
void floyd()
{
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]);
// 这里d不是到源点的距离,而是 i j的距离, 即 领接矩阵
}

最小生成树

  • 普利姆算法(prim)

    • 稠密图 - 朴素班prim O(n^2)
    • 稀疏图 - 堆优化 Omlogn)
  • 克鲁斯卡尔算法(Kruskal) O(mlogm) << 稀疏图

prim算法求最小生成树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int prim(int n)
{
// 初始化
int ret = 0;
memset(dist, 0x3f, sizeof dist);


for(int i = 0; i < n; i++)
{

// t为与集合距离最近的点
int t = -1;
for(int j = 1; j <= n; j++)
{
if(!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
}

// 如果当前距离与集合的最小距离为无穷大, 则表示不是一个连通图
if(i && dist[t] == 0x3f3f3f3f) return 0x3f3f3f3f;
if(i) ret += dist[t]; // 第一个点不属于任何集合所以要排除第一个点的距离
st[t] = 1;
// st数组用来表示是否在集合内部


// 依次更新距离集合最短距离
for(int j = 1; j <= n; j++)
{
if(dist[j] > e[t][j]) dist[j] = e[t][j];
}

}

return ret;
}

克鲁斯卡尔算法

并查集的应用

对所有边按权重排序

从最小的开始依次查询两条边是否在一个集合内, 如果不在一个集合内将点加入集合并累加边的权重.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int kruskal()
{
// 所有的边按权重排序
sort(edges, edges + m);

for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集

// 分别记录最小路径和边数
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ )
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;

a = find(a), b = find(b);

// 如果连个点未连接,则连接两点并记录
if (a != b)
{
p[a] = b;
res += w;
cnt ++ ;
}
}

// 总边数等于点数减一
if (cnt < n - 1) return INF;
return res;
}

二分图

  • 染色法 O(m+n)
  • 匈牙利算法 O(mn), 实际运行时间远小于O(mn)

染色法

​ 深度优先遍历, 分别染色. 如果已经被染过则不是二分图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include<iostream>
#include<cstring>

using namespace std;

const int N = 100000 + 10;
int h[N], e[2 * N], idx, ne[2 * N];
int n, m;
int st[N];

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

bool dfs(int x, int c)
{
// c表示的是当前的颜色
st[x] = c;
for(int i = h[x]; i != -1; i = ne[i])
{
int d = e[i];
if(st[d] == 0) // 如果没有被染色
{
if(!dfs(d,3 - c)) return false;
}
if(st[d] == c) // 如果连接的点的颜色等于当前点的颜色,则不是二分图
return false;
}

return true;
}

int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
while(m--)
{
int a, b;
cin >> a >> b;
add(a,b);
add(b,a);
}

// 分别遍历所有没有遍历过的点,有可能二分图不是连通图
bool ret = true;
for(int i = 1; i <= n; i++)
{
if(st[i] == 0)
{
if(!dfs(i,1))
{
ret = false;
break;
}
}
}


if(ret) cout << "Yes";
else cout << "No";
return 0;
}

匈牙利算法