魔法甜点之和:小包的新挑战
问题描述
小R不再追求甜点中最高的喜爱值,今天他想要的是甜点喜爱值之和正好匹配他的预期值 S。为了达到这个目标,他可以使用魔法棒来改变甜点的喜爱值,使其变为原来喜爱值的阶乘。每个甜点只能使用一次魔法棒,也可以完全不用。
下午茶小哥今天带来了 N 个甜点,每个甜点都有一个固定的喜爱值。小R有 M 个魔法棒,他可以选择任意甜点使用,但每个甜点只能使用一次魔法棒。他的目标是通过选择一些甜点,可能使用魔法棒,使得这些甜点的喜爱值之和恰好为 S。
请计算小R有多少种不同的方案满足他的要求。如果两种方案中,选择的甜点不同,或者使用魔法棒的甜点不同,则视为不同的方案。
测试样例
样例1:
输入:n = 3, m = 2, s = 6, like = [1, 2, 3]
输出:5
样例2:
输入:n = 3, m = 1, s = 1, like = [1, 1, 1]
输出:6
样例3:
输入:n = 5, m = 3, s = 24, like = [1, 2, 3, 4, 5]
输出:1
样例4:
输入:n = 4, m = 0, s = 10, like = [1, 3, 3, 3]
输出:1
样例5:
输入:n = 6, m = 1, s = 35, like = [5, 5, 5, 5, 5, 5]
输出:0
解题思路
分析
题目要求计算有多少种不同的方案选择甜点,使得喜爱值之和正好为 S。每个甜点有三种处理方式:
- 不选该甜点。
- 选该甜点,不使用魔法棒,其喜爱值不变。
- 选该甜点,使用魔法棒,将喜爱值变为其阶乘。
需要注意的条件:
- 每个甜点只能使用一次魔法棒。
- 总共只有 M 根魔法棒。
- 方案的不同体现在选择的甜点集合不同,或者使用魔法棒的甜点不同。
解法
考虑使用 DP 来解决这个问题。
状态定义
设 dp[i][j][k] 表示前 i 个甜点,使用了 j 根魔法棒,达到总和为 k 的方案数。
i:处理到第i个甜点。j:已经使用的魔法棒数量,0 <= j <= M。k:当前喜爱值总和,0 <= k <= S。
但三维 DP 的空间复杂度较高,考虑优化。
优化
由于每次处理一个甜点时,只与前一个状态有关,可以使用二维DP数组,表示使用 j 根魔法棒达到总和为 k 的方案数。
设 dp[j][k] 表示使用 j 根魔法棒,达到总和为 k 的方案数。
转移方程
对于每个甜点,有三种选择:
-
不选该甜点:
dp[j][k]保持不变。 -
选该甜点,不使用魔法棒:
- 如果
k - like_i >= 0,则:dp[j][k] += dp_prev[j][k - like_i] dp_prev表示上一个状态的DP数组。
- 如果
-
选该甜点,使用魔法棒:
- 如果
j > 0且k - factorial(like_i) >= 0,则:dp[j][k] += dp_prev[j - 1][k - factorial(like_i)]
- 如果
实现细节
- 预计算每个甜点喜爱值的阶乘,以便在DP转移时使用。
- 为了节省空间和时间,使用字典(
defaultdict)来存储可能的k值。 - 遍历甜点时,逆序遍历魔法棒的使用数量,防止重复计数。
算法步骤
-
预处理:计算每个甜点喜爱值的阶乘,存储在数组中。
-
初始化:
dp[0][0] = 1,表示使用 0 根魔法棒,喜爱值总和为 0 的方案数为 1(什么都不选)。
-
遍历甜点:
对于每个甜点,执行以下操作:
-
复制当前的 DP 状态,避免在同一轮更新时影响结果。
-
遍历魔法棒数量:
-
从
M到0逆序遍历j。 -
遍历当前已有的喜爱值总和
k:-
选该甜点,不使用魔法棒:
dp[j][k + like_i] += dp_prev[j][k]条件是
k + like_i <= S。 -
选该甜点,使用魔法棒:
dp[j + 1][k + factorial(like_i)] += dp_prev[j][k]条件是
j + 1 <= M且k + factorial(like_i) <= S。
-
-
-
-
计算结果:
- 最终,遍历
0到M的魔法棒使用数量,将dp[j][S]累加,得到总的方案数。
- 最终,遍历
注意
- 避免重复计数:通过逆序遍历魔法棒数量,确保每个甜点的处理不会影响未处理的状态。
- 空间优化:由于可能的总和范围较大,使用字典存储有效的
k值,避免稀疏的二维数组浪费空间。
代码实现
def solution(n, m, s, like):
from math import factorial
from collections import defaultdict
factorial_like = [factorial(l) for l in like]
dp = [defaultdict(int) for _ in range(m + 1)]
dp[0][0] = 1
for i in range(n):
current_like = like[i]
transformed_like = factorial_like[i]
for magic_wands_used in range(m, -1, -1):
current_dp = dp[magic_wands_used].copy()
for current_sum, count in current_dp.items():
new_sum = current_sum + current_like
if new_sum <= s:
dp[magic_wands_used][new_sum] += count
if magic_wands_used + 1 <= m:
new_sum_transformed = current_sum + transformed_like
if new_sum_transformed <= s:
dp[magic_wands_used + 1][new_sum_transformed] += count
total_ways = sum(dp[magic_wands_used][s] for magic_wands_used in range(m + 1))
return total_ways
if __name__ == "__main__":
print(solution(3, 2, 6, [1,2,3]) == 5 )
print(solution(3, 1, 1, [1,1,1]) == 6 )
print(solution(5, 3, 24, [1,2,3,4,5]) == 1)
print(solution(4, 0, 10, [1,3,3,3]) == 1)
print(solution(6, 1, 35, [5,5,5,5,5,5]) == 0)
测试用例说明
-
样例1:
- 输入:
n = 3, m = 2, s = 6, like = [1, 2, 3] - 输出:
5 - 解释:共有 5 种不同的方案,使得喜爱值总和为 6。
- 输入:
-
样例2:
- 输入:
n = 3, m = 1, s = 1, like = [1, 1, 1] - 输出:
6 - 解释:共有 6 种不同的方案,使得喜爱值总和为 1。
- 输入:
-
样例3:
- 输入:
n = 5, m = 3, s = 24, like = [1, 2, 3, 4, 5] - 输出:
1 - 解释:只有一种方案,选择甜点值为 4,使用魔法棒后喜爱值为 24(4! = 24)。
- 输入:
-
样例4:
- 输入:
n = 4, m = 0, s = 10, like = [1, 3, 3, 3] - 输出:
1 - 解释:没有魔法棒,只能选择所有 3 个喜爱值为 3 的甜点,喜爱值总和为 9(无法达到 10),但由于题目要求方案的喜爱值总和恰好为 S,所以只有一种方案,选择值为 1 和 3 的甜点。
- 输入:
-
样例5:
- 输入:
n = 6, m = 1, s = 35, like = [5, 5, 5, 5, 5, 5] - 输出:
0 - 解释:即使使用魔法棒将一个 5 变为 120(5!),其他 5 个甜点的喜爱值为 5,总和最小为 120 + 5 = 125,无法达到 35。
- 输入:
总结
本题考查了多重背包问题的变形,结合了魔法棒的使用,使得状态转移更加复杂。 通过动态规划和状态压缩,我们可以成功解决方案数的计算问题。