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)。
这节课怎么走
- 先写「枚举前驱」的朴素 DP,看它为什么 O(n²) 超时
- 换一个问法,状态一变就线性了
- 互动:从右往左把 dp 表一格一格填出来
- 自测 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。
点「下一步」,从最右边开始倒着填。
四、自己选答案
Q1 为什么把状态从「以 i 结尾」换成「从 i 开始」就快了?
Q2 dp 应该按什么顺序计算?
Q3 哪个写法是对的(下标严格递增)?
五、手推结果
| i | a[i] | b[i] | nxt | 不选 dp[i+1] | 选 a[i]+dp[nxt] | dp[i] |
| 4 | 4 | 1 | 5 | 0 | 4+0 = 4 | 4 |
| 3 | 3 | 1 | 4 | 4 | 3+4 = 7 | 7 |
| 2 | 2 | 3 | 5 | 7 | 2+0 = 2 | 7 |
| 1 | 1 | 3 | 4 | 7 | 1+4 = 5 | 7 |
答案 = 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]):前项不选、后项选。
常见错误:
- nxt 只写
i+b[i],忘了下标必须严格递增。
- 答案用
int:a[i] 可达 10⁹,总和会溢出,必须 long long。
- nxt 超过 n 没压到 n+1,数组越界。
- 写成贪心「只挑 a 大的」—— 这题必须 DP,选大的可能卡掉后面更优的。
七、和 LIS 像在哪、不像在哪
第一眼会觉得像 LIS:都在递增下标里做选择,也都能先写成「枚举前驱」的朴素 DP。
| LIS | 本题 选数 |
| 核心在问 | 很多合法前驱里,哪个最好? | 当前这个位置,选还是不选? |
| 优化方向 | 二分 / 树状数组优化前驱查询 | 换状态,直接线性,不用优化前驱 |
所以它不是「套 LIS 优化」,而是「换个状态定义后,本来就只需要 O(n)」。
八、课后迁移
- 同型必做:P1048 采药 —— 经典「选 / 不选」DP(0-1 背包)。
- 练手感:再找一道「带跳跃 / 间隔限制」的线性 DP,体会 nxt 的写法。
- 思考题:如果要求输出选了哪些位置,代码加什么?(提示:记录每个 i 是「选」还是「不选」,再从 dp[1] 倒着还原)