子集II
- 力扣题目链接
- 这道题是子集的升级版,可包含重复元素数组的子集,涉及最终解集的去重
- 按照子集的写法写一遍,去重的逻辑是排序+used数组或startIndex
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int start){
result.push_back(path);
if(start>=nums.size()) return;
for(int i=start; i<nums.size(); i++){
// 树层去重
if(i>start && nums[i]==nums[i-1]) continue;
path.push_back(nums[i]);
backtracking(nums, i+1);
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
if(nums.empty()) return result;
sort(nums.begin(), nums.end());
backtracking(nums, 0);
return result;
}
递增子序列
- 使用unordered_set对树层去重:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int start){
if(path.size()>1) result.push_back(path);
if(start>=nums.size()) return;
unordered_set<int> u; // 使用set对本层元素进行去重
for(int i=start; i<nums.size(); i++){
if((!path.empty() && nums[i]<path.back())
|| u.find(nums[i])!=u.end()) continue;
u.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;
}
- 优化后使用数组记录:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int start){
if(path.size()>1) result.push_back(path);
if(start>=nums.size()) return;
int used[201]={0}; // 使用used数组对本层元素进行去重
for(int i=start; i<nums.size(); i++){
if((!path.empty() && nums[i]<path.back())
|| used[nums[i]+100]==1) continue;
used[nums[i]+100]=1;
path.push_back(nums[i]);
backtracking(nums, i+1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
全排列
- 力扣题目链接
- 排列问题下标从0开始了,但需要used数组的参与
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(used[i]) continue;
path.push_back(nums[i]);
used[i]=1;
backtracking(nums, used);
path.pop_back();
used[i]=0;
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
}
全排列 II
- 力扣题目链接
- 可包含重复数字的,同样的去重方法(排序的used数组就有双重用处了)
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)
continue;
if(!used[i]){
path.push_back(nums[i]);
used[i]=true;
backtracking(nums, used);
path.pop_back();
used[i]=false;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
}
used[i - 1] == true也可以,但效率不高(树枝去重<树层去重)
重新安排行程
- 力扣题目链接
- 从本题往下3题,就是难题了,题目涉及的内容非常综合
- 这题是图论额外拓展题,涉及深搜,但是深搜的本质是递归!
- 使用unordered_map记录机票的图结构
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
bool backtracking(int ticketNum, vector<string>& result){
// ticketNum表示有多少个航班
if(result.size()==ticketNum+1) return true;
for(pair<const string, int>& target : targets[result[result.size()-1]]){
if(target.second>0){ // 记录机票是否用过
result.push_back(target.first);
target.second--;
if(backtracking(ticketNum, result)) return true;
target.second++;
result.pop_back();
}
}
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
for(const vector<string>& vec : tickets)
// 目的是保证每张机票只用一次
targets[vec[0]][vec[1]]++; // 记录映射关系
vector<string> result;
result.push_back("JFK"); // 起始机场
backtracking(tickets.size(), result);
return result;
}
- 一定要加上引用即
& target,因为后面有对target.second做减减操作,如果没有引用,单纯复制,这个结果就没记录下来,那最后的结果就不对了。加上引用之后,就必须在string前面加上const,因为map中的key 是不可修改了,这就是语法规定了。
N皇后
- 力扣题目链接
- 一维递归问题,注意合法性的判断
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行
// 之所以chessboard设置为参数,是因为不知道n的大小
void backtracking(int n, int row, vector<string>& chessboard){
if(row==n) {result.push_back(chessboard); return;}
for(int col=0; col<n; col++)
if(isValid(row, col, chessboard, n)){
chessboard[row][col]='Q';
backtracking(n, row+1, chessboard);
chessboard[row][col]='.'; // 回溯
}
}
bool isValid(int row, int col, vector<string>& chessboard, int n){
// 检查列
for(int i=0; i<row; i++)
if(chessboard[i][col]=='Q') return false;
// 检查主对角线是否有皇后
for(int i=row-1, j=col-1; i>=0 && j>=0; i--, j--)
if(chessboard[i][j]=='Q') return false;
// 检查副对角线是否有皇后
for(int i=row-1, j=col+1; i>=0 && j<n; i--, j++)
if(chessboard[i][j]=='Q') return false;
return true;
}
vector<vector<string>> solveNQueens(int n) {
// 注意棋盘的初始化方法
vector<string> chessboard(n, string(n, '.'));
backtracking(n, 0, chessboard);
return result;
}
解数独
- 力扣题目链接
- 二维递归,理解了逻辑代码并不难
// 二维递归回溯
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 k='1'; k<='9'; k++)
if(isValid(board, i, j, k)){
board[i][j]=k;
// 因为返回值是bool
if(backtracking(board)) return true;
board[i][j]='.';
}
return false; // 试完9个数都不行
}
return true; // 遍历完没有返回false,说明找到了合适棋盘位置了
}
bool isValid(vector<vector<char>>& board, int row, int col, char num){
// 行
for(int j=0; j<9; j++)
if(board[row][j]==num) return false;
// 列
for(int i=0; i<9; i++)
if(board[i][col]==num) return false;
// 九宫格
int startRow=(row/3)*3;
int startCol=(col/3)*3;
for(int i=startRow; i<startRow+3; i++)
for(int j=startCol; j<startCol+3; j++)
if(board[i][j]==num) return false;
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
总结
- 递归和回溯是非常重要的思维方式,相当于半只脚踏入了DP的门槛,接下来让我们来迎接图论和DP的暴击!
参考资料
[1] 代码随想录
[2] Leetcode题解