【学习笔记】KM 算法

122 篇文章 0 订阅
27 篇文章 0 订阅

省流: K M \rm KM KM 算法可以认为是 zkw \texttt{zkw} zkw 费用流放在二分图最大权匹配上了。我们暂且希望它还能有常数上的优势吧。

壹、二分图

这个我实在不想讲。

1. 定义

可以用两种颜色将点染色,使得任意两个有边直接相连的节点的颜色不同,则该图为二分图。

任取一种染色方案(不妨设染成了黑白两色),染成黑色的点的集合是 X X X 部(或写作 X \frak X X 部),染成白色的点的集合是 Y Y Y 部(或写成 Y \frak Y Y 部)。

Comment. 有人问我为什么要用旧德式字体,我的回答是:我找不出其他任何地方会用到了

2. 完美匹配

大小为 ∣ X ∣ = ∣ Y ∣ |\frak X|=|\frak Y| X=Y 的匹配,就是完美的。

3. 完备匹配

指最大匹配等于 min ⁡ ( ∣ X ∣ , ∣ Y ∣ ) \min(|\frak X|,|\frak Y|) min(X,Y) 的情况。完美匹配是完备匹配的特殊情况。

贰、匈牙利算法

用于解决二分图最大匹配问题。

1. 增广路

任意取一个点 x ∈ X x\in\frak X xX,你可以试着去跟 y ∈ Y y\in\frak Y yY 匹配。如果 y y y 已经有匹配了呢?就让原本与 y y y 匹配的 p ∈ X p\in\frak X pX 另寻新欢!也就是说,如果存在另一个匹配,能够维持现有匹配的大小,并且 y y y 不在匹配中,那就可以加入 ⟨ x , y ⟩ \langle x,y\rangle x,y 这条边。而想要 y y y 不在匹配中,就必须让 p p p 取消这个原有的匹配 ⟨ p , y ⟩ \langle p,y\rangle p,y

如果所有 p p p 都别无选择呢?那么 x x x 是否匹配无所谓。毕竟我们需要的只是最大化匹配数量。 x x x 的匹配必然带来别人匹配的丢失,同时 已匹配的 y y y 的集合不变,所以 对后续匹配无影响

如果用 f ( x ) f(x) f(x) 表示能否让 x x x 寻到一个匹配(为零则不可,为正数则可以),用 p ( x ) p(x) p(x) 表示 x x x 与谁匹配,则
f ( x ) = ∑ ⟨ x , y ⟩ ∈ E [ p ( y ) = 0 ] + f [ p ( y ) ] f(x)=\sum_{\lang x,y\rang\in{\Bbb E}}\big[p(y)=0\big]+f[p(y)] f(x)=x,yE[p(y)=0]+f[p(y)]

此时有一个大问题:转移有环!举个例子:原本 ⟨ A , B ⟩ , ⟨ C , D ⟩ \lang A,B\rang,\lang C,D\rang A,B,C,D 玩得好好的,突然来个 E E E 说: D D D 给我, C C C 滚蛋。 然后 C C C B B B ,说服 A A A 找别人。 A A A 看到了 D D D ,结果又让 C C C 去更换!

考虑 f ( x ) f(x) f(x) 的作用:让某个 x 0 x_0 x0 获得匹配的机会。既然如此(好像没什么因果关系),我们 规定转移不能出现环。那么,在递归过程中,已经访问了 S S S 集合中的点,就不能继续访问了。

转移确实无环了。但此时的 f f f 似乎会算错……么?如果将 f ( x ) f(x) f(x) 的计算视为 d f s \rm dfs dfs 的计算,那么 f [ p ( y ) ] f[p(y)] f[p(y)] 就是递归子状态。一个很重要的特点是,只要一个 f ≠ 0 f\ne 0 f=0 就可以使得 f ( x ) ≠ 0 f(x)\ne 0 f(x)=0,因为它会从所有子节点得到信息。换句话说,我们只需要找到一个 p ( y ) = 0 p(y)=0 p(y)=0 的递归出口。所以我们实质上只需要找出 f ( x 0 ) f(x_0) f(x0) 可以到达的状态有哪些;故而状态转移中的环可以直接忽略。

