Bootstrap

代码随想录1刷—动态规划篇(三)

代码随想录1刷—动态规划篇(三)

198. 打家劫舍

1、dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]

2、决定 d p [ i ] dp[i] dp[i] 的因素就是第 i i i 房间偷还是不偷。

  • 如果偷第 i i i 房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第 i − 1 i-1 i1 房一定是不考虑的,找出 下标 i − 2 i-2 i2 (包括 i − 2 i-2 i2 )以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱
  • 如果不偷第 i i i 房间,那么dp[i] = dp[i - 1],即考虑 i-1 房(注意是考虑,不是一定偷 i − 1 i-1 i1 房)

然后 d p [ i ] dp[i] dp[i] 取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);

3、递推公式的基础就是 d p [ 0 ] dp[0] dp[0] d p [ 1 ] dp[1] dp[1]dp[0] = nums[0],dp[1] = max(nums[0], nums[1]);

4、 d p [ i ] dp[i] dp[i] 是根据 d p [ i − 2 ] dp[i-2] dp[i2] d p [ i − 1 ] dp[i-1] dp[i1] 推导出来的,那么一定是从前到后遍历

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

213. 打家劫舍 II

和 198.打家劫舍 的唯一区别就是 房屋成环了。对于一个数组,成环的话主要有如下三种情况:
在这里插入图片描述

情况二 和 情况三 包含情况一,所以只考虑情况二和情况三就可以。

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 0) return 0;
        if(nums.size() == 1) return nums[0];
        int result1 = robRange(nums, 0, nums.size() - 2);	//情况2
        int result2 = robRange(nums, 1, nums.size() - 1);	//情况3
        return max(result1, result2);
    }
    int robRange(vector<int>& nums, int start, int end){
        if(end == start) return nums[start];
        vector<int> dp(nums.size(), 0);
        dp[start] = nums[start];
        dp[start + 1] = max(nums[start], nums[start + 1]);
        for(int i = start + 2; i <= end; i++){
            dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);	//思路和 198.打家劫舍 一样
        }
        return dp[end];
    }
};

337. 打家劫舍 III

如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就考虑抢左右孩子(注意是“考虑”不是必抢)。

后序遍历(左右中),因为需要通过递归函数的返回值来做下一步计算

暴力递归(超时)
class Solution {
public:
    int rob(TreeNode* root) {
        if(root == nullptr) return 0;
        if(root->left == nullptr && root->right == nullptr) return root->val;
        //偷父节点
        int val1 = root->val;
        if(root->left) val1 += rob(root->left->left) + rob(root->left->right);
        if(root->right) val1 += rob(root->right->left) + rob(root->right->right);
        //不偷父节点
        int val2 = rob(root->left) + rob(root->right);
        return max(val1, val2);
    }
};

这个递归的过程中其实是有重复计算的。计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。

记忆化递归

可以使用map把计算过的结果保存,如果计算过孙子了,那算孩子的时候可以复用孙子节点的结果。

class Solution {
public:
    unordered_map<TreeNode* ,int> umap;
    int rob(TreeNode* root) {
        if(root == nullptr) return 0;
        if(root->left == nullptr && root->right == nullptr) return root->val;
        if(umap[root]) return umap[root];
        //偷父节点
        int val1 = root->val;
        if(root->left) val1 += rob(root->left->left) + rob(root->left->right);
        if(root->right) val1 += rob(root->right->left) + rob(root->right->right);
        //不偷父节点
        int val2 = rob(root->left) + rob(root->right);
        umap[root] = max(val1, val2);
        return max(val1, val2);
    }
};
动态规划

前两种方法其实对一个节点 偷与不偷得到的最大金钱 都没有做记录,而是进行实时计算。

而动态规划其实就是使用状态转移容器来记录状态的变化,因此这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。

树形dp,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解

1、确定递归函数的参数和返回值

要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组,参数为当前节点。

