首页 > 其他分享 >动态规划之 01背包问题

动态规划之 01背包问题

时间:2023-07-08 16:45:47浏览次数:36  
标签:01 容量 int 背包 物品 最优 动态 dp

1.  什么是01背包问题?

01背包问题是一种经典的组合优化问题,它的描述如下:

有n种物品和一个容量为C的背包,每种物品有一个重量w[i]和一个价值v[i],其中i=1,2,…,n。问如何选择物品放入背包,使得背包内的物品总价值最大,且不超过背包的容量?

这里的01表示每种物品只能选择放入或不放入,不能分割或重复。

2.   为什么可以用动态规划解决?

动态规划是一种解决复杂问题的方法,它的核心思想是将一个大问题分解为若干个子问题,然后自底向上地求解子问题,并将子问题的解保存起来,避免重复计算。最后,通过子问题的解组合出原问题的解。

动态规划适用于具有最优子结构和重叠子问题的问题。最优子结构指的是一个问题的最优解可以由其子问题的最优解构成;重叠子问题指的是在求解过程中,有些子问题会被多次遇到和求解。

01背包问题就具有这两个特点。首先,我们可以将原问题分解为n个阶段,每个阶段对应一种物品。在每个阶段,我们需要决定是否将该物品放入背包。如果我们已经知道了前i-1个阶段的最优解,那么第i个阶段的最优解就可以由以下两种情况中的较大者得到:

  • 如果第i种物品的重量w[i]大于当前背包的剩余容量j,那么我们不能放入该物品,此时第i个阶段的最优解就等于前i-1个阶段的最优解,即V(i,j)=V(i-1,j);
  • 如果第i种物品的重量w[i]小于等于当前背包的剩余容量j,那么我们可以选择放入或不放入该物品。如果不放入,那么第i个阶段的最优解仍然等于前i-1个阶段的最优解,即V(i,j)=V(i-1,j);如果放入,那么第i个阶段的最优解等于前i-1个阶段在容量为j-w[i]时的最优解加上第i种物品的价值v[i],即V(i,j)=V(i-1,j-w[i])+v[i]。因此,在这种情况下,我们需要在这两种选择中取较大者作为第i个阶段的最优解,即V(i,j)=max{V(i-1,j),V(i-1,j-w[i])+v[i]}。

这样,我们就得到了一个递推关系式,它表明了原问题的最优解可以由其子问题的最优解构成,即具有最优子结构。

其次,在求解过程中,我们会遇到很多相同或类似的子问题。例如,在求解V(i,j)时,我们需要知道V(i-1,j)和V(i-1,j-w[i]);而在求解V(i+1,j)时,我们又需要知道V(i,j)和V(i,j-w[i+1])。这些子问题都涉及到前面某些阶段在某些容量下的最优解。如果我们每次都重新计算这些子问题,那么会造成很多重复的工作。为了避免这种情况,我们可以用一个二维数组来保存子问题的解,每次遇到一个子问题,我们先查看数组中是否已经有该子问题的解,如果有,就直接使用;如果没有,就按照递推关系式计算,并将结果存入数组中。这样,我们就可以利用已经求解过的子问题的解,避免重复计算,即利用了重叠子问题。

3.   如何用动态规划求解?

根据上面的分析,我们可以用以下步骤来求解01背包问题:

  • 定义一个二维数组dp[n+1][C+1],其中dp[i][j]表示在前i种物品和容量为j的背包下的最优解;
  • 初始化边界条件,即dp[0][j]=0(没有物品时,最优解为0)和dp[i][0]=0(没有容量时,最优解为0);
  • 从第一种物品开始,遍历每一种物品i;
  • 对于每一种物品i,从第一个单位容量开始,遍历每一个单位容量j;
  • 对于每一个单位容量j,根据递推关系式计算dp[i][j];
  • 最后,返回dp[n][C]作为原问题的最优解。

4.  有什么实例可以说明?

假设有4种物品和一个容量为8的背包,每种物品的重量和价值如下表所示:

 

