【算法整理】——贪心

219 阅读9分钟

贪心

  • 贪心问题题解代码量小,思维含量大,证明存在相对较大难度
  • 贪心基本不存在固定模版,套路不唯一(需要多见多刷贪心问题)
  • 因此:解答过程中 思路+证明 更为重要

为什么有些题目可以用贪心解答

  • 贪心题目的答案多数为求某条件下的最值,最值的形成过程,是由多部分的最优解累计而来,换而言之,答案的是一个单峰的突函数,存在最值
  • 因此,可以用贪心求解的题目,一定有鲜明的多个子问题(子过程)累加属性,有可能和dp很像。

贪心算法的合理性

  • 正因为上述所说的,贪心问题具有累加属性,因此,如果可以将大问题,转化一系列具有递进属性的小问题,对每个小问题,求其在局部条件下的最优解,通过局部最优解进行累加,最终达到整个问题的最优解
  • 通俗来说,让一个问题的每一环节都尽可能达到当前最优,最终结果就是最优的

题目分类

具体分为四大类:

  1. 区间处理求最优解问题
  2. 与子节点贡献(深度)构造Huffman树求解问题
  3. 数学不等式求最值问题
  4. 公式推理问题

1.区间处理求最优解问题

  • 区间问题一般求解路线:
  1. 对整个 N 个区间按左/右端点排序,根据区间长度排序,and so on;
  2. 从小到大/从大到小遍历所有区间
  3. 用双指针或堆等,对区间进行处理与更新
  4. 判断是否有解,进行答案的输出

Q1——区间合并

AcWing 803. 区间合并

  • 思路 对于给定n个区间,将其可以合并为多少个大区间, 用pair来存每个区间,进行左端点排序,得到从小到大的序列, 由于 l 已经是从小到大了,所以只需要判断一下 r 和下一区间的关系 只有下一区间的 l 比当前的 r还要大才更新答案,同时更新 l 和 r, 其余的只是求一下新的 l 和旧的 l 的最大值。
  • 证明 设最优解为ans,贪心解为cnt

我们可以证明: ans≥cntans≤cnt 来得证 ans == cnt

  1. ans≤cnt 由于,这种方式更新区间时,遍历了所有的区间,cnt答案合法,时所有解的其中之一,ans为最优解,因此可以得出 ans≤cnt

  2. ans≤cnt 在更新过程中,若此时res++,当前区间的左端点大于之前已合并区间的最大右端点,对于已经遍历过的区间一定是严格在当前区间左侧的,因此,当前的res一定时严格小于等于最优解中当前的res的,因此ans≤cnt

  • 代码
#include <algorithm>
#include <cstdio>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int INF = 0x3f3f3f3f;
vector<PII> alls;
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        int l, r;
        scanf("%d%d", &l, &r);
        alls.push_back(PII(l, r));
    }

    sort(alls.begin(), alls.end());
    int res = 1;
    int l = INF, r = INF;
    for (auto i : alls) {
        int a = i.first, b = i.second;
        if (l == INF && r == INF) {
            l = a, r = b;
            continue;
        }
        if (a > r) {
            l = a, r = b;
            res++;
        } else {
            r = max(r, b);
        }
    }
    printf("%d", res);
    return 0;
}

Q2——区间选点/区间求最大交集

AcWing 905. 区间选点 AcWing 908. 最大不相交区间数量

  • 思路 对于多个区间求交集,先对所有区间按右端点进行排序,遍历所有区间,按序选取区间的右端点max并记录,用当前的右端点max与下一区间的左端点进行比较,如果下一区间左端点在当前的右边,则两区间无交集,更新右端点maxres++,如果在左边,证明两个区间存在交集,不更新ans右端点max
  • 证明 还是从证明 来证明 == 证明与上一题目基本相似,ans≤res 是解的合法性,ans≥res 是解的局部最优性
  • 代码
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const//运算符重载
    {
        return r < W.r;
    }
}range[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) 
        scanf("%d%d", &range[i].l, &range[i].r);

    sort(range, range + n);

    int res = 0, ed = -0x3f3f3f3f;
    for (int i = 0; i < n; i ++ )
        if (range[i].l > ed)
        {
            res ++ ;
            ed = range[i].r;
        }

    printf("%d\n", res);

    return 0;
}
  • 直接按照更新区间的办法写
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
vector<PII> alls, ans;
const int INF = 0x3f3f3f3f;
int n;

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        int l, r;
        scanf("%d %d", &l, &r);
        alls.push_back(PII(l, r));
    }

    sort(alls.begin(), alls.end());

    int l = INF, r = INF;

    for (auto i : alls) {
        int x = i.first, y = i.second;
        //printf("%d %d\n", l, r);
        if (l == INF && r == INF) {
            l = x, r = y;
            continue;
        }

        if (l <= x && r >= y) {
            l = x, r = y;
        } else if (x >= l && y >= r && x < r) {
            l = x;
        } else if (x > r) {
            ans.push_back(PII(l, r));
            l = x, r = y;
        }
    }

    printf("%d", ans.size()+1);
}

