代码随想录算法训练营day40

0 阅读11分钟

复习背包问题

背包问题采用动态规划解题思路,重点分为01背包(物品只能选取一次,且不考虑组合),完全背包(物品选取次数不限量,而且存在排列问题),多重背包(物品选择存在数量限制,且存在排列组合问题)

01背包问题: 重点难点,物品只能取一次,采用一维数组遍历时必须先物品后背包遍历,内部背包遍历必须从大到小遍历,防止出现重复添加物品。 按照动态规划的五步走。 1.设计dp数组,是采用二维数组还是一维数组,明确dp数组的下标。常见的dp数组的下标表示背包的容量

2.得出递推方程。在理清思路之后,逐步进行迭代,这步通常是多数题目之间的区别,可能时采用max,min等取最值,也可能是单纯加减。动态规划的核心也是得出递推方程进行迭代。

3.初始化dp数组。 采用二维数组通常需要初始化最上行和最右一列。通常背包容量为0时,需要考虑填充1,因为即使不方物品也是一种装填方式,用来初始化列。而最上行,因为只有物品0,所以也可以迭代初始化。 采用一维数组则需要拥有滚动迭代的思想。在初始化时,将dp数组初始化为0,dp[0]初始化为1,然后使用物品0开始初始化迭代 4.确定遍历顺序 动态规划需要进行双层遍历,一层是对背包进行遍历,一层是对物品进行遍历,针对题目要求的不同,内外层遍历的顺序也不相同。01背包问题的一维数组遍历必须先物品再背包,内层循环的背包必须从大到小遍历来防止出现重复添加物品。

5.举例推导dp数组,这一步是再纸面上验证算法的可行性

完全背包问题: 重点难点:物品可以取无限次,同样可以采用一维数组和二维数组进行遍历,但是存在组合和排列的区别。因此在遍历顺序上也有大量考究。

1.设计dp数组,明确下标含义和内部存储意义,同01背包 2.得出递推方程,因为物品可以被无限次重复选用,因次在对比时需要增加是添加新物品,还是添加旧物品的区别。 3.初始化dp数组,同01数组,因为物品可以无限取用,所以在物品0的取用时,最上行需要迭代初始化,如果采用一维数组的化,则仍然可以考虑初始化为0或者1,根据情况选择。 4.确定遍历顺序,完全背包与01背包的解题最大区别在于完全背包存在组合问题和排列问题: 在二维数组中,遍历顺序并不需要区分,但是在一维数组中,遍历顺序必须严格区分 但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

相关题目如下:

如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:

对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了

多重背包问题:简化为多重01背包问题,代码的重点难点讲解有:


// 超时了
#include<iostream>
#include<vector>
using namespace std;
int main() {
    int bagWeight,n;//此处的n是指物品种类的数量
    cin >> bagWeight >> n;
    vector<int> weight(n, 0); 
    vector<int> value(n, 0);
    vector<int> nums(n, 0);
    for (int i = 0; i < n; i++) cin >> weight[i];
    for (int i = 0; i < n; i++) cin >> value[i];
    for (int i = 0; i < n; i++) cin >> nums[i];    
    
    for (int i = 0; i < n; i++) {
        while (nums[i] > 1) { // 物品数量不是一的,都展开,逐一将物品加入到数组向量当中,变成了多重01背包问题
            weight.push_back(weight[i]);
            value.push_back(value[i]);
            nums[i]--;
        }
    }
 
    vector<int> dp(bagWeight + 1, 0);
    for(int i = 0; i < weight.size(); i++) { // 遍历物品,注意此时的物品数量不是n,这里使用的是物品的总数
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    cout << dp[bagWeight] << endl;
}

超时是因为进行展开时需要对向量进行扩容。

 for (int i = 0; i < n; i++) {
        while (nums[i] > 1) { // 物品数量不是一的,都展开,逐一将物品加入到数组向量当中,变成了多重01背包问题
            weight.push_back(weight[i]);
            value.push_back(value[i]);
            nums[i]--;
        }
    }
 

两种方法解决该问题,一种是提前申请足够空间的容量,免去扩容花费的时间,第二种是在循环内部对物品数量进行循环遍历。


#include<iostream>
#include<vector>
using namespace std;
int main() {
    int bagWeight,n;
    cin >> bagWeight >> n;
    vector<int> weight(n, 0);
    vector<int> value(n, 0);
    vector<int> nums(n, 0);
    for (int i = 0; i < n; i++) cin >> weight[i];
    for (int i = 0; i < n; i++) cin >> value[i];
    for (int i = 0; i < n; i++) cin >> nums[i];

    vector<int> dp(bagWeight + 1, 0);

    for(int i = 0; i < n; i++) { // 遍历物品
        for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
            // 以上为01背包,然后加一个遍历个数
            for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
                dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
            }
        }
    }

    cout << dp[bagWeight] << endl;
}

打家劫舍review

一维普通打家劫舍: 1.根据题型设计数组,明确下标含义和存储内容,一般下标表示盗窃到第i个房间,存储内容为盗窃到第i个房间已经盗窃的金额 2.推出递推公式,简单的打家劫舍公式为求最大最优解,所以可以参考背包问题 3.数组初始化,分不同情况进行初始化选择,3. dp数组如何初始化

从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]

从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]); 4.确定遍历顺序,普通打家劫舍只需要遍历一次数组 5.代入数组进行举例推导

