在数据结构与算法的学习路上,“堆箱子”问题绝对是考验算法选型能力的经典案例。它看似简单——给定一批长方体箱子,要堆叠出最高的高度,却藏着对“局部最优能否推导全局最优”的深度思考。今天我们就聚焦贪心算法在这个问题中的应用,从原理剖析到代码实现,再到局限性探讨,带你完整吃透这个经典问题。

一、先搞懂问题:堆箱子的核心约束是什么?

        在动手写算法前,我们必须明确问题的“游戏规则”,避免后期走偏。堆箱子问题的标准定义如下:

        给定n个长方体箱子,每个箱子有长、宽、高三个维度。箱子可以自由旋转(即长宽高三个维度可以任意重新分配),但堆叠时必须满足:上方箱子的长和宽都要严格小于下方箱子的长和宽。求能堆叠出的最大高度。

这里有两个关键细节需要重点关注:

  • 旋转的处理:每个箱子有6种潜在的旋转方式(3个维度的全排列),但我们可以通过“排序标准化”简化——将每个箱子的长宽高按“长≥宽≥高”排序,这样就把6种旋转方式统一成1种,避免重复判断。

  • 严格小于约束:必须同时满足长和宽的严格递减,仅一个维度满足是无效的,这是堆叠的核心前提。

二、贪心算法登场:为什么它适合这个问题?

        贪心算法的核心思想很朴素:每一步都做出当前看来最优的选择,通过局部最优积累得到全局最优。那在堆箱子问题中,“当前最优”该如何定义呢?

我们的目标是“最大高度”,但直接按高度排序并不可行——一个很高但底面积极小的箱子,可能无法承载任何其他箱子,最终总高度反而很低。那换个思路:优先选择“基础更稳固”的箱子,也就是底面积更大的箱子。因为底面积大的箱子能为上方堆叠更多箱子提供可能,这就是我们的贪心策略。

基于这个思路,我们确定贪心算法的核心步骤:

  1. 箱子标准化:对每个箱子的长宽高排序,保证长≥宽≥高,消除旋转干扰。

  2. 贪心排序:按箱子的底面积(长×宽)降序排序,底面积相同时按高度降序排序——优先选底面积大的,底面积相同时选更高的。

  3. 堆叠验证:遍历排序后的箱子,依次尝试堆叠到当前栈顶,只要满足“长和宽严格小于栈顶箱子”就加入,累计高度。

三、代码实现:从理论到实践

        我们用Python实现上述思路,为了让代码更清晰,先定义一个Box类封装箱子的属性和标准化逻辑,再实现贪心堆叠函数。

3.1 完整代码

class Box:
    def __init__(self, length, width, height):
        sorted_dims = sorted([length, width, height], reverse=True)
        self.length = sorted_dims[0]
        self.width = sorted_dims[1]
        self.height = sorted_dims[2]
    
    def __repr__(self):
        return f"Box(长={self.length}, 宽={self.width}, 高={self.height})"

def greedy_stack_boxes(original_boxes):
    processed_boxes = [Box(*box) for box in original_boxes]
    
    processed_boxes.sort(
        key=lambda b: (-b.length * b.width, -b.height)
    )
    
    stack = []
    total_height = 0
    for box in processed_boxes:
        if not stack:
            stack.append(box)
            total_height += box.height
        else:
            top_box = stack[-1]
            if box.length < top_box.length and box.width < top_box.width:
                stack.append(box)
                total_height += box.height
    
    return stack, total_height

if __name__ == "__main__":
    test_case1 = [(1,2,3), (2,3,4), (3,4,5), (4,5,6)]
    stack1, height1 = greedy_stack_boxes(test_case1)
    print("测试用例1(理想场景):")
    print(f"堆叠顺序:{stack1}")
    print(f"最大高度:{height1}\n")
    
    test_case2 = [(10,10,1), (9,9,10), (8,8,20)]
    stack2, height2 = greedy_stack_boxes(test_case2)
    print("测试用例2(贪心局限性):")
    print(f"堆叠顺序:{stack2}")
    print(f"贪心得到高度:{height2}")
    print("全局最优解:Box(9,9,10)+Box(8,8,20),高度30")

3.2 代码关键解析

  • Box类的作用:通过sorted函数将长宽高逆序排序,确保每个箱子的长≥宽≥高,这样无论原始输入如何,同一箱子的标准化结果唯一,简化了后续的堆叠判断。

  • 排序key的设计:用“-底面积”实现降序(负号将升序转为降序),底面积相同时用“-高度”排序,保证在底面积相同的情况下优先选更高的箱子,贴合“最大高度”的目标。

  • 堆叠逻辑:用栈存储当前堆叠的箱子,每次取排序后的箱子与栈顶对比,满足约束就加入,不满足则跳过——这是贪心“能选就选”的直接体现。

四、关键思考:贪心算法的局限性

        运行测试用例2时你会发现,贪心算法得到的高度是1,而全局最优高度是30。这暴露了贪心算法的核心局限性:局部最优不一定等于全局最优。

        在测试用例2中,贪心算法优先选择了底面积最大的Box(10,10,1),但这个箱子的长和宽太大,没有任何其他箱子能堆在它上面,总高度只有1;而放弃这个箱子,选择底面积稍小的Box(9,9,10)和Box(8,8,20),反而能得到更高的总高度。

        贪心算法的有效性依赖“贪心选择性质”——即全局最优解可以通过一系列局部最优解得到。堆箱子问题不满足这个性质,因此贪心算法只能得到局部最优解。

        那如果需要得到全局最优解该怎么办?答案是动态规划。我们可以定义dp[i]为“以第i个箱子为顶的最大高度”,通过遍历所有可能的前驱箱子,更新dp[i]的值,最终取dp数组的最大值。不过动态规划的时间复杂度更高(O(n²)),而贪心算法的时间复杂度主要来自排序,为O(nlogn),在对结果要求不严格的场景下,贪心算法的效率优势很明显。

五、总结:贪心算法的适用场景与启示

        通过堆箱子问题的实战,我们可以总结出贪心算法的适用场景和使用技巧:

  1. 适用场景:问题需满足“贪心选择性质”和“最优子结构性质”,比如哈夫曼编码、活动选择问题、零钱兑换(特定硬币体系)等。

  2. 核心技巧:确定“贪心策略”是关键——比如堆箱子的“底面积降序”、活动选择的“结束时间早优先”,策略的设计直接决定结果质量。

  3. 权衡取舍:当问题不满足贪心选择性质时,贪心算法能提供高效的“近似解”,若需全局最优则需结合动态规划等更复杂的算法。

        最后提醒大家,算法学习不是“一招鲜吃遍天”,而是根据问题的约束和需求选择最合适的工具。贪心算法虽有局限性,但在合适的场景下,它的简洁性和高效性无可替代。希望这篇堆箱子问题的实战解析,能让你对贪心算法有更深刻的理解!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