二分图大合集——二分图最大匹配(最小覆盖数),完美匹配以及最优匹配(带权最大匹配)

二分图:

定义:二分图又称作二部图,是图论的一种特殊模型。设G=(V, E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A , B),且图中的每条边(i, j)所关联的两个定点分别属于这两个不同的顶点集(i in A, j in B),则称图G为一个二分图。

简单的说,一个图被分成了两部分,相同的部分没有边,那这个图就是二分图,二分图是特殊的图。(不含奇环)
由定义可知,二分图没有自回路(关联于同一结点的一条边);零图,平凡图(指仅有一个结点的图)可以看成是特殊的二分图

判断是否为二分图的方法:
①定义法
关键是看点集是否能分成两个独立的点集。
这里写图片描述
上图中U和V构造的点集所形成的循环圈不为奇数,所以是二分图。
这里写图片描述
上图中U和V和W构造的点集所形成的的循环圈为奇数,所以不是二分图。

②定理:无向图 G= < V, E >为二分图的充要条件是G的所有回路的长度均为偶数。

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

const int M = 10050;
int color[M], line[M][M];

//0为白,1为黑
bool bfs(int s, int n){
    int i, beg;
    queue <int> q;
    q.push(s);
    color[s] = 1;
    while(!q.empty()){
        beg = q.front();
        q.pop();
        for(i = 1; i <= n; i++){
            if(line[beg][i] && color[i] == 0){
                q.push(i);
                color[i] = !color[beg];//染成不同颜色
            }
            if(line[beg][i] && color[beg] == color[i]){//颜色相同则不是二分图
                return 0;
            }
        }
    }
    return 1;
}

int main(){
    int i, j, n, m, a, b;//n表示有多少个点,m表示有多少条边
    bool flag = 1;//初始化无向图是二分图
    memset(color, 0, sizeof(color));
    scanf("%d%d", &n, &m);
    for(i = 1; i <= m; i++) {
        cin >> a >> b;
        line[a][b] = line[b][a] = 1;//存储无向图的边
    }
    for(i = 1; i <= n; i++)
        if(color[i] == 0 && !bfs(i, n)) {//对每个连通分支染色,如果两个相邻的点颜色相同,则不是二分图。
            flag = 0;
            break;
        }
    if(flag)
        cout << "YES" <<endl;
    else
        cout << "NO" <<endl;
    return 0;
}

匹配:

  1. 定义:给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。
    匹配点:匹配边上的两点

  2. 极大匹配(Maximal Matching):是指在当前已完成的匹配下,无法再通过增加未完成匹配的边的方式来增加匹配的边数。

  3. 最大匹配(maximum matching):是所有极大匹配当中边数最大的一个匹配,设为M。选择这样的边数最大的子集称为图的最大匹配问题。

  4. 完美匹配(完备匹配):一个图中所有的顶点都是匹配点的匹配,即2|M| = |V|。完美匹配一定是最大匹配,但并非每个图都存在完美匹配。

  5. 最优匹配:最优匹配又称为带权最大匹配,是指在带有权值边的二分图中,求一个匹配使得匹配边上的权值和最大。一般X和Y集合顶点个数相同,最优匹配也是一个完备匹配,即每个顶点都被匹配。如果个数不相等,可以通过补点加0边实现转化。一般使用KM算法解决该问题。(KM(Kuhn and Munkres)算法,是对匈牙利算法的一种贪心扩展。)

  6. 最小覆盖
    二分图的最小覆盖分为最小顶点覆盖和最小路径覆盖:

    ①最小顶点覆盖是指最少的顶点数使得二分图G中的每条边都至少与其中一个点相关联
    注:二分图的最小顶点覆盖数=二分图的最大匹配数

    ②最小路径覆盖也称为最小边覆盖,是指用尽量少的不相交简单路径覆盖二分图中的所有顶点。
    注:二分图的最小路径覆盖数=|V|-二分图的最大匹配数

  7. 最大独立集
    最大独立集是指寻找一个点集,使得其中任意两点在图中无对应边。对于一般图来说,最大独立集是一个NP完全问题,对于二分图来说最大独立集=|V|-二分图的最大匹配数。最大独立集S 与 最小覆盖集T 互补

