按横坐标滚动的一维 DP:上升用完全背包式链式递推把任意次点击压成一次升序扫描,下降与管道过滤用切片,同一趟同时维护最少点击和最多过管两个目标。
OJ: roj
题目 ID: 3620
难度:普及+/提高-
标签:动态规划DPpython
创建: 2026-10-02 10:54
更新: 2026-10-10 11:30
形式化题目
给定长
- 每个横坐标
配一个上升量 与下降量 ,且 ; 根管道位于横坐标 (互不相同、不保证有序),缝隙是开区间 ,满足 ,即缝隙里至少留有一个整数高度; ,首列没有管道。
小鸟从
一次也不点则
样例 1(1 / 6。一条最优路线从高度
样例 2(管道在 0 / 3。起点高度
正解
思路
朴素做法与瓶颈。 把每个 (横坐标, 高度) 当状态做搜索或 DP 是自然的,但转移要枚举"这一格点几次":
状态与转移。 设 tap[h] 是当前列高度 cnt[h] 是对应路径的最多过管数(不可达为
| 转移 | 条件 | 落点 | tap(取 |
cnt(取 |
|---|---|---|---|---|
| 点 |
来源可达 | 纳入来源 |
纳入来源 | |
| 一次不点 | 来源可达且 |
纳入来源 | 纳入来源 | |
| 过管道(抵达 |
该列有缝隙 |
高度不变 | 不变 | 缝隙外整段置为不可达;缝隙内 |
表中前两行是水平移动产生的候选,第三行是到达新列后的过滤;三行对 tap / cnt 的更新形式完全同构(一个取
关键观察:点 nxt_tap / nxt_cnt)。轮到下标
下降与管道都用切片。 下降只有单一候选:落点
两个目标同趟完成。 边能不能走只取决于状态是否可达,与两个聚合值无关;点击用 cnt 的最大值必须逐列维护,不能只看最后一列。
样例 1 前两列的 DP 快照(
| 高度 |
tap |
tap |
tap |
过管计数 cnt |
|---|---|---|---|---|
| — | ||||
| — | ||||
| — | ||||
| — | ||||
| — | ||||
| — | ||||
| — |
表中 tap 为最少点击数,— 表示已撞管。出发时任意高度代价都是
正确性要点。 用归纳法:处理完第 rise 枚举完"点 fall 枚举完"不点"的唯一落点,pass_pipe 的切片精确删除缝隙外状态、给缝隙内 tap 的最小值有限当且仅当可通关,且就是最少点击数。
代码
/**
* 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;
const int INF=1000000000;
void rise(vector<int>&nt, vector<int>&nc, vector<int>&tap, vector<int>&cnt, int dx, int m){ for(int h=1;h<=m;h++){int t=h+dx; if(t>m) t=m; int v=tap[h]+1; if(v<nt[t]) nt[t]=v; if(cnt[h]>nc[t]) nc[t]=cnt[h]; int w=nt[h]+1; if(w<nt[t]) nt[t]=w; if(nc[h]>nc[t]) nc[t]=nc[h];}}
void fall(vector<int>&nt, vector<int>&nc, vector<int>&tap, vector<int>&cnt, int dy, int m){ for(int to=1;to<=m-dy;to++){int from=to+dy; if(tap[from]<nt[to]) nt[to]=tap[from]; if(cnt[from]>nc[to]) nc[to]=cnt[from];}}
void pass_pipe(vector<int>&nt, vector<int>&nc, pair<int,int> gap, int m){int low=gap.first, high=gap.second; for(int h=1;h<=low;h++){nt[h]=INF; nc[h]=-1;} for(int h=high;h<=m;h++){nt[h]=INF; nc[h]=-1;} for(int h=low+1;h<high;h++) if(nc[h]>=0) nc[h]++;}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr); int n,m,k; if(!(cin>>n>>m>>k)) return 0; vector<pair<int,int> > steps(n); for(int i=0;i<n;i++) cin>>steps[i].first>>steps[i].second; vector<pair<int,int> > pipe(n, {-1,-1}); for(int i=0;i<k;i++){int p,l,h;cin>>p>>l>>h; pipe[p]={l,h};} vector<int> tap(m+1), cnt(m+1); int best=0; for(int x=0;x<n;x++){vector<int> nt(m+1,INF), nc(m+1,-1); rise(nt,nc,tap,cnt,steps[x].first,m); fall(nt,nc,tap,cnt,steps[x].second,m); if(x+1<n && pipe[x+1].first!=-1) pass_pipe(nt,nc,pipe[x+1],m); for(int h=1;h<=m;h++) best=max(best,nc[h]); tap=nt; cnt=nc;} int few=INF; for(int h=1;h<=m;h++) few=min(few,tap[h]); bool won=few<INF; cout<<(won?1:0)<<'\n'<<(won?few:best)<<'\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:54
# update_at: 2026-10-02 11:20
import sys
INF = 10**9 # 最少点击数的不可达哨兵:任一合法路径点击数不超过 n*m <= 1e7
def rise(nxt_tap: list[int], nxt_cnt: list[int], tap: list[int], cnt: list[int], dx: int, m: int) -> None:
"""上升转移:本单位时间至少点一次,多次点击效果叠加,到顶 m 封顶。
按高度升序扫一遍即可传播"点 t 次":写入 nxt_tap[t] 的是"从上一列点一次",
而 nxt_tap[h] 在轮到 h 时已经定型(只有下标 h-dx 会写它),所以
`nxt_tap[h] + 1` 正是"同单位时间再叠一次"。此刻 nxt 里只有上升结果,
链式叠加不会混入下降值。
"""
for h in range(1, m + 1):
t = h + dx
if t > m:
t = m # 到顶后再点也升不上去,统一收进 m
v = tap[h] + 1 # 上一列点一次到达 t
if v < nxt_tap[t]:
nxt_tap[t] = v
if cnt[h] > nxt_cnt[t]:
nxt_cnt[t] = cnt[h]
w = nxt_tap[h] + 1 # 同一单位时间继续叠加点击
if w < nxt_tap[t]:
nxt_tap[t] = w
if nxt_cnt[h] > nxt_cnt[t]:
nxt_cnt[t] = nxt_cnt[h]
def fall(nxt_tap: list[int], nxt_cnt: list[int], tap: list[int], cnt: list[int], dy: int, m: int) -> None:
"""下降转移:一次也不点,整体下落 dy;落到 0 即坠毁,只保留高度 >= 1 的落点。"""
tgt = slice(1, m - dy + 1) # 落点 1..m-dy
src = slice(dy + 1, m + 1) # 起点 dy+1..m,落差正好 dy
nxt_tap[tgt] = [a if a < b else b for a, b in zip(nxt_tap[tgt], tap[src])]
nxt_cnt[tgt] = [a if a > b else b for a, b in zip(nxt_cnt[tgt], cnt[src])]
def pass_pipe(nxt_tap: list[int], nxt_cnt: list[int], gap: tuple[int, int], m: int) -> None:
"""过管转移:缝隙 (low, high) 外的高度撞管坠毁,缝隙内存活者通过计数 +1。"""
low, high = gap
nxt_tap[1:low + 1] = [INF] * low # 撞下边沿及其下方
nxt_tap[high:m + 1] = [INF] * (m + 1 - high) # 撞上边沿及其上方
nxt_cnt[1:low + 1] = [-1] * low
nxt_cnt[high:m + 1] = [-1] * (m + 1 - high)
# 计数只加在存活者身上:-1 表示该高度本就不可达
nxt_cnt[low + 1:high] = [v + 1 if v >= 0 else -1 for v in nxt_cnt[low + 1:high]]
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data) # 界面长度:横坐标 0..n-1,走完 n 步到最右边
m = next(data) # 界面高度:存活高度 1..m,0 即坠毁
k = next(data) # 管道数
# 每列一行 (X, Y):点一次上升 X[x],一次不点下降 Y[x]
steps = [(next(data), next(data)) for _ in range(n)]
# pipe_at[x] = (L, H):缝隙是开区间 (L, H),贴边即撞管
pipe_at: list[tuple[int, int] | None] = [None] * n
for _ in range(k):
p, low, high = next(data), next(data), next(data)
pipe_at[p] = (low, high)
tap = [0] * (m + 1) # 当前列各高度的最少点击数(列 0 任选高度出发)
cnt = [0] * (m + 1) # 当前列各高度的最多通过管道数
best = 0 # 全局最多通过数:路径随时可能坠毁,要逐列取最大
for x in range(n):
dx, dy = steps[x] # 本列的上升/下降步长
nxt_tap = [INF] * (m + 1)
nxt_cnt = [-1] * (m + 1)
rise(nxt_tap, nxt_cnt, tap, cnt, dx, m)
fall(nxt_tap, nxt_cnt, tap, cnt, dy, m)
# 落在下一列 x+1 的管道上:缝隙外坠毁,缝隙内通过计数 +1
gap = pipe_at[x + 1] if x + 1 < n else None
if gap is not None:
pass_pipe(nxt_tap, nxt_cnt, gap, m)
best = max(best, max(nxt_cnt))
tap, cnt = nxt_tap, nxt_cnt
fewest = min(tap[1:]) # 列 n:任一高度抵达即通关
won = fewest < INF
print(1 if won else 0)
print(fewest if won else best)
if __name__ == "__main__":
solve()复杂度
- 时间复杂度:
——每列三次线性扫描(升序点击链、下降切片、有管道时的过滤切片), ;相对朴素的 ,省掉的正是逐个枚举点击次数的那一维。 - 空间复杂度:
—— tap/cnt与nxt_tap/nxt_cnt四个滚动数组,外加两份输入缓存。 - 本地用题目仓的 20 组真实数据实测全部 PASS,最大规模(
)约 、峰值内存约 ;相对 OJ 的 限时,Python 侧存在 TLE 风险,但算法与 C++ 正解同阶,不属于以暴力换时间。
总结
- 小鸟在每个横坐标只有"高度"一个自由度,历史不影响未来 → 按列滚动的一维 DP,状态就是存活高度。
- 任意次点击不是分支爆炸的来源:点
次 = 沿步长 的链走 步,一次升序扫描(完全背包式正序更新)就把所有 收齐,上升、下降、管道过滤全部压成每列三次 的线性操作。 - 管道是"节点过滤 + 计数":缝隙
外整段切片置死即"贴边撞管",缝隙内 即"通过";失败路径由不可达自然表达。 - 最少点击(
)与最多过管( )共享同一张转移图,一趟 DP 同时维护;失败答案要逐列取最大,因为路径可能早已坠毁。
图示解析
这张图串起本题从建模到得到两行答案的主线:
题面建模:每格只有"点 t 次上升(到顶封顶)"与"不点下降",贴地、贴管即死
|- 朴素做法:枚举每格点击数 t → O(n·m²/X),重复计算同一条链
`- 关键观察:点 t 次 = 沿 h→h+dx 的链走 t 步,每步代价 +1
`- 一维滚动 DP,每列 O(m):升序链式递推 → 下降切片 → 缝隙过滤
`- 同一趟 min/max 双聚合:可通关输出最少点击,否则输出最多过管先看中间那条关键观察:它把"枚举点击次数"改写成"沿高度链走步",是整个优化的支点。再看它如何决定实现顺序——先上升链后下降,链里才不会混入下降值。最后注意两个答案共享同一次扫描,只在聚合算子上分道扬镳。
