二分圖匹配模版及題型總結

今天主要學習了二分圖的概念和各種二分圖的題型及解決辦法。二分圖,即所有點可以被分爲兩個集合,這兩個集合中兩個點互不相鄰。即可以分成左邊和右邊兩堆不相鄰的點的圖。。首先先是學習了二分圖的概念以及如何判斷一個圖是否是二分圖。方法很簡單,就是判斷有沒有奇環,用染色法DFS即可。

模版如下

int col[N];

int judge(int u ,int co){			//DFS染色判斷是否是二分圖
	col[u] = co;
	for(int i=head[u];i!=-1;i = e[i].next){
		int v = e[i].v;
		if(!col[v]){
			col[v] = -col[u];
			if(!judge(v,-co))return 0;
		}
		else if(col[v] == col[u])
			return 0;
	}
	return 1;
}

int isBigraph(){					//有可能存在不連通的多個塊
	memset(col,0,sizeof(col));
	for(int u = 1;u<=v;u++)
		if(col[u]==0)
			if(!judge(u,-1))return 0;
	return 1;
}


接下來就是二分圖的最大匹配,對於一個二分圖,找出一個邊集,使得每條邊的兩個頂點分別在兩個點集中,則稱此邊集爲一個匹配,尋找邊集中邊最多的方案就是最大匹配。尋找最大匹配用的是匈牙利算法,思想是尋找增廣路來增加匹配數。不詳細說,模版如下:

//鄰接表
int dfs(int u){
	for(int i= head[u];i!=-1;i = e[i].next){
		int v = e[i].v;
		if(!check[v]){
			check[v] = 1;
			if(match[v] == -1 || dfs(match[v])){
				match[v] = u;
				return 1;
			}
		}
	}
	return 0;
}

int hungarian(){
	int ans = 0;
	memset(match,-1,sizeof(match));
	for(int u = 1;u<=n;u++){				//根據題目改一下
		memset(check,0,sizeof(check));
		if(dfs(u))
			ans++;
	}
	return ans;
}
//鄰接矩陣
bool dfs(int u)  
{  
    for (int i = 1; i <= cnt; ++i)  
    {  
        if (g[u][i] && !visit[i])   //如果節點i與u相鄰並且未被查找過  
        {  
            visit[i] = true;   //標記i爲已查找過  
            if (match[i] == -1 || dfs(match[i]))   //如果i未在前一個匹配M中,或者i在匹配M中,但是從與i相鄰的節點出發可以有增廣路徑  
            {  
                match[i] = u;  //記錄查找成功記錄,更新匹配M(即“取反”)  
                return true;   //返回查找成功  
            }  
        }  
    }  
    return false;  
}  
int MaxMatch()  
{  
    int i,sum=0;  
    memset(match,-1,sizeof(match));  
    for(i = 1 ; i <= cnt ; ++i)  
    {  
        memset(visit,false,sizeof(visit));   //清空上次搜索時的標記  
        if( dfs(i) )    //從節點i嘗試擴展  
        {  
            sum++;  
        }  
    }  
    return sum;  
}  

第三種是最優匹配,由於還沒看 先留空。。。。


題型總結:

一、判斷一個圖是否是二分圖

Uva 1000Bicoloring

題意:給一張圖,圖上有一些點和邊,問你是否能把這張圖上的所有點染色,使得每條邊的兩個點顏色不同

分析:就是很裸的一道題,問這個圖是不是二分圖,其實直接模擬染色就可以了,也就是判斷的標準做法。

代碼:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
struct edge{
    int u,v,next;
}e[405];
int head[205],n,cnt,m;
void addedge(int u,int v){
    e[cnt].u = u;
    e[cnt].v = v;
    e[cnt].next = head[u];
    head[u] = cnt++;
}
int col[205];
int judge(int u ,int co){			//DFS染色判斷是否是二分圖
	col[u] = co;
	for(int i=head[u];i!=-1;i = e[i].next){
		int v = e[i].v;
		if(!col[v]){
			col[v] = -col[u];
			if(!judge(v,-co))return 0;
		}
		else if(col[v] == col[u])
			return 0;
	}
	return 1;
}

