关于二分图的最大匹配(最小点覆盖)问题的资料整理

关于二分图最大匹配(最小点覆盖)问题的资料整理(一)

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明http://programmers-in.blogbus.com/logs/47011285.html

用匈牙利算法求二分图的最大匹配
什么是二分图,什么是二分图的最大匹配,这些定义我就不讲了,网上随便都找得到。二分图的最大匹配有两种求法,第一种是最大流(我在此假设读者已有网络流的知识);第二种就是我现在要讲的匈牙利算法。这个算法说白了就是最大流的算法,但是它跟据二分图匹配这个问题的特点,把最大流算法做了简化,提高了效率。匈牙利算法其实很简单,但是网上搜不到什么说得清楚的文章。所以我决定要写一下。
最大流算法的核心问题就是找增广路径(augment path)。匈牙利算法也不例外,它的基本模式就是:

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

可见和最大流算法是一样的。但是这里的增广路径就有它一定的特殊性,下面我来分析一下。
(注:匈牙利算法虽然根本上是最大流算法,但是它不需要建网络模型,所以图中不再需要源点和汇点,仅仅是一个二分图。每条边也不需要有方向。)

 


图1是我给出的二分图中的一个匹配:[1,5]和[2,6]。图2就是在这个匹配的基础上找到的一条增广路径:3->6->2->5->1->4。我们借由它来描述一下二分图中的增广路径的性质:

(1)有奇数条边。
(2)起点在二分图的左半边,终点在右半边。
(3)路径上的点一定是一个在左半边,一个在右半边,交替出现。(其实二分图的性质就决定了这一点,因为二分图同一边的点之间没有边相连,不要忘记哦。)
(4)整条路径上没有重复的点。
(5)起点和终点都是目前还没有配对的点,而其它所有点都是已经配好对的。(如图1、图2所示,[1,5]和[2,6]在图1中是两对已经配好对的点;而起点3和终点4目前还没有与其它点配对。)
(6)路径上的所有第奇数条边都不在原匹配中,所有第偶数条边都出现在原匹配中。(如图1、图2所示,原有的匹配是[1,5]和[2,6],这两条配匹的边在图2给出的增广路径中分边是第2和第4条边。而增广路径的第1、3、5条边都没有出现在图1给出的匹配中。)
(7)最后,也是最重要的一条,把增广路径上的所有第奇数条边加入到原匹配中去,并把增广路径中的所有第偶数条边从原匹配中删除(这个操作称为增广路径的取反),则新的匹配数就比原匹配数增加了1个。(如图2所示,新的匹配就是所有蓝色的边,而所有红色的边则从原匹配中删除。则新的匹配数为3。)

不难想通,在最初始时,还没有任何匹配时,图1中的两条灰色的边本身也是增广路径。因此在这张二分图中寻找最大配匹的过程可能如下:

(1)找到增广路径1->5,把它取反,则匹配数增加到1。
(2)找到增广路径2->6,把它取反,则匹配数增加到2。
(3)找到增广路径3->6->2->5->1->4,把它取反,则匹配数增加到3。
(4)再也找不到增广路径,结束。

当然,这只是一种可能的流程。也可能有别的找增广路径的顺序,或者找到不同的增广路径,最终的匹配方案也可能不一样。但是最大匹配数一定都是相同的。

对于增广路径还可以用一个递归的方法来描述。这个描述不一定最准确,但是它揭示了寻找增广路径的一般方法:
“从点A出发的增广路径”一定首先连向一个在原匹配中没有与点A配对的点B。如果点B在原匹配中没有与任何点配对,则它就是这条增广路径的终点;反之,如果点B已与点C配对,那么这条增广路径就是从A到B,再从B到C,再加上“从点C出发的增广路径”。并且,这条从C出发的增广路径中不能与前半部分的增广路径有重复的点。

比如图2中,我们要寻找一条从3出发的增广路径,要做以下3步:
(1)首先从3出发,它能连到的点只有6,而6在图1中已经与2配对,所以目前的增广路径就是3->6->2再加上从2出发的增广路径。
(2)从2出发,它能连到的不与前半部分路径重复的点只有5,而且5确实在原匹配中没有与2配对。所以从2连到5。但5在图1中已经与1配对,所以目前的增广路径为3->6->2->5->1再加上从1出发的增广路径。
(3)从1出发,能连到的不与自已配对并且不与前半部分路径重复的点只有4。因为4在图1中没有与任何点配对,所以它就是终点。所以最终的增广路径是3->6->2->5->1->4。

