回溯算法
什么是回溯算法?
回溯算法是⼀种经典的递归算法,通常用于解决组合问题、排列问题和搜索问题等。回溯算法的基本思想:从一个初始状态开始,按照一定的规则向前搜索,当搜索到某个状态无法前进时,回退到前一个状态,再按照其他的规则搜索。回溯算法在搜索过程中维护一个状态树,通过遍历状态树来实现对所有可能解的搜索。
回溯算法的核心思想:“试错”,即在搜索过程中不断地做出选择,如果选择正确,则继续向前搜索;否则,回退到上一个状态,重新做出选择。回溯算法通常用于解决具有多个解,且每个解都需要搜索才能找到的问题。
回溯算法的模板:
void backtrack(vector<int>& path, vector<int>& choice, ...)
{
// 满⾜结束条件
if (/* 满⾜结束条件 */)
{
// 将路径添加到结果集中
res.push_back(path);
return;
}
// 遍历所有选择
for (int i = 0; i < choices.size(); i++)
{
// 做出选择
path.push_back(choices[i]);
// 做出当前选择后继续搜索
backtrack(path, choices);
// 撤销选择
path.pop_back();
}
}
其中, path 表示当前已经做出的选择, choices 表示当前可以做的选择。在回溯算法中,我们需要做出选择,然后递归地调用回溯函数。如果满足结束条件,则将当前路径添加到结果集中;否则,我们需要撤销选择,回到上一个状态,然后继续搜索其他的选择。
回溯算法的时间复杂度通常较高,因为它需要遍历所有可能的解。但是,回溯算法的空间复杂度较低,因为它只需要维护⼀个状态树。在实际应用中,回溯算法通常需要通过剪枝等⽅法进行优化,以减少搜索的次数,从而提高算法的效率。
回溯算法的应用
- 组合问题
组合问题是指从给定的⼀组数(不重复)中选取出所有可能的 k 个数的组合。例如,给定数集 [1,2,3],要求选取 k=2 个数的所有组合。
结果为:[1,2]、[1,3]、[2,3]
- 排列问题
排列问题是指从给定的⼀组数(不重复)中选取出所有可能的 k 个数的排列。例如,给定数集 [1,2,3],要求选取 k=2 个数的所有排列。
结果为:[1,2]、[2,1]、[1,3]、[3,1]、[2,3]、[3,2]
- 子集问题
子集问题是指从给定的一组数中选取出所有可能的子集,其中每个子集中的元素可以按照任意顺序排列。例如,给定数集 [1,2,3],要求选取所有可能的子集。
结果为:[]、[1]、[2]、[3]、[1,2]、[1,3]、[2,3]、[1,2,3]
总结
回溯算法是⼀种非常重要的算法,可以解决许多组合问题、排列问题和搜索问题等。回溯算法的核心思想是搜索状态树,通过遍历状态树来实现对所有可能解的搜索。回溯算法的模板非常简单,但是实现起来需要注意⼀些细节,比如如何做出选择、如何撤销选择等。
1. 全排列
题目链接 -> Leetcode -46.全排列
Leetcode -46.全排列
题目:给定一个不含重复数字的数组 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]]
提示:
- 1 <= nums.length <= 6
- -10 <= nums[i] <= 10
- nums 中的所有整数互不相同
思路:典型的回溯题目,我们需要在每一个位置上考虑所有的可能情况并且不能出现重复。通过深度优先搜索的方式,不断地枚举每个数在当前位置的可能性,并回溯到上一个状态,直到枚举完所有可能性,得到正确的结果。每个数是否可以放入当前位置,只需要判断这个数在之前是否出现即可。具体地,在这道题目中,我们可以通过一个递归函数 dfs 和标记数组 check 来实现全排列。
递归流程如下:
- 首先定义一个二维数组 ret 用来存放所有可能的排列,一个一维数组 sub 用来存放每个状态的排列,一个一维数组 check 标记元素,然后从第一个位置开始进行递归;
- 在每个递归的状态中,我们维护一个步数 step,表示当前已经处理了几个数字;
- 递归结束条件:当 step 等于 nums 数组的长度时,说明我们已经处理完了所有数字,将当前数组存入结果中;
- 在每个递归状态中,枚举所有下标 i,若这个下标未被标记,则使用 nums 数组中当前下标的元素:
- 将 check[i] 标记为 1;
- ** sub 数组中第 step 个元素被 nums[i] 覆盖;**
- 对第 step+1 个位置进行递归;
- 将 check[i] 重新赋值为 0,表示回溯;
- 最后,返回 res.
特别地,我们可以不使用标记数组,直接遍历 step 之后的元素(未被使用),然后将其与需要递归的位置进行交换即可。
代码如下:文章来源地址https://www.toymoban.com/news/detail-777100.html
class Solution
{
vector<vector<int>> ret;
vector<bool> cheak;
vector<int> sub;
public:
void dfs(vector<int>& nums)
{
if(nums.size() == sub.size())
{
ret.push_back(sub);
return;
}
for(int i = 0; i < nums.size(); i++)
{
// 如果当前位置没被用过
if(cheak[i] == false)
{
// 当前位置已经被用过标记为 true
sub.push_back(nums[i]);
cheak[i] = true;
dfs(nums);
// 恢复现场
sub.pop_back();
cheak[i] = false;
}
}
}
vector<vector<int>> permute(vector<int>& nums)
{
cheak.resize(nums.size());
dfs(nums);
return ret;
}
};
2. 子集
题目链接 -> Leetcode -78.子集
Leetcode -78.子集
题目:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1, 2, 3]
输出: [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]
示例 2:
输入:nums = [0]
输出: [[], [0]]
提示:
- 1 <= nums.length <= 10
- -10 <= nums[i] <= 10
- nums 中的所有元素 互不相同
思路:对于每个元素有两种选择:1. 不进行任何操作;2. 将其添加至当前状态的集合。在递归时我们需要保证递归结束时当前的状态与进行递归操作前的状态不变,而当我们在选择进行步骤 2 进行递归时,当前状态会发生变化,因此我们需要在递归结束时撤回添加操作,即进行回溯。
递归流程如下:
- 递归结束条件:如果当前需要处理的元素下标越界,则记录当前状态并直接返回;
- 在递归过程中,对于每个元素,我们有两种选择:
- 不选择当前元素,直接递归到下一个元素;
- 选择当前元素,将其添加到数组末尾后递归到下一个元素,然后在递归结束时撤回添加操作;
- 所有符合条件的状态都被记录下来,返回即可。
如下状态图:
代码如下:
方法一:
class Solution
{
vector<vector<int>> ret;
vector<int> path;
public:
// 方法一
void dfs(vector<int>& nums, int i)
{
if(i == nums.size())
{
ret.push_back(path);
return;
}
// 选
path.push_back(nums[i]);
dfs(nums, i + 1);
path.pop_back(); // 恢复现场
// 不选
dfs(nums, i + 1);
}
vector<vector<int>> subsets(vector<int>& nums)
{
int i = 0;
dfs(nums, i);
return ret;
}
};
方法二:
class Solution
{
vector<vector<int>> ret;
vector<int> path;
public:
// 方法二
void dfs(vector<int>& nums, int pos)
{
// 因为每个节点都是需要返回的答案,所以一进入函数就需要插入 ret 中
ret.push_back(path);
for(int i = pos; i < nums.size(); i++)
{
path.push_back(nums[i]);
dfs(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums)
{
int pos = 0;
dfs(nums, pos);
return ret;
}
};
3. 找出所有子集的异或总和再求和
题目链接 -> Leetcode -1863.找出所有子集的异或总和再求和
Leetcode -1863.找出所有子集的异或总和再求和
题目:一个数组的 异或总和 定义为数组中所有元素按位 XOR 的结果;如果数组为 空 ,则异或总和为 0 。
例如,数组[2, 5, 6] 的 异或总和 为 2 XOR 5 XOR 6 = 1 。
给你一个数组 nums ,请你求出 nums 中每个 子集 的 异或总和 ,计算并返回这些值相加之 和 。
注意:在本题中,元素 相同 的不同子集应 多次 计数。
数组 a 是数组 b 的一个 子集 的前提条件是:从 b 删除几个(也可能不删除)元素能够得到 a 。
示例 1:
输入:nums = [1, 3]
输出:6
解释:[1, 3] 共有 4 个子集:
- 空子集的异或总和是 0 。
- [1] 的异或总和为 1 。
- [3] 的异或总和为 3 。
- [1, 3] 的异或总和为 1 XOR 3 = 2 。
- 0 + 1 + 3 + 2 = 6
示例 2:
输入:nums = [5, 1, 6]
输出:28
解释:[5, 1, 6] 共有 8 个子集:
- 空子集的异或总和是 0 。
- [5] 的异或总和为 5 。
- [1] 的异或总和为 1 。
- [6] 的异或总和为 6 。
- [5, 1] 的异或总和为 5 XOR 1 = 4 。
- [5, 6] 的异或总和为 5 XOR 6 = 3 。
- [1, 6] 的异或总和为 1 XOR 6 = 7 。
- [5, 1, 6] 的异或总和为 5 XOR 1 XOR 6 = 2 。
- 0 + 5 + 1 + 6 + 4 + 3 + 7 + 2 = 28
示例 3:
输入:nums = [3, 4, 5, 6, 7, 8]
输出:480
解释:每个子集的全部异或总和值之和为 480 。
提示:
- 1 <= nums.length <= 12
- 1 <= nums[i] <= 20
思路:所有子集可以解释为:每个元素选择在或不在一个集合中(因此,子集有 2^n 个)。本题我们需要求出所有子集,将它们的异或和相加。因为异或操作满足交换律,所以我们可以定义一个变量,直接记录当前状态的异或和。使用递归保存当前集合的状态(异或和),选择将当前元素添加至当前状态与否,并依次递归数组中下一个元素。当递归到空元素时,表示所有元素都被考虑到,记录当前状态(将当前状态的异或和添加至答案中)。
代码如下:
class Solution
{
int ret = 0;
int x = 0;
public:
void dfs(vector<int>& nums, int pos)
{
ret += x;
for(int i = pos; i < nums.size(); i++)
{
x ^= nums[i];
dfs(nums, i + 1);
// 恢复现场
x ^= nums[i];
}
}
int subsetXORSum(vector<int>& nums)
{
dfs(nums, 0);
return ret;
}
};
4. 全排列Ⅱ
题目链接 -> Leetcode -47.全排列Ⅱ
Leetcode -47.全排列Ⅱ
题目:给定一个可包含重复数字的序列 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]]
提示:
- 1 <= nums.length <= 8
- -10 <= nums[i] <= 10
思路:因为题目不要求返回的排列顺序,因此我们可以对初始状态排序,将所有相同的元素放在各自相邻的位置,方便之后操作。因为重复元素的存在,我们在选择元素进行全排列时,可能会存在重复排列,例如:[1, 2, 1],全排列为121、112、211、211、112、121;可以看到,有效排列只有三种[1, 1, 2],[1, 2, 1],[2, 1, 1],其中每个排列都出现两次。因此,我们需要对相同元素定义一种规则,使得其组成的排列不会形成重复的情况。
本题与第一题全排列的不同在于,本题有重复的元素,而且不能有重复的排列,所以我们在选元素的时候,当当前元素已经在前面选过,在本次中就不能再选,如下图分析,绿色X代表当前元素已经被选过;红色X代表当前元素前面已经选过,也不能选:
代码如下:
class Solution
{
vector<vector<int>> ret;
vector<int> path;
vector<bool> cheak;
public:
void dfs(vector<int>& nums, vector<int>& path)
{
if(nums.size() == path.size())
{
ret.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i++)
{
// 符合条件的分支
// 1、当前位置没被使用
// 2、当前位置和前一个位置元素不相同;如果相同,则判断前一个位置是否被使用了,使用了则合法,没使用则不合法
if(cheak[i] == false && (i == 0 || nums[i] != nums[i - 1] || cheak[i - 1] == true))
{
path.push_back(nums[i]);
cheak[i] = true;
dfs(nums, path);
path.pop_back();
cheak[i] = false;
}
}
}
vector<vector<int>> permuteUnique(vector<int>& nums)
{
sort(nums.begin(), nums.end());
cheak.resize(nums.size());
dfs(nums, path);
return ret;
}
};
5. 电话号码的字母组合
题目链接 -> Leetcode -17.电话号码的字母组合
Leetcode -17.电话号码的字母组合
题目:给定一个仅包含数字 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’] 的一个数字。
思路:每个位置可选择的字符与其他位置并不冲突,因此不需要标记已经出现的字符,只需要将每个数字对应的字符依次填入字符串中进行递归,在回溯是撤销填入操作即可。
代码如下:
class Solution
{
// 用数组建立对应数字映射的字母组合
const char* numStr[10] = { "","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz" };
public:
// digits 为题目给的数字组合;i 为组合成的字母组合的长度;combineStr 为目前的字母组合;ret 为返回的 vector
// 只要 i 满足长度了,就将 combineStr 尾插到 ret 中
void dfs(const string& digits, int i, string combineStr, vector<string>& ret)
{
if (i == digits.size())
{
ret.push_back(combineStr);
return;
}
// 取出每一位数字,然后将对应映射的字母组合放到 Str 中,遍历 Str,递归到下一层去取下一位数字对应映射的字母组合
const string Str = numStr[digits[i] - '0'];
for (auto ch : Str)
{
dfs(digits, i + 1, combineStr + ch, ret);
}
}
vector<string> letterCombinations(const string& digits)
{
vector<string> ret;
if (digits.size() == 0)
return ret;
int i = 0;
string combineStr;
dfs(digits, i, combineStr, ret);
return ret;
}
};
6. 括号生成
题目链接 -> Leetcode -22.括号生成
Leetcode -22.括号生成
题目:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例 1:
输入:n = 3
输出:[“((()))”, “(()())”, “(())()”, “()(())”, “()()()”]
示例 2:
输入:n = 1
输出:[“()”]
提示:
- 1 <= n <= 8
思路:从左往右进行递归,在每个位置判断放置左右括号的可能性,若此时放置左括号合理,则放置左括号继续进行递归,右括号同理。
一种判断括号是否合法的方法:从左往右遍历,左括号的数量始终大于等于右括号的数量,并且左括号的总数量与右括号的总数量相等。因此我们在递归时需要进行以下判断:
-
放入左括号时需判断此时左括号数量是否小于字符串总长度的一半(若左括号的数量大于等于字符串长度的一半时继续放置左括号,则左括号的总数量一定大于右括号的总数量);
-
放入右括号时需判断此时右括号数量是否小于左括号数量。
class Solution { vector<string> ret; string path; public: void dfs(int n, int right, int left) { if(path.size() == 2*n) { ret.push_back(path); return; } // 添加左括号 if(n > right) { path.push_back('('); dfs(n, right + 1, left); path.pop_back(); // 恢复现场 } // 添加右括号 if(right > left) { path.push_back(')'); dfs(n, right, left + 1); path.pop_back(); // 恢复现场 } } vector<string> generateParenthesis(int n) { int right = 0, left = 0; dfs(n, right, left); return ret; } };
7. 组合
题目链接 -> Leetcode -77.组合
Leetcode -77.组合
题目:给定两个整数 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]]
提示:
- 1 <= n <= 20
- 1 <= k <= n
思路:题目要求我们从 1 到 n 中选择 k 个数的所有组合,其中不考虑顺序。也就是说,[1,2] 和 [2,1] 等价。我们需要找出所有的组合,但不能重复计算相同元素的不同顺序的组合。对于选择组合,我们需要进行如下流程:
- 所有元素分别作为首位元素进行处理;
- 在之后的位置上同理,选择所有元素分别作为当前位置元素进行处理;
- 为避免计算重复组合,规定选择之后位置的元素时必须比前一个元素大,这样就不会有重复的组合([1,2] 和 [2,1] 中 [2,1] 不会出现)。
代码如下:
class Solution
{
vector<vector<int>> ret;
vector<int> path;
public:
void dfs(int n, int k, int pos)
{
if(path.size() == k)
{
ret.push_back(path);
return;
}
for(int i = pos; i <= n; i++)
{
path.push_back(i);
dfs(n, k, i + 1);
path.pop_back(); // 恢复现场
}
}
vector<vector<int>> combine(int n, int k)
{
dfs(n, k, 1);
return ret;
}
};
8. 目标和
题目链接 -> Leetcode -494.目标和
Leetcode -494.目标和
题目:给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1, 1, 1, 1, 1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
- -1 + 1 + 1 + 1 + 1 = 3
- +1 - 1 + 1 + 1 + 1 = 3
- +1 + 1 - 1 + 1 + 1 = 3
- +1 + 1 + 1 - 1 + 1 = 3
- +1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1
提示:
- 1 <= nums.length <= 20
- 0 <= nums[i] <= 1000
- 0 <= sum(nums[i]) <= 1000
- -1000 <= target <= 1000
思路:对于每个数,可以选择加上或减去它,依次枚举每一个数字,在每个数都被选择时检查得到的和是否等于目标值。如果等于,则记录结果。
需要注意的是,为了优化时间复杂度,可以提前计算出数组中所有数字的和 sum,以及数组的长度 len;这样可以快速判断当前的和减去剩余的所有数是否已经超过了目标值 target ,或者当前的和加上剩下的数的和是否小于目标值 target,如果满足条件,则可以直接回溯。
例如以下图例:
代码如下:
class Solution
{
int ret = 0;
public:
void dfs(vector<int>& nums, int target, int sum, int pos)
{
if(pos == nums.size())
{
if(target == sum) ret++;
return;
}
// 选正
dfs(nums, target, sum + nums[pos], pos + 1);
// 选负
dfs(nums, target, sum - nums[pos], pos + 1);
}
int findTargetSumWays(vector<int>& nums, int target)
{
int sum = 0;
dfs(nums, target, sum, 0);
return ret;
}
};
9. 组合总和
题目链接 -> Leetcode -39.组合总和
Leetcode -39.组合总和
题目:给你一个 无重复元素 的整数数组 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
输出 : []
提示:
- 1 <= candidates.length <= 30
- 2 <= candidates[i] <= 40
- candidates 的所有元素 互不相同
- 1 <= target <= 40
思路:candidates 的所有元素 互不相同,因此我们在递归状态时只需要对每个元素进行如下判断:
- 跳过,对下一个元素进行判断;
- 将其添加至当前状态中,我们在选择添加当前元素时,之后仍可以继续选择当前元素(可以重复选择同一元素)。
因此,我们在选择当前元素并向下传递下标时,应该直接传递当前元素下标。
代码如下:
class Solution
{
vector<vector<int>> ret;
vector<int> path;
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
int sum = 0;
dfs(candidates, target, sum, 0);
return ret;
}
void dfs(vector<int>& nums, int target, int sum, int pos)
{
// 返回条件
if(sum >= target)
{
if(sum == target) ret.push_back(path);
return;
}
// 枚举个数
for(int i = pos; i < nums.size(); i++)
{
path.push_back(nums[i]);
dfs(nums, target, sum + nums[i], i);
path.pop_back();
}
}
};
10. 字母大小写全排列
题目链接 -> Leetcode -784.字母大小写全排列
Leetcode -784.字母大小写全排列
题目:给定一个字符串 s ,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。
返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。
示例 1:
输入:s = “a1b2”
输出:[“a1b2”, “a1B2”, “A1b2”, “A1B2”]
示例 2:
输入: s = “3z4”
输出 : [“3z4”, “3Z4”]
提示 :
- 1 <= s.length <= 12
- s 由小写英文字母、大写英文字母和数字组成
思路:只需要对英文字母进行处理,处理每个元素时存在三种情况:
- 不进行处理;
- 若当前字母是英文字母并且是大写,将其修改为小写;
- 若当前字母是英文字母并且是小写,将其修改为大写。
代码如下:
class Solution
{
vector<string> ret;
string path;
public:
vector<string> letterCasePermutation(string s)
{
dfs(s, 0);
return ret;
}
void dfs(string s, int pos)
{
if(pos == s.size())
{
ret.push_back(path);
return;
}
// 不改变
path += s[pos];
dfs(s, pos + 1);
path.pop_back();
// 当是字母时改变
if(s[pos] > '9')
{
// 变大写
if(s[pos] >= 'a' && s[pos] <= 'z')
{
path += toupper(s[pos]);
dfs(s, pos + 1);
path.pop_back();
}
// 变小写
else
{
path += tolower(s[pos]);
dfs(s, pos + 1);
path.pop_back();
}
}
}
};
11. 优美的排列
题目链接 -> Leetcode -526.优美的排列
Leetcode -526.优美的排列
题目:假设有从 1 到 n 的 n 个整数。用这些整数构造一个数组 perm(下标从 1 开始),只要满足下述条件 之一 ,该数组就是一个 优美的排列 :
perm[i] 能够被 i 整除
i 能够被 perm[i] 整除
给你一个整数 n ,返回可以构造的 优美排列的数量 。
示例 1:
输入:n = 2
输出:2
解释:
第 1 个优美的排列是[1, 2]:
- perm[1] = 1 能被 i = 1 整除
- perm[2] = 2 能被 i = 2 整除
第 2 个优美的排列是[2, 1]:
-perm[1] = 2 能被 i = 1 整除 - i = 2 能被 perm[2] = 1 整除
示例 2:
输入:n = 1
输出:1
提示:
- 1 <= n <= 15
思路:我们需要在每一个位置上考虑所有的可能情况并且不能出现重复。通过深度优先搜索的方式,不断地枚举每个数在当前位置的可能性,并回溯到上一个状态,直到枚举完所有可能性,得到正确的结果。
我们需要定义一个变量用来记录所有可能的排列数量,一个一维数 visited 标记元素,然后从第一个位置开始进行递归;
代码如下:
class Solution
{
vector<bool> cheak;
int ret = 0;
public:
int countArrangement(int n)
{
cheak.resize(n+1);
dfs(1, n);
return ret;
}
void dfs(int pos, int n)
{
if(pos == n + 1)
{
ret++;
return;
}
for(int i = 1; i <= n; i++)
{
if(cheak[i] == false && (i % pos == 0 || pos % i == 0))
{
cheak[i] = true;
dfs(pos + 1, n);
cheak[i] = false; // 回溯
}
}
}
};
12. N皇后
题目链接 -> Leetcode -51.N皇后
Leetcode -51.N皇后
题目:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
示例 1:
输入:n = 4
输出: [[“.Q…”, “…Q”, “Q…”, “…Q.”], [“…Q.”, “Q…”, “…Q”, “.Q…”]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
示例 2:
输入:n = 1
输出: [[“Q”]]
提示:
- 1 <= n <= 9
思路:首先,我们在第一行放置第一个皇后,然后遍历棋盘的第二行,在可行的位置放置第二个皇后,然后再遍历第三行,在可行的位置放置第三个皇后,以此类推,直到放置了 n 个皇后为止。
我们需要用一个数组来记录每一行放置的皇后的列数。在每一行中,我们尝试放置一个皇后,并检查是否会和前面已经放置的皇后冲突。如果没有冲突,我们就继续递归地放置下一行的皇后,直到所有的皇后都放置完毕,然后把这个方案记录下来。
在检查皇后是否冲突时,我们可以用一个数组来记录每一列是否已经放置了皇后,并检查当前要放置的皇后是否会和已经放置的皇后冲突。对于对角线,我们可以用两个数组来记录从左上角到右下角的每一条对角线上是否已经放置了皇后,以及从右上角到左下角的每一条对角线上是否已经放置了皇
后。
对于对角线是否冲突的判断可以通过以下流程解决:
- 从左上到右下:相同对角线的行列之差相同;
- 从右上到左下:相同对角线的行列之和相同;
因此,我们需要创建用于存储解决方案的二维字符串数组 solutions ,用于存储每个皇后的位置的一维整数数组 queens ,以及用于记录每一列和对角线上是否已经有皇后的布尔型数组 columns 、 diagonals1 和 diagonals2.
代码如下:
class Solution
{
// 分别检查列、主对角线、副对角线
vector<bool> Cheakcol, Cheakdig1, Cheakdig2;
vector<vector<string>> ret;
vector<string> path;
int n;
public:
vector<vector<string>> solveNQueens(int _n)
{
n = _n;
Cheakcol.resize(n);
// Cheakdig1 和 Cheakdig2 需要开 2n 个空间,因为检查对角线的时候使用到数学知识 y = x + b(正对角线)和 y = -x + b(副对角线)
Cheakdig1.resize(n * 2);
Cheakdig2.resize(n * 2);
path.resize(n);
for (int i = 0; i < n; i++)
path[i].append(n, '.');
dfs(0); // row
return ret;
}
void dfs(int row)
{
if (row == n)
{
ret.push_back(path);
return;
}
// 尝试在这一行中放皇后
for (int col = 0; col < n; col++)
{
// 剪枝(检查这个位置可不可以放皇后)
if (!Cheakcol[col] && !Cheakdig1[row - col + n] && !Cheakdig2[col + row])
{
// 放皇后
Cheakcol[col] = Cheakdig1[row - col + n] = Cheakdig2[col + row] = true;
path[row][col] = 'Q';
// 这一行放了去下一行放
dfs(row + 1);
// 恢复现场
Cheakcol[col] = Cheakdig1[row - col + n] = Cheakdig2[col + row] = false;
path[row][col] = '.';
}
}
}
};
13. 有效的数独
注意:本题的思路不是回溯算法,只是为了提前适应下题解数独题目。
题目链接 -> Leetcode -36.有效的数独
Leetcode -36.有效的数独
题目:请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。
数字 1 - 9 在每一行只能出现一次。
数字 1 - 9 在每一列只能出现一次。
数字 1 - 9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
注意:
一个有效的数独(部分已被填充)不一定是可解的。
只需要根据以上规则,验证已经填入的数字是否有效即可。
空白格用 ‘.’ 表示。
示例 1:
输入: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”]]
输出:true
示例 2:
输入:board =
[[“8”, “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”]]
输出:false
解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。
提示:
- board.length == 9
- board[i].length == 9
- board[i][j] 是一位数字(1 - 9)或者 ‘.’
思路:创建三个数组标记行、列以及 3*3 小方格中是否出现 1~9 之间的数字即可。详细思路参考代码。
代码如下:
class Solution
{
bool row[9][10];
bool col[9][10];
bool gird[3][3][10];
public:
bool isValidSudoku(vector<vector<char>>& board)
{
for(int i = 0; i < board.size(); i++)
{
for(int j = 0; j < board[0].size(); j++)
{
// 判断当前数在这一行、这一列、3x3 宫内是否出现过
if(board[i][j] != '.')
{
int num = board[i][j] - '0';
// row[i][num] 表示第 i 行 num 这个数是否出现过
// col[j][num] 表示第 j 列 num 这个数是否出现过
// gird[i / 3][j / 3][num]:将 9x9 宫格每3行3列划分为一个宫格,一共划分为九个;
// gird[i / 3][j / 3][num] 表示第 i/3 行、第 j/3 列 的大宫格是否有 num 这个数
if(row[i][num] || col[j][num] || gird[i / 3][j / 3][num]) return false;
row[i][num] = col[j][num] = gird[i / 3][j / 3][num] = true;
}
}
}
return true;
}
};
14. 解数独
题目链接 -> Leetcode -37.解数独
Leetcode -37.解数独
题目:编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
数字 1 - 9 在每一行只能出现一次。
数字 1 - 9 在每一列只能出现一次。
数字 1 - 9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
示例 1:
输入: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”]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
提示:
- board.length == 9
- board[i].length == 9
- board[i][j] 是一位数字或者 ‘.’
- 题目数据 保证 输入数独仅有一个解
思路:为了存储每个位置的元素,我们需要定义一个二维数组。首先,我们记录所有已知的数据,然后遍历所有需要处理的位置,并遍历数字 1~9;对于每个位置,我们检查该数字是否可以存放在该位置,同时检查行、列和九宫格是否唯一。
我们可以使用一个二维数组来记录每个数字在每一行中是否出现,一个二维数组来记录每个数字在每一列中是否出现。对于九宫格,我们可以以行和列除以 3 得到的商作为九宫格的坐标,并使用一个三维数组来记录每个数字在每一个九宫格中是否出现。在检查是否存在冲突时,只需检查行、列和九宫格里对应的数字是否已被标记。如果数字至少有一个位置(行、列、九宫格)被标记,则存在冲突,因此不能在该位置放置当前数字。
特别地,在本题中,我们需要直接修改给出的数组,因此在找到一种可行的方法时,应该停止递归,以防止正确的方法被覆盖。
初始化定义:
- 定义行、列、九宫格标记数组以及找到可行方法的标记变量,将它们初始化为 false;
- 定义一个数组来存储每个需要处理的位置;
- 将题目给出的所有元素的行、列以及九宫格坐标标记为 true;
- 将所有需要处理的位置存入数组;
代码如下:
class Solution {
bool row[9][10];
bool col[9][10];
bool gird[3][3][10];
public:
void solveSudoku(vector<vector<char>>& board)
{
// 初始化原来宫格中的数独
for(int i = 0; i < 9; i++)
{
for(int j = 0; j < 9; j++)
{
if(board[i][j] != '.')
{
int num = board[i][j] - '0';
row[i][num] = col[j][num] = gird[i / 3][j / 3][num] = true;
}
}
}
dfs(board);
}
bool dfs(vector<vector<char>>& board)
{
for(int i = 0; i < 9; i++)
{
for(int j = 0; j < 9; j++)
{
if(board[i][j] == '.')
{
for(int num = 1; num <= 9; num++)
{
if(!row[i][num] && !col[j][num] && !gird[i / 3][j / 3][num])
{
row[i][num] = col[j][num] = gird[i / 3][j / 3][num] = true;
board[i][j] = num + '0';
// 如果当前位置是正确的,就说明前面的位置是正确的,就提前返回,不要让上一层继续遍历下去了
if(dfs(board)) return true;
// 否则,恢复现场
row[i][num] = col[j][num] = gird[i / 3][j / 3][num] = false;
board[i][j] = '.';
}
}
return false; // 1-9 填完了都没有返回,说明前面的位置有错,返回false
}
}
}
return true; // 两个for循环填完了就返回true
}
};
15. 单词搜索
题目链接 -> Leetcode -79.单词搜索
Leetcode -79.单词搜索
题目:给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。
同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [[“A”, “B”, “C”, “E”], [“S”, “F”, “C”, “S”], [“A”, “D”, “E”, “E”]], word = “ABCCED”
输出:true
示例 2:
输入:board = [[“A”, “B”, “C”, “E”], [“S”, “F”, “C”, “S”], [“A”, “D”, “E”, “E”]], word = “SEE”
输出:true
示例 3:
输入:board = [[“A”, “B”, “C”, “E”], [“S”, “F”, “C”, “S”], [“A”, “D”, “E”, “E”]], word = “ABCB”
输出:false
提示:
- m == board.length
- n = board[i].length
- 1 <= m, n <= 6
- 1 <= word.length <= 15
- board 和 word 仅由大小写英文字母组成
思路:我们需要假设每个位置的元素作为第一个字母,然后向相邻的四个方向进行递归,并且不能出现重复使用同一个位置的元素。通过深度优先搜索的方式,不断地枚举相邻元素作为下一个字母出现的可能性,并在递归结束时回溯,直到枚举完所有可能性,得到正确的结果。
代码如下:
class Solution
{
// 当前坐标的偏移量
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
// 检查当前路径的当前位置是否已经走过
bool Cheakpath[7][7];
int m, n;
public:
bool exist(vector<vector<char>>& board, string word)
{
m = board.size(), n = board[0].size();
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
// 先找到第一个字符的起始位置
if(board[i][j] == word[0])
{
// 当前位置标记 true
Cheakpath[i][j] = true;
// 如果当前位置的路径是正确的,返回true
if(dfs(board, i, j, word, 1)) return true;
// 否则恢复现场,继续试下一个位置
Cheakpath[i][j] = false;
}
}
}
// 两层循环没有返回true就返回false
return false;
}
bool dfs(vector<vector<char>>& board, int row, int col, string word, int pos)
{
if(pos == word.size()) return true;
// 通过偏移量依次试当前坐标的上下左右坐标
for(int k = 0; k < 4; k++)
{
int x = row + dx[k], y = col + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && !Cheakpath[x][y] && board[x][y] == word[pos])
{
Cheakpath[x][y] = true;
if(dfs(board, x, y, word, pos + 1)) return true;
Cheakpath[x][y] = false;
}
}
return false;
}
};
16. 黄金矿工
题目链接 -> Leetcode -1219.黄金矿工
Leetcode -1219.黄金矿工
题目:你要开发一座金矿,地质勘测学家已经探明了这座金矿中的资源分布,并用大小为 m* n 的网格 grid 进行了标注。
每个单元格中的整数就表示这一单元格中的黄金数量;如果该单元格是空的,那么就是 0。
为了使收益最大化,矿工需要按以下规则来开采黄金:
每当矿工进入一个单元,就会收集该单元格中的所有黄金。
矿工每次可以从当前位置向上下左右四个方向走。
每个单元格只能被开采(进入)一次。
不得开采(进入)黄金数目为 0 的单元格。
矿工可以从网格中 任意一个 有黄金的单元格出发或者是停止。
示例 1:
输入:grid = [[0, 6, 0], [5, 8, 7], [0, 9, 0]]
输出:24
解释:
[[0, 6, 0],
[5, 8, 7],
[0, 9, 0]]
一种收集最多黄金的路线是:9 -> 8 -> 7。
示例 2:
输入:grid = [[1, 0, 7], [2, 0, 6], [3, 4, 5], [0, 3, 0], [9, 0, 20]]
输出:28
解释:
[[1, 0, 7],
[2, 0, 6],
[3, 4, 5],
[0, 3, 0],
[9, 0, 20]]
一种收集最多黄金的路线是:1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7。
提示:
- 1 <= grid.length, grid[i].length <= 15
- 0 <= grid[i][j] <= 100
- 最多 25 个单元格中有黄金。
思路:枚举矩阵中所有的位置当成起点,来一次深度优先遍历,统计出所有情况下能收集到的黄金数的最大值即可。
代码如下:
class Solution
{
bool Cheak[15][15];
int dx[4] = { 0, 0, 1, -1 };
int dy[4] = { 1, -1, 0, 0 };
int ret, m, n;
public:
int getMaximumGold(vector<vector<int>>& grid)
{
m = grid.size(), n = grid[0].size();
// 依次枚举每个不为 0 的点开始开采
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
if (grid[i][j] != 0 && !Cheak[i][j])
{
Cheak[i][j] = true;
dfs(grid, i, j, grid[i][j]);
Cheak[i][j] = false;
}
}
}
return ret;
}
void dfs(vector<vector<int>>& grid, int row, int col, int path)
{
// 更新返回值
ret = ret > path ? ret : path;
// 上下左右四个方向
for (int k = 0; k < 4; k++)
{
int x = row + dx[k], y = col + dy[k];
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] != 0 && Cheak[x][y] == false)
{
Cheak[x][y] = true;
dfs(grid, x, y, path + grid[x][y]);
Cheak[x][y] = false;
}
}
}
};
17. 不同路径III
题目链接 -> Leetcode -980.不同路径III
Leetcode -980.不同路径III
题目:在二维网格 grid 上,有 4 种类型的方格:
1 表示起始方格。且只有一个起始方格。
2 表示结束方格,且只有一个结束方格。
0 表示我们可以走过的空方格。
-1 表示我们无法跨越的障碍。
返回在四个方向(上、下、左、右)上行走时,从起始方格到结束方格的不同路径的数目。
每一个无障碍方格都要通过一次,但是一条路径中不能重复通过同一个方格。
示例 1:
输入: [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 2, -1]]
输出:2
解释:我们有以下两条路径:
- (0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (1, 1), (1, 0), (2, 0), (2, 1), (2, 2)
- (0, 0), (1, 0), (2, 0), (2, 1), (1, 1), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (2, 2)
示例 2:
输入: [[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 2]]
输出:4
解释:我们有以下四条路径:
- (0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (1, 1), (1, 0), (2, 0), (2, 1), (2, 2), (2, 3)
- (0, 0), (0, 1), (1, 1), (1, 0), (2, 0), (2, 1), (2, 2), (1, 2), (0, 2), (0, 3), (1, 3), (2, 3)
- (0, 0), (1, 0), (2, 0), (2, 1), (2, 2), (1, 2), (1, 1), (0, 1), (0, 2), (0, 3), (1, 3), (2, 3)
- (0, 0), (1, 0), (2, 0), (2, 1), (1, 1), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (2, 2), (2, 3)
示例 3:
输入: [[0, 1], [2, 0]]
输出:0
解释:
没有一条路能完全穿过每一个空的方格一次。
请注意,起始和结束方格可以位于网格中的任意位置。
提示:
- 1 <= grid.length * grid[0].length <= 20
思路:对于四个方向,我们可以定义一个二维数组 next ,大小为 4 ,每一维存储四个方向的坐标偏移量(详见代码)。题目要求到达题目标位置时所有无障碍方格都存在路径中,我们可以定义一个变量记录 num 当前状态中剩余的未走过的无障碍方格个数,则当我们走到目标地点时只需要判断 num 是否为 0 即可;在移动时需要判断是否越界。文章来源:https://www.toymoban.com/news/detail-777100.html
代码如下:
class Solution
{
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
bool vis[20][20];
int ret, m, n, count;
public:
int uniquePathsIII(vector<vector<int>>& grid)
{
m = grid.size(), n = grid[0].size();
int bx = 0, by = 0;
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
if(grid[i][j] == 0)
{
count++; // 统计方格中 0 的个数
}
else if(grid[i][j] == 1)
{
bx = i; // 记录 1 的位置
by = j;
}
count += 2; // 加上起始和终点位置
dfs(grid, bx, by, 1);
return ret;
}
void dfs(vector<vector<int>>& grid, int row, int col, int path)
{
// 如果走到终点,判断路径的长度是否等于 count
if(grid[row][col] == 2)
{
if(path == count) ret++;
return;
}
// 判断当前位置的上下左右位置
for(int k = 0; k < 4; k++)
{
int x = row + dx[k], y = col + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && (grid[x][y] == 0 || grid[x][y] == 2))
{
vis[x][y] = true;
dfs(grid, x, y, path + 1);
vis[x][y] = false;
}
}
}
};
到了这里,关于【算法专题】回溯算法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!