leetcode刷题分类刷题之动态规划

本文详细介绍了如何使用动态规划解决LeetCode上的问题,从入门篇开始,讲解了动态规划的基本思想,包括如何确定问题是否适用动态规划,状态转移方程的建立,以及边界条件的设定。通过实例解析了诸如买卖股票最佳时机、爬楼梯、最大子序和等问题,逐步从简单到中等难度,展示了动态规划的应用,并讨论了部分难题的解题策略。

leetcode刷题分类刷题之动态规划

入门篇

如何使用动态规划

首先我们要确认是否能使用动态规划

  1. 问题的答案依赖于问题的规模 ,也就是说这个问题能否被拆分为一个小规模的问题也就是一个F(N)的问题能否转化为F(i)的问题
  2. F(N) 是否与之前的F(i)答案有关系 也就是能否建立状态转移方程

状态转移方程

  1. 也就是我们常说的dp[i] 或者dp[i][j]到底是代表什么
  2. dp[i] 和之前的有什么联系能够用dp [0] --dp[i-1]的值推出dp[i]

确认边界条件

  1. 也就是dp[0] 是多少

简单

leetcode121 买卖股票的最佳时机

  1. 定义子问题
    dp[i] 表示的就是前i天中 能够获得的最大利润

  2. 思考递推公式
    第i天的时候我们有两种选择
    第一种就是 不卖也就是
    dp[i] = dp[i-1];
    第二种就是卖
    dp[i] = nums[i] -minPrices;
    所以综合一下就有
    dp[i] = Max(dp[i-1],nums[i]-minPrices)

  3. 确认边界条件
    dp[0]=0;
    minPrices=nums[i];

  4. 能够做空间优化
    由于根据递推公式我们可以得到实际上每个dp[i] 只和dp[i-1]相关 所以我们也就不需要这个数组 直接用 一个变量来保存就可以了

leetcode70爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

  1. dp[i] 代表 第i层有的方法
  2. 第二层解题可以怎么上呢
    从i-1上一层
    从i-2上两层
    可以得到 dp[i]=dp[i-1]+dp[i-2]
  3. 边界 dp(1) =1 dp(2)=2
  4. 优化 找两个变量替代dp[i-1] 和dp[i-2]
int i1 =1;
int i2=2;
int res =0;
for(int i =0;i<n;i++)
{
res = i1+i2;
i1 =i2;
i2 =res;
}
return res ;

leetcode57 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和

1.dp[i]代表以nums[i]元素结尾的的最大值
2. 两种情况 选num[i] 和不选它
dp[i] = Max(dp[i-1]+nums[i],nums[i])
3. 边界dp(1) = num[i];
4. 结果MAX(dp)
5. 优化空间 这个题目我们就无法用一个变量来保存dp[i-1] 因为 注意了我们的dp[i]定义的是以nums[i] 元素结尾的最大值 所以dp[n] 可能就不是我们最终要的结果 所以,要保存过程中的解

leetcode198 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

  1. dp[i] 表示前i个的打劫能抢到的最高金额
  2. nums[i] 有两种情况 打劫和不劫
    dp[i] = Max(dp[i-2]+nums[i],dp[i-1])
  3. dp(0) = nums(0) dp(1)= Max(nums(1),nums(0))
  4. 优化 可以用两个变量保存因为我们求的就是前n个的最高金额

中等难度

中等难度的问题一般

  • dp数组也由一维变成 了二维 循环也由一维变成二维
  • dp的动态表达也更加有难度

leetcode392判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

  1. dp[i][j] 就代表 s的前i个字符串 能不能匹配 t数组的前j个字符

  2. 如果 s[i] == t[j] 那么也就是说 dp[i][j] =dp[i-1][j-1](因为 s中的i字符已经和t中的j字符匹配了,所以看dp[i][j] 也就是看dp[i-1][j-1]
    )
    如果s[i]!=t[j] 那么dp[i][j] =dp[i][j-1];

  3. 边界条件 s[0] ==t[0] —>dp[0][0] =true;
    反之就是dp[0][0] =false;

  4. 优化 应该也可以 用一个一维数组去替换一个二维数组

