网络流

网络流

基本性质

网络:一张带权有向图,有源点汇点,源点可以无限提供流量,中间的每个点不产生新流量,但是会用一个流量上限(容量),要求源点只有流出的流,汇点只有流入的流,这样的图被称作网络流

同时根据网络的定义,我们也可以引申出其他名词的定义

源点:只有流出流的点

汇点:只有流入流的点

流量:一条边上可以流过的流量

容量:一条边上可供流过的最大流量

残量(flow):一条边上的容量减去流量

增广路:找到一条源点到汇点的路径使得这条边上的残量大于0

我们据此,可以推理出以下性质:

  1. 对于任何一条流,都有流量 <= 容量
  2. \(\sum_{p\in E} k_{p,u} = \sum_{q\in E}k_{u, q}\) 原因:一条边的入流与出流相等
  3. 对于任何一条有向边,总有:\(k_{u,v} = - k_{v, u}\) 原因是对于一条有向边来说,有 \(k_{u,v}\) 的流从 \(u\)\(v\),就相当于有 \(-k_{v,u}\)\(v\)\(u\)

网络流基本算法

网络流最大流

找到一个分配流量的方案使得网络中流量最大 :

1.找到这条路径上最小的 \(F[u][v]\)(我们设 \(F[u][v]\) 表示 \(u->v\) 这条边上的残量即剩余流量),下面记为 \(flow\)
2.将这条路径上的每一条有向边 \(u->v\) 的残量减去 \(flow\),同时对于起反向边 \(v->u\) 的残量加上 \(flow\)
3.重复上述过程,直到找不出增广路,此时我们就找到了最大流

以上算法是基于增广路定理:网络达到最大流当且仅当残留网络中没有增广路(证明咕了)

我们考虑,假如说我们不想走这条图,我们肯定是要反悔的,那么我们建一条反向边,这样的话我们反悔时可以走反向边退回去,同时根据反向边的权值抵消掉原有的权值即可(注意:如果要建边的话 \(\mathrm{cnt}\) 要从1开始)

考虑优化上述算法,我们使用 \(\mathrm{Dinic}\) 算法

\(\mathrm{Dinic}\) 算法的核心很简单,其实就是我们直接跑这个图不一定达到最优,我们不妨进行一些操作使得其更优

我们考虑分层图分配深度,这样防止一些残量为0的节点被重复选择,同时便于判断程序是否结束

inline bool bfs() {
	memset(dep, 0, sizeof(dep));
	queue <int> q;
	q.push(S);	dep[S] = 1;
	while (!q.empty()) {
		int u = q.front(); q.pop();
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].to, w = e[i].val;
			if(w && !dep[v]) dep[v] = dep[u] + 1, q.push(v);
		}
	}
	return dep[T];
}

inline int dfs(int u, int flow_in) {
	if (u == T) return flow_in;
	int flow_out = 0;
	for (int i = head[u]; i && flow_in; i = e[i].nxt) {
		int v = e[i].to;
		if (e[i].val && dep[v] == dep[u] + 1) {
			int res = dfs(v, min(e[i].val, flow_in));
			e[i].val -= res;
			e[i ^ 1].val += res;
			flow_in -= res;
			flow_out += res;
		}
	}
	if (flow_out == 0) dep[u] = 0;//这样写就不会再次跑到这个点了,是一个巨大的优化
	return flow_out;
}

inline void Dinic() {
	read(n, m, S, T);
	rep (i, 1, m) {
		int u, v, w;
		read(u, v, w);
		add(u, v, w);
		add(v, u, 0);
	}
	while (bfs()) 
		ans += dfs(S, inf);
	write(ans, '\n');
}

网络流最小费用最大流

我们在网络流最小流的基础上增加一个费用 \(w_i\),每当水流流过这条边时,这条边没流1的流量会花费 \(w_i\) 的费用。要在最大流的前提下求出最小费用

做法:我们每次增广一条从源点到汇点的最短路,保证路上的边都没有满流。我们可以考虑通过spfa来进行操作

