题目描述
思路解析
一句话答案:LeetCode 948 令牌放置用排序加对撞双指针:便宜令牌花能量换分、体力不够时把最贵的令牌卖成能量,两头相向夹逼榨出最高分数。时间 O(n log n)、空间 O(1)。
令牌能正面买分、反面买体力,这题求的是什么
给一个令牌数组 tokens 和初始能量 power。每个令牌只能用一次:正面朝上,能量够付 tokens[i] 就花掉换 +1 分;反面朝下,手里有分就花 1 分换回 tokens[i] 那么多能量。求整场攒到的最高分。题面 tokens=[40,25,60,30,15,50]、power=35 时答案是 2。
每个令牌都要选正反、还要排顺序,硬枚举有多大
每个令牌要么正面买分、要么反面卖分、要么不用,先后顺序还能任意排。想穷举所有组合,方案数随令牌个数指数级膨胀,n 个令牌光排列就 n! 种,稍多几个就算不完。得找条规律把「买哪个、卖哪个」定死,不再逐个搜索。
买分挑最便宜、卖分挑最值钱,为什么这样贪最优
买分是在花能量,花得越省、后面能买的次数越多,所以每次都挑当前最便宜的令牌买,代价最小。卖分正相反:卖一次要付出宝贵的 1 分,当然要换回最多能量,也就是挑当前最值钱的。tokens 排序后最便宜的全在左边、最值钱的全在右边,正好左指针负责买、右指针负责卖,两头往中间夹。
还有个容易漏掉的量:分数会为换能量主动跌回去,但答案要的是整场里的最高点。所以另记一个 ans 追历史峰值,score 每涨到新高就刷新 ans,卖分让它回落时 ans 不动。
左右指针每一步怎么定买还是卖,什么时候收手
循环只在左指针不超过右指针时继续,每步看三种情况:能量够买最便宜的那张令牌,就正面朝上——能量减掉它、score 加 1、左指针右移,顺手用 ans 记峰值;买不动但手里还有分(score > 0),就反面朝下卖最值钱的那张——能量加上它、score 减 1、右指针左移;既买不动又没分可卖,就直接跳出。两指针交错时同样停,返回 ans。
power=35 起手,两指针把这组 tokens 夹到底
先排序得 [15,25,30,40,50,60],左右指针分踞两端,power=35、score 与 ans 都是 0。第一步 power=35 ≥ 15,买 15:power=20、score=1,ans 刷成 1。第二步 power=20 < 25 买不动、还剩 1 分,卖 60:power=80、score=0。第三步 power=80 ≥ 25,买:power=55、score=1,ans 仍是 1。第四步 power=55 ≥ 30,买:power=25、score=2,ans 刷成 2。第五步 power=25 < 40 买不动,卖 50:power=75、score=1。第六步 power=75 ≥ 40,买:power=35、score=2,ans 仍是 2。左指针越过右指针,循环停,返回 2。
排序扛住主要开销,空数组和开局卡死各怎么收场
整个流程时间 O(n log n),瓶颈在排序;双指针只是一遍线性扫,额外空间 O(1)。两个边界要想清:tokens 为空时左指针在 0、右指针在 -1,循环条件一进就不成立,直接返回 0;开局 power 连最便宜的都付不起、score 又是 0,第一步撞上「既买不动又没分卖」跳出,答案是 0。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这句「小的换分、大的换能量、能买就买买不动就卖」,下面每一帧都在套它。答案 ans 只升不随 score 回落,记的是历史峰值。
这是发到手的令牌,顺序是乱的。贪心策略要反复取「当前最小」和「当前最大」,乱序下没法一眼定位,所以第一步先排序。
排好序后,最左边的 15 是最便宜的令牌、买分优先选它;最右边的 60 是最值钱的、卖分优先选它。一头一尾正好对应两个指针。
左指针 i 指向最小令牌、负责买分,右指针 j 指向最大令牌、负责卖分。只要 i 不超过 j 就继续:先试着用 i 买,买不动再考虑用 j 卖。
先看左指针的最小令牌 15。当前能量 35 不小于它,说明买得起,那就贪心地用最便宜的把这一分先拿下。
正面朝上!花掉 15 点能量,能量从 35 降到 20,分数加到 1。这个令牌标绿,表示它被换成了分。
分数 1 创了新高,ans 刷新到 1。左指针右移,去看下一个最便宜的令牌还能不能接着买。
现在能量 20 连最小令牌 25 都买不起了。好在手里还有 1 分,可以反面朝下:盯住最值钱的 60,花 1 分把它换成一大笔能量。
反面朝下!花 1 分换来 60 点能量,能量从 20 升到 80,分数回落到 0。注意 ans 记的是历史峰值 1,卖分不会把它抹掉。右指针左移。
先看左指针的最小令牌 25。当前能量 80 不小于它,说明买得起,那就贪心地用最便宜的把这一分先拿下。
正面朝上!花掉 25 点能量,能量从 80 降到 55,分数加到 1。这个令牌标绿,表示它被换成了分。
分数 1 没超过历史最高 1,ans 保持不动。左指针右移,继续往下看。
先看左指针的最小令牌 30。当前能量 55 不小于它,说明买得起,那就贪心地用最便宜的把这一分先拿下。
正面朝上!花掉 30 点能量,能量从 55 降到 25,分数加到 2。这个令牌标绿,表示它被换成了分。
分数 2 创了新高,ans 刷新到 2。左指针右移,去看下一个最便宜的令牌还能不能接着买。
现在能量 25 连最小令牌 40 都买不起了。好在手里还有 2 分,可以反面朝下:盯住最值钱的 50,花 1 分把它换成一大笔能量。
反面朝下!花 1 分换来 50 点能量,能量从 25 升到 75,分数回落到 1。注意 ans 记的是历史峰值 2,卖分不会把它抹掉。右指针左移。
先看左指针的最小令牌 40。当前能量 75 不小于它,说明买得起,那就贪心地用最便宜的把这一分先拿下。
正面朝上!花掉 40 点能量,能量从 75 降到 35,分数加到 2。这个令牌标绿,表示它被换成了分。
分数 2 没超过历史最高 2,ans 保持不动。左指针右移,继续往下看。
左右指针交错,i 已经超过 j,可用的令牌都处理完了。绿色是买分用掉的、灰色是卖能量用掉的,过程到此为止。
回看整场:score 升到 2 时是最辉煌的一刻,后面虽然为换能量又卖回到 1,但 ans 早把这个峰值 2 记下了。所以最终答案就是 2。
边界先想清:空数组、开局就卡死、能量大到全买,三种极端心里有数。
面试重点:会用交换论证说清贪心的正确性,并讲清何时该停。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class Solution: def bagOfTokensScore(self, tokens: List[int], power: int) -> int: tokens.sort() ans = score = 0 i, j = 0, len(tokens) - 1 while i <= j: if power >= tokens[i]: power -= tokens[i] score, i = score + 1, i + 1 ans = max(ans, score) elif score: power += tokens[j] score, j = score - 1, j - 1 else: break return ans复杂度
- 时间:O(n log n),排序主导;之后双指针一遍线性扫
- 空间:O(1),只用几个变量;不计排序自身开销
易错点
面试追问把动画讲成自己的话
追问怎么证明「买最小、卖最大」这个贪心是对的?
追问什么时候应该停止卖分?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
煎饼排序
LeetCode 969 · 中等 · 沿着 双指针套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题