在分配值时,您应该使用已知均匀分布的舍入方式对所有值进行四舍五入。最后,最后一个值将被分配不同以将总和舍入为1
。
让我们缓慢开始,否则事情会变得非常混乱。首先,让我们看看如何分配最后一个值以获得所需值的总和。
sum = 0;
for (i = 0; i < output.length - 1; i++)
{
output[i] = input[i] * total;
sum += output[i];
}
output[i] = total - sum;
最后一行需要一些解释。在 for(..)
循环中,i
的值将比允许的最大整数多1,因此它将是:
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算法。
为了简化问题,我在这里将展示如何在一维中实现它(这就是你的情况)。
让我们先讨论一个术语:余数。这是在你四舍五入数字后剩下的东西。它被计算为你希望得到的值与实际值之间的差:
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 ]
只有当所有值都远离0时,您才能确信最后一个值也将为正。因此,对于一般情况下的Bresenham算法需要使用floor函数,从而得出以下实现:
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
数组必须具有相同的大小,并且 input
中的值必须是一个分区(总和为1)。
这种算法在概率和统计计算中非常常见。