前言
博主很笨 ,如有紕漏,歡迎在評論區指出討論。
二分圖的最大匹配使用 \(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;
}