火龙信奥
  • 分享
  • 课程
  • 题库
  • 打卡
    • 代码对战
    • 快速对战
  • 题单
  • 团队
  • 荣誉墙
  • 商城
  • 登录 / 注册

贪心算法专题教程

作者: 作者的头像   huolong , 时间:2026-08-21 11:44:25 , 所有人可见, 阅读  48

贪心算法专题教程

一、 从生活实例理解贪心

1.1 找零钱问题

假设你是一家超市的收银员,需要给顾客找零 $36$ 元。你手中有面值为 $20$ 元、$10$ 元、$5$ 元、$1$ 元的纸币。怎样用最少的纸币数量找零?

你可能会这样做: 1. 先拿一张 $20$ 元(剩下 $16$ 元) 2. 再拿一张 $10$ 元(剩下 $6$ 元) 3. 再拿一张 $5$ 元(剩下 $1$ 元) 4. 最后拿一张 $1$ 元

总共用了 $4$ 张纸币。这个想法的核心是:每次都用当前面值最大的纸币。用 $20$ 元比用两张 $10$ 元更省纸币,这种“每次选择当前最好的”策略,就是贪心。

1.2 贪心不一定总是对的

如果把纸币面值换成 $1$ 元、$3$ 元、$4$ 元,要找 $6$ 元: * 贪心策略:先拿 $4$ 元(剩 $2$ 元) $\to$ 再拿 $1$ 元 $\to$ 再拿 $1$ 元 $\to$ 共需 $3$ 张。 * 最优解:$3$ 元 + $3$ 元 $\to$ 共需 $2$ 张。

结论:贪心算法不是万能的!它只适用于某些满足特定性质的问题。

1.3 找假币问题(分治与贪心的结合)

给你一个装有 $16$ 个硬币的袋子,其中有一个是伪造的。已知伪造的硬币比真硬币稍轻。你的任务是找出这个伪造的硬币。提供一台可用来比较两组硬币重量的仪器,通过它可以知道两组硬币的重量是否相同。

  • 方法一(逐个比较):先比较硬币1与硬币2。若硬币1轻,则1是伪币;若硬币2轻,则2是伪币;若相等,则说明都不是伪币,继续比较硬币3和硬币4……按此方式,最多通过 $8$ 次比较即可找出伪币。
  • 方法二(二分法):把 $16$ 个硬币分成两组,每组 $8$ 个。称一次即可确定伪币在哪个组中。再将含有伪币的那组分成两组(每组 $4$ 个)……依此类推,只需 $4$ 次称量就能找出伪币。
  • 方法三(三分法):把 $16$ 个硬币分成三组:A组 $5$ 个、B组 $5$ 个、C组 $6$ 个。称量 A 和 B:
  • 若平衡,伪币在 C 组($6$ 个)。再把 C 组分成 2、2、2 三组,称一次确定在哪组,最后再称 $1$ 次即可找出。最多只需 $3$ 次。
  • 若不平衡,伪币在较轻的一组($5$ 个)。再把该组分成 2、2、1 三组,类似操作,最多也只需 $3$ 次。

这个例子告诉我们:将一个大问题分解成若干个规模较小的相同子问题并逐步解决,是一种重要的算法思想。在每一步都做出当前看起来最优的选择,则是贪心算法的基石。


二、 贪心算法的基本概念

贪心法在解决最优化问题时,从问题的某一个初始解出发,采用逐步构造最优解的方法向给定的目标前进。在每个局部阶段,都做出一个当前看来最优的决策(局部最优解),并期望通过每次所做的局部最优选择产生出一个全局最优解。

做出贪心决策的依据称为贪心准则(策略)。决策一旦做出,就不可再更改。贪心与递推不同,它不是依据某一固定的递推式推进,而是做一个当时看似最佳的贪心选择,不断地将问题实例归纳为更小的相似子问题。因此,归纳、分析和选择正确合适的贪心准则是关键。

贪心算法在每一层递归或迭代上都有以下决策步骤: 1. 建立数学模型:明确问题的输入、输出和约束条件。 2. 分解子问题:将原问题分解成若干个子问题。 3. 贪心选择:在每个子问题中做出局部最优选择,然后更新状态。

