算法笔记-回溯相关

image.png

回溯算法

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

一般来说,对其数组进行回溯后可得出的数据,如下[1,2,3,4]全排列,可视化后得出回溯树,可根据图像进行理解记忆。

通过上图可知需要控制其可选列表即可,递归时可以通过传递下标或者检查当前路径是否已经包含即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
List<List<Integer>> ret = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
backtrack(nums,new LinkedList<>());
return ret;
}
public void backtrack(int[] nums,LinkedList<Integer> path){
if(path.size()==nums.length){
ret.add(new ArrayList<>(path));
return;
}
for(int num:nums){
//不选已经选过的元素(控制可选列表)
if(path.contains(num))continue;
//做选择
path.add(num);
backtrack(nums,path);
//撤销选择
path.removeLast();
}
}
}

组合(相同的顺序)时的过滤方法是,建立一个visited数据组,通过判断当前分支

        // visited[i - 1] == true,说明同一树支nums[i - 1]使用过
        // visited[i - 1] == false,说明同一树层nums[i - 1]使用过
        // 如果同一树层nums[i - 1]使用过则直接跳过

参考 [全排列III]

相关题目

组合(中等)

给定两个整数 n 和 k,返回 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

public class Solution {
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> res = new ArrayList<>();
if (k <= 0 || n < k) {
return res;
}
// 为了防止底层动态数组扩容,初始化的时候传入最大长度
Deque<Integer> path = new ArrayDeque<>(k);
dfs(1, n, k, path, res);
return res;
}

private void dfs(int begin, int n, int k, Deque<Integer> path, List<List<Integer>> res) {
if (k == 0) {
res.add(new ArrayList<>(path));
return;
}
// 基础版本的递归终止条件:if (begin == n + 1) {
if (begin > n - k + 1) {
return;
}
// 不选当前考虑的数 begin,直接递归到下一层
dfs(begin + 1, n, k, path, res);
// 不选当前考虑的数 begin,递归到下一层的时候 k - 1,这里 k 表示还需要选多少个数
path.addLast(begin);
dfs(begin + 1, n, k - 1, path, res);
// 深度优先遍历有回头的过程,因此需要撤销选择
path.removeLast();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
//思路: 回溯(可选列表只能向后选取)
List<List<Integer>> ret = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
backtrack(n,k,1,new LinkedList<>());
return ret;
}
public void backtrack(int n,int k,int start,LinkedList<Integer> path){
if(path.size()==k){
ret.add(new ArrayList<>(path));
return;
}
for(int i=start;i<=n;i++){
path.add(i);
backtrack(n,k,i+1,path);
path.removeLast();
}
}
}

全排列(中等)

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

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
class Solution{
//存储结果
List<List<Integer>> res = new LinkedList<>();
//函数入口
List<List<Integer>> permute(int[] nums){
//记录走过的路径
LinkedList<Integer> integers = new LinkedList<>();
//入口
backtrack(nums,integers);
return res;
}
//回溯函数
public void backtrack(int[] nums, LinkedList<Integer> track){
//如果走过的路径包含所有的选择则完成一轮
if (nums.length==track.size()) {
res.add(new LinkedList(track));
return;
}
for (int i = 0; i <nums.length; i++) {
//排除不合法的选择
if (track.contains(nums[i]))continue;
//做选择
track.add(nums[i]);
//进入下一层决策树
backtrack(nums,track);
//撤销选择
track.removeLast();
}
}
}

全排列II(中等)

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
class Solution {
//最主要考虑怎么去重复,通过建立visited数组记录已经访问过。如果当前元素和前一个元素相同则可直接跳过本轮
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
//从小到大排序,跳过重复项
Arrays.sort(nums);
boolean[] visited = new boolean[nums.length];
backtrack(res, new ArrayList<>(), nums, visited, 0);
return res;
}

public void backtrack(List<List<Integer>> res, List<Integer> list, int[] nums, boolean[] visited, int n) {
//终止条件, 说明找到一组组合
if(n == nums.length) {
res.add(new ArrayList<>(list));
return;
}
for(int i = 0; i < nums.length; i++) {
if(visited[i] == true) continue;
// visited[i - 1] == true,说明同一树支nums[i - 1]使用过
// visited[i - 1] == false,说明同一树层nums[i - 1]使用过
// 如果同一树层nums[i - 1]使用过则直接跳过
if(i > 0 && nums[i] == nums[i - 1] && visited[i - 1] == false) continue;
//选择当前值
visited[i] = true;
list.add(nums[i]);
//递归
backtrack(res, list, nums, visited, n + 1);
//撤销选择
list.remove(list.size() - 1);
visited[i] = false;
}
}
}

