回溯算法

回溯算法

回溯算法思路

回溯算法:本质是递归函数,是穷举的一种方法,在中间添加了每层的执行逻辑和剪枝策略。

回溯算法用于解决:

  • 组合问题:N个数里面按一定规则找出k个数的集合,模板题。

  • 切割问题:一个字符串按一定规则有几种切割方式,注意每次startIndex传递i+1即可,但切割不能排序

  • 子集问题:一个N个数的集合里有多少符合条件的子集,这是要收集所有节点的结果,每次递归都add进result集合就行

  • 排列问题:N个数按一定规则全排列,有几种排列方式。注意不能排序。用used数组的树枝去重解即可。

  • 棋盘问题:N皇后,解数独等等。难点本质在剪枝部分。

本质上都是规则一定的情况下,遍历所有可能,但在分步解决问题的时候,可能有些分布不能达成有效的解,就会取消上一步或者上几步的计算,再通过其他可能的分布继续尝试寻找答案。

理解回溯法的关键是一个决策树的遍历过程。

在该决策树中,关键理解以下几点:

  1. 决策路径:这是纵向的,递归一层就多一个节点。
  2. 决策选择:这是横向的,在for括号里面定义的选择里,也在单层搜索中的if continue逻辑中,本质就是个剪枝。

image-20231121224909854

回溯算法模板框架:

1
2
3
4
5
6
7
8
9
10
11
12
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}

for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
  1. 确定递归函数的返回值以及参数。

    返回值按照题目的要求,一般是返回一个集合。

    参数一般包括题目给的大集合大小n、所求的小集合大小k、startIndex(递增参数,防止出现重复的集合)。

    • 什么时候用startIndex?

      startIndex适用于给出一个集合的情况下,如果多个集合(例如电话号码的字母组合),就不用startIndex。

    注意:递增参数要配合输入集合的排序使用,因为当终止条件(一般是大于某个值)判断这条路径失败后,就不会执行下面的组合了,因此集合要排序,确保不会遗漏组合。

  2. 确定回溯函数的终止条件。

    一般是返回集合的大小达到k,就说明找到了一个符合条件的样本。

  3. 确定单层搜索的过程。

    回溯法的遍历基本都是树形的结构,for循环是用来横向遍历,而递归的过程是纵向遍历。

    1. 例如:在解决组合问题时,需要从一系列数字中选择一个数字加入到当前组合中,而For表示从当前的选择集合中选择一个不同的选项。因此,For实际上就是在当前节点的水平层面上进行分支的选择。

      一般剪枝都在For括号里边进行剪枝,

    2. 一旦做出分支选择,需要继续向下做出更多的决策,递归就是实现重复逻辑操作的函数。

  4. 确定如何剪枝操作(难点)。

    剪枝就是减少遍历不必要的叶子节点,基本的是再for中的参数进行修改。

    如何去重?

    1. 树枝去重:就是决策路径不能有重复的元素,这里可以用hashset先对输入的集合去重;或者排序后直接跳过重复的元素;或者使用used数组,先排序,在相邻元素相等的情况下,当前一个元素used过,就说明在同一树枝使用过。
    2. 树层去重:就是决策路径的同一层次不能有重复的元素,可以使用used数组,先排序,在相邻元素相等的情况下,当前一个元素没used过,就说明该元素回溯过,也就是在同一树层使用过。

77. 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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){
//终止条件,如果决策路径长度满足要求就终止,并往大集合add决策路径
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
//单层决策逻辑
//1. For决定决策树的宽度,每一步面临的分支选择,这里是数字递增,这里的逻辑是去除比当前小集合元素大的大集合元素。
//2. 单层的逻辑:小集合add,递归,然后回溯,回溯的目的是退回到上一个分支进行另一个处理,因此要弹出最后一个元素
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.add(i);
combineHelper(n, k, i + 1);
path.removeLast();
}
}
}

216. 组合总和 III

找出所有相加之和为 nk 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
combineSum(n, k, 1, 0);
return result;
}
private void combineSum(int n, int k, int startIndex, int sum){
if(path.size() == k){
if(sum == n){
result.add(new ArrayList<>(path));
return;
}
}
for(int i = startIndex; i <= 9; i++){
path.add(i);
sum += i;
combineSum(n, k, i+1, sum);
path.removeLast();
sum -= i;
}
}
}

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