我们考虑一个问题,就是我们每次只会增广一条路径,那么我们对于当前整条增广路来说,同时减少的流量是相等的,所以大概就是说,我们对于当前这条路来说,我们等于是 \(flow_i\) 这条路径分别乘上每个点的费用,就是走这一次增广所用的费用,好了,这道题我们就能合理的解决了

同时,我们要关注,如果你要走回头边的话,你得把费用减回去,因此,你的反向边应该是费用的相反数

问题:我们现在学会了网络流最小费用最大流,那么请问网络流最大费用最大流怎么办捏?

: 我们直接跑费用的相反数,然后最后输出 \(\mathrm{-min_cost}\)

代码如下:

inline void adde(int u, int v, int flow, int val) {
    add(u, v, flow, val);
    add(v, u, 0, -val);
}
inline bool spfa() {
    rep (i, 0, N - 1) dis[i] = inf, flow[i] = 0;
    queue <int> q;
    q.push(S); dis[S] = 0; flow[S] = inf;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        ins[u] = false;
        for (int i = head[u]; i; i = e[i].nxt) {
            int v = e[i].to, flo = e[i].flow, w = e[i].val;
            if (flo && dis[u] + w < dis[v]) {
                dis[v] = dis[u] + w; pre[v] = i;
                flow[v] = min(flow[u], flo);
                if (!ins[v]) q.push(v), ins[v] = true;
            }
        }
    }
    return flow[T];
}
inline void update() {
    int x = T, t = flow[T];
    while (x != S) {
        e[pre[x]].flow -= t;
        e[pre[x] ^ 1].flow += t;
        x = e[pre[x] ^ 1].to;
    }
    min_cost += t * dis[T];
    max_flow += t;
}
inline void EK() {
    while (spfa()) update();
    write(-min_cost, '\n');
}

最小割

割的定义:在原网络图中移出的边集,要求使得网络不连通

最小割:边权和最小的割

最大流最小割定理:最大流的值等于最小割的值

技巧:

求最小边的最小割

设总边数为 \(E\),跑最大流前我们把所有的边都乘 \(E + 1\) 然后再 \(+1\)

得到的结果应该是 $min_{cut} \times {E+1} + $ 割边数量、

输出任意一种最小割的方案

跑过一次最大流之后,在残量网络上,s和t之间不连通了
进行一次 \({dfs/bfs}\),求出从 \(s\) 出发能到达的点集 \(S\),和不能到达的点集 \(T\)
所有从 \(S\) 跨越到 \(T\) 的满流边(残留网络为0)构成了一组最小割

判断一条边是否满流

运行一次最大流算法,得到一个残量网络
取残量网络上的一条满流边(u, v),判断这条边是否一定满流
对残量网络运行Tarjan算法,求出所有SCC
当u和v不属于同一个SCC的时候,这条边一定满流
否则,我们可以在SCC中找到一个包含这条边的反向边的环,沿着环增广一次,仍然不破坏流量平衡,但是这条边已经不满流了

判断某一条边是否可能为最小割中的一条

所有一定满流的边都可能为最小割

判断某条边是否一定出现在最小割中

首先还是对残量网络求SCC
考虑一条满流边(u, v),判断她是否一定出现在最小割中
当u和s属于同一个SCC,并且v和t属于同一个SCC的时候,这条边一定出现在最小割中


清新例题

