《代码随想录 11 章(2)》二刷题解及心得体会

第十一章

11.20 买卖股票的最佳时机

力扣题号:121.买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:
1 <= prices.length <= 105
0 <= prices[i] <= 104

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock

11.20.1 暴力解法

  最容易想到的肯定是暴力解法,因为本题只让买卖一次,所以我们需要找到两个元素i,j,其prices[j]-prices[i]最大。
  整体代码如下:
题解

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

  不出意外暴力解法都会超时。

11.20.2 贪心算法

  因为股票只买卖一次,为了让差值最大,我们需要找到左边最小值和右边最大值。左边最小值通过low=min(low, prices[i])来保持,右边最大值通过result=max(result, prices[i]-low)间接来保持。
  整体代码如下:
题解

class Solution {
public:
    int maxProfit(vector<int>& prices) {
		int result = 0;
		int low = INT_MAX;
		for (int i = 0; i < prices.size(); i++) {
			low = min(low, prices[i]);
			result = max(result, prices[i] - low);
		}
		return result;
    }
};

11.20.3 动态规划

  1.确定dp数组及下标的含义
  dp[i][0] 表示第i天持有股票所得最多现⾦ ,这⾥可能有同学疑惑,本题中只能买卖⼀次,持有股票之后哪还有现⾦呢?其实⼀开始现⾦是0,那么加⼊第i天买⼊股票现⾦就是 -prices[i], 这是⼀个负数。dp[i][1] 表示第i天不持有股票所得最多现⾦。 注意这⾥说的是“持有”,“持有”不代表就是当天“买⼊”!也有可能是昨天就买⼊了,今天保持持有的状态很多同学把“持有”和“买⼊”没分区分清楚。

  2.确定递推公式
  如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

  • 第i-1天就持有股票,此时就应该保持原状,所得现金就是昨天持有股票的所得现金。即dp[i-1][0];
  • 第i天才买入股票,此时所得现金就是今天买入后所得现金(负数)即-prices[i];
      很明显dp[i][0]应该选取二者最大值即dp[i][0] = max(dp[i - 1][0], -prices[i]);
      如果第i天不持有股票即dp[i][1],那么可以由两个状态推出
  • 第i-1天就不持有了,应该保持现状,即dp[i-1][1];
  • 第i天才不持有(卖出),则应该是第i-1天持有股票时的现金加上第i天股票的价格,即dp[i-1][0]+pirces[i];
      很明显dp[i][1]也应该选取最大的即dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);

  3.dp数组应该如何初始化
  从递推公式可以看出dp[i]都是由i之前的dp[j]推出来的,所以dp[0][0]代表第0天就持有股票所得最多现金(第0天就买入)即dp[0][0] = -prices[0];。而dp[0][1]代表第0天不持有股票所得最多现金,当然就是不买不卖的状态,即dp[0][1] = 0;

  4.确定遍历顺序
  从递推公式很容易看出应该从前往后遍历。

  5.举例推导dp数组
在这里插入图片描述
  最后的结果就是dp[5][1],为什么不是dp[5][0]呢?因为不持有股票所持现金肯定比持有股票多,所以不要买股票(不是)。
  整体代码如下:
题解

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

  从递推公式我们可以看出,dp[i]只依赖于dp[i-1],所以我们可以使用滚动数组来节省空间。
  整体代码如下:
题解

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

11.21 买卖股票的最佳时机Ⅱ

力扣题号: 122.买卖股票的最佳时机Ⅱ
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。

示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。

示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。

示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii

思路
  我们在讲解贪心算法的时候已经做过这道题了,贪心算法是把每天的正利润都相加,最后全局最优就是最大利润。
  现在我们要把这道题用动态规划的思想再做一遍。这和上道题区别主要体现在递推公式上,其他都一样。

  2.确定递推公式
  这⾥重申⼀下dp数组的含义:dp[i][0] 表示第i天持有股票所得最大现⾦。dp[i][1] 表示第i天不持有股票所得最多现⾦。
如果第i天持有股票最大现金为dp[i][0],那么可以由两个状态推导出来了:

  • 如果第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金:dp[i-1][0]
  • 如果第i天才持有股票,那么所得现金就是昨天不持有股票所得现金减去今天的股票价格:dp[i-1][1]-prices[i]。(而上一题只能买卖一次,所以是-prices[i],dp[i-1][1]代表的是之前经过买卖后的现金)
    这二者要去最大值即:dp[i][0] = max(dp[i - 1][1], dp[i - 1][1] - prices[i]);

如果第i天不持有股票最大现金为dp[i][1],那么可以由两个状态推导出来了:

  • 如果第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金:dp[i-1][1]
  • 如果第i天才不持有股票(卖出),那么所得现金就是昨天持有股票+今天股票的价格:dp[i-1][0]+prices[i]

  整体代码如下:
题解

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

  滚动数组版本
题解

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

11.22 买卖股票的最佳时机Ⅲ

力扣题号:123.买卖股票的最佳时机Ⅲ
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
示例 4:

输入:prices = [1]
输出:0

提示:

1 <= prices.length <= 105
0 <= prices[i] <= 105

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii
思路
  这道题比之前难了不少,难在最多只允许买卖两次,这怎么统计啊?最多只允许买卖两次也就是允许买卖0次,允许买卖一次,允许买卖两次。将这种几种状态都使用dp数组进行管理,思路就有了。

  1.确定dp数组及下标的含义
  一天有5种状态:0.没有操作;1.第一次买入;2.第一次卖出;3.第二次买入;4.第二次卖出;
  dp[i][j]:表示第i天第j种状态所有的最大现金。

  2.确定递推公式
  这里需要注意dp[i][1],表示的是第i天,买入股票的状态(第一次持有股票,不一定是第i天才买入的,也可能是之前就买了)要想达到dp[i][1]状态,需要从第i-1天推导:

  • 第i天买入了股票,那么所有现金就是第i-1天没有操作这个状态所有现金-当天的股票价格即dp[i][1] = dp[i-1][0] - prices[i];
  • 第i天没有操作,之前就买入了,那么所有现金就沿用之前的第一次买入的状态即dp[i][1]=dp[i-1][1];
  • 这两个一定是选最大的,所以dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);

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

  • 第i天卖出股票了,那么所有现金就是第i-1天第一次买入状态+当天的股票价格即dp[i][2]=dp[i-1][1] + prices;
  • 第i天没有股票,在之前就卖出了,沿用前一天第一次卖出的状态即dp[i][2]=dp[i-1][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,即:dp[0][0] = 0;第0天做第⼀次买⼊的操作,dp[0][1] = -prices[0];第0天做第⼀次卖出的操作,这个初始值应该是多少呢?⾸先卖出的操作⼀定是收获利润,整个股票买卖最差情况也就是没有盈利即全程⽆操作现⾦为0,从递推公式中可以看出每次是取最⼤值,那么既然是收获利润如果⽐0还⼩了就没有必要收获这个利润了。所以dp[0][2] = 0;第0天第⼆次买⼊操作,初始值应该是多少呢?不⽤管第⼏次,现在⼿头上没有现⾦,只要买⼊,现⾦就做相应的减少。所以第⼆次买⼊操作,初始化为:dp[0][3] = -prices[0];同理第⼆次卖出初始化dp[0][4] = 0;

  4.确定遍历顺序
  一定是从前往后。

  5.举例推导dp数组
在这里插入图片描述
  ⼤家可以看到红⾊框为最后两次卖出的状态。现在最⼤的时候⼀定是卖出的状态,⽽两次卖出的状态现⾦最⼤⼀定是最后⼀次卖出。所以最终最⼤利润是dp[4][4]。
  我们可不可以使用滚动数组呢?可以是可以但有些细节需要注意!!!
  dp[1] = max(dp[1], dp[0] - prices[i]); 如果max中dp[1]取dp[1],即保持买⼊股票的状态,那么 dp[2] =max(dp[2], dp[1] + prices[i]);中dp[1] + prices[i] 就是今天卖出。这没问题
  如果dp[1]取dp[0] - prices[i],今天买⼊股票,那么dp[2] = max(dp[2], dp[1] + prices[i]);中的dp[1] +prices[i]相当于是今天再卖出股票,⼀买⼀卖收益为0,今天买今天卖对所得现⾦没有影响。相当于今天买⼊股票⼜卖出股票,等于没有操作,保持昨天卖出股票的状态了。
  我们就不写保存整个dp数组的代码了,直接用滚动数组吧!整体代码如下:
题解

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 = 0; 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];
    }
};

