一、介绍回溯
回溯算法一本质上是一种深度优先搜索。它的核心思路是:从一个初始状态出发,逐步构建解,在每一步尝试所有可能的选择;如果当前路径满足条件,就继续深入;如果不满足或已经无法得到正确结果,就撤销上一步操作(回溯),再尝试其他选择。
这个过程类似在一棵"决策树"中不断向下搜索,并在不符合条件时返回上层节点继续尝试其他分支。回溯算法通常用于求解组合、排列、子集、棋盘问题(如N皇后)、路径搜索等问题。
其基本模板包括三个部分:路径(当前已经选择的结果)、选择列表(当前可以做的选择)以及结束条件(满足条件时收集结果)。实现时一般通过递归完成,每次递归前做选择,递归后撤销选择。
void backtrack(参数){
if(结束条件){
记录结果;
return;
}
for(选择 : 选择列表){
做选择;
backtrack(参数);
撤销选择; // 回溯
}
}
二、实战
216. 组合总和 III
这道题是一道中等难度的题,我们首先要搞清楚组合和排列,在组合中,数字的位置不同不产生影响。比如[2,1,6]与[6,2,1]在这题中是一样的,所以我们可以使用回溯。

我们每次递归大概是这个样子。代码如下:
class Solution {
List<List<Integer>> result=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
backtracking(n,k,0,1);
return result;
}
public void backtracking(int targetSum,int k,int sum,int startIndex){
if(sum>targetSum){
return;
}
if(path.size()==k){
if(targetSum==sum){
result.add(new ArrayList<>(path));
}
}
for(int i=startIndex;i<=9;i++){
sum+=i;
path.add(i);
backtracking(targetSum,k,sum,i+1);
sum-=i;
path.remove(path.size()-1);
}
}
}
result 用于存放最终所有满足条件的结果集合,path 用于记录当前递归路)。在 backtracking 方法中,首先进行剪枝判断:如果当前路径的和 sum 已经大于目标值 targetSum,说明这条路径不可能再得到正确结果,直接返回。接着判断终止条件,当 path 中元素个数等于 k 时,如果此时路径和正好等于目标值,就将当前路径加入结果集中。然后进入 for 循环,从 startIndex 开始遍历到 9,依次尝试选择每一个数字。每次选择一个数字 i,就将其加入路径,并更新当前和 sum,然后递归进入下一层搜索,同时将起始位置设为 i+1,以保证不会重复使用同一个数字。递归结束后,通过 sum -= i 和移除路径最后一个元素来撤销本次选择,这一步就是回溯的核心操作,使程序能够继续尝试其他可能的组合。
90. 子集 II
这题也是一个中等难度的题,重点在去重,我们需要横向去重,也就是说,如果
nums = [1,2,2],如图

