回溯算法C++详解(知识点+相关LeetCode题目)

目录

前言

一、回溯算法相关知识

1.1 回溯算法用于解决的问题

1.2 回溯算法使用模板

二、回溯算法经典力扣题目

组合 77

组合总和三 216

电话号码的字母组合 17

组合总和 39

组合总和二

分割字符串 131

复原ip地址 93

子集 78

子集2:

递增子序列 491

全排列 46 

全排列 二 

N皇后 51

解数独 37


前言

本文章将从回溯算法的适用场景,使用方式展开,然后落实到使用回溯算法的经典力扣题目中,希望能对大家有所帮助

一、回溯算法相关知识

回溯法可抽象为n叉树的结构,宽度为集合的大小,纵方向为递归处理(只要有递归,就会有回溯),其本身属于纯暴力搜索  

1.1 回溯算法用于解决的问题

组合问题(组合为无序,【1,2】和【2,1】意义相同),可以将回溯算法抽象为树,组合的结果就是树中的树叶,而回溯算法的路径就构成了整个树的结构

切割问题(字符串切割的方式),子集问题,这类问题,就需要对回溯算法中的单层搜索逻辑进行限制,使得满足条件

排列问题,这属于对组合问题的一个延伸,可见的是会对剪枝操作进行一定的减少限制

棋盘问题 ,这是对回溯算法的直观演绎,因为需要对每一个棋子进行判断回溯

1.2 回溯算法使用模板

模板:终止条件+单层搜索逻辑

终止条件:(在递归回溯函数的最上面),对传入的元素进行首先判断,一般与return搭配使用

单层搜索逻辑:这里一般是遍历形成,结构是循环中,判断成立条件,设置数组存入元素后,进行递归回溯,然后再从数组弹出元素。可以概括为,收集结果(集合),集合元素,处理节点。


二、回溯算法经典力扣题目

组合 77

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

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
class Solution {
private:
    vector<vector<int>> result;
    vector<int> single;
    void GetResult(int n, int k,int startindex) {
        //终止条件
        if(single.size() == k){
            result.push_back(single);
            return;
        }

        //递归--从startindex开始
        for(int i = startindex;i<=n;i++){
            single.push_back(i);
            GetResult(n,k,i+1);
            single.pop_back();//回溯,去掉之前遍历的节点
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        GetResult(n,k,1);
        return result;
    }
};

题解:

本题用回溯算法解题,设置回溯函数的关键分别为终止条件和递归算法,在本题中,使用一个二维数组保存另一个递归获得的一维数组的思路,当递归中的数组长度满足要求,就向上回溯,清空当前存储的值,重新以上节点向下递归遍历。依次遍历,最终返回存储了这些数组的二维数组


组合总和三 216

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次 

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
class Solution {
private:
    vector<vector<int>> result;//保存的数组
    vector<int> single;
    void GetResult(int targerSum,int k,int sum,int currentIndex){
        //终止条件
        if(single.size()==k){
            if(sum == targerSum){
                //满足条件则返回
                result.push_back(single);
            }
            return;
        }

        //递归过程
        for(int i = currentIndex;i<=9;i++){
            sum += i;
            single.push_back(i);
            GetResult(targerSum,k,sum,i+1);//组合

            //回溯
            single.pop_back();
            sum -= i;
        }

    }

public:
    vector<vector<int>> combinationSum3(int k, int n) {
        GetResult(n,k,0,1);
        return result;
    }
};

题解:

这一题采用的思路和解法类似于组合这道题,思路上仍然是二维数组依次保存一维数组,结构上是先设置终止条件,后设置递归过程,跟组合那道题不同的是,终止条件还需要满足sum和目标sum相同才会存储进入二维数组,递归过程要sum加上,回溯需要sum减去对应的值。通过以上过程,最终得到包含所有组合的二维数组。


电话号码的字母组合 17

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution {
private:
    const string letters[10]={
        "",//索引为0
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz",
    };
    vector<string> result;
    void GetResult(const string& digits,int index,const string& s){
        if(digits.size()==index){
            result.push_back(s);//存入结果数组
            return;
        }

        int currentindex = digits[index]-'0';//获得当前第一个数字
        string currentss = letters[currentindex];//获得当前的string

        //从当前的string向下遍历
        for(int i=0;i<currentss.size();i++){
            //遍历过程
            GetResult(digits,index+1,s+currentss[i]);
        }
    }  
public:
    vector<string> letterCombinations(string digits) {
        result.clear();
        if(digits.size()==0){
            return result;
        }
        GetResult(digits,0,"");
        return result;
    }
};

