问题描述
众所周知,小包是一名非常喜欢吃甜点的小朋友,他在工作时特别爱吃下午茶里的甜食。
这天,下午茶小哥像往常一样送来了今天的 N 个甜点。小包对每个甜点有自己的喜爱值。但是今天的他不再贪心,并不想要喜爱值越高越好。他今天吃的甜点的喜爱值的和,一定要等于他的预期值 S。
但是他的预期值很高,小哥的甜点似乎不一定满足得了他,所以他准备了 M 个魔法棒,每个魔法棒可以对一个他要吃的甜点使用 1 次,使用后这个甜点的喜爱值会变成原来的喜爱值的阶乘。无法对一个甜点使用多次魔法棒,也不需要使用完所有魔法棒,也无法对不吃的甜点使用魔法棒。
小包很好奇,他有多少种方案,可以吃到喜爱值刚好为他的预期值的甜点。如果两种方案中小包食用了不同的甜点,或者对不同的甜点使用了魔法棒,那么这两种方案都视为不同。
输入格式
输入第一行,包含 3 个整数 ,分别代表甜点的数量,魔法棒的数量,以及小包的预期值
接下来一行,包含 N 个整数,代表每个甜点的喜爱值
输出格式
输出为一个整数,代表小包的方案数
数据范围
10% 的数据保证
30% 的数据保证
100% 的数据保证
问题分析
读题时
这道题考察的是动态规划。但是我发现这道题 的范围有亿点大,直接开数组存状态绝对不行,而且随着喜爱值增加,施法(阶乘)的结果可能直接远超 ,也就是说会出现很多不必要的状态,要考虑一下如何剪枝。
解题时
状态方程推导
设已使用 根魔法棒且喜爱值之和为 时有 种方案。根据问题描述,对于每个甜点,存在两种选择:
- 如果不用魔法棒,直接累加当前甜点的喜爱值 ,魔法棒数量保持不变,即:
- 如果用魔法棒,该甜点的喜爱值将变为其阶乘 ,魔法棒数量增加 1 ,即:
初始状态是没有选择甜点和使用魔法棒,显然可知为
约束条件剪枝
- 已使用的魔法棒数量不能超过总数,所以在状态转移时要满足
- 喜爱值总和不能超过目标值 ,所以在状态转移时要满足 或 。
状态存储问题
在本题中, 的范围为 。一般的 DP 通常会开数组来存状态,但这里 的范围特别大,假设我们直接开数组,大小为 。最大所需空间会达到 字节,显然不现实。
还有一点,即使 范围很大,但具体分析一下,每次选择甜点后,累加的喜爱值是离散的,并非连续增长;使用魔法棒后,甜点的喜爱值变为其阶乘,这大概率会导致 直接远超 。对于很多甜点,使用魔法棒后的状态根本无效。也就是说,有效状态 的分布既稀疏又不连续,数组存储状态会导致存储资源的严重浪费。
于是我采用了哈希表(这里是 Python 的 defaultdict),只为有效状态分配空间。哈希表的插入和访问操作在均摊情况下为 ,在状态稀疏时效率极高。而且当同一个状态被多次更新时,哈希表可以直接累加方案数,确保状态转移逻辑正确。
代码分析及实现
from collections import defaultdict
magic = [1] * 100
def solution(n: int, m: int, s: int, like: list[int]):
for i in range(1, 100):
magic[i] = magic[i - 1] * i
dpmap = defaultdict(int)
dpmap[(0, 0)] = 1
for i in range(1, n + 1):
# copy是为了防止当前状态被污染而影响遍历
current_dpmap = dpmap.copy()
for (stick_count, like_sum), count in current_dpmap.items():
if like_sum + like[i - 1] <= s:
dpmap[(stick_count, like_sum + like[i - 1])] += count
if stick_count + 1 <= m and like_sum + magic[like[i - 1]] <= s:
dpmap[(stick_count + 1, like_sum + magic[like[i - 1]])] += count
result = 0
for i in range(m + 1):
result += dpmap[(i, s)]
return result
if __name__ == "__main__":
# 测试用例,此处省略
总结
这道题考察了背包问题,空间优化+离散化,让我更加理解了动态规划和哈希表结合使用的技巧。在问题规模较大且状态稀疏的情况下,哈希表能显著优化存储和计算效率。
就这道题来说,预期值 S 的范围过大,无法用数组直接存储所有可能状态,但哈希表只存储实际可转移到的有效状态,避免了大规模的空间浪费。设计动态规划的状态存储时,应根据问题特性选择合适的数据结构。