总结战利品篇(回溯算法入门)
跟着carl哥的第X+3天
x天是因为在床上躺了两周,从现在开始学习!!!
缺的会开始补回来的,我向天发誓
还没看N皇后跟数独(比较难),而且刚好是早上,所以看一下总结篇
半路开香槟了属于是
回溯算法基础模板
void backtracking(参数){
//终止条件一般是当一个元素符合条件以后,将该元素存入结果集 然后返回到上一层再去向下遍历其他元素
if(终止条件){
存放结果;
return;
}
//for循环就是用来遍历数组的每一个元素(横向遍历)
for(选择:本层集合中元素(树中结点孩子的数量就是集合的大小)){
处理节点;
//递归向下遍历 在终止条件中判断是否符合条件
backtracking(路径,选择列表); //递归
//递归之后返回上一层后,再进行回溯处理,把原有下一层的元素去除 重新遍历该层中的其他元素
//这便是回溯法
回溯,撤销处理结果
}
}
回溯思路(回溯三部曲):
-
第一步:确定好递归后返回的参数(一般采用全局变量,不需要在回溯方法中传太多参数)
-
第二步:就是确定好回溯法的终止条件 (在这道题是存储单一答案的数组大小 == k值)
-
第三步:单层搜索的过程 (每一层我们应该怎么写 只看一层就不会乱 )
其实回溯法就是暴力搜索,然后运用递归实现n个for循环。横向遍历是数组宽度,纵向遍历是递归。
总结回溯算法
- 组合问题:N个数里面按一定规则找出k个数的集合
- 切割问题:一个字符串按一定规则进行字符串的切割共有几种方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 排列问题:N个数按一定规则进行全排列
每一个问题其实只要画出来树形图,基本就能清楚个大概了 ,所以建议都画画图。
组合问题
leetcode77.组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
- 输入:n = 4, k = 2
- 输出:[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4],]
这是我们熟悉回溯模板的第一步,现在回头看来只需要注意终止条件,直接套用模板就可以了
以 for 循环作为横向便利,横向遍历( backTrack )的每一个元素中都要进行深序遍历(递归)
当深序遍历到符合题目条件( if (单个结果的数组大小 = = k) ) 直接return到上一层,然后回溯(把刚刚在这一层添加的元素去掉,添加的值减掉) 然后继续遍历for循环的下一个元素,继续判断,当这一层遍历完以后,继续return,直到最上面一层的原数组遍历完成
那我们怎么知道要遍历下一层的哪一个元素呢? 我们怎么避免结果集中有重复元素呢?
这个时候就要用到 startIndex,传参时加上它可以让你遍历到上一层for循环中遍历到元素的下一个,让程序可以继续横向便利和深序遍历而不重复!
额外收获:因为回溯属于暴力遍历,所以如果能够剪枝,会让时间复杂度减少许多,从这道题看可以剪枝的地方我们就在递归中每一层的for循环所选择的起始位置,因为如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
k - path.size()(还需要的元素个数)<= n - i + 1(剩余的总元素)
leetcode216.组合总和III
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
- 输入: k = 3, n = 9
- 输出: [[1,2,6], [1,3,5], [2,3,4]]
- 解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。
这道题跟上一道题本质没有区别,哈哈哈因为都是组合问题,只是加了一个sum变量,然后终止条件上加了一个判断就是 sum = = n。
leetcode17.电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
- 输入:“23”
- 输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”]
这道题相较于前两题,看本质,跟前面不同的是前面是一个数组内组合,这道题是两个不同或者两个以上的数组的组合=====》这个其实就是影响到startIndex,因为startIndex是根据上一层的startIndex来遍历接下来的同一层下个元素跟下一层的元素的,但是因为是不同数组,所以第一个数组的元素需要从0开始去遍历另外一个数组的元素!!!
我的额外收获:使用了LinkedList去删除数组(其实已经是链表了)最后一个元素的话,不需要再写
path.remove(path.size() - 1),而是直接path.removeLast(),因为链表有记录链表大小所以直接使用该方法就行,主要还是因为代码少哈哈。还有就是用用一个字符串数组的下标来表示每一个按键的值。
leetcode39.组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
- 输入:candidates = [2,3,6,7], target = 7
- 输出:[[2,2,3],[7]]
- 解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
这个就是在刚刚第二题的基础上 增加了元素的重复操作。那我们观察这道题,可以发现又是跟startIndex有关,因为可以元素可以重复,所以下一层的startIndex需要从本层的startIndex开始。
leetcode40.组合总和II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
- 输入: candidates = [10,1,2,7,6,1,5], target = 8,
- 输出:[[1,1,6],[1,2,5],[1,7],[2,6]]
我们观察一下这道题,发现需要查重了,而且因为只是判断集合中的元素与target之间的关系,所以这道题元素之间的顺序无关,既然无关我们可以先对原数组进行排序,然后进行查重判断。怎么判断呢,因为从这道题我们可以知道,如果是在不同层不同元素但是值相同(一个结果数组中的不同下标)是可以使用的,但是在相同层(也就是不同结果中的相同下标)不能使用相同值的。
所以我们要先弄构建一个used数组,当使用一个元素之后,给used数组的该下标上的值赋值为true。当不同层但是值相同的情况下,因为used数组下标不同,并且是上一层的该下标下的used的值为true,这一层的下标下的used值为flase,我们即可以使用。 但是相同层并且值相同的情况下,上一个遍历过的元素used[i - 1]的值为false,则需要跳过,因为该层已经使用该值了。
切割问题
leetcode131.分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
- 输入:s = “aab”
- 输出:[[“a”,“a”,“b”],[“aa”,“b”]]
这道题开始就有点区间判断那味了,就是说 你得判断一个区间内是不是回文串,那用什么来定义区间的左右两个边界呢? 刚好我们在单层判断中有一个有一个变量i,还有一个传过来的参数startIndex。因为同一层中startIndex一直是一个区间中的左边界(因为是上一层传过来的需要遍历的第一个值)而且单层遍历过程中 i 会一直 ++,所以我们让startIndex为左边界,i为右边界,然后判断是不是回文串,如果是就加到结果集,不是便continue继续 i ++。然后再进行回溯。
leetcode93.复原IP地址
这道题要注意的点是我们要怎样去写终止条件,怎么样去拼接出一个IP地址,怎么判断是不是IP地址中的一块子字符串等。
因为是IP地址,所以终止条件我们可以判断IP字符串中的 · 的个数。
拼接字符串:当判断IP地址中的子字符串符合IP地址的要求,则在该位置后面加一个 ·
判断IP地址:根据题目编写相对应代码(注意不能直接Integer.parseInt()直接强转再判断,因为00转过来是0 并且会报错)
最后这三个步骤完成后再参照上一道题的思想即可
子集问题
leetcode78.子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
- 输入:nums = [1,2,3]
- 输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
这个好像没有什么限制,并且直接套用模板直接输出每一个节点即可。
leetcode90.子集II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
- 输入:nums = [1,2,2]
- 输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
因为只需返回数组里的子集,并且可以任意顺序排列,所以直接套用上面 leetcode40.组合总和II 中的查重思想,又因为还是没啥终止条件,所以直接添加所有节点即可。
leetcode491.递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
实例1:
- 输入:nums = [4,6,7,7]
- 输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
实例2:
- 输入:nums = [4,4,3,2,1]
- 输出:[[4,4]]
这道题乍一看还以为跟上一道题一样,直接套用上面查重的思想,只是终止条件修改一下即可。
但是它不能够排序,所以不能套用上面那个公式直接判断与前一个的关系。
所以就要想一下该怎么查重:
- used数组判断元素是否在同一层有用过—>用map记录用过的元素(map里的元素不能重复)
- 递增子序列:运用刚刚区间那两道题的思想,判断结果数组中的右边界元素与剩下元素(区间中的)即将添加进来元素的大小。
考虑这两点就能做出来了。
排列问题
全排列II
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
- 输入:nums = [1,2,3]
- 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
这道题不能使用startIndex,因为我们每一次遍历要从数组的第一个元素开始,然后通过used的数组来存储已经使用过的数字,然后进行回溯遍历即可。 终止条件可以看出是当结果数组 == 原数组大小。
全排列II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
- 输入:nums = [1,1,2]
- 输出:[[1,1,2],[1,2,1],[2,1,1]]
因为是包含重复数字,并且可以按照任意顺序返回,所以是前面查重的思想 + 上一道题的遍历方法。
leetcode51. N 皇后
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
- 输入:n = 4
- 输出:[[“.Q…”,“…Q”,“Q…”,“…Q.”][“…Q.”,“Q…”,“…Q”,“.Q…”]]
- 解释:如上图所示,4 皇后问题存在两个不同的解法。
这道题跟前面差不多,唯一的不同就是for循环中每次遍历一层,在这一层中的每个元素检查是否符合规则,如果符合规则则继续往下面遍历,知道成功或者是失败并进行回溯。
判定元素是否符合,需要判断对角(45° 135°) 以及所在列。
终止条件是当遍历完最后一列后,即返回一整个二维数组即可。
答案:
public class Test01 {
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static List<List<String>> res = new ArrayList<>();
public static void main(String[] args) throws IOException {
int n = Integer.parseInt(in.readLine());
List<List<String>> result = solveNQueens(n);
out.flush();
out.close();
}
private static List<List<String>> solveNQueens(int n) {
if (n == 2 || n ==3) {
return res;
}
//初始化棋盘
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backtrack(chessboard, n, 0);
return res;
}
private static void backtrack(char[][] chessboard, int n, int row) {
if (row == n) {
res.add(arrayToList(chessboard));
return;
}
for (int col = 0; col < n; col ++) {
if (isValid(chessboard, row, col, n)) {
chessboard[row][col] = 'Q';
backtrack(chessboard, n, row + 1);
chessboard[row][col] = '.';
}
}
}
private static boolean isValid(char[][] chessboard, int row, int col, int n) {
//检查坐标点所在的那一列
for (int i = 0; i < row; i ++) {
if (chessboard[i][col] == 'Q') {
return false;
}
}
//检查对角
for (int i = row - 1, j = col - 1; i > -1 && j > -1; i --, j --) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
for (int i = row - 1, j = col + 1; i > -1 && j < n; i --, j ++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
private static List<String> arrayToList(char[][] chessboard) {
List<String> path = new ArrayList<>();
for (char[] chars : chessboard) {
path.add(String.copyValueOf(chars));
}
return path;
}
}
leetcode37. 解数独
编写一个程序,通过填充空格来解决数独问题。
一个数独的解法需遵循如下规则: 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 ‘.’ 表示。
答案被标成红色。
- 给定的数独序列只包含数字 1-9 和字符 ‘.’ 。
- 你可以假设给定的数独只有唯一解。
- 给定数独永远是 9x9 形式的。
这道题跟前面有很大的不同,就是相对于之前的模板中的单层for循环处理逻辑,因为这道题需要对二维数组中的每一个空位进行遍历,所以我们单层逻辑要转换成双层for循环,可以仔细想想,进入递归以后就是对接下来的数进行判定,因为是二维数组,所以就用双层for。
然后判定元素条件跟上面的N皇后有一点相似,判断元素所在的列跟行,还有所在的九宫格。
判断终止条件不需要写,因为遍历到最后如果没有返回false的话,就返回true 即解数独成功。
答案:
public class Test02 {
static BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out));
static List<List<String>> res = new ArrayList<>();
public static void main(String[] args) {
}
public void solveSudoku(char[][] board) {
boolean flag = backtrack(board);
}
private boolean backtrack(char[][] board) {
for (int i = 0; i < board.length; i ++) {
for (int j = 0; j < board[0].length; j ++) {
if (board[i][j] == '.') {
for (char k = '1'; k <= '9'; k ++) {
if (isValid(board, i, j, k)) {
board[i][j] = k;
if (backtrack(board)) {
return true;
}
board[i][j] = '.';
}
}
return false;
}
}
}
return true;
}
// 判断该元素所在的九宫格跟列跟行是否会重复
private boolean isValid(char[][] board, int row, int col, char k) {
// 行
for (int i = 0; i < board.length; i ++) {
if (board[row][i] == k) {
return false;
}
}
// 列
for (int j = 0; j < board[0].length; j ++) {
if (board[j][col] == k) {
return false;
}
}
int x = (row / 3) * 3;
int y = (col / 3) * 3;
//九宫格
for (int i = x; i < x + 3; i ++) {
for (int j = y; j < y + 3; j ++) {
if (board[i][j] == k) {
return false;
}
}
}
return true;
}
}
自我总结
回溯终于入门了!!
单层循环中的 i 与 startIndex递归时,不能再传错参了。。
leetcode交题不能用static 不然会报错。
大概就是这样,大家懂了吗
每一道题都在我往期的文章中有详细题解,有哪里还有模糊的可以看看
有什么不懂的评论区评论或者私信我吧!!