网络流学习笔记

0.相关概念(来自oi-wiki)

网络(network)是指一个特殊的有向图 \(G=(V,E)\),其与一般有向图的不同之处在于有容量和源汇点。

\(E\) 中的每条边 \((u, v)\) 都有一个被称为容量(capacity)的权值,记作$ c(u, v)$。当 \((u,v)\notin E\) 时,可以假定 \(c(u,v)=0\)

\(V\) 中有两个特殊的点:源点(source)\(s\) 和汇点(sink)\(t\)\(s \neq t\))。

对于网络 \(G=(V, E)\),流(flow)是一个从边集 \(E\) 到整数集或实数集的函数,其满足以下性质。

容量限制:对于每条边,流经该边的流量不得超过该边的容量,即 \(0 \leq f(u,v) \leq c(u,v)\)

流守恒性:除源汇点外,任意结点 \(u\) 的净流量为 \(0\)。其中,我们定义 \(u\) 的净流量为 \(f(u) = \sum_{x \in V} f(u, x) - \sum_{x \in V} f(x, u)\)

对于网络 \(G = (V, E)\) 和其上的流 \(f\),我们定义 f 的流量 \(|f|\) 为 s 的净流量 \(f(s)\)。作为流守恒性的推论,这也等于 t 的净流量的相反数 \(-f(t)\)

对于网络 \(G = (V, E)\),如果 \(\{S, T\}\) 是 V 的划分(即 \(S \cup T = V\)\(S \cap T = \varnothing\)),且满足 \(s \in S, t \in T\),则我们称 \(\{S, T\}\)\(G\) 的一个 \(s-t 割\)(cut)。我们定义 \(s-t\)\(\{S, T\}\) 的容量为 \(||S, T|| = \sum_{u \in S} \sum_{v \in T} c(u, v)\)

瞄一眼就行。

0.5.用途

网络流和 dfa 都是维护信息的工具,状态较为简单的时候 dfa 就能维护,但是当状态有难以通过自动机维护的约束的时候一般就用到了网络流。NOIP 阶段碰到需要维护状态的东西直接考虑 dfa 结构就行了,但是 NOIP+,包括 xcpc 比赛中,就需要根据网络流的常见模型进行建模,也就是相当于 dp 中的状态设计。

这样来说的话感觉网络流题还是挺困难的,主要是构造不是很明显。 ---- 2025/10/23

1.板子选讲

(1)网络最大流

板子题: Luogu P3376

口糊思路:先用 BFS 给整张图分层,判断 \(s\)\(t\) 是否连通。只要两者联通,就跑 DFS。 DFS 之前,在现有网络基础上建立残余网络。残余网络的定义是在任意时刻,网络中所有节点以及剩余容量大于 \(0\) 的边构成的子图.每次跑 DFS,就对当前动作做一次增广,所谓增广,可以理解为扩张。

如果理解不了上面的话,可以把网络流理解成水流,自己模拟一下。

代码附上:

inline bool bfs(int s,int t)
{
 for(int i=1;i<=n;i++)d[i]=-1;
 queue<int> q;
 q.push(s);d[s]=0;
 while(!q.empty())
 {
  int u=q.front();q.pop();
  for(int i=h[u];~i;i=e[i].nxt)
  {
   int v=e[i].v;
   if(d[v]==-1&&e[i].w)q.push(v),d[v]=d[u]+1;
  }
 }
 return d[t]!=-1;
}
int dfs(int u,int minf)
{
 if(!minf||u==t)return minf;
 int f,flow=0;
 for(int i=cur[u];~i;i=e[i].nxt)
 {
  cur[u]=i;int v=e[i].v;
  if(d[v]==d[u]+1&&(f=dfs(v,min(minf,e[i].w))))
  {
   minf-=f,flow+=f;
   e[i].w-=f,e[i^1].w+=f;
   if(minf==0)return flow;
  }
 }
 return flow;
}
inline int Dinic()
{
 MaxFlow=0;
 while(bfs(s,t))
 {
  for(int i=1;i<=n;i++)cur[i]=h[i];
  MaxFlow+=dfs(s,inf);
 }
 return MaxFlow;
}
signed main()
{
 memset(h,-1,sizeof(h));
 scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
 for(int i=1;i<=m;i++)
 {
  int u,v,w;
  scanf("%lld%lld%lld",&u,&v,&w);
  addedge(u,v,w), addedge(v,u,0);
 }
 printf("%lld\n",Dinic());
 return 0;
}

有了网络最大流,我们就可以通过建模的方式完成二分图最大匹配问题。具体建模如下:

