题目链接 :(及格的组合方式探索 - MarsCode)
题目分析
小S的目标是计算课程组合中满足平均分不低于60分的所有情况。对于每门课程,最高分为100,最低分为0,步进为5(每题5分,满分20题),这形成了一系列可能的得分组合。我们很容易想到通过dfs暴力枚举组合的方法来解决这道问题 , 但是很显然 , 时间复杂度不允许 , 注意 , 这里有一个细节 : 结果是返回方案数 , 而不是具体的方案 , 很显然 , 这是一个动态规划/记忆化搜索的组合问题
我们这里采用记忆化搜索的方式解决这道题()
第一步
我们先用普通做法 : 使用dfs爆搜来实现这道题
思路
-
递归构建组合:
- 从第1门课开始,递归地为每门课选择分数,选择范围是
{0, 5, 10, ..., 100}。对于每种分数,将其累加到总分sum并递归地处理下一门课程。
- 从第1门课开始,递归地为每门课选择分数,选择范围是
-
递归终止条件:
- 当递归深度等于课程总数(即
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 到第 i 位操作
mask | (1 << i)判断第 i 位元素值的操作mask >> i & 1 - 插入数据范围是 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; 支持存各种数据结构 , 缺点 : 效率慢一点 ,但方便