贪心:临项交换 & 带悔贪心
之前基础不好,贪心都是乱搞。直到模拟赛遇见才会。确实曾想过临项交换和带悔贪心的区别和联系,但是没有深入研究,今天老师又提到了这个问题,来写一下。
一般这一类题目的特点:需要选择的元素有两个权值,均对所求答案的排列顺序、统计造成影响。
1.临项交换:排序,每次用相邻两项进行比较交换。
一般思路:如果先选 a 再选 b,这种选择的先后顺序会对 b 造成什么影响。若先选 b 再选 a,这种选择的顺序对 a 又造成什么影响。影响一般就指代价。然后比较两种代价,哪一种排序方式的代价最小就采取哪种。
我们注意到这种交换,对于这两项之外的其他项都不会造成影响,只对两项自身选择有影响。所以仅在对于其他项不会对相邻两项产生影响的情况下可以使用。
2.带悔贪心:选择时无论当前的选项是否最优都存下来,在后面的选择中与之进行比较,如果选择之后不再最优,则反悔,舍弃掉这个选项;否则,正式接受。如此往复。
可以发现,这类题目中,当前的选择不光对自己有影响,也会对序列中其他项的选择情况造成影响。
常用优先队列维护之前已经选择的选项。一般难度在于怎么反悔。
感觉带悔贪心还是比较抽象的。
临项交换例题:洛谷 P1842 奶牛玩杂技。 本来想贴国王游戏,那道题确实更经典,但是发现我没写,因为需要高精度,而且题目本身也略复杂不适合做例题(?)(别问,问就是找借口)(不重要,有空补。)
每个奶牛有体重 w 和力量 s,奶牛需要叠放在一起,下面的牛受到的压力是其上奶牛的重量总和,若超出当前奶牛的力量,会产生危险受到压力总和与牛的力量之差的危险。找一个排列顺序使所有奶牛中最大的危险指数最小。(这时候才看到有最大值最小那么也可二分做)。然后就没什么好说的了。一个简单的推式子过程。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n;
ll ans = -0x7fffffff, tot = 0;
struct niu{int w;ll s;} a[50010];
bool cmp(niu x, niu y) {return x.s + x.w < y.s + y.w;}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++ ) scanf("%d%d", &a[i].w, &a[i].s);
sort(a + 1, a + n + 1, cmp);
for(int i = 1; i <= n; i ++ )
{
ans = max(ans, tot - a[i].s);
tot += a[i].w;
}
printf("%lld", ans);
return 0;
}
反悔贪心例题:
题意:做工作,每个任务有一个价值和最晚截止时间,完成每个工作需要一个单位时间。求最大获利。
注意,截止时间一般没说的话,指的是小于这个时间才符合。像下题这样的特别声明的,才是小于等于截止时间。
然后就是套路思想:按照截止时间排序进行选择,用小根堆维护完成工作的价值。只要时间方面来说可以完成当前工作,就加入小根堆。若时间不允许,就与堆顶比较(已经加入选择范围的价值最小的工作),如果发现当前更优,就进行一个反悔操作,弹出堆顶加入当前所选。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e5 + 10;
struct node{int t, v;}a[N];
int n;
ll ans;
priority_queue<int, vector<int>, greater<int> > q;
bool cmp(node x, node y) {return x.t < y.t; }
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++ ) scanf("%d%d", &a[i].t, &a[i].v);
sort(a + 1, a + n + 1, cmp);
for(int i = 1; i <= n; i ++ )
{
if(a[i].t <= q.size())
{
while(a[i].v > q.top() && !q.empty())
{
ans -= q.top();
q.pop();
q.push(a[i].v);
ans += a[i].v;
}
}
else
{
q.push(a[i].v);
ans += a[i].v;
}
}
printf("%lld", ans);
return 0;
}
- 洛谷:P4053 建筑抢修
5.11 那天队列模拟赛的题,吐槽一句,因为前一天专门看了看带悔贪心,然后就比较有思路,一上来就做的这道题。然后就比较快的凭感觉写了个,然后当时感觉不太对,换了一种写法。
这不是重点,重点是赛后发现一开始提交的代码竟然有 30 分,然后看了一眼,竟然是把大根堆写成小根堆了啊哈哈哈。笑死了,这也能有 30 分感觉非常了不起。
废话完毕。
题意:修复建筑,对于每个建筑有一个修复需要的时间,以及最晚完成时间。只有不超过最晚时间完成建筑修复才算完成一个修复。求最大完成数量。
然后就是套路了,按照每个建筑的截止时间排序,先修复时间紧迫的。用一个优先队列维护建筑修复耗时。每次处理时,如果从当前角度来看能完成修复,那就加入堆中。否则,看堆顶,也就是目前来说修复耗时最多的建筑,看一下若把它删了是否能完成当前的修复。如果是,就删了即进行一个反悔操作。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e5 + 10;
int n, ans;
priority_queue<ll> q;
struct node{int t, c;} a[N];
bool cmp(node x, node y) {return x.t < y.t;}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i ++ ) scanf("%d%d", &a[i].c, &a[i].t);
sort(a + 1, a + n + 1, cmp);
ll sum = 0;
for(int i = 1; i <= n; i ++ )
{
if(a[i].c + sum <= a[i].t)
{
ans ++;
sum += a[i].c;
q.push(a[i].c);
}
else
{
if(a[i].c < q.top())
{
sum += a[i].c - q.top();
q.pop();
q.push(a[i].c);
}
}
}
printf("%d", ans);
return 0;
}
关于反悔贪心,偏套路,掌握后看起来就没有那么困难了。
个人认为带悔贪心要特别注意的一点思路:在你选择当前时,不需要做过多判断,直接加入当前维护的优先队列中就行。然后在之后才会判断是否合法,不合法再进行反悔。这样叫反悔。而不是没做出决定就放弃。
本文作者:Moyyer_suiy
本文链接:https://www.cnblogs.com/Moyyer-suiy/p/17395051.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步