Leetcode刷题之动态规划(Java)

Posted 哈喽喔德

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Leetcode刷题之动态规划(Java)相关的知识,希望对你有一定的参考价值。

算法解释

可以用局部最优解来推到全局最优解,即动态规划。动态规划在查找有很多重叠子区间问题的最优解时最有效。它将问题重新组合成子问题,为避免多次解决这些子问题,结果都逐渐被计算并保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。

要使用动态规划需要满足两个条件:1. 最优子结构,即局部最优解能决定全局最优解;2. 当前状态是前面状态的完美总结,即现状态不会影响前面的状态。

解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和存储子问题的解来求解最终问题。在一些情况下动态规划可以看作带有状态记录的优先搜索。状态记录的意思是如果一个子问题在优先搜索前被计算过一次,我们可以把它的结果存储下来,之后遍历到该子问题的时候可以直接返回存储的结果。动态规划是下而上,搜索是自上而下的。

与贪心算法的差异

贪心算法当前阶段的解用上一阶段直接推导出来。动态规划当前问题的最优解,不能从上一阶段子问题简单得出,需要前面多阶段多层子问题共同计算出,因此需要保留历史上求过的子问题及其最优解。

动态规划的一般过程

  1. 找到过程中变化的量(状态),以及变化的规律(状态转移方程)
  2. 确定一些初始状态,通常需要dp数组来保存
  3. 利用状态转移方程,推出最终答案

动态规划的解法

  1. 自顶向下,即从父问题到子问题,使用递归;如果有重叠子区间,采用记忆性递归进行性能优化。
  2. 自底向上,即先解决子问题,再解决父问题,使用递推。

力扣例题

一维

70. 爬楼梯

分析:

典型的斐波那契数列,DP方程为f(n)=f(n-1)+f(n-2)。由于不需要记录中间量,设置两个提前变量和一个当前变量即可达到求解目的,节约空间。

题解:

class Solution 
    public int climbStairs(int n) 
    	if (n<=2) 
			return n;
		
    	int pre1 = 1;
    	int pre2 = 2;
    	int cur = 0;
    	//循环求解第n个台阶的登顶方法,并更新提前变量
    	for(int i=2;i<n;++i) 
    		cur = pre1+pre2;
    		pre1 = pre2;
    		pre2 = cur;
    	
    	
    	return cur;
    

198. 打家劫舍

分析:

假设dp[i]表示抢到第i个房子时获取的最大金额。在碰到第i个房子时我们可以考虑两种情况:一种是抢,那么当前总金额就是nums[i-1]+dp[i-2];如果不抢这个房子,当前金额就是前面i-1个房子的最大金额dp[i-1]。同样不需要保存每一步的结果,因此设置两个提前变量即可。

题解:

class Solution 
    public int rob(int[] nums) 
    	if (nums.length==0) 
			return 0;
		
    	else if(nums.length==1) 
    		return nums[0];
    	
    	
    	int pre1 = 0;
    	int pre2 = nums[0];
    	int cur = 0;
    	
    	for(int i=2;i<=nums.length;++i) 
    		cur = Math.max(pre1+nums[i-1], pre2);
    		pre1 = pre2;
    		pre2 = cur;
    	
    	return cur;
    

413. 等差数列划分

分析:

设dp[i]为以第i个数结尾的等差数列的总数, 等差数列的条件为nums[i]-nums[i-1]==nums[i-1]-nums[i-2]。这里特别需要注意的是,dp[i] = dp[i-1]+1是需要观察或列举演算得到的规律,即等差数列每增加一个元素,新的等差数列以最后一个元素(新增元素)结尾的子数列个数为原来的+1。

题解:

class Solution 
    public int numberOfArithmeticSlices(int[] nums) 
    	int n = nums.length;
    	if (n<3) 
			return 0;
		
    	
    	int[] dp = new int[n];
    	for(int i=2;i<n;++i) 
    		if (nums[i]-nums[i-1]==nums[i-1]-nums[i-2]) 
				dp[i] = dp[i-1]+1;
			
    	

        int sum = 0;
        for(int i=0;i<n;++i)
            sum+=dp[i];
        
    	
    	return sum;
    

二维

64. 最小路径和

分析:

对于二维的题目,我们同样可以定义一个二维的dp数组,dp[i][j]表示走到当前点的最短路径长度,由于之内向下或向右走,因此dp[i][j] = min(dp[i-1][j],dp[i][j-1])+grid[i][j],这里需要注意边界的dp方程有所调整。

题解:

class Solution 
    public int minPathSum(int[][] grid) 
    	int m = grid.length;
    	int n = grid[0].length;
    	int[][] dp = new int[m][n];
    	
    	for(int i=0;i<m;++i) 
    		for(int j=0;j<n;++j) 
    			if(i==0 && j==0) 
    				dp[0][0] = grid[0][0];
    			
    			else if(i==0) 
    				//该位置的上一步只能是从右边过来的
    				dp[i][j] = dp[i][j-1]+grid[i][j];
    			
    			else if(j==0) 
    				//该位置的上一步只能是上边过来的
    				dp[i][j] = dp[i-1][j]+grid[i][j];
    			
    			else 
    				dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1])+grid[i][j];
    			
    		
    	
    	
    	return dp[m-1][n-1];
    

221. 最大正方形

分析:

定义一个二维dp数组,其中dp[i][j]表示面积最大的正方形右下角元素。如果当前位置是0,dp[i][j]=0;如果当前位置是1,dp[i][j]=k2,其充分条件为dp[i-1][j-1]、dp[i][j-1]、dp[i-1][j]必须都不小于(k-1)2,否则(i,j)位置不可以构成边长为k的正方形。同理,如果这三个值中的最小值为k-1,则(i,j)位置一定且最大可以构成一个边长为k的正方形。(画图即可理解)

dp方程即为:dp[i][j] = min(dp[i-1][j-1], dp[i][j-1], dp[i-1][j] )+1

题解:

class Solution 
    public int maximalSquare(char[][] matrix) 
    	int m = matrix.length;
    	int n = matrix[0].length;
    	int[][] dp = new int[m][n];
    	int maxSide = 0;
    	
    	for(int i=0;i<m;++i) 
    		for(int j=0;j<n;++j) 
    			//遇到1的时候开始构建dp方程
    			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][j-1], dp[i-1][j]))+1;
	          
	          //保存最大的面积
						maxSide = Math.max(maxSide, dp[i][j]);
					
    		
    	
    	return maxSide*maxSide;
    

分割类型

279. 完全平方数

分析:

对于分割型题目,动态规划的状态转移方程通常不依赖相邻的位置,而是以来满足分割条件的位置。定义一个一维数组dp,其中dp[i]表示i最少可以由几个完全平方数相加构成。本题中i只依赖i-k^2的位置。因此dp方程为:dp[i] = min(dp[i-1], dp[i-4], dp[i-9], …)

题解:

class Solution 
    public int numSquares(int n) 
    	//dp[i]表示组成i的最小平方数个数
    	int[] dp = new int[n];
    	//初始除0位置以外赋给最大值
    	Arrays.fill(dp, Integer.MAX_VALUE);
    	dp[0] = 0;
    	for(int i=0;i<=n;++i) 
    		for(int j=1;j*j<=i;++j) 
    			//加上一个平方数或自己本身
    			dp[i] = Math.min(dp[i-j*j]+1, dp[i]);
    		
    	
    	return dp[n];
    

91. 解码方法

分析:

设dp[i]表示前i个字母的解码方法数,则dp[i]有两种情况:在位置i使用一个字符,dp[i] = dp[i-1];如果使用两个字符即i和i-1,dp[i] = dp[i-2]。字符串为空的时候只有一种空编码的情况。

题解:

class Solution 
    public int numDecodings(String s) 
    	char[] chs = s.toCharArray();
    	int[] dp = new int[s.length()+1];
    	dp[0] = 1;
    	
    	for(int i=1;i<=s.length();++i) 
    		if(chs[i-1]!='0') 
    			dp[i] += dp[i-1];
    		
    		
    		if(i>=2 && (chs[i-2]-'0')*10+(chs[i-1]-'0')<=26 && chs[i-2]!='0') 
    			dp[i] += dp[i-2];
    		
    	
    	
    	return dp[s.length()];
    

139. 单词拆分

分析:

dp[i]表示字符串s前i个字符组成的字符串s’是否能被拆分为字典中的单词。如果设j为中间的一个分割点,dp[j]=true表示前j个字母已经能被字典表示,则[j+1,i]是一个单词的话,那么dp[i]也为true。

题解:

class Solution 
    public boolean wordBreak(String s, List<String> wordDict) 
    	boolean[] dp = new boolean[s.length()+1];
    	dp[0] = true;
    	
    	for(int i=0;i<=s.length();++i) 
    		//这里特别要注意i是字符在串中的位置
    		//j是字符的下标,因此substring方法包含的位置是下标[j,i-1]
    		//实际表示串中的[j+1,i]位置
    		for(int j=0;j<i;++j) 
    			if(dp[j] && wordDict.contains(s.substring(j, i))) 
    				dp[i] = true;
    				break;
    			
    		
    	
    	
    	return dp[s.length()];
    

