关于java:如何生成一个n个随机正整数序列,它们加起来有些值?

How to generate a sequence of n random positive integers which add up to some value?

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
}

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

Could not find 100 numbers adding up to 4194304 . Final sum =
4194305.0

我想我在这一点上失去了准确性:

1
(sum * ret[i]) / iniSum

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


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

缓解这一问题的两种方法:

首先缩放列表中的所有值,但要跟踪理想缩放值(实数)和存储的缩放值之间的差异。使用截断而不是舍入,以便谨慎性始终为正。如果存在差异,可以根据理想数量和当前存储数量之间的差异增加一些值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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]++;
}

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


我有个主意。将数组初始化为全部0。在数组中随机选取一个数字,加1,减1,直到和为0。当总和较大时,这根本不实际。)

1
2
3
4
5
6
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个数与s个数相加的方法有关。这并不复杂,但奇怪的是,还没有人提到这一点,所以下面是:

假设我们有一个包含n-1x和so的字符串,例如,对于s=5,n=3,一个示例字符串是

oXooXoo

注意x把o分成三组:一组是长度1、长度2和长度2。这对应于溶液[1,2,2]。每一个可能的字符串都给了我们一个不同的解决方案,通过计算分组在一起的O的数目(0是可能的:例如,XoooooX对应于[0,5,0])。

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

  • [1, s+n-1]之间生成n-1唯一随机数。这样做对于拒绝抽样来说已经足够简单了——如果一个数字被选择了两次,只需丢弃它并选择另一个。
  • 把它们分类,然后算出每个x之间的o的数目。这个数字原来是currentValue - previousValue - 1
  • 最后,这里有一些Java(未经测试)应该做的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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;
    }


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

    1
    2
    3
    4
    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不是一个区间的终点。你可以通过拒绝来做到这一点,或者你可以选择要分割的时间间隔,然后在哪里分割这个时间间隔,但是在这种情况下,你需要小心不要引入偏见。(如果你关心偏见的话)。


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


    -这个怎么样?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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;
    }


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

    "理想"的解决方案是生成所有可能的加法组合(称为分区),然后随机选择一个。然而,最著名的算法确实非常昂贵。关于这个问题有很多很好的资料来源。下面是一篇关于分区算法的特别好的文章:

    网址:http://www.site.uottawa.ca/~ivan/f49-int-part.pdf

    在这种情况下,部分问题是找出你所说的"随机"是什么意思。例如,如果要查找5个数字的数组,总和为1000,1000,0,0,0,0与200200200200200一样有效,后者与136238124,66436一样有效。一个生成这些集合的机会相等的算法是真正随机的。

    不过,我猜你不是在寻找一个完全随机的分布,而是介于两者之间的。

    不幸的是,我的Java是很生疏的,现在我在C语言中做的事情最多,所以我将出现在C语言中……不应该花太多的翻译把这些算法转换成Java。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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.a c.uk/jkelehe/partitions.php提供,翻译成c):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    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翻译中,EDCOX1,2)意味着把第一个EDOCX1的3个元素从数组EDOCX1中删去,4……我确信在Java中有一些相当的魔力。

    这将返回一个分区列表……只需随机选择一个分区,就可以得到一个完美的随机分布。


    拿一个数字。从中减去一个合理的随机数。重复,直到你得到足够的数字。它们的和按定义是固定的,等于初始数。你做的完全是n操作,其中n是你需要的数量,不需要猜测。

    下面是python代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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

    这是可行的,但也有一个缺点:数字越大,越小。您可能需要对结果列表进行无序排列以与之抗争,或者使用较小的上界。


    建议:

    1
    2
    3
    4
      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)。保留最后一个元素进行调整。

    也不要调用RandomNrange(1,sum),而是使用RandomNrange(1,sum inisum)。这将减少所需的规范化。

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


    这个怎么样:

    1
    2
    3
    4
    5
    6
    7
    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;

    换句话说,将所有舍入误差放入第一个元素。