2.1 贪心算法的两个核心性质

一个问题能用贪心算法求解,通常需要满足以下两个性质:

  1. 贪心选择性质:问题的整体最优解可以通过一系列局部最优的贪心选择达到。这是贪心法可行的第一个基本要素,也是它与动态规划的主要区别。
  2. 区别:在动态规划中,每步决策往往依赖于相关子问题的解,因此必须先解出子问题。而贪心算法是先做出局部最优选择,再去解决选择后产生的相应子问题。贪心算法可以依赖以往的选择,但决不依赖将来的选择或子问题的解。
  3. 最优子结构性质:当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。

2.2 贪心算法与动态规划算法的比较

虽然二者都要求具有最优子结构性质,但它们不能混用。例如,0/1 背包问题和部分背包问题都具有最优子结构,但前者不能用贪心(因为存在空间浪费),而必须用动态规划;后者则能用贪心求得最优解。

对比项 贪心算法 动态规划
决策方式 只考虑当前,做出局部最优选择 考虑所有可能,保留并递推多种状态
子问题关系 沿单方向进行,子问题独立或顺次解决 需要求解并组合所有子问题的解
效率 通常更快($O(n \log n)$ 或 $O(n)$) 较慢($O(n^2)$ 或更高)
适用范围 满足贪心选择性质的问题 有最优子结构和重叠子问题的问题
是否保证最优 不一定(需严格证明) 一定能得到最优解

三、 删数问题

问题描述 键盘输入一个高精度的正整数 $n$($\le 240$ 位),去掉其中任意 $s$ 个数字后,剩下的数字按原左右次序组成一个新的正整数。 编程对给定的 $n$ 和 $s$,寻找一种方案,使得剩下的数字组成的新数最小。

  • 输入:$n$ 和 $s$。
  • 输出:最后剩下的最小数。
  • 输入样例: text 178543 4
  • 输出样例: text 13

3.1 问题分析

由于 $n$ 的有效位数高达 $240$ 位,我们需要采用字符串存贮。

如何确定删除哪 $s$ 位?直接删除最大的 $s$ 个数字显然是错误的(例如 $n = 129$, $s = 1$,删最大数 $9$ 剩 $12$;但若删 $2$ 剩 $19$,删 $1$ 剩 $29$。最优解是删除首位 $1$ 得到 $29$ 还是删 $9$ 得到 $12$?此例中删 $9$ 较小,但若是 $n = 912$, $s = 1$,删最大数 $9$ 剩 $12$,比删 $2$ 剩 $91$ 更好)。

为了让剩下的数最小,我们应尽量让高位数字更小。 贪心策略:每一步总是选择一个使剩下的数最小的数字删去。即按高位到低位的顺序搜索,若各位数字递增,则删除最后一个数字;否则删除第一个递减区间的首字符(即若 $n[i] > n[i+1]$,则删除 $n[i]$)。

实例演示:$n = 178543, s = 4$

n = 1 7 8 5 4 3
      ↓
第一步:搜索发现 7 < 8 递增,而 8 > 5 下降。删除首个下降位置的前驱 8 → 1 7 5 4 3
第二步:1 7 5 4 3,搜索发现 7 > 5 下降。删除 7 → 1 5 4 3
第三步:1 5 4 3,搜索发现 5 > 4 下降。删除 5 → 1 4 3
第四步:1 4 3,搜索发现 4 > 3 下降。删除 4 → 1 3

最终结果为 13。

注意事项: 1. 每删完一个数字后,前面的数字可能会和后面的数字形成新的递减关系,因此每次删除后要回到串首重新扫描(或使用单调栈优化)。 2. 删除后可能会产生前导零,例如 105 删去 1 后变成 05,需要特殊处理,去除无用的前导零(除非整个数字就是 0)。

3.2 算法框架 (Pascal 风格伪代码)

输入 n,s;

