【问题标题】:How to divide a line of numbers into N groups such that the sums of each group are closest to their mean?如何将一行数字分成 N 组,使每组的总和最接近它们的平均值?
【发布时间】:2012-02-14 10:27:59
【问题描述】:

我有以下问题:我有 M 个数字排列成一行。我需要将线分成 N 组,以使每组的数字的总和最接近这些总和的平均值。实际度量并不重要:我们可以选择最小化绝对差之和或方差等,这取决于哪个导致最简单的解决方案。

一个类似的问题是集合的划分,这是 NP Hard。然而,这里我们有额外的限制:组必须打包连续的数字,所以可能有一个不涉及暴力搜索的解决方案。数字很​​大。

编辑

例子:

数字:1 2 3 4 5 6 7 8 9 10,需要分成3组

假设我们想要最小化绝对差之和 (SAD)。

组:(1) 1 2 3 4 5 6(总和 = 21); (2) 7 8 (总和 = 15); (3) 9 10 (总和 = 19)

平均值 = (21+15+19)/3 = 18.33, SAD = 21-18.33 + 18.33-15 + 19-18.33 = 6.67

【问题讨论】:

  • 你能举一些输入和预期输出的例子吗?
  • 我不确定这个问题陈述是否有意义,我无法理解。您说“每组的总和最接近这些总和的平均值”:您的意思是要最小化各组总和的离散度吗?如果是这种情况,我预计这些组将高度不平衡。

标签: algorithm list


【解决方案1】:

一旦您知道总和应该是多少,您就可以创建接近该总和的组。如果您的指标很好,那么您应该能够使用二进制搜索来找到实际总和。当您针对特定总和时,您可以通过列表将数字添加到组中,直到组总和超过总和大小。然后取或不取最后一个整数。遍历整个列表,看看哪些组的总和与总和的偏差最大。然后返回列表,尝试属于偏差范围内的组大小组合。它应该足够快。否则使用动态规划。

