(二)LeetCode系列题型 | 全排列和组合问题


1. 简介

全排列和组合问题是 L e e t C o d e {\rm LeetCode} LeetCode中的经典问题,常涉及到使用回溯等方法解决问题。本文将介绍各种类型的全排列和组合问题,包括全排列Ⅰ Ⅱ、组合、组合总和Ⅰ Ⅱ Ⅲ Ⅳ。在开始接下来的内容前,可以首先参考回溯法的基本内容,链接


2. 全排列

题目来源 46.全排列

题目描述 给定一个没有重复数字的序列,返回其所有可能的全排列。

如输入数组为[1, 2, 3],则返回结果为[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

先来看怎么手动计算得到上述全排列的结果,通常做法是首先固定第一位,然后第二位的选择变为两种,固定第二位后随之第三位也固定,如果数组元素大于三个类似。显然,由于给定输入数组的元素个数位三,所以当回溯路径上的元素个数为三时满足结束条件。这里的选择列表即为我们可选择的范围,首先第一次我们有三种可选的元素,第二次有两个可选的元素,以此类推。这里的做选择即为不断填充路径,使其满足结束条件,即得到一个可行的排列结果。当得到一组可行解后,我们需要寻找下一组解,这时候我们要破坏前一组的结果,即撤回先前做的选择,这在递归后执行。最后,由于排列中不包含重复元素,我们额外使用一个数组来标识当前元素是否已经使用过。程序如下:

vector<int> path;
vector<vector<int>> res;

void backtrack(vector<int>& nums, vector<int>& used) {
	// 结束条件
	if (path.size() == nums.szie()) {
		res.push_back(path);
		return;
	}
	// 遍历
	for (int i = 0; i < nums.size(); ++i) {
		// 排除重复选择
		if (used[i]) continue;
		// 置当前元素已经访问
		used[i] = 1;
		// 做选择
		path.push_back(nums[i]);
		// 开始尝试继续做选择
		backtrack(nums, used);
		// 撤销选择
		path.pop_back();
		// 置当前元素没有访问
		used[i] = 0;
	}
}
// 主体函数
vector<vector<int>> permute(vector<int>& nums) {
	vector<int> u(nums.size(), 0);
	backtrack(nums, u);
	return res;
}

其他题解 官方题解


3. 全排列Ⅱ

题目来源 47.全排列Ⅱ

题目描述 与上一题要求不同的是,本题的序列可包含重复数字,按要求返回所有不重复的全排列。

如输入数组为[1, 1, 2],则返回结果为[[1, 1, 2], [1, 2, 1], [2, 1, 1]]

如果我们按照上一题的程序来解答此题,则返回的结果为[[1, 1, 2], [1, 2, 1], [1, 1, 2], [1, 2, 1], [2, 1, 1], [2, 1, 1]]。其中会包含很多重复的序列,造成这个的原因是输入数组中包含重复元素。为了方便对重复元素的处理,我们首先对原数组按从小到大的规则排序,这时重复元素会处于相邻位置。我们将上述输入数组改为[1, 1', 2],在第一次回溯时会得到结果[1, 1', 2];在第二次回溯时固定第二位即1',如果再选择第一个1的话就会得到重复的序列[1', 1, 2]。出现重复序列的情景是当前位置元素的值等于前一位置元素的值,并且前一位置元素没有被使用(如果前一位置元素的值被使用过,就会得到上述第一种序列,此时不会得到重复序列),我们在回溯时对这一情况进行剪枝即可。我们在上一题的程序的基础上,加上这一剪枝条件即可得到答案。程序如下:

vector<int> path;
vector<vector<int>> res;

void backtrack(vector<int>& nums, vector<int>& used) {
	// 结束条件
	if (path.size() == nums.size()) {
		res.push_back(path);
		return;
	}
	// 遍历
	for (int i = 0; i < nums.size(); ++i) {
		// 剪枝,i > 0保证nums[i - 1]有效
		if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
		// 排除重复选择
		if (used[i]) continue;
		// 置当前元素已经访问
		used[i] = 1;
		// 做选择
		path.push_back(nums[i]);
		// 开始尝试继续做选择
		backtrack(nums, used);
		// 撤销选择
		path.pop_back();
		// 置当前元素没有访问
		used[i] = 0;
	}
}
// 主体函数
vector<vector<int>> permute(vector<int>& nums) {
	vector<int> u(nums.size(), 0);
	backtrack(nums, u);
	return res;
}