 题解:

首先确定本题思路为回溯算法,需要通过最初字符串的每一个字符,按顺序从第一个字符开始,获得它对应的字符串,然后从字符串的每一个字符向下递归。具体实现如下:

终止条件是当前index(保存递归次数)和最初字符串长度相同,如果相同就将之前传进来的字符串s存入。在接下来的递归过程中,每次传入最初字符串、index+1(标志着向下递归)和需要保存入最终string<vector>容器的字符串(该字符串为当前得到的字符串加上当前遍历的字符)。如此不断递归,回溯上来之后,遍历继续向下进行(脑图上回溯呈波浪式


组合总和 39

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
class Solution {
private:
    vector<vector<int>> result;
    vector<int> single;
    void GetResult(vector<int>& candidates,int target,int startIndex,int sum){
        //终止条件
        if(sum > target){
            return;//不满足也要回溯上去
        }
        if(sum == target){
            result.push_back(single);
            return;
        }
        //单层搜索逻辑
        for(int i =startIndex;i<candidates.size();i++){
            sum += candidates[i];
            single.push_back(candidates[i]);//存储和刷新sum和
            GetResult(candidates,target,i,sum);

            //回溯的刷新
            sum -= candidates[i];
            single.pop_back();
        }
    }

public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        //传入数组和目标值
        GetResult(candidates,target,0,0);
        return result;
    }
};

题解:

本题是组合问题的一种形式,区别于上面几道组合题,这道组合题不限制单个组合的数量,这就导致转化成树无法计算树的高度,需要优化"单层搜索的逻辑",递归的时候,每次传入的startindex就是当前的数的索引(因为可以无限选取),还需要优化"终止条件",对不满足的和满足的进行判断,并且返回,注意如果当前的和已经大于了目标值,就没必要向下继续讨论


组合总和二

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。 

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
class Solution {
private:
    vector<vector<int>> result;
    vector<int> comb;
    void GetResult(vector<int>& candidates,int target,int sum,int startindex,vector<bool>& used){
        if(sum >target){
           return;
        }
        if(sum == target){
            result.push_back(comb);
            return;
        }

        //单层搜索逻辑
        for(int i = startindex;i<candidates.size();i++){
            if(i>0 && candidates[i]==candidates[i-1] && used[i-1] == false){
                //used判定的作用这个时候就体现
                //used如果为负,作为树枝的根节点就使用过了
                continue;//当前情况说明前一个根节点使用过了,避免重合,则跳过当前准备使用的根节点
            }
            sum += candidates[i];
            used[i] = true;//作为树枝的根节点还没有被使用
            comb.push_back(candidates[i]);
            GetResult(candidates,target,sum,i+1,used);//向下递归
            comb.pop_back();
            used[i] = false;//作为树枝的根节点就使用过了
            sum -= candidates[i];

        }
    }

public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(),false);//定义标识数组
        result.clear();
        comb.clear();
        sort(candidates.begin(),candidates.end());
        GetResult(candidates,target,0,0,used);
        return result;
    }
};

题解:

本题主要是考虑到如何去重,其他的逻辑和之前组合问题相似,也是在递归函数中,设置终止条件和单层搜索逻辑,不同的是,搜索逻辑上需要上一层锁,为了防止重复,设置used布尔数组,在每次传入前设置当前索引对应值为真,代表还没有作为根节点使用过,当回溯上来后,再设置当前索引对应值为假,这样就可以在一开始判断,如果前一个被使用过,并且值相同,就不再对这个节点进行逻辑操作(因为会重复)。其他地方和一般组合问题相同,索引加1传入递归函数,向下递归


