网络流学习笔记
参考文章:网络流 by Alex_Wei、网络流 by rvalue、Dinic 复杂度分析证明、网络流与二分图相关概念、学长留下的总结文档
一些定义
-
网络:一个有向图 \(G=(V,E)\),每条边都有一个容量 \(c(u,v)\),存在源点 \(s\) 与 汇点 \(t\)。
-
流:\(f(u,v)\) 为边 \((u,v)\) 上的流,满足 \(f(u,v)\le c(u,v)\),\(f(u,v)=-f(v,u)\) 且 \(\forall x\in V \sum_{(u,x)\in E} f(u,x)=\sum_{(x,v)\int E} f(x,v)\)。即满足:容量限制、写对称性、流守恒性。
形式化定义:
\[f(u,v)=\begin{cases} f(u,v)&(u,v)\in E\\ -f(v,u)&(v,u)\in E\\ 0&(u,v)\notin E,(v,u)\notin E \end{cases} \] -
剩余容量:\(c(u,v)-f(u,v)\)
-
残量网络:剩余容量组成的网络
最大流与最小割
增广路算法与 EK 算法
先考虑一个假做法:每次搜索,能加流量就加。
这个算法的问题在于, \((u,v),(v,w),(x,w),(w,y)\) 四条边中 \((w,y)\) 流满时,可以让 \((u,v),(v,w)\) 带来的贡献尽量的小而让 \((u,v)\) 能流向其他节点。
简洁地说:我们需要反悔贪心。
于是构建反向边,初始容量为 \(0\),每次找到一条增广路( \(s\) 到 \(t\) 容量均不为 \(0\))的路径,就将路径上所有边容量减少,反向对应增加,就达到的反悔的效果。
一个简单的优化:每次只增广当前的最短增广路,对于每条边来说,其被流满再被反悔时,最短路一定增加,于是增广上界是 \(O(nm)\),而 \(\text{bfs}\) 找最短路是 \(O(m)\) 的,复杂度 \(O(nm^2)\),上界要多松有多松。这就是 \(\text{Edmonds-Karp}\) 算法。
点击查看代码
int n,m,S,T;
struct Graph{
struct edge{
int to,nxt;
ll lim;
}e[maxm<<2];
int head[maxn],cnt=1;
inline void add_edge(int u,int v,int w){
e[++cnt].to=v,e[cnt].nxt=head[u],e[cnt].lim=w,head[u]=cnt;
e[++cnt].to=u,e[cnt].nxt=head[v],e[cnt].lim=0,head[v]=cnt;
}
ll now_flow[maxn];
int pre[maxn];
inline ll max_flow(int S,int T){
ll flow=0;
while(1){
queue<int> q;
memset(now_flow,-1,sizeof(now_flow));
now_flow[S]=maxxn;
q.push(S);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(e[i].lim&&now_flow[v]==-1){
now_flow[v]=min(now_flow[u],e[i].lim);
pre[v]=i;
q.push(v);
}
}
}
if(now_flow[T]==-1) return flow;
flow+=now_flow[T];
for(int u=T;u!=S;u=e[pre[u]^1].to){
e[pre[u]].lim-=now_flow[T];
e[pre[u]^1].lim+=now_flow[T];
}
}
}
}G;
Dinic 算法
由于最短路只有 \(O(n)\) 级别,能不能每次都直接将所有当前最短路都增广以达到优化的目的?使用 \(\text{dfs}\) 但不能打标记,这是指数级的,不过有很多无用的遍历出现在已经流满的边,于是使用当前弧优化和删点优化,每次只搜索可能增广的边。注意这里的优化是对于单次 \(\text{dfs}\) 的,也就是不会影响下一次对残量网络的最短路的增广,同时当前最短路一定是个分层图(甚至可以理解成树),得到 \(\text{Dinic}\) 算法。
点击查看代码
int n,m,S,T;
struct Graph{
struct edge{
int to,nxt,lim;
}e[maxm<<1];
int head[maxn],cnt=1,cur[maxn];
inline void add_edge(int u,int v,int w){
e[++cnt].to=v,e[cnt].nxt=head[u],e[cnt].lim=w,head[u]=cnt;
e[++cnt].to=u,e[cnt].nxt=head[v],e[cnt].lim=0,head[v]=cnt;
}
int dis[maxn];
ll dfs(int u,ll rest){
if(u==T) return rest;
ll flow=0;
for(int i=cur[u];i&&rest;i=e[i].nxt){
//当前弧优化
cur[u]=i;
int v=e[i].to,c=min(rest,(ll)e[i].lim);
if(dis[u]+1==dis[v]&&c){
ll k=dfs(v,c);
flow+=k,rest-=k;
e[i].lim-=k,e[i^1].lim+=k;
}
}
//删点优化
if(!flow) dis[u]=-1;
return flow;
}
//所有的优化都是对单次dfs有效,是在优化dfs,对整个算法流程没有正确性的影响
inline ll max_flow(int S,int T){
ll flow=0;
//对残量网络跑bfs最短路
while(1){
queue<int> q;
memcpy(cur,head,sizeof(head));
memset(dis,-1,sizeof(dis));
dis[S]=0;
q.push(S);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(dis[v]==-1&&e[i].lim){
dis[v]=dis[u]+1;
q.push(v);
}
}
}
if(dis[T]==-1) return flow;
flow+=dfs(S,maxxn);
}
}
}G;
最大流模型构建
网络流算法与实现并不难理解,主要是建模。
-
点权化边权
流量实际上是对次数的限制,而当我们要求某个点的流量受限时,将其拆成两个点,中间连边的边权即为限制的点权。
-
求最小的问题
求最小代价,最小用时等问题时有以下两种建模方法。
一是考虑补集转化,即求最小代价改成求最大无需的代价,变成最大流模型。
二是考虑二分,这种情况往往是要根据答案来构建模型,有流量的限制,不得不在已知答案的情况下判断是否合法。
最大流最小割定理
最小割:割去一些边使得 \(s\) 与 \(t\) 不连通,且割去的边容量之和最小
最大流等于最小割。
考虑对于每个流,一定存在一个关键边(被流满的)使得其不能再增广,那么割去这条边的容量就是割去的流量,得出最大流等与最小割。
一些图上概念与性质
-
匹配:一个集合 \(E'\in E\) 使得 \(E'\) 中边两两无公共顶点点。
-
独立集:一个集合 \(V'\in V\) 使得 \(V'\) 中点两两无公共边。
-
边覆盖:一个集合 \(E'\in E\) 使得对于 \(u\in V\) 都存在一条以 \(u\) 为顶点的边。
-
点覆盖:一个集合 \(V'\in V\) 使得对于 \(e\in E\) 的至少一个顶点出现在集合中。
-
\(|E_{\max}|+|E_{\min}|=n\)
证明考虑在最大匹配基础上连边,有 \(n-2|E_{\max}|\) 个节点没有连边,每次连一条边就会增加一个节点有边覆盖,那么 \(|E_{\min}|=|E_{\max}|+n-2|E_{\max}|\),也就是:\(|E_{\max}|+|E_{\min}|=n\)
-
\(|V_{\max}|+|V_{\min}|=n\)
可以证明一个更强的结论:独立集的补集是点覆盖,考虑独立集中两两无连边,也就是说独立集中非孤立点的连边一定在这个补集中,也就是这个补集构成了点覆盖,独立集最大时点覆盖最小。
-
在二分图中,\(|E_{\max}|=|V_{\min}|\)
考虑网络流建模,一侧点向源点,另一侧向汇点,容量为 \(1\),其余的边容量无限大。那么最大流一定是最大匹配,因为一个节点最多流 \(1\) 的流量,而最小割一定是割去向源点或汇点的连边,而对于每条二分图的边其左右两条边一定会被割去一条,也就是最小割,根据最大流最小割定理,可证:\(|E_{\max}|=|V_{\min}|\)。
最小割模型构建
(无向图最小割正反向边初始容量都是原容量可以减少边数)
也是比较套路
-
有叠加限制的问题
可以是矩形中相邻,可以是同时选或不选会造成影响。
将源点与汇点视作选与不选,割之后与源点相连的选,反之不选。这时两两连边就代表了限制,初始默认全部都选的情况,求容量要列出方程,考虑都选、都不选以及选择其一时断掉的边以及对应减少的贡献,可以解出方程,注意方程的解常规情况下都是使同类边意义或数值相等的,求出最小割即可。
-
出现负数或零
在上一模型的中,可能会出现正贡献与负贡献同时出现,或容量解出恰好是 \(0\)多数出现在与源点或汇点相连的边中,而这种边选取的个数是固定的,于是可以给一个附加权值,保证是正数。
-
二分图模型
对于不能共存的模型,连无限大的边表示不能共存,如果能保证二分图的性质,可以拆点后与源点汇点连权值的边,不共存的连无限大的边,由于要保证二分图,模型受限比较多。
-
离散变量模型
遇到形如在每个序列中选取一个数,要求权值和最小且相邻序列选取元素的距离不能超过一个定值。
如果没有距离限制可以直接把序列串成链跑最小割,有距离的限制就是不能割两个超过此距离的,只需要保证割完仍然连通,同上面叠加限制问题相似,向距离上界的点连一条容量无限大的边使得割去这两条边仍然连通,注意这里应当是从位置靠后的向考前的连,可以画图理解。
例题:BZOJ-3144 切糕
-
最大权闭合子图模型(另一种叠加限制模型)
将正贡献点连源点,负贡献点连汇点,中间连边表示一种限制关系(必须用同时选),这样最小割要么是牺牲正贡献,要么是接受负贡献。
例题:BZOJ-1497 [NOI2006]最大获利、BZOJ-1565 [NOI2009]植物大战僵尸、BZOJ-4873 [SHOI2017]寿司餐厅
-
平面图最小割转对偶图最短路
把平面图每个边围成的极小封闭图形看作一个点,在 \(S\) 与 \(T\) 之间,连一条边,这条边与原图围成的部分视作一个点,剩余部分视作另一个点。对于两个极小封闭图形之间的边,在构造的图上连一条边。这样求最小割就是在最短路上求刚刚增加的两个点之间最短路。
无向图首先要拆成两条有向边,在网格图当中,常用顺时针法,即原图中边的方向顺时针旋转 \(90\degree\) 得到对偶图边的方向。
最小割树
可以处理任意两点间最小割。
结合 \(\text{Kruksal}\) 重构树的思想,钦定两点 \(s,t\) 为源点汇点,求出最小割后的两个连通块 \(S,T\) 中 \(x\in S,y\in T\),二者最小割一定是 \(\operatorname{mincut}(x,y)\le\operatorname{mincut}(s,t)\),反之 \(x,y\) 连通则 \(s,t\) 连通。
根据这个性质就可以每次选取两个仍然连通的点,跑最小割,在树中加入这条边,然后分成两个小连通块再分治,这样一来,两个点的最小割实际上就是在把他们不断分开的割中取最小值,就是树上路径问题了。
点击查看代码
int n,m,q;
struct Tree{
struct edge{
int v,w;
edge()=default;
edge(int v_,int w_):v(v_),w(w_){}
};
vector<edge> E[maxn];
inline void add_edge(int u,int v,int w){
E[u].push_back(edge(v,w));
E[v].push_back(edge(u,w));
}
int fa[maxn],dep[maxn],siz[maxn],son[maxn],W[maxn];
int top[maxn],dfn[maxn],dfncnt;
int st[maxn][10];
void dfs1(int u,int f,int d){
fa[u]=f,dep[u]=d,siz[u]=1;
int maxson=-1;
for(edge e:E[u]){
int v=e.v,w=e.w;
if(v==f) continue;
W[v]=w;
dfs1(v,u,d+1);
siz[u]+=siz[v];
if(siz[v]>maxson) maxson=siz[v],son[u]=v;
}
}
void dfs2(int u,int t){
top[u]=t,dfn[u]=++dfncnt;
st[dfn[u]][0]=W[u]?W[u]:1e9;
if(!son[u]) return;
dfs2(son[u],t);
for(edge e:E[u]){
int v=e.v;
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
}
}
inline void build_st(){
for(int k=1;k<=9;++k){
for(int i=1;i+(1<<k)-1<=n;++i){
st[i][k]=min(st[i][k-1],st[i+(1<<k-1)][k-1]);
}
}
}
inline int query_st(int l,int r){
int k=log2(r-l+1);
return min(st[l][k],st[r-(1<<k)+1][k]);
}
inline int query(int u,int v){
int res=1e9;
while(top[u]!=top[v]){
if(dep[top[u]]>dep[top[v]]) swap(u,v);
res=min(res,query_st(dfn[top[v]],dfn[v]));
v=fa[top[v]];
}
if(dep[u]>dep[v]) swap(u,v);
if(u!=v) res=min(res,query_st(dfn[u]+1,dfn[v]));
return res;
}
}Tr;
pii res1,res2;
struct Graph{
struct edge{
int to,nxt,lim,tmp;
}e[maxm<<1];
int head[maxn],cnt=1;
inline void add_edge(int u,int v,int w){
e[++cnt].to=v,e[cnt].nxt=head[u],e[cnt].lim=w,e[cnt].tmp=w,head[u]=cnt;
e[++cnt].to=u,e[cnt].nxt=head[v],e[cnt].lim=0,e[cnt].tmp=0,head[v]=cnt;
}
inline void init(){
for(int i=1;i<=cnt;++i) e[i].lim=e[i].tmp;
}
int cur[maxn],dis[maxn];
int dfs(int u,int T,int rest){
if(u==T) return rest;
int flow=0;
for(int i=cur[u];i&&rest;i=e[i].nxt){
cur[u]=i;
int v=e[i].to,C=min(rest,e[i].lim);
if(dis[u]+1==dis[v]&&C){
int k=dfs(v,T,C);
flow+=k,rest-=k;
e[i].lim-=k,e[i^1].lim+=k;
}
}
if(!flow) dis[u]=-1;
return flow;
}
int col[maxn],tot;
inline int max_flow(int S,int T){
int flow=0;
while(1){
queue<int> q;
memcpy(cur,head,sizeof(head));
memset(dis,-1,sizeof(dis));
dis[S]=0;
q.push(S);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(dis[v]==-1&&e[i].lim){
dis[v]=dis[u]+1;
q.push(v);
}
}
}
if(dis[T]==-1){
res1=make_pair(S,0),res2=make_pair(T,0);
++tot;
for(int i=1;i<=n;++i){
if(col[i]!=col[S]) continue;
if(dis[i]!=-1){
if(i!=S) res1.second=i;
}
else{
col[i]=tot;
if(i!=T) res2.second=i;
}
}
return flow;
}
flow+=dfs(S,T,1e9);
}
}
}G;
inline void build(pii node){
int S=node.first,T=node.second;
G.init();
int w=G.max_flow(S,T);
pii tmp1=res1,tmp2=res2;
Tr.add_edge(S,T,w);
if(tmp1.second) build(tmp1);
if(tmp2.second) build(tmp2);
}
最小割的可行边与必须边
可行边:存在于一种最小割方案的边。
必须边:存在于所有最小割方案的边。
首先这需要是满流的边。于是我们在最大流之后的残量网络上跑,如果这条边两端点仍连通,那么这条边不是割。剩下的就是可行边了,而对于必须边而言,其无可替代的地位在于存在一条流,使得这条边是唯一的关键边,要割掉这个流且保证最小割只能割这条,也就是说残量网络上,\(S\) 与 \(u\) 连通,\(v\) 与 \(T\) 连通。
这还不够,直接按照这个判断非常麻烦,我们要借助于最大流算法中用于反悔的反向边。
发现对于判断与源汇是否连通的情况,这条路径一定有流量并且没流满,也就是正反都在残量网络;而判断是不是割,在满流的前提下,反向边出现可以保证,如果这时有正向路径就说明不是割,这其实是求强连通分量。
费用流
EK 费用流
给每条边加上一个费用,即单位流量流经这条边的花费,现在求在保证最大流的前提下的最小花费。
考虑增广的优先级,如果每次增广流量都为 \(1\) 时,一定增广费用最小的一条边,也就是我们以费用最短路为优先级增广是正确的。
反悔时的反向边费用与正向边为相反数,于是求最短路需要用 \(\text{SPFA}\),复杂度 \(O(n^2m^2)\),依旧是跑不满。
这里的算法是基于 \(\text{Edmonds-Karp}\) 的,每次最短路后记录下转移路径,一次只能处理一条增广路。
对于最大费用,在没有负环的情况下可以直接建费用未负的边跑最小费用,为避免负环有时需要拆点。
点击查看代码
int n,m,S,T;
struct Graph{
struct edge{
int to,nxt;
ll lim,c;
}e[maxm<<1];
int head[maxn],cnt=1;
inline void add_edge(int u,int v,ll w,ll c){
e[++cnt].to=v,e[cnt].nxt=head[u],e[cnt].lim=w,e[cnt].c=c,head[u]=cnt;
e[++cnt].to=u,e[cnt].nxt=head[v],e[cnt].lim=0,e[cnt].c=-c,head[v]=cnt;
}
int pre[maxn];
ll dis[maxn];
bool vis[maxn];
inline void SPFA(){
queue<int> q;
memset(dis,0x3f,sizeof(dis));
memset(vis,0,sizeof(vis));
dis[S]=0,vis[S]=1;
q.push(S);
while(!q.empty()){
int u=q.front();
vis[u]=0;
q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
ll c=e[i].c;
if(dis[u]+c<dis[v]&&e[i].lim){
pre[v]=i;
dis[v]=dis[u]+c;
if(vis[v]) continue;
vis[v]=1;
q.push(v);
}
}
}
}
inline pair<ll,ll> max_flow(){
ll flow=0,min_cost=0;
while(1){
SPFA();
if(dis[T]==maxxn) return make_pair(flow,min_cost);
ll mn=maxxn;
for(int u=T;u!=S;u=e[pre[u]^1].to) mn=min(mn,e[pre[u]].lim);
flow+=mn;
for(int u=T;u!=S;u=e[pre[u]^1].to){
min_cost+=e[pre[u]].c*mn;
e[pre[u]].lim-=mn,e[pre[u]^1].lim+=mn;
}
}
}
}G;
其实把 \(\text{Dinic}\) 的 \(\text{bfs}\) 改成 \(\text{SPFA}\) 也可以达到同样的效果,不过貌似不是正统做法。
费用流模型构建
大体与最大流相同。
-
拆点模型
与时间轴相关时,将每个时间点拆成两个点分别表示一天开始与结束。在此基础上建模,其中同一时间两点之间的流量为这一天消耗或使用的个数。
比较经典的模型:有一天结束向另一天开始连边表示可以继承顺延以及对应的花费;有限制流量需求的在一天结束与下一天开始连容量无限,费用为 \(0\) 的边,一天内部连容量为无限与需求之差,费用为 \(0\) 的边,接下来对限制连边,费用依题意,这样为了保证最大流为无限,就需要用对应的有费用边去补全一天内部减去的需求量;若一天内流量有严格下界限制“恰好”、“至少”等,在此基础上需要后面提到的上下界。
优化建图
动态建图
这样一道题:BZOJ-2879 [NOI2012]美食节
如果直接暴力连边跑的话,点数是 \(O(n+mp)\),边数是 \(O(nmp)\) 的,然而由于流量只有 \(p\),有很多边是无用的,而考虑到同一个厨师,相对更早建出的点一定更优,就需要使用动态建图。
具体是每次增广时,如果经过了某个厨师的最新节点时,说明我们需要下一步可能会增广更新的一个节点,这时再把这个节点建出来,于是点数可以优化到 \(O(n+m+p)\),边数可以优化到 \(O(n(m+p))\)。
上下界网络流
给流量一个下界的限制,即满足 \(b(u,v)\le f(u,v)\le c(u,v)\)。
无源汇上下界可行流
也就是在流量确定的情况下,可以无限地流。
与正常网络流不同的地方就在于有一个下界,先假定每个边的流量为下界,设 \(W_i=\sum_{(u,x)\in E} b(u,x)-\sum_{(x,u)\in E} b(x,u)\),若 \(W_i\) 为正,则需要多流入,反之则需要多流出。
建立超级源点 \(SS\),超级汇点 \(TT\),将 \(SS\) 与所有 \(W_i\) 为负的点相连,表示在以下界为流量情况下可以多流入 \(|W_i|\),同理将所有 \(W_i\) 为正的点相连,表示在以下界为流量情况下可以多流出 \(|W_i|\)。同时把原图的边调整成 \(c(u,v)-b(u,v)\)。
这样求出的最大流如果与 \(SS\) 连出边的容量总和相等,说明经过调整可以得到一条可行流。
有源汇上下界可行流
将 \(T\) 向 \(S\) 连一条 \([0,+\infty]\) 的边,转化成无源汇问题。
而这条边的流量也就是 \(S\to T\) 的流量,即要求的可行流。
有源汇上下界最大流
在可行流的基础上解决问题,此时由于我们已然保证下界,刚刚跑过最大流的图中,属于原图上边的流量加上其对应下界就构成了一种方案,在原图的残量网络上跑最大流即可。
这里有两种正确的写法,一是跑完第一次无源汇之后删去增加的边,答案为这条边的流量与残量网络上最大流的和;二是不删这条边,直接跑最大流,由于可行流答案是 \(T\to S\) 的流量,也就是反向边中 \(S\to T\) 的剩余容量,因此直接跑最大流也可以统计到这个答案。
有源汇上下界最小流
依旧是在可行流的残量网络上处理,保证最小就要退掉一些无用的,于是跑 \(T\to S\) 最大流,用可行流减去即可。
网络流24题
餐巾计划
考虑把每天分成旧毛巾和新毛巾,新毛巾代表的点向汇点有 \(r_i\) 的容量,通过最大流限制合法。
获取新毛巾有三种方法,购买:从原点向新毛巾位置连容量无限大费用为 \(p\) 的边、快洗:从第 \(i-m\) 天的旧毛巾向第 \(i\) 天连容量无限大费用为 \(f\) 的边、慢洗:与快洗同理,同时旧毛巾可以顺延到下一天,于是向下一天连边。
这里还需要限制一个问题:旧毛巾的实际来源除了上一天未使用的,还有当天用过的,而直接新毛巾连回来就无法满足最大流都流向汇点了,这里可以由源点向旧毛巾位置连边,容量为 \(r_i\),就限制了从当天获得的旧毛巾数量。
标签:容量,增广,int,flow,网络,笔记,最小,学习,dis From: https://www.cnblogs.com/SoyTony/p/Network_Flow_Algorithm_Learning_Notes.html