最小割树小结

介绍

emm……不能说简单,但是是个很容易理解的东西,思想很巧妙。

大概能帮你求出一个无向图中两两点之间的最小割,也可能有一些奇妙的用途。

正题

为了方便,下面记 x x x y y y 的最小割中,与 x x x 相连的点集为 V x V_x Vx,与 y y y 相连的点集为 V y V_y Vy,设 f ( x , y ) f(x,y) f(x,y) 为这个最小割的容量。

顾名思义,这东西是棵树。每条边 ( x , y ) (x,y) (x,y) 满足一个性质:断开这条边后,与 x x x 相连的点集恰好为 V x V_x Vx,与 y y y 相连的点集恰好为 V y V_y Vy,并且这条边的权值恰好为 f ( x , y ) f(x,y) f(x,y)

然后这棵树上就有个很妙的结论:对于任意两点 x , y x,y x,y,他们之间的最小割就是两点路径中最小的边权


先证明这个奇妙的结论:

  • 引理1: 对于 u ∈ V x , v ∈ V y u\in V_x,v\in V_y uVx,vVy,有 f ( u , v ) ≤ f ( x , y ) f(u,v)\leq f(x,y) f(u,v)f(x,y)
  • 证明: 显然割断了 x , y x,y x,y 的同时也割断了 u , v u,v u,v,所以割断 u , v u,v u,v 的代价是不超过 f ( x , y ) f(x,y) f(x,y) 的。
  • 引理2: f ( x , y ) ≥ min ⁡ { f ( x , z ) , f ( z , y ) } f(x,y)\geq \min\{f(x,z),f(z,y)\} f(x,y)min{ f(x,z),f(z,y)}
  • 证明: 由于最小割等于最大流,在最大流的角度看 min ⁡ { f ( x , z ) , f ( z , y ) } \min\{f(x,z),f(z,y)\} min{ f(x,z),f(z,y)} 就是从 x x x 流到 y y y 的流量中经过 z z z 的部分,那这个部分肯定是比总流量少的。
  • 引理2的推论: f ( x , y ) ≥ min ⁡ { f ( x , z 1 ) , f ( z 1 , z 2 ) , . . . , f ( z k , y ) } f(x,y)\geq \min\{f(x,z_1),f(z_1,z_2),...,f(z_k,y)\} f(x,y)min{ f(x,z1),f(z1,z2),...,f(zk,y)}
  • 证明: 简单归纳一下就好,一开始我们有 f ( x , y ) ≥ min ⁡ { f ( x , z 1 ) , f ( z 1 , y ) } f(x,y)\geq \min\{f(x,z_1),f(z_1,y)\} f(x,y)min{ f(x,z1),f(z1,y)}
    假设此时已经满足 f ( x , y ) ≥ min ⁡ { f ( x , z 1 ) , f ( z 1 , z 2 ) , . . . , f ( z i − 1 , y ) } f(x,y)\geq \min\{f(x,z_1),f(z_1,z_2),...,f(z_{i-1},y)\} f(x,y)min{ f(x,z1),f(z1,z2),...,f(zi1,y)},那么不难发现将 f ( z i − 1 , y ) f(z_{i-1},y) f(zi1,y) 替换为 min ⁡ { f ( z i − 1 , z i ) , f ( z i , y ) } \min\{f(z_{i-1},z_i),f(z_i,y)\} min{ f(zi1,zi),f(zi,y)},上式依然成立,带入就得到了 f ( x , y ) ≥ min ⁡ { f ( x , z 1 ) , f ( z 1 , z 2 ) , . . . , f ( z i , y ) } f(x,y)\geq \min\{f(x,z_1),f(z_1,z_2),...,f(z_i,y)\} f(x,y)min{ f(x,z1),f(z1,z2),...,f(zi,y)}

此时通过引理2的推论其实已经能看出路径的影子了,一开始的结论就可以简单地证明出来。

假设 ( u , v ) (u,v) (u,v) x x x y y y 路径上权值最小的边,那么根据这个推论, f ( x , y ) ≥ f ( u , v ) f(x,y)\geq f(u,v) f(x,y)f(u,v)。而根据最小割树地定义, x ∈ V u , y ∈ V v x\in V_u,y\in V_v xVu,yVv,所以有 f ( u , v ) ≥ f ( x , y ) f(u,v)\geq f(x,y) f(u,v)f(x,y),即 f ( u , v ) = f ( x , y ) f(u,v)=f(x,y) f(u,v)=f(x,y)

这样就证完了。


有了这个结论,我们可以通过倍增或预处理之类的求出两两点之间的最小割,那么要如何构造这棵树呢?