分割字符串 131

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
class Solution {
private:
    vector<vector<string>> result;
    vector<string> single;//小组合
    vector<vector<bool>> isOFM;//回文串判断数组
    void GetRusult(const string& s,int startIndex){
        //终止条件
        if(startIndex >= s.size()){
            result.push_back(single);//当前的single已经是一个结果集合了
            return;//已经没有可遍历的了,就终止并返回
        }


        //单层搜索逻辑-->从第一个数字起一个个向后讨论
        for(int i=startIndex;i<s.size();i++){
            if(isOFM[startIndex][i]){
                //回文串
                string str = s.substr(startIndex,i-startIndex+1);//切割出从startindex开始并且长度是到i的字符串
                single.push_back(str);
            }else{
                continue;//否则继续遍历寻找
            }
            
            GetRusult(s,i+1);//如果当前已经满足,则分割,向下递归
            single.pop_back();

        }
    }


    void JudgeISOFM(const string& s){
        isOFM.resize(s.size(),vector<bool>(s.size(),false));//将isofm的每个元素(一维数组)赋值为大小为size,值为false的一维数组
        for(int i = s.size()-1;i>=0;i--){
            for(int j = i;j<s.size();j++){
                //从后往前的遍历方式,为了回文串判断与定义的方便
                if(j == i){
                    isOFM[i][j] = true;//重合一定自身为回文串
                }else if(j-i==1){
                    isOFM[i][j] = (s[i]==s[j]);//挨着的时候,一定要彼此相等
                }else{
                    isOFM[i][j] = (s[i]==s[j] && isOFM[i+1][j-1]);//不挨着的时候,因为j在逐渐像size靠近,只要缩小区间还能相等,那么内部一定完全对称--回文串
                }
            }
        }
    }
public:
    vector<vector<string>> partition(string s) {
        result.clear();
        single.clear();
        JudgeISOFM(s);
        GetRusult(s,0);
        return result;

    }
};

题解:

本题不同于一般的组合问题,首先是递归思路上面,需要讨论所有的字符串的切割可能,所以思路是从0开始切割,直到满足回文串后,切割成为一个切割集合中的一个元素,并且从下一个元素开始递归,终止条件也要改变为判断到最后一个元素才返回,区别是在于本题要求切割集合的集合,所以才有了以上的改变。在判断回文串方面,使用倒序讨论,分三种情况-重合、挨着、有距离,分别进行对称相同判断,同时在“有距离”的判断中,也使用到了类似递归的一个思维,就是如果缩短范围仍然满足回文串,那么才能说明当前范围内是满足回文串的。


复原ip地址 93

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245""192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
class Solution {
private:
    vector<string> result;//字符串集合
    void GetResult(string& s,int startIndex,int pointcount){
        if(pointcount == 3){
            //已经打上了3个点
            if(isVaild(s,startIndex,s.size()-1)){
                result.push_back(s);//保存入结果
            }
            return;
        }

        for(int i = startIndex;i<s.size();i++){
            if(isVaild(s,startIndex,i)){
                //startindex始终作为前一个切割点
                pointcount++;
                s.insert(s.begin()+i+1,'.');//在指定位置之前插入一个.
                GetResult(s,i+2,pointcount);//从插入的位置的下个位置开始切割
                pointcount--;
                s.erase(s.begin()+i+1);//回溯删除所有的
            }else{
                break;
                //不满足就直接退出循环
            }
        }
    }
    bool isVaild(string& s,int start,int end){
        if(start > end){
            return false;
        }
        if(s[start] == '0' && start != end){
            //该数字以零开头,自然不合理
            return false;
        }
        int sum = 0;
        for(int i = start;i<=end;i++){
            if(s[i] < '0' ||s[i] > '9'){
                return false;//确保为正数字
            }
            sum = sum*10 + (s[i] - '0');//遍历使得sum为当前的数字总和
            if(sum > 255){
                return false;
            }
        }
        return true;
    }
public:
    vector<string> restoreIpAddresses(string s) {
        result.clear();
        if(s.size()<4 || s.size()>12){
            return result;//剪枝操作
        }
        GetResult(s,0,0);
        return result;
    }
};

题解:

这道题类似于上一题切割回文串,这里也是要切割但是不同的是,这一题的切割是切割完了之后,就直接加上一个点,然后切割的逻辑和回文一致,都是切割到底,直到满足后存入vector容器,再向上回溯。本题也需要设置一个判断的条件,判断字符串是否有效,涉及到ip地址的相关知识,不能大于225并且为一个正整数,并且数字组合不可以以0开头(重要)


