Làm thế nào để tránh tràn trong expr. A B C D


161

Tôi cần tính toán một biểu thức trông giống như : A*B - C*D, trong đó các kiểu của chúng là: signed long long int A, B, C, D; Mỗi số có thể thực sự lớn (không vượt quá kiểu của nó). Trong khi A*Bcó thể gây ra tràn, biểu hiện đồng thời A*B - C*Dcó thể rất nhỏ. Làm thế nào tôi có thể tính toán chính xác?

Ví dụ : MAX * MAX - (MAX - 1) * (MAX + 1) == 1, where MAX = LLONG_MAX - nvà n - một số số tự nhiên.


17
Độ chính xác quan trọng như thế nào?
Anirudh Ramanathan

1
@Cthulhu, câu hỏi tuyệt vời. Anh ta có thể cố gắng tạo một hàm tương đương bằng cách sử dụng số nhỏ hơn bằng cách chia tất cả chúng cho 10 hoặc một cái gì đó, sau đó nhân kết quả.
Chris

4
Vars A, B, C, D được ký. Điều này ngụ ý A - Ccó thể tràn. Đó là một vấn đề cần xem xét hoặc bạn có biết rằng điều này sẽ không xảy ra với dữ liệu của bạn?
William Morris

2
@MooingDuck nhưng bạn có thể kiểm tra trước nếu hoạt động sẽ tràn stackoverflow.com/a/3224630/158285
bradgonesurfing

1
@Chris: Không, tôi đang nói rằng không có cách di động nào để kiểm tra xem có xảy ra tràn tràn đã ký hay không. (Brad là chính xác mà bạn có thể phát hiện ra rằng nó sẽ xảy ra). Sử dụng lắp ráp nội tuyến là một trong nhiều cách không di động để kiểm tra.
Vịt Mooing

Câu trả lời:


120

Điều này có vẻ quá tầm thường tôi đoán. Nhưng A*Blà một trong những có thể tràn.

Bạn có thể làm như sau, mà không mất độ chính xác

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)

Sự phân hủy này có thể được thực hiện thêm .
Như @Gian đã chỉ ra, có thể cần phải cẩn thận trong quá trình trừ nếu loại không được ký dài.


Ví dụ, với trường hợp bạn có trong câu hỏi, chỉ cần một lần lặp,

 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, chỉ cần áp dụng thuật toán tương tự choC*D
Chris

2
Tôi nghĩ bạn nên giải thích những gì E đại diện.
Caleb

7
Cả dài và gấp đôi là 64 bit. Vì double phải phân bổ một số bit cho số mũ, nên nó có phạm vi giá trị nhỏ hơn có thể có mà không mất độ chính xác.
Jim Garrison

3
@Cthulhu - đối với tôi như thế này sẽ chỉ hoạt động nếu tất cả số lượng rất lớn ... ví dụ: bạn vẫn sẽ bị tràn với {A, B, C, D} = {MAX, MAX, MAX, 2}. OP nói "Mỗi số có thể thực sự lớn", nhưng không rõ ràng từ tuyên bố vấn đề rằng mỗi số phải thực sự lớn.
Kevin K

4
Điều gì xảy ra nếu bất kỳ trong số đó A,B,C,Dlà tiêu cực? Sẽ không Ehay Fthậm chí còn lớn hơn?
Supr

68