image-20231124170004727

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Solution {

//设置全局列表存储最后的结果
//ArrayList表明这是有顺序的
List<String> list = new ArrayList<>();

public List<String> letterCombinations(String digits) {
//这里进行一个初始判断,最原始的剪枝
if (digits == null || digits.length() == 0) {
return list;
}
//用数组对应所有的数字,0 和 1 设置为空
String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
//迭代处理
backTracking(digits, numString, 0);
return list;

}

//每次迭代获取一个字符串,所以会设计大量的字符串拼接,所以这里选择更为高效的 StringBuild
StringBuilder temp = new StringBuilder();

//digits表示传进来的数字,numString表示数字和字母的对应数组,num表示遍历到第几个数字
public void backTracking(String digits, String[] numString, int num) {
//终止条件是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++) {
//对当前num对应的字符串进行分割,并组合
temp.append(str.charAt(i));
//纵向探索剩余的可能
backTracking(digits, numString, num + 1);
//剔除末尾的继续尝试
temp.deleteCharAt(temp.length() - 1);
}
}
}

39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
//照例进行结果储存的全局变量设置
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
//主方法
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backTrack(candidates, target, 0, 0);
return result;
}
//这是个求组合问题,因此要有递增参数startIndex防止出现重复组合,sum作为求和的临时变量
private void backTrack(int[] candidates, int target,int sum, int startIndex){
//临界值是sum等于target
if(sum == target){
result.add(new LinkedList<> (path));
return;
}
//sum大于target就没必要往下了,跳出来回溯
if(path.size() == 150 || sum > target){
return;
}
for(int i = startIndex; i < candidates.length; i++){
//这是递归操作
sum += candidates[i];
path.add(candidates[i]);
backTrack(candidates,target, sum, i);
//以下是回溯操作
sum -= candidates[i];
path.removeLast();
}
}
}

40. 组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

注意:解集不能包含重复的组合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
//照例进行结果储存的全局变量设置
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
//因为candidates中的每个数字在组合中只能使用一次,设置一个标志数组记录该数字是否在以前的决策路径中被使用了
boolean[] used;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
//长度设置为candidates的长度,一一对应
used = new boolean[candidates.length];
//题解不能包含重复的组合,因此要先排序
Arrays.sort(candidates);
//开始回溯
backTrack(candidates, target, 0, 0);
return result;
}
//设置的参数还是照例的四个,包含sum和startIndex
private void backTrack(int[] candidates, int target,int sum, int startIndex){
//终止条件1,等于target,添加到结果集
if(sum == target){
result.add(new LinkedList<> (path));
return;
}
//终止条件2,sum再往下加就大了,终止决策路径
if(path.size() == 150 || sum > target){
return;
}
//每次决策阶段的操作
for(int i = startIndex; i < candidates.length; i++){
//这里有一个跳过同一决策层面中,下一个决策选择的逻辑,当前后元素相同且前面的元素被使用时
if(i > 0 && !used[i-1] && candidates[i-1] == candidates[i]){
continue;
}
//这里还是典型的集合添加,加了一个遍历标志
sum += candidates[i];
path.add(candidates[i]);
used[i] = true;
backTrack(candidates,target, sum, i+1);
//以下是回溯操作:不过不懂为什么标志设置为false就代表在正确的决策路径上了?
used[i] = false;
sum -= candidates[i];
path.removeLast();
}
}
}

131. 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。

思路:先画决策树图,确定横向的决策选择是分割的位置,纵向的决策路径是分割的次数