但是严格地说,以上过程中从2出发的增广路径(2->5->1->4)和从1出发的增广路径(1->4)并不是真正的增广路径。因为它们不符合前面讲过的增广路径的第5条性质,它们的起点都是已经配过对的点。我们在这里称它们为“增广路径”只是为了方便说明整个搜寻的过程。而这两条路径本身只能算是两个不为外界所知的子过程的返回结果。
显然,从上面的例子可以看出,搜寻增广路径的方法就是DFS,可以写成一个递归函数。当然,用BFS也完全可以实现。

至此,理论基础部份讲完了。但是要完成匈牙利算法,还需要一个重要的定理:

如果从一个点A出发,没有找到增广路径,那么无论再从别的点出发找到多少增广路径来改变现在的匹配,从A出发都永远找不到增广路径。

要用文字来证明这个定理很繁,话很难说,要么我还得多画一张图,我在此就省了。其实你自己画几个图,试图举两个反例,这个定理不难想通的。(给个提示。如果你试图举个反例来说明在找到了别的增广路径并改变了现有的匹配后,从A出发就能找到增广路径。那么,在这种情况下,肯定在找到别的增广路径之前,就能从A出发找到增广路径。这就与假设矛盾了。)
有了这个定理,匈牙利算法就成形了。如下:

初始时最大匹配为空
for 二分图左半边的每个点i
    do 从点i出发寻找增广路径。如果找到,则把它取反(即增加了总了匹配数)。

如果二分图的左半边一共有n个点,那么最多找n条增广路径。如果图中共有m条边,那么每找一条增广路径(DFS或BFS)时最多把所有边遍历一遍,所花时间也就是m。所以总的时间大概就是O(n * m)。

在UVA上,二分图匹配的题目有67010080,祝好运。

以下是我的标程。是用BFS搜索增广路径的。虽然DFS可能写起来比较简单,但是我不想让它递归很多层。
欢迎使用我的标程。

/
//Bipartite graphic and maximum matching with Hungarian algorithm.
/
#include <list>
#include <cstring>

using namespace std;

const int MAX_LEFT = 500;
const int MAX_RIGHT = 500;

class Bipartite {
private:
    struct Edge {
        int to;
        Edge* next;

        Edge(int _to) {
            to = _to;
        }
    };

    Edge* m_adjList[MAX_LEFT];
    int m_lCnt;
    int m_rCnt;
    int m_lMatchR[MAX_RIGHT];
    int m_rMatchL[MAX_LEFT];
    int m_preL[MAX_LEFT];
    bool m_visitR[MAX_RIGHT];
    //This matrix is just used to prevent adding two repeated edges.
    bool m_matrix[MAX_LEFT][MAX_RIGHT];

    void clear() {
        for (int i = 0; i < m_lCnt; i++) {
            Edge* e = m_adjList[i];
            while (e != NULL) {
                Edge* pre = e;
                e = e->next;
                delete pre;
            }
            m_adjList[i] = NULL;
        }
        memset(m_matrix, 0, sizeof(m_matrix));
    }

    void findAugment(int start) {
        for (int i = 0; i < m_lCnt; i++) {
            m_preL[i] = -1;
        }
        memset(m_visitR, 0, sizeof(bool) * m_rCnt);
        list<int> que;
        que.push_back(start);
        bool found = false;
        while (!que.empty() && !found) {
            int from = que.front();
            que.pop_front();
            Edge* edge = m_adjList[from];
            while (edge != NULL && !found) {
                int to = edge->to;
                if (!m_visitR[to]) {
                    m_visitR[to] = true;
                    if (m_rMatchL[to] == -1) {
                        found = true;
                        reverse(from, to);
                    }
                    else {
                        que.push_back(m_rMatchL[to]);
                        m_preL[m_rMatchL[to]] = from;
                    }
                }
                edge = edge->next;
            }
        }
    }

    void reverse(int left, int right) {
        m_rMatchL[right] = left;
        while(m_preL[left] != -1) {
            int nextR = m_lMatchR[left];
            m_rMatchL[nextR] = m_preL[left];
            m_lMatchR[left] = right;
            left = m_preL[left];
            right = nextR;
        }
        m_lMatchR[left] = right;
    }

public:
    Bipartite() {
        memset(m_adjList, 0, sizeof(m_adjList));
        m_lCnt = 0;
        m_rCnt = 0;
    }

