回溯法
基本知识
1. 思想
穷举所有可能,选出想要的答案。
2. 解决的问题
-
组合问题:N个数中按一定规则找出k个数的集合;
-
切割问题:一个字符串按一定规则有几种切割方式;
-
子集问题:一个N个数的集合中有多少符合条件的子集;
-
排列问题:N个数按一定规则全排列,有几种排列方式;
-
棋盘问题:N皇后,解数独;
3. 模板
void backtracking(参数){ if(终止条件){ 存放结果; return ; } for(选择:本层集合中元素(树中节点孩子的数量就是集合的大小)){ 处理节点; backtracking(路径,选择列表); //递归 回溯,撤销处理结果 } }
典型例题
1. LeetCode 77. 组合
题目
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
思路
-
设置result记录最终结果,path记录结果中每个符合条件的项;
-
所求问题也就为从1-n的n个数中选取k个数填入k个位置,但由于是组合问题,这k个数不存在相对位置问题;
-
采用递归函数填写每一个位置的数,并采用循环遍历该位置的所有可能,利用startindex控制递归结束条件,以及循环中起始位置用来避免组合数的重复;
-
回溯体现在对于每一个位置数的尝试,尝试一次后需要恢复现场(尝试前的状态);
代码
class Solution { private: vector<vector<int>>result; vector<int>path; void function(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); function(n, k, i+1); path.pop_back(); } } public: vector<vector<int>> combine(int n, int k) { result.clear(); path.clear(); function(n, k, 1); return result; } };
2. LeetCode 17.电话号码的字母组合
题目
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
-
设置全局变量存储1-9个每个按键的字母;
-
采用递归函数填写给定按键每个位置的相应字母;
-
采用循环尝试每个按键位置的可能字母;
-
每次尝试后进行下一次尝试前利用回溯法进行现场恢复;
代码
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) { s.clear(); result.clear(); if (digits.size() == 0) { return result; } backtracking(digits, 0); return result; } };
3. LeetCode 39.组合总和
题目
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
1.优化前
-
采用递归向已有序列中添加新数,若已经满足条件或不再能满足条件则返回;
-
采用循环尝试添加新数的数值,并随之修改sum;
-
调用递归时,由于选取元素值可重复,则传递参数时startIndex设置为添加新数下标即可,不需+1;
-
尝试一个数后,尝试下一个数前需要恢复现场;
2.优化后
-
首先对给定数列进行从小到大排序;
-
循环尝试添加新数的数值时,判断若sum已经不符合条件则结束本次循环(是由于后面未被尝试数的数值大于当前尝试数的数值,当前情况仍然不能符合要求,则后面的数更不能符合条件);
代码
1.优化前
// 版本一 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) { result.clear(); path.clear(); backtracking(candidates, target, 0, 0); return result; } };
2.优化后
class Solution { private: vector<vector<int>> result; vector<int> path; void backtracking(vector<int>& candidates, int target, int sum, int startIndex) { 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); sum -= candidates[i]; path.pop_back(); } } public: vector<vector<int>> combinationSum(vector<int>& candidates, int target) { result.clear(); path.clear(); sort(candidates.begin(), candidates.end()); // 需要排序 backtracking(candidates, target, 0, 0); return result; } };
4. LeetCode 40.组合总和II
题目
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
1.方法一
-
首先对给定数组进行排序;
-
设置used数组存储与给定数组下标相同对应元素使用情况,true代表使用,false代表未使用;
-
由于给定数组可能包含相同值的元素,按照原有遍历方法就可能产生结果的重复,所以在循环的遍历中,若当前元素与之前一个元素值相等并且前一个元素未使用,则此次选择会产生重复,判断下一个元素;
2.方法二
-
采用startIndex避免选择结果的重复;
-
若在循环中向后选择数时遇到前一次选择的数值(i > startIndex && candidates[i] == candidates[i - 1]),则可等同于在树的遍历过程中同一层中出现重复节点,则需要进行结果去重,判断下一个元素;
代码
1.方法一
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) { result.push_back(path); return; } for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 // 要对同一树层使用过的元素进行跳过 if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { continue; } 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); path.clear(); result.clear(); // 首先把给candidates排序,让其相同的元素都挨在一起。 sort(candidates.begin(), candidates.end()); backtracking(candidates, target, 0, 0, used); return result; } };
2.方法二
class Solution { private: vector<vector<int>> result; vector<int> path; void backtracking(vector<int>& candidates, int target, int sum, int startIndex) { if (sum == target) { result.push_back(path); return; } for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { // 要对同一树层使用过的元素进行跳过 if (i > startIndex && candidates[i] == candidates[i - 1]) { continue; } sum += candidates[i]; path.push_back(candidates[i]); backtracking(candidates, target, sum, i + 1); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次 sum -= candidates[i]; path.pop_back(); } } public: vector<vector<int>> combinationSum2(vector<int>& candidates, int target) { path.clear(); result.clear(); // 首先把给candidates排序,让其相同的元素都挨在一起。 sort(candidates.begin(), candidates.end()); backtracking(candidates, target, 0, 0); return result; } };
5. LeetCode 131.分割回文串
题目
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
思路
-
利用递归分析符号串每个位置开始所具有的所有回文串;
-
利用循环寻找从startIndex开始的回文串,找到后添加至结果并调用递归函数继续判断符号串后面的回文性;
代码
class Solution { private: vector<vector<string>> result; vector<string> path; // 放已经回文的子串 void backtracking (const string& s, int startIndex) { // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 if (startIndex >= s.size()) { result.push_back(path); return; } for (int i = startIndex; i < s.size(); i++) { if (isPalindrome(s, startIndex, i)) { // 是回文子串 // 获取[startIndex,i]在s中的子串 string str = s.substr(startIndex, i - startIndex + 1); path.push_back(str); } else { // 不是回文,跳过 continue; } backtracking(s, i + 1); // 寻找i+1为起始位置的子串 path.pop_back(); // 回溯过程,弹出本次已经填在的子串 } } bool isPalindrome(const 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; } public: vector<vector<string>> partition(string s) { result.clear(); path.clear(); backtracking(s, 0); return result; } };
6. LeetCode 93.复原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 中的任何数字。你可以按 任何 顺序返回答案。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
-
采用递归函数寻找从数字串指定位置开始的合法部分;
-
循环用于尝试从指定位置开始的各个截至位置,若该子串合法,则调用递归函数进行下一部分子串的选择;
-
若循环中至某一位置的子串已经不合法,从数值上可知其已经超过255,则继续添加后面的的数值更不可能合法,所以可以直接跳出循环;
-
判断子串合法性时,
-
若子串存在超过一个数字,则第一个数字为0时非法;
-
若子串中含有非0-9字符时,子串非法;
-
若子串代表数值超过255时,子串非法;
-
若最初给定字符串的长度已经超过12时,则该IP地址一定非法;
-
代码
class Solution { private: vector<string> result;// 记录结果 // startIndex: 搜索的起始位置,pointNum:添加逗点的数量 void backtracking(string& s, int startIndex, int pointNum) { if (pointNum == 3) { // 逗点数量为3时,分隔结束 // 判断第四段子字符串是否合法,如果合法就放进result中 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 , '.'); // 在i的后面插入一个逗点 pointNum++; backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2 pointNum--; // 回溯 s.erase(s.begin() + i + 1); // 回溯删掉逗点 } else break; // 不合法,直接结束本层循环 } } // 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法 bool isValid(const string& s, int start, int end) { if (start > end) { return false; } if (s[start] == '0' && start != end) { // 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; } };
7. LeetCode 78.子集
题目
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思路
-
已知数组中的元素互不相同,则每一个位置可以唯一确定元素,所以利用下标进行求取子集时不存在重复子集情况;
-
由于求取数组的所有子集,则每次根据下标向path中添加一个元素所构成的子数组都是原数组的子集,因此每次进入递归函数时不需要判断,可以直接将path添加至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()) { // 终止条件可以不加 return; } 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) { result.clear(); path.clear(); backtracking(nums, 0); return result; } };
8. LeetCode 90.子集II
题目
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
1.方法一
-
首先对给定数组进行排序,并构造used数组存储给定数组中元素的使用情况;
-
递归函数用于选择数组startIndex下标开始的元素添加到path中;
-
循环用于尝试选择startIndex下标开始的元素,并对path进行添加与恢复操作;
2.方法二
-
采用unordered_set类型变量存储递归函数该层曾使用过的元素;
-
循环若遍历到一个元素出现在unordered_set类型变量中,则说明该值已经进行过尝试,所以如果该元素继续进行尝试则会引起重复,从而避免结果重复;
代码
1.方法一
class Solution { 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; } };
2.方法二
class Solution { private: vector<vector<int>> result; vector<int> path; void backtracking(vector<int>& nums, int startIndex, vector<bool>& used) { result.push_back(path); unordered_set<int> uset; for (int i = startIndex; i < nums.size(); i++) { if (uset.find(nums[i]) != uset.end()) { continue; } uset.insert(nums[i]); path.push_back(nums[i]); backtracking(nums, i + 1, used); 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; } };
9. LeetCode 491.递增子序列
题目
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
1.优化前
-
递归函数用于从startIndex下标开始选取元素添加到path,适时添加到结果result中;
-
循环用于尝试startIndex开始每一个元素,并调用递归函数进入下一层处理;
-
采用unordered_set用于记录该层内曾经尝试过的元素,若发现重复尝试元素,则进行跳过,从而进行去重,同时采用递增条件限制进入下一层的条件;
2.优化后
-
采用数组记录该层内曾经尝试过的元素,利用下标与值的对应关系进行哈希
-
从而避免程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且每次重新定义set,insert的时候其底层的符号表也要做相应的扩充,也是费事的。
1.优化前
// 版本一 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对本层元素进行去重 for (int i = startIndex; i < nums.size(); i++) { if ((!path.empty() && nums[i] < path.back()) || 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) { result.clear(); path.clear(); 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() > 1) { result.push_back(path); } int used[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100] for (int i = startIndex; 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(); } } public: vector<vector<int>> findSubsequences(vector<int>& nums) { result.clear(); path.clear(); backtracking(nums, 0); return result; } };
10. LeetCode 46.全排列
题目
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
思路
-
采用used数组用于记录对应下标给定数组元素的使用情况;
-
每次递归从未被使用的元素中选取一个元素添加到path中,并适时添加到result;
代码
class Solution { public: 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) { result.clear(); path.clear(); vector<bool> used(nums.size(), false); backtracking(nums, used); return result; } };
11. LeetCode 47.全排列II
题目
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
思路
-
由于给定序列有重复元素,所以需要进行去重;
-
首先对给定序列进行排序;
-
构造used数组用于记录给定序列元素的使用情况;
-
每次递归用于从未被使用元素中选取一个元素添加至path中;
-
循环用于尝试未被选取的每个元素;
-
若循环中当前尝试值与前一个元素值相同,并且前一个元素未被使用,则此次尝试会引起重复,需要跳过;
代码
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++) { // used[i - 1] == true,说明同一树枝nums[i - 1]使用过 // used[i - 1] == false,说明同一树层nums[i - 1]使用过 // 如果同一树层nums[i - 1]使用过则直接跳过 if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { continue; } if (used[i] == false) { 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) { result.clear(); path.clear(); sort(nums.begin(), nums.end()); // 排序 vector<bool> used(nums.size(), false); backtracking(nums, used); return result; } };
12. LeetCode 332.重新安排行程
题目
给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前。 假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
-
采用unordered_map<string, map<string, int>>类型存储第一个字符串到第二个字符串的飞机票情况,在添加飞机票情况时,默认进行升序排列;
-
递归函数用于选择目前站到下一站的飞机票;
-
循环用于尝试所有可能使用的飞机票;
-
若记录站数等于给定票数+1,说明所有飞机票全部使用,结束;
代码
class Solution { private: // unordered_map<出发机场, map<到达机场, 航班次数>> targets 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]]) { 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) { targets.clear(); vector<string> result; for (const vector<string>& vec : tickets) { targets[vec[0]][vec[1]]++; // 记录映射关系 } result.push_back("JFK"); // 起始机场 backtracking(tickets.size(), result); return result; } };
13. LeetCode 51.N皇后
题目
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
-
递归函数用于在棋盘指定行某一位置放置皇后;
-
循环用于尝试该行内所有位置,若与之前放置的皇后不冲突,则放置皇后,调用递归函数进行下一行皇后的放置;
-
判断皇后放置位置的有效性时,需要判断该列,主对角线,副对角线是否存在皇后;
-
不需判断横行是否存在皇后的原因是,在循环添加皇后时,每添加一个皇后进入下一行,已经保证一行只存在一个皇后;
-
n行均放置完成后,得到一个可行解;
代码
class Solution { private: vector<vector<string>> result; // n 为输入的棋盘大小 // row 是当前递归到棋盘的第几行了 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) { int count = 0; // 检查列 for (int i = 0; i < row; i++) { // 这是一个剪枝 if (chessboard[i][col] == '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); return result; } };
14. LeetCode 37. 解数独
题目
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图) 数独部分空格内已填入了数字,空白格用 '.' 表示。
来源:力扣(LeetCode) 链接:力扣 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路
-
递归函数用于向棋盘中放置数的位置选择数值;
-
循环遍历棋盘未被放置数的位置,对每一个位置尝试1-9数,若符合规定则在该条件下调用递归函数进行剩余空闲位置的尝试;
-
若对于一个位置9个数均尝试未成功,则返回上一个位置尝试其他数值;
-
若所有位置均在不冲突情况下填充完毕,则结束;
代码
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,说明找到了合适棋盘位置了 } bool isValid(int row, int col, char val, vector<vector<char>>& board) { for (int i = 0; i < 9; i++) { // 判断行里是否重复 if (board[row][i] == val) { 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); } };