大厂高频经典面试题(46)-迷路的机器人

1. 题目:

设想有个机器人坐在一个网格的左上角,网格r行c列。机器人只能向下或向右移动,但不能走到一些被禁止的网格。设计一种算法,寻找机器人从左上角移动到右下角的路径。

2. 解题思路:

这是一个典型的网格路径搜索问题,可以使用回溯法或动态规划来解决。

下面分别实现这2种方式的代码,和对应的图解流程。

3. 回溯法图解流程:

使用多图来演示回溯法的路径搜索的流程,使用代码里提供的这个网格示例:

网格示例(1表示可通过,0表示禁止):
1 0 1 1
1 1 0 1
0 1 1 0
1 1 1 1

图示1:初始状态

机器人从(0,0)出发,可以选择向右或向下移动

(0,0) → 0 → 1 → 1
 ↓
 1 → 1 → 0 → 1
 ↓
 0 → 1 → 1 → 0
 ↓
 1 → 1 → 1 → 1(终点)

图示2:第一次尝试(向右)

路径:(0,0)→
遇到(0,1)是禁止的(值为0),回溯

图示3:第二次尝试(向下)

路径:(0,0)→(1,0)→
从(1,0)可以向右或向下

图示4:继续向右

路径:(0,0)→(1,0)→(1,1)→
从(1,1)可以向右或向下

图示5:尝试向右

路径:(0,0)→(1,0)→(1,1)→(1,2)→
(1,2)是禁止的,回溯

图示6:尝试向下

路径:(0,0)→(1,0)→(1,1)→(2,1)→
从(2,1)可以向右或向下

图示7:继续向右

路径:(0,0)→(1,0)→(1,1)→(2,1)→(2,2)→
从(2,2)可以向右或向下

图示8:尝试向右

路径:(0,0)→(1,0)→(1,1)→(2,1)→(2,2)→(2,3)→
(2,3)是禁止的,回溯

图示9:尝试向下

路径:(0,0)→(1,0)→(1,1)→(2,1)→(2,2)→(3,2)→
从(3,2)可以向右或向下

图示10:最终路径

成功路径:(0,0)→(1,0)→(1,1)→(2,1)→(2,2)→(3,2)→(3,3)

路径可视化:

 ■ → 0 → 1 → 1
 ↓
 1 → 1 → 0 → 1
     ↓
 0 → 1 → 1 → 0
         ↓
 1 → 1 → 1 → ■

以上的流程展示了回溯算法如何逐步探索可能的路径,遇到障碍时回溯,最终找到从起点到终点的可行路径。

每个步骤都体现了算法的决策过程(向右或向下)和回溯机制。

4. 动态规划图解流程:

使用多图来演示动态规划解决路径问题的完整流程,使用相同的网格示例:

网格示例(1=可通过,0=禁止):
1 0 1 1
1 1 0 1
0 1 1 0
1 1 1 1

图示1:初始化DP表

DP表初始状态:
■ □ □ □
□ □ □ □
□ □ □ □
□ □ □ □

■ 表示起点(0,0)初始化为true(可通过)

图示2:初始化第一列

1
1
0
1
↓ 只能向下走
DP表:
■ □ □ □
■ □ □ □
0 □ □ □
■ □ □ □

图示3:初始化第一行

1 0 1 1 → 只能向右走
DP表:
■ 0 ■ ■
■ □ □ □
0 □ □ □
■ □ □ □

注意(0,1)因为网格禁止变为false

图示4:填充DP表(第2行)

检查(1,1):
上方(0,1)=false,左侧(1,0)=true → dp[1][1] = true

■ 0 ■ ■
■ ■ □ □
0 □ □ □
■ □ □ □

图示5:填充DP表(第2行)

检查(1,2):
上方(0,2)=true,左侧(1,1)=true → 但网格(1,2)=0 → dp[1][2]=false

■ 0 ■ ■
■ ■ 0 □
0 □ □ □
■ □ □ □

图示6:填充DP表(第3行)

检查(2,1):
上方(1,1)=true,左侧(2,0)=false → dp[2][1] = true

■ 0 ■ ■
■ ■ 0 □
0 ■ □ □
■ □ □ □