    ~Bipartite() {
        clear();
    }

    //Add an edge between vertex "left" and "right" while "left" and "right" are
    //the indices of two vertices in the left/right parts of the graph. Indices
    //in the left and right parts are separated and they both begin from 0.
    void addEdge(int left, int right) {
        if (!m_matrix[left][right]) {
            m_matrix[left][right] = true;
            Edge* newEdge = new Edge(right);
            newEdge->next = m_adjList[left]; 
            m_adjList[left] = newEdge;
        }
    }

    //Before invoking this function, "maxMatch()" must be invoked. This function
    //returns the index of the matching vertex of "left" while "left" is the
    //index of a vertex in the left part of the graphic.
    int getLMatchR(int left) const {
        return m_lMatchR[left];
    }

    //See "getLMatchR()", and this function is opposite to it.
    int getRMatchL(int right) const {
        return m_rMatchL[right];
    }

    void init(int leftCnt, int rightCnt) {
        clear();
        m_lCnt = leftCnt;
        m_rCnt = rightCnt;
        for (int i = 0; i < m_lCnt; i++) {
            m_lMatchR[i] = -1;
        }
        for (int i = 0; i < m_rCnt; i++) {
            m_rMatchL[i] = -1;
        }
    }

    int maxMatch() {
        for (int i = 0; i < m_lCnt; i++) {
            findAugment(i);
        }
        int result = 0;
        for (int i = 0; i < m_lCnt; i++) {
            if (m_lMatchR[i] != -1) {
                result++;
            }
        }
        return result;
    }
};

//Test suites.
#include <iostream>

int main() {
    Bipartite match;
    match.init(300, 400);
    int a[] = {0, 0, 1, 1, 2, 2, 2};
    int b[] = {1, 2, 1, 3, 0, 1, 2};
    for (int i = 0; i < 7; i++) {
        match.addEdge(a[i], b[i]);
    }
    int maxMatch = match.maxMatch();
    cout << maxMatch << " ";
    for (int i = 0; i < 3; i++) {
        cout << match.getLMatchR(i) << " ";
    }
    for (int i = 0; i < 4; i++) {
        cout << match.getRMatchL(i) << " ";
    }
    cout << endl;//Correct: 3 2 3 1 -1 2 0 1
    return 0;
}

 

 


关于二分图最大匹配(最小点覆盖)问题的资料整理(二)

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明http://programmers-in.blogbus.com/logs/47011461.html

二分图匹配算法总结

转自http://old.blog.edu.cn/user3/Hailer/archives/2007/1829623.shtml

二分图最大匹配的匈牙利算法 

 二分图是这样一个图,它的顶点可以分类两个集合X和Y,所有的边关联在两个顶点中,恰好一个属于集合X,另一个属于集合Y。

最大匹配: 图中包含边数最多的匹配称为图的最大匹配。 

完美匹配: 如果所有点都在匹配边上,称这个最大匹配是完美匹配。

最小覆盖: 最小覆盖要求用最少的点(X集合或Y集合的都行)让每条边都至少和其中一个点关联。可以证明:最少的点(即覆盖数)=最大匹配数

最小路径覆盖:

用尽量少的不相交简单路径覆盖有向无环图G的所有结点。解决此类问题可以建立一个二分图模型。把所有顶点i拆成两个:X结点集中的i和Y结点集中的i',如果有边i->j,则在二分图中引入边i->j',设二分图最大匹配为m,则结果就是n-m。

最大独立集问题:

在N个点的图G中选出m个点,使这m个点两两之间没有边.求m最大值.

如果图G满足二分图条件,则可以用二分图匹配来做.最大独立集点数 = N - 最大匹配数

 

二分图最大匹配问题的匈牙利算法:

 

#define N 202

int useif[N];   //记录y中节点是否使用

int link[N];   //记录当前与y节点相连的x的节点

int mat[N][N]; //记录连接x和y的边,如果i和j之间有边则为1,否则为0

int gn,gm;    //二分图中x和y中点的数目

int can(int t)

{

    int i;

    for(i=1;i<=gm;i++)

    {

       if(useif[i]==0 && mat[t][i])

       {

           useif[i]=1;

           if(link[i]==-1 || can(link[i]))

           {

              link[i]=t;

              return 1;

           }

       }

    }

    return 0;

}

