当前位置: 首页 > news >正文

一般图最大权匹配

引入

前些天看到了一个比较有趣的题目,需要用到一般图最大权匹配。可是我只会二分图最大匹配,甚至不会 KM 和带花树的原理,于是就进行了一个资料的搜,顺便增长一下板子库。然而——

对一般图最大权匹配,网上现成高质量资料较少。

苦于资料杂乱、且无论正确性还是代码具体实现都没有一个清晰的全流程教程,在吸取众长、奋斗三天后,终于通过了 UOJ 上的模板题。

这篇博客主要目的在于,梳理整个流程(包含代码实现)中应用的原理和使用的技巧。一般图最大权匹配主要通过下面四个问题逐步转化:

  1. 二分图最大匹配
  2. 一般图最大匹配
  3. 二分图最大权匹配
  4. 一般图最大权匹配

本文主要描述,如何通过解决前三个问题的思路,解决第四个更为复杂的问题。本文的前置知识只有匈牙利算法

基本定义

匹配:对于图 G=(V,E)G=(V,E) 的一个匹配 MM 是边集 EE 的子集,其中两两不共用顶点。

最大匹配:边数最多的匹配。

完美匹配:包含原图所有点的匹配。

最大权匹配:对于图 G(V,E)G(V,E),边 e∈Ee∈E 的权重为 w(e)w(e),最大权匹配 M=(V′,E′)M=(V′,E′) 为 ∑ew(e)∑e​w(e) 最大的匹配。

最大权最大匹配:最大权最大匹配 M(V′,E′)M(V′,E′) 为 ∑ew(e)∑e​w(e) 最大的最大匹配。

匹配点、未匹配点、匹配边、非匹配边:字面含义,对应于某个匹配的状态。

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

增广路:一个末尾是未匹配点的交错路径。这样的路径可以通过翻转整条路上边的选择状态获得一个长度恰好增加 11 的匹配

下列各图展示了一个无向二分图(箭头只是表示增广路的顺序)某时刻对增广路增广的情况,分别为一个二分图、一条交错路、一条增广路和对增广路增广的结果。(怎么不支持 html 啊,不会横着放,再次传送门获得更好的阅读体验)

 | 

 | 

 | 

 | | -----------: | -----------: | -----------: | -----------: |

交错树:从未匹配点 rr 开始寻找增广路时,交错路径组成的树。其中,我们称向根方向匹配的点为 SS 点(黑点,偶点)背向根方向匹配的点为 TT 点(白点,奇点)。下图展示了二分图和以 1 为根的交错树。

二分图最大匹配

二分图匹配可以用网络流做,Dinic 在二分图上的复杂度和 Hopcroft-Karp 相同,均为 O(EV)O(EV​) ,不赘述。

匈牙利算法流传较广的是 DFS 做法,即枚举左部每个未匹配点,记录右部节点匹配的左部标号 matchqmatchq​,检查交错路是否为增广路,复杂度 O(VE)O(VE)。正确性是通过 Berge's Theorm (如果找不到某点为端点的增广路,则最大匹配可以不包括这个点)证明的,比较显然。

int dfs(int p,int t){ // p: 左半边编号 for(auto q:G[p])if(t!=vis[q]){vis[q]=t;if(match[q]==-1||dfs(mt[q],t))return match[q]=p,1;}return 0;
}
int maxmatch(){int ans=0;memset(match,-1,sizeof(match));for(int i=1;i<=n;i++)if(match[i]==-1&&dfs(i,i))ans++;return ans;
}

同样地,可以进行 BFS,即初始时所有点都未匹配,将未匹配点入队。逐个取出队首元素作为交错树的根,对交错树进行 BFS 匹配。可以看出,增广的本质是在以每个点为根的交错树上寻找增广路。而二分图左部永远是黑点,右部永远是白点,因此增广路的首末必分别在左部和右部

一般图最大匹配

对于一般图,就没有这么好的性质了。但唯一的不同是,一般图中可能有奇数长度的环,下简称奇环。在奇环上的每一个点连出去的非匹配边,都能够成为某个增广路上的边,这导致在增广的过程中难以判断如何翻转。下图是增广路经过奇环的两种形式。

我们发现,奇环具有了所有黑点需要具备的性质:任意点连出去的非匹配边都可以在从根开始的交错路上;奇环的根向根方向匹配。于是,将奇环缩成一个点,称为。奇环的,也就是整个环中唯一连了两个环内非匹配边的点,称为花托(上图中的 root)。

将花缩成一个点后,环上所有点变成黑点,并可以在交错树上向外延伸。

具体实现中,不需要真正地记录花,只需要记录每个点对应花根位置 fafa(本质是个并查集);同时为了方便上述两种情况的判断,对于所有黑点,记录交错树上从根开始连向当前点的非匹配边 prepre(对于非花点 ii,preiprei​ 就是父节点)。

每当找到黑-黑边(S-S 边)时进行缩花(shrink)操作,花托就是两点的 LCA ,可以暴力求出;将花内所有白点变黑入队,并更新这些点的 prepre 。由于实现比较容易,下面不解释地给出参考代码(后来还是写了一些注释便于理解)。

