【算法学习计划】回溯 -- 综合题目(上)

目录

46.全排列

78.子集

1863.找出所有子集的异或总和再求和

47.全排列Ⅱ

17.电话号码的字母组合

22.括号生成

77.组合

494.目标和

结语


这个专题共收集了17道回溯综合题,由于数量过多所以分为了上下两篇来写

下一篇的链接在这里:【算法学习计划】回溯 -- 综合题目(下)

这一篇共有 8 道,都是非常值得学习的题目

(下文中的标题都是leedcode对应题目的链接)

46.全排列

这道题目的名字其实就告诉我们这道题目应该怎么解了

由于我们需要全排列,所以我们可以一次固定一个数,但是比如 123

我们固定了 1 之后,我们再进入递归的时候,我们还会选到 1,所以我们就需要剪枝,其实就是判断这种请款能不能而已

所以我们可以在全局开一个 bool 数组 check,我们每到达一个位置,我们就将这个位置的值设置为 true,而我们在判断要不要选某个数的时候,我们直接判断一下这个位置的check数组是否为false 即可

最后,我们还需要回溯一下,也就是操作还原,比如还是 123,我们在递归完 1 的情况之后,我们就需要遍历 2 的情况了,但是此时 123 都是true,我们就无法进行下一步

再比如 1234,我们在选完 1 之后,我们可以选 234,但是假如我选了 2,那么递归完 2 的情况之后,我进入 3 的递归逻辑就应该还可以选择 2 4,所以我们在递归完之后还需要将某个位置的值重新设置为 false

最后最后,就是设置一个 vector<int>path 数组,我们每选到一个值,我们就往里面push_back一个值,最后当path的数量和nums数组的大小相等的时候,就证明我们以及将所有数都选完了,这其实就是我们的递归出口

代码如下:

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    vector<int> check;
    vector<int> nums;
    void dfs()
    {
        if(path.size() == nums.size())
        {
            ret.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i++)
        {
            if(check[i] == 1) continue;
            check[i] = 1;
            path.push_back(nums[i]);
            dfs();
            check[i] = 0;
            path.pop_back();
        }
    }
    vector<vector<int>> permute(vector<int>& nums1) 
    {
        for(auto e : nums1) nums.push_back(e);
        check.resize(nums.size());
        dfs();
        return ret;
    }
};

78.子集

这道题就是求子集,而且有种情况需要我们注意,也就是,[1、2] 和 [2、1] 在这道题目里面算是一种情况

所以我们就需要大量的剪枝操作了

比如这张图(该图版权归比特就业课所有),我们可以看到,当我们选择 1 的时候,里面的 1、2 刚好和 选完 2 之后的 2、1 抵消了,也就是 2、1 这种情况我们直接就不需要了

而我们就可以传一个 pos 在递归里面,pos代表我们遍历到几

比如此时我们遍历到了 1,那么pos就是 1 的下标,那么后续的遍历就是从 1 位置的下一个位置开始,因为 1 位置以及遍历过了所有包含 1 的情况,如果不是 1 位置是 2 位置的话,那么就是前面所有的情况我们都遍历过了,所以我们的循环不需要从前面的位置开始

当然,这里就不需要check数组了,因为我们是直接循环的,所以在某个循环里面,一个数只会被选到一次

同时我们也会发现,在我们的决策树里面,每一个节点都是我们的答案,所以我们就需要每遍历到一个位置,就放入到一个全局数组ret里面,最后返回ret,当然,我们还需要一个path数组,这个数组的作用是帮助我们收集沿途的数

代码如下:

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    vector<int> n;

    void dfs(int pos)
    {
        for(int i = pos; i < n.size(); i++)
        {
            path.push_back(n[i]);
            ret.push_back(path);
            dfs(i+1);
            path.pop_back();
        }
    }

    vector<vector<int>> subsets(vector<int>& nums) 
    {
        for(auto e : nums) n.push_back(e);
        ret.push_back({});
        dfs(0);
        return ret;
    }
};

