3.25-DP
完全背包问题
518. 零钱兑换 II - 力扣(LeetCode)
思路:
DP数组含义
dp[j]表示容量为j的背包有dp[j]种方式凑成总金额j
递推关系式
dp[j] += dp[j - coins[i]]
循环关系
在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。
因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!
而本题要求凑成总和的组合数,元素之间明确要求没有顺序。
所以纯完全背包是能凑成总和就行,不用管怎么凑的。
本题是求凑出来的方案个数,且每个方案个数是为组合数。
那么本题,两个for循环的先后顺序可就有说法了。
我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。
代码如下:
for (int i = 0; i < coins.size(); i++) { // 遍历物品 for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量 dp[j] += dp[j - coins[i]]; } }假设:coins[0] = 1,coins[1] = 5。那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。
所以这种遍历顺序中dp[j]里计算的是组合数!
如果把两个for交换顺序,代码如下:
for (int j = 0; j <= amount; j++) { // 遍历背包容量 for (int i = 0; i < coins.size(); i++) { // 遍历物品 if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]]; } }背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。
此时dp[j]里算出来的就是排列数!
总结:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
初始化
vector<int> dp(amount + 1 , 0) , dp[0] = 1另外,本题后台数据经过了修改,会出现爆int情况,将int->unsigned int即可过。
vector<unsigned int> dp(amount + 1, 0);举例
代码:
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<unsigned int> dp(amount + 1 , 0);
dp[0] = 1;
for(int i : coins){
for (int j = i; j <= amount; j++) {
dp[j] += dp[j - i];
}
}
return dp[amount];
}
};
377. 组合总和 Ⅳ - 力扣(LeetCode)
请注意,顺序不同的序列被视作不同的组合。
这句话说明是排列问题,循环顺序倒一下即可。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned int> dp(target + 1 , 0);
dp[0] = 1;
for (int j = 0; j <= target; j++) {
for (int i = 0; i < nums.size(); i++) {
if(j >= nums[i]) dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}
};
57. 爬楼梯(第八期模拟笔试) (kamacoder.com)
思路:
完全背包 && 求排列
代码:
#include<iostream>
#include<vector>
using namespace std;
int main(){
int n , m;
cin >> n >> m;
vector<int> dp(n + 1 , 0);
dp[0] = 1;
for(int j = 0 ; j <= n ; j ++){
for (int i = 1; i <= m; i++) {
if(j >= i) dp[j] += dp[j - i];
}
}
return dp[n];
}
322. 零钱兑换 - 力扣(LeetCode)
完全背包问题
dp[j] 表示凑成总金额j所需的最少硬币为dp[j]
dp[j] = dp[j - coins[i]] + 1 (x)
dp[j] = min(dp[j] , d[j - coins[i]] + 1)
只求最终硬币数,循环顺序无所谓。
考虑到递推公式的特性,dp[j]必须初始化为一个最大的数
vector<int> dp(amount + 1 ,INT_MAX)
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<unsigned int> dp(amount + 1 , INT_MAX);
dp[0] = 0;
for(int i : coins){
for (int j = i; j <= amount; j++) {
dp[j] = min(dp[j] ,dp[j - i] + 1);
}
}
if(dp[amount] == INT_MAX) return -1;
else return dp[amount];
}
};
279. 完全平方数 - 力扣(LeetCode)
同上一题,完全背包问题。
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1 , INT_MAX);
dp[0] = 0;
for (int i = 1; i <= sqrt(n); i++) {
for (int j = i * i; j <= n; j++) {
dp[j] = min(dp[j] , dp[j - i * i] + 1);
}
}
return dp[n];
}
};
139. 单词拆分 - 力扣(LeetCode)
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词
如果确定
dp[j]是true,且[j, i]这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。所以递推公式是
if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么
dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。本题求的是
排列数,先容量后物品,dp[s.size()]就是最终结果。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> st(wordDict.begin() , wordDict.end());
vector<bool> dp(s.length() + 1 , false);
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {//外层容量,内层物品
for (int j = 0; j < i; j++) {
string word = s.substr(j , i - j);
//cout << word << endl;
if(st.contains(word) && dp[j]) dp[i] = true;
// for (int k = 0; k <= s.size(); k++) cout << dp[k] << " "; //这里打印 dp数组的情况
// cout << endl;
}
}
return dp[s.length()];
}
};
- 时间复杂度:\(O(n^3)\),因为
substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度) - 空间复杂度:\(O(n)\)
验证:
输入
s ="applepenapple"
wordDict =["apple","pen"]
标准输出
a 1 0 0 0 0 0 0 0 0 0 0 0 0 0 ap 1 0 0 0 0 0 0 0 0 0 0 0 0 0 p 1 0 0 0 0 0 0 0 0 0 0 0 0 0 app 1 0 0 0 0 0 0 0 0 0 0 0 0 0 pp 1 0 0 0 0 0 0 0 0 0 0 0 0 0 p 1 0 0 0 0 0 0 0 0 0 0 0 0 0 appl 1 0 0 0 0 0 0 0 0 0 0 0 0 0 ppl 1 0 0 0 0 0 0 0 0 0 0 0 0 0 pl 1 0 0 0 0 0 0 0 0 0 0 0 0 0 l 1 0 0 0 0 0 0 0 0 0 0 0 0 0 apple 1 0 0 0 0 1 0 0 0 0 0 0 0 0 pple 1 0 0 0 0 1 0 0 0 0 0 0 0 0 ple 1 0 0 0 0 1 0 0 0 0 0 0 0 0 le 1 0 0 0 0 1 0 0 0 0 0 0 0 0 e 1 0 0 0 0 1 0 0 0 0 0 0 0 0 applep 1 0 0 0 0 1 0 0 0 0 0 0 0 0 pplep 1 0 0 0 0 1 0 0 0 0 0 0 0 0 plep 1 0 0 0 0 1 0 0 0 0 0 0 0 0 lep 1 0 0 0 0 1 0 0 0 0 0 0 0 0 ep 1 0 0 0 0 1 0 0 0 0 0 0 0 0 p 1 0 0 0 0 1 0 0 0 0 0 0 0 0 applepe 1 0 0 0 0 1 0 0 0 0 0 0 0 0 pplepe 1 0 0 0 0 1 0 0 0 0 0 0 0 0 plepe 1 0 0 0 0 1 0 0 0 0 0 0 0 0 lepe 1 0 0 0 0 1 0 0 0 0 0 0 0 0 epe 1 0 0 0 0 1 0 0 0 0 0 0 0 0 pe 1 0 0 0 0 1 0 0 0 0 0 0 0 0 e 1 0 0 0 0 1 0 0 0 0 0 0 0 0 applepen 1 0 0 0 0 1 0 0 0 0 0 0 0 0 pplepen 1 0 0 0 0 1 0 0 0 0 0 0 0 0 plepen 1 0 0 0 0 1 0 0 0 0 0 0 0 0 lepen 1 0 0 0 0 1 0 0 0 0 0 0 0 0 epen 1 0 0 0 0 1 0 0 0 0 0 0 0 0 pen 1 0 0 0 0 1 0 0 1 0 0 0 0 0 en 1 0 0 0 0 1 0 0 1 0 0 0 0 0 n 1 0 0 0 0 1 0 0 1 0 0 0 0 0 applepena 1 0 0 0 0 1 0 0 1 0 0 0 0 0 pplepena 1 0 0 0 0 1 0 0 1 0 0 0 0 0 plepena 1 0 0 0 0 1 0 0 1 0 0 0 0 0 lepena 1 0 0 0 0 1 0 0 1 0 0 0 0 0 epena 1 0 0 0 0 1 0 0 1 0 0 0 0 0 pena 1 0 0 0 0 1 0 0 1 0 0 0 0 0 ena 1 0 0 0 0 1 0 0 1 0 0 0 0 0 na 1 0 0 0 0 1 0 0 1 0 0 0 0 0 a 1 0 0 0 0 1 0 0 1 0 0 0 0 0 applepenap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 pplepenap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 plepenap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 lepenap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 epenap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 penap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 enap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 nap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 ap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 p 1 0 0 0 0 1 0 0 1 0 0 0 0 0 applepenapp 1 0 0 0 0 1 0 0 1 0 0 0 0 0 pplepenapp 1 0 0 0 0 1 0 0 1 0 0 0 0 0 plepenapp 1 0 0 0 0 1 0 0 1 0 0 0 0 0 lepenapp 1 0 0 0 0 1 0 0 1 0 0 0 0 0 epenapp 1 0 0 0 0 1 0 0 1 0 0 0 0 0 penapp 1 0 0 0 0 1 0 0 1 0 0 0 0 0 enapp 1 0 0 0 0 1 0 0 1 0 0 0 0 0 napp 1 0 0 0 0 1 0 0 1 0 0 0 0 0 app 1 0 0 0 0 1 0 0 1 0 0 0 0 0 pp 1 0 0 0 0 1 0 0 1 0 0 0 0 0 p 1 0 0 0 0 1 0 0 1 0 0 0 0 0 applepenappl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 pplepenappl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 plepenappl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 lepenappl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 epenappl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 penappl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 enappl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 nappl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 appl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 ppl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 pl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 l 1 0 0 0 0 1 0 0 1 0 0 0 0 0 applepenapple 1 0 0 0 0 1 0 0 1 0 0 0 0 0 pplepenapple 1 0 0 0 0 1 0 0 1 0 0 0 0 0 plepenapple 1 0 0 0 0 1 0 0 1 0 0 0 0 0 lepenapple 1 0 0 0 0 1 0 0 1 0 0 0 0 0 epenapple 1 0 0 0 0 1 0 0 1 0 0 0 0 0 penapple 1 0 0 0 0 1 0 0 1 0 0 0 0 0 enapple 1 0 0 0 0 1 0 0 1 0 0 0 0 0 napple 1 0 0 0 0 1 0 0 1 0 0 0 0 0 apple 1 0 0 0 0 1 0 0 1 0 0 0 0 1 pple 1 0 0 0 0 1 0 0 1 0 0 0 0 1 ple 1 0 0 0 0 1 0 0 1 0 0 0 0 1 le 1 0 0 0 0 1 0 0 1 0 0 0 0 1 e 1 0 0 0 0 1 0 0 1 0 0 0 0 1输出
true
如果代码改变循环顺序
for (int j = 0; j < wordDict.size(); j++) { // 物品 for (int i = wordDict[j].size(); i <= s.size(); i++) { // 背包 string word = s.substr(i - wordDict[j].size(), wordDict[j].size()); // cout << word << endl; if ( word == wordDict[j] && dp[i - wordDict[j].size()]) { dp[i] = true; } // for (int k = 0; k <= s.size(); k++) cout << dp[k] << " "; //这里打印 dp数组的情况 // cout << endl; } }输入
s ="applepenapple"
wordDict =["apple","pen"]
标准输出
apple 1 0 0 0 0 1 0 0 0 0 0 0 0 0 pplep 1 0 0 0 0 1 0 0 0 0 0 0 0 0 plepe 1 0 0 0 0 1 0 0 0 0 0 0 0 0 lepen 1 0 0 0 0 1 0 0 0 0 0 0 0 0 epena 1 0 0 0 0 1 0 0 0 0 0 0 0 0 penap 1 0 0 0 0 1 0 0 0 0 0 0 0 0 enapp 1 0 0 0 0 1 0 0 0 0 0 0 0 0 nappl 1 0 0 0 0 1 0 0 0 0 0 0 0 0 apple 1 0 0 0 0 1 0 0 0 0 0 0 0 0 app 1 0 0 0 0 1 0 0 0 0 0 0 0 0 ppl 1 0 0 0 0 1 0 0 0 0 0 0 0 0 ple 1 0 0 0 0 1 0 0 0 0 0 0 0 0 lep 1 0 0 0 0 1 0 0 0 0 0 0 0 0 epe 1 0 0 0 0 1 0 0 0 0 0 0 0 0 pen 1 0 0 0 0 1 0 0 1 0 0 0 0 0 ena 1 0 0 0 0 1 0 0 1 0 0 0 0 0 nap 1 0 0 0 0 1 0 0 1 0 0 0 0 0 app 1 0 0 0 0 1 0 0 1 0 0 0 0 0 ppl 1 0 0 0 0 1 0 0 1 0 0 0 0 0 ple 1 0 0 0 0 1 0 0 1 0 0 0 0 0最后dp[s.size()] = 0 即 dp[13] = 0 ,而不是1,因为先用 "apple" 去遍历的时候,dp[8]并没有被赋值为1 (还没用"pen"),所以 dp[13]也不能变成1。
除非是先用 "apple" 遍历一遍,再用 "pen" 遍历,此时 dp[8]已经是1,最后再用 "apple" 去遍历,dp[13]才能是1。
补充:
另外,本题集合的构造方式也值得注意: unordered_set
st(wordDict.begin() , wordDict.end()); 在C++中,
std::string是标准库提供的字符串类,支持丰富的操作。以下是常用函数及其用法的详细说明,包含示例代码和关键注意事项。
一、构造与初始化
#include <string> using namespace std; // 默认构造:空字符串 string s1; // 用C风格字符串初始化 string s2("Hello"); // 拷贝构造 string s3(s2); // 重复字符构造:5个'a' string s4(5, 'a'); // "aaaaa" // 子串构造:从s2的第1个字符开始,取2个字符 string s5(s2, 1, 2); // "el"
二、赋值操作
string s; s = "Hello"; // 直接赋值 s.assign("World"); // 使用assign方法 s.assign(s2, 1, 3); // 从s2的第1位开始取3个字符:"ell" s.assign(5, 'x'); // "xxxxx"
三、字符串拼接
string s = "Hello"; s += " World"; // 追加字符串 s.append("!!!"); // "Hello World!!!" s.append(s2, 1, 3); // 追加s2的子串(从1开始3字符):"ell" s.push_back('?'); // 末尾添加单个字符
四、字符串比较
string a = "apple", b = "banana"; // 使用运算符 if (a < b) { /* true */ } // 使用compare函数 int res = a.compare(b); // res < 0: a < b; res == 0: 相等; res > 0: a > b
五、查找与替换
1. 查找
string s = "Hello World"; size_t pos; pos = s.find("World"); // 6(首次出现位置) pos = s.find('o', 5); // 从下标5开始找'o',结果为7 pos = s.find("xyz"); // string::npos(未找到) pos = s.rfind('o'); // 反向查找,结果为7 pos = s.find_first_of("aeiou"); // 第一个元音的位置('e'在1) pos = s.find_last_not_of("dlorW "); // 最后一个不属于指定字符的位置2. 替换
s.replace(6, 5, "C++"); // 从6开始替换5个字符为"C++" → "Hello C++" s.replace(s.begin(), s.begin()+5, "Hi"); // 迭代器范围替换 → "Hi C++"
六、子串操作
string s = "Hello World"; string sub = s.substr(6, 5); // 从6开始取5个字符 → "World"
七、大小与容量
s.size(); // 返回字符数(与length()等价) s.empty(); // 是否为空 s.resize(10); // 调整大小为10,不足部分填充'\0' s.reserve(100); // 预分配内存,避免频繁扩容 s.clear(); // 清空字符串
八、元素访问
char c1 = s[1]; // 下标访问(不检查越界) char c2 = s.at(1); // 越界时抛出std::out_of_range异常
九、修改操作
s.insert(5, " insert"); // 在位置5插入字符串 → "Hello insert World" s.erase(5, 7); // 删除位置5开始的7个字符 → "Hello World" s.pop_back(); // 删除最后一个字符
十、类型转换
字符串 ↔ 数值
int num = stoi("123"); // 字符串转整数 double d = stod("3.14"); // 字符串转双精度 string str_num = to_string(123); // 数值转字符串与C风格字符串互转
const char* cstr = s.c_str(); // 返回以'\0'结尾的字符数组 const char* data = s.data(); // 直接访问底层数组(C++11后可能不以'\0'结尾)
十一、STL算法应用
#include <algorithm> string s = "hello"; reverse(s.begin(), s.end()); // 反转字符串 → "olleh" sort(s.begin(), s.end()); // 按字符升序排列 → "ehllo"
十二、字符串流处理
#include <sstream> string input = "Name: John Age: 30"; istringstream iss(input); string name_part, age_part; iss >> name_part >> name_part; // 分割提取内容 ostringstream oss; oss << "PI=" << 3.14; // 格式化输出到字符串 string oss_str = oss.str(); // 获取结果:"PI=3.14"
注意事项
- 越界访问:使用
at()时注意异常,[]不检查边界。npos处理:查找失败时返回string::npos(通常为size_t最大值),需用size_t类型接收。- 迭代器失效:修改字符串可能导致迭代器、指针或引用失效。
- 性能优化:频繁拼接时使用
reserve()预分配内存提升效率。
在 C++ 中,哈希表(
std::unordered_map)和哈希集合(std::unordered_set)是标准库提供的两种无序关联容器。它们通过哈希函数实现快速查找和插入。以下是它们的初始化方法及示例代码:
一、哈希表 (
std::unordered_map)1. 空初始化
#include <unordered_map> std::unordered_map<int, std::string> map1; // 空的哈希表2. 初始化列表 (C++11+)
std::unordered_map<int, std::string> map2 = { {1, "Apple"}, {2, "Banana"}, {3, "Cherry"} };3. 范围构造(从其他容器的迭代器)
std::vector<std::pair<int, std::string>> vec = {{4, "Dog"}, {5, "Cat"}}; std::unordered_map<int, std::string> map3(vec.begin(), vec.end());4. 复制构造
std::unordered_map<int, std::string> map4(map2); // 深拷贝5. 自定义哈希函数和比较器
struct MyHash { size_t operator()(const int& key) const { return key % 100; // 简单示例哈希 } }; struct MyEqual { bool operator()(const int& a, const int& b) const { return a == b; } }; std::unordered_map<int, std::string, MyHash, MyEqual> map5;
二、哈希集合 (
std::unordered_set)1. 空初始化
#include <unordered_set> std::unordered_set<int> set1; // 空的哈希集合2. 初始化列表 (C++11+)
std::unordered_set<int> set2 = {1, 2, 3, 4, 5};3. 范围构造(从其他容器的迭代器)
std::vector<int> vec = {6, 7, 8}; std::unordered_set<int> set3(vec.begin(), vec.end());4. 复制构造
std::unordered_set<int> set4(set2); // 深拷贝5. 自定义哈希函数和比较器
struct MyHash { size_t operator()(const std::string& s) const { return s.length(); // 按字符串长度哈希(仅为示例) } }; struct MyEqual { bool operator()(const std::string& a, const std::string& b) const { return a[0] == b[0]; // 仅比较首字母是否相同(示例) } }; std::unordered_set<std::string, MyHash, MyEqual> set5;
三、动态插入元素
1. 哈希表插入
std::unordered_map<int, std::string> map; map.insert({4, "Duck"}); // 插入键值对 map.emplace(5, "Elephant"); // 原地构造(更高效) map[6] = "Fox"; // 下标操作(若键不存在则插入)2. 哈希集合插入
std::unordered_set<int> set; set.insert(10); // 插入元素 set.emplace(20); // 原地构造
四、性能优化
1. 预分配桶空间
std::unordered_map<int, std::string> map; map.reserve(100); // 预分配至少 100 个元素的存储空间2. 设置负载因子
map.max_load_factor(0.5); // 控制哈希表的负载因子(桶的平均元素数)
五、特殊初始化场景
1. 从
std::map转换std::map<int, std::string> ordered_map = {{1, "A"}, {2, "B"}}; std::unordered_map<int, std::string> unordered_map(ordered_map.begin(), ordered_map.end());2. 过滤重复元素
std::vector<int> vec = {1, 2, 2, 3, 3, 3}; std::unordered_set<int> unique_set(vec.begin(), vec.end()); // 自动去重
六、注意事项
- 哈希冲突:哈希表的性能依赖于哈希函数的质量,需避免大量冲突。
- 自定义类型:若键是自定义类型(如类或结构体),必须提供哈希函数和相等比较器。
- 迭代器失效:插入或删除元素可能导致迭代器失效。
- C++11+ 特性:初始化列表和
emplace需要 C++11 或更高版本支持。
七、完整示例
#include <iostream> #include <unordered_map> #include <unordered_set> int main() { // 哈希表示例 std::unordered_map<std::string, int> word_count = { {"apple", 5}, {"banana", 3} }; word_count.emplace("cherry", 10); std::cout << "Apple count: " << word_count["apple"] << std::endl; // 哈希集合示例 std::unordered_set<std::string> unique_words = {"hello", "world"}; unique_words.insert("hello"); // 重复插入无效 std::cout << "Unique words size: " << unique_words.size() << std::endl; return 0; }输出:
Apple count: 5 Unique words size: 2通过灵活选择初始化方式,可以高效构建哈希表/集合,适应不同场景需求。
56. 携带矿石资源(第八期模拟笔试) (kamacoder.com)
多重背包问题:
从代码里可以看出是01背包里面在加一个for循环遍历一个每种商品的数量。 和01背包还是如出一辙的。
当然还有那种二进制优化的方法,其实就是把每种物品的数量,打包成一个个独立的包。
和以上在循环遍历上有所不同,因为是分拆为各个包最后可以组成一个完整背包,具体原理我就不做过多解释了,大家了解一下就行,面试的话基本不会考完这个深度了,感兴趣可以自己深入研究一波。
多重背包在面试中基本不会出现,力扣上也没有对应的题目,对多重背包的掌握程度知道它是一种01背包,并能在01背包的基础上写出对应代码就可以了。
至于背包九讲里面还有混合背包,二维费用背包,分组背包等等这些,大家感兴趣可以自己去学习学习,这里也不做介绍了,面试也不会考。
#include<iostream>
#include<vector>
using namespace std;
int main() {
int bagWeight,n;
cin >> bagWeight >> n;
vector<int> weight(n, 0);
vector<int> value(n, 0);
vector<int> nums(n, 0);
for (int i = 0; i < n; i++) cin >> weight[i];
for (int i = 0; i < n; i++) cin >> value[i];
for (int i = 0; i < n; i++) cin >> nums[i];
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < n; i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
// 以上为01背包,然后加一个遍历个数
for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数
dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);
}
}
}
cout << dp[bagWeight] << endl;
}
时间复杂度:\(O(m * n * k)\),m:物品种类个数,n背包容量,k单类物品数量
300. 最长递增子序列 - 力扣(LeetCode)
思路:
dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度。为什么一定表示 “以
nums[i]结尾的最长递增子序” ,因为我们在做递增比较的时候,如果比较nums[j]和nums[i]的大小,那么两个递增子序列一定分别以nums[j]为结尾 和nums[i]为结尾, 要不然这个比较就没有意义了,不是尾部元素的比较那么如何算递增呢。位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。所以:
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);注意这里不是要
dp[i]与dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值。每一个i,对应的
dp[i](即最长递增子序列)起始大小至少都是1.
dp[i]是有0到i-1各个位置的最长递增子序列推导而来,那么遍历i一定是从前向后遍历。j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯从前向后遍历。
遍历i的循环在外层,遍历j则在内层,代码如下:
for (int i = 1; i < nums.size(); i++) { for (int j = 0; j < i; j++) { if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1); } if (dp[i] > result) result = dp[i]; // 取长的子序列 }
- 举例推导dp数组
输入:[0,1,0,3,2],dp数组的变化如下:
如果代码写出来,但一直AC不了,那么就把dp数组打印出来,看看对不对!
本题还需注意边界样例
nums.size() == 0 || 1,最后输出的是dp数组的最大值,而不是dp[nums.size() - 1]!!
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() <= 1) return nums.size();
vector<int> dp(nums.size() , 1);
int res = 0;
for (int i = 1; i < nums.size(); i++) {
for(int j = 0 ; j < i ; j ++){
if(nums[i] > nums[j])
dp[i] = max(dp[i] , dp[j] + 1);
}
res = max(res , dp[i]);
}
return res;
}
};
注意如果最后输出dp[nums.size() - 1],得到的不一定是正确结果!!如以下样例
return dp[nums.size() - 1];

