介绍
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 u∈Vx,v∈Vy,有 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(zi−1,y)},那么不难发现将 f ( z i − 1 , y ) f(z_{i-1},y) f(zi−1,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(zi−1,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 x∈Vu,y∈Vv,所以有 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
板子题,权当练练手,代码就想必不需要了。
要注意的是初始化,以及这题行末不能有空格……
板子题二号。
板子题三号。
例题2
先建出最小割树,问题变成找一个排列,每次从 a i a_i ai 走到 a i + 1 a_{i+1} ai+1,权值为路径上最小的边权。
考虑树上边权最小的那条边,显然这条边不可能经过多次,假如 x → y , y → z x\to y,y\to z x→y,y→z 经过了两次这条边,那么显然 x → z , z → y x\to z,z\to y x→z,z→y 是更优的,因为 x → z x\to z x→z 不经过这条边,那么权值一定更大。
然后删掉这条边,递归考虑两棵子树的最小边权的边,反复考虑最后发现每条边只会产生一次贡献,答案就是每条边权值之和。
构造答案的话,就是先断开最小边权的边,先走左子树,走完之后跨到右子树继续走,也是递归一下就好了。
去掉最小割树模板后的代码:
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
首先当然还是要建出最小割树。那么每次询问相当于:找一个点集 V V V,在 ∑ v ∈ V w v ≥ x \sum_{v\in V}w_v\geq x ∑v∈Vwv≥x 的前提下, min u , v ∈ V f ( u , v ) \min_{u,v\in V}f(u,v) minu,v∈Vf(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]);
}
}