知识点:二分图最大权匹配
知识点:二分图最大权匹配
知识点概要:
二分图最大权匹配是指在二分图中,每一条边都会有一个权值,询问的是匹配之后的最大权值而不是最大的匹配数,主要的做法有KM和最费用流的做法。
知识点详解:
二分图最大权匹配的主要做法是KM和费用流。而费用流的做法是比较简单的,新建一个源点和一个汇点,然后从源点向每一个左集合中的点连一条流量1,费用为0的边,从右集合中的点向汇点连一条流量为1,费用为0的边,最后再将原图中的边连起来,设置成流量为1,费用为权值的边,这样跑一边最小费用最大流之后,得到的就是二分图的最大权匹配。
接下来开始讲KM的做法。KM的做法的核心点在于对于二分图左右两个集合中的每一个点分别记一个标杆数组\(X[i]\),\(Y[i]\),并且在算法执行的仍以时刻,我们都要保持\(X[i]+Y[j]>=w(i,j)\)这个性质。在理解KM算法之前,我们需要具体的理解这个标杆数组在KM算法中的作用。首先我们需要知道KM算法的正确性基于这个定理:
若二分图中由所有\(X[i]+Y[j]=w(i,j)\)的边\((i,j)\)组成的导出子图具有完备匹配,那么这个完备匹配就是原来二分图中的最大权匹配。
这为什么是正确的呢?我们之前在定义标杆数组的时候,就保证了\(X[i]+Y[j]>=w(i,j)\)这个性质在仍以时刻都会成立。而我们导出的子图由于由完备匹配,所以这个完备匹配的权值一定就是所有标杆数组的和,而又根据\(X[i]+Y[j]>=w(i,j)\),所以我们在原图中任意其他的匹配方案,都无法比这个匹配方案更加优秀。所以我们的导出子图的完备匹配的权值就是原图的最大权匹配。
于是我们接下来就需要求这个完备匹配了。我们先初始化标杆数组\(X\)为与这个点相连的边的最大权值,初始\(Y\)为0,这样我们就先保证了在初始情况下满足我们的标杆数组的性质。但是现在的问题在于,对于现在的标杆数组\(X\)和\(Y\),我们的导出子图并不一定是存在完备匹配的,于是我们就需要调整这个标杆数组使得这个导出子图能够包括原图中更多的边,来让它具有完备匹配。并且我们需要这个保证这个调整的量要足够小,让它不会增加一些不必要的边导致答案变劣。于是我们可以这样进行调整:我们通过调整之前的最后一次\(Dfs\)交错路,找到所有\(i\)被访问过的,但是\(j\)并没有被访问到的边\((i,j)\),记\(d\)为\(X[i]+Y[j]-w(i,j)\)的最小值,然后把每一个左集合被访问的点的标杆\(X[i]\)减去\(d\),每一个右集合被访问的点的标杆\(Y[i]\)加上\(d\)。然后继续求完备匹配。为什么这样的方法是正确的呢,我们这样分析:
1.对于已经在导出子图中的边\((i,j)\),由于\(X[i]\)减少了\(d\),\(Y[i]\)增加了\(d\),所以这个标杆和还是不变的,仍然在这个导出子图中。
2.对于不在导出子图的边\((i,j)\),如果\(i\)被访问过了,\(j\)没有被访问过,那么这个标杆和\(X[i]+Y[j]\)会变小,那么这个标杆和就有可能等于\(w(i,j)\),所以这条边就会有可能加入这个导出子图。
3.对于不在导出子图的边\((i,j)\),如果\(i\)没有被访问过,\(j\)被访问过了,那么这个标杆和\(X[i]+Y[j]\)就会变大,那么这条边仍然不能加入导出子图。
这样我们就可以证明这种修改的方式是正确的了。所以我们现在就可以得出二分图最大权匹配的步骤了:
1.初始化标杆数组
2.枚举每一个点进行二分图的匹配
3.如果这个点没有匹配到,那么修改标杆数组继续进行匹配
4.如果这个点匹配到了,那么继续匹配下一个点
最后把所有的匹配边的权值加起来就是最大权匹配的答案了。
但是我们分析一下,枚举每一个点,需要\(O(n)\)的复杂度,求出标杆数组的修改量,需要\(O(n^2)\)的复杂度,每个点最多修改\(O(n)\)次,所以总复杂度为\(O(n^4)\)。然后我们观察我们这些步骤,发现我们复杂度的瓶颈实际是在求标杆数组的修改量上的。因为枚举点是不可避免的,然后每个点的修改也是期望的,所以我们只能对求修改量的方法进行优化。朴素的方法是枚举每一个访问的\(i\)和未访问的\(j\),然后我们可以对此进行优化,记\(slack\)数组为松弛变量,每次访问到一个\(i\)的时候,就把与这个点相连的点\(j\),并且当前无法加入导出子图的点,用\(X[i]+Y[j]-w(i,j)\)来更新\(slack[j]\)。然后我们只需要枚举每一个右集合中未访问到的点\(v\),然后用\(slack[v]\)更新修改量\(d\),这样就可以做到求修改量的复杂度为\(O(n)\)的了,然后总复杂度就可以优化成\(O(n^3)\)的了。不过还要注意的是,修改右集合中的标杆\(Y[i]\)的时候,也要同时修改\(slack[i]\)。
模板
HDU2255 奔小康赚大钱
题目传送门
Code
#pragma GCC optimize (3,"inline","Ofast")
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
bool Finish_read;
template<class T>inline void read(T &x){Finish_read=0;x=0;int f=1;char ch=getchar();while(!isdigit(ch)){if(ch=='-')f=-1;if(ch==EOF)return;ch=getchar();}while(isdigit(ch))x=x*10+ch-'0',ch=getchar();x*=f;Finish_read=1;}
template<class T>inline void print(T x){if(x/10!=0)print(x/10);putchar(x%10+'0');}
template<class T>inline void writeln(T x){if(x<0)putchar('-');x=abs(x);print(x);putchar('\n');}
template<class T>inline void write(T x){if(x<0)putchar('-');x=abs(x);print(x);}
/*================Header Template==============*/
const int N=505;
const int inf=2e9+7;
int n;
int wx[N],wy[N],weight[N][N],slack[N];
int belong[N],visx[N],visy[N];
/*==================Define Area================*/
int FindPath(int u) {
visx[u]=1;
for(int v=1;v<=n;v++) {
if(visy[v]) continue;
int t=wx[u]+wy[v]-weight[u][v];
if(!t) {
visy[v]=1;
if(belong[v]==-1||FindPath(belong[v])) {
belong[v]=u;
return 1;
}
}
else if(slack[v]>t) slack[v]=t;
}
return 0;
}
int Km() {
for(int i=1;i<=n;i++) wx[i]=-inf;
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
wx[i]=max(wx[i],weight[i][j]);
}
}
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
slack[j]=inf;
}
while(1) {
memset(visx,0,sizeof visx);
memset(visy,0,sizeof visy);
if(FindPath(i)) break;
int ret=inf;
for(int j=1;j<=n;j++) {
if(!visy[j]&&ret>slack[j]) ret=slack[j];
}
for(int j=1;j<=n;j++) {
if(visx[j]) wx[j]-=ret;
}
for(int j=1;j<=n;j++) {
if(visy[j]) wy[j]+=ret;
else slack[j]-=ret;
}
}
}
int ans=0;
for(int i=1;i<=n;i++) {
if(~belong[i]) ans+=weight[belong[i]][i];
}
return ans;
}
int main() {
while(scanf("%d",&n)!=EOF) {
memset(belong,-1,sizeof belong);
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
read(weight[i][j]);
}
}
printf("%d\n",Km());
}
return 0;
}