vector<int> robTree(TreeNode* cur) {

其实这里的返回数组就是dp数组。dp数组下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。

长度为2的数组怎么标记树中每个节点的状态呢?在递归的过程中,系统栈会保存每一层递归的参数。

2、确定终止条件:在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回

if (cur == NULL) return vector<int>{0, 0};

这也相当于dp数组的初始化。

3、确定遍历顺序:后序遍历。 因为通过递归函数的返回值来做下一步计算。

4、确定单层递归的逻辑:

  • 如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0];

  • 如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> result = robTree(root);
        return max(result[0], result[1]);
    }
    vector<int> robTree(TreeNode* cur){
        if(cur == nullptr) return vector<int> {0,0};
        vector<int> left = robTree(cur->left);
        vector<int> right = robTree(cur->right);
        int val1 = cur->val + left[0] + right[0];                    //偷cur
        int val2 = max(left[0],left[1]) + max(right[0],right[1]);    //不偷cur
        return {val2, val1};
    }
};

121. 买卖股票的最佳时机

暴力解法(超时)
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int result = 0;
        for (int i = 0; i < prices.size(); i++) {
            for (int j = i + 1; j < prices.size(); j++){
                result = max(result, prices[j] - prices[i]);
            }
        }
        return result;
    }
};//找最优间距
贪心解法

股票就买卖一次,那么取最左最小值,取最右最大值,那么得到的差值就是最大利润。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int low = INT_MAX;
        int result = 0;
        for (int i = 0; i < prices.size(); i++) {
            low = min(low, prices[i]);  			// 取最左最小价格
            result = max(result, prices[i] - low);  // 直接取最大区间利润
        }
        return result;
    }
};
动态规划

1、 d p [ i ] [ 0 ] dp[i][0] dp[i][0] 表示第 i i i 天持有股票所得最多现金 ; d p [ i ] [ 1 ] dp[i][1] dp[i][1]表示第 i i i 天不持有股票所得最多现金。

2、如果第 i i i 天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0], 那么可以由两个状态推出来:

  • i − 1 i-1 i1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
  • i i i 天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]

那么 d p [ i ] [ 0 ] dp[i][0] dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);

如果第 i i i 天不持有股票即 d p [ i ] [ 1 ] dp[i][1] dp[i][1], 也可以由两个状态推出来:

  • i − 1 i-1 i1 天就不持有股票,那就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
  • i i i 天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]

同样 d p [ i ] [ 1 ] dp[i][1] dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);

本题中不持有股票状态所得金钱一定比持有股票状态得到的多!最后直接取 d p [ i ] [ 1 ] dp[i][1] dp[i][1]就可以啦。

3、由递推公式可以看出其基础都是要从 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0] d p [ 0 ] [ 1 ] dp[0][1] dp[0][1] 推导出来的。那么 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]表示第 0 0 0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] = -prices[0]; 而 d p [ 0 ] [ 1 ] dp[0][1] dp[0][1]表示第 0 0 0 天不持有股票,不持有股票那么现金就是 0 0 0,所以dp[0][1] = 0;

4、从递推公式可以看出 d p [ i ] dp[i] dp[i] 都是有 d p [ i − 1 ] dp[i - 1] dp[i1] 推导出来的,那么一定是从前向后遍历

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];
    }
};
优化

从递推公式可以看出, d p [ i ] dp[i] dp[i]只是依赖于 d p [ i − 1 ] dp[i - 1] dp[i1]的状态。那只需要记录当前天的 d p dp dp 状态和前一天的 d p dp dp 状态就可以了,可以使用滚动数组来节省空间。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if(len == 0) return 0;
        vector<vector<int>> dp( 2,vector<int>(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];
    }
};

122. 买卖股票的最佳时机 II

本题和 121. 买卖股票的最佳时机 的唯一区别本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票),这个区别主要是体现在递推公式上,其他和121. 买卖股票的最佳时机一样。

  • dp[i][0] 表示第i天持有股票所得现金。dp[i][1] 表示第i天不持有股票所得最多现金。

如果第i天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0], 那么可以由两个状态推出来

  • 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即: d p [ i − 1 dp[i - 1 dp[i1][0]
  • 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即: d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i1][1]prices[i]

在 121 中股票全程只能买卖一次,如果买入股票,那 i i i 天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0]一定是 − p r i c e s [ i ] -prices[i] prices[i]。而本题因为一只股票可买卖多次,所以第 i i i 天买入股票时,所持有的现金可能有之前买卖过的利润。那第 i i i天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0],如果第 i i i天买入,所得现金就是昨天不持有股票的所得现金减去今天的股票价格 即: d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i1][1]prices[i]

