未命名文件-1
图的简介
简介
图论是数学的一个分支,图是由若干个点和边所组成的集合。点代表事物,而边代表事物之间的关系。
概念
图的构成
图是一个二元组 \(G=(V,E)\),\(V\) 是一个非空集合,表示点,\(E\) 表示边。图分为有向图和无向图,其实就是边有没有方向的区别。边上是可以有权值的,有权值的图成为有权图。
度
- 在有向图中,一个点的前驱结点的数量就是这个结点的入度 \(d^-(v)\),后继结点的数量就是这个结点的出度,记作 \(d^+(v)\),这个结点的度就是入度和出度的和,记作 \(d(v)\) \((d(v)=d^+(v)+d^-(v))\);
- 在无向图中,一个结点的邻点数量就是这个结点的度,记作 \(d(v)\)。
对于一个有向图:
对于一个结点,不同度数的情况也不一样:
| 度数 | 名称 | 英文名称 |
|---|---|---|
| \(d(v)=0\) | 孤立点 | isolated vertex |
| \(d(v)=1\) | 叶子结点 | leaf vertex |
| \(2\mid d(v)\) | 偶点 | even vertex |
| \(2\nmid d(v)\) | 奇点 | odd vertex |
| $d(v)= | V | -1$ |
一个图当中的所有结点的度的最大值记作 \(\Delta(G)\),所有结点的度的最小值记作 \(\delta(G)\)。说人话就是 \(\Delta(G)=\max\limits_{v\in G}d(v)\),\(\delta(G)=\min\limits_{v\in G}d(v)\)。
简单图
- 自环:如果有一条边 \((u,v)\),其中 \(u=v\),那么 \((u,v)\) 就是一个自环;
- 重边:如果有两条边完全相等,就称作重边;
- 简单图:如果一个图里面没有自环和重边,这个图就被称作为简单图。
存图
直接存边
直接存下集合 \(E\),这样空间牺牲较小。但是牺牲了很大的访问速度。可以用一个数组 \(e\) 表示每一条边,第 \(i\) 条边为 \(e_i\),那么假如第 \(i\) 条边接通了 \(u\) 和 \(v\),权值为 \(w\),\(e_i=(u,v,w)\)。
邻接矩阵
可以使用一个 \(|V|\times|V|\) 的二维数组 \(e\),对于 \(u\) 和 \(v\) 两个点,如果他们之间有边的话,\(e_{(u,v)}=1\)。如果是一张带权图的话,那么 \(e_{(u,v)}=w\);反之若 \(u\) 和 \(v\) 不连通,\(e_{(u,v)}=\infin\)。空间可能会过大。
邻接表
可以使用 \(|V|\) 个动态数组,第 \(i\) 个动态数组表示为 \(i\) 的邻边,那么如果 \(u\) 到 \(v\) 有边,就直接将 \(v\) 存到第 \(u\) 个动态数组里面就行了。这种存图方法理论上来讲是最为优秀的,但是不可以 \(\mathcal O(1)\) 判断任意两点是否有边。
邻接表还有链表优化版本,叫做链式前向星,感兴趣可以自己百度,再次不做阐述。
复杂度对比
假设点数为 \(n\),边数为 \(m\),\(i\) 的邻点数量为 \(f(i)\)。
| 存图方法 | 空间复杂度 | 判断任意两点是否有边 | 遍历任意点的邻边 | 遍历整张图 |
|---|---|---|---|---|
| 直接存边 | \(\mathcal O(m)\) | \(\mathcal O(m)\) | \(\mathcal O(m)\) | \(\mathcal O(nm)\) |
| 邻接矩阵 | \(\mathcal O(n^2)\) | \(\mathcal O(1)\) | \(\mathcal O(n)\) | \(\mathcal O(n^2)\) |
| 邻接表 | \(\mathcal O(m)\) | \(\mathcal O(f(i))\) | \(\mathcal O(f(i))\) | \(\mathcal O(n+m)\) |
算法
最短路
Bellman-Ford
松弛操作是求最短路的基础。Bellman-Ford 是一种可以求单源最短路径的算法,基于松弛操作。
我们可以想到,假设 \(d(i)\) 表示源点到 \(i\) 的最短路径,\(e(u,v)\) 表示 \(u\) 到 \(v\) 的路径长度,那么 \(d(v)=d(i)+e(i,v)\),也就是两条最短路径组成了一条长的最短路径。反过来想,若 \(d(i,j)\) 的是 \(i\) 到 \(j\) 的最短路径,那么我们取这条路径上面的任意一点 \(k\),\(d(i,k)\) 和 \(d(k,j)\) 都是最短路径。
Bellman-Ford 就是基于这种思想,因为最短路径的长度最大为 \(n-1\),那么我们就循环 \(n\) 次,然后枚举所有的边,进行松弛操作。这种算法的时间复杂度是 \(\mathcal O(nm)\) 的。
vector<ll> bellmanFord(ll s) {
vector<ll> d(n + 1, 1e9);
d[s] = 0;
for (ll i = 0; i <= n; i++) {
for (ll j = 0; j <= n; j++) {
for (auto k : e[j]) {
d[k.to] = min(d[k.to], d[j] + k.v);
}
}
}
return d;
}
Dijkstra
Dijkstra 也是一种求单源最短路的算法。我们首先枚举 \(n\) 个点,假设是 \(i\),然后找离 \(i\) 最近的结点 \(j\),然后从从结点 \(j\) 开始,往周围拓展,通过松弛操作更改路径。这种算法与 Bellman-Ford 不同的是使用了一种贪心的思路,找最近的点转移必定会找出更近的路径。但是凡事都有例外,如果是带着负边权的图,那么 Dijkstra 就无能为力了。算法时间复杂度为 \(\mathcal O(n^2)\)。
接下来是一种重要的优化:堆优化。我们知道,这里找到最近的点是需要 \(\mathcal O(n)\) 的时间的,那么我们其实可以开一个优先队列,每次都能够获取到最近的点。这种思路就是以从小到大的路径长度作为拓扑序,不断地向后转移,进行松弛操作。这种算法的时间复杂度为 \(\mathcal O((n+m)\log_2 m)=\mathcal O(m\log_2 m)\)。
这里使用的是优先队列,如果使用二叉堆的话,其实在优先队列内就可以进行更改了,而 STL 中实现这种操作的是 set,但是 set 的常数巨大,所以我们必须手写。可以将时间复杂度降到 \(\mathcal O(m\log_2 n)\)。
这里使用的是优先队列的方法。
vector<ll> dijkstra(ll s) {
vector<ll> d(n + 1, 1e9), f(n + 1, 0);
for (q.push({s, 0}), d[s] = 0; q.size(); q.pop()) {
if (f[(t = q.top()).to]) {
continue;
}
f[t.to] = 1;
for (auto i : e[t.to]) {
if (d[t.to] + i.v < d[i.to]) {
q.push({i.to, d[i.to] = d[t.to] + i.v});
}
}
}
return d;
}
SPFA
SPFA (Sortest Path Faster Algorithm) 是一种可以快速求负边权最短路的算法,他其实是 Bellman-Ford 的升级版,但是在 Bellman-Ford 的操作中,其实有很多点是不需要进行松弛操作的,因此我们就把需要进行松弛操作的点给存进队列里面,然后等到下次又可以进行松弛操作的时候进行更改。SPFA 在随机图上跑得非常快,基本上能够达到 \(\mathcal O(n+m)\) 的速度,但是需要卡也是非常简单的,最坏会达到 \(\mathcal O(nm)\) 的巨大复杂度。最后,请大家记住一点:
关于 SPFA,她死了。

浙公网安备 33010602011771号