6. 贪心

6.1 区间问题

例题AcWing 905. 区间选点

题目:给定 n 个闭区间 [li,ri],在数轴上选出最少数量的点,使得每个区间至少包含一个被选择的点。1n105,109li,ri109

思路

贪心的想法是很显然的:先将所有区间按左端点排序。定义 L=l1,R=r1,cnt=1,重复以下步骤直到遍历完所有区间:

  • 若第 i 个区间被区间 [L,R] 包含,即 LliR,则将这两个区间合并,令 L=li,R=min(R,ri)(相当于在区间 [li,min(R,ri)] 中选一个点)。
  • 否则将 L 更新为 liR 更新为 ricnt 增加 1

最终的答案即为 cnt

这样,我们就可以让每个被选到的点都属于尽可能多的区间。时间复杂度 O(nlogn)

贪心思路的证明:

显然,上述贪心算法一定满足题目的要求。我们只需要证明 cnt 最小即可。

不妨设通过算法上述所得到点为 d1,d2,,dcnt。令 Ai 表示第 i 个区间,设 d1A1,A2,,Ai。假设在执行算法第一步的过程中,我们直接跳至下一个区间,使得 d1A1,A2,,Aj(1j<i),之后再继续按原来的算法执行。设这样得到的点数为 cnt。由于 Aj+1Ai 至少还需要一个点,假设 Aj+1Ai 的区间并为 [L,R], 根据算法第二步有 R<li+1,又因为我们已经按区间左端点排序,所以 Aj+1Ai 不可能再与任何一个区间合并,那么就有 cntcnt(取等当且仅当 cnt=n),所以根据上述算法求出的 cnt 一定为最小值。

得证。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

typedef pair<int, int> pii;

const int N = 1e5+10;

int n, cnt = 1;
pii a[N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d%d", &a[i].first, &a[i].second);
    
    sort(a+1, a+n+1);
    int L = a[1].first, R = a[1].second;
    for (int i = 2; i <= n; ++i) {
        int L_ = a[i].first, R_ = a[i].second;
        if (L <= L_ && L_ <= R) L = L_, R = min(R_, R);
        else L = L_, R = R_, cnt ++;
    }
    printf("%d\n", cnt);
    return 0;
}

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

题目:给定 n 个闭区间 [li,ri],选择尽可能多的区间使得这些区间两两不相交,输出最大数量。1n105,109li,ri109

思路:显然这道题与 AcWing 905. 区间选点 的答案是一模一样的。

例题AcWing 906. 区间分组

题目:给定 n 个闭区间 [li,ri],将这些区间分成尽量少的组,使得每组内部的区间两两没有交集。1n105,109li,ri109

思路

题目可以转化为:计算出一个点最多被几个区间包含(因为这些区间一定不能分在一组)。

cnt 表示当时点的区间数量。我们可以分别将 li,ri 从小到大排序,并按从小到大的顺序遍历 l,r 数组,遇到 licnt 增加 1,遇到 ricnt 减少 1cnt 在此过程中的最大值即为答案。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

int n, cnt = 0, ans;
int start[N], close[N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) scanf("%d%d", &start[i], &close[i]);
    
    sort(start+1, start+n+1), sort(close+1, close+n+1);
    
    int i = 1, j = 1;
    while (i <= n || j <= n) {
        if (start[i] <= close[j] && i <= n) cnt ++, i ++;
        else if (j <= n) cnt --, j ++;
        ans = max(ans, cnt);
    }
    printf("%d\n", ans);
    return 0;
}

例题AcWing 907. 区间覆盖

题目:给定一个区间 [s,t]n 个区间 [li,ri],在这 n 个区间选择尽可能少的区间使得这些区间能够完全覆盖区间 [s,t],若无解则输出 -11n105,109li,ri109,109st109

思路

贪心算法:先将 n 个区间按左端点排序,重复以下步骤直到遍历完所有区间:

  • 找到第一个覆盖了 [s,t] 且右端点最大的区间(即 lis,risri 最大),所需区间增加 1,若找不到这样的区间,则无解;
  • rit 则说明已经找到答案,退出循环;否则令 s=ri

证明比较显然,因为在剩下所有的能覆盖 [s,t] 的区间中,右端点最大的区间所能覆盖的部分更多,若选择其他区间则一定不优于根据贪心算法求得的解。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

typedef pair<int, int> pii;

const int N = 1e5+10;

int n, s, t, ans;
bool check;
pii a[N];

int main() {
    scanf("%d%d%d", &s, &t, &n);
    for (int i = 1; i <= n; ++i) scanf("%d%d", &a[i].first, &a[i].second);
    
    sort(a+1, a+n+1);
    for (int i = 1; i <= n; ++i) {
        int j = i, r = - 2e9;
        while (j <= n && a[j].first <= s) r = max(r, a[j].second), j ++;
        if (r < s) {ans = -1; break;}
        
        ans ++;
        if (r >= t) {check = 1; break;}
        s = r;
        i = j-1;
    }
    
    if (!check) ans = -1;
    printf("%d\n", ans);
    return 0;
}

