这篇博客是因为模拟赛被吊打,于是写一下总结
普通贪心注重的只是当前情况下的最优,不考虑后面的状态
这样有时可以得到很高的分数,但大部分情况下并不理想
那么我们思考问题所在,我们在贪心出每一个所需量的时候
是不可撤销的,即使后面出现了更优的解,我们也无法返回到之前状态撤销
可是我们想撤销,怎么办呢
于是就有了可撤销的贪心
对于一些答案累加的题目,如果只针对当前状态的贪心不够优秀,我们可以将他存起来
有什么用呢?
举个例子,有
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的权值
因为选b影响的权值−b的权值
为什么这么弄呢?假设我们这时选了
c
c
c,那么
a
n
s
ans
ans变成了什么呢?
a
n
s
=
b
+
被
b
影
响
的
,
这
里
是
a
−
b
+
新
权
值
,
这
里
为
c
ans=b + 被b影响的,这里是a - b +新权值,这里为c
ans=b+被b影响的,这里是a−b+新权值,这里为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
∣posi−posj∣∗Z
在保证
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
posi∗Z−posj∗Z
i
i
i是当前处理点,能直接得到,所以我们只需要维护
p
o
s
j
∗
Z
pos_j*Z
posj∗Z就好了!
有这么简单吗?这样我们怎么知道会不会直接得到土更优呢?
我们考虑最初的贪心式
(
p
o
s
i
−
p
o
s
j
)
∗
Z
<
=
X
+
Y
(pos_i-pos_j)*Z<=X+Y
(posi−posj)∗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
posi∗Z−(posj∗Z+X/Y)<=Y/X
XY取决于你是缺的还是多的
然后就很明显了,维护
p
o
s
j
∗
Z
+
X
/
Y
pos_j*Z+X/Y
posj∗Z+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) pos新∗Z−(posi∗Z+X/Y),仍然可达到减去过去解效果,这种情况没有用到反悔点
2.和别的土地交互得来的
这个实际上就是向后面拿,填土,是撤销的核心,也就是用到了反悔点
式子是这样的,该点的实时权值为
p
o
s
新
∗
Z
−
反
悔
点
值
pos_新*Z-反悔点值
pos新∗Z−反悔点值
因为要达到反悔的目的,保证
p
o
s
后
−
反
悔
点
pos_后-反悔点
pos后−反悔点的值为新值
我们令该点的反悔值为
p
o
s
i
∗
Z
+
p
o
s
i
∗
Z
−
运
用
的
反
悔
点
值
pos_i*Z + pos_i*Z - 运用的反悔点值
posi∗Z+posi∗Z−运用的反悔点值
发现了吗,后面实际上就是该点的新权值,减一下,我们就达到了反悔的目的
还不理解看代码注释吧
#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);
}