网络流 最大流 最小割 费用流

【腾讯文档】网络流初步

网络流初步


问题:

一、网络流简介

网络流是算法竞赛中的一个重要的模型,它有两个部分:网络
在这里插入图片描述

图片来源

1. 网络

网络就是一张有向图 G = (V,E)。(G是Graph、V是Vertex、E是Edge)

特别的,它拥有一个源点(Source)和一个汇点(Sink),在上图中,1是源点,3是汇点。

有向图中的边权称为容量(Capacity)用C(u,v)表示,在上图中1->2的容量表示为C(1,2) = 3。

特别的,当两点之间没有边相连时,有C(u,v) = 0。

2. 流

,就像是水流。如果把网络想象成一个自来水管道网络,那流就是其中流动的水。每条边上的流不能超过它的容量,并且对于除了源点和汇点外的所有点(即中继点),流入的流量都等于流出的流量。

设f = (u,v),其中u和v均属于V,u和v均是V集合中的元素,流满足下面三条性质

  1. 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即
    对 任 意 u , v ∈ V , f ( u , v ) ≤ c ( u , v ) 对任意u,v∈V,f(u,v) ≤c(u,v) u,vVf(u,v)c(u,v)

  2. 斜对称性:每条边的流量与其相反边的流量之和为 0,即
    对 任 意 u , v ∈ V , f ( u , v ) = − f ( v , u ) 对任意u,v∈V,f(u,v) = -f(v,u) u,vVf(u,v)=f(v,u)

  3. 流守恒性:除了源点S汇点T外,从源点流出的流量等于汇点流入的流量

∑ f ( u , v ) = 0 , ( u , v ) ∈ E ∑f(u,v)=0,(u,v)∈E f(u,v)=0(u,v)E

那么f称为网络G的流函数,f(u,v)称为边的流量,C(u,v) - f(u,v) 称为边的剩余容量,整个网络的流量为从源点发出的所有流量之和
在这里插入图片描述

图片来源

3. 再次理解网络流

网络是一张带权有向图。我们把它比喻成“自来水管道”,

源点S是大水库,想输出多少就输出多少。

但是,想要输出到目的地也就是汇点T,需要经过中继点。中继点不产生新流量、也不私吞;接受多少流量,就同时输出多少流量

点与点之间管道的容量是有限的;一条边上的流量不能超出其容量,容量很可能是有残余的。因为这些边上的限制,源头并不能无限输出。

二、常见题型(三种)

  • 最大流【模板】网络最大流

    • 有一张图,要求从源点流向汇点的最大流量(可以有很多条路到达汇点)。
    • FF算法、EK增广路算法、Dinic算法、ISAP算法、HLPP算法。
  • 最小费用最大流【模板】最小费用最大流

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

    • 割其实就是删边的意思,当然最小割就是割掉X条边来让S跟T不互通。我们要求X条边加起来的流量总和最小。这就是最小割问题。

三、相关问题对应算法介绍

1.最大流

[问题概述] 网络最大流

给出一个网络图,以及其源点和汇点,求出其网络最大流。

请添加图片描述

(1) FF算法 - Ford-Fulkerson算法

该算法是不断用dfs寻找增广路,是一个暴力的算法,效率是所有相关算法里最差的,要求理解,因为后续的算法都是在它上面改进升级的。

(此部分参考链接,只做了些修改)FF算法讲解-知乎

FF算法核心在于寻找增广路(Augmenting Path)来更新最大流。

何谓增广路?例如下图中我首先选择1->2->3,这是一条增广路,提供2流量;

然后我们相应地扣除选择路径上各边的容量,1->2的容量变成1,2->3的容量变成0,这时的容量称为残余容量

然后我们再找到1->2->4->3这条路径,按残余容量计算流量,它提供1流量(选择这两条路的顺序可以颠倒)。1->2->4->3也是一条增广路。