6.2 Huffman 树

例题AcWing 148. 合并果子

题目:有编号为 1nn 堆石子,第 i 堆石子的质量为 ai。现在要将这 n 堆石子合为一堆,合并任意两堆的代价是这两堆的石子质量之和,求出合并的最小总代价。1n10000,1ai20000

思路:合并果子是经典的 Huffman 树模型,其贪心思路很简单:每次取出两个最小值,将它们合并,直到只剩一堆石子。这个过程可以用小根堆实现,时间复杂度 O(nlogn)

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <vector>

using namespace std;

int n, sum;
priority_queue<int, vector<int>, greater<int>> heap;

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

6.3 排序不等式

例题AcWing 913. 排队打水

题目:有 n 个人排队打水,第 i 个人打水的时间为 ti,求出所有人的最小等候时间之和。1n105,1ti104

思路

贪心思路:先将 t 数组从小到大排序,令 si=j=1i1tj,则答案即为 i=1nsi

证明:

假设我们将 t 数组从小到大排序后,交换两个数 ai,aj(i<j) 的位置,令 si=j=1i1tj,那么由于 ajai,则有 si+1si+1,si+2si+2,,sjsj。所以 i=1nsii=1nsi,即交换后的答案一定不优于原来的答案。

得证。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 1e5+10;

int t; ll sum;
int a[N];
ll s[N];

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

6.4 绝对值不等式

例题AcWing 104. 货仓选址

题目:在一条数轴上有 n 个点 ai,在数轴上选出一个点 c,使得 i=1n|cai| 最小。1n105,1ai40000

思路

贪心思路:先将 a 数组从小到大排序,令 c=an/2+1 即可。

证明:

我们先考虑只有 2 个点的情况。分类讨论:

  • c<a1:题目所求为 (a1c)+(a2c)=(a2a1)+2(a1c)
  • a1ca2:题目所求为 (ca1)+(a2c)=a2a1
  • c>a2:题目所求为 (ca1)+(ca2)=(a2a1)+2(ca2)

显然当 a1ca2 时,题目所求最小。在数轴上画出来,可以更好地理解。

类似地,只有 3 个点时,c=a2 时最小。

这两种情况可以推广至 n 个点的情况,即:若 n 为偶数,则 an/2can/2+1;若 n 为奇数,则 c=an/2+1

得证。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

const int N = 1e5+10;

int n, t, sum;
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);
    t = a[n/2+1];
    for (int i = 1; i <= n; ++i) sum += abs(t-a[i]);
    printf("%d\n", sum);
    return 0;
}

6.5 推公式

例题AcWing 125. 耍杂技的牛

题目:有 n 头奶牛,第 i 头的重量为 wi,强壮程度为 si。这些奶牛要表演叠罗汉,一头牛的风险值 di 为它头上所有牛的总重量减去它的身体强壮程度的值。请你确定一种方案,使得所有奶牛的风险值的最大值尽可能小,输出这个值。1n5×104,1wi104,1si109

思路

考虑叠罗汉时相邻的两头奶牛 i,j。设放在 i,j 上面的所有牛的总重量为 W。分类讨论:

  • i 放在 j 上面:此时 di=Wsi,dj=W+wisj
  • j 放在 i 上面:此时 di=W+wjsi,dj=Wsj

考虑什么时候 i 放在 j 上面更优。则 max(Wsi,W+wisj)max(W+wjsi,Wsj)。由于 wi,wj>0,所以 Wsi<W+wjsi,W+wisj>Wsj

那么当 W+wisjW+wjsi ,即 wi+siwj+sj 时,i 放在 j 上面更优。

我们可以将所有牛按 ti=wi+si 从小到大排序,ti 小的牛放在上面,若有 ti 相同的牛,则 si 小的放在上面。

代码

#include <iostream>
#include <cstdio>
#include <algorithm>

using namespace std;

typedef long long ll;
typedef pair<int, int> pii;

const int N = 5e4+10;

int n;
ll ans = -1e18, sum;
pii c[N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        int w, s; scanf("%d%d", &w, &s);
        c[i].first = w+s, c[i].second = s;
    }
    
    sort(c+1, c+n+1);
    for (int i = 1; i <= n; ++i) {
        sum -= (ll)c[i].second;
        ans = max(ans, sum);
        sum += (ll)c[i].first;
    }
    printf("%lld\n", ans);
    return 0;
}

本文作者:Jasper08

本文链接:https://www.cnblogs.com/Jasper08/p/17461503.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Jasper08  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
🔑