LeeteCode 之 递归回溯法总结(组合问题)

首先参考下面的原题,关于电话号码摁键上的字母的组合问题:Leet Code 17

https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/description/

本意是根据输入的字符串形式的数字,对应数字输出所有的可能性的组合,当然首先想到的可以通过循环的方式进行,建立好对照的简单的字典表之后就可以通过循环索引对应的按键上的字符进行组合即可,但是仔细分析之后却发现,这个问题没有那么简单,因为输入的数字字符串的长短不确定,也就是说循环遍历的嵌套层数不确定,这时候我们遇到了瓶颈。今天通过这个问题使用的回溯法,并通过这个例题的分析,的到其使用的条件,以及出现的问题。

换又说回来,那这个问题我们怎么解决呢?

可以参考下面的解决方案:

class Solution {
    public List<String> letterCombinations(String digits) {
        ArrayList<String> result = new ArrayList<String>();
        if(digits == null || digits.length() == 0){
            return result;
        }        
        String[] phoneNumbers = {" ", " ", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};  
        dfsPhoneNum(digits,0,phoneNumbers,result,new StringBuffer());
        return result;
    }  
  
    //深搜加递归
    private void dfsPhoneNum(String digits, int index,String[] phoneNumbers,List<String> list, StringBuffer buffer){
        
        //当累计当数字字符串中字符的总数时,将此事的buffer中的内容转换为字符串添加到list列表
        if(index == digits.length()){
            list.add(buffer.toString());
            return ;
        }
        
        //获取数字字符串中第index位置的字符串对应的数字内容,并索引到phoneNumber字符串数字中对应位置的字符串
        String phoneNumber = phoneNumbers[digits.charAt(index) - '0'];
        for(int i = 0; i < phoneNumber.length();i++){
            //先将每个数字对应的字符串中的第一个字符按照顺序添加进来
            buffer.append(phoneNumber.charAt(i));
            //然后将index + 1指向的字符串中的添加进来
            dfsPhoneNum(digits,index + 1,phoneNumbers,list,buffer);
            //当添加完之后,其实当遇到上面的终止条件时才能停下,当添加之后也就挺下了,
            //执行上面的终止条件,然后跳出到上一层的循环中,此时,循环将digits.length - 1 个的
            //字符串中的第二个字符串添加到buffer里面,然后在执行上面的语句,最后在执行终止条件
            //最外层的是最先添加的index = 0 时的字符串的第一个字符,然后按照循环再次进行上面的过程
            buffer.deleteCharAt(buffer.length() - 1);
        }
    }

}

可以看到,在这个dfs方法中,深度是有index来控制的,index控制的深度和digits字符串的长度是相同的,所以类似于一个循环遍历的过程,但是使用递归来实现,这样就可以摆脱上面所遇到的问题。同时,终止条件的确定使得遍历按照一定会的顺序进行,每次都会在自身调用深一层的index + 1情形的方法内容,dfsPhoneNum(digits,index + 1,phoneNumbers,list,buffer);这样就可以一步步先深入到最底层,然后一层层的往回进行遍历,类似于深度优先的算法的定义,所以遇到排列,组合等的可以考虑使用深度优先的递归方法进行实现。同样的下面几个算法题:

例如Leet Code第46题:

https://leetcode-cn.com/problems/permutations/description/

此题为一个排列组合类型的,其主要大意为求一个没有重复数字的序列,返回其所有可能的全排列。按照我们平时的数学算法可以是先固定一个,然后求其他元素的全排列,然后综合起来就是数列的全排列。接着我们上面讨论的内容,我们使用递归回溯方法好像是可以很轻松的实现,将数组中的深度确定为数组的长度,递归里面的元素就可以实现,但是仔细考虑下,起会遇到重复的问题。例如[1,1,1,1],这种排列方式并不符合我们需要的全排列的方式。为了避免这种情况,我们进行了改进,应用一个与输入数组等长的Boolean类型的数组来标识其是否别使用过,只要被添加到buffer里面就改变状态为true,代表使用过,当第二次递归的时候则遇到此位置的数字被使用过就跳过,这些状态仅是在一次遍历过程中,当在回溯时,由于执行了终止状态跳出了递归,则更改其状态返回false,以便于能够进行下一次的有效正确的递归。之后按照这个逻辑进行,进行完剩下的遍历回溯过程。

