3.25-DP

完全背包问题

518. 零钱兑换 II - 力扣(LeetCode)

思路:

  1. DP数组含义

    dp[j]表示容量为j的背包有dp[j]种方式凑成总金额j

  2. 递推关系式

    dp[j] += dp[j - coins[i]]

  3. 循环关系

    在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。

    因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!

    而本题要求凑成总和的组合数,元素之间明确要求没有顺序

    所以纯完全背包是能凑成总和就行,不用管怎么凑的。

    本题是求凑出来的方案个数,且每个方案个数是为组合数。

    那么本题,两个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]里算出来的就是排列数!

    总结:

    1. 如果求组合数就是外层for循环遍历物品,内层for遍历背包。

    2. 如果求排列数就是外层for遍历背包,内层for循环遍历物品。

  4. 初始化

    vector<int> dp(amount + 1 , 0) , dp[0] = 1

    另外,本题后台数据经过了修改,会出现爆int情况,将int->unsigned int即可过。

    vector<unsigned int> dp(amount + 1, 0);

  5. 举例

代码:

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"

注意事项

  1. 越界访问:使用at()时注意异常,[]不检查边界。
  2. npos处理:查找失败时返回string::npos(通常为size_t最大值),需用size_t类型接收。
  3. 迭代器失效:修改字符串可能导致迭代器、指针或引用失效。
  4. 性能优化:频繁拼接时使用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()); // 自动去重

六、注意事项

  1. 哈希冲突:哈希表的性能依赖于哈希函数的质量,需避免大量冲突。
  2. 自定义类型:若键是自定义类型(如类或结构体),必须提供哈希函数和相等比较器。
  3. 迭代器失效:插入或删除元素可能导致迭代器失效。
  4. 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]; // 取长的子序列
}
  1. 举例推导dp数组

输入:[0,1,0,3,2],dp数组的变化如下:

300.最长上升子序列

如果代码写出来,但一直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];

image-20250325203458879


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),其中 nnums 的长度。
  • 空间复杂度:O(1)。仅用到若干额外变量。

思路二:DP

  1. 定义 f[i] 表示以 nums[i] 结尾的最大子数组和。

  2. 递推公式

    分类讨论:

  • 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)。

  1. 初始化

    vector<int> dp(nums.size() , 0)
      dp[0] = nums[0];
    
  2. 循环

    从左到右

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)

思路:

  1. dp[i]表示nums[i]之前包括nums[i]的非空连续子数组的最大乘积
  2. 递推公式:
  • 单独一个数 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]

  1. 初始化

    vector dp(nums.size() , 1)

    dp[0] = nums[0] , ans = INT_MIN

    遍历过程中记录max(ans , dp[i])

  2. 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),其中 nnums 的长度。
  • 空间复杂度: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),其中 nnums 的长度。
  • 空间复杂度: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),其中 nnums 的长度。
  • 空间复杂度:O(1)。Python 忽略切片空间。

32. 最长有效括号 - 力扣(LeetCode)

思路:

结合题目,有最长这个字眼,可以考虑尝试使用动态规划进行分析。这是最值型动态规划的题目。

定义 dp[i]表示以下标为 i 的字符结尾的最长有效子字符串的长度。

状态转移方程:

  • 情况1s[i] = ')'s[i-1] = '('
    此时形如 ...(),则:
    dp[i] = dp[i-2] + 2
    例如 "()"dp[1] = 2
  • 情况2s[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]

截屏2020-04-17下午4.30.46.png

截屏2020-04-17下午4.26.34.png

根据上面的分析,我们得到了如下两个计算公式:

  • 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. 初始化栈:栈中先压入 -1 作为基准,用于后续计算有效长度。
  2. 遍历字符串
    • 遇到 '(' 时,将其索引压入栈。
    • 遇到 ')' 时,弹出栈顶元素:
      • 若栈为空,说明当前 ')' 无法匹配,将当前索引压入栈作为新基准。
      • 若栈不空,计算当前索引与栈顶的差值,即为当前有效括号长度,更新最大值。

解决代码

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. 初始栈 [-1]
  2. i=0 ('('): 栈变为 [-1, 0]
  3. i=1 ('('): 栈变为 [-1, 0, 1]
  4. i=2 (')'): 弹出 1,栈顶 0,有效长度 2-0=2max_len=2
  5. i=3 ('('): 栈变为 [-1, 0, 3]
  6. i=4 (')'): 弹出 3,栈顶 0,有效长度 4-0=4max_len=4
  7. i=5 (')'): 弹出 0,栈顶 -1,有效长度 5-(-1)=6max_len=6
    最终结果为 6

这种方法通过维护栈中的索引基准点,简洁高效地解决了最长有效括号的匹配问题。


posted @ 2025-03-25 23:45  七龙猪  阅读(1)  评论(0)    收藏  举报
-->