LeetCode刷题日记之动态规划(二)

目录


前言

在上一篇文章中,我们初步了解了动态规划的基本思路。这次我将继续分享几道经典的动态规划题目,进一步深入这一解题方法的细节与技巧。希望这篇文章能够帮助大家更好地掌握动态规划✍✍✍


完全平方数

LeetCode题目链接

就是给一个整数n,然后让返回和为这个整数n的完全平方数的最少数量,所谓完全平方数就是另一个整数的平方

我们直接来梳理动态规划的五部曲

  • 确定dp数组和下标含义

    • 可以定义dp[i]来表示将整数i拆分为若干个完全平方数和时的最少完全平方数数量
  • 确定递推公式

    其中j*j是一个小于等于i的完全平方数
    dp[i] = min(dp[i], dp[i - j * j] + 1)

  • dp数组如何初始化

    因为i最多完全平方数组合便是由i个1组成,所以可以初始化为i
    dp[i] = i

  • 确定遍历顺序

    • 外层循环遍历 i 从 1 到 n,依次计算每个 i 的最优解。内层循环枚举每个小于等于 i 的完全平方数 j,更新 dp[i] 的值
  • 举例推导dp数组

    n = 12
    初始化 dp[12] = 12(最坏情况,12个1组成)
    dp[12] = min(dp[12], dp[12-11] + 1) = min(12, dp[11] + 1)
    dp[12] = min(dp[12], dp[12-2
    2] + 1) = min(dp[12], dp[8] + 1)
    dp[12] = min(dp[12], dp[12-3*3] + 1) = min(dp[12], dp[3] + 1)
    最终dp[12] 会更新为 3(4 + 4 + 4)

完整代码如下

java 复制代码
class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1];//初始化dp数组

        for(int i = 0; i <= n; i++){//初始化
            dp[i] = i;
        }

        //遍历1到n,计算每个dp[i]的值
        for(int i = 1; i <= n; i++){
            for(int j = 1; j * j <= i; j++){//遍历每个小于等于i的完全平方数
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1);//递推公式
            }
        }

        return dp[n];
    }
}

零钱兑换

LeetCode题目链接

这道题是有一个硬币数组coins,然后的话返回凑成一个所给总金额amount所需的最少硬币数

直接梳理动态规划的五部曲

  • 确定dp数组和下标含义

    • 定义 dp[i] 表示凑成金额 i 所需的最少硬币数
  • 确定递推公式

    对于每一个金额 i,如果选择某个硬币 coin,可以得到凑成金额 i - coin 所需的最少硬币数 dp[i - coin]
    dp[i]=min(dp[i],dp[i−coin]+1)

  • dp数组如果初始化

    • dp[0] = 0,因为凑成金额 0 不需要任何硬币,其他的dp[i]初始化为amount+1,一个大数表示无法凑成也不会影响递推
  • 确定遍历顺序

    • 外层循环遍历金额从 1amount, 内层循环遍历每种硬币,更新每个 dp[i] 的最优解
  • 举例推导dp数组

    coins = [1, 2, 5],amount = 11
    对于 i = 1,用硬币 1,dp[1] = min(dp[1], dp[1-1] + 1) = 1
    对于 i = 2,用硬币 1 或 2,dp[2] = min(dp[2], dp[2-1] + 1) = min(dp[2], dp[2-2] + 1) = 1
    对于 i = 11,尝试用硬币 1、2、5,最终 dp[11] = 3(由硬币 5 + 5 + 1 组成)

完整代码如下