11.23 买卖股票的最佳时机Ⅳ

力扣题号: 188.买卖股票的最佳时机Ⅳ
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。

提示:
0 <= k <= 100
0 <= prices.length <= 1000
0 <= prices[i] <= 1000

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv

  1.确定dp数组及下标的含义
  dp[i][j]:第i天的状态为j时,所有最大现金。而状态有k种,j的状态表示为:

  • 0 表示不操作
  • 1 第⼀次买⼊
  • 2 第⼀次卖出
  • 3 第⼆次买⼊
  • 4 第⼆次卖出

  •   应该发现规律了,除了0以外,偶数就是卖出,奇数就是买入。所以j的范围为0~2*k.

  2.确定递推公式
  递推公式可以模仿上一节的题,唯一不同的就是此题可以最多买卖k次。代码如下:

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]);//j为奇数买入
	dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);//j为偶数卖出
}

  3.dp数组如何初始化
  模仿上一节的题,买入的时候应该dp[0][j]应该初始化为-prices;卖出的时候dp[0][j]应该初始化为0。代码如下:

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

  4.确定遍历顺序
  依旧从前往后

  5.举例推导dp数组
在这里插入图片描述
  最后⼀次卖出,⼀定是利润最⼤的,dp[prices.size() - 1][2 * k]即红⾊部分就是最后求解。
  我们依旧直接使用滚动数组来写代码,整体代码如下:
题解

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

  小结:当然有的解法是定义⼀个三维数组dp[i][j][k],第i天,第j次买卖,k表示买还是卖的状态,从定义上来讲
是⽐较直观。但感觉三维数组操作起来有些麻烦,我是直接⽤⼆维数组来模拟三位数组的情况,代码看起来也清爽⼀些。

11.24 买卖股票的最佳时机(含冷冻期)

力扣题号:309.最佳买卖股票时机含冷冻期
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。​
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

示例 2:
输入: prices = [1]
输出: 0

提示:
1 <= prices.length <= 5000
0 <= prices[i] <= 1000

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown

思路
  相对于动态规划:122.买卖股票的最佳时机II,本题加上了⼀个冷冻期。在动态规划:122.买卖股票的最佳时机II 中有两个状态,持有股票后的最多现⾦,和不持有股票的最多现⾦。

  1.确定dp数组及下标的含义
  dp[i][j]:表示第i天状态j时,所有最大现金数。我们具体分析这道题的各种情况,可以分为四种状态:

  • 状态0:买入股票状态(今天买或者之前就已经买入)
  • 状态1:两天前就卖出了股票(度过了冷冻期,今天没有操作,保持卖出股票的状态)
  • 状态2:今天卖出的股票
  • 状态3:今天为冷冻期,但明天就不是
      很多同学觉得这道题比较混乱,其实是把这四种状态合并成了一种(状态1和状态3合并到了一起)。代码上讲确实可以合并,但是从逻辑上讲就比较混乱。

  2.确定递推公式
  要想到达dp[i][0]有两种方向

  • 前一天就是持有股票状态,所以今天不用操作即dp[i][0]=dp[i-1][0];
  • 今天才买入股票,有两种情况:1.前一天是冷冻期,冷冻期过后立马买,即dp[i][0]=dp[i-1][3]-prices[i]; 2.前一天是保持卖出股票状态,今天买入,即dp[i][0]=dp[i-1][1]-prices[i];这个方向的两种情况需要取二者最大值即max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);
  • 这两种方向也要取最大值,综上所述:dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);

  要想到达dp[i][1]有两种方向

  • 前一天是冷冻期即dp[i][1]=dp[i-1][3];
  • 前一天就是状态1(两天前卖出股票,已过冷冻期)即dp[i][1]=dp[i-1][1];
  • 这两种方向要取最大值,dp[i][1] = max(dp[i - 1][3], dp[i - 1][1]);

  要想到达dp[i][2]只有一种方向

  • 只能是昨天已经是买入股票的状态1,今天卖出即dp[i][2] = dp[i - 1][0] + prices[i];

  要想到达dp[i][3]也只有一种方向

  • 昨天卖出状态2即dp[i][3] = dp[i - 1][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.dp数组如何初始化
  因为dp[i]都是从dp[i-k]递推出来的,我们应该初始化dp[0][j]。跟之前一样dp[0][0]应该初始化为买入第1天股票所有的现金即-prices[0];dp[0][1]为第1天没有卖出股票的状态,和之前一样初始化为0即可;dp[0][2]为今天卖出股票的状态,同样应该是0,因为最少收益就是0,绝不会是负数;dp[0][3]为冷冻期的状态,同样应该初始化为0。

  4.确定遍历顺序
  从前往后。

  5.举例推导dp数组
在这里插入图片描述
  最后结果是状态1,2,3中的最大值,别忘了状态3,因为最后一天是冷冻期也可能是最大值。
  整体代码如下:
题解

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][3], max(dp[prices.size() - 1][2], dp[prices.size() - 1][1]));
    }
};

  我们也可以使用滚动数组,但是需要注意,这道题不能直接用滚动数组,因为dp[i][0]的改变会影响到dp[i][2],dp[i][2]的改变会影响到dp[i][3]…我们可以使用二维dp数组,保存前一天的状态,也可以使用局部变量来保存这些值。

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

  这次把冷冻期这道题⽬,讲的很透彻了,细分为四个状态,其状态转移也⼗分清晰,建议⼤家都按照四个状态来分析,如果只划分三个状态确实很容易给⾃⼰绕进去。

11.25 买卖股票的最佳时机(含手续费)

力扣题号: 714.买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费

示例 1:
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8

示例 2:
输入:prices = [1,3,7,5,10,3], fee = 3
输出:6

提示:
1 <= prices.length <= 5 * 104
1 <= prices[i] < 5 * 104
0 <= fee < 5 * 104

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee

思路
  之前我们讲过买卖股票的最佳时机Ⅱ,本题和它的区别在于卖出股票时需要减去手续费,代码几乎是一样的。唯一的区别在于递推公式部分。
  1.确定dp数组及下标的含义
  dp[i][0]:表示第i天持有股票时所有最大现金;dp[i][1]:表示第i天不持有股票时的最大现金。

  2.确定递推公式
  状态0,第i天持有股票可以从两个方向推导出来:

  • 1.第i-1天就持有股票,则不用操作,保持第i-1天的持有股票的最大现金即可,即dp[i][0]=dp[i-1][0];
  • 2.第i天才买入股票,所得现金就是昨天不持有股票的最大现金减去今天的股票价格,即dp[i][0]=dp[i-1][1]-prices[i];
  • 二者要去最大值即:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);

  状态1,第i天不持有股票可以从两个方向推导出来:

  • 1.第i-1天就不持有股票,则保持昨天的状态,即dp[i][1]=dp[i-1][1];
  • 2.第i天才卖出股票,则应该加上今天的股票价格再减去手续费即:dp[i][1]=dp[i-1][0] + prices[i]-fee;//差别就在这
  • 二者也要取最大值即:dp[i][1] = max(dp[i - 1][1], dp[i - 1][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]);
 	}
};

  本题也可以使用滚动数组,代码如下:
题解

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
		int n = prices.size();
		if (n == 0)	return 0;
		vector<int> dp(2, 0);
		dp[0] = -prices[0];
		for (int i = 1; i < n; i++) {
			dp[0] = max(dp[0], dp[1] - prices[i]);
			dp[1] = max(dp[1], dp[0] + prices[i] - fee); 
		}
		return dp[1];//卖出股票一定比持有股票现金多
    }	
};

  总结:
