2023-04-23 算法面试中常见的动态规划问题

news/2024/4/25 14:39:58/文章来源:https://blog.csdn.net/lsg19920625/article/details/130331392

动态规划

1 什么是动态规划

以菲波那切数列求和为例,通过

  • 1.普通的递归
  • 2.引入记忆数组memo
  • 3.自下而上地解决问题,即动态规划

动态规划的定义

dynamic programming (also known as dynamic optimization) is a method for solving a complex problem by breaking it down into a collection of simpler subproblems, solving each of those subproblems just once, and storing their solutions – ideally, using a memory-based data structure.

将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。

简单说:自下而上地,先解决最小子问题,然后最小子问题不断向上推,最终解决完整的问题

体会其中自下而上地解决问题的思路,有点类似数学归纳法

递归与动态规划的联系和区别

递归与动态规划的联系和区别

LeetCode上的对应题目:509. 斐波那契数

class Solution {/*** 自下而上地解决问题,也称动态规划,有点像数学归纳法*/public int fib(int n) {if(n == 0 || n == 1){return n;}List<Integer> memo = new ArrayList<>(n + 1);for (int i = 0; i < n + 1; i++) {memo.add(-1);}memo.set(0, 0);memo.set(1, 1);for (int i = 2; i <= n; i++) {memo.set(i, memo.get(i - 1) + memo.get(i - 2));}return memo.get(n);}
}

2 第一个动态规划问题:70.Climbing Stairs

有一个楼梯,总共有n阶台阶。每一次,可以上一个台阶,也可以上两个台阶。问,爬上这样的一个楼梯,一共有多少不同的方法?
如 n = 3,可以爬上这个楼梯的方法有:[1,1,1] , [1,2] , [2,1] , 所以答案为3

一、递归法(可以看到有重复子问题存在)

ClimbingStairs的递归求解

代码见ClimbingStairs的递归求解

public class Solution1 {public static int num;public int climbStairs(int n) {num++;if (n == 0) {return 1;}if (n == 1) {return 1;}return climbStairs(n - 1) + climbStairs(n - 2);}public static void main(String[] args) {int n = 20;int ways = new Solution1().climbStairs(n);System.out.println("一共有" + ways + "种爬楼梯的方法");System.out.println("一共进入递归函数" + num + "次");}
}/*** 输出如下:* <p>* 一共有10946种爬楼梯的方法* 一共进入递归函数21891次*/

但是实际这个方法是存在重复子问题地,如下图蓝框所示:

ClimbingStairs递归求解的重复子问题1

ClimbingStairs递归求解的重复子问题2

二、记忆数组法

public class Solution2 {public static int num;/*** 记忆数组memory,用于存储子问题是否已经被访问*/public static int[] memo;public int climbStairs(int n) {num++;if (n == 0) {return 1;}if (n == 1) {return 1;}if (memo[n] == -1) {memo[n] = climbStairs(n - 1) + climbStairs(n - 2);}return memo[n];}public static void main(String[] args) {int n = 20;memo = new int[n + 1];Arrays.fill(memo, -1);int ways = new Solution2().climbStairs(n);System.out.println("一共有" + ways + "种爬楼梯的方法");System.out.println("一共进入递归函数" + num + "次");}
}/*** 输出如下(可以看到记忆数组节省了大量进入递归的操作):* <p>* 一共有10946种爬楼梯的方法* 一共进入递归函数39次*/

三、动态规划法

public class Solution3 {public int climbStairs(int n) {if(n == 1 || n ==2){return n;}/*** 记忆数组memory,用于存储子问题是否已经被访问*/int[] memo = new int[n + 1];memo[1] = 1;memo[2] = 2;for (int i = 3; i <= n; i++) {memo[i] = memo[i - 1] + memo[i - 2];}return memo[n];}public static void main(String[] args) {int n = 20;int ways = new Solution3().climbStairs(n);System.out.println("一共有" + ways + "种爬楼梯的方法");System.out.println("非递归法,不需要递归函数");}
}/*** 输出如下:* <p>* 一共有-10946种爬楼梯的方法* 非递归法,不需要递归函数*/

类似的问题还有120号和64号问题

120.三角形最小路径和

  • 1.普通的递归
  • 2.引入记忆数组memo,跳过重复子问题
  • 3.自下而上地解决问题,即动态规划

64.最小路径和

  • 1.普通的递归
  • 2.引入记忆数组memo,跳过重复子问题
  • 3.自下而上地解决问题,即动态规划

3 发现重叠子问题:343.整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。示例 1:输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

拆分4为例,发现存在重复子问题

IntegerBreak4

扩展搭配拆分n,重复子问题就更多了

IntegerBreakN

经过前两节可知,可以用记忆数组或者动态规划的方法来大大提高代码效率

最优子结构的含义:通过求解子问题的最优解,可以获得原问题的最优解

IntegerBreakNOptimize

代码

  • 纯递归
  • 递归+记忆数组
  • 动态规划实现

类似问题还有279、91、62、63号问题

279.完全平方数

在第06章_栈和队列.md#65-bfs和图的最短路径279完全平方数中用BFS实现过

