回溯法求解数组全排列和全组合问题(c++)

0介绍

回溯法是学习算法的一个坎,它将许多代码基础不够扎实的同学挡在通往更高水平的门外。究其原因,是因为回溯法本身与递归有着千丝万缕的关系,同时也要求学习者具备较强的抽象问题的能力。所以在学习回溯法之前,学习者必须熟练解决一些常见的递归问题,如二叉树的遍历、LCA问题等,这些问题leetcode均有题目,读者不妨先去练练手再回来。如果能熟练写出常见递归问题的代码,回溯法也就如庖丁解牛了。下面介绍两个回溯法练习的入门题,数组元素的全排列和全组合求解。

1 回溯法求解数组全排列

leetcode第46题考察了这个问题。问题很简单,高中生也懂,如下:

给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

如果手算这个问题的答案,我们是如何计算呢?
(1)选取排列的第一个数字,可选1、2、3,按序从数组中拿出1,
(2)选取排列的第二个数字,由于1已经被选了,可选2、3,先选2,
(3)选取排列的第三个数字,由于1、2被选,只能选3,得到第一个排列[1,2,3]。
注意在第二步的时候,我们可选2、3,但是我们已经选了2;所以在第三步结束时我们退回第二步,再选3,然后进入第三步,这时可选2,这样就得到了[1、3、2]。最终退回第一步重复即可得到所有的排列。
在上述的叙述中,有一个退回的步骤,这就是我们所说的回溯。它是用递归实现的,相当于使用了一个栈保存了"案发现场",等退回上一次递归时,程序继续执行。有兴趣的读者可以画一画上面所说的过程,可以发现这个选择过程其实就是一棵树。
下面看看代码,我们用visited来标记这个数是否访问过,curNums是已经选取的数字集合,当它的size和nums的size一致时就结束递归。

class Solution {
    
    
public:
    vector<vector<int>> permute(vector<int>& nums) 
    {
    
    
        vector<vector<int>> res;
        if(nums.empty())
            return res;
        vector<int> curNums;
        vector<int> visited(nums.size(),0);
        dfs(res,nums,visited,curNums);
        return res;    
    }

    void dfs(vector<vector<int>>& res,const vector<int>&nums,vector<int>& visited,vector<int>& curNums)
    {
    
    
        if(curNums.size() == nums.size())
        {
    
    
            res.push_back(curNums);
            return;
        }
        for(int i = 0; i < nums.size(); ++i)
        {
    
    
            if(!visited[i])
            {
    
    
                curNums.push_back(nums[i]);
                visited[i] = 1;
                dfs(res,nums,visited,curNums);
                visited[i] = 0;//回溯,修改为未访问
                curNums.pop_back();
            }
        }
    }
};

进阶:leetcode47题

2 回溯法求解数组全子集

这个问题题意也不难,求出一个集合的所有子集,是高中的组合问题。

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]

这个问题比求解排列简单一些,如果手算的话步骤如下:
(1)从nums中得到1,[1]是子集,加入结果
(2)从nums中得到2,[1,2]是子集,加入结果
(3)从nums中得到3,[1,2,3]是子集,加入结果
从第3步退回第2步,得到[1,3]子集加入结果、退回第1步从2开始搜索即可。注意我们每次搜索均从nums当前数字的下一个数字开始搜索,这样确保结果不重复。

class Solution {
    
    
public:
    vector<vector<int>> subsets(vector<int>& nums) 
    {
    
    
        vector<vector<int>> res;
        vector<int> curNums;
        dfs(res,nums,curNums,0);
        return res;    
    }

    void dfs(vector<vector<int>>& res,const vector<int>& nums,vector<int>& curNums,int start)
    {
    
    
        res.push_back(curNums);

        for(int i = start; i < nums.size(); ++i)
        {
    
    
            curNums.push_back(nums[i]);
            dfs(res,nums,curNums,i+1);//从下一处开始搜索
            curNums.pop_back();//回溯
        }
    }
};

进阶:leetcode90题

3 总结

回溯法属于难者不会会者不难的方法,打好递归基础,做到心中可以模拟递归过程的话可以很快入门。

猜你喜欢

转载自blog.csdn.net/MoonWisher_liang/article/details/108206813