在这里插入图片描述

11.26 最长递增子序列

力扣题号:300.最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104

进阶:
你能将算法的时间复杂度降低到 O(n log(n)) 吗?

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-increasing-subsequence

11.26.1 动态规划

思路
  最⻓上升⼦序列是动规的经典题⽬,这⾥dp[i]是可以根据dp[j] (j < i)推导出来的,那么依然⽤动规五部曲来分析详细⼀波:
  1.确定dp数组及下标的含义
  dp[i]:表示下标[0,i]序列的最长递增子序列长度。

  2.确定递推公式
  dp[i]可以有dp[j] (j<i) 推导而来,具体来讲,如果nums[i]>nums[j],dp[i]等于各个dp[j]+1([0,j]序列中最长递增子序列长度+1)的最大值、 注意这⾥不是要dp[i] 与 dp[j] + 1中的最大值,⽽是要取所有dp[j] + 1的最⼤值。代码如下:

if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);

  3.dp数组初始化
  每个i,对应的dp[i]大小至少是1,因为nums[i]自身就是一个递增子序列。

  4.确定遍历顺序
  一定是外循环是遍历i,内循环是遍历j。遍历i一定是从前往后,遍历j从前往后从后往前都可。

  5.举例推导dp数组
在这里插入图片描述
  整体代码如下:
题解

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
		if (nums.size() <= 1)	return nums.size();
		vector<int> dp(nums.size(), 1);
		int result = 1;//存储最大递增子序列长度
		for (int i = 1; i < nums.size(); i++) {
			for (int j = i - 1; j >= 0; j--) {
				if (nums[i] > nums[j])dp[i] = max(dp[i], dp[j] + 1);
			}
			if (dp[i] > result)	result = dp[i];
		} 
		return result;
    }
};

  本题最关键的是要想到dp[i]由哪些状态可以推出来,并取最⼤值,那么很⾃然就能想到递推公式:dp[i]= max(dp[i], dp[j] + 1);⼦序列问题是动态规划的⼀个重要系列,本题算是⼊⻔题⽬,好戏刚刚开始!

11.26.2 贪心算法+二分查找

思路
  考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
  基于上面的贪心思路,我们维护一个数组 d[i]。d[i] :表示长度为 i的最长上升子序列的末尾元素的最小值,用len 记录目前最长上升子序列的长度,起始时 len 为 11,d[1]=nums[0]。
  同时我们可以注意到 d[i] 是关于 i 单调递增的。因为如果d[j]≥d[i] 且 j < i,我们考虑从长度为 i 的最长上升子序列的末尾删除 i−j 个元素,那么这个序列长度变为 j ,且第 j 个元素 x(末尾元素)必然小于 d[i],也就小于 d[j]。那么我们就找到了一个长度为 j的最长上升子序列,并且末尾元素比 d[j] 小,从而产生了矛盾。因此数组 d 的单调性得证
  我们依次遍历数组 nums 中的每个元素,并更新数组 d 和 len 的值。如果nums[i]>d[len] 则更新len=len+1,d[len]=nums[i];否则在 d[1…len]中找满足 d[i−1]<nums[j]<d[i] 的下标 i,并更新d[i]=nums[j]。
  最后整个算法流程为:
  设当前已求出的最长上升子序列的长度为len(初始时为 1),从前往后遍历数组ums,在遍历到 nums[i] 时:

  • 如果 nums[i]>d[len] ,则直接加入到 d 数组末尾,并更新len=len+1;
  • 如果nums[i] <= d[len],在 d 数组中二分查找,找到第一个比 nums[i] 小的数d[k] ,并更新d[k+1]=nums[i];

以输入序列 [0, 8, 4, 12, 2][0,8,4,12,2] 为例:
第一步插入 0,d=[0];
第二步插入 8,d=[0,8];
第三步插入 4,d=[0,4];
第四步插入 12,d=[0,4,12];
第五步插入 2,d=[0,2,12]。
最终得到最大递增子序列长度为 3。可以看到贪心算法只求结果,不对过程做保证,最大递增子序列长度确实是3,但不是[0,2,12],而是[0,4,12]

  整体代码如下:
题解

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int len = 1, n = (int)nums.size();
        if (n == 0) {
            return 0;
        }
        vector<int> d(n + 1, 0);
        d[len] = nums[0];
        for (int i = 1; i < n; ++i) {
            if (nums[i] > d[len]) {
                d[++len] = nums[i];
            } else {
                int l = 1, r = len, pos = 0; // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    if (d[mid] < nums[i]) {
                        pos = mid;
                        l = mid + 1;
                    } else {
                        r = mid - 1;
                    }
                }
                d[pos + 1] = nums[i];
            }
        }
        return len;
    }
};

11.27 最长连续递增序列

力扣题号: 674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。

示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。

示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。

提示:
1 <= nums.length <= 104
-109 <= nums[i] <= 109

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-continuous-increasing-subsequence

11.27.1 动态规划

思路
  这道题和上一题不同的是要求连续子序列,连续子序列我们就不用比较i之前所有的nums[j]了,只用和前一个比较。

  1.确定dp数组及下标的含义
  dp[i]:表示以下标i为结尾的数组的连续递增子序列长度(要求以下标i为结尾,未要求以0为起始位置)

  2.确定递推公式
  当nums[i+1]>nums[i]时,那么dp[i+1]就应该等于dp[i]+1。否则dp[i+1]保持不变(初始化为1)注意这⾥就体现出和动态规划:300.最⻓递增⼦序列的区别!因为本题要求连续递增⼦序列,所以就必要⽐较nums[i + 1]与nums[i],⽽不⽤去⽐较nums[j]与nums[i] (j是在0到i之间遍历)。既然不⽤j了,那么也不⽤两层for循环,本题⼀层for循环就⾏,⽐较nums[i + 1] 和 nums[i]。

  3.dp数组如何初始化
  以下标i为结尾的数组的连续递增的⼦序列⻓度最少也应该是1,即就是nums[i]这⼀个元素。所以dp[i]应该初始1;

  4.确定遍历顺序
  从递推公式可以看出,一定是从前往后遍历

  5.举例推导dp数组
在这里插入图片描述
  我们要输出的是整个dp数组中最大值,代码中应用result时刻保持dp数组的最大值。整体代码如下:
题解

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

11.27.2 贪心算法

  本题也可以使用贪心算法来做,如果nums[i] > nums[i-1],count++,否则count就重置为1.代码如下:
题解

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
		if (nums.size() == 0)	return 0;
        int result = 1;
        int count = 1;
		for (int i = 1; i < nums.size(); i++) {
			if (nums[i] > nums[i - 1])	count++;
			else count = 1;
			if (result < count)		result = count;
		}
		return result;
    }
};

  小结:本题也是动规⾥⼦序列问题的经典题⽬,但也可以⽤贪⼼来做,⼤家也会发现贪⼼好像更简单⼀点,⽽且空间复杂度仅是O(1)。在动规分析中,关键是要理解和动态规划:300.最⻓递增⼦序列的区别。要联动起来,才能理解递增⼦序列怎么求,递增连续⼦序列⼜要怎么求。概括来说:不连续递增⼦序列的跟前0-i 个状态有关,连续递增的⼦序列只跟前⼀个状态有关。 本篇我也把区别所在之处重点介绍了,关键在递推公式和遍历⽅法上,⼤家可以仔细体会⼀波!

11.28 最长重复子数组

力扣题号:718.最长重复子数组
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。

示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。

示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5

