# 图论概念、存储结构及遍历
@@
## something to talk...
- 作业为什么进不去?
- 加入团队
- 为什么 UKE?
- 尝试换语言提交
- 作业什么时候讲?
- 周日 14:30-17:00
- 是不是一定得 C++?
- C++ $>$ Python, C++ $>$ C
- Actually, C+STL
- 我听不懂
- 讲师太菜了
- 新增“推荐阅读”
@@
## 图论
- 源自 Euler 对[七桥问题](https://en.wikipedia.org/wiki/Seven_Bridges_of_K%C3%B6nigsberg)的研究
- 图是由若干给定的顶点及连接两顶点的边所构成的图形,用来描述某些事物之间的某种特定关系
- 顶点用于代表事物,连接两顶点的边则用于表示两个事物间具有这种关系
---
# 图论的基础概念
@@
## 图的形式化定义
- 二元组 $G=\langle V,E\rangle$,其中 $V,E$ 是集合
- 点集 $V$ 中的元素称为顶点/节点/点
- 对于无向图,边集 $E$ 中的元素是无序二元组 $e=(u,v)$,称为无向边,$u,v\in V$ 称为 $e$ 的端点
- 对于有向图,边集 $E$ 中的元素是有序二元组 $e=(u,v)$,称为有向边,$u\in V$ 称为 $e$ 的起点,$v\in V$ 称为 $e$ 的终点
- 不难理解为什么图能反映事物间的关系
- 简单图:没有自环和重边的图
- 自环:$e=(u,u)\in E$
- 重边:$e_1=e_2\in E$
若无歧义,$n=|V|,m=|E|$
@@
## 邻接
对于无向图
- 若 $v$ 是 $e$ 的一个端点,则称 $v$ 和 $e$ 是关联的;对于 $u,v\in V$,若存在 $e$ 与它们均关联,则称 $u,v$ 是相邻的
- 点 $u$ 的邻域 $N(u)$:所有与 $u$ 相邻的点的集合
- 点 $u$ 的度数 $d(u)$:与点 $u$ 关联的边的条数,特别地,边 $(u,u)$ 要对 $d(u)$ 产生 $2$ 的贡献(Well-Defined)
对于有向图
- 点 $u$ 的出度 $d^+(u)$:以 $u$ 为起点的边的条数
- 点 $u$ 的入度 $d^-(u)$:以 $u$ 为终点的边的条数
@@
## 邻接
对于无向简单图,有 $d(u)=|N(u)|$
对于任意无向图有
$$
\sum_{u\in V}d(u)=2|E|
$$
对于任意有向图有
$$
\sum_{u\in V}d^+(u)=\sum_{u\in V}d^-(u)=|E|
$$
@@
## 路
- 途径(walk):一个点和边的交错序列,其中首尾是点:
$$
v_0,e_1,v_2,e_2,v_2,\cdots,e_k,v_k
$$
其中 $e_i$ 的两个端点为 $v_{i-1}$ 和 $v_i$,有时简写为 $v_0\to v_1\to\cdots\to v_k$
- 迹(trail):满足边两两不同的途径
- 路径(path)/简单路径(simple paht):除了 $v_0$ 和 $v_k$ 以外的点两两不同的迹
- 回路:满足 $v_0=v_k$ 的迹
- 环/圈(cycle)/简单回路(simple circuit):满足 $v_0=v_k$ 的简单路径
@@
## 连通性
- 连通:对于 $u,v\in V$,若存在途径 $w$ 使得 $v_0=u,v_k=v$,则称 $u,v$ 是连通的
- 连通性:若 $G=\langle V,E\rangle$ 满足 $\forall u,v\in V$ 均连通,则称 $G$ 是连通图,具有连通性
对于有向图
- 连通
- 若 $u$ 可达 $v$ 且 $v$ 可达 $u$,则称 $u,v$ 是强连通的
- 若 $u$ 可达 $v$ 或 $v$ 可达 $u$,则称 $u,v$ 是弱连通的
- 连通性
- 若 $\forall u,v$ 均强连通,则称 $G$ 是强连通图
- 若 $\forall u,v$ 均弱连通,则称 $G$ 是弱连通图
@@
## 子图,补图与反图
- 子图:对于图 $G=\langle V,E\rangle, H=\langle V',E'\rangle$,若 $V'\subseteq V, E'\subseteq E$,则称 $H$ 是 $G$ 的子图,记作 $H\subseteq G$
- 导出子图/诱导子图:$\forall u,v\in V', (u,v)\in E\implies (u,v)\in E'$,记作 $G[V']$
- 闭合子图:对于有向图 $G$,若 $H=G[V^*]$ 满足 $\forall v\in V^*, (v,u)\in E\implies u\in V^*$
- 补图:无向简单图 $G=\langle V,E\rangle$ 的补图 $\bar G\langle V',E'\rangle$ 满足 $V=V'$ 且 $\forall e\in E\Longleftrightarrow e\notin E'$
- 反图:有向图 $G\langle V,E\rangle$ 的反图 $G'\langle V',E'\rangle$ 满足 $V=V'$ 且 $\forall (u,v)\in E\Longleftrightarrow (v,u)\in E'$
@@
## 特殊图
- 完全图:$\forall u,v\in V,u\neq v$ 有 $(u,v)\in E$
- 链:所有边恰好构成一条简单路径
- 环/圈图:所有边恰好构成一个圈
- 星/菊花图:存在 $v$ 与任意 $u\in V,u\neq v$ 连边,且除 $v$ 以外任意两点间无边
- 树:没有环的无向连通简单图
- 非空的 $n$ 个点的树恰好有 $n-1$ 条边,这既是无环图的边数上界,也是连通图的边数下界
- 森林:若干不相交树的并
- 无向简单图是森林 $\Longleftrightarrow$ 无环
- 基环树:$n$ 个点 $n$ 条边的连通图
- 基环树森林:$n$ 个点 $n$ 条边的图
- $k$ 分图:可以将点集划分为 $k$ 个部分,同一部分内部没有边相连
@@
## 特殊点/边集
- 团:$V'\subseteq V$ 使得 $\forall u,v\in V'$ 都相邻
- (点)独立集:$V'\subseteq V$ 使得 $\forall u,v\in V'$ 都不相邻
- 点覆盖:$V'\subseteq V$ 使得 $\forall (u,v)\in E$ 都有 $u\in V'\lor v\in V'$
- 匹配(边独立集):$E'\subseteq E$ 使得 $\forall e_1,e_2\in E',e_1\neq e_2$ 无公共点
- 边数最多的匹配称为最大匹配;若边带权,权值和最大的匹配称为最大权匹配
- 完美匹配:$\forall u\in V$ 都是匹配的
- 边覆盖:$E'\subseteq E$ 使得 $\forall u\in V$ 都存在 $e\in E'$ 与 $u$ 关联
@@
## 特殊点/边集
对于图 $G$
- 任意一个团都是 $\overline G$ 的一个独立集,任意一个独立集都是 $\overline G$ 的团
- 任意一个点覆盖的补集都是 $G$ 的一个独立集,任意一个独立集的补集都是 $G$ 的点覆盖
判定一张图是否存在大小为 $k$ 的团/独立集/点覆盖/匹配/边覆盖都是 NPC 的
功能性版本:最大团/最大独立集/最小点覆盖/最大匹配/最小边覆盖
@@
## 树
- 树是二分图
- 树上任意两点间只有一条简单路径
指定一个节点作为这棵树的根,那么可以根据每个节点到根的距离对节点分层
- 节点的深度/高度:到根的距离,记作 $dep_v$
- 树高/树深:深度最大的节点的深度
对于树上的一条边 $e=(u,v)$,不妨设 $dep_u<dep_v$
- $u$ 称为 $v$ 的父亲,$v$ 称为 $u$ 的儿子,记作 $fa_v=u,v\in son_u$
- $v$ 的祖先:从根到 $u$ 的所有节点都是 $v$ 的祖先
- $u$ 的子孙:所有祖先为 $u$ 的节点
- $u$ 的子树:$u$ 和 $u$ 的子孙
- 叶子:没有儿子的节点
@@
## 树
- $k$ 叉树:所有节点的儿子个数不超过 $k$
特别地,当 $k=2$ 时有
- 二叉树:所有节点的儿子个数不超过 $2$
- 一个节点有不超过两个儿子,分别称为左儿子和右儿子,类似地有左子树和右子树
- 完全二叉树:除最后一层外每一层都是满的
- 满二叉树:每一层都是满的
深度为 $k$ 的满二叉树第 $i$ 层有 $2^{i-1}$ 个节点,一共有 $2^k-1$ 个节点
---
# 图的存储结构
@@
## 邻接矩阵
注意:有 $E\subseteq V^2$
这也是为什么图能用来抽象事物间的关系,并且维护许多信息(DSU,2-SAT,差分约束)
存储 $V^2$ 可以使用邻接矩阵,即一个 $n\times n$ 的矩阵 $G$
- 若 $G_{i,j}=1$ 则表示点 $i$ 向点 $j$ 连边
- 对于无向图,有 $G_{i,j}=G_{j,i}, G_{i,i}=1$
- 对于有权图,令 $G_{i,j}$ 表示边 $(i,j)$ 的边权
对于稀疏图($n=10^5,m=5\times 10^5$,那么 $m\ll n^2$)造成空间和时间上的浪费
@@
## 邻接表
只关注邻接矩阵中有意义的位置:
- 对于节点 $i$ 存储所有 $i$ 连向的节点
我们需要某个数据结构能够支持:每个节点存储的信息不一样多,可能达到 $O(n)$,但是总大小不超过 $O(m)$
使用 STL 内置的变长数组 `vector`
@@
## 邻接表
设点数上界为 $N$
- 声明(全局):`vector<int> G[N];`
- 加边 $(u,v)$:`G[u].push_back(v)`
- 无向图需要双向建边
- 遍历 $u$ 的出边:
```cpp
for(int i = 0; i < G[u].size(); ++i) {
int v = G[u][i]; // u -> v
}
```
@@
## 邻接表
若有边权,则存储的内容从 $v$ 变为 $(v,w)$:
```cpp
vector<pair<int, int>> G[N]; // 声明
G[u].push_back({v, w}) // 加边
for (int i = 0; i < G[u].size(); ++i) { // 遍历
int v = G[u][i].first, w = G[u][i].second;
}
```
对于现代 C++
```cpp
G[u].emplace_back(v); // C++11
for (int v : G[u]) { // C++11
//...
}
G[u].emplace_back(v, w); // C++11
for (auto[v, w] : G[u]) { // C++17
//...
}
```
@@
## 链式前向星
本质与邻接表相同,不过数据结构部分使用链表实现
- 声明:`int head[N], nex[M*2], to[M*2], _w[M*2], cnt;`
- 加边:
```cpp
void add(int u, int v, int w) {
++cnt;
nex[cnt] = head[u]
to[cnt] = v;
_w[cnt] = w;
head[u] = cnt;
}
```
- 遍历:
```cpp
for (int i = head[u]; i; i = nex[i]) {
int v = to[i], w = _w[i];
}
```
@@
## 三种存储方式的比较
- 邻接矩阵
- 优点:直观,方便查询两点间边的信息
- 缺点:时空复杂度大,不便遍历
- 邻接表:
- 优点:遍历复杂度低($O(d(u))$)
- 缺点:无法直接查询两点间信息(用哈希表/map 也可以做到)
- 链式前向星
- 优点:直观展现边的信息(欧拉回路/网络流常用)
- 缺点:代码稍长
---
# 图的遍历
@@
## DFS/BFS
把 DFS/BFS 放在具体的图上即可
- 为了保证复杂度,记录每个节点是否访问过(`vis[]`)
```cpp
void dfs(int u) {
if (vis[u]) return ;
vis[u] = 1;
for (int v : G[u]) {
dfs(v);
}
}
```
@@
## DFS/BFS
```cpp
auto bfs = [](int s) { // C++11
queue<int> q;
q.emplace(s); // C++11, C++98 为 push
vis[s] = 1;
d[s] = 0;
while (q.size()) {
int u = q.front();
q.pop();
for (int v : G[u]) {
if (vis[v]) continue;
vis[v] = 1;
d[v] = d[u] + 1;
q.emplace(v);
}
}
};
```
@@
## DFS/BFS
DFS 可以将原图的边分类:树边和非树边(有向图的非树边:前向边,后向边和横叉边)
- DFS/BFS 树:DFS/BFS 给出的一棵生成树
- DFS/BFS 序:以 DFS/BFS 访问的顺序记录每个点
- 括号序:当 DFS 进入一个点时记录一个左括号,退出一个点时记录一个右括号
@@
## 例 1:[Fence Planning](https://www.luogu.com.cn/problem/P5429)
> 有 $n(n\le 10^5)$ 头奶牛,第 $i$ 头奶牛的位置在 $(x_i,y_i)$,同时它们之间有 $m(m\le 10^5)$ 对“哞叫”关系,两头互相哞叫的奶牛属于同一个“哞网”。
>
> 现在你需要划出一个周长最小的矩形围栏(要求边与坐标轴平行),使得这个围栏包含至少一个完整的“哞网”。
- 简化&抽象题意:给定二维上的 $n$ 个点,有 $m$ 条无向边,找出周长最小的矩形(四边与坐标轴平行)使其包含一个闭合子图。
- 事实上给出了这 $n$ 个点的一个划分(等价关系),对每个(极大)连通块做 DFS 并更新最上/下/左/右的点坐标即可
@@
## 例 2:[Flight Routes Check](https://vjudge.net/problem/CSES-1682)
> There are $n(n\le 10^5)$ cities and $m(m\le 2\times 10^5)$ flight connections. Your task is to check if you can travel from any city to any other city using the available flights.
- 简化&抽象题意:给定一张有向图,判断其是否为强连通图
- Kosaraju 算法:建立图 $G$ 的反图 $G'$,任选点 $v$ 在 $G,G'$ 分别跑一次 DFS,判断在 $G,G'$ 中 $v$ 是否均可达任意一点
- 正确性:
- 必要性显然
- 对于充分性,对 $\forall u\in V,u\neq v$,都存在 $G$ 上的路径 $p:v\to u$ 和 $G'$ 上的路径 $p':v\to u$,将 $p'$ 反向,说明 $G$ 上 $u$ 可达 $v$,即任意一点可达 $v$ 且 $v$ 可达任意一点
@@
## 树的遍历
- 对于树 DFS 时,无需使用 `vis[]` 数组
```cpp
void dfs(int u, int fa) {
for (int v : G[u]) {
if (v == fa) continue;
dfs(v, u);
}
}
```
- 对于二叉树而言,根据记录根与儿子访问的先后顺序分为前序遍历(根左右)、中序遍历(左根右)和后序遍历(左右根)
- 已知前序+中序或后序+中序可以推出另一个
- 已知前序+后序不一定能推出中序
@@
## 例 3:[Journey](https://www.luogu.com.cn/problem/CF839C)
> 给出一个 $n(n\le 10^5)$ 个点的有根树,你从根出发,每次等概率随机走到一个儿子上,问期望多少次走到叶子。
- 设 $f_u$ 表示从 $u$ 出发,走到叶子的期望步数
$$
f_u=\dfrac 1{|son_u|}\sum_{v\in son_u}(f_v+1)
$$
- DFS 回溯时转移即可
```cpp
for (int v : G[u]) {
if (v == fa) continue;
dfs(v, u), ++cnt;
f[u] += f[v] + 1;
}
f[u] /= cnt;
```
@@
## 例 4:[Milk Visits](https://www.luogu.com.cn/problem/P5836)
> 给出一个 $n(n\le 10^5)$ 个点的树,每个点有 `G` 或 `H` 中的某一种颜色,$q(q\le 10^5)$ 次询问点 $a$ 和 $b$ 之间的路径上是否含有颜色为 $c$ 的点。
- 对每个节点 $u$ 维护 $f_u$:从 $u$ 开始向上走与 $u$ 同色的点至多走到哪里
- $f_a=f_b\Longleftrightarrow $ $a$ 和 $b$ 的路径上颜色都相同
- 设 $u$ 的父亲是 $p$
`$$
f_u=\begin{cases}
u & c_u\neq c_{p}\\
f_{p} & c_u=c_{p}\\
\end{cases}
$$`