11

这是问题所在:

您有 N 个(N 代表您拥有的数字的数量)个数。将它们分成两组,使组中数字之和之间的差异最小。

例子:

5 // N

1, 9, 5, 3, 8 // The numbers

如果我们将 1、9 和 3 放入 A 组,将 5 和 8 放入 B 组,则差异为 0。

我认为首先我应该计算所有数字的总和并将其除以 2。然后检查任何可能的数字组合,其总和不高于所有数字总和的一半。完成此操作后,我将选择最大的数字并打印出组。

我在遍历所有组合时遇到问题,特别是当 N 是大数字时。如何运行所有组合?


另外我的想法有点不同,我会按降序对数字进行分组,然后将最大的数字放在 A 组中,将最小的数字放在 B 组中。然后我反过来做。这适用于某些数字,但有时它不会显示最佳分组。例如:

如果我使用前面的例子。按降序排列数字。

9, 8, 5, 3, 1.

把最大的放在 A 组,把最小的放在 B 组。

Group A: 9
Group B: 1

反过来。

Group A: 9, 3
Group B: 1, 8

等等。如果最后我只有一个数字,我会把它放在总和较低的组中。所以我最终会得到:

Group A: 9, 3
Group B: 1, 8, 5

这不是最佳分组,因为差异为 2,但以不同的方式分组,差异可能为 0,正如我所展示的。

如何获得最佳分组?

代码:

#include <iostream>
#include <cmath>
#include <string>
using namespace std;
int convertToBinary(int number) {
    int remainder;
    int binNumber = 0;
    int i = 1;
    while(number!=0)
    {
        remainder=number%2;
        binNumber=binNumber + (i*remainder);
        number=number/2;
        i=i*10;
    }
    return binNumber;
}
int main()
{
    int number, combinations, sum = 0;
    double average;
    cin >> number;
    int numbers[number];
    for(int i = 0; i<number; i++)
    {
        cin >> numbers[i];
        sum += numbers[i];
    }
    if(sum%2 == 0)
    {
        average = sum/2;
    }
    else
    {
        average = sum/2 + 0.5;
    }
    combinations = pow(2,number-1);
    double closest = average;
    for(int i = 0; i<=combinations;i++)
    {
        int rem;
        int temp_sum = 0;
        int state = convertToBinary(i);
        for(int j = 0; state!=0; j++)
        {
            int rem =state%10;
            state = state/10;
            if(rem == 1)
            {
                temp_sum = temp_sum + numbers[j];
            }
        }
        if(abs(average-temp_sum)<closest)
        {
            closest = abs(average-temp_sum);
            if(closest == 0)
            {
                break;
            }
        }
    }
    cout << closest*2;
    return 0;
}
4

4 回答 4

2

尽管正如其他人所评论的那样,这是一个 NP-Complete 问题,但您提供了两个相当有用的界限:您只想将一组数字分成两组,并且希望使两组的总和尽可能接近.

您提出计算数字总和并将其除以二的建议是正确的起点 - 这意味着您知道每个组的理想总和是多少。我还怀疑您最好的选择是首先将最大的数字放入 A 组。(它必须放入一组,而这是稍后放置的最差的一组,所以为什么不把它放在那里呢?)

这是当我们进入启发式方法时,您循环遍历直到组完成:

N: Size of list of numbers.
t: sum of numbers divided by two (t is for target)

1. Is there a non-placed number which gets either group to within 0.5 of t? If so, put it in that group, put the remaining numbers in the other group and you're done.
2. If not, place the biggest remaining number in the group with the current lowest sum
3. go back to 1.

无疑会有失败的案例,但作为一种粗略的方法,这应该经常接近。要实际编写上述代码,您需要将数字放在有序列表中,以便从大到小轻松处理它们。(然后,步骤 1 也可以通过检查(针对两个“迄今为止的组”)从最大剩余向下直到添加到正在检查的数字的“迄今为止的组”比 t 低于 1.0 来简化 - 之后条件不能满足.)

请让我知道这是否有效!

于 2013-03-06T18:34:14.213 回答
1

如果你能找到一组数字,其总和正好是总数的一半,那么使用只有两组的约束 zour 问题就已经解决了。因此,我建议您尝试找到该组并将其余部分明显放入另一组。

将最大数放在第一组的假设很简单。现在剩下的就更棘手了。

这在二进制系统中很简单:考虑到每个数字都有一个位。比特蜂鸣1表示该号码在组中A,否则为在组中B。整个分布可以通过这些位的串联来描述。这可以被认为是一个数字。所以要检查所有组合,您必须遍历所有数字并计算组合。

代码:

#include <iostream>
#include <memory>
using namespace std;

int partition(const std::unique_ptr<int[]>& numbers, int elements) {
    int sum = 0;

    for(int i=0; i<elements; ++i) {
        sum += numbers[i];
    }

    double average = sum/2.0;
    double closest = average+.5;

    int beststate = 0;


    for(int state=1; state< 1<<(elements-1);++state) { 
        int tempsum = 0;
        for(int i=0; i<elements; ++i) {
            if( state&(1<<i) ) {
                tempsum += numbers[i];
            }
        }

        double delta=abs(tempsum-average);
        if(delta < 1) { //if delta is .5 it won't get better i.e. (3,5) (9) => average =8.5
            cout << state;
            return state;
        }

        if(delta<closest) {
            closest   = delta;
            beststate = state;
        }
    }

    return beststate;
}

void printPartition(int state, const std::unique_ptr<int[]>& numbers, int elements) {
    cout << "(";
    for(int i=0; i<elements; ++i) {
        if(state&(1<<i)) {
            cout << numbers[i]<< ",";
        }
    }
    cout << ")" << endl;
}

int main()
{
    int elements;

    cout << "number of elements:";
    cin >> elements;

    std::unique_ptr<int[]> numbers(new int[elements]);

    for(int i = 0; i<elements; i++)
    {
        cin >> numbers[i];
    }

    int groupA = partition(numbers, elements);

cout << "\n\nSolution:\n";
    printPartition(groupA, numbers, elements);
    printPartition(~groupA,numbers, elements);
    return 0;
}

编辑:有关生成所有可能性的进一步(和更好)解决方案,请检查此 awnser。这是我在这里找到的knuths 书的链接

edit2:按要求解释枚举的概念:

假设我们有三个元素,1,23,5。可以通过填写表格来生成所有可能的组合,而不考虑排列:

1 | 23 | 5         Concatination   Decimal interpretation
-----------
0 |  0 | 0         000                0
0 |  0 | 1         001                1
0 |  1 | 0         010                2
0 |  1 | 1         011                3
1 |  0 | 0         100                4
1 |  0 | 1         101                5
1 |  1 | 0         110                6
1 |  1 | 1         111                7

如果我们现在考虑一下4这个映射到的数字100,表示第一个数字在组中A,而第二个和第三个数字不是(这意味着它们在 B 组中)。因此A1while Bis 23,5

现在解释一下为什么我只需要看一半的技巧:如果我们看一下十进制解释3011二进制),我们会得到 GroupA 23,5和 Group B 1。如果我们将此与示例进行比较,4我们会注意到我们将相同的数字分组,只是在完全相反的组名中。由于这对您的问题没有任何影响,因此我们不必查看此内容。

编辑3:我添加了真实代码进行尝试,在伪代码中我做出了错误的假设,即我总是会在总和中包含第一个元素,这是错误的。至于我开始使用的代码:您不能像那样分配数组。代替数组的另一种解决方案是vector<int>避免必须将数组大小传递给函数的问题。使用这将是一个很大的改进。此外,此代码远非良好。您将遇到问题int size(通常这应该适用于多达 32 个元素)。你可以解决这个问题(试试这个也许如何处理任意大的整数)。或者您实际上阅读了 knuth(见上文)我相信您会找到一些递归方法。这段代码也很慢,因为它总是重建整个总和。一种优化是研究格雷码(我认为 Knuth 也描述了它们)。这样,您只需为每个测试的排列添加/减去一个数字。这将是性能的提升n,因为您将n-1加法替换为1加法/减法。

于 2013-03-08T11:33:18.170 回答
0

如果单个组的平均值与整组的平均值相同,那么显然两组之间的差异会更小。通过使用它,我们可以为这个问题提出一个算法。

  1. 得到整套的平均值。
  2. 取集合中的最大值并将其放入组中的一个。
  3. 得到单个组的平均值与整组平均值的差值。
  4. 将下一个最大的数字放在具有最大差异的组中。
  5. 重复步骤 3 和 4,直到放置所有数字。

这将是获得接近最优解的有效方法。

于 2013-03-06T22:15:19.840 回答
0

这个怎么样:

  1. 对数字列表进行排序。
  2. 将最大的数字放在 A 组中。从列表中删除该数字。
  3. 如果 A 组中所有数字的总和小于 B 组中所有数字的总和,则转到 2。
  4. 将最大的数字放在 B 组中。从列表中删除该数字。
  5. 如果 B 组中所有数字的总和小于 A 组中所有数字的总和,则转到 4。
  6. 如果列表中剩余的数字超过零,则转到 2。
于 2013-03-08T18:53:28.340 回答