提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 100

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/maximum-length-of-repeated-subarray
思路
  注意题⽬中说的⼦数组,其实就是 连续⼦序列。这种问题动规最拿⼿,动规五部曲分析如下:

  1.确定dp数组及下标的含义
  dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最⻓重复⼦数组⻓度为dp[i][j]。此时细⼼的同学应该发现,那dp[0][0]是什么含义呢?总不能是以下标-1为结尾的A数组吧。其实dp[i][j]的定义也就决定着,我们在遍历dp[i][j]的时候i 和 j都要从1开始。那有同学问了,我就定义dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,最⻓重复⼦数组⻓度。不⾏么?⾏倒是⾏! 但实现起来就麻烦⼀点,⼤家看下⾯的dp数组状态图就明⽩了。

  2.确定递推公式
  根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;根据递推公式可以看出,遍历i 和 j 要从1开始!

  3.dp数组如何初始化
  根据dp[i][j]的定义,dp[i][0] 和dp[0][j]其实都是没有意义的!但dp[i][0] 和dp[0][j]要有初始值,因为 为了⽅便递归公式dp[i][j] = dp[i - 1][j - 1] + 1;所以dp[i][0] 和dp[0][j]初始化为0。举个例⼦A[0]如果和B[0]相同的话,dp[1][1] = dp[0][0] + 1,只有dp[0][0]初始为0,正好符合递推公式逐步累加起来。

  4.确定遍历顺序
  内层遍历A和内层遍历B都可以。

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 (result < dp[i][j])	result = dp[i][j];
	}
}
for (int j = 1; j <= nums2.size(); j++) {
	for (int i = 1; i <= nums1.size(); i++) {
		if (nums1[i - 1] == nums2[j - 1])	dp[i][j] = dp[i - 1][j - 1] + 1;
		if (result < dp[i][j])	result = dp[i][j];
	}
}

  5.举例推导dp数组
在这里插入图片描述
  整体代码如下:
题解

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
		if (nums1.size() == 0|| nums2.size() == 0)	return 0;
		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 (result < dp[i][j])	result = dp[i][j];
			}
		}
		return result;
    }
};

  本题也可以用滚动数组,因为我们可以看出dp[i][j]都是由dp[i - 1][j - 1]推出。那么压缩为⼀维数组,也就是dp[j]都是由dp[j - 1]推出。也就是相当于可以把上⼀层dp[i - 1][j]拷⻉到下⼀层dp[i][j]来继续⽤。此时遍历B数组的时候,就要从后向前遍历,这样避免重复覆盖。整体代码如下:
题解

class Solution {
public:
    int findLength(vector<int>& nums1, vector<int>& nums2) {
		if (nums1.size() == 0 || nums2.size() == 0)	return 0;
		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;
				//因为是dp[j-1],如果从前往后遍历,可能会把本层刚修改的dp[j]当成是上一层的dp[j]使用 
				else dp[j] = 0;//不相等时要重置为0,防止下层继续使用上上层的数据 
                if (result < dp[j]) result = dp[j];
			}
		}
		return result; 
    }
};

11.29 最长公共子序列

力扣题号: 1143.最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。

示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。

示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-common-subsequence

思路
  本题和上一题不同的是不要求连续了,但是要有相对顺序,也就是我们不仅得比较前一个元素,而是要比较前面的所有元素。

  1.确定dp数组及下标的含义
  dp[i][j]:表示长度为⻓度为[0, i - 1]的字符串text1与⻓度为[0, j - 1]的字符串text2的最⻓公共⼦序列长度。有同学会问:为什么要定义⻓度为[0, i - 1]的字符串text1,定义为⻓度为[0, i]的字符串text1不⾹么?这样定义是为了后⾯代码实现⽅便,如果⾮要定义为为⻓度为[0, i]的字符串text1也可以,⼤家可以试⼀试!

  2.确定递推公式
  如果text1[i - 1] 与 text2[j - 1]相同,那么找到了⼀个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1; 如果不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最⻓公共⼦序列 和text1[0, i - 1]与text2[0, j - 2]的最⻓公共⼦序列,取最⼤的。代码如下:

if (text1[i - 1] == text2[j - 1])	dp[i][j] = dp[i - 1][j - 1];
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);

  3.dp数组如何初始化
  dp[i][0]表示text1[0,i-1]与空串的最长公共子序列长度,肯定是0,dp[0][j]表示空串与text2[0, j -1]的最长公共子串,肯定也为0。其他下标都是根据这些初值递推出来的(覆盖掉),所以初始化多少都可,为方便统一初始化为0。

  4.确定遍历顺序
在这里插入图片描述

  从递推公式我们可以看出,有三个方向可以推出dp[i][j],左、上、和左上角,为了保证递推过程中都是使用已经计算过的数值,我们要从上到下,从左到右遍历。

  5.举例推导dp数组
在这里插入图片描述
  最后红框dp[text1.size()][text2.size()]为最终结果。
  整体代码如下:
题解

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() + 1; i++) {
			for (int j = 1; j < text2.size() + 1; 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()];
    }
};

  让我们试试可不可以使用滚动数组,dp[i][j]由左上方,上方和左方来推导:
在这里插入图片描述
  对于左方和上方的数据都可以直接用滚动数组,但是左上方的不行,因为左上方的是上一轮的外循环的dp[j-1](dp[i-1][j-1]),要是我们直接用temp=dp[j-1],则表示的是左方的dp[i][j-1]。怎么才能记录上一轮的dp[j-1]呢?用两个临时变量temp1和temp2,在第一层循环内进行初始化,都为dp[0](每一层第i层都重置为上一轮已经处理好的dp[0]即dp[i-1][0]。temp2记录上一轮内循环未处理的dp[j](处理之后就是dp[i][j-1]了)用于下轮(j+1)(即dp[i-1][j-1])。temp1赋值为temp2,获得上一轮的dp[j-1]。记住这种方法,编辑距离题还会用到。代码如下:
题解

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
    	vector<int> dp(text2.size() + 1, 0);
		for (int i = 1; i < text1.size() + 1; i++) {
            int temp1 = dp[0], temp2 = dp[0]; //用于记录上一轮的dp[i-1][j-1]
			for (int j = 1; j < text2.size() + 1; j++) {
				temp1 = temp2;//temp1代表上一轮的dp[j-1],即dp[i-1][j-1] 
				temp2 = dp[j];//记录上一轮循环的dp[j],即dp[j-1] 
				//不能用temp1=dp[j-1],因为这是本轮循环的dp[j-1]即dp[j]左边 
				if (text1[i - 1] == text2[j - 1])	dp[j] = temp1 + 1;
				else dp[j] = max(dp[j], dp[j - 1]);
			}
		}
		return dp[text2.size()];
    }
};

  本题可以用内层循环逆序来避免dp[j]存储的是已经处理的dp[i-1][j-1]吗?不可以因为dp[i][j]不仅由dp[i-1][j-1]、dp[i-1][j]推出,还由 dp[i][j-1]推出,倒序的话dp[j-1]其实是dp[i-1][j-1],而不是dp[i][j-1]

11.30 不相交的线

力扣题号: 1035.不相交的线
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:
nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 1:
在这里插入图片描述

输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。

示例 2:
输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3

示例 3:
输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2

提示:
1 <= nums1.length, nums2.length <= 500
1 <= nums1[i], nums2[j] <= 2000

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/uncrossed-lines

思路
  相信不少录友看到这道题⽬都没啥思路,我们来逐步分析⼀下。绘制⼀些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且直线不能相交!直线不能相交,这就是说明在字符串A中 找到⼀个与字符串B相同的⼦序列,且这个⼦序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。拿示例⼀A = [1,4,2], B = [1,2,4]为例,相交情况如上图。
  其实也就是说A和B的最⻓公共⼦序列是[1,4],⻓度为2。 这个公共⼦序列指的是相对顺序不变(即数字4在字符串A中数字1的后⾯,那么数字4也应该在字符串B数字1的后⾯)这么分析完之后,⼤家可以发现:本题说是求绘制的最⼤连线数,其实就是求两个字符串的最⻓公共⼦序列的⻓度!
  那么本题就和我们刚刚讲过的这道题⽬动态规划:1143.最⻓公共⼦序列就是⼀样⼀样的了。⼀样到什么程度呢? 把字符串名字改⼀下,其他代码都不⽤改,直接copy过来就⾏了。其实本题就是求最⻓公共⼦序列的⻓度,介于我们刚刚讲过动态规划:1143.最⻓公共⼦序列,所以本题我就不再做动规五部曲分析了。
  我们直接使用滚动数组,整体代码如下:
题解

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

11.31 最大子序和

力扣题号: 53.最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。

示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:
输入:nums = [1]
输出:1

示例 3:
输入:nums = [5,4,-1,7,8]
输出:23

提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104

进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/maximum-subarray

思路
  我们已经讲过本题的贪心算法:局部最优:当前连续和为负数时立刻放弃计算,从下一个元素重新计算连续和。全局最优:选取了最大的子序和。大概的思路是:遍历nums,从头开始用count累积,如果count加上nums[i]变为负数,则应该从nums[i+1]重新计算count,count恢复0。这相当于在暴力解法中不断调整最大子序列和区间的起始位置。
  我们再用动态规划的思路来解答一下本题

  1.确定dp数组及下标的含义
  dp[i]:表示nums[0,i]的最大子序和。

  2.确定递推公式
  dp[i]有两个方向可以推导出来:

  • nums[i]加入当前连续子序和即dp[i-1]+nums[i]
  • 从头开始计算连续子序和即nums[i]
  • 这两个方向一定是取最大的,所以dp[i] = max(nums[i], dp[i - 1] + nums[i]);

  3.dp数组如何初始化
  从递推公式可以看出,dp[i]依赖dp[i-1],所以dp[0]是递推的基础。dp[0]应该如何初始化呢?dp[0]表示nums[0,0]的最大连续子序和,所以应该初始化为nums[0].

  4.确定遍历顺序
  从递推公式可以看出,应该从前往后遍历。

  5.举例推导dp数组
在这里插入图片描述
  我们应该注意,此时的最终 结果可不是dp数组最后一位,因为dp[i]的定义是nums[0,i]的最大连续子序和,而以最后一个元素结尾的子序列不一定有最大连续子序和,所以我们遍历的时候应该用result保持最大值。

  整体代码如下:
题解

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
    	if (nums.size() == 0)	return 0;
		vector<int> dp(nums.size(), 0);
		dp[0] = nums[0];
		int result = dp[0];//不能初始化为0,因为如果遍历结束后,最大子序和不超过第一个元素值,则应该返回第一个元素值
		//例如[1,-1,-1,-1] 
		for (int i = 1; i < nums.size(); i++) {
			dp[i] = max(dp[i - 1] + nums[i], nums[i]);
			if (result < dp[i])		result = dp[i];
		}
		return result;
    }
};

  小结:这道题⽬⽤贪⼼也很巧妙,但有⼀点绕,需要仔细想⼀想,如果想回顾⼀下贪⼼就看这⾥吧:贪⼼算法:最⼤⼦序和动规的解法还是很直接的。

11.32 判断子序列

力扣题号:392.判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

示例 1:
输入:s = “abc”, t = “ahbgdc”
输出:true

示例 2:
输入:s = “axc”, t = “ahbgdc”
输出:false

提示:

0 <= s.length <= 100
0 <= t.length <= 10^4
两个字符串都只由小写字符组成。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/is-subsequence

11.32.1 暴力解法

思路
  本题一开始很容易想到暴力解法用两层循环来遍历两个字符串。不出意外肯定会超时,代码如下:

class Solution {
public:
	bool isSubsequence(string s, string t) {
		//暴力的解法
	    //为空串时 也是子串
	    int cnt=0;
	    bool flag;
	    for(int i=0;i<s.size();i++){
	    	flag=false;//标记不能找到
		    for(int j=cnt;j<t.size();j++){
	        	if(s[i]==t[j]){
	            	flag=true;//标记能找到
	            	cnt=j+1;//从匹配的字符后面下一个开始 继续查找,当长度10亿时,超时
	            	break;
	        	}
	     	}
	     	if(flag==false){//没有匹配的单词,可以返回false 了
	     		return false;
	     	}
	    }
	    return true;
	}
};

11.32.2 双指针

思路
  仔细一想,其实用不到两层循环来遍历两个字符串,用双指针一样可以。本题询问的是,ss 是否是 tt 的子序列,因此只要能找到任意一种 ss 在 tt 中出现的方式,即可认为 ss 是 tt 的子序列。而当我们从前往后匹配,可以发现每次贪心地匹配靠前的字符是最优决策。 这样,我们初始化两个指针 i 和 j,分别指向 s 和 t 的初始位置。每次贪心地匹配,匹配成功则 i 和 j 同时右移,匹配 ss 的下一个位置,匹配失败则 j 右移,i 不变,尝试用 t 的下一个字符匹配 s。最终如果 i 移动到 s 的末尾,就说明 s 是 t 的子序列。代码如下:
题解

class Solution {
public:
    bool isSubsequence(string s, string t) {
		int len1 = s.size(), len2 = t.size();
		int i = 0, j = 0;
		while (i < len1 && j < len2) {
			if (s[i] == t[j]) {
				i++;
				j++;
			}
			else	j++;
		}
		return i == len1;
    }
};

11.32.3 动态规划

思路
  这道题应该算是编辑距离的⼊⻔题⽬,因为从题意中我们也可以发现,只需要计算删除的情况,不⽤考虑增加和替换的情况。所以掌握本题也是对后⾯要讲解的编辑距离的题⽬打下基础。

  1.确定dp数组及下标的含义
  dp[i][j]:表示以下标i-1为结尾的字符串s和以下标为j-1为结尾的字符串t,相同子序列的长度

  2.确定递推公式
  dp[i][j]有两个方向可以推导出来:

  • if(s[i-1] == t[j-1]),说明找到了一个相同字符,相同子序列长度应该在dp[i-1][j-1]的基础上+1,即dp[i][j]=dp[i-1][j-1]+1;
  • if(s[i-1] != t[j-1]),说明相当于t要删除t[j-1],如果删除掉这个元素,那么dp[i][j]就应该看以s[i-1]为结尾的字符串s和以t[j-1]为结尾的字符串t相同子序列的长度,即dp[i][j] = dp[i][j-1];
  • 注意这里和最长公共子序列不同,那道题取dp[i][j-1]和dp[i-1][j]中的最大值,因为我们要找两个字符串中的公共部分;而这道题我们要确定s是不是t的一部分,也就是说要确定s和t的公共部分是不是s。如果s还要删除或者回退那么就和题意不符了,所以我们不取dp[i][j-1]和dp[i-1][j]中的最大值,只取dp[i][j-1].

  3.dp数组应该如何初始化
  从递推公式可以看出,dp[i][j]都是由dp[i-1][j-1]或dp[i][j-1]推导出来的,dp[i][0]和dp[0][0]是一定要初始化的。这时候我们就发现dp数组为什么一定要表示以i-1为结尾的字符串s、以j-1为结尾的字符串t了。因为这样的定义在dp二维数组中可以留出初始化的区间。
在这里插入图片描述
  如果要是定义的dp[i][j]是以下标i为结尾的字符串s和以下标j为结尾的字符串t,初始化就⽐较麻烦了。这⾥dp[i][0]和dp[0][j]是没有含义的,仅仅是为了给递推公式做前期铺垫,所以初始化为0。

  4.确定遍历顺序
  从递推公式和上图可以看出,遍历顺序应该是从上到下,从左到右。

  5.举例推导dp数组
在这里插入图片描述
  dp[i][j]表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同⼦序列的⻓度,所以如果dp[s.size()][t.size()] 与 字符串s的⻓度相同说明:s与t的最⻓相同⼦序列就是s,那么s 就是 t 的⼦序列。图中dp[s.size()][t.size()] = 3, ⽽s.size() 也为3。所以s是t 的⼦序列,返回true。

  整体代码如下:
题解

