二分图匹配总结

二分图算是一个挺神奇的东西,有些不可解的问题到二分图就可解了。


匈牙利算法


二分图的最大匹配可以通过最大流来解,建模并不难,但匈牙利算法无论时间空间代码量都更好一些。

匈牙利算法的流程就不多说了。个人觉得《挑战程序设计竞赛》的范例比较清晰易懂而且很短,不懂的话自己模拟运行两次就可以了。

附上我修改过的版本

vector<int> g[maxn];
int match[1010010], n;
bitset<1010010> vis;
bool dfs(int u){
    vis[u] = 1;
    rep(i, g[u].size()){
        int v = g[u][i];
        int w = match[v];
        if (w == -1 || !vis[w] && dfs(w)){
            match[v] = u;
            match[u] = v;
            return true;
        }
    }
    return false;
}
int bmatch(){
    int ans = 0;
    fill(match, -1);
    rep1(i, n) {
        vis.reset();
        if (dfs(i)) ++ans;
    }
    return ans;
}

要注意几点:

1、建图要注意,左侧1号点和右侧1号点连边千万别g[1].push_back(1); g[u].push_back(v + n)比较好

2、还是建图,只要连左到右的有向边就行了,为什么?因为每次dfs的参数都是左侧点。所以图在存储时vector可以省去右侧的空间。(比如左侧1W个点,右侧100W个点),只要开vector<int> g[10010];就足够了,当然其他数组要开到101W大小)

3、我一开始写的时候会碰到RE,原因是dfs中的判断if (w == -1 || !vis[w] && dfs(w))

我当时写的是if (w == -1 || && dfs(w)) 这样会无限递归下去,直到爆栈。

if (w == -1 || dfs(w) && !vis[w]) 目测这么写也是爆栈,短路运算符的优势在此处就体现出来了,而且是两次。如果w已经访问过就返回false而不执行dfs,同样如果w没有匹配点,也不会调用vis[w],而是直接返回True。从而避免负数下标。

4、为什么vis要设成bitset?经过测试,int a[1000000]这个数组做10000次memset需要4s,而bitset<1000000> 做1000000次reset需要4s。也就是一旦碰到点数多的稀疏图,用int存是否访问基本是送死。(100W次memset)


5、《挑战程序设计竞赛》上的范例有一处我认为没有必要。bmatch()的第三行是这样的

rep1(i, n) if (match[i] == -1)

我认为match[i]铁定是-1,不用判断,测试下来删了这句也能A题。

虽然不用执着于经典算法的优化,但想清楚一些细节能很好地加深对算法的理解。


最大匹配和最小覆盖、最大独立集、最小顶点覆盖之间的关系


这部分很搞,用表格来做比较清晰。

匹配

边集

集合中的边的顶点不重复

边覆盖

边集

集合中的边的顶点集等于图的顶点集

独立集

点集

集合中任意两个点不存在边

顶点覆盖

点集

图的任意边都至少有一个顶点属于集合

一些结论:


对连通一般图最大匹配 + 最小边覆盖 = V

假设最大匹配为X,最小点覆盖在最大匹配基础上再加边,每一次加边,至多能多覆盖一个点(否则最大匹配还能更大),可以想象有这样的方法每次加边都能多覆盖一个点。加的数量就是V - X

 

最大独立集 + 最小顶点覆盖 = V

只需证“对任意点集X,若X是G的一个独立集,则X在V中的补集为G的一个顶点覆盖”

这X在V中的补集为Y,考虑每条边,这条边的两个端点至多有一个点属于X,这等价于两个端点至少有一个属于Y,等价于Y是顶点覆盖。

 

对于二分图,最大匹配 = 最小顶点覆盖

首先计算最大匹配。对于左边所有没有匹配点的点,把和它所有相连的点全部打上标记,这样左右两边都可能有一些点被打上了标记,最小顶点覆盖就是左边没打上标记的点并上右边打了标记的点。


汇总

名称

数量

怎么输出结果

最大匹配

X

匹配的match数组

最小边覆盖

V - X

??

最大独立集

V - X

不是最小顶点覆盖中的点

最小顶点覆盖

X

左边匹配的未匹配点进行搜索,访问到的便是

 

注:三个结论当中只有第二个算是较为严谨的证明。命题和证明都非本人原创。


一些例子:

USACO 2005 NOV 小行星群  和 UVa 11419 SAM I AM

这两题一模一样,先是以行列为点建图,然后把最小覆盖转成最大匹配。UVa还多了一步,要求输出这个最小覆盖。