int mat[N],color[N],n; // color 1: black 2: white
int pre[N]; // walk through a unmatched edge
int fa[N]; // union-find set
vector<int>G[N];
queue<int>Q; 
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
void rev(int p){ // reverse augment path from rt to p// !!!!!!!!!!!! graph needs to be [1,n]if(p){rev(mat[pre[p]]);mat[p]=pre[p];mat[pre[p]]=p;}
}
void shrink(int u,int v,int r){// shrink odd cycle into 1 point: color all along to black ([x])// pre: -> [x] ->//             <- [y] <-while(find(u)!=r){// [pre[mat[u]] <- mat[u] -- [u] -- [v]// turn into (-> stands for pre)//         [u]  <- [v]    -- []  -> []pre[u]=v;fa[mat[u]]=fa[u]=r;if(color[mat[u]]==2)color[mat[u]]=1,Q.push(mat[u]);v=mat[u];u=pre[v];}
}
int vis[N];
int lca(int x,int y){static int t=0;++t;while(1){if(vis[x=find(x)]==t)return x;else if(x)vis[x]=t;x=pre[mat[x]];swap(x,y);}
}
int augment(int rt){int i;for(i=0;i<=n;i++)fa[i]=i,color[i]=pre[i]=0;color[rt]=1;Q=queue<int>({rt});while(!Q.empty()){p=Q.front();Q.pop();assert(color[p]==1); // blackfor(auto q:G[p]){if(color[q]==1){int r=lca(p,q); // root of flowershrink(p,q,r);shrink(q,p,r);}else if(!color[q]){color[q]=2; // whitepre[q]=p;if(!mat[q]){rev(q);return 1;}else{color[mat[q]]=1;Q.push(mat[q]);}}}}return 0;
}
int maxmatch(){int i,m;scanf("%d%d",&n,&m);int ans=0;for(i=0;i<m;i++){int x,y;scanf("%d%d",&x,&y);G[x].pb(y);G[y].pb(x);if(!mat[x]&&!mat[y])mat[x]=y,mat[y]=x,ans++;}for(i=1;i<=n;i++)if(!mat[i])ans+=augment(i);return ans;
}

二分图最大权匹配

和二分图最大匹配一样,最大权匹配也可以通过网络流那一套求解:将每条边的边权看做一个费用,然后求一下最大费用最大流即可。但为了扩展到一般图,考虑 KM 算法。

学之前,我对二分图最大权匹配的印象仅停留在 KM 算法的板子上,没有真正去理解 KM 算法。下面简单介绍一下 KM 算法(如果你是第一次学习 KM 算法,推荐一个比较有趣的引入:KM算法详解+模板,仅看找对象部分大致了解一下即可)。

最大权完美匹配

我们先考虑最大权完美匹配(最大权匹配可以通过增加零边变成完美匹配)。设对于边 ee,选/不选该边表示为 xe=0xe​=0 或 xe=1xe​=1;设 δ(v)δ(v) 表示 vv 所连的边集,则我们要求的最大权匹配即为

max⁡∑e∈Ew(e)xemax∑e∈E​w(e)xe​,满足条件 {x(δ(v))=1:∀v∈Vxe∈{0,1}:∀e∈E{​x(δ(v))=1:∀v∈Vxe​∈{0,1}:∀e∈E​

这是一个整数规划问题,通过原始对偶(Primal-Dual)思想转化为对偶问题:

min⁡∑v∈Vzvmin∑v∈V​zv​,满足条件 {ze=zu+zv−w(e)≥0:∀e∈Ezu≥0:∀v∈V{​ze​=zu​+zv​−w(e)≥0:∀e∈Ezu​≥0:∀v∈V​

同时,根据互补松弛条件, xe>0⇒ze=0xe​>0⇒ze​=0。即,对于选中的边 ee,必有 ze=0ze​=0。

这里的 zz 就是 KMKM 算法中的顶标(vertex labeling)。定义边 e(u,v)e(u,v) 为”等边“当且仅当 zu+zv=w(e)zu​+zv​=w(e),也即 ze=0ze​=0,根据上述互补松弛条件,匹配只能在等边构成的子图上建。初始时,左部(黑点)每个点的顶标均为其能够连到的最大边权,右部(白点)每个点的顶标均为 00。与匈牙利算法类似,每次增广先在“等边”构成的一张二分子图上进行尝试匹配,如果匹配不到,则松弛(将标准放低一些)后继续尝试;直至所有边都成为等边仍未找到增广,算法结束。

松弛的目的是使一些新边成为等边,加入二分子图,让左部点能尝试匹配的点更多;同时保证已有匹配边 ze=0ze​=0 。对于所有未匹配边,求出最小的 delta=min⁡eze=zu+zv−w(e)delta=mine​ze​=zu​+zv​−w(e),然后将交错树中黑点的顶标降低 deltadelta,交错树中白点的顶标增加 deltadelta。这样暴力做的复杂度是 O(n4)O(n4),下面给出参考代码。

暴力实现

const ll inf=0x3f3f3f3f3f3f3f3f;
ll w[N][N],lx[N],ly[N];
int mat[N],visx[N],visy[N],n;
int dfs(int x){visx[x]=1;int y;for(y=1;y<=n;y++){if(!visy[y]&&lx[x]+ly[y]==w[x][y]){visy[y]=1;if(!mat[y]||dfs(mat[y]))return mat[y]=x,1;}}return false;
}
ll km(){int i,x,y;memset(lx,0,sizeof(lx));memset(ly,0,sizeof(ly));memset(mat,0,sizeof(mat));for(x=1;x<=n;x++)for(y=1;y<=n;y++)lx[x]=max(lx[x],w[x][y]);for(i=1;i<=n;i++){while(1){memset(visx,0,sizeof(visx));memset(visy,0,sizeof(visy)); if(dfs(i))break;ll d=inf;for(x=1;x<=n;x++)for(y=1;y<=n;y++)if(visx[x]&&!visy[y])d=min(d,lx[x]+ly[y]-w[x][y]);if(d==inf)break;for(x=1;x<=n;x++)if(visx[x])lx[x]-=d;for(y=1;y<=n;y++)if(visy[y])ly[y]+=d;}}ll ans=0;for(i=1;i<=n;i++)ans+=w[mat[i]][i];return ans;
}

不太行,考虑优化。

优化方案

一种可行的方案是,对于所有右部点 yy,记录 slvy=argminx∈T(zx+zy−w(x,y))slvy​=argminx∈T​(zx​+zy​−w(x,y)) ,即固定了 yy 后, zeze​ 最小值对应 xx ,其中 TT 为交错树。每次扩展的时候,都是取 slvyslvy​ 计算出结果最小的 yy 进行扩展,这样就能够保留已有的交错树进而继续增广,每次增广的复杂度变成 O(n2)O(n2),总复杂度 O(n3)O(n3)。下面给出示例代码,其中为方便增广,使用 prepre 记录黑点在交错上父亲的父亲(最近黑点祖先)。

const ll inf=0x3f3f3f3f3f3f3f3f;
ll w[N][N],lx[N],ly[N];
int mx[N],my[N],visx[N],visy[N],n;
int slv[N],pre[N];
queue<int>Q;
ll calc_slv(int u,int v){return lx[u]+ly[v]-w[u][v];}
void add_to_tree(int x){ // add x to tree, update all slv[y]Q.push(x);visx[x]=1;int y;for(y=1;y<=n;y++)if(!slv[y]||calc_slv(x,y)<calc_slv(slv[y],y))slv[y]=x;
}
void link(int x,int y){int tx,ty;while(x){tx=pre[x];ty=mx[x];my[y]=x;mx[x]=y;x=tx,y=ty;}
}
int augment(int p){int x,y;while(1){while(!Q.empty()){int x=Q.front();Q.pop();for(y=1;y<=n;y++)if(!visy[y]&&!calc_slv(x,y)){visy[y]=1;if(!my[y])return link(x,y),1;pre[my[y]]=x;add_to_tree(my[y]);}} ll d=inf;for(y=1;y<=n;y++)if(!visy[y])d=min(d,calc_slv(slv[y],y));if(d==inf)break;for(x=1;x<=n;x++)if(visx[x])lx[x]-=d;for(y=1;y<=n;y++)if(visy[y])ly[y]+=d;// add new edgesfor(y=1;y<=n;y++)if(!visy[y]&&!calc_slv(slv[y],y)){visy[y]=1;if(!my[y])return link(slv[y],y),1;pre[my[y]]=slv[y]; // tree: slv -> y == my[y], == stands for matchadd_to_tree(my[y]);}}return 0;
}
ll km(){int i,x,y;memset(lx,0,sizeof(lx));memset(ly,0,sizeof(ly));memset(mx,0,sizeof(mx));memset(my,0,sizeof(my));for(x=1;x<=n;x++)for(y=1;y<=n;y++)lx[x]=max(lx[x],w[x][y]);for(i=1;i<=n;i++){memset(visx,0,sizeof(visx));memset(visy,0,sizeof(visy)); memset(slv,0,sizeof(slv)); Q=queue<int>();add_to_tree(i);augment(i);}ll ans=0;for(i=1;i<=n;i++)ans+=w[my[i]][i];return ans;
}

Bonus

还有一种使用同样思想,利用局部最优获得全局最优的实现方式。对于每一个黑点 xx,找到某个白点加入交错树,这个过程中始终是选择 zeze​ 较小的边进行扩展的,直到找到 xx 所对应的增广路。

假设当前已经处理到 xx,1…x−11…x−1 都已经有对应的增广路了。设对于未匹配白点 yy,与所有已匹配黑点 xx 中的 zeze​ 最小值为 slackyslacky​,有 slacky=min⁡i∈[1,x)zi+zy−wi,yslacky​=mini∈[1,x)​zi​+zy​−wi,y​。那么加入交错树的 yy=argminyslackyyy=argminy​slacky​;同时,根据 yyyy 的 z(i,yy)z(i,yy)​ 去更新黑白已匹配点的顶标下一轮的 slackslack,同时将 matyymatyy​ 这个黑点加入交错树(如果 yyyy 没有匹配那就说明增广路找着了,退出循环)。

这样的方法对于顶标的初始值没有要求(可以不初始化),每次得到的是 xx 的局部最优解,但由于可以进行增广,因此最后得到的也是全局最优解,非常巧妙。下面给出代码。其中 prepre 记录的是白点在交错树上父亲的父亲(最近白点祖先),visyvisy 记录白点 ii 是否在交错树中。这个实现常数略小一点。

const ll inf=0x3f3f3f3f3f3f3f3f;
ll a[N],p[N],b[N],c[N];
ll w[N][N],slack[N],lx[N],ly[N];
// for right points
//   pre: last right point in augment path
//   visy: if i is in augment path
//   left: left match
int pre[N],visy[N],left[N]; 
int n;
void augment(int p){int i,x=p,y=0,yy;memset(pre,0,sizeof(pre));memset(slack,0x3f,sizeof(slack));left[0]=p;while(1){x=left[y],visy[y]=1;ll delta=inf;for(i=1;i<=n;i++){if(visy[i])continue;ll d=lx[x]+ly[i]-w[x][i];if(slack[i]>d)slack[i]=d,pre[i]=y;if(slack[i]<delta)delta=slack[i],yy=i;}for(i=0;i<=n;i++){if(visy[i])lx[left[i]]-=delta,ly[i]+=delta;else slack[i]-=delta;}y=yy;if(left[y]==-1)break;}// reverse augment pathwhile(y)left[y]=left[pre[y]],y=pre[y]; 
}
ll km(){int i,j;memset(left,-1,sizeof(left));memset(lx,0,sizeof(lx));memset(ly,0,sizeof(ly));for(i=1;i<=n;i++){memset(visy,0,sizeof(visy));augment(i);}ll res=0;for(i=1;i<=n;i++)if(left[i]>0)res+=w[left[i]][i];return res;
}

其他匹配的形式

上述讨论的都是左右部大小相等完全二分图上的最大权完美匹配;对于 n1≠n2n1​=n2​ 的情况,可以补齐不足元素进行计算;对于不要求完美匹配的最大权匹配,可以直接应用本模板;对于非完全二分图,增广过程中对不存在的边进行特判即可。

一般图最大权匹配

仍然考虑 KM 算法在带花树上跑的过程。当进行松弛操作时,如果仍然按照上面的处理方式,白点 +delta+delta,黑点 −delta−delta,那么对于一个缩成点的花来说,其中有很多交错树上的边 (u,v)(u,v),uu 和 vv 的顶标都被加或者减掉了 deltadelta,导致这条边不满足 ze≥0ze​≥0 的条件。于是需要对于整个花 BB,设置额外的花标 zBzB​,在松弛时额外对所有黑花 +2delta+2delta,对白花 −2delta−2delta,才能保持平衡。

对偶问题

设 OO 为大小为 ≥3≥3 奇数的集合的集合(包含所有花),γ(S)γ(S) 表示 SS 集合中的边,这个过程仍然可通过线性规划转化为对偶问题:

max⁡∑e∈Ew(e)xemax∑e∈E​w(e)xe​,满足条件 {x(δ(v))=1:∀v∈Vxe∈{0,1}:∀e∈Ex(γ(B))≤⌊∣B∣2⌋:∀B∈O⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧​​x(δ(v))=1:∀v∈Vxe​∈{0,1}:∀e∈Ex(γ(B))≤⌊2∣B∣​⌋:∀B∈O​

对偶问题

min⁡∑v∈Vzv+∑B∈O⌊∣B∣2⌋zBmin∑v∈V​zv​+∑B∈O​⌊2∣B∣​⌋zB​,满足条件 {zB≥0:∀B∈Oze=zu+zv−w(e)+∑B∈O,(u,v)∈γ(B)ZB≥0:∀e∈E⎩⎪⎪⎨⎪⎪⎧​​zB​≥0:∀B∈Oze​=zu​+zv​−w(e)+B∈O,(u,v)∈γ(B)∑​ZB​≥0:∀e∈E​

与二分图最大权匹配相同,根据互补松弛条件, xe>0⇒ze=0xe​>0⇒ze​=0。即,对于选中的边 ee,必有 ze=0ze​=0;除此以外,还有 zB>0⇒x(γ(B))=⌊∣B∣2⌋zB​>0⇒x(γ(B))=⌊2∣B∣​⌋,即所有 zB>0zB​>0 的集合 BB,都被选了集合大小一半的边,也即 BB 是一朵花。同时,我们加入一个条件: zB>0⇒x(δ(B))=1zB​>0⇒x(δ(B))=1,即只有花 BB 向外连了一条边的时候, zB>0zB​>0 才是有意义的。这很容易根据 zBzB​ 的用途理解。

具体实现

因此,我们需要维护的就是:

  1. 任意边 ze≥0ze​≥0,取等时是交错树里(包括形成花的边)的边;
  2. zB≥0zB​≥0。

通过类似 KM 的办法,我们就可以容易地维护带花树了!本文完

难点就在它的实现步骤。这里我分享一下通过借鉴一派 UOJ 神仙提交代码,提纯结晶后得到的模板实现思路与细节。min25 的板子真心看不懂,有懂哥欢迎补充!

先摆一下是如何存储这张带花树的:使用 edgeedge 类型存边,大有讲究!对于有花参与的连边,edgeedge 存储的 u,vu,v 不是花本身,而是花内真实节点的边。

#define N M*2+1
struct edge{int u,v;T w;edge(){}edge(int u,int v,T w):u(u),v(v),w(w){}
};
// Graph
int n,n_x; // [1, n]: point; [n+1, n_x]: flower
edge g[N][N]; // adjacent matrix
// flower
vector<int>flower[N]; // nodes in flower i (outer flower)
int root[N]; // flower root, root<=n root=i: normal nodes
int flower_from[N][N]; // flower_from[b][x]: outermost flower in b that contains x

其中,与普通带花树不同,这里的花是额外进行存储的(存储在 n+1n+1 到 nxnx​ 内),因此需要开两倍空间。花可以嵌套,而且花需要在某些时刻进行展开(后面会详细讲述),因此需要保存额外的信息。对于一个花 BB,存储其根 rootroot,对于花内任意点有 rooti∈B=Brooti∈B​=B;存储花内所有节点 flowerBflowerB​,是以花托为起始的环;同时,花内每一点 ii 保存“花内最大父花” flower_fromB,iflower_fromB,i​ 表示最大的包含 ii 的 BB 的子花。

对于上面这朵嵌套花:{1,2,3}∈B1,{B1,4,5,6}∈B2{1,2,3}∈B1,{B1,4,5,6}∈B2,存储为:

flower[B1]={1,2,3} // 1 是花托,按照某个顺序转圈,也可能是 {1,3,2}
flower[B2]={6,B1,4,5} // 6 是花托,按照某个顺序转圈,也可能是 {6,5,4,B1}
root[1]=root[6]=root[B1]=root[B2]=B2 // i 最外层的花托
flower_from[B2][B1]=B1
flower_from[B2][1]=B1
flower_from[B2][4]=4
flower_from[B1][1]=1
root[7]=7 // 非花点 root 为自身

然后是 slackslack 和匹配相关:

// slack
T label[N]; // node label, [1, n] point label, [n+1, n_x] flower label
int col[N]; // color saved at flower root
int slv[N]; // slack node of NON LEAF NODES, slv[y]=x z(x,y) min_x
// match
int mat[N]; // match, mat[x]=y (x,y)\in E
int fa[N]; // fa in cross tree
int vis[N]; // if in pathqueue<int>Q; // bfs queue

labellabel 也就是对偶问题里的 zz。colcol 是节点颜色,为黑色或白色。slvyslvy​ 表示与 yy 邻接且不在一朵花的、 z(x,y)z(x,y) 最小的 xx,因为仅会在 x,yx,y 不在一朵花中的时候用到,计算时可以直接 zx+zy−w(x,y)zx​+zy​−w(x,y)。 slvslv 的目的是计算松弛:每次松弛时,找到的边只有两种,即未访问点 yy 和某个黑点 xx 之间的边;已访问的黑点 yy 和某个已访问且不同花黑点之间的边。因此通过 slvslv 可以方便地进行计算,总复杂度 O(n3)O(n3)。

matmat 可能是花之间的匹配,即 matx=ymatx​=y 中 x>n,y>nx>n,y>n 均是被允许的。visvis 只用来计算 LCALCA ,可以暂时忽略。fafa 表示某白点在交错树上的父节点。注意到黑点交错树上的父节点是 matimati​,利用这两个东西就可以在交错树上追溯到根了

接下来是对 slvslv 的更新和计算:

// calculate slv
inline T calc_slv(edge e){return label[e.u]+label[e.v]-e.w;}
inline void update_slv(int u,int v){if(!slv[v]||calc_slv(g[u][v])<calc_slv(g[slv[v]][v]))slv[v]=u;
}
inline void recalc_slv(int u){slv[u]=0;for(int i=1;i<=n;i++)if(g[i][u].w>0&&root[i]!=u&&col[root[i]]==1)update_slv(i,u);
}

对于 BFS 队列,在将花加入队列时需要将花中所有点都加入队列;设置最外层花时,对于花中所有点都要进行设置。

// only push nodes, not flowers
void q_push(int x){if(x<=n)Q.push(x);else for(auto p:flower[x])q_push(p);
}// set root of all nodes in x to r
void set_root(int x,int r){root[x]=r;if(x>n)for(auto p:flower[x])set_root(p,r);
}

获取某朵花 bb 中,从花托到 xx 一条交错路径:对花连出去边的两种情况分别处理,返回一个指针。

// return a (+-)^k path in flower b from root[b] to x
int get_even_path_in_flower(int b,int x){int pr=find(flower[b].begin(),flower[b].end(),x)-flower[b].begin();assert(b>n&&b<=n_x&&pr<flower[b].size()); // b is flower, x in bif(pr%2==0)return pr;reverse(flower[b].begin()+1,flower[b].end());return flower[b].size()-pr;
}

某次增广使得需要设置 uu 匹配 vv,uu 可能是花(vv 不管):对于 uu 为花的情况,从花托到真实 uu 的边都进行翻转匹配,最后花托移位,旋转花到正确位置。

// set (u->v) match, can be flower
void set_match(int u,int v){mat[u]=g[u][v].v;if(u>n){edge e=g[u][v];int xr=flower_from[u][e.u];int pr=get_even_path_in_flower(u,xr);for(int i=0;i<pr;i++)set_match(flower[u][i],flower[u][i^1]);set_match(xr,v);rotate(flower[u].begin(),flower[u].begin()+pr,flower[u].end()); // change receptacle}
}

连接两个不在同一个花里的 S 点(黑点):两个点到根的路径,加上两点之间的边,能构成一条增广路。

// link 2 S points
void side_augment(int u,int v){int nv=root[mat[u]],nu=root[fa[nv]];while(1){set_match(u,v);u=nu,v=nv;if(!nv)break;set_match(nv,nu);nv=root[mat[u]],nu=root[fa[nv]];}
}
void linkSS(int u,int v){side_augment(u,v); side_augment(v,u);
}

暴力查询两点 LCA:直接跳父链打标记。

int get_lca(int u,int v){static int t=0;++t; // to avoid clearing viswhile(u||v){if(vis[u]==t)return u;vis[u]=t;u=root[mat[u]];if(u)u=root[fa[u]];if(!u)swap(u,v);}return 0;
}

增加一朵奇花:需要申请一个 idid 设为 bb,清空所有数据,并重构。需要重构的部分有:颜色;匹配(继承花托的匹配);花内节点;花内节点的 rootflower_from;邻接矩阵;slv 值。其中,邻接矩阵取的是 zeze​ 最小的值,这可以保证正确性。

void add_blossom(int u,int v,int r){int i,b=n+1;while(b<=n_x&&root[b])b++;if(b>n_x)++n_x;// clearcol[b]=1;label[b]=0;mat[b]=mat[r];flower[b].clear();for(i=1;i<=n_x;i++)g[i][b].w=g[b][i].w=0;for(i=1;i<=n;i++)flower_from[b][i]=0;// construct flowerwhile(u!=r){flower[b].pb(u);u=root[mat[u]];q_push(u);flower[b].pb(u);u=root[fa[u]];}flower[b].pb(r);reverse(flower[b].begin(),flower[b].end());while(v!=r){flower[b].pb(v);v=root[mat[v]];q_push(v);flower[b].pb(v);v=root[fa[v]];}// set as outermost flowerset_root(b,b);// calculate slackfor(auto p:flower[b]){for(i=1;i<=n_x;i++){// set to min slaveif(!g[b][i].w||calc_slv(g[p][i])<calc_slv(g[b][i])){g[b][i]=g[p][i];g[i][b]=g[i][p];}}for(i=1;i<=n;i++)if(flower_from[p][i])flower_from[b][i]=p;}recalc_slv(b);
}

尝试增广一条等边:如果对点未染过色,则必然已经有匹配,将其匹配染色后丢入队列;如果对点是黑点,分两种情况,如果 LCA=0LCA=0 即不在同一花内,则 linkSS,否则添加花。

// found_edge
int augment_path(edge e){int u=root[e.u],v=root[e.v];if(!col[v]){assert(mat[v]);fa[v]=e.u;col[v]=2;int nu=root[mat[v]];slv[nu]=slv[v]=0;col[nu]=1;q_push(nu);}else if(col[v]==1){int r=get_lca(u,v);if(r)add_blossom(u,v,r);else return linkSS(u,v),1;}return 0;
}

一次增广:将所有未匹配点入队进行 BFS。先尝试使用当前 labellabel 下的等边构成的子图跑增广,如果增广成功直接返回。如果无法成功,则需要计算出松弛的大小,并将 zu,zBzu​,zB​ 分别进行更新。更新后,如果某点 (i,slvi)(i,slvi​) 路径被加入等边子图中,则进行尝试增广。如果没有,则进行新的一轮增广。

有几个细节需要注意:

  1. 白花 zBzB​ 会减掉一个正数,但要求保证 zB≥0zB​≥0。
  2. 如何计算松弛的大小

由于有 zB≥0zB​≥0 这样的特殊约束,当一朵白花 zB=0zB​=0 时,需要对其进行开花操作 expand_blossom,即将花展开,将花中在交错树上的部分链加入交错树中,并将其他节点设置为“没有听说过”,即 coli=0coli​=0。

上面也提到过,每次松弛需要考虑的只有下列情况:

  1. 某个黑点 uu 和某个未匹配点 vv 之间的边
  2. 某两个不在同一个花内的黑点 u,vu,v 之间的连边

加上 zB≥0,zu≥0zB​≥0,zu​≥0 两个条件,总共需要找的是四种情况,其中要用到 zB22zB​​、ze22ze​​ 的形式,因此边权整体乘二;但 zBzB​ 要小于零了可以拆花,zuzu​ 要小于零可就真的找不到匹配路径了,因此如果某次结束后 zu=0zu​=0,直接返回增广失败。

int augment(){int i;memset(col,0,sizeof(int)*(n_x+1));memset(slv,0,sizeof(int)*(n_x+1));memset(fa,0,sizeof(int)*(n_x+1));Q=queue<int>();for(i=1;i<=n_x;i++)if(root[i]==i&&!mat[i]){// add all unmatched pointscol[i]=1;q_push(i);}if(Q.empty())return 0;while(1){while(!Q.empty()){int p=Q.front();Q.pop();assert(col[root[p]]==1);for(i=1;i<=n;i++){if(g[p][i].w==0||root[i]==root[p])continue;// not in same flowerT d=calc_slv(g[p][i]);if(!d){if(augment_path(g[p][i]))return 1;}else if(col[root[i]]!=2)update_slv(p,root[i]);}}T delta=INF;// calc deltafor(i=1;i<=n;i++)if(col[root[i]]==1)delta=min(delta,label[i]);for(i=n+1;i<=n_x;i++)if(root[i]==i&&col[i]==2)delta=min(delta,label[i]/2);for(i=1;i<=n_x;i++){if(root[i]!=i||!slv[i])continue;if(!col[i])delta=min(delta,calc_slv(g[slv[i]][i]));else if(col[i]==1)delta=min(delta,calc_slv(g[slv[i]][i])/2);}// update labelfor(i=1;i<=n;i++){if(col[root[i]]==1)label[i]-=delta;else if(col[root[i]]==2)label[i]+=delta;}for(i=n+1;i<=n_x;i++){if(root[i]!=i)continue;if(col[i]==1)label[i]+=2*delta;else if(col[i]==2)label[i]-=2*delta;}for(i=1;i<=n;i++)if(label[i]<=0)return 0;for(i=1;i<=n_x;i++){if(root[i]!=i||!slv[i]||root[slv[i]]==i)continue;if(calc_slv(g[slv[i]][i])==0&&augment_path(g[slv[i]][i]))return 1;}// expandfor(i=n+1;i<=n_x;i++)if(root[i]==i&&col[i]==2&&label[i]==0)expand_blossom(i);}return 0;
}

开花的具体实现:

// only expand outermost blossom b, b is T(white) blossom
void expand_blossom(int b){int i,x;for(auto p:flower[b])set_root(p,p);x=flower_from[b][g[b][fa[b]].u];// [0,pr]: (+-)^k, insert into tree, add black to queueint pr=get_even_path_in_flower(b,x);col[x]=2;fa[x]=fa[b];for(i=0;i<pr;i+=2){// from bottom to upper layer in treeint white=flower[b][i];int black=flower[b][i+1];col[black]=1;col[white]=2;fa[white]=g[black][white].u;slv[black]=slv[white]=0;q_push(black);}// others: color=0for(i=pr+1;i<flower[b].size();i++){col[flower[b][i]]=0;recalc_slv(flower[b][i]);}// delete broot[b]=0;flower[b].clear();
}

参考代码

加上主函数,下面放一下完整的模板实现(题目是 UOJ #81):

#define M 403
using T=ll;
const T INF=0x3f3f3f3f3f3f3f3f;
namespace blossom_tree{
#define N M*2+1struct edge{int u,v;T w;edge(){}edge(int u,int v,T w):u(u),v(v),w(w){}};// Graphint n,n_x; // [1, n]: point; [n+1, n_x]: floweredge g[N][N]; // adjacent matrix// flowervector<int>flower[N]; // nodes in flower i (outer flower)int root[N]; // flower root, root<=n root=i: normal nodesint flower_from[N][N]; // flower_from[b][x]: outermost flower in b that contains x// slackT label[N]; // node label, [1, n] point label, [n+1, n_x] flower labelint col[N]; // color saved at flower rootint slv[N]; // slack node of NON LEAF NODES, slv[y]=x z(x,y) min_x// matchint mat[N]; // match, mat[x]=y (x,y)\in Eint fa[N]; // fa in cross treeint vis[N]; // if in pathqueue<int>Q; // bfs queue// calculate slvinline T calc_slv(edge e){return label[e.u]+label[e.v]-e.w;}inline void update_slv(int u,int v){if(!slv[v]||calc_slv(g[u][v])<calc_slv(g[slv[v]][v]))slv[v]=u;}inline void recalc_slv(int u){slv[u]=0;for(int i=1;i<=n;i++)if(g[i][u].w>0&&root[i]!=u&&col[root[i]]==1)update_slv(i,u);}// only push nodes, not flowersvoid q_push(int x){if(x<=n)Q.push(x);else for(auto p:flower[x])q_push(p);}// set root of all nodes in x to rvoid set_root(int x,int r){root[x]=r;if(x>n)for(auto p:flower[x])set_root(p,r);}// return a (+-)^k path in flower b from root[b] to xint get_even_path_in_flower(int b,int x){int pr=find(flower[b].begin(),flower[b].end(),x)-flower[b].begin();assert(b>n&&b<=n_x&&pr<flower[b].size()); // b is flower, x in bif(pr%2==0)return pr;reverse(flower[b].begin()+1,flower[b].end());return flower[b].size()-pr;}// set (u,v) match, can be flowervoid set_match(int u,int v){mat[u]=g[u][v].v;if(u>n){edge e=g[u][v];int xr=flower_from[u][e.u];int pr=get_even_path_in_flower(u,xr);for(int i=0;i<pr;i++)set_match(flower[u][i],flower[u][i^1]);set_match(xr,v);rotate(flower[u].begin(),flower[u].begin()+pr,flower[u].end()); // change receptacle}}// link 2 S pointsvoid side_augment(int u,int v){int nv=root[mat[u]],nu=root[fa[nv]];while(1){set_match(u,v);u=nu,v=nv;if(!nv)break;set_match(nv,nu);nv=root[mat[u]],nu=root[fa[nv]];}}void linkSS(int u,int v){side_augment(u,v); side_augment(v,u);}int get_lca(int u,int v){static int t=0;++t; // to avoid clearing viswhile(u||v){if(vis[u]==t)return u;vis[u]=t;u=root[mat[u]];if(u)u=root[fa[u]];if(!u)swap(u,v);}return 0;}void add_blossom(int u,int v,int r){int i,b=n+1;while(b<=n_x&&root[b])b++;if(b>n_x)++n_x;// clearcol[b]=1;label[b]=0;mat[b]=mat[r];flower[b].clear();for(i=1;i<=n_x;i++)g[i][b].w=g[b][i].w=0;for(i=1;i<=n;i++)flower_from[b][i]=0;// construct flowerwhile(u!=r){flower[b].pb(u);u=root[mat[u]];q_push(u);flower[b].pb(u);u=root[fa[u]];}flower[b].pb(r);reverse(flower[b].begin(),flower[b].end());while(v!=r){flower[b].pb(v);v=root[mat[v]];q_push(v);flower[b].pb(v);v=root[fa[v]];}// set as outermost flowerset_root(b,b);// calculate slackfor(auto p:flower[b]){for(i=1;i<=n_x;i++){// set to min slaveif(!g[b][i].w||calc_slv(g[p][i])<calc_slv(g[b][i])){g[b][i]=g[p][i];g[i][b]=g[i][p];}}for(i=1;i<=n;i++)if(flower_from[p][i])flower_from[b][i]=p;}//recalc_slv(b);}// only expand outermost blossom b, b is T(white) blossomvoid expand_blossom(int b){int i,x;for(auto p:flower[b])set_root(p,p);x=flower_from[b][g[b][fa[b]].u];// [0,pr]: (+-)^k, insert into tree, add black to queueint pr=get_even_path_in_flower(b,x);col[x]=2;fa[x]=fa[b];for(i=0;i<pr;i+=2){// from bottom to upper layer in treeint white=flower[b][i];int black=flower[b][i+1];col[black]=1;col[white]=2;fa[white]=g[black][white].u;slv[black]=slv[white]=0;q_push(black);}// others: color=0for(i=pr+1;i<flower[b].size();i++){col[flower[b][i]]=0;recalc_slv(flower[b][i]);}// delete broot[b]=0;flower[b].clear();}// found_edgeint augment_path(edge e){int u=root[e.u],v=root[e.v];if(!col[v]){assert(mat[v]);fa[v]=e.u;col[v]=2;int nu=root[mat[v]];slv[nu]=slv[v]=0;col[nu]=1;q_push(nu);}else if(col[v]==1){int r=get_lca(u,v);if(r)add_blossom(u,v,r);else return linkSS(u,v),1;}return 0;}int augment(){int i;memset(col,0,sizeof(int)*(n_x+1));memset(slv,0,sizeof(int)*(n_x+1));memset(fa,0,sizeof(int)*(n_x+1));Q=queue<int>();for(i=1;i<=n_x;i++)if(root[i]==i&&!mat[i]){// add all unmatched pointscol[i]=1;q_push(i);}if(Q.empty())return 0;while(1){while(!Q.empty()){int p=Q.front();Q.pop();assert(col[root[p]]==1);for(i=1;i<=n;i++){if(g[p][i].w==0||root[i]==root[p])continue;// not in same flowerT d=calc_slv(g[p][i]);if(!d){if(augment_path(g[p][i]))return 1;}else if(col[root[i]]!=2)update_slv(p,root[i]);}}T delta=INF;// calc deltafor(i=1;i<=n;i++)if(col[root[i]]==1)delta=min(delta,label[i]);for(i=n+1;i<=n_x;i++)if(root[i]==i&&col[i]==2)delta=min(delta,label[i]/2);for(i=1;i<=n_x;i++){if(root[i]!=i||!slv[i])continue;if(!col[i])delta=min(delta,calc_slv(g[slv[i]][i]));else if(col[i]==1)delta=min(delta,calc_slv(g[slv[i]][i])/2);}// update labelfor(i=1;i<=n;i++){if(col[root[i]]==1)label[i]-=delta;else if(col[root[i]]==2)label[i]+=delta;}for(i=n+1;i<=n_x;i++){if(root[i]!=i)continue;if(col[i]==1)label[i]+=2*delta;else if(col[i]==2)label[i]-=2*delta;}for(i=1;i<=n;i++)if(label[i]<=0)return 0;for(i=1;i<=n_x;i++){if(root[i]!=i||!slv[i]||root[slv[i]]==i)continue;if(calc_slv(g[slv[i]][i])==0&&augment_path(g[slv[i]][i]))return 1;}// expandfor(i=n+1;i<=n_x;i++)if(root[i]==i&&col[i]==2&&label[i]==0)expand_blossom(i);}return 0;}void init(int _n,vector<pair<T,pii>>edges){int i,j;n=n_x=_n;memset(mat,0,sizeof(mat));for(i=0;i<=n;i++){root[i]=i;flower[i].clear();for(j=0;j<=n;j++){flower_from[i][j]=(i==j)?i:0;g[i][j]=edge(i,j,0);}}T w_max=0;for(auto pr:edges){int u=pr.se.fi,v=pr.se.se;T w=pr.fi;g[u][v]=edge(u,v,w*2);g[v][u]=edge(v,u,w*2);w_max=max(w_max,w);}for(i=1;i<=n;i++)label[i]=w_max;}pair<int,T>calc(){int i,cnt=0;T s=0;while(augment())++cnt;for(i=1;i<=n;i++)if(mat[i]>i)s+=g[i][mat[i]].w/2;return mp(cnt,s);}
}
int main(){int i,n,m;vector<pair<T,pii>>edges;scanf("%d%d",&n,&m);for(i=0;i<m;i++){int u,v;T w;scanf("%d%d%lld",&u,&v,&w);edges.pb(mp(w,mp(u,v)));}blossom_tree::init(n,edges);printf("%lld\n",blossom_tree::calc().se);for(i=1;i<=n;i++)printf("%d ",blossom_tree::mat[i]);return 0;
}

(全抄会 WA 的哦!)

参考资料与工具

  1. OI-wiki,一般图最大匹配
  2. Fuyuki,题解 P6113 【模板】一般图最大匹配
  3. 胡拉哥,算法设计技巧:Primal-Dual
  4. wenruo,KM算法详解+模板
  5. Shawn-Yang,KM算法--学习笔记
  6. 陈胤伯,2015 年国家集训队论文《浅谈图的匹配算法及其应用》
  7. jacky860226,general-graph-weighted-match-slides
  8. vfleaking,妈妈我终于会一般图最大权匹配了!
  9. Joris_VR,Maximum Weighted Matching
  10. zhongzihao,一般图最大权(最大)匹配
  11. Kurt Mehlhorn et al.,Implementation of O(nmlogn) weighted matchings in general graphs: the power of data structures
  12. Visualizations of Graph Algorithms
  13. Graph Editor
  14. NetworkX

提交通道

  1. 洛谷
  2. UOJ
  3. Library checker

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 前端面试——什么是原型和原型链
  • 这个暑假作业有点特别,帮100位老人开启这个功能
  • 一个很大的文件,文件的每一行是一个很大的数字,如果给你一个单机,内存比较小,存不了这么大的文件,但是硬盘是无限大的,如何对文件做一个排序输出
  • K8S部署MySQL5.7的主从服务
  • MFC程序设计(三)常用复杂控件的使用
  • 从零到上线,乔拓云助力快速构建在线教育平台
  • 【面试题系列Vue05】跟其他人不太一样的 Vue生命周期总结
  • 文案生成器,快速生成改写文案的捷径
  • 《黑神话:悟空》研发公司的薪资水平
  • SpringBoot集成kafka-自定义拦截器(可以在拦截器中做记录日志、安全检查等操作)
  • 汽车线束品牌服务商推荐-力可欣:致力于汽车连接线束和汽车连接器的开发、生产和应用
  • 挂个人-CSDN Java优秀内容博主rundreamsFly抄袭
  • C语言从头学51—多文件项目
  • 培训第三十八天(上传镜像,私有仓库下载镜像,跨主机容器间的通信,harbor软件包下载)
  • RK3568平台(平台总线篇)SPI驱动框架分析
  • 【许晓笛】 EOS 智能合约案例解析(3)
  • 【跃迁之路】【669天】程序员高效学习方法论探索系列(实验阶段426-2018.12.13)...
  • 2017年终总结、随想
  • Android开源项目规范总结
  • Asm.js的简单介绍
  • JavaScript类型识别
  • Java-详解HashMap
  • Joomla 2.x, 3.x useful code cheatsheet
  • PHP 小技巧
  • vue--为什么data属性必须是一个函数
  • webpack+react项目初体验——记录我的webpack环境配置
  • webpack入门学习手记(二)
  • 缓存与缓冲
  • 聊一聊前端的监控
  • 入口文件开始,分析Vue源码实现
  • 算法之不定期更新(一)(2018-04-12)
  • 一加3T解锁OEM、刷入TWRP、第三方ROM以及ROOT
  • 翻译 | The Principles of OOD 面向对象设计原则
  • 回归生活:清理微信公众号
  • 如何正确理解,内页权重高于首页?
  • 选择阿里云数据库HBase版十大理由
  • ​Linux Ubuntu环境下使用docker构建spark运行环境(超级详细)
  • ​比特币大跌的 2 个原因
  • #VERDI# 关于如何查看FSM状态机的方法
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • (2)关于RabbitMq 的 Topic Exchange 主题交换机
  • (Matlab)基于蝙蝠算法实现电力系统经济调度
  • (vue)页面文件上传获取:action地址
  • (备份) esp32 GPIO
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (二)【Jmeter】专栏实战项目靶场drupal部署
  • (二)测试工具
  • (附源码)spring boot校园健康监测管理系统 毕业设计 151047
  • (附源码)springboot炼糖厂地磅全自动控制系统 毕业设计 341357
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (四)Android布局类型(线性布局LinearLayout)
  • (一)模式识别——基于SVM的道路分割实验(附资源)
  • .【机器学习】隐马尔可夫模型(Hidden Markov Model,HMM)
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • .NET Framework与.NET Framework SDK有什么不同?