class Solution {
public:
    bool isSubsequence(string s, string t) {
		vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
		for (int i = 1; i <= s.size(); i++) {
			for (int j = 1; j <= t.size(); j++) {
				if (s[i - 1] == t[j - 1])	dp[i][j] = dp[i - 1][j - 1] + 1;
				else dp[i][j] = dp[i][j - 1];
			}
		}
		return dp[s.size()][t.size()] == s.size();
    }
};

  本题也可以用滚动数组优化空间复杂度,temp来保持dp[i-1][j-1],上面做最大公共子序和不相交的线已经用过了,直接上代码,整体代码如下:
题解

class Solution {
public:
    bool isSubsequence(string s, string t) {
		vector<int> dp(t.size() + 1, 0);
		for (int i = 1; i <= s.size(); i++) {
			int temp1 = dp[0], temp2 = dp[0];
			for (int j = 1; j <= t.size(); j++) {
				temp1 = temp2;
				temp2 = dp[j];
				if (s[i - 1] == t[j - 1])	dp[j] = temp1 + 1;
				else dp[j] = dp[j - 1];
			}
		}
		return dp[t.size()] == s.size();
    }
};
//为什么用时还变长了呢?

  可以用内层循环逆序来避免dp[j]存储的是已经处理的dp[i-1][j-1]吗?不可以因为dp[i][j]不仅由dp[i-1][j-1]推出,还由 dp[i][j-1]推出,倒序的话dp[j-1]其实是dp[i-1][j-1],而不是dp[i][j-1]

  小结:这道题⽬算是编辑距离的⼊⻔题⽬(毕竟这⾥只是涉及到减法),也是动态规划解决的经典题型。这⼀类题都是题⽬读上去感觉很复杂,模拟⼀下也发现很复杂,⽤动规分析完了也感觉很复杂,但是最终代码却很简短。编辑距离的题⽬最能体现出动规精髓和巧妙之处,⼤家可以好好体会⼀下。

11.33 不同的子序列

力扣题号:115.不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。

示例 1:
输入:s = “rabbbit”, t = “rabbit”
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 “rabbit” 的方案。
rabbbit
rabbbit
rabbbit

示例 2:
输入:s = “babgbag”, t = “bag”
输出:5
解释:
如下图所示, 有 5 种可以从 s 中得到 “bag” 的方案。
babgbag
babgbag
babgbag
babgbag
babgbag

提示:
0 <= s.length, t.length <= 1000
s 和 t 由英文字母组成

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/distinct-subsequences

思路
  上面那道题判断子序列我们是用dp[i][j]来表示以i-1为结尾的字符串s和以j-1为结尾的字符串t相同子序列的长度。这道题不同,这道题是要求字符串t在字符串s中出现的次数。

  1.确定dp数组及下标的含义
  dp[i][j]:表示以i-1为结尾的字符串s中出现以j-1为结尾的t的个数.

  2.确定递推公式
  这种问题一般都要考虑两种情况:1.s[i-1]==t[j-1];2.s[i-1]!=t[j-1]
  1.s[i-1]==t[j-1]: 这种情况dp[i][j]有两部分组成:

  • 一部分是用s[i-1]来匹配,那么个数是dp[i-1][j-1]
  • 一部分是不用s[i-1]来匹配,那么个数就是dp[i-1][j]。这⾥可能有同学不明⽩了,为什么还要考虑 不⽤s[i - 1]来匹配,都相同了指定要匹配啊。例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不⽤s[3]来匹配,即⽤s[0]s[1]s[2]组成的bag。当然也可以⽤s[3]来匹配,即:s[0]s[1]s[3]组成的bag。
  • 所以当s[i-1]==t[j-1]时,这两种情况要相加,即dp[i][j] = dp[i - 1][j -1] + dp[i - 1][j];

  2.s[i-1]!=t[j-1]这种情况dp[i][j]只有一部分组成:

  • 不用s[i-1]来匹配,即dp[i][j] = dp[i - 1][j];

  3.dp数组如何初始化
  从递推公式可以看出,dp[i][j]由dp[i-1][j-1]和dp[i-1][j]来推导,所以dp[i][0]和dp[0][j]是必须初始化的。dp[i][0]表示以s[i-1]为结尾的字符串s中删除任意些元素,出现空字符串的次数。由这个定义可知,dp[i][0]应该初始化为0,因为删除所有字符出现空字符串的个数是1。再看dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数,dp[0][j]一定是0,空字符怎么会出现非空字符呢?最后再看dp[0][0],空字符串删除0个元素可以出现空字符串一次,dp[0][0]=1。分析完毕初始化代码如下:

vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
for (int i = 0; i <= s.size(); i++)	dp[i][0] = 1;
for (int j = 0; j <= t.size(); j++)	dp[0][j] = 0;//这行可以不写 

  4.确定遍历顺序
  由递推公式可以看出,遍历顺序一定是从上到下,从左到右。

  5.举例推导dp数组
在这里插入图片描述
  首先要声明一点*_t是typedef定义的表示标志,是一种规范化的表示。即uint8_t/uint16_t/uint32t/uint64_t都不是新的数据类型,而是通过typedef给类型起的别名这些数据类型都是在C99标准中定义的,具体如下:

/* There is some amount of overlap with <sys/types.h> as known by inet code */
#ifndef __int8_t_defined
# define __int8_t_defined
typedef signed char             int8_t;
typedef short int               int16_t;
typedef int                     int32_t;
# if __WORDSIZE == 64
typedef long int                int64_t;
# else
__extension__
typedef long long int           int64_t;
# endif
# endif

/* Unsigned.  */
typedef unsigned char           uint8_t;
typedef unsigned short int      uint16_t;
#ifndef __uint32_t_defined
typedef unsigned int            uint32_t;
# define __uint32_t_defined
#endif
#if __WORDSIZE == 64
typedef unsigned long int       uint64_t;
#else
__extension__
typedef unsigned long long int  uint64_t;
#endif

  此题的某些数据长度太长,int不能存下,我们使用无符号long型——uint64_t,整体代码如下:
题解

class Solution {
public:
    int numDistinct(string s, string t) {
		vector<vector<uint64_t>> dp(s.size() + 1, vector<uint64_t>(t.size() + 1, 0));
		for (int i = 0; i <= s.size(); i++)	dp[i][0] = 1;
		for (int j = 1; j <= t.size(); j++)	dp[0][j] = 0;//这行可以不写 
		for (int i = 1; i <= s.size(); i++) {
			for (int j = 1; j <= t.size(); j++) {
				if (s[i - 1] == t[j - 1])	dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
				else dp[i][j] = dp[i - 1][j];
			}
		} 
		return dp[s.size()][t.size()];
    }
};

  让我们试试可不可以使用滚动数组。同样用temp来保持dp[i-1][j-1],整体代码如下:
题解

class Solution {
public:
    int numDistinct(string s, string t) {
		vector<uint64_t> dp(t.size() + 1, 0);
		dp[0] = 1;
		for (int i = 1; i <= s.size(); i++) {
			int temp1 = dp[0], temp2 = dp[0];
			for (int j = 1; j <= t.size(); j++) {
                temp1 = temp2;
                temp2 = dp[j];
				if (s[i - 1] == t[j - 1])	dp[j] = temp1 + dp[j];
				else dp[j] = dp[j];
			}
		} 
		return dp[t.size()];
    }
};

  本题可以用内层循环逆序来避免dp[j]存储的是已经处理的dp[i-1][j-1]. 代码如下:
题解

class Solution {
public:
    int numDistinct(string s, string t) {
		vector<uint64_t> dp(t.size() + 1, 0);
		dp[0] = 1;
		for (int i = 1; i <= s.size(); i++) {
			for (int j = t.size(); j > 0; j--) {
				if (s[i - 1] == t[j - 1])	dp[j] = dp[j - 1] + dp[j];
				else dp[j] = dp[j];
			}
		} 
		return dp[t.size()];
    }
};

  很好,时空复杂度都降低了!

11.34 两个字符串的删除操作

力扣题号:583. 两个字符串的删除操作
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符

