24暑假算法刷题 | Day38 | 动态规划 VI 完全&多重背包 | LeetCode 322. 零钱兑换,279. 完全平方数,139. 单词拆分;卡码网56. 携带矿石资源


322. 零钱兑换

点此跳转题目链接

题目描述

给你一个整数数组 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 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104

题解

典型的 完全背包问题 ——因为每种硬币的数量是无限的。同时,题目要求的是硬币的个数,也就是求组合数,故按照常规的 先遍历物品、再遍历背包大小 的顺序 动态规划 即可解决。

  • dp 数组含义: dp[j] 表示凑成总金额 j 需要的最小硬币个数

  • 状态转移方程: dp[j] = min(dp[j], dp[j - coin] + 1)

    考虑一个新的硬币 coin ,如果加上它恰好凑成总金额 j ,则之前的总金额为 j - coin ,彼时所用的最小硬币个数为 dp[j - coin] ;加上新的这枚 coin ,所用硬币个数为 dp[j - coin] + 1 。然后取这个值和原来的 dp[j] 中较小的那个。

⚠️ 如果凑不出则返回-1,且每次要取 dp 最小值,故一开始要将 dp 数组中的元素初始化为极大值。

代码(C++)

int coinChange(vector<int> &coins, int amount)
{
    vector<int> dp(amount + 1, INT_MAX);
    dp[0] = 0;
    for (int coin : coins) {
        for (int j = coin; j <= amount; ++j) {
            if (dp[j - coin] < INT_MAX)
                dp[j] = min(dp[j], dp[j - coin] + 1);
        }
    }
    return dp[amount] == INT_MAX ? -1 : dp[amount];
}

279. 完全平方数

点此跳转题目链接

题目描述

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

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

示例 1:

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

示例 2:

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

提示:

  • 1 <= n <= 104

题解

由题可知,拆分出来的完全平方数是可以重复的,所以这是个 完全背包问题 ;同时,题目要求的是完全平方数的个数,也就是求组合数,故按照常规的 先遍历物品、再遍历背包大小 的顺序 动态规划 即可解决。

  • dp 数组含义: dp[j] 表示和为 j 的完全平方数的最少数量

  • 状态转移方程: dp[j] = min(dp[j], dp[j - pow(i, 2)] + 1)

    考虑一个新的完全平方数 pow(i, 2)i 的平方),如果加上它恰好凑成 j ,则之前的和为 j - pow(i, 2) ,彼时所用的最少完全平方数个数为 dp[j - pow(i, 2)] ;加上新的这个数,所用完全平方数个数为 dp[j - pow(i, 2)] + 1 。然后取这个值和原来的 dp[j] 中较小的那个。

⚠️ 每次要取 dp 最小值,故一开始要将 dp 数组中的元素初始化为极大值。

代码(C++)

int numSquares(int n)
{
    vector<int> dp(n + 1, INT_MAX);
    dp[0] = 0;
    for (int i = 1; pow(i, 2) <= n; ++i) {
        for (int j = pow(i, 2); j <= n; ++j)
            dp[j] = min(dp[j], dp[j - pow(i, 2)] + 1);
    }
    return dp[n];
}

139. 单词拆分

点此跳转题目链接

题目描述

给你一个字符串 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 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • swordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

题解

这题乍一看似乎就是经典的 排列问题 ,所以首先尝试了一下回溯算法:

class Solution // 回溯算法,超时
{
private:
    // KMP算法,判断某个字符串是不是另一个字符串的子串
    bool isSubStr(const string &text, const string &pattern)
    {
        // 获取next数组
        auto Next = [](const string &pattern) -> vector<int>
        {
            vector<int> next(pattern.size(), 0);
            int j = 0;
            for (int i = 1; i < pattern.size(); ++i)
            {
                while (j > 0 && pattern[i] != pattern[j])
                    j = next[j - 1];
                if (pattern[i] == pattern[j])
                    j++;
                next[i] = j;
            }
            return next;
        };
        vector<int> next = Next(pattern);
        // 检查模式串是否为文本串的子串
        int j = 0;
        for (int i = 0; i < text.size(); ++i)
        {
            while (j > 0 && text[i] != pattern[j])
                j = next[j - 1];
            if (text[i] == pattern[j])
                j++;
            if (j == pattern.size())
                return true;
        }
        return false;
    }

    // 回溯算法
    string path = "";
    bool flag = false;
    int count = 0;
    void backTracking(
        string &path, 
        const string &target,
        const vector<string> &words) {
        // 递归出口(纵向遍历)
        if (path == target)
        {
            flag = true;
            return;
        }
        if (path.size() > target.size() || path != target.substr(0, path.size())) 
            return; 
    
        // 横向遍历
        for (int i = 0; i < words.size(); ++i)
        {
            path += words[i];                           // 处理
            backTracking(path, target, words);          // 递归
            path.resize(path.size() - words[i].size()); // 回溯
        }
    }

public:
    bool wordBreak(string s, vector<string> &wordDict)
    {
        // 获取可能用于组成s的单词
        vector<string> subStrs;
        for (const string &word : wordDict)
        {
            if (isSubStr(s, word))
                subStrs.push_back(word);
        }

        // 回溯算法
        backTracking(path, s, subStrs);
        return flag;
    }
};

