学习来源:
代码随香炉:https://www.programmercarl.com/
labuladong算法:https://labuladong.github.io/algo/
回溯算法
**一般来说:
组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
**
递归终止条件
单层搜索的逻辑
0 回溯算法框架
解决for循环无法写出的场景,通过回溯可以暴力找出解决方法
1 组合
组合问题 (返回一个序列的组合)
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
示例: 输入: n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
减枝 优化 已经存了x个,剩下的通过总数k进行减去
组合总和III
组合总和 (数组无重复元素,无限制选取)
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。(无k个数的限制)
candidates 中的数字可以无限制重复被选取。
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
不用i+1,表示可以重读选择
组合总和II (数组有重复元素,每个只能用一次)
startindex用了i+1,表示只能选择一次
used数组进行去重
我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
强调一下,树层去重的话,需要对数组排序!
如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]。
在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下:
used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
used[i - 1] == false,说明同一树层candidates[i - 1]使用过
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) // 剪枝 (前提已经排序好)
电话号码的字母组合
2 分割
终止条件的判断 1 分割线到达末尾,或者按照段数分割 2 注意break 和continue的使用,回文 IP合法的判断
分割回文串
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案。
示例: 输入: “aab” 输出: [ [“aa”,“b”], [“a”,“a”,“b”]
关键点:
切割问题,有不同的切割方式
判断回文
例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中在选组第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段…。
需要startIndex 切割过的地方不能重复切割
复原IP
- 判断子串是否合法:
主要考虑到如下三点:
段位以0为开头的数字不合法
段位里有非正整数字符不合法
段位如果大于255了不合法
3. 子集
但是要清楚子集问题和组合问题、分割问题的的区别,子集是收集树形结构中树的所有节点的结果。
而组合问题、分割问题是收集树形结构中叶子节点的结果。
子集 (数组元素不同)
子集II (数组元素有相同的 子集不重复)
去重 (树枝去重 和 树层去重)
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出: [ [2], [1], [1,2,2], [2,2], [1,2], []]
去重的话一定要先排序
从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集!
用集合去重
本题也可以不使用used数组来去重,因为递归的时候下一个startIndex是i+1而不是0。
4 排列
全排列 (数组不含重复数字)
排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示:
全排列2 (数组含重复数字)
startindex不用定义,直接从0开始遍历到整个数组大小,使用used数组进行树枝去用过的元素。
去重过程中,树层去重和树枝去重都是可以的。树层去重更加高效一些。
回溯的时间复杂度
5 棋盘问题
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
一个for循环遍历列 递归遍历深度的行
解数独 二维递归 两个for循环
6 深搜
递增子序列
set去重
重新安排行程 !!
7 岛屿题目
7.1 岛屿数量
二维矩阵四周可以认为也是被海水包围的,所以靠边的陆地也算作岛屿。
class Solution {
public:
int res = 0;
int numIslands(vector<vector<char>>& grid) {
int row = grid.size();
int col = grid[0].size();
// 遍历 grid
for(int i =0;i<row;i++){
for(int j =0;j<col;j++){
if(grid[i][j]=='1'){
res++;// 每发现一个岛屿,岛屿数量加一
dfs(grid,i,j); // 然后使用 DFS 将岛屿淹了
}
}
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(vector<vector<char>>& grid,int i,int j){
int row = grid.size();
int col = grid[0].size();
if(i<0||i>=row||j<0||j>=col){
return;
}
if(grid[i][j]=='0'){
return;
}
grid[i][j]='0';// 将 (i, j) 变成海水
// 淹没上下左右的陆地
dfs(grid,i-1,j);
dfs(grid,i+1,j);
dfs(grid,i,j-1);
dfs(grid,i,j+1);
}
};
7.2 封闭岛屿的数量
靠边的陆地不算作「封闭岛屿」。
class Solution {
public:
int closedIsland(vector<vector<int>>& grid) {
int m = grid.size(), n = grid[0].size();
for (int j = 0; j < n; j++) {
// 把靠上边的岛屿淹掉
dfs(grid, 0, j);
// 把靠下边的岛屿淹掉
dfs(grid, m - 1, j);
}
for (int i = 0; i < m; i++) {
// 把靠左边的岛屿淹掉
dfs(grid, i, 0);
// 把靠右边的岛屿淹掉
dfs(grid, i, n - 1);
}
// 遍历 grid,剩下的岛屿都是封闭岛屿
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 0) {
res++;
dfs(grid, i, j);
}
}
}
return res;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void dfs(vector<vector<int>>& grid, int i, int j) {
int m = grid.size(), n = grid[0].size();
if (i < 0 || j < 0 || i >= m || j >= n) {
return;
}
if (grid[i][j] == 1) {
// 已经是海水了
return;
}
// 将 (i, j) 变成海水
grid[i][j] = 1;
// 淹没上下左右的陆地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
};
7.3 最大的岛屿面积。
可以假设 grid 的四个边缘都被 0(代表水)包围着。
class Solution {
public:
int maxAreaOfIsland(vector<vector<int>>& grid) {
// 记录岛屿的最大面积
int res = 0;
int m = grid.size(), n = grid[0].size();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
// 淹没岛屿,并更新最大岛屿面积
res = max(res, dfs(grid, i, j));
}
}
}
return res;
}
// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
int dfs(vector<vector<int>>& grid, int i, int j) {
int m = grid.size(), n = grid[0].size();
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引边界
return 0;
}
if (grid[i][j] == 0) {
// 已经是海水了
return 0;
}
// 将 (i, j) 变成海水
grid[i][j] = 0;
return dfs(grid, i + 1, j)
+ dfs(grid, i, j + 1)
+ dfs(grid, i - 1, j)
+ dfs(grid, i, j - 1) + 1;
}
};
7.4 统计子岛屿
class Solution {
public:
int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) {
int m = grid1.size(), n = grid1[0].size();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid1[i][j] == 0 && grid2[i][j] == 1) {
// 这个岛屿肯定不是子岛,淹掉
dfs(grid2, i, j);
}
}
}
// 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
int res = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid2[i][j] == 1) {
res++;
dfs(grid2, i, j);
}
}
}
return res;
}
// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
void dfs(vector<vector<int>>& grid, int i, int j) {
int m = grid.size(), n = grid[0].size();
if (i < 0 || j < 0 || i >= m || j >= n) {
// 超出索引边界
return;
}
if (grid[i][j] == 0) {
// 已经是海水了
return;
}
// 将 (i, j) 变成海水
grid[i][j] = 0;
// 淹没上下左右的陆地
dfs(grid, i + 1, j);
dfs(grid, i, j + 1);
dfs(grid, i - 1, j);
dfs(grid, i, j - 1);
}
};
7.5 不同的岛屿数量
岛屿序列化的结果,只要每次使用 dfs 遍历岛屿的时候生成这串数字进行比较,就可以计算到底有多少个不同的岛屿了
// 稍微改造 dfs 函数,添加一些函数参数以便记录遍历顺序
void dfs(int[][] grid, int i, int j, string sb, int dir) {
int m = grid.length, n = grid[0].length;
if (i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0) {
return;
}
// 前序遍历位置:进入 (i, j)
grid[i][j] = 0;
sb.append(dir).append(',');
dfs(grid, i - 1, j, sb, 1); // 上
dfs(grid, i + 1, j, sb, 2); // 下
dfs(grid, i, j - 1, sb, 3); // 左
dfs(grid, i, j + 1, sb, 4); // 右
// 后序遍历位置:离开 (i, j)
sb.append(-dir).append(',');
}
// dir 记录方向,dfs 函数递归结束后,sb 记录着整个遍历顺序。有了这个 dfs 函数就好办了,我们可以直接写出最后的解法代码:
int numDistinctIslands(int[][] grid) {
int numDistinctIslands(int[][] grid) {
int m = grid.length, n = grid[0].length;
// 记录所有岛屿的序列化结果
set<string> st;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
// 淹掉这个岛屿,同时存储岛屿的序列化结果
string sb;
// 初始的方向可以随便写,不影响正确性
dfs(grid, i, j, sb, 666);
st.insert(sb);
}
}
}
// 不相同的岛屿数量
return st.size();
}
8 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
class Solution {
public:
vector<string> generateParenthesis(int n) {
if (n == 0) return {};
// 记录所有合法的括号组合
vector<string> res;
// 回溯过程中的路径
string track;
// 可用的左括号和右括号数量初始化为 n
backtrack(n, n, track, res);
return res;
}
// 可用的左括号数量为 left 个,可用的右括号数量为 rgiht 个
void backtrack(int left, int right,
string& track, vector<string>& res) {
// 若左括号剩下的多,说明不合法
if (right < left) return;
// 数量小于 0 肯定是不合法的
if (left < 0 || right < 0) return;
// 当所有括号都恰好用完时,得到一个合法的括号组合
if (left == 0 && right == 0) {
res.push_back(track);
return;
}
// 尝试放一个左括号
track.push_back('('); // 选择
backtrack(left - 1, right, track, res);
track.pop_back(); // 撤消选择
// 尝试放一个右括号
track.push_back(')'); // 选择
backtrack(left, right - 1, track, res);
track.pop_back(); // 撤消选择
}
};