赞
踩
回溯算法的基础是递归,如果你对递归还不熟悉的话,建议先去看看递归的概念,做一些递归的练习题,也可以看我之前写的递归的文章:递归算法详解
回溯算法其实是一种穷举的搜索方法,利用回溯解决问题就是在穷举所有的可能,然后找到我们想要的答案。使用回溯算法时需要考虑如下的三个问题:
(1) 可选列表:所有可作出的选择;
(2) 已选列表:也就是之前已经做出的选择;
(3) 结束条件:当已选列表满足题目条件,可以结束穷举;
vector<elemType> res = {}
void backtrace(可选列表, 已选列表){
if(到达结束条件){
res.push_back(已选列表);
return;
}
for(选择 : 选择列表){
排除无效的选择(剪枝)
做选择;
backtrace(可选列表, 已选列表);
撤销选择;
}
}
[注]:上述框架中剪枝的意思是对已经访问过或者不符合题意的元素进行跳过
这类题型的题有:
LeetCode 46. 全排列
LeetCode 51. N皇后
LeetCode 52. N皇后II
LeetCode 37. 解数独
解题模板:
vector<vector<int>> res; //存放结果 vector<int> visited; //用来标记已经填过的数 void backtrack(vector<int>& nums, vector<int> track){ if(track.size() == nums.size()){ //结束条件 res.push_back(track); return; } for(int i = 0; i < nums.size(); i++){ if(visited[i]) continue; //剪枝,访问过的元素就跳过,visited[i]也可以是一个返回值为bool值的函数来代替 track.push_back(nums[i]); //做选择 visited[i] = 1; //标记为已访问过 backtrack(nums, track); track.pop_back(); //撤销选择 visited[i] = 0; //标记为未访问过 } } vector<vector<int>> problem(vector<int>& nums) { visited.resize(nums.size(), 0); //全部值设置为0,表示都未访问过 vector<int> track; //初始化路径为空,即track=[] backtrack(nums, track); return res; }
由于题目所给数组中会包含重复的元素,因此如果直接按照框架进行回溯结果中会包含重复的答案,举个例子,假定给定的数组为 n u m s = [ 1 , 3 , 3 , 4 ] nums = [1, 3, 3, 4] nums=[1,3,3,4],则遍历的结果中就会包含两个 [ 3 , 3 , 1 , 4 ] [3, 3, 1, 4] [3,3,1,4],其中第一个 [ 3 , 3 , 1 , 4 ] [3, 3, 1, 4] [3,3,1,4]中的首元素3是原数组位置2上的3,第二个 [ 3 , 3 , 1 , 4 ] [3, 3, 1, 4] [3,3,1,4]中的首元素3是原数组位置3上的3,这样穷举结果中就会包含重复的结果,我们要避免这样的情况首先需要先对数组排序,然后对重复数字第2次,第3次,…,第n次的出现进行剪枝,下面是代码模板:
vector<vector<int>> res; //存放结果 vector<int> visited; //用来标记已经填过的数 void backtrack(vector<int>& nums, vector<int> track){ if(track.size() == nums.size()){ //结束条件 res.push_back(track); return; } for(int i = 0; i < nums.size(); i++){ //避免重复的核心是要在判断条件中加上!visited[i-1], 因为按照从左到右的访问顺序是nums[i-1]先访问, //再到nums[i],而当这个遍历完成后就会撤销选择,也就是visited[i-1]恢复为0,然后先访问nums[i]再访 //问nums[i-1],此时通过visited[i-1]=0来剪枝就能避免重复的结果 if((i > 0 && nums[i] == nums[i-1] && !visited[i-1]) || visited[i]) continue; track.push_back(nums[i]); //做选择 visited[i] = 1; //标记为已访问过 backtrack(nums, track); track.pop_back(); //撤销选择 visited[i] = 0; //标记为未访问过 } } vector<vector<int>> problem(vector<int>& nums) { sort(nums.begin(), nums.end()); //排序数组 visited.resize(nums.size(), 0); //全部值设置为0,表示都未访问过 vector<int> track; //初始化路径为空,即track=[] backtrack(nums, track); return res; }
这类题型的题目有:
LeetCode 47. 全排列II
子集重复的意思是说:当我们有数组 n u m s = [ 1 , 2 , 3 ] nums = [1,2,3] nums=[1,2,3]时,按照框架去穷举的话结果中同时包含子集 [ 1 , 3 ] [1,3] [1,3]和子集 [ 3 , 1 ] [3,1] [3,1],也会同时包含子集[1,2,3]、[1,3,2]、[2,1,3]、[2,3,1]、[3,1,2]、[3,2,1],其实上述子集中的元素是相同的,只是顺序不同,而有的题目不允许出现这样的结果,因此要避免子集重复。避免子集重复可以通过在穷举过程中避免回头实现,具体来说就是可以按顺序对每个位置进行穷举(选择当前位置或者不选择当前位置),这样结果中元素的前后位置就是固定:
vector<vector<int>> res; //存放结果 //不需要辅助的visited数组,因为这里是按从前到后遍历的,也就是说不会访问之前已经访问过的元素 void backtrack(vector<int>& nums, vector<int> track,int cur){//这里多了一个指示当前位置的参数cur if(cur == nums.size()){ //结束条件 res.push_back(track); return; } //选择当前位置 track.push_back(nums[i]); backtrack(nums, track, cur+1); track.pop_back(); //不选择当前位置 backtrack(nums,track, cur+1); } vector<vector<int>> problem(vector<int>& nums) { vector<int> track; //初始化路径为空,即track=[] backtrack(nums, track, 0); return res; }
这类题型的题目有:
LeetCode 39. 组合总和
LeetCode 77. 组合
LeetCode 78. 子集
LeetCode 216. 组合总和III
这一类问题相当于2和3的结合体,因此解题首先需要对数组进行排序,然后要按位置穷举,同时做好剪枝:
vector<vector<int>> res; vector<int> visited; void backtrack(vector<int>& nums, vector<int> &track, int cur){ if(cur == nums.size()){ if(结束条件) res.push_back(track); return; } if(!(cur > 0 && nums[cur] == nums[cur-1] && !visited[cur-1])){//如果以前穷举过相同数字,则会跳过当前元素的选择 track.push_back(nums[cur]); visited[cur] = 1; backtrack(nums, track, cur+1); track.pop_back(); //撤销选择 visited[cur] = 0; } backtrack(nums, track, cur+1); }
这类题型的题目有:
LeetCode 40. 组合总和II
LeetCode 90. 子集II
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。