青训营X豆包MarsCode技术训练营 | 魔法甜点之和:小包的新挑战

40 阅读5分钟

问题描述

小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

动态规划常用类型

1. 重叠子问题:

动态规划特别适合处理具有重叠子问题的情况。在这道题中,许多组合的计算会涉及到相同的子问题。例如,计算选择某些项目的组合数时,可能会多次计算相同的“喜欢”值和选择数量的组合。动态规划通过存储这些子问题的结果,避免了重复计算,从而提高了效率。

2. 最优子结构:

动态规划依赖于最优子结构的性质,即一个问题的最优解可以由其子问题的最优解构成。在这道题中,选择某个项目的组合数可以通过之前选择的项目的组合数来推导。因此,动态规划能够有效地构建出最终的解。

3. 状态空间的可管理性:

在这道题中,状态空间是有限的,主要由选择的项目数量和“喜欢”值的总和构成。动态规划通过定义状态(如 f[(a, b)])来管理这些组合,使得问题的复杂性得以控制。相比于其他方法(如暴力搜索),动态规划能够在合理的时间内找到解。

4. 时间复杂度的优化:

动态规划通常能够将问题的时间复杂度从指数级降低到多项式级。在这道题中,使用动态规划可以有效地减少计算量,使得即使在较大的输入规模下也能在可接受的时间内得到结果。

5. 清晰的逻辑结构:

动态规划提供了一种清晰的逻辑结构,使得问题的解决过程易于理解和实现。通过定义状态、状态转移和结果计算,代码的可读性和可维护性得以提高。

6. 适应性强:

动态规划可以灵活地适应不同的约束条件和目标。在这道题中,动态规划能够处理选择数量的限制(m)和目标和的限制(s),使得算法能够适应多种情况。

题解

from collections import defaultdict

def precompute_magic():
    magic = [1] * 100
    for i in range(1, 100):
        magic[i] = magic[i - 1] * i
    return magic

def solution(n, m, s, like):
    magic = precompute_magic()
    f = defaultdict(int)
    f[(0, 0)] = 1  # 初始化状态

    for i in range(1, n + 1):
        g = f.copy()  # 复制当前状态
        for (a, b), v in g.items():
            if b + like[i - 1] <= s:
                f[(a, b + like[i - 1])] += v
            if a + 1 <= m and b + magic[like[i - 1]] <= s:
                f[(a + 1, b + magic[like[i - 1]])] += v

    sum_result = sum(f[(i, s)] for i in range(m + 1))
    return sum_result

动态规划的基本步骤

  1. 定义状态: 在动态规划中,首先需要定义一个状态来表示问题的某个特定情况。在这段代码中,状态 f[(a, b)] 表示选择了 a 个项目,并且这些项目的“喜欢”值的总和为 b 的组合数。

  2. 初始化状态: 在开始动态规划之前,需要初始化状态。在代码中,f[(0, 0)] = 1 表示没有选择任何项目时,组合数为 1。这是动态规划的基础状态。

  3. 状态转移: 状态转移是动态规划的核心,指的是如何从已知状态推导出未知状态。在这段代码中,状态转移的过程如下:

    • 对于每个项目 i,我们考虑将其加入到当前的组合中。
    • 复制当前状态 g = f.copy(),以便在更新时不影响正在迭代的状态。
    • 遍历当前状态 g 中的每个组合 (a, b) 和其对应的值 v
    • 对于每个组合,检查是否可以将当前项目的“喜欢”值 like[i - 1] 添加到组合中。如果添加后不超过目标和 s,则更新状态 f[(a, b + like[i - 1])]
    • 还需要检查是否可以将当前项目的阶乘值 magic[like[i - 1]] 添加到组合中,同时确保选择的项目数量不超过 m,并且和不超过 s。如果条件满足,则更新状态 f[(a + 1, b + magic[like[i - 1]])]
  4. 计算结果: 在所有项目都处理完后,最终的结果是所有选择数量在 0m 之间且和为 s 的组合数的总和。通过遍历状态 f 中的所有可能的选择数量,计算出符合条件的组合数。

  5. 返回结果: 最后,返回计算得到的结果。