简体   繁体   English

如何以最佳方式在一组百分比上分配值?

[英]How do I optimally distribute values over an array of percentages?

Let's say I have the following code:假设我有以下代码:

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

To find out how to equally distribute a hundred over the array is simple, it's a case of:要找出如何在数组上平均分配 100 个很简单,有一个例子:

0.1 * 100 = 10
0.5 * 100 = 50
...

Or doing it using a for loop:或者使用 for 循环来做:

for (var i = 0; j < arr.length; i++) {
    arr2[i] = arr[i] * value;
}

However, let's say each counter is an object and thus has to be whole .但是,假设每个计数器都是一个对象,因此必须是 whole How can I equally (as much as I can) distribute them on a different value.我如何平等地(尽可能多地)以不同的价值分配它们。 Let's say the value becomes 12.假设该值变为 12。

0.1 * 12 = 1.2
0.5 * 12 = 6
...

How do I deal with the decimal when I need it to be whole?当我需要整数时如何处理小数? Rounding means that I could potentially not have the 12 pieces needed.四舍五入意味着我可能没有所需的 12 件。

A correct algorithm would -正确的算法将 -

Take an input/iterate through an array of values (for this example we'll be using the array defined above.通过一个值数组进行输入/迭代(在这个例子中,我们将使用上面定义的数组。

Turn it into a set of whole values, which added together equal the value (which will equal 100 for this)把它变成一组完整的值,它们加起来等于这个值(这将等于 100)

Output an array of values which, for this example it will look something like [10,50,20,20] (these add up to 100, which is what we need to add them up to and also are all whole).输出一个值数组,对于这个例子,它看起来像 [10,50,20,20] (这些加起来是 100,这是我们需要把它们加起来并且都是完整的)。

If any value is not whole, it should make it whole so the whole array still adds up to the value needed (100).如果任何值不是完整的,它应该使它完整,这样整个数组仍然加起来需要的值 (100)。

TL;DR dealing with decimals when distributing values over an array and attempting to turn them into an integer TL;DR在数组上分配值并尝试将它们转换为整数时处理小数

Note - Should this be posted on a different stackoverflow website, my need is programming, but the actual question will likely be solved using a mathematics.注意 -如果将其发布在不同的 stackoverflow 网站上,我需要的是编程,但实际问题可能会使用数学来解决。 Also, I had no idea how to word this question, which makes googling incredibly difficult.另外,我不知道如何表达这个问题,这使得谷歌搜索变得非常困难。 If I've missed something incredibly obvious, please tell me.如果我遗漏了一些非常明显的东西,请告诉我。

You should round all values as you assign them using a rounding that is known to uniformly distribute the rounding.您应该在使用已知均匀分布舍入的舍入来分配所有值时舍入所有值。 Finally, the last value will be assigned differently to round the sum up to 1 .最后,最后一个值将以不同的方式分配以将总和四舍五入为1

Let's start slowly or things get very confused.让我们慢慢开始,否则事情会变得非常混乱。 First, let's see how to assign the last value to have a total of the desired value.首先,让我们看看如何分配最后一个值以获得所需值的总和。

// 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;

That last line needs some explanation.最后一行需要一些解释。 The i will be one more than the last allowed int the for(..) loop, so it will be: i将比for(..)循环中最后一个允许的 int 多一个,所以它将是:

output.length - 1 // last index

The value we assign will be so that the sum of all elements is equal to total .我们分配的值将使所有元素的sum等于total We already computed the sum in a single-pass during the assignment of the values, and thus don't need to iterated over the elements a second time to determine it.我们已经在分配值期间单次计算总和,因此不需要第二次迭代元素来确定它。

Next, we will approach the rounding problem.接下来,我们将处理舍入问题。 Let's simplify the above code so that it uses a function on which we will elaborate shortly after:让我们简化上面的代码,让它使用一个我们稍后会详细说明的函数:

sum = 0;
for (i = 0; i < output.length - 1; i++)
{
    output[i] = u(input[i], total);
    sum += output[i];
}

output[i] = total - sum;

As you can see, nothing has changed but the introduction of the u() function.如您所见,除了u()函数的引入之外,没有任何变化。 Let's concentrate on this now.现在让我们专注于这一点。

There are several approaches on how to implement u() .关于如何实现u()有几种方法。

DEFINITION
u(c, total) ::= c * total

By this definition you get the same as above.根据这个定义,你得到与上面相同的结果。 It is precise and good, but as you have asked before, you want the values to be natural numbers (eG integers).它既精确又好,但正如您之前所问的那样,您希望这些值是自然数(例如整数)。 So while for real numbers this is already perfect, for natural numbers we have to round it.因此,虽然对于实数这已经是完美的,但对于自然数,我们必须对其进行四舍五入。 Let's suppose we use the simple rounding rule for integers:假设我们对整数使用简单的舍入规则:

[ 0.0, 0.5 [  => round down
[ 0.5, 1.0 [  => round up

This is achieved with:这是通过以下方式实现的:

function u(c, total)
{
    return Math.round(c * total);
}

When you are unlucky, you may round up (or round down) so much values that the last value correction will not be enough to honor the total constraint and generally, all value will seem to be off by too much.当您不走运时,您可能会向上舍入(或向下舍入)太多值,以至于最后一次值修正不足以满足总约束条件,并且通常所有值似乎都偏离了太多。 This is a well known problem of which exists a multi-dimensional solution to draw lines in 2D and 3D space which is called the Bresenham algorithm .这是一个众所周知的问题,其中存在一种在 2D 和 3D 空间中绘制线条的多维解决方案,称为Bresenham 算法

To make things easy I'll show you here how to implement it in 1 dimension (which is your case).为方便起见,我将在此处向您展示如何在 1 维中实现它(这是您的情况)。

Let's first discuss a term: the remainder .让我们首先讨论一个术语:余数 This is what is left after you have rounded your numbers.这是四舍五入后剩下的。 It is computed as the difference between what you wish and what you really have:它被计算为你想要的和你真正拥有的之间的差异:

DEFINITION
WISH ::= c * total
HAVE ::= Math.round(WISH)
REMAINDER ::= WISH - HAVE

Now think about it.现在想想。 The remained is like the piece of paper that you discard when you cut out a shape from a sheet.剩下的就像你从一张纸上剪下一个形状时丢弃的一张纸。 That remaining paper is still there but you throw it away.剩下的那张纸还在那里,但你把它扔掉了。 Instead of this, just add it to the next cut-out so it is not wasted:取而代之的是,只需将其添加到下一个切口中,以免浪费:

WISH ::= c * total + REMAINDER_FROM_PREVIOUS_STEP
HAVE ::= Math.round(WISH)
REMAINDER ::= WISH - HAVE

This way you keep the error and carry it over to the next partition in your computation.通过这种方式,您可以保留错误并将其转移到计算中的下一个分区 This is called amortizing the error.这称为摊销错误。

Here is an amortized implementation of u() :这是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;
}

On your own accord you may wish to have another rounding rule as Math.floor() or Math.ceil() .根据您的意愿,您可能希望有另一个舍入规则,如Math.floor()Math.ceil()

What I would advise you to do is to use Math.floor() , because it is proven to be correct with the total constraint.我建议你做的是使用Math.floor() ,因为它被证明在总约束下是正确的。 When you use Math.round() you will have smoother amortization, but you risk to not have the last value positive.当您使用Math.round()您的摊销会更平滑,但您可能会面临最后一个值为正的风险。 You might end up with something like this:你可能会得到这样的结果:

[ 1, 0, 0, 1, 1, 0, -1 ]

Only when ALL VALUES are far away from 0 you can be confident that the last value will also be positive.只有当ALL VALUES远离0您才能确信最后一个值也是正数。 So, for the general case the Bresenham algoritm would use flooring, resulting in this last implementation:因此,对于一般情况, Bresenham 算法将使用地板,导致最后一个实现:

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;

Obviously, input and output array must have the same size and the values in input must be a paritition (sum up to 1).显然, inputoutput数组必须具有相同的大小,并且input的值必须是一个分区(总和为 1)。

This kind of algorithm is very common for probabilistical and statistical computations.这种算法在概率和统计计算中非常常见。

Alternate implementation - it remembers a pointer to the biggest rounded value and when the sum differs of 100, increment or decrement value at this position.替代实现 - 它记住一个指向最大舍入值的指针,当总和相差 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;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM