(及格的组合方式探索)讲解 | 豆包MarsCode AI刷题

213 阅读5分钟

题目链接 :(及格的组合方式探索 - MarsCode)

题目分析

小S的目标是计算课程组合中满足平均分不低于60分的所有情况。对于每门课程,最高分为100,最低分为0,步进为5(每题5分,满分20题),这形成了一系列可能的得分组合。我们很容易想到通过dfs暴力枚举组合的方法来解决这道问题 , 但是很显然 , 时间复杂度不允许 , 注意 , 这里有一个细节 : 结果是返回方案数 , 而不是具体的方案 , 很显然 , 这是一个动态规划/记忆化搜索的组合问题

我们这里采用记忆化搜索的方式解决这道题()

第一步

我们先用普通做法 : 使用dfs爆搜来实现这道题

思路

  • 递归构建组合

    • 从第1门课开始,递归地为每门课选择分数,选择范围是 {0, 5, 10, ..., 100}。对于每种分数,将其累加到总分 sum 并递归地处理下一门课程。
  • 递归终止条件

    • 当递归深度等于课程总数(即 n + 3),判断累计分数 sum 是否达标(即总分是否不低于 60 * (n + 3))。
    • 如果达标,返回 1 表示这是一个有效的组合,否则返回 0
  • 结果累加

    每次递归返回时累加有效组合数,并在每次递归中对结果取模 202220222022,避免溢出。

#include <bits/stdc++.h>
using namespace std;

const long Mod = 202220222022;

string solution(int n) {
   n += 3;  // 加上3门必修课

   // 非记忆化的DFS函数
   auto dfs = [&](auto&& dfs, int i, long long sum) -> long long {
       // 递归终止条件
       if (i == n)
           return sum >= 60 * n;  // 判断是否达标,达标返回1,不达标返回0

       long long ans = 0;
       // 遍历分数的选择:0, 5, 10, ..., 100
       for (int j = 0; j <= 100; j += 5) {
           ans = (ans + dfs(dfs, i + 1, sum + j)) % Mod;  // 递归求解下一门课程
       }

       return ans;
   };

   // 调用DFS并返回最终结果
   return to_string(dfs(dfs, 0, 0) % Mod);
}

int main() {
   cout << (solution(3) == "19195617") << endl;
   cout << (solution(6) == "135464411082") << endl;
   cout << (solution(49) == "174899025576") << endl;
   cout << (solution(201) == "34269227409") << endl;
   cout << (solution(888) == "194187156114") << endl;

   return 0;
}

第二步

对题目分析得 , 在dfs的搜索过程中 , 其中很多中间状态是会重复到达的 , 所以我们可以进行记忆化

  • 记忆化搜索

    • 为避免重复计算,dp[i][sum] 用于缓存递归结果。如果已经计算过,则直接返回缓存值。
  • 取模运算

    • 每次更新时对结果取模 202220222022,以避免数据溢出并满足题目要求。
#include <bits/stdc++.h>
using namespace std;

const long  Mod = 202220222022;

string solution(int n) {
    int mask[101];
    vector<vector<long long>> dp;

    memset(mask , 0 , sizeof mask);
    // Please write your code here
    n += 3;
    dp.resize(n+1 , vector<long long>(n * 100+1 , -1));

    auto dfs = [&](auto&& dfs , int i , long long sum) -> long long {
      
        if(i == n)
            return sum   >= 60 * n;                        

        if(dp[i][sum] != -1) return dp[i][sum];

        long long ans = 0;
        // 任选
        for(int j = 0; j <= 100; j += 5){
            mask[j]++;
            ans += dfs(dfs , i + 1 , sum + j);
            mask[j]--;
        }

        return dp[i][sum] = ans % Mod;
    };

    return to_string(dfs(dfs , 0 , 0) % Mod);
}

int main() {
    // You can add more test cases here
    cout << (solution(3) == "19195617") << endl;
    cout << (solution(6) == "135464411082") << endl;
    cout << (solution(49) == "174899025576") << endl;
    cout << (solution(201) == "34269227409") << endl;
    cout << (solution(888) == "194187156114") << endl;

    return 0;
}

总结

记忆化搜索的精髓就在于两步走策略 , 我们先实现普通dfs版 , 然后发现 , 很多中间状态可以进行记忆化 那我们就可以进行通过记忆化的方式进行搜索

扩展

本题中 , 我们用数组dp[i][sum] 进行记忆 ,但是通常记忆化的题目会和状态压缩一起出 , 因为数组的长度是有限的 , 但数值不进行取模的时候 , 就需要用一些特殊方法来记忆状态 , 下面我将详细介绍我总结的三种状压方式

状压的三种方式

注意 : 前两种压缩方式都是需要明确所存数据的数据范围

1. 二进制状压

当我们要存储的数组超过 100010 时或者希望存储多个状态值时 , 我们可以用 longlong 哈希表压缩成二进制

压缩方法 : 第一个元素左移 第二个元素最大数据范围 , 第二个元素左移动 第三个元素的最大数据范围 ,中间用 | 连接

eg : long long mask = (long long) i << 32 | j << 1 | pre_down; // 这里压缩了三个状态元素

二进制字符中指定中位置 i 的操作
  1. 插入1 到第 i 位操作 mask | (1 << i) 判断第 i 位元素值的操作 mask >> i & 1
  2. 插入数据范围是 4 bits 的元素 mask |= ((longlong)arr[i] << (i * 4)); // 这里的 i 是数组第 i 位 , 每位元素占有 4bit 空间

提取数据范围是 4 bits 的第 i 个元素的值 value = (mask >> (i * 4)) & 0b1111; // 或者可以使用 & 15

例题 : 错题集 2376. 统计特殊整数

2. 整数压缩

当我们要压缩一个无序哈希无法存储的 key 时 , 可以用 unordered<long long , int > hash;

然后将 key ( 例如 : 一个数组 ) 通过 x = x * 100 + arr[i]; 的方式进行压缩 这里的 100 是由元素的数据范围决定的

例题: 错题集 : 638. 大礼包

3. map 压缩

在 c++中 , 存储的数组超过 100010 时 或者 存一个数组的映射等 , 无序 hash 可能会报错

但是有普通哈希 : map<vector , int > hash; 支持存各种数据结构 , 缺点 : 效率慢一点 ,但方便