Skip to content

Commit

Permalink
update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Old-Second committed Jan 30, 2024
1 parent 8ec7c6c commit b1e211e
Show file tree
Hide file tree
Showing 7 changed files with 777 additions and 0 deletions.
5 changes: 5 additions & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export default {
{text: '22.线段树', link: '/数据结构/22.线段树'},
{text: '23.分块', link: '/数据结构/23.分块'},
{text: '笛卡尔树', link: '/数据结构/笛卡尔树'},
{text: '并查集', link: '/数据结构/并查集'},
]
},
],
Expand Down Expand Up @@ -189,6 +190,8 @@ export default {
{text: '18.图与树入门', link: '/图论/18.图与树入门'},
{text: '图论相关概念', link: '/图论/图论相关概念'},
{text: '二分图', link: '/图论/二分图'},
{text: '最短路', link: '/图论/最短路'},
{text: '生成树', link: '/图论/生成树'},
]
},
],
Expand All @@ -206,6 +209,8 @@ export default {
items: [
{text: '对顶堆', link: '/杂项/对顶堆'},
{text: '对顶栈', link: '/杂项/对顶栈'},
{text: 'WQS二分', link: '/杂项/WQS二分'},
{text: '二进制分组', link: '/杂项/二进制分组'},
]
},
],
Expand Down
244 changes: 244 additions & 0 deletions docs/图论/最短路.md
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)
113 changes: 113 additions & 0 deletions docs/图论/生成树.md
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)
Loading

0 comments on commit b1e211e

Please sign in to comment.