Giải pháp đơn giản và tổng quát nhất là sử dụng một biểu diễn không thể tràn, bằng cách sử dụng thư viện số nguyên dài (ví dụ: http://gmplib.org/ ) hoặc biểu diễn bằng cách sử dụng một cấu trúc hoặc mảng và thực hiện một kiểu nhân dài ( tức là tách mỗi số thành hai nửa 32 bit và thực hiện phép nhân như sau:

(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

Giả sử kết quả cuối cùng phù hợp với 64 bit, bạn thực sự không cần hầu hết các bit của R3 và không có R4 nào


8
Tính toán ở trên thực sự không phức tạp như vẻ ngoài của nó, nó thực sự là phép nhân dài đơn giản trong cơ sở 2 ^ 32, và mã trong C sẽ trông đơn giản hơn. Ngoài ra, sẽ là một ý tưởng tốt để tạo các hàm chung để thực hiện công việc này trong chương trình của bạn.
Ofir

46

Lưu ý rằng đây không phải là tiêu chuẩn vì nó phụ thuộc vào dòng chảy tràn đã ký. (GCC có các cờ biên dịch cho phép điều này.)

Nhưng nếu bạn chỉ thực hiện tất cả các phép tính trong long long, kết quả của việc áp dụng công thức trực tiếp:
(A * B - C * D)sẽ chính xác miễn là kết quả đúng phù hợp với a long long.


Đây là một cách giải quyết chỉ dựa vào hành vi được xác định theo thực thi của việc truyền số nguyên không dấu thành số nguyên đã ký. Nhưng điều này có thể được dự kiến ​​sẽ hoạt động trên hầu hết các hệ thống ngày nay.

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

Điều này ép đầu vào unsigned long long nơi mà hành vi tràn được đảm bảo được bao bọc bởi tiêu chuẩn. Truyền lại cho một số nguyên đã ký ở cuối là phần do xác định thực hiện, nhưng sẽ hoạt động trên hầu hết các môi trường hiện nay.


Nếu bạn cần thêm giải pháp mô phạm, tôi nghĩ bạn phải sử dụng "số học dài"


+1 Bạn là người duy nhất nhận thấy điều này. Phần khó khăn duy nhất là thiết lập trình biên dịch để thực hiện tràn xung quanh và kiểm tra xem kết quả chính xác có thực sự phù hợp với a hay không long long.
Bí ẩn

2
Ngay cả phiên bản ngây thơ mà không có bất kỳ thủ thuật nào cũng sẽ làm điều đúng đắn trên hầu hết các triển khai; nó không được đảm bảo theo tiêu chuẩn, nhưng bạn sẽ phải tìm một máy bổ sung 1 hoặc một số thiết bị khá kỳ lạ khác để làm cho nó thất bại.
hobbs

1
Tôi nghĩ rằng đây là một câu trả lời quan trọng. Tôi đồng ý rằng nó có thể không phải là lập trình chính xác để đảm nhận hành vi cụ thể thực hiện, nhưng mọi kỹ sư nên hiểu số học modulo và làm thế nào để có được các cờ biên dịch đúng để đảm bảo hành vi nhất quán nếu hiệu suất là cần thiết. Các kỹ sư DSP dựa trên hành vi này để triển khai bộ lọc điểm cố định, trong đó câu trả lời được chấp nhận sẽ có hiệu suất không được chấp nhận.
Peter M

18

Điều này sẽ làm việc (tôi nghĩ):

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);

Đây là nguồn gốc của tôi:

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
Cảm ơn @bradgonesurfing - bạn có thể cung cấp đầu vào như vậy không? Tôi đã cập nhật câu trả lời của mình, thực hiện nó và nó hoạt động (bd và ca là 0) ...
paquetp

1
Hừm. Bây giờ tôi nghĩ về nó có thể không. Trường hợp thoái hóa với d = 1 và a = 1 và b = maxint và c = maxint nó vẫn hoạt động.
Thật

1
@paquetp: a = 1, b = 0x7fffffffffffffff, c = -0x7fffffffffffffff, d = 1 (lưu ý c là âm). Mặc dù vậy, tôi chắc chắn rằng mã của bạn xử lý tất cả các số dương một cách chính xác.
Vịt Mooing

3
@MooingDuck nhưng câu trả lời cuối cùng cho bộ của bạn cũng bị tràn vì vậy nó không phải là một thiết lập hợp lệ. Nó chỉ hoạt động nếu mỗi bên có cùng dấu nên phép trừ kết quả nằm trong phạm vi.
bradgonesurfing

1
Có một điều kỳ lạ với StackOverflow khi câu trả lời này là đơn giản nhất và tốt nhất có điểm thấp như vậy so với câu trả lời được ghi điểm cao nhất.
bradgonesurfing

9

Bạn có thể xem xét tính toán một yếu tố chung lớn nhất cho tất cả các giá trị của mình và sau đó chia chúng cho yếu tố đó trước khi thực hiện các phép toán số học của bạn, sau đó nhân lại. Điều này giả định rằng một yếu tố như vậy tồn tại, tuy nhiên (ví dụ, nếu A, B, CDxảy ra được nguyên tố cùng nhau, họ sẽ không có một yếu tố phổ biến).

Tương tự như vậy, bạn có thể xem xét làm việc trên thang đo log, nhưng điều này sẽ hơi đáng sợ, tùy thuộc vào độ chính xác của số.


1
Logarit có vẻ tốt nếu long doublecó sẵn. Trong trường hợp đó, mức độ chính xác có thể chấp nhận có thể đạt được (và kết quả được làm tròn).

9

Nếu kết quả khớp với một int dài dài thì biểu thức A * BC * D vẫn ổn vì nó thực hiện mod số học 2 ^ 64 và sẽ cho kết quả chính xác. Vấn đề là để biết nếu kết quả phù hợp trong một int dài dài. Để phát hiện điều này, bạn có thể sử dụng thủ thuật sau bằng cách sử dụng nhân đôi:

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

Vấn đề với cách tiếp cận này là bạn bị giới hạn bởi độ chính xác của lớp phủ đôi (54 bit?), Vì vậy bạn cần giới hạn các sản phẩm A * B và C * D xuống còn 63 + 54 bit (hoặc có thể ít hơn một chút).


Đây là ví dụ thực tế nhất. Xóa và đưa ra câu trả lời đúng (hoặc ném Ngoại lệ khi đầu vào không tốt).
Đánh dấu Lakata

1
Đẹp và thanh lịch! Bạn đã không rơi vào cái bẫy mà những người khác đã yêu. Chỉ một điều nữa: tôi cá là có một số ví dụ trong đó phép tính kép nằm dưới MAX_LLONG chỉ do lỗi làm tròn. Bản năng toán học của tôi nói với tôi rằng bạn nên tính toán sự khác biệt của kết quả kép và kết quả dài thay vào đó, và so sánh nó với MAX_LLONG / 2 hoặc một cái gì đó. Sự khác biệt này là các lỗi làm tròn của phép tính kép và cộng với tràn và thường sẽ tương đối thấp, nhưng trong trường hợp tôi đã đề cập, nó sẽ lớn. Nhưng ngay bây giờ tôi quá lười để tìm hiểu cho chắc chắn. :-)
Hans-Peter Störr

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

sau đó

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

7

Bạn có thể viết mỗi số trong một mảng, mỗi phần tử là một chữ số và thực hiện các phép tính dưới dạng đa thức . Lấy đa thức kết quả, là một mảng và tính kết quả bằng cách nhân từng phần tử của mảng với 10 với công suất của vị trí trong mảng (vị trí đầu tiên là lớn nhất và cuối cùng là 0).

Số 123có thể được thể hiện là:

123 = 100 * 1 + 10 * 2 + 3

mà bạn chỉ cần tạo một mảng [1 2 3].

Bạn làm điều này cho tất cả các số A, B, C và D, và sau đó bạn nhân chúng thành đa thức. Một khi bạn có đa thức kết quả, bạn chỉ cần xây dựng lại số từ nó.


2
không biết đó là gì nhưng tôi sẽ phải tìm. đặt :) . đây là một giải pháp trên đỉnh đầu của tôi trong khi tôi đang mua sắm với bạn gái của mình :)
Mihai

bạn đang thực hiện bignums trong một mảng base10. GMP là một thư viện bignum chất lượng sử dụng cơ sở 4294967296. NHIỀU nhanh hơn. Mặc dù không có downvote, vì câu trả lời là chính xác và hữu ích.
Vịt Mooing

cảm ơn :) . Thật hữu ích khi biết rằng đây là một cách làm nhưng có những cách tốt hơn vì vậy đừng làm như thế này. ít nhất là không trong tình huống này :)
Mihai

dù sao đi nữa ... bằng cách sử dụng giải pháp này, bạn có thể sử dụng số máy tính lớn hơn nhiều so với bất kỳ loại nguyên thủy nào có thể in đậm (như số 100 chữ số) và giữ kết quả dưới dạng một mảng. điều này xứng đáng được bình chọn: p
Mihai

Tôi không chắc chắn nó có được một upvote, vì phương pháp này (mặc dù hiệu quả và tương đối dễ hiểu) là bộ nhớ đói và chậm.
Vịt Mooing