如果在图G左右两边加上源汇点后,图G等价于一个网络流,二分图最大匹配问题可以转为最大流的问题。解决此问题的匈牙利算法的本质就是寻找最大流的增广路径。
注意:匈牙利算法,除了二分图多重匹配外在二分图匹配中都可以使用。
二分图匹配中还有一个hk算法,复杂度为o(sqrt(n)*e)复杂度降低较低,代码量飙升,不过hdu2389卡这个算法

交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。

增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(agumenting path)。

增广路有一个重要特点:非匹配边比匹配边多一条。因此,研究增广路的意义是改进匹配。只要把增广路中的匹配边和非匹配边的身份交换即可。由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质。交换后,图中的匹配边数目比原来多了 1 条。

我们可以通过不停地找增广路来增加匹配中的匹配边和匹配点。找不到增广路时,达到最大匹配(这是增广路定理)。匈牙利算法正是这么做的。

匈牙利算法讲解:

初始时最大匹配为空
while 找得到增广路径
do 把增广路径加入到最大匹配中去

(注:匈牙利算法虽然根本上是最大流算法,但是它不需要建网络模型,所以图中不再需要源点和汇点,仅仅是一个二分图。每条边也不需要有方向。)

匈牙利树一般由 BFS 构造(类似于 BFS 树)。从一个未匹配点出发运行 BFS(唯一的限制是,必须走交替路),直到不能再扩展为止。
注:匈牙利树中所有叶子节点均为匹配点

注:以下转自 http://blog.csdn.net/dark_scope/article/details/8880547

匈牙利算法是由匈牙利数学家Edmonds于1965年提出,因而得名。匈牙利算法是基于Hall定理中充分性证明的思想,它是部图匹配最常见的算法,该算法的核心就是寻找增广路径,它是一种用增广路径求二分图最大匹配的算法。

——-等等,看得头大?那么请看下面的版本:

通过数代人的努力,你终于赶上了剩男剩女的大潮,假设你是一位光荣的新世纪媒人,在你的手上有N个剩男,M个剩女,每个人都可能对多名异性有好感(惊讶-_-||暂时不考虑特殊的性取向),如果一对男女互有好感,那么你就可以把这一对撮合在一起,现在让我们无视掉所有的单相思(好忧伤的感觉快哭了),你拥有的大概就是下面这样一张关系图,每一条连线都表示互有好感。
这里写图片描述
本着救人一命,胜造七级浮屠的原则,你想要尽可能地撮合更多的情侣,匈牙利算法的工作模式会教你这样做:

===============================================================================

一: 先试着给1号男生找妹子,发现第一个和他相连的1号女生还名花无主,got it,连上一条蓝线
这里写图片描述

===============================================================================

二:接着给2号男生找妹子,发现第一个和他相连的2号女生名花无主,got it

这里写图片描述

三:接下来是3号男生,很遗憾1号女生已经有主了,怎么办呢?

我们试着给之前1号女生匹配的男生(也就是1号男生)另外分配一个妹子。

(黄色表示这条边被临时拆掉)
这里写图片描述
与1号男生相连的第二个女生是2号女生,但是2号女生也有主了,怎么办呢?我们再试着给2号女生的原配(发火发火)重新找个妹子(注意这个步骤和上面是一样的,这是一个递归的过程)
这里写图片描述
此时发现2号男生还能找到3号女生,那么之前的问题迎刃而解了,回溯回去

2号男生可以找3号妹子~~ 1号男生可以找2号妹子了~~~ 3号男生可以找1号妹子
这里写图片描述这里写图片描述这里写图片描述
所以第三步最后的结果就是:

这里写图片描述

四: 接下来是4号男生,很遗憾,按照第三步的节奏我们没法给4号男生腾出来一个妹子,我们实在是无能为力了……香吉士同学走好。

===============================================================================
这就是匈牙利算法的流程,其中找妹子是个递归的过程,最最关键的字就是“腾”字

其原则大概是:有机会上,没机会创造机会也要上

以下代码改编自:http://dsqiu.iteye.com/blog/1689505
大佬绝对的大佬~~渣渣弱弱的瑟瑟发抖,不敢说话

