回溯法的本质就是在递归过程中将生成和检查过程有机结合起来,从而减少不必要的枚举,是一种系统性的搜索算法。只要可以把问题分解为不太多的步骤,每个步骤含有不太多的选择,均可以使用回溯法(有超时的风险)。
回溯法从根节点出发,以深度优先搜索方式搜索整个解空间。回溯法以这种工作方式递归地在解空间中搜索,直到找到所要求的解或解空间所有解都被遍历过为止。
伪代码:
void dfs(int curr_state)
{
if(到达基例) return;
// 1. 进行尝试操作,当前状态转为next_state
opeartion(curr_state);
// 2. 进入下一层深搜6
dfs(next_state);
// 3. 子过程返回后将状态还原为curr_state
rev_opeartion(next_state);
}
题目描述:
给定一个 没有重复 数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
算法思想:
数字的全排列,意味着我们需要遍历所有的解空间。数学知识告诉我们 N N N 个数字的全排列有 N ! N! N! 种情况。
假设当前已经确定好排列的字符串为 S ( i , j ) S(i,j) S(i,j),那么只需要在 j + 1 j+1 j+1位置枚举出现的所有的可能的情况。所以这个题目也是一个树型问题,我们只需要按顺序枚举每一位可能出现的情况,已经选择的数字在接下来要确定的数字中不能出现。按照这种策略选取就能够做到不重不漏,把可能的全排列都枚举出来。
枚举每一位可能出现的情况,其实就是一个回溯过程,假设当前位置为K
,将这个状态传递下去,得到这个状态下所有的字符串。然后再将当前位置改为K'
,继续传递下去。
算法步骤:
start
为当前遍历到的位置,从start至结尾枚举下一位置i
start
与i
交换位置,进入下一位置,重复步骤1代码:
vector<vector<int>> permute(vector<int>& nums)
{
if(nums.size() == 0) return vc;
int len = nums.size();
permutehelp(nums,0,len);
return vc;
}
void permutehelp(vector<int>& nums,int start,int end)
{
if(start == end)
{
vc.push_back(nums);
return;
}
for(int i=start;i<end;i++)
{
swap(nums,start,i);
permutehelp(nums,start+1,end);
swap(nums,start,i);
}
}
题目描述:
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
算法思想:
此题思想与上一题一样,唯一多加一点的地方就是去除重复。如何去除重复?
如果当前交换的位置元素与之前位置元素值相同(前面出现过),则直接丢弃这一次的枚举结果。
这样一来,就不会存在重复排列了
算法步骤:
start
为当前遍历到的位置,从start至结尾枚举下一位置i
i
位置与之前位置元素不同,将位置start
与i
交换位置,进入下一位置,重复步骤1 (这里没遍历一个字符,就加入set中,后续有相同字符,就会被检测出来)
代码:
vector<vector<int>> permuteUnique(vector<int>& nums)
{
if(nums.size() == 0) return vc;
int len = nums.size();
permutehelp(nums,0,len);
return vc;
}
void permutehelp(vector<int>& nums,int start,int end)
{
if(start == end)
{
vc.push_back(nums);
return;
}
unordered_set<int> uniq;
for(int i=start;i<end;i++)
{
if(uniq.count(nums[i]))
{
continue;
}
swap(nums,start,i);
permutehelp(nums,start+1,end);
swap(nums,start,i);
uniq.insert(nums[i]);
}
}
题目描述:
你有 4 张写有 1 到 9 数字的牌。你需要判断是否能通过 *
,/
,+
,-
,(
,)
的运算得到 24。
示例:
输入: [4, 1, 8, 7]
输出: True
解释: (8-4) * (7-1) = 24
算法思想:
需要枚举四个数以及对应的操作,从四个数选择两个数进行操作,然后放回。然后从三个数选择两个进行操作,放回。最后将剩下两个数进行运算,判断答案是否出现24点。
上述过程中,需要不断改变的就是两个数之间的操作,这就是需要回溯的点。只需要从序列中枚举两个点,然后试探每一种操作,寻找到是否有满足条件的情况即可。
算法步骤:
代码:
bool judgePoint24(vector<int>& nums)
{
bool res = false;
double eps = 0.001;
vector<double> arr(nums.begin(), nums.end());
helper(arr, eps, res);
return res;
}
void helper(vector<double>& nums, double eps, bool &res)
{
// 因为传递的是引用,如果有一个例子找到了,那么剩下的过程都会直接返回,提前结束
if (res) return;
if (nums.size() == 1)
{
if (abs(nums[0] - 24) < eps)
{
res = true;
}
return;
}
// 枚举两个数
for (int i = 0; i < nums.size(); ++i)
{
for (int j = 0; j < i; ++j)
{
double p = nums[i], q = nums[j];
// 枚举这两个数所有可能的操作
vector<double> t{p + q, p - q, q - p, p * q};
//除数不为0
if (p > eps)
{
t.push_back(q / p);
}
if (q > eps)
{
t.push_back(p / q);
}
// 下面是回溯部分代码
nums.erase(nums.begin() + i);
nums.erase(nums.begin() + j);
for (double k : t)
{
nums.push_back(k);
helper(nums, eps, res);
nums.pop_back();
}
nums.insert(nums.begin() + j, q);
nums.insert(nums.begin() + i, p);
}
}
}
}
题目描述:
给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列。
转换需遵循如下规则:
每次转换只能改变一个字母。
转换后得到的单词必须是字典中的单词。
说明:
如果不存在这样的转换序列,返回一个空列表。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例:
输入:
beginWord = “hit”,
endWord = “cog”,
wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]输出:
[
[“hit”,“hot”,“dot”,“dog”,“cog”],
[“hit”,“hot”,“lot”,“log”,“cog”]
]
算法思想:
一次转化可以改变一个字母,从一个单词找到另一个单词。把单词看成图的节点,单词转化就变成了图的节点之间存在一条边。题目就变成了从源节点到目标节点的单元最短路径。
从源节点开始,尝试一次改变可以到达的节点,然后依次递推,直到到达目标节点。虽然这样可以找到路径,但是无法确定最短路径。有一种做法就是将路径长度也作为参数传递下去,到达基例时比较长短,更新结果。这样虽然不失为一种解决方法,但是会遍历所有的路径,所以很不幸的超时了!
那么如何改进呢?说到最短,我们就可以想到BFS算法,而且这个图里边的权值可以当成1来处理。因此对于这个图,首先使用BFS确定图中的每个节点所在的层次(距离根节点的距离)。同时通过BFS就可以判断从源点是否存在去往根节点的路径。
然后,就可以寻找路径了。可以从源节点开始,按照层次递增的规矩向下遍历。也可以从目的节点开始,按照层次递减的规矩向上遍历。但是无论是那种方式,都要尝试可能的路径,这样一来就无法避免的要使用回溯。
算法步骤:
代码:
class Solution {
public:
// 判断两个单词是否一步转化
bool sim(const string& s1, const string& s2) {
int diff = 0;
for (int i = 0; i < s1.size(); ++i) {
diff += s1[i] != s2[i];
}
return diff <= 1;
}
// 从目的节点开始 反向深搜 找到路径
void dfs(const vector<vector<int> >& g, const vector<int>& dfn, const vector<string>& wordList,
int i, vector<string>& path, vector<vector<string> >& paths) {
if (dfn[i] == 0) {
vector<string> v(path);
reverse(v.begin(), v.end());
paths.push_back(v);
return;
}
for (auto j : g[i]) {
if (dfn[j] == dfn[i] - 1) {
// 回溯过程 找到上一层的一个路径
path.push_back(wordList[j]);
dfs(g, dfn, wordList, j, path, paths);
path.pop_back();
}
}
}
vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
wordList.push_back(beginWord);
int N = wordList.size();
vector<vector<int> > g(N);
int endi = -1;
// 构图
for (int i = 0; i < N; ++i) {
if (wordList[i] == endWord) endi = i;
for (int j = i + 1; j < N; ++j) {
if (sim(wordList[i], wordList[j])) {
g[i].push_back(j);
g[j].push_back(i);
}
}
}
if (endi == -1) return {};
// 层级编号
vector<int> dfn(N, -1);
queue<int> q;
q.push(N - 1);
dfn[N - 1] = 0;
while (!q.empty()) {
auto i = q.front();
q.pop();
for (auto j : g[i]) {
if (dfn[j] == -1) {
dfn[j] = dfn[i] + 1;
q.push(j);
}
}
}
if (dfn[endi] == -1) return {};
// 回溯路径
vector<string> path{endWord};
vector<vector<string> > paths;
dfs(g, dfn, wordList, endi, path, paths);
return paths;
}
};