模拟费用流小记

command_block

2021-04-03 19:59:31

Personal

# 0. 概论 费用流问题是 OI 中的一个应用广泛的经典模型,可以解决种类繁多的最优化问题,以思路巧妙,变化多端著称。 然而,与良好的普适性相对,费用流常用算法的效率较为低下,只能处理较小的数据范围。 在一些特殊的费用流模型中,可以利用题目的特殊性质,结合数据结构等技巧,设计更高效的费用流算法,称之为“模拟费用流”。 这种思路先将原问题转化为费用流问题,再进一步转化成其他更简化的(数据结构)问题。 其中,费用流模型扮演了关键的桥梁角色,帮助我们规范分析,洞察性质,并施展积累的经验技巧。 - **参考资料** - 陈江伦(laofu) WC2019 讲义 《模拟费用流问题》 # 1. 费用流模型与常用性质 费用流基础知识可见 : [网络流/二分图相关笔记(干货篇)](https://www.luogu.com.cn/blog/command-block/wang-lao-liu-er-fen-tu-xiang-guan-bi-ji-gan-huo-pian-post) 下面也开列一些有用的事实。 为了便于叙述,默认为最小费用流。 - ### 费用流经典算法 - **EK** : 每次寻找最短的增广路进行增广。( zkw和EK本质相同,只是实现方式不同 ) - **消圈** : 先随意求出一个流,然后不断消去图中的负环。 可以直接模拟上述算法的思路,也可以基于这些算法的正确性,设计类似的规则。 - ### 费用流的凸性 根据 EK 算法,增广路的长度会不断增长,则费用是关于流量的凸函数。 - ### 费用流问题的变形 - **最小费用任意流** : 可用 EK 求解,每次增广最短路,当单次费用为正时停止。 - **负费用最大流** : 可用 EK 求解,每次增广最短路,当总费用非正时停止。 上述做法由凸性易证正确性。 - **增量-最小费用任意流** : 每次在图中增加一些点和边,并求解新的最小费用任意流。 只需考虑所有新的 “源到汇的负路径” 以及 “负环” 的贡献。 也可以看做源和汇之间有容量为无穷的边连接,这样“源到汇的负路径”实际上就是“负环”。 - ### 经典结论 - 在非增量费用流中,源汇边不会退流。 - 增广路不会多次经过同一个点。 # 2. 经典例题 - $\large\color{blue}\bf\Delta$ [P4694 [PA2013]Raper](https://www.luogu.com.cn/problem/P4694) **题意** : 你需要生产 $k$ 张光盘。每张光盘都要经过两道工序:先在 A 工厂进行挤压,再送到 B 工厂涂上反光层。 现在有 $n$ 天时间。 每天可以先送一张光盘到 A 工厂(或者不送),然后再送一张已经在 A 工厂加工过的光盘到 B 工厂(或者不送),每家工厂一天只能对一张光盘进行操作,同一张光盘在一天内生产出来是允许的。 每天两工厂加工的费用不尽相同, A 工厂第 $i$ 天的要价为 $a_i$, B 工厂即为 $b_i$。 求生产出 $k$ 张光盘的最小花费。 ------------ - **费用流模型** 从源点向每天的 A 厂连边,容为 $1$ ,费用为 $a_i$。 从每天的 B 厂向汇点连边,容为 $1$ ,费用为 $b_i$。 从第 $i$ 天的 A 厂向 $i\leq j\leq n$ 的第 $j$ 天的 B 厂连边。 限制源的总流量为 $k$。 不难发现,这是一个类似于费用匹配的问题。 ------------ - 解法 A : **WQS二分** + **模拟费用流** 这是市面上流传较广的解法。 费用流告诉我们,这个问题的答案关于 $k$ 有凸性,故可以使用 WQS 二分来优化。 在二分惩罚 $C$ 之后,只需要给全体 $a_i$ 加上 $C$。此时不需要再考虑 $k$ 的约束。 考虑使用 “**增量-最小费用任意流**” 模型。 依次考虑 $a_n...a_1$ ,它们能匹配的 $b$ 的集合依次增大。相当于在图上依次增加点。 考虑新加入的两个点以及相关的边能引出怎样的 “源到汇的路径” 以及 “环” : ![](https://cdn.luogu.com.cn/upload/image_hosting/3o3amb67.png) 如图 I ,红色边为新加入的边(方向确定),黑色边为旧的边,方向是不确定的(可能由于增广而反向) - 源汇路径 : 必然经过 S 惟一的红出边,所有 T 的入边都是可选的。这对应选择一个最小的未被匹配的 $b$ 进行匹配。如图 II 。 - 负环 : 经过讨论,图中可能的新环只有形如图 III,IV 的两种形式。 对于形式 III ,对应以目前的 $a$ 替换掉之前的一个比较大的 $a$。 对于形式 IV ,实际上永远不优。其可以拆分成一条 S→T 和一条 T→S。由于所有 T→S 的权值必定小于 0 (否则当初不会选取),故该方案不如单走一个 S→T。 然而,在有流量限制的原问题中,单走一个 S→T 可能不合法,使得我们的证明失效。这也是为什么我们要用 WQS 二分去掉 $k$ 的限制。 上述合法的两种决策容易用堆维护(注意还需维护匹配个数),故复杂度为 $O(n\log^2n)$。 ------------ - 解法B : **直接模拟费用流** 考虑直接模拟 EK 算法,直接在整张图上增广。 不难发现,增广的过程中,必然不会退掉和源点、汇点直接相连的边,只有中间的边可能被退流。 为了彻底规避对退流的分析,我们可以根据源汇边的状态来判定中间的边是否可能合法。 具体地,将操作 A (增广一条源边)视作左括号,操作 B (增广一条汇边)视作右括号,方案合法当且仅当括号构成合法的括号序列。 由于源汇边不会退流,每次增广都**恰会增加一对括号**,且保持括号序列合法。 于是,我们将原问题转化为了对括号序列的维护。 将 $($ 记为 $+1$ ,而 $)$ 记为 $-1$ ,则合法括号序列的充要条件是 : 前缀和均为正。记 $S_i$ 为括号权值的前缀和。 若增加的括号为 $i:(,\ \ j:)$ ,分两类讨论。 - $i\leq j$ ,即选出 `...(...)...` ,则 $S_{[i,j)}$ 加一。 - $i>j$ ,即选出 `...)...(...` ,则 $S_{[j,i)}$ 减一,这要求操作前区间内所有 $S$ 大于 $0$。 使用线段树维护,对于区间 $[l,r]$,记录下列值 : - $mxl$ : 左括号权值最小值。 - $mxr$ : 右括号权值最小值。 - $mxl2$ : 左括号权值最小值,设位置为 $A_i$ ,还需满足 $[l,i)$ 内的最小值严格大于区间最小值。 - $mxr2$ : 右括号权值最小值,设位置为 $B_i$ ,还需满足 $[i,r]$ 内的最小值严格大于区间最小值。 - $mxs$ : $S$ 的最小值。 - $tg$ : $S$ 的加法标记。 - $ans1$ : $i\leq j$ 时的最佳答案。 - $ans2$ : $i>j$ 时的最佳答案(不考虑是否合法)。 - $ans3$ : $i>j$ 时的最佳答案,还需满足 $[j,i)$ 内的最小值严格大于区间最小值。 具体转移见代码。 由于 $S_{1\sim n}$ 必定 $\geq 0$ ,且 $S_n$ 恒为 $0$ ,故每次取出节点 $[1,n]$ 的 $ans1,ans3$ 中的较优解。 复杂度 $O(n+k\log n)$。 此做法能分别求出 $k=1\sim n$ 时的答案。 [评测记录](https://www.luogu.com.cn/record/48904400) ------------ - [CF865D Buy Low Sell High](https://www.luogu.com.cn/problem/CF865D) (**老鼠进洞其一**) **题意** : 已知接下来 $n$ 天的股票价格 $c_i$,每天你可以买进一股股票,卖出一股股票,或者什么也不做。 假设你拥有无限本金,求 $n$ 天之后能得到的最大利润。 ------------ - $\small\blacksquare$ **费用流模型** 这也是一个费用匹配问题。 从源点向每天连边,容为 $1$ ,费用为 $-c_i$。 从每天向汇点连边,容为 $1$ ,费用为 $c_i$。 每天向第二天连边,容为 $+\infty$ ,费用为 $0$。 求最大费用任意流。 - $\small\blacksquare$ **模拟费用流** 考虑使用 “**增量-最大费用任意流**” 模型。 ![](https://cdn.luogu.com.cn/upload/image_hosting/2czgq5eo.png) 图二为新加一个点产生的边。 图三,四为可能生效的增广路、负环。 容易发现,我们的决策只有两种 : - 选择之前较为便宜的一天买入,然后在今天卖出。 - 选择之前出售过的某一天,改为保留到今天卖出。 容易用堆维护。事实上这就是“反悔贪心”。 复杂度为 $O(n\log n)$。 ```cpp #include<algorithm> #include<cstdio> #include<queue> using namespace std; priority_queue<int> q; int n; int main() { scanf("%d",&n); long long ans=0; for (int i=1,x;i<=n;i++){ scanf("%d",&x); q.push(-x); if (x+q.top()<0)continue; ans+=x+q.top();q.pop();q.push(-x); }printf("%lld",ans); return 0; } ``` - $\large\color{blue}\bf\Delta$ [P1484 种树](https://www.luogu.com.cn/problem/P1484) **题意** : 在一排 $n$ 个坑上种树。种在第 $i$ 个坑的收益为 $c_i$ ,但不能在相邻两个坑同时种树。 问至多种 $k$ 棵树能获得的最大收益。 - $\small\blacksquare$ **费用流模型** 为每个“间隔”建立一个点,坑则是边。 对奇数个间隔,从源点连接,容量为 $1$。对于偶数个间隔,连向汇点,容量为 $1$。 对于位置 $i$ ,两侧是间隔 $i-1,i$ ,从奇数间隔连向偶数间隔,容量为 $1$ ,费用为 $c_i$。 ![](https://cdn.luogu.com.cn/upload/image_hosting/ylgl4g3s.png) 限制流量为 $k$ ,求最大费用任意流即可。 (有了费用流做法,就能证明该问题的凸性,然后可以使用 WQS 二分求解) - $\small\blacksquare$ **模拟费用流** 考虑直接模拟 EK 算法。 观察一次增广,会连续流过中间的若干条边(包含部分反向边,表示反悔),然后将它们的状态取反。 对应到原问题中,相当于将一个区间的树的状态取反,要求这个区间必然形如 “空【空树空树空……树】空”(中间空树严格相间,外侧必为空)。 观察可选的增广区间,如图 : ![](https://cdn.luogu.com.cn/upload/image_hosting/11ruwght.png) 每个极长的两边空的空树相间区间都是可选的,其余区间都不可选。只需要维护这类区间即可。 进一步观察,每次增广会将相邻的三个区间合并为一个。 用双向链表和堆维护,复杂度为 $O(n\log n)$ ,具体方法见代码。 ```cpp #include<algorithm> #include<cstdio> #include<queue> #define Pr pair<int,int> #define fir first #define sec second #define mp make_pair #define ll long long #define MaxN 500500 using namespace std; int n,k,l[MaxN],r[MaxN],a[MaxN],tot; ll ans;bool e[MaxN]; priority_queue<Pr> t; int main() { scanf("%d%d",&n,&k); for (int i=1;i<=n;i++){ scanf("%d",&a[i]); t.push(mp(a[i],i)); l[i]=i-1;r[i]=i+1; } e[0]=e[n+1]=1; while(tot<k){ Pr now=t.top();t.pop(); if (now.fir<0)break; int p=now.sec; if (e[p]||now.fir!=a[p])continue; ans+=a[p];tot++; a[p]=a[l[p]]+a[r[p]]-a[p]; e[l[p]]=e[r[p]]=1; l[p]=l[l[p]];r[p]=r[r[p]]; r[l[p]]=p;l[r[p]]=p; t.push(mp(a[p],p)); }printf("%lld",ans); return 0; } ``` - $\large\color{blue}\bf\Delta$ [CF730I Olympiad in Programming and Sports](https://www.luogu.com.cn/problem/CF730I) **题意** : 有 $n$ 个人,每个人有两个权值 $a_i,b_i$。 选出大小为 $k_1,k_2$ 的两组人 $S_1,S_2$ ,要求 $S_1∩S_2=\varnothing$。 获得的收益为 $\sum_{i\in S_1}a_i+\sum_{i\in S_2}b_i$ ,最大化这个值。 - $\small\blacksquare$ **费用流模型** 这是一个二分图费用多重匹配问题。特殊地,右部的点数非常少,只有 $2$。 - $\small\blacksquare$ **模拟费用流** 暴力讨论增广路,可以得到以下几种策略 : - 一个没有归属的人选 $S_1$。 - 一个没有归属的人选 $S_2$。 - 一个没有归属的人选 $S_1$,一个本来在 $S_1$ 内的人改选 $S_2$。 - 一个没有归属的人选 $S_2$,一个本来在 $S_2$ 内的人改选 $S_1$。 于是用 $4$ 个堆来维护 $4$ 类可能的决策 : - 没有归属,欲选 $S_1$ - 没有归属,欲选 $S_2$ - 已选 $S_2$,欲选 $S_1$ - 已选 $S_1$,欲选 $S_2$ 复杂度为 $O(n\log n)$。 扩展到右部点为 $k$ 个也有类似做法。 [CF评测记录](https://codeforces.com/contest/730/submission/119530159) ------------ - $\large\color{blue}\bf\Delta$ BZOJ4877 [Lydsy1708月赛]跳伞求生 (**老鼠进洞其二**) **题意** : 有 $n$ 名玩家,第 $i$ 名玩家战斗力为 $a_i$ 。 有 $m$ 名敌人,第 $i$ 个敌人战斗力为 $b_i$ ,赏金为 $c_i$。 若 $a_i>b_j$ ,则第 $i$ 名玩家可以消灭第 $j$ 名敌人,并获得 $a_i-b_j+c_i$ 的分数。 每名玩家至多消灭一名敌人,也可以什么都不做,求最大得分。 ------------ 和“老鼠进洞其一”的建图很类似。 按照 $a,b$ 排序,从小到大添加人物。只有下列几种决策 : - 用当前的 $a_i$ 匹配之前的某个 $b_k$。收益为 $a_i-b_k+w_k$。 - 用当前的 $a_i$ 换掉之前的某个 $a_k$。收益为 $a_i-a_k$。 用堆维护 $-b_k+w_k,-a_k$ 等决策即可。复杂度为 $O(n\log n)$。 ```cpp #include<algorithm> #include<cstdio> #include<queue> #define ll long long #define MaxN 100500 using namespace std; const int INF=1000000000; struct Data{int x,c;}a[MaxN<<1]; bool cmp(const Data &A,const Data &B) {return A.x==B.x ? A.c<B.c: A.x<B.x;} priority_queue<int> q; int n,m; int main() { scanf("%d%d",&n,&m); for (int i=1;i<=n;i++){ scanf("%d",&a[i].x); a[i].c=-1; } for (int i=n+1;i<=n+m;i++) scanf("%d%d",&a[i].x,&a[i].c); sort(a+1,a+n+m+1,cmp); q.push(-INF); ll ans=0; for (int i=1;i<=n+m;i++) if (a[i].c==-1){ if (q.top()+a[i].x>0){ ans+=q.top()+a[i].x;q.pop(); q.push(-a[i].x); } }else q.push(a[i].c-a[i].x); printf("%lld",ans); return 0; } ``` ------------ - $\large\color{blue}\bf\Delta$ [#455. 【UER #8】雪灾与外卖](https://uoj.ac/problem/455) (老鼠进洞其三) **题意** : 在一条直线上,有 $n$ 名送餐员与 $m$ 个餐厅。 第 $i$ 个送餐员的位置为 $x_i$。第 $i$ 家餐厅的位置为 $y_i$ ,最多生产 $c_i$ 道菜,每道菜要价 $w_i$ 元。 每个送餐员只能送一道菜,若让第 $i$ 个送餐员前往第 $j$ 个餐厅取菜,他会索要 $|x_i-y_j|$ 的报酬。 求让 $n$ 个送餐员都拿到菜的最小花费。 ------------ 建图仍然是类似的,只不过中间的边变成了双向的,而且有了费用。 为每个送餐员附加 $-\infty$ 的费用,这样才能完全转化为“最小费用任意流”。 仍然按照坐标从左到右考虑。(实际上中间的点不会既连源又连汇,这只是示意图) ![](https://cdn.luogu.com.cn/upload/image_hosting/opakfkrt.png) 在左侧很远处加很多个很差的餐厅,这样送餐员总能够全部匹配,可以忽略 $\rm III$。 接下来考虑其余 $3$ 种增广路在原问题中的意义。 注意到中间的边有正有负,意义并不明确。我们需要进一步观察原问题的性质。 - 一定存在一种最优匹配,使得交叉的匹配不存在。 显然,交叉的匹配可以调整为包含的,且不会变劣。 - 在最优匹配中,每一对匹配中间不会有未匹配的 $x$。 如果有,拿这个 $x$ 来匹配更优。 - 被匹配 $(x_i,y_j)$ 包含的所有匹配方向都与 $(x_i,j_y)$ 相同,否则可以调整更优。 因此,我们的匹配是长这样的 : - $\rm III$ 决策有四种,分别是 : - 对于餐厅 $i$ - 选一个前面的送餐员 $j$ 领菜。花费为 $y_i-x_j+w_i-\infty$。 - 选一个前面被取过菜的餐厅 $j$,改取自己的菜。花费为 $y_i+w_i-y_j-c_j$ - 对于送餐员 $i$ - 选一个前面的餐厅 $j$ 前往取菜。花费为 $x_i-y_j+w_j-\infty$。 - 选一个前面取过菜的送餐员,取他的菜。花费为 $x_i-x_j$。 用四个堆分别维护 : - $-x_j$ (未取菜的送餐员) - $-y_j-w_j$ (已取的菜) - $-y_j+w_j$ (未取的菜) - $-x_j$ (已取菜的送餐员) 注意餐厅有容量,故可能匹配多次。在堆中不仅要记录权值,还要记录个数。(也要记录来源地) 复杂度为 $O(n\log n)$。 ```cpp ``` ------------ - $\large\color{blue}\bf\Delta$ [P6122 [NEERC2016]Mole Tunnels](https://www.luogu.com.cn/problem/P6122) (老鼠进洞其四) **题意** : 给出一棵 $n$ 个点的树,编号为 $1\sim n$ ,以 $1$ 为根。节点 $i\ (2\leq i\leq n)$ 的父亲为 $\lfloor i/2\rfloor$。 第 $i$ 个洞内有 $c_i$ 个食物,共有 $m$ 只鼹鼠,第 $i$ 只住在点 $p_i$。 对于所有 $k=1\sim m$ ,前 $k$ 只鼹鼠各找到一个食物,在树上行走的最小总距离。 ------------ - $\small\blacksquare$ **费用流模型** - 对于树上的每条边 $u\leftrightarrow v$ ,连边 $u\leftrightarrow v$ ,容量无限,费用为 $1$。 - 对于点 $u$ ,连边 $u\rightarrow T$ ,容量为 $c_i$ ,无权。 - 对于鼹鼠 $i$ ,连边 $S\rightarrow p_i$ ,容量为 $1$ ,无权。 求解最小费用最大流。 ------------ - $\small\blacksquare$ **模拟费用流** 首先观察建图 : ![](https://cdn.luogu.com.cn/upload/image_hosting/ta4sbsh4.png) 考虑将每条鼹鼠边额外加一个很小的权值 $-C$ ,然后问题就转化为最小费用任意流。 使用 “**增量-最小费用任意流**” 模型。逐个考虑鼹鼠。 由于必然满流,和 $S$ 相连的边不会退流,不会产生反向边。因此,图中不存在新的环,于是只需考虑新增的源汇路。 由于树的直径是 $O(\log n)$ 级别,可以每次找出最短增广路,并大力处理反向边。 问题不难转化为 : - 查询到点 $u$ 的最近关键点。 - 将树上的边 $u\rightarrow v$ 的权值从 $1$ 改为 $-1$ 或者从 $-1$ 改为 $1$。 - 删除一个关键点。 设 $s[u]$ 为 $u$ 到子树内关键点的最近距离。不难用类似线段树的方法维护。 当查询时,一路向上并查看分支即可。 ```cpp #include<algorithm> #include<cstdio> #define MaxN 100500 using namespace std; const int INF=1000000000; struct Node{int c1,c2,c,p,d;}a[MaxN]; inline void up(int u,int v){ int dl=a[v].d+(a[v].c1 ? -1 : 1); if (dl<a[u].d){a[u].d=dl;a[u].p=a[v].p;} } int n; inline void up(int u) { a[u].d=INF; if ((u<<1)<=n)up(u,u<<1); if ((u<<1|1)<=n)up(u,u<<1|1); if (a[u].c&&a[u].d>0){a[u].d=0;a[u].p=u;} } void build(int u){ if (u>n)return ; build(u<<1);build(u<<1|1);up(u); } void upd(int u){while(u){up(u);u>>=1;}} int e[MaxN],ef; void tag(int u){while(u){e[u]=ef;u>>=1;}} void mdf1(int u){while(e[u]!=ef){(a[u].c2 ? a[u].c2-- : a[u].c1++);u>>=1;}} void mdf2(int u){while(e[u]!=ef){(a[u].c1 ? a[u].c1-- : a[u].c2++);u>>=1;}} int m,ans; int main() { scanf("%d%d",&n,&m); for (int i=1;i<=n;i++)scanf("%d",&a[i].c); build(1); for (int i=1,u;i<=m;i++){ scanf("%d",&u); int v=u,dis=INF,p,sav=0; while(v){ int d=a[v].d+sav; if (dis>d){dis=d;p=a[v].p;} sav+=(a[v].c2 ? -1 : 1); v>>=1; }ans+=dis; a[p].c--; ef++;tag(p);mdf1(u); ef++;tag(u);mdf2(p); printf("%d ",ans); upd(u);upd(p); }return 0; } ``` ------------ - $\large\color{blue}\bf\Delta$ [P3826 [NOI2017] 蔬菜](https://www.luogu.com.cn/problem/P3826) 见 [题解【P3826 [NOI2017] 蔬菜】](https://www.luogu.com.cn/blog/command-block/solution-p3826) ------------ - $\large\color{blue}\bf\Delta$ [P5470 [NOI2019] 序列](https://www.luogu.com.cn/problem/P5470) 见 [题解 【P5470 [NOI2019] 序列】](https://www.luogu.com.cn/blog/command-block/solution-p5470) ------------ - [Loj#574. 「LibreOJ NOI Round #2」黄金矿工](https://loj.ac/p/574) - [Loj#6405. 「ICPC World Finals 2018」征服世界](https://loj.ac/p/6405) **题意** : 给出一棵树,边有边权,均为正。 第 $i$ 个节点上初始时有 $x_i$ 个石子,最终需要至少 $y_i$ 个石子。 将一个石子沿某条边移动,代价为该边的边权。求完成目标所需的最小代价。 $n\leq 2.5\times 10^5,\sum x\leq 10^6$。 ------------ 为了方便,将 $x,y$ 反过来, 则模型和上一题相同。不过方法有较大区别。 仍然为每棵需要进洞的石子加权一个很小的值 $-C$。你 考虑使用 “**增量-最小费用任意流**” 模型。不需要逐个增加流量,于是可以按照任意顺序添加。 在树中由深至浅添加点,如图 : ![](https://cdn.luogu.com.cn/upload/image_hosting/zcj22n7y.png) 左 : 红色为新增的边。 中,右 : 可能的新增广路。不难发现,加入 $u$ 点时只需要考虑 $\rm lca$ 为 $u$ 的点对的贡献。 不能像上一题那样暴力维护反向边了,考虑继续发掘性质。 咕咕。