射箭比赛中的最大得分 图解题解
这道题到底在问什么
- 输入
- numArrows=9, aliceArrows=[1,1,0,1,0,0,2,1,0,1,2,0]
- 输出
- [0,0,0,0,1,1,0,0,1,2,3,1](总分 4+5+8+9+10+11 = 47)
- 输入
- numArrows=3, aliceArrows=[0,0,1,0,0,0,0,0,0,0,0,2]
- 输出
- [0,0,0,0,0,0,0,0,1,1,1,0](总分 8+9+10 = 27)
先想最直接的笨办法
记牢这套:打第 k 区要 alice[k]+1 支箭、赚 k 分;枚举每个子集算花箭和得分,箭够用里挑得分最高;余箭塞第 0 区。下面按这套一个子集一个子集地试。(动画第 3 步)
最优解:一步一步想明白
- 3记牢这套:打第 k 区要 alice[k]+1 支箭、赚 k 分;枚举每个子集算花箭和得分,箭够用里挑得分最高;余箭塞第 0 区。下面按这套一个子集一个子集地试。
- 4先把 Alice 的箭数摆出来。第 0 到第 4 共 5 个区,格子里的数字是 Alice 在这个区射的箭数,格子的位置就是区号,也正好是打赢它能拿的分。右边这张小表把每个区的得分和 Bob 打赢它要的箭都列好了。Bob 手上一共 8 支箭,任务是把它们分配得让总分最高。
- 5先看要花多少箭。第 4 区 Alice 射了 3 支,Bob 想赢就得严格多于她,至少 4 支,因为打平的话这个区归 Alice。同理第 0 区要 1 支、第 1 区要 2 支、第 2 和第 3 区各要 3 支。右表最后一列就是这几个数。可惜 Bob 只有 8 支箭,不可能每个区都拿下,得挑着来。
- 6有个区要特别说一下:第 0 区。就算 Bob 赢了它,也只加 0 分,花箭去打它纯属浪费。所以真正值得争的是第 1 到第 4 区。第 0 区的用处是最后收尾:Bob 的箭必须全部射完、总数正好等于 numArrows,挑完区如果还剩箭,就全塞进第 0 区,反正不影响得分。心里有了这些,开始一个一个子集地试。
- 7试试打第1区、第2区、第3区、第4区这一组,绿色标出来。把它们要的箭加起来:2 + 3 + 3 + 4 = 12 支。手上预算是 8 支,先看看够不够。
- 8算下来要 12 支箭,已经超过预算 8 支了,标红。箭根本不够,这一组直接淘汰,连得分都不用算。可见区选得越多、越贪,花箭涨得越快,预算这道坎必须先过。
- 9试试打第2区、第3区、第4区这一组,绿色标出来。把它们要的箭加起来:3 + 3 + 4 = 10 支。手上预算是 8 支,先看看够不够。
- 10算下来要 10 支箭,已经超过预算 8 支了,标红。箭根本不够,这一组直接淘汰,连得分都不用算。可见区选得越多、越贪,花箭涨得越快,预算这道坎必须先过。
- 11试试打第3区、第4区这一组,绿色标出来。把它们要的箭加起来:3 + 4 = 7 支。手上预算是 8 支,先看看够不够。
- 12这组箭够用,花 7 支。算得分:3 + 4 = 7 分。比之前记的 best 还高,把最好成绩刷新成 7 分,先记住打的是第3区、第4区。
- 13试试打第2区、第4区这一组,绿色标出来。把它们要的箭加起来:3 + 4 = 7 支。手上预算是 8 支,先看看够不够。
- 14这组箭够用,花 7 支。算得分:2 + 4 = 6 分。不如现在手里的 best 7 分,留着旧的不动。可行不等于最优,得分说了算。
- 15试试打第1区、第3区、第4区这一组,绿色标出来。把它们要的箭加起来:2 + 3 + 4 = 9 支。手上预算是 8 支,先看看够不够。
- 16算下来要 9 支箭,已经超过预算 8 支了,标红。箭根本不够,这一组直接淘汰,连得分都不用算。可见区选得越多、越贪,花箭涨得越快,预算这道坎必须先过。
- 17试试打第1区、第2区、第3区这一组,绿色标出来。把它们要的箭加起来:2 + 3 + 3 = 8 支。手上预算是 8 支,先看看够不够。
- 18这组箭够用,花 8 支。算得分:1 + 2 + 3 = 6 分。不如现在手里的 best 7 分,留着旧的不动。可行不等于最优,得分说了算。
- 19试试打第2区、第3区这一组,绿色标出来。把它们要的箭加起来:3 + 3 = 6 支。手上预算是 8 支,先看看够不够。
- 20这组箭够用,花 6 支。算得分:2 + 3 = 5 分。不如现在手里的 best 7 分,留着旧的不动。可行不等于最优,得分说了算。
- 21试试打第4区这一组,绿色标出来。把它们要的箭加起来:4 = 4 支。手上预算是 8 支,先看看够不够。
- 22这组箭够用,花 4 支。算得分:4 = 4 分。不如现在手里的 best 7 分,留着旧的不动。可行不等于最优,得分说了算。
- 23有代表性的子集都试过了,得分最高的是打第3区、第4区,共 7 分,花 7 支箭。别的组要么超支、要么得分更低,都灰掉了。真实的参考代码会把全部子集都枚举一遍,道理和这里一模一样。接下来把这个选择翻译成 Bob 的箭数分配。
- 24先给中选的区落箭。要压过 Alice,就射她的箭数加一支:第3区 Alice 2 支,Bob 射 3 支;第4区 Alice 3 支,Bob 射 4 支。加起来投了 7 支,手里还剩 1 支没射出去。
- 25箭必须全部射完,总数得正好等于 8。现在还剩 1 支,全塞进第 0 区,那里赢了也只加 0 分,塞多少都不影响 Bob 的总分。第 0 区现在有 1 支。这么一来 Bob 的箭刚好射满,得分仍然是 7 分。
- 26Bob 的分配就出来了:bobArrows = [1, 0, 0, 3, 4]。第 3、第 4 区各比 Alice 多一支、稳稳拿下,得 3 加 4 共 7 分;剩下的箭堆在第 0 区不亏分。箭总数 8 支正好用满。这就是这组小例子的最优解,套到 12 个区上思路完全一样。
⚠️ 容易写错的地方
✗ 错:打赢第 k 区只射和 Alice 一样多的箭
✓ 对:要射 alice[k]+1 支,严格多于 Alice
规则是 Bob 的箭数严格多于 Alice 才得分,平局归 Alice,所以最少要多一支
✗ 错:挑完区就返回,忘了处理余箭
✓ 对:把没用完的箭全加到答案的第 0 区
bobArrows 之和必须恰好等于 numArrows,不塞余箭总数就对不上,是无效答案;第 0 区得 0 分,塞在那不影响得分
✗ 错:按打的区数最多或花箭最少来挑
✓ 对:要挑总得分最高的子集
目标是 Bob 的总分最大,得分是区号之和,和区数、花箭都不成正比,只能按得分比
✗ 错:花箭去打第 0 区想多拿分
✓ 对:第 0 区得 0 分,永远不值得主动打
打赢第 0 区加 0 分却要耗 alice[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 maximumBobPoints(self, numArrows: int, aliceArrows: List[int]) -> List[int]:
st = mx = 0
m = len(aliceArrows)
for mask in range(1, 1 << m):
cnt = s = 0
for i, x in enumerate(aliceArrows):
if mask >> i & 1:
s += i
cnt += x + 1
if cnt <= numArrows and s > mx:
mx = s
st = mask
ans = [0] * m
for i, x in enumerate(aliceArrows):
if st >> i & 1:
ans[i] = x + 1
numArrows -= ans[i]
ans[0] += numArrows
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:
vector<int> maximumBobPoints(int numArrows, vector<int>& aliceArrows) {
int st = 0, mx = 0;
int m = aliceArrows.size();
for (int mask = 1; mask < 1 << m; ++mask) {
int cnt = 0, s = 0;
for (int i = 0; i < m; ++i) {
if (mask >> i & 1) {
s += i;
cnt += aliceArrows[i] + 1;
}
}
if (cnt <= numArrows && s > mx) {
mx = s;
st = mask;
}
}
vector<int> ans(m);
for (int i = 0; i < m; ++i) {
if (st >> i & 1) {
ans[i] = aliceArrows[i] + 1;
numArrows -= ans[i];
}
}
ans[0] += numArrows;
return ans;
}
};Java
import java.util.*;
class Solution {
public int[] maximumBobPoints(int numArrows, int[] aliceArrows) {
int st = 0, mx = 0;
int m = aliceArrows.length;
for (int mask = 1; mask < 1 << m; ++mask) {
int cnt = 0, s = 0;
for (int i = 0; i < m; ++i) {
if ((mask >> i & 1) == 1) {
s += i;
cnt += aliceArrows[i] + 1;
}
}
if (cnt <= numArrows && s > mx) {
mx = s;
st = mask;
}
}
int[] ans = new int[m];
for (int i = 0; i < m; ++i) {
if ((st >> i & 1) == 1) {
ans[i] = aliceArrows[i] + 1;
numArrows -= ans[i];
}
}
ans[0] += numArrows;
return ans;
}
}复杂度
时间
O(2^m · m)
m 是计分区个数,这里恒等于 12。外层枚举 2 的 12 次方约 4096 个子集,内层每个子集扫 12 位算花箭和得分,合起来约 4096 乘 12 次常数操作。因为 m 固定是 12,整体是一个有上界的常数,不随 numArrows 增长
空间
O(m)
按峰值算。除了长度为 12 的答案数组,只用了 mask、s、cnt、mx、st 几个整数变量,没有额外随规模增长的结构,所以额外空间是常数、算上输出是 O(m)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 射箭比赛中的最大得分 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题为什么能用枚举子集暴力做?+
因为计分区固定只有 12 个,每个区对 Bob 来说只有打或不打两种选择,一共 2 的 12 次方约 4096 种组合,规模很小。用一个整数的二进制位表示打哪些区,枚举所有子集、算花箭和得分、取箭够用里得分最高的那个,总操作数约 4096 乘 12,常数级别,完全跑得动。
除了位掩码,还有别的做法吗?+
本质是一个 0 到 1 背包:12 个区当物品,打第 k 区的代价是 alice[k]+1 支箭、价值是 k 分,背包容量是 numArrows,求价值最大。可以写成回溯,对每个区选打或不打;也可以写成容量维度的动态规划。位掩码枚举因为物品只有 12 个,是最直接的写法。
为什么最后要把余箭塞进第 0 区?+
题目要求 bobArrows 之和严格等于 numArrows,一支不多一支不少。挑完区、给中选区各射 alice+1 支之后,往往还剩几支箭。第 0 区赢了也只得 0 分,把余箭全堆在那既不影响得分,又能把总数补齐,是最省心的收尾。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 射箭比赛中的最大得分 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。