《程序员面试金典(第六版)》面试题 08.02. 迷路的机器人(动态规划,回溯法)
Posted 阿宋同学
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《程序员面试金典(第六版)》面试题 08.02. 迷路的机器人(动态规划,回溯法)相关的知识,希望对你有一定的参考价值。
题目解析
设想有个机器人坐在一个网格的左上角,网格 r 行 c 列。机器人只能向下或向右移动,但不能走到一些被禁止的网格(有障碍物)。设计一种算法,寻找机器人从左上角移动到右下角的路径。
网格中的障碍物和空位置分别用 1 和 0 来表示。
返回一条可行的路径,路径由经过的网格的行号和列号组成。左上角为 0 行 0 列。如果没有可行的路径,返回空数组。
示例 1:
- 输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: [[0,0],[0,1],[0,2],[1,2],[2,2]]
解释:
- 输入中标粗的位置即为输出表示的路径,即
0行0列(左上角) -> 0行1列 -> 0行2列 -> 1行2列 -> 2行2列(右下角)
说明:r 和 c 的值均不超过 100。
解题分析与代码
这道题是一道经典的网格迷宫问题,我们可以利用回溯法去解决。
方法一:回溯法
-
这道题我们只需要找到一条可以返回的路径就完全ok了。所以我们先要想想我们需要在什么地方去返回。首先,你碰到了石头,就要返回把。其次,超出了边界,是不是也要返回?到达了终点是不是更要返回?
-
还有,我们需要一个二维数组去确定自己走过的路径,走过的路径不可能再走一遍,所以也要返回。我们也需要用一个标志位,代表我已经找到了一条路径,找到了就把标志位变位,这样再遇到其他路径的时候就直接不走,也算是一种返回吧。这个其实就是回溯算法中的剪枝思想。
-
把所有的边界条件想清楚后,这道题,也就不是一道难题了,十分好理解的。我们通过不断地尝试新的路径,并通过回溯的方式来回到之前的状态。就完全可以解出这道题了。
具体的代码如下:
class Solution
public:
vector<vector<int>> result; //最终的结果集
vector<vector<int>> pathWithObstacles(vector<vector<int>>& obstacleGrid)
int row = obstacleGrid.size();
int cal = obstacleGrid[0].size();
if(obstacleGrid[0][0] == 1 || obstacleGrid[row-1][cal-1] == 1) return ;
vector<vector<bool>> flag (row,vector<bool>(cal,false)); //记录走过的路径
bool path = false;//找到一条路径就可以返回了
backtracking(obstacleGrid,flag,row,cal,0,0,path);
return result;
void backtracking(vector<vector<int>>& obstacleGrid,vector<vector<bool>>& flag, int row,int cal,int i, int j,bool& path)
if(i >= row || j >= cal || obstacleGrid[i][j] == 1 || path || flag[i][j] )
return ;
if(i == row -1 && j == cal -1)
result.push_back(i,j);
path = true;
return;
flag[i][j] = true;
result.push_back(i,j);
backtracking(obstacleGrid,flag,row,cal,i+1,j,path);
backtracking(obstacleGrid,flag,row,cal,i,j+1,path);
if(!path) result.pop_back();
;
复杂度分析
当obstacleGrid的大小为m行n列时,该代码的时间复杂度和空间复杂度如下:
-
时间复杂度:O(mn * 2^(m+n) )。其中,m*n是obstacleGrid的大小,2^(m+n)是所有可能的路径数量。在backtracking函数中,每个格子有两个方向可以探索(向下和向右),因此,每个格子的搜索分支最多有两个。对于每一个分支,都需要遍历整个obstacleGrid,并递归探索下一个格子。因此,时间复杂度是O(mn * 2^(m+n))。
-
空间复杂度:O(mn + 2^(m+n) )。其中,mn是flag数组的大小,2(m+n)是结果数组result的大小。在backtracking函数中,需要使用一个二维的flag数组来记录已经访问过的格子,因此,空间复杂度是O(mn)。同时,结果数组result也需要记录所有的路径,因此空间复杂度也是O(2(m+n))。因此,总的空间复杂度是O(mn + 2^(m+n))。
需要注意的是,由于时间复杂度和空间复杂度都与obstacleGrid的大小呈指数关系,因此,当obstacleGrid的大小比较大时,该算法的时间复杂度和空间复杂度都会变得非常高,导致算法的性能下降。因此,需要对算法进行优化,以降低时间复杂度和空间复杂度。
方法二:动态规划法
用动态规划的方法与用回溯法的方法,在某些地方是一样的。比如第一开始的条件判断。若起点终点有障碍物,我们就直接返回。
-
不一样的点在于,回溯法,是一步一步的去试探,这道道路能不能走到终点,如果走不到,我们就原路返回,换条路再走走看。
-
动态规划法,是检测有多少条路径能够到达终点,如果大于0,就直接构造路径。如果等于0就返回空数组。
-
而有关动态规划的题,它是有特定的解题步骤的。
我们就用动态规划的五部曲,来去分析一下这道题,我们是怎么做的:
第一步:确定dp数组的下标以及含义
dp[i][j]
:i是纵坐标,j是横坐标,dp[i][j]代表的是到达坐标为(i,j)
这个格子的路径数量
第二步:确定(递推)推导公式
-
由题意可知,机器人只能向下或向右移动,因此到达坐标(i,j)的格子的路径数量只可能是从(i-1,j)或(i,j-1)格子的状态转移而来。因此,我们可以得到递推公式:
dp[i][j] = dp[i-1][j] + dp[i][j-1];
-
特别需要注意的是
,在obstacleGrid[i][j]的值为1
的情况下(也就是这个格子上有障碍物),dp[i][j]的值要为0(也就是表示没有一条路径可以到达这个格子)。所以,最终的递推公式应该长这样:dp[i][j] = obstacleGrid[i][j] == 1 ? 0 : dp[i-1][j] + dp[i][j-1];
第三步:初始化dp数组
-
由于机器人只能选择向下走或向右走,除第一行与第一列外的格子,需要由第一行与第一列的dp值去推导出来,所以我们要去初始化dp数组的第一行与第一列。因为现在起点是没有石头的,
所以dp[0][0] = 1
。 -
在推导的时候需要注意的是,dp值由两个要素决定,一个是你obstacleGrid数组中对应的值是是否为0(为0代表这个格子没有障碍物),第二个是你前一个dp值是否为1(为1就是有路径可以到达这个格子),这两个条件缺一不可。
-
知道了这些前提条件后,我们就可以用for循环来初始化第一行与第一列啦。具体代码如下:
for(int i = 1;i < row; ++i) if(obstacleGrid[i][0] == 0 && dp[i-1][0] == 1) dp[i][j] = 1
for(int j = 1;j < cal; ++i) if(obstacleGrid[0][j] == 0 && dp[0][j-1] == 1) dp[i][j] = 1
第四步,确定变量顺序
- 由于机器人是从右上角走到左下角,所以我们遍历的时候,可以先由左到右,然后再由上至下。
具体的代码长这样:
for(int i = 1; i < row; ++i)
for(int j = 1; j < cal; ++j)
if(obstacleGrid[i][j] == 0)
dp[i][j] = dp[i-1][j]%10000000 + dp[i][j-1]%10000000;
- 由于本题给的案例样本太大,所以必须给它取模,否则会报错。
第五步,举例推导dp数组
至此,动态规划的部分结束了。动态规划部分最重要的是推断出是否有路径能从左上角到达右下角。那如果dp[i][j] = 0,则其实就代表没有路径可以到达终点,我们返回空数组就可以了。
构造路径
-
那其他情况,就不管有几条路径可以到达终点,我们自己构造一条路径不就好了嘛?
-
从终点往起点去推,先设置两个变量,将终点的坐标表示出来:
int i = row - 1, j = cal - 1;
- 之后我们用while循环,如果说dp[i][j] > 0 ,就代表着这个坐标我们可以通过,就直接加入结果集就好,然后
--i ,--j
。那如果i先减到0了,那就代表我们已经回到了第一行,该--j
了。如果j先减到0了,那就代表我们已经回到了第一列,该--i
了。当i与j都小于0时,就代表着循环该结束了
。由此,我们就构建出了一条由终点指向起点的路径。
-
我们再reverse一下,不就得到了一条由起点指向终点的路径嘛?
-
但我们不能从起点往终点去构建路径,因为我发现,如果这么去构建路径了话,会出现遇到石头,但它不停下的情况。
整个题解的具体代码如下:
class Solution
public:
vector<vector<int>> result; //最终的结果集
vector<vector<int>> pathWithObstacles(vector<vector<int>>& obstacleGrid)
int row = obstacleGrid.size();
int cal = obstacleGrid[0].size();
if(obstacleGrid[0][0] == 1 || obstacleGrid[row-1][cal-1] == 1) return ;
vector<vector<long long>> dp(row,vector<long long>(cal,0)); // 记录到达每个格子的路径数量
dp[0][0] = 1;
// 初始化第一行和第一列
for(int i = 1; i < row; i++)
if(obstacleGrid[i][0] == 0 && dp[i-1][0] == 1) dp[i][0] = 1;
for(int j = 1; j < cal; j++)
if(obstacleGrid[0][j] == 0 && dp[0][j-1] == 1) dp[0][j] = 1;
// 动态规划
for(int i = 1; i < row; i++)
for(int j = 1; j < cal; j++)
if(obstacleGrid[i][j] == 0)
dp[i][j] = dp[i-1][j]%10000000 + dp[i][j-1]%10000000;
// 如果到达终点的路径数量为0,则无法到达终点
if(dp[row-1][cal-1] == 0) return ;
// 构造路径
int i = row - 1, j = cal - 1;
while(i >= 0 && j >= 0)
result.push_back(i,j);
if(i == 0) j--;
else if(j == 0) i--;
else
if(dp[i-1][j] > 0) i--;
else j--;
reverse(result.begin(), result.end());
return result;
;
复杂度分析
时间复杂度分析:
- 第1个 for 循环的时间复杂度为 O(row),第2个 for 循环的时间复杂度为 O(cal),第3个嵌套循环的时间复杂度为 O(row * cal)。因此,总的时间复杂度为 O(row * cal)。
空间复杂度分析:
- 使用了一个大小为 rowcal 的 dp 数组,因此空间复杂度为 O(row * cal)。除此之外,使用了一些辅助变量,但其空间占用很小,因此可以忽略不计。
因此,该算法的时间复杂度为 O(row * cal),空间复杂度为 O(row * cal)。
比起回溯法,动态规划的时间复杂度与空间复杂度已经降了很低很低了。
总结
这道题是一道用来练习动态规划与回溯法的好题。你可以仔细去揣摩里面的每一个步骤,它为什么要这么做?会导致什么后果。做完这道题我受益良多。
程序员面试金典-面试题 16.08. 整数的英语表示
题目:
给定一个整数,打印该整数的英文描述。
示例 1:
输入: 123
输出: "One Hundred Twenty Three"
示例 2:
输入: 12345
输出: "Twelve Thousand Three Hundred Forty Five"
示例 3:
输入: 1234567
输出: "One Million Two Hundred Thirty Four Thousand Five Hundred Sixty Seven"
示例 4:
输入: 1234567891
输出: "One Billion Two Hundred Thirty Four Million Five Hundred Sixty Seven Thousand Eight Hundred Ninety One"
分析:
没想到什么太好的办法,就是按个千百万十亿统计,再将字符串拼接起来。
程序:
class Solution { public String numberToWords(int num) { if(num == 0) return "Zero"; List<String> res = new ArrayList<>(); int hundred = 0; int thousand = 0; int million = 0; int billion = 0; if(num < 0){ res.add("Negative"); num = -num; } if(num > 0){ hundred = num % 1000; num /= 1000; if(num > 0){ thousand = num % 1000; num /= 1000; if(num > 0){ million = num % 1000; num /= 1000; if(num > 0){ billion = num % 1000; } } } } if(billion > 0){ for(String str:change(billion)) res.add(str); res.add("Billion"); } if(million > 0){ for(String str:change(million)) res.add(str); res.add("Million"); } if(thousand > 0){ for(String str:change(thousand)) res.add(str); res.add("Thousand"); } if(hundred > 0){ for(String str:change(hundred)) res.add(str); } StringBuilder ans = new StringBuilder(); for(String s:res){ if(ans.length() == 0) ans.append(s); else{ ans.append(" " + s); } } return ans.toString(); } private LinkedList<String> change(int num){ LinkedList<String> list = new LinkedList<>(); int n = num % 100; if(0 < n && n < 20){ list.addFirst(num0_19[n]); }else if(n >= 20){ int m = n % 10; int mm = n / 10; if(m > 0) list.addFirst(num0_19[m]); list.addFirst(num0_90[mm]); } if(num >= 100){ list.addFirst("Hundred"); list.addFirst(num0_19[num / 100]); } return list; } private String[] num0_19 = new String[]{ "Zero","One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine","Ten","Eleven","Twelve","Thirteen","Fourteen","Fifteen","Sixteen","Seventeen","Eighteen","Nineteen" }; private String[] num0_90 = new String[]{ "Zero","Ten","Twenty","Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" }; }
以上是关于《程序员面试金典(第六版)》面试题 08.02. 迷路的机器人(动态规划,回溯法)的主要内容,如果未能解决你的问题,请参考以下文章