❗⭕⭕算法——回溯(暴力穷举所有情况)

1 模板

回溯详解

  • 请输出所有组合/情况
  • 写backtrack函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集
  • 可以使用Linkedlist removeLast来实现撤销操作。
result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return

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

2 例题

2.1 全排列问题

详解1

问题

  • 输入一个不包含重复数字的数组 nums,返回这些数字的全部排列
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();
    }
}

2.2 组合问题

详情2
问题:

  • 输入两个数字 n, k,算法输出 [1…n] 中 k 个数字的所有组合。
vector<vector<int>>res;

vector<vector<int>> combine(int n, int k) {
    if (k <= 0 || n <= 0) return res;
    vector<int> track;
    backtrack(n, k, 1, track);
    return res;
}

void backtrack(int n, int k, int start, vector<int>& track) {
    // 到达树的底部
    if (k == track.size()) {
        res.push_back(track);
        return;
    }
    // 注意 i 从 start 开始递增
    for (int i = start; i <= n; i++) {
        // 做选择
        track.push_back(i);
        backtrack(n, k, i + 1, track);
        // 撤销选择
        track.pop_back();
    }
}

2.3 N皇后问题

详解1

问题

  • 给你一个 N×N 的棋盘,让你放置 N 个皇后,使得它们不能互相攻击。

  • PS:皇后可以攻击同一行、同一列、左上左下右上右下四个方向的任意单位。

vector<vector<string>> res;

/* 输入棋盘边长 n,返回所有合法的放置 */
vector<vector<string>> solveNQueens(int n) {
    // '.' 表示空,'Q' 表示皇后,初始化空棋盘。
    vector<string> board(n, string(n, '.'));
    backtrack(board, 0);
    return res;
}

// 路径:board 中小于 row 的那些行都已经成功放置了皇后
// 选择列表:第 row 行的所有列都是放置皇后的选择
// 结束条件:row 超过 board 的最后一行
void backtrack(vector<string>& board, int row) {
    // 触发结束条件
    if (row == board.size()) {
        res.push_back(board);
        return;
    }

    int n = board[row].size();
    for (int col = 0; col < n; col++) {
        // 排除不合法选择
        if (!isValid(board, row, col)) 
            continue;
        // 做选择
        board[row][col] = 'Q';
        // 进入下一行决策
        backtrack(board, row + 1);
        // 撤销选择
        board[row][col] = '.';
    }
}