图示7:完成DP表

最终DP表:
■ 0 ■ ■
■ ■ 0 ■
0 ■ ■ 0
■ ■ ■ ■

右下角(3,3)=true表示存在路径

图示8:路径回溯过程

从终点(3,3)开始回溯:
1. 优先向上:(2,3)=false → 不能向上
2. 向左:(3,2)=true → 移动到(3,2)
3. 向上:(2,2)=true → 移动到(2,2)
4. 向上:(1,2)=false → 向左:(2,1)=true
5. 向上:(1,1)=true → 移动到(1,1)
6. 向上:(0,1)=false → 向左:(1,0)=true
7. 向上:(0,0)=true → 到达起点

图示9:最终路径

逆向路径点:
(3,3)←(3,2)←(2,2)←(2,1)←(1,1)←(1,0)←(0,0)

反转后得到正确路径:
(0,0)→(1,0)→(1,1)→(2,1)→(2,2)→(3,2)→(3,3)

路径可视化

 ■ → 0 → 1 → 1
 ↓
 1 → 1 → 0 → 1
     ↓
 0 → 1 → 1 → 0
         ↓
 1 → 1 → 1 → ■

这个流程展示了动态规划的详细步骤:

  1. 自底向上构建DP表记录可达性
  2. 通过状态转移方程计算每个位置的可达性
  3. 从终点回溯构建实际路径
  4. 最终输出最优路径

整个过程的时间复杂度为O(mn),远优于回溯法的指数级复杂度。

5. 代码完整实现(C++):

#include <algorithm>
#include <iostream>
#include <vector>

using namespace std;

/**
 * 打印路径
 * @param path 存储路径的向量
 */
void printPath(const vector<pair<int, int>>& path) {
    for (size_t i = 0; i < path.size(); ++i) {
        cout << "(" << path[i].first << "," << path[i].second << ")";
        if (i != path.size() - 1) {
            cout << " -> ";
        }
    }
    cout << endl;
}

/**
 * 回溯法:寻找机器人从左上角到右下角的路径
 * @param grid 网格,true表示可通过,false表示禁止
 * @param path 存储找到的路径
 * @param row 当前行
 * @param col 当前列
 * @param r 总行数
 * @param col 总列数
 * @return 是否找到路径
 */
bool findPathWithBacktrack(const vector<vector<bool>>& grid,
                           vector<pair<int, int>>& path,
                           int row,
                           int col,
                           int r,
                           int c) {
    // 越界或遇到禁止网格
    if (row >= r || col >= c || !grid[row][col]) {
        return false;
    }

    // 到达终点
    if (row == r - 1 && col == c - 1) {
        path.emplace_back(row, col);
        return true;
    }

    // 尝试向右移动
    path.emplace_back(row, col);
    if (findPathWithBacktrack(grid, path, row, col + 1, r, c)) {
        return true;
    }

    // 尝试向下移动
    if (findPathWithBacktrack(grid, path, row + 1, col, r, c)) {
        return true;
    }

    // 回溯
    path.pop_back();
    return false;
}

void testBacktrack() {
    // 示例网格,true表示可通过,false表示禁止
    vector<vector<bool>> grid = {{true, false, true, true},
                                 {true, true, false, true},
                                 {false, true, true, false},
                                 {true, true, true, true}};

    int r = grid.size();
    int c = grid[0].size();
    vector<pair<int, int>> path;

    if (findPathWithBacktrack(grid, path, 0, 0, r, c)) {
        cout << "回溯法:找到路径: ";
        printPath(path);
    } else {
        cout << "回溯法:没有可行路径" << endl;
    }
}

/**
 * 动态规划:寻找机器人从左上角到右下角的路径
 * @param grid 网格矩阵,true表示可通过,false表示禁止
 * @return 返回找到的路径,若不存在则返回空路径
 */
