LeetCode 11 - 回溯

回溯本质上是暴力穷举算法。解决一个回溯问题,实际上就是一个决策树的遍历过程。需要考虑三个问题:

  1. 路径:已经做出的选择是什么?
  2. 选择列表:当前可以做什么选择?
  3. 结束条件:到达决策树底层的判断条件是什么?

解决回溯问题的思路是:

  1. 画出递归树,据此找出 状态变量 (递归函数中的参数)。
  2. 确定回溯的结束条件,满足条件则将这一路径加入结果,并直接返回当前递归。
  3. 根据状态变量,确定选择列表,针对每一个选项:
    1. 判断是否需要剪枝
    2. 做出选择
    3. 递归调用,进入递归树的下一层。
    4. 撤销选择

相关问题:

类型 题目
子集、组合 子集、子集 II、组合、组合总和、组合总和 II
排列 全排列、全排列 II、字符串的全排列、字母大小写全排列
搜索 解数独、单词搜索、N皇后、分割回文串、二进制手表

子集问题、组合问题、分割问题都可以抽象为一棵决策树,组合问题和分割问题都是收集决策树的叶结点,而子集问题是收集所有结点。子集也是一种组合问题。

因为组合问题中选过的元素不会重复选取,所以开始索引为 start ;而排列问题中,索引从 0 开始。(选择列表的 for 循环)

子集问题#

78. 子集#

问题:输入一个 不包含重复数字 的数组,输出这些数字的所有子集。

分析: {1,2,3} 的子集相当于在 {1,2} 的每个子集中加入数字 3。可以用回溯来做。

  1. 状态变量:因为是组合而非排列,所以需要记录一个 当前遍历位置,在此位置之前的元素都不能再被重复选择,这就是该问题的状态变量 start
  2. 结束条件:因为路径上的中间值都是符合条件的组合,而非只有路径终点的值才符合条件,所以没有特殊的结束条件,或者说,当遍历完数组时就自动结束了。
  3. 选择列表:从 start 到数组结尾的每一个数。
List<List<Integer>> result = new ArrayList<>();

public List<List<Integer>> subsets(int[] nums) {
  ArrayList<Integer> track = new ArrayList<>();
  backtrack(nums, 0, track);
  return result;
}

void backtrack(int[] nums, int start, ArrayList<Integer> track) {
  // 没有结束条件
  result.add(new ArrayList<Integer>(track));
  // 选择列表
  for (int i = start; i < nums.length; i++) {
    // 做出选择
    track.add(nums[i]);
    // 递归处理下一层
    backtrack(nums, i+1, track);
    // 撤销选择
    track.remove(track.size()-1);
  }
}

90. 子集 II#

问题:输入一个 包含重复数字 的数组,输出这些数字的所有子集。

和上一个问题的不同之处在于,这里需要加上剪枝条件。递归树如下图所示,其中橙色的是重复分支,需要剪除。这个剪枝条件是:当前位置和上一个位置的元素一样

List<List<Integer>> result = new ArrayList<>();

List<List<Integer>> subsetsWithDuplicates(int[] nums) {
    ArrayList<Integer> track = new ArrayList<>();
    Arrays.sort(nums);
    backtrack(nums, 0, track);
    return result;
}

void backtrack(int[] nums, int start, ArrayList<Integer> track) {
  result.add(new ArrayList<Integer>(track));
  for (int i = start; i < nums.length; i++) {
    // 剪枝(去重)
    if (i > start && nums[i] == nums[i-1]) continue;
    // 做出选择
    track.add(nums[i]);
    // 递归处理下一层
    backtrack(nums, i+1, track);
    // 撤销选择
    track.remove(track.size() - 1);
  }
}

去重操作还可以使用(全局)used 数组来实现,这时剪枝条件为 i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false 。(见 全排列 II 问题的解答)

491. 递增子序列#

这个问题的决策树如下:

可以看到两个特点:

  • 同一层不会取出相同元素
  • 所取元素小于子序列中的最后一个元素时,当前回溯就结束了。

第一个特点表示需要去重,第二个特点是结束条件。

前面子集问题的去重操作是:if (i > start && nums[i] == nums[i-1]) continue; 但是本问题中数组不能被排序,所以不能这样去重。从决策树上可以看出:同一父节点下的同层上选择过的数字就不能再次被选择了。