int isBigraph(){					//有可能存在不連通的多個塊
	memset(col,0,sizeof(col));
	for(int u = 0;u<n;u++)
		if(col[u]==0)
			if(!judge(u,-1))return 0;
	return 1;
}
int main()
{
    while(scanf("%d",&n),n){
        scanf("%d",&m);
        int u,v;
        cnt = 0;
        memset(head,-1,sizeof(head));
        for(int i=1;i<=m;i++)
            scanf("%d%d",&u,&v),addedge(u,v),addedge(v,u);
        if(isBigraph())puts("BICOLORABLE.");
        else puts("NOT BICOLORABLE.");
    }
    return 0;
}


二、求最大匹配

POJ 1274 The Perfect Stall
題意:有一些牛和一些牛欄,每頭牛有自己心儀的牛欄,每個牛欄只能裝一頭牛,求最多能給多少頭牛分配自己心儀的牛欄

分析:很裸很裸的求匹配。。把牛放在一邊,牛欄放在另一邊建圖求匹配即可

代碼:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int g[205][205],check[205],match[205];
int n,m;
int dfs(int u){
	for(int i= 1;i<=m;i++){
		if(!check[i]&&g[u][i]){
			check[i] = 1;
			if(match[i] == -1 || dfs(match[i])){
				match[i] = u;
				return 1;
			}
		}
	}
	return 0;
}

int hungarian(){
	int ans = 0;
	memset(match,-1,sizeof(match));
	for(int u = 1;u<=n;u++){
        memset(check,0,sizeof(check));
        if(dfs(u))
            ans++;
	}
	return ans;
}
int main()
{
    while(scanf("%d%d",&n,&m)==2){
        memset(g,0,sizeof(g));
        int flag = 1;
        for(int i=1;i<=n;i++){
            int s,v;
            scanf("%d",&s);
            if(!s)flag = 0;
            while(s--){
                scanf("%d",&v);
                g[i][v] =1;
            }
        }
        if(!n||!m||!flag){puts("0");continue;}
        printf("%d\n",hungarian());
    }
    return 0;
}

POJ 2584 T-Shirt Gumbo

題意:有一些比賽者,需要舉辦方給發放T恤,然而每個比賽者有一些自己可以接受的尺寸範圍,總共有五個尺寸,每個尺寸有固定件的衣服,問是否能夠滿足每個比賽者都能分到合適的衣服

分析:把每件衣服當作一個點來做,然後在每個人和他可以穿的衣服之間連邊建圖,求最大匹配,如果和人的數量相同,就可以分到。

代碼:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
using namespace std;
char temp[20],contest[25][5],cloth[105];
int g[30][150];
int match[150],check[150];
int x,n;
char si[10] = "SMLXT";
int dfs(int u){
	for(int i = 1;i<=n;i++){
		if(!check[i]&&g[u][i]){
			check[i] = 1;
			if(match[i] == -1 || dfs(match[i])){
				match[i] = u;
				return 1;
			}
		}
	}
	return 0;
}
map<char ,int > ma;
int hungarian(){
	int ans = 0;
	memset(match,-1,sizeof(match));
	for(int u = 1;u<=x;u++){
		memset(check,0,sizeof(check));
		if(dfs(u))
			ans++;
	}
	return ans;
}
int main()
{
    for(int i=0;i<5;i++)ma[si[i]] = i+1;
    while(scanf("%s",temp),strcmp(temp,"ENDOFINPUT")!=0){
        scanf("%d",&x);
        memset(g,0,sizeof(g));
        for(int i=1;i<=x;i++)scanf("%s",contest[i]);
        int ts[6];
        ts[0] = 0;
        for(int i=1;i<=5;i++){
            scanf("%d",&ts[i]);
            ts[i]+=ts[i-1];
        }
        n = ts[5];
        scanf("%*s");
        for(int i=1;i<=x;i++){
            int a = ma[contest[i][0]],b = ma[contest[i][1]];
            //printf("st:%d ed:%d\n",a,b);
            for(int j=ts[a-1]+1;j<=ts[b];j++)g[i][j] = 1;//printf("fff:%d %d\n",i,j);
        }
        if(hungarian()==x)printf("T-shirts rock!\n");
        else printf("I'd rather not wear a shirt anyway...\n");
    }
    return 0;
}

