5. 回溯问题

183 阅读6分钟

参考:
1. 回溯算法秒杀所有排列/组合/子集问题
2. 一文秒杀所有岛屿题目

问题总览

微信图片_20230330134028.jpg

类型问题完成
46. 全排列
剑指 Offer II 083. 没有重复元素集合的全排列
51. N 皇后
78. 子集
77.组合
47. 全排列 II
90.子集 II
40. 组合总和 II
1020. 飞地的数量
1254. 统计封闭岛屿的数目
1905. 统计子岛屿
200. 岛屿数量
694. 不同岛屿的数量
695. 岛屿的最大面积
剑指 Offer II 105. 岛屿的最大面积
52. N皇后 II
剑指 Offer II 083. 没有重复元素集合的全排列
698. 划分为k个相等的子集
37. 解数独
752. 打开转盘锁
剑指 Offer II 109. 开密码锁
773. 滑动谜题

准备知识

❗️ 回溯需要注意的三个元素:

  1. 走过的路径
  2. 可选列表
  3. 结束条件

❓️ 什么是组合:给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合

一、排列组合子集问题

问题列表

类型问题完成
无重复数字不可复选46. 全排列
51. N 皇后
78. 子集
77.组合
216. 组合总和 III
包含重复数字47. 全排列 II
90.子集 II
40. 组合总和 II
无重复元素可重复选39. 组合总和

相同问题:
剑指 Offer II 079. 所有子集
剑指 Offer II 080. 含有 k 个元素的组合
剑指 Offer II 081. 允许重复选择元素的组合
剑指 Offer II 082. 含有重复元素集合的组合
剑指 Offer II 083. 没有重复元素集合的全排列
剑指 Offer II 084. 含有重复元素集合的全排列

1.1 分析

🏳️‍🌈有四种情况:

  1. 无重复数字、不可重复选择

  2. 有重复数字、不可重复选择

    我们通过保证元素之间的相对顺序不变来防止出现重复的子集。

  3. 无重复数字,可重复选择

  4. 有重复数字、可重复选择(其实和3没什么区别,因为可重复选择其实不就是有重复数字的意思么)

🏳️‍🌈有三种输出:

  1. 排列:在回溯完成后输出;

    for (int col = 0; col < len; col++)

    boolean[] used = new boolean[]来判断这个元素是否已经遍历过

  2. 组合:组合问题其实就是子集问题,组合是固定长度的子集;

    for (int i = start; i < n; i++)

  3. 子集:在回溯的过程中记录元素并输出。

    // 无重复元素的子集
    void backtrack(int[] nums, int start) {
        for (int i = start; i < n; i++) {
            backtrack(nums, i+1);
        }
    }
    

1.2 不包含重复数字

特点是:输出的元素个数和输入相同,结束条件一般是:

track.size() == nums.length

46. 全排列

class Solution {
    boolean[] visited;
    List<List<Integer>> ans;

    public List<List<Integer>> permute(int[] nums) {
        visited = new boolean[nums.length];
        ans = new ArrayList<>();
        backtrace(nums, new LinkedList<Integer>());
        return ans;
    }

    public void backtrace(int[] nums, LinkedList<Integer> path) {
        if (path.size() == nums.length) {
            ans.add(new ArrayList<>(path));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (visited[i]) {
                continue;
            }
            path.add(nums[i]);
            visited[i] = true;

            backtrace(nums, path);

            path.removeLast();
            visited[i] = false;
        }
    }
}
// 时间复杂度:O(n^2)
// 空间复杂度:O(n^2)

51. N 皇后

基本套路和全排列是一样的,只是判断是否可以放置皇后的判断复杂一些

不过N皇后没有使用used数组,而是使用了row来计数

import java.util.LinkedList;