【讨论】:

    【解决方案2】:

    这是一个有趣的问题。我将使用您的示例将数字 1..10 分成三组来说明我的答案。该解决方案将适用于任何一组数字和任何数量的组。当然,当数字集的大小很大时,您可能无法使用蛮力方法。话虽如此,大型数字集也可以用类似的方式处理,但稍后会详细介绍。

    假设我们在集合中有 M 个连续数字,表示为 (1..M),我们希望将它们分成 N 个组。

    首先要确定的是您将比较每个组的总和的值。这只是一组数字的总和除以组数 N。

    在示例中 sumOf(1..M) = 55 和 N = 3,因此 55/3 = 18.33 是每个组应求和的值。您希望最小化组和与 18.33 之间的差异

    作为另一个例子,如果你想将数字 1..20 分成两组,那么你需要最小化组和之间的差异,并且 sumOf(1..20) = 210 除以 2 组 = 210/2 = 105。

    下一步是找到所有可能的组。这是另一个有趣的问题,考虑到 proups 包含连续数字的限制,组组合的总数并不像您预期​​的那么多。

    寻找组合是一个递归问题,很容易计算出一个通用方程。

    让我们从一个简单的案例开始。集合中有多少个 10 个数字的组合 (1..10)。好吧,只有一组,数字 (1..10)

    现在,10 个数字中有多少 2 个组的组合。答案是 M-1 或 10-1 = 9,即

    (1),(2..10)
    (1..2) (3..10)
    (1..3) (4..10)
    (1..4) (5..10)
    (1..5) (6..10)
    (1..6) (7..10)
    (1..7) (8..10)
    (1..8) (9..10)
    (1..9) (10)
    

    所以一组大小为 M 的组有 M-1 个组合。这是递归的基础。

    这 10 个数字中有多少个 3 组的组合。

    嗯,第一组将是以下之一

    (1),(1..2),(1..3) ,(1..4) ,(1..5),(1..6) ,(1..7) ,(1..8)  
    

    将其中任何一个作为第一组,让我们计算出剩余数字中有多少 2 个组的组合。

    让三个中的第一组 = (1)。我们还剩下九个数字,并且知道这些数字可以构成 9-1 = 8 个不同的 2 组组合 让三个中的第一组 = (1..5)。我们还剩下五个数字,它们可以组成 5-1 = 4 个不同的 2 个数字组。

    所以,我们总共会有

    (1) -> 8 combinations
    (1..2) -> 7 combinations
    (1..3) -> 6 combinations
    (1..4) -> 5 combinations
    (1..5) -> 4 combinations
    (1..6) -> 3 combinations
    (1..7) -> 2 combinations
    (1..8) -> 1 combinations
    

    给出 SumOf(1..8) ,或者一般来说 (sum(1..M-2),组的组合。SumOf(1..8) = 8*9/2 = 36

    所以 10 个数字中有 3 个组的 36 种组合,每个组包含连续的数字。

    顺便说一句,对于 100 个数字中的 3 个组,您有 sumOf(1..98) = 98*99/2 = 4851 个组组合,因此随着 M 的增加,您将获得更多组合,并且作为 M 的某个值蛮力方法可能是不可能的。

    上述方法可用于设计一个简单的递归算法,以获取集合 (1..M) 中的所有组组合。

    此外,可以为一组 M 个数中的任意数量 N 个组制定一个简单的方程。例如,如果您移动到 ​​10 个数字中的 4 个组,那么您会遇到第一组是 (1..3) 的情况,然后在剩余的 7 个数字中找到 3 个组的组合。将有 sum(1..M-2) = sum(1..5)..etc。

    不管怎样,回到问题上来。您拥有组的所有组合,因此您可以遍历组并计算每个组合的 SAD,然后选择最小化 SAD 的组合。

    当组合的数量非常多并且您无法查看每个组合时,您可以尝试引导以随机选择组或某种进化算法方法,从随机选择的组合数量开始,然后随机选择将数字从一组转移到另一组,并保留 SAD 最低的那些。继续此步骤,直到您看到 SAD 没有进一步改善。

    或者您可以按照@Robert King 的建议进行操作,从一个组合开始,然后通过将数字从一组移动到另一组来改进它。

    【讨论】:

      【解决方案3】:

      对数组进行降序排序 有三个数字存储总和 遍历循环并将当前数字添加到最小和 答案是 (10,5,4),(9,6,3),(8,7,2,1)

      #include<iostream>
      #include<stdio.h>
      #include <algorithm>
      
      using namespace std;
      int maximum(int x, int y, int z) {
      int max = x; /* assume x is the largest */
      
      if (y > max) { /* if y is larger than max, assign y to max */
          max = y;
      } /* end if */
      
      if (z > max) { /* if z is larger than max, assign z to max */
          max = z;
      } /* end if */
      
      return max; /* max is the largest value */
      } 
      
      int main()
       {
      int array[] = {1 ,2, 3, 4, 5, 6, 7, 8, 9, 10};
      int size = sizeof(array)/sizeof(array[0]);
      int part1=0;
      int part2=0;
      int part3=0;
      
      sort(array,array+size,greater<int>());
      for(int x=0;x<size;x++)
      {
          if( part1 < part2 && part1 < part3)
          {
              part1 +=array[x];
          }else if(part2 < part3){
              part2 +=array[x];
          }else{
              part3 +=array[x];
          }
      }
      
      printf("first part1 = %d\n",part1 );
      printf("first part2 = %d\n",part2 );
      printf("first part3 = %d\n",part3 );
      
      printf("-------------------------------\n");
      printf("largest number = %d\n",maximum(part1,part2,part3));
      
      }
      

      【讨论】:

        【解决方案4】:

        我想我知道你来自哪里。作为程序员,我以数字顺序来考虑它,我已经快速将一些东西放在一起作为它的情人节,我要出去吃饭了:) 这是一个简单的版本:

        a = all numbers added together
        b = number of groups
        m = a/b (value is mean)
        
        c = array(a)DES (add all numbers to an array in decending order)
        
        foreach c
            if((m-(c[0] + c[1])) < (m-(c[0]))
                if((m-(c[0] + c[1] + c[2])) < (m-(c[0] + c[1])))
                else
                g1 = c[0],c[1]
                c = c - (c[0],c[1])
        
            else
            g1 = c[0]
            c = c - c[0]
        
        foreach c
            if((m-(c[0] + c[1])) < (m-(c[0]))
            else
            g2 = c[0]
        

        我已经很快把它放在一起,所以它可能不准确,但希望你能看到顺序和过程。当然,所有的“c”值都会像每个“foreach”循环一样被动态选择。最后可能需要一个 foreach 语句来处理任何剩余的数字并将它们添加到最接近均值的值中。

        情人节快乐!

        【讨论】:

          【解决方案5】:

          这是一个有效的(虽然没有经过全面测试)JavaScript 解决方案。

          它本质上是使用动态脚本来构建蛮力堆叠的 for 循环(有序组合)来获取数组中每个组的起始索引。

          var A = [1,2,3,4,5,6,7,8,9,10];
          var G = 3;
          function find(line, groups) {
              var length = line.length;
              var mean = line.sum() / groups;
              var temp = [0];
              var bestsad = 4294967295;
              var beststarts = [];
              var dynamic = "var x0 = 0; ";
              for(var i=1; i<groups; i++) {
                  dynamic += "for(var x" + i + "=x" + (i-1) + "+1;x" + i + "<" + length + ";x" + i + "++) ";
                  temp.push("x" + i);
              }
              dynamic += "{ var sad = getSAD(line, mean, [" + temp.join(",") + "]);";
              dynamic += "if(sad < bestsad) { bestsad = sad; beststarts = [" + temp.join(",") + "] ;} }"
              eval(dynamic);
              console.log("Best SAD " + bestsad);
              console.log("Best Start Indexes " + beststarts);
              return beststarts;
          }
          function getSAD(line, mean, starts) {
              var sums = [];
              var sad;
              for(var i = 0; i < starts.length-1; i++)
              {
                  var idx = i;
                  sums.push(line.slice(starts[idx], starts[i+1]).sum());
              }
              sums.push(line.slice(starts[starts.length-1]).sum());
              sad = sums.sad(mean);
              return sad;
          }
          
          Array.prototype.sum = function() {
              var result = 0;
              for(var i=0; i<this.length; i++)
                  result += this[i];
              return result;
          }
          Array.prototype.sad = function(mean) {
              var result = 0;
              for(var i=0; i<this.length; i++)
                  result += Math.abs(this[i] - mean);
              return result;
          }
          find(A, G);
          

          这是 var dynamic 变量/字符串保存/执行的脚本。

          var x0 = 0; 
          for(var x1=x0+1;x1<10;x1++) 
           for(var x2=x1+1;x2<10;x2++) { 
            var sad = getSAD(line, mean, [0,x1,x2]);
            if(sad < bestsad) { 
             bestsad = sad; 
             beststarts = [0,x1,x2] ;
            } 
          }
          

          为什么不直接使用组索引向量 + 递归?对于这种类型的递归问题,迭代方法是最优的。诚然,动态脚本的开销(和增加的复杂性)会抵消小型数组的任何好处,但在处理实际数据(大型数组)时,它会更快地产生答案。

          【讨论】:

            猜你喜欢
            • 2013-06-01
            • 2015-04-07
            • 1970-01-01
            • 2015-09-22
            • 2020-07-22
            • 2018-11-29
            • 2021-05-02
            • 1970-01-01
            • 2011-05-27
            相关资源
            最近更新 更多