将数字公平分配为两组的算法

时间:2009-10-02 05:17:05

标签: algorithm math

给定一组n个数(1 <= n <= 100),其中每个数是1到450之间的整数,我们需要将这些数字集分配到两个集A和B中,以便以下两个案例成立:

  1. 每组中的总数最多相差1个。
  2. A中所有数字的总和与B中所有数字的总和几乎相等,即分布应该是公平的。
  3. 有人可以建议一个有效的算法来解决上述问题吗?

    谢谢。

13 个答案:

答案 0 :(得分:9)

由于数字很小,因此不是NP完全。

要解决此问题,您可以使用动态编程:

制作一张布尔三维表 其中t [s,n,i]处的true表示可以使用索引i下的n个元素的子集来达到和s。 要计算t [s,n,i]的值,请检查t [s,n,i-1]和t [s - a [i],n-1,i-1]。 然后在第二个索引n / 2处查看表格以找到最佳解决方案。

编辑:您实际上不需要一次完整的表格。您可以为每个索引i创建一个二维表t_i [s,n],并从表中为i-1计算i的表,因此您只需要这些二维表中的两个,这样可以节省大量内存。 (感谢Martin Hock。)

答案 1 :(得分:3)

这是Number Partioning Problem的约束版本。通常目标是找到任何2个不相交的子集,以最小化总和的差异。您的问题在某种意义上受到限制,您只考虑1个可能性:2组大小N / 2(或1组N / 2和一组N / 2 + 1,如果总数不均匀)。这大大减少了搜索空间,但我现在无法想出一个好的算法,我会考虑一下。

答案 2 :(得分:2)

如果数字是连续的,那么你只需在A和B之间交替分配它们。

我怀疑他们不是,在这种情况下......

将最大的未分配数分配给具有最低总和的组,除非这些组的大小差异小于或等于未分配数字的数量(在这种情况下将所有剩余数字分配给较小的组)。 / p>

在所有情况下都找不到最佳解决方案,但它非常简单。

答案 3 :(得分:2)

没关系,我认为数字是连续的。这看起来有点像Knapsack Problem,这是NP难的。


这些数字是连续的吗?

  1. 将最大数字放在A
  2. 将下一个最大的数字放在B
  3. 将下一个最大的数字放在B
  4. 将下一个最大的数字放在A
  5. 重复步骤1,直到分配完所有号码。
  6. 证明:

    在分配了4个数字的每个数字后,A和B都包含相同数量的项目,并且每个组中的项目总和是相同的,因为

    (n) + (n - 3) == (n - 1) + (n - 2)
    

    在最后一次迭代中,我们在上面的步骤1中,我们剩下0,1 1,2 [1,2]或3 [1,2,3]个数字。

    在案例0中,我们完成了,并且组的数量和重量相等。

    在案例1中,我们将数字1分配给组A.组A还有一个项目和一个权重。在这种情况下,这是公平的。

    在案例2中,我们将数字2分配给组A,将数字1分配给组B.现在组具有相同数量的项目,组A具有一个额外权重。再次,这是我们可以得到的公平。

    在案例3中,将号码3分配给组A,并将号码2和1分配给组B.现在组具有相同的权重(3 == 2 + 1),组B有一个额外项目。

答案 4 :(得分:2)

首先,在没有第一个约束的情况下找到问题的解决方案(即 - 使得和尽可能接近)。使用DP方法可以解决这个问题(您可以阅读有关DP here的更多信息,第一个问题 - 关于硬币 - 与您的非常相似)。

一旦你可以解决它,你可以再添加一个状态到DP - 已经选择到子集的人数。这给你一个N ^ 3算法。

答案 5 :(得分:2)

我有一个算法。它使用了很多递归和迭代的概念。

