假设我有以下代码:
arr = [0.1,0.5,0.2,0.2]; //The percentages (or decimals) we want to distribute them over.
value = 100; //The amount of things we have to distribute
arr2 = [0,0,0,0] //Where we want how many of each value to go
要了解如何在阵列上平均分配一百个很简单,就是这样的情况:
0.1 * 100 = 10
0.5 * 100 = 50
...
或者使用for循环执行:
for (var i = 0; j < arr.length; i++) {
arr2[i] = arr[i] * value;
}
但是,假设每个计数器都是一个对象,因此必须是完整的。我怎样才能(尽我所能)将它们分配到不同的值上。假设该值变为12。
0.1 * 12 = 1.2
0.5 * 12 = 6
...
当我需要整数时,如何处理小数?舍入意味着我可能不需要12件。
正确的算法 -
获取输入/迭代值数组(对于此示例,我们将使用上面定义的数组。
将它变成一组整数值,它们加在一起等于该值(对于此值将等于100)
输出一个值数组,对于这个例子,它看起来像[10,50,20,20](这些加起来就是100,这是我们需要添加它们的所有内容,而且都是完整的)
如果任何值不是完整的,那么它应该是整数,所以整个数组仍然加起来所需的值(100)。
TL; DR 在数组上分配值并尝试将它们转换为整数时处理小数
注意 - 如果要在不同的stackoverflow网站上发布,我需要编程,但实际问题可能会用数学来解决。此外,我不知道如何说出这个问题,这使谷歌搜索难以置信。如果我错过了一些非常明显的东西,请告诉我。
答案 0 :(得分:11)
您应该使用已知均匀分布舍入的舍入对所有值进行舍入。最后,将对最后一个值进行不同的分配,以将总和四舍五入为1
。
让我们慢慢开始,或者让事情变得非常困惑。首先,让我们看看如何指定最后一个值以获得所需的总值。
// we will need this later on
sum = 0;
// assign all values but the last
for (i = 0; i < output.length - 1; i++)
{
output[i] = input[i] * total;
sum += output[i];
}
// last value must honor the total constraint
output[i] = total - sum;
最后一行需要一些解释。 i
将比for(..)
循环中的最后一个允许的多一个,因此它将是:
output.length - 1 // last index
我们分配的值将使所有元素的sum
等于total
。我们已经在赋值期间在单遍中计算了和,因此不需要第二次迭代元素来确定它。
接下来,我们将解决舍入问题。让我们简化上面的代码,以便它使用我们将在不久之后详细阐述的函数:
sum = 0;
for (i = 0; i < output.length - 1; i++)
{
output[i] = u(input[i], total);
sum += output[i];
}
output[i] = total - sum;
正如您所看到的,除u()
函数的引入外,一切都没有改变。让我们现在专注于此。
有几种方法可以实现u()
。
DEFINITION
u(c, total) ::= c * total
通过这个定义,你得到与上面相同的内容。这是精确和好的,但正如您之前所问,您希望值是自然数(例如整数)。因此,对于实数而言,这已经是完美的,对于自然数,我们必须围绕它。我们假设我们对整数使用简单的舍入规则:
[ 0.0, 0.5 [ => round down
[ 0.5, 1.0 [ => round up
这是通过以下方式实现的:
function u(c, total)
{
return Math.round(c * total);
}
当你运气不好时,你可能会向上舍入(或向下舍入)这么多值,以至于最后一次值修正不足以兑现总约束,通常,所有值似乎都会过多。这是一个众所周知的问题,它存在一种在2D和3D空间中绘制线条的多维解决方案,称为Bresenham algorithm。
为了方便起见,我将在此向您展示如何在一维中实现它(这是您的情况)。
让我们先讨论一个术语:余数。这是您舍入数字后剩余的内容。它是根据你想要的和你真正拥有的东西之间的差异来计算的:
DEFINITION
WISH ::= c * total
HAVE ::= Math.round(WISH)
REMAINDER ::= WISH - HAVE
现在想一想。剩下的就像从纸张上切下一个形状时丢弃的那张纸。剩下的纸张仍在那里,但你把它扔掉了。而不是这个,只需将它添加到下一个切口,这样就不会浪费:
WISH ::= c * total + REMAINDER_FROM_PREVIOUS_STEP
HAVE ::= Math.round(WISH)
REMAINDER ::= WISH - HAVE
这样您可以保留错误并将其转移到计算中的下一个分区。这称为分摊错误。
以下是u()
的摊销实施:
// amortized is defined outside u because we need to have a side-effect across calls of u
function u(c, total)
{
var real, natural;
real = c * total + amortized;
natural = Math.round(real);
amortized = real - natural;
return natural;
}
您可能希望根据Math.floor()
或Math.ceil()
制定另一个舍入规则。
我建议你做的是使用Math.floor()
,因为证明与总约束一致。当您使用Math.round()
时,您将获得更顺畅的摊销,但您可能没有最后一个正值。你最终会得到这样的东西:
[ 1, 0, 0, 1, 1, 0, -1 ]
只有所有价值 远离<{1}} 时,您才能确信最后一个值也是正值。因此,对于一般情况, Bresenham algoritm 将使用地板,从而导致最后一次实施:
0
显然,function u(c, total)
{
var real, natural;
real = c * total + amortized;
natural = Math.floor(real); // just to be on the safe side
amortized = real - natural;
return natural;
}
sum = 0;
amortized = 0;
for (i = 0; i < output.length - 1; i++)
{
output[i] = u(input[i], total);
sum += output[i];
}
output[i] = total - sum;
和input
数组必须具有相同的大小,output
中的值必须是paritition(总计为1)。
这种算法在概率和统计计算中非常常见。
答案 1 :(得分:0)
另一种实现方式-它记住一个指向最大舍入值的指针,并且当总和相差100时,将在此位置递增或递减值。
const items = [1, 2, 3, 5];
const total = items.reduce((total, x) => total + x, 0);
let result = [], sum = 0, biggestRound = 0, roundPointer;
items.forEach((votes, index) => {
let value = 100 * votes / total;
let rounded = Math.round(value);
let diff = value - rounded;
if (diff > biggestRound) {
biggestRound = diff;
roundPointer = index;
}
sum += rounded;
result.push(rounded);
});
if (sum === 99) {
result[roundPointer] += 1;
} else if (sum === 101) {
result[roundPointer] -= 1;
}