网络流学习记录
跟开了森林书一样。
最大流
概念
有一个有源点
有点抽象,举个例子:
这里有一个源点为
首先,我们走
其次,我们走
最后,我们走
这种方案是最优的(之一)。故该网络的最大流为
解法
Ford–Fulkerson 方法
这是【贪心】算法在网络流上的总称。
我们首先考虑以下贪心:有路径可流,就往下流。
但是这显然不正确。有图如下:
有源点为
但是我们可以沿着路径
这说明直接贪心是错误的。
我们考虑反悔贪心。
我们引入【退流】操作。对于每条边,建立反向边,初始容量为
当一条边(也可以是某条正向边的反向边)被流经时,若流了
这样,我们就可以以【错误】的贪心顺序求得正确的答案。
我们考虑这样的图(来自 OI Wiki):
专家发现【退流】操作相当于反悔。这样做是对的。
如果直接这样做,时间复杂度如何?
我们先给出几个定义:
- 剩余容量:一条边的容量减去实际流量。
- 增广路:一条从
到 的路径,路径上边的剩余容量最小值大于 ,即对答案有贡献。 - 残量网络:当前的图中,所有剩余容量大于
的边(包括正向边和反向边)和所有点的集合。
记
每轮增广的时间复杂度显然是
极端情况下,有图:
可能以
总复杂度是和值域
于是要优化。
Edmonds-Karp 算法
我们考虑用 BFS 实现 FF 方法。具体地,每轮用 BFS 搜出一条增广路,并将其加入答案。每轮 BFS 的时间复杂度是 专家可以证明总的增广轮数是不超过
Dinic 算法
我们专家思考 EK 算法的瓶颈。增广时,BFS 是乱搜的,如果我们以一定的顺序增广,可能获得更优的时间复杂度。
在增广前用一遍 BFS 建出(无需显式)最短路径图(即以
有两个优化需要注意:
- 当前弧优化:每次我们维护搜到结点
的出边表中第一条【值得尝试】的边 。【值得尝试】指该边的剩余容量大于 ,且它没有被增广路走过(如果走过,那么它的下游仍没有机会增广)。这是保证 Dinic 复杂度的优化,让其不退化至 (?)。
值得一提的是,我们的 指针必须要指向第一个值得尝试的边,不能指向第二条或更后,否则复杂度会假。常见的错误是判 写错位置,一定要在 更新前判断。 - 多路增广:我们在一轮增广时,可以不仅搜出一条增广路,可以在某处寻找一个岔路进行继续增广,不立即从头再来。这是 Dinic 的第一个常数优化。
总时间复杂度
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 202, M = 5002;
struct Graph {
int n, m, s, t, tot;
int head[N], cur[N];
struct edge {
int v, w, next;
edge() { }
edge(int a, int b, int c) : v(a), w(b), next(c) { }
} e[M << 1];
void add_edge(int u, int v, int w) {
e[tot] = edge(v, w, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, head[v]), head[v] = tot++;
}
void init() {
cin >> n >> m >> s >> t;
tot = 0;
memset(head, -1, sizeof(head)); //如果tot=0
for(int i = 1, u, v, w; i <= m; ++i) {
cin >> u >> v >> w;
add_edge(u, v, w);
}
}
queue<int> q;
int dis[N];
int BFS() { //在残量网络中打出层次图
for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
queue<int>().swap(q);
q.push(s);
dis[s] = 0;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(e[i].w > 0 && dis[v] > 1e18) {
q.push(v);
dis[v] = dis[u] + 1;
if(v == t) return 1; //常数优化
}
}
}
return 0;
}
int DFS(int u, int sum = Linf) { //sum:当前流量
if(u == t) return sum;
int res = 0;
for(int i = cur[u]; ~i && sum > 0; i = e[i].next) { //多路增广
cur[u] = i;
int v = e[i].v;
if(e[i].w > 0 && dis[v] == dis[u] + 1) {
int k = DFS(v, min(sum, e[i].w));
if(k == 0) dis[v] = Linf;
e[i].w -= k, e[i ^ 1].w += k;
res += k, sum -= k;
}
}
return res;
}
int Dinic() {
int ans = 0;
while(BFS()) ans += DFS(s);
return ans;
}
} G;
void main() {
G.init();
cout << G.Dinic();
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
例题
二分图最大匹配
我们新建立一个超级源点
这时,跑从
luogu P3386 【模板】二分图最大匹配
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 1005, M = 5e4+5;
struct Graph {
int n, n1, n2, m, s, t, tot;
set<pii> mark;
int head[N], cur[N];
struct edge {
int v, w, next;
edge() { }
edge(int a, int b, int c) : v(a), w(b), next(c) { }
} e[M << 1];
void add_edge(int u, int v, int w) {
e[tot] = edge(v, w, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, head[v]), head[v] = tot++;
}
void init() {
cin >> n1 >> n2 >> m;
tot = 0;
memset(head, -1, sizeof(head));
for(int i = 1, u, v; i <= m; ++i) {
cin >> u >> v;
if(mark.count(pii(u, v))) continue;
mark.insert(pii(u, v));
add_edge(u, v+n1, 1);
}
s = n1+n2+1, t = n1+n2+2;
for(int i = 1; i <= n1; ++i) add_edge(s, i, 1);
for(int i = n1+1; i <= n1+n2; ++i) add_edge(i, t, 1);
n = n1+n2+2;
}
queue<int> q;
int dis[N];
int BFS() {
for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
queue<int>().swap(q);
q.push(s);
dis[s] = 0;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(e[i].w > 0 && dis[v] > 1e18) {
q.push(v);
dis[v] = dis[u] + 1;
if(v == t) return 1;
}
}
}
return 0;
}
int DFS(int u, int sum = Linf) {
if(u == t) return sum;
int res = 0;
for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
cur[u] = i;
int v = e[i].v;
if(e[i].w > 0 && dis[v] == dis[u] + 1) {
int k = DFS(v, min(sum, e[i].w));
if(k == 0) dis[v] = Linf;
e[i].w -= k, e[i ^ 1].w += k;
res += k, sum -= k;
}
}
return res;
}
int Dinic() {
int ans = 0;
while(BFS()) ans += DFS(s);
return ans;
}
} G;
void main() {
G.init();
cout << G.Dinic();
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
如果要输出匹配边,看看哪些左右点之间的边
luogu P2763 试题库问题
若每个类型要出
超级源点连所有试题,所有拆点后的类型连超级汇点,容量均为
我们发现它实质上就是二分图最大匹配。
输出方案稍微处理一下即可。
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 1e6+2, M = 1e6+5;
struct Graph {
int k, n, n1, m, s, t, tot;
vector<int> vec[N];
int head[N], cur[N];
struct edge {
int v, w, next;
edge() { }
edge(int a, int b, int c) : v(a), w(b), next(c) { }
} e[M << 1];
void add_edge(int u, int v, int w) {
e[tot] = edge(v, w, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, head[v]), head[v] = tot++;
}
void init() {
cin >> k >> n;
n1 = n;
tot = 0, memset(head, -1, sizeof(head));
for(int i = 1, x; i <= k; ++i) {
cin >> x;
for(int j = m+1; j <= m+x; ++j) vec[i].push_back(j);
m += x;
}
for(int i = 1, p; i <= n; ++i) {
cin >> p;
for(int j = 1, x; j <= p; ++j) {
cin >> x;
for(auto ele : vec[x]) add_edge(i, ele + n, 1);
}
}
s = n+m+1, t = n+m+2;
for(int i = 1; i <= n; ++i) add_edge(s, i, 1);
for(int i = n+1; i <= n+m; ++i) add_edge(i, t, 1);
n += m+2;
}
queue<int> q;
int dis[N];
int BFS() {
for(int i = 1; i <= n; ++i) dis[i] = inf, cur[i] = head[i];
queue<int>().swap(q);
q.push(s);
dis[s] = 0;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(e[i].w > 0 && dis[v] > 1e9) {
q.push(v);
dis[v] = dis[u] + 1;
if(v == t) return 1;
}
}
}
return 0;
}
int DFS(int u, int sum = inf) {
if(u == t) return sum;
int res = 0;
for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
cur[u] = i;
int v = e[i].v;
if(e[i].w > 0 && dis[v] == dis[u] + 1) {
int k = DFS(v, min(sum, e[i].w));
if(k == 0) dis[v] = inf;
e[i].w -= k, e[i ^ 1].w += k;
res += k, sum -= k;
}
}
return res;
}
int Dinic() {
int ans = 0;
while(BFS()) ans += DFS(s);
return ans;
}
vector<int> ans[N];
void solve() {
int x = Dinic();
if(x < m) return puts("No Solution!"), void();
for(int i = 1; i <= n1; ++i)
for(int j = head[i]; ~j; j = e[j].next)
if(e[j].v != s && e[j].w == 0) ans[e[j].v].push_back(i);
for(int i = 1; i <= k; ++i) {
cout << i << ": ";
for(auto j : vec[i])
for(auto ele : ans[j + n1]) cout << ele << ' ';
puts("");
}
}
} G;
void main() {
G.init();
G.solve();
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
luogu P3425 [POI 2005] KOS-Dicing
我们将源点
我们思考这个容量的实际意义。该边的实际意义就是每个人最多有几个胜场。又注意到题目要求最大值最小,故考虑二分这个容量
至于 check 怎么写,只需要看看最大流是不是(大于等于)等于总游戏次数
输出方案也很简单,看看每个比赛连哪个人的边满流即可。
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 2e4+5, M = 4e4+5;
struct game {
int u, v, w;
game() { }
game(int u, int v) : u(u), v(v) { }
} a[M];
int n, m;
struct Graph {
int n, m, s, t, tot;
int head[N], cur[N];
struct edge {
int v, w, next;
edge() { }
edge(int a, int b, int c) : v(a), w(b), next(c) { }
} e[M << 1];
void add_edge(int u, int v, int w) {
e[tot] = edge(v, w, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, head[v]), head[v] = tot++;
}
void init(int n, int m, int l) {
this->m = m;
s = n+m+1, t = n+m+2;
tot = 0, memset(head, -1, sizeof(head));
for(int i = 1; i <= m; ++i) {
add_edge(s, i, 1);
add_edge(i, a[i].u + m, 1), add_edge(i, a[i].v + m, 1);
}
for(int i = 1; i <= n; ++i) add_edge(i + m, t, l);
this->n = n+m+2;
}
queue<int> q;
int dis[N];
int BFS() { //在残量网络中打出层次图
for(int i = 1; i <= n; ++i) dis[i] = inf, cur[i] = head[i];
queue<int>().swap(q);
q.push(s);
dis[s] = 0;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(e[i].w > 0 && dis[v] > 1e9) {
q.push(v);
dis[v] = dis[u] + 1;
if(v == t) return 1;
}
}
}
return 0;
}
int DFS(int u, int sum = inf) {
if(u == t) return sum;
int res = 0;
for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
cur[u] = i;
int v = e[i].v;
if(e[i].w > 0 && dis[v] == dis[u] + 1) {
int k = DFS(v, min(sum, e[i].w));
if(k == 0) dis[v] = inf;
e[i].w -= k, e[i ^ 1].w += k;
res += k, sum -= k;
}
}
return res;
}
int Dinic(int op = 0) {
int ans = 0;
while(BFS()) ans += DFS(s);
if(op) {
for(int i = 1; i <= m; ++i)
cout << (e[head[i]].v == a[i].u ^ e[head[i]].w == 1) << '\n';
}
return ans;
}
} G;
bool check(int l) {
G.init(n, m, l);
return G.Dinic() == m;
}
void print(int l) { G.init(n, m, l), G.Dinic(1); }
void main() {
cin >> n >> m;
for(int i = 1; i <= m; ++i) cin >> a[i].u >> a[i].v;
int L = 1, R = n;
while(L < R) {
int mid = L + R >> 1;
if(check(mid)) R = mid;
else L = mid+1;
}
cout << L << '\n';
print(L);
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
luogu P2766 最长不下降子序列问题
第一问是 naive 的 DP,设
第二问我们考虑怎么建图。所谓【取出】即【不相交】,有点类似匹配。
记 LIS 的长度为
我们将源点
输出最大流即可。
这么连边显然是正确的,每一条
第三问只需要在第二问的基础上把
注意这一问要特判 但是输出无穷大也有道理?
套路
- 转化成二分图匹配,或类似问题。
- 保证一个点只经过一次,可以拆点。
最小割
概念
- 割:一个源点为
,汇点为 的网络 ,将 划分成 和 两部分,其中 。那么 称为 的一个 割。 - 割边:一条边
,满足 ,则该边称为割边。 - 割的容量(大小):即所有割边的容量之和。
解法
对于任意图,有最小割等于最大流。
- 输出最小割的割边:我们从
开始 DFS,只走残量网络中的边,能到达的所有结点的集合就是 。接下来在原图中枚举每个 中点的出边,到达的点不属于 的就是割边。- 一定不能直接输出所有满流边,因为有图:
都是满流边,显然不对。
- 一定不能直接输出所有满流边,因为有图:
例题
luogu P5934 [清华集训 2012] 最小生成树
我们考虑最小生成树的过程,即 Kruskal。如果从小到大枚举边,当前加过的边构成的图中,
故权为
于是我们在【小于
Q:相加即是对的呢?
A:因为两个子图的边集没有交集,割掉的边不会有重复。
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 3.2e5+2, M = 6.4e6+2;
struct Graph {
int n, s, t, tot;
int head[N], cur[N];
struct edge {
int v, w, next;
edge() { }
edge(int a, int b, int c) : v(a), w(b), next(c) { }
} e[M << 1];
void add_edge(int u, int v, int w) {
e[tot] = edge(v, w, head[u]), head[u] = tot++;
e[tot] = edge(u, w, head[v]), head[v] = tot++;
}
void init(int n, int s, int t) {
this->n = n, this->s = s, this->t = t;
tot = 0, memset(head, -1, sizeof(head));
}
queue<int> q;
int dis[N];
int BFS() {
for(int i = 1; i <= n; ++i) dis[i] = inf, cur[i] = head[i];
queue<int>().swap(q);
q.push(s);
dis[s] = 0;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(e[i].w > 0 && dis[v] > 1e9) {
q.push(v);
dis[v] = dis[u] + 1;
if(v == t) return 1;
}
}
}
return 0;
}
int DFS(int u, int sum = inf) {
if(u == t) return sum;
int res = 0;
for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
cur[u] = i;
int v = e[i].v;
if(e[i].w > 0 && dis[v] == dis[u] + 1) {
int k = DFS(v, min(sum, e[i].w));
if(k == 0) dis[v] = inf;
e[i].w -= k, e[i ^ 1].w += k;
res += k, sum -= k;
}
}
return res;
}
int Dinic() {
int ans = 0;
while(BFS()) ans += DFS(s);
return ans;
}
} G1, G2;
int n, m;
int u[N], v[N], w[N];
int s, t, L;
void main() {
cin >> n >> m;
for(int i = 1; i <= m; ++i) cin >> u[i] >> v[i] >> w[i];
cin >> s >> t >> L;
G1.init(n, s, t), G2.init(n, s, t);
for(int i = 1; i <= m; ++i) {
if(w[i] < L) G1.add_edge(u[i], v[i], 1);
else if(w[i] > L) G2.add_edge(u[i], v[i], 1);
}
cout << G1.Dinic() + G2.Dinic();
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
Atcoder ABC239G Builder Takahashi
这题的最小割再显然不过了。但是这道题是点权。
有一个拆点的技巧:将每个点分为入点和出点。原图中的边正常连,权值设为
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define int long long
#define all(v) v.begin(), v.end()
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 1002, M = 1e6+2;
struct Graph {
int n, m, s, t, tot;
int head[N], cur[N];
struct edge {
int v, w, next;
edge() { }
edge(int a, int b, int c) : v(a), w(b), next(c) { }
} e[M << 1];
void add_edge(int u, int v, int w) {
e[tot] = edge(v, w, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, head[v]), head[v] = tot++;
}
void init() {
cin >> n >> m;
s = n+1, t = n;
tot = 0;
memset(head, -1, sizeof(head));
for(int i = 1, u, v; i <= m; ++i) {
cin >> u >> v;
add_edge(u+n, v, Linf), add_edge(v+n, u, Linf);
}
for(int i = 1, c; i <= n; ++i) {
cin >> c;
add_edge(i, i+n, c);
}
n *= 2;
}
queue<int> q;
int dis[N];
int BFS() {
for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
queue<int>().swap(q);
q.push(s);
dis[s] = 0;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(e[i].w > 0 && dis[v] > 1e18) {
q.push(v);
dis[v] = dis[u] + 1;
if(v == t) return 1;
}
}
}
return 0;
}
int DFS(int u, int sum = Linf) {
if(u == t) return sum;
int res = 0;
for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
cur[u] = i;
int v = e[i].v;
if(e[i].w > 0 && dis[v] == dis[u] + 1) {
int k = DFS(v, min(sum, e[i].w));
if(k == 0) dis[v] = Linf;
e[i].w -= k, e[i ^ 1].w += k;
res += k, sum -= k;
}
}
return res;
}
int Dinic() {
int ans = 0;
while(BFS()) ans += DFS(s);
return ans;
}
vector<int> ans;
int vis[N];
void mark(int u) {
vis[u] = 1;
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(!vis[v] && e[i].w) mark(v);
}
}
void print() {
mark(s);
for(int i = 1; i <= n; ++i)
if(i <= n/2 && vis[i] && !vis[i + n/2]) ans.push_back(i);
cout << ans.size() << '\n';
for(auto i : ans) cout << i << ' ';
}
} G;
void main() {
G.init();
cout << G.Dinic() << '\n';
G.print();
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
luogu P2774 方格取数问题
一个常用的策略是,最大收益等于总收益减去最小损失。
如果直接建无向网格图的话,既不能体现点权,也不能体现相邻不能同时选。
我们考虑如下建图:
记点
我们建出源点
最小割就是最小损失。
考虑这么做为什么是对的。
显然,因为是最小割,故不可能割掉白点与黑点之间的边。
割掉一条源点与白点之间的边就代表白点不选,黑点同理。
那么得到的最小割,就不存在
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 1e4+5, M = 1e6+2;
struct Graph {
int n, m, s, t, tot, sum;
const int dir[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
int head[N], cur[N];
struct edge {
int v, w, next;
edge() { }
edge(int a, int b, int c) : v(a), w(b), next(c) { }
} e[M << 1];
void add_edge(int u, int v, int w) {
e[tot] = edge(v, w, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, head[v]), head[v] = tot++;
}
void init() {
cin >> n >> m;
s = n*m+1, t = n*m+2;
tot = 0;
memset(head, -1, sizeof(head));
for(int i = 1; i <= n; ++i)
for(int j = 1, a; j <= m; ++j) {
scanf("%lld", &a), sum += a;
if(i + j & 1) add_edge((i-1) * m + j, t, a);
else {
for(int o = 0; o < 4; ++o) {
int nx = i + dir[o][0], ny = j + dir[o][1];
if(nx < 1 || nx > n || ny < 1 || ny > m) continue;
add_edge((i-1) * m + j, (nx-1) * m + ny, Linf);
}
add_edge(s, (i-1) * m + j, a);
}
}
n = n * m + 2;
}
queue<int> q;
int dis[N];
int BFS() {
for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
queue<int>().swap(q);
q.push(s);
dis[s] = 0;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(e[i].w > 0 && dis[v] > 1e18) {
q.push(v);
dis[v] = dis[u] + 1;
if(v == t) return 1;
}
}
}
return 0;
}
int DFS(int u, int sum = Linf) {
if(u == t) return sum;
int res = 0;
for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
cur[u] = i;
int v = e[i].v;
if(e[i].w > 0 && dis[v] == dis[u] + 1) {
int k = DFS(v, min(sum, e[i].w));
if(k == 0) dis[v] = Linf;
e[i].w -= k, e[i ^ 1].w += k;
res += k, sum -= k;
}
}
return res;
}
int Dinic() {
int ans = 0;
while(BFS()) ans += DFS(s);
return ans;
}
int solve() { return sum - Dinic(); }
} G;
void main() {
G.init();
cout << G.solve();
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
luogu P1646 [国家集训队] happiness
仍然考虑最大收益为总收益减去最小损失。原因:我们直接做无法保证邻座同科的额外收益。
对于单人的文理选择,这是一个二者选其一,故我们直接考虑源点向当前点连容量为文科收益的边,当前点向汇点连容量为理科收益的边。
对于邻座同科的额外收益,我们发现:
- 只要两个人有一个人选理科,那么邻座同文科的收益就要割掉。
记这两个人为
同时理科同理,改成向汇点连边。
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 1e6+2, M = 2e6+2;
struct Graph {
int n, m, s, t, tot, sum;
int head[N], cur[N];
struct edge {
int v, w, next;
edge() { }
edge(int a, int b, int c) : v(a), w(b), next(c) { }
} e[M << 1];
void add_edge(int u, int v, int w) {
e[tot] = edge(v, w, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, head[v]), head[v] = tot++;
}
void init() {
cin >> n >> m;
int vertices = n * m;
s = ++vertices, t = ++vertices;
tot = 0;
memset(head, -1, sizeof(head));
auto pos = [=] (int i, int j) { return (i-1) * n + j; };
for(int i = 1; i <= n; ++i)
for(int j = 1, x; j <= m; ++j) {
cin >> x, sum += x;
add_edge(s, pos(i, j), x);
}
for(int i = 1; i <= n; ++i)
for(int j = 1, x; j <= m; ++j) {
cin >> x, sum += x;
add_edge(pos(i, j), t, x);
}
for(int i = 1; i < n; ++i)
for(int j = 1, x; j <= m; ++j) {
cin >> x, sum += x;
add_edge(s, ++vertices, x);
add_edge(vertices, pos(i, j), inf);
add_edge(vertices, pos(i+1, j), inf);
}
for(int i = 1; i < n; ++i)
for(int j = 1, x; j <= m; ++j) {
cin >> x, sum += x;
add_edge(++vertices, t, x);
add_edge(pos(i, j), vertices, inf);
add_edge(pos(i+1, j), vertices, inf);
}
for(int i = 1; i <= n; ++i)
for(int j = 1, x; j < m; ++j) {
cin >> x, sum += x;
add_edge(s, ++vertices, x);
add_edge(vertices, pos(i, j), inf);
add_edge(vertices, pos(i, j+1), inf);
}
for(int i = 1; i <= n; ++i)
for(int j = 1, x; j < m; ++j) {
cin >> x, sum += x;
add_edge(++vertices, t, x);
add_edge(pos(i, j), vertices, inf);
add_edge(pos(i, j+1), vertices, inf);
}
n = vertices;
}
queue<int> q;
int dis[N];
int BFS() {
for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
queue<int>().swap(q);
q.push(s);
dis[s] = 0;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(e[i].w > 0 && dis[v] > 1e18) {
q.push(v);
dis[v] = dis[u] + 1;
if(v == t) return 1;
}
}
}
return 0;
}
int DFS(int u, int sum = Linf) {
if(u == t) return sum;
int res = 0;
for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
cur[u] = i;
int v = e[i].v;
if(e[i].w > 0 && dis[v] == dis[u] + 1) {
int k = DFS(v, min(sum, e[i].w));
if(k == 0) dis[v] = Linf;
e[i].w -= k, e[i ^ 1].w += k;
res += k, sum -= k;
}
}
return res;
}
int Dinic() {
int ans = 0;
while(BFS()) ans += DFS(s);
return ans;
}
int solve() { return sum - Dinic(); }
} G;
void main() {
G.init();
cout << G.solve();
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
luogu P2762 太空飞行计划问题
本题仍然有【一个实验所需的仪器集合中有一个不用,那么实验的收益要割掉】的模型。
源点连向实验,代表实验的收益;实验连向仪器,表示依赖关系;仪器连向汇点,不割表示不选,割掉表示选。
总收益减最小割就是答案。
注意读入和方案构造。
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 2002, M = 50002;
struct Graph {
int n, n0, m, s, t, tot, sum;
int head[N], cur[N];
struct edge {
int v, w, next;
edge() { }
edge(int a, int b, int c) : v(a), w(b), next(c) { }
} e[M << 1];
void add_edge(int u, int v, int w) {
e[tot] = edge(v, w, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, head[v]), head[v] = tot++;
}
void readtools(int i) {
char tools[10000];
memset(tools, 0, sizeof(tools));
cin.getline(tools, 10000);
int ulen = 0, tool;
while(sscanf(tools + ulen, "%lld", &tool) == 1) {
add_edge(i, tool + n, Linf);
if(tool == 0) ++ulen;
else while(tool) {
tool /= 10;
++ulen;
}
++ulen;
}
}
void init() {
cin >> n >> m;
s = n + m + 1, t = n + m + 2;
tot = 0, memset(head, -1, sizeof(head));
for(int i = 1, v; i <= n; ++i) {
scanf("%lld", &v), add_edge(s, i, v), sum += v;
readtools(i);
}
for(int i = 1, price; i <= m; ++i) {
cin >> price;
add_edge(i + n, t, price);
}
n0 = n;
n += m + 2;
}
queue<int> q;
int dis[N];
int BFS() {
for(int i = 1; i <= n; ++i) dis[i] = Linf, cur[i] = head[i];
queue<int>().swap(q);
q.push(s);
dis[s] = 0;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if(e[i].w > 0 && dis[v] > 1e18) {
q.push(v);
dis[v] = dis[u] + 1;
if(v == t) return 1;
}
}
}
return 0;
}
int DFS(int u, int sum = Linf) {
if(u == t) return sum;
int res = 0;
for(int i = cur[u]; ~i && sum > 0; i = e[i].next) {
cur[u] = i;
int v = e[i].v;
if(e[i].w > 0 && dis[v] == dis[u] + 1) {
int k = DFS(v, min(sum, e[i].w));
if(k == 0) dis[v] = Linf;
e[i].w -= k, e[i ^ 1].w += k;
res += k, sum -= k;
}
}
return res;
}
int Dinic() {
int ans = 0;
while(BFS()) ans += DFS(s);
return ans;
}
void solve() {
int ans = sum - Dinic();
for(int i = 1; i <= n0; ++i)
if(dis[i] < 1e18) cout << i << ' ';
cout << '\n';
for(int i = n0+1; i <= n0+m; ++i)
if(dis[i] < 1e18) cout << i - n0 << ' ';
cout << '\n' << ans << '\n';
}
} G;
void main() {
G.init();
G.solve();
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
套路
- 看似是最大流但是不可做的题目,可以将最大收益转化成总收益减最小损失,用最小割建图处理。
- 上下一定的依赖关系,可以有【下面有一个不选,上面的收益就要割掉】的建图。
- 点权可以拆点转化成边权。
费用流
概念
网络的边不仅有容量,还有一个费用
我们主要研究最小费用最大流(Minimum Cost Maximum Flow),即在最大流的基础上费用要最小。最大费用最大流同理。
解法
以下陈述 MCMF 的解法。
我们在 Dinic 求最大流的时候,用 BFS 将当前残量网络处理出层次图,沿着最短路的更新方向增广。
现在我们需要在保证最大流的基础上,让费用最小。我们自然地想到用亖了的 SPFA 或 Bellman-Ford 处理出残量网络中
专家可以证明这是对的。
但是要注意,该算法不能用于求解
Q:为什么要用 SPFA?
A:即使原图中
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 5e3+2, M = 5e4+2;
template<class T1, class T2>
pair<T1, T2> operator + (pair<T1, T2> a, pair<T1, T2> b) { return pii(a.first + b.first, a.second + b.second); }
struct Graph {
int n, m, s, t;
int head[N], tot, cur[N];
struct edge {
int v, w, cost, next;
edge() { }
edge(int a, int b, int c, int d) : v(a), w(b), cost(c), next(d) { }
} e[M << 1];
void add_edge(int u, int v, int w, int cost) {
e[tot] = edge(v, w, cost, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, -cost, head[v]), head[v] = tot++;
}
void init() {
cin >> n >> m >> s >> t;
tot = 0, memset(head, -1, sizeof(head));
for(int i = 1, u, v, w, cost; i <= m; ++i) {
scanf("%lld%lld%lld%lld", &u, &v, &w, &cost);
add_edge(u, v, w, cost);
}
}
int dis[N], exist[N], vis[N];
queue<int> q;
bool SPFA() {
for(int i = 1; i <= n; ++i) dis[i] = Linf, exist[i] = 0, cur[i] = head[i], vis[i] = 0;
dis[s] = 0, exist[s] = 1;
queue<int>().swap(q);
q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
exist[u] = 0;
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v, cost = e[i].cost;
if(e[i].w > 0 && dis[u] + cost < dis[v]) {
dis[v] = dis[u] + cost;
if(!exist[v]) exist[v] = 1, q.push(v);
}
}
}
return dis[t] < 1e18;
}
pii DFS(int u, int sum = Linf) {
if(u == t) return pii(sum, 0);
vis[u] = 1;
int res = 0, c = 0;
for(int i = cur[u]; ~i; i = cur[u] = e[i].next) {
int v = e[i].v, cost = e[i].cost;
if(vis[v]) continue; //防止零环让 DFS 反复更新
if(e[i].w > 0 && dis[v] == dis[u] + cost) {
auto [a, b] = DFS(v, min(sum, e[i].w));
if(a == 0) dis[v] = Linf;
e[i].w -= a, e[i ^ 1].w += a;
res += a, c += a * cost + b, sum -= a;
if(sum == 0) break;
}
}
return pii(res, c);
}
pii Dinic() {
pii ans = pii(0, 0);
while(SPFA()) ans = ans + DFS(s);
return ans;
}
} G;
void main() {
G.init();
pii ans = G.Dinic();
cout << ans.first << ' ' << ans.second;
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
最大费用最大流,将所有边的费用改为相反数,跑最小费用最大流之后费用取负即可。(前提是原图没有正环)
例题
luogu P4013 数字梯形问题
一堆路径从上面流到下面,一看就很最大流。
但是容量是什么呢?
我们发现,容量只能用来限制题目中的条件。于是,就可以考虑引入费用,计算最大收益。
对于第一问,每个【点】只能经过一次。经典地,将一个点拆成两个,中间连上容量为
对于第二问,每条【边】只能经过一次。我们将第一问的点内边的容量改为
对于第三问,除了源点连向第一排点的边,其他边容量全是
跑最大费用最大流即可。
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 1402, M = 14002;
template<class T1, class T2>
pair<T1, T2> operator + (pair<T1, T2> a, pair<T1, T2> b) { return pii(a.first + b.first, a.second + b.second); }
int n, m, a[42][42], pos[42][42], idx;
struct Graph {
int n, m, s, t;
int head[N], tot, cur[N];
struct edge {
int v, w, cost, next;
edge() { }
edge(int a, int b, int c, int d) : v(a), w(b), cost(c), next(d) { }
} e[M << 1];
void add_edge(int u, int v, int w, int cost) {
e[tot] = edge(v, w, cost, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, -cost, head[v]), head[v] = tot++;
}
void init(int opt) {
s = 2*idx + 1, t = 2*idx + 2;
n = Traveller::n, m = Traveller::m;
tot = 0, memset(head, -1, sizeof(head));
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m + i - 1; ++j) add_edge(pos[i][j], pos[i][j] + idx, opt == 1 ? 1 : Linf, -a[i][j]);
for(int i = 1; i < n; ++i)
for(int j = 1; j <= m + i - 1; ++j)
add_edge(pos[i][j] + idx, pos[i+1][j], opt <= 2 ? 1 : Linf, 0),
add_edge(pos[i][j] + idx, pos[i+1][j+1], opt <= 2 ? 1 : Linf, 0);
for(int i = 1; i <= m; ++i) add_edge(s, pos[1][i], 1, 0);
for(int i = 1; i <= n + m - 1; ++i) add_edge(pos[n][i] + idx, t, opt == 1 ? 1 : Linf, 0);
n = 2*idx + 2;
}
int dis[N], exist[N], vis[N];
queue<int> q;
bool SPFA() {
for(int i = 1; i <= n; ++i) dis[i] = Linf, exist[i] = 0, cur[i] = head[i], vis[i] = 0;
dis[s] = 0, exist[s] = 1;
queue<int>().swap(q);
q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
exist[u] = 0;
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v, cost = e[i].cost;
if(e[i].w > 0 && dis[u] + cost < dis[v]) {
dis[v] = dis[u] + cost;
if(!exist[v]) exist[v] = 1, q.push(v);
}
}
}
return dis[t] < 1e18;
}
pii DFS(int u, int sum = Linf) {
if(u == t) return pii(sum, 0);
vis[u] = 1;
int res = 0, c = 0;
for(int i = cur[u]; ~i; i = cur[u] = e[i].next) {
int v = e[i].v, cost = e[i].cost;
if(vis[v]) continue;
if(e[i].w > 0 && dis[v] == dis[u] + cost) {
auto [a, b] = DFS(v, min(sum, e[i].w));
if(a == 0) dis[v] = Linf;
e[i].w -= a, e[i ^ 1].w += a;
res += a, c += a * cost + b, sum -= a;
if(sum == 0) break;
}
}
return pii(res, c);
}
pii Dinic() {
pii ans = pii(0, 0);
while(SPFA()) ans = ans + DFS(s);
return ans;
}
int solve() { return -Dinic().second; }
} G;
void main() {
cin >> m >> n;
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m + i - 1; ++j) cin >> a[i][j];
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m + i - 1; ++j) pos[i][j] = ++idx;
G.init(1), cout << G.solve() << '\n';
G.init(2), cout << G.solve() << '\n';
G.init(3), cout << G.solve() << '\n';
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
luogu P2604 [ZJOI2010] 网络扩容
第一问简单。记算出来的最大流为
第二问要求将最大流增加
我们可以新建源点
我们将原图中的所有边分为【免费边】和【付费边】。【免费边】即为原来跑出最大流所需的,不用算在扩容费用中;【付费边】容量设为
#include <bits/stdc++.h>
#define inf 0x3f3f3f3f
#define Linf 0x3f3f3f3f3f3f3f3f
#define pii pair<int, int>
#define all(v) v.begin(), v.end()
#define int long long
using namespace std;
//#define filename "xxx"
#define FileOperations() freopen(filename".in", "r", stdin), freopen(filename".out", "w", stdout)
//#define multi_cases 1
namespace Traveller {
const int N = 5e3+2, M = 5e4+2;
template<class T1, class T2>
pair<T1, T2> operator + (pair<T1, T2> a, pair<T1, T2> b) { return pii(a.first + b.first, a.second + b.second); }
struct node {
int a, b, c, d;
node() { }
node(int a, int b, int c, int d) : a(a), b(b), c(c), d(d) { }
};
vector<node> vec;
int ans;
struct Graph {
int n, m, k, s, t;
int head[N], tot, cur[N];
struct edge {
int v, w, cost, next;
edge() { }
edge(int a, int b, int c, int d) : v(a), w(b), cost(c), next(d) { }
} e[M << 1];
void add_edge(int u, int v, int w, int cost) {
e[tot] = edge(v, w, cost, head[u]), head[u] = tot++;
e[tot] = edge(u, 0, -cost, head[v]), head[v] = tot++;
}
void init() {
cin >> n >> m >> k;
s = 1, t = n;
tot = 0, memset(head, -1, sizeof(head));
for(int i = 1, u, v, w, cost; i <= m; ++i) {
scanf("%lld%lld%lld%lld", &u, &v, &w, &cost);
add_edge(u, v, w, 0);
vec.emplace_back(u, v, w, cost);
}
}
void work() {
s = n+1, t = n++;
tot = 0, memset(head, -1, sizeof(head));
add_edge(s, 1, ans+k, 0);
for(auto [u, v, w, cost] : vec)
add_edge(u, v, w, 0), add_edge(u, v, Linf, cost);
}
int dis[N], exist[N], vis[N];
queue<int> q;
bool SPFA() {
for(int i = 1; i <= n; ++i) dis[i] = Linf, exist[i] = 0, cur[i] = head[i], vis[i] = 0;
dis[s] = 0, exist[s] = 1;
queue<int>().swap(q);
q.push(s);
while(!q.empty()) {
int u = q.front(); q.pop();
exist[u] = 0;
for(int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v, cost = e[i].cost;
if(e[i].w > 0 && dis[u] + cost < dis[v]) {
dis[v] = dis[u] + cost;
if(!exist[v]) exist[v] = 1, q.push(v);
}
}
}
return dis[t] < 1e18;
}
pii DFS(int u, int sum = Linf) {
if(u == t) return pii(sum, 0);
vis[u] = 1;
int res = 0, c = 0;
for(int i = cur[u]; ~i && sum > 0; i = cur[u] = e[i].next) {
int v = e[i].v, cost = e[i].cost;
if(vis[v]) continue;
if(e[i].w > 0 && dis[v] == dis[u] + cost) {
auto [a, b] = DFS(v, min(sum, e[i].w));
if(a == 0) dis[v] = Linf;
e[i].w -= a, e[i ^ 1].w += a;
res += a, c += a * cost + b, sum -= a;
}
}
return pii(res, c);
}
pii Dinic() {
pii ans = pii(0, 0);
while(SPFA()) ans = ans + DFS(s);
cerr << '\n';
return ans;
}
} G;
void main() {
G.init();
cout << (ans = G.Dinic().first) << ' ';
G.work();
cout << G.Dinic().second << '\n';
}
}
signed main() {
#ifdef filename
FileOperations();
#endif
signed _ = 1;
#ifdef multi_cases
scanf("%d", &_);
#endif
while(_--) Traveller::main();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步