class Solution {public int numSquares(int n) {// meme[i]代表找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 nint[] memo = new int[n + 1];Arrays.fill(memo, Integer.MAX_VALUE);memo[0] = 0;memo[1] = 1;for (int i = 2; i <= n; i++) {for (int j = 1; i - j * j >= 0; j++) {// 之所以还要和memo[i]比较是因为在内层循环中会不算更新meme[i]memo[i] = Math.min(memo[i], memo[i - j * j] + 1);}}return memo[n];}
}

91.解码方法

class Solution {public int numDecodings(String s) {int n = s.length();if (n == 0) {return 0;}int[] memo = new int[n + 1];memo[n] = 1;memo[n - 1] = s.charAt(n - 1) != '0' ? 1 : 0;for (int i = n - 2; i >= 0; i--) {if (s.charAt(i) != '0') {memo[i] = (Integer.parseInt(s.substring(i, i + 2)) <= 26) ? memo[i + 1] + memo[i + 2] : memo[i + 1];}}return memo[0];}
}

62.不同路径

执行用时 : 0 ms , 在所有 Java 提交中击败了 100.00% 的用户 内存消耗 : 32.9 MB , 在所有 Java 提交中击败了 74.78% 的用户

class Solution {public int uniquePaths(int m, int n) {int[][] memo = new int[m][n];for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (i == 0 || j == 0) {memo[i][j] = 1;continue;}memo[i][j] = memo[i - 1][j] + memo[i][j - 1];}}return memo[m - 1][n - 1];}
}

63.不同路径 II

在62题的基础上要考虑足够多的特殊场景.
执行用时 : 2 ms , 在所有 Java 提交中击败了 90.06% 的用户 内存消耗 : 36.9 MB , 在所有 Java 提交中击败了 71.65% 的用户

class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {int m = obstacleGrid.length;int n = obstacleGrid[0].length;int[][] memo = new int[m][n];if(obstacleGrid[0][0]==1 || obstacleGrid[m-1][n-1]==1){return 0;}for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {memo[i][j] = -1;}}for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (memo[i][j] != -1) {// 已经处理过了就不要再处理了continue;}if (i == 0) {if (obstacleGrid[i][j] == 1) {for (int k = j; k < n; k++) {// 往后列的值都为0了memo[i][k] = 0;}} else {memo[i][j] = 1;}continue;}if (j == 0) {if (obstacleGrid[i][j] == 1) {for (int k = i; k < m; k++) {// 往后列的值都为0了memo[k][j] = 0;}} else {memo[i][j] = 1;}continue;}if (obstacleGrid[i][j] == 1) {memo[i][j] = 0;continue;}memo[i][j] = memo[i - 1][j] + memo[i][j - 1];}}return memo[m - 1][n - 1];}
}

4 状态的定义和转移 198.House Robber

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。示例 1:输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。

递归过程分析如下

本节的分析是自上而下分析的,可以看到存在重复子问题。实际自下而上地分析更简单。下面的代码就是自下而上实现地

HouseRobber的递归分析

状态转移方程

本题中的状态转移方程为:memo[i] = Math.max(nums[i] + memo[i - 2], memo[i - 1]);

代码

下面的代码实际是从下往上进行的,解决基础的问题后不断向前推导,最终解决了完整的问题。老师的代码是从上往下地,不好理解

执行用时 : 0 ms , 在所有 Java 提交中击败了 100.00% 的用户 内存消耗 : 34 MB , 在所有 Java 提交中击败了 80.45% 的用户

class Solution {public int rob(int[] nums) {int n = nums.length;if(n == 0){return 0;}if(n == 1){return nums[0];}int[] memo = new int[n + 1];memo[0] = nums[0];memo[1] = Math.max(nums[0], nums[1]);for (int i = 2; i < n; i++) {memo[i] = Math.max(nums[i] + memo[i - 2], memo[i - 1]);}return memo[n - 1];}
}

类似的问题:213、337、309

213.打家劫舍 II

第1个和最后一个不能同时抢,分别把第一个和最后一个从数组剔除,按照198的方法分别去求最多抢劫的钱,然后取一下两者中的较大者即可

执行用时 : 0 ms , 在所有 Java 提交中击败了 100.00% 的用户 内存消耗 : 34.2 MB , 在所有 Java 提交中击败了 42.36% 的用户

public class Solution {// 198号问题实现地打家劫舍public int rob198(int[] nums) {int n = nums.length;if (n == 0) {return 0;}if (n == 1) {return nums[0];}int[] memo = new int[n + 1];memo[0] = nums[0];memo[1] = Math.max(nums[0], nums[1]);for (int i = 2; i < n; i++) {memo[i] = Math.max(nums[i] + memo[i - 2], memo[i - 1]);}return memo[n - 1];}// 核心是:第1个和最后一个不能同时抢,分别把第一个和最后一个从数组剔除,按照198的方法分别去求最多抢劫的钱,然后取一下两者中的较大者即可public int rob(int[] nums) {int n = nums.length;if (n == 0) {return 0;}if (n == 1) {return nums[0];}// 去除第一个元素int[] nums1 = new int[n - 1];for (int i = 1; i < n; i++) {nums1[i - 1] = nums[i];}// 去除最后一个元素int[] nums2 = new int[n - 1];for (int i = 0; i < n - 1; i++) {nums2[i] = nums[i];}return Math.max(rob198(nums1), rob198(nums2));}
}

337.打家劫舍 III

