回溯算法经典问题:
1.子集
class Solution {
List<List<Integer>> sub=new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
LinkedList<Integer> ans = new LinkedList<>();
backtrack(ans,nums,0);
return sub;
}
void backtrack(LinkedList<Integer> ans,int[] nums,int start){
//在 Java 中,因为都是值传递,对象类型变量在传参的过程中,复制的都是变量的地址。这些地址被添加到 res 变量,但实际上指向的是同一块内存地址,因此我们会看到 6 个空的列表对象。解决的方法很简单,在 res.add(path); 这里做一次拷贝即可
//即sub.add(ans)结果全是空的;所以要按照下面这样做一下复制。
sub.add(new LinkedList<Integer>(ans));
for(int i=start;i<nums.length;i++){
ans.add(nums[i]);
backtrack(ans,nums,i+1);
ans.removeLast();
}
}
}
2.组合
class Solution {
List<List<Integer>> ans=new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
LinkedList<Integer> curr=new LinkedList<>();
backtrack(n,k,curr,1);
return ans;
}
void backtrack(int n,int k,LinkedList<Integer> curr,int start){
if(curr.size()==k){
ans.add(new LinkedList<Integer>(curr));
}
for(int i=start;i<=n;i++){
curr.add(i);
backtrack(n,k,curr,i+1);
curr.removeLast();
}
}
}
这就是典型的回溯算法,k
限制了树的高度,n
限制了树的宽度,直接套我们以前讲过的回溯算法模板框架就行了.
3.排列
class Solution {
List<List<Integer>> res = new LinkedList<>();
/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
// 触发结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track)); //这里是把track的值赋给一个新的linkedlist并返回这个新的
//每次这样返回完,track本身会逐次removelast
return;
}
for (int i = 0; i < nums.length; i++) {
// 排除不合法的选择
if (track.contains(nums[i]))
continue;
// 做选择
track.add(nums[i]);
// 进入下一层决策树
backtrack(nums, track);
// 取消选择
track.removeLast();
}
}
}
回溯模板依然没有变,但是根据排列问题和组合问题画出的树来看,排列问题的树比较对称,而组合问题的树越靠右节点越少。
在代码中的体现就是,排列问题每次通过 contains
方法来排除在 track
中已经选择过的数字;而组合问题通过传入一个 start
参数,来排除 start
索引之前的数字。
以上,就是排列组合和子集三个问题的解法,总结一下:
子集问题可以利用数学归纳思想,假设已知一个规模较小的问题的结果,思考如何推导出原问题的结果。也可以用回溯算法,要用 start
参数排除已选择的数字。
组合问题利用的是回溯思想,结果可以表示成树结构,我们只要套用回溯算法模板即可,关键点在于要用一个 start
排除已经选择过的数字。
排列问题是回溯思想,也可以表示成树结构套用算法模板,不同之处在于使用contains
方法排除已经选择的数字,前文有详细分析,这里主要是和组合问题作对比。
对于这三个问题,关键区别在于回溯树的结构,不妨多观察递归树的结构,很自然就可以理解代码的含义了。
是用contains排除还是用start参数排除主要看结果中能否存在不同顺序,即[1,2]和[2,1]能否共存,若能共存则用contains,不能共存用start,contains只能保证同一个数组中不会出现重复数字,start参数则直接不再考虑那个数字了。
4.处理合法括号问题:leet22
class Solution {
List<String> ans=new LinkedList<>();
public List<String> generateParenthesis(int n) {
StringBuffer curr=new StringBuffer();
backtrack(n,n,curr);
return ans;
}
void backtrack(int left,int right,StringBuffer curr){
//这里是通过剩下的left和right括号的数量来剪枝
if(left==0&&right==0){
ans.add(curr.toString());
return;
}
if(left<0||right<0){
return;
}
if(left>right){
return;
}
//其实相当于 for in ['(',')'],但因为只有两种情况所以直接分开写
curr.append('(');
backtrack(left-1,right,curr);
curr.deleteCharAt(curr.length() - 1);
curr.append(')');
backtrack(left,right-1,curr);
curr.deleteCharAt(curr.length() - 1);
}
}
括号问题可以简单分成两类,一类是判断括号合法性的,我放在次条了 ;一类是合法括号的生成,本文介绍。对于括号合法性的判断,主要是借助「栈」这种数据结构,而对于括号的生成,一般都要利用回溯递归的思想
有关括号问题,你只要记住两个个性质,思路就很容易想出来:
1、一个「合法」括号组合的左括号数量一定等于右括号数量,这个显而易见。
2、对于一个「合法」的括号字符串组合****p
,必然对于任何0 <= i < len(p)
都有:子串p[0..i]
中左括号的数量都大于或等于右括号的数量。
这个命题和题目的意思完全是一样的对吧,那么我们先想想如何得到全部2^(2n)
种组合,然后再根据我们刚才总结出的合法括号组合的性质筛选出合法的组合,不就完事。对于2n
个位置,必然有n
个左括号,n
个右括号,所以我们不是简单的记录穷举位置i
,而是用left记录还可以使用多少个左括号,用right记录还可以使用多少个右括号,这样就可以通过刚才总结的合法括号规律进行筛选了。