概述
算法解释
可以用局部最优解来推到全局最优解,即动态规划。动态规划在查找有很多重叠子区间问题的最优解时最有效。它将问题重新组合成子问题,为避免多次解决这些子问题,结果都逐渐被计算并保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。
要使用动态规划需要满足两个条件: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]];
}
}
}
return dp[n-1][target];
}
}
474. 一和零
分析:
该题中的物品容量有两种,一种是1的容量,一种是0的容量。因此定义的dp数组为三位数组。dp[i][j][k]表示前i个子串集合中有j个1,k个0的最大子串长度。当i为0,即没有字串可用是,dp[i][j][k]=0。
对于strs[i],首先要统计其中0和1的个数。如果0或者1的个数大于j和k,则不能选择第i个字符串,dp[i][j][k]=dp[i-1][j][k];如果0和1的个数都小于等于j和k,dp[i][j][k]=dp[i-1][j-one][k-zero]+1。
题解:
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
int[][][] dp = new int[len+1][m+1][n+1];
//三层循环遍历
for(int i=1;i<=len;++i) {
for(int j=0;j<=m;++j) {
for(int k=0;k<=n;++k) {
int[] zeroOne = getZeroOne(strs[i-1]);
//背包有容量装下该串
//选择子串长度更大的情况
if(zeroOne[0]<=j && zeroOne[1]<=k) {
dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i-1][j-zeroOne[0]][k-zeroOne[1]]+1);
}
else {
//装不下跟上一层情况相同
dp[i][j][k] = dp[i-1][j][k];
}
}
}
}
return dp[len][m][n];
}
//获取子串中0和1的个数
private int[] getZeroOne(String str) {
int zero = 0;
int one = 0;
for(char c:str.toCharArray()) {
if(c=='0') {
zero++;
}
else if(c=='1') {
one++;
}
}
return new int[] {zero,one};
}
}
322. 零钱兑换
分析:
dp[i]表示组成金额i所需最少的硬币数量,则状态转移方程为:dp[i] = dp[i-c]+1。
题解:
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
//首先要将dp数组填充为比amount大的数
//因为就算全由1元硬币组成的amount需要的数量也不会超过amount
//采用小于amount的数可能被当作中间结果返回
Arrays.fill(dp, Integer.MAX_VALUE-1);
dp[0] = 0;
for(int i=1;i<=amount;++i) {
for(int j=0;j<coins.length;++j) {
if(coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i-coins[j]]+1);
}
}
}
return dp[amount] > amount ? -1 :dp[amount];
}
}
最后
以上就是闪闪楼房为你收集整理的Leetcode刷题之动态规划(Java)算法解释力扣例题的全部内容,希望文章能够帮你解决Leetcode刷题之动态规划(Java)算法解释力扣例题所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复