我有一个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:很多很棒的解决方案,我需要一段时间来检查它。
答案 0 :(得分:5)
认为这是NP完全问题(减少到subset-sum problem)。一个简单的解决方案就是:
D
丢弃并转到列表中的下一个数字D
创建新组并添加此数字,请继续列表中的下一个数字D
a。如果存在当前总和加上此数字等于或小于D
的活动组,请将此数字添加到该组并更新该组的当前总和值
b。否则创建一个新组添加此编号并将组的总和值设置为此数字D
注意:您必须维护一个活动组列表及其当前总和值。
上述算法将提供线性时间的解决方案,但不一定是最优解(参见上面的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种可能性,如果您没有找到解决方案,您可以说“无法找到此数据的解决方案”或“此数据的解决方案可能不存在”或您可以使用可以返回的启发式解决方案不是“完美”但是“接近”。