学习笔记 反悔贪心

0.写在前面

好久没更了,这周是开学第一周 A C M ACM ACM队临时安排讲课任务,没人讲,我就揽下来这活了。前两天有一道 c f cf cf的 d i v 2 C div2C div2C用到了反悔贪心这个技巧,也不需要什么前置算法就可以学,所以我第一时间想到的就是讲反悔贪心了,顺便更一下好久没更过的博客当备课了。

这个技巧的思维含量不是很高,大家可能或多或少在之前无意间用出来过。我第一次了解这个技巧是在去年暑假的时候,我的队友跟我说要给我讲一个他新学的小 t r i c k trick trick,给我看了一道题洛谷P1484 种树,我之前从来没了解过这个小技巧但是做过这个题。

1.随便讲讲

字面意思,反悔贪心就是可以反悔的贪心。贪心本身是没有反悔操作的,通过对一个问题应用特定的问题选出最优解。然而有时候我们发现贪心只能得到局部最优解,这时候可以进行反悔操作。为了实现反悔操作,我们要记录下前面操作的最大/最小值等信息,因此反悔贪心大多会使用一些数据结构进行实现。

毕竟这只是一个小技巧不是一个算法,只靠说大家可能不好理解,我们直接进入例题,就题分析。

2.例题

CF865D Buy Low Sell High

一个我觉得比较好解释反悔贪心思想的例题

题意:对于一支股票,你可以知道后面 N N N天这个股票的价格,每天可以在两种操作中选一种:买入一股该股票、卖出一股之前买入的股票。要求在最后一天手里不能有任何股票。求最后最多能赚多少钱。其中 N ≤ 3 × 1 0 5 N\le 3\times 10^5 N≤3×105。

对于买股票的操作,我们观察到题目要求最后一天手里不能有任何股票,所以我们只要没有卖出的操作,就直接进行买入操作,如果最后这支股票卖不出去,我们进行反悔,这股票我不买了退了,就当我那一天没买过。

对于卖股票的操作,我们什么时机卖呢,肯定是价格高于当前买入,但是如果卖亏了怎么办,我们选择反悔上次卖的,相当于上次不卖了退回来,这次再卖。

这就是反悔贪心的思想,具体怎么实现呢?

我们使用小根堆 q q q来存储买入的价格。

对于第 i i i天价格 a i a_i ai,如果该价格不超过当前小根堆堆顶的价格,那么我们无法进行卖出操作,只能进行买入操作,因此将 a i a_i ai入堆即可。此时入堆的 a i a_i ai的意义为:记录下当前的买入价格,为后续卖出操作准备。

如果该价格超过了当前小根堆堆顶的价格,即 a i > q . t o p a_i>q.top ai>q.top,意味着我们可以进行卖出操作,此时我们就将堆顶元素弹出,并将 a i a_i ai入堆,该操作堆答案的贡献为 a i − q . t o p a_i-q.top ai−q.top。此时入堆的 a i a_i ai的意义为:记录当前卖出的价格,为后续反悔操作准备。此时我们会发现,如果后续进行了反悔操作,那么就相当于这一天没有卖出,因此还要将这一天的价格再次入堆,此时入堆的 a i a_i ai的意义为:填补进行了反悔操作后这一天的空挡。

这样我们就通过用一个小根堆来维护了我们反悔贪心的思想。

代码很短↓

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll ans;
int n;
priority_queue<int,vector<int>,greater<int>> q;
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1,x;i<=n;i++){
        cin>>x;
        if(!q.empty()&&q.top()<x)
            ans+=x-q.top(),
            q.pop(),
            q.push(x);
        q.push(x);
    }
    cout<<ans<<'\n';
}
洛谷P1484 种树

一道非常经典的反悔贪心题

题意:给定一个长度为 n ( n ≤ 3 × 1 0 5 ) n(n\le3\times 10^5) n(n≤3×105)的序列 a a a,第 i i i个数为 a i ( − 1 0 6 ≤ a i ≤ 1 0 6 ) a_i(-10^6\le a_i\le 10^6) ai(−106≤ai≤106),求在该数列中选出不超过 k ( 0 ≤ k ≤ ⌊ n 2 ⌋ ) k(0\le k\le\lfloor\frac{n}{2}\rfloor) k(0≤k≤⌊2n⌋)个数的和最大。限制条件为选了第 i i i个位置的 a i a_i ai就不能选该位置两侧的 a i − 1 a_{i-1} ai−1和 a i + 1 a_{i+1} ai+1。

对问题进行简化:取消限制条件,即在 n n n个数中选出不超过 k k k个数的和最大。

利用贪心的思想,从最大的数开始选,直到选满 k k k个数或选到负数为止。

