如何避免expr中的溢出。A B C D


161

我需要计算一个看起来像:的表达式 A*B - C*D,其类型为:signed long long int A, B, C, D; 每个数字都可以很大(不会溢出其类型)。虽然A*B可能导致溢出,但表达式A*B - C*D的确很小。如何正确计算?

例如:MAX * MAX - (MAX - 1) * (MAX + 1) == 1,其中MAX = LLONG_MAX - n和n-一些自然数。


17
准确性有多重要?
Anirudh Ramanathan,2012年

1
@克苏鲁,很好的问题。他可以尝试通过使用所有数字除以10或某个值,然后将结果相乘来使用较小的数字来生成等效函数。
克里斯

4
Vars A,B,C,D已签名。这意味着A - C可能会溢出。是要考虑的问题,还是您知道数据不会发生这种情况?
威廉·莫里斯

2
@MooingDuck,但您可以事先检查操作是否会溢出stackoverflow.com/a/3224630/158285
bradgonesurfing 2012年

1
@Chris:不,我是说,没有可移植的方法来检查是否发生了签名溢出。(布拉德是正确的,你能够方便地检测到它发生)。使用内联汇编是许多不可移植的检查方法之一。
Mooing Duck 2012年

Answers:


120

我想这似乎太琐碎了。但是A*B可能会溢出。

您可以执行以下操作,而不会失去精度

A*B - C*D = A(D+E) - (A+F)D
          = AD + AE - AD - DF
          = AE - DF
             ^smaller quantities E & F

E = B - D (hence, far smaller than B)
F = C - A (hence, far smaller than C)

此分解可以进一步完成
正如@Gian所指出的,如果类型为unsigned long long,则在减法操作期间可能需要小心。


举例来说,如果您遇到问题,只需要进行一次迭代,

 MAX * MAX - (MAX - 1) * (MAX + 1)
  A     B       C           D

E = B - D = -1
F = C - A = -1

AE - DF = {MAX * -1} - {(MAX + 1) * -1} = -MAX + MAX + 1 = 1

4
@Caleb,只需将相同的算法应用于C*D
Chris

2
我认为您应该解释E代表什么。
Caleb

7
long long和double均为64位。由于double必须为指数分配一些位,因此它具有较小范围的可能值而不会损失精度。
Jim Garrison

3
@Cthulhu-在我看来,这只有在所有数字都非常大的情况下才能起作用...例如,您仍然会出现{A,B,C,D} = {MAX,MAX,MAX,2}溢出的情况。OP表示“每个数字都可能非常大”,但是从问题陈述中尚不清楚每个数字都必须非常大。
凯文(Kevin K)

4
如果有任何A,B,C,D负面结果怎么办?那会E还是F更大?
2012年

68

