动态规划

dp五部曲:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

基础题目

1.使用最小花费爬楼梯

题目描述

题目链接

解题思路

代码

class Solution {
    //dp[i]爬到第i个台阶的最低花费
    //dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int dp0 = 0;
        int dp1 = 0;
        for(int i=2; i<=n; i++) {
            int tmp = Math.min(dp1 + cost[i-1], dp0 + cost[i-2]);
            dp0 = dp1;
            dp1 = tmp;
        }
        return dp1;
    }
}

2.不同路径

题目描述

题目链接
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

解题思路

代码

class Solution {
    //dp[i][j] = dp[i-1][j] + dp[i][j-1]
    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];
    }
}

滚动数组:

class Solution {
    //dp[i][j] = dp[i-1][j] + dp[i][j-1]
    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        for(int i=1; i<m; i++) {
            for(int j=1; j<n; j++) {
                dp[j] = dp[j] + dp[j-1];
            }
        }
        return dp[n-1];
    }
}

3.不同路径II

题目描述

题目链接
比上一题多了障碍物

解题思路

  • 第一行和第一列,如果前面的不能到达,那么后面的全都不能到达

  • 对于有障碍物的位置,dp直接设为0就可以了

代码

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        for(int i=0; i<m; i++) {
            if(obstacleGrid[i][0] != 1) {
                dp[i][0] = 1;
            } else {
                break;  //第一行和第一列,如果前面的不能到达,那么后面的全都不能到达
            }
        }
        for(int j=0; j<n; j++) {
            if(obstacleGrid[0][j] != 1) {
                dp[0][j] = 1;
            } else {
                break;
            }
        }
        for(int i=1; i<m; i++) {
            for(int j=1; j<n; j++) {
                if(obstacleGrid[i][j] == 0) {   //对于障碍物的位置,dp直接设为0就可以了
                    dp[i][j] = dp[i-1][j] + dp[i][j-1];
                }
            }
        }
        return dp[m-1][n-1];
    }
}

滚动数组:

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[] dp = new int[n];
        for(int j=0; j<n; j++) {
            if(obstacleGrid[0][j] != 1) {
                dp[j] = 1;
            } else {
                break;
            }
        }
        for(int i=1; i<m; i++) {
            for(int j=0; j<n; j++) {
                if(obstacleGrid[i][j] == 1) {
                    dp[j] = 0;
                } else if(j > 0) {
                    dp[j] += dp[j-1];
                }
            }
        }
        return dp[n-1];
    }
}

4.正数拆分🔺

题目描述

题目链接

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积 。

解题思路

  • dp[i] = 拆分数字i可以获得的最大乘积

  • dp[i] = Math.max(dp[i], Math.max(j *(i-j), j * dp[i-j]));

  • j是从1到i/2,因为至少会拆分成两个正整数,拆分的数越相近乘积越大(未知的是应该拆分成几个数),所以j只需要遍历到i/2(大于i/2的部分重复了,相当于i和j交换)

  • j *(i-j)是拆分成两个数,j * dp[i-j]是拆分成三个及三个以上的数;取其中最大的

代码

class Solution {
    //dp[i] = 拆分数字i可以获得的最大乘积
    public int integerBreak(int n) {
        int[] dp = new int[n+1];
        dp[2] = 1;
        for(int i=3; i<=n; i++) {
            for(int j=1; j<=i/2; j++) {
                dp[i] = Math.max(dp[i], Math.max(j *(i-j), j * dp[i-j]));
            }
        }
        return dp[n];
    }
}

5.不同的二叉搜索树🔺

题目描述

题目链接

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

解题思路

  • dp[i] = 从小到大连续i个数,能构成的不同BST的个数

  • 与具体的数字无关,重点是 从小到大连续i个数

  • 连续i个数中,分别以1, ..., i为头节点,共有多少种 = 左子树种类数 * 右子树种类数

  • 左右子树也分别是从小到大连续的n个数

  • 注意dp[0] = 1,便于相乘

代码