int MaxMatch()

 

{

    int i,num;

    num=0;

    memset(link,0xff,sizeof(link));

    for(i=1;i<=gn;i++)

    {

       memset(useif,0,sizeof(useif));

       if(can(i)) num++;

    }

    return num;

}

算法思想:

 

算法的思路是不停的找增广轨,并增加匹配的个数,增广轨顾名思义是指一条可以使匹配数变多的路径,在匹配问题中,增广轨的表现形式是一条"交错轨",也就是说这条由图的边组成的路径,它的第一条边是目前还没有参与匹配的,第二条边参与了匹配,第三条边没有..最后一条边没有参与匹配,并且始点和终点还没有被选择过.这样交错进行,显然他有奇数条边.那么对于这样一条路径,我们可以将第一条边改为已匹配,第二条边改为未匹配...以此类推.也就是将所有的边进行"反色",容易发现这样修改以后,匹配仍然是合法的,但是匹配数增加了一对.另外,单独的一条连接两个未匹配点的边显然也是交错轨.可以证明,当不能再找到增广轨时,就得到了一个最大匹配.这也就是匈牙利算法的思路.

 

 

 

一、二分图最大匹配

 

    二分图最大匹配的经典匈牙利算法是由Edmonds在1965年提出的,算法的核心就是根据一个初始匹配不停的找增广路,直到没有增广路为止。

匈牙利算法的本质实际上和基于增广路特性的最大流算法还是相似的,只需要注意两点:

(一)每个X节点都最多做一次增广路的起点;

(二)如果一个Y节点已经匹配了,那么增广路到这儿的时候唯一的路径是走到Y节点的匹配点(可以回忆最大流算法中的后向边,这个时候后向边是可以增流的)。

    找增广路的时候既可以采用dfs也可以采用bfs,两者都可以保证O(nm)的复杂度,因为每找一条增广路的复杂度是O(m),而最多增广n次,dfs在实际实现中更加简短。

 

二、Hopcroft-Karp算法

 

    SRbGa很早就介绍过这个算法,它可以做到O(sqrt(n)*e)的时间复杂度,并且在实际使用中效果不错而且算法本身并不复杂。

    Hopcroft-Karp算法是Hopcroft和Karp在1972年提出的,该算法的主要思想是在每次增广的时候不是找一条增广路而是同时找几条不相交的最短增广路,形成极大增广路集,随后可以沿着这几条增广路同时进行增广。

    可以证明在寻找增广路集的每一个阶段所寻找到的最短增广路都具有相等的长度,并且随着算法的进行最短增广路的长度是越来越长的,更进一步的分析可以证明最多只需要增广ceil(sqrt(n))次就可以得到最大匹配(证明在这里略去)。

    因此现在的主要难度就是在O(e)的时间复杂度内找到极大最短增广路集,思路并不复杂,首先从所有X的未盖点进行BFS,BFS之后对每个X节点和Y节点维护距离标号,如果Y节点是未盖点那么就找到了一条最短增广路,BFS完之后就找到了最短增广路集,随后可以直接用DFS对所有允许弧(dist[y]=dist[x]+1,可以参见高流推进HLPP的实现)进行类似于匈牙利中寻找增广路的操作,这样就可以做到O(m)的复杂度。

    实现起来也并不复杂,对于两边各50000个点,200000条边的二分图最大匹配可以在1s内出解,效果很好:)

 