于是暴力 d f s \tt dfs dfs 求出 f ( x 0 ) f(x_0) f(x0) 即可,时间复杂度是 O ( n + m ) \mathcal O(n+m) O(n+m) 的。

求出了 f ( x 0 ) ≠ 0 f(x_0)\ne 0 f(x0)=0 却找不到匹配,悲剧啊!当然你可以把 d f s \rm dfs dfs 树建出来,然后一路往下。事实上, d f s \rm dfs dfs 的回溯过程(返回值为 t r u e \rm true true 的那条链)中即可更改。

总复杂度 O ( n 2 + n m ) \mathcal O(n^2+nm) O(n2+nm) ,但难以达到 n 3 n^3 n3,因为稠密图的匹配很容易找到。——其实匈牙利的本质就是 E K EK EK 求最大流,而网络流的复杂度是玄学

2. 代码

由于 f f f 可以保留,在 d f s \rm dfs dfs 回溯时不清空 v i s vis vis 数组。但是更改 x 0 x_0 x0 就需要清空了!

bool vis[MAXN]; // need to clear
vector<int> G[MAXN]; // graph
int match[MAXN]; // part Y
bool dfs(int x){
	if(vis[x]) return false;
	vis[x] = true; // remove cycle
	for(auto y : G[x]) // ∀ <x,y>
		if(!match[y] || dfs(match[y]))
			return match[y] = x, true;
	return false; // unable to change
}

而我们注意到,所有会访问到的 x x x 都是 m a t c h ( y ) match(y) match(y),除了最初的 x 0 x_0 x0,并且它一定不会被 m a t c h ( y ) match(y) match(y) 访问到。所以将 v i s vis vis 数组定义在 Y \frak Y Y 部上同样可行。效果是相同的,但是代码短了一行,在 ∣ Y ∣ < ∣ X ∣ |\frak Y|<|\frak X| Y<X 时可以略微减小常数。

bool dfs(int x){
	for(int y : G[x])
		if(!match[y] || (vis[y] == false
		&& (vis[y] = true) && dfs(match[y])))
			return match[y] = x, true;
	return false;
}

还有 b f s \rm bfs bfs 写法,据说 性能更好,可是比较麻烦——比如没有了函数递归的隐式栈,我们要用 p r e pre pre 模拟栈来修改。

void getAugment(int y,int x){
	while(y != 0){
		match[y] = x;
		y = prex[x]; x = prey[y];
	}
}
bool bfs(int x){
	int *fro = que, *bac = fro+1;
	for(prex[*que=x]=0; fro!=bac; ++fro)
		for(int y : G[*fro]) if(!vis[y]){
			if(!match[y]) return getAugment(y,*fro), true;
			prey[y] = *fro, prex[match[y]] = y;
			*(bac ++) = match[y], vis[y] = true;
		}
	return false;
}

3. 例题

  • 洛谷板题:用上面的 d f s \rm dfs dfs 代码即可通过。这里只补充主函数。
int main(){
	int X = readint(), Y = readint();
	int m = readint();
	for(int i=1; i<=m; ++i){
		int x = readint();
		G[x].push_back(readint());
	}
	int ans = 0;
	for(int i=1; i<=X; ++i){
		memset(vis+1,0,X);
		if(dfs(i)) ++ ans;
	}
	printf("%d\n",ans);
    return 0;
}
  • 劈配:我特意转载的匈牙利做法,为了让大家更熟悉匈牙利。其实是因为我没学懂网络流。

叁、 K M \rm KM KM 算法

解决二分图最大权完美匹配。也就是说,保证是完美匹配时,让边权和最大。

1. 思想

把边权变为点权。为什么可以这样?或许是出于 L P \rm LP LP 对偶吧,我猜。

边权完全等价地放在点上,不可能——否则我们将得出,所有的匹配的权值都是一样的,于是最大权匹配就讲完啦!所以我们只能考虑 “最大化” 背景下的转化。等式行不通,考虑不等式。令 v ( x ) v(x) v(x) 为点权,那么必须满足
∀ ⟨ x , y ⟩ ∈ E ,    v ( x ) + v ( y ) ⩾ w ( x , y ) \forall \lang x,y\rang\in{\Bbb E},\;v(x)+v(y)\geqslant w(x,y) x,yE,v(x)+v(y)w(x,y)

