我需要计算一个看起来像:的表达式
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-一些自然数。
A - C
可能会溢出。是要考虑的问题,还是您知道数据不会发生这种情况?
我需要计算一个看起来像:的表达式
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-一些自然数。
A - C
可能会溢出。是要考虑的问题,还是您知道数据不会发生这种情况?
Answers:
我想这似乎太琐碎了。但是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
C*D
A,B,C,D
负面结果怎么办?那会E
还是F
更大?
最简单,最通用的解决方案是使用不会溢出的表示形式,或者使用长整数库(例如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
请注意,这不是标准的,因为它依赖于环绕签名溢出。(GCC具有启用此功能的编译器标志。)
但是,如果您只进行所有计算long long
,则直接应用公式的结果:
(A * B - C * D)
只要正确的结果适合,结果将是准确的long long
。
这是一种变通方法,仅依赖于将无符号整数转换为有符号整数的实现定义的行为。但这可以预期在当今几乎所有系统上都有效。
(long long)((unsigned long long)A * B - (unsigned long long)C * D)
这会将输入转换unsigned long long
为标准保证溢出行为得到保证的地方。最后,将其强制转换回有符号整数是实现定义的部分,但将在当今几乎所有环境中运行。
如果您需要更多的学问解决方案,我认为您必须使用“长算术”
long long
。
这应该工作(我认为):
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)
如果结果适合长整型,则表达式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位(或者可能少一点)。
您可以将每个数字写到数组中,每个元素都是一个数字,然后将其作为多项式进行计算。取所得的多项式(一个数组),并通过将数组的每个元素乘以10到数组中位置的幂(第一个位置为最大,最后一个位置为零)来计算结果。
该数字123
可以表示为:
123 = 100 * 1 + 10 * 2 + 3
为此,您只需创建一个数组[1 2 3]
。
对所有数字A,B,C和D都执行此操作,然后将它们乘以多项式。获得了多项式后,就可以从中重新构造数字。
虽然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_10
到E_20
(移位32,然后加上,然后擦除的上半部分E_10
)。
现在,您可以E_11
通过将带有正确符号(从非进位部分获得)的进位添加到进位来摆脱进位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
成立,我们现在有
E_20=0
E_10=0
E_00=E
如果您知道最终结果可以用整数类型表示,则可以使用以下代码快速执行此计算。因为C标准指定无符号算术是模算术并且不会溢出,所以可以使用无符号类型来执行计算。
以下代码假定存在相同宽度的无符号类型,并且该带符号类型使用所有位模式表示值(无陷阱表示,带符号类型的最小值为无符号类型模量的一半的负数)。如果这在C实现中不成立,则可以为此对ConvertToSigned例程进行简单的调整。
以下使用signed char
和unsigned char
演示代码。对于您的实现,更改Signed
to typedef signed long long int Signed;
的定义和Unsigned
to 的定义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;
}
您可以尝试将方程式分解为一些较小的组件,这些组件不会溢出。
AB - CD
= [ A(B - N) - C( D - M )] + [AN - CM]
= ( AK - CJ ) + ( AN - CM)
where K = B - N
J = D - M
如果组件仍然溢出,则可以将它们递归分解为较小的组件,然后重新组合。
K
和J
,为什么不N
和M
。另外,我认为您正在将方程分解成更大的部分。由于您的第3步与OP的问题相同,除了更复杂(AK-CJ)
->(AB-CD)
我可能没有涵盖所有边缘情况,也没有进行严格的测试,但这实现了我记得在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
在我看来,它正在工作。
我敢打赌,我错过了一些细微之处,例如看标志溢出等,但我认为本质就在这里。
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/C
或D/A
可能会溢出。如果可能,根据输入检查,可能需要更复杂的操作。
选择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-C
和B-D
是小数。
A-C+B-D
一个小数字。由于A,B,C和D为大数,因此AC为小数。
A - sqrt(A)
:)