图论(未完成)
-1 最短路径
最短路径是指在一个有权图中,一个点到另一个点的路径中,边权和最小的路径。
在本章所有内容中,我们始终定义
-1.1 单源最短路径
单源最短路径是指单起点,多终点的最短路径。
在本节中,我们始终定义
-1.1.1 Bellman-Ford
Bellman-Ford 是一种最基础的求解单源最短路径的算法,其整体思想是暴力,但是用途十分广泛。
具体实现中,该算法将
正确性证明:在一个有
同时,Bellman-Ford 算法还可以处理有负权边的图的最短路,并且可以判断负环。
但是此算法由于其时间复杂度过高,为
核心代码:
for(int i = 1; i < n; i++)//枚举n - 1轮 for(int j = 1; j <= m; j++)//枚举每一条边 { int x = e[j].x, y = e[j].y, w = e[j].w; if(dis[x] + w < dis[y])//松弛操作 dis[y] = dis[x] + w; }
-1.1.1.1 SPFA
关于 SPFA,它死了。
SPFA 是 Bellman-Ford 的队列优化版本,在中国于 1994 年由西南交通大学的段凡丁提出。
在 Bellman-Ford 算法中,每一条边都会被松弛
算法过程中,只要点
关于算法的时间复杂度,段凡丁在
模板题P3371 【模板】单源最短路径(弱化版)代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e4 + 5; int n, m, s, dis[N]; bool vis[N]; struct node { int y, w; }; vector<node> e[N]; void spfa(int s) { for(int i = 1; i <= n; i++) dis[i] = INT_MAX; queue<int> q; q.push(s);//源点入队 vis[s] = true;//标记入队 dis[s] = 0; while(!q.empty()) { int x = q.front(); q.pop(); vis[x] = false;//出队标记 for(auto i : e[x])//不会还有人不会用auto吧 { int y = i.y, w =i.w; if(dis[x] + w < dis[y])//松弛操作 { dis[y] = dis[x] + w; if(vis[y])//如果已经在队中,跳过 continue; q.push(y); vis[y] = true; } } } return; } signed main() { cin >> n >> m >> s; for(int i = 1; i <= m; i++) { int x, y, w; cin >> x >> y >> w; e[x].push_back({y, w}); } spfa(s); for(int i = 1; i <= n; i++) cout << dis[i] << " "; return 0; }
-1.1.1.2 负环
如果一个图中含有边权和为负的回路,那么这个回路就称作负环。
如何判断一个图中有没有负环呢?我们可以使用 Bellman-Ford 或 SPFA 算法来判定。
在 Bellman-Ford 算法的讲述中,我们提到:
任意两点的最短路至多经过
个点。
那么有没有一种情况,使得两点的最短路经过超过
那么,在跑 SPFA 时,只要一个点入队超过
模板题P3385 【模板】负环代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e4 + 5; struct node { int y, w; }; int t, n, m, dis[N], cnt[N]; bool vis[N]; vector<node> e[N]; void init() { for(int i = 1; i <= n; i++) e[i].clear(); memset(dis, 0x3f, sizeof(dis)); memset(cnt, 0, sizeof(cnt)); memset(vis, false, sizeof(vis)); return; } bool spfa(int s) { queue<int> q; dis[s] = 0; q.push(s); vis[s] = true; while(!q.empty()) { int x = q.front(); q.pop(); vis[x] = false; for(auto i : e[x]) { int y = i.y, w = i.w; if(dis[x] + w < dis[y]) { dis[y] = dis[x] + w; cnt[y] = cnt[x] + 1; if(cnt[y] >= n)//如果入队次数大于n - 1,就有负环 return false; if(vis[y]) continue; q.push(y); vis[y] = true; } } } return true; } void slove() { cin >> n >> m; init(); for(int i = 1; i <= m; i++) { int x, y, w; cin >> x >> y >> w; e[x].push_back({y, w}); if(w >= 0) e[y].push_back({x, w}); } if(!spfa(1)) cout << "YES\n"; else cout << "NO\n"; return; } signed main() { cin >> t; while(t--) slove(); return 0; }
-1.1.1.3 差分约束
差分约束系统是指:
给出一组包含
的不等式组,求任意一组满足这个不等式组的解或者是否有解(
我们单独来看一看一个不等式
对于每一个不等式
但是需要注意的一点是,如果图中存在负环,那么显然无解,因为不可能出现
模板题P5960 【模板】差分约束代码:
#include<bits/stdc++.h> using namespace std; const int N = 5e3 + 5; struct node { int y, w; }; int n, m, dis[N], cnt[N]; bool vis[N]; vector<node> e[N]; bool spfa(int s) { queue<int> q; memset(dis, 0x3f, sizeof(dis)); q.push(s); vis[s] = true; dis[s] = 0; while(!q.empty()) { int x = q.front(); q.pop(); vis[x] = false; for(auto i : e[x]) { int y = i.y, w = i.w; if(dis[x] + w < dis[y]) { dis[y] = dis[x] + w; cnt[y] = cnt[x] + 1; if(cnt[y] > n) return true; if(vis[y]) continue; q.push(y); vis[y] = true; } } } return false; } int main() { cin >> n >> m; for(int i = 1; i <= m; i++) { int x, y, w; cin >> y >> x >> w; e[x].push_back({y, w});//建边 } for(int i = 1; i <= n; i++) e[0].push_back({i, 0});//超级源点 if(spfa(0))//无解 { cout << "NO"; return 0; } for(int i = 1; i <= n; i++) cout << dis[i] << " "; return 0; }
-1.1.2 Dijkstra
Dijkstra 是一种基于贪心的一种单源最短路径算法,其整体思想是蓝白点。
在算法实现中, 首先将源点标记为蓝点(求出最短路径的点,反之则白点),然后循环
关于正确性,此处引用@Alex_Wei 在初级图论中的正确性证明。
归纳假设已经拓展过的节点
初始令源点
但是需要注意的是,Dijkstra 算法无法处理有负权边的图的最短路,如有负权边,需使用 SPFA 算法。
该算法的时间复杂度为
-1.1.2.1 堆优化 Dijkstra
根据上文所说,Dijkstra 算法是时间复杂度为
在寻找距离源点的最短路最小的白点时,一个一个找显然很慢,而这里我们可以使用小根堆优化。松弛的时候,只要条件成立,就将这个点压入堆中,然后将这些点一个一个取出对它的邻接点进行松弛。
这里的小根堆可以使用 C++ STL 中的 priority_queue
(优先队列)来实现。
此时的时间复杂度降为
模板题P4779 【模板】单源最短路径(标准版)代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int N = 1e5 + 5; int n, m, s, dis[N]; bool vis[N]; struct node { int y, w; }; vector<node> e[N]; void dijkstra(int s) { for(int i = 1; i <= n; i++)//将dis赋为极大值 dis[i] = INT_MAX; priority_queue< pair<int, int> > pq; pq.push(make_pair(0, s));//将源点入队 dis[s] = 0;//源点到源点的距离为0 while(!pq.empty()) { int x = pq.top().second; pq.pop(); if(vis[x])//如果是蓝点,那么跳过 continue; vis[x] = true;//标记为蓝点 for(auto i : e[x]) { int y = i.y, w = i.w; if(dis[x] + w < dis[y])//松弛操作 { dis[y] = dis[x] + w; pq.push(make_pair(-dis[y], y));//入队 } } } return; } signed main() { cin >> n >> m >> s; for(int i = 1; i <= m; i++) { int x, y, w; cin >> x >> y >> w; e[x].push_back({y, w});//建边 } dijkstra(s); for(int i = 1; i <= n; i++) cout << dis[i] << " "; return 0; }
-1.1.3 扩展问题
-1.1.3.4 分层图最短路
分层图最短路的模型是指在一个图上,有
顾名思义,分层图就是将原图进行分层,对于
-1.1.4 例题
P1948 [USACO08JAN] Telephone Lines S
考虑分层图。第
Code:
-1.2 多源最短路径
多源最短路径是指多起点,多终点的最短路径。
-1.2.1 Floyd
Floyd 是一种多源最短路径算法,可以求出图上任意两点之间的最短路。
Floyd 本质是一个 DP。我们需要使用邻接矩阵来存图。
算法实现中,首先需要枚举起点和终点
需要注意的是,枚举时,我们要先枚举中转点
模板题B3647 【模板】Floyd 算法代码:
#include<bits/stdc++.h> using namespace std; const int N = 1e2 + 5; int n, m, dis[N][N]; void floyd() { for(int k = 1; k <= n; k++)//枚举中转点 for(int i = 1; i <= n; i++)//枚举起点 for(int j = 1; j <= n; j++)//枚举终点 dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);//松弛操作 return; } int main() { cin >> n >> m; memset(dis, 0x3f, sizeof(dis)); for(int i = 1; i <= m; i++) { int x, y, w; cin >> x >> y >> w; dis[x][y] = dis[y][x] = min(dis[x][y], w); } for(int i = 1; i <= n; i++) dis[i][i] = 0; floyd(); for(int i = 1; i <= n; i++) { for(int j = 1; j <= n; j++) cout << dis[i][j] << " "; cout << "\n"; } return 0; }
-1.2.1.1 传递闭包
定义一个图的传递闭包为一个
传递闭包可使用 Floyd 算法实现。我们同样枚举中转点
核心代码:
for(int k = 1; k <= n; k++) for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) if(g[i][k] && g[k][j])//如果(i, k)、(j, k)存在路径,那么(i, j)存在路径 G[i][j] = true;
-2 最小生成树
一个无向带权连通图的生成树是指图中的一个极小连通子图,它含有图中全部顶点,但由于其是一棵树,它只含有图中的
图的所有生成树中具有边权和最小的生成树称为图的最小生成树。
-2.1 Kruskal
Kruskal 是一种基于贪心的一种算法,以边为对象进行贪心。
首先我们存储每一条边和两个端点,按照边权从小到大排序。依次枚举每一条边,设这一条边的两个端点分别为
关于正确性,由于我们对所有边从小到大进行了排序,所以每一次加入最小生成树的边一定是最小的。只要一条边的两个端点
关于时间复杂度,由于排序其实才是最慢的一步,所以算法的时间复杂度为
模板题P3366 【模板】最小生成树代码:
#include<bits/stdc++.h> using namespace std; const int M = 2e5 + 5, N = 5e3 + 5; struct node { int x, y, w; }a[M]; int n, m, fa[N], ans, cnt; int find(int x) { if (fa[x] == x) return x; return fa[x] = find(fa[x]); } void merge(int x, int y) { int fx = find(x), fy = find(y); if (fx == fy) return; fa[fy] = fx; return; } void kruskal() { for (int i = 1; i <= n; i++)//并查集初始化 fa[i] = i; for (int i = 1; i <= m; i++) { int fx = find(a[i].x), fy = find(a[i].y); if (fx != fy)//如果不连通,那么加入 { merge(fx, fy); cnt++, ans += a[i].w; } if (cnt == n - 1)//如果已经选了n-1条边了,结束循环 return; } return; } bool cmp(node x, node y) { return x.w < y.w; } int main() { cin >> n >> m; for (int i = 1; i <= m; i++) cin >> a[i].x >> a[i].y >> a[i].w; sort(a + 1, a + 1 + m, cmp);//排序 kruskal();//开始Kruskal if (cnt != n - 1)//如果不连通,输出"orz" { cout << "orz"; return 0; } cout << ans; return 0; }
-2.2 Prim
Prim 算法和 Kruskal 算法一样,都是基于贪心的算法,不过 Prim 算法是以点为对象的算法,也就是蓝白点,与 Dijkstra 算法相同。
我们设最小生成树(蓝点)的顶点集为
在算法实现中, 首先将源点标记为蓝点(求出最短路径的点,反之则白点),然后循环
该算法时间复杂度为
“找出当前所有点中距离
优化后该算法的时间复杂度为
由于一般不使用该算法,因此不再提供代码。
-3 无向图连通性
本章与下一章均与且只与 Tarjan 算法有关。
在这里,笔者不得不说一句:Tarjan 老爷子真是神。
图的连通性问题是图论中非常重要的一部分。
(能说这一段我是抄的Alex_Wei的吗)
-3.1 割点与桥
Tarjan 算法是一种可以在线性时间复杂度内求出无向图的割点与桥的算法,同时也可以在同样的时间内求出无向图的双连通分量,有向图的强连通分量。Tarjan算法基于图的深度优先遍历(DFS)。
在学习该算法前,我们需要了解几个概念:
搜索树
在一个无向连通图中,我们选定一个节点出发,进行深度优先遍历,遍历到的边所组成的一棵树,就称为这个图的搜索树。
时间戳
在图的深度优先遍历中,按照每个节点第一次被访问的顺序,依次给每个节点进行编号,每个点的编号就称作这个点的时间戳,记作
形象化一点,就是在深度优先遍历时,统计每个点第一次搜索到的序号。
追溯值
时间戳是自然存在于 DFS 中的,而在 Tarjan 算法中,还需引入一个新的概念:追溯值。
我们定义节点
关于求值,以
-3.1.1 割点
对于一个连通图,如果一个点
上文我们提到,
参考文章
第二章:
所有章节
参考文献
算法竞赛进阶指南 - 李煜东
本文作者:Luckies
本文链接:https://www.cnblogs.com/Luckies/p/graph_theory.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步