博主很笨 ,若有紕漏,歡迎在評論區指出討論。ios
二分圖的最大匹配使用 \(Dinic\) 算法進行實現,時間複雜度爲 \(O(n\sqrt{e})\),其中, \(n\)爲二分圖中左部點的數量, \(e\) 爲二分圖中的邊數。如果匈牙利算法,時間複雜度爲 \(O(nm)\) , \(m\) 爲二分圖中右部點的數量,不建議使用。算法
定理內容:二分圖最小點覆蓋的點的數量等於二分圖最大匹配的邊的數量。函數
構造方法 \(+\) 簡單證實:spa
首先求出二分圖中的最大匹配,建議使用 \(Dinic\) 。code
從每個非匹配點出發,沿着非匹配邊正向進行遍歷,沿着匹配邊反向進行遍歷到的點進行標記。選取左部點中沒有被標記過的點,右部點中被標記過的點,則這些點能夠造成該二分圖的最小點覆蓋。ci
遍歷代碼實現以下:get
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); } }
那麼就有如下性質:string
有了上述的三條性質,能夠發現:按照選取左部點中沒有被標記過的點,右部點中被標記過的點的規則,選出來的點的點數必然爲最大匹配的邊數。左部的非匹配點必然被訪問,則必不會被選,右部的非匹配點必不會被訪問,則必不會被選。而第三條性質決定了,對於一組匹配點,會選擇有且僅有一個點。故而選出的點的點數等於最大匹配的邊數。io
其次須要解決一個問題:保證這些點覆蓋了全部的邊。具體能夠分爲四類:
最後在確保這是最小的方案:一條邊都只選了一個點,不存在浪費。
如上,證畢。
題目來源: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\) 內求出答案。
#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; }