把每段固定方向的时间看成一次行或列上的区间转移,设 dp[x][y] 表示当前位置最大滑行距离,再用单调队列优化每段的滑动窗口最大值。
OJ: luogu
题目 ID: P2254
难度:提高+/省选-
标签:动态规划单调队列网格
日期: 2026-06-21 06:17
题意
舞厅是一个 n x m 的网格,x 表示有家具,. 表示空地。
钢琴一开始在 (start_x, start_y)。
接下来有 k 段时间,第 i 段时间内船体倾斜方向固定。
这一段里的每一秒:
- 可以施魔法,让钢琴原地不动
- 也可以不施魔法,让钢琴朝当前固定方向滑动一格
但钢琴不能撞到家具,也不能滑出舞厅。
要求最大化总滑行距离。
思路
先看一个小数据暴力:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 15;
const int MAXT = 205;
const int NEG_INF = -1000000000;
int n, m, start_x, start_y, seg_cnt;
char grid_map[MAXN][MAXN];
int dp[MAXN][MAXN], nxt_dp[MAXN][MAXN];
int dir_list[MAXT], total_time;
int dx[5] = {0, -1, 1, 0, 0};
int dy[5] = {0, 0, 0, -1, 1};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// brute.cpp:按时间一秒一秒地做 DP。
// 每一秒只有两种选择:施魔法原地不动,或者顺着当前方向滑动一格。
cin >> n >> m >> start_x >> start_y >> seg_cnt;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> grid_map[i][j];
}
}
total_time = 0;
for (int i = 1; i <= seg_cnt; i++) {
int l, r, d;
cin >> l >> r >> d;
int len = r - l + 1;
for (int t = 1; t <= len; t++) {
dir_list[++total_time] = d;
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = NEG_INF;
}
}
dp[start_x][start_y] = 0;
for (int t = 1; t <= total_time; t++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
nxt_dp[i][j] = NEG_INF;
}
}
int d = dir_list[t];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (dp[i][j] <= NEG_INF / 2) {
continue;
}
// 这一秒施魔法,原地不动。
nxt_dp[i][j] = max(nxt_dp[i][j], dp[i][j]);
// 这一秒不施魔法,顺着当前方向滑一格。
int ni = i + dx[d];
int nj = j + dy[d];
if (ni >= 1 && ni <= n && nj >= 1 && nj <= m &&
grid_map[ni][nj] != 'x') {
nxt_dp[ni][nj] = max(nxt_dp[ni][nj], dp[i][j] + 1);
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = nxt_dp[i][j];
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
ans = max(ans, dp[i][j]);
}
}
cout << ans << '\n';
return 0;
}暴力按“秒”来做 DP。
设 dp[x][y] 表示当前这一秒结束后,钢琴停在 (x,y) 的最大滑行距离。
每秒只有两种决策:
- 施魔法,留在原地
- 不施魔法,沿当前方向走一格
这样总时间是 T,总复杂度大约是
本题 T 最多到 40000,虽然不算特别离谱,但如果把每段时间单独拆成一秒一秒处理,常数会比较大,而且很难进一步利用“同一段方向固定”这个性质。
正解的关键是按“整段时间”处理。
设:
dp[x][y] 表示处理完前若干段时间后,钢琴停在 (x,y) 的最大滑行距离。
考虑某一整段时间,方向固定,长度为 len。
假设这一段方向是“向右”。
如果这一段结束后停在 (x,y),那么这一整段开始时一定在同一行的某个位置 (x,k),并且:
k <= yy-k <= len- 从
k到y中间不能跨过家具
如果这段时间里从 k 走到了 y,新增的滑行距离就是 y-k,所以转移式是:
dp[x][y] = max(old_dp[x][k] + y-k)
整理一下:
dp[x][y] = y + max(old_dp[x][k] - k)
这里 k 的可选范围是一个长度不超过 len+1 的滑动窗口。
对固定的一行来说,y 从左到右枚举时,这个窗口也在一起右移,所以可以用单调队列维护
old_dp[x][k] - k
的最大值。
向左、向上、向下完全同理,只是把维护的式子换成:
- 向左:
old_dp[x][k] + k - 向上:
old_dp[k][y] + k - 向下:
old_dp[k][y] - k
另外,家具会把一行或一列切成若干互不连通的连续空地段。
一旦扫到家具,这一格就不能落脚,也不能跨过去,因此单调队列必须立刻清空,表示窗口不能跨障碍延续。
于是每一段时间都可以在
DP 转移方程
核心状态:
dp[x][y] 为处理完若干段后停在格子的最大距离
核心转移:
向右: new[x][y]=y+max(old[x][k]-k)
答案收束:
所有格子的最大 dp
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 205;
const int NEG_INF = -1000000000;
int n, m, start_x, start_y, seg_cnt;
char grid_map[MAXN][MAXN];
// dp[x][y]:处理完当前这段时间后,钢琴停在 (x,y) 的最大滑行距离。
int dp[MAXN][MAXN];
// old_dp 是上一段时间结束后的状态,当前段的所有转移都只能从它转来。
int old_dp[MAXN][MAXN];
// 单调队列里存的是某一行/列上的候选下标。
int q[MAXN];
struct Segment {
int l, r, dir;
} seg[MAXN];
// 处理“向上滑动 len 步”的整段转移。
void move_up(int len) {
for (int col = 1; col <= m; col++) {
int head = 0, tail = -1;
for (int row = n; row >= 1; row--) {
if (grid_map[row][col] == 'x') {
head = 0;
tail = -1;
dp[row][col] = NEG_INF;
continue;
}
// 向上最多走 len 步,所以候选起点行号不能超过 row + len。
while (head <= tail && q[head] > row + len) {
head++;
}
// 转移式:
// old_dp[k][col] + (k - row)
// 对固定 row 来说,只要维护 old_dp[k][col] + k 的最大值即可。
while (head <= tail &&
old_dp[q[tail]][col] + q[tail] <= old_dp[row][col] + row) {
tail--;
}
q[++tail] = row;
if (old_dp[q[head]][col] <= NEG_INF / 2) {
dp[row][col] = NEG_INF;
} else {
dp[row][col] = old_dp[q[head]][col] + q[head] - row;
}
}
}
}
// 处理“向下滑动 len 步”的整段转移。
void move_down(int len) {
for (int col = 1; col <= m; col++) {
int head = 0, tail = -1;
for (int row = 1; row <= n; row++) {
if (grid_map[row][col] == 'x') {
head = 0;
tail = -1;
dp[row][col] = NEG_INF;
continue;
}
// 向下最多走 len 步,所以候选起点行号不能小于 row - len。
while (head <= tail && q[head] < row - len) {
head++;
}
// 转移式:
// old_dp[k][col] + (row - k)
// 对固定 row 来说,维护 old_dp[k][col] - k 的最大值。
while (head <= tail &&
old_dp[q[tail]][col] - q[tail] <= old_dp[row][col] - row) {
tail--;
}
q[++tail] = row;
if (old_dp[q[head]][col] <= NEG_INF / 2) {
dp[row][col] = NEG_INF;
} else {
dp[row][col] = old_dp[q[head]][col] - q[head] + row;
}
}
}
}
// 处理“向左滑动 len 步”的整段转移。
void move_left(int len) {
for (int row = 1; row <= n; row++) {
int head = 0, tail = -1;
for (int col = m; col >= 1; col--) {
if (grid_map[row][col] == 'x') {
head = 0;
tail = -1;
dp[row][col] = NEG_INF;
continue;
}
// 向左最多走 len 步,所以候选起点列号不能超过 col + len。
while (head <= tail && q[head] > col + len) {
head++;
}
// 转移式:
// old_dp[row][k] + (k - col)
while (head <= tail &&
old_dp[row][q[tail]] + q[tail] <= old_dp[row][col] + col) {
tail--;
}
q[++tail] = col;
if (old_dp[row][q[head]] <= NEG_INF / 2) {
dp[row][col] = NEG_INF;
} else {
dp[row][col] = old_dp[row][q[head]] + q[head] - col;
}
}
}
}
// 处理“向右滑动 len 步”的整段转移。
void move_right(int len) {
for (int row = 1; row <= n; row++) {
int head = 0, tail = -1;
for (int col = 1; col <= m; col++) {
if (grid_map[row][col] == 'x') {
head = 0;
tail = -1;
dp[row][col] = NEG_INF;
continue;
}
// 向右最多走 len 步,所以候选起点列号不能小于 col - len。
while (head <= tail && q[head] < col - len) {
head++;
}
// 转移式:
// old_dp[row][k] + (col - k)
while (head <= tail &&
old_dp[row][q[tail]] - q[tail] <= old_dp[row][col] - col) {
tail--;
}
q[++tail] = col;
if (old_dp[row][q[head]] <= NEG_INF / 2) {
dp[row][col] = NEG_INF;
} else {
dp[row][col] = old_dp[row][q[head]] - q[head] + col;
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> start_x >> start_y >> seg_cnt;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> grid_map[i][j];
}
}
for (int i = 1; i <= seg_cnt; i++) {
cin >> seg[i].l >> seg[i].r >> seg[i].dir;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = NEG_INF;
}
}
dp[start_x][start_y] = 0;
for (int i = 1; i <= seg_cnt; i++) {
// 这一段时间的总秒数。
int len = seg[i].r - seg[i].l + 1;
for (int x = 1; x <= n; x++) {
for (int y = 1; y <= m; y++) {
old_dp[x][y] = dp[x][y];
}
}
// 方向编号:
// 1 上,2 下,3 左,4 右
if (seg[i].dir == 1) {
move_up(len);
} else if (seg[i].dir == 2) {
move_down(len);
} else if (seg[i].dir == 3) {
move_left(len);
} else {
move_right(len);
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
ans = max(ans, dp[i][j]);
}
}
cout << ans << '\n';
return 0;
}复杂度
时间复杂度
总结
这题表面是在网格上模拟滑动,核心其实是“固定方向的一整段时间”的区间最值转移。
一旦把每一段压成一次行/列 DP,就能自然地看到单调队列优化的窗口结构。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