//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
    List<List<String>> ans;
    int N = 0;

    public List<List<String>> solveNQueens(int n) {
        ans = new ArrayList<>();
        N = n;
        char[][] paths = new char[n][n];
        for (char[] path : paths) {
            Arrays.fill(path, '.');
        }
        backtrace(0, paths);
        return ans;
    }

    public List<String> convert(char[][] paths) {
        List<String> ans = new ArrayList<>();
        for (int i = 0; i < paths.length; i++) {
            StringBuilder sb = new StringBuilder();
            for (char c : paths[i]) {
                sb.append(c);
            }
            ans.add(sb.toString());
        }
        return ans;
    }

    public void backtrace(int row, char[][] paths) {
        if (row == N) {
            ans.add(convert(paths));
            return;
        }
        for (int col = 0; col < N; col++) {
            if (!canPut(paths, row, col)) {
                continue;
            }
            paths[row][col] = 'Q';
            backtrace(row + 1, paths);
            paths[row][col] = '.';
        }
    }

    public boolean canPut(char[][] path, int row, int col) {
        for (int i = row - 1; i >= 0; i--) {
            // 检查同一列,如果已经有Q了,那么这个位置不能放
            if (path[i][col] == 'Q') {
                return false;
            }
        }

        // 检查左上
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (path[i][j] == 'Q') {
                return false;
            }
        }
        // 检查右上
        for (int i = row - 1, j = col + 1; i >= 0 && j < N; i--, j++) {
            if (path[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }
}
// 时间复杂度:O(n!)
// 空间复杂度:O(n^2)

52. N 皇后 II

class Solution {
    char[][] chars;
    int N = 0;
    int count = 0;
    public int totalNQueens(int n) {
        N = n;
        chars = new char[n][n];
        
        for(char[] c: chars){
            Arrays.fill(c, '.');
        }
        // 开始尝试从第一行,第一个元素开始放置皇后
        put(chars, 0);
        return count;
    }

    private void put(char[][] chars, int row){
        if(row == N){
            count++;
            return;
        }
        for(int col = 0 ;col < N; col++){
            // 检查该位置是否可以放置皇后
            if(!canPut(chars, row, col)){
                continue;
            }
            chars[row][col] = 'Q';
            put(chars, row + 1);
            chars[row][col] = '.';
        }
    }

    private boolean canPut(char[][] chars, int row,int col){
        // 同一列
        for(int i= 0;i<row;i++){
            if(chars[i][col] == 'Q'){
                return false;
            }
        }
        // 左斜上
        for(int i=row-1,j=col-1;i>=0 && j>=0;i--,j--){
            if(chars[i][j] == 'Q'){
                return false;
            }
        }
        // 右斜上,最多走到右边角
        for(int i=row-1,j=col+1;i>=0 && j<chars[0].length;i--,j++){
            if(chars[i][j] == 'Q'){
                return false;
            }
        }
        return true;
    }
}

78. 子集

class Solution {
    List<List<Integer>> ans;

    public List<List<Integer>> subsets(int[] nums) {
        ans = new ArrayList<>();
        backtrace(nums, 0, new LinkedList<Integer>());
        return ans;
    }

    public void backtrace(int[] nums, int idx, LinkedList<Integer> path) {
        ans.add(new ArrayList<>(path));
        for (int i = idx; i < nums.length; i++) {
            path.add(nums[i]);
            backtrace(nums, i + 1, path);
            path.removeLast();
        }
    }
}
// 时间复杂度:O(n^2)
// 空间复杂度:O(n^2)

77.组合

class Solution {
    private LinkedList<Integer> track = new LinkedList<>();
    private List<List<Integer>> res = new ArrayList<>();

    public List<List<Integer>> combine(int n, int k) {
        backtrace(n, 0, k);
        return res;
    }

    public void backtrace(int n, int start, int k) {
        if (track.size() == k) {
            res.add(new LinkedList<>(track));
            return;
        }

        for (int i = start; i < n; i++) {
            track.add(i + 1);
            backtrace(n, i + 1, k);
            track.removeLast();
        }
    }
}
import java.util.LinkedList;

//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
    List<List<Integer>> ans;
    int n;
    int k;

    public List<List<Integer>> combinationSum3(int k, int n) {
        // 元素只能使用1-9,并且不重复使用
        ans = new ArrayList<>();
        this.n = n;
        this.k = k;
        backtrace(1, new LinkedList<Integer>(), 0);
        return ans;
    }

    public void backtrace(int idx, LinkedList<Integer> path, int sum) {
        if (sum == n && path.size() == k) {
            ans.add(new LinkedList<>(path));
            return;
        }
        if (sum > n) {
            return;
        }

        for (int i = idx; i < 10; i++) {
            path.add(i);
            sum += i;
            backtrace(i + 1, path, sum);
            path.removeLast();
            sum -= i;
        }
    }
}
//leetcode submit region end(Prohibit modification and deletion)

1.3 元素重复,不可复选

47. 全排列 II

算法精髓:

  1. 先排序
  2. 额外加一步剪枝
import java.util.ArrayList;
import java.util.List;

//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
    private List<List<Integer>> res = new ArrayList<>();

    public List<List<Integer>> permuteUnique(int[] nums) {
        // 先排序
        Arrays.sort(nums);
        boolean[] used = new boolean[nums.length];
        backtrace(nums, new LinkedList<>(), used);
        return res;
    }

    public void backtrace(int[] nums, LinkedList<Integer> trace, boolean[] used) {
        if (nums.length == trace.size()) {
            res.add(new LinkedList<>(trace));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (used[i]) {
                continue;
            }
            // 剪枝的条件是和前一个元素相等,但是前一个元素
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }
            trace.add(nums[i]);
            used[i] = true;
            backtrace(nums, trace, used);
            trace.removeLast();
            used[i] = false;
        }
    }
}
//leetcode submit region end(Prohibit modification and deletion)

