LeetCode刷题记录(回溯篇)
LeetCode刷题记录(回溯篇),个人向。
[46] Permutations
难度:Medium
语言:C++
题目描述。
使用used
来记录当前已使用的元素,尝试增加used
,递归调用,记录path
,在path
长度达到nums.size()
时将结果记录到result
即可。
class Solution {
public:
void backtracking(vector<int> &nums, vector<bool> &used, vector<int> &path,
vector<vector<int>> &result) {
int size = nums.size();
if (path.size() == size) {
result.push_back(path);
return;
}
for (int i = 0; i < size; ++i) {
if (used[i]) {
continue;
}
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used, path, result);
path.pop_back();
used[i] = false;
}
return;
}
vector<vector<int>> permute(vector<int> &nums) {
vector<bool> used(nums.size(), false);
vector<int> path;
vector<vector<int>> result;
backtracking(nums, used, path, result);
return result;
}
};
[78] Subsets
难度:Medium
语言:C++
题目描述。
相比上一题求全排列,这一题的不同点有二:
- 我们要求的是子集,单个集合不要求包含所有元素,因此在这棵递归树上,我们要求的不止是叶子节点,而是所有节点。因此不是在递归结束的时候记录结果,而是每次调用都记录一下结果。
- 相比全排列,集合对元素的顺序不再做要求。因此相比之前使用
used
数组,这里可以只记录startIndex
,然后每次递归从startIndex
往后遍历,来做到包含不同的元素组合。
class Solution {
public:
void backtracking(vector<int> &nums, int startIndex, vector<int> &path,
vector<vector<int>> &result) {
int size = nums.size();
result.push_back(path);
for (int i = startIndex; i < size; ++i) {
path.push_back(nums[i]);
backtracking(nums, i + 1, path, result);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int> &nums) {
vector<int> path;
vector<vector<int>> result;
backtracking(nums, 0, path, result);
return result;
}
};
[17] Letter Combinations of a Phone Number
难度:Medium
语言:C++
题目描述。
构造一个mapping
用来存数字到字母的映射,同时用mapping[digits[startIndex] - '2']
来取这个映射。回溯部分和之前大同小异,更新一下startIndex,同时记录路径,最后加进result
即可。
class Solution {
public:
const vector<string> mapping = {"abc", "def", "ghi", "jkl",
"mno", "pqrs", "tuv", "wxyz"};
void backtracking(string &digits, int startIndex, string &path,
vector<string> &result) {
int size = digits.size();
if (startIndex >= size) {
if (size > 0) {
result.push_back(path);
}
return;
}
string chars = mapping[digits[startIndex] - '2'];
int numChars = chars.size();
for (int i = 0; i < numChars; ++i) {
path.push_back(chars[i]);
backtracking(digits, startIndex + 1, path, result);
path.pop_back();
}
}
vector<string> letterCombinations(string digits) {
vector<string> result;
string path;
backtracking(digits, 0, path, result);
return result;
}
};
[39] Combination Sum
难度:Medium
语言:C++
题目描述。
对回溯的代码逐渐熟练,同样是一次写出答案。
和之前的代码大同小异。这里因为candidates[i]
是大于0的,所以出现new_sum == target
就不用继续递归了。
当我们选择了某个数字,则以后的递归只会考虑该数字以及之后的数字,不再考虑之前的数字,这保证了结果的唯一性(不出现重复组合)。
这道题的关键在于数字是可以重复使用的,这里我们用递归参数所使用的index
为i
而不是i+1
来体现。
class Solution {
public:
void backtracking(const vector<int> &candidates, const int target,
int index, int cur_sum, vector<int> &choosen,
vector<vector<int>> &result) {
int size = candidates.size();
if (index >= size) {
return;
}
for (int i = index; i < size; ++i) {
int new_sum = cur_sum + candidates[i];
if (new_sum > target) {
continue;
}
choosen.push_back(candidates[i]);
if (new_sum == target) {
result.push_back(choosen);
// 0 will not appear in candidates, and 2 <= candidates[i] <= 40
// so no need to backtrack further
} else {
backtracking(candidates, target, i, new_sum, choosen, result);
}
choosen.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int> &candidates, int target) {
vector<int> choosen;
vector<vector<int>> result;
backtracking(candidates, target, 0, 0, choosen, result);
return result;
}
};
[22] Generate Parentheses
难度:Medium
语言:C++
题目描述。
回溯时同时记录左括号和右括号的剩余数量。若左括号剩余数量大于0则可以添加左括号。若右括号剩余数量大于左括号(防止先右括号后左括号导致无法匹配)则可以添加右括号。
class Solution {
public:
void backtracking(int left, int right, string &s, vector<string> &result) {
if (left == 0 && right == 0) {
result.push_back(s);
return;
}
if (left > 0) {
s.push_back('(');
backtracking(left - 1, right, s, result);
s.pop_back();
}
if (right > left) {
s.push_back(')');
backtracking(left, right - 1, s, result);
s.pop_back();
}
}
vector<string> generateParenthesis(int n) {
string s;
vector<string> result;
backtracking(n, n, s, result);
return result;
}
};
[79] Word Search
难度:Medium
语言:C++
题目描述。
代码有些类似DFS。记录当前坐标x
和y
以及要匹配的字符的index k
。如果当前字符不对则递归终止,否则看k
是否已经到word
的最后一个了,如果没有就通过DFS的方式去搜索周围四个。
class Solution {
public:
vector<pair<int, int>> directions = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
bool backtracking(const vector<vector<char>> &board, const string &word,
int x, int y, int k, vector<vector<bool>> &visited) {
if (board[x][y] != word[k]) {
return false;
} else if (k == word.size() - 1) {
return true;
}
int m = board.size();
int n = board[0].size();
visited[x][y] = true;
for (auto [dx, dy] : directions) {
int nx = x + dx, ny = y + dy;
if (nx < 0 || ny < 0 || nx >= m || ny >= n || visited[nx][ny]) {
continue;
}
if (backtracking(board, word, nx, ny, k + 1, visited)) {
return true;
}
}
visited[x][y] = false;
return false;
}
bool exist(vector<vector<char>> &board, string word) {
int m = board.size();
int n = board[0].size();
vector<vector<bool>> visited(board.size(),
vector<bool>(board[0].size(), false));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (backtracking(board, word, i, j, 0, visited)) {
return true;
}
}
}
return false;
}
};
[131] Palindrome Partitioning
难度:Medium
语言:C++
题目描述。
还是回溯题的老套路。记录一下startIndex
,然后for
循环从startIndex
开始找下一个回文子串。若startIndex
到i
是回文子串,则记录到path
,然后从i+1
递归即可。
至于判断是否是回文串,这里写了两个版本。一种是常规使用双指针来判断字符串是否是回文串(isPalindrome
);另一种是优化方法,使用了动态规划做预处理,提前算好所有子串是否是回文串(palindromeTable
),判断的时候直接查表即可,避免了重复计算。
class Solution {
vector<vector<bool>> palindromeTable;
bool isPalindrome(const string &s, int startIndex, int endIndex) {
while (startIndex <= endIndex) {
if (s[startIndex] != s[endIndex]) {
return false;
}
++startIndex;
--endIndex;
}
return true;
}
void backtracking(const string &s, int startIndex, vector<string> &path,
vector<vector<string>> &result) {
int size = s.size();
if (startIndex >= size) {
result.push_back(path);
return;
}
for (int i = startIndex; i < size; ++i) {
// if (isPalindrome(s, startIndex, i)) {
if (palindromeTable[startIndex][i]) {
path.push_back(s.substr(startIndex, i - startIndex + 1));
backtracking(s, i + 1, path, result);
path.pop_back();
}
}
}
public:
vector<vector<string>> partition(string s) {
vector<string> path;
vector<vector<string>> result;
int size = s.size();
palindromeTable.assign(size, vector<bool>(size, true));
for (int i = size - 1; i >= 0; --i) {
for (int j = i + 1; j < size; ++j) {
palindromeTable[i][j] =
palindromeTable[i + 1][j - 1] && s[i] == s[j];
}
}
backtracking(s, 0, path, result);
return result;
}
};
[51] N-Queens
难度:Hard
语言:C++
题目描述。
大名鼎鼎的N皇后问题。看似暴力穷举的背后又存在许多可剪枝优化的空间。
- 每轮递归无需两个坐标轴双重循环遍历。我们可规定递归的顺序是从最上面一行到最下面一行,每次在一行里面找下一个N皇后的位置。这样一方面保证了顺序(防止出现重复结果),另一方面我们实际只需要一层循环,即遍历这一行的位置,看哪一列可作为N皇后的位置即可。如果找到位置,再递归进入下一行的遍历查找。
- 检查位置是否合法时,由于我们填N皇后的位置是从上到下(按行)来填的,因此我们只需要检查行坐标小于当前坐标的那些位置即可,可减少检查判断的数量。(要注意皇后除了横着竖着还能斜着走,一开始不了解规则都没注意这个)
当我们最后填完了最后一行(n == row
),说明每一行我们都已经找到了一个合法的N皇后的位置,将此时的棋盘加入结果即可(result.push_back(board);
)。
class Solution {
bool check(int n, int x, int y, const vector<string> &board) {
for (int i = 0; i < x; ++i) {
if (board[i][y] == 'Q') {
return false;
}
}
const vector<pair<int, int>> direction = {{-1, 1}, {-1, -1}};
for (int i = 1; i < n; ++i) {
for (auto [dx, dy] : direction) {
int nx = x + i * dx;
int ny = y + i * dy;
if (nx < 0 || ny < 0 || ny >= n) {
continue;
}
if (board[nx][ny] == 'Q') {
return false;
}
}
}
return true;
}
void backtracking(int n, int row, vector<string> &board,
vector<vector<string>> &result) {
if (n == row) {
result.push_back(board);
return;
}
for (int j = 0; j < n; ++j) {
if (check(n, row, j, board)) {
board[row][j] = 'Q';
backtracking(n, row + 1, board, result);
board[row][j] = '.';
}
}
}
public:
vector<vector<string>> solveNQueens(int n) {
vector<string> board(n, string(n, '.'));
vector<vector<string>> result;
backtracking(n, 0, board, result);
return result;
}
};
总结
回溯的题目基本都存在一套函数模版:
void backtracking(parameters) {
if (termination_condition) {
store_result;
return;
}
for (choice : elements in the current level's set (the number of children of a tree node is the size of the set)) {
process_node;
backtracking(path, choices); // recursive call
backtrack, undo processing result
}
}
这里的几道题目的代码基本都是按着上面的模版来的,无非有一些小修改。除了[22] Generate Parentheses在递归上略为特殊,其记录了剩余左括号和右括号的数量,然后再根据该数量关系来决定如何递归。