class Solution {
    //dp[i] = 从小到大连续i个数,能构成的不同BST的个数
    //与具体的数字无关,重点是 从小到大连续i个数
    public int numTrees(int n) {
        if(n <= 2) return n;
        int[] dp = new int[n+1];
        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        for(int i=3; i<=n; i++) {
            for(int j=1; j<=i; j++) {    //以j为头节点
                //左子树的值都比j小,是1...j-1,共j-1个从小到大且连续的数
                //右子树的值都比j大,是j+1到i,共i-j个从小到大且连续的数
                dp[i] += dp[j-1] * dp[i-j];
            }
        }
        return dp[n];
    }
}

背包问题

背包问题

打家劫舍

1. 打家劫舍

题目描述

题目链接

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

解题思路

代码

也可以用滚动数组,但是懒得写了

class Solution {
    //dp[j] = [0...j]房间能偷到的最大金额
    //dp[j] = max(dp[j-2] + nums[j], dp[j-1])
    //dp[0] = nums[0]
    //dp[1] = max(nums[0], nums[1])
    public int rob(int[] nums) {
        if(nums.length == 1) return nums[0];
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for(int i=2; i<nums.length; i++) {
            dp[i] = Math.max(dp[i-2] + nums[i], dp[i-1]);
        }
        return dp[nums.length-1];
    }
}

2. 打家劫舍II

题目描述

题目链接

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

解题思路

  • 和打家劫舍的区别在于,首尾是相连的

  • 所以首和尾必有一个不取的(剩下一个取不取不重要)

  • 分开求两种情况(设置start和end),取最大值。

代码

class Solution {
    public int rob(int[] nums) {
        if(nums.length == 1) return nums[0];
        int ans1 = rob1(nums, 0, nums.length-1);
        int ans2 = rob1(nums, 1, nums.length);
        return Math.max(ans1, ans2);
    }
    public int rob1(int[] nums, int start, int end) {  //左闭右开
        if(end - start == 1) return nums[start];
        int[] dp = new int[end - start];
        dp[0] = nums[start];
        dp[1] = Math.max(nums[start], nums[start+1]);
        for(int i=2; i<end-start; i++) {
            dp[i] = Math.max(dp[i-2] + nums[start + i], dp[i-1]);
        }
        return dp[end-start-1];
    }
}

3. 打家劫舍III

题目描述

题目链接

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

解题思路

  • 树形打家劫舍

  • 显然,子问题是子树,所以后序遍历

  • root偷还是不偷,取决于子节点偷没偷,所以要用数组返回偷/没偷的两种情况

  • 子节点都没偷,就可以偷root;子节点有偷的,就不能偷root

代码

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode() {}
 *     TreeNode(int val) { this.val = val; }
 *     TreeNode(int val, TreeNode left, TreeNode right) {
 *         this.val = val;
 *         this.left = left;
 *         this.right = right;
 *     }
 * }
 */
class Solution {
    //0偷,1不偷
    public int rob(TreeNode root) {
        int[] ans = backtrack(root);
        return Math.max(ans[0], ans[1]);
    }
    int[] backtrack(TreeNode root) {
        int[] ans = new int[2];
        if(root == null) return ans;
        int[] left = backtrack(root.left);
        int[] right = backtrack(root.right);
        ans[0] = left[1] + right[1] + root.val;
        ans[1] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        return ans;
    }
}

股票问题

1. 买卖股票的最佳时机

题目描述

题目链接

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

解题思路

  • 只能买/卖一次

  • 设置两个状态:持有/不持有

代码

class Solution {
    //dp[i][0] = 第i天不持有股票的最大利润
    //dp[i][1] = 第i天持有股票的最大利润
    //dp[i][0] = max(dp[i-1][0], dp[i-1][1]+prices[i])
    //dp[i][1] = max(dp[i-1][1], -prices[i])
    //dp[0][0] = 0, dp[0][1] = -prices[0]
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for(int i=1; i<prices.length; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
        }
        return dp[prices.length-1][0];
    }
}

2. 买卖股票的最佳时机II

题目描述

题目链接

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

解题思路

  • 可以买/卖无数次

  • 设置两个状态:持有/不持有

代码

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        for(int i=1; i<prices.length; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);
        }
        return dp[prices.length-1][0];
    }
}

3. 买卖股票的最佳时机III

