【学习笔记】网络流
本文目前在不定期更新。
网络流定义
「网络」是指一张特殊的有向图,其中有一个「源点」(不是记忆源点)和一个汇点
在某些语境下,「流」代表一个值,是
最大流
给你一张网络,每条边有一个「容量」
。你需要求出从 到 的网络最大流。
思想
首先一开始对每条边建立一条容量为
然后每轮执行以下操作:
- 在残量网络(即还能流的网络)中找到一条
到 的增广路。 - 令
增广路所有边剩余容量的最小值。则:
+=- 增广路上所有边的剩余容量 -=
- 增广路上所有反向边的剩余容量 +=
举个典型的例子:
显然,最大流为
此时答案加一,然后进行反悔操作:
然后此时程序又找到了一条经过反向边的增广路,于是答案再加一:
然后就得到了想要的结果,因为这个方案跟我们的方案是本质一样的。
至于原理可以参考匈牙利算法。刚刚的反悔就相当于,寻找一个点的匹配时,是否可以换掉别人的匹配。
比如这张图中,第二条路径本来想要直接从下面到
那么不难看出,以上就是一种很正确的带有反悔行为的策略。
但是还有一个问题,没有增广路了就说明找到最大流了吗?答案是肯定的,参见 OI-wiki上的证明
FF算法
FF 算法就是把最大流的思想用最直接的方式实现,即 dfs 找增广路。
但是,dfs 有时会效率很低,如:
此时用 dfs 的话可能会出现以下的过程:
那要是边权从
EK算法
在 FF 算法上做一个“简单”的优化:每次找一条边数最少的增广路,也就是把 dfs 换成 bfs。这就是 EK 算法。感觉效率高了一些!
分析一下时间复杂度。这里引入一个结论:增广总轮数的上界是 证明不会qwq,想了解的可以去 OI-Wiki),然后每次的 bfs 是
然而这个
点击查看代码
// Author: AquariusZhao #include <bits/stdc++.h> using namespace std; #define ll long long #define inf 0x3f3f3f3f3f3f3f3f const int N = 205; int n, m, pre[N]; ll g[N][N], flow[N]; ll bfs(int s, int t) { memset(pre, -1, sizeof(pre)); flow[s] = inf; pre[s] = 0; queue<int> q; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); if (u == t) return flow[t]; for (int v = 1; v <= n; v++) if (pre[v] == -1 && g[u][v] > 0) { pre[v] = u; q.push(v); flow[v] = min(flow[u], g[u][v]); } } return -1; } ll maxflow(int s, int t) { ll res = 0; while (true) { ll x = bfs(s, t); if (x == -1) break; int v = t; while (v != s) { int u = pre[v]; g[u][v] -= x; g[v][u] += x; v = u; } res += x; } return res; } int main() { int s, t; cin >> n >> m >> s >> t; int u, v, w; for (int i = 1; i <= m; i++) scanf("%d%d%d", &u, &v, &w), g[u][v] += w; printf("%lld\n", maxflow(s, t)); return 0; }
以上代码建议理解,但没必要背下来,因为下面要讲的 Dinic 算法比它快而且码量差不多。
Dinic算法
EK 算法每次找增广路都要跑一遍 bfs,是不是有点浪费了呀……每次只能找一条路径,而计算完流量后又要从新开始。为什么不能在之前的结果上继续找呢?
Dinic 算法可以看作 EK 算法的优化†,它会不断执行以下步骤直到 bfs 时发现走不到
- 用 bfs 给每个点定一个
,表示从该点到 的最短距离; - 用 dfs 找增广路,但是深度为
的点只能走到 的点。
†虽然 Dinic 算法可以看作 EK 算法的优化,但后者其实要出现的晚一些。
另外,Dinic 算法有两个优化,详见代码。(好像还有一些厉害的优化,但不太实用,想了解可以去看看 P4722 【模板】最大流 加强版 / 预流推进 的题解区关于 Dinic 的其他优化。)
Dinic 算法的时间复杂度是
点击查看代码
// Author: AquariusZhao #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f3f3f3f3f using namespace std; const int N = 205, M = 5005; int n, m, s, t; int pos = 1, head[N], now[N]; struct node { int u, v, w, nxt; } e[M << 1]; ll ans; void addEdge(int u, int v, int w) { e[++pos] = {u, v, w, head[u]}; head[u] = pos; } int dep[N]; bool bfs() { memset(dep, -1, sizeof(dep)); dep[s] = 0; queue<int> q; q.push(s); now[s] = head[s]; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w > 0 && dep[v] == -1) { dep[v] = dep[u] + 1; now[v] = head[v]; q.push(v); if (v == t) return true; } } } return false; } ll dfs(int u, ll sum) { if (u == t) return sum; ll res = 0; for (int &i = now[u]; i; i = e[i].nxt) // 优化一:当前弧优化,走到第i条边时sum还>0,说明前面的边到汇点没有增广路了,下次不必再走 { int v = e[i].v; if (e[i].w > 0 && dep[v] == dep[u] + 1) { ll x = dfs(v, min(sum, (ll)(e[i].w))); if (x == 0) // 优化二:如果从v找不到增广路了,可以将dep设为-1,以后就不会再搜了 dep[v] = -1; e[i].w -= x; e[i ^ 1].w += x; sum -= x; res += x; } if (sum <= 0) break; } return res; } int main() { cin >> n >> m >> s >> t; int u, v, w; for (int i = 1; i <= m; i++) { scanf("%d%d%d", &u, &v, &w); addEdge(u, v, w), addEdge(v, u, 0); } while (bfs()) ans += dfs(s, inf); cout << ans << endl; return 0; }
最小割
对于一个网络,一个「割」是在网络中删掉一些边之后,
最小割问题:求所有割中总费用最小的。
其实,在一张网络中,
我觉得这个结论比较显然。考虑一个最大流,则此时找不到从
严谨的证明还是前往 OI-wiki 吧。qwq
最大权闭合图
指的是这样一类问题:
有
有
最大化选出物品的价值和。
套路做法是,
答案就是
原因是,对于某个限制,先考虑
而如果限制两边都是正,就相当于一个大的连通块,都是负同理。
至于一负一正,是稍微复杂一点的情况,考虑下面这张图:
不合法的情况只有负的选了但是正的没选,也就是说两条 1 边都割了,这就意味着原图存在经过这两个边的几条路,则此时这个限制会构成一个更长的路(
总之,这个算法是正确的。
P4174 [NOI2006] 最大获利
板子题。如果得到一个用户的收益,那必须付出建两个中转站的代价。
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f using namespace std; const int N = 1e5 + 5, M = 2e5 + 5; int n, m, p[N], s, t; int head[N], pos = 1; struct Edge { int u, v, w, nxt; } e[M << 1]; void addEdge(int u, int v, int w) { e[++pos] = {u, v, w, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0); } int dep[N], now[N]; bool bfs() { queue<int> q; q.push(s); memset(dep, -1, sizeof(dep)); dep[s] = 0; now[s] = head[s]; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w > 0 && dep[v] == -1) { dep[v] = dep[u] + 1; now[v] = head[v]; if (v == t) return true; q.push(v); } } } return false; } int dfs(int u, int sum) { if (u == t) return sum; int res = 0; for (int &i = now[u]; i; i = e[i].nxt) { int v = e[i].v, w = e[i].w; if (w > 0 && dep[v] == dep[u] + 1) { int x = dfs(v, min(sum, w)); if (x == 0) dep[v] = -1; e[i].w -= x; e[i ^ 1].w += x; sum -= x; res += x; } if (sum <= 0) break; } return res; } int Dinic() { int res = 0; while (bfs()) res += dfs(s, inf); return res; } int main() { cin >> n >> m; s = 0, t = n + m + 1; int sum = 0; for (int i = 1; i <= n; i++) { scanf("%d", p + i); addEdge(m + i, t, p[i]); } int u, v, w; for (int i = 1; i <= m; i++) { scanf("%d%d%d", &u, &v, &w); sum += w; addEdge(s, i, w); addEdge(i, m + u, inf); addEdge(i, m + v, inf); } cout << sum - Dinic() << endl; return 0; }
P2762 太空飞行计划问题
和刚刚那题几乎一模一样,但是要输出方案。
我们规定,如果过不了限制边就是
输出方案
for (int i = 1; i <= m; i++) if (dep[i] != -1) printf("%d ", i); puts(""); for (int i = 1; i <= n; i++) if (dep[i + m] != -1) printf("%d ", i); puts("");
注意,不能通过判断边的容量是否用光来确定连通性。因为有可能
比如考虑一条
总之错误原因就是判断依据不充分。
错误代码:
for (int i = head[s]; i; i = e[i].nxt) if (e[i].w) printf("%d ", e[i].v); puts(""); for (int i = head[t]; i; i = e[i].nxt) if (e[i ^ 1].w == 0) printf("%d ", e[i].v - m); puts("");
CF103E Buying Sets
如果得到一个集合的价值,集合里的数都必须选。但是由于要求最小方案所以集合权值取反一下。
但是题目要求选集合数要等于选的数的个数,不过满足任意多个集合的并集大小不小于集合数。所以采用套路,把所有数的权值
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long using namespace std; const int N = 605, M = 1e5, inf = 1e9; int n; int head[N], pos = 1; struct Edge { int u, v, w, nxt; } e[M << 1]; void addEdge(int u, int v, int w) { e[++pos] = {u, v, w, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0); } int s, t, dep[N], now[N]; bool bfs() { queue<int> q; q.push(s); memset(dep, -1, sizeof(dep)); dep[s] = 0; now[s] = head[s]; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w > 0 && dep[v] == -1) { dep[v] = dep[u] + 1; now[v] = head[v]; if (v == t) return true; q.push(v); } } } return false; } ll dfs(int u, ll sum) { if (u == t) return sum; ll res = 0; for (int &i = now[u]; i; i = e[i].nxt) { int v = e[i].v; ll w = e[i].w; if (w > 0 && dep[v] == dep[u] + 1) { ll x = dfs(v, min(sum, w)); if (x == 0) dep[v] = -1; e[i].w -= x; e[i ^ 1].w += x; sum -= x; res += x; } if (sum <= 0) break; } return res; } ll Dinic() { ll res = 0; while (bfs()) res += dfs(s, inf); return res; } int main() { cin >> n; s = 0, t = n + n + 1; int m, v; for (int i = 1; i <= n; i++) { scanf("%d", &m); while (m--) scanf("%d", &v), addEdge(i, v + n, inf); } ll sum = 0; for (int i = 1; i <= n; i++) { scanf("%d", &v), addEdge(s, i, inf - v); sum += inf - v; addEdge(i + n, t, inf); } cout << -(sum - Dinic()) << endl; return 0; }
最大密度子图
给定一张无向图。选出一个点集,则这个点集的密度为
求出所有点集的最大密度。
考虑分数规划。二分一个
则条件转为
边产生 1 的贡献,且需要选上端点;点产生
其他经典最小割模型
最小割树
问题:给一张带权无向图,询问任意两点间最小割。
解决方式是一个分治的思想:
在当前点集(初始就是原图点集)随便找两个点求一下最小割,同时维护一棵树,每次求完割就连接这两个点,边权为最小割。然后把割成的两个点集再递归下去。注意最小割要在原图上求。
然后就可以建成一棵树。则任意两点的最小割就是树上两点路径上的最小边权。
证明还在想,先咕着
时间复杂度
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f using namespace std; const int N = 505, M = 1505; int n, m; int head[N], pos = 1; struct Edge { int u, v, w, nxt; } e[M << 2]; vector<pair<int, int> > g[N]; void addEdge(int u, int v, int w) { e[++pos] = {u, v, w, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0); } int s, t, dep[N], now[N]; void init() { for (int i = 2; i <= pos; i += 2) { e[i].w += e[i ^ 1].w; e[i ^ 1].w = 0; } } bool bfs() { queue<int> q; memset(dep, -1, sizeof(dep)); q.push(s); dep[s] = 0; now[s] = head[s]; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w && dep[v] == -1) { dep[v] = dep[u] + 1; now[v] = head[v]; if (v == t) return true; q.push(v); } } } return false; } int dfs(int u, int sum) { if (u == t) return sum; int res = 0; for (int &i = now[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w > 0 && dep[v] == dep[u] + 1) { int x = dfs(v, min(e[i].w, sum)); if (!x) dep[v] = -1; e[i].w -= x; e[i ^ 1].w += x; sum -= x; res += x; } if (sum <= 0) break; } return res; } int Dinic(int x, int y) { init(); s = x, t = y; int res = 0; while (bfs()) res += dfs(s, inf); return res; } void Dfs(vector<int> o) { if (o.size() < 2) return; int w = Dinic(o[0], o[1]); g[o[0]].push_back({o[1], w}); g[o[1]].push_back({o[0], w}); vector<int> v1, v2; for (auto u : o) { if (dep[u] != -1) v1.push_back(u); else v2.push_back(u); } Dfs(v1); Dfs(v2); } bool vis[N]; int query(int s, int t) { queue<pair<int, int> > q; q.push({s, inf}); memset(vis, 0, sizeof(vis)); vis[s] = true; while (!q.empty()) { auto cur = q.front(); q.pop(); if (cur.first == t) return cur.second; for (auto i : g[cur.first]) { if (!vis[i.first]) { q.push({i.first, min(cur.second, i.second)}); vis[i.first] = true; } } } return inf; } int main() { cin >> n >> m; int u, v, w; for (int i = 1; i <= m; i++) { scanf("%d%d%d", &u, &v, &w); addEdge(u, v, w), addEdge(v, u, w); } vector<int> _v; for (int i = 1; i <= n; i++) _v.push_back(i); Dfs(_v); int Q; cin >> Q; while (Q--) { scanf("%d%d", &u, &v); printf("%d\n", query(u, v)); } return 0; }
CF343E Pumping Stations
首先把最小割树建出来。
然后考虑分治,先把当前最小的边割了。这样就变成了两个连通块,然后就分成了两个子问题,一半走完再跨过这个边走另一半。如此,每条边都恰好产生一次贡献。
首先显然不会有答案比这个还优了。
其次,我一开始觉得边有可能会经过两次,毕竟有时走完另一半之后要回来再走一次这个边,走到之前那一半。
然而实际上,这种情况下,这条边不会产生贡献。因为你既然走完另一半,那就意味着整个连通块的点都走过了,却还要回来,说明他肯定会退出这个连通块,经过这个连通块的父亲边,贡献就肯定不属于它了(因为从小往大割的)。
那其实就已经证完了,只有其中一半走完再走向另一半才会有贡献。如果再回来就必然没有贡献。
所以,答案就是最小割树上的边权和(这也说明一个性质,无论最小割树是怎么建的,边权的集合一定一样),排列就是 dfs 序。
希望不会有人像我一样傻傻的以为建树过程就是割最小边。
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f using namespace std; const int N = 205, O = 205, M = 2005; int n, m, ans; vector<int> res; int head[O], pos = 1; struct Edge { int u, v, w, nxt; } e[M << 1]; void addEdge(int u, int v, int w) { e[++pos] = {u, v, w, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0); } int s, t, dep[O], now[O]; bool bfs() { queue<int> q; q.push(s); memset(dep, -1, sizeof(dep)); dep[s] = 0; now[s] = head[s]; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w > 0 && dep[v] == -1) { dep[v] = dep[u] + 1; now[v] = head[v]; if (v == t) return true; q.push(v); } } } return false; } int dfs(int u, int sum) { if (u == t) return sum; int res = 0; for (int &i = now[u]; i; i = e[i].nxt) { int v = e[i].v, w = e[i].w; if (w > 0 && dep[v] == dep[u] + 1) { int x = dfs(v, min(sum, w)); if (x == 0) dep[v] = -1; e[i].w -= x; e[i ^ 1].w += x; sum -= x; res += x; } if (sum <= 0) break; } return res; } int Dinic() { int res = 0; while (bfs()) res += dfs(s, inf); return res; } void init() { for (int i = 2; i <= pos; i++) { e[i].w += e[i ^ 1].w; e[i ^ 1].w = 0; } } int Pos = 1, Head[N]; Edge E[N << 1]; void AddEdge(int u, int v, int w) { E[++Pos] = {u, v, w, Head[u]}; Head[u] = Pos; } void Dfs(vector<int> o) // build tree { if (o.size() < 2) return; s = o[0], t = o[1]; init(); int w = Dinic(); ans += w; AddEdge(s, t, w), AddEdge(t, s, w); vector<int> v1, v2; for (auto u : o) { if (dep[u] != -1) v1.push_back(u); else v2.push_back(u); } Dfs(v1); Dfs(v2); } int mnw, mne; void DFS(int u, int fa) // find min edge { for (int i = Head[u]; i; i = E[i].nxt) if (E[i].w && E[i].v != fa) { if (E[i].w < mnw) mnw = E[i].w, mne = i; DFS(E[i].v, u); } } void DFs(int u) // dfs tree { mnw = inf; DFS(u, 0); if (mnw == inf) { res.push_back(u); return; } E[mne].w = E[mne ^ 1].w = 0; int tmp = mne; DFs(E[tmp].u); DFs(E[tmp].v); } int main() { cin >> n >> m; int u, v, w; for (int i = 1; i <= m; i++) scanf("%d%d%d", &u, &v, &w), addEdge(u, v, w), addEdge(v, u, w); vector<int> o; for (int i = 1; i <= n; i++) o.push_back(i); Dfs(o); DFs(1); cout << ans << endl; for (auto u : res) printf("%d ", u); return 0; }
费用流
最小费用最大流,简称费用流。
这种问题的网络边还有一个权值
求最大流的前提下,最小化费用。
考虑 EK 的算法过程,每次找一个最短增广路。这个也一样,不过找的是
反向边的
至于 Dinic,当然也基本同理,但是用的不多。如果遇到
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f using namespace std; const int N = 5e3 + 5, O = 5e3 + 5, M = 5e4 + 5; struct Edge { int u, v, w, c, nxt; }; namespace flow { int pos = 1, head[O]; Edge e[M << 1]; void addEdge(int u, int v, int w, int c) { e[++pos] = {u, v, w, c, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0, -c); } int s, t, dis[O], pre[O], val[O]; bool vis[O]; bool spfa() { queue<int> q; q.push(s); memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; val[s] = inf; vis[s] = true; while (!q.empty()) { int u = q.front(); q.pop(); vis[u] = false; for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w && dis[v] > dis[u] + e[i].c) { dis[v] = dis[u] + e[i].c; val[v] = min(val[u], e[i].w); pre[v] = i; if (!vis[v]) { q.push(v); vis[v] = true; } } } } return dis[t] != inf; } pair<int, int> solve(int x, int y) { s = x, t = y; int res = 0, cost = 0; while (spfa()) { int u = t, i = pre[t]; while (u != s) { e[i].w -= val[t]; e[i ^ 1].w += val[t]; u = e[i].u; i = pre[u]; } res += val[t]; cost += dis[t] * val[t]; } return {res, cost}; } }; int n, m, s, t; int main() { cin >> n >> m >> s >> t; int u, v, w, c; for (int i = 1; i <= m; i++) { scanf("%d%d%d%d", &u, &v, &w, &c); flow::addEdge(u, v, w, c); } auto ans = flow::solve(s, t); cout << ans.first << ' ' << ans.second << endl; return 0; }
一些例题
费用流本身只是个工具,建图往往是比较困难的部分。
P2053 [SCOI2007] 修车
首先考虑每个人对答案的贡献。假设有
则此时总等待时间为
那把它反过来不就行了:
然后做一个经典的操作:对每个师傅建
然后每个顾客
跑费用流即可。输出平均等待时间,除以
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f using namespace std; const int N = 65, O = 605, M = 1e5 + 5; int n, m, a[N][N]; int trans(int i, int j) { return i * n + j; } struct Edge { int u, v, w, c, nxt; }; namespace flow { int pos = 1, head[O]; Edge e[M << 1]; void addEdge(int u, int v, int w, int c) { e[++pos] = {u, v, w, c, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0, -c); } int s, t, dis[O], pre[O], val[O]; bool vis[O]; bool spfa() { queue<int> q; q.push(s); memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; val[s] = inf; vis[s] = true; while (!q.empty()) { int u = q.front(); q.pop(); vis[u] = false; for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w && dis[v] > dis[u] + e[i].c) { dis[v] = dis[u] + e[i].c; val[v] = min(val[u], e[i].w); pre[v] = i; if (!vis[v]) { q.push(v); vis[v] = true; } } } } return dis[t] != inf; } pair<int, int> solve(int x, int y) { s = x, t = y; int res = 0, cost = 0; while (spfa()) { int u = t, i = pre[t]; while (u != s) { e[i].w -= val[t]; e[i ^ 1].w += val[t]; u = e[i].u; i = pre[u]; } res += val[t]; cost += dis[t] * val[t]; } return {res, cost}; } }; int main() { cin >> m >> n; for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) scanf("%d", &a[i][j]); int s = 0, t = (m + 1) * n + 1; for (int i = 1; i <= n; i++) { flow::addEdge(s, i, 1, 0); for (int j = 1; j <= m; j++) for (int k = 1; k <= n; k++) { flow::addEdge(i, trans(j, k), 1, a[i][j] * k); if (i == n) flow::addEdge(trans(j, k), t, 1, 0); } } double ans = flow::solve(s, t).second; ans /= double(n); printf("%.2lf\n", ans); return 0; }
P2050 [NOI2012] 美食节
刚刚那题的加强版,数据范围变大了。
首先,每个菜品
算一算复杂度?
点数:
边数:
EK 的增广轮数:
即使把 SPFA 看成 如果你觉得它有希望的话可以尝试一下
怎么优化?观察一下增广的过程,发现很多厨师的点是无用的。具体来讲,每个厨师有用的点一定是一个前缀,因为
于是考虑动态开点 建点。每次增广完之后看看这一轮用了哪个厨师的,就新建一个点。
这样点数就变为了 剧烈 很大优化。放心,可以过的!
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f using namespace std; const int N = 105, O = 1e5 + 5, M = 1e5 + 5; int n, m, p[N], a[N][N], cnt[N]; int trans(int i, int j) { return i * 801 + j; } struct Edge { int u, v, w, c, nxt; }; namespace flow { int pos = 1, head[O]; Edge e[M << 1]; void addEdge(int u, int v, int w, int c) { e[++pos] = {u, v, w, c, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0, -c); } int s, t, dis[O], pre[O], val[O]; bool vis[O]; bool spfa() { queue<int> q; q.push(s); memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; val[s] = inf; vis[s] = true; while (!q.empty()) { int u = q.front(); q.pop(); vis[u] = false; for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w && dis[v] > dis[u] + e[i].c) { dis[v] = dis[u] + e[i].c; val[v] = min(val[u], e[i].w); pre[v] = i; if (!vis[v]) { q.push(v); vis[v] = true; } } } } return dis[t] != inf; } pair<int, int> solve(int x, int y) { s = x, t = y; int res = 0, cost = 0; while (spfa()) { int u = t, i = pre[t]; int j = e[i].u / 801; cnt[j]++; for (int i = 1; i <= n; i++) addEdge(i, trans(j, cnt[j]), 1, a[i][j] * cnt[j]); addEdge(trans(j, cnt[j]), t, 1, 0); while (u != s) { e[i].w -= val[t]; e[i ^ 1].w += val[t]; u = e[i].u; i = pre[u]; } res += val[t]; cost += dis[t] * val[t]; } return {res, cost}; } }; int main() { cin >> n >> m; for (int i = 1; i <= n; i++) scanf("%d", p + i); for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) scanf("%d", &a[i][j]); int s = 0, t = 1e5; for (int i = 1; i <= n; i++) { flow::addEdge(s, i, p[i], 0); for (int j = 1; j <= m; j++) { cnt[j] = 1; flow::addEdge(i, trans(j, 1), 1, a[i][j]); if (i == 1) flow::addEdge(trans(j, 1), t, 1, 0); } } cout << flow::solve(s, t).second << endl; return 0; }
P4249 [WC2007] 剪刀石头布
也是类似的模型。难点在于转化。
考虑把满的胜负情况看成一张竞赛图。
然后画几个三元环看看。。发现如果它不是“剪刀石头布”,当且仅当存在一个点入度为 2。
设点
于是问题就变成了最小化后面那坨。
然后就可以自己试试建图。
建图方案
假如给你的矩阵全 2,那么:
对每组
然后每个
这里就不用对每个
如果有非 2 的,
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f using namespace std; const int N = 105, O = N * (N + 1), M = 4 * N * N; int n, g[N][N], d[N]; int trans(int i, int j) { return i * n + j; } struct Edge { int u, v, w, c, nxt; }; namespace flow { int pos = 1, head[O]; Edge e[M << 1]; void addEdge(int u, int v, int w, int c) { e[++pos] = {u, v, w, c, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0, -c); } int s, t, dis[O], pre[O], val[O]; bool vis[O]; bool spfa() { queue<int> q; q.push(s); memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; val[s] = inf; vis[s] = true; while (!q.empty()) { int u = q.front(); q.pop(); vis[u] = false; for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w && dis[v] > dis[u] + e[i].c) { dis[v] = dis[u] + e[i].c; val[v] = min(val[u], e[i].w); pre[v] = i; if (!vis[v]) { q.push(v); vis[v] = true; } } } } return dis[t] != inf; } void solve(int x, int y) { s = x, t = y; while (spfa()) { int u = t, i = pre[t]; while (u != s) { e[i].w -= val[t]; e[i ^ 1].w += val[t]; u = e[i].u; i = pre[u]; } } for (int u = 1; u <= n; u++) for (int v = u + 1; v <= n; v++) if (g[u][v] == 2) { int o = 0; for (int i = head[trans(u, v)]; i; i = e[i].nxt) if (e[i].w == 0) o = e[i].v; g[u + v - o][o] = 1; g[o][u + v - o] = 0; } } }; int main() { cin >> n; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { scanf("%d", &g[i][j]); if (g[i][j] == 1) d[j]++; } int s = 0, t = trans(n, n) + 1; for (int u = 1; u <= n; u++) for (int v = u + 1; v <= n; v++) if (g[u][v] == 2) { flow::addEdge(s, trans(u, v), 1, 0); flow::addEdge(trans(u, v), u, 1, 0); flow::addEdge(trans(u, v), v, 1, 0); } for (int u = 1; u <= n; u++) for (int i = d[u] + 1; i <= n; i++) flow::addEdge(u, t, 1, i - 1); flow::solve(s, t); memset(d, 0, sizeof(d)); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) if (g[i][j] == 1) d[j]++; int ans = n * (n - 1) * (n - 2) / 6; for (int i = 1; i <= n; i++) { ans -= d[i] * (d[i] - 1) / 2; g[i][i] = 0; } cout << ans << endl; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) printf("%d%c", g[i][j], " \n"[j == n]); return 0; }
P4307 [JSOI2009] 球队收益 / 球队预算
首先根据已经举行的比赛可以确定每个队至少赢了几场。
然后剩下的比赛假设双方都输,然后发现,如果
这个东西是随胜利场数变多而递增的,于是可以连边了,费用变化量。
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f using namespace std; const int N = 5005, O = 6005, M = 1e4 + 5; int n, m, a[N], b[N], c[N], d[N], cnt[N], ans; struct Edge { int u, v, w, c, nxt; }; namespace flow { int pos = 1, head[O]; Edge e[M << 1]; void addEdge(int u, int v, int w, int c) { e[++pos] = {u, v, w, c, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0, -c); } int s, t, dis[O], pre[O], val[O]; bool vis[O]; bool spfa() { queue<int> q; q.push(s); memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; val[s] = inf; vis[s] = true; while (!q.empty()) { int u = q.front(); q.pop(); vis[u] = false; for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w && dis[v] > dis[u] + e[i].c) { dis[v] = dis[u] + e[i].c; val[v] = min(val[u], e[i].w); pre[v] = i; if (!vis[v]) { q.push(v); vis[v] = true; } } } } return dis[t] != inf; } pair<int, int> solve(int x, int y) { s = x, t = y; int res = 0, cost = 0; while (spfa()) { int u = t, i = pre[t]; while (u != s) { e[i].w -= val[t]; e[i ^ 1].w += val[t]; u = e[i].u; i = pre[u]; } res += val[t]; cost += dis[t] * val[t]; } return {res, cost}; } }; int main() { cin >> n >> m; for (int i = 1; i <= n; i++) scanf("%d%d%d%d", a + i, b + i, c + i, d + i); int s = 0, t = n + m + 1, x, y; for (int i = 1; i <= m; i++) { scanf("%d%d", &x, &y); cnt[x]++, cnt[y]++; b[x]++, b[y]++; flow::addEdge(s, i + n, 1, 0); flow::addEdge(i + n, x, 1, 0); flow::addEdge(i + n, y, 1, 0); } for (int i = 1; i <= n; i++) { ans += a[i] * a[i] * c[i] + b[i] * b[i] * d[i]; for (int j = 1; j <= cnt[i]; j++) { flow::addEdge(i, t, 1, c[i] + d[i] + 2 * a[i] * c[i] - 2 * b[i] * d[i]); a[i]++, b[i]--; } } cout << ans + flow::solve(s, t).second << endl; return 0; }
P3980 [NOI2008] 志愿者招募
神仙题。
题目说是一个区间加,很不好做,就考虑用一些手段差分掉。
那就可以开始尝试推柿子。
假设有三类志愿者,有四天,覆盖的区间分别是
不等式经常不好处理,所以强制转成等式:
其中
然后在前后补上空不等式,做个差分:
这下就把区间拆成左右端点了,离答案不远了!
为了方便建图,移一下项:
这个就很明显可以建图了,每个等式为一个点,正号看成入边,负号看成出边。
经过尝试,可以得到一种很好的建图方案:
单个数的边权指的是容量,费用为 0。
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f3f3f3f3f using namespace std; const int O = 1e3 + 10, M = 2e4 + 5; int n, m; struct Edge { int u, v; ll w, c; int nxt; }; namespace flow { int pos = 1, head[O]; Edge e[M << 1]; void addEdge(int u, int v, ll w, ll c) { e[++pos] = {u, v, w, c, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0, -c); } int s, t, pre[O]; ll dis[O], val[O]; bool vis[O]; bool spfa() { queue<int> q; q.push(s); memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; val[s] = inf; vis[s] = true; while (!q.empty()) { int u = q.front(); q.pop(); vis[u] = false; for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w && dis[v] > dis[u] + e[i].c) { dis[v] = dis[u] + e[i].c; val[v] = min(val[u], e[i].w); pre[v] = i; if (!vis[v]) { q.push(v); vis[v] = true; } } } } return dis[t] != inf; } pair<ll, ll> solve(int x, int y) { s = x, t = y; ll res = 0, cost = 0; while (spfa()) { int u = t, i = pre[t]; while (u != s) { e[i].w -= val[t]; e[i ^ 1].w += val[t]; u = e[i].u; i = pre[u]; } res += val[t]; cost += dis[t] * val[t]; } return {res, cost}; } }; int main() { cin >> n >> m; int s = 0, t = n + 2; int a, l, r, c; for (int i = 1; i <= n; i++) { scanf("%d", &a); flow::addEdge(s, i, a, 0); flow::addEdge(i + 1, t, a, 0); flow::addEdge(i + 1, i, inf, 0); } for (int i = 1; i <= m; i++) { scanf("%d%d%d", &l, &r, &c); flow::addEdge(l, r + 1, inf, c); } cout << flow::solve(s, t).second << endl; return 0; }
AT_agc034_d [AGC034D] Manhattan Max Matching
一种容易想到的连法是两两点之间连费用,但是边数过多。
考虑拆绝对值。
于是建四个点作为中转点,每个点连四条边就好了。
点击查看代码
// Author: Aquizahv #include <bits/stdc++.h> #define ll long long #define inf 0x3f3f3f3f3f3f3f3f using namespace std; const int O = 2e3 + 10, M = 1e4 + 5; int n; struct Edge { int u, v; ll w, c; int nxt; }; namespace flow { int pos = 1, head[O]; Edge e[M << 1]; void addEdge(int u, int v, ll w, ll c) { e[++pos] = {u, v, w, c, head[u]}; head[u] = pos; if (!(pos & 1)) addEdge(v, u, 0, -c); } int s, t, pre[O]; ll dis[O], val[O]; bool vis[O]; bool spfa() { queue<int> q; q.push(s); memset(dis, 0x3f, sizeof(dis)); dis[s] = 0; val[s] = inf; vis[s] = true; while (!q.empty()) { int u = q.front(); q.pop(); vis[u] = false; for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; if (e[i].w && dis[v] > dis[u] + e[i].c) { dis[v] = dis[u] + e[i].c; val[v] = min(val[u], e[i].w); pre[v] = i; if (!vis[v]) { q.push(v); vis[v] = true; } } } } return dis[t] != inf; } pair<ll, ll> solve(int x, int y) { s = x, t = y; ll res = 0, cost = 0; while (spfa()) { int u = t, i = pre[t]; while (u != s) { e[i].w -= val[t]; e[i ^ 1].w += val[t]; u = e[i].u; i = pre[u]; } res += val[t]; cost += dis[t] * val[t]; } return {res, cost}; } }; int main() { cin >> n; int n2 = 2 * n; int s = 0, t = n2 + 1; int A = n2 + 2, B = n2 + 3, C = n2 + 4, D = n2 + 5; int x, y, c; for (int i = 1; i <= n; i++) { scanf("%d%d%d", &x, &y, &c); flow::addEdge(s, i, c, 0); flow::addEdge(i, A, 10, x + y); flow::addEdge(i, B, 10, -(x + y)); flow::addEdge(i, C, 10, x - y); flow::addEdge(i, D, 10, -(x - y)); } for (int i = n + 1; i <= n2; i++) { scanf("%d%d%d", &x, &y, &c); flow::addEdge(i, t, c, 0); flow::addEdge(A, i, 10, -(x + y)); flow::addEdge(B, i, 10, x + y); flow::addEdge(C, i, 10, -(x - y)); flow::addEdge(D, i, 10, x - y); } cout << -flow::solve(s, t).second << endl; return 0; }
本文作者:Aquizahv's Blog
本文链接:https://www.cnblogs.com/aquizahv/p/18440490
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步