去重操作的实现:在每一层决策树中,使用一个 HashSet 记录已经加入 path 的数字,在遍历到每一个数字时,如果当前数字已经在 HashSet 中,直接跳过。

注意只有在选择元素时做了 set.add(nums[i]) 操作,在回溯时并没有执行相应的 set.remove(nums[i]) 操作,这是因为我们只考虑当前层兄弟结点之间不能重复选择,不考虑更深层的操作。

List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
    ArrayList<Integer> path = new ArrayList<>();
    backtrack(nums, 0, path);
    return result;
}

public void backtrack(int[] nums, int start, ArrayList<Integer> path) {
    if (path.size() >= 2) {
        result.add(new ArrayList<>(path));
    }
    // 在当前层回溯中使用
    HashSet<Integer> set = new HashSet<>();
    for (int i = start; i < nums.length; i++) {
        if (set.contains(nums[i])) continue;
        if (!path.isEmpty() && nums[i] < path.get(path.size() - 1))
            continue;
        path.add(nums[i]);
        set.add(nums[i]);
        backtrack(nums, i + 1, path);
        path.remove(path.size()-1);
    }
}

698. 划分为k个相等的子集#

问题:给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。

分析:可以想象有 k 个桶,要往这些桶中分别放入总和为 sum/k 的数。所以可以按照 DFS 来遍历这些桶,找出满足条件的放置方式。

三要素的确定:

  • 路径:前面所有桶中分别放入的数字之和。
  • 选择列表:剩余可供挑选的数字。
  • 结束条件:找到符合条件的放置方式。

可以预先将数组排序,这样在判断当前桶是否可以放入下一个数字时,一旦找到过大的数字,那么后面的数字肯定也不满足要求,省去很多不必要的计算。

boolean canPartitionKSubsets(int[] nums, int k) {
    // 排除一些简答情况
    int sum = 0;
    for (int num : nums) sum += num;
    if (sum % k != 0) return false;
    if (k == 1) return true;

    sum /= k;
    Arrays.sort(nums);
    if (nums[nums.length-1] > sum) return false;
    int[] buckets = new int[k];
    Arrays.fill(buckets, sum);
    return dfs(nums, nums.length-1, buckets);
}

boolean dfs(int[] nums, int cur, int[] buckets) {
    // 结束条件:nums 遍历完成,说明前面的所有数都正好可以放入桶里,所有桶的值都变成 0
    if (cur == -1) return true;
    // 遍历每个桶
    for (int i = 0; i < buckets.length; i++) {
        // 剪枝条件:相邻桶的值一样,只考虑其中最后一个桶
        if (i > 0 && buckets[i] == buckets[i-1]) continue;
        // 如果剩余容量不够,则跳过(nums 按照从大到小顺序处理)
        if (buckets[i] < nums[cur]) continue;
        // 做选择:将当前数放入当前桶
        buckets[i] -= nums[cur];
        // 处理下一个数
        if (dfs(nums, cur-1, buckets)) return true;
        // 如果没有找到符合条件的方式,则撤销选择
        buckets[i] += nums[cur];
    }
    // 当前数字放入哪个桶都行不通
    return false;
}

组合问题#

77. 组合#

求出 1..n 中所有的 k-组合。

和排列不同之处在于,每一轮递归中选择列表不再是固定的,每一层递归都会「消耗」一个数字,层次越深,可供选择的数字越少。这里用 for(int i = start; i <= n; i++) 来实现。

递归树如下:

 List<List<Integer>> result = new ArrayList<>();
 // track 不再作为参数传递给 backtrack,而是全局变量
 List<Integer> track = new ArrayList<>();
 
 List<List<Integer>> combine(int n, int k) {
     backtrack(n, k, 1);
     return result;
 }
 
 void backtrack(int n, int k, int start) {
     // 结束条件
     if(track.size() == k) {
         result.add(new ArrayList<>(track));
         return;
     }
     // 选择列表
     for(int i = start; i <= n; i++) {
         track.add(i);            // 做出选择
         backtrack(n, k, i+1);  // 进入下一层
         track.remove(track.size()-1);    // 撤销选择
     }
 }

其实在遍历到数组中某个位置之后(start 之后的子数组长度不够 k 了),就可以停止了,据此可以进行一定的优化:

void backtrack(int n, int k, int start) {
    // ... 结束条件
    // 选择列表
    for (int i = start; i <= n - (k-track.size()) + 1) {
        // ...
    }
}