while s > 0 do
begin
    i := 1;                    {从串首开始找}
    while (i < length(n)) and (n[i] <= n[i+1]) do
        i := i + 1;
    delete(n, i, 1);           {删除字符串 n 的第 i 个字符}
    s := s - 1;
end;

while (length(n) > 1) and (n[1] = '0') do
    delete(n, 1, 1);           {删去串首可能产生的前导零}

输出 n;

3.3 C++ 代码实现

#include <iostream>
#include <string>
using namespace std;

int main() {
    string n;
    int s;
    if (!(cin >> n >> s)) return 0;

    while (s > 0) {
        int i = 0;
        // 从串首开始寻找第一个下降的位置
        // 注意:i < (int)n.length() - 1 确保不越界
        while (i < (int)n.length() - 1 && n[i] <= n[i + 1]) {
            i++;
        }
        // 删除第 i 个字符
        n.erase(i, 1);
        s--;
    }

    // 去掉前导零
    while (n.length() > 1 && n[0] == '0') {
        n.erase(0, 1);
    }

    cout << n << endl;

    return 0;
}

四、 取数游戏

问题描述 给出 $2 \times n$($n \le 100$)个自然数(数小于等于 $30000$)。游戏双方分别为 A 方(计算机)和 B 方(玩家)。 规则:只允许从数列两端取数。A 先取,然后双方依次轮流。取完时,取得的数字总和最大者获胜;双方和相等则算 A 胜。 问:A 方是否存在必胜策略?

  • 输入:$n$ 以及 $2 \times n$ 个自然数。
  • 输出:共 $3 \times n + 2$ 行,展示游戏过程(每 3 行为 A 的选择、B 的选择提示及 B 的实际选择),最后输出双方的总得分。

4.1 问题分析

设 $n = 4$,数列为:7 9 3 6 4 2 5 3。

如果设计一种简单的局部贪心策略——“A 每次都取两端中较大的数”,B 也同样聪明: * A 取左端 7(剩余 9 3 6 4 2 5 3)。 * B 取两端中较大的 9(剩余 3 6 4 2 5 3)。 * A 接着只能在两端取较大值…… 以此推导,A 最终可能落败。

然而,我们发现了一个有趣的规律: 初始时有 $2n$ 个数,奇数位置和偶数位置上的数各有 $n$ 个。 * 如果 A 第一次取走了奇数位置的数(即第 $1$ 个数),那么剩下数列的两端就都处于偶数位置。 * B 无论取左端还是右端,取走的必定是一个偶数位置的数。 * B 取完后,剩下数列的两端又都变回了奇数位置。 * 依此类推,A 总能控制并取走所有奇数位置的数,或者所有偶数位置的数。

全局贪心策略: A 提前计算所有奇数位置的数之和与所有偶数位置的数之和,并选择和较大的那组。这样 A 便有必胜策略。

4.2 C++ 代码实现

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n;
    if (!(cin >> n)) return 0;
    vector<int> a(2 * n + 1);  // 下标从 1 开始

    for (int i = 1; i <= 2 * n; i++) {
        cin >> a[i];
    }

    // 计算奇数位置和偶数位置的和
    int sumOdd = 0, sumEven = 0;
    for (int i = 1; i <= n; i++) {
        sumOdd += a[2 * i - 1];
        sumEven += a[2 * i];
    }

    int j;  // j = 1 表示取奇数位置,j = 0 表示取偶数位置
    int SA, SB;
    if (sumOdd >= sumEven) {
        j = 1;
        SA = sumOdd;
        SB = sumEven;
    } else {
        j = 0;
        SA = sumEven;
        SB = sumOdd;
    }

    int lp = 1, rp = 2 * n;

    for (int i = 1; i <= n; i++) {
        // A 方根据策略取数
        if ((j == 1 && lp % 2 == 1) || (j == 0 && lp % 2 == 0)) {
            cout << "A: " << a[lp] << endl;
            lp++;
        } else {
            cout << "A: " << a[rp] << endl;
            rp--;
        }

        // B 方取数
        char ch;
        cout << " B=L/R ? ";
        do {
            cin >> ch;
            if (ch == 'L') {
                cout << "B: " << a[lp] << endl;
                lp++;
            } else if (ch == 'R') {
                cout << "B: " << a[rp] << endl;
                rp--;
            }
        } while (ch != 'L' && ch != 'R');
    }

    cout << "SA = " << SA << endl << "SB = " << SB << endl;

    return 0;
}