Pow(x, n)(中等)

递归和迭代拆分进行计算

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public double quickMul(double x, long N) {
if (N == 0) return 1.0;//0次幂
double y = quickMul(x, N / 2);//折半计算
return N % 2 == 0 ? y * y : y * y * x;//减少计算
}
public double myPow(double x, int n) {
long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);//处理负次方幂
}
}
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 {
double quickMul(double x, long N) {
double ans = 1.0;
// 贡献的初始值为 x
double x_contribute = x;
// 在对 N 进行二进制拆分的同时计算答案
while (N > 0) {
if (N % 2 == 1) {
// 如果 N 二进制表示的最低位为 1,那么需要计入贡献
ans *= x_contribute;
}
// 将贡献不断地平方
x_contribute *= x_contribute;
// 舍弃 N 二进制表示的最低位,这样我们每次只要判断最低位即可
N /= 2;
}
return ans;
}
public double myPow(double x, int n) {
long N = n;
return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
}
}

多数元素(简单)

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素
   哈希表,排序,摩尔投票

Boyer-Moore 投票算法

思路
如果我们把众数记为 +1,把其他数记为 −1,将它们全部加起来,显然和大于 0,从结果本身我们可以看出众数比其他数多。
算法
Boyer-Moore 算法的本质和方法四中的分治十分类似。我们首先给出 Boyer-Moore 算法的详细步骤:

1
2
3
4
5
我们维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值,count 为 0;
我们遍历数组 nums 中的所有元素,对于每个元素 x,在判断 x 之前,如果 count 的值为 0,我们先将 x 的值赋予 candidate,随后我们判断 x:
如果 x 与 candidate 相等,那么计数器 count 的值增加 1;
如果 x 与 candidate 不等,那么计数器 count 的值减少 1。
在遍历完成后,candidate 即为整个数组的众数。