[P2891 [USACO07OPEN]Dining G]([P2891 USACO07OPEN]Dining G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

题目描述:有F种食物和D种饮料,每种食物或饮料只能供一头牛享用,且每头牛只享用一种食物和一种饮料。现在有n头牛,每头牛都有自己喜欢的食物种类列表和饮料种类列表,问最多能使几头牛同时享用到自己喜欢的食物和饮料。(1 <= f <= 100, 1 <= d <= 100, 1 <= n <= 100)

我们首先考虑题意,题意是说,对于每头牛,只能同时选一种食物和一种饮料,但是一种食物和一种饮料可以被多头牛喜欢,且一种饮料只能被一头牛选

我们会发现这是个匹配问题,可以考虑用网络流来解决,大概长成 food --> cow --> drink

那么我们可以考虑用网络流来进行操作,我们考虑先从源点到食物全部连边,然后食物向牛连边,牛向饮料连边,饮料向汇点连边

我们考虑其他限制,我们会发现,每一头牛只能搭配一种食物和一种饮料,那么我们考虑所有的流量都为1,跑最大流即可解决

但是我们会发现答案大了,原因是什么?

我们考虑这样一个图

我们发现3直接跑了好多次,所以,我们要让 3 只跑一次

因此决定我们用一种重要的方法——拆点

把 3 拆开,把每一头牛拆成入牛和出牛,中间连边容量为1,即可限制牛只进出一次

//把头文件和define删了以后长得好怪异啊
read(n, F, D);
S = 0, T = F + n + n + D + 1;
rep (i, 1, n) {
    add(F + i, F + n + i, 1);
    add(F + n + i, F + i, 0);
}
rep (i, 1, F) {
    add(S, i, 1);
    add(i, S, 0);
}
rep (i, 1, D) {
    add(F + n + n + i, T, 1);
    add(T, F + n + n + i, 0);
}
rep (i, 1, n) {
    int a, b, c;
    read(a, b);
    rep (j, 1, a) {
        read(c);
        add(c, F + i, 1);
        add(F + i, c, 0);
    }
    rep (j, 1, b) {
        read(c);
        add(F + n + i, F + n + n + c, 1);
        add(F + n + n + c, F + n + i, 0);
    }
}
Dinic();read(n, F, D);
S = 0, T = F + n + n + D + 1;
rep (i, 1, n) {
    add(F + i, F + n + i, 1);
    add(F + n + i, F + i, 0);
}
rep (i, 1, F) {
    add(S, i, 1);
    add(i, S, 0);
}
rep (i, 1, D) {
    add(F + n + n + i, T, 1);
    add(T, F + n + n + i, 0);
}
rep (i, 1, n) {
    int a, b, c;
    read(a, b);
    rep (j, 1, a) {
        read(c);
        add(c, F + i, 1);
        add(F + i, c, 0);
    }
    rep (j, 1, b) {
        read(c);
        add(F + n + i, F + n + n + c, 1);
        add(F + n + n + c, F + n + i, 0);
    }
}
Dinic();

P2045 方格取数加强版

给出一个 \(n*n\) 的矩阵,每一格有一个非负整数 \(A_{ij}\),(\(A_{ij} <= 1000\))现在从 \((1,1)\) 出发,可以往右或者往下走,最后到达 \((n,n)\),每达到一格,把该格子的数取出来,该格子的数就变成 \(0\),这样一共走 \(K\) 次,现在要求 \(K\) 次所达到的方格的数的和最大

这道题的题意很明确,虽然 $n \le 50,k\le 10 $ 很小,但是搜索仍然是过不去

所以我们考虑其他方法,我们会发现一个点只会通往相邻两个点,我们可以考虑用费用流,把每个点拆成入点和出点,然后入点向出点连两条边,一条表示要取这个点,那么就是费用为 \(a_{i,j}\) ,容量为 \(1\)(防止重复取),另一条表示不取这条边,那么容量为 \(k\) ,费用为0即可。同时,从出点往下面和右面的入点各连一条边,容量为 \(k\),费用为0即可

inline int in(int i, int j) {
    return (i - 1) * n + j;
}
inline int out(int i, int j) {
    return (i - 1) * n + j + n * n;
}

signed main() {
    read(n, k);
    T = n * n * 2 + 1;
    adde(S, 1, k, 0);
    adde(n * n * 2, T, k, 0);
    rep (i, 1, n) {
        rep (j, 1, n) {
            int x; read(x);
            adde(in(i, j), out(i, j), 1, -x);
            adde(in(i, j), out(i, j), k, 0);
            if (i < n) adde(out(i, j), in(i + 1, j), k, 0);
            if (j < n) adde(out(i, j), in(i, j + 1), k, 0);
        }
    }
    EK();
    return 0;
}
P4016 负载平衡问题

\(G\) 公司有 \(n\) 个沿铁路运输线环形排列的仓库,每个仓库存储的货物数量不等。如何用最少搬运量可以使 \(n\) 个仓库的库存数量相同。搬运货物时,只能在相邻的仓库之间搬运。

\(1 \le n \le 100\)

这个题是个贪心题……

但是既然它可以用网络流写,那我们就写一写

我们考虑,对于一个车站来说,其实它最后的值是确定的

那么我们可以考虑,最后的边可以都连向终点,容量为平均数,费用为 \(0\)

那么我们同时可以将源点连向每个点,容量均为 \(inf\),费用为 \(0\)

最后,我们在相邻两点之间连边,容量为 \(inf\),费用为 \(1\)

这是第一种方法

另一种方法就是

按照 比最终库存多与少分开

多的连源点,少的连汇点,这种方法说实话,我不知道怎么想出来的


P2770 航空路线问题

给定一张航空图,图中顶点代表城市,边代表两城市间的直通航线,并且不存在任何两个城市在同一条经线上。现要求找出一条满足下述限制条件的且途经城市最多的旅行路线。

  1. 从最西端城市出发,单向从西向东途经若干城市到达最东端城市,然后再单向从东向西飞回起点(可途经若干城市)。
  2. 除起点城市外,任何城市只能访问一次。

对于给定的航空图,试设计一个算法找出一条满足要求的最佳航空旅行路线。

\(1 \le n \le 100\)

首先,题目要求是求一个方案从左边走到最右边,再从最右边回到左边,每个点只能经过一次,同时要求经过的点尽可能多

首先我们发现有两个限制,一是只经过一次,而是经过的点尽可能多,那我们就可以考虑费用流了

每个点只经过一次我们肯定熟啊,肯定是拆点,中间连一条容量为1的边,由于我们每经过一个点,对答案的贡献为1,我们直接让费用也是1即可

其次,我从左到右再从右到左的不相交路径,我们可以看成从起点到终点的两条不同路径

那么我们给起点和终点的容量赋成2,费用为0,连边的点容量为1,费用为0即可

同时我们考虑统计答案,由于我们每条路顶多走一次,同时容量为1,所以我们考虑最后走过的边必然是残量为0的那几条

我们首先正着跑一遍,然后输出每一条边并进行标记,然后倒着跑一遍,倒着输出即可

注意:

  1. 可能存在一条从 \(1\sim n\) 再从 \(n\sim 1\) 的路径
  2. 输出的时候要输出 \(\Huge !\)
map<int, string> mp1;
map<string, int> mp2;
inline int in(int x) {return x;}
inline int out(int x) {return x + n;}
bool vis[N];
inline void dfs1(int u) {
    vis[u] = true;
    cout << mp1[u - n] << endl;
    for (int i = head[u]; i; i = e[i].nxt) {
        int v = e[i].to, flo = e[i].flow;
        if (v <= n && !flo) {
            dfs1(out(v));
            break;
        }
    }
}

inline void dfs2(int u) {
    for (int i = head[u]; i; i = e[i].nxt) {
        int v = e[i].to, flo = e[i].flow;
        if (v <= n && !vis[out(v)] && !flo)
            dfs2(out(v));
    }
    cout << mp1[u - n] << endl;
}

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    cin >> n >> m;
    S = 1, T = out(n);
    rep (i, 1, n) {
        string a;
        cin >> a;
        mp1[i] = a;
        mp2[a] = i;
        if (i == 1 || i == n)link(in(i), out(i), 2, 0);
        else link(in(i), out(i), 1, 0);
    }
    bool flag = 0;
    rep (i, 1, m) {
        string a, b;
        cin >> a >> b;
        int u = mp2[a], v = mp2[b];
        if (u > v) swap(u, v);
        if (u == 1 && v == n) flag = true;
        link(out(u), in(v), 1, -1);
    }
    EK();
    if (max_flow == 0) return puts("No Solution!"), 0;
    if (max_flow == 1 && !flag) return puts("No Solution!"), 0;
    if (max_flow == 1) {
        cout << 2 << endl << mp1[1] << endl << mp1[n] << endl << mp1[1] << endl;
        return 0;
    }
    cout << -min_cost << endl;
    dfs1(out(1));
    dfs2(out(1));
    return 0;
}

