【问题标题】:How to generate a sequence of n random positive integers which add up to some value?如何生成一个由n个随机正整数组成的序列,它们加起来等于某个值?
【发布时间】:2012-04-05 02:12:00
【问题描述】:

我正在尝试生成一个整数数组,其中包含随机数加起来一个特定的值。这是我的代码:

private long[] getRandoms(long size , long sum) throws Exception {
  double iniSum = 0;
  System.out.println("sum = " + sum);
  long[] ret = new long[(int) size];
  for (int i = 0 ; i < ret.length; i++) {
    ret[i] = randomInRange(1, sum);
    iniSum += ret[i];
  }

  double finSum = 0;
  for (int i = 0 ; i < ret.length; i++) {
    ret[i] =  Math.round((sum * ret[i]) / iniSum);
    System.out.println("ret[" + i +"] = " + ret[i]);
    finSum += ret[i];
  }

  if (finSum != sum) throw new Exception("Could not find " + size + " numbers adding up to " + sum  + " . Final sum = " + finSum);
  return ret;
}



private long randomInRange(long min , long max) {
  Random rand = new Random();
  long ret = rand.nextInt((int) (max - min + 1)) + min;
  System.out.println("ret = " + ret);
  return ret;
} 

但是,结果并不准确,例如:

找不到 4194304 的 100 个数字。最终总和 = 4194305.0

我认为我在这方面失去了准确性:

(sum * ret[i]) / iniSum

您能否在我的代码中推荐一种替代算法或修复方法来帮助我实现这一目标?

【问题讨论】:

  • 是每次都抛出同一个异常,还是多次运行程序时异常不同?
  • “我正在尝试生成一个整数数组,其中包含随机数加起来一个特定的值。”(抓挠头)这是否意味着至少有一个集合中的值不是随机的
  • @AndrewThompson 笑得很开心!
  • 不管怎样,单元测试不应该有随机行为。他们应该有专门设计的行为来测试特定案例。
  • @QuantumMechanic: 取n随机数(任意范围,只要分布均匀),求和,再除以总和就是从[0,1] 生成n 随机“公平”数字的标准方法,总和为1。很明显,将它们全部乘以另一个数字k 将为您提供n 来自[0,k] 的随机“公平”数字.

标签: java math random precision


【解决方案1】:

每次使用ret[i] = Math.round((sum * ret[i]) / iniSum) 缩放一个值时,都会损失一些精度,部分原因是除法运算本身,但主要是因为将缩放值存储为整数。后一种情况类似于比例选举制度,其中少数席位必须按比例分配给更多选票。

缓解此问题的两种技术:

首先缩放列表中的所有值,但要跟踪理想的缩放值(实数)和存储的缩放值之间的差异。使用截断而不是四舍五入,以便差异始终为正。如果存在差异,您可以按照理想数量和当前存储数量之间的差异顺序递增一些值。

long finSum = 0;  // Might as well be a long
float[] idealValues = new float[size];
for (int i = 0 ; i < ret.length; i++) {
    ideal[i] = (sum * ret[i]) / iniSum;
    ret[i] = (long) (ideal[i]);  // Truncate, not round
    System.out.println("ret[" + i +"] = " + ret[i]);
    finSum += ret[i];
}

/* There are iniSum - finSum extra amounts to add. */
for (int i = 0; i < iniSum - finSum; i++)
{
    /* Pick a target slot to increment.  This could be done by keeping a priority queue. */
    int target = 0;
    float bestScore = 0.0;
    for (int j = 0; j < size; j++) {
        float score = (ideal[j] - ret[j])/ret[j];
        if (score > bestScore) {
            target = j;
            bestScore = score;
        }
    }

    /* Allocate an additional value to the target. */
    ret[target]++;
}

或者更简单地说,您可以将列表中的最后一个值设置为在缩放完成所有其他值之后突出的值。然而,这确实会在统计上扭曲输出。

【讨论】:

  • 我喜欢这种方法,但是您可以通过选择一个随机插槽来取出这些额外的金额。我认为它不会使分布比现有方法更差。
  • 此解决方案缓解了浮点不准确性问题,但并未消除它。有些解决方案根本不依赖浮点,例如@woodings 或我的答案。
【解决方案2】:

刚刚有了一个想法。将数组初始化为全0。随机取数组中的一个数,加1,减1,直到总和为0。总和很大时根本不实用:)

long ret = new long[size];
for(int i=0;i<size;i++) ret[i]=0;
for(int i=0;i<sum;i++) {
  int n = random(0,size);
  ret[n]++;
}

【讨论】:

  • 这样的优点是不偏不倚,缺点是速度慢。
  • 对此的修改是将数字平均(或几乎相等)分成N个部分,然后重复随机选择两个部分并在它们之间移动一些值。有点像洗牌。我不确定你需要洗多少才能得到真正随机的东西。你也可以这样做,逐渐减少数量……从大开始,然后从小开始……这可能会更好地分配数字。
