算法及数据结构系列 - 回溯算法

系列文章目录
算法及数据结构系列 - 二分查找
算法及数据结构系列 - BFS算法
算法及数据结构系列 - 动态规划
算法及数据结构系列 - 双指针

回溯算法

框架思路

思路

解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:

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

代码框架

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」

经典题型

全排列问题

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

List<List<Integer>> res = new LinkedList<>();

/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
    // 记录「路径」
    LinkedList<Integer> track = new LinkedList<>();
    backtrack(nums, track);
    return res;
}

// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
    // 触发结束条件
    if (track.size() == nums.length) {
        res.add(new LinkedList(track));
        return;
    }
    
    for (int i = 0; i < nums.length; i++) {
        // 排除不合法的选择
        if (track.contains(nums[i]))
            continue;
        // 做选择
        track.add(nums[i]);
        // 进入下一层决策树
        backtrack(nums, track);
        // 取消选择
        track.removeLast();
    }
}

子集问题

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    public List<List<Integer>> subsets(int[] nums) {
        helper(new LinkedList<Integer>(), nums, 0);
        return res;
    }

    public void helper(LinkedList<Integer> path, int[] nums, int fromIndex){
        res.add(new LinkedList<Integer>(path));
        for(int i = fromIndex; i < nums.length; i++){
            path.add(nums[i]);
            helper(path, nums, i + 1); // 每次只能选择选过元素之后的元素
            path.removeLast();
        }
    }
}

组合问题

给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

提示: 通过指定 fromIndex 去重

class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    public List<List<Integer>> combine(int n, int k) {
        helper(k,n, new LinkedList<Integer>(), 1);
        return res;
    }
    public void helper(int k, int n, LinkedList<Integer> path , int fromIndex){
        if(k == 0){
            res.add(new LinkedList(path));
            return;
        }
        for(int i = fromIndex; i <= n ;i ++){
            path.add(i);
            helper(k - 1, n, path, i +1);
            path.removeLast();
        }
    }
}

N皇后问题

n个皇后不冲突的放在n*n 的棋盘上

注意:对角线也不能重复

提示: 按行放置

class Solution {
    List<List<String>> res = new ArrayList<List<String>>();
    public List<List<String>> solveNQueens(int n) {
        char[][] path = new char[n][n];
        for(int i = 0; i < n;i++){
            for(int j= 0;j < n;j ++){
                path[i][j] = '.';
            }
        }
        helper(path, 0, n, new HashSet<Integer>(), new HashSet<Integer>(), new HashSet<Integer>());
        return res;
    }

    public void helper(char[][] path, int row, int n, Set<Integer> cols, Set<Integer> angles1, Set<Integer> angles2){
        if(row == n){
            List<String> tmp = new ArrayList<String>();
            for(int i = 0; i < n; i ++){
                StringBuilder sb = new StringBuilder();
                for(int j = 0; j < n; j ++){
                    sb.append(path[i][j]);
                }
                tmp.add(sb.toString());
            }
            res.add(tmp);
            return;
        }
        for(int i = 0;i < n;i++){
            if(cols.contains(i)){
                // 纵向不能重复
                continue;
            }
            if(angles1.contains(row - i)){
                // 左上 - 右下 对角线不能重复
                continue;
            }
            if(angles2.contains(row + i)){
                // 左下 - 右上 对角线不能重复
                continue;
            }

            cols.add(i);
            angles1.add(row - i);
            angles2.add(row+i);
            path[row][i] = 'Q';
            helper(path, row+1, n, cols, angles1, angles2);
            cols.remove(i);
            angles1.remove(row - i);
            angles2.remove(row+i);
            path[row][i] = '.';
        }
    }
}

刷题

113.路径总和 II

给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。 说明: 叶子节点是指没有子节点的节点。 思路:回溯算法,路径/当前可选择/选择及撤销选择

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        List<Integer> path = new ArrayList<Integer>();
        if(root != null){
            path.add(root.val);
        }
        recurve(root, sum, path);
        return res;
    }

    public void recurve(TreeNode root, int sum, List<Integer> path){
        if(root == null){
            return;
        }
        if(root.left == null && root.right == null && sum == root.val){
            // 到叶子节点
            res.add(new ArrayList<Integer>(path));
            return;
        }
        if(root.left != null){
            path.add(root.left.val);
            recurve(root.left, sum - root.val, path);
            path.remove(path.size() - 1);
        }
        if(root.right != null){
            path.add(root.right.val);
            recurve(root.right, sum - root.val, path);
            path.remove(path.size() - 1);
        }
    }
}