增广路,是从源点到汇点的路径,其上所有边的残余容量均大于0。FF算法就是不断寻找增广路,直到找不到为止。这个算法一定是正确的吗?好像不一定吧,比如这张图……

请添加图片描述

如果我们首先找到了1->2->3->4这条边,那么残余网络会变成这样:

请添加图片描述

现在已经找不到任何增广路了,最终求得最大流是1。但是,很明显,如果我们分别走1->3->4和1->2->4,是可以得到2的最大流的。

为了解决这个问题,我们引入反向边。在建边的同时,在反方向建一条边权为0的边:

请添加图片描述

我们仍然选择1->2->3->4,但在扣除正向边的容量时,反向边要加上等量的容量。

请添加图片描述

这时我们可以另外找到一条增广路:1->3->2->4。

请添加图片描述

其实可以把反向边理解成一种撤销,走反向边就意味着撤回上次流经正向边的若干流量,这也合理解释了为什么扣除正向边容量时要给反向边加上相应的容量:反向边的容量意味着可以撤回的量。

加入了反向边这种反悔机制后,我们就可以保证,当找不到增广路的时候,流到汇点的流量就是最大流。

算法模板

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 10010, E = 200010;
int n, m, s, t;
ll head[N];
ll to[E], nxt[E], val[E];
int tot = 1;//反向边操作,起始为1
bool vis[N];//限制增广路不要重复走点,否则很容易爆栈 
void addE(int u, int v, ll w) {
	to[++tot] = v, val[tot] = w;//真实数据
	nxt[tot] = head[u], head[u] = tot; //在表头x处插入
}
ll dfs(int u, ll flow) 
{//注意,在走到汇点之前,无法得知这次的流量到底有多少 
	if (u == t)//走到汇点才return一个实实在在的流量 
		return flow;
	vis[u] = true;
	for (int i = head[u]; i; i = nxt[i]) 
    {
		int v = to[i];
		if (val[i] == 0 || vis[v])//无残量,走了也没用 
			continue;
		int res = 0;
		if ((res = dfs(v, min(flow, val[i]))) > 0) 
        {//顺着流过去,要受一路上最小容量的限制
			val[i] -= res;//此边残余容量减小
			val[i ^ 1] += res;//以后可以顺着反向边收回这些容量,前提是对方有人了 
			return res;
		}
	}
	return 0;//我与终点根本不连通
}
int main() 
{
	scanf("%d%d%d%d", &n, &m, &s, &t);
	while(m--)
    {
		int u, v; ll w;
		scanf("%d%d%lld", &u, &v, &w);
		addE(u, v, w);
		addE(v, u, 0);//反向边初始化为0
	}
	ll res = 0, maxflow = 0;
	while (memset(vis, 0, sizeof(vis)) && (res = dfs(s, 1e18/*水库无限*/)) > 0)
		maxflow += res;//进行若干回合的增广
	printf("%lld\n", maxflow);
	return 0;
}
(2)EK算法 - Edmonds-Karp增广路算法

该算法是不断用BFS寻找增广路,直到网络上不存在增广路为止。

在每轮寻找增广路的过程中,EK算法只考虑图中所有 f(x,y)<c(x,y)的边,用BFS找到任意一条从S到T的路径,同时计算出路径上各边的剩余容量的最小值 minf(最小流量),则网络的流量就可以增加 minf。需要注意的是,当一条边的流量f(x,y)>0时,根据斜对称性质,它的反向边流量f(y,x)<0,此时必定有f(y,x)<c(y,x)。故 EK算法在 BFS 时除了原图的边集 E 之外,还应该考虑遍历 E 中每条边的反向边(这里与FF算法是一样的,就不再赘述)。