vector<pair<int, int>> findPathWithDP(const vector<vector<bool>>& grid) {
    if (grid.empty() || grid[0].empty())
        return {};

    int rows = grid.size();
    int cols = grid[0].size();

    // dp[i][j] 表示从(0,0)到(i,j)是否存在路径
    vector<vector<bool>> dp(rows, vector<bool>(cols, false));

    // 初始化第一列:只能从上往下走
    dp[0][0] = grid[0][0];
    for (int i = 1; i < rows; ++i) {
        dp[i][0] = grid[i][0] && dp[i - 1][0];
    }

    // 初始化第一行:只能从左往右走
    for (int j = 1; j < cols; ++j) {
        dp[0][j] = grid[0][j] && dp[0][j - 1];
    }

    // 填充dp表
    for (int i = 1; i < rows; ++i) {
        for (int j = 1; j < cols; ++j) {
            if (grid[i][j]) {
                dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
            }
        }
    }

    // 如果终点不可达
    if (!dp[rows - 1][cols - 1]) {
        return {};
    }

    // 回溯构建路径
    vector<pair<int, int>> path;
    int i = rows - 1, j = cols - 1;

    while (i > 0 || j > 0) {
        path.emplace_back(i, j);

        // 优先向上回溯(相当于优先向下走)
        if (i > 0 && dp[i - 1][j]) {
            --i;
        }
        // 次优先向左回溯(相当于优先向右走)
        else if (j > 0 && dp[i][j - 1]) {
            --j;
        }
    }

    path.emplace_back(0, 0);            // 添加起点
    reverse(path.begin(), path.end());  // 反转路径顺序

    return path;
}

void testDp() {
    // 示例网格,true表示可通过,false表示禁止
    vector<vector<bool>> grid = {{true, false, true, true},
                                 {true, true, false, true},
                                 {false, true, true, false},
                                 {true, true, true, true}};

    vector<pair<int, int>> path = findPathWithDP(grid);

    if (!path.empty()) {
        cout << "动态规划:找到路径: ";
        printPath(path);
    } else {
        cout << "动态规划:没有可行路径" << endl;
    }
}

int main() {
    testBacktrack();

    testDp();

    return 0;
}

6. 回溯法代码分析:

算法说明:

1)网格表示:

  • 使用二维布尔数组表示网格,true表示可通过,false表示禁止

2)回溯过程:

  • 从起点(0,0)开始
  • 优先尝试向右移动
  • 如果向右不通则尝试向下移动
  • 如果都不通则回溯

3)终止条件:

  • 到达终点(r-1, c-1)
  • 越界或遇到禁止网格

4)时间复杂度:

  • 最坏情况下为O(2^(r+c)),因为每个点有两种选择
  • 可以使用动态规划优化到O(r*c)

5)空间复杂度:

  • 主要取决于路径长度,最坏为O(r+c)

7. 动态规划代码分析:

算法说明:

1)DP表定义:

  • dp[i][j]表示从起点(0,0)到(i,j)是否存在可行路径
  • 值为true表示存在路径,false表示不可达

2)初始化:

  • 起点dp[0][0]直接等于网格值
  • 第一列只能从上往下走:dp[i][0] = grid[i][0] && dp[i-1][0]
  • 第一行只能从左往右走:dp[0][j] = grid[0][j] && dp[0][j-1]

3)状态转移:

  • 对于其他位置:dp[i][j] = grid[i][j] && (dp[i-1][j] || dp[i][j-1])
  • 当前位置可达当且仅当:当前网格可通过,且上方或左方可达

4)路径回溯:

  • 从终点开始,优先向上回溯(相当于优先向下走)
  • 次优先向左回溯(相当于优先向右走)
  • 最后反转路径得到正确顺序

5)复杂度分析:

  • 时间复杂度:O(rows×cols) 填充DP表
  • 空间复杂度:O(rows×cols) 存储DP表

这个实现比回溯法更高效,特别适合大规模网格的情况。动态规划解法通过自底向上的方式避免了递归带来的性能开销。

8. 运行结果:

回溯法:找到路径: (0,0) -> (1,0) -> (1,1) -> (2,1) -> (2,2) -> (3,2) -> (3,3)
动态规划:找到路径: (0,0) -> (1,0) -> (1,1) -> (2,1) -> (2,2) -> (3,2) -> (3,3)

感谢您的阅读。原创不易,如您觉得有价值,请点赞,关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水草

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值