将具有不同值的N个项目分配到仅在组中的项目总和为D时才有效的组中

时间:2015-11-18 07:23:06

标签: java algorithm

我有一个N个整数数组,每个整数都有5到20之间的不同值。我想从它们中形成尽可能多的组,使得它们的总和是整数值D.

对于形成的组数,组中有多少个数或者必须使用数组的每个元素没有限制;我们必须创建尽可能多的组,只有一组足够。

以下是我的限制:

  

5< = V:项目的值< = 20
  5< = N:元素数< = 10000
  20< = D:单个组的数量之和< = 2000

示例

A:
阵列:{5,7,7,6,6,8,8,10,11,12}
d:40
一种公认的解决方案:
[[5,7,8,8,12],[7,6,6,10,11]]

B:
阵列:{5,6,6,7,7,10}
d:20
一种公认的解决方案:
[[6,7,7]]

建议的算法最好应该是psicocode或基于C语言,Java或Phyton。如果接受的答案不是Java,那么在接受解决方案之后,它将被移植到Java中并在此处共享。

感谢。

编辑:似乎无论选择何种方法,如果N非常大,我将花费大量时间来计算,我将尝试将N的最大值限制为50-100倍,然后测试更大的情况。< / p>

EDIT2:很多很棒的解决方案,我需要一段时间来检查它。

3 个答案:

答案 0 :(得分:5)

认为这是NP完全问题(减少到subset-sum problem)。一个简单的解决方案就是:

  1. 遍历每个号码
  2. 如果数字大于D丢弃并转到列表中的下一个数字
  3. 如果数字等于D创建新组并添加此数字,请继续列表中的下一个数字
  4. 如果数量少于D   a。如果存在当前总和加上此数字等于或小于D的活动组,请将此数字添加到该组并更新该组的当前总和值   b。否则创建一个新组添加此编号并将组的总和值设置为此数字
  5. 继续直到列表中的所有数字都已遍历
  6. 获取当前总和值为D
  7. 的所有活动组

    注意:您必须维护一个活动组列表及其当前总和值。

    上述算法将提供线性时间的解决方案,但不一定是最优解(参见上面的NP-complete)

    <强>更新

    可以使用local-search techniques来优化上述算法,这可以使其更频繁地找到最优解,但仍然不一定总是多项式时间内的最优解。本地搜索将放在上述算法的第4步中。

    <强> UPDATE2

    请注意,问题严重依赖于实际数据(而不仅仅是其大小)。例如,对于具有全1(值为1)的数据,问题是多项式。

    <强> UPDATE3

    另一种方法(可能比完全穷举搜索稍快)是预先计算包含您感兴趣的数字范围的partitions of number D。然后遍历列表并检查如果当前数字是D的任何(预先计算的)分区的一部分。这将提供精确的最佳解决方案。但计算分区通常不是多项式的。

答案 1 :(得分:3)

我会发布一个部分解决方案,但是,我相信在你的大多数情况下会帮助你。我的建议是你将这个用于解决太大问题的解决方案近似(我认为这应该是相对罕见的 - 所有MMORPG玩家都面临10000个数字吗?)。

首先,让我们稍微转换你的数据集,用一个计算每个数字出现次数的地图代替数字集:

{6,7,8,5,6,9,11,6,5,5,5} - &gt; {5 =&gt; 4,6 =&gt; 3,7 =&gt; 1,8 =&gt; 1,9 =&gt; 1,11 =&gt; 1} Java的解决方案(上面=>实际上让我无法编写一个包含20个值的数组,其中大部分都是零。上面考虑numberMap [5] = 4等。)

int [] numberMap = new int[21];
for (Integer number : numbers) {
   numberMap[number]++;
}

为了继续我的解决方案,我需要对整个数字集的子组进行编码和解码的函数:

int [] encode(int [] subgroup) {
    long result = 0;
    for (int i = 0; i <= 20; i++) {
        if (i > 0) {
           result *= numberMap[i - 1];
        }
        result += subgroup[i];
    }
    return result;
}

int [] decode(long code) {
    int []result = new int[21];
    for (int i = 20; i >= 0; i++) {
       result[i] = code % numberMap[i];
       code /= numberMap;
    }
    return result
}