子集 78

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
class Solution {
private:
    vector<vector<int>> result;
    vector<int> single;
    void GetResult(vector<int>& nums,int startindex){
        result.push_back(single);//每次递归的时候,需要把之前的数组立刻存储进入结果集-->同时包括了空的子集
        if(startindex >= nums.size()){
            return;
        }

        for(int i =startindex;i<nums.size();i++){
            single.push_back(nums[i]);
            GetResult(nums,i+1);//组合问题从下一个开始遍历
            single.pop_back();
        }
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        result.clear();
        single.clear();
        GetResult(nums,0);
        return result;
    }
};

题解-注意点:

本题用到了回溯算法,需要将各种组合放入集合中,将整道题的思路变为树型结构后不难发现,每个节点都是题目需要的集合元素。需要注意的一点是,需要将每次的裁剪过的元素存入数组中,且没有限制,一个是因为要考虑空集也是子集,还有一个是子集也可以是真子集。在遍历条件方面也和之前的一样,而且比较直观,就是先存入vector容器中,然后递归,回溯上来再弹出存入的元素即可。


子集2:

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

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

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
class Solution {
private:
    vector<vector<int>> result;
    vector<int> single;
    void GetResult(vector<int>& nums,int startindex,vector<bool>& numUsed){
        result.push_back(single);//每个节点都是子集

        if(startindex >= nums.size()){
            return;
        }

        for(int i = startindex;i< nums.size();i++){
            if(i>0 && nums[i] == nums[i-1] && numUsed[i-1] == false){
                continue;//前一个节点不是当前节点的树枝父节点还相等,则跳过该节点
            }
            single.push_back(nums[i]);
            numUsed[i] = true;//树枝已经取用了该点
            GetResult(nums,i+1,numUsed);//向下遍历,防止重复
            numUsed[i] = false;//该节点不再作为树枝使用
            single.pop_back();
        }

    }
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        result.clear();
        single.clear();
        vector<bool> numUsed(nums.size(),false);//false说明该节点和节点平行,位于同一树枝下
        sort(nums.begin(),nums.end());//排序,从小到大
        GetResult(nums,0,numUsed);
        return result;
    }
};

题解:

这题和子集1相比加入了一次去重的逻辑,在这里使用vector布尔数组进行去重的保存,在每次遍历之前将当前数组元素,设置为真,用来标记改元素为树枝元素,所以当向下递归的时候,前一个元素与当前元素相同的时候,不要急着去舍弃掉本次递归,要确定好是不是同一层的节点(在同一个树枝下),所以如果前一个元素的vector布尔值为真,说明它是之前的树枝,而本元素是其单独的树叶,这个时候是不需要去重的。相反如果是同一层的,那么前一个节点一定被使用过了,所以这个节点就不需要再向下递归,此时需要去重。最终在执行开始逻辑时,记得对目标数组进行排序,这样才能有序去重


递增子序列 491

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
class Solution {
private:
    vector<vector<int>> result;
    vector<int> single;
    void GetResult(vector<int>& nums,int startindex){
        if(single.size()>1){
            result.push_back(single);
            //不需要return因为接下来还可以继续存储
        }

        unordered_set<int> used;
        for(int i = startindex;i< nums.size();i++){
            //去重
            if(!single.empty() && nums[i] < single.back()  || used.find(nums[i])!= used.end()){
                continue;
                //分为两种情况 ,一种如果当前元素比之前最后的元素要小,就不必存了,因为已经不为递增
                //第二种是当前层(已经默认是当前的层,因为每次要新创建used)如果之前存在过该元素的值,去重
            }
            used.insert(nums[i]);
            single.push_back(nums[i]);
            GetResult(nums,i+1);
            single.pop_back();
        }
    }
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear();
        single.clear();
        GetResult(nums,0);
        return result;
    }
};

题解:

本题是组合问题的一个变形,需要注意几点。一个要注意不能重复元素(去重),一个要保证元素递增。但是本题的前置条件已经简化了这些,题目要求是子序列,说明顺序要按照目标数组来,这样就还是按照顺序遍历处理即可。限制条件中,需要保证是2个元素以上,则需要在一开始的“终止条件”中设置存储限制,同样本题需要所以子序列,所以不用立刻去存储。在去重方面,当前层(已经默认是当前的层,因为每次要新创建used)如果之前存在过该元素的值,则去重该元素。在元素递增方面,当前元素比之前存储的子序列数组最后的元素要小,就不必存了,因为已经不为递增。综上进行递归回溯,则完成本题


