使用位掩码+DP将数组分成K个子集,使得所有子集的总和相同
Dividing an array into K subsets such that sum of all subsets is same using bitmasks+DP
所以,这个问题我不知道如何解决问题陈述是:
Given a set S of N integers the task is decide if it is possible to
divide them into K non-empty subsets such that the sum of elements in
every of the K subsets is equal.
N最多可以有20个。K最多可以有8个
问题具体使用DP+Bitmasks解决!
我不知道从哪里开始!由于要维护 K 个集合,我不能采用 K 个状态,每个状态代表一些或另一个!
如果我尝试将整个集合作为一个状态,将 K 作为另一个状态,我在创建循环关系时会遇到问题!
你能帮忙吗??
原题linkProblem
UPD:我把 N 和 K 弄混了,我的想法是正确的,但不是 efficient.Efficient 最后添加的想法
假设到目前为止您已经创建了 k-1 个子集,现在您想要创建第 k 个子集。要创建第 k 个子集,您需要能够回答以下两个问题:
1-第k个子集的元素之和应该是多少?
2- 到目前为止使用了哪些元素?
第一个问题的答案很简单,总和应该等于所有元素的总和除以K,我们命名为subSum.
对于第二个问题,我们需要知道每个元素的状态,使用与否。这里就需要用到位掩码思路了。
这是 dp 循环:
dp[i][mask] = 表示是否可以创建 i 个子集,每个子集的总和等于 subSum,使用 1(未使用)的元素掩码(以其位表示),因此 dp[i][mask] 是布尔类型。
dp[i][mask] = OR(dp[i-1][mask2]) 对于所有可能的 mask2 状态。 mask2 将通过将掩码的某些 1 转换为 0 来生成,即我们希望成为第 i 个子集的元素的那些 1。
为了检查所有可能的 mask2,你需要检查可用 1 的所有 2^n 个可能的子集 bits.Therefore,总的来说,时间复杂度将是 O(N*(2^n)*(2^ n)).在你的问题中是 20*2^8*2^8= 10*2^17 < 10^7 可以通过时间限制。
显然,对于基本情况,您必须自己处理 dp[0][mask],而不使用 recurrence.Final 答案是 dp[K][2^N-1] 是真还是没有。
__UPD__:为了获得更好的性能,在进入DP之前,你可以用subSum[=52=之和预处理所有子集].然后,为了计算mask2,你只需要遍历预处理后的列表,看它们与mask的AND运算是否会得到列表中的子集。
UPD2:
为了获得有效的解决方案,而不是找到合适的 mask2,我们可以利用这样一个事实,即在每一步中,我们都知道到该点为止的元素总和。所以我们可以将元素一个一个地添加到掩码中,只要我们有一个可以被 K 整除的总和,我们就可以进入下一步创建下一个子集。
if(掩码使用的元素之和能被K整除)
dp[i][mask]= dp[i+1][mask];
其他
dp[i][mask]|=dp[i][mask ^(1<<i)] provided that i-th item is not used and can not exceed the current sum more than i*subSum.
这是 JavaScript 中的工作 O(K*2^N*N) 实现。来自伪代码https://discuss.codechef.com/questions/58420/sanskar-editorial
function equality(set, size, count) {
if(size < count) { return false; }
var total = set.reduce(function(p, c) { return p + c; }, 0);
if((total % count) !== 0) { return false }
var subsetTotal = total / count;
var search = {0: true};
var nextSearch = {};
for(var i=0; i<count; i++) {
for(var bits=0; bits < (1 << size); bits++){
if(search[bits] !== true) { continue; }
var sum = 0;
for(var j=0; j < size; j++) {
if((bits & (1 << j)) !== 0) { sum += set[j]; }
}
sum -= i * subsetTotal;
for(var j=0; j < size; j++) {
if((bits & (1 << j)) !== 0) { continue; }
var testBits = bits | (1 << j);
var tmpTotal = sum + set[j];
if(tmpTotal == subsetTotal) { nextSearch[testBits] = true; }
else if(tmpTotal < subsetTotal) { search[testBits] = true; }
}
}
search = nextSearch;
nextSearch = {};
}
if(search[(1 << size) - 1] === true) {
return true;
}
return false;
}
console.log(true, equality([1,2,3,1,2,3], 6, 2));
console.log(true, equality([1, 2, 4, 5, 6], 5, 3));
console.log(true, equality([10,20,10,20,10,20,10,20,10,20], 10, 5));
console.log(false, equality([1,2,4,5,7], 5, 3));
EDIT 该算法找到所有满足条件(总和 tmpTotal小于或等于理想子集总和subsetTotal)。按所需的子集数量 count 重复此过程,你要么有一个位掩码,其中设置了所有 size 位,这意味着成功或测试失败。
示例
设置 = [1, 2, 1, 2]
大小 = 4
count = 2,我们想尝试将集合分成2个子集
子集总数 = (1+2+1+2) / 2 = 3
迭代 1:
搜索 = {0b:正确,1b:正确,10b:正确,100b:正确,1000b:正确,101b:正确}
nextSearch = {11b:正确,1100b:正确,110b:正确,1001b:正确}
迭代 2:
搜索 = {11b:正确,1100b:正确,110b:正确,1001b:正确,111b:正确,1101b:正确}
nextSearch = {1111b: true}
最终检查
(1 << 尺寸) == 10000b, (1 << 尺寸) - 1 == 1111b
由于 nextSearch[1111b] 存在,我们 return 成功。
你可以在O(N * 2^N)中解决这个问题,所以K对于复杂度来说是没有意义的。
首先让我警告你所有数字都为零的极端情况 N < K,其中答案是 "no"。
我的算法思路如下。假设我们已经计算了每个掩码的总和(可以在 O(2^N) 中完成)。我们知道对于每一组,总和应该是总和除以K。
我们可以做一个带有掩码的 DP,其中状态只是一个二进制掩码,告诉已经使用了哪些数字。从算法复杂性中移除 K 的关键思想是注意到如果我们知道使用了哪些数字,我们就知道到目前为止的总和,所以我们也知道我们现在正在填充哪个组(当前总和/组总和)。然后尝试select组的下一个数字:如果我们不超过组预期总和,它将有效。
你可以查看我的C++代码:
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
typedef long long ll;
ll v[21 + 5];
ll sum[(1 << 21) + 5];
ll group_sum;
int n, k;
void compute_sums(int position, ll current_sum, int mask)
{
if (position == -1)
{
sum[mask] = current_sum;
return;
}
compute_sums(position - 1, current_sum, mask << 1);
compute_sums(position - 1, current_sum + v[position], (mask << 1) + 1);
}
void solve_case()
{
cin >> n >> k;
for (int i = 0; i < n; ++i)
cin >> v[i];
memset(sum, 0, sizeof(sum));
compute_sums(n - 1, 0, 0);
group_sum = sum[(1 << n) - 1];
if (group_sum % k != 0)
{
cout << "no" << endl;
return;
}
if (group_sum == 0)
{
if (n >= k)
cout << "yes" << endl;
else
cout << "no" << endl;
return;
}
group_sum /= k;
vector<int> M(1 << n, 0);
M[0] = 1;
for (int mask = 0; mask < (1 << n); ++mask)
{
if (M[mask])
{
int current_group = sum[mask] / group_sum;
for (int i = 0; i < n; ++i)
{
if ((mask >> i) & 1)
continue;
if (sum[mask | (1 << i)] <= group_sum * (current_group + 1))
M[mask | (1 << i)] = 1;
}
}
}
if (M[(1 << n) - 1])
cout << "yes" << endl;
else
cout << "no" << endl;
}
int main()
{
int cases;
cin >> cases;
for (int z = 1; z <= cases; ++z)
solve_case();
}
所以,这个问题我不知道如何解决问题陈述是:
Given a set S of N integers the task is decide if it is possible to divide them into K non-empty subsets such that the sum of elements in every of the K subsets is equal.
N最多可以有20个。K最多可以有8个
问题具体使用DP+Bitmasks解决!
我不知道从哪里开始!由于要维护 K 个集合,我不能采用 K 个状态,每个状态代表一些或另一个!
如果我尝试将整个集合作为一个状态,将 K 作为另一个状态,我在创建循环关系时会遇到问题!
你能帮忙吗??
原题linkProblem
UPD:我把 N 和 K 弄混了,我的想法是正确的,但不是 efficient.Efficient 最后添加的想法
假设到目前为止您已经创建了 k-1 个子集,现在您想要创建第 k 个子集。要创建第 k 个子集,您需要能够回答以下两个问题:
1-第k个子集的元素之和应该是多少?
2- 到目前为止使用了哪些元素?
第一个问题的答案很简单,总和应该等于所有元素的总和除以K,我们命名为subSum.
对于第二个问题,我们需要知道每个元素的状态,使用与否。这里就需要用到位掩码思路了。
这是 dp 循环:
dp[i][mask] = 表示是否可以创建 i 个子集,每个子集的总和等于 subSum,使用 1(未使用)的元素掩码(以其位表示),因此 dp[i][mask] 是布尔类型。
dp[i][mask] = OR(dp[i-1][mask2]) 对于所有可能的 mask2 状态。 mask2 将通过将掩码的某些 1 转换为 0 来生成,即我们希望成为第 i 个子集的元素的那些 1。
为了检查所有可能的 mask2,你需要检查可用 1 的所有 2^n 个可能的子集 bits.Therefore,总的来说,时间复杂度将是 O(N*(2^n)*(2^ n)).在你的问题中是 20*2^8*2^8= 10*2^17 < 10^7 可以通过时间限制。
显然,对于基本情况,您必须自己处理 dp[0][mask],而不使用 recurrence.Final 答案是 dp[K][2^N-1] 是真还是没有。
__UPD__:为了获得更好的性能,在进入DP之前,你可以用subSum[=52=之和预处理所有子集].然后,为了计算mask2,你只需要遍历预处理后的列表,看它们与mask的AND运算是否会得到列表中的子集。
UPD2: 为了获得有效的解决方案,而不是找到合适的 mask2,我们可以利用这样一个事实,即在每一步中,我们都知道到该点为止的元素总和。所以我们可以将元素一个一个地添加到掩码中,只要我们有一个可以被 K 整除的总和,我们就可以进入下一步创建下一个子集。
if(掩码使用的元素之和能被K整除)
dp[i][mask]= dp[i+1][mask];
其他
dp[i][mask]|=dp[i][mask ^(1<<i)] provided that i-th item is not used and can not exceed the current sum more than i*subSum.
这是 JavaScript 中的工作 O(K*2^N*N) 实现。来自伪代码https://discuss.codechef.com/questions/58420/sanskar-editorial
function equality(set, size, count) {
if(size < count) { return false; }
var total = set.reduce(function(p, c) { return p + c; }, 0);
if((total % count) !== 0) { return false }
var subsetTotal = total / count;
var search = {0: true};
var nextSearch = {};
for(var i=0; i<count; i++) {
for(var bits=0; bits < (1 << size); bits++){
if(search[bits] !== true) { continue; }
var sum = 0;
for(var j=0; j < size; j++) {
if((bits & (1 << j)) !== 0) { sum += set[j]; }
}
sum -= i * subsetTotal;
for(var j=0; j < size; j++) {
if((bits & (1 << j)) !== 0) { continue; }
var testBits = bits | (1 << j);
var tmpTotal = sum + set[j];
if(tmpTotal == subsetTotal) { nextSearch[testBits] = true; }
else if(tmpTotal < subsetTotal) { search[testBits] = true; }
}
}
search = nextSearch;
nextSearch = {};
}
if(search[(1 << size) - 1] === true) {
return true;
}
return false;
}
console.log(true, equality([1,2,3,1,2,3], 6, 2));
console.log(true, equality([1, 2, 4, 5, 6], 5, 3));
console.log(true, equality([10,20,10,20,10,20,10,20,10,20], 10, 5));
console.log(false, equality([1,2,4,5,7], 5, 3));
EDIT 该算法找到所有满足条件(总和 tmpTotal小于或等于理想子集总和subsetTotal)。按所需的子集数量 count 重复此过程,你要么有一个位掩码,其中设置了所有 size 位,这意味着成功或测试失败。
示例
设置 = [1, 2, 1, 2]
大小 = 4
count = 2,我们想尝试将集合分成2个子集
子集总数 = (1+2+1+2) / 2 = 3
迭代 1:
搜索 = {0b:正确,1b:正确,10b:正确,100b:正确,1000b:正确,101b:正确}
nextSearch = {11b:正确,1100b:正确,110b:正确,1001b:正确}
迭代 2:
搜索 = {11b:正确,1100b:正确,110b:正确,1001b:正确,111b:正确,1101b:正确}
nextSearch = {1111b: true}
最终检查
(1 << 尺寸) == 10000b, (1 << 尺寸) - 1 == 1111b
由于 nextSearch[1111b] 存在,我们 return 成功。
你可以在O(N * 2^N)中解决这个问题,所以K对于复杂度来说是没有意义的。
首先让我警告你所有数字都为零的极端情况 N < K,其中答案是 "no"。
我的算法思路如下。假设我们已经计算了每个掩码的总和(可以在 O(2^N) 中完成)。我们知道对于每一组,总和应该是总和除以K。
我们可以做一个带有掩码的 DP,其中状态只是一个二进制掩码,告诉已经使用了哪些数字。从算法复杂性中移除 K 的关键思想是注意到如果我们知道使用了哪些数字,我们就知道到目前为止的总和,所以我们也知道我们现在正在填充哪个组(当前总和/组总和)。然后尝试select组的下一个数字:如果我们不超过组预期总和,它将有效。
你可以查看我的C++代码:
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
typedef long long ll;
ll v[21 + 5];
ll sum[(1 << 21) + 5];
ll group_sum;
int n, k;
void compute_sums(int position, ll current_sum, int mask)
{
if (position == -1)
{
sum[mask] = current_sum;
return;
}
compute_sums(position - 1, current_sum, mask << 1);
compute_sums(position - 1, current_sum + v[position], (mask << 1) + 1);
}
void solve_case()
{
cin >> n >> k;
for (int i = 0; i < n; ++i)
cin >> v[i];
memset(sum, 0, sizeof(sum));
compute_sums(n - 1, 0, 0);
group_sum = sum[(1 << n) - 1];
if (group_sum % k != 0)
{
cout << "no" << endl;
return;
}
if (group_sum == 0)
{
if (n >= k)
cout << "yes" << endl;
else
cout << "no" << endl;
return;
}
group_sum /= k;
vector<int> M(1 << n, 0);
M[0] = 1;
for (int mask = 0; mask < (1 << n); ++mask)
{
if (M[mask])
{
int current_group = sum[mask] / group_sum;
for (int i = 0; i < n; ++i)
{
if ((mask >> i) & 1)
continue;
if (sum[mask | (1 << i)] <= group_sum * (current_group + 1))
M[mask | (1 << i)] = 1;
}
}
}
if (M[(1 << n) - 1])
cout << "yes" << endl;
else
cout << "no" << endl;
}
int main()
{
int cases;
cin >> cases;
for (int z = 1; z <= cases; ++z)
solve_case();
}