二分图最大匹配/最小覆盖数之dfs实现

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

typedef long long ll;
const int inf=0x3f3f3f3f;
const int maxn = 10005;
int edge[maxn][maxn];//edge[i][j] == 1 代表i,j可以匹配
int vis[maxn];//用来记录该点是否被访问过
int cx[maxn], cy[maxn];//用来记录x集合中匹配的y元素是哪个
int nx, ny;//nx表示x集合的顶点个数, ny表示y集合的顶点个数

int line(int u){
    int v;
    for(v = 0; v < ny; v++){
        if(edge[u][v] && !vis[v]){
            vis[v] = 1;
            if(cy[v] == -1 || line(cy[v])){//如果y集合中的v没有匹配或v已经匹配,但从cy[v]中可以找到一条增广路
                cx[u] = v;//找到增广路,修改匹配
                cy[v] = u;
                return 1;
            }
        }
    }
    return 0;
}

int maxmatch(){
    int sum = 0, i;
    memset(cx, 0xff, sizeof(cx));//初始值为-1表示两个集合中都没有匹配的元素
    memset(cy, 0xff, sizeof(cy));
    for(i = 0; i <= nx; i++){
        if(cx[i] == -1){//还没被匹配就执行内部代码
            memset(vis, 0, sizeof(vis));//重置标记为未访问
            sum += line(i);//以 i 为起点开始查找增广路,返回true ,匹配数+1
        }
    }
    return sum;
}
int main(){
    int x, y, t;
    while(scanf("%d", &t) && t){
        scanf("%d%d", &nx, &ny);
        memset(edge, 0, sizeof(edge));
        for(int i = 0; i < t; i++){
            scanf("%d%d", &x, &y);
            edge[x][y] = 1;
        }
        int sum = maxmatch();
        printf("%d\n", sum);
    }
    return 0;
}

Kuhn-Munkers算法流程

(1)初始化可行顶标的值
(2)用匈牙利算法寻找完备匹配
(3)若未找到完备匹配则修改可行顶标的值
(4)重复(2)(3)直到找到相等子图的完备匹配为止

二分图完备匹配判定
代码摘自:http://www.cnblogs.com/moonbay/archive/2012/08/16/2642042.html

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

const int MAXN = 305;
vector<int> mymap[MAXN];
int N, M, mymatch[MAXN];
bool visited[MAXN];

void init() {
    for(int i = 0; i < N; i++) {
        mymap[i].clear();
    }
}

bool dfs(int k) {
    int t, I;
    for(int i = 0; i < (int)mymap[k].size(); i++) {
        I = mymap[k][i];
        if(!visited[I]) {
            visited[I] = true;
            t = mymatch[I];
            mymatch[I] = k;
            if(t == -1 || dfs(t)) {
                return true;
            }
            mymatch[I] = t;
        }
    }
    return false;
}

int hungary () {
    memset(mymatch, -1, sizeof(mymatch));
    int ans = 0;
    for (int i = 0; i < N; i++) {
        memset(visited, false, sizeof(visited));
        if (dfs(i)) {
            ans++;
        }
    }
    return ans;
}

bool buildgraph() {
    int t, K;
    init();
    for (int i = 0; i < N; i++) {
        scanf("%d", &K);
        for(int k = 0; k < K; k++) {
            scanf("%d", &t);
            mymap[i].push_back(t - 1);
        }
    }
    return true;
}

int main() {
    int T, P;
    scanf("%d", &T);
    while(T--) {
        scanf("%d%d", &N, &P);
        buildgraph();
        puts(hungary() == N ? "YES" : "NO");
    }
    return 0;
}

二分图最优匹配/带权最大匹配
代码选自:https://blog.csdn.net/pi9nc/article/details/12250247

 #include <iostream>  
#include <cstdio>  
#include <memory.h>  
#include <algorithm>   

using namespace std;  

#define MAX 100  

int n;  
int weight[MAX][MAX];           //权重  
int lx[MAX],ly[MAX];                //定点标号  
bool sx[MAX],sy[MAX];          //记录寻找增广路时点集x,y里的点是否搜索过  
int match[MAX];                       //match[i]记录y[i]与x[match[i]]相对应  

