网络流
网络流
基本概念
网络
网络指的是一张有向图 , 对于 , , 有一权值为 ,为这条边的容量,当 ,。
另有两个特殊点 和 ,,分别称作源点、汇点。
流
设 是定义在节点二元组 上的实数函数,且满足:
- ;
- ;
- ,;
这三条定律分别称为容量限制、斜对称、流量守恒。
我们可以用一个例子来解释:源点就是自来水厂,汇点就是你家,你家和自来水厂用管道连接。这三条定律分别说的是:你给自来水厂交钱之后自来水厂可以给你家疯狂灌水,但是有管道的限制,不可能超出管道容量。第二个说的是,自来水厂给你家灌了 升水,相当于你家往自来水厂灌了 升水。第三条说的是,管道只是管道,不能存水,流进了多少水,就要流出去多少水。
流函数的完整定义为:
不过这里要注意一下,对于反向的流量是负数这一点可能很难理解,实际上算法导论上并没有给出这斜对称一性质,“反向边”这个概念在残留网络时才会提到,用于退流。建图时可能会存在反向边,这时候其实我们可以直接在反向边上加一个节点,由于流量守恒,所以加了节点的新的网络与之前的网络等价。
流量
流量是对于不同的流函数而言的,对于一个可行流,也就是满足上述三条定律是流函数,其流量定义为所有流出源点的流量减去所有流入源点的流量之差,格式化地,即为: 。而最大流即为流量最大的可行流。
最大流
Edmond-Karp 算法
Ek算法的基本思想就是逐一找增广路。
这个算法很简单,就是 BFS 找增广路,然后对其进行 增广,这个思想被称为 增广,而 算法则是对 的具体实现。
增广路
你可能会问,怎么找?怎么增广?
- 找?我们就从源点一直 BFS 走来走去,碰到汇点就停,然后增广(每一条路都要增广)。我们在 BFS 的时候就注意一下流量合不合法就可以了。
- 增广?其实就是按照我们找的增广路在重新走一遍。走的时候把这条路的能够成的最大流量减一减,然后给答案加上最小流量就可以了。
反向边
增广的时候要注意建造反向边,原因是这条路不一定是最优的,这样子程序可以进行反悔,也就是退流。假如我们对这条路进行增广了,那么其中的每一条边的反向边的流量就是它的流量。
还有关于一些小细节。如果是常用的链式前向星,那么在加入边的时候就要先加入反向边。那么在用的时候呢,我们直接让边的编号异或1 就可以了 。为什么呢?这就是成对存储,我们在加入正向边后加入反向边,就是靠近的,所以可以使用 。我们还要注意一开始的边的编号要设置为1,因为边要从编号 1开始,这样子才有效果。
EK 算法的时间复杂度为 (其中 为点数, 为边数),但其实网络流的上界是很宽松的,EK算法 ~ 一般都能跑过。
#include <bits/stdc++.h>
#define int long long
#define maxn 10005
#define inf 1e9
using namespace std;
inline int read(){
int x = 0 , f = 1 ; char c = getchar() ;
while( c < '0' || c > '9' ) { if( c == '-' ) f = -1 ; c = getchar() ; }
while( c >= '0' && c <= '9' ) { x = x * 10 + c - '0' ; c = getchar() ; }
return x * f ;
}
struct edge{
int v, w, nxt;
}e[maxn];
int head[maxn], cnt;
int n, m, s, t;
void add(int u, int v, int w){
e[++cnt].v = v, e[cnt].w = w;
e[cnt].nxt = head[u], head[u] = cnt;
}
queue<int> q;
int v[maxn];
int maxflow = 0;
int incf[maxn], pre[maxn];
bool bfs(){
memset(v, 0, sizeof(v));
while(q.size()) q.pop();
q.push(s), v[s] = 1;
incf[s] = inf;
while(!q.empty()){
int x = q.front();
q.pop();
for(int i = head[x]; i; i = e[i].nxt){
if(e[i].w){
int y = e[i].v;
if(v[y]) continue;
incf[y] = min(incf[x], e[i].w);
pre[y] = i;
q.push(y);
v[y] = 1;
if(y == t) return 1;
}
}
}
return 0;
}
void update(){
int x = t;
while(x != s){
int i = pre[x];
e[i].w -= incf[t];
e[i ^ 1].w += incf[t];
x = e[i ^ 1].v;
}
maxflow += incf[t];
}
signed main() {
n = read(), m = read(), s = read(), t = read();
cnt = 1;
for(int i = 1; i <= m; i++){
int u = read(), v = read(), w = read();
add(u, v, w), add(v, u, 0);
}
while(bfs()) update();
cout << maxflow;
}
Dinic 算法
#include <bits/stdc++.h>
#define int long long
#define maxn 10005
#define inf 1e9
using namespace std;
inline int read(){
int x = 0 , f = 1 ; char c = getchar() ;
while( c < '0' || c > '9' ) { if( c == '-' ) f = -1 ; c = getchar() ; }
while( c >= '0' && c <= '9' ) { x = x * 10 + c - '0' ; c = getchar() ; }
return x * f ;
}
struct edge{
int v, w, nxt;
}e[maxn];
int head[maxn], cnt;
int n, m, s, t;
void add(int u, int v, int w){
e[++cnt].v = v, e[cnt].w = w;
e[cnt].nxt = head[u], head[u] = cnt;
}
queue<int> q;
int d[maxn], now[maxn];
bool bfs(){
memset(d, 0, sizeof(d));
while(q.size()) q.pop();
q.push(s);
d[s] = 1;
now[s] = head[s];
while(!q.empty()){
int x = q.front(); q.pop();
for(int i = head[x]; i; i = e[i].nxt){
if(e[i].w && !d[e[i].v]){
q.push(e[i].v);
now[e[i].v] = head[e[i].v];
d[e[i].v] = d[x] + 1;
if(e[i].v == t) return 1;
}
}
}
return 0;
}
int dinic(int x, int flow){
if(x == t) return flow;
int rest = flow, k, i;
for(int i = now[x]; i && rest; i = e[i].nxt){
now[x] = i;
if(e[i].w && d[e[i].v] == d[x] + 1){
k = dinic(e[i].v, min(rest, e[i].w));
if(!k) d[e[i].v] = 0;
e[i].w -= k;
e[i ^ 1].w += k;
rest -= k;
}
}
return flow - rest;
}
int maxflow = 0;
signed main() {
n = read(), m = read(), s = read(), t = read();
cnt = 1;
for(int i = 1; i <= m; i++){
int u = read(), v = read(), w = read();
add(u, v, w), add(v, u, 0);
}
int flow = 0;
while(bfs()){
while(flow = dinic(s, inf)) maxflow += flow;
}
cout << maxflow;
}
模型
题意魔改板子型
例题:【地震逃生】
题目描述
汶川地震发生时,四川**中学正在上课,一看地震发生,老师们立刻带领 名学生逃跑,整个学校可以抽象地看成一个有向图,图中有 个点, 条边。 号点为教室, 号点为安全地带,每条边都只能容纳一定量的学生,超过楼就要倒塌,由于人数太多,校长决定让同学们分成几批逃生,只有第一批学生全部逃生完毕后,第二批学生才能从 号点出发逃生,现在请你帮校长算算,每批最多能运出多少个学生, 名学生分几批才能运完。
输入格式
第一行三个整数 ;以下 行,每行三个整数 (,)描述一条边,分别代表从 点到 点有一条边,且可容纳 名学生。
输出格式
两个整数,分别表示每批最多能运出多少个学生, 名学生分几批才能运完。如果无法到达目的地( 号点)则输出
Orz Ni Jinan Saint Cow!
。对于 的数据,,,。
这类题就是魔改板子,可以一眼看出直接建图套板子即可。
#include <bits/stdc++.h>
#define int long long
#define maxn 10005
#define inf 1e9
using namespace std;
inline int read(){
int x = 0 , f = 1 ; char c = getchar() ;
while( c < '0' || c > '9' ) { if( c == '-' ) f = -1 ; c = getchar() ; }
while( c >= '0' && c <= '9' ) { x = x * 10 + c - '0' ; c = getchar() ; }
return x * f ;
}
struct edge{
int v, w, nxt;
}e[maxn];
int head[maxn], cnt;
int n, m, s, t;
void add(int u, int v, int w){
e[++cnt].v = v, e[cnt].w = w;
e[cnt].nxt = head[u], head[u] = cnt;
}
queue<int> q;
int d[maxn], now[maxn];
bool bfs(){
memset(d, 0, sizeof(d));
while(q.size()) q.pop();
q.push(s);
d[s] = 1;
now[s] = head[s];
while(!q.empty()){
int x = q.front(); q.pop();
for(int i = head[x]; i; i = e[i].nxt){
if(e[i].w && !d[e[i].v]){
q.push(e[i].v);
now[e[i].v] = head[e[i].v];
d[e[i].v] = d[x] + 1;
if(e[i].v == t) return 1;
}
}
}
return 0;
}
int dinic(int x, int flow){
if(x == t) return flow;
int rest = flow, k, i;
for(int i = now[x]; i && rest; i = e[i].nxt){
now[x] = i;
if(e[i].w && d[e[i].v] == d[x] + 1){
k = dinic(e[i].v, min(rest, e[i].w));
if(!k) d[e[i].v] = 0;
e[i].w -= k;
e[i ^ 1].w += k;
rest -= k;
}
}
return flow - rest;
}
int maxflow = 0;
int x = 0;
signed main() {
n = read(), m = read(), x = read();
cnt = 1;
s = 1, t = n;
for(int i = 1; i <= m; i++){
int u = read(), v = read(), w = read();
add(u, v, w), add(v, u, 0);
}
int flow = 0;
while(bfs()){
while(flow = dinic(s, inf)) maxflow += flow;
}
if(maxflow == 0) {
cout << "Orz Ni Jinan Saint Cow!";
return 0;
}
cout << maxflow << " ";
int tmp = x / maxflow;
if(x % maxflow == 0) cout << tmp;
else cout << tmp + 1;
}
类似题目还有:
[USACO4.2]草地排水Drainage Ditches:【洛谷P2740】
[USACO09JAN]Total Flow S:【洛谷P2936】
最小割
选出边权和最小的边使得源点不能到汇点。
最大流最小割定理:最大流 = 最小割
对于以下三个命题,它们互相等价:
- 流函数 是最大流;
- 的残留网络 中无增广路;
- 存在一种 和 的划分方式,此时 。(实际上此时的 就是最小割)
模型
二者取一式问题
最小割模型一般是二者取一。
例题:【善意的投票】
题目描述
幼儿园里有 个小朋友打算通过投票来决定睡不睡午觉。
为了照顾一下自己朋友的想法,他们也可以投和自己本来意愿相反的票。
我们定义一次投票的冲突数为好朋友之间发生冲突的总数加上和所有和自己本来意愿发生冲突的人数。
应该怎样投票,才能使冲突数最小?
输入格式
第一行两个整数 。其中 代表总人数, 代表好朋友的对数。
第二行 个整数,第 个整数代表第 个小朋友的意愿:用 表示
接下来 行,每行有两个整数 ,表示 是一对好朋友,我们保证任何两对 不会重复。
输出格式
一行一个整数,即可能的最小冲突数。
这样建图:直接将S连向同意的人,T连向不同意的人,若两人是朋友,则在他们之间连一条双向边(这里有些人不理解:若两个人有冲突,则只需要其中任意一个人改变意见就行了,简单说是让a同意b的意见或者b同意a的意见,所以只需割掉一条边满足一种情况就可以了,但是有两种情况,所以建双向边)。最后就是求最小割了,割掉一条边就是一次冲突。直接套上最大流的模板就ok了。
#include <bits/stdc++.h>
#define int long long
#define maxn 190005
#define inf 1e9
using namespace std;
inline int read(){
int x = 0 , f = 1 ; char c = getchar() ;
while( c < '0' || c > '9' ) { if( c == '-' ) f = -1 ; c = getchar() ; }
while( c >= '0' && c <= '9' ) { x = x * 10 + c - '0' ; c = getchar() ; }
return x * f ;
}
struct edge{
int v, w, nxt;
}e[maxn];
int head[maxn], cnt;
int n, m, s, t;
void add(int u, int v, int w){
e[++cnt].v = v, e[cnt].w = w;
e[cnt].nxt = head[u], head[u] = cnt;
}
queue<int> q;
int d[maxn], now[maxn];
bool bfs(){
memset(d, 0, sizeof(d));
while(q.size()) q.pop();
q.push(s);
d[s] = 1;
now[s] = head[s];
while(!q.empty()){
int x = q.front(); q.pop();
for(int i = head[x]; i; i = e[i].nxt){
if(e[i].w && !d[e[i].v]){
q.push(e[i].v);
now[e[i].v] = head[e[i].v];
d[e[i].v] = d[x] + 1;
if(e[i].v == t) return 1;
}
}
}
return 0;
}
int dinic(int x, int flow){
if(x == t) return flow;
int rest = flow, k, i;
for(int i = now[x]; i && rest; i = e[i].nxt){
now[x] = i;
if(e[i].w && d[e[i].v] == d[x] + 1){
k = dinic(e[i].v, min(rest, e[i].w));
if(!k) d[e[i].v] = 0;
e[i].w -= k;
e[i ^ 1].w += k;
rest -= k;
}
}
return flow - rest;
}
int tot = 0;
int maxflow = 0;
signed main() {
n = read(), m = read();
s = 0, t = n + 1;
cnt = 1;
for(int i = 1; i <= n; i++) {
int pos = read();
if(pos) add(s, i, 1), add(i, s, 0);
else add(i, t, 1), add(t, i, 0);
}
for(int i = 1; i <= m; i++) {
int u = read(), v = read();
add(u, v, 1), add(v, u, 0);
add(v, u, 1), add(u, v, 0);
}
int flow = 0;
while(bfs()){
while(flow = dinic(s, inf)) maxflow += flow;
}
cout << maxflow;
}
类似例题:
小M的作物:【洛谷P1361】
费用流
给出一个包含 个点和 条边的有向图(下面称其为网络) ,该网络上所有点分别编号为 ,所有边分别编号为 ,其中该网络的源点为 ,汇点为 ,网络上的每条边 都有一个流量限制 和单位流量的费用 。
你需要给每条边 确定一个流量 ,要求:
- (每条边的流量不超过其流量限制);
- ,(除了源点和汇点外,其他各点流入的流量和流出的流量相等);
- (源点流出的流量等于汇点流入的流量)。
定义网络 的流量 ,网络 的费用 。
SSP算法
SSP(Successive Shortest Path)算法是一个贪心的算法。它的思路是每次寻找单位费用最小的增广路进行增广,直到图上不存在增广路为止。
如果图上存在单位费用为负的圈,SSP 算法正确无法求出该网络的最小费用最大流。此时需要先使用消圈算法消去图上的负圈。
#include <bits/stdc++.h>
#define int long long
#define maxn 10005
#define inf 1e9
using namespace std;
inline int read(){
int x = 0 , f = 1 ; char c = getchar() ;
while( c < '0' || c > '9' ) { if( c == '-' ) f = -1 ; c = getchar() ; }
while( c >= '0' && c <= '9' ) { x = x * 10 + c - '0' ; c = getchar() ; }
return x * f ;
}
struct edge{
int v, w, nxt, cost;
}e[10 * maxn];
int head[maxn], cnt;
int n, m, s, t;
void add(int u, int v, int w, int c){
e[++cnt].v = v, e[cnt].w = w;
e[cnt].nxt = head[u], head[u] = cnt;
e[cnt].cost = c;
}
int v[maxn];
int maxflow = 0;
int incf[maxn], pre[maxn], d[maxn];
bool bfs(){
queue<int> q;
memset(v, 0, sizeof(v));
memset(d, 0x3f, sizeof(d));
q.push(s), v[s] = 1, d[s] = 0;
incf[s] = 1 << 30;
while(!q.empty()){
int x = q.front();
v[x] = 0;
q.pop();
for(int i = head[x]; i; i = e[i].nxt){
if(e[i].w){
int y = e[i].v;
if(d[y] > d[x] + e[i].cost){
d[y] = d[x] + e[i].cost;
incf[y] = min(incf[x], e[i].w);
pre[y] = i;
if(!v[y]) v[y] = 1, q.push(y);
}
}
}
}
if(d[t] < 1e9) return 1;
return 0;
}
int ans =0 ;
void update(){
int x = t;
while(x != s){
int i = pre[x];
e[i].w -= incf[t];
e[i ^ 1].w += incf[t];
x = e[i ^ 1].v;
}
maxflow += incf[t];
ans += d[t] * incf[t];
}
signed main() {
n = read(), m = read(), s = read(), t = read();
cnt = 1;
for(int i = 1; i <= m; i++){
int u = read(), v = read(), w = read(), c = read();
add(u, v, w, c), add(v, u, 0, -c);
}
while(bfs()) update();
cout << maxflow << " " << ans;
}
模型
拆点
例题:【餐巾计划问题】
题目描述
一个餐厅在相继的 天里,每天需用的餐巾数不尽相同。假设第 天需要 块餐巾( i=1,2,...,N)。餐厅可以购买新的餐巾,每块餐巾的费用为 分;或者把旧餐巾送到快洗部,洗一块需 m 天,其费用为 f 分;或者送到慢洗部,洗一块需 天(),其费用为 分()。
每天结束时,餐厅必须决定将多少块脏的餐巾送到快洗部,多少块餐巾送到慢洗部,以及多少块保存起来延期送洗。但是每天洗好的餐巾和购买的新餐巾数之和,要满足当天的需求量。
试设计一个算法为餐厅合理地安排好 天中餐巾使用计划,使总的花费最小。编程找出一个最佳餐巾使用计划。
输入格式
由标准输入提供输入数据。文件第 1 行有 1 个正整数 ,代表要安排餐巾使用计划的天数。
接下来的一行是餐厅在相继的 天里,每天需用的餐巾数。
最后一行包含5个正整数。 是每块新餐巾的费用; 是快洗部洗一块餐巾需用天数; 是快洗部洗一块餐巾需要的费用; 是慢洗部洗一块餐巾需用天数; 是慢洗部洗一块餐巾需要的费用。
输出格式
将餐厅在相继的 N 天里使用餐巾的最小总花费输出
我们拆点,将一天拆成晚上和早上,每天晚上会受到脏餐巾(来源:当天早上用完的餐巾,在这道题中可理解为从原点获得),每天早上又有干净的餐巾(来源:购买、快洗店、慢洗店)。
1.从原点向每一天晚上连一条流量为当天所用餐巾x,费用为0的边,表示每天晚上从起点获得x条脏餐巾。
2.从每一天早上向汇点连一条流量为当天所用餐巾x,费用为0的边,每天白天,表示向汇点提供x条干净的餐巾,流满时表示第i天的餐巾够用 。 3.从每一天晚上向第二天晚上连一条流量为INF,费用为0的边,表示每天晚上可以将脏餐巾留到第二天晚上(注意不是早上,因为脏餐巾在早上不可以使用)。
4.从每一天晚上向这一天+快洗所用天数t1的那一天早上连一条流量为INF,费用为快洗所用钱数的边,表示每天晚上可以送去快洗部,在地i+t1天早上收到餐巾 。
5.同理,从每一天晚上向这一天+慢洗所用天数t2的那一天早上连一条流量为INF,费用为慢洗所用钱数的边,表示每天晚上可以送去慢洗部,在地i+t2天早上收到餐巾 。
6.从起点向每一天早上连一条流量为INF,费用为购买餐巾所用钱数的边,表示每天早上可以购买餐巾 。 注意,以上6点需要建反向边!3~6点需要做判断(即连向的边必须<=n)
#include <bits/stdc++.h>
using namespace std;
#define maxn 200005
#define int long long
const int inf = 1e14 + 7;
inline int read() {
int x = 0, f = 1; char c = getchar();
while (c < '0' || c > '9') { if (c == '-') f = -1; c = getchar(); }
while (c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
return x * f;
}
int N, p, m, n, f, s1;
int s, t;
struct edge {
int v, w, c, nxt;
}e[maxn];
int head[maxn], cnt;
void add(int u, int v, int w, int c) {
e[++cnt].v = v, e[cnt].w = w;
e[cnt].c = c;
e[cnt].nxt = head[u], head[u] = cnt;
}
int v[maxn];
int maxflow = 0;
int incf[maxn], pre[maxn], d[maxn];
bool bfs() {
queue<int> q;
memset(v, 0, sizeof(v));
memset(d, 0x3f, sizeof(d));
q.push(s), v[s] = 1, d[s] = 0;
incf[s] = 1 << 30;
while (!q.empty()) {
int x = q.front();
v[x] = 0;
q.pop();
for (int i = head[x]; i; i = e[i].nxt) {
if (e[i].w) {
int y = e[i].v;
if (d[y] > d[x] + e[i].c) {
d[y] = d[x] + e[i].c;
incf[y] = min(incf[x], e[i].w);
pre[y] = i;
if (!v[y]) v[y] = 1, q.push(y);
}
}
}
}
if (d[t] < 1e9) return 1;
return 0;
}
int ans = 0;
void update() {
int x = t;
while (x != s) {
int i = pre[x];
e[i].w -= incf[t];
e[i ^ 1].w += incf[t];
x = e[i ^ 1].v;
}
maxflow += incf[t];
ans += d[t] * incf[t];
}
signed main() {
N = read();
s = 0, t = 2 * N + 1;
cnt = 1;
for (int i = 1; i <= N; i++) {
int r = read();
add(s, i, r, 0), add(i, s, 0, 0);
add(i + N, t, r, 0), add(t, i + N, 0, 0);
}
p = read(), m = read(), f = read(), n = read(), s1 = read();
for (int i = 1; i <= N; i++) {
add(s, i + N, inf, p), add(i + N, s, 0, -p);
if (i + 1 <= N)
add(i, i + 1, inf, 0), add(i + 1, i, 0, 0);
if (i + m <= N)
add(i, i + N + m, inf, f), add(i + N + m, i, 0, -f);
if (i + n <= N)
add(i, i + N + n, inf, s1), add(i + N + n, i, 0, -s1);
}
while (bfs()) update();
cout << ans;
}
除了将一天拆为早上和晚上,还可以将一个点拆为进入的点和出去的点,如K取方格数;也可以将一个人拆成多个人,如美食节
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
· 张高兴的大模型开发实战:(一)使用 Selenium 进行网页爬虫