/* 是否可以在 board[row][col] 放置皇后? */
bool isValid(vector<string>& board, int row, int col) {
    int n = board.size();
    // 检查列是否有皇后互相冲突
    for (int i = 0; i < n; i++) {
        if (board[i][col] == 'Q')
            return false;
    }
    // 检查右上方是否有皇后互相冲突
    for (int i = row - 1, j = col + 1; 
            i >= 0 && j < n; 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;
}

实战
leetcode51 N皇后问题

package com.zknode.回溯;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ti_51 {
    public ArrayList<List<String>> res = new ArrayList<>();
    int sizeQueue;
    public List<List<String>> solveNQueens(int n) {
        this.sizeQueue = n;
        //初始化棋牌
        char[][] ints = new char[sizeQueue][sizeQueue];
        for(int i =0; i<sizeQueue ;i++){
            for(int j =0; j<sizeQueue ;j++){
                ints[i][j] = '.';
            }
        }
//      开始回溯
        trackback(ints,0);
        return res;
    }

    //回溯,传入地图,第几行,至少传入遍历的图,第几行
    public void trackback(char[][] board,int row){
        //到底了,return
        if(sizeQueue == row){
            res.add(convert(board));
            return;
        }
        for(int col =0;col<sizeQueue;col++){
            if(!isValid(board,row,col)){
                continue;
            }
            board[row][col] = 'Q';
            trackback(board,row+1);
            board[row][col] ='.';
        }

    }

    private List<String> convert(char[][] board) {
        ArrayList<String> strings = new ArrayList<>();
        for(char[] i:board){
            strings.add(String.valueOf(i));
        }
        return strings;
    }

    //检查上方,右上左上是否有皇后
    private boolean isValid(char[][] board, int row, int col) {
//        上方
        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<sizeQueue;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;

    }



    public static void main(String[] args) {
        List<List<String>> lists = new ti_51().solveNQueens(4);
//        for(List<String> i:lists){
//            for(String j:i){
//                System.out.println(j);
//            }
//        }
        System.out.println(lists.toString());

    }

}

2.4 括号生成

详情

问题

  • LeetCode 第 22 题,请你写一个算法,输入是一个正整数n,输出是n对儿括号的所有合法组合
  • 可以理解成: 现在有2n个位置,每个位置可以放置字符(或者),组成的所有括号组合中,有多少个是合法的?
vector<string> generateParenthesis(int n) {
    if (n == 0) return {};
    // 记录所有合法的括号组合
    vector<string> res;
    // 回溯过程中的路径
    string track;
    // 可用的左括号和右括号数量初始化为 n
    backtrack(n, n, track, res);
    return res;
}

// 可用的左括号数量为 left 个,可用的右括号数量为 rgiht 个
void backtrack(int left, int right, 
            string& track, vector<string>& res) {
    // 若左括号剩下的多,说明不合法
    if (right < left) return;
    // 数量小于 0 肯定是不合法的
    if (left < 0 || right < 0) return;
    // 当所有括号都恰好用完时,得到一个合法的括号组合
    if (left == 0 && right == 0) {
        res.push_back(track);
        return;
    }

    // 尝试放一个左括号
    track.push_back('('); // 选择
    backtrack(left - 1, right, track, res);
    track.pop_back(); // 撤消选择

    // 尝试放一个右括号
    track.push_back(')'); // 选择
    backtrack(left, right - 1, track, res);
    track.pop_back();// 撤消选择
}

2.5 不重复的所有子集

详情

问题:

  • 问题很简单,输入一个不包含重复数字的数组,要求算法输出这些数字的所有子集。
vector<vector<int>> res;

vector<vector<int>> subsets(vector<int>& nums) {
    // 记录走过的路径
    vector<int> track;
    backtrack(nums, 0, track);
    return res;
}

void backtrack(vector<int>& nums, int start, vector<int>& track) {
    res.push_back(track);
    // 注意 i 从 start 开始递增
    for (int i = start; i < nums.size(); i++) {
        // 做选择
        track.push_back(nums[i]);
        // 回溯
        backtrack(nums, i + 1, track);
        // 撤销选择
        track.pop_back();
    }
}

2.6 重复的所有子集

问题

  • leetcode90. 子集 II

  • 给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

  • 解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

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

分析

  • 回溯暴搜
  • 回溯决策:添加该结点,回溯下一层,(集合均有该节点);删除该节点,进入下一个决策结点。(集合不包含该节点)
  • 结果集合添加时机:找到一组就加一组。以为是子集不像排列组合,长度固定。
  • 决策层:从当前index开始往后找,因为不考虑顺序性,考虑顺序性就要按照全排列做,从头开始扫描

代码

package com.zknode.回溯;

import java.util.*;


//给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
//
//解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
//
// 
//子集不要求顺序,可以排序减少工作量
//不能包括重复子集,可以使用set去重
//所有组合情况,可以考虑使用回溯暴力搜索
//双指针扫描也可以
public class ti_90 {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        Set<List<Integer>> res = new HashSet<>();
        ArrayList<Integer> list = new ArrayList<>();
        dfs(nums,0,res,list);
        return new ArrayList<>(res);

    }

    //回溯必要三要素,路径集合,当前位置,当前路径选择,总结果可加可不加,可以作为成员变量
    public void dfs(int[] nums, int index, Set<List<Integer>> res,List<Integer> list){
		//找到一个加一个
		res.add(new ArrayList<>(list));
		//index开始的可以不加index
        for(int i = index ;i<nums.length ;i++){
            //加入当前结点
            list.add(nums[i]);
            dfs(nums,i+1,res,list);
            //撤销选择,不考虑当前结点
            list.remove(list.size()-1);
        }

    }
}

2.7 数独问题

详解
问题

boolean backtrack(char[][] board, int r, int c) {
    int m = 9, n = 9;
    if (c == n) {
        // 穷举到最后一列的话就换到下一行重新开始。
        return backtrack(board, r + 1, 0);
    }
    if (r == m) {
        // 找到一个可行解,触发 base case
        return true;
    }
    // 就是对每个位置进行穷举
    for (int i = r; i < m; i++) {
        for (int j = c; j < n; j++) {

            if (board[i][j] != '.') {
                // 如果有预设数字,不用我们穷举
                return backtrack(board, i, j + 1);
            } 

            for (char ch = '1'; ch <= '9'; ch++) {
                // 如果遇到不合法的数字,就跳过
                if (!isValid(board, i, j, ch))
                    continue;

                board[i][j] = ch;
                // 如果找到一个可行解,立即结束
                if (backtrack(board, i, j + 1)) {
                    return true;
                }
                board[i][j] = '.';
            }
            // 穷举完 1~9,依然没有找到可行解,此路不通
            return false;
        }
    }
    return false;
}

boolean isValid(char[][] board, int r, int c, char n) {
       for (int i = 0; i < 9; i++) {
        // 判断行是否存在重复
        if (board[r][i] == n) return false;
        // 判断列是否存在重复
        if (board[i][c] == n) return false;
        // 判断 3 x 3 方框是否存在重复
        if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n)
            return false;
    }
    return true;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zkFun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值