【算法】回溯总结

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

一、前言

解决一个回溯问题:实际上就是一个 决策树 的遍历问题,即多叉树遍历问题。

主要思考 3 个问题:

  1. 路径:已经做出的选择
  2. 选择列表:当前可以做的选择
  3. 结束条件:到达决策树底层,无法再做选择的条件

2022-07-1316-57-07.png

回溯算法框架:

result = []; // 结果void backtrack(路径, 选择列表) {
    if (满足结束条件) {
        result.add(路径);
        return;
    }
    
    for (选择 : 选择列表) {
        // 1. 做选择
        // 2. 调用
        backtrack(路径, 选择列表);
        // 3. 撤销选择
    }
}

二、排列组合问题

(1)全排列

题目

题目:给你一个整数数组,并且数组中没有重复元素,你要返回这个数组所有可能的排列。

# 比如说给你的数组是:
[0, 1, 2]
​
# 你要返回的所有排列是:
0, 1, 2
0, 2, 1
1, 0, 2
1, 2, 0
2, 0, 1
2, 1, 0

思路

思路:遍历一颗决策树。

2022-07-1316-57-07.png

  1. 路径:已经做出的选择
  2. 选择列表:当前可以做的选择
  3. 结束条件:到达决策树底层,无法再做选择的条件

AC题解:

class Solution {
    // Time: O(n*n!), Space: O(n)
    public List<List<Integer>> permute(int[] nums) {
        if (nums == null || nums.length == 0) return Collections.emptyList();
        List<List<Integer>> result = new ArrayList<>();
​
        List<Integer> list = new ArrayList<>();
        for (int num: nums) list.add(num);
​
        permuteRec(list, 0, result);
        return result;
    }
​
    private void permuteRec(List<Integer> list, int start, List<List<Integer>> result) {
        // 结束条件:最后节点了
        if (start == list.size()) {
            result.add(new ArrayList<>(list));
            return;
        }
        // 选择列表
        for (int i = start; i < list.size(); ++i) {
            Collections.swap(list, i , start);   // 选择
            permuteRec(list, start + 1, result); // 调用
            Collections.swap(list, start, i);    // 撤销选择
        }
    }
}

(2)组合总数

题目

LeetCode 39 组合总数

题目:给你一个正整数数组,数组中不包含重复元素,同时给你一个正整数目标值,你要找到数组中和为目标值的所有组合。

  • 另外,数组中每个元素都可以使用无限多次,并且答案中不能包含重复组合。
# 比如说,给你的数组是:
[4, 2, 8]
​
# 给你的目标值是 6。数组中和为 6 的组合有:
[4, 2]
[2, 2, 2]

思路

同上,回溯问题。

AC 题解:

class Solution {
    // Time: O(n ^ (target / min)), Space: O(target / min)
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates == null || candidates.length == 0) return null;
        List<List<Integer>> result = new ArrayList<>();
​
        Arrays.sort(candidates);
​
        combSum(candidates, target, 0, new ArrayList<>(), result);
        return result;
    }
​
    private void combSum(int [] nums, int target, int start, List<Integer> elem,
                         List<List<Integer>> result) {
        // 结束条件:最后节点了
        if (target == 0) {
            result.add(new ArrayList<>(elem));
            return;
        }
​
        // 结束条件:超出目标值
        if (target < 0) return;
        for (int i = start; i < nums.length; ++i) {
            if (nums[i] > target) break; // 剪枝叶:超出目标值
            // 选择
            elem.add(nums[i]);
            // 调用
            combSum(nums, target - nums[i], i, elem, result);
            // 撤销选择
            elem.remove(elem.size() - 1); // T: O(1)
        }
    }
}

(3)下一个排列

题目

LeetCode 31 下一个排列

题目:给你一个整数数组,每一个元素是一个 0 到 9 的整数,数组的排列形成了一个有效的数字。

  • 你要找到数组的下一个排列,使它形成的数字是大于当前排列的第一个数字。
  • 如果当前排列表示的已经是最大数字,则返回这个数组的最小排列。
示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
​
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
​
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]

思路

思路:

  1. 从后往前找第一个数 pnums[p] < nums[p + 1]
  2. 从后往前找第一个数 inums[i] > nums[p],再对 nums[i]nums[j] 进行交换
  3. [p + 1, 数组长度) 进行升序:这里只需要两两交换就能满足

举个栗子:[2, 1, 8, 4, 2, 1]

2022-07-1400-13-08.png

AC 题解:

