浅谈网络流
浅谈网络流
序
最近网络流做了一堆,感觉有微弱的进步!
尝试用 尽量人话 的东西讲清网络流的一些 算法和套路
里面可能有很多 感性理解的东西,需要详细证明的可以看对应部分的 参考资料 等
并记录一些 好的错误,以便以后再错
这东西算法十分的多,本篇博客 只讲了本人比较常用的部分算法(做题够用了)
纠结效率的移步 https://en.wikipedia.org/wiki/Maximum_flow_problem
本文 \(Typora\) 建议阅读时长 \(115~min\),如果不了解的可以 耐心的 看(可能要看一下午)
所有给出的 部分代码 都 只需 在 前面加上 本文中对应费用流/最大流板子 后即可 正常通过
本文中所有题和部分练习 可以在 这个题单 中找到,简要题解和完整代码 可以下载 这个包
目录
概念
不是很会讲概念,从 \(OI\_WiKi\) 上贺了一些下来
也引用了 \(command\_block\) 佬在 这篇博客 中的一些话
前言
网络流作为一个玄学算法,其题目难度主要在构图上。
模型
网络流的基本模型 可以理解成一个 有向水管连成的图
源点 \(S\) 就是 入水口,汇点 \(T\) 就是 出水口,无源汇 就是 管道连成 环 了
咱要求解的就是有关 这堆水管 的 一些信息
定义
-
边的容量 \(c(u, v)\),就是 水管最多能流多少水
-
源点 \(S\),入水口
-
汇点 \(T\),出水口
-
流 \(f\),一个描述水流的 函数 / 方式
-
流量 \(|f|\),两点间的水流大小
-
割,将 两点划分开 的一个 边集(一堆水管使得堵了之后 两边流不通)
-
费用 \(Cost(u, v)\),就是 这个水管流一份水要花多少钱
-
可行流,一个 流量 大于 \(0\) 的,联通 源点汇点 的 流
-
残量网络,原来的水管 流了个可行流 剩下来的可用部分
-
满流边,满了的水管,没法再流更多的水了
-
弱流边,没满的水管,还能流都算弱流边(就算一点没流)
性质
-
流量守恒,除了源汇点,每条边 流出流量 = 流入流量(只是流经,又不停)
-
斜对称性,一个点 正反边流量和 = 该边容量(流了多少,就可以 反悔多少)
-
容量限制,一个点 流量 小于 容量(废话)
常见问题
- 最大流,求 源点 \(S\) 到 汇点 \(T\) 可行流量最大值(水管不爆 的话能流多少水?)
- 最小割,源点 \(S\) 和 汇点 \(T\) 的 割 中 总流量最小 的(堵水管让它联不通,水管总容量 最小)
- 最小费用最大流,每个 边 有 单位费用,然后如题(走水管要收钱,一份水一份钱)
板子
根据地方法律法规,最大流 中 \(Dinic\) 以及 费用流 中 \(EK\) 不应当被卡,望周知
下面并没有出现 \(HLPP\) 的任何板子
因为这个东西 十分的难调 并 理论时间复杂度很对(一定不是指 上界极紧)
导致我根本不会写(根本原因)
最大流
多用各种 增广路算法,下面给出 两种常用算法实现
容易被卡,而且 绝绝绝大多数情况下 能被 \(Dinic\) 平替的 \(EK\) 就直接跳了
\(2024.03.11 - UPD:\) 好像还真有题会用到 单路增广 这种东西?\(Ford-Fulkerson\),启动!
最大流增广路 大体思路 就是每次找一条 可行通路,增流到 被塞满了!
然后有一层的水管 都被塞满了!
于是 进不去,怎么想也进不去吧!(指流量)
就结束了
最大流的正确性很显然 —— \(\textsf {Meatherm}\)
\(Edmonds-Karp\)
即 \(EK\) 算法
就是 \(BFS\) 找到一条 能到汇点 \(T\) 的 增广路 之后就 暴力增广
#include <bits/stdc++.h>
using namespace std;
namespace Dinic {
const int MAXN = 200005;
const long long INF = 1e16;
struct Node {
int to, nxt;
long long f;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f) {
E[++ tot] = {v, H[u], f}, H[u] = tot;
E[++ tot] = {u, H[v], 0}, H[v] = tot;
}
int S = 10005, T = 10010;
bool Vis[MAXN];
int U[MAXN], I[MAXN];
inline long long BFS (const int x, const long long MAXF) {
queue <int> q; q.push (x), Vis[x] = 1;
int u, v;
while (!q.empty ()) {
u = q.front (), q.pop ();
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to;
if (!Vis[v] && E[i].f)
U[v] = u, I[v] = i, Vis[v] = 1, q.push (v);
if (Vis[T]) break ;
}
if (Vis[T]) break ;
}
if (!Vis[T]) return 0;
long long Ans = INF;
u = T;
while (u != S) Ans = min (Ans, E[I[u]].f), u = U[u];
u = T;
while (u != S) E[I[u]].f -= Ans, E[I[u] ^ 1].f += Ans, u = U[u];
return Ans;
}
inline long long dinic () {
long long MF = 1, F = 0;
while (MF) MF = BFS (S, INF), F += MF, memset (Vis, 0, sizeof Vis);
return F;
}
}
namespace Value {
int N, M, u, v, f;
inline void Solve () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M >> Dinic::S >> Dinic::T;
for (int i = 1; i <= M; ++ i)
cin >> u >> v >> f, Dinic::Add_Edge (u, v, f);
cout << Dinic::dinic () << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
\(Ford-Fulkerson\)
即 \(Furina\) 算法 ,\(FF\) 算法
就是把 \(BFS\) 换成 \(DFS\),似乎会更慢
我的芙芙被爆力
\(Dinic\)
其实就是 \(EK - Pro\),注意到我们的 \(EK\) 和 \(FF\) 每次都只 增广一条路
而 \(Dinic\) 则是 到一个点 就 将从该点出发 所有能增广的路 都跑一遍
注意到这个图(下面还会再出现一次)
设左边一排点 从上到下 为 \(L_1 \sim L_5\),中间为 \(M\),右边 从上到下 为 \(R_1 \sim R_5\),后链接 汇点
如果是 \(EK\),则流程为 \(L_1, L_2, L_3, L_4, L_5, M, R_1\),\(L_1, L_2, L_3, L_4, L_5, M, R_2\)...
如果是 \(FF\),则流程为 \(L_1, M, R_1\),\(L_1, M, R_2\),...,\(L_2, M, R_1\),...,\(L_5, M, R_5\)
如果为 \(Dinic\),则流程为 \(L_1, M, R_1, R_2, R_3, R_4, R_5\),结束!
namespace Dinic {
const int MAXN = 100005;
const long long INF = 1e16;
struct Node {
int to, nxt;
long long f;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f) {
E[++ tot] = {v, H[u], f}, H[u] = tot;
E[++ tot] = {u, H[v], 0}, H[v] = tot;
}
int S, T;
int Cur[MAXN], Dep[MAXN];
inline bool BFS () {
fill (Dep, Dep + MAXN, -1);
queue <int> q;
q.push (S), Dep[S] = 0, Cur[S] = H[S];
int u, v, f;
while (!q.empty ()) {
u = q.front (), q.pop ();
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, f = E[i].f;
if (Dep[v] == -1 && f) {
Dep[v] = Dep[u] + 1, Cur[v] = H[v], q.push (v);
if (v == T) return 1;
}
}
}
return 0;
}
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dep[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
}
}
return F;
}
inline long long dinic () {
long long F = 0;
while (BFS ()) F += DFS (S, INF);
return F;
}
}
注意 细节 的实现!
万恶之源:当前弧优化!!!
首先,注意,这不是一个可选优化(因为不加的话,您在如下图的 红框的点增广 中复杂度将变成 \(EK\))
考虑这个东西 我曾经写过 以下 3 * 3 种方式,感觉把 能错的都错了
在初始化上
inline bool BFS () { fill (Dep, Dep + MAXN, -1); queue <int> q; q.push (S), Dep[S] = 0, Cur[S] = H[S]; // 开头特判源点 int u, v, f; while (!q.empty ()) { u = q.front (), q.pop (); for (int i = H[u]; i; i = E[i].nxt) { v = E[i].to, f = E[i].f; if (Dep[v] == -1 && f) { // 每次初始化 边终点 (E[i].to) Dep[v] = Dep[u] + 1, Cur[v] = H[v], q.push (v); if (v == T) return 1; } } } return 0; }
这个第一种写法是对的,考虑每个点 \(Dep\) 都将被更新到,后面 \(DFS\) 可以正常执行多路增广。
这个写法就是要注意 特判源点,但不需要额外辅助变量,时间也很对,十分规范!
inline bool BFS () { fill (Dep, Dep + MAXN, -1); for (int i = 1; i <= N; ++ i) Cur[i] = H[i]; // or memcpy (Cur, H, sizeof H); 在开头统一初始化 queue <int> q; q.push (S), Dep[S] = 0; int u, v, f; while (!q.empty ()) { u = q.front (), q.pop (); for (int i = H[u]; i; i = E[i].nxt) { v = E[i].to, f = E[i].f; if (Dep[v] == -1 && f) { Dep[v] = Dep[u] + 1, q.push (v); if (v == T) return 1; } } } return 0; }
这个第二种写法也是对的,在 开头全部复制 这个一听就不可能有问题。
但是你要么需要把 \(N\) 写在函数前面(本人习惯板子单独放前面,所以 \(N\) 一般在后面)
要么 \(memset\) 一整个数组(当你数组大小和实际点数差异大时,会浪费很多时间)
总之不很完美
inline bool BFS () { fill (Dep, Dep + MAXN, -1); queue <int> q; q.push (S), Dep[S] = 0; int u, v, f; while (!q.empty ()) { u = q.front (), q.pop (), Cur[u] = H[u]; // 每次初始化 边起点 for (int i = H[u]; i; i = E[i].nxt) { v = E[i].to, f = E[i].f; if (Dep[v] == -1 && f) { Dep[v] = Dep[u] + 1, q.push (v); if (v == T) return 1; } } } return 0; }
这个第三种写法就很错了,而我甚至在 相当长一段时间 都是这么写的
(
因为他确实比前面俩简单仔细思考会发现,它只更新到 第一个能到达汇点的点,然后就 退出了!
于是 十分逆天,这玩意儿直接退化成了 \(EK\)(在 \(DFS\) 的时候每次只有 一路可用)
讲一个 题 外 话
在 费用流 的 \(SPFA\) 实现中,第三种写法并无问题
原因显然,\(SPFA\) 的实现中 并不允许到汇点就提前跳出
在使用上
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) { Cur[x] = i; if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) { long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f)); if (!TmpF) Dep[E[i].to] = -1; F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF; } // if (F >= MAXF) break; }
这个第一种写法是对的,不取地址,手动记录
然后你 \(F < MAXF\) 的判断就可以放在循环里(
虽然很容易忘)相当的规范,我十分喜欢!
for (int &i = Cur[x]; i; i = E[i].nxt) { // Cur[x] = i; if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) { long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f)); if (!TmpF) Dep[E[i].to] = -1; F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF; } if (F >= MAXF) break; }
这个第二种写法也是对的,可取地址,不用写 \(Cur[x] = i\) 这个东西
但是 \(F\) 和 \(MAXF\) 但判断要 在循环末尾写出来,不很美观
for (int &i = Cur[x]; i && F < MAXF; i = E[i].nxt) { // Cur[x] = i; if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) { long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f)); if (!TmpF) Dep[E[i].to] = -1; F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF; } // if (F >= MAXF) break; }
这个第三种写法就很错了,感觉上就是把前两种结合起来吧?
但事实上这个十分的假,每次都会增广不完,效率低下
根据考证,问题还是出在 循环条件执行顺序 的理解上
考虑原来理解循环的顺序是 先执行判断,再执行赋值
但事实上,完整的 循环执行步骤 是这样的:
(第一次执行时)1.执行 for 的 第一语句 (第一次执行时)2.判断 for 的 第二语句 (后续循环){ 1.循环第一行 2.循环最后一行 3.执行 for 的第三语句 4.判断 for 的第二语句 }
比较神秘,带进去就可以很快判断出三种写法 对为什么对,错为什么错(
一些 容易忘 的地方
1.建边
- 单向边反边 流量为 0,双向边反边 流量为 \(f\)
- \(tot\) 初始值为 1 !!!!!
- 开 边集数组 \(E\) 时,大概率 \(<< 1\) 是不够的
- 注意 源汇 及 其它点 编号是否 大于 \(MAXN\)
不要忘记给 \(S,T\) 赋值,不然你这辈子都跑不出来2.BFS
- 特判 源点当前弧,当前弧,当前弧!!!
- \(q.pop () ~~ q.push(v)\)
一生之敌- 不要写成 \(SPFA\) 还加个 \(Vis\),
这显得很傻3.DFS
勇敢加
inline
,不要迷信 递归函数不加inline
的说法(\(\textsf {Meatherm}\):必不可能快)开局是 \(return ~ MAXF\) 不是 \(return ~ 0\) !
记得判断 \(F < MAXF\)
记得最后 \(return ~ F\)
感觉上述问题每次总会 随机一个 出现,火大!
yny の 神 秘 优 化
namespace Dinic {
struct Edge {
int u, v;
long long w;
inline bool operator < (const Edge &a) const {
return w > a.w;
}
};
struct Node {
int to, id;
long long f;
};
vector <Edge> Tmp;
vector <Node> E[MAXN];
int Now[MAXN];
inline void Add_Edge (const int u, const int v, const long long w) {
Tmp.push_back ({u, v, w});
}
inline void add_edge (const int x) {
E[Tmp[x].u].push_back ({Tmp[x].v, Now[Tmp[x].v] ++, Tmp[x].w});
E[Tmp[x].v].push_back ({Tmp[x].u, Now[Tmp[x].u] ++, 0});
}
int Cur[MAXN], Dep[MAXN];
int S, T;
inline bool BFS () {
memset (Cur, 0, sizeof Cur);
memset (Dep, 0, sizeof Dep);
queue <int> q;
q.push (S), Dep[S] = 1;
int u;
while (!q.empty ()) {
u = q.front (), q.pop ();
for (auto i : E[u])
if (!Dep[i.to] && i.f) {
Dep[i.to] = Dep[u] + 1, q.push (i.to);
if (i.to == T) return 1;
}
}
return 0;
}
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
long long f = 0;
for (int i = Cur[x]; i < (int) E[x].size () && f < MAXF; ++ i) {
Cur[x] = i;
if (Dep[E[x][i].to] == Dep[x] + 1 && E[x][i].f) {
long long TmpF = DFS (E[x][i].to, min (MAXF - f, E[x][i].f));
if (!TmpF) Dep[E[x][i].to] = 0;
f += TmpF, E[x][i].f -= TmpF, E[E[x][i].to][E[x][i].id].f += TmpF;
}
}
return f;
}
inline long long Solve () {
long long f = 0;
while (BFS ()) f += DFS (S, INF);
return f;
}
inline long long dinic () {
sort (Tmp.begin(), Tmp.end());
long long Ans = 0;
for (int i = 1e9, j = 0; j < (int) Tmp.size(); i /= 20) {
while (Tmp[j].w >= i && j < (int) Tmp.size()) add_edge (j), ++ j;
Ans += Solve ();
}
return Ans;
}
}
本质上是 按值域分块加边,把 相近流量 的边 放在一起
块长 这个东西每次 \(\div 20\) 十分合适,也有 \(<< 4\) 之类的,动态调整就好
需要用 \(vector\) 来存边,初始加边只是存入,真正跑之前再 排序 并 加边
能轻松跑过 Luogu P4722 【模板】最大流 加强版 / 预流推进 什么实力不用多说
但是这个优化很玄学,就...(一直不知道在理论复杂度上真的有优化吗?
然后显然,这东西并不能在 动态加边 的过程中 保持良好的复杂度
因为本身就是 先把边集离线了 然后特定方式加边
有一定局限性,但还是很值得学
费用流 似乎也能应用 这个东西?没有仔细研究过 正确性 上的证明?
我实现过一份,但是板子题 \(60~pts ~~ TLE\) 了,有种卡死的感觉
其它点倒没有 \(WA\)
先鸽了,会的 \(DALAO\) 教教!!!
\(ISAP\)
注意,以下板子 有可能 是假的!
主要是因为按理说 这玩意儿 要比 上面那玩意儿 快一些
但我写 这篇随笔 之前专门去测了一下...(Luogu P3376 | P4722)
嘶,效果并不理想(至少在 板子题上)
namespace _ISAP {
struct Edge {
int to, nxt;
long long f;
} E[MAXN << 1];
int H[MAXN], tot = 1, N; // FUCK
inline void Add_Edge (const int u, const int v, const int f) {
E[++ tot] = {v, H[u], f}, H[u] = tot;
E[++ tot] = {u, H[v], 0}, H[v] = tot;
}
int Dep[MAXN], Gap[MAXN], Cur[MAXN];
int S = 11451, T = 19198;
inline void BFS () {
memset (Dep, -1, sizeof Dep);
memset (Gap, 0, sizeof Gap);
queue <int> q;
q.push (T), Gap[0] = 1, Dep[T] = 0;
int u, v;
while (!q.empty ()) {
u = q.front (), q.pop ();
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to;
if (Dep[v] != -1) continue;
q.push (v), Dep[v] = Dep[u] + 1, ++ Gap[Dep[v]];
}
}
return;
}
inline long long DFS (const int x, const long long MAXF) {
if (x == T) return MAXF;
long long F = 0;
for (int i = H[x]; i; i = E[i].nxt) {
if (E[i].f && Dep[E[i].to] + 1 == Dep[x]) {
long long TmpF = DFS (E[i].to, min (E[i].f, MAXF - F));
if (TmpF) F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
if (F == MAXF) return F;
}
}
-- Gap[Dep[x]];
if (Gap[Dep[x]] == 0) Dep[S] = N;
Gap[++ Dep[x]] ++;
return F;
}
inline long long ISAP () {
long long F = 0;
BFS ();
while (Dep[S] < N) F += DFS (S, INF);
return F;
}
}
这个东西的优化 讲人话就是 省掉了多次 \(BFS\)
通过 \(Gap\) 数组记录每个 \(Dep\) 的点数,由于 \(BFS\) 是反向跑的
然后每次把当前点 增广完之后就更新当前点 \(Dep\)(当前点不可用了)
那么显然 如果源点 \(S\) 的 \(Dep\) 被推到 \(N\),或者说出现了 断层(没法联通了)
那就完了!返回最大流就行
问题是这东西丑就丑在你需要 把 \(N\) 放在前面!!!
没有一点好!!!!!
但不然的话 第一个结束条件 就没法判,时间会很有问题
其他的各种算法 还是 看板子题题解 来的好,我不了解的也就不写了
关于 效率问题 / 严谨的时间复杂度 可以看 这个,只是多数 网络流题常数根本卡不满...
费用流
即 最小费用最大流,但是 板子可以用到各种地方
不排除
当你懒得改板子的时候甚至可以拿来写 最大流
下面将给出一种 垃圾的增广路实现 并 使得它变强的办法
以及一种 本身就非常强的 网络单纯形实现
\(Dinic\)
实现
先讲实现,本质就是从 最大流 \(Dinic\) 扩展而来
把 \(BFS\) 部分改成 您喜欢的 最短路算法(\(SPFA, Dijkstra...\) 都行)
边权就是 边上费用,\(DFS\) 增流时 统计费用 即可
注意由于 反边大多情况下是带负权的,所以此处的 \(Dijkstra\) 实现参考 \(Johnson\) 全源最短路,需要先跑一遍 \(Bellman-Ford ~ / ~ SPFA\) 来 处理负边权问题(设计势能)。
考虑到上面的问题,显然 \(Dijkstra\) 实现 并不支持动态加边
(不然你加一次,跑一次 \(SPFA\) 处理,不如直接 \(SPFA\))
并且 常数 和 堆的实现 有很大关系
(这导致在 \(C++\) 语言下是否打开 \(O2\) 对其影响是 致命的)
有一定局限性,就不写了(
\(SPFA + Dinic\) 实现
namespace MCMF {
struct Node {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
bool Vis[MAXN];
int S, T;
int Dis[MAXN], Cur[MAXN];
inline bool SPFA () {
memset (Dis, 63, sizeof Dis);
deque <int> q;
q.push_front (S), Dis[S] = 0;
int u, v, w, f;
while (!q.empty ()) {
u = q.front (), q.pop_front (), Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w;
if (!Vis[v]) {
if (q.empty () || Dis[v] > Dis[q.front ()]) q.push_back (v), Vis[v] = 1;
else q.push_front (v), Vis[v] = 1;
}
}
}
}
if (Dis[T] < 1e9) return 1;
return 0;
}
long long MinC = 0;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
Vis[x] = 1;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0;
while (SPFA ()) F += DFS (S, INF);
return F;
}
}
一些优化点
- 如果 建双向边 那么一般 只需要改动 \(Add\_Edge\) 使反边 流量 / 费用 等于正边即可
- 万恶的 当前弧!
- \(SPFA\) 可以应用 双向队列优化!
- 注意到其实 最后 \(while\) 里的 \(DFS\) 不需要再套 \(while\),不知道以前在写啥(最大流 一样)
- yny の 神秘优化?(详见 最大流 部分)
- 对 \(Dinic\) 效率有较高要求的话 在多数题也可以尝试 \(Dijkstra\) 的实现
- 对于特定题采用 动态加边 的方式,详见下文对 Luogu P2050 [NOI2012] 美食节 的讲
正确性?
前日与 \(\textsf {Meatherm}\) 先生讨论了这个实现的 正确性问题,豁然开朗!
首先十分重要的一点,基础的增广路算法 不能处理任何时候的 负圈
\(\textsf {Meatherm}\) 先生 那天提出一种情况
如果我现有边 \(A, B\) 并 \(Cost = 2, Flow = 1\)
再加入边 \(C\) 有 \(Cost = 1, Flow = 1\) 再跑一次
最小费用是几喃?几费喃?三费!
然后可以尝试自己跑跑 平凡的增广路费用流 板子,\(MinCost = 4\) !!!
S = 1, T = 3; Add_Edge (1, 2, 1, 2); Add_Edge (2, 3, 1, 2); MCMF(); Add_Edge (1, 2, 1, 1); // Add_Edge (1, 2, 1, 3) is Right MCMF(); cout << MINC << endl; // 流程
(当然也很有可能直接 死循环)
怎么回事 呢?考虑第一次增广的时候,\(A, B\) 满流,于是反边有 \(1\) 的可用流量
加入 \(C\) 之后,显然 \(A\) 的反边和 \(C\) 形成了 负环!(隐藏负环 \(+1\))寄!
(把 \(C\) 边权改成 \(> 2\) 就不会出事)
由此也启示出一个需要注意的点
基础的增广路算法 动态加边 的时候需注意 不能产生负环(不应出现 同流同起始边费用更小 之类的情况)
(但无需担心是否会在跑的时候产生 额外负环,考虑每次选的是 最短路,容易证明)
然后考虑为什么 最短增广路 就可以得到 最小费用最大流
我们似乎得出了一些 共同认同的 感性理解 以及 理性证明 如下
首先你的 最短路算法 显然是从 最大流 \(Dinic\) 的 \(BFS\) 扩展而来
保证有 可行流增广路 就 不会停止 的特性
那么在 这一部分
然后只需证明它能 取到最小费用 就行
感性理解 就是
我每次增广,走了 当前的最优解,留下了 给以后反悔用的边
每一次对 原图的影响 实质上相当于 选了一条边 并 反悔了一些边(就像 反悔贪心)
反边消除了 可能的后效性影响,于是直接做,就很对
理性一点的话
考虑我们 增流的过程,每次找 最短路 增流
设 \(F_i\) 在这个图上表示 流量为 \(i\) 时的最小费用
然后假使你 当前要增加 \(1\) 的流量,并且 无后效性
显然直接 贪心在最短路上增流 就行,于是一定有 \(F_{i + 1} = F_i + Dis_{S-T} * MinFlow\)
(\(MinFlow\) 是 最短路上的可行流量最小值)
到这里,正确性的问题差不多解决了(可能写的不是很清楚?)
优劣分析
优势就是十分常见,这使得 相关题目 根本不会卡你这种东西
并且有很多 基于此研发的优化 以及 使用这个算法的题解
使得其 可拓展性强,适用范围广,也很好学
题外?话 #2
有几次发现在 最大流 中,\(EK\) 和 \(Dinic\) 效率差异巨大
但是好像在 费用流 中这东西就不明显,以至于多数情况都不会卡 \(EK\) 实现的 费用流
十分神秘,然后想了半天似乎是这么一回事
就 最大流 只要求 分层,同一层的都可以被增广,所以 \(Dinic\) 多路一次性可以增广很多
相较 \(EK\) 一次只能 增广一条路,效率优势明显
而 费用流 求的是 最短路,图中最短路长相等的路径 不会太多
所以 \(Dinic\) 一次也 不会增广太多路径,优势就不明显
而从理论上来讲,费用流 \(Dinic\) 适合那种 边的费用较为固定,种类较少
但是 边数又比较多 的题,诸如 二分图左右部点连边,但边权多为 \(1\) 时
此类题 相较于 \(EK\) 就会有 明显优势
劣势也较为明显,即 在不加一些玄学优化的情况下,效率不很理想
当 一些毒瘤出题人 要求卡常或针对性优化时,就很 容易寄了
同时它 天生不支持处理任何有负环的情况,如果要使用 消圈算法,不如看看下面这个
并且实现细节还是较多,很容易 忘东忘西然后调半天(多可以参照 最大流 \(Dinic\))
\(Simplex\)
即 网络单纯形 法,由 线性规划问题 的 单纯形算法 变形而来
(循环流 可以规约成 线性规划问题的标准型,而 有源汇费用流 加边 就能变成循环流)
关于 线性规划 与 网络流 基本原理这一部分可以看 这个(感觉挺能懂的一个 课件)
这里只讲 算法步骤,实现细节 之类的
算法步骤
- 为了满足 线性规划标准形,我们先把图转成 循环流,也就是 \(T\) 向 \(S\) 连容量 \(INF\),费用 \(-INF\) 边
这个也可以理解成为 找负环增流 做准备,因为多数情况下 建出的图没有负环,而加边后就构造出一个
- 之后我们找到一个 可行支撑树,也就是 单纯形算法中的 基
这里 可行支撑树 就是一棵树上都是 未满流边 的 生成树,也即 在支撑树上存在可行流
- 之后就开始 判负环,具体就是 枚举边,判断在 支撑树 上 加入该边后是否形成 负环
显然加入一边会形成一个 基环树,然后有一个 均摊十分快 的方法来判断 是否为负环,后面会讲
- 加入 这条边(入基边),然后 找到负环上剩余流量最小边,并在这个过程中 存下这个负环
注意还需存储 最小边的绝对位置 和 相对位置(在 入基边 左还是右)
- 然后 推流,给这个环上所有边都加上 最小剩余流量,同时 更新费用(流量 * 途径边费用)
推完流之后就会 把一条边塞满,对应 单纯形算法 中的 转轴变换(的前半部分)
即把 入基变量与离基变量 在单纯形表中交点格 变换成 \(0\)
- 推流 之后 重构可行支撑树,可以发现就是 反转入基边到删除边之间的链
这里 反转的具体起止点 需要根据相对位置 简单分讨,依旧 后面随代码讲
这一步就对应 转轴变换 的后一部分,入基变量入基,离基变量离基,重构单纯形表
- 返回这一次的 费用,累加到 \(MinCost\),回到 \(Step~3\) 直到 图中没有负环
使得 没有更优的可能,对应使 所有非基本变量系数非负
- 现在 最大流 就是 \(Step ~ 1\) 中加上的 \(INF\) 边的流量,\(MinCost\) 要补上这个 最大流 * \(INF\)
考虑 循环流 保证流量平衡,故 \(S_{out} = S_{in}, T_{out} = T_{in}\)
那么显然,\(S_{out} -> T_{in}\) 就是理解的通常 最大流(在 不可增流时 从 \(S\) 流到 \(T\) 的流量)
然后 \(T_{out} -> S_{in}\) 就是初始加入的 \(INF\) 边,显然与 最大流 相等
补费用 这个操作十分容易理解,因为你 \(INF\) 边的有 额外的费用 \(-INF\),不应计入答案
到此结束,可能看上去 有些抽象,下面举一个 特例 帮助理解
真的是特例,只是用于帮助理解的,不应在这里纠结 是否能推广的问题
假设原图是 一条链,链上每条边费用容量不等(容量均为 正值)
于是先 执行 \(Step~1\),从 \(T\) 向 \(S\) 连了一条 \(INF\) 边,随后 构建支撑树(链的话好像...
显然这里 整个新图构成了一个负环,于是找到 环上流量最小的一条边
由于 \(T -> S\) 的边容量为 \(INF\),显然剩余容量最小边在 原图上
然后 推流,也就是 给整个环加上等于最小边容量的流量,更新费用,重构支撑树
这里的 支撑树 就是 原图 - 容量最小边 + \(INF\) 边
回到 \(Step ~ 3\) 试图找负环,由于 唯一不在支撑树上的边(刚的容量最小边)满流了
找不到负环了,结束!费用加上 \(INF\) * 容量最小边容量
这时候检查我们得到的答案,最大流 就是 链上容量最小边容量
最小费用 是 最大流量 * 链上边费用和(刚刚 整个链都在负环 中,都被统计了)
很对吧,那么
你已经学会一条链的情况了,快推广到任意图吧!
正确性?
根据刚刚的算法流程,我们惊奇的发现这东西与 增广路算法 相反
在 关于最小费用 的 正确性上没有太多疑难(没有负环了费用没法变小嘛)
但是在 最大流 的正确性上 可能值得思考(也有可能只有我愣了?)
这里我们回到 \(INF\) 边流量等于最大流 的证明并简单推广
很显然由于存在 容量 \(INF\),费用 \(-INF\) 的这条边,当 \(S->T\) 仍存在可行流时
\(S->T\) 的可行流路径 与 这条 \(INF\) 边 可以构成一个负环 并增流
于是类比于 上面链的例子,我们对其 增流 直到这条路径 有一条边被塞满
此时 \(S->T\) 就不存在可行路径,即 无法构造出更大流量情形,正确性显然
实现
由于 细节不多,这一部分会将 需要注意的点 顺带讲了,就不像 \(Dinic\) 一样单独列出了
笔者仍然采用 链式前向星 存边,为了方便,我们额外记录 边的起点 \(u\)
struct Edge {
int u, v, nxt;
long long f, w;
} E[MAXN * CONST];
仍然注意 边数和点数关系,小心 \(RE\)
来看看我们 需要 记录哪些东西
long long Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int S = 999, T = 998, Now = 0;
\(fa[i]\) 指 \(i\) 的父亲,\(fe[i]\) 指 \(fa[i] -> i\) 这条边的编号(有 \(E[fe[i]].u = fa[i],~E[fe[i]].v = i\))
\(Cir\) 用于记录负圈,\(Tag[i]\) 指 \(i\) 最近在 第几次推流时被访问,就像 最后修改时间
\(Pre[i]\) 指的是 支撑树上从根到 \(i\) 的路径费用和
\(Now\) 代表 当前时间,\(S, T\) 即 源汇点
我们可爱的 \(zhicheng\) 先生曾经写下 \(S = 0\) 这种东西... 这并不正确
这里的 \(Tag\) 其实就是为了 判断 \(i\) 的 \(Pre\) 值是否需要更新
如果在 本次访问中已经更新过,则 \(Tag[i] = Now\) 无需再更新
否则由于在 \(Step~6\) 中 重构了支撑树,受影响的点 需要重新计算 \(Pre\)
首先我们 建立支撑树
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
Tag[x] = Nod, fe[x] = e, fa[x] = E[e].u;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
这里的 \(Tag\) 相当于类似 \(Vis\) 的用法,只是临时借用,不作 现在时间 的含义
注意判断 当前边是否有剩余容量!
然后需要一个函数来维护 \(Pre\),这里暴力维护就行
inline long long Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
注意 数据类型 的选择,更新后 打上时间戳(\(Tag\))
咱似乎很喜欢把 \(Sum(fa[x])\) 写成 \(Pre[fa[x]]\),跟唐氏儿一样
之后就是 重点:推流环节
inline long long Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Del = 0, P = 2, Cnt = 0;
++ Now; // 这真不能忘!
// 这里是找 LCA 的过程,LCA 就是环的顶部,同时要对路径打时间标记
// 因为这个环上的点 都 有可能 受后面 Step.6 影响,需要更新 Pre
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
// 这里找环的最小容量边
long long Cost = 0, F = E[x].f;
for (int u = E[x].u; u != lca; u = fa[u]) {
// 在 入基边 的左边(起点 -> LCA)
Cir[++ Cnt] = fe[u];
if (E[fe[u]].f < F) F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
// 在 入基边 的右边 (终点 -> LCA)
Cir[++ Cnt] = fe[u] ^ 1; // 记住这部分要取 反边!
// 考虑 树是向下的,但这个环是 下 -> 入基边 -> 上 的,所以取 反边
if (E[fe[u] ^ 1].f < F) F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x; // 这也不能忘
for (int i = 1; i <= Cnt; ++ i) // 推流 并 更新费用
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost; // 如果 入基边 就是 最小容量边,那 支撑树 根本不用改
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v); // 如果 最小容量边 在 入基边右侧,则起点在 u
int Lst_u = v, Lst_e = x ^ P, Tmp; // 否则起点在 v
while (Lst_u != Del) { // 遍历直到删除的那条边
Lst_e ^= 1, Tag[u] --;
swap (fe[u], Lst_e); // 反转每条边的方向
Tmp = fa[u], fa[u] = Lst_u, Lst_u = u, u = Tmp; // 向上走
}
return Cost;
}
我是最大唐氏,居然纠结这个 \(Cost\) 返回的是不是负的...
然后就是 算法的主函数,照着步骤写就行
inline long long Simplex () {
Add_Edge (T, S, INF, - INF); // 加 INF 边
Init_ZCT (T, 0, ++ Now);
Tag[T] = ++ Now, fa[T] = 0; // 注意前面用来当 Vis 的 Now 不能再用力
bool Run = 1;
while (Run) { // 直到没有负圈
Run = 0;
for (int i = 2; i <= tot; ++ i) // 遍历边
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f; // INF 边流量即为 最大流
}
判断 负圈 条件就是 入基边权 + 左边边权 + 右边反边权 的意思,\(LCA\) 以上部分被抵消了
最后放个 完整的板子
namespace MCMF {
const int MAXN = 100005;
const long long INF = 1e9; // Don't Be Too Large
struct Edge {
int u, v, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
long long Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Now = 0, S = 114514, T = 191981;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
inline long long Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline long long Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
long long F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (F > E[fe[u]].f)
F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (F > E[fe[u] ^ 1].f)
F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
long long MinC = 0;
inline long long Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0, ++ Now);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
注意 \(INF\) 不要开太大,因为可能的 \(Cost_{max}\) 取值应该在 \(INF * |E|\) 左右
拓展功能!
来看看它都能支持 什么花活
- 考虑 分块 维护链的信息,效率会提高,本人不会,但是可以看 这个
- 想到 链加,链求和,链反转,\(LCT\) 维护是个可行思路(完全不会)
这东西好像现在 网上没看到有人写... 期待有巨佬实现
可能前面讲的 两点 都没啥用... 因为从效率上讲,这玩意儿已经 十分的高了
- 可以支持 动态加边,注意一下 \(INF\) 边的位置就行
这里给出一份 很丑的实现(因为每次都重建了 \(INF\) 边并 重构生成树)
咱认为 \(INF\) 边的位置应该可以靠 记录(当然最后就不能用 \(tot\) 来直接指向)
然后 重构生成树 当且仅当 加边的过程中 有新点加入,否则 是不必要的
\(UPD ~ 24.03.18\)
也可以 简单的删除边 \(tot\) 和 \(tot - 1\)(反边),然后对应更改 \(Head\) 数组
这时候 你可能会发现 不删边直接跑 也不会错,怎么会是呢?
实际上注意到我们每次会 新建一条 \(T \to S\) 的边,然后跑以 \(T\) 开始的 生成树
于是我们 第一条边 就会遍历到 刚新建的 \(T \to S\),然后 \(S\) 就被标记了
之后遍历到的(原来建出来的)\(T \to S\) 边 就会被忽略(\(S\) 已经访问过了)
于是每次都只有 最新的 \(T \to S\) 边 在生成树里,会被增流
而一般情况下 还没遍历到之前的 \(T \to S\) 边时 增流已经完成了
故正确性一般不会有问题
(后面 Luogu P2050 [NOI2012] 美食节 的讲有 完整实现)
inline int Simplex () { Add_Edge (T, S, INF, - INF); Init_ZCT (T, 0); Tag[T] = ++ Now, fa[T] = 0; bool Run = 1; int F = 0; while (Run) { Run = 0; for (int i = 2; i <= tot; ++ i) if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0) MinC += Push_Flow (i), Run = 1; F += E[tot].f, Clear (tot); for (int i = 1; i <= M; ++ i) { if (E[H[G (i, Flr[i])]].f == 0) { ++ Flr[i]; for (int j = 1; j <= N; ++ j) Add_Edge (j, G (i, Flr[i]), 1, C[j][i] * Flr[i]); Add_Edge (G (i, Flr[i]), T, 1, 0); } } Add_Edge (T, S, INF, - INF), Init_ZCT (T, 0, ++ Now), Tag[T] = ++ Now, fa[T] = 0; } MinC += F * INF; return F; }
虽然十分丑陋,但是
跑得快嘛,不寒掺
- 可以 跑有负圈的的费用流(可能在说废话)
但是确实比 消圈算法 绝大多数时间快得多,而且代码真心不难
(前四个都是 \(Simplex\) 实现)
优劣分析
优势明显啊,可以跑负圈,代码细节少,支持常用扩展
跑得飞快*(甚至可以在 费用 \(0\) 的时候跑过 Luogu P4722 【模板】最大流 加强版 / 预流推进)
* : 这里指 平均时间复杂度 为 小常数 \(O(VE)\) (\(O(NM)\))—— 参考
放点神秘提交记录
还有一些不放了
本人非常喜欢这个东西,它可以帮你在乱搞的时候多过几个点
劣势主要还是相对 代码量会大一些,然后 跑的奇慢无比*
* : 指根据 单纯形算法 下界来看,这东西最慢是 指数级 的
但是从来没被卡过,也不知道咋卡
复杂度也有说是 \(O(M^2c_{max}f_{max})\) 的,就前面 推荐的那个课件
\(c_{max}\) 是 边上最大费用,\(f_{max}\) 是 边上最大容量(不计额外 \(INF\) 边)
两者都... 我只能说挺不符合实际的,玄学!
此部分参考(都写得十分的好)
一个神秘的问题
考虑 存边 到底是选择 \(vector\) 还是 链式前向星 呢?
先给出结论:如果 访问边的操作 比 建边等操作 多非常多,考虑 \(vector\),否则 链式前向星
这里的 非常多 一般是指 \(10\) 倍及以上,且次数差距应大于 \(10^7\) 次
如果打开 \(O2\) 优化,则 \(vector\) 在多数情况下占优
速度差距 来源于哪里呢?实际测试表明
- 建边 的时候 由于 \(vector\) 要新开空间,速度约是 链式前向星 的 \(50 \%\)
- 访问 的时候 由于 \(vector\) 使用 \(++ i\),而 链式前向星 需要多访问一个 变量,更慢
\(O0\) 不开优化 的情况下 \(vector\) 访问速度 约是 链式前向星 的 \(280 \%\)
实测如果 新建数组 \(K\),\(K_i = i + 1\),\(vector\) 自增改为 访问 \(K_i\)
则 速度退化 至 与 链式前向星 无异
- 访问 的时候 由于 \(vector\) 是连续空间,\(Cache\) 命中率高,所以更快
\(Cache\) 命中率大约是 链式前向星 的 \(160 %\),随数据变化
测试代码(调整 第一层循环 来改变 访问次数)
链式前向星
#include <bits/stdc++.h>
const int MAXN = 100005;
using namespace std;
struct Edge {
int to, nxt;
} E[1 << 27];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v) {
E[++ tot] = {v, H[u]}, H[u] = tot;
}
int t;
const int N = 50000000;
int main () {
for (int i = 1; i <= N; ++ i) Add_Edge (1, i);
cerr << (t = clock ()) << endl;
for (int j = 1; j <= 100; ++ j)
for (int i = H[1]; i; i = E[i].nxt);
cerr << clock () - t << endl;
return 0;
}
\(vector\) 自增
#include <bits/stdc++.h>
const int MAXN = 100005;
using namespace std;
vector <int> E[MAXN];
inline void Add_Edge (const int u, const int v) {
E[u].push_back (v);
}
const int N = 50000000;
int t;
int main () {
for (int i = 1; i <= N; ++ i) Add_Edge (1, i);
cerr << (t = clock ()) << endl;
for (int j = 1; j <= 100; ++ j)
for (int i = 0; i < (int) E[1].size (); ++ i);
cerr << clock () - t << endl;
return 0;
}
\(vector\) 访问数组
#include <bits/stdc++.h>
const int MAXN = 100005;
using namespace std;
vector <int> E[MAXN];
inline void Add_Edge (const int u, const int v) {
E[u].push_back (v);
}
const int N = 50000000;
int t, K[N];
int main () {
for (int i = 1; i <= N; ++ i) Add_Edge (1, i);
for (int i = 0; i <= N; ++ i) K[i] = i + 1;
cerr << (t = clock ()) << endl;
for (int j = 1; j <= 100; ++ j)
for (int i = 0; i < (int) E[1].size (); i = K[i]);
cerr << clock () - t << endl;
return 0;
}
在 \(Ubuntu ~ Server ~ 20.04.6\) 环境下,使用 \(perf\) 工具进行测试
套路
常见的一些套路,再整点题
网络流与线性规划 24 题 里面还是有很多好套路的,也比较板,该做
最大流最小割定理
即熟悉的 一个网络中 最大流流量 = 最小割容量
感性理解 这个东西比较简单,也比较对
考虑 割 相当于一个 瓶颈,因为 割断了之后 无法增流
而没割断时 \(S\) 与 \(T\) 连通,一定可以增流
所以你 最大流 对应的 残量网络 就是恰好 \(S\) 与 \(T\) 不连通时
即恰好 割断 的时候,对应 最小的割(每个路径上的 瓶颈 都刚好满流的时候)
应用就是 求最小割 时 直接用 最大流的板子 或 思路
很标准的题 愣是找不到什么,但确实是有些十分经典的
Luogu P4313 文理分科
非常典型的 二选一 外加 附加条件贡献,可以考虑到 最小割
先来个简单情况,如果只有 一个人,两种选择,没有附加条件
显而易见的 源点 连 人 连 汇点,两边流量分别是 两种选择的贡献
然后 找最小割 就行
感性理解这个东西,我 割掉一条边,就相当于放弃这个选择
那么求 最大贡献,就是要求 放弃的最少,就是 最小割
回到原题,二选一 的部分和 一个人的情况一样,源点 对应 理科,汇点 对应 文科
源点汇点直接连,流量 放 对应满意值 就行,关键是这个 相邻同学的选择
可以考虑 这份附加贡献 要怎么拿到?相邻的四个同学选择相同才行
反过来说,我们可以构造一种方式
使得只要 拿到一科的附加贡献,那么 相邻的人对应点 和 对应科目的点 就 在同一个连通块内
也就是让他们和对应科目 无法被割断
想到我们求的是 最小割,肯定是 不会割流量大的边的
于是我们对每个人 新建两个点
源点连接一个新点,流量为 都选理科的贡献,新点连 相邻的人加上中间的人 ,流量 \(INF\)
同理 五个人 连 另一个新点,容量 \(INF\),另一个新点 连 汇点,容量为 都选文科的贡献
连接共 \(10\) 条 \(INF\) 边
可以保证 \(INF\) 边 不会被割,很好地满足了我们想要的性质,于是跑板子就行了
由于 边是有向的,这里可以保证 不割掉 \(INF\) 边,五人选科不同时 点互不连通
最后答案是 总共的可能贡献 减去 最小割容量
建边细节不多,板子细节在上面,这里直接放代码
#include <bits/stdc++.h>
const int MAXN = 200005;
const long long INF = 1e16;
using namespace std;
namespace Dinic {
struct Node {
int to, nxt;
long long f;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f) {
E[++ tot] = {v, H[u], f}, H[u] = tot;
E[++ tot] = {u, H[v], 0}, H[v] = tot;
}
int S = 114514, T = 191981;
int Cur[MAXN], Dep[MAXN];
inline bool BFS () {
fill (Dep, Dep + MAXN, -1);
queue <int> q;
q.push (S), Dep[S] = 0, Cur[S] = H[S];
int u, v, f;
while (!q.empty ()) {
u = q.front (), q.pop ();
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, f = E[i].f;
if (Dep[v] == -1 && f) {
Dep[v] = Dep[u] + 1, Cur[v] = H[v], q.push (v);
if (v == T) return 1;
}
}
}
return 0;
}
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dep[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
}
}
return F;
}
inline long long dinic () {
long long F = 0;
while (BFS ()) F += DFS (S, INF);
return F;
}
}
namespace Value {
int N, M, P;
long long Sum = 0;
inline int G (const int x, const int y, const int t) {
return x * 100 + y + t * 15000;
}
using namespace Dinic;
inline void Solve () {
cin >> N >> M;
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
cin >> P, Sum += P, Add_Edge (S, G (i, j, 1), P);
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
cin >> P, Sum += P, Add_Edge (G (i, j, 1), T, P);
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j) {
cin >> P, Sum += P;
Add_Edge (S, G (i, j, 2), P);
Add_Edge (G (i, j, 2), G (i, j, 1), INF);
if (i > 1) Add_Edge (G (i, j, 2), G (i - 1, j, 1), INF);
if (i < N) Add_Edge (G (i, j, 2), G (i + 1, j, 1), INF);
if (j > 1) Add_Edge (G (i, j, 2), G (i, j - 1, 1), INF);
if (j < M) Add_Edge (G (i, j, 2), G (i, j + 1, 1), INF);
}
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j) {
cin >> P, Sum += P;
Add_Edge (G (i, j, 3), T, P);
Add_Edge (G (i, j, 1), G (i, j, 3), INF);
if (i > 1) Add_Edge (G (i - 1, j, 1), G (i, j, 3), INF);
if (i < N) Add_Edge (G (i + 1, j, 1), G (i, j, 3), INF);
if (j > 1) Add_Edge (G (i, j - 1, 1), G (i, j, 3), INF);
if (j < M) Add_Edge (G (i, j + 1, 1), G (i, j, 3), INF);
}
cout << Sum - dinic () << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
Luogu P3227 [HNOI2013] 切糕
典中典
网络流套路就是 网络流 \(24\) 题 加上 切糕 吧 —— \(\textsf {Meatherm}\)
看完题意可以想到一些 大概的思路
就是有 \(P * Q\) 个链,每个链上 割一个点,有一定限制,求 割的点权值和最小
还是先考虑 简化版本,也就是 没有限制 情况
显然我们可以直接排序贪心 用 最小割 的思想
源点 连接 每条链的头,每条链的尾 连 汇点,直接跑 最大流(最小割) 板子就行
然后加上限制,和上面的题 一样的想法,我们想要 构造一些边
使得我在 某条链上割了某个点后,相邻链上就 只能割符合条件的点
通过 上面的题我们发现,我们可以 添加 \(INF\) 边 来 “保护” 一些边,使之不被割
换句话说,我们在使得 非法操作无效化
在这里也一样,假设我已经割了位于 \((x, y)\) 链上的高度 \(h\) 为 \(k\) 的点 \((k > D)\)
这里钦定 源点 连 链头 \(h = 1\),链尾 \(h = R\) 连 汇点
那么显然,在 相邻四条链 中,\(h < k - D\) 与 \(h > k + D\) 部分上的点 都不应被割
连接 \((x, y, k)\) 与 \((x - 1, y, k - D), (x + 1, y, k - D), (x, y - 1, k - D) ,(x, y + 1, k - D)\)
容量设为 \(INF\),这时显然 割掉相邻链中 \(h < k - D\) 的点 已经无效了
感觉还要考虑 \(h > k + D\) 的部分?仔细想想,其实我们已经 做完了
因为 相邻链上 \(h = k + D\) 的点 会连接到 这条链 \(h = k\) 的位置
也就是 我割了 相邻链上 \(h > k + D\) 的点 的话
再割 这条链 \(h = k\) 的 的这个操作就会被 无效化
反之自然成立(因为 两条链上 流量 并不具有严格先后顺序)
于是直接结束,跑板子,输出最小割容量 即可,依旧只放代码
#include <bits/stdc++.h>
using namespace std;
namespace Dinic {
const int MAXN = 200005;
const long long INF = 1e16;
struct Node {
int to, nxt;
long long f;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f) {
E[++ tot] = {v, H[u], f}, H[u] = tot;
E[++ tot] = {u, H[v], 0}, H[v] = tot;
}
int S = 114514, T = 191981;
int Cur[MAXN], Dep[MAXN];
inline bool BFS () {
fill (Dep, Dep + MAXN, -1);
queue <int> q;
q.push (S), Dep[S] = 0, Cur[S] = H[S];
int u, v, f;
while (!q.empty ()) {
u = q.front (), q.pop ();
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, f = E[i].f;
if (Dep[v] == -1 && f) {
Dep[v] = Dep[u] + 1, Cur[v] = H[v], q.push (v);
if (v == T) return 1;
}
}
}
return 0;
}
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dep[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
}
}
return F;
}
inline long long dinic () {
long long F = 0;
while (BFS ()) F += DFS (S, INF);
return F;
}
}
namespace Value {
int P, Q, R, D, C;
inline int G (const int x, const int y, const int z) {
return x * 2000 + y * 45 + z;
}
using namespace Dinic;
inline void Solve () {
cin >> P >> Q >> R >> D;
for (int i = 1; i <= R; ++ i)
for (int j = 1; j <= P; ++ j)
for (int k = 1; k <= Q; ++ k) {
cin >> C;
if (i == 1) Add_Edge (S, G (i, j, k), INF);
if (i == R) Add_Edge (G (i, j, k), T, C);
else Add_Edge (G (i, j, k), G (i + 1, j, k), C);
if (i > D) {
if (j > 1) Add_Edge (G (i, j, k), G (i - D, j - 1, k), INF);
if (j < P) Add_Edge (G (i, j, k), G (i - D, j + 1, k), INF);
if (k > 1) Add_Edge (G (i, j, k), G (i - D, j, k - 1), INF);
if (k < Q) Add_Edge (G (i, j, k), G (i - D, j, k + 1), INF);
}
}
cout << dinic () << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
等价流树
这是一个 非常有趣,也很有用的概念
它和 最短路树 相似,可以快速地 求出 无向图 两点间的 最小割容量 / 最大流 的值
构造一棵等价流树
我们如果把割了之后的 源点连通块 和 汇点连通块 看成两点,编号为 源汇点编号 并 连边
然后 最小割容量 为边权,这样 对两个连通块分治,就可以得到 一棵树
比如我们拿到这么一个图,源点 \(S = 1\),汇点 \(T = 6\)
(后续 淡灰色背景 为 原图,淡黄色背景 为 等价流树)
显然 它的最小割容量 是 \(14\),一种 最小割 如下
这个最小割,将点集 划分成 \({1, 3, 5}\) 和 \(2, 4, 6\) 俩部分
于是可以 建出等价流树上的第一条边 \((1, 6)\)
接着 分别分治 俩部分
此时点集 \(1, 3, 5\) 有 \(S = 1, T = 5\)
(
懒得再标记边了)显然 最小割容量 为 \(11\)
这将 当前点集 分为 \(1,3\) 与 \(5\) 俩部分,同时可以 建出等价流树上一条边 \((1, 5)\)
接着处理 点集 \(1, 3\),求出 两点间最小割
最小割容量 为 \(9\),将 点集 分开成 \(1\) 和 \(3\) 两个点集
同时 等价流树上加边 \((1, 3)\)
至此,点集 \(1, 3, 5\) 被分治成 三个 元素个数为 \(1\) 的点集,这边结束!
同理 对点集 \(2, 4, 6\) 操作(不配图了)
第一次 \(S = 2, T = 6, MinCut = 7\),分成点集 \(2\) 和 \(4, 6\),等价流树 连边 \((2, 6)\)
第二次操作点集 \(4, 6\) ,有 \(S = 4, T = 6, MinCut = 15\),等价流树 连边 \((4, 6)\),结束
(此时的 割 为 \((1, 2), (4, 6), (5, 6)\))
于是最后的 等价流树 长这样
等价流树的性质
可以结合上面的 构造 来理解
- 树上边权 等于 两端点 最小割容量
显然,因为 加边规则 如此
- 这是一颗 生成树
因为 分治过程 是在 端点序 上进行的
并且每次分治 点集不交
显然一直到连通块大小为 1,每次加边一定有 一个端点为新的
易证
- 原图 任意两点最小割容量 等于 树上对应两点 路径最小值
根据 性质 1,设有 \(MinCut_{A, B} = k_1, MinCut_{B, C} = k_2\) 且 \(k_1 < k_2\)
则显然 \(MinCut_{A, C} \ge k_1\)
假设有 \(MinCut_{A, C} > k_1\) 则 原图 可以有 路径 \(A -> C -> B\)
使得 \(MinCut_{A, B} > k_1\),不符合条件
故假设不成立,原命题得证
- 查询 任意两点间最小割 时间复杂度 很低,建树需要跑 \(O~(N)\) 次 最大流
\(UPD ~ 24.03.13\)
建树还真不是 \(O(\log N)\) 次 最大流,而是 \(O(N)\) 次
但是暴力是 \(O(N ^ 2)\) 次的,赢!
查询的 具体复杂度 取决于你 最后的处理方式,毕竟树都建出来了
相当于就是 维护两点间路径最小值(
再跑一遍 最大流)对于 点数小,询问多 的,甚至可以直接 把所有答案预处理,然后 \(O(1)\) 查询
修正
\(\textsf {Meatherm}\) 先生,提出了一个修正,非常的规范!
这个树是 等价流树 而非 最小割树
此处的区别在于 最小割树 需要保证
树上两点 断边后 对应的两个连通块 恰是 断的边两端点 在 原图上的一种最小割
这一部分有关性质,及 最小割树 的一些东西 详见 这篇论文 \(P63\) 起的文章
等价流树的实现
Luogu P4897 【模板】最小割树(Gomory-Hu Tree)
前面 求最大流的板子 没有任何需要修改的,重点是 下面这段核心代码
namespace MinCutTree {
struct Edge {
int u, v, w;
} G[MAXN]; // 存 等价流树 的边
int Cnt = 0, A[MAXN], B[MAXN];
using namespace Dinic;
inline void Build (const int L, const int R) { // 分治
if (L >= R) return;
int l = L - 1, r = R + 1;
G[++ Cnt] = {S = A[L], T = A[R]}, G[Cnt].w = dinic ();
// A[L, R] 即当前点集,加边连接 当前点集两端,权值为 最小割容量
for (int i = L; i <= R; ++ i) {
// 判断连通块,考虑最后一次 BFS 遍历到的 (Dep 被赋值过的),一定连通 源点 S
if (Dep[A[i]] != -1) B[++ l] = A[i];
else B[-- r] = A[i];
}
for (int i = L; i <= R; ++ i) A[i] = B[i];
// A[L, l] 是 此点集中 与 源点 联通的
// A[r, R] 是 此点集中 与 汇点 联通的
for (int i = 2; i <= tot; i += 2) E[i].f = E[i ^ 1].f = (E[i].f + E[i ^ 1].f) >> 1; // 重置流量
return Build (L, l), Build (r, R); // 递归建树
}
}
然后这个题吧,数据范围比较水,每次询问 暴力跳找最小值 也能过
本题时限 \(500~ms\),暴力查询 \(O(NQ)\) 刚好 \(5e7\),十分规范!
完整代码就不放了,没有实际意义
Luogu P4123 [CQOI2016] 不同的最小割
无需多言,建出等价流树,遍历检查 等价流树上有多少 权值不同的边 就行了
(拿 \(unordered\_map\) 存一下,最后输出 \(size\) 就行)
代码参考
#include <bits/stdc++.h>
const int MAXN = 1005;
const int INF = 1e9;
using namespace std;
namespace Dinic {
struct Edge {
int to, nxt, f;
} E[MAXN << 5];
int H[MAXN], Cur[MAXN], Dep[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const int f) {
E[++ tot] = {v, H[u], f}, H[u] = tot;
E[++ tot] = {u, H[v], f}, H[v] = tot; // 无向图!
}
int S, T, K;
inline bool BFS () {
fill (Dep, Dep + MAXN, -1);
for (int i = 1; i <= K; ++ i) Cur[i] = H[i];
queue <int> q;
q.push (S), Dep[S] = 0;
int u, v, f;
while (!q.empty ()) {
u = q.front (), q.pop ();// Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, f = E[i].f;
if (Dep[v] == -1 && f) {
Dep[v] = Dep[u] + 1, q.push (v);
if (v == T) return 1;
}
}
}
return 0;
}
inline int DFS (const int x, const int MAXF) {
if (x == T || MAXF == 0) return MAXF;
int F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) {
int TmpF = DFS (E[i].to, min (E[i].f, MAXF - F));
if (!TmpF) Dep[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
}
}
return F;
}
inline int dinic () {
int F = 0;
while (BFS ()) F += DFS (S, INF);
return F;
}
}
namespace MinCutTree {
struct Edge {
int u, v, w;
} G[MAXN];
int Cnt = 0, A[MAXN], B[MAXN];
using namespace Dinic;
inline void Build (const int L, const int R) {
if (L >= R) return;
int l = L - 1, r = R + 1;
G[++ Cnt] = {S = A[L], T = A[R]}, G[Cnt].w = dinic ();
for (int i = L; i <= R; ++ i) {
if (Dep[A[i]] != -1) B[++ l] = A[i];
else B[-- r] = A[i];
}
for (int i = L; i <= R; ++ i) A[i] = B[i];
for (int i = 2; i <= tot; i += 2) E[i].f = E[i ^ 1].f = (E[i].f + E[i ^ 1].f) >> 1;
return Build (L, l), Build (r, R);
}
}
namespace Value {
int L, R, W, N, M;
unordered_set <int> Ans;
inline void Solve () {
using namespace Dinic;
using namespace MinCutTree;
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M; K = N;
for (int i = 1; i <= N; ++ i) A[i] = i;
for (int i = 1; i <= M; ++ i)
cin >> L >> R >> W, Add_Edge (L, R, W);
Build (1, N);
for (int i = 1; i <= Cnt; ++ i) Ans.insert (G[i].w);
cout << Ans.size () << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
黑白染色
这个技巧在很多 网格图 需要用到
特别是神秘的 \(1 \times 2\) 骨牌这种问题,下面 三道例题 都用到这个技巧
Luogu P7231 [COCI2015-2016#3] DOMINO
看到这类 骨牌问题,可以先考虑对网格 黑白染色
\((x + y) \bmod 2 = 1\) 染黑,反之染白(反过来也行)
为了保证 骨牌不重叠,我们先限制使得 每个点只能覆盖一次
即 源点 连 黑点,容量 \(1\),白点 连 汇点,容量 \(1\)
又要求 覆盖的点权和,一共 两个限制,考虑 费用流,点权设成费用
保证了骨牌不重叠后,只需要保证骨牌能正常 占据相邻的两格 即可
于是 黑点 向 四周白点 连 容量 \(1\) 的 免费边
显然 一个黑点 只能选一次,而当一个 黑点 流量流到 某个 白点 后
该 白点 满流,也不会在后面增广到,此时 路径费用 恰好为 这对 黑白点 权值和
非常规范!
这个题(可能因为正解不是 费用流 )卡空间,火大
想想会发现单点 流量 和 费用 都很小
类型直接从 \(int\) 改成 \(short\),\(359.24 ~ MB\) 轻松通过
参考代码(\(Simplex\))
#include <bits/stdc++.h>
const int MAXN = 4500005;
const short INF = 32000;
using namespace std;
inline bool G (const int x, const int y) {
return (x + y) % 2;
}
inline int P (const int x, const int y) {
return x * 2000 + y;
}
namespace MCMF {
const int INF = 32760; // Don't Be Too Large
struct Edge {
int u, v, nxt;
short f, w;
} E[24000000];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const short f, const short w) {
E[++ tot] = {u, v, H[u], f, short (+ w)}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, short (- w)}, H[v] = tot;
}
int Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Now = 1, S = 4002001, T = 4002002;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
inline int Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline int Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
int F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (F > E[fe[u]].f)
F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (F > E[fe[u] ^ 1].f)
F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
int MinC = 0;
inline int Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
namespace Value {
int N, K, Sour = 4002003;
short Num[2005][2005];
long long Sum = 0;
inline void Solve () {
cin >> N >> K;
using namespace MCMF;
Add_Edge (S, Sour, K, 0);
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= N; ++ j) {
cin >> Num[i][j], Sum += Num[i][j];
if (G (i, j)) Add_Edge (Sour, P (i, j), 1, - Num[i][j]);
else Add_Edge (P (i, j), T, 1, - Num[i][j]);
}
for (int i = 1; i <= N; ++ i) {
for (int j = 1; j <= N; ++ j) {
if (G (i, j)) {
if (i != 1) Add_Edge (P (i, j), P (i - 1, j), 1, 0);
if (j != 1) Add_Edge (P (i, j), P (i, j - 1), 1, 0);
if (i != N) Add_Edge (P (i, j), P (i + 1, j), 1, 0);
if (j != N) Add_Edge (P (i, j), P (i, j + 1), 1, 0);
}
}
}
cerr << Simplex () << endl;
cout << Sum + MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
Luogu P4701 粘骨牌
首先考虑 转化,将 骨牌的移动 转化成 空白格 的移动
构造使其 无法移动到 关键格子
对棋盘 黑白交替染色,观察性质(钦定 角上颜色 是 黑)
由于 \(N, M\) 均为奇数,那么 四个角颜色都是黑的
- 空白格 一定是 黑的
显然可以发现 棋盘中 黑格比白格总数多 \(1\)
而 一个骨牌 一定覆盖 一黑一白,故得证
- 每个骨牌 无论移动与否 一定覆盖一个特定白格
只有 一个空格,其实 每个骨牌 只有一种 可能的移动方式
接着往下,对应的,我们发现
- 空白格 也只有一种 移动方式
即在 对应骨牌 移动后,空白格 跳到 相邻的黑格 上
于是
若 骨牌横向,则 \((x, y)\) 与 \((x + 2, y)\) 的 黑格 可跳
若 骨牌纵向,则 \((x, y)\) 与 \((x, y + 2)\) 的 黑格 可跳
我们考虑 不露出关键点 就是 关键点不可达
而 固定骨牌,则对应位置 空白格 不能跳,相当于 删去对应边
求最小的 固定骨牌数 本质就是 求 权值和最小的边集 使 源汇不可达
不就是 最小割吗? 跑板子就行了,代码先鸽了(写的太丑)
Luogu P4003 无限之环
非常经典的 一道好题,用到了 黑白染色,也会用到一些下面会讲的 一个套路
就是 流量平衡 或 出入度平衡 性质的利用
首先先考虑 平衡的性质
也就是我一个水管 流出多少水,最后总会在某个水管 流入多少水
然后是怎么联系到 黑白染色的呢?
因为整个图 无源无汇,也就是没有给定 流入水的 地方 和 流出水的 地方
所以这个 源汇,就得我们 自己来加
也就是 要给一些地方 加水,让一些地方 接水
最后使得判断 加水和接水 的量相等,去得到答案
怎么来加呢?
显然,我们需要保证 每个水管连通块 都至少有 一个入水口 和 一个出水口
观察可以发现,一个 连通块的大小 至少为 \(2\),怎么保证上面的条件呢?
黑白染色,就呼之欲出了
不言而喻,一目了然
我们给整个棋盘黑白染色,其中钦定所有 黑格为出水口,白格为入水口
具体建图可以考虑 每个节点分成五个点,中间一个连接 超级源 / 汇点
四周四个 出 / 入口 与相邻格子 出 / 入口 相连接
之后 每种水管 对应的 出 / 入口 和 中心点 相连
如何解决旋转的问题?这里给出一种例子
连接 左 和 上 的水管,旋转 \(90°\) 后就变成连接 右 和 上 或连接 左 和 下 了
如果 左上 \(\to\) 右上,相当于可以花费 \(1\) 的代价使本身从 左 出 / 入 变成从 右 出 / 入
于是 左点 与 右点 相连即可
(偷懒可以连 双向边,不然可以 出的时候左向右,入的时候右向左)
那么 左上 \(\to\) 左下 也是同理,上下 连接即可
这里旋转 \(180°\)恰好是两次 \(90°\)的 边 都用上的结果,所以 不用 特意连 新的边
具体对应 不同水管的 (旋转)连边方式可以参考 洛谷上的第一篇题解
也可以直接看下面的代码,应该比较清晰(\(0\) 是 中间点)
真是个码农题
#include <bits/stdc++.h>
const int MAXN = 200005;
const int INF = 1e7;
using namespace std;
namespace MCMF {
struct Edge {
int u, v, nxt;
int f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const int f, const int w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Pre[MAXN];
int S = 114514, T = 191981, Now = 1;
inline void Init_ZCT (const int x, const int e) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = 1;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Now && E[i].f) Init_ZCT (E[i].v, i);
}
inline int Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline int Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
int F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (E[fe[u]].f < F) F = E[fe[u]].f, P = 0, Del = u;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (E[fe[u] ^ 1].f < F) F = E[fe[u] ^ 1].f, P = 1, Del = u;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lst_u = v, Lst_e = x ^ P, Tmp;
while (Lst_u != Del) {
Lst_e ^= 1, -- Tag[u];
swap (Lst_e, fe[u]);
Tmp = fa[u], fa[u] = Lst_u, Lst_u = u, u = Tmp;
}
return Cost;
}
int MinC = 0;
inline int Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += INF * E[tot].f;
return E[tot].f;
}
}
namespace Value {
using namespace MCMF;
int N, M;
int Mp[2005][2005], Num[2005][2005];
int FlowS = 0, FlowT = 0;
inline int G (const int x, const int y, const int Dir) {
return x * (M + 10) * 5 + y * 5 + Dir;
}
inline int K (const int k) {
int Cnt = 0;
for (int i = 0; i < 4; ++ i) if ((k >> i) & 1) ++ Cnt;
return Cnt;
}
inline void Add_1 (const int x, const int y) {
if (Mp[x][y] == 5 || Mp[x][y] == 10) return ;
if (Mp[x][y] % 3 == 0) {
Add_Edge (G (x, y, 1), G (x, y, 3), 1, 1);
Add_Edge (G (x, y, 3), G (x, y, 1), 1, 1);
Add_Edge (G (x, y, 2), G (x, y, 4), 1, 1);
Add_Edge (G (x, y, 4), G (x, y, 2), 1, 1);
} else {
Add_Edge (G (x, y, 1), G (x, y, 2), 1, 1);
Add_Edge (G (x, y, 2), G (x, y, 3), 1, 1);
Add_Edge (G (x, y, 3), G (x, y, 4), 1, 1);
Add_Edge (G (x, y, 4), G (x, y, 1), 1, 1);
Add_Edge (G (x, y, 4), G (x, y, 3), 1, 1);
Add_Edge (G (x, y, 3), G (x, y, 2), 1, 1);
Add_Edge (G (x, y, 2), G (x, y, 1), 1, 1);
Add_Edge (G (x, y, 1), G (x, y, 4), 1, 1);
}
}
inline void Add_2 (const int x, const int y) {
if ((x + y) & 1) {
for (int i = 0; i < 4; ++ i)
if ((Mp[x][y]) & (1 << i))
Add_Edge (G (x, y, 0), G (x, y, i + 1), 1, 0);
} else {
for (int i = 0; i < 4; ++ i)
if ((Mp[x][y]) & (1 << i))
Add_Edge (G (x, y, i + 1), G (x, y, 0), 1, 0);
}
}
inline void Add_3 (const int x, const int y) {
if (((x + y) & 1) == 0) return ;
if (x > 1) Add_Edge (G (x, y, 1), G (x - 1, y, 3), 1, 0);
if (x < N) Add_Edge (G (x, y, 3), G (x + 1, y, 1), 1, 0);
if (y > 1) Add_Edge (G (x, y, 4), G (x, y - 1, 2), 1, 0);
if (y < M) Add_Edge (G (x, y, 2), G (x, y + 1, 4), 1, 0);
}
inline void Solve () {
cin >> N >> M;
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
cin >> Mp[i][j], Num[i][j] = K (Mp[i][j]);
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j) {
if ((i + j) & 1) Add_Edge (S, G (i, j, 0), Num[i][j], 0), FlowT += Num[i][j];
else Add_Edge (G (i, j, 0), T, Num[i][j], 0), FlowS += Num[i][j];
} // 连接超级源汇
if (FlowS != FlowT) return cout << -1 << endl, void ();
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
Add_1 (i, j), Add_2 (i, j), Add_3 (i, j);
// Add_1 处理旋转变换,Add_2 是中心和四周连,Add_3 是和相邻格子连
int Ans = Simplex ();
if (Ans != FlowS) return cout << -1 << endl, void ();
cerr << Ans << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
出入度平衡
听上去这是一个性质(确实
但是有一些题的 思路 的确和这个 密不可分
想不到啥好的类型名字,就用这个吧
Luogu P3965 [TJOI2013] 循环格
完美符合 这个专题 的一道题,反正我第一次做的时候比较蒙
观察性质,如果 每个格子是一个点
可以发现由于 箭头指向一个方向,所以 每个点的出度只有 \(1\)
由于 出入度平衡,显然 总入度 是 \(N*M\) 的
又考虑到要 构成循环,那么每个格子应当有 至少 \(1\) 的入度
所以每个点应当 入度为 \(1\),出度为 \(1\)
又可以发现,不同循环间的 路径应当没有交叉
否则是不能保证 每个点 出入度 均为 \(1\) 的
然后就可以想到 费用流,先 拆点
然后每个点的 左部点 向 当前指向方向右部点 连接容量 \(1\) 的 免费边
像其余 三个方向右部点 连接容量 \(1\),费用为 \(1\) 的边
最后源点向 所有左部点 连容量为 \(1\) 的免费边,所有右部点 向汇点 连相同的边
显然,最大流 即为 点数(所有点入度均为 \(1\) 时)保证了 上文性质
最小费用 就是 最少修改元素的个数,注意处理 到边界的循环 即可
#include <bits/stdc++.h>
const int MAXN = 100005;
const long long INF = 1e12;
using namespace std;
namespace MCMF {
struct Edge {
int u, v, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
long long Pre[MAXN];
int S = 11451, T = 19198, Now = 1;
inline void Init_ZCT (const int x, const int e) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = 1;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Now && E[i].f) Init_ZCT (E[i].v, i);
}
inline long long Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline long long Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Del = 0, Cnt = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
long long F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (E[fe[u]].f < F) F = E[fe[u]].f, P = 0, Del = u;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (E[fe[u] ^ 1].f < F) F = E[fe[u] ^ 1].f, P = 1, Del = u;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lst_u = v, Lst_e = x ^ P, Tmp;
while (Lst_u != Del) {
Lst_e ^= 1, -- Tag[u];
swap (fe[u], Lst_e);
Tmp = fa[u], fa[u] = Lst_u, Lst_u = u, u = Tmp;
}
return Cost;
}
long long MinC = 0;
inline long long Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
namespace Value {
int N, M;
char Opt;
using namespace MCMF;
inline int G (const int x, const int y, const bool f) {
return f * 5000 + x * 100 + y;
}
inline void Add (const int x, const int y, const char c) {
if (x > 1) Add_Edge (G (x, y, 0), G (x - 1, y, 1), 1, (c != 'U'));
else Add_Edge (G (x, y, 0), G (N, y, 1), 1, (c != 'U'));
if (x < N) Add_Edge (G (x, y, 0), G (x + 1, y, 1), 1, (c != 'D'));
else Add_Edge (G (x, y, 0), G (1, y, 1), 1, (c != 'D'));
if (y > 1) Add_Edge (G (x, y, 0), G (x, y - 1, 1), 1, (c != 'L'));
else Add_Edge (G (x, y, 0), G (x, M, 1), 1, (c != 'L'));
if (y < M) Add_Edge (G (x, y, 0), G (x, y + 1, 1), 1, (c != 'R'));
else Add_Edge (G (x, y, 0), G (x, 1, 1), 1, (c != 'R'));
}
inline void Solve () {
cin >> N >> M;
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
cin >> Opt, Add (i, j, Opt);
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
Add_Edge (S, G (i, j, 0), 1, 0), Add_Edge (G (i, j, 1), T, 1, 0);
cerr << Simplex () << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
Luogu P2469 [SDOI2010] 星际竞速
和上个题差不多,甚至 性质是直接给出的
每个点 只能经过一次
所以首先可以 拆点,然后同样的 源点连左部点,右部点连汇点,容量 \(1\) 免费
然后,星球之间有 双向边,但实际上由于 引力限制,就是一条 向引力大的星球的 单向边
连接之后直接跑费用流就可以得到 没有跳跃操作时的 答案
考虑有 跳跃 这个操作啊,相当于出发就直接到达某个点
故 源点 向 每个点右部点 连容量 \(1\) 费用等于跳跃费用 的边就行了
为什么要 源点向左部点 连 免费边 而非等于 跳跃费用 的边?
这里实际上 是对路径进行了 拆分,每次源点到的某个点 并不是一条路径的起点
实质是 路径中的某个点,不需要额外跳跃
比如对于原图上 \(A \to B \to C\) 这条路径,放到 建出的的图 上就成了
\(S \to A_1 \to B_2 \to T,S \to B_1 \to C_2 \to T\) 两条路径
显然会发现,由于没有入度,每条路径开头对应右部点 是不会被访问的
所以需要 一次跳跃 来补偿,正确性显然
#include <bits/stdc++.h>
const int MAXN = 100005;
const int INF = 1e9;
using namespace std;
namespace MCMF {
const long long INF = 1e9; // Don't Be Too Large
struct Edge {
int u, v, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
long long Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Now = 1, S = 114514, T = 191981;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
inline long long Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline long long Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
long long F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (F > E[fe[u]].f)
F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (F > E[fe[u] ^ 1].f)
F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
long long MinC = 0;
inline long long Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
namespace Value {
int N, M, U, V, C;
inline void Solve () {
using namespace MCMF;
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M;
for (int i = 1; i <= N; ++ i)
cin >> V, Add_Edge (S, i, 1, 0), Add_Edge (S, i + 1000, 1, V);
for (int i = 1; i <= M; ++ i) {
cin >> U >> V >> C;
if (U > V) swap (U, V);
Add_Edge (U, V + 1000, 1, C);
}
for (int i = 1; i <= N; ++ i)
Add_Edge (i + 1000, T, 1, 0);
cerr << Simplex () << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
// The Best !
return 0;
}
Luogu P4553 80人环游世界
上面那道题的 加强?版,无非就是一个点 可以经过多次
同时一共有 \(M\) 个人同时出发,而非 \(1\) 个人
但是少了 跳跃 这个神秘东西
也可以理解成因为人不止一个,可以分散,所以不需要这个东西
由于每个国家会被经过 \(V_i\) 次,显然 出入度 均为 \(V_i\)
直接 拆点,源点连左部点(先别急),右部点连汇点
左右部点 按航线连接,费用给出
同时因为出发时 \(M\) 个人 随意分散,所以可建立虚点 \(Z\)
源点 向 \(Z\) 连 容量 \(M\) 的 免费边,\(Z\) 向 左部点 连 容量 \(INF\) 的 免费边
直接跑板子就行
#include <bits/stdc++.h>
const int MAXN = 2005;
using namespace std;
namespace MCMF {
const long long INF = 1e9; // Don't Be Too Large
struct Edge {
int u, v, nxt;
long long f, w;
} E[MAXN << 4]; // 32000 > 100 * 100 * 2
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
long long Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Now = 1, S = 1145, T = 1919;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
inline long long Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline long long Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
long long F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (F > E[fe[u]].f)
F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (F > E[fe[u] ^ 1].f)
F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
long long MinC = 0;
inline long long Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
namespace Value {
int N, M, V, C;
const int Z = 1234;
using namespace MCMF;
inline int G (const int x) {
return x + 100;
}
inline void Solve () {
cin >> N >> M;
for (int i = 1; i <= N; ++ i) {
cin >> V;
Add_Edge (S, i, V, 0), Add_Edge (G (i), T, V, 0);
}
Add_Edge (S, Z, M, 0);
for (int i = 1; i <= N; ++ i) Add_Edge (Z, G (i), INF, 0);
for (int i = 1; i < N; ++ i) {
for (int j = 1; j <= N - i; ++ j) {
cin >> C;
if (C != -1) Add_Edge (i, G (i + j), INF, C);
}
}
cerr << Simplex () << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
// The Best !
return 0;
}
动态加边优化
直接加边 数量太多,但是实际上 有价值的边 有限
并会随着 增广结果 成 单调变化(一直 加边 / 删边 可以得到最优解)
这种时候就可以尝试(每次增广后)动态加边 来解决问题
Luogu P2050 [NOI2012] 美食节
这个题应该属于十分经典,做完就可以去秒切 Luogu P2053 [SCOI2007] 修车
形式化题意就是一共有 \(N\) 种共 \(\sum\limits_{1}^{N} {p_i} = P\) 个任务,又有 \(M\) 个工具人来完成
每个人对每个任务有不同的完成时间 \(C_{i,j}~~~(i \in [1, M], j \in [1, N])\)
求一种方式使 完成任务总时间最短(每个任务的完成间包含完成 前面任务的等待时间)
考虑把工具人切成 \(P\) 片,代表他的 可能任务队列
每种任务 \(i\) 向每个工具人 \(j\) 的第 \(k\) 片连
费用为 \(C_{j, i} * k\) 的边,表示这个任务 实际贡献 \(k\) 次时间(后面的任务等待用时)
同时容量为 \(1\),因为 一片工具人只能做一个任务
源点 \(S\) 向每种任务连容量为 \(p_i\)(任务个数)的免费边
最后 每片工具人 向 汇点 \(T\) 连容量为 \(1\) 的免费边
简单想想知道我选某一个工具人时 必然是从第一片连续向后面选
因为第一片在带来 同样效果 的同时 是最便宜的
不可能跳着选某一个工具人的切片,不然显然不优,所以 正确性在线
交上去 \(60~pts,TLE\)。
同样的套路放到 Luogu P2053 [SCOI2007] 修车 这个题已经可以过了
然后想想优化
根据 连续选择切片 的性质,一个人的第 \(K\) 个切片会被用到(增广到)
当且仅当他的前 \(K - 1\) 个切片都被增广了
人话就是你第 \(K\) 次给工具人的队列里加任务,那他的队列里一定已经有过 \(K - 1\) 个任务了
反过来说,当这个工具人只用到前 \(K\) 个切片时,他的第 \(K + 2\) 及往后的切片 都没有价值
也就是后面切片和任务,汇点之间的边都不用连
于是记录每个工具人 当前用到前几个切片 了,每次增广完遍历工具人 当前切片是否被使用
使用了就增加新切片,新增边,否则不更新
这样 最开始 就只需要连工具人的第一个切片
并且 由于 任务总数有限,最后利用的 工具人总切片数 也是有限的
具体来讲从 \(O (P * M)\) 降到了 \(O (P + M)\)
优化的很好,可以轻松通过
\(Dinic\)
#include <bits/stdc++.h>
const int MAXN = 2000005;
const long long INF = 1e16;
using namespace std;
inline int G (const int x, const int t) {
return t * 1000 + x;
}
namespace MCMF {
struct Node {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
bool Vis[MAXN];
int S = 1919810, T = 1919811;
int Dis[MAXN], Cur[MAXN], Flr[MAXN];
inline bool SPFA () {
memset (Vis, 0, sizeof Vis);
memset (Dis, 63, sizeof Dis);
queue <int> q;
q.push (S), Dis[S] = 0;
int u, v, w, f;
while (!q.empty ()) {
u = q.front (), q.pop ();
Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w;
if (!Vis[v]) q.push (v), Vis[v] = 1;
}
}
}
memset (Vis, 0, sizeof Vis);
if (Dis[T] < 1e9) return 1;
return 0;
}
long long MinC = 0;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
Vis[x] = 1;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
}
}
Vis[x] = 0;
return F;
}
int N, M;
int P[MAXN], C[45][105], Sum = 0;
inline long long Dinic () {
long long F = 0;
while (SPFA ()) {
F += DFS (S, INF);
for (int i = 1; i <= M; ++ i) {
if (E[H[G (i, Flr[i])]].f == 0) {
++ Flr[i];
for (int j = 1; j <= N; ++ j) Add_Edge (j, G (i, Flr[i]), 1, C[j][i] * Flr[i]);
Add_Edge (G (i, Flr[i]), T, 1, 0);
}
}
}
return F;
}
}
namespace Value {
using namespace MCMF;
inline void Solve () {
cin >> N >> M;
for (int i = 1; i <= N; ++ i) cin >> P[i], Sum += P[i];
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
cin >> C[i][j];
for (int i = 1; i <= N; ++ i) Add_Edge (S, i, P[i], 0);
for (int k = 1; k <= 1; ++ k)
for (int j = 1; j <= M; ++ j) {
for (int i = 1; i <= N; ++ i)
Add_Edge (i, G (j, k), 1, C[i][j] * k);
Add_Edge (G (j, k), T, 1, 0), Flr[j] = k;
}
cerr << Dinic () << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
\(Simplex\)
#include <bits/stdc++.h>
const int MAXN = 200005;
using namespace std;
inline int G (const int x, const int t) {
return t * 100 + x;
}
namespace MCMF {
const int INF = 1e9; // Don't Be Too Large
struct Edge {
int u, v, nxt;
int f, w;
} E[MAXN << 2];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const int f, const int w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
int Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN], Flr[MAXN];
int Now = 1, S = 114514, T = 191981;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
inline int Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline int Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
int F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (F > E[fe[u]].f)
F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (F > E[fe[u] ^ 1].f)
F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
int MinC = 0;
int N, M;
int P[MAXN], C[45][105];
inline void Clear (const int x) {
E[x] = E[x ^ 1] = {0, 0, 0, 0, 0};
}
inline int Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
int F = 0;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
F += E[tot].f, Clear (tot);
for (int i = 1; i <= M; ++ i) {
if (E[H[G (i, Flr[i])]].f == 0) {
++ Flr[i];
for (int j = 1; j <= N; ++ j) Add_Edge (j, G (i, Flr[i]), 1, C[j][i] * Flr[i]);
Add_Edge (G (i, Flr[i]), T, 1, 0);
}
}
Add_Edge (T, S, INF, - INF), Init_ZCT (T, 0, ++ Now), Tag[T] = ++ Now, fa[T] = 0;
}
MinC += F * INF;
return F;
}
}
namespace Value {
using namespace MCMF;
inline void Solve () {
cin >> N >> M;
for (int i = 1; i <= N; ++ i) cin >> P[i];
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
cin >> C[i][j];
for (int i = 1; i <= N; ++ i) Add_Edge (S, i, P[i], 0);
for (int k = 1; k <= 1; ++ k)
for (int j = 1; j <= M; ++ j) {
for (int i = 1; i <= N; ++ i)
Add_Edge (i, G (j, k), 1, C[i][j] * k);
Add_Edge (G (j, k), T, 1, 0), Flr[j] = k;
}
cerr << Simplex () << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
Luogu P3529 [POI2011] PRO-Programming Contest
非常好 \(POI\),使我的套路 \(TLE\),爱来自 美食节
一看和 Luogu P2050 [NOI2012] 美食节 套路很像蛤!
\(N\) 个 小师傅 做 \(M\) 个任务 的咩!有完成时间,求 最小时间点和 和 方案
直接给 小师傅 切片嘛,任务 是 不变的噻,源点 连起 容量 \(1\) 免费
每个任务 分给 每个小师傅 的 每个片片 噻,容量 \(1\),费用对应
每个片片 连上 汇点 嘛,容量 \(1\) 免费,完喽?
你就完喽!想想看 \(500 * 500 * 500 -> 1.25 * 10 ^ 8\) 条边,做你玛玛
然后和 美食节 一样咧优化噻,小师傅 先只给一个片片,用到再给
一看了 \(TLE ~ 90 ~ pts\)?啷个咧?
考虑如果只有 一个小师傅疯狂做,做了 \(500\) 个题,会咋样喃?
多了 \(500\) 个片片儿,每个 \(500\) 个边?瓜起
虽然说一次 \(500 ^ 2\) 的规模本来不会 \(T\),但是跑了 \(500\) 次这种瓜东西,还是过不到
咋办嘛?还得再转化一下
考虑到这个题咧性质蛤,做一道题时间是恒等的,和美食节那个 还不太一样
你可以把 小师傅 放到前头,源点 连起,容量 \(1\) 费用 \(R\)
因为不管后头做到哪道题 都用 \(R\) 咧时间
然后后头 任务 连 汇点,容量 \(1\) 免费,最后 小师傅 连 可以做咧题目,也是容量 \(1\) 免费
钱在前头给过了得嘛
这个时候喃,一个 小师傅 还 只能做一道题 蛤,跑一遍
如果发现 最大流变多了,就是做咧题变多喽
那就说明得行,阔以给 小师傅 们 加题
然后给 做了题 咧 小师傅们 再奖励一道
也就是从 源点 连一条 容量 \(1\),费用 \(2R\) 咧边就行喽
再跑一遍,以此类推蛤,直到跑到小师傅累罗,做不到多的题才停
方案很简单噻,显然我们 只关心每个小师傅到底做了些啥子
至于啥子时候开始做咧?随便安排一个就行
自己做的题换换顺序根本不影响噻
遍历一遍边,判断哪些满流了就好嘛
这里再讲一个优化,考虑如何 快速判断 这个 小师傅 这一轮做没做题喃?
把 源点 和 小师傅 的边 始终放到最后连
这个时候这条边一定是 \(H[小师傅]\) 对应的反边,判断满没满就好
#include <bits/stdc++.h>
const int MAXN = 300005;
const long long INF = 1e16;
using namespace std;
namespace MCMF {
struct Edge {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
int Cur[MAXN], Dis[MAXN];
bool Vis[MAXN];
int S = 114514, T = 191981;
inline bool SPFA () {
memset (Dis, 63, sizeof Dis);
deque <int> q;
q.push_front (S), Dis[S] = 0;
int u, v, w, f;
while (!q.empty ()) {
u = q.front (), q.pop_front (), Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w;
if (!Vis[v]) {
if (q.empty () || Dis[v] < Dis[q.front ()]) q.push_front (v), Vis[v] = 1;
else q.push_back (v), Vis[v] = 1;
}
}
}
}
if (Dis[T] < 1e9) return 1;
return 0;
}
long long MinC = 0;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
long long F = 0;
Vis[x] = 1;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (E[i].f, MAXF - F));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += E[i].w * TmpF;
}
}
Vis[x] = 0;
return F;
}
int Flr[MAXN];
int N, M, R, P, K, u, v;
inline long long Dinic () {
long long F = 1, MAXF = 0;
while (F) {
F = 0;
while (SPFA ()) F += DFS (S, INF);
MAXF += F;
if (F) {
for (int i = 1; i <= N; ++ i) {
if (E[H[i] ^ 1].f == 0 && Flr[i] + 1 <= P / R)
++ Flr[i], Add_Edge (S, i, 1, Flr[i] * R);
}
}
}
return MAXF;
}
}
namespace Value {
inline void Solve () {
using namespace MCMF;
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N >> M >> R >> P >> K;
for (int i = 1; i <= M; ++ i)
Add_Edge (i + 1000, T, 1, 0);
for (int i = 1; i <= K; ++ i)
cin >> u >> v, Add_Edge (u, v + 1000, 1, 0);
for (int i = 1; i <= N; ++ i)
if (P / R >= 1)
Add_Edge (S, i, 1, R), Flr[i] = 1;
long long Ans = 0;
cout << (Ans = Dinic ()) << ' ' << MinC << endl;
memset (Flr, 0, sizeof Flr);
for (int j = 2; j <= tot; j += 2) {
if (E[j ^ 1].to <= N && E[j].to <= M + 1000 && E[j].f == 0) {
cout << E[j ^ 1].to << ' ' << E[j].to - 1000 << ' ' << ((Flr[E[j ^ 1].to] ++) * R) << endl;
}
}
}
}
int main () {
Value::Solve ();
return 0;
}
区间选择
给定 \([1, M]\) 中的 \(N\) 个区间 \([L_i, R_i]\),每个区间 选择一次 的代价为 \(w_i\),最多选 \(p_i\) 次
要求使 任意点 \(j\) 被选择的次数在 \([a_j, b_j]\) 之间,求 代价最值
这个模型的建立比较巧妙,边数是 \(O(M + N)\) 级别的,十分好用
我们先建出 一条从 \(1\) 到 \(M + 1\) 的链,下面将用 \((i, i + 1)\) 这条边 描述 \(i\) 被覆盖的次数
对于每个区间 \([L_i, R_i]\),我们连边 \((L_i, R_i + 1)\),从这条边上 流一个流量 表示 选择一次这个区间
代价和选择次数 对应 这条边的费用和容量
注意到如果 \(1\) 的流量 流经 \((i, i + 1)\) 这条边,意味着有 \(1\) 的流量 没有经过跨过 \(i\) 的任意区间
也就是 \(i\) 没有被选择,故而设 \((i, i + 1)\) 流量为 \(f_i\),\(F\) 为 总流量
则其 被选择的次数为 \(F - f_i\),我们对此建 上下界限制,也就是 \(f_i \in [F - b_j, F - a_j]\) 即可
注意,访问上界 决定 流量下界,反之亦然
Luogu P3358 [网络流 24 题] 最长 \(K\) 可重区间集问题
题面太过形式化...
注意到 模型中的 \(M = \max (R_i)\),\(w_i = R_i - L_i\)(线段长),\(p_i = 1\),\(a_i = 0, b_i = k\) 即可
这道题访问上界是 固定的 \(k\),故可以把总流量设为 \(k\) 消去流量下界
注意到此题给出的是 开区间线段,线段中不包含端点,可以通过 闭区间右端点减一 实现
给出部分实现,主要是检查细节
namespace Value {
int N, K;
int L, R;
using namespace MCMF;
inline void Solve () {
cin >> N >> K;
Add_Edge (S, 1, N - K, 0);
Add_Edge ((MAXN >> 1) + 1, T, N - K, 0);
for (int i = 1; i <= (MAXN >> 1); ++ i) Add_Edge (i, i + 1, K, 0);
for (int i = 1; i <= N; ++ i) cin >> L >> R, -- R, Add_Edge (L, R + 1, 1, - R + L - 1);
cerr << Simplex () << endl;
cout << - MinC << endl;
}
}
Luogu P6967 [NEERC 2016] Delight for a Cat
注意到我们把 原题中的区间 看成 模型中的点,讨论 选择睡觉的次数
以下记 区间 \([l, l + k - 1]\) 为 模型中的点 \(l\)(区间左端点)
当我们在一个第 \(t\) 小时 选择睡觉 时,其将对 \([t - k + 1, t]\) 到 \([t, t + k - 1]\) 这 \(k\) 个区间贡献
也就相当于 模型中,选择一次 \([t - k + 1, t]\) 这个区间
显然一个小时中 只能有一种状态,于是这个区间的 最大选择次数 \(p_t = 1\)
同时注意到 当我们不选这个区间的时候(不在这个小时 睡觉 时)
相当于选择了 在这个小时进食,将得到 \(e_t\) 的快乐值
所以当选择 在这个小时睡觉 时,我们 额外得到 的快乐值是 \(s_t - e_t\),也就是这个 区间的代价
而原题中 每个长为 \(k\) 的区间 要求睡觉时间 \(\ge m_s\)
我们可以理解为 模型中每个点 至少被选 \(m_s\) 次
而 要求的进食时间,由于只有两种选择,可以将其转化成 要求不能睡觉的时间 \(m_e\)
于是 模型中每个点 至多被选 \(k - m_e\) 次,上下界都有了
总结一下,对于此题,我们有模型
给定 \([1, n - k + 1]\) 中的 \(n - k + 1\) 个区间 \([L_i, L_i + k - 1]\)
每个区间 选择一次 的代价为 \(s_i - e_i\),最多选 \(1\) 次
要求使 任意点 \(j\) 被选择的次数在 \([m_s, k - m_e]\) 之间,求 代价最大值
跑一遍模型就行
代码上有一些 细节 需要注意
- 由于区间长为 \(k\),故总流量为 \(k\) 而不是 \(n\)
- 注意到 容量上下界 是 \([m_e, k - m_s]\),并且 补完流之后 原边容量应设成 \(k - m_s - m_e\)
- 为了满足 两种活动必须选一种,我们应 跑满流量,也就是求 最大流 而非 可行流
- 这道题访问上界是 固定的 \(k - m_e\),故可以把总流量设为 \(k - m_e\) 消去流量下界
namespace Value {
using namespace MCMF;
int N, K, Ms, Me;
long long Sum;
long long Slp[MAXN], Eat[MAXN], A[MAXN];
inline void Solve () {
cin >> N >> K >> Ms >> Me;
for (int i = 1; i <= N; ++ i) cin >> Slp[i];
for (int i = 1; i <= N; ++ i) cin >> Eat[i], Sum += Eat[i];
for (int i = 1; i <= N - K + 1; ++ i) Add_Edge (i, i + 1, K - Ms - Me, 0);
int Pos = tot + 1;
for (int i = 1; i < K; ++ i) Add_Edge (1, min (1 + i, N - K + 2), 1, - Slp[i] + Eat[i]);
for (int i = 1; i <= N - K + 1; ++ i) Add_Edge (i, min (i + K, N - K + 2), 1, - Slp[i + K - 1] + Eat[i + K - 1]);
Add_Edge (N - K + 2, T, K - Me, 0);
Add_Edge (S, 1, K - Me, 0);
cerr << Simplex () << ' ' << Pos << endl;
cout << Sum - (MinC) << endl;
for (int i = Pos; i < Pos + (N << 1); i += 2) {
if (E[i].f == 1) cout << 'E';
else cout << 'S';
}
}
}
Luogu P3980 [NOI2008] 志愿者招募(这个可以看下面的,但是可能没有往这个模型上讲)
上下界费用流
对一条边的 流量限制 不再是 不超过 \(Flow\)
而是 在 \([L, R]\) 范围内 的 一类问题
一般情况下是这样做的
考虑 有源汇上下界 一般不方便做 流量平衡
所以会从 原始汇点 \(T\) 向 原始源点 \(S\) 连 容量 \(INF\) 的 免费边
这里和 \(Simplex\) 算法 的想法很像,只是由于 不用构造负环,所以费用不是 \(- INF\)
然后重点在 建立虚拟源汇 \(VS,VT\) 与 补流
直接提出 这个做法 显得比较怪,还是先来 感性理解 一下
我们简记 一条边 \((u, v)\) 容量为 \([L, R]\),而 实际流量 为 \(F\)
可以变形有 \(F = L + G\)(实际流量 = 容量下界 + 额外流量)
于是当所有边都满足 容量下界 时,我们得到一个所有边容量为 \(R - L\) 的 残量网络
并发现此时 \(G\) 应当为可行流 (不代表下文中 \(G\) 一定满足 平衡条件)
但是 还原到原始流 (加上 \(L\) 的流量) 时
这组流发现并 不满足流量平衡 \(F_{in} = F_{out}\)
考虑补流,转换式子 发现我们需要满足 \(\sum {L_{in}} + \sum {G_{in}} = \sum {L_{out}} + \sum {G_{out}}\)
于是 \(\sum L_{in} - \sum L_{out} = \sum G_{out} - \sum G_{in}\)
此时设 \(A_i = \sum L_{in} - \sum L_{out}\) 则 \(\sum G_{in} + A_i = \sum G_{out}\)
显然,我们需要 流入流量 等于 流出流量,如下
若 \(A_i > 0\) 则 \(\sum G_{in} + A_i = \sum G_{out}\),故 虚拟源点 向 \(i\) 连容量为 \(A_i\) 的边
若 \(A_i < 0\) 则 \(\sum G_{in} = |A_i| + \sum G_{out}\),故 \(i\) 向 虚拟汇点 连容量为 \(|A_i|\) 的边
$UPD ~ 04.03.13 $ 第二种情况 \(i\) 向 虚拟汇点 连边 而非 虚拟源点 向 \(i\) 连边
故正确性完备,最后在 虚拟源汇 跑 费用流 即可
参考资料 [1] https://blog.csdn.net/clove_unique/article/details/54884437
参考资料 [2] https://www.luogu.com.cn/blog/post/463966
Luogu P3980 [NOI2008] 志愿者招募
先来讲个 转化补流 的思路,也是 上下界费用流的核心思想
这道题可以 不直接用上下界 的套路,设每天需要志愿者 \(Need_i\)
考虑 最大流 取决于 路径上最小容量边,我们可以给一个 初始流量 \(INF\)
将 每个日子做一条边,容量为 \(INF - Need_i\)
也就是这一天缺了 \(Need_i\) 个志愿者
然后 源点连 \(Day_1\),\(Day_N\) 连汇点,容量 \(INF\),即 期望的最大流 为 \(INF\)
也就是期望 最后志愿者没有缺
但显然中间边流量不够 \(INF\),就需要其他边来补,补的边就是 花钱招募志愿者 的操作
若 \([L, R]\) 天有志愿者工作,招募一人花 \(C\) 元
则连边 \(Day_L \to Day_R\) 容量 \(INF\) ,费用 \(C\)
意义是招人来 补齐 \(L, R\) 天 流量的空缺,招 \(1\) 个人花 \(C\) 元,可以招 \(INF\) 个
连边完跑费用流板子即可
#include <bits/stdc++.h>
const int MAXN = 100005;
using namespace std;
namespace MCMF {
struct Node {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
bool Vis[MAXN];
int S, T;
int Dis[MAXN], Cur[MAXN];
inline bool SPFA () {
memset (Vis, 0, sizeof Vis);
memset (Dis, 63, sizeof Dis);
queue <int> q;
q.push (S), Dis[S] = 0;
int u, v;
long long w, f;
while (!q.empty ()) {
u = q.front (), q.pop ();
Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w;
if (!Vis[v]) q.push (v), Vis[v] = 1;
}
}
}
memset (Vis, 0, sizeof Vis);
if (Dis[T] < 1e9) return 1;
return 0;
}
long long MinC = 0;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
Vis[x] = 1;
long long F = 0;
for (int i = Cur[x]; i; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0;
while (SPFA ()) F += DFS (S, (1ll << 32));
return F;
}
}
namespace Work {
const long long inf = (1ll << 32);
int N, M, bg, ed, t;
inline void Solve () {
using namespace MCMF;
cin >> N >> M;
for (int i = 1; i <= N; ++ i)
cin >> t, Add_Edge (i, i + 1, inf - t, 0);
S = N + 1211, T = N + 1215;
Add_Edge (S, 1, inf, 0), Add_Edge (N + 1, T, inf, 0);
for (int i = 1; i <= M; ++ i)
cin >> bg >> ed >> t, Add_Edge (bg, ed + 1, inf, t);
Dinic ();
cout << MinC << endl;
}
}
int main () {
Work::Solve ();
return 0;
}
Luogu P4043 [AHOI2014/JSOI2014] 支线剧情
这个图本身建边方式很简单,直接 可达剧情相连,费用即花费时间
同时 原始源点 连点 \(1\) 容量 \(INF\) 免费,非 \(1\) 点 均连 原始汇点,容量 \(INF\) 免费
难点在于 剧情之间的边 容量 为 \([1, INF]\) 而非 \([0, Flow]\),需要补流
参照上面的板子即可,这里就 放一种实现
Luogu P5192 Zoj3229 Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流
这个所谓的 板子题 就是 最大流 的情形,外加 傻逼的多测
个人评价是不如直接做 支线剧情 这个题
#include <bits/stdc++.h>
const int MAXN = 100005;
const int INF = 1e9;
using namespace std;
namespace MCMF {
struct Edge {
int u, v, nxt;
int f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const int f, const int w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Pre[MAXN];
int S = 114514, T = 191981, Now = 1;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].f && Tag[E[i].v] != Nod) Init_ZCT (E[i].v, i, Nod);
}
inline int Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline int Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Del = 0, P = 2, Cnt = 0;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
int F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (E[fe[u]].f < F) F = E[fe[u]].f, P = 0, Del = u;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (E[fe[u] ^ 1].f < F) F = E[fe[u] ^ 1].f, P = 1, Del = u;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += E[Cir[i]].w * F, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v, Tmp;
if (P == 1) swap (u, v);
int Lst_u = v, Lst_e = x ^ P;
while (Lst_u != Del) {
Lst_e ^= 1, -- Tag[u];
swap (fe[u], Lst_e);
Tmp = fa[u], fa[u] = Lst_u, Lst_u = u, u = Tmp;
}
return Cost;
}
int MinC = 0;
inline int Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0);
fa[T] = 0, Tag[T] = ++ Now;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += INF * E[tot].f;
return E[tot].f;
}
}
namespace Value {
int N, K, V, C, VS = 11451, VT = 19198;
int A[MAXN];
int Sum = 0;
using namespace MCMF;
inline void Solve () {
cin >> N;
Add_Edge (S, 1, INF, 0);
for (int i = 1; i <= N; ++ i) {
cin >> K, A[i] -= K;
for (int j = 1; j <= K; ++ j)
cin >> V >> C, ++ A[V], Add_Edge (i, V, INF, C), Sum += C;
if (i != 1) Add_Edge (i, T, INF, 0);
}
for (int i = 1; i <= N; ++ i) {
if (A[i] > 0) Add_Edge (VS, i, A[i], 0);
else Add_Edge (i, VT, - A[i], 0);
}
Add_Edge (T, S, INF, 0);
S = VS, T = VT;
int Ans = Simplex ();
cerr << Ans << endl;
cout << MinC + Sum << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
最小积费用流
一种比较罕见的 简单套路
就是把 求解最短路 的部分换成 积的形式 即可
但要注意判断 初始值 选用 \(0\) 还是 \(1\),也需注意 精度问题
同时 加反边的时候 权值不再是 负的,而是 正边权值的倒数
统计费用时需要注意,不能像一般费用流 一样在 \(DFS\) 过程中统计
因为此处的费用实际表示一种从 源点 到 汇点 的概率,并不具有结合律
不能一堆中途的东西加起来得到
然后一般这种问题不建议使用 \(Simplex\) 算法
因为 单次增广的概率 通常是 单件任务完成概率 ^ 件数(\(Cost ^ {Flow}\))
涉及到 \(INF\) 流量就直接 爆精度 了,不是很方便
Luogu P4329 [COCI2006-2007#1] Bond
甚至是 绿题 ,好像说是因为 数据范围过小
状压 \(DP\) 可以直接 艹 过,但这里不讲
来考虑更为复杂的做法
显然可以考虑到 用流量来限制任务数,费用来表示成功率
那么可以从 源点 向 每个人 连边,容量 \(1\) 费用 \(1\),也就是 完全成功!(还没做任务)
同样 每个任务 向 汇点 连边,容量 \(1\) 费用 \(1\)
然后 每个人 向 对应任务 连容量 \(1\),费用等于 完成概率 的边
跑费用流即可,注意 更新最小费用 的时候 $MinCost = MinCost * NowCost ^ {NowFlow} $
因为 \(NowCost\) 表示的是 这次的单件完成率,要乘上 件数次方 才是 真实完成率
同时这道题要求的是所有任务 都完成的概率,所以 \(MinCost\) 是 累乘 而非 累加
#include <bits/stdc++.h>
const int MAXN = 100005;
const long double EPS = 1e-8;
const long long INF = 1e16;
using namespace std;
namespace MCMF {
struct Edge {
int to, nxt;
long long f;
long double w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long double w) {
E[++ tot] = {v, H[u], f, w / 1.0}, H[u] = tot;
E[++ tot] = {u, H[v], 0, 1.0 / w}, H[v] = tot;
}
int Cur[MAXN];
bool Vis[MAXN];
long double Dis[MAXN];
int S = 11451, T = 19198;
inline bool SPFA () {
fill (Dis, Dis + MAXN, 0);
queue <int> q;
q.push (S), Dis[S] = 1;
int u, v, f;
long double w;
while (!q.empty ()) {
u = q.front (), q.pop (), Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (w != 0 && Dis[v] + EPS < Dis[u] * w && f) {
Dis[v] = Dis[u] * w;
if (!Vis[v]) Vis[v] = 1, q.push (v);
}
}
}
if (Dis[T] != 0) return 1;
return 0;
}
long double MinC = 1;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
long long F = 0;
Vis[x] = 1;
for (int i = Cur[x]; i; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && E[i].w != 0 && Dis[E[i].to] - Dis[x] * E[i].w <= EPS && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = 0;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0, MAXF = 0;
while (SPFA ()) {
F = DFS (S, INF), MAXF += F, MinC *= pow (Dis[T], F);
cerr << F << ' ' << Dis[T] << ' ' << MinC << endl;
}
return MAXF;
}
}
namespace Value {
int N;
long double p;
inline void Solve () {
using namespace MCMF;
cin >> N;
for (int i = 1; i <= N; ++ i) Add_Edge (S, i, 1, 1);
for (int i = 1; i <= N; ++ i) Add_Edge (i + 5000, T, 1, 1);
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= N; ++ j)
cin >> p, p = p / 100.0, Add_Edge (i, j + 5000, 1, p);
long long Ans = 0;
cerr << (Ans = Dinic ()) << endl;
if (Ans == N) cout << fixed << setprecision (6) << MinC * 100.0 << endl;
else cout << "0.000000" << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
Luogu P5814 [CTSC2001] 终极情报网
上一道题的加强版,多了几个步骤,但本质是相同的
建边非常好想,源点 拆开控制流量,容量为 \(K\)
源点 向 每个点 连容量 \(AN_i\)(实现中用的 \(AM[i]\)), 费用 \(AS_i\) 的边
点之间 连容量 \(M_{i, j}\),费用 \(S_{i, j}\) 的边
最后 能向敌军传递情报的点 向 汇点 连容量 \(INF\),费用 \(1\) 的边
后面考虑 神秘输出问题
Luogu 吃枣药丸
洛谷没有 \(Special~Judge\),要求强制 保留 5 位 有 效 数 字
前面 0 不计入有效位,在第一个有效数字后的 0 计入有效位,末尾 0 要补全
如果直接用 \(cin << setprecision(5)\) 会省略 后导 \(0\)(不加 \(fixed\) 就是保留有效数字)
最后解决方法看实现,想了个还 比较简单的法子
#include <bits/stdc++.h>
const int MAXN = 500005;
const long long INF = 1e16;
const long double EPS = 1e-12;
using namespace std;
inline long double Qpow (long double x, int y) {
long double Ret = 1;
while (y) {
if (y & 1) Ret *= x;
x *= x, y >>= 1;
}
return Ret;
}
namespace MCMF {
struct Node {
int to, nxt;
long long f;
long double w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long double w) {
E[++ tot] = {v, H[u], f, (w) / 1.0}, H[u] = tot;
E[++ tot] = {u, H[v], 0, 1.0 / (w)}, H[v] = tot;
}
bool Vis[MAXN];
int S = 114514, T = 114515;
int Cur[MAXN];
long double Dis[MAXN];
inline bool SPFA () {
fill (Dis, Dis + MAXN, 0);
queue <int> q;
q.push (S), Dis[S] = 1;
int u, v, f;
long double w;
while (!q.empty ()) {
u = q.front (), q.pop (), Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (- Dis[v] + Dis[u] * w > EPS && f && w != 0) {
Dis[v] = Dis[u] * w;
if (!Vis[v]) q.push (v), Vis[v] = 1;
}
}
}
if (Dis[T] != 0) return 1;
return 0;
}
long double MinC = 1;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
Vis[x] = 1;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] - Dis[x] * E[i].w <= EPS /*EPS*/ && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0, MAXF = 0;
while (SPFA ()) {
F = DFS (S, INF);
// cerr << F << ' ' << MAXF << endl;
MinC *= Qpow (Dis[T], F);
MAXF += F;
}
return MAXF;
}
inline void Print () {
cout << "0.";
MinC *= 10;
while (MinC < 1) cout << 0, MinC *= 10;
MinC *= 10000;
cout << (int) (MinC + 0.5);
}
}
namespace Value {
int N, K, u, v, Sour = 191981;
bool Con = 0;
int AN[505], M;
long double AS[505], P;
inline void Solve () {
using namespace MCMF;
cin >> N >> K;
Add_Edge (S, Sour, K, 1);
for (int i = 1; i <= N; ++ i) cin >> AS[i];
for (int i = 1; i <= N; ++ i) cin >> AN[i];
for (int i = 1; i <= N; ++ i) Add_Edge (Sour, i, AN[i], AS[i]);
for (int i = 1; i <= N; ++ i) {
cin >> Con;
if (Con) Add_Edge (i, T, INF, 1);
}
while (cin >> u >> v) {
if (u == v && u == -1) break;
cin >> P >> M;
Add_Edge (u, v, M, P), Add_Edge (v, u, M, P);
}
long long Ans = 0;
cerr << (Ans = Dinic ()) << endl;
if (Ans < K) cout << 0 << endl;
else Print ();
}
}
int main () {
Value::Solve ();
return 0;
}
调配问题
这类都是很简单的东西
Luogu P4016 负载平衡问题
源点 向 每个仓库 连 容量 等于 当前量 的边,免费
每个仓库 向 汇点 连 容量 等于 平衡量(平均值)的边,免费
相邻仓库 连 容量 \(INF\),费用 \(1\) 的 双向边 即可
通过 最大流性质 来保证达到 负载平衡
#include <bits/stdc++.h>
const int MAXN = 100005;
const long long INF = 1e16;
using namespace std;
namespace MCMF {
struct Node {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
bool Vis[MAXN];
int S = 11451, T = 19198;
int Dis[MAXN], Cur[MAXN];
inline bool SPFA () {
memset (Dis, 63, sizeof Dis);
queue <int> q;
q.push (S), Dis[S] = 0;
int u, v, w, f;
while (!q.empty ()) {
u = q.front (), q.pop (), Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w;
if (!Vis[v]) q.push (v), Vis[v] = 1;
}
}
}
if (Dis[T] < 1e9) return 1;
return 0;
}
long long MinC = 0;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
Vis[x] = 1;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0;
while (SPFA ()) F += DFS (S, INF);
return F;
}
}
namespace Value {
int N;
int C[MAXN];
long long Sum = 0;
inline void Solve () {
using namespace MCMF;
cin >> N;
for (int i = 1; i <= N; ++ i) cin >> C[i], Sum += C[i];
for (int i = 1; i <= N; ++ i) Add_Edge (S, i, C[i], 0);
for (int i = 1; i <= N; ++ i) Add_Edge (i, T, Sum / N, 0);
for (int i = 1; i <= N; ++ i) Add_Edge (i, i % N + 1, INF, 1);
for (int i = 1; i <= N; ++ i) Add_Edge (i, (i + N - 2) % N + 1, INF, 1);
cerr << "Mea" << endl;
cerr << Dinic () << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
Luogu P4015 运输问题
源点 向 仓库 连 容量 \(A_i\),免费边
仓库 向 商店 连 容量 \(INF\),费用 \(C_{i, j}\) 的边
商店 向 汇点 连 容量 \(B_i\),免费边
跑费用流板子就行,最大最小需要两遍
#include <bits/stdc++.h>
const int MAXN = 100005;
const long long INF = 1e16;
using namespace std;
namespace MCMF {
struct Node {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
bool Vis[MAXN];
int S = 11451, T = 19198;
int Dis[MAXN], Cur[MAXN];
inline bool SPFA () {
memset (Dis, 63, sizeof Dis);
queue <int> q;
q.push (S), Dis[S] = 0;
int u, v, w, f;
while (!q.empty ()) {
u = q.front (), q.pop (), Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w;
if (!Vis[v]) q.push (v), Vis[v] = 1;
}
}
}
if (Dis[T] < 1e9) return 1;
return 0;
}
long long MinC = 0;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
Vis[x] = 1;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0;
while (SPFA ()) F += DFS (S, INF);
return F;
}
}
namespace Value {
int N, M;
int P[105], C[105][105], L[105];
inline void Init () {
using namespace MCMF;
memset (H, 0, sizeof H);
memset (E, 0, sizeof E);
tot = 1, MinC = 0;
}
inline void Solve () {
using namespace MCMF;
cin >> N >> M;
for (int i = 1; i <= N; ++ i) cin >> P[i];
for (int i = 1; i <= M; ++ i) cin >> L[i];
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
cin >> C[i][j];
for (int i = 1; i <= N; ++ i) Add_Edge (S, i, P[i], 0);
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
Add_Edge (i, j + 1000, INF, C[i][j]);
for (int i = 1; i <= M; ++ i) Add_Edge (i + 1000, T, L[i], 0);
cerr << Dinic () << endl;
cout << MinC << endl;
Init ();
for (int i = 1; i <= N; ++ i) Add_Edge (S, i, P[i], 0);
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
Add_Edge (i, j + 1000, INF, - C[i][j]);
for (int i = 1; i <= M; ++ i) Add_Edge (i + 1000, T, L[i], 0);
cerr << Dinic () << endl;
cout << - MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
匹配问题
这类问题十分的经典,主要还是看 因题而异的处理
剩下的一般就是一个 二分图(最小费用)最大匹配 的板子
Luogu P4134 [BJOI2012] 连连看
比较有趣
第一眼板题,思路确实好想,考虑数据范围很小
暴力 把符合的点连起来,费用 \(x + y\),容量 \(1\),时间上一看就很对
有老哥讨论 在原图直接连边 是否是二分图的问题,麻烦
直接拆点,符合条件的时候 左部点 连 右部点,解决问题
新的问题是怎么保证我选了 \((x, y)\) 这条边,右边的 \(x\) 不会再被 其他边选到
因为本质只有一个数,不能选两次
直接把 \((y, x)\) 也连上,费用容量 与 \((x, y)\) 保持一致,最后 答案除以二 即可
感性理解一下正确性,这样连边之后可以保证 整个图是对称的
同时 \((x, y), (y, x)\) 两条边的贡献显然等价,会一起被选到
也可以想如果有边 \((x, z)\) 比 \((x, y)\) 更优,显然存在 \((z, x)\) 优于 \((y, x)\)
故对称的两边 要么同时选到,要么都不选,可以保证答案正确
实现如下
#include <bits/stdc++.h>
const int MAXN = 1000005;
const long long INF = 1e16;
using namespace std;
inline int G (const int x) {
return 10000 + x;
}
namespace MCMF {
struct Edge {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
int Cur[MAXN], Dis[MAXN];
bool Vis[MAXN];
int S = 22222, T = 33333;
inline bool SPFA () {
memset (Dis, 63, sizeof Dis);
queue <int> q;
q.push (S), Dis[S] = 0;
int u, v, f, w;
while (!q.empty ()) {
u = q.front (), q.pop (), Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w;
if (!Vis[v]) Vis[v] = 1, q.push (v);
}
}
}
if (Dis[T] < 1e9) return 1;
return 0;
}
long long MinC = 0;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
long long F = 0;
Vis[x] = 1;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1e9;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += E[i].w * TmpF;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0;
while (SPFA ()) F += DFS (S, INF);
return F;
}
}
namespace Value {
int L, R;
bool IsN[1000005];
inline bool Check (const int x, const int y) {
if (x <= y) return 0;
int z = sqrt (x * x - y * y);
if (!IsN[x * x - y * y]) return 0;
if (__gcd (y, z) != 1) return 0;
return 1;
}
inline void Solve () {
using namespace MCMF;
cin >> L >> R;
for (int i = 1; i <= R; ++ i) IsN[i * i] = 1;
for (int i = L; i <= R; ++ i) {
Add_Edge (S, i, 1, 0);
for (int j = L; j <= R; ++ j)
if (Check (i, j)) Add_Edge (i, G(j), 1, - (i + j)), Add_Edge (j, G(i), 1, - (i + j));
Add_Edge (G(i), T, 1, 0);
}
cout << Dinic () / 2 << ' ' << - MinC / 2 << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
Luogu P3705 [SDOI2017] 新生舞会
费用并 不独立计算,显然 没法直接做
可以考虑 分数规划,转 极值 为 存在
显然 \(C\) 值越小,越可能存在方案,存在单调性,二分 \(C\) 的大小即可
简单推一下式子有 \(\sum A_i - C * \sum B_i = 0\)
若 \(C\) 为常数,显然 \(\sum A_i - C* \sum B_i\) 可以 独立计算 后 累加求得
于是二分 \(C\) 值后讲 每两个人之间
左部点 向 右部点 连容量 \(1\),费用 \(\sum A_i - C* \sum B_i\) 的边
源点 连 所有左部点 容量 \(1\) 免费边
所有右部点 连 汇点 容量 \(1\) 免费边
跑费用流,判断此时 \(MinCost\) 与 \(0\) 的关系,显然当 \(C = C_{max}\) 时,\(MinCost\) 恰为 \(0\)
若 \(MinCost < 0\) ,那么 \(C\) 大了(\(\dfrac {\sum A_i}{\sum B_i} < C\))
若 \(MinCost > 0\),那么 \(C\) 小了(\(\dfrac {\sum A_i}{\sum B_i} > C\))
调整即可,实现如下
#include <bits/stdc++.h>
const int MAXN = 100005;
const int INF = 1e9;
const long double EPS = 1e-7;
using namespace std;
namespace MCMF {
struct Node {
int to, nxt;
int f;
long double w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const int f, const long double w) {
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
bool Vis[MAXN];
int S = 11451, T = 19198;
long double Dis[MAXN];
int Cur[MAXN];
inline bool SPFA () {
fill (Dis, Dis + MAXN, -1e16);
queue <int> q;
q.push (S), Dis[S] = 0;
int u, v, f;
long double w;
while (!q.empty ()) {
u = q.front (), q.pop (), Vis[u] = 0, Cur[u] = H[u];
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] < Dis[u] + w && f) {
Dis[v] = Dis[u] + w;
if (!Vis[v]) q.push (v), Vis[v] = 1;
}
}
}
if (Dis[T] > -1e16) return 1;
return 0;
}
long double MinC = 0;
inline int DFS (const int x, const int MAXF) {
if (x == T || MAXF == 0) return MAXF;
Vis[x] = 1;
int F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
int TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
}
}
Vis[x] = 0;
return F;
}
inline int Dinic () {
int F = 0;
while (SPFA ()) F += DFS (S, INF);
return F;
}
}
namespace Value {
int N;
int A[105][105], B[105][105];
long double C, L = 0, R = 10000, Ans = 1e18;
inline bool Check () {
using namespace MCMF;
memset (H, 0, sizeof H);
memset (E, 0, sizeof E);
tot = 1, MinC = 0;
for (int i = 1; i <= N; ++ i) Add_Edge (S, i, 1, 0);
for (int i = 1; i <= N; ++ i) Add_Edge (i + 1000, T, 1, 0);
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= N; ++ j)
Add_Edge (i, j + 1000, 1, A[i][j] - C * B[i][j]);
Dinic ();
return MinC >= 0;
}
inline void Solve () {
cin >> N;
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= N; ++ j)
cin >> A[i][j];
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= N; ++ j)
cin >> B[i][j];
while (R - L > EPS) {
C = (R + L) / 2.0;
if (Check ()) L = C;
else R = C;
}
cout << fixed << setprecision (6) << C << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
分层图问题
也是一个非常常用的 建图手段 了
主要用于控制一些 匀速递减的变量,比如 汽车行驶时的油量 等
或者用于 控制递进的条件关系,比如 先到一个状态才能去下一个状态 等
Luogu P4009 汽车加油行驶问题
先分 \(K\) 层,代表你还剩的油
下文 \(VxCy \to F_n\) 指 向第 \(n\) 层某点连容量 \(x\),费用 \(y\) 的边)
除了 最后一层 每个普通点 向下一层 右下方格连 \(V1C0\)
向 左上方格 连 \(V1CB\)
加油站 强制缴费,于是 每层每个加油站
向 右下方格 连 \(V1CA \to F_2\),左上方格 连 \(V1C(A+B) \to F_2\)
像原地连 \(V1CA \to F_1\)
向相邻方格 第二层连边 就是强制 加了油之后 才能走一格
最后一层 普通点要建站,直接向原地 连 \(V1C(A+C) \to F_1\) 即可
#include <bits/stdc++.h>
const int MAXN = 2000005;
const long long INF = 1e16;
using namespace std;
inline int G (const int x, const int y, const int f) {
return f * 40000 + (x * 200 + y);
}
namespace MCMF {
struct Edge {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
int Cur[MAXN], Dis[MAXN];
bool Vis[MAXN];
int S = 1919810, T = 1919811;
inline bool SPFA () {
memset (Dis, 63, sizeof Dis);
queue <int> q;
q.push (S), Dis[S] = 0;
int u, v, w, f;
while (!q.empty ()) {
u = q.front (), q.pop (), Cur[u] = H[u], Vis[u] = 0;
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w;
if (!Vis[v]) Vis[v] = 1, q.push (v);
}
}
}
if (Dis[T] < 1e9) return 1;
return 0;
}
long long MinC = 0;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
long long F = 0;
Vis[x] = 1;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0;
while (SPFA ()) F += DFS (S, INF);
return F;
}
}
namespace Value {
int N, K, A, B, C;
char Mp[105][105];
inline void Solve () {
cin >> N >> K >> A >> B >> C; ++ K;
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= N; ++ j)
cin >> Mp[i][j];
using namespace MCMF;
for (int k = 1; k <= K; ++ k)
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= N; ++ j) {
if (Mp[i][j] == '0' && k != K) {
if (i != 1) Add_Edge (G (i, j, k), G (i - 1, j, k + 1), 1, B);
if (j != 1) Add_Edge (G (i, j, k), G (i, j - 1, k + 1), 1, B);
if (i != N) Add_Edge (G (i, j, k), G (i + 1, j, k + 1), 1, 0);
if (j != N) Add_Edge (G (i, j, k), G (i, j + 1, k + 1), 1, 0);
}
if (Mp[i][j] == '1' && k != 1) {
Add_Edge (G (i, j, k), G (i, j, 1), 1, A);
if (i != 1) Add_Edge (G (i, j, k), G (i - 1, j, 2), 1, B + A);
if (j != 1) Add_Edge (G (i, j, k), G (i, j - 1, 2), 1, B + A);
if (i != N) Add_Edge (G (i, j, k), G (i + 1, j, 2), 1, A);
if (j != N) Add_Edge (G (i, j, k), G (i, j + 1, 2), 1, A);
}
if (Mp[i][j] == '0' && k == K)
Add_Edge (G (i, j, k), G (i, j, 1), 1, C + A);
}
Add_Edge (S, G (1, 1, 1), 1, 0);
for (int i = 1; i <= K; ++ i) Add_Edge (G (N, N, i), T, 1, 0);
cerr << Dinic () << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
Luogu P4542 [ZJOI2011] 营救皮卡丘
注意到什么叫
两 面 包 夹 芝 士
这个是 最优解
这个是 最劣解
这究竟是怎么一回事呢?请看下文
挺有趣的这道题,我们先来
分析一下限制
最基础的就是 每个点都需要经过 这一点,并且要求 总路程最小
很容易想到的就是 路径覆盖问题,进而可以尝试 费用流 去求解
在有向图 \(G\) 中,设 \(P\) 是一个 简单路(顶点不相交)集合
如果 \(G\) 中的 每个顶点 都在 \(P\) 中的 一条路上,那么 \(P\) 就是 \(G\) 的 一个路径覆盖
而 路径覆盖问题 就是 在 \(G\) 中 求得特殊的的路径覆盖 \(P\) 的问题
例如 最小路径覆盖问题,注意到这里的最小不是指 最小权值,而是 路径条数最少
这类问题的基本思路就是 将每个点拆成两个点,建立 二分图
在原图上 有边的点对应的二分图,左右 / 右左 连边,求解(最小费用)最大匹配
难就难在这题有两个 特殊限制,第一个是 有 \(k\) 个人,也就是 最多同时走 \(k\) 路
但只是这个还是好解决的,我们可以限制 源点 到 起点(对应的左部点)的流量
但是注意到另一个条件,即 到达点 \(K\) 时,必须已经经过过点 \(1 \sim K - 1\)
当时我就感觉 有点难搞啊,想了一会儿,考虑到这种 条件递进 的关系
于是有了 分层图 的想法(埋下伏笔)
最劣解是怎么来的
这种想法 确实非常直观,只是...需要 大大大力卡常 + 代码复杂
有一种 \(ZJOI \to Ynoi\) 的美
我们可以直接 暴力把点分成 \(N\) 层,每层每个点向下一层对应点连单向边(保证不会走回来)
注意到 每个点必须经过一次,而且得 按顺序经过
于是我们可以对 第 \(i\) 个点从第 \(i\) 层连向 \(i + 1\) 的边 做一个 下界流量为 \(1\) 的限制
这里 可以直接用 上下界网络流 的套路
也可以 \(u \to v\) 连一条 容量 \(1\),费用 \(- \inf\) 的边,连一条 容量 \(\inf\),费用 \(0\) 的边
那么求解最小费用时,显然 费用 \(-\inf\) 的边会被走到,最后 总费用 加上 \(N\) 个 \(\inf\) 即可
而其他边 不做限制,此时我们就已经保证 到第 \(i\) 层时,必然经过 \(1 \sim i - 1\) 的点
之后再来考虑 原图上的边,我们钦定原图有边 \((u, v)\),\(u < v\)
于是我们显然可以在每一层 层内连接 \(u \to v\) 的边
而由于 \(v > u\),为了防止破坏条件,我们需要保证 \(v\) 被炸掉之后 才能 往 比 \(v\) 小的点走
也就是在第 \(v\) 层之后,我们才连接 \(v \to u\) 的边
否则可能先炸掉 \(v\) 再炸掉 \(u\) 费用更少,但显然不合题意
虽然说这里可以在 第 \(i\) 层 就直接跳到 后面的点 \(j ~ (j > i)\)
但由于我们保证在 第 \(j\) 层之前,\(j\) 不能 “往回跳” 到 比 \(j\) 小的点
故而这种情况实际上相当于 一个人在第 \(i\) 个点,决定了去炸 \(j\)(以及后面的点)
但是其 不会途径 \(j\) 去炸 \(1 \sim j - 1\) 中的点
所以只需要等待其他人把 \(1 \sim j - 1\) 炸完再动即可,没有破坏条件
这里附赠一个样例,手玩一下有助于理解上面这段抽象的东西
5 7 2 0 1 100 1 2 3 0 3 1 3 2 1 2 4 2 3 4 100 2 5 1 Ans = 108
最后把第 \(N\) 层的 每个点连向汇点 \(T\),大功告成
交上去,你不得不承认这玩意儿是对的,但是 \(TLE + MLE ~ 70 ~ pts\)
于是开始了 漫长的卡常
\(2024.03.07 ~~ 21:47 \to 70 ~ pts\)
发现 值域很小,于是把 long long
改成 int
,快了一些,不 \(MLE\) 了
\(2024.03.08 ~~ 11:12 \to 80 ~ pts\)
发现在前 \(i\) 层时,每层建一组 “向后走” 的边 十分浪费,实质上一共 只需要一组,省一半边
\(2024.03.08 ~~ 12:07 \to 90 ~ pts\)
注意到如果存在点 \(A, B, C\),满足 \(A < B < C\) 且 \(Dis (A, B) + Dis (B, C) < Dis (A, C)\)
那么边 \((A, C)\) 可以松弛,于是用 \(Bellman-Ford\) 遍历所有可以松弛的边
\(2024.03.08 ~~ 12:27 \to AC\)
发现上文中的边 \((A, C)\) 其实 完全没用,于是在建分层图的时候可以 忽略这些边
总算过了...
然后一看提交记录... 最劣解,主要是这样建图 边数的级别 是 \(O(N M)\),或说 \(O(N ^ 3)\)
即是砍掉了很多,在最终 \(AC\) 的版本上,最大的点还是会建出 \(742233\) 条边,十分喃伻
仔细想想,发现 点数 / 边数上可以少乘个 \(N\)... 这里先给个 劣质的代码
#include <bits/stdc++.h>
using namespace std;
namespace MCMF {
const int MAXN = 50005;
const int MAXM = 20000;
const int INF = 1e5; // Don't Be Too Large
struct Edge {
int u, v, nxt, f, w;
} E[MAXM * 38];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const int f, const int w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
int Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Now = 0, S = 31451, T = 49198;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
inline int Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline int Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
int F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (F > E[fe[u]].f)
F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (F > E[fe[u] ^ 1].f)
F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
int MinC = 0;
inline int Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0, ++ Now);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
namespace Value {
using namespace MCMF;
const int MAXP = 155;
int D[MAXP][MAXP];
int N, M, K, u, v, c;
int A[MAXN];
int VS = 31455, VT = 49199;
inline int G (const int x, const int f) {
return x + f * MAXP;
}
inline void Solve () {
cin >> N >> M >> K, ++ N;
for (int i = 1; i <= N; ++ i) {
for (int j = 1; j <= i; ++ j)
Add_Edge (G (j, i - 1), G (j, i), K, 0);
Add_Edge (S, G (i, i), 1, 0), Add_Edge (G (i, i - 1), T, 1, 0);
}
memset (D, 63, sizeof D);
for (int i = 1; i <= M; ++ i) {
cin >> u >> v >> c, ++ u, ++ v;
if (u > v) swap (u, v);
D[u][v] = min (D[u][v], c);
}
for (int i = 1; i < N; ++ i)
for (int j = i + 2; j <= N; ++ j)
for (int k = i + 1; k < j; ++ k)
if (D[i][j] < 10000 && D[i][k] + D[k][j] < D[i][j])
D[i][j] = D[i][k] + D[k][j];
for (int i = 1; i < N; ++ i)
for (int j = i + 2; j <= N; ++ j)
for (int k = i + 1; k < j; ++ k)
if (D[i][j] < 10000 && D[i][k] + D[k][j] == D[i][j])
D[i][j] = 10001;
for (u = 1; u <= N; ++ u)
for (v = u; v <= N; ++ v)
if (D[u][v] < 10000) {
c = D[u][v];
Add_Edge (G (u, u - 1), G (v, v - 1), K, c);
Add_Edge (G (u, v - 1), G (v, v - 1), K, c);
for (int k = v; k <= N; ++ k)
Add_Edge (G (v, k), G (u, k), K, c), Add_Edge (G (u, k), G (v, k), K, c);
}
for (int i = 1; i <= N; ++ i) Add_Edge (G (i, N), VT, K, 0);
Add_Edge (VS, G (1, 0), K, 0), Add_Edge (VT, VS, K, 0);
int Ans = Simplex ();
cerr << tot << endl;
cerr << Ans << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
最优解是怎么来的
我们注意到 第三个条件 本质上就是 不要 先炸了后面的再回头炸前面的
而我们刚刚 卡常的倒数第二步 抓住了一个关键,也就是 最短路
事实上,我们可以用 \(Bellman-Ford\) 预处理出 任意两点间 不经过后面点 的 最短路径
于是我们可以 回到二分图,同一个点 拆成左右两点,连边,保证下界为 \(1\)
按照路径覆盖的套路,设 \(Dis (u, v) = c\),我们将 \(u\) 的 右部点 与 \(v\) 的 左部点 连接,费用为 \(c\)
注意到为防止 ”回头炸前面的“,我们需要保证此处 \(u < v\)
由于前面我们已经处理出 任意两点距离,故这里实际上任意 \(u, v\) 之间均有边,与原图无关
也就是 点号较小的点对应的右部点 与 点号较大的点对应的左部点 连 单向边
这样也就不可能出现 回头 的情况
但是可能有人会有疑问,如果 需要借道前面走过的点 时,正确性会不会有问题
我们注意到此时的 一条边 实际上代表的是 \(Bellman-Ford\) 处理出来的 一条路径
这里路径是 包括了 借道的情况,比如前面给的小样例中的 最优情况
就会存在一条 \(3 \to 2 \to 4\) 的 需要借道 的路径
而反映到 二分图 中,我们预处理出的 \(3, 4\) 的 最短距离 就是 \(3\)(\(3 \to 2 \to 4\) 的长度)
于是会有 \(3\) 的右部点 连向 \(4\) 的左部点 的 一条费用为 \(3\) 的边,也就代表了这种情况
故而容易知道,正确性保证
于是直接连边做就行了,这样边数显然只有 \(O(N ^ 2)\) 的级别
在最大的点上实际建出了 \(23561\) 条边,是上一种方法的 \(\dfrac {1} {32}\) 左右,十分的快
#include <bits/stdc++.h>
using namespace std;
namespace MCMF {
const int MAXN = 50005;
const int MAXM = 20000;
const int INF = 1e5; // Don't Be Too Large
struct Edge {
int u, v, nxt, f, w;
} E[MAXM * 2];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const int f, const int w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
int Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Now = 0, S = 31451, T = 49198;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
inline int Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline int Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
int F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (F > E[fe[u]].f)
F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (F > E[fe[u] ^ 1].f)
F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
int MinC = 0;
inline int Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0, ++ Now);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
namespace Value {
using namespace MCMF;
const int MAXP = 155;
int D[MAXP][MAXP];
int N, M, K, u, v, c;
int A[MAXN];
int VS = 31455, VT = 49199;
inline int G (const int x, const int f) {
return x + f * MAXP;
}
inline void Solve () {
cin >> N >> M >> K, ++ N;
for (int i = 1; i <= N; ++ i) {
Add_Edge (G (i, 0), G(i, 1), 1, - 1e6);
Add_Edge (G (i, 0), G (i, 1), K, 0);
Add_Edge (G (i, 1), T, K, 0);
}
memset (D, 63, sizeof D);
for (int i = 1; i <= M; ++ i) {
cin >> u >> v >> c, ++ u, ++ v;
if (u > v) swap (u, v);
D[v][u] = D[u][v] = min (D[u][v], c);
}
for (int i = 1; i < N; ++ i)
for (int j = i; j <= N; ++ j)
for (int k = 1; k < j; ++ k)
if (D[i][k] + D[k][j] < D[i][j])
D[j][i] = D[i][j] = D[i][k] + D[k][j];
for (int i = 1; i < N; ++ i)
for (int j = i + 1; j <= N; ++ j)
Add_Edge (G (i, 1), G (j, 0), K, D[i][j]);
Add_Edge (S, G (1, 0), K, 0);
int Ans = Simplex ();
cerr << Ans << endl;
cout << (MinC + N * 1000000) << endl;
}
}
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
Value::Solve ();
return 0;
}
构造调优
这是一个新的 \(Part\),也不知道应当取什么名字
这个思想大概 不止 在 网络流 中 会有应用
其核心是 构造一种可行解,在规则限制内 不断调整 直到最优
可以发现 \(Simplex\) 本质上 就是这种思想的体现
有些时候也可能表现为 有两种操作,我们把其中一种 暴力做完
然后 只去考虑另一种,同时 反悔第一种操作,这种思想也对某些题有帮助
Luogu P4486 [BJWC2018] Kakuro
好题,牛牛的一个套路 —— \(\color {black} \textsf {H}\)\(\color {red} \textsf {anghang}\)
请注意 输入格式 小心被搞
什么叫 构造调优 呢,看这个题,注意到如果我们可以 任意改动给定数字(代价均非 \(-1\))
线索(也就是连通块指定的和)与 空格内填的数字 都是可以变的
容易想到 我们一定能构造出一种解,即 空格全部填成 \(1\),线索填成对应方向连通块长度
统计出从 初始局面 修改到 此时局面 需要的代价,即为 \(Pre\)
于是可以 在此基础上进行优化,由于 空格中的数 要求为 正整数
所以只需要考虑 从当前情况下增大,而不用考虑再减小的操作
也就是 把减小的操作 暴力做完,然后只去考虑 增大,同时 反悔部分减小操作
注意到 一个空格的数 会给其 左边第一个线索 和 上面第一个线索,一共 两个线索 做贡献
可以想到 构建二分图,源点 \(S\) 连接 左部点,右部点 连接 汇点 \(T\)
在 左部点 放上 所有 在左下角的线索(对于空格来说,往上面贡献 到的线索)
在 右部点 放上 所有 在右上角的线索(对于空格来说,往左边贡献 到的线索)
每个空格 就充当 其贡献的两个方向线索 之间的边,于是边就建完了,一共 三类边
考虑其 容量 以及 费用,注意到 每个格子在初始局面被给定了数字 \(S_{i, j}\)
而现在的假定是 所有空格填 \(1\) 对应的 合法解,设当前每个格子对应数字为 \(T_{i, j}\)
假设我们是 从初始局面减小得到的(这显然是 很可能的情况)
则 在这个格子被增加到初始局面之前,我们都相当于在 ”反悔“ 之前 减小 的操作,代价在变小
于是 三类边 都建出一条 容量为 \(S_{i, j} - T_{i, j}\) ,费用为 \(- Cost_{i, j}\) 的 边
注意到 当 \(T'_{i, j} > S_{i, j}\) 时,再增加(再流流量)就是 花费代价 的行为了,此时增加的值 没有上限
于是 三类边 再建出一条 容量为 \(INF\) ,费用为 \(Cost_{i, j}\) 的 边
最后跑一遍 最小费用可行流,答案就是 \(Pre + MinCost\)
注意在这道题中由于 \(Pre\) 已经是 一种可行解构造 的代价了,也就是花费 \(Pre\) 一定符合题意
故当某次增广的 \(Cost > 0\) 时,可以 停止增广(否则 一定不优)
反映到代码上就是 \(SPFA\) 求得的 \(Dis_T \ge 0\) 时,停止增广,返回答案
最后注意一下 不可修改的情况,也就是 给定代价为 \(-1\) 的情况,这会 带来一些无解
我们把这种东西的 对应边 设定代价成 \(\pm ~ INF\)(对应 \(\pm ~ Cost_{i, j}\) 的情况)
保证其 不会在 有其它”正常的边“ 可以增广时 被先增广到
最后跑完的时候判断所有 代价为 \(\pm ~ INF\) 的边 是否有流量,有的话 声明无解
反之输出答案即可
#include <bits/stdc++.h>
const int MAXN = 2000;
const long long INF = 1e9;
using namespace std;
namespace MCMF {
struct Node {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
if (f < 0) return ;
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
bool Vis[MAXN];
int S = 1990, T = 1992;
long long Dis[MAXN];
int Cur[MAXN];
inline bool SPFA () {
memset (Dis, 63, sizeof Dis);
deque <long long> q;
q.push_front (S), Dis[S] = 0, Cur[S] = H[S];
int u, v; long long w, f;
while (!q.empty ()) {
u = q.front (), q.pop_front (), Vis[u] = 0;
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w, Cur[v] = H[v];
if (!Vis[v]) {
if (q.empty () || Dis[v] > Dis[q.front ()]) q.push_back (v), Vis[v] = 1;
else q.push_front (v), Vis[v] = 1;
}
}
}
}
return Dis[T] < 0;
}
long long MinC = 0;
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
Vis[x] = 1;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0;
while (SPFA ()) F += DFS (S, INF);
return F;
}
}
namespace Value {
const int MAXP = 35;
int Typ[MAXP][MAXP];
long long Cost[MAXP][MAXP][2];
long long Sum[MAXP][MAXP][2];
int N, M, P, R;
using namespace MCMF;
inline int G (const int x, const int y, const int f) {
return x * 30 + y + f * 1000;
}
inline bool Check () {
for (int i = 2; i <= tot; i += 2)
if ((E[i].w == INF && E[i ^ 1].f > 0) || (E[i].w == -INF && E[i].f > 0)) return 0;
return 1;
}
inline int GetRight (const int x, const int y) {
int Ret = 1;
while (Typ[x][y + Ret] == 4) ++ Ret;
return Ret - 1;
}
inline int GetDown (const int x, const int y) {
int Ret = 1;
while (Typ[x + Ret][y] == 4) ++ Ret;
return Ret - 1;
}
inline int GetUp (const int x, const int y) {
int Ret = 1;
while (Typ[x - Ret][y] != 1 && Typ[x - Ret][y] != 3) ++ Ret;
return x - Ret;
}
inline int GetLeft (const int x, const int y) {
int Ret = 1;
while (Typ[x][y - Ret] != 2 && Typ[x][y - Ret] != 3) ++ Ret;
return y - Ret;
}
long long Ans = 0;
inline void Solve () {
cin >> N >> M;
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
cin >> Typ[i][j];
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
for (int k = 0; k <= (Typ[i][j] == 3) - (Typ[i][j] == 0); ++ k)
cin >> Sum[i][j][k];
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j)
for (int k = 0; k <= (Typ[i][j] == 3) - (Typ[i][j] == 0); ++ k)
cin >> Cost[i][j][k], Cost[i][j][k] == -1 ? Cost[i][j][k] = INF : 0;
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= M; ++ j) {
if (Typ[i][j] == 1) P = GetDown (i, j), Add_Edge (S, G (i, j, 0), Sum[i][j][0] - P, - Cost[i][j][0]), Add_Edge (S, G (i, j, 0), INF, Cost[i][j][0]), Ans += abs (Sum[i][j][0] - P) * Cost[i][j][0];
if (Typ[i][j] == 3) P = GetDown (i, j), Add_Edge (S, G (i, j, 0), Sum[i][j][0] - P, - Cost[i][j][0]), Add_Edge (S, G (i, j, 0), INF, Cost[i][j][0]), Ans += abs (Sum[i][j][0] - P) * Cost[i][j][0];
if (Typ[i][j] == 2) P = GetRight (i, j), Add_Edge (G (i, j, 1), T, Sum[i][j][0] - P, - Cost[i][j][0]), Add_Edge (G (i, j, 1), T, INF, Cost[i][j][0]), Ans += abs (Sum[i][j][0] - P) * Cost[i][j][0];
if (Typ[i][j] == 3) P = GetRight (i, j), Add_Edge (G (i, j, 1), T, Sum[i][j][1] - P, - Cost[i][j][1]), Add_Edge (G (i, j, 1), T, INF, Cost[i][j][1]), Ans += abs (Sum[i][j][1] - P) * Cost[i][j][1];
if (Typ[i][j] == 4) P = GetUp (i, j), R = GetLeft (i, j), Add_Edge (G (P, j, 0), G (i, R, 1), Sum[i][j][0] - 1, - Cost[i][j][0]), Add_Edge (G (P, j, 0), G (i, R, 1), INF, Cost[i][j][0]), Ans += abs (Sum[i][j][0] - 1) * Cost[i][j][0];
}
cerr << Ans << endl;
cerr << Dinic () << ' ' << MinC << endl;
if (!Check ()) cout << -1 << endl;
else cout << MinC + Ans << endl;
}
}
int main () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
Value::Solve ();
return 0;
}
注意到 可行流 的话 \(Simplex\) 也是可以做的,由于我们只需要找 原图负权路径 的部分
所以把 \(Simplex\) 函数中 额外加的 汇点到源点 的 \(INF\) 边 费用改成 \(0\)
本来这条边就是用来 强制在有流量的地方构建负环 用的
但现在我们 并不需要最大流,仅需要 增广权值为负
于是我们只需加边 构建环即可,当原图有 负权路径 时这个环 自然为负环
参考代码
#include <bits/stdc++.h>
const int MAXN = 2000;
const long long INF = 1e9;
using namespace std;
namespace MCMF2 {
struct Node {
int to, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
if (f < 0) return ;
E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
}
bool Vis[MAXN];
int S = 1990, T = 1992;
long long Dis[MAXN];
int Cur[MAXN];
inline bool SPFA () {
memset (Dis, 63, sizeof Dis);
deque <long long> q;
q.push_front (S), Dis[S] = 0, Cur[S] = H[S];
int u, v; long long w, f;
while (!q.empty ()) {
u = q.front (), q.pop_front (), Vis[u] = 0;
for (int i = H[u]; i; i = E[i].nxt) {
v = E[i].to, w = E[i].w, f = E[i].f;
if (Dis[v] > Dis[u] + w && f) {
Dis[v] = Dis[u] + w, Cur[v] = H[v];
if (!Vis[v]) {
if (q.empty () || Dis[v] > Dis[q.front ()]) q.push_back (v), Vis[v] = 1;
else q.push_front (v), Vis[v] = 1;
}
}
}
}
return Dis[T] < 0;
}
long long MinC = 0;
inline void Print () {
cerr << "Total : " << tot << endl;
for (int i = 2; i <= tot; i += 2)
cerr << "From : " << E[i ^ 1].to << " To : " << E[i].to << " Flow : " << E[i].f << " Cost : " << E[i].w << endl;
}
inline long long DFS (const int x, const long long MAXF) {
if (x == T || MAXF == 0) return MAXF;
Vis[x] = 1;
long long F = 0;
for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
Cur[x] = i;
if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
if (!TmpF) Dis[E[i].to] = -1;
F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
}
}
Vis[x] = 0;
return F;
}
inline long long Dinic () {
long long F = 0;
while (SPFA ()) F += DFS (S, INF);
return F;
}
}
namespace MCMF {
struct Edge {
int u, v, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
if (f < 0) return ;
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
long long Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Now = 0, S = 1990, T = 1992;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
inline long long Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline long long Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
long long F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (F > E[fe[u]].f)
F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (F > E[fe[u] ^ 1].f)
F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
inline void Print () {
cerr << "Total : " << tot << endl;
for (int i = 2; i <= tot; i += 2)
cerr << "From : " << E[i].u << " To : " << E[i].v << " Flow : " << E[i].f << " Cost : " << E[i].w << endl;
}
long long MinC = 0;
inline long long Simplex () {
Add_Edge (T, S, INF, 0); // Diff
Init_ZCT (T, 0, ++ Now);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
// MinC += E[tot].f * INF;
return E[tot].f;
}
}
批题乱讲
各种优化 以及 神秘东西
Luogu P2488 [SDOI2011] 工作安排
其实和 动态加边 那一部分的题很像的,但是不需要
考虑到这个题中 愤怒值 是 \(S_i + 1 \le 6\) 段的一个 函数
并且 无需累加计算
也就是一件产品 \(w\),两件是 \(2*w\) 而非 \((1 + 2) * w\)
所以直接向 工具人 \(i\) 连 代表一段的边
容量 \(T_{i, j} - T_{i, j - 1}\),费用 \(w_{i, j}\),不要容量 \(1\) 的边建一堆
暴力连完就行,不用动态加边(因为段数很少)
然后直接跑板子就完了
#include <bits/stdc++.h>
const int MAXN = 300005;
using namespace std;
namespace MCMF {
const long long INF = 1e12;
struct Edge {
int u, v, nxt;
long long f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
long long Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN];
int Tag[MAXN], Now = 1;
int S = 11451, T = 19198;
inline void Init_ZCT (const int x, const int e) { // Make a Random Zhicheng Tree
fe[x] = e, fa[x] = E[fe[x]].u, Tag[x] = 1;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].f && !Tag[E[i].v]) Init_ZCT (E[i].v, i);
}
inline long long Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline long long Push_Flow (const int x) {
// Find LCA (Top of Circle)
int rt = E[x].u, lca = E[x].v, Cnt = 0;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
// Find Circle
long long F = E[x].f; int Del = 0, P = 2;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (E[fe[u]].f < F)
F = E[fe[u]].f, P = 0, Del = u;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (E[fe[u] ^ 1].f < F)
F = E[fe[u] ^ 1].f, P = 1, Del = u;
}
Cir[++ Cnt] = x;
// Push Flow
long long Cost = 0;
for (int i = 1; i <= Cnt; ++ i)
Cost += E[Cir[i]].w * F, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost; // MinFlow on Edge You Add
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
long long MinC = 0;
inline long long Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
namespace Value {
using namespace MCMF;
int N, M, C;
int CS[255];
long long CT[255][255], CW[255][255];
inline void Solve () {
cin >> M >> N;
for (int i = 1; i <= N; ++ i)
cin >> C, Add_Edge (i, T, C, 0);
for (int i = 1; i <= M; ++ i)
for (int j = 1; j <= N; ++ j)
cin >> C, (C == 1) ? Add_Edge (i + 1000, j, INF, 0) : void ();
for (int i = 1; i <= M; ++ i) {
cin >> CS[i];
for (int j = 1; j <= CS[i]; ++ j) cin >> CT[i][j];
for (int j = 1; j <= CS[i] + 1; ++ j) cin >> CW[i][j];
CT[i][CS[i] + 1] = (1 << 30);
}
for (int i = 1; i <= M; ++ i)
for (int j = 1; j <= CS[i] + 1; ++ j)
Add_Edge (S, i + 1000, CT[i][j] - CT[i][j - 1], CW[i][j]);
cerr << Simplex () << endl;
cout << MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
Luogu P8021 [ONTAK2015] Bajtman i Okrągły Robin
线段树优化建图,好像还挺重要的(但是费用流里面似乎不是很多?)
不排除这题直接暴力是能过的
显然有 源点 向 每个时间刻 连边,然后 对应时间段 向小偷连边
但是 这样的话 理论的边数是 \(O(N^2) = 2.5 \times 10^7\) 的,会寄
理论是理论,实际是实际,不要把理论当成实际
所以 在时刻的点上 套线段树,这样每个小偷理论上就只会连接 \(O(\log)\) 个区间
总共 \(O(N \log N)\) 条边,非常合理
#include <bits/stdc++.h>
const int MAXN = 300005;
using namespace std;
namespace MCMF {
const long long INF = 1e9;
struct Edge {
int u, v, nxt;
long long f, w;
} E[2000000];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
int fe[MAXN], fa[MAXN], Tag[MAXN], Cir[MAXN];
long long Pre[MAXN];
int S = 114514, T = 191981, Now = 1;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].f && Tag[E[i].v] != Nod) Init_ZCT (E[i].v, i, Nod);
}
inline long long Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline long long Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Del = 0, P = 2, Cnt = 0;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
long long F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (E[fe[u]].f < F) F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (E[fe[u] ^ 1].f < F) F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v, Tmp;
if (P == 1) swap (u, v);
int Lst_u = v, Lst_e = x ^ P;
while (Lst_u != Del) {
Lst_e ^= 1, Tag[u] --;
swap (fe[u], Lst_e);
Tmp = fa[u], fa[u] = Lst_u, Lst_u = u, u = Tmp;
}
return Cost;
}
long long MinC = 0;
inline long long Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
namespace SegTree {
struct Node {
int L, R, C;
} T[MAXN << 1];
#define LC (x << 1)
#define RC (x << 1 | 1)
#define M ((T[x].L + T[x].R) >> 1)
inline int G (const int x) {
return x + 10000;
}
inline void Build (const int L, const int R, int x = 1) {
T[x].L = L, T[x].R = R;
if (L == R) return MCMF::Add_Edge (L, G (x), R - L + 1, 0), T[x].C = 1, void ();
Build (L, M, LC), Build (M + 1, R, RC), T[x].C = T[LC].C + T[RC].C;
MCMF::Add_Edge (G (LC), G (x), T[LC].C, 0), MCMF::Add_Edge (G (RC), G (x), T[RC].C, 0);
}
inline void Add (const int L, const int R, const int Con, int x = 1) {
if (L <= T[x].L && T[x].R <= R) return MCMF::Add_Edge (G (x), Con, T[x].C, 0);
if (L <= M) Add (L, R, Con, LC);
if (R > M) Add (L, R, Con, RC);
}
}
namespace Value {
int N, L, R, C, Cnt = 0;
int Min = 1e5, Max = 0;
struct Node {
int l, r;
} P[MAXN];
using namespace MCMF;
inline void Solve () {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> N;
for (int i = 1; i <= N; ++ i) {
cin >> L >> R >> C;
Min = min (Min, L), Max = max (Max, R - 1);
P[++ Cnt] = {L, R - 1};
Add_Edge (i + 100000, T, 1, - C);
}
SegTree::Build (Min, Max);
for (int i = 1; i <= N; ++ i) SegTree::Add (P[i].l, P[i].r, i + 100000, 0);
for (int i = Min; i <= Max; ++ i) Add_Edge (S, i, 1, 0);
cerr << Simplex () << endl;
cout << - MinC << endl;
}
}
int main () {
Value::Solve ();
return 0;
}
CF1383F Special Edges
边数 \(10 ^ 4\),带修改边权,询问次数 \(2 \times 10 ^ 5\),一看这题就是在 发披风
直接做肯定会 寄,但是我们发现 特殊边 这个东西只有 \(k \le 10\) 个
容易想到 预处理答案 之后 快速回答询问
考虑 最大流最小割 定理,我们可以 从最小割 的方向考虑
注意到 一条边 只存在 是否被割(是否满流) 两种状态
可以想到去 枚举每条特殊边是否被割,可以用 \(01\) 串表示,显然一共 \(2 ^ k\) 种情况
最终的答案应当是 割集的边权和 再加上 剩余网络的最大流
而 割集的边权 我们并不知道(询问时给出)
于是我们得想办法在此情况下 求出剩余网络的最大流
我们将 被割的 特殊边 容量设为 \(0\),其余 特殊边 设为 \(INF\),普通边即为 本身容量
容易发现,此时求出的就是 剩余网络的最大流,跑 \(2 ^ k\) 次 最大流 即可求解 所有情况
如果每次都重新建图,跑最大流,显然复杂度 不可承受(\(2 ^ k\) 次 \(M = 10 ^ 4\))
可以发现每种情况实质上都是将 某种情况 一条特殊边 强制踢出割集 得到的
\(eg. 10100 \to 10101\) 实质上就是把 第五条特殊边 踢出割集
我们只需要将 第五条特殊边 的容量从 \(0\) 改为 \(INF\),然后在 \(10100\) 的 残余网络 上跑就行
于是 遍历割集情况时,记录下前面情况的 残余网络,每次在某个基础上改一条边权再跑
具体来讲,我们基于 i - lowbit(i)
的 残余网络,将 lowbit(i)
对应边 容量改为 \(INF\)
只需要 增广一次 即可,这里显然使用 \(Dinic\) 就没有一点优
而 \(Ford-Fulkerson\) 与 \(Simplex\) 本质都是 找到一条增广路径就增广,符合这题情况
(我的芙芙被卡常了!!!
最终我们根据询问 求得每种情况下的割集边权和,加上对应的 剩余流量 取 \(\min\) 即可
我们明明求的是 最大流,为什么这里要取 \(\min\)?
注意从 最小割 角度 去考虑,注意到事实上 每种情况下 我们都将原图 割开
(否则则对应情况下 剩余流量 可以再增加)
而原图的 最大流 仅与 最小的割 对应,故应当取 \(\min\)
注意到使用 \(FF\) 的 时间复杂度是 \(O(2 ^ k wm + 2 ^ k q)\) 的
\(O(wm)\) 即 单次增广 时间,\(m\) 为 边数,\(w\) 为 边容量(\(INF = 25\))
由于 每次增广 可以基于 前面的情况 加一条边 得到
而 \(Simplex\) 的理论复杂度也应当是 \(O(2 ^ k wm + 2 ^ k q)\) 的,但是实际就比较玄学了
洛谷暂时 \(rk3\),但 \(CodeForces\) 上的家伙 好像都跑得飞快
#include <bits/stdc++.h>
#define lowbit(x) (x & -x)
using namespace std;
namespace Fastio {
#define USE_FASTIO 1
#define IN_LEN 50000
#define OUT_LEN 50000
char ch, c; int len;
short f, top, s;
inline char Getchar() {
static char buf[IN_LEN], *l = buf, *r = buf;
if (l == r) r = (l = buf) + fread(buf, 1, IN_LEN, stdin);
return (l == r) ? EOF : *l++;
}
char obuf[OUT_LEN], *ooh = obuf;
inline void Putchar(char c) {
if (ooh == obuf + OUT_LEN) fwrite(obuf, 1, OUT_LEN, stdout), ooh = obuf;
*ooh++ = c;
}
inline void flush() { fwrite(obuf, 1, ooh - obuf, stdout); }
#undef IN_LEN
#undef OUT_LEN
struct Reader {
template <typename T> Reader& operator >> (T &x) {
x = 0, f = 1, c = Getchar();
while (!isdigit(c)) { if (c == '-') f *= -1; c = Getchar(); }
while ( isdigit(c)) x = (x << 3) + (x << 1) + (c ^ 48), c = Getchar();
x *= f;
return *this;
}
Reader() {}
} cin;
const char endl = '\n';
struct Writer {
typedef int mxdouble;
template <typename T> Writer& operator << (T x) {
if (x == 0) { Putchar('0'); return *this; }
if (x < 0) Putchar('-'), x = -x;
static short sta[40];
top = 0;
while (x > 0) sta[++top] = x % 10, x /= 10;
while (top > 0) Putchar(sta[top] + '0'), top--;
return *this;
}
Writer& operator << (const char *str) {
int cur = 0;
while (str[cur]) Putchar(str[cur++]);
return *this;
}
inline Writer& operator << (char c) {Putchar(c); return *this;}
Writer() {}
~ Writer () {flush();}
} cout;
#define cin Fastio::cin
#define cout Fastio::cout
#define endl Fastio::endl
}
namespace MCMF {
const int MAXN = 10050;
const int MAXP = (1 << 10) + 02;
const int INF = 1e6; // Don't Be Too Large
struct Edge {
int u, v, nxt;
int f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const int f, const int w = 0) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
int Pre[MAXN];
int fa[MAXN], fe[MAXN], Cir[MAXN], Tag[MAXN];
int Now = 0, S = 10001, T = 10002;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (Tag[E[i].v] != Nod && E[i].f) Init_ZCT (E[i].v, i, Nod);
}
inline int Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline int Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Cnt = 0, Del = 0, P = 2;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
int F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (F > E[fe[u]].f)
F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (F > E[fe[u] ^ 1].f)
F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += F * E[Cir[i]].w, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lste = x ^ P, Lstu = v, Tmp;
while (Lstu != Del) {
Lste ^= 1, -- Tag[u];
swap (fe[u], Lste);
Tmp = fa[u], fa[u] = Lstu, Lstu = u, u = Tmp;
}
return Cost;
}
int MinC = 0;
inline int Simplex () {
memset (fa, 0, sizeof fa);
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0, ++ Now);
Tag[T] = ++ Now, fa[T] = MinC = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MinC += E[tot].f * INF;
return E[tot].f;
}
}
namespace Value {
using namespace MCMF;
int N, M, K, Q;
int u, v, c;
struct KeyEdge {
int u, v, c;
} P[15];
int Val[MAXP][MAXN << 1];
int Low[MAXP], Log[MAXP];
int Ans[MAXP];
int Pmx[MAXP];
int Hzc[MAXP];
int Tmp[15];
inline void Solve () {
cin >> N >> M >> K >> Q;
Add_Edge (S, 1, INF);
Add_Edge (N, T, INF);
for (int i = 1; i <= K; ++ i) cin >> u >> v >> c, Hzc[i] = tot + 1, Add_Edge (u, v, c);
for (int i = K + 1; i <= M; ++ i) cin >> u >> v >> c, Add_Edge (u, v, c);
for (int i = 1; i < MAXP; ++ i) Low[i] = lowbit (i);
for (int i = 1; i < MAXP; i <<= 1) Log[i] = Log[i >> 1] + 1;
for (int k = 2; k < tot; ++ k) Val[0][k] = E[k].f;
for (int i = 0; i < (1 << K); ++ i) {
for (int k = 2; k < tot; ++ k) E[k].f = Val[i ^ Low[i]][k];
E[Hzc[Log[Low[i]]]].f = 25;
Ans[i] = Ans[i ^ Low[i]] + Simplex ();
for (int k = 2; k < tot; ++ k) Val[i][k] = E[k].f;
}
for (int i = 1; i <= Q; ++ i) {
for (int k = 1; k <= K; ++ k) cin >> Tmp[k];
int ans = 1e9; Pmx[0] = 0;
for (int k = 1; k < (1 << K); ++ k) Pmx[k] = Pmx[k ^ Low[k]] + Tmp[Log[Low[k]]];
for (int k = 0; k < (1 << K); ++ k) ans = min (ans, Ans[k] + Pmx[k ^ ((1 << K) - 1)]);
cout << ans << '\n';
}
}
}
int main () {
Value::Solve ();
return 0;
}
注意到不知道为什么 不加快读 的话,这玩意儿会在 求最大流 的时候 卡住
对,不是在输入的时候卡...
非常神秘
Luogu P4249 [WC2007] 剪刀石头布
考虑 形式化题意
即 在一个完全图中,存在若干 有向边 和 无向边,现将 无向边 定向
求 定向后 图中 有向三元环 的个数最大值(我们可以将 出入度 理解成 输赢的场次)
注意到在完全图中 任选三个点,其间必有边相连
简单画图可以发现,如果三边 均有向
若其构成 有向三元环,则其中 每个点的出入度 均为 \(1\)
否则必 有且仅分别有一点 出度为 \(2\) 和 入度为 \(2\)
于是我们考虑将 有向三元环的计数 转换到 点出入度 \(D_i\) 的统计 上
注意到这里 \(D_i\) 为 出度 或 入度 都行,不要一起统计就好,后面会讲原因
这里先给出结论,若这个完全图有 \(N\) 个点,则 经过一个点 \(i\) 的 有向三元环 个数为
最终答案 就是
其实前面 \(N(N - 1)(N - 2)\) 就是 图中任意三元组 个数
后面 \(\sum_{i = 1} ^ N D_i (D_i - 1)\) 就是每个点 不能形成有向三元环 的 三元组个数,最后除以排列
我们可以举例一个 点数为 \(5\) 的完全图 来感性理解上面的结论
任取一点后,那么在 剩余的点 中 任取两点 就可以构成一个三元组
所以对于 一个点,三元组个数是 \(4 \times 3 \div 2 = 6\) 个
考虑 计数有向三元环,假设这个点 出/入度 为 \(3\)
那么意味着 这个点 的 \(6\) 个 三元组 中有 \(3 \times 2 \div 2 = 3\) 个 不构成有向三元环
可以理解为每个 不构成有向三元环 的方案需要三元组一个点 出/入度 为 \(2\),于是 \(3\) 选 \(2\)
然后对每个点进行同理考虑即可
可以知道,不构成有向三元环 的个数就是从 一个点 的 \(D_i\) 个 出/入度 里 选 \(2\) 个
(加上这个点本身),构成的三元组个数,也就是 \(D_i (D_i - 1)\) 个
建图,直接考虑 将 每条无向边 对应一个 新点,源点 \(S\) 连接,容量 \(1\) 费用 \(0\)
无向边对应点 连 这条无向边在 原图上的两端点对应的点,容量 \(1\) 费用 \(0\)
上面就表示 无向边 \(\to\) 两支队伍有一只会赢 \(\to\) 有一个贡献 出入度
初始的有向边 可以直接转化成 源点 \(S\) 连接 对应端点,也就是 这支队伍肯定会贡献
优化的话也可以考虑 统计初始就固定的 出入度,就可以不用建这些东西
最后每支队伍向 汇点 \(T\) 连接费用为 \(0,1,2,...,N\) 的 等差数列 费用的边
表示每增加 \(1\) 的 \(D_i\),增加的 ”不构成有向三元环“ 的个数,也就是 \(\sum_{i = 1} ^ N D_i (D_i - 1)\) 的 增量
最后 跑费用流板子就可以了
前面有提到,\(D_i\) 统计一个点 出度 或 入度 即可,不能两个都统计
那为什么都行捏?它们 答案相等 吗?
\(UPD ~ 24.03.13\)
这里 \(Union\_of\_Britain\) 老师提供了一种 很好的理解方式
我们 把每条边反转,然后...
好我是小丑感性理解 会有人说 出度 和 入度,一一对应,且总和相等,所以答案应当相等
但这是错误的,我们实际上可以举出 以下例子,假设 \(N = 6\),那么 出/入度和 应分别为 \(15\)
\(In\) \(1\) \(5\) \(5\) \(0\) \(1\) \(3\) \(Out\) \(4\) \(0\) \(0\) \(5\) \(4\) \(2\) 显然根据 \(D_i (D_i - 1)\) 这个式子,上表的 出入度 根本不能 一一对应 起来,但是...
\(Ans_{in} = ((1 \times 0) + (5 \times 4) + (5 \times 4) + (0 \times -1) + (1 \times 0) + (3 \times 2)) \div 2 = 23\)
\(Ans_{out} = ((4 \times 3) + (0 \times -1) + (0 \times -1) + (5 \times 4) + (4 \times 3) + (2 \times 1)) \div 2 = 23\)
艹还真是相等的?我们可以来证明一下,设 \(T = N - 1\),此处不妨令 \(D_i\) 等于 入度 \(Deg_{in}\)于是我们有 \(\sum D_i = \dfrac {T (T + 1)} {2}\)(下文均默认 \(i \in [1,N]\))
于是显然 \(Ans_{in} = \sum D_i (D_i - 1) = \sum {D_i ^ 2} - \sum {D_i}\)
$Ans_{out} = \sum (T - D_i) (T - D_i - 1) = \sum {(T ^ 2 - D_i (2 T + 1) + D_i ^ 2 - T)} $
\[\\ \therefore Ans_{in} = \sum {D_i ^ 2} - \sum {D_i} \\ \]\[Ans_{out} = \sum {(T ^ 2 - D_i (2 T + 1) + D_i ^ 2 - T)} \\ \]\[= \sum {D_i ^ 2} - \sum {D_i} (2 T - 1) + (T + 1) (T ^ 2 - T) \]\[\\ \because \sum {D_i} = \dfrac {(T + 1) T} {2} \\ \]\[\therefore (T + 1) (T ^ 2 - T) = \dfrac {(T + 1) T} {2} (T - 1) \times 2 = \sum {D_i} \times (2 T - 2) \\ \]\[\therefore Ans_{out} = \sum {D_i ^ 2} - \sum {D_i} \times (2 T - 1) + \sum {D_i} \times (2 T - 2) \\ \]\[= \sum {D_i ^ 2} - \sum {D_i} \\ \]\[= Ans_{in} \]得证
最后贴个代码
#include <bits/stdc++.h>
const int MAXN = 15005;
const int INF = 1e9;
using namespace std;
namespace MCMF2 {
struct Edge {
int u, v, nxt, f, w;
} E[MAXN << 1];
int H[MAXN], tot = 1;
inline void Add_Edge (const int u, const int v, const int f, const int w) {
E[++ tot] = {u, v, H[u], f, + w}, H[u] = tot;
E[++ tot] = {v, u, H[v], 0, - w}, H[v] = tot;
}
int fa[MAXN], fe[MAXN], Tag[MAXN], Pre[MAXN], Cir[MAXN];
int S = 11451, T = 14514, Now = 0;
inline void Init_ZCT (const int x, const int e, int Nod = 1) {
fe[x] = e, fa[x] = E[e].u, Tag[x] = Nod;
for (int i = H[x]; i; i = E[i].nxt)
if (E[i].f && Tag[E[i].v] != Nod) Init_ZCT (E[i].v, i, Nod);
}
inline int Sum (const int x) {
if (Tag[x] == Now) return Pre[x];
Tag[x] = Now, Pre[x] = Sum (fa[x]) + E[fe[x]].w;
return Pre[x];
}
inline int Push_Flow (const int x) {
int rt = E[x].u, lca = E[x].v, Del = 0, P = 2, Cnt = 0;
++ Now;
while (rt) Tag[rt] = Now, rt = fa[rt];
while (Tag[lca] != Now) Tag[lca] = Now, lca = fa[lca];
int F = E[x].f, Cost = 0;
for (int u = E[x].u; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u];
if (E[fe[u]].f < F) F = E[fe[u]].f, Del = u, P = 0;
}
for (int u = E[x].v; u != lca; u = fa[u]) {
Cir[++ Cnt] = fe[u] ^ 1;
if (E[fe[u] ^ 1].f < F) F = E[fe[u] ^ 1].f, Del = u, P = 1;
}
Cir[++ Cnt] = x;
for (int i = 1; i <= Cnt; ++ i)
Cost += E[Cir[i]].w * F, E[Cir[i]].f -= F, E[Cir[i] ^ 1].f += F;
if (P == 2) return Cost;
int u = E[x].u, v = E[x].v;
if (P == 1) swap (u, v);
int Lst_u = v, Lst_e = x ^ P, Tmp;
while (Lst_u != Del) {
Lst_e ^= 1, Tag[u] --;
swap (fe[u], Lst_e);
Tmp = fa[u], fa[u] = Lst_u, Lst_u = u, u = Tmp;
}
return Cost;
}
int MinC = 0, MaxF = 0;
inline int Simplex () {
Add_Edge (T, S, INF, - INF);
Init_ZCT (T, 0, ++ Now);
Tag[T] = ++ Now, fa[T] = 0;
bool Run = 1;
while (Run) {
Run = 0;
for (int i = 2; i <= tot; ++ i)
if (E[i].f && E[i].w + Sum (E[i].u) - Sum (E[i].v) < 0)
MinC += Push_Flow (i), Run = 1;
}
MaxF = E[tot].f, MinC += MaxF * INF;
return MaxF;
}
}
namespace Value {
int N;
int Mp[105][105], I[105], O[105];
inline int G (const int x, const int y) {
return x * 100 + y + 1000;
}
using namespace MCMF2;
inline void Solve () {
cin >> N;
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= N; ++ j) {
cin >> Mp[i][j];
if (Mp[i][j] == 1) ++ I[i];
if (j > i && Mp[i][j] == 2) ++ O[i], ++ O[j], Add_Edge (S, G (i, j), 1, 0), Add_Edge (G (i, j), i, 1, 0), Add_Edge (G (i, j), j, 1, 0);
}
for (int i = 1; i <= N; ++ i) MinC += I[i] * (I[i] - 1) / 2;
for (int i = 1; i <= N; ++ i)
for (int j = 1; j <= O[i]; ++ j)
Add_Edge (i, T, 1, I[i] + j - 1);
Simplex ();
cout << N * (N - 1) * (N - 2) / 6 - MinC << endl;
for (int i = 1; i <= N; ++ i) {
for (int j = 1; j <= N; ++ j) {
if (j > i)
for (int k = H[G (i, j)]; k; k = E[k].nxt) {
if (E[k].v == i && E[k].f == 0) Mp[i][j] = 1, Mp[j][i] = 0;
if (E[k].v == j && E[k].f == 0) Mp[i][j] = 0, Mp[j][i] = 1;
}
cout << Mp[i][j] << ' ';
}
cout << endl;
}
}
}
int main () {
Value::Solve ();
return 0;
}
没写,鸽了;
后记
网络流的题有非常多,但是很大部分都是 套路东西
剩下一小部分一般都是 看了题解也不会 的批题
这篇博客 重点 对 前面的东西 进行了一个写,可能写的很烂
然后算法部分 只讲了一些 本人认为比较有用的(鞭尸 \(HLPP\))
所以可能 十分的不完善,也非常欢迎 各位巨佬 批评指正(这篇文章可能在之后会有修改)
这里给出写作过程中帮助极大的一些参考 资料/ 博客,都写的很牛
[1] NOI 一轮复习 I:二分图网络流 - 洛谷专栏 (luogu.com)(体系梳理的很清晰,例题很全)
(求 yny の 优化 在 费用流 上的实现)
(求 \(LCT\) / 分块 的 \(Simplex\) 实现)
就这样,模拟费用流 的部分比较复杂,就不在这里提了