题目解析
问题背景
小R有N个甜点,每个甜点都有一个固定的喜爱值。小R手上有M个魔法棒,他的目标是通过选择一些甜点,可能使用魔法棒使得这些甜点的喜爱值之和恰好为S。
问题解读
每个甜点的喜爱值可以通过两种方式来影响:
- 不使用魔法棒:直接取该甜点的喜爱值。
- 使用魔法棒:该甜点的喜爱值变为它的阶乘。
小R的目标是选择一些甜点,使用魔法棒或不使用魔法棒,使得这些甜点的喜爱值之和恰好为S,并且不超过M次使用魔法棒。
问题分析
本题可以分为以下几个步骤:
- 选择甜点:每个甜点可以选择或不选择,选择后可以决定是否使用魔法棒。
- 阶乘计算:每个甜点的阶乘值是有限制的,如果阶乘值超过S,就无法使用魔法棒。
- 分治法:由于有N个甜点,我们可以将问题拆成两部分,分别处理前半部分和后半部分。这样有助于减少问题的规模,使得每个子问题的规模较小,避免了暴力搜索带来的计算复杂度。
思路与解法
1. 分治法
由于问题中存在N个甜点和M个魔法棒,我们可以将问题分为两部分来解决。具体做法如下:
- 将甜点数组分为两部分:
likes_first和likes_second。 - 分别计算每部分的所有可能的组合,包括选择的甜点和使用魔法棒的次数。
- 使用字典(
sum_counts_first和sum_counts_second)存储每部分所有可能的“喜爱值总和”和“使用魔法棒的次数”。
2. 动态规划+递归
- 递归地遍历每一部分甜点,计算其所有可能的组合,包括不使用魔法棒、使用魔法棒、以及阶乘计算。
- 使用一个缓存机制来避免重复计算阶乘,确保计算效率。
3. 合并结果
- 计算完两部分后,我们将它们的结果进行合并,检查两部分组合的喜爱值之和是否等于S,并且魔法棒的使用次数总和不超过M。
代码详解
def solution(n, m, s, like):
from collections import defaultdict
import sys
sys.setrecursionlimit(1 << 25) # 增大递归深度限制
# 预计算阶乘,避免重复计算
factorial_cache = {}
def factorial(x):
if x in factorial_cache:
return factorial_cache[x]
result = 1
for i in range(2, x + 1):
result *= i
if result > s:
result = s + 1 # 超过 s 的阶乘值设为 s+1,用于剪枝
break
factorial_cache[x] = result
return result
# 将甜点分为两部分
mid = n // 2
likes_first = like[:mid]
likes_second = like[mid:]
# 处理第一部分
sum_counts_first = defaultdict(lambda: defaultdict(int))
def dfs_first(i, total_like, wands_used):
if i == len(likes_first):
sum_counts_first[total_like][wands_used] += 1
return
# 不选择当前甜点
dfs_first(i + 1, total_like, wands_used)
# 选择当前甜点,不使用魔法棒
dfs_first(i + 1, total_like + likes_first[i], wands_used)
# 选择当前甜点,使用魔法棒(如果阶乘值不超过 s)
fact = factorial(likes_first[i])
if fact <= s:
dfs_first(i + 1, total_like + fact, wands_used + 1)
dfs_first(0, 0, 0)
# 处理第二部分
sum_counts_second = defaultdict(lambda: defaultdict(int))
def dfs_second(i, total_like, wands_used):
if i == len(likes_second):
sum_counts_second[total_like][wands_used] += 1
return
# 不选择当前甜点
dfs_second(i + 1, total_like, wands_used)
# 选择当前甜点,不使用魔法棒
dfs_second(i + 1, total_like + likes_second[i], wands_used)
# 选择当前甜点,使用魔法棒(如果阶乘值不超过 s)
fact = factorial(likes_second[i])
if fact <= s:
dfs_second(i + 1, total_like + fact, wands_used + 1)
dfs_second(0, 0, 0)
# 合并两部分的结果
total_ways = 0
for sum_first in sum_counts_first:
sum_second_needed = s - sum_first
if sum_second_needed in sum_counts_second:
counts_first = sum_counts_first[sum_first]
counts_second = sum_counts_second[sum_second_needed]
for wands_first in counts_first:
for wands_second in counts_second:
if wands_first + wands_second <= m:
total_ways += counts_first[wands_first] * counts_second[wands_second]
return total_ways
关键点分析
-
阶乘计算:
- 为了避免重复计算阶乘,我们使用了一个缓存(
factorial_cache)来存储已经计算过的阶乘结果。 - 如果阶乘值超出S,我们立即停止计算并返回S+1,这样可以通过剪枝来减少计算量。
- 为了避免重复计算阶乘,我们使用了一个缓存(
-
分治法:
- 将甜点分成两部分进行处理,每一部分递归地计算所有可能的组合。
- 通过
sum_counts_first和sum_counts_second分别记录每一部分的结果,从而避免了暴力枚举。
-
合并结果:
- 在最终合并结果时,我们通过遍历两部分的可能结果,检查是否满足喜爱值之和为S且魔法棒使用次数不超过M。
知识总结与个人思考
知识点总结
- 递归与分治法:通过将大问题拆解为两个较小的子问题,可以显著降低问题的复杂度,避免暴力枚举。
- 动态规划与缓存:通过缓存计算结果(例如阶乘),避免重复计算,从而提高了程序效率。
- 剪枝优化:通过对阶乘值进行剪枝,可以避免不必要的计算,进一步优化性能。
学习建议
- 递归思维:遇到这类问题时,应该尝试将问题拆分成更小的子问题,分治法能够帮助我们有效解决。
- 缓存与优化:在遇到重复计算时,考虑使用缓存或记忆化技术来避免重复计算,从而提高效率。
- 阶乘计算与剪枝:在处理阶乘问题时,需要注意阶乘增长速度较快,因此通过剪枝优化,可以避免过大的数字影响计算结果。
总的来说,本题通过分治法和递归来优化问题的求解过程,并使用缓存和剪枝技术大幅提高了效率。对于入门同学,理解递归的拆解和优化技术是非常有帮助的。