39. 组合总和#

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

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

条件:

  • 数组不包含重复元素。
  • 同一个数字可以重复选择。

方法:搜索回溯

对于这类 寻找可行解 的题,都可以用搜索回溯的方法来解决。

要素的确定:

  • 状态变量:已遍历到数组的哪个位置 pos,以及还剩多少份额 target 需要组合。
  • 路径:已加入组合的数字列表。
  • 终止条件: target <= 0 或者 candidates 数组已被全部用完。
  • 选择列表:从当前位置开始到数组末尾的每一个元素(注意到每个数字可以被无限制重复选取,因此搜索下标的开始位置和上一层一样)。
List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
    ArrayList<Integer> combine = new ArrayList<>();
    backtrack(candidates, 0, target, combine);
    return result;
}

void backtrack(int[] candidates, int start, int target, ArrayList<Integer> combine) {
    // 结束条件
    if (start == candidates.length) return;
    if (target == 0) {
        result.add(new ArrayList<Integer>(combine));
        return;
    }
    // 选择列表
    for (int i = start; i < candidates.length; i++) {
        if (target < candidates[i]) break;
        combine.add(candidates[i]); // 做选择
                // 递归搜索:仍然从 i 位置开始,确保元素可以被重复选取
        backtrack(candidates, i, target-candidates[i], combine); 
        combine.remove(combine.size()-1); // 撤销选择
    }
}

40. 组合总和 II#

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

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

条件:

  • 数组中包含重复元素。
  • 每个数字只能被选择一次

整体思路和 组合总和 问题一样,但是因为条件的两处不同,需要做出对应的修改:

  • 每个数字只能被选择一次:下一层递归的开始位置不能是 start ,而是 start+1
  • 因为存在重复元素,所以需要对重复组合去重,为此先对数组排序,然后对于每一个重复的元素,只在第一次出现的时候进行处理,后续重复出现时直接跳过。

从决策树可以看出,在同一层中重复出现的元素会被剪掉(因为这个分支是重复的),而不同层之间允许出现重复,这一剪枝条件为: i > start && nums[i] == nums[i-1] 。也可以用 used 数组来实现去重,如果前一个相同元素刚被撤销选择 nums[i] == nums[i-1] && used[i-1] == false,则跳过。(见 全排列 II 问题的解答)

List<List<Integer>> result;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    result = new ArrayList<>();
    Arrays.sort(candidates);
    backtrack(candidates, target, 0, new ArrayList<>());
    return result;
}
void backtrack(int[] candidates, int target, int start, ArrayList<Integer> path) {
    if (target == 0) {
        result.add(new ArrayList<Integer>(path));
        return;
    }
    if (start == candidates.length) return;
    // 选择列表
    for (int i = start; i < candidates.length && candidates[i] <= target; i++) {
        // 去重(在同一层中相邻的两个相同数字,只对第一个进行搜索)
                if (i > start && candidates[i] == candidates[i-1]) continue;
        path.add(candidates[i]);
        // 递归搜索:起始位置为 i+1,确保每个元素只被选取一次
        backtrack(candidates, target - candidates[i], i+1, path);
        path.remove(path.size()-1);
    }
}

216. 组合总和 III#

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

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

返回 所有可能的有效组合的列表 (不能包含重复组合) 。

这里的 k 规定了树的深度,只要到达第 k 层就要返回。

List<List<Integer>> result;

public List<List<Integer>> combinationSum3(int k, int n) {
    result = new ArrayList<>();
    backtrack(k, n, new ArrayList<>(), 1);
    return result;
}

void backtrack(int k, int n, ArrayList<Integer> path, int start) {
    // 在第 k 层结束
        if (path.size() == k) {
        if (n == 0)
            result.add(new ArrayList<>(path));
        return;
    }
    // 选择列表
    for (int i = start; i <= 9; i++) {
        path.add(i);
                // 递归:从 i + 1 开始,确保每个元素只被选取一次
        backtrack(k, n-i, path, i + 1);
        path.remove(path.size() - 1);
    }
}

17. 电话号码的字母组合#

电话拨号键盘上每个数字都对应了三到四个字母,例如 1a,b,c2d,e,f ,那么给定数字串 "12" ,一个字母组合就是从 “abc”"def" 中分别挑出一个字母组合起来。给定数字串,求出所有这样的字母组合。

方法:回溯