构造两个虚点 \(s\)\(t\),把其中一部分的点和 \(s\) 连边,另一部分的点和 \(t\) 连边,让图中边的流量为1,从 \(s\)\(t\) 跑一边网络最大流,结果就是二分图最大匹配。

代码:

signed main(){
 memset(h,-1,sizeof(h));
 cin>>n>>m>>ed;
 t=n+m+1;
 for(int i=1;i<=n;i++)addedge(s,i,1),addedge(i,s,0);
 for(int i=1;i<=ed;i++)
 {
  int u,v;cin>>u>>v;
  if(u>n||v>m)continue;
  addedge(u,v+n,1),addedge(v+n,u,0);
 }
 for(int i=1+n;i<=m+n;i++)addedge(i,t,1),addedge(t,i,0);
 Dinic();
 cout<<MaxFlow<<"\n";
 return 0;
}

练习:飞行员配对方案问题

(2)最大流最小割定理

什么是割?对于一个网络流图 \(G=(V,E)\),其割的定义为一种 点的划分方式:将所有的点划分为 \(S\)\(T=V-S\) 两个集合,其中源点 \(s\in S\),汇点 \(t\in T\)。就是把整个图劈成 \(S,T\) 两半。定义割的容量为所有从 \(S\)\(T\) 的边的容量和。最小割就是所有割中,割的容量最小的一条割的容量。

最大流最小割定理就是说最小割就等于最大流。这个可以用木桶原理感性理解一下,这样,我们就可以直接跑一遍最大流,求出最小割。

(3)费用流

每个边除了流量还有一个费用,我们要求在最大流基础上的最小(或最大)费用。

1) SSP 算法

贪心的 SSP 算法,每次找单位费用最小的路径增广,直到图上找不到增广路为止。时间复杂度 \(O(nmf)\),实际上跑不满。实现只用将 bfs 改成 spfa 就行。

如果图上存在单位费用为负的圈,SSP 算法无法正确求出该网络的最小费用最大流。此时需要先使用消圈算法消去图上的负圈。因为 SSP 算法是正确的当且仅当不存在负圈。

SSP 算法的时间复杂度是 \(\Omega(nmf)\) 的,其中 \(f\) 是最大流,但是通过构造 \(m=n^2,f=2^{n/2}\) 的网络可以使时间复杂度成为 \(O(n^32^{n/2})\),所以 SSP 不是多项式时间复杂度的。

bool spfa()
{
 for(int i=1;i<=t;i++)dis[i]=INF,vis[i]=false;
 dis[s]=0,vis[s]=true;q.push(s);
 while(!q.empty())
 {
  int u=q.front();q.pop();vis[u]=false;
  for(int i=head[u];i;i=e[i].nxt)
  {
   int v=e[i].v;
   if(dis[v]>dis[u]+e[i].w&&e[i].c)
   {
    dis[v]=dis[u]+e[i].w;
    if(!vis[v])q.push(v),vis[v]=true;
   }
  }
 }
 return dis[t]!=INF;
}
int dfs(int u,int minf)
{
 if(u==t||!minf) return minf;
 int flow=0,f;
 vis[u]=true;
 for(int i=cur[u];i;i=e[i].nxt)
 {
  cur[u]=i;
  int v=e[i].v;
  if(!vis[v]&&e[i].c&&dis[v]==dis[u]+e[i].w)
  {
   f=dfs(v,min(minf,e[i].c));
   if(f)
   {
    e[i].c-=f,e[i^1].c+=f,minf-=f,flow+=f,ans+=f*e[i].w;
    if(!minf)return vis[u]=false,flow;
   }
  }
 }
 return vis[u]=false,flow;
}
void solve()
{
 ans=0;
 while(spfa())
 {
  for(int i=1;i<=t;i++)cur[i]=head[i];
  dfs(s,INF);
 }
}
2) Primal-Dual 原始对偶算法

首先你需要会 Johnson 算法。考虑如何通过转换边权的方式让这个东西可以 Dijkstra。先 spfa 求出每个点到源点的最短路 \(d_u\),将 \((u\rightarrow v,w)\) 的边权 \(w\leftarrow w+d_u-d_v\),这样根据 Johnson 的正确性便可以保证这样做是对的。一个问题是每次增广之后图的形态会发生变化。结论是假设新的最短路为 \(d'\),那么对于每个 \(i\)\(h_i\leftarrow h_i+d_i'\) 即可。正确性如下:考虑增广路上的一条边 \((i,j)\),一定有 \(d_i'+w(i,j)+h_i-h_j=d_j'\),变换得到 \(w(j,i)+(h_j+d_j')-(h_i+d_i')=0\),所以新增的边权非负;对于正常的一条边,加上之后肯定也非负,非负就有正确性。

