【题解】魔法甜点之和:小包的新挑战 | 豆包MarsCode AI刷题

101 阅读2分钟

问题描述

众所周知,小包是一名非常喜欢吃甜点的小朋友,他在工作时特别爱吃下午茶里的甜食。

这天,下午茶小哥像往常一样送来了今天的 N 个甜点。小包对每个甜点有自己的喜爱值。但是今天的他不再贪心,并不想要喜爱值越高越好。他今天吃的甜点的喜爱值的和,一定要等于他的预期值 S。

但是他的预期值很高,小哥的甜点似乎不一定满足得了他,所以他准备了 M 个魔法棒,每个魔法棒可以对一个他要吃的甜点使用 1 次,使用后这个甜点的喜爱值会变成原来的喜爱值的阶乘。无法对一个甜点使用多次魔法棒,也不需要使用完所有魔法棒,也无法对不吃的甜点使用魔法棒。

小包很好奇,他有多少种方案,可以吃到喜爱值刚好为他的预期值的甜点。如果两种方案中小包食用了不同的甜点,或者对不同的甜点使用了魔法棒,那么这两种方案都视为不同。

输入格式

输入第一行,包含 3 个整数 NMSN,M,S,分别代表甜点的数量,魔法棒的数量,以及小包的预期值

接下来一行,包含 N 个整数,代表每个甜点的喜爱值

输出格式

输出为一个整数,代表小包的方案数

数据范围

10% 的数据保证 M0,1N10M \leq 0, 1 \leq N \leq 10

30% 的数据保证 1N121 \leq N \leq 12

100% 的数据保证 1N25,0MN,1S10161 \leq N \leq 25, 0 \leq M \leq N, 1 \leq S \leq 10^{16}

问题分析

读题时

这道题考察的是动态规划。但是我发现这道题 SS 的范围有亿点大,直接开数组存状态绝对不行,而且随着喜爱值增加,施法(阶乘)的结果可能直接远超 SS,也就是说会出现很多不必要的状态,要考虑一下如何剪枝。

解题时

状态方程推导

设已使用 xx 根魔法棒且喜爱值之和为 ss 时有 cc 种方案。根据问题描述,对于每个甜点,存在两种选择:

  • 如果不用魔法棒,直接累加当前甜点的喜爱值 ,魔法棒数量保持不变,即:
f[(x,s+like[i])]+=f[(x,s)]f[(x, s + like[i])] += f[(x, s)]
  • 如果用魔法棒,该甜点的喜爱值将变为其阶乘 magic[like[i]]magic[like[i]],魔法棒数量增加 1 ,即:
f[(x+1,s+magic[like[i]])]+=f[(x,s)]f[(x + 1, s + magic[like[i]])] += f[(x, s)]

初始状态是没有选择甜点和使用魔法棒,显然可知为 f[(0,0)]=1f[(0,0)] = 1

约束条件剪枝

  • 已使用的魔法棒数量不能超过总数,所以在状态转移时要满足 x+1Mx+1 \leq M
  • 喜爱值总和不能超过目标值 SS ,所以在状态转移时要满足 s+like[i]Ss + like[i] \leq Ss+magic[like[i]]Ss + magic[like[i]] \leq S

状态存储问题

在本题中, SS 的范围为 1S10161 \leq S \leq 10^{16}。一般的 DP 通常会开数组来存状态,但这里 SS 的范围特别大,假设我们直接开数组,大小为 O(M×S)O(M \times S)。最大所需空间会达到 25×1016×425 \times 10^{16} \times 4 字节,显然不现实。

还有一点,即使 SS 范围很大,但具体分析一下,每次选择甜点后,累加的喜爱值是离散的,并非连续增长;使用魔法棒后,甜点的喜爱值变为其阶乘,这大概率会导致 ss 直接远超 SS 。对于很多甜点,使用魔法棒后的状态根本无效。也就是说,有效状态 (x,s)(x,s) 的分布既稀疏又不连续,数组存储状态会导致存储资源的严重浪费。

于是我采用了哈希表(这里是 Python 的 defaultdict),只为有效状态分配空间。哈希表的插入和访问操作在均摊情况下为 O(1)O(1),在状态稀疏时效率极高。而且当同一个状态被多次更新时,哈希表可以直接累加方案数,确保状态转移逻辑正确。

代码分析及实现

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 的范围过大,无法用数组直接存储所有可能状态,但哈希表只存储实际可转移到的有效状态,避免了大规模的空间浪费。设计动态规划的状态存储时,应根据问题特性选择合适的数据结构。