P2766 最长不下降子序列问题

给定正整数序列 \(x_1,x_2,...x_n\)

  1. 计算其最长不下降子序列的长度 \(s\)
  2. 如果每一个元素只允许用一次,求最多有几个长度为 \(s\) 的最长不下降子序列
  3. 如果允许一个 \(x_1\)\(x_n\) 被多次使用,但是一个序列中每个元素只允许使用一次,求最多有几个长度为 \(s\) 的最长不下降子序列

第一问我们运用传统DP思想,设 \(f_i\) 表示前 \(i\) 位最长长度,转移方程可以很轻松的写成

\[f_i = \max\{f_j + 1\}(j\in [1,i)\cap a[i]\le a[j]) \]

第二问的话,我们考虑如果一个数在长度为 \(s\) 的最长不下降子序列中,那么它们必然在位置 \(f_i\)

简单证明一下:

如果 \(pos_i < f_i\),此时整个数列的长度为 \(s\),我们会考虑,对于 \(pos_i\) 之前必然会小于 \(f_i - 1\) 个数,这样的话,必然存在另一个长度等于 \(f_i - 1\) 的序列代替它的最长不下降子序列必然大于等于 \(s+1\)

换一种方式解释:设 \(pos_1\) 前的长度为 \(len\),那么 \(len < f_i - 1\),所以必然存在一个长度大于 \(len\) 的一个位于 \(pos_i\)之前的数列来代替它,这样的话长度就会大于 \(s\)