五、 活动选择问题

问题描述 假设有一个需要使用某一资源的 $n$ 个活动组成的集合 $S = {1, 2, \dots, n}$。该资源一次只能被一个活动占用。 每一个活动有一个开始时间 $b_i$ 和结束时间 $e_i$($b_i \le e_i$)。若 $b_i \ge e_j$ 或 $b_j \ge e_i$,则活动 $i$ 和活动 $j$ 兼容。 你的任务是:选择由互相兼容的活动组成的最大集合。

  • 输入格式: text n b1 e1 ... bn en
  • 输出格式: 最大集合中选中的活动序号,以及最大兼容活动数。
  • 样例输入: text 11 3 5 1 4 12 14 8 12 0 6 8 11 6 10 5 7 3 8 5 9 2 13
  • 样例输出: text 14 2 3 6 8 (注:样例输出的第一行 14 为最后选中活动的最大结束时间/总占用时刻,第二行为选中的原始活动序号。)

5.1 问题分析

贪心策略: 为了能在有限的时间内安排尽可能多的活动,我们每次都应该选择结束时间最早的活动,从而为后续活动留出尽可能多的时间。

按结束时间递增对活动进行排序: $$e_1' \le e_2' \le \dots \le e_n'$$

样例数据排序表

活动序号 $i$ 开始时间 $b_i$ 结束时间 $e_i$ 排序后的序号 $j$
1 3 5 2
2 1 4 1
3 12 14 11
4 8 12 9
5 0 6 3
6 8 11 8
7 6 10 7
8 5 7 4
9 3 8 5
10 5 9 6
11 2 13 10

贪心选择过程模拟

时间轴:0  1  2  3  4  5  6  7  8  9  10 11 12 13 14

活动 2 (1-4): 选,当前结束时间 = 4
活动 1 (3-5): 开始 3 < 4,冲突,放弃
活动 5 (0-6): 开始 0 < 4,冲突,放弃
活动 8 (5-7): 开始 5 ≥ 4,兼容,选,当前结束时间 = 7
活动 9 (3-8): 开始 3 < 7,冲突,放弃
活动 10(5-9): 开始 5 < 7,冲突,放弃
活动 7 (6-10):开始 6 < 7,冲突,放弃
活动 6 (8-11):开始 8 ≥ 7,兼容,选,当前结束时间 = 11
活动 4 (8-12):开始 8 < 11,冲突,放弃
活动 11(2-13):开始 2 < 11,冲突,放弃
活动 3 (12-14):开始 12 ≥ 11,兼容,选,当前结束时间 = 14

最终选中的活动为:{2, 8, 6, 3}。

5.2 贪心选择性质的证明(交换论证法)

  • 证明: 设贪心算法得到的解为 $G = {g_1, g_2, \dots, g_k}$,最优解为 $O = {o_1, o_2, \dots, o_m}$,二者均按结束时间递增排列。我们要证明 $k = m$。

因为 $g_1$ 是所有活动中结束时间最早的,所以有 $e(g_1) \le e(o_1)$。 * 若 $g_1 = o_1$,则问题归约为剔除该活动后的子问题。 * 若 $g_1 \neq o_1$,构造一个新解 $O' = {g_1, o_2, \dots, o_m}$。由于 $e(g_1) \le e(o_1)$,且 $o_1$ 与 $o_2$ 兼容(即 $b(o_2) \ge e(o_1)$),所以必然有 $b(o_2) \ge e(g_1)$,即 $g_1$ 与 $o_2, \dots, o_m$ 均兼容。因此 $O'$ 是一个合法的可行解,且 $|O'| = |O| = m$。

这说明用贪心选择 $g_1$ 替换最优解的第一个元素,得到的解仍然是最优解。通过逐步替换,可将 $O$ 转换为 $G$ 且大小不变,即证明了贪心解就是最优解。

