LeetCode 698
LeetCode 2022.9.20 的打卡题目
698 划分为k个相等的子集 [https://leetcode.cn/problems/partition-to-k-equal-sum-subsets]
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
示例 2:输入: nums = [1,2,3,4], k = 3
输出: false提示:
1 <= k <= len(nums) <= 16
0 < nums[i] < 10000
每个元素的频率在 [1,4] 范围内
解法1 搜索剪枝
回溯法,枚举每个数字是否放进某个组里。
但是直接搜索会超时。
剪枝思路:
- 从大到小对数字进行排序,这样有比较大的概率在 (targets[i] >= nums[current])这一步进行跳过;
- 对于已经处理过的同样大小的组,如果当前数没有成功放进去,那它就不需要再次进行计算了,这一步剪枝可以使搜索算法通过此题。这里用unordered_set存储了一下已经废掉的解;网上题解多是使用targets[i] == targets[i - 1]来判断,好像会更快一些。
class Solution {
public:
bool dfs(vector<int>& nums, vector<int>& targets, int current) {
if (current == nums.size()) {
for (int i = 0; i < targets.size(); ++i) {
if (targets[i]) {
return false;
}
}
return true;
}
unordered_set<int> unsolved;
for (int i = 0; i < targets.size(); ++i) {
if ((solved.find(targets[i]) == unsolved.end()) && targets[i] >= nums[current]) {
targets[i] -= nums[current];
if (dfs(nums, targets, current + 1)) {
return true;
}
targets[i] += nums[current];
unsolved.insert(targets[i]);
}
}
return false;
}
bool canPartitionKSubsets(vector<int>& nums, int k) {
int sum = accumulate(begin(nums), end(nums), 0);
if (sum % k != 0) {
return false;
}
sort(begin(nums), end(nums), greater<int>());
vector<int> targets(k, sum / k);
return dfs(nums, targets, 0);
}
};
解法2:状态压缩DP
由于数组最大长度是16,用一个2^16
的整数正好可以表达所有的可能性。
从低位向高位枚举,dp[state]表示当前状态下所被计算过的值之和(模去target表示一组已经满足)。
如果dp[state] + nums[j] > target
说明这种情况下是不能往里填数的,这组解不能使用。
最终只要能够一直沿着合法路线达到 (1 << nums.size()) - 1
这个状态,说明问题是可解的。
class Solution {
public:
bool canPartitionKSubsets(vector<int>& nums, int k) {
int sum = accumulate(begin(nums), end(nums), 0);
if (sum % k != 0) {
return false;
}
sum /= k;
vector<int> dp(1 << 16, -1);
dp[0] = 0;
sort(begin(nums), end(nums), greater<int>());
for (int i = 0; i < (1 << nums.size()); ++i) {
if (dp[i] == -1) {
continue;
}
for (int j = 0; j < nums.size(); ++j) {
if (dp[i] + nums[j] > sum) {
continue;
}
if (!(i & (1 << j))) {
int next = i | (1 << j);
dp[next] = (dp[i] + nums[j]) % sum;
}
}
}
return dp[(1 << nums.size()) - 1] == 0;
}
};
解法3:启发式搜索
从状压DP看来,实际上没有做DP,只是递推,通过状态压缩的方式在状态图上找了一条路径而已。
结合回溯的计算目标是所有的组合,由于每个组合都可以用位图来表示,用BFS的方式对位图进行扩张直到它能到达最终点。
这里直接BFS是可行的,但是会比较慢(不知道为什么比状压递推的方式还慢),用一个启发来做了优化,即优先考虑与最终目标位数差别较小的状态来搜索。
class Solution {
public:
typedef unsigned int Bits;
bool canPartitionKSubsets(vector<int>& nums, int k) {
int sum = accumulate(begin(nums), end(nums), 0);
if (sum % k != 0) {
return false;
}
sum /= k;
sort(begin(nums), end(nums), greater<int>());
const auto count_1 = [](int val) {
int count = 0;
while (val) {
count += val & 1;
val >>= 1;
}
return count;
};
const auto cmp = [&count_1](const auto& l, const auto& r) {
return count_1(l.first) < count_1(r.first);
};
priority_queue<pair<Bits, int>, vector<pair<Bits, int> >, decltype(cmp)> pq(cmp);
unordered_set<Bits> visited;
pq.emplace(Bits(0), 0);
visited.insert(Bits(0));
while (!pq.empty()) {
auto pr = pq.top();
pq.pop();
if (pr.first == (1 << nums.size()) - 1) {
return pr.second == 0;
}
for (int i = 0; i < nums.size(); ++i) {
Bits next = pr.first | (1 << i);
if (pr.second + nums[i] <= sum && visited.find(next) == visited.end()) {
pq.emplace(next, (pr.second + nums[i]) % sum);
visited.insert(next);
}
}
}
return false;
}
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了