回溯 leetcode
因为所有dfs都可以看成树,所以每个dfs至少有一个参数(高度),并且终止的条件一般是(一个变量等于高度的时候)
回溯即在dfs下面加上一个pop()移除刚进去的元素即可,(vector deque)
需要去重吗?两种方法:排序后数组保存(需要pop) , 不能排序则使用哈希表(一般unordered set,map,数据小还可用数组开a[],弹药分清楚应不应该放在uset)去重(每层都初始化,只记录本层是否前面有没有又过不需要erase,因为进入下一层又会又新的uset)
是排序问题码? 如果是 每次循环从i=0 开始, 如果不是,是组合问题 从i=startindex开始吧
需要排序吗?
for循环起点从那里开始
是排列问题还是组合问题:排列问题必须是从0开是
可以重复使用吗?:不可以就是i+1 可以就是i
是不是每个节点都需要记录,还是只需要叶子节点 :关乎于递归结束的设置 是进入递归就直接记录 还是开始点大于等于数组长度就好
需要找到一个就直接返回吗 ? 若是,则dfs返回值用bool(解数独,),进入下一层使用if() 如果为真就返回, 若不是就返回错误!
组合数
class Solution {
private:
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
void backtracking(int n, int k, int startIndex) {//startindex,传递每个开始的地方一定要掌握!!!
if (path.size() == k) {//终点如果一开始选择了4,会因为长度不够没满而没有放进答案数组里**
result.push_back(path);
return;//记得及时返回,不然会往下走一直遍历的
}
for (int i = startIndex; i <= n; i++) {
path.push_back(i); // 处理节点 横向
backtracking(n, k, i + 1); // 递归从i+1开始保证不重复,i+1保证了横向遍历
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
vector<vector<int>> combine(int n, int k) {
result.clear(); // 可以不写
path.clear(); // 可以不写
backtracking(n, k, 1);
return result;
}
};
上面减枝
void backtracking(int n, int k, int startIndex) {
if (path.size() == k) {
result.push_back(path);
return;
}
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方 树宽-(高度-数组个数)+1
//n=4表示从1-4里面选,3;当size=0,即选了0个,那么最多从(3-0)=还需要的元素个数,(n-还需要的个数)等于还能选几个到里面的边界(范围),是肯定会出答案的,
//还需要的数越多,可以选择的范围也越大
//为什么是+1,当n=4,k=4,size=0的时候 循环条件为i<=0,什么进入不了循环
当size=4的时候,n-(k-4)+1,
path.push_back(i); // 处理节点
backtracking(n, k, i + 1);
path.pop_back(); // 回溯,撤销处理的节点
}
}
数组总和2 总和等于n dfs的参数 (必须有的树的高度 和宽度,还有 因为需要 记录遍历到的中间值sum 答案)
class Solution {
private:
vector<vector<int>> result; // 存放结果集
vector<int> path; // 符合条件的结果
// targetSum:目标和,也就是题目中的n。
// k:题目中要求k个数的集合。
// sum:已经收集的元素的总和,也就是path里元素的总和。
// startIndex:下一层for循环搜索的起始位置。
void backtracking(int targetSum, int k, int sum, int startIndex) {
if (path.size() == k) {//如果长度已经相等 ,必返回,但返回之前发现sum刚好等于目标值
if (sum == targetSum) result.push_back(path);//还需要放进去
return; // 如果path.size() == k 但sum != targetSum 直接返回
}
for (int i = startIndex; i <= 9; i++) {
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(n, k, 0, 1);
return result;
}
};
剪枝
if (sum > targetSum) { // 剪枝操作,数已经大于就不需要看下面的操作
return; // 如果path.size() == k 但sum != targetSum 直接返回
}
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return;
}
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝,数字从小到大找,如果数取了前面的后便就不用取了
sum += i; // 处理
path.push_back(i); // 处理
backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
sum -= i; // 回溯
path.pop_back(); // 回溯
}
}
电话号码的字母组合(每个数子选择都是不同元素的组合)
class Solution {
private:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};//定义对应的字符串,表示这个数组拥有的
public:
vector<string> result;
string s;
void backtracking(const string& digits, int index) {
if (index == digits.size()) {
result.push_back(s);//满了就放入结果
return;
}
int digit = digits[index] - '0'; // 将index指向的数字转为int,是哪个数字
string letters = letterMap[digit]; // 定义字符串赖存放取数字对应的字符集,
for (int i = 0; i < letters.size(); i++) {
s.push_back(letters[i]); // 处理
backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了
s.pop_back(); // 回溯
}
}
vector<string> letterCombinations(string digits) {
if (digits.size() == 0) {
return result;
}
backtracking(digits, 0);
return result;
}
};
组合总和(不同于上面的题,本题没有层数的限制)
如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window)
如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母
注意仅仅适用于组合问题
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {//需要传入题给数组,目标值,当前sum,初始点
if (sum > target) {//因为没有对层数限制,只有对和的限制,可以一直递归,递归终止的两个情况,sum>目标值或sum==目标值
return;
}
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size(); i++) {//从起点开始
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i); //对于可重复这点: 不用i+1了,表示可以重复读取当前的数
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
result.clear();
path.clear();
backtracking(candidates, target, 0, 0);
return result;
}
};
数组总和二
class Solution {
private:
vector<vector<int>> result;
vector<int> path;//
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used) {//多了一个数组,为什么是这几个参数,因为全局变量path和result是自己弄的,但是这个candidate和sum是题目给的,不是全局变量,需要传进去否则过不了,sum可以放出去
if (sum == target) {//
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {//&&的右边是进行了剪枝,因为数组已经排序了
你只能在答案数组的不同位置有同一个数,不能在同一个位置有不同的数字相同数,这样可能会导致和已经放入数组的答案重复!
即同一层相同的数证明是第一个分支的真子集
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) {
//i>0是因为i-可能会造成越界SEGV on unknow address
// 如果和前面一个数相等,而且used[i - 1] == true,说明同一树支candidates[i - 1]使用过,
也就是说,剪枝发生在:同一层数值相同的结点第 22、33 ... 个结点,因为数值相同的第 11 个结点已经搜索出了包含了这个数值的全部结果,同一层的其它结点,候选数的个数更少,搜索出的结果一定不会比第 11 个结点更多,并且是第 11 个结点的子集。
// used[i - 1] == false,说明左边的candidates[i - 1]使用过, 要对同一树层使用过的元素进行跳过
continue;
//左边一个树枝,使用了candidates[i - 1],只有当used为真才是上一层用过的,否则就是同一层左边的人用过的(因为前面的数必然相等且使用过使用的),
}
sum += candidates[i];
path.push_back(candidates[i]);
used[i] = true;//告诉下一层用了
backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次
used[i] = false;
sum -= candidates[i];
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(), false);//与上面的题目不同,保存一个used数组用于记录用过的数字的下标,达到去重
// 首先把给candidates排序,让其相同的元素都挨在一起。
sort(candidates.begin(), candidates.end());
backtracking(candidates, target, 0, 0, used);
return result;
}
};
使用过分两个维度,一个是横着的,一个是竖着的
分割回文字符串
vector<vector<string>> res;
vector<string> v;
void dfs(string &s,int startval){
if(startval>=s.size()){//进行分割的分割点大于等于要分割的长度说明没得好割了
res.push_back(v);
return ;
}
for(int i=startval;i<s.size();i++){//
if(is(s,startval,i)){//s,0,0表示一个字符字符
string sub=s.substr(startval,i-startval+1);//函数表示s.substr(pos,len)截取的是string,包含s中从pos开始的len个字符的拷贝
v.push_back(sub);
dfs(s,i+1);
v.pop_back();
}else continue ;
}
}
bool is(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;
}
vector<vector<string>> partition(string s) {
if(!s.size()) return res;
dfs(s,0);
return res;
}
复原ip地址 传入(字符串,开始的位置,逗号数量(终止条件)),结果是在原来的字符串上进行修改
startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。
本题我们还需要一个变量pointNum,记录添加逗点的数量。
只能分四段,所以递归终止条件是 逗号数量=3的时候,而且如果第四段区间的合法,那么还将其放入结果集中
class Solution {
private:
vector<string> result;// 记录结果
// startIndex: 搜索的起始位置,pointNum:添加逗点的数量
void backtracking(string& s, int startIndex, int pointNum) {
if (pointNum == 3) { // 逗点数量为3时,分隔结束
if (isValid(s, startIndex, s.size() - 1)) {//判断第四段子字符串是否合法,如果合法就放进result中
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 , '.'); // insert(iterator pos,s)在pos位置的前面一个位置插入一个字符串,这里是在i(i一直是从startindex开始)的后面插入一个逗点,表示处理了
pointNum++;
backtracking(s, i + 2, pointNum); // 将插入逗点之后的串传入,另外下一个子串的起始位置为i+2逗号加1了,但是i还是原来的i,指向那个逗号前面
pointNum--; // 回溯
s.erase(s.begin() + i + 1); // erase(pos,n)删除从pos开始的后面的n个字符erase(iterator pos)删除迭代器指向的地方的东西 这里是回溯删掉逗点
} else break ; // 因为是从左向右截断的,一般都会合法的,当出现不合法时,说明后面的怎么截断都会失败,直接结束本层循环,这里写break是为了更好的理解循环,其实可以写成return
}
}
// 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法
bool isValid(const string& s, int start, int end) {
if (start > end) {
return false;
}
if (s[start] == '0' && start != end) { //弱国这个区间不是只有一个字符那么 那么第一位为0即0开头的数字不合法
return false;
}
int num = 0;
for (int i = start; i <= end; i++) {
if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法 这里包括了数字是负数的情况
return false;
}
num = num * 10 + (s[i] - '0');
if (num > 255) { // 如果大于255了不合法
return false;
}
}
return true;
}
public:
vector<string> restoreIpAddresses(string s) {
result.clear();
if (s.size() > 12) return result; //, 算是剪枝了
backtracking(s, 0, 0);//传入字符串,开始位置,逗号点数
return result;
}
};
子集
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {//只需要传入开始的点
result.push_back(path); // 收集子集,与上面不同,因为要收集的是每个树结点,而不是叶子结点,每次进入的时候就可以放入结果,先收集下面再判断,因为上一层没有搜集
if (startIndex >= nums.size()) { // 加入长度为1,那么开始的位置需要是0,如果也为1那么搜索应该是停止的,终止条件可以不加,
return;//当开始的位置大于原来数组的长度就可以返回,当作搜完
}
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracking(nums, i + 1);//i+1下一层,
path.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
};
自己二
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) {
result.push_back(path);
for (int i = startIndex; i < nums.size(); i++) {
// used[i - 1] == true,说明同一树支candidates[i - 1]使用过
// used[i - 1] == false,说明同一树层candidates[i - 1]使用过
// 而我们要对同一树层使用过的元素进行跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
path.push_back(nums[i]);
used[i] = true;
backtracking(nums, i + 1, used);
used[i] = false;
path.pop_back();
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
result.clear();
path.clear();
vector<bool> used(nums.size(), false);
sort(nums.begin(), nums.end()); // 去重需要排序
backtracking(nums, 0, used);
return result;
}
递增子序列 去重,子集,不能重复使用
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
if (path.size() > 1) {
result.push_back(path);
// 注意这里不要加return,要取树上的节点
}
unordered_set<int> uset; // 使用set对本层元素进行去重,如果放在全局变量那么每次都会insert递归先增加一个记录满足不了,知道搜到尽头才擦去这个记录
for (int i = startIndex; i < nums.size(); i++) {
if ((!path.empty() && nums[i] < path.back())//使用path.back()前必须使用.empty判断非空。
|| uset.find(nums[i]) != uset.end()) {//如果路不为空而且将要放入的元素小于路径中的最后一个元素,或者找到这个元素,都说明同一层用过
continue;
}
uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了,插入这个元素
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
};
上面的优化版本使用数组左哈希表
void backtracking(vector<int>& nums, int startIndex) {
if (path.size() > 1) {
result.push_back(path);
}
int used[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100]开201,这里面还有个0
for (int i = startIndex; i < nums.size(); i++) {
if ((!path.empty() && nums[i] < path.back())
|| used[nums[i] + 100] == 1) {//num[i+100]去除反方的选手
continue;
}
used[nums[i] + 100] = 1; // 记录这个元素在本层用过了,本层后面不能再用了
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
全排列 :叶子节点记录, 一个元素在一个组合只能出现一次需要进行used去重,需不要要在递归过程中放置变量i
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] == true) continue; // path里已经收录的元素,直接跳过
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(), false);//创建了一个数组后,直接括号包起来就行了,(元素个数,放置的值)
backtracking(nums, used);
return result;
}
重复数组全排列二
vector<vector<int>> result;
vector<int> path;
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(), false);//创建了一个数组后,直接括号包起来就行了,(元素个数,放置的值)
sort(nums.begin(),nums.end());
backtracking(nums, used);
return result;
}
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] == true||i>0&&nums[i]==nums[i-1]&&!used[i-1]) continue; // path里已经收录的元素,直接跳过
//比上面多了一个情况 当值相等而且前面一个数字已经用过的时候 也会跳过
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
重新安排路程
一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。
使用unordered_map<string, multiset
intunordered_map<string, map<string, int>> targets只要int存在就说明可飞,保留了迭代器顺便还等效于删除了操作
如果“航班次数”大于零,说明目的地还可以飞,如果如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。
相当于说我不删,我就做一个标记!
五张票六个地点
class Solution {
private:
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;//使用一个哈希图存放题目给的数据,使用map让终点在放进去就自动排序了
bool backtracking(int ticketNum, vector<string>& result) {//返回值的意思是找到了就不用找了,dfs参数是还有票数,答案数组(一定要是传引入)
if (result.size() == ticketNum + 1) {
return true;
}
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {//以result最后一个点为起点,
//迭代targets[结果集的最后一个]获得以这个点为起点的所有机票,必须是&传引用这样后面才能操作,因为key值不能修改,所以还需要再string前面加const
if (target.second > 0 ) { // 如果能飞才飞,不能飞的target,此时target表示目的地
result.push_back(target.first);//能飞放入终点
target.second--;
if (backtracking(ticketNum, result)) return true;//bool唯一作用当找到了 就直接返回,因为没有准备专门存放答案的字符串数组,当然你可以准备一个字符串数组
result.pop_back();
target.second++;
}
}
return false;
}
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {//这里一定要是
targets.clear();
vector<string> result;//结果是一个string型 的数组
for (const vector<string>& vec : tickets) {//这里的for,让vec等于vector里每个元素即["JFK","SFO"]
//unordered_map<string, map<string, int>> targets;//使用一个哈希图存放题目给的数据
targets[vec[0]][vec[1]]++; // 记录映射关系,vec[0]是起飞点字符串,vec[1]是终点字符串
//逻辑targets[key]=value即 map<string,int> targets[key][key]=value即 int
}
result.push_back("JFK"); // 起始机场
backtracking(tickets.size(), result);//因为ticket+1数量等于结果的长度
return result;
}
};
使用void 的dfs,这么做会导致超时!!!没有找到就及时返回!!!所以还是需要bool
class Solution {
private:
vector<string> res;
// unordered_map<出发机场, map<到达机场, 航班次数>> targets
unordered_map<string, map<string, int>> targets;
void backtracking(int ticketNum, vector<string>& result) {//传引用直接操作地址,否则会逐个复制,效率太低
if (result.size() == ticketNum + 1) {
if(!res.size())//只有第一次的时候记录
res=result;cout<<"找到结果"<<"";
return ;
}
for (pair<const string, int>& target : targets[result[result.size() - 1]]) {
if (target.second > 0 ) { // 记录到达机场是否飞过了
result.push_back(target.first);cout<<"放入"<<target.first<<" ";
target.second--;
backtracking(ticketNum, result);
result.pop_back();cout<<"取出"<<target.first<<" ";
target.second++;
}
}
return ;
}
};
n皇后
class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到棋牌的第几行了
void backtracking(int n, int row, vector<string>& chessboard) {//参数 棋盘还有本层是第几行,总共n行(n皇后),为了递归终止的条件
if (row == n) {
result.push_back(chessboard);//到了最后一个就是答案啊了
return;
}
for (int col = 0; col < n; col++) {//从0开始
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) {参数 第几行第几列,棋盘,n皇后
int count = 0;
// 检查列
for (int i = 0; i < row; i++) { // 这是一个剪枝
if (chessboard[i][col] == 'Q') {检查本行上面每一行的本列元素是不是q这个值
return false;
}
}
// 检查 45度角是否有皇后
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {//这里的条件一定要写成是&&且否则数组越界
if (chessboard[i][j] == 'Q') {//检查本行本列左上角的元素是不是放置了皇后
return false;
}
}
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {//检查本行本列右上角是不是放置了皇后,这里条件一定要写成是且&&
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
public:
vector<vector<string>> solveNQueens(int n) {
result.clear();
std::vector<std::string> chessboard(n, std::string(n, '.'));//新建一个棋盘全部是填号了点的
backtracking(n, 0, chessboard);//从0开始
return result;
}
};
解数独 二维递归 两个for循环里放着一个递归
class Solution {
private:
bool backtracking(vector<vector<char>>& board) {
for (int i = 0; i < board.size(); i++) { // 遍历行
for (int j = 0; j < board[0].size(); j++) { // 遍历列
if (board[i][j] != '.') continue;//不是点是数字才需要填充,
for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适
if (isValid(i, j, k, board)) {
board[i][j] = k; // 放置k
if (backtracking(board)) return true; // 如果找到合适一组立刻返回
board[i][j] = '.'; // 回溯,撤销k
}
}
return false; // 9个数都试完了,都不行,那么就返回false
}
}
return true; // 遍历完没有返回false,说明找到了合适棋盘位置了,一i的那个是这个
}
bool isValid(int row, int col, char val, vector<vector<char>>& board) {
for (int i = 0; i < 9; i++) { // 判断行里是否重复,
if (board[row][i] == val) {//不要使用i-1
return false;
}
}
for (int j = 0; j < 9; j++) { // 判断列里是否重复
if (board[j][col] == val) {
return false;
}
}
int startRow = (row / 3) * 3;//这样无论是不是都可以找到所在九宫格的位置第一个格子的坐标
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复
for (int j = startCol; j < startCol + 3; j++) {
if (board[i][j] == val ) {
return false;
}
}
}
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};