暑假第一周笔记
Wee1
心得:本周共学习了两个大的图论知识点,其中也可以套许多图论算法解决问题,最难的还是最先确定模型和建图的过程
Part1 二分图
1. 定义
二分图是指图中有两个部分 $x$ 和 $y$,且图中每条边连接的两个顶点一定是一个位于 $x$ ,一个位于 $y$ .
2.匹配
所谓匹配就是一个图的边集,集合中任意两条边都没有公共顶点
例如这张图$M ${(A,C),(B,F)}就是一个匹配:
这里还有一个概念,未盖点 它的意义为如果点$v$不与任何一条属于$M$的边相关联,则称点$v$是一个未盖点,比如上图中的点$D,E$都是相对于$M$的未盖点
在这基础上,还引出两个概念:最大匹配和完全匹配
- 最大匹配就是图中这样的边数最多的集合
- 完全匹配则是图中的每个顶点都与匹配中的某条边相关联,也称作完备匹配
3.求最大匹配的算法
这才是二分图的精髓
怎么求最大匹配呢? 目前有两种方法。
- 先在二分图中找出所有的匹配,再在此保留匹配数最大的。但时间复杂度为边数的指数级函数,因此我们需要更高效的算法
- 匈牙利算法
在讲匈牙利算法之前,又先引出一个概念:增广路
增广路的定义(也称增广轨或交错轨):若 $P$ 是图 $G$ 中一条连通两个未匹配顶点的路径,并且属于匹配边集$M$的边和不属$M$的边(即已匹配和未匹配的边)在$P$上交替出现,则称$P$为相对于$M$的一 条增广路径。
增广路的起点和终点都是未盖点(未匹配点), 并且属于$M$的边和不属于$M$的边交替出现 把$P$中原来属于$M$边从$M$中删除,把P中原来不属于$M$边加入 到$M$中,变化后得到的新的匹配$M’$恰好比原匹配多一条边。
所以匈牙利算法就是通过寻找增广路来得到最大匹配
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=10000,M=10000;
bool road[N],Map[N][M];//map[x][y]=true表示点x和点y有边相连
//road[i]记录点i是否已在当前增广路中,防止死循环
int link[N],n,tot,m;//link[i]记录增广路上与i相连的前一个节点的编号,即记录已求出的匹配
//简单的说link[i]用于记录匹配集合中的边
bool Find(int v){
int i=1;//find查找从v点出发是否有可增广路
for(i=1;i<=m;i++){//可改用邻接表 //枚举在下半部分图中与v点相关联的点
if(Map[v][i]&&(!road[i])){//如果该点不在增广路上
road[i]=1;//把i标记为已讨论,防止死循环
if(link[i]==0||Find(link[i])){//i是未匹配点(未盖点) 或者 从i的匹配点出发有可增广路
link[i]=v;//修改与i匹配的点为v
return true;//则从v出发可找到增广路,返回true;
}//则从v出发可找到增广路,返回true;
}
}
return false;//如果从v出发没有增广路,返回false
}
int main(){
scanf("%d%d",&n,&m);//首先读入图结构到map数组中
for(int i=1;i<=n;i++){
int x;
scanf("%d",&x);
for(int j=1;j<=x;j++){
int h;
scanf("%d",&h);
Map[i][h]=1;
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)road[j]=0;//依次从上半部图的点出发,寻找增广路
if(Find(i))++tot;//每找到一条增广路,匹配数加1(最多n个匹配)
}
printf("%d\n",tot);//输出最大匹配数
return 0;
}
算法复杂度为 $O(nm)$,因为要讨论$n$个节点,每个节点又要讨论$m$条边
与最大匹配相关的一些定理
König 定理: 一个二分图的最大匹配数等于这个图的最小点覆盖数。
最小点覆盖: 假如选了一个点就相当于覆盖了和它相关联的所有边。最少需要选择多少个点才能覆盖图中所有的边,这就是最小点覆盖数。
二分图的最大独立集 = 顶点数 - 二分图的最大匹配数
二分图的最小顶点覆盖 = 二分图的最大匹配数
二分图的最小路径覆盖 = 顶点数 - 二分图的最大匹配数
DAG的最小路径覆盖 = 顶点数n - 对应二分图最大匹配数
DAG的最小路径覆盖
DAG$(有向无环图的缩写)$的最小路径覆盖是指,在该图中选出最少的路 径条数,使得图中所有点都被覆盖,且路径间公共无交点。
那解法是什么呢?
我们可以考虑把$n$的点拆成$2n$个点,$x$号点拆为$x$和$x'$两个点,原图中$x$连向$y$的边就从$x$出发,连接一条边到$y$
例如:
心得体会
二分图的模板不难,但建图的思路很难想,需要勤加练习增强
Part 2 连通性
如果无向图 G 的子图是连通图,则称该子图为连通子图。
最大连通子图:将任何不在这个子图中的点加入该子图,它将不再连通。
也就是平常说的连通块。
连通分量:无向图中的最大连通子图。
强连通图:在有向图中,如果这个图的每个点都有路径到达其他任何点,称这个图为“强连通图”。
强连通子图:字面意义。
最大强连通子图:定义同“最大连通子图”
强连通分量(Strong Connected Component, SCC):图中的最大强连通子图。
求强连通分量算法:
Tarjan 算法
在 Tarjan 算法中为每个结点 $u$ 维护了以下几个变量:
- $\textit{dfn}_u$:深度优先搜索遍历时结点 $u$ 被搜索的次序。
- $\textit{low}_u$:在 $u$ 的子树中能够回溯到的最早的已经在栈中的结点。设以 $u$ 为根的子树为 $\textit{Subtree}_u$。$\textit{low}_u$ 定义为以下结点的 $\textit{dfn}$ 的最小值:$\textit{Subtree}_u$ 中的结点;从 $\textit{Subtree}_u$ 通过一条不在搜索树上的边能到达的结点。
一个结点的子树内结点的 dfn 都大于该结点的 dfn。
从根开始的一条路径上的 dfn 严格递增,low 严格非降。
按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索,维护每个结点的 dfn
与 low
变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。在搜索过程中,对于结点 $u$ 和与其相邻的结点 $v$($v$ 不是 $u$ 的父节点)考虑 3 种情况:
- $v$ 未被访问:继续对 $v$ 进行深度搜索。在回溯过程中,用 $\textit{low}_v$ 更新 $\textit{low}_u$。因为存在从 $u$ 到 $v$ 的直接路径,所以 $v$ 能够回溯到的已经在栈中的结点,$u$ 也一定能够回溯到。
- $v$ 被访问过,已经在栈中:根据 low 值的定义,用 $\textit{dfn}_v$ 更新 $\textit{low}_u$。
- $v$ 被访问过,已不在栈中:说明 $v$ 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
对于一个连通分量图,我们很容易想到,在该连通图中有且仅有一个 $u$ 使得 $\textit{dfn}_u=\textit{low}_u$。该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 dfn 和 low 值最小,不会被该连通分量中的其他结点所影响。
因此,在回溯的过程中,判定 $\textit{dfn}_u=\textit{low}_u$ 是否成立,如果成立,则栈中 $u$ 及其上方的结点构成一个 SCC。
实现
int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc; // 结点 i 所在 SCC 的编号
int sz[N]; // 强连通 i 的大小
void tarjan(int u) {
low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
for (int i = h[u]; i; i = e[i].nex) {
const int &v = e[i].t;
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (in_stack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
++sc;
while (s[tp] != u) {
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
}
Kosaraju 算法
过程
该算法依靠两次简单的 DFS 实现:
第一次 DFS,选取任意顶点作为起点,遍历所有未访问过的顶点,并在回溯之前给顶点编号,也就是后序遍历。
第二次 DFS,对于反向后的图,以标号最大的顶点作为起点开始 DFS。这样遍历到的顶点集合就是一个强连通分量。对于所有未访问过的结点,选取标号最大的,重复上述过程。
两次 DFS 结束后,强连通分量就找出来了,Kosaraju 算法的时间复杂度为 $O(n+m)$。
实现
// g 是原图,g2 是反图
void dfs1(int u) {
vis[u] = true;
for (int v : g[u])
if (!vis[v]) dfs1(v);
s.push_back(u);
}
void dfs2(int u) {
color[u] = sccCnt;
for (int v : g2[u])
if (!color[v]) dfs2(v);
}
void kosaraju() {
sccCnt = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i]) dfs1(i);
for (int i = n; i >= 1; --i)
if (!color[s[i]]) {
++sccCnt;
dfs2(s[i]);
}
}
Part 3 心得体会
本周学习的都是图论算法,有些概念和知识点在课上不是很明白,下来查找资料才慢慢理解透了,还有做题的时候也不是很快就能想到这道题考什么,需要和哪些以前学过知识点融合起来,并且有时还会忘了旧的知识点,要重头复习,总之我会继续努力的。
————by wilbur