算法笔记-动态规划-1

01背包

问题介绍

标准的01背包问题是指,有n(int型)个物品和最多装重量W(int型)的背包。weight数组表示物品的重量,即weight[i]表示第i个物品的重量;value数组表示物品的价值,即value[i]表示第i个物品的价值。问把哪些物品装入背包使得物品价值总和最大,每个物品只能装一次。

举例1,假设背包重量为4,物品信息由表格描述:

重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

问题转化

如果遇到的问题符合01背包的特点,可以思考是否能转化为01背包问题

  • 每个物品(元素)只能使用一次
  • 背包刚好装满
  • 放入的物品(元素)重量为元素的数值,价值也为元素的数值

求解方法

回溯法

每个物品要么取,要么不取,用回溯法搜索所有情况,时间复杂度O(2^n)

动态规划

用动态规划方法,以五个步骤来解决问题。以下都是针对例子1用二维dp数组描述

第一步确定dp数组含义

dp数组如图所示
在这里插入图片描述
dp[i][j]表示从下标为[0~i]的物品任意取,放入容量为j的背包,价值总和最大值
i表示物品,j表示容量

  • 注意i是小于n的,比如n=3有三个物品,i=1时,只能选择编号为0、1的物品放入背包
  • 注意j小于等于背包重量W。背包最多装W,但是当前的dp数组对应的背包最多装j

第二步确定递推公式

只有两个方向可以推出dp[i][j]

  • 由dp[i-1][j]推出。当前背包容量为j,只能从编号为[0 ~ i-1]的物品选择放入的最大价值,现在新增选项编号i的物品,dp[i][j]就是dp[i - 1][j]
  • 由dp[i-1][j-weight[i]推出。dp[i - 1][j - weight[i]] 表示背包容量为j - weight[i]的时候不放物品i的最大价值,现在背包刚好腾出物品i的重量,尝试加入物品i。即dp[i][j]=dp[i - 1][j - weight[i]]+value[i]

所以递推公式为

dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])

第三步初始化

dp数组初始化不能违背dp数组的定义
由dp数组递推公式知。推导dp数组即要用到dp数组前一行的值,也要用到前列的值,因此初始化dp数组第一行和第一列
第一列时背包容量为0,因此全初始化为0,即dp[i][0]=0,i为0~n-1
在这里插入图片描述

第一行时只能选择物品0,背包容量逐渐增加到W。因此dp[0][j]中,容量j大于等于物品0的重量weight[0]时,dp[0][j]为物品0的价值,j小于其时为0。
在这里插入图片描述
所以初始化第一行时,遵循递推公式的正序遍历,代码有两次循环,要想只有一次循环,应采用倒序遍历

for (int j = W; j >= weight[0]; j--) {
    dp[0][j] = dp[0][j - weight[0]] + value[0];
}

循环遍历初始化时,代码也要遵循递推公式,但是正序遍历,会把物品0加入多次

for (int j = weight[0]; j <= W; j++) {
    dp[0][j] = dp[0][j - weight[0]] + value[0];
}

第四步确定遍历顺序

第三步初始化的dp数组如下
在这里插入图片描述
有两个遍历维度:物品和背包重量,因此有两层for循环
这里先遍历物品或先遍历背包重量都可以,但先遍历物品更好理解

for(int i = 1; i < n; i++) { // 遍历物品
    for(int j = 0; j <= W; j++) { // 遍历背包容量 
        if (j < weight[i]) dp[i][j] = dp[i - 1][j];
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        
    }
}

第五步推导dp数组

最终的dp数组结果如下
在这里插入图片描述
最终结果就是dp[2][4]

如果修改遍历时的判断条件:

if (j - weight[i] >= 0)  
  dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

dp数组变成:
在这里插入图片描述
完整代码如下:

 vector<int> weight = {1, 3, 4};
    vector<int> value = {15, 20, 30};
    int W = 4; //背包重量
    int n = 3;  //物品数量

    // 二维数组
    vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));

    // 初始化 
    for (int j = W; j >= weight[0]; j--) {
        dp[0][j] = dp[0][j - weight[0]] + value[0];
    }

    // weight数组的大小 就是物品个数
    for(int i = 1; i < n; i++) { // 遍历物品
        for(int j = 0; j <= W; j++) { // 遍历背包容量
            if (j - weight[i] >= 0)  dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
        }
        cout << dp[n][W]<<endl;
    }

优化方法

可以用一维dp数组(滚动数组)解决01背包问题
因为发现上一行的数据可以重复利用,推导的第一种dp数组上一行的数据是直接拷贝到下一行
因此可以把dp数组压缩成一维数组:
dp : [0, 15, 15, 20, 35]
dp[j]表示容量为j的背包,所背的物品价值可以最大为dp[j]。
其递推公式是

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

需要注意的是,一维dp数组的遍历顺序是背包容量从大到小,为了保证每个物品只放入一次
所以遍历顺序只能是先遍历物品再遍历背包容量

vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
int W = 4; //背包重量
int n = 3;  //物品数量

// 初始化
vector<int> dp(W + 1, 0);
for(int i = 0; i < n; i++) { // 遍历物品
    for(int j = W; j >= weight[i]; j--) { // 遍历背包容量
       dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
cout << dp[W] << endl;

猜你喜欢

转载自blog.csdn.net/MinutkiBegut/article/details/114131230
今日推荐