曾记否,到中流击水,浪遏飞舟。|

Moyyer_suiy

园龄:2年7个月粉丝:4关注:18

贪心:临项交换 & 带悔贪心

之前基础不好,贪心都是乱搞。直到模拟赛遇见才会。确实曾想过临项交换和带悔贪心的区别和联系,但是没有深入研究,今天老师又提到了这个问题,来写一下。


一般这一类题目的特点:需要选择的元素有两个权值,均对所求答案的排列顺序、统计造成影响。

1.临项交换:排序,每次用相邻两项进行比较交换。

一般思路:如果先选 a 再选 b,这种选择的先后顺序会对 b 造成什么影响。若先选 b 再选 a,这种选择的顺序对 a 又造成什么影响。影响一般就指代价。然后比较两种代价,哪一种排序方式的代价最小就采取哪种。

我们注意到这种交换,对于这两项之外的其他项都不会造成影响,只对两项自身选择有影响。所以仅在对于其他项不会对相邻两项产生影响的情况下可以使用。

2.带悔贪心:选择时无论当前的选项是否最优都存下来,在后面的选择中与之进行比较,如果选择之后不再最优,则反悔,舍弃掉这个选项;否则,正式接受。如此往复。

可以发现,这类题目中,当前的选择不光对自己有影响,也会对序列中其他项的选择情况造成影响。

常用优先队列维护之前已经选择的选项。一般难度在于怎么反悔。

感觉带悔贪心还是比较抽象的。


临项交换例题:洛谷 P1842 奶牛玩杂技。 本来想贴国王游戏,那道题确实更经典,但是发现我没写,因为需要高精度,而且题目本身也略复杂不适合做例题(?)(别问,问就是找借口)(不重要,有空补。)

每个奶牛有体重 w 和力量 s,奶牛需要叠放在一起,下面的牛受到的压力是其上奶牛的重量总和,若超出当前奶牛的力量,会产生危险受到压力总和与牛的力量之差的危险。找一个排列顺序使所有奶牛中最大的危险指数最小。(这时候才看到有最大值最小那么也可二分做)。然后就没什么好说的了。一个简单的推式子过程。

image

#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;
}

反悔贪心例题:

1.洛谷:P2949 Work Scheduling G

题意:做工作,每个任务有一个价值和最晚截止时间,完成每个工作需要一个单位时间。求最大获利。

注意,截止时间一般没说的话,指的是小于这个时间才符合。像下题这样的特别声明的,才是小于等于截止时间。

然后就是套路思想:按照截止时间排序进行选择,用小根堆维护完成工作的价值。只要时间方面来说可以完成当前工作,就加入小根堆。若时间不允许,就与堆顶比较(已经加入选择范围的价值最小的工作),如果发现当前更优,就进行一个反悔操作,弹出堆顶加入当前所选。

#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;
}
  1. 洛谷: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 中国大陆许可协议进行许可。

posted @   Moyyer_suiy  阅读(149)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起