POJ 2536 Gopher II

題意:有一些地鼠,在一些固定的點上,有一隻老鷹要來抓他們。然而有一些洞, 也是固定座標。所有地鼠的速度都是固定的,如果不在S秒內跑回洞,那麼就會被老鷹喫掉。求出最少被老鷹喫掉的數量。

分析:只要算出每隻地鼠能在固定時間內到達的洞,並連邊,求出最大匹配就是能逃回洞的地鼠數量,然後要注意題目要求的是被喫掉的數量,所以要用n-ans.

代碼:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
struct point{
    double x,y;
};
point gopher[105],hole[105];
int g[105][105];
int n,m,s,v;
double d(point a,point b){
    return sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));
}
int check[105],match[105];
int dfs(int u){
	for(int i=1;i<=m;i++){
		if(!check[i]&&g[u][i]){
			check[i] = 1;
			if(match[i] == -1 || dfs(match[i])){
				match[i] = u;
				return 1;
			}
		}
	}
	return 0;
}
int hungarian(){
	int ans = 0;
	memset(match,-1,sizeof(match));
	for(int u = 1;u<=n;u++){
		memset(check,0,sizeof(check));
		if(dfs(u))
			ans++;
	}
	return ans;
}
int main()
{
    while(scanf("%d%d%d%d",&n,&m,&s,&v)==4){
        memset(g,0,sizeof(g));
        for(int i=1;i<=n;i++)
            scanf("%lf%lf",&gopher[i].x,&gopher[i].y);
        for(int i=1;i<=m;i++)
            scanf("%lf%lf",&hole[i].x,&hole[i].y);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
                if(d(gopher[i],hole[j])<=s*v)
                    g[i][j] = 1;
        printf("%d\n",n-hungarian());
    }
    return 0;
}

POJ 2446 Chessboard

題意:給一個M*N的棋盤,並且棋盤上有一些格子有障礙,問是否能用1*2的矩形來將其剩餘格子填滿,不能重疊。

分析:對於每個不是障礙的格子,可以把這個格子和其相鄰的格子分成兩個集合,即這整個圖就是一個二分圖。我們直接求出這個二分圖的最大匹配,就是能放的方塊數*2(因爲建圖時每條邊會被計算兩次)對於這種圖的DFS可以改一下,改成往四個方向DFS即可。求出最大匹配後再與無障礙的格子數比較。在計算的時候如果無障礙格子是奇數可以直接跳出循環(奇偶剪枝)

AC代碼:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n,m,k,cnt;
int dx[4] = {1,-1,0,0};
int dy[4] = {0,0,1,-1};
int check[40][40],match[40][40][2],id[40][40];
int dfs(int x,int y){
	for(int i=0;i<4;i++){
        int nx = x+dx[i],ny = y+dy[i];
        if(nx<1||ny<1||nx>n||ny>m)continue;
        if(id[nx][ny]==-1)continue;
		if(!check[nx][ny]){
			check[nx][ny] = 1;
			if(match[nx][ny][0] == -1&&match[nx][ny][1]==-1 || dfs(match[nx][ny][0],match[nx][ny][1])){
				match[nx][ny][0] = x;
				match[nx][ny][1] = y;
				return 1;
			}
		}
	}
	return 0;
}
int hungarian(){
	int ans = 0;
	memset(match,-1,sizeof(match));
	for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        if(id[i][j]!=-1){
		memset(check,0,sizeof(check));
		if(dfs(i,j))
			ans++;
	}
	return ans;
}
int main()
{
    while(scanf("%d%d%d",&m,&n,&k)==3){
        int x,y;
        cnt = 0;
        memset(id,0,sizeof(id));
        for(int i=1;i<=k;i++){
            scanf("%d%d",&x,&y);
            id[x][y] = -1;
        }
        if((m*n-k)%2){puts("NO");continue;}
        if(hungarian()==m*n-k)
            puts("YES");
        else puts("NO");
    }
    return 0;
}

