网络流学习笔记

网络流

1.关于网络的一些定义:

(1)网络:

网络(又称流网络 Flow Network)是指一个有向图 G=(V,E)

每条边(u,v)E都有个权值c(u,v),称之为容量(Capacity),(u,v)E,c(u,v)=0.
其中有两个特殊点:分别是源点(Source)sV和汇点(Sink) tV(st)(可以简单理解为起点和终点)

(2)流

f(u,v)是定义在二元组(uV,vV) 上的实数函数且满足一下3点

1). 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即f(u,v)<=c(u,v)(这个其实很好理解,比如你家下水道水流量只有一定阈值,超过这个阈值你就不能再多流了)

2).斜对称性:每条边流量与其相反边流量之和为零即f(u,v)+f(v,u)=0

3).流守恒性:从源点流出的流量等于汇点流入的流量,即xV{s,t},(u,x)Ef(u,x)=(x,v)Ef(x,v)

那么f成为网络G的流函数.对于(u,v)E,f(u,v)称为边的流量,c(u,v)f(u,v)称为边的剩余容量,整个网络流量为(s,v)Ef(s,v),即从源点发出的所有流量之和.
一般而言也可以把网络流理解为整个图的流量,流量均满足上述性质.

2.网络流的常见问题

1).最大流

我们有一张图,要求从源点向汇点的最大流量(可以有很多条路到达汇点),就是最大流问题.

2).最小费用最大流

最小费用最大流问题:每条边都有一个费用,代表单位流量经过这条边的开销.我们要在求出最大流的同时,要求花费费用最小.

3).最小割

割其实就是删边的意思,当然最小割是删掉X条边来让ST不相通.我们要求X条边加起来流量总和最小.这为最小割问题.

----------------------------------------------(分割线)

以下是算法部分

2.最大流

本页面主要介绍最大流问题相关的算法知识

(1)FordFulkerson增广

FordFulkerson增广式计算最大流的一类算法总称.该算法运用贪心思想,通过寻找增广路来更新并求解.

概述

网络GG上的流f定义见上.

我们将G中所有节点和剩余容量大于0的边构成的子图称为残量网络Gf(Residual Network),即Gf=(V,Ef),其中Ef=(u,v)|cf(u,v)>0.(这里要注意的是下文将会介绍到流量可能为负值,因此,Ef的边不一定在E中.)

我们将Gf上一条从源点s到回电t的路径称为增广路(AugmentingPath).对于一条增广路,我们给每一条边(u,v)都加上等量的流量,以令整个网络流量增加.这一过程称为增广(Augment).由此,最大流的求解可以被视为若干次增光分别得到流的叠加.值得注意的是,根据流的斜对称性,我们在增广式应该退流,即f(u,v)增加时f(v,u)要减少同等量.我们方便讨论,在{(u,v)是正向的}的前提下,我们称(v,u)为反向边.

Tips:在最大流算法的代码实现中,我们往往需要支持快速访问反向边的操作。在邻接矩阵中,这一操作是 trivial(琐碎的)的(gu,v<->gv,u)主流做法是用链式前向星.一个常用技巧:令边从偶数(通常是0)开始编号,并在加边时总是紧接着加入其反向边使他们编号相邻.由此,我们可以令编号为i的边和编号为i xor 1的边始终保持互为反向边的关系.

初次接触可能会遇到违反直觉的情形--反向边的流量可能是个负值.实际上我们可以注意到,在增广过程中真正有意义的是剩余容量cf,而f(v,u)的绝对值是不重要的,我们可以将它的减少视为cf(v,u)的增加.

假设G是一个单位容量的网络,考虑一下过程:

1).G上有多条增广路,其中,我们选择进行一次先后经过u,v的增广,流量增加1.

2).我们注意到,如果进行中图上的增广,这个局部最大流量不是1是2.但由于指向u的边和从v出发的边在第一次增广中耗尽了容量,此时我们无法进行中图上的增广.这意味着我们当前的流量是不够优的,但局部可能已经没有其它的增广路了.

