网络流
一个网络 \(G=(V,E)\) 是一张有向图,图中每条有向边 \((x,y)\in E\) 都有一个给定的权值 \(c(x,y)\),称为边的容量。特别地,若 \((x,y) \notin E\),则 \(c(x,y)=0\)。图中还有两个指定的特殊节点 \(S,T \in V(S \neq T)\) 分别被称为源点和汇点
设 \(f(x,y)\) 是定义在节点二元组 \((x \in V,y \in V)\) 上的实数函数,且满足:
容量限制:\(f(x,y) \leq c(x,y)\)
斜对称:\(f(x,y)=-f(y,x)\)
流量守恒:\(\forall x \neq S,\ x \neq T,\ \sum_{(u,x)\ \in E} f(u,x) = \sum_{(x,v)\ \in E}f(x,v)\)
\(f\) 称为网络的流函数,对于 \((x,y) \in E\),\(f(x,y)\) 称为边的流量,\(c(x,y)-f(x,y)\) 称为边的剩余流量
\(\sum_{(S,v)\ \in E} f(S,v)\) 称为整个网络的流量(\(S\) 为源点)
最大流
Edmond—Karp算法
若一条从源点 \(S\) 到汇点 \(T\) 的路径上各条边的剩余容量都大于 \(0\),则称这条路径为一条增广路
\(EK\) 算法为用 \(bfs\) 不断寻找增广路,直到网络上不存在增广路为止
用 \(bfs\) 找到任意一条从 \(S\) 到 \(T\) 的路径,记录路径上各边的剩余容量的最小值,则网络的流量就可以增加这个最小值
利用邻接表成对存储来实现 \((x,y)\) 剩余容量的减小,\((y,x)\) 剩余容量的增大
时间复杂度上界为 \(O(nm^2)\),一般可以处理 \(1e3 \sim 1e4\) 规模的网格
\(code\):
bool bfs()
{
memset(vis,0,sizeof(vis));
queue<int> q;
q.push(s);
vis[s]=true;
res[s]=inf;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v;
if(vis[y]||!v) continue;
res[y]=min(res[x],v);
pre[y]=i;
q.push(y);
vis[y]=true;
}
}
return vis[t];
}
void update()
{
int x=t;
while(x!=s)
{
int i=pre[x];
e[i].v-=res[t];
e[i^1].v+=res[t];
x=e[i^1].to;
}
ans+=res[t];
}
......
while(bfs()) update();
Dinic算法
在任意时刻,网络中所有节点以及剩余容量大于\(0\)的边构成的子图称为残量网络
\(Dinic\)算法引入分层图的概念,\(d_x\) 表示从 \(S\) 到 \(x\) 最少需要经过的边数,为了方便处理设 \(d_S=1\),分层图为残量网络中满足 \(d_y =d_x +1\) 的边 \((x,y)\) 构成的子图
时间复杂度上界为 \(O(n^2m)\),一般可以处理 \(1e4 \sim 1e5\) 规模的网格,求解二分图最大匹配的时间复杂度为 \(O( \sqrt nm)\)
在 \(Dinic\) 算法中还可以加入若干剪枝来优化
\(res\),表示当前节点的流量剩余,若 \(res \leqslant 0\),停止寻找增广路
\(cur_x\),表示当到达到\(x\)节点时,直接从 \(cur_x\) 对应的边开始遍历,实际表示上一次从 \(x\) 遍历到了哪一条边,因为在这之间的边都已经被彻底增广过了,所以可以直接跳转,称为当前弧优化
\(code\):
bool bfs()
{
queue<int> q;
for(int i=s;i<=t;++i) d[i]=0,cur[i]=head[i];
d[s]=1,q.push(s);
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
if(d[y]||!e[i].v) continue;
d[y]=d[x]+1,q.push(y);
}
}
return d[t];
}
int dfs(int x,int lim)
{
if(x==t) return lim;
int res=lim,flow;
for(int &i=cur[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v;
if(d[y]!=d[x]+1||!v) continue;
if(flow=dfs(y,min(v,res)))
{
res-=flow;
e[i].v-=flow;
e[i^1].v+=flow;
if(!res) break;
}
}
return lim-res;
}
int dinic()
{
int flow,ans=0;
while(bfs())
while(flow=dfs(s,inf))
ans+=flow;
return ans;
}
若要求每条边的所用的流量,可以将原图备份,跑完最大流后,用原图的容量减去当前的剩余容量即可求得所用流量
对于容量为实数,应该这样写:
\(code:\)
bool bfs()
{
queue<int> q;
for(int i=s;i<=t;++i) d[i]=0,cur[i]=head[i];
q.push(s),d[s]=1;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
double v=e[i].v;
if(d[y]||fabs(v)<eps) continue;
d[y]=d[x]+1,q.push(y);
}
}
return d[t];
}
double dfs(int x,double lim)
{
if(x==t) return lim;
double res=lim,flow,sum=0;
for(int &i=cur[x];i;i=e[i].nxt)
{
int y=e[i].to;
double v=e[i].v;
if(d[y]!=d[x]+1||fabs(v)<eps) continue;
flow=dfs(y,min(res,v)),sum+=flow;
res-=flow,e[i].v-=flow,e[i^1].v+=flow;
if(fabs(res)<eps) break;
}
return sum;
}
不能像之前一样用流量限制减去剩余流量,因为限制可能为一个极大值,其减去一个较小值后,会舍掉精度
二分图最大匹配的必须边和可行边
跑完网络流后在残量网络上进行 \(Tarjan\) 缩点
必须边:\((x,y)\) 流量为 \(1\),且 \(x\) 和 \(y\) 在残量网络上属于不同的强连通分量
可行边:\((x,y)\) 流量为 \(1\),或 \(x\) 和 \(y\) 在残量网络上属于同一个强连通分量
最小割
图中所有的割中,边权值和最小的割为最小割,最大流 \(=\) 最小割
利用最小割,将求解最大收益转化为最小代价
平面图最小割 \(=\) 对偶图最短路,应用有狼抓兔子和海拔
最小割的必须边和可行边
可行边:被某一种最小割的方案包含
判断:满流,在残量网络上不存在 \(x\) 到 \(y\) 的路径。(缩点后判断,等价于 \(x,y\) 属于不同的强连通分量)
证明:存在路径,说明其该边不存在必要性
必须边:一定在最小割中、扩大容量后能增大最大流
判断:满流,是可行边,在残量网络上存在 \(S\) 到 \(x\) 和 \(y\) 到 \(T\) 的路径。(缩点后判断,等价于 \(x\) 和 \(S\) 属于同一个强连通分量,\(y\) 和 \(T\) 属于同一个强连通分量)
证明:不割的话,\(S\) 和 \(T\) 就会连通
最大权闭合子图
若有向图 \(G\) 的子图 \(V\) 满足:\(V\) 中顶点的所有出边均指向 \(V\) 内部的顶点,则称 \(V\) 是 \(G\) 的一个闭合子图
若 \(G\) 中的点有点权,则点权和最大的闭合子图称为有向图 \(G\) 的最大权闭合子图
建立源点 \(S\) 和汇点 \(T\) ,源点 \(S\) 连所有点权为正的点,容量为该点点权;其余点连汇点 \(T\),容量为该点点权的相反数,对于原图中的边 \((x,y)\),连边 \((x,y,inf)\),割从源点出发的边表示不选这个点,割指向汇点的边表示选这个点
最大权闭合图的点权和 \(=\) 所有正权点权值和 \(-\) 最小割
也就是最大收益转化为了最小代价
在残量网络中由源点 \(S\) 能够访问到的点,就构成一个点数最少的最大权闭合图
最大密度子图
一个无向图 \(G=(V,E)\) 的边数 \(|E|\) 与点数 \(|V|\) 的比值 \(D=\frac{|E|}{|V|}\) 称为它的密度
求 \(G\) 的一个子图 \(G^\prime=(V^\prime,E^\prime)\),使得 \(D^\prime=\frac{|E^\prime|}{|V^\prime|}\) 最大
二分 \(g\leqslant\frac{|E|}{|V|}\),得 \(|E|-|V|×g \geqslant0\)
源点 \(S\) 向所有边连容量为 \(1\) 的边,边向其两端的点连容量为 \(inf\) 的边,点向汇点 \(T\) 连容量为 \(g\) 的边
二分下界:\(\frac{1}{n}\),上界:\(m\),精度:\(\frac{1}{n^2}\)
\(code:\)
bool bfs()
{
for(int i=s;i<=t;++i) cur[i]=head[i];
memset(d,0,sizeof(d));
queue<int> q;
q.push(s);
d[s]=1;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
double v=e[i].v;
if(d[y]||fabs(v)<eps) continue;
d[y]=d[x]+1;
q.push(y);
}
}
return d[t];
}
double dfs(int x,double lim)
{
if(x==t) return lim;
double res=lim,flow;
for(int &i=cur[x];i;i=e[i].nxt)
{
int y=e[i].to;
double v=e[i].v;
if(d[y]!=d[x]+1||fabs(v)<eps) continue;
if(fabs(flow=dfs(y,min(res,v)))>=eps)
{
res-=flow;
e[i].v-=flow;
e[i^1].v+=flow;
if(fabs(res)<eps) break;
}
}
return lim-res;
}
double dinic()
{
double flow,ans=0;
while(bfs())
while(fabs(flow=dfs(s,inf))>=eps)
ans+=flow;
return ans;
}
double check(double x)
{
edge_cnt=1;
memset(head,0,sizeof(head));
for(int i=1;i<=n;++i) add(i+m,t,x);
for(int i=1;i<=m;++i)
{
int x=ed[i].x,y=ed[i].y;
add(s,i,1.0),add(i,x+m,inf),add(i,y+m,inf);
}
return m*1.0-dinic();
}
int work()
{
int ans=0;
memset(du,0,sizeof(du));
memset(vis,0,sizeof(vis));
check(g);
for(int i=1;i<=m;++i)
{
int x=ed[i].x,y=ed[i].y;
if(d[i])
{
if(++du[x]==1) ans++,vis[x]=true;
if(++du[y]==1) ans++,vis[y]=true;
}
}
return ans;
}
......
l=0,r=m,g=0;
while(l+1/((double)n*(double)n)<r)
{
double mid=(l+r)/2.0;
if(check(mid)>eps) g=l=mid;
else r=mid;
}
printf("%d\n",work());
for(int i=1;i<=n;++i)
if(vis[i])
printf("%d\n",i);
最小割二元关系
二元关系指如选和不选的关系
建立最小割模型,来解决一系列问题,如happiness、文理分科和人员雇佣
费用流
Edmond—Karp算法
\(code\):
bool spfa()
{
for(int i=1;i<=n;++i) dis[i]=inf;
memset(vis,0,sizeof(vis));
queue<int> q;
q.push(s);
vis[s]=true;
dis[s]=0;
res[s]=inf;
while(!q.empty())
{
int x=q.front();
q.pop();
vis[x]=false;
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v,c=e[i].c;
if(dis[y]>dis[x]+c&&v)
{
dis[y]=dis[x]+c;
res[y]=min(res[x],v);
pre[y]=i;
if(!vis[y])
{
vis[y]=true;
q.push(y);
}
}
}
}
return dis[t]!=inf;
}
void update()
{
int x=t;
while(x!=s)
{
int i=pre[x];
e[i].v-=res[t];
e[i^1].v+=res[t];
x=e[i^1].to;
}
ans+=res[t];
sum+=res[t]*dis[t];
}
......
while(spfa()) update();
Dinic算法
\(code\):
bool spfa()
{
queue<int> q;
for(int i=1;i<=n;++i) dis[i]=inf,vis[i]=false;
q.push(s),vis[s]=true,dis[s]=0;
while(!q.empty())
{
int x=q.front();
q.pop(),vis[x]=false;
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,c=e[i].c;
if(dis[y]<=dis[x]+c||!e[i].v) continue;
dis[y]=dis[x]+c;
if(!vis[y]) q.push(y),vis[y]=true;
}
}
return dis[t]!=inf;
}
int dfs(int x,int lim)
{
if(x==t) return lim;
vis[x]=true;
int flow,res=lim;
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v;
if(dis[y]!=dis[x]+e[i].c||!v||vis[y]) continue;
if(flow=dfs(y,min(res,v)))
{
res-=flow;
e[i].v-=flow;
e[i^1].v+=flow;
if(!res) break;
}
}
return lim-res;
}
void dinic()
{
int flow;
while(spfa())
while(flow=dfs(s,inf))
ans+=flow,sum+=flow*dis[t];
}
可以去求解二分图带权匹配
有上下界限制的网络流
无源汇有上下界可行流
\(n\)个点,\(m\)条边的网络,求一个可行解,使得边 \((x,y)\) 的流量介于 \([\ low_{x,y},up_{x,y}\ ]\) 之间,并且整个网络满足流量守恒
将 \(up_{x,y}-low_{x,y}\) 作为容量上界,\(0\) 作为容量下界
设 \(in_x=\sum\limits_{i\to x} low(i,x)-\sum\limits_{x\to i} low(x,i)\)
若 \(in_x >0\),则从源点 \(S\) 向 \(x\) 连边,容量为 \(in_x\),反之,则从 \(x\) 向汇点 \(T\) 连边,容量为 \(-in_x\)
在该网络上求最大流,求完后每条边的流量再加上容量下界即为一种可行流
\(code:\)
bool bfs()
{
memcpy(cur,head,sizeof(head));
memset(d,0,sizeof(d));
queue<int> q;
q.push(s);
d[s]=1;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v;
if(d[y]||!v) continue;
d[y]=d[x]+1;
q.push(y);
}
}
return d[t];
}
int dfs(int x,int lim)
{
if(x==t) return lim;
int res=lim,k;
for(int &i=cur[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v;
if(d[y]!=d[x]+1||!v) continue;
if(k=dfs(y,min(res,v)))
{
res-=k;
e[i].v-=k;
e[i^1].v+=k;
if(!res) break;
}
}
return lim-res;
}
int dinic()
{
int flow,ans=0;
while(bfs())
while(flow=dfs(s,inf))
ans+=flow;
return ans;
}
bool check()
{
for(int i=head[s];i;i=e[i].nxt)
if(e[i].v)
return false;
return true;
}
......
for(int i=1;i<=m;++i)
{
int a,b,up;
read(a),read(b),read(low[i]),read(up);
in[a]-=low[i],in[b]+=low[i];
add(a,b,up-low[i]);
}
for(int i=1;i<=n;i++)
{
if(in[i]>0) add(s,i,in[i]);
else add(i,t,-in[i]);
}
dinic();
if(check())
{
puts("YES");
for(int i=1;i<=m;i++) printf("%d\n",e[(i<<1)^1].v+low[i]);
}
else puts("NO");
有源汇有上下界最大流
从 \(T\) 向 \(S\) 连一条容量上界为 \(inf\),容量下界为 \(0\) 的边,使有源汇转化为无源汇
在残量网络上再求原源点到原汇点的最大流
\(code:\)
bool bfs()
{
memcpy(cur,head,sizeof(head));
memset(d,0,sizeof(d));
queue<int> q;
q.push(s);
d[s]=1;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v;
if(d[y]||!v) continue;
d[y]=d[x]+1;
q.push(y);
}
}
return d[t];
}
int dfs(int x,int lim)
{
if(x==t) return lim;
int res=lim,k;
for(int &i=cur[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v;
if(d[y]!=d[x]+1||!v) continue;
if(k=dfs(y,min(res,v)))
{
res-=k;
e[i].v-=k;
e[i^1].v+=k;
if(!res) break;
}
}
return lim-res;
}
int dinic()
{
int flow,ans=0;
while(bfs())
while(flow=dfs(s,inf))
ans+=flow;
return ans;
}
bool check()
{
for(int i=head[s];i;i=e[i].nxt)
if(e[i].v)
return false;
return true;
}
......
for(int i=1;i<=m;++i)
{
int a,b,up,low;
read(a),read(b),read(low),read(up);
in[a]-=low,in[b]+=low;
add(a,b,up-low);
}
for(int i=1;i<=n;i++)
{
if(in[i]>0) add(s,i,in[i]);
else add(i,t,-in[i]);
}
add(T,S,inf);
dinic();
ans=e[edge_cnt].v;
e[edge_cnt].v=e[edge_cnt^1].v=0;
if(check())
{
s=S,t=T;
printf("%d",ans+dinic());
}
else puts("NO");
有源汇有上下界最小流
先不添加 \(T\) 到 \(S\) 的边,求一次超级源到超级汇的最大流。
然后再添加一条从 \(T\) 到 \(S\) 下界为 \(0\) ,上界为 \(inf\) 的边,在残量网络上再求一次超级源到超级汇的最大流
流经 \(T\) 到 \(S\) 的边的流量就是最小流的值
\(code:\)
bool bfs()
{
memcpy(cur,head,sizeof(head));
memset(d,0,sizeof(d));
queue<int> q;
q.push(s);
d[s]=1;
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v;
if(d[y]||!v) continue;
q.push(y);
d[y]=d[x]+1;
}
}
return d[t];
}
int dfs(int x,int lim)
{
if(x==t) return lim;
int res=lim,k;
for(int &i=cur[x];i;i=e[i].nxt)
{
int y=e[i].to,v=e[i].v;
if(d[y]!=d[x]+1||!v) continue;
if(k=dfs(y,min(res,v)))
{
res-=k;
e[i].v-=k;
e[i^1].v+=k;
if(!res) break;
}
}
return lim-res;
}
int dinic()
{
int k,flow=0;
while(bfs())
{
while(k=dfs(s,inf))
{
flow+=k;
}
}
return flow;
}
bool check()
{
for(int i=head[s];i;i=e[i].nxt)
if(e[i].v)
return false;
return true;
}
......
for(int i=1;i<=m;++i)
{
int a,b,up,low;
read(a),read(b),read(low),read(up);
in[a]-=low,in[b]+=low;
add(a,b,up-low);
}
for(int i=1;i<=n;i++)
{
if(in[i]>0) add(s,i,in[i]);
else add(i,t,-in[i]);
}
dinic();
add(T,S,inf);
dinic();
if(!check())
{
puts("please go home to sleep");
return 0;
}
printf("%d",e[edge_cnt].v);
循环流
以最大费用循环流为例,对于边 \((x,y,v,c)\),若费用为正,则将其先流满,记录费用总和 \(sum\),通过建立源汇点来实现补流,边正常连。若费用为负,则连边 \((x,y,v,-c)\)
然后跑最小费用最大流得出费用 \(ans\),最终最大费用循环流求解的答案为 \(sum-ans\)
JZOJ Tree
最大费用循环流,树上的边从上向下连,容量为 \(d\),费用为 \(0\),每条路径的边从下向上连,容量为 \(1\),费用为 \(c\)
求解时,先将所有边跑满流,然后增加源汇点来使流量平衡,通过跑最小费用最大流来实现退流,删去不合法的边的贡献
还有一种更强的做法:
转载自JZOJ 100003. 【NOI2017模拟.4.1】 Tree(费用流)
\(code:\)
#include<bits/stdc++.h>
#define maxn 500010
#define maxm 5000010
#define inf 1000000000000000
using namespace std;
typedef long long ll;
template<typename T> inline void read(T &x)
{
x=0;char c=getchar();bool flag=false;
while(!isdigit(c)){if(c=='-')flag=true;c=getchar();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
if(flag)x=-x;
}
int T,n,m,s,t;
ll ans;
int in[maxn],de[maxn];
ll dis[maxn];
bool vis[maxn];
struct edge
{
int to,nxt,v;
ll c;
}e[maxm];
int head[maxn],edge_cnt;
void add(int from,int to,int val,int cost)
{
e[++edge_cnt]=(edge){to,head[from],val,cost};
head[from]=edge_cnt;
e[++edge_cnt]=(edge){from,head[to],0,-cost};
head[to]=edge_cnt;
}
void Add(int from,int to,int val,ll cost)
{
in[from]+=val,in[to]-=val,ans+=cost,add(from,to,val,cost);
}
struct Edge
{
int to,nxt,v;
}ed[maxn];
int hd[maxn],e_cnt;
void link(int from,int to,int val)
{
ed[++e_cnt]=(Edge){to,hd[from],val};
hd[from]=e_cnt;
}
void dfs_pre(int x,int fa)
{
de[x]=de[fa]+1;
for(int i=hd[x];i;i=ed[i].nxt)
{
int y=ed[i].to;
if(y==fa) continue;
Add(x,y,ed[i].v,0),dfs_pre(y,x);
}
}
bool spfa()
{
for(int i=s;i<=t;++i) vis[i]=0,dis[i]=inf;
queue<int> q;
q.push(s),dis[s]=0,vis[s]=true;
while(!q.empty())
{
int x=q.front();
q.pop(),vis[x]=false;
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
ll v=e[i].v,c=e[i].c;
if(dis[y]>dis[x]+c&&v)
{
dis[y]=dis[x]+c;
if(!vis[y])
{
vis[y]=true;
q.push(y);
}
}
}
}
return dis[t]!=inf;
}
ll dfs(int x,ll lim)
{
if(x==t) return lim;
vis[x]=true;
ll res=lim,flow;
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
ll v=e[i].v,c=e[i].c;
if(dis[y]!=dis[x]+c||!v||vis[y]) continue;
if(flow=dfs(y,min(res,v)))
{
res-=flow;
e[i].v-=flow;
e[i^1].v+=flow;
if(!res) break;
}
}
return lim-res;
}
ll dinic()
{
ll flow,sum=0;
while(spfa())
while(flow=dfs(s,inf))
sum+=flow*dis[t];
return sum;
}
void clear()
{
e_cnt=ans=0,edge_cnt=1;
memset(in,0,sizeof(in));
memset(hd,0,sizeof(hd));
memset(head,0,sizeof(head));
}
int main()
{
read(T);
while(T--)
{
clear(),read(n),read(m),t=n+1;
for(int i=1;i<n;++i)
{
int x,y,v;
read(x),read(y),read(v);
link(x,y,v),link(y,x,v);
}
dfs_pre(1,0);
for(int i=1;i<=m;++i)
{
int x,y,v;
read(x),read(y),read(v);
if(de[x]<de[y]) swap(x,y);
Add(x,y,1,v);
}
for(int i=1;i<=n;++i)
{
if(in[i]>0) add(s,i,in[i],0);
else add(i,t,-in[i],0);
}
printf("%lld\n",ans-dinic());
}
return 0;
}