算法模板

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 505, M = 2*5005;//由于有反向边的存在我们要开2倍的边
int head[M];
int to[M], nxt[M], pre[M];// 记录前驱,便于找到最长路的实际方案
ll val[M], incf[M];//incf是增广路上各边最小剩余容量
int tot = 1;
int n, m, s, t;
ll maxflow;
bool vis[N];//限制增广路不要重复走点
void addE(int u, int v, ll w){//两种写法都是可以的
    to[++tot] = v, val[tot] = w, nxt[tot] = head[u], head[u] = tot;//正向边
    //to[++tot] = u, val[tot] = 0, nxt[tot] = head[v], head[v] = tot;//反向边
}
bool bfs()
{
    memset(vis,false,sizeof(vis));
    queue<int> q;
    q.push(s);
    vis[s] = true;
    incf[s] = 1e18;//增广路上各边最小剩余容量
    while(!q.empty())
    {
        int x = q.front(); q.pop();
        for(int i = head[x]; i; i = nxt[i])
            if(val[i])
            {
                int v = to[i];
                if(vis[v]) continue;//已经访问过了
                incf[v] = min(incf[x], val[i]);
                pre[v] = i;
                q.push(v);
                vis[v] = true;
                if(v == t) return true;
            }
    }
    return false;
}
void updata()//更新增广路及其反向边的剩余容量
{
    ll x = t;
    while(x != s)
    {
        ll i = pre[x];
        val[i] -= incf[t];
        val[i ^ 1] += incf[t]; //方向边也要更新 xor 1 的技巧
        x = to[i ^ 1];
    }
    maxflow += incf[t];
}
int main()
{
    scanf("%d%d%d%d", &n, &m, &s, &t);
    memset(head, 0, sizeof(head));
    while(m--)
    {
        int u, v; ll w;
        scanf("%d%d%lld", &u, &v, &w);
        addE(u, v, w); 
        addE(v, u, 0);
    }
    maxflow = 0;
    while(bfs()) updata();
    printf("%lld\n", maxflow);
    return 0;
}
(3)Dinic算法

Dinic算法是对EK算法的优化,且比EK算法好敲。 Dinic算法可以解决99%的网络流算法问题,剩下的1%可以由ISAP算法或HLPP算法解决。(还解决不了可以解决出题人)

EK算法的问题:观察EF算法,它可能每次遍历整个残量网络却只找到1条增广路。

能不能一次多找几条增广路?于是Dinic算法就出现了,它的核心就是一次寻找多条增广路。

  • 分层图&DFS

根据BFS宽度优先搜索,我们知道对于一个节点x,我们用d[x]来表示它的层次,即S到x最少需要经过的边数。在残量网络中,满足d[y]=d[x]+1的边(x,y)构成的子图被称为分层图。而分层图很明显是一张有向无环图。

Dinic算法不断重复一下步骤以下步骤,直到残量网络中S不能到达T:

  1. 在残量网络上BFS求出节点的层次,构造分层图。
  2. 在分层图上DFS寻找增广路,在回溯时同时更新剩余容量(边权)。

所谓分层就是预处理出源点到每个点的距离。我们只往层数高的方向增广,可以保证不走回头路也不绕圈子。

给出一张图:

请添加图片描述

接着BFS分层:

请添加图片描述

DFS求增广路:

请添加图片描述

我们先观察一下,在残量网络上第一次DFS的时候,我们找到了1->3->7这一条增广路,贡献的流为2,之后我们更新残量网络(绿色的标记);

之后我们寻找第二条增广路,我们先找到了1->3->7,随后发现了3->7这条路上的剩余容量为0,于是接着回溯寻找到了1->2->7这一条增广路,贡献的流为3,之后我们更新残量网络;

之后我们寻找第三条增广路,我们先找到了1->3->7,随后发现了3->7这条路上的剩余容量为0,于是接着回溯寻找到了1->2->7这一条增广路,随后发现了2->7这条路上的剩余容量为0,于是回溯寻找到了1->5->7,贡献为5,之后我们更新残量网络;

