「学习笔记」反悔贪心
一.什么是反悔贪心
顾名思义,反悔贪心就是两个操作:“反悔” + “贪心”。
一般来说,贪心仅能解出局部最优解。
那么在要求全局最优解时,我们就可以利用“反悔”这个操作解决。
反悔贪心的思想是:每次都进行操作,在以后有最优情况的时候再取消这次操作。
二.基本反悔贪心
一般来说,我们采用一个堆来存选的元素,堆顶是最劣解。
分两种情况:
\(1.\) 符合某种条件时,推入堆,计算答案。
\(2.\) 不符合某种条件时,与堆顶比较,做出操作。
接下来用一些例题来帮助大家理解这个算法。
三.反悔贪心例题
CF865D Buy Low Sell High
可以算是反悔贪心的模板了。
对于当前操作,我们要让收益最大,那么就要在最低点买入,最高点卖出。
我们可以用一个堆来记录今天之前的最小值,然后操作,就等于今天卖出最小值那天买入。
显然,这不是最优解法,因为我们没有考虑到后面的更优解法。
那么我们采用“反悔”操作,在卖出时,将当前价格推入堆,当做买入,那么这样计算的答案就是最优解。
也就是说,当前卖出价格 \(x\),更优卖出价格 \(y\),买入价格 \(z\),最优解 \(y - z\) 等于 \((x-z)+(y-x)\)。
代码就很好写了:
#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\) \((1\le i<N)\)。
\(3.\) 在第 \(N\) 个加油站即使满油也无法到达 \(D\)。
然后可以进行反悔贪心。
首先,为了从第 \(i\) 个加油站到第 \(i+1\) 个加油站(或终点),我们都需要加满油。(贪心操作)
第 \(i\) 个加油站到第 \(i+1\) 个加油站之间多余的油,可以退掉。(反悔操作)
而应该如何使用油呢?显然,我们优先使用单位价格低的油,这样就能使总花费最小。
#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
反悔的操作比较明显。
如果可以进行操作,那么就操作。
如果当前任务已经截止,就反悔操作,与最劣解比较。
#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.\) 当前任务可以完成时:贪心操作,完成该操作,推入堆。
代码:
#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;
}