其他题解 官方题解


4. 组合

题目来源 77.组合

题目描述 给定两个整数n和k,返回1…n中所有可能的k个数的组合。

如输入为n = 4, k = 2,则返回结果为[[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]

如果我们将n = 4改写成数组形式即[1, 2, 3, 4],则该题的解法和第二个问题-全排列非常类似。但本题的另外一个特殊之处在于结果不能出现[1, 2][2, 1]的组合,即我们只能选择上一次选择元素的后面元素,我们通过在回溯的时候指定回溯起始点实现,同时也不会出现重复使用元素的情况。程序如下:

vector<int> path;
vector<vector<int>> result;

void backtrack(int start, int k, int n) {
	// 结束条件
    if (path.size() == k) {
        result.push_back(path);
        return ;
    }
    // 遍历
    for (int i = start; i <= n; ++i) {
       	// 做选择
        path.push_back(i);
        // 开始尝试继续做选择
        backtrack(i + 1, k, n);
        // 撤销选择
        path.pop_back();
    }
}
// 主体函数
vector<vector<int>> combine(int n, int k) {
    backtrack(1, k, n);
    return result;
}

其他题解 官方题解


5. 组合总和

题目来源 39.组合总和

题目描述 给定一个无重复元素的数组candidates和一个目标数target,找出candidates中所有可以数字和为target的组合。并且,candidates中的数字可以无限制重复选取。同时,题目中所给出的数字全部为正整数;解集中不能包含重复的集合。

如输入数组为candidates = [2, 3, 6, 7]、目标数为target = 7,则返回结果为[[2, 2, 3], [7]]

首先,数字可以无限制重复选取,那么回溯形式肯定不能是上一题中的backtrack(i + 1, ..., ...),因为i + 1规定了下一个元素只能从当前位置的后一位置开始寻找。其次,由于解集中不能包含重复的集合,即不能存在[2, 2, 3][2, 3, 2]的组合,则我们最多只能在当前位置多次停留选择元素,而不能往回寻找。所以,最终的回溯结构应该是backtrack(i, ..., ...)。同时为了避免产生重复的解集合,首先对原输入数组排序。程序如下:

vector<int> path;
vector<vector<int>> result;

void backtrack(int start, vector<int>& candidates, int target) {
	// 结束条件
	if (target == 0) {
		result.push_back(path);
		return;
	}
	// 遍历
	for (int i = start; i < candidates.size() && target - candidates[i] >= 0; ++i) {
		// 做选择
		path.push_back(candidates[i]);
		// 开始尝试继续做选择
		backtrack(i, candidates, target - candidates[i]);
		// 撤销选择
		path.pop_back();
	}
}
// 主体函数
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
	sort(candidates.begin(), candidates.end());
	backtrack(0, candidates, target);
	return result;
}

其他题解 官方题解


6. 组合总和Ⅱ

题目来源 40.组合总和Ⅱ

题目描述 该题的要求与上一题有两点不同:candidates数组中可能包含重复元素;candidates中的每个数字仅能选择一次。并且不能包含重复的解集合,其他要求均相同。

如是如输入数组为candidates = [10, 1, 2, 7, 6, 1, 5]、目标数为target = 8,则返回结果为[[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]

该题的解法与第三个问题-全排列Ⅱ基本一致,不过后者要求的是直接输出所有组合,而本题做了每个组合的和必须等于目标数target的限制。这里我们额外定义函数,求一条路径上的元素总和。由于不能包含重复的解集合且每个数字仅能选择一次,我们还要结合第四个问题-组合的思路。最后,如果路径上的元素总和大于给定的目标数,则没有必须再往下执行,这是回溯法的一种剪枝。程序如下:

vector<int> path;
vector<vector<int>> result;

int sumOfPath(vector<int>& nums) {
	int s = 0;
	for (int num: nums) 
		s += num;
	return s;
}

void backtrack(int start, vector<int>& candidates, int target, vector<int>& used) {
	// 结束条件
	if (sumOfPath(path) >= target) {
		if (sumOfPath(path) == target)
			result.push_back(path);
		return;
	}
	// 遍历,注意循环及其循环体的形式
	for (int i= start; i < candidates.size(); ++i) {
		// 剪枝,i > 0保证nums[i - 1]有效
		if (i > start && candidats[i] == candidates[i - 1] && !used[i - 1]) continue;
		// 排除重复选择
		if (used[i]) continue;
		// 做选择
		used[i] = 1;
		path.push_back(candidates[i]);
		// 开始继续尝试做选择
		backtrack(i + 1, candidates, target, used);
		// 撤销选择
		path.pop_back();
		used[i] = 0;
	}
}
// 主体函数
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
	sort(candidates.begin(), candidates.end());
	vector<int> u(candidates.size(), 0);
	backtrack(0, candidates, target, u);
	return result;
}