二、最小點覆蓋


根據學習的PPT,二分圖有許多定理,其中就有關於最小點覆蓋的,什麼是最小點覆蓋?假設你覆蓋了圖中的一個點就代表覆蓋了與這個點相連的所有邊,問最少覆蓋幾個點能把所有邊覆蓋,這就是最小點覆蓋。由König定理知道,最小點覆蓋就等於最大匹配數(證明就略了。。。),於是就可以開始做題了。。


POJ 3041 Asteroids

題意:一個N*N的方格,某些格子有一些小行星,你有一種武器,每射擊一次可以消滅一行或者一列上的小行星。問要把所給方格中所有小行星都消滅,最少需要射擊幾次。

分析:所給的是小行星的座標,我們不妨把所給格子的每一行作爲一個點,每一列作爲一個點,若這一行這一列存在小行星則連邊,構造一個二分圖。這樣的話我只需要找到某些行和列,每條邊都代表一顆小行星,找到最少的行和列包括所有小行星即可。這就是求最小點覆蓋。

AC代碼如下:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int g[505][505],match[505],check[505];
int r,c;
int n,k;
int dfs(int u){
	for(int v = 1;v<=n;v++){
		if(!check[v]&&g[u][v]){
			check[v] = 1;
			if(match[v] == -1 || dfs(match[v])){
				match[v] = u;
				return 1;
			}
		}
	}
	return 0;
}

int hungarian(){
	int ans = 0;
	memset(match,-1,sizeof(match));
	for(int u = 1;u<=n;u++){
		memset(check,0,sizeof(check));
		if(dfs(u))
			ans++;
	}
	return ans;
}
int main()
{
    while(scanf("%d%d",&n,&k)==2){
        memset(g,0,sizeof(g));
        for(int i=1;i<=k;i++)
            scanf("%d%d",&r,&c),g[r][c] = 1;
        printf("%d\n",hungarian());
    }
    return 0;
}


POJ 1325 Machine Schedule

題意:有兩臺機器A,B,A機器有N種工作模式,B機器有M種工作模式,有一些產品,可以在A機器的第x種模式下生產,也可以在b機器的第y種模式下生產,問最少改變幾次機器的模式,把所有產品都生產出來。

分析:其實和上一題是異曲同工,每種產品當作一條邊,在AB的所有模式中選出最少的一些模式覆蓋所有邊,就是算最小點覆蓋。注意這題模式是從0開始的,所以如果兩個模式中有一個模式是0就不需要建邊了。

代碼如下:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int g[105][105];
int a,b,num;
int check[105],match[105];
int dfs(int u){
    for(int v=0;v<b;v++){
        if(!check[v]&&g[u][v]){
            check[v] = 1;
            if(match[v] == -1|| dfs(match[v])){
                match[v] = u;
                return 1;
            }
        }
    }
    return 0;
}
int hungarian(){
    int ans = 0;
    memset(match,-1,sizeof(match));
    for(int u=0;u<a;u++){
        memset(check,0,sizeof(check));
        if(dfs(u))ans++;
    }
    return ans;
}
int main()
{
    while(scanf("%d",&a),a){
        scanf("%d%d",&b,&num);
        memset(g,0,sizeof(g));
        for(int i=0;i<num;i++){
            int u,v;
            scanf("%*d%d%d",&u,&v);
            if(!u||!v)continue;
            g[u][v] = 1;
        }
        printf("%d\n",hungarian());
    }
    return 0;
}


POJ 2226 Muddy Fields