题目描述

题目链接

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

解题思路

  • 最多买/卖两次

  • 设置四个状态:第一次持有,第一次不持有,第二次持有,第二次不持有。还有一个初始状态,其实不用设置

  • 现在最大的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出。如果想不明白的录友也可以这么理解:如果第一次卖出已经是最大值了,那么我们可以在当天立刻买入再立刻卖出。所以dp[4][4]已经包含了dp[4][2]的情况。也就是说第二次卖出手里所剩的钱一定是最多的。

代码

class Solution {
    //-1 未进行任何操作
    //dp[i][0] = 第一次不持有股票的最大利润(指把第一次买的卖了)
    //dp[i][1] = 第一次持有股票的最大利润
    //dp[i][2] = 第二次不持有股票的最大利润
    //dp[i][3] = 第二次持有股票的最大利润
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][4];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        dp[0][3] = -prices[0];
        for(int i=1; i<prices.length; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
            dp[i][2] = Math.max(dp[i-1][2], dp[i-1][3] + prices[i]);
            dp[i][3] = Math.max(dp[i-1][3], dp[i-1][0] - prices[i]);
        }
        return dp[prices.length-1][2];
    }
}

4. 买卖股票的最佳时机IV

题目描述

题目链接

给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

解题思路

  • 每天都有第i次持有/不持有,i=1...k 种状态, 共2*k种

代码

class Solution {
    //每天都有第i次持有/不持有,i=1...k 种状态, 共2*k种
    public int maxProfit(int k, int[] prices) {
        int[][] dp = new int[prices.length][2*k];
        for(int i=0; i<2*k; i++) {
            if(i%2 == 0) {
                dp[0][i] = -prices[0];
            }
        }
        for(int i=1; i<prices.length; i++) {
            dp[i][0] = Math.max(dp[i-1][0], - prices[i]);
            for(int j=1; j<2*k ;j++) {
                if(j%2 == 0) {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-1] - prices[i]);
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-1] + prices[i]);
                }
            }
        }
        return dp[prices.length-1][2*k-1];
    }
}

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

题目描述

题目链接

解题思路

  • 为了区别冷冻期,需要设置 卖出状态和今天卖出状态
状态0 持有状态
状态1 卖出状态
状态2 今天卖出
状态3 冷冻状态

代码

class Solution {
    //状态0 持有状态
    //状态1 卖出状态
    //状态2 今天卖出
    //状态3 冷冻状态
    //dp[i][j] = 第i天的状态为j, 剩余的最大现金数
    //dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i], dp[i-1][3]-prices[i])
    //dp[i][1] = max(dp[i-1][1], dp[i-1][3])
    //dp[i][2] = dp[i-1][0] + prices[i]
    //dp[i][3] = dp[i-1][2]
    //dp[0][0] = -prices[0]
    //dp[0][1] = 0
    //dp[0][2] = 0
    //dp[0][3] = 0
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length][4];
        dp[0][0] = -prices[0];
        for(int i=1; i<prices.length; i++) {
            dp[i][0] = Math.max(Math.max(dp[i-1][0], dp[i-1][1] - prices[i]), dp[i-1][3] - prices[i]);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][3]);
            dp[i][2] = dp[i-1][0] + prices[i];
            dp[i][3] = dp[i-1][2];
        }
        return Math.max(Math.max(dp[prices.length-1][1], dp[prices.length-1][2]), dp[prices.length-1][3]);
    }
}

6. 买卖股票的最佳时机含手续费

题目描述

题目链接

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

解题思路

  • 完成一笔交易再交手续费

代码

class Solution {
    //状态0 卖出
    //状态1 持有
    public int maxProfit(int[] prices, int fee) {
        int[][] dp = new int[prices.length][2];
        dp[0][0] = 0;   //这里注意, dp[i][j]是指处于当前状态剩余的最大利润, 所以当然初始化为不买入不卖出
        dp[0][1] = -prices[0];
        for(int i=1; i<prices.length; i++) {
            dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i] - fee);
            dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] - prices[i]);
        }
        return Math.max(dp[prices.length-1][0], dp[prices.length-1][1]);
    }
}

子序列问题

