算法训练--动态规划(二)
股票问题
121. 买卖股票的最佳时机
-
题目描述
-
题解1:二维数组
-
确定dp数组(dp table)以及下标的含义
dp[i][0] 表示第i天持有股票所得最多现金 ,这里可能有疑惑,本题中只能买卖一次,持有股票之后哪还有现金呢? 其实一开始现金是0,那么加入第i天买入股票现金就是 -prices[i], 这是一个负数。 dp[i][1] 表示第i天不持有股票所得最多现金 注意这里说的是“持有”,“持有”不代表就是当天“买入”!也有可能是昨天就买入了,今天保持持有的状态
-
确定递推公式
如果第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天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0] 同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
-
dp数组如何初始化
由递推公式 dp[i][0] = max(dp[i - 1][0], -prices[i]); 和 dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);可以看出 其基础都是要从dp[0][0]和dp[0][1]推导出来。 那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] -= prices[0]; dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0;
-
确定遍历顺序
从递推公式可以看出dp[i]都是有dp[i - 1]推导出来的,那么一定是从前向后遍历
-
举例推导dp数组
以示例1,输入:[7,1,5,3,6,4]为例,dp数组状态如下:
/** 本题中不持有股票状态所得金钱一定比持有股票状态得到的多! */ class Solution { public int maxProfit(int[] prices) { int len=prices.length; //dp[i][0]:第i天持有股票;dp[i][1]:第i天不持有股票 int[][] dp=new int[len][2]; dp[0][0]=-prices[0]; dp[0][1]=0; for(int i=1;i<len;i++){ dp[i][0]=Math.max(dp[i-1][0],-prices[i]); dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]); } return dp[len-1][1]; } }
-
122. 买卖股票的最佳时机 II
-
题目描述
-
题解
唯一区别本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票)
递推公式
这里重申一下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] 注意这里和121. 买卖股票的最佳时机唯一不同的地方,就是推导dp[i][0]的时候,第i天买入股票的情况。 在121. 买卖股票的最佳时机中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。 而本题,因为一只股票可以买卖多次,所以当第i天买入股票的时候,所持有的现金可能有之前买卖过的利润。 那么第i天持有股票即dp[i][0],如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金 减去 今天的股票价格 即:dp[i - 1][1] - prices[i]。 在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1] 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0] 注意这里和121. 买卖股票的最佳时机就是一样的逻辑,卖出股票收获利润(可能是负值)天经地义!
/** 区别,可以买卖多次 因此持有收益不能固定为当天price的负数 而应该由前一天卖出状态减去当天的price */ class Solution { public int maxProfit(int[] prices) { //dp[][0]:持有股票的最大收益 //dp[][1]:不持有股票的最大收益 int len=prices.length; int[][] dp=new int[len][2]; dp[0][0]=-prices[0]; dp[0][1]=0; for(int i=1;i<len;i++){ //注意这里是和121. 买卖股票的最佳时机唯一不同的地方 dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]); dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]); } return dp[len-1][1]; } }
123. 买卖股票的最佳时机 III
-
题目描述
-
题解
关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖
-
确定dp数组以及下标的含义
一天一共就有五个状态,
- 没有操作
- 第一次买入
- 第一次卖出
- 第二次买入
- 第二次卖出
i表示第i天,j为 [0 - 4] 五个状态,表示第i天状态j所剩最大现金
-
递推公式
需要注意:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区。 达到dp[i][1]状态,有两个具体操作: 操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i] 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1] 那么dp[i][1]究竟选 dp[i-1][0] - prices[i],还是dp[i - 1][1]呢? 一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]); 同理dp[i][2]也有两个操作: 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i] 操作二:第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]);
-
dp数组初始化
第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0; 第0天做第一次买入的操作,dp[0][1] = -prices[0]; 第0天做第一次卖出的操作,这个初始值应该是多少呢? 首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0, 从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了。 所以dp[0][2] = 0; 第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢? 第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。 所以第二次买入操作,初始化为:dp[0][3] = -prices[0]; 同理第二次卖出初始化dp[0][4] = 0;
现在最大的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出
class Solution { public int maxProfit(int[] prices) { int len = prices.length; // 边界判断, 题目中 length >= 1, 所以可省去 if (prices.length == 0) return 0; /* * 定义 5 种状态: * 0: 没有操作, 1: 第一次买入, 2: 第一次卖出, 3: 第二次买入, 4: 第二次卖出 */ int[][] dp = new int[len][5]; dp[0][1] = -prices[0]; // 初始化第二次买入的状态是确保 最后结果是最多两次买卖的最大利润 dp[0][3] = -prices[0]; for (int i = 1; i < len; i++) { dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); dp[i][2] = Math.max(dp[i - 1][2], dp[i][1] + prices[i]); dp[i][3] = Math.max(dp[i - 1][3], dp[i][2] - prices[i]); dp[i][4] = Math.max(dp[i - 1][4], dp[i][3] + prices[i]); } return dp[len - 1][4]; } }
-
188. 买卖股票的最佳时机 IV
-
题目描述
-
题解
确定dp数组及下标定义
使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j] j的状态表示为: 0 表示不操作 1 第一次买入 2 第一次卖出 3 第二次买入 4 第二次卖出 ..... 大家应该发现规律了吧 ,除了0以外,偶数就是卖出,奇数就是买入。 题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。 所以二维dp数组的C++定义为: vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));
确定递推公式
还要强调一下:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区。 达到dp[i][1]状态,有两个具体操作: 操作一:第i天买入股票了,那么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天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i] 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2] 所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]) 同理可以类比剩下的状态,代码如下: 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]); }
本题和动态规划:123.买卖股票的最佳时机III (opens new window)最大的区别就是这里要类比j为奇数是买,偶数是卖的状态
数组初始化
第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][j]当j为奇数的时候都初始化为 -prices[0] 代码如下: for (int j = 1; j < 2 * k; j += 2) { dp[0][j] = -prices[0]; } 在初始化的地方同样要类比j为偶数是卖、奇数是买的状态。
最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]即红色部分就是最后求解
class Solution { public int maxProfit(int k, int[] prices) { if(prices.length==0) return 0; //[天数][股票状态] //奇数表示第k次交易 持有/买入;偶数表示第k次交易不持有/卖出;0表示没有操作 int[][] dp=new int[prices.length][2*k+1]; for(int i=1;i<2*k;i+=2){ dp[0][i]=-prices[0]; } for(int i=1;i<prices.length;i++){ for(int j=0;j<2*k-1;j+=2){ dp[i][j+1]=Math.max(dp[i-1][j+1],dp[i-1][j]-prices[i]); dp[i][j+2]=Math.max(dp[i-1][j+2],dp[i-1][j+1]+prices[i]); } } return dp[prices.length-1][2*k]; } }
309. 最佳买卖股票时机含冷冻期
-
题目描述
-
题解
相对于动态规划:122.买卖股票的最佳时机II (opens new window),本题加上了一个冷冻期
在动态规划:122.买卖股票的最佳时机II (opens new window)中有两个状态,持有股票后的最多现金,和不持有股票的最多现金。
确定dp数组以及下标的含义 dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。 出现冷冻期之后,状态其实是比较复杂度,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。 具体可以区分出如下四个状态: 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)卖出股票状态,这里就有两种卖出股票状态 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态 状态三:今天卖出了股票 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天! j的状态为: 0:状态一 1:状态二 2:状态三 3:状态四 注意这里的每一个状态,例如状态一,是买入股票状态并不是说今天已经就买入股票,而是说保存买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态。
确定递推公式:
达到买入股票状态(状态一)即:dp[i][0],有两个具体操作: 操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0] 操作二:今天买入了,有两种情况 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i] 前一天是保持卖出股票状态(状态二),dp[i - 1][1] - prices[i] 所以操作二取最大值,即:max(dp[i - 1][3], dp[i - 1][1]) - prices[i] 那么dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]); 达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作: 操作一:前一天就是状态二 操作二:前一天是冷冻期(状态四) dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); 达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作: 操作一:昨天一定是买入股票状态(状态一),今天卖出 即:dp[i][2] = dp[i - 1][0] + prices[i]; 达到冷冻期状态(状态四),即:dp[i][3],只有一个操作: 操作一:昨天卖出了股票(状态三) 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];
dp数组初始化
这里主要讨论一下第0天如何初始化。 如果是持有股票状态(状态一)那么:dp[0][0] = -prices[0],买入股票所剩现金为负数。 保持卖出股票状态(状态二),第0天没有卖出dp[0][1]初始化为0就行, 今天卖出了股票(状态三),同样dp[0][2]初始化为0,因为最少收益就是0,绝不会是负数。 同理dp[0][3]也初始为0。
最后结果是取 状态二,状态三,和状态四的最大值,状态四是冷冻期,最后一天如果是冷冻期也可能是最大值
class Solution { public int maxProfit(int[] prices) { //四种状态 0:买入状态 1:保持卖出状态 2:今天卖出了 3:冷冻期 int len=prices.length; if(len==0) return 0; int[][] dp=new int[len][4]; dp[0][0]=-prices[0]; for(int i=1;i<len;i++){ dp[i][0]=Math.max(dp[i-1][0],Math.max(dp[i-1][3],dp[i-1][1])-prices[i]); dp[i][1]=Math.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 Math.max(dp[len-1][1],Math.max(dp[len-1][2],dp[len-1][3])); } }
714. 买卖股票的最佳时机含手续费
-
题目描述
-
题解
本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。 唯一差别在于递推公式部分,所以本篇也就不按照动规五部曲详细讲解了,主要讲解一下递推公式部分。 这里重申一下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] 所以:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); 在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1] 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,注意这里需要有手续费了即:dp[i - 1][0] + prices[i] - fee 所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee); 本题和动态规划:122.买卖股票的最佳时机II (opens new window)的区别就是这里需要多一个减去手续费的操作。
class Solution { public int maxProfit(int[] prices, int fee) { //dp[i][0] 表示第i天持有股票所省最多现金。 dp[i][1] 表示第i天不持有股票所得最多现金 int len=prices.length; if(len==0) return 0; int[][] dp=new int[len][2]; dp[0][0]=-prices[0]; for(int i=1;i<len;i++){ dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]); //卖出操作的时候减去手续费 dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]-fee); } return dp[len-1][1]; } }
子序列问题
300. 最长递增子序列
-
题目描述
-
题解
动规五部曲:
确定dp数组及下标含义
dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度
状态转移方程
位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。
所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值
dp数组初始化
每一个i,对应的dp[i](即最长上升子序列)起始大小至少都是1
确定遍历顺序
dp[i] 是有0到i-1各个位置的最长升序子序列 推导而来,那么遍历i一定是从前向后遍历。
j其实就是0到i-1,遍历i的循环在外层,遍历j则在内层
class Solution { public int lengthOfLIS(int[] nums) { int[] dp=new int[nums.length]; Arrays.fill(dp,1); int res=1; for(int i=1;i<nums.length;i++){ for(int j=0;j<i;j++){ if(nums[i]>nums[j]){ //这里不是要比较dp[i]和dp[j+1] //而是要取dp[j+1]的最大值 dp[i]=Math.max(dp[i],dp[j]+1); } } res=res>dp[i]?res:dp[i]; } return res; } }
674. 最长连续递增序列
-
题目描述

-
题解
确定dp数组(dp table)以及下标的含义 dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]。 注意这里的定义,一定是以下标i为结尾,并不是说一定以下标0为起始位置。 确定递推公式 如果 nums[i + 1] > nums[i],那么以 i+1 为结尾的数组的连续递增的子序列长度 一定等于 以i为结尾的数组的连续递增的子序列长度 + 1 。即:dp[i + 1] = dp[i] + 1; 注意这里就体现出和动态规划:300.最长递增子序列 (opens new window)的区别! 因为本题要求连续递增子序列,所以就必要比较nums[i + 1]与nums[i],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。 既然不用j了,那么也不用两层for循环,本题一层for循环就行,比较nums[i + 1] 和 nums[i]。
class Solution { public int findLengthOfLCIS(int[] nums) { int len=nums.length; if(len==0) return 0; int[] dp=new int[len]; Arrays.fill(dp,1); int res=1; //连续子序列最少也是1 for(int i=0;i<len-1;i++){ if(nums[i+1]>nums[i]){ //连续记录 dp[i+1]=dp[i]+1; } if(dp[i+1]>res) res=dp[i+1]; } return res; } }
718. 最长重复子数组
-
题目描述
-
题解
注意题目中说的子数组,其实就是连续子序列。这种问题动规最拿手,动规五部曲分析如下:
确定dp数组(dp table)以及下标的含义 dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 确定递推公式 根据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开始! 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,正好符合递推公式逐步累加起来。 确定遍历顺序 外层for循环遍历A,内层for循环遍历B。 同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i][j]的最大值记录下来。 代码如下: for (int i = 1; i <= A.size(); i++) { for (int j = 1; j <= B.size(); j++) { if (A[i - 1] == B[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } if (dp[i][j] > result) result = dp[i][j]; } }
class Solution { public int findLength(int[] nums1, int[] nums2) { int[][] dp=new int[nums1.length+1][nums2.length+1]; int res=0; for(int i=1;i<=nums1.length;i++){ for(int j=1;j<=nums2.length;j++){ //比较的是i-1,j-1 所以下标从1开始 <=length if(nums1[i-1]==nums2[j-1]){ dp[i][j]=dp[i-1][j-1]+1; res=Math.max(res,dp[i][j]); } } } return res; } }
1143. 最长公共子序列
-
题目描述
-
题解
本题和动态规划:718. 最长重复子数组 (opens new window)区别在于这里不要求是连续的了,但要有相对顺序,即:"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
继续动规五部曲分析如下: 确定dp数组(dp table)以及下标的含义 dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j] 确定递推公式 主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同 text1[i - 1] 与 text2[j - 1]不相同 如果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]); 代码如下: 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]); } dp数组如何初始化 先看看dp[i][0]应该是多少呢? test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0; 同理dp[0][j]也是0。 其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
/** 子序列和子数组的区别 不连续 子数组:nums1[i-1]==nums2[j-1] ==> dp[i][j]=dp[i-1][j-1]+1; 子序列:nums1[i-1]==nums2[j-1] ==> dp[i][j]=dp[i-1][j-1]+1; nums1[i-1]!=nums2[j-1] ==> dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]); */ class Solution { public int longestCommonSubsequence(String text1, String text2) { int[][] dp=new int[text1.length()+1][text2.length()+1]; for(int i=1;i<=text1.length();i++){ char ch1=text1.charAt(i-1); for(int j=1;j<=text2.length();j++){ char ch2=text2.charAt(j-1); if(ch1==ch2){ dp[i][j]=dp[i-1][j-1]+1; }else{ dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]); } } } return dp[text1.length()][text2.length()]; } }
1035. 不相交的线
-
题目描述
-
题解
直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
拿示例一A = [1,4,2], B = [1,2,4]为例,相交情况如图:
其实也就是说A和B的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串A中数字1的后面,那么数字4也应该在字符串B数字1的后面)
这么分析完之后,大家可以发现:本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!
/** 最长递增子序列 */ class Solution { public int maxUncrossedLines(int[] nums1, int[] nums2) { int[][] dp=new int[nums1.length+1][nums2.length+1]; for(int i=1;i<=nums1.length;i++){ for(int j=1;j<=nums2.length;j++){ if(nums1[i-1]==nums2[j-1]){ dp[i][j]=dp[i-1][j-1]+1; }else{ dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]); } } } return dp[nums1.length][nums2.length]; } }
53. 最大子序和
-
题目描述
-
题解
/** dp[i]只有两个方向可以推出来: dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和 nums[i],即:从头开始计算当前连续子序列和 */ class Solution { public int maxSubArray(int[] nums) { int res=nums[0]; int[] dp=new int[nums.length]; dp[0]=nums[0]; for(int i=1;i<nums.length;i++){ dp[i]=Math.max(dp[i-1]+nums[i],nums[i]); res=Math.max(res,dp[i]); } return res; } }
392. 判断子序列
-
题目描述
-
题解
这道题应该算是编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况。
所以掌握本题也是对后面要讲解的编辑距离的题目打下基础
动态规划五部曲分析如下: 确定dp数组(dp table)以及下标的含义 dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。 注意这里是判断s是否为t的子序列。即t的长度是大于等于s的。 确定递推公式 在确定递推公式的时候,首先要考虑如下两种操作,整理如下: if (s[i - 1] == t[j - 1]):t中找到了一个字符在s中也出现了 if (s[i - 1] != t[j - 1]):相当于t要删除元素,继续匹配 if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1(如果不理解,在回看一下dp[i][j]的定义) if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1]; dp数组如何初始化 从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],所以dp[0][0]和dp[i][0]是一定要初始化的。 这里大家已经可以发现,在定义dp[i][j]含义的时候为什么要表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。
class Solution { public boolean isSubsequence(String s, String t) { int sLen=s.length(); int tLen=t.length(); int[][] dp=new int[sLen+1][tLen+1]; for(int i=1;i<=sLen;i++){ char ch1=s.charAt(i-1); for(int j=1;j<=tLen;j++){ char ch2=t.charAt(j-1); if(ch1==ch2){ dp[i][j]=dp[i-1][j-1]+1; }else{ dp[i][j]=dp[i][j-1]; } } } if(sLen==dp[sLen][tLen]) return true; return false; } }
115. 不同的子序列
-
题目描述
-
题解
这道题目如果不是子序列,而是要求连续序列的,那就可以考虑用KMP。
这道题目相对于72. 编辑距离,简单了不少,因为本题相当于只有删除操作,不用考虑替换增加之类的
确定dp数组(dp table)以及下标的含义 dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。 确定递推公式 这一类问题,基本是要分析两种情况 s[i - 1] 与 t[j - 1]相等 s[i - 1] 与 t[j - 1] 不相等 当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。 例如: 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]; 当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配,即:dp[i - 1][j] 所以递推公式为:dp[i][j] = dp[i - 1][j]; dp数组如何初始化 从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][0] 和dp[0][j]是一定要初始化的。 每次当初始化的时候,都要回顾一下dp[i][j]的定义,不要凭感觉初始化。 dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。
class Solution { public int numDistinct(String s, String t) { int sLen=s.length(); int tLen=t.length(); int[][] dp=new int[sLen+1][tLen+1]; for (int i = 0; i < sLen + 1; i++) { dp[i][0] = 1; } for(int i=1;i<=sLen;i++){ char ch1=s.charAt(i-1); for(int j=1;j<=tLen;j++){ char ch2=t.charAt(j-1); if(ch1==ch2){ dp[i][j]=dp[i-1][j-1]+dp[i-1][j]; }else{ dp[i][j]=dp[i-1][j]; } } } return dp[sLen][tLen]; } }
583. 两个字符串的删除操作
-
题目描述
-
题解
本题和动态规划:1143.最长公共子序列 (opens new window)基本相同,只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
class Solution { public int minDistance(String word1, String word2) { int len1=word1.length(),len2=word2.length(); int[][] dp=new int[len1+1][len2+1]; for(int i=1;i<=len1;i++){ for(int j=1;j<=len2;j++){ if(word1.charAt(i-1)==word2.charAt(j-1)){ dp[i][j]=dp[i-1][j-1]+1; }else{ dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]); } } } return len1+len2-2*dp[len1][len2]; } }
72. 编辑距离
-
题目描述
-
题解
1. 确定dp数组(dp table)以及下标的含义 dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。 2. 确定递推公式 在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下: if (word1[i - 1] == word2[j - 1]) 不操作 if (word1[i - 1] != word2[j - 1]) 增 删 换 也就是如上4种情况。 if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1]; 那么就在回顾上面讲过的dp[i][j]的定义,word1[i - 1] 与 word2[j - 1]相等了,那么就不用编辑了,以下标i-2为结尾的字符串word1和以下标j-2为结尾的字符串word2的最近编辑距离dp[i - 1][j - 1]就是 dp[i][j]了。 在整个动规的过程中,最为关键就是正确理解dp[i][j]的定义! if (word1[i - 1] != word2[j - 1]),此时就需要编辑了,如何编辑呢? 操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。即 dp[i][j] = dp[i - 1][j] + 1; 操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。即 dp[i][j] = dp[i][j - 1] + 1; word2添加一个元素,相当于word1删除一个元素,例如 word1 = "ad" ,word2 = "a",word1删除元素'd' 和 word2添加一个元素'd',变成word1="a", word2="ad", 最终的操作数是一样! dp数组如下图所示意的: a a d +-----+-----+ +-----+-----+-----+ | 0 | 1 | | 0 | 1 | 2 | +-----+-----+ ===> +-----+-----+-----+ a | 1 | 0 | a | 1 | 0 | 1 | +-----+-----+ +-----+-----+-----+ d | 2 | 1 | +-----+-----+ 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增加元素,那么以下标i-2为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个替换元素的操作。即 dp[i][j] = dp[i - 1][j - 1] + 1; 综上,当 if (word1[i - 1] != word2[j - 1]) 时取最小的,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1; 递归公式代码如下: 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 - 1][j], dp[i][j - 1]}) + 1; } 3. dp数组如何初始化 再回顾一下dp[i][j]的定义: dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。 那么dp[i][0] 和 dp[0][j] 表示什么呢? dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。 那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i; 同理dp[0][j] = j;
/** dp[i][j] 表示以下标i-1为结尾的字符串word1 和以下标j-1为结尾的字符串word2 最近编辑距离为dp[i][j] */ class Solution { public int minDistance(String word1, String word2) { int len1=word1.length(),len2=word2.length(); int[][] dp=new int[len1+1][len2+1]; //初始化参考dp数组的定义 //dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2 //那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i; for(int i=0;i<=len1;i++)dp[i][0]=i; for(int j=0;j<=len2;j++)dp[0][j]=j; for(int i=1;i<=len1;i++){ for(int j=1;j<=len2;j++){ if(word1.charAt(i-1)==word2.charAt(j-1)){ dp[i][j]=dp[i-1][j-1]; }else{ dp[i][j]=Math.min(Math.min(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1])+1; } } } return dp[len1][len2]; } }
647. 回文子串
-
题目描述
-
题解
动规五部曲: 确定dp数组(dp table)以及下标的含义 布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。 确定递推公式 在确定递推公式时,就要分析如下几种情况。 整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。 当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。 当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串 情况二:下标i 与 j相差为1,例如aa,也是回文子串 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。 以上三种情况分析完了,那么递归公式如下: if (s[i] == s[j]) { if (j - i <= 1) { // 情况一 和 情况二 result++; dp[i][j] = true; } else if (dp[i + 1][j - 1]) { // 情况三 result++; dp[i][j] = true; } } result就是统计回文子串的数量。 dp数组如何初始化 dp[i][j]初始化为false。 确定遍历顺序 遍历顺序可有有点讲究了。 一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
class Solution { public int countSubstrings(String s) { int len = s.length(); boolean[][] dp = new boolean[len][len]; int res = 0; //倒叙防止重复统计 for (int i = len-1; i >=0; i--) { for (int j = i; j < len; j++) { if (s.charAt(j) == s.charAt(i)) { if(j-i<3){ dp[i][j]=true; }else{ //向内收敛 dp[i][j] = dp[i + 1][j - 1]; } }else{ dp[i][j]=false; } //倒叙 不用担心重复计算 if(dp[i][j]) res++; } } return res; } }
516. 最长回文子序列
-
题目描述
-
题解
我们刚刚做过了 动态规划:回文子串 (opens new window),求的是回文子串,而本题要求的是回文子序列, 要搞清楚这两者之间的区别。
回文子串是要连续的,回文子序列可不是连续的! 回文子串,回文子序列都是动态规划经典题目。
回文子串,可以做这两题:
- 647.回文子串
- 5.最长回文子串
思路其实是差不多的,但本题要比求回文子串简单一点,因为情况少了一点
动规五部曲:
确定dp数组(dp table)以及下标的含义 dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。 确定递推公式 在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同。 如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;
如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子串的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。 加入s[j]的回文子序列长度为dp[i + 1][j]。 加入s[i]的回文子序列长度为dp[i][j - 1]。 那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
dp数组如何初始化 首先要考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出 递推公式是计算不到 i 和j相同时候的情况。 所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。 其他情况dp[i][j]初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 中dp[i][j]才不会被初始值覆盖。
class Solution { public int longestPalindromeSubseq(String s) { //dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j] int len = s.length(); int[][] dp = new int[len][len]; for (int i = 0; i < len; i++) dp[i][i] = 1; //回文倒序 for (int i = len - 1; i >= 0; i--) { //注意此处 i=0时防止 j-1=-1 导致下标越界 for (int j = i + 1; j < len; j++) { if (s.charAt(i) == s.charAt(j)) { dp[i][j] = dp[i + 1][j - 1] + 2; } else { //区别 子序列 这边都是 i+1 j-1 dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); } } } return dp[0][len-1]; } } //解法二 //回文,注意遍历顺序 倒叙倒叙倒序 //回文子串数的基础上 跟新回文长度 int maxLen=0; boolean[][] dp = new boolean[A.length()][A.length()]; for (int i = A.length()-1; i >= 0; i--) { for (int j = i; j < A.length(); j++) { if (A.charAt(i) == A.charAt(j)) { if (j - i < 3) { dp[i][j]=true; }else{ dp[i][j]=dp[i+1][j-1]; } //更新maxLen if(dp[i][j] && j-i+1>maxLen){ maxLen=j-i+1; } } } } return maxLen;
647. 回文子串
-
题目描述
-
题解
/** dp[i][j]:s字符串下标i到下标j的字串是否是一个回文串,即s[i, j] 当两端字母一样时,才可以两端收缩进一步判断 i--,j++,即两端收缩之后i,j指针指向同一个字符或者i超过j了,必然是一个回文串 */ class Solution { public int countSubstrings(String s) { int res=0; int len=s.length(); boolean[][] dp=new boolean[len][len]; for(int i=len-1;i>=0;i--){ for(int j=i;j<len;j++){ if(s.charAt(i)==s.charAt(j)){ if(j-i<3){ dp[i][j]=true; }else{ dp[i][j]=dp[i+1][j-1]; } }else{ dp[i][j]=false; } if(dp[i][j]) res++; } } return res; } }
CodeTOP系列
85. 最大矩形
-
题目描述
-
题解
class MaximalRectangle { /** * 85. 最大矩形 */ public int maximalRectangle(char[][] matrix) { if (matrix.length == 0 || matrix[0].length == 0) { return 0; } int col = matrix.length; int row = matrix[0].length; int[] heights = new int[row]; int ans = 0; for (int i = 0; i < col; i++) { for (int j = 0; j < row; j++) { if (matrix[i][j] ==1) { heights[j] += 1; } else { heights[j] = 0; } } ans = Math.max(ans, largestRectangleArea(heights)); } return ans; } /** * 84. 柱状图中最大的矩形 */ public int largestRectangleArea(int[] heights) { int res = 0; Deque<Integer> stack = new ArrayDeque<>(); int[] new_heights = new int[heights.length + 2]; for (int i = 1; i < heights.length + 1; i++) { new_heights[i] = heights[i - 1]; } for (int i = 0; i < new_heights.length; i++) { while (!stack.isEmpty() && new_heights[stack.peek()] > new_heights[i]) { int cur = stack.pop(); res = Math.max(res, (i - stack.peek() - 1) * new_heights[cur]); } stack.push(i); } return res; }
1143. 最长公共子序列
-
题目描述
-
题解
/** 子序列和子数组的区别 不连续 子数组:nums1[i-1]==nums2[j-1] ==> dp[i][j]=dp[i-1][j-1]+1; 子序列:nums1[i-1]==nums2[j-1] ==> dp[i][j]=dp[i-1][j-1]+1; nums1[i-1]!=nums2[j-1] ==> dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]); */ class Solution { public int longestCommonSubsequence(String text1, String text2) { int len1=text1.length(),len2=text2.length(); int[][] dp=new int[len1+1][len2+1]; for(int i=1;i<=len1;i++){ for(int j=1;j<=len2;j++){ if(text1.charAt(i-1)==text2.charAt(j-1)){ dp[i][j]=dp[i-1][j-1]+1; }else{ dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]); } } } return dp[len1][len2]; } }
139. 单词拆分
-
题目描述
-
题解
class Solution { public boolean wordBreak(String s, List<String> wordDict) { boolean[] dp=new boolean[s.length()+1]; dp[0]=true; for(int i=1;i<=s.length();i++){ for(int j=0;j<i;j++){ if(wordDict.contains(s.substring(j,i))&& dp[j]){ dp[i]=true; } } } return dp[s.length()]; } }
53. 最大子数组和
-
题目描述
-
题解
/** dp[i]只有两个方向可以推出来: dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和 nums[i],即:从头开始计算当前连续子序列和 */ class Solution { public int maxSubArray(int[] nums) { int res=nums[0]; int[] dp=new int[nums.length]; dp[0]=nums[0]; for(int i=1;i<nums.length;i++){ dp[i]=Math.max(dp[i-1]+nums[i],nums[i]); res=Math.max(res,dp[i]); } return res; } }
剑指 Offer 42. 连续子数组的最大和
-
题目描述
-
题解
class Solution { public int maxSubArray(int[] nums) { int[] dp=new int[nums.length]; dp[0]=nums[0]; int res=nums[0]; for(int i=1;i<nums.length;i++){ dp[i]=Math.max(nums[i],dp[i-1]+nums[i]); res=Math.max(dp[i],res); } return res; } }
322. 零钱兑换
-
题目描述
-
题解
class Solution { public int coinChange(int[] coins, int amount) { int[] dp=new int[amount+1]; int max=Integer.MAX_VALUE; Arrays.fill(dp,max); dp[0]=0; for(int i=0;i<coins.length;i++){ for(int j=0;j<=amount;j++){ if(j>=coins[i] && dp[j-coins[i]]!=max){ dp[j]=Math.min(dp[j],dp[j-coins[i]]+1); } } } return dp[amount]==max?-1:dp[amount]; } }
518. 零钱兑换 II
-
题目描述
-
题解
/** 求组合(完全背包:硬币数量不限) 求装满背包有几种方法,一般公式都是:dp[j] += dp[j - nums[i]]; */ class Solution { public int change(int amount, int[] coins) { int[] dp=new int[amount+1]; dp[0]=1; for(int i=0;i<coins.length;i++){ for(int j=0;j<=amount;j++){ if(j>=coins[i]){ dp[j]+=dp[j-coins[i]]; } } } return dp[amount]; } }
72. 编辑距离
-
题目描述
-
题解
/** dp[i][j] 表示以下标i-1为结尾的字符串word1 和以下标j-1为结尾的字符串word2 最近编辑距离为dp[i][j] */ class Solution { public int minDistance(String word1, String word2) { int len1=word1.length(),len2=word2.length(); int[][] dp=new int[len1+1][len2+1]; //初始化参考dp数组的定义 //dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2 //那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i; for(int i=0;i<=len1;i++)dp[i][0]=i; for(int j=0;j<=len2;j++)dp[0][j]=j; for(int i=1;i<=len1;i++){ for(int j=1;j<=len2;j++){ if(word1.charAt(i-1)==word2.charAt(j-1)){ dp[i][j]=dp[i-1][j-1]; }else{ dp[i][j]=Math.min(Math.min(dp[i][j-1],dp[i-1][j]),dp[i-1][j-1])+1; } } } return dp[len1][len2]; } }
221. 最大正方形
-
题目描述
-
题解
class Solution { public int maximalSquare(char[][] matrix) { int maxSize=0; int row=matrix.length,col=matrix[0].length; //dp定义:以i,j为右边界下标所能构成的正方形的最大边长 int[][] dp=new int[row][col]; for(int i=0;i<row;i++){ for(int j=0;j<col;j++){ if(matrix[i][j]=='1'){ if(i==0 || j==0){ dp[i][j]=1; }else{ //递推公式 dp[i][j]=Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i][j-1]))+1; } maxSize=Math.max(maxSize,dp[i][j]); } } } return maxSize * maxSize; } }
647. 回文子串
-
题目描述
-
题解
class Solution { public int countSubstrings(String s) { int res=0; int len=s.length(); boolean[][] dp=new boolean[len][len]; for(int i=len-1;i>=0;i--){ for(int j=i;j<len;j++){ if(s.charAt(i)==s.charAt(j)){ if(j-i<3){ dp[i][j]=true; }else{ dp[i][j]=dp[i+1][j-1]; } }else{ dp[i][j]=false; } if(dp[i][j]) res++; } } return res; } }
198. 打家劫舍
-
题目描述
-
题解
class Solution { public int rob(int[] nums) { if(nums.length==0) return 0; if(nums.length==1) return nums[0]; int[] dp=new int[nums.length]; dp[0]=nums[0]; dp[1]=Math.max(nums[1],dp[0]); for(int i=2;i<nums.length;i++){ dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]); } return dp[nums.length-1]; } }
213. 打家劫舍 II
-
题目描述
-
题解
跟上一题差不多,唯一区别就是成环了
class Solution { public int rob(int[] nums) { if(nums.length==1) return nums[0]; //不偷第一间房 包含尾元素,不包含首元素 int res1=robTraversal(nums,1,nums.length); //不偷最后一间房 包含首元素不包含尾元素 int res2=robTraversal(nums,0,nums.length-1); return Math.max(res1,res2); } //198.打家劫舍的逻辑 public int robTraversal(int[] nums,int start,int end){ if(end-start==1) return nums[start]; int[] dp=new int[nums.length]; dp[start]=nums[start]; dp[start+1]=Math.max(nums[start],nums[start+1]); for(int i=start+2;i<end;i++){ dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]); } return dp[end-1]; } }
121. 买卖股票的最佳时机
-
题目描述
-
题解
/** 本题中不持有股票状态所得金钱一定比持有股票状态得到的多! */ class Solution { public int maxProfit(int[] prices) { int len=prices.length; //dp[i][0]:第i天持有股票;dp[i][1]:第i天不持有股票 int[][] dp=new int[len][2]; dp[0][0]=-prices[0]; dp[0][1]=0; for(int i=1;i<len;i++){ dp[i][0]=Math.max(dp[i-1][0],-prices[i]); dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]); } return dp[len-1][1]; } }
123. 买卖股票的最佳时机 III
-
题目描述
-
题解
class Solution { public int maxProfit(int[] prices) { int len = prices.length; if (prices.length == 0) return 0; /* * 定义 5 种状态: * 0: 没有操作, 1: 第一次买入, 2: 第一次卖出, 3: 第二次买入, 4: 第二次卖出 */ int[][] dp = new int[len][5]; //初始化第二次买入的状态是确保 最后结果是最多两次买卖的最大利润 dp[0][1] = -prices[0]; dp[0][3] = -prices[0]; for (int i = 1; i < len; i++) { dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); dp[i][2] = Math.max(dp[i - 1][2], dp[i][1] + prices[i]); dp[i][3] = Math.max(dp[i - 1][3], dp[i][2] - prices[i]); dp[i][4] = Math.max(dp[i - 1][4], dp[i][3] + prices[i]); } return dp[len - 1][4]; } }
300. 最长递增子序列
-
题目描述
-
题解
class Solution { public int lengthOfLIS(int[] nums) { int[] dp=new int[nums.length]; Arrays.fill(dp,1); int res=1; for(int i=1;i<nums.length;i++){ for(int j=0;j<i;j++){ if(nums[i]>nums[j]){ //这里不是要比较dp[i]和dp[j+1] //而是要取dp[j+1]的最大值 dp[i]=Math.max(dp[i],dp[j]+1); } } res=Math.max(res,dp[i]); } return res; } }
718. 最长重复子数组
-
题目描述
-
题解
class Solution { public int findLength(int[] nums1, int[] nums2) { int len1=nums1.length,len2=nums2.length; int res=0; int[][] dp=new int[len1+1][len2+1]; for(int i=1;i<=len1;i++){ for(int j=1;j<=len2;j++){ if(nums1[i-1]==nums2[j-1]){ dp[i][j]=dp[i-1][j-1]+1; res=Math.max(dp[i][j],res); } } } return res; } }
64. 最小路径和
-
题目描述
-
题解
class Solution { public int minPathSum(int[][] grid) { int m=grid.length,n=grid[0].length; int[][] dp=new int[m][n]; dp[0][0]=grid[0][0]; for(int i=1;i<m;i++)dp[i][0]+=dp[i-1][0]+grid[i][0]; for(int j=1;j<n;j++)dp[0][j]+=dp[0][j-1]+grid[0][j]; for(int i=1;i<m;i++){ for(int j=1;j<n;j++){ dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j]; } } return dp[m-1][n-1]; } }
152. 乘积最大子数组
-
题目描述
-
题解
class Solution { public int maxProduct(int[] nums) { //一个保存最大的,一个保存最小的 int max = Integer.MIN_VALUE, imax = 1, imin = 1; for(int i=0; i<nums.length; i++){ //如果数组的数是负数,那么会导致最大的变最小的,最小的变最大的。因此交换两个的值 if(nums[i] < 0){ int tmp = imax; imax = imin; imin = tmp;} imax = Math.max(imax*nums[i], nums[i]); imin = Math.min(imin*nums[i], nums[i]); max = Math.max(max, imax); } return max; } }
圆环回原点
-
题目描述
-
题解
//递推公式 //dp[i][j]表示从0出发,走i步到j的方案数 dp[i][j] = dp[i-1][(j-1+length)%length] + dp[i-1][(j+1)%length]
32. 最长有效括号
-
题目描述
-
题解
class Solution { public int longestValidParentheses(String s) { if(s.length()<=1) return 0; char[] sArr=s.toCharArray(); int[] dp=new int[sArr.length]; dp[1]= sArr[0]=='('&& sArr[1]==')'?2:0; int max=dp[1]; for(int i=2;i<sArr.length;i++){ if(sArr[i]=='(') dp[i]=0; else{ if(sArr[i-1]=='('){ dp[i]=dp[i-2]+2; }else{ if(i-dp[i-1]-1<0 || sArr[i-dp[i-1]-1]==')'){ dp[i]=0; }else{ dp[i]=i-dp[i-1]-1>=1?dp[i-1]+2+dp[i-dp[i-1]-2]:dp[i-1]+2; } } } max=Math.max(max,dp[i]); } return max; } }