今天leetcode日常打卡,题目是leetcode63.不同路径Ⅱ。考虑到这道题是62.不同路径 的进阶版,就先把62做了,再做的63。特此记录做这两道题的过程和一些自己的做题总结。
62.不同路径
其实,第一眼看到这个想到的就是DFS,因为这道题每一步(机器人移动)都需要做出选择,每做出一次选择有一个分支。如果做出选择A,得到结果不是我想要的,那么我就返回到做出选择A之前的那个时间点。做出另一个选择B,看是否能得到我想要的结果。如同走迷宫一样,能走通,就继续走,走不通就要原路返回,再走另外一条路。
class Solution {
private:
int count=0;
public:
int uniquePaths(int m, int n) {//m是列,n是行
helper(m,n,0,0);
return count;
}
void helper(int m,int n,int cx,int cy){
//如果做出的选择能最终走到终点,那么证明这个选择是对的,计数+1,同时回到做出选择的那个点
//以便做出另一个选择,判断另一个选择是不是符合要求
if((cx==m-1)&&(cy==n-1)){
count++;
return;//回到做出选择的那个点
}
if(cx>m-1) return;//如果越界了,意味着这条路明显走不通,立即返回
//如同走迷宫时,碰到了死胡同,那肯定得原路返回,走另外一条路
if(cy>n-1) return;
helper(m,n,cx+1,cy);//向下移动
helper(m,n,cx,cy+1);//向右移动
}
};
跑了几个简单的例子,都过了,以为没问题。结果已提交,直接超时。再参考了评论区讲解,发现了超时是因为做了很多已经做过的工作。为了更好的理解,超时的原因,特意画了下面的图方便理解(其实题解里有人做了解释,但是树状的图加上单纯的数字有点不够直观)。
下面是加了备忘录的DFS
class Solution {
public:
int uniquePaths(int m, int n) {//m是列,n是行
vector<vector<long long>> mem(m,vector<long long>(n,-1));
return helper(m,n,0,0,mem);;
}
int helper(int m,int n,int cx,int cy,vector<vector<long long>>& mem){
if((cx==m-1)&&(cy==n-1)){
return 1;
}
if(cx>m-1) return 0;
if(cy>n-1) return 0;
//发现当前位置被打了标记,就不需要走这条路,直接返回之前记录的结果
if(mem[cx][cy]!=-1){
return mem[cx][cy];
}
long long total=0;//用long long,是因为m、n太大时,导致结果超出了int范围
total+=helper(m,n,cx+1,cy,mem);//向下移动
total+=helper(m,n,cx,cy+1,mem);//向右移动
//打上标记,方便后面做参考
mem[cx][cy]=total;
return total;
}
};
这道题还可以采用动态规划的解法
动态规划的本质就是填表格,尤其是对于这种二维动态规划,根据填表格能够更加清楚地退出状态转移
对于当前位置(i,j)来说,机器人可能是从上边过来的,也可能是左边过来的。
根据问题,定义状态dp[i][j],表示到达位置(i,j)的路径数
那么状态转移方程可以很容易得到,dp[ i ][ j ]=dp[ i-1 ][ j ]+dp[ i ][ j-1 ]
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(n,vector<int>(m,0));
//初始化第一行
//对于第一行来说,每一个位置只能从左边移动过来,只有一条路
for(int i=0;i<m;i++){
dp[0][i]=1;
}
//初始化第一列
//对于第一列来说,每一个位置只能从上边移动过来,只有一条路
for(int i=0;i<n;i++){
dp[i][0]=1;
}
for(int i=1;i<n;i++){
for(int j=1;j<m;j++){
dp[i][j]=dp[i][j-1]+dp[i-1][j];
}
}
return dp[n-1][m-1];
}
};
63.不同路径Ⅱ
这道题多了一个限制条件,就是障碍物是否存在。根据62的题解,同样可以用记忆化的DFS和动态规划求解
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int rows=obstacleGrid.size();
if(rows==0) return 0;
int cols=obstacleGrid[0].size();
if(obstacleGrid[0][0]==1) return 0;
vector<vector<int>> mem(rows,vector<int>(cols,-1));
return helper(obstacleGrid,rows,cols,0,0,mem);;
}
int helper(vector<vector<int>>& obstacleGrid,int rows,int cols,int row,int col,vector<vector<int>>& mem){
//在到达目的地的基础上,目的地必须不存在障碍物
if((row==rows-1)&&(col==cols-1)&&(obstacleGrid[row][col]==0)){
return 1;
}
//在越界的基础上,如果存在障碍物代表这条路不同
if(row>=rows||col>=cols||(obstacleGrid[row][col]==1)) return 0;
//调用之前记录的结果
if(mem[row][col]!=-1){
return mem[row][col];
}
int total=0;
total+=helper(obstacleGrid,rows,cols,row+1,col,mem);
total+=helper(obstacleGrid,rows,cols,row,col+1,mem);
mem[row][col]=total;
return total;
}
};
动态规划的解法主要注意初始化和状态转移
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int rows=obstacleGrid.size();
if(rows<1) return 0;
int cols=obstacleGrid[0].size();
if(obstacleGrid[0][0]==1) return 0;//路口就是障碍物,直接返回0(这个是真的坑)
//全部初始化为1,即默认对于每个位置而言,都至少有一条路径可以到达
vector<vector<int>> dp(rows,vector<int>(cols,1));
//第一列
for(int i=1;i<rows;i++){
//当前位置有障碍物,则不可能有路径可以抵达当前位置
//或者之前的状态为0,意味着前一个位置无法抵达,那么当前位置肯定也无法抵达
if((obstacleGrid[i][0]==1)||(dp[i-1][0]==0)) dp[i][0]=0;
}
//第一行
for(int i=1;i<cols;i++){
if((obstacleGrid[0][i]==1)||(dp[0][i-1]==0)) dp[0][i]=0;
}
for(int i=1;i<rows;i++){
for(int j=1;j<cols;j++){
//如果当前位置本身就存在障碍物,那么无论如何都办法到达当前位置
if(obstacleGrid[i][j]==1) dp[i][j]=0;
//如果当前位置没有障碍物,那么状态取决于上边和左边
else dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[rows-1][cols-1];
}
};