題意:給一塊地,有些格子上是泥。用寬爲1的木板去鋪這些泥地,不能把草地也給蓋上,但是木板可以重疊。木板長度不限。問最少用幾塊木板可以鋪上。

分析:其實和小行星那題很像,唯一有區別的是隻能鋪連續的一塊地。所以直接把連續的一塊地作爲一個點就可以了。其他做法和小行星那題如出一轍。

AC代碼:

#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
int ma[55][55][2];
int g[700][700];
int r,c,n,m;
char gra[55][55];
int vis[700],match[700];
int dfs(int u){
    for(int i=1;i<=m;i++){
        if(!vis[i]&&g[u][i]){
            vis[i] = 1;
            if(!~match[i]|| dfs(match[i])){
                match[i] = u;
                return 1;
            }
        }
    }
    return 0;
}
int hungarian(){
    int ans = 0;
    memset(match,-1,sizeof(match));
    for(int u=1;u<=n;u++){
        memset(vis,0,sizeof(vis));
        if(dfs(u))ans++;
    }
    return ans;
}
int main()
{
    while(scanf("%d%d",&r,&c)==2){
        for(int i=0;i<r;i++)
            scanf("%s",gra[i]);
        n = m = 0;
        memset(g,0,sizeof(g));
        memset(ma,0,sizeof(ma));
        for(int i=1;i<=r;i++)
        for(int j=1;j<=c;j++){
            while(gra[i-1][j-1]=='.'&&j<=c)j++;
            if(j>c)break;
            while(gra[i-1][j-1]=='*'&&j<=c){
                ma[i][j][0] = n+1;
                j++;
            }
            n++;
        }

        for(int j=1;j<=c;j++)
        for(int i=1;i<=r;i++){
            while(gra[i-1][j-1]=='.'&&i<=r)i++;
            if(i>r)break;
            while(gra[i-1][j-1]=='*'&&i<=r){
                ma[i][j][1] = m+1;
                i++;
            }
            m++;
        }
        for(int i=1;i<=r;i++)
            for(int j=1;j<=c;j++)
                if(gra[i-1][j-1]=='*')
                    g[ ma[i][j][0] ][ ma[i][j][1] ] = 1;
        printf("%d\n",hungarian());
    }
    return 0;
}

三、最大獨立集

什麼是最大獨立集?就是取出點集中的一些點,使得這些點互相獨立,沒有邊相連。這樣的點最多的集合就是最大獨立集。由二分圖的性質,最大獨立集  =  點數  - 最大匹配數,證明略。

POJ 2724 Purifying Machine

題意:一個人有一些奶酪,他有一臺淨化奶酪的機器,上面有N個開關,每個開關有三種狀態,1,0和*,如果是*代表可以是0也可以是1,每次最多隻有一個開關是*,每次會把所有開關序列所代表的二進制數的奶酪給淨化了。有一天他的機器被污染了,他發現的時候已經污染了一些奶酪,他立即清洗了機器,問最少幾次能把被污染的奶酪重新淨化。

分析:其實就是給了一些二進制數,問最少幾次能把這些數遍歷了,因爲每次可以遍歷兩個二進制位之差一位的數,所以可以把給的數中可以一次遍歷的兩個數連一條邊,構成了一個二分圖。可以知道,這個二分圖的最大獨立集就是所求的遍歷次數。

判斷兩個數是否差一位的方法:c = a^b;if(c && (c&(c-1)) == 0) return 1;

