二分图最小点覆盖构造方案+König定理证明
前言
博主很笨 ,如有纰漏,欢迎在评论区指出讨论。
二分图的最大匹配使用 \(Dinic\) 算法进行实现,时间复杂度为 \(O(n\sqrt{e})\),其中, \(n\)为二分图中左部点的数量, \(e\) 为二分图中的边数。若是匈牙利算法,时间复杂度为 \(O(nm)\) , \(m\) 为二分图中右部点的数量,不建议使用。
König定理
定理内容:二分图最小点覆盖的点的数量等于二分图最大匹配的边的数量。
构造方法 \(+\) 简单证明:
首先求出二分图中的最大匹配,建议使用 \(Dinic\) 。
从每一个非匹配点出发,沿着非匹配边正向进行遍历,沿着匹配边反向进行遍历到的点进行标记。选取左部点中没有被标记过的点,右部点中被标记过的点,则这些点可以形成该二分图的最小点覆盖。
遍历代码实现如下:
void dfs(int now) {
vis[now] = true;
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i].to;
if(vis[next] || !v[now][i].val)//正向边的容量为0说明是匹配边,反向边的容量为0说明是非匹配边
continue;
dfs(next);
}
}
那么就有以下性质:
- 若该点为左边的非匹配点,则这个点必被访问,因为这个点是整个 \(dfs\) 的起点
- 若该点为右边的非匹配点,则这个点必不会被访问,若是由左边的非匹配点才到达了这个点,那么可以将这条边变为匹配边,则匹配数 \(+1\) ,与最大匹配相冲突。若是左边的匹配点才到达了这个点,那么这个点的路径为左边非匹配点 → 右边匹配点 → 左边非匹配点 → 右边匹配点 → …… → 左边匹配点 → 右边非匹配点 ,很明显,上述路径为增广路,与最大匹配相冲突。所以,右边的非匹配点必不会被访问。
- 对于一组匹配点,要么两个都被标记,要么都不被标记。因为左部的匹配点是由右部的匹配点来遍历到的,出现必然成双成对。
有了上述的三条性质,可以发现:按照选取左部点中没有被标记过的点,右部点中被标记过的点的规则,选出来的点的点数必然为最大匹配的边数。左部的非匹配点必然被访问,则必不会被选,右部的非匹配点必不会被访问,则必不会被选。而第三条性质决定了,对于一组匹配点,会选择有且仅有一个点。故而选出的点的点数等于最大匹配的边数。
其次需要解决一个问题:保证这些点覆盖了所有的边。具体可以分为四类:
- 左部为非匹配点,右部为非匹配点。性质二已经讨论过,不可能出现这种情况,出现就不满足最大匹配的前提。
- 左部为匹配点,右部为非匹配点。同理性质二,路径类似,会出现增广路,那么这个左部的匹配点一定没有被访问过,必然被选。
- 左部为匹配点,右部为匹配点。一对匹配点中必选一个。
- 左部为非匹配点,右部为匹配点。这条边为非匹配边,而起点就是从左部的非匹配点点开始,那么右部的这个点必然被访问过,必然被选。
最后在确保这是最小的方案:一条边都只选了一个点,不存在浪费。
如上,证毕。
题目来源:COCI 2019/2020 Contest #6 T4. Skandi
题目大意
给定一个 \(n\times m\) 的矩阵,其中的白色点为 \(0\) , 黑色点为 \(1\) 。黑色点可以往下一直扩展到底部,把白色点变成蓝色点,直到遇到黑色点为止。同理,也可向右扩展。问整个矩阵经过最小多少次扩展才能扩展为整个矩阵到不存在白色,并打印出每次扩展是从哪个点开始的,并打印出扩展方向。题目满足第一行第一列一定为黑色点。
思路
一道建模题。
一个白色点变为蓝色点只有两种方法,从它上方或左方的黑色点扩展而来,且只需要一个点扩展即可。可以考虑到最小点覆盖问题。
由于对于一个黑色点来说,它可以往右或往下扩展。那么它就有两个身份,也就是说一个点拥有两个编号。一个编号为把整个矩阵拉成一条链的顺序,另一个编号为前一个编号 \(+n\times m\) ,这样不会发生冲突。获得编号的函数:
int GetHash(int i, int j) {
return (i - 1) * m + j;
}
那么不难发现一个白色点,与其相关的是一个编号 \(\leqslant n\times m\) 的点,和一个编号 \(>n\times m\) 的点。把这两个点连接起来,就是一张二分图。
问题就转换为找这张图的最小点覆盖问题。使用 \(Dinic\) ,在根据上述 \(König\) 定理构造即可。
边数为白点的个数,左部点为黑点的个数,则时间复杂度为 \(O(nm\sqrt{nm})\) ,即 \(O(n^{\frac{3}{2}}m^{\frac{3}{2}})\) ,本题的 \(n\) , \(m\) 均小于 \(500\) ,大概能够在 \(1s\) 内求出答案。
C++代码
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
#define INF 0x3f3f3f3f
const int MAXN = 1e6 + 5;
const int MAXM = 5e2 + 5;
struct Node {
int to, val, rev;//依次为:下一个点,边的容量,相反的边的编号
Node() {}
Node(int T, int V, int R) {
to = T;
val = V;
rev = R;
}
};
vector<Node> v[MAXN];//用vector存图的癖好...
int dn[MAXN], rt[MAXN];//预处理白色点可以右那两个点扩展而来
queue<int> q;
int de[MAXN], be[MAXN];
int twin[MAXN];
bool vis[MAXN];
int n, m, s, t;
int arr[MAXM][MAXM];
bool bfs() {//将残量网络分层
bool flag = 0;
memset(de, 0, sizeof(de));
while(!q.empty())
q.pop();
q.push(s);
de[s] = 1; be[s] = 0;
while(!q.empty()) {
int now = q.front();
q.pop();
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i].to;
if(v[now][i].val && !de[next]) {
q.push(next);
be[next] = 0;
de[next] = de[now] + 1;
if(next == t)
flag = 1;
}
}
}
return flag;
}
int dfs(int now, int flow) {//沿着增广路增广
if(now == t || !flow)
return flow;
int i, surp = flow;
int SIZ = v[now].size();
for(i = be[now]; i < SIZ && surp; i++) {
be[now] = i;
int next = v[now][i].to;
if(v[now][i].val && de[next] == de[now] + 1) {
int maxnow = dfs(next, min(surp, v[now][i].val));
if(!maxnow)
de[next] = 0;
v[now][i].val -= maxnow;
v[next][v[now][i].rev].val += maxnow;
surp -= maxnow;
}
}
return flow - surp;
}
int Dinic() {//网络最大流,亦可用于二分图匹配
int res = 0;
int flow = 0;
while(bfs())
while(flow = dfs(s, INF))
res += flow;
return res;
}
int GetHash(int i, int j) {//获取点的编号
return (i - 1) * m + j;
}
void Down(int now, int i, int j) {//黑点向下扩展,每个白点最多遍历到一次
if(i != now)
dn[GetHash(now, j)] = GetHash(i, j);
if(arr[now + 1][j] == 2)
Down(now + 1, i, j);
}
void Right(int now, int i, int j) { //黑点向右扩展,每个白点最多遍历到一次
if(j != now)
rt[GetHash(i, now)] = GetHash(i, j) + n * m;
if(arr[i][now + 1] == 2)
Right(now + 1, i, j);
}
void GetMin(int now) {//dfs求构造方式
vis[now] = true;
int SIZ = v[now].size();
for(int i = 0; i < SIZ; i++) {
int next = v[now][i].to;
if(vis[next] || !v[now][i].val)
continue;
GetMin(next);
}
}
int main() {
scanf("%d %d", &n, &m);
s = 0; t = 2 * n * m + 1;//源点和汇点初始化
char ch;
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
cin >> ch;
if(ch == '1')
arr[i][j] = 1;
else
arr[i][j] = 2;
}
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(i == 1 && j == 1)
continue;
if(arr[i][j] == 1) {//向右或向下扩展,一个白点会被访问2次
Down(i, i, j);
Right(j, i, j);
}
}
}
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++) {
if(arr[i][j] == 1) {//源点到左部点,汇点到右部点连边
int now = GetHash(i, j);
int idnow = v[now].size();
int ids = v[s].size();
v[s].push_back(Node(now, 1, idnow));
v[now].push_back(Node(s, 0, ids));
now = GetHash(i, j) + n * m;
idnow = v[now].size();
int idt = v[t].size();
v[now].push_back(Node(t, 1, idt));
v[t].push_back(Node(now, 0, idnow));
}
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(i == 1 && j == 1)
continue;
if(arr[i][j] == 1)
continue;
int A = dn[GetHash(i, j)];//左部点到右部点连边
int B = rt[GetHash(i, j)];
int idA = v[A].size();
int idB = v[B].size();
v[A].push_back(Node(B, 1, idB));
v[B].push_back(Node(A, 0, idA));
}
}
printf("%d\n", Dinic());
GetMin(s);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(arr[i][j] == 2)
continue;
if(!vis[GetHash(i, j)])//打印答案
printf("%d %d DOLJE\n", i, j);
if(vis[GetHash(i, j) + n * m])
printf("%d %d DESNO\n", i, j);
}
}
return 0;
}
二分图最小点覆盖构造方案+König定理证明的更多相关文章
- UVA1194 Machine Schedule[二分图最小点覆盖]
题意翻译 有两台机器 A,B 分别有 n,m 种模式. 现在有 k 个任务.对于每个任务 i ,给定两个整数$ a_i\(和\) b_i$,表示如果该任务在 A上执行,需要设置模式为 \(a_i\): ...
- POJ2226 Muddy Fields(二分图最小点覆盖集)
题目给张R×C的地图,地图上*表示泥地..表示草地,问最少要几块宽1长任意木板才能盖住所有泥地,木板可以重合但不能盖住草地. 把所有行和列连续的泥地(可以放一块木板铺满的)看作点且行和列连续泥地分别作 ...
- POJ1325 Machine Schedule(二分图最小点覆盖集)
最小点覆盖集就是在一个有向图中选出最少的点集,使其覆盖所有的边. 二分图最小点覆盖集=二分图最大匹配(二分图最大边独立集) 这题A机器的n种模式作为X部的点,B机器的m种模式作为Y部的点: 每个任务就 ...
- hihoCoder #1127:二分图最小点覆盖和最大独立集
题目大意:求二分图最小点覆盖和最大独立集. 题目分析:如果选中一个点,那么与这个点相连的所有边都被覆盖,使所有边都被覆盖的最小点集称为最小点覆盖,它等于最大匹配:任意两个点之间都没有边相连的最大点集称 ...
- [POJ] 2226 Muddy Fields(二分图最小点覆盖)
题目地址:http://poj.org/problem?id=2226 二分图的题目关键在于建图.因为“*”的地方只有两种木板覆盖方式:水平或竖直,所以运用这种方式进行二分.首先按行排列,算出每个&q ...
- 二分图 最小点覆盖 poj 3041
题目链接:Asteroids - POJ 3041 - Virtual Judge https://vjudge.net/problem/POJ-3041 第一行输入一个n和一个m表示在n*n的网格 ...
- HihoCoder1127 二分图三·二分图最小点覆盖和最大独立集
二分图三·二分图最小点覆盖和最大独立集 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 在上次安排完相亲之后又过了挺长时间,大家好像都差不多见过面了.不过相亲这个事不是说 ...
- hdu 2236(二分图最小点覆盖+二分)
无题II Time Limit: 2000/2000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submis ...
- 四川第七届 D Vertex Cover(二分图最小点覆盖,二分匹配模板)
Vertex Cover frog has a graph with nn vertices v(1),v(2),…,v(n)v(1),v(2),…,v(n) and mm edges (v(a1), ...
随机推荐
- 排查 Linux 系统运行速度慢
排查 Linux 系统运行速度慢 一.检查CPU信息 二.使用top检查cpu负载 三.iotop进行检查 四.检查启动的服务 五.free检查闲置内存空间 一.检查CPU信息 在 Linux 系统中 ...
- Jenkins安装部署项目
Jenkins安装部署项目 配置JDK git maven 部署到服务器 一.新建任务 二.配置jenkins 三.添加构建信息 四.应用.保存 五.踩坑填坑记录 5.1没有jar包的情况 5.2无法 ...
- 杂论-FTP
FTP 一 简单介绍 FTP 是File Transfer Protocol(文件传输协议)的英文简称,而中文简称为"文传协议".用于Internet上的控制文件的双向传输.同时, ...
- CCF CSP 202009-1 称检查点查询
202009-1 称检查点查询 题目背景 2020年6月8日,国务院联防联控机制发布<关于加快推进新冠病毒核酸检测的实施意见>,提出对"密切接触者"等八类重点人群&qu ...
- ACM 模板库
Template For ACM 一. 字符串 标准库 sscanf sscanf(const char *__source, const char *__format, ...) :从字符串 __s ...
- Codeforces Round #677 (Div. 3) G. Reducing Delivery Cost(dijkstra算法)
题目链接:https://codeforces.com/contest/1433/problem/G 题解 跑 \(n\) 遍 \(dijkstra\) 得到任意两点间的距离,然后枚举哪一条边权为 \ ...
- hdu5432Rikka with Array (数位dp+十进制转化为二进制)
Problem Description As we know, Rikka is poor at math. Yuta is worrying about this situation, so he ...
- 【POJ 2411】【Mondriaans Dream】 状压dp+dfs枚举状态
题意: 给你一个高为h,宽为w的矩阵,你需要用1*2或者2*1的矩阵填充它 问你能有多少种填充方式 题解: 如果一个1*2的矩形横着放,那么两个位置都用二进制1来表示,如果是竖着放,那么会对下一层造成 ...
- 牛客小白月赛17 A 小sun的假期
传送门 题意: 第一行两个数n,m,代表总共有n天,m个安排.接下来有m行,每行是一个安排l,r,代表从第l天到第r天,小sun有安排了.安排可能会重复. 小 sun 非常喜欢放假,尤其是那种连在一起 ...
- pthread_create函数
函数简介 pthread_create是UNIX环境创建线程函数 头文件 #include<pthread.h> 函数声明 int pthread_create(pthread_t *re ...