物品编号重量价值
1 2 3
2 3 4
3 4 5
4 5 6

按照动态规划的步骤,我们可以得到以下的二维数组:

i\j012345678
0 0 0 0 0 0 0 0 0 0
1 0 0 3 3 3 3 3 3 3
2 0 0 3 4 4 7 7 7 7
3 0 0 3 4 5 7 8 9 9
4  0 3 5 8  9  10

我们可以看到,在填写第一行时,由于没有物品可选,所以所有的最优解都是0;在填写第二行时,由于只有第一种物品可选,所以只有当容量大于等于2时才能放入该物品,并获得价值为3的最优解;在填写第三行时,由于有两种物品可选,所以当容量大于等于2时可以选择放入或不放入第二种物品,并取较大者作为最优解;以此类推,在填写第四行时,由于有三种物品可选,所以当容量大于等于4时可以选择放入或不放入第三种物品,并取较大者作为最优解。

最后,在填写第五行时,由于有四种物品可选,所以当容量大于等于5时可以选择放入或不放入第四种物品,并取较大者作为最优解。填写完毕后

 

我们可以看到,最右下角的值dp[4][8]就是原问题的最优解,即在四种物品和容量为8的背包下的最大价值为10。如果我们想知道这个最优解是由哪些物品组成的,我们可以根据递推关系式反向推导。由于dp[4][8]=dp[3][8-w[4]]+v[4]=dp[3][3]+6=4+6=10,说明第四种物品被放入了背包,并且在放入之前,背包的剩余容量为3,对应的最优解为4。然后我们继续查看dp[3][3],由于dp[3][3]=dp[2][3-w[3]]+v[3]=dp[2][0]+5=0+5=5,说明第三种物品也被放入了背包,并且在放入之前,背包的剩余容量为0,对应的最优解为0。这时候,我们已经找到了所有被放入背包的物品,即第三种和第四种物品。因此,我们可以得出结论:在四种物品和容量为8的背包下的最大价值为10,且是由第三种和第四种物品组成的。

  

5.   如何用Java实现动态规划算法?

根据上面的步骤和逻辑,我们可以用Java语言来实现动态规划算法。以下是一个可能的代码:

public class Knapsack {

    // 物品的重量
    private int[] w = {2, 3, 4, 5};
    // 物品的价值
    private int[] v = {3, 4, 5, 6};
    // 物品的数量
    private int n = w.length;
    // 背包的容量
    private int C = 8;
    // 动态规划表
    private int[][] dp = new int[n + 1][C + 1];

    // 求解01背包问题
    public void solve() {
        // 初始化边界条件
        for (int i = 0; i <= n; i++) {
            dp[i][0] = 0;
        }
        for (int j = 0; j <= C; j++) {
            dp[0][j] = 0;
        }
        // 填写动态规划表
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= C; j++) {
                if (j < w[i - 1]) {
                    // 第i种物品的重量大于当前背包的剩余容量,不能放入
                    dp[i][j] = dp[i - 1][j];
                } else {
                    // 第i种物品的重量小于等于当前背包的剩余容量,可以选择放入或不放入
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
                }
            }
        }
        // 输出最优解
        System.out.println("最大价值为:" + dp[n][C]);
        // 输出最优解的组成
        System.out.print("最优解的组成为:");
        int j = C;
        for (int i = n; i > 0; i--) {
            if (dp[i][j] > dp[i - 1][j]) {
                // 第i种物品被放入了背包
                System.out.print("物品" + i + " ");
                j = j - w[i - 1];
            }
            if (j == 0) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        Knapsack knapsack = new Knapsack();
        knapsack.solve();
    }
}

输出结果为:

最大价值为:10
最优解的组成为:物品4 物品3

自行实现如下:

public static void main(String[] args) {


int[] w = {2, 3, 4, 5}; // weight
int[] v = {3, 4, 5, 6}; // value
// 背包的容量
int N = w.length;
int C = 8;
System.out.println(pack(N, C, w, v));
System.out.println(pack2(N, C, w, v));

}
public static int pack(int N, int C, int[] w, int[] v) {

int[][] dp = new int[N + 1][C + 1];
for (int i = 1; i <= N; i++) {
for (int j = 0; j <= C; j++) {
if (j < w[i - 1]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i - 1]] + v[i - 1]);
}
}
}
return dp[N][C];

}

6.  有什么优化和变形的方法?

动态规划算法虽然可以有效地求解01背包问题,但是它也有一些缺点和局限性。例如,它需要一个二维数组来存储子问题的解,这会占用很多空间;它也需要遍历所有的物品和容量,这会消耗很多时间。因此,有一些优化和变形的方法可以提高动态规划算法的效率和适用性。

  • 空间优化:由于动态规划表的每一行只依赖于上一行,所以我们可以用一个一维数组来代替二维数组,从而节省空间。具体地,我们可以用一个长度为C+1的数组dp来表示当前阶段的最优解,然后从后往前更新dp的值,即dp[j]=max{dp[j],dp[j-w[i-1]]+v[i-1]}。这样,我们就不需要保存之前阶段的最优解了,只需要一个一维数组就可以完成动态规划。
  • 时间优化:由于动态规划表的每一行中,只有部分元素会被更新,而且更新的范围是从w[i-1]到C,所以我们可以只遍历这部分元素,从而节省时间。具体地,我们可以用一个变量maxW来记录当前阶段能够达到的最大重量,然后在每次更新dp时,只遍历从w[i-1]到maxW之间的元素,并更新maxW的值为max{maxW,j}。这样,我们就不需要遍历所有的容量了,只需要遍历可能被更新的容量就可以完成动态规划。
  • 变形问题:01背包问题还有一些变形和扩展的问题,例如完全背包问题(每种物品可以无限制地放入)、多重背包问题(每种物品有限定的数量)、分组背包问题(物品分为若干组,每组只能选择一个)、多维背包问题(物品有多种属性,背包有多种限制)等等。这些问题都可以用类似的思路和方法来求解,只是在递推关系式和填表过程中有一些不同。感兴趣的读者可以自行查阅相关资料和代码。

 

7.  一维数组的优化方法

7.1  二维转 一维的两个问题

问题1: 为什么二维可以转化为一维

二维数组的每一行表示在前i种物品和不同容量下的最优解,而每一行的值只依赖于上一行的值,即dp[i][j]只与dp[i-1][j]和dp[i-1][j-w[i-1]]有关。因此,我们不需要保存所有的行,只需要保存当前行和上一行的值就可以了。而由于当前行的值会覆盖上一行的值,所以我们可以用一个一维数组来代替二维数组,只保存当前行的值,即dp[j]表示在当前阶段和容量为j的背包下的最优解。这样,我们就可以节省空间,从而优化动态规划算法。

问题2: 为什么是逆序而不是顺序

如果我们按照顺序更新dp[j]的值,即从j=0到j=C,那么我们会遇到一个问题:当我们更新dp[j]时,我们需要用到dp[j-w[i-1]]的值,但是这个值可能已经被更新过了,不再是上一阶段的值,而是当前阶段的值。这样就会导致错误的结果。例如,在更新第二种物品时,如果我们按照顺序更新dp[3]的值,那么我们会得到dp[3]=max{dp[3],dp[3-3]+4}=max{7,4}=7,这里的dp[3]已经是放入第一种物品后的最优解了,而不是上一阶段的最优解。这样就忽略了不放入第二种物品的情况,导致错误的结果。

为了避免这个问题,我们可以按照逆序更新dp[j]的值,即从j=C到j=w[i-1],这样就可以保证在更新dp[j]时,dp[j-w[i-1]]还是上一阶段的值,没有被覆盖。例如,在更新第二种物品时,如果我们按照逆序更新dp[3]的值,那么我们会得到dp[3]=max{dp[3],dp[3-3]+4}=max{0,4}=4,这里的dp[3]还是上一阶段的最优解,没有被覆盖。这样就可以正确地考虑放入或不放入第二种物品的情况,得到正确的结果。

