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

时间:2012-11-05 17:16:08

标签: c++ c overflow

我需要计算一个表达式,如下所示: 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 - 某个自然数。

15 个答案:

答案 0 :(得分: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所指出的那样,如果类型长整数为无符号,则可能需要在减法运算期间小心。


例如,对于问题中的案例,只需要一次迭代,

 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

答案 1 :(得分: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都不需要

答案 2 :(得分:46)

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

但是,如果您只是在long long中进行所有计算,直接应用公式的结果:只要正确的结果符合{{1},(A * B - C * D)就是准确的}}


这是一种解决方法,它只依赖于将无符号整数转换为有符号整数的实现定义行为。但是,预计今天几乎每个系统都可以使用它。

long long

这会将输入转换为(long long)((unsigned long long)A * B - (unsigned long long)C * D) ,其中溢出行为可以保证由标准环绕。最后回转到有符号整数是实现定义的部分,但是现在几乎可以在所有环境中使用。


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

答案 3 :(得分: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)

答案 4 :(得分:9)

您可以考虑计算所有值的最大公因子,然后在进行算术运算之前将它们除以该因子,然后再乘以。这假设存在这样一个因素(例如,如果ABCD碰巧是相对素数,那么它们就不会有共同点因子)。

同样,您可以考虑使用对数刻度,但这会有点可怕,但要符合数值精度。

答案 5 :(得分:9)

如果结果适合long long int,那么表达式A * B-C * D就可以了,因为它执行算术模2 ^ 64,并且会给出正确的结果。问题是要知道结果是否适合长long int。要检测到这一点,您可以使用以下技巧使用双打:

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

这种方法的问题在于你受到双精度尾数(54位?)的精度限制,所以你需要将乘积A * B和C * D限制在63 + 54位(或者可能是一点点)减)。

答案 6 :(得分: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 :(得分:7)

您可以在数组中编写每个数字,每个元素都是一个数字,并按polynomials进行计算。获取生成的多项式(数组),并通过将数组的每个元素乘以10来计算结果,计算结果中位置的幂(第一个位置最大,最后一个为零)。

数字123可表示为:

123 = 100 * 1 + 10 * 2 + 3

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

对所有数字A,B,C和D执行此操作,然后将它们乘以多项式。得到多项式后,只需从中重构数字。

答案 8 :(得分:6)

虽然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_iCD_i进行减法,每次使用一个额外的进位(精确地为1位整数)。因此,如果我们说E = A * B-C * 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_10的上半部分转移到E_20(移位32并添加,然后删除E_10的上半部分)。

现在你可以通过将正号(从非进位部分获得)添加到E_11来摆脱进位[{1}}。如果这会触发溢出,则结果也不适合。

E_20现在有足够的“空间”从E_10(移位,添加,删除)和进位位E_00获取上半部分。

E_01现在可能会再次变大,所以我们重复转移到E_10

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

最后一步是将E_10的下半部分再次转移到E_20

如果期望E_10适合E=A*B+C*D成立,我们现在有

signed long long int

答案 9 :(得分:3)

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

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

以下使用signed charunsigned char来演示代码。对于您的实施,请将Signed的定义更改为typedef signed long long int Signed;,将Unsigned的定义更改为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;
}

答案 10 :(得分:2)

您可以尝试将等式分解为不会溢出的较小组件。

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

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

    where K = B - N
          J = D - M

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

答案 11 :(得分: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

在我看来它正在发挥作用。

我敢打赌,我错过了一些微妙之处,例如观看标志溢出等等,但我认为其实质是存在的。

答案 12 :(得分:2)

为了完整起见,由于没有人提到它,一些编译器(例如GCC)现在实际上为你提供了128位整数。

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

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

答案 13 :(得分:1)

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

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

答案 14 :(得分: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是小数字。