二分圖常見問題總結

1.二分圖

二分圖是指一個圖能夠分成兩部分,每一部分的點之間互相沒有連邊
比如說這個東西就是一個二分圖

因爲他能夠被分成uuvv兩部分,uu中的點相互之間沒有連邊,vv中的點相互之間也沒有連邊
二分圖也叫做二部圖,偶圖等等
在OI中,幾乎所有二分圖的題目都可以用網絡流來實現,但是二分圖本身是有非常優秀的算法的,下面我們來介紹一下

2.二分圖的判定

2.1 定義法判定二分圖

二分圖的判定方法就是基於他的定義的
顯然,根據定義,一個二分圖不能存在奇環
如果存在奇環的話顯然不是一個二分圖,這個大家可以參照上面的圖理解一下,應該不難
那麼這道題的內容就轉化成了判斷一個圖是否有奇環
這個做法怎麼做呢?
我們常見的做法是進行黑白染色
給有邊連接的兩個點染成不同的顏色,如果dfs(bfs)的過程中,出現一個點的標記顏色和我們要標的顏色不同的話,那麼就出現了奇環,這個圖就不是二分圖
當然判斷二分圖還有其他方法比如拓撲排序之類的,但是這種做法有一個優點就是他最後直接爲我們分好了兩部分
簡單的放一下這種做法的代碼

inline void dfs(int u,int c){
	color[u]=c;
	for(int i=head[u];~i;i=e[i].next){
		int v=e[i].to;
		if(color[v]==-1)dfs(v,c^1);
		else if(color[v]==c){
			puts("0");
			exit(0);
		}
	}
}

2.2 二分圖判定方法的應用

NOIP2008 雙棧排序
這道題的做法就是根據進哪個棧預處理出一個圖,如果不是二分圖就無解,如果是二分圖就有解,然後根據題目要求輸出就可以了

3.二分圖匹配

3.1 增廣路

先說題目意思吧,二分圖匹配是我們要讓二分圖一部分的點儘可能的去匹配另一部分的點,當然一個點只能匹配一個點啦

這裏我們要引入一個叫做增廣路的東西,這也是網絡流的基礎
比如說對於這樣的一個二分圖
在這裏插入圖片描述
我們現在要進行匹配,那麼我們先枚舉左邊的點,從第一個開始
匹配右邊的第一個
在這裏插入圖片描述
當我們枚舉第二個點的時候,我們發現,他已經不能和第一個匹配了,所以我們連向右邊第三個點
在這裏插入圖片描述
然後我們枚舉左邊的第三個點,他已經不能和右邊第三個匹配了,但是他沒有別的出邊了,怎麼辦呢,我們觀察這條紅色的路徑
在這裏插入圖片描述
這條路徑,從左3出發,經過未匹配邊->匹配邊->未匹配邊->匹配邊->未匹配邊,我們發現,如果我們把這條路徑上的匹配情況整體取反一下,匹配邊變成未匹配邊,未匹配邊變成匹配邊,就變成了
在這裏插入圖片描述
那麼我們發現,這時候匹配數就增加了1!
像這樣,經過未匹配邊->匹配邊->…->匹配邊->未匹配邊的一條路徑,就叫做一條增廣路

3.2 匈牙利算法

匈牙利算法正是基於增廣路的一種解決二分圖匹配的做法
我們發現,當我們找到一條增廣路,我們把這條路徑上的所有匹配關係取反之後,匹配數就+1,那麼匈牙利算法就是不停的找增廣路,一旦找到一條,那麼就整體匹配關係取反,答案+1
那麼實現也很簡單,大概放下代碼吧

bool dfs(int u){
    if(vis[u])return false;//vis表示這一次找增廣路的時候有沒有訪問過這個點,如果一個點之前已經訪問過了說明找不到增廣路
    vis[u]=true;
    RepG(i,u){//枚舉出邊
        int v=e[i].to;
        if(!match[v]||dfs(match[v])){
        //match[v]表示v點之前匹配的點,如果v之前還沒有一個點和他匹配,或者往他之前匹配的那個點去找增廣路找到了
        //就把匹配關係在回溯的時候整體取反一下,讓match[v]=u,然後返回true說明找到了一條增廣路
            match[v]=u;
            return true;
        }
    }
    return false;//如果沒找到,返回false
}