第二种思路比较难理解

  1. dp[i][j] 代表的是t中从i 开始字母j第一次出现的位置(这其中字母就是a-z)也就是说dp[i][a]…dp[i][z]的一个i*26的二维数组

  2. t中没有s[i] 返回false

         if(dp[add][s[i] - 'a'] == m){
             return false;
         }
    

    否则直接跳到t中s[i]第一次出现的位置之后一位

         add = dp[add][s[i] - 'a'] + 1;
    
        for(int i = 0;i<n;i++){ //对s进行遍历
            //t中没有s[i] 返回false
            if(dp[add][s[i] - 'a'] == m){
                return false;
            }
            //否则直接跳到t中s[i]第一次出现的位置之后一位
            add = dp[add][s[i] - 'a'] + 1;
        }
  1. 从后向前遍历 初始化边界条件,dp[i][j] = m表示t中不存在字符j
    for(int i=0;i<26;i++){
    dp[i][m] = m;
    }

leetcode 62 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?

在这里插入图片描述

  1. dp[i][j] 代表的就是机器人到达num[i][j] 位置的几种方法
  2. 很明显 dp[i][j] = dp[i][j-1]+dp[i-1][j] 但是考虑边界问题
    边界上方和左方的到达路径可能
    上方 dp[i][j] = d[i][j-1]
    左方 dp[i][j] = d[i-1][j]
  3. 很明显的是上方和左方的格子dp[i][j] 全部都是1
  4. 优化问题 我们实际上用到的只有当前行cur 和之前的行 pre
    因此只需要保留当前行与上一行的数据 (在动态方程中,即pre[j] = dp[i-1][j]),两行,空间复杂度O(2n)
        Arrays.fill(cur,1);
        for (int i = 1; i < m;i++){
            for (int j = 1; j < n; j++){
                cur[j] = cur[j-1] + pre[j];
            }
            pre = cur.clone();
        }
        return pre[n-1]; 
  1. 优化2
    cur[j] += cur[j-1], 即cur[j] = cur[j] + cur[j-1] 等价于思路二–>> cur[j] = pre[j] + cur[j-1],因此空间复杂度为O(n).

意思就是现在的cur 其实就是我们在优化一中的上一行pre 没变化的

        int[] cur = new int[n];
        Arrays.fill(cur,1);
        for (int i = 1; i < m;i++){
            for (int j = 1; j < n; j++){
                cur[j] += cur[j-1] ;
            }
        }
        return cur[n-1]

leetcode 中还有一个扩展问题 有障碍物 那其实也是一样的只是多了一种情况在这里插入图片描述

最小路径和

  1. dp[i][j]dp[i][j] 的值代表直到走到 (i,j) 的最小路径和
  2. dp[i][j] = min(dp[i-1][j],dp[i][j-1])+num[i][j]
    考虑边界情况 上方和左方
    左方 :grid[i][j] = grid[i][j - 1] + grid[i][j];
    上方:grid[i][j] = grid[i - 1][j] + grid[i][j];
  3. 初始就是dp[0][0] 就是num[0][0]
  4. 空间优化 就是可以原地bp 就是修改原数组中的内容

该题的最好做法是

最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。

  1. 第一个注意点是对数组而言 子序列是可以不连续的 子串是必须连续的
  2. 上升代表着 [1,2,2,2] 这种就不是上升 也就是 a[t]>a[t-1]

言归正传

  • 定义dp[i] 是以第i个元素结尾最长子序列长度
  • 动态规划 也比较容易想到 但是注意这个和我们之前的题目是不同的 之前一般只要找到dp[i] 之前的一个或者两个元素 就可以了 但是该题的不同点是 不仅仅需要 dp[i] 之前所有的长度信息 而且还要结合着num[i] 进行判断
    也就是dp[i] = Max(所有满足条件的dp[u]+1)
    所谓的满足条件也就是 所有的dp[u] 对应的nums[u] < nums[i]-(也就是dp[i]对应的的数字)
