【问题标题】:How to round floats to integers while preserving their sum?如何在保留总和的同时将浮点数舍入为整数?
【发布时间】:2009-04-27 06:53:15
【问题描述】:

假设我有一个浮点数数组,按排序(假设升序)顺序,其总和已知为整数N。我想将这些数字“四舍五入”为整数,同时保持它们的总和不变。换句话说,我正在寻找一种将浮点数数组(称为fn)转换为整数数组(称为in)的算法,这样:

  1. 两个数组的长度相同
  2. 整数数组的总和是N
  3. 每个浮点数 fn[i] 与其对应的整数 in[i] 之间的差小于 1(如果必须,则等于 1)
  4. 鉴于浮点数按排序顺序 (fn[i] <= fn[i+1]),整数也将按排序顺序 (in[i] <= in[i+1])

鉴于这四个条件都满足,最小化舍入方差的算法 (sum((in[i] - fn[i])^2)) 是可取的,但这不是什么大问题。

例子:

[0.02, 0.03, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.11, 0.12, 0.13, 0.14] => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] [0.1, 0.3, 0.4, 0.4, 0.8] => [0, 0, 0, 1, 1] [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] => [0, 0, 0, 0, 0, 0, 0, 0, 0, 1] [0.4, 0.4, 0.4, 0.4, 9.2, 9.2] => [0, 0, 1, 1, 9, 9] 更可取 => [0, 0, 0, 0, 10, 10] 是可以接受的 [0.5, 0.5, 11] => [0, 1, 11] 很好 => [0, 0, 12] 在技术上是不允许的,但我会在紧要关头接受它

回答 cmets 中提出的一些优秀问题:

  • 两个数组都允许重复元素(尽管我也有兴趣了解仅在浮点数组不包含重复时才有效的算法)
  • 没有单一的正确答案 - 对于给定的浮点输入数组,通常有多个整数数组满足这四个条件。
  • 我想到的应用程序是 - 这有点奇怪 - 在 MarioKart 游戏中将积分分配给排名靠前的玩家 ;-) 我自己从未真正玩过这款游戏,但在观看其他人时我注意到有 24积分分布在前 4 名选手中,我想知道如何根据完成时间分配积分(因此,如果某人以较大的领先优势完成比赛,他们将获得更大的积分份额)。游戏将总点数作为整数进行跟踪,因此需要进行这种四舍五入。

为了好奇,这里是 the test script 我用来识别哪些算法有效。

【问题讨论】:

  • 您想将小数部分从浮点数数组中分离出来?
  • 如果你有一个包含 1000 个 .001 的数组会发生什么?你希望它如何表现?是否允许重复?
  • @ojblass:在这种情况下,您会将其中的 999 个向下舍入为 0,然后将最后一个向上舍入为 1。这将满足要求。
  • 我在应用程序(估算软件)中看到了这一点,其中所有内容都四舍五入到美元并且底线数字必须匹配。
  • 还有另一个相关问题:stackoverflow.com/questions/13483430/…

标签: algorithm language-agnostic rounding


【解决方案1】:

您可以尝试的一个选项是“级联舍入”。

对于此算法,您需要跟踪两个运行总计:一个是浮点数,另一个是整数。 要获得下一个整数,请将下一个 fp 数添加到运行总计中,将运行总计四舍五入,然后从四舍五入的运行总计中减去整数运行总计:-

number  running total   integer integer running total
   1.3       1.3          1           1
   1.7       3.0          2           3
   1.9       4.9          2           5
   2.2       8.1          3           8
   2.8      10.9          3          11
   3.1      14.0          3          14

