Marvel-Site Marvel-Site
首页
  • Java

    • Java基础
    • Java进阶
    • Java容器
    • Java并发编程
    • Java虚拟机
  • 计算机基础

    • 数据结构与算法
    • 计算机网络
    • 操作系统
    • Linux
  • 框架|中间件

    • Spring
    • MySQL
    • Redis
    • MQ
    • Zookeeper
    • Git
  • 架构

    • 分布式
    • 高并发
    • 高可用
    • 架构
  • 框架

    • React
    • 其他
  • 实用工具
  • 安装配置

    • Linux
    • Windows
    • Mac
  • 开发工具

    • IDEA
    • VsCode
  • 关于
  • 收藏
  • 草稿
  • 索引

    • 分类
    • 标签
    • 归档
GitHub (opens new window)

Marvel

吾必当乘此羽葆盖车
首页
  • Java

    • Java基础
    • Java进阶
    • Java容器
    • Java并发编程
    • Java虚拟机
  • 计算机基础

    • 数据结构与算法
    • 计算机网络
    • 操作系统
    • Linux
  • 框架|中间件

    • Spring
    • MySQL
    • Redis
    • MQ
    • Zookeeper
    • Git
  • 架构

    • 分布式
    • 高并发
    • 高可用
    • 架构
  • 框架

    • React
    • 其他
  • 实用工具
  • 安装配置

    • Linux
    • Windows
    • Mac
  • 开发工具

    • IDEA
    • VsCode
  • 关于
  • 收藏
  • 草稿
  • 索引

    • 分类
    • 标签
    • 归档
GitHub (opens new window)
  • Java

  • 计算机基础

    • 数据结构与算法

      • 算法复杂度
      • 排序算法
      • 背包问题
        • 1. 01 背包问题
          • 1.1 题目
          • 1.2 基本思路
          • 1.3 优化空间复杂度
          • 1.4 初始化细节
        • 2 完全背包问题
          • 2.1 题目
          • 2.2 基本思路
          • 2.3 优化
          • 2.4 转化为 01 背包问题求解
      • 二叉树
      • 红黑树
      • 并查集
      • 最小生成树算法
      • 最短路径算法
    • 计算机网络

    • 操作系统

    • Linux

  • 框架|中间件

  • 架构

  • 后端
  • 计算机基础
  • 数据结构与算法
Marvel
2022-09-06
目录

背包问题

# 背包问题

参考背包问题九讲,对 01 背包问题和完全背包问题进行学习。

参考:背包问题九讲 (opens new window)

# 1. 01 背包问题

# 1.1 题目

有 N 件物品和一个容量为 V 的背包。放入第 i 件物品消耗的费用是 Ci,得到的价值是 Wi,求解讲哪些物品装入背包可以使价值总和最大。

# 1.2 基本思路

这是最基本的背包问题,特点:每种物品仅有一件,可以选择放或不妨。

用子问题定义状态:F[i, v] 表示前 i 件物品恰放入一个容量为 v 的背包可以获得的最大价值。则其转移方程便是:

$$ F[i,v]=max{F[i-1,v],F[i-1,v-C_i]+W_i} $$

这个方程十分重要,基本上所有的背包相关的问题的方程都是由它衍生出来的。解释一下:“将前 i 件物品放入容量为 v 的背包中” 这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为一个只和前 i-1 件物品相关的问题。如果不放第 i 件物品,那么问题就转化为 ”前 i-1 件物品放入容量为 v 的背包中“,价值为 F[i-1,v];如果放第 i 件物品,那么问题就转化为 ”前 i-1 件物品放入剩下的容量为 v-Ci 的背包中“,此时能获得的最大价值就是 F[i-1,v-Ci] 再加上通过放入第 i 件物品获得的价值 Wi。

根据基本思路写出的代码:

/**
 * 01背包问题解法
 * @param c 每个物品的体积,cost
 * @param w 每个物品的价值,worth
 * @param v 背包的容积
 * @return 最大价值
 */
public static int knapsackProblem01(int[] c, int[] w, int v) {
    int n = c.length; // 物品数量
    int[][] dp = new int[n + 1][v + 1];
    for (int i = 1; i <= n; i++) {  // 先循环物品
        for (int j = 0; j <= v; j++) {   // 再循环容量
            dp[i][j] = dp[i - 1][j];  // 不选第i件物品
            if (j >= c[i - 1]) {
                dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - c[i - 1]] + w[i - 1]);  // 选第i件物品
            }
        }
    }
    return dp[n][v];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 1.3 优化空间复杂度

以上方法的时间和空间复杂度均为 O(VN),其中时间复杂度不能再优化了,但是空间复杂度却可以优化到 O(V)。

需要考虑,如果只用一个一维数组,能不能保证第 i 次循环结束后 F[v] 中表示的就是我们定义的状态 F[i, v] 呢?F[i, v] 是由 F[i-1, v] 和 F[i-1,v-Ci] 两个子问题递推而来,能否保证再推 F[i, v] 时能够取用 F[i-1, v] 和 F[i-1,v-Ci] 的值呢?只要保证 v 的循环为递减顺序,就可以保证计算 F[v] 时 F[v-Ci] 保存的时状态 F[i-1,v-Ci] 的值。