class Solution {
    // Time: O(n), Space: O(1)
    public void nextPermutation(int[] nums) {
        if (nums == null || nums.length < 2) return;
        int n = nums.length;
        int p = n - 2;
        // 1. 从后往前找第一个数 p:nums[p] < nums[p + 1]
        while (p >= 0 && nums[p] >= nums[p + 1]) --p;
​
        // 2. 从后往前找第一个数 i:nums[i] > nums[p]
        if (p >= 0) {
            int i = n - 1;
            while (i > p && nums[i] <= nums[p]) --i;
            swap(nums, i, p);
        }
        
        // 3. 对 [p + 1, 数组长度) 进行升序:这里只需要两两交换就能满足
        for (int i = p + 1, j = n - 1; i < j; ++i, --j)
            swap(nums, i, j);
    }
    // 元素交换方法:
    private void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }
}

三、N皇后问题

N皇后是经典算法:

  • 棋盘中皇后可以攻击同一行、同一列、左上、左下、右上、右下的任意单位
  • 现给你一个 N * N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击

2022-07-1408-21-58.png

这个问题本质上跟全排列问题差不多,决策树 的每一层表示棋盘上的每一行。

  • 路径:放置好的皇后列表
  • 选择列表:在某一行任意一列放置一个皇后
  • 结束条件:棋盘上放置满了皇后

模板如下:

  1. 主干:回溯
  2. 判断此位置是否可放置皇后
// 函数找到一个答案后就返回 true
boolean backtrack(int[][] board, int n, int row) {
    if (row == n) {
        result.add(buildList(board));
        return;
    }
    
    for (int col = 0; col < n; col++) {
        if (!isValid(board, row, col)) continue;
        
        // 做选择
        board[row][col] = 'Q';
        // 进入下一行决策
        if (backtrack(board, n, row + 1)) return true;
        // 撤销选择
        board[row][col] = '.';
    }
    
    return false;
}
​
boolean isValid(int[][] board, int row, int col) {
    int n = board.size();
    // 检查列中是否有皇后互相冲突
    for (int i = 0; i < row; ++i) {
        if (board[i][col] == 'Q') return false;
    }
    
    // 检查右上方是否有皇后互相冲突
    for (int i = row - 1, j = col + 1; i >=0 & j >= 0; i--, j--) {
        if (board[i][j] == 'Q') return false;
    }
    
    // 检查左上方是否有皇后互相冲突
    for (int i = row - 1, j = col - 1; i >=0 & j >= 0; i--, j--) {
        if (board[i][j] == 'Q') return false;
    }
    return true;
}

(1)N皇后

LeetCode51 N皇后

题目

题目:给你一个整数 n,你要返回 n 皇后问题的所有解。其中,每个解是一个棋盘布局,用字符 'Q' 表示一个皇后,用字符 '.' 表示一个空位置。

n 皇后问题的定义是,你要把 n 个皇后放到一个 n x n 的棋盘上,使得任意两个皇后之间都不能互相攻击,也就是说任意两个皇后不能位于同一行、同一列以及同一斜线

# 比如说,给你的 n 等于 4。

# 4 皇后问题有以下两个解:
[
 [
  ".Q..",
  "...Q",
  "Q...",
  "..Q."
 ],

 [
  "..Q.",
  "Q...",
  "...Q",
  ".Q.."
 ]
]

题解

可以直接套上面模板。

这里做了个小优化:备忘录法,空间换时间,避免重复判断

# visited = new boolean[3][2*n]
# 这块判断时间复杂度从 O(n) 降为 O(1)
1. 判断每一列是否冲突:col, [0][col]
2. 主对角线各是否冲突:(row - col + n),[1][row - col + n] 
3. 副对角线各是否冲突:(row + col), [2][row + col]

2022-07-1411-44-25.png

AC 题解:

public class LeetCode_51 {

    // Time: O(n!), Space: O(n^2), Faster: 99.80%
    public List<List<String>> solveNQueens(int n) {
        List<List<String>> result = new ArrayList<>();
        // 记录是否访问过:列、主对角线、副对角线
        boolean[][] visited = new boolean[3][2*n];
        // 画棋盘
        char[][] board = new char[n][n];
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < n; ++j) {
                board[i][j] = '.';
            }
        }

        solve(0, n, result, board, visited);
        return result;
    }

    private void solve(int row, int n, List<List<String>> result,
                       char[][] board, boolean[][] visited) {
        if (row == n) {
            result.add(buildList(board));
            return;
        }
        for (int col = 0; col < n; ++col) {
            // 列、主对角线、副对角线
            if (!visited[0][col] && !visited[1][row-col+n] && !visited[2][row+col]) {
                board[row][col] = 'Q';
                visited[0][col] = visited[1][row-col+n] = visited[2][row+col] = true;
                solve(row+1, n, result, board, visited);
                visited[0][col] = visited[1][row-col+n] = visited[2][row+col] = false;
                board[row][col] = '.';
            }
        }
    }

    private List<String> buildList(char[][] board) {
        List<String> list = new ArrayList<>();
        for (char[] row: board) {
            list.add(new String(row));
        }
        return list;
    }
}

猜你喜欢

转载自juejin.im/post/7126566299840282660
今日推荐