最长抖动子序列:两个滚动变量 up/down 沿相邻高度比较 O(n) 完成转移。
OJ: roj
题目 ID: 3612
难度:提高
标签:DP线性DP贪心子序列
创建: 2026-10-02 10:29
更新: 2026-10-10 11:30
形式化题目
给定长度为
- 条件 A(偶数位是"峰"):对所有
,有 且 ; - 条件 B(偶数位是"谷"):对所有
,有 且 。
当
即:在
样例:5 3 2 1 2 中留下第 5 1 2:
正解
思路
最直接的想法是子序列 DP:设
这是
观察 1:最优解的结尾可以往后挪。 如果某个"末段下降"的最优子序列结尾在位置
观察 2:只有相邻高度关系有意义。 设滚动变量:
up= 到当前位置为止,末段上升的波浪子序列的最大长度;down= 到当前位置为止,末段下降的波浪子序列的最大长度。
扫一遍
(新花更高):上升段只能接在下降段后面 → up = down + 1;此时不更新down。(新花更矮):对称地 down = up + 1,不更新up。:等高的花不能共存于波浪序列中,跳过。
这里有一个微妙但关键的点:连续同向的比较不会让值虚增。例如一路下降 5 3 2 1,up 始终是 1,每步 down = up + 1 赋的都是同一个 2——它不会涨成 3、4。这正是我们想要的:5 3 2 1 并不是合法的"谷峰"波浪序列(波浪要求高低关系交替),而滚动赋值"同向不叠加"恰好排除了连续同向。反过来,只要高低关系真的交替了一次,接龙才会让长度
用样例 5 3 2 1 2 推演(初始 up = down = 1):
| 与前一项比较 | 动作 | up |
down |
||
|---|---|---|---|---|---|
| 0 | 5 | 初始 | — | 1 | 1 |
| 1 | 3 | down = up + 1 |
1 | 2 | |
| 2 | 2 | down 赋回 2,不变 |
1 | 2 | |
| 3 | 1 | down 赋回 2,不变 |
1 | 2 | |
| 4 | 2 | up = down + 1 |
3 | 2 |
答案 5 1 2,与样例一致。
两个滚动变量各自记录"最后一次转向"的信息,整个算法只有一次线性扫描。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main(){ios::sync_with_stdio(false); cin.tie(nullptr); int n; if(!(cin>>n)) return 0; vector<ll> h(n); for(int i=0;i<n;i++) cin>>h[i]; int up=1,down=1; for(int i=1;i<n;i++){ if(h[i]>h[i-1]) up=down+1; else if(h[i]<h[i-1]) down=up+1; } cout<<max(up,down)<<'\n'; return 0;}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 10:29
# update_at: 2026-10-04 10:39
import sys
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
h = [next(data) for _ in range(n)]
# up / down:当前最长"抖动"序列的长度,
# up = 末段在上升,down = 末段在下降;初始各留一株花,长度都是 1
up = down = 1
for i in range(1, n):
if h[i] > h[i - 1]: # 新花更高:只能接在"下降"序列后形成上升段
up = down + 1 # down 保守不变:这时接上不会让 down 更优
elif h[i] < h[i - 1]: # 新花更矮:只能接在"上升"序列后形成下降段
down = up + 1
# 相等的花必被移走,两个状态都维持不变
print(max(up, down))
if __name__ == "__main__":
solve()复杂度
- 时间:
,一次扫描,每步 。 - 空间:
存高度数组(读入所需;逻辑上只需 个变量)。
图示解析
波浪的两种形状与"转向接龙"
下图展示扫描时的状态机:每读入一株新花,按它与前一株的高低关系在 up / down 两个状态间接龙:
flowchart LR
S[读入相邻对 hi-1 hi] --> C1{"hi 与 hi-1 的高低关系?"}
C1 -- "hi 更高:上升" --> U["up = down + 1(接在下降段后)"]
C1 -- "hi 更矮:下降" --> D["down = up + 1(接在上升段后)"]
C1 -- "等高:不能共存" --> K["两状态都不变"]
U --> N["答案 max up down"]
D --> N
K --> N
把状态机套到样例 5 3 2 1 2 上,保留方案 5 1 2(先降后升的"谷型"波浪,满足条件 B)对应这样一条路径:
| 步骤 | 高度比较 | 走的分支 | 保留的花 |
|---|---|---|---|
| 初始 | — | up = down = 1 |
5 |
| 1 | down = up + 1 = 2 |
5 3(末段下降) |
|
| 2 | down 赋回 2,不变 |
仍 5 3 |
|
| 3 | down 赋回 2,不变 |
换成 5 1,长度不变 |
|
| 4 | up = down + 1 = 3 |
5 1 2 |
观察要点:
- 全序列
5 3 2 1 2中间有一段连续下降5>3>2>1,高低关系没有交替,不是合法波浪; - 状态机里"同向下降"三次都走
down = up + 1,赋的是同一个 2 —— 连续同向不会叠加长度,这恰好把5 3 2 1这类非法序列排除在外; - 直到
出现转向, up才从 2 变成 3,得到最优解5 1 2,答案 3。
总结
- 本题是经典的最长抖动(摆动)子序列:合法保留方案等价于相邻高低关系交替的子序列。
- 朴素
子序列 DP 中,利用"最优解结尾可往后挪"的交换论证,把转移压缩成相邻比较,得到两个滚动变量 up/down的解。 - 实现要点:上升时
up = down + 1且不动down,下降对称,等高跳过;连续同向的赋值天然不叠加,保证了波浪的交替性。