1863.找出所有子集的异或总和再求和

首先这题最重要的就是求子集,然后一个一个异或操作

对于求子集,我们在上一题详细讲过了,这里简单讲一下,我们将一个pos传入递归逻辑中,pos代表我们从哪个位置开始循环选数字,而我们的循环就从 pos 位置开始,每一次就传 i + 1 进入递归中,因为 pos 位置之前的情况后面再枚举就会重复

最后我们可以创建两个变量mid、sum,mid负责看到一个数就异或一下,sum负责将结果全部加起来最后返回

代码如下:

class Solution {
public:
    int mid, sum;
    vector<int> n;
    void dfs(int pos)
    {
        for(int i = pos; i < n.size(); i++)
        {
            mid ^= n[i];
            sum += mid;
            dfs(i+1);
            mid ^= n[i];
        }
    }
    int subsetXORSum(vector<int>& nums) 
    {
        for(auto e : nums) n.push_back(e);
        mid = 0, sum = 0;
        dfs(0);
        return sum;
    }
};

47.全排列Ⅱ

这道题目还是全排列,但是不一样的是,这里面有重复元素

所以第一步,我们呢需要sort一下,然后就是剪枝,也就是有重复元素的情况,我们就不选

比如 1112,那么我们第一次选的时候,只能选出 12进行下一次的递归

但是这里面有一个点就是,我们第一次选完了第一个 1 之后,在这个 1 的逻辑里面,我们需要将这个位置用check数组标记为 true,然后在过完这个 1 的逻辑之后,我们再将其设置为 false

那么在第二个 1 开始递归逻辑的时候,第一个 1 在check位置的值就是false,也就是没有标记,所以我们就可以这么判断,也就是当当前数字和前面一个数字的值相同并且前面一个数字check标记为false的时候,直接跳过,这样,我们就解决了重复的情况

到这里其实还有一个小问题,也就是我们是当前数字和前一个比较的

但如果当前是第一个数字的话,前面是空,所以我们可以在最前面放多一个数或者特判一下即可

博主这里比较懒,就在全局新创了一个数组,在最前面加了一个数,这样dfs函数也就是不用传这个参数了

代码如下:

class Solution {
public:
    vector<int> check;
    vector<vector<int>> ret;
    vector<int> path;
    vector<int> n;
    void dfs()
    {
        if((path.size() + 1) == n.size())
        {
            ret.push_back(path);
            return;
        }
        for(int i = 1; i < n.size(); i++)
        {
            if(check[i] == 1)continue;
            if(n[i] == n[i-1] && check[i-1] == 0) continue;
            check[i] = 1;
            path.push_back(n[i]);
            dfs();
            path.pop_back();
            check[i] = 0;
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) 
    {
        sort(nums.begin(), nums.end());
        check.resize(nums.size()+1);
        n.push_back(INT_MAX);
        for(auto e : nums) n.push_back(e);
        dfs();
        return ret;
    }
};

17.电话号码的字母组合

这里其实就是一个一个数地看,然后直接全部列举出来就是了

首先做这道题目,我们可以先将所有的数字和字母的映射放进哈希表里面

string tmp[10] = {"",    "",    "abc",  "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

接着,我们可以将那一段数字变成全局的(定义一个全局变量,赋值给那个变量即可)

放一个pos参数进递归的逻辑,我们每一次就选择数字数组中pos位置的值进行递归

如果是数组循环的话,那么假如是13,那么我们选完 1 的情况之后,我们必然是需要将check位置的 1 给复原的,那么我们在后面就会选到 31 的情况,所以这里不能用循环,我们直接用pos,一次选一个数即可

最后的暴搜就没什么好说的,循环列举该数字对应的字母情况

代码如下:

class Solution {
public:
    unordered_map<int, string> hash;
    vector<string> ret;
    string path;
    vector<int> check;
    void dfs(string d, int k) {
        if (path.size() == d.size()) {
            ret.push_back(path);
            return;
        }
        int n = d[k] - '0';
        string r = hash[n];
        for (int j = 0; j < r.size(); j++) {
            path.push_back(r[j]);
            dfs(d, k + 1);
            path.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        if (digits.size() == 0)
            return ret;
        string tmp[10] = {"",    "",    "abc",  "def", "ghi",
                          "jkl", "mno", "pqrs", "tuv", "wxyz"};
        for (int i = 2; i < 10; i++)
            hash[i] = tmp[i];
        check.resize(digits.size());
        dfs(digits, 0);
        return ret;
    }
};

22.括号生成

这道题我们需要分情况讨论

首先是左括号的情况,当我们的左括号数量小于 n 的时候,我们都可以选择左括号的情况

接着是右括号,我们只有在右括号的数量严格小于左括号的时候我们才可以选择右括号进行递归,因为如果相等或者大于的话,那么我们就没有左括号对应,那么就是无效的组合

接着其实主逻辑还是一样的,选一个括号,递归,下一个括号选择,再递归,仅此而已

最后就是递归出口,在这里我们需要创建一个全局的path数组,依次将括号放入数组中,当我们path数组的大小等于二倍的 n 的时候,我们再将这个path数组插入到一个全局的返回数组ret中,最后在主函数返回 ret 即可

代码如下:

class Solution {
public:
    vector<string> ret;
    string path;
    vector<int> check;// 左右括号的数量
    void dfs(int n)
    {
        if(path.size() == 2 * n)
        {
            ret.push_back(path);
            return;
        } 
        if(check[0] < n)
        {
            //左括号
            check[0]++;
            path += "(";
            dfs(n);
            path.pop_back();
            check[0]--;
        }
        if(check[1] < check[0])
        {
            //右括号
            check[1]++;
            path += ")";
            dfs(n);
            path.pop_back();
            check[1]--;
        }

    }
    vector<string> generateParenthesis(int n) 
    {
        check.resize(2);
        dfs(n);
        return ret;
    }
};

77.组合

这题没什么好说的,算是第二道题目(子集)的简单版

将pos作为参数传进去,一次固定一个数往下递归,然后每次进行选数的时候都需要循环,但是都是从pos位置开始循环,因为前面的情况我们在前面已经考虑完了

代码如下:
 

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;
    
    void dfs(int n, int k, int pos)
    {
        if(path.size() == k)
        {
            ret.push_back(path);
            return;
        }

        for(int i = pos; i <= n ; i++)
        {
            path.push_back(i);
            dfs(n, k, i+1);
            path.pop_back();
        }

    }
    vector<vector<int>> combine(int n, int k) 
    {
        dfs(n, k, 1);
        return ret;
    }
};

494.目标和

这道题其实没什么好说的,就是暴搜,固定一个数,这个数加和减两种情况各自进行一次递归,不断传递下去

另外,我们的递归深度就是数组的长度,当我们的深度到了的时候,也就是最后一次的递归逻辑里面,我们就可以设置递归出口了,当最后一次加或者减最后一个元素,如果等于我们的target,那么我们就让全局的ret++即可

最后我们主函数的返回值就是ret

代码如下:

class Solution {
public:
    int ret;
    void dfs(vector<int>& nums, int target, int sum, int time)
    {
        if(!time)
        {
            
            if(sum + nums[0] == target) ret++;
            if(sum - nums[0] == target) ret++;
            return;
        }
        dfs(nums, target, sum + nums[time], time - 1);
        dfs(nums, target, sum - nums[time], time - 1);
    }
    int findTargetSumWays(vector<int>& nums, int target) 
    {
        ret = 0;
        dfs(nums, target, 0, nums.size()-1);    
        return ret;
    }
};

结语

本篇博客到这里就结束啦~( ̄▽ ̄)~*

如果对你有帮助的话,希望可以关注一下喔

 下一篇的链接在这里:【算法学习计划】回溯 -- 综合题目(下)