31.下一个排列

实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。

如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

必须原地修改,只允许使用额外常数空间。

以下是一些例子,输入位于左侧列,其相应输出位于右侧列。

1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

提示: 从后往前; 顺序对; 大于a的最小的数

class Solution {
    public void nextPermutation(int[] nums) {
        int i;
        for(i = nums.length-1; i>=1; i -- ){
            // 从后向前找第一个逆序对
            if(nums[i] > nums[i-1]){
                // 从后向前找到最小的比它大的数字
                for(int j = nums.length - 1; j >= i; j --){
                    if(nums[j] >  nums[i - 1]){
                        // 交换
                        int tmp = nums[j];
                        nums[j] = nums[i-1];
                        nums[i - 1] = tmp;
                        break; 
                    }
                }
                break;
            }
        }

        // 从i开始翻转排序
        for(int j = i, k = nums.length - 1; j < k; j ++,k--){
            int tmp = nums[j];
            nums[j] = nums[k];
            nums[k] = tmp;
        }
    }
}

47. 全排列 II

给定一个可包含重复数字的序列,返回所有不重复的全排列。

示例:

输入: [1,1,2]
输出:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

提示: 回溯,剪枝,数字次数

class Solution {
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        Map<Integer, Integer> cnt = new HashMap<Integer, Integer>(); // 记录递归过程中哪些数字被用过了
        for(int i = 0; i < nums.length; i++){
            cnt.put(nums[i], cnt.getOrDefault(nums[i], 0) + 1);
        }
        LinkedList<Integer> path = new LinkedList<Integer>();
        helper(nums, path, cnt);
        return res;
    }

    public void helper(int[] nums, LinkedList<Integer> path, Map<Integer, Integer> cnt){
        if(path.size() == nums.length){
            res.add(new LinkedList(path));
            return;
        }
        Set<Integer> set = new HashSet<Integer>(); // 记录同一层有没有遍历过
        for(int i = 0; i < nums.length; i++){
            if(set.contains(nums[i])){
                continue; // 重复的数
            }
            if(cnt.get(nums[i]) == 0){
                continue; // 判断是否已经被用完
            }
            path.add(nums[i]);
            set.add(nums[i]);
            cnt.put(nums[i], cnt.get(nums[i]) - 1);

            helper(nums, path, cnt);

            path.removeLast();
            cnt.put(nums[i], cnt.get(nums[i]) + 1);
        }
    }
}

996. 正方形数组的数目

给定一个非负整数数组 A,如果该数组每对相邻元素之和是一个完全平方数,则称这一数组为正方形数组。

返回 A 的正方形排列的数目。两个排列 A1 和 A2 不同的充要条件是存在某个索引 i,使得 A1[i] != A2[i]。

提示: 思路同上

class Solution {
    int res = 0;
    public int numSquarefulPerms(int[] A) {
        if(A.length == 0){
            return res;
        }
        Map<Integer, Integer> cnt = new HashMap<Integer, Integer>(); // 记录递归过程中哪些数字被用过了
        for(int i = 0; i < A.length; i++){
            cnt.put(A[i], cnt.getOrDefault(A[i], 0) + 1);
        }
        helper(A, cnt, -1, 0);
        return res;
    }

    public void helper(int[] nums, Map<Integer, Integer> cnt, int last, int used){
        if(used == nums.length){
            res++;
            return;
        }
        Set<Integer> set = new HashSet<Integer>(); // 记录同一层有没有遍历过
        for(int i = 0; i < nums.length; i++){
            if(set.contains(nums[i])){
                continue; // 重复的数
            }
            if(cnt.get(nums[i]) == 0){
                continue; // 判断是否已经被用完
            }
            if(last >= 0 && !isSquare(nums[i] + last)){
                continue; //判断是否符合题目条件
            }
            set.add(nums[i]);
            cnt.put(nums[i], cnt.get(nums[i]) - 1);
            used ++;
            helper(nums, cnt, nums[i], used );
            cnt.put(nums[i], cnt.get(nums[i]) + 1);
            used--;
        }
    }

    public boolean isSquare(int num){
        int i;
        for(i = 0; i*i < num; i++){
        }
        if(i * i == num){
            return true;
        }
        return false;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值