backtracking Leetcode 回溯算法题

77.组合

第一个位置选择有 n 种,接下来每个位置只能在前面选择数字的后面选,所以有了 beg 参数,才能保持不重复

剪枝:res.size + (n - beg + 1) < k , 已有答案的长度 + 剩余所有未选择的个数 都小于最终答案长度了 就没有必要尝试下去

class Solution {
    
    List<List<Integer>> ans = new ArrayList<List<Integer>>();

    public List<List<Integer>> combine(int n, int k) {
        List<Integer> res = new ArrayList<Integer>();
        dfs(res, 1, n, k);
        return ans;
    }

    public void dfs(List<Integer> res, int beg, int n, int k){
        if(k == 0){
            // 这里一定要 new 一个新的 ArrayList 啊,否则最后加进去的 res 都是 null
            ans.add(new ArrayList<Integer>(res));
            return;
        }
        if(res.size() + n - beg + 1 < k) return;

        for(int i = beg; i <= n; ++ i){
            res.add(i);
            dfs(res, i + 1, n, k - 1);
            res.remove(res.size() - 1);
        }
    }
}

216.组合总和III

class Solution {
    
    List<Integer> res = new ArrayList<>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        dfs(1, k, n);
        return ans;
    }

    public void dfs(int beg, int k, int n){
        if(k < 0 || n < 0){
            return;
        }

        // 剩下最大的 k 个数相加都小于 n || 最小的 k 个数相加都大于 n : 也就没有必要继续遍历了
        if(k*(19-k)/2 < n || k*(2*beg+k-1)/2 > n){
            return;
        }

        if(k == 0 && n == 0){
            ans.add(new ArrayList(res));
            return;
        }

        for(int i = beg; i <= 9; ++ i){
            res.add(i);
            dfs(i + 1, k - 1, n - i);
            res.remove(res.size() - 1);
        }
    }
}

随想录代码:

class Solution {
	List<List<Integer>> result = new ArrayList<>();
	LinkedList<Integer> path = new LinkedList<>();

	public List<List<Integer>> combinationSum3(int k, int n) {
		backTracking(n, k, 1, 0);
		return result;
	}

	private void backTracking(int targetSum, int k, int startIndex, int sum) {
		// 减枝
		if (sum > targetSum) {
			return;
		}

		if (path.size() == k) {
			if (sum == targetSum) result.add(new ArrayList<>(path));
			return;
		}

		// 减枝 9 - (k - path.size()) + 1
		for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
			path.add(i);
			sum += i;
			backTracking(targetSum, k, i + 1, sum);
			//回溯
			path.removeLast();
			//回溯
			sum -= i;
		}
	}
}

17.电话号码的字母组合

第一次遍历判断第一个位置的 beg 所对应的字符 idx,遍历所有字符的可能

第二次遍历判断第二个位置的 beg + 1 所对应的字符 idx,遍历所有字符的可能

一直到所有位置都被遍历完,也就是 digits 所有位置都被遍历完,那么 beg 就等于 digits.length() 了,此时记录答案

注意,digits 的第 beg 位置,对应的数字是 idx,该 idx 对应的字符才是要遍历的字符

  • 字符串中提取对应位置的字符:digits.chatAt(beg)
  • 字符 char 转为 int 类型:
    • 首先 char 转为 String :String.valueOf(x)
    • String 转为 int : Integer.parseInt(xx)
class Solution {
    List<String> table = Arrays.asList("", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz");
    List<String> ans = new ArrayList<>();
    public List<String> letterCombinations(String digits) {
        if(digits.equals("")) return ans;
        dfs(digits, new StringBuilder(), 0);
        return ans;
    }

    public void dfs(String digits, StringBuilder res, int beg){
        if(beg == digits.length()){
            ans.add(res.toString());
            return;
        }
        int idx = Integer.parseInt(String.valueOf(digits.charAt(beg)));
        for(int j = 0; j < table.get(idx).length(); ++j){
            res.append(table.get(idx).charAt(j));
            dfs(digits, res, beg + 1);
            res.delete(beg, beg + 1);
        }
    }
}

39.组合总和

不限制元素使用次数,使其达到目标数,的所有不同组合