要素的确定:

  • 路径:当前已遍历数字对应的字母序列;
  • 状态变量:当前正在访问的数字的索引;
  • 选择列表:当前数字对应的三到四个字母;
  • 结束条件:遍历完所有数字,序列长度达到数字串的长度。
List<String> result = new ArrayList<>();
Map<Character, String> map = new HashMap<>();
List<String> letterCombinations(String digits) {
    if(digits.length() == 0) return new ArrayList<>();
    map.put('1', "abc");
    // ...
    StringBuilder builder = new StringBuilder();
    backtrack(digits, 0, builder);
    return result;
}

void backtrack(String digits, int start, StringBuilder builder) {
    // 结束条件
    if(builder.length()==digits.length()) {
        result.add(builder.toString());
        return;
    }
    // 选择列表
    for(char c : map.get(digits.charAt(start)).toCharArray()) {
        builder.append(c); // 做出选择
        backtrack(digits, start+1, builder); // 进入下一层
        builder.delete(builder.length()-1, builder.length()); // 撤销选择
    }
}

分割问题#

93. 复原 IP 地址#

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。

回溯要素确定:

  • 确定状态变量start 用来记录当前遍历位置,另外因为 IP 地址最多只能有四段,所以还需要一个 segId 变量来记录当前正在遍历哪一段。
  • 结束条件:确定状态变量之后就能确定结束条件了:segId == 4 说明确定了全部四段,或者 start == s.length() ,说明遍历完了字符串。当这两个条件同时满足时,说明这个路径是合法的,将其加入结果集;只满足一个说明当前路径不合法,直接返回即可。
  • 选择列表:从当前遍历位置 start 开始,延伸到后续 255 范围内。如果 start 位置是 0 ,则这个 0 单独成段,因为不能有前缀 0.
List<String> result = new ArrayList<>();
int[] addrs = new int[4]; // 存储每一段的值(path)

public List<String> restoreIpAddresses(String s) {
    backtrack(s, 0, 0);
    return result;    
}

void backtrack(String s, int start, int segId) {
    // 结束条件
    if (segId == 4) {
        if (start == s.length()) {
            StringBuilder builder = new StringBuilder();
            for (int num : addrs) {
                builder.append(num).append(".");
            }
            builder.deleteCharAt(builder.length()-1);
            result.add(builder.toString());
        }
        return;
    } else if (start == s.length()) {
        return;
    }
    // 选择列表
    if (s.charAt(start) == '0') {
        addrs[segId] = 0;
        backtrack(s, start + 1, segId + 1);
    }
    // 下面其实是 else 段,但可以不写 else
    int num = 0;
    for (int i = start; i < s.length(); i++) {
        num = num * 10 + (s.charAt(i) - '0');
        if (num > 0 && num <= 255) {
            addrs[segId] = num;
            backtrack(s, i + 1, segId + 1);
        } else {
            break;
        }
    }
}

131. 分割回文串#

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

要素确定:

  • 状态变量:切分位置 start ,当前已切分出的回文子串列表 path
  • 结束条件:字符串遍历完成: s.length() == start
  • 选择列表:从当前位置到最后一个字符之间的所有前缀子串,如果这个子串是回文串,则继续递归;否则就跳过这个子串,继续查看后面更长的子串。
List<List<String>> result = new ArrayList<>();

public List<List<String>> partition(String s) {
    ArrayList<String> path = new ArrayList<>();
    backtrack(s, 0, path);
    return result;
}

void backtrack(String s, int start, ArrayList<String> path) {
    if (start == s.length()) {
        result.add(new ArrayList<>(path));
        return;
    }
    for (int i = start; i < s.length(); i++) {
        if (isPalindrome(s, start, i)){
            path.add(s.substring(start, i+1));
            backtrack(s, i+1, path);
            path.remove(path.size()-1);
        }
    }
}

public boolean isPalindrome(String s, int left, int right) {
    while (left < right) {
        if (s.charAt(left) != s.charAt(right)) return false;
        left++;
        right--;
    }
    return true;
}

排列问题#

46. 全排列#

给定不包含重复数字的数组,返回所有排列。

方法:回溯

// 使用全局变量来保存最后结果
List<List<Integer>> result = new ArrayList<>();
List<List<Integer>> permute(int[] nums) {
    List<Integer> track = new ArrayList<>(); // 保存决策树的一条路径
    backtrack(nums, track);
    return result;
}

