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
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
示例 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
s
和wordDict[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]
个可用。不同类型的矿石在地球上的市场价值不同。你需要计算如何在不超过宇航舱容量的情况下,最大化你所能获取的总价值。
输入描述
输入共包括四行,第一行包含两个整数 C
和 N
,分别表示宇航舱的容量和矿石的种类数量。
接下来的三行,每行包含 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;
}