之后我们寻找第四条增广路,我们先找到了1->3->7,随后发现了3->7这条路上的剩余容量为0,于是接着回溯寻找到了1->2->7这一条增广路,随后发现了2->7这条路上的剩余容量为0,于是回溯寻找到了1->5->7,随后发现了1->5这条路上的剩余容量为0,于是接着回溯找到了1->6->8->7这条路(非增广路),但是由于点8是在分层图上的第三层,与汇点属于同一层,不满足条件,随后退出。

以上是第一次BFS分层后,寻找多条增广路的模拟过程。

我们可以发现,在DFS的过程中,已经寻找到增广路的那条路径没有任何的利用价值,并且还会对后续的DFS造成影响,于是我们需要优化它,这就是当前弧优化。

  • 当前弧优化

有人将边叫做弧,具体是谁不知道,对于边的优化也叫弧优化。

对于一个节点u,当它在DFS中走到了第i条弧时,前i-1条弧到汇点的流一定已经被流满而没有可行的路线了。

那么当下一次再访问u节点时,前i-1条弧就没有任何意义了。

所以我们可以在每次枚举节点x所连的弧时,改变枚举的起点,这样就可以删除起点以前的所有弧,来达到优化剪枝的效果

对应到代码中,就是now数组。

或者说如果一条边已经被增广过,那么它就没有可能被增广第二次。那么,我们下一次进行增广的时候,就可以不必再走那些已经被增广过的边。

now数组就是做head数组不能做的事情,也可以说它是Dinic算法的精髓所在。

当然,在DFS的过程中我们还有优化的地方,详细见代码。

算法模板

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int M = 5005*2;
int head[M], to[M], nxt[M], d[M], now[M]/*当前弧优化*/;
ll val[M], flow, maxflow;
int tot = 1, n, m, s, t;
void addE(int u, int v, ll w){
    to[++tot] = v, val[tot] = w, nxt[tot] = head[u], head[u] = tot;
    to[++tot] = u, val[tot] = 0, nxt[tot] = head[v], head[v] = tot; 
}
bool bfs()//在残量网络上构造分层图,返回是否搜索到了汇点
{
    memset(d,0,sizeof(d));//初始化分层
    queue<int> q;
    q.push(s);
    d[s] = 1; // 源点是第一层
    now[s] = head[s];//当前弧优化
    while(q.size())
    {
        int x = q.front();q.pop();
        for(int i=head[x]; i; i=nxt[i])
        {
            int p = to[i];
            if(val[i] && !d[p])//当前的剩余容量不为0 && 该节点是没有分层的,也就是新的节点
            {
                q.push(p);
                now[p] = head[p];//继承head的信息
                d[p] = d[x] + 1;//更新分层图
                if (p == t) return true;
            }
        }
    }
    return false;
}
int dfs(int u, ll flow)//先BFS分层,再DFS寻找增广路, 找一条链 S -> T
{//flow是u收到的流量,不一定可以全部都用掉
    if(u == t) return flow;
    ll rest = flow, k, i;//k是v真正输出到汇点的流量
    for(i=now[u];i && rest;i=nxt[i])
    {
        if(val[i] && d[to[i]] == d[u]+1)//仅允许流向下一层
        {
            k = dfs(to[i], min(rest, val[i]));//受到一路上最小流量的限制
            if(!k) d[to[i]] = 0;//剪枝,去掉增广完毕的点
            val[i] -= k;
            val[i ^ 1] += k;
            rest -= k;
        }        
    }
    now[u] = i;//当前弧优化,避免重复遍历从u出发不可扩展的边
    return flow - rest;
}
void dinic()
{
    ll flow = 0;
    while(bfs())//BFS分层
        while(flow = dfs(s, 1e18))//DFS寻找增广路
            maxflow += flow;    
}
int main()
{
    scanf("%d%d%d%d", &n ,&m, &s, &t);
    while(m--)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        addE(u, v, w);
    }
    dinic();
    cout << maxflow << endl; 
    return 0;
}
(4)ISAP算法

