贪心算法

贪心算法

1. 贪心原理

原理

  • 不同于动态规划,一般来说动态规划每次会有多种决策方式;贪心算法每次贪心的选择一种决策方式,我们要保证这样的贪心做法能得到最优解,这一般是需要我们证明的。这也是贪心最难的地方。

2. AcWing上的贪心算法题目

AcWing 905. 区间选点

问题描述

分析

  • 本题的步骤如下:

    (1)将每个区间按照右端点从小到大排序;

    (2)从前向后依次枚举每个区间:如果当前区间已经包含点,直接继续考察下一个点;否则,选择当前区间的右端点为选出的点。

  • 证明:设上述做法得到的点数为cnt,最优解对应的点数为ans,则一定有cnt>=ans;另外按照上面的贪心做法选出的点所有的区间一定没有交集,否则有交集的话就不会选择该区间的右端点了,因此覆盖这些区间至少需要cnt个点,所以ans>=cnt;因此有cnt==ans。如下图:

在这里插入图片描述

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

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++) {
        int l, r;
        scanf("%d%d", &l, &r);
        range[i] = {l, r};
    }
    
    sort(range, range + n);
    
    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i++)
        if (range[i].l > ed) {
            res++;
            ed = range[i].r;
        }
    
    printf("%d\n", res);
    
    return 0;
}

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

问题描述

分析

  • 这一题的解题过程和AcWing 905. 区间选点完全一样,代码也完全一样。下面是证明:

  • 假设贪心做法选择出了cnt个点(按照905的做法,这cnt个点一定完全覆盖了所有区间),最优解中选出了ans个点,则ans>=cnt;下面证明ans<=cnt,反证法,如果ans>cnt,那么至少需要ans个点才能覆盖这些区间,和用cnt个点就一定可以覆盖了所有区间矛盾,因此假设不成立,所以有cnt==ans

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

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++) {
        int l, r;
        scanf("%d%d", &l, &r);
        range[i] = {l, r};
    }
    
    sort(range, range + n);
    
    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i++)
        if (range[i].l > ed) {
            res++;
            ed = range[i].r;
        }
    
    printf("%d\n", res);
    
    return 0;
}

AcWing 906. 区间分组

问题描述

分析

  • 本题的做法如下:

    (1)将每个区间按照左端点从小到大排序;

    (2)从前向后依次枚举每个区间:记录每组中最右侧的端点,如果当前考察的区间的左端点为l,判断是否存在一组中区间最右侧的端点小于l,如果存在,可以将这个区间加入到这一组中;否则不存在的话,说明这个区间和所有的组都存在交集,只能新开一组,同时将这新的一组右端点记录下来。

  • 为了判断是否存在一组中区间最右侧的端点小于l,可以使用小根堆记录每组的右端点。

  • 下面证明上述做法是正确的,假设最优解分组为ans,我们贪心解分组为cnt,则ans<=cnt;下面证明ans>=cnt,考虑我们的贪心做法已经得到了cnt-1组,则第一次得到第cnt个组对应的区间一定和前面的cnt-1个组有交集(否则按照我们的算法不会新开一组),因此至少需要开cnt组,所以ans>=cnt,这就证明了cnt==ans

代码

  • C++
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 100010;

int n;
struct Range {
    int l, r;
    bool operator< (const Range &w) const {
        return l < w.l;
    }
} range[N];

int main() {
    
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        int l, r;
        scanf("%d%d", &l, &r);
        range[i] = {l, r};
    }
    
    sort(range, range + n);
    
    priority_queue<int, vector<int>, greater<int>> heap;
    for (int i = 0; i < n; i++) {
        auto r = range[i];
        if (heap.empty() || heap.top() >= r.l) heap.push(r.r);
        else {
            heap.pop();
            heap.push(r.r);
        }
    }
    
    printf("%d\n", heap.size());
    
    return 0;
}

AcWing 907. 区间覆盖

问题描述

