“回溯”相关题型

1、符串全排列问题

打印一个字符串的全排列(比如abc的排列包括:abc,cab,cba,,,,),剑指offer38

又或者,给一串数字,求这串数字的全排列,比如:leetcode46

例子

思路1、首先可以分成两种情况:一种是里面没有重复的字符,一种是有重复的字符(只需要加一个hashset,每次查看需要交换的在set里面是不是已经存在)。整体的结构是:交换其中某两位,然后继续递归后面的,然后在交换回来(恢复交换的 元素),准备迎接下一轮。

过程就如下图所示:

示意图
public ArrayList<String> res;
    public String[] permutation(String s) {
        if(s==null||s.length()<1)
            return null;
        res=new ArrayList<>();
        char[] chas=s.toCharArray();
        process(chas,0);
        return res.toArray(new String[res.size()]);
    }
    public void process(char[] chas,int index){
        if(index==chas.length)
           res.add(String.valueOf(chas));
        HashSet<Character> set=new HashSet<>();//防止重复
        for(int i=index;i<chas.length;i++){
            if(!set.contains(chas[i])){
                set.add(chas[i]);
                swap(chas,index,i);
                process(chas,index+1);
                 swap(chas,index,i);
            }    
        } 
    }
    public void swap(char[] chas,int i,int j){
        char tmp=chas[i];
        chas[i]=chas[j];
        chas[j]=tmp;
    }

思路2、用举的第二个例子来说明。这里不使用上面的交换来实现,而是将每种可能性组成一个链表(LinkedList),每次更新链表的时候查询链表里面存不存在这个元素,如果存在了直接跳过,示意图如下:

public List<List<Integer>> permute(int[] nums) {
        if(nums==null||nums.length<1)
            return null;
        List<List<Integer>> res=new LinkedList<>();//存放结果   
        backtrack(nums,res,new LinkedList<Integer>());
            return res;
        }
        public void backtrack(int[] nums,List<List<Integer>> res,LinkedList<Integer> tmp ){
            if(tmp.size()==nums.length)
                res.add(new LinkedList(tmp));
            for(int i=0;i<nums.length;i++){
                if(tmp.contains(nums[i]))
                    continue;
                tmp.add(nums[i]);
                backtrack(nums,res,tmp);
               tmp.removeLast();
            }
        }

2、子集问题

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。
leetcode78

思路1:首先也是类似dfs递归的方法,每个位置都可以选择要,或者不要,但是需要注意的一个点是因为我们重复利用了同一个List,因此我们每次add之后别忘记remove

public List<List<Integer>> subsets1(int[] nums) {
        if(nums==null||nums.length<1){
            return new ArrayList<>();
        }
        res=new ArrayList<>();
        process(nums,0,new ArrayList<Integer>());
        return res;
}
    public void process(int[] arr, int index,List<Integer> tmp){
        if(index==arr.length)
           {
                res.add( new ArrayList<>(tmp));
                return ;
           } 
        //这里选择了重复利用tmp,没有复制一个新的放入递归里面
        process(arr,index+1,tmp);
        tmp.add(arr[index]);
        process(arr,index+1,tmp);
        tmp.remove(tmp.size()-1);//因此最后需要删除当前元素 
    }

思路2:使用回溯算法,模板如下:

这题的数组的长度没有限制,因此每次递归的一开始可以直接把当前结果放入res。不需要等到数的叶子节点。如果像上面一题长度有限制,也就是必须到达叶子节点,那么就不能这样。

 public List<List<Integer>> res;
    public List<List<Integer>> subsets(int[] nums) {
        if(nums==null||nums.length<1){
            return new ArrayList<>();
        }
         res=new ArrayList<>();
        backtrack(0,nums,new ArrayList<Integer>());
         return res;
    }
     private void backtrack(int i, int[] nums,  ArrayList<Integer> tmp) {
            res.add(new ArrayList<>(tmp));//直接返回
        for (int j = i; j < nums.length; j++) {
            tmp.add(nums[j]);//需要这一位的数据
            backtrack( j + 1,nums, tmp);
            tmp.remove(tmp.size() - 1);//不需要这一位,直接恢复,等待下一个候选人
        }
     }

使用回溯算法的示意图:

类似的问题:打印字符串的全部子序列

打印一个字符串的全部子序列,包括空字符串(abc的子序列包括a,ab,abc,bc,,,,)

思路1:使用树的思想,每一位都可以选择要或者不要,因为使用的是String,每次添加字符都会自动新生成一个String对象,不是共用一个对象,所以不需要每次使用完之后删除。

思路2:使用上面的回溯模型,因为没有规定相同的长度,所以不必等到叶子节点才返回。每一轮直接就可以返回。


3、组合问题

输入两个数字 n, k,算法输出 [1…n] 中 k 个数字的所有组合。比如输入 n = 4, k = 2,输出如下结果,顺序无所谓,但是不能包含重复(按照组合的定义,[1,2] 和 [2,1] 也算重复):leetcode77

使用回溯算法的时候,示意图如下:

 public List<List<Integer>> combine(int n, int k) {
        if(n<1||k<1)
            return new ArrayList<>();
        List<List<Integer>> res=new ArrayList<>();
        backtrack(n,1,k,res,new ArrayList<Integer>());
        return res;
    }
    public void backtrack(int n,int index,int k,List<List<Integer>> res,List<Integer> tmp){
        if(tmp.size()==k)
            res.add(new ArrayList<>(tmp));
        for(int i=index;i<=n;i++){
            tmp.add(i);
            backtrack(n,i+1,k,res,tmp);
            tmp.remove(tmp.size()-1);
        }

    }

总结

这三类题目非常相似,但是又略有不同,只要记住了回溯的那个框架,其实都能写出来,需要注意的是两个问题:

第一个:这个要求的子序列是不是长度固定的,如果不是,那么在回溯里面直接就可以加入结果集,而不需要等到长度一定(到达叶子节点)时才能返回结果。

第二个:如果其中暂存每种结果的集合是公用的,每次add之后必须remove。

最重要的还是脑海里面有一张上面的二叉树的那张图,有了这个画面就好写了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值