如果第 i i i 天不持有股票即 d p [ i ] [ 1 ] dp[i][1] dp[i][1],依然可以由两个状态推出来:

  • i − 1 i-1 i1 天就不持有股票,那就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
  • i i i 天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0]
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];
    }
};
优化
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]);
        }
        return dp[(len - 1) % 2][1];
    }
};

123. 买卖股票的最佳时机 III

至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。

1、确定dp数组以及下标的含义,一天一共有五个状态,如下:

  1. 没有操作
  2. 第一次买入
  3. 第一次卖出
  4. 第二次买入
  5. 第二次卖出

dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。

需要明确:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票

2、递推公式:

达到 d p [ i ] [ 1 ] dp[i][1] dp[i][1]状态,有两个具体操作:

  • 操作一:第 i i i天买入股票了,那么 d p [ i ] [ 1 ] = d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] dp[i][1] = dp[i-1][0] - prices[i] dp[i][1]=dp[i1][0]prices[i]
  • 操作二:第 i i i天没有操作,而是沿用前一天买入的状态,即: d p [ i ] [ 1 ] = d p [ i − 1 ] [ 1 ] dp[i][1] = dp[i - 1][1] dp[i][1]=dp[i1][1]

所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);

同理 d p [ i ] [ 2 ] dp[i][2] dp[i][2]也有两个操作:

  • 操作一:第 i i i天卖出股票了,那么 d p [ i ] [ 2 ] = d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] dp[i][2] = dp[i - 1][1] + prices[i] dp[i][2]=dp[i1][1]+prices[i]
  • 操作二:第 i i i天没有操作,沿用前一天卖出股票的状态,即: d p [ i ] [ 2 ] = d p [ i − 1 ] [ 2 ] dp[i][2] = dp[i - 1][2] dp[i][2]=dp[i1][2]

所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])

同理,可推出剩下状态部分:

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]);

3、初始化:

没有操作:dp[0][0] = 0;

第一次买入:dp[0][1] = -prices[0];

第一次卖出:卖出操作一定收获利润,但最差情况也就是没有盈利即全程无操作现金为0,递推公式中可以看出每次是取利润最大值,那么收获利润如果比0还小了就没有必要收获这个利润了。所以dp[0][2] = 0;

第二次买入:第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。所以第二次买入操作,初始化为:dp[0][3] = -prices[0];

第二次卖出:同理第二次卖出初始化dp[0][4] = 0;

4、一定是从前向后遍历,因为 d p [ i ] dp[i] dp[i],依靠 d p [ i − 1 ] dp[i - 1] dp[i1]的数值。

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(vector<int>& prices) {
        if(prices.size() == 0) return 0;
        vector<int> dp(5, 0);
        dp[1] = -prices[0];
        dp[3] = -prices[0];
        for(int i = 1; i < prices.size(); i++){
            dp[1] = max(dp[1], dp[0] - prices[i]);
            dp[2] = max(dp[2], dp[1] + prices[i]);
            dp[3] = max(dp[3], dp[2] - prices[i]);
            dp[4] = max(dp[4], dp[3] + prices[i]);
        }
        return dp[4];
    }
};

188. 买卖股票的最佳时机 IV

要求至多有k次交易。

1、使用二维数组 d p [ i ] [ j ] dp[i][j] dp[i][j] :第 i i i天的状态为 j j j,所剩下的最大现金是 d p [ i ] [ j ] dp[i][j] dp[i][j]

j j j 的状态表示为:

  • 0 表示不操作
  • 1 第一次买入
  • 2 第一次卖出
  • 3 第二次买入
  • 4 第二次卖出
  • …除了0以外,偶数就是卖出,奇数就是买入。

题目要求是至多有K笔交易,那么 j j j 的范围就定义为 2 ∗ k + 1 2 * k + 1 2k+1就可以了。

所以二维dp数组定义为:

vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));

PS: d p [ i ] [ 1 ] dp[i][1] dp[i][1],表示的是第 i i i天,买入股票的状态,并不是说一定要第 i i i天买入股票

