前言
本文大概是作者对图论大部分内容的分析和总结吧,\(\text{OI}\)和语文能力有限,且部分说明和推导可能有错误和不足,希望能指出。
创作本文是为了提供彼此学习交流的机会,也算是作者在忙碌的中考后对此部分的复习和延伸吧。
本文顾名思义是探讨\(\text{DFS}\)在图论中的重要作用,可能心情比较好会丢个链接作拓展,下面就步入正文。
目录
1 基础篇
\(1.1\) 图的定义和深度优先搜索
\(1.2\) 图的连通分量和二分图染色
2 进阶篇
\(2.1\) 割顶和桥
\(2.2\) 无向图的双连通分量(\(\text{BCC}\))和有向图的强连通分量(\(\text{SCC}\))
\(2.3\) 二分图匹配问题
关键字
深度优先搜索(\(\text{DFS}\))、图的遍历、连通分量、二分图染色、二分图匹配、割顶、桥、双连通分量、强连通分量、\(\text{Tarjan}\)、增广路。
1 基础篇
总言:这里是\(\text{PJ}\)内容,相对来说较为简单。
1.1 图的定义和深度优先搜索
这一部分比较简单,大佬可以直接跳过~
在\(\text{OI}\)中图被抽象成点和边,边连接着两个顶点,可分成无向边和有向边,所有的点和边组在一起构成一个图,记作\(G=\),\(G\)表示图,\(V,E\)分别表示点集和边集。如下图所示,都可称作图。
图的存储主要有两种:邻接矩阵和邻接表。
邻接矩阵:就是用矩阵的行和列来记录两个结点之间是否有边相连,如果有边\(u \rightarrow v\),则\(e[u,v]=1\),否则为\(0\)。
优点:访问速度\(\text{O}(1)\)。
缺点:占用内存\(\text{O}(n^2)\)。
int e[maxn][maxn]; // 邻接矩阵
void add(int u, int v) { // 添加新边
e[u][v] = e[v][u] = 1; // 无向图
e[u][v] = 1; // 有向图
}
例如中间的图,邻接矩阵即为$$\begin{bmatrix} \text{u\v} & V1 & V2 & V3 & V4 & V5 & V6 \ V1 & 0 & 1 & 0 & 0 & 0 & 0 \ V2 & 0 & 0 & 1 & 0 & 0 & 0 \ V3 & 1 & 0 & 0 & 0 & 0 & 0 \ V4 & 0 & 0 & 0 & 0 & 1 & 0 \ V5 & 0 & 0 & 0 & 1 & 0 & 0 \ V6 & 0 & 0 & 0 & 0 & 0 & 1 \end{bmatrix}$$ 邻接矩阵:就是通过链表的形式将与当前结点有关联的结点连起来。
优点:所需内存大小只与边的多少有关。
缺点:随机访问某条边的速度较慢。不过如果按顺序遍历目标结点速度很快。
// 实现1 : STL
vector e[maxn];
void add(int u, int v) {
e[u].push_back(v);
e[v].push_back(u); // 无向图时使用
}
// 实现2 : 前向星
struct Edge {
int u, v, pre; // e[i]表示第i+1条边,pre表示链接,若为-1则说明已经指向表头
} e[maxn * maxn];
int G[maxn], m; // G[i]表示所构成的i结点有关的结点构成的链的最后一条边,m表示边数
void init() {
m = 0;
memset(G, -1, sizeof(G)); // 清空G数组
}
void add(int u, int v) {
e[m++] = (Edge){u, v, G[u]}; // 添加新边,新边指向边G[u]
G[u] = m-1; // 将G[u]指向新边
// 处理无向图用以下
e[m++] = (Edge){v, u, G[v]};
G[v] = m-1;
}
// summary : 方案2比方案1好在常数较小
// 方案2中边的链接顺序相较于读入顺序相反。如果要一致可以改链接方式
例如最后一个图中,链接的情况:$$\begin{array}{ll} V1 \rightarrow 2 \ V2 \rightarrow 1 \rightarrow 3 \rightarrow 5 \ V3 \rightarrow 2 \rightarrow 4 \rightarrow 6 \ V4 \rightarrow 3 \ V5 \rightarrow 2 \ V6 \rightarrow 3 \end{array}$$ 接着再说深搜(\(\text{DFS}\))和遍历。深搜顾名思义就是一直往下搜索,遇到阻碍再回头一步,再继续向下,直到所有的情况都搜索过。
深搜用于遍历图的话,好处很多,比如说代码短小精悍且复杂度为线性。对于上面最后一个图,如果起点在\(1\)号结点,那么访问的顺序:\(1\rightarrow 2 \rightarrow 3 \rightarrow 6 \rightarrow 4 \rightarrow 5\)。
// 在此代码之后全部都采用前向星存储图
bool vis[maxn]; // 是否访问过某结点
void dfs(int u) {
vis[u] = 1; // 访问过的标记
cout << u; // 输出遍历顺序
for (register int i = G[u]; ~i; i = e[i].pre) { // 遍历邻接表,~i表示当i=-1时结束
int v = e[i].v; // 边指向的结点
// do something before dfs
if (!vis[v]) dfs(v); // 若未访问过指向的结点,访问
// do something after dfs
}
}
// 这个代码展现了dfs的基本框架,下文及以后的dfs基本上与此大同小异
1.2 图的连通分量和二分图染色
连通分量:在无向图中,如果从结点\(u\)可以到达结点\(v\),那么结点\(v\)必然可以到达结点\(u\)(对称性);如果从结点\(u\)可以到达结点\(v\),而结点\(v\)可以到达结点\(w\),则结点\(u\)一定可以到达结点\(w\)(传递性),再加上原地不动的话,结点自身可以到达自身(自反性),这些结点满足等价关系,可以组成一个等价类,我们把这些相互可达的结点称作一个连通分量(\(\text{CC, connected component}\))。例如下面的图,有\(3\)个连通分量,分别为\(\{1,2,3,4\},\{5,6,7\},\{8\}\)。
原理:找到一个未标记的点,然后将所有能够直接或间接到达的结点全部标记。不断重复其操作。
int cc[maxn], cc_cnt; // 记录结点所在连通分量的编号,同时若cc不为0,则说明该结点被访问过
void dfs(int u) {
cc[u] = cc_cnt; // 标记连通分量的编号
for (register int i = G[u]; ~i; i = e[i].pre) {
int v = e[i].v;
if (!cc[v]) dfs(v); // 继续访问
}
}
void work() {
cc_cnt = 0; // 清空连通分量数
memset(cc, 0, sizeof(cc)); // 清空 标号&&访问
for (register int i = 1; i <= N; i++)
if (!cc[i]) { // 没被标记
cc_cnt++; // 新的连通分量
dfs(i); // 将所有能访问到的连通分量访问
}
}
二分图:如果一个图\(G=\),将\(V\)分成\(X\)和\(Y=V-X\),能使得\(E\)中任意一条边,两个端点分别在\(X\)集和\(Y\)集中,则此图为二分图。下图的左图即为二分图,而右图不是。