一个网络 (G=(V,E)) 是一张有向图,图中每条有向边 ((x,y)in E) 都有一个给定的权值 (c(x,y)),称为边的容量。特别地,若 ((x,y) otin E),则 (c(x,y)=0)。图中还有两个指定的特殊节点 (S,T in V(S eq 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 eq S, x eq 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|}) 最大
二分 (gleqslantfrac{|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
",work());
for(int i=1;i<=n;++i)
if(vis[i])
printf("%d
",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=sumlimits_{i o x} low(i,x)-sumlimits_{x o 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
",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
",ans-dinic());
}
return 0;
}