第七章 回溯算法
day 24今日内容:
● 理论基础
● 77. 组合
24理论基础
回溯法,一般可以解决如下几种问题:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
可解决问题
代码
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案
解题思路
n个数返回k个数组合,开始看例子使用二重循环写出了k=2的答案,从i开始for循环,二重循环从i+1循环,但是k=20就要写20个循环,每次循环都是一样的操作,只是开头不同,故采用回溯算法,每次for循环放入一个数,递归直到答案有k个返回,满足条件以后要移除最后的值,即回溯。
代码
class Solution {
public:
vector<vector<int>> ans;
vector<int> tmp;
void backtracking(int n,int k,int start){
//终止条件
if(tmp.size() == k){
ans.push_back(tmp);
return;
}
//单层递归
for(int i = start; i <= n; i++){
tmp.push_back(i);
backtracking(n, k, i + 1);
tmp.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
backtracking(n,k,1);
return ans;
}
};
day 25今日内容:
● 216.组合总和III
● 17.电话号码的字母组合
216.组合总和III
找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:
只使用数字1到9
每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
解题思路
组合总和在上述计算k个数集合的条件 多了sum;对sum变量进行判断是否等于n;每次遍历要加i,回溯要减i;
代码
class Solution {
public:
vector<int> tmp;
vector<vector<int>> ans;
int sum;
void backtrack(int k, int n,int start){
if(n == sum && tmp.size() == k){
ans.push_back(tmp);
return;
}
for(int i = start; i <= 9; i++){
sum += i;
tmp.push_back(i);
backtrack(k, n, i + 1);
sum -= i;
tmp.pop_back();
}
}
vector<vector<int>> combinationSum3(int k, int n) {
sum = 0;
backtrack(k, n, 1);
return ans;
}
};
、
17.电话号码的字母组合
解题思路
电话号码组合和数字组合不同,数字回溯是数字开始增加,即第一次循环是1-9,第二次是2-9,第三次是3-9……电话号码第一次循环是第一个数对应的字符串,第二次循环是第二个数对应的字符串…………
代码
class Solution {
public:
const string telmap[10] = {
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz",
};
vector<string> ans;
string s;
void backtrack(string digits,int index){
if(s.size() == digits.size()){
ans.push_back(s);
return;
}
int num = digits[index] - '0';
string tels = telmap[num];
for(int i = 0; i < tels.size(); i++){
s.push_back(tels[i]);
backtrack(digits, index + 1);
s.pop_back();
}
}
vector<string> letterCombinations(string digits) {
if(digits.size() == 0)
return ans;
backtrack(digits,0);
return ans;
}
};
● 39. 组合总和
● 40.组合总和II
● 131.分割回文串
39. 组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
解题思路
组合问题使用 回溯法,同一个数组可重复代表每次开始的位置即为start,第一次写的时候我忽略了每次开始变量,所以每次开始都是数组0开始,会有重复的答案。
代码
class Solution {
public:
vector<vector<int>> ans;
vector<int> tmp;
int sum;
void backtrack(vector<int>& candidates,int target,int start){
if(sum > target)
return;
if(sum == target){
ans.push_back(tmp);
return;
}
for(int i = start; i < candidates.size(); i++){
sum += candidates[i];
tmp.push_back(candidates[i]);
backtrack(candidates,target,i);
sum -= candidates[i];
tmp.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sum = 0;
backtrack(candidates,target,0);
return ans;
}
};
40.组合总和II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
解题思路
代码
class Solution {
public:
vector<vector<int>> ans;
vector<int> tmp;
int sum;
void backtrack(vector<int>& candidates,int target,int start){
if(sum > target)
return;
if(sum == target){
ans.push_back(tmp);
return;
}
for(int i = start; i < candidates.size(); i++){
if(i > start && candidates[i] == candidates[i - 1])
continue;
sum += candidates[i];
tmp.push_back(candidates[i]);
backtrack(candidates,target,i + 1);
sum -= candidates[i];
tmp.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sum = 0;
sort(candidates.begin(),candidates.end());
backtrack(candidates,target,0);
return ans;
}
};
131.分割回文串
解题思路
分割回文串,分割问题可看作组合问题,for循环每个字母为开始,然后判断以某个字母开始满足条件的回文串;
判断回文串的函数是从头和尾判断两个字母是否相同;
用到取子串函数s.subsr(起始位置,长度)
代码
class Solution {
public:
vector<vector<string>> ans;
vector<string> tmp;
void backtrack(string s, int start){
if(start >= s.size()){
ans.push_back(tmp);
return;
}
for(int i = start; i < s.size(); i++){
if(isMirror(s,start,i)){
string str = s.substr(start,i - start + 1);
tmp.push_back(str);
}
else
continue;
backtrack(s, i + 1);
tmp.pop_back();
}
}
bool isMirror(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) {
backtrack(s, 0);
return ans;
}
};
#28 **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 中的任何数字。你可以按 任何 顺序返回答案。
解题思路
和分割回文串一样的思路,
判断条件改为对ip单个数字;1)IP单个数字当长度大于2时首字母不能为0;2)对每个字符判断在0-9之间👎单个数字大小小于255;
回溯条件结束情况要么段数大于4了即point > 3或者开始的位置大于s的长度即第三个点后没有字母了;当满足三个点以后还要判断最后一个ip字符是否满足要求再加入ans
然后就是回溯时数组插入函数insert(位置,插入的字符);插入字符以后新的i不再是i + 1,而是i+2;删除函数erase(删除的位置)
代码
class Solution {
public:
vector<string> ans;
int point;
void trackback(string s, int start, int point){
//判读条件
if(start > s.size() - 1){
return;
}
if(point == 3){
if(isIP(s,start,s.size()-1)){
ans.push_back(s);
return;
}
}
for(int i = start; i < s.size(); i++){
if(isIP(s, start , i)){
s.insert(s.begin() + i + 1, '.');
point++;
trackback(s, i + 2, point);
point--;
s.erase(s.begin() + i + 1);
}
else
break;
}
}
bool isIP(string s, int start ,int end){
//判断start开始,end结束的s是否满足单个数要求
int length = end - start + 1;
if(length >= 2 && s[start] == '0'){
return false;
}
int nums = 0;
for(int i = start; i <= end; i++){
if(s[i] > '9' || s[i] < '0')
return false;
nums = nums * 10 + (s[i] - '0');
if(nums > 255){
return false;
}
}
return true;
}
vector<string> restoreIpAddresses(string s) {
trackback(s,0,0);
return ans;
}
};
78.子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
解题思路
子集是组合问题,子集是要回溯树的所有节点,故 没有判断条件,把路径放入答案即可;
代码
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& nums,int start){
ans.push_back(path);
for(int i = start; i < nums.size(); i++){
path.push_back(nums[i]);
backtrack(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backtrack(nums, 0);
return ans;
}
};
90.子集II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
解题思路
这里重要的是对去重,先对数组排序以后,对每次循环判断i大于start且前一个数与其相同就congtinue不进入循环;
代码
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& nums, int start){
ans.push_back(path);
for(int i = start; i < nums.size() ; i++){
//去重
if(start < i && nums[i] == nums[i - 1]){
continue;
}
path.push_back(nums[i]);
backtrack(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(),nums.end());
backtrack(nums, 0);
return ans;
}
};
- 491.递增子序列
- 46.全排列
- 47.全排列 II
491.递增子序列
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
解题思路
去重不能对数组排序以后判断i大于start以后是否等于前一个数;
所有需要一个数组来记录这个数有没有被使用过
递归的结束的条件是大于等于两个数的数组就可以;
单层递归,path里有数,当前的数小于path最后一个数的大小或者path为空,这个数已经出现过都需要跳过循环;
if((!path.empty() && nums[i] < path.back()) || used[nums[i] + 100] == 1)
代码
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& nums,int start){
if(path.size() >= 2){
ans.push_back(path);
}
int used[201] = {0};
for(int i = start; i < nums.size(); i++){
// 1、path不空但数字不满足递增则下一个 2、该数字已经出现过;
if((!path.empty() && nums[i] < path.back()) || used[nums[i] + 100] == 1){
continue;
}
used[nums[i] + 100] = 1;
path.push_back(nums[i]);
backtrack(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backtrack(nums, 0);
return ans;
}
};
46.全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
解题思路
排列问题其实就是组合的有序,即回溯时开始的指标就没有了;递归终止条件就是收集元素等于数组元素个数时停止;单层逻辑需要使用一个used数组来存储该数字是否使用过,加入path前标记其为true;弹出以后要还原其为false
代码
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& nums, vector<bool>& used){
if(path.size() == nums.size()){
ans.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++){
if(used[i] == true){
continue;
}
used[i] = true;
path.push_back(nums[i]);
backtrack(nums,used);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(),false);
backtrack(nums, used);
return ans;
}
};
46.全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
解题思路
排序去重
代码
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void backtrack(vector<int>& nums, vector<bool>& used){
if(path.size() == nums.size()){
ans.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++){
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]);
backtrack(nums, used);
path.pop_back();
used[i] = false;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<bool> used(nums.size(),false);
backtrack(nums, used);
return ans;
}
};