90.子集 II

class Solution {
    private LinkedList<Integer> track = new LinkedList<>();
    private List<List<Integer>> res = new LinkedList<>();

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        backtrace(nums, 0);
        return res;
    }

    public void backtrace(int[] nums, int start) {
        res.add(new LinkedList<>(track));
        for (int i = start; i < nums.length; i++) {
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }
            track.add(nums[i]);
            backtrace(nums, i + 1);
            track.removeLast();
        }
    }
}

40. 组合总和 II

class Solution {
    private List<List<Integer>> res = new ArrayList<>();
    private LinkedList<Integer> track = new LinkedList<>();
    private int sum = 0;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        // 可重不可复选,等于子集问题
        // 先排序
        Arrays.sort(candidates);
        boolean[] used = new boolean[candidates.length];
        backtrack(candidates, target, 0);
        return res;
    }

    public void backtrack(int[] nums, int target, int start){
        if(target == sum){
            res.add(new LinkedList<>(track));
            return;
        }
        if(sum > target){
            return;
        }
        for(int i=start; i<nums.length;i++){
            if(i>start && nums[i] == nums[i-1]){
                continue;
            }
            track.add(nums[i]);
            sum += nums[i];
            backtrack(nums, target, i+1);
            track.removeLast();
            sum -= nums[i];
        }
    }
}

1.4 元素无重可以复选

39. 组合总和

class Solution {
    List<List<Integer>> ans;
    int target;

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        //无重就不需要排序
        ans = new ArrayList<>();
        this.target = target;

        backtrace(candidates, 0, new LinkedList<>(), 0);
        return ans;
    }

    public void backtrace(int[] nums, int idx, LinkedList<Integer> path, int sum) {
        if (sum == target) {
            ans.add(new LinkedList<>(path));
            return;
        }
        if (sum > target) {
            return;
        }

        for (int i = idx; i < nums.length; i++) {
            path.add(nums[i]);
            sum += nums[i];
            backtrace(nums, i, path, sum);
            path.removeLast();
            sum -= nums[i];
        }
    }
}

二、岛屿问题

类型问题完成
1020. 飞地的数量
1254. 统计封闭岛屿的数目
1905. 统计子岛屿
200. 岛屿数量
305. 岛屿数量 II
694. 不同岛屿的数量
695. 岛屿的最大面积

相同问题:
剑指 Offer II 105. 岛屿的最大面积

1020. 飞地的数量

class Solution {
    int[][] directions = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};

    public int numEnclaves(int[][] grid) {
        // 到达边界也可以离开,所以要寻找的是不与边界连接的陆地网格
        int m = grid.length;
        int n = grid[0].length;
        
        for(int i=0;i<m;i++){
            flooded(grid, i,0);
            flooded(grid, i,n-1);
        }
        for(int j=0;j<n;j++){
            flooded(grid, 0,j);
            flooded(grid, m-1,j);
        }
        int count = 0;
        for(int i=1;i<m-1;i++){
            for(int j=1;j<n-1;j++){
                if(grid[i][j] == 1){
                    count++;
                }
            }
        }
        return count;
    }

    // 寻找当前陆地能够找到的所有陆地,将其淹没
    public void flooded(int[][] grid , int i , int j){
        if(i<0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == 0){
            return;
        }
        grid[i][j] = 0;
        for(int[] dir: directions){
            int x = i + dir[0];
            int y = j + dir[1];
            flooded(grid, x, y);
        }
    }
}

1254. 统计封闭岛屿的数目

class Solution {
    int[][] directions = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};
    public int closedIsland(int[][] grid) {
        // 到达边界也可以离开,所以要寻找的是不与边界连接的陆地网格
        int m = grid.length;
        int n = grid[0].length;
        
        for(int i=0;i<m;i++){
            flooded(grid, i,0);
            flooded(grid, i,n-1);
        }
        for(int j=0;j<n;j++){
            flooded(grid, 0,j);
            flooded(grid, m-1,j);
        }
        int count = 0;
        for(int i=1;i<m-1;i++){
            for(int j=1;j<n-1;j++){
                if(grid[i][j] == 0){
                    count++;
                    // 一个海岛只保留一块陆地
                    flooded(grid, i,j);
                }
            }
        }
        return count;
    }

    public void flooded(int[][] grid , int i , int j){
        if(i<0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == 1){
            return;
        }
        //置为海洋
        grid[i][j] = 1;
        for(int[] dir: directions){
            int x = i + dir[0];
            int y = j + dir[1];
            flooded(grid, x, y);
        }
    }
}

1905. 统计子岛屿

