- 图论
学好图论的基础:
必须意识到图论hendanteng
xuehuifangqi(雾
- 图
G = (V,E)
一般来说,图的存储难度主要在记录边的信息
无向图的存储中,只需要将一条无向边拆成两条即可
- 存图:
1.邻接矩阵(经典):代码连接
用一个二维数组 edg[N][N] 表示
edg[i][j] 就对应由 i 到 j 的边信息
edg[i][j] 可以记录 Bool,也可以记录边权
举个栗子:
0 0 1 0
0 0 1 1
1 1 0 1
0 1 1 0
首先这是个无向图(因为它是对称的),其次,图如下:
缺点:如果有重边有时候不好处理
空间复杂度 O(V2)
点度等额外信息也是很好维护的
关于利用邻接矩阵存图及遍历:
#include <bits/stdc++.h> using namespace std; const int N = 5005; int ideg[N]/*记录入度*/, odeg[N]/*记录出度*/, n, m, edg[N][N]/*邻接矩阵*/; bool visited[N];//标记某个点是否被遍历过 void travel(int u, int distance/*边权*/)//遍历 { cout << u << " " << distance << endl; visited[u] = true; for (int v = 1; v <= n; v++)//遍历u的所有出边 if (edg[u][v] != -1 && !visited[v]) travel(v, distance + edg[u][v]); //if there is an edge (u, v) and v has not been visited, then travel(v) } int main() { cin >> n >> m;//n点数,m边数 memset(edg, -1, sizeof edg);//先初始化数组为-1(这里定没有负边权) memset(visited, false, sizeof visited); for (int u, v, w, i = 1; i <= m; i++)//疑似有向图 cin >> u >> v >> w, edg[u][v] = w, odeg[u]++,/*出度*/ ideg[v]++;//入度 for (int i = 1; i <= n; i++) cout << ideg[i] << " " << odeg[i] << endl; for (int i = 1; i <= n; i++) if (!visited[i]) travel(i, 0); } /* Given a graph with N nodes and M unidirectional edges. Each edge e_i starts from u_i to v_i and weights w_i Output a travelsal from node 1 and output degree of each node. 输出从1开始的遍历 并且 输出每个点的度数 */
2.邻接表:
对每一个点 u 记录一个 List[u],包含所有从 u 出发的边
手写链表(前向星/指针)
指针:
(我的内心os只剩下这个了)(真的没看懂啊)
前向星:
这个之前介绍过了,嗯。详情见:【图论】最短路问题之spfa
wyx的和上面介绍的差不多,至少思路是一样的qwq:
#include <bits/stdc++.h> using namespace std; const int N = 5005; struct edge { int u, v, w, next; }edg[N]; int head[N]; //List[u] stores all edges start from u int ideg[N], odeg[N], n, m, cnt; //cnt: numbers of edges bool visited[N]; void add(int u, int v, int w) { int e = ++cnt; edg[e] = (edge){u, v, w, head[u]}; head[u] = e; } void travel(int u, int distance) { cout << u << " " << distance << endl; visited[u] = true; for (int e = head[u]; e ; e = edg[e].next) if (!visited[edg[e].v]) travel(edg[e].v, distance + edg[e].w); //if there is an edge (u, v) and v has not been visited, then travel(v) } int main() { cin >> n >> m; cnt = 0; memset(visited, false, sizeof visited); memset(head, 0, sizeof head); for (int u, v, w, i = 1; i <= m; i++) cin >> u >> v >> w, add(u, v, w), odeg[u]++, ideg[v]++; for (int i = 1; i <= n; i++) cout << ideg[i] << " " << odeg[i] << endl; for (int i = 1; i <= n; i++) if (!visited[i]) travel(i, 0); } /* Given a graph with N nodes and M unidirectional edges. Each edge e_i starts from u_i to v_i and weights w_i Output a travelsal from node 1 and output degree of each node. */
邻接矩阵中会有很多空余的空间,为了实现变长的数组:
用 STL 中的 vector 实现变长数组
只需要 O(V + E) 的空间就能实现图的存储
关于vector:
vector 本质就是 c++ 模板库帮我们实现好的变长数组
向一个数组 a 的末尾加入一个元素 x a:push_back(x)
询问数组 a 的长度 a:size()
注意: vector 中元素下标从 0 开始
代码实现(存储+遍历):
#include <bits/stdc++.h> using namespace std; const int N = 5005; struct edge { int u, v, w; }; vector<edge> edg[N];//edg[1],edg[2]……都是变长数组 //vector<edge> edg表示edg是一个变长数组 int ideg[N], odeg[N], n, m, cnt; //cnt: numbers of edges bool visited[N]; void add(int u, int v, int w)//加边 { edg[u].push_back((edge){u, v, w}); } void travel(int u, int distance) { cout << u << " " << distance << endl; visited[u] = true; for (int e = 0; e < edg[u].size(); e++)//从0开始枚举直到数组长度 (也就是枚举u的所有出边) if (!visited[edg[u][e].v]) travel(edg[u][e].v, distance + edg[u][e].w); //if there is an edge (u, v) and v has not been visited, then travel(v) } int main() { cin >> n >> m; cnt = 0; memset(visited, false, sizeof visited); for (int u, v, w, i = 1; i <= m; i++) cin >> u >> v >> w, add(u, v, w), odeg[u]++, ideg[v]++; for (int i = 1; i <= n; i++) cout << ideg[i] << " " << odeg[i] << endl; for (int i = 1; i <= n; i++) if (!visited[i]) travel(i, 0); } /* Given a graph with N nodes and M unidirectional edges. Each edge e_i starts from u_i to v_i and weights w_i Output a travelsal from node 1 and output degree of each node. */
- 生成树
给定一个连通无向图 G = (V; E)
E′⊂ E
G′= (V, E′) 构成一棵树
G′就是 G 的一个生成树
我们发现:生成树的数量是很多很多的(指数级别的qwq)
所以,引出————
- 最小生成树:
给定一个 n 个点 m 条边的带权无向图,求一个生成树,使得生成树中最大边权的最小?
数据范围: n; m ≤ 106(不过这真的确定不是瓶颈生成树吗qwq)
解决算法:
1.Kruskal
2.Prim
3.Kosaraju
写在算法之前:
并查集:
生成树的本质:选取若干条边使得任意两点联通;
维护图中任意两点的连通性
查询任意两点连通性
添加一条边,使两个端点所在连通块合并
Kruskal:
算法流程:
1.将原图无向边按照边权从小到大排序;
2.找到当前边权最小的边e(u,v);
如果u和v已经连通,则直接删除这条边
(判断是否连通:利用并查集来判断,如果在连接之前已经在一个并查集中,则为环,不能加入)
如果u和v未连通,将之加入生成树;
3.重复上述过程;
感性证明:
显然我们最后要把两个连通块连起来,现在找最小的连起来,比以后连起来和要小(不严谨qwq)
something amazing:
Rigorous proof:
消圈算法:在图上找到一个圈,删掉权值最大的一条
理性代码(这是我见到的第一个用万能头的老师)
#include <bits/stdc++.h> using namespace std; const int maxn = 1000005; struct edge { int u, v, w; }edg[maxn]; int n, m, p[maxn], ans = 0; bool cmp(edge a, edge b)//cmp {return a.w < b.w;} int findp(int t) //找“父亲” {return p[t] ? p[t] = findp(p[t]) : t;} bool merge(int u, int v)//并查集的过程 (即判断是否形成了环) { u = findp(u); v = findp(v); if (u == v) return false; p[u] = v; return true; } int main() { cin >> n >> m; for (int i = 1, u, v, w; i <= m; i++) cin >> u >> v >> w, edg[i] = (edge){u, v, w};//因为只需要访问边,所以只记下来边 sort(edg + 1, edg + m + 1, cmp);//排序 for (int i = 1; i <= m; i++)//从最小到最大进行枚举 if (merge(edg[i].u, edg[i].v)) ans = max(ans, edg[i]. w); cout << ans << endl; }
prim:
先选择1号点,连接所有与1相连的边中最小的连上去,组成一个连通块,然后找到和当前组成的连通块相连的点中最小的加进去松弛;
Kosaraju:
先认为每个点都是孤立的连通块,然后对于每个点,找到和它相连的最小的边,将这两个“连通块”连成一个,重复多次;
- 路径:
P = p0,……, pn 为 u 到 v 的路径
p0 = u, pn = v
对于任意i 属于 [1, n]; 存在e : (pi−1; pi) 属于 E
P 的长度:
length(P) = ∑e∈P length(e)
- 简单路径:
简单来说就是不要闲着没事在图里绕圈圈的路径;
树中的简单路径是唯一的,图中的简单路径不一定是唯一的;
负权环
如果存在一个环,其边权和为负数,则称为负全环;
u=>v存在一个负全环,d(u,v)= −∞
- 最短路径问题(SSP):
给定一个有向图 G,询问 u 到 v 之间最短路径长度
记 d(u, v) 表示 u 到 v 的最短路径长度
为方便起见,不妨规定 u 和 v 不连通时, d(u, v) = +∞
Algorithms for Shortest Path Problem
floyd
Bellman-Ford
SPFA
Dijkstra
四种算法都要掌握qwq因为每一种算法都有各自的优势和缺点,都不能互相替代啊qwq
松弛操作(SSP 算法的本质思想就是不断进行松弛操作):
对于最短路d(u, v),满足:
d(u, v)(注意这里是指最短路径了已经) ≤ d(u, w) + d(w, v)
松弛:记当前算出一个d(u,v)(或许它不是最短的),所以枚举找到更短的,即取min;
- floyd:
开始时,d(u, v)表示从u到v的边权;
用邻接矩阵的形式记录d;
设u和c没有边,d(u, v)=+∞;
u和v有重边,保留最小边权;
三层循环枚举 k; i; j,执行松弛操作:
d(i, j) = min{d(i,j),d(i,k) + d(k, j)};
证明bulabula的:
为什么三层循环完了以后就都是最短值了???:
假设3=>5的最短路径为3=>2=>4=>1=>7=>5
那么4=>7min路:4=>1=>7
k=1的时候,d(4,7)一定是真实值
k=2的时候,d(3,4)一定是真实值
k=3的时候,没有什么影响
k=4的时候,d(2,1)为真实值,那么d(3,7)就为真实值
k=5的时候,没有什么影响
k=6的时候,没有什么影响
k=7的时候,d(1,5)为真实值,那么d(3,5)就为真实值
floyd总结:
大约只能跑n=500
不断枚举松弛操作的中间点k(注意必须先枚举k)
算法时间复杂度O(n^3);
优势:处理出 d 之后,任意两点 SP 能 O(1) 询问;
floyd判负环:
完成floyd后检查是否存在d(u,u)<0;
#include <bits/stdc++.h> using namespace std; const int N = 505; const int inf = 1 << 29; int d[N][N], n, m; int main() { cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++){ if(i==j) d[i][j]=0;//自己到自己为0 else d[i][j] = inf;//先全都设成无穷大 } for (int u, v, w, i = 1; i <= m; i++) cin >> u >> v >> w, d[u][v] = min(d[u][v], w);//取最小的边(min主要针对重边) for (int k = 1; k <= n; k++)//先枚举中间点 for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); }
- 单源最短路:
不需要知道图中任意两个点的SP
只需要知道某一点u到其他点的SP
u 称为源点,单源最短路就是求从 u 出发的最短路
为方便,下面记源点为 S
- Bellman-ford:
将 d(S,u) 简写为 d(u),初始 d(S) = 0, d(u) = +∞
执行 n 次全局松弛操作
枚举图中每条边 e : (u,v,w)
松弛 d(v) = min{d(v),d(u) + w}(思想:DP+贪心)
why?
优势:算法很直观(并不觉得它直观qwq)
算法时间复杂度 O(nm)
判负权环:
再多进行一次全局松弛,如果有 d 被更新,一定有负权环
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; const int inf = 1 << 29; struct edge{ int u, v, w; }edg[N]; int d[N], n, m, S; int main() { cin >> n >> m >> S; for (int i = 1; i <= n; i++) d[i] = inf;//初始化为inf for (int u, v, w, i = 1; i <= m; i++) cin >> u >> v >> w, edg[i] = (edge){u, v, w};//输入,加边 d[S] = 0;//源点到源点的单源最短路显然为0 for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) { int u = edg[j].u, v = edg[j].v, w = edg[j].w; d[v] = min(d[v], d[u] + w);//emmm只可意会不可言传怎么办qwq } }
- SPFA简单介绍(因为已经专门写过博客了qwq):
(Bellman Ford的优化)
没有必要把所有点的出边全部更新,可以只用一部分的点来更新(用来更新的点是上一次被更新的,因为它的值发生了改变,所以需要更新qwq);
算法何时终止?
记录每个点加入 Queue 的次数
u 被加入 Queue 一次,意味着 d(u) 被更新了一次
u 最多进入队列 n − 1 次,否则肯定有负权环
时间复杂度肯定不劣于 Bellman-ford,实际远不到 O(nm)
(毒瘤数据还是会达到O(nm)(网格图类型),最优可以到O(2m))
网格图会炸掉=>我们可以在做题时利用它来判断是不是网格图。如果使用SPFA跑某个图很慢的话,八九不离十是网格图;
我爱spfa
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; const int inf = 1 << 29; struct edge{ int u, v, w; }; vector<edge> edg[N]; int d[N], n, m, S; queue<int> Queue; bool inQueue[N]; int cntQueue[N]; void add(int u, int v, int w) { edg[u].push_back((edge){u, v, w}); } int main() { cin >> n >> m >> S; for (int i = 1; i <= n; i++) d[i] = inf; for (int u, v, w, i = 1; i <= m; i++) cin >> u >> v >> w, add(u, v, w); d[S] = 0; inQueue[S] = true; Queue.push(S); while (!Queue.empty()) { int u = Queue.front(); Queue.pop(); inQueue[u] = false; for (int e = 0; e < edg[u].size(); e++) { int v = edg[u][e].v, w = edg[u][e].w; if (d[v] > d[u] + w) { d[v] = d[u] + w; if (!inQueue[v]) { Queue.push(v); ++cntQueue[v]; inQueue[v] = true; if (cntQueue[v] >= n) {cout << "Negative Ring" << endl;/*判断负权环*/ return 0;} } } } } for (int i = 1; i <= n; i++) cout << d[i] << endl; }
- dijkstar:
适用于无负权边的图
在Queue中的点d不会再更新
detail:怎么找到不在Queue中最下的u???
法1:
暴力扫描:
从原点s开始,找权值最小的一条边,它的对应点为i,加入队列,松弛与i相连的点。再找离点i权值最小的点……以此循环往复。
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; const int inf = 1 << 29; struct edge{ int u, v, w; }; vector<edge> edg[N];//表示每一个edg[i]都是一个动态数组 int d[N], n, m, S; bool relaxed[N];//表示一个点是否在队列里 1不在 0在 /*struct Qnode { int u, du; bool operator<(const Qnode &v) const {return v.du < du;} }; priority_queue<Qnode> PQueue;*/ void add(int u, int v, int w) { edg[u].push_back((edge){u, v, w}); } int main() { cin >> n >> m >> S; for (int i = 1; i <= n; i++) d[i] = inf; for (int u, v, w, i = 1; i <= m; i++) cin >> u >> v >> w, add(u, v, w); d[S] = 0; for (int i = 1; i <= n; i++) { int u = 1; while (relaxed[u]) ++u;//找到第一个编号不在队列中的u for (int j = 1; j <= n; j++) if (!relaxed[j] && d[j] < d[u]) u = j; //找到距离最小的一个还未被松弛的点 //find a node u not relaxed yet with least(smallest) d(u) relaxed[u] = true;//放进队列 for (int e = 0; e < edg[u].size(); e++) { int v = edg[u][e].v, w = edg[u][e].w; d[v] = min(d[v], d[u] + w);//枚举u所有出边,松弛 } } for (int i = 1; i <= n; i++) cout << d[i] << endl; }
法2:
优先队列
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; const int inf = 1 << 29; struct edge{ int u, v, w; }; vector<edge> edg[N]; int d[N], n, m, S; bool relaxed[N]; struct Qnode {//堆内的元素 int u, du; bool operator<(const Qnode &v)//重载emm显然我不会写 const/*划重点不能少*/ {return v.du < du;}//表示每次取堆顶的最小值 }; priority_queue<Qnode> PQueue;//类型 Qnode void add(int u, int v, int w) { edg[u].push_back((edge){u, v, w}); } int main() { cin >> n >> m >> S; for (int i = 1; i <= n; i++) d[i] = inf; for (int u, v, w, i = 1; i <= m; i++) cin >> u >> v >> w, add(u, v, w); d[S] = 0; PQueue.push((Qnode){S, 0});//存储需要更新的点d最小的 while (!PQueue.empty()) { int u = PQueue.top().u; PQueue.pop(); if (relaxed[u]) continue;//已经松弛过 //if edges staring from u are already relaxed, no need to relax again. relaxed[u] = true; for (int e = 0; e < edg[u].size(); e++)//枚举u所有出边 { int v = edg[u][e].v, w = edg[u][e].w; if (d[v] > d[u] + w) { d[v] = d[u] + w;//更新松弛 PQueue.push((Qnode){v, d[v]}); //if d(v) is updated, push v into PQueue } } } for (int i = 1; i <= n; i++) cout << d[i] << endl; }
- DAG(有向无环图):
DAG不要求弱联通:
弱联通:把有向图改为无向图后连通
- 拓扑排序:
有拓扑序的一定是DAG,是DAG的一定有拓扑序
算法??
代码:
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 5; const int inf = 1 << 29; struct edge{ int u, v; }; vector<edge> edg[N]; int n, m, outdeg[N]/*记录出边数*/, ans[N]/*储存答案的*/; queue<int> Queue;//度数为0的点 void add(int u, int v) { edg[u].push_back((edge){u, v}); } int main() { cin >> n >> m; for (int u, v, i = 1; i <= m; i++) cin >> u >> v, add(v, u),/*反着记边(记录某个点的入边)这样可以保证删除时容易删除*/ outdeg[u]++; for (int i = 1; i <= n; i++) if (outdeg[i] == 0) Queue.push(i); for (int i = 1; i <= n; i++) { if (Queue.empty())//如果没有度数为0的点,说明不是DAG {printf("Not DAG"); return 0;} int u = Queue.front(); Queue.pop(); ans[n - i + 1] = u;//因为拓扑最先剔除的应该是最后的,所以倒着存储 for (int e = 0; e < edg[u].size(); e++) { int v = edg[u][e].v; if (--outdeg[v] == 0) Queue.push(v); } } }
如何利用拓扑排序求单源最短路?
已知拓扑排序:3 1 2 7 4 6 5(1为源点)(图片from yy)
https://blog.csdn.net/zzran/article/details/8926295
怎么算一个图的拓扑排序有多少个???不可能,做不了;
- 最近公共祖先LCA:
O(n)做法:
先跳到同一高度,再同时向上跳直到相遇;
只讲树上倍增;
算是递归吧:x的2j层的祖先,等于x的2j-1的祖先的2j-1的祖先
:=赋值的意思(区分前面qwq)
从log2n开始向0枚举(2^j的枚举),如果相遇,则不动,如果未相遇,就向上跳,直到跳的长度为1,那么此时答案就是他们的父亲。
证明:
(MST最小生成树)