第二十五天继续打卡,今天的题做了两天,主要想把后面3个hard问题看懂,递增子序列的难度也挺大的,排列问题需要用到used数组。
491.非递减子序列
做题过程
- 有几个点没想清楚没做出来:
- 数组里只有一个数该怎样收获结果,答案是数组size大于1就可以收获了
- 数组如何去重,答案是用哈希表标记同一树层遍历过的数,不能用之前的方法因为不能打乱数组的顺序
知识点
unordered_set<int> uset;
是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层- 时间复杂度: O(n * 2^n)
- 空间复杂度: O(n)
- 数值范围[-100,100],所以完全可以用数组来做哈希
回溯法
class Solution {
public:
vector<vector<int>>result;
vector<int>path;
void backtracking(vector<int>& nums, int startIndex) {
if (path.size() > 1) {
result.push_back(path);
}
unordered_set<int>uset;
for (int i = startIndex; i < nums.size(); i++) {
if ((!path.empty() && nums[i] < path.back()) || uset.find(nums[i]) != uset.end()) {
continue;
}
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
};
46.全排列
解题过程
- 使用数组来记录哪一个数组元素已经用过了,每次从下标0开始遍历,用过的数就跳过
知识点
- 时间复杂度: O(n!)
- 空间复杂度: O(n)
回溯法
class Solution {
public:
vector<vector<int>>result;
vector<int>path;
void backtracking(vector<int>& nums, vector<bool>& used) {
if (path.size() == nums.size()) {
result.push_back(path);
}
for (int i = 0; i < nums.size(); i++) {
if (used[i] == true) continue;
path.push_back(nums[i]);
used[i] = true;
backtracking(nums, used);
used[i] = false;
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool>used(nums.size(), false);
backtracking(nums, used);
return result;
}
};
47.全排列Ⅱ
做题过程
- 用used数组记录使用过的数,本题当前的数使用过(排列去重) 或者 之前的数未使用且现在的数等于之前的数(树层去重) 就跳过
知识点
- 去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了。
- 时间复杂度: O(n! * n)
- 空间复杂度: O(n)
回溯法
class Solution {
public:
vector<vector<int>>result;
vector<int>path;
void backtracking(vector<int>& nums, vector<bool>& used) {
if (path.size() == nums.size()) {
result.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
if ((i != 0 && nums[i] == nums[i - 1] && used[i - 1] == false) || used[i] == true) continue;
path.push_back(nums[i]);
used[i] = true;
backtracking(nums, used);
used[i] = false;
path.pop_back();
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(), false);
sort(nums.begin(), nums.end());
backtracking(nums, used);
return result;
}
};
332.重新安排行程
做题过程
- 题目看懂了,知道用dfs思想做,但看到有排序就不知道怎么做了
知识点
- 本题回溯需要有返回值,如果没有返回值的话会取到然排序更大更靠后的有效的行程。如果有返回值,第一次遇到正确答案就会直接返回不会进行后面的回溯过程。
- 存放映射关系定义为
unordered_map<string, map<string, int>> targets
- 含义为
unordered_map<string, map<string, int>> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets
回溯法
class Solution {
public:
vector<string>result;
unordered_map<string, map<string, int>>targets;
bool backtracking(vector<vector<string>>& tickets) {
if (result.size() == tickets.size() + 1) return true;
for (pair<const string, int>& target : targets[result.back()]) {
if (target.second > 0) {
result.push_back(target.first);
target.second--;
if (backtracking(tickets)) return true;
target.second++;
result.pop_back();
}
}
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
for (vector<string>&ticket : tickets) {
targets[ticket[0]][ticket[1]]++;
}
result.push_back("JFK");
backtracking(tickets);
return result;
}
};
51.N皇后
做题过程
- 想用两层循环嵌套回溯法遍历棋盘,但其实只用一层棋盘即可,棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了。
知识点
- 由于我们是从上到下添加皇后,所以判断皇后是否有效只需要判断该棋子上方、左上方、右上方是否有皇后
回溯法
class Solution {
public:
vector<vector<string>>result;
void backtracking(int n, int row, vector<string>&board) {
if (row == n) {
result.push_back(board);
return;
}
for (int col = 0; col < n; col++) {
if (!isValid(row, col, n, board)) continue;
board[row][col] = 'Q';
backtracking(n, row + 1, board);
board[row][col] = '.';
}
}
bool isValid(int row, int col, int n, vector<string>&board) {
for (int i = 0; i < row; i++) { // 检查棋子上方
if (board[i][col] == 'Q') return false;
}
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { // 检查棋子左上角
if (board[i][j] == 'Q') return false;
}
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { // 检查棋子右上角
if (board[i][j] == 'Q') return false;
}
return true;
}
vector<vector<string>> solveNQueens(int n) {
vector<string>board(n, string(n, '.'));
backtracking(n, 0, board);
return result;
}
};
37.解数独
做题过程
- 想到用二维递归,但没想到i j的递归范围和回溯for用什么来循环
知识点
- 递归函数的返回值需要是bool类型,因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
回溯法
class Solution {
public:
bool backtracking(vector<vector<char>>& board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') continue;
for (char c = '1'; c <= '9'; c++) {
if (isValid(i, j, c, board)) {
board[i][j] = c;
if (backtracking(board)) return true;
board[i][j] = '.';
}
}
return false;
}
}
return true;
}
bool isValid(int row, int col, char c, vector<vector<char>>& board) {
for (int i = 0; i < 9; i++) {
if (board[i][col] == c) return false;
}
for (int j = 0; j < 9; j++) {
if (board[row][j] == c) return false;
}
int rowBase = row / 3 * 3;
int colBase = col / 3 * 3;
for (int i = rowBase; i < rowBase + 3; i++) {
for (int j = colBase; j < colBase + 3; j++) {
if (board[i][j] == c) return false;
}
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};
回溯总结
回溯法理论基础
回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。
回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。
回溯算法能解决如下问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
遍历过程抽象为树形结构方便理解
回溯法的模板:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
回溯法经典问题
- 组合问题
- 77.组合
- 剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了。
- 216.组合总和III
- 已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉
- 在for循环加上
i <= 9 - (k - path.size()) + 1
的限制
- 39. 组合总和
- 如果是一个集合来求组合的话,就需要startIndex
- 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex
- 40.组合总和II
- **“树层去重”**很重要
- 17.电话号码的字母组合
- 本题每一个数字代表的是不同集合,也就是求不同集合之间的组合
- 77.组合
- 切割问题
- 131.分割回文串
- 几个难点:
- 切割问题其实类似组合问题
- 如何模拟那些切割线
- 切割问题中递归如何终止
- 在递归循环中如何截取子串
- 如何判断回文
- 几个难点:
- 131.分割回文串
- 重新安排行程(图论额外拓展)
- 棋盘问题
-
性能分析
-
子集问题分析:
- 时间复杂度:O(2n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2n)
- 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
排列问题分析:
- 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。
- 空间复杂度:O(n),和子集问题同理。
组合问题分析:
- 时间复杂度:O(2n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
- 空间复杂度:O(n),和子集问题同理。
N皇后问题分析:
- 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(nn),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * … * 1。
- 空间复杂度:O(n),和子集问题同理。
解数独问题分析:
- 时间复杂度:O(9m) , m是’.'的数目。
- 空间复杂度:O(n2),递归的深度是n2
-