关于Leetcode T698. 划分为k个相等的子集的详细递归思路讲解

题目

给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:

输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总

提示:

1 <= k <= len(nums) <= 16
0 < nums[i] < 10000

解题思路

我一直秉持的一个观点的是“其实递归本身就是完成一种重复功能的形式化描述”,最好在函数命名中能体现出来提醒自己。那么,对于这个题,其实有两个重要的点,需要我们去理解,理解透这两个点,这道题的解法我们就完全理解了,还是,先上代码。

代码

class Solution {
  public boolean canPartitionKSubsets(int[] nums, int k) {
        int sum=0;
        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
        }
        //数组必然是k的倍数
        if(sum%k!=0) return false;
        //求出子集的和
        sum=sum/k;
        //排序,升序
        Arrays.sort(nums);
        //如果数组的最大值大于子集的和,则返回false
        if(nums[nums.length-1]>sum) return false;
        //建立一个人长度为k的桶
        int[]arr=new int[k];
        //桶的每一个值都是子集的和,即空桶的容量为子集的和
        Arrays.fill(arr,sum);
        //从数组最后一个数开始进行递归
        return forEveryNum(nums,nums.length-1,arr,k);
    }

    boolean forEveryNum(int[]nums,int cur,int[]arr,int k){
       //cur走到-1时,说明所有的数全部都放进桶里了。这时一定是true
       if(cur<0){
           return true;
       }
       //对每个数来说,遍历k个桶,选择一个桶放入
       for(int i=0;i<k;i++){
           //如果正好能放下当前的数或者放下当前的数后,桶里的容量还有剩余
           if(arr[i]==nums[cur]||(cur>0&&arr[i]-nums[cur]>=nums[0])){
               //放当前的数到桶里
               arr[i]-=nums[cur];
               //递归放下一个数
               if(forEveryNum(nums,cur-1, arr,k)) return true; //只需要找到一个即可。
               //这个数不该放在桶i中,从桶中拿回当前的数
               arr[i]+=nums[cur];
           }
       }
       return false;
    }

}

两个重要的点

  1. for循环的那两句代码
for(int i=0;i<k;i++){
           //如果正好能放下当前的数或者放下当前的数后,桶里的容量还有剩余
           if(arr[i]==nums[cur]||(cur>0&&arr[i]-nums[cur]>=nums[0])){
  1. if+递归函数 return true这一句代码
if(forEveryNum(nums,cur-1, arr,k)) return true; //只需要找到一个即可。

不过这两个重要的点的核心逻辑其实就只有一个,我们要理解这个递归函数的功能。首先,我将递归函数命名为forEveryNum,就是想透露一个信息,这个递归函数是对nums里的每个数字进行处理的一个重复性功能的描述。

       //对每个数来说,遍历k个桶,选择一个桶放入
       for(int i=0;i<k;i++){
           //如果正好能放下当前的数或者放下当前的数后,桶里的容量还有剩余
           if(arr[i]==nums[cur]||(cur>0&&arr[i]-nums[cur]>=nums[0])){

其实上面这个for循环的意思就是说,对于每个数来讲,其都有k个选择(就是k个桶),每个数都需要尝试放在第i(i>=0&&i<K)个桶里,,如果大家对高中所学的排列组合还很熟练的话,其实这就是高中排列组合经常考的体型,那么这总共会有多少种组合呢?会是kn种,在这个kn种有一种组合满足要求即可。理解了这个,我们再来看看,这个for循环写的是不是这个意思?对每个cur指针指向的nums数组里的数来讲,都要进行这个for循环,即对nums数组里的每个数来讲,k个桶要尝试一遍,只要当前尝试的第i个桶正好能放下当前的数或者放下当前的数后桶里的容量还有剩余,那么这个数就可以放在这个桶里,那么我们就可以进行对下一个数选择可以放入的桶了。

if(forEveryNum(nums,cur-1, arr,k)) return true; //只需要找到一个即可。

因为我们在进行递归函数之前,就进行了预处理,这个预处理是按升序排序,且能进入到递归函数时,就已经保证了数组对最后一个数是小于空桶的容量的,所以,对于最后一个数我们我们选择第一个桶总没有错,所以,我们可以固定第一个数放在第一个桶来来进行剩下的处理,那么总的组合数其实就变成了k^(n-1)种,但是对于递归函数来讲,我们要涵盖k^(n)种情况,因为递归函数是重复功能的形式化描述,所以我们没有必要固定第一个数放在第一个桶里,所以,从这个角度也可以理解,为什么这句代码if(forEveryNum(nums,cur-1, arr,k)) return true是这么写的,if(forEveryNum(nums,cur-1, arr,k))保证了如果我们找到了,即if(cur<0){ return true; }。那么if的条件判断为true,说明我们总的函数就可以退出了,返回true即可。

不知道我有没有把我的思路说明白,如果还有些地方不理解,请移步IDE多debug几次。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值