7.2   推导过程

我们仍然用上面的例子来说明一维数组的优化方法。假设有4种物品和一个容量为8的背包,每种物品的重量和价值如下表所示:

物品编号重量价值
1 2 3
2 3 4
3 4 5
4 5 6

我们定义一个一维数组dp[C+1],其中dp[j]表示在当前阶段和容量为j的背包下的最优解。我们按照以下步骤来更新dp的值:

    • 初始化边界条件,即dp[0]=0(没有容量时,最优解为0);
    • 从第一种物品开始,遍历每一种物品i;
    • 对于每一种物品i,从容量为C开始,递减到w[i-1],遍历每一个单位容量j;
    • 对于每一个单位容量j,根据递推关系式更新dp[j],即dp[j]=max{dp[j],dp[j-w[i-1]]+v[i-1]};
    • 最后,返回dp[C]作为原问题的最优解。

我们可以用以下的表格来表示dp数组的变化过程:

i\j012345678
初始状态                  

我们可以看到,在初始化时,dp数组的所有元素都是0;在更新第一种物品时,由于只有当容量大于等于2时才能放入该物品,并获得价值为3的最优解,所以我们从后往前更新dp[2]到dp[8]的值;在更新第二种物品时,由于有两种物品可选,所以当容量大于等于3时可以选择放入或不放入第二种物品,并取较大者作为最优解,所以我们从后往前更新dp[3]到dp[8]的值;以此类推,在更新第三种和第四种物品时,我们也从后往前更新dp数组的值。最后,我们得到了dp[8]=10作为原问题的最优解。

7.3 方程式

根据上面的推导过程,我们可以得到以下的方程式:

​dp[0]=0dp[j]=max{dp[j],dp[j−w[i−1]]+v[i−1]},i=1,2,...,n;j=w[i−1],w[i−1]+1,...,C​

7.4  Java实现

根据上面的方程式,我们可以用Java语言来实现一维数组的优化方法。以下是一个可能的代码:

public class Knapsack {

    // 物品的重量
    private int[] w = {2, 3, 4, 5};
    // 物品的价值
    private int[] v = {3, 4, 5, 6};
    // 物品的数量
    private int n = w.length;
    // 背包的容量
    private int C = 8;
    // 动态规划数组
    private int[] dp = new int[C + 1];

    // 求解01背包问题
    public void solve() {
        // 初始化边界条件
        dp[0] = 0;
        // 遍历每一种物品
        for (int i = 1; i <= n; i++) {
            // 遍历每一个单位容量,从后往前更新
            for (int j = C; j >= w[i - 1]; j--) {
                // 更新dp[j]的值
                dp[j] = Math.max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
            }
        }
        // 输出最优解
        System.out.println("最大价值为:" + dp[C]);
        // 输出最优解的组成
        System.out.print("最优解的组成为:");
        int j = C;
        for (int i = n; i > 0; i--) {
            if (dp[j] > dp[j - w[i - 1]]) {
                // 第i种物品被放入了背包
                System.out.print("物品" + i + " ");
                j = j - w[i - 1];
            }
            if (j == 0) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        Knapsack knapsack = new Knapsack();
        knapsack.solve();
    }
}

输出结果为:

最大价值为:10
最优解的组成为:物品4 物品3

 

 

自行实现如下:

public static int pack2(int N, int C, int[] w, int[] v) {

int[] dp = new int[C + 1];
for (int i = 1; i <= N; i++) {
for (int j = C; j >= 0; j--) {
if (j >= w[i - 1]) {
dp[j] = Math.max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
}
}
}
return dp[C];

}

8.  总结

动态规划是一种强大而灵活的算法思想,它可以有效地解决许多组合优化问题。01背包问题是动态规划的经典例子,它可以帮助我们理解动态规划的原理和步骤,并为我们解决其他类似或更复杂的问题提供思路和方法。希望本文能够对您有所帮助。谢谢!

 

 

 

 

 

标签:01,容量,int,背包,物品,最优,动态,dp
From: https://www.cnblogs.com/shoshana-kong/p/17533310.html

相关文章

