动态规划学习记录(自用)
一、适用范围
当问题可分解为相互独立的子问题时,考虑动态规划。例如:斐波那契数列、爬楼梯问题、最值问题、背包问题等。
二、动态规划经典题型
1、最简单的爬楼梯问题
问题描述:
假设爬 n 阶楼梯,每次可以爬 1 或 2 个台阶,有多少种不同的爬楼方法?
思路:
要到n阶,只能从n-1阶或n-2阶上来,因此定义dp[i]表示爬到i阶对应的方法数,状态转移方程即为dp[i]=dp[i-1]+dp[i-2]。接下来需要判断边界值:根据状态转移方程,显然i>=2。因此初步确定边界值为dp[0]=0,dp[1]=1,此时代入状态转移方程得dp[2]=0+1=1。根据实际情况,要到2阶楼梯,要么一次爬2阶,要么一次爬1阶爬2次,共有两种方法。因此dp[2]也是边界值。
核心算法:
int[] dp=new int[n+1];//dp[i]表示爬到i阶的方法数。
if n<=2:return n;
else://dp[0]用不到,不需要赋值,默认为0
for i=3,i<=n:
dp[i]=dp[i-1]+dp[i-2];
return dp[n];
代码:
public static int climbStairs(int n) {
int res=0;
int[]dp=new int[n+1];
dp[0]=0;
dp[1]=1;
if(n>=2) {
dp[2]=2;
for(int i=3;i<=n;i++) {
dp[i]=dp[i-1]+dp[i-2];
}
}
res=dp[n];
return res;
}
爬楼问题推广一:爬楼途径不定
问题描述:
假设爬 n 阶楼梯,climbs数组存放一次爬几阶楼梯,默认不重复,有多少种不同的爬楼方法?例:输入:n=4,climbs={1,2,3} 输出:7
思路:
本题是原爬楼问题的扩展,相当于climbs中只有两个元素1和2。对于本题,解题思想与上述爬楼问题一致。要到n阶,只能从n-climbs[j]阶上来,因此需要遍历climbs数组。定义dp[i]表示爬到i阶对应的方法数,状态转移方程即为dp[i]=dp[i-climbs[0]]+dp[i-climbs[1]]+…+dp[i-climbs[len-1]]。接下来需要求边界值:dp[0]=0。需要注意的是,当状态转移方程中存在dp[0]时,需要加1。参见核心算法。
核心算法:
int[]dp=new int[n+1];
dp[0]=0;
for i=1,i<=n:
for j=0,j<climbs.length:
if i-climbs[j]==0:
dp[i]=dp[i]+dp[i-climbs[j]]+1;//状态转移方程中存在dp[0]时,需要加1
else if i-climbs[j]>0://不考虑小于0的情况,小于0说明不存在合法组合
dp[i]=dp[i]+dp[i-climbs[j]];
return dp[n];
代码:
public static int climbStairs(int[]climbs,int n) {
int res=0;
int[]dp=new int[n+1];
dp[0]=0;
for(int i=1;i<=n;i++) {
for(int j=0;j<climbs.length;j++) {
if(i-climbs[j]==0) {
dp[i]=dp[i]+dp[i-climbs[j]]+1;
//当存在dp[0]时,相当于从0阶一次跳i阶到达目的地,只有一种跳法。而dp[0]=0,故需加1
}
else if(i-climbs[j]>0) {
dp[i]=dp[i]+dp[i-climbs[j]];
}
}
}
res=dp[n];
return res;
}
爬楼问题推广二:零钱问题之不同组合数
给定amount代表金额,coins数组存放零钱,假设每种零钱有无限个,求兑换amount的组合数。若不存在返回0。例如:输入:amount = 5, coins = [1, 2, 5],输出:4。解释:有四种方式可以凑成总金额:5=5;5=2+2+1;5=2+1+1+1;5=1+1+1+1+1。
思路:
本题与爬楼问题推广一题干条件一致,区别在于,推广一求的是排列数,而推广二求的是组合数,排列数不考虑顺序,组合数考虑顺序,因此两题的算法不同。实际上,推广二本质上属于完全背包问题(参考2章节背包问题),即将不同面额的硬币放到总金额为n的包里,求刚好装满包的组合数。
定义dp[i][j]表示前i个硬币组成金额j的组合数。
(1)若第i个硬币面额大于j,则第i个硬币不能放进包中,此时dp[i][j]=dp[i-1][j];
(2)若第i个硬币面额小于等于j,则第i个硬币能放进包中,此时有k+1种选择:不放,放0个,放1个,…放k个,假设放k个硬币i,首先需判断ki是否小于等于j,若等于,此时的组合数应为dp[i-1][j-ki]*1,将以上k+1种选择对应的组合数相加即为dp[i][j]的值,故dp[i][j]=dp[i-1][j]+dp[i][j-coins[i-1]]);
边界值:dp[i][0]=1;dp[0][0]=1;dp[0][1~amount]=0(力扣题目用例测试时要求dp[i][0]=1)。
代码:
public static int coinChange(int[] coins, int amount) {
int res=0;
int[][]dp=new int[coins.length+1][amount+1];
for(int m=0;m<coins.length+1;m++) {
dp[m][0]=1;
}
for(int i=1;i<=coins.length;i++) {
for(int j=1;j<=amount;j++) {
if(j>=coins[i-1]) {
dp[i][j]=dp[i-1][j]+dp[i][j-coins[i-1]];
}
else {
dp[i][j]=dp[i-1][j];
}
}
}
res=dp[coins.length][amount];
return res;
}
2、背包问题
2.1 0-1背包问题
问题描述:
给定int数组weights存放物品重量,int数组values存放物品价值,n存放包的容量,求装入背包的物品的最大价值。每个物品只能放0次或1次。
思路:
定义dp[i][j]表示前 i 件物品放入一个容量为 j 的背包可以获得的最大价值(每件物品最多放一次),则可能有以下两种情况:
(1)第i件物品不能放进包中,即第i件物品的重量大于j【注1】,超重了,此时只能将前i-1件物品放进容量为j的包中,dp[i][j]相当于将前i-1件物品放进容量为j的包中可以获得的最大价值,故有:dp[i][j]=dp[i-1][j];
(2)第i件物品能放进包中,即第i件物品的重量小于等于j,此时有两种选择【注2】,要么放要么不放。若放,此时dp[i][j]相当于将前i-1件物品放进容量为j-w(i)的包中可以获得的最大价值加上第i件物品的价值(w(i)表示第i件物品的重量,v(i)表示第i件物品的价值),故有:dp[i][j]=dp[i-1][j-w(i)]+v(i)。若不放,则有dp[i][j]=dp[i-1][j]。需要取这两种选择中的最大值。即dp[i][j]=max(dp[i-1][j-w(i)]+v(i),dp[i-1][j])。
注1:为什么判断第i件物品的重量与j的关系,而不是前i件物品的重量和与j的关系?因为本质上是看第i件物品能不能放到包里,可以只放第i件物品然后根据状态转移方程求最大价值,不需要考虑前i-1件物品。在纸上举个实例求dp数组的值就能理解了。
注2:因为物品是有重量的,有可能第i件物品重量特别大,而价值特别小,这时候不放第i件物品价值更大。例如包容量为10时,一个物品重量等于10,而价值只有1,其他物品重量为1,而每个物品价值都为10,显然此时不放重量为10的物品能得到的价值更高。
核心算法:
int[][]dp=new int[weights.length+1][n+1];
//边界值
for p<=weights.length:dp[p][0]=0;
for q<=n:dp[0][q]=0;
//状态转移方程
for i=1;i<=weights.length:
for j=1;j<=n:
if weights[i-1]<=j:
dp[i][j]=max(dp[i-1][j-w(i)]+v(i),dp[i-1][j]);
else:
dp[i][j]=dp[i-1][j];
return dp[weights.length][n];
代码:
public static int bags(int[]weights,int[]values,int n) {
int res=0;
int len=weights.length;
int[][]dp=new int[len+1][n+1];
//边界值
for(int p=0;p<=len;p++) {
dp[p][0]=0;
}
for(int q=0;q<=n;q++) {
dp[0][q]=0;
}
//状态转移方程
for(int i=1;i<=len;i++) {
for(int j=1;j<=n;j++) {
if(weights[i-1]<=j) {
dp[i][j]=Math.max(dp[i-1][j], dp[i-1][j-weights[i-1]]+values[i-1]);
}
else {
dp[i][j]=dp[i-1][j];
}
}
}
res=dp[len][n];
return res;
}
2.2 完全背包问题
问题描述:
给定int数组weights存放物品重量,int数组values存放物品价值,n存放包的容量,求装入背包的物品的最大价值。每个物品能放无限次。
思路:
类似于0-1背包问题,区别在于此时的第i件物品并非只有放和不放两种选择,而是有不放、放1件、放2件…放k件共k+1种选择,因此可能有以下两种情况:
(1)第i件物品不能放进包中,即第i件物品的重量大于j,超重了,此时只能将前i-1件物品放进容量为j的包中,dp[i][j]相当于将前i-1件物品放进容量为j的包中可以获得的最大价值,故有:dp[i][j]=dp[i-1][j];
(2)第i件物品能放进包中,即第i件物品的重量小于等于j,此时有k+1种选择:不放、放1件、放2件…放k件。若不放,则有dp[i][j]=dp[i-1][j]。若放,需要判断放k件物品i是否会超重,即比较kw(i)与j的关系,对应的,dp[i][j]=dp[i-1][j-kw(i)]+kv(i)
(w(i)表示第i件物品的重量,v(i)表示第i件物品的价值)。需要取这k+1种选择中的最大值。即dp[i][j]=max(dp[i-1][j],dp[i-1][j-w(i)]+v(i),dp[i-1][j-2w(i)]+2v(i),…dp[i-1][j-kw(i)]+kv(i)) 记为①式。由于dp[i][j-w(i)]=max(dp[i-1][j-w(i)],dp[i-1][j-2w(i)]+v(i),dp[i-1][j-3w(i)]+2v(i),…dp[i-1][j-k*w(i)]+(k-1)*v(i)) 记为②式,将②式代入①式得,dp[i][j]=max(dp[i-1][j],dp[i][j-w(i)]+v(i)),此为优化后的完全背包的状态转移方程。
代码:
public static int bags(int[]weights,int[]values,int n) {
int res=0;
int len=weights.length;
int[][]dp=new int[len+1][n+1];
for(int p=0;p<=len;p++) {
dp[p][0]=0;
}
for(int q=0;q<=n;q++) {
dp[0][q]=0;
}
//
for(int i=1;i<=len;i++) {
for(int j=1;j<=n;j++) {
if(weights[i-1]<=j) {
//与0-1背包问题只有这一行代码的区别
dp[i][j]=Math.max(dp[i-1][j], dp[i][j-weights[i-1]]+values[i-1]);
}
else {
dp[i][j]=dp[i-1][j];
}
}
}
res=dp[len][n];
return res;
}
2.3 背包问题的特殊说明
以上讨论的背包问题默认要求不超过背包容量,直接将所有值初始为0即可,即所有背包一定存在一个合法解:背包中什么都不放时价值为0。它虽然不是最优解,没有满足背包中物品价值最大,但属于一个合法解。
但还存在另一种背包问题,即要求恰好装满背包。此时的合法解必须满足恰好装满背包,否则即使价值最大,也是无效解。显然只有dp[i][0]=0是合法解,其他的dp值必须由合法解推出,因此其他dp值全部赋为无穷大或无穷小。
2.4 背包问题实例–查找充电设备组合(华为机试真题)
问题描述:
给定充电设备数n,最大输出功率maxPower,int数组存放充电设备的功率,输出最优元素。假设任意个充电设备(不需要连续)的功率总和,为p的一个元素,p中最接近最大输出功率的为最优元素(小于等于最大输出功率),不存在最优元素则输出0。示例:输入n=4 ,powers={50 20 10 60} ,maxPower=90,输出:90。
思路:
本题属于0-1背包,相当于将不同价值的物品(功率数组对应价值数组)放进容量为maxPower的包中,求最大价值。定义dp[i][j]表示前i件物品放进容量为j的包中的最大价值,则有:
(1)若powers[i-1]>j,说明第i件物品不能放进包中,此时dp[i][j]=dp[i-1][j];
(2)若powers[i-1]<=j,说明第i件物品能放进包中,此时可选择放或者不放,若不放,dp[i][j]=dp[i-1][j];若放,dp[i][j]=dp[i-1][j-powers[i-1]]+powers[i-1];
边界值默认为0,不需特别处理。
代码:
public static int maxOutputPower(int n,String str,int maxPower) {
int res=0;
int[]powers=new int[n];
String[] st=str.split(" ");
for(int k=0;k<n;k++) {
powers[k]=Integer.parseInt(st[k]);
}
int[][]dp=new int[n+1][maxPower+1];
for(int i=1;i<=n;i++) {//{50 20 10 60} 90
for(int j=1;j<=maxPower;j++) {
if(powers[i-1]>j) {
dp[i][j]=dp[i-1][j];
}
else {
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-powers[i-1]]+powers[i-1]);
}
}
}
res=dp[n][maxPower];
return res;
}
3、零钱问题之最少货币数
问题描述:
给定amount代表金额,coins数组存放零钱,假设每种零钱有无限个,求兑换amount的最少货币数。若无法兑换返回-1。例如:输入:amount=11,coins={1,2,5} 输出:3
解法一:二维dp
思路:
本题可当作完全背包处理,相当于将不同面额的零钱(coins数组对应价值数组)放进容量为amount的包中,求恰好装满的最少货币数。定义dp[i][j]表示前i种货币放进容量j的包中恰好装满的最少货币数,则有:
(1)若coins[i-1]>j,说明第i件物品不能放进包中,此时dp[i][j]=dp[i-1][j];
(2)若coins[i-1]<=j,说明第i件物品能放进包中,此时有k+1种选择,若不放,dp[i][j]=dp[i-1][j];若放k个货币,dp[i][j]=dp[i-1][j-kcoins[i-1]]+1k;需要取这k+1种选择中的最小值,可优化为dp[i][j]=min(dp[i-1][j],dp[i][j-coins[i-1]]]+1)。
由于本题要求恰好装满,因此需要关注边界值。令dp[i][0]=0,其他所有值赋为无穷大。
代码:
public static int coinChange(int[] coins, int amount) {
int res=0;
int[][]dp=new int[coins.length+1][amount+1];
//边界值
// double dmax = Double.POSITIVE_INFINITY; // 1.0 / 0.0, 正无穷大
// int imax=(int)dmax;
for(int k=0;k<=coins.length;k++) {
dp[k][0]=0;
for(int p=1;p<=amount;p++) {
dp[k][p]=0x3f3f3f3f;
}
}
//状态转移方程
for(int i=1;i<=coins.length;i++) {
for(int j=1;j<=amount;j++) {
if(coins[i-1]>j) {
dp[i][j]=dp[i-1][j];
}
else {
dp[i][j]=Math.min(dp[i-1][j], dp[i][j-coins[i-1]]+1);
}
}
}
if(dp[coins.length][amount]>amount) {
res=-1;
}
else {
res=dp[coins.length][amount];
}
return res;
}
解法二:二维转一维dp(未更新,先不管)
思路:
把二维dp优化为一维dp。如何优化?????有时间再研究,目前掌握思路一即可。二维转一维清晰详解
4、最长递增子序列(LIS)
力扣链接
问题描述:
给定一个整数数组 nums(长度大于等于1) ,找到其中最长严格递增子序列的长度。示例:输入:nums = [10,9,2,5,3,7,101,18];输出:4。解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
解法一:动态规划
思路:
本题可转化为求nums的严格递增子序列长度的最大值。定义dp[i]表示以nums[i]结尾的LIS的长度,需要注意的是,以nums[i]结尾的LIS未必是前i个元素组成的数组的LIS,例如{4,10,4,3,8},以3结尾的LIS={3},但0~3下标的元素所组成的数组的LIS={4,10},因此需要将以数组中每个元素结尾的LIS的长度都求出来,然后取最大值。要求以nums[i]结尾的LIS的长度,需要将nums[i]与前面i-1个元素结尾的LIS进行比较:
(1)若nums[i]比前面的某个元素nums[j]大,说明nums[i]能添加进nums[j]结尾的LIS中,此时dp[i]=dp[j]+1。dp[i]要取满足此条件的最大值。
(2)若nums[i]小于等于前面的所有元素时,说明nums[i]结尾的LIS中只有这一个元素,此时dp[i]=1;
边界值:dp[1]=1。
核心算法:
int[]dp=new int[nums.length];
dp[0]=1;
for i=1;i<nums.length:
dp[i]=1;
for j=0;j<i:
//若不执行以下的if语句,说明nums[i]不能加入到任何以nums[j]结尾的LIS中
//即以nums[i]结尾的LIS只有一个元素,长度为1
if nums[i]>nums[j]:dp[i]=max(dp[i],dp[i-1]+1);
return dp数组的最大值;
代码:
public static int lengthOfLIS(int[] nums) {
int res=0;
int[]dp=new int[nums.length];
dp[0]=1;
for(int i=1;i<nums.length;i++) {
dp[i]=1;
for(int j=0;j<i;j++) {
if(nums[i]>nums[j]) {
dp[i]=Math.max(dp[i], dp[j]+1);
}
}
}
for(int k=0;k<dp.length;k++) {
if(dp[k]>res) {
res=dp[k];
}
}
return res;
}
解法二:二分查找优化时间复杂度(未更新,先不管)
思路:
代码:
LIS推广–输出LIS
问题描述:
输出LIS,若有多个,输出字典序最小的。示例:输入:[1,2,8,6,4];输出:[1,2,4];说明:其最长递增子序列有3个,(1,2,8)、(1,2,6)、(1,2,4)其中第三个 按数值进行比较的字典序 最小,故答案为(1,2,4)。
思路:
先按求LIS长度的步骤赋值dp数组,然后遍历dp数组获取最大值及对应下标index,然后从nums数组index处从后往前遍历,找到j使得nums[j]<nums[index]且dp[index]=dp[j]+1,nums[j]即为倒数第二个数,重复此操作。
代码:
public static ArrayList<Integer> printLIS(int[] nums) {
int res=0;
int[]dp=new int[nums.length];
//更新dp数组的值
dp[0]=1;
for(int i=1;i<nums.length;i++) {
dp[i]=1;
for(int j=0;j<i;j++) {
if(nums[i]>nums[j]) {
dp[i]=Math.max(dp[i], dp[j]+1);
}
}
}
//获取dp数组中最大值所在下标
int index=0;
for(int k=0;k<dp.length;k++) {
if(dp[k]>res) {
res=dp[k];
index=k;
}
}
//获取LIS,虽然不知道为什么,但是好像自动按字典序输出了?
ArrayList<Integer> arrLIS=new ArrayList<Integer>();
arrLIS.add(nums[index]);//添加LIS的最后一个元素
for(int r=index-1;r>=0;r--) {
if(nums[r]<nums[index]&&dp[index]==dp[r]+1) {
arrLIS.add(0, nums[r]);
index=r;
}
}
return arrLIS;
}
5、最长公共子序列(LCS)
力扣链接
问题描述:
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。示例:输入:text1 = “abcde”, text2 = “ace” ;输出:3 。
思路:
涉及两个字符串/数组的,一般考虑二维dp。定义dp[i][j]表示长度为i的text1与长度为j的text2的LCS的长度。
(1)当text1[i-1]=text2[j-1]时,此时长度为i的text1与长度为j的text2的最后一个元素相同,因此其LCS的长度等于长度为i-1的text1与长度为j-1的text2的LCS的长度加1,即dp[i][j]=dp[i-1][j-1]+1;
(2)当text1[i-1]不等于text2[j-1]时,此时有两种选择:要么求长度为i-1的text1与长度为j的text2的LCS的长度,要么求长度为i的text1与长度为j-1的text2的LCS的长度,取这两种选择的较大值,即dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
边界值:dp[i][0]=0,dp[0][j]=0。
核心算法:
int[][]dp=new int[text1.length+1][text2.length+1];
dp[i][0]=0;
dp[0][j]=0;
for i=1;i<=text1.length:
for j=1;j<=text2.length:
if text1[i-1]==text2[j-1]:dp[i][j]=dp[i-1][j-1]+1;
else:dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
return dp[text1.length][text2.length];
代码:
public static int longestCommonSubsequence(String text1, String text2) {
int res=0;
int[][]dp=new int[text1.length()+1][text2.length()+1];
//步骤一:赋值边界值
for(int m=0;m<=text1.length();m++) {
dp[m][0]=0;
}
for(int n=0;n<=text2.length();n++) {
dp[0][n]=0;
}
//步骤二:状态转移方程
for(int i=1;i<=text1.length();i++) {
String str1=text1.substring(i-1,i);
for(int j=1;j<=text2.length();j++) {
String str2=text2.substring(j-1,j);
if(str1.equals(str2)) {
dp[i][j]=dp[i-1][j-1]+1;
}
else {
dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]);
}
}
}
res=dp[text1.length()][text2.length()];
return res;
}
6、最长公共子串
问题描述:
给定两个字符串str1和str2,输出两个字符串的最长公共子串 题目保证str1和str2的最长公共子串存在且唯一。示例:输入:abccb acbcb 输出:bc。注意:子序列不要求连续,子串要求连续,也就是说子串比子序列要求更严格,子串是特殊的子序列。
思路:
当无法确定状态转移方程时,可以举一个具体的例子求dp数组的值。
本题可综合LIS和LCS的求解思路来思考,定义dp[i][j]表示长度为i的text1与长度为j的text2且以nums[i-1]和nums[j-1]为结尾的最长公共子串的长度。因此nums[i-1]和nums[j-1]必须要相等,若不相等则dp[i][j]=0。例如ab和acb,以b结尾的最长公共子串即为b,长度为1;而ab和ac,由于b和c不相等,因此不存在以b和c结尾的最长公共子串,长度为0。故本题的状态转移方程为:
(1)若nums[i-1]=nums[j-1],则dp[i][j]=dp[i-1][j-1]+1;
(2)若nums[i-1]不等于nums[j-1],则dp[i][j]=0。
边界值:dp[i][0]=0,dp[0][j]=0。
核心算法:
int[][]dp=new int[text1.length+1][text2.length+1];
dp[i][0]=0;
dp[0][j]=0;
for i=1;i<=text1.length:
for j=1;j<=text2.length:
if text1[i-1]==text2[j-1]:dp[i][j]=dp[i-1][j-1]+1;
else:dp[i][j]=0;
return dp的最大值;
代码:
public static int longestCommonSubstring(String text1, String text2) {
int res=0;
int[][]dp=new int[text1.length()+1][text2.length()+1];
//步骤一:赋值边界值
for(int m=0;m<=text1.length();m++) {
dp[m][0]=0;
}
for(int n=0;n<=text2.length();n++) {
dp[0][n]=0;
}
//步骤二:状态转移方程
for(int i=1;i<=text1.length();i++) {
String str1=text1.substring(i-1,i);
for(int j=1;j<=text2.length();j++) {
String str2=text2.substring(j-1,j);
if(str1.equals(str2)) {
dp[i][j]=dp[i-1][j-1]+1;
}
else {
dp[i][j]=0;
}
}
}
//步骤三:返回dp数组的最大值
for(int s1=0;s1<dp.length;s1++) {
for(int s2=0;s2<dp.length;s2++) {
if(dp[s1][s2]>res) {
res=dp[s1][s2];
}
}
}
return res;
}
7、最大子数组和
力扣链接
问题描述:
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。示例:输入:nums = [-2,1,-3,4,-1,2,1,-5,4];输出:6;解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
思路:
本题涉及子数组,可参考最大回文子串的做法。定义dp[i][j]表示数组从i到j下标的元素和,求出dp的所有值后,找到其中的最大值,再返回对应的子数组。
手动求dp数组可发现:dp[i][j]=dp[i][j-1]+nums[j]。
边界值:L=1时,dp[i][j]=nums[i];
核心算法:
int[][]dp=new int[nums.length][nums.length];
L=1时:dp[i][j]=nums[i];
for L=2;L<=nums.length:
for i=0;i<nums.length:
j=L+i-1;
if j<nums.length:dp[i][j]=dp[i][j-1]+nums[j];
思路:
上述算法运行时超出空间限制,需要对空间优化。参考LIS的做法。定义dp[i]表示以nums[i]结尾的最大连续和数组的和,求出dp的所有值后,输出其中的最大值。
在求LIS时,需要判断元素i能否加入dp[j]对应的LIS中,若能加入,dp[j]对应的LIS长度加1,得到dp[i]对应的LIS长度;若不能加入,元素i自身作为自己的LIS,长度为1。
本题同理,不同的是j只能取i-1(本题要求连续,LIS不要求连续),判断元素i能否加入dp[i-1]对应的最大连续和数组中,若元素i的值大于dp[i-1]的值,说明不需要加入,元素i单独作为一个子数组即可使和最大,dp[i]=nums[i];若元素i的值小于等于dp[i-1]的值,说明需要将i加入dp[i-1]对应的最大连续和数组中,才能使以nums[i]结尾的最大连续和数组的和最大,dp[i]=dp[i-1]+nums[i]。故有状态转移方程:
(1)若nums[i]>dp[i-1],dp[i]=nums[i];
(2)若nums[i]<=dp[i-1],dp[i]=dp[i-1]+nums[i]。
以上只考虑了nums[i]大于0且dp[i-1]小于0的情况,不全面,不能通过全部用例测试。实际上,dp[i]的取值只有两种情况,要么加入元素i,要么不加,只需取这两种选择中的最大值最为dp[i]的值即可。故有状态转移方程:dp[i]=max(nums[i],dp[i-1]+nums[i])。
边界值:dp[0]=nums[0]。
注:涉及到求最值的动态规划问题,注意考虑是否需要赋值无穷大或无穷小,或者赋最大值+1/最小值-1。
代码:
public static int maxSubArray(int[]nums){
Arrays.sort(nums);
int res=nums[0]-1;//不能简单赋为0,否则当nums中为负数时,就会出现错误结果
int[]dp=new int[nums.length];
dp[0]=nums[0];
for(int i=1;i<nums.length;i++) {
//以下的if else语句只讨论了nums[i]大于0且dp[i-1]小于0的情况,不全面,不能通过全部用例测试
// if(nums[i]>dp[i-1]) {
// dp[i]=nums[i];
// }
// else {
// dp[i]=dp[i-1]+nums[i];
// }
dp[i]=Math.max(nums[i],dp[i-1]+nums[i]);
}
for(int k=0;k<dp.length;k++) {
if(dp[k]>res) {
res=dp[k];
}
}
return res;
}
8、最长无重复子串
问题描述:
给定一个字符串 s (长度可以为0),请你找出其中不含有重复字符的 最长子串 的长度。示例:输入: s = “abcabcbb”;输出: 3 ;解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
解法一:动态规划
思路:
定义dp[i]表示以元素i结尾的最长无重复子串的长度,因为子串要求连续,因此只需比较元素i与元素i-1对应的最长无重复子串即可。则有:
代码:
public static int lengthOfLongestSubstring(String s) {
if(s.length()==0) {
return 0;
}
else {
int res=0;
int[]dp=new int[s.length()];
dp[0]=1;
int idx=0;//存放以nums[i]结尾的最长无重复子串的首个元素下标
for(int i=1;i<s.length();i++) {
String subStr=s.substring(idx,i);//存放以nums[i-1]结尾的最长无重复子串
int idx_i=subStr.indexOf(s.substring(i,i+1));//判断第i个元素是否在以nums[i-1]结尾的最长无重复子串中
if(idx_i==-1) {//未找到下标,说明不在其中
dp[i]=dp[i-1]+1;
}
else {
dp[i]=subStr.length()-idx_i;
idx=i-dp[i]+1;
}
}
for(int j=0;j<dp.length;j++) {
if(dp[j]>res) {
res=dp[j];
}
}
return res;
}
}
解法二:滑动窗口
思路:
给定一个字符串 s (长度可以为0),请你找出其中不含有重复字符的 最长子串 的长度。示例:输入: s = “abcabcbb”;输出: 3 ;解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
滑动窗口左边界值和右边界值初始值都为0,遍历右边界,直到右边界到达字符串的最后一个元素。
代码:
public static int lengthOfLongestSubstring(String s) {
if(s.length()==0) {
return 0;
}
else {
HashMap<Character, Integer> hm=new HashMap<Character, Integer>();//存放字符及下标
int left=0;
int temp=0;//存放不重复子串的最大长度
for(int right=0;right<s.length();right++) {
if(hm.containsKey(s.charAt(right))==false) {
hm.put(s.charAt(right), right);//若不在hm中,则将这个字符和对应下标添加到hm
temp=Math.max(right-left+1, temp);
}
else {//若hm已有此字符,则更新left
left=Math.max(left,hm.get(s.charAt(right))+1);//先更新left,再更新hm,否则left的值有误
//例如abba,当遍历至最后一个a时,left=hm.get(a)+1=1,但实际上此时left=2,因此取最大值
hm.put(s.charAt(right), right);
// left=Math.max(left,hm.get(s.charAt(right))+1);
// //例如abba,当遍历至最后一个a时,left=hm.get(a)+1=1,但实际上此时left=2,因此取最大值
temp=Math.max(temp, right-left+1);
}
}
return temp;
}
9、最长回文子串
力扣链接
问题描述:
输入字符串s,输出s的最长回文子串。s的长度>=1,s仅由数字和英文字母组成。例如cbbd,输出bb;babad,输出bab或aba。
思路:
先考虑问题能否拆分为重复的子问题,如何拆分?
本题根据回文串性质:如果一个长度大于2的字符串是回文串,那么它去掉首尾两个字母所得的字符串也是回文串。定义dp[i][j]表示下标i到j的子串是否为回文串。求出dp数组后,获取所有值为true的i,j值,即可求出该回文子串的长度,取长度最大值并输出回文子串。状态转移方程如下:
(1)若s[i-1]==s[j+1],则dp[i][j]=dp[i-1][j+1];
(2)若s[i-1]不等于s[j+1],则dp[i][j]=false;
以上状态转移方程是不合理的,测试时发现无论如何遍历,始终无法求出dp[0][j]的值。实际上,应该将dp[i][j]对应的子串去掉首尾两个元素,即比较s[i]和s[j]的值是否相等。例如abbc,要求dp[0][3]的值,去掉首尾元素得到bb,即dp[1][2],若s[0]等于s[3],则dp[0][3]=dp[1][2]。因此正确的状态转移方程为:
(1)若s[i]==s[j],则dp[i][j]=dp[i+1][j-1];
(2)若s[i]不等于s[j],则dp[i][j]=false;
边界值:dp[0][0]=true;若j<i,则dp[i][j]=false;若i=j,则dp[i][j]=true。
特别地,本题的遍历顺序不同于上文中的题,如果按下文“错误示范”遍历发现,当获取dp数组i行的值时,需要用到i+1行的值,而i+1行的值还没有求出来。此种遍历方式是不合理的。因此考虑遍历回文子串的长度。见核心算法。
错误示范:
for i=0;i<s.length:
for j=i+1;j<s.length:
if s[i]==s[j]:dp[i][j]=dp[i+1][j-1];
else:dp[i][j]=false;
核心算法:
int[][]dp=new int[s.length][s.length];
if i==j:dp[i][j]=true;//L=1时dp值为true
else:dp[i][j]=false;
for L=2;L<=s.length:
for i=0;i<s.length:
j=L+i-1;
if j<=s.length-1:
if s[i]==s[j]:
if L==2:dp[i][j]=true;
else:dp[i][j]=dp[i+1][j-1];
else:break;
遍历dp:
if dp[i][j]==true:
temp=j-1+1;
if temp>maxLen:
maxLen=temp;startIndex=i;endIndex=j;
return s.substring(i,j+1);//含左不含右
代码:
public static String longestPalindrome(String s) {
String resStr="";
//步骤一:赋初值,子串长度为1时,必为true,其他默认赋为false
Boolean[][] dp=new Boolean[s.length()][s.length()];
for(int m=0;m<s.length();m++) {
for(int n=0;n<s.length();n++) {
if(m==n) {
dp[m][n]=true;
}
else {
dp[m][n]=false;
}
}
}
//步骤二:状态转移方程,赋值dp
char[] ch=s.toCharArray();
for(int L=2;L<=ch.length;L++) {
for(int i=0;i<ch.length;i++) {
int j=L+i-1;
if(j>ch.length-1) {
break;
}
else {
if(ch[i]==ch[j]) {
if(L==2) {
dp[i][j]=true;
}
else {
dp[i][j]=dp[i+1][j-1];
}
}
else {
dp[i][j]=false;
}
}
}
}
//步骤三:找到dp值为true,且长度最大的子串
int resLen=1;
int firstIndex=0;
int lastIndex=0;
for(int p=0;p<ch.length;p++) {
for(int q=0;q<ch.length;q++) {
if(dp[p][q]==true) {
int tempLen=q-p+1;
if(tempLen>resLen) {
resLen=tempLen;
firstIndex=p;
lastIndex=q;
}
}
}
}
resStr=s.substring(firstIndex,lastIndex+1);
return resStr;
}
10、求路径
题型一:最小路径和
问题描述:
给定一个包含非负整数的 m x n 网格 grid (m,n>=1),请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。说明:每次只能向下或者向右移动一步。示例:输入:grid = [[1,3,1],[1,5,1],[4,2,1]];输出:7;解释:因为路径 1→3→1→1→1 的总和最小。
思路:
定义dp[i][j]表示到达grid[i][j]位置时的最小路径和,输出dp[m-1][n-1]即为所求答案。显然,要到达grid[i][j]处,有两种选择,要么从grid[i-1][j]处向下走一步到达,要么从grid[i][j-1]处向右走一步到达。取这两种选择的最小值。故有状态转移方程:dp[i][j]=min(dp[i-1][j]+grid[i][j],dp[i][j-1]+grid[i][j])。
边界值:dp[0][0]=grid[0][0],dp[i][0]=dp[i-1][0]+grid[i][0],dp[0][j]=dp[0][j-1]+grid[0][j]。
核心算法:
int[][]dp=new int[m][n];
dp[0][0]=grid[0][0];
i>0时,dp[i][0]=dp[i-1][0]+grid[i][0];
j>0时,dp[0][j]=dp[0][j-1]+grid[0][j];
for i=1;i<m:
for j=1;j<n:
dp[i][j]=min(dp[i-1][j]+grid[i][j],dp[i][j-1]+grid[i][j]);
return dp[m-1][n-1];
代码:
public static int minPathSum(int[][]grid) {
int res=0;
int[][]dp=new int[grid.length][grid[0].length];
dp[0][0]=grid[0][0];
for(int m=1;m<grid.length;m++) {
dp[m][0]=dp[m-1][0]+grid[m][0];
}
for(int n=1;n<grid[0].length;n++) {
dp[0][n]=dp[0][n-1]+grid[0][n];
}
for(int i=1;i<grid.length;i++) {
for(int j=1;j<grid[0].length;j++) {
dp[i][j]=Math.min(dp[i-1][j]+grid[i][j], dp[i][j-1]+grid[i][j]);
}
}
res=dp[grid.length-1][grid[0].length-1];
return res;
}
题型二:不同路径数
力扣链接
问题描述:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?示例:输入:m = 3, n = 2;输出:3;解释:从左上角开始,总共有 3 条路径可以到达右下角:向右 -> 向下 -> 向下;向下 -> 向下 -> 向右;向下 -> 向右 -> 向下。
思路:
本质上和爬楼梯问题是一样的,区别在于爬楼梯问题是一维dp,本题是二维dp问题。定义dp[i][j]表示到达i,j位置时的不同路径数,输出dp[m-1][n-1]即为所求答案。显然,要到达i,j处,有两种选择,要么从i-1,j处向下走一步到达,要么从i,j-1处向右走一步到达。将这两种选择对应的路径数相加即为到达i,j位置时的不同路径数。故有状态转移方程:dp[i][j]=dp[i-1][j]+dp[i][j-1]。
边界值:dp[0][j]=1;dp[i][0]=1。
核心算法:
int[][]dp=new int[m][n];
dp[0][j]=1;
dp[i][0]=1;
for i=1;i<m:
for j=1;j<n:
dp[i][j]=dp[i-1][j]+dp[i][j-1];
return dp[m-1][n-1];
代码:
public static int uniquePaths(int m,int n) {
int res=0;
int[][]dp=new int[m][n];
for(int k1=0;k1<m;k1++) {
dp[k1][0]=1;
}
for(int k2=0;k2<n;k2++) {
dp[0][k2]=1;
}
for(int i=1;i<m;i++) {
for(int j=1;j<n;j++) {
dp[i][j]=dp[i-1][j]+ dp[i][j-1];
}
}
res=dp[m-1][n-1];
return res;
}
11、最大正方形
问题描述:
在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。示例:输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]];输出:4。
思路:
定义dp[i][j]表示以matrix[i][j]为右下角的只含1的正方形的最大边长,求出dp数组后,其中的最大值即为所求最大正方形的边长,输出其平方即为最大面积,故有状态转移方程:
(1)当matrix[i][j]=0时,不存在这样的正方形,此时dp[i][j]=0;
(2)当matrix[i][j]=1时,此时dp[i][j]与dp[i-1][j]、dp[i][j-1]、dp[i-1][j-1]三个值有关。以下列表格为例,dp[2][2]表示以matrix[2][2]为右下角的只含1的正方形的最大边长。由于matrix[2][2]=1,matrix[1][2]=1,matrix[2][1]=1,matrix[1][1]=0,观察表格可得:dp[2][2]=1、dp[1][2]=1、dp[2][1]=1、dp[1][1]=0。显然dp[2][2]的值等于dp[1][2]、dp[2][1]、dp[1][1]中的最小值加1。即dp[i][j]=min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1;
- | - | - | - | - |
---|---|---|---|---|
1 | 0 | 1 | 0 | 0 |
1 | 0 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 |
1 | 0 | 0 | 1 | 0 |
边界值:由状态转移方程可知,i,j的值从1开始,因此边界值为dp[i][0]、dp[0][j],直接判断对应的矩阵元素是否为0即可,若为0,则对应dp值为0,否则,对应dp值为1。
核心算法:
int[][]dp=new int[matrix.length][matrix[0].length];
if matrix[i][0]==0:dp[i][0]=0;
if matrix[i][0]==1:dp[i][0]=1;
if matrix[0][j]==0:dp[0][j]=0;
if matrix[0][j]==1:dp[0][j]=1;
for i=1;i<matrix.length:
for j=1;j<matrix[0].length:
if matrix[i][j]=0:dp[i][j]=0;
else:dp[i][j]=min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1;
return dp的最大值的平方;
代码:
public static int maximalSquare(char[][] matrix) {
int res=0;
int[][]dp=new int[matrix.length][matrix[0].length];
//赋值边界值
for(int m=0;m<matrix.length;m++) {
if(matrix[m][0]=='0') {
dp[m][0]=0;
}
else {
dp[m][0]=1;
}
}
for(int n=0;n<matrix[0].length;n++) {
if(matrix[0][n]=='0') {
dp[0][n]=0;
}
else {
dp[0][n]=1;
}
}
//更新dp数组
for(int i=1;i<matrix.length;i++) {
for(int j=1;j<matrix[0].length;j++) {
if(matrix[i][j]=='0') {
dp[i][j]=0;
}
else {
dp[i][j]=Math.min(dp[i-1][j],Math.min(dp[i][j-1],dp[i-1][j-1]))+1;
}
}
}
//求dp数组最大值
for(int m=0;m<matrix.length;m++) {
for(int n=0;n<matrix[0].length;n++) {
if(dp[m][n]>res) {
res=dp[m][n];
}
}
}
return res*res;
}
扩展—DFS求岛屿的最大面积(待更新,先不管)
力扣链接
问题描述:
思路:
12、字符串排列
问题描述:
给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。换句话说,s1 的排列之一是 s2 的 子串 。示例:输入:s1 = “ab” s2 = “eidbaooo”;输出:true;解释:s2 包含 s1 的排列之一 (“ba”)。
思路:
暴力法:直接将s1和s2排序,然后判断s1在不在s2中。(此方法不能完全通过用例测试,通过比例为72/108,例如s1= “ab” s2 = "eidboaoo"预期输出false,实际输出true)。
暴力法优化:将s1和s2中所有长度为s1.len的子串排序并比较,只要存在相等,就说明符合题意。见代码中方法一优化。
动态规划:利用滑动窗口解题,下一次比较时:将右边界向右移一位,窗口中需要添加right+1下标处的值;将左边界向右移一位,窗口中需要去掉left下标处的值。
代码:
//方法二:动态规划
//不能用hashmap,否则还要讨论hashmap中值的顺序,不如直接用数组,将26个字母的位置都预设出来,按字母顺序添加元素
public static boolean checkInclusion(String s1,String s2) {
boolean bl=false;
int len=s1.length();
if(s1.length()<=s2.length()) {
int[] s1Count=countChar(s1);//存放26个字母出现的次数
int left=0;//滑动窗口左边界初始值
String subStr=s2.substring(left,len-1);
int[]s2SubstrCount=countChar(subStr);//初始值只统计前len-1个字符,right下标处的值进入循环后再统计
for(int right=len-1;right<s2.length();) {
s2SubstrCount[Integer.valueOf(s2.charAt(right))]++;//right下标处的字符出现次数加1
if(Arrays.equals(s1Count, s2SubstrCount)) {//判断数组是否相等
bl=true;
break;
}
else {
s2SubstrCount[Integer.valueOf(s2.charAt(left))]--;//left下标处的字符出现次数减1
left++;
right++;
}
}
//若循环结束都没能令bl=true,说明不满足题意,返回false
}
return bl;
}
//统计字符串中字母次数并存放到数组中
public static int[] countChar(String s) {
int[] countChar=new int[123];//97-122,直接用ascii值对应的下标存放出现次数
for(int i=0;i<s.length();i++) {
char ch=s.charAt(i);//i下标处的字符
countChar[Integer.valueOf(ch)]++;
//countChar[Integer.valueOf(ch)]=countChar[Integer.valueOf(ch)]+1;
}
return countChar;
}
// //方法一:s1= "ab" s2 = "eidboaoo" 通过用例72/108
// public static boolean checkInclusion(String s1,String s2) {
// boolean bl=false;
// char[] s1Arr=s1.toCharArray();
// Arrays.sort(s1Arr);
// String s1Sorted=new String(s1Arr);
// char[] s2Arr=s2.toCharArray();
// Arrays.sort(s2Arr);
// String s2Sorted=new String(s2Arr);
// if(s2Sorted.contains(s1Sorted)) {
// bl=true;
// }
// return bl;
// }
// //方法一优化,通过全部用例
// public static boolean checkInclusion(String s1,String s2) {
// boolean bl=false;
// char[] s1Arr=s1.toCharArray();
// Arrays.sort(s1Arr);
// String s1Sorted=new String(s1Arr);
// int len=s1.length();
// if(s1.length()<=s2.length()) {//若s1比s2长,返回false
// for(int i=0;i<s2.length();i++) {
// int j=i+len-1;
// if(j<s2.length()) {//保证下标不越界
// String subStr=s2.substring(i,j+1);//含左不含右
// char[] sArr=subStr.toCharArray();
// Arrays.sort(sArr);
// String sSorted=new String(sArr);
// if(sSorted.equals(s1Sorted)) {
// bl=true;
// break;
// }
// }
// }
// }
// return bl;
// }
13、编辑距离(待更新)
力扣链接
问题描述:
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。你可以对一个单词进行如下三种操作:插入一个字符;删除一个字符;替换一个字符。示例:输入:word1 = “horse”, word2 = “ros”;输出:3;解释:horse -> rorse (将 ‘h’ 替换为 ‘r’);rorse -> rose (删除 ‘r’);rose -> ros (删除 ‘e’)。
思路:
核心算法:
在这里插入代码片
代码:(抄的)
public static int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();//目标单词
int dp[][] = new int[m + 1][n + 1];
//初始化
for(int i = 1; i <= n; i++) {
dp[0][i] = i;
}
for(int i = 1; i <= m; i++) {
dp[i][0] = i;
}
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
if(word1.charAt(j - 1) == word2.charAt(i - 1)) {
dp[i][j] = dp[i - 1][j - 1];
}else{//替换、插入、删除,取最小值
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
}
}
}
return dp[m][n];
}
14、股票问题
股票系列力扣题解-常规版—不太好理解不看了
股票系列力扣题解-小故事版
题型一:买卖股票的最佳时机Ⅰ
问题描述:
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。示例:输入:[7,1,5,3,6,4];输出:5;解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
思路:
本题实际上不需要用到动态规划,只要双重循环遍历prices数组,求出prices[j]-prices[i]的最大值即可(j>i,且prices[j]应大于prices[i])。但为了解决更高难度的股票问题,还是先利用动态规划思想解决本题。
定义dp[i]表示第i天卖出股票获取的最大利润。i只能从2开始。要求出dp[i],需要找到prices[i]之前的元素中最小且小于prices[i]的,作为买入价格,若找不到,说明无法卖出,dp[i]=0;若能找到,dp[i]=prices[i]-最小值。最终输出dp数组的最大值,即为所求答案。
边界值:dp[0]=0;dp[1]=0;
核心算法:
int[]dp=new int[prices.length];
dp[0]=0;
temp=正无穷大;
for i=1;i<prices.length:
temp=min(temp,prices[i-1]);
if temp<=prices[i]:dp[i]=prices[i]-temp;
else:dp[0]=0;
return dp的最大值;
代码:
public static int maxProfit(int[] prices) {
int res=0;
int[]dp=new int[prices.length];
dp[0]=0;
int temp=0x3f3f3f3f;
for(int i=1;i<prices.length;i++) {
temp=Math.min(temp, prices[i-1]);//存放最小值
if(temp<=prices[i]) {
dp[i]=prices[i]-temp;
}
else {
dp[i]=0;
}
}
for(int k=0;k<prices.length;k++) {
res=Math.max(res, dp[k]);
}
return res;
}
题型二:买卖股票的最佳时机Ⅱ
力扣链接
问题描述:
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。在每一天,你可以决定是否购买和/或出售股票。最多只能持有一股股票。你也可以先购买,然后在同一天出售。返回 你能获得的 最大 利润 。示例:输入: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 。
解法一:贪心算法
思路:
与题型一的区别是,题型一只有一次买入卖出机会,题型二有多次买入卖出机会。
考虑贪心算法:每次涨的前一天买入,买入的第二天卖出,这样所有涨的利润都能得到。
注:这道题 「贪心」 的地方在于,对于 「今天的股价 - 昨天的股价」,得到的结果有 3 种可能:① 正数,② 0,③负数。贪心算法的决策是: 只加正数 。
代码:
//不用动态规划,使用贪心算法
public static int maxProfit(int[] prices) {
int res=0;
for(int i=1;i<prices.length;i++) {
if(prices[i]>=prices[i-1]) {
res=res+prices[i]-prices[i-1];
}
}
return res;
}
解法二:动态规划
思路:
首先明确第i天的状态:要么有股票,要么没有股票。定义dp[i][j]表示第i天持有股票状态为j时的最大收益。j只有两个取值,j取0时表示不持有股票,j取1时表示持有股票。最终输出dp[prices.length-1][0]。若dp[prices.length-1][0]<0,则输出0。 (说明:没加这个限制条件也能输出0,暂时还不清楚原理,先放着,以后再看)
对于第i天的两种状态:
(1)持有股票(j=1):
------a.持有股票的状态是“今天买入”导致的,此时dp[i][1]=dp[i-1][0]-prices[i]。说明:因为本题可多次交易,现在不清楚今天以前是否完成过买入卖出操作,因此要用昨天不持有股票的最大收益减去今天买股票花的钱。
------b.持有股票的状态是昨天延续过来的,因此今天不能进行任何操作(若卖出则不符合持有股票的状态,若买入则对应a),今天持有股票的最大收益和昨天持有股票的最大收益相同,此时dp[i][1]=dp[i-1][1]。
dp[i][1]取a,b两种情况的最大值。
(2)不持有股票(j=0):
------a.不持有股票的状态是“今天卖出”导致的,此时dp[i][0]=dp[i-1][1]+prices[i]。说明:因为本题可多次交易,现在不清楚今天以前是否完成过买入卖出操作,因此要用昨天持有股票的最大收益加上今天卖出股票得到的钱。
------b.不持有股票的状态是昨天延续过来的,因此今天不能进行任何操作(若买入则不符合不持有股票的状态,若卖出则对应a),今天不持有股票的最大收益和昨天不持有股票的最大收益相同,此时dp[i][0]=dp[i-1][0]。
dp[i][0]取a,b两种情况的最大值。
边界值:dp[0][0]=0;dp[0][1]=0-prices[0];
代码:
public static int maxProfit(int[] prices) {
int res=0;
int[][]dp=new int[prices.length][2];
dp[0][0]=0;
dp[0][1]=0-prices[0];
for(int i=1;i<prices.length;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]);
}
res=dp[prices.length-1][0];
return res;
}
题型三:买卖股票的最佳时机Ⅲ
力扣链接
问题描述:
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成两笔交易,且不能同时参与多笔交易(必须在再次购买前出售掉之前的股票)。示例:输入:prices = [3,3,5,0,0,3,1,4];输出:6;解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
思路:
题型一有1次买入卖出机会;题型二有无限次买入卖出机会;题型三有至多2次买入卖出机会。
首先明确第i天的状态:无操作,第一次买入,第一次卖出,第二次买入,第二次卖出(说明:题型二不限制交易次数,因此第几次交易不重要,可以直接分为持有股票和不持有股票两种状态。而本题多了一个约束条件“最多交易两次”,因此需要考虑交易次数)。定义dp[i][j]表示第i天持有股票状态为j时的最大收益。j取0时表示无操作,j取1时表示第一次买入,j取2时表示第一次卖出,j取3时表示第二次买入,j取4时表示第二次卖出。最终输出dp[prices.length-1][4]。若dp[prices.length-1][4]<0,则输出0。
对于第i天的五种状态:
(1)j=0时:如果无操作,则收益始终为0,即dp[i][0]=0;(说明:这个状态没什么用,写程序的时候可以不操作,保留初始值。)
(2)j=1时:
------a.第一次买入的状态是“今天买入”导致的,此时dp[i][1]=0-prices[i]。说明:因为是第一次买入,所以之前的收益一定是0,因此要用0减去今天买股票花的钱。
------b.第一次买入的状态是昨天延续过来的,此时dp[i][1]=dp[i-1][1]。
dp[i][1]取a,b两种情况的最大值。
(3)j=2时:
------a.第一次卖出的状态是“今天卖出”导致的,此时dp[i][2]=dp[i-1][1]+prices[i]。说明:因为是第一次卖出,所以要用昨天“第一次买入”状态的最大收益加上今天卖出股票得到的钱。
------b.第一次卖出的状态是昨天延续过来的,此时dp[i][2]=dp[i-1][2]。
dp[i][2]取a,b两种情况的最大值。
(4)j=3时:
------a.第二次买入的状态是“今天买入”导致的,此时dp[i][3]=dp[i-1][2]-prices[i]。说明:因为是第二次买入,说明之前完成了一次买入卖出交易,因此要用昨天“第一次卖出”状态的最大收益减去今天买股票花的钱。
------b.第二次买入的状态是昨天延续过来的,此时dp[i][3]=dp[i-1][3]。
dp[i][3]取a,b两种情况的最大值。
(5)j=4时:
------a.第二次卖出的状态是“今天卖出”导致的,此时dp[i][4]=dp[i-1][3]+prices[i]。说明:因为是第二次卖出,所以要用昨天“第二次买入”状态的最大收益加上今天卖出股票得到的钱。
------b.第二次卖出的状态是昨天延续过来的,此时dp[i][4]=dp[i-1][4]。
dp[i][4]取a,b两种情况的最大值。
边界值:dp[0][1]=0-prices[0];dp[0][2]=0;dp[0][3]=0-prices[0];dp[0][4]=0;
代码:
public static int maxProfit(int[] prices) {
int res=0;
int[][]dp=new int[prices.length][5];
dp[0][0]=0;
dp[0][1]=0-prices[0];
dp[0][2]=0;
dp[0][3]=0-prices[0];
dp[0][4]=0;
for(int i=1;i<prices.length;i++) {
dp[i][1]=Math.max(dp[i-1][1], 0-prices[i]);
dp[i][2]=Math.max(dp[i-1][2], dp[i-1][1]+prices[i]);
dp[i][3]=Math.max(dp[i-1][3], dp[i-1][2]-prices[i]);
dp[i][4]=Math.max(dp[i-1][4], dp[i-1][3]+prices[i]);
}
res=dp[prices.length-1][4];
return res;
}
题型四:买卖股票的最佳时机Ⅳ
力扣链接
问题描述:
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。示例:输入:k = 2, prices = [3,2,6,5,0,3];输出:7;解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
思路:
题型一有1次买入卖出机会;题型二有无限次买入卖出机会;题型三有至多2次买入卖出机会;题型四有至多k次买入卖出机会,k由键盘输入。本题和题型三一个思路。
首先明确第i天的状态:无操作,第一次买入,第一次卖出,第二次买入,第二次卖出…第k次买入,第k次卖出。定义dp[i][j]表示第i天持有股票状态为j时的最大收益。k=0时直接返回0;k>0时,j有2k+1个取值,j取0时表示无操作,j取2(k-1)+1时表示第k次买入,j取2k时表示第k次卖出。最终输出dp[prices.length-1][2k]。若dp[prices.length-1][4]<0,则输出0。
对于第i天的2k+1种状态可分为三类:
(1)j=0时:如果无操作,则收益始终为0,即dp[i][0]=0;(说明:这个状态没什么用,写程序的时候可以不操作,保留初始值。)
(2)j=2(k-1)+1时:(k>=1)
------a.第k次买入的状态是“今天买入”导致的,此时dp[i][2(k-1)+1]=dp[i-1][2(k-1)]-prices[i]。(类比题型三很容易理解)
------b.第k次买入的状态是昨天延续过来的,此时dp[i][2(k-1)+1]=dp[i-1][2(k-1)+1]。
dp[i][2(k-1)+1]取a,b两种情况的最大值。
(3)j=2k时:
------a.第k次卖出的状态是“今天卖出”导致的,此时dp[i][2k]=dp[i-1][2k-1]+prices[i]。(类比题型三很容易理解)
------b.第k次卖出的状态是昨天延续过来的,此时dp[i][2k]=dp[i-1][2k]。
dp[i][2k]取a,b两种情况的最大值。
边界值:j=2(k-1)+1时,dp[0][2(k-1)+1]=0-prices[0];j=2k时,dp[0][2k]=0;
代码:
public static int maxProfit(int k,int[] prices) {
int res=0;
int[][]dp=new int[prices.length][2*k+1];
for(int j=1;j<=k;j++) {
dp[0][2*(j-1)+1]=0-prices[0];
dp[0][2*j]=0;
}
for(int i=1;i<prices.length;i++) {
for(int j=1;j<=k;j++) {
dp[i][2*(j-1)+1]=Math.max(dp[i-1][2*(j-1)+1], dp[i-1][2*(j-1)]-prices[i]);
dp[i][2*j]=Math.max(dp[i-1][2*j], dp[i-1][2*j-1]+prices[i]);
}
}
res=dp[prices.length-1][2*k];
return res;
}
题型五:最佳买卖股票时机含冷冻期
力扣链接
问题描述:
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。示例:输入: prices = [1,2,3,0,2];输出: 3 ;解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]。
思路:
定义dp[i][j]表示第i天持有股票状态为j时的最大收益。本题可进行多次交易,参考题型二。题型二中第i天有两种状态:持有股票和不持有股票。本题比题型二多了一个冷冻期,即卖出后第二天不能买入。说明卖出没有限制,而买入前需要先判断是否是卖出的第二天,因此需要将“卖出”单独拎出来作为一个约束条件。故可得第i天的状态:持有股票,当天卖出导致不持有股票,前一天延续来的不持有股票。最终输出后两者的最大值。
对于第i天的三种状态:
(1)当天卖出导致不持有股票(j=0):dp[i][0]=dp[i-1][2]+prices[i]。说明:因为本题可多次交易,现在不清楚今天以前是否完成过买入卖出操作,因此要用昨天持有股票的最大收益加上今天卖出股票得到的钱。
(2)前一天延续来的不持有股票(j=1):dp[i][1]=max(dp[i-1][1],dp[i-1][0])。说明:前一天不持有股票有两种可能,因此取两者最大值。
(3)持有股票(j=2):
------a.持有股票的状态是“今天买入”导致的,此时dp[i][2]=dp[i-1][1]-prices[i]。说明:今天买入,说明昨天没有执行卖出操作,对应j=1的状态。
------b.持有股票的状态是昨天延续过来的,dp[i][1]=dp[i-1][2]。
dp[i][2]取a,b两种情况的最大值。
边界值:dp[0][0]=0;dp[0][1]=0;dp[0][2]=0-prices[0];
代码:
public static int maxProfit(int[] prices) {
int res=0;
int[][]dp=new int[prices.length][3];
dp[0][0]=0;
dp[0][1]=0;
dp[0][2]=0-prices[0];
for(int i=1;i<prices.length;i++) {
dp[i][0]=dp[i-1][2]+prices[i];
//dp[i][1]=dp[i-1][1];//第一次写代码时只考虑了dp[i-1][1]的值
dp[i][1]=Math.max(dp[i-1][0],dp[i-1][1]);
dp[i][2]=Math.max(dp[i-1][1]-prices[i], dp[i-1][2]);
}
res=Math.max(dp[prices.length-1][0],dp[prices.length-1][1]);
return res;
}
题型六:买卖股票的最佳时机含手续费
力扣链接
问题描述:
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。整数 fee 代表了交易股票的手续费用。设计一个算法来计算你所能获取的最大利润。注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。示例:输入: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
思路:
定义dp[i][j]表示第i天持有股票状态为j时的最大收益。本题可进行多次交易,参考题型二。题型二中第i天有两种状态:持有股票和不持有股票。本题比题型二多了一个手续费,即卖出后需要支付手续费。说明持有股票状态和题型二一致,不持有股票状态发生了一定的变化。最终输出dp[prices.length-1][0]。若dp[prices.length-1][0]<=0,则输出0。
对于第i天的两种状态:
(1)持有股票(j=1):
------a.持有股票的状态是“今天买入”导致的,此时dp[i][1]=dp[i-1][0]-prices[i]。说明:因为本题可多次交易,现在不清楚今天以前是否完成过买入卖出操作,因此要用昨天不持有股票的最大收益减去今天买股票花的钱。
------b.持有股票的状态是昨天延续过来的,因此今天不能进行任何操作(若卖出则不符合持有股票的状态,若买入则对应a),今天持有股票的最大收益和昨天持有股票的最大收益相同,此时dp[i][1]=dp[i-1][1]。
dp[i][1]取a,b两种情况的最大值。
(2)不持有股票(j=0):
------a.不持有股票的状态是“今天卖出”导致的,此时dp[i][0]=dp[i-1][1]+prices[i]-fee。说明:需要扣除手续费。
------b.不持有股票的状态是昨天延续过来的,dp[i][0]=dp[i-1][0]。
dp[i][0]取a,b两种情况的最大值。
边界值:dp[0][0]=0-fee; dp[0][0]=0;dp[0][1]=0-prices[0];注意:要求的是最大收益,dp[0][0]显然应取0,而非扣除手续费。
代码:
public static int maxProfit(int[] prices,int fee) {
int res=0;
int[][]dp=new int[prices.length][2];
//dp[0][0]=0-fee;//注意:要求的是最大收益,显然应取0,而非扣除手续费
dp[0][0]=0;
dp[0][1]=0-prices[0];
for(int i=1;i<prices.length;i++) {
dp[i][0]=Math.max(dp[i-1][0], dp[i-1][1]+prices[i]-fee);
dp[i][1]=Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);
}
if(dp[prices.length-1][0]>0) {
res=dp[prices.length-1][0];
}
return res;
}
三、存在问题
1、什么是滚动数组?什么是滑动窗口?
2、贪心算法、回溯法、动态规划、剪枝是什么意思?
3、深度优先遍历与广度优先遍历算法?
4、
四、滑动窗口类
题型一:DNA序列
DNA序列详解
问题描述:
一个 DNA 序列由 A/C/G/T 四个字母的排列组合组成。 G 和 C 的比例(定义为 GC-Ratio )是序列中 G 和 C 两个字母的总的出现次数除以总的字母数目(也就是序列长度)。在基因工程中,这个比例非常重要。因为高的 GC-Ratio 可能是基因的起始点。给定一个很长的 DNA 序列,以及限定的子串长度N ,请帮助研究人员在给出的 DNA 序列中从左往右找出 GC-Ratio 最高且长度为 N 的第一个子串。注意是子串,而非子序列。数据范围:字符串长度满足 1≤n≤1000,输入的字符串只包含 A/C/G/T 字母。输入一个string型基因序列,和子串的长度。找出GC比例最高的子串,如果有多个则输出第一个的子串。
代码:
//法二:滑动窗口 16:20-17:00 40分钟
public static String DNASequence(String s,int n) {
String resStr="";
double count=0;//方便做除法
double GCRatio=0;
//不用求出来,因为长度是固定的,即分母固定,因此只要分子最大,求出来的值就最大
//计算滑动窗口初始值
for(int k=0;k<n;k++) {
char c=s.charAt(k);
if(c=='C'||c=='G') {
count++;
}
}
if(n==s.length()) {//子串长度与s相等,直接输出s
resStr=s;
}
if(n<s.length()){//不考虑子串长度非法的情况
int L=0;
double maxCount=count;
int resL=0;//存放最大值对应的左边界
for(int R=n;R<s.length();R++) {//右边界从n下标开始
L=R-n+1;//n=R-L+1
char cL=s.charAt(L-1);//判断删掉的元素是否为C/G
char cR=s.charAt(R);
if(cL=='C'||cL=='G') {//若左边界为C/G则每次移动,数量-1
count--;
}
if(cR=='C'||cR=='G') {//若右边界为C/G则每次移动,数量+1
count++;
}
if(count>maxCount) {
maxCount=count;
resL=L;
}
}
resStr=s.substring(resL,resL+n);//长度=j-i+1,R=L+n-1,含左不含右,故要再加1
}
return resStr;
}
//法一:暴力法
public static String DNASequence(String s,int n) {
double res=0;
double count=0;//方便做除法
double GCRatio=0;
int L=0;
ArrayList<Double> arrList=new ArrayList<Double>();
for(int R=n-1;R<s.length();R++) {
L=R-n+1;//左边界
for(int j=L;j<=R;j++) {
char c=s.charAt(j);
if(c=='C'||c=='G') {
count++;
}
}
GCRatio=count/n;
arrList.add(GCRatio);
count=0;//清空count,重新计数
}
for(int i=0;i<arrList.size();i++) {
if(arrList.get(i)>res) {
//如果加等号:当有多个相同的最大数时,会输出最后一个
res=arrList.get(i);
L=i;
}
}
String resStr=s.substring(L,L+n);
//长度=j-i+1,R=L+n-1,含左不含右,故要再加1
return resStr;
}