class Solution {
    int[][] directions = new int[][]{{0,1},{1,0},{0,-1},{-1,0}};
    boolean isSub = true;
    public int countSubIslands(int[][] grid1, int[][] grid2) {
        int m = grid1.length;
        int n = grid1[0].length;
        int count = 0;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(grid2[i][j] == 1){
                    isSub = true;
                    flooded(grid1, grid2, i, j);
                    if(isSub){
                        count++;
                    }
                }
            }
        }
        return count;

    }
    private void flooded(int[][] grid1,int[][] grid2, int i , int j){
        if(i<0 || j < 0 || i >= grid2.length || j >= grid2[0].length || grid2[i][j] == 0){
            return;
        }
        // 先判断是否为子岛屿
        if(grid1[i][j] == 0){
            isSub = false;
        }
        //置为海洋
        grid2[i][j] = 0;
        for(int[] dir: directions){
            int x = i + dir[0];
            int y = j + dir[1];
            flooded(grid1, grid2, x, y);
        }
    }
    
}

200. 岛屿数量

class Solution {
    public int numIslands(char[][] grid) {
        int m=grid.length;
        int n=grid[0].length;
        int count = 0;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(grid[i][j] == '1'){
                    count++;
                    flooded(grid, i ,j);
                }
            }
        }
        return count;
    }

    public void flooded(char[][] grid , int i , int j){
        if(i<0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == '0'){
            return;
        }
        //置为水
        grid[i][j] = '0';
        flooded(grid, i + 1, j);
        flooded(grid, i - 1, j);
        flooded(grid, i, j+1);
        flooded(grid, i, j-1);
    }
}

695. 岛屿的最大面积

class Solution {
    int maxArea = 0;
    public int maxAreaOfIsland(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                // 找到岛屿了,开始计算最大面积
                if(grid[i][j] == 1){
                    maxArea = Math.max(flooded(grid, i, j),maxArea);
                }
            }
        }
        return maxArea;
    }

    public int flooded(int[][] grid , int i , int j){
        if(i<0 || j < 0 || i >= grid.length || j >= grid[0].length || grid[i][j] == 0){
            return 0;
        }
        // 置为水
        grid[i][j] = 0;
        // 为什么要加1?
        // 可以理解为面积为自身和四周所有面积之和,自身面积为1
        return flooded(grid, i + 1, j) + 
        flooded(grid, i - 1, j)+
        flooded(grid, i, j+1)+
        flooded(grid, i, j-1)+1;
    }
}

694. 不同岛屿的数量
305. 岛屿数量 II

三、其他

类型问题完成
131. 分割回文串
93. 复原 IP 地址
698. 划分为k个相等的子集
37. 解数独
752. 打开转盘锁
剑指 Offer II 109. 开密码锁
773. 滑动谜题

131. 分割回文串

class Solution {
    LinkedList<String> path = new LinkedList<>();
    List<List<String>> ans = new ArrayList<>();

    public List<List<String>> partition(String s) {
        backtrace(s, 0);
        return ans;
    }

    public void backtrace(String s, int start) {
        if (start == s.length()) {
            ans.add(new ArrayList<>(path));
            return;
        }
        for (int i = start; i < s.length(); i++) {
            if (!isPalindrome(s, start, i)) {
                continue;
            }
            path.add(s.substring(start, i + 1));
            backtrace(s, i + 1);
            path.removeLast();
        }
    }

    public boolean isPalindrome(String s, int lo, int hi) {
        while (lo < hi) {
            if (s.charAt(lo) == s.charAt(hi)) {
                lo++;
                hi--;
            } else {
                return false;
            }
        }
        return true;
    }
}

93. 复原 IP 地址

class Solution {
    LinkedList<String> path = new LinkedList<>();
    List<String> ans = new ArrayList<>();

    public List<String> restoreIpAddresses(String s) {
        // 最少是4位,最多是12位
        backtrace(s, 0);
        return ans;
    }

    public void backtrace(String s, int start) {
        if (start == s.length() && path.size() == 4) {
            StringBuilder sb = new StringBuilder();
            for (String p : path) {
                sb.append(p).append(".");
            }
            ans.add(sb.deleteCharAt(sb.length() - 1).toString());
            return;
        }
        if (start < s.length() && path.size() == 4) {
            return;
        }
        for (int i = start; i < s.length(); i++) {
            if (!isSubIp(s, start, i)) {
                continue;
            }
            path.add(s.substring(start, i + 1));
            backtrace(s, i + 1);
            path.removeLast();
        }
    }

    public boolean isSubIp(String s, int start, int end) {
        // 如果只有一个字符,肯定是满足的,因为这个字符在0-9范围内
        if (start == end) {
            return true;
        }

        if (s.charAt(start) == '0') {
            return false;
        }

        if (end - start < 2) {
            return true;
        } else if (end - start > 2) {
            return false;
        }
        if (Integer.parseInt(s.substring(start, end + 1)) > 255) {
            return false;
        }
        return true;
    }
}