可撤销贪心总结

这篇博客是因为模拟赛被吊打,于是写一下总结
普通贪心注重的只是当前情况下的最优,不考虑后面的状态
这样有时可以得到很高的分数,但大部分情况下并不理想
那么我们思考问题所在,我们在贪心出每一个所需量的时候
是不可撤销的,即使后面出现了更优的解,我们也无法返回到之前状态撤销
可是我们想撤销,怎么办呢
于是就有了可撤销的贪心
对于一些答案累加的题目,如果只针对当前状态的贪心不够优秀,我们可以将他存起来
有什么用呢?
举个例子,有 a b c a b c abc三点,你要选几个点保证权值最大,不可以连续选,你在只处理到 a b ab ab时贪心的选了 b b b,于是你选不了了 a c ac ac了,但是你发现,好像选 a c ac ac更优怎么办
没事,我们存一个东西,
因 为 选 b 影 响 的 权 值 − b 的 权 值 因为选b影响的权值 - b的权值 bb
为什么这么弄呢?假设我们这时选了 c c c,那么 a n s ans ans变成了什么呢?
a n s = b + 被 b 影 响 的 , 这 里 是 a − b + 新 权 值 , 这 里 为 c ans=b + 被b影响的,这里是a - b +新权值,这里为c ans=b+bab+c
也就是
a n s = a + c ans=a+c ans=a+c达到了选取更优权值的目的

例题

最经典的莫过于种树了
P1792 [国家集训队]种树
对于一个环上的所有树,我们要消去且总权值最小,这符合答案累加和互相影响的条件
我们可以使用撤销贪心,我们思考如何设计状态
我们按照公式
被 影 响 的 , 这 里 是 相 邻 的 两 棵 树 − b 的 权 值 , 这 里 是 被 选 的 树 权 值 被影响的,这里是相邻的两棵树 - b的权值,这里是被选的树权值 b
按照这个生成一个新的点,我们只要选了他就相当于撤销了一次
然后扔进小根堆维护就好

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<queue>
using namespace std;



const int maxn = 200007;
int n,k;

struct fuckccf
{
	int val,pos;
	friend bool operator <(const fuckccf &a,const fuckccf &b)
	{
		return a.val<b.val;
	}
}tr[maxn];

struct listt
{
	int front,next,val;
	
}list[maxn*2];

priority_queue< fuckccf >q;
int tot;
int main()
{
	scanf("%d%d",&n,&k);
	if(k>n/2)
	{
		cout<<"Error!";
		return 0;
	}
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&list[i].val);
		list[i].front=i-1;
		list[i].next=i+1;
		q.push((fuckccf){list[i].val,i});
	}
	list[1].front=n;
	list[n].next=1;
	int ans=0;
	int cnt=0;
	int cut[maxn]={0};
	while(cnt<k)
	{
		fuckccf f1=q.top();
		q.pop();
		if(cut[f1.pos])continue;
		cut[list[f1.pos].front]=cut[list[f1.pos].next]=1;
		ans+=f1.val;
		list[f1.pos].val=list[list[f1.pos].front].val+list[list[f1.pos].next].val-list[f1.pos].val;
		q.push( (fuckccf){ list[f1.pos].val,f1.pos } );
		list[f1.pos].front=list[list[f1.pos].front].front;
		list[f1.pos].next=list[list[f1.pos].next].next;
		list[list[f1.pos].next].front=f1.pos;
		list[list[f1.pos].front].next=f1.pos;
		
		cnt++;
	}
	cout<<ans;
}

第二个就是我们今天模拟赛的题目,好像DP也能写
但是我很棍的写了一个正常的贪心,成绩也非常的不理想
P3049 [USACO12MAR]园林绿化Landscaping
这里主要是多了一个直接加减土的操作,不然和重排草场以及糖果传递一模一样…
我的贪心思路很简单,对于每一种土,预处理出和他类型相同或不同的下一块土地的位置
然后判断,如果运过去比直接扔和买优,就运,不然就扔或买了。

