LeetCode 948中等双指针 · 滑窗
令牌放置 图解题解
这道题到底在问什么
给一个令牌数组 tokens 与初始能量 power。正面朝上:power ≥ tokens[i] 时,花掉 tokens[i] 能量换 +1 分;反面朝下:分数 ≥ 1 时,花 1 分换得 tokens[i] 能量。求能得到的最高分数。
- 输入
- tokens=[40,25,60,30,15,50], power=35
- 输出
- 2
最优解:一步一步想明白
- 3记住这句「小的换分、大的换能量、能买就买买不动就卖」,下面每一帧都在套它。答案 ans 只升不随 score 回落,记的是历史峰值。
- 4这是发到手的令牌,顺序是乱的。贪心策略要反复取「当前最小」和「当前最大」,乱序下没法一眼定位,所以第一步先排序。
- 5排好序后,最左边的 15 是最便宜的令牌、买分优先选它;最右边的 60 是最值钱的、卖分优先选它。一头一尾正好对应两个指针。
- 6左指针 i 指向最小令牌、负责买分,右指针 j 指向最大令牌、负责卖分。只要 i 不超过 j 就继续:先试着用 i 买,买不动再考虑用 j 卖。
- 7先看左指针的最小令牌 15。当前能量 35 不小于它,说明买得起,那就贪心地用最便宜的把这一分先拿下。
- 8正面朝上!花掉 15 点能量,能量从 35 降到 20,分数加到 1。这个令牌标绿,表示它被换成了分。
- 9分数 1 创了新高,ans 刷新到 1。左指针右移,去看下一个最便宜的令牌还能不能接着买。
- 10现在能量 20 连最小令牌 25 都买不起了。好在手里还有 1 分,可以反面朝下:盯住最值钱的 60,花 1 分把它换成一大笔能量。
- 11反面朝下!花 1 分换来 60 点能量,能量从 20 升到 80,分数回落到 0。注意 ans 记的是历史峰值 1,卖分不会把它抹掉。右指针左移。
- 12先看左指针的最小令牌 25。当前能量 80 不小于它,说明买得起,那就贪心地用最便宜的把这一分先拿下。
- 13正面朝上!花掉 25 点能量,能量从 80 降到 55,分数加到 1。这个令牌标绿,表示它被换成了分。
- 14分数 1 没超过历史最高 1,ans 保持不动。左指针右移,继续往下看。
- 15先看左指针的最小令牌 30。当前能量 55 不小于它,说明买得起,那就贪心地用最便宜的把这一分先拿下。
- 16正面朝上!花掉 30 点能量,能量从 55 降到 25,分数加到 2。这个令牌标绿,表示它被换成了分。
- 17分数 2 创了新高,ans 刷新到 2。左指针右移,去看下一个最便宜的令牌还能不能接着买。
- 18现在能量 25 连最小令牌 40 都买不起了。好在手里还有 2 分,可以反面朝下:盯住最值钱的 50,花 1 分把它换成一大笔能量。
- 19反面朝下!花 1 分换来 50 点能量,能量从 25 升到 75,分数回落到 1。注意 ans 记的是历史峰值 2,卖分不会把它抹掉。右指针左移。
- 20先看左指针的最小令牌 40。当前能量 75 不小于它,说明买得起,那就贪心地用最便宜的把这一分先拿下。
- 21正面朝上!花掉 40 点能量,能量从 75 降到 35,分数加到 2。这个令牌标绿,表示它被换成了分。
- 22分数 2 没超过历史最高 2,ans 保持不动。左指针右移,继续往下看。
- 23左右指针交错,i 已经超过 j,可用的令牌都处理完了。绿色是买分用掉的、灰色是卖能量用掉的,过程到此为止。
- 24回看整场:score 升到 2 时是最辉煌的一刻,后面虽然为换能量又卖回到 1,但 ans 早把这个峰值 2 记下了。所以最终答案就是 2。
⚠️ 容易写错的地方
✗ 错:买分时随便挑一个令牌花能量
✓ 对:永远买当前最小的(左指针)
买分要尽量省能量,花得越少越能多买
✗ 错:卖分时也挑小的换能量
✓ 对:永远卖当前最大的(右指针)
卖分只换 1 次,当然要换回最多的能量
✗ 错:用最终 score 当答案
✓ 对:用 ans 记历史峰值
为换能量会主动卖分,结尾的 score 可能比中途峰值低
✗ 错:score = 0 时还想卖分
✓ 对:没分不能反面朝下,直接跳出
反面朝下的前提是至少有 1 分可花
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from 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 ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
int bagOfTokensScore(vector<int>& tokens, int power) {
sort(tokens.begin(), tokens.end());
int ans = 0, score = 0;
for (int i = 0, j = tokens.size() - 1; i <= j;) {
if (power >= tokens[i]) {
power -= tokens[i++];
ans = max(ans, ++score);
} else if (score > 0) {
power += tokens[j--];
--score;
} else {
break;
}
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int bagOfTokensScore(int[] tokens, int power) {
Arrays.sort(tokens);
int ans = 0, score = 0;
for (int i = 0, j = tokens.length - 1; i <= j;) {
if (power >= tokens[i]) {
power -= tokens[i++];
ans = Math.max(ans, ++score);
} else if (score > 0) {
power += tokens[j--];
--score;
} else {
break;
}
}
return ans;
}
}复杂度
时间
O(n log n)
排序主导;之后双指针一遍线性扫
空间
O(1)
只用几个变量;不计排序自身开销
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 令牌放置 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
怎么证明「买最小、卖最大」这个贪心是对的?+
买分要消耗能量,能量越省后续机会越多,所以每次买都选最小代价的令牌不会更差;卖分是用宝贵的 1 分去换能量,当然要换回最多,也就是卖最大的。交换论证可证:任何更优解里若没按这个顺序,都能换成「买更小、卖更大」而结果不变差。
什么时候应该停止卖分?+
当 score 降到 0 就不能再卖了,因为反面朝下需要至少 1 分。实现上当「买不动且 score = 0」时直接 break。另外也不必把分卖光去硬换能量,因为 ans 已经记住了峰值,卖分只是为了博取更高的新峰值。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 令牌放置 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。