考虑下面四个百分比,用浮点数表示:

    13.626332%
    47.989636%
     9.596008%
    28.788024%
   -----------
   100.000000%

我需要用整数表示这些百分比。如果我简单地使用Math.round(),我最终得到的总数是101%。

14 + 48 + 10 + 29 = 101

如果我使用parseInt(),我最终得到了97%。

13 + 47 + 9 + 28 = 97

有什么好的算法可以将任何百分比数表示为整数,同时还保持总数为100%?


编辑:在阅读了一些评论和回答后,显然有很多方法可以解决这个问题。

在我看来,为了保持数字的真实性,“正确”的结果是最小化总体误差的结果,定义为相对于实际值会引入多少误差舍入:

        value  rounded     error               decision
   ----------------------------------------------------
    13.626332       14      2.7%          round up (14)
    47.989636       48      0.0%          round up (48)
     9.596008       10      4.0%    don't round up  (9)
    28.788024       29      2.7%          round up (29)

在平局的情况下(3.33,3.33,3.33)可以做出任意的决定(例如3,4,3)。


当前回答

我的JS实现由Varun Vohra投票的答案

const set1 = [13.626332, 47.989636, 9.596008, 28.788024];
// const set2 = [24.25, 23.25, 27.25, 25.25];

const values = set1;

console.log('Total: ', values.reduce((accum, each) => accum + each));
console.log('Incorrectly Rounded: ', 
  values.reduce((accum, each) => accum + Math.round(each), 0));

const adjustValues = (values) => {
  // 1. Separate integer and decimal part
  // 2. Store both in a new array of objects sorted by decimal part descending
  // 3. Add in original position to "put back" at the end
  const flooredAndSortedByDecimal = values.map((value, position) => (
    {
        floored: Math.floor(value),
        decimal: value - Number.parseInt(value),
        position
    }
  )).sort(({decimal}, {decimal: otherDecimal}) => otherDecimal - decimal);

  const roundedTotal = values.reduce((total, value) => total + Math.floor(value), 0);
  let availableForDistribution = 100 - roundedTotal;

  // Add 1 to each value from what's available
  const adjustedValues = flooredAndSortedByDecimal.map(value => {
    const { floored, ...rest } = value;
    let finalPercentage = floored;
    if(availableForDistribution > 0){
        finalPercentage = floored + 1;
        availableForDistribution--;
    }

    return {
        finalPercentage,
        ...rest
    }
  });

  // Put back and return the new values
  return adjustedValues
    .sort(({position}, {position: otherPosition}) => position - otherPosition)
    .map(({finalPercentage}) => finalPercentage);
}

const finalPercentages = adjustValues(values);
console.log({finalPercentages})

// { finalPercentage: [14, 48, 9, 29]}

其他回答

您可以尝试跟踪由于舍入而产生的误差,如果累计误差大于当前数字的小数部分,则再反向舍入。

13.62 -> 14 (+.38)
47.98 -> 48 (+.02 (+.40 total))
 9.59 -> 10 (+.41 (+.81 total))
28.78 -> 28 (round down because .81 > .78)
------------
        100

不确定这是否适用于一般情况,但如果顺序相反,似乎也会有类似的效果:

28.78 -> 29 (+.22)
 9.59 ->  9 (-.37; rounded down because .59 > .22)
47.98 -> 48 (-.35)
13.62 -> 14 (+.03)
------------
        100

我相信在某些情况下,这种方法可能会失效,但任何方法都至少在某种程度上是任意的,因为您基本上是在修改输入数据。

检查如果这是有效的或不就我的测试用例,我能够得到这个工作。

假设number是k;

按降序排序百分比。 从降序遍历每个百分比。 计算k的百分比第一个百分比采取数学。输出的天花板。 下一个k = k-1 遍历直到所有百分比被消耗。

我曾经写过一个un舍入工具,来找到一组数字的最小扰动来匹配一个目标。这是一个不同的问题,但理论上可以在这里使用类似的想法。在这种情况下,我们有一系列的选择。

因此,对于第一个元素,我们可以四舍五入到14,也可以四舍五入到13。这样做的代价(在二进制整数编程的意义上)对于向上舍入比向下舍入要小,因为向下舍入需要我们将该值移动更大的距离。同样,我们可以把每个数字四舍五入,所以我们总共有16个选择。

  13.626332
  47.989636
   9.596008
+ 28.788024
-----------
 100.000000

我通常会在MATLAB中使用bintprog(一种二进制整数编程工具)解决一般问题,但这里只有几个选项需要测试,所以用简单的循环就可以很容易地测试出16个选项中的每一个。例如,假设我们将这个集合四舍五入为:

 Original      Rounded   Absolute error
   13.626           13          0.62633
    47.99           48          0.01036
    9.596           10          0.40399
 + 28.788           29          0.21198
---------------------------------------
  100.000          100          1.25266

总绝对误差为1.25266。它可以通过以下替代舍入来略微减少:

 Original      Rounded   Absolute error
   13.626           14          0.37367
    47.99           48          0.01036
    9.596            9          0.59601
 + 28.788           29          0.21198
---------------------------------------
  100.000          100          1.19202

事实上,这就是绝对误差的最优解。当然,如果有20项,搜索空间的大小将是2^20 = 1048576。对于30或40个术语,这个空间将是相当大的。在这种情况下,您将需要使用能够有效搜索空间的工具,可能使用分支和绑定方案。

舍入的目标是产生最少的错误。当您对单个值进行舍入时,这个过程简单而直接,大多数人都很容易理解。当你同时四舍五入多个数字时,这个过程变得更加棘手——你必须定义如何组合错误,即必须最小化的错误。

