回溯算法思想
解决⼀个回溯问题,实际上就是⼀个决策树的遍历过程。你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,⽆法再做选择的条件。
如果你不理解这三个词语的解释,没关系,我们后⾯会⽤「全排列」和「N皇后问题」这两个经典的回溯算法问题来帮你理解这些词语是什么意思,现在你先留着印象。
代码⽅⾯,回溯算法的框架:
result = []
def backtrack(路径, 选择列表):
if 满⾜结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径,选择列表)
撤销选择
其核⼼就是 for 循环⾥⾯的递归,在递归调⽤之前「做选择」,在递归调⽤之后「撤销选择」,特别简单。
回溯搜索是深度优先搜索(DFS)的一种,回溯法通俗的将其采用的思想是“一直向下走,走不通就掉头”,类似于树的先序遍历。dfs和回溯法其主要的区别是:回溯法在求解过程中不保留完整的树结构,而深度优先搜索则记下完整的搜索树。
为了减少存储空间,在深度优先搜索中,用标志的方法记录访问过的状态,这种处理方法使得深度优先搜索法与回溯法没什么区别了。
回溯法在实现上也是遵循深度优先的,即一步一步往前探索,而不像广度优先那样,由近及远一层一层地扫。
力扣回溯算法题目总结
1、全排列 --中等
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
if(nums.empty())
return {{}};
vector<int> temp;
backtrack(nums,temp);
return res;
}
void backtrack(const vector<int> & nums,vector<int> &temp)
{
if(temp.size() == nums.size())
{
res.push_back(temp);
return ;
}
for(int i = 0;i<nums.size();i++)
{
if(find(temp.begin(),temp.end(),nums[i]) != temp.end()) //去重
continue;
temp.push_back(nums[i]);
backtrack(nums,temp);
temp.pop_back();
}
}
private:
vector<vector<int>> res;
};
2、子集 --中等
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<int> temp;
sort(nums.begin(),nums.end());
backtrack(nums,0,temp);
return res;
}
void backtrack(const vector<int> & nums,int begin,vector<int> &temp)
{
res.push_back(temp);
for(int i = begin;i<nums.size();i++)
{
if(i > begin && nums[i] == nums[i-1]) //去重
continue;
temp.push_back(nums[i]);
backtrack(nums,i+1 ,temp);
temp.pop_back();
}
}
private:
vector<vector<int>> res;
};
3、组合 --中等
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
class Solution {
public:
vector<vector<int>> combine(int n, int k) {
if(n < 1 || k <=0)
return {};
vector<int> temp;
backtrack(n,k,1,temp);
return result;
}
void backtrack(int n, int k, int start,vector<int>& temp)
{
if(temp.size() == k)
result.push_back(temp);
for(int i = start;i<=n;i++)
{
temp.push_back(i);
backtrack(n,k,i+1,temp);
temp.pop_back();
}
return;
}
private:
vector<vector<int>> result;
};
4、水域面积 --中等
你有一个用于表示一片土地的整数矩阵land,该矩阵中每个点的值代表对应地点的海拔高度。若值为0则表示水域。由垂直、水平或对角连接的水域为池塘。池塘的大小是指相连接的水域的个数。编写一个方法来计算矩阵中所有池塘的大小,返回值需要从小到大排序。
示例:
输入:
[
[0,2,1,0],
[0,1,0,1],
[1,1,0,1],
[0,1,0,1]
]
输出: [1,2,4]
提示:
0 < len(land) <= 1000
0 < len(land[i]) <= 1000
class Solution {
public:
vector<int> pondSizes(vector<vector<int>>& land) {
if(land.empty())
return {};
vector<int> result;
rows = land.size();
cols = land[0].size();
for(int i = 0;i<rows;i++)
{
for(int j = 0;j<cols;j++)
{
if(land[i][j] == 0)
result.push_back(backtrack(land,i,j));
}
}
sort(result.begin(),result.end());
return result;
}
int backtrack(vector<vector<int>>& land,int i,int j)
{
if(i < 0 || i >= rows || j < 0 ||j>=cols || land[i][j] !=0)
return 0;
int area = 1;
land[i][j] = 1;
for(int k = 0;k<8;k++)
{
area += backtrack(land,i+around[k].first,j+around[k].second);
}
return area;
}
private:
vector<pair<int,int>> around = {{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}};//land [i][j]周围的方向
int rows;
int cols;
};
5、电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
class Solution {
public:
vector<string> letterCombinations(string digits) {
if(digits.empty())
return vector<string> {};
string temp;
backtrack(digits,temp,0);
return res;
}
void backtrack(const string &digits,string & temp,int begin)
{
if(temp.size() == digits.size())
{
res.push_back(temp);
return;
}
string s = str[digits[begin] - '2'];
for(int j = 0;j<s.size();j++)
{
temp.push_back(s[j]);
backtrack(digits,temp,begin+1);
temp.pop_back();
}
}
private:
vector<string> str = {"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
vector<string> res;
};
6、机器人的运动范围
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 2:
输入:m = 3, n = 1, k = 0
输出:1
提示:
1 <= n,m <= 100
0 <= k <= 20
class Solution {
public:
int movingCount(int m, int n, int k) {
if( m <=0 || n <=0)
return 0;
int count;
vector<vector<bool>> visited(m,vector<bool>(n,true));
count = backtrack(m,n,k,0,0,visited);
return count;
}
int backtrack(int m, int n,int k, int rows,int cols,vector<vector<bool>>& visited)
{
if(!isValid(m,n,k,rows,cols,visited))
return 0;
int sum = 1;
visited[rows][cols] = false;
for(int i = 0;i<4;i++)
{
//visited[rows][cols] = false;
int x = rows + around[i].first;
int y = cols + around[i].second;
sum += backtrack(m,n,k,x,y,visited);
}
return sum;
}
bool isValid(int m,int n, int k, int rows,int cols,vector<vector<bool>>& visited)
{
if(rows < 0 || rows >= m || cols < 0 || cols >=n || check(rows,cols) > k || !visited[rows][cols])
return false;
return true;
}
int check(int rows,int cols)
{
int sum = 0;
while(rows)
{
sum += rows % 10;
rows = rows / 10;
}
while(cols)
{
sum += cols % 10;
cols = cols / 10;
}
return sum;
}
private:
vector<pair<int,int>> around = {{0,1},{0,-1},{1,0},{-1,0}}; //上下左右四个方向
};
7、解数独 --困难
编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
空白格用 ‘.’ 表示。
一个数独。
答案被标成红色。
Note:
给定的数独序列只包含数字 1-9 和字符 '.' 。
你可以假设给定的数独只有唯一解。
给定数独永远是 9x9 形式的。
class Solution {
public:
void solveSudoku(vector<vector<char>>& board) {
if(board.empty())
return ;
backtrack(board,0,0);
return;
}
bool backtrack(vector<vector<char>> &board, int i, int j) {
int m = 9, n = 9;
if (j == n) {
// 穷举到最后一列的话就换到下一行重新开始。
return backtrack(board, i + 1, 0);
}
if (i == m) {
// 找到一个可行解,触发 base case
return true;
}
if (board[i][j] != '.') {
// 如果有预设数字,不用我们穷举
return backtrack(board, i, j + 1);
}
for (char ch = '1'; ch <= '9'; ch++) {
// 如果遇到不合法的数字,就跳过
if (!isValid(board, i, j, ch))
continue;
board[i][j] = ch;
// 如果找到一个可行解,立即结束
if (backtrack(board, i, j + 1)) {
return true;
}
board[i][j] = '.';
}
// 穷举完 1~9,依然没有找到可行解,此路不通
return false;
}
// 判断 board[i][j] 是否可以填入 n
bool isValid(const vector<vector<char>> &board, int r, int c, char n) {
for (int i = 0; i < 9; i++) {
// 判断行是否存在重复
if (board[r][i] == n) return false;
// 判断列是否存在重复
if (board[i][c] == n) return false;
// 判断 3 x 3 方框是否存在重复
if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n)
return false;
}
return true;
}
};
8、岛屿的数量 --中等
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if(grid.empty())
return 0;
rows = grid.size();
cols = grid[0].size();
vector<vector<bool>> visited(rows,vector<bool>(cols,false));
int count = 0;
for(int i = 0;i<rows;i++)
{
for(int j = 0;j<cols;j++)
{
if(grid[i][j] == '1' && !visited[i][j])
{
count++;
backtrack(i,j,grid,visited);
}
}
}
return count;
}
void backtrack(int i,int j,vector<vector<char>>& grid,vector<vector<bool>>& visited)
{
if( i < 0 || i >= rows || j < 0 || j>= cols || grid[i][j] == '0' || visited[i][j] )
return;
visited[i][j] = true;
for(int k = 0;k<4;k++)
{
int x = i + around[k].first;
int y = j + around[k].second;
backtrack(x,y,grid,visited);
}
return;
}
private:
vector<pair<int,int>> around = {{0,1},{0,-1},{1,0},{-1,0}};
int rows;
int cols;
};
9、N皇后 --困难
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
上图为 8 皇后问题的一种解法。
给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。
每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例:
输入: 4
输出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
if(n <= 0)
return {};
vector<string> board(n,string(n,'.'));
backtrack(board,0);
return result;
}
void backtrack(vector<string>& board,int rows)
{
if(rows == board.size())
{
result.push_back(board); //相当于模板中的加路径
return;
}
for(int cols = 0;cols < board[0].size();cols++)
{
if(!isValid(board,rows,cols))
continue;
board[rows][cols] = 'Q'; //做选择
backtrack(board,rows + 1); //回溯
board[rows][cols] = '.'; //撤销选择
}
return;
}
bool isValid(vector<string>& board, int rows, int cols)
{
int n = board.size();
//检查列是否有皇后冲突,由于每一行只会放一个皇后,所有不需要判断行是否有皇后冲突
for(int i = 0;i<n;i++)
{
if(board[i][cols] == 'Q')
return false;
}
//检查主对角线是否皇后冲突(主对角线性质为:cols - rows = 0),只有算左上方,下边的还没有填充
for(int i = rows -1,j = cols -1; i>=0 && j>=0; i--,j--)
{
if(board[i][j] == 'Q')
return false;
}
//检查次对角线是否皇后冲突(主对角线性质为:cols + rows = n)只有算右上方,下边的还没有填充
for(int i = rows - 1,j = cols + 1; i>=0 && j<n;i--,j++)
{
if(board[i][j] == 'Q')
return false;
}
return true;
}
private:
vector<vector<string>> result;
};