Leetcode刷题之动态规划
Posted 哈喽喔德
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Leetcode刷题之动态规划相关的知识,希望对你有一定的参考价值。
算法解释
可以用局部最优解来推到全局最优解,即动态规划。动态规划在查找有很多重叠子区间问题的最优解时最有效。它将问题重新组合成子问题,为避免多次解决这些子问题,结果都逐渐被计算并保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。
要使用动态规划需要满足两个条件:1. 最优子结构,即局部最优解能决定全局最优解;2. 当前状态是前面状态的完美总结,即现状态不会影响前面的状态。
解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和存储子问题的解来求解最终问题。在一些情况下动态规划可以看作带有状态记录的优先搜索。状态记录的意思是如果一个子问题在优先搜索前被计算过一次,我们可以把它的结果存储下来,之后遍历到该子问题的时候可以直接返回存储的结果。动态规划是下而上,搜索是自上而下的。
与贪心算法的差异
贪心算法当前阶段的解用上一阶段直接推导出来。动态规划当前问题的最优解,不能从上一阶段子问题简单得出,需要前面多阶段多层子问题共同计算出,因此需要保留历史上求过的子问题及其最优解。
动态规划的一般过程
- 找到过程中变化的量(状态),以及变化的规律(状态转移方程)
- 确定一些初始状态,通常需要dp数组来保存
- 利用状态转移方程,推出最终答案
动态规划的解法
- 自顶向下,即从父问题到子问题,使用递归;如果有重叠子区间,采用记忆性递归进行性能优化。
- 自底向上,即先解决子问题,再解决父问题,使用递推。
力扣例题
一维
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()];
子序列问题
子序列问题通常有两种动态规划的方法:
- 定义一个dp数组,dp[i]表示以i结尾的子序列的性质,在处理好每个位置后,遍历一遍dp数组可以得到结果。
- 定义一个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刷题之动态规划的主要内容,如果未能解决你的问题,请参考以下文章