回溯法
回溯法的套路解法在这篇文章中给了详细的说明。
岛屿数量
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
- 图的深度优先遍历,将
1
置为0
public int numIslands(char[][] grid) {
int num = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
if (grid[i][j] == '1') {
num++;
mark(grid, i, j);
}
}
}
return num;
}
private void mark(char[][] grid, int i, int j) {
if (i < 0 || i >= grid.length ||
j < 0 || j >= grid[i].length ||
grid[i][j] == '0') {
return;
}
grid[i][j] = '0';
mark(grid, i + 1, j);
mark(grid, i - 1, j);
mark(grid, i, j + 1);
mark(grid, i, j - 1);
}
Subsets
返回一个数组的所有 subsets:
Input: nums = [1,2,3]
Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
解法:
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, 0);
return list;
}
private void backtrack(List<List<Integer>> list , List<Integer> tempList, int [] nums, int start){
list.add(new ArrayList<>(tempList));
for(int i = start; i < nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
如果这个数组中有重复的数字,那么下面算法展示的是去掉重复数字的写法:
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, 0);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int start){
list.add(new ArrayList<>(tempList));
for (int i = start; i < nums.length; i++){
if (i > start && nums[i] == nums[i-1])
continue; // skip duplicates
tempList.add(nums[i]);
backtrack(list, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
全排列
对于全排列 (Permutations):
Input: nums = [1,2,3]
Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
它的套路写法如下:
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
// Arrays.sort(nums); // not necessary
backtrack(list, new ArrayList<>(), nums);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums){
if (tempList.size() == nums.length){
list.add(new ArrayList<>(tempList));
} else{
for (int i = 0; i < nums.length; i++){
if (tempList.contains(nums[i]))
continue; // element already exists, skip
tempList.add(nums[i]);
backtrack(list, tempList, nums);
tempList.remove(tempList.size() - 1);
}
}
}
但是这样子写,这个 contains
方法的复杂度是 O(n)
,所以也可以使用 HashSet
来优化:
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> ret = new ArrayList<>();
backtrack(ret, new ArrayList<>(), new HashSet<>(), nums);
return ret;
}
private void backtrack(List<List<Integer>> ret, List<Integer> tmpList, Set<Integer> tmpSet, int[] nums) {
if (tmpSet.size() == nums.length) {
ret.add(new ArrayList<>(new ArrayList<>(tmpList)));
return;
}
for (int i = 0; i < nums.length; i++) {
if (tmpSet.contains(nums[i]))
continue;
tmpSet.add(nums[i]);
tmpList.add(nums[i]);
backtrack(ret, tmpList, tmpSet, nums);
tmpSet.remove(tmpList.get(tmpList.size()-1));
tmpList.remove(tmpList.size() - 1);
}
}
如果它的数组也是包含重复的,那么去掉重复数字的全排列写法如下,我们这个地方还有技术债,那就是 if
条件为什么要这样写:
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, new boolean[nums.length]);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, boolean [] used){
if (tempList.size() == nums.length){
list.add(new ArrayList<>(tempList));
} else{
for (int i = 0; i < nums.length; i++){
if (used[i] || i > 0 && nums[i] == nums[i-1] && !used[i - 1])
continue;
used[i] = true;
tempList.add(nums[i]);
backtrack(list, tempList, nums, used);
used[i] = false;
tempList.remove(tempList.size() - 1);
}
}
}
Combination Sum
找到所有加起来等于某个 target
的候选解法,数组中无重复数字:
Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3],[7]]
它的解法如下:
public List<List<Integer>> combinationSum(int[] nums, int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, target, 0);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int remain, int start){
if (remain < 0) {
return;
} else if (remain == 0) {
list.add(new ArrayList<>(tempList));
} else {
for (int i = start; i < nums.length; i++){
tempList.add(nums[i]);
backtrack(list, tempList, nums, remain - nums[i], i); // not i + 1 because we can reuse same elements
tempList.remove(tempList.size() - 1);
}
}
}
下面是数组中一个数字只能使用一次的版本:
public List<List<Integer>> combinationSum2(int[] nums, int target) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
backtrack(list, new ArrayList<>(), nums, target, 0);
return list;
}
private void backtrack(List<List<Integer>> list, List<Integer> tempList, int [] nums, int remain, int start){
if (remain < 0) {
return;
} else if(remain == 0) {
list.add(new ArrayList<>(tempList));
} else{
for (int i = start; i < nums.length; i++){
if (i > start && nums[i] == nums[i-1])
continue; // skip duplicates
tempList.add(nums[i]);
backtrack(list, tempList, nums, remain - nums[i], i + 1);
tempList.remove(tempList.size() - 1);
}
}
}
生成括号
输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"]
// 时间复杂度,总的括号对个数是 Catalan number 个
// O(C(2n, n) / (n + 1))
public List<String> generateParenthesis(int n) {
List<String> res = new LinkedList<>();
helper(res, "", 0, 0, n);
return res;
}
private void helper(List<String> res, String cur, int open, int close, int n) {
if (open == n && close == n) {
res.add(cur);
return;
}
if (open < n) {
helper(res, cur + "(", open + 1, close, n);
}
if (close < open) {
helper(res, cur + ")", open, close + 1, n);
}
}
添加不同括号的方式
// 头条面试题
//
// https://leetcode.com/problems/different-ways-to-add-parentheses/
// Input: "2*3-4*5"
// Output: [-34, -14, -10, -10, 10]
// Explanation:
// (2*(3-(4*5))) = -34
// ((2*3)-(4*5)) = -14
// ((2*(3-4))*5) = -10
// (2*((3-4)*5)) = -10
// (((2*3)-4)*5) = 10
//
// 只有 +、- 和 *
//
// 有几种不同的结果
public class DifferentWaysToAddParentheses {
public List<Integer> diffWaysToCompute(String input) {
List<Integer> ret = new LinkedList<Integer>();
for (int i = 0; i < input.length(); i++) {
if (input.charAt(i) == '-' ||
input.charAt(i) == '*' ||
input.charAt(i) == '+' ) {
String part1 = input.substring(0, i);
String part2 = input.substring(i + 1);
List<Integer> part1Ret = diffWaysToCompute(part1);
List<Integer> part2Ret = diffWaysToCompute(part2);
for (Integer p1:part1Ret) {
for (Integer p2:part2Ret) {
int c = 0;
switch (input.charAt(i)) {
case '+': c = p1 + p2;
break;
case '-': c = p1 - p2;
break;
case '*': c = p1 * p2;
break;
}
ret.add(c);
}
}
}
}
if (ret.size() == 0) {
// input 是一个单一数字
ret.add(Integer.valueOf(input));
}
return ret;
}
}
目标和
给定一个非负整数的列表a1,a2,…an,再给定一个目标 S。现在用+和-两种运算,对于每一个整数,选择一个作为它前面的符号。
找出有多少种方法,使得这些整数的和正好等于 S。
输入: nums为 [1, 1, 1, 1, 1], S 为 3.
输出: 5
解释:
-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3
有5种方法让和为3.
题解:
class Solution {
int count = 0;
public int findTargetSumWays(int[] nums, int target) {
backtrack(nums, target, 0, 0);
return count;
}
public void backtrack(int[] nums, int target, int index, int sum) {
if (index == nums.length) {
if (sum == target) {
count++;
}
} else {
backtrack(nums, target, index + 1, sum + nums[index]);
backtrack(nums, target, index + 1, sum - nums[index]);
}
}
}
复杂度分析
时间复杂度:O(2^n)
,其中 n 是数组数组的长度。回溯需要遍历所有不同的表达式,共有 2^n 种不同的表达式,每种表达式计算结果需要 O(1) 的时间,因此总时间复杂度是 O(2^n)
。
空间复杂度:O(n)
,其中 n 是数组的长度。空间复杂度主要取决于递归调用的栈空间,栈的深度不超过 n。