文章目录
- 1. 基本概念
- 2. 组合问题
- 3. 组合总和Ⅲ
- 4. 电话号码的字母组合
- 5. 分割回文串
- 6. 复原IP地址
- 7. 子集
- 8. 全排列
- 9. 全排列Ⅱ
- 10. N皇后
- 11. 解数独
1. 基本概念
递归和回溯相辅相成。只要有递归,就会有回溯。
回溯法是一种纯暴力的搜索,并不是一种高效的算法。
回溯法可以解决的问题:
- 组合问题
- 切割问题
- 子集问题
- 排列问题
- 棋盘问题
如何理解回溯法
回溯法,都可以抽象为一个n叉树形结构。树的宽度一般就是要处理的集合的大小,树的深度就是递归的深度。
回溯法的模板
回溯法一般没有返回值,方法一般命名为backtracking
。
确定终止条件,收集结果。
处理完终止条件,进入单层搜索的逻辑。
void backtracking(Paramters){if(终止条件){收集结果;return;}for(集合的元素集){处理节点;递归函数;回溯操作;}return;
}
2. 组合问题
力扣第77题。
问题描述
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。你可以按 任何顺序 返回答案。
样例
输入:n = 4, k = 2
输出:
[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4],
]输入:n = 1, k = 1
输出:[[1]]
题解
比如给定n=4
,k=2
。则
回溯三部曲:
- 确定递归函数的参数及返回值
- 递归的终止条件
- 单层递归的逻辑
代码实现
import java.util.ArrayList;
import java.util.List;class Solution {List<List<Integer>> result = new ArrayList<>(); // 存储最终结果List<Integer> path = new ArrayList<>(); // 存储一条路径上的结果public List<List<Integer>> combine(int n, int k) {backtracking(n, k, 1);return result;}public void backtracking(int n,int k,int startIndex){// 递归终止条件 收集结果if (path.size()==k) {result.add(new ArrayList<>(path));return;}// 单层递归的逻辑// i<=n+1-(k-path.size()) 由 if n-i+1<k-path.size() return; 得来for(int i=startIndex;i<=n+1-(k-path.size());i++){ // 剪枝 path.add(i); // 处理节点backtracking(n, k, i+1); // 递归path.remove(path.size()-1); // 回溯}}
}
在回溯做剪枝操作时,一般从循环的范围下手,尽量缩短循环的范围。
3. 组合总和Ⅲ
本题为力扣216题。
问题描述
找出所有相加之和为 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
没有其他符合的组合了。输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
代码实现
import java.util.ArrayList;
import java.util.List;class Solution {List<List<Integer>> result = new ArrayList<>(); // 存储最终结果List<Integer> path = new ArrayList<>(); // 存储当前路径上的节点public List<List<Integer>> combinationSum3(int k, int n) {backtracking(k, n, 1);return result;}public void backtracking(int k,int n,int startIndex){// 结束条件 收集结果int sum = path.stream().mapToInt(Integer::intValue).sum();if (path.size()==k && sum==n) {result.add(new ArrayList<>(path));}// if path.size()+9-i+1 < k -> i<=path.size()+10-kfor(int i=startIndex;i<=path.size()+10-k;i++){ // 剪枝if (path.stream().mapToInt(Integer::intValue).sum()>=n) { // 剪枝return;}path.add(i); // 处理节点backtracking(k, n, i+1); // 递归path.remove(path.size()-1); // 回溯}}
}
4. 电话号码的字母组合
本题为力扣第17题。
问题描述
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
输入输出样例
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]输入:digits = ""
输出:[]
题解
代码实现
import java.util.ArrayList;
import java.util.List;class Solution {List<String> result = new ArrayList<>();StringBuffer path = new StringBuffer();public List<String> letterCombinations(String digits) {if ("".equals(digits)) {return new ArrayList<String>();}List<List<Character>> chars = new ArrayList<>();char[] charArray = digits.toCharArray();for (char c : charArray) {switch (c) {case '2':List<Character> l2 = new ArrayList<Character>();l2.add('a');l2.add('b');l2.add('c');chars.add(l2);break;case '3':List<Character> l3 = new ArrayList<Character>();l3.add('d');l3.add('e');l3.add('f');chars.add(l3);break;case '4':List<Character> l4 = new ArrayList<Character>();l4.add('g');l4.add('h');l4.add('i');chars.add(l4);break;case '5':List<Character> l5 = new ArrayList<Character>();l5.add('j');l5.add('k');l5.add('l');chars.add(l5);break;case '6':List<Character> l6 = new ArrayList<Character>();l6.add('m');l6.add('n');l6.add('o');chars.add(l6);break;case '7':List<Character> l7 = new ArrayList<Character>();l7.add('p');l7.add('q');l7.add('r');l7.add('s');chars.add(l7);break;case '8':List<Character> l8 = new ArrayList<Character>();l8.add('t');l8.add('u');l8.add('v');chars.add(l8);break;case '9':List<Character> l9 = new ArrayList<Character>();l9.add('w');l9.add('x');l9.add('y');l9.add('z');chars.add(l9);break;default:break;}}backtracking(chars, 0);return result;}public void backtracking(List<List<Character>> chars,int index){if (path.length() == chars.size()) {String string = String.valueOf(path);result.add(string);return;}List<Character> list = chars.get(index);for(int i=0;i<list.size();i++){path.append(list.get(i));backtracking(chars, index+1);path.deleteCharAt(path.length()-1);}}
}
5. 分割回文串
本题为力扣第131题。
问题描述
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
输入输出样例
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]输入:s = "a"
输出:[["a"]]
题解
代码实现
import java.util.ArrayList;
import java.util.List;class Solution {List<List<String>> result = new ArrayList<>(); // 存储返回结果List<String> cutList = new ArrayList<>(); // 存储当前路径分割方案public List<List<String>> partition(String s) {backtracking(s, 0);return result;}public void backtracking(String s,int startIndex){if (startIndex == s.length()) {result.add(new ArrayList<>(cutList)); // 收集结果return;}for(int i=startIndex+1;i<=s.length();i++){String temp = s.substring(startIndex, i);if (isHuiwen(temp)) {cutList.add(temp);backtracking(s, i);cutList.remove(cutList.size()-1);}}}// 判断是否为回文串public boolean isHuiwen(String str){int start = 0;int end = str.length()-1;while(start<end){if (str.charAt(start)!=str.charAt(end)) {return false;}start++;end--;}return true;}
}
6. 复原IP地址
本题为力扣第93题。
问题描述
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
- 例如:
"0.1.2.201"
和"192.168.1.1"
是 有效 IP 地址,但是"0.011.255.245"
、"192.168.1.312"
和"192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
输入输出样例
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
题解
代码实现
import java.util.ArrayList;
import java.util.List;class Solution {int cutNum = 0;List<String> result = new ArrayList<>();StringBuffer ip = new StringBuffer();public List<String> restoreIpAddresses(String s) {backtracking(s,0);return result;}public void backtracking(String s ,int startIndex){if (startIndex == s.length() && cutNum==4) {StringBuffer resStr = new StringBuffer(ip);resStr.deleteCharAt(resStr.length()-1);result.add(resStr.toString());return;}for(int i=startIndex+1;i<=s.length();i++){String temp = s.substring(startIndex,i);if ((temp.length()>=2 && temp.startsWith("0")) || temp.length()>3 || cutNum>3) {return;}int strInt = Integer.valueOf(temp);if (strInt>=0 && strInt<=255) {int ipLength = ip.length();cutNum++;ip.append(temp + ".");backtracking(s,i);ip.delete(ipLength,ip.length());cutNum--;}}}
}
7. 子集
本题为力扣第78题。
问题描述
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集不能 包含重复的子集。你可以按 任意顺序 返回解集。
输入输出样例
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]输入:nums = [0]
输出:[[],[0]]
题解
代码实现
import java.util.ArrayList;
import java.util.List;class Solution {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();boolean endFlag = false;public List<List<Integer>> subsets(int[] nums) {backtracking(nums, 0);return result;}public void backtracking(int[] nums,int startIndex){if (endFlag) { // 结束条件result.add(new ArrayList<>(path)); // 收集结果return;}for(int i=startIndex-1;i<nums.length;i++){if (i==startIndex-1) { // 空集endFlag = true;backtracking(nums, i+1);endFlag = false;}else{path.add(nums[i]);if (i==nums.length-1) {endFlag = true;}backtracking(nums, i+1);endFlag = false;path.remove(path.size()-1);}}}
}
8. 全排列
本题为力扣第46题。
问题描述
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
输入输出样例
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]输入:nums = [0,1]
输出:[[0,1],[1,0]]
题解
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;class Solution {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> permute(int[] nums) {List<Integer> list = Arrays.stream(nums).boxed().collect(Collectors.toList());backtracking(list);return result;}public void backtracking(List<Integer> nums){if (nums.size()==0) {result.add(new ArrayList<>(path));return;}for(int i=0;i<nums.size();i++){Integer current = nums.remove(i);path.add(current);backtracking(nums);path.remove(path.size()-1);nums.add(i,current);}}
}
9. 全排列Ⅱ
本题为力扣第47题。
问题描述
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
输入输出样例
输入:nums = [1,1,2]
输出:
[[1,1,2],[1,2,1],[2,1,1]]输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
题解
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;class Solution {List<List<Integer>> result = new ArrayList<>();List<Integer> path = new ArrayList<>();public List<List<Integer>> permuteUnique(int[] nums) {Arrays.sort(nums);List<Integer> list = Arrays.stream(nums).boxed().collect(Collectors.toList());backtracking(list);return result;}public void backtracking(List<Integer> list){if (list.size() == 0) {result.add(new ArrayList<>(path));return;}for(int i=0;i<list.size();i++){if (i>0 && list.get(i)==list.get(i-1)) {continue;}Integer current = list.remove(i);path.add(current);backtracking(list);path.remove(path.size()-1);list.add(i,current);}}
}
10. N皇后
本题为力扣第51题。
问题描述
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
输入输出样例
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。输入:n = 1
输出:[["Q"]]
题解
代码实现
import java.util.ArrayList;
import java.util.List;class Solution {List<List<String>> result = new ArrayList<>(); //结果int[][] chessboard; // 棋盘public List<List<String>> solveNQueens(int n) {chessboard = new int[n][n]; // 初始化棋盘,默认0填充backtracking(n,0);return result;}public void backtracking(int n,int row){if (row==n) {List<String> r = new ArrayList<>();for (int[] chess : chessboard) { // 棋盘转化为字符串形式String temp = "";for(int i=0;i<chess.length;i++){if (chess[i]==0) {temp = temp + ".";}else if(chess[i]==1){temp = temp + "Q";}}r.add(temp);}result.add(r); // 收集结果return;}for(int i=0;i<n;i++){boolean isOk = isVaild(row, i, n); // 判断该位置是否可以放置if (!isOk) {continue;}chessboard[row][i] = 1; // 处理当前节点backtracking(n, row+1); // 递归chessboard[row][i] = 0; // 回溯}}// 判断是否可以放置public boolean isVaild(int row,int i,int n){for(int j=0;j<row;j++){ // 判断所在列是否存在皇后if (chessboard[j][i]==1) {return false;}}int currentRow = row;int currentCol = i;// 判断左上是否存在皇后while(currentRow>=0 && currentCol>=0){if (chessboard[currentRow][currentCol]==1) {return false;}currentRow--;currentCol--;}currentRow = row;currentCol = i;// 判断右上是否存在皇后while(currentRow>=0 && currentCol<n){if (chessboard[currentRow][currentCol]==1) {return false;}currentRow--;currentCol++;}return true;}
}
11. 解数独
本题为力扣第37题。
问题描述
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.'
表示。
输入输出样例
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
题解
整体思路:
- 遍历每个位置,判断该位置是否可以放置当前数字(1-9遍历)
判断当前九宫格是否可以放置,处理逻辑:
- 首先获取当前位置所处的九宫格
- 根据当前所处的九宫格得到当前九宫格左上角的元素的位置坐标
- 遍历当前九宫格,判断是否存在相同数值
代码实现
class Solution {char[][] boardMain;char[][] result;public void solveSudoku(char[][] board) {boardMain = board;result = new char[9][9];backtracking(0, 0);}public void backtracking(int row,int col){if (row==9) {boardMain = result; // 收集结果return;}// 当前位置原来已经存在数字,直接存入,不做处理,处理下一位置char current = boardMain[row][col];if (current!='.') {boardMain[row][col] = current;result[row][col] = current;int[] rc = handler(row, col);backtracking(rc[0], rc[1]);return;}for(int n=1;n<=9;n++){boolean vaild = isVaild(row, col, n); // 判断当前位置是否可以放置if (vaild) {boardMain[row][col] = (char)(n+'0'); // 处理当前位置result[row][col] = (char)(n+'0');int[] handler = handler(row, col);backtracking(handler[0], handler[1]); // 递归boardMain[row][col] = '.'; // 回溯}}}// 获取递归传入的行数和列数public int[] handler(int row,int col){int[] r = new int[2];if (col==8) {row++;col = 0;}else{col++;}r[0] = row;r[1] = col;return r;}// 判断该 位置 是否可以放置public boolean isVaild(int row,int col,int n){// 处理行char[] currentRow = boardMain[row];char nc = (char)(n+'0');for (char c : currentRow) {if (c==nc) {return false;}}// 处理列for(int i=0;i<9;i++){char c = boardMain[i][col];if (c==nc) {return false;}}// 处理九宫格int ar = (int)Math.ceil((row+1)/3.0);int ac = (int)Math.ceil((col+1)/3.0);int startRow = (ar-1)*3;int startCol = (ac-1)*3;for(int i=startRow;i<startRow+3;i++){for(int j=startCol;j<startCol+3;j++){if (boardMain[i][j]==nc) {return false;}}}return true;}
}