回溯算法的定義:回溯算法也叫試探法,它是一種系統地搜索問題的解的方法。回溯算法的基本思想是:從一條路往前走,能進則進,不能進則退回來,換一條路再試。
- 遞歸函數的開頭寫好中止條件,或者跳出條件,滿足條件纔將當前結果加入總結果中,或者不滿足讓函數return,防止重複遍歷
- 已經經過的地點不在經過(已經搜索過的解空間不再重複搜索)
- 遍歷過當前節點後,爲了回溯到上一步,要去掉已經加入到結果list中的當前節點。
目錄
一、矩陣中的路徑
1.1 題幹
例如 a b c e s f c s a d e e 矩陣中包含一條字符串"bccced"的路徑,但是矩陣中不包含"abcb"路徑,判斷是否存在,存在返回true,不存在返回false
1.2 思路
遍歷,進行匹配
- 依次從每個位置出發,與匹配的路徑進行對比
- 設置bool矩陣,尺寸大小與矩陣相同,已經經過的位置,則將bool中的值設置爲true,防止重複經過
- 不想被改變的量,我們可以加上const修飾,以免不小心將其改變,如果改變const的變量,編譯器會報錯
- 經過的矩陣以對於moving中的reached矩陣,不要加引用&,因爲這樣,每調用一次,會重新傳遞參數給相應的函數。
- 此代碼在每次進入下次函數遞歸的時候加判斷,可能會導致代碼不夠精簡。如果直接可以在每次函數開始之前加判斷,可能能夠精簡代碼。因此,我們可以重新編寫函數。
#include<iostream>
#include<vector>
#include<cstdio>
#include<stdio.h>
#include<algorithm>
using namespace std;
class Solution {
public:
bool hasPath(char* matrix, int rows, int cols, const char* str)
{
bool exist = false;
int length = strlen(str);
for (int idx_r = 0; idx_r < rows; idx_r++){
for (int idx_c = 0; idx_c < cols; idx_c++){
vector<bool> reached(rows*cols, false);
exist = exist || (moving(matrix, idx_r, idx_c, rows, cols, str, reached, 0, length));
if (exist)return true;
}
}
return exist;
}
bool moving(const char* matrix, int cur_r, int cur_c, int rows, int cols,
const char* str, vector<bool> reached, int str_loc, const int length){
int location = cur_r*cols + cur_c;
if (matrix[location] != str[str_loc])return false;//不相等則此路徑不存在
else if (str_loc == length - 1){//餘下情況是可能存在路徑的情況
//如果已經到了str的結尾,則路徑存在,return true
return true;
}
//如果沒有到str結尾,則接着往下比較
reached[location] = true;//關於reached矩陣,相當於每個往函數裏面都複製了一份,千萬不要用引用&,而要直接傳入
bool exist = false;
// right
int right = cur_r*cols + cur_c + 1;
if (cur_c + 1 < cols && !reached[right]){
exist = exist || (moving(matrix, cur_r, cur_c + 1, rows, cols, str, reached, str_loc + 1, length));
}
// left
int left = cur_r*cols + cur_c - 1;
if (cur_c - 1 >= 0 && !reached[left]){
exist = exist || (moving(matrix, cur_r, cur_c - 1, rows, cols, str, reached, str_loc + 1, length));
}
// up
int up = (cur_r + 1)*cols + cur_c;
if (cur_r + 1 < rows && !reached[up]){
exist = exist || (moving(matrix, cur_r + 1, cur_c, rows, cols, str, reached, str_loc + 1, length));
}
//down
int down = (cur_r - 1)*cols + cur_c;
if (cur_r - 1 >= 0 && !reached[down]){
exist = exist || (moving(matrix, cur_r - 1, cur_c, rows, cols, str, reached, str_loc + 1, length));
}
return exist;
}
};
int main(){
char *matrix = "abcesfcsadee";
int rows = 3; int cols = 4;
char *str1 = "bcced"; // true
char *str2 = "abcb"; // false
Solution s1;
cout << s1.hasPath(matrix, rows, cols, str1) << endl;//true
cout << s1.hasPath(matrix, rows, cols, str2) << endl;//true
int end; cin >> end;
return 0;
}
下面的代碼,先判斷,後遞歸,從代碼量上已經下降很多:
class Solution {
public:
bool hasPath(char* matrix, int rows, int cols, const char* str)
{
bool exist = false;
int length = strlen(str);
for (int idx_r = 0; idx_r < rows; idx_r++){
for (int idx_c = 0; idx_c < cols; idx_c++){
vector<bool> reached(rows*cols, false);
exist = exist || (moving(matrix, idx_r, idx_c, rows, cols, str, reached, 0, length));
if (exist)return true;
}
}
return exist;
}
bool moving(const char* matrix, int cur_r, int cur_c, int rows, int cols,
const char* str, vector<bool> reached, int str_loc, const int length){
int location = cur_r*cols + cur_c;
if (cur_r<0 || cur_r >= rows || cur_c<0 || cur_c >= cols || reached[location] == true || matrix[location] != str[str_loc])return false;//不相等則此路徑不存在
else if (str_loc == length - 1){//餘下情況是可能存在路徑的情況
//如果已經到了str的結尾,則路徑存在,return true
return true;
}
//如果沒有到str結尾,則接着往下比較
reached[location] = true;//關於reached矩陣,相當於每個往函數裏面都複製了一份,千萬不要用引用&,而要直接傳入
bool exist = false;
// right
exist = exist || (moving(matrix, cur_r, cur_c + 1, rows, cols, str, reached, str_loc + 1, length));
// left
exist = exist || (moving(matrix, cur_r, cur_c - 1, rows, cols, str, reached, str_loc + 1, length));
// up
exist = exist || (moving(matrix, cur_r + 1, cur_c, rows, cols, str, reached, str_loc + 1, length));
//down
exist = exist || (moving(matrix, cur_r - 1, cur_c, rows, cols, str, reached, str_loc + 1, length));
return exist;
}
};
二、矩陣中的遞增路徑
1.1 題幹
2019.9.7 依圖面試,當時面試的時候,list不小心弄成了&list,導致運行失敗。但是思路基本沒有錯誤。事後馬上就編寫出來了。
輸出最長遞減序列路徑以及路徑中的最後一個元素的位置:
給定一個矩陣,輸入
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 9, 0, 8 }
輸出其最長遞減的子字符串的長度和第一個元素的位置
輸出2,2 5最長路徑的起點是8,其位置爲2,2
1.2 思路及代碼
- 用list存當前路徑,如果存不下,則list中止,存入全局變量all_list之中。
- 如果存得下,就繼續往下遞歸
#include <iostream>
#include<vector>
using namespace std;
int row;
int col;
void moving(int loc_r, int loc_c, vector<vector<int>> matrix, vector<int> list, vector<vector<int>>&all_list){
list.push_back(matrix[loc_r][loc_c]);
bool exist = false;
// down
if ((loc_r + 1< ::row) && matrix[loc_r + 1][loc_c]>list[list.size() - 1]){
exist = true;
moving(loc_r + 1, loc_c, matrix, list, all_list);
}
//up
if (loc_r - 1 >= 0 && matrix[loc_r - 1][loc_c]>list[list.size() - 1]){
exist = true;
moving(loc_r - 1, loc_c, matrix, list, all_list);
}
//left
if (loc_c - 1 >= 0 && matrix[loc_r][loc_c - 1]>list[list.size() - 1]){
exist = true;
moving(loc_r, loc_c - 1, matrix, list, all_list);
}
//right
if (loc_c + 1< ::col && matrix[loc_r][loc_c + 1]>list[list.size() - 1]){
exist = true;
moving(loc_r, loc_c + 1, matrix, list, all_list);
}
if (!exist){
list.push_back(loc_r);
list.push_back(loc_c);
all_list.push_back(list);
//list.clear();
return;
}
}
int main() {
vector<vector<int>>matrix{ { 1, 2, 3 }, { 4, 5, 6 }, { 9, 0, 8 } };
row = matrix.size();
col = matrix[0].size();
vector<int> list;
vector<vector<int>>all_list;
for (int idx = 0; idx<row; idx++){
for (int idx_c = 0; idx_c<col; idx_c++){
moving(idx, idx_c, matrix, list, all_list);
}
}
int max_len = -2;
int last_row, last_col;
for (auto item : all_list){
cout << "max_len " << max_len << " size-2 " << item.size() - 2 << endl;
int length = item.size() - 1;
if (length > max_len){
cout << "in if max_len " << max_len << " size-2 " << item.size() - 2 << endl;
max_len = item.size() - 2;
last_row = item[item.size() - 2];
last_col = item[item.size() - 1];
}
}
cout << last_row << ',' << last_col << " " << max_len << endl;
int end; cin >> end;
return 0;
}
三、不同路徑
3.1 題幹
LeetCode 980,OJ:
https://leetcode-cn.com/problems/unique-paths-iii/
在二維網格 grid 上,有 4 種類型的方格:
- 1 表示起始方格。且只有一個起始方格。
- 2 表示結束方格,且只有一個結束方格。
- 0 表示我們可以走過的空方格。
- -1 表示我們無法跨越的障礙。
返回在四個方向(上、下、左、右)上行走時,從起始方格到結束方格的不同路徑的數目,每一個無障礙方格都要通過一次。即012的方格,必須且有且只有通過一次,-1的方格不能經過。
輸入:[[1,0,0,0],[0,0,0,0],[0,0,2,-1]]
輸出:2
3.2 分析
又是典型的走迷宮問題。需要回歸的是走法的方法數目。
設置遞歸函數:
- 設置已經經過的路徑用vector<vector<bool>>passed來實現
- 如果-1,則return false;表明此路徑失敗,不存在
- 如果是2,則表示運行到終點,如果每個0的點都經過了,則此路徑運行成功
#include<iostream>
#include<vector>
#include<cstdio>
#include<stdio.h>
#include<string>
#include<algorithm>
using namespace std;
class Solution {
public:
int uniquePathsIII(vector<vector<int>>& grid) {
int row = grid.size();
if (row < 1)return 0;
int col = grid[0].size();
if (col < 1)return 0;
//設置經過的位置的矩陣,經過則設爲passed
vector<bool>passed_col(col, false);
vector<vector<bool>>passed(row, passed_col);
int methods = 0;
for (int r = 0; r < row; r++){
for (int c = 0; c < col; c++){
if (grid[r][c] == 1){
moving(row, col, grid, methods, r, c, passed);
}
}
}
return methods;
}
void moving(const int row, const int col, const vector<vector<int>>& grid,
int &methods, int cur_r, int cur_c, vector<vector<bool>>passed){
//走出格子之外,或者碰壁-1,或者走回頭路,則直接返回
if (cur_r >= row || cur_r<0 || cur_c >= col || cur_c < 0 || grid[cur_r][cur_c] == -1 || passed[cur_r][cur_c]){
return;
}
//成功走到終點2,且每個都經過了一次,經過的方法數目+1
if (grid[cur_r][cur_c] == 2){
//判斷是否經過的0或者1均經過,如果不是的話,則直接返回
for (int r = 0; r < row; r++){
for (int c = 0; c < col; c++){
if (grid[r][c] == 0 || grid[r][c] == 1){
if (!passed[r][c]){
return;
}
}
}
}
methods++;
return;
}
// 如果其他情況,則繼續往下走
if (grid[cur_r][cur_c] == 0 || grid[cur_r][cur_c] == 1){
passed[cur_r][cur_c] = true;
moving(row, col, grid, methods, cur_r + 1, cur_c, passed);
moving(row, col, grid, methods, cur_r - 1, cur_c, passed);
moving(row, col, grid, methods, cur_r, cur_c + 1, passed);
moving(row, col, grid, methods, cur_r, cur_c - 1, passed);
}
}
};
int main(){
vector<vector<int>> grid = { { 1, 0, 0, 0 },
{ 0, 0, 0, 0 }, { 0, 0, 2, -1 } };// out 2
vector<vector<int>> grid2 = { { 1, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 2 } };//out 4
Solution s1;
cout << s1.uniquePathsIII(grid) << endl;
cout << s1.uniquePathsIII(grid2) << endl;
int end; cin >> end;
return 0;
}
3.2 障礙物的不同路徑
機器人希望從左上角到右下角,中間有障礙物。只能向左或者向下走,問有多少種走法?障礙物在矩陣中用1表示。
輸入[
[0,0,0],
[0,1,0],
[0,0,0]]
輸出: 2
3.3 思路及解法
動態規劃方法
- 很簡單,可以看作動態規劃,當前路徑爲左邊和上邊的路徑數
- 如果當前爲1,障礙物,則路徑必不存在,則當前方法數目爲0
#include<iostream>
#include<vector>
#include<cstdio>
#include<stdio.h>
#include<string>
#include<algorithm>
using namespace std;
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int row = obstacleGrid.size();
if (row < 1)return 0;
int col = obstacleGrid[0].size();
if (col < 1)return 0;
if (obstacleGrid[0][0] == 1)return 0;
vector<unsigned long long int> col_methods(col, 0);
vector<vector<unsigned long long int>>methods(row, col_methods);
//到每一點的方法數目
methods[0][0] = 1;
//第一列,行的方法數目
for (int idx = 1; idx < row; idx++){
if (obstacleGrid[idx][0] == 1)methods[idx][0] = 0;
else{
methods[idx][0] = methods[idx - 1][0];
}
}
for (int idx = 1; idx < col; idx++){
if (obstacleGrid[0][idx] == 1)methods[0][idx] = 0;
else{
methods[0][idx] = methods[0][idx - 1];
}
}
//每一個位置的方法數目是其左邊和上邊方法數的和
for (int r = 1; r < row; r++){
for (int c = 1; c < col; c++){
if (obstacleGrid[r][c] == 1)methods[r][c] = 0;
else{
methods[r][c] += methods[r - 1][c] + methods[r][c - 1];
}
}
}
return methods[row - 1][col - 1];
}
};
int main(){
vector<vector<int>> grid = { { 0, 0, 0 },
{ 0, 1, 0 }, { 0, 0, 0 } };// out 2
Solution s1;
cout << s1.uniquePathsWithObstacles(grid) << endl;
int end; cin >> end;
return 0;
}
四、N皇后問題
N皇后問題,Leetcode 51
OJ:https://leetcode-cn.com/problems/n-queens/
4.1 題幹
n 皇后問題研究的是如何將 n 個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。即這N個皇后不能同行同列同斜列。
輸入,棋盤,輸出放置的方法:
輸入: 4
輸出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]]
4.2 分析
看作回溯法的問題:
- 每行找到一個元素,放置之後,如果可以的話往下一個行放
- 每行放入的時候,如果滿足條件,才往下一行放,不滿足則不放
- 到結尾的時候,滿足條件則對最終結果push_back
- 用每行的列位置來存儲,則能簡化存儲與運算。最後再恢復出來
代碼:
#include<iostream>
#include<vector>
#include<cstdio>
#include<set>
#include<string>
#include<algorithm>
using namespace std;
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> result_matrix;
if (n < 1)return result_matrix;
vector<int> col_loc(n);// col[idx=row],一共n列
vector<vector<int>>result; //用於存儲所有滿足條件的位置的集合
find_location(n, 0, col_loc, result);
if (result.empty())return result_matrix;
else{//根據行列值恢復出皇后矩陣
for (auto item : result){
string each_row(n, '.');
vector<string> matrix(n, each_row);//空矩陣
for (int idx = 0; idx < n; idx++){//填入皇后
matrix[idx][item[idx]] = 'Q';
}
result_matrix.push_back(matrix);
}
}
return result_matrix;
}
//判斷當前row位置的元素是否與其他元素(<row)衝突
bool judge_correct(const int n, const int row, const vector<int> col_loc){
for (int idx = 0; idx < row; idx++){
if (col_loc[idx] == col_loc[row])return false;//列不能相等
if (idx + col_loc[idx] == row + col_loc[row])return false;//右上、左下不衝突
if (idx - col_loc[idx] == row - col_loc[row])return false;//左上,右下不衝突
}
return true;
}
//給當前row位置元素判斷
void find_location(const int n, int row, vector<int> col_loc, vector<vector<int>> &result){
//如果已經到最後一行,可以加入則將元素加入之後返回
if (row == n - 1){
for (int idx = 0; idx < n; idx++){
col_loc[row] = idx;
if (judge_correct(n, row, col_loc)){
result.push_back(col_loc);
}
}
return;
}
//如果沒有到最後一行,則加入元素之後進行遞歸
for (int idx = 0; idx < n; idx++){
col_loc[row] = idx;
if (judge_correct(n, row, col_loc)){
find_location(n, row + 1, col_loc, result);
}
}
}
};
int main(){
Solution s1;
vector<vector<string>> result_matrix = s1.solveNQueens(4);
if (result_matrix.empty())cout << "Empty!" << endl;
else{
for (auto matrix : result_matrix){
cout << endl << "matrix:" << endl;
for (auto row_string : matrix){
cout << row_string << endl;
}
}
}
int end; cin >> end;
return 0;
}
五、數獨解
OJ:Leetcode 37
https://leetcode-cn.com/problems/sudoku-solver/
5.1 題幹
將數獨填完:
- 數字 1-9 在每一行只能出現一次。
- 數字 1-9 在每一列只能出現一次。
- 數字 1-9 在每一個以粗實線分隔的 3x3 宮內只能出現一次。
輸入:
53..7....
6..195...
.98....6.
8...6...3
4..8.3..1
7...2...6
.6....28.
...419..5
....8..79
輸出
534678912
672195348
198342567
859761423
426853791
713924856
961537284
287419635
345286179
5.2 分析
- 針對每一個元素進行填充,如果符合規矩,則繼續向下遞歸填充下一個未填充的元素
- 當前元素填充試過所有的,即進行return
- 運行超時,電腦上運行發現程序沒有錯誤,但是運行較慢,下面代碼是暴力窮舉法,顯然不行,必須加入約束條件進行判斷
#include<iostream>
#include<vector>
#include<cstdio>
#include<set>
#include<string>
#include<algorithm>
using namespace std;
class Solution {
public:
void solveSudoku(vector<vector<char>>& board) {
vector<vector<char>>result;
find_sudoku(board, result);
board = result;
}
//判斷新加入元素是否符合
bool num_match(const vector<vector<char>> board, const int row, const int col){
for (int idx = 0; idx < 9; idx++){
//行不同
if (idx != col){
if (board[row][col] == board[row][idx])return false;
}
//列不同
if (idx != row){
if (board[row][col] == board[idx][col])return false;
}
}
//每個九宮格內不同
int grid_row = row / 3; int grid_col = col / 3;
for (int r = 3 * grid_row; r < 3 * grid_row + 3; r++){
for (int c = 3 * grid_col; c < 3 * grid_col + 3; c++){
if (r != row || c != col){
if (board[r][c] == board[row][col])return false;
}
}
}
return true;
}
void find_sudoku(vector<vector<char>> board, vector<vector<char>> &result){
int row = 0; int col = 0;
for (row = 0; row < 9; row++){
for (col = 0; col < 9; col++){
//遍歷每一行每一列,找到新的未填充的元素
if (board[row][col] == '.'){
//給每個元素填入相應的值,判斷能否合規
for (int idx = 1; idx <= 9; idx++){
board[row][col] = '0' + idx;
//填充值恰當則繼續往下填充
if (num_match(board, row, col)){
find_sudoku(board, result);
}
}
return;//需要在當前元素填充結束之後,結束當前遍歷。
}
}
}
if (row == 9 && col == 9)result = board;
}
};
int main(){
vector<char>each_row(9);
vector<vector<char>> board(9, each_row);
/*
53..7....
6..195...
.98....6.
8...6...3
4..8.3..1
7...2...6
.6....28.
...419..5
....8..79
*/
for (int row = 0; row < 9; row++){
for (int col = 0; col < 9; col++){
cin >> board[row][col];
}
}
Solution s1;
s1.solveSudoku(board);
//輸出矩陣
for (int row = 0; row < 9; row++){
for (int col = 0; col < 9; col++){
cout << board[row][col];
}
cout << endl;
}
int end; cin >> end;
return 0;
}
六、推箱子
6.1 題幹
體規則就是在一個N*M的地圖上,有1個玩家、1個箱子、1個目的地以及若干障礙,其餘是空地。玩家可以往上下左右4個方向移動,但是不能移動出地圖或者移動到障礙裏去。如果往這個方向移動推到了箱子,箱子也會按這個方向移動一格,當然,箱子也不能被推出地圖或推到障礙裏。當箱子被推到目的地以後,遊戲目標達成。現在告訴你遊戲開始是初始的地圖佈局,請你求出玩家最少需要移動多少步才能夠將遊戲目標達成。
輸入描述:
- 每個測試輸入包含1個測試用例
- 第一行輸入兩個數字N,M表示地圖的大小。其中0<N,M<=8。
- 接下來有N行,每行包含M個字符表示該行地圖。其中 . 表示空地、X表示玩家、*表示箱子、#表示障礙、@表示目的地。
- 每個地圖必定包含1個玩家、1個箱子、1個目的地。
輸出描述:
輸出一個數字表示玩家最少需要移動多少步才能將遊戲目標達成。當無論如何達成不了的時候,輸出-1。
4 4
....
..*@
....
.X..
輸出3
...#..
......
#*##..
..##.#
..X...
.@#...
輸出11
1 3
X*@
輸出1
6.2 思路及程序
思路:
- 一個記錄當前人和箱子位置的變量
- 如果溢出或者撞牆,則return
- 如果沒有溢出或者沒有撞牆,則繼續往下
- 人和箱子挨着且與方向一致則推箱子,不一致人自己走
- 運算複雜度較高,運行一次耗時較長
#include <iostream>
#include <vector>
#include <algorithm>
#include<string>
using namespace std;
int row, col;
int moves = -1;
void moving(int r, int c, int br, int bc, int current_moves,
const vector<vector<char>> matrix, vector<vector<bool>>p_reach){
// 運行失敗
if (r < 0 || r >= row || c < 0 || c >= col || p_reach[r][c] == true || matrix [r][c]=='#'||
br < 0 || br >= row || bc < 0 || bc >= col || matrix[br][bc]=='#'){
//cout << "fail" << endl;
return;
}
//// 輸出當前矩陣狀態
//cout << endl;
//for (int idx = 0; idx < row; idx++){
// for (int cdx = 0; cdx < col; cdx++){
// if (idx == r && cdx == c)cout << 'X';
// else if (idx == br && cdx == bc)cout << '*';
// else cout << matrix[idx][cdx];
// }
// cout << endl;
//}
//運行成功
if (matrix[br][bc] == '@'){
if (moves == -1)moves = current_moves;
else if (current_moves < moves){
//cout << "success,current:"<<current_moves<<"moves:"<<moves << endl;
moves = current_moves;
}
return;
}
current_moves++;
p_reach[r][c] = true;
//推或者走
// down
if (r + 1 == br && c == bc)moving(r + 1, c, br + 1, bc, current_moves, matrix, p_reach);
else moving(r + 1, c, br, bc, current_moves, matrix, p_reach);
//up
if (r - 1 == br && c == bc)moving(r - 1, c, br - 1, bc, current_moves, matrix, p_reach);
else moving(r - 1, c, br, bc, current_moves, matrix, p_reach);
//right
if (r == br && c+1 == bc)moving(r , c+1, br , bc+1, current_moves, matrix, p_reach);
else moving(r , c+1, br, bc, current_moves, matrix, p_reach);
//left
if (r == br && c - 1 == bc)moving(r, c - 1, br, bc- 1, current_moves, matrix, p_reach);
else moving(r, c - 1, br, bc, current_moves, matrix, p_reach);
return;
}
int main(){
cin >> row >> col;
vector<char> each_row(col);
vector<vector<char>>matrix(row, each_row);
for (int r = 0; r < row; r++){
for (int c = 0; c < col; c++){
cin >> matrix[r][c];
}
}
vector<bool> r_reach(col, false);
vector<vector<bool>>p_reach(row, r_reach);
int pr, pc, br, bc;
for (int r = 0; r < row; r++){
for (int c = 0; c < col; c++){
if (matrix[r][c] == 'X'){
pr = r; pc = c;
}
if (matrix[r][c] == '*'){
br = r; bc = c;
}
}
}
moving(pr, pc, br, bc, 0, matrix,p_reach);
cout << ::moves << endl;
int end; cin >> end;
return 0;
}