现在回到这个问题,如果继续利用之前的贪心的思想,很容易举出反例。例如 a [ ] = { 2 , 3 , 2 } a[]=\{2,3,2\} a[]={2,3,2},从最大的开始选会选到 a 2 = 3 a_2=3 a2=3,但是最有策略明显是选择 a 1 a_1 a1和 a 3 a_3 a3,贡献为 a 1 + a 3 = 4 > 3 = a 2 a_1+a_3=4>3=a_2 a1+a3=4>3=a2。通过上述例子,我们发现选当前位置的两侧和选当前位置 i i i的差值 d = a i + 1 + a i − 1 − a i d=a_{i+1}+a_{i-1}-a_i d=ai+1+ai−1−ai。如果只考虑这三个位置,当 d > 0 d>0 d>0时,我们选择两侧的更优;当 d < 0 d<0 d<0时,我们选择第 i i i个位置更优。

因此考虑反悔贪心,我们每次选到 a i a_i ai的时候,将 a i a_i ai记录进答案,同时将当前位置和两侧的差值 d d d放回 a i a_i ai,并将 a i − 1 a_{i-1} ai−1和 a i + 1 a_{i+1} ai+1拿出序列。这样如果我们再次选到了 a i a_i ai,这时第 i i i个位置对答案的贡献为第一次的 a i a_i ai和第二次的 d = a i + 1 + a i − 1 − a i d=a_{i+1}+a_{i-1}-a_i d=ai+1+ai−1−ai,总贡献为 a i + 1 + a i − 1 a_{i+1}+a{i-1} ai+1+ai−1,其意义是不选择 a i a_i ai选择 a i + 1 a_{i+1} ai+1和 a i − 1 a_{i-1} ai−1。

因此只需要开一个大根堆,代表待选的数的集合,维护一个 l , r l,r l,r数组代表当前位置的左侧和右侧,同时记录已经被选过的位置防止选重即可。时间复杂度为 O ( k log ⁡ n ) O(k\log n) O(klogn)。

代码↓

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 100;
struct node{
    int id,w;
    node(){
        id=w=0;
    }
    node(int x, int y){
        id=x,w=y;
    }
    bool operator<(const node &x) const{
        return w<x.w;
    }
};
int n,k,a[N],l[N],r[N];
bool vis[N];
priority_queue<node> q;
typedef long long ll;
ll ans;
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        l[i]=i-1,r[i]=i+1;
        q.push(node(i,a[i]));
    }
    for(int i=1;i<=k;i++){
        while(vis[q.top().id])q.pop();
        node now=q.top();
        if(now.w<0)break;
        q.pop();
        ans+=now.w;
        a[now.id]=now.w=a[l[now.id]]+a[r[now.id]]-a[now.id];
        q.push(now);
        vis[l[now.id]]=vis[r[now.id]]=1;
        l[now.id]=l[l[now.id]],r[now.id]=r[r[now.id]];
        l[r[now.id]]=r[l[now.id]]=now.id;
    }
    cout<<ans<<'\n';
    
}
cf1935C Messenger in MAC

最新的用到反悔贪心的cf比赛题

题意:有 n ( 1 ≤ n ≤ 2000 ) n(1\le n\le 2000) n(1≤n≤2000)条短信,第 i i i条短信包含两个信息 a i a_i ai和 b i ( 1 ≤ a i , b i ≤ 1 0 9 ) b_i(1\le a_i,b_i\le 10^9) bi(1≤ai,bi≤109),读取下标为 p 1 , p 2 , ... , p k p_1,p_2,\dots,p_k p1,p2,...,pk的短信所需要的时间为 ∑ i = 1 k a p k + ∑ i = 1 k − 1 ∣ b p k − b p k + 1 ∣ \sum_{i=1}^{k}a_{p_k}+\sum_{i=1}^{k-1}\mid b_{p_k}-b_{p_{k+1}} \mid ∑i=1kapk+∑i=1k−1∣bpk−bpk+1∣。求在给定时间 l ( 1 ≤ l ≤ 1 0 9 ) l(1\le l\le 10^9) l(1≤l≤109)内最多能读取多少条短信。 T ( 1 ≤ T ≤ 5 × 1 0 4 ) T(1\le T\le 5\times10^4) T(1≤T≤5×104)组数据, ∑ n 2 ≤ 4 × 1 0 6 \sum n^2\le 4\times10^6 ∑n2≤4×106。

对问题进行拆分,分成两个问题:选出第 p 1 , p 2 , ... , p k p_1,p_2,\dots,p_k p1,p2,...,pk位置的短信,如何安排能使得总时间最短;以及如何选出 p 1 , p 2 , ... , p k p_1,p_2,\dots,p_k p1,p2,...,pk。