这是基本性质。因为这一条可以保证 答案不超过点权和。而我们只需要想办法取到点权和。也就是让满足 v ( x ) + v ( y ) = w ( x , y ) v(x)+v(y)=w(x,y) v(x)+v(y)=w(x,y) 的边构成完美匹配。称这些边为亮边,其余边为暗边。

模拟 H u n g a r i a n \rm Hungarian Hungarian 算法的做法,考虑每次让一个 X \frak X X 部的点获得匹配。如果亮边可以找到增广路,那就直接增广。否则,我们要调整点权,这也是算法的核心。实际上就一句话的事儿:对于 d f s \rm dfs dfs 中访问过的每一个点,如果属于 X \frak X X 部,将其权值减小 d    ( d > 0 ) d\;(d>0) d(d>0),如果属于 Y \frak Y Y 部,将其权值增大 d d d

来看看,这样操作之后会发生什么。不妨设 d f s \rm dfs dfs 中访问了集合 S S S 中的点。对于边 ⟨ i , j ⟩ \lang i,j\rang i,j,不妨设 i ∈ X i\in{\frak X} iX j ∈ Y j\in\frak Y jY,分成四类来考虑。

  • 如果 i ∈ S ,    j ∈ S i\in S,\;j\in S iS,jS 或者 i ∉ S ,    j ∉ S i\notin S,\;j\notin S i/S,j/S,显然没有影响(基本性质仍然满足)。
  • 如果 i ∈ S ,    j ∉ S i\in S,\;j\notin S iS,j/S,那么这条边是暗边——因为 d f s \rm dfs dfs 的对象是 X \frak X X 部的点,这条边是亮边则必然被访问。现在 v ( i ) v(i) v(i) 减小了,这条边可能变亮,但是也可能变得不满足基本性质。
  • 如果 i ∉ S ,    j ∈ S i\notin S,\;j\in S i/S,jS,天知道这条边是亮还是暗。反正 v ( j ) v(j) v(j) 增大会让它暗下去,并且不可能导致非法情况。

为了使得第二类边不出问题(仍然满足基本性质:端点权值和不小于边权),取
d = min ⁡ ⟨ i , j ⟩ , i ∈ S ∩ X , j ∉ S [ v ( i ) + v ( j ) − w ( i , j ) ] d=\min_{\lang i,j\rang,i\in S\cap{\frak X},j\notin S}\big[v(i)+v(j)-w(i,j)\big] d=i,j,iSX,j/Smin[v(i)+v(j)w(i,j)]

而后 S S S 必然增大。因为原有的边不消失,新加入的边一定可以用到。 ∣ S ∣ = 2 n |S|=2n S=2n 时,当前点必然获得一个匹配。如果我们每次操作都是 O ( m ) \mathcal O(m) O(m) 暴力找 d d d 加上 O ( n + m ) \mathcal O(n{\rm+}m) O(n+m) 求增广路,一个点要操作 n n n 次使得 ∣ S ∣ |S| S 增大到 2 n 2n 2n,那么总复杂度 O [ n ⋅ n ⋅ ( n + m ) ] = O ( n 3 + m n 2 ) \mathcal O[n\cdot n\cdot (n+m)]=\mathcal O(n^3+mn^2) O[nn(n+m)]=O(n3+mn2) 的。

你可能有这样的疑惑:一定存在二类边吗?答案是肯定的。否则,这说明 S ∩ X S\cap{\frak X} SX 的邻点都属于 S S S,但是在 S S S 中仍然找不到 S ∩ X S\cap{\frak X} SX 的完备匹配——于是原图中也就不存在完美匹配了。

考虑到 m = O ( n 2 ) m=\mathcal O(n^2) m=O(n2),整个算法并不是很快。复杂度瓶颈在于找 d d d 以及求增广路,两个都和 m m m 相关。我们尝试优化一下,先从求增广路开始吧!

算法流程中,每次 ∣ S ∣ |S| S 只增大 2 2 2 ,也就是 只加入了一条边,但是我们要 从头开始 d f s \rm dfs dfs 。这难道是合理的吗?而且终极目标都是给 x 0 x_0 x0 找到匹配罢了。上面 H u n g a r i a n \rm Hungarian Hungarian 算法中也有提到,我们只需要知道从 x 0 x_0 x0 出发能走到哪些 x ∈ X x\in\frak X xX 罢了。于是我们考虑 保留 d f s \rm dfs dfs 树原有形态,也就是说, x 0 x_0 x0 能走到的点还是能走到,只看能不能走到更多的点。