class Solution {
    public List<List<Integer>> permute(int[] nums) {  
        List<List<Integer>> result = new ArrayList<List<Integer>>();
        if(nums== null || nums.length == 0){
            return result;
        }
        dfsNums(nums,result, new ArrayList<Integer>(),new boolean[nums.length]);
        return result;
    }
    
    //新增boolea型等长数组,标识数组元素在这次遍历中是否被使用。
    private void dfsNums(int[] nums,List<List<Integer>> list, List<Integer> templist,boolean[] occupied){
        
        //终止条件
        if(templist.size() == nums.length){
            list.add(new ArrayList<Integer>(templist));
            return ;
        }
        
        for(int i = 0; i< nums.length; i++){
            //新增判断条件,对应位置没有使用过执行下面的过程,使用过则
            //执行continue,跳过
            if(occupied[i]){
                continue;
            }
            //使用之后将对应位置处的Boolean设置为true,代表使用过
            occupied[i] = true;
            templist.add(nums[i]);
            dfsNums(nums,list,templist,occupied);
            //只有当跳出递归的那一层才可以执行到下面的内容,所以跳出即执行了终止条件
            //代表这个遍历的结束,所以将对为位置的值设置为false,便于下次正确的递归
            occupied[i] = false;
            templist.remove(templist.size() -1);
        }
    }
    
}

相似类型的还有全排列II,Leet Code 47

https://leetcode-cn.com/problems/permutations-ii/description/

这个同样的也是进行全排列,只不过输入变了,变成了含有重复的元素,不在是原来无重复输入的情况了,所以分析一下,假如直接对含有重复元素的数组套用上面的解法来的话,会出现这个情况,【1(1),1(2),2】,【1(2),1(1),2】,其实在表面上看这是同一种情况,因为如果不区分位置的话,这两个排列情况的包含的元素和顺序都一样,所以我们要去除这个重复。如何去除呢,其实我们分析一下就可以得到结论了,假如我们对3原有的数组重新排序过后,相同的值都挨一起,所以,我们可以判断,当进行完以第一个元素为不动点,寻找其他的元素的排列组合的时候,第二元素和第一个元素相同时,并且第一个元素的状态为false,标识未被使用的情况下,即同时满足( i > 0 && nums[i] == nums[j] && !occupied[i -1] )时,此时其可以跳过了,同时也要有原来的条件,occupied[i] 是不是为true,如果为true则代表查找到自己本身了,因为自己本身被使用过了,所以为true,必须跳过,也就是说,我们还要提出occpied中在当前为true的前一个元素与次元素相等但是其状态为false的也要跳过。所以代码可以改造如下:

class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> result = new ArrayList<List<Integer>>();
        if(nums== null || nums.length == 0){
            return result;
        }
        Arrays.sort(nums);
        dfsNums(nums,result, new ArrayList<Integer>(),new boolean[nums.length]);
        return result;
    }
    
    private void dfsNums(int[] nums,List<List<Integer>> list, List<Integer> templist,boolean[] occupied){
        
        if(templist.size() == nums.length){
            list.add(new ArrayList<Integer>(templist));
            return ;
        }
        
        for(int i = 0; i< nums.length; i++){
            //此条件首先确定了使用过的不行
            //其次,当不为第一个的时候,nums[i] 和他的前一个(没使用过)相等的时候
            if(occupied[i] || i > 0 && nums[i] == nums[i-1] && !occupied[i - 1]){
                continue;
            }
            occupied[i] = true;
            templist.add(nums[i]);
            dfsNums(nums,list,templist,occupied);
            occupied[i] = false;
            templist.remove(templist.size() -1);
        }
    }
}

