【问题标题】:Preventing overflows and preserving precision when scaling (long-)integers缩放(长)整数时防止溢出并保持精度
【发布时间】:2013-06-28 14:03:24
【问题描述】:

假设我在给定的范围中有一个位置pos,这样:

  0 pos 范围

范围内的这个位置可以包含在两种不同的上下文中,一种是范围是整数值,即 pos range 31 sup>,另一个范围是长整数,即最大 pos range 63。如果我想在这些上下文之间移动,我需要将位置缩放到新范围,以便正确地向下舍入到最接近的(长)整数值。所以,从技术上讲,我想做的就是:

   pos new = floor( pos old * range新的 / 范围旧的 )

不幸的是,这种直截了当的方法并不能解决问题,因为它要么溢出(如 pos old * range new 可以大到 ~294) 如果我先做乘法或如果我先做除法会给我舍入错误。使用浮点值进行数学运算通常也无济于事,因为它们不能提供足够的精度位,因此也可能导致不正确的舍入(我只有双精度可用)。

我找到了一种从整数范围正确缩放到长整数范围的方法:

public long scaleUp(int oldPos, int oldRange, long newRange) {
    return (newRange / oldRange) * oldPos + 
           (newRange % oldRange) * oldPos / oldRange;
}

这可确保计算在任何时候都不会超出长整数的限制,也不会因过早舍入而降低精度(模数捕获在第一次除法中舍入丢失的部分)。

我现在想弄清楚的是一种反向缩放的方法:

public int scaleDown(long oldPos, long oldRange, int newRange) {
    return ??? ;
}

不确定这是否应该比其他功能更困难,但不知何故我没有看到它。

几点说明:

  • 我想避免使用浮点运算,因为我总是很难说服自己,由于四舍五入,在某些极不常见的情况下,给定公式确实不可能产生意外结果
  • 我宁愿不使用 BigInteger 库
  • 虽然这里的代码示例是 Java,但这确实是一个与语言无关的问题

【问题讨论】:

  • 它比您想象的更特定于语言:IIRC java 使得使用基本工具(例如计算数字的前导零或获得两个 64- 的 128 位乘积)变得更加困难/低效位整数。甚至可以做无符号算术! (更不用说某些语言内置了任意精度的整数;很难想象 python 实现可能比p * r // R 更好)

标签: integer precision long-integer integer-overflow


【解决方案1】:

我找到了一个不是 100% 完整的答案,但涵盖了我的程序中出现的所有特殊情况。您可以在我在数学 StackExchange 上发布的相应问题的答案中找到推导的详细信息:https://math.stackexchange.com/q/433729/84557

这是粗略的大纲:

public int scaleDown(long oldPos, long oldRange, int newRange) {
    if (oldPos <= Long.MAX_VALUE/newRange)
        return (int) (oldPos*newRange/oldRange);
    assert oldRange >= newRange*newRange : "Case not supported yet"; // Never happens in my code
    int newPos = (int) (oldPos / (oldRange/newRange));
    if (!isOk(newPos)) newPos--; // Check might be implementation specific
    return newPos;
}

不完整,但也许对某人有用。

【讨论】:

    【解决方案2】:

    我参加这个聚会非常非常晚,但我遇到了完全相同的问题。

    请注意,我一直使用术语 x 表示您的 posy 表示您的 oldRange,并使用 w 表示您的 newRange,即求解方程 @987654330 中的 z @。

    以下是我考虑过的解决方案:

    我可以使用浮点数,但是对于 64 位整数,精度是否可以接受完全取决于平台是否支持尾数至少为 64 位的浮点数。 (许多平台没有。)

    我可以将输入域重新定义为比输出范围小不超过一半的域,然后按比例放大 - 即q = y / w; scaleUp(x / q / 2, y / q / 2, w);,在最坏的情况下,最低位将完全丢失,并且在最好的情况是根本不会丢失任何信息,但无论如何我都不想丢失低位。

    我可以使用一种二进制搜索来查找要从 x / (y / w) 中减去的值,以补偿错误,但我认为使用循环或递归对性能的影响是不可接受的。

    现在我想出的最好的方法是通常计算 x / (y / w) - (y % w) * x / y 并且当乘法溢出时——当然,这应该只适用于相对较小的输入域(当然输入比仅仅计算 @ 少987654334@) - 使用诸如this one之类的算法来捕获乘积的高位。

    但我仍然觉得有必须有一个更简单的方法来做到这一点。

    【讨论】:

    • 看起来你的公式有点像我在这里发布的递归解决方案的第一步:math.stackexchange.com/questions/433729/…(UPDATE 下的最后一个公式)
    • 我以与y &lt;= w 相同的方式处理它,即通过简单的除法和加法(在y &lt;= w 的情况下)或减法(在y &gt; w) 错误补偿项的情况。我想出了int q = y / w,然后是x / q - (y / q - w) * x / y。从本质上讲,这将x in [0, y) 的范围缩小到x in [0, n),其中w &lt;= n &lt; 2w 没有精度损失。它并没有消除溢出的机会,但大大减少了发生溢出的输入域。该操作相当于x / (y / w) - (y % w) * x / y
    • 哦,不,我是完全稠密的,它不能被重写为模数。那是我的错。 x / q - (y / q - w) * x / y句号。 (虽然我也一直在研究编写 x / q - (y / q - w) * (x / q) / (y / q) 以进一步减少导致溢出的输入域的选项 - 我认为但不是 100% 确定这不会降低结果的准确性。)
    • 您的情况是否也可以确定y &gt;= w^2?如果是这样,您应该能够使我在上面发布的解决方案起作用。如果没有,对于y &lt; w^2 的情况,您可以回退到任意精度数学库(或其简化版本,允许您进行高达 128 位整数的数学运算)...
    • 但是,如果您找到更优雅的答案,请告诉我! :) 过去一个月的大部分时间里,我一直在为这个问题绞尽脑汁,希望得到更完整的解决方案。
    【解决方案3】:

    编辑:此解决方案不正确,请参阅 cmets 中的讨论。

    我有一个没有特殊情况的解决方案。我没有正确性的证明,但它看起来很合理,我找不到反例。

    int scaleDown2(long longPos, long longRange, int shortRange) {
        int p = longPos / (longRange / shortRange);
        return p - ((p * (longRange % shortRange)) / longRange);
    }
    

    【讨论】:

    • 感谢您的回复!我会尝试看看我是否可以证明这是有效的。
    • 看起来这个公式实际上是不正确的。一个非常简单的反例是 scaleDown2(1, 3, 2) 返回 1,但实际答案应该是 0,因为 1*2/3 = 0.6666...,应该向下舍入为 0。
    • 看来我误解了你在寻找什么功能。我试图从字面上扭转 scaleUp。为了让 scaleUp(X, 2, 3) 产生 1,我们需要 X=1,所以我认为这就是我们要寻找的。​​span>
    • 这是有道理的。我想我应该指定scaleDown 不一定是scaleUp 的倒数。我确实 - 或者我猜在 2013 年“做到了”;) - 需要根据 posₙₑᵥᵥ 的公式向下舍入结果。我正在使用这些映射来实现范围编码算法。
    猜你喜欢
    • 2017-12-06
    • 2021-10-01
    • 2019-08-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-17
    • 2018-06-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多