LeetCode 11 - 回溯
回溯本质上是暴力穷举算法。解决一个回溯问题,实际上就是一个决策树的遍历过程。需要考虑三个问题:
- 路径:已经做出的选择是什么?
- 选择列表:当前可以做什么选择?
- 结束条件:到达决策树底层的判断条件是什么?
解决回溯问题的思路是:
- 画出递归树,据此找出 状态变量 (递归函数中的参数)。
- 确定回溯的结束条件,满足条件则将这一路径加入结果,并直接返回当前递归。
- 根据状态变量,确定选择列表,针对每一个选项:
- 判断是否需要剪枝。
- 做出选择。
- 递归调用,进入递归树的下一层。
- 撤销选择。
相关问题:
类型 | 题目 |
---|---|
子集、组合 | 子集、子集 II、组合、组合总和、组合总和 II |
排列 | 全排列、全排列 II、字符串的全排列、字母大小写全排列 |
搜索 | 解数独、单词搜索、N皇后、分割回文串、二进制手表 |
子集问题、组合问题、分割问题都可以抽象为一棵决策树,组合问题和分割问题都是收集决策树的叶结点,而子集问题是收集所有结点。子集也是一种组合问题。
因为组合问题中选过的元素不会重复选取,所以开始索引为 start
;而排列问题中,索引从 0
开始。(选择列表的 for
循环)
子集问题#
78. 子集#
问题:输入一个 不包含重复数字 的数组,输出这些数字的所有子集。
分析: {1,2,3}
的子集相当于在 {1,2}
的每个子集中加入数字 3
。可以用回溯来做。
- 状态变量:因为是组合而非排列,所以需要记录一个 当前遍历位置,在此位置之前的元素都不能再被重复选择,这就是该问题的状态变量
start
。 - 结束条件:因为路径上的中间值都是符合条件的组合,而非只有路径终点的值才符合条件,所以没有特殊的结束条件,或者说,当遍历完数组时就自动结束了。
- 选择列表:从
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. 电话号码的字母组合#
电话拨号键盘上每个数字都对应了三到四个字母,例如 1
→ a,b,c
, 2
→ d,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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 葡萄城 AI 搜索升级:DeepSeek 加持,客户体验更智能
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