预备知识
- 二分图:如果一张无向图((V,E))存在点集(A,B),满足(|A|,|B|≥1),(A∩B=empty),(A∪B=V),且对于(x,y∈A)或(x,y∈B),((x,y) otin E),则称这张无向图为二分图,(A,B)分别为二分图的左部和右部。
- 图的匹配:对于一张无向图((V,E)),若存在一个边集(E'),满足(E'sube E),且对于任意(p,qin E'),(p,q)没有公共端点,则称(E')为这张无向图的一组匹配。
- 匹配边/非匹配边:对于任意一组匹配(S),属于(S)的边称为匹配边,不属于(S)的边称为非匹配边。
- 匹配点/非匹配点:匹配边的端点称为匹配点,其他点称为非匹配点。
- 匹配的增广路:如果二分图中存在一条连接两个非匹配点的路径(path),使得非匹配边与匹配边在(path)上交替出现,则称(path)是匹配(S)的增广路。
- 性质1:长度为奇数。
- 性质2:奇数边是非匹配边,偶数边是匹配边。
- 性质3:如果把路径上所有边的状态(是否为匹配边)取反,那么得到的新的边集(S')仍是一组匹配,并且匹配的边数增加了1.
- 最大匹配:在二分图中,包含边数最多的一组匹配。
- 完备匹配:在二分图(((A,B),E))中,设最大匹配为(E'),且有(|A|=|B|=|E'|),则称二分图有完备匹配。
- 最优匹配:对于一张边有边权的二分图,所有最大匹配中边权总和最大的,称为最优匹配。
二分图判定
判定定理
一个无向图是二分图,当且仅当图中不存在奇环。
核心流程
一般应用染色法即可。
- 将所有节点初始化为未染色。
- 从一个未染色的节点(u)开始,染色为(c)。(如果(u)没有前驱,则(c)取(0)或(1)均可)
- 从(u)出发遍历所有与其连接的节点(v),如果:
- (v)未染色,设(c)为节点(u)的相反色,跳转至第2步;
- (v)颜色与(u)相同,则该图不是二分图;
- (v)颜色是(u)的相反色,继续;
可以看出其实是一个DFS的过程。如果给定图是连通图,则一次DFS即可;否则需要遍历每一个点,每次从未染色的点开始DFS。
模板
bool dfs(int u, int c) {
col[u] = c;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].to;
if (col[v] == col[u]) return false;
if (col[v] == -1 && !dfs(v, !col[u])) return false;
}
return true;
}
二分图匹配
二分图最大匹配
判定定理
二分图的一组匹配(S)是最大匹配,当且仅当图中不存在(S)的增广路。
匈牙利算法
- 初始时设匹配(S)为空集,即所有边都是非匹配边。
- 枚举二分图左部上的点(x),给(x)寻找与其相连的右部点(y)尝试匹配,当满足下列条件之一时,匹配成功(找到增广路):
- (y)为非匹配点
- (y)已与(x')匹配,但从(x')出发能找到另一个(y')与之匹配
- 重复第2步,直到找不到增广路。
时间复杂度(O(p imes e+q)),其中(p)为左部点数量,(q)为右部点数量,(e)为图的边数。
一个小小的优化:当左部点数量明显大于右部点数量时,改为枚举右部点。
另(Dinic)跑最大流的时间复杂度是(O(nsqrt e)),其中(n)为节点总数,(e)为图的边数。
模板
bool vis[maxn];
int res[maxn];
//vis[i]记录节点i在试图改变匹配对象时成功与否
//res[i]记录节点i的匹配对象
bool match(int x) {
//注意,参数x都是左部点
for (int i = head[x]; i; i = e[i].next) {
int y = e[i].to;
if (!vis[y]) {
//对于左部点x而言,右部点y还没有试图“腾出来”过
vis[y] = true;
//尝试了就只有两种结果:y要么最终配上了x,要么实在腾不出来
//无论是哪一种,y都没必要再次尝试“腾出来”了,所以只试一次就行
if (!res[y] || match(res[y])) {
res[x] = y;
res[y] = x;
//这里默认左部点和右部点的编号没有重复的
return true;
}
//y还没有匹配过,或y的匹配对象x'可以找到新的匹配对象
//则本次x与y的匹配成功
}
}
return false;
}
int main() {
...
int ans = 0;
for (int i = 1; i <= p; i++) {
memset(vis, false, sizeof(vis));
//对于枚举的每一个左部点,右部点的状态都是还没尝试过
if (match(i)) ans++;
}
...
}
二分图最优匹配
对于一张边有边权的二分图,所有最大匹配中边权总和最大的,称为最优匹配。
KM算法
- 对于每一个左部点,以与它相连的所有边中的最大边权,给它赋一个期望值。对于每一个右部点,期望值设为0.
- 枚举每一个左部点,开始匹配。原则:只选择边权与左右部点期望值之和相同的边。
- 如果匹配成功,继续枚举。如果找不到符合要求的未匹配的边,那么尝试让符合要求的已匹配的边的右部点改换匹配对象。这一步与匈牙利算法大致一样,可以看作是寻找增广路(path)。
- (path)上的所有左部点期望值(-z);(path)上的所有右部点期望值(+z).其中(z)为能使左部点找到新边的最小改变量。
- 重复第3步。
注意:在匈牙利算法中,(vis)数组是用于记录右部点(y)是否已尝试过;而在KM算法中,(vis)数组既有前述功能,也标记了节点(i)是否在增广路(path)上,以便期望值的增减。所以每枚举一个左部点,都要令vis[x]=true
.
模板
int gap;
//记录能使节点配对成功的最小改变量
bool match(int x) {
vis[x] = true;
//别漏了这一步
for (int i = head[x]; i; i = e[i].next) {
int y = e[i].to;
if (!vis[y]) {
int tmp = val[x] + val[y] - e[i].dis;
if (tmp == 0) {
vis[y] = true;
if (!res[y] || match(res[y])) {
res[x] = y;
res[y] = x;
return true;
}
}
else if (tmp > 0) {
gap = min(gap, tmp);
}
}
}
return false;
}
void km() {
for (int i = 1; i <= n; i++) {
while (1) {
gap = INF;
memset(vis, false, sizeof(vis));
if (match(i)) break;
//找不到符合要求的边,降低期望,重新尝试匹配
for (int i = 1; i <= p; i++) {
if (vis[i]) val_x[i] -= gap;
}
//左部点降低期望
for (int i = 1; i <= q; i++) {
if (vis[i]) val_y[i] += gap;
}
//右部点提高期望
}
}
}
二分图最小点权覆盖集
定义
- 图的点覆盖:对于无向图((V,E)),若存在一个点集(V'sube V),对于任意(ein E),(e)至少有一个端点属于(V'),则称(V')为图的一组点覆盖
- 二分图最小点权覆盖:在二分图中,包含点最少/点权最小的一组点覆盖为最小点权覆盖。
定理
最小点覆盖(所包含的点数)(=) 最大匹配(包含的边数)
二分图最大独立集
定义
- 图的独立集:对于无向图((V,E)),若存在一个点集(V'),满足(V'sube V),且对于任意(p,qin V'),边((p,q) otin E),则称(V')为这张图的独立集。
- 图的最大独立集:包含点数最多的独立集。
- 图的团:对于无向图((V,E)),若存在一个点集(V'),满足(V'sube V),且对于任意(p,qin V'),边((p,q)in E),则称(V')为这张图的一组团。
- 图的最大团:包含点数最多的团。
定理
对于一张(n)个节点的二分图,设其最大独立集为(V'),最小点覆盖集为(A'),最大匹配为(B'),有(|V'|=n-|A'|=n-|B'|)
DAG最小路径覆盖
定义
- 最小不相交路径覆盖:能覆盖所有节点且互不相交的路径的最少数量。
- 最小可相交路径覆盖:能覆盖所有节点且可以相交的路径的最少数量。
- 拆点二分图:设DAG的节点总数为(n),将每个节点拆成编号为(x)和(x+n)的两个点。建立一张新的二分图,其中编号(1)$n$的节点为左部,编号$n+1$(2n)的节点为右部。对于原图的每条有向边((x,y)),在二分图的左部点(x)与右部点(y+n)之间连边。最后得到的二分图为原图的拆点二分图。
定理
DAG的最小不相交路径覆盖=原图的节点数 - 新图的最大匹配
DAG的最小可相交路径覆盖的求法:用(Floyd)对原图求传递闭包,即可转化为求最小不相交路径覆盖。
模型要素
二分图匹配模型
- 点能分成内部没有边的两个集合
- 每个点只能与1条匹配边相连
二分图最小点覆盖模型
- 每条边有两个端点,二者至少选择一个