其他题解 官方题解


7. 组合总和Ⅲ

题目来源 216.组合总和Ⅲ

题目描述 找出所有相加之和为n的k个数的组合。组合中只允许含有1-9的正整数,并且每种组合中不存在重复的数字。

如输入为k = 3n = 9,则返回结果为[[1, 2, 6], [1, 3, 5], [2, 3, 4]]

首先,题目给出的输入形式与第四个问题-组合相同。不同的是,前者要求返回的是具体组合,而本题要求返回的组合满足和为n的条件,再综合上一题的解答。例外,由于题目要求组合中只允许含有1-9的正整数,则需要再对其中的某些情况剪枝。程序如下:

vector<int> path;
vector<vector<int>> result;

int sumOfPath(vector<int>& nums) {
	int s = 0;
	for (int num: nums) 
		s += num;
	return s;
}

void backtrack(int start, int k, int n) {
	// 结束条件
	if (sumOfPath(path) >= n) {
		if (sumOfPath(path) == n && path.size() == k)
			result.push_back(path);
		return;
	}
    // 遍历
    for (int i = start; i <= n; ++i) {
        if (i > 9) return;
       	// 做选择
        path.push_back(i);
        // 开始尝试继续做选择
        backtrack(i + 1, k, n);
        // 撤销选择
        path.pop_back();
    }
}
// 主体函数
vector<vector<int>> combine(int n, int k) {
    backtrack(1, k, n);
    return result;
}

8. 组合总和Ⅳ

题目来源 377.组合总和Ⅳ

题目描述 给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数这里将顺序不同的序列视为不同的组合。

如输入为nums = [1, 2, 3]target = 4,则返回结果为7、即[[1, 1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 3], [2, 1, 1], [2, 2], [3, 1]]

首先,题目将顺序不同的序列视为不同的组合,则回溯时可以到当前位置的前面去寻找,这与第二个问题-全排列一致。此外,根据输入输出实例,可以在重复选取数组中的元素,则我们可以在此基础上不使用used数组来判断是否选取重复元素。程序如下:

int cnt = 0;
vector<int> path;

int sumOfPath(vector<int>& nums) {
	int s = 0;
	for (int num: nums) 
		s += num;
	return s;
}

void backtrack(vector<int>& candidates, int target) {
	// 结束条件
	if (sumOfPath(path) >= target) {
	    if (sumOfPath(path) == target)
			cnt += 1;
		return;
	}
	// 遍历
	for (int i = 0; i < candidates.size(); ++i) {
		// 做选择
		path.push_back(candidates[i]);
		// 开始尝试继续做选择
		backtrack(candidates, target);
		// 撤销选择
		path.pop_back();
	}
}
// 主体函数
int combinationSum(vector<int>& candidates, int target) {
	sort(candidates.begin(), candidates.end());
	backtrack(candidates, target);
	return cnt;
}

但是直接使用上述代码,当target较大时会出现超时。一种可以AC的方法是使用动态规划,将本题视为背包问题即可解决。本文不再讨论该方法(以后介绍背包问题的时候再仔细说),解答方法见下,链接


9. 全排列和组合问题总结

在全排列和组合问题中,最基本的是第二个问题-全排列,这时的输入数组中不包含重复元素,通过额外定义一个数组used来保证得到的集合中没有重复元素(不同排列顺序的集合视为不同集合);如果输入数组中包含重复元素,往往首先需要通过对输入数组排序以去重(第三个问题-全排列Ⅱ第六个问题-组合总和Ⅱ);如果不同排列顺序的集合视为同一集合,则在回溯时只能通过往后寻找下一个元素(第四个问题-组合第六个问题-组合总和Ⅱ第六个问题-组合总和Ⅲ);而最后一个问题组合总和Ⅳ是对前面问题的综合。


参考

  1. https://leetcode-cn.com/problemset/all/.


  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值