「学习笔记」反悔贪心

一.什么是反悔贪心#

顾名思义,反悔贪心就是两个操作:“反悔” + “贪心”。

一般来说,贪心仅能解出局部最优解。

那么在要求全局最优解时,我们就可以利用“反悔”这个操作解决。

反悔贪心的思想是:每次都进行操作,在以后有最优情况的时候再取消这次操作。

二.基本反悔贪心#

一般来说,我们采用一个堆来存选的元素,堆顶是最劣解

分两种情况:

1. 符合某种条件时,推入堆,计算答案。

2. 不符合某种条件时,与堆顶比较,做出操作。

接下来用一些例题来帮助大家理解这个算法。

三.反悔贪心例题#

CF865D Buy Low Sell High#

可以算是反悔贪心的模板了。

对于当前操作,我们要让收益最大,那么就要在最低点买入,最高点卖出。

我们可以用一个堆来记录今天之前的最小值,然后操作,就等于今天卖出最小值那天买入。

显然,这不是最优解法,因为我们没有考虑到后面的更优解法。

那么我们采用“反悔”操作,在卖出时,将当前价格推入堆,当做买入,那么这样计算的答案就是最优解。

也就是说,当前卖出价格 x,更优卖出价格 y,买入价格 z,最优解 yz 等于 (xz)+(yx)

代码就很好写了:

Copy
#include <bits/stdc++.h> using namespace std; #define int long long struct Node { int x; Node (int x = 0) : x(x) { } bool operator < (const Node &xx) const { return xx.x < x; } }; priority_queue<Node> q;//小根堆。 int n, ans = 0ll; signed main () { cin >> n; for (int i = 1, x; i <= n; i ++) { cin >> x; q.push (x);//每一个都当做买入。 if (!q.empty() && q.top().x < x) { //当前卖出价格大于最低价格时。 ans += x - q.top().x; //当前价格和堆顶相减,累计答案。 q.pop (); //弹出最低价格。 q.push (x); //推入当前价格,反悔操作。 } } cout << ans; return 0; }

P2209 [USACO13OPEN]Fuel Economy S#

正好打模拟赛碰到,一起写了。

显然,我们有一个反悔贪心的思路。

首先要判断无解情况:
1. B 个单位的油无法到达第一个加油站。

2.i 个加油站与第 i+1 个加油站的距离大于 G (1i<N)

3. 在第 N 个加油站即使满油也无法到达 D

然后可以进行反悔贪心。

首先,为了从第 i 个加油站到第 i+1 个加油站(或终点),我们都需要加满油。(贪心操作)

i 个加油站到第 i+1 个加油站之间多余的油,可以退掉。(反悔操作)

而应该如何使用油呢?显然,我们优先使用单位价格低的油,这样就能使总花费最小。

Copy
#include <bits/stdc++.h> using namespace std; const int N = 55555; #define int long long int n, g, b, d; struct Node { int x; int y; Node (int x = 0, int y = 0) : x (x), y (y){ } bool operator < (const Node & xx) const { return x < xx.x; } }a[N]; int l = 0, r = -1; struct Oil { int v; int t; Oil (int v = 0, int t = 0) : v (v), t (t) { } bool operator < (const Oil & xx) const { return v < xx.v; } //单调性。 }q[N]; int calc (int len) {//走距离为len的花费 int res = 0; while (len > 0) { if (q[l].t > len) {//不需要用完最便宜的油就可以走到。 res += len * q[l].v; q[l].t -= len; len = 0; } else {//需要用光最便宜的油。 res += q[l].t * q[l].v; len -= q[l].t; l ++; } } return res; } signed main () { cin >> n >> g >> b >> d; for (int i = 1; i <= n; i ++) { cin >> a[i].x >> a[i].y; } a[++ n] = Node (d, 0);//把终点也当做加油站。 sort (a + 1, a + 1 + n); if (a[1].x >= b) { puts ("-1"); return 0; } for (int i = 2; i <= n; i ++) { if (a[i].x - a[i - 1].x > g) { puts ("-1"); return 0; } } //判无解。 q[++ r] = Oil (0, b);//加入初始油。 int ans = 0; a[0].x = 0; for (int i = 1; i <= n; i ++) { ans += calc (a[i].x - a[i - 1].x);//从第i-1个加油站到第i个加油站。 b -= a[i].x - a[i - 1].x; if (i == n) { cout << ans; return 0; } while (l <= r && q[r].v > a[i].y) {//反悔操作。 //从r开始退油,保证退的油较贵。 b -= q[r].t; r --; } q[++ r] = Oil (a[i].y, g - b);//加满油。 b = g; } return 0; }

P2949 [USACO09OPEN]Work Scheduling G#

反悔的操作比较明显。

如果可以进行操作,那么就操作。

如果当前任务已经截止,就反悔操作,与最劣解比较。

Copy
#include <bits/stdc++.h> using namespace std; typedef long long ll; #define int ll const int N = 1e5 + 7; struct Node { int d; int p; }a[N]; inline bool cmp (Node x, Node y) { return x.d < y.d; } int n; priority_queue<int, vector<int>, greater<int> > q;//小根堆。 signed main () { cin >> n; for (int i = 1; i <= n; i ++) { cin >> a[i].d >> a[i].p; } sort (a + 1, a + 1 + n, cmp);//按截止时间排序。 int ans = 0; for (int i = 1; i <= n; i ++) { if (a[i].d <= q.size ()) { //每项任务花费时间都是1,那么队列长度就是当前时间。 //于是这行就表示当前任务已经截止的情况。 if (q.top() < a[i].p) { ans += a[i].p - q.top();//反悔操作。 q.pop (); q.push (a[i].p); } } else { ans += a[i].p; q.push (a[i].p); //贪心操作,每项都做。 } } cout << ans; return 0; }

P4053 [JSOI2007]建筑抢修#

和上一题很像。

我们开一个大根堆,存入工程需要的时间。

堆顶放的是最劣解,也就是说工程需要的时间越久越不好,所以开大根堆。

分两种情况:

1. 当前任务无法完成时:反悔操作,与堆顶比较,取用时短的较优解。

2. 当前任务可以完成时:贪心操作,完成该操作,推入堆。

代码:

Copy
#include <bits/stdc++.h> using namespace std; typedef long long ll; template <typename T> inline T read() { T x = 0, f = 1; char ch = getchar(); while (!isdigit(ch)) { if(ch=='-') { f = -1; ch = getchar(); } } while (isdigit (ch)) { x = x * 10 + (ch ^ 48); ch = getchar(); } return x * f; } const int N = 155555; struct Node { int t1; int t2; inline bool operator < (const Node &x) const { return t2 < x.t2; } }a[N]; int n; int main () { int n = read<int>(), mx = -1; for (int i = 1; i <= n; i ++) { a[i].t1 = read<int>(); a[i].t2 = read<int>(); } sort (a + 1, a + 1 + n);//按截止时间排序。 priority_queue<int> q; int now = 0, ans = 0; //now表示当前时间。 for (int i = 1; i <= n; i ++) { if (now + a[i].t1 > a[i].t2) {//反悔操作。 if (a[i].t1 < q.top ()) { now -= q.top () - a[i].t1;//时间减少。 q.pop (); q.push (a[i].t1); } } else {//贪心操作。 q.push (a[i].t1); now += a[i].t1; ans ++;//任务可以完成。 } } cout << ans << endl; return 0; }
posted @   cyhyyds  阅读(686)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示
CONTENTS