[Leaf] [P1941] 飞扬的小鸟

luogu-p1941-飞扬的小鸟">Luogu-P1941 飞扬的小鸟

1. 题面梗概

中译中: 一个像素小鸟在 N×MN \times M 的网格里飞。每秒只能往右走 1 步。你可以点击屏幕让它上升 XiX_i,或者不点让它下降 YiY_i。路边有水管(不能碰到)。求最少点几次能通关。

2. 逻辑推导

这题第一眼看过去像是一个搜索或者普通的路径规划。但是,题目给出了很多状态:“坐标”,“点击次数”,“是否要点击”…… 很明显可以使用 DP 来解决。

2.1 状态

既然题目要求最少点几次,那么我们可以定义 ff 数组的值为最少点击次数。然后考虑使用什么状态——明显就是坐标啦~

定义:dp[i][j]dp[i][j] 表示移动到 (i,j)(i, j) 位置最少的点击次数。

同时因为每秒往右走一步,所以 ii 不止是横坐标,同时也是时间轴,读者可以想想若每秒不止走 1 步的情况呢?

2.2 转移

我们考虑状态如何转移,对于某一个点 (i,j)(i, j) 来说,我们能做的,只有下面两种操作:

  1. 点击向上飞,可以点无数次 —— 细品这不是就是完全背包吗?

    • 高度 jj 是背包容量,点击次数是价值(代价),每次点击上升 XiX_i,代价为 11。
  2. 不点击下降 —— 这不就是01背包吗?

    • 点和不点,不点就直接从 (i−1,j+Yi)(i-1, j + Y_i) 转移过来。

好,分析完了之后我们需要考虑两者之间的关系,很明显二者是互斥的。如果这一秒点了,那么就不会触发下降效果。

所以我们的解决办法是,先做完全背包更新上升态,再用上一秒的状态处理降落。

2.3 细节

注意到题目给的两个边界情况:天花板和水管。

  • 天花板:高度上限是 MM。如果跳过了 MM,也强制算在 MM 上。所以 f[i][M]f[i][M] 要从所有能跳过 MM 的状态里取最优。

  • 水管:每一秒结束,如果位置在水管范围内,就死了(强制设为无穷大)。

结论:每一秒先跑一遍完全背包,再修正天花板,再跑一遍 0/1 背包处理下降,最后清理合法区间。

3. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include<bits/stdc++.h>
using namespace std;

const int maxn = 1e4 + 50;
const int maxm = 2e3 + 50;
const int inf = 0x3f3f3f3f;

int l[maxn], h[maxn];
bool flag[maxn];
int dp[maxn][maxm];
int x[maxn], y[maxn];
int n, m, k;

int main( )
{
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);

cin >> n >> m >> k;
for( int i = 1; i <= n; ++ i )
{
cin >> x[i] >> y[i];
l[i] = 0;
h[i] = m + 1;
flag[i] = 0;
}

for( int i = 1; i <= k; ++ i )
{
int p, _l, _h;
cin >> p >> _l >> _h;
l[p] = _l, h[p] = _h;
flag[p] = 1;
}

memset( dp, 127, sizeof( dp ) );

for( int j = 1; j <= m; ++ j )
{
dp[0][j] = 0;
}

int cnt = 0;
for( int i = 1; i <= n; ++ i )
{
for( int j = x[i] + 1; j <= m; ++ j )
{
dp[i][j] = min( dp[i][j], dp[i-1][j - x[i]] + 1 );
dp[i][j] = min( dp[i][j], dp[i][j - x[i]] + 1 );
}

for( int j = m - x[i]; j <= m; ++ j )
{
dp[i][m] = min( dp[i][m], dp[i][j] + 1 );
dp[i][m] = min( dp[i][m], dp[i-1][j] + 1);
}

for( int j = 1; j <= m; ++ j )
{
dp[i][j] = min( dp[i][j], dp[i-1][ j + y[i] ] );
}

for( int j = 1; j <= l[i]; ++ j ) dp[i][j] = inf;
for( int j = h[i]; j <= m; ++ j ) dp[i][j] = inf;

int mn = inf;
for( int j = 1; j <= m; ++ j )
{
mn = min( mn, dp[i][j] );
}

if( mn == inf )
{
cout << 0 << '\n';
cout << cnt << '\n';
return 0;
}

if( flag[i] ) cnt ++;
}

int ans = inf;
for( int j = 1; j <= m; ++ j )
{
ans = min( ans, dp[n][j] );
}

cout << 1 << '\n';
cout << ans << '\n';

return 0;
}

4. 复盘

  • 复杂度: O(NM)O( NM )。

  • 碎碎念: 这类问题属于混合背包,问题的关在在于梳理状态转移以及转移的顺序。小Trick:同一秒能干多次的事用完全背包,每秒只能干一次或必干的事用 0/1 背包从上一时刻转移。

  • 关联笔记: 背包DP


[Leaf] [P1941] 飞扬的小鸟
http://miqiu.space/posts/eed28785/
作者
Miqiu
发布于
2025年11月23日
许可协议