我们举一个具体的例子,例如下面的这个数组:
[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
在遍历到数组中的第一个元素以及每个在 | 之后的元素时,candidate 都会因为 count 的值变为 0 而发生改变。最后一次 candidate 的值从 5 变为 7,也就是这个数组中的众数。
Boyer-Moore 算法的正确性较难证明,这里给出一种较为详细的用例子辅助证明的思路,供读者参考:
首先我们根据算法步骤中对 count 的定义,可以发现:在对整个数组进行遍历的过程中,count 的值一定非负。这是因为如果 count 的值为 0,那么在这一轮遍历的开始时刻,我们会将 x 的值赋予 candidate 并在接下来的一步中将 count 的值增加 1。因此 count 的值在遍历的过程中一直保持非负。
那么 count 本身除了计数器之外,还有什么更深层次的意义呢?我们还是以数组
[7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
作为例子,首先写下它在每一步遍历时 candidate 和 count 的值:
nums:      [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
candidate:  7  7  7  7  7  7   5  5   5  5  5  5   7  7  7  7
count:      1  2  1  2  1  0   1  0   1  2  1  0   1  2  3  4
我们再定义一个变量 value,它和真正的众数 maj 绑定。在每一步遍历时,如果当前的数 x 和 maj 相等,那么 value 的值加 1,否则减 1。value 的实际意义即为:到当前的这一步遍历为止,众数出现的次数比非众数多出了多少次。我们将 value 的值也写在下方:
nums:      [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
value:      1  2  1  2  1  0  -1  0  -1 -2 -1  0   1  2  3  4
有没有发现什么?我们将 count 和 value 放在一起:
nums:      [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
count:      1  2  1  2  1  0   1  0   1  2  1  0   1  2  3  4
value:      1  2  1  2  1  0  -1  0  -1 -2 -1  0   1  2  3  4
发现在每一步遍历中,count 和 value 要么相等,要么互为相反数!并且在候选众数 candidate 就是 maj 时,它们相等,candidate 是其它的数时,它们互为相反数!
为什么会有这么奇妙的性质呢?这并不难证明:我们将候选众数 candidate 保持不变的连续的遍历称为「一段」。在同一段中,count 的值是根据 candidate == x 的判断进行加减的。那么如果 candidate 恰好为 maj,那么在这一段中,count 和 value 的变化是同步的;如果 candidate 不为 maj,那么在这一段中 count 和 value 的变化是相反的。因此就有了这样一个奇妙的性质。
这样以来,由于:
我们证明了 count 的值一直为非负,在最后一步遍历结束后也是如此;
由于 value 的值与真正的众数 maj 绑定,并且它表示「众数出现的次数比非众数多出了多少次」,那么在最后一步遍历结束后,value 的值为正数;
在最后一步遍历结束后,count 非负,value 为正数,所以它们不可能互为相反数,只可能相等,即 count == value。因此在最后「一段」中,count 的 value 的变化是同步的,也就是说,candidate 中存储的候选众数就是真正的众数 maj。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int majorityElement(int[] nums) {
int count = 0;
Integer candidate = null;

for (int num : nums) {
if (count == 0) {
candidate = num;
}
count += (num == candidate) ? 1 : -1;
}
return candidate;
}
}

电话号码的字母组合(中等)

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

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
class Solution {
/**
* 方法入口
* @param digits
* @return
*/
public List<String> letterCombinations(String digits) {
List<String> combinations = new ArrayList<String>();
if (digits.length() == 0) {
return combinations;
}
Map<Character, String> phoneMap = new HashMap<Character, String>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
backtrack(combinations, phoneMap, digits, 0, new StringBuffer());
return combinations;
}
/**
* 回溯
* @param combinations
* @param phoneMap
* @param digits
* @param index
* @param combination
*/
public void backtrack(List<String> combinations, Map<Character, String> phoneMap, String digits, int index, StringBuffer combination) {
if (index == digits.length()) {//结束条件
combinations.add(combination.toString());
} else {
char digit = digits.charAt(index);//获取当前数字
String letters = phoneMap.get(digit);//获取当前数字对应的字符串
int lettersCount = letters.length();//字符串长度
for (int i = 0; i < lettersCount; i++) {//遍历字符串
combination.append(letters.charAt(i));//追加字符到字符串末尾
backtrack(combinations, phoneMap, digits, index + 1, combination);
combination.deleteCharAt(index);//移除追加的字符串
}
}
}
}

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
class Solution{
//存放结果
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {

//用二维char数组初始化棋盘
char[][] board = new char[n][n];
for(int i = 0; i < n; i++){
Arrays.fill(board[i],'.');
}
backtrack(0,board);
return res;
}
public void backtrack(int row,char[][] board){
if(board.length == row){
res.add(charArraysToList(board));
return;
}
int n = board[row].length - 1;
for(int col = 0; col <= n; col++){
//排除不合法的选项
if (!isValid(board,row,col))
continue;
//做选择
board[row][col]='Q';
//进入下一行决策
backtrack(row+1,board);
//撤销选择
board[row][col]='.';
}

}
public static boolean isValid(char[][] board,int row , int col){
int n = board.length;
//注意由于我们是从上往下的来一个一个排列 所以不用检查下方的棋盘
//检查正上方有没有冲突(列冲突)
for (int r = 0; r< n;r++){
if ( board[r][col] =='Q')
return false;
}
//检查左上方
for (int i = row-1,j=col-1; i>=0&&j>=0;i--,j--){
if (board[i][j]=='Q')return false;
}
//检查右上方
for (int i = row-1,j=col+1; i>=0&&j<board[i].length;i--,j++){
if (board[i][j]=='Q')return false;
}
return true;
}
public static List<String> charArraysToList(char[][] board){
//将char数组变成list<String>
List<String> list = new ArrayList<>();
for (char[] c : board){
String str = new String(c);
list.add(str);
}
return list;
}
}

52. N皇后 II(hard)

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
class Solution {
//回溯
int ret;
public int totalNQueens(int n) {
boolean[][] grid = new boolean[n][n];
backtrack(grid,0);
return ret;
}
public void backtrack(boolean[][] grid,int row){
int n = grid.length;
if(row==n){
ret++;
return;
}
for(int i=0;i<n;i++){
if(valid(grid,row,i)){
grid[row][i]=true;
backtrack(grid,row+1);
grid[row][i]=false;
}
}
}
public boolean valid(boolean[][] grid,int x,int y){
int n = grid.length;
//左上
for(int i=x,j=y;i>=0&&j>=0;i--,j--)if(grid[i][j])return false;
//右上
for(int i=x,j=y;i>=0&&j<n;i--,j++)if(grid[i][j])return false;
//行
for(int i=0;i<=y;i++)if(grid[x][i])return false;
//列
for(int i=0;i<=x;i++)if(grid[i][y])return false;
return true;
}
}

最小基因变化(中等)

回溯思想、广度优先遍历

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 {
public int minMutation(String start, String end, String[] bank) {
HashSet<String> set = new HashSet<>(Arrays.asList(bank));
//不包含结果直接返回-1
if (!set.contains(end)) {
return -1;
}
//
char[] four = {'A', 'C', 'G', 'T'};
Queue<String> queue = new LinkedList<>();
queue.offer(start);//
set.remove(start);//集合中移除
int step = 0;
while (queue.size() > 0) {
step++;
for (int count = queue.size(); count > 0; --count) {
char[] temStringChars = queue.poll().toCharArray();
for (int i = 0, len = temStringChars.length; i < len; ++i) {
char oldChar = temStringChars[i];
for (int j = 0; j < 4; ++j) {
temStringChars[i] = four[j];
String newGenetic = new String(temStringChars);
if (end.equals(newGenetic)) {
return step;
} else if (set.contains(newGenetic)) {
set.remove(newGenetic);
queue.offer(newGenetic);
}
}
temStringChars[i] = oldChar;
}
}
}
return -1;
}
}

回溯思想、深度优先遍历

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
class Solution {
int minStepCount = Integer.MAX_VALUE;//设置默认值
//函数入口
public int minMutation(String start, String end, String[] bank) {
dfs(new HashSet<String>(), 0, start, end, bank);
return (minStepCount == Integer.MAX_VALUE) ? -1 : minStepCount;
}
//深度优先遍历
private void dfs (HashSet<String> step, int stepCount,String current, String end, String[] bank) {
//满足条件进行比较赋值
if (current.equals(end)) minStepCount = Math.min(stepCount, minStepCount);
//遍历基因库
for (String str: bank) {
//遍历
int diff = 0;
for (int i = 0; i < str.length(); i++)
if (current.charAt(i) != str.charAt(i)) if (++diff > 1) break;//记录基因不同的位置
//当前有基因不同 && 基因库中不包含当前基因序列(已使用过处理)
if (diff == 1 && !step.contains(str)) {
step.add(str);//添加当前序列
dfs(step, stepCount + 1, str, end, bank);//进入下一层遍历,步数+1,并且使用当前序列作为current
step.remove(str);//移除当前序列
}
}
}
}

在每个树行中找最大值(中等)

广度优先

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public List<Integer> largestValues(TreeNode root) {
List<Integer> ans = new ArrayList<Integer>();
LinkedList<TreeNode> queue = new LinkedList<>();
if(!Objects.isNull(root))queue.add(root);

while(!queue.isEmpty()){
int size = queue.size();
int max = queue.getFirst().val;
for(int i=0;i<size;i++){
TreeNode node = queue.poll();
if(node.val>max)max=node.val;
if(node.left!=null)queue.add(node.left);
if(node.right!=null)queue.add(node.right);
}
ans.add(max);
}
return ans;
}
}

306.累加数(mid)

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 {
//回溯+剪枝
boolean valid;
public boolean isAdditiveNumber(String num) {
if(num.length()==1)return false;
char[] nums = num.toCharArray();
int n = nums.length;
int num1 = 0;
//找到一个合法的序列然后进行回溯
for(int i=0,o=n;i<o;i++){//num1=[start,j)
if(i==0&&nums[i]=='0')o=i+1;
num1=num1*10+nums[i]-'0';
int num2 = 0;
for(int j=i+1,l=n;j<l;j++){//num2=[j,k)
if(j==i+1&&nums[j]=='0')l=j+1;//首位为0只能是0
num2=num2*10+nums[j]-'0';
int num3 = 0;
for(int k=j+1,m=n;k<m;k++){//num3=[k,)
if(k==j+1&&nums[k]=='0')m=k+1;
num3=num3*10+nums[k]-'0';
if(num1+num2==num3)backtrack(nums,num2,num3,k+1);
if(valid) return valid;
}
}
}
return valid;
}
public void backtrack(char[] nums,int num1,int num2,int start){
int n = nums.length;
if(start==n){
valid = true;
return;
}
//在[start,n)中查找num=num2+num3的下标
int num3 = 0;
for(int i=start,l=n;i<l;i++){
if(i==start&&nums[i]=='0')break;
num3=num3*10+nums[i]-'0';
if(num1+num2==num3){
backtrack(nums,num2,num3,i+1);
}
}
}
}

93 复原 IP 地址(mid)

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 {
//思路:回溯
//四段地址,每段长度划分1-3位,不能0开头
private List<String> ans = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
backtrace(s,0,new LinkedList<>());
return ans;
}
public void backtrace(String s,int index,LinkedList<String> path){
if(path.size()==4){
StringBuilder sb = new StringBuilder();
for(int i=0;i<4;i++){
sb.append(path.get(i));
if(i!=3)sb.append(".");
}
ans.add(sb.toString());
return;
}
for(int i=1;i<4&&index+i<=s.length();i++){
String addr = s.substring(index,index+i);
if(path.size()==3)addr = s.substring(index);
if(addr.length()>3||(addr.length()>1&&addr.charAt(0)=='0')||Integer.valueOf(addr)>255)continue;
path.add(addr);
backtrace(s,index+i,path);
path.removeLast();
if(path.size()==3)break;
}
}
}

括号生成

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
class Solution {
//统计
public List<String> generateParenthesis(int n) {
List<String> res = new ArrayList<String>();
generate(res, "", 0, 0, n);
return res;
}
//count1统计“(”的个数,count2统计“)”的个数
public void generate(List<String> res , String ans, int count1, int count2, int n){
if(count1 > n || count2 > n) return;
if(count1 == n && count2 == n) res.add(ans);
if(count1 >= count2){
String ans1 = new String(ans);
generate(res, ans+"(", count1+1, count2, n);//必须先左括号先,否则不是有效的括号
generate(res, ans1+")", count1, count2+1, n);
}
}
//思路:暴力枚举(回溯)每次可选'('或者')',当选择的个数达到2*n判断是否为有效括号
List<String> ret = new ArrayList<>();
int size;
public List<String> generateParenthesis(int n) {
if(n==0)return ret;
size = 2 * n;
backtrack(new StringBuilder());
return ret;
}

public void backtrack(StringBuilder path){
if(path.length()==size){
if(valid(path))ret.add(path.toString());
return;
}
path.append('(');
backtrack(path);
path.deleteCharAt(path.length()-1);
path.append(')');
backtrack(path);
path.deleteCharAt(path.length()-1);
}
//验证是否是有效的括号
public boolean valid(StringBuilder sb){
int blance = 0;
for(int i=0;i<sb.length();i++){
char c = sb.charAt(i);
if(c=='(')++blance;
else --blance;
if(blance<0)return false;
}
return blance==0;
}

组合总和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
//思路:回溯
private List<List<Integer>> ans = new ArrayList<>();
private int[] candidates;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
this.candidates = candidates;
backtrace(0,target,new LinkedList<Integer>());
return ans;
}
public void backtrace(int index,int target,LinkedList<Integer> path){
if (target<0)return;
if(0==target){
ans.add(new ArrayList<>(path));
return;
}
for (int i=index;i<candidates.length;i++){
path.add(candidates[i]);
target-=candidates[i];
backtrace(i,target,path);
path.removeLast();
target+=candidates[i];
}
}
}

组合总和 II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
List<List<Integer>> list=new ArrayList<>();
List<Integer> path=new ArrayList<>();
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates,target,0);
return list;
}
private void dfs(int[] candidates, int target,int index){
if(target==0){
list.add(new ArrayList<>(path));
return;
}
for(int i=index;i<candidates.length;i++){
if(candidates[i]<=target){
//去重复
if(i>index&&candidates[i]==candidates[i-1])continue;
path.add(candidates[i]);
dfs(candidates,target-candidates[i],i+1);
path.remove(path.size()-1);
}
}
}
}