image-20231127152539023

  1. 确定传入参数:s, startIndex,因为不能重复,因此要有startIndex。
  2. 终止条件:切到不能再切,也就是startIndex达到最右边。
  3. 单层搜索逻辑:判断是否回文,若是,则放入集合。然后回溯,移出集合。
  4. 判断回文:双指针左右两边比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Solution {
List<List<String>> lists = new ArrayList<>();
Deque<String> deque = new LinkedList<>();

public List<List<String>> partition(String s) {
backTracking(s, 0);
return lists;
}

private void backTracking(String s, int startIndex) {
//如果起始位置大于s的大小,就说明递归到底了,这个时候deque有什么,就是最终的回文字符串
if (startIndex >= s.length()) {
lists.add(new ArrayList(deque));
return;
}
for (int i = startIndex; i < s.length(); i++) {
//如果是回文子串,则记录
if (isPalindrome(s, startIndex, i)) {
String str = s.substring(startIndex, i + 1);
deque.addLast(str);
//不是则继续往右探寻决策选择
} else {
continue;
}
//起始位置后移,保证不重复
backTracking(s, i + 1);
deque.removeLast();
}
}
//判断是否是回文串
private boolean isPalindrome(String s, int startIndex, int end) {
for (int i = startIndex, j = end; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}
}

93. 复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 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 中的任何数字。你可以按 任何 顺序返回答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Solution {
//只需要一个集合,因为需要返回字符串的集合就可以
List<String> result = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
//要对字符串修改,最好使用StringBuilder
StringBuilder sb = new StringBuilder(s);
backTracking(sb, 0, 0);
return result;
}
//回溯参数有仨,分别是字符串流、开始参数(防止重复)和递归次数统计(终止条件)
private void backTracking(StringBuilder s, int startIndex, int dotCount){
//如果当前是第四次递归,如果剩下那段也是网址格式,就add进集合,否则直接终止
if(dotCount == 3){
if(isValid(s, startIndex, s.length() - 1)){
result.add(s.toString());
}
return;
}
//横向是分割位置,纵向是分割次数
for(int i = startIndex; i < s.length(); i++){
//每次分割,都判断该段是否符合网址格式,符合就开始决策路径,不符合就换一个决策方向
if(isValid(s, startIndex, i)){
//在当前分隔符后边加入 '.'
s.insert(i + 1, '.');
//回溯
backTracking(s, i + 2, dotCount + 1);
//删除当前分隔符后边的 '.'
s.deleteCharAt(i + 1);
}else{
break;
}
}
}
//这里是具体的判断该分隔片段是否符合网址的要求的方法
private boolean isValid(StringBuilder s, int start, int end){
//这里是防止左右边界冲突
if(start > end)
return false;
//判断第一个函数不等于0
if(s.charAt(start) == '0' && start != end)
return false;
int num = 0;
//判断数字是否大于255
for(int i = start; i <= end; i++){
int digit = s.charAt(i) - '0';
num = num * 10 + digit;
if(num > 255)
return false;
}
return true;
}
}

78. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backTracking(nums, 0);
return result;
}
private void backTracking(int[] nums, int startIndex){
result.add(new ArrayList<>(path));
for(int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
backTracking(nums, i+1);
path.removeLast();
}
}
}

90. 子集 II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
//不能包含重复的子集,因此要使用标志数组
boolean[] used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
used = new boolean[nums.length];
//要先进行排序,方便将相同的元素相邻,以便后面的决策选择(树层)剪枝操作
Arrays.sort(nums);
backTracking(nums, 0);
return result;
}
private void backTracking(int[] nums, int startIndex){
//树枝(决策路径)进袋操作
result.add(new ArrayList<> (path));
for(int i = startIndex; i < nums.length; i++){
//决策选择(树层)剪枝操作
if(i > 0 && nums[i-1] == nums[i] && !used[i-1]){
continue;
}
//标志数组以便排除同一树层相同的元素
path.add(nums[i]);
used[i] = true;
backTracking(nums, i+1);
used[i] = false;
path.removeLast();
}
}
}

491. 递增子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
//子序列问题不能排序
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
//在同一个组合里,因此还是需要startIndex
backTracking(nums, 0);
return result;
}
private void backTracking(int[] nums, int startIndex){
//确保至少有两个元素
if(path.size() >= 2)
result.add(new ArrayList<>(path));
//使用hashSet去重
HashSet<Integer> hs = new HashSet<>();
for(int i = startIndex; i < nums.length; i++){
//决策路径不为空并且路径的最后一个元素大于即将add的元素,或者hashSet中含有该元素时,跳过该决策选择
//使用hashSet的原因是确保同一树层没有相同的元素
//每次递归调用backTracking方法时都会创建一个新的HashSet对象hs,因此每次递归都有其独立的hs
if(!path.isEmpty() && path.get(path.size() -1 ) > nums[i] || hs.contains(nums[i]))
continue;
hs.add(nums[i]);
path.add(nums[i]);
backTracking(nums, i + 1);
path.removeLast;
}
}
}