递归遍历,当前节点作为根节点,按照当前节点被抢劫和未被抢劫统计最大值

class Solution {public int rob(TreeNode root) {int[] res = doRob(root);// 返回抢根节点和不抢根节点两种情况下的最大值return Math.max(res[0], res[1]);}// 在遍历中找最大值private int[] doRob(TreeNode node){int[] res = new int[2];if(node == null){return res;}int[] left = doRob(node.left);int[] right = doRob(node.right);// 不抢当前的根节点node,直接比较左右子树即可res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);// 抢当前根节点node,那么左右子树的根节点就不能抢了,所以left[1]和right[1]就不用考虑了res[1] = left[0] + right[0] + node.val;return res;}}

309.最佳买卖股票时机含冷冻期

比较复杂的状态转移,涉及多个状态

class Solution {public int maxProfit(int[] prices) {int n = prices.length;if(n == 0){return 0;}// sell[i]表示截至第i天,最后一个操作是卖时的最大收益;int[] sell = new int[n];// buy[i]表示截至第i天,最后一个操作是买时的最大收益;int[] buy  = new int[n];// cool[i]表示截至第i天,最后一个操作是冷冻期时的最大收益;int[] cool = new int[n];// 第一天是买入,纯赔钱.sell[0]和cool[0]用默认的0作为初始值即可buy[0] = -prices[0];for(int i = 1; i < n; i++){// 第一项表示第i天卖出,第二项表示第i天冷冻sell[i] = Math.max(buy[i - 1]  + prices[i], sell[i - 1]);// 第一项表示第i天买进,第二项表示第i天冷冻buy[i]  = Math.max(cool[i - 1] - prices[i], buy[i - 1]);// 根据题目的状态转换形式,cool[i]=sell[i-1]。其不存在从buy状态和cold状态再到cold状态的过程,只存在sell状态到cold状态。因此cool[i]=sell[i-1]的物理意义才是正确的.状态方程的第三个是没有任何意义的。cool[i] = sell[i - 1];}return sell[n - 1];}
}

5 0-1背包问题

0-1背包问题的来源

参考《趣学算法》P37,结合所在章节体会背包问题0-1背包问题的区别

