【学习笔记】反悔贪心
简介
反悔贪心其实就是带反悔的贪心,当发现某个题目 DP 不了,且贪心正确性有问题时,就考虑反悔贪心。
例题
Work Scheduling G
题解
按截至日期从小到大考虑选,能选就选,遇到一个工作完成不了时,就从前面已完成的工作中找一个利润最小的丢弃掉。发现 \(D_i\) 是递增的,所以丢弃到一个后排在他以前的用时不变,依旧能完成;排在他后边用时更少,显然能完成;新加入的(如果没被丢弃)用时和丢弃前相同,也能完成。
用小根堆维护,时间复杂度 \(O(n\log n)\)。
code
#include<bits/stdc++.h>
#define Fo(a, b) for(auto a : b)
#define fo(a, b, c) for(int b = a; b <= c; b++)
#define _fo(a, b, c) for(int b = a; b >= c; b--)
#define pb push_back
#define lb lower_bound
#define int long long
#define N 500010
using namespace std;
struct node{
int d, p;
bool operator <(const node &x) const{
return p > x.p;
}
}a[N];
bool cmp(node x, node y){
return x.d < y.d;
}
int n, sumt, ans, T;
priority_queue<node>q;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
fo(1, i, n) cin >> a[i].d >> a[i].p;
sort(a + 1, a + n + 1, cmp);
fo(1, i, n){
sumt++, ans += a[i].p;
q.push(a[i]);
if(sumt > a[i].d){
ans -= q.top().p, sumt--;
q.pop();
}
}
cout << ans << "\n";
return 0;
}
Job Completion G
考虑两个工作选择哪一个更优。设当前时刻为 t,当前有两个工作 i,j,且完成了 i 不能完成 j,而完成了 j 还可以完成 i。此时显然有先做 j 再做 i 更优。对应这种情况,变为表达式则有 \(t++t_i>s_j,t+tj\le s_i\)
解得此时 \(s_i+t_i>s_j+t_j\)。
所以我们只需要先将所有的工作按照 \(s_i+t_i\) 从小到大进行排序,那么最终的操作顺序一定是这里的一段子序列。我们用反悔贪心去选择,记当前时间为 \(now\):
- 直接选择这个工作 \(now\le s,now=t+s\);
- 无法选择这个工作 \(now>s\),如果之前完成的工作最长用时大于当前任务的 t,直接替换掉。
所有完成的工作只需要用大根堆存 t。
code
#include<bits/stdc++.h>
#define Fo(a, b) for(auto a : b)
#define fo(a, b, c) for(int b = a; b <= c; b++)
#define _fo(a, b, c) for(int b = a; b >= c; b--)
#define pb push_back
#define lb lower_bound
#define int long long
#define N 500010
using namespace std;
struct node{
int t1, t2;
bool operator <(const node &x) const{
return t1 < x.t1;
}
}a[N];
bool cmp(node x, node y){
return x.t1 + x.t2 < y.t1 + y.t2;
}
int n, sumt, ans, T;
priority_queue<node>q;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> T;
while(T--){
sumt = ans = 0;
while(!q.empty()) q.pop();
cin >> n;
fo(1, i, n) cin >> a[i].t2 >> a[i].t1;
sort(a + 1, a + n + 1, cmp);
fo(1, i, n){
sumt += a[i].t1;
q.push(a[i]);
if(sumt <= a[i].t2 + a[i].t1) ans++;
else{
sumt -= q.top().t1;
q.pop();
}
}
cout << ans << "\n";
}
return 0;
}
种树
神仙贪心题。
第一眼 DP,但发现空间时间好像都会炸。QWQ
于是,贪心。
考虑 \(m = 2\) 时:要么是选最大值,并选一个它左右数字之外的数,要么选它左右的数。
因为如果选了最大值左边的数,而不选最大值右边的数,把左边的数,换成最大值一定更优。
考虑一般情况:显然可以按最大值排序,然后从小到大选,但是如上,有时选最大值,可能不如选他两边的数优。
因此(退流操作),如果要选 \(x\),可以把答案加上 \(val_x\) 以后,将 \(x\) 节点的值改成 \(val_l + val_r - val_x\),这样相当于增加了反悔的机会(\(val_x + val_l +val_r - val_x = val_l + val_r\))。同时,因为更新后相当于在左右两边种了树,因此要把 \(x\) 的左边改成它左边的左边,右边改成右边的右边。
用双向链表,优先队列维护即可。

浙公网安备 33010602011771号