反悔贪心 & 模拟费用流

反悔贪心 & 模拟费用流

参考资料来源 cyt

前言

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

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

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

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

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

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


反悔堆

用时一定

用时为 \(1\),使价值最大。

P2949 [USACO09OPEN] Work Scheduling G

按截止时间排序。

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

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

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

价值一定

价值为 \(1\),使工作最多。

P4053 [JSOI2007] 建筑抢修

还是按截止时间排序。

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

练习

P2107 小Z的AK计划

显然先按照 \(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\),此时对答案的贡献和即为

\[(x-p_i)+(p_i-p_j)=x-p_j \]

就变成了用 \(p_j\) 匹配 \(x\),自动变成了最优方案。

双向链表+堆 反悔自动机

P1484 种树

贪心思路:每次选最大的价值,选 \(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\) 退掉,代价和为

\[a_i+(a_{i-1}+a_{i+1}-a_i)=a_{i-1}+a_{i+1} \]

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

老鼠进洞模型

UOJ 455 雪灾与外卖

\(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\) 为洞):

\[B_1\leftarrow A_1\\ \downarrow\\ B_1,A_1\leftarrow B_2\\ \downarrow\\ B_1\leftarrow A_1,B_2\leftarrow A_2 \]

初始时 \(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_1\leftarrow A_1\\ \downarrow\\ B_1,A_1\leftarrow B_2\\ \downarrow\\ B_1,A_1,B_2,B_3(A_1\leftarrow B_3)\\ \downarrow\\ B_1,A_1,B_2,B_3,A_2(A_1\leftarrow B_3,B_2\leftarrow A_2) \]

其中 \(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 二分可以看这篇博客

PA2013 Raper

首先设 \(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 替换掉它以后得代价和就为:

\[(b_i+a_j)+(b_k+(-b_i))=b_k+a_j \]

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

posted @ 2024-08-16 16:33  dengchengyu  阅读(15)  评论(0编辑  收藏  举报