目录:
本文中,使用mtc表示match,即对应的匹配点。 check表示本轮是否已经被操作。
参考文献:http://www.renfei.org/blog/bipartite-matching.html
具体来说,就是一个找增广路的过程。
对于一个二分图,我们想办法为其左侧的每一个点选择一个右侧的点进行匹配。
那么成功找到一个匹配的方式有两种:
1.当前搜到节点还未被匹配,直接匹配上即可。
2.当前搜到的节点已被匹配,尝试让 当前点的匹配点 去匹配其他点,从而把当前点让出来用于匹配。
特别注意:对于每一条边,要连接两次,连双向边!!(左右点的标号不能重复!)
求解的算法为匈牙利算法(Hungarian算法)。他的实现方式有两种,DFS与BFS。
.
DFS算法简单好打,思路清晰,缺点为时间效率较慢。
bool Hungarian(RG int u,RG int vis){
for(RG int i = head[u];i;i = t[i].next){
int v = t[i].to;
if(check[v] != vis){
check[v] = vis; //标记为这一轮已经走过
if(!mtc[v] || Hungarian(mtc[v],vis)){ //未匹配 或者 可以让步
mtc[v] = u; mtc[u] = v; //匹配
return true;
}
}
}return false;
}
int main()
{
freopen("testdate.in","r",stdin);
N = gi(); M = gi(); U = gi(); RG int Ans = 0;
for(RG int i = 1; i <= U; i ++){
RG int d = gi(),e = gi()+N; if(d>N || e>M+N)continue;
t[++cnt] = (Road){e,head[d]}; head[d] = cnt;
t[++cnt] = (Road){d,head[e]}; head[e] = cnt;
}
for(RG int i = 1; i <= N; i ++)if(!mtc[i])if(Hungarian(i,i))Ans++;
printf("%d",Ans); return 0;
}
.
BFS算法效率超级高,但是难打,容易打错。
IL int Hungarian(){
RG int Ans = 0;
memset(check,-1,sizeof(check)); memset(mtc,-1,sizeof(mtc));
for(RG int st = 1; st <= N; st ++){
if(mtc[st] != -1)continue; //找到左侧的一个未匹配点。
while(!Q.empty())Q.pop();
pre[st] = -1; Q.push(st); // 设 st 为路径起点
flag = false; // 尚未找到增广路
while(!flag && !Q.empty()){
RG int u = Q.front(); Q.pop(); //cout<
for(RG int i = head[u]; i && !flag;i = t[i].next){
RG int v = t[i].to;
if(check[v] == st)continue; //确保本轮没搞过
check[v] = st;
Q.push(mtc[v]);
if(mtc[v] >= 0){pre[mtc[v]] = u;} // 此点为匹配点(已经匹配);看看能否让步
else{ // 找到未匹配点,交替路变为增广路,开始修改
flag = true; RG int d = u , e = v; //当前的d、e
while(d != -1){
RG int tmp = mtc[d]; //d 的原来匹配点可以与 其前驱匹配
mtc[d] = e; mtc[e] = d;
d = pre[d]; e = tmp; //d为 d的前驱 , e为 d的原来匹配
}
}
}
}
if(mtc[st] != -1)Ans ++; //st成功新增了匹配。
}return Ans;
}
int main()
{
freopen("testdate.in","r",stdin);
N = gi(); M = gi(); U = gi();
for(RG int i = 1; i <= U; i ++){
RG int d = gi(),e = gi()+N; if(d>N || e>M+N)continue;
t[++cnt] = (Road){e,head[d]}; head[d] = cnt;
t[++cnt] = (Road){d,head[e]}; head[e] = cnt;
}
printf("%d",Hungarian()); return 0;
}
.
两个版本的时间复杂度均为O(V⋅E)
。DFS 的优点是思路清晰、代码量少,但是性能不如 BFS。我测试了两种算法的性能。对于稀疏图,BFS 版本明显快于 DFS 版本;而对于稠密图两者则不相上下。在完全随机数据 9000 个顶点 4,0000 条边时前者领先后者大约 97.6%,9000 个顶点 100,0000 条边时前者领先后者 8.6%, 而达到 500,0000 条边时 BFS 仅领先 0.85%。
如果需要匹配呈字典序,那么我们应该确保连边为从小到大。
然后逆序,从后往前匹配。 应为这样的话,可以确保前面的点优先级最大。
详细可以见例题:
Lougu P1963 变换序列 https://www.luogu.org/problemnew/show/P1963
最大匹配数:最大匹配的匹配边的数目
最小点覆盖数:选取最少的点,使任意一条边至少有一个端点被选择
定理1:最小点覆盖数 = 最大匹配数(这是 Konig 定理)
最小路径覆盖数:对于一个 DAG(有向无环图),选取最少条路径,使得每个顶点属于且仅属于一条路径。路径长可以为 0(即单个点)。
二分图模型:把所有顶点 i 拆成两个:X集合中的 i 和Y集合中的 i’。若有边 i->j,则在二分图中引入边 i->j’。
定理2:最小路径覆盖数 = 顶点数 - 最大匹配数
最大独立集:选出一些顶点使得这些顶点两两不相邻,则这些点构成的集合称为独立集。找出一个包含顶点数最多的独立集称为最大独立集。
定理3:最大独立集 = 所有顶点数-最小点覆盖数
最大团:简单说,就是选出一些顶点,这些顶点两两之间都有边。最大团就是使得选出的这个顶点集合最大。
定理4二分图的最大团 = 补图的最大独立集
补图的定义是:对于二分图中左边一点x和右边一点y,若x和y之间有边,那么在补图中没有,否则有。
参考文献:http://blog.csdn.net/yulin11/article/details/4385207
定义:
可行顶标L[x] :对于任意x∈X,任意y∈Y,有L[x] + L[y] >= W[x][y]成立。
相等子图: M*={(x,y)|(x,y)∈M,L(x)+L(y)=W(x,y)} ,称 M*为边集的M之生成子图 为 相等子图
重要定理: M*的完备匹配 即为 M的最佳带权匹配。
那么KM算法就是通过不断改变L(x),L(y),使得相等子图的体积不断扩大,使得其完备匹配更大。
具体来说,流程为:
假设我们匹配{X}与{Y}。 Lx[i],Ly[i]分别为其节点的可行顶标。
1.Memset()
Lx[i] = max( W[x][i] ,i∈{Y} ); Ly[i] = 0;
2.KM()
对于每一个节点进行尝试(Hungarian算法<建议DFs>):
匹配时记录S[i],T[i],分别表示{X},{Y}中的点是否到过。
如果匹配成功,跳出即可。 否则进行Modify(),然后再次尝试。
3.Modify()
求解得 修改值chg = min{W[i][j]}; (i,j)∈{(i,j) | S[i] && !T[j]};
进行修改:if(S[i])Lx[i]-=chg; if(T[i])Ly[i]+=chg;
4.输出最大路径和,略。
.
代码:
IL void Memset(){ //初始化Lx,Ly。
for(RG int i = 1; i <= N; i ++){
Lx[i] = 0;
for(RG int j = 1; j <= N; j ++)
Lx[i] = max(Lx[i],W[i][j]);
}
for(RG int j = 1; j <= N; j ++)Ly[j] = 0; return;
}
bool Hungarian(RG int x){ //匈牙利算法 尝试匹配。
S[x] = true;
for(RG int j = 1; j <= N; j ++)
if(!T[j] && W[x][j] == Lx[x] + Ly[j]){ //没到过并且符合相等子图条件
T[j] = true;
if(!mtc[j] || Hungarian(mtc[j])){
mtc[j] = x; return true; //修改(注:mtc[i]为{Y}中的i 所匹配的 {X}中的节点编号)
}
}
return false;
}
IL void Modify(){ //修改Lx、Ly
RG long long chg = INF;
for(RG int i = 1; i <= N; i ++)if(S[i]) //{X}中到过的
for(RG int j = 1; j <= N; j ++)if(!T[j]) //{Y}中没到过的
chg = min( chg , Lx[i] + Ly[j] - W[i][j] ); //求解chg
for(RG int i = 1; i <= N; i ++){
if(S[i])Lx[i] = Lx[i] - chg; //Lx[i] -= chg;
if(T[i])Ly[i] = Ly[i] + chg; //Ly[i] += chg;
}return;
}
IL void KM(){
for(RG int i = 1; i <= N; i ++){
for(;;){ //不断尝试
for(RG int j = 1; j <= N; j ++)S[j] = T[j] = false; //清零访问数组
if(Hungarian(i))break; else Modify(); //匹配与调整
}
}return;
}
IL long long GetSum(){ //求解路径和。
RG long long Sum = 0;
for(RG int i = 1; i <= N; i ++)
if(mtc[i]!=0)Sum += W[mtc[i]][i];
return Sum;
}
int main()
{
freopen("testdate.in","r",stdin);
N = gi(); //设{X},{Y}大小都为 N
for(RG int i = 1; i <= N; i ++)
for(RG int j = 1; j <= N; j ++)
W[i][j] = gi(); //X -> Y的距离
Memset(); KM(); cout<return 0;
}
__有N男N女,每个人都按照他对异性的喜欢程度排名。现在需要写出一个算法安排这N个男的、N个女的结婚,要求两个人的婚姻应该是稳定的。
__何为稳定?
__有两对夫妻M1 F2,M2 F1。M1心目中更喜欢F1,但是他和F2结婚了,M2心目中更喜欢F2,但是命运却让他和F1结婚了,显然这样的婚姻是不稳定的,随时都可能发生M1和F1私奔或者M2和F2私奔的情况。所以在做出匹配选择的时候(也就是结婚的时候),我们需要做出稳定的选择,以防这种情况的发生。
__第一轮,每个男人都选择自己名单上排在首位的女人,并向她表白。这种时候会出现两种情况:
(1)该女士还没有被男生追求过,则该女士接受该男生的请求。
(2)若该女生已经接受过其他男生的追求,那么该女生会将该男士与她的现任男友进行比较,若更喜欢她的男友,那么拒绝这个人的追求,否则,抛弃其男友……
__第一轮结束后,有些男人已经有女朋友了,有些男人仍然是单身。
__在第二轮追女行动中,每个单身男都从所有还没拒绝过他的女孩中选出自己最中意的那一个,并向她表白,不管她现在是否是单身。
__这种时候还是会遇到上面所说的两种情况,还是同样的解决方案。直到所有人都不在是单身。
<1> 飞行员配对方案问题 https://www.luogu.org/problemnew/show/2756
<2> [SCOI2010]连续攻击游戏 https://www.luogu.org/problemnew/show/P1640
<3> [ZJOI2009]假期的宿舍 https://www.luogu.org/problemnew/show/P2055
<4> [ZJOI2007]矩阵游戏 https://www.luogu.org/problemnew/show/P1129
<5> 酒店之王 https://www.luogu.org/problemnew/show/P1402
<1> [USACO11NOV]牛的障碍 https://www.luogu.org/problemnew/show/P3033
<1> 运动员最佳匹配问题 https://www.luogu.org/problemnew/show/1559