博客首页:热爱编程的大李子
专栏首页:LeetCode刷题
博主在学习阶段,如若发现问题,请告知,非常感谢
同时也非常感谢各位小伙伴们的支持
每日一语:I walk slowly, but I never walk backwards.
感谢: 我只是站在巨人们的肩膀上整理本篇文章,感谢走在前路的大佬们!
最后,祝大家每天进步亿点点! 欢迎大家点赞➕收藏⭐️➕评论支持博主!
⭐️ ⭐️上篇文章-<冲刺大厂之算法刷题>栈和队列 ⭐️ ⭐️
本章回溯算法可以解决的问题如下:
回溯法的模板:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
回溯法三部曲
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); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
path.pop_back(); // 回溯,撤销处理的节点
}
代码优化
可以剪枝的地方在递归中每一层的for循环条件处。
如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
i <= n-(k-path.size())+1
vector<vector<int>> result;
vector<int> path;
void backtracking(int n,int k,int startIndex) {
//回溯结束条件
if(path.size()==k) {
result.push_back(path);
return;
}
//剪枝优化1
// if(path.size()+n-startIndex+1 < k) {
// return;
// }
// for(int i = startIndex;i<= n-(k-path.size())+1;i++){//剪枝优化2
for(int i = startIndex; i<= n; i++) { //
path.push_back(i);
backtracking(n,k,i+1);//递归+
path.pop_back();//回溯
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(n,k,1);
return result;
}
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
本题相对于上个题只是多个 之和为n
的限制,其他的处理思路和上题类似
#include
using namespace std;
vector path;
vector> result;
void backtracking(int n,int k,int startIndex,int sum) {
if(path.size()==k){
if(sum == n){
result.push_back(path);
}
return;
}
for(int i = startIndex;i <= 9 - (k-path.size())+1;i++){
path.push_back(i);
backtracking(n,k,i+1,sum+i);
path.pop_back();
}
}
vector> combinationSum3(int k, int n) {
backtracking(n,k,1,0);
return result;
}
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = ""
输出:[]
示例 3:
输入:digits = "2"
输出:["a","b","c"]
提示:
0 <= digits.length <= 4
digits[i] 是范围 [‘2’, ‘9’] 的一个数字。
输入:“23”,抽象为树形结构,如图所示:
图中可以看出遍历的深度就是输入的 字符串的长度,而叶子节点就是我们要收集的结果,宽度为 字符所对应的字符串长度 ,如2==>abc
回溯三部曲
#include
using namespace std;
//LeetCode17:https://leetcode.cn/problems/letter-combinations-of-a-phone-number
//本题属于组合问题的 “多个集合的组合”
string path;
vector<string> result;
string arr[10] = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
void backtracking(string& digits,int startIndex) {
//结束条件
if(startIndex==digits.size()) {
result.push_back(path);
return;
}
int num = digits[startIndex] - '0';//要遍历的字符串数组下标
for(char ch : arr[num]) { //横向for循环
path.push_back(ch);
backtracking(digits,startIndex+1);//纵向递归
path.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if(digits=="") {//特殊情况判断
return result;
}
backtracking(digits,0);
return result;
}
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例 1:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:
输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:
输入: candidates = [2], target = 1
输出: []
#include<iostream>
using namespace std;
//本题同LeetCode77只是结束条件变化了,其他思路都一致
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int>& candidates, int target,int startIndex,int sum){
if(sum > target){
return;
}else if(sum == target){
result.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size(); i++){
path.push_back(candidates[i]);
backtracking(candidates,target,i,sum+candidates[i]);
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
backtracking(candidates,target,0,0);
return result;
}
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]
本题要是仅凭读题感觉好像比上一题还简单,但是看过示例之后就会发现,是我太天真了! 主要有如下区别:
本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合。
很容易想到的思路:把所有组合求出来,再用set或者map去重,但这么做很容易超时!
组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。 (同一树枝:也就是组合中数的维度,组合中的数可以有重复的. 同一树层:数组中数循环的维度,由于数组有重复数,所以上次可以循环到这个数,下次循环也可以到这个数,但是这俩数是在同一for循环内的.)
那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢?
回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。
去重之前,我们需要对数进行排序.(为什么排序,大家可以思考一下)
树形结构如图所示
used数组存储的是本层数的使用情况, 用于判断是否是同一个树层
⭐️⭐️⭐️回溯三部曲
=
时,还要将其加入到结果集中(由于后序会有剪枝操作,所以sum>targetSum可以省略)⭐️⭐️⭐️本题核心就是去重: candidates[i] == candidates[i - 1]
代码剪枝:本题剪枝方案和上题类似
#include
using namespace std;
vector<int> path;
vector<vector<int>> result;
void backtracking(vector<int>& candidates, int target,int startIndex,int sum,vector<bool>& used) {
if(sum > target) {
return;
} else if(sum == target) {
result.push_back(path);
return;
}
for(int i = startIndex; i < candidates.size(); i++) {
if(i >=1 && candidates[i]==candidates[i-1] && used[i-1]==false) { //数层去重
continue;
}
used[i] = true;
path.push_back(candidates[i]);
backtracking(candidates,target,i+1,sum+candidates[i]);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<bool> used(candidates.size(),false);
sort(candidates.begin(),candidates.end()) ;//排序
backtracking(candidates,target,0,0,used);
return result;
}
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
本题这涉及到两个关键问题:
切割问题类似组合问题
对于字符串abcdef:
组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中在选组第三个…。
切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段…。
⭐️⭐️⭐️ 注意: 组合的index是选取的组合数的下标位置.而切割的index是切割位置数的index,但是实际位置却是index+1,所以下一次传入的将是index+1.
树形结构图
如图所示 index=0,切割位置1,下一轮传入的也是1,当所有的切割完毕,下一次传入的是 arr.size().这个将作为递归的结束条件
>=
s的大小,说明已经找到了一组分割方案了,结束递归即可[startIndex,i]
是不是回文,如果是回文,就加入在vector path
中,然后递归,完毕后回溯. 如果不是则 i++
判断下一个…回文串的判断我们可以使用双指针法或者反转字符串进行比较的方法
#include
using namespace std;
vector<string> path;
vector<vector<string>> result;
bool isHuiWen(string& str,int i,int j){//判断是否是回文串
for(;i<j;i++,j--) {
if(str[i]!=str[j]){
return false;
}
}
return true;
}
void backtracking(string& s,int startIndex){
//结束条件
if(startIndex>=s.size()){
result.push_back(path);
return;
}
for(int i = startIndex; i < s.size();i++){
if(isHuiWen(s,startIndex,i)){
path.push_back(s.substr(startIndex,i-startIndex+1));
}else{
continue;
}
backtracking(s,i+1);
path.pop_back();
}
}
vector<vector<string>> partition(string s) {
backtracking(s,0);
return result;
}
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “[email protected]” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
示例 2:
输入:s = "0000"
输出:["0.0.0.0"]
示例 3:
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
这个题和上文的分割回文串很相似,先截取,再判断,符合则进行继续递归
⭐️⭐️⭐️ 回溯三部曲
.
的数量pointNum.
做分割,但是也要判断最后一段是否是合格串.
然后递归,回溯,进行下一轮循环. 如果不是,则直接 i++
…合格串需要满足的条件
#include
using namespace std;
vector<string> result;
//本题和上题非常相似,只是判断是否是IP这个过程复杂了些
void backtracking(string& s,int startIndex,int pointNum) {
if(pointNum == 3) {
if(isValid(s,startIndex,s.size()-1)) {
result.push_back(s);
}
return;
}
for(int i = startIndex; i < s.size(); i++) {//i子串结束位置的索引 [startIndex,i]
if(isValid(s,startIndex,i)) {
s.insert(s.begin()+i+1,'.');
backtracking(s,i+2,pointNum+1);
s.erase(s.begin()+i+1);
} else {
continue;
}
}
}
bool isValid(string& s,int begin,int end) {
if(begin > end) { //空字符
return false;
}
if(s[begin]=='0'&&begin != end) { //1.是否以‘0’开头
return false;
}
int num = 0;
for(int i = begin; i<=end; i++) {
if(s[i] < '0' || s[i]>'9') { //3.是否不是数字
return false;
}
num = num*10 + s[i]-'0' ;//注意不能写成 num *=10 + s[i]-'0'
if(num > 255) { //2.数字大小是否 》=0 && 《=255
return false;
}
}
return true;
}
vector<string> restoreIpAddresses(string s) {
backtracking(s,0,0);
return result;
}
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!
以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下:
回溯三部曲
递归参数:需要遍历的数组 vector
,每层遍历的起始下标startIndex
.
返回值:由于定义全局变量来保存最终结果集以及单一结果,所以并不需要返回值.
由于这次是寻找所有的组合,所以并不需要结束条件.没递归一层就把对应的结果放入到结果集中.
将当前组合数加入单一结果集,递归,回来后进行回溯.进行下一次循环.
#include
using namespace std;
vector<int> path;
vector<vector<int>> result;
void backtracing(vector<int>& nums,int startIndex){
result.push_back(path);
for(int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracing(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backtracing(nums,0);
return result;
}
给你一个整数数组 nums
,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
这个题的数组nums
可能包含重复元素,则需要对树层进行去重. 目前我们有三种去重方法:利用vector
, 利用startIndex
, 利用ordered_set
,下面我们将逐一进行演示.
用示例中的[1, 2, 2] 来举例,如图所示: (注意去重需要先对集合排序)
本题不再分析递归三部曲了,关键部分将添加相关注释.
#include
using namespace std;
//https://leetcode.cn/problems/subsets-ii
vector<int> path;
vector<vector<int>> result;
//其实相比上题只是多了个同层去重的步骤
void backtracing(vector<int>& nums,int startIndex,vector<bool>& used){
result.push_back(path);
for(int i = startIndex; i < nums.size(); i++){
if(i>=1 && used[i-1]== false && nums[i-1]==nums[i]){//树层去重
continue;
}
used[i] = true;
path.push_back(nums[i]);
backtracing(nums,i+1,used);
used[i] = false;
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(),0);
sort(nums.begin(),nums.end());
backtracing(nums,0,used);
return result;
}
给你一个整数数组 nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
示例 2:
输入:nums = [4,4,3,2,1]
输出:[[4,4]]
本题的一个坑: 不能对nums进行排序
, 不然就和示例对不上了.
以[4, 7, 6, 7]这个数组为例,抽象为树形结构如图:
递归三部曲:
递归参数:虽然求的是序列,有顺序,但是因为元素不可以重复,并且有一定的顺序(从前往后的方向),所以需要startIndex
. 当然也需要递归的数组: vector
返回值: 因为定义了全局变量,所以并不需要
当元素个数 path.size() >= 2
,可将临时组合存入结果集中.
由于数组中有重复元素,所以需要进行去重.而且是无序的,所以采用 ordered_set
. 另外序列也需要递增
每次循环判断当前组合数是否本层重复以及递增,如果是,则放入临时组合集=> uset更新=>递归=>回溯=>循环下一个数. 如果不是,则进入跳过当前的数,进行下一个循环.
备注:因为 -100 <= nums[i] <= 100
, 数据范围较小所以我们可以使用 int used[201]
去重.这个相比ordered_set效率更高.
#include
using namespace std;
vector<int> path;
vector<vector<int>> result;
//https://leetcode.cn/problems/increasing-subsequences
//void backtracing(vector& nums,int startIndex) {
// if(path.size() > 1 ) {//回溯结束条件
// result.push_back(path);
// }
// unordered_set uset;
// for(int i = startIndex; i < nums.size(); i++) {
// if((!path.empty() && path.back() > nums[i]) || uset.find(nums[i]) != uset.end() ) {//如果不是递增,或者元素同层已经使用过了
// continue;
// }
// uset.insert(nums[i]);
// path.push_back(nums[i]);
// backtracing(nums,i+1);//递归
// path.pop_back();//回溯
// }
//}
//由于-100 <= nums[i] <= 100,所以可以使用 数组来做hash表
void backtracing(vector<int>& nums,int startIndex) {
if(path.size() > 1 ) {//回溯结束条件
result.push_back(path);
}
int used[201] = {0};// -100-100 0-200
for(int i = startIndex; i < nums.size(); i++) {
if((!path.empty() && path.back() > nums[i]) || used[nums[i]+100] == 1 ) {//如果不是递增,或者元素同层已经使用过了
continue;
}
used[nums[i]+100] = 1;
path.push_back(nums[i]);
backtracing(nums,i+1);//递归
path.pop_back();//回溯
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtracing(nums,0);//这个题不能进行排序..
return result;
}
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
排列里面的数字有一定顺序性,而组合与顺序无关.这决定了startIndex的起始位置.
以[1,2,3]为例,抽象成树形结构如下:
回溯三部曲
递归参数:递归的数组vector
,用于标记元素的数组 vector
递归返回值:定义全局变量,无需返回值.
全排列要求排列后的数据和数组里面的数据个数相同. 所以结束条件: path.size()==nums.size()
判断当前循环的数是否被使用过,没被使用=>放入临时全排列集=>改变used=>递归=>回溯=>循环下一个数
注意:used数组常用于组合中树层/树枝的去重(常伴随着元素先排序), 也常用于排列中数据的标记. ordered_set常用于进行树层去重.
#include
using namespace std;
vector<int> path;
vector<vector<int>> result;
void backtracing(vector<int> &nums,vector<int>& used) {
if(path.size()==nums.size()) {//结束条件
result.push_back(path);
return;
}
for(int i = 0 ; i < nums.size(); i++) {
if(used[i] == 1) {//如果已经用过了,则不再使用
continue;
}
path.push_back(nums[i]);
used[i] = 1;//做标记
backtracing(nums,used);//递归
used[i] = 0;//回溯
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
//因为backtracing不需要同层去重,所以不需要对nums进行排序
vector<int> used(nums.size(),0);//这里没必要使用hash表,直接使用一个数组标记即可。
backtracing(nums,used);
return result;
}
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
这道题目和 46.全排列 的区别在与给定一个可包含重复数字的序列,要返回所有不重复的全排列,牵涉到树层的去重. 我们可以采取 vector
, 利用ordered_set
进行去重.
我以示例中的 [1,1,2]为例 ,抽象为一棵树,去重过程如图:
#include
using namespace std;
vector<int> path;
vector<vector<int>> result;
void backtracing(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>=1 && nums[i] == nums[i-1] && used[i-1]==false) { //同层去重
continue;
}
if(used[i]==false) { //如果当前元素没有被使用
path.push_back(nums[i]);
used[i] = true;
backtracing(nums,used);
used[i] = false;
path.pop_back();
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(),0);
sort(nums.begin(),nums.end());//因为要同层去重,所以要排序
backtracing(nums,used) ;
return result;
}
给你一份航线列表 tickets
,其中 tickets[i] = [fromi, toi]
表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK
(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK
开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
["JFK", "LGA"]
与 ["JFK", "LGB"]
相比就更小,排序更靠前。假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
示例 1:
输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
输出:["JFK","MUC","LHR","SFO","SJC"]
示例 2:
输入:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。
这道题目的几个难点:
1、如何理解死循环
从例子可以看出 ,出发机场和到达机场也会重复的,如果在解题的过程中没有对集合元素处理好,就会死循环
2、该记录映射关系
有多种解法,字母序靠前排在前面,如何该记录映射关系呢 ?
一个机场映射多个机场,机场之间要靠字母序排列。一个机场映射多个机场,可以使用std::unordered_map
,如果让多个机场之间再有顺序的话,就是用std::map
或者std::multimap
或者 std::multiset
。
这样存放映射关系可以定义为 unordered_map
或者 unordered_map
。
含义如下:
这两个结构,我选择了后者,因为如果使用unordered_map
遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。
再说一下为什么一定要增删元素呢,正如前面图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。
所以搜索的过程中就是要不断的删multiset里的元素,那么推荐使用unordered_map
。
在遍历 unordered_map<出发机场, map<到达机场, 航班次数>> targets
的过程中,可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。
如果“航班次数”大于零,说明目的地还可以飞,如果如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。
相当于说我不删,我就做一个标记!
以 [[“JFK”, “KUL”], [“JFK”, “NRT”], [“NRT”, “JFK”]为例,抽象为树形结构如下:
回溯三部曲
递归参数:航班的映射关系 unordered_map
我们定义成全局变量(控制参数的个数), 还需要ticketNum表示一共有多少个航班(用于结束条件),还需要一个vector
记录结果.
返回值:我们需要找到一个航程,就是在树形结构中找一条通往叶子节点的路线.所以我们只需要找到就直接返回就可以了,也不用继续往下寻找了.
**备注:**本回溯函数中的参数都可作为全局变量.
根据观察得知,如果最终的行程里的机场个数要比航线列表中的航班数量+1.我们就找到了一个形成,就把所有的航班串到一起了,递归结束.
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;
result.pop_back();
target.second++;
}
}
#include
using namespace std;
// unordered_map<出发机场,map<到达机场,航班次数>>
unordered_map<string,map<string,int>> targets;
bool backtracking(int ticketNum,vector<string>& result) {
if(result.size() == ticketNum+1) { //结束条件
return true;
}
for(pair<const string,int> &target : targets[result[result.size()-1]]) {//以结束点为起始点开往新的航线。 注意:target必须设定为引用,因为要修改targets的属性值
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) {
targets.clear();
vector<string> result;
for(vector<string> vec : tickets) {//vec既可以用引用也可以再复制一份
targets[vec[0]][vec[1]]++;//记录映射关系
}
result.push_back("JFK") ;
backtracking(tickets.size(),result);
return result;
}
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
示例 1:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出:[["Q"]]
皇后们的约束条件:
确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。
下面用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:
可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
回溯三部曲
参数:当前进行到棋盘的行数row,棋盘规模n,以及整个棋盘vector
返回值:当我们把所有的棋子摆完,也就是把棋盘遍历一遍,则就结束,把棋盘加入结果集即可.因为结果有多个,所以并不需要返回值.
当进行的row和棋盘的规模相等,则递归结束
判断当前位置是否可以放置,如果可以则放棋子.然后递归进入下一行.递归回来进行回溯尝试其他情况.
#include
using namespace std;
vector<vector<string>> result;
void backtracing(int row,vector<string>& checkerboard,int n){
if(row == n){//结束条件
result.push_back(checkerboard) ;
return;
}
for(int col = 0;col < n;col++) {
if(isValid(row,col,checkerboard,n)){
checkerboard[row][col] = 'Q';
backtracing(row+1,checkerboard,n);
checkerboard[row][col] = '.';
}
}
}
bool isValid(int row,int col,vector<string>& checkerboard,int n){
//是否在同一列
for(int i = 0;i < row;i++) {
if(checkerboard[i][col] == 'Q'){
return false;
}
}
//是否在对角线上
for(int i = row-1,j = col-1; i >=0 && j>=0; i--,j--){
if(checkerboard[i][j] == 'Q'){
return false;
}
}
//是否在斜对角线上
for(int i = row-1,j = col+1; i >=0 && j <n;i--,j++) {
if(checkerboard[i][j] == 'Q'){
return false;
}
}
return true;
}
vector<vector<string>> solveNQueens(int n) {
string s(n,'.');
vector<string> path(n,s);
backtracing(0,path,n);
return result;
}
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
1-9
在每一行只能出现一次。1-9
在每一列只能出现一次。1-9
在每一个以粗实线分隔的 3x3
宫内只能出现一次。(请参考示例图)数独部分空格内已填入了数字,空白格用 '.'
表示。
示例:
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:[["5","3","4","6","7","8","9","1","2"],["6","7","2","1","9","5","3","4","8"],["1","9","8","3","4","2","5","6","7"],["8","5","9","7","6","1","4","2","3"],["4","2","6","8","5","3","7","9","1"],["7","1","3","9","2","4","8","5","6"],["9","6","1","5","3","7","2","8","4"],["2","8","7","4","1","9","6","3","5"],["3","4","5","2","8","6","1","7","9"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
N皇后问题 是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来来遍历列,然后一行一列确定皇后的唯一位置。
本题就不一样了,本题中棋盘的每一个位置都要放一个数字,并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。
因为这个树形结构太大了,我抽取一部分,如图所示:
回溯三部曲
参数:由于数独的行列已知,所以参数只需要传入九宫格vector
即可.
返回值:由于答案只有一种,找到了就直接返回,没找到就尝试下一种情况,所以返回值是bool类型.
递归的话只要把数字填满就返回true,然后递归逐渐跳出并结束.
由于每一行需要填充多个数,也需要填充多个列,所以需要双层循环.如果当前位置需要填充,就先判断要填充的数是否合法(同行,同列,九宫格都不重复),如果合法就尝试下一个位置,不合法就尝试下一个数字.
#include
using namespace std;
bool backtracing(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++ ) {
if(isValid(i,j,k,board)) {
board[i][j] = k;
if(backtracing(board)) {//如果找到了方案,则立即返回
return true;
}
board[i][j] = '.' ;
}
}
return false;
}
}
return true;
}
bool isValid(int row,int col,char k,vector<vector<char>>& board) {
//同行
for(int j = 0; j < board[0].size(); j++) {
if(board[row][j]==k) {
return false;
}
}
//同列
for(int i = 0; i < board.size(); i++) {
if(board[i][col]==k) {
return false;
}
}
//同一个九宫格
int startX = ( row / 3) * 3;
int startY = (col / 3) * 3;
for(int i = startX ; i < startX+3; i++) {
for(int j = startY; j<startY+3; j++) {
if(board[i][j]==k) {
return false;
}
}
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracing(board);
}