【讨论】:

  • 我认为这会比我的四舍五入更准确。
  • 我认为这会使“如果 fn 已排序 -> in 已排序”规则无效,而无需额外的排序步骤。 [0.4, 0.2, 0.4, 0.4, 0.2, 0.4, ...] 将四舍五入为 [0, 1, 0, 0, 1, 0, ... ]。
  • 呃。好点子。我混合了两个反例。原来是 [ 0.3, 0.3, 0.3, 0.3, ... ],它将变成 [0, 1, 0, 0, ...]。 [0.4, 0.2, 0.4, 0.2, ..] 示例应该是 [0.4, 1.2, 2.4],这表明舍入误差小于最佳舍入误差。由于前 0.4 舍入为 0(误差 0.4),1.2 舍入为 2(误差总和 1.2),2.4 再次舍入为 2(误差总和 2.8),而最佳值将向上舍入 0.4 或 2.4(误差 0.6),其余 0.4 向下舍入(错误总和 1.0 和 1.2 下降(错误总和 1.2)。
  • 我一直在测试解决方案,看起来如果你放弃排序要求,这可行,尽管它仍然不是最佳的。
  • 对于其他人,这里是这个算法的 javascript fiddle 实现:jsfiddle.net/cd8xqy6e
【解决方案2】:

这是一个应该完成任务的算法。与其他算法的主要区别在于,这个算法总是以正确的顺序对数字进行四舍五入。 最小化舍入误差。

该语言是一些可能源自 JavaScript 或 Lua 的伪语言。应该说明点。请注意基于一的索引(对于循环,x 到 y 更好。:p)

// Temp array with same length as fn.
tempArr = Array(fn.length)

// Calculate the expected sum.
arraySum = sum(fn)

lowerSum = 0
-- Populate temp array.
for i = 1 to fn.lengthf
    tempArr[i] = { result: floor(fn[i]),              // Lower bound
                   difference: fn[i] - floor(fn[i]),  // Roundoff error
                   index: i }                         // Original index

    // Calculate the lower sum
    lowerSum = lowerSum + tempArr[i].result
end for

// Sort the temp array on the roundoff error
sort(tempArr, "difference")

// Now arraySum - lowerSum gives us the difference between sums of these
// arrays. tempArr is ordered in such a way that the numbers closest to the
// next one are at the top.
difference = arraySum - lowerSum

// Add 1 to those most likely to round up to the next number so that
// the difference is nullified.
for i = (tempArr.length - difference + 1) to tempArr.length
    tempArr.result = tempArr.result + 1
end for

// Optionally sort the array based on the original index.
array(sort, "index")

【讨论】:

【解决方案3】:

一个非常简单的方法是把所有的小数部分加起来。根据您的问题的定义,该数字必须是整数。从最大的数字开始均匀分配该整数。然后将 1 分配给第二大的数字……以此类推,直到您分发的东西用完为止。

注意这是伪代码......并且可能在索引中偏离了一个......它已经晚了,我很困。

float accumulator = 0;

for (i = 0; i < num_elements; i++)  /* assumes 0 based array */
{
   accumulator += (fn[i] - floor(fn[i])); 
   fn[i] =  (fn[i] - floor(fn[i]);
}

i = num_elements;

while ((accumulator > 0) && (i>=0))
{
    fn[i-1] += 1;   /* assumes 0 based array */
    accumulator -= 1;
    i--;
}

更新:还有其他方法可以根据对每个值执行多少截断来分配累积值。这需要保留一个单独的列表,称为 loss[i] = fn[i] - floor(fn[i])。然后,您可以重复 fn[i] 列表并重复给最大损失项 1(之后将 loss[i] 设置为 0)。它很复杂,但我想它可以工作。

【讨论】:

  • ... 并跳过那些在增加时会变得未排序的数字。
  • 嗯,我认为从最大的往下走,你会按照定义保持排序顺序......我错过了什么吗?例子?
  • 如果您从最大、第二大等开始 - 那么它们将如何变得未排序?
  • 不错的答案 - 我喜欢这个 ;-p
  • [ 0.4, 0.4, 0.4, 0.4, 9.2, 9.2 ] 怎么样?我相信算法应该在这里提供 [ 0, 0, 1, 1, 9, 9 ] 的答案。
【解决方案4】:

怎么样:

a) start: array is [0.1, 0.2, 0.4, 0.5, 0.8], N=3, presuming it's sorted
b) round them all the usual way: array is [0 0 0 1 1]
c) get the sum of the new array and subtract it from N to get the remainder.
d) while remainder>0, iterate through elements, going from the last one
   - check if the new value would break rule 3.
   - if not, add 1
e) in case that remainder<0, iterate from first one to the last one
   - check if the new value would break rule 3.
   - if not, subtract 1

【讨论】:

  • 可以确定前 n 个值,(其中 n 是浮点数和整数总和之间的差)这将消除对数组进行排序的需要。使用堆获取前 n 个值可以进行 O(n) 操作。
【解决方案5】:

基本上你要做的是在四舍五入后将剩菜分配给最有可能的候选人。

  1. 像往常一样对浮点数进行舍入,但要跟踪从舍入到fnin 的相关索引的增量。
  2. 按增量对第二个数组进行排序。
  3. sum(in) &lt; N 时,从最大的负增量向前推进,增加舍入值(确保您仍然满足规则 #3)。
  4. 或者,在 sum(in) &gt; N 时,从最大的正增量向后工作,减小舍入值(确保您仍然满足规则 #3)。

例子:

[0.02, 0.03, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.11, 0.12, 0.13, 0.14] N=1

1. [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] sum=0
和 [[-0.02, 0], [-0.03, 1], [-0.05, 2], [-0.06, 3], [-0.07, 4], [-0.08, 5],
     [-0.09, 6], [-0.1, 7], [-0.11, 8], [-0.12, 9], [-0.13, 10], [-0.14, 11]]

2.排序会反转数组

3. 从最大的负余数开始计算,得到 [-0.14, 11]。
增加 `in[11]` 得到 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] sum=1
完成。

【讨论】:

  • 在你使用的时候买一个磁通电容器。
  • 是的,绝对同意这有点乱,但它应该可以工作。
  • 这应该可以。虽然您可以通过始终向一个方向(例如向下)四舍五入然后从最大的 abs(delta) 工作并进行补偿来使其更清晰。
  • 按小数部分对数组进行排序将是一个额外的 O(NlgN) 步骤,但它可以使最坏情况的舍入误差最小化。对于某些数据集,它可能仍然任意接近 1(例如,如果有一百万个值,所有这些值都以 .999999 结尾,则必须向下舍入)但按 delta 排序将允许一个实现最小绝对舍入任何给定数据集都可以实现的错误。
【解决方案6】:

你能试试这样的吗?

in [i] = fn [i] - int (fn [i]);
fn_res [i] = fn [i] - in [i];

fn_res → 是结果分数。 (我认为这是基本的......),我们错过了什么吗?

【讨论】:

  • 让我们用它的值替换in [i]fn_res [i] = fn [i] - ( fn [i] - int (fn [i]) ) = int (fn [i]) 。所以,似乎缺少了什么;)
【解决方案7】:

嗯,4 是痛点。否则,您可以执行诸如“通常向下取整并累积剩余部分;当累加器 >= 1 时向上取整”之类的操作。 (编辑:实际上,只要您交换他们的位置,那可能仍然可以?)

可能有一种方法可以通过线性规划来做到这一点? (这是数学“编程”,而不是计算机编程 - 你需要一些数学才能找到可行的解决方案,尽管你可能会跳过通常的“优化”部分)。

作为线性规划的一个例子 - 用例子 [1.3, 1.7, 1.9, 2.2, 2.8, 3.1] 你可以有规则:

1 <= i < 2
1 <= j < 2
1 <= k < 2
2 <= l < 3
3 <= m < 4
i <= j <= k <= l <= m
i + j + k + l + m = 13

然后应用一些线性/矩阵代数;-p 提示:有一些产品可以基于“Simplex”算法之类的东西来完成上述操作。常见的大学素材也是如此(我在 uni 为我的期末项目写了一篇)。