同时还有一些组合题:

Leet Code; 77

https://leetcode-cn.com/problems/combinations/description/

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

其实,核心的东西还是不变的,只是求取的组合的大小给出了限制,限制为看,所以我们改造下dfs方法,同样的可以按照递归回溯方法进行:

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        if (n < k || n == 0)
			return null;
		List<List<Integer>> list = new ArrayList<List<Integer>>();
		genereteComb(n, k, 1, new ArrayList<Integer>(), list);
		return list;
    }
   private static void genereteComb(int n, int k, int start, List<Integer> list, List<List<Integer>> res) {
       
       //终止条件,当组合中的元素个数等于k就可以添加了
		if (list.size() == k) {
			res.add(new ArrayList<Integer>(list));
			return;
		}
       //注意循环条件的变化,如果列表为空,则size()方法结果为0则循环为终点n - k ,起点为start,而起点则是会变化的
       //因为输入为不重复,所以i和i + 1不相同,所以不必判断是否是否有重读元素 ,要注意,这里添加数字,而不是数组i位置处的内容了
		for (int j = start; j <= n - k + list.size() + 1; j++) {
			list.add(j);
			genereteComb(n, k, j + 1, list, res);
			list.remove(list.size() - 1);
		}
	}
}

组合总和问题:Leet Code:

https://leetcode-cn.com/problems/combination-sum/description/

此问题为添加了一个条件的组合问题,可以理解为是上一个题的改造:主要变化了输入的参数,有了target目标值和remain做减法后的差值,同时每次开始的位置也是从本身开始了,因为题目中,可以出现如,【2,2,2,2】的结果。判断如果小于0则直接返回,大于0 则就继续寻找,等于0则添加到结果列表中(体现在终止条件上):

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) { 
        List<List<Integer>> result = new ArrayList<List<Integer>>();
        dfsCom(result,new ArrayList<Integer>(),candidates,target,target,0);
        return result;
    }  
    
    private void dfsCom(List<List<Integer>> list,List<Integer> templist,int[] nums,int target,int remain,int start){
       
        //终止条件1
        if(remain < 0){
            return;
        }
        
        //终止条件2
        if(remain == 0){
            list.add(new ArrayList<Integer>(templist));
        }
        
        for(int i = start;i < nums.length; i++){
            templist.add(nums[i]);
            //开始遍历的位置不在是i + 1 了,而是从i开始
            dfsCom(list,templist,nums,target,remain-nums[i],i);
            templist.remove(templist.size() - 1);
        }
    }

}

Leet Code  40:组合总和2

https://leetcode-cn.com/problems/combination-sum-ii/description/

此时,输入变了,如果我们还想借用上面的算法过程,则需要先对内容进行排序,同时跟聚会要求可以发现,不可以有重复的组合,也就是target = 8 【2,2,2,2】结果形式不在允许,所以我们只要将开始的位置有i 变为 i+1即可:

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        List<List<Integer>> result = new ArrayList<List<Integer>>();
        //前提条件,先进行排序
        Arrays.sort(candidates);
        dfsCom(result,new ArrayList<Integer>(),candidates,target,target,0);
        return result;
    }
    
    private void dfsCom(List<List<Integer>> result, List<Integer> templist,int[] nums,int target,int remain,int start){
        
        if(remain < 0){
            return;
        }
        
        if(remain == 0){
            result.add(new ArrayList<Integer>(templist));
        }
        for(int i = start; i < nums.length; i++){
            //改变位置1:值相等时,则跳过
            if(i > start && nums[i] == nums[i - 1]) {
                continue;
            }
            templist.add(nums[i]);
            //改变位置2:开始位置不在是i而是i+1,排除了仅有本身的组合的结果
            dfsCom(result,templist,nums,target,remain - nums[i],i+1);
            templist.remove(templist.size() - 1);
        }
    }
}
先总结这些写,后续更新。


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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值