Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
youngyangyang04 committed Sep 9, 2020
1 parent 7747476 commit 1526105
Show file tree
Hide file tree
Showing 9 changed files with 411 additions and 28 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@
* [字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw)
* [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg)
* [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)
* [字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ)
* [字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug)
* [字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg)
* [字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ)
* [字符串:KMP算法还能干这个!](https://mp.weixin.qq.com/s/lR2JPtsQSR2I_9yHbBmBuQ)

(持续更新中....)

Expand Down
Binary file added pics/347.前K个高频元素.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pics/39.组合总和.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pics/77.组合.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions problems/0028.实现strStr().md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,46 @@ public:
}
};
```

前缀表不减一版本
```
class Solution {
public:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) {
j = next[j - 1];
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
```
> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。
58 changes: 48 additions & 10 deletions problems/0039.组合总和.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,36 +30,74 @@ candidates 中的数字可以无限制重复被选取。

# 思路

题目中的**无限制重复被选取,吓得我赶紧想想 出现0 可咋办**,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了。


这道题上来可以这么想,看看一个数能不能构成target,一个for循环遍历一遍,再看看两个数能不能构成target,两个for循环遍历,在看看三个数能不能构成target,三个for循环遍历,直到candidates.size()个for循环遍历一遍。

遇到这种问题,就要想到递归的层级嵌套关系就可以解决这种多层for循环的问题,而回溯则帮我们选择每一个合适的集合!

那么使用回溯的时候,要知道求的是排列,还是组合,排列和组合是不一样的。

一些同学可能海分不清,我大概说一下:

**组合是不强调元素顺序的,排列是强调元素顺序的。**

例如 集合 1,2 和 集合 2,1 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,1,2 和 2,1 就是两个集合了。

**求组合,和求排列的回溯写法是不一样的,代码上有小小细节上的改变。**

本题选组过程如下:

<img src='../pics/39.组合总和.png' width=600> </img></div>


分析完过程,回溯算法的模板框架如下:
```
backtracking() {
if (终止条件) {
存放结果;
}
for (选择:选择列表(可以想成树中节点孩子的数量)) {
递归,处理节点;
backtracking();
回溯,撤销处理结果
}
}
```

按照模板不难写出如下代码,但很一些细节,我在注释中标记出来了。

# C++代码

```
// 无限制重复被选取。 吓得我赶紧想想 0 可咋办
class Solution {
private:
vector<vector<int>> result;
void backtracking(vector<int>& candidates, int target, vector<int>& vec, int sum, int startIndex) {
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum > target) {
return;
}
if (sum == target) {
result.push_back(vec);
result.push_back(path);
return;
}
// 因为可重复,所以我们从0开始, 这道题目感觉像是47.全排列II,其实不是
// 这里i 依然从 startIndex开始,因为求的是组合,如果求的是排列,那么i每次都从0开始
for (int i = startIndex; i < candidates.size(); i++) {
sum += candidates[i];
vec.push_back(candidates[i]);
backtracking(candidates, target, vec, sum, i); // 关键点在这里,不用i+1了
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); // 关键点在这里,不用i+1了,表示可以重复读取当前的数
sum -= candidates[i];
vec.pop_back();
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<int> vec;
backtracking(candidates, target, vec, 0, 0);
backtracking(candidates, target, 0, 0);
return result;
}
};
Expand Down
144 changes: 139 additions & 5 deletions problems/0077.组合.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,99 @@

# 思路

这是回溯法的经典题目。

直觉上当然是使用for循环,例如示例中k为2,很容易想到 用两个for循环,这样就可以输出 和示例中一样的结果。

代码如下:
```
int n = 4;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
cout << i << " " << j << endl;
}
}
```

输入:n = 100, k = 3
那么就三层for循环,代码如下:

```
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
for (int u = j + 1; u <=n; n++) {
}
}
}
```
**如果n为 100,k为50呢,那就50层for循环,是不是开始窒息。**

那么回溯法就能解决这个问题了。

回溯是用来做选择的,递归用来节点层叠嵌套,**每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。**

其实子集和组合问题都可以抽象为一个树形结构,如下:


<img src='../pics/77.组合.png' width=600> </img></div>

可以看一下这个棵树,一开始集合是 1,2,3,4, 从左向右去数,取过的数,不在重复取。

第一取1,集合变为2,3,4 ,因为k为2,我们只需要去一个数就可以了,分别取,2,3,4, 得到集合[1,2] [1,3] [1,4],以此类推。

**其实这就转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围**

如何在这个树上遍历,然后收集到我们要的结果集呢,用的就是回溯搜索法,**可以发现,每次搜索到了叶子节点,我们就找到了一个结果。**

分析完过程,我们来看一下 回溯算法的模板框架如下:
```
backtracking() {
if (终止条件) {
存放结果;
}
for (选择:选择列表(可以想成树中节点孩子的数量)) {
递归,处理节点;
backtracking();
回溯,撤销处理结果
}
}
```

分析模板:

什么是达到了终止条件,树中就可以看出,搜到了叶子节点了,就找到了一个符合题目要求的答案,就把这个答案存放起来。

看一下这个for循环,这个for循环是做什么的,for 就是处理树中节点各个孩子的情况, 一个节点有多少个孩子,这个for循环就执行多少次。

最后就要看这个递归的过程了,注意这个backtracking就是自己调用自己,实现递归。

一些同学对递归操作本来就不熟练,递归上面又加上一个for循环,可能就更迷糊了, 我来给大家捋顺一下。

这个backtracking 其实就是向树的叶子节点方向遍历, for循环可以理解是横向遍历,backtracking 就是纵向遍历,这样就把这棵树全遍历完了。

那么backtracking就是一直往深处遍历,总会遇到叶子节点,遇到了叶子节点,就要返回,那么backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。

分析完模板,本题代码如下:

# C++ 代码

```
class Solution {
private:
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> vec; // 用来存放符合条件结果
vector<int> path; // 用来存放符合条件结果
void backtracking(int n, int k, int startIndex) {
if (vec.size() == k) {
result.push_back(vec);
if (path.size() == k) {
result.push_back(path);
return;
}
// 这个for循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始
for (int i = startIndex; i <= n; i++) {
vec.push_back(i);
path.push_back(i); // 处理节点
backtracking(n, k, i + 1);
vec.pop_back();
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
Expand All @@ -43,3 +118,62 @@ public:
}
};
```

## 剪枝优化

在遍历的过程中如下代码 :

```
for (int i = startIndex; i <= n; i++)
```

这个遍历的范围是可以剪枝优化的,怎么优化呢?

来举一个例子,n = 4, k = 4的话,那么从2开始的遍历都没有意义了。

已经选择的元素个数:path.size();

要选择的元素个数 : k - path.size();

在集合n中开始选择的起始位置 : n - (k - path.size());

因为起始位置是从1开始的,而且代码里是n <= 起始位置,所以 集合n中开始选择的起始位置 : n - (k - path.size()) + 1;

所以优化之后是:

```
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)
```

整体代码如下:

```
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循环有讲究,组合的时候 要用startIndex,排列的时候就要从0开始
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;
}
};
```



# 观后感
我来写一下观后感: 很厉害,转化成从集合中选取子集的问题,可选择的范围随着选择的进行而限缩,于是做剪枝,调整可选择的范围。 每一次的递归是层叠嵌套的关系,可以用于解决多层嵌套循环的问题。 每一层递归中,尽量节省循环次数,这样在后续的递归调用中,节省下来的循环会被以至少指数等级放大。
Loading

0 comments on commit 1526105

Please sign in to comment.