46m. 全排列
方法一:回溯搜索
用时:16m36s
思路
在回溯搜索的基础上设置一个哈希集合hashSet
,用于去重,每次遍历的时候不能遍历重复的元素。哈希集合可以用unordered_set
或者数组实现。
- 时间复杂度:
O
(
n
⋅
n
!
)
O(n \cdot n!)
O(n⋅n!),全排列数有
n
!
n!
n!种排列方式,将结果保存至
res
需要 O ( n ) O(n) O(n)时间复杂度。 - 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
unordered_set<int> hashSet;
void backTracking(vector<int>& nums) {
if (path.size() == nums.size()) { // 如果全部数组全部遍历完了,则将结果保存并终止递归
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); ++i) {
if (hashSet.find(nums[i]) == hashSet.end()) { // 只有当遍历的元素是非重复元素是才加入path
path.push_back(nums[i]);
hashSet.insert(nums[i]);
backTracking(nums); // 递归
path.pop_back(); // 回溯
hashSet.erase(nums[i]);
}
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
backTracking(nums);
return res;
}
};
看完讲解的思考
无。
代码实现遇到的问题
无。
47m. 全排列 II
方法一:回溯
用时:7m19s
思路
在上一题的基础上再使用一个哈希表used进行去重,与hashSet不同,used是在每层递归中创建的,用于for循环时不要遍历当前for循环已经遍历过的元素。
- 时间复杂度: O ( n ⋅ n ! ) O(n \cdot n!) O(n⋅n!)。
- 空间复杂度: O ( n 2 ) O(n^2) O(n2)。unordered_set在每次递归中总共需要的空间是 O ( ( n − 1 ) + ( n − 2 ) + . . . + 1 ) = O ( n 2 ) O((n-1)+(n-2)+...+1)=O(n^2) O((n−1)+(n−2)+...+1)=O(n2)。
C++代码
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
bool hashSet[8] = {false};
void backTracking(vector<int>& nums) {
if (path.size() == nums.size()) {
res.push_back(path);
return;
}
unordered_set<int> used;
for (int i = 0; i < nums.size(); ++i) {
if (!hashSet[i] && used.find(nums[i]) == used.end()) {
path.push_back(nums[i]);
hashSet[i] = true;
used.insert(nums[i]);
backTracking(nums);
path.pop_back();
hashSet[i] = false;
}
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
backTracking(nums);
return res;
}
};
方法二:排序+回溯
用时:8m27s
思路
方法一每层递归都要创建一个哈希集合,空间复杂度高。
可以通过先排序来去重。首先将数组排序,然后在for循环遍历过程中,如果当前元素等于上一个元素,并且上一个元素并不是已经选取在path中的元素,那么就跳过。
- 时间复杂度: O ( n ⋅ n ! ) O(n \cdot n!) O(n⋅n!)。
- 空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
private:
vector<vector<int>> res;
vector<int> path;
bool hashSet[8] = {false};
void backTracking(vector<int>& nums) {
if (path.size() == nums.size()) {
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); ++i) {
// 条件一 !hashSet[i] :当前位置的元素并不在path中,防止重复选取某个位置的元素
// 条件二 i == 0 || nums[i] != nums[i - 1] || hashSet[i - 1] :
// 当前位置是第一个,或者当前位置的元素不等于上一个位置的元素,或者上一个位置的元素是被选取的
// 条件一是为了不要选取到重复位置的元素,因为每个位置的元素只能用一次
// 条件二是为了不要有重复的全排列结果
if (!hashSet[i] && (i == 0 || nums[i] != nums[i - 1] || hashSet[i - 1])) {
path.push_back(nums[i]);
hashSet[i] = true;
backTracking(nums);
path.pop_back();
hashSet[i] = false;
}
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
backTracking(nums);
return res;
}
};
看完讲解的思考
回溯算法中如果要去重就要想到先排序!!!
代码实现遇到的问题
无。
332h. 重新安排行程
方法一:回溯
用时:1h23m32s
思路
首先遍历航班列表,记录每个出发机场都可以去到哪个机场,以及有几趟航班,这里选择的数据结构是unordered_map<string, map<string, int>>
,表示<出发机场, <到达机场, 航班次数>>,unordered_map
出发机场的键值用map
数据结构的原因是,我们最终的答案需要的是字典排序最小的路径,而map
会自动根据键进行排序,这样在遍历的过程我们就是先遍历字典序小的机场。
递归三部曲:
-
返回值及参数:
本题的答案路径是唯一的,所以我们不需要用额外的数组来记录路径,只用一个数组来记录当前的路径即可,由于答案唯一,当我们找到符合要求的路径时,我们就直接结束所有递归,返回res
即可。因为需要判断不同的递归路径是否找到有效路径,递归函数的返回值我们设置为bool
。当递归函数返回true
时,表明当前递归路径已经找到了有效路径,不用继续循环和递归了;当递归函数返回false
时,表明当前递归路径还没找到有效路径,需要继续循环和递归。
当前路径res
可以作为参数传递,也可以直接放在类的成员属性中。 -
递归终止条件:当res的长度等于机票数ticketNum+1时,表明全部地点都已去过,终止递归。
-
单层搜索逻辑:
- 当前所在的机场是res.back(),遍历当前机场能去到的机场:
由于for (pair<const string, int>& target : targets[res.back()]) { if (target.second > 0) {...}
map
会自动根据键进行排序,这样在遍历的过程我们就是先遍历字典序小的机场,所以找到的第一条有效路径也是字典序最小的有效路径。 - “飞往”下一个机场进行递归:
如果在当前递归路径上找到了有效路径,也就是递归函数返回了true,则直接return true。res.push_back(target.first); // 飞往下一个机场,则路径上添加新机场 --target.second; // 此条航线的航班次数减一 if (backTracking()) return true;
- 如果没有找到有效路径,即递归函数返回了false,则回溯,遍历下一个机场。
- 遍历完当前机场的所有航线都没有找到有效路径,则返回false。
- 当前所在的机场是res.back(),遍历当前机场能去到的机场:
-
时间复杂度: O ( n ) O(n) O(n)。
-
空间复杂度: O ( n ) O(n) O(n)。
C++代码
class Solution {
private:
unordered_map<string, map<string, int>> targets; // <出发机场, <到达机场, 航班次数>>
int ticketNum;
vector<string> res;
bool backTracking() {
// 由于答案是唯一的,所以找到符合要求的路径后,直接终止递归并返回当前路径,所以不需要用额外的数组来记录答案
if (res.size() == ticketNum + 1) return true;
// 由于map会根据key自动排序,所以遍历的过程中,是先遍历字典排序靠前的字符串,那么第一条满足要求的路径就一定是字典排序最小的有效路径
for (pair<const string, int>& target : targets[res.back()]) {
if (target.second > 0) { // 当剩余航班次数大于0
res.push_back(target.first); // 飞往下一个地方
--target.second; // 此条航线的航班次数减一
if (backTracking()) return true; // 递归,如果当前递归路径找到了有效路径,则直接返回true,无需再继续搜索其他路径
res.pop_back(); // 若没有找到有效路径,则回溯
++target.second;
}
}
// 当前机场遍历完全部航线都没能找到有效路径,返回false,继续寻找其他路径
return false;
}
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
ticketNum = tickets.size();
for (const vector<string>& ticket : tickets) ++targets[ticket[0]][ticket[1]];
res.push_back("JFK");
backTracking();
return res;
}
};
看完讲解的思考
本题回溯的逻辑不难,难的是想到用这样的数据结构来存储信息以及设计这整个代码逻辑。
代码实现遇到的问题
一开始自己写的代码其实已经实现功能了,但是一开始自己想的方法还在纠结于怎么比较当前找到的路径是不是字典序最小的路径,用各种字符串操作之类的,最后时间复杂度太高,力扣上当测试案例数据量较大时就超出时间限制了,没有想到用map
自动排序。
最后的碎碎念
今天做了目前接触到的第二道hard,这道hard也没有想象中的难,但还是不能独自AC,而且也搞了好久,回溯专题准备结束噜。