8.2 KiB
C++ 算法讲义:贪心算法(上)
1. 引言
1.1 问题引入:如何使得资源分配最优化
在很多领域,我们频繁面对资源受限下的最优化问题。一个经典的抽象模型是:给定一组物品,每种物品都具有特定的价值和成本(例如重量、体积或时间消耗),以及一个总的成本预算(例如背包容量)。目标是在不超过预算的前提下,选取一部分或全部物品,使得总价值最大化。
这个问题,被称为背包问题,是运筹学和算法理论中的一个核心问题。根据物品是否可分割,它又分为两种主要变体。我们从其中更适合贪心策略的部分背包问题入手。
1.2 贪心算法的地位与应用
贪心算法是一种基础而强大的算法。它代表了一种简洁、高效的解决问题的世界观。其核心在于:
- 高效性:相较于需要穷举所有可能性的暴力搜索算法,贪心算法通常具有多项式
O(n+m)时间复杂度,实现高效。在处理大规模数据时,这种效率优势尤为关键。 - 基础性:许多图论和组合优化领域的基础算法都根植于贪心思想。例如,计算图的最小生成树的 Prim 算法和 Kruskal 算法,以及计算单源最短路径的 Dijkstra 算法,其核心都包含了一个贪心选择的过程,我们以后就将会学习二叉树与图论。
- 启发性算法:即便在贪心策略无法保证得到全局最优解的复杂问题(NP-hard 问题)中,贪心算法也常被用作一种有效的近似算法或启发式方法,就算得不到最优解,也能够快速给出一个质量较高的可行解。
2. 核心思想
2.1 问题引入:部分背包问题(例12-1 OJ:B0174)
题目描述:
阿里巴巴走进了装满宝藏的藏宝洞。藏宝洞里面有 N(N \le 100) 堆金币,第 i 堆金币的总重量和总价值分别是 $m_i,v_i(1\le m_i,v_i \le 100)$。阿里巴巴有一个承重量为 T(T \le 1000) 的背包,但并不一定有办法将全部的金币都装进去。他想装走尽可能多价值的金币。所有金币都可以随意分割,分割完的金币重量价值比(也就是单位价格)不变。请问阿里巴巴最多可以拿走多少价值的金币?
面对部分背包问题,即物品可被分割,我们的目标是最大化总价值。直觉上,存在多种可能的“贪心”标准:
- 价值最高优先:优先装入总价值最高的物品。此策略的缺陷是显而易见的,一个价值极高但重量巨大的物品可能会占据所有容量,使得总价值远低于组合多个小物品的情况。
- 重量最轻优先:优先装入重量最轻的物品,以期装入更多数量的物品。此策略同样存在问题,因为它完全忽略了物品的价值,可能会导致背包被大量低价值物品填满。
上述直觉性策略都是不成立了,于是我们应该寻找一个正确的贪心标准。
对于部分背包问题,一个更合理的标准应该是衡量单位重量所能提供的价值,即价值密度(性价比)。
2.2 贪心算法的性质
定义:贪心算法在求解问题的每一步中,都做出一个局部最优的选择。它寄希望于一系列的局部最优选择能够产生一个全局最优解。
一个问题能够使用贪心算法求解,通常需要满足以下两个关键性质:
-
无后效性 即某个状态以前的过程不会影响以后的状态,只与当前状态有关。换句话说,我们做出的一个贪心选择,只会将原问题简化为一个规模更小的、同性质的子问题,而我们此前的选择并不会影响这个子问题的最优解。也就是一个全局最优解可以通过一系列局部最优的选择来达到。
-
最优子结构 一个问题的最优解包含了其子问题的最优解。当我们做出一个贪心选择后,原问题就简化为了一个规模更小的、性质相同的子问题。例如,当我们将一个物品装入背包后,剩下的问题就变成了:使用剩余的背包容量,在剩余的物品中获取最大价值。这个子问题的解,结合我们已做出的选择,将构成原问题的解。
一个贪心策略的正确性必须经过严格的数学证明,通常使用“交换论证法”(也叫反证法)来证明。即假设存在一个不采用贪心选择的最优解,通过一系列交换操作,可以将其转化为一个采用贪心选择且结果不劣于原解的方案,从而证明贪心选择的正确性。
3. 实例:部分背包问题
3.1 案例描述
给定一个容量为 W 的背包和 n 个物品。第 i 个物品的重量为 weight[i],价值为 value[i]。物品可以被分割。求解如何装包使得总价值最大。
样例输入:
- 背包容量
W = 50 - 物品 (weight, value):
- Item 1: (10, 60)
- Item 2: (20, 100)
- Item 3: (30, 120)
3.2 思路展示与推演
- 贪心标准:选择价值密度
value/weight最高的物品。 - 计算并排序:
- Item 1: 密度 = 60 / 10 = 6.0
- Item 2: 密度 = 100 / 20 = 5.0
- Item 3: 密度 = 120 / 30 = 4.0 排序后的顺序为:Item 1 > Item 2 > Item 3。
- 决策过程:
- 处理 Item 1:背包剩余容量为50。Item 1重量为10,可完全装入。
- 装入10kg的Item 1。
- 背包更新:容量
50 - 10 = 40。总价值+ 60。
- 处理 Item 2:背包剩余容量为40。Item 2重量为20,可完全装入。
- 装入20kg的Item 2。
- 背包更新:容量
40 - 20 = 20。总价值+ 100。
- 处理 Item 3:背包剩余容量为20。Item 3重量为30,无法完全装入。
- 只能装入剩余容量允许的部分,即20kg的Item 3。
- 背包更新:容量
20 - 20 = 0。总价值+ (20/30) * 120 = 80。
- 处理 Item 1:背包剩余容量为50。Item 1重量为10,可完全装入。
- 最终结果:
- 背包已满。总价值为
60 + 100 + 80 = 240。
- 背包已满。总价值为
4. 代码实现
为了实现上述逻辑,我们通常定义一个结构体来表示物品,然后使用C++标准库的 sort 配合自定义比较函数来完成排序。
4.1 关键代码
#include <bits/stdc++.h>
using namespace std;
// 定义物品结构体
struct Item {
int m; // 重量
int v; // 价值
double ratio; // 价值密度 (性价比)
};
// 自定义比较函数,用于sort排序,按价值密度降序
bool cmp(const Item& a, const Item& b) {
return a.ratio > b.ratio;
}
int main() {
int n;
double t; // 背包容量可能需要参与浮点数运算,用double更方便
cin >> n >> t;
vector<Item> items(n);
for (int i = 0; i < n; ++i) {
cin >> items[i].m >> items[i].v;
// 计算价值密度,注意要进行浮点数除法
items[i].ratio = (double)items[i].v / items[i].m;
}
// 按价值密度从高到低排序
sort(items.begin(), items.end(), cmp);
double total_value = 0.0;
// 遍历排序后的物品
for (int i = 0; i < n; ++i) {
// 如果背包容量为0,提前结束
if (t == 0) {
break;
}
// 如果能装下整个物品
if (t >= items[i].m) {
total_value += items[i].v;
t -= items[i].m;
} else { // 如果只能装下部分物品
// 装入部分物品,价值 = 剩余容量 * 价值密度
total_value += t * items[i].ratio;
t = 0; // 背包已满
}
}
// 按格式输出,保留两位小数
cout << fixed << setprecision(2) << total_value << endl;
return 0;
}
4.2 代码解读
struct Item:封装了物品的重量,价值等属性。cmp/sort:这是贪心策略的实现。我们通过提供一个自定义比较函数,让sort算法按照我们定义的贪心标准(价值密度降序)来排列物品。- 循环:循环体内的
if-else结构实现了贪心决策。首先尝试完整地装入一个物品;如果容量不足,则计算能装入的部分,并将其价值计入总和,随后由于背包已满,循环提前终止。