三、二分图最优匹配

 

    二分图最优匹配的经典算法是由Kuhn和Munkres独立提出的KM算法,值得一提的是最初的KM算法是在1955年和1957年提出的,因此当时的KM算法是以矩阵为基础的,随着匈牙利算法被Edmonds提出之后,现有的KM算法利用匈牙利树可以得到更漂亮的实现。

    KM算法中的基本概念是可行顶标(feasible vertex labeling),它是节点的实函数并且对于任意弧(x,y)满足l(x)+l(y)≥w(x,y),此外一个概念是相等子图,它是G的一个生成子图,但是只包含满足l(xi)+l(yj)=w(xi,yj)的所有弧(xi,yj)。

    有定理:如果相等子图有完美匹配,那么该匹配是最大权匹配,证明非常直观也非常简单,反设其他匹配是最优匹配,它的权必然比相等子图的完美匹配的权要小。

    KM算法主要就是控制了怎样修改可行顶标的策略使得最终可以达到一个完美匹配,首先任意设置可行顶标(如每个X节点的可行顶标设为它出发的所有弧的最大权,Y节点的可行顶标设为0),然后在相等子图中寻找增广路,找到增广路就沿着增广路增广。

    而如果没有找到增广路呢,那么就考虑所有现在在匈牙利树中的X节点(记为S集合),所有现在在匈牙利树中的Y节点(记为T集合),考察所有一段在S集合,一段在not T集合中的弧,取

    delta =  min {l(xi)+l(yj)-w(xi,yj),xi ∈ S, yj ∈ not T}

    明显的,当我们把所有S集合中的l(xi)减少delta之后,一定会有至少一条属于(S,not T)的边进入相等子图,进而可以继续扩展匈牙利树,为了保证原来属于(S,T)的边不退出相等子图,把所有在T集合中的点的可行顶标增加delta。

    随后匈牙利树继续扩展,如果新加入匈牙利树的Y节点是未盖点,那么找到增广路,否则把该节点的对应的X匹配点加入匈牙利树继续尝试增广。

    复杂度分析:由于在不扩大匹配的情况下每次匈牙利树做如上调整之后至少增加一个元素,因此最多执行n次就可以找到一条增广路,最多需要找n条增广路,故最多执行n^2次修改顶标的操作,而每次修改顶标需要扫描所有弧,这样修改顶标的复杂度就是O(n^2)的,总的复杂度是O(n^4)的。

    事实上我现在看到的几个版本的实现都是这样实现的,但是实际效果还不错,因为这个界通常很难达到。

    对于not T的每个元素yj,定义松弛变量slack(yj) = min{l(xi)+l(yj)-w(xi,yj),xi ∈ S},很明显的每次的delta=min{slack(yj),yj∈ not T},每次增广之后用O(n^2)的时间计算所有点的初始slack,由于生长匈牙利树的时候每条弧的顶标增量相同,因此修改每个slack需要常数时间(注意在修改顶标后和把已盖Y节点对应的X节点加入匈牙利树的时候是需要修改slack的)。这样修改所有slack值时间是O(n)的,每次增广后最多修改n次顶标,那么修改顶标的总时间降为O(n^2),n次增广的总时间复杂度降为O(n^3)。事实上我这样实现之后对于大部分的数据可以比O(n^4)的算法快一倍左右。

 

四、二分图的相关性质

 

    本部分内容主要来自于SRbGa的黑书,因为比较简单,仅作提示性叙述。

    (1) 二分图的最大匹配数等于最小覆盖数,即求最少的点使得每条边都至少和其中的一个点相关联,很显然直接取最大匹配的一段节点即可。

    (2) 二分图的独立数等于顶点数减去最大匹配数,很显然的把最大匹配两端的点都从顶点集中去掉这个时候剩余的点是独立集,这是|V|-2*|M|,同时必然可以从每条匹配边的两端取一个点加入独立集并且保持其独立集性质。

(3) DAG的最小路径覆盖,将每个点拆点后作最大匹配,结果为n-m,求具体路径的时候顺着匹配边走就可以,匹配边i→j',j→k',k→l'....构成一条有向路径。

 

【最优完备匹配】

对于二分图的每条边都有一个权(非负),要求一种完备匹配方案,使得所有匹配边的权和最大,记做最优完备匹配。(特殊的,当所有边的权为1时,就是最大完备匹配问题)

KM算法:(全称是Kuhn-Munkras,是这两个人在1957年提出的,有趣的是,匈牙利算法是在1965年提出的)

为每个点设立一个顶标Li,先不要去管它的意义。

vi,j-为(i,j)边的权,如果可以求得一个完备匹配,使得每条匹配边vi,j=Li+Lj,其余边vi,j≤Li+Lj。

此时的解就是最优的,因为匹配边的权和=∑Li,其余任意解的权和都不可能比这个大

 

定理:二分图中所有vi,j=Li+Lj的边构成一个子图G,用匈牙利算法求G中的最大匹配,如果该匹配是完备匹配,则是最优完备匹配。

 

问题是,现在连Li的意义还不清楚。

其实,我们现在要求的就是L的值,使得在该L值下达到最优完备匹配。

 

L初始化:

Li=max{wi,j}(i∈x,j∈y)

Lj=0

 

建立子图G,用匈牙利算法求G的最大匹配,如果在某点i (i∈x)找不到增广轨,则得不到完备匹配。

此时需要对L做一些调整:

S为寻找从i出发的增广轨时访问的x中的点的集合,T为访问的y中的点的集合。

找到一个改进量dx,dx=min{Li+Lj-wi,j}(i∈S,j不∈T)

Li=Li-dx (i∈S)

Li=Li+dx (i∈T)

 

重复以上过程,不断的调整L,直到求出完备匹配为止。

 

从调整过程中可以看出:

每次调整后新子图中在包含原子图中所有的边的基础上添加了一些新边。

每次调整后∑Li会减少dx,由于每次dx取最小,所以保证了解的最优性。

 

复杂度分析:

n为点数,m为边数,从每个点出发寻找增广轨的复杂度是O(m),如果找不到增广轨,对L做调整的复杂度也是O(m),而一次调整或者找到一条增广轨,或者将两个连通分量合成一个,而这两种情况最多都只进行O(n)次,所以总的复杂度是O(nm)

 

扩展:

根据KM算法的实质,可以求出使得所有匹配边的权和最小的匹配方案。

 

L初始化:

Li=min{wi,j}(i∈x,j∈y)

Lj=0

 

dx=min{wi,j-Li-Lj}(i∈S,j不∈T)

Li=Li+dx (i∈S)

Li=Li-dx (i∈T)

 

 

【最优匹配】

与最优完备匹配很相似,但不必以完备匹配为前提。

只要对KM算法作一些修改就可以了:

将原图转换成完全二分图(m=|x||y|),添加原图中不存在的边,并且设该边的权值为0。


 




关于二分图的最大匹配(最小点覆盖)问题的资料整理(三)

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明http://programmers-in.blogbus.com/logs/47011618.html

该文根据
http://imlazy.ycool.com/post.1603708.html
http://blog.edu.cn/user3/Hailer/archives/2007/1829623.shtml
总结而来,向以上两位作者表示感谢。
二分图是这样一个图,它的顶点可以分类两个集合X和Y,所有的边关联在两个顶点中,恰好一个属于集合X,另一个属于集合Y。给定一个二分图G,M为G边集的一个子集,如果M满足当中的任意两条边都不依附于同一个顶点,则称M是一个匹配。图中包含边数最多的匹配称为图的最大匹配。
二分图的最大匹配有两种求法,第一种是最大流;第二种就是我现在要讲的匈牙利算法。这个算法说白了就是最大流的算法,但是它跟据二分图匹配这个问题的特点,把最大流算法做了简化,提高了效率。
最大流算法的核心问题就是找增广路径(augment path)。匈牙利算法也不例外,它的基本模式就是:
初始时最大匹配为空
while 找得到增广路径
    do 把增广路径加入到最大匹配中去
可见和最大流算法是一样的。但是这里的增广路径就有它一定的特殊性,下面我来分析一下。
(注:匈牙利算法虽然根本上是最大流算法,但是它不需要建网络模型,所以图中不再需要源点和汇点,仅仅是一个二分图。每条边也不需要有方向。)
            
图1 图2

图1是我给出的二分图中的一个匹配:[1,5]和[2,6]
图2就是在这个匹配的基础上找到的一条增广路径:3->6->2->5->1->4。我们借由它来描述一下二分图中的增广路径的性质:

(1)有奇数条边。
(2)起点在二分图的左半边,终点在右半边。
(3)路径上的点一定是一个在左半边,一个在右半边,交替出现。(其实二分图的性质就决定了这一点,因为二分图同一边的点之间没有边相连,不要忘记哦。)
(4)整条路径上没有重复的点。
(5)起点和终点都是目前还没有配对的点,而其它所有点都是已经配好对的。(如图1、图2所示,[1,5]和[2,6]在图1中是两对已经配好对的点;而起点3和终点4目前还没有与其它点配对。)
(6)路径上的所有第奇数条边都不在原匹配中,所有第偶数条边都出现在原匹配中。(如图1、图2所示,原有的匹配是[1,5]和[2,6],这两条配匹的边在图2给出的增广路径中分边是第2和第4条边。而增广路径的第1、3、5条边都没有出现在图1给出的匹配中。)
(7)最后,也是最重要的一条,把增广路径上的所有第奇数条边加入到原匹配中去,并把增广路径中的所有第偶数条边从原匹配中删除(这个操作称为增广路径的取反),则新的匹配数就比原匹配数增加了1个。(如图2所示,新的匹配就是所有蓝色的边,而所有红色的边则从原匹配中删除。则新的匹配数为3。)
不难想通,在最初始时,还没有任何匹配时,图1中的两条灰色的边本身也是增广路径。因此在这张二分图中寻找最大配匹的过程可能如下:
(1)找到增广路径1->5,把它取反,则匹配数增加到1。
(2)找到增广路径2->6,把它取反,则匹配数增加到2。
(3)找到增广路径3->6->2->5->1->4,把它取反,则匹配数增加到3。
(4)再也找不到增广路径,结束。
当然,这只是一种可能的流程。也可能有别的找增广路径的顺序,或者找到不同的增广路径,最终的匹配方案也可能不一样。但是最大匹配数一定都是相同的。
对于增广路径还可以用一个递归的方法来描述。这个描述不一定最准确,但是它揭示了寻找增广路径的一般方法:
“从点A出发的增广路径”一定首先连向一个在原匹配中没有与点A配对的点B。如果点B在原匹配中没有与任何点配对,则它就是这条增广路径的终点;反之,如果点B已与点C配对,那么这条增广路径就是从A到B,再从B到C,再加上“从点C出发的增广路径”。并且,这条从C出发的增广路径中不能与前半部分的增广路径有重复的点。
比如图2中,我们要寻找一条从3出发的增广路径,要做以下3步:
(1)首先从3出发,它能连到的点只有6,而6在图1中已经与2配对,所以目前的增广路径就是3->6->2再加上从2出发的增广路径。
(2)从2出发,它能连到的不与前半部分路径重复的点只有5,而且5确实在原匹配中没有与2配对。所以从2连到5。但5在图1中已经与1配对,所以目前的增广路径为3->6->2->5->1再加上从1出发的增广路径。
(3)从1出发,能连到的不与自已配对并且不与前半部分路径重复的点只有4。因为4在图1中没有与任何点配对,所以它就是终点。所以最终的增广路径是3->6->2->5->1->4。
但是严格地说,以上过程中从2出发的增广路径(2->5->1->4)和从1出发的增广路径(1->4)并不是真正的增广路径。因为它们不符合前面讲过的增广路径的第5条性质,它们的起点都是已经配过对的点。我们在这里称它们为“增广路径”只是为了方便说明整个搜寻的过程。而这两条路径本身只能算是两个不为外界所知的子过程的返回结果。
显然,从上面的例子可以看出,搜寻增广路径的方法就是DFS,可以写成一个递归函数。当然,用BFS也完全可以实现。
至此,理论基础部份讲完了。但是要完成匈牙利算法,还需要一个重要的定理:
如果从一个点A出发,没有找到增广路径,那么无论再从别的点出发找到多少增广路径来改变现在的匹配,从A出发都永远找不到增广路径。要用文字来证明这个定理很繁,话很难说,要么我还得多画一张图,我在此就省了。其实你自己画几个图,试图举两个反例,这个定理不难想通的。(给个提示。如果你试图举个反例来说明在找到了别的增广路径并改变了现有的匹配后,从A出发就能找到增广路径。那么,在这种情况下,肯定在找到别的增广路径之前,就能从A出发找到增广路径。这就与假设矛盾了。)
有了这个定理,匈牙利算法就成形了。如下:
初始时最大匹配为空
for 二分图左半边的每个点i
 do 从点i出发寻找增广路径。如果找到,则把它取反(即增加了总了匹配数)
如果二分图的左半边一共有n个点,那么最多找n条增广路径。如果图中共有m条边,那么每找一条增广路径(DFS或BFS)时最多把所有边遍历一遍,所花时间也就是m。所以总的时间大概就是O(n * m)。
总结一下算法思想:
算法的思路是不停的找增广路径,并增加匹配的个数,增广路径顾名思义是指一条可以使匹配数变多的路径,在匹配问题中,增广路径的表现形式是一条"交错路径",也就是说这条由图的边组成的路径,它的第一条边是目前还没有参与匹配的,第二条边参与了匹配,第三条边没有..最后一条边没有参与匹配,并且始点和终点还没有被选择过.这样交错进行,显然他有奇数条边.那么对于这样一条路径,我们可以将第一条边改为已匹配,第二条边改为未匹配...以此类推.也就是将所有的边进行"反色",容易发现这样修改以后,匹配仍然是合法的,但是匹配数增加了一对.另外,单独的一条连接两个未匹配点的边显然也是交错轨.可以证明,当不能再找到增广轨时,就得到了一个最大匹配.这也就是匈牙利算法的思路.
下面是一个简单的实现:
//二分图最大匹配的匈牙利算法详细请访问:
http://blog.csdn.net/china8848
#include <memory.h>
#include <stdio.h>
//分别定义左右最大元素
#define LEFT_MAX 101
#define RIGHT_MAX 301
 