  • [模板]01trie,维护异或最大值
    //查询异或最大值,每次插入和查询时间都是log(C)template<classT>classtrie01{vector<vector<T>>tree;public:trie01():tree(1,vector<T>(2,0)){}//插入待检查的数字voidinsert(Tx){intp=0;for(inti=sizeof......
  • 动态规划之 多重背包
    动态规划之多重背包问题1. 问题描述及分析动态规划是一种解决复杂问题的方法,它将一个大问题分解为若干个子问题,通过求解子问题,从而得到原问题的最优解。动态规划的核心思想是避免重复计算,利用已有的结果进行状态转移。背包问题是一类经典的动态规划问题,它描述了如何在给......
  • 动态规划 背包问题总结
     目录 01背包二维写法01背包一维写法完全背包二维带枚举写法完全背包二维普通写法完全背包一维写法多重背包二维写法多重背包二进制优化 1. 01背包二维写法dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1]) //......
  • 动态规划之 完全背包
    1.题目有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关......
  • 统计的系统客观性与动态进化性•Freq频率与Bayes两大学派及争论•统计推断•Bayes学派
    统计的系统客观性:统计数据及其活动不是片面的,而是系统客观反映客观现象。周期的做“总体统计”+随机/按需/周期做“抽样统计”;统计的动态进化性:统计数据及其活动不是静止的,持续的更新(量变)与进化(质变)。先验信息的收集挖掘和加工,数量化,形成"先验分布"并持续进化。p......
  • 一文彻底搞懂MySQL基础:B树和B+树的区别 转载 https://blog.csdn.net/a519640026/arti
    写在前面大家在面试的时候,肯定都会被问到MySql的知识,以下是面试场景:面试官:对于MySQL,你对他索引原理了解吗?我:了解面试官:MySQL的索引是用什么数据机构的?我:B+树面试官:为什么要用B+树,而不是B树?我:…面试官:用B+树作为MySql的索引结构,用什么好处?我:…B树和B+树是MySQL索引使用的数据结构......
  • [安洵杯 2019]crackMe
    [安洵杯2019]crackMe将exe文件放入ida打开后,首先按shift+F12查看字符串,发现了base64的编码表和一串疑似经过加密/编码处理过后的字符串此时推测该字符串不是由正常base64编码得来,可能进行了换码操作,因此点进引用了该编码表的地方此时发现该函数对base64的编码表进行了大小写......
  • JavaScript-Day01
    1、JavaScript:是与网页交互的脚本语言。2、组成部分:{ ECMAScript,文档对象模型(DOM),浏览器对象模型(BOM)}    2.1 ECMAScript(核心):由ECMA-262定义并提供核心功能。<!--宿主环境-->        1.基本层面定义:语法、类型、语句、关键、保留字、操作符、全局对象。   ......
  • 题解 P8648【[蓝桥杯 2017 省 A] 油漆面积】
    怎么题解区全是扫描线,还有个\(O(n^3)\)暴力老哥。为防止误导新人,给个理论上稳过的\(O(n^2)\)解法。二维前缀和可以处理若干次单点加,最后若干次矩形查的问题。将其差分,即可处理若干次矩形加,最后若干次单点查的问题。于是我们使用差分将所有矩形加上,然后做一遍二维前缀和,即......
  • https://www.zhihu.com/tardis/bd/art/627016379?source_id=1001
    1、ODS原始数据层ODS层保存所有操作数据,不对原始数据做任何处理。在业务系统和数据仓库之间形成一个隔离,源系统数据结构的变化不影响其他数据分层。减轻业务系统被反复抽取的压力,由ODS统一进行抽取和分发。记住ODS层数据要保留数据的原始性。处理原则:根据源业务系统表的情况以......