ISAP是对Dinic的一个小改进,两者思路大体一致。ISAP是增广路算法中最快的一种。

Dinic算法的问题:每次DFS完后需要重新再BFS来构成分层图。

可不可以边DFS边修改点的层数?于是ISAP就出现了,它只在一开始bfs一次,之后对点层次的修改均在dfs中进行。

大体步骤如下:

  1. 从t(汇点)到s(源点)跑BFS

  2. 从s到t跑DFS

  3. 重复操作2直到出现断层(下面给出解释)

ISAP只跑一遍 BFS标记深度(是从汇点开始的!!!),然后每个点都会随着一次次 DFS而变高。

这样我们需要引进 gap数组, gap[i]表示高度为 i的点的个数。显而易见,当 gap[i]=0时会出现断层,也就是 s和 t不再联通,我们也就可以直接退出程序,停止寻找。

我们从终点向起点跑完BFS得到最初的高度。但是我们发现,此时还剩下一些流,那么我们将其高度提高,下一次遍历时,就可以把这个流推给其他边。

算法模板

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int M = 5005*2;
int tot = 1, head[M], cur[M], to[M], nxt[M], dep[M], gap[M];
ll val[M];
int n, m, s, t;
void addE(int u, int v, ll w){
    to[++tot] = v, val[tot] = w, nxt[tot] = head[u], head[u] = tot;
    to[++tot] = u, val[tot] = 0, nxt[tot] = head[v], head[v] = tot; 
}
void bfs()//倒着搜索
{
    queue<int> q; 
    q.push(t);//汇点t点入栈 
    for(int i=1;i<=tot;i++) dep[i] = -1;//把深度变为-1(0会导致gap崩坏) 
    for(int i=1;i<=tot;i++) gap[i] = 0;
    dep[t] = 0; //汇点深度为0 
    gap[0] = 1;//深度为0的点有1个 
    while (!q.empty())
    {
        int front = q.front(); 
        q.pop();
        for (int i = head[front]; i != 0; i = nxt[i])
        {
            if (val[i] == 0 && dep[to[i]] == -1)//dep[to[i]]==-1相当于to[i]点没有搜索过 
            {
                dep[to[i]] = dep[front] + 1;
                ++(gap[dep[to[i]]]);
                q.push(to[i]);
            }
        }//直到所有点都被遍历过 
    }
    return;
}//从t到s跑一遍bfs,标记深度

ll dfs(int p = s, ll flow = 1e18)
{
    if (p == t) return flow;
    ll used = 0;
    for (int i = cur[p]; i != 0; i = nxt[i])
    {
        cur[p] = i;
        ll  v = to[i];
        if (dep[v] + 1 == dep[p] && val[i] > 0)//注意这里的条件
        {//如果这条边的残量大于0,且没有断层
            int k = dfs(v, min(val[i], flow - used));
            used += k;
            val[i] -= k;
            val[i ^ 1] += k;
            if (used == flow) return flow;
        }
    }
    //如果已经到了这里,说明该点出去的所有点都已经流过了
    //并且从前面点传过来的流量还有剩余
    //则此时,要对该点更改dep
    //使得该点与该点出去的点分隔开
    if (--gap[dep[p]] == 0) dep[s] = n;//出现断层,无法到达t了
    ++dep[p];//层++ 
    ++gap[dep[p]];//层数对应个数++
    return used;
}

ll isap()
{
    bfs();//一次BFS构建分层图
    ll max_flow = 0;
    while (dep[s] < n)
    {
        for(int i=1;i<=tot;i++) cur[i] = head[i];
        max_flow += dfs();
    }
    return max_flow;
}

int main()
{
    scanf("%d%d%d%d", &n, &m, &s, &t);
    while(m--)
    {
        int u, v; ll w;
        scanf("%d%d%lld", &u, &v, &w);
        addE(u, v, w);
    }
    printf("%lld", isap());
    return 0;
}

