【随想录8 】是否允许重复选择元素的组合问题

参考:代码随想录回溯算法

组合问题

待选组合无重复元素

77. 组合

216. 组合总和 III

17. 电话号码的字母组合

待选组合无重复元素,但可以多次选取同样的元素

39. 组合总和

待选组合有重复元素,且待选元素只可以被使用一次

40. 组合总和 II

组合问题

一般是给定一个集合和特定要求,求有多少种子集合能符合特定要求。

需要合理暴力才能在规定时间内完成算法,如果只是n个for循环再加时间不限,确实是可以满足要求,但咱作为有追求的coder是不允许自己写出那种代码的。

待选组合无重复元素

77. 组合

tt

借用卡哥的图来翻译翻译

t

给定一个集合,从中取出有限个个数,且不能重复选取

有点类似于取球游戏

第一次取出的球,不放回去,再拿剩下的n个,均不放回。
取完记录下来,将除了第一次取出的球全部放回,循环往复记录结果

不同的是,数组中的元素是有序的,为了不漏掉每一种结果,可以使用循环的方式来实现这个过程

代码:

    //结果集合
    List<List<Integer>> res = new ArrayList<>();
    //路径集合
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combine( int n, int k ) {
        // 从n个数中选取 k 个数, 从 1 开始
        show( n, k, 1 );
        return res;
    }


    private void show( int n, int k, int start ) {
        // 收集到了k 个数,res 记录
        if ( path.size() == k ) {
            printIndent( count );
            System.out.println( "进入收集,第" + count + "层 ,收集到结果=" + path.toString() );
            res.add( new ArrayList<>( path ) );
            return;
        }
        // 从 start 开始,再选 x 个数,x 取决于之前选取了几个数
        for ( int i = start; i <= n; i++ ) {
            printIndent( count++ );
            System.out.println( "i=" + i + "第" + count + "层 ,回溯前=" + path.toString() );
            // 新的数加入到集合中
            path.add( i );

            // 从下一个数开始选择,直到 当前 path 收集到了k个数
            show( n, k, i + 1 );

            //将上一个加入的数剔除,从下一个数继续执行选择。
            path.removeLast();
            
            printIndent( --count );
            System.out.println( "i=" + i + "第" + count + "层 ,回溯后=" + path.toString() );
        }
    }


    // 全局变量,记录递归函数的递归层数
    int count = 0;
    // 输入 n,打印 n 个 tab 缩进
    void printIndent( int n ) {
        for ( int i = 0; i < n; i++ ) {
            System.out.printf( "    " );
        }
    }

通过打印中间的递归函数调用过程,可以更好的理解整个回溯过程

i=11,回溯前=[1]
    i=22,回溯前=[1, 2]
        进入收集,第2,收集到结果=[1, 2]
    i=21,回溯后=[1]
    i=32,回溯前=[1, 3]
        进入收集,第2,收集到结果=[1, 3]
    i=31,回溯后=[1]
    i=42,回溯前=[1, 4]
        进入收集,第2,收集到结果=[1, 4]
    i=41,回溯后=[1]
i=10,回溯后=[]
i=21,回溯前=[2]
    i=32,回溯前=[2, 3]
        进入收集,第2,收集到结果=[2, 3]
    i=31,回溯后=[2]
    i=42,回溯前=[2, 4]
        进入收集,第2,收集到结果=[2, 4]
    i=41,回溯后=[2]
i=20,回溯后=[]
i=31,回溯前=[3]
    i=42,回溯前=[3, 4]
        进入收集,第2,收集到结果=[3, 4]
    i=41,回溯后=[3]
i=30,回溯后=[]
i=41,回溯前=[4]
i=40,回溯后=[]

zuhe
例子: 1,2,3,4。 选2个数组合

i = 1 时,第一个数是 1 , 只能从剩下的 2,3,4 中选。
	先选 2 , 收集到了 2 个数,记录res.add(【1,2】)
	然后回溯到开始选择之前,path集合中只有【1】
	
	i = 3 ,选择 3 ,收集到了 2个数,记录【1,3】
	回溯,path集合为【1】 
	
	i = 4 ,选择 4 ,收集到了 2个数,记录【1,4】
	回溯,path集合为【1】 