dp[i][j]的定义,到底是以XXX结尾的XXX,还是序列[0...XXX],需要斟酌,对应的状态转移也不同,返回值也不同。

不连续

1. 最长递增子序列

题目描述

题目链接

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

解题思路

  • dp[i] = 以nums[i]结尾的最长递增子序列的长度

  • 因为不要求连续,以nums[i]结尾的最长递增子序列的倒数第二个数不一定是谁,所以要遍历求最大值

  • dp[i] = max(dp[i], dp[j] + 1), j=0,...,i-1 nums[j] < nums[i]

  • 最后的结果是所有的dp[i]中的最大值

代码

class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        int ans = 1;
        for(int i=1; i<nums.length; i++) {
            for(int j=0; j<i; j++) {
                if(nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            ans = Math.max(ans, dp[i]);
        }
        return ans;
    }
}

2. 最长公共子序列🔺

题目描述

题目链接

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

解题思路

  • 两个序列text1和text2求公共,dp数组是二维的,dp[i][j] = 序列1[i-1]和序列2[j-1]...,这样比较方便初始化

  • dp[i][j] = 以text1[i-1]和text2[j-1]结尾的最长公共子序列的长度 ---> 不可行,因为不连续,还得遍历前面的,这样嵌套的循环太多

  • dp[i][j] = 序列text1[0...i-1]和序列text2[0...j-1]最长公共子序列的长度

  • 因为dp[i][j]依赖于dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1],不仅依赖于左上角、上面,还依赖于右边(本行),所以不能用滚动数组

代码

class Solution {
    //求两个序列的公共,dp[i][j] = 序列1[i-1]和序列2[j-1],这样比较好初始化
    //dp[i][j] = 以text1[i-1]和text2[j-1]结尾的最长公共子序列的长度---不可行,因为不连续,还得遍历前面的
    //dp[i][j] = text1[0...i-1]和text2[0...j-1]最长公共子序列的长度
    public int longestCommonSubsequence(String text1, String text2) {
        int[][] dp = new int[text1.length()+1][text2.length()+1];
        for(int i=1; i<=text1.length(); i++) {
            for(int j=1; j<=text2.length(); 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-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[text1.length()][text2.length()];
    }
}

3. 不相交的线

题目描述

题目链接

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:

nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数.

解题思路

  • 其实就是求最长公共子序列的长度

  • dp[i][j] = 序列nums1[0...i-1]和序列nums2[0...j-1]的 最长公共子序列的长度

代码

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length+1][nums2.length+1];
        for(int i=1; i<=nums1.length; i++) {
            for(int j=1; j<=nums2.length; j++) {
                if(nums1[i-1] == nums2[j-1]) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[nums1.length][nums2.length];
    }
}

连续

1. 最长连续递增序列

题目描述

题目链接

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

解题思路

  • 和上一题的区别在于子序列必须是连续的

  • 以nums[i]结尾的最长递增子序列的倒数第二个数(如果有的话)一定是nums[i-1]

  • 所以只需要看nums[i-1]是否小于nums[i]

代码

class Solution {
    //dp[i] = 以nums[i]结尾的最长连续递增子序列的长度
    public int findLengthOfLCIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        int ans = 1;
        for(int i=1; i<nums.length; i++) {
            if(nums[i-1] < nums[i]) {
                dp[i] = dp[i-1] + 1;
                ans = Math.max(ans, dp[i]);
            }
        }
        return ans;
    }
}

2. 最长重复子数组🔺

题目描述

题目链接

给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。

解题思路

  • dp[i][j] = 以nums1[i]和nums[j]结尾的最长重复子数组的长度

  • 连续的

  • if(nums1[i] == nums2[j]) dp[i][j] = dp[i-1][j-1] + 1;

  • dp[i][j]依赖于dp[i-1][j-1],所以dp[i][0]dp[0][j]都要初始化

  • 单独初始化代码太多

  • 合并初始化:dp[i][j] = 以nums1[i-1]和nums[j-1]结尾的最长重复子数组的长度

  • 合并初始化 + 滚动数组:注意nums1[i-1] != nums[j-1]时,要将dp[j]置0。为什么要置0用脑子想想就知道了

