硬币分裂算法的性能

时间:2012-10-31 11:29:56

标签: java arrays performance recursion split

我的问题是关于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上。

2 个答案:

答案 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] == 1m

我们从归零数组开始并且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次优化:

  1. 按降序排列数组。否则一美元硬币将一次覆盖整个阵列。
  2. 将数组的大小限制为N+1(包括0),以更快地解决较小的硬币集。
  3. 由于我们几乎总是得到2个相同的结果:mN-m,因此请将数组大小减小为N/2。添加new_possible_sum的绑定检查。丢掉更多可能的金额。