零基础数据结构与算法——第五章:高级算法-动态规划经典-背包问题

5.1.3 动态规划经典-背包问题

背包问题是动态规划中的经典问题,也是理解动态规划思想的绝佳例子。

问题描述

有N件物品和一个容量为V的背包。第i件物品的重量是wi,价值是vi。求解将哪些物品装入背包可使价值总和最大。

生活例子

想象你是一名登山者,准备一次登山旅行。你有一个容量有限的背包,和许多可能带上的物品(食物、水、装备等)。每件物品都有自己的重量和价值(对登山的帮助程度)。你需要决定带哪些物品,使得在背包容量限制下,总价值最大。

0-1背包问题

问题特点:每件物品只能用一次(要么放入背包,要么不放)。

问题分析

  1. 定义状态:dpij表示考虑前i件物品,背包容量为j时能获得的最大价值。

  2. 状态转移方程:对于第i件物品,有两种选择:

    • 不放入背包:dpij = dpi-1j
    • 放入背包(如果容量允许):dpij = dpi-1j-w\[i-1] + vi-1
      取两者的最大值。
  3. 初始条件:dp0j = 0(没有物品时,价值为0)

动态规划解法

java 复制代码
public static int knapsack01(int[] weights, int[] values, int capacity) {
    int n = weights.length;
    // 创建dp数组,dp[i][j]表示考虑前i件物品,背包容量为j时能获得的最大价值
    int[][] dp = new int[n + 1][capacity + 1];
    
    // 填充dp数组
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= capacity; j++) {
            if (weights[i - 1] <= j) {
                // 可以放入背包,取放入或不放入的最大值
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
            } else {
                // 不能放入背包
                dp[i][j] = dp[i - 1][j];
            }
        }
    }
    
    return dp[n][capacity]; // 返回最终结果
}

图解过程

假设有3件物品,重量分别为2, 3, 4,价值分别为3, 4, 5,背包容量为5。

  1. 初始化dp数组:所有dp0j和dpi0都为0
  2. i=1, j=1: 物品0重量为2 > 1,不能放入,dp11 = dp01 = 0
  3. i=1, j=2: 物品0重量为2 = 2,可以放入,dp12 = max(dp02, dp00 + 3) = max(0, 0 + 3) = 3
  4. 依此类推...

最终dp数组:

复制代码
    0  1  2  3  4  5  (容量)
0   0  0  0  0  0  0
1   0  0  3  3  3  3  (考虑物品0)
2   0  0  3  4  4  7  (考虑物品0,1)
3   0  0  3  4  5  7  (考虑物品0,1,2)

结果是7,表示最大价值为7。

空间优化

我们可以将二维dp数组优化为一维,因为每个状态只依赖于上一行的状态:

java 复制代码
public static int knapsack01Optimized(int[] weights, int[] values, int capacity) {
    int n = weights.length;
    int[] dp = new int[capacity + 1];
    
    for (int i = 0; i < n; i++) {
        // 注意这里要从后往前遍历,避免物品被重复使用
        for (int j = capacity; j >= weights[i]; j--) {
            dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
        }
    }
    
    return dp[capacity];
}
完全背包问题

问题特点:每件物品可以用无限次(只要背包容量允许)。

问题分析

与0-1背包问题类似,但状态转移方程有所不同:

  • 对于第i件物品,可以选择放0次、1次、2次...直到背包容量不允许

动态规划解法

java 复制代码
public static int knapsackComplete(int[] weights, int[] values, int capacity) {
    int n = weights.length;
    // 创建dp数组,dp[j]表示背包容量为j时能获得的最大价值
    int[] dp = new int[capacity + 1];
    
    // 填充dp数组
    for (int i = 0; i < n; i++) {
        // 注意这里要从前往后遍历,允许物品被重复使用
        for (int j = weights[i]; j <= capacity; j++) {
            dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
        }
    }
    
    return dp[capacity]; // 返回最终结果
}

图解过程

假设有2件物品,重量分别为2, 3,价值分别为3, 4,背包容量为5。

  1. 初始化dp数组:所有dpj都为0
  2. i=0 (物品0):
    • j=2: dp2 = max(dp2, dp0 + 3) = max(0, 0 + 3) = 3
    • j=3: dp3 = max(dp3, dp1 + 3) = max(0, 0 + 3) = 3
    • j=4: dp4 = max(dp4, dp2 + 3) = max(0, 3 + 3) = 6
    • j=5: dp5 = max(dp5, dp3 + 3) = max(0, 3 + 3) = 6
  3. i=1 (物品1):
    • j=3: dp3 = max(dp3, dp0 + 4) = max(3, 0 + 4) = 4
    • j=4: dp4 = max(dp4, dp1 + 4) = max(6, 0 + 4) = 6
    • j=5: dp5 = max(dp5, dp2 + 4) = max(6, 3 + 4) = 7

最终dp数组:0, 0, 3, 4, 6, 7,结果是7,表示最大价值为7。

背包问题的变种

  1. 多重背包问题:每种物品有特定的数量限制
  2. 分组背包问题:物品分为多个组,每组最多选一个物品
  3. 二维背包问题:背包有两种容量限制(如重量和体积)

应用场景

背包问题在资源分配、投资决策、物流规划等领域有广泛应用。理解背包问题对于掌握动态规划思想至关重要。

编辑距离

问题描述

给定两个单词word1和word2,计算将word1转换成word2所使用的最少操作数。你可以对一个单词进行三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