示例 1:
输入: word1 = “sea”, word2 = “eat”
输出: 2
解释: 第一步将 “sea” 变为 “ea” ,第二步将 "eat "变为 “ea”

示例 2:
输入:word1 = “leetcode”, word2 = “etco”
输出:4

提示:
1 <= word1.length, word2.length <= 500
word1 和 word2 只包含小写英文字母

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/delete-operation-for-two-strings

思路
  本题和动态规划:115.不同的⼦序列相⽐,其实就是两个字符串可以都可以删除了,情况虽说复杂⼀些,但整体思路是不变的。这次是两个字符串可以相互删了,这种题⽬也知道⽤动态规划的思路来解,动规五部曲,分析如下:

  1.确定dp数组及下标的含义
  上题我们定义的是以下标i-1为结尾的字符串s和以j-1为结尾的字符串t,t在s中出现的次数,本题我们也按类似的套路来定义dp数组。dp[i][j]:表示以下标i-1为结尾的字符串word1和以j-1为结尾的字符串word2,要想让这两个字符串达到相等,所需删除元素的最小次数。

  2.确定递推公式
  就像之前说的,这种题一般都要考虑两种情况:1.word1[i-1]=word2[j-1];2word1[i-1]!=word2[j-1]
  1.当word1[i-1]=word2[j-1]时,此时只有一种情况。dp[i][j]=dp[i-1][j-1]此时不用删除word1[i-1]或word2[j-1]因为这两个元素相同,不能删除,否则我们求的就不是最小步骤了,(最后相同的字符串一定是尽可能长)
  2.当word1[i-1]!=word2[j-1],此时由三种子情况:

  • 删除word1[i-1],此时的dp[i][j]就应该等于dp[i-1][j] + 1(+1是因为删除word1[i-1])
  • 删除word2[j-1],此时的dp[i][j]就应该等于dp[i][j-1]+1(+1是因为删除word2[j-1])
  • 两个元素都删除,此时的dp[i][j]就等于dp[i-1][j-1]+2(+2是因为删除了word1[i-1]、word2[j-1])
  • 当然,这三种情况要取最小值,因为我们求的是最小步骤。所以dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1})

  3.dp数组如何初始化
  由递推公式可知,dp[i][j]由dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1]得来,所以dp[0][j]、dp[i][0]都应该初始化。那么应该初始化为多少呢?dp[0][j]的含义是空字符串word1和以j-1为结尾的字符串word2,最少需要删除几次才能相等,很明显是j次。同理dp[i][0]=i.

  4.确定遍历顺序
  从递推公式可以看出,一定是从上到下从左到右。

  5.举例推导dp数组
在这里插入图片描述

  整体代码如下:
题解

class Solution {
public:
    int minDistance(string word1, string word2) {
		vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
		for (int i = 1; i <= word1.size(); i++)	dp[i][0] = i;
		for (int j = 1; j <= word2.size(); j++)	dp[0][j] = j;
		for (int i = 1; i <= word1.size(); i++) {
			for (int j = 1; j <= word2.size(); j++) {
				if (word1[i - 1] == word2[j - 1])	dp[i][j] = dp[i - 1][j - 1];
				else dp[i][j] = min({dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 2});
			}
		}
		return dp[word1.size()][word2.size()]; 
    }
};

  可以使用滚动数组吗?可以,只能用temp来保持dp[i-1][j-1],不能用内层循环倒序遍历来保证dp[j-1]未被处理。还有需要注意的是,每遍历一行,就要重新设置dp[0](dp[i][0])的值即dp[0]=i;。temp2应初始化为i-1即上一轮的dp[0]即dp[i-1][0]。整体代码如下:
题解

class Solution {
public:
    int minDistance(string word1, string word2) {
		vector<int> dp(word2.size() + 1, 0); 
		for (int j = 1; j <= word2.size(); j++)	dp[j] = j;
		for (int i = 1; i <= word1.size(); i++) {
			int temp1, temp2 = i - 1;
            dp[0] = i;
			for (int j = 1; j <= word2.size(); j++) {
				temp1 = temp2;
				temp2 = dp[j];
				if (word1[i - 1] == word2[j - 1])	dp[j] = temp1;
				else dp[j] = min({dp[j] + 1, dp[j - 1] + 1, temp1 + 2});
			}
		}
		return dp[word2.size()]; 
    }
};

  很好,用时和内存消耗都降低了!

11.35 编辑距离

力扣题号: 72.编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符

示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)

示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)

提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/edit-distance

思路
  编辑距离终于来了,这道题⽬如果⼤家没有了解动态规划的话,会感觉超级复杂。编辑距离是⽤动规来解决的经典题⽬,这道题⽬看上去好像很复杂,但⽤动规可以很巧妙的算出最少编辑距离。

  1.确定dp数组及下标的含义
  老规矩我们的dp[i][j]还是这么定义:表示以下标i-1为结尾的word1和以下标为j-1为结尾的word2,最近的编辑距离为dp[i][j]。

  2.确定递推公式
  像之前讲过的那样,这类题一般要考虑两种大情况:1.word1[i-1]=word2[j-1];2.word[i-1]!=word2[j-1]
  1.word1[i-1]=word2[j-1],此时说明不用进行任何操作,保持原来的编辑距离就行,即dp[i][j]=dp[i-1][j-1];
  2.word2[i-1]!=word2[j-1],此时就需要编辑了,而编辑有三种情况:word1删除、word1添加(相当于word2删除)、word1替换。

  • 操作⼀:word1删除⼀个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离再加上⼀个删除操作,即dp[i][j] = dp[i - 1][j] + 1;
  • 操作⼆:word1添加一个元素,相当于在word2删除这个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上⼀个删除操作,即dp[i][j] = dp[i][j - 1] + 1;
  • 操作三:替换元素, word1 替换 word1[i - 1] ,使其与 word2[j - 1] 相同,此时不⽤增加元素,那么以下标 i-2 为结尾的 word1 与 j-2 为结尾的 word2 的最近编辑距离 加上⼀个替换元素的操作,即dp[i][j] = dp[i - 1][j - 1] + 1;
  • 当然这三种子情况要取最小的,所以dp[i][j] = min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}) + 1;

  3.dp数组如何初始化
  从递推公式我们可以看出,dp[i][j]由dp[i-1][j-1]、dp[i][j-1]、dp[i-1][j]推导出来,所以必须要初始化的是dp[i][0]和dp[0][j],dp[i][0]表示以word1[i-1]为结尾的字符串word1和空字符串word2,最近编辑距离,一定是i。同理dp[0][j]一定是j。

  4.确定遍历顺序
  从递推公式可以看出一定是从左到右从上到下。

  5.举例推导dp数组
在这里插入图片描述
  整体代码如下:
题解

class Solution {
public:
    int minDistance(string word1, string word2) {
		vector<vector<int>> dp(word1.size() + 1, vector<int>(word2.size() + 1, 0));
		for (int i = 0; i <= word1.size(); i++)	dp[i][0] = i;
		for (int j = 0; j <= word2.size(); j++) dp[0][j] = j;
		for (int i = 1; i <= word1.size(); i++) {
			for (int j = 1; j <= word2.size(); j++) {
				if (word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1];
				else dp[i][j] = min({dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]}) + 1;
			}
		} 
		return dp[word1.size()][word2.size()];
    }
};

  让我们试试可不可以用滚动数组,这道题和上题是一样的,只能用temp来保持dp[i-1][j-1],而每遍历一行就要给dp[0]赋新值i,temp2则保持上一行的dp0整体代码如下:
题解

class Solution {
public:
    int minDistance(string word1, string word2) {
		vector<int> dp(word2.size() + 1, 0);
		for (int j = 0; j <= word2.size(); j++) dp[j] = j;
		for (int i = 1; i <= word1.size(); i++) {
			int temp1, temp2 = i - 1;
			dp[0] = i;
			for (int j = 1; j <= word2.size(); j++) {
				temp1 = temp2;
				temp2 = dp[j];
				if (word1[i - 1] == word2[j - 1]) dp[j] = temp1;
				else dp[j] = min({temp1, dp[j - 1], dp[j]}) + 1;
			}
		} 
		return dp[word2.size()];
    }
};

  很好,耗时和内存占用都降低了。
  总结:我们用前三道题为编辑距离这道题做铺垫,最后才引出了编辑距离!