全排列 46 

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
class Solution {
private:
    vector<vector<int>> result;
    vector<int> single;
    void GetResult(vector<int>& nums,vector<bool>& used){
        if(single.size() == nums.size()){
            result.push_back(single);
            return;//直到大小满足了,才说明当前的single满足要求
        }

        for(int i =0; i<nums.size();i++){
            //每一个元素都是需要必要遍历的
            if(used[i] == true){
                continue;//说明之前-树枝-已经出现过了该节点
            }
            used[i] = true;
            single.push_back(nums[i]);
            GetResult(nums,used);
            single.pop_back();
            used[i] = false;
        }
    }
public:
    vector<vector<int>> permute(vector<int>& nums) {
        result.clear();
        single.clear();
        vector<bool> used(nums.size(),false);
        GetResult(nums,used);
        return result;
    }
};

题解:

本题实际上是组合的引申,有很多组合问题的影子。首先不含重复元素,就可以用到used布尔数组,保存之前树枝的真值,如果为真,则本树叶结点不需要再存入排列中。其次是所有可能的全排列,则需要更改单层处理逻辑为,从0开始遍历,并且在“终止条件”中,当排列数组的大小和目标数组一致的时候,则满足了条件,存入结果集并且回溯


全排列 二 

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

示例 1:

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]
class Solution {
private:
    vector<vector<int>> result;
    vector<int> single;
    void GetResult(vector<int>& nums,vector<bool>& used){
        if(single.size() == nums.size()){
            result.push_back(single);
            return;
        }

        for(int i=0;i<nums.size();i++){
            //全排列去重,需要保证之前遍历过的不能再次遍历-->为true说明为树枝的子节点,所以不用去重
            if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false){
                continue;//同一层级继续遍历
            }
            if(used[i] == false){
                single.push_back(nums[i]);
                used[i] = true;
                GetResult(nums,used);
                used[i] = false;
                single.pop_back();
            }//没有了startindex,则用used作为时刻判断条件
            
        }
    }
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        sort(nums.begin(),nums.end());
        GetResult(nums,used);
        return result;
    }
};

题解:

在本题中,注意到全排列,也就是要保证每种顺序都是一种解,所以不存在startindex。但是本题要求全排列不重复且可包含重复数字。如果不含重复数字,只要限制树枝出现过的节点,子节点不再出现即可。但是可包含的情况下,就不能简单的限制出现了,为了保证全排列不重复,那就需要保证树枝的出现,一定是不和前面的树枝重复的,只要树枝相同重复了,那么向下的解一定会重复,同时要确定当前的节点,是没有使用过的,不然如果树枝和树叶重复(这里不仅仅是指数值,而是整体的索引,为了不重复,不可以重复利用索引的,这一点类似组合),这样也是不满足题目要求的,会溢出解。这样检查过之后,才可以存入并进行下面的递归回溯


N皇后 51

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。

示例 1:

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
class Solution {
private:
    vector<vector<string>> result;
    void GetResult(vector<string>& single,int n,int row){
        if(row == n){
            //注意是row到达需求,因为single大小是一开始定好的
            result.push_back(single);
            return;
        }

        //对每一行进行遍历
        for(int i = 0;i<n;i++){
            if(isvalid(row,i,n,single)){
                single[row][i] = 'Q';//字符‘’引用
                GetResult(single,n,row+1);
                single[row][i] = '.';//回溯返回重新开始一套解
            }
        }
    }

