反悔贪心 & 模拟费用流

反悔贪心 & 模拟费用流

参考资料来源 cyt

前言

很多找到一种可行的方案,匹配(选择)某些东西,使价值最优化的问题可以建出费用流模型。

但是直接跑费用流的复杂度是不对的。

我们又想到可以用简单的贪心思路解决这些问题,然而一般的贪心都假掉了。

于是我们考虑模拟费用流的退流操作来做贪心,这就是反悔贪心,其实也是在模拟费用流的各种操作。

所以说反悔贪心(可撤销贪心)和模拟费用流是一个东西。

不过对于一道题目,我们可能会从费用流建模的角度入手。也可能从贪心的角度入手,在它之上加可撤销操作。这大概就是不同点,但最后的代码都是一样的。


反悔堆

用时一定

用时为 1,使价值最大。

P2949 [USACO09OPEN] Work Scheduling G

按截止时间排序。

接着对于一个工作,若截止时间内还有空,则直接选。

否则我们用一个堆,堆里面放选了的工作,按价值从小到大。

把价值最小的找到,看是否比当前工作的价值更劣,是则用当前工作将它替换。

价值一定

价值为 1,使工作最多。

P4053 [JSOI2007] 建筑抢修

还是按截止时间排序。

还是用堆,把用时最长的找到,看用当前工作替换是否更优。

练习

P2107 小Z的AK计划

显然先按照 xi 排序。

相当于枚举一个走到的位置 xi,然后在这个位置之前的机房选要最多个使 xi+km

注意到 xi 选的机房会继承 xi1 的。

则每次先把 i 选了,让 sumfi

sum+xi 是否不超过 m

否则,拿个堆,一直把最大的 f 退掉。

注意是一直退掉,而不是用 fi 与堆顶做比较,这是因为堆中存的是走到 xi1 这个位置的答案,而不是走到 xi 的答案。

若最后 sum+xi 不超过 m,则此时选的个数即为走到 xi 的最大答案。

对所有可行的答案取最大值即可。

反悔自动机

堆 反悔自动机

CF865D(简易老鼠进洞模型)

考虑在卖出股票时同时结算买和卖。

每一天都买,我们把它插入堆里,插入堆无代价。

每天再看能否卖,若把堆顶现在卖出有收益,则卖出。

但有问题:

我们用当前 pi 的价格卖出堆顶的 x 元买进的股票,收益是 pix

但有可能后面有 j>i 使得 pjx 更优,然而 x 这个股票已经卖掉了。

解决方案就是,我们用 pi 卖掉 x 的股票时,往堆里再插入一个 pi,注意这里的 pi 和每天买进的股票不同。

这样若后面有 pj 更优,则 pj 就会匹配上 pi,此时对答案的贡献和即为

(xpi)+(pipj)=xpj

就变成了用 pj 匹配 x,自动变成了最优方案。

双向链表+堆 反悔自动机

P1484 种树

贪心思路:每次选最大的价值,选 k 次。

然而选了 ii1i+1 就不能选了,此时可能会出现选 i1i+1 更优的情况。

发现这种情况只能是 i1i+1 同时选,这样才会比 i 大。

于是考虑选了堆顶 ai 后,把 i1,i,i+1 这三个点缩成一个价值为 ai1+ai+1ai 的点,将它插入堆里。

这样下次选到这个点,就相当于选 i1,i+1,同时把原来选的 i 退掉,代价和为

ai+(ai1+ai+1ai)=ai1+ai+1

拿一个双向链表维护当前的前驱和后继即可。

老鼠进洞模型

UOJ 455 雪灾与外卖

n 个老鼠,m 个洞,老鼠有坐标 xi,洞有坐标 yi,每只老鼠都要进洞。

i 个洞每进一只老鼠花费 wi ,最多进 ci 只老鼠。

老鼠可以左右移动,花费移动距离的代价。

要求最小化花费。

两个堆 基础模型

考虑洞没有权值,容量 ci 都为 1 时应该怎么做。

先把老鼠和洞放在一起按坐标排序。

为了去掉绝对值的影响,我们把洞匹配老鼠和老鼠匹配洞分开考虑(即让右边匹配左边)。

i 老鼠匹配洞 j,若 i<j,则贡献为 yj+(xi),反之为 xi+(yj)

用反悔贪心的思路,维护一个洞堆和一个老鼠堆,从左往右扫一遍。

如果当前扫到一个老鼠

那么取出洞堆的堆顶让它和这个洞匹配(初始时负无穷处有足够多个洞),设堆顶为 v,对答案的贡献为 xi+v

为了让右边没扫到的洞使这个老鼠能反悔,让它匹配右边一个新的洞。

那么如果它要匹配右边一个新的洞 j,那么代价的增量为 (yjxi)(xi+v)=yj+(2xiv),于是往老鼠堆插入 2xiv

如果扫到一个洞

考虑有没有老鼠要反悔来这个洞,从老鼠堆中取出堆顶 u,若 yi+u<0 则更优,让老鼠反悔,加上贡献。

若这个洞原本应当让后面一个老鼠 j 占,于是让这个洞反悔,往洞堆中插入 2yiu

贡献和即 (yi+u)+(xj+(2yiu))=xjyi,是正确的。

若没有老鼠能反悔来这个洞,则直接插入 yi

就像这样(A 为老鼠,B 为洞):

B1A1B1,A1B2B1A1,B2A2

初始时 A1 匹配 B1

然后 A1 反悔,让 B2 匹配 A1

后来 A2 才应该匹配 B2,于是 A1 又重新匹配 B1

当然在权值上没有谁匹配谁的关系,我们只需使最后的权值和正确即可。

洞有权值

如果在基础模型的基础上,洞有权值 w 该怎么做。