void backtrack(int[] nums, List<Integer> track) {
    // 回溯结束条件:决策树到达底层
    if(track.size() == nums.length) {
        // 注意需要新建 ArrayList,track 是一个对象引用
        result.add(new ArrayList<Integer>(track));
        return;
    }
    // 选择列表
    for(int num : nums) {
        // 首先排除不合法选择(在当前分支上已经选过的数字就不能再选了)
        // 也可以用一个 used 数组来记录每个数是否已被选择(见下一题 全排列II)
        if(track.contains(num)) continue;
        // 做出选择
        track.add(num);
        // 进入决策树下一层
        backtrack(nums, track);
        // 撤销刚才的选择
        backtrack.remove(track.size()-1);
    }
}

47. 全排列 II#

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

回溯 过程包含了一系列选择和撤销。图中红色的分支是重复的,需要被剪除,这个问题的关键就是如何识别这些重复分支,从而能够及时停止相关搜索,避免重复。换句话说,这些重复分支的状态变量有什么特点?

注意图中标注 ① 和 ② 的两个分支,它们都是选择了 1,但是 ① 处的分支是和左边的兄弟分支重复的,需要被剪除。它们的不同之处在于:

  • ① 处选择 1' 时前一个 1 刚被撤销(所以后面还可以再选 1 ,造成重复)。
  • ② 处选择 1' 时前一个 1 刚被选择。

所以剪枝条件就比较明显了:先对数组排序,然后对于将要选择的数 x ,如果它前面也是 x ,且前面的 x 刚被撤销选择,那么直接跳过,因为再搜索这个 x 将会出现重复。

List<List<Integer>> result = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
    ArrayList<Integer> path = new ArrayList<>();
    boolean[] used = new boolean[nums.length];
    Arrays.sort(nums);
    backtrack(nums, used, path);
    return result;
}

void backtrack(int[] nums, boolean[] used, ArrayList<Integer> path) {
    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]) continue;
        path.add(nums[i]);
        used[i] = true;
        backtrack(nums, used, path);
        path.remove(path.size()-1);
        used[i] = false;
    }
}

搜索问题#

51. N 皇后#

这是一个很经典的问题,给定一个 nxn 棋盘,让你放置 n 个皇后,使它们不能相互攻击,即不能有任何两个在同一行,或同一列,或45度角的左上左下、右上右下。求出所有可能的放置方式。

方案用字符串列表表示,Q 表示皇后,. 表示空位。

和全排列问题类似,可以看成从第一行开始做选择,然后逐行向下遍历。三要素如下:

  • 路径:上面所有行已经放置皇后的位置。
  • 选择列表:当前行的所有位置。
  • 结束条件:路径长度达到 n。

另外,全排列问题中每次排除的不合法选择是已经加入 track 的数字,而这里,不合法选择需要根据 不互相攻击 这个条件来判断。

List<List<String>> result = new ArrayList<>();
public List<List<String>> solveQueens(int n) {
    // 初始化每一行的位置
    ArrayList<String> board = new ArrayList<>();
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < n; i++) {
        builder.append(".");
    }
    for (int i = 0; i < n; i++) {
        board.add(builder.toString());
    }

    backtrack(board, 0);
    return result;
}

public void backtrack(ArrayList<String> board, int row) {
    // 结束条件
    if (row == board.size()) {
        result.add(new ArrayList<String>(board));
        return;
    }
    for (int col = 0; col < board.size(); col++) {
        // 排除不合法选择
        if (!isValid(board, row, col)) continue;
        // 做选择:在 (row,col) 位置放置一个 Queen
        board.set(row, board.get(row).substring(0, col) + "Q" + 
                  board.get(row).substring(col+1));
        // 下一行
        backtrack(board, row + 1);
        // 撤销选择
        board.set(row, board.get(row).substring(0, col) + "." +             
                  board.get(row).substring(col+1));
    }
}

boolean isValid(ArrayList<String> board, int row, int col) {
    // 检查同一列
    for (int i = 0; i < row; i++) {
        if (board.get(i).charAt(col) == 'Q') return false;
    }
    // 检查坐上
    for (int i = row-1, j = col-1; i >=0 && j >= 0; i--, j--) {
        if (board.get(i).charAt(j) == 'Q') return false;
    }
    // 检查右上
    for (int i = row-1, j = col+1; i >= 0 && j < board.size(); i--, j++) {
        if (board.get(i).charAt(j) == 'Q') return false;
    }
    return true;
}