LA 3415 保守的老师 和 LA 3126 出租车 这两题都比较经典,书上的解释也足够了。


BZOJ 1059 矩阵游戏


以行列建二分图,这样交换两行就是交换二分图左边的两个点,这样可以看出有完美匹配就能通过交换找到解(解就是一张n条横杠的二分图),否则就不行。


BZOJ 1854 和 BZOJ 1191


这两题都不是最大匹配,而是“从1开始连续匹配”,只要调整匈牙利算法,使得“如果从第i个点找不到增广路就退出记答案”就行了。当然如果能完美匹配,要特判下答案等于n。

1854这题二分图左边有1W个点,右边有100W个点,就要用到之前的技巧了,用数组存是否访问是TLE的


KM算法

概念也不多说了。仍然讨论一些问题。

首先是为什么相等子图有完美匹配等价于这个匹配是最大权匹配。有不少人是这么说的:“相等子图有完美匹配意味着这个匹配的权和等于顶标之和,而图的任意匹配权和都小于等于顶标之和。"我认为这么说有问题,需要一个附加条件:"顶标之和不会变大“。

然后是如何修改顶标的。现在,我们有一个顶标数组L和R、一个匹配节点数组match、一个记录节点是否访问的数组visl和visr,从当前节点寻找增广路已经失败,但留下了寻找痕迹。

考虑dfs的最后一个节点,这个节点一定存在一个相邻节点没有匹配点却无法访问(否则可以继续dfs).所以要降这个点的顶标。降多少呢?在满足顶标的定义下尽量大。

这样就能解释更新顶标的过程。要降低左边已访问的点的顶标,但只降左边肯定不行,因为根本降不下去,有匹配边顶着,所以还要提高右边的已访问点的顶标,降低和提高的值应该一样,这样原来匹配的边就不会丢失,寻找增广路就可能成功。降多少?按照定义尽量多降就行了,但右边已访问边的顶标会修改,所以不用考虑右边已访问的点。

至于每次更新顶标顶标和为什么会变小,因为每次访问点左边都比右边多,这是因为dfs的终点在左边。


#define rep(i,n) for(int i=0;i<(n);i++)
#define rep1(i,n) for(int i=1;i<=(n);i++)
#define FOR(i,a,b) for (int i=(a);i<=(b);i++)
#define clr(x) memset(x,0,sizeof(x))
#define fill(x, n) memset(x, n, sizeof(x))
#define PN printf("\n")
#define ll long long
#define inf 1<<30
using namespace std;
/*
KM算法 四次方级
注意邻接矩阵和一般图的邻接矩阵不同,没有对称性
注释部分都是写错的部分
样例
5
9 10 12 8 7
10 8 5 15 12
11 4 7 11 17
7 15 11 11 14
6 12 16 14 12
*/ 
const int maxn = 100;
int n, g[maxn][maxn], l[maxn], r[maxn], visl[maxn], visr[maxn], match[maxn];
bool dfs(int u){
	visl[u] = 1;
	rep(v, n)if (g[u][v] == l[u] + r[v]  && !visr[v]){//if (g[u][v] == l[u] + r[v]){
		visr[v] = 1;
		if (match[v] == -1 || dfs(match[v])){
			match[v] = u;
			//match[u] = v;
			return true;
		}
	}
	return false;
}
void update(){
	int a = inf;
	rep(i, n) if (visl[i]) rep(j, n) if (!visr[j]) a = min(a, l[i] + r[j] - g[i][j]);//rep(i, n) if (visl[i]) rep(j, n) if (visr[j]) a = min(a, g[i][j] - l[i] - r[j]);
	rep(i, n) if (visl[i])  l[i] -= a;
	rep(i, n) if (visr[i])  r[i] += a;
}
void KM(){
	fill(match, -1); 
	rep(i, n) {
		l[i] = 0; r[i] = 0;
		rep(j, n)  l[i] = max(l[i], g[i][j]);
	}
	rep(i, n){
		while(1){
			clr(visl); clr(visr);
			if (dfs(i)) break; else update();
		}
	}
}
int main()
{
    scanf("%d", &n);
    rep(i, n) rep(j, n) scanf("%d", &g[i][j]);
    KM();
    int ans = 0;
    rep(i, n){printf("%d %d\n", l[i], r[i]); ans += l[i] + r[i];}
    printf("%d\n", ans);
    return 0;
}


注意事项全在代码里了。虽然是4次方算法,n在500规模的时候还是可以0.2s出解,不过估计可以卡。




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值