解决智力题 图解题解
这道题到底在问什么
- 输入
- questions=[[3,2],[4,3],[4,4],[2,5]]
- 输出
- 5(做第 0 题和第 3 题)
- 输入
- questions=[[1,1],[2,2],[3,3],[4,4],[5,5]]
- 输出
- 7
最优解:为什么这么做
一句话答案:LeetCode 2140 解决智力题:做第 i 题拿 points 分、但后面 brainpower 道题作废,问最高总分。倒序一维 DP,dp[i] 取做题与跳过的较大值,从最后一题往前填,时间 O(n)、空间 O(n)。
解决智力题到底在选什么
questions 的每一项是 [points, brainpower]:做第 i 题拿 points 分,代价是紧随的 brainpower 道题全部作废;也可以不做、直接看下一题。问总分最多是多少。例子一 [[3,2],[4,3],[4,4],[2,5]],做第 0 题和第 3 题共 5 分最优。
为什么不能把每题做不做全试一遍
每道题都是做与不做两个分支,n 道题就是 2ⁿ 种组合,n 到 10⁵ 根本数不完。大量组合的后半段还重复:不管前面怎么选,只要都落到第 3 题,从第 3 题起的最高分都一样,硬枚举重算无数遍,存一次就够。
dp[i] 为什么要从最后一题倒着填
这就是动态规划(DP,把「从第 i 题起最多能拿几分」的答案存进表里,后面直接取)。定义 dp[i]:站在第 i 题、后面随意选,能拿到的最高分,答案是 dp[0]。
这张表得倒着填。原因在依赖方向:第 i 题做不做划算,取决于更靠后的题,dp[i] 要用靠右的格子,右边不先算左边没数可取。表末补一格 dp[n]=0 当哨兵(越过最后一题一分也拿不到),最右边的题才有格子可接。
做完第 i 题,下一个落点怎么数才不越界
每格两条路。不做第 i 题:顺延到下一题,得 dp[i+1]。做第 i 题:拿下 points,其后 brainpower 道全作废,下一道是第 i+brainpower+1 题,得 points+dp[i+brainpower+1]。两路取大即 dp[i],这条规则叫转移(由已填好的格子推出当前格)。
落点最容易数错:跳过 brainpower 道,自己这格也要迈过去,所以加 brainpower 再加 1;它可能冲出表外,参考代码用 nxt = min(n, i+brainpower+1) 夹回哨兵,冲多远都算 0 分。
题面四道题,倒着一路填到 dp[0]=5
先摆哨兵 dp[4]=0。第 3 题 [2,5]:做它落点 min(4, 3+5+1)=4,得 2+dp[4]=2,不做得 0,dp[3]=2。第 2 题 [4,4]:落点夹到 4,做得 4+0=4,不做得 2,dp[2]=4。第 1 题 [4,3]:落点也夹到 4,做得 4,不做得 dp[2]=4,打平,dp[1]=4。
第 0 题 [3,2]:落点 min(4, 0+2+1)=3,做得 3+dp[3]=5,不做得 dp[1]=4,取大 dp[0]=5。正是题面的 5:做第 0 题、作废第 1、2 题,落到第 3 题收 2 分。
nxt 少加 1,样例一为什么会算成 7 分
倒着扫一遍,每格常数次比较加法,时间 O(n)(n 是题数);dp 长 n+1,空间 O(n),依赖跨度随 brainpower 变,压不成两格。
落点写成 i+brainpower,自己这格没迈过去,样例一会得 7 而不是 5;nxt 不夹 min(n,…) 会读到表外,brainpower 最大 10⁵、一跳就穿;总分可达 10⁵ 题各 10⁵ 分共 10¹⁰,C++ 用 long long、Java 用 long,Python 整数不设限。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住这条「跳过取 dp[i+1]、解决取 points + dp[跳过后的位置],两者取大」,下面每一帧都在套它。倒着填,是因为 dp[i] 依赖更靠后的格子。
- 4先放一个哨兵 dp[7] = 0,表示「越过最后一题之后什么都拿不到」。它是整张表的地基,下面从最右边的第 6 题开始,一格一格往左填。
- 5看第 6 题(紫色,分数 7、做完要跳 1 题)。两条路:跳过它,得右边一格 dp[7] = 0;解决它,拿 7 分再跳到终点哨兵 dp[7]接上 dp[7] = 0,共 7。蓝色是已经填好的格子。
- 6解决更划算(7 > 0),dp[6] = 7,这一格记下「在这里选择做第 6 题」(绿色)。做完它后面已无可做的题,转移夹到终点哨兵 dp[7]。
- 7第 6 题已落在末尾附近,做完它之后已无题可跳;按规则任何越界位置都统一夹到哨兵 dp[7] = 0,所以转移落到 dp[7]。
- 8看第 5 题(紫色,分数 5、做完要跳 1 题)。两条路:跳过它,得右边一格 dp[6] = 7;解决它,拿 5 分再跳到终点哨兵 dp[7]接上 dp[7] = 0,共 5。蓝色是已经填好的格子。
- 9跳过更划算(跳过 7 > 解决 5),dp[5] = 7,与右边一格相同,等于「这题不做、留着后面的分」。
- 10选了跳过这格,dp[5] 直接等于右邻 dp[6] = 7,跨度为 1,把决策权留给更靠后的题。
- 11看第 4 题(紫色,分数 6、做完要跳 1 题)。两条路:跳过它,得右边一格 dp[5] = 7;解决它,拿 6 分再跳到第 6 题接上 dp[6] = 7,共 13。蓝色是已经填好的格子。
- 12解决更划算(13 > 7),dp[4] = 13,这一格记下「在这里选择做第 4 题」(绿色)。注意做完它会直接跳到第 6 题。
- 13把「做第 4 题」的代价标出来:红色是它做完被迫跳过的第 5 题,正因为跳过它们,转移才直接落到第 6 题。这就是 brainpower 把「选了就限制后续」量化进 DP 的地方。
- 14看第 3 题(紫色,分数 2、做完要跳 5 题)。两条路:跳过它,得右边一格 dp[4] = 13;解决它,拿 2 分再跳到终点哨兵 dp[7]接上 dp[7] = 0,共 2。蓝色是已经填好的格子。
- 15跳过更划算(跳过 13 > 解决 2),dp[3] = 13,与右边一格相同,等于「这题不做、留着后面的分」。
- 16选了跳过这格,dp[3] 直接等于右邻 dp[4] = 13,跨度为 1,把决策权留给更靠后的题。
- 17看第 2 题(紫色,分数 4、做完要跳 4 题)。两条路:跳过它,得右边一格 dp[3] = 13;解决它,拿 4 分再跳到终点哨兵 dp[7]接上 dp[7] = 0,共 4。蓝色是已经填好的格子。
- 18跳过更划算(跳过 13 > 解决 4),dp[2] = 13,与右边一格相同,等于「这题不做、留着后面的分」。
- 19选了跳过这格,dp[2] 直接等于右邻 dp[3] = 13,跨度为 1,把决策权留给更靠后的题。
- 20看第 1 题(紫色,分数 4、做完要跳 3 题)。两条路:跳过它,得右边一格 dp[2] = 13;解决它,拿 4 分再跳到第 5 题接上 dp[5] = 7,共 11。蓝色是已经填好的格子。
- 21跳过更划算(跳过 13 > 解决 11),dp[1] = 13,与右边一格相同,等于「这题不做、留着后面的分」。
- 22选了跳过这格,dp[1] 直接等于右邻 dp[2] = 13,跨度为 1,把决策权留给更靠后的题。
- 23看第 0 题(紫色,分数 3、做完要跳 2 题)。两条路:跳过它,得右边一格 dp[1] = 13;解决它,拿 3 分再跳到第 3 题接上 dp[3] = 13,共 16。蓝色是已经填好的格子。
- 24解决更划算(16 > 13),dp[0] = 16,这一格记下「在这里选择做第 0 题」(绿色)。注意做完它会直接跳到第 3 题。
- 25把「做第 0 题」的代价标出来:红色是它做完被迫跳过的第 1、2 题,正因为跳过它们,转移才直接落到第 3 题。这就是 brainpower 把「选了就限制后续」量化进 DP 的地方。
- 26整张表填满,左边第一格 dp[0] = 16 就是答案。绿色是这条最优路线实际做的题(第 0、4、6 题:分别拿 3、6、7 分,共 16);灰色的题被它们的「跳过」规则越过或不划算而放弃。
⚠️ 容易写错的地方
✗ 错:从前往后填 dp
✓ 对:从后往前填
dp[i] 要用到更靠后的 dp[i+1] 和 dp[nxt](nxt = min(n, i+brainpower+1)),正着填时右边还没算好;倒着填才能保证依赖项已就绪
✗ 错:跳过位置忘了 min(n, …)
✓ 对:nxt = min(n, i+brainpower+1)
brainpower 很大时 i+brainpower+1 会越过数组末尾,不夹住就会越界;夹到哨兵 dp[n]=0 表示后面无题
✗ 错:用 int 存累加结果
✓ 对:C++/Java 用 long long / long
n 可达 1e5、单题分数大,总分可能超过 32 位 int 上限而溢出
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def mostPoints(self, questions: List[List[int]]) -> int:
n = len(questions)
dp = [0] * (n + 1)
for i in range(n - 1, -1, -1):
points, skip = questions[i]
nxt = min(n, i + skip + 1)
dp[i] = max(dp[i + 1], points + dp[nxt])
return dp[0]C++
#include <algorithm>
#include <vector>
using namespace std;
class Solution {
public:
long long mostPoints(vector<vector<int>>& questions) {
int n = questions.size();
vector<long long> dp(n + 1);
for (int i = n - 1; i >= 0; --i) {
int nxt = min(n, i + questions[i][1] + 1);
dp[i] = max(dp[i+1], (long long)questions[i][0] + dp[nxt]);
}
return dp[0];
}
};Java
import java.util.*;
class Solution {
public long mostPoints(int[][] questions) {
int n = questions.length;
long[] dp = new long[n + 1];
for (int i = n - 1; i >= 0; i--) {
int next = Math.min(n, i + questions[i][1] + 1);
dp[i] = Math.max(dp[i + 1], questions[i][0] + dp[next]);
}
return dp[0];
}
}复杂度
时间
O(n)
n 是题数。倒着扫一遍,每题做常数次比较与加法
空间
O(n)
dp 数组长 n+1;依赖跨度不定,无法压成常数变量
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 解决智力题 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么这题只能倒着填,正着推不行吗?+
填表方向跟着依赖方向走:dp[i] 要用 dp[i+1] 和 dp[i+brainpower+1],都在右边,从右往左填才能保证取到的格子已经算好。正着推也有等价写法,叫刷表法——每算定一个位置,就把它的值主动送去更新它能到达的位置,但「能到达谁」得自己维护,写起来更绕;倒序版一行 max 就收工,是这题的标准姿势。
它和打家劫舍(LeetCode 198)是什么关系?+
骨架同为一排里「选或不选」的一维 DP。打家劫舍偷了第 i 家只须跳过 1 家,跨度固定,dp 只依赖最近两格、能压成两个变量;本题做了第 i 题要跳 brainpower 道,每题跨度不同,brainpower 全是 1 时它就退化成打家劫舍。跨度不定也是它必须整条数组、倒着填的原因。
空间为什么压不到 O(1)?+
滚动变量的前提是每格只依赖固定的最近几格。本题 dp[i] 除了 dp[i+1],还要 dp[i+brainpower+1],这个跨度由数据决定、最大可到表尾,任何一个老格子都可能在很靠左的位置被再次用到,一格都不敢丢,所以 dp 数组要完整保留,空间 O(n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 解决智力题 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。