动态规划经典问题:背包问题的五种变种解析

动态规划经典问题:背包问题的五种变种解析

LeetCode-Py ⛽️「算法通关手册」:超详细的「算法与数据结构」基础讲解教程,从零基础开始学习算法知识,800+ 道「LeetCode 题目」详细解析,200 道「大厂面试热门题目」。 LeetCode-Py 项目地址: https://gitcode.com/gh_mirrors/le/LeetCode-Py

背包问题是动态规划中的经典问题,在实际应用中存在多种变种。本文将深入解析五种常见的背包问题变种,包括恰好装满背包、方案总数、最优方案数、具体方案以及字典序最小的具体方案。

1. 恰好装满背包的最大价值

问题定义

在给定背包容量和物品重量、价值的情况下,要求恰好装满背包时能获得的最大价值总和。

关键思路

与普通背包问题不同,恰好装满背包需要在初始化时进行特殊处理:

  1. 只有容量为0的背包可以被"恰好装满"(不装任何物品),因此初始化dp[0] = 0
  2. 其他容量的背包初始状态设为-∞,表示无法恰好装满

代码实现

def zeroOnePackJustFillUp(weight, value, W):
    size = len(weight)
    dp = [float('-inf')] * (W + 1)
    dp[0] = 0
    
    for i in range(1, size + 1):
        for w in range(W, weight[i - 1] - 1, -1):
            dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1])
    
    return dp[W] if dp[W] != float('-inf') else -1

复杂度分析

  • 时间复杂度:O(n×W)
  • 空间复杂度:O(W)

2. 背包问题的方案总数

问题定义

计算在不超过背包容量的情况下,有多少种不同的物品组合方式。

关键思路

将状态转移方程中的"求最大值"改为"求和":

  1. 初始化dp[0] = 1(容量为0时有1种方案:不装任何物品)
  2. 状态转移方程:dp[w] += dp[w - weight[i-1]]

代码实现

def zeroOnePackNumbers(weight, value, W):
    size = len(weight)
    dp = [0] * (W + 1)
    dp[0] = 1
    
    for i in range(1, size + 1):
        for w in range(W, weight[i - 1] - 1, -1):
            dp[w] += dp[w - weight[i - 1]]
                
    return dp[W]

复杂度分析

  • 时间复杂度:O(n×W)
  • 空间复杂度:O(W)

3. 最优方案的数量

问题定义

在不超过背包容量的前提下,计算能够获得最大价值的方案数量。

关键思路

需要同时维护两个状态数组:

  1. dp[w]记录最大价值
  2. count[w]记录对应价值的方案数

状态转移时需要考虑三种情况:

  1. 选择当前物品获得更高价值:更新价值和方案数
  2. 两种选择获得相同价值:累加方案数
  3. 不选择当前物品获得更高价值:保持原有方案数

代码实现

def zeroOnePackMaxProfitNumbers(weight, value, W):
    size = len(weight)
    dp = [0] * (W + 1)
    count = [1] * (W + 1)
    
    for i in range(1, size + 1):
        for w in range(W, weight[i - 1] - 1, -1):
            if dp[w] < dp[w - weight[i - 1]] + value[i - 1]:
                dp[w] = dp[w - weight[i - 1]] + value[i - 1]
                count[w] = count[w - weight[i - 1]]
            elif dp[w] == dp[w - weight[i - 1]] + value[i - 1]:
                count[w] += count[w - weight[i - 1]]
    
    return count[W]

复杂度分析

  • 时间复杂度:O(n×W)
  • 空间复杂度:O(W)

4. 具体方案输出

问题定义

不仅要求最大价值,还需要输出具体选择了哪些物品。

关键思路

使用额外的path数组记录状态转移路径:

  1. path[i][w]标记是否选择了第i件物品
  2. 从最终状态逆向追踪,重构具体方案

代码实现

def zeroOnePackPrintPath(weight, value, W):
    size = len(weight)
    dp = [[0] * (W + 1) for _ in range(size + 1)]
    path = [[False] * (W + 1) for _ in range(size + 1)]
    
    for i in range(1, size + 1):
        for w in range(W + 1):
            if w < weight[i - 1]:
                dp[i][w] = dp[i - 1][w]
                path[i][w] = False
            else:
                if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]:
                    dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1]
                    path[i][w] = True
                elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]:
                    dp[i][w] = dp[i - 1][w]
                    path[i][w] = True
                else:
                    dp[i][w] = dp[i - 1][w]
                    path[i][w] = False
    
    res = []
    i, w = size, W
    while i >= 1 and w >= 0:
        if path[i][w]:
            res.append(str(i - 1))
            w -= weight[i - 1]
        i -= 1
        
    return " ".join(res[::-1])

复杂度分析

  • 时间复杂度:O(n×W)
  • 空间复杂度:O(n×W)

5. 字典序最小的具体方案

问题定义

在输出具体方案时,要求物品编号的字典序最小。

关键思路

  1. 先将物品顺序反转,使得优先考虑原序号小的物品
  2. 使用与普通具体方案相同的追踪方法
  3. 输出时再将序号转换回来

代码实现

def zeroOnePackPrintPathMinOrder(weight, value, W):
    size = len(weight)
    weight = weight[::-1]
    value = value[::-1]
    
    dp = [[0] * (W + 1) for _ in range(size + 1)]
    path = [[False] * (W + 1) for _ in range(size + 1)]
    
    for i in range(1, size + 1):
        for w in range(W + 1):
            if w < weight[i - 1]:
                dp[i][w] = dp[i - 1][w]
                path[i][w] = False
            else:
                if dp[i - 1][w] < dp[i - 1][w - weight[i - 1]] + value[i - 1]:
                    dp[i][w] = dp[i - 1][w - weight[i - 1]] + value[i - 1]
                    path[i][w] = True
                elif dp[i - 1][w] == dp[i - 1][w - weight[i - 1]] + value[i - 1]:
                    dp[i][w] = dp[i - 1][w]
                    path[i][w] = True
                else:
                    dp[i][w] = dp[i - 1][w]
                    path[i][w] = False
    
    res = []
    i, w = size, W
    while i >= 1 and w >= 0:
        if path[i][w]:
            res.append(str(size - i))
            w -= weight[i - 1]
        i -= 1
        
    return " ".join(res)

复杂度分析

  • 时间复杂度:O(n×W)
  • 空间复杂度:O(n×W)

总结

本文详细介绍了背包问题的五种常见变种及其解决方案。理解这些变种的关键在于:

  1. 明确问题要求(最大价值、方案数、具体方案等)
  2. 设计合适的状态表示和转移方程
  3. 根据需求调整初始条件和状态转移逻辑

掌握这些变种可以帮助我们更好地应对实际应用中各种复杂的背包问题场景。

LeetCode-Py ⛽️「算法通关手册」:超详细的「算法与数据结构」基础讲解教程,从零基础开始学习算法知识,800+ 道「LeetCode 题目」详细解析,200 道「大厂面试热门题目」。 LeetCode-Py 项目地址: https://gitcode.com/gh_mirrors/le/LeetCode-Py

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

班岑航Harris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值