动态规划之背包问题(01背包,完全背包,多重背包,分组背包)

0、1背包问题

概述

0 - 1 背包问题是一个经典的组合优化问题,属于动态规划算法的典型应用场景。该问题描述如下:
有一个容量为 C的背包,以及n个物品,每个物品有对应的重量 w i w_i wi和价值 v i ( i = 1 , 2... n ) v_i(i=1,2...n) vi(i=1,2...n)。对于每个物品,我们只有两种选择:要么将其放入背包,要么不放入,即 “0 - 1” 选择(选是 1,不选是 0)。目标是在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大。

问题分析与解决思路

状态定义

通常使用二维数组 dp[i][j] 来表示状态,其中 i 表示考虑前 i 个物品,j 表示背包的当前容量,dp[i][j] 则表示在前 i 个物品中选择,背包容量为 j 时所能获得的最大价值。

状态转移方程

对于第 i 个物品,有两种情况:

  • 不放入第 i 个物品:此时 dp[i][j] 就等于考虑前 i - 1 个物品、背包容量为 j 时的最大价值,即 dp[i][j] = dp[i - 1][j]
  • 放入第 i 个物品:前提是当前背包容量 j 要大于等于第 i 个物品的重量 ,放入后 dp[i][j] 等于考虑前 i - 1 个物品、背包容量为 j - w[i] 时的最大价值加上第 i 个物品的价值 ,即 dp[i][j] = dp[i - 1][j - w[i]] + v[i]
    综合这两种情况,状态转移方程为:
    d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] if  j < w [ i ] max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] ) if  j ≥ w [ i ] dp[i][j] = \begin{cases} dp[i - 1][j] & \text{if } j < w[i] \\ \max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]) & \text{if } j \geq w[i] \end{cases} dp[i][j]={dp[i1][j]max(dp[i1][j],dp[i1][jw[i]]+v[i])if j<w[i]if jw[i]

结果

最终的最大价值存储在 dp[n][C] 中,其中 n 是物品的总数,C 是背包的容量。

代码实现

def knapsack(weights, values, capacity):
    n = len(weights)
    # 创建二维数组 dp 并初始化
    dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]

    # 填充 dp 数组
    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            if j < weights[i - 1]:
                # 当前背包容量小于第 i 个物品的重量,不能放入
                dp[i][j] = dp[i - 1][j]
            else:
                # 可以选择放入或不放入第 i 个物品,取最大值
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])

    return dp[n][capacity]

# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(knapsack(weights, values, capacity))

复杂度分析

  • 时间复杂度 O ( n C ) O(nC) O(nC):,其中 n是物品的数量,C是背包的容量。需要填充一个 n ∗ C n*C nC 的二维数组。
  • 空间复杂度 O ( n C ) O(nC) O(nC):,主要用于存储 dp 数组。不过,空间复杂度可以通过优化降低到 O ( C ) O(C) O(C)(改用一维数组)。

优化为一维数组

优化思路
二维 dp 数组 dp[i][j] 表示考虑前 i 个物品,背包容量为 j 时的最大价值。在状态转移过程中,dp[i][j] 只依赖于 dp[i - 1][j] 和 dp[i - 1][j - w[i]],也就是说,当前行的状态只和上一行的状态有关。因此,我们可以使用一维数组 dp[j] 来表示背包容量为 j 时的最大价值,在更新 dp[j] 时,我们需要逆序遍历背包容量,这样可以保证更新 dp[j] 时使用的是上一轮(即考虑前 i - 1 个物品时)的状态。

def knapsack_optimized(weights, values, capacity):
    n = len(weights)
    # 初始化一维 dp 数组
    dp = [0] * (capacity + 1)

    # 遍历每个物品
    for i in range(n):
        # 逆序遍历背包容量
        for j in range(capacity, weights[i] - 1, -1):
            # 更新 dp[j] 的值
            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])

    return dp[capacity]

# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(knapsack_optimized(weights, values, capacity))

通俗易懂的例子:小偷背包问题

假设有一个小偷潜入了一家珠宝店,他带着一个容量为 8 千克的背包。店里有 4 件珠宝,每件珠宝有对应的重量和价值,如下表所示:

珠宝编号 重量(千克) 价值(元)
1 2 3
2 3 4
3 4 5
4 5 6

小偷只能选择拿或者不拿某件珠宝,不能只拿一部分。他的目标是在不超过背包容量的情况下,拿到总价值最高的珠宝组合。
当考虑第一件珠宝(重量 2 千克,价值 3 元)时:
如果背包容量小于 2 千克,那么无法放入这件珠宝,最大价值和不考虑这件珠宝时一样。
如果背包容量大于等于 2 千克,就可以选择放入,此时要比较放入和不放入哪种情况能获得更大价值。
依次类推,考虑第二件、第三件、第四件珠宝,不断更新不同背包容量下的最大价值。最终,就能得出在背包容量为 8 千克时能拿到的珠宝的最大总价值。

完全背包问题

概述

完全背包问题是一个经典的动态规划问题,它是 0 - 1 背包问题的扩展。在 0 - 1 背包问题中,每种物品只有一个,我们只能选择放入或者不放入背包;而在完全背包问题里,每种物品有无限个,即每种物品可以选择放入背包 0 次、1 次、2 次…… 直到背包放不下为止。

问题描述为:有 n种物品,每种物品的重量为 w i w_i wi,价值为 v i ( i = 1 , 2..... n ) v_i(i=1,2.....n) vi(i=1,2.....n),背包的容量为C,每种物品可以使用任意多次,求在不超过背包容量的前提下,能获得的最大价值。

问题分析与解决思路

状态定义

同样使用二维数组 d p [ i ] [ j ] dp[i][j] dp[i][j]来表示状态,其中 i 表示考虑前 i 种物品,j 表示背包的当前容量, d p [ i ] [ j ] dp[i][j] dp[i][j]表示在前 i 种物品中选择,背包容量为 j 时所能获得的最大价值。

状态转移方程

对于第 i 种物品,有多种选择:可以不放入(即放入 0 次),也可以放入 1 次、2 次…… 直到背包放不下为止。所以状态转移方程可以表示为:
d p [ i ] [ j ] = max ⁡ k = 0 ⌊ j / w [ i ] ⌋ ( d p [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] ) dp[i][j] = \max_{k = 0}^{\lfloor j / w[i] \rfloor}(dp[i - 1][j - k \times w[i]] + k \times v[i]) dp[i][j]=k=0maxj/w[i]⌋(dp[i1][jk×w[i]]+k×v[i])
不过,我们可以对这个方程进行优化,得到更简洁的形式。从另一个角度考虑, d p [ i ] [ j ] dp[i][j] dp[i][j] 的值要么是不放入第 i 种物品时的最大价值 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i1][j],要么是放入至少一个第 i 种物品时的最大价值 d p [ i ] [ j − w [ i ] ] + v [ i ] dp[i][j - w[i]] + v[i] dp[i][jw[i]]+v[i] 。因此优化后的状态转移方程为:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] if  j < w [ i ] max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w [ i ] ] + v [ i ] ) if  j ≥ w [ i ] dp[i][j] = \begin{cases} dp[i - 1][j] & \text{if } j < w[i] \\ \max(dp[i - 1][j], dp[i][j - w[i]] + v[i]) & \text{if } j \geq w[i] \end{cases} dp[i][j]={dp[i1][j]max(dp[i1][j],dp[i][jw[i]]+v[i])if j<w[i]if jw[i]

最终结果

最终的最大价值存储在 d p [ i ] [ C ] dp[i][C] dp[i][C] 中,其中 n 是物品的种类数,C 是背包的容量。

代码实现

def complete_knapsack(weights, values, capacity):
    n = len(weights)
    # 创建二维数组 dp 并初始化
    dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]

    # 填充 dp 数组
    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            if j < weights[i - 1]:
                # 当前背包容量小于第 i 种物品的重量,不能放入
                dp[i][j] = dp[i - 1][j]
            else:
                # 可以选择放入或不放入第 i 种物品,取最大值
                dp[i][j] = max(dp[i - 1][j], dp[i][j - weights[i - 1]] + values[i - 1])

    return dp[n][capacity]

# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(complete_knapsack(weights, values, capacity))

复杂度分析

  • 时间复杂度 O ( n C ) O(nC) O(nC):,其中 n是物品的数量,C是背包的容量。需要填充一个 n ∗ C n*C nC 的二维数组。
  • 空间复杂度 O ( n C ) O(nC) O(nC):,主要用于存储 dp 数组。不过,空间复杂度可以通过优化降低到 O ( C ) O(C) O(C)(改用一维数组)。

通俗易懂的例子:超市购物凑满减

假设你要去超市购物,超市正在进行满减活动,满 200 元减 50 元。你有一张长长的购物清单,上面列了各种商品,每种商品都有对应的价格和你对它的喜爱程度(可以理解为价值),而且每种商品的库存是无限的。你想在凑满 200 元的前提下,尽可能地买到你喜欢的商品,也就是让商品的总价值最大,这就是一个完全背包问题。
商品的价格相当于物品的重量,商品的喜爱程度相当于物品的价值,满减的金额门槛相当于背包的容量,你需要在不超过这个金额门槛的情况下,选择合适的商品组合,使得总价值最大。

变种例子

动态规划之背包问题(01背包,完全背包,多重背包,分组背包)_第1张图片
这是一个典型的完全背包问题的变种,我们可以使用动态规划来解决这个问题。动态规划的核心思想是将大问题分解为小问题,通过求解小问题的最优解来得到大问题的最优解。

代码实现

def minCoins(arr, aim):
    # 创建 dp 数组并初始化
    dp = [float('inf')] * (aim + 1)
    dp[0] = 0

    # 遍历每种面值的货币
    for coin in arr:
        # 遍历所有可能的金额
        for i in range(coin, aim + 1):
            # 更新 dp[i] 的值
            dp[i] = min(dp[i], dp[i - coin] + 1)

    # 如果最终无法组成 aim,则返回 -1
    if dp[aim] == float('inf'):
        return -1
    else:
        return dp[aim]

# 测试示例
arr = [1, 2, 5]
aim = 11
print(minCoins(arr, aim))

多重背包问题

概述

多重背包问题也是一个经典的动态规划问题,它介于 0 - 1 背包问题和完全背包问题之间。在多重背包问题中,有 n种物品,每种物品有对应的重量 w i w_i wi、价值 v i v_i vi 以及数量上限 c i ( i = 1 , 2... n ) c_i(i=1,2...n) ci(i=1,2...n),背包的容量为C。我们需要在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大,且每种物品放入的数量不能超过其数量上限。

问题分析与解决思路

状态定义

同上。。。。

状态转移方程

对于第 i 种物品,我们可以选择放入 0 , 1 , . . . m i n ( c i , ∣ j / w i ∣ ) 0,1,...min(c_i,|j/w_i|) 0,1,...min(ci,j/wi) 个,状态转移方程为:
d p [ i ] [ j ] = max ⁡ k = 0 min ⁡ ( c i , ⌊ j / w i ⌋ ) ( d p [ i − 1 ] [ j − k × w i ] + k × v i ) dp[i][j] = \max_{k = 0}^{\min(c_i, \lfloor j / w_i \rfloor)}(dp[i - 1][j - k\times w_i] + k\times v_i) dp[i][j]=k=0maxmin(ci,j/wi⌋)(dp[i1][jk×wi]+k×vi)
其中,k 表示第 i 种物品放入的数量, min ⁡ ( c i , ⌊ j / w i ⌋ ) \min(c_i, \lfloor j / w_i \rfloor) min(ci,j/wi⌋) 确保放入的数量既不超过该物品的数量上限 c i c_i ci ,也不超过当前背包容量所能容纳的该物品的最大数量。