如果 \(pos_i > f_i\),说明 \(pos_i\) 前面有大于等于 \(f_i\) 个数,那么 \(f_i+1 = f_i\),显然矛盾

因此 \(pos_i = f_i\),证明完毕

我们重新考虑这个题的写法,对于每个元素,它的位置我们已经固定且只能用一次,那么我们可以考虑用最大流

老规矩,只能用一次,直接拆点,拆成两个点,中间的容量为 1,对于每个点的位置固定,所以从源点 \(S\) 向每个 \(f_i\) 为 1 的点连边,由于这个 \(f_i= 1\) 的点只能被用一次,所以我们直接让其容量为 1 即可,最后再让 \(f_i\)\(s\) 的连向 \(T\)

第三问,允许多次使用?直接容量为inf即可

int f[N];
inline int in(int x) {return x;}
inline int out(int x) {return x + n;}
inline void ask1() {
    rep (i, 1, n){
        f[i] = 1;
        rep (j, 1, i - 1)
            if (a[i] >= a[j]) 
                f[i] = max(f[i], f[j] + 1);
    }
    rep (i, 1, n) maxn = max(maxn, f[i]);
    write(maxn, '\n');
}

inline void ask2() {
    S = 0, T = 2 * n + 1;
    rep (i, 1, n) link(in(i), out(i), 1);
    rep (i, 1, n) {
        if (f[i] == 1) link(S, in(i), 1);
        if (f[i] == maxn) link(out(i), T, 1);
        rep (j, 1, i - 1) 
            if (a[i] >= a[j] && f[i] == f[j] + 1) 
                link(out(j), in(i), 1);
    }
    Dinic();
}

inline void ask3() {
    if (n == 1) return cout << 1 << endl, void();
    memset(head, 0, sizeof(head));
    memset(e, 0, sizeof(e));
    cnt = 1;
    rep (i, 1, n) {
        if (i == 1 || i == n) link(in(i), out(i), inf); 
        else link(in(i), out(i), 1);

        if (f[i] == 1) {
            if(i == 1)  link(S, in(i), inf);
            else link(S, in(i), 1);
        }

        if (f[i] == maxn) {
            if(i == n) link(out(i), T, inf);
            else link(out(i), T, 1);
        }
        rep (j, i + 1, n) {
            if (f[i] + 1 == f[j] && a[i] <= a[j])
                link(out(i), in(j), 1);
        }
    }
    Dinic();
}
//write:RevolutionBP
posted @ 2022-02-22 16:33  RevolutionBP  阅读(120)  评论(0编辑  收藏  举报