代码

笨蛋初始化:

class Solution {
    //dp[i][j] = 以nums1[i]和nums[j]结尾的最长重复子数组的长度
    public int findLength(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length][nums2.length];
        int ans = 0;
        for(int i=0; i<nums1.length; i++) {
            dp[i][0] = nums1[i] == nums2[0] ? 1 : 0;
            ans = Math.max(ans, dp[i][0]);
        }
        for(int j=0; j<nums2.length; j++) {
            dp[0][j] = nums1[0] == nums2[j] ? 1 : 0;
            ans = Math.max(ans, dp[0][j]);
        }
        for(int i=1; i<nums1.length; i++) {
            for(int j=1; j<nums2.length; j++) {
                if(nums1[i] == nums2[j]) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                    ans = Math.max(ans, dp[i][j]);
                }
            }
        }
        return ans;
    }
}

不笨蛋初始化:

class Solution {
    //dp[i][j] = 以nums1[i-1]和nums[j-1]结尾的最长重复子数组的长度
    public int findLength(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];
        int ans = 0;
        for(int i=1; i<=nums1.length; i++) {
            for(int j=1; j<=nums2.length; j++) {
                if(nums1[i-1] == nums2[j-1]) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                    ans = Math.max(ans, dp[i][j]);
                }
            }
        }
        return ans;
    }
}

滚动数组:

class Solution {
    //dp[i][j] = 以nums1[i-1]和nums[j-1]结尾的最长重复子数组的长度
    public int findLength(int[] nums1, int[] nums2) {
        int[] dp = new int[nums2.length + 1];
        int ans = 0;
        for(int i=1; i<=nums1.length; i++) {
            for(int j=nums2.length; j>=1; j--) {
                if(nums1[i-1] == nums2[j-1]) {
                    dp[j] = dp[j-1] + 1;
                    ans = Math.max(ans, dp[j]);
                } else {
                    dp[j] = 0;  //注意赋0
                }
            }
        }
        return ans;
    }
}

3. 最大子序和

题目描述

题目链接

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

解题思路

  • dp[i] = 以nums[i]结尾的连续子数组的最大和

代码

class Solution {
    //dp[i] = 以nums[i]结尾的连续子数组的最大和
    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int ans = nums[0];
        for(int i=1; i<nums.length; i++) {
            dp[i] = Math.max(dp[i-1] + nums[i], nums[i]);
            ans = Math.max(ans, dp[i]);
        }
        return ans;
    }
}

滚动数组:

class Solution {
    public int maxSubArray(int[] nums) {
        int dp0 = nums[0];
        int ans = nums[0];
        for(int i=1; i<nums.length; i++) {
            dp0 = Math.max(dp0 + nums[i], nums[i]);
            ans = Math.max(ans, dp0);
        }
        return ans;
    }
}

编辑距离

1. 判断子序列

题目描述

题目链接

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

解题思路

  • 方法1:双指针,i指向t,j指向s,循环i,只有当s[j] == t[i],j才往前走一步

  • 方法2:判断s和t的最长公共子序列的长度是否等于s的长度

代码

方法1:

class Solution {
    public boolean isSubsequence(String s, String t) {
        int j = 0;
        for(int i=0; i<t.length() && j < s.length(); i++) {
            if(s.charAt(j) == t.charAt(i)) {
                j++;
            }
        }
        if(j == s.length()) {
            return true;
        }
        return false;
    }
}

方法2:

class Solution {
    //s和t的最长公共子序列是否等于s的长度
    //不连续
    //dp[i][j] = 序列0...i-1和序列0...j-1的最长公共子序列的长度
    public boolean isSubsequence(String s, String t) {
        int[][] dp = new int[s.length()+1][t.length()+1];
        for(int i=1; i<=s.length(); i++) {
            for(int j=1; j<=t.length(); j++) {
                if(s.charAt(i-1) == t.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        if(dp[s.length()][t.length()] == s.length()) return true;
        return false;
    }
}

2. 不同的子序列

题目描述

题目链接

给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 10^9 + 7 取模。

解题思路

  • 在代码注释里

代码

class Solution {
    //dp[i][j] = s[0...i-1]中 出现 以t[0...j-1]的个数
    //注意:s不一定以s[i-1]结尾,但是t一定要以t[j-1]结尾,这样才符合题意
    /*
        case 1: if(s[i-1] == t[j-1]), dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
        即两种情况:
            1) 用s[i-1]来匹配t[j-1], 此时只需要考虑s[0...i-2]的子序列有多少个t[0...j-2]
            2) 不用s[i-1]来匹配t[j-1], 此时需要考虑s[0...i-2]的子序列有多少个t[0...j-1]
        case 2: if(s[i-1] != t[j-1]), dp[i][j] = dp[i-1][j]
            这时只能考虑s[0...i-2]的子序列有多少个t[0...j-1]
    */
    /*
        初始化:
        画表格的时候注意是下标0对应的是第-1个字符, 下标1对应的才是第0个字符
        因为计算dp[i][j]依赖于dp[i-1][j-1]和dp[i-1][j] , 因此必须初始化dp[0][j]和dp[i][0] (画图可以看出来)
        dp[i][0] = 以i-1为结尾的s的子序列出现空串的个数 = 1
        dp[0][j] = 空串s的子序列出现以j-1为结尾的t的个数 = 0
    */
    public int numDistinct(String s, String t) {
        int[][] dp = new int[s.length()+1][t.length()+1];
        for(int i=0; i<=s.length(); i++) dp[i][0] = 1; //以i-1为结尾的s的子序列出现空串的个数 = 1
        for(int j=1; j<=t.length(); j++) dp[0][j] = 0; //空串s的子序列出现以j-1为结尾的t的个数 = 0,注意这里从1开始,其实可以不写这一句
        for(int i=1; i<=s.length(); i++) {
            for(int j=1; j<=t.length(); j++) {
                if(s.charAt(i-1) == t.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1] + dp[i-1][j];
                } else {
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[s.length()][t.length()];
    }
}

3. 两个字符串的删除操作

题目描述

题目链接

给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。

每步 可以删除任意一个字符串中的一个字符。

解题思路

  • 方法1:把word1和word2都变成它俩的最长公共子序列,(word1长度-最长公共子序列的长度) + (word2长度-最长公共子序列的长度)

  • 方法2:状态转移

代码

方法1:

class Solution {
    //(word1长度-最长公共子序列的长度) + (word2长度-最长公共子序列的长度)
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length()+1][word2.length()+1];
        for(int i=1; i<=word1.length(); i++) {
            for(int j=1; j<=word2.length(); j++) {
                if(word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return word1.length() + word2.length() - 2 * dp[word1.length()][word2.length()];
    }
}

方法2:

class Solution {
    //dp[i][j] = 以i-1为结尾的word1和以j-1为结尾的word2, 要想达到相同, 最少需要删除的次数
    /**
        case 1: if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1]
        case 2: if(word1[i-1] != word2[j-1])
                (1) 删除word1[i-1]: dp[i][j] = dp[i-1][j] + 1
                (2) 删除word2[j-1]: dp[i][j] = dp[i][j-1] + 1
                (3) 删除word1[i-1]和word2[j-1]: dp[i][j] = dp[i-1][j-1] + 2
     */
     /**
        画图可知需要初始化dp[i][0]和dp[0][j]
        dp[i][0] = i
        dp[0][j] = j
     */
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length()+1][word2.length()+1];
        for(int i=0; i<=word1.length(); i++) dp[i][0] = i;
        for(int j=0; j<=word2.length(); j++) dp[0][j] = j;
        for(int i=1; i<=word1.length(); i++) {
            for(int j=1; j<=word2.length(); j++) {
                if(word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1];
                } else {
                    dp[i][j] = Math.min(dp[i-1][j]+1, Math.min(dp[i][j-1]+1, dp[i-1][j-1]+2));
                }
            }
        }
        return dp[word1.length()][word2.length()];
    }
}

4. 编辑距离

题目描述

题目链接

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

解题思路

  • dp[i][j] = 将word1[0...i-1]转换成word2[0...j-1]所使用的最少操作数

  • 删除word1和word2中的字符是不同的

  • 插入和删除是一样的,在word1中插入元素相当于删除word2的元素

  • 替换word1和word2是一样的,只需要替换一个就可以

代码

class Solution {
    //dp[i][j] = 将word1[0...i-1]转换成word2[0...j-1]所使用的最少操作数
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length()+1][word2.length()+1];
        for(int i=0; i<=word1.length(); i++) dp[i][0] = i;  //删除
        for(int j=0; j<=word2.length(); j++) dp[0][j] = j;  //删除
        for(int i=1; i<=word1.length(); i++) {
            for(int j=1; j<=word2.length(); j++) {
                if(word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1];
                } else {
                    //删除word1的一个元素:dp[i-1][j] + 1
                    //删除word2的一个元素:dp[i][j-1] + 1
                    //插入和删除是一样的,在word1中插入元素相当于删除word2的元素
                    //替换(替换word1和word2是一样的,只需要替换一个就可以):dp[i-1][j-1] + 1
                    dp[i][j] = Math.min(dp[i-1][j] + 1, Math.min(dp[i][j-1] + 1, dp[i-1][j-1] + 1));
                }
            }
        }
        return dp[word1.length()][word2.length()];
    }
}