现在,让我们使用递归

查找将为您提供金额G的所有子组
List<Long> subgroupsSumG = new ArrayList<Long>();

recursion(5, new int[21], 0);

void recursion(int currentIndex, int [] subgroup, int sum) {
  if (currentIndex == 21) {
     if (sum == G) {
        subgroupSumG.add(encode(subgroup));
     }
     return;
  }
  if (sum > G) {
     return;
  }
  for (int i = 0; i <= numberMap[currentIndex]; i++) {
     subgroup[currentIndex] = i;
     recursion(currentIndex + 1, subgroup, sum + i * currentIndex);
  }
}

完美,所以现在我们知道哪些子组可以形成一个具有和G的单个组。我们应该如何找到最多可以获得多少个这样的子组?这现在变成了具有多个尺寸的物品的背包问题。

long maximumGroupCode = encode(numberMap);
int [] optimalNumberOfGroups = new int[maximumGroupCode];
int maximumNumberOfGroups = 0;
for (long i = 0; i < maximumGroupCode; i++) {
   if (i != 0 && optimalNumberOfGroups[i] == 0) {
     continue;
   }
   int [] decodedGroup = decode(i);
   group_for: for (Long subgroupCode : subgroupsSumG) {
       int [] subgroup = decode(subgroupCode);
       for (int j = 5; j <= 20; j++) {
          if (numberMap[j] < decodedGroup[j] + subgroup[j]) {
              continue group_for;
          }
       }
       long neighbouringGroupCode = subgroupCode + i;
       optimalNumberOfGroups[neighbouringGroupCode] = Math.max(optimalNumberOfGroups[neighbouringGroupCode], optimalNumberOfGroups[i] + 1);
       maximumNumberOfGroups = Math.max(maximumNumberOfGroups, optimalNumberOfGroups[neighbouringGroupCode]);
    }
}

完美,所以现在你的答案是maximumNumberOfGroups变量。当代码完成其工作时,此代码将始终为您提供最佳解决方案。这会发生在:

  • maximumGroupCode * subgroupsSumG.size() * 16大约小于10 ^ 10
  • maximumGroupCode小于10 ^ 8(出于内存原因)。

请确保在运行第一个程序framgent之前计算maximumGroupCode,因为如果此数字变得非常大,可能会花费无限的时间。并且顺便说一下,即使long可能太小而无法保持maximumGroupCode值。也许BigInteger应该用于初始计算。

答案 2 :(得分:2)

回溯是您正在寻找的解决方案。

提示:对于您的数组Array:{5,7,7,6,6,8,8,10,11,12},您可以创建相同大小的true / false或0/1数组。

Array :{5,7,7,6,6,8,8,10,11,12}
Array2:{0,0,0,0,0,0,0,0, 0, 0}

Array2代表实际的组。 0表示它不属于那里,1表示它属于那里。如果你想迭代所有的解决方案,你可以把它作为增加二进制数来处理,在这种情况下你得到

0000000000
0000000001
0000000010
0000000011
0000000100
0000000101
....
1111111111

例如,如果你有这个

Array :{5,7,7,6,6,8,8,10,11,12}
Array2:{1,0,0,0,1,0,0,1, 0, 0}

子组是5+6+10=21(您只需编写可以计算的方法)

对于迭代所有解决方案,您可以使用回溯或仅实现“将二进制数增加一个”

还要记住,这是NPC问题,这意味着复杂度为O(2 ^ n)。在现实生活中,它意味着在个人计算机上,只有30或更小的阵列大小才能在合理的时间内获得解决方案。

对于40+的数组大小,可能需要数小时或更长时间。

然而,在现实生活中,它很大程度上取决于输入数据。如果一个解决方案足够并且您获得了丰富的解决方案数据,那么您可以在合理的时间内获得平均结果。

例如,您可以尝试2 ^ 30种可能性,如果您没有找到解决方案,您可以说“无法找到此数据的解决方案”或“此数据的解决方案可能不存在”或您可以使用可以返回的启发式解决方案不是“完美”但是“接近”。

相关问题