假设你有n个Xn,其中1&lt; = n&lt; = 100且1&lt; = Xn&lt; = 450。

  1. 如果n&lt; 3然后分配数字并停止算法,

  2. 如果n> 2然后按升序对您的号码列表进行排序,

  3. 计算所有数字的总和 S
  4. 然后将之前的总 S 除以(n - n%2)/ 2并获得A值,
  5. 现在我们将创建几个数字,这些数字的添加将尽可能接近A.获取第一个数字并找到第二个数字,以便获得一个总和 S1 尽可能接近A将 S1 放入新的数字列表中,并在内存中保留计算总和的方式,以便以后获得基数。
  6. 执行5.直到列表中的数字为&lt; 2.然后将剩余的数字放入总和列表,并重新启动算法,使用新列表指向1.
  7. 示例:

    假设:n = 7且数字为10,75,30,45,25,15,20

    通过1:

    因为n> 2对列表进行排序:10,15,20,25,30,45,75

    总和S = 220

    A = 220 /((7-1)/ 2)= 73

    全部:

    10&amp; 75 =&gt; 85

    15&amp; 45 =&gt; 60

    20&amp; 30 =&gt; 50

    剩余数字是&lt; 2所以在总和列表中加25:85(10,75),60(15,45),50(20,30),25(25)

    传递2:

    n = 4且数字为85,60,50,25

    列表计数是&gt; 2所以排序清单:25(25),50(20,30),60(15,45),85(10,75)

    总和S仍然相同(S = 220)但A必须重新计算:A = 220 /((4-0)/ 2)= 110

    全部:

    25&amp; 85 =&gt; 110

    50&amp; 60 =&gt; 110

    总和名单是:110(25(25),85(10,75)),11​​0(50(20,30),60(15,45))

    传递3:

    n = 2且数字为110,110

    n&lt; 3所以分配数字:

    A = 25,10,75

    B = 20,30,15,45

    这适用于我测试的每个场景。

答案 6 :(得分:1)

您对#2的要求需要澄清,因为: “A中所有数字的总和尽可能与B中所有数字的总和几乎相等”是明确的,但是你的陈述“分配应该公平”使得一切都不清楚。 “公平”究竟意味着什么?该过程是否需要随机元素?

答案 7 :(得分:1)

@ShreevatsaR指出,下面的算法称为贪婪算法。它在某些输入上表现不佳(我尝试了10组不同的随机生成的大小为100的输入集,在所有情况下,总和非常接近,这使我认为对输入的排序足以使该算法成功)。

另请参阅"The Easiest Hard Problem", American Scientist, March-April 2002推荐的ShreevatsaR

#!/usr/bin/perl

use strict;
use warnings;

use List::Util qw( sum );

my @numbers = generate_list();

print "@numbers\n\n";

my (@A, @B);
my $N = @numbers;

while ( @numbers ) {
    my $n = pop @numbers;
    printf "Step: %d\n", $N - @numbers;
    {
        no warnings 'uninitialized';

        if ( sum(@A) < sum(@B) ) {
            push @A, $n;
        }
        else {
            push @B, $n;
        }
        printf "A: %s\n\tsum: %d\n\tnum elements: %d\n",
            "@A", sum(@A), scalar @A;
        printf "B: %s\n\tsum: %d\n\tnum elements: %d\n\n",
            "@B", sum(@B), scalar @B;
    }
}

sub generate_list { grep { rand > 0.8 } 1 .. 450 }

请注意,generate_list会按升序返回列表。

答案 8 :(得分:0)

我认为数字不是连续的,你不能重新平衡?

由于约束1,您总是需要每隔一次插入切换桶。因此,每当您没有被迫选择一个桶时,选择一个逻辑桶(添加该号码将使得该总和更接近另一个桶)。如果这个铲斗与你之前的铲斗不同,你可以在另一个转弯处进行转弯。

答案 9 :(得分:0)

任何双背包算法都可以(无论数字的分布如何)。

答案 10 :(得分:0)

Simulated Annealing可以很快找到更好更好的答案。你可以保持1.真实,同时提高2的接近度。

答案 11 :(得分:0)

如果您需要完美的答案,那么您必须生成并循环遍历所有可能的答案集。如果您需要一个非常好的答案,那么模拟退火等技术就是您的选择。下面是一些使用非常原始的冷却计划来找到答案的C代码。

