反悔贪心 & 模拟费用流
反悔贪心 & 模拟费用流
参考资料来源 cyt
前言
很多找到一种可行的方案,匹配(选择)某些东西,使价值最优化
的问题可以建出费用流模型。
但是直接跑费用流的复杂度是不对的。
我们又想到可以用简单的贪心思路解决这些问题,然而一般的贪心都假掉了。
于是我们考虑模拟费用流的退流操作来做贪心,这就是反悔贪心,其实也是在模拟费用流的各种操作。
所以说反悔贪心(可撤销贪心)和模拟费用流是一个东西。
不过对于一道题目,我们可能会从费用流建模的角度入手。也可能从贪心的角度入手,在它之上加可撤销操作。这大概就是不同点,但最后的代码都是一样的。
反悔堆
用时一定
用时为 \(1\),使价值最大。
P2949 [USACO09OPEN] Work Scheduling G
按截止时间排序。
接着对于一个工作,若截止时间内还有空,则直接选。
否则我们用一个堆,堆里面放选了的工作,按价值从小到大。
把价值最小的找到,看是否比当前工作的价值更劣,是则用当前工作将它替换。
价值一定
价值为 \(1\),使工作最多。
还是按截止时间排序。
还是用堆,把用时最长的找到,看用当前工作替换是否更优。
练习
显然先按照 \(x_i\) 排序。
相当于枚举一个走到的位置 \(x_i\),然后在这个位置之前的机房选要最多个使 $x_i+\sum k\le m $。
注意到 \(x_i\) 选的机房会继承 \(x_{i-1}\) 的。
则每次先把 \(i\) 选了,让 \(sum\) 加 \(f_i\)。
看 \(sum+x_i\) 是否不超过 \(m\)。
否则,拿个堆,一直把最大的 \(f\) 退掉。
注意是一直退掉,而不是用 \(f_i\) 与堆顶做比较,这是因为堆中存的是走到 \(x_{i-1}\) 这个位置的答案,而不是走到 \(x_i\) 的答案。
若最后 \(sum+x_i\) 不超过 \(m\),则此时选的个数即为走到 \(x_i\) 的最大答案。
对所有可行的答案取最大值即可。
反悔自动机
堆 反悔自动机
CF865D(简易老鼠进洞模型)
考虑在卖出股票时同时结算买和卖。
每一天都买,我们把它插入堆里,插入堆无代价。
每天再看能否卖,若把堆顶现在卖出有收益,则卖出。
但有问题:
我们用当前 \(p_i\) 的价格卖出堆顶的 \(x\) 元买进的股票,收益是 \(p_i-x\)。
但有可能后面有 \(j>i\) 使得 \(p_j-x\) 更优,然而 \(x\) 这个股票已经卖掉了。
解决方案就是,我们用 \(p_i\) 卖掉 \(x\) 的股票时,往堆里再插入一个 \(p_i\),注意这里的 \(p_i\) 和每天买进的股票不同。
这样若后面有 \(p_j\) 更优,则 \(p_j\) 就会匹配上 \(p_i\),此时对答案的贡献和即为
就变成了用 \(p_j\) 匹配 \(x\),自动变成了最优方案。
双向链表+堆 反悔自动机
贪心思路:每次选最大的价值,选 \(k\) 次。
然而选了 \(i\) 后 \(i-1\) 和 \(i+1\) 就不能选了,此时可能会出现选 \(i-1\) 和 \(i+1\) 更优的情况。
发现这种情况只能是 \(i-1\) 和 \(i+1\) 同时选,这样才会比 \(i\) 大。
于是考虑选了堆顶 \(a_i\) 后,把 \(i-1,i,i+1\) 这三个点缩成一个价值为 \(a_{i-1}+a_{i+1}-a_i\) 的点,将它插入堆里。
这样下次选到这个点,就相当于选 \(i-1,i+1\),同时把原来选的 \(i\) 退掉,代价和为
拿一个双向链表维护当前的前驱和后继即可。
老鼠进洞模型
有 \(n\) 个老鼠,\(m\) 个洞,老鼠有坐标 \(x_i\),洞有坐标 \(y_i\),每只老鼠都要进洞。
第 \(i\) 个洞每进一只老鼠花费 \(w_i\) ,最多进 \(c_i\) 只老鼠。
老鼠可以左右移动,花费移动距离的代价。
要求最小化花费。
两个堆 基础模型
考虑洞没有权值,容量 \(c_i\) 都为 \(1\) 时应该怎么做。
先把老鼠和洞放在一起按坐标排序。
为了去掉绝对值的影响,我们把洞匹配老鼠和老鼠匹配洞分开考虑(即让右边匹配左边)。
设 \(i\) 老鼠匹配洞 \(j\),若 \(i<j\),则贡献为 \(y_j+(-x_i)\),反之为 \(x_i+(-y_j)\)。
用反悔贪心的思路,维护一个洞堆和一个老鼠堆,从左往右扫一遍。
如果当前扫到一个老鼠
那么取出洞堆的堆顶让它和这个洞匹配(初始时负无穷处有足够多个洞),设堆顶为 \(v\),对答案的贡献为 \(x_i+v\)。
为了让右边没扫到的洞使这个老鼠能反悔,让它匹配右边一个新的洞。
那么如果它要匹配右边一个新的洞 \(j\),那么代价的增量为 \((y_j-x_i)-(x_i+v)=y_j+(-2x_i-v)\),于是往老鼠堆插入 \(-2x_i-v\)。
如果扫到一个洞
考虑有没有老鼠要反悔来这个洞,从老鼠堆中取出堆顶 \(u\),若 \(y_i+u<0\) 则更优,让老鼠反悔,加上贡献。
若这个洞原本应当让后面一个老鼠 \(j\) 占,于是让这个洞反悔,往洞堆中插入 \(-2y_i-u\)。
贡献和即 \((y_i+u)+(x_j+(-2y_i-u))=x_j-y_i\),是正确的。
若没有老鼠能反悔来这个洞,则直接插入 \(-y_i\)。
就像这样(\(A\) 为老鼠,\(B\) 为洞):
初始时 \(A_1\) 匹配 \(B_1\)。
然后 \(A_1\) 反悔,让 \(B_2\) 匹配 \(A_1\)。
后来 \(A_2\) 才应该匹配 \(B_2\),于是 \(A_1\) 又重新匹配 \(B_1\)。
当然在权值上没有谁匹配谁的关系,我们只需使最后的权值和正确即可。
洞有权值
如果在基础模型的基础上,洞有权值 \(w\) 该怎么做。
区别在于一个老鼠可能会匹配较远的洞。
老鼠匹配洞时有堆最优化,无影响。
而当洞匹配老鼠就有影响了。
首先是贡献为 \(y_i+w_i+u\)。
考虑到我们让洞 \(i\) 匹配老鼠 \(j\) 时,后面可能还有洞 \(k\) 与老鼠 \(j\) 匹配更优。
所以,设 \(u\) 为老鼠 \(j\) 在堆中的权值,则我们用 \(k\) 再匹配 \(j\) 的增量为,\((y_k+w_k+u)-(y_i+w_i+u)=(y_k+w_k)+(-y_i-w_i)\),所以用 \(i\) 匹配完 \(j\) 后,要往老鼠堆中插入 \(-y_i-w_i\),让洞 \(k\) 与这个虚拟老鼠匹配。
由于新增的 \(w\) 的影响,扫到一个洞后,有老鼠来时插入到洞堆的权值不变,无反悔时插入的权值变为 \(-y_i+w_i\)。
这时你可能会有疑问:
如果洞 \(i\) 在洞堆和老鼠堆插入的两个权值都从堆顶取出了怎么办。
那它应该要长成这样:
其中 \(B_2\) 就是被取出两次的洞,它开始匹配 \(A_1\),后来 \(B_3\) 替换了它,取了一次老鼠堆顶。
然后 \(A_2\) 以为 \(B_2\) 还连着 \(A_1\),取了一次洞堆顶,它以为它替换掉了 \(B_2\) 连着的 \(A_1\),然而 \(B_2\) 实际并没有连着 \(A_1\),那么 \(A_2\) 连 \(B_2\) 时加的权值就不对了。
我们观察一下, 发现这种情况时不可能出现的,因为对于 \(i<j<k<l\),其中 \(i,l\) 为老鼠,\(j,k\) 为洞,\(i\) 连 \(k\) 并且 \(j\) 连 \(l\) 一定不优,因为 \(i\) 连 \(j\) 并且 \(k\) 连 \(l\) 比这更优。
那么对于上面的例子,最后一行会变成 \(B_1,A_1\leftarrow B_2,B_3\leftarrow A_2\) 或 \(B_1,A_1,B_2,B_3,A_2(B_1\leftarrow A_2,A_1\leftarrow B_3)\)。
所以我们不用担心取两次的情况。
洞有权值,也有容量
如果直接拆点,复杂度是 \(O(n+\sum c\log(n+\sum c))\) 不能通过。
然而我们可以往堆中插入 pair
,多存一位个数。
具体来说,我们扫到一个老鼠匹配洞时,把那个洞的容量减一。
然后扫到洞时,我们记录有多少个老鼠来这个洞,设其为 \(cnt\),因为注意到往老鼠堆插入的权值都是 \(-y_i-w_i\),所以一次性插入 \(cnt\) 个这样的老鼠即可。
往洞堆中插入的就无法一次性插了。
如果 \(cnt<c_j\) 则对应无老鼠来这个洞的情况,往洞堆插入 \(c_j-cnt\) 个 \(-y_i+w_i\) 即可。
注意我们并不需要撤销某只老鼠先前选的洞的容量,因为这是可撤销贪心,不用管它。
至于时间复杂度,一只老鼠只会从往左走转变为往右走至多一次,我们扫到洞时批量插入老鼠也保证了时间复杂度。
最后的复杂度就是一只 \(\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 二分可以看这篇博客。
首先设 \(f(k)\) 为恰好选 \(k\) 张光盘的答案,合理猜测 \(f(k)\) 是一个凸函数,一般可以选择用暴力打表观察验证猜测,但这题是容易证明的。
知道它是一个凸函数后,使用 wqs 二分解决它。
二分斜率后,\(b=f(x)-kx\),因为是下凸函数,我们要让 \(b\) 最小,\(b\) 的意义刚好就是没有限制个数且每张光盘减少 \(k\) 花费时的答案。
考虑没有限制个数怎么做,用反悔贪心(也可以说是模拟费用流)。
考虑一张光盘从 B 工厂出来时结算它此前的所有代价。
从左往右扫一遍,我们搞一个堆存储所有 A 工厂的花费,然后对于 B 工厂,我们从堆里找一个 A 和它匹配,如果答案会变得更小那么就匹配。
但是可能有后面一个 B 匹配这个 A 更优的情况,于是我们用一个 \(b_i\) 匹配完一个 A 后,往 A 的堆里再插入 \(-b_i\) 即可,这样后面一个 B 替换掉它以后得代价和就为:
我们开始要减掉 \(k\) 的花费,我们可以选择全部在 \(a\) 减或全部在 \(b\) 减。