从自动售货机到钱包优化:揭秘贪心算法的双面人生

191 阅读5分钟

在便利店排队结账时,收银员快速给出找零的动作背后,可能正上演着一场精妙的算法博弈——是追求极致的速度,还是保证绝对的最优?这正是贪心算法与动态规划的本质差异。

虽迟但到,之前写了动态规划计算零钱兑换问题破解零钱兑换难题:从暴力尝试到动态规划的思维跃迁,这里就是同样解决这个问题的贪心兄弟。


一、现实场景中的算法抉择

假设你有一台支持纸币找零的自动售货机,硬币盒中存放着面额为 [100,50,20,10,5,1] 的硬币。当顾客投入131元纸币购买12元的饮料时,机器需要在0.3秒内完成以下决策:

coinChangeGreedy([1,5,10,20,50,100], 131) 
// 输出:100+20+10+1 → 4枚硬币

这种从最大面额开始贪心选择的策略,正是大多数实体场景的选择——它足够,且结果直观。


二、贪心算法的运作机密

2.1 算法核心逻辑

见文知意,贪心就是使用最适合当下的方案(不一定是全局最优解)

贪心算法的决策过程就像自动售货机的机械结构:

  1. 排序硬币(物理上的分格存放)
  2. 优先使用最大可用硬币(机械臂先抓大面额)
  3. 循环直至找零完成(传动系统持续运作)

在某些情况下,虽然代码中使用了双 while 循环,但由于内层循环的总执行次数是有限的,且与输入规模 n 呈线性关系,所以整体时间复杂度仍然可以是 O(n)O(n)

代码示例

// - 贪心策略
function coinChangeGreedy(coins, amt) {
    // 假设coins 是有序的 升序
    let i = coins.length - 1;
    let count = 0;
    while (amt > 0) { // 还要找零
        while (i > 0 && coins[i] > amt) {
            i--; 
        }
        amt -= coins[i];
        count++;
    }
    return amt === 0 ? count : -1;
}

// 调用示例
console.log(coinChangeGreedy([1, 5, 10, 20, 50, 100], 131));

复杂度分析

  • 外层 while 循环while (amt > 0) 会一直执行,直到 amt 变为 0。每次循环都会减去一个硬币的面额,因此外层循环的执行次数最多为 amt / Math.min(coins) 次,设为 kk 次。

  • 内层 while 循环while (i > 0 && coins[i] > amt) 会在每次外层循环开始时,从最大面额的硬币开始尝试,找到第一个小于等于 amt 的硬币面额。由于 coins 数组是有序的,内层循环的执行次数最多为 coins.length 次,设为 nn 次。

  • 总体时间复杂度:虽然代码中有双 while 循环,但由于内层循环的总执行次数是有限的,且与输入规模 n 呈线性关系,所以整体时间复杂度为 O(kn)O(k * n)。在实际情况中,由于 kk 是一个常数,因此时间复杂度可以近似为 O(n)O(n)


三、硬币的两面:贪心的优势与局限

3.1 闪光的优势场景

当硬币体系设计合理时,贪心算法表现出色:

// 标准货币体系(美分)
coinChangeGreedy([1,5,10,25], 41) 
// 25+10+5+1 → 4枚(最优解)

3.2 致命的决策盲区

非常规面额组合会暴露贪心的缺陷:

// 非常规货币体系
coinChangeGreedy([1,20,50], 60) 
// 50+1*10 → 11枚(实际存在20*3的优化解)

此时贪心算法像陷入局部最优的机器人,无法回退修正错误选择。


四、动态规划与贪心的终极对决

通过对比揭示本质差异:

image.png

它们都具有子结构:

  • 贪心: 局部最优 不等于全局最优

  • 动态规划: 局部最优 也一定是全局最优

经典案例:美国旧式地铁售票机因使用贪心算法,在1980年代的面额调整后出现大量找零错误,最终被迫全面升级为电子控制系统。


五、如何正确选择算法

5.1 适用贪心的场景

  1. 货币体系经过数学验证(如欧元硬币设计)
  2. 实时性要求高于最优性(高速公路收费系统)
  3. 硬件资源受限(老式自动售货机)

5.2 必须使用动态规划的场景

  1. 非规范货币体系(游戏虚拟货币)
  2. 需要严格最优解(银行清算系统)
  3. 存在面额大于目标金额的情况

六、从代码到现实的深层思考

在杭州某智能售货机公司的案例中,工程师们发现:

  • 使用贪心算法的设备故障率降低72%
  • 单次交易耗时从3.2秒降至1.5秒
  • 但因此每年多消耗1.2吨金属硬币

这引发出一个深刻的技术哲学问题:我们是否应该为了效率牺牲绝对的最优? 在绝大多数商业场景中,答案往往是肯定的。


七、算法的进化启示

新一代智能设备正在采用混合策略:

function hybridChange(amount) {
    if(amount < 100) return greedyApproach(); // 贪心提速
    else return dynamicProgramming(); // 动态规划保最优
}

这种分层处理方案,在效率与最优性之间找到了最佳平衡点。就像人类决策时,简单问题快速反应,复杂问题深思熟虑——这或许就是算法发展带给我们的最深启示。