回溯问题作为面试算法中经典问题之一,同时也是很容易总结出一套固定解题模板的算法类别,这里使用Leetcode中top-100 liked 为例,并尝试在解题过程中总结出对应的解题思路和解题模板
重点:
1.对于回溯问题最重要的一点就是在 foward—>backtrack 这一过程状态的变化 例如从一个状态i 向前进行若干操作之后回溯到状态i 此时要把对应的其他改变的值恢复到状态i
2.对于回溯使用的场景大多是穷尽所有的情况 同时又不易通过遍历将情况一一穷举出来的时候。
示例1
Letter Combinations of a Phone Number
Given a string containing digits from 2-9 inclusive, return all possible letter combinations that the number could represent.
Input: "23"
Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
class Solution {
List<String> result=new ArrayList<>();
Map<String, String> phone = new HashMap<String, String>() {{
put("2", "abc");
put("3", "def");
put("4", "ghi");
put("5", "jkl");
put("6", "mno");
put("7", "pqrs");
put("8", "tuv");
put("9", "wxyz");
}};
public List<String> letterCombinations(String digits) {
if(digits.length()==0){
return result;
}
backTrack(digits,0,"");
return result;
}
public void backTrack(String digits,int i,String cur){
if(i==digits.length()){
result.add(cur);
return;
}
String tmp=phone.get(digits.substring(i,i+1));
for(int j=0;j<tmp.length();j++){
cur=cur+tmp.substring(j,j+1);
backTrack(digits,i+1,cur);
cur=cur.substring(0,cur.length()-1);
}
}
}
这里刨除本题特有的 例如map部分不看 可以得到一个此类题的解题框架
首先如果是要返回一个list 可以定义全局变量 List<所需类型> result=new ArrayList<>(); 用来保存最终的结果
然后定义一个backTrack函数
public void backTrack(String digits ,int index,String cur) // 对于输出list类型的题 backTrack返回值类型为void ,第一个参数设定为输入的要进行遍历的数据,第二个参数设定为遍历到原始数据digits的第几个位置 一般使用index记录forward的深度 同时利用index作为返回的判断依据 ,第三个参数作为forward为 index 深度时临时得到的结果 ,之后继续forward 可以在此结果的基础上进行操作
public void backTrack(String digits,int index,String cur){
// 终止条件判断
if(index==digits.length()){
result.add(cur);//将当前的中间状态作为最后的终止状态
return;
}
String tmp=phone.get(digits.substring(i,i+1));
for(int j=0;j<tmp.length();j++){
cur=cur+tmp.substring(j,j+1); //修改中间状态
backTrack(digits,i+1,cur); //forward
cur=cur.substring(0,cur.length()-1);// 将中间状态修改回forward之前的状态
}
}
示例二
Generate Parentheses
Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.
For example, given n = 3, a solution set is:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
下面可以使用上述总结的方法进行实践
和上述问题略有不同的是 修改中间状态到forward之前的状态不用把( 去掉 而是直接在其后增加)
class Solution {
List<String> result=new ArrayList<>();
public List<String> generateParenthesis(int n) {
backTrack("",0,0,n);
return result;
}
public void backTrack(String cur,int left,int right,int n){
if(cur.length()==2*n){
result.add(cur);
return;
}
if(left<n){
backTrack(cur+"(",left+1,right,n);
}
if(right<left){
backTrack(cur+')',left,right+1,n);
}
}
}
示例三
全排列问题 有重复数字的全排列和无重复数字的全排列
全排列问题也是一个典型的回溯问题 该问题解题的关键点就是如何在每一轮全排列的时候把已经放入到list中的数据不再进行排列,对于没有重复数字的情况而言,可以通过查看list是否contains该数字 有就跳过 ,对于有重复数字的全排列情况而言 可以通过一个boolean数组来保存每一个数字被选择的情况
class Solution {
private List<List<Integer>> result=new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
if(nums.length==0){
return result;
}
ArrayList<Integer> tmp=new ArrayList<>();
backTrack(nums,tmp);
return result;
}
public void backTrack(int[]nums,ArrayList<Integer> tmp){
if(tmp.size()==nums.length){
result.add(new ArrayList<>(tmp));
return;
}
for(int i=0;i<nums.length;i++){
boolean isContain=false;
if(tmp.contains(nums[i])){
isContain=true;
}
if(!isContain){
tmp.add(nums[i]);
backTrack(nums,tmp);
tmp.remove(tmp.size()-1);
}
}
}
}
#有重复的全排列
class Solution {
List<List<Integer>> result=new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
ArrayList<Integer> tmp=new ArrayList<>();
boolean[] isUsed=new boolean[nums.length];
Arrays.sort(nums);
backTrack(nums,isUsed,tmp);
return result;
}
public void backTrack(int[] nums,boolean[] isUsed,ArrayList<Integer> tmp){
if(tmp.size()==nums.length){
result.add(new ArrayList<>(tmp));
return;
}
for(int i=0;i<nums.length;i++){
if(isUsed[i]==true){
continue;
}
if(i>0&&nums[i]==nums[i-1]&&!isUsed[i-1]){ # 这句话是关键可以保证有序数组不会以完全相同的状态出现两次
continue;
}
tmp.add(nums[i]);
isUsed[i]=true;
backTrack(nums,isUsed,tmp);
tmp.remove(tmp.size()-1);
isUsed[i]=false;
}
}
}