总结
  1. FF算法是不断用DFS找增广路,效率低下,用来了解什么是增广路。
  2. EK算法是不断用BFS找增广路, 时间复杂度为O(nm^2),可以处理1e13–1e14规模的网络。
  3. Dinic算法是是用BFS在残量网络上构造分层图,之后再DFS寻找增广路。属于主流解题算法。
  4. ISAP算法是用BFS倒着跑一遍标记深度,最后DFS边找增广路,边修改深度。属于主流解题算法。

2.最小割

给定一张无向图,求最少去掉多少个边,可以使图不连通。N<=50。

这种是比较直接的问法,但通常情况下,比赛不会问的这么直接。

最大流最小割定理

任何一个网络的最大流量等于最小割中边的容量之和,简记为“最大流 = 最小割”。

简证:假设“最小割 < 最大流”,那么割去这些边之后,因为网络流量尚未最大化,所以仍然可以找到一条S到T的增广路,与 S,T 不连通矛盾,故“最小割≥ 最大流”。如果我们能给出一个“最小割 = 最大流”的构造方案,即可得到上述定理。

求出最大流后,从源点开始沿残量网络 BFS,标记能够到达的点。E 中所有连接“已标记点 x”和“未标记点 y”的边(x,y)构成该网络的最小割。

以上是直接给出的结论,但作为一名ACMer,我们需要知道它是怎么来的,这样面对变种的题型我们才能从源头设计解决问题。

(以下过程重点参考了南京大学蒋炎岩教授的讲解)

  • 给定一张有向图G(V,E),s, t∈V
    • 问是否存在一条s —>t的路径?

对于这个问题,相信大家用脚写BFS/DFS就可以解决。但是,你并没有证明这个过程,谁告诉你BFS/DFS就是正确的?(当然大家主观判断上是对的,但我们需要严谨一点)

  • 能否针对G(V,E),s, t∈V给出一个“证明”?
    请添加图片描述

在上图中V = {s, t, a, b, c, d}

我们可以很直观的感受到存在路径使得s—>t

接下来我们将b–>c和c–>t的方向反过来

请添加图片描述

那么这个时候我们可以直观的感受到是不存在一条s–>t的路径

我们可以列出以下路径(部分)

s–>t

s–>a–>t s–>b–>t s–>d–>t s–>c–>t

s–>a–>b–>t s–>d–>b–>t s–>a–>c–>t

s–>a–>b–>c–>t s–>d–>b–>c–>t

由于没有s–>t的边,在图上也没有找到s–>a–>t、s–>b–>t、s–>d–>t、s–>c–>t、s–>a–>b–>t s–>d–>b–>t s–>a–>c–>t、s–>a–>b–>c–>t、s–>d–>b–>c–>t等边,用枚举法可以证明不存在s–>t的路径。

以上证明虽然是正确的,但当点的数量n到达一定程度的时候,那么所列举的边就会到达一个很恐怖的数,对于证明并不友好,所以我们需要另选他法。

我们思考一下BFS/DFS是如何找不到路径的。

这两个算法会找到所有s可以到达的节点

  • 我们将s点可以到达的点写成一个集合S
  • 将不能到达的点写成一个集合T

假设BFS/DFS正确,一定不存在S到T的边

请添加图片描述

这里我们就可以引入一个重要的概念:割

割(CUT)

定义:有向图G(V,E)的s-t割

  • 两个集合S,T满足
    • S ∪ T = V, S ∩ T = Ø
    • s∈S, t∈T
    • 割的大小:{(u,v) ∈ E| u∈S,v∈T}
      • 就是S–>T方向穿过割的边数

通俗解释

  • 我们将节点分为两个部分,一个是包含了点s的S集合,一个是包含了点t的T集合。
  • 割就是对这些节点任意的划分成了两个部分(包含了以上的设定)。
  • 割的大小就是从S集合穿过割到达T集合的边数。

