前言
该算法专栏主要补充一下动态规划的知识点
主要跟的up主,链接如下:
动态规划理论基础
基础题目
509. 斐波那契数(简单)
题目:
斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给定 n ,请计算 F(n) 。
示例 1:
输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:
输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:
输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3
提示:
0 <= n <= 30
思路:
通过递归迭代的思路(时间比较久)
class Solution {
public int fib(int n) {
if(n < 2)return n;
return fib(n - 1)+fib(n - 2);
}
}
通过设置dp的数组进行遍历
class Solution {
public int fib(int n) {
//初始判断条件条件不要遗忘
if(n <= 1)return n;
// 这里需要定义为n + 1
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for(int i = 2;i <= n;i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
也可以不要dp数组,使用两个临时变量存储,可以节约空间存储
class Solution {
public int fib(int n) {
int a=0;
int b=1;
int c=0;
for(int i=1;i<=n;i++){
c=a+b;
a=b;
b=c;
}
return a;
}
}
70. 爬楼梯(简单)
题目:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
提示:
1 <= n <= 45
思路:
class Solution {
public int climbStairs(int n) {
// 因为下面直接对dp[2]操作了,防止空指针
if (n <= 1) return n;
int[] dp=new int[n+1];
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
//这一步台阶是前一个台阶的总和,加上 前两个台阶的总和
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
}
如果不想数组越界,可以从0开始算起
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
// 数组的初始化 要找点,尤其是此处的动态规划
dp[0] = 1;
dp[1] = 1;
for(int i = 2;i <= n;i++){
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
也可通过变量代替数组
class Solution {
public int climbStairs(int n) {
int a=0;
int b=1;
int c=0;
for(int i=1;i<=n;i++){
c=a+b;
a=b;
b=c;
}
return b;
}
}
746. 使用最小花费爬楼梯(简单)
题目:
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
示例 2:
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
提示:
2 <= cost.length <= 1000
0 <= cost[i] <= 999
思路:
制定第一步花费,后面输出的时候输出为具体值即可
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int [n + 1];
//定义初始值的时候是需要消费步数的
dp[0] = cost[0];
dp[1] = cost[1];
// 此处不用越过最后一步
for(int i = 2; i < n ;i++){
dp[i] = Math.min(dp[i - 1],dp[i - 2]) + cost[i];
}
return Math.min(dp[n - 1],dp[n - 2]);
}
}
或者
制定第一步不花费部署,后面输出的时候要花费步数
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n=cost.length;
int []dp=new int [n+1];
//定义初始值的时候是不需要步数
dp[0]=0;
dp[1]=0;
//注意是要到n,越界后的顶来判断
for(int i=2;i<=n;i++){
dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
//返回最小的数值
return dp[n];
}
62. 不同路径(中等)
题目:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
示例 3:
输入:m = 7, n = 3
输出:28
示例 4:
输入:m = 3, n = 3
输出:6
提示:
1 <= m, n <= 100
题目数据保证答案小于等于 2 * 109
思路:
class Solution {
public int uniquePaths(int m, int n) {
int [][]dp=new int [m][n];
//默认初始化为1
dp[0][0]=1;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
//四种情况,少一种都不可
if(i==0&&j==0)continue;
if(i==0){
dp[0][j]=1;
}else if(j==0){
dp[i][0]=1;
}else if(i!=0&&j!=0){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
return dp[m-1][n-1];
}
}
为了减少遍历的情况
可用如下代码:
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 j = 0;j < n;j++){
dp[0][j] = 1;
}
// 注意此处的下标是从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];
}
}
63. 不同路径 II(中等)
题目:
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
示例 2:
输入:obstacleGrid = [[0,1],[0,0]]
输出:1
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j] 为 0 或 1
思路:
这道题和上一道题一个思路,只不过有了障碍
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++){
// 判断条件是obstacleGrid函数,别弄错了
if(obstacleGrid[i][0] == 0){
dp[i][0] = 1;
}else {
break;
}
}
for(int j = 0;j < n;j++){
if(obstacleGrid[0][j] == 0){
dp[0][j] = 1;
}else {
break;
}
dp[0][j] = 1;
}
// 注意此处的下标是从1开始,越过左上角
for(int i = 1;i < m;i++){
for(int j = 1;j < n;j++){
//如果有障碍,则跳过这个值
if(obstacleGrid[i][j] == 1)continue;
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
}
343. 整数拆分(中等)***
题目:
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
提示:
2 <= n <= 58
思路:
class Solution {
public int integerBreak(int n) {
int []dp = new int[n + 1];
//默认初始化dp【0】和dp【1】的值都是0
//数字 0 和1 都是不能拆分,从下标2开始
for(int i = 2;i<= n;i++){
这里的 j 其实最大值为 i-j,再大只不过是重复而已,
for(int j = 1;j <= i - j;j++){
// j * (i - j) 是单纯的把整数 i 拆分为两个数 也就是 i,i-j ,再相乘
//而j * dp[i - j]是将 i 拆分成两个以及两个以上的个数,再相乘。
dp[i] = Math.max(dp[i],Math.max(j * (i - j),j * dp[i - j]));
}
}
//返回最后一个dp下标值
return dp[n];
}
}
96. 不同的二叉搜索树(中等)**
题目:
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3
输出:5
示例 2:
输入:n = 1
输出:1
提示:
1 <= n <= 19
思路:
class Solution {
public int numTrees(int n) {
int []dp = new int [n + 1];
//初始化设置 0 和1的下标都是为1
dp[0] = 1;
dp[1] = 1;
//从第二个结点开始。计算每个结点dp值,前一个节点的dp会影响后者
for(int i = 2;i <= n;i++){
//计算每一个节点的值,每个节点主要由其 节点作为根节点,以此展开左右子树的探索,j注意范围
for(int j = 1;j <= i;j++){
//比如3这个节点的值,是由 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量。所以要想加
//左右子树的相加组合,可看上面那张图
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
}
01背包问题
二维数组
dp[i][j]
表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
一般i是物品数量,j是背包容量
将其变为二维的dp数组,按照物品遍历接着背包遍历
初始化的话,通过背包容量为0,则每个物品中的容量每一列都是为0,dp[i][0] = 0;
- 如果放进来的容量大于背包容量,则下一个dp还是上一个dp的容量
dp[i][j] = dp[i - 1][j];
- 如果放进来的容量小于背包容量,则下一个dp取其价值最高的那个
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
具体示例代码:
背包最大重量为4。
物品为:
物品行号 | 重量 | 价值 |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagsize = 4;
testweightbagproblem(weight, value, bagsize);
}
public static void testweightbagproblem(int[] weight, int[] value, int bagsize){
int wlen = weight.length, value0 = 0;
//定义dp数组:dp[i][j]表示背包容量为j时,前i个物品能获得的最大价值
int[][] dp = new int[wlen + 1][bagsize + 1];
//初始化:背包容量为0时,能获得的价值都为0
for (int i = 0; i <= wlen; i++){
dp[i][0] = value0;
}
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 1; i <= wlen; i++){
for (int j = 1; j <= bagsize; j++){
if (j < weight[i - 1]){
dp[i][j] = dp[i - 1][j];
}else{
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
}
}
}
//打印dp数组
for (int i = 0; i <= wlen; i++){
for (int j = 0; j <= bagsize; j++){
System.out.print(dp[i][j] + " ");
}
System.out.print("\n");
}
}
执行结果:
一维数组
如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。
dp[j]
表示:容量为j的背包,所背的物品价值可以最大为dp[j]
dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。。dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
这里push一下代码随想录的背包一维数组讲解
讲解得非常清楚
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagWeight + 1];
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 0; i < wLen; i++){
for (int j = bagWeight; j >= weight[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
}
遍历结果:
如果正序遍历,前面的数据会被后面的覆盖掉
但是倒序遍历,可以避免这种情况
416. 分割等和子集(中等)
题目:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100
思路:
回溯的用法会超时
使用01背包问题(不可重复的数据)
class Solution {
public boolean canPartition(int[] nums) {
int n=nums.length;
//遍历集合,求出总和,将其一分为2,即为每个背包的target
int sum=0;
for(int i=0;i<n;i++){
sum+=nums[i];
}
//先判断如果不能被整除,直接返回false
if(sum%2==1)return false;
int medium=sum/2;
//设置dp为数组长度,以及背包target
int [][]dp=new int[n][medium+1];
//初始化一开始的第0个重量 每个nums数值
for (int j = nums[0]; j <= medium; j++) {
dp[0][j] = nums[0];
}
//从第一个nums【i】开始,到n结束,注意符号
for(int i=1;i<n;i++){
//j为背包重量,也就是数量的数目,从【0,medium】
for(int j=0;j<=medium;j++){
//如果背包数量小于要添加的则不添加
if(j<nums[i]){
dp[i][j]=dp[i-1][j];
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);
}
}
}
//输出二维矩阵的最后一个数值,判断是否等于medium
return dp[n-1][medium]==medium;
}
}
使用一维数组进行存储遍历
class Solution {
public boolean canPartition(int[] nums) {
if(nums == null || nums.length == 0) return false;
int n=nums.length;
int sum=0;
for(int i=0;i<n;i++){
sum+=nums[i];
}
if(sum%2==1)return false;
int medium=sum/2;
int []dp=new int[medium+1];
//从0开始遍历
for(int i=0;i<n;i++){
//j为背包重量,要大于等于nums【i】,也就是大于其中某一个值
for(int j=medium;j>=nums[i];j--){
//如果背包数量小于要添加的则不添加
dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
//输出一维矩阵的最后一个数值,判断是否等于medium
return dp[medium]==medium;
}
}
1049. 最后一块石头的重量 II(中等)
题目:
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
示例 1:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
示例 2:
输入:stones = [31,26,33,21,40]
输出:5
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 100
思路:
打家劫舍
198. 打家劫舍(中等)
题目:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
思路:
class Solution {
public int rob(int[] nums) {
// 初始值条件这个比较固定,不要忘记!
if(nums == null||nums.length == 0)return 0;
if(nums.length == 1)return nums[0];
int n = nums.length;
int[] dp = new int[n];
//初始值条件就是 递推公式的基础就是dp[0] 和 dp[1]
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
//找到动态规划的条件,偷与不偷
//偷的话就是dp[i-2]+nums[i],不偷的话就是dp[i-1]
for(int i = 2;i < n; i++){
dp[i] = Math.max(dp[i - 1],dp[i - 2] + nums[i]);
}
return dp[n - 1];
}
}
213. 打家劫舍 II(中等)
题目:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3]
输出:3
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
思路:
因为成环了,所以比较其【0,n-2】以及【1,n-1】的最大值即可
class Solution {
public int rob(int[] nums) {
// 动态规划的初始化 特别是临界值,需要注意
if(nums == null||nums.length == 0)return 0;
if(nums.length == 1)return nums[0];
else if(nums.length == 2)return Math.max(nums[0],nums[1]);
int n = nums.length;
return Math.max(robs(nums,0,n - 2),robs(nums,1,n - 1));
}
public int robs(int[] nums,int left,int right){
//这三个条件可要可不要,要的执行内存比较快而已
if (nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
if (left == right) {
return nums[left];
}
//此处的数组长度是nums.length,而不是right
int n = nums.length;
int[] dp = new int [n];
// 此处传输的值,left 以及 right
dp[left] = nums[left];
dp[left + 1] = Math.max(nums[left],nums[left + 1]);
// 此处的right 要等于,因为传进来的边界不会越界
for(int i = left + 2;i <= right;i++){
dp[i] = Math.max(dp[i - 1],dp[i - 2] + nums[i]);
}
return dp[right];
}
}
337. 打家劫舍 III(中等)
题目:
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
提示:
树的节点数在 [1, 104] 范围内
0 <= Node.val <= 104
思路:
/**
* 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 {
public int rob(TreeNode root) {
int []result=robss(root);
//不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱
return Math.max(result[0],result[1]);
}
public int[] robss(TreeNode root){
//在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回
if (root == null) return new int[2];
//0 代表不偷,1 代表偷
int []dp=new int[2];
int []left=robss(root.left);
int []right=robss(root.right);
//当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
dp[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
//当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
dp[1]=root.val+left[0]+right[0];
return dp;
}
}
股票问题
121. 买卖股票的最佳时机(简单)
题目:
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 105
0 <= prices[i] <= 104
思路:
class Solution {
public int maxProfit(int[] prices) {
int max = 0;
int[] dp = new int[prices.length];
dp[0] = prices[0];
for (int i = 1; i < prices.length; i++) {
//dp[i]表示截止到i,价格的最低点是多少.
dp[i] =Math.min(dp[i - 1] , prices[i]) ;
//用当前的价值减去前i-1天的最低价值,就有最大价值了
max = Math.max((prices[i] - dp[i]),max);
}
return max;
}
}
或者使用另外的一种代码模式
不用每次都保存当前最大值与最小值
public class Solution {
public int maxProfit(int prices[]) {
int minprice = Integer.MAX_VALUE;
int maxprofit = 0;
for (int i = 0; i < prices.length; i++) {
if (prices[i] < minprice) {
minprice = prices[i];
} else if (prices[i] - minprice > maxprofit) {
maxprofit = prices[i] - minprice;
}
}
return maxprofit;
}
}
另外的动态规划代码模式:
class Solution {
public int maxProfit(int[] prices) {
int max = 0;
int[][] dp = new int[prices.length][2];
dp[0][0]=-prices[0];
dp[0][1]=0;
//本身初始化了数组0开始,所以这里的遍历要从1开始
for(int i=1;i<prices.length;i++){
//持有股票数,昨天也持有以及今天买了(收益变成负数)
dp[i][0]=Math.max(dp[i-1][0],-prices[i]);
//不持有股票数,昨天也没有持有今天昨天持有今天卖了(收益变成正的)
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
}
return dp[prices.length-1][1];
}
}
122. 买卖股票的最佳时机 II(中等)
题目:
给定一个数组 prices ,其中 prices[i] 表示股票第 i 天的价格。
在每一天,你可能会决定购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以购买它,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: prices = [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104
思路:
一种巧妙的做法:把所有上坡都加起来即可(贪心算法)
class Solution {
public int maxProfit(int[] prices) {
int n=prices.length;
//两者之差的上坡之和。此处不用取初始化第一个,默认只有一个数,也是sum=0
int sum=0;
for(int i=1;i<n;i++){
if(prices[i]>prices[i-1])
//将其所有上坡之和都加起来即可,本身不限制次数
sum+=(prices[i]-prices[i-1]);
}
return sum;
}
}
写的常规一些代码如下:
class Solution {
public int maxProfit(int[] prices) {
int ans = 0;
int n = prices.length;
for (int i = 1; i < n; ++i) {
ans += Math.max(0, prices[i] - prices[i - 1]);
}
return ans;
}
}
运用动态规划的做法:
class Solution {
public int maxProfit(int[] prices) {
int n=prices.length;
//定义数组的时候不要越界
int [][]dp=new int[n][2];
//dp[0][0]不持有股票,dp[0][1]持有股票
dp[0][0]=0;//卖了所以为0,也就是不持有
//持有股票数是买了,所以资金为负数
dp[0][1]=-prices[0];
for(int i=1;i<n;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[n-1][0];
}
}
这道题和上道题的区别在于临界条件
持有股票数的不一样
//持有,可能昨天就还有(没卖)以及昨天不持有入股买了一个(买到的要减去收益)
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
//持有股票数,昨天也持有以及今天买了(收益变成负数)
dp[i][1]=Math.max(dp[i-1][1],-prices[i]);
123. 买卖股票的最佳时机 III(困难)
题目:
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
示例 4:
输入:prices = [1]
输出:0
提示:
1 <= prices.length <= 105
0 <= prices[i] <= 105
思路:
class Solution {
public int maxProfit(int[] prices) {
int n=prices.length;
//本身只可以操作两次,所以定义了5个操作数
// 0代表没有操作
// 1代表第一次买入,所以经济效益一开始就是减去第0个数字
// 2代表第一次卖出,但是一开始没有经济,卖出也是为0,所以默认初始化就是0
// 3 同上面的第二次买入
// 4 同上面的第二次卖出
int [][]dp=new int [n][5];
dp[0][1]=-prices[0];
dp[0][3]=-prices[0];
//遍历所有的数组,从1开始遍历
for(int i=1;i<n;i++){
//第一次的买入和上一次的买入相关,或者上一次没持有,加上这次的效益
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
//第一次的卖出和上一次的卖出有关,或者上一次的买入减去这一次的卖出利益
dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
//下面的3 4同理,
dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
dp[i][4]=Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
}
//输出最后一次卖出的结果
return dp[n-1][4];
}
}
188.买卖股票的最佳时机IV(困难)
题目:
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
提示:
0 <= k <= 100
0 <= prices.length <= 1000
0 <= prices[i] <= 1000
思路:
//这道题跟上一道题有区别的地方在于,可以多次买卖,而不是只有2次
//设置过多的操作买卖次数,数组比较大,不好设置
//观察发现 买的时候奇数,卖的时候为次数,所以dp数组就很好设置,跟上一道题逻辑差不多
class Solution {
public int maxProfit(int k, int[] prices) {
//这里这个条件要初始化判断,否则会出错
if (prices.length == 0) return 0;
int n=prices.length;
//比如两次买卖需要一共4次,以及一个0的无操作次数,所以为2*k+1
int [][]dp=new int[n][k*2+1];
//不用专门分奇偶数区分,直接奇数赋值为 -prices[0],偶数默认不处理,直接系统初始化为0
//默认从1开始初始化,0不用初始化,因为0为无操作数,可以看上一题的思路初始化细节,差不多
for(int i=1;i<k*2;i+=2){
dp[0][i]=-prices[0];
}
//遍历整个数组
for(int i=1;i<n;i++){
//0操作数是没有的,所以从1开始遍历,遍历到k*2即可。不可携程j+2.需要写为j+=2
for(int j=1;j<k*2;j+=2){
//上一个买入的操作数,或者是上一个没有持股 买了这一支股票(效益减少,所以为-prices[i])
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
//上一个卖出的操作数,或者是上一个持股卖了了这一支股票(效益增加,所以为+prices[i])
dp[i][j+1]=Math.max(dp[i-1][j+1],dp[i-1][j]+prices[i]);
}
}
return dp[n-1][k*2];
}
}
309. 最佳买卖股票时机含冷冻期(中等)
题目:
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1]
输出: 0
提示:
1 <= prices.length <= 5000
0 <= prices[i] <= 1000
思路:
此处分为三个状态
class Solution {
public int maxProfit(int[] prices) {
//默认的初始条件记得写,此处可写可不写
if (prices == null || prices.length == 0) return 0;
int n=prices.length;
//此处定义三个状态变量
// 0为持股
// 1为不持股 且还在冷冻期
// 2为不持股,过了冷冻期
int [][]dp=new int[n][3];
dp[0][0]=-prices[0];
for(int i=1;i<n;i++){
//持有股票,分为昨天持有,或者过了冷冻期买入的持有
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][2]-prices[i]);
//不持有股票,还在冷冻期,即为上一份持有股票卖出
dp[i][1]=dp[i-1][0]+prices[i];
//不持有股票,过了冷冻期,即为上一次一直每卖出,或者上一次刚过冷冻期
dp[i][2]=Math.max(dp[i-1][1],dp[i-1][2]);
}
//因为此处分为两种情况,持股的一定效益最低
//比较两个没有持股的大小即可
return Math.max(dp[n-1][1],dp[n-1][2]);
}
}
714. 买卖股票的最佳时机含手续费(中等)
题目:
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 1:
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8
示例 2:
输入:prices = [1,3,7,5,10,3], fee = 3
输出:6
提示:
1 <= prices.length <= 5 * 104
1 <= prices[i] < 5 * 104
0 <= fee < 5 * 104
思路:
卖出的时候付fee,就不会弄混淆
class Solution {
public int maxProfit(int[] prices, int fee) {
int n=prices.length;
int [][]dp=new int[n][2];
// 0为持有股票买入
// fee先不着急付,在卖出的时候在付
dp[0][0]=-prices[0];
for(int i=1;i<n;i++){
// 1.昨天也持有;2. 昨天不持有,今天买入。两者取较大值。
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
// 卖出的时候 付fee的费用
//1. 昨天也不持有;2. 昨天持有,今天卖出。两者取较大值。
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]-fee);
}
return dp[n-1][1];
}
}
这是买入的时候付fee,注意其中的区别
class Solution {
public int maxProfit(int[] prices, int fee) {
int n=prices.length;
int [][]dp=new int[n][2];
// 0为持有股票买入,一开始先支付订单
dp[0][0]=-prices[0]-fee;
for(int i=1;i<n;i++){
//一开始可能不会买到上一个持有股票的,可能是卖出去的时候支付了订单,所以要在1这里付个费用(-fee 效益减少)
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 dp[n-1][1];
}
}
小总结
只有一次买卖
//持有股票数,昨天也持有以及今天买了(收益变成负数)
dp[i][0]=Math.max(dp[i-1][0],-prices[i]);
//不持有股票数,昨天也没有持有今天昨天持有今天卖了(收益变成正的)
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[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]);
限定了两次买卖
这部分的代码初始条件以及动态规划条件比较绕
//第一次的买入和上一次的买入相关,或者上一次没持有,加上这次的效益
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
//第一次的卖出和上一次的卖出有关,或者上一次的买入减去这一次的卖出利益
dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
//下面的3 4同理,
dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
dp[i][4]=Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
限定了k次买卖
不能再像上面一样,写那么多dp
需要找到它的动态规划的规律
for(int i=1;i<k*2;i+=2){
dp[0][i]=-prices[0];
}
//遍历整个数组
for(int i=1;i<n;i++){
//0操作数是没有的,所以从1开始遍历,遍历到k*2即可。不可携程j+2.需要写为j+=2
for(int j=1;j<k*2;j+=2){
//上一个买入的操作数,或者是上一个没有持股 买了这一支股票(效益减少,所以为-prices[i])
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
//上一个卖出的操作数,或者是上一个持股卖了了这一支股票(效益增加,所以为+prices[i])
dp[i][j+1]=Math.max(dp[i-1][j+1],dp[i-1][j]+prices[i]);
}
}
如果加了冷冻期
买卖的次数就多一个状态
买 以及 还在冷冻期的卖 不在冷冻期的卖
dp[0][0]=-prices[0];
for(int i=1;i<n;i++){
//持有股票,分为昨天持有,或者过了冷冻期买入的持有
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][2]-prices[i]);
//不持有股票,还在冷冻期,即为上一份持有股票卖出
dp[i][1]=dp[i-1][0]+prices[i];
//不持有股票,过了冷冻期,即为上一次一直每卖出,或者上一次刚过冷冻期
dp[i][2]=Math.max(dp[i-1][1],dp[i-1][2]);
}
如果加了手续费
则和上面多次买卖一样的思路,只是多了个手续费一样
dp[0][0]=-prices[0];
for(int i=1;i<n;i++){
// 1.昨天也持有;2. 昨天不持有,今天买入。两者取较大值。
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
// 卖出的时候 付fee的费用
//1. 昨天也不持有;2. 昨天持有,今天卖出。两者取较大值。
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]-fee);
}
子序列问题
子序列不满足都会有保存上一个的临界值,具体看情况而定
而子数组不满足条件,直接保存0
300. 最长递增子序列(中等)*(不连续)
题目:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
进阶:
你能将算法的时间复杂度降低到 O(n log(n)) 吗?
思路:
动态规划:
class Solution {
public int lengthOfLIS(int[] nums) {
//子序列不用连续即可
int n=nums.length;
//创建一个dp的数组
int [] dp=new int[n];
//将dp这个数组默认初始化为1,只有一个值也是一个序列
Arrays.fill(dp,1);
//统计其序列的最大长度
int ans=0;
//外层循环为序列最后一个值 的对比值
for(int i=0;i<n;i++){
//内层循坏为序列前面所有值,如果大于则dp值改变,而且改变的是dp【j】+1去比较,这个值不用存储
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;
}
}
另外一种方式是:动态规划+二分查找(这种题解比较难熬)
具体参考题解如下:
k神题解
// Dynamic programming + Dichotomy.
class Solution {
public int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int res = 0;
for(int num : nums) {
int i = 0, j = res;
while(i < j) {
int m = (i + j) / 2;
if(tails[m] < num) i = m + 1;
else j = m;
}
tails[i] = num;
if(res == j) res++;
}
return res;
}
}
674. 最长连续递增序列(简单)(连续)*
题目:
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。
提示:
1 <= nums.length <= 104
-109 <= nums[i] <= 109
思路:
这道题与上一道题一样,只不过这道题是连续的
暴力遍历,两层for循环
class Solution {
public int findLengthOfLCIS(int[] nums) {
//先判断初始值条件,如果为空或者长度为0,则返回0
if (nums == null || nums.length == 0) return 0;
//如果长度为1,提前返回为1,因为后面的条件进不去了
if(nums.length==1)return 1;
//创建一个dp数组,并且一开始初始化都为1
int []dp=new int[nums.length];
Arrays.fill(dp,1);
int sum=0;
//外层为【0,n-1),内层为【1,n)
//下一个数组大于上一个数组值,则dp【i】++。
//本身是二维数组,要用一维数组存储各个值
for(int i=0;i<nums.length-1;i++){
for(int j=i+1;j<nums.length;j++){
if(nums[j]>nums[j-1])dp[i]++;
else break;
}
sum=Math.max(sum,dp[i]);
}
return sum;
}
}
优化上一个代码模块,变为一个for循环即可
class Solution {
public int findLengthOfLCIS(int[] nums) {
int[] dp = new int[nums.length];
Arrays.fill(dp,1);
int res = 1;
for (int i = 0; i < nums.length - 1; i++) {
//判断各个数之间的值,都记录在上一个dp【i+1】中
if (nums[i ] < nums[i+1]) {
//dp数组一个个计数
//dp【0】为1, dp【1】开始计数
dp[i + 1] = dp[i] + 1;
}
res=Math.max(dp[i+1],res);
}
return res;
}
}
或者使用滑动窗口,这种时间比上一种要快得多
class Solution {
public int findLengthOfLCIS(int[] nums) {
//left标记一开始的节点,right节点用来比较以及跑动
int left = 0, right = 0;
int res = 1;
//right节点小于数组长度
while (right < nums.length) {
while (right < nums.length - 1 && nums[right] < nums[right + 1]) {
right++;
}
//本身默认的自身最小也为1
res = Math.max(res, right - left + 1);
//right以及left节点都变回下一个字符,重新开始匹配
right = right + 1;
left=right;
}
return res;
}
}
718. 最长重复子数组(中等)(连续)
题目:
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 100
思路:
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int m=nums1.length;
int n=nums2.length;
int [][]dp=new int [m+1][n+1];
int sum=0;
//求当前值的关系,通过上一个值推导出,dp[i][j]=dp[i-1][j-1]+1;
//所以创建的数组长度要多加一个1
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(nums1[i-1]==nums2[j-1]){
//下一个dp是由上一个dp推导出来
dp[i][j]=dp[i-1][j-1]+1;
sum=Math.max(sum,dp[i][j]);
}
}
}
return sum;
}
}
用一维数组保存
class Solution {
public int findLength(int[] nums1, int[] nums2) {
//用一维滚动数组保存其值就可,任何一个数组都可
int []dp=new int[nums2.length+1];
int sum=0;
//一个数字从第一位,另外一个数字从最后一位,每个数字都是一个dp保存着。依次遍历
for(int i=1;i<=nums1.length;i++){
//这边这个遍历要从后往前,因为从前往后会被覆盖掉
for(int j=nums2.length;j>0;j--){
if(nums1[i-1]==nums2[j-1]){
dp[j]=dp[j-1]+1;
}else {
dp[j]=0;
}
sum=Math.max(sum,dp[j]);
}
}
return sum;
}
}
1143. 最长公共子序列(中等)(不连续)
题目:
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
示例 2:
输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。
示例 3:
输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。
提示:
1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。
思路:
与上一道题有些类似,只不过这是不连续的
上一道题如果是相等的,则dp[i][j] = dp[i - 1][j - 1] + 1;
,不相等则置为0,并且找不到对应的关系了。而且最大值的取值临界条件不一样,注意区分
但是这一道题,如果相等,则dp[i][j] = dp[i - 1][j - 1] + 1;
,不相等,上一个对应的条件不要遗漏掉,而不是置为0
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m=text1.length();
int n=text2.length();
//上一个边界影响下一个边界,最后加到m 与n,所以定义为[m+1][n+1]的长度
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-1][j],dp[i][j-1]);
}
}
}
return dp[m][n];
}
}
1035. 不相交的线(中等)(不连续)
题目:
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足满足:
- nums1[i] == nums2[j]
- 且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
示例 1:
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。
示例 2:
输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3
示例 3:
输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2
提示:
1 <= nums1.length, nums2.length <= 500
1 <= nums1[i], nums2[j] <= 2000
思路:
这道题其实和上道题思路一样,求公共子序列的最大值,只有公共子序列一模一样了才不会相交
不用公共子数组去计算
class Solution {
public int maxUncrossedLines(int[] nums1, int[] nums2) {
int m=nums1.length;
int n=nums2.length;
int [][]dp=new int[m+1][n+1];
for(int i=1;i<=m;i++){
for(int j=1;j<=n;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[m][n];
}
}
53. 最大子数组和(简单)(连续)
题目:
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
思路:
最主要的思路是这样,但是为了减少多一个空间存储,可以利用滑动数组
通过pre存储其各个值。而且每个数组位置也要存储其最大值。用两个math.max函数
//比较经典的动态规划题目
class Solution {
public int maxSubArray(int[] nums) {
int pre=0;
int max=nums[0];
for(int i=0;i<nums.length;i++){
pre=Math.max(pre+nums[i],nums[i]);
max=Math.max(max,pre);
}
return max;
}
}
编辑距离
392. 判断子序列(简单)
题目:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
进阶:
如果有大量输入的 S,称作 S1, S2, … , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?
致谢:
特别感谢 @pbrother 添加此问题并且创建所有测试用例。
示例 1:
输入:s = “abc”, t = “ahbgdc”
输出:true
示例 2:
输入:s = “axc”, t = “ahbgdc”
输出:false
提示:
0 <= s.length <= 100
0 <= t.length <= 104
两个字符串都只由小写字符组成。
思路:
同样此题暴力遍历不会超时:
利用双指针
class Solution {
public boolean isSubsequence(String s, String t) {
int n = s.length(), m = t.length();
int i = 0, j = 0;
while (i < n && j < m) {
if (s.charAt(i) == t.charAt(j)) {
i++;
}
j++;
}
return i == n;
}
}
这道题目同样也是子序列的题目,同样的思路
多了一个判断的条件而已
最后如果总和等于其s的长度,即为满足
class Solution {
public boolean isSubsequence(String s, String t) {
int m=s.length();
int n=t.length();
int [][]dp=new int[m+1][n+1];
for(int i=1;i<=m;i++){
for(int j=1;j<=n;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]);
}
}
}
return dp[m][n]==m;
}
}
115. 不同的子序列(困难)
题目:
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)
题目数据保证答案符合 32 位带符号整数范围。
示例 1:
输入:s = “rabbbit”, t = “rabbit”
输出:3
解释:
如下图所示, 有 3 种可以从 s 中得到 “rabbit” 的方案。
rabbbit
rabbbit
rabbbit
示例 2:
输入:s = “babgbag”, t = “bag”
输出:5
解释:
如下图所示, 有 5 种可以从 s 中得到 “bag” 的方案。
babgbag
babgbag
babgbag
babgbag
babgbag
提示:
0 <= s.length, t.length <= 1000
s 和 t 由英文字母组成
思路:
class Solution {
public int numDistinct(String s, String t) {
int m=s.length();
int n=t.length();
int [][]dp=new int[m+1][n+1];
//初始化第一列都为0,也就是字符串s删除到最后剩下一个空字符串也会和t字符串匹配,也就一个字符配对
//dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。
//那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1
//具体初始化的范围为【0,m】0也要算进去,此题比较特殊,因为它的动态规划公式涉及到上一步的操作,(也就是s这一行上一步的操作)
for(int i=0;i<=m;i++){
dp[i][0]=1;
}
//不用初始化第一行,也就是字符串s再怎么删除 都不会跟空字符串配对,所以都变为0,默认不用初始化了
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(s.charAt(i-1)==t.charAt(j-1)){
//如果配对成功,则执行上一步的操作dp[i-1][j-1](二维矩阵的正对角)再加上这一步的操作dp[i-1][j]
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
}else {
//配对不成功,则保存上一步的值,通过s这个字符串配对,所以是dp[i-1][j]
dp[i][j]=dp[i-1][j];
}
}
}
return dp[m][n];
}
}
583. 两个字符串的删除操作(中等)
题目:
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步 可以删除任意一个字符串中的一个字符。
示例 1:
输入: word1 = “sea”, word2 = “eat”
输出: 2
解释: 第一步将 “sea” 变为 “ea” ,第二步将 "eat "变为 “ea”
示例 2:
输入:word1 = “leetcode”, word2 = “etco”
输出:4
提示:
1 <= word1.length, word2.length <= 500
word1 和 word2 只包含小写英文字母
思路:
这道题跟上一道题不大一样,这个是两个字符串都可以删除
如果两者相等以及两者不同的情况,具体情况如下:
- 相同的情况下,
dp[i][j] = dp[i - 1][j - 1];
- 不相同的情况下,删除s字符串的一个,或者删除t字符串的一个,或者同时删除s与t字符串才配对
dp[i][j] = Math.min(dp[i - 1][j - 1] + 2,Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
具体的代码如下:
初始化的方式和上一道题有所区别,具体看下面的代码注释
class Solution {
public int minDistance(String word1, String word2) {
int m=word1.length();
int n=word2.length();
int [][]dp=new int [m+1][n+1];
//word1删除元素,每删除一个,初始化都是下标删除的个数
for(int i=0;i<=m;i++){
dp[i][0]=i;
}
//word2删除元素,同理
for(int j=0;j<=n;j++){
dp[0][j]=j;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
//如果配对,则保存上一个的值
dp[i][j]=dp[i-1][j-1];
}else {
//如果不配对要分三种情况,第一种是word1删除,第二种是word2删除,第三种是两者都删除。三者之间比较出最小值,记得每删除一个元素都是要加1
dp[i][j]=Math.min(dp[i-1][j-1]+2,Math.min(dp[i-1][j],dp[i][j-1])+1);
}
}
}
return dp[m][n];
}
}
72. 编辑距离(困难)
题目:
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
示例 2:
输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
提示:
0 <= word1.length, word2.length <= 500
word1 和 word2 由小写英文字母组成
思路:
此题的不匹配操作与上一题不一样
注意区分开
class Solution {
public int minDistance(String word1, String word2) {
int m=word1.length();
int n=word2.length();
int [][]dp=new int [m+1][n+1];
//word1删除元素,每删除一个,初始化都是下标删除的个数
for(int i=0;i<=m;i++){
dp[i][0]=i;
}
//word2删除元素,同理
for(int j=0;j<=n;j++){
dp[0][j]=j;
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
//如果配对,则保存上一个的值(和上一题一样)
dp[i][j]=dp[i-1][j-1];
}else {
//如果不配对要分三种情况,第一种是word1删除(也就是word2添加),第二种是word2删除(也就是word1添加)
//第三种是两者都删除(但是它的距离就变多了,变成跟上一题一样了)。所以此处变成替换(那操作还是加1)
//三者之间比较出最小值
dp[i][j]=Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i][j-1]))+1;
}
}
}
return dp[m][n];
}
}
小总结
主要总结个比较容易混淆的
最长递增子序列(刚开始初始化都为1)
//外层循环为序列最后一个值 的对比值
for(int i=0;i<n;i++){
//内层循坏为序列前面所有值,如果大于则dp值改变,而且改变的是dp【j】+1去比较,这个值不用存储
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]);
}
最长连续递增序列:(刚开始初始化都为1)
int res = 1;
for (int i = 0; i < nums.length - 1; i++) {
//判断各个数之间的值,都记录在上一个dp【i+1】中
if (nums[i ] < nums[i+1]) {
//dp数组一个个计数
//dp【0】为1, dp【1】开始计数
dp[i + 1] = dp[i] + 1;
}
res=Math.max(dp[i+1],res);
}
以下两个容易混淆的:
最长公共子序列的条件为:
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]);
}
最长重复子数组:
if (nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
result = Math.max(result, dp[i][j]);
}
//或者
if (nums1[i - 1] == nums2[j - 1]) {
dp[j] = dp[j - 1] + 1;
} else {
dp[j] = 0;
}
result = Math.max(result, dp[j]);
其他的编辑距离,条件比较清晰,如果概念不清晰的可看看上面的代码模块
647. 回文子串(中等)*
题目:
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = “abc”
输出:3
解释:三个回文子串: “a”, “b”, “c”
示例 2:
输入:s = “aaa”
输出:6
解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”
提示:
1 <= s.length <= 1000
s 由小写英文字母组成
思路:
补充中心拓展的思路做法:
挨个遍历,只不过中心可能是1或者2的往外延伸
class Solution {
public int countSubstrings(String s) {
//定义数组长度以及统计的个数sum
int n=s.length();
int sum=0;
//遍历整个字符串
for(int i=0;i<n;i++){
//中心拓展,只遍历两个数
for(int j=0;j<=1;j++){
//左边为left等于i
int left=i;
//右边向外拓展为 i+j
int right=i+j;
//此处往外拓展 往外循环,但是前提要满足以下条件
while(left>=0 && right<n && s.charAt(left--)==s.charAt(right++)){
sum++;
}
}
}
return sum;
}
}
如果用动态规划的思路做,具体代码如下:
主要题解参考了
两道回文子串的解法(详解中心扩展法)
字符配对成功的情况下:(多加下面的条件)
- 是否只有一个字符,也就是j-i要小于等于1
- 上一个dp的条件是否满足为true
具体dp的值主要通过如下推导。判断cabac是否是回文,则判断aba的区间为 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
class Solution {
public int countSubstrings(String s) {
// 动态规划法
boolean[][] dp = new boolean[s.length()][s.length()];
int ans = 0;
for (int j = 0; j < s.length(); j++) {
for (int i = 0; i <= j; i++) {
if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1])) {
dp[i][j] = true;
ans++;
}
}
}
return ans;
}
}
516. 最长回文子序列(中等)
题目:
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = “bbbab”
输出:4
解释:一个可能的最长回文子序列为 “bbbb” 。
示例 2:
输入:s = “cbbd”
输出:2
解释:一个可能的最长回文子序列为 “bb” 。
提示:
1 <= s.length <= 1000
s 仅由小写英文字母组成
思路:
还有另外一种解法是将其字符串反转,之后求其两个字符串的最长公共子序列
class Solution {
public int longestPalindromeSubseq(String s) {
int n=s.length();
//先将String转换为StringBuilder类型
StringBuilder str=new StringBuilder(s);
//在调用StringBuilder的反转函数,之后输出toString类型变为字符串类型
String s1=str.reverse().toString();
int [][]dp=new int[n+1][n+1];
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(s.charAt(i-1)==s1.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[n][n];
}
}
动态规划的代码如下:
(耗时比上面的少一半)
class Solution {
public int longestPalindromeSubseq(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for (int i = n - 1; i >= 0; i--) {
//两者相等的时候为1,默认一个字符就是1
dp[i][i] = 1;
//注意两者的范围选择
for (int j = i + 1; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) {
//从递推可看出,下层推上层,所以递归的顺序应该为从下到上,从左到右
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
//如果两者不相等,则取左边的i+1或者右边的j-1最大值即可
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
//返回右上角的值
return dp[0][n - 1];
}
}