2、类比123即可。唯一不同的是需要类比j为奇数是买,偶数是卖的状态

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]);
}

3、不用管第几次,现在手头上没有现金,只要买入,现金就做相应的减少。因此 d p [ 0 ] [ j ] dp[0][j] dp[0][j] j j j 为奇数的时候都初始化为 − p r i c e s [ 0 ] -prices[0] prices[0]

for (int j = 1; j < 2 * k; j += 2) {
    dp[0][j] = -prices[0];
}

4、一定是从前向后遍历,因为 d p [ i ] dp[i] dp[i],依靠 d p [ i − 1 ] dp[i - 1] dp[i1]的数值。

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));
        for(int j = 1; j < 2 * k; j += 2){
            dp[0][j] = -prices[0];
        }
        for(int i = 1; i < prices.size(); i++){
            for(int j = 0; j < 2 * k; 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];
    }
};

309. 最佳买卖股票时机含冷冻期

122. 买卖股票的最佳时机 II相比唯一的不同在于:含有冷冻期。

1、dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。

  • 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)
  • 卖出股票状态
    • 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态
    • 状态三:今天卖出了股票
  • 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

2、递推公式

达到买入股票状态(状态一)即: d p [ i ] [ 0 ] dp[i][0] dp[i][0],有两个具体操作:

  • 操作一:前一天就是持有股票状态(状态一), d p [ i ] [ 0 ] = d p [ i − 1 ] [ 0 ] dp[i][0] = dp[i - 1][0] dp[i][0]=dp[i1][0]
  • 操作二:今天买入了,有两种情况
    • 前一天是冷冻期(状态四), d p [ i − 1 ] [ 3 ] − p r i c e s [ i ] dp[i - 1][3] - prices[i] dp[i1][3]prices[i]
    • 前一天是保持卖出股票状态(状态二), d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i1][1]prices[i]

所以操作二取最大值,即: m a x ( d p [ i − 1 ] [ 3 ] , d p [ i − 1 ] [ 1 ] ) − p r i c e s [ i ] max(dp[i - 1][3], dp[i - 1][1]) - prices[i] max(dp[i1][3],dp[i1][1])prices[i]

那么 d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , m a x ( d p [ i − 1 ] [ 3 ] , d p [ i − 1 ] [ 1 ] ) − p r i c e s [ i ] ) ; dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]); dp[i][0]=max(dp[i1][0],max(dp[i1][3],dp[i1][1])prices[i]);

达到保持卖出股票状态(状态二)即: d p [ i ] [ 1 ] dp[i][1] dp[i][1],有两个具体操作:

  • 操作一:前一天就是状态二, d p [ i − 1 ] [ 1 ] dp[i-1][1] dp[i1][1]
  • 操作二:前一天是冷冻期(状态四), d p [ i − 1 ] [ 3 ] dp[i - 1][3] dp[i1][3]

d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 3 ] ) ; dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); dp[i][1]=max(dp[i1][1],dp[i1][3]);

达到今天就卖出股票状态(状态三),即:$dp[i][2] $,昨天一定是买入股票状态(状态一),今天卖出

即: d p [ i ] [ 2 ] = d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] ; dp[i][2] = dp[i - 1][0] + prices[i]; dp[i][2]=dp[i1][0]+prices[i];

达到冷冻期状态(状态四),即: d p [ i ] [ 3 ] dp[i][3] dp[i][3],昨天一定卖出了股票(状态三), d p [ i ] [ 3 ] = d p [ i − 1 ] [ 2 ] ; dp[i][3] = dp[i - 1][2]; dp[i][3]=dp[i1][2];

综上分析,递推代码如下:

dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];

3、如果是持有股票状态(状态一)那么: d p [ 0 ] [ 0 ] = − p r i c e s [ 0 ] dp[0][0] = -prices[0] dp[0][0]=prices[0],买入股票所剩现金为负数。保持卖出股票状态(状态二),第0天没有卖出 d p [ 0 ] [ 1 ] dp[0][1] dp[0][1]初始化为0就行,今天卖出了股票(状态三),同样 d p [ 0 ] [ 2 ] dp[0][2] dp[0][2]初始化为0,因为最少收益就是0,绝不会是负数。同理 d p [ 0 ] [ 3 ] dp[0][3] dp[0][3]也初始为0。

