组合和排列的区别:
组合不强调元素的顺序,而排列强调元素顺序
回溯法解决的问题都可以抽象为树形结构,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。
回溯法模板:
-
回溯函数模板返回值及参数
-
回溯函数终止条件
-
回溯搜索的遍历过程
for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。
for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这棵树全遍历完了,一般来说,搜索叶子节点就是找的其中一个结果了。
void backtracking(参数){
if(终止条件){
存放结果;
return;
}
for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)){
处理节点;
backtracking(路径,选择列表);//递归
回溯,撤销处理结果
}
}
(一)组合
返回1…n中所有可能的k个数的组合(注意这里是从1开始的)
用回溯替代for循环
一维数组存放组合path
二维数组存放所有的组合result
void backtracking(n, k, startIndex){
//终止条件
if(path.size() == k){
result.push_back(path);
return;
}
//单层搜索逻辑,从startIndex开始,不能重复搜索
for(i = startIndex; i <= n; i++){
path.push_back(i);
backtracking(n, k, i + 1);//按顺序搜索,取过的数不重复
path.pop();//回溯过程
}
}
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void backtracking(int n, int k, int startIndex){
if(path.size() == k){
result.push_back(path);
return;
}
for(int i = startIndex; i <= n; i++){
path.push_back(i);
backtracking(n, k, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
backtracking(n, k, 1);
return result;
}
};
(二)组合优化
剪枝操作:减去一些没有必要搜索的子孩子
当前层最多从n-(k-path.size)+1开始(因为是包括当前startIndex,所以要加上1)
void backtracking(n, k, startIndex){
//终止条件
if(path.size() == k){
result.push_back(path);
return;
}
//单层搜索逻辑,从startIndex开始,不能重复搜索
//缩小i的范围,剪枝
//for(i = startIndex; i <= n; i++){
for(i = startIndex; i <= n - (k - path.size() + 1; i++){
path.push_back(i);
backtracking(n, k, i + 1);//按顺序搜索,取过的数不重复
path.pop();//回溯过程
}
(三)组合总和III
注意:组合不强调元素的顺序
[1,9]里面取元素
剪枝:和为n,如果当前Sum已经大于targetSum,可以直接return
k为2,当前层最多从9-(k-path.size)+1开始
path 一维数组
result 二维数组
//sum为当前累加和,targetSum为目标累加和,targetIndex为当前层从哪里开始
void backtracking(targetSum, k, Sum, targetIndex){
//终止条件
if(Sum > targetSum) return; //约束剪枝
if(path.size() == k){
if(targetSum == Sum){//目标集合
result.push_back(path);
}
}
for(int i = startIndex; i <= 9-(k - path.size) + 1; i++){
Sum += i;
path.push_back(i);
backtracking(targetSum, k, Sum, i + 1);
sum -= i; //回溯
path.pop_back(i);
}
}
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void backtracking(int targetSum, int k, int startIndex, int Sum){
if(Sum > targetSum) return;
if(path.size() == k && Sum == targetSum){
result.push_back(path);
return;
}
for(int i = startIndex; i <= 9 - (k - path.size()) + 1; i ++){
path.push_back(i);
Sum += i;
backtracking(targetSum, k, i + 1, Sum);
Sum -= i;
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(n, k, 1, 0);
return result;
}
};
(四)电话号码的字母组合
哈希表存储映射关系
const string letterMap[10] = {
“”, // 0
“”, // 1
“abc”, // 2
“def”, // 3
“ghi”, // 4
“jkl”, // 5
“mno”, // 6
“pqrs”, // 7
“tuv”, // 8
“wxyz”, // 9
};
树的深度由输入的数字个数控制。每个数字对应的字母的长度对应树的宽度。
String s; //单个结果
vector<String> result;//所有结果
void backtracking(digits, index){//index指的是遍历到给定字符串遍历到第几个了,不需要startIndex了
if(index == digits.size()){//遍历到头了,注意不要减去1
result.push_back(s);
return;
}
int digit = digit[index] - '0';//将其变为一个真正的数字
String letter = letterMap[digit];//获取数字对应的字符串
for(int i = 0; i < letter.size(); i ++){//这里都是从0开始
s.push_back(letter[i]);
backtracking(digits, index + 1); //下一层递归
s.pop_back(letter[i]);
}
}
class Solution {
private:
const string letterMap[10]={
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz",
};
vector<string> result;
string s;
void backtracking(string digits, int index){
if(index == digits.size()){
result.push_back(s);
return;
}
int digit = digits[index] - '0';
string letters = letterMap[digit];
for(int i = 0; i < letters.size(); i ++){
s.push_back(letters[i]);
backtracking(digits, index + 1);
s.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
//注意这种情况,如果digits为空的话,输出空vector
if(digits.size() == 0){
return result;
}
backtracking(digits, 0);
return result;
}
};
(五)组合总和
树的深度不确定,而是靠组合的和的情况来决定树的深度
元素可以重复使用,下一层遍历都从index开始选取,注意也不是从0开始选取,因为都从0开始选取可能和前面的重复起来。
如果和>目标和,剪枝,直接return
二维数组result;
一维数组 path;
void backtracking(candidate, target, sum, startIndex){
if(sum > target) return;
if(sum == target){
result.push_back(path);
return;
}
for(int i = startIndex; i < candidate.size(); i ++){
path.push_back(candidate[i]);
sum += candidate[i];
backtracking(candidate, target, sum, i);//注意这里可以重复,所以不需要加1,将本层使用的元素还继续传给下一层
sum -= candidate[i];
path.pop_back();
}
}
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void traversal(vector<int>& candidates, int target, int sum, int startIndex){
if(sum > target) return;
if(sum == target){
result.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size(); i ++){
path.push_back(candidates[i]);
sum += candidates[i];
traversal(candidates, target, sum, i);
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
traversal(candidates, target, 0, 0);
return result;
}
};
剪枝:
在for循环里面剪枝:对数组先排序,当搜索到某个分支大于target后,就不用再搜索了。
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void traversal(vector<int>& candidates, int target, int sum, int startIndex){
if(sum > target) return;
if(sum == target){
result.push_back(path);
return;
}
// 如果 sum + candidates[i] > target 就终止遍历
for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i ++){
path.push_back(candidates[i]);
sum += candidates[i];
traversal(candidates, target, sum, i);
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
//先排序
sort(candidates.begin(), candidates.end());
traversal(candidates, target, 0, 0);
return result;
}
};
(六)组合总和II
区别:集合中有重复元素,但是结果中不能有重复的组合。数组中每个数字在组合中只能使用一次。
这里的去重,是在同一层上去除已经使用过的元素。而同一个树枝上(一个结果集中),可以有相同的元素。
树层去重(√)
树枝去重
注意去重要先对数组进行排序,让相同的元素放在一起,相同的元素只要搜索一次。
path 一维
result 二维
//used数组,标记哪个元素使用过
void backtracking(nums, targetSum, Sum, startIndex, used){
if(sum > targetSum) return;
if(sum == targetSum){
result.push_back(path);
return;
}
//剪枝放在单层搜索逻辑中
for(i = startIndex; i < nums.size(); i++){
//在排序后,相同元素是紧靠在一起的
//这里used[i - 1] == 0,是确保不在前一个的分支下面,防止树枝去重
if(i > 0 && nums[i] == nums[i - 1]&&used[i - 1] == 0){
continue;
}
path.push_back(nums[i]);
sum += nums[i];
used[i] = true; //维护Used数组去重
backtracking(nums, targetSum, Sum, i + 1, used);
used[i] = false;
sum -= nums[i];
path.pop_back();
}
}
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex){
if(sum > target) return;
if(sum == target){
result.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size(); i ++){
// 树层去重,要对同一树层使用过的元素进行跳过
if(i > startIndex && candidates[i] == candidates[i - 1]){
continue;
}
path.push_back(candidates[i]);
sum += candidates[i];
backtracking(candidates, target, sum, i + 1);
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
//注意这里要先排序
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0);
return result;
}
};
(七)分割回文串
类比组合问题选取元素的过程
一维数组path
二维数组result
//startIndex表示切割线
void backtracking(String s, startIndex){
if(startIndex >= s.size()){
result.push_back(path);
return;
}
for(int i = startIndex; i < s.size(); i++){
//[startIndex,i]这是当前切割的子串
//如果是回文子串,收集结果
if(isPartition(s, startIndex, i)){
path.push_back(子串);
}else{
continue;
}
backtracking(s, i + 1);
path.pop_back(); //回溯
return;
}
}
class Solution {
private:
vector<string> path;
vector<vector<string>> result;
void backtracking(string s, int startIndex){
if(startIndex == s.size()){
result.push_back(path);
return;
}
for(int i = startIndex; i < s.size(); i ++){
if(isPartiton(s, startIndex, i)){
//这是区分一般组合问题的关键,就是这里取的是字符串,用了substr方法
string str = s.substr(startIndex, i - startIndex + 1);
path.push_back(str);
}else{
continue;
}
backtracking(s, i + 1);
path.pop_back();
}
}
bool isPartiton(string str, int start, int end){
for(int i = start,j = end; i < j; i++,j--){
if(str[i] != str[j]){
return false;
}
}
return true;
}
public:
vector<vector<string>> partition(string s) {
backtracking(s, 0);
return result;
}
};
(八)复原IP地址
对输入进行切割,对每段进行合法性判断
vector<string> result;
void backtracking(s, startIndex, pointSum){
//pointSum为加上的逗点位置,这里是通过逗点的个数作为结束条件
if(pointSum == 3){
if(isvalid(s, startIndex, s.size() - 1){//左闭右闭
result.push_back(s);
return;
}
}
for(int i = startIndex; i < s.size(); i++){
if(isvalid(s, startIndex, i)){//左闭右闭区间,[startIndex, i]
//插入操作
s.insert(s.begin() + i + 1,'.');
pointSum ++;
//注意插入.后要从i+2开始
backtracking(s, i + 2, pointSum);
pointSum --;
s.erase(s.begin() + i + 1);
}
}
}
class Solution {
private:
vector<string> result;
void backtracking(string s, int startIndex, int pointSum){
if(pointSum == 3){
if(isValid(s, startIndex, s.size() - 1)){//左闭右闭
result.push_back(s);
return;
}
}
for(int i = startIndex; i < s.size(); i ++){
if(isValid(s, startIndex, i)){
s.insert(s.begin() + i + 1, '.');
pointSum ++;
backtracking(s, i + 2, pointSum);
pointSum --;
s.erase(s.begin() + i + 1);
}
}
}
bool isValid(string s, int startIndex, int endIndex){
if(startIndex > endIndex){
return false;
}
if(s[startIndex] == '0' && startIndex != endIndex){//只有'0'才合法,其他以0开头的非法
return false;
}
int num = 0;
for(int i = startIndex; i <= endIndex; i ++){
if(s[i]>'9' || s[i] < '0'){//非法
return false;
}
num = num*10 + (s[i] - '0');
if(num > 255){
return false;
}
}
return true;
}
public:
vector<string> restoreIpAddresses(string s) {
backtracking(s, 0, 0);
return result;
}
};
(九)子集
模板题
注意在add操作在终止判断前,因为所有节点都要加入结果集中
class Solution {
public:
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int>& nums, int startIndex){
result.push_back(path);//一开始加入的是空集
if(startIndex >= nums.size()){
return;
}
for(int i = startIndex; i < nums.size(); i ++){
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
};
(十)子集II
模板题,同层剪枝,注意去重要先剪枝
class Solution {
private:
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int>& nums, int startIndex){
result.push_back(path);
if(startIndex >= nums.size()){
return;
}
for(int i = startIndex; i < nums.size(); i++){
if(i > startIndex && nums[i] == nums[i - 1]){
continue;
}
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
//注意去重要先排序
sort(nums.begin(), nums.end());
backtracking(nums, 0);
return result;
}
};
(十一)递增子序列
注意这里面不可以排序,这样就改变了原数组的顺序,得到的递增子数列不是原数组的递增子序列
剪枝:树层去重,非递增树枝
unorderedset另一种去重方式
path 一维
result 二维
void backtracking(nums, startIndex){
//子集问题可以不写终止条件,因为for循环里面遍历完了自动终止
if(path.size() > 1){
result.push_back(result);//这里只收个大小2以上的自己
}
unorderedset<int> uset;//用set记录for循环里面已经去过的数,用来树层去重
for(int i = startIndex; i < nums.size(); i++){
// 1. path不为空,且当前数小于path最后一个数(不满足递增),剪枝
// 2. uset中已经有当前数(不满足集合中没有重复子集),单层去重剪枝
if(!path.empty() && nums[i] < path.back() || uset.find(nums[i]) != uset.end()){
continue; //注意这里不能直接break,因为后面还可能有符合的
}
//正常取元素的过程
uset.insert(nums[i]);
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
//这里uset只对当前递归有效,用于树层去重,进入下层递归后,uset重新定义了,又是一个新的集合,所以没必要回溯。和path没关系
}
}
(十二)全排列
处理排列问题就不用使用startIndex, 但排列问题需要一个used数组,标记一条路径上已经选择的元素
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void tranvsersal(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] = true;
tranvsersal(nums, used);
used[i] = false;
path.pop_back();
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(), false);
tranvsersal(nums, used);
return result;
}
};
(十三)全排列II
序列中可包含重复数字,需要同层去重(先排序,再去重)
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void transerval(vector<int>& nums, vector<bool>& used){
if(path.size() == nums.size()){
result.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++){
// 注意这里一定要加上used[i - 1] == false的限制,这说明是同层的重复,如果used[i - 1] == true,则是同一个树枝上的去重
if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false){
continue;
}
if(used[i] == false){
path.push_back(nums[i]);
used[i] = true;
transerval(nums, used);
used[i] = false;
path.pop_back();
}
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(), false);
sort(nums.begin(), nums.end());
transerval(nums, used);
return result;
}
};
(十四)重新安排行程
注意: 同一个行程可能有多张票!!!
unordered_map<string, multiset> targets:unordered_map<出发机场, 到达机场的集合> targets
unordered_map<string, map<string, int>> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets
这两个结构,我选择了后者,因为如果使用unordered_map<string, multiset<string>> targets
遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。
出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。
如果“航班次数”大于零,说明目的地还可以飞,如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。
private:
// unordered_map<出发机场,map<到达机场,航班次数>> targets
unordered_map<string, map<string, int>> targets;
// 返回值为bool,在找到第一个合适行程后,就可以返回
bool backtracking(int ticketNum, vector<string>& result){
if(result.size() == ticketNum + 1){
return true;
}
// 每轮都取result的最后一个作为开始机场,遍历其到达机场
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;
result.pop_back();
target.second ++;
}
}
return false;
}
public:
vector<string> findItinerary(vector<vector<string>>& tickets){
vector<string> result;
for(const vector<string>& vec: tickets){
targets[vec[0]][vec[1]] ++;
}
result.push_back("JFK");//起始机场
backtracking(tickets.size(), result);
return result;
}
(十五)N皇后
不能同行、同列,对角线
矩阵的高度为树的高度,矩阵的宽度为树的宽度
result 三维数组,包含多种合理的棋盘排布(二维)
// chessboard为棋盘,n表示棋盘大小,row为当前排到第几行
void backtracking(chessboard, n, row){
//终止条件
if(row == n){//遍历到最后一行
result.push_back(chessboard);
return;
}
for(int col = 0; col < n; col ++){
// 判断在col的位置放皇后是否合法
if(isValid(row, col, chessboard, n)){
chessboard[row][col] = 'Q';
backtracking(chessboard, n, row + 1);
chessboard[row][col] = '.';
}
}
}
class Solution {
private:
vector<vector<string>> result;
void backtracking(vector<string>& chessboard, int n, int row){
if(row == n){
result.push_back(chessboard);
return;
}
for(int col = 0; col < n; col ++){
if(isValid(chessboard, row, col, n)){
chessboard[row][col] = 'Q';
backtracking(chessboard, n, row + 1);
chessboard[row][col] = '.';
}
}
}
bool isValid(vector<string>& chessboard, int row, int col, int n){
for(int i = 0; i < row; i++){
if(chessboard[i][col] == 'Q'){
return false;
}
}
for(int i = 0; i < row; i++){
for(int j = 0; j < n; j++){
if(abs(i - row) == abs(j - col) && chessboard[i][j] == 'Q'){
return false;
}
}
}
return true;
}
public:
vector<vector<string>> solveNQueens(int n) {
//初始化vector<string>
vector<string> chessboard(n, std::string(n, '.'));
backtracking(chessboard, n, 0);
return result;
}
};
(十六)解数独
求一个解法即可
二维递归,每个位置都要枚举可能的数值,再向下递归
//求一个数组就可以了,因此如果是true,直接返回即可
bool backtracking(board){
//这里不需要终止条件,在下面的处理中包含了终止
for(int i = 0; i < board.size(); i ++)
for(int j = 0; j < board[0].size(); j ++){
if(board[i][j] == '.'){
for(char k = '1', k <= '9'; k ++){
if(isValid(i, j, k, board)){
board[i][j] = k;
bool result = backtracking(board);
if(result == true) return true;
board[i][j] = '.';
}
}
//如果9个数都尝试了都不行
return false;
}
}
// 每个空格都搜索了一遍,所有棋盘都填满,得到符合条件的解
return true;
}
class Solution {
private:
bool backtracking(vector<vector<char>>& board, int row){
//优化了一点,每次都从当前行开始排查为‘.’的格子,但是每行对应的列还是要从0开始排查
for(int i = row; i < 9; i ++){
for(int j = 0; j < 9; j ++){
if(board[i][j] == '.'){
for(char k = '1'; k <= '9'; k ++){
if(isValid(board, i, j, k)){
board[i][j] = k;
//注意传入的是当前行
if(backtracking(board, i)) return true;
board[i][j] = '.';
}
}
return false;
}
}
}
return true;
}
bool isValid(vector<vector<char>>& board, int row, int col, char k){
//横向
for(int i = 0; i < 9; i ++){
if(board[row][i] == k) return false;
}
//纵向
for(int i = 0; i < 9; i ++){
if(board[i][col] == k) return false;
}
// 方格
int startXIndex = (row/3)*3;
int startYIndex = (col/3)*3;
for(int i = startXIndex; i < startXIndex + 3; i ++){
for(int j = startYIndex; j < startYIndex + 3; j ++){
if(board[i][j] == k) return false;
}
}
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
backtracking(board, 0);
}
};