5.3 C++ 代码实现

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Activity {
    int id, start, finish;
};

bool cmp(const Activity& a, const Activity& b) {
    return a.finish < b.finish;  // 按结束时间升序排序
}

int main() {
    int n;
    if (!(cin >> n)) return 0;
    vector<Activity> acts(n);

    for (int i = 0; i < n; i++) {
        acts[i].id = i + 1;
        cin >> acts[i].start >> acts[i].finish;
    }

    sort(acts.begin(), acts.end(), cmp);

    vector<int> selected;
    int lastFinish = -1;
    int totalTime = 0;

    for (int i = 0; i < n; i++) {
        if (acts[i].start >= lastFinish) {
            selected.push_back(acts[i].id);
            lastFinish = acts[i].finish;
            totalTime = acts[i].finish;
        }
    }

    cout << totalTime << endl;
    for (int i = 0; i < (int)selected.size(); i++) {
        cout << selected[i] << (i == (int)selected.size() - 1 ? "" : " ");
    }
    cout << endl;

    return 0;
}

六、 部分背包问题

问题描述 有一个容量为 $W$ 的背包和 $n$ 件物品。第 $i$ 件物品重量为 $w_i$,价值为 $v_i$。 物品可以分割(例如金粉,可以只拿走一部分)。如何装入才能使总价值最大?

6.1 问题分析

部分背包问题与 0/1 背包问题的核心区别在于:物品可分割。 因此,我们可以采用性价比(单位重量价值 $v_i/w_i$)降序的贪心策略。每次都尽量多地装入当前性价比最高的物品,直到背包装满。

6.2 C++ 代码实现

#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>
using namespace std;

struct Item {
    double weight, value;
};

bool cmp(const Item& a, const Item& b) {
    return (a.value / a.weight) > (b.value / b.weight);  // 性价比降序
}

int main() {
    int n;
    double W;
    if (!(cin >> n >> W)) return 0;
    vector<Item> items(n);

    for (int i = 0; i < n; i++) {
        cin >> items[i].weight >> items[i].value;
    }

    sort(items.begin(), items.end(), cmp);

    double ans = 0;
    double remaining = W;

    for (int i = 0; i < n && remaining > 0; i++) {
        if (items[i].weight <= remaining) {
            // 可以完整装下
            ans += items[i].value;
            remaining -= items[i].weight;
        } else {
            // 只能装下一部分
            ans += (items[i].value / items[i].weight) * remaining;
            break;
        }
    }

    cout << fixed << setprecision(2) << ans << endl;

    return 0;
}

6.3 为什么 0/1 背包不能用贪心?

0/1 背包问题中,物品不可分割。如果盲目使用性价比贪心,会因为“空间无法完美填满”而导致浪费,最终使得总价值降低。 * 反例:背包容量 $W = 30$。 * 物品 A:重 $20$,价值 $40$(性价比 = $2.0$) * 物品 B:重 $15$,价值 $25$(性价比 = $1.67$) * 物品 C:重 $15$,价值 $25$(性价比 = $1.67$) * 贪心策略:先装 A,余下容量 $10$,无法再装 B 或 C。总价值为 $40$。 * 最优解:装 B 和 C,总重量 $30$。总价值为 $50$。


七、 哈夫曼编码(合并果子)

问题描述 有 $n$ 堆果子,每次可以合并任意两堆。合并所需的体力消耗等于两堆果子的重量之和。 求将所有果子合并为一堆所需的最小总体力消耗。

  • 样例输入: text 3 1 2 9
  • 样例输出: text 15 (解释:先合并 1 和 2 消耗 3,得到重量为 3 的新堆;再合并 3 和 9 消耗 12。总消耗 = 3 + 12 = 15)

7.1 问题分析

这是经典的哈夫曼树构造问题。 贪心策略:每次都选择当前重量最小的两堆进行合并。

交换论证法证明