考虑递归,假设当前的点集为 V V V,从中随便选两个点 x , y x,y x,y,跑一遍dinic,然后将 x , y x,y x,y 连边,以最小割中分成的两个连通块作为新的点集继续构造,时间复杂度 O ( n 3 m ) O(n^3m) O(n3m),但众所周知dinic往往是跑不满的……

正确性的话,其实就是要保证,假如某次将 V V V 割成了 V 1 , V 2 V_1,V_2 V1,V2 两个点集,递归构造时会在 V 1 V_1 V1 中选两个点 x , y x,y x,y 跑dinic,跑完之后在 x x x y y y 的最小割中, V x V_x Vx V y V_y Vy 完整包含 V 2 V_2 V2

E 1 E_1 E1 为将 V V V 割成 V 1 , V 2 V_1,V_2 V1,V2 的割边,事实上,在 x , y x,y x,y 的最小割中,假如 x x x y y y 的一条增广路经过 V 2 V_2 V2,那么割掉的边就一定是 E 1 E_1 E1 中的,于是最后 V 2 V_2 V2 一定是完整的。手玩一下不难发现。

模板题传送门

于是代码如下:

#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
#define maxn 510
#define pb push_back

int n,m,Q,a[maxn];
struct edge{
    
    int y,z,next;}e[6010];
int first[maxn],len=1;
void buildroad(int x,int y,int z){
    
    e[++len]=(edge){
    
    y,z,first[x]};first[x]=len;}
void ins(int x,int y,int z){
    
    buildroad(x,y,z);buildroad(y,x,0);}
int S,T,q[maxn],st,ed,h[maxn],cur[maxn];
bool bfs(){
    
    
	memset(h,0,sizeof(h));
	q[st=ed=1]=S;h[S]=1;
	while(st<=ed){
    
    
		int x=q[st++];cur[x]=first[x];
		for(int i=first[x];i;i=e[i].next){
    
    
			int y=e[i].y;
			if(!h[y]&&e[i].z)h[y]=h[x]+1,q[++ed]=y;
		}
	}
	return h[T];
}
int dfs(int x,int flow){
    
    
	if(x==T)return flow; int tt=0;
	for(int i=cur[x];i;i=e[i].next){
    
    
		int y=e[i].y;cur[x]=i;
		if(h[y]==h[x]+1&&e[i].z){
    
    
			int p=dfs(y,min(e[i].z,flow-tt));tt+=p;
			e[i].z-=p;e[i^1].z+=p;
		}
		if(tt==flow)break;
	}
	if(!tt)h[x]=0;
	return tt;
}
vector<int> E[maxn],w[maxn];
int tmpl[maxn],tmpr[maxn],lt,rt;
void buildGHtree(int l,int r){
    
    
	if(l==r)return;
	S=a[l];T=a[l+1];
	for(int i=2;i<=len;i+=2)//注意将图初始化,也就是将反边的流量丢回给正边
		e[i].z+=e[i^1].z,e[i^1].z=0;
	int val=0;while(bfs())val+=dfs(S,1e9);
	E[S].pb(T);E[T].pb(S);w[S].pb(val);w[T].pb(val);
	lt=rt=0;for(int i=l;i<=r;i++)
		if(h[a[i]])tmpl[++lt]=a[i];
		else tmpr[++rt]=a[i];
	for(int i=1;i<=lt;i++)a[l+i-1]=tmpl[i];
	for(int i=1;i<=rt;i++)a[l+lt+i-1]=tmpr[i];
	int sp=lt;buildGHtree(l,l+sp-1);buildGHtree(l+sp,r);
}
int ans[maxn][maxn];//由于复杂度问题n一般很小,所以可以n^2预处理两两间的答案
void dfs_getans(int x,int fa,int d){
    
    
	for(int i=0;i<E[x].size();i++){
    
    
		int y=E[x][i];if(y==fa)continue;
		if(fa==-1)ans[d][y]=w[x][i];
		else ans[d][y]=min(ans[d][x],w[x][i]);
		dfs_getans(y,x,d);
	}
}

int main()
{
    
    
	scanf("%d %d",&n,&m);
	for(int i=1,x,y,z;i<=m;i++)
		scanf("%d %d %d",&x,&y,&z),ins(x,y,z),ins(y,x,z);
	for(int i=0;i<=n;i++)a[i]=i;
	buildGHtree(0,n);
	for(int i=0;i<=n;i++)dfs_getans(i,-1,i);
	scanf("%d",&Q);
	for(int i=1,x,y;i<=Q;i++)
		scanf("%d %d",&x,&y),printf("%d\n",ans[x][y]);
}

例题1

All Pairs Maximum Flow

板子题,权当练练手,代码就想必不需要了。

要注意的是初始化,以及这题行末不能有空格……

ZJOI 2011 最小割

板子题二号。

CQOI 2016 不同的最小割

板子题三号。

例题2

CF343E Pumping Stations

