已知一个 NxN 的国际象棋棋盘,棋盘的行号和列号都是从 0 开始。即最左上角的格子记为 (0, 0),最右下角的记为 (N-1, N-1)。
现有一个 “马”(也译作 “骑士”)位于 (r, c) ,并打算进行 K 次移动。
如下图所示,国际象棋的 “马” 每一步先沿水平或垂直方向移动 2 个格子,然后向与之相垂直的方向再移动 1 个格子,共有 8 个可选的位置。
现在 “马” 每一步都从可选的位置(包括棋盘外部的)中独立随机地选择一个进行移动,直到移动了 K 次或跳到了棋盘外面。
求移动结束后,“马” 仍留在棋盘上的概率。
示例:
输入: 3, 2, 0, 0
输出: 0.0625
解释:
输入的数据依次为 N, K, r, c
第 1 步时,有且只有 2 种走法令 “马” 可以留在棋盘上(跳到(1,2)或(2,1))。对于以上的两种情况,各自在第2步均有且只有2种走法令 “马” 仍然留在棋盘上。
所以 “马” 在结束后仍在棋盘上的概率为 0.0625。
注意:
N 的取值范围为 [1, 25]
K 的取值范围为 [0, 100]
开始时,“马” 总是位于棋盘上
分析:
此题我一开始想的方法是利用搜索,找到“马”所有可能跳到的位置,然后判断哪些位置是合法的,借此得到概率。但是感觉过于麻烦。
在题解中我看到了利用动态规划的方法,首先官方题解中的方法用的是三维数组,但是我在该题解中看到了开辟两个二维dp数组的方法。实质上这两种方法是相同的,不过个人觉得第二种方法更易于理解,所以采用了此方法。
我们尝试统计在“马”走了k步之后,仍留在棋盘中各个位置的概率,将这些概率相加,即可得到我们的最终结果。为此我们开辟一个名为cur的二维数组,表示在当前这一步,马留在棋盘中各个位置的概率;然后开辟一个名为nxt的二维数组,表示下一步各个位置的概率。那么一直进行到第k步即可。
那么我们的初始状态即cur[r][c] = 1,既然“马”有八个方向,它到各个位置的概率都是相等的,所以是1/8,这样我们很轻易就能得到第一步各个位置的概率。同时我们的状态转移方程也就呼之欲出了:nxt[x][y] += cur[i][j] / 8,其中(i, j)是移动前的位置,(x, y)是移动后的位置。
该题与其他动态规划题目不同的地方在于状态转移方程是两个数组之间的转移。当然也可以像官方题解一样,在一个三维数组之间进行状态转移。该类题目还需要多多积累。
class Solution {
public:
double knightProbability(int n, int k, int row, int column) {
int dx[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
int dy[8] = {1, 2, 2, 1, -1, -2, -2, -1};
double **cur = new double *[n];
for(int i = 0; i < n; i++){
cur[i] = new double[n];
memset(cur[i], 0, n * sizeof(double));
}
//开辟二维数组及初始化
cur[row][column] = 1;
for(int K = 0; K < k; K++){
double **nxt = new double *[n];
for(int i = 0; i < n; i++){
nxt[i] = new double[n];
memset(nxt[i], 0, n * sizeof(double));
}
//开辟二维数组及初始化
for(int x = 0; x < n; x++){
for(int y = 0; y < n; y++){
for(int i = 0; i < 8; i++){
int newX = x + dx[i], newY = y + dy[i];
if(newX >= 0 && newX < n && newY >= 0 && newY < n)
nxt[newX][newY] += cur[x][y] / 8.0;
}
}
}
cur = nxt;
}
double res = 0.0;
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
res += cur[i][j];
return res;
}
};