单词搜索

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
class Solution {
//思路:回溯
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};//方向
int m,n;//长度
public boolean exist(char[][] board, String word) {
m = board.length;
n = board[0].length;
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(board[i][j]==word.charAt(0)){
if(dfs(board,word,0,i,j))return true;
}
}
}
return false;
}
public boolean dfs(char[][] board,String word,int index,int x,int y){
if(index==word.length()-1)return true;
char c =board[x][y];board[x][y]='-';//不可选择选过的节点
for(int[] dir:dirs){
int newX = dir[0]+x,newY = dir[1]+y;
//满足条件
if(newX<m&&newX>=0&&newY<n&&newY>=0&&word.charAt(index+1)==board[newX][newY]){
if(dfs(board,word,index+1,newX,newY))return true;
}
}
board[x][y]=c;//重新赋值
return false;
}
}

黄金矿工

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 {
//思路:dfs
private int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
private int max,cloums,rows;
public int getMaximumGold(int[][] grid) {
rows = grid.length;
cloums = grid[0].length;
for(int i=0;i<rows;i++){
for(int j=0;j<cloums;j++){
if(grid[i][j]>0)dfs(grid,i,j,0);
}
}
return max;
}
public void dfs(int[][] grid,int x,int y,int count){
int gridv = grid[x][y];
count += gridv;
max=Math.max(max,count);
grid[x][y] = 0;
for(int[] dir:dirs){
int newX = dir[0]+x,newY = dir[1]+y;
if(newX>=0&&newX<rows&&newY>=0&&newY<cloums&&grid[newX][newY]>0){
dfs(grid,newX,newY,count);
}
}
grid[x][y] = gridv;
}
}

