文章目录
-
- 一、费用流
-
- [1.1 概念](#1.1 概念)
- [1.2 Edmonds-Karps 增广路算法](#1.2 Edmonds-Karps 增广路算法)
- [1.3 算法流程](#1.3 算法流程)
- [1.4 算法实现/模板练习](#1.4 算法实现/模板练习)
- 二、OJ练习
-
- [2.1 运输问题](#2.1 运输问题)
- [2.2 负载平衡问题](#2.2 负载平衡问题)
- [2.3 分配问题](#2.3 分配问题)
- [2.4 数字梯形问题](#2.4 数字梯形问题)
- [2.5 K取方格数](#2.5 K取方格数)
- [2.6 P4012 深海机器人问题](#2.6 P4012 深海机器人问题)
- [2.7 餐巾计划问题](#2.7 餐巾计划问题)
- [2.8 [NOI2008\] 志愿者招募](#2.8 [NOI2008] 志愿者招募)
- [三、(补档)Primal Dual 算法](#三、(补档)Primal Dual 算法)
-
- [3.1 定义](#3.1 定义)
-
- [3.1.1 势](#3.1.1 势)
- [3.2 实现](#3.2 实现)
-
- [3.2.1 势的求解](#3.2.1 势的求解)
- [3.2.2 代码实现](#3.2.2 代码实现)
- [3.2.3 时间复杂度](#3.2.3 时间复杂度)
一、费用流
1.1 概念
给定一个网络 G = (V, E)
,每条边(u, v)
除了有容量限制 c(u, v)
,还有一个给定的"单位费用"w(u, v)
。当边(u, v)
的流量为 f(u, v)
时,就要花费 f(u, v) * w(u, v)
。
该网络中总花费最小的最大流被称为**"最小费用最大流",总花费最大的最大流被称为"最大费用最大流",二者合称为 "费用流"**模型。注意:费用流的前提是最大流,然后才考虑费用的最值。
类似于"二分图最大匹配"与最大流的关系,"二分图带权最大匹配"可直接用最大费用最大流求解,每条边的权值就是它的单位费用。
PS:由于"最大费用最大流"只需把最短路改为最长路,所以下面只讨论"最小费用最大流"
1.2 Edmonds-Karps 增广路算法
在Edmonds-Karps 求解最大流的基础上, 把"通过 bfs 寻找增广路" 改为 "用spfa寻找一条单位费用之和最小的增广路"(即w(u, v)当作边权, 在残留网络上求最短路 ), 即可求出最小费用最大流.
证明:
数学归纳法:
对于当前流量 |f1|
下的我们找到一个最小费用流f1
,然后在f1
的残留网络上通过spfa()
增广出一条最小费用增广路, 该流为f2
那么叠加后有新流f = f1 + f2
只需证明f
是流量|f|
下的最小费用流
假设f
费用不最小,那么存在流f'
, |f'| = |f|
我们令f2' = f' - f1
, 那么有|f2'| = |f2|, dst(f2') < dst(f2)
这与f2
是f1
的残留网络上的最小费用增广路, 相矛盾, 得证
1.3 算法流程
- 将EK算法的
bfs
改为spfa
- 在EK算法的更新流量的同时更新费用
- 为了退流,反向边的初始容量为0,反向边的容量每次 +f
- 为了退费,反向边的初始费用为-w,走反向边的花费 + f * (-w)(和退流不同, 退流还要更改反向边剩余容量, 但是退费反向边的费用是固定不变的)
1.4 算法实现/模板练习
原题链接
P3381 【模板】最小费用最大流 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
AC代码
c++
#include <iostream>
#include <cstring>
#include <algorithm>
using PII = std::pair<int, int>;
const int N = 5005, M = 1e5 + 10, inf = 1e9;
int n, m, s, t, idx;
int head[N], pre[N], incf[N], dst[N], q[N];
bool vis[N];
struct edge{
int v, c, w, nxt;
} edges[M];
void addedge(int u, int v, int c, int w) {
edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
}
void add(int u, int v, int c, int w) {
addedge(u, v, c, w), addedge(v, u, 0, -w);
}
void update(int& f, int& c) {
for(int v = t; v != s; ) {
int i = pre[v];
edges[i].c -= incf[t];
edges[i ^ 1].c += incf[t];
v = edges[i ^ 1].v;
}
f += incf[t], c += incf[t] * dst[t];
}
bool spfa() {
memset(incf, 0, sizeof incf);
memset(dst, 0x3f, sizeof dst);
int f = 0, b = 0;
dst[q[b ++] = s] = 0, vis[s] = true, incf[s] = inf;
while(b - f) {
int u = q[f ++];
f %= N;
vis[u] = false;
for(int i = head[u]; ~i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].c && dst[v] > dst[u] + edges[i].w) {
dst[v] = dst[u] + edges[i].w;
incf[v] = std::min(incf[u], edges[i].c);
pre[v] = i;
if(vis[v]) continue;
vis[q[b ++] = v] = true;
b %= N;
}
}
}
return incf[t];
}
PII EK() {
int f = 0, c = 0;
while(spfa())
update(f, c);
return std::make_pair(f, c);
}
int main() {
memset(head, -1, sizeof head);
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
std::cin >> n >> m >> s >> t;
for(int i = 0, a, b, c, d; i < m; i ++)
std::cin >> a >> b >> c >> d, add(a, b, c, d);
auto [f, c] = EK();
std::cout << f << ' ' << c;
return 0;
}
二、OJ练习
2.1 运输问题
原题链接
P4015 运输问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
就是一个模板题
仓库和商店构成二分图
考虑这样建模:
边<x, y>代表仓库x到商店y的边,边的费用就是题目定义的费用,流量为正无穷
建立源点向每个左部点(仓库)连费用为0,流量为储货量的边
建立汇点每个右部点(商店)向汇点连费用为0,流量为需货量的边
我们跑最小费用最大流和最大费用最大流的板子
这样我们跑出来的最大流就是左部点能给右部点提供的最大货量,由于题目保证了储货量等于需货量,所以此时跑出来是满流也符合题目要求
最小费用和最大费用就是题目所求
我们没有必要写两边板子,求最大费用,把边权取反,然后输出相反数就行了
AC代码
c++
#include <bits/stdc++.h>
const int N = 205, M = 20105, inf = 1e9;
struct edge {
int v, c, w, nxt;
} edges[M];
int n, m, s, t;
int head[N], idx;
int q[N], dst[N], incf[N], pre[N];
bool vis[N];
void addedge(int u, int v, int c, int w) {
edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
}
void add(int u, int v, int c, int w) {
addedge(u, v, c, w), addedge(v, u, 0, -w);
}
bool spfa() {
memset(incf, 0, sizeof incf);
memset(dst, 0x3f, sizeof dst);
int f = 0, b = 0;
dst[q[b ++] = s] = 0, vis[s] = true, incf[s] = inf;
while (b - f) {
int u = q[f ++];
f %= N;
vis[u] = false;
for (int i = head[u]; ~i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].c && dst[v] > dst[u] + edges[i].w) {
dst[v] = dst[u] + edges[i].w;
incf[v] = std::min(incf[u], edges[i].c);
pre[v] = i;
if (vis[v]) continue;
vis[q[b ++] = v] = true;
b %= N;
}
}
}
return incf[t];
}
void update(int& c) {
for (int v = t; v != s; ) {
int i = pre[v];
edges[i].c -= incf[t];
edges[i ^ 1].c += incf[t];
v = edges[i ^ 1].v;
}
c += dst[t] * incf[t];
}
int EK() {
int c = 0;
while (spfa())
update(c);
return c;
}
int main() {
memset(head, -1, sizeof head);
std::cin >> m >> n;
s = 0, t = n + m + 1;
for (int i = 1, a; i <= m; i ++)
std::cin >> a, add(s, i, a, 0);
for (int i = 1, a; i <= n; i ++)
std::cin >> a, add(i + m, t, a, 0);
for (int i = 1; i <= m; i ++)
for (int j = 1, a; j <= n; j ++) {
std::cin >> a;
add(i, m + j, inf, a);
}
std::cout << EK() << '\n';
for (int i = 0; i < idx; i += 2) {
std::swap(edges[i].w, edges[i ^ 1].w);
edges[i].c += edges[i ^ 1].c, edges[i ^ 1].c = 0;
}
std::cout << -EK() << '\n';
return 0;
}
2.2 负载平衡问题
原题链接
P4016 负载平衡问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
我们考虑最终每个仓库的货物数目都是平均数avg。
那么初始的仓库可以分为:货物数目大于avg的,货物数目小于等于avg,我们不妨称为左部点,右部点
我们考虑这样建模,边的流量即二者之间流通的货物量,平均费用自然为1
然后对于初始货物量大于平均数的仓库,从源点向其连容量为a[i] - avg的边,平均费用为0
对于初始货物量小于等于平均数的仓库,向汇点连容量为avg - a[i]的边,平均费用为0
然后我们在流网络中可以求出一个最小费用最大流 ,下面只需证明最小费用最大流的费用就是原问题的解
由于原问题的可行性,所以我们的最大流一定是满流,所以只需证明最小费用就是最小运输量
每一点流量都代表一次交换中的货物数目,再乘上平均费用就对应了此次交换的运输量,所以我们的可行流一定对应一个方案中的运输量
而最小费用就是所有方案中的最小运输量,因为原问题的解是可行流的子集,而整数可行流的解在此子集中。
AC代码
c++
#include <bits/stdc++.h>
const int N = 105, M = 610, inf =1e9;
int n, s, t, avg, idx;
int head[N], dst[N], pre[N], q[N], a[N], incf[N];
bool vis[N];
struct edge{
int v, c, w, nxt;
} edges[M];
inline void addedge(int u, int v, int c, int w) {
edges[idx] = { v, c, w, head[u] }, head[u] = idx ++ ;
}
inline void add(int u, int v, int c, int w) {
addedge(u, v, c, w), addedge(v, u, 0, -w);
}
bool spfa() {
int f = 0, b = 0;
memset(dst, 0x3f, sizeof dst), memset(incf, 0, sizeof incf);
dst[q[b ++] = s] = 0, vis[s] = true, incf[s] = inf;
while (b - f) {
int u = q[f ++];
f %= N;
vis[u] = false;
for (int i = head[u]; ~i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].c && dst[u] + edges[i].w < dst[v]) {
pre[v] = i;
incf[v] = std::min(incf[u], edges[i].c);
dst[v] = dst[u] + edges[i].w;
if (vis[v]) continue;
vis[q[b ++] = v] = true;
b %= N;
}
}
}
return incf[t];
}
void update(int& f, int& c) {
for (int v = t; v != s; ) {
int i = pre[v];
edges[i].c -= incf[t], edges[i ^ 1].c += incf[t];
v = edges[i ^ 1].v;
}
f += incf[t], c += dst[t] * incf[t];
}
int EK() {
int f = 0, c = 0;
while (spfa())
update(f, c);
return c;
}
int main () {
memset(head, -1, sizeof head);
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
std::cin >> n, s = 0, t = n + 1;
for (int i = 1; i <= n; i ++ ) {
std::cin >> a[i], avg += a[i];
add(i, i < n ? i + 1 : 1, inf, 1);
add(i, i > 1 ? i - 1 : n, inf, 1);
}
avg /= n;
for (int i = 1; i <= n; i ++ )
if (a[i] > avg) add(s, i, a[i] - avg, 0);
else add(i, t, avg - a[i], 0);
std::cout << EK();
return 0;
}
2.3 分配问题
原题链接
P4014 分配问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
原问题就是一个二分图模型
左部点为员工,右部点为任务
我们可以求出若干组最大匹配并且最大匹配为完备匹配,而要求的是所有完备匹配中花费最小和最大的方案
考虑建立虚拟源点s,汇点t,s向左部点连容量为1,费用为0,右边点向汇点连容量为1,费用为0的边
原图边容量也为1,费用为题目给的费用
我们可以求出最大流且最大流是满流,只要求出最大流中的最小费用流,那么就是原问题的花费最小方案
如果求最大,我们将边的费用取反,然后再求最小费用流,费用取反就是答案
AC代码
c++
#include <bits/stdc++.h>
const int N = 105, M = 20205, inf = 1e9;
struct edge{
int v, c, w, nxt;
} edges[M];
int n, s, t;
int head[N], dst[N], q[N], incf[N], pre[N], idx;
bool vis[N];
void addedge(int u, int v, int c, int w) {
edges[idx] = { v, c, w, head[u] }, head[u] = idx ++ ;
}
void add(int u, int v, int c, int w) {
addedge(u, v, c, w), addedge(v, u, 0, -w);
}
bool spfa() {
memset(dst, 0x3f, sizeof dst);
memset(incf, 0, sizeof incf);
int b = 0, f = 0;
dst[q[b ++] = s] = 0, incf[s] = 1e9, vis[s] = true;
while (b - f) {
int u = q[f ++];
f %= N;
vis[u] = false;
for (int i = head[u]; ~i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].c && dst[u] + edges[i].w < dst[v]) {
dst[v] = edges[i].w + dst[u];
//std::cout << u << " " << v << " " << dst[v] << '\n';
incf[v] = std::min(edges[i].c, incf[u]);
pre[v] = i;
if (vis[v]) continue;
vis[q[b ++] = v] = true;
b %= N;
}
}
}
//std::cout << incf[t] << " " << dst[t]; exit(0);
return incf[t];
}
void update(int& f, int& c) {
for (int u = t; u != s; ) {
int i = pre[u];
edges[i].c -= incf[t], edges[i ^ 1].c += incf[t];
u = edges[i ^ 1].v;
}
f += incf[t], c += dst[t] * incf[t];
}
int EK() {
int f = 0, c = 0;
while (spfa())
update(f, c);
return c;
}
int main () {
memset(head, -1, sizeof head);
std::cin >> n;
s = 0, t = n << 1 | 1;
for (int i = 1, m; i <= n; i ++ ) {
add(s, i, 1, 0), add(i + n, t, 1, 0);
for (int j = 1; j <= n; j ++ ) {
std::cin >> m;
add(i, j + n, 1, m);
}
}
std::cout << EK() << '\n';
for (int i = 0; i < idx; i += 2 )
std::swap(edges[i].w, edges[i ^ 1].w), edges[i].c -= edges[i ^ 1].c, edges[i ^ 1].c = 0;
std::cout << EK() << '\n';
return 0;
}
2.4 数字梯形问题
原题链接
P4013 数字梯形问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
这题也挺简单,建图逻辑很清晰
我们建立虚拟源汇点s、t,s向第一层连容量为1费用为0的边
最后一层向汇点连边,费用为0,容量根据三种规则而定
由于要考虑每个点用几次,所以要拆点
规则1:点和边都只能用一次------入点和出点容量为1,出点和下一层入点间容量为1,最后一层到汇点容量是1
规则2:点用多次,边只能用一次------入点和出点容量为无穷,出点和下一层入点间容量为1,最后一层到汇点容量是无穷
规则3:点用多次,边用多次------入点和出点容量为无穷,出点和下一层入点间容量为无穷,最后一层到汇点容量是无穷
然后三个规则都建图跑费用流即可
AC代码
c++
#include <bits/stdc++.h>
const int N = 1205, M = 5005, inf = 1e9;
struct edge{
int v, c, w, nxt;
} edges[M];
int n, m, s, t;
int head[N], q[N], dst[N], incf[N], pre[N], idx;
bool vis[N];
void addedge (int u, int v, int c, int w) {
edges[idx] = { v, c, w, head[u] }, head[u] = idx ++ ;
}
void add(int u, int v, int c, int w) {
addedge(u, v, c, w), addedge(v, u, 0, -w);
}
bool spfa() {
memset(incf, 0, sizeof incf), memset(dst, -0x3f, sizeof dst);
int f = 0, b = 0;
dst[q[b ++] = s] = 0, incf[s] = inf, vis[s] = true;
while (b - f) {
int u = q[f ++ ];
vis[u] = false;
f %= N;
for (int i = head[u]; ~i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].c && dst[v] < dst[u] + edges[i].w) {
dst[v] = dst[u] + edges[i].w;
incf[v] = std::min(incf[u], edges[i].c);
pre[v] = i;
if (vis[v]) continue;
vis[q[b ++] = v] = true;
b %= N;
}
}
}
return incf[t];
}
void update(int& f, int& c) {
for (int v = t; v != s; ) {
int i = pre[v];
edges[i].c -= incf[t], edges[i ^ 1].c += incf[t];
v = edges[i ^ 1].v;
}
f += incf[t], c += dst[t] * incf[t];
}
int EK() {
int f = 0, c = 0;
while (spfa())
update(f, c);
return c;
}
int main () {
std::cin >> m >> n;
int tot = 0;
std::vector<std::vector<int>> nums(n, std::vector<int>(m + n));
std::vector<std::vector<int>> id(n, std::vector<int>(m + n));
for (int i = 0; i < n; i ++)
for (int j = 0; j < i + m; j ++ )
std::cin >> nums[i][j], id[i][j] = ++ tot;
s = 0, t = tot << 1 | 1;
//solve1
memset(head, -1, sizeof head), idx = 0;
for (int i = 0; i < m; i ++) add(s, id[0][i], 1, 0);
for (int i = 0; i < m + n - 1; i ++) add(id[n - 1][i] + tot, t, 1, 0);
for (int i = 0; i < n; i ++)
for (int j = 0; j < m + i; j ++) {
add(id[i][j], id[i][j] + tot, 1, nums[i][j]);
if (i < n - 1)
add(id[i][j] + tot, id[i + 1][j], 1, 0),
add(id[i][j] + tot, id[i + 1][j + 1], 1, 0);
}
std::cout << EK() << '\n';
//solve2
memset(head, -1, sizeof head), idx = 0;
for (int i = 0; i < m; i ++) add(s, id[0][i], 1, 0);
for (int i = 0; i < m + n - 1; i ++) add(id[n - 1][i] + tot, t, 1e9, 0);
for (int i = 0; i < n; i ++)
for (int j = 0; j < m + i; j ++) {
add(id[i][j], id[i][j] + tot, 1e9, nums[i][j]);
if (i < n - 1)
add(id[i][j] + tot, id[i + 1][j], 1, 0),
add(id[i][j] + tot, id[i + 1][j + 1], 1, 0);
}
std::cout << EK() << '\n';
//solve3
memset(head, -1, sizeof head), idx = 0;
for (int i = 0; i < m; i ++) add(s, id[0][i], 1, 0);
for (int i = 0; i < m + n - 1; i ++) add(id[n - 1][i] + tot, t, 1e9, 0);
for (int i = 0; i < n; i ++)
for (int j = 0; j < m + i; j ++) {
add(id[i][j], id[i][j] + tot, 1e9, nums[i][j]);
if (i < n - 1)
add(id[i][j] + tot, id[i + 1][j], 1e9, 0),
add(id[i][j] + tot, id[i + 1][j + 1], 1e9, 0);
}
std::cout << EK() << '\n';
return 0;
}
2.5 K取方格数
原题链接
思路分析
考虑最大费用最大流 + 拆点
由于每个格子走一次,我们将格子拆为入点和出点
左和上向入点连容量无穷费用0的边
入点向出点连一条容量为1,费用为grid[i][j] 的边,再连一条容量为无穷,费用为0的边
源点向(0, 0) 连 容量无穷,费用0的边
(n - 1, n - 1) 向汇点连 容量无穷,费用0的边
AC代码
c++
#include <bits/stdc++.h>
// #include <ranges>
// #define DEBUG
using i64 = long long;
using u32 = unsigned;
using u64 = unsigned long long;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1E18 + 7;
constexpr double eps = 1E-9;
using i64 = long long;
const int N = 5005, M = 25050;
int s, t;
int head[N], idx;
auto clear = [] {
memset(head, -1, sizeof head);
return 0;
}();
struct edge {
int v, c, w, nxt;
}edges[M];
void addedge(int u, int v, int c, int w) {
edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
}
void add(int u, int v, int c, int w) {
addedge(u, v, c, w);
addedge(v, u, 0, -w);
}
int dist[N], pre[N], q[N], incf[N];
bool vis[N];
bool spfa() {
memset(dist, -0x3f, sizeof dist);
memset(incf, 0, sizeof incf);
int f = 0, b = 0;
dist[q[b ++] = s] = 0;
incf[s] = inf32;
while (b - f) {
int u = q[f ++];
f %= N;
vis[u] = false;
for (int i = head[u]; ~i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].c > 0 && dist[u] + edges[i].w > dist[v]) {
dist[v] = dist[u] + edges[i].w;
incf[v] = std::min(edges[i].c, incf[u]);
pre[v] = i;
if (!vis[v]) {
vis[q[b ++] = v] = true;
b %= N;
}
}
}
}
return incf[t] > 0;
}
void update(int &f, int &c) {
for (int v = t; v != s; v = edges[pre[v] ^ 1].v) {
edges[pre[v]].c -= incf[t];
edges[pre[v] ^ 1].c += incf[t];
}
f += incf[t];
c += dist[t] * incf[t];
}
std::pair<int, int> EK() {
int f = 0, c = 0;
while (spfa()) {
update(f, c);
}
return std::pair(f, c);
}
void solve() {
int n, k;
std::cin >> n >> k;
std::vector<std::vector<int>> g(n, std::vector<int>(n));
s = n * n * 2, t = n * n * 2 + 1;
for (int i = 0; i < n; ++ i)
for (int j = 0; j < n; ++ j) {
std::cin >> g[i][j];
int u = i * n + j, v = u + n;
if (i) {
add((i - 1) * n + j + n * n, u, inf32, 0);
}
if (j) {
add(i * n + j - 1 + n * n, u, inf32, 0);
}
add(i * n + j, i * n + j + n * n, inf32, 0);
add(i * n + j, i * n + j + n * n, 1, g[i][j]);
}
add(s, 0, k, 0);
add(t - 2, t, k, 0);
std::cout << EK().second;
}
auto FIO = []{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
return 0;
} ();
int main() {
#ifdef DEBUG
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
int t = 1;
// std::cin >> t;
while (t --)
solve();
return 0;
}
2.6 P4012 深海机器人问题
原题链接
P4012 深海机器人问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
和上一道题一样,这个题思路也是拆点,但是实际建图的时候不用拆点,因为这个题的图比较特殊,输入也比较逆天
AC代码
c++
#include <bits/stdc++.h>
// #include <ranges>
// #define DEBUG
using i64 = long long;
using u32 = unsigned;
using u64 = unsigned long long;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1E18 + 7;
constexpr double eps = 1E-9;
const int N = 500, M = 2005;
int s, t;
int head[N], idx;
auto clear = [] {
memset(head, -1, sizeof head);
return 0;
}();
struct edge {
int v, c, w, nxt;
}edges[M << 1];
void addedge(int u, int v, int c, int w) {
edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
}
void add(int u, int v, int c, int w) {
addedge(u, v, c, w);
addedge(v, u, 0, -w);
}
int dist[N], pre[N], q[N], incf[N];
bool vis[N];
bool spfa() {
memset(dist, -0x3f, sizeof dist);
memset(incf, 0, sizeof incf);
int f = 0, b = 0;
dist[q[b ++] = s] = 0;
incf[s] = inf32;
while (b - f) {
int u = q[f ++];
f %= N;
vis[u] = false;
for (int i = head[u]; ~i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].c > 0 && dist[u] + edges[i].w > dist[v]) {
dist[v] = dist[u] + edges[i].w;
incf[v] = std::min(edges[i].c, incf[u]);
pre[v] = i;
if (!vis[v]) {
vis[q[b ++] = v] = true;
b %= N;
}
}
}
}
return incf[t] > 0;
}
void update(int &f, int &c) {
for (int v = t; v != s; v = edges[pre[v] ^ 1].v) {
edges[pre[v]].c -= incf[t];
edges[pre[v] ^ 1].c += incf[t];
}
f += incf[t];
c += dist[t] * incf[t];
}
std::pair<int, int> EK() {
int f = 0, c = 0;
while (spfa()) {
update(f, c);
}
return std::pair(f, c);
}
void solve() {
int a, b;
std::cin >> a >> b;
int P, Q;
std::cin >> P >> Q;
s = (P + 1) * (Q + 1), t = s + 1;
for (int i = 0, x; i <= P; ++ i)
for (int j = 0; j < Q; ++ j) {
std::cin >> x;
add(i * (Q + 1) + j, i * (Q + 1) + j + 1, 1, x);
add(i * (Q + 1) + j, i * (Q + 1) + j + 1, inf32, 0);
}
for (int j = 0, x; j <= Q; ++ j)
for (int i = 0; i < P; ++ i) {
std::cin >> x;
add(i * (Q + 1) + j, (i + 1) * (Q + 1) + j, 1, x);
add(i * (Q + 1) + j, (i + 1) * (Q + 1) + j, inf32, 0);
}
for (int i = 0, k, x, y; i < a; ++ i) {
std::cin >> k >> x >> y;
add(s, x * (Q + 1) + y, k, 0);
}
for (int i = 0, k, x, y; i < b; ++ i) {
std::cin >> k >> x >> y;
add(x * (Q + 1) + y, t, k, 0);
}
std::cout << EK().second;
}
auto FIO = []{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
return 0;
} ();
int main() {
#ifdef DEBUG
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
int t = 1;
// std::cin >> t;
while (t --)
solve();
return 0;
}
2.7 餐巾计划问题
原题链接
P1251 餐巾计划问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
考虑每天拆成两个点:使用的r[i] 条 干净毛巾(可能是新的可能是旧的)记为入点,当天结束后产生的 r[i] 条旧毛巾,记为出点
- 出点向汇点连容量为r[i],费用为0的边
- 汇点向入点连容量为r[i],费用为0的边,即每天一定会产生的旧毛巾
- 汇点向出点连容量为r[i],费用为p的边,即每天可以买新毛巾
- 入点向m天后的出点连容量为无穷,费用为 f 的边,即第m + 1天就可以用快洗的毛巾
- 入点向n天后的出点连容量为无穷,费用为 s 的边,即第m + 1天就可以用慢洗的毛巾
- 入点向下一天的入点连容量为无穷,费用为0的边,即可以存到下一天
建模后一定存在满流,一点流量用一次,不存在毛巾被非法使用,所以一定对应原题一个可行,合法的解
由于是最小费用流,所以是最优解
AC代码
c++
#include <bits/stdc++.h>
// #include <ranges>
// #define DEBUG
using i64 = long long;
using u32 = unsigned;
using u64 = unsigned long long;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1E18 + 7;
constexpr double eps = 1E-9;
constexpr int N = 4002, M = 25005;
int head[N], idx;
auto clear = []{
memset(head, -1, sizeof head);
return 0;
}();
struct edge{
int v, c, w, nxt;
} edges[M];
void addedge(int u, int v, int c, int w) {
edges[idx] = { v, c, w, head[u] }, head[u] = idx ++;
assert(idx < M);
}
void add(int u, int v, int c, int w) {
addedge(u, v, c, w);
addedge(v, u, 0, -w);
}
int s, t;
int pre[N], incf[N], q[N];
i64 dist[N];
bool vis[N];
bool spfa() {
memset(dist, 0x3f, sizeof dist);
memset(incf, 0, sizeof incf);
int b = 0, f = 0;
dist[q[b ++] = s] = 0;
incf[s] = inf32;
while (b - f) {
int u = q[f ++];
f %= N;
vis[u] = false;
for (int i = head[u]; ~i; i = edges[i].nxt) {
int v = edges[i].v, c = edges[i].c, w = edges[i].w;
if (c > 0 && dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
incf[v] = std::min(incf[u], c);
pre[v] = i;
if (!vis[v]) {
vis[q[b ++] = v] = true;
b %= N;
}
}
}
}
return incf[t] > 0;
}
void update(int &f, i64 &c) {
for (int v = t; v != s; v = edges[pre[v] ^ 1].v) {
edges[pre[v]].c -= incf[t];
edges[pre[v] ^ 1].c += incf[t];
}
f += incf[t];
c += incf[t] * dist[t];
}
std::pair<int, i64> EK() {
int f = 0;
i64 c = 0;
while (spfa())
update(f, c);
return std::make_pair(f, c);
}
void solve() {
int n;
std::cin >> n;
s = 2 * n, t = 2 * n + 1;
std::vector<int> r(n);
for (int i = 0; i < n; ++ i) {
std::cin >> r[i];
add(i + n, t, r[i], 0);
add(s, i, r[i], 0);
if (i + 1 < n)
add(i, i + 1, inf32, 0);
}
int a, b, c, d, e; // p m f n s
std::cin >> a >> b >> c >> d >> e;
for (int i = 0; i < n; ++ i) {
if (i + b < n) // 快洗
add(i, i + b + n, inf32, c);
if (i + d < n) // 慢洗
add(i, i + d + n, inf32, e);
add(s, i + n, inf32, a);
}
std::cout << EK().second;
}
auto FIO = []{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
return 0;
} ();
int main() {
#ifdef DEBUG
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
int t = 1;
// std::cin >> t;
while (t --)
solve();
return 0;
}
2.8 [NOI2008] 志愿者招募
原题链接
[P3980 NOI2008] 志愿者招募 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
洛谷上费用流的题解感觉很多都没说到点上
首先:所有种类志愿者的服务区间并集是[1, n],保证一定有解
我们网络流建模一定要保证 建模后的可行流/满流 能对应原问题的一个可行解
我也不会正面解释建模方式正确性,我是将原问题转换了一下:
转换为初始有 inf 个人工作,保证每一天都至少有 a[i] 个人休息,现在有若干休息方案:在[s[i], t[i]] 休息,一个人的花费为c[i]
问满足限制的最小花费
这样建模就好解释了
有 inf 个人要从 虚拟源点 s 到 虚拟汇点 t,中间有[0, n] 这 n + 1个点,从点i 到 i + 1,代表在第 i 天工作
为了保证每天都至少有a[i]个人休息 ,我们让工作的边,即 <i, i + 1> 边的容量为 inf - a[i],费用为0 ,即当天最多有这么多人工作
然后对于 休息方案 :[s[i], t[i]],我们连接<s[i], t[i] + 1> ,边容量为inf,费用为c[i],即想休息多少休息多少,但是休息一个人费用为c[i]
我们跑最小费用最大流就是答案
证明:
- 一定满流,如果不满流说明有员工消失了(
- 因为区间并集为[0, n],所以一定有0开始的工作区间[s[0], t[0]]
- 从 0 开始,源点向0连了inf容量的边,工作边能流inf - a[0],休息边能流a[0],所以此处满流,只需证明这inf流量能顺利流到汇点t
- 工作边的a0 点流量流到t[0] + 1后,因为区间并集为[0, n],所以t[0] + 1以及其左边一定有休息区间起点,那么这a[0] 点流量可以兵分两路,一部分往右边走工作边和休息边,一部分退流到某个休息区间起点,接着走休息边
- 如此下去......当然可以更严谨地证明每次兵分多路都有足够的容量流a0
- 故我们一定能够得到满流
- 因而满流对应原问题一个可行解,休息边代表区间内休息的人数对应原问题雇佣的人数
- 由于是最小费用流,所以是最优解
AC代码
c++
#include <bits/stdc++.h>
// #include <ranges>
// #define DEBUG
using i64 = long long;
using u32 = unsigned;
using u64 = unsigned long long;
constexpr int inf32 = 1E9 + 7;
constexpr i64 inf64 = 1E18 + 7;
constexpr double eps = 1E-9;
constexpr int N = 1003, M = 22010;
struct edge{
int v, c, w, nxt;
} edges[M];
int n, m, s, t;
int head[N], q[N], dst[N], incf[N], pre[N], idx;
bool vis[N];
void addedge (int u, int v, int c, int w) {
edges[idx] = { v, c, w, head[u] }, head[u] = idx ++ ;
}
void add(int u, int v, int c, int w) {
addedge(u, v, c, w), addedge(v, u, 0, -w);
}
bool spfa() {
memset(incf, 0, sizeof incf), memset(dst, 0x3f, sizeof dst);
int f = 0, b = 0;
dst[q[b ++] = s] = 0, incf[s] = inf32, vis[s] = true;
while (b - f) {
int u = q[f ++ ];
vis[u] = false;
f %= N;
for (int i = head[u]; ~i; i = edges[i].nxt) {
int v = edges[i].v;
if (edges[i].c && dst[v] > dst[u] + edges[i].w) {
dst[v] = dst[u] + edges[i].w;
incf[v] = std::min(incf[u], edges[i].c);
pre[v] = i;
if (vis[v]) continue;
vis[q[b ++] = v] = true;
b %= N;
}
}
}
return incf[t];
}
void update(int& f, int& c) {
for (int v = t; v != s; ) {
int i = pre[v];
edges[i].c -= incf[t], edges[i ^ 1].c += incf[t];
v = edges[i ^ 1].v;
}
f += incf[t], c += dst[t] * incf[t];
}
int EK() {
int f = 0, c = 0;
while (spfa())
update(f, c);
return c;
}
void solve() {
int n, m;
std::cin >> n >> m;
memset(head, -1, sizeof head);
s = n + 1, t = n + 2;
for (int i = 0, x; i < n; ++ i) {
std::cin >> x;
add(i, i + 1, inf32 - x, 0);
}
for (int i = 0, a, b, c; i < m; ++ i) {
std::cin >> a >> b >> c;
add(a - 1, b, inf32, c);
}
add(s, 0, inf32, 0);
add(n, t, inf32, 0);
std::cout << EK();
}
auto FIO = []{
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
return 0;
} ();
int main() {
#ifdef DEBUG
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
#endif
int t = 1;
// std::cin >> t;
while (t --)
solve();
return 0;
}
三、(补档)Primal Dual 算法
2024-08-09 16:20:21,现在很少见到网络流的题目其实,今天偶然间发现原来费用流除了spfa增广外和消圈法外,还有很多方法,这里更新一下Primal Dual 算法,多的也不想学了,学习算法毕竟还是为了训练思维。
3.1 定义
3.1.1 势
势 指的是给每个顶点赋予一个新的标号 h(v)。
其作用为 :对于原图的边<u, v, c, w>(c为容量,w为花费),将 h[u] - h[v] 累加到 w 上后,保证 w' = w + h[u] - h[v] >= 0 ,这样,我们的spfa 就可以用 堆优化 djkstra替代,在大多数图中,有着比spfa更为稳定的效率
缺陷:仍未解决负环问题, 但是我也没精力学习消圈法了
3.2 实现
3.2.1 势的求解
对于所有边都 >=0的图,我们直接令 h[] = 0即可
对于存在负权边,但是不存在负环的图,我们跑 spfa算法 求解从源点 s 到所有点的最短距离
令h[u] = mindist(s, u)
则 对于边 <u, v, c, w>,h[u] + w >= h[v] => w + h[u] - h[v] >=0 ,就完成了构造
3.2.2 代码实现
因为第三部分和前两部分时间差了3、4个月,所以码风有些改变()
c++
using i64 = long long;
struct MCFGraph{
struct Edge{
int v, cap, w;
Edge(int _v, int _cap, int _w) : v(_v), cap(_cap), w(_w) {}
};
const int n;
std::vector<Edge> e;
std::vector<std::vector<int>> g;
std::vector<i64> h, dis; // h 为 势 数组
std::vector<int> pre;
std::vector<bool> vis;
void spfa(const int s, const int t) {
static std::queue<int> q;
h.assign(n, std::numeric_limits<i64>::max());
q.push(s);
h[s] = 0;
while (q.size()) {
int u = q.front();
q.pop();
vis[u] = false;
for (int i : g[u]) {
const auto& [v, cap, w] = e[i];
if (cap > 0 && h[v] > h[u] + w) {
h[v] = h[u] + w;
if (!vis[v])
q.push(v), vis[v] = true;
}
}
}
}
bool dijkstra(const int s, const int t){
static std::priority_queue<std::pair<i64, int>, std::vector<std::pair<i64, int>>, std::greater<std::pair<i64, int>>> pq;
dis.assign(n, std::numeric_limits<i64>::max());
pre.assign(n, -1);
dis[s] = 0;
pq.emplace(0, s);
while (pq.size()) {
auto [d, u] = pq.top();
pq.pop();
if (dis[u] < d) continue;
for (int i : g[u]) {
const auto& [v, cap, w] = e[i];
if (cap > 0 && dis[v] > w + d + h[u] - h[v]) {
dis[v] = w + d + h[u] - h[v];
pre[v] = i;
pq.emplace(dis[v], v);
}
}
}
return dis[t] < std::numeric_limits<i64>::max();
}
MCFGraph(int _n) : n(_n), g(n), vis(n)
{}
void addEdge(int u, int v, int c, int f) { // 最大流
g[u].push_back(e.size());
e.emplace_back(v, c, f);
g[v].push_back(e.size());
e.emplace_back(u, 0, -f);
}
std::pair<int, i64> flow(const int s, const int t) {
int flow = 0;
i64 cost = 0;
h.assign(n, 0);
spfa(s, t);
while (dijkstra(s, t)) {
// 更新h 为实际dis
for (int i = 0; i < n; ++ i) h[i] += dis[i];
int aug = std::numeric_limits<int>::max();
for (int i = t; i != s; i = e[pre[i] ^ 1].v)
aug = std::min(aug, e[pre[i]].cap);
for (int i = t; i != s; i = e[pre[i] ^ 1].v)
e[pre[i]].cap -= aug, e[pre[i] ^ 1].cap += aug;
flow += aug;
cost += (i64)aug * h[t];
}
return std::make_pair(flow, cost);
}
};
3.2.3 时间复杂度
O(NM + (N + M) logN)