【解决方案3】:

是的,有一种标准方法可以避免浮点不准确。与counting the number of ways for n numbers to sum to s的方法有关。它并不复杂,但奇怪的是还没有人提到它,所以这里是:


假设我们有一个字符串,其中包含n-1 X 和s o。因此,例如,对于 s=5,n=3,一个示例字符串将是

oXooXoo

请注意,X 将 o 分为三个不同的组:长度 1、长度 2 和长度 2 之一。这对应于解 [1,2,2]。每个可能的字符串都为我们提供了不同的解决方案,通过计算组合在一起的 o 的数量(可能为 0:例如,XoooooX 将对应于 [0,5,0])。 p>

所以,我们需要做的就是想象通过为n-1 X 选择随机位置来创建这样一个字符串,然后找出对应的解决方案。以下是更详细的细分:

  1. [1, s+n-1] 之间生成n-1 唯一随机数。使用rejection sampling 执行此操作非常简单 - 如果一个数字被选择了两次,只需放下它并选择另一个。
  2. 对它们进行排序,然后找出每个 X 之间的 o 数。这个数字原来是currentValue - previousValue - 1

最后,这里有一些示例 Java(未经测试)应该这样做:

private List<long> getRandomSequenceWithSum(int numValues, long sum)
{
    List<long> xPositions = new List<long>();
    List<long> solution = new List<long>();
    Random rng = new Random();

    //Determine the positions for all the x's
    while(xPositions.size() < numValues-1)
    {
        //See https://stackoverflow.com/a/2546186/238419
        //for definition of nextLong
        long nextPosition = nextLong(rng, sum+numValues-1) + 1; //Generates number from [1,sum+numValues-1]
        if(!xPositions.contains(nextPosition))
            xPositions.add(nextPosition);
    }

    //Add an X at the beginning and the end of the string, to make counting the o's simpler.
    xPositions.add(0);
    xPositions.add(sum+numValues);
    Collections.sort(xPositions);

    //Calculate the positions of all the o's
    for(int i = 1; i < xPositions.size(); i++)
    {
        long oPosition =  xPositions[i] - xPositions[i-1] - 1;
        solution.add(oPosition);
    }

    return solution;
}