37.解数独(hard)

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
class Solution {
//思路:回溯(预处理:已经存在的数和可填入的'.'的位置)
private boolean[][] line = new boolean[9][9];//某行的某个数是否已经存在
private boolean[][] column = new boolean[9][9];//列
private boolean[][][] block = new boolean[3][3][9];//3x3方格
private boolean valid = false;
private List<int[]> spaces = new ArrayList<int[]>();

public void solveSudoku(char[][] board) {
for (int i = 0; i < 9; ++i) {
for (int j = 0; j < 9; ++j) {
if (board[i][j] == '.') {
spaces.add(new int[]{i, j});//可填入列表
} else {
int digit = board[i][j] - '0' - 1;//取其下标
//设置某行、某列和某个方格的某个数是否已经存在,[i / 3][j / 3]可计算得出第digit个方格
line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = true;
}
}
}
dfs(board, 0);//dfs遍历
}

public void dfs(char[][] board, int pos) {
if (pos == spaces.size()) {
valid = true;//全部正确停止回溯
return;
}
int[] space = spaces.get(pos);//当前'.'坐标
int i = space[0], j = space[1];
//只有可能是1-9(方便下标所以使用0-8)
for (int digit = 0; digit < 9 && !valid; ++digit) {
//行、列、3x3小方格都不能有digit+1
if (!line[i][digit] && !column[j][digit] && !block[i / 3][j / 3][digit]) {
//做选择
line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = true;
board[i][j] = (char) (digit + '0' + 1);
//进入下一层递归
dfs(board, pos + 1);
//撤销选择
line[i][digit] = column[j][digit] = block[i / 3][j / 3][digit] = false;

}
}
}
}