//上面的话理解了的活  它在程序中的表现 其实也就是两层for循环
        for(int t =1;t<i;t++)
                for(int j =0;j<t;j++)
                {
                    if(nums[j]<nums[t])
                        dp[t]= Math.max(dp[t],dp[j]+1);
                }
  • 初始值就是 dp[1-n]=1
  • 空间优化 没有办法 因为用到了之前的数据 而不是和某个数据相关联的

丑数
编写一个程序,找出第 n 个丑数。丑数就是质因数只包含 2, 3, 5 的正整数。
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。

  • 这个dp还是比较难想到的
    + dp2 = 上次乘以2的最大值的坐标位置
    + dp3 = 上次乘以 3 的最大值的坐标位置
    + dp5 = 上次乘以 5 的坐标位置
  • num[i] = Min(num[dp2]*2,num[dp3]*3,num[dp5]*5)
      + 注意比完之后别忘了更新 dp2,dp3,dp5的位置
    
  • 初始化就是 dp2=dp3=dp5=0; //从第一个位置开始
  • 返回num[n]
        for i in range(1, 1690):
            ugly = min(nums[i2] * 2, nums[i3] * 3, nums[i5] * 5)
            nums.append(ugly)

            if ugly == nums[i2] * 2: 
                i2 += 1
            if ugly == nums[i3] * 3:
                i3 += 1
            if ugly == nums[i5] * 5:
                i5 += 1

不同的二叉搜索树

这的题目就属于经验了

  • 假设n个节点存在二叉排序树的个数是G(n),令f(i)为以i为根的二叉搜索树的个数,则
    G(n) = f(1) + f(2) + f(3) + f(4) + … + f(n)
  • 当i为根节点时,其左子树节点个数为i-1个,右子树节点为n-i,则
    f(i) = G(i-1)*G(n-i)f(i)
  • 综合两个公式可以得到 卡特兰数 公式
    G(n) = G(0)*G(n-1)+G(1)*(n-2)+…+G(n-1)*G(0)

乘积最大子数组

困难

困难的题目主要回 体现在

  • dp的转移方程中 一般非常难想到
  • 其他问题结合dp来实现 就是dp可能只是解决该问题的一个子问题

零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

在这里插入图片描述
本来是没有灵感的但是看到上面这个图 嗯 有点思路了
其实本质上还是青蛙跳的题目
现在有一个青蛙 能跳[1,2,5] 问他最少跳几下跳到n层楼 只是现在没有你有几种跳到方法了

  • 我们dp[i] 就代表 青蛙最少跳到i层楼的次数
  • 在这里插入图片描述
    public int coinChange(int[] coins, int amount) {
        int dp[] = new int[amount+1];
        dp[0]=0;
        Arrays.fill(dp,1,amount+1, Integer.MAX_VALUE-1);  //fill是一个左闭右开的函数  ,注意这个地方也必须要减一 因为 下面可能会产生越界
        for (int i = 0; i < dp.length; i++) {

            for(int t:coins)
            {
                if(i-t<0)
                continue;//不符合条件 直接跳过
                else
                dp[i] = Math.min(dp[i-t]+1,dp[i]);
            }
        }
        System.out.println(Arrays.toString(dp));
        if(dp[amount]==Integer.MAX_VALUE-1)
        return -1;
        else
        return dp[amount];
    }

方法二
上面的是dp 其实你还是应该想到 遇到这种问题我们最本能的应该是
先用大的减 减完用小的减 不行了在回溯

回溯的思想

但是要注意一个很烦人的问题 [2,7] 要凑8元 本来是可以的但是减完 7 就不行了 所以
回溯的 硬币终点因该是0

