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()];
    }
}

这里可能不易理解的点

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

总结

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

相关推荐
卡尔特斯3 小时前
Android Kotlin 项目代理配置【详细步骤(可选)】
android·java·kotlin
白鲸开源3 小时前
Ubuntu 22 下 DolphinScheduler 3.x 伪集群部署实录
java·ubuntu·开源
ytadpole3 小时前
Java 25 新特性 更简洁、更高效、更现代
java·后端
纪莫3 小时前
A公司一面:类加载的过程是怎么样的? 双亲委派的优点和缺点? 产生fullGC的情况有哪些? spring的动态代理有哪些?区别是什么? 如何排查CPU使用率过高?
java·java面试⑧股
JavaGuide4 小时前
JDK 25(长期支持版) 发布,新特性解读!
java·后端
用户3721574261354 小时前
Java 轻松批量替换 Word 文档文字内容
java
白鲸开源4 小时前
教你数分钟内创建并运行一个 DolphinScheduler Workflow!
java
CoovallyAIHub4 小时前
中科大DSAI Lab团队多篇论文入选ICCV 2025,推动三维视觉与泛化感知技术突破
深度学习·算法·计算机视觉
Java中文社群5 小时前
有点意思!Java8后最有用新特性排行榜!
java·后端·面试
代码匠心5 小时前
从零开始学Flink:数据源
java·大数据·后端·flink