错的很明显…我考场还真就觉得是对了,可能是小瞧了这题…
为什么错呢?因为距离最近不代表运土的优先级最高,所以不可以那么写
但是如果枚举每一块的话就滚回 O ( n 2 ) O(n^2) O(n2)了,死的更惨
我们考虑可撤销的贪心
分为两种情况,和别的土地有交互的和没有的。
我们开两个队列,一个维护少土的反悔点,另一个维护多土的反悔点
那么反悔点怎么设立呢?
注意这题的移动权值定义,是绝对值,具有线性传递性
∣ p o s i − p o s j ∣ ∗ Z |pos_i-pos_j |*Z posiposjZ
在保证 p o s i pos_i posi大于 p o s j pos_j posj的情况下实际上就是
p o s i ∗ Z − p o s j ∗ Z pos_i*Z - pos_j*Z posiZposjZ
i i i是当前处理点,能直接得到,所以我们只需要维护 p o s j ∗ Z pos_j*Z posjZ就好了!
有这么简单吗?这样我们怎么知道会不会直接得到土更优呢?
我们考虑最初的贪心式
( p o s i − p o s j ) ∗ Z < = X + Y (pos_i-pos_j)*Z<=X+Y (posiposj)Z<=X+Y
稍微移一下项
p o s i ∗ Z − ( p o s j ∗ Z + X / Y ) < = Y / X pos_i*Z -(pos_j*Z + X/Y) <=Y/X posiZ(posjZ+X/Y)<=Y/X
XY取决于你是缺的还是多的
然后就很明显了,维护 p o s j ∗ Z + X / Y pos_j*Z+X/Y posjZ+X/Y
那么分两种情况,通过判断上式的成立与否来决定是哪种

1.直接得到的

根据上面,维护 X / Y + p o s i X/Y+pos_i X/Y+posi即可,可能有人会说不符合公式,因为对于每一块不同的土地,新权值都不同,实际上新答案为 p o s 新 ∗ Z − ( p o s i ∗ Z + X / Y ) pos_新*Z-(pos_i*Z+X/Y) posZ(posiZ+X/Y),仍然可达到减去过去解效果,这种情况没有用到反悔点

2.和别的土地交互得来的

这个实际上就是向后面拿,填土,是撤销的核心,也就是用到了反悔点
式子是这样的,该点的实时权值为
p o s 新 ∗ Z − 反 悔 点 值 pos_新*Z-反悔点值 posZ
因为要达到反悔的目的,保证 p o s 后 − 反 悔 点 pos_后-反悔点 pos的值为新值
我们令该点的反悔值为
p o s i ∗ Z + p o s i ∗ Z − 运 用 的 反 悔 点 值 pos_i*Z + pos_i*Z - 运用的反悔点值 posiZ+posiZ
发现了吗,后面实际上就是该点的新权值,减一下,我们就达到了反悔的目的
还不理解看代码注释吧

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 1e5+7;
typedef long long ll;
priority_queue<ll>q1;//  维护每一个多土的位置 
priority_queue<ll>q2;//  维护每一个少土的位置 
int n;
ll X,Y,Z;
ll ans;

int main(){
	scanf("%d%lld%lld%lld",&n,&X,&Y,&Z);
	for(int i=1;i<=n;i++){
		ll a,b;
		scanf("%lld%lld",&a,&b);
		if(a>b){
			for(int j=1;j<=a-b;j++){   // 对于每一块土都需要贪心 
				if(q1.empty()||i*Z-q1.top()>=Y){ // 发现没有前面没有少的土坑,或者发现前面的土坑不如直接卖 
					ans+=Y;q2.push(i*Z+Y);       // 扔进多土的队列里,这时改点的权值是 Y ,所以队列里的是 i*Z + Y 
				}
				else{
					int mid=i*Z-q1.top();q1.pop();// 发现可以换土 ,那么我们直接得出新的答案 
					ans+=mid;                     // 此时这个多的土的实时权值为mid,所以把i*Z + mid放进队列 
					q2.push(i*Z+mid);                   
				}
			}
		}
		else{                          // 与上同理 
			for(int j=1;j<=b-a;j++){
				if(q2.empty()||i*Z-q2.top()>=X){
					ans+=X;q1.push(i*Z+X);
				}
				else{
					int mid=i*Z-q2.top();q2.pop();
					ans+=mid;
					q1.push(i*Z+mid);
				}
			}
		}
	}
	printf("%lld",ans);
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值