小红的皇后

小红的皇后

方法一:动态规划(DP)+ 分段最小值维护

状态定义

设 dp[i][j] 表示皇后从左上角 (0,0) 到达格子 (i,j) 所需的最少移动步数。

由于皇后只能向右、向下、向右下移动,因此坐标严格单调递增,状态图是一个有向无环图(DAG)。我们可以按照行从上到下、每行从左到右的顺序计算所有 dp 值。

朴素转移(会超时)

到达 (i,j) 的前驱可能是:

  • 同一行左侧任意空格 (i, y'),其中 y' < j 且中间无障碍;
  • 同一列上方任意空格 (x', j),其中 x' < i 且中间无障碍;
  • 同一对角线(左上→右下方向)上的任意空格 (i-k, j-k),其中 k ≥ 1 且中间无障碍。

若枚举所有前驱,最坏复杂度为 O(nm(n+m)),不可接受。

优化:连续段内维护最小值

观察发现,对于同一行,障碍物会切断连续的可移动区间。在一个连续的空格段内,任意两个格子之间都可以一步到达(同一行)。因此,如果我们能快速知道当前连续段内最小的 dp 值,就能在 O(1) 时间内完成转移。

具体做法是,在扫描过程中维护三个最小值:

  • rowMin:当前行中,当前扫描到的连续空格段内的最小 dp 值;
  • colMin[j]:第 j 列中,当前扫描到的连续空格段内的最小 dp 值;
  • diagMin[diagIdx]:同一条对角线(i-j 为定值)上,当前扫描到的连续空格段内的最小 dp 值。为了索引非负,令 diagIdx = i - j + m。

转移过程

按 i = 0 → n-1,j = 0 → m-1 依次处理:

  1. 若 grid[i][j] == '*'(障碍):

    • 该格子不可达,且它会切断行、列、对角线上的连续段,因此将 rowMin、colMin[j]、diagMin[diagIdx] 全部重置为 INF。
    • 跳过后续步骤。
  2. 否则(空格):

    • 若 (i,j) == (0,0),则 dp[0][0] = 0。
    • 否则,取 best = min(rowMin, colMin[j], diagMin[diagIdx])。
      • 若 best == INF,则当前格子不可达,dp[i][j] = INF。
      • 否则 dp[i][j] = best + 1(因为从任意前驱一步即可到达)。
    • 若 dp[i][j] < INF,则用 dp[i][j] 更新 rowMin、colMin[j]、diagMin[diagIdx](取较小值)。

答案

最终 dp[n-1][m-1] 即为最少步数;若为 INF,输出 -1。

复杂度

  • 时间复杂度:O(nm),每个格子常数次操作。
  • 空间复杂度:O(nm) 存储 dp(可优化到 O(m) 或 O(n+m),但 2000×2000 规模下 O(nm) 可接受)。

方法二:0-1 BFS(最短路径)

核心思想

将皇后的“一次移动”拆解为“沿同一方向连续走多格”的过程。如果我们把每一步(走一格)看作图上的边,则:

  • 如果当前方向和下一步方向相同,说明还在同一次移动中,不增加移动次数;
  • 如果方向改变,说明开始了一次新的移动,步数加 1。

这样,问题就转化为一个 0-1 边权图的最短路问题,其中边权为 0(同方向)或 1(转向)。

状态定义

状态为 (x, y, dir),其中:

  • (x, y) 为当前所在格子;
  • dir 表示到达当前格子时,上一次移动的方向(0=向右,1=向右下,2=向下)。

初始状态

起点 (0,0) 可以向三个方向出发,第一次移动必须计为 1 步。因此,初始时,对于三个方向 dir = 0,1,2,设置 dist[0][0][dir] = 0,并将它们全部入队。但在扩展时,从起点出发,任何方向的边权强制为 1(而不是 0)。

转移

对于当前状态 (x, y, dir),尝试向三个方向 nd ∈ {0,1,2} 走一步到 (nx, ny):

  • 如果 (nx, ny) 超出边界或为障碍,则跳过;
  • 计算本次移动的代价 cost:
    • 如果 (x,y) 是起点 (0,0),则 cost = 1(第一次移动必须计数);
    • 否则,如果 nd == dir,则 cost = 0(继续同方向,仍为同一次移动);
    • 否则 cost = 1(转向,开始新的一次移动);
  • 新距离 ndist = dist[x][y][dir] + cost;
  • 如果 ndist < dist[nx][ny][nd],更新距离,并根据 cost 值入队:
    • 若 cost == 0,则使用 push_front(0-1 BFS 的常规操作,优先处理 0 权边);
    • 若 cost == 1,则使用 push_back。

答案

最终答案为 min_{dir} dist[n-1][m-1][dir],若为 INF,输出 -1。

正确性解释

  • 由于同方向的连续扩展代价为 0,BFS 会一次性沿同一方向推进到障碍前或边界,等价于皇后的一次合法移动。
  • 转向代价为 1,正好对应移动次数的累加。
  • 0-1 BFS 保证每个状态第一次出队时距离已是最短,因此最终答案正确。

复杂度

  • 状态总数:n × m × 3。
  • 每个状态转移尝试 3 个方向,因此时间复杂度 O(nm)。
  • 空间复杂度:O(nm) 存储距离数组(可用 uint16_t 等紧凑类型)。

两种方法对比

方面 DP + 分段最小值 0-1 BFS
核心思路 利用 DAG 顺序,维护连续段最小 dp 值 将连续移动建模为 0 权边,转化为 0-1 最短路
实现复杂度 较简单,只需按行扫描 需要 BFS 队列,状态三维
空间占用 dp[n][m] + 若干辅助数组 dist[n][m][3]
对障碍处理 遇到障碍重置对应最小值 障碍不扩展,自然截断同向 0 权边
通用性 仅适用于单调方向(右、下、右下) 可推广到更多方向或更复杂的移动规则

两种方法均为 O(nm),均能通过本题数据范围。读者可根据喜好选用。

#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9;

void solve() {
    int n, m; cin >> n >> m;
    vector<string> grid(n);
    for(int i = 0; i < n; i ++) cin >> grid[i];

    vector<vector<int>> dp(n, vector<int>(m, INF));
    vector<int> colMin(m, INF);
    vector<int> diagMin(n + m, INF); // 对角线索引范围 [-(m-1), n-1],偏移 m 使之非负
    int offset = m;

    for(int i = 0; i < n; i ++) {
        int rowMin = INF; // 当前行连续段最小 dp
        for(int j = 0; j < m; j++) {
            if(grid[i][j] == '*') {// 障碍切断连续段,重置相关最小值
                rowMin = INF;
                colMin[j] = INF;
                diagMin[i - j + offset] = INF;
                continue;
            }

            if(i == 0 && j == 0) dp[i][j] = 0;
            else {
                int best = min({rowMin, colMin[j], diagMin[i - j + offset]});
                dp[i][j] = (best == INF ? INF : best + 1);
            }
        
            if(dp[i][j] < INF) {// 如果当前格子可达,更新三个最小值
                rowMin = min(rowMin, dp[i][j]);
                colMin[j] = min(colMin[j], dp[i][j]);
                diagMin[i - j + offset] = min(diagMin[i - j + offset], dp[i][j]);
            }
        }
    }

    int ans = dp[n - 1][m - 1];
    cout << (ans == INF ? -1 : ans) << '\n';
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    solve();
    return 0;
}

双端队列优化,(0-1)BFS做法

#include <bits/stdc++.h>
using namespace std;
const int N = 2000, M = 2000;
const uint16_t INF = 0xFFFF;
uint16_t dist[N][M][3];
int dx[3] = {0, 1, 1};
int dy[3] = {1, 1, 0};

void solve() {
    int n, m; cin >> n >> m;
    vector<string> grid(n);
    for (int i = 0; i < n; i ++) cin >> grid[i];

    memset(dist, 0xFF, sizeof(dist));
    deque<tuple<int, int, int>> dq;
    for(int d = 0; d < 3; d ++) {
        dist[0][0][d] = 0;
        dq.push_back({0, 0, d});
    }

    while(!dq.empty()) {
        auto [x, y, dir] = dq.front(); dq.pop_front();

        uint16_t cur = dist[x][y][dir];
        for(int nd = 0; nd < 3; ++nd) {
            int nx = x + dx[nd], ny = y + dy[nd];
            if(nx >= n || ny >= m || grid[nx][ny] == '*') continue;

            uint16_t cost;
            if(x == 0 && y == 0) cost = 1;
            else cost = (nd == dir) ? 0 : 1;
            uint16_t ndist = cur + cost;
            if(ndist < dist[nx][ny][nd]) {
                dist[nx][ny][nd] = ndist;
                if(cost == 0) dq.push_front({nx, ny, nd});
                else dq.push_back({nx, ny, nd});
            }
        }
    }

    uint16_t ans = INF;
    for(int d = 0; d < 3; d ++) ans = min(ans, dist[n-1][m-1][d]);
    if(ans == INF) cout << -1 << '\n';
    else cout << (int)ans << '\n';
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr);
    solve();
    return 0;
}
posted @ 2026-09-08 19:41  thenrify  阅读(8)  评论(0)    收藏  举报