经典回溯算法五大问题思路与详解

引言

        如果给你一道题,给定两个整数 10 和 2,让你返回范围 [1, 10] 中所有可能的 2 个数的组合。

        我们可能会用嵌套两层for循环求解。

        那如果给定两个整数100和10呢,1000和100呢,又或者是完全未知的n和k呢?这时候就连for循环嵌套都无从下手。

        这时候,就需要回溯算法


回溯算法是什么

  • 回溯算法本质是一种暴力的穷举。
  • 回溯法解决的问题都可以抽象为树形结构。
  • 并且回溯算法有其固定的模板。

    回溯算法五大经典题型和模板

    • 回溯算法主要解决五类题型:

    题型

    核心目标

    组合问题

    N 个数中按规则找 k 个数的集合

    切割问题

    字符串按规则切割的方式数

    子集问题

    N 个数集合中符合条件的子集数量 / 种类

    排列问题

    N 个数按规则全排列的方式数

    棋盘问题

    解决 N 皇后、解数独等棋盘类约束问题

    • 回溯算法的模板:
    void backtracking(参数) {
        //1.判断终止
        if (终止条件) {
            存放结果;
            return;
        }
    
        //2.遍历集合,收集元素
        for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
            处理节点;
    
        //3.递归和回溯
            backtracking(路径,选择列表); // 递归
            回溯,撤销处理结果
        }
    }

    题型一:组合问题

    • 此处给出的例题是力扣 77.组合

    • 分析:

            在套用回溯算法的过程中需要注意去重,因为[1,2]和[2,1]是一个组合,不能重复算入。

    本题树形结构

    本题模板套用

    vector<int> path;
    vector<vector<int>> result;
    void backtracking(int n, int k, int startidx) {
            //1.判断终止(当path内存入数据大于等于k终止)
            if (path.size() >= k) {
                result.push_back(path);
                return;
            }
            //2.遍历集合,i从startidx开始是去重操作,收集元素
            for (int i = startidx; i <= n; i++) {
                path.push_back(i);
            //3.递归和回溯,pop掉path最后一个元素是溯回的过程
                backtracking(n, k, i + 1);
                path.pop_back();
            }
        }
    • 完整题解代码:
    class Solution {
    public:
        void backtracking(int n, int k, int startidx) {
            if (path.size() >= k) {
                result.push_back(path);
                return;
            }
            for (int i = startidx; i <= n; i++) {
                path.push_back(i);
                backtracking(n, k, i + 1);
                path.pop_back();
            }
        }
        vector<vector<int>> combine(int n, int k) {
            backtracking(n, k, 1);
            return result;
        }
        vector<int> path;
        vector<vector<int>> result;
    };

     题型二:切割问题

    • 此处给出的例题是力扣 131.分割回文串

    • 分析:

            本题和组合问题非常像,在套用回溯算法的过程中要注意去重,已经截取过的部分得去掉,不能重复截取。

    本题树形结构

    本题模板套用

    void backtracking(string &s, int startidx) {
            //1.判断终止
            if (startidx >= s.size()) {
                result.push_back(path);
                return;
            }
            //2.去重遍历
            for (int i = startidx; i < s.size(); i++) {
                //此处判断是否为回文字符串,若是则收集元素,不是就跳过
                if (isPalindrome(s, startidx, i)) {
                    string str = s.substr(startidx, i - startidx + 1);
                    path.push_back(str);
                } else {
                    continue;
                }
            //3.递归和回溯
                backtracking(s, i + 1);
                path.pop_back();
            }
        }
    • 完整题解代码:
    class Solution {
    public:
        bool isPalindrome(string &s, int startidx, int i) {
            int left = startidx;
            int right = i;
            while (left < right) {
                if (s[left] != s[right]) {
                    return false;
                }
                left++;
                right--;
            }
            return true;
        }
    
        void backtracking(string &s, int startidx) {
            if (startidx >= s.size()) {
                result.push_back(path);
                return;
            }
            for (int i = startidx; i < s.size(); i++) {
                if (isPalindrome(s, startidx, i)) {
                    string str = s.substr(startidx, i - startidx + 1);
                    path.push_back(str);
                } else {
                    continue;
                }
                backtracking(s, i + 1);
                path.pop_back();
            }
        }
        vector<vector<string>> partition(string &s) {
            backtracking(s, 0);
            return result;
        }
        vector<string> path;
        vector<vector<string>> result;
    };

     题型三:子集问题

    • 此处给出的例题是力扣 78.子集

    • 分析:

            本题在大体思路上与组合问题有些相似,但要注意的是,本题在收集结果的过程中,需要收集所有结果,而不是等数组内元素达到一个规定个数再进行收集。

    本题树形结构

    本题模板套用

    vector<int> path;
    vector<vector<int>> result;
        //引用形式调用是为了提高运行速度
        void backtracking(vector<int>& nums, int startidx) {
            //先做元素收集是因为要取每一个子集,包括空集
            result.push_back(path);
            //1.判断终止
            if (startidx >= nums.size()) {
                return;
            }   
            //2.遍历与元素收集
            for (int i = startidx; i < nums.size(); i++) {
                path.push_back(nums[i]);
            //3.递归与回溯
                backtracking(nums, i + 1);
                path.pop_back();
            }
        }
    • 完整题解代码:
    class Solution {
    public:
        vector<int> path;
        vector<vector<int>> result;
        void backtracking(vector<int>& nums, int startidx) {
            result.push_back(path);
            if (startidx >= nums.size()) {
                return;
            }
            for (int i = startidx; i < nums.size(); i++) {
                path.push_back(nums[i]);
                backtracking(nums, i + 1);
                path.pop_back();
            }
        }
        vector<vector<int>> subsets(vector<int>& nums) {
            backtracking(nums, 0);
            return result;
        }
    };

     题型四:排列问题

    • 此处给出的例题是力扣 46.全排列

    • 分析:

            本题中[1,2]和[2,1]算作两个可能的全排列,与组合问题不用,也就是说,本题不需要向前去重,但要保证每一个结果内包含nums中的所有元素,所以需要对nums内的每一个元素做记号,来记录其时候已经被录入全排列数组中。(我们这里选择创建一个全为0的used数组,当对应位置的元素被录入结果集合,将0变为1)

    本题树形结构

    本题模板套用

    vector<int> path;
    vector<vector<int>> result;
        //引用传递减少运行速度
        void backtracking(vector<int>& nums, vector<int>& used) {
            //1.当path内元素个数与数组个数相同,代表全排列完成
            if (path.size() == nums.size()) {
                result.push_back(path);
                return;
            }
            //2.遍历,若元素再used数组中未被记录,加入排列中
            for (int i = 0; i < nums.size(); i++) {
                if (used[i] == 1) {
                    continue;
                }
                path.push_back(nums[i]);
            //3.递归与溯回,这次不光要怕pop元素,还要清除used数组中对于此元素的记录
                used[i] = 1;
                backtracking(nums, used);
                used[i] = 0;
                path.pop_back();
            }
        }
    • 完整题解代码:
    class Solution {
    public:
        vector<int> path;
        vector<vector<int>> result;
        void backtracking(vector<int> &nums, vector<int>& used) {
            if (path.size() == nums.size()) {
                result.push_back(path);
                return;
            }
            for (int i = 0; i < nums.size(); i++) {
                if (used[i] == 1) {
                    continue;
                }
                path.push_back(nums[i]);
                used[i] = 1;
                backtracking(nums, used);
                used[i] = 0;
                path.pop_back();
            }
        }
        vector<vector<int>> permute(vector<int>& nums) {
            vector<int> used(nums.size(), 0);
            backtracking(nums, used);
            return result;
        }
    };

     题型五:棋盘问题

    • 此处给出的例题是力扣 51. N皇后

    • 分析:

            我们以每一行(row)为单位,一行内的所有元素遍历,去除不符合条件的(即同行或者同列或者斜角有元素),得出最后的结果。

    本题树形结构

    本题模板套用

    vector<vector<string>> result;
        void backtracking(vector<string>& queenChess, int n, int row) {
            //1.行数到达最后,就终止
            if (row >= n) {
                result.push_back(queenChess);
                return;
            }
            //2.遍历每一行的可填空格
            for (int i = 0; i < n; i++) {
                //此处判断是否为可填入空格
                if (isValid(row, i, queenChess, n)) {
                    queenChess[row][i] = 'Q';
            //3.递归与溯回
                    backtracking(queenChess, n, row + 1);
                    queenChess[row][i] = '.';
                } else {
                    continue;
                }
            }
        }
    • 完整题解代码:
    class Solution {
    public:
        bool isValid(int row, int column, vector<string>& queenChess, int n) {
            for (int i = 0; i < row; i++) {
                if (queenChess[i][column] == 'Q') {
                    return false;
                }
            }
            for (int i = row - 1, j = column - 1; j >= 0 && i >= 0; i--, j--) {
                if (queenChess[i][j] == 'Q') {
                    return false;
                }
            }
            for (int i = row - 1, j = column + 1; i >= 0 && j < n; i--, j++) {
                if (queenChess[i][j] == 'Q') {
                    return false;
                }
            }
            return true;
        }
        vector<vector<string>> result;
        void backtracking(vector<string>& queenChess, int n, int row) {
            if (row >= n) {
                result.push_back(queenChess);
                return;
            }
            for (int i = 0; i < n; i++) {
                if (isValid(row, i, queenChess, n)) {
                    queenChess[row][i] = 'Q';
                    backtracking(queenChess, n, row + 1);
                    queenChess[row][i] = '.';
                } else {
                    continue;
                }
            }
        }
        vector<vector<string>> solveNQueens(int n) {
            vector<string> queenChess(n, string(n,'.'));
            backtracking(queenChess, n, 0);
            return result;
        }
    };

    总结

    • 回溯算法本质是暴力穷举,问题可抽象为树形结构,有固定解题模板。

    • 核心解决组合、切割、子集、排列、棋盘五类典型问题。

    • 模板核心三步:判断终止、遍历选择元素、递归 + 回溯。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值