Leetcode hot100
回溯
1.DFS 和回溯算法区别
DFS 是一个劲的往某一个方向搜索,而回溯算法建立在 DFS 基础之上的,但不同的是在搜索过程中,达到结束条件后**,恢复状态,回溯上一层,再次搜索**。因此回溯算法与 DFS 的区别就是有无状态重置
2.何时使用回溯算法
当问题需要 “回头”,以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止
3.怎么样写回溯算法(从上而下,※代表难点,根据题目而变化)
①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
②根据题意,确立结束条件
③找准选择列表(与函数参数相关),与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择
作者:Tom也要Offer
链接:https://leetcode.cn/problems/subsets/submissions/519617280/
1.电话号码的字母组合
电话号码的字母组合
准备
首先使用哈希表存储每个数字对应的所有可能的字母,然后进行回溯操作。
递归函数
path存放排列的组合,ans存放最终答案,start index确定排列的元素
void dfs(string &digits, string &path, int start)
递归终止条件:
当path的长度等于digits的长度时,说明已经排列完了
递归过程:
先遍历每个输入数字;
取出每个数字对应字符串;
遍历字符串,取出字符串中每个元素,加入path;
递归,此时用index控制递归层数;
退出path;
//遍历每一个输入的字符
for (int i = start; i < digits.size(); i++) {
//取出字符对应的字符串
string num = mp[digits[i]];
//遍历每一个字符串中的字符并添加到路径中
for (int j = 0; j < num.length(); j++) {
path.push_back(num[j]);
//递归
dfs(digits, path, i + 1);
//退出字符
path.pop_back();
}
}
代码
class Solution {
public:
unordered_map<char, string> mp = {
{'2', "abc"},
{'3', "def"},
{'4', "ghi"},
{'5', "jkl"},
{'6', "mno"},
{'7', "pqrs"},
{'8', "tuv"},
{'9', "wxyz"}
};
vector<string> ans;
vector<string> letterCombinations(string digits) {
string path;
if (digits.size() == 0) return ans;
dfs(digits, path, 0);
return ans;
}
void dfs(string &digits, string &path, int start) {
//递归终止条件
if (path.size() == digits.size()) {
ans.push_back(path);
return;
}
//遍历每一个输入的字符
for (int i = start; i < digits.size(); i++) {
//取出字符对应的字符串
string num = mp[digits[i]];
//遍历每一个字符串中的字符并添加到路径中
for (int j = 0; j < num.length(); j++) {
path.push_back(num[j]);
//递归
dfs(digits, path, i + 1);
//退出字符
path.pop_back();
}
}
}
};
2.括号生成
括号生成
本题是求“()”不重复的全排列,还必须有效。
递归函数:
n确定递归树层数,path记录递归路径,left right分别记录 ( 左括号和右括号 ) 的个数, ans存放最终答案
void dfs(int n, string &path, int left, int right)
递归终止条件:
如果path内元素的左右括号个数分别为n则加入ans,递归结束
如何确保添加的都有效呢?
可以用left,right判断现有string内的元素。
当左括号的个数小于n的时候,可以一直往里加,也就是说加左括号的情况是left<n
当右括号的个数小于左括号时,可以加右括号,也就是 left>right时加右括号
class Solution {
public:
vector<string> ans;
vector<string> generateParenthesis(int n) {
if (n == 0) return ans;
string path;
dfs(n, path, 0, 0);
return ans;
}
void dfs(int n, string &path, int left, int right) {
//无效情况
if (left > n || right > n || right > left) {
return;
}
//有效情况
if (left == n && right == n) {
ans.push_back(path);
return;
}
//左括号数量不满足,先放置左括号
if (left < n) {
path.push_back('(');
left++;
dfs(n, path, left, right);
//回溯,当前位置还可能放右括号
left--;
path.pop_back();
}
//右括号数量小于左括号时,可以放置右括号
if (left > right) {
path.push_back(')');
right++;
dfs(n, path, left, right);
right--;
path.pop_back();
}
}
};
3.单词搜索
单词搜索
递归函数:
使用深度优先搜索(DFS)+ 剪枝解决。
深度优先搜索: 即暴力法遍历矩阵中所有字符串可能性。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
剪枝: 在搜索中,遇到“这条路不可能和目标字符串匹配成功”的情况,例如当前矩阵元素和目标字符不匹配、或此元素已被访问,则应立即返回,从而避免不必要的搜索分支。
递归参数:
当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 index , 当前元素是否被访问过的记录visited。
终止条件:
- 返回 false :
- (1) 行或列索引越界
- (2) 当前矩阵元素与目标字符不同
- (3) 当前矩阵元素已访问过
- 返回 true:
- index = len(word) - 1 ,即字符串 word 已全部匹配。
递归工作:
- 标记当前矩阵元素: 将 visited[i][j] 修改为 true ,代表此元素已访问过,防止之后搜索时重复访问。
- 搜索下一单元格: 朝当前元素的 上、下、左、右 四个方向开启下层递归,并记录结果至 res 。
- 还原当前矩阵元素: 如果没找到匹配元素,则将 visited[i][j] 元素还原至初始值 false。
返回值: 返回布尔量 res ,代表是否搜索到目标字符串。
为什么要用两层循环把dfs套在里面?
我们知道很多题目,直接dfs求解就可以返回了,而这题不可以,关键在于这个字符串的起始位置可以不在[0, 0]这个位置。而你如果不用循环套起来,就默认起点只能在[0, 0],这样会丢掉很多解。
为什么要用visit?
由于我们这条路是可以回头的,并非只能往右下方向走,所以可能会遇到回踩前一个刚刚访问过的格子,而这个格子,题目里说是不可以重复使用的。
为什么visit需要复位?
因为当前格子作为中途某一处的起始点,并且走不通时,它是可以回退到上一个格子,并且选择其他方向重新开始的。而此时我们不希望当前格子的遍历路径影响到回退后新路径的尝试。
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
vector<vector<bool>> visited (board.size(), vector(board[0].size(), false));
//从每一个位置开始搜索
for (int i = 0; i < board.size(); i++) {
for (int j = 0; j < board[0].size(); j++) {
if(dfs(board, visited, word, 0, i, j)) return true;
}
}
return false;
}
bool dfs(vector<vector<char>>& board, vector<vector<bool>>& visited, string &word, int index, int i, int j) {
//已经到最后一个元素了
if (index == word.size()) return true;
//false的情况,访问越界 或者 不等于word对应字符,或者已经访问过
if (i >= board.size() || i < 0 || j >= board[0].size() || j < 0 ||
board[i][j] != word[index] || visited[i][j] == true) {
return false;
}
visited[i][j] = true;
//上下左右搜索
if (dfs(board, visited, word, index + 1, i + 1, j) || dfs(board, visited, word, index + 1, i, j + 1) ||
dfs(board, visited, word, index + 1, i - 1, j) || dfs(board, visited, word, index + 1, i, j - 1))
return true;
//没搜索到,置为未访问
visited[i][j] = false;
return false;
}
};
4.分割回文串
分割回文串
作者:代码随想录
链接:https://leetcode.cn/problems/palindrome-partitioning/solutions/640336/131-fen-ge-hui-wen-chuan-hui-su-sou-suo-yp2jq/
本题这涉及到两个关键问题:
切割问题,有不同的切割方式
判断回文
例如对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段…。
感受出来了不?
所以切割问题,也可以抽象为一棵树形结构,如图:
递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。
此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。
回溯三部曲
- 递归函数参数
全局变量数组path存放切割后回文的子串,二维数组result存放结果集。 (这两个参数可以放到函数参数里)
本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。
代码如下:
vector<vector<string>> result;
vector<string> path; // 放已经回文的子串
void backtracking (const string& s, int startIndex) {
- 递归函数终止条件
从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。
那么在代码里什么是切割线呢?
在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线。
所以终止条件代码如下:
void backtracking (const string& s, int startIndex) {
// 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
if (startIndex >= s.size()) {
result.push_back(path);
return;
}
}
- 单层搜索的逻辑
在for (int i = startIndex; i < s.size(); i++)循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。
首先判断这个子串是不是回文,如果是回文,就加入在vector path中,path用来记录切割过的回文子串。
代码如下:
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) { // 是回文子串
// 获取[startIndex,i]在s中的子串
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
} else { // 如果不是则直接跳过
continue;
}
backtracking(s, i + 1); // 寻找i+1为起始位置的子串
path.pop_back(); // 回溯过程,弹出本次已经填在的子串
}
注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1。
- 判断回文子串
最后我们看一下回文子串要如何判断了,判断一个字符串是否是回文。
可以使用双指针法,一个指针从前向后,一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了。
那么判断回文的C++代码如下:
bool isPalindrome(const string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
class Solution {
public:
vector<vector<string>> result;
vector<string> path;
vector<vector<string>> partition(string s) {
dfs(s, 0);
return result;
}
bool isPalindrome(const string& s, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (s[i] != s[j]) {
return false;
}
}
return true;
}
void dfs(string &s, int startindex) {
//当切割的位置大于最后一个字符,说明已经找到了一组切割方案了
if (startindex >= s.size()) {
result.push_back(path);
}
//从当前位置开始遍历切割
for (int i = startindex; i < s.size(); i++) {
//如果子串是回文传
if (isPalindrome(s, startindex, i)) {
//提取子串加入path
string str = s.substr(startindex, i - startindex + 1);
path.push_back(str);
} else {
//子串不是回文串,继续
continue;
}
//从下一个位置开始继续递归
dfs(s, i + 1);
path.pop_back();
}
}
};