P1002 过河卒

文章目录

题目信息

字段 内容
题号 P1002
难度 橙题(普及-)
知识点 动态规划、棋盘型DP

题目描述

有一个 n × m 的棋盘(中国象棋棋盘),卒从左上角 (0, 0) 出发,要到达右下角 (n, m)。 棋盘上有一匹马,位置在 (x, y)。象棋马的走法是「日」字,马所在的格子以及马下一步能跳到的 8 个位置都会被控制,卒不能经过这些格子。

求从 (0, 0)(n, m) 共有多少条不同的路径(只能向右或向下走)。

输入格式:四个整数 n, m, x, y,空格分隔。

输出格式:一个整数,表示路径数量。

解题思路

核心思想

分两步走:

  1. 标记禁区:马在 (x, y) 位置,会控制周围的 8 个格子。先把这些格子标记为不可达。
  2. 方向 DP:棋盘上只能向右或向下走,所以每个格子的路径数 = 从上方来的 + 从左方来的,这就是经典的棋盘路径 DP。

马的控制范围

与中国象棋的马走法一致,从 (x, y) 出发共 8 个目标位置:

  • (x-2)
    • (y-1)
    • (y+1)
  • (x+2)
    • (y-1)
    • (y+1)
  • (x-1)
    • (y-2)
    • (y+2)
  • (x+1)
    • (y-2)
    • (y+2)

超出棋盘边界的格子直接忽略。

为什么用动态规划?

因为只能向右或向下走,棋盘形成了一个 DAG(有向无环图)。从左到右、从上到下按顺序遍历,每个格子只会被访问一次,时间复杂度是 O(n×m)。

完整代码

C++

#include <iostream>

using namespace std;

// 马的八个跳跃方向(象棋马走日字)
const int kDx[9] = {0, 1, 2, 2, 1, -1, -2, -2, -1};
const int kDy[9] = {0, 2, 1, -1, -2, -2, -1, 1, 2};

long long dp[30][30];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    long long n, m, x, y;
    cin >> n >> m >> x >> y;

    // dp 数组大小为 (n+1) × (m+1),索引范围 0~n 和 0~m
    // 标记禁区:马所在格及其控制格
    for (int i = 0; i <= 8; i++) {
        int tx = x + kDx[i];
        int ty = y + kDy[i];
        if (tx >= 0 && tx <= n && ty >= 0 && ty <= m) {
            dp[tx][ty] = -1;  // -1 表示不可达
        }
    }

    // 方向 DP:只能从上方或左方到达
    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            if (dp[i][j] != -1) {
                long long from_top = 0;
                long long from_left = 0;
                if (i - 1 >= 0 && dp[i - 1][j] != -1) {
                    from_top = dp[i - 1][j];
                }
                if (j - 1 >= 0 && dp[i][j - 1] != -1) {
                    from_left = dp[i][j - 1];
                }
                dp[i][j] = from_top + from_left;
                if (i == 0 && j == 0) {
                    dp[i][j] = 1;  // 起点有一种方式
                }
            }
        }
    }

    cout << dp[n][m] << "\n";
    return 0;
}

关键代码讲解

1. 禁区标记

const int kDx[9] = {0, 1, 2, 2, 1, -1, -2, -2, -1};
const int kDy[9] = {0, 2, 1, -1, -2, -2, -1, 1, 2};

dx[0]dy[0] 都是 0,表示马所在的自身格子也要标记为禁区。其余 8 个是标准象棋马的跳跃偏移量。

if (tx >= 0 && tx <= n && ty >= 0 && ty <= m) {
    dp[tx][ty] = -1;  // 在棋盘范围内才标记
}

边界检查必不可少,否则数组会越界。

2. 方向 DP 转移

dp[i][j] = (i > 0 ? dp[i-1][j] : 0) + (j > 0 ? dp[i][j-1] : 0);

由于棋盘只能从左和从上两个方向到达,状态转移方程就是上方的路径数加上左方的路径数。

3. 起点的特殊处理

if (i == 0 && j == 0) dp[i][j] = 1;

左上角 (0, 0) 没有上方也没有左方,特殊处理为 1(自己算一种走法)。

复杂度分析

方法 时间复杂度 空间复杂度
棋盘 DP O(n × m) O(n × m)

n, m ≤ 25,复杂度完全足够。

易错点

  1. 索引范围:棋盘大小是 (n+1) × (m+1),循环上界要用 <= n<= m
  2. 马的控制范围包括自身:dx[0]=0, dy[0]=0 不要漏掉,否则起点或终点恰好是马的位置时会算错
  3. 负数索引:边界检查 <= n<= m 不能写成 < n< m
  4. long long:路径数可能很大,答案会超过 int 范围,必须用 long long

其他语言解法

Python

"""P1002 过河卒 - Python 实现"""
from typing import List


def solve() -> None:
    """读取输入,计算从 (0,0) 到 (n,m) 的路径数"""
    n, m, x, y = map(int, input().split())

    # dp[i][j]: 到达 (i, j) 的路径数,-1 表示不可达
    dp: List[List[int]] = [[0] * (m + 1) for _ in range(n + 1)]

    # 马的八个跳跃方向
    k_dx = [0, 1, 2, 2, 1, -1, -2, -2, -1]
    k_dy = [0, 2, 1, -1, -2, -2, -1, 1, 2]

    # 标记禁区
    for i in range(9):
        tx = x + k_dx[i]
        ty = y + k_dy[i]
        if 0 <= tx <= n and 0 <= ty <= m:
            dp[tx][ty] = -1

    # 方向 DP
    for i in range(n + 1):
        for j in range(m + 1):
            if dp[i][j] != -1:
                if i == 0 and j == 0:
                    dp[i][j] = 1
                else:
                    from_top = dp[i - 1][j] if i > 0 and dp[i - 1][j] != -1 else 0
                    from_left = dp[i][j - 1] if j > 0 and dp[i][j - 1] != -1 else 0
                    dp[i][j] = from_top + from_left

    print(dp[n][m])


