[学习笔记] 最小割树
做 \(\tt CQOI\) 的时候突然发现这个知识点没有学过,所以来学一下,虽然用处不大但是还是很有趣的。
最小割树的构建
最小割树用来解决这样的问题:一个 \(n\) 个点的图,想要求 \((i,j)\) 之间的最小割。
我们先来解决他的构建,先来构建他再证明有关定理。
构建是一个分治的过程,我们先随便选两个点 \((x,y)\) ,然后求出他们的最小割。我们在最小割树上将 \((x,y)\) 连一条长度为最小割的边,此时原图就被割裂成了两个连通块:\(V_x,V_y\),我们可以知道每个点属于哪个连通块,以此我们将所有点分成两部分,递归进行上面的过程,注意我们求的是 在原图上的最小割 ,这样分治一定能建出一棵树。
最小割树的性质
最重要的性质就是两点树上路径边权的最小值就是在原图上这两点的最小割,先来证明一些定理。为了描述方便,我们记 \(\lambda(a,b)\) 表示 \((a,b)\) 之间的最小割,还是要提醒:最小割后每个点要么属于 \(V_x\) ,要么属于 \(V_y\)
定理1 :对于任意 \(a\in V_x,b\in V_y\) ,有 \(\lambda(a,b)\leq\lambda(x,y)\)
证明很简洁,因为我们可以用 \(\lambda(x,y)\) 的花费把 \(a,b\) 割成两个联通块,他们真正的最小割只会更小。
定理2:对于任意三点 \(a,b,c\) 有 \(\lambda(a,b)\geq\min(\lambda(b,c),\lambda(a,c))\)
你会发现只有 \(\lambda(a,b)\) 是三者中最小的时候才有证明的必要,否则就直接成立了。
我们考虑求出 \(\lambda(a,b)\) 的最小割后 \(c\) 是属于 \(V_b\) 的(另一种情况同理),那么根据定理\(1\) :\(\lambda(a,b)\geq\lambda(a,c)\),又因为 \(\lambda(a,b)\) 是最小的,所以 \(\lambda(a,b)\leq\lambda(a,c)\),所以 \(\lambda(a,b)=\lambda(a,c)\),那么这个等式就证明了。
定理3:对于最小割树上的路径 \((x,y)\) ,路径上最小的最小割是 \(\lambda(a,b)\) ,有 \(\lambda(x,y)=\lambda(a,b)\)
根据定理\(2\):\(\lambda(x,y)\) 大于等于树上路径上最小割的最小值,也就是 \(\lambda(x,y)\geq\lambda(a,b)\)
然后又因为最小割树的构造,\((a,b)\) 的最小割是把 \((x,y)\) 划分成了两部分的,所以 \(\lambda(x,y)\leq\lambda(a,b)\)
所以 \(\lambda(x,y)=\lambda(a,b)\) ,证毕。
例题
[模板] 最小割树,好像这道题的数据并没有问题。
基本的代码就写一下网络流,类似整体二分的分治、\(\tt lca\) 就可以了,听起来很简单是吧,但是把我写傻了。
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int M = 3005;
const int inf = 0x3f3f3f3f;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,q,tot,s,t,f[M],dis[M],a[M],b[M],c[M];
int dep[M],fa[M][10],mi[M][10];
struct edge
{
int v,c,next;
edge(int V=0,int C=0,int N=0) : v(V) , c(C) , next(N) {}
}e[2*M];
struct node
{
int v,c;
node(int V=0,int C=0) : v(V) , c(C) {}
};vector<node> g[M];
//网络流部分
void add(int u,int v,int c)
{
e[++tot]=edge(v,c,f[u]),f[u]=tot;
e[++tot]=edge(u,0,f[v]),f[v]=tot;
}
int bfs()
{
queue<int> q;
memset(dis,0,sizeof dis);
q.push(s);dis[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();
if(u==t) return 1;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(!dis[v] && e[i].c>0)
{
dis[v]=dis[u]+1;
q.push(v);
}
}
}
return 0;
}
int dfs(int u,int ept)
{
if(u==t) return ept;
int flow=0;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(dis[v]==dis[u]+1 && e[i].c>0)
{
int tmp=dfs(v,min(e[i].c,ept));
if(!tmp) continue;
ept-=tmp;
e[i].c-=tmp;
e[i^1].c+=tmp;
flow+=tmp;
if(!ept) break;
}
}
return flow;
}
int work(int x,int y)
{
for(int i=2;i<=tot;i+=2)
{
e[i].c=e[i].c+e[i^1].c;
e[i^1].c=0;//还原原图
}
int mx=0;s=x;t=y;
while(bfs()) mx+=dfs(s,inf);
return mx;
}
//最小割树部分
void solve(int l,int r)
{
if(l==r) return ;
int w=work(a[l],a[r]),tl=0,tr=0;
g[a[l]].push_back(node(a[r],w));
g[a[r]].push_back(node(a[l],w));
for(int i=l;i<=r;i++)
{
if(dis[a[i]]) b[++tl]=a[i];
else c[++tr]=a[i];
}
for(int i=1;i<=tl;i++) a[l+i-1]=b[i];
for(int i=1;i<=tr;i++) a[l+tl+i-1]=c[i];
solve(l,l+tl-1);
solve(l+tl,r);
}
void pre(int u,int p)
{
fa[u][0]=p;
dep[u]=dep[p]+1;
for(int i=1;i<10;i++)
{
fa[u][i]=fa[fa[u][i-1]][i-1];
mi[u][i]=min(mi[u][i-1],mi[fa[u][i-1]][i-1]);
}
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i].v,c=g[u][i].c;
if(v==p) continue;
mi[v][0]=c;
pre(v,u);
}
}
int lca(int u,int v)
{
int ans=inf;
if(dep[u]<dep[v]) swap(u,v);
for(int i=9;i>=0;i--)
if(dep[fa[u][i]]>=dep[v])
{
ans=min(ans,mi[u][i]);
u=fa[u][i];
}
if(u==v) return ans;
for(int i=9;i>=0;i--)
if(fa[u][i]^fa[v][i])
{
ans=min(ans,mi[u][i]);
ans=min(ans,mi[v][i]);
u=fa[u][i];v=fa[v][i];
}
ans=min(ans,min(mi[u][0],mi[v][0]));
return ans;
}
signed main()
{
n=read();m=read();tot=1;
for(int i=1;i<=m;i++)
{
int u=read(),v=read(),c=read();
add(u,v,c);
add(v,u,c);//无向连通图
}
for(int i=1;i<=n;i++) a[i]=i;
solve(1,n);
pre(1,0);
q=read();
while(q--)
{
int u=read(),v=read();
printf("%d\n",lca(u,v));
}
}