bool dijkstra()
{
 priority_queue<mypair> q;
 for (int i = 1; i <= n; i++) dis[i] = INF;
 memset(vis, 0, sizeof(vis));
 dis[s] = 0, q.push(mypair(0, s));
 while (!q.empty())
 {
  int u = q.top().id;
  q.pop();
  if (vis[u]) continue;
  vis[u] = 1;
  for (int i = head[u]; i; i = e[i].next)
  {
   int v = e[i].v, nc = e[i].c + h[u] - h[v];
   if (e[i].f && dis[v] > dis[u] + nc)
   {
    dis[v] = dis[u] + nc, p[v].v = u, p[v].e = i;
    if (!vis[v]) q.push(mypair(dis[v], v));
   }
  }
 }
 return dis[t] != INF;
}
void spfa()
{
 queue<int> q;
 memset(h, 63, sizeof(h));
 h[s] = 0, vis[s] = 1, q.push(s);
 while (!q.empty())
 {
  int u = q.front();
  q.pop(),vis[u] = 0;
  for (int i = head[u]; i; i = e[i].next)
  {
   int v = e[i].v;
   if (e[i].f && h[v] > h[u] + e[i].c)
   {
    h[v] = h[u] + e[i].c;
    if (!vis[v]) vis[v] = 1, q.push(v);
   }
  }
 }
}
int main()
{
 scanf("%d%d%d%d", &n, &m, &s, &t);
 for (int i = 1; i <= m; i++)
 {
  int u, v, f, c;
  scanf("%d%d%d%d", &u, &v, &f, &c);
  addedge(u, v, f, c), addedge(v, u, 0, -c);
 }
 spfa();
 while (dijkstra())
 {
  int minf = INF;
  for (int i = 1; i <= n; i++) h[i] += dis[i];
  for (int i = t; i != s; i = p[i].v) minf = min(minf, e[p[i].e].f);
  for (int i = t; i != s; i = p[i].v) e[p[i].e].f -= minf, e[p[i].e ^ 1].f += minf;
  maxf += minf, minc += minf * h[t];
 }
 printf("%d %d\n", maxf, minc);
 return 0;
}

(4)上下界网络流

一般有限制 \(b(u,v)\leq f(u,v)\leq c(u,v)\)

参考资料:https://zhuanlan.zhihu.com/p/324507636

1)无源汇上下界可行流

给定无源汇流量网络 \(G\),问是否存在一种标定每条边流量的方式,让每条边流量满足上下界限制同时每个点流量平衡。

假设每条边 \((u,v)\) 均先流了 \(b(u,v)\) 的量,然后建新图,容量为 \(c(u,v)-b(u,v)\),假设一个点初始的入量和出量的差为 \(M\)。如果 \(M=0\) 就不用管本来就是平衡的,如果 \(M<0\) 意味着流出的过多了,建一个虚拟汇点 \(T\),连到 \(T\) 的流量为 \(-M\) 的新边;\(M>0\) 就新建虚拟源点 \(S\),连流量为 \(M\) 的新边。那么可行当且仅当附加边满流。

2)有源汇上下界可行流

给定有源汇流量网络 \(G\)。询问是否存在一种标定每条边流量的方式,使得每条边流量满足上下界同时除了源点和汇点每一个点流量平衡。

考虑加一条 \((T,S)\) 上界为 \(\infty\) 下界为 \(0\) 的边然后做无源汇上下界可行流。

3)有源汇上下界最大流

先用上面的方法找出一条可行流,记求无源汇上下界可行流中新建的虚拟源汇点为 \(S,T\),我们先找出一条可行流来,然后在残量网络上求最大流和可行流加起来就是对的。

4)有源汇上下界最小流

类似的,我们考虑将残量网络中不需要的流退掉。我们找到网络上的任意一个可行流。如果找不到解就可以直接结束。否则我们考虑删去所有附加边之后的残量网络。我们在残量网络上再跑一次 \(T\)\(S\) 的最大流,将可行流流量减去最大流流量即为答案。

2.网络流算法建模经典模型

(1)最大权闭合子图

首先讲一下定义。最大权闭合子图,就是一个带点权的图中,点权和最大的闭合子图。闭合子图的定义就是说,设这个子图的点集为 \(G\),对于任何 \(u \in G\)\(u\) 的所有出边所到达的点 \(v\) 都满足 \(v \in G\)。通法是将权为正的点连源点,权为负的连汇点,容量为权的绝对值,点与点之间连边,容量为 \(\inf\),然后跑最小割。

例题:太空计划飞行问题