i == 4  退出循环,再次回溯
path 集合 【】

i = 2  ,第一个数是 2 , 只能从剩下的 3,4 中选。

然后过程与上面类似

216. 组合总和 III

tt
和上一个题类似,只不过结果条件变成了集合中的数需要等于某个数,且子集合的元素不能重复选取

    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int sum = 0;
    int end = 9;


    public List<List<Integer>> combinationSum3( int k, int n ) {
        show( k, n, 1 );
        return res;
    }


    private void show( int k, int n, int start ) {
        //System.out.println(n+"--"+sum +"  "+ k+"--"+ path.size());
        if ( path.size() == k && sum == n ) {
            //printIndent( count );
            //System.out.println( "进入收集,第" + count + "层 ,收集到结果=" + path.toString() );
            res.add( new ArrayList<>( path ) );
            return;
        } else if ( sum > n ) {
            return;
        }

        for ( int i = start; i <= end; i++ ) {
            if ( path.contains( i ) ) {
                continue;
            }

            path.add( i );
            sum = sum + i;

            //printIndent( count++ );
            //System.out.println( "i=" + i + "第" + count + "层 ,回溯前=" + path.toString() + "sum="+sum);
            show( k, n, i + 1 );
            path.removeLast();
            sum = sum - i;

            printIndent( --count );
            //System.out.println( "i=" + i + "第" + count + "层 ,回溯后=" + path.toString() );
        }
    }


    // 全局变量,记录递归函数的递归层数
    int count = 0;


    // 输入 n,打印 n 个 tab 缩进
    void printIndent( int n ) {
        for ( int i = 0; i < n; i++ ) {
            System.out.printf( "    " );
        }
    }

17. 电话号码的字母组合
tt

 //设置全局列表存储最后的结果
    List<String> list = new ArrayList<>();

    public List<String> letterCombinations(String digits) {
        if (digits == null || digits.length() == 0) {
            return list;
        }
        //初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
        String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        //迭代处理
        backTracking(digits, numString, 0);
        return list;

    }

    //每次迭代获取一个字符串,所以会设计大量的字符串拼接,所以这里选择更为高效的 StringBuild
    StringBuilder temp = new StringBuilder();

    //比如digits如果为"23",num 为0,则str表示2对应的 abc
    public void backTracking(String digits, String[] numString, int num) {
        //遍历全部一次记录一次得到的字符串
        if (num == digits.length()) {
            list.add(temp.toString());
            return;
        }
        //str 表示当前num对应的字符串
        String str = numString[digits.charAt(num) - '0'];
        for (int i = 0; i < str.length(); i++) {
            temp.append(str.charAt(i));
            //c
            backTracking(digits, numString, num + 1);
            //剔除末尾的继续尝试
            temp.deleteCharAt(temp.length() - 1);
        }
    }

待选组合无重复元素,但可以多次选取同样的元素

39. 组合总和

tt

216. 组合总和 III 不同的是,这次可以在集合中重复选取某个元素,其集合中的元素值是唯一的。

建议自己思考如何实现子集合在回溯过程中,重复选取一个元素
代码中有答案

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int sum = 0;
    // 全局变量,记录递归函数的递归层数
    int count = 0;


    // 输入 n,打印 n 个 tab 缩进
    void printIndent( int n ) {
        for ( int i = 0; i < n; i++ ) {
            System.out.printf( "    " );
        }
    }

    public List<List<Integer>> combinationSum( int[] candidates, int target ) {
        getCombination(candidates,target,0);
        return res;
    }
    private void getCombination( int[] candidates, int target,int start) {
        if(sum>target)return;
        if(sum==target){
            //System.out.println( "进入收集,第" + count + "层 ,收集到结果=" + path.toString() );
            res.add(new ArrayList<>(path)); 
            return;
        }

        for(int i=start;i<candidates.length;i++){
        
            sum+=candidates[i];
            path.add( candidates[i] );  


            // printIndent( count++ );
            // System.out.println( "i=" + i + "第" + count + "层 ,回溯前=" + path.toString() + "sum="+sum);
            // 此处从 i 位置出发,代表当前子集合还可以再次选取 这个元素
            getCombination( candidates,target,i );
            
            sum-=candidates[i];
            path.removeLast();
            // printIndent( --count );
            // System.out.println( "i=" + i + "第" + count + "层 ,回溯后=" + path.toString() );
        }
    }
}