bool useif[RIGHT_MAX];
//link[]记录与右边元素连接的元素,-1表示没有连接
int link[RIGHT_MAX];
//定义左右两边元素
int left_num,right_num;
//array定义左右两边元素是否有连接
bool array[LEFT_MAX][RIGHT_MAX];
 
bool can(int t)
{
    int i;
    
    for(i=0;i<right_num;i++)
    {
       if(!useif[i]&&array[t][i])
       {
           useif[i]=true;
           if(link[i]==-1||can(link[i]))
           {
              link[i]=t;
              return true;
           }
       }
 
    }
    return false;
}
 
int main()
{
    int j,i,k,num,count,temp,temp2;
    //array,num清零,link清为-1
    memset(link,0xFF,sizeof(link));
    memset(array,0,sizeof(array));
    
    num=0;
    //匹配,num为结果
    for(i=0;i<left_num;i++)
    {
       memset(useif,0,sizeof(useif));
       if(can(i))
           num++;
    }
    printf("%d\n",num);
    }
    return 1;
}

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/china8848/archive/2008/04/12/2287769.aspx

 

 

/*  
ID: linjd821  
LANG: C++  
TASK: air raid(pku)  
*/  
/*  
//有向无环图的最小路径覆盖  
//1:拆点+求最大二分匹配M  
//2: 最小路径覆盖就是N-M  
*/  
#include <stdio.h>   
#include <string.h>   
#include <stdlib.h>   
#include <math.h>   
#include <assert.h>   
#include <ctype.h>   
#include <map>   
#include <string>   
#include <set>   
#include <bitset>   
#include <utility>   
#include <algorithm>   
#include <vector>   
#include <stack>   
#include <queue>   
#include <iostream>   
#include <fstream>   
#include <list>   
  
using  namespace  std;   
  
/*******************************************************************************    
匈牙利算法:参考算法《算法设计技巧与分析》p444    
输入:g[][]二维数组为图的链接矩阵,N为点的个数    
输出:ans最大匹配值,pre[]数组:pre[i]表示与点i匹配的点     
********************************************************************************/     
const  int MAXN = 120+5;      
int   N, M;      
int   g[MAXN][MAXN*2];      
bool  used[MAXN*2];      
int   pre[MAXN*2];      
//深搜找交错路径       
bool dfs(int t)      
{      
    int i, tmp;      
    for(i = N+1; i <= 2*N; i++)      
        if(g[t][i] && !used[i])      
        {      
            used[i] = true;      
            if(pre[i]== -1 || dfs(pre[i]))    
            {   
                pre[i] = t;   
                return true;      
            }     
        }      
    return  false;      
}     
  
int  Match()   
{   
    int i, j;   
    int  ans  = 0;   
    memset(pre, -1, sizeof(pre));   
    //贪心初始流,但是ms没有什么效果   
    for(i = 1; i <= n; i++)   
    {   
        for(j = n+1; j <= n+m; j++)   
            if(g[i][j] && pre[j] == -1)   
            {   
                pre[j] = i; ans++;   
                break;   
            }   
        if(j == n+m+1)   
        {   
            memset(used, 0, sizeof(used));   
            if(dfs(i)) ans++;   
        }   
    }   
    return  ans;   
}   
  
int  main()   
{   
    int  i, u, v, T;   
    scanf("%d", &T);   
    while(T--)   
    {   
        scanf("%d %d", &N, &M);   
        memset(g, 0, sizeof(g));   
        for(i = 0; i < M; i++)   
        {   
            scanf("%d %d", &u, &v);   
            g[u][v+N] = 1;   
        }   
        printf("%d\n", N-Match());   
    }   
    return 0;   


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值