11.36 回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串

示例 1:
输入:s = “abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”

示例 2:
输入:s = “aaa”
输出:6
解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”

提示:
1 <= s.length <= 1000
s 由小写英文字母组成

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/palindromic-substrings

11.36.1 暴力解法

思路
  用两层for循环,遍历区间起始位置和终止位置,用双指针法判断这个区间是否是回文串。很明显会超时。

class Solution {
private:
	bool istrue(string SubStr) {
		int i = 0, j = SubStr.size() - 1;
		bool result = true;
		while (i <= j) {
			if (SubStr[i] != SubStr[j])	{
				result = false;
				return false;
			}
		}
		return result;
	} 
public:
    int countSubstrings(string s) {
    	int count = 0;
		for (int i = 0; i < s.size(); i++) {
			for (int j = i; j < s.size(); j++) {
				string SubStr = s.substr(i, j - i + 1);
				if (istrue(SubStr)) count++; 
			}
		}
		return count;
    }
};

11.36.1 动态规划

  1.确定dp数组及下标的含义
  布尔类型的dp[i][j]:表示区间范围[i,j]左闭右闭的子串是否是回文子串。

  2.确定递推公式
  还是和之前一样,分为两种大情况:1.s[i]=s[j];2.s[i]!=s[j];
  1.s[i]=s[j],这种情况又分为三种小情况:

  • 一:i和j相等(同一个字符)当然是回文串,dp[i][j]=true;
  • 二:i和j相差为1(两个字符)也是回文串dp[i][j]=true;
  • 三:i和j相差大于1,此时就要看其区间去头去尾s[i+1,j-1]是不是回文串,如果是,则dp[i][j]=true;否则为dp[i][j]=false。

  2.s[i]!=s[j],这种情况当然不是回文串,dp[i][j]=false;
  每当dp[i][j]=true时,就要用result统计数量。整体代码如下:

if (s[i] = s[j]) {
	if (i == j || i - j == 1) {
		dp[i][j] = true;
		result++;
	}
	else if (dp[i + 1][j - 1])	dp[i][j] = true;
}

  3.dp数组如何初始化
  dp[i][j]表示s[i,j]是否是回文串,刚开始都要初始化为false,因为还没判断就不能说是回文串。

  4.确定遍历顺序
  从递推公式可以看出,dp[i][j]由dp[i+1][j-1]推出,也就是由dp[i][j]的左下方推出,在推导过程中,要保证左下方的数值已经被确定了,所以下到上从左到右遍历。

  5.举例推导dp数组
在这里插入图片描述
  整体代码如下:
题解

class Solution {
public:
    int countSubstrings(string s) {
    	vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
    	int result = 0;
		for (int i = s.size() - 1; i >= 0; i--) {
			for (int j = i; j < s.size(); j++) {
				if (s[i] == s[j]) {
					if (j - i <= 1)	{
						dp[i][j] = true;
						result++;
					}
					else if (dp[i + 1][j - 1])	{
						dp[i][j] = true;
						result++;
					}
				}
			}
		}
		return result;
    }
};

  上述代码是为了区分各种情况,当然是可以简化的:
题解

class Solution {
public:
 	int countSubstrings(string s) {
 		vector<vector<bool>> dp(s.size(), vector<bool>(s.size(), false));
 		int result = 0;
	 	for (int i = s.size() - 1; i >= 0; i--) {
 			for (int j = i; j < s.size(); j++) {
 				if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) {
 					result++;
 					dp[i][j] = true;
 				}
 			}
 		}
 		return result;
 	}
};

11.36.2 双指针法

思路
  动态规划的空间复杂度是偏⾼的,我们再看⼀下双指针法。⾸先确定回⽂串,就是找中⼼然后想两边扩散看是不是对称的就可以了。在遍历中⼼点的时候,要注意中⼼点有两种情况。⼀个元素可以作为中⼼点,两个元素也可以作为中⼼点。那么有⼈同学问了,三个元素还可以做中⼼点呢。其实三个元素就可以由⼀个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。所以我们在计算的时候,要注意⼀个元素为中⼼点和两个元素为中⼼点的情况。这两种情况可以放在⼀起计算,但分别计算思路更清晰,我倾向于分别计算,代码如下:
题解

class Solution {
private:
	int istrue(const string& s, int i, int j, int n) {
	//以i和j为中心起点向外扩散,分别判断所包含的字符串是否是回文串,数量统计在res中 
		int res = 0;
		while (i >= 0 && j < n && s[i] == s[j]) {
			i--;
			j++;
			res++;
		}
		return res; 
	}
public:
    int countSubstrings(string s) {
		int result = 0;
		for (int i = 0; i < s.size(); i++) {
			result += istrue(s, i, i, s.size());
			result += istrue(s, i, i + 1, s.size());
		}
		return result;
    }
};

11.37 最长回文子序列

力扣题号:516.最长回文子序列
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:
输入:s = “bbbab”
输出:4
解释:一个可能的最长回文子序列为 “bbbb” 。

示例 2:
输入:s = “cbbd”
输出:2
解释:一个可能的最长回文子序列为 “bb” 。

提示:
1 <= s.length <= 1000
s 仅由小写英文字母组成

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-palindromic-subsequence

思路
  我们刚刚做过了 动态规划:回⽂⼦串,求的是回⽂⼦串,⽽本题要求的是回⽂⼦序列, 要搞清楚这两者之间的区别。回⽂⼦串是要连续的,回⽂⼦序列可不是连续的! 回⽂⼦串,回⽂⼦序列都是动态规划经典题⽬。回⽂⼦串,可以做这两题:647.回⽂⼦串5.最⻓回⽂⼦串。思路其实是差不多的,但本题要⽐求回⽂⼦串简单⼀点,因为情况少了⼀点。

  1.确定dp数组及下标的含义
  dp[i][j]:表示字符串s[i,j]范围内的最长回文子序列的长度。

  2.确定递推公式
  同样的本题推导dp[i][j] 有两种情况:1.s[i]=s[j];2.s[i]!=s[j]
  1.s[i] = s[j]:当这两个字符相同时,就说明dp[i][j] = dp[i+1][j-1] + 2;,等于内部的回文子序列长度加上头尾两个字符。
  2.s[i] !=s[j]:当这两个字符不相同时,说明回文子序列同时加上这两个字符并不能使回文子序列长度变长。那么我们就要分别加入s[i]、s[j]看看哪个可以组成更长的回文子序列:

  • 加入s[i]的回文子串长度为dp[i][j+1].
  • 加入s[j]的回文子串长度为dp[i+1][j]
  • 这两种情况一定是取最大的,即dp[i][j] = max(dp[i][j+1], dp[i+1][j]);

  3.dp数组如何初始化
  从递推公式可以看出,dp[i][j]由dp[i+1][j-1]、dp[i][j+1]、dp[i+1][j]推导而来,由此可知,dp[i][j]是不能计算到i和j相同这种情况。所以需要我们手动初始化。dp[i][j]当i和j相同时,一定是初始化为1,因为一个字符的回文子序列长度就是1。其他的情况初始化为0就行,这样递推公式dp[i][j] = max(dp[i+1][j],dp[i][j-1]);中,dp[i][j]才不会被初始值覆盖。

  4.确定遍历顺序
  由递推公式可以看出,dp[i][j]由其左方、下方、左下方推导而出,所以为保证推导dp[i][j]时使用的是已经处理过的数值,我们要从下到上,从左到右来遍历。

  5.举例推导dp数组
在这里插入图片描述
  红色框就是最终结果。
  整体代码如下:
题解

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

  总结:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值