我的问题是关于CodeFu练习题(2012年第2轮问题3)。它基本上归结为将一个整数数组分成两个(几乎)相等的一半并返回两者之间的最小可能差异。我在下面列出了问题描述。如评论中所述,这可以描述为balanced partition problem,这是dynamic programming领域的一个问题。
现在已经讨论了类似的问题,但是我找不到这个特定问题的有效解决方案。问题当然是,对于强力搜索而言,遍历的可能组合的数量很快变得太大(至少在使用递归时)。我有一个递归解决方案,除了最大的问题集之外,它可以正常工作。我试图添加一些可以提前停止递归的优化,但是在CodeFu允许的最大长度(30)内解决一些最大长度(30)的数组的性能仍然太慢。有关如何改进或重写代码的任何建议都将非常受欢迎。我也很想知道它是否有助于制作迭代版本。
this fine site上的更新:有关于平衡分区问题的理论讨论,它可以很好地了解如何以动态方式解决这个问题。这就是我所追求的,但我不知道如何将理论付诸实践。电影提到两个子集合中的元素可以“使用旧指针”找到,但我不知道如何。
你和你的朋友有很多不同金额的硬币。您 需要将两组硬币分开以便区别 最小的那些群体。
E.g。尺寸为1,1,1,3,5,10,18的硬币可分为:1,1,1,3,5和 10,18 1,1,1,3,5,10和18或1,1,3,5,10和1,18第三 组合是有利的,因为在这种情况下的差异 群体只有1.限制:硬币将在2到30之间 元素包含硬币的每个元素将在1和之间 100000包含
返回值:硬币拆分时可能出现的最小差异 两组
注意:CodeFu规则规定CodeFu服务器上的执行时间不得超过5秒。
Arrays.sort(coins);
lower = Arrays.copyOfRange(coins, 0,coins.length-1);
//(after sorting) put the largest element in upper
upper = Arrays.copyOfRange(coins, coins.length-1,coins.length);
smallestDifference = Math.abs(arraySum(upper) - arraySum(lower));
return findSmallestDifference(lower, upper, arraySum(lower), arraySum(upper), smallestDifference);
private int findSmallestDifference (int[] lower, int[] upper, int lowerSum, int upperSum, int smallestDifference) {
int[] newUpper = null, newLower = null;
int currentDifference = Math.abs(upperSum-lowerSum);
if (currentDifference < smallestDifference) {
smallestDifference = currentDifference;
}
if (lowerSum < upperSum || lower.length < upper.length || lower[0] > currentDifference
|| lower[lower.length-1] > currentDifference
|| lower[lower.length-1] < upper[0]/lower.length) {
return smallestDifference;
}
for (int i = lower.length-1; i >= 0 && smallestDifference > 0; i--) {
newUpper = addElement(upper, lower[i]);
newLower = removeElementAt(lower, i);
smallestDifference = findSmallestDifference(newLower, newUpper,
lowerSum - lower[i], upperSum + lower [i], smallestDifference);
}
return smallestDifference;
}
以下是一个需要很长时间才能解决的集合示例。
{100000,60000,60000,60000,60000,60000,60000,60000,60000, 60000,60000,60000,60000,60000,60000,60000,60000,60000, 60000,60000,60000,60000,60000,60000,60000,60000,60000, 60000,60000,60000}
如果您想要整个源代码,我已将其放在Ideone上。
答案 0 :(得分:3)
编辑 只是为了清楚:我已经在问题中指定了在5秒内运行的额外限制之前写了这个答案。我也写这篇文章只是为了表明有时候蛮力是可能的,即使它似乎不是。因此,这个答案并不是解决这个问题的“最佳”答案:它恰恰意味着是一个强力解决方案。作为附带好处,这个小解决方案可以帮助某人编写另一个解决方案,在可接受的时间内验证他们对“大型”阵列的答案是否正确。
问题当然是可能的组合数量 对于蛮力搜索,遍历变得太大了。
鉴于最初陈述的问题(在指定5秒的最大运行时间之前),我完全对该声明提出异议;)
您特别写道,最大长度为30。
请注意,我不是在谈论其他解决方案(例如,动态编程解决方案,根据您的约束可能会或可能不会)。
我所说的是 2 30 并不大。它有点超过十亿,就是这样。
现代CPU可以在一个内核上执行每秒数十亿个周期。
你不需要递归来解决这个问题:递归会破坏你的筹码。有一种简单的方法可以确定所有可能的左/右组合:简单地从0到2计数30 - 1并检查每一位(确定,例如,一点意味着你将值放在左边,而关闭意味着你放右边的价值)。
所以给出问题陈述,如果我没有弄错,下面的方法,没有任何优化,应该工作:
public static void bruteForce( final int[] vals) {
final int n = vals.length;
final int pow = (int) Math.pow(2, n);
int min = Integer.MAX_VALUE;
int val = 0;
for (int i = pow -1; i >= 0; i--) {
int diff = 0;
for ( int j = 0; j < n; j++ ) {
diff += (i & (1<<j)) == 0 ? vals[j] : -vals[j];
}
if ( Math.abs(diff) < min ) {
min = Math.abs(diff);
val = i;
}
}
// Some eye-candy now...
for ( int i = 0 ; i < 2 ; i ++ ) {
System.out.print( i == 0 ? "Left:" : "Right:");
for (int j = 0; j < n; j++) {
System.out.print(((val & (1 << j)) == (i == 0 ? 0 : (1<<j)) ? " " + vals[j] : ""));
}
System.out.println();
}
}
例如:
bruteForce( new int[] {2,14,19,25,79,86,88,100});
Left: 2 14 25 79 86
Right: 19 88 100
bruteForce( new int[] {20,19,10,9,8,5,4,3});
Left: 20 19
Right: 10 9 8 5 4 3
在30个元素的数组上,在我的廉价CPU上运行125秒。这是一个“初稿”,在单个核心上运行的完全未经优化的解决方案(所述问题对于并行化来说是微不足道的。)
你当然可以获得更多的发现并重复使用很多很多中间结果,因此在不到125秒的时间内解决了30个元素的数组。
答案 1 :(得分:2)
说N
是所有硬币的总和。我们需要找到一个硬币子集,其中硬币的总和最接近N/2
。让我们计算所有可能的总和并选择最好的总和。在最坏的情况下,我们可能期望2 ^ 30个可能的总和,但这可能不会发生,因为最大可能的总和是100K * 30,即3M - 远小于2 ^ 30,这将是大约1G。因此,一组3M整数或3M位应该足以容纳所有可能的总和。
所以当且仅当a
是可能的总和时,我们才有数组a[m] == 1
和m
。
我们从归零数组开始并且a[0]=1
,因为总和0
是可能的(一个没有硬币)。
for (every coin)
for (int j=0; j<=3000000; j++)
if (a[j] != 0)
// j is a possible sum so far
new_possible_sum = j + this_coin
a[new_possible_sum] = 1
当您以30 * 3M步骤完成时,您将知道所有可能的总和。找到最接近m
的号码N/2
。您的结果是abs(N-m - m)
。我希望我能适应时间和记忆。
修改:需要进行修正并进行2次优化:
N+1
(包括0),以更快地解决较小的硬币集。m
和N-m
,因此请将数组大小减小为N/2
。添加new_possible_sum
的绑定检查。丢掉更多可能的金额。