本文的全部内容源自:https://github.com/youngyangyang04/leetcode-master
这里展示所有回溯相关算法均为Java递归算法。
有关回溯需要注意的问题:
回溯法一般可以解决如下几种问题:
(1)组合问题:N个数里面按一定规则找出k个数的集合
(2)切割问题:一个字符串按一定规则有几种切割方式
(3)子集问题:一个N个数的集合里有多少符合条件的子集
(4)排列问题:N个数按一定规则全排列,有几种排列方式
(5)棋盘问题:N皇后,解数独等等
其中组合不强调元素顺序,排列强调元素顺序。
对于组合问题,如果是在一个集合中求组合的话,就需要startIndex,例如下题1、2;如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:下题3。
对于求和问题,排序之后加剪枝是常见的套路。
组合问题是无序的,去过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始;而排列问题,就要从0开始,因为集合是有序的,{1,2}与{2,1}是两个集合。
一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
回溯三部曲:
- 回溯函数模板返回值以及参数( 参数不太好确定,一般先写逻辑,然后需要什么参数,就填什么参数)
- 回溯函数终止条件(一般是到达叶节点,也就是找到满足条件的一个答案,然后把答案存起来,结束本层递归)
- 回溯搜索的遍历过程(回溯一般是在集合中递归搜索,集合的大小构成树的宽度,递归的深度构成树的深度)
回溯法模板如下:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
5. 组合(给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。)
这里给出的版本是剪枝之后的,剪枝的位置是在for循环中i的终止条件处,剪枝的依据是如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
combineHelper(n, k, 1);
return result;
}
/**
* 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex
* @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
*/
private void combineHelper(int n, int k, int startIndex){
//终止条件
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
//这里用到了剪枝策略,本质上是这么回事,需要k个数,已经有path.size()个数,目前还能取n-startIndex+1个数,所以就看if(n-startIndex+1 > k-path.size())是否成立,不成立直接break就好了。
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.add(i);
combineHelper(n, k, i + 1);
path.removeLast();
}
}
}
- 组合总和(找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。)
class Solution {
static List<List<Integer>> res = new ArrayList<>();
static List<Integer> list2 = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
res.clear();
list2.clear();
backtracking2(n, k, 0, 1);
return res;
}
public static void backtracking2(int targetSum, int k, int sum, int startIndex){
if(list2.size() == k){
if(sum == targetSum) res.add(new ArrayList<>(list2));
return ;
}
for(int i = startIndex; i <= 9; i ++){
sum += i;
list2.add(i);
backtracking2(targetSum, k, sum, i + 1);
sum -= i;
list2.remove(list2.size() - 1);
}
}
}
- 电话号码的字母组合
(在写这道题时发现自己的思路比较明确,但是无法转化到代码,说明一是对于某些数据结构掌握的不够扎实,二是回溯掌握的还是不够)
class Solution {
//设置全局列表存储最后的结果
List<String> list = new ArrayList<>();
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) {
return list;
}
//初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
//迭代处理
backTracking(digits, numString, 0);
return list;
}
//每次迭代获取一个字符串,所以会设计大量的字符串拼接,所以这里选择更为高效的 StringBuild
StringBuilder temp = new StringBuilder();
//比如digits如果为"23",num 为0,则str表示2对应的 abc
public void backTracking(String digits, String[] numString, int num) {
//遍历全部一次记录一次得到的字符串
if (num == digits.length()) {
list.add(temp.toString());
return;
}
//str 表示当前num对应的字符串
String str = numString[digits.charAt(num) - '0'];
for (int i = 0; i < str.length(); i++) {
temp.append(str.charAt(i));
//c
backTracking(digits, numString, num + 1);
//剔除末尾的继续尝试
temp.deleteCharAt(temp.length() - 1);
}
}
}
- 组合总和(给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。)
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1: 输入:candidates = [2,3,6,7], target = 7, 所求解集为: [ [7], [2,2,3] ]
示例 2: 输入:candidates = [2,3,5], target = 8, 所求解集为: [ [2,2,2,2], [2,3,3], [3,5] ]
class Solution {
static List<List<Integer>> res = new ArrayList<>();
static List<Integer> list2 = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
res.clear();
list2.clear();
Arrays.sort(candidates);
backtracking2(candidates, target, 0, 0);
return res;
}
public void backtracking2(int[] candidates, int target, int sum, int startIndex){
if(sum > target)
return ;
if(sum == target)
{
res.add(new ArrayList<>(list2));
return ;
}
//这里剪枝的时候需要注意是小于等于,因为等于的话必须得把当前的这个数加入进去
for(int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i ++)
{
list2.add(candidates[i]);
sum += candidates[i];
//这里注意不是i+1,而是i,因为元素可以重复使用
//这也是这道题与上面组合求和的不同的关键所在
backtracking2(candidates, target, sum, i);
sum -= candidates[i];
list2.remove(list2.size() - 1);
}
}
}
- 复原IP地址(给定数字字符串,求出所有合法的IP地址)
class Solution {
List<String> result = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
if (s.length() > 12) return result; // 算是剪枝了
backTrack(s, 0, 0);
return result;
}
// startIndex: 搜索的起始位置, pointNum:添加逗点的数量
private void backTrack(String s, int startIndex, int pointNum) {
if (pointNum == 3) {// 逗点数量为3时,分隔结束
// 判断第四段⼦字符串是否合法,如果合法就放进result中
if (isValid(s,startIndex,s.length()-1)) {
result.add(s);
}
return;
}
for (int i = startIndex; i < s.length(); i++) {
if (isValid(s, startIndex, i)) {
s = s.substring(0, i + 1) + "." + s.substring(i + 1); //在str的后⾯插⼊⼀个逗点
pointNum++;
backTrack(s, i + 2, pointNum);// 插⼊逗点之后下⼀个⼦串的起始位置为i+2
pointNum--;// 回溯
s = s.substring(0, i + 1) + s.substring(i + 2);// 回溯删掉逗点
} else {
break;
}
}
}
// 判断字符串s在左闭⼜闭区间[start, end]所组成的数字是否合法
private Boolean isValid(String s, int start, int end) {
if (start > end) {
return false;
}
if (s.charAt(start) == '0' && start != end) { // 0开头的数字不合法
return false;
}
int num = 0;
for (int i = start; i <= end; i++) {
if (s.charAt(i) > '9' || s.charAt(i) < '0') { // 遇到⾮数字字符不合法
return false;
}
num = num * 10 + (s.charAt(i) - '0');
if (num > 255) { // 如果⼤于255了不合法
return false;
}
}
return true;
}
}
- n皇后问题
class Solution {
// n 为输入的棋盘大小
// row 是当前递归到棋牌的第几行了
List<List<String>> result = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
result.clear();
char[][] chessboard = new char[n][n];
for(char[] c : chessboard){
Arrays.fill(c, '.');
}
backtracking(n, 0, chessboard);
return result;
}
public void backtracking(int n, int row, char[][] chessboard){
if(row == n){
result.add(Array2List(chessboard));
return ;
}
for(int col = 0; col < n; col ++){
if(isValid(row, col, chessboard, n)){//验证合法就可以放
chessboard[row][col] = 'Q'; //放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = '.'; //回溯,撤销皇后
}
}
}
public List Array2List(char[][] chessboard){
List<String> list = new ArrayList<>();
for(char[] c : chessboard){
list.add(String.copyValueOf(c));
}
return list;
}
boolean isValid(int row, int col, char[][] chessboard, int n){
int count = 0;
//检查列
for(int i = 0; i < row; i ++){ //这是一个剪枝
if(chessboard[i][col] == 'Q'){
return false;
}
}
//检查45°角是否有皇后
for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i --, j --){
if(chessboard[i][j] == 'Q'){
return false;
}
}
//检查135°角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i --, j ++){
if(chessboard[i][j] == 'Q'){
return false;
}
}
return true;
}
}
- 解数独问题
这一题中用到了二维递归
class Solution {
public void solveSudoku(char[][] board) {
solveSudokuHelper(board);
}
private boolean solveSudokuHelper(char[][] board){
//「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,
// 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」
for (int i = 0; i < 9; i++){ // 遍历行
for (int j = 0; j < 9; j++){ // 遍历列
if (board[i][j] != '.'){ // 跳过原始数字
continue;
}
for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适
if (isValidSudoku(i, j, k, board)){
board[i][j] = k;
if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回
return true;
}
board[i][j] = '.';
}
}
// 9个数都试完了,都不行,那么就返回false
return false;
// 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解!
// 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」
}
}
// 遍历完没有返回false,说明找到了合适棋盘位置了
return true;
}
/**
* 判断棋盘是否合法有如下三个维度:
* 同行是否重复
* 同列是否重复
* 9宫格里是否重复
*/
private boolean isValidSudoku(int row, int col, char val, char[][] board){
// 同行是否重复
for (int i = 0; i < 9; i++){
if (board[row][i] == val){
return false;
}
}
// 同列是否重复
for (int j = 0; j < 9; j++){
if (board[j][col] == val){
return false;
}
}
// 9宫格里是否重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val){
return false;
}
}
}
return true;
}
}