首先第一个问题:选出第 p 1 , p 2 , ... , p k p_1,p_2,\dots,p_k p1,p2,...,pk位置的短信,如何安排顺序能使得总时间最短?

观察式子,可以发现对于信息 a a a,如何安排对时间的贡献是相同的,因此只需要考虑信息 b b b,不难发现将 b b b按顺序排列是最优的,此时所需时间为 ∑ i = 1 k a p k + b m a x − b m i n \sum_{i=1}^k a_{p_k}+b_{max}-b_{min} ∑i=1kapk+bmax−bmin。即对于选出的第 p 1 , p 2 , ... , p k p_1,p_2,\dots,p_k p1,p2,...,pk位置的短信,我们读取这些短信所需时间为所有短信 a a a的总和加上这些短信的 b b b中最大与最小的差。

对于第二个问题,我们考虑到上面化简的式子中,如果确定了 b b b的值,那么就是选出最小的 a i a_i ai,因此我们考虑按 b b b从小到大排序, b b b相同的按 a a a从小到大排序,并枚举最小的 b b b。我们的贪心策略是使得 b m a x − b m i n b_{max}-b_{min} bmax−bmin尽可能的小,因此先将 b b b相同的选出,再去考虑 b b b不同的。对于一个 b b b大于当前 b b b的短信,读取它的时间为 a i + b − b n o w a_i+b-b_{now} ai+b−bnow。此时在考虑反悔操作,如果前面选了 a p > a i + b − b n o w a_p>a_i+b-b_{now} ap>ai+b−bnow,就将 a p a_p ap反悔,不进行选择,并将当前的 b b b进行更新。通过建一个堆即可维护反悔操作。

赛时代码↓

cpp 复制代码
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N = 3e5 + 100;
const int mod = 998244353;
inline void read(int &x){
    int s=0,w=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
    x=s*w;
}
struct node{
    int a,b;
    node(){
        a=b=0;
    }
    node(int x, int y){
        a=x,b=y;
    }
}ms[N];
bool cmp(node x, node y){
    if(x.b!=y.b)return x.b<y.b;
    else return x.a<y.a;
}
int n,T,nxt[N];
ll l;
void solve(){
    cin>>n>>l;
    for(int i=1;i<=n;i++)cin>>ms[i].a>>ms[i].b;
    sort(ms+1,ms+1+n,cmp);
    ms[n+1]=node(0,0);
    nxt[n]=n+1;
    for(int i=n-1;i;i--){
        nxt[i]=nxt[i+1];
        if(ms[i].b!=ms[i+1].b)nxt[i]=i+1;
    }
    int ans=0;
    for(int i=1;i<=n;i++){
        ll now=ms[i].a;
        int nb=ms[i].b,cnt=1;
        if(now>l)continue;
        priority_queue<int> q;
        for(int j=i+1;j<nxt[i];j++){
            if(now+ms[j].a<=l)now+=ms[j].a,q.push(ms[j].a),cnt++;
            else break;
        }
        ans=max(ans,cnt);
        for(int j=nxt[i];j<=n;j++){
            if(ms[j].b!=nb){
                now+=ms[j].b-nb;
                nb=ms[j].b;
                while(now>l&&!q.empty()){
                    now-=q.top();
                    q.pop();
                    cnt--;
                }
                if(now>l)break;
            }
            if(now+ms[j].a<=l)now+=ms[j].a,q.push(ms[j].a),cnt++;
            else {
                if(!q.empty()){
                    if(ms[j].a<q.top()){
                        now+=ms[j].a;
                        now-=q.top();
                        q.pop();
                        q.push(ms[j].a);
                    }
                }
            }
            ans=max(ans,cnt);
        }

    }
    cout<<ans<<'\n';
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>T;
    while(T--)solve();
    return 0;
}
相关推荐
只做开心事43 分钟前
C++之红黑树模拟实现
开发语言·c++
程序员老冯头2 小时前
第十五章 C++ 数组
开发语言·c++·算法
程序猿会指北3 小时前
【鸿蒙(HarmonyOS)性能优化指南】启动分析工具Launch Profiler
c++·性能优化·harmonyos·openharmony·arkui·启动优化·鸿蒙开发
AC使者6 小时前
5820 丰富的周日生活
数据结构·算法
cwj&xyp7 小时前
Python(二)str、list、tuple、dict、set
前端·python·算法
无 证明7 小时前
new 分配空间;引用
数据结构·c++
xiaoshiguang311 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡11 小时前
【C语言】判断回文
c语言·学习·算法
别NULL11 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇11 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