46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {

List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
//整体的逻辑是在同一决策路径使用过的元素不会再被使用,因此需要标志数组
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0){
return result;
}
used = new boolean[nums.length];
permuteHelper(nums);
return result;
}

private void permuteHelper(int[] nums){
//这里是终止条件,决策路径大小等于数组长度就停止
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++){
//关键的剪枝,同一路径使用过的将不会被再次使用
if (used[i]){
continue;
}
used[i] = true;
path.add(nums[i]);
//以上是决策路径往前,主要是标志遍历的元素
permuteHelper(nums);
//以下是决策路径回溯,主要是去标志遍历的元素
path.removeLast();
used[i] = false;
}
}
}

47. 全排列 II

给定一个可包含重复数字的序列 nums按任意顺序 返回所有不重复的全排列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
//要去重要全排列都得用数组
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
used = new boolean[nums.length];
//去重得先排序
Arrays.sort(nums);
backTracking(nums);
return result;

}
private void backTracking(int[] nums){
if(path.size() >= nums.length){
result.add(new ArrayList<> (path));
return;
}
for(int i = 0; i < nums.length; i++){
//这里的剪枝包括去重或者全排列两个条件
if(i > 0 && !used[i-1] && nums[i-1] == nums[i] || used[i]){
continue;
}
path.add(nums[i]);
used[i] = true;
backTracking(nums);
used[i] = false;
path.removeLast();
}
}
}

332. 重新安排行程(困难)

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 ["JFK", "LGA"]["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
//创建两个,一个放路径,一个放结果
private LinkedList<String> res;
private LinkedList<String> path = new LinkedList<>();

public List<String> findItinerary(List<List<String>> tickets) {
//这里用了Lambda表达式,表示对tickets集合中的第二个元素排序
Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1)));
//路径以JFK起始
path.add("JFK");
//设置一个标记数组,作为每次回溯的传参值,标记着哪张机票已经被使用
boolean[] used = new boolean[tickets.size()];
backTracking((ArrayList) tickets, used);
return res;
}
//这里的回溯函数不再返回空,而是有一个boolean类型的返回值
public boolean backTracking(ArrayList<List<String>> tickets, boolean[] used) {
//如果机票被用光了,就说明路径到头了,返回真
if (path.size() == tickets.size() + 1) {
res = new LinkedList(path);
return true;
}
//这里是一个for循环,遍历所有的ticket
for (int i = 0; i < tickets.size(); i++) {
//如果当前机票没有被使用,并且该机票的出发地等于path上最后一个地点,就放置目的地进路径中,并标记该机票已被使用
if (!used[i] && tickets.get(i).get(0).equals(path.getLast())) {
path.add(tickets.get(i).get(1));
used[i] = true;
//注意,这里是路径到头了才进行回溯
if (backTracking(tickets, used)) {
return true;
}
//回溯后标记当前机票没有是哟,并且在路径中移除
used[i] = false;
path.removeLast();
}
}
return false;
}
}

51. N 皇后

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Solution {
List<List<String>> res = new ArrayList<>();

public List<List<String>> solveNQueens(int n) {
//新建一个棋盘数组
char[][] chessboard = new char[n][n];
//给棋盘数组全部填充空位
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backTrack(n, 0, chessboard);
return res;
}

//有三个回溯参数,分别是棋盘大小,当前行数和棋盘数组
public void backTrack(int n, int row, char[][] chessboard) {
//终止条件,当当前行数到底时,将棋盘数组转换成ArrayList然后返回
if (row == n) {
res.add(Array2List(chessboard));
return;
}
//这里的自增在左边,一个个去试皇后
for (int col = 0;col < n; ++col) {
//如果符合规则,就将该位置修改为Q
if (isValid (row, col, n, chessboard)) {
chessboard[row][col] = 'Q';
//找到一个正确结果就向下
backTrack(n, row+1, chessboard);
//回溯,撤销操作
chessboard[row][col] = '.';
}
}

}

//这里是将char数组转换成ArrayList
public List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();

for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}


public boolean isValid(int row, int col, int n, char[][] chessboard) {
// 检查列,只需检查当前列数上边的就行
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-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}

37. 解数独

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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宫格里是否重复,这里采用了巧妙的3取余数
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;
}
}

回溯算法
https://wengerblogs.me/2025/05/13/回溯算法/
Author
Wenger
Posted on
May 13, 2025
Licensed under