-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8ec7c6c
commit b1e211e
Showing
7 changed files
with
777 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
--- | ||
title: 最短路 | ||
titleTemplate: 图论 | ||
--- | ||
|
||
## 最短路 | ||
|
||
给定一张 $n$ 个点, $m$ 条边的图,每条边都有长度,问从一个点到另一个点的最短路长度。 | ||
|
||
如果起点固定,则称为单源最短路。 | ||
|
||
如果起点不固定,则称为多源最短路。 | ||
|
||
这张图必须保证没有长度为负数的环,否则在这个环上一直绕圈,长度会越来越小。 | ||
|
||
### BFS | ||
|
||
如果边的长度都是 $1$ 则之间广度优先搜索 | ||
|
||
### floyd | ||
|
||
假设边用邻接矩阵存储,此时 $eg[i][j]$ 是连接 $i$ 点和 $j$ 点的边的长度。 | ||
|
||
设 $f[k][i][j]$ 是只允许**途径** $1\sim k$ 的情况下,从 $i$ 点到 $j$ 点的最短路径长度。 | ||
|
||
则 $f[n][i][j]$ 就是最终的最短路。 | ||
|
||
设 $f[0][i][j]=eg[i][j]$,则有一个~~显然~~的转移方程 | ||
|
||
```cpp | ||
f[k][i][j]=min(f[k-1][i][j],f[k-1][i][k]+f[k-1][k][j]); | ||
``` | ||
|
||
用三重循环把转移完整地写出来: | ||
|
||
```cpp | ||
for(int k=1;k<=n;++k) | ||
{ | ||
for(int i=1;i<=n;++i) | ||
{ | ||
for(int j=1;j<=n;++j) | ||
{ | ||
f[k][i][j]=min(f[k-1][i][k]+f[k-1][k][j]); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
事实上第一维对转移没有影响 | ||
|
||
因为无论是从 $f[k-1][i][k]$ 转移还是从 $f[k][i][k]$ 转移,都不可能经过两次 $k$ 点,否则就会成环,而因为没有负环,所以代价一定会变大。 | ||
|
||
```cpp | ||
for(int k=1;k<=n;++k) | ||
{ | ||
for(int i=1;i<=n;++i) | ||
{ | ||
for(int j=1;j<=n;++j) | ||
{ | ||
f[i][j]=min(f[i][k]+f[k][j]); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
注意这里的 $k$ 的循环一定要在**最外面** | ||
|
||
当 $k>i>j$ 时 更新 $f[i][j]=min(f[i][k]+f[k][j])$, | ||
|
||
若 $k$ 不在最外层,则 $f[i][k]$ 还没有进行任何更新,说明 $f[i][k]$ 里面的值不是最小的,自然得到的 $f[i][j]$ 也不是最小值。 | ||
|
||
$floyd$ 的时间复杂度是 $O(n^3)$ ,空间复杂度是 $O(n^2)$ | ||
|
||
弗洛伊德得到的最短路是全源最短路,任意两个点之间的距离都能得到 | ||
|
||
[B3647 【模板】Floyd ](https://www.luogu.com.cn/problem/B3647) | ||
|
||
#### 最小环 | ||
|
||
假设图是简单图(没有重边和自环) | ||
|
||
最小环一定是一个简单环(不存在环套环) | ||
|
||
设环中编号最大的点是 $u$ 。 | ||
|
||
当得到 $f[u-1][x][y]$ 时,$f[u-1][x][y]$ 与 $(x,u)$ 和 $(y,u)$ 形成简单环,依次判断该最小值。 | ||
|
||
[P6175 无向图的最小环问题 ](https://www.luogu.com.cn/problem/P6175) | ||
|
||
#### 传递闭包 | ||
|
||
当某种关系具有传递性时(必须某两个点是否联通),可以用$floyd$ 将性质传递 | ||
|
||
$f[i][j]$ 表示 $i$ 到 $j$ 联通(或者有关系从 $i$ 指向 $j$ ) | ||
|
||
```cpp | ||
f[i][j]|=f[i][k]&f[k][j]; | ||
``` | ||
|
||
进一步可以用 bitset 优化到 $O(\frac{n^3}{w})$ | ||
|
||
[B3611 【模板】传递闭包 ](https://www.luogu.com.cn/problem/B3611) | ||
|
||
### bellman-ford | ||
|
||
该算法适用于求单源最短路,即从固定起点出发到其他点的最短路 | ||
|
||
设一个数组 $dis$ ,其中 $dis[u]$ 表示从出发点到 $i$ 点的最短路径长度。 | ||
|
||
如果存在一条 $u->t$ 且长度为 $v$ 的边,则可以用 `dis[t]=min(dis[t],dis[u]+v)` 来**松弛** $t$ 节点。 | ||
|
||
因为最短路最多经过 $n-1$ 条边,所以最多只用 $n-1$ 次松弛。 | ||
|
||
可以设计一个算法,每次用所有边进行依次松弛,最终一定能得到最短路。 | ||
|
||
```cpp | ||
for(int i=1;i<n;++i)//松弛次数 | ||
{ | ||
for(int u=1;u<=n;++u) | ||
{ | ||
for(auto [t,v]:eg[u])//u->t,长度为v的边。 | ||
{ | ||
dis[t]=min(dis[t],dis[u]+v); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
也可以进行 $n$ 轮松弛,而且如果第 $n$ 轮仍然能进行松弛,说明图中存在负环(总长度为负数的环)。 | ||
|
||
每轮松弛访问所有边一次,总时间复杂度 $O(nm)$ | ||
|
||
### spfa | ||
|
||
$bellman-ford$ 有很大的改进空间,比如某一轮松弛不可能所有点所有边都会用到。 | ||
|
||
第一次松弛,只有起点的距离是已知的,说明只有从起点出发的边是有用的。 | ||
|
||
第二次松弛,只有起点周围一圈的距离是已知的,说明只有这一圈点出发的边是有用的。 | ||
|
||
…… | ||
|
||
第 $k$ 次松弛,如果某一个点在第 $k-1$ 次松弛中,$dis$ 没有发生改变,那么以它为起点的边是没有必要松弛的,只有 $dis$ 发生改变的点才有必要松弛。 | ||
|
||
开一个队列,将所有 $dis$ 更改过的点值更改过的点放进队列里,用这些点出发的边进行下一次松弛 | ||
|
||
```cpp | ||
while(!q.empty()) | ||
{ | ||
int now=q.front(); | ||
q.pop(); | ||
//vis[now] 表示 now 是否在队列里,防止一个点占好几个位置 | ||
vis[now]=0;//出队列 | ||
for(auto [t,v]:eg[now]) | ||
{ | ||
if(dis[t]>dis[now]+v) | ||
{ | ||
dis[t]=dis[now]+v; | ||
if(!vis[t]) q.push(t);//如果发生了改变,进队列 | ||
} | ||
} | ||
} | ||
``` | ||
|
||
可以给每个点记录一下 $dis$ 的修改次数,如果达到 $n$ 次则说明图中有负环。 | ||
|
||
它的最坏复杂度也是 $O(nm)$ ,但是平均期望复杂度是 $O(nlogn)$。 | ||
|
||
不推荐在平常情况下写这个算法,除非是判断负环或者边有负数。 | ||
|
||
[P3385 【模板】负环 ](https://www.luogu.com.cn/problem/P3385) | ||
|
||
### dijkstra | ||
|
||
当图中的边长度没有负数时求单源最短路: | ||
|
||
那么有一个神奇的事情,如果一个点的 $dis$ 是所有点中最小的那个,那么这个 $dis$ 一定是它的最短路。 | ||
|
||
举例: | ||
|
||
![](https://s3.bmp.ovh/imgs/2024/01/27/4e1c4b0b85f68a9a.png) | ||
|
||
如果没有边的长度是负数,那么此时 $1->2$ 的最短路径就是 $5$ 不会变得更短了。 | ||
|
||
如果有边的长度是负数,那么则不一定,有可能 $3->2$ 的长度是 $-8$ ,这时候如果先用 $2$ 更新其他点,再用 $3->2$这条边松弛 $2$ ,那 $2$ 之前的更新就都作废了。 | ||
|
||
建立在没有负数长度边的前提下,我们可以每次直接取出还没取过的 $dis$ 最小的那个点,因为它的答案确定了,所以它可以毫无压力地松弛别人。 | ||
|
||
它的本质可以说是贪心算法。 | ||
|
||
要取最小的,所以把队列改成堆(优先队列),存两个参数,第一个是当前点的 $dis$ ,第二个是节点的编号。 | ||
|
||
```cpp | ||
while(!q.empty()) | ||
{ | ||
int now=q.top()[1];//[0] 是长度, [1] 是编号 | ||
q.pop(); | ||
if(vis[now]) continue;//如果这个点被选过就跳过 | ||
vis[now]=1; | ||
for(auto [t,v]:eg[now]) | ||
{ | ||
if(dis[t]>dis[now]+v) | ||
{ | ||
dis[t]=dis[now]+v; | ||
q.push(pr{dis[t],t});//如果成功松弛,将松弛后的点加入优先队列。 | ||
//因为优先队列中要存dis[t],而且每次松弛后dis[t] 长度不同,所以相同的点也必须都加进去,而不是像spfa只存一个。 | ||
} | ||
} | ||
} | ||
``` | ||
|
||
时间复杂度: | ||
|
||
最多 $m$ 次进堆和出堆,如果按每次时间复杂度 $logm$ 算,则时间复杂度是 $O(mlogm)$ | ||
|
||
[P4779 【模板】单源最短路径(标准版) ](https://www.luogu.com.cn/problem/P4779) | ||
|
||
### 分层图 | ||
|
||
[P4568 [JLOI2011] 飞行路线 ](https://www.luogu.com.cn/problem/P4568) | ||
|
||
在无负长度边情况下,求最短路径,且可以令任意 $k$ 条边的长度变为 $0$,求 $1$ 号点到 $n$ 号的最短路径。 | ||
|
||
将图视作一共 $k$ 层的分层图,每一层都是一整张图,而上一层的边可以通过 $0$ 代价走到下一层。 | ||
|
||
即如果本来存在 $u→v$ 长度为 $len$ 的边。 | ||
|
||
可以从 $(u,1)→(v,2)$ 长度为 $0$ 的边,第二个参数是层数。 | ||
|
||
层数增加是不可逆的,符合了建模的想法,最后 $(1,0)$ 到 $(n,k)$ 的距离就是最短路。 | ||
|
||
可以直接将状态设计成 $dis[u][k]$ 表示第 $k$ 层的节点 $u$ 。 | ||
|
||
### 练习题 | ||
|
||
[P1144 最短路计数 ](https://www.luogu.com.cn/problem/P1144) | ||
|
||
[P2865 [USACO06NOV] Roadblocks G ](https://www.luogu.com.cn/problem/P2865) | ||
|
||
[P1462 通往奥格瑞玛的道路 ](https://www.luogu.com.cn/problem/P1462) | ||
|
||
[P1119 灾后重建 ](https://www.luogu.com.cn/problem/P1119) | ||
|
||
[E - Skiing](https://atcoder.jp/contests/abc237/tasks/abc237_e) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
--- | ||
title: 生成树 | ||
titleTemplate: 图论 | ||
--- | ||
|
||
## 生成树 | ||
|
||
前置知识:并查集,树与图 | ||
|
||
给定一张连通图,从图中选出数量最少的边,使得所有点仍然是连通的,那么选出的图一定是一棵树。 | ||
|
||
### 最小生成树 | ||
|
||
在生成树,边权之和最小的那一棵,称为最小生成树。 | ||
|
||
例如在市政规划时,管道要连接所有的节点,但是希望总长度最短等等 | ||
|
||
### prim | ||
|
||
把节点分为两个集合,一个是连通集合 $S$ ,另一个是未连通集合 $T$ 。 | ||
|
||
先随便选一个点作为 $S$ 的初始节点,然后重复以下步骤: | ||
|
||
选择连接 $S$ 和 $T$ 节点的权值最小的边,然后将这条边和端点都加入 $S$ 。 | ||
|
||
一共只有 $n-1$ 条边。 | ||
|
||
```cpp | ||
bool vis[N];//是否在 S 集合 | ||
int dis[N];//S 集合到该点的最短距离 | ||
int sum=0; | ||
vis[1]=1;dis[1]=0; | ||
memset(dis,0x3f,sizeof(dis)); | ||
for(int k=1;k<=n;++k) | ||
{ | ||
int pos=0; | ||
for(int i=1;i<=n;++i) | ||
{ | ||
if(!vis[i]&&dis[i]<dis[pos]) pos=i;//找到最近的那一条边 | ||
} | ||
sum+=dis[pos]; | ||
vis[pos]=1;//将这个点加入集合 | ||
for(auto [t,v]:eg[pos]) dis[t]=min(dis[t],v);//将到 T 中的点的距离更新 | ||
} | ||
``` | ||
这样复杂度是 $O(n^2)$。 | ||
正确性证明: | ||
假设某一时刻连通集合是 $S_0$,未连通集合是 $T_0$,此时最短的边 $e_1(u,v,len)$ 没被选择,而选择了另一条边 $e_2$ | ||
当整张图连通时,将 $e_2$ 换成 $e_1$ ,图仍然连通,且答案不会更差。 | ||
### kruskal | ||
将边按长度从小到大排序,若这条边的两个端点还没有连通(用并查集判断),则将这条边选中。 | ||
```cpp | ||
typeedef array<int,3> edge; | ||
vector<edge> eg(m+1); | ||
for(int i=1;i<=m;++i) | ||
{ | ||
cin>>e[i][0]>>a[i][1]>>a[i][2]; | ||
//e[i][0]和e[i][1]之间有一条权值为e[i][2]的边。 | ||
} | ||
sort(e.begin()+1,e.end(),[&](edge a,edge b){ | ||
return a[2]<b[2]; | ||
}); | ||
vector<int> f(n+1);//并查集 | ||
for(int i=1;i<=n;++i) f[i]=i; | ||
int sum=0; | ||
for(int i=1;i<=n;++i) | ||
{ | ||
int x=e[i][0],y=e[i][1]; | ||
x=find(x),y=find(y); | ||
if(x==y) continue; | ||
sum+=e[i][2]; | ||
f[x]=y; | ||
} | ||
``` | ||
|
||
这样复杂度是 $O(mlogm)$ ,在多数情况下都比 $prim$ 要好,除非是特别稠密的图。 | ||
|
||
[P3366 【模板】最小生成树 ](https://www.luogu.com.cn/problem/P3366) | ||
|
||
[P1194 买礼物 ](https://www.luogu.com.cn/problem/P1194) | ||
|
||
### Boruvka | ||
|
||
是一种多路增广的改进 $prim$ 算法。 | ||
|
||
开局有 $n$ 个节点,也就是 $n$ 个连通块,每轮操作给每个连通块找和其他连通块相连的长度最小的边,然后一起合并。 | ||
|
||
因为每一轮每个连通块至少节点数量翻倍,所以一共只有 $log$ 轮。 | ||
|
||
关键在于能否快速找到两个连通块之间的长度最小的边 | ||
|
||
例如:每个点有一个权值 $a_i$ ,两个点之间的边的权值是 $|a_i-a_j|$,在这张完全图上求最小生成树。 | ||
|
||
开一个 $multiset$ 存所有点的 $a_i$ ,当处理到某一个连通块的时候,先把这个连通块内的点删去,然后在 $set$ 里二分找最接近 $a_i$ 的点,找到后再添加回去。 | ||
|
||
每一轮的话要花 $O(nlogn)$ 时间,所以总时间是 $O(nlog^2n)$。 | ||
|
||
[CF1550F Jumping Around](https://codeforces.com/problemset/problem/1550/F) | ||
|
||
### 练习题 | ||
|
||
[P2330 [SCOI2005] 繁忙的都市 ](https://www.luogu.com.cn/problem/P2330) | ||
|
||
[Shichikuji and Power Grid ](https://www.luogu.com.cn/problem/CF1245D) | ||
|
||
[P2573 [SCOI2012] 滑雪 ](https://www.luogu.com.cn/problem/P2573) |
Oops, something went wrong.