698. 划分为k个相等的子集[回溯]

题解

思路:

  1. 首先确定能否分为k组,即sum(nums) % k == 0是否成立,如果不成立显然无法分组
  2. 得到每组集合的和avg = sum(nums) / k.
  3. 判断能否找到一种分组方式s1,s2,...,sk,使得sum(s1)=sum(s2)=...=sum(sk)=avg

使用回溯来进行分组,在所有可能的情况中找到一个。单纯的回溯会超时:

class Solution {
    public boolean canPartitionKSubsets(int[] nums, int k) {
        int sum = 0, avg, maxNum = 0;

        for(int num : nums){
            sum += num;
            maxNum = Math.max(maxNum, num);
        }
        if(sum % k != 0){
            return false;
        }
        avg = sum / k;  //获取平均值
        //至少有一个数大于了均值,则一定无法分组
        if(maxNum > avg){
            return false;
        }

        //能否找到k组,每组和的值为avg
        return dfs(0, nums, new int[k], avg, k);
    }
    
    private boolean dfs(int start, int[] nums, int[] set, int target, int k){
        if(start == nums.length){
            boolean ret = true;
            for (int i = 0; i < k; i++) {
                if(set[i] != target) {
                    ret = false;
                    break;
                }
            }
            return ret;
        }
        
        boolean ret = false;
        //当前数加到某个集合中
        for (int j = 0; j < k; j++) {
            if (set[j] + nums[start] <= target){
                set[j] += nums[start];
                ret = ret || dfs(start+1, nums, set, target, k);
                set[j] -= nums[start];  //回溯
            }
        }   
        return ret;
    }
    
}

需要利用一些剪枝操作来减少部分复杂度:

  1. 对数组从大到小进行排序,使得递归过程中的set[j] + nums[start] > target命中率(?)更高
  2. 需要注意的是,遍历到某个数num时,对应的k个集合为s1,s2,...,sk,如果有多个集合的和是相同的,那么当前值只需要选择一个集合放入即可,不需要全部放入一次进行多余的判断。(这个是剪枝的一个重点)
  3. 递归退出条件的简化:start == nums.length时,不需要再做额外的判断,直接返回true即可。因为每个数都是在满足set[j] + nums[start] <= target时才会被放入,而不能均分的情况已经在一开始被判断掉了,因此如果所有数都被选择到,则当前k个集合一定是我们需要的那个答案。
  4. 对于第一个整数,其放在任意一个集合中的情况得到的结果都是相同的,因此直接放在第一个集合中即可,不判断后续集合。

修改后的代码如下:

class Solution {
    public boolean canPartitionKSubsets(int[] nums, int k) {
        int sum = 0, avg, maxNum = 0;

        for(int num : nums){
            sum += num;
            maxNum = Math.max(maxNum, num);
        }
        if(sum % k != 0){
            return false;
        }
        avg = sum / k;  //获取平均值
        //至少有一个数大于了均值,则一定无法分组
        if(maxNum > avg){
            return false;
        }

        //逆序排列,提高命中率
        int[] sortedNums = Arrays.stream(nums).boxed().sorted((a, b) -> b - a).mapToInt(value -> value).toArray();

        //能否找到k组,每组和的值为avg
//        System.out.println(new Date());
        boolean dfs = dfs(0, sortedNums, new int[k], avg, k);
//        System.out.println(new Date());
        return dfs;
    }

    private boolean dfs(int start, int[] nums, int[] set, int target, int k){
        if(start == nums.length){
            return true;
        }

        boolean ret = false;
        //当前数加到某个集合中
        for (int j = 0; j < k; j++) {
            //第一个球的情况
            if (start == 0 && j > 0) break;

            // 重复排列的选择,连续的多个集合内和相同时,当前值选择一个放置即可
            // 如果当前集合和上一个集合内的元素和相等,则跳过
            // 原因:如果元素和相等,那么 nums[] 选择上一个集合和选择当前集合得到的结果是一致的
            if (j > 0 && set[j] == set[j - 1]) continue;

            if (set[j] + nums[start] > target) continue;

            set[j] += nums[start];
            ret = dfs(start+1, nums, set, target, k);
            if (ret){
                return true;
            }
            set[j] -= nums[start];  //回溯
        }
        return ret;
    }
    
}

参考题解

经典回溯算法:集合划分问题「重要更新 🔥🔥🔥」

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值