不同的二叉搜索树 II

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
class Solution {
//思路:
public List<TreeNode> generateTrees(int n) {
return n==0?new LinkedList<TreeNode>():generateTrees(1,n);
}
//
public List<TreeNode> generateTrees(int start,int end){
List<TreeNode> res = new LinkedList<TreeNode>();
if(start>end){
res.add(null);
return res;
}
for(int i=start;i<=end;i++){
List<TreeNode> subLeftTree = generateTrees(start,i-1);
List<TreeNode> subRightTree = generateTrees(i+1,end);
for(TreeNode left:subLeftTree){
for(TreeNode right:subRightTree){
TreeNode node = new TreeNode(i);
node.left = left;
node.right = right;
res.add(node);
}
}
}
return res;
}
}

78. 子集(mid)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
//思路:回溯+控制可选列表
List<List<Integer>> ret = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
int n = nums.length;
if(n==0)return ret;
backtrack(nums,new LinkedList<Integer>(),0);
return ret;
}
public void backtrack(int[] nums,LinkedList<Integer> path,int index){
ret.add(new ArrayList<>(path));
for(int i=index;i<nums.length;i++){
path.add(nums[i]);
backtrack(nums,path,i+1);
path.removeLast();
}
}
}

90. 子集 II(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
//思路:回溯(索引可选列表+去重)
List<List<Integer>> ret = new ArrayList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
int n = nums.length;
boolean[] visited = new boolean[n];
Arrays.sort(nums);//排序
backtrack(nums,visited,new LinkedList<>(),0);
return ret;
}
public void backtrack(int[] nums,boolean[] visited,LinkedList<Integer> path,int index){
ret.add(new ArrayList<>(path));
for(int i=index;i<nums.length;i++){
if(visited[i]||(i>0&&nums[i]==nums[i-1]&&!visited[i-1]))continue;
visited[i]=true;
path.add(nums[i]);
backtrack(nums,visited,path,i+1);
path.removeLast();
visited[i]=false;
}
}
}

剑指 Offer 38. 字符串的排列(mid)

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
class Solution {
List<String> ret = new ArrayList<>();
public String[] permutation(String s) {
char[] arr = s.toCharArray();
int n = arr.length;
Arrays.sort(arr);
boolean[] visited = new boolean[n];
backtrack(arr,visited,new StringBuilder());
return ret.toArray(new String[ret.size()]);
}
public void backtrack(char[] arr,boolean[] visited,StringBuilder sb){
if(sb.length()==arr.length){
ret.add(sb.toString());
return;
}
for(int i=0;i<arr.length;i++){
//visited[i-1]==true表示当前arr[i]和arr[i-1]在同一分支上
//visited[i-1]==false表示当前arr[i]和arr[i-1]在同一层上,因为不能重复,所以只能取一个
if(visited[i]||(i>0&&arr[i]==arr[i-1]&&!visited[i-1]))continue;
visited[i]=true;
sb.append(arr[i]);
backtrack(arr,visited,sb);
sb.deleteCharAt(sb.length()-1);
visited[i]=false;
}
}
}

89. 格雷编码(mid)

1
2
3
4
5
6
7
8
9
10
//序列之间每两个数的二进制表示中只有一位不同,同时序列第一位和最后一位对应的二进制也只有一位不同
class Solution {
public List<Integer> grayCode(int n) {
List<Integer> ret = new ArrayList<Integer>();
for (int i = 0; i < 1 << n; i++) {
ret.add((i >> 1) ^ i);
}
return ret;
}
}

