如何在一系列百分比中优化分配值?

时间:2014-12-06 09:45:41

标签: javascript arrays

假设我有以下代码:

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网站上发布,我需要编程,但实际问题可能会用数学来解决。此外,我不知道如何说出这个问题,这使谷歌搜索难以置信。如果我错过了一些非常明显的东西,请告诉我。

2 个答案:

答案 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;
}