command_block
2021-04-03 19:59:31
费用流问题是 OI 中的一个应用广泛的经典模型,可以解决种类繁多的最优化问题,以思路巧妙,变化多端著称。
然而,与良好的普适性相对,费用流常用算法的效率较为低下,只能处理较小的数据范围。
在一些特殊的费用流模型中,可以利用题目的特殊性质,结合数据结构等技巧,设计更高效的费用流算法,称之为“模拟费用流”。
这种思路先将原问题转化为费用流问题,再进一步转化成其他更简化的(数据结构)问题。
其中,费用流模型扮演了关键的桥梁角色,帮助我们规范分析,洞察性质,并施展积累的经验技巧。
参考资料
费用流基础知识可见 : 网络流/二分图相关笔记(干货篇)
下面也开列一些有用的事实。
为了便于叙述,默认为最小费用流。
EK : 每次寻找最短的增广路进行增广。( zkw和EK本质相同,只是实现方式不同 )
消圈 : 先随意求出一个流,然后不断消去图中的负环。
可以直接模拟上述算法的思路,也可以基于这些算法的正确性,设计类似的规则。
根据 EK 算法,增广路的长度会不断增长,则费用是关于流量的凸函数。
最小费用任意流 : 可用 EK 求解,每次增广最短路,当单次费用为正时停止。
负费用最大流 : 可用 EK 求解,每次增广最短路,当总费用非正时停止。
上述做法由凸性易证正确性。
增量-最小费用任意流 : 每次在图中增加一些点和边,并求解新的最小费用任意流。
只需考虑所有新的 “源到汇的负路径” 以及 “负环” 的贡献。
也可以看做源和汇之间有容量为无穷的边连接,这样“源到汇的负路径”实际上就是“负环”。
在非增量费用流中,源汇边不会退流。
增广路不会多次经过同一个点。
从源点向每天的 A 厂连边,容为
从每天的 B 厂向汇点连边,容为
从第
限制源的总流量为
不难发现,这是一个类似于费用匹配的问题。
这是市面上流传较广的解法。
费用流告诉我们,这个问题的答案关于
在二分惩罚
考虑使用 “增量-最小费用任意流” 模型。
依次考虑
考虑新加入的两个点以及相关的边能引出怎样的 “源到汇的路径” 以及 “环” :
如图 I ,红色边为新加入的边(方向确定),黑色边为旧的边,方向是不确定的(可能由于增广而反向)
源汇路径 : 必然经过 S 惟一的红出边,所有 T 的入边都是可选的。这对应选择一个最小的未被匹配的
负环 : 经过讨论,图中可能的新环只有形如图 III,IV 的两种形式。
对于形式 III ,对应以目前的
对于形式 IV ,实际上永远不优。其可以拆分成一条 S→T 和一条 T→S。由于所有 T→S 的权值必定小于 0 (否则当初不会选取),故该方案不如单走一个 S→T。
然而,在有流量限制的原问题中,单走一个 S→T 可能不合法,使得我们的证明失效。这也是为什么我们要用 WQS 二分去掉
上述合法的两种决策容易用堆维护(注意还需维护匹配个数),故复杂度为
考虑直接模拟 EK 算法,直接在整张图上增广。
不难发现,增广的过程中,必然不会退掉和源点、汇点直接相连的边,只有中间的边可能被退流。
为了彻底规避对退流的分析,我们可以根据源汇边的状态来判定中间的边是否可能合法。
具体地,将操作 A (增广一条源边)视作左括号,操作 B (增广一条汇边)视作右括号,方案合法当且仅当括号构成合法的括号序列。
由于源汇边不会退流,每次增广都恰会增加一对括号,且保持括号序列合法。
于是,我们将原问题转化为了对括号序列的维护。
将
若增加的括号为
使用线段树维护,对于区间
具体转移见代码。
由于
复杂度
此做法能分别求出
评测记录
CF865D Buy Low Sell High (老鼠进洞其一)
题意 : 已知接下来
假设你拥有无限本金,求
这也是一个费用匹配问题。
从源点向每天连边,容为
从每天向汇点连边,容为
每天向第二天连边,容为
求最大费用任意流。
考虑使用 “增量-最大费用任意流” 模型。
图二为新加一个点产生的边。
图三,四为可能生效的增广路、负环。
容易发现,我们的决策只有两种 :
选择之前较为便宜的一天买入,然后在今天卖出。
选择之前出售过的某一天,改为保留到今天卖出。
容易用堆维护。事实上这就是“反悔贪心”。
复杂度为
#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;
}
为每个“间隔”建立一个点,坑则是边。
对奇数个间隔,从源点连接,容量为
对于位置
限制流量为
(有了费用流做法,就能证明该问题的凸性,然后可以使用 WQS 二分求解)
考虑直接模拟 EK 算法。
观察一次增广,会连续流过中间的若干条边(包含部分反向边,表示反悔),然后将它们的状态取反。
对应到原问题中,相当于将一个区间的树的状态取反,要求这个区间必然形如 “空【空树空树空……树】空”(中间空树严格相间,外侧必为空)。
观察可选的增广区间,如图 :
每个极长的两边空的空树相间区间都是可选的,其余区间都不可选。只需要维护这类区间即可。
进一步观察,每次增广会将相邻的三个区间合并为一个。
用双向链表和堆维护,复杂度为
#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;
}
这是一个二分图费用多重匹配问题。特殊地,右部的点数非常少,只有
暴力讨论增广路,可以得到以下几种策略 :
一个没有归属的人选
一个没有归属的人选
一个没有归属的人选
一个没有归属的人选
于是用
没有归属,欲选
没有归属,欲选
已选
已选
复杂度为
扩展到右部点为
CF评测记录
和“老鼠进洞其一”的建图很类似。
按照
用当前的
用当前的
用堆维护
#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;
}
建图仍然是类似的,只不过中间的边变成了双向的,而且有了费用。
为每个送餐员附加
仍然按照坐标从左到右考虑。(实际上中间的点不会既连源又连汇,这只是示意图)
在左侧很远处加很多个很差的餐厅,这样送餐员总能够全部匹配,可以忽略
接下来考虑其余
注意到中间的边有正有负,意义并不明确。我们需要进一步观察原问题的性质。
一定存在一种最优匹配,使得交叉的匹配不存在。
显然,交叉的匹配可以调整为包含的,且不会变劣。
在最优匹配中,每一对匹配中间不会有未匹配的
如果有,拿这个
被匹配
因此,我们的匹配是长这样的 :
决策有四种,分别是 :
对于餐厅
选一个前面的送餐员
选一个前面被取过菜的餐厅
对于送餐员
选一个前面的餐厅
选一个前面取过菜的送餐员,取他的菜。花费为
用四个堆分别维护 :
注意餐厅有容量,故可能匹配多次。在堆中不仅要记录权值,还要记录个数。(也要记录来源地)
复杂度为
对于树上的每条边
对于点
对于鼹鼠
求解最小费用最大流。
首先观察建图 :
考虑将每条鼹鼠边额外加一个很小的权值
使用 “增量-最小费用任意流” 模型。逐个考虑鼹鼠。
由于必然满流,和
由于树的直径是
问题不难转化为 :
查询到点
将树上的边
删除一个关键点。
设
当查询时,一路向上并查看分支即可。
#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;
}
见 题解【P3826 [NOI2017] 蔬菜】
见 题解 【P5470 [NOI2019] 序列】
Loj#574. 「LibreOJ NOI Round #2」黄金矿工
Loj#6405. 「ICPC World Finals 2018」征服世界
题意 : 给出一棵树,边有边权,均为正。
第
将一个石子沿某条边移动,代价为该边的边权。求完成目标所需的最小代价。
为了方便,将
仍然为每棵需要进洞的石子加权一个很小的值
考虑使用 “增量-最小费用任意流” 模型。不需要逐个增加流量,于是可以按照任意顺序添加。
在树中由深至浅添加点,如图 :
左 : 红色为新增的边。
中,右 : 可能的新增广路。不难发现,加入
不能像上一题那样暴力维护反向边了,考虑继续发掘性质。
咕咕。