回溯法可以看成蛮力法的升级版,它从解决问题的每一步的所有可能选项里系统地选择一个可行的解决方案。回溯法适合由多个步骤组成的问题,并且每个步骤有多个选项。
用回溯法解决的问题的所有选项可以用树状结构形象地表示,在某一步有n个可能的选项,那么该步骤可以看做树状结构中的一个节点,每个选项看成树中节点连接线,经过这些连接线可以到达该节点的n个子节点。树的叶节点对应着终极状态,如果叶节点的状态满足题目约束条件,那么就找到了一个可行方案。如果叶节点的状态不满足约束方程,那么只好回溯到它的上一个节点再尝试其他选项。如果上一个节点的所有选项都已尝试过仍然不能到达满足约束条件的终结状态,则再次回溯到上一个节点。如果所有节点的所有选项都尝试过仍不能满足题目的约束条件,则该问题无解。
通常回溯法适合用递归实现。
面试题12:矩阵中的路径。请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。如以下矩阵包含字符串“bfce”的路径,不包含“abfb”的路径。
首先在矩阵中选择一个格子作为路径起点。假设路径中某格子的字符为ch,并且这个格子将对应于路径上第i个字符,假设路径上第i个字符不是ch,那么这个格子不可能处在路径上第i个位置,如果路径上的第i个字符正好是ch,那么到此格子的相邻格子上寻找第i+1个字符。除矩阵边界上的格子外,其他格子都有4个相邻的格子。重复这个过程。
由于回溯法的递归特性,路径可以看成一个栈,当在矩阵中定位了路径中前n个字符的位置之后,在与第n个字符对应的格子周围都没有找到第n+1个字符,这时只好返回路径上第n-1个字符,重新定位第n个字符。
由于路径不能重复进入相同格子,还需定义和字符矩阵大小一样的布尔值矩阵,来标识路径是否已经进入过。
以下为代码实现:
#include <iostream>
using namespace std;
bool hasPathCore(const char *matrix, int rows, int cols, int row, int col, const char *str, bool *visited, int &pathLength) {
if (str[pathLength] == '\0') { // 当递归到pathLength与输入的串长度相等时即找到了完整路径
return true;
}
bool hasPath = false;
// 当row和col未越界并且当前格子值与str当前位置的值相等并且此格子未被访问过,说明这个格子暂时有效
if (row >= 0 && col >= 0 && row < rows && col < cols && matrix[row * cols + col] == str[pathLength] && !visited[row * cols + col]) {
++pathLength; // 当前路径长度增加
visited[row * cols + col] = true;
// 遍历该格子周围格子
hasPath = hasPathCore(matrix, rows, cols, row - 1, col, str, visited, pathLength)
|| hasPathCore(matrix, rows, cols, row + 1, col, str, visited, pathLength)
|| hasPathCore(matrix, rows, cols, row, col - 1, str, visited, pathLength)
|| hasPathCore(matrix, rows, cols, row, col + 1, str, visited, pathLength);
if (!hasPath) { // 如下一个字符不能匹配该格子周围所有格子时,说明此格子无效
--pathLength;
visited[row * cols + col] = false;
}
}
return hasPath;
}
bool HasPath(const char* matrix, int rows, int cols, const char* str) {
if (matrix == nullptr || rows < 1 || cols < 1 || str == nullptr) {
return false;
}
bool* visited = new bool[rows * cols]; // 保存路径上访问过的节点
memset(visited, false, rows * cols); // 将visited中的值初始化为false,注意第三个参数是字节数,但bool值所占大小为1字节,因此字节数等于布尔值数量
int pathLength = 0; // 路径长度
for (int row = 0; row < rows; ++row) { // 循环遍历所有起点
for (int col = 0; col < cols; ++col) {
if (hasPathCore(matrix, rows, cols, row, col, str, visited, pathLength)) { // 如找到一条路径
return true;
}
}
}
delete[] visited;
return false;
}
int main() {
const char* matrix = "abtgcfcsjdeh"; // 矩阵以上图中矩阵为例
if (HasPath(matrix, 3, 4, "abts")) {
cout << "存在一条路径。" << endl;
} else {
cout << "不存在路径。" << endl;
}
return 0;
}
面试题13:机器人的运动范围。地上有一个m行n列的方格。规定左下角的格子坐标为(0,0),一个机器人从坐标(0,0)的格子开始移动,它每次可以向左、右、上、下移动一格,但不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k=18时,机器人能够进入方格(35,37),因为3+5+3+7=18。但它不能进入方格(35,38),因为3+5+3+8=19。请问该机器人能够到达多少个格子?
和前面题目类似,这个方格也可以看作一个m*n的矩阵,同样,在这个矩阵中,除边界上的格子外,其他格子都有4个相邻的格子:
#include <iostream>
using namespace std;
int GetDigitSum(int num) { // 求数字的各数位之和
int sum = 0;
while (num > 0) {
sum += num % 10;
num /= 10;
}
return sum;
}
int MovingCountCore(int threshold, int rows, int cols, int row, int col, bool* visited) {
int count = 0;
if (row < rows && col < cols && row >= 0 && col >= 0 && (visited[row * cols + col] == false) && (GetDigitSum(row) + GetDigitSum(col) <= threshold)) {
visited[row * cols + col] = true;
count = 1 + MovingCountCore(threshold, rows, cols, row + 1, col, visited)
+ MovingCountCore(threshold, rows, cols, row - 1, col, visited)
+ MovingCountCore(threshold, rows, cols, row, col + 1, visited)
+ MovingCountCore(threshold, rows, cols, row, col - 1, visited);
}
return count;
}
int MovingCount(int threshold, int rows, int cols) {
if (threshold < 0 || rows < 1 || cols < 1) {
return 0;
}
bool* visited = new bool[rows * cols]; // 保存已经计算过的格子
memset(visited, false, rows * cols);
int count = MovingCountCore(threshold, rows, cols, 0, 0, visited);
delete[] visited;
return count;
}
int main() {
cout << MovingCount(2, 10, 10) << endl;
return 0;
}