将原问题拆解成若干个子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。
最优子结构:通过求子问题的最优解,可以获得原问题的最优解。且有重叠子结构,才能用记忆化搜索和动态规划
第一二节,动态规划
70. 爬楼梯(简单)
自己的想法
方法一:递归(自上而下的解决问题)
超时,也可以使用记忆化搜索解决。
class Solution {
public int climbStairs(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
}
方法二:动态规划(自下而上)
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35.1 MB, 在所有 Java 提交中击败了82.77%的用户
class Solution {
public int climbStairs(int n) {
int a = 0;
int b = 1;
int c = 0;
while (n-- != 0) {
c = a + b;
a = b;
b = c;
}
return b;
}
}
120. 三角形最小路径和(中等)
看题解后
方法一:动态规划(从上向下)
使用 dp 数组记录从 dp[0][0] 走到 dp[i][j] 所需的最小值。
- 执行用时:3 ms, 在所有 Java 提交中击败了75.08%的用户
- 内存消耗:38.7 MB, 在所有 Java 提交中击败了22.26%的用户
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
// 获取三角形高度
int length = triangle.size();
// 够建记忆数组,记录从 dp[0][0] 走到 dp[i][j] 所需的最小值
int[][] dp = new int[length][length];
// 初始化顶部
dp[0][0] = triangle.get(0).get(0);
for (int i = 1; i < length; i++) {
// 为了防止越界,第一个特殊处理
dp[i][0] = dp[i - 1][0] + triangle.get(i).get(0);
for (int j = 1; j < i; j++) {
// 当前值 + (dp[i - 1][j]、dp[i - 1][j - 1] 的最小值)求出到 dp[i][j] 所需的最小值
dp[i][j] = triangle.get(i).get(j) + Math.min(dp[i - 1][j], dp[i - 1][j - 1]);
}
// 最后一个特殊处理,因为 dp[i - 1][j] 为 0
dp[i][i] = dp[i - 1][i - 1] + triangle.get(i).get(i);
}
// 求出到底部的最短路径
int min = dp[length - 1][0];
for (int i = 1; i < length; ++i) {
min = Math.min(min, dp[length - 1][i]);
}
return min;
}
}
方法二:空间优化
将方法一的二维数组,优化为一维数组,每次循环进行覆盖之前的值。
- 执行用时:2 ms, 在所有 Java 提交中击败了94.40%的用户
- 内存消耗:38.8 MB, 在所有 Java 提交中击败了6.42%的用户
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
// 获取三角形高度
int length = triangle.size();
int[] dp = new int[length];
// 初始化顶部
dp[0] = triangle.get(0).get(0);
for (int i = 1; i < length; i++) {
// 最后一个特殊处理,因为 dp[i] 为 0
dp[i] = dp[i - 1] + triangle.get(i).get(i);
for (int j = i - 1; j > 0; j--) {
dp[j] = triangle.get(i).get(j) + Math.min(dp[j], dp[j - 1]);
}
// 为了防止越界,第一个特殊处理
dp[0] += triangle.get(i).get(0);
}
// 求出到底部的最短路径
int min = dp[0];
for (int i = 1; i < length; ++i) {
min = Math.min(min, dp[i]);
}
return min;
}
}
方法三:原地修改
不适用额外数组,在集合上进行操作
- 执行用时:7 ms, 在所有 Java 提交中击败了12.29%的用户
- 内存消耗:38.9 MB, 在所有 Java 提交中击败了5.18%的用户
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
// 获取三角形高度
int length = triangle.size();
// 初始化顶部
for (int i = 1; i < length; i++) {
// 为了防止越界,第一个特殊处理
triangle.get(i).set(0, triangle.get(i - 1).get(0) + triangle.get(i).get(0));
for (int j = 1; j < i; j++) {
// 当前值 + (triangle[i - 1][j]、triangle[i - 1][j - 1] 的最小值)求出到 triangle[i][j] 所需的最小值
triangle.get(i).set(j, triangle.get(i).get(j) + Math.min(triangle.get(i - 1).get(j), triangle.get(i - 1).get(j - 1)));
}
// 最后一个特殊处理,因为 triangle[i - 1][j] 为 0
triangle.get(i).set(i, triangle.get(i - 1).get(i - 1) + triangle.get(i).get(i));
}
// 求出到底部的最短路径
int min = triangle.get(length - 1).get(0);
for (int i = 1; i < length; ++i) {
min = Math.min(min, triangle.get(length - 1).get(i));
}
return min;
}
}
方法四:动态规划(从下往上)
- 执行用时:2 ms, 在所有 Java 提交中击败了94.40%的用户
- 内存消耗:38.7 MB, 在所有 Java 提交中击败了12.76%的用户
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
// 获取三角形高度
int length = triangle.size();
int[] dp = new int[length + 1];
// 从下往上遍历
for (int i = length - 1; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[j] = triangle.get(i).get(j) + Math.min(dp[j], dp[j + 1]);
}
}
return dp[0];
}
}
64. 最小路径和(中等)
自己的想法
方法一:动态规划
- 执行用时:3 ms, 在所有 Java 提交中击败了87.31%的用户
- 内存消耗:41.4 MB, 在所有 Java 提交中击败了9.17%的用户
class Solution {
public int minPathSum(int[][] grid) {
// 初始化行
for (int i = 1; i < grid[0].length; i++) {
grid[0][i] += grid[0][i - 1];
}
// 初始化列
for (int i = 1; i < grid.length; i++) {
grid[i][0] += grid[i - 1][0];
}
for (int i = 1; i < grid.length; i++) {
for (int j = 1; j < grid[0].length; j++) {
grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]);
}
}
return grid[grid.length - 1][grid[0].length - 1];
}
}
看题解后
方法二:动态规划
不修改原数组,使用一维数组记录路径值。
- 执行用时:2 ms, 在所有 Java 提交中击败了97.98%的用户
- 内存消耗:41.3 MB, 在所有 Java 提交中击败了31.21%的用户
class Solution {
public int minPathSum(int[][] grid) {
int n = grid[0].length;
int[] state = new int[n];
state[0] = grid[0][0];
// 给第一行初始化
for (int i = 1; i < n; i++) {
state[i] = grid[0][i] + state[i - 1];
}
for (int i = 1; i < grid.length; i++) {
int j = 0;
// 计算到达 grid[i][j] 的最小路径
state[j] = state[j] + grid[i][j];
for (j = 1; j < n; j++) {
// state[j - 1] 存储的是到达 grid[i][j - 1] 的最小路径。
// state[j] 存储的是到达 grid[i - 1][j] 的最小路径。
state[j] = Math.min(state[j - 1], state[j]) + grid[i][j];
}
}
return state[n - 1];
}
}
第三节,发现重叠子问题
343. 整数拆分(中等)
自己的想法
这个题和切绳子一样。
方法一:递归(超时)
n 可以拆分为 (1, n - 1)、(2, n - 2)、(3, n - 3) ... (i, n - i),对于 (n - i) 可以拆分也可以不拆分。
class Solution {
public int integerBreak(int n) {
if (n == 1) {
return 1;
}
int max = 0;
for (int i = 1; i <= n / 2; i++) {
// 不拆分
max = Math.max(max, i * (n - i));
// 拆分
max = Math.max(max, i * integerBreak(n - i));
}
return max;
}
}
方法二:记忆化搜索
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35.2 MB, 在所有 Java 提交中击败了62.33%的用户
class Solution {
int[] dp;
public int integerBreak(int n) {
dp = new int[n + 1];
return dp(n);
}
public int dp(int n) {
if (n == 1) {
return 1;
}
if (dp[n] != 0) {
return dp[n];
}
int max = 0;
for (int i = 1; i <= n / 2; i++) {
max = Math.max(max, i * (n - i));
max = Math.max(max, i * dp(n - i));
}
dp[n] = max;
return max;
}
}
方法三:动态规划
对于 n 可以拆分为:1*dp[n-1]
,2*dp[n-2]
,3*dp[n-3]
...也可以不对 n-1,n-2...进行拆分,所以对于 n 有两种情况:i*(n-i)
和 i*dp[n-i]
。
- 执行用时:1 ms, 在所有 Java 提交中击败了65.26%的用户
- 内存消耗:34.9 MB, 在所有 Java 提交中击败了93.84%的用户
class Solution {
public int integerBreak(int n) {
// 长度为n的最佳的拆分方案
int[] dp = new int[n + 1];
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], j * (i - j));
dp[i] = Math.max(dp[i], j * dp[i - j]);
}
}
return dp[n];
}
}
代码优化:二层循环可以优化为
for (int j = 1; j < i / 2 + 1; j++)
或
for (int j = 1; j <= i / 2; j++)
方法四:贪心
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35.2 MB, 在所有 Java 提交中击败了43.04%的用户
class Solution {
public int integerBreak(int n) {
if (n <= 3) {
return (n - 1) * 1;
}
int result = 1;
if (n % 3 == 1) {
result *= 4;
n -= 4;
} else if (n % 3 == 2) {
result *= 2;
n -= 2;
}
return (int)(result * Math.pow(3, n / 3));
}
}
279. 完全平方数(中等)
看题解后
方法一:递归(超时)
对于每个数都进行拆分,只要 n - i * i大于 0(i>0),则向下递归。
class Solution {
public int numSquares(int n) {
// 默认全部拆分为 1
int min = n;
// 开始分割
for (int i = 1; n - i * i >= 0; i++) {
// 将其进行分割
min = Math.min(min, numSquares(n - i * i) + 1);
}
return min;
}
}
方法二:记忆化搜索
- 执行用时:86 ms, 在所有 Java 提交中击败了20.12%的用户
- 内存消耗:38.6 MB, 在所有 Java 提交中击败了10.46%的用户
class Solution {
int[] dp;
public int numSquares(int n) {
dp = new int[n + 1];
return dp(n);
}
public int dp(int n) {
// 默认全部拆分为 1
int min = n;
if (dp[n] != 0) {
return dp[n];
}
// 开始分割
for (int i = 1; n - i * i >= 0; i++) {
// 将其进行分割
min = Math.min(min, dp(n - i * i) + 1);
}
dp[n] = min;
return min;
}
}
方法三:动态规划
- 执行用时:41 ms, 在所有 Java 提交中击败了76.57%的用户
- 内存消耗:37.3 MB, 在所有 Java 提交中击败了90.60%的用户
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
dp[i] = i;
for (int j = 1; i - j * j >= 0; j++) {
// 将其进行分割
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
}
方法四:贪心和数学(不会)
91. 解码方法
自己的想法
方法一:记忆化搜索(从后往前)
递归(超时),改为记忆化搜索。
- 执行用时:125 ms, 在所有 Java 提交中击败了9.46%的用户
- 内存消耗:36.7 MB, 在所有 Java 提交中击败了51.85%的用户
class Solution {
int[] str;
int[] dp;
public int numDecodings(String s) {
int n = s.length();
if (n == 0) {
return 0;
}
str = new int[n];
dp = new int[n];
// 转为数字数组
for (int i = 0; i < n; i++) {
str[i] = s.charAt(i) - '0';
}
return dp(n - 1);
}
public int dp(int i) {
// 遍历完,则返回
if (i == -1) {
return 1;
}
if (dp[i] != 0) {
return dp[i];
}
int value = 0;
// 当前值不为0,则可以拆分
if (str[i] != 0) {
value += dp(i - 1);
}
// 前一个值不为0且组合起来<=26,则可以拆分
if (i > 0 && str[i - 1] != 0 && (str[i - 1] * 10 + str[i] <= 26)) {
value += dp(i - 2);
}
dp[i] = value;
return value;
}
}
方法二:记忆化搜索(从前往后)
- 执行用时:13 ms, 在所有 Java 提交中击败了9.46%的用户
- 内存消耗:36.5 MB, 在所有 Java 提交中击败了91.52%的用户
class Solution {
int[] str;
int[] dp;
int n;
public int numDecodings(String s) {
n = s.length();
if (n == 0) {
return 0;
}
str = new int[n];
dp = new int[n];
// 转为数字数组
for (int i = 0; i < n; i++) {
str[i] = s.charAt(i) - '0';
}
return dp(0);
}
public int dp(int i) {
// 遍历完,则返回
if (i == n) {
return 1;
}
if (dp[i] != 0) {
return dp[i];
}
int value = 0;
// 当前值不为0,则可以拆分
if (str[i] != 0) {
value += dp(i + 1);
}
// 当前值不为0且组合起来<=26,则可以拆分
if (i < n - 1 && str[i] != 0 && (str[i] * 10 + str[i + 1] <= 26)) {
value += dp(i + 2);
}
dp[i] = value;
return value;
}
}
方法三:动态规划(从前往后)
- 执行用时:1 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:36.7 MB, 在所有 Java 提交中击败了41.35%的用户
class Solution {
public int numDecodings(String s) {
int n = s.length();
int[] str = new int[n + 1];
int[] dp = new int[n + 1];
// 转为数字数组
for (int i = 1; i <= n; i++) {
str[i] = s.charAt(i - 1) - '0';
}
// 类似于记忆化搜索的 -1
dp[0] = 1;
for (int i = 1; i <= n; i++) {
if (str[i] != 0) {
dp[i] += dp[i - 1];
}
// 判断等不能向前进行两组合
if (i > 1 && str[i - 1] != 0 && (str[i - 1] * 10 + str[i] <= 26)) {
dp[i] += dp[i - 2];
}
}
return dp[n];
}
}
方法四:动态规划(从后往前)
- 执行用时:1 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:36.8 MB, 在所有 Java 提交中击败了27.58%的用户
class Solution {
public int numDecodings(String s) {
int n = s.length();
int[] str = new int[n + 1];
int[] dp = new int[n + 1];
// 转为数字数组
for (int i = 0; i < n; i++) {
str[i] = s.charAt(i) - '0';
}
// 类似于记忆化搜索的 n
dp[n] = 1;
for (int i = n - 1; i >= 0; i--) {
if (str[i] != 0) {
dp[i] += dp[i + 1];
}
// 判断等不能向后进行两组合
if (i < n - 1 && str[i] != 0 && (str[i] * 10 + str[i + 1] <= 26)) {
dp[i] += dp[i + 2];
}
}
return dp[0];
}
}
方法五:动态规划(从前往后,空间压缩)
- 执行用时:1 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:36.7 MB, 在所有 Java 提交中击败了38.01%的用户
class Solution {
public int numDecodings(String s) {
int n = s.length();
int[] str = new int[n + 1];
// 转为数字数组
for (int i = 1; i <= n; i++) {
str[i] = s.charAt(i - 1) - '0';
}
// 只记录中间变量,节约空间
int a = 1, b = 1, c = 0;
for (int i = 1; i <= n; i++) {
// 每次初始化 c
c = 0;
if (str[i] != 0) {
c += b;
}
// 判断等不能向前进行两组合
if (i > 1 && str[i - 1] != 0 && (str[i - 1] * 10 + str[i] <= 26)) {
c += a;
}
a = b;
b = c;
}
return c;
}
}
62. 不同路径
自己的想法
方法一:记忆化搜索
递归(超时)。
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35 MB, 在所有 Java 提交中击败了86.47%的用户
class Solution {
int[][] dp;
int m, n;
// 使用递归
public int uniquePaths(int m, int n) {
this.m = m;
this.n = n;
dp = new int[m][n];
return dp(m - 1, n - 1);
}
public int dp(int i, int j) {
// 越界则结束
if (i < 0 || i == m || j < 0 || j == n) {
return 0;
}
// 到终点
if (i == 0 && j == 0) {
return 1;
}
// 如果不为 0,则该节点走过
if (dp[i][j] != 0) {
return dp[i][j];
}
int tmp = dp(i - 1, j) + dp(i, j - 1);
dp[i][j] = tmp;
return tmp;
}
}
方法二:动态规划
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35.1 MB, 在所有 Java 提交中击败了72.66%的用户
class Solution {
// 使用递归
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 行列初始化
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int i = 0; i < n; i++) {
dp[0][i] = 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];
}
}
return dp[m - 1][n - 1];
}
}
方法三:空间压缩
使用一维的 dp 数组存储当前行的所有点的路径数,然后遍历每一行。dp[j - 1] 存储的是到达 dp[i][j - 1] 的最小路径,dp[j] 存储的是到达 dp[i - 1][j] 的最小路径。
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35 MB, 在所有 Java 提交中击败了86.86%的用户
class Solution {
// 使用递归
public int uniquePaths(int m, int n) {
int[] dp = new int[n];
// 行列初始化
for (int i = 0; i < n; i++) {
dp[i] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// dp[j - 1] 存储的是到达 dp[i][j - 1] 的最小路径。
// dp[j] 存储的是到达 dp[i - 1][j] 的最小路径。
dp[j] += dp[j - 1];
}
}
return dp[n - 1];
}
}
63. 不同路径 II
自己的想法
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:37.8 MB, 在所有 Java 提交中击败了46.23%的用户
方法一:动态规划
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int n = obstacleGrid.length;
int m = obstacleGrid[0].length;
// 使用一维数组记录当前行的每格的路径数
int[] dp = new int[m];
dp[0] = obstacleGrid[0][0] == 0 ? 1 : 0;
// 遍历每一行
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
// 如果当前不是障碍物,则进行累加
if (obstacleGrid[i][j] != 1) {
if (j != 0) {
dp[j] += dp[j - 1];
}
} else {
// 是障碍物,则路径为0
dp[j] = 0;
}
}
}
return dp[m - 1];
}
}
第四节,状态的定义和状态转移
198. 打家劫舍
自己的想法
方法一:递归(超时)
class Solution {
public int rob(int[] nums) {
return dfs(nums, 0, true, 0);
}
public int dfs (int[] nums, int index, boolean is, int sum) {
// 到结尾则返回
if (index == nums.length) {
return sum;
}
// 不选的情况
int value = dfs( nums, index + 1, true, sum);
// 前一个没选,当前可以选
if (is) {
value = Math.max(value, dfs(nums, index + 1, false, sum + nums[index]));
}
return value;
}
}
方法二:动态规划
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35.8 MB, 在所有 Java 提交中击败了57.87%的用户
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) {
return nums[0];
}
int[] dp = new int[nums.length + 1];
dp[1] = nums[0];
for (int i = 2; i <= nums.length; i++) {
// 选和不选两种情况
dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
}
return dp[nums.length];
}
}
看题解后
方法三:递归(超时)
class Solution {
public int rob(int[] nums) {
return dfs(nums, 0);
}
public int dfs(int[] nums, int index) {
if (index >= nums.length) {
return 0;
}
int sum = 0;
for (int i = index; i < nums.length; i++) {
sum = Math.max(sum, nums[i] + dfs(nums, i + 2));
}
return sum;
}
}
方法四:记忆化搜索(超时)
class Solution {
int[] dp;
public int rob(int[] nums) {
dp = new int[nums.length];
return dfs(nums, 0);
}
public int dfs(int[] nums, int index) {
if (index >= nums.length) {
return 0;
}
if (dp[index] != 0) {
return dp[index];
}
int sum = 0;
for (int i = index; i < nums.length; i++) {
sum = Math.max(sum, nums[i] + dfs(nums, i + 2));
}
dp[index] = sum;
return sum;
}
}
方法五:动态规划
- 执行用时:2 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35.8 MB, 在所有 Java 提交中击败了46.99%的用户
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 0) {
return 0;
}
int[] dp = new int[n];
// 偷最后一个
dp[n - 1] = nums[n - 1];
for (int i = n - 2; i >= 0; i--) {
for (int j = i; j < n; j++) {
dp[i] = Math.max(dp[i], nums[j] + (j + 2 < n ? dp[j + 2] : 0));
}
}
return dp[0];
}
}
213. 打家劫舍 II
动态规划
class Solution {
public int rob(int[] nums) {
int length = nums.length;
if (length == 1) {
return nums[0];
} else if (length == 2) {
return Math.max(nums[0], nums[1]);
}
return Math.max(robRange(nums, 0, length - 1), robRange(nums, 1, length));
}
public int robRange(int[] nums, int start, int end) {
int[] dp = new int[end + 1];
dp[start + 1] = nums[start];
for (int i = 2; i <= end; i++) {
// 选和不选两种情况
dp[i] = Math.max(dp[i - 2] + nums[i - 1], dp[i - 1]);
}
return dp[end];
}
}
2、最大子序和(简单)
自己的想法
方法一:动态规划
对于序列中的每一个数都有选与不选两种情况。因为要求序列是连续的子数组,所有如果选,值是最大子序列和+当前数;如果不选,值是当前数。我们只需要记录当前序列的最大值,就可以推出后面序列的最大值。
- 执行用时:1 ms, 在所有 Java 提交中击败了94.73%的用户
- 内存消耗:38.4 MB, 在所有 Java 提交中击败了46.62%的用户
class Solution {
public int maxSubArray(int[] nums) {
// 记录当前子序列和的最大值
int take = 0;
int result = Integer.MIN_VALUE;
for (int num : nums) {
// 求出对于当前数,选与不选,哪个价值最高
take = Math.max(take + num, num);
// 记录最大的值
result = Math.max(take, result);
}
return result;
}
}
3、最长递增子序列(中等)
看题解后
方法一:动态规划
使用数组记录以当前下标值结尾的最长递增子序列的长度(初始化为 1)。
- 执行用时:74 ms, 在所有 Java 提交中击败了47.45%的用户
- 内存消耗:38 MB, 在所有 Java 提交中击败了79.39%的用户
class Solution {
public int lengthOfLIS(int[] nums) {
// 记录以当前下标值结尾的最长递增子序列的长度
int[] dp = new int[nums.length];
int max = 0;
for (int i = 0; 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);
}
}
max = Math.max(max, dp[i]);
}
return max;
}
}
方法二:贪心 + 二分查找
上升子序列的结尾的数越小,那么遍历的时候后面接上一个数,会有更大的可能构成一个长度更长的上升子序列。既然结尾越小越好,我们可以记录在长度固定的情况下,结尾最小的那个元素的数值。
用 tail[i] 表示:长度为 i 的所有上升子序列的结尾的最小值。tail[1] 表示长度为 1 的所有上升子序列中,结尾最小的元素的数值。
- 执行用时:2 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:37.8 MB, 在所有 Java 提交中击败了95.81%的用户
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
// 记录指定长度结尾的最小数,下标代表长度,值代表最小的数
int[] tail = new int[nums.length + 1];
tail[1] = nums[0];
// 记录 tail 有效元素的下标,即递增子序列的最大长度
int index = 1;
// 遍历数组
for (int i = 1; i < nums.length; i++) {
// 如果当前数,大于序列的最后元素,则向后追加。表示序列可以变长
if (nums[i] > tail[index]) {
tail[++index] = nums[i];
} else {
// 如果序列不能改变,则更新数组的值
// 因为只有子序列的结尾的数越小,那么遍历的时候后面接上一个数,会有更大的可能构成一个长度更长的上升子序列
// 使用二分查找,查找 tail 数组中第一个大于等于 nums[i] 的数进行替换
int left = 1, right = index;
while (left < right) {
int mid = (left + right) >> 1;
if (tail[mid] < nums[i]) {
left = mid + 1;
} else {
right = mid;
}
}
tail[left] = nums[i];
}
}
return index;
}
}
6、打家劫舍(中等)
自己的想法
方法一:动态规划
创建 dp 数组,记录对于当前房间,偷与不偷的最大价值。
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35.7 MB, 在所有 Java 提交中击败了81.95%的用户
class Solution {
public int rob(int[] nums) {
int n = nums.length;
// 0 代表偷了,1 代表没偷
int[][] dp = new int[n][2];
dp[0][0] = nums[0];
for (int i = 1; i < n; i++) {
dp[i][0] = nums[i] + dp[i - 1][1];
dp[i][1] = Math.max(dp[i - 1][0], dp[i - 1][1]);
}
return Math.max(dp[n - 1][0], dp[n - 1][1]);
}
}
看题解后
方法二:动态规划(优化空间)
创建 dp 数组,记录对于当前房间,偷或不偷的最大价值。
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35.4 MB, 在所有 Java 提交中击败了98.99%的用户
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < n; i++) {
// 计算偷与不偷哪个价值大
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[n - 1];
}
}
方法三:动态规划 + 滚动数组(优化空间)
省去 dp 数组,使用滚动数组,在每个时刻只需要存储前两间房屋的最高总金额。使用两个中间变量记录值。
- 执行用时:0 ms, 在所有 Java 提交中击败了100.00%的用户
- 内存消耗:35.6 MB, 在所有 Java 提交中击败了89.89%的用户
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
int first = nums[0], second = Math.max(nums[0], nums[1]);
for (int i = 2; i < n; i++) {
int tmp = second;
// 计算偷与不偷哪个价值大
second = Math.max(first + nums[i], second);
first = tmp;
}
return second;
}
}
标题:动态规划练习题——LeetCode
作者:Yi-Xing
地址:http://zyxwmj.top/articles/2021/04/02/1617373427204.html
博客中若有不恰当的地方,请您一定要告诉我。前路崎岖,望我们可以互相帮助,并肩前行!