131. 分割回文串(mid)

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<String>> ret = new ArrayList<>();
public List<List<String>> partition(String s) {
StringBuilder sb = new StringBuilder(s);
backtrack(sb,new LinkedList<String>(),0);
return ret;
}
public void backtrack(StringBuilder sb,LinkedList<String> path,int start){
if(start==sb.length())ret.add(new ArrayList<>(path));
for(int i=start+1;i<=sb.length();i++){
if(valid(sb,start,i-1)){
path.add(sb.substring(start,i));
backtrack(sb,path,i);
path.removeLast();
}
}
}

//是否是回文串
public boolean valid(StringBuilder sb,int left,int right){
while(left<right){
if(sb.charAt(left)!=sb.charAt(right))return false;
left++;right--;
}
return true;
}
}

842. 将数组拆分成斐波那契序列(mid)

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
class Solution {
public List<Integer> splitIntoFibonacci(String num) {
List<Integer> list = new ArrayList<Integer>();
backtrack(list, num, num.length(), 0, 0, 0);
return list;
}
//回溯
public boolean backtrack(List<Integer> list, String num, int length, int index, int sum, int prev) {
//回溯结束条件当遍历的下标大小等于字符串长度结束
if (index == length) return list.size() >= 3;
//分为当前数、上一个数、
long currLong = 0;
//开始下标
for (int i = index; i < length; i++) {
//如果首位是0只能是一位
if (i > index && num.charAt(index) == '0') {
break;
}
//计算当前数
currLong = currLong * 10 + num.charAt(i) - '0';
//溢出直接结束
if (currLong > Integer.MAX_VALUE) {
break;
}
//强转
int curr = (int) currLong;
//剪枝f(n)=f(n-1)+f(n-2),小于大于都不满足
if (list.size() >= 2) {
if (curr < sum) {
continue;
} else if (curr > sum) {
break;
}
}
list.add(curr);
//进入下一轮回溯
if (backtrack(list, num, length, i + 1, prev + curr, curr)) {
return true;
} else {
list.remove(list.size() - 1);
}
}
return false;
}
}

2044. 统计按位或能得到最大值的子集数目(mid)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
//思路:回溯+位运算
int max;
int ret;
public int countMaxOrSubsets(int[] nums) {
int n = nums.length;
for(int i=0;i<n;i++)max|=nums[i];
dfs(nums,0,0);
return ret;
}
public void dfs(int[] nums,int result,int p){
if(p==nums.length){
if(result==max)ret++;
return;
}
dfs(nums,result,p+1);
dfs(nums,result|nums[p],p+1);
}
}

140. 单词拆分 II(hard)

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
class Solution {
//思路:回溯
List<String> ret = new ArrayList<>();
List<String> wordDict;
public List<String> wordBreak(String s, List<String> wordDict) {
this.wordDict = wordDict;
int n = s.length();
backtrack(s,new StringBuilder(),0);
return ret;
}
public void backtrack(String s,StringBuilder sb,int start){
if(start==s.length()){
sb.deleteCharAt(sb.length()-1);
ret.add(sb.toString());
return;
}
for(int i=start+1;i<=s.length();i++){
String str = s.substring(start,i);
if(wordDict.contains(str)){
int index = sb.length();
sb.append(str).append(' ');
backtrack(s,sb,i);
sb.delete(index,sb.length());
}
}
}
}

60. 排列序列(hard)

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
public class Solution {
//思路:每次都确定一个数直到最后一个数
/**
* 记录数字是否使用过
*/
private boolean[] used;
/**
* 阶乘数组
*/
private int[] factorial;
private int n;
private int k;
public String getPermutation(int n, int k) {
this.n = n;
this.k = k;
calculateFactorial(n);
// 查找全排列需要的布尔数组
used = new boolean[n + 1];
StringBuilder path = new StringBuilder();
dfs(0, path);
return path.toString();
}
/**
* @param index 在这一步之前已经选择了几个数字,其值恰好等于这一步需要确定的下标位置
* @param path
*/
private void dfs(int index, StringBuilder path) {
if (index == n) return;
// 计算还未确定的数字的全排列的个数,第 1 次进入的时候是 n - 1
int cnt = factorial[n - 1 - index];
for (int i = 1; i <= n; i++) {
if (used[i]) continue;//已经使用过了
if (cnt < k) {//cnt(区间个数)必须大于等于k才有可能在这个区间内,否则在下一个区间搜索
k -= cnt;
continue;
}
path.append(i);
used[i] = true;
dfs(index + 1, path);
// 注意 1:不可以回溯(重置变量),算法设计是「一下子来到叶子结点」,没有回头的过程
// 注意 2:这里要加 return,后面的数没有必要遍历去尝试了
return;
}
}
/**
* 计算阶乘数组
* @param n
*/
private void calculateFactorial(int n) {
factorial = new int[n + 1];
factorial[0] = 1;
for (int i = 1; i <= n; i++) {
factorial[i] = factorial[i - 1] * i;
}
}
}