Q3——区间分组

AcWing 906. 区间分组

  • 思路 分组,每组内的所有区间不想交,求最小组数 先将所有区间按左端点排序,遍历所有区间 将每个区间做为一个独立的组,与之前已有的组进行合并, 合并过程是比较新的区间的左端点与之前组里区间的右端点最小值进行比较 (allrmin),如果新的l大于allrmin,则此时原有的所有组不能吧新点加入,创建新组,若是小于等于,则更新allrmin新的r。 在这个过程中,只需要记录每组的rmax即可,由于动态维护所有组的rmax,每次取出最小的rmax,因此可以使用堆优化,来记录每组的rmax,最终的组数就是堆中的元素个数

  • 证明

  1. ans≤cnt的证明是解的合理性
  2. ans≥cnt证明 在开设新的组时,当前区间一定时没有组可以加入的,由于每次开设新组,一定是在当前区间“走投无路”时才开的,所以当前的解时当前条件下的最优解,则有在最优解中的当前条件的ans’ans‘≥res的,由于ans‘ans的一部分,合理外推后ans≥cnt
  • 代码
#include <algorithm>
#include <cstdio>
#include <queue>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
int n;
vector<PII> alls;
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        int l, r;
        scanf("%d%d", &l, &r);
        alls.push_back(PII(l, r));
    }
    sort(alls.begin(), alls.end());

    priority_queue<int,vector<int>,greater<int>> heap;
    for (auto i : alls) {
        if (heap.empty() || heap.top() >= i.first)
            heap.push(i.second);
        else{
            heap.pop();
            heap.push(i.second);
        }
    }

    printf("%d", heap.size());
    return 0;
}

Q3——区间覆盖

AcWing 907. 区间覆盖

  • 思路 类似于之前求区间交集,用最少的区间来覆盖目的区间*[st,ed]*,

先对所有区间按左端点排序,遍历所有区间

覆盖可以理解为,求并集,用最小的数目实现覆盖,就要让所选区间尽可能的饭不在 [st,ed],即让区间的l尽可能的向左远离ed,让区间的r尽可能向右远离st。 接下来这是一个递归处理的问题,当每次找到了:区间的l尽可能的向左远离ed,区间的r尽可能向右远离st的区间,将sted更新为rl

由于已经按左端点排序,此时只需要找区间的r尽可能向右远离st的区间,找到旧更新一下st,最终如果r超过了ed,则已经覆盖结束,如果没有超过,但是已经遍历完了,则无解。

  • 证明 这个的证明直接来证ans == res就可以了(调整法) 对于ans中的每一个所选区间,都可以被贪心选出的区间覆盖,贪心的区间保证了每个区间对覆盖*[st,ed]*的贡献每部分都是最优的,因此,cnt所选的区间符合最优情况,ans == cnt

PS:虽然说区间个数相同,但是所选的区间可能不用,可能存在从左往右和从右往用同样个数的区间都可以覆盖的情况,eg:目标区间是[1,5],有区间[1,3],[3,5],[1,2],[2,5],如果按左端点排序选的就是前两个,按右端点就是后两个,但总数相同,(新的出题点,求可以覆盖的总方案数#(滑稽#)

  • 代码(双指针扫描所有区间)
#include <algorithm>
#include <cstdio>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
vector<PII> alls;
int st, ed;
int n;
int main()
{
    scanf("%d %d", &st, &ed);
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        int l, r;
        scanf("%d%d", &l, &r);
        alls.push_back(PII(l, r));
    }
    sort(alls.begin(), alls.end());
    int res = 0;
    bool flag = false;
    //这里一定要开一个flag,以防遍历完所有区间后r还没到大ed
    for (int i = 0; i < n;i++) {
        int j = i;
        int r = -2e9;
        while (j < n && alls[j].first <= st){
            r = max(r, alls[j].second);
            j++;
        }
        //如果没有可以到达st的区间,这个时候就可以直接退出了
        if(r<st){/
            res = -1;
            break;
        }
        res++;
        if(r>=ed){//如果到了更新flag
            flag = true;
            break;
        }
        st = r;
        i = j-1;
    }
    if(flag){
        printf("%d",res);
    }
    else printf("-1");
    return 0;
}

