浅谈网络流
浅谈网络流
序
最近网络流做了一堆,感觉有微弱的进步!
尝试用 尽量人话 的东西讲清网络流的一些 算法和套路
里面可能有很多 感性理解的东西,需要详细证明的可以看对应部分的 参考资料 等
并记录一些 好的错误,以便以后再错
这东西算法十分的多,本篇博客 只讲了本人比较常用的部分算法(做题够用了)
纠结效率的移步 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\) 实现)
就这样,模拟费用流 的部分比较复杂,就不在这里提了