【讨论】:

  • 您可以避免使用拒绝采样(这在某些边缘情况下变得非常讨厌),通过拆分间隔(在您的情况下运行“o”s),按数量加权间隔的选择内部 o-o 边界。 (例如,如果你已经像这样分割你的总和 ooooXoXoo 你有 3 个区间,第一个的权重为 3,第二个的权重为 0,第三个的权重为 1。所以从 1 到 4 中选择一个数字会让你选择要分裂。(这基本上是我在上面的答案中概述的方法)。
【解决方案4】:

我相信您的问题在于 Math.round 尝试修改您的代码以使用双精度并避免任何精度损失

【讨论】:

    【解决方案5】:

    一个好的方法是使用一个间隔列表,然后在每个步骤中对其进行拆分。 这是伪代码

     array intervals = [(0,M)]
       while number intervals<desired_number
         pick an split point x
         find the interval containing x
         split that interval at x
    

    如果你想非常小心,你需要检查你的分割点 x 不是间隔的终点。您可以通过拒绝来做到这一点,或者您可以选择要拆分的区间,然后在哪里拆分该区间,但在这种情况下,您需要小心不要引入偏差。 (如果您关心偏见)。

    【讨论】:

      【解决方案6】:

      这个怎么样:

      long finSum = 0;
      for (int i = 0 ; i < ret.length; i++) {
        ret[i] =  Math.round((sum * ret[i]) / iniSum);
        System.out.println("ret[" + i +"] = " + ret[i]);
        finSum += ret[i];
      }
      ret[0] += sum - finSum;
      

      也就是说,把所有的舍入误差都放到第一个元素中。

      【讨论】:

        【解决方案7】:

        建议:

          for (int i = 0 ; i < ret.length; i++) {
            ret[i] = randomInRange(1, sum);
            iniSum += ret[i];
          }
        

        在第一个 for 循环中,不是从 0 迭代到 (ret.length-1),而是只使用从 0 到 (ret.length-2) 的迭代。保留最后一个元素进行调整。

        也不要调用 randomInRange(1, sum),而是使用 randomInRange(1, sum-iniSum)。这将减少所需的标准化。

        最后,最后的输出数将是 (sum-iniSum)。因此,可以删除第二个 for 循环。

        【讨论】:

          【解决方案8】:

          拿一个号码。从中减去一个合理的随机数。重复直到你得到足够的数字。根据定义,它们的总和是固定的,并且等于初始数字。您准确地进行n 操作,其中n 是您需要的数字数量;不用猜了。

          这是 Python 代码:

          import random
          def generate_randoms(initial, n):
            # generates n random numbers that add up to initial.
            result = [] # empty list
            remainder = initial # we'll subtract random numbers and track the remainder
            numbers_left = n
            while numbers_left > 1:
              # guarantee that each following step can subtract at least 1
              upper_bound = remainder - numbers_left 
              next_number = random.randrange(1, upper_bound) # [1, upper_bound) 
              result.append(next_number)
              remainder -= next_number # chop off
              numbers_left -= 1
            result.append(remainder) # we've cut n-1 numbers; don't forget what remains
            return result
          

          它有效,但它有一个缺点:数字越远,它们越小。您可能需要打乱结果列表来解决这个问题,或者使用更小的上限。

          【讨论】:

            【解决方案9】:

            这确实是一个有趣的问题(赞成!),我期待看到一些更有创意的解决方案。我对您列出的代码的一个问题是您正在使用Random 类,它只返回整数,而不是长整数,但是您只需将其转换为长整数。所以如果你的函数真的需要 long,你就必须找到一个不同的随机数生成器。

            “理想”的解决方案是生成所有可能的加法组合(称为分区),然后随机选择一个。然而,最知名的算法确实非常昂贵。关于这个问题有很多很好的源材料。这是关于分区算法的特别好的阅读:

            http://www.site.uottawa.ca/~ivan/F49-int-part.pdf

            部分问题在于在这种情况下弄清楚“随机”是什么意思。例如,如果您要查找一个由 5 个数字组成且总和为 1000 的数组,则 {1000,0,0,0,0} 与 {200,200,200,200,200} 一样有效,与 {136,238,124,66,436} 一样有效.具有相同机会生成任何这些集合的算法将是真正随机的。

            但是,我猜您不是在寻找完全随机的分布,而是介于两者之间。

            不幸的是,我的 Java 已经很生疏了,而且这些天我几乎都在用 C# 做所有的事情,所以我将用 C# 来演示……将这些算法转换成 Java 不需要太多的翻译。

            这是我对这个问题的看法:

            public int[] GetRandoms( int size, int sum, Random r=null ) {
              if( r==null ) r = new Random();
              var ret = new int[size];
                while( sum > 0 ) {
                  var n = r.Next( 1, (int)Math.Ceiling((double)sum/size)+1 );
                  ret[r.Next(size)] += n;
                  sum -= n;
                }
                return ret;
            }
            

            (实际上,看看我是如何传入我的随机数生成器的?我将其默认为null,并且为了方便起见只是创建一个新的,但现在我可以传入一个种子随机数生成器,并根据需要生成相同的结果,这非常适合测试目的。每当您有使用随机数生成器的方法时,我强烈建议采用这种方法。)

            这个算法基本上一直在向数组中的随机条目添加随机数,直到达到总和。由于它的加法性质,它将有利于更大的数字(即,小数字极不可能出现在结果中)。但是,这些数字看起来是随机的,并且它们的总和是适当的。

            如果您的目标数量很小(例如 100 以下),您实际上可以实现真正的随机方法,即生成所有分区并随机选择一个。例如,这是一个分区算法(由http://homepages.ed.ac.uk/jkellehe/partitions.php提供,翻译成C#):

            public List<int[]> GetPartitions( int n ) {
            var result = new List<int[]>();
            var a = new int[n+1];
            var k = 1;
            a[0] = 0;
            var y = n - 1;
            while( k != 0 ) {
                var x = a[k-1] + 1;
                k--;
                while( 2*x <= y ) {
                    a[k] = x;
                    y -= x;
                    k++;
                }
                var l = k + 1;
                while( x <= y ) {
                    a[k] = x;
                    a[l] = y;
                    result.Add( a.Take( k + 2 ).ToArray() );
                    x++;
                    y--;
                }
                a[k] = x + y;
                y = x + y - 1;
                result.Add( a.Take( k + 1 ).ToArray() );
            }
            return result;
            }
            

            (为了帮助您进行 Java 翻译,a.Take(x) 意味着从数组 a 中取出第一个 x 元素...我敢肯定,这些天在 Java 中有一些等效的魔法可以做到这一点。)

            这会返回一个分区列表...只需随机选择一个,您将获得完美的随机分布。

            【讨论】:

              【解决方案10】:

              -这个怎么样?

              private long[] getRandoms(int size , long sum) {
                  long[] array = new long[size];
                  long singleCell = sum / size;
                  Arrays.fill(array, singleCell);
                  array[size-1] += sum % singleCell;
                  int numberOfCicles = randomInRange(size, size*10);
                  for (int i = 0; i < numberOfCicles; i++) {
                      int randomIndex = randomInRange(0, size);
                      long randomNumToChange = randomInRange(0, array[randomIndex]);
                      array[randomIndex] -= randomNumToChange;
                      array[randomInRange(0, size)] += randomNumToChange;
                  }
                  return array;
              }
              

              【讨论】:

                猜你喜欢
                • 2018-12-16
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2013-09-12
                • 2011-06-26
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多