将数字分组 C++

Posted

技术标签:

【中文标题】将数字分组 C++【英文标题】:Group the numbers C++ 【发布时间】:2013-02-15 09:16:09 【问题描述】:

问题来了:

你有 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;

【问题讨论】:

这是 NP-Complete 的背包问题的变体。这意味着您无法在多项式时间内找到最佳解决方案(正如您正确识别的那样,对于大 N,迭代所有组合会很昂贵) 如果数字的范围相对有限,则有一个相当有效的(“伪多项式”)算法(这是duplicate)。顺便说一句,NP-complete 并不意味着你不能找到一个有效的算法——它只意味着很多人已经找了很长时间,但都失败了,所以如果你找到了所以,这将是一个巨大的突破。 我忘了补充问题状态 N 小于或等于 300 并且数字小于或等于 100 000 如果 N=5,您只有 31 个组合需要检查 但是如果 N 是 300 呢? 【参考方案1】:

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

您建议计算数字的总和并将其除以 2 是正确的起点 - 这意味着您知道每个组的理想总和是多少。我还怀疑您最好的选择是首先将最大的数字放入 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 来简化 - 之后条件不能满足.)

如果这可行,请告诉我!

【讨论】:

8 @Neil Townsend 它仅在数字之间的差异很小并且总和之间的差异为0时才有效。这是它不起作用的示例。 301, 301, 300, 241, 173, 168, 118,117 平均为 859.5 2.检查 301(301 + 301 不在 859.5 附近)。所以我们将 301 添加到 B 组 3.检查 300 (301 + 300 = 601 4.检查 241 (301 + 300 + 241 = 842 5.检查 173 (301 + 300 + 173 = 774 【参考方案2】:

如果你能找到一组数字的总和正好是总数的一半,那么使用只有两组的约束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;

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

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 组中)。因此A1B23,5

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

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

【讨论】:

如何使用模拟退火来解决这个问题?这个方法我没用过,能不能稍微解释一下。 这一行是什么意思: if( state&(1 @Stefan4024 它计算当前组A的总和。状态,是状态位的连接。所以第 n 位表示天气,第 n 位是否在 a 组中。要检查这一点,使用位掩码检查第 n 位是否设置为1。如果是,我们将其包含在 A 组的总和中。 @Stefan4024 这是一个惰性伪代码,可以通过添加或减去从上一个状态添加或删除的元素来优化。 我其实不明白你是怎么跑完所有组合的,怎么在A组和B组之间切换号码。【参考方案3】:

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

    获取整个集合的平均值。 取集合中的最大值并将其放入组中。 获取单个组的平均值与整组平均值的差值。 将下一个最大的数字放在具有最大差异的组中。 重复第 3 步和第 4 步,直到所有数字都放好。

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

【讨论】:

你能解释一下第 3 步和第 4 步吗?【参考方案4】:

这个怎么样:

    对数字列表进行排序。 将最大的数字放入 A 组。从列表中删除该数字。 如果 A 组中所有数字的总和小于 B 组中所有数字的总和,则转到 2。 将最大的数字放入 B 组。从列表中删除该数字。 如果 B 组中所有数字的总和小于 A 组中所有数字的总和,则转到 4。 如果列表中剩余的数字超过零,请转到 2。

【讨论】:

19 39 40 49 319 315 295 244 222 198 184 182 172 143 139 120 97 71 64 49 40 39 1. 将 319 放入 A 组 (319) 2. 将 315 放入 B 组 (315) 3. 将 295 放入 B 组 (610) 4. 将 244 放入 A 组 (563) 5. 将 222 放入 A 组 (785) 6. 将 198 放入 B 组 (808) 7. 将 184 放入 A 组 (969) 8. 将 182 放入 B 组 (990) 9. 将 172 放入 A 组 (1141) 10. 将 143 放入 B 组 (1133) 11. 将 139 放入 B 组 (1272) 12. 将 139 放入 A 组 (1280) 13. 将 120 人放入 B 组 (1392) 14. 将 97 人放入 A 组 (1377) 15. 将 71 人放入 A 组 (1448) 16. 将 64 人放入 B 组 (1456) 17.将 49 放入 A 组 (1497) 18. 将 40 放入 B 组 (1496) 19. 将 39 放入 B 组 (1535) 差异为 38,尽管最佳差异为 0。

以上是关于将数字分组 C++的主要内容,如果未能解决你的问题,请参考以下文章

SQL查询将连续范围的数字分组到不同的分组集中

C++ 类函数分组

在 C++ 中对大量元素进行分组的最快方法

SQL 将数字分配给日期,按值分组

将时间间隔从文本转换为数字并按客户时间间隔分组

通过将数字分组到一个范围内在 matlab / octave 中绘图