回文

1. 回文子串

题目描述

题目链接

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

解题思路

  • dp[i][j] = s[i...j]是不是回文子串

  • dp[i][j]依赖于dp[i+1][j-1],所以从下往上,从左往右遍历

  • 只有主对角线上面的一半有意义

代码

class Solution {
    //dp[i][j] = s[i...j]是不是回文子串
    public int countSubstrings(String s) {
        int ans = 0;
        int[][] dp = new int[s.length()][s.length()];
        for(int i=0; i<s.length(); i++) {
            dp[i][i] = 1;
            ans++;
        }
        for(int i=s.length()-1; i>=0; i--) {
            for(int j=i+1; j<s.length(); j++) {
                if(s.charAt(i) == s.charAt(j) && (j-i == 1 || dp[i+1][j-1] == 1)) {
                    dp[i][j] = 1;
                    ans++;
                }
            }
        }
        return ans;
    }
}

2. 最长回文子序列

题目描述

题目链接

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

解题思路

  • 最长回文子序列的长度,是子序列不是子串

代码

class Solution {
    //dp[i][j] = s[i...j]的最长回文子序列的长度
    public int longestPalindromeSubseq(String s) {
        int[][] dp = new int[s.length()][s.length()];
        for(int i=0; i<s.length(); i++) dp[i][i] = 1;
        for(int i=s.length()-1; i>=0; i--) {
            for(int j=i+1; j<s.length(); j++) {
                if(s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i+1][j-1] + 2;
                } else {
                    dp[i][j] = Math.max(dp[i][j-1], dp[i+1][j]);
                }
            }
        }
        return dp[0][s.length()-1];
    }
}

如果是最长回文子串的话(不一定对):

class Solution {
    //dp[i][j] = s[i...j]的最长回文子序列的长度
    public int longestPalindromeSubseq(String s) {
        int[][] dp = new int[s.length()][s.length()];
        for(int i=0; i<s.length(); i++) dp[i][i] = 1;
        for(int i=s.length()-1; i>=0; i--) {
            for(int j=i+1; j<s.length(); j++) {
                if(s.charAt(i) == s.charAt(j) && dp[i+1][j-1] == j-i-1) {
                    dp[i][j] = dp[i+1][j-1] + 2;
                } else {
                    dp[i][j] = Math.max(dp[i][j-1], dp[i+1][j]);
                }
            }
        }
        return dp[0][s.length()-1];
    }
}

鸡蛋掉落

题目描述

题目链接
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?

解题思路

代码

class Solution {
    public int superEggDrop(int K, int N) {
        int[][] dp = new int[K + 1][N + 1];
        for (int m = 1; m <= N; m++) {
            dp[0][m] = 0; // zero egg
            for (int k = 1; k <= K; k++) {
                dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1;
                if (dp[k][m] >= N) {
                    return m;
                }
            }
        }
        return N;
    }
}

题目描述

题目链接

解题思路

代码


posted @ 2023-10-23 21:23  shimmer_ghq  阅读(28)  评论(0)    收藏  举报