竞赛编程:以最低成本通关所有关卡——未通过全部测试用例
嘿,我来帮你梳理这个问题的正确解法思路,你的问题属于典型的动态规划问题,尤其是状态压缩DP的范畴,咱们一步步来拆解:
问题本质先搞清楚
首先得明确题目规则的正确理解(我猜你可能在这里没踩准?):
你可以按任意顺序挑战关卡,但挑战某个关卡时,必须拥有该关卡要求的所有武器——如果有缺失的,必须一次性买齐这些缺失的武器,花费是新增武器数量的平方;武器买了之后就一直携带,后续挑战其他关卡可以重复使用。目标是通关所有关卡,总花费最小。
如果是这个规则,那核心就是找一个关卡挑战顺序,让每次新增武器的平方和最小。
两种可行的DP思路
思路一:以「已通关的关卡集合」为状态
适合关卡数量N较小的情况(比如N≤20,因为2^20≈1e6,计算量可控):
- 状态定义:用二进制数
U表示已通关的关卡集合(比如U的第i位为1表示关卡i已通关),dp[U]表示通关U中所有关卡的最小总花费。另外预处理一个数组mask[U],表示U中所有关卡要求的武器的并集(也就是通关这些关卡后你拥有的武器集合)。 - 初始化:
dp[0] = 0(没通关任何关卡,花费0),其他dp[U]初始化为无穷大;mask[0] = 0(没武器)。 - 状态转移:遍历每个已通关集合
U,再遍历所有未通关的关卡i:- 计算新的通关集合
U' = U | (1 << i) - 计算这次需要新增的武器数量:
x = __builtin_popcount( mask[U] ^ (mask[U] | S[i]) )(其实就是S[i]中不在mask[U]里的武器数量) - 更新
dp[U'] = min( dp[U'], dp[U] + x*x ),同时更新mask[U'] = mask[U] | S[i]
- 计算新的通关集合
- 最终答案:
dp[(1<<N)-1](所有关卡都通关的状态)
思路二:以「已拥有的武器集合」为状态
适合武器种类M较小的情况(比如M≤20,2^20≈1e6):
- 状态定义:用二进制数
mask表示已拥有的武器集合,dp[mask]表示拥有这些武器的最小总花费。 - 初始化:
dp[0] = 0(没武器,花费0),其他dp[mask]初始化为无穷大。 - 状态转移:遍历每个武器集合
mask,再遍历所有关卡i:- 计算新的武器集合
new_mask = mask | S[i](挑战关卡i后,你会拥有所有该关卡要求的武器) - 新增武器数量
x = __builtin_popcount(new_mask) - __builtin_popcount(mask) - 更新
dp[new_mask] = min( dp[new_mask], dp[mask] + x*x )
- 计算新的武器集合
- 最终答案:
dp[full_mask],其中full_mask是所有关卡要求的武器的并集(因为拥有这些武器后,你可以通关所有关卡,不需要额外花费)
为什么你的解法可能没过测试用例?
常见的坑点:
- 用了贪心算法:比如每次选当前新增武器最少的关卡,但贪心不一定最优!举个例子:关卡A要{0,1},关卡B要{2,3},关卡C要{0,2}。贪心先选A(花费4)再选B(花费4)总8,但最优是先选C(花费4),再选A(新增1件,花费1),再选B(新增1件,花费1),总6。
- 状态转移计算错误:比如把新增武器数量算成了
S[i]的总大小,而不是S[i]中未拥有的数量。 - DP数组初始化问题:比如初始值设得不够大(比如用
int而不是long long,虽然大部分情况int够,但保险起见用long long避免溢出),或者初始值没设为无穷大,导致错误的最小值。 - 状态遗漏:比如没遍历所有可能的状态,或者转移时漏掉了某些关卡。
代码实现小技巧
- 预处理
mask[U]可以用递推:mask[U] = mask[U ^ (1<<i)] | S[i],其中i是U中任意一个已通关的关卡,这样比每次计算并集快。 - 计算二进制中1的数量,C++可以用
__builtin_popcount(),Python可以用bin(mask).count('1')。 - 如果N或M较大(比如N=25),可以考虑优化:比如用状态合并,对于相同武器集合的不同通关集合,只保留最小花费的那个,减少计算量。
内容的提问来源于stack exchange,提问作者John Stevens
相关产品推荐
相关产品推荐

