Medium
题目描述
给定一个整数数组 nums 和一个整数 k,如果可以将这个数组分成 k 个非空子集,使得每个子集的元素和都相等,则返回 true。
示例 1:
输入: nums = [4,3,2,3,5,2,1], k = 4
输出: true
解释: 可以将其分成 4 个子集 (5), (1, 4), (2,3), (2,3),每个子集的和都是 5。
示例 2:
输入: nums = [1,2,3,4], k = 3
输出: false
约束条件:
1 <= k <= nums.length <= 161 <= nums[i] <= 10^4- 每个元素的频率在
[1, 4]范围内。
解题思路
解题思路
这是一个经典的回溯问题。核心思想是将数组分成k个和相等的子集。
预处理检查:
- 计算数组总和,如果不能被k整除,直接返回false
- 目标子集和 = 总和 / k
- 如果存在任何元素大于目标和,返回false
两种主要解法:
- 按元素分配(推荐):遍历每个元素,尝试将其放入k个桶中的某一个
- 按桶填充:依次填充k个桶,每个桶尝试所有可能的元素组合
优化策略:
- 将数组降序排列,优先处理大数字,减少搜索空间
- 使用visited数组避免重复使用元素
- 剪枝:如果当前桶的和加上当前元素超过目标值,跳过
按元素分配的方法更高效,因为它避免了重复的子集组合。时间复杂度为O(k^n),其中n是数组长度。
代码实现
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
int sum = 0;
for (int num : nums) sum += num;
if (sum % k != 0) return false;
int target = sum / k;
sort(nums.rbegin(), nums.rend());
if (nums[0] > target) return false;
vector<int> buckets(k, 0);
return backtrack(nums, buckets, 0, target);
}
private:
bool backtrack(vector<int>& nums, vector<int>& buckets, int index, int target) {
if (index == nums.size()) {
return true;
}
for (int i = 0; i < buckets.size(); i++) {
if (buckets[i] + nums[index] <= target) {
buckets[i] += nums[index];
if (backtrack(nums, buckets, index + 1, target)) {
return true;
}
buckets[i] -= nums[index];
}
// 剪枝:如果当前桶为空,后续的空桶也会失败
if (buckets[i] == 0) break;
}
return false;
}
};
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
total = sum(nums)
if total % k != 0:
return False
target = total // k
nums.sort(reverse=True)
if nums[0] > target:
return False
buckets = [0] * k
def backtrack(index):
if index == len(nums):
return True
for i in range(k):
if buckets[i] + nums[index] <= target:
buckets[i] += nums[index]
if backtrack(index + 1):
return True
buckets[i] -= nums[index]
# 剪枝:如果当前桶为空,后续的空桶也会失败
if buckets[i] == 0:
break
return False
return backtrack(0)
public class Solution {
public bool CanPartitionKSubsets(int[] nums, int k) {
int sum = nums.Sum();
if (sum % k != 0) return false;
int target = sum / k;
Array.Sort(nums, (a, b) => b.CompareTo(a));
if (nums[0] > target) return false;
int[] buckets = new int[k];
return Backtrack(nums, buckets, 0, target);
}
private bool Backtrack(int[] nums, int[] buckets, int index, int target) {
if (index == nums.Length) {
return true;
}
for (int i = 0; i < buckets.Length; i++) {
if (buckets[i] + nums[index] <= target) {
buckets[i] += nums[index];
if (Backtrack(nums, buckets, index + 1, target)) {
return true;
}
buckets[i] -= nums[index];
}
// 剪枝:如果当前桶为空,后续的空桶也会失败
if (buckets[i] == 0) break;
}
return false;
}
}
var canPartitionKSubsets = function(nums, k) {
const sum = nums.reduce((a, b) => a + b, 0);
if (sum % k !== 0) return false;
const target = sum / k;
nums.sort((a, b) => b - a);
if (nums[0] > target) return false;
const used = new Array(nums.length).fill(false);
function backtrack(start, k, currentSum) {
if (k === 1) return true;
if (currentSum === target) {
return backtrack(0, k - 1, 0);
}
for (let i = start; i < nums.length; i++) {
if (used[i] || currentSum + nums[i] > target) continue;
used[i] = true;
if (backtrack(i + 1, k, currentSum + nums[i])) {
return true;
}
used[i] = false;
if (currentSum === 0) break;
}
return false;
}
return backtrack(0, k, 0);
};
复杂度分析
| 复杂度类型 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(k^n) | 最坏情况下每个元素都要尝试放入k个桶中 |
| 空间复杂度 | O(k + n) | 递归栈深度O(n),桶数组O(k) |