先建出最小割树,问题变成找一个排列,每次从 a i a_i ai 走到 a i + 1 a_{i+1} ai+1,权值为路径上最小的边权。

考虑树上边权最小的那条边,显然这条边不可能经过多次,假如 x → y , y → z x\to y,y\to z xy,yz 经过了两次这条边,那么显然 x → z , z → y x\to z,z\to y xz,zy 是更优的,因为 x → z x\to z xz 不经过这条边,那么权值一定更大。

然后删掉这条边,递归考虑两棵子树的最小边权的边,反复考虑最后发现每条边只会产生一次贡献,答案就是每条边权值之和。

构造答案的话,就是先断开最小边权的边,先走左子树,走完之后跨到右子树继续走,也是递归一下就好了。

去掉最小割树模板后的代码:

bool v[maxn][maxn];
void go(int x,int fa,int &now){
    
    
	a[now++]=x;for(int y:E[x])
		if(!v[x][y]&&y!=fa)go(y,x,now);
}
void getans(int l,int r){
    
    
	if(l==r){
    
    printf("%d ",a[l]);return;}
	int x,y,z=1e9;
	for(int i=l;i<=r;i++)
		for(int j=0;j<E[a[i]].size();j++)
			if(!v[a[i]][E[a[i]][j]]&&w[a[i]][j]<z)//找到边权最小的边
				x=a[i],y=E[a[i]][j],z=w[a[i]][j];
	v[x][y]=v[y][x]=true;//标记一下
	int now=l;go(x,0,now);//找到左右子树
	int sp=now;go(y,0,now);
	getans(l,sp-1);getans(sp,r);//递归
}

int main()
{
    
    
	scanf("%d %d",&n,&m);
	for(int i=1,x,y,z;i<=m;i++)
		scanf("%d %d %d",&x,&y,&z),ins(x,y,z),ins(y,x,z);
	for(int i=1;i<=n;i++)a[i]=i;
	buildGHtree(1,n);
	printf("%d\n",ans);getans(1,n);
}

例题3

洛谷 P3729 曼哈顿计划EX

首先当然还是要建出最小割树。那么每次询问相当于:找一个点集 V V V,在 ∑ v ∈ V w v ≥ x \sum_{v\in V}w_v\geq x vVwvx 的前提下, min ⁡ u , v ∈ V f ( u , v ) \min_{u,v\in V}f(u,v) minu,vVf(u,v) 最大, f ( u , v ) f(u,v) f(u,v) u u u v v v 的最小割。

先删去树上的所有边,从大到小依次加入,加入了一条长度为 c c c 的边,连接了 u , v u,v u,v 所在的连通块后,假设这个新连通块内所有点 w w w 之和为 W W W,那么就可以使 a n s W ans_W ansW c c c max ⁡ \max max

这样求出的 a n s i ans_i ansi 就是权值和恰好为 i i i 的最大的最小割,再求个后缀 max ⁡ \max max 就是至少。

不难发现这样求出来的 a n s ans ans 是正确的,要使两两最小割最大,那么点集一定在同一个连通块内,那么就不妨将连通块内所有点都算在点集里,毕竟少几个点贡献也是 c c c,多一点 W W W 还能变大,贡献更多 a n s ans ans

代码如下:

bool cmp(Edge x,Edge y){
    
    return x.z>y.z;}
int fa[maxn],ans[1000010];
int findfa(int x){
    
    return x==fa[x]?x:fa[x]=findfa(fa[x]);}
void link(int x,int y){
    
    
	x=findfa(x),y=findfa(y);
	w[x]+=w[y];fa[y]=x;
}
void chkmax(int &x,int y){
    
    if(y>x)x=y;}

int main()
{
    
    
	scanf("%d %d %d",&n,&m,&Q);
	for(int i=1;i<=n;i++)scanf("%d",&w[i]);
	for(int i=1,x,y;i<=m;i++)
		scanf("%d %d",&x,&y),ins(x,y,1),ins(y,x,1);
	for(int i=1;i<=n;i++)a[i]=fa[i]=i;
	buildGHtree(1,n);sort(E.begin(),E.end(),cmp);//E存放了最小割树上的边
	for(int i=1;i<=n;i++)chkmax(ans[w[i]],n+1);
	for(Edge i:E){
    
    
		link(i.x,i.y);
		int x=findfa(i.x);
		chkmax(ans[min(w[x],1000000)],i.z);
	}
	for(int i=999999;i>=1;i--)chkmax(ans[i],ans[i+1]);
	for(int i=1,x;i<=Q;i++){
    
    
		scanf("%d",&x);
		if(!ans[x])puts("Nuclear launch detected");
		else if(ans[x]==n+1)puts("nan");
		else printf("%d\n",ans[x]);
	}
}

猜你喜欢

转载自blog.csdn.net/a_forever_dream/article/details/113925091