最简单,最通用的解决方案是使用不会溢出的表示形式,或者使用长整数库(例如http://gmplib.org/),或者使用结构或数组并实现一种长乘法(例如,将每个数字分成两个32位的一半,并按如下方式进行乘法:

(R1 + R2 * 2^32 + R3 * 2^64 + R4 * 2^96) = R = A*B = (A1 + A2 * 2^32) * (B1 + B2 * 2^32) 
R1 = (A1*B1) % 2^32
R2 = ((A1*B1) / 2^32 + (A1*B2) % 2^32 + (A2*B1) % 2^32) % 2^32
R3 = (((A1*B1) / 2^32 + (A1*B2) % 2^32 + (A2*B1) % 2^32) / 2^32 + (A1*B2) / 2^32 + (A2*B1) / 2^32 + (A2*B2) % 2^32) %2^32
R4 = ((((A1*B1) / 2^32 + (A1*B2) % 2^32 + (A2*B1) % 2^32) / 2^32 + (A1*B2) / 2^32 + (A2*B1) / 2^32 + (A2*B2) % 2^32) / 2^32) + (A2*B2) / 2^32

假设最终结果适合64位,那么您实际上并不需要R3的大多数位,也不需要R4


8
上面的计算实际上并不像它看起来的那么复杂,它实际上是以2为底数32的简单的长乘法,并且C语言中的代码应该看起来更简单。同样,创建通用函数来在程序中完成此工作也是个好主意。
Ofir 2012年

46

请注意,这不是标准的,因为它依赖于环绕签名溢出。(GCC具有启用此功能的编译器标志。)

但是,如果您只进行所有计算long long,则直接应用公式的结果:
(A * B - C * D)只要正确的结果适合,结果将是准确的long long


这是一种变通方法,仅依赖于将无符号整数转换为有符号整数的实现定义的行为。但这可以预期在当今几乎所有系统上都有效。

(long long)((unsigned long long)A * B - (unsigned long long)C * D)

这会将输入转换unsigned long long为标准保证溢出行为得到保证的地方。最后,将其强制转换回有符号整数是实现定义的部分,但将在当今几乎所有环境中运行。


如果您需要更多的学问解决方案,我认为您必须使用“长算术”


+1您是唯一注意到这一点的人。唯一棘手的部分是将编译器设置为环绕溢出,并检查正确的结果是否确实适合long long
Mysticial

2
即使是完全没有任何技巧的朴素版本,在大多数实现中也可以做正确的事情。它不是标准所保证的,但是您必须找到1的补码机或其他相当奇怪的设备才能使其失效。
hobbs 2012年

1
我认为这是一个重要的答案。我同意假定实现特定的行为可能不是正确的编程,但是每个工程师都应该了解模运算以及如何获得正确的编译器标志以确保性能必不可少的行为。DSP工程师将这种行为用于定点滤波器的实现,对此可接受的答案将具有不可接受的性能。
彼得M,

18

这应该工作(我认为):

signed long long int a = 0x7ffffffffffffffd;
signed long long int b = 0x7ffffffffffffffd;
signed long long int c = 0x7ffffffffffffffc;
signed long long int d = 0x7ffffffffffffffe;
signed long long int bd = b / d;
signed long long int bdmod = b % d;
signed long long int ca = c / a;
signed long long int camod = c % a;
signed long long int x = (bd - ca) * a * d - (camod * d - bdmod * a);

这是我的推论:

x = a * b - c * d
x / (a * d) = (a * b - c * d) / (a * d)
x / (a * d) = b / d - c / a

now, the integer/mod stuff:
x / (a * d) = (b / d + ( b % d ) / d) - (c / a + ( c % a ) / a )
x / (a * d) = (b / d - c / a) - ( ( c % a ) / a - ( b % d ) / d)
x = (b / d - c / a) * a * d - ( ( c % a ) * d - ( b % d ) * a)

1
感谢@bradgonesurfing-您能提供这样的输入吗?我已经更新了答案,执行了它,并且它可以正常工作(bd和ca为0)...
paquetp 2012年

1
嗯 现在我想起来也许不是。d = 1且a = 1且b = maxint和c = maxint的简并情况仍然有效。酷:)
bradgonesurfing 2012年

1
@paquetp:a = 1,b = 0x7fffffffffffffffff,c = -0x7fffffffffffffffff,d = 1(注意c为负)。不过很聪明,我敢肯定您的代码可以正确处理所有正数。
Mooing Duck 2012年

3
@MooingDuck,但您设置的最终答案也溢出了,因此它不是有效的设置。仅当两边都具有相同的符号时才起作用,因此所得的减法在范围内。
bradgonesurfing 2012年

1
与最得分高的答案相比,最简单,最优秀的答案与StackOverflow有所不同。
bradgonesurfing 2012年

9

您可以考虑为所有值计算最大公因数,然后在进行算术运算之前将其除以该因数,然后再相乘。这假定然而,这样的因子存在,(例如,如果ABC并且D恰好是互质,他们将不会有一个共同的因素)。

同样,您可以考虑在对数刻度上工作,但这受数字精度的影响会有些吓人。


1
如果long double可用,对数似乎很好。在这种情况下,可以达到可接受的精度水平(结果取整)。

9

如果结果适合长整型,则表达式A * BC * D可以执行2 ^ 64的算术mod,并且可以给出正确的结果。问题是要知道结果是否适合长整型。要检测到这一点,可以使用以下技巧使用双精度:

if( abs( (double)A*B - (double)C*D ) > MAX_LLONG ) 
    Overflow