例如,将"horse"转换为"ros"的最少操作数是3:

  • 将'h'替换为'r':horse -> rorse
  • 删除'r':rorse -> rose
  • 删除'e':rose -> ros

生活例子

想象你在使用文字处理软件,需要将一篇文章中的某个单词修改为另一个单词。编辑距离就是你需要进行的最少键盘操作次数。这在拼写检查、DNA序列比对、语音识别等领域有广泛应用。

问题分析

这是一个典型的动态规划问题。我们需要定义状态和状态转移方程:

  1. 定义状态:dpij表示将word1的前i个字符转换为word2的前j个字符所需的最少操作数。

  2. 状态转移方程

    • 如果word1i-1 == word2j-1(当前字符相同),则dpij = dpi-1j-1(不需要额外操作)
    • 否则,dpij = 1 + min(dpi-1j-1, dpi-1j, dpij-1),分别对应替换、删除和插入操作
  3. 初始条件

    • dpi0 = i(将word1的前i个字符全部删除)
    • dp0j = j(插入word2的前j个字符)

动态规划解法

java 复制代码
public static int minDistance(String word1, String word2) {
    int m = word1.length();
    int n = word2.length();
    
    // dp[i][j]表示word1的前i个字符转换到word2的前j个字符需要的最少操作数
    int[][] dp = new int[m + 1][n + 1];
    
    // 初始化边界条件
    for (int i = 0; i <= m; i++) {
        dp[i][0] = i; // 将word1的前i个字符全部删除
    }
    for (int j = 0; j <= n; j++) {
        dp[0][j] = j; // 插入word2的前j个字符
    }
    
    // 填充dp数组
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                // 当前字符相同,不需要操作
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 当前字符不同,取三种操作的最小值
                dp[i][j] = 1 + Math.min(
                    dp[i - 1][j - 1],  // 替换操作
                    Math.min(
                        dp[i - 1][j],   // 删除操作
                        dp[i][j - 1]   // 插入操作
                    )
                );
            }
        }
    }
    
    return dp[m][n]; // 返回最终结果
}

图解过程

以word1 = "horse",word2 = "ros"为例:

  1. 初始化dp数组:

    复制代码
     ""  r   o   s

    "" 0 1 2 3
    h 1 ? ? ?
    o 2 ? ? ?
    r 3 ? ? ?
    s 4 ? ? ?
    e 5 ? ? ?

  2. 填充dp数组:

    • dp11:'h'和'r'不同,取min(dp00+1, dp01+1, dp10+1) = min(1, 2, 2) = 1
    • dp12:'h'和'o'不同,取min(dp01+1, dp02+1, dp11+1) = min(2, 3, 2) = 2
    • 依此类推...
  3. 最终dp数组:

    复制代码
     ""  r   o   s

    "" 0 1 2 3
    h 1 1 2 3
    o 2 2 1 2
    r 3 2 2 2
    s 4 3 3 2
    e 5 4 4 3

  4. 结果是dp53 = 3,表示最少需要3次操作。

操作路径追踪

我们可以通过回溯dp数组来找出具体的操作步骤:

  1. 从dp53开始,比较'e'和's':不同,查看dp42、dp43和dp52中的最小值
  2. dp43 = 2是最小值,表示删除'e'(从word1的末尾删除)
  3. 从dp43开始,比较's'和's':相同,移动到dp32
  4. 从dp32开始,比较'r'和'o':不同,查看最小值
  5. dp21 = 2是最小值,表示删除'r'
  6. 从dp21开始,比较'o'和'r':不同,查看最小值
  7. dp10 = 1是最小值,表示替换'h'为'r'

因此,操作步骤是:替换'h'为'r',删除'r',删除'e'。

性能分析

  • 时间复杂度:O(m*n),其中m和n分别是两个字符串的长度
  • 空间复杂度:O(m*n),需要一个m+1行n+1列的dp数组

应用场景

  1. 拼写检查:找出与错误单词最相似的正确单词
  2. DNA序列比对:计算两个DNA序列的相似度
  3. 语音识别:评估识别结果与实际文本的差异
  4. 机器翻译:评估翻译质量
  5. 文本相似度分析:判断两段文本的相似程度
相关推荐
MartinYeung531 分钟前
[论文学习]DP2Unlearning:高效且具保证的大型语言模型遗忘框架(基于差分隐私的 LLM Unlearning 方法)
学习·算法·语言模型
Tian_Hang43 分钟前
C++原型模式(Protype)
开发语言·c++·算法
bIo7lyA8v44 分钟前
算法复杂度的渐进分析与实际运行时间的差异的技术8
算法
yuan199972 小时前
欧拉梁静力与屈曲计算的 MATLAB 实现(有限差分法 + 解析解)
开发语言·算法·matlab
汉克老师3 小时前
GESP7级C++考试语法知识(二、指数函数(3、综合练习)
c++·算法·数学建模·指数函数·gesp7级·复利
林间码客3 小时前
04 ROC曲线与AUC:从零开始手动计算
大数据·人工智能·算法
Irissgwe3 小时前
map/set/multimap/multiset 的底层逻辑与实现
数据结构·c++·算法·二叉树·stl·c·红黑树
IronMurphy3 小时前
【算法五十八】23. 合并 K 个升序链表
数据结构·算法·链表
思茂信息3 小时前
CST软件基于液态金属开关的方向图可重构天线
服务器·算法·重构·cst·仿真软件·电磁仿真
月疯4 小时前
PPG研究中暑的算法记录
算法