设最优解中第一次合并的不是最小的两堆 $x$ 和 $y$(设 $x \le y$),而是 $x$ 和某个更大的堆 $z$($z \ge y$)。 如果在决策树中交换 $y$ 和 $z$ 的位置,合并 $x$ 与 $y$ 的代价为 $x + y$,小于或等于合并 $x$ 与 $z$ 的代价 $x + z$。并且由于 $y$ 处于更深的叶子节点,总代价不会增加,甚至可能变小。因此,每次合并最小的两堆一定是最优的。

7.2 C++ 代码实现

#include <iostream>
#include <queue>
#include <vector>
using namespace std;

int main() {
    int n;
    if (!(cin >> n)) return 0;

    // 小顶堆:自动维护并输出最小值
    priority_queue<long long, vector<long long>, greater<long long>> pq;

    for (int i = 0; i < n; i++) {
        long long x;
        cin >> x;
        pq.push(x);
    }

    long long ans = 0;
    while (pq.size() > 1) {
        long long a = pq.top(); pq.pop();
        long long b = pq.top(); pq.pop();
        long long sum = a + b;
        ans += sum;
        pq.push(sum);
    }

    cout << ans << endl;

    return 0;
}
  • 时间复杂度:使用优先队列,每次操作时间复杂度为 $O(\log n)$,共合并 $n-1$ 次,总体时间复杂度为 $O(n \log n)$。

八、 国王游戏

问题描述 国王和 $n$ 位大臣的左、右手各写有一个数字。他们排成一排,国王排在最前面。 每一位大臣获得的奖励为:他前面所有人左手数字的乘积 除以 他自己右手上的数字。 $$\text{奖励} = \frac{\prod_{j=0}^{i-1} a_j}{b_i}$$ 其中国王的左手数字为 $a_0$,右手为 $b_0$(国王不参与奖励计算)。 问如何排列大臣,使得获得奖励最多的大臣拿到的奖励最少?

8.1 问题分析(邻项交换法)

设排在相邻位置的两位大臣为 $i$ 和 $j$。设他们前面所有人的左手乘积为 $P$。

方案 1:$i$ 在前,$j$ 在后

  • 大臣 $i$ 的奖励:$R_i = \frac{P}{b_i}$
  • 大臣 $j$ 的奖励:$R_j = \frac{P \times a_i}{b_j}$
  • 两人中的最大奖励:$\max\left(\frac{P}{b_i}, \frac{P \times a_i}{b_j}\right)$

方案 2:$j$ 在前,$i$ 在后

  • 大臣 $j$ 的奖励:$R_j' = \frac{P}{b_j}$
  • 大臣 $i$ 的奖励:$R_i' = \frac{P \times a_j}{b_i}$
  • 两人中的最大奖励:$\max\left(\frac{P}{b_j}, \frac{P \times a_j}{b_i}\right)$

若想让方案 1 优于方案 2,只需满足: $$\max\left(\frac{P}{b_i}, \frac{P \times a_i}{b_j}\right) \le \max\left(\frac{P}{b_j}, \frac{P \times a_j}{b_i}\right)$$

两边同除 $P$ 并乘 $b_i b_j$,得: $$\max(b_j, a_i b_i) \le \max(b_i, a_j b_j)$$

因为 $a_i, b_i \ge 1$,显然有 $a_i b_i \ge b_i$ 且 $a_j b_j \ge b_j$。 要使上式成立,只需: $$a_i b_i \le a_j b_j$$

贪心策略:按 左、右手数字的乘积 $a_i \times b_i$ 从小到大 进行升序排序。

8.2 C++ 高精度代码实现

由于乘积非常大,会超出 64 位整型的范围,需要配合高精度计算。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Minister {
    int left, right;
};

bool cmp(const Minister& a, const Minister& b) {
    return (long long)a.left * a.right < (long long)b.left * b.right;
}

struct BigInt {
    vector<int> digits;  // 低位在前

    BigInt() {}
    BigInt(int x) {
        if (x == 0) { digits.push_back(0); return; }
        while (x > 0) {
            digits.push_back(x % 10);
            x /= 10;
        }
    }