硬币 假设一堆由1分、2分、5分组成的n个硬币总面值为m分,求一共有多少种可能的组合方式(某种面值的硬币可以数量可以为0)。

这个题目是滴滴的面试题目 当时想的用深度遍历

后来仔细思考 发现如下思路
如果问题变为 只有1 和2硬币的话
那么能够唯一组成区间[n,2n]范围里的任意面值。 且方法只有一种

        for(i = 0 ; i <= m/5 && i <= n; i ++){  //首先确定面值为5 的硬币可能出现的张数。
            int temp = m - i*5;  //剩下的面值
            int tempcount = n - i;  //剩下的张数

            if(tempcount * 1 <= temp && tempcount * 2 >= temp)
                ans ++;
        }

最大的子数组乘积
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

这一题 我想错了第一开始我想构建的dp[i] 是以nums[i] 结尾的最大子数组乘积然后
然后再用一个标志位来替代前面有没有负数 结果导致 条件太多太复杂没过…

正确的思路应该是

  • 遍历数组时计算当前最大值,不断更新
  • 令imax为当前最大值,则当前最大值为 imax = max(imax * nums[i], nums[i])
  • 由于存在负数,那么会导致最大的变最小的,最小的变最大的。因此还需要维护当前最小值imin,imin = min(imin * nums[i], nums[i])
  • 当负数出现时则imax与imin进行交换再进行下一步计算

作者:guanpengchn
链接

class Solution {
    public int maxProduct(int[] nums) {
        int max = Integer.MIN_VALUE, imax = 1, imin = 1;
        for(int i=0; i<nums.length; i++){
            if(nums[i] < 0){ //交换
              int tmp = imax;
              imax = imin;
              imin = tmp;
            }
            imax = Math.max(imax*nums[i], nums[i]); //算最大
            imin = Math.min(imin*nums[i], nums[i]);//最小
            max = Math.max(max, imax);//保存最大值
        }
        return max;
    }
}

最长有效括号

正则表达式匹配

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

dp思路就是

dp[i] 代表i被拆分的最大值

dp[i] = Max(j * (Max(i-j),dp[i -j])(1<j<i))

初始化 dp[0] = 0 dp[1] =0 dp[2]=1

class Solution {
    public int integerBreak(int n) {

        int dp [] = new int[n+1];
        dp[0] =0;
        dp[1] =0;
        dp[2]=1;
        for (int i = 3; i < dp.length; i++) {
            for (int j = 1; j < i; j++) {
                dp[i] = Math.max(dp[i],j*Math.max(i-j,dp[i-j])); 
           //j*Math.max(i-j,dp[i-j]) 寻找每次拆分的最大值  
           // dp[i] = Math.max(dp[i],j*Math.max(i-j,dp[i-j])) 保存住拆分的最大值
            }
        }
        return dp[dp.length-1];
    }
}

当然本题最好的做法还是找3的数量

class Solution {
    public int integerBreak(int n) {
        if(n <= 3) return n - 1;
        int a = n / 3, b = n % 3;
        if(b == 0) return (int)Math.pow(3, a);
        if(b == 1) return (int)Math.pow(3, a - 1) * 4; // 2*2>3*1  要变化一下
        return (int)Math.pow(3, a) * 2;
    }
}

编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)

这里根据 大佬的理解写一遍自己的题解

dp[i][j] 代表单词A的前i个变成单词B的前j个要 编辑的距离 单词AB可能为空 所以我们要考虑i和j等于0的情形

在这里插入图片描述
也就是是下面这种 也即 我们的初始化 dp[0][i] =i dp[i][0] =i

状态转移方程

  1. 增,dp[i][j] = dp[i][j - 1] + 1
  2. 删,dp[i][j] = dp[i - 1][j] + 1
  3. 改,dp[i][j] = dp[i - 1][j - 1] + 1
    所以由 dp [i][j] = min(增,删,改)

初始化
也就是dp[0][i] =i dp[i][0] =i

结果就是dp[A.length][B.length]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值