建边:考虑抽象一下这个题。我们建个二分图,一边是实验,一边是仪器,然后连上边。我们就这样构造出了一个二分图。如果我们将实验点的点权设为正的,仪器点的点权设为负的,那我们就得到了一个最大权闭合子图问题。

我们有一种冲动,叫做建源点、汇点,源点连实验,汇点连仪器,边权分别是实验的收益、仪器的代价,然后实验和仪器根据关联连边,边权为 \(\inf\)。可以证明,如果这个时候我们搞到这个图的最小割,那这个割肯定不会割到中间边权为 \(\inf\) 的边,也就是说割完之后,与源点相连的点构成的子图就是最大权闭合子图。因此我们只需要跑一边最大流求出最小割,可知这个割上的边,要么代表着带来负收益的实验,要么代表着必须花的配仪器的钱,那么我们只用把所有实验的收益加在一起,减去最小割就是最大利润。至于方案,我们只需要最后跑一边 bfs,看哪些点是源点可以达到的,那就说明这个点肯定在方案里。完结撒花。

放建边的代码。

signed main()
{
 memset(head,-1,sizeof(head));
 scanf("%lld%lld",&m,&n);
 s=m+n+1,t=m+n+2;
 for(int i=1;i<=m;i++)
 {
  scanf("%lld",p+i);
  sum+=p[i];
  add(s,i,p[i]),add(i,s,0);
  char tools[10000];
  memset(tools,0,sizeof tools);
  cin.getline(tools,10000);
  int ulen=0,tool;
  while(sscanf(tools+ulen,"%d",&tool)==1)
  {
   add(i,tool+m,INF),add(tool+m,i,0);
   if(tool==0) ulen++;
   else while(tool)tool/=10,ulen++;
   ulen++;
  }
 }
 for(int i=1;i<=n;i++)
 {
  scanf("%lld",c+i);
  add(i+m,t,c[i]),add(t,i+m,0);
 }
 int ans=Dinic(); bfs(); 
 for(int i=1;i<=m;i++)if(d[i]!=-1)printf("%lld ",i);
 printf("\n");
 for(int i=m+1;i<=m+n;i++)if(d[i]!=-1)printf("%lld ",i-m);
 printf("\n%lld\n",sum-ans);
 return 0;
}

圆桌问题也是此类题,要简单一点。

(2)最小路径覆盖集

例题:最小路径覆盖问题

考虑最劣的情况,就是每个点只和自己连边,这样答案就是 \(n\),如果我们能将两个集合通过连边的方式合并的话,那答案就会 \(-1\)。考虑建图,把每个点拆开,建二分图,从二分图左侧向右侧连给的图中的边,源点连左边,右边连汇点,途中流量全设为 1。可以知道,这样跑出来的网络流,结果就是最大的合并次数,用 \(n\) 减去这个次数就是我们要求的答案。

对于输出路径,我们可以通过维护并查集来实现。

#define INF 0x7f7f7f7f
const int N=1005,M=6005;
int n,m,s,t,head[N],tot=-1,fa[N];bool vis[N];
int getf(int x){return fa[x]==x?x:fa[x]=getf(fa[x]);}
struct edge{int v,w,nxt;}e[M<<1];
void add(int u,int v,int w){}
int d[N],cur[N];
queue<int> q;
bool bfs(){}
int dfs(int u,int minf){}
int Dinic(){}
void ddfs(int u)
{
 if(u!=s&&u!=t)cout<<u<<" ";
 for(int i=head[u];~i;i=e[i].nxt)
 {
  int v=e[i].v;
  if(!e[i].w&&v>n&&v<s)ddfs(v-n);
 }
}
int main()
{
 ios::sync_with_stdio(false);
 cin.tie(0);cout.tie(0);
 memset(head,-1,sizeof(head));
 cin>>n>>m;s=2*n+1,t=2*n+2;
 for(int i=1;i<=n;i++)fa[i]=i,add(s,i,1),add(i,s,0);
 for(int i=1;i<=m;i++)
 {
  int u,v;cin>>u>>v;
  add(u,v+n,1),add(v+n,u,0);
 }
 for(int i=1;i<=n;i++)add(i+n,t,1),add(t,i+n,0);
 int ans=Dinic();
 for(int i=0;i<=tot;i++)
 {
  int u=e[i^1].v,v=e[i].v;
  if(u>=1&&u<=n&&v>n&&v<s&&!e[i].w)fa[getf(v-n)]=getf(u);
 }
 for(int i=1;i<=n;i++)
 {
  int x=getf(i);
  if(i==x){ddfs(x);cout<<"\n";}
 }
 cout<<n-ans<<"\n";
 return 0;
}
posted @ 2023-09-30 19:09  lhc0707  阅读(87)  评论(0)    收藏  举报