第九章 动态规划part06

零钱兑换

题目描述

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

解题思路

  1. 确定dp数组以及下标的含义

    • dp[j]:凑足总额为j的金额需要的最少钱币个数为dp[j]
  2. 确定递推公式

    • 递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
    • 凑足总额为j - coins[i]的钱币最少个数为dp[j - coins[i]],因此只需要加上一个钱币coins[i]就能够凑足总额为j的金额,此时所需要的最小金额就是dp[j]dp[j - coins[i]] + 1 总的最小值。
  3. dp数组初始化

    • 凑足金额j为0的钱币个数一定是0,因此dp[0] = 0,根据递推公式特性,因为我们需要取得较小值,因此可以将其他金额的情况初始化为整数最大值
  4. 确定遍历顺序

    • 本题中先遍历钱币在遍历背包都是可以的,无非是写法不同而已,本题使用的先遍历钱币后遍历背包的方式,遍历方式都是正序遍历。
  5. 举例推导dp数组

    • 以输入:coins = [1, 2, 5], amount = 5为例,手动推举dp数组:

      image

代码实现

测试地址:https://leetcode.cn/problems/coin-change/

class Solution {
public:
    int coinChange(vector<int> &coins, int amount) {
        // 初始化 dp 数组
        vector<int> dp(amount + 1, INT_MAX);
        dp[0] = 0;

        // 遍历所有硬币
        for (int i = 0; i < coins.size(); i++) {
            // 遍历从该硬币面额到总金额的所有金额
            for (int j = coins[i]; j <= amount; j++) {
                // 如果可以组成金额 j - coins[i]
                if (dp[j - coins[i]] != INT_MAX) {
                    // 更新 dp[j]
                    dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
                }
            }
        }

        // 如果无法组成该金额,返回 -1
        if (dp[amount] == INT_MAX)
            return -1;

        // 否则返回组成该金额所需的最小硬币数量
        return dp[amount];
    }
};

完全平方数

题目描述

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

示例 1:

输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9

解题思路

  1. 确定dp数组以及下标的含义

    • dp[j]:要组合和为j的完全平方数所需要使用的最少数量为dp[j]
  2. 确定递推公式

    • dp[j]可以由dp[j-i*i]所推出,因此dp[j - i * i] + 1 便可以凑成dp[j],由于我们需要求的是使用的最小平方数的个数
    • 递推公式:dp[j] = min(dp[j - i * i] + 1, dp[j]);
  3. dp数组初始化

    • 题目提到了n从1开始,而且并没有明确表示平方数从0开始,因此我们可以初始化dp[0]=0,因为我们需要求的是最少个数,因此可以将非0下标的其他值初始化为最大值。
  4. 确定遍历顺序

    • 先遍历背包和先遍历物品都是可以的,本题采用的是先遍历背包后遍历物品的方式,顺序均为正序。
  5. 举例推导dp数组

    • 已输入n为5例,dp状态图如下:

      image

代码实现

测试地址:https://leetcode.cn/problems/perfect-squares/

class Solution {
public:
    int numSquares(int n) {
        // 创建一个 dp 数组,大小为 n+1, 初始值设为 INT_MAX 表示无限大
        vector<int> dp(n + 1, INT_MAX);
        // 递归基,0的最小分解为0个完全平方数
        dp[0] = 0;

        // 遍历从1到n的所有数字
        for (int j = 0; j <= n; j++) {
            // 遍历所有可能的完全平方数,从1开始,i*i是完全平方数
            for (int i = 1; i * i <= j; i++) {
                // 更新 dp[j],选取使用当前完全平方数i*i和不使用时的较小值
                dp[j] = min(dp[j - i * i] + 1, dp[j]); // dp[j - i*i] + 1 表示如果选用当前的完全平方数,则需加1(因为使用了一次)
            }
        }
        // 返回组成整数n所需的最少完全平方数
        return dp[n];
    }
};

单词拆分

题目描述

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

注意: 不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

解题思路

  1. 确定dp数组以及下标的含义

    • dp[i] 表示字符串s的前i个字符能否被成功分割成一个或多个在字典中的单词。这里i是从0到字符串s的长度。
  2. 确定递推公式

    • 如果存在一个j(0 <= j < i),使得从ji的子字符串(即s.substr(j, i - j))是字典中的一个词,并且dp[j]true(表示s的前j个字符可以被成功分割),那么dp[i]应该是true
    • 递推公式:dp[i]=dp[j] and s[j:i] in wordDict for any j such that 0≤j<i
  3. dp数组初始化

    • 初始化dp[0] = true,因为空字符串可以被认为是任何字典的子集,不需要分割。其余dp[i]初始化为false,表示开始时假设没有有效分割。
  4. 确定遍历顺序

    • 外层循环遍历字符串s从1到长度n(这里n是s.size()),内层循环遍历从0到i-1,尝试找到一个j,使得s.substr(j, i - j)是字典中的词。这样确保我们在决定dp[i]前,已经计算了所有的dp[j]
  5. 举例推导dp数组

    • s = "leetcode"wordDict = ["leet", "code"]为例,手推dp数组如下:

      • dp[0] = true(基础情况)
      • dp[4]j = 0时,s.substr(0, 4) = "leet"是字典中的词,dp[0]true,所以dp[4]成为true
      • dp[8]j = 4时,s.substr(4, 4) = "code"是字典中的词,dp[4]true,所以dp[8]成为true
      • dp[s.size()]dp[8]true,表示字符串可以完全分割使用字典中的词。

代码实现

测试地址:https://leetcode.cn/problems/word-break/

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        // 将字典转换为无序集合,以便快速查找
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        // 初始化dp数组,大小为s.size() + 1,所有元素初始化为false
        vector<bool> dp(s.size() + 1, false);
        // 基础情况:空字符串可以被成功分割
        dp[0] = true;

        // 外层循环遍历字符串s的每个位置i,表示当前考虑的子字符串的结束位置
        for (int i = 1; i <= s.size(); i++) {   // 遍历背包
            // 内层循环遍历从0到i-1的每个位置j,表示当前考虑的子字符串的起始位置
            for (int j = 0; j < i; j++) {       // 遍历物品
                // 获取从j到i的子字符串
                string word = s.substr(j, i - j); //substr(起始位置,截取的个数)
                // 检查子字符串是否在字典中,并且dp[j]是否为true
                if (wordSet.find(word) != wordSet.end() && dp[j]) {
                    // 如果条件满足,设置dp[i]为true
                    dp[i] = true;
                }
            }
        }
        // 返回dp数组的最后一个元素,表示整个字符串是否可以被成功分割
        return dp[s.size()];
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值