void hungary(){
    Rep(i,1,totx){
        memset(vis,0,sizeof(vis));
        if(dfs(i))ans++;//如果找到了增廣路,就說明這個點能匹配上,答案+1
    }
}

匈牙利算法的複雜度可以證明是O(nm)O(nm)的,因爲每個點都要枚舉一遍,最差情況下每條邊都要經過一遍,但是這只是最差情況下複雜度,大家如果接觸過網絡流的話就知道網絡流的複雜度是很難跑滿的

3.3 網絡流求二分圖匹配

網絡流我之前有講過,大家可以看這裏
用網絡流跑二分圖匹配就是建立一個超級源點和匯點,源點往左半部分每個點連一個容量爲1的邊,右半部分每個點往匯點連一個容量爲1的點,然後每條邊都相應的連上,容量均爲1,就可以跑最大流了,最後的答案就是最大流量
那麼問題來了,怎麼判斷最後匹配的情況呢?
因爲用匈牙利的話我們可以直接用match看出匹配情況,但是網絡流怎麼看呢?
我們可以判斷原圖上的每一條邊,他的反邊是否有流量(因爲這道題容量都是1,看正邊容量是不是0也行),如果有,說明這兩個點是匹配的
dinicdinic求二分圖匹配的複雜度最差可以證明是O(nn)O(n\sqrt n),一樣也是通常跑不到
而且一般出題人也不會看匈牙利吧
代碼就不放了

3.4 二分圖匹配的應用

應用就很多了,這也是二分圖匹配最常考的了
最常見的就是兩種題型吧,一個是題目裏明確說了要匹配的,正常連邊就可以
還有一個是二分圖的一個轉化模型,比如說放棋子,每行只能放一個,每列只能放一個這種的,那麼我們可以把每一行當成左半部分的點,每一列當成右半部分的點,每個能放棋子的地方就把他的行向列連一條邊就可以了,這樣的題目也不少,比如說
P1263 宮廷守衛 上面的模型稍微變化一下就好了
[SCOI2015] 小凸玩矩陣 二分一下就好等等
然後還有一些其他的,比如說
[JSOI2016] 反質數序列判斷質數,按奇偶性連邊
這裏就不一一列舉了

4.二分圖最小點覆蓋

4.1 定義

二分圖最小點覆蓋的定義對於一個二分圖,選擇最少的點,讓圖中所有的邊至少有一個端點屬於這些點

4.2 二分圖最小點覆蓋定理

內容
二分圖最小點覆蓋定理:二分圖最小點覆蓋=二分圖最大匹配數
證明
我們先構造出一個二分圖的匹配情況
在這裏插入圖片描述
我們從左側每一個沒有被匹配的點出發(在這幅圖是左2,5),走所有不完全的增廣路,就是說構成是 未匹配邊->匹配邊->…->匹配邊這樣的路徑,標記經過的點,這幅圖中應該有兩條,分別是
在這裏插入圖片描述
紅色的和藍色的這兩條
然後我們看左半部分沒有被訪問過的(左3)和右半部分被訪問過的(右2,4)
這就是我們二分圖的一個最大獨立集
爲什麼呢?

我們可以用反證法來證明
假設還有一條邊沒有被這幾個點覆蓋,那麼顯然他的左端點是一個匹配了的點
當這條邊是一個匹配邊時
如果這條邊的右端點只有一條邊,那麼他的右端點不會訪問到,所以沒有一條不完全增廣路會從右邊過來,而左端點又不是不完全增廣路的起點(因爲已經匹配),所以他不會被覆蓋,所以他的左端點屬於左邊沒有被訪問到的點,矛盾
如果這條邊的右端點有大於一條邊,那麼他一定會屬於一條不完全增廣路,那麼右端點一定屬於右邊別訪問到的點,矛盾
當這條邊不是一個匹配邊時
他連向的點一定是右邊被訪問的,所以這條邊也別覆蓋,矛盾。
所以這些點一定可以覆蓋所有的邊

