递归
游戏开发可能使用栈结构,编程语言的一些功能实现也会使用栈结构,实现函数递归调用就需要栈,但不是每种编程语言都支持递归,例如:
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
相信大家应该遇到过一种错误就是栈溢出,系统输出的异常是
Segmentation fault
(当然不是所有的Segmentation fault
都是栈溢出导致的) ,如果你使用了递归,就要想一想是不是无限递归了,那么系统调用栈就会溢出。
回溯
递归是一种算法结构,形式上表现为直接或间接的自己调用自己;
回溯是一种算法思想,它是用递归实现的。
回溯法也可以叫做回溯搜索法,是一种搜索的方式。
回溯是递归的副产品,有递归就会有回溯。
回溯函数即递归函数,指的都是同个函数。
backtracking中的
for循环的次数就是横向遍历,就是一个根节点的子节点数量,就是样本集合的size(可供选择的样本数量);
递归的次数就是纵向遍历,就是需要取的次数,就是要取多少次样(树的深度)。
回溯代码模板:
void backtracking(参数) {
if (剪枝条件) return;//剪枝
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
存放结果组合的vector result
单个组合结果 path
void backtracking(参数如候选列表的集合) {
if (剪枝条件) return;//剪枝
if (终止条件) {
result.push_back(path);//存放结果;
return;
}
for (选择:本层集合中元素即候选列表(树中节点孩子的数量就是集合的大小)) {
处理节点;(做选择)
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果(撤销选择)
}
}
回溯,撤销处理结果(撤销选择):(详细解释)
如图所示,
路径2 已经走到了 目的地节点6,
那么 路径2 是如何撤销,然后改为 路径3呢?
这就是 回溯的过程,撤销路径2,走换下一个方向。
组合问题,子集问题,排列问题 会出现的去重的情况:
元素在组合内可重复,但组合间不可有重复——树层去重(同一树层不可重复使用)
元素在组合内不可出现重复——树枝去重(同一树枝不可重复使用)
(ps:树层去重需对数组排序)
组合内可出现重复元素,但不能有相同的组合==》数层去重==》需要对数组排序!
在candidates[i] == candidates[i - 1]相同的情况下:
- used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
- used[i - 1] == false,说明同一树层candidates[i - 1]使用过
为什么 used[i - 1] == false 就是同一树层呢?
横向遍历for每次结束都会重置为false==》false代表同一数层使用过。
纵向遍历回溯每次结束都没用走到重置为false的位置==》一直是true==》true代表同一树枝使用过。
以下同理:
同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。
而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上。
如图所示:
1 、组合问题
N个数里面按一定规则找出k个数的集合
一个集合中求组合:需要 startIndex
多个集合中求组合:无需
需要 startIndex 时:
递归调用时传入参数startIndex为i + 1 ,说明 集合中每个数字 在每个组合中 均只能使用一次
递归调用时传入参数startIndex为i,//关键点: 不用 i+1 了,表示可以重复读取当前的数
组合内可出现重复元素,但不能有相同的组合==》数层去重==》需要对数组排序!
1.1 组合(1-n中找k个数的组合)
给定两个整数
n
和k
,返回范围[1, n]
中所有可能的k
个数的组合。可以按 任何顺序 返回答案。
class Solution {
private:
vector<vector<int>> result; //存放符合条件的结果集合
vector<int> path; //存放符合条件的单一结果
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++) {//横向遍历,遍历元素( “n - (k - path.size()) + 1” 是剪枝操作)
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;
}
};
1.2 组合总和 III(1-9中找和为n的k个数的组合)
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
class Solution {
private:
vector<vector<int>> result; //存放符合条件的结果集合
vector<int> path; //存放符合条件的单一结果
void backtracking(int k, int n, int startIndex, int sum) {// startIndex:下一层for循环搜索的起始位置。
if (sum > n) return;//剪枝,累加总和肯定不满足的就不用继续往下走了,可以直接返回
if (path.size() == k) {
//int sum = 0;
//for (auto i : path) {
// sum += i;
//}
if (sum == n) result.push_back(path);
return;
}
//该题数字使用范围只有1到9,n代表的是需要达到的数字相加总和
for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) {
sum += i;
path.push_back(i);
backtracking(k, n, i + 1, sum);
path.pop_back();
sum -= i;
}
}
public:
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(k, n, 1, 0);
return result;
}
};
1.3 话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
法一:
class Solution {
private:
vector<string> result;
string path;
const string letterMap[10] = {
"", //数字按键0
"", //数字按键1
"abc", //数字按键2(后面由此类推)
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz" //数字按键9, 此时下标也为9
};
//int index为传入的数字列表中对应数字的下标
void backtracking(string digits, int index) {
if (digits.size() == index) {//当传入的数字列表中所有数字都已访问完
result.push_back(path);
return;
}
int digit = digits[index] - '0';//将digits中的每一个数字转换成整型,以便访问对应的字母
for (int i = 0; i < letterMap[digit].size(); i++) {//遍历当前数字的对应字母
path.push_back(letterMap[digit].at(i));
backtracking(digits, index + 1);
path.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
if (digits.size() == 0) return result;
backtracking(digits, 0);
return result;
}
};
法二:回溯函数只传一个参数。
class Solution {
private:
vector<string> result;
string path;
vector<string> letters = {
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
void backtracking(string digits) {
if (path.size() == digits.size()) {
result.push_back(path);
return;
}
int curIdx = digits[path.size()] - '0';
for (int i = 0; i < letters[curIdx].size(); i++) {
path.push_back(letters[curIdx][i]);
backtracking(digits);
path.pop_back();
}
}
public:
vector<string> letterCombinations(string digits) {
if (digits.size() == 0) return result;
backtracking(digits);
return result;
}
};
1.4 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
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++) {
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) {
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) {
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++) {
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) {
sort(candidates.begin(), candidates.end()); // 需要排序
backtracking(candidates, target, 0, 0);
return result;
}
};
1.5 组合总和 II(结果中不可有重复的组合,即需去重)
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool> used) {
if (sum > target) return;//有了循环条件里的那个剪枝操作后,该行其实可以忽略
if (sum == target) {
result.push_back(path);
return;
}
for (int i = startIndex; i < candidates.size() && sum + candidates.at(i) <= target; i++) {
/*
candidates.at(i) == candidates.at(i - 1)的情况下:
used[i - 1] == false 代表 同一树层中(待选数) ,出现重复元素,即 candidates[i - 1]使用过
used[i - 1] == true 代表 同一树枝中(待填坑) ,出现重复元素,即 candidates[i - 1]使用过
结果组合不能出现重复,则需要在 同一数层 出现重复元素时,进行去重操作
//即 要对同一树层使用过的元素进行跳过
*/
if (i >= 1 && candidates.at(i) == candidates.at(i - 1) && used[i - 1] == false) continue;//同一树层中(待选数) ,出现重复元素,需进行去重,不可重复选取
used[i] = true;
sum += candidates.at(i);
path.push_back(candidates.at(i));
backtracking(candidates, target, sum, i + 1, used);//i + 1 说明 集合中每个数字 在每个组合中 均只能使用一次
sum -= candidates.at(i);
path.pop_back();
used[i] = false;
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
//此题重点是集合元素有重复,但出来的结果组合又不能有重复
/*
增加bool型的数组used,
每一次backtracking递归时就将使用过的集合里的元素所对应位置赋值true,代表已使用
因为backtracking中
每次for都为横向遍历,对集合中的待选数遍历,
每次函数递归都为纵向遍历,对结果组合中的待填坑遍历,
在递归中对used数组就行赋值为true,就代表本次的结果组合中,该数已使用过
*/
vector<bool> used(candidates.size(), false);
sort(candidates.begin(), candidates.end());//先将集合里的元素排个序
backtracking(candidates, target, 0, 0, used);
return result;
}
};
2、分割问题
N个数里面按一定规则找出k个数的集合
2.1 分割回文串
给你一个字符串
s
,请你将s
分割成一些子串,使每个子串都是 回文串 。返回s
所有可能的分割方案。回文串 是正着读和反着读都一样的字符串。
class Solution {
private:
vector<vector<string>> result;
vector<string> path;
void backtracking(string s, int startIndex) {
if (startIndex >= s.size()) {//分割位置到尽头,则可直接return
result.push_back(path);
return;
}
for (int i = startIndex; i < s.size(); i++) {
if (isPalindrome(s, startIndex, i)) {
string str;
str = s.substr(startIndex, i - startIndex + 1);//截取从下标为startIndex开始的i - startIndex + 1个字符作为新字符串
path.push_back(str);
}
else continue;//不是回文则直接continue跳过
backtracking(s, i + 1);
path.pop_back();//回溯
}
}
//判断是否回文
bool isPalindrome(string s, int startIndex, int i) {
//for (int begin = startIndex, end = i; begin < end; begin++, end--) {//双指针法
// if (s[begin] != s[end]) return false;
//}
string str = s.substr(startIndex, i - startIndex + 1);
for (int index = 0; index <= str.size() / 2; index++) {
if (str[index] != str[str.size() - 1 - index]) return false;
}
return true;
}
public:
vector<vector<string>> partition(string s) {
backtracking(s, 0);
return result;
}
};
2.2 复原 IP 地址
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
class Solution {
private:
vector<string> result;
void backtracking(string &s, int startIndex, int pointNum) {
if (pointNum == 3) {//已插入了三个分隔点,证明前面已分好三个整数,剩下的一直到末尾为第四个整数,即最后一个整数
if (isValid(s, startIndex, s.size() - 1)) result.push_back(s);//若最后一个整数也合法,则可存入结果vector
return;
}
for (int i = startIndex; i < s.size(); i++) {
if (isValid(s, startIndex, i)) {
pointNum++;//对应将分隔点的数量+1
s.insert(s.begin() + i + 1, '.');//在整数后插入一个分隔点
backtracking(s, i + 2, pointNum);//因为插入了个分隔点,所以此处需要再往后走一个位置,即+2
s.erase(s.begin() + i + 1);//回溯,删掉前面插入的那个分隔点
pointNum--;//对应将分隔点数量-1
}
else break;//不合法,直接不再横向遍历,结束本层循环
}
}
//判定区域内的整数是否合法有效
bool isValid(string s, int start, int end) {
if (start > end) return false;//整数的起始坐标和末尾坐标不合法
if (s[start] == '0' && start != end) return false;//有前导0的整数不合法
int num = 0;//记录当前整数的值
for (int i = start; i <= end; i++) {
if (s[i] < '0' || s[i] > '9') return false;//非数字字符为不合法
num = num * 10 + (s[i] - '0');
if (num > 255) return false;//超出0-255范围的整数不合法
}
return true;
}
public:
vector<string> restoreIpAddresses(string s) {
if (s.size() < 4 || s.size() > 12) return result;//输入的数据不合法直接返回
int pointNum = 0;
backtracking(s, 0, pointNum);
return result;
}
};
3、子集问题
一个N个数的集合里有多少符合条件的子集
3.1 子集(数组中元素互不相同)
给你一个整数数组
nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
法1:
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()) {//终止条件可加可不加
return;//因为 startIndex >= nums.size() 时,横向遍历也结束了,所以也可以不加该终止条件
}
//或将上方代码替换成以下
//if (startIndex >= nums.size()) {
// result.push_back(path);//将最后一个子集(与完整数组一模一样的子集)也push进去
// return;
//}
//result.push_back(path);
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
};
法2:
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex) {
if (path.size() == nums.size()) {
result.push_back(path);
return;
}
result.push_back(path);
for (int i = startIndex; i < nums.size(); i++) {
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums, 0);
return result;
}
};
3.2 子集 II(数组中元素可能存在重复)
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列
不使用used数组方法:
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()) {
return;
}
for (int i = startIndex; i < nums.size(); i++) {
//i > startIndex是 同数层中的待选元素
//位于startIndex往前的是已选的,无需要看 <
//位于startIndex位置上的是碰到重复时的首位,可入 ==
//位于startIndex往后的是待选的,若有重复则直接跳过不看(即去重)>
if (i > startIndex) {//这里的 i > startIndex 解释如上
if (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;
}
};
使用used数组方法:
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex, vector<bool> used) {
result.push_back(path);
// if (startIndex >= nums.size()) {//可有可无
// return;
// }
for (int i = startIndex; i < nums.size(); i++) {
//used[i - 1]==true ,代表元素nums[i - 1]在同一树枝已使用过
//used[i - 1]==false,代表元素nums[i - 1]在同一树层已使用过
//组合中的元素可以出现重复,但组合不可有重复,所以需将同一树层的元素去重
if (i >= 1) {
if (nums[i] == nums[i - 1] && !used[i - 1]) continue;
}
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, i + 1, used);
path.pop_back();
used[i] = false;
}
}
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<bool> used(nums.size(), false);
sort(nums.begin(), nums.end());//排个序,以便后面进行去重
backtracking(nums, 0, used);
return result;
}
};
4、排列问题
N个数按一定规则全排列,有几种排列方式
4.1 全排列(原始数组中不含重复数字)
给定一个不含重复数字的数组
nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
//vector<bool> used某个元素是否已使用过
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;//已使用过的元素直接跳过
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
}
};
4.2 全排列 II(原始数组中元素可能存在重复)
给定一个可包含重复数字的序列
nums
,按任意顺序 返回所有不重复的全排列。
class Solution {
private:
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;//使用过的元素直接跳过
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;//在数层中去重
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> used(nums.size(), false);
sort(nums.begin(), nums.end());//排好序,以便后面做去重
backtracking(nums, used);
return result;
}
};
5、棋盘问题
八皇后问题(递归算法)
在一个8*8的棋盘上,放置8个皇后,使他们分别在 不同行 不同列 不同对角线,
问:有多少种情况,并求出每种情况。(可用递归调用)
从矩阵的特点上可找到规律:
如果在同一行,则行号相同;
如果在同一列,则列号相同;
如果在/斜线上,则行列值之和相同,
如果在\斜线上,则行列值之差相同。
递归调用:
queen的下标代表当前皇后的行数,queen[row]的内容代表当前皇后的列数。
#include "stdafx.h"
#include<iostream>
using namespace std;
bool isSafe(int row, int queen[8])//判断皇后是否相撞,即当前皇后摆放位置是否合法
{
for (int i = 0; i < row; i++) {
if (queen[i] == queen[row] || //同列相撞
queen[i] + i == queen[row] + row || //同一反对角线相撞
queen[i] - i == queen[row] - row) { //同一正对角线相撞
return false;//只要有一项不合法都可以直接返回false
}
}
return true;//全部都通过了则可以返回true
}
void eightQueens(int row, int queens[8], int& cnt)
{
//printf("eightQueens\n");
if (row == 8) {//终止条件,摆放完第八个皇后
cnt++;
printf("当前方案 No.%d:(", cnt);
for (int i = 0; i < 8; i++) {
printf("%d, ", queens[i] + 1);
}
printf(")\n No.%d:具体如下 \n", cnt);
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if (j == queens[i]) printf("0 ");
else printf("- ");
}
cout << endl;
}
return;
}
else {
for (int col = 0; col < 8; col++) {
queens[row] = col;//当前行中,先尝试把皇后摆放的位置
if(isSafe(row, queens)) eightQueens(row + 1, queens, cnt);//若该摆放位置合理,则开始摆下一行的皇后
}
}
}
int main() {
int cnt = 0;
int queens[8];
eightQueens(0, queens, cnt);
//for (int i = 0; i < 8; i++) {
// for (int j = 0; j < 8; j++) {
// printf("[%d,%d] ", i, j);
// }
// cout << endl;
//}
system("Pause");
return 0;
}
5.1 N 皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位
class Solution {
private:
vector<vector<string>> result;
//void backtracking(int n, int row, )
bool isSafe(int row, vector<int> que) {
for (int i = 0; i < row; i++) {
if (que[i] == que[row] ||
que[i] + i == que[row] + row ||
que[i] - i == que[row] - row) {
return false;
}
}
return true;
}
void queens(int n, int row, vector<int> &que, int &cnt) {
if (row == n) {
cnt++;
vector<string> curScheme(n);
for (int i = 0; i < n; i++) {//按题目要求填入棋盘方案
string curStringS(n, '.')
curStringS[que[i]] = 'Q';
curScheme.at(i) = curStringS;//填入当前方案棋盘的当前行信息
}
result.push_back(curScheme);//填入当前方案的全部棋盘信息
return;
}
for (int i = 0; i < n; i++) {
que.at(row) = i;
if (isSafe(row, que)) {
queens(n, row + 1, que, cnt);
}
}
}
public:
vector<vector<string>> solveNQueens(int n) {
vector<int> que(n, 0);
int cnt = 0;
queens(n, 0, que, cnt);
cout << "cnt" << cnt << endl;
return result;
}
};
或:
class Solution {
private:
vector<vector<string>> result;
bool isSafe(int row, vector<int> que) {
for (int i = 0; i < row; i++) {
if (que[i] == que[row] ||
que[i] + i == que[row] + row ||
que[i] - i == que[row] - row) {
return false;
}
}
return true;
}
void queens(int n, int row, vector<int> &que, vector<string> &curScheme, int &cnt) {
//vector<string> curScheme(n);
string curStringS(n, '.');
if (row == n) {//放完第n行皇后,已出一种解决方案
cnt++;
result.push_back(curScheme);//填入当前方案的全部棋盘信息
return;
}
for (int i = 0; i < n; i++) {
que.at(row) = i;
if (isSafe(row, que)) {
curScheme[row][i] = 'Q';//放置皇后
queens(n, row + 1, que, curScheme, cnt);
curScheme[row][i] = '.';//撤销处理结果,撤回皇后
}
}
}
public:
vector<vector<string>> solveNQueens(int n) {
result.clear();
vector<int> que(n, 0);
int cnt = 0;
vector<string> curScheme(n, string(n, '.'));
queens(n, 0, que, curScheme, cnt);
cout << "\ncnt=" << cnt << endl;
return result;
}
};
或(经典回溯模板):
class Solution {
private:
vector<vector<string>> result;
//vector<string> path;
void backtracking(int n, int row, vector<string> &path) {
if (row == n) {//摆放完最后一行皇后了,则可增加一组摆放结果
result.push_back(path);
return;
}
for (int i = 0; i < n; i++) {
if (isSafe(n, i, row, path)) {//当前行的摆放安全,才会继续往下摆放下一行
path[row][i] = 'Q';
backtracking(n, row + 1, path);
path[row][i] = '.';
}
}
}
bool isSafe(int n, int col, int row, vector<string> path) {
//根据前面已存的列判断当前摆放的皇后是否有与前面已摆的皇后相撞
for (int i = 0; i < row; i++) {
if (path[i][col] == 'Q') return false;//同一列有皇后相撞
}
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (path[i][j] == 'Q') return false;//反对角线有皇后相撞
}
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (path[i][j] == 'Q') return false;//正对角线有皇后相撞
}
return true;//同一列,正对角线,反对角线都没有皇后相撞,证明安全
}
public:
vector<vector<string>> solveNQueens(int n) {
vector<string> path(n, string(n, '.'));
backtracking(n, 0, path);
return result;
}
};
5.2 解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.' 表示。
易读易理解版:
class Solution {
private:
bool backtracking(vector<vector<char>>& board) {
for (int row = 0; row < board.size(); row++) { //遍历行
for (int col = 0; col < board.at(row).size(); col++) { //遍历列
if (board[row][col] == '.') {//非空格直接跳过,因为无需填数字
for (char val = '1'; val <= '9'; val++) {
if (isValid(row, col, val, board)) {//若该数字合理
board[row][col] = val;//尝试摆放
if (backtracking(board)) return true;//找到了合适的一组,直接返回true
board[row][col] = '.';//没有找到合适的一组,则回溯撤销前面的摆放
}
}
return false;//全部数字都试过,没有合理的数字,就返回false
}
}
}
return true;//全部顺利遍历完后,中途没有返回false的,就证明全部摆放完毕并且合理
}
bool isValid(int row, int col, char val, vector<vector<char>> &board) {
for (int i = 0; i < board.size(); i++) {
if (board[i][col] == val) return false;//列上有相同数字
}
for (int i = 0; i < board[0].size(); i++) {
if (board[row][i] == val) return false;//行上有相同数字
}
int startY = row / 3 * 3;
int startX = col / 3 * 3;
for (int i = startY; i < startY + 3 && i < board.size(); i++) {
for (int j = startX; j < startX + 3 && j < board[i].size(); j++) {
if (board[i][j] == val) return false;//3*3的九宫格中有相同数字
}
}
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
};
优化版:
class Solution {
private:
bool backtracking(int startRow, int startCol, vector<vector<char>>& board) {
for (int row = startRow; row < board.size(); row++) { //遍历行
for (int col = startCol; col < board.at(row).size(); col++) { //遍历列
if (board[row][col] == '.') {//非空格直接跳过,因为无需填数字
for (char val = '1'; val <= '9'; val++) {
if (isValid(row, col, val, board)) {//若该数字合理
board[row][col] = val;//尝试摆放
if (backtracking(startRow, col + 1, board)) return true;//找到了合适的一组,直接返回true
board[row][col] = '.';//没有找到合适的一组,则回溯撤销前面的摆放
}
}
return false;//全部数字都试过,没有合理的数字,就返回false
}
}
startRow++;
startCol = 0;
}
return true;//全部顺利遍历完后,中途没有返回false的,就证明全部摆放完毕并且合理
}
bool isValid(int row, int col, char val, vector<vector<char>> &board) {
for (int i = 0; i < board.size(); i++) {
if (board[i][col] == val) return false;//列上有相同数字
}
for (int i = 0; i < board[0].size(); i++) {
if (board[row][i] == val) return false;//行上有相同数字
}
int startY = row / 3 * 3;
int startX = col / 3 * 3;
for (int i = startY; i < startY + 3 && i < board.size(); i++) {
for (int j = startX; j < startX + 3 && j < board[i].size(); j++) {
if (board[i][j] == val) return false;//3*3的九宫格中有相同数字
}
}
return true;
}
public:
void solveSudoku(vector<vector<char>>& board) {
backtracking(0, 0, board);
}
};
6、其他
一般来说哈希表都是用来快速判断一个元素是否出现集合里。
unordered_set 容器,可直译为“无序 set 容器”,即 unordered_set 容器和 set 容器很像,唯一的区别就在于 set 容器会自行对存储的数据进行排序,而 unordered_set 容器不会。
unordered_set 底层是哈希表。
(unordered_set 存储的都是键和值相等的键值对,为了节省存储空间,该类容器在实际存储时选择只存储每个键值对的值。)
6.1 递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
1
6.2 重新安排行程
给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
class Solution {
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
}
};