else 
    return A*B-C*D;

这种方法的问题是,您受到双精度(54位?)尾数精度的限制,因此您需要将乘积A * B和C * D限制为63 + 54位(或者可能少一点)。


这是最实际的例子。清除并给出正确的答案(如果输入不正确,则抛出异常)。
Mark Lakata 2012年

1
尼斯和优雅!您没有落入别人所为的陷阱。再说一件事:我敢打赌,由于舍入错误,有些例子中的double计算低于MAX_LLONG。我的数学本能告诉我,您应该计算双精度和长精度结果之差,然后将其与MAX_LLONG / 2进行比较。这种差异是重复计算的四舍五入误差加上溢出,通常应该比较小,但是在我提到的情况下,它会很大。但是现在我懒得确定。:-)
汉斯·彼得·斯托尔

9
E = max(A,B,C,D)
A1 = A -E;
B1 = B -E;
C1 = C -E;
D1 = D -E;

然后

A*B - C*D = (A1+E)*(B1+E)-(C1+E)(D1+E) = (A1+B1-C1-D1)*E + A1*B1 -C1*D1

7

您可以将每个数字写到数组中,每个元素都是一个数字,然后将其作为多项式进行计算。取所得的多项式(一个数组),并通过将数组的每个元素乘以10到数组中位置的幂(第一个位置为最大,最后一个位置为零)来计算结果。

该数字123可以表示为:

123 = 100 * 1 + 10 * 2 + 3

为此,您只需创建一个数组[1 2 3]

对所有数字A,B,C和D都执行此操作,然后将它们乘以多项式。获得了多项式后,就可以从中重新构造数字。


2
不知道那是什么,但我必须找到。把:)。这是我和女友一起购物时脑海中的一个解决方案:)
Mihai 2012年

您正在以base10数组实现bignums。GMP是一个高质量的bignum库,它使用4294967296作为基础。速度更快。但是,请不要投反对票,因为答案是正确且有用的。
Mooing Duck 2012年

谢谢 :) 。知道这样做是一种有用的方法,但是有更好的方法,所以请不要这样做。至少不在这种情况下:)
Mihai 2012年

无论如何...使用此解决方案,您可以算出的数字比任何原始类型能加粗的数字(例如100位数字s)大得多,并将结果保留为数组。:p
Mihai

我不确定它是否会赞成,因为这种方法(虽然有效并且相对容易理解)会占用大量内存并且速度很慢。
Mooing Duck

6

虽然a signed long long int不会成立A*B,但其中两个会成立。因此A*B可以分解为不同指数的树术语,其中任何一个都适合一个signed long long int

A1=A>>32;
A0=A & 0xffffffff;
B1=B>>32;
B0=B & 0xffffffff;

AB_0=A0*B0;
AB_1=A0*B1+A1*B0;
AB_2=A1*B1;

相同C*D

按照直截了当的方式,可以对每一对进行相减,AB_i并且CD_i同样地,对每对使用一个附加的进位位(精确地为1位整数)。因此,如果我们说E = A * BC * D,您将得到类似:

E_00=AB_0-CD_0 
E_01=(AB_0 > CD_0) == (AB_0 - CD_0 < 0) ? 0 : 1  // carry bit if overflow
E_10=AB_1-CD_1 
...

我们继续将的上半部分转移E_10E_20(移位32,然后加上,然后擦除的上半部分E_10)。

现在,您可以E_11通过将带有正确符号(从非进位部分获得)的进位添加到进位来摆脱进位E_20。如果这触发了溢出,则结果也不适合。

E_10现在有足够的“空间”来占用E_00 (移位,添加,擦除)的上半部分和进位位E_01

E_10可能现在又更大了,因此我们将转移重复到E_20

此时,E_20必须变为零,否则结果将不合适。E_10转移的结果也是的上半部分为空。

最后一步是对的下半部转移E_20E_10一次。

如果期望E=A*B+C*D将适合的signed long long int成立,我们现在有

E_20=0
E_10=0
E_00=E

1
这实际上是使用Ofir的乘法公式并删除所有无用的临时结果时得到的简化公式。
dronus 2012年

