枚举跳跃高度和连跳次数,在固定参数下对位置与当前连跳进度做记忆化搜索,最后减去升级费用并比较最优方案。
OJ: luogu
题目 ID: P3257
难度:提高+/省选-
标签:动态规划记忆化搜索状态设计枚举
日期: 2026-06-21 13:32
题意
在一个 n x m 的地图里,从起点 (0,1) 跑到终点线右侧。
每个格子有收益,-1 表示这个点不能经过。
开始前要先确定两件事:
- 跳跃高度
H - 连跳数
C
升级这两个参数都要花费分数:
- 跳跃高度每升一级花
cost1 - 连跳数每升一级花
cost2
问最终最大净收益是多少,并输出对应的最小连跳数、最小跳跃高度。
如果无论如何都跑不出终点,就输出 mission failed。
思路
先看一个可以直接验证想法的朴素解:
// brute.cpp:小数据暴力搜索,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 30;
const int MAXM = 10;
const long long NEG_INF = -(1LL << 60);
int n, m, cost1, cost2;
int mp[MAXM][MAXN];
int H, C;
long long brute_dfs(int x, int y, int used) {
if (x > n) {
return 0;
}
if (mp[y][x] == -1) {
return NEG_INF;
}
if (y == 1) {
used = 0;
}
long long best = NEG_INF;
if (y == 1) {
best = max(best, brute_dfs(x + 1, 1, 0));
}
else {
if (y - 1 == 1) {
best = max(best, brute_dfs(x + 1, 1, 0));
}
else {
best = max(best, brute_dfs(x + 1, y - 1, used));
}
}
if (used < C && y + H <= m) {
long long extra = 0;
bool ok = true;
for (int i = 1; i < H; i++) {
int nx = x + i;
int ny = y + i;
if (nx > n) {
break;
}
if (mp[ny][nx] == -1) {
ok = false;
break;
}
extra += mp[ny][nx];
}
if (ok) {
best = max(best, extra + brute_dfs(x + H, y + H, used + 1));
}
}
if (best == NEG_INF) {
return NEG_INF;
}
return best + mp[y][x];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> cost1 >> cost2;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
cin >> mp[i][j];
}
}
long long best_score = NEG_INF;
int best_c = -1;
int best_h = -1;
for (int c = 1; c <= 5; c++) {
for (int h = 1; c * h < m; h++) {
C = c;
H = h;
long long ret = brute_dfs(0, 1, 0);
if (ret == NEG_INF) {
continue;
}
long long score = ret - 1LL * cost1 * (h - 1) - 1LL * cost2 * (c - 1);
if (score > best_score ||
(score == best_score && (c < best_c || (c == best_c && h < best_h)))) {
best_score = score;
best_c = c;
best_h = h;
}
}
}
if (best_c == -1) {
cout << "mission failed\n";
}
else {
cout << best_score << ' ' << best_c << ' ' << best_h << '\n';
}
return 0;
}这题最麻烦的地方在于:跳跃高度和连跳数不是过程中的决策,而是开始前一次性设定好的。
因此最自然的处理方式是:
- 先枚举跳跃高度
H - 再枚举连跳数
C - 在固定
(H, C)的前提下,求一条最优路径
接下来只看固定参数下怎么做。
把状态记成:
- 当前横坐标
x - 当前高度
y - 这一轮腾空过程中已经用了多少次跳跃
used
如果当前在地面:
- 可以继续向前跑一格
- 可以开始一次跳跃
如果当前在空中:
- 可以继续下落一格
- 如果还没超过连跳次数,就可以再次起跳
注意这里的“再次起跳”是从当前位置再斜着向右上跳 H 格,中间经过的上升点收益要一起算进去。
这就形成了一个只向右转移的多段图,所以可以直接做记忆化搜索。
记忆化转移方程
固定 (H,C) 后,设 dfs(x,y,used) 表示从当前位置继续跑到终点线右侧的最大额外收益。
主要转移有两类:
表示继续向右跑或在空中下落;若还能继续跳,则:
其中 extra 是起跳过程中经过的中间格收益。
每组 (H,C) 求出的路径收益还要减去升级费用。
再注意一个合法性约束:
- 既然设定好参数后必须保证不会跳出高度上限
那么只需要枚举满足:
C * H < m
的参数组合即可。
对于每组 (H, C),求出最大路径收益后,再减去升级代价:
cost1 * (H - 1) + cost2 * (C - 1)
最后按题意要求比较:
- 净收益最大
- 若净收益相同,连跳数最小
- 若还相同,跳跃高度最小
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
const int MAXM = 25;
const long long NEG_INF = -(1LL << 60);
int n, m, cost1, cost2;
int mp[MAXM][MAXN];
long long dp[MAXN][MAXM][6];
bool vis[MAXN][MAXM][6];
int H, C;
// 记忆化搜索:
// dfs(x, y, used) 表示当前在 (x, y),当前这次腾空过程已经用了 used 次跳跃,
// 从这里跑到终点线右侧的最大额外收益(包含当前位置收益,不含起点 (0,1))。
long long dfs(int x, int y, int used) {
if (x > n) {
return 0;
}
if (mp[y][x] == -1) {
return NEG_INF;
}
if (y == 1) {
used = 0;
}
if (vis[x][y][used]) {
return dp[x][y][used];
}
vis[x][y][used] = true;
long long best = NEG_INF;
if (y == 1) {
// 在地面上可以继续向前跑一格。
best = max(best, dfs(x + 1, 1, 0));
}
else {
// 在空中可以继续下落一格;落地后连跳计数清零。
if (y - 1 == 1) {
best = max(best, dfs(x + 1, 1, 0));
}
else {
best = max(best, dfs(x + 1, y - 1, used));
}
}
// 如果当前还能继续跳一次,则尝试从当前位置起跳/连跳。
if (used < C && y + H <= m) {
long long extra = 0;
bool ok = true;
// 起跳/连跳过程中,会上升经过这些中间点。
for (int i = 1; i < H; i++) {
int nx = x + i;
int ny = y + i;
if (nx > n) {
break;
}
if (mp[ny][nx] == -1) {
ok = false;
break;
}
extra += mp[ny][nx];
}
if (ok) {
best = max(best, extra + dfs(x + H, y + H, used + 1));
}
}
if (best == NEG_INF) {
dp[x][y][used] = NEG_INF;
}
else {
dp[x][y][used] = best + mp[y][x];
}
return dp[x][y][used];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> cost1 >> cost2;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
cin >> mp[i][j];
}
}
long long best_score = NEG_INF;
int best_c = -1;
int best_h = -1;
for (int c = 1; c <= 5; c++) {
for (int h = 1; c * h < m; h++) {
C = c;
H = h;
memset(vis, 0, sizeof(vis));
long long ret = dfs(0, 1, 0);
if (ret == NEG_INF) {
continue;
}
long long score = ret - 1LL * cost1 * (h - 1) - 1LL * cost2 * (c - 1);
if (score > best_score ||
(score == best_score && (c < best_c || (c == best_c && h < best_h)))) {
best_score = score;
best_c = c;
best_h = h;
}
}
}
if (best_c == -1) {
cout << "mission failed\n";
}
else {
cout << best_score << ' ' << best_c << ' ' << best_h << '\n';
}
return 0;
}复杂度
设可枚举的参数组数量为 S。
对每组 (H, C),状态数大约是:
而 m <= 20、C <= 5,状态非常小。
总复杂度可以看作
总结
这题的关键有两层:
- 开始前先枚举固定的跳跃参数
- 固定参数后,把过程看成一个只会向右推进的记忆化搜索
本质上是“参数枚举 + 小状态 DP”的组合题。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。



