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 |
号
你能在我的代码中推荐一种替代算法或修复方法来帮助我实现这个目标吗?
每次使用
缓解这一问题的两种方法:
首先缩放列表中的所有值,但要跟踪理想缩放值(实数)和存储的缩放值之间的差异。使用截断而不是舍入,以便谨慎性始终为正。如果存在差异,可以根据理想数量和当前存储数量之间的差异增加一些值。
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]++; } |
是的,有一种标准的方法可以避免浮点误差。它与计算
假设我们有一个包含
oXooXoo
号
注意x把o分成三组:一组是长度1、长度2和长度2。这对应于溶液[1,2,2]。每一个可能的字符串都给了我们一个不同的解决方案,通过计算分组在一起的O的数目(0是可能的:例如,
所以,我们需要做的就是想象一下,通过为
最后,这里有一些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不是一个区间的终点。你可以通过拒绝来做到这一点,或者你可以选择要分割的时间间隔,然后在哪里分割这个时间间隔,但是在这种情况下,你需要小心不要引入偏见。(如果你关心偏见的话)。
我相信你的问题在于
-这个怎么样?
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; } |
这当然是一个有趣的问题(赞成票!)我期待着看到一些更有创意的解决方案。我对您列出的代码有一个问题,那就是您使用的是
"理想"的解决方案是生成所有可能的加法组合(称为分区),然后随机选择一个。然而,最著名的算法确实非常昂贵。关于这个问题有很多很好的资料来源。下面是一篇关于分区算法的特别好的文章:
网址: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 |
(实际上,看看我是如何传递随机数生成器的?我默认为
这个算法基本上一直在向数组中的随机项添加随机数,直到求和为止。由于它的加法性质,它将倾向于更大的数字(即,在结果中极不可能出现较小的数字)。然而,这些数字似乎是随机的,并且它们的总和也将是适当的。
如果您的目标数量很小(比如说低于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中有一些相当的魔力。
这将返回一个分区列表……只需随机选择一个分区,就可以得到一个完美的随机分布。
拿一个数字。从中减去一个合理的随机数。重复,直到你得到足够的数字。它们的和按定义是固定的,等于初始数。你做的完全是
下面是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 |
号
换句话说,将所有舍入误差放入第一个元素。