suxxsfe
2020-05-23 23:21:22
在我的博客园查看
二分图(可以带权)中的最大匹配问题,一般图要用带花树 (并不会
一些定义和性质可能在算法讲解中用不到,但是下面的题目中会用到
匈牙利算法也要基于这点
首先有奇环一定不是二分图,奇环上的点满足不了二分图性质
再说明充分性,下面讨论的是每个联通块的情况,也就是为了应对图不连通
任意取一个顶点
对于任意一条边
这个结论在图论里也是比较常见的,下面一个题也用到了
设点数为
首先,由于
其次显然有
这样又多出
综合上述得出
这个显然吧,任意两个点没有变放在补图里就是任意两个点都有边
这个也是匈牙利算法的基础,说的是匹配数最大等价于不存在增广路
不存在增广路的必要性很好证明,如果有增广路,因为增广路结构是非匹配边,匹配边......非匹配边,那么把所有匹配边和非匹配边交换就行了
这样匹配数加一,则原来的匹配不是最大匹配
充分性稍难,证明思路是看的维基
要先定义两个集合的“对称差”,如果某元素在且只在其中的一个集合中(不能同时在两个中),那么这个元素在这两个集合的对称差集合中,类似于异或
我们设当前的匹配是
这个
又因为
这两种情况中,在
原因是,对于路径,肯定是一个边在
如果不是,那么如果一个路径开头结尾都是一个集合内的点,那么必然会对另一个集合产生增广路
举个简单的例子,虽然是个很特殊的情况但也足够说明这个寻找的过程
依然是红色代表匹配边
如果搜索了一圈,发现怎么也找不到增广路,那么此时答案就不能增加,否则答案加一
我看模板题的题解里把这个描述成“协商与匹配”,其实就是通过增广路的原理重新调整匹配与非匹配边,当然本质上一样
放上代码
int n,m,e;
int match[N],check[N];//match 记录对应的匹配点,check 记录有没有被访问
inline int dfs(int u){
for(reg int v,i=G.fir[u];i;i=G.nex[i]){
v=G.to[i];
if(check[v]) continue;
check[v]=1;
if(!match[v]||dfs(match[v])) return match[v]=u,match[u]=v,1;//维护新的匹配信息
}
return 0;
}
int main(){
n=read();m=read();e=read();
for(reg int a,b,i=1;i<=e;i++){
a=read();b=read();
G.add(a,b+n);
}
int cnt=0;
for(reg int i=1;i<=n;i++){
std::memset(check,0,sizeof check);cnt+=dfs(i);
}
std::printf("%d",cnt);
return 0;
}
还有一种bfs版本,原理相同,但在稀疏图中跑的似乎比dfs快得多(网上看的,具体实验没自己做)
但码量更大,而且有的题目只能使用dfs或bfs其中一种,下面例题里有例子
bfs需要开一个 pre
数组来记录路径
和上面一样,对每一个左边的点 pre
数组,沿着走过的路径一路走回去,标记匹配,怎么借助在下面
如果匹配了,就把原匹配点加入队列,并记录 pre[match[v]]=u
,这里 match
数组记录的就是原匹配
什么意思呢?具体来说 pre[i]
是路径上点
是在上一个新的(增广路修正后的)匹配边中,同样位置的点,看下图,红边是当前记录的匹配边
注意图中说的 e=match[d]
指的是更新信息前的 match[d]
那么经过对增广路的匹配非匹配边的调换,得到下面的样子:
那么我们成功从
代码
int num_left,num_right,m;
int pre[N],match[N];
//match[i],表示一条包含点 i 的匹配边的另一个端点
//pre[i],维护几个匹配边和未匹配边组成的路径信息,具体来说 pre[i] 是路径上往回的第二个节点
//比如增广路1->2->3->4->5,其中,1->2,3->4 是匹配边,那么 pre[4]=2
//也就是,我们可以通过 d=pre[d],来让 d 变成上一个匹配路径中,和 d 在同一个位置中的点
//这就是更新路径信息时的做法
int que[N],tail,head,check[N];
inline int max_match(){
int ans=0,u,v;
for(reg int i=1;i<=num_left;i++){
tail=head=0;que[0]=i;
pre[i]=0;
while(tail<=head){
u=que[tail++];
for(reg int j=G.fir[u];j;j=G.nex[j]){
v=G.to[j];
if(check[v]==i) continue;//本次 bfs 访问过
check[v]=i;
if(match[v]) pre[match[v]]=u,que[++head]=match[v];
//是匹配点,那么把已经和他匹配的另一个点入队
else{//找到未匹配点,是个增广路,要更新路径
int d=u,e=v,t;
while(d){
//d 是增广路上匹配边的终点,e 是增广路上下一个匹配边的起点
//然后让 d,e 互相匹配
//然后通过 e=match[d],d=pre[d] 来推到前一条边,此时 d,e 仍满足前一行说的性质
t=match[d];
match[d]=e;match[e]=d;
d=pre[d];e=t;
}
goto BREAK;
}
}
}
BREAK:;
if(match[i]) ans++;
}
return ans;
}
int main(){
num_left=read();num_right=read();m=read();
for(reg int u,v,i=1;i<=m;i++){
u=read();v=read()+num_left;G.add(u,v);
}
std::printf("%d",max_match());
return 0;
}
容易发现,复杂度
这个是叫KM算法,用在带权值的二分图上,找一些匹配边使得它们权值之和最大
求最大匹配时,要求所有边权非负;当有负数边权是,可以求出一个最大的完美匹配
UOJ#80. 二分图最大权匹配
首先,这个算法是针对于完全图,不过也没有什么本质区别,就把不在实际中的边边权设为零就好。
并且二分图左右两边的点数相同,这个就取一个
很显然,根据上面所说的,完全图的最大权匹配一定是完美匹配(边权非负)
为每个顶点确定一个“顶标”,
定义“可行顶标”,是使得对于边
的顶标
在定义“相等子图”,是对于一个子图内(包含原图所有顶点,但不一定包含所有边),任意边
的子图
由这个相等子图性质,我们有以下结论,对于一个包含完美匹配的相等子图,则这个子图的最大权匹配的权值和,就是所有顶标加起来
同时,也是这个子图的最大权匹配
进一步讲,也是原图的最大权匹配和,因为如果去除掉子图完美匹配的某些边,加入另一些边,使得它还是一个完美匹配
那么这些加入的边的权值肯定是小于等于两端点顶标和,所以总体地看,他就小于等于所有顶标和,也就不是最大权匹配了
所以总结出来就是:相等子图存在完美匹配,则该匹配是最大权匹配
那么,我们不断调整顶标,使得相等子图存在完美匹配不就行了
于是算法的大体结构出来了
然后实际中,第三步跑好多遍搜索是可以优化掉的,而且事实上不把他优化掉的话交到UOJ会T掉
不过还是先来看如何调整顶标,这是算法的关键
要先初始化顶标,一个可行的初始化是对于每个
用
维护一个
这个什么意思?
就是我们要找到一个最小的
这样,他就一定能访问到一个右边的还没有被访问的节点,也就能访问到这个节点的对应匹配(如果有的话)
注意这里是一定,这有关算法复杂度
那为什么要最小呢?我们要让其它边两端节点可行顶标的性质不被破坏
而要让以前在相等子图中的点,还在相等子图,所以要让所有能搜索到的右节点,顶标加上
其实严格来讲这里还不能称作相等子图,因为还没有包含所有点
然后顶标的变化需要调整
那么,哪一个
设这个节点为
直到循环被跳出
然后跳出以后,要再跑一遍搜索,来更新一下
复杂度:最外层循环循环
那么总复杂度 感觉也不会跑满,主要是因为
然后发现那个第三部的搜索自然而然的优化掉了
其实不优化掉的方法是不维护
这部分可能比较难理解,可以看代码中的注释
#define N 808
int num_left,num_right,m;
int G[N][N];
int vis[N],pre[N],match[N],lw[N],d[N];
int que[N],tail,head;
inline int bfs(int u){
tail=head=0;que[0]=u;
pre[u]=0;
while(tail<=head){
u=que[tail++];vis[u]=1;
for(reg int i=num_left+1;i<=num_right;i++)if(G[u][i]==lw[u]+lw[i]){
if(vis[i]) continue;
vis[i]=1;
if(match[i]) pre[match[i]]=u,que[++head]=match[i];
else{
int d=u,e=i,t;
while(d){
t=match[d];
match[d]=e;match[e]=d;
d=pre[d];e=t;
}
return 1;
}
}
}
return 0;
}
inline LL max_match(){
for(reg int i=1;i<=num_left;i++){//初始化 lx
for(reg int j=num_left+1;j<=num_right;j++) lw[i]=std::max(lw[i],G[i][j]);
}
for(reg int i=1;i<=num_left;i++){
std::memset(vis,0,sizeof vis);std::memset(d,0x7f,sizeof d);
if(bfs(i)) continue;//能增广了就退出找下一个
for(reg int j=1;j<=num_left;j++)if(vis[j])
for(reg int k=num_left+1;k<=num_right;k++)
if(!vis[k]) d[k]=std::min(d[k],lw[j]+lw[k]-G[j][k]);
while(1){
int now=1e9,to,s;
for(reg int j=num_left+1;j<=num_right;j++)if(!vis[j]) now=std::min(now,d[j]);
for(reg int j=1;j<=num_left;j++)if(vis[j]) lw[j]-=now;
for(reg int j=num_left+1;j<=num_right;j++)
if(vis[j]) lw[j]+=now;//为了维持以前在相等子图的点还在相等子图,左边点减 now,右边加 now
else d[j]-=now,to=d[j]?to:j;//to 记录了哪个是要被连接到(d[j]=0,加入相等子图)的右顶点
//d[j]-=now 是因为对于 vis[j]=0 的 j,它们所连到的左边的满足 vis[k]=1 的点的 lx[j] 会减 now
//那再取个 min 还是减 now
//这样更新了 d[j] 还求出了 to
if(!match[to]) break;
s=match[to];vis[to]=vis[s]=1;//打上 vis 标记
for(reg int j=num_left+1;j<=num_right;j++)
//更新 d,这里再次更新,是因为 match[to] 被打上了 vis 标记
//属于了左边能到达的点,所以要对于每个 j 和 match[to] 来更新 d[j]
if(!vis[j]) d[j]=std::min(d[j],lw[s]+lw[j]-G[s][j]);
}
std::memset(vis,0,sizeof vis);
bfs(i);
}
LL ans=0;
for(reg int i=1;i<=num_right;i++) ans+=lw[i];
//答案直接把每个 lx,ly 加起来就行了,因为最后是个完美匹配
return ans;
}
int main(){
num_left=read();num_right=read();m=read();
int nnn=num_left;
num_left=num_right=std::max(num_left,num_right);
num_right+=num_left;
for(reg int u,v,i=1;i<=m;i++){
u=read();v=read()+num_left;G[u][v]=G[v][u]=read();
}
std::printf("%lld\n",max_match());
for(reg int i=1;i<=nnn;i++)
std::printf("%d ",(match[i]&&G[i][match[i]])?(match[i]-num_left):0);//边权要大于一,是实际中的边
return 0;
}
题目还是匈牙利的居多,二分图权匹配的题本来好像就不多
here,同bzoj1433
一些学生,一部分在校(有宿舍的床),一部分是外校的(没床)
在校的学生有一些要回家(不占用床),外校的学生都要来来学校(占用床),当然,不回家的在校学生也占床
给出一些朋友关系(双向),每个人只能睡自己或朋友的床,问能不能安排合适的方案使得每个人都有床
比较简单,为每个在校学生新建一个点,表示它们的床的编号,这样看谁能睡谁的床,向他的床连边就行
人和人,床和床不会连边,所以是二分图,跑一下匈牙利看一看匹配数是不是和需要床的人数相等即可
注意不回家在校生可以睡自己的床
code
here
求无向图最大独立集,用刚才证明的性质,二分图的最大独立集大小是顶点个数减去最大匹配数
是二分图是因为曾经跳过舞的一定是男生与女生
太裸,代码就不放了
here,同bzoj1059
首先有没有解很好判,建个点数
这是个二分图,且左右点数相同,然后看一下有没有完美匹配就行了
这个 match
关键是如何让字典序最小
回顾之前说的匈牙利算法的过程,先成为匹配边的边,在后面的节点寻找增广路时,如果它们能组成增广路,则会在修正这个增广路以产生更多匹配数而被“抹掉”,也就是成为非匹配边
字典序是先比较前面字符的大小,那么肯定是最小化前面的点的 match
那么,我们在从每个点为起点找增广路时,从
然后为了让每个点在不被其它点影响的情况下,得到最小的对应匹配点,就也要让编号小的点先被访问
邻接表的访问是倒序的,所以就先加入序号大的边就行了
这里就是刚才提到的不能用bfs的情况
因为为了字典序最小,找到一个可能成立的较小编号的点后,就要从这开始一直访问下去,如果不行在考虑其它点
而bfs时,从
但如果又在访问其它
因此一定要根据实际情况看用bfs还是dfs
但似乎目前还并没有遇到dfs不行的题
code
here,同bzoj2437
题面挺复杂,不简述了
这题做的我简直想打死兔兔和蛋蛋,一晚上+一上午+一中午差点死在这题上/kk
首先可以看作是这个空格子在移动,而且不会经过重复的点,所以只要经过了一个点不用,也不能再考虑这个格子了
如何说明不重复?假设空格子从
那么要是想让空格子再回到
也就是一共走了奇数步,回到原来位置
把移动按横纵方向拆开,横纵方向都是走到一个格,再走回来,应该是偶数
基于这一点再做分析,考虑什么时候先手必胜
我们构造一个二分图,把黑点和空格的起始点放在左边,白点放在右边
对于另个相邻的格子,如果颜色不同,连边,表示能从这两个格子之间走过
这是个二分图,因为边在黑和白之间
如果对于任意一种最大匹配方式,先手操作前所在的格子都是匹配点,那么先手必胜
先手可以先走一个匹配边,那么后手走的就是非匹配边,以此类推,当一定会存在当先手走完一个匹配边后,后手无路可走的情况
因为一个格子在任意一种最大匹配中都是匹配点,那么以它开始的一个路径,肯定会是一种“修正过的增广路”
就是第一条,最后一条边都是匹配边
如果不是,那么以匹配边开始,非匹配边结束,就可以让所有边在匹配/非匹配中互换,则匹配数不变,起点却不是匹配点了
这种点,叫做最大匹配的关键点
如何判断?先跑出任意一种匹配方式,如果这个点都没匹配上肯定直接不是关键点
否则强制让这个点和他的原匹配不被匹配在一起,就是把匹配删掉
从他的原匹配开始搜索,看能不能再通过别的方式增广,能的话就说明不是,否则就是关键点
因为之前提到过,已经经过的点不能再次经过,所以还要用一个 deleted
数组记录是否被走过,之前一直卡在这
code
这里开始是最大权匹配的题了
here
一共有
每种可以贷到
一开始没钱,问在某时刻,手中最多可以有多少钱
显然,整个贷款的过程不会超过
最终结束也是在一个月的月初,且这个月初刚开始一个贷款
如果第
拿到了
所以,可以建图,让
连边表示在倒数第
然后跑一个最大权匹配就行了
code
here,似乎是洛谷题库里唯一能搜到的二分图权匹配题
一张有向图,用一些环和孤立的点覆盖所有点,环的代价是所有边的权值和,点的代价是点权,问最小代价
一开始没看见是有向的直接全WA/kk
先Floyd一下求出任意两点之间的最短路
然后发现,对于点
然后每个
则一定存在完美匹配,而且每一个完美匹配都对应一个可行的覆盖方案
然后在模板代码上改一改改成求最小权匹配就行了
code,发现用一个大数减去实际边权跑最大匹配再转换回来是不可行的
P3033 [USACO11NOV]Cow Steeplechase G
P4304 [TJOI2013]攻击装置
P1640 [SCOI2010]连续攻击游戏
P2825 [HEOI2016/TJOI2016]游戏
匈牙利算法
最大匹配
作者太菜,一般图的最大匹配和最大权匹配并没有讲,发现光二分图的匹配算法和题就学了半天才弄懂
难免会有错误,所以希望能在评论区或私信指出,感谢,轻喷