下面要證明這些點少一個一定不行,這個證明方法跟上面類似,這裏就不再過多贅述了,大家可以下去自己證一下。

那麼通過這種構造方法,就得到了一個二分圖最小點覆蓋。
那麼爲啥這個最小點覆蓋是最大匹配數呢?
我們可以逆着增廣路來想,每個右邊被訪問過的點一定對應一條匹配邊,左邊每個沒有被訪問過的點一定也對應一條匹配邊(如果左邊點沒有被匹配他就是不完全增廣路的起點了),所以我們得到結論

=\boxed{二分圖最小點覆蓋=二分圖最大匹配數}

5.二分圖最大獨立集

5.1 定義

二分圖最大獨立集是指,從二分圖中選出一些點,讓這些點之間互相沒有邊相連

5.2 二分圖最大獨立集定理

內容
二分圖最大獨立集=總點數-二分圖最小點覆蓋=總點數-二分圖最大匹配數
證明
一句話證明:把最小點覆蓋挖掉剩下的就是最大獨立集了嘛,嚴格的證明就不寫了qwq

略去過程QED,由上可知證畢

所以我們得到結論
=\boxed{二分圖最大獨立集=總點數-二分圖最小點覆蓋}

5.3 二分圖最大團

定義
二分圖最大獨立集是指,從二分圖中選出一些點,讓這些點之間互相都有邊相連
好像和最大獨立集很像?仔細看
二分圖最大獨立集是指,從二分圖中選出一些點,讓這些點之間互相有邊相連
二分圖最大團定理
這個東西我們發現,就是二分圖的補圖的最大獨立集嗎
所以求二分圖最大團就是求出他的補圖,然後跑最大獨立集就可以了
=\boxed{二分圖最大團=二分圖的補圖的最大獨立集}

6.DAG上最長反鏈

6.1 定義

