GESP 六级 · 2026-03 真题 · 考前限时训练讲评

P15800 选数
一道最典型的「选 / 不选」线性 DP

每个位置只做一个决定:选它,还是跳过它?把状态从「以 i 结尾」换成「从 i 开始」,O(n²) 就变成了 O(n)。

一、先回答问题

结论

dp[i] = 从位置 i 开始,往后最多能拿多少分。每个位置只有两种选择:

nxt = max(i+1, i+b[i])
dp[i] = max( dp[i+1] , a[i] + dp[nxt] )

从右往左填,答案就是 dp[1]。时间 O(n)

这节课怎么走
  1. 先写「枚举前驱」的朴素 DP,看它为什么 O(n²) 超时
  2. 换一个问法,状态一变就线性了
  3. 互动:从右往左把 dp 表一格一格填出来
  4. 自测 3 题 + 精读 8 行核心代码

二、慢做法 → 快做法

核心观察:换个问法,状态就省了

😟 朴素 DP:以 i 结尾

dp[i] = 最后一个选的是 i 时的最大和。

转移要往前找所有合法的 j(j+b[j] ≤ i),两层循环 → O(n²)

n 最大 10⁵,O(n²) ≈ 10¹⁰,必超时。

😄 正解:从 i 开始

dp[i] = 从 i 开始往后的最大和。

站在 i 只问一句:这个位置选不选?不选 → dp[i+1];选 → a[i]+dp[nxt]。一层循环 → O(n)

不再枚举「前一个是谁」,直接跳到下一个合法位置。

同一道题,换个状态定义,复杂度从 O(n²) 掉到 O(n) —— 这就是 DP「状态设计」的威力。
nxt 为什么要写 max(i+1, i+b[i])?下标必须严格变大,所以即使 b[i]=0,下一个也不能还是 i,至少是 i+1。漏了这个 max,b[i]=0 时会原地死循环 / 算错。

三、互动演示:从右往左填 dp 表

样例 1:a = [1, 2, 3, 4],b = [3, 3, 1, 1]。橙框 = 正在算的 i,蓝框 = 它跳到的 nxt。

不选 i → dp[i+1]
选 i → a[i] + dp[nxt]
点「下一步」,从最右边开始倒着填。

四、自己选答案

Q1 为什么把状态从「以 i 结尾」换成「从 i 开始」就快了?
Q2 dp 应该按什么顺序计算?
Q3 哪个写法是对的(下标严格递增)?

五、手推结果

ia[i]b[i]nxt不选 dp[i+1]选 a[i]+dp[nxt]dp[i]
441504+0 = 44
331443+4 = 77
223572+0 = 27
113471+4 = 57
答案 = dp[1] = 7 ✅(与样例一致)。注意 i=2、i=1 都选了「不选」,因为它们的 b 太大,选了反而把后面好位置全卡掉了。

六、C++14 参考代码

① dp 开到 n+2
这样 dp[n+1]=0 能直接当边界用,不必特判越界。
② 倒序循环
for(i=n; i>=1; i--) —— 被依赖的右边先算好。
③ 一行转移
dp[i]=max(dp[i+1], a[i]+dp[nxt]):前项不选、后项选。
常见错误:
  1. nxt 只写 i+b[i],忘了下标必须严格递增。
  2. 答案用 int:a[i] 可达 10⁹,总和会溢出,必须 long long
  3. nxt 超过 n 没压到 n+1,数组越界。
  4. 写成贪心「只挑 a 大的」—— 这题必须 DP,选大的可能卡掉后面更优的。

七、和 LIS 像在哪、不像在哪

第一眼会觉得像 LIS:都在递增下标里做选择,也都能先写成「枚举前驱」的朴素 DP。

LIS本题 选数
核心在问很多合法前驱里,哪个最好?当前这个位置,选还是不选?
优化方向二分 / 树状数组优化前驱查询换状态,直接线性,不用优化前驱
所以它不是「套 LIS 优化」,而是「换个状态定义后,本来就只需要 O(n)」。

八、课后迁移