6

Trong khi một người signed long long intsẽ không giữ A*B, hai trong số họ sẽ. Vì vậy, A*Bcó thể được phân tách theo các thuật ngữ cây theo số mũ khác nhau, bất kỳ trong số chúng phù hợp với một 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;

Tương tự cho C*D.

Theo cách thẳng, phép trừ có thể được thực hiện cho mọi cặp AB_iCD_itương tự, bằng cách sử dụng một bit mang bổ sung (chính xác là số nguyên 1 bit) cho mỗi cặp. Vì vậy, nếu chúng ta nói E = A * BC * D, bạn sẽ nhận được một cái gì đó như:

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 
...

Chúng tôi tiếp tục bằng cách chuyển phía trên một nửa số E_10để E_20(shift 32 và thêm, sau đó xóa nửa trên của E_10).

Bây giờ bạn có thể loại bỏ bit carry E_11bằng cách thêm nó với dấu bên phải (thu được từ phần không mang theo) vào E_20. Nếu điều này gây ra tràn, kết quả sẽ không phù hợp.

E_10bây giờ có đủ 'không gian' để lấy nửa trên E_00 (thay đổi, thêm, xóa) và bit mang E_01.

E_10bây giờ có thể lớn hơn một lần nữa, vì vậy chúng tôi lặp lại việc chuyển sang E_20.

Tại thời điểm này, E_20phải trở thành số không, nếu không kết quả sẽ không phù hợp. Nửa trên E_10là trống do kết quả của quá trình chuyển.

Bước cuối cùng là chuyển nửa dưới E_20vào E_10một lần nữa.

Nếu kỳ vọng E=A*B+C*Dsẽ phù hợp với các khoản signed long long intgiữ, bây giờ chúng ta có

E_20=0
E_10=0
E_00=E

1
Đây thực sự là công thức đơn giản hóa mà người ta sẽ nhận được nếu sử dụng công thức nhân của Ofir và loại bỏ mọi kết quả tạm thời vô dụng.
dronus

3

Nếu bạn biết kết quả cuối cùng có thể biểu thị theo kiểu số nguyên của mình, bạn có thể thực hiện phép tính này một cách nhanh chóng bằng mã bên dưới. Vì tiêu chuẩn C chỉ định rằng số học không dấu là số học modulo và không tràn, nên bạn có thể sử dụng loại không dấu để thực hiện phép tính.

Đoạn mã sau giả định có một loại không dấu có cùng chiều rộng và loại đã ký sử dụng tất cả các mẫu bit để biểu diễn các giá trị (không có biểu diễn bẫy, mức tối thiểu của loại đã ký là âm của một nửa mô đun của loại không dấu). Nếu điều này không có trong triển khai C, có thể thực hiện các điều chỉnh đơn giản cho thói quen ConvertToSign cho điều đó.

Việc sử dụng sau đây signed charunsigned charđể chứng minh mã. Đối với thực hiện của bạn, thay đổi định nghĩa của Signedđể typedef signed long long int Signed;và định nghĩa của 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;
}

2

Bạn có thể thử phá phương trình thành các thành phần nhỏ hơn mà không tràn.

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

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

    where K = B - N
          J = D - M

Nếu các thành phần vẫn tràn, bạn có thể chia chúng thành các thành phần nhỏ hơn theo cách đệ quy và sau đó kết hợp lại.


Điều này có thể đúng hoặc không chính xác, nhưng chắc chắn là khó hiểu. Bạn xác định KJ, tại sao không NM. Ngoài ra, tôi nghĩ rằng bạn đang phá vỡ phương trình thành các phần lớn hơn . Vì bước 3 của bạn giống với câu hỏi của OP, ngoại trừ phức tạp hơn (AK-CJ)->(AB-CD)
Vịt Mooing

N không được đơn giản hóa từ bất cứ điều gì. Nó chỉ là một số được trừ từ A để làm cho nó nhỏ hơn. Trên thực tế nó là một giải pháp tương tự nhưng kém hơn so với paquetp. Ở đây tôi đang sử dụng phép trừ thay vì chia số nguyên để làm cho nó nhỏ hơn.
bradgonesurfing