区别在于一个老鼠可能会匹配较远的洞。

老鼠匹配洞时有堆最优化,无影响。

而当洞匹配老鼠就有影响了。

首先是贡献为 yi+wi+u

考虑到我们让洞 i 匹配老鼠 j 时,后面可能还有洞 k 与老鼠 j 匹配更优。

所以,设 u 为老鼠 j 在堆中的权值,则我们用 k 再匹配 j 的增量为,(yk+wk+u)(yi+wi+u)=(yk+wk)+(yiwi),所以用 i 匹配完 j 后,要往老鼠堆中插入 yiwi,让洞 k 与这个虚拟老鼠匹配。

由于新增的 w 的影响,扫到一个洞后,有老鼠来时插入到洞堆的权值不变,无反悔时插入的权值变为 yi+wi

这时你可能会有疑问:

如果洞 i 在洞堆和老鼠堆插入的两个权值都从堆顶取出了怎么办。

那它应该要长成这样:

B1A1B1,A1B2B1,A1,B2,B3(A1B3)B1,A1,B2,B3,A2(A1B3,B2A2)

其中 B2 就是被取出两次的洞,它开始匹配 A1,后来 B3 替换了它,取了一次老鼠堆顶。

然后 A2 以为 B2 还连着 A1,取了一次洞堆顶,它以为它替换掉了 B2 连着的 A1,然而 B2 实际并没有连着 A1,那么 A2B2 时加的权值就不对了。

我们观察一下, 发现这种情况时不可能出现的,因为对于 i<j<k<l,其中 i,l 为老鼠,j,k 为洞,ik 并且 jl 一定不优,因为 ij 并且 kl 比这更优。

那么对于上面的例子,最后一行会变成 B1,A1B2,B3A2B1,A1,B2,B3,A2(B1A2,A1B3)

所以我们不用担心取两次的情况。

洞有权值,也有容量

如果直接拆点,复杂度是 O(n+clog(n+c)) 不能通过。

然而我们可以往堆中插入 pair,多存一位个数。

具体来说,我们扫到一个老鼠匹配洞时,把那个洞的容量减一。

然后扫到洞时,我们记录有多少个老鼠来这个洞,设其为 cnt,因为注意到往老鼠堆插入的权值都是 yiwi,所以一次性插入 cnt 个这样的老鼠即可。

往洞堆中插入的就无法一次性插了。

如果 cnt<cj 则对应无老鼠来这个洞的情况,往洞堆插入 cjcntyi+wi 即可。

注意我们并不需要撤销某只老鼠先前选的洞的容量,因为这是可撤销贪心,不用管它。

至于时间复杂度,一只老鼠只会从往左走转变为往右走至多一次,我们扫到洞时批量插入老鼠也保证了时间复杂度。

最后的复杂度就是一只 log 的。

详见代码

提交记录

int n,m;
const int N=1e5+5;
int X[N],Y[N],w[N],c[N];
ll ans;
priority_queue<pair<ll,int>,vector<pair<ll,int>>,greater<>> a,b;
signed main(){
usefile("snow");
read(n),read(m);
ll csum=0;
fo(i,1,n)read(X[i]);
fo(i,1,m)read(Y[i]),read(w[i]),read(c[i]),csum+=c[i];
if(csum<n){
write(-1);
return 0;
}
int i=1,j=1;
fo(i,1,n)b.push({1e12,1});
while(i<=n||j<=m){
if(i<=n&&(j>m||X[i]<=Y[j])){
auto x=b.top();
ans+=X[i]+x.first;
a.push({-2ll*X[i]-x.first,1});
b.pop();
if(x.second>1)b.push({x.first,x.second-1});
++i;
}
else {
int cnt=0;
while(cnt<c[j]&&a.size()&&a.top().first+Y[j]+w[j]<0){
auto x=a.top();
int ca=min(c[j]-cnt,x.second);
ans+=(ll)ca*(x.first+Y[j]+w[j]);
a.pop();
if(ca!=x.second)a.push({x.first,x.second-ca});
b.push({-2ll*Y[j]-x.first,ca});
cnt+=ca;
}
if(cnt<c[j])b.push({-Y[j]+w[j],c[j]-cnt});
if(cnt) a.push({-Y[j]-w[j],cnt});
++j;
}
}
write(ans);
return 0;
}

wqs 二分优化

费用流的费用是流量的凸函数,我们可以利用 wqs二分 优化反悔贪心。

不会 wqs 二分可以看这篇博客

PA2013 Raper

首先设 f(k) 为恰好选 k 张光盘的答案,合理猜测 f(k) 是一个凸函数,一般可以选择用暴力打表观察验证猜测,但这题是容易证明的。

知道它是一个凸函数后,使用 wqs 二分解决它。

二分斜率后,b=f(x)kx,因为是下凸函数,我们要让 b 最小,b 的意义刚好就是没有限制个数且每张光盘减少 k 花费时的答案。

考虑没有限制个数怎么做,用反悔贪心(也可以说是模拟费用流)。

考虑一张光盘从 B 工厂出来时结算它此前的所有代价。

从左往右扫一遍,我们搞一个堆存储所有 A 工厂的花费,然后对于 B 工厂,我们从堆里找一个 A 和它匹配,如果答案会变得更小那么就匹配。

但是可能有后面一个 B 匹配这个 A 更优的情况,于是我们用一个 bi 匹配完一个 A 后,往 A 的堆里再插入 bi 即可,这样后面一个 B 替换掉它以后得代价和就为:

(bi+aj)+(bk+(bi))=bk+aj

我们开始要减掉 k 的花费,我们可以选择全部在 a 减或全部在 b 减。

posted @   dengchengyu  阅读(32)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示