Varun Vohra的答案将绝对误差的总和最小化,而且实现起来非常简单。然而,有一些边缘情况它不能处理-舍入24.25,23.25,27.25,25.25的结果应该是什么?其中一个需要被围捕,而不是减少。你可能会任意选择列表中的第一个或最后一个。

也许用相对误差比绝对误差更好。将23.25四舍五入到24会使它变化3.2%,而将27.25四舍五入到28只会使它变化2.8%。现在有一个明显的赢家。

我们还可以做进一步的调整。一种常见的技术是对每个错误进行平方运算,这样大错误的计数就不成比例地多于小错误。我还会使用非线性除数来得到相对误差——1%的误差比99%的误差重要99倍,这似乎是不对的。在下面的代码中,我使用了平方根。

完整算法如下:

将这些百分比四舍五入后相加,再减去100。这将告诉您这些百分比中有多少必须四舍五入。 为每个百分比生成两个错误分数,一个是四舍五入,另一个是四舍五入。取两者之差。 对上面产生的误差差异进行排序。 对于需要四舍五入的百分比数,从已排序的列表中选取一项,并将四舍五入后的百分比增加1。

您仍然可能有多个具有相同错误和的组合,例如33.3333333,33.3333333,33.3333333。这是不可避免的,结果完全是任意的。下面给出的代码倾向于四舍五入左边的值。

在Python中把它们放在一起是这样的。

from math import isclose, sqrt

def error_gen(actual, rounded):
    divisor = sqrt(1.0 if actual < 1.0 else actual)
    return abs(rounded - actual) ** 2 / divisor

def round_to_100(percents):
    if not isclose(sum(percents), 100):
        raise ValueError
    n = len(percents)
    rounded = [int(x) for x in percents]
    up_count = 100 - sum(rounded)
    errors = [(error_gen(percents[i], rounded[i] + 1) - error_gen(percents[i], rounded[i]), i) for i in range(n)]
    rank = sorted(errors)
    for i in range(up_count):
        rounded[rank[i][1]] += 1
    return rounded

>>> round_to_100([13.626332, 47.989636, 9.596008, 28.788024])
[14, 48, 9, 29]
>>> round_to_100([33.3333333, 33.3333333, 33.3333333])
[34, 33, 33]
>>> round_to_100([24.25, 23.25, 27.25, 25.25])
[24, 23, 28, 25]
>>> round_to_100([1.25, 2.25, 3.25, 4.25, 89.0])
[1, 2, 3, 4, 90]

正如您在最后一个示例中看到的,该算法仍然能够提供非直观的结果。尽管89.0不需要四舍五入,但是列表中的一个值需要四舍五入;相对误差最小的结果是将较大的值舍入,而不是较小的可选值。

这个答案最初主张遍历所有可能的向上舍入/向下舍入组合,但正如评论中指出的那样,更简单的方法效果更好。算法和代码反映了这种简化。

我写了一个c#版本的舍入帮助器,算法和Varun Vohra的答案一样,希望对你有帮助。

public static List<decimal> GetPerfectRounding(List<decimal> original,
    decimal forceSum, int decimals)
{
    var rounded = original.Select(x => Math.Round(x, decimals)).ToList();
    Debug.Assert(Math.Round(forceSum, decimals) == forceSum);
    var delta = forceSum - rounded.Sum();
    if (delta == 0) return rounded;
    var deltaUnit = Convert.ToDecimal(Math.Pow(0.1, decimals)) * Math.Sign(delta);

    List<int> applyDeltaSequence; 
    if (delta < 0)
    {
        applyDeltaSequence = original
            .Zip(Enumerable.Range(0, int.MaxValue), (x, index) => new { x, index })
            .OrderBy(a => original[a.index] - rounded[a.index])
            .ThenByDescending(a => a.index)
            .Select(a => a.index).ToList();
    }
    else
    {
        applyDeltaSequence = original
            .Zip(Enumerable.Range(0, int.MaxValue), (x, index) => new { x, index })
            .OrderByDescending(a => original[a.index] - rounded[a.index])
            .Select(a => a.index).ToList();
    }

    Enumerable.Repeat(applyDeltaSequence, int.MaxValue)
        .SelectMany(x => x)
        .Take(Convert.ToInt32(delta/deltaUnit))
        .ForEach(index => rounded[index] += deltaUnit);

    return rounded;
}

通过以下单元测试:

[TestMethod]
public void TestPerfectRounding()
{
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> {3.333m, 3.334m, 3.333m}, 10, 2),
        new List<decimal> {3.33m, 3.34m, 3.33m});

    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> {3.33m, 3.34m, 3.33m}, 10, 1),
        new List<decimal> {3.3m, 3.4m, 3.3m});

    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> {3.333m, 3.334m, 3.333m}, 10, 1),
        new List<decimal> {3.3m, 3.4m, 3.3m});


    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 13.626332m, 47.989636m, 9.596008m, 28.788024m }, 100, 0),
        new List<decimal> {14, 48, 9, 29});
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 16.666m, 16.666m, 16.666m, 16.666m, 16.666m, 16.666m }, 100, 0),
        new List<decimal> { 17, 17, 17, 17, 16, 16 });
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 33.333m, 33.333m, 33.333m }, 100, 0),
        new List<decimal> { 34, 33, 33 });
    CollectionAssert.AreEqual(Utils.GetPerfectRounding(
        new List<decimal> { 33.3m, 33.3m, 33.3m, 0.1m }, 100, 0),
        new List<decimal> { 34, 33, 33, 0 });
}