分析

  • 本题的做法如下:

    (1)将每个区间按照左端点从小到大排序;

    (2)从前向后依次枚举每个区间:在所有能覆盖start的区间中,选择右端点最大的区间,然后将start更新成右端点的最大值。

  • 可以证明最优解通过替换一定可以替换为贪心解。因此贪心解就是最优解

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int st, ed;
int n;
struct Range {
    int l, r;
    bool operator< (const Range &w) const {
        return l < w.l;
    }
} range[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);
        range[i] = {l, r};
    }
    
    sort(range, range + n);
    
    int res = 0;
    bool success = false;
    for (int i = 0; i < n; i++) {
        int j = i, r = -2e9;
        while (j < n && range[j].l <= st) {
            r = max(r, range[j].r);
            j++;
        }
        if (r < st) {
            res = -1;
            break;
        }
        
        res++;
        if (r >= ed) {
            success = true;
            break;
        }
        st = r;
        i = j - 1;
    }
    
    if (!success) res = -1;
    printf("%d\n", res);
    
    return 0;
}

AcWing 148. 合并果子

问题描述

分析

  • 经典哈夫曼树的模型,每次合并重量最小的两堆果子即可。

代码

  • C++
#include <iostream>
#include <queue>

using namespace std;

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

AcWing 913. 排队打水

问题描述

分析

  • 结论是:按照从小到大的顺序排队,总时间最小。

  • 假设有n个人,每个人的打水时间为t[1]、t[2]、......、t[n],则总等待时间为T = t[1]x(n-1) + t[2]x(n-2) + ... + t[n] x 0。只有这个序列时升序排列的时候,这个T才能最小。假设不成立的话,一定至少存在一对逆序(假设为a、b,且a<b),假设需要的时间为T',如果存在一对逆序则T'-T=b-a>0,存在更多逆序对,则T'会更大。

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010;

int n;
int a[N];

int main() {
    
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    
    sort(a + 1, a + n + 1);
    
    LL res = 0;
    for (int i = 1; i <= n; i++) res += a[i] * (n - i);
    
    printf("%lld\n", res);
    
    return 0;
}

AcWing 104. 货仓选址

问题描述

分析

  • 对于x、y,如果任选一个点a,则有 ∣ x − a ∣ + ∣ y − a ∣ ≥ ∣ x − y ∣ |x-a|+|y-a| \ge |x-y| xa+yaxy。当ax、y之间等号成立。

  • 因此本题我们可以将数组升序排列,然后第一个数和最后一个数一组,第二个数和倒数第二个数一组,然后每两个数中间的点可以随便选,这里选择所有组数的交集。因此当数组元素为奇数个时,变为中位数即可;当为偶数个数时,假设中间两个数为a, b(a<b),选取[a, b]中间任意一个数都行。

代码

  • C++
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 100010;

int n;
int a[N];

int main() {
    
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    
    sort(a, a + n);
    
    LL res = 0;
    for (int i = 0; i < n; i++) res += abs(a[i] - a[n / 2]);
    
    printf("%lld\n", res);
    
    return 0;
}

AcWing 125. 耍杂技的牛

问题描述

分析

  • 结论:按照w+s从小到大(顶部是最小的)的顺序排列,最大的危险系数一定是最小的。可以使用反证法证明。

  • 假设存在两头相邻的牛不是按照上述规则排序,例如第i头牛和第i+1头牛,根据假设有w[i]+s[i]>w[i+1]+s[i+1],则交换前后,两头牛的危险系数如下图:

在这里插入图片描述

  • 为了对比方便,将上图中的四项都加上-(w[1]+w[2]+...+w[i-1])+s[i]+s[i+1],可以得到:

在这里插入图片描述

  • 可以看到,交换后危险系数降低了,因此结论是正确的。

  • 另外还要考虑如果w[i]+s[i]==w[i+1]+s[i+1],较重的牛应该在下面,因为这是s是危险系数,w越大,s越小,危险系数越小。

代码

  • C++
#include <iostream>
#include <algorithm>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 50010;

int n;
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] = {w + s, w};
    }
    
    sort(a, a + n);
    
    int res = -1e9;
    for (int i = 0, sum = 0; i < n; i++) {
        int w = a[i].y, s = a[i].x - w;
        res = max(res, sum - s);
        sum += w;
    }
    printf("%d\n", res);
    
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值