3

如果您知道最终结果可以用整数类型表示,则可以使用以下代码快速执行此计算。因为C标准指定无符号算术是模算术并且不会溢出,所以可以使用无符号类型来执行计算。

以下代码假定存在相同宽度的无符号类型,并且该带符号类型使用所有位模式表示值(无陷阱表示,带符号类型的最小值为无符号类型模量的一半的负数)。如果这在C实现中不成立,则可以为此对ConvertToSigned例程进行简单的调整。

以下使用signed charunsigned char演示代码。对于您的实现,更改Signedto typedef signed long long int Signed;的定义和Unsignedto 的定义typedef unsigned long long int Unsigned;

#include <limits.h>
#include <stdio.h>
#include <stdlib.h>


//  Define the signed and unsigned types we wish to use.
typedef signed char   Signed;
typedef unsigned char Unsigned;

//  uHalfModulus is half the modulus of the unsigned type.
static const Unsigned uHalfModulus = UCHAR_MAX/2+1;

//  sHalfModulus is the negation of half the modulus of the unsigned type.
static const Signed   sHalfModulus = -1 - (Signed) (UCHAR_MAX/2);


/*  Map the unsigned value to the signed value that is the same modulo the
    modulus of the unsigned type.  If the input x maps to a positive value, we
    simply return x.  If it maps to a negative value, we return x minus the
    modulus of the unsigned type.

    In most C implementations, this routine could simply be "return x;".
    However, this version uses several steps to convert x to a negative value
    so that overflow is avoided.
*/
static Signed ConvertToSigned(Unsigned x)
{
    /*  If x is representable in the signed type, return it.  (In some
        implementations, 
    */
    if (x < uHalfModulus)
        return x;

    /*  Otherwise, return x minus the modulus of the unsigned type, taking
        care not to overflow the signed type.
    */
    return (Signed) (x - uHalfModulus) - sHalfModulus;
}


/*  Calculate A*B - C*D given that the result is representable as a Signed
    value.
*/
static signed char Calculate(Signed A, Signed B, Signed C, Signed D)
{
    /*  Map signed values to unsigned values.  Positive values are unaltered.
        Negative values have the modulus of the unsigned type added.  Because
        we do modulo arithmetic below, adding the modulus does not change the
        final result.
    */
    Unsigned a = A;
    Unsigned b = B;
    Unsigned c = C;
    Unsigned d = D;

    //  Calculate with modulo arithmetic.
    Unsigned t = a*b - c*d;

    //  Map the unsigned value to the corresponding signed value.
    return ConvertToSigned(t);
}


int main()
{
    //  Test every combination of inputs for signed char.
    for (int A = SCHAR_MIN; A <= SCHAR_MAX; ++A)
    for (int B = SCHAR_MIN; B <= SCHAR_MAX; ++B)
    for (int C = SCHAR_MIN; C <= SCHAR_MAX; ++C)
    for (int D = SCHAR_MIN; D <= SCHAR_MAX; ++D)
    {
        //  Use int to calculate the expected result.
        int t0 = A*B - C*D;

        //  If the result is not representable in signed char, skip this case.
        if (t0 < SCHAR_MIN || SCHAR_MAX < t0)
            continue;

        //  Calculate the result with the sample code.
        int t1 = Calculate(A, B, C, D);

        //  Test the result for errors.
        if (t0 != t1)
        {
            printf("%d*%d - %d*%d = %d, but %d was returned.\n",
                A, B, C, D, t0, t1);
            exit(EXIT_FAILURE);
        }
    }
    return 0;
}

2

您可以尝试将方程式分解为一些较小的组件,这些组件不会溢出。

AB - CD
= [ A(B - N) - C( D - M )] + [AN - CM]

= ( AK - CJ ) + ( AN - CM)

    where K = B - N
          J = D - M

如果组件仍然溢出,则可以将它们递归分解为较小的组件,然后重新组合。


这可能正确,也可能不正确,但绝对令人困惑。您可以定义KJ,为什么不NM。另外,我认为您正在将方程分解成更大的部分。由于您的第3步与OP的问题相同,除了更复杂(AK-CJ)->(AB-CD)
Mooing Duck 2012年