    BigInt multiply(int x) const {
        BigInt res;
        res.digits.clear();
        int carry = 0;
        for (size_t i = 0; i < digits.size() || carry; i++) {
            long long cur = carry;
            if (i < digits.size()) {
                cur += (long long)digits[i] * x;
            }
            res.digits.push_back(cur % 10);
            carry = cur / 10;
        }
        while (res.digits.size() > 1 && res.digits.back() == 0) {
            res.digits.pop_back();
        }
        return res;
    }

    BigInt divide(int x) const {
        BigInt res;
        res.digits.clear();
        long long remainder = 0;
        for (int i = (int)digits.size() - 1; i >= 0; i--) {
            remainder = remainder * 10 + digits[i];
            res.digits.push_back(remainder / x);
            remainder %= x;
        }
        reverse(res.digits.begin(), res.digits.end());
        while (res.digits.size() > 1 && res.digits.back() == 0) {
            res.digits.pop_back();
        }
        return res;
    }

    bool lessThan(const BigInt& other) const {
        if (digits.size() != other.digits.size()) {
            return digits.size() < other.digits.size();
        }
        for (int i = (int)digits.size() - 1; i >= 0; i--) {
            if (digits[i] != other.digits[i]) {
                return digits[i] < other.digits[i];
            }
        }
        return false;
    }

    void print() const {
        for (int i = (int)digits.size() - 1; i >= 0; i--) {
            cout << digits[i];
        }
    }
};

int main() {
    int n;
    if (!(cin >> n)) return 0;

    int kingLeft, kingRight;
    cin >> kingLeft >> kingRight;

    vector<Minister> ministers(n);
    for (int i = 0; i < n; i++) {
        cin >> ministers[i].left >> ministers[i].right;
    }

    sort(ministers.begin(), ministers.end(), cmp);

    BigInt product(kingLeft);
    BigInt maxReward(0);

    for (int i = 0; i < n; i++) {
        BigInt reward = product.divide(ministers[i].right);
        if (maxReward.lessThan(reward)) {
            maxReward = reward;
        }
        product = product.multiply(ministers[i].left);
    }

    maxReward.print();
    cout << endl;

    return 0;
}

九、 雇佣计划

问题描述 经理需要确定每个月雇佣的工人数,已知每月最少需要的工人数。 * 雇佣新工人费用为 $h$/人,解雇工人费用为 $f$/人。 * 工人在职期间,即使不干活也需要支付工资 $s$/月/人。 求 $n$ 个月控制总费用的最低方案。

9.1 问题分析与贪心思想

当月实际工人数 now 必须满足当月最小需求 need[i]。

  1. 若人数不足 (need[i] > now):必须新雇佣 $need[i] - now$ 人,使人数恰好达到最低限度。
  2. 若人数多余 (need[i] < now):是否立即解雇多余的工人?
  3. 考虑:如果未来几个月内还需要同样多的人手,解雇后再重新雇佣的费用 $f + h$ 可能大于维持多余人手所支付的额外工资。
  4. 贪心策略:向后搜索未来月份。若保留这些人付出的额外工资,小于解雇并重新雇佣的费用,则选择不解雇,维持当前人数。

9.2 C++ 代码实现

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    int n;
    if (!(cin >> n)) return 0;

    int h, s, f;
    cin >> h >> s >> f;

    vector<int> need(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> need[i];
    }

    int mincost = 0;
    int now = 0;

    for (int i = 1; i <= n; i++) {
        if (need[i] > now) {
            // 人手不足,必须雇佣
            mincost += h * (need[i] - now);
            now = need[i];
        } else if (need[i] < now) {
            // 人手多余,决策是否解雇
            bool keep = false;
            for (int j = i + 1; j <= n; j++) {
                if (need[j] >= now) {
                    keep = true;
                    break;
                }
                if (need[j] >= need[i]) {
                    // 额外工资总额 vs 解雇与重新雇佣成本
                    int extraWage = s * (j - i) * (now - need[j]); 
                    int costFireHire = (f + h) * (now - need[j]);
                    if (costFireHire > extraWage) {
                        keep = true;
                        break;
                    }
                }
            }
            if (!keep) {
                // 解雇多余人手至当月最低限度
                mincost += f * (now - need[i]);
                now = need[i];
            }
        }
        mincost += now * s; // 支付当月工资
    }

    cout << mincost << endl;
    return 0;
}