【讨论】:

    【解决方案8】:

    在我看来,问题在于没有指定排序算法。或者更像 - 无论它是否是稳定的排序。

    考虑以下浮点数组:

    [0.2 0.2 0.2 0.2 0.2]

    总和是 1。那么整数数组应该是:

    [0 0 0 0 1]

    但是,如果排序算法不稳定,它可以将“1”排序到数组中的其他位置...

    【讨论】:

    • 从这个意义上说,它应该是一个稳定的排序。 [0 0 0 0 1] 将是期望的结果。规则 4 的意图是说 [0 0 0 1 0] 是不可接受的。 (不过很好)
    【解决方案9】:

    使总的差异小于 1,并检查以进行排序。 有些喜欢,

    while(i < sizeof(fn) / sizeof(float)) {
        res += fn[i] - floor(fn[i]);
        if (res >= 1) {
            res--;
            in[i] = ceil(fn[i]);
        }
        else
            in[i] = floor(fn[i]);
        if (in[i-1] > in[i])
            swap(in[i-1], in[i++]);
    }
    

    (这是纸质代码,所以我没有检查有效性。)

    【讨论】:

      【解决方案10】:

      在@mikko-rantanen 的代码的python 和numpy 实现下方。我花了一点时间才把这些放在一起,所以这可能对未来的 Google 员工有所帮助,尽管这个话题已经很老了。

      import numpy as np
      from math import floor
      
      original_array = np.array([1.2, 1.5, 1.4, 1.3, 1.7, 1.9])
      
      # Calculate length of original array
      # Need to substract 1, as indecies start at 0, but product of dimensions
      # results in a count starting at 1
      array_len = original_array.size - 1 # Index starts at 0, but product at 1
      
      # Calculate expected sum of original values (must be integer)
      expected_sum = np.sum(original_array)
      
      # Collect values for temporary array population
      array_list = []
      lower_sum = 0
      for i, j in enumerate(np.nditer(original_array)):
          array_list.append([i, floor(j), j - floor(j)]) # Original index, lower bound, roundoff error
      # Calculate the lower sum of values
      lower_sum += floor(j)
      
      # Populate temporary array
      temp_array = np.array(array_list)
      
      # Sort temporary array based on roundoff error
      temp_array = temp_array[temp_array[:,2].argsort()]
      
      # Calculate difference between expected sum and the lower sum
      # This is the number of integers that need to be rounded up from the lower sum
      # The sort order (roundoff error) ensures that the value closest to be
      # rounded up is at the bottom of the array
      difference = int(expected_sum - lower_sum)
      
      # Add one to the number most likely to round up to eliminate the difference
      temp_array_len, _ = temp_array.shape
      for i in xrange(temp_array_len - difference, temp_array_len):
          temp_array[i,1] += 1
      
      # Re-sort the array based on original index
      temp_array = temp_array[temp_array[:,0].argsort()]
      
      # Return array to one-dimensional format of original array
      array_list = []
      for i in xrange(temp_array_len):
          array_list.append(int(temp_array[i,1]))
      new_array = np.array(array_list)
      

      【讨论】:

      • 我在问题中链接的脚本中也有一个Python实现
      • 为什么我没有早点看到这个 - 可以节省我一些时间。 ;) 不过非常有帮助,谢谢!
      【解决方案11】:

      计算sum of floorsum of numbers。 将sum of numbers 舍入,用sum of floor 减去,区别在于我们需要修补多少个天花板(我们需要多少个+1)。 将数组按照上限与数字的差异从小到大进行排序。

      对于diff 次(diff 是我们需要修补的上限),我们将结果设置为ceiling of number。其他人设置结果为floor of numbers

      public class Float_Ceil_or_Floor {
      
      public static int[] getNearlyArrayWithSameSum(double[] numbers) {
      
          NumWithDiff[] numWithDiffs = new NumWithDiff[numbers.length];
          double sum = 0.0;
          int floorSum = 0;
          for (int i = 0; i < numbers.length; i++) {
              int floor = (int)numbers[i];
              int ceil = floor;
              if (floor < numbers[i]) ceil++; // check if a number like 4.0 has same floor and ceiling
              floorSum += floor;
              sum += numbers[i];
              numWithDiffs[i] = new NumWithDiff(ceil,floor, ceil - numbers[i]);
          }
      
          // sort array by its diffWithCeil
          Arrays.sort(numWithDiffs, (a,b)->{
              if(a.diffWithCeil < b.diffWithCeil)  return -1;
              else return 1;
          });
      
          int roundSum = (int) Math.round(sum);
          int diff = roundSum - floorSum;
          int[] res = new int[numbers.length];
      
          for (int i = 0; i < numWithDiffs.length; i++) {
              if(diff > 0 && numWithDiffs[i].floor != numWithDiffs[i].ceil){
                  res[i] = numWithDiffs[i].ceil;
                  diff--;
              } else {
                  res[i] = numWithDiffs[i].floor;
              }
          }
          return res;
      }
      public static void main(String[] args) {
          double[] arr = { 1.2, 3.7, 100, 4.8 };
          int[] res = getNearlyArrayWithSameSum(arr);
          for (int i : res) System.out.print(i + " ");
      
      }
      

      }

      class NumWithDiff {
          int ceil;
          int floor;
          double diffWithCeil;
          public NumWithDiff(int c, int f, double d) {
              this.ceil = c;
              this.floor = f;
              this.diffWithCeil = d;
          }
      }
      

      【讨论】:

        【解决方案12】:

        在不最小化方差的情况下,这是一个微不足道的:

        1. 从左到右对值进行排序。
        2. 四舍五入到下一个整数。
        3. 让这些整数的总和为 K。将 N-K 最右边的值增加 1。
        4. 恢复原订单。

        这显然满足您的条件 1.-4。或者,您可以四舍五入到最接近的整数,并增加您向下舍入的 N-K。您可以通过原始值和舍入值之间的差异贪婪地做到这一点,但每次舍入值只能从右向左增加,以保持排序顺序。

        【讨论】:

        【解决方案13】:

        如果您可以在改善方差的同时接受总数的微小变化,这将在概率上保留 python 中的总数:

        import math
        import random
        integer_list = [int(x) + int(random.random() <= math.modf(x)[0]) for x in my_list]
        

        为了解释它,将所有数字向下舍入并以等于小数部分的概率加一,即十分之一的0.1 将变为1,其余为0

        这适用于将大量小数人转换为 1 人或 0 人的统计数据

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-10-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多