leetcode-master/problems/0077.组合优化.md

419 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://programmercarl.com/other/xunlianying.html" target="_blank">
<img src="../pics/训练营.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
# 77.组合优化
## 算法公开课
**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)[组合问题的剪枝操作](https://www.bilibili.com/video/BV1wi4y157er),相信结合视频在看本篇题解,更有助于大家对本题的理解。**
## 思路
在[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)中我们通过回溯搜索法解决了n个数中求k个数的组合问题。
文中的回溯法是可以剪枝优化的本篇我们继续来看一下题目77. 组合。
链接https://leetcode.cn/problems/combinations/
**看本篇之前,需要先看[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)**
大家先回忆一下[77. 组合]给出的回溯法的代码:
```CPP
class Solution {
private:
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n; i++) {
path.push_back(i); // 处理节点
backtracking(n, k, i + 1); // 递归
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
result.clear(); // 可以不写
path.clear(); // 可以不写
backtracking(n, k, 1);
return result;
}
};
```
## 剪枝优化
我们说过,回溯法虽然是暴力搜索,但也有时候可以有点剪枝优化一下的。
在遍历的过程中有如下代码:
```CPP
for (int i = startIndex; i <= n; i++) {
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
```
这个遍历的范围是可以剪枝优化的,怎么优化呢?
来举一个例子n = 4k = 4的话那么第一层for循环的时候从元素2开始的遍历都没有意义了。 在第二层for循环从元素3开始的遍历都没有意义了。
这么说有点抽象,如图所示:
![77.组合4](https://code-thinking-1253855093.file.myqcloud.com/pics/20210130194335207.png)
图中每一个节点图中为矩形就代表本层的一个for循环那么每一层的for循环从第二个数开始遍历的话都没有意义都是无效遍历。
**所以可以剪枝的地方就在递归中每一层的for循环所选择的起始位置**
**如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了**
注意代码中i就是for循环里选择的起始位置。
```CPP
for (int i = startIndex; i <= n; i++) {
```
接下来看一下优化过程如下:
1. 已经选择的元素个数path.size();
2. 所需需要的元素个数为: k - path.size();
3. 列表中剩余元素n-i >= 所需需要的元素个数k - path.size()
4. 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1开始遍历
为什么有个+1呢因为包括起始位置我们要是一个左闭的集合。
举个例子n = 4k = 3 目前已经选取的元素为0path.size为0n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的可以是组合[2, 3, 4]。
这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。
所以优化之后的for循环是
```CPP
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
```
优化后整体代码如下:
```CPP
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方
path.push_back(i); // 处理节点
backtracking(n, k, i + 1);
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
```
* 时间复杂度: O(n * 2^n)
* 空间复杂度: O(n)
## 总结
本篇我们针对求组合问题的回溯法代码做了剪枝优化,这个优化如果不画图的话,其实不好理解,也不好讲清楚。
所以我依然是把整个回溯过程抽象为一棵树形结构,然后可以直观的看出,剪枝究竟是剪的哪里。
**就酱学到了就帮Carl转发一下吧让更多的同学知道这里**
## 其他语言版本
### Java
```java
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
combineHelper(n, k, 1);
return result;
}
/**
* 每次从集合中选取元素可选择的范围随着选择的进行而收缩调整可选择的范围就是要靠startIndex
* @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。
*/
private void combineHelper(int n, int k, int startIndex){
//终止条件
if (path.size() == k){
result.add(new ArrayList<>(path));
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){
path.add(i);
combineHelper(n, k, i + 1);
path.removeLast();
}
}
}
```
### Python
```python
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
result = [] # 存放结果集
self.backtracking(n, k, 1, [], result)
return result
def backtracking(self, n, k, startIndex, path, result):
if len(path) == k:
result.append(path[:])
return
for i in range(startIndex, n - (k - len(path)) + 2): # 优化的地方
path.append(i) # 处理节点
self.backtracking(n, k, i + 1, path, result)
path.pop() # 回溯,撤销处理的节点
```
### Go
```Go
var (
path []int
res [][]int
)
func combine(n int, k int) [][]int {
path, res = make([]int, 0, k), make([][]int, 0)
dfs(n, k, 1)
return res
}
func dfs(n int, k int, start int) {
if len(path) == k {
tmp := make([]int, k)
copy(tmp, path)
res = append(res, tmp)
return
}
for i := start; i <= n - (k-len(path)) + 1; i++ {
path = append(path, i)
dfs(n, k, i+1)
path = path[:len(path)-1]
}
}
```
### JavaScript
```js
var combine = function(n, k) {
const res = [], path = [];
backtracking(n, k, 1);
return res;
function backtracking (n, k, i){
const len = path.length;
if(len === k) {
res.push(Array.from(path));
return;
}
for(let a = i; a <= n + len - k + 1; a++) {
path.push(a);
backtracking(n, k, a + 1);
path.pop();
}
}
};
```
### TypeScript
```typescript
function combine(n: number, k: number): number[][] {
let resArr: number[][] = [];
function backTracking(n: number, k: number, startIndex: number, tempArr: number[]): void {
if (tempArr.length === k) {
resArr.push(tempArr.slice());
return;
}
for (let i = startIndex; i <= n - k + 1 + tempArr.length; i++) {
tempArr.push(i);
backTracking(n, k, i + 1, tempArr);
tempArr.pop();
}
}
backTracking(n, k, 1, []);
return resArr;
};
```
### Rust
```Rust
impl Solution {
fn backtracking(result: &mut Vec<Vec<i32>>, path: &mut Vec<i32>, n: i32, k: i32, start_index: i32) {
let len= path.len() as i32;
if len == k{
result.push(path.to_vec());
return;
}
// 此处剪枝
for i in start_index..= n - (k - len) + 1 {
path.push(i);
Self::backtracking(result, path, n, k, i+1);
path.pop();
}
}
pub fn combine(n: i32, k: i32) -> Vec<Vec<i32>> {
let mut result = vec![];
let mut path = vec![];
Self::backtracking(&mut result, &mut path, n, k, 1);
result
}
}
```
### C
```c
int* path;
int pathTop;
int** ans;
int ansTop;
void backtracking(int n, int k,int startIndex) {
//当path中元素个数为k个时我们需要将path数组放入ans二维数组中
if(pathTop == k) {
//path数组为我们动态申请若直接将其地址放入二维数组path数组中的值会随着我们回溯而逐渐变化
//因此创建新的数组存储path中的值
int* temp = (int*)malloc(sizeof(int) * k);
int i;
for(i = 0; i < k; i++) {
temp[i] = path[i];
}
ans[ansTop++] = temp;
return ;
}
int j;
for(j = startIndex; j <= n- (k - pathTop) + 1;j++) {
//将当前结点放入path数组
path[pathTop++] = j;
//进行递归
backtracking(n, k, j + 1);
//进行回溯,将数组最上层结点弹出
pathTop--;
}
}
int** combine(int n, int k, int* returnSize, int** returnColumnSizes){
//path数组存储符合条件的结果
path = (int*)malloc(sizeof(int) * k);
//ans二维数组存储符合条件的结果数组的集合。数组足够大避免极端情况
ans = (int**)malloc(sizeof(int*) * 10000);
pathTop = ansTop = 0;
//回溯算法
backtracking(n, k, 1);
//最后的返回大小为ans数组大小
*returnSize = ansTop;
//returnColumnSizes数组存储ans二维数组对应下标中一维数组的长度都为k
*returnColumnSizes = (int*)malloc(sizeof(int) *(*returnSize));
int i;
for(i = 0; i < *returnSize; i++) {
(*returnColumnSizes)[i] = k;
}
//返回ans二维数组
return ans;
}
```
### Swift
```swift
func combine(_ n: Int, _ k: Int) -> [[Int]] {
var path = [Int]()
var result = [[Int]]()
func backtracking(start: Int) {
// 结束条件,并收集结果
if path.count == k {
result.append(path)
return
}
// 单层逻辑
// let end = n
// 剪枝优化
let end = n - (k - path.count) + 1
guard start <= end else { return }
for i in start ... end {
path.append(i) // 处理结点
backtracking(start: i + 1) // 递归
path.removeLast() // 回溯
}
}
backtracking(start: 1)
return result
}
```
### Scala
```scala
object Solution {
import scala.collection.mutable // 导包
def combine(n: Int, k: Int): List[List[Int]] = {
var result = mutable.ListBuffer[List[Int]]() // 存放结果集
var path = mutable.ListBuffer[Int]() //存放符合条件的结果
def backtracking(n: Int, k: Int, startIndex: Int): Unit = {
if (path.size == k) {
// 如果path的size == k就达到题目要求添加到结果集并返回
result.append(path.toList)
return
}
// 剪枝优化
for (i <- startIndex to (n - (k - path.size) + 1)) {
path.append(i) // 先把数字添加进去
backtracking(n, k, i + 1) // 进行下一步回溯
path = path.take(path.size - 1) // 回溯完再删除掉刚刚添加的数字
}
}
backtracking(n, k, 1) // 执行回溯
result.toList // 最终返回result的List形式return关键字可以省略
}
}
```
<p align="center">
<a href="https://programmercarl.com/other/kstar.html" target="_blank">
<img src="../pics/网站星球宣传海报.jpg" width="1000"/>
</a>