我正在尝试生成一个整数数组,其中包含与特定值相加的randoms。这是我的代码:
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
您能否在我的代码中推荐替代算法或修复程序,以帮助我实现此目标?
答案 0 :(得分:6)
每次使用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]++;
}
或者更简单地说,您可以将列表中的最后一个值设置为缩放后执行所有其他值。然而,这在统计上会扭曲输出。
答案 1 :(得分:3)
刚才有个主意。将数组初始化为全0。随机选取数组中的数字并增加1并将总和减1,直到sum为0.当sum很大时,它根本不实用:)
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]++;
}
答案 2 :(得分:2)
是的,有一种标准方法可以避免浮点不准确。它与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])。
所以,我们需要做的就是想象通过选择n-1
X的随机位置来创建这样的字符串,然后找出对应的解决方案。这是一个更详细的细分:
n-1
之间生成[1, s+n-1]
个唯一随机数。使用rejection sampling这样做很简单 - 如果选择一个数字两次,只需删除它并选择另一个。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;
}
答案 3 :(得分:1)
我相信您的问题在于Math.round
尝试修改您的代码以使用双精度并避免任何精度损失
答案 4 :(得分:1)
一个好的方法是使用一个间隔列表,然后在每个步骤中拆分。 这是伪代码
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不是间隔的终点。您可以通过拒绝来执行此操作,或者您可以选择要拆分的间隔,然后选择拆分该间隔的位置,但在这种情况下,您需要注意不要引入偏差。 (如果你关心偏见)。
答案 5 :(得分:0)
这个怎么样:
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;
换句话说,将所有舍入错误放入第一个元素。
答案 6 :(得分:0)
建议:
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循环。
答案 7 :(得分:0)
拿个号码。从中减去一个合理的随机数。重复,直到你有足够的数字。根据定义,它们的总和是固定的并且等于初始数。您完全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
它有效,但它有一个缺点:数字越远,它们就越小。您可能需要对结果列表进行随机播放以对抗此操作,或者使用较小的上限进行播放。
答案 8 :(得分:0)
这肯定是一个有趣的问题(upvote!),我期待看到一些更有创意的解决方案。我对你列出的代码有一个问题就是你使用的是Random
类,它只返回整数而不是长整数,但是你只需要将它转换为long。因此,如果您的函数真的需要多头,那么您将不得不找到一个不同的随机数生成器。
“理想”解决方案是生成所有可能的添加剂组合(称为分区),然后随机选择一个。然而,实现这一点的最着名的算法确实非常昂贵。关于这个问题有很多好的源材料。这是一个关于分区算法的特别好的读物:
http://www.site.uottawa.ca/~ivan/F49-int-part.pdf
问题的一部分是在这个例子中找出“随机”的意思。例如,如果您要查找总数为1000的5个数字的数组,{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)
意味着从数组x
中取出第一个a
元素...我确信这些天在Java中有一些等效的魔法这样做。)
这会返回一个分区列表...只需随机选择一个,你就会有一个完美的随机分布。
答案 9 :(得分:0)
- 怎么样?
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;
}