3).现在引入退流操作,第一次增广后,退流意味着cfv,u增加了1剩余容量,即相当于新增(v,u)这条边,因此我们可以再进行一次先后经过p,v,u,q的增广。无向边(u,v)上的流量在两次增广中抵消,我们惊奇的发现这一结果实际上和中图等价的.

由上可知,退流带来的效果使我们无需担心我们按照顺序选择增广路.

容易发现,只要Gf上存在增广路,那么对其增广就可以令总流量增加;否则说明总流量一定达到最大可能值.在整数流量的网络G=(V,E)上,FordFulkerson增广时间复杂度上界:O(|E||f|),其中fG
上最大流.这是因为单轮增广时间复杂度为O(|E|),而增广会导致总流量增加,故增广轮数不可能超过|f|.

FordFulkerson增广的实现:

(1).EdmondsKarp算法

思想

· 如果在Gf上我们可以从s出发BFSt,则我们找到了新的增广路.

· 对于增广路p,我们计算出p经过的边的剩余容量的最小值Δ=min(u,v)pcf(u,v).我们给p上的每条边都加上Δ,令最大流增加Δ.

· 因为我们修改流量,所以我们得到形Gf,我们在新的Gf上重复上述过程,直至增广路不存在,流量不增加.
时间复杂度:O(|V||E|2)(一眼顶针,鉴定为废物算法,写费用流可以用)

code:


#define maxn 250
#define INF 0x3f3f3f3f

struct Edge {
  int from, to, cap, flow;

  Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};

struct EK {
  int n, m;             // n:点数,m:边数
  vector<Edge> edges;   // edges:所有边的集合
  vector<int> G[maxn];  // G:点 x -> x 的所有边在 edges 中的下标
  int a[maxn], p[maxn];  // a:点 x -> BFS 过程中最近接近点 x 的边给它的最大流
                         // p:点 x -> BFS 过程中最近接近点 x 的边

  void init(int n) {
    for (int i = 0; i < n; i++) G[i].clear();
    edges.clear();
  }

  void AddEdge(int from, int to, int cap) {
    edges.push_back(Edge(from, to, cap, 0));
    edges.push_back(Edge(to, from, 0, 0));
    m = edges.size();
    G[from].push_back(m - 2);
    G[to].push_back(m - 1);
  }

  int Maxflow(int s, int t) {
    int flow = 0;
    for (;;) {
      memset(a, 0, sizeof(a));
      queue<int> Q;
      Q.push(s);
      a[s] = INF;
      while (!Q.empty()) {
        int x = Q.front();
        Q.pop();
        for (int i = 0; i < G[x].size(); i++) {  // 遍历以 x 作为起点的边
          Edge& e = edges[G[x][i]];
          if (!a[e.to] && e.cap > e.flow) {
            p[e.to] = G[x][i];  // G[x][i] 是最近接近点 e.to 的边
            a[e.to] =
                min(a[x], e.cap - e.flow);  // 最近接近点 e.to 的边赋给它的流
            Q.push(e.to);
          }
        }
        if (a[t]) break;  // 如果汇点接受到了流,就退出 BFS
      }
      if (!a[t])
        break;  // 如果汇点没有接受到流,说明源点和汇点不在同一个连通分量上
      for (int u = t; u != s;
           u = edges[p[u]].from) {  // 通过 u 追寻 BFS 过程中 s -> t 的路径
        edges[p[u]].flow += a[t];      // 增加路径上边的 flow 值
        edges[p[u] ^ 1].flow -= a[t];  // 减小反向路径的 flow 值
      }
      flow += a[t];
    }
    return flow;
  }
};

(2).Dinic算法

思想

考虑在增广前先对GfBFS分层,即根据节点u的源点s的距离dist(u)把节点分成若干层.令经过u的流量只能流向下一层的节点v,即删除u想层数标号相等或者更小的节点的出边,我们成Gf剩下的部分为层次图(LevelGraph).形式化地,我们称GL=(V,EL)Gf=(V,Ef)的层次图,其中EL={(u,v)|(u,v)Ef,dist(u)+1=d(v)}.