2.Huffman树构造问题

可以使用Huffman树在于题目中每次的选择点对答案的贡献度是不同的,转化在Huffman树上就是,贡献越大的子节点度越深,对答案影响更大 一般是要求解最小的花费,最少的代价等,都是和求总和有关的题目

合并果子

AcWing 148. 合并果子
(这个题有很多变形,如果吧题目改为只能合并相邻的果子就是dp问题了)

  • 思路 不难发现,越先合并的果子,对答案的影响越大,越先合并,算的次数就越多,有典型的叶子结点贡献不同的特点,因此先合并轻的,再合并重的就可以使答案最小,动态维护最小值,使用堆优化即可

  • 证明

  1. Huffman树是一棵完全二叉树,因此每一个节点都会参与运算,确保了构造前后的同一性
  2. 每个同度的节点(同一层的),对答案的贡献相同,可以任意交换,两两配对
  3. 每个小部分的解都是当前的最优解(选了当前最小的两个进行合并),因此保证了答案的最优性
  • 代码
#include <algorithm>
#include <cstdio>
#include <queue>
using namespace std;

int n;
priority_queue<int, vector<int>, greater<int>> heap;
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n;i++){
        int x;
        scanf("%d",&x);
        heap.push(x);
    }
    int sum = 0;
    while(heap.size()>=2){
        int a = heap.top();
        heap.pop();
        int b = heap.top();
        heap.pop();
        sum += a + b;
        heap.push(a + b);
    }

    printf("%d", sum);
    return 0;
}

3.数学不等式求最值的问题

  • 常用的不等式(证明略)
  1. 排序不等式
  2. 绝对值(三角)不等式
  3. 几何不等式、柯西不等式、均值不等式,ALG等(一般用到了题目就比较难了)

排序不等式

类似于Huffman树,每一个点对答案的贡献都不同

iShot2022-01-29 01.30.03.png

经典例题 排队打水

AcWing 913. 排队打水

  • 思路 让用时最少的先打,用时最少的最后打,可以用sort也可以用堆,用堆就是Huffman树的解法
  • 代码(拿堆写的会比排序慢一点点)
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
typedef long long LL;

int n;
int main()
{
    priority_queue<int, vector<int>, greater<int>> heap;
    scanf("%d", &n);
    for (int i = 0; i < n;i++){
        int x;
        scanf("%d", &x);
        heap.push(x);
    }
    LL res = 0;
    while (heap.size()) {
        res += heap.top() * (heap.size()-1);
        heap.pop();
    }

    printf("%lld", res);
    return 0;
}

绝对值(三角)不等式

一般用于求距离问题,这里的距离一般是曼哈顿距离 iShot2022-01-29 01.36.53.png 要理解绝对值的含义————代表距离

货仓选址

AcWing 104. 货仓选址

  • 思路和证明 将所有的坐标从小到大排序,x1xn一组,x2xn-1 一组... 设仓库坐标为x,表示到每组两个点的距离 IMG_959F53693507-1.jpeg 因此,应该把仓库放在所有点的最终间,xi的中位数处

  • 代码

#include <algorithm>
#include <cstdio>
#include <cmath>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int n;

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d",&a[i]);
    }
    sort(a, a + n);
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += abs(a[i] - a[n / 2]);//n是奇、偶都可以
    }
    printf("%d", sum);
    return 0;
}

4.公式推理问题

  • 这类题目技巧性很强,因此建议见到多进行总结,碰见多尝试,像写博弈论一样多模拟一些样例

耍杂技的牛

AcWing 125. 耍杂技的牛

  • 结论 按照 w+s 从小到大排序,小的放上面,大的放下面,最后求出hmax即可

  • 证明(遇事不决反证法) 假设最优解不是按照 w+s 从小到大排序,那么必然会存在一队相邻的牛 上面的 w+s 是大于下面的 w+s 的,给她们编号为ii+1:

IMG_43B6DA1BC930-1.jpeg

  • 代码
#include <cstdio>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
typedef long long LL;
int n;
const int N = 5e5 + 10;
PII a[N];
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n;i++){
        int w, s;
        scanf("%d%d", &w, &s);
        a[i] = PII(w+s,w);
    }

    sort(a, a + n);
    LL sw = 0l;
    LL h = -0x3f3f3f3f;
    for (int i = 0; i<n; i++) {
        int w = a[i].second;
        int s = a[i].first-a[i].second;
        h = max(h, sw-s);
        sw += w;
    }
    printf("%lld", h);
    return 0;
}