【算法学习】反悔贪心
前言
反悔贪心,先选择局部最优,当不断向后更新时发现前面的决策在现在来看并不优就进行调整过去决策进行调整到全局最优,可以说反悔贪心一步步扩宽自己的视野从而明白过去的错误。、
如果动态规划是将各种削苹果的方式都展示出来的话,那反悔贪心就是削一下补回来一点再削。
当然反悔贪心的题是可以用动态规划做的,但是在有些题数据范围很大或不方便转移时就可以考虑反悔贪心。
反悔堆
用堆维护最差的决策,之后有更优的决策可以快速掉替换这个决策。
P2949 Work Scheduling G
因为有个截至时间,所以我们贪心地想截至时间越晚的我们越靠后考虑,当然我们也想让价值更大,所以我们按照截至时间从小到大排序,其次按照权值排序。
然后我们用小根堆维护选择的数,如果我们新加的数的截至时间为堆内元素个数,那么我们就要看看能不能删掉数,我们查看堆顶元素是否小于新加的元素,如果小于我们就删掉堆顶让当前元素入堆,我们不断地进行贪心与策略调整。
远古时期的代码,和我讲的有一点点点区别。
#include <bits/stdc++.h>
using namespace std;
int n;
long long ans=0;
struct ss{
int t,p;
}a[100005];
bool cmp(ss g,ss h){
return g.t<h.t;
}
priority_queue<int,vector<int>,greater<int> >q;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&a[i].t,&a[i].p);
}
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++){
if(a[i].t<=q.size()){
if(q.top()<a[i].p){
ans-=q.top();
q.pop();
q.push(a[i].p);
ans+=a[i].p;
}
}
else{
ans+=a[i].p;
q.push(a[i].p);
}
}
cout<<ans;
return 0;
}
P4053 [JSOI2007] 建筑抢修
通过不断调整使得花费总时间尽量少,改一改上面那题的条件与不断调整 \(sum\) 值。
反悔自动机
通过特殊的策略使得总是维护最优策略,通常应用到有特殊限制的题目上,例如 \(A\) 和 \(B\) 两者选 \(1\),\(C\) 和 \(D\) 两者选 \(1\),假设我们已选 \(A,C\),我们想选 \(B\) 的话就要加上 \(B-A\) 使得自动反悔 \(A\) 从而维护最优策略。
Buy Low Sell High
反悔堆自动机。
我们考虑我们答案形式并进行转变为 \(C_{sell}-C_{buy}\) 我们通过售价减购价的形式,我们考虑引入一个反悔物使得我们每当销售例如 \(C_i-C_j\) 就会产生这个反悔物 \(val\),当我们之后选择这个反悔物时就可以更新为新的获利 \(C_i-C_k\),于是我们有以下方程 \(C_i-C_j+C_k-val=C_i-C_k\),最后可知 \(C_j=val\),所以我们用小根堆维护最小的数,如果最小的数一个新的数那也就是产生新的组合,总的加多少减多少是不变的,反正不亏,如果最小的数是反悔物那么我们也进行反悔策略,调整变得更大,反正不亏。
#include <bits/stdc++.h>
#define int long long
#define re register
using namespace std;
int n;
int ans;
priority_queue<int,vector<int>,greater<int> > q;
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n;
for(int i=1;i<=n;i++){
int x;
cin>>x;
if(!q.empty()&&q.top()<x){
ans+=x-q.top();
q.pop();
q.push(x);
}
q.push(x);
}
cout<<ans;
return 0;
}
P1484 种树
双端链表反悔堆自动机。
我们想选上这个数之后是与左右是互斥的,所以我们选上这个数时将左右数标记,加入一个反悔物,那反悔物就是我选左右而不选这个的策略,即 \(a_l+a_r-a_i\),并且一直用大根堆维护拿出最大的值。
#include <bits/stdc++.h>
#define int long long
#define ls p<<1
#define rs p<<1|1
#define re register
const int N=3e5+10;
using namespace std;
int n,m;
struct ss{
int val,l,r;
}a[N];
struct node{
int val,id;
bool operator < (node it)const {
return val<it.val;
}
};
int vis[N];
priority_queue<node> q;
int ans;
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i].val;
a[i].l=i-1;
a[i].r=i+1;
q.push({a[i].val,i});
}
for(int i=1;i<=m;i++){
while(vis[q.top().id]){
q.pop();
}
node u=q.top();
int x=u.val;
int id=u.id;
q.pop();
if(x<=0){
break;
}
ans+=x;
vis[a[id].l]=1;
vis[a[id].r]=1;
a[id].val=a[a[id].l].val+a[a[id].r].val-x;
q.push({a[id].val,id});
a[id].l=a[a[id].l].l;
a[id].r=a[a[id].r].r;
a[a[id].l].r=id;
a[a[id].r].l=id;
}
cout<<ans;
return 0;
}
大佬的练习题单
板刷!!!!
P1792 [国家集训队] 种树
和上面那个种树一样,不过这题要把全部树都种上!!
P3620 [APIO/CTSC2007] 数据备份
可以想到这个数与前面的差是他们的权值,而且选了这个上一个元素和下一个元素都没法配对,所以又变成了上面那道题,不过要处理边界,而我不会处理,糟糕!!!
a[0].val=a[n].val=1e9;
就可以了。
P2107 小Z的AK计划
还是太菜了,我们不断更新,如果当前时间超了就将之前用时最多的 AK 变成 WA 即可,然后要在更新过程中记录最大值。
P4945 最后的战役
dp 思路非常简单,不说,直接想贪心怎么做,我们先忽略掉双倍的限制,这样每个点的最大权值是可以求出来的,我们如果选择用药相当于是失去 \(y_i\) 而多了 \(y_{i+1}\),这样我们就可以反悔自动机了,因为与左右有关所以要用反悔双向链表自动机,这样时间和空间都比 dp 优。
话说这个建模还是有点难的啊,对这个有影响也影响下一个考虑反悔贪心。
贪心正确性:我选这个点向后双倍了,上一个位置就不能选我了,下一个位置也不能向后双倍。
F - Vouchers
比较简单的题,但是我不会/(ㄒoㄒ)/但是理解了也不难,想要贪心必须要同一变量,我们按照物品体积从小到大排序,再按打折券从小到大排序,对于一个物品我们可以讲小于等于他的打折券全部放入大根堆,我们再从堆中取出最大优惠的卷。
这题虽然没有用到反悔贪心,但是我们理解了要先同一变量使得满足单调性。
P11268 【MX-S5-T2】买东西题
这题和上一题一模一样,只不过我们讲优惠价看作打折卷放进去即可,我们取出最大的时要么是打折卷要么是优惠价,你肯定会问如何之后的数取出我们的优惠价那可以吗,当然可以,我们就可以看作是最后的抢了我们这个已用的数的打折卷让这个数用了优惠价。