53. 最大子数组和 - 力扣(LeetCode)
思路1:前缀和
我们可以一边遍历数组计算前缀和pre_sum,一边维护前缀和的最小值min_pre_sum(相当于股票最低价格),用当前的前缀和(卖出价格)减去前缀和的最小值(买入价格),就得到了以当前元素结尾的子数组和的最大值(利润),用它来更新答案的最大值ans(最大利润)。
由于题目要求子数组不能为空,应当先计算前缀和-最小前缀和,再更新最小前缀和。相当于不能在同一天买入股票又卖出股票。
如果先更新最小前缀和,再计算前缀和-最小前缀和,就会把空数组的元素和 0 算入答案。
class Solution { public: int maxSubArray(vector<int>& nums) { int ans = INT_MIN; int min_pre_sum = 0; int pre_sum = 0; for(int i : nums){ pre_sum += x; // 当前的前缀和 ans = max(ans, pre_sum - min_pre_sum); // 减去前缀和的最小值 min_pre_sum = min(min_pre_sum, pre_sum); // 维护前缀和的最小值 } return ans; } };复杂度分析
- 时间复杂度:O(n),其中 n 为 nums 的长度。
- 空间复杂度:O(1)。仅用到若干额外变量。
思路二:DP
定义 f[i] 表示以 nums[i] 结尾的最大子数组和。
递推公式
分类讨论:
- nums[i] 单独组成一个子数组,那么 f[i]=nums[i]。
- nums[i] 和前面的子数组拼起来,也就是在以 nums[i−1] 结尾的最大子数组和之后添加 nums[i],那么 f[i]=f[i−1]+nums[i]。
两种情况取最大值,得
f[i]= max(f[i−1],0)+nums[i], i≥1
简单地说,如果 nums[i] 左边的子数组元素和是负的,就不用和左边的子数组拼在一起了。
答案为 max(f)。
初始化
vector<int> dp(nums.size() , 0) dp[0] = nums[0];循环
从左到右
class Solution { public: int maxSubArray(vector<int>& nums) { vector<int> f(nums.size()); f[0] = nums[0]; for (int i = 1; i < nums.size(); i++) { f[i] = max(f[i - 1], 0) + nums[i]; } return ranges::max(f); } };空间优化
由于计算 f[i] 只会用到 f[i−1],不会用到更早的状态,所以可以用一个变量滚动计算。
状态转移方程简化为:f=max(f,0)+nums[i]
f 可以初始化成 0 或者任意负数。
class Solution { public: int maxSubArray(vector<int>& nums) { int ans = INT_MIN; // 注意答案可以是负数,不能初始化成 0 int f = 0; for (int x : nums) { f = max(f, 0) + x; ans = max(ans, f); } return ans; } };复杂度分析
- 时间复杂度:O(n),其中 n 为 nums 的长度。
- 空间复杂度:O(1)。仅用到若干额外变量。
152. 乘积最大子数组 - 力扣(LeetCode)
思路:
- dp[i]表示nums[i]之前包括nums[i]的非空连续子数组的最大乘积
- 递推公式:
- 单独一个数 nums[i]
- 与前面数相乘 dp[i - 1] * nums[i]
if(nums[i] > 0 && dp[i - 1] > 0) dp[i] = max(1, dp[i - 1]) * nums[i]
if(nums[i] > 0 &&dp[i - 1] < 0) dp[i] = nums[i]
if(nums[i] < 0 && dp[i - 1] > 0)
合起来就是:dp[i] = max(1, dp[i - 1]) * nums[i]
初始化
vector
dp(nums.size() , 1) dp[0] = nums[0] , ans = INT_MIN
遍历过程中记录max(ans , dp[i])
for(int i = 1 ; i < nums.size() ; i ++)
本题是 53. 最大子数组和 的乘法版本
寻找子问题
例如 nums=[−2,1,−3,4],讨论右端点为 nums[3]=4 的子数组的最大乘积:
- 4 单独组成一个子数组。
- 4 和前面的子数组拼起来,也就是在右端点为 nums[2]=−3 的乘积最大子数组之后添加 4。
又例如 nums=[−2,1,−3,−4],讨论右端点为 nums[3]=−4 的子数组的最大乘积:
- −4 单独组成一个子数组。
- −4 和前面的子数组拼起来,由于 −4 是负数,要想得到最大的乘积,根据负负得正,我们可以在右端点为 nums[2]=−3 的乘积最小子数组之后添加 −4。
状态定义与状态转移方程
上面两个例子启发我们,需要在遍历 nums 的同时,维护两个信息:
- 右端点下标为 i 的子数组的最大乘积,记作 fmax[i]。
- 右端点下标为 i 的子数组的最小乘积,记作 fmin[i]。
设 x=nums[i],分类讨论:
- x 单独组成一个子数组,那么 fmax[i]=x。
- x 和前面的子数组拼起来,也就是在右端点下标为 i−1 的乘积最大子数组之后添加 x,那么 fmax[i]=fmax[i−1]⋅x;
- 也可以在右端点下标为 i−1 的乘积最小子数组之后添加 x,那么 fmax[i]=fmin[i−1]⋅x。把这两种都算一下,这样我们就无需判断 x 到底是正还是负了。
三种情况取最大值,得
fmax[i]=max(fmax[i−1]⋅x,fmin[i−1]⋅x,x)
同理得
fmin[i]=min(fmax[i−1]⋅x,fmin[i−1]⋅x,x)
由于以 nums[0] 为右端点的子数组乘积只能是 nums[0],所以初始值为 fmax[0]=fmin[0]=nums[0]。这是一种初始化的方法,下文会讲另外一种。
答案为 max(fmax)。
class Solution {
public:
int maxProduct(vector<int>& nums) {
vector<int> fmax(nums.size()) , fmin(nums.size());
fmax[0]= fmin[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
fmax[i] = max(max(fmax[i - 1] * nums[i] , fmin[i - 1] * nums[i]) , nums[i]);
fmin[i] = min(min(fmax[i - 1] * nums[i] , fmin[i - 1] * nums[i]) , nums[i]);
}
return ranges::max(fmax);
}
};
复杂度分析
- 时间复杂度:O(n),其中 n 是 nums 的长度。
- 空间复杂度:O(n)。
写法二(空间优化)
由于计算 fmax[i] 和 fmin[i] 只会用到 fmax[i−1] 和 fmin[i−1],不会用到更早的状态,所以可以用两个变量 fmax 和 fmin 滚动计算。
状态转移方程简化为:
fmax=max(fmax⋅x,fmin⋅x,x)
fmin=min(fmax⋅x,fmin⋅x,x)
注意这两个式子要同时计算。用一个临时变量mx存fmax
代码实现时,可以初始化 fmax=fmin=1,因为 1 乘以 nums[0] 等于 nums[0],这样我们可以从下标 0 开始遍历 nums,代码写起来更简单。
class Solution {
public:
int maxProduct(vector<int>& nums) {
int ans = INT_MIN;
int f_max = 1 , f_min = 1;
for(int x : nums){
int mx = f_max;//用一个临时变量mx存f_max
f_max = max({f_max * x , f_min * x , x});
f_min = min({mx * x , f_min * x , x});
ans = max(ans , f_max);
}
return ans;
}
};
复杂度分析
- 时间复杂度:O(n),其中 n 是 nums 的长度。
- 空间复杂度:O(1)。
拓展题:2708. 一个小组的最大实力值 - 力扣(LeetCode)
本题比上一题多了一种情况:不连续 ,即num[i]可以不选,则mx/mn不变
从左到右遍历 nums,考虑 nums[i] 选或不选:
- 不选 nums[i],那么当前的最大实力值就是前面 nums[0] 到 nums[i−1] 中所选元素的乘积。
- 选 nums[i],那么有如下情况可以得到最大实力值:
- nums[i] 单独一个数作为最大实力值。
- 如果 nums[i] 是正数,把 nums[i] 和前面所选元素值的最大乘积相乘。
- 如果 nums[i] 是负数,由于负负得正,把 nums[i] 和前面所选元素值的最小乘积相乘。
具体来说,我们需要在遍历 nums 的同时,维护所选元素的最小乘积 mn 和最大乘积 mx。
无论 x=nums[i] 是正是负还是零,mn 是以下四种情况的最小值:
- 不选,mn 不变。
- x 单独一个数组成子序列。
- mn⋅x。如果 x 是正数,这可能得到最小乘积。
- mx⋅x。如果 x 是负数,这可能得到最小乘积。
⚠注意:不需要考虑 mn 和 mx 的正负性,只要取到上述四种情况的最小值,就能保证结果一定是最小的。
同理,mx 是以下四种情况的最大值:
- 不选,mx 不变。
- x 单独一个数组成子序列。
- mn⋅x。如果 x 是负数,这可能得到最大乘积。
- mx⋅x。如果 x 是正数,这可能得到最大乘积。
整理得
mn=min(mn,x,mn⋅x,mx⋅x)
mx=max(mx,x,mn⋅x,mx⋅x)
注意这两个式子要同时计算。
初始值:mn=mx=nums[0]。
答案:mx。
class Solution {
public:
long long maxStrength(vector<int>& nums){
long long mx = nums[0] , mn = mx;
for (int i = 1; i < nums.size(); i++) {
long long x = nums[i] , tmp = mx;
mx = max({mx , x , mx * x , mn * x});
mn = min({mn , x , tmp * x , mn * x});
}
return mx;
}
};
复杂度分析
- 时间复杂度:O(n),其中 n 是 nums 的长度。
- 空间复杂度:O(1)。Python 忽略切片空间。
32. 最长有效括号 - 力扣(LeetCode)
思路:
结合题目,有最长这个字眼,可以考虑尝试使用动态规划进行分析。这是最值型动态规划的题目。
定义 dp[i]表示以下标为 i 的字符结尾的最长有效子字符串的长度。
状态转移方程:
- 情况1:
s[i] = ')'且s[i-1] = '('
此时形如...(),则:
dp[i] = dp[i-2] + 2
例如"()"→dp[1] = 2。- 情况2:
s[i] = ')'且s[i-1] = ')'
此时形如...)),需检查s[i - dp[i-1] - 1]是否为'(':
若匹配(如"(()())"的最后一个')'):
dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2]
即当前匹配的括号长度2,加上前一段的有效长度dp[i - dp[i-1] - 2]。
根据上面的分析,我们得到了如下两个计算公式:
dp[i]=dp[i−2]+2
dp[i]=dp[i−1]+dp[i−dp[i−1]−2]+2
那么,求dp[i]就变成了求dp[i−1]、 dp[i−2]、dp[i−dp[i−1]−2]的子问题。
初始条件和边界情况:
初始条件: dp[i]=0
边界情况:需要保证计算过程中:i−2>=0 和 i−dp[i−1]−2>=0
计算顺序:
从前往后,结果是max(dp[i])
复杂度计算:
时间复杂度: 遍历了一遍字符串,所以时间复杂度是:O(N)
空间复杂度:需要和字符串长度相同的数组保存每个位置的最长有效括号长度,所以空间复杂度是:O(N)
class Solution {
public:
int longestValidParentheses(std::string s) {
int n = s.size(), max_len = 0;
vector<int> dp(n, 0); // dp[i]: 以s[i]结尾的最长有效括号长度
for (int i = 1; i < n; ++i) {
if (s[i] == ')') {
// 情况1:直接匹配 "()"
if (s[i-1] == '(') {
dp[i] = (i >= 2 ? dp[i-2] : 0) + 2;//单独处理dp[1]
}
// 情况2:嵌套或连续匹配 "))"
else if (i - dp[i-1] > 0 && s[i - dp[i-1] - 1] == '(') {
int prev = (i - dp[i-1] >= 2) ? dp[i - dp[i-1] - 2] : 0;
dp[i] = dp[i-1] + 2 + prev;
}
max_len = max(max_len, dp[i]);
}
}
return max_len;
}
};
法二:栈
要解决最长有效括号问题,可以使用栈来跟踪括号的匹配情况。该方法思路清晰且代码简洁,以下是详细步骤及实现:
方法思路
- 初始化栈:栈中先压入
-1作为基准,用于后续计算有效长度。- 遍历字符串:
- 遇到
'('时,将其索引压入栈。- 遇到
')'时,弹出栈顶元素:
- 若栈为空,说明当前
')'无法匹配,将当前索引压入栈作为新基准。- 若栈不空,计算当前索引与栈顶的差值,即为当前有效括号长度,更新最大值。
解决代码
class Solution { public: int longestValidParentheses(string s) { stack<int> st; st.push(-1);//初始基准 , 1-(-1) = 2 int res = 0; for (int i = 0; i < s.size(); i++) { if(s[i] == '(') st.push(i);//遇左括号入栈 else{ st.pop();//弹出栈顶 if(st.empty()) st.push(i);//更新基准 else res = max(res , i - st.top());//计算有效长度 } } return res; } };
代码解释
- 栈初始化:
st.push(-1)处理以')'开头的情况,如")()"。- 遍历逻辑:
- 左括号:记录索引,后续可能匹配。
- 右括号:
- 弹出栈顶后若栈空,说明当前
')'无匹配,更新基准为当前索引。- 若栈不空,当前有效长度为
i - st.top(),取最大值。- 复杂度:时间复杂度 O(n),空间复杂度 O(n)(最坏情况)。
示例分析
以字符串
"(()())"为例:
- 初始栈
[-1]。i=0('('): 栈变为[-1, 0]。i=1('('): 栈变为[-1, 0, 1]。i=2(')'): 弹出1,栈顶0,有效长度2-0=2,max_len=2。i=3('('): 栈变为[-1, 0, 3]。i=4(')'): 弹出3,栈顶0,有效长度4-0=4,max_len=4。i=5(')'): 弹出0,栈顶-1,有效长度5-(-1)=6,max_len=6。
最终结果为6。
这种方法通过维护栈中的索引基准点,简洁高效地解决了最长有效括号的匹配问题。




浙公网安备 33010602011771号