但是如果是1,2,3,4,2这种又不需要去重,这是为什么呢?
在回溯算法中,去重的本质是避免"同一树层"出现重复选择,而不是禁止路径中出现相同元素。对于排序后的数组,如果两个相同元素出现在同一层(即作为兄弟节点),选择它们会产生重复结果,因此需要跳过;但如果是在不同层(即一个是另一个的子节点),则表示是不同位置的选择,是合法的组合,不需要去重。因此,像 [1,2,2] 这种同层重复需要去重,而像路径中连续选择两个 2(不同层)则是允许的。代码如下:
class Solution {
List<List<Integer>> result=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
boolean[] used=new boolean[nums.length];
backtracking(nums,0,used);
return result;
}
public void backtracking(int[] nums,int startIndex,boolean[] used){
result.add(new ArrayList<>(path));
for(int i=startIndex;i<nums.length;i++){
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
continue;
}
path.add(nums[i]);
used[i]=true;
backtracking(nums,i+1,used);
path.remove(path.size()-1);
used[i]=false;
}
}
}
整体思路是基于回溯算法,通过深度优先搜索枚举所有可能的子集组合,同时利用去重策略避免生成重复结果。程序中使用 result 保存最终结果集合,path 用来记录当前构造的子集。在 backtracking 方法中,每进入一层递归,都会先将当前路径加入结果集,这是因为子集问题中,每一个状态(节点)本身就是一个合法子集。随后通过 for 循环,从 startIndex 开始遍历数组,依次尝试选择每一个元素。为了避免重复子集,代码中使用了关键的去重逻辑:当当前元素与前一个元素相同(nums[i] == nums[i-1]),并且前一个元素在当前树层没有被使用(used[i-1] == false)时,说明这是同一层的重复选择,需要跳过,从而实现"树层去重"。如果可以选择当前元素,就将其加入 path,并标记为已使用,然后递归进入下一层搜索,起始位置更新为 i+1,保证每个元素只被使用一次。递归结束后,通过移除路径最后一个元素并重置 used[i] 来完成回溯操作,使程序可以继续探索其他分支。整个过程本质是在一棵决策树上进行搜索,并通过排序加条件判断有效避免重复结果。
47. 全排列 II
这题代码与上一题相似,不同的是这一题是排列,那么如[1,2]和[2,1]在这里是完全不同的,所以在for循环中,我们不再从startIndex开始,而是从0开始,同时还是要进行横向去重。代码如下:
class Solution {
List<List<Integer>> result=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
boolean[] used=new boolean[nums.length];
backtracking(nums,used);
return result;
}
public void backtracking(int[] nums,boolean[] used){
if(path.size()==nums.length){
result.add(new ArrayList<>(path));
return;
}
for(int i=0;i<nums.length;i++){
if(i>0&&nums[i]==nums[i-1]&&used[i-1]==false){
continue;
}
if(used[i]==true){
continue;
}
used[i]=true;
path.add(nums[i]);
backtracking(nums,used);
used[i]=false;
path.remove(path.size()-1);
}
}
}
首先在 permuteUnique 方法中对数组进行排序,这是去重的关键前提,因为只有相同元素相邻,后续的剪枝条件才成立。然后定义一个 used 布尔数组,用来标记每个位置的元素在当前路径中是否已经被使用过,避免同一个元素被重复选取。回溯函数中,当路径 path 的长度等于数组长度时,说明已经形成一个完整排列,就将其加入结果集中。
在递归过程中,通过遍历每个位置的元素进行选择,如果当前元素已经被使用(used[i] == true),就直接跳过;而真正实现"去重"的关键在于这一句:当 i > 0 且当前元素 nums[i] 等于前一个元素 nums[i-1],并且前一个元素在当前递归路径中没有被使用(used[i-1] == false)时,说明这两个相同元素处于同一层递归中,如果此时再选择当前元素,就会产生重复排列,因此需要用 continue 跳过。这个条件本质是在控制"同一层不能重复选相同元素",而不是限制不同层的选择。
51. N 皇后
这道题的难点在于如何去搜这个二维的棋盘,如果我们采用暴力的解法,那么就要有n个for循环,这样太多了,所以我们可以使用回溯算法。

代码如下:
class Solution {
List<List<String>> result=new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard=new char[n][n];
for(int i=0;i<n;i++){
Arrays.fill(chessboard[i],'.');
}
backtracking(chessboard,n,0);
return result;
}
public void backtracking(char[][] chessboard,int n,int row){
if(row==n){
result.add(arrayToList(chessboard));
return;
}
for(int i=0;i<n;i++){
if(isValid(row,i,chessboard,n)){
chessboard[row][i]='Q';
backtracking(chessboard,n,row+1);
chessboard[row][i]='.';
}
}
}
public boolean isValid(int row,int col,char[][] chessboard,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>=0&&j>=0;i--,j--){
if(chessboard[i][j]=='Q')
return false;
}
for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
if(chessboard[i][j]=='Q')
return false;
}
return true;
}
public List<String> arrayToList(char[][] chessboard){
List<String> list=new ArrayList<>();
for(char[] row:chessboard){
list.add(new String(row));
}
return list;
}
}
首先在 solveNQueens 方法中初始化一个 n × n 的棋盘,用字符 '.' 表示空位,然后从第 0 行开始调用回溯函数 backtracking。回溯函数的含义是:在当前 row 行,尝试在每一列放置一个皇后。如果当前行已经等于 n,说明前面每一行都成功放置了皇后,此时构成一个完整解,就把当前棋盘转换成字符串列表加入结果集中。
在回溯过程中,通过一个 for 循环遍历当前行的每一列位置,对于每个位置 (row, col),都会调用 isValid 方法判断是否可以放皇后。这个判断包括三部分:第一,检查当前列上方是否已有皇后;第二,检查左上对角线是否有皇后;第三,检查右上对角线是否有皇后。因为回溯是按行从上往下进行的,所以只需要检查"上方"的情况即可,不需要检查下方。
如果某个位置合法,就在该位置放置 'Q',然后递归处理下一行;当递归返回时,再将该位置恢复为 '.',这一步叫"回溯",目的是尝试其他可能的放法。通过这种"尝试---递归---撤销"的过程,可以枚举出所有可能的解。
最后,arrayToList 方法的作用是把当前棋盘从二维字符数组转换为 List<String> 的形式,这是题目要求的输出格式。每一行被转换成一个字符串,所有行组成一个解。