if __name__ == "__main__":
    solve()

Java

import java.io.IOException;

public class Main {
    // 洛谷强制要求类名为 Main,禁止使用 Scanner,用 BufferedInputStream
    private static final byte[] buffer = new byte[1 << 16];
    private static int ptr = 0;
    private static int len = 0;

    private static int read() throws IOException {
        if (ptr >= len) {
            len = System.in.read(buffer);
            ptr = 0;
            if (len <= 0) return -1;
        }
        return buffer[ptr++];
    }

    private static final int[] kDx = {0, 1, 2, 2, 1, -1, -2, -2, -1};
    private static final int[] kDy = {0, 2, 1, -1, -2, -2, -1, 1, 2};

    public static void main(String[] args) throws IOException {
        long n = readInt();
        long m = readInt();
        long x = readInt();
        long y = readInt();

        long[][] dp = new long[(int) (n + 1)][(int) (m + 1)];

        // 标记禁区
        for (int i = 0; i <= 8; i++) {
            int tx = (int) (x + kDx[i]);
            int ty = (int) (y + kDy[i]);
            if (tx >= 0 && tx <= n && ty >= 0 && ty <= m) {
                dp[tx][ty] = -1;
            }
        }

        // 方向 DP
        for (int i = 0; i <= n; i++) {
            for (int j = 0; j <= m; j++) {
                if (dp[i][j] != -1) {
                    if (i == 0 && j == 0) {
                        dp[i][j] = 1;
                    } else {
                        long fromTop = (i > 0 && dp[i - 1][j] != -1) ? dp[i - 1][j] : 0;
                        long fromLeft = (j > 0 && dp[i][j - 1] != -1) ? dp[i][j - 1] : 0;
                        dp[i][j] = fromTop + fromLeft;
                    }
                }
            }
        }

        System.out.println(dp[(int) n][(int) m]);
    }

    private static long readInt() throws IOException {
        int c;
        do {
            c = read();
        } while (c <= ' ' && c != -1);
        boolean negative = false;
        if (c == '-') {
            negative = true;
            c = read();
        }
        long val = 0;
        while (c > ' ') {
            val = val * 10 + (c - '0');
            c = read();
        }
        return negative ? -val : val;
    }
}

Go

package main

import (
    "bufio"
    "fmt"
    "os"
)

var (
    kDx = [9]int{0, 1, 2, 2, 1, -1, -2, -2, -1}
    kDy = [9]int{0, 2, 1, -1, -2, -2, -1, 1, 2}
)

func main() {
    in := bufio.NewReader(os.Stdin)

    var n, m, x, y int
    fmt.Fscan(in, &n, &m, &x, &y)

    dp := make([][]int, n+1)
    for i := 0; i <= n; i++ {
        dp[i] = make([]int, m+1)
    }

    // 标记禁区
    for i := 0; i <= 8; i++ {
        tx := x + kDx[i]
        ty := y + kDy[i]
        if tx >= 0 && tx <= n && ty >= 0 && ty <= m {
            dp[tx][ty] = -1
        }
    }

    // 方向 DP
    for i := 0; i <= n; i++ {
        for j := 0; j <= m; j++ {
            if dp[i][j] != -1 {
                if i == 0 && j == 0 {
                    dp[i][j] = 1
                } else {
                    fromTop := 0
                    fromLeft := 0
                    if i > 0 && dp[i-1][j] != -1 {
                        fromTop = dp[i-1][j]
                    }
                    if j > 0 && dp[i][j-1] != -1 {
                        fromLeft = dp[i][j-1]
                    }
                    dp[i][j] = fromTop + fromLeft
                }
            }
        }
    }

    fmt.Println(dp[n][m])
}

JavaScript

'use strict';

/**
 * 马的八个跳跃方向(象棋马走日字)
 * @type {number[]}
 */
const kDx = [0, 1, 2, 2, 1, -1, -2, -2, -1];
const kDy = [0, 2, 1, -1, -2, -2, -1, 1, 2];

/**
 * 主函数:从 (0,0) 到 (n,m) 的路径数,排除马的控制点
 */
function main() {
    const input = require('fs').readFileSync('/dev/stdin', 'utf8').trim().split(/\s+/);
    const [n, m, x, y] = input.map(Number);

    /** @type {number[][]} */
    const dp = Array.from({ length: n + 1 }, () => Array(m + 1).fill(0));

    // 标记禁区:马所在格及其控制格
    for (let i = 0; i <= 8; i++) {
        const tx = x + kDx[i];
        const ty = y + kDy[i];
        if (tx >= 0 && tx <= n && ty >= 0 && ty <= m) {
            dp[tx][ty] = -1;
        }
    }

    // 方向 DP:只能从上方或左方到达
    for (let i = 0; i <= n; i++) {
        for (let j = 0; j <= m; j++) {
            if (dp[i][j] !== -1) {
                if (i === 0 && j === 0) {
                    dp[i][j] = 1;
                } else {
                    const fromTop = (i > 0 && dp[i - 1][j] !== -1) ? dp[i - 1][j] : 0;
                    const fromLeft = (j > 0 && dp[i][j - 1] !== -1) ? dp[i][j - 1] : 0;
                    dp[i][j] = fromTop + fromLeft;
                }
            }
        }
    }

    console.log(dp[n][m]);
}

main();