最终结果

同上。。。。

代码实现

def multiple_knapsack(weights, values, counts, capacity):
    n = len(weights)
    # 创建二维数组 dp 并初始化
    dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]

    # 填充 dp 数组
    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            # 不放入第 i 种物品
            dp[i][j] = dp[i - 1][j]
            # 尝试放入不同数量的第 i 种物品
            for k in range(1, min(counts[i - 1], j // weights[i - 1]) + 1):
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * weights[i - 1]] + k * values[i - 1])

    return dp[n][capacity]

# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
counts = [2, 3, 1, 2]
capacity = 8
print(multiple_knapsack(weights, values, counts, capacity))

复杂度分析

  • 时间复杂度 O ( n C ∑ i = 1 n c i ) O(nC\sum_{i=1}^nc_i) O(nCi=1nci):,其中 n 是物品的种类数,C 是背包的容量, c i c_i ci 是第 i 种物品的数量上限。因为对于每种物品,都需要遍历其可能的放入数量,并且对于每个背包容量都要进行更新
  • 空间复杂度 O ( n C ) O(nC) O(nC):,主要用于存储 dp 数组。不过,空间复杂度可以通过优化降低到 O ( C ) O(C) O(C)(改用一维数组)。

空间复杂度优化

和 0 - 1 背包问题类似,多重背包问题也可以将二维的 dp 数组优化为一维数组,优化后的代码如下:

def multiple_knapsack_optimized(weights, values, counts, capacity):
    n = len(weights)
    # 初始化一维 dp 数组
    dp = [0] * (capacity + 1)

    # 遍历每个物品
    for i in range(n):
        # 逆序遍历背包容量
        for j in range(capacity, 0, -1):
            # 尝试放入不同数量的第 i 种物品
            for k in range(1, min(counts[i], j // weights[i]) + 1):
                dp[j] = max(dp[j], dp[j - k * weights[i]] + k * values[i])

    return dp[capacity]

# 示例数据
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
counts = [2, 3, 1, 2]
capacity = 8
print(multiple_knapsack_optimized(weights, values, counts, capacity))

复杂度分析

  • 时间复杂度 O ( n C ∑ i = 1 n c i ) O(nC\sum_{i=1}^nc_i) O(nCi=1nci):,和之前相同
  • 空间复杂度 O ( C ) O(C) O(C):,只使用了一个长度为C+1 的一维数组

通俗易懂的例子:文具采购问题

假设你是一位老师,要为班级采购文具,预算是 8 元。文具店有 4 种文具,每种文具有对应的价格、价值(比如对学生学习的帮助程度)和库存数量,如下表所示:

文具编号 价格(元) 价值 库存数量
1 2 3 2
2 3 4 3
3 4 5 1
4 5 6 2

你需要在不超过预算的情况下,选择合适的文具组合,使得采购的文具总价值最大,同时每种文具的采购数量不能超过其库存数量。这就是一个多重背包问题,其中预算相当于背包容量,文具的价格相当于物品的重量,文具的价值就是物品的价值,库存数量就是物品的数量上限。

分组背包

概述

分组背包问题是背包问题的一种变体。有 n 个物品,这些物品被划分成了 g 组,每组中的物品相互冲突,即每组中最多只能选择一个物品放入背包。每个物品有对应的重量 w i j w_ij wij和价值 v i j v_ij vij,其中 i 表示物品所在的组编号, j 表示该组内物品的编号,背包的容量为 C。目标是在不超过背包容量的前提下,选择合适的物品放入背包,使得背包中物品的总价值最大。

问题分析与解决思路

状态定义

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示考虑前 i 组物品,背包容量为 j 时所能获得的最大价值。

状态转移方程

对于第 i 组物品,我们有两种选择:要么不选这一组的任何物品,此时 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j] = dp[i-1][j] dp[i][j]=dp[i1][j] ;要么从这一组中选一个物品 k 放入背包,前提是背包容量 j 要大于等于该物品的重量 w i k w_ik wik ,此时 d p [ i ] [ j ] = d p [ i − 1 ] [ j − w i k ] + v i k dp[i][j] = dp[i - 1][j - w_{ik}] + v_{ik} dp[i][j]=dp[i1][jwik]+vik
所以状态转移方程为:
d p [ i ] [ j ] = max ⁡ ( d p [ i − 1 ] [ j ] , max ⁡ k ∈ group i , j ≥ w i k ( d p [ i − 1 ] [ j − w i k ] + v i k ) ) dp[i][j]=\max\left(dp[i - 1][j],\max_{k\in\text{group}_i,j\geq w_{ik}}(dp[i - 1][j - w_{ik}]+v_{ik})\right) dp[i][j]=max(dp[i1][j],kgroupi,jwikmax(dp[i1][jwik]+vik))
其中 group i \text{group}_i groupi 表示第 i 组物品的集合。

最终结果

最终的最大价值存储在 d p [ j ] [ C ] dp[j][C] dp[j][C] 中,其中 g 是物品的组数,C 是背包的容量。

代码实现

def group_knapsack(groups, weights, values, capacity):
    g = len(groups)
    # 创建二维数组 dp 并初始化
    dp = [[0 for _ in range(capacity + 1)] for _ in range(g + 1)]

    # 遍历每组物品
    for i in range(1, g + 1):
        for j in range(1, capacity + 1):
            # 不选第 i 组的任何物品
            dp[i][j] = dp[i - 1][j]
            group = groups[i - 1]
            for k in group:
                if j >= weights[k]:
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - weights[k]] + values[k])

    return dp[g][capacity]