bool search_path(int u) {          //给x[u]找匹配,这个过程和匈牙利匹配是一样的  
    sx[u]=true;  
    for(int v=0; v<n; v++){  
        if(!sy[v] &&lx[u]+ly[v] == weight[u][v]){  
            sy[v]=true;  
            if(match[v]==-1 || search_path(match[v])){  
                match[v]=u;  
                return true;  
            }  
        }  
    }  
    return false;  
}  

int Kuhn_Munkras(bool max_weight){  
    if(!max_weight){ //如果求最小匹配,则要将边权取反  
        for(int i=0;i<n;i++)  
            for(int j=0;j<n;j++)  
                weight[i][j]=-weight[i][j];  
    }  
    //初始化顶标,lx[i]设置为max(weight[i][j] | j=0,..,n-1 ), ly[i]=0;  
    for(int i=0;i<n;i++){  
        ly[i]=0;  
        lx[i]=-0x7fffffff;  
        for(int j=0;j<n;j++)  
            if(lx[i]<weight[i][j])  
                lx[i]=weight[i][j];  
    }  

    memset(match,-1,sizeof(match));  
    //不断修改顶标,直到找到完备匹配或完美匹配  
    for(int u=0;u<n;u++){   //为x里的每一个点找匹配  
        while(1){  
            memset(sx,0,sizeof(sx));  
            memset(sy,0,sizeof(sy));  
            if(search_path(u))       //x[u]在相等子图找到了匹配,继续为下一个点找匹配  
                break;  
            //如果在相等子图里没有找到匹配,就修改顶标,直到找到匹配为止  
            //首先找到修改顶标时的增量inc, min(lx[i] + ly [i] - weight[i][j],inc);,lx[i]为搜索过的点,ly[i]是未搜索过的点,因为现在是要给u找匹配,所以只需要修改找的过程中搜索过的点,增加有可能对u有帮助的边  
            int inc=0x7fffffff;  
            for(int i=0;i<n;i++)  
                if(sx[i])  
                    for(int j=0;j<n;j++)  
                        if(!sy[j]&&((lx[i] + ly [j] - weight[i][j] )<inc))  
                            inc = lx[i] + ly [j] - weight[i][j] ;  
            //找到增量后修改顶标,因为sx[i]与sy[j]都为真,则必然符合lx[i] + ly [j] =weight[i][j],然后将lx[i]减inc,ly[j]加inc不会改变等式,但是原来lx[i] + ly [j] !=weight[i][j]即sx[i]与sy[j]最多一个为真,lx[i] + ly [j] 就会发生改变,从而符合等式,边也就加入到相等子图中  
            if(inc==0)  cout<<"fuck!"<<endl;  
            for(int i=0;i<n;i++){  
                if(sx[i])   //  
                    lx[i]-=inc;  
                if(sy[i])  
                    ly[i]+=inc;  
            }  
        }  

    }  
    int sum=0;  
    for(int i=0;i<n;i++)  
        if(match[i]>=0)  
            sum+=weight[match[i]][i];  

    if(!max_weight)  
        sum=-sum;  
    return sum;  


}  
int main(){  

    scanf("%d",&n);  
    for(int i=0;i<n;i++)  
        for(int j=0;j<n;j++)  
            scanf("%d",&weight[i][j]);  
    printf("%d\n",Kuhn_Munkras(1));  
    system("pause");  
    return 0;  
}

Kuhn-Munkers算法的几种变形应用

1.要求最小权完备匹配只需将所有的边权值取其相反数,求最大权完备匹配,匹配的值再取相反数即可。

2.Kuhn-Munkers算法的运行要求是必须存在一个完备匹配,如果求一个最大权匹配(不一定完备)可以把不存在的边权值赋为0。

3.若要边权之积最大则每条边权取自然对数,然后求最大和权匹配,求得的结果a再算出e^a就是最大积匹配。

学的有点恶心。。。到了完全匹配那里已经不想学了。。。先这样半原创半转载的写吧。。。让我先缓两天~

  • 38
    点赞
  • 195
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值