二分图匹配(匈牙利算法)

  • 详细讲解:
    http://blog.csdn.net/pi9nc/article/details/11848327
  • 以下为粗略概括:
  • 二分图定义:
    简单来说,就是图中的点可以被分为两组,并且使得所有的边都跨越组的边界,则这就是一个二分图。另一个等价定义是:不含有“奇数条边的环”的图。

  • 匹配:
    在图论中,匹配是边的集合,这个集合中任意两条边都没有公共定点。

  • 最大匹配:
    一个图的所有匹配中,所喊匹配变数最多的匹配,称为这个图的最大匹配。

  • 完美匹配:
    如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。

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

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

  • 匈牙利算法:
    增广路有一个重要特点,非匹配边比匹配边多一条。因此,增广路的意义是改进匹配。只要把增广路中的匹配边和非匹配边的身份交换即可。这样既不会破坏匹配的性质,交换后,途中的匹配边数比原来多了一条。我们可以通过不停地找增广路来增加匹配中的匹配边和匹配点。找不到增广路的时候,达到最大匹配(这是增广路定理)。

  • 下面给出匈牙利算法的DFS和BFS代码:

  • 邻接表储存(顶点,边的编号均从0开始):

struct Edge
{
    int from;
    int to;
    int weight;
    Edge(int f,int t, int w):from(f),to(t), weight(w){}
};

vector<int>G[__maxNodes];
/* G[i] 存储顶点 i 出发的边的编号 */

vector<Edge> edges;

typedef vector<int>::iterator iterator_t;

int num_nodes;

int num_left;

int num_right;

int num_edges;
  • DFS版本:
int matching[__maxNodes]; // 存储求解结果
int check[__maxNodes];

bool DFS(int u)
{
    for(iterator_ti = G[u].begin(); i!=G[u].end(); i++)//对u的每个邻接点
    {
        int v = edges[*i].to;
        if(!check[v])//要求不在交替路中
        {  
            check[v] = true; //放入交替路
            if(matching[v] == -1 || DFS(matching[v])) //如果是未覆盖点,说明交替路为增广路,则交换路径,并返回成功
            {
                matching[v] = u;
                matching[u] = v;
                return true;
            }
        }
    }
    return false //不存在增广路,返回失败
}

int Hungarian()
{
    int ans = 0;
    memset(matching, -1, sizeof(matching));
    for(int u = 0;u<num_left;u++)
    {
        if(matching[u] == -1)
        {
            memset(check, 0, sizeof(check));
            if(DFS(u))
                ans++;
        }
    }
    return ans;
}
  • BFS版本:
queue<int> Q;

int pre[__maxNodes];

int Hungarian()
{
    int ans = 0;
    memset(matching, -1, sizeof(matching));
    memset(check, -1, sizeof(check));
    for(int i=0;i<num_left;i++)
    {
        if(matching[i] == -1)
        {
            while(!Q.empty())
                Q.pop();
            Q.push(i);
            prev[] = -1;//设i为路径起点
            bool flag = false;//尚未找到增广路
            while(!Q.empty()&&!flag)
            {
                int u = Q.front(); 
                for(iterator_tix = G[u].begin(); ix!=G[u].end()&&!flag;ix++)
                {
                    int v = edges[*ix].eo;
                    if(check[v]!=i)
                    {
                        check[v] = i;
                        Q.push(matching[v]);
                        if(matching[v] >= 0)
                        {//此为匹配点
                            prev[matching[v]] = u;
                        }
                        else
                        {//找到未匹配点,交替路变增广路
                            flag = true;
                            int d=u,e=v;
                            while(d!=-1)
                            {
                                int t=mathcing[d];
                                matching[d] = e;
                                matching[e] = d;
                                d = prev[d];
                                e = t;
                            }
                        }
                    }
                }
                Q.pop();
            }
            if(matching[i]!=-1)
                ans++;
        }
    }
    return ans;
}
  • 匈牙利算法的要点:

    1. 从左边第 1 个顶点开始,挑选未匹配点进行搜索,寻找增广路。
      1. 如果经过一个未匹配点,说明寻找成功。更新路径信息,匹配边数 +1,停止搜索。
      2. 如果一直没有找到增广路,则不再从这个点开始搜索。事实上,此时搜索后会形成一棵匈牙利树。我们可以永久性地把它从图中删去,而不影响结果。
    2. 由于找到增广路之后需要沿着路径更新匹配,所以我们需要一个结构来记录路径上的点。DFS 版本通过函数调用隐式地使用一个栈,而 BFS 版本使用 prev 数组。
  • 性能比较:
    两个版本的时间复杂度均为 O(V⋅E) 。DFS 的优点是思路清晰、代码量少,但是性能不如 BFS。我测试了两种算法的性能。对于稀疏图,BFS 版本明显快于 DFS 版本;而对于稠密图两者则不相上下。在完全随机数据 9000 个顶点 4,0000 条边时前者领先后者大约 97.6%,9000 个顶点 100,0000 条边时前者领先后者 8.6%, 而达到 500,0000 条边时 BFS 仅领先 0.85%。

  • 定理:
    最小点覆盖 = 最大匹配数;
    最小路径覆盖 = 总点数 - 最大匹配数;
    最大独立集数 = 总点数 - 最大匹配数;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值