待选组合有重复元素,且待选元素只可以被使用一次

40. 组合总和 II
tt
这次要求,在可能含有重复元素的集合中,找出子集合之和为target,且集合内的元素只能使用一次 的 所有不重复的子集合

如果按照之前的办法,直接回溯,结果集中会包含重复的集合
比如 【1,2(a),2(b),2(c),5】 target
此处用a\b\c来代表标记重复元素

正确解集应该是 【【1,2(a、b、c),2(除了前一个元素的认一个)】,【5】】

但如果直接回溯,会出现【1,2,2】、【1,2,2】、【1,2,2】三个相同的解集
这是因为在回溯过程中,尽管不同的2 值一样,但他们在集合的位置中是不同的,上面的回溯代码无法处理,相同值不同位置的情况 , 也就出现了看起来是一样的子集合

图来自代码随想录回溯部分
tt

所以本题的重点在于,如何在回溯过程中, 不使用之前正确集合中使用过的值 ,且完成所有解集的搜集。

可以使用一个标记数组,来判断这个数字是否使用过,先对给定集合数组排序,然后建立对应的标记数组。

tt

去重操作应该为:
如果当前path集合中,新选取的元素和上一个选取的元素是一样的,且之前的解集集合中使用过了当前元素, 那就跳过
对应到代码就是

candidates[i] == candidates[i - 1] //新选取的元素和上一个元素一样
&&
used[i - 1] == false // 之前解集集合使用过当前元素

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort( candidates );
        boolean []used = new boolean[candidates.length];
        getCombination(candidates,used,target,0);
        return res;
    }

    int sum =0;
    private void getCombination( int[] candidates,  boolean[] used,int target,int start ) {
        if(sum>target)return;
        if(sum==target){
            res.add(new ArrayList<>(path));
            return;
        }
    
        for(int i=start;i<candidates.length;i++){
            
            //只能剔除第一位重复 题目要求是每一位开头只求一次
            //if(path.size()==0 && i>0  && candidates[i]==candidates[i-1])continue;

            // 相当于把重复元素值的解给剔除了
            //if(path.size()>0 && path.getLast()==candidates[i])continue;

            //正确剔除重复解的办法
            if(i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]){
                continue;
            }

            used[i]=true;
            sum+=candidates[i];
            path.add(candidates[i]);

            getCombination(candidates,used,target,i+1);

            used[i]=false;
            int temp = path.getLast();
            sum-=temp;
            path.removeLast();

        }
    }

}

不使用标记数组的办法:

上面的办法中,去重要求
i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]

递归为:
getCombination(candidates,used,target,i+1);

其实当 i > start 时,它新选择的元素已经不会是之前选择过的元素,所以可以不使用标记数组

class Solution {
List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int sum = 0;


    public List<List<Integer>> combinationSum2( int[] candidates, int target ) {
        //为了将重复的数字都放到一起,所以先进行排序
        Arrays.sort( candidates );
        backTracking( candidates, target, 0 );
        return res;
    }


    private void backTracking( int[] candidates, int target, int start ) {
        if ( sum == target ) {
            res.add( new ArrayList<>( path ) );
            return;
        }
        for ( int i = start; i < candidates.length && sum + candidates[i] <= target; i++ ) {
            //正确剔除重复解的办法
            //跳过同一树层使用过的元素
            if ( i > start && candidates[i] == candidates[i - 1] ) {
                continue;
            }

            sum += candidates[i];
            path.add( candidates[i] );
            // i+1 代表当前组内元素只选取一次
            backTracking( candidates, target, i + 1 );

            int temp = path.getLast();
            sum -= temp;
            path.removeLast();

        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值