5289. 公平分发饼干(mid)

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 {
//查找出分配的最均匀的一组数据
//回溯+剪枝
int min = Integer.MAX_VALUE;
int ret = Integer.MAX_VALUE;
int[] cookies;
public int distributeCookies(int[] cookies, int k) {
this.cookies = cookies;
distributeCookies(0,new int[k]);
return ret;
}
//cookies = [6,1,3,2,2,4,1,2], k = 3
//三个槽位 [],[],[]
//三个槽位 [6],[],[]
//三个槽位 [],[6],[]
//三个槽位 [],[],[6]
private void distributeCookies(int index,int[] sum) {
if (index==cookies.length){
int max = Arrays.stream(sum).max().getAsInt();
int min = Arrays.stream(sum).min().getAsInt();
if (max == 0 || min == 0 )return;
if (max-min<this.min){
ret = max;
this.min = max - min;
}
return;
}
for (int i = 0; i < sum.length; i++) {
sum[i]+=cookies[index];
if(sum[i]<ret){//剪枝,如果当前sum[i]大于ret那说明sum[i]求出来的差值肯定大于ret,所以无需再继续
distributeCookies(index+1,sum);
}
sum[i]-=cookies[index];
}
}
}

1723. 完成所有工作的最短时间(mid)

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 {
//二分+回溯+剪枝
public int minimumTimeRequired(int[] jobs, int k) {
Arrays.sort(jobs);
int low = 0, high = jobs.length - 1;
while (low < high) {
int temp = jobs[low];
jobs[low] = jobs[high];
jobs[high] = temp;
low++;
high--;
}
int l = jobs[0], r = Arrays.stream(jobs).sum();
while (l < r) {
int mid = (l + r) >> 1;
if (check(jobs, k, mid)) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}

public boolean check(int[] jobs, int k, int limit) {
int[] workloads = new int[k];
return backtrack(jobs, workloads, 0, limit);
}

public boolean backtrack(int[] jobs, int[] workloads, int i, int limit) {
if (i >= jobs.length) {
return true;
}
int cur = jobs[i];
for (int j = 0; j < workloads.length; ++j) {
if (workloads[j] + cur <= limit) {
workloads[j] += cur;
if (backtrack(jobs, workloads, i + 1, limit)) {
return true;
}
workloads[j] -= cur;
}
// 如果当前工人未被分配工作,那么下一个工人也必然未被分配工作
// 或者当前工作恰能使该工人的工作量达到了上限
// 这两种情况下我们无需尝试继续分配工作
if (workloads[j] == 0 || workloads[j] + cur == limit) {
break;
}
}
return false;
}
}

473. 火柴拼正方形(mid)

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
public boolean makesquare(int[] matchsticks) {
int totalLen = Arrays.stream(matchsticks).sum();
if (totalLen % 4 != 0) {
return false;
}
Arrays.sort(matchsticks);//排序
//降序
for (int i = 0, j = matchsticks.length - 1; i < j; i++, j--) {
int temp = matchsticks[i];
matchsticks[i] = matchsticks[j];
matchsticks[j] = temp;
}
//四条边
int[] edges = new int[4];
//进行dfs
return dfs(0, matchsticks, edges, totalLen / 4);
}
//dfs回溯
public boolean dfs(int index, int[] matchsticks, int[] edges, int len) {
//如果下标==matchsticks.length说明数组已经遍历完成,此时返回true
if (index == matchsticks.length) {
return true;
}
//遍历四条边,判断当前火柴可以加在那个边上,再进行dfs
for (int i = 0; i < edges.length; i++) {
//加在当前边上
edges[i] += matchsticks[index];
//如果小于等于len且在进行下一轮dfs
if (edges[i] <= len && dfs(index + 1, matchsticks, edges, len)) {
//如果dfs返回true则直接返回ture无需再遍历了
return true;
}
//从当前边上移除(取消选择)
edges[i] -= matchsticks[index];
}
return false;
}

相关资料


算法笔记-回溯相关
https://mikeygithub.github.io/2020/04/22/yuque/算法笔记-回溯相关/
作者
Mikey
发布于
2020年4月22日
许可协议