下面举几个例子帮助大家理解

请添加图片描述
在这里插入图片描述

我们可以理解为:当BFS/DFS“找不到路径”的时候会找到一个大小为0的割。

我们可以得出一个结论:

任何一个大小为0的s-t割(S,T)都是“找不到路径”的证明。

请添加图片描述

那么对于相比于枚举法纳法,当我们发现了一个大小为0的割,我们就可以断定s–>t是不存在路径的。

接下来我们加入割的概念走一遍FF算法,帮助大家理解记忆

请添加图片描述

我们走最坏的情况,蓝色边为走完之后形成的反向边

请添加图片描述

绿色为我们第二次走完之后形成的反向边

请添加图片描述

之后我们观察一下结果

  • 我们找到了两条不相交的路径s–>a–>b–>c–>t和s–>d–>b–>e–>t

  • 出现了大小为0的割

    所以我们在则张残量网络上找不到其它的增广路了,算法结束

请添加图片描述

当割的大小为0的时候,我们再也找不到一条增广路进行增广。

当最小割的大小为1的时候,我们可以找到一条增广路进行增广。

当最小割的大小为n的时候,我们可以找到n条增广路进行增广。

我们回到一开始的两张图中,在第一张图中,我们找到的最小割为1,则我们可以找到一条s–>t的路径

请添加图片描述

在这张图中,我们可以找到的最小割的大小为2,当然也可以找到大小为3的割,但最终能找到的不相交路径就是由最小割决定的2

请添加图片描述

  • dfs或bfs在残留网络上找到一个大小为0的割, 一定对应了原图上恰好等于路径数量的一个割
  • 割是不相交路径的上界
  • 用割去逼近最大不相交路径的数量
  • 我们希望求出所有st割中最小的那个
  • 我们求的是所有割中最小的那个
  • 最小的那个割就等于最多数量的那个路径

总的来说就是先利用反向边、增广路径求出一个不相交路径数m(下界),然后通过反证法证得最终残留网络会对应一个原图中大小为m的割(上界)。上界等于下界,m就是最优解。

这就是最大流最小割定理

最小割的问题可以转化为最大流的算法,这里就不在提供了。

3.费用流

在原有的网络的基础上,除了容量外,还有一个属性:单位费用。一条边上的费用等于流量×单位费用。我们知道,网络最大流往往可以用多种不同的方式达到,所以现在要求:在保持流最大的同时,找到总费用最少/最多的一种,费用流全名叫做最小费用最大流,一般是找最小的那个。

如下图,有很多种方法可以求到最大流3,

  • S–>a–>T (2) + S–>a–>b–>T (1)这种流法的费用是7×2+5×1=19
  • S–>a–>T (2) + S–>c–>b–>T (1)这种流法的费用则是7×2+4×1=18
  • S–>a–>T (2) + S–>b–>T (1) 这种流法的费用则是7×2+6×1=20
    • 第二种最小,第三种最大。事实上,第二种正是这个网络的最小费用最大流,第三种正是这个网络的最大费用最大流。

请添加图片描述

这个问题好解决,我们已经知道,只要建了反向边,无论增广的顺序是怎样,都能求出最大流。所以我们只需要每次都增广费用最少的一条路径即可。具体地,把Dinic算法里的BFS换成SPFA,EK算法里的BFS换成SPFA。

为什么是SPFA而不是Dijskra,因为Dijskra解决不了负边问题。

SFAP就是Dinic算法里的BFS换成SPFA。