优化代码如下:

/**
 * 01背包问题解法
 * @param c 每个物品的体积,cost
 * @param w 每个物品的价值,worth
 * @param v 背包的容积
 * @return 最大价值
 */
public static int knapsackProblem01(int[] c, int[] w, int v) {
    int n = c.length; // 物品数量
    int[] dp = new int[v + 1];
    for (int i = 1; i <= n; i++) {  // 先循环物品
        for (int j = v; j >= 0; j--) {   // 再循环容量
            if (j >= c[i - 1]) {
                dp[j] = Math.max(dp[j], dp[j - c[i - 1]] + w[i - 1]);  // 选第i件物品
            }
        }
    }
    return dp[v];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

注意:容量的遍历一定要从后往前遍历,因为再计算 F[v] 时需要 F[v-Ci] 的值,也就是后面的值依赖于前面的值。

# 1.4 初始化细节

背包问题中有两种不同的问法。有的题目要求”恰好装满背包“时的最优解,有的没有要求装满。区别两种问法的实现时再初始化的适合有所不同。

  • 如果要求恰好装满背包,那么初始化时除了 F[0] 为 0,其他 F[1:V] 均设为负无穷,这样就可以保证最终得到的 F[V] 是一种恰好装满背包的最优解。
  • 如果没有要求装满背包,那么初始化时将 F[0:V] 全部设为 0。

理解:初始化 F 数组事实上就是在没有物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可以在什么也不装的情况下被恰好装满,其他容量的背包均没有合法解,应该被赋值为负无穷。如果背包非必须被装满,那么任何容量的背包都由一个合法解,所以初始化时状态的值也就全部为 0。

# 2 完全背包问题

# 2.1 题目

有 N 件物品和一个容量为 V 的背包,每种物品都有无限个。放入第 i 件物品消耗的费用是 Ci,得到的价值是 Wi,求解讲哪些物品装入背包可以使价值总和最大。

# 2.2 基本思路

这个问题非常类似于 01 背包问题,所不同的时每种物品由无限件。也就是从物品的角度考虑,与它相关的策略已并非取或不取两种,而是取 0 件,取 1 件,取 2件……直至取 [V/Ci] 件等。

如果仍然按照 01 背包时的思路,令 F[i, v] 表示前 i 种物品放入一个容量为 v 的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:

$$ F[i,v]=max{F[i-1,v-kC_i]+W_i | 0\leq kC_i \leq v} $$

这跟 01 背包问题一样有 O(VN) 个状态需要求解,但求解每个状态的时间已经不是常数了,求解状态 F[i,v] 的时间是 O(v/Ci),总的复杂度可以认为是 O(NVΣv/Ci),是比较大的。

# 2.3 优化

  1. 若两件物品 i 和 j 满足 Ci <= Cj 且 Wi >= Wj ,那么可以直接去掉 j 物品。
  2. 直接去掉体积大于 V 的物品。

# 2.4 转化为 01 背包问题求解

完全背包问题的代码可以由 01 背包问题(一维数组)的代码简单转化而来。

01 背包问题按照 v 递减的次序来循环,让 v 递减是为了保证第 i 次循环的状态 F[i, v] 是由 F[i-1, v] 和 F[i-1,v-Ci] 递推而来。换句话说,这正是为了保证每间物品只选一次,保证在考虑 ”选入第 i 件物品“ 这件策略时,依据的是一个未选择第 i 件物品的子结果 F[i-1,v-Ci]。

而完全背包问题的特点时每种物品可以选无限件,所以在考虑 "加选第 i 件物品" 这种策略时,正需要一个可能已选入第 i 种物品的子结果 F[i,v-Ci],所以必须采用 v 递增的顺序取循环。

同时可以颠倒两层 for 循环获得算法时间常数上的优化。

代码:

/**
 * 完全背包问题解法
 * @param c 每个物品的体积,cost
 * @param w 每个物品的价值,worth
 * @param v 背包的容积
 * @return 最大价值
 */
public static int knapsackProblemAll(int[] c, int[] w, int v) {
    int n = c.length; // 物品数量
    int[] dp = new int[v + 1];
    for (int j = 0; j <= v; j--) {   // 循环容量
        for (int i = 1; i <= n; i++) {  // 循环物品
            if (j >= c[i - 1]) {
                dp[j] = Math.max(dp[j], dp[j - c[i - 1]] + w[i - 1]);  // 选第i件物品
            }
        }
    }
    return dp[v];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
编辑 (opens new window)
#数据结构与算法
上次更新: 2024/05/11, 20:31:24
排序算法
二叉树

← 排序算法 二叉树→

最近更新
01
位运算
05-21
02
二叉树
05-12
03
Spring三级缓存解决循环依赖
03-25
更多文章>
Theme by Vdoing | Copyright © 2022-2024 Marvel
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式