☰
LeetCode 948令牌放置:贪心算法与双指针实战解析
2026/10/28 4:49:18 网站建设 项目流程

1. 问题背景与题目解析

今天我们来拆解一道LeetCode中等难度题目——948. Bag of Tokens(令牌放置)。这道题在周赛和日常练习中出现的频率较高,考察的是对贪心算法和双指针技巧的综合运用能力。题目描述如下:

你有一个初始能量为P的背包和初始分数为0的分数。给定一组令牌tokens,其中每个token[i]表示第i个令牌的能量值。你可以通过以下两种方式操作令牌:

  1. 消耗当前令牌的能量值(token[i]),获得1分(必须满足P >= token[i])
  2. 消耗1分,获得当前令牌的能量值(必须至少有1分)

你可以以任意顺序使用令牌,也可以不使用某些令牌。求通过最优操作能够获得的最大分数。

提示:这道题的关键在于理解"能量"和"分数"之间的转换关系,以及如何通过合理的操作顺序最大化最终得分。

2. 贪心策略与双指针解法

2.1 基本解题思路

这道题的核心在于如何在"消耗能量获取分数"和"消耗分数获取能量"之间找到最优平衡点。经过分析,我们可以得出以下关键观察:

  1. 当能量充足时,优先消耗小能量令牌获取分数(因为这样可以用最少的能量换取分数)
  2. 当能量不足时,用分数换取大能量令牌(因为这样可以用最少的分数换取最多的能量)
  3. 最终目标是最大化分数,而不是能量

基于这些观察,我们可以采用贪心算法结合双指针的策略:

def bagOfTokensScore(tokens, P): tokens.sort() left, right = 0, len(tokens) - 1 score = max_score = 0 while left <= right: if P >= tokens[left]: P -= tokens[left] score += 1 left += 1 max_score = max(max_score, score) elif score > 0: P += tokens[right] score -= 1 right -= 1 else: break return max_score

2.2 算法步骤详解

  1. 排序预处理:首先对令牌数组进行排序(时间复杂度O(n log n))。这使得我们可以方便地获取最小和最大能量的令牌。

  2. 初始化指针和变量:

    • left指针指向当前最小能量令牌
    • right指针指向当前最大能量令牌
    • score记录当前分数
    • max_score记录历史最高分数
  3. 主循环逻辑:

    • 当剩余能量足够获取最小令牌时:消耗能量获取分数,左指针右移
    • 当能量不足但还有分数时:用分数换取最大令牌的能量,右指针左移
    • 当既不能获取分数也不能换取能量时:终止循环
  4. 边界条件处理:

    • 确保left和right指针不会越界
    • 每次获得新分数时更新max_score

3. 时间复杂度与空间复杂度分析

3.1 时间复杂度

该算法的时间复杂度主要由两部分组成:

  1. 排序阶段:O(n log n),使用Python内置的Timsort算法
  2. 双指针遍历阶段:O(n),每个令牌最多被访问一次

因此总体时间复杂度为O(n log n),对于LeetCode题目来说是完全可接受的。

3.2 空间复杂度

空间复杂度取决于排序的实现方式:

  1. 如果使用原地排序(如Python的list.sort()),则空间复杂度为O(1)
  2. 如果使用非原地排序,则可能需要O(n)的额外空间

在实际编程竞赛和面试中,通常可以认为Python的sort()是原地排序,因此空间复杂度为O(1)。

4. 常见错误与调试技巧

4.1 典型错误案例

在解决这个问题时,容易出现以下几种错误:

  1. 未排序直接操作:直接使用原始令牌顺序,导致无法正确应用贪心策略

    • 示例错误:tokens = [100,200,300,400], P = 200,正确结果应为2,但未排序可能得到错误结果
  2. 指针移动条件错误:在能量不足时错误地移动指针

    • 错误示例:在P < tokens[left]且score == 0时仍移动指针
  3. max_score更新时机不当:只在循环结束时更新max_score,错过中间更高分数

4.2 调试技巧

为了验证算法的正确性,可以采用以下测试用例:

test_cases = [ ([100], 50), # 预期: 0 ([100,200], 150), # 预期: 1 ([100,200,300,400], 200), # 预期: 2 ([71,55,82], 54), # 预期: 0 ([], 100), # 预期: 0 ([50,50,50], 100) # 预期: 2 ]

在实现时,建议添加详细的打印语句跟踪变量变化:

print(f"left={left}, right={right}, P={P}, score={score}, max_score={max_score}")

5. 算法优化与变种思考

5.1 可能的优化方向

虽然当前解法已经相当高效,但仍有一些优化空间:

  1. 提前终止条件:当剩余令牌数小于等于当前max_score时,可以提前终止循环
  2. 能量累积检查:在能量不足时,可以检查剩余令牌是否值得用分数换取

优化后的代码可能如下:

def bagOfTokensScore(tokens, P): tokens.sort() left, right = 0, len(tokens) - 1 score = max_score = 0 while left <= right: if P >= tokens[left]: P -= tokens[left] score += 1 left += 1 max_score = max(max_score, score) elif score > 0 and (tokens[right] - tokens[left]) > 0: P += tokens[right] score -= 1 right -= 1 else: break # 提前终止条件 if right - left + 1 <= max_score: break return max_score

5.2 相关变种题目

理解这道题的核心思想后,可以尝试解决以下类似题目:

  1. LeetCode 860. Lemonade Change(柠檬水找零)
  2. LeetCode 455. Assign Cookies(分发饼干)
  3. LeetCode 122. Best Time to Buy and Sell Stock II(买卖股票的最佳时机II)

这些题目都涉及到贪心策略的应用,通过解决它们可以加深对这类问题的理解。

6. 实际应用场景分析

虽然这道题看起来是纯粹的算法练习,但其核心思想在实际开发中有多种应用:

  1. 资源分配系统:如何在有限资源下最大化收益,类似于云计算中的资源调度
  2. 游戏开发:角色能量与技能点的转换机制设计
  3. 金融交易:资金与资产的转换策略,寻找最优交易时机

理解这种能量-分数转换模型,可以帮助我们在面对类似的实际问题时快速建立解决方案。

7. 个人解题心得

在多次解决这类贪心算法题目后,我总结出以下几点经验:

  1. 排序是贪心的好朋友:大多数贪心问题都需要先对数据进行某种排序
  2. 双指针的灵活性:不仅可以用在数组求和,还能用于这种资源转换问题
  3. 边界条件的重要性:特别是当能量和分数都为0时的处理很容易遗漏
  4. 变量命名的清晰性:好的变量名如max_score比简单的res更能表达意图

在实际面试中,建议先明确说出解题思路,再开始编码。对于这道题,可以这样表达:

"我计划先对令牌排序,然后用双指针策略,左边获取分数,右边获取能量,同时跟踪最大分数..."

这种结构化的表达方式会给面试官留下良好印象。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询