环形打家劫舍,仅仅多区分头尾两种情况,变成两种一维打家劫舍可以解决。

树形打家劫舍: 难点:采用的递归遍历方式,树形打家劫舍最后需要归结为是否对根节点进行盗窃,所以根节点是最后进行读取遍历的,因此要采用后序遍历。

1.明确采用后序遍历,采用递归返回值进行计算 2.递归公式为max(偷父节点,不偷父节点),存在两个递归值 3.为精简时间复杂度,应当采用记忆化数组或者使用动态规划的方法。

动态规划执行打家劫舍:

采用递归同动态规划相结合的方式。 1.明确递归函数返回值,传参。树形动态规划的传参是树根,返回值为规模为2的向量,确定该节点偷或者不偷,0表示不偷,1表示偷,存储内容分别为盗窃该节点金额,不盗窃该节点金额。 2.确定递归的终止条件,等同于初始化dp数组 3.确定遍历顺序,采用后序遍历。 4.明确递归递推公式也是单层递归逻辑。dp[0]和dp[1]分别采用递归的返回值进行计算。 5.代入实例进行验证递推。


class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> result = robTree(root);
        return max(result[0], result[1]);
    }
    // 长度为2的数组,0:不偷,1:偷
    vector<int> robTree(TreeNode* cur) {
        if (cur == NULL) return vector<int>{0, 0};
        vector<int> left = robTree(cur->left);
        vector<int> right = robTree(cur->right);
        // 偷cur,那么就不能偷左右节点。
        int val1 = cur->val + left[0] + right[0];
        // 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况
        int val2 = max(left[0], left[1]) + max(right[0], right[1]);
        return {val2, val1};
    }
};

股票买入问题 贪心算法可以回顾解决,这里采用动态规划五步走 1.设计二维数组,行下标表示第i只股票,列下标0,1分别表示为持有股票或者不持有股票 2.得出递推公式。当持有股票时,方程表达为dp[i][0]=max(dp[i-1][0],-prices[i]);表示两种选择,第一种选择为持有之前的股票不变,第二种为之前没有购入股票,现在购入股票。选择最大值。dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]);q前者表示为继续保持之前不持有股票的的状态,后者表示卖出当前持有股票的状态。一样两者对比选出极大值。 3.初始化,dp[0][0]=-prices[0];此时直接购入股票,金额为负,dp[0][1]=0;不持有股票,金额为0; 4.确定遍历顺序,只需要对prices进行一次遍历就可以了 5.举例对递推数组进行验证


class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if (len == 0) return 0;
        vector<vector<int>> dp(len, vector<int>(2));
        dp[0][0] -= prices[0];
        dp[0][1] = 0;
        for (int i = 1; i < len; i++) {
            dp[i][0] = max(dp[i - 1][0], -prices[i]);
            dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
        }
        return dp[len - 1][1];
    }
};

// 版本二
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        vector<vector<int>> dp(2, vector<int>(2)); // 注意这里只开辟了一个2 * 2大小的二维数组
        dp[0][0] -= prices[0];
        dp[0][1] = 0;
        for (int i = 1; i < len; i++) {
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]);
        }
        return dp[(len - 1) % 2][1];
    }
};

买卖股票II 与买卖股票I的区别在于股票可以被多次买入卖出,因此在递推公式方面略有区别。 递推公式。当持有股票时,方程表达为dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i]);表示两种选择,第一种选择为持有之前的股票不变,第二种为之前没有购入股票,现在购入股票。选择最大值。dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]);q前者表示为继续保持之前不持有股票的的状态,后者表示卖出当前持有股票的状态。一样两者对比选出极大值。


class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        vector<vector<int>> dp(len, vector<int>(2, 0));
        dp[0][0] -= prices[0];
        dp[0][1] = 0;
        for (int i = 1; i < len; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); // 注意这里是和121. 买卖股票的最佳时机唯一不同的地方。
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
        }
        return dp[len - 1][1];
    }
};

版本2


class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        vector<vector<int>> dp(2, vector<int>(2)); // 注意这里只开辟了一个2 * 2大小的二维数组
        dp[0][0] -= prices[0];
        dp[0][1] = 0;
        for (int i = 1; i < len; i++) {
            dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]);
            dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]);
        }//注意,这里%2用来交替滚动状态,因为数组规模为2,但是仅仅需要前一天和今天的状态交替,所以i仅仅需要%2就可以交替
        return dp[(len - 1) % 2][1];
    }
};

复杂有限次数买卖股票问题 问题的关键在于将多次买卖股票拆解为多个单次买卖股票关系,得出递推方程。


class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.size() == 0) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
        dp[0][1] = -prices[0];
        dp[0][3] = -prices[0];
        for (int i = 1; i < prices.size(); i++) {
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
            dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
        }
        return dp[prices.size() - 1][4];
    }
};

推广到指定次数的购买股票次数

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
         if (prices.size() == 0) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));//最多k次交易,因此一共有2k+1种状态
        for (int j = 1; j < 2 * k; j += 2) {//进行初始化操作,直接在第1只股票时执行反复买入卖出操作进行初始化,奇数买入,偶数不操作
            dp[0][j] = -prices[0];
        }
        for (int i = 1;i < prices.size(); i++) {//递推公式,同时对各状态进行迭代
            for (int j = 0; j < 2 * k - 1; j += 2) {
                dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
                dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
            }
        }
        return dp[prices.size() - 1][2 * k];
        
    }
};