#M8022. 贪心

贪心

贪心策略

近4年CSP-J初赛考察:

题号 题型 分值
2020 第20题 完善程序 15分
2021

难易度:中等

2024年备考建议

贪心思想是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,如果要得到整个问题的最优答案,那么每一步都要尽可能的得到最优的答案
首先初赛必然无法考察贪心的证明。聚焦在贪心的经典题型,又因为贪心算法,方便与其他知识点关联,比如结构体排序后贪心,比如二分答案里做贪心,所以往往代码量和思维度都适合放在压轴题的位置。
解决初赛中的贪心问题,先要熟悉贪心的常见题型。



常见题型

最常见的贪心有两种。

  • 「我们将 XXX 按照某某顺序排序,然后按某种顺序(例如从小到大)选择。」。

  • 「我们每次都取 XXX 中最大/小的东西,并更新 XXX。」(有时「XXX 中最大/小的东西」可以优化,比如用优先队列维护)

第二种方式基本要用到“优先队列”这种数据结构,在CSP-J的组别里不可能出现,因为只需要准备第一种贪心。

傻瓜贪心

举个栗子 USACO DEC07 超级书架

题意是给定 n 个正整数和一个正整数 k ,问最少选择多少个正整数使得它们之和大于等于 k 。

很显然这类问题的求解,就是排序 + 统计。

//题目的完整代码
#include <bits/stdc++.h>
using namespace std;

int main() {
    //定义 n 表示奶牛的头数, 定义 B 表示书架高度,
    //接下来 读入 n 行整数,每行整数表示 一头奶牛的身高
    int n, B, h[20005];
    cin >> n >> B;
    for (int i = 0; i < n; i++) cin >> h[i];
    
    //一、按照从小到大做排序
    sort(h, h + n);
    
    //二、选择身高前cnt高的牛
    int cnt = 0, sum = 0;
    for (int i = n-1; i >= 0; i--) {  // 遍历所有奶牛(从高的开始)
        sum += h[i];                  // 把第i头牛落到叠的上面
        cnt++;                        // 牛的数量+1
        if (sum >= B) break;          // 如果达到了书架高度 那么就退出循环了
    }
    cout << cnt;
    return 0;
}


哈夫曼树

哈夫曼树是一种二叉树,用于数据压缩。它的构建方式是通过对一个数据集中的元素进行频率统计,然后将频率较小的元素放在树的底部(远端),频率较大的元素放在树的顶部(近端),以此来减小数据的存储空间。

在哈夫曼树中,从根节点到某个叶子节点的路径上的编码,就是该叶子节点所表示字符的哈夫曼编码



构造哈夫曼树

哈夫曼树的构建可以使用贪心算法来实现,时间复杂度为O(nlogn)O(nlogn),其中 n 为数据集中元素的个数。

具体构建方式是,首先将所有元素按照出现频率从小到大排序,然后选取频率最小的两个元素作为左右子节点,将它们的频率相加作为父节点的频率,然后将父节点插入到排序后的元素列表中,重复上述步骤直到只剩下一个根节点为止。最后,从根节点开始递归遍历哈夫曼树,将每个叶子节点所表示的字符及其对应的哈夫曼编码存储起来。

假设有如下8个字符及对应的权值:

字符 题号
A 2
B 3
C 7
D 10
E 11
F 13
G 14
H 20

我们可以按照以下步骤构造哈夫曼树:

  1. 将所有节点按照权值从小到大排序,得到 2,3,7,10,11,13,14,20。
  2. 选取权值最小的两个节点2和3,将它们合并为一个新节点,其权值为2+3=5。这个新节点代表的子树有2和3两个节点。
  3. 将得到的新节点5插入集合中,集合变为 5,7,10,11,13,14,20。
  4. 选取权值最小的两个节点5和7,将它们合并为一个新节点,其权值为5+7=12。这个新节点代表的子树有5和7两个节点。
  5. 将得到的新节点12插入集合中,集合变为 10,11,12,13,14,20。
  6. 选取权值最小的两个节点10和11,将它们合并为一个新节点,其权值为10+11=21。这个新节点代表的子树有10和11两个节点。
  7. 将得到的新节点21插入集合中,集合变为 12,13,14,20,21。
  8. 选取权值最小的两个节点12和13,将它们合并为一个新节点,其权值为12+13=25。这个新节点代表的子树有12和13两个节点。
  9. 将得到的新节点25插入集合中,集合变为 14,20,21,25。
  10. 选取权值最小的两个节点14和20,将它们合并为一个新节点,其权值为14+20=34。这个新节点代表的子树有14和20两个节点。
  11. 将得到的新节点34插入集合中,集合变为 21, 25, 34。
  12. 选取权值最小的两个节点21和25,将它们合并为一个新节点,其权值为21+25=46。这个新节点代表的子树有21和25两个节点。
  13. 将得到的新节点46插入集合中,集合变为 34, 46。
  14. 选取权值最小的两个节点34和46,将它们合并为一个新节点,其权值为34+46=80。这个新节点代表的子树有34和46两个节点。
  15. 哈夫曼树构建完成,根节点的权值为80,左子树的权值为34,右子树的权值为46。

下图为本例的哈夫曼树:

构建完哈夫曼树后,从哈夫曼树的根节点开始,遍历左子树的路径为 0,遍历右子树的路径为 1,则每个叶子节点对应的字符编码为从根节点到该叶子节点经过的路径上的 0 和 1 序列。注意,由于哈夫曼树是一棵前缀编码树,因此每个字符的编码不会是另一个字符编码的前缀。

本例中所有节点对应的编码如下:

字符 权值 编码
A 2 11000
B 3 11001
C 7 111
D 10 100
E 11 101
F 13 111
G 14 00
H 20 01