【学习笔记】反悔贪心

简介

反悔贪心其实就是带反悔的贪心,当发现某个题目 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\) 的左边改成它左边的左边,右边改成右边的右边。

用双向链表,优先队列维护即可。

posted @ 2025-11-09 09:26  GuoSN0410  阅读(7)  评论(0)    收藏  举报