    bool isvalid(int row,int col,int n,vector<string>& single)
    {
        //检查列--上检查--回溯
        for(int i = 0;i< row ;i++){
            if(single[i][col] == 'Q'){
                return false;//说明已经存在
            }
        }

        //检查行--左检查--回溯
        for(int i = 0;i<col;i++){
            if(single[row][i] == 'Q'){
                return false;//说明已经存在
            }
        }

        //检查斜线--45度--向左上--回溯
        for(int i = row -1, j = col -1;i>=0 && j>=0;i--,j--){
            //每个变量的声明和初始化应该用逗号分割,并且不需要再加上声明类型
            if(single[i][j] == 'Q'){
                return false;
            }
        }

        //检查斜线--135度--向右上--回溯
        for(int i = row -1, j = col +1;i>=0 && j<n ;i--,j++){
            if(single[i][j] == 'Q'){
                return false;
            }
        }

        return true;

    }

public:
    vector<vector<string>> solveNQueens(int n) {
        result.clear();
        vector<string> single(n,string(n,'.'));//创建内部由固定元素string字符串组成的数组
        GetResult(single,n,0);
        return result;
    }
};

题解:

本题N皇后,完美的应用了回溯算法的知识。仍然使用树形结构进行思维,可知每种结果,就是一个树叶节点,从棋盘上的第一行第一列开始遍历,不断向下遍历。遍历到结尾的,也就是树的深度等于目标n,则存入结果而二维数组。每种结果的保存方式也比较不同,创建一个string数组,string为字符串所以其每一个字符都是一个元素,综上这个string数组其实可以看做是一个二维数组。在最重要的单层判断逻辑上,需要从当前的棋字(元素)向上向之前去遍历,具体表现为向上遍历(到本节点前),向左遍历,向左上遍历,向右上遍历,一旦发现元素为'Q',则说明该节点不能成为皇后,则切断该树枝,不再向下递归回溯。

最终通过创建数组,进行递归,剪枝,进行回溯的方式,返回得到目标数组


解数独 37

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

示例 1:

输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:

class Solution {
private:
    bool GetResult(vector<vector<char>>& board){
        for(int i = 0;i<board.size();i++){
            for(int j = 0;j<board[0].size();j++){
                if(board[i][j] == '.'){
                    for(char k = '1';k<='9';k++){
                        //字符遍历
                        if(isVaild(board,i,j,k)){
                            board[i][j] = k;
                            if(GetResult(board)){
                                return true;//在当前的节点赋值下,如果能找到解决方案,就返回真
                            }
                            board[i][j] = '.';//没有找到方案,就继续向下遍历
                        }
                    }
                    return false;//遍历后仍然到了这里,说明没有找到可以放置的节点,没有方案
                }
                
            }
        }
        return true;//到这一步同样返回真
    }

    bool isVaild(vector<vector<char>>& board,int row,int col,int k){

        //检查同一行-有没有和目标值一样的元素
        for(int i = 0 ;i<board[0].size();i++){
            if(board[row][i] == k){
                return false;//出现了重复值
            }
        }

        //检查同一列
        for(int j = 0;j<board.size();j++){
            if(board[j][col] == k){
                return false;
            }
        }

        //定位到对应规则化的九宫格内,判断有没有出现元素
        int realrow = (row/3)*3;
        int realcol = (col/3)*3;

        for(int i = realrow;i<realrow+3;i++){
            for(int j = realcol;j<realcol+3;j++){
                if(board[i][j] == k){
                    return false;
                }
            }
        }

        return true;
    }
public:
    void solveSudoku(vector<vector<char>>& board) {
        GetResult(board);
    }
};

 题解:

这道题也是使用的回溯算法解决,和一般的组合问题不同的是,本题不用单独列出终止条件,而是直接在函数中return真假即可,这也是和之前组合问题回溯形式不同的地方。整体上,遍历每一处节点,并且遍历当前节点是否可以存储1-9间的任意值,可以存储则继续向下递归回溯,直到最终得到一套解决方案,则会回溯上来真假的返回值,为真则该节点形成的解决方案完全成立,返回真即可(接下来不用操作),反之如果返回假,则当前节点形成的解决方案有问题,则回溯上来,继续按顺序赋值并且遍历,如果遍历到最后都没有成立,则返回假(这个逻辑其实主要是为了递归回溯方便,因为最后读取是直接读board的),在这个过程中,board值被直接改变。

在判断节点是否成立的时候,行和列的逻辑相似,注意临界点行列的不同即可。重要的是需要定位到对应规则化的九宫格内,判断有没有出现元素,这里需要转换坐标为九宫格启动位置。

最终在主函数中调用定义好的递归回溯函数,然后需要的board就会随之被赋值好了。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

花火の云

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

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

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

打赏作者

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

抵扣说明:

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

余额充值