代码随想录刷题题Day20

刷题的第二十天,希望自己能够不断坚持下去,迎来蜕变。
刷题语言:C++
Day20 任务
● 理论基础
● 77. 组合

1 回溯算法理论基础

代码随想录刷题题Day20_第1张图片

1.1 回溯法

回溯法是一种搜索的方式,是递归的副产品(只要有递归,就会有回溯)
回溯函数就是递归函数

1.2 回溯法的效率

回溯的本质是穷举,穷举所有可能,选出想要的答案。想让回溯法高效一些,可以加一些剪枝的操作但也改不了回溯法就是穷举的本质

1.3 回溯法能解决的问题

(1)组合:N个数里面按一定规则找出k个数的集合
(2)切割:一个字符串按一定规则有几种切割方式
(3)子集:一个N个数的集合里有多少符合条件的子集
(4)排列:N个数按一定规则全排列,有几种排列方式
(5)棋盘:N皇后,解数独
组合是不强调元素顺序的,排列是强调元素顺序。

理解回溯法
回溯法解决的问题都可以抽象为树形结构,因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度,因为递归有终止条件,所以必然是一棵高度有限的N叉树
代码随想录刷题题Day20_第2张图片

1.4 回溯法模板
void backtracking(参数) {
	if (终止条件) {
		存放结果;
		return;
	}
	for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
		处理节点;
		backtracking(路径,选择列表); // 递归
		回溯,撤销处理结果;
	}
}

2 组合

77. 组合
代码随想录刷题题Day20_第3张图片
思路:
回溯法解决这种k层for循环嵌套的问题
把回溯法的搜索过程抽象为树形结构
代码随想录刷题题Day20_第4张图片

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
图中可以发现n相当于树的宽度,k相当于树的深度

回溯法
(1)递归函数的返回值以及参数
定义两个全局变量:存放符合条件的单一结果、存放符合条件结果的集合

vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path;           // 用来存放符合条件结果

返回值:void
参数:n,k,startIndex

startIndex 就是防止出现重复的组合

代码随想录刷题题Day20_第5张图片
在集合[1,2,3,4]取1之后,下一层递归,在[2,3,4]中取数了,靠的就是startIndex:记录下一层递归,搜索的起始位置

void backtracking(int n, int k, int startIndex)

(2)回溯函数终止条件
到叶子节点,就是path数组大小为k,说明找到了一个子集大小为k的组合。此时用result二维数组,把path保存起来,并终止本层递归

if (path.size() == k) {
	result.push_back(path);
	return;
}

(3)单层搜索的过程
回溯法的搜索过程就是一个树型结构的遍历过程
代码随想录刷题题Day20_第6张图片

for (int i = startIndex; i <= n; i++) {// 树的横向遍历
	path.push_back(i);// 处理节点
	backtracking(n, k, i + 1);// 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
	path.pop_back();// 回溯,撤销处理的节点
}

C++:

class Solution {
public:
    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();// 回溯,撤销处理的节点
        }
    }
    vector<vector<int>> combine(int n, int k) {
    	result.clear();
    	path.clear();
        backtracking(n, k, 1);
        return result;
    }
};

时间复杂度: O ( n ∗ 2 n ) O(n * 2^n) O(n2n)
空间复杂度: O ( n ) O(n) O(n)
组合问题是回溯法解决的经典问题

3 剪枝优化

n = 4,k = 4,第一层for循环的时候,从元素2开始的遍历都没有意义了。
代码随想录刷题题Day20_第7张图片
图中每一个节点代表本层的一个for循环,每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历
以剪枝的地方就在递归中每一层的for循环所选择的起始位置
优化:
(1)已经选择的元素个数:path.size()
(2)还需要的元素个数:k - path.size()
(3)在集合n中至多从该起始位置:n - (k -path.size()) + 1,开始遍历
有个+1是因为包括起始位置,我们要是一个左闭的集合

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)// i为本次搜索的起始位置

C++:
优化后的代码

class Solution {
public:
    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();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        result.clear();
        path.clear();
        backtracking(n, k, 1);
        return result;
    }
};

鼓励坚持二十一天的自己

你可能感兴趣的:(代码随想录刷题,算法,c++,数据结构,代码随想录,回溯)