  • 由于可以重复选取,所以 i 没有+1
  • 由于结果要求组合不同,所以不能选 i 之前的数,这里用for循环让 i 只能往后选择剩余的数
class Solution {
    List<Integer> res = new ArrayList<>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        dfs(res, candidates, target, 0);
        return ans;
    }

    public void dfs(List<Integer> res, int[] candidates, int target, int beg){
        
        if(target < 0){
            return;
        }
        if(target == 0){
            ans.add(new ArrayList(res));
            return;
        }

        for(int i = beg; i < candidates.length; ++ i){
            res.add(candidates[i]);
            dfs(res, candidates, target - candidates[i], i);
            res.remove(res.size() - 1);
        }
    }
}

40.组合总和II

元素有重复,选出总和达到目标数的不同组合

由于元素有重复,所以一个数只能连续的被使用,那么就需要对原数组排序

我这里使用了一个freq记录每个数的可使用次数,当这个数的使用次数被用完,或者以后都不打算使用这个数的时候,才可以用下一个数

class Solution {
    List<int[]> freq = new ArrayList<>();
    List<Integer> res = new ArrayList<>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        int num = 1, size = candidates.length;
        for(int i = 0; i < size; ++ i){
            if(i < size -1 && candidates[i] == candidates[i+1]){
                ++num;
            }else{
                freq.add(new int[]{candidates[i], num});
                num = 1;
            }
        }
        dfs(target, 0);
        return ans;
    }

    public void dfs(int target, int beg){
        if(target < 0){
            return;
        }
        if(target == 0){
            ans.add(new ArrayList<>(res));
        }

        for(int i = beg; i < freq.size(); ++i){
            res.add(freq.get(i)[0]);
            --freq.get(i)[1];
            dfs(target-freq.get(i)[0], freq.get(i)[1] == 0 ? i+1 : i);
            res.remove(res.size() - 1);
            ++freq.get(i)[1];
        }
    }
}

更优雅

另一个方法是让当前搜索层级里不出现相同的元素,要想同一个值使用多次,则必须在dfs的下一个层级

class Solution {
    List<Integer> res = new ArrayList<>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        dfs(candidates, target, 0);
        return ans;
    }

    public void dfs(int[] candidates, int target, int beg){
        if(target < 0){
            return;
        }
        if(target == 0){
            ans.add(new ArrayList<>(res));
        }

        for(int i = beg; i < candidates.length; ++i){
            if(i > beg && candidates[i] == candidates[i-1]){
                continue;
            }
            res.add(candidates[i]);
            dfs(candidates, target-candidates[i], i+1);
            res.remove(res.size() - 1);
        }
    }
}

131.分割回文串

动态规划

动态规划判断一个字串是否回文:

f[i][j] = f[i+1][j-1] && (s.charAt(i) == s.charAt(j));

注意,先定后边界 j,再遍历所有前边界 i

dfs 遍历所有可能划分

class Solution {
    boolean[][] f;
    List<String> seq = new ArrayList<>();
    List<List<String>> res = new ArrayList<List<String>>();
    public List<List<String>> partition(String s) {
        int n = s.length();
        f = new boolean[n][n];
        for(int i = 0; i < n; ++ i){
            for(int j = 0; j <= i; ++ j){
                f[i][j] = true;
            }
        }

        for(int j = 1; j < n; ++ j){
            for(int i = 0; i < j; ++ i){
                f[i][j] = f[i+1][j-1] && (s.charAt(i) == s.charAt(j));
            }
        }

        dfs(s, 0);
        return res;
    }

    public void dfs(String s, int beg){
        if(beg == s.length()){
            res.add(new ArrayList<String>(seq));
            return;
        }
        for(int j = beg; j < s.length(); ++ j){
            if(f[beg][j]){
                seq.add(s.substring(beg, j + 1));
                dfs(s, j + 1);
                seq.remove(seq.size() - 1);
            }
        }
    }
}

记忆化搜索

将回文串的判断用dfs实现:

class Solution {
    int[][] f;
    List<String> seq = new ArrayList<>();
    List<List<String>> res = new ArrayList<List<String>>();
    public List<List<String>> partition(String s) {
        int n = s.length();
        f = new int[n][n];
        dfs(s, 0);
        return res;
    }