4、从递归公式上可以看出, d p [ i ] dp[i] dp[i] 依赖于 d p [ i − 1 ] dp[i-1] dp[i1],所以是从前向后遍历。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() == 0) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(4, 0));
        dp[0][0] = -prices[0];
        for(int i = 1; i < prices.size(); i++){
            dp[i][0] = max(dp[i-1][0], max(dp[i-1][3],dp[i-1][1]) - prices[i]);
            dp[i][1] = max(dp[i-1][1], dp[i-1][3]);
            dp[i][2] = dp[i-1][0] + prices[i];
            dp[i][3] = dp[i-1][2];
        }
        return max(dp[prices.size()-1][1], max(dp[prices.size()-1][2], dp[prices.size()-1][3]));
    }
};
//时间复杂度:O(n);空间复杂度:O(n)
优化
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if(prices.size() == 0) return 0;
        int f0 = -prices[0];
        int f1 = 0, f2 = 0, f3 = 0;
        for(int i = 1; i < prices.size(); i++){
            int newf0 = max(f0, max(f3,f1)-prices[i]);
            int newf1 = max(f1, f3);
            int newf2 = f0 + prices[i];
            int newf3 = f2;
            f0 = newf0;
            f1 = newf1;
            f2 = newf2;
            f3 = newf3;
        }
        return max(f1, max(f2, f3));
    }
};
//时间复杂度:O(n);空间复杂度:O(1)

714. 买卖股票的最佳时机含手续费

相对于122. 买卖股票的最佳时机 II,本题只需要在计算卖出操作的时候减去手续费就可以了。

d p [ i ] [ 0 ] dp[i][0] dp[i][0]表示第 i i i天持有股票所省最多现金。 d p [ i ] [ 1 ] dp[i][1] dp[i][1]表示第 i i i天不持有股票所得最多现金

如果第 i i i天持有股票即 d p [ i ] [ 0 ] dp[i][0] dp[i][0], 那么可以由两个状态推出来

  • i − 1 i-1 i1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即: d p [ i − 1 ] [ 0 ] dp[i - 1][0] dp[i1][0]
  • i i i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即: d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] dp[i - 1][1] - prices[i] dp[i1][1]prices[i]

所以: d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] ) ; dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); dp[i][0]=max(dp[i1][0],dp[i1][1]prices[i]);

如果第 i i i天不持有股票即 d p [ i ] [ 1 ] dp[i][1] dp[i][1]的情况, 依然可以由两个状态推出来

  • i − 1 i-1 i1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即: d p [ i − 1 ] [ 1 ] dp[i - 1][1] dp[i1][1]
  • i i i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,注意这里需要有手续费了,即: d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] − f e e dp[i - 1][0] + prices[i] - fee dp[i1][0]+prices[i]fee

所以: d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] − f e e ) ; dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee); dp[i][1]=max(dp[i1][1],dp[i1][0]+prices[i]fee);

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(2, 0));
        dp[0][0] -= prices[0]; // 持股票
        for (int i = 1; i < n; 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] - fee);
        }
        return max(dp[n - 1][0], dp[n - 1][1]);
    }
};

300. 最长递增子序列

1、dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度

2、位置 i i i 的最长升序子序列等于 j j j 0 0 0 i − 1 i-1 i1各个位置的最长升序子序列 + 1 +1 +1 的最大值。所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);注意这里不是要 d p [ i ] dp[i] dp[i] d p [ j ] + 1 dp[j]+1 dp[j]+1进行比较,而是要取 d p [ j ] + 1 dp[j]+1 dp[j]+1的最大值。

3、每一个 i i i ,对应的 d p [ i ] dp[i] dp[i](即最长上升子序列)起始大小至少都是 1 1 1

4、 d p [ i ] dp[i] dp[i] 是由 0 0 0 i − 1 i-1 i1各个位置的最长升序子序列 推导而来,那么遍历 i i i一定是从前向后遍历。 j j j其实就是 0 0 0 i − 1 i-1 i1,遍历 i i i的循环在外层,遍历 j j j则在内层。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if(nums.size() <= 1) return nums.size();
        vector<int> dp(nums.size(), 1);
        int result = 0;
        for(int i = 1;i < nums.size();i++){
            for(int j = 0;j < i;j++){
                 if (nums[i] > nums[j]){ 
                    dp[i] = max(dp[i], dp[j] + 1);
                 }
            }
            if(dp[i] > result){
                result = dp[i];
            }
        }
        return result;
    }
};

