zoukankan      html  css  js  c++  java
  • 【力扣算法】数组(7): 组合总和

    原题说明:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。

    注意:有数字(包括 target)都是正整数。解集不能包含重复的组合。 

    原题链接:https://leetcode-cn.com/problems/combination-sum


    题目分析

    这道题目一拿到手,想到的就是两个递归和动态规划。委实说,到写这篇博客之时,我对这些算法还是分不太清楚。希望每周一次的算法/数据结构学习能改善这样的局面。

    这道题目我是查看了题解和一些人给的代码。尽管如此,我还是能以自己的理解来表达这个问题。

    官方的题解和大多数人的评论都是回溯算法,那么关于这个算法,截止写这篇博客时我不甚了解,这里主要是给出怎么运用这样的算法。我有看到一些人说,回溯算法是一种思想,可能实现方式是依靠递归?

    下面进入正题,这次的博客将按照从无到有的顺序介绍代码。

    首先,这里先引一段网友给出的思路:

    1. 定义全局结果数组
    2. 返回全局结果数组
    3. 调用递归函数
    4. 定义递归函数

    1) 参数,动态变化,一般为分支结果、限制条件等
    2) 终止条件,将分支结果添加到全局数组
    3) 调用递归逐步产生结果,回溯搜索下一结果
    4) 剪枝条件

    我按照自己的理解、并参考了大家的代码,完成了解题。

    第一&二步,定义全局结果数组

    public ArrayList<ArrayList<Integer>> easyversion(int[] candidates, int target) {
    	ArrayList<ArrayList<Integer>> res1 = new ArrayList<ArrayList<Integer>>();		
    	return res1;
    }
    

    这里$res1$就是结果数组,当然数据结构不一定是List形式。我有看到一些题解将这个参数作为全局变量,这样就可以避免在每次调用递归函数的时候,多传入一个固定的变量了。

    第三步,调用递归函数

    public ArrayList<ArrayList<Integer>> easyversion(int[] candidates, int target) {
    	ArrayList<ArrayList<Integer>> res1 = new ArrayList<ArrayList<Integer>>();
    	backtracking_easy(candidates, 0, target, res1, new ArrayList<Integer>());    	
    	return res1;
    }
    

    第三行就是递归函数。这里我回想起之前听过的课程,其实对于函数接口的设计有两种方案:其一就是事先想好,都需要实现哪些方法,然后在设计的时候就完成编写;其二就是将一些反复出现的操作封装起来,设计好输入和输出的形式,之后再编写方法。

    其实对于第三行代码, new ArrayList<Integer>()在这里其实是没有意义的,但是考虑到递归函数调用的统一,需要有这个参数传递。

    第四步,定义递归函数

    图1展示的是本题的回溯过程(以$[1,2,3,4,5,6]$为给定数组,$target=6$)

     

    STEP1. 参数动态变化(分支结构和限制条件)

    private void backtracking_easy(int[] nums, int start, int target, ArrayList<ArrayList<Integer>> res, ArrayList<Integer> list) {	
        if(target<0) {
    
    	}
    	else if(target==0) { 
    
    	}
    	else {
    
    	}		
    }
    

     这里先说明递归函数传入的各个参数的含义。$nums$是提供的数组,$start$是当前遍历的起点,$target$是当前的目标值,$res1$是全局结果的返回值,$list$是当前存储组合的参数。

    从图1中可以看到,回溯的思路,就是穷尽组合时,不断计算组合的加和。这个时候$target$不断变化,直到满足条件($target==0$)。因此这也就是递归的分支和限制条件。

    分别就三种条件讨论:

    $target<0$。说明上一层加入的新元素使得组合总数超过了目标值,因此要返回上一层并剪枝(移除新加入的元素),之后在考虑加入下一个元素;

    $target==0$。说明正好满足条件,加入新元素之后的$list$可以append到$res1$中。再返回上一层,同样也要剪枝并考虑加入下一元素;

    $target>0$。说明达到目标值仍然需要加入元素。$start$代表的索引就是刚加入的元素位置,应当以此为起点,继续加入该元素。

    STEP2. 终止条件设定

    private void backtracking_easy(int[] nums, int start, int target, ArrayList<ArrayList<Integer>> res, ArrayList<Integer> list) {
    	if(target<0) {
    		return;
    	}
    	else if(target==0) { 
    		res.add(new ArrayList<>(list));
    		return;
    	}
    	else {
    		for(int i = start; i < nums.length; i++) {
    
    		}
    	}		
    }
    

     因为要求是找到所有组合,所以这里就是循环遍历。循环变量$i$的初始条件在$target>$已经提到了。这里的终止条件本来有两个(参考其它的答案),其一是元素的遍历不能发生数组越界$i<nums.length$,其二是组合的加和不超过目标值$target>0$。逐个分析一下:

    第一点是为了保证数组的元素能都被遍历,穷尽所有的组合可能;

    第二点是为了能够加入新的元素,逐渐接近目标值。但是因为之前有分支结果讨论过了,所以这点在这里就不必出现。

    STEP3. 调用递归函数 

    private void backtracking_easy(int[] nums, int start, int target, ArrayList<ArrayList<Integer>> res, ArrayList<Integer> list) {
    	if(target<0) {
    		return;
    	}
    	else if(target==0) { 
    		res.add(new ArrayList<>(list));
    		return;
    	}
    	else {
    		for(int i = start; i < nums.length; i++) {
    			//target -= nums[i]
    			list.add(nums[i]);
    			backtracking_easy(nums, i, target - nums[i], res, list);
    		}
    	}		
    }
    

     在调用下一层递归函数之前,需要在当前存储的参数$list$中append新的加入的元素。若之后加和满足目标值,则可以将加入新元素之后的list再append到$res1$中;或再往其中加入新的元素、再返回(剪枝);或返回(剪枝)。

    之后就可以调用递归函数,进行递归(其实我认为这个函数不是递归函数,而是因为发生“自己调用自己”所以递归了)。

    这里要注意的是,如果目标值先减去新加入的元素、再作为输入的参数加入到递归函数中,会产生错误的结果。

    所以要将新的目标值放在递归的函数中,作为参数进行传递。

    STEP4. 剪枝

    private void backtracking_easy(int[] nums, int start, int target, ArrayList<ArrayList<Integer>> res, ArrayList<Integer> list) {
    	if(target<0) {
    		return;
    	}
    	else if(target==0) { 
    		res.add(new ArrayList<>(list));
    		return;
    	}
    	else {
    		for(int i = start; i < nums.length; i++) {
    			list.add(nums[i]);
    			backtracking_easy(nums, i, target - nums[i], res, list);
    			list.remove(list.size()-1);//remove the last element
    		}
    	}		
    }
    

    每次调用下层递归函数返回之后,只能有两种情况:$target<0$和$target==0$不管哪种情况,都说明当前加入的元素不满足组合加和的要求。因此需要从$list$中弹出当前元素,加入下个元素,直到遍历完数组中所有的元素。

    以下是全部的代码

     1 public ArrayList<ArrayList<Integer>> easyversion(int[] candidates, int target) {
     2     ArrayList<ArrayList<Integer>> res1 = new ArrayList<ArrayList<Integer>>();
     3     
     4     if(candidates == null && candidates.length < 1)
     5         return res1;
     6     
     7     Arrays.sort(candidates);
     8     backtracking_easy(candidates, 0, target, res1, new ArrayList<Integer>());        
     9     return res1;
    10 }
    11 
    12 private void backtracking_easy(int[] nums, int start, int target, ArrayList<ArrayList<Integer>> res, ArrayList<Integer> list) {
    13     if(target<0) {
    14         return;
    15     }
    16     else if(target==0) { 
    17         res.add(new ArrayList<>(list));
    18         return;
    19     }
    20     else {
    21         for(int i = start; i < nums.length; i++) {
    22             list.add(nums[i]);
    23             backtracking_easy(nums, i, target - nums[i], res, list);
    24             list.remove(list.size()-1);//remove the last element
    25         }
    26     }        
    27 }

     在主函数部分,多出的代码首先是确保边界条件,还有就是对给定的数组进行排序,提高效率。对于数组中重复的元素,其实可以看做是再加入一个已有的元素,所以并不会有影响。

    下面是一个简单的递归实例说明,给定的数组是$[2,1,3]$(故意没有排序的),目标值是6。


      

    总结:

    • 感觉算法好神奇,就是一行行的语言,但是可视化之后的呈现是那么丰富。
    • 对于算法的结果,还是强调在一定范围内容用实例多测试
    • 目前感觉,对于递归(于我而言还是很抽象的),可以从一定的阶段推导一下。 
  • 相关阅读:
    maven工程下添加oracle驱动包
    面试题----入参两个Integer,无返回值,然后使这个两个值在调用函数后交换
    使用spring data solr 实现搜索关键字高亮显示
    MyBatisPlus忽略映射字段注解
    Linux下Solr单机版、集群版安装与配置
    AngularJs实现全选功能
    SpringBoot2.0整合mybatis、shiro、redis实现基于数据库权限管理系统
    springboot2.0集成shiro出现ShiroDialect报错找不到AbstractTextChildModifierAttrPr
    linux下ssh远程登录服务器入门操作
    英文邮件寻求帮助的礼貌用语
  • 原文地址:https://www.cnblogs.com/RicardoIsLearning/p/12076579.html
Copyright © 2011-2022 走看看