    public void dfs(String s, int beg){
        if(beg == s.length()){
            res.add(new ArrayList<String>(seq));
            return;
        }
        for(int j = beg; j < s.length(); ++ j){
            if(isHuiwen(s, beg, j) == 1){
                seq.add(s.substring(beg, j + 1));
                dfs(s, j + 1);
                seq.remove(seq.size() - 1);
            }
        }
    }
    public int isHuiwen(String s, int i, int j){
        if(f[i][j] != 0){
            return f[i][j];
        }
        if(i >= j){
            f[i][j] = 1;
        }else if(s.charAt(i) == s.charAt(j)){
            f[i][j] = isHuiwen(s, i+1, j-1);
        }else{
            f[i][j] = -1;
        }
        return f[i][j];
    }
}

78.子集

  • 迭代法:可以枚举二进制
  • 回溯法:
class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    List<Integer> ans = new ArrayList<>();
    public List<List<Integer>> subsets(int[] nums) {
        dfs(nums, 0);
        return res;
    }

    public void dfs(int[] nums, int beg){
        res.add(new ArrayList(ans));
        if(beg == nums.length){
            return;
        }

        for(int i = beg; i < nums.length; ++ i){
            ans.add(nums[i]);
            dfs(nums, i + 1);
            ans.remove(ans.size()-1);
        }
    }
}

90.子集II

与之前的 40.组合总和II 解法相同,但没有达到目标数的要求,更简单。

class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    List<Integer> ans = new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        dfs(nums, 0);
        return res;
    }

    public void dfs(int[] nums, int beg){
        res.add(new ArrayList(ans));
        for(int i = beg; i < nums.length; ++ i){
            if(i > beg && (nums[i-1] == nums[i])){
                continue;
            }
            ans.add(nums[i]);
            dfs(nums, i + 1);
            ans.remove(ans.size() - 1);
        }
    }
}

491.递增子序列

由于元素有重复且需要维护相对顺序,所以无法通过排序去重

使用 HashSet 对同一层的数据去重

class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    List<Integer> ans = new ArrayList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        dfs(nums, 0, -101);
        return res;
    }

    public void dfs(int[] nums, int beg, int pre){
        if(ans.size() > 1){
            res.add(new ArrayList(ans));
        }
        Set<Integer> used = new HashSet<>();
        for(int i = beg; i < nums.length; ++ i){
            if(nums[i] < pre || used.contains(nums[i])) {
                continue;
            }
            used.add(nums[i]);
            ans.add(nums[i]);
            dfs(nums, i + 1, nums[i]);
            ans.remove(ans.size() - 1);
        }
    }
}

46.全排列

将一个无重复元素的数组全排列

used 记录已经使用过的元素

每次都遍历所有,只往下 dfs 未使用的元素

class Solution {
    boolean[] used;
    int n;
    List<Integer> ans = new ArrayList<>();
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    public List<List<Integer>> permute(int[] nums) {
        n = nums.length;
        used = new boolean[n];
        dfs(nums);
        return res;
    }

    public void dfs(int[] nums){
        if(ans.size() == n){
            res.add(new ArrayList(ans));
            return;
        }
        for(int i = 0; i < n; ++ i){
            if(used[i] == false){
                ans.add(nums[i]);
                used[i] = true;
                dfs(nums);
                used[i] = false;
                ans.removeLast();
            }
        }
    }
}

47.全排列II

有重复元素的全排列

求全排列的方法加上去重算法

由于顺序无所谓,所以可以先排序,

对于同一层的搜索:让上一次搜索的元素与下一次搜索的元素不同

记录上一次用到的元素,判断当前元素是否与前一个元素相同

class Solution {
    boolean[] used;
    int n;
    List<Integer> ans = new ArrayList<>();
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        n = nums.length;
        used = new boolean[n];
        Arrays.sort(nums);
        dfs(nums);
        return res;
    }

    public void dfs(int[] nums){
        if(ans.size() == n){
            res.add(new ArrayList(ans));
            return;
        }
        int pre = 100;
        for(int i = 0; i < n; ++ i){
            if(used[i] == false && nums[i] != pre){
                ans.add(nums[i]);
                used[i] = true;
                pre = nums[i];
                dfs(nums);
                used[i] = false;
                ans.removeLast();
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值