贪心
大约 6 分钟
贪心
贪心算法
贪心算法是一种常见的解决优化问题的算法,其基本思想是在问题的每个决策阶段,都选择当前看起来最优的选择,即贪心地做出局部最优的决策,以期获取全局最优解。贪心算法简洁且高效,在许多实际问题中都有着广泛的应用。
贪心算法和动态规划都常用于解决优化问题,它们之间存在着一些相似之处,比如都依赖最优子结构。
- 动态规划会根据过去阶段的所有决策来考虑当前决策,并使用过去子问题的解来构建当前子问题的解。
- 贪心算法不会考虑过去的决策,而是一路向前地进行贪心选择,不断缩小问题范围,直到问题被解决。
给定n中硬币,第i中硬币面值为coins[i-1],目标金额为amt,每种硬币都可被重复选取,问能凑出目标金额的最少硬币数量,若无法凑出,返回-1。
/* 凑硬币 */
// 前置条件:coins升序
function cointChangeGreedy(coins = [], amt = 0) {
if (amt === 0) return -1;
let i = coins.length - 1;
// 硬币数量
count = 0;
while (amt > 0) {
// 找到小于且最接近剩余金额的硬币
while (i > 0 && coins[i] > amt) {
i -= 1;
}
// 选取coint
amt -= coins[i];
count += 1;
}
return count;
}
let res = cointChangeGreedy([1, 5, 10, 20], 88);
console.log(res);
贪心算法的优点与局限性
贪心算法不仅操作直接、实现简单,而且通常效率较高。然而对于某些硬币面值组合,贪心算法并不能找到最优解,例如下列:
- 正例:coins=[1,5,10,20,50,100],在该硬币组合下任意的amt都能找到最优解。
- 反例:coints=[1,20,50],当amt为60时贪心算法会给出50+1x10的组合,共计11枚,但动态规划可以找到最优解20x3,仅需三枚。
- 反例:coins=[1,49,50],当amt为98时,贪心组合为50+1x49,而动态规划则为49x2.
也就是说对于零钱兑换问题,贪心算法无法保证找到全局最优解,并且有可能找到非常差的解。一般情况下,贪心算法的适用情况分以下两种:
- 可以保证找到最优解:贪心算法在这种情况下往往是最优选择,因为它往往比回溯、动态规划更高效。
- 可以找到近似最优解:贪心算法在这种情况下也是可用的,对于复制的情况来说,寻找最优解往往非常困难,能以高效找到次优解也是不错的。
贪心算法特性
相较于动态规划,贪心算法的适用条件更为苛刻,其主要关注问题的两个性质。
- 贪心选择性质:只有当局部最优选择始终可以导致全局最优解时,贪心算法才能保证获取最优解。
- 最优子结构:原问题的最优解包含子问题的最优解。
贪心算法解题步骤
贪心问题大致可分为三步:
- 问题分析:梳理与理解问题特性,包括状态定义、优化目标和约束条件等。
- 确定贪心策略:确定在每一步中做出贪心选择,这个策略能在每一步减少问题的规模,并最终解决问题。
- 正确性证明:通常需要证明问题具有贪心选择性质和最优子结构。
贪心算法经典例题
- 硬币找零问题:在某些硬币组合下,贪心算法总是可以得到最优解。
- 区间调度问题:假设你有一些任务,每个任务在一段时间内进行,你的目标是完成尽可能多的任务。如果每次都选择结束时间最早的任务,那么贪心算法就可以得到最优解。
- 分数背包问题:给定一组物品和一个载重量,你的目标是选择一组物品,使得重量不超过载重量,且价值最大。如果每次选择性价比最高的物品,那么贪心算法在一些情况下可以得到最优解。
- 股票买卖问题:给定一组股票的历史价格,你可以进行对此买卖,但如果你已经持有股票,那么在卖出之前不能再购买,目标是获取最大利益。
- 霍夫曼编码:霍夫曼编码是一种用于无损数据压缩的贪心算法。通过构建霍夫曼树,每次选择出现频率最低的两个节点合并,最后得到的霍夫曼树的带权路径长度最小。
- Dikjstra算法:它是一种解决给定源订单到其他各顶点的最短路径的贪心算法。
分数背包问题
给定n个物品,第i个物品的重量为wgt[i-1],价值为val[i-1],以及一个容量为cap的背包。每个物品只能选取一次,但是可以选择物品的一部分,价值依据选择的重量比例计算,问在有限的背包容量下如何使得背包内的物品价值最高。
/* 分数背包问题 */
function fractionalPack(wgt = [], val = [], cap = 0) {
// 汇总所有物品
let tings = [];
wgt.map((el, index) => {
let obj = {
w: el, // 重量
v: val[index], // 总价值
unit: val[index] / el, // 单位价值
};
tings.push(obj);
});
// 排序
let sortTings = tings.sort((a, b) => b.unit - a.unit);
console.log(sortTings);
let res = 0; // 总价值
while (cap > 0) {
// 选取单位价值最大的物品进行填充
let max = sortTings[0];
// 直接占满
if (max.w > cap) {
res += cap * max.unit;
cap = 0;
} else {
// 未占满
res += max.v;
cap -= max.w;
sortTings.shift()
}
}
return res;
}
let res = fractionalPack([20, 40, 10, 30, 50], [120, 210, 50, 150, 240], 50);
console.log(res);
最大容量问题
输入一个数组ht,其中每个元素代表一个垂直隔板的高度。数组中的任意两个隔板,以及它们之间的空间可以组成一个容器。容器的容量等于高度和宽度的乘积,其中的高度由较短的隔板控制,宽度是两个隔板的索引之差。请在数组中选择两个隔板,使得组成的容器容量最大,返回最大容量。
思考:只有向内收缩较短隔板,才有可能使得容量变大。
/* 最大容量问题 */
function maxCapcity(ht = []) {
// 定义i,j使其分布数组两端
let i = 0,
j = ht.length - 1;
let res = 0;
while (i < j) {
// 计算容量
let cap = Math.min(ht[i], ht[j]) * (j - i);
// 存储最大
res = Math.max(res, cap);
// 移动隔板
if (ht[i] > ht[j]) {
j--;
} else {
i++;
}
}
return res;
}
let res = maxCapcity([3, 8, 5, 2, 7, 7, 3, 4]);
console.log(res);
最大切分乘积问题
给定一个正整数n,将其切分为至少两个正整数的和,求切分后所有整数的乘积最大是多少?
/* 最大切分乘积:贪心 */
function maxProductCutting(n) {
// 当 n <= 3 时,必须切分出一个 1
if (n <= 3) {
return 1 * (n - 1);
}
// 贪心地切分出 3 ,a 为 3 的个数,b 为余数
let a = Math.floor(n / 3);
let b = n % 3;
if (b === 1) {
// 当余数为 1 时,将一对 1 * 3 转化为 2 * 2
return Math.pow(3, a - 1) * 2 * 2;
}
if (b === 2) {
// 当余数为 2 时,不做处理
return Math.pow(3, a) * 2;
}
// 当余数为 0 时,不做处理
return Math.pow(3, a);
}
let res = maxProductCutting(8)
console.log(res)
Loading...