N并没有简化。只是从A减去一个数字以使其更小。实际上,这是与paquetp类似但次等的解决方案。在这里,我使用减法而不是整数除法来减小它。
bradgonesurfing 2012年

2

我可能没有涵盖所有边缘情况,也没有进行严格的测试,但这实现了我记得在80年代尝试在16位cpu上进行32位整数数学运算时使用的技术。本质上,您将32位拆分为两个16位单元,并分别使用它们。

public class DoubleMaths {
  private static class SplitLong {
    // High half (or integral part).
    private final long h;
    // Low half.
    private final long l;
    // Split.
    private static final int SPLIT = (Long.SIZE / 2);

    // Make from an existing pair.
    private SplitLong(long h, long l) {
      // Let l overflow into h.
      this.h = h + (l >> SPLIT);
      this.l = l % (1l << SPLIT);
    }

    public SplitLong(long v) {
      h = v >> SPLIT;
      l = v % (1l << SPLIT);
    }

    public long longValue() {
      return (h << SPLIT) + l;
    }

    public SplitLong add ( SplitLong b ) {
      // TODO: Check for overflow.
      return new SplitLong ( longValue() + b.longValue() );
    }

    public SplitLong sub ( SplitLong b ) {
      // TODO: Check for overflow.
      return new SplitLong ( longValue() - b.longValue() );
    }

    public SplitLong mul ( SplitLong b ) {
      /*
       * e.g. 10 * 15 = 150
       * 
       * Divide 10 and 15 by 5
       * 
       * 2 * 3 = 5
       * 
       * Must therefore multiply up by 5 * 5 = 25
       * 
       * 5 * 25 = 150
       */
      long lbl = l * b.l;
      long hbh = h * b.h;
      long lbh = l * b.h;
      long hbl = h * b.l;
      return new SplitLong ( lbh + hbl, lbl + hbh );
    }

    @Override
    public String toString () {
      return Long.toHexString(h)+"|"+Long.toHexString(l);
    }
  }

  // I'll use long and int but this can apply just as easily to long-long and long.
  // The aim is to calculate A*B - C*D without overflow.
  static final long A = Long.MAX_VALUE;
  static final long B = Long.MAX_VALUE - 1;
  static final long C = Long.MAX_VALUE;
  static final long D = Long.MAX_VALUE - 2;

  public static void main(String[] args) throws InterruptedException {
    // First do it with BigIntegers to get what the result should be.
    BigInteger a = BigInteger.valueOf(A);
    BigInteger b = BigInteger.valueOf(B);
    BigInteger c = BigInteger.valueOf(C);
    BigInteger d = BigInteger.valueOf(D);
    BigInteger answer = a.multiply(b).subtract(c.multiply(d));
    System.out.println("A*B - C*D = "+answer+" = "+answer.toString(16));

    // Make one and test its integrity.
    SplitLong sla = new SplitLong(A);
    System.out.println("A="+Long.toHexString(A)+" ("+sla.toString()+") = "+Long.toHexString(sla.longValue()));

    // Start small.
    SplitLong sl10 = new SplitLong(10);
    SplitLong sl15 = new SplitLong(15);
    SplitLong sl150 = sl10.mul(sl15);
    System.out.println("10="+sl10.longValue()+"("+sl10.toString()+") * 15="+sl15.longValue()+"("+sl15.toString()+") = "+sl150.longValue() + " ("+sl150.toString()+")");

    // The real thing.
    SplitLong slb = new SplitLong(B);
    SplitLong slc = new SplitLong(C);
    SplitLong sld = new SplitLong(D);
    System.out.println("B="+Long.toHexString(B)+" ("+slb.toString()+") = "+Long.toHexString(slb.longValue()));
    System.out.println("C="+Long.toHexString(C)+" ("+slc.toString()+") = "+Long.toHexString(slc.longValue()));
    System.out.println("D="+Long.toHexString(D)+" ("+sld.toString()+") = "+Long.toHexString(sld.longValue()));
    SplitLong sanswer = sla.mul(slb).sub(slc.mul(sld));
    System.out.println("A*B - C*D = "+sanswer+" = "+sanswer.longValue());

  }

}