# 示例数据
groups = [[0, 1], [2, 3]]
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(group_knapsack(groups, weights, values, capacity))

复杂度分析

  • 时间复杂度 O ( g ∗ C ∗ s ) O(g*C*s) O(gCs):,其中 g 是物品的组数,C 是背包的容量,s 是每组物品的平均数量。因为需要遍历每组物品,对于每个容量,还需要遍历每组内的物品。
  • 空间复杂度 O ( g ∗ C ) O(g*C) O(gC):,主要用于存储 dp 数组。可以优化到 O ( C ) O(C) O(C)

空间复杂度优化

同样可以将二维的 dp 数组优化为一维数组,优化后的代码如下:

def group_knapsack_optimized(groups, weights, values, capacity):
    g = len(groups)
    # 初始化一维 dp 数组
    dp = [0] * (capacity + 1)

    # 遍历每组物品
    for i in range(g):
        for j in range(capacity, -1, -1):
            group = groups[i]
            for k in group:
                if j >= weights[k]:
                    dp[j] = max(dp[j], dp[j - weights[k]] + values[k])

    return dp[capacity]


# 示例数据
groups = [[0, 1], [2, 3]]
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 8
print(group_knapsack_optimized(groups, weights, values, capacity))

通俗易懂的例子:旅行套餐选择问题

假设你计划去旅行,有不同类型的旅行套餐可供选择,每种类型的套餐只能选一个。比如有住宿套餐组、交通套餐组等。每个套餐有对应的价格(相当于物品重量)和旅行体验评分(相当于物品价值),而你旅行的预算是固定的(相当于背包容量)。你需要在预算范围内,从每个类型的套餐组中选择一个套餐,使得总的旅行体验评分最高,这就是一个分组背包问题。

你可能感兴趣的:(动态规划,算法,经验分享,python)