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 → ■
这个流程展示了动态规划的详细步骤:
- 自底向上构建DP表记录可达性
- 通过状态转移方程计算每个位置的可达性
- 从终点回溯构建实际路径
- 最终输出最优路径
整个过程的时间复杂度为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)
感谢您的阅读。原创不易,如您觉得有价值,请点赞,关注。