将数组划分为K个子集,使得使用位掩码+ DP的所有子集的总和相同

时间:2015-07-16 18:50:21

标签: algorithm language-agnostic dynamic-programming

所以,这个问题我没有任何线索如何解决它的问题陈述是:

  

给定N个整数的集合S,任务决定是否可能   将它们分成K个非空子集,使得它们中的元素之和   每个K子集都是相等的。

N可以是最大值20.K可以是最大值8

问题是要使用DP + Bitmasks专门解决!

我无法理解从哪里开始!由于有K套维护,我不能把K各自代表一些或另一个!

如果我尝试将整个集合作为状态而将K作为另一个,我在创建循环关系时遇到问题!

你能帮忙吗?

原始问题Problem

的链接

3 个答案:

答案 0 :(得分:4)

你可以在O(N * 2 ^ N)中解决问题,因此K对于复杂性没有意义。

首先让我警告你角落情况N< K所有数字都为零,答案为“否”。

我的算法的想法如下。假设我们已经计算了每个掩模的总和(可以在O(2 ^ N)中完成)。我们知道,对于每个组,总和应该是总和除以K.

我们可以使用掩码执行DP,其中状态只是一个二进制掩码,告诉我们使用了哪些数字。从算法复杂度中删除K的关键思想是注意到如果我们知道使用了哪些数字,我们知道到目前为止的总和,所以我们也知道我们现在正在填充哪个组(当前和/组和)。然后尝试选择组的下一个数字:如果我们不超过组预期总和,它将有效。

您可以查看我的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();
}

答案 1 :(得分:3)

这是JavaScript中的工作O(K * 2 ^ N * N)实现。来自伪代码https://discuss.codechef.com/questions/58420/sanskar-editorial

http://jsfiddle.net/d7q4o0nj/

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));

编辑该算法找到符合条件的所有位掩码(代表子集)(总和 tmpTotal 小于或等于理想子集sum subsetTotal )。通过所需的子集数量 count 重复此过程,您可以使用位掩码,其中设置所有 size 位,这意味着成功或测试失败。

示例

设置 = [1,2,1,2]

size = 4

count = 2,我们想尝试将该组划分为2个子集

subsetTotal =(1 + 2 + 1 + 2)/ 2 = 3

迭代1:

search = {0b:true,1b:true,10b:true,100b:true,1000b:true,101b:true}

nextSearch = {11b:true,1100b:true,110b:true,1001b:true}

迭代2:

search = {11b:true,1100b:true,110b:true,1001b:true,111b:true,1101b:true}

nextSearch = {1111b:true}

最终检查

(1&lt;&lt; size)== 10000b,(1&lt;&lt; size) - 1 == 1111b

由于 nextSearch [1111b] 存在,我们将返回成功。

答案 2 :(得分:-1)

UPD:我把N和K相互混淆了,我的想法是对的,但效率不高。最后添加了有效的想法

假设到目前为止您已经创建了k-1个子集,现在您想要创建第k个子集。要创建第k个子集,您需要能够回答这两个问题:

1-第k个子集的元素总和是什么?

2-到目前为止使用了哪些元素?

回答第一个问题很简单,总和应该等于所有元素之和除以K,我们将其命名为 subSum

对于第二个问题,我们需要使用或不使用每个元素的状态。这里我们需要使用位掩码的想法。

这里是dp重现:

dp [i] [mask] =表示可以创建i子集,每个子​​集的总和等于 subSum ,使用掩码中的1(未使用)元素(在其位中)表示),所以dp [i] [mask]是一个布尔类型。

  对于所有可能的 mask2 状态,

dp [i] [mask] = OR(dp [i-1] [mask2])。 mask2 将通过将1的掩码转换为0来生成,即我们希望成为第i个子集的元素的那些1。

为了检查所有可能的mask2,你需要检查可用1位的所有2 ^ n个子集。因此,总的来说,时间复杂度将是O(N *(2 ^ n)*(2 ^ N))。你的问题是20 * 2 ^ 8 * 2 ^ 8 = 10 * 2 ^ 17&lt; 10 ^ 7可以通过时限。

显然,对于基本情况,你必须自己处理dp [0] [mask],而不使用recurrence.Final答案是dp [K] [2 ^ N-1]是否为真。

__ UPD __ :为了获得更好的性能,在进入DP之前,您可以使用 subSum 之和预处理所有子集。然后,为了计算mask2,您只需要遍历预处理列表,并查看带掩码的AND操作是否会导致列表中的子集。

<强> UPD2: 为了获得有效的解决方案,我们可以使用这样一个事实:在每一步,我们都知道直到那一点的元素总和。所以我们可以逐个添加元素到掩码中,每当我们有一个可被 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.