避免了多次增广,已经将一个点的 O ( n ) \mathcal O(n) O(n) 次操作的总复杂度降至 O ( n + m ) \mathcal O(n{\rm+}m) O(n+m),好耶!

接下来解决求 d d d 的问题。用 S Y ( x ) {\rm SY}(x) SY(x),意思是 S e a r c h    Y \rm Search\;\frak Y SearchY 部,存储每个 Y \frak Y Y 部的点 作为端点的边的最小值。即
S Y ( y ) = min ⁡ ⟨ i , y ⟩ , i ∈ S [ v ( i ) + v ( y ) − w ( i , y ) ] ( y ∈ Y ) {\rm SY}(y)=\min_{\lang i,y\rang,i\in S}\big[v(i)+v(y)-w(i,y)\big]\quad(y\in\frak Y) SY(y)=i,y,iSmin[v(i)+v(y)w(i,y)](yY)

为什么不用 X \frak X X 部的点呢?因为它们随着 S S S 变大,条件 j ∉ S j\notin S j/S 越发难满足,而 min ⁡ \min min 是很难删除的。与此相对的是, S Y ( x ) {\rm SY}(x) SY(x) 是容易维护的,并且第二类边实际上加入的就是 Y \frak Y Y 部的点,这与 p r i m \tt prim prim 比较相似。

此时,我们只需要用 O ( n ) \mathcal O(n) O(n) 扫一遍 S Y {\rm SY} SY 数组,就能找到我们要新加入的点。加入的点直接开始增广。增广的过程中还可以顺便修改 S Y {\rm SY} SY 的值,一举多得啊!

于是两个含有 m m m 的操作都不见了!时间复杂度就变成 O ( n 3 + n m ) \mathcal O(n^3+nm) O(n3+nm) 的了,基本上是匈牙利算法的复杂度。

2. 代码

洛谷题解区有许多 b f s \tt bfs bfs 的代码,欲者自取。这里贴一份 d f s \tt dfs dfs 的代码吧。

const int INFTY = (1<<30)-1;
const int NO_SUCH_THING = -INFTY;
const int MAXN = 502;
int g[MAXN][MAXN]; ///< if no edge exists, be @a NO_SUCH_THING
int match[MAXN], prex[MAXN], prey[MAXN];
void getAugment(int y,int x){
	while(y) match[y] = x, swap(y,prex[x]), x = prey[y];
}
int vx[MAXN], vy[MAXN], dis[MAXN];
bool vis[MAXN]; // defined on X part
int dfs(int x,const int &n){
	vis[x] = true; int d;
	for(int i=1,o; i<=n; ++i){
		if(g[x][i] == NO_SUCH_THING) continue;
		if(match[i] && vis[match[i]]) continue;
		if((d = vx[x]+vy[i]-g[x][i]) == 0){
			prey[i] = x; if(!match[i]) return i;
			if(!!(o = dfs(match[i],n))) return o;
		}
		else if(dis[i] > d) dis[i] = d, prey[i] = x;
	}
	return 0; // no augment path found
}
int main(){
	int n = readint();
	rep(i,1,n) rep(j,1,n)
		g[i][j] = NO_SUCH_THING;
	for(int m=readint(),a,b; m; --m){
		a = readint(), b = readint();
		g[a][b] = readint();
	}
	rep(i,1,n) rep(j,1,n)
		if(g[i][j] != NO_SUCH_THING)
			vx[i] = max(vx[i],g[i][j]);
	for(int x=1,o; x<=n; ++x){
		memset(vis+1,false,n);
		fill(dis+1,dis+n+1,INFTY);
		for(o=dfs(x,n); o==0; ){
			int_ slack = INFTY;
			rep(i,1,n) if(!vis[match[i]])
				getMin(slack,dis[i]);
			rep(i,1,n) if(vis[i]) vx[i] -= slack;
			rep(i,1,n) // Y part
				if(vis[match[i]]) vy[i] += slack;
				else if(dis[i] != INFTY) dis[i] -= slack;
			for(int i=1; i<=n&&o==0; ++i)
				if(!vis[match[i]] && !dis[i]){
					if(!match[i]) o = i;
					else o = dfs(match[i],n);
				}
		}
		getAugment(o,prey[o]);
	}
}