#include <stdio.h>
#include <stdlib.h>

#define MAXPAR 50
#define MAXTRIES 10000000

int data1[] = {192,130,446,328,40,174,218,31,59,234,26,365,253,11,198,98,
               279,6,276,72,219,15,192,289,289,191,244,62,443,431,363,10
              } ;
int data2[] = { 1,2,3,4,5,6,7,8,9 } ;

// What does the set sum to
int sumSet ( int data[], int len )
{
    int result = 0 ;
    for ( int i=0; i < len; ++i )
        result += data[i] ;
    return result ;
}

// Print out a set
void printSet ( int data[], int len )
{
    for ( int i=0; i < len; ++i )
        printf ( "%d ", data[i] ) ;
    printf ( " Sums to %d\n", sumSet ( data,len ) ) ;
}

// Partition the values using simulated annealing
void partition ( int data[], size_t len )
{
    int set1[MAXPAR] = {0} ;    // Parttition 1
    int set2[MAXPAR] = {0} ;    // Parttition 2
    int set1Pos, set2Pos, dataPos, set1Len, set2Len ;  // Data about the partitions
    int minDiff ; // The best solution found so far
    int sum1, sum2, diff ;
    int tries = MAXTRIES ; // Don't loop for ever

    set1Len = set2Len = -1 ;
    dataPos = 0 ;

    // Initialize the two partitions
    while ( dataPos < len )
    {
        set1[++set1Len] = data[dataPos++] ;
        if ( dataPos < len )
            set2[++set2Len] = data[dataPos++] ;
    }


    // Very primitive simulated annealing solution
    sum1 = sumSet ( set1, set1Len ) ;
    sum2 = sumSet ( set2, set2Len ) ;
    diff = sum1 - sum2 ;    // The initial difference - we want to minimize this
    minDiff = sum1 + sum2 ;
    printf ( "Initial diff is %d\n", diff ) ;

    // Loop until a solution is found or all are tries are exhausted
    while ( diff != 0 && tries > 0 )
    {
        // Look for swaps that improves the difference
        int newDiff, newSum1, newSum2 ;
        set1Pos = rand() % set1Len ;
        set2Pos = rand() % set2Len ;

        newSum1 = sum1 - set1[set1Pos] + set2[set2Pos] ;
        newSum2 = sum2 + set1[set1Pos] - set2[set2Pos] ;
        newDiff = newSum1 - newSum2 ;
        if ( abs ( newDiff ) < abs ( diff ) ||      // Is this a better solution?
                tries/100 > rand() % MAXTRIES )     // Or shall we just swap anyway - chance of swap decreases as tries reduces
        {
            int tmp = set1[set1Pos] ;
            set1[set1Pos] = set2[set2Pos] ;
            set2[set2Pos] = tmp ;
            diff = newDiff ;
            sum1 = newSum1 ;
            sum2 = newSum2 ;

            // Print it out if its the best we have seen so far
            if ( abs ( diff ) < abs ( minDiff ) )
            {
                minDiff = diff ;
                printSet ( set1, set1Len ) ;
                printSet ( set2, set2Len ) ;
                printf ( "diff of %d\n\n", abs ( diff ) ) ;
            }
        }


        --tries ;
    }

    printf ( "done\n" ) ;
}


int main ( int argc, char **argv )
{
    // Change this to init rand from the clock say if you don't want the same
    // results repoduced evert time!
    srand ( 12345 ) ;
    partition ( data1, 31 ) ;
    partition ( data2, 9 ) ;
    return 0;
}

答案 12 :(得分:-1)

我会尝试使用遗传算法,因为这似乎是应用它们的一个非常好的问题。

编码只是长度为N的二进制字符串,表示0表示第一组,1表示第二组。当每组中的元素数量不同时给出负面适应度,当总和相似时给出正适应性......类似于:

fitness(gen) = (sum(gen)-n/2))^2 + (sum(values[i]*(-1)**gen[i] for i in 0..n))^2

(并尽量减少适应性)

当然,这可以给你一个次优的答案,但对于大的现实世界的问题,这通常就足够了。