7.21 反悔贪心初步
—— 人生无法 \(\rm Ctrl\;Z\) ,但反悔贪心可以
反悔贪心初步
反悔贪心基于贪心,即一开始也是按照某一贪心策略执行贪心。但是反悔贪心引入了一个新操作,即反悔。
我们可以在贪心的过程中做出一些处理,将我们假想/构造的代表反悔选项的决策同时插入决策集合,然后继续按照步骤贪心。
若之后选择了已经构造的反悔选项,代表撤销(反悔)之前的劣的决策而选用新的更优决策。
所以,反悔贪心的基本思想是:既然这次不一定是最优,那么就先放着,如果以后找到更优的再取消这次操作,选取更优的操作。
这样的话只要保证当前最优就可以了,因为以后的更优就会反悔。
维护当前最优和以后的反悔通常用堆实现。
反悔操作一个经典的应用就是网络流中寻找增广路时“建反边”的操作。
例题
\(\rm CF865D\) \(\text{Buy Low Sell High}\)
- 已知接下来 \(N\) 天的股票价格,每天你可以买进一股股票,卖出一股股票,或者什么也不做。\(N\) 天之后你拥有的股票应为\(0\)。当然,希望这N天内能够赚足够多的钱。
一个朴素(错误)的贪心想法就是,依次考虑每一天,看到当前天和之前的某一天的价格构成差价就选择交易。当然这样的贪心是不够优的。例如价格依次为 1 2 100
的时候这个贪心将错过最优解,因为一旦选择在 \(1\) 买入,在 \(2\) 卖出就已经确定了交易。
我们考虑加入反悔操作:在卖出 \(2\) 这一股后我们构造出一个反悔决策,将这个决策再次插入决策集合之中作为购买的价格出现在后面。可以理解为在 \(i\) 买入,贪心地在 \(j\) 卖出,但是在后面 \(k\) 处发现了更优的决策,于是决定将 \(i\) 处购买的股票拖到 \(k\) 来卖,等价于在 \(i\) 购买,在 \(j\) 卖出,然后再把它买回来,再在 \(k\) 卖出。这样,我们就通过构造反悔决策,使在同一决策集合中通过反悔操作修正,达成等价于更优策略的状态。
为了保证这个算法的正确性,我们还要解决如下几个问题:
Q1: 如果我只选了那个可选物,而不选反悔物,那么这一天岂不是又买入又卖出?。
A1: 事实上,由于可选物与反悔物的价格相同,我们可以认为优先选择反悔物,而不会出现冲突。
Q2: 为何被反悔的物品不能选择一个次优的物品买入?在此算法中一个物品被反悔后只能作为价格低的物品被买入。
A2: 我们可以注意到,一个物品的决策被反悔当且仅当它是这个集合中最小的数,即没有比它更小的数供它选择买入、同时也没有比它更小的,在它后面的数抢它的东西。
Q3: 为何一个物品不被反悔就只能作为较大数卖出?这个物品作为较小数买入为何不可能更优?
A3: 因为一个物品被反悔与一个物品作为较小数买入更优的条件一样。
$\to\text{My Code}\leftarrow$
int n, x;
LL ans;
priority_queue <int, vector <int>, greater <int> > Q;
signed main() {
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> x;
if(!Q.empty() && Q.top() < x) { ans += 1LL * x - Q.top(); Q.pop(); Q.push(x); }
Q.push(x);
}
cout << ans;
}
不开LL见祖宗 x1
\(\text{LuoguP2949 [USACO09OPEN]Work Scheduling G}\)
约翰有太多的工作要做。为了让农场高效运转,他必须靠他的工作赚钱,每项工作花一个单位时间。 他的工作日从 \(0\) 时刻开始,有 \(10^9\) 个单位时间。在任一时刻,他都可以选择编号 \(1\) 到 \(N\) 的 \(N(1 \leq N \leq 10^5)\) 项工作中的任意一项工作来完成。 因为他在每个单位时间里只能做一个工作,而每项工作又有一个截止日期,所以他很难有时间完成所有N个工作,虽然还是有可能。 对于第 \(i\) 个工作,有一个截止时间 \(D_i(1 \leq D_i \leq 10^9)\),如果他可以完成这个工作,那么他可以获利 \(P_i( 1\leq P_i\leq 10^9 )\). 在给定的工作利润和截止时间下,约翰能够获得的利润最大为多少.
(这题和《算法竞赛进阶指南》二叉堆一节 \(\rm Supermarket\) 一题一模一样,双倍经验。之前没想到题目解析中介绍的解法可以按反悔理解)
将任务按到期时间排序,依次扫描,我们先贪心地选当前利润最大的任务,将其插入决策集合中(代表已选)。若发现时间不足以完成所有的任务,则从当前决策集合中剔除利润最小的决策直到时间足以完成这些任务。这样我们就得到了最优解。
$\to\text{My Code}\leftarrow$
struct T {
LL p, d;
}a[N];
int n;
priority_queue <LL, vector <LL>, greater <LL> > Q;
inline bool cmp(T A, T B) {
return A.d < B.d;
}
signed main() {
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i].d >> a[i].p;
}
sort(a + 1, a + n + 1, cmp);
for(int i = 1; i <= n; i++) {
Q.push(a[i].p);
while(Q.size() > a[i].d) Q.pop();
}
LL ans = 0;
while(Q.size()) {
ans += 1LL * Q.top();
Q.pop();
}
cout << ans;
}
不开LL见祖宗 x2
$ \text{P4053 [JSOI2007]}$ 建筑抢修
小刚在玩 JSOI 提供的一个称之为“建筑抢修”的电脑游戏:经过了一场激烈的战斗,T 部落消灭了所有 Z 部落的入侵者。但是 T 部落的基地里已经有 \(N\) 个建筑设施受到了严重的损伤,如果不尽快修复的话,这些建筑设施将会完全毁坏。现在的情况是:T 部落基地里只有一个修理工人,虽然他能瞬间到达任何一个建筑,但是修复每个建筑都需要一定的时间。同时,修理工人修理完一个建筑才能修理下一个建筑,不能同时修理多个建筑。如果某个建筑在一段时间之内没有完全修理完毕,这个建筑就报废了。你的任务是帮小刚合理的制订一个修理顺序,以抢修尽可能多的建筑。
\(N\) 行,每行两个整数 \(T_1,T_2\) 描述一个建筑:修理这个建筑需要 \(T_1\) 秒,如果在 \(T_2\) 秒之内还没有修理完成,这个建筑就报废了。
考虑反悔。我们按照建筑的自爆时间排序,尽可能修复自爆时间靠前的建筑。若当前修复所用的总时间大于一个建筑的自爆时间,说明我们无法全部修复这所有的前若干建筑。所以我们考虑反悔之前的决策,即放弃修复已修复的建筑中修复时间最长的建筑转而修复当前建筑,用优先队列维护即可。
点击查看代码
struct T {
LL usage, limit;
}a[N];
LL n, ans;
priority_queue <LL> Q;
inline bool operator < (T A, T B) {
return A.limit < B.limit;
}
signed main() {
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i].usage >> a[i].limit;
sort(a + 1, a + n + 1);
LL Timenow = 0;
for(int i = 1; i <= n; i++) {
Q.push(a[i].usage);
if(a[i].limit < Timenow + a[i].usage) {
Timenow -= Q.top() - a[i].usage;
Q.pop();
}
else {
Timenow += a[i].usage;
++ans;
}
}
cout << ans;
}
不开LL见祖宗 x3
\(\#\) \(\text{LuoguP3620 [APIO/CTSC2007]}\) 数据备份
给出递增的 \(x[1...n]\)(对应原题的坐标,范围\(10^9\)),在其中选出 \(k\) 对数 \((x_i, x_j)\),最小化 \(\sum |x_i-x_j|\)。
容易发现最优决策中相邻两个办公楼一定是配对的。我们求出每相邻两个办公楼之间的距离(差分),那么问题可以转化为从差分数组 \(a\) 中选出不超过 \(k\) 个数,使他们的和最小,并且相邻两个数不能同时被选。
转化问题后,设当前决策集合(差分数组)中最小值为 \(a_i\),则最优解一定属于一下情况之一(略加思考即可证明):
- 选 \(a_i\)和其他的数。
- 选 \(a_{i-1}\) 和 \(a_{i+1}\)。
于是我们考虑用链表维护这个决策集合:先选上 \(a\) 数组中的最小值(第一种情况),然后将然后将 \(a_{i-1},a_i,a_{i+1}\) 从数列中删除,并在原位置插入一个新元素 \(a_{i-1}-a_i+a_{i+1}\)。这样原问题就变成了一个从 \(a\) 数组中选 \(k-1\) 的数的子问题,重复这个操作 \(k-1\) 次就可以求出最终结果。这样若决策 \(a_{i-1}-a_i+a_{i+1}\) 被包含在了后续的决策集合中,就相当于当初不选 \(a_i\) 而选择 \(a_{i-1},a_{i+1}\)。对应到最小值和不能选相邻这两个条件后续决策均不受影响。
\(\rm Code\) 再说吧。。。
upd:10.22 T4 考了道几乎一样的题。
那就不用补代码了
#include <bits/stdc++.h>
#define N 500007
#define inf 1e14
using LL = long long;
using namespace std;
struct List_Node {
LL val;
int l, r;
LL kk;
}p[N];
struct Node {
LL val;
int id;
LL kk;
inline bool operator < (Node it) const {
return kk < it.kk;
}
};
int n, last;
LL ans, m, sum;
bool vis[N];
priority_queue <Node> q;
void Del(int x) {
p[x].l = p[p[x].l].l;
p[x].r = p[p[x].r].r;
p[p[x].l].r = x;
p[p[x].r].l = x;
}
int main() {
freopen("restart.in", "r", stdin);
freopen("restart.out", "w", stdout);
//freopen("in.txt", "r", stdin);
//freopen("Ans.txt", "w", stdout);
scanf("%d%lld%d", &n, &m, &last);
sum = last;
LL ii = 0;
for(int i = 1; i < n; ++i) {
int in;
scanf("%d", &in);
sum += in;
p[i].val = 1LL * in + last;
ii = max(ii, 1LL * in + last);
last = in;
p[i].l = i - 1;
p[i].r = i + 1;
p[i].kk = min(m, p[i].val);
q.push((Node){p[i].val, i, p[i].kk});
}
p[0].val = p[n].val = -inf;
p[0].kk = p[n].kk = -inf;
for(int i = 1; i <= n / 2; ++i) {
while(vis[q.top().id]) q.pop();
Node now = q.top();
q.pop();
//ans = max(ans, ans + now.kk);
ans += now.kk;
//cout << "now: " << now.id << ' ' << now.val << ' ' << now.kk << endl;
vis[p[now.id].l] = vis[p[now.id].r] = 1;
p[now.id].val = p[p[now.id].l].val + p[p[now.id].r].val - p[now.id].val;
p[now.id].kk = p[p[now.id].l].kk + p[p[now.id].r].kk - p[now.id].kk;
//cout << "new: " << p[now.id].kk << endl;
q.push((Node){p[now.id].val, now.id, p[now.id].kk});
Del(now.id);
printf("%lld\n", sum - ans);
}
return 0;
}