DAG上最長反鏈是指在一個DAG上選擇一些點,讓這些點之間互相不連通(對於任意兩個點xx,yyxx不能到yyyy也不能到xx
那麼我們可以用DilworthDilworth定理,一個DAG中最長反鏈大小,等於最小可重鏈覆蓋大小
其實也很好理解啦,因爲在一條鏈上的點,我們最多隻能選一個,所以讓可重鏈覆蓋的大小盡量的小,那麼就是最長反鏈的大小了

6.2 建立模型

那麼這個最小可重鏈覆蓋大小怎麼求呢,我們可以把每一個點拆成兩部分——出發點和到達點(這也是網絡流中常見的建模方法)和他能夠到達的點連一條邊,然後跑最大獨立集就可以了,爲什麼呢?因爲我們連出去的每一條邊,都可以抽象成一條鏈,我們選出來的每個點都不在一條鏈上,那麼就是最小可重鏈大小了

所以我們可以先floydfloyd傳遞閉包,然後連邊,然後跑最大獨立集就好了

6.3 DAG上最長反鏈的應用

[CTSC2008] 祭祀
這道題就是一個求DAG上最長反鏈的模板
第一問就是構造二分圖,輸出最大獨立集
第二問就運用我們構造最小點覆蓋的方法,取反一下就是最大獨立集
第三問,枚舉每個點,把這個點和與其相鄰的點都刪掉,然後再跑最大獨立集,判斷如果最大獨立集=第一問答案-1,說明他在最長反鏈上

代碼:

#include <bits/stdc++.h>
using namespace std;

# define Rep(i,a,b) for(int i=a;i<=b;i++)
# define _Rep(i,a,b) for(int i=a;i>=b;i--)
# define RepG(i,u) for(int i=head[u];~i;i=e[i].next)

typedef long long ll;

const int N=105;

template<typename T> void read(T &x){
   x=0;int f=1;
   char c=getchar();
   for(;!isdigit(c);c=getchar())if(c=='-')f=-1;
   for(;isdigit(c);c=getchar())x=(x<<1)+(x<<3)+c-'0';
    x*=f;
}

int n,m,ans;
int head[N],cnt;
int match[N];
bool vis[N];
bool visl[N],visr[N];
bool matched[N],ban[N];
bool g[N][N];

struct Edge{
    int to,next;
}e[N*N<<1];

void add(int x,int y){
    e[++cnt]=(Edge){y,head[x]},head[x]=cnt;
}

void floyd(){
    Rep(k,1,n)
        Rep(i,1,n)
            Rep(j,1,n)
                g[i][j]|=g[i][k]&&g[k][j];
}

bool dfs1(int u){
    if(vis[u]||ban[u])return false;
    vis[u]=true;
    RepG(i,u){
        int v=e[i].to;
        if(ban[v])continue;
        if(!match[v]||dfs1(match[v])){
            match[v]=u;
            return true;
        }
    }
    return false;
}

int hungary(){
    int res=0;
    memset(match,0,sizeof(match));
    Rep(i,1,n){
        if(ban[i])continue;
        memset(vis,0,sizeof(vis));
        if(dfs1(i))res++;
    }
    return res;
}

void dfs2(int u){
    if(visl[u])return;
    visl[u]=true;
    RepG(i,u){
        int v=e[i].to;
        if(visr[v])continue;
        visr[v]=true,dfs2(match[v]);
    }
}

int main()
{
    memset(head,-1,sizeof(head));
    read(n),read(m);
    Rep(i,1,m){
        int x,y;
        read(x),read(y);
        g[x][y]=true;
    }
    floyd();
    Rep(i,1,n)
        Rep(j,1,n)
            if(g[i][j])
                add(i,j);
    ans=n-hungary();
    printf("%d\n",ans);
    Rep(i,1,n)matched[match[i]]=true;
    Rep(i,1,n)if(!matched[i])dfs2(i);
    Rep(i,1,n)printf("%d",visl[i]&&!visr[i]);
    puts("");
    Rep(i,1,n){
        memset(ban,0,sizeof(ban));
        int tot=0;
        Rep(j,1,n)
            if(i==j||g[i][j]||g[j][i])ban[j]=true;
            else tot++;
        printf("%d",tot-hungary()==ans-1);
    }
    puts("");
    return 0;
}

7.帶權二分圖匹配/二分圖最優匹配

7.1 KM算法

KM算法運用到了貪心的思想其實
這裏要引入一個東西——頂標,用來表達預期
在這裏插入圖片描述
開始時,我們把左邊的點的頂標設成他出邊的最大值,右邊設成0
我們用頂標表示我們期望的匹配的權值
在這裏插入圖片描述
然後,還是仿照匈牙利的做法,我們枚舉左邊的每個點,嘗試匹配,還是找增廣路,但是我們只能走一條路徑的邊權=兩端點的頂標之和的路,這樣我們就保證了讓匹配權值儘量大
我們先匹配1,1->1的路徑上,4+0>3,不能走,1->3這條路可以走,所以我們把1和3連上
然後看2,他想去3,但是3已經被佔了,所以他要降低要求,怎麼降低要求呢?我們用和最小點覆蓋相似的做法,找出一條不完全的增廣路2312-3-1,然後找到這條路徑上的左右邊中,兩個頂點的頂標-邊權最小的那個Δ\Delta(相等的不算),然後把這條路徑經過的左半部分的頂標Δ-\Delta,右半部分的點的頂標+Δ+\Delta,因爲這條不完全的增廣路左半部分的點數比右半部分多1,所以我們這樣做就可以達到減少預期的結果
第一次找完之後頂標變成
在這裏插入圖片描述
但是我們注意,這個時候,我們的2還沒有匹配,左1和右3還是在一起
所以我們還要從左2出發去匹配,那麼這個時候,212-1這條路就可以被選擇了,所以我們把左2和右1匹配上
然後我們嘗試給3進行匹配,那麼這個時候我們發現頂標5+1>5,所以我們再按照上面的方法把頂標降到4,然後再匹配就可以啦

簡單放一下代碼吧

bool dfs(int u){
    if(visl[u])return false;//visl,visr分別表示左邊,右邊的點這次有沒有被訪問到
    visl[u]=true;
    Rep(v,1,n)//這裏我默認是一個完全圖
        if(g[u][v]==l[u]+r[v]){//如果可以走
            visr[v]=true;//正常匈牙利
            if(!match[v]||dfs(match[v])){
                match[v]=u;
                return true;
            }
        }
        else delta=min(delta,l[u]+r[v]-g[u][v]);//否則找差最小的
    return false;
}

void KM(){
    Rep(i,1,n)
        while(1){//直到找到了才能停
            delta=inf;
            memset(visl,0,sizeof(visl));
            memset(visr,0,sizeof(visr));
            if(dfs(i))break;
            Rep(i,1,n){
                if(visl[i])l[i]-=delta;//更改頂標值
                if(visr[i])r[i]+=delta;
            }
        }
}

但是注意,KM算法只能求最大完美匹配值,如果想求不完備的,就需要用下面的費用流做法了

這種KM的寫法複雜度最差是O(n4)O(n^4)的(n2mn^2m

7.2 網絡流求帶權二分圖匹配

這裏運用了費用流的做法
按照題意連容量爲1,費用爲權值的邊,跑費用流就可以

8.slack優化KM算法

我們發現,普通KM的複雜度是O(n4)O(n^4)實在是有點大
我們考慮能不能優化一下
我們發現,我們找Δ\Delta的值得時候因爲我們只有一個數組,所以很容易求出來的Δ\Delta是容易不對的,所以優化的方法就是給右半部分的每一個點都開一個slack數組表示鬆弛量,然後我們找的時候就不會出現因爲每次鬆弛量不準而造成的複雜度太大的問題了,可以證明這種做法的複雜度是O(n3)O(n^3)(nmnm)
然而我不會證qwq

代碼:

bool dfs(int u,int x,int y){
    if(visl[u])return false;
    visl[u]=true;
    Rep(v,1,n){
        if(u==x&&v==y)continue;
        if(lx[u]+rx[v]==g[u][v]){
            visr[v]=true;
            if(!match[v]||dfs(match[v],x,y)){
                match[v]=u;
                return true;
            }
        }
        else if(lx[u]+rx[v]>g[u][v])slack[v]=min(slack[v],lx[u]+rx[v]-g[u][v]);//更新slack
    }
    return false;
}

int KM(int x,int y){
    memcpy(lx,_max,sizeof(_max));
    memset(match,0,sizeof(match));
    memset(rx,0,sizeof(rx));
    Rep(i,1,n){
        memset(slack,0x3f,sizeof(slack));
        while(1){
            memset(visl,0,sizeof(visl));
            memset(visr,0,sizeof(visr));
            delta=inf;
            if(dfs(i,x,y))break;
            Rep(i,1,n)
                if(!visr[i])delta=min(delta,slack[i]);//找走過的中slack最小的那個
            Rep(i,1,n){
                if(visl[i])lx[i]-=delta;
                if(visr[i])rx[i]+=delta;
                else slack[i]-=delta;//如果沒有經過整體slack也減去鬆弛量
            }
        }
    }
    int res=0;
    Rep(i,1,n)res+=lx[i]+rx[i];
    return res;
}

9.寫在最後

這篇blogblog大概寫了兩個多小時吧,總算是寫完了,基本上總結了一下二分圖上常見的考點吧
這篇文章中的圖片有的來自百度百科,但是大部分是我手畫的qwq,所以順手點個讚唄>_< (最後賣個萌qwq

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章