算法模板

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<math.h>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
const int maxn = 50050;
const int inf = 0x3f3f3f3f;
int n, m, s, t, tot = 1;
int maxflow, mincost;
int dis[maxn], head[maxn], incf[maxn], pre[maxn];//dis表示最短路,incf表示当前增广路上最小流量,pre表示前驱
bool vis[maxn];
struct Edge {
	int nxt, to, dis, flow;
}edge[maxn * 2];
void addE(int u, int v, int w, int dis) {
	edge[++tot].nxt = head[u];edge[tot].to = v;
    edge[tot].dis = dis;edge[tot].flow = w;head[u] = tot;
}
bool spfa() 
{
	queue <int> q;
	for(int i=0;i<maxn;i++) dis[i] = inf;
	for(int i=0;i<maxn;i++) vis[i] = 0;
	q.push(s);
	dis[s] = 0;
	vis[s] = 1;
	incf[s] = 1 << 30;
	while(!q.empty()) 
    {
		int u = q.front();q.pop();
		vis[u] = 0;
		for(int i = head[u]; i; i = edge[i].nxt) 
        {
			if(!edge[i].flow) continue;//没有剩余流量
			int v = edge[i].to;
			if(dis[v] > dis[u] + edge[i].dis) 
            {
				dis[v] = dis[u] + edge[i].dis;
				incf[v] = min(incf[u], edge[i].flow);//更新incf
				pre[v] = i;
				if(!vis[v]) vis[v] = 1, q.push(v);
			}
		}
	}
	if(dis[t] == inf) return 0;
	return 1;
}
void MCMF() 
{
	while(spfa()) {//如果有增广路
		int x = t;
		maxflow += incf[t];
		mincost += dis[t] * incf[t];
		int i;
		while(x != s) {//遍历这条增广路,正向边减流反向边加流
			i = pre[x];
			edge[i].flow -= incf[t];
			edge[i^1].flow += incf[t];
			x = edge[i^1].to;
		}
	}
}
int main() 
{
	scanf("%d%d%d%d", &n,&m,&s,&t);
	while(m--)
    {
        int u, v, w, x;
		scanf("%d%d%d%d",&u,&v,&w,&x);
		addE(u,v,w,x);
		addE(v,u,0,-x);//反向边费用为-f[i]
	}
	MCMF();//最小费用最大流
	printf("%d %d\n",maxflow,mincost);
	return 0;
}

[参考来源]

  1. 网络流初步-知乎
  2. 网络流初步-OIWiki
  3. 网络流初步-洛谷
  4. 增广路定理 简证
  5. 求解最大流的四种算法介绍、利用最大流模型解题入门
  6. 最大流-最小割定理
  7. 网络流基础、最大流最小割定理以及证明最大流最小割定理
  8. 网络流题集
  9. [算法竞赛入门] 网络流基础:理解最大流/最小割定理 (蒋炎岩)_哔哩哔哩_bilibili
  10. 最小费用最大流 - 知乎

[题目练习]

  1. 序号题号标题题型题目难度
    1P3376【模板】网络最大流最大流1
    2HDU 1532Drainage Ditches最大流1
    3HDU 3549Flow Problem最大流1
    4HDU 3572Task Schedule最大流+判断满流2
    5HDU 2732Leapin’ Lizards最大流5
    6HDU 3338Kakuro Extension最大流5
    7HDU 2883kebab最大流+判断满流4
    8HDU 3605Escape最大流3
    9HDU 4183Pahom on Water最大流2
    10HDU 4240Route Redundancy最大流2
    11HDU 3081Marriage Match II最大流+并查集3
    12HDU 3277Marriage Match III最大流+并查集4
    13HDU 3416Marriage Match IV最大流+最短路4
    14HDU 3468Treasure Hunting最大流+最短路4
    15P1361小M的作物最小割1
    16P1343地震逃生最小割1
    17P1345奶牛的电信最小割2
    18HDU 3046Pleasant sheep and big big wolf最小割1
    19HDU 1565方格取数(1)最小割2
    20HDU 1569方格取数(2)最小割2
    21HDU 3820Golden Eggs最小割5
    22HDU 1533Going Home费用流1
    23HDU 3488Tour费用流2
    24HDU 3435A new Graph Game费用流2

洛谷网络流24题:网络流24题(做完上面再来做这个)

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TUStarry

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值