2

Tôi có thể không bao gồm tất cả các trường hợp cạnh, tôi cũng chưa kiểm tra nghiêm ngặt điều này nhưng điều này thực hiện một kỹ thuật tôi nhớ sử dụng trong thập niên 80 khi thử làm toán số nguyên 32 bit trên cpu 16 bit. Về cơ bản, bạn chia 32 bit thành hai đơn vị 16 bit và làm việc riêng với chúng.

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());

  }

}

Bản in:

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

có vẻ như tôi đang làm việc

Tôi cá là tôi đã bỏ lỡ một số sự tinh tế như xem dấu hiệu tràn, v.v. nhưng tôi nghĩ bản chất là ở đó.


1
Tôi nghĩ rằng đây là một triển khai của những gì @Ofir đề xuất.
OldCurmudgeon

2

Để hoàn thiện, vì không ai đề cập đến nó, một số trình biên dịch (ví dụ GCC) thực sự cung cấp cho bạn một số nguyên 128 bit hiện nay.

Do đó, một giải pháp dễ dàng có thể là:

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

1

AB-CD = (AB-CD) * AC / AC = (B/C-D/A)*A*C. Không thể B/Ccũng không D/Athể tràn, vì vậy hãy tính toán (B/C-D/A)trước. Vì kết quả cuối cùng sẽ không tràn theo định nghĩa của bạn, bạn có thể thực hiện các phép nhân còn lại và tính toán một cách an toàn(B/C-D/A)*A*C đó là kết quả bắt buộc.

Lưu ý, nếu đầu vào của bạn cũng có thể cực kỳ nhỏ , B/Choặc D/Acó thể tràn. Nếu có thể, các thao tác phức tạp hơn có thể được yêu cầu theo kiểm tra đầu vào.


2
Điều đó sẽ không hoạt động khi phân chia số nguyên mất thông tin (phần nhỏ của kết quả)
Ofir

@Ofir là chính xác, tuy nhiên bạn không thể ăn bánh và để nó không bị ảnh hưởng. Bạn phải trả bằng chính xác hoặc bằng cách sử dụng các tài nguyên bổ sung (như bạn đã đề xuất trong câu trả lời của mình). Câu trả lời của tôi là về bản chất toán học trong khi của bạn là định hướng máy tính. Mỗi có thể đúng tùy thuộc vào hoàn cảnh.
SomeWittyUsername

2
Bạn đã đúng - tôi nên nói theo cách đó - sẽ không cho kết quả chính xác thay vì không hoạt động, vì toán học là chính xác. Tuy nhiên, lưu ý trong các trường hợp có khả năng khiến người gửi câu hỏi quan tâm (ví dụ trong ví dụ trong câu hỏi), lỗi có thể sẽ lớn đến mức đáng ngạc nhiên - lớn hơn nhiều so với bất kỳ ứng dụng thực tế nào. Trong mọi trường hợp - đó là một câu trả lời sâu sắc và tôi không nên sử dụng ngôn ngữ đó.
Ofir

@Ofir Tôi không nghĩ ngôn ngữ của bạn không phù hợp. OP rõ ràng đã yêu cầu một phép tính "chính xác", không phải là một phép tính sẽ mất độ chính xác vì lợi ích của việc được thực hiện dưới các ràng buộc tài nguyên cực đoan.
dùng4815162342

1

Chọn K = a big number(ví dụ. K = A - sqrt(A))

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

Tại sao?

(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)

Lưu ý rằng vì A, B, C và D là số lớn, do đó A-CB-Dlà số nhỏ.


Làm thế nào để bạn chọn K trong thực tế? Ngoài ra, K * (A - C + BD) vẫn có thể bị tràn.
ylc

@ylc: Chọn K = sqrt (A), Không phải A-C+B-Dlà một con số nhỏ. Vì A, B, C và D là số lớn nên AC là số nhỏ.
Amir Saniyan

Nếu bạn chọn K = sqrt (A) , thì (AK) * (BK) có thể tràn lại.
ylc

@ylc: OK! Tôi đổi nó thành A - sqrt(A):)
Amir Saniyan

Sau đó K * (A-C + BD) có thể tràn.
ylc
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.