AC代碼:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1100;
int n,m;
int num[N],g[N][N];
int vis[N];
int cnt ;
int match[N],check[N];
int dfs(int u){
    for(int i=1;i<=cnt;i++){
        if(!check[i]&&g[u][i]){
            check[i] = 1;
            if(match[i] == -1 || dfs(match[i])){
                match[i] = u;
                return 1;
            }
        }
    }
    return 0;
}
int hungarian(){
    int ans = 0;
    memset(match,-1,sizeof(match));
    for(int u=1;u<=cnt;u++){
        memset(check,0,sizeof(check));
        if(dfs(u))ans++;
    }
    return ans;
}
int main()
{
    while(scanf("%d%d",&n,&m),n+m){
        memset(vis,0,sizeof(vis));
        memset(num,0,sizeof(num));
        memset(g,0,sizeof(g));
        char str[20];
        cnt = 0;
        for(int i=1;i<=m;i++){
            scanf("%s",str);
            int num1 = 0,num2 = 0;
            for(int j=0;j<n;j++){
                if(str[j] == '1')num1+=(1<<(n-j-1)),num2+=(1<<(n-j-1));
                else if(str[j] == '*')num1+=(1<<(n-j-1));
            }
            if(num1 == num2&&!vis[num1])vis[num1] = 1,num[++cnt] = num1;
            else{
                if(!vis[num1])vis[num1] = 1,num[++cnt] = num1;
                if(!vis[num2])vis[num2] = 1,num[++cnt] = num2;
            }
        }
        for(int i=1;i<=cnt;i++){
            for(int j=1;j<=cnt;j++){
                int c = num[i]^num[j];
                if(c && (c&(c-1))==0)g[i][j] = 1;
            }
        }
        printf("%d\n",cnt-hungarian()/2);
    }
    return 0;
}


四、最小路徑覆蓋

在有向無環圖中找一些不相交的路徑,使得這些路徑覆蓋圖的所有頂點,這樣的覆蓋中路徑數最少的成爲最小路徑覆蓋,而二分圖的最小路徑覆蓋數 = 頂點數 - 最大匹配數,證明略
POJ 2060 Taxi Cab Scheme

題意:有一些出租車的預約單,分別包含出發時間,出發地點座標和目的地地點座標。出租車在兩地之間需要的時間是這兩個地點的曼哈頓距離。要求出租車在出發時間前一分鐘到達出發地點。問最少需要幾輛出租車來完成這些預約。

分析:把預約作爲點,如果兩個預約單之間可以由一輛出租車完成,所以每一條路徑都可以由一輛出租車完成,所以就變成了求最小路徑覆蓋。建完圖直接求出來即可。

AC代碼:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>//純粹是爲了abs
using namespace std;
struct node{
    int t,sx,sy,ex,ey;
}cab[510];
int n,g[510][510];
int vis[510],match[510];
int dfs(int u){
    for(int i=1;i<=n;i++){
        if(!vis[i]&&g[u][i]){
            vis[i] = 1;
            if(match[i] == -1|| dfs(match[i])){
                match[i] = u;
                return 1;
            }
        }
    }
    return 0;
}
int hungarian(){
    int ans = 0;
    memset(match,-1,sizeof(match));
    for(int u=1;u<=n;u++){
        memset(vis,0,sizeof(vis));
        if(dfs(u))ans++;
    }
    return ans;
}
int main()
{
    int T;
    cin>>T;
    while(T--){
        scanf("%d",&n);
        memset(g,0,sizeof(g));
        for(int i=1;i<=n;i++){
            int h,m;
            node &temp = cab[i];
            scanf("%d:%d%d%d%d%d",&h,&m,&temp.sx,&temp.sy,&temp.ex,&temp.ey);
            temp.t = h*60+m;
        }
        for(int i=1;i<=n;i++){
            for(int j=i+1;j<=n;j++){
                node &a = cab[i];
                node &b = cab[j];
                int t1 = abs(a.ex-a.sx)+abs(a.ey-a.sy);
                int t2 = abs(b.sx-a.ex)+abs(b.sy-a.ey);
                if(a.t+t1+t2+1<=b.t)g[i][j] = 1;
            }
        }
        printf("%d\n",n-hungarian());
    }
    return 0;
}

暫時就貼這麼多題,做完這些題,發現二分圖的應用其實特別廣泛,沒有奇環這個性質還是十分容易滿足的,二分圖的性質又非常特殊,以後如果對於圖論的一些覆蓋、匹配問題,一定可以看看是否是一個二分圖再做。




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