如果我们在层次图GL上找到一个增广流fb,使得仅在GL上是不可能找出更大的增广流的,则我们称fbGL的阻塞流(BlockFlow).

注意:尽管上文中我们仅在单挑增广路上定义了增广/增广流,广义地, 增广 一词不仅可以用于单条路径上的增广流,也可以用于若干增广流的并--后者才是我们定义阻塞流时使用的意义.

定义层次图和阻塞流后,Dinic算法的流程如下.

1).在GfBFS出层次图GL.

2).在GL上DFS出阻塞流fb.

3).将fb并到原先的流f中,ff+fb

4).重复以上过程直到不存在从st的路径.

此时f即为最大流.

弧优化:

注意到GLDFS的过程中,如果结点u同事具有大量入边和出边,并且u每次接受来自入边的流量时都便利出边表来决定将流量传递给哪条出边,则u这个局部的时间复杂度最坏可达O(|E|2).为避免这一缺陷,如果某一时刻我们已经直到边(u,v)已经增广到极限(边(u,v)已经没有剩余容量或者v的后侧已增广至阻塞),则u的流量没有必要再尝试流向出边(u,v).根据这个缘故,对于每个结点u,我们维护u的出边表中第一条还有必要尝试的出边.习惯上,我们称维护这个指针为当前弧,我们成这个做法叫做当前弧优化.

多路增广

多路增广是Dinic算法的一个常数优化--如果我们在层次图上找到一条从st的增广路p,
则接下来我们未必需要重新从s出发找下一条增广路,而可能从p上最后一个仍有剩余容量的位置出发寻找一条岔路进行增广.考虑到其回溯形式的一致性,这一优化在DFS的代码实现中也是自然的.(多路增广不影响复杂度的常数优化).

实现:

#include <bits/stdc++.h>
using namespace std;
const long long inf=2005020600;
int n,m,s,t,u,v;
long long w,ans,dis[520010];
int tot=1,now[520010],head[520010]; 

struct node {
	int to,net;
	long long val;
} e[520010];

inline void add(int u,int v,long long w) {
	e[++tot].to=v;//如果使用e[++tot]这样形式来加边,请将tot初值赋为1
	e[tot].val=w;
	e[tot].net=head[u];
	head[u]=tot;
	
	e[++tot].to=u;
	e[tot].val=0;
	e[tot].net=head[v];
	head[v]=tot;
}

inline int bfs() {  //在残量网络中构造分层图 
	for(register int i=1;i<=n;i++) dis[i]=inf;
	queue<int> q;
	q.push(s);
	dis[s]=0;
	now[s]=head[s];
	while(!q.empty()) {
		int x=q.front();
		q.pop();
		for(register int i=head[x];i;i=e[i].net) {
			int v=e[i].to;
			if(e[i].val>0&&dis[v]==inf) {
				q.push(v);
				now[v]=head[v];
				dis[v]=dis[x]+1;
				if(v==t) return 1;
			}
		}
	}
	return 0;
}

inline int dfs(int x,long long sum) {  //sum是整条增广路对最大流的贡献
	if(x==t) return sum;
	long long k,res=0;  //k是当前最小的剩余容量 
	for(register int i=now[x];i&&sum;i=e[i].net) {
		now[x]=i;  //当前弧优化 
		int v=e[i].to;
		if(e[i].val>0&&(dis[v]==dis[x]+1)) {
			k=dfs(v,min(sum,e[i].val));
			if(k==0) dis[v]=inf;  //剪枝,去掉增广完毕的点 
			e[i].val-=k;
			e[i^1].val+=k;
			res+=k;  //res表示经过该点的所有流量和(相当于流出的总量) 
			sum-=k;  //sum表示经过该点的剩余流量 
		}
	}
	return res;
}

int main() {
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(register int i=1;i<=m;i++) {
		scanf("%d%d%lld",&u,&v,&w);
		add(u,v,w);
	}
	while(bfs()) {
		ans+=dfs(s,inf);  //流量守恒(流入=流出) 
	}
	printf("%lld",ans);
	return 0;
}

//code by @Eleven谦 

MPM算法

大家都不学那我就不写了(’∇’)シ┳━┳