37. 解数独#

三要素的确定:

  • 路径:棋盘当前的填充情况,用一个二维数组记录;
  • 选择列表:数字 1~9;
  • 结束条件:填充完整个 board,即 row==9
public void solveSudoku(char[][] board) {
    return backtrack(board0 ,0, 0);
}

boolean backtrack(char[][] board, int row, int col) {
    // 结束条件:填满整个棋盘
    if (row == 9) return true;
    // 逐行填充
    if (col == 9) return backtrack(board, row+1, 0);
    if (board[row][col] != '.') return backtrack(board, row, col+1);
    // 选择列表
    for (char c = '1'; c < '9'; c++) {
        // 排除不合法选择
        if (!isValid(board, row, col, c)) continue;
        board[row][col] = c;
        if (backtrack(board, row, col+1)) return true;
        board[row][col] = '.';
    }
    return false;
}

boolean isValid(char[][] board, int row, int col, char digit) {
    for (int i = 0; i < board.length; i++) {
        // 当前行不能存在 digit
        if (board[row][i] == digit) return false;
        if (board[i][col] == digit) return false;
        if (board[(row/3) * 3 + (i/3)][(col/3) * 3 + (i%3)] == digit) return false;
    }
    return true;
}

22. 括号生成#

问题:生成所有可能的并且 有效的 n 对括号组合。

方法:回溯

为了保证括号有效,只需保证下面两条规则:

  • 如果左括号数量不大于 n,我们可以放一个左括号。
  • 如果右括号数量小于左括号的数量,我们可以放一个右括号。

要素的确定:

  • 路径:当前列出的括号序列;
  • 状态变量:已生成的左右括号的数量;
  • 结束条件:生成 n 对括号,即序列长度达到 2n;
  • 选择列表:左括号 / 右括号。

使用一个 StringBuilder 保存当前的括号序列,当序列长度达到 2n 时将该序列加入结果集。在回溯的每一层,可以视情况添加一个左括号,也可以添加一个右括号,按照这两个选项进行更深层的回溯。

List<String> result = new ArrayList<>();

List<String> generateParenthesis(int n) {
    StringBuilder builder = new StringBuilder();
    backtrack(n, builder, 0, 0);
    return result;
}

void backtrack(int n, StringBuilder builder, int leftCount, int rightCount) {
    // 结束条件
    if(builder.length() == n * 2) {
        result.add(builder.toString());
        return;
    }
    // 两个选项
    if(leftCount < n) {
        builder.append('('); // 做出选择
        backtrack(n, builder, leftCount+1, rightCount); // 进入下一层
        builder.deleteCharAt(builder.length() - 1); // 撤销选择
    }
    if(rightCount < leftCount) {
        builder.append(')');
        backtrack(n, builder, leftCount, rightCount+1);
        builder.deleteCharAt(builder.length() - 1);
    }
}

79. 单词搜索#

给定一个包含若干字母的二维数组和一个目标单词,从二维数组中搜索是否存在这样的目标单词。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

方法:回溯

boolean exist(char[][] board, String word) {
    int rows = board.length, cols = board[0].length;
    boolean[][] visited    = new boolean[rows][cols];
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < cols; j++) {
            if(check(board, word, visited, i, j, 0)) return true;
        }
    }
    return false;
}

boolean check(char[][] board, String word, boolean[][] visited, int r, int c, int pos) {
    // 排除不合法选择
    if(board[r][c] != word.charAt(pos)) return false;
    // 结束条件
    if(pos == word.length() - 1) return true;
    // 做选择
    visited[r][c] = true;
    // 递归处理下一层(下一个字符)
    int[] rowInc = {-1, 1, 0, 0};
    int[] colInc = {0, 0, -1, 1};
    for(int i = 0; i < 4; i++) {
        int newR = r + rowInc[i], newC = c + colInc[i];
        if(newR >= 0 && newR < board.length 
                && newC >= 0 && newC < board[0].length) {
            if(!visited[newR][newC]) {
                if(check(board, word, newR, newC, pos+1)) {
                    return true;
                }
            }
        }
    }
    // 撤销选择
    visited[r][c] = false;
    return false;
}
posted @   李志航  阅读(44)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏
点击右上角即可分享
微信分享提示
主题色彩