动态规划学习记录

动态规划学习记录(自用)

一、适用范围

当问题可分解为相互独立的子问题时,考虑动态规划。例如:斐波那契数列、爬楼梯问题、最值问题、背包问题等。

二、动态规划经典题型

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-2
w(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;

-----
10100
10111
11111
10010

边界值:由状态转移方程可知,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;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值