ISAP

Step:

1).和Dinic算法一样,我们还是先跑BFS对图上的点进行分层,不过与Dinic略有不同,我们在反图上,从ts进行BFS.

2).执行完1).后,我们通过DFS找最短路.

3).增广过程和Dinic过程类似,我们只选择比当前点层数少1的点来增广(因为我们分层是从t->s,所以从s->t应该每次层数少1)

然后与Dinic不同,我们不会重新来跑一次BFS来对图上的点重新分层,而是在增广过程中就完成重分层过程.

具体说,设i号点的层数为depi,当我们结束再i号点的增广过程后,我们在遍历残量网络上i的所有出边,找到层最小的出点j,随后令di=dj+1.特别地,若残量网络上i无出边.则di=n.

容易发现,当ds>=n时,图上不存在增广路,此时可以终止算法.

Dinic类似,ISAP也存在 当前弧优化

ISAP还存在另一个优化,我们记录层数为i的点的数量numi,每当将一个点的层数从x更新到y的时候,更新num数组的值,若在更新后numx=0,意味着图上出现断层,无法找到增广路,此时直接终止算法(实现时直接将ds标记为n),此优化称为GAP

PushRelabel预流推进算法

该方法在求解过程中忽略流守恒性,并对每个节点更新信息,以求解最大流

通用的预流退进算法

首先我们介绍预流推进算法的主要思想,以及一个可行的暴力实现算法.
预流推进算法通过对单个节点的更新操作,直到没有节点需要更新来求解最大流.

算法过程维护的流函数不一定保持流守恒形,对于一个节点,我们允许进入节点的流超过流出节点的流,
超过的部分被称为节点u(uV{s,t})超额流 e(u):

e(u)=(x,u)Ef(x,u)(u,y)Ef(u,y)

e(u)>0称节点u,注意当我们提到溢出节点时,不包括st.

预留推进算法维护每个结点的高度h(u),冰洁规定溢出的节点u如果要推送超额流,
只能向高度小于u的结点推送;如果u没有相邻的高度小于u的结点,就修改u的高度(重贴标签)

高度函数

准确地说,预留推进维护以下的一个映射h:V>N:

· h(s)=|V|,h(t)=0

· (u,v)Ef,h(u)<=h(v)+1

h是残量网络Gf=(Vf,Ef)的高度函数

引理1:设Gf上的高度函数为h,对于任意两个节点u,vV如果h(u)>h(v)+1,则(u,v)不是Gf中的边.

算法只会在h(u)=h(v)+1的边执行推送

推送(Push)

适用条件:结点u溢出,且存在节点v((u,v)Ef,c(u,v)f(u,v)>0,h(v)=h(v)+1),则push操作使用于(u,v).

于是,我们尽可能将超额流从u推送到v,推送过程中我们只关心超额流c(u,v)f(u,v))的最小值,不关心v是否溢出.

如果(u,v)在推送玩之后满流,将其从残量网络中删除.

重贴标签(Relabel)

适用条件:如果节点u溢出,且(u,v)Ef,h(u)<=h(v),则relabel操作使用于u.

则将h(u)更新为min(u,v)Efh(v)+1即可.

初始化