3. 注解

边权转为点权,还是不等式,这太糟糕了……它到底想干什么?仔细想想 v ( x ) + v ( y ) ⩾ w ( x , y ) v(x)+v(y)\geqslant w(x,y) v(x)+v(y)w(x,y),有点像松弛?难道说它是某种 最短路?也就是说——它其实就是最小费用流吗?

从最小费用流的角度考虑,边权是 − w ( x , y ) -w(x,y) w(x,y),令 v ( y ) v(y) v(y) 变为原来的相反数,由上式有 [ − w ( x , y ) ] + v ( x ) − v ( y ) ⩾ 0 [-w(x,y)]+v(x)-v(y)\geqslant 0 [w(x,y)]+v(x)v(y)0 。这就是最小费用流的 d i j k s t r a \tt dijkstra dijkstra 方法中的 “势函数” 嘛!

然而 d i j k s t r a \tt dijkstra dijkstra 方法中 h ( x ) h(x) h(x) 就是残留网络上从 S S S 出发的最短路,这里则应该是到汇点 T T T 的距离。 K M \rm KM KM 相当于假定 d i s ( x ) = v ( x ) dis(x)=v(x) dis(x)=v(x),然后慢慢调整。用狗狗的话说:“好蠢呐!”

另外一个困扰了我很久的问题:顶标的范围有多大?值得注意的是,每次进行 v ( x ) v(x) v(x) d d d 同时 v ( y ) v(y) v(y) d d d 的操作时, ∑ v ( x ) + ∑ v ( y ) \sum v(x)+\sum v(y) v(x)+v(y) 都减小了 d d d,因为 x x x 的数量比 y y y 多一个。显然 ∑ d = O ( n w ) \sum d=\mathcal O(nw) d=O(nw),其中 w w w 是边权的值域。那么 v ( x ) v(x) v(x) 每次都减小 d d d,就可能达到 n w nw nw 的修改量!

而这个上界是可以达到的——只需要让一条增广路径经过 ( n − 1 ) (n{-}1) (n1) 条边权最大的边。也就是 X \frak X X 部的 i i i 号点向 Y \frak Y Y 部的 ( i + 1 ) (i{+}1) (i+1) 号点连边,权值为 max ⁡ v \max v maxv,它们会最初作为匹配边;然后 X \frak X X 部的 i i i 号点向 Y \frak Y Y 部的 i i i 号点连边,权值为 min ⁡ v \min v minv,为了达到完美匹配,不得不使用这些边。显然此时到 T T T 的最短路长度是 O ( n w ) \mathcal O(nw) O(nw),于是顶标也是这个范围了!

另外说一句:当 w ≪ max ⁡ v w\ll\max v wmaxv 时,顶标的瓶颈是 O ( max ⁡ v ) \mathcal O(\max v) O(maxv) 的初始顶标。解决方案是将每条边的权值都减去 min ⁡ v \min v minv

4. 推广

这个 K M \rm KM KM 算法的限制太紧了——必须是完美匹配,这在很多情况下都难以实现。来看看这两个拓展:

  • 求原图的最大权值匹配(所有匹配中权值最大的一个)。
  • 求原图的最大权值最大匹配(最大匹配中权值最大的一个)。

对于第一个,不妨设 ∣ X ∣ ⩾ ∣ Y ∣ |\frak X|\geqslant|\frak Y| XY,把 Y \frak Y Y 部补充一些虚点,使得 ∣ X ∣ = ∣ Y ∣ |\frak X|=|\frak Y| X=Y 。然后用权值为 0 0 0 的边将图补充成完全图。于是我们就有完美匹配啦!由于权值是 0 0 0,肯定是最后被选择的,仅用来补全完美匹配;负数边权可以直接扔掉嘛。

对于第二个,操作方式更神奇。与上面相同的补边,但是边权要设置为 − ∞ -\infty 才行!只有这样,我们才会心甘情愿地选出最大匹配——总的边数是定值,存在的边构成的匹配数越大, − ∞ -\infty 即不存在的边数量越少。似乎 可以在上面的代码上做微调,鉴于我从未实现过,不妄加评论。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值