子序列问题

子序列问题通常有两种动态规划的方法:

  1. 定义一个dp数组,dp[i]表示以i结尾的子序列的性质,在处理好每个位置后,遍历一遍dp数组可以得到结果。
  2. 定义一个dp数组,dp[i]表示到位置i为止的子序列的性质,这样dp数组的最后一位即为题目所求。

300. 最长递增子序列

分析:

设dp[i]为以i结尾的最长严格递增子序列的长度。如果已有dp[j]成立,则nums[i]>dp[j]代表dp[i]=dp[j]+1。

题解:

class Solution 
    public int lengthOfLIS(int[] nums) 
    	int n = nums.length;
    	int max = 0;
    	int[] dp = new int[n];
    	Arrays.fill(dp, 1);
    	
    	for(int i=0;i<n;++i) 
    		for(int j=0;j<i;++j) 
    			if(nums[j]<nums[i]) 
    				//这里本质是找出最大的dp[j]
    				//然后再+1赋值为dp[i]
    				dp[i] = Math.max(dp[i], dp[j]+1);
    			
    		
    		max = Math.max(max, dp[i]);
    	
    	
    	return max;
    

1143. 最长公共子序列

分析:

设置一个二维dp数组,dp[i][j]表示text1中前 i 个元素和text2中前 j 个的最长公共子序列的长度。设置两个指针分别指向两串的字母,如果相等:dp[i][j] = dp[i-1][j-1]+1;如果不相等,那么两个指针中的一个需要回退,留下那个更长的公共子串。

题解:

class Solution 
    public int longestCommonSubsequence(String text1, String text2) 
    	int m = text1.length();
    	int n = text2.length();
    	int[][] dp = new int[m+1][n+1];
    	
    	for(int i=1;i<=m;++i) 
    		for(int j=1;j<=n;++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][j-1], dp[i-1][j]);
    			
    		
    	
    	
    	return dp[m][n];
    

背包问题

通常题目形式如:有N个物品和容量为W的背包,每个物品都有自己的体积w和价值v,求拿哪些物品可以使得背包装下的物品的总价值最大。如果限定每种物品只能选择0个或1个,则称01背包问题;如果不限制每种物品个数,则为完全背包问题。

可以使用动态规划来解决背包问题,以01背包为例:可以定义一个二维数组dp存储最大价值,dp[i][j]表示前i件物品体积不超过j的情况下的最大价值。我们在遍历到第i件物品时,在当前背包总容量为j的情况下,如果不将i物品放入背包,那么dp[i][j] = dp[i-1][j];如果物品i放入背包,那么dp[i][j] = dp[i-1][j-w]+v。我们只需要在遍历过程中对这两种情况取最大值即可。

在完全背包问题中,一个物品可以拿多次。状态转移方程为:dp[i][j] = max(dp[i-1][j], dp[i][j-w]+v)。

416. 分割等和子集

分析:

该问题等价于01背包问题,只需要选取一部分物品使得他们的值的总和为sum/2,因此只需要通过一个布尔值矩阵来表示状态转移方程。dp[i][j]表示在下标[0,i]区间内的所有整数,当前能否选出一些数使得他们之和恰好为j。

题解:

class Solution 
    public boolean canPartition(int[] nums) 
    	int sum = 0;
    	for(int i:nums) 
    		sum+=i;
    	
    	//如果和不为偶数,直接返回false
    	if(sum % 2!=0) 
    		return false;
    	
    	
    	int n = nums.length;
    	int target = sum/2;
    	boolean[][] dp = new boolean[n][target+1];
    	
    	//第一行只能填满容量为nums[0]的背包
    	if(nums[0] <= target) 
    		dp[0][nums[0]] = true;
    	
    	
    	//从第二行开始填表
    	for(int i=1;i<n;++i) 
    		for(int j=0;j<=target;++j) 
    			//不考虑将当前位置加入背包
    			dp[i][j] = dp[i-1][j];
    			
    			//当前位置刚好独自可以填满背包
    			if (nums[i]==j) 
    				dp[i][j] = true;
					continue;
				
    			
    			//如果当前物品小于j, 可以不加入也可以加入背包
    			if (nums[i]<j) 
    				dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i]];
				
   

以上是关于Leetcode刷题之动态规划(Java)的主要内容,如果未能解决你的问题,请参考以下文章

PAT刷题之动态规划专题

算法题之动态规划

leetcode之动态规划刷题总结1(Java)

LeetCode刷题之搜索(Java)

LeetCode刷题之搜索(Java)

leetcode刷题之二分查找(Java)