基本概念
二分圖又稱二部圖
定義:
設G=(U,V,E)是一個無向圖,U和V是點的集合,E是邊的集合。
如果符合:
- 集合U,V之間有邊。
- U集合內部沒有邊。
- V集合內部沒有邊。
則稱圖G為二分圖。
例如:
作用:
進行匹配,比如說給程序員分配工作,為動物分配主人。
判斷是否為二部圖
思想
染色法
步驟:
- 任意取一個結點染成紅色。
- 重復下面過程,直到所有結點都被染色
-
- 將紅色結點的連接點染成藍色。
-
- 將藍色結點的連接點染成紅色。
-
- 如果發現某結點和鄰接點的顏色相同,則終止程序,該圖不是二分圖。
- 如果沒有發現某結點和鄰接點的顏色相同,則該圖是二分圖。
舉例:
該圖是二分圖。
代碼
思想:
利用DFS/BFS
圖示:
- 先選任意結點入隊,染成紅色。
- 重復下面過程,直到隊列沒有元素
- 將隊頭元素出隊,將他的未染色的鄰居染色並且入隊
- 判斷他已經染色的鄰居是否與他顏色不同。
循環1
循環2
循環3
....不貼圖了
代碼:
#include<stdio.h>
#include<string.h>
const int maxn=1e5+5;
const int maxm=1e5+5;
int head[maxn],point[maxm<<1],nxt[maxm<<1],size;
int c[maxn]; //color,每個點的黑白屬性,-1表示還沒有標記,0/1表示黑白
int num[2]; //在一次DFS中的黑白點個數
bool f=0; //判斷是否出現奇環
void init(){
memset(head,-1,sizeof(head));
size=0;
memset(c,-1,sizeof(c));
}
void add(int a,int b){
point[size]=b;
nxt[size]=head[a];
head[a]=size++;
point[size]=a;
nxt[size]=head[b];
head[b]=size++;
}
void dfs(int s,int x){
if(f)return;
c[s]=x;
num[x]++;
for(int i=head[s];~i;i=nxt[i]){
int j=point[i];
if(c[j]==-1)dfs(j,!x);
else if(c[j]==x){
f=1;
return;
}
}
}
//下面是主函數內的調用過程
for(i=1;i<=n&&(!f);i++){
if(c[i]==-1){
num[0]=num[1]=0;
dfs(i,1);
}
}
匹配
基本概念
匹配:
給定一個二分圖,在圖G的一個子圖G’中,
如果G’的邊集中的任意兩條邊都不依附於同一個頂點,
也就是說:每個頂點只被一條邊連接
則稱G’的邊集為G的一個匹配。
例如:
最大匹配:
在所有的匹配中,邊數最多的那個匹配
稱為二分圖的最大匹配。
例如:
注意:最大匹配可能不唯一。
無權二部圖中的最大匹配
轉換成最大流即可
有權二部圖中的最大/小匹配
方法:
匈牙利算法
有權二部圖中的最大匹配是指:權值和最大的匹配。
最大匹配和最小匹配可以相互轉換
:只需要將權值取負號即可。
二分圖的最小頂點覆蓋=最大匹配數
作用:
實現整體效益最大化。比如說
概述
交替路:
從一個未匹配點出發,依次經過非匹配邊、匹配邊、非匹配邊...形成的路徑叫交替路。*
增廣路:
從一個未匹配點出發,走交替路,以另一個非匹配點結束,則這條交替路稱為增廣路(agumenting path)。
顯然,這種路徑可以通過匹配邊非匹配邊的互換來產生更多的匹配。
也可以理解為是把增廣路里的匹配邊集合與這條路徑上所有邊的集合做對稱差。
增廣路定理: 匹配數最大 等價於 不存在增廣路
也就是說:
對於一個不是最大匹配的匹配,一定存在增廣路
也就是說:
沒有了增廣路,一定是最大匹配
點擊查看證明
不存在增廣路的必要性很好證明,如果有增廣路,因為增廣路結構是非匹配邊,匹配邊......非匹配邊,那么把所有匹配邊和非匹配邊交換就行了
這樣匹配數加一,則原來的匹配不是最大匹配
充分性稍難,證明看維基
匈牙利算法
匈牙利算法實際上就是一個不斷找增廣路直到找不到的過程。
代碼:
int M, N; //M, N分別表示左、右側集合的元素數量
int Map[MAXM][MAXN]; //鄰接矩陣存圖
int p[MAXN]; //記錄當前右側元素所對應的左側元素
bool vis[MAXN]; //記錄右側元素是否已被訪問過
bool match(int i)
{
for (int j = 1; j <= N; ++j)
if (Map[i][j] && !vis[j]) //有邊且未訪問
{
vis[j] = true; //記錄狀態為訪問過
if (p[j] == 0 || match(p[j])) //如果暫無匹配,或者原來匹配的左側元素可以找到新的匹配
{
p[j] = i; //當前左側元素成為當前右側元素的新匹配
return true; //返回匹配成功
}
}
return false; //循環結束,仍未找到匹配,返回匹配失敗
}
int Hungarian()
{
int cnt = 0;
for (int i = 1; i <= M; ++i)
{
memset(vis, 0, sizeof(vis)); //重置vis數組
if (match(i))
cnt++;
}
return cnt;
}
穩定婚配問題
問題描述:
有N男N女,每個人都按照他對異性的喜歡程度排名。現在需要寫出一個算法安排這N個男的、N個女的結婚,要求兩個人的婚姻應該是穩定的。
何為穩定?
當前假設1號男生的對象是1號女生,2號男生的對象是2號女生。
但如果1號男生對2號女生的好感度大於對1號女生的好感度,並且2號女生對1號男生的好感度也大於對2號男生的好感度,
那么1號男生會和2號女生在一起,這場婚姻就是不穩定的,反之就是穩定。
要求:
男生和女生數量一致。
男生和女生的喜歡都是單項的
Gale-Shapley算法
算法步驟:
- 每個單身男生向自己最喜歡的女生求婚
要求:
-
- 可以向任何女生求婚,即使女生已經結婚
-
- 一個男生只能向一個女生求婚一次。
- 當一個女生有多個男生追求時(丈夫也算追求者),選取最喜歡的那個。
- 所有男生都划掉已經求婚過的女生
- 重復123直到沒有單身狗。
(結果對男生更有利)
算法復雜度: On2
(一共有n個男生,每個男生有n個女生的心動排名)
代碼:
#include <bits/stdc++.h>
#define mod 1000000007
#define MAXN 1000
typedef long long ll;
using namespace std;
int ManArray[MAXN][MAXN], GirArray[MAXN][MAXN]; //ManArray[i][j]代表編號為i的男生的第j位心儀女生是幾號,GirArray[i][j]代表編號為i的女生的第j位心儀男生是幾號
int Man[MAXN], Gir[MAXN]; //Man[i]代表i號男生所匹配到的女生是幾號,Gir[i]代表i號女生所匹配到的男生是幾號
int ManStarPos[MAXN]; //ManStarPos[i]代表i號男生現在匹配到的女生是他心目中的第幾號心儀女生
int n;
stack<int> q; //存放單身男生的編號
//求編號為ManI的男生在編號為GirI的女生的心中的排名
int GetPositionFromLaday(int GirI, int ManI)
{
for (int i = 0; i < n; i++)
if (GirArray[GirI][i] == ManI)
return i;
return -1;
}
//為編號為ManI的男生匹配女生
void ManLookGir(int ManI)
{
int NowGir = ManArray[ManI][ManStarPos[ManI]]; //得到這個男生應該匹配的女生的編號
if (Gir[NowGir] == -1) //如果這個女生單身,那么就匹配上
{
Man[ManI] = NowGir;
Gir[NowGir] = ManI;
}
else //如果這個女生現在已經有男朋友了
{
//得到現男友在這個女孩心中的排名
int OldMan = GetPositionFromLaday(NowGir, Gir[NowGir]);
//得到我們要匹配的男生在這個女孩心中的排名
int NowMan = GetPositionFromLaday(NowGir, ManI);
if (OldMan < NowMan) //如果這個女孩更喜歡現任男友,那么這個女孩不換男朋友
{
ManStarPos[ManI]++;
q.push(ManI);
}
else //這個女孩更喜歡我們要匹配的這個男生,那么女孩換男朋友
{
ManStarPos[Gir[NowGir]]++;
q.push(Gir[NowGir]); //現任男友單身
Man[ManI] = NowGir;
Gir[NowGir] = ManI;
}
}
}
int main()
{
cin >> n;
//初始化
memset(Man, -1, sizeof(Man));
memset(Gir, -1, sizeof(Gir));
memset(ManStarPos, 0, sizeof(ManStarPos));
//輸入每個男生心目中對女生的排序
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> ManArray[i][j];
//輸入每個女生心目中對男生的排序
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> GirArray[i][j];
//剛開始對每個男生都進行一次匹配,從每個男生最心儀的女生開始匹配。
for (int i = 0; i < n; i++) ManLookGir(i);
//對剩下的單身男生進行匹配
while (!q.empty())
{
int i = q.top();
q.pop();
ManLookGir(i);
}
for (int i = 0; i < n; i++)
cout << "Man NO.: " << i << " Laday NO.: " << Man[i] << endl;
}
最小點覆蓋問題
另外一個關於二分圖的問題是求最小點覆蓋:我們想找到最少的一些點,使二分圖所有的邊都至少有一個端點在這些點之中。倒過來說就是,刪除包含這些點的邊,可以刪掉所有邊。
性質:
最大團 = 補圖的最大獨立集
最小邊覆蓋 = 二分圖最大獨立集 = |V| - 最小路徑覆蓋
最小路徑覆蓋 = |V| - 最大匹配數
最小頂點覆蓋 = 最大匹配數
最小頂點覆蓋 + 最大獨立數 = |V|
最小割 = 最小點權覆蓋集 = 點權和 - 最大點權獨立集