以下内容整理自leetcode题解
排列
无重复元素
问题描述
给定一个没有重复数字的序列,返回其所有可能的全排列。
比如:
对于序列[2,3,1]。
需要返回[ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]。
即
P
3
3
P_{3}^{3}
P33的所有可能情况。
思路
对于一个没有重复数字序列的全排列,我们应该是首先选择第一个数字,然后从剩余的数字中选择出第二个数字,直到所有的数字都被选择出来。这样我们就选择除了一个可行的排列,遍历每种选择可能即可得到所有的排列。
那么转化为递归的思路就是:
我们先选择出第一个数字,然后找到将剩余数字序列的全排列找出。即遍历第一个数字的所有可能,分别找到对应剩余数字序列的全排列,然后进行拼接。
代码
res
是返回的所有的全排列,path
是目前的排列,vis
是存储对应数字是否已使用,nums
是数字序列。
void backtrack(vector< vector<int> >& res,vector<int>& path,vector<bool>& vis,vector<int> &nums,int start) {
if (start == vis.size()) {
res.push_back(path);
return;
}
for (int i = 0; i < (int)nums.size(); ++i) {
if (vis[i]) {
continue;
}
path.emplace_back(nums[i]);
vis[i] = true;
backtrack(res, path, vis, nums,start+1);
vis[i] = false;
path.pop_back();
}
}
有重复元素
问题描述
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
比如:
对于序列[2,2,1]。
需要返回[ [1,2,2], [2,1,2], [2,2,1] ]
思路
这里对于重复数字序列的全排列应该是高中的知识了。如果要计算排列的数量应该是
P
3
3
/
P
2
2
P_{3}^{3}/P_{2}^{2}
P33/P22。很明显
P
3
3
P_{3}^{3}
P33是不包含重复数字的全排列。
这里我们假设2分为2a和2b,那么所有的全排列就是
P
3
3
P_{3}^{3}
P33。但是[1,2a,2b]和[1,2b,2a]应该被认为是重复的,只能有一个,也就是和’2’的种类没有关系。而对于2个’2’,其分配的种类可能其实也是一个全排列
P
2
2
P_{2}^{2}
P22。
这样我们可以要求在全排列中2a一定在2b前面,从而形成一个全排列去重的方式,来从中剔除掉重复的全排列。
所以如果我们在选择的时候对2进行标号,只有当前面的2都已经被使用过,我们才可以使用当前2,否则不使用。这样我们就可是通过简单的判断进行去重了。
代码
这里改动的地方就是在If
语句添加了一个新的判断条件。
i>0
是为了防止后面的数组越界,因为重复数字数量一定大于2.
nums[i]==nums[i-1]
表示这个数字与前一个相同,即不是第一次出现。
!vis[i-1]
表示如果前一个重复数字没有使用,那么这个数字也不能使用,直接跳过。
void backtrack(vector< vector<int> >& res,vector<int>& path,vector<bool>& vis,vector<int> &nums,int start) {
if (start == vis.size()) {
res.push_back(path);
return;
}
for (int i = 0; i < (int)nums.size(); ++i) {
if (vis[i]||(i>0&&nums[i]==nums[i-1]&&!vis[i-1])) {
continue;
}
path.emplace_back(nums[i]);
vis[i] = true;
backtrack(res, path, vis, nums,start+1);
vis[i] = false;
path.pop_back();
}
}
组合
无重复元素
问题描述
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
输入:
n = 4, k = 2
输出:
[ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4] ]
思路
组合问题可以看成二进制枚举的过程,假定我们用二进制序列来表示在Nums上的组合情况,比如对于n=4,k=2,二进制序列1001表示[1,4],即1表示在组合中,0表示不在组合中。那么我们通过如下的两个规则可以遍历所有的组合情况。初始化二进制序列为0011(4321)。即前k个数字为1,其余为0。
- 如果二进制序列以0结束,那么我们可推出从尾部开始一定有t个连续的0,然后是m个连续的1。那么我们只需将倒数第t+m+1的0和t+m的1进行调换,然后将剩余的1移到结尾处。例如011 0011 1000->011 0100 0011。
- 如果二进制序列以1结束,那么我们尾部一定有t个连续的1,那我们只需将t和t+1的0进行调换即可。
按照这种方式,我们就可以遍历所有的组合可能。
当然,这种不使用递归的方式可能编码会复杂一点。在看一下递归的实现。
这个问题假设我们将组合从小到小进行排列,那么假设第一个数字为m。则这个问题就被分成了m+在[m+1,n]中选择k-1个数字的组合情况,问题的规模就被降低了。
因此我们可以在每个递归步骤内遍历所有的可选的’m’,并递归找到[m+1,n]中选择k-1个数字的组合。
代码
其中res
是返回的所有组合情况,path
是目前搜索到的位置,也是组合内的元素,start,n,k表示从[start,n]中选择出k个数字。
void comb(vector<vector<int>>& res,vector<int>& path,int start,int n,int k){
if(path.size()==k){
res.emplace_back(path);
return;
}
for(int i=start;i<=n;i++){
path.emplace_back(i);
comb(res,path,i+1,n,k);
path.pop_back();
}
}
有重复元素
问题描述
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
输入:
candidates = [10,1,2,7,6,1,5], target = 8
输出:
[ [1, 7], [1, 2, 5], [2, 6], [1, 1, 6] ]
思路
这里就只介绍递归的实现了,因为编码比较简单。
和全排列的去重方式类似,我们也是对数组进行排序,然后对于重复数字,如果前面有没有使用的重复数字,那我们就不会选择它。比如说对于[1,2a,2b,2c],target=5。我们第一次会找到[1,2a,2b],但是我们不会返回[1,2b,2c],因为前面的2a没有被选择,所以后面的也不会在组合内。即重复数字按顺序优先选择。按照这种遍历方式选择和为target的组合即可。
tip:当组合和大于target时则需要剪枝,因为所有数字都是正数,此时已不可能通过添加数字使得结果等于target。
代码
start
,target
表示在[start,~]中寻找组合中数字和为target的所有组合。
void comb(vector<vector<int>>& res,vector<int>& path,vector<bool>& vis,vector<int>& nums,int start,int target){
if(!target){
res.emplace_back(path);
return;
}
for(int i=start;i<(int)nums.size();i++){
if(i>0&&nums[i-1]==nums[i]&&!vis[i-1]){
continue;
}
if(target>=nums[i]){
path.emplace_back(nums[i]);
vis[i]=true;
comb(res,path,vis,nums,i+1,target-nums[i]);
path.pop_back();
vis[i]=false;
}
}
}