背包问题(Knapsack Problem)

144 阅读3分钟

背包问题(Knapsack Problem)是一个经典的组合优化问题,广泛应用于计算机科学、运筹学等领域。其基本形式是:

0/1 背包问题(0/1 Knapsack Problem)

给定一组物品,每个物品有一个重量和一个价值,同时给定一个背包的容量,要求在不超过背包容量的前提下,选择若干物品,使得这些物品的总价值最大。

问题描述:

  • 有 ( n ) 件物品,每件物品有一个重量 ( w_i ) 和价值 ( v_i )。
  • 背包的容量为 ( W )。
  • 目标是从这些物品中选择若干个物品放入背包中,使得背包中的物品总重量不超过背包容量 ( W ),并且物品的总价值最大。

0/1 背包问题的递推公式

定义一个二维数组 ( dp[i][w] ),表示前 ( i ) 个物品,放入容量为 ( w ) 的背包中所能获得的最大价值。

递推关系为:

  • 如果第 ( i ) 个物品不放入背包中,则最大价值为 ( dp[i-1][w] )。
  • 如果第 ( i ) 个物品放入背包中,则最大价值为 ( dp[i-1][w - w_i] + v_i )(前提是 ( w \geq w_i ))。

因此,递推公式为: [ dp[i][w] = \max(dp[i-1][w], dp[i-1][w-w_i] + v_i) ]

其中:

  • ( dp[0][w] = 0 ) 对于所有 ( w ),表示没有物品时,背包价值为 0。
  • ( dp[i][0] = 0 ) 对于所有 ( i ),表示背包容量为 0 时,无论有多少物品,最大价值都是 0。

动态规划算法实现

1. 基本动态规划算法

function knapsack(weights, values, capacity) {
    const n = weights.length;
    const dp = Array(n + 1).fill().map(() => Array(capacity + 1).fill(0));
    
    // 填充 dp 数组
    for (let i = 1; i <= n; i++) {
        for (let w = 1; w <= capacity; w++) {
            if (weights[i - 1] <= w) {
                dp[i][w] = Math.max(dp[i - 1][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]);
            } else {
                dp[i][w] = dp[i - 1][w];
            }
        }
    }
    
    return dp[n][capacity]; // 最大价值
}

// 示例
const weights = [2, 3, 4, 5];
const values = [3, 4, 5, 6];
const capacity = 5;
console.log(knapsack(weights, values, capacity)); // 输出 7

2. 空间优化版本

由于动态规划的状态转移仅依赖于前一行的状态,因此可以将二维数组优化为一维数组,空间复杂度从 ( O(nW) ) 降到 ( O(W) )。

function knapsack(weights, values, capacity) {
    const n = weights.length;
    const dp = Array(capacity + 1).fill(0);
    
    // 填充 dp 数组
    for (let i = 0; i < n; i++) {
        for (let w = capacity; w >= weights[i]; w--) {
            dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
        }
    }
    
    return dp[capacity]; // 最大价值
}

// 示例
const weights = [2, 3, 4, 5];
const values = [3, 4, 5, 6];
const capacity = 5;
console.log(knapsack(weights, values, capacity)); // 输出 7

解释:

  • 状态转移: 在每次迭代中,对于每个物品 ( i ) 和每个容量 ( w ),我们有两个选择:

    • 不选择当前物品 ( i ),即背包的最大价值为 dp[i-1][w]
    • 选择当前物品 ( i ),此时背包的最大价值为 dp[i-1][w-w_i] + v_i,即从剩余容量中放入物品 ( i ) 后的价值。

    我们选择这两者中的最大值。

  • 空间优化: 由于每一行的状态只依赖于上一行的状态,所以我们可以用一个一维数组代替二维数组,逐步更新数组中的每个值。

复杂度分析:

  • 时间复杂度:
    对于每个物品,我们需要遍历所有的容量,从 1 到 ( W ),因此时间复杂度为 ( O(nW) ),其中 ( n ) 是物品数量,( W ) 是背包的容量。

  • 空间复杂度:
    基本动态规划算法的空间复杂度为 ( O(nW) ),而空间优化版本的空间复杂度为 ( O(W) ),因为只使用了一个一维数组来存储当前的最大价值。

扩展问题:

  • 完全背包问题(Unbounded Knapsack): 允许每个物品可以多次放入背包。此时,状态转移公式会有所不同: [ dp[w] = \max(dp[w], dp[w - w_i] + v_i) ]
  • 多重背包问题(Multiple Knapsack): 每个物品有多个可用数量,需要考虑每个物品的数量限制。

结论:

0/1 背包问题是一个经典的动态规划问题,适用于物品选择和优化问题。通过合适的优化方法,可以使其在大数据集下更高效地求解。