存储表示
图的邻接表(稀疏图,一般$\left| E \right|<\ \left| V \right|\log \left| V \right|$时视为稀疏图)数组实现、初始化与插入函数(y 总版本的):
#include<bits/stdc++.h> #define ll long long #define endl "\n" #define mem(vis, num) memset(vis, num, sizeof(vis)); using namespace std; const int maxn = 10001; int h[maxn], ne[maxn], e[maxn], w[maxn]; void get(int a, int b, int q){ ne[idx] = h[a]; e[idx] = b; w[idx] = q; h[a] = idx++; } int idx = 0; mem(h, -1);
遍历
//DFS int st[maxn]; mem(st, 0); int dfs(int u){ st[u] = 1; // st[u] 表示点 u 已经被遍历过 for (int i = h[u]; i != -1; i = ne[i]){ int j = e[i]; if (!st[j]){ dfs(j); } } }
//BFS int st[maxn]; mem(st, 0); queue<int> q; st[1] = 1; // 表示 1 号点已经被遍历过 q.push(1); while (!q.empty()){ int t = q.front(); q.pop(); for (int i = h[t]; i != -1; i = ne[i]){ int j = e[i]; if (!st[j]){ st[j] = 1; // 表示点 j 已经被遍历过 q.push(j); } } }
前面两种遍历在功能上的差异,主要体现为每一步迭代中对新顶点的选取策略不同。比如,BFS 搜索会优先考查更早被发现的顶点,而 DFS 搜索则恰好相反,会优先考查最后被发现的顶点。每一种选取策略都等效于,给所有顶点赋予不同的优先级,而且随着算法的推进不断调整;而每一步迭代所选取的顶点,都是当时的优先级最高者。按照这种理解,包括 BFS 和 DFS 在内的几乎所有图搜索,都可纳入统一的框架。鉴于优先级在其中所扮演的关键角色,故亦称作优先级搜索($priority$-$first$ $search$, $PFS$),或最佳优先搜索($best$-$first$ $search$, $BFS$)。
$PFS$搜索的基本过程和功能与常规的图搜索算法一样,也是以迭代方式逐步引入顶点和边,最终构造出一棵遍历树(或者遍历森林)。如前所述,每次都是引入当前优先级最高(优先级数最小)的顶点$s$,然后按照不同的策略更新其邻接顶点的优先级数。
在无向连通图$G$中,当且仅当删去$G$中的顶点$v$及所有依附于$v$的所有边后,可将图分割成两个或两个以上的连通分量,则称顶点$v$为关节点。没有关节点的连通图叫做重连通图。在重连通图上, 任何一对顶点之间至少存在有两条路径, 在删去某个顶点及与该顶点相关联的边时, 也不破坏图的连通性。一个连通图$G$如果不是重连通图,那么它可以包括几个重连通分量。在一个无向连通图$G$中, 重连通分量可以利用深度优先生成树找到。
从某点出发,深度优先遍历图,各节点的访问次序记为深度优先数$dfn$。其中,深度优先树的祖先节点的$dfn$一定小于子孙节点。原图中未被深度优先搜索访问的边叫做回边。为求解关节点,再定义一个最小深度优先数$low$值,计算方式为:
$$
low\left[ u \right] =\min \left( dfn\left[ u \right] ,\min \left( low\left[ w \right] ,\min \left( dfn\left[ x \right] \right) \right) \right)
$$
其中$w$为$u$的子女,$(u,x)$为点$u$的回边。那么$u$为关节点的充要条件是要么$u$为树根有两个子女;要么$u$不为树根但有一子女$w$使得$low\left[ w \right] \ge dfn\left[ u \right] $成立。
拓扑排序
拓扑排序的前提是有向无环图,同一有向图的拓扑排序未必唯一,拓扑排序也未必存在。有向无环图的拓扑排序必然存在;反之亦然。这是因为,有向无环图对应于偏序关系,而拓扑排序则对应于全序关系。在顶点数目有限时,与任一偏序相容的全序必然存在。
时间复杂度$O(n+m)$,$n$表示点数,$m$表示边数:
//拓扑排序 bool topsort(){ int hh = 0, tt = -1; // d[i] 存储点 i 的入度 for (int i = 1; i <= n; i ++ ){ if (!d[i]){ q[++tt] = i; } } while (hh <= tt){ int t = q[hh++]; for (int i = h[t]; i != -1; i = ne[i]){ int j = e[i]; if (--d[j] == 0) q[++tt] = j; } } // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。 return tt == n - 1; }
最小生成树
连通图$G$的某一无环连通子图$T$若覆盖$G$中所有的顶点,则称作$G$的一棵支撑树或生成树。就保留原图中边的数目而言,支撑树既是“禁止环路”前提下的极大子图,也是“保持连通”前提下的最小子图。
时间复杂度是$O(n^2+m)$,$n$表示点数,$m$表示边数。
//prim 算法 int n; // n 表示点数 int g[N][N]; // 邻接矩阵,存储所有边 int dist[N]; // 存储其他点到当前最小生成树的距离 bool st[N]; // 存储每个点是否已经在生成树中 // 如果图不连通,则返回 INF(值是 0x3f3f3f3f), 否则返回最小生成树的树边权重之和 int prim(){ memset(dist, 0x3f, sizeof dist); int res = 0; for (int i = 0; i < n; i++ ){ int t = -1; for(int j = 1; j <= n; j ++ ){ if(!st[j] && (t == -1 || dist[t] > dist[j])){ t = j; } } //t 是不在已有生成树中的距离最小点 if(i && dist[t] == INF){ return INF; } if(i){ res += dist[t] } st[t] = 1; for(int j = 1; j <= n; j ++ ){ dist[j] = min(dist[j], g[t][j]); } } return res; }
时间复杂度是$O(mlogm)$,$m$表示边数。
//kruskal 算法 int n, m; // n 是点数,m 是边数 int p[N]; // 并查集的父节点数组 struct Edge{ // 存储边 int a, b, w; bool operator< (const Edge &W)const{ return w < W.w; } }edges[M]; int find(int x){ // 并查集核心操作 if(p[x] != x){ p[x] = find(p[x]) } return p[x]; } int kruskal(){ sort(edges, edges + m); for(int i = 1; i <= n; i ++ ){ // 初始化并查集 p[i] = i; } int res = 0, cnt = 0; for(int i = 0; i < m; i ++ ){ int a = edges[i].a, b = edges[i].b, w = edges[i].w; a = find(a); b = find(b); if(a != b){ // 如果两个连通块不连通,则将这两个连通块合并 p[a] = b; res += w; cnt++ ; } } if (cnt < n - 1){ return INF; } return res; }