674. 最长连续递增序列

动态规划

300. 最长递增子序列最大的区别在于 本题 要求 连续。

不连续递增子序列的跟前 0 − i 0-i 0i 个状态有关,连续递增的子序列只跟前一个状态有关

1、dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]。注意这里的定义,一定是以下标 i i i为结尾,并不是说一定以下标 0 0 0为起始位置。

2、如果 n u m s [ i + 1 ] > n u m s [ i ] nums[i + 1] > nums[i] nums[i+1]>nums[i],那么以 i + 1 i+1 i+1为结尾的数组的连续递增的子序列长度 一定等于 以 i i i 为结尾的数组的连续递增的子序列长度 + 1 +1 +1 。即:dp[i + 1] = dp[i] + 1;

3、dp[i] = 1;

4、 d p [ i + 1 ] dp[i + 1] dp[i+1]依赖 d p [ i ] dp[i] dp[i],所以一定是从前向后遍历

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        int result = 1;
        vector<int> dp(nums.size(), 1);
        for(int i = 0;i < nums.size() - 1;i++){
            if (nums[i + 1] > nums[i]){
                dp[i + 1] = dp[i] + 1;
            }
            if(dp[i + 1] > result){
                result = dp[i + 1];
            }
        }
        return result;
    }
};
//时间复杂度:O(n);空间复杂度:O(n)
贪心解法

遇到 n u m s [ i + 1 ] > n u m s [ i ] nums[i + 1] > nums[i] nums[i+1]>nums[i]的情况, c o u n t + + count++ count++,否则 c o u n t count count 1 1 1,记录 c o u n t count count的最大值就可以了。

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        int result = 1; 
        int count = 1;
        for (int i = 0; i < nums.size() - 1; i++) {
            if (nums[i + 1] > nums[i]) { // 连续记录
                count++;
            } else { 					 // 不连续,count从头开始
                count = 1;
            }
            if (count > result) result = count;
        }
        return result;
    }
};
//时间复杂度:O(n);空间复杂度:O(1)

718. 最长重复子数组

1、dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。注意,在遍历 d p [ i ] [ j ] dp[i][j] dp[i][j]的时候 i i i j j j都要从 1 1 1开始。

2、根据 d p [ i ] [ j ] dp[i][j] dp[i][j]的定义, d p [ i ] [ j ] dp[i][j] dp[i][j]的状态只能由 d p [ i − 1 ] [ j − 1 ] dp[i - 1][j - 1] dp[i1][j1]推导出来。即当 A [ i − 1 ] A[i - 1] A[i1] B [ j − 1 ] B[j - 1] B[j1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;

3、根据 d p [ i ] [ j ] dp[i][j] dp[i][j]的定义, d p [ i ] [ 0 ] dp[i][0] dp[i][0] d p [ 0 ] [ j ] dp[0][j] dp[0][j]其实都是没有意义的,但 d p [ i ] [ 0 ] dp[i][0] dp[i][0] d p [ 0 ] [ j ] dp[0][j] dp[0][j]要初始值,因为为了方便递归公式 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 ; dp[i][j] = dp[i - 1][j - 1] + 1; dp[i][j]=dp[i1][j1]+1;所以 d p [ i ] [ 0 ] dp[i][0] dp[i][0] d p [ 0 ] [ j ] dp[0][j] dp[0][j]初始化为 0 0 0

举个例子: A [ 0 ] = B [ 0 ] A[0] = B[0] A[0]=B[0],则 d p [ 1 ] [ 1 ] = d p [ 0 ] [ 0 ] + 1 dp[1][1] = dp[0][0] + 1 dp[1][1]=dp[0][0]+1,只有 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]初始为 0 0 0,符合递推公式逐步累加。