十、 贪心算法在树和图论中的应用

图论中许多经典的高效算法都深度应用了贪心思想。

10.1 最小生成树 (MST)

  • Prim 算法:从任意顶点开始逐步向外扩展树。每一步选择一条连接树内顶点与树外顶点中权重最小的边。
  • Kruskal 算法:将所有边按权重从小到大排序,每次选择不与已选边构成环的权值最小的边。
// Kruskal 算法核心贪心选择
sort(edges.begin(), edges.end(), cmp);
for (Edge e : edges) {
    int fu = find(e.u), fv = find(e.v);
    if (fu != fv) {
        parent[fu] = fv;
        ans += e.w;
        cnt++;
        if (cnt == n - 1) break;
    }
}

10.2 单源最短路径 (Dijkstra 算法)

用于求解无负权图的单源最短路径。 * 贪心策略:每次从未确定最短路径的节点中,选择一个当前距离源点最近的节点 $u$,加入已确定集合,并用它更新相邻节点的距离(松弛操作)。

// Dijkstra 算法核心贪心选择
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;
vector<int> dist(n+1, INF);
dist[s] = 0;
pq.push({0, s});

while (!pq.empty()) {
    auto [d, u] = pq.top(); pq.pop();
    if (d > dist[u]) continue;
    for (auto [v, w] : g[u]) {
        if (dist[v] > dist[u] + w) {
            dist[v] = dist[u] + w;
            pq.push({dist[v], v});
        }
    }
}

十一、 小结

11.1 贪心算法的基本要素

  1. 贪心选择性质:可以通过一系列局部最优决策达到全局最优。
  2. 最优子结构性质:问题的最优解包含其子问题的最优解。

11.2 证明方法

由于贪心策略往往伴随着直觉,在设计出算法后,通常需要进行合理性证明: * 交换论证法:假设存在一个最优解,证明将其中的非贪心决策替换为贪心决策后,解不会变差。 * 数学归纳法:适用于阶段性递推明显的问题。 * 反例构造法:在思考贪心准则时,多尝试构造小规模的极限数据来推翻错误的直觉。

11.3 常见贪心模型与策略总结

经典题型 贪心决策
区间调度(选最多不相交区间) 按右端点升序排序,每次选最早结束的
区间覆盖(用最少区间覆盖目标) 按左端点升序排序,每次选能延伸最远的
排队等待时间最小 按处理/服务时间升序排序
删数问题 从左往右,删去第一个下降趋势的前驱数字
哈夫曼编码(合并果子) 每次选择权值最小的两堆进行合并
部分背包问题 按单位重量的价值(性价比)降序选择
国王游戏 按 $a_i \times b_i$(左手 $\times$ 右手)升序排序

—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com

关于火龙

  • 关于我们
  • 学员获奖
  • 预约试听
  • ACM课程
  • CSP课程
  • 学习指南

帮助中心

  • 用户协议
  • 打字练习
  • 在线画图
  • DevC++下载
  • CSP报名
  • GESP官网

推荐课程

  • C++零基础入门(可试看)
  • C++进阶提升
  • GESP考级辅导
  • GESP打卡
  • CSP-J/S打卡

公众号

火龙信奥公众号二维码

地址:义乌市北门街188号新天地商厦二楼2F 邮箱:wdlok305@126.com

© 2017-2026 义乌市睿码科技有限公司版权所有 浙ICP备2021013995号

火龙信奥
请输入登录信息


请完成安全验证
验证码底图 滑块
向右拖动滑块完成验证
请输入用户名 / 绑定的手机号码



请输入注册信息(手机号验证码注册)





验证码5分钟有效,60秒内不可重复获取,每日最多3次

微信登录

微信登录二维码

正在生成二维码...

账号已过期,请续期。
去续期

绑定手机号

📱

为了更好地保护您的账号安全,享受完整的平台服务

请您尽快绑定手机号码