java 复制代码
class Solution {
    public int coinChange(int[] coins, int amount) {
        // 定义dp数组
        int[] dp = new int[amount + 1];

        // 初始化dp数组,dp[0] = 0,其他初始化为一个大数,表示暂时无法凑成
        Arrays.fill(dp, amount + 1);
        dp[0] = 0;

        for(int i = 1; i <= amount; i++){//外层循环遍历金额
            for(int coin : coins){//内层循环遍历每个硬币
                if(i - coin >= 0){//如果可以使用当前硬币
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                }
            }
        }

        // 返回结果,如果dp[amount]还是初始值,说明无法凑成
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

单词拆分

这道题就是给一个字符串s和字符串列表wordDict作为字典,判断字符串能否利用字典里的词来拼接出来

我们来梳理动态规划五部曲

  • 确定dp数组和下标含义

    • 定义布尔型数组 dp,其中 dp[i] 表示字符串 s 的前 i 个字符 s[0:i] 是否可以通过字典中的单词拼接而成
  • 确定递推公式

    对于每一个 i,我们检查从 0 到 i 的每个分割点 j,如果 dp[j] 为 true,且 s[j:i] 在字典 wordDict 中,则 dp[i] = true。
    dp[i]=dp[j] 且 s[j:i] 在字典中

  • dp数组如何初始化

    • dp[0] = true 表示空字符串可以被成功拼接,其他 dp[i] 初始化为 false,表示暂时无法拼接
  • 确定遍历顺序

    • 我们需要遍历从 1s.length() 的每个位置 i,并且对于每个 i 再遍历 j,检查 dp[j]s[j:i] 是否在字典中
  • 举例推导dp数组

    s = "leetcode",wordDict = ["leet", "code"]
    初始化 dp[0] = true,因为空字符串总是可以被拼接
    对于 i = 4,发现 s[0:4] = "leet" 在字典中,且 dp[0] = true,所以 dp[4] = true
    对于 i = 8,发现 s[4:8] = "code" 在字典中,且 dp[4] = true,所以 dp[8] = true
    最终,dp[8] = true,表示整个字符串 s 可以被字典中的单词拼接而成

完整代码如下

java 复制代码
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // 定义dp数组,dp[i]表示前i个字符是否可以被字典拼接成
        boolean[] dp = new boolean[s.length() + 1];

        dp[0] = true;// 初始化dp数组

        // 将wordDict转换为HashSet,方便快速查找
        Set<String> wordSet = new HashSet<>(wordDict);

        for(int i = 0; i <= s.length(); i++){
            for(int j = 0; j < i; j++){ // 遍历每一个分割点j
                // 检查dp[j]是否为true,且s[j:i]是否在字典中
                if(dp[j] && wordSet.contains(s.substring(j, i))){
                    dp[i] = true;// 更新dp[i]
                    break;
                }
            }
        }

        return dp[s.length()];
    }
}

这里可能不易理解的点

  • 为什么不用双指针一前一后的方式来解决呢?
    • 这种思路的问题在于,它并不会尝试所有可能的分割方式,只会走一条"贪心"路径。也就是说,如果在某个时刻有多种分割方式,它只能选择当前最匹配的单词,而不会回溯去考虑其他可能的分割方式。这种策略在某些情况下无法找到正确的拼接方式

总结

这里单词拆分的递推公式的逻辑大家可能需要多梳理几遍,弄清楚和贪心策略的区别,大家一起加油✊✊ ✊

相关推荐
黑胡子大叔的小屋27 分钟前
基于springboot的海洋知识服务平台的设计与实现
java·spring boot·毕业设计
ThisIsClark30 分钟前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
火星机器人life1 小时前
基于ceres优化的3d激光雷达开源算法
算法·3d
虽千万人 吾往矣1 小时前
golang LeetCode 热题 100(动态规划)-更新中
算法·leetcode·动态规划
雷神乐乐1 小时前
Spring学习(一)——Sping-XML
java·学习·spring
arnold662 小时前
华为OD E卷(100分)34-转盘寿司
算法·华为od
小林coding2 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
V+zmm101342 小时前
基于小程序宿舍报修系统的设计与实现ssm+论文源码调试讲解
java·小程序·毕业设计·mvc·ssm
ZZTC2 小时前
Floyd算法及其扩展应用
算法
文大。2 小时前
2024年广西职工职业技能大赛-Spring
java·spring·网络安全