4、外层 f o r for for循环遍历 A A A,内层 f o r for for循环遍历 B B B。内外层调转也可以。同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把 d p [ i ] [ j ] dp[i][j] dp[i][j]的最大值记录下来。

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp(nums1.size() + 1, vector<int> (nums2.size() + 1 ,0));
        int result = 0;
        for(int i = 1; i <= nums1.size(); i++){
            for(int j = 1; j <= nums2.size(); j++){
                if(nums1[i - 1] == nums2[j - 1]){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                if(dp[i][j] > result){
                    result = dp[i][j];
                }
            }
        }
        return result;
    }
};
	//时间复杂度:O(n × m),n 为A长度,m为B长度;空间复杂度:O(n × m)
优化:滚动数组

d p [ i ] [ j ] dp[i][j] dp[i][j]都是由 d p [ i − 1 ] [ j − 1 ] dp[i - 1][j - 1] dp[i1][j1]推出。那么压缩为一维数组,就是 d p [ j ] dp[j] dp[j]都是由 d p [ j − 1 ] dp[j - 1] dp[j1]推出,相当于可以把上一层 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i1][j]拷贝到下一层 d p [ i ] [ j ] dp[i][j] dp[i][j]来继续用。此时遍历B数组的时候要从后向前遍历,避免重复覆盖。

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
        vector<int> dp(nums2.size() + 1 ,0);
        int result = 0;
        for(int i = 1; i <= nums1.size(); i++){
            for(int j = nums2.size(); j >= 1; j--){
                if(nums1[i - 1] == nums2[j - 1]){
                    dp[j] = dp[j - 1] + 1;
                }else{
                    dp[j] = 0;  
                    //如果不相等需要赋值为0 ,否则上层的数据未被覆盖会对后面的推导造成影响
                }
                if(dp[j] > result){
                    result = dp[j];
                }
            }
        }
        return result;
    }
};

1143. 最长公共子序列

1、dp[i][j]:长度为[0, i - 1]的字符串1与长度为[0, j - 1]的字符串2的最长公共子序列为dp[i][j]

2、如果text1[i - 1] == text2[j - 1],则找到了一个公共元素:dp[i][j] = dp[i - 1][j - 1] + 1;如果text1[i - 1] ≠ text2[j - 1],则看text1[0, i - 2]与text2[0, j - 1] text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

3、 t e s t 1 [ 0 , i − 1 ] test1[0, i-1] test1[0,i1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;同理dp[0][j]=0。其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。

4、从递推公式,可以看出,有三个方向可以推出 d p [ i ] [ j ] dp[i][j] dp[i][j],为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。

image-20220712185921693
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
        for(int i = 1; i <= text1.size(); i++){
            for(int j = 1;j <= text2.size(); j++){
                if(text1[i - 1] == text2[j - 1]){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else{
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[text1.size()][text2.size()];
    }
};

1035. 不相交的线

绘制一些连接两个数字 A [ i ] A[i] A[i] B [ j ] B[j] B[j] 的直线,只要 A [ i ] = = B [ j ] A[i] == B[j] A[i]==B[j],且直线不能相交!

直线不能相交,这就是说明在字符串 A A A中找到一个与字符串 B B B​相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!那么就和1143. 最长公共子序列是一样的了。代码0改动…

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
        for(int i = 1; i <= nums1.size(); i++){
            for(int j = 1;j <= nums2.size(); j++){
                if(nums1[i - 1] == nums2[j - 1]){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else{
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[nums1.size()][nums2.size()];
    }
};

53. 最大子数组和

1、dp[i]:包括下标i之前的最大连续子序列和为dp[i]

2、 d p [ i ] dp[i] dp[i]只有两个方向可以推出来:

  • dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和
  • nums[i],即:从头开始计算当前连续子序列和

3、dp[0] = nums[0]

4、 d p [ i ] dp[i] dp[i]依赖于 d p [ i − 1 ] dp[i - 1] dp[i1]的状态,需要从前向后遍历

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if(nums.size() == 0) return 0;
        vector<int> dp(nums.size(), 0);  //dp[i]表示包括i之前的最大连续子序列和
        dp[0] = nums[0];
        int result = dp[0];
        for(int i = 1; i < nums.size(); i++){
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);  //状态转移公式
            if(dp[i] > result)
                result = dp[i];
        }
        return result;
    }
};
;