688. 骑士在棋盘上的概率题解
题目来源:688. 骑士在棋盘上的概率
2022.02.17 每日一题
每日一题专栏地址:LeetCode 每日一题题解更新中
题目大意是判断国际象棋中的骑士从初始坐标 ( r o w , c o l u m n ) (row,column) (row,column)开始在 k k k步之后能不能走出棋盘
本题计算概率的方法是:分子是你到K步为止,总共有多少种情况,分母是总共的步数,K为1是8次,K为2是64次。即为 p o w ( 8 , k ) pow(8,k) pow(8,k)
我首先想到的方法是直接进行dfs搜索,但是最后直接 TLE 了,( dfs 超时代码放在文章最后了)
法一:记忆深搜
然后通过分析发现,在递归过程中会有很多重复的步骤
例如:首次选择了 ( 1 , 2 ) (1,2) (1,2)方向,下一跳选择了 ( − 1 , − 2 ) (-1,-2) (−1,−2)方向,就回到了原地,就造成了重复,因此可以使用一个数组来记录已经走过的点,来减少时间
class Solution {
public:
int n;
vector<vector<int>> dirs = {{-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}, {1, 2}, {2, 1}, {2, -1}, {1, -2}};
vector<vector<vector<double>>> dp;
double knightProbability(int n, int k, int row, int column) {
// 创建一个缓存数组,用来存储已经递归过的值
dp = vector(n, vector(n, vector(k + 1, 0.0)));
this->n = n;
return dfs(row, column, k);
}
double dfs(int i, int j, int times) {
// 如果骑士已经不在棋盘上,则骑士在棋盘上面的概率为 0
if (i < 0 || j < 0 || i >= n || j >= n)
return 0;
// 这是骑士一定在棋盘上,但是移动次数已经没有了,、
// 此时骑士一定在棋盘上,返回 1
if (times == 0)
return 1;
// 如果此时的点已经被递归过了,则可以直接返回原来递归的值,免于重复递归,浪费时间
if (dp[i][j][times] != 0.0)
return dp[i][j][times];
// 创建一个 temp 记录此时递归的值
double temp = 0.0;
for (vector<int> dir: dirs) {
// 获取下一跳的坐标
int x = i + dir[0], y = j + dir[1];
// 进行下一次递归,并且得到对应的结果
temp += dfs(x, y, times - 1) / 8;
}
// 将temp的值赋给 cache[i][j][times]
dp[i][j][times] = temp;
return temp;
}
};
class Solution {
int n;
int[][] dirs = {{-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}, {1, 2}, {2, 1}, {2, -1}, {1, -2}};
double[][][] dp;
public double knightProbability(int n, int k, int row, int column) {
// 创建一个缓存数组,用来存储已经递归过的值
dp = new double[n][n][k + 1];
this.n = n;
return dfs(row, column, k);
}
double dfs(int i, int j, int times) {
// 如果骑士已经不在棋盘上,则骑士在棋盘上面的概率为 0
if (i < 0 || j < 0 || i >= n || j >= n) return 0;
// 这是骑士一定在棋盘上,但是移动次数已经没有了,、
// 此时骑士一定在棋盘上,返回 1
if (times == 0) return 1;
// 如果此时的点已经被递归过了,则可以直接返回原来递归的值,免于重复递归,浪费时间
if (dp[i][j][times] != 0.0) return dp[i][j][times];
// 创建一个 temp 记录此时递归的值
double temp = 0.0;
for (int[] dir : dirs) {
// 获取下一跳的坐标
int x = i + dir[0], y = j + dir[1];
// 进行下一次递归,并且得到对应的结果
temp += dfs(x, y, times - 1) / 8;
}
// 将temp的值赋给 cache[i][j][times]
dp[i][j][times] = temp;
return temp;
}
}
- 时间复杂度 O ( k n 2 ) O(kn^2) O(kn2)
- 空间复杂度 O ( k n 2 ) O(kn^2) O(kn2)
法二:动态规划
有了记忆化搜索,那也少不了dp了。
想知道第 k 步有多少的概率,可以从第 k - 1 步开始推算
根据本题目,我们知道一个骑士有 8 个方向的选择,每个方向的选择概率是相等的,因此每个方向被选中的概率都是 1 8 \frac{1}{8} 81,因此第 k 步就是 8 个方向的每个 1 8 \frac{1}{8} 81的概率进行相加
class Solution {
public:
int n;
vector<vector<int>> dirs = {{-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}, {1, 2}, {2, 1}, {2, -1}, {1, -2}};
double knightProbability(int n, int k, int row, int column) {
// 给动态规划开辟空间
vector<vector<vector<double>>> dp = vector(k + 1, vector(n, vector(n, 0.0)));
for (int l = 0; l <= k; l++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// 如果 l (k值)等于 0 ,其概率为 1
if (l == 0) {
dp[l][i][j] = 1;
} else {
for (vector<int> dir: dirs) {
int x = i + dir[0], y = j + dir[1];
if (x >= 0 && y >= 0 && x < n && y < n)
dp[l][i][j] += dp[l - 1][x][y] / 8;
}
}
}
}
}
return dp[k][row][column];
}
};
class Solution {
int[][] dirs = {{-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}, {1, 2}, {2, 1}, {2, -1}, {1, -2}};
public double knightProbability(int n, int k, int row, int column) {
// 给动态规划开辟空间
double[][][] dp = new double[k + 1][n][n];
for (int l = 0; l <= k; l++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// 如果 l (k值)等于 0 ,其概率为 1
if (l == 0) {
dp[l][i][j] = 1;
} else {
for (int[] dir : dirs) {
int x = i + dir[0], y = j + dir[1];
if (x >= 0 && y >= 0 && x < n && y < n) dp[l][i][j] += dp[l - 1][x][y] / 8;
}
}
}
}
}
return dp[k][row][column];
}
}
- 时间复杂度 O ( k n 2 ) O(kn^2) O(kn2)
- 空间复杂度 O ( k n 2 ) O(kn^2) O(kn2)
以上方法都要比直接模拟的复杂度 O ( 8 k ) O(8^k) O(8k)小的多
以下是 d f s dfs dfs的超时代码
class Solution {
public:
// success 代表最后跳出棋盘的个数
// all - success 代表最后留在棋盘上面的个数
int sucess = 0, all = 0;
int n;
// 八个方向的访问坐标
vector<vector<int>> dirs = {{-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}, {1, 2}, {2, 1}, {2, -1}, {1, -2}};
double knightProbability(int n, int k, int row, int column) {
this->n = n;
dfs(row, column, k);
return (all - sucess) * 1.0 / pow(8, k);
}
void dfs(int i, int j, int times) {
// 如果不在棋盘范围内,则证明骑士已经跳出了棋盘
if (i < 0 || j < 0 || i >= n || j >= n) {
// 对应值加一,退出递归
sucess++;
all++;
return;
}
// 如果 times 的次数小于 1 证明是最后一次移动,同时骑士还在棋盘上
if (times < 1) {
all++;
return;
}
// 遍历方向数组,8 个方向进行下一次递归操作
for (vector<int> dir: dirs) {
int x = i + dir[0], y = j + dir[1];
dfs(x, y, times - 1);
}
}
};
class Solution {
// success 代表最后跳出棋盘的个数
// all - success 代表最后留在棋盘上面的个数
int sucess = 0, all = 0;
int n;
// 八个方向的访问坐标
int[][] dirs = {{-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}, {1, 2}, {2, 1}, {2, -1}, {1, -2}};
double knightProbability(int n, int k, int row, int column) {
this.n = n;
dfs(row, column, k);
return (all - sucess) * 1.0 / Math.pow(8, k);
}
void dfs(int i, int j, int times) {
// 如果不在棋盘范围内,则证明骑士已经跳出了棋盘
if (i < 0 || j < 0 || i >= n || j >= n) {
// 对应值加一,退出递归
sucess++;
all++;
return;
}
// 如果 times 的次数小于 1 证明是最后一次移动,同时骑士还在棋盘上
if (times < 1) {
all++;
return;
}
// 遍历方向数组,8 个方向进行下一次递归操作
for (int[] dir : dirs) {
int x = i + dir[0], y = j + dir[1];
dfs(x, y, times - 1);
}
}
}