Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 94d21be

Browse files
Update
1 parent d1cf776 commit 94d21be

13 files changed

+233
-54
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@
144144
* [回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)
145145
* [本周小结!(回溯算法系列二)](https://mp.weixin.qq.com/s/uzDpjrrMCO8DOf-Tl5oBGw)
146146
* [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)
147+
* [回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ)
148+
* [回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw)
149+
* [回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA)
147150

148151
(持续更新中....)
149152

pics/46.全排列.png

3.06 KB
Loading

pics/47.全排列II1.png

85.4 KB
Loading

pics/47.全排列II2.png

21.6 KB
Loading

pics/47.全排列II3.png

162 KB
Loading

pics/491. 递增子序列1.png

10.3 KB
Loading

pics/491. 递增子序列4.png

88.3 KB
Loading

problems/0047.全排列II.md

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,90 @@
1-
## 题目地址
2-
https://leetcode-cn.com/problems/permutations-ii/
31

4-
## 思路
5-
6-
这道题目和46.全排列的区别在与**给定一个可包含重复数字的序列**,要返回**所有不重复的全排列**
2+
> 排列也要去重了
3+
> 通知:很多录友都反馈之前看「算法汇总」的目录要一直往下拉,很麻烦,这次Carl将所有历史文章汇总到一篇文章中,有一个整体的目录,方便录友们从前面系列开始卡了,依然在公众号左下角[「算法汇总」](https://mp.weixin.qq.com/s/weyitJcVHBgFtSc19cbPdw),这里会持续更新,大家快去瞅瞅哈
74
8-
这里就涉及到去重了。
5+
# 47.全排列 II
96

7+
题目链接:https://leetcode-cn.com/problems/permutations-ii/
108

11-
要注意**全排列是要取树的子节点的,如果是子集问题,就取树上的所有节点。**
9+
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
1210

13-
很多同学在去重上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。
11+
示例 1:
12+
输入:nums = [1,1,2]
13+
输出:
14+
[[1,1,2],
15+
[1,2,1],
16+
[2,1,1]]
1417

15-
这个去重为什么很难理解呢,**所谓去重,其实就是使用过的元素不能重复选取。** 这么一说好像很简单!
18+
示例 2:
19+
输入:nums = [1,2,3]
20+
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
1621

22+
提示:
23+
* 1 <= nums.length <= 8
24+
* -10 <= nums[i] <= 10
1725

18-
但是什么又是“使用过”,我们把排列问题抽象为树形结构之后,**“使用过”在这个树形结构上是有两个维度的**,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。
26+
## 思路
1927

28+
这道题目和[回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw)的区别在与**给定一个可包含重复数字的序列**,要返回**所有不重复的全排列**
2029

21-
**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。**
30+
这里又涉及到去重了。
2231

23-
那么排列问题,既可以在 同一树层上的“使用过”来去重,也可以在同一树枝上的“使用过”来去重!
32+
[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ)[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)我们分别详细讲解了组合问题和子集问题如何去重。
2433

25-
理解这一本质,很多疑点就迎刃而解了
34+
那么排列问题其实也是一样的套路
2635

27-
**还要强调的是去重一定要对元素经行排序,这样我们才方便通过相邻的节点来判断是否重复使用了**
36+
**还要强调的是去重一定要对元素经行排序,这样我们才方便通过相邻的节点来判断是否重复使用了**
2837

29-
首先把示例中的 [1,1,2] (为了方便举例,已经排序)抽象为一棵树,然后在同一树层上对nums[i-1]使用过的话,进行去重如图
38+
我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图
3039

3140
<img src='../pics/47.全排列II1.png' width=600> </img></div>
3241

3342
图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。
3443

35-
代码如下:
44+
**一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果**
45+
46+
[回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw)中已经详解讲解了排列问题的写法,在[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ)[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中详细讲解的去重的写法,所以这次我就不用回溯三部曲分析了,直接给出代码,如下:
3647

3748
## C++代码
3849

3950
```
4051
class Solution {
4152
private:
4253
vector<vector<int>> result;
43-
void backtracking (vector<int>& nums, vector<int>& vec, vector<bool>& used) {
54+
vector<int> path;
55+
void backtracking (vector<int>& nums, vector<bool>& used) {
4456
// 此时说明找到了一组
45-
if (vec.size() == nums.size()) {
46-
result.push_back(vec);
57+
if (path.size() == nums.size()) {
58+
result.push_back(path);
4759
return;
4860
}
49-
5061
for (int i = 0; i < nums.size(); i++) {
51-
// 这里理解used[i - 1]非常重要
52-
// used[i - 1] == true,说明同一树支nums[i - 1]使用过
62+
// used[i - 1] == true,说明同一树支nums[i - 1]使用过
5363
// used[i - 1] == false,说明同一树层nums[i - 1]使用过
5464
// 如果同一树层nums[i - 1]使用过则直接跳过
55-
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
65+
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
5666
continue;
5767
}
5868
if (used[i] == false) {
5969
used[i] = true;
60-
vec.push_back(nums[i]);
61-
backtracking(nums, vec, used);
62-
vec.pop_back();
70+
path.push_back(nums[i]);
71+
backtracking(nums, used);
72+
path.pop_back();
6373
used[i] = false;
6474
}
6575
}
6676
}
67-
6877
public:
6978
vector<vector<int>> permuteUnique(vector<int>& nums) {
70-
sort(nums.begin(), nums.end());
79+
result.clear();
80+
path.clear();
81+
sort(nums.begin(), nums.end()); // 排序
7182
vector<bool> used(nums.size(), false);
72-
vector<int> vec;
7383
backtracking(nums, vec, used);
7484
return result;
75-
7685
}
7786
};
87+
7888
```
7989

8090
## 拓展
@@ -87,14 +97,14 @@ if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
8797
}
8898
```
8999

90-
可是如果把 `used[i - 1] == true` 也是正确的,去重代码如下:
100+
**如果改成 `used[i - 1] == true` 也是正确的!**,去重代码如下:
91101
```
92102
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
93103
continue;
94104
}
95105
```
96106

97-
这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用`used[i - 1] == false`如果要对树枝前一位去重用用`used[i - 1] == true`
107+
这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用`used[i - 1] == false`如果要对树枝前一位去重用`used[i - 1] == true`
98108

99109
**对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!**
100110

@@ -110,5 +120,28 @@ if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
110120

111121
<img src='../pics/47.全排列II3.png' width=600> </img></div>
112122

113-
大家应该很清晰的看到,树层上去重非常彻底,效率很高,树枝上去重虽然最后可能得到答案,但是多做了很多无用搜索。
123+
大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。
124+
125+
# 总结
126+
127+
这道题其实还是用了我们之前讲过的去重思路,但有意思的是,去重的代码中,这么写:
128+
```
129+
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
130+
continue;
131+
}
132+
```
133+
和这么写:
134+
```
135+
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
136+
continue;
137+
}
138+
```
139+
140+
都是可以的,这也是很多同学做这道题目困惑的地方,知道`used[i - 1] == false`也行而`used[i - 1] == true`也行,但是就想不明白为啥。
141+
142+
所以我通过举[1,1,1]的例子,把这两个去重的逻辑分别抽象成树形结构,大家可以一目了然:为什么两种写法都可以以及哪一种效率更高!
143+
144+
是不是豁然开朗了!!
145+
146+
就酱,很多录友表示和「代码随想录」相见恨晚,那么大家帮忙多多宣传,让更多的同学知道这里,感谢啦!
114147

problems/0491.递增子序列.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图:
3939

40-
<img src='../pics/491. 递增子序列1.jpg' width=600> </img></div>
40+
<img src='../pics/491. 递增子序列1.png' width=600> </img></div>
4141

4242

4343
## 回溯三部曲
@@ -69,11 +69,15 @@ if (path.size() > 1) {
6969

7070
* 单层搜索逻辑
7171

72-
<img src='../pics/491. 递增子序列1.jpg' width=600> </img></div>
72+
<img src='../pics/491. 递增子序列1.png' width=600> </img></div>
73+
在图中可以看出,**同一父节点下的同层上使用过的元素就不能在使用了**,注意这里要求的是**同一父节点下的同层**,这里和[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中去重的有本质区别。
7374

74-
在图中可以看出,同层上使用过的元素就不能在使用了,**注意这里和[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中去重的区别**
75+
[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)是要整棵树的同一层进行去重,所以进行排序!
7576

76-
**本题只要同层重复使用元素,递增子序列就会重复**,而[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中是排序之后看相邻元素是否重复使用。
77+
如图:
78+
![491. 递增子序列4](https://img-blog.csdnimg.cn/20201112162206843.png)
79+
80+
**本题只要同一父节点下的同层上重复使用元素,递增子序列就会重复**,而[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中是排序之后看相邻元素是否重复使用。
7781

7882

7983
还有一种情况就是如果选取的元素小于子序列最后一个元素,那么就不能是递增的,所以也要pass掉。

problems/0514.自由之路.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
//dp[i][j],key的0~i位字符拼写后,ring的第j位对齐12:00方向,需要的最小步数
3+
//前提:key[i] = ring[j],若不满足,dp[i][j] = INT_MAX
4+
5+
这道题目我服! 没做出来
6+
7+
https://blog.csdn.net/qq_41855420/article/details/89058979
8+
9+
```
10+
class Solution {
11+
public:
12+
int findRotateSteps(string ring, string key) {
13+
//int dp[101][101] = {0};
14+
int n = ring.size();
15+
vector<vector<int>> dp(key.size() + 1, vector<int>(ring.size(), 0));
16+
for (int i = key.size() - 1; i >= 0; i--) {
17+
for (int j = 0; j < ring.size(); j++) {
18+
dp[i][j] = INT_MAX;
19+
for (int k = 0; k < ring.size(); k++) {
20+
if (ring[k] == key[i]) {
21+
int diff = abs(j - k);
22+
int step = min(diff, n - diff);
23+
dp[i][j] = min(dp[i][j], step + dp[i + 1][k]);
24+
}
25+
}
26+
}
27+
}
28+
for (int i = 0; i < dp.size(); i++) {
29+
for (int j = 0; j < dp[0].size(); j++) {
30+
cout << dp[i][j] << " ";
31+
}
32+
cout << endl;
33+
}
34+
return dp[0][0] + key.size();
35+
}
36+
};
37+
```
38+
39+
2 3 4 5 5 4 3
40+
2 1 0 0 1 2 3
41+
42+
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
2+
## 思路
3+
这道题目直接的想法可能是两层for循环再加上used数组表示使用过的元素。这样的的时间复杂度是O(n^2)。
4+
5+
### 方法一
6+
其实这道题可以用很朴实的方法,时间复杂度就就是O(n)了,C++代码如下:
7+
8+
```
9+
class Solution {
10+
public:
11+
vector<int> sortArrayByParityII(vector<int>& A) {
12+
vector<int> even(A.size() / 2); // 初始化就确定数组大小,节省开销
13+
vector<int> odd(A.size() / 2);
14+
vector<int> result(A.size());
15+
int evenIndex = 0;
16+
int oddIndex = 0;
17+
int resultIndex = 0;
18+
// 把A数组放进偶数数组,和奇数数组
19+
for (int i = 0; i < A.size(); i++) {
20+
if (A[i] % 2 == 0) even[evenIndex++] = A[i];
21+
else odd[oddIndex++] = A[i];
22+
}
23+
// 把偶数数组,奇数数组分别放进result数组中
24+
for (int i = 0; i < evenIndex; i++) {
25+
result[resultIndex++] = even[i];
26+
result[resultIndex++] = odd[i];
27+
}
28+
return result;
29+
}
30+
};
31+
```
32+
33+
时间复杂度:O(n)
34+
空间复杂度:O(n)
35+
36+
### 方法二
37+
以上代码我是建了两个辅助数组,而且A数组还相当于遍历了两次,用辅助数组的好处就是思路清晰,优化一下就是不用这两个辅助树,代码如下:
38+
39+
```
40+
class Solution {
41+
public:
42+
vector<int> sortArrayByParityII(vector<int>& A) {
43+
vector<int> result(A.size());
44+
int evenIndex = 0; // 偶数下表
45+
int oddIndex = 1; // 奇数下表
46+
for (int i = 0; i < A.size(); i++) {
47+
if (A[i] % 2 == 0) {
48+
result[evenIndex] = A[i];
49+
evenIndex += 2;
50+
}
51+
else {
52+
result[oddIndex] = A[i];
53+
oddIndex += 2;
54+
}
55+
}
56+
return result;
57+
}
58+
};
59+
```
60+
61+
时间复杂度O(n)
62+
空间复杂度O(n)
63+
64+
### 方法三
65+
66+
当然还可以在原数组上修改,连result数组都不用了。
67+
68+
```
69+
class Solution {
70+
public:
71+
vector<int> sortArrayByParityII(vector<int>& A) {
72+
int oddIndex = 1;
73+
for (int i = 0; i < A.size(); i += 2) {
74+
if (A[i] % 2 == 1) { // 在偶数位遇到了奇数
75+
while(A[oddIndex] % 2 != 0) oddIndex += 2; // 在奇数位找一个偶数
76+
swap(A[i], A[oddIndex]); // 替换
77+
}
78+
}
79+
return A;
80+
}
81+
};
82+
```
83+
84+
时间复杂度:O(n)
85+
空间复杂度:O(1)
86+
87+
这里时间复杂度并不是O(n^2),因为偶数位和奇数位都只操作一次,不是n/2 * n/2的关系,而是n/2 + n/2的关系!
88+

problems/回溯算法理论基础.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
回溯法,一般可以解决如下几种问题:
2929

3030
* 组合问题:N个数里面按一定规则找出k个数的集合
31-
* 排列问题:N个数按一定规则全排列,有几种排列方式
3231
* 切割问题:一个字符串按一定规则有几种切割方式
3332
* 子集问题:一个N个数的集合里有多少符合条件的子集
33+
* 排列问题:N个数按一定规则全排列,有几种排列方式
3434
* 棋盘问题:N皇后,解数独等等
3535

3636
**相信大家看着这些之后会发现,每个问题,都不简单!**

0 commit comments

Comments
 (0)