图论问题概述总结
对于**图论**,我们尊熟悉的算法是比较多的,这次,我就找了集中常用的算法。
## 几种算法
1. **最短路**算法(Dijkstra,SPFE,FLOYD)
- Dijkstra单源最短算法
首先,此算法适用于计算一个点到另一个点的最短路径,且算法绝对不能出现负环。这个算法的速度慢,只用于接觉小规模的问题,如图:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190609113100303.png)这个图就是求算法的基本思路。算法过程:
- 从节点上找到最近点那个节点,将他标记,加入集合U。
- 将定点U连出边的邻点相连接,不在集合U中寻找。
- 重复前面的操作,用来指导V=U是,查找结束,最后结束流程。
本算法的算法流程图:
https://wenku.baidu.com/view/8a5c11303968011ca300916a.html
参考代码:
```
#include <iostream> #include <cstring> using namespace std; const int N = 1e3 + 9; const int M = 1e4 + 9; const int inf = 0x3f3f3f3f; struct edge { int v, w, next; edge() {} edge(int _v, int _w, int _next) { v = _v; w = _w; next = _next; } } e[M << 1]; int head[N], len; void init() { memset(head, -1, sizeof head); len = 0; } void add(int u, int v, int w) { e[len] = edge(v, w, head[u]); head[u] = len++; } void add2(int u, int v, int w) { add(u, v, w); add(v, u, w); } int n, m; int dis[N]; bool vis[N]; void dijkstra(int u) { memset(vis, false, sizeof vis); memset(dis, inf, sizeof dis); dis[u] = 0; for (int i = 0; i < n; ++i) { int mi = inf; for (int j = 1; j <= n; ++j) { if (!vis[j] && dis[j] < mi) { mi = dis[u = j]; } } if (mi == inf) { return; } vis[u] = true; for (int j = head[u]; ~j; j = e[j].next) { int v = e[j].v; int w = e[j].w; if (!vis[v] && dis[v] > dis[u] + w) { dis[v] = dis[u] + w; } } } } int main() { init(); int u, v, w; cin >> n >> m; while (m--) { cin >> u >> v >> w; add2(u, v, w); } dijkstra(1); cout << dis[n] << endl; return 0; }
```
这只是一个基本的流程代码,你可在刷掉模板的基础上,在进行修改。
**它的基本思想是以起始点为中心往外层扩展(广度优先搜索+贪心),直到扩展到终点为止。**
这就是我们的算法,因此,可以解决很多问题。
```
3 3
1 2 5
2 3 5
3 1 2
```
输入数据,看看会怎么样!
-堆优化Dijkstra
正对于稀疏图的算法,我们用与优化。
```
#include <iostream> #include <cstring> #include <set> using namespace std; const int N = 1e3 + 9; const int M = 1e4 + 9; const int inf = 0x3f3f3f3f; typedef pair<int, int> pall; #define X first #define Y second struct edge { int v, w, next; edge() {} edge(int _v, int _w, int _next) { v = _v; w = _w; next = _next; } } e[M << 1]; int head[N], len; void init() { memset(head, -1, sizeof head); len = 0; } void add(int u, int v, int w) { e[len] = edge(v, w, head[u]); head[u] = len++; } void add2(int u, int v, int w) { add(u, v, w); add(v, u, w); } int n, m; int dis[N]; bool vis[N]; void dijkstra(int u) { memset(vis, false, sizeof vis); memset(dis, inf, sizeof dis); dis[u] = 0; } int main() { init(); int u, v, w; cin >> n >> m; while (m--) { cin >> u >> v >> w; add2(u, v, w); } dijkstra(1); cout << dis[n] << endl; return 0; }
```
找最小值,这里是和普通 Dijkstra 的核心不同之处,我们只需要获取堆顶元素即可(堆自动实现排序,
堆顶元素就是我们需要的最小值)。然后我们把这个元素加入集合(标记就是加入集合的意思)。
- SPFA单源最短路算法
在 SPFA 算法中,使用 di表示从源点到顶点 i的最短路,额外用一个队列来保存即将进行拓展的顶点
列表,并用ingi 来标识顶点 i是不是在队列中。
如图:
![](https://img-blog.csdnimg.cn/20190609150752915.png)
SPFA 的空间复杂度为O(V),有点像这个稀疏图的步揍。他用队列来运行,因此,SPEA只是是在对列上升级以下,并不是那种十分多的。**在一定程度上,可以认为 SPFA 是由 BFS 的思想转化而来。从不含边权或者说边权为 个单位长度的图上的 BFS,推广到带权图上,就得到了 SPFA。**
算法步揍:
- 用一个队列来保存多个扩展的队列。
- 用一个队列中取的一个元素,并且对其他点进行松弛。
- 当所有不在队列的点都入了队列,则程序结束,算法进行完毕。
```
bool inq[MAX_N]; int d[MAX_N]; // 如果到顶点 i 的距离是 0x3f3f3f3f,则说明不存在源点到 i 的最短路 void spfa(int s) { memset(inq, 0, sizeof(inq)); memset(d, 0x3f, sizeof(d)); d[s] = 0; inq[s] = true; queue<int> q; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); inq[u] = false; for (int i = p[u]; i != -1; i = e[i].next) { int v = e[i].v; if (d[u] + e[i].w < d[v]) { d[v] = d[u] + e[i].w; if (!inq[v]) { q.push(v); inq[v] = true; } } } } } ``` 算法的图:https://i.loli.net/2019/06/09/5cfcb3db1267062487.jpg 上面的代码就是SPEA的全部结构,初始化与第一个算法一样的结构一样的。 **判断负环**的SPEA算法: ``` #include <iostream> #include <cstring> #include <queue> using namespace std; const int N = 1e3 + 9; const int M = 1e4 + 9; const int inf = 0x3f3f3f3f; struct edge { int v, w, next; edge() {} edge(int _v, int _w, int _next) { v = _v; w = _w; next = _next; } } e[M << 1]; int head[N], len; void init() { memset(head, -1, sizeof head); len = 0; } void add(int u, int v, int w) { e[len] = edge(v, w, head[u]); head[u] = len++; } void add2(int u, int v, int w) { add(u, v, w); add(v, u, w); } int n, m; int main() { init(); int u, v, w; cin >> n >> m; while (m--) { cin >> u >> v >> w; add2(u, v, w); } return 0; }
```
接下来就可以运行程序了:
```
3 3
1 2 5
2 3 5
3 1 2
```
- Floyd 多源最短路算法
Floyd 算法是一种计算给定的带权图中任意两个顶点之间最短路径的算法。相比于重复执行多次单源最
短路算法,Floyd 具有高效、代码简短的优势,在解决图论最短路题目时比较常用。
**floyd 算法是解决负环(可以计算出任意两点之间的最短路)有向图或无向图的多源最短路问题。常
用邻接矩阵存储,算法的时间复杂度 O(N2),空间复杂度O(N2) 。**
算法方式:
- 如果不经过第 个点,那么就是 dp[k-1][i][j]。
- 如果经过第 个点,那么就是 dp[k-1][i][j]+dp[k-1][i][j]。
所以就变成了:
```
dp[k][i][j] = min(dp[k − 1][i][j], dp[k − 1][i][k] + dp[k − 1][k][j])
```
所以就变成了如下的代码:
```
int g[N][N]; // 邻接矩阵存图 int dp[N][N][N]; void floyd(int n) { for (int k = 0; k <= n; ++k) { for (int i = 1; i <= n; ++i) { for (int j = 1; j <= n; ++j) { if (k == 0) { dp[k][i][j] = g[i][j]; } else { dp[k][i][j] = min(dp[k - 1][i][j], dp[k - 1][i][k] + dp[k - 1][k][j]); } } } } } ``` 我们写出最终的 Floyd 的形式,这也是常用的写法,优化了一维的空间。并且写法更加简单。这里要注 意,枚举的中间点 一定要写在最外面。没有注意这一点,很容易把 3 个循环的顺序弄错了,那么结 果也就是错的。 刚才的分析得出: ``` int g[N][N]; void floyd(int n) { for (int k = 1; k <= n; ++k) { for (int i = 1; i <= n; ++i) { for (int j = 1; j <= n; ++j) { g[i][j] = min(g[i][j], g[i][k] + g[k][j]); } } } }
```
算法到这里就完成了,接下来给大家介绍**差分约束系统**。
- 差分约束系统
我们在求解差分约束系统时,可以将其转化为图论中单源最短路(或最长路)问题。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190609154201381.png)
途中有负环是,就可以这样。
```
#include <iostream> #include <cstring> #include <queue> using namespace std; const int N = 1e3 + 9; const int M = 1e4 + 9; const int inf = 0x3f3f3f3f; struct edge { int v, w, next; edge() {} edge(int _v, int _w, int _next) { v = _v; w = _w; next = _next; } } e[M << 1]; int head[N], len; void init() { memset(head, -1, sizeof head); len = 0; } void add(int u, int v, int w) { e[len] = edge(v, w, head[u]); head[u] = len++; } void add2(int u, int v, int w) { add(u, v, w); add(v, u, w); } int n, m; int dis[N], in[N]; bool vis[N]; bool spfa(int u) { memset(vis, false, sizeof vis); vis[u] = true; memset(dis, -1, sizeof dis); dis[u] = 0; memset(in, 0, sizeof in); in[u] = 1; queue<int> q; q.push(u); while (!q.empty()) { u = q.front(); q.pop(); vis[u] = false; for (int j = head[u]; ~j; j = e[j].next) { int v = e[j].v; int w = e[j].w; if (dis[v] < dis[u] + w) { // 求最长路,和求最短路相反 dis[v] = dis[u] + w; if (!vis[v]) { q.push(v); vis[v] = true; ++in[v]; if (in[v] > n + 1) { return true; } } } } } return false; } int main() { init(); int u, v, w, op; cin >> n >> m; while (m--) { cin >> op; cin >> u >> v >> w; } if (op == 1) { add(u, v, -w); }else if (op == 2) { add(v, u, w); }else { add(u, v, -w); add(v, u, w); } for (int i = 1; i <= n; ++i) { add(0, i, 0); } if (spfa(0)) { cout << "no" << endl; }else { for (int i = 1; i <= n; ++i) { cout << "x" << i << " = " << dis[i] << endl; } } return 0; }
```
这就是差分约束,现在运行你的程序:
```
4 3
1 1 2 3
2 3 2 2
3 3 4 1
```
~~点个赞呗~~
**本期图论结束,下期再见!**