  • 物品 可分割 的装载问题称为背包问题,可以用贪心算法,优先选择性价比高地装入
  • 物品不可分割的装载问题称为0-1背包问题,该类问题已经不再具有贪心选择性质,即原问题的整体最优解无法通过一系列局部最优解的选择得到。此时只能通过动态规划获取

0-1背包问题的模型阐述

有一个背包,它的容量为C (Capacity),。现在有n种不同的物品,编号为0…n-1,其中每一件物品的重量为w(i),价值为v(i)。问可以向这个背包中盛放哪些物品,使得在不超过背包容量(容量即可以容纳地最大重量)的基础上,物品的总价值最大。

0-1背包问题的状态方程

# F( n , C ) 表示将n个物品放进容量为C的背包,使得价值最大.v表示value即价值,w表示weight即重量值
F ( i , c )  =   F( i-1 , c ) # 情况1:第i个物品不再放入容器,价值已经最大了,i和i-1对应的值相同

或者

F ( i , c )  =   v(i) + F( i-1 , c - w(i) ) # 情况2:第i个物品放入容器,那么当前的容量往下递归时需要减去i的重量w(i),并加上i的价值v(i)

所以最大价值的状态转移方程是:

F ( i , c )  =   max( F( i-1 , c ) , v(i) + F( i-1 , c - w(i) )

背包问题的动态规划过程举例

背包问题示例

代码如下, 自己debug调试下:

/************************************************************ @Description : 递归求解背包问题,记忆数组实现,避免了重复子问题* @author      : 梁山广(Liang Shan Guang)* @date        : 2019/8/24 20:31* @email       : liangshanguang2@gmail.com***********************************************************/
package Chapter09DynamicAllocate.Section5Knapsack;public class Solution3Dynamic {/*** 背包问题:在容量C下求最大价值** @param w 重量weight数组* @param v 价值value数组* @param C 容器总容量* @return 背包问题的总价值*/public int knapsack(int[] w, int[] v, int C) {if (w == null || v == null || w.length != v.length) {throw new IllegalArgumentException("Invalid w or v");}if (C < 0) {throw new IllegalArgumentException("C must be greater or equal to zero.");}int n = w.length;if (n == 0 || C == 0) {return 0;}int[][] memo = new int[n][C + 1];for (int i = 0; i < n; i++) {for (int j = 0; j < C + 1; j++) {memo[i][j] = -1;}}for (int j = 0; j <= C; j++) {// 对第0个物体遍历所有的容量可能值.当容量值大于第0个物体的重量时就取底0个物体的容量值,否则取0memo[0][j] = j >= w[0] ? v[0] : 0;}// 计算0往后的物体for (int i = 1; i < n; i++) {for (int j = 0; j <= C; j++) {if (j >= w[i]) { // 如果当前容量大于等于第i个物体的重量,那么就尝试下放入新物体// 上一个物体加入时的最大价值memo[i-1][j]// 与// 新物体加入后的价值v[i] + memo[i-1][j-w[i]]// 两者中的较大值memo[i][j] = Math.max(memo[i - 1][j], v[i] + memo[i - 1][j - w[i]]);} else { // 如果当前容量小于第i个物体的重量,就不用放入新物体了,直接用上一步求地最大价值即可memo[i][j] = memo[i - 1][j];}}}return memo[n - 1][C];}public static void main(String[] args) {int[] weight = {1, 2, 3};int[] value = {6, 10, 12};int C = 5;int bestValue = new Solution3Dynamic().knapsack(weight, value, C);System.out.println("最大价值是:" + bestValue);System.out.println("动态规划不需要进入递归");}
}/*** 输出结果为(动态规划不需要进入递归):* <p>* 最大价值是:22* 动态规划不需要进入递归*/

多种实现方法

  • 纯递归实现
  • 递归+记忆数组去除递归重复子问题
  • 动态递归实现,也是上面图示的实现

6 背包问题的优化和变种

背包问题的时间复杂度和空间复杂度

  • 时间复杂度:O(n * C)
  • 空间复杂度:O(n * C)

空间复杂度的优化

F( n , C ) 考虑将n个物品放进容量为C的背包,使得价值最大

F ( i , c )  =   max( F( i-1 , c ) , v(i) + F( i-1 , c - w(i) )

第i行元素只依赖于第i-1行元素。理论上,只需要保持两行元素。
空间复杂度:O( 2 * C ) = O(C)

代码见空间复杂度的优化

时间复杂度优化

时间复杂度优化

代码见时间复杂度优化

0-1背包问题更多变种

  • 多重背包问题:每个物品不止1个,有num(i)个
  • 多维费用背包问题:要考虑物品的体积和重量两个维度?
  • 物品间加入更多约束
  • 物品间可以互相排斥;也可以互相依赖
  • 完全背包问题:每个物品可以无限使用

7 面试中的0-1背包问题416.分割等和子集

题干

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意:

  • 每个数组中的元素不会超过 100
  • 数组的大小不会超过 200

上面连个条件的含义是:

  • 所有数字和最大为20000
  • 背包最大为100^2=10000
  • 时间复杂度为:n*sum/2 = 100*10000 = 100万

示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5][11]
 
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.

题目分析

典型的背包问题,在n个物品中选出一定物品,填满sum/2的背包

F( n , C ) 考虑将n个物品填满容量为C的背包

F ( i , c )  =   F( i-1 , c ) || F( i-1 , c - w(i) )

时间复杂度:O( n * sum/2 ) = O( n * sum )

代码实现

  • 纯递归实现
  • 递归+记忆数组去除递归重复子问题
  • 动态递归实现,也是上面图示的实现
// 从n个物体中选择几个,使得其和等于sum/2
class Solution {/*** nums[i]表示第i个物体的重量** @param nums 存放物体重量的数组* @return 是否存在等和子集*/public boolean canPartition(int[] nums) {int sum = 0;for (int i = 0; i < nums.length; i++) {assert nums[i] > 0;sum += nums[i];}// 能整除2才能分成两个相等的子集if (sum % 2 != 0) {return false;}// 数组长度int n = nums.length;// 背包的容量,除以2是因为要看是否可以划分为两个相等的子集int C = sum / 2;// memo[i]表示背包容量为i时,现有的物体能否将其填满boolean[] memo = new boolean[C + 1];// 数组初始化为falseArrays.fill(memo, false);// 只放入第一个物体(编号为0)时,正好能把背包填满的情况设置为True,否则设置为Falsefor (int i = 0; i < C + 1; i++) {memo[i] = (nums[0] == i);}// 背包问题的核心过程for (int i = 1; i < n; i++) { // i代表物体编号,下面依次再放入1~n-1号物体for (int j = C; j >= nums[i]; j--) { // j代表当前背包剩余的容量// 容量为j的情况下放入nums[i]。每一轮都要从C到nums[i]刷新一遍~memo[j] = memo[j] || memo[j - nums[i]]; // 如果之前j处的容量(可能是初始化时设置地)或者减去nums[i]后剩余的容量已经能被填满,则说明可以分割}}return memo[C];}
}

类似问题

  • 322.零钱兑换
  • 377.组合总和 Ⅳ
  • 474.一和零们
  • 139.单词分割
  • 494.目标和

322.零钱兑换

class Solution {public int coinChange(int[] coins, int amount) {if (coins.length == 0) {return -1;}//dp[j]代表当钱包的总价值为j时,所需要的最少硬币的个数int[] dp = new int[amount + 1];Arrays.fill(dp, Integer.MAX_VALUE);// 钱包总价值为0时,需要的最少硬币数肯定是0,这是动态规划的开始dp[0] = 0;//i代表可以使用的硬币索引,i=2代表只在第0个,第1个,第2个这三个硬币中选择硬币for (int i = 0; i < coins.length; i++) {/*** 	当外层循环执行一次以后,说明在只使用前i-1个硬币的情况下,各个钱包的最少硬币个数已经得到,*  如果有些钱包的值还是无穷大,说明在仅使用前i-1个硬币的情况下,不能凑出钱包的价值* 	现在开始再放入第i个硬币,要想放如coins[i],钱包的价值必须满足j>=coins[i],所以在开始放入第i个硬币时,j从coins[i]开始*/for (int j = coins[i]; j <= amount; j++) {/*** 	如果钱包当前的价值j仅能允许放入一个coins[i],那么就要进行权衡,以获得更少的硬币数* 	  如果放入0个:此时钱包里面硬币的个数保持不变: v0=dp[j]* 	  如果放入1个:此时钱包里面硬币的个数为:		v1=dp[j-coins[i]]+1*   【前提是dp[j-coins[i]]必须有值,如果dp[j-coins[i]]是无穷大,说明无法凑出j-coins[i]价值的钱包,那么把coins[i]放进去以后,自然也凑不出dp[j]的钱包】* 	所以,此时当钱包价值为j时,里面的硬币数目为 dp[j]=min{v0,v1}* 	如果钱包当前价值j能够放入2个coins[i],就要再进行一次权衡* 		如果不放人第2个coins[i],此时钱包里面硬币数目为,v1=dp[j]=min{v0,v1}* 		如果放入第2个coins[i],  此时钱包里面硬币数目为,v2=dp[j-coins[i]]+1* 	所以,当钱包的价值为j时,里面的硬币数目为dp[j]=min{v1,v2}=min{v0,v1,v2}* 	......如此循环下去....* 	钱包价值j能允许放入3个,4个.........w[i],不断更新dp[j],最后得到在仅使用前i个硬币(各个硬币的数目是不确定地)的时候,每个钱包里的最少硬币数目** 	好好体会下随着j增加,j逐渐容纳下多个coins[i]的过程*/if (dp[j - coins[i]] != Integer.MAX_VALUE) {dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);}}}if (dp[amount] != Integer.MAX_VALUE) {return dp[amount];}return -1;}
}

139.单词分割

/*** Word Break* 动规, 时间复杂度O(n^2), 空间复杂度O(n)*/
class Solution {public boolean wordBreak(String s, List<String> dict) {// 长度为n的字符串有n+1个隔板boolean[] memo = new boolean[s.length() + 1];// 空字符串memo[0] = true;for (int i = 1; i <= s.length(); ++i) {for (int j = i - 1; j >= 0; --j) {if (memo[j] && dict.contains(s.substring(j, i))) {memo[i] = true;break;}}}return memo[s.length()];}
}

8 LIS(最长子序列)问题 300.最长上升子序列Longest Increasing Subsequence

题干

给定一个无序的整数数组,找到其中最长上升子序列的长度。示例:输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?注意:
注意1:什么是子序列?
注意2:什么是上升?
注意3:一个序列可能有多个最长上升子序列;但这个最长的长度只有1个

暴力解法

选择所有的子序列进行判断:O( (2^n) * n )

动态规划解法

LIS(i) 表示以第 i 个数字为结尾的最长上升子序列的长度

上面的说法等效于:

LIS(i) 表示 [0...i] 的范围内,选择数字nums[i]可以获得的最长上升子序列的长度

nums[]表示存放数字的数组,这里认为是总的序列

LIS(i) = max( 1 + LIS(j) if nums[i] > nums[j] )j<i

上面表达式的含义:从i处往前遍历,遍历到的位置下标为j(j<i),如果nums[j]小于nums[i],且LIS[j]的值不小于LIS[i],则把LIS[i]的值更新为LIS[j]+1,否则保持LIS[i]的值不变,伪代码如下:

// 第1个元素不用考虑了,其最长子序列一定是1,已经初始化好了
for (int i = 1; i < nums.length; i++) {for (int j = 0; j < i; j++) {if (nums[j] < nums[i]) {// 如果i前面的值有大于i处的值的,更新子序列长度// memo[i]表示当前i处的子序列长度,1+memo[i]表示考虑j的情况下最长子序列长度加1// memo[i]在 `j in 0~i`的循环中可能已经被跟新多次了,不再是初始值1了memo[i] = Math.max(memo[i], 1 + memo[j]);}}
}

最长上升子序列举例

例子1:

最长上升子序列举例

  • 初始化所有的LIS[i]=1,表示每个下标处的元素至少可以自己组成一个长度为1的上升序列
  • i=0, nums[0]=10,前面没有元素了,所以保留初始值LIS[0]=1
  • i=1, nums[1]=9,前面元素都比9大,所以保留初始值LIS[1]=1
  • i=2, nums[2]=2,前面元素都比2大,所以保留初始值LIS[2]=1
  • i=3, nums[3]=5, 遍历前面元素,只有nums[2]=2<nums[3]=5,所以LIS[3]=max{初始值1, LIS[2]+1}=LIS[2]+1=1+1=2
  • i=4, nums[4]=3, 遍历前面元素,只有nums[2]=2<nums[4]=3,所以LIS[4]=max{初始值1, LIS[2]+1}=LIS[2]+1=1+1=2
  • i=5, nums[5]=7, 遍历前面元素,nums[2]=2、nums[3]=5、nums[4]=3都要比nums[5]=7要小,所以LIS[5]=max{初始值1, LIS[2]+1, LIS[3]+1, LIS[4]+1}=LIS[4]+1=2+1=3
  • i=6, nums[6]=101, 遍历前面元素,nums[2]=2、nums[3]=5、nums[4]=3、nums[5]=7都要比nums[6]=101要小,所以LIS[6]=max{初始值1, LIS[2]+1, LIS[3]+1, LIS[4]+1, LIS[5]+1}=LIS[5]+1=3+1=4
  • i=7, nums[7]=18, 遍历前面元素,nums[2]=2、nums[3]=5、nums[4]=3、nums[5]=7都要比nums[7]=18要小,所以LIS[7]=max{初始值1, LIS[2]+1, LIS[3]+1, LIS[4]+1, LIS[5]+1}=LIS[5]+1=3+1=4
  • 总上可知最长子序列为4

例子2:

  • 最长上升必须列的值不一定是递增地
  • 每个位置的最长上升子序列的值不一定是前面的加1

最长上升子序列举例2

  • 初始化所有的LIS[i]=1,表示每个下标处的元素至少可以自己组成一个长度为1的上升序列
  • i=0, nums[0]=10,前面没有元素了,所以保留初始值LIS[0]=1
  • i=1, nums[1]=15, 遍历前面元素,只有nums[0]=10<nums[1]=15, 所以LIS[1]=max(初始值1, LIS[0]+1)=LIS[0]+1=2
  • i=2, nums[2]=10, 遍历前面元素,nums[0]=10、nums[1]=15都比nums[2]=20要小,所以LIS[2]=max{初始值1, LIS[0]+1, LIS[1]+1}=LIS[1]+1=2+1=3
  • i=3, nums[3]=11, 遍历前面元素,只有nums[0]=10<nums[3]=11, 所以LIS[3]=max(初始值1, LIS[0]+1)=LIS[0]+1=2
  • i=4, nums[4]=9,前面元素都比9大,所以保留初始值LIS[4]=1
  • i=5,nums[5]=101,遍历前面元素,nums[0]=10、nums[1]=15、nums[2]=10、nums[3]=11、nums[4]=9都比nums[5]=101要小,所以LIS[5]=max{初始值1, LIS[0]+1, LIS[1]+1, LIS[2]+1, LIS[3]+1, LIS[4]+1}=LIS[2]+1=3+1=4
  • 总上可知最长子序列为4

动态规划实现

代码实现

public class Solution {public int lengthOfLIS(int[] nums) {if (nums.length == 0) {return 0;}// memo[i]表示0~i区间内以nums[i]为结尾的的最长上升子序列的长度int[] memo = new int[nums.length];// 初始化Arrays.fill(memo, 1);// 第1个元素不用考虑了,其最长子序列一定是1,已经初始化好了for (int i = 1; i < nums.length; i++) {for (int j = 0; j < i; j++) {// 注意小于的才算上升,等于地不算if (nums[j] < nums[i]) {// 如果i前面的值有大于i处的值的,更新子序列长度// memo[i]表示当前i处的子序列长度,1+memo[i]表示考虑j的情况下最长子序列长度加1// memo[i]在 `j in 0~i`的循环中可能已经被跟新多次了,不再是初始值1了memo[i] = Math.max(memo[i], 1 + memo[j]);}}}int maxLIS = 1;// 从最长子序列数组中取最大地返回for (int i = 0; i < nums.length; i++) {maxLIS = Math.max(maxLIS, memo[i]);}return maxLIS;}public static void main(String[] args) {int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};int maxLIS = new Solution().lengthOfLIS(nums);System.out.println(maxLIS);// 4}
}

类似题目:376. 摆动序列 wiggle subsequence

关键两点:一是去除重复元素;二是判断子序列是否为摆动序列。后面用贪心来解决其实会更简单

class Solution {// leetcode第27题,删除连续的元素,只保留一个。// 主要是为了应付[0,0,0,0]这样的用例private int removeDuplicates(int[] nums) {int k = 0;for(int i = 1; i< nums.length; i++){if(nums[i] != nums[k]){ // 有序数组,所以直接和前面的比较就知道有没有排序了nums[++k] = nums[i];}}return k + 1; // k从0开始,所以数组长度最后要+1}// 高效判断符号是否相反private boolean oppositeSigns(int x, int y){// 位运算判断两个数是否符号相反return (x ^ y) < 0;}public int wiggleMaxLength(int[] nums) {int n = nums.length;if(n == 0){return n;}n = removeDuplicates(nums);// LS:longest wiggle sequence 最长子序列int[] LWS = new int[n];Arrays.fill(LWS, 2);// 只有一个元素时,摆动序列为1LWS[0] = 1;for(int i = 2; i < n; i++){for(int j = 1; j < i; j++){int diff1 = nums[i] - nums[j];int diff2 = nums[j] - nums[j - 1];// 两个差正负相反,才算一个摆动序列if(oppositeSigns(diff1, diff2)){LWS[i] = Math.max(LWS[i], LWS[j] + 1);}}}int max = 0;for(int lws : LWS){if(lws > max){max = lws;}}return max;}
}

354. 俄罗斯套娃信封问题

/************************************************************ @Description : T354_信封嵌套问题* https://leetcode-cn.com/problems/russian-doll-envelopes/* 参考第300号问题(最长上升子序列),先把一个维度排序后,堆第二个维度排序即可* @author      : 梁山广(Liang Shan Guang)* @date        : 2020/2/5 17:58* @email       : liangshanguang2@gmail.com***********************************************************/
package C12_动态规划.T354_信封嵌套问题;import java.util.Arrays;
import java.util.Comparator;public class Solution {public int maxEnvelopes(int[][] envelopes) {int N = envelopes.length;if (N == 0) {return 0;}int[] LIS = new int[N];Arrays.fill(LIS, 1);// 按照长度进行排序,然后下面比较信封宽度即可!这步很关键!!Arrays.sort(envelopes, Comparator.comparingInt(o -> o[0]));for (int i = 0; i < N; i++) {for (int j = 0; j < i; j++) {// i前面的j信封长和宽都要比i信封的小,说明是上升序列的一部分,加入到i处的LISif (envelopes[j][0] < envelopes[i][0] && envelopes[j][1] < envelopes[i][1]) {// 不断用更大的LIS值来更新i处的LISLIS[i] = Math.max(LIS[i], LIS[j] + 1);}}}int max = 0;for (int lis : LIS) {if (lis > max) {max = lis;}}return max;}
}

9 LCS、最短路、求动态规划的具体解以及更多

LCS 最共公共子字符串问题 LeetCode 1143号问题

非常类似的问题还有72.编辑距离

最长子序列

最长子序列2

最长子序列3

class Solution {public int longestCommonSubsequence(String str1, String str2) {if (str1 == null || str2 == null) {return 0;}int m = str1.length();int n = str2.length();if (m == 0 || n == 0) {return 0;}// dp[i][j]代表chs1[0...i]和chs2[0...j]的最长公共子序列.注意此时的下标从1开始int[][] dp = new int[m + 1][n + 1];// 给非第1行和第1列的中间元素赋值for (int i = 1; i <= m; i++) {for (int j = 1; j <= n; j++) {if (str1.charAt(i - 1) == str2.charAt(j - 1)) {// 字符相等地话直接等于前一个字符串的dp值加1dp[i][j] = dp[i - 1][j - 1] + 1;} else {// 比较相邻两种情况dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);}}}return dp[m][n];}
}

dijkstra单源最短路径算法也是动态规划

dijkstra单源最短路径算法也是动态规划

动态规划问题的具体解

用一个pre数组记录前后关系,然后逆序向前查找即可,图算法里面很常用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_102311.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C嘎嘎~~ 【初识C++ 下篇】

初识C 下篇 1.引用1.1引用的概念1.2引用的特点1.3常引用1.4引用使用的场景1.5引用和指针的区别 2.指针空值 --- nullptr3.内联函数3.1 内联函数的概念3.2内联函数的使用场景3.3内联函数的特性 1.引用 1.1引用的概念 相信大家小时候&#xff0c; 肯定有小名、绰号、亲朋好友的昵…

Zuul源码解析(二)

Zuul 的自动配置 ZuulProxyAutoConfiguration 如何触发 如上图&#xff0c;在 spring.factory 中配置 ZuulProxyAutoConfiguration 自动配置了&#xff0c;直接点进去 如上图所示&#xff0c;发现这有个条件注解&#xff0c;需要有 org.springframework.cloud.netflix.zuul.Zu…

“支付+SaaS”赋能传统产业数字化转型

易观&#xff1a;传统支付业务利润空间在政策监管和市场竞争下不断被压缩&#xff0c;多家上市支付机构逐步将业务经营重点转移切入到企业交易环节&#xff0c;为企业提供包括SaaS服务、行业解决方案、营销服务及金融科技服务等在内的企业服务收入成为新的增长点。 伴随着“十四…

Linux网络服务之yum仓库

目录 一、yum仓库简介二. ftp搭建yum源三. 搭建国内在线源四. 本地源和在线yum同时使用五. 通过缓存的方式保存所下载的软件包六 . 制作yum仓库 一、yum仓库简介 yum是一个基于RPM包&#xff08;是Red-Hat Package Manager红帽软件包管理器的缩写&#xff09;构建的软件更新机…

Java入门教程||Java 继承||Java 重写(Override)与重载(Overload)

Java 继承 继承是所有 OOP 语言和 Java 语言不可缺少的组成部分。 继承是 Java 面向对象编程技术的一块基石&#xff0c;是面向对象的三大特征之一&#xff0c;也是实现软件复用的重要手段&#xff0c;继承可以理解为一个对象从另一个对象获取属性的过程。 如果类 A 是类 B …

Automa自动化爬取文本(一)

目录 介绍 下载地址 安装教程 爬取百度热搜 介绍 Automa 是一个免费、开源的 Chrome 扩展&#xff0c;它通过目前流行的 No Code 无代码方式&#xff0c;只需要拖拽模块就实现了浏览器自动化&#xff0c;比如自动填写表格、执行重复性任务。 在工作中&#xff0c;如果我们…

learn_C_deep_5 (语句和表达式的概念、if语句的多种语法结构、C语言有没有布尔类型、“零值”的比较)

目录 语句和表达式的概念 if语句的多种语法结构 注释的便捷方法&#xff08;环境vs&#xff09; if语句执行的过程 逻辑与&& 逻辑或|| 运算关系的顺序 ​编辑 C语言有没有布尔类型 C99标准 sizeof(bool)的值为多少&#xff1f; _Bool原码 BOOL、TRUE、…

ERTEC200P-2 PROFINET设备完全开发手册(9-2)

9.2 运行AC1/AC4参考代码 修改源代码usrapp_cfg.h的宏为 #define EXAMPL_DEV_CONFIG_VERSION 44 编译后下载到评估板运行AC4示例程序 在TIA中导入GSDML-V2.35-Siemens-ERTEC200pApp44-20210623.xml。新建项目&#xff0c;添加PLC和Devkit设备。 按照如下图所示配置模块&am…

基于vfw的局域网语音聊天室系统源码论文

语音视频聊天 UDP套接字的运用 在实现语音视频聊天时,采用的是基于UDP套接字的点对点模式,而UDP面向的是无连接的数据服务,其套接字的使用如图10所示。 图10 UDP套接字的使用 视频的捕获 利用VFW接口&#xff0c;视频捕获可以分为以下几个步骤&#xff1a; 建立视频采集窗口…

stm32在SDIO模式下SD写入错误的问题

1、问题描述 使用FAT32 f_write 多次执行写操作时&#xff0c;会报FR_DISK_ERR错误&#xff0c;而且是刚开始写不报错&#xff0c;写几次后会一直报错。 设断点跟踪到HAL_SD_WriteBlocks中&#xff0c;在调用SDMMC_CmdWriteMultiBlock时&#xff0c;会报SDMMC_ERROR_TX_UNDERR…

【MYSQL】Java的JDBC编程(idea连接数据库)

1. 配置 &#xff08;1&#xff09;新建一个项目 &#xff08;2&#xff09;Build System 那里选择Maven,下一步Create &#xff08;3&#xff09;配置pom.xml文件 首先查看自己的MYSQL版本&#xff1a;进入MySQL命令窗口 我的MYSQL版本是8.0版本的. 下一步&#xff0c;…

C/C++占位符,%x和%p的区别

遇到的问题 今天遇到了一个很奇怪的问题&#xff0c;当使用malloc分配了一个堆空间后&#xff0c;分别尝试用cout和printf尝试打印该地址&#xff0c;出现了两个地址不一样的情况&#xff1a; int *pp (int*)malloc(10*sizeof(int)); *pp 1234; cout << pp << …

常用数据加密

一、加密和解密 1、 加密 数据加密的基本过程&#xff0c;就是对原来为明 的文件或数据按某种算法进行处理&#xff0c;使其成为不可读的一段代码&#xff0c;通常称为 “密文”。通过这样的途径&#xff0c;来达到保护数据不被 非法人窃取、阅读的目的。 2、解密 加密的逆…

2023年,人工智能和数据训练呈现哪些新趋势?

最近&#xff0c;多才多艺的“全能网友”ChatGPT一次次火出圈。未来人工智能将以多快的速度、发展到何种地步&#xff1f;人们已经开始坐下来认真地探讨和思考。 我们生活在这个日新月异的时代。新的一年&#xff0c;人工智能的真正能力和应用场景又发生了哪些演进&#xff1f;…

Java性能优化之序列化优化

1、Java 序列化及其缺陷 Java 提供了一种序列化机制&#xff0c;这种机制能够将一个对象序列化为二进制形式&#xff08;字节数组&#xff09;&#xff0c;用于写入磁盘或输出到网络&#xff0c;同时也能从网络或磁盘中读取字节数组&#xff0c;反序列化成对象&#xff0c;在程…

java Excel清除表格条件格式规则

© Ptw-cwl 目录 文章目录 目录Excel清除表格条件格式规则1.开始 -> 条件格式2.条件格式 -> 清除规则3.管理规则也能删除 代码报java.lang.IllegalArgumentException: Specified CF index 43 is outside the allowable range (0..42)如何解决源码 Excel清除表格条件…

GitHub 开源神器 Bark模型,让文本转语音更简单

今天跟大家分享一个文本转语音的开源模型&#xff1a;Bark Bark 是由Suno创建的基于转换器的文本到音频模型。Bark 可以生成高度逼真的多语言语音以及其他音频 - 包括音乐、背景噪音和简单的音效。该模型还可以产生非语言交流&#xff0c;如大笑、叹息和哭泣。 该项目刚开源不…

JQuery-原理示意图-- 选择器-- 选择器综合代码--jQuery 的 DOM 操作--增删改查节点--常用遍历节点方法--多选框应用--全部综合代码

目录 JQuery 基本介绍 jQuery 的原理示意图 JQuery 基本开发步骤 说明: jQuery简单示例 jQuery 对象和 DOM 对象 什么是 jQuery 对象 DOM 对象转成 jQuery 对象 应用实例 jQuery 对象转成 DOM 对象 代码演示 jQuery 选择器 jQuery 选择器介绍 jQuery 选择器的优…

根据cadence设计图学习硬件知识 day03 了解 一些芯片 和 数据手册下载的地方

1. MT53D512M32D2DS 芯片&#xff08;动态随机存取存储器&#xff09;的技术指标 1.1 16n Prefetch (预加载) (n --芯片位宽) DDR 体系 链接&#xff1a;DDR扫盲—-关于Prefetch(预取)与Burst(突发)的深入讨论_ddr prefetch_qq_25814297-npl的博客-CSDN博客 1.2 每个通…

webhub123 前端技术社区和技术交流学习网站导航

整理了学习前端技术可以参考学习和技术交流的一些网站集合&#xff0c;全部收录到 webhub123 前端技术社区和技术交流学习网站导航http://​www.webhub123.com/#/home/detail?projectHashid30929575&ownerUserid22053727 整理后的效果如下&#xff0c;我们已经按照不同类…