印刷品:

A*B - C*D = 9223372036854775807 = 7fffffffffffffff
A=7fffffffffffffff (7fffffff|ffffffff) = 7fffffffffffffff
10=10(0|a) * 15=15(0|f) = 150 (0|96)
B=7ffffffffffffffe (7fffffff|fffffffe) = 7ffffffffffffffe
C=7fffffffffffffff (7fffffff|ffffffff) = 7fffffffffffffff
D=7ffffffffffffffd (7fffffff|fffffffd) = 7ffffffffffffffd
A*B - C*D = 7fffffff|ffffffff = 9223372036854775807

在我看来,它正在工作。

我敢打赌,我错过了一些细微之处,例如看标志溢出等,但我认为本质就在这里。


1
我认为这是@Ofir建议的实现。
OldCurmudgeon 2012年

2

为了完整起见,由于没有人提到它,因此如今某些编译器(例如GCC)实际上为您提供了128位整数。

因此,一个简单的解决方案可能是:

(long long)((__int128)A * B - (__int128)C * D)

1

AB-CD = (AB-CD) * AC / AC = (B/C-D/A)*A*C。既B/C不能也不D/A可以溢出,因此(B/C-D/A)请先计算。由于最终结果不会根据您的定义溢出,因此您可以安全地执行剩余的乘法并计算(B/C-D/A)*A*C出所需的结果。

请注意,如果您的输入也可能非常小,则B/CD/A可能会溢出。如果可能,根据输入检查,可能需要更复杂的操作。


2
这将不起作用,因为整数除法会丢失信息(结果的分数)
Ofir 2012年

@Ofir是正确的,但是您不能吃蛋糕,也不能碰它。您必须通过精确度或使用其他资源来支付(如您在答案中建议的那样)。我的答案是数学性质的,而您的答案是面向计算机的。每种情况都可能是正确的,具体取决于情况。
SomeWittyUsername 2012年

2
您是正确的-我应该这样说-因为数学是正确的,所以不会给出确切的结果而不是不会起作用。但是,请注意,在可能使问题提交者感兴趣的情况下(例如,在问题示例中),该错误可能会出乎意料地大-比任何实际应用都可接受的大得多。无论如何-这是一个有见地的答案,我不应该使用该语言。
Ofir 2012年

@Ofir我认为您的语言不合适。OP明确要求进行“正确”的计算,而不是为了在极端的资源限制下执行而损失精度。
user4815162342

1

选择K = a big number(例如K = A - sqrt(A)

A*B - C*D = (A-K)*(B-K) - (C-K)*(D-K) + K*(A-C+B-D); // Avoid overflow.

为什么?

(A-K)*(B-K) = A*B - K*(A+B) + K^2
(C-K)*(D-K) = C*D - K*(C+D) + K^2

=>
(A-K)*(B-K) - (C-K)*(D-K) = A*B - K*(A+B) + K^2 - {C*D - K*(C+D) + K^2}
(A-K)*(B-K) - (C-K)*(D-K) = A*B - C*D - K*(A+B) + K*(C+D) + K^2 - K^2
(A-K)*(B-K) - (C-K)*(D-K) = A*B - C*D - K*(A+B-C-D)

=>
A*B - C*D = (A-K)*(B-K) - (C-K)*(D-K) + K*(A+B-C-D)

=>
A*B - C*D = (A-K)*(B-K) - (C-K)*(D-K) + K*(A-C+B-D)

请注意,因为A,B,C和D是大数,所以A-CB-D是小数。


您如何实际选择K?此外,K *(A-C + BD)可能仍然溢出。
ylc 2012年

@ylc:选择K = sqrt(A),不是A-C+B-D一个小数字。由于A,B,C和D为大数,因此AC为小数。
阿米尔·萨尼扬

如果选择K = sqrt(A),则(AK)*(BK)可能会再次溢出。
ylc 2012年

@ylc:好!我将其更改为A - sqrt(A):)
阿米尔·萨尼扬

然后,K *(A-C + BD)可能会溢出。
ylc 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.