用按高度滚动的动态规划合并连续点击与重力转移,并在管道位置过滤非法高度。
OJ: luogu
题目 ID: P1941
难度:普及+/提高
标签:动态规划DP模拟
日期: 2026-07-24 17:47
题意
小鸟从横坐标 0 的任意高度出发,每个单位时间向右移动一格。点击可以上升 x[i],同一时间可以点击多次;不点击则下降 y[i]。高度超过 m 时停在 m,高度为 0 或碰到管道边界则失败。
如果能到达横坐标 n,求最少点击次数;否则求最多通过的管道数量。
思路
先看一个直接枚举每一步操作的朴素解:
cpp
/**
* 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-07-24 17:47
* update_at: 2026-07-24 17:53
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int INF = 1000000000;
const int MAXN = 20;
int n, m, k;
int up_step[MAXN], down_step[MAXN];
int has_pipe[MAXN], pipe_low[MAXN], pipe_high[MAXN];
int best_clicks = INF;
int most_passed = 0;
void search_paths(int position, int height, int clicks, int passed_pipes) {
most_passed = max(most_passed, passed_pipes);
if (position == n) {
best_clicks = min(best_clicks, clicks);
return;
}
int rise = up_step[position];
// 枚举这一步点击 1 次、2 次……,直到高度达到 m。
// 到达 m 后再点击一次仍会停在 m,这也是一次合法的时间转移。
int target = height;
for (int count = 1;; count++) {
target = min(m, target + rise);
int next_position = position + 1;
if (!has_pipe[next_position]
|| (target > pipe_low[next_position] && target < pipe_high[next_position])) {
search_paths(next_position, target, clicks + count, passed_pipes + has_pipe[next_position]);
}
if (target == m) {
break;
}
}
// 枚举这一步不点击的情况。
target = height - down_step[position];
if (target > 0) {
int next_position = position + 1;
if (!has_pipe[next_position]
|| (target > pipe_low[next_position] && target < pipe_high[next_position])) {
search_paths(next_position, target, clicks, passed_pipes + has_pipe[next_position]);
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
for (int position = 0; position < n; position++) {
cin >> up_step[position] >> down_step[position];
}
for (int i = 1; i <= k; i++) {
int position, low, high;
cin >> position >> low >> high;
has_pipe[position] = 1;
pipe_low[position] = low;
pipe_high[position] = high;
}
// 小数据从每个合法初始高度出发,完整枚举每一步的点击次数。
for (int height = 1; height <= m; height++) {
search_paths(0, height, 0, 0);
}
if (best_clicks < INF) {
cout << 1 << '\n' << best_clicks << '\n';
} else {
cout << 0 << '\n' << most_passed << '\n';
}
return 0;
}这个暴力把每次点击次数和不点击都枚举出来,只适合小数据。优化时定义 dp[h] 为到达当前列高度 h 的最少点击次数。一次点击先转移到 min(m,h+x),再在当前数组中从低到高补齐连续点击;目标高度为 m 时,m-x 到 m-1 的来源都要纳入。另一条转移是不点击,从旧高度 h+y 到新高度 h,代价不变。
到达管道位置后只保留 l<height<h 的状态。若没有状态,说明当前管道无法通过;否则增加已经通过的管道计数。
DP 状态表
下面的表展示抓取样例前两次移动中,上一列状态、上升转移、下降转移和合并结果。单元格是到达对应高度的最少点击次数,∞ 表示不可达。
| 状态 | h=1 | h=2 | h=3 | h=4 | h=5 | h=6 | h=7 | h=8 | h=9 | h=10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 位置 0 前的 dp | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 位置 1:上升(含连续点击) | ∞ | ∞ | ∞ | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 位置 1:下降(不点击) | 0 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
| 位置 1:合并后 dp | 0 | ∞ | ∞ | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 位置 1 前的 dp | 0 | ∞ | ∞ | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 位置 2:上升(含连续点击) | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 1 |
| 位置 2:下降(不点击) | 1 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
| 位置 2:合并后 dp | 1 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 1 |
第一步的 x=3,y=9,所以点击结果从高度 4 开始可达,下降结果只有从 10 降到 1。第二步的 x=9 会把多种来源都封顶到 10,这正是代码单独处理 next_dp[m] 的原因。
代码
cpp
/**
* 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-07-24 17:47
* update_at: 2026-07-24 17:51
*/
#include <bits/stdc++.h>
using namespace std;
const int INF = 1000000000;
const int MAXN = 10005;
const int MAXM = 1005;
int n, m, k;
int up_step[MAXN], down_step[MAXN];
int has_pipe[MAXN], pipe_low[MAXN], pipe_high[MAXN];
int dp[MAXM], next_dp[MAXM];
bool has_reachable_height() {
for (int height = 1; height <= m; height++) {
if (dp[height] < INF) {
return true;
}
}
return false;
}
int minimum_clicks() {
for (int height = 1; height <= m; height++) {
dp[height] = 0;
}
int passed_pipes = 0;
for (int position = 1; position <= n; position++) {
for (int height = 1; height <= m; height++) {
next_dp[height] = INF;
}
int rise = up_step[position - 1];
int fall = down_step[position - 1];
// 点击一次到达 min(m, height + rise),再用下面的循环补齐连续点击。
for (int height = 1; height <= m; height++) {
if (dp[height] >= INF) {
continue;
}
int target = min(m, height + rise);
next_dp[target] = min(next_dp[target], dp[height] + 1);
}
// 对于还没有到顶的高度,每增加 rise 高度需要再点击一次。
for (int height = rise + 1; height < m; height++) {
if (next_dp[height - rise] < INF) {
next_dp[height] = min(next_dp[height], next_dp[height - rise] + 1);
}
}
// 到达 m 时会封顶,所以 m-rise 到 m-1 的状态都能一步到达 m。
int source_begin = max(1, m - rise);
for (int height = source_begin; height < m; height++) {
if (next_dp[height] < INF) {
next_dp[m] = min(next_dp[m], next_dp[height] + 1);
}
}
// 不点击时下降 fall,高度不能降到 0。
for (int height = fall + 1; height <= m; height++) {
if (dp[height] < INF) {
next_dp[height - fall] = min(next_dp[height - fall], dp[height]);
}
}
for (int height = 1; height <= m; height++) {
dp[height] = next_dp[height];
}
if (has_pipe[position]) {
for (int height = 1; height <= m; height++) {
if (height <= pipe_low[position] || height >= pipe_high[position]) {
dp[height] = INF;
}
}
if (!has_reachable_height()) {
cout << 0 << '\n' << passed_pipes << '\n';
return -1;
}
passed_pipes++;
} else if (!has_reachable_height()) {
cout << 0 << '\n' << passed_pipes << '\n';
return -1;
}
}
int answer = INF;
for (int height = 1; height <= m; height++) {
answer = min(answer, dp[height]);
}
cout << 1 << '\n' << answer << '\n';
return answer;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
for (int position = 0; position < n; position++) {
cin >> up_step[position] >> down_step[position];
}
for (int i = 1; i <= k; i++) {
int position, low, high;
cin >> position >> low >> high;
has_pipe[position] = 1;
pipe_low[position] = low;
pipe_high[position] = high;
}
minimum_clicks();
return 0;
}复杂度
- 时间复杂度:
O(nm)。 - 空间复杂度:
O(n+m)。
总结
这道题的核心是把完整飞行路线压缩成“当前位置 + 高度”的最优状态。连续点击可以在同一层用递推展开,重力是不点击的反向高度转移,管道则只是对状态集合做过滤。