(u,v)E,f(u,v)={c(u,v),u=s0,otherwise

uV,h(u)={|V|,u=s0,otherwise

e(u)=(x,u)Ef(x,u)(u,y)Ef(u,y)

上述将(s,v)E充满流,并将h(s)抬高,使得(s,v)Ef,因为h(s)>h(v)而且(s,v)毕竟满流,没必要留在残量网络中;上述还将e(s)初始化为(s,v)Ef(s,v)为相反数.

HLPP算法

最高标号预流推进算法(HighestLabelPreflowPush)在上述通用的预流推进算法是,都优先选择高度最高的溢出节点,其算法复杂度为O(n2m)

HLPP算法过程如下:

1.初始化(基于预留推进算法);

2.选择溢出结点中高度最高的结点u,并对它所有可以推送的边进行推送;

3.如果u仍然溢出,对他重新贴上标签,回到步骤2;

4.如果没有溢出的节点,算法结束.

BFS优化

HLPP的上界为O(n2m),但是在使用时卡的比较紧;我们可以再初始化高度时进行优化.具体地,我们初始化h(u)为dis(u,t);特别地,h(s)=n

BFS的同时我们顺便检查图的联通性,[排除误解情况.

GAP优化

HLPP推送的条件是h(u)=h(v)+1,而如果在算法的某一时刻,h(u)=t的节点个数为0,那么对于h(u)>t的节点就永远无法推送的到超额流到t,因此只能送回s,那么我们就在这时直接让他们的高度变成至少n+1,以尽快推送回s,减少重贴标签的操作.

我们可以使用N21个痛B,其中B[i]中记录所有当前高度为i的溢出节点.加入了以上提到的两种优化.并且只处理了高度小于n的溢出结点.

(持续更新中其实就是从oiwiki爬下来当笔记的,一个一个手敲非粘贴,代码是在学习过程中在题解中搞下来的代码)

3.最小割

概念

对于一个网络流图G=(V,E),其割的定义为一种带你的划分方式:将所有的点划分为S和T=V-S两个集合,其中源点sT,回电tT

割的容量

我们定义割(S,T)的容量c(S,T)表示所有从ST的边的容量之和,即c(S,T)=\limtsuS,vTc(u,v).

最小割

最小割就是求得一个割(S,T)使得割的容量c(S,T)最小.

最大流最小割定理

定理 :f(s,t)max=c(s,t)min

code(其实就是最大流的Dinic算法...):


#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>

const int N = 1e4 + 5, M = 2e5 + 5;
int n, m, s, t, tot = 1, lnk[N], ter[M], nxt[M], val[M], dep[N], cur[N];

void add(int u, int v, int w) {
  ter[++tot] = v, nxt[tot] = lnk[u], lnk[u] = tot, val[tot] = w;
}

void addedge(int u, int v, int w) { add(u, v, w), add(v, u, 0); }

int bfs(int s, int t) {
  memset(dep, 0, sizeof(dep));
  memcpy(cur, lnk, sizeof(lnk));
  std::queue<int> q;
  q.push(s), dep[s] = 1;
  while (!q.empty()) {
    int u = q.front();
    q.pop();
    for (int i = lnk[u]; i; i = nxt[i]) {
      int v = ter[i];
      if (val[i] && !dep[v]) q.push(v), dep[v] = dep[u] + 1;
    }
  }
  return dep[t];
}

int dfs(int u, int t, int flow) {
  if (u == t) return flow;
  int ans = 0;
  for (int &i = cur[u]; i && ans < flow; i = nxt[i]) {
    int v = ter[i];
    if (val[i] && dep[v] == dep[u] + 1) {
      int x = dfs(v, t, std::min(val[i], flow - ans));
      if (x) val[i] -= x, val[i ^ 1] += x, ans += x;
    }
  }
  if (ans < flow) dep[u] = -1;
  return ans;
}

int dinic(int s, int t) {
  int ans = 0;
  while (bfs(s, t)) {
    int x;
    while ((x = dfs(s, t, 1 << 30))) ans += x;
  }
  return ans;
}

int main() {
  scanf("%d%d%d%d", &n, &m, &s, &t);
  while (m--) {
    int u, v, w;
    scanf("%d%d%d", &u, &v, &w);
    addedge(u, v, w);
  }
  printf("%d\n", dinic(s, t));
  return 0;
}


ps:由于最大流=最小割,所以我们可以直接用Dinic来求最小割

求出所有S点集内的点,我们可以通过源点s开始DFS每次都走残量大于0的边.

割边数量

如果需要再最小割的前提喜爱最小化割边数量,那么先求出最小割,把没有满流的边改成.满流的边容量改成 1,重新跑一遍最小割就可以去出最小割边数量;如果没有最小割的前提,直接把所有边容量设成1,求一遍最小割即可.

问题模型1

有n个物品和两个集合A,B,如果一个物品没有放入A会花费ai,没有放入B会花费bi;还有若干个形如ui,vi,wi的限制条件,如果uivi同时不在一个集合会花费wi.每个物品必须且只能属于一个集合,求最小代价.

这时一个景点 二者选其一 的最小割题目.我们对于每个集合设置源点s和汇点t,第i个点由s连一条容量为ai的边,向t连一条容量为bi的边.对于限制条件u,v,w,我们在u,v之间连容量为w的双向边.

注意到当源点和汇点不相连的时候,代表这些点都选择其中一个集合.如果将连向st的边割开,表示不放在AB集合,如果吧物品之间的边割开,表示这两个物品不放在同一集合.

最小割即最小花费.

问题模型

最大权值闭合图,即给定一张有向图,每个点都有一个权值(可以为正或负或0),你需要选择一个权值和最大子图,使得子图中每个点的后继都在子图中.

做法:建立超级源点s和超级汇点t,若节点u权值为正,则su连一条有向边,边权即为该点点权;
若节点权值为负,则由ut连一条有向边,边权即为该点点权的相反数.原图上所有边权改为.跑网络最大流,将所有正权值之和减去最大流,即为答案.

费用流

给定一个网络G=(V,E),每条边除了有容量限制c(u,v),还有一个单位流量的费用w(u,v).

u,v的流量为f(u,v)时,需要花费f(u,v)·w(u,v)的费用.

w也满足斜对称性,即w(u,v)=w(v,u).

则该网络中总花费最小的最大流称为最小费用最大流,即在最大化(s,v)Ef(s,v)的前提下最小化(u,v)Ef(u,v)·w(u,v).

SSP算法

SSP(SuccessiveShortestPath)算法是一个贪心算法.他的思路是每次寻找单位费用最早的增广路进行增广,直到图上不存在增广路为止.

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

实现(基于Dinic算法):


#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>

const int N = 5e3 + 5, M = 1e5 + 5;
const int INF = 0x3f3f3f3f;
int n, m, tot = 1, lnk[N], cur[N], ter[M], nxt[M], cap[M], cost[M], dis[N], ret;
bool vis[N];

void add(int u, int v, int w, int c) {
  ter[++tot] = v, nxt[tot] = lnk[u], lnk[u] = tot, cap[tot] = w, cost[tot] = c;
}

void addedge(int u, int v, int w, int c) { add(u, v, w, c), add(v, u, 0, -c); }

bool spfa(int s, int t) {
  memset(dis, 0x3f, sizeof(dis));
  memcpy(cur, lnk, sizeof(lnk));
  std::queue<int> q;
  q.push(s), dis[s] = 0, vis[s] = 1;
  while (!q.empty()) {
    int u = q.front();
    q.pop(), vis[u] = 0;
    for (int i = lnk[u]; i; i = nxt[i]) {
      int v = ter[i];
      if (cap[i] && dis[v] > dis[u] + cost[i]) {
        dis[v] = dis[u] + cost[i];
        if (!vis[v]) q.push(v), vis[v] = 1;
      }
    }
  }
  return dis[t] != INF;
}

int dfs(int u, int t, int flow) {
  if (u == t) return flow;
  vis[u] = 1;
  int ans = 0;
  for (int &i = cur[u]; i && ans < flow; i = nxt[i]) {
    int v = ter[i];
    if (!vis[v] && cap[i] && dis[v] == dis[u] + cost[i]) {
      int x = dfs(v, t, std::min(cap[i], flow - ans));
      if (x) ret += x * cost[i], cap[i] -= x, cap[i ^ 1] += x, ans += x;
    }
  }
  vis[u] = 0;
  return ans;
}

int mcmf(int s, int t) {
  int ans = 0;
  while (spfa(s, t)) {
    int x;
    while ((x = dfs(s, t, INF))) ans += x;
  }
  return ans;
}

int main() {
  int s, t;
  scanf("%d%d%d%d", &n, &m, &s, &t);
  while (m--) {
    int u, v, w, c;
    scanf("%d%d%d%d", &u, &v, &w, &c);
    addedge(u, v, w, c);
  }
  int ans = mcmf(s, t);
  printf("%d %d\n", ans, ret);
  return 0;
}


posted @   a_sad_soul  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示