• LeetCode - 698 划分为k个相等的子集


    目录

    题目来源

    题目描述

    示例

    提示

    题目解析

    算法源码


    题目来源

    698. 划分为k个相等的子集 - 力扣(LeetCode)

    题目描述

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

    示例

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

    提示

    • 1 <= k <= len(nums) <= 16
    • 0 < nums[i] < 10000
    • 每个元素的频率在 [1,4] 范围内

    题目解析

    本题其实是一道子集问题,可以利用回溯法求解。

    首先,我们可以求出nums数组的所有元素之和sum,如果sum % k 不为0的话,说明每一份子集的和不是整数,但是题目说了nums数组中的元素都是整数,因此每一个子集的和也应该是整数,因此如果sum % k 不为0的话,则返回false。

    如果sum % k 为0,则我们假设subSum = sum / k,即每一个子集的和为subSum,那么subSum应该大于等于nums数组的最大值,因为nums数组的每一个子集至少有一个元素组成,因此subSum应该大于或等于每一个nums元素,即至少不小于nums数组的最大值。

    当面两个条件都符合后,还不能说明nums可以被分为k个和相等的子集,比如nums=[2,2,2,2,3,4,5] ,且k=4。

    nums数组的和sum = 20,k=4,因此subSum = 5,而subSum >= max(nums[i]) = 5,因此符合上面两个条件,但是nums数组却无法分为4个等和子集。

    因此,我们需要一种算法来判断一个数组是否可以划分k个和相等的子集,此时我们就需要借助回溯算法

    我们可以想象划分子集问题,可以看成向k个桶中放球,每个桶的承重为subSum,而现在nums中存放着重量不一的多个球,如果nums中的球都能放到k个桶中,则可以划分子集成功。

    比如nums = [4, 3, 2, 3, 5, 2, 1], k = 4,对应的subSum=5

    首先,nums的重量5的球可以独占一个桶,我们可以减少考虑一个桶的处理

     接下来,我们需要将球按照降序排序,然后依次放入桶中

     

    由于 4 + 3 > 5,因此球3需要放到下一个桶中

    同理处理下面的球

    这里我们对nums进行了降序排序,而不是升序排序,因为升序排序可能会产生非常复杂的放置行为,这将不利于我们理解。 

    上述逻辑对应的算法代码如下

    JS

    1. /**
    2. * @param {number[]} nums
    3. * @param {number} k
    4. * @return {boolean}
    5. */
    6. var canPartitionKSubsets = function(nums, k) {
    7. const sum = nums.sort((a,b) => b - a).reduce((p, c) => p + c)
    8. if(sum % k !== 0) return false
    9. const subSum = sum / k
    10. if(subSum < nums[0]) return false
    11. while(nums[0] === subSum) {
    12. nums.shift()
    13. k--
    14. }
    15. const buckets = new Array(k).fill(0)
    16. return partition(nums, 0, buckets, subSum)
    17. };
    18. function partition(nums, index, buckets, target) {
    19. if(index === nums.length) return true
    20. const selected = nums[index]
    21. for(let i = 0; i < buckets.length; i++) {
    22. if(selected + buckets[i] <= target) {
    23. buckets[i] += selected
    24. if(partition(nums, index+1, buckets, target)) return true
    25. buckets[i] -= selected
    26. }
    27. }
    28. return false
    29. }

    可以发现,性能非常差。

    有这样一种情况,即我们选择了一个球,但是发现此时每一个桶中的球重量之和都相等,也就是说此时选择的球放到哪一个桶中,最终结果都是相同的,比如球放到桶1中重量超了,则放到其他桶中重量肯定也是超的。

    但是根本上面算法逻辑,我们肯定会在选择的球放到桶1超了之后,继续到桶2中尝试,而桶2超了之后,必然放到桶3中尝试,这其实就是一种无意义的试探操作。

    因此我们再进入递归流程前,先判断 buckets[i]的重量是否和buckets[i-1]的重量是否相等,若相等的话,则buckets[i-1]试过了,则buckets[i]就不用试了。

    最终代码如下

    JavaScript算法源码

    1. /**
    2. * @param {number[]} nums
    3. * @param {number} k
    4. * @return {boolean}
    5. */
    6. var canPartitionKSubsets = function(nums, k) {
    7. const sum = nums.sort((a,b) => b - a).reduce((p, c) => p + c)
    8. if(sum % k !== 0) return false
    9. const subSum = sum / k
    10. if(subSum < nums[0]) return false
    11. while(nums[0] === subSum) {
    12. nums.shift()
    13. k--
    14. }
    15. const buckets = new Array(k).fill(0)
    16. return partition(nums, 0, buckets, subSum)
    17. };
    18. function partition(nums, index, buckets, target) {
    19. if(index === nums.length) return true
    20. const selected = nums[index]
    21. for(let i = 0; i < buckets.length; i++) {
    22. if (i > 0 && buckets[i] === buckets[i - 1]) continue // 此步剪枝将大幅提升性能
    23. if(selected + buckets[i] <= target) {
    24. buckets[i] += selected
    25. if(partition(nums, index+1, buckets, target)) return true
    26. buckets[i] -= selected
    27. }
    28. }
    29. return false
    30. }

    Java算法源码

    1. import java.util.Arrays;
    2. class Solution {
    3. public boolean canPartitionKSubsets(int[] nums, int k) {
    4. int sum = 0;
    5. for (int num : nums) sum += num;
    6. if (sum % k != 0) return false;
    7. int subSum = sum / k;
    8. // nums升序
    9. Arrays.sort(nums);
    10. // nums元素反转,实则实现降序
    11. int l = 0;
    12. int r = nums.length - 1;
    13. while (l < r) {
    14. int tmp = nums[l];
    15. nums[l] = nums[r];
    16. nums[r] = tmp;
    17. l++;
    18. r--;
    19. }
    20. if (subSum < nums[0]) return false;
    21. int[] buckets = new int[k];
    22. return partition(nums, 0, buckets, subSum);
    23. }
    24. public boolean partition(int[] nums, int index, int[] buckets, int target) {
    25. if (index == nums.length) return true;
    26. int selected = nums[index];
    27. for (int i = 0; i < buckets.length; i++) {
    28. if (i > 0 && buckets[i] == buckets[i - 1]) continue;
    29. if (selected + buckets[i] <= target) {
    30. buckets[i] += selected;
    31. if (partition(nums, index + 1, buckets, target)) return true;
    32. buckets[i] -= selected;
    33. }
    34. }
    35. return false;
    36. }
    37. }

    Python算法源码

    1. class Solution(object):
    2. def canPartitionKSubsets(self, nums, k):
    3. """
    4. :type nums: List[int]
    5. :type k: int
    6. :rtype: bool
    7. """
    8. total = sum(nums)
    9. if total % k != 0:
    10. return False
    11. avg = total // k
    12. nums.sort(reverse=True)
    13. if avg < nums[0]:
    14. return False
    15. buckets = [0]*k
    16. return partition(nums, 0, buckets, avg)
    17. def partition(nums, index, buckets, target):
    18. if index == len(nums):
    19. return True
    20. selected = nums[index]
    21. for i in range(len(buckets)):
    22. if i>0 and buckets[i] == buckets[i-1]:
    23. continue
    24. if selected + buckets[i] <= target:
    25. buckets[i] += selected
    26. if partition(nums, index+1, buckets, target):
    27. return True
    28. buckets[i] -= selected
    29. return False

  • 相关阅读:
    【蓝桥杯2025备赛】素数判断:从O(n^2)到O(n)学习之路
    软件加密系统Themida应用程序保护指南(一):应用信息界面
    windows下vim+mingw+gtk环境搭建
    [2023.09.11]: Yew的SSR中的Cargo.toml配置
    【DataOps】- 数据开发治理一体化之网易数帆数据治理2.0实践分享
    渗透测试-逻辑漏洞专题
    Android动态更换图标
    iterative farthest point sample (IFPS or FPS)
    字节跳动2023秋招研发第五场笔试【客户端方向】
    Flutter 上传文件和保存在应用程序存储目录
  • 原文地址:https://blog.csdn.net/qfc_128220/article/details/127761308