不出意外的超时了。其实我就是想复习一下KMP算法(确信)。

但是通过 记忆化搜索 ,我们可以减少回溯算法的问题规模,从而较大幅度降低其时间复杂度,例如下面的代码就通过了LeetCode测试:

class Solution // 回溯算法+记忆化搜索
{
private:
    unordered_map<int, bool> memory; // 记录某子串(起始位置为start)能否由wordSet中单词组成
    bool backTracking(string s, int start, const unordered_set<string> wordSet) {
        // 递归出口(纵向遍历)
        if (start == s.size())
            return true;
        
        // 采用记忆化搜索,利用已处理过的子问题的结果
        if (memory.find(start) != memory.end())
            return memory[start];

        // 回溯搜索(横向遍历)
        for (int end = start + 1; end <= s.size(); ++end) {
            if (wordSet.find(s.substr(start, end - start)) != wordSet.end()
                && backTracking(s, end, wordSet)) {
                memory[start] = true;
                return true;
            }
        }
        
        memory[start] = false;
        return false;
    }

public:
    bool wordBreak(string s, vector<string> & wordDict)
    {
        unordered_set<string> wordSet{wordDict.begin(), wordDict.end()}; // 存储单词的哈希表,加速查找
        return backTracking(s, 0, wordSet);
    }
};

这里的思路和上一个代码略有不同:上一个算法是“试图用所给单词组合出目标字符串”,该算法是“试图将所给字符串划分为目标单词”,殊途同归。

接下来采用更加优雅的算法:将问题转化为 完全背包问题 (单词可以重复使用),且要求的是 排列 (单词的排列顺序有影响),故按照 先遍历背包容量、再遍历物品 的顺序 动态规划 即可解决。

不过,这题似乎也没必要将问题具体化到“背包”的场景,直接从 dp 数组的意义入手也很好理解:

  • dp 数组含义: dp[j] 表示字符串 s 的前 j 个字符组成的子串,能否由 wordDict 中的单词组成

  • 状态转移方程(伪代码): dp[j] = dp[i] && s[i, j] in wordDict

    s 的前 i 个字符构成的子串能由 wordDict 中的单词组成(即 dp[i] == true] ),且子串 s[i, j] 恰好是 wordDict 中的某个单词,那么它俩加起来(即 s 的前 j 个字符构成的子串)也能由 wordDict 中的单词组成。

代码(C++)

bool wordBreak(string s, vector<string> &wordDict)
{
    vector<bool> dp(s.size() + 1, false);
    dp[0] = true;
    unordered_set<string> wordSet{wordDict.begin(), wordDict.end()};
    for (int i = 0; i < s.size(); ++i) {
        for (int j = i + 1; j <= s.size(); ++j) {
            if (!dp[i])
             	break;
            if (wordSet.find(s.substr(i, j - i)) != wordSet.end())
                dp[j] = true;
        }
    }
    return dp[s.size()];
}

优雅,太优雅了。


卡码网56. 携带矿石资源

点此跳转题目链接

题目描述

你是一名宇航员,即将前往一个遥远的行星。在这个行星上,有许多不同类型的矿石资源,每种矿石都有不同的重要性和价值。你需要选择哪些矿石带回地球,但你的宇航舱有一定的容量限制。

给定一个宇航舱,最大容量为 C 。现在有 N 种不同类型的矿石,每种矿石有一个重量 w[i] ,一个价值 v[i] ,以及最多 k[i] 个可用。不同类型的矿石在地球上的市场价值不同。你需要计算如何在不超过宇航舱容量的情况下,最大化你所能获取的总价值。

输入描述

输入共包括四行,第一行包含两个整数 CN ,分别表示宇航舱的容量和矿石的种类数量。

接下来的三行,每行包含 N 个正整数。具体如下:

第二行包含 N 个整数,表示 N 种矿石的重量。

第三行包含 N 个整数,表示 N 种矿石的价格。

第四行包含 N 个整数,表示 N 种矿石的可用数量上限。

输出描述

输出一个整数,代表获取的最大价值。

输入示例

10 3
1 3 4
15 20 30
2 3 2

输出示例

90

提示信息

数据范围:
1 <= C <= 10000
1 <= N <= 10000
1 <= w[i], v[i], k[i] <= 10000

题解

典型的 多重背包问题 :每个物品的可使用次数有限但可能不止一次。

按照传统的01背包问题框架,加一个遍历每个物品的使用次数的循环即可。更详细的解析参见 代码随想录-多重背包

代码(C++)

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main() {
    int c, n;
    cin >> c >> n;

    vector<int> weights(n);
    for (int i = 0; i < n; ++i)
        cin >> weights[i];
    
    vector<int> values(n);
    for (int i = 0; i < n; ++i)
        cin >> values[i];
    
    vector<int> nums(n);
    for (int i = 0; i < n; ++i)
        cin >> nums[i];

    vector<int> dp(c + 1, 0);
    for (int i = 0; i < n; ++i) {
        for (int j = c; j >= weights[i]; --j) {
            for (int k = 1; k <= nums[i] && j >= k * weights[i]; ++k) 
                dp[j] = max(dp[j], dp[j - k * weights[i]] + k * values[i]);
        }
    }        
    
    cout << dp[c] << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值