Giải thích số học chính xác tùy ý


92

Tôi đang cố gắng học C và không thể làm việc với các số THỰC SỰ lớn (ví dụ: 100 chữ số, 1000 chữ số, v.v.). Tôi biết rằng có các thư viện để thực hiện việc này, nhưng tôi muốn tự mình thực hiện nó.

Tôi chỉ muốn biết liệu có ai có hoặc có thể cung cấp một lời giải thích rất chi tiết, sâu sắc về số học có độ chính xác tùy ý hay không.

Câu trả lời:


162

Tất cả đều là vấn đề của việc lưu trữ đầy đủ và các thuật toán để coi các con số như những phần nhỏ hơn. Giả sử bạn có một trình biên dịch trong đó an intchỉ có thể từ 0 đến 99 và bạn muốn xử lý các số lên đến 999999 (chúng tôi sẽ chỉ lo lắng về các số dương ở đây để giữ cho nó đơn giản).

Bạn thực hiện điều đó bằng cách cho mỗi số ba ints và sử dụng các quy tắc tương tự mà bạn (lẽ ra) đã học ở trường tiểu học để cộng, trừ và các phép toán cơ bản khác.

Trong một thư viện độ chính xác tùy ý, không có giới hạn cố định về số lượng kiểu cơ sở được sử dụng để biểu thị các số của chúng ta, chỉ cần bất kỳ bộ nhớ nào có thể chứa.

Bổ sung cho ví dụ 123456 + 78::

12 34 56
      78
-- -- --
12 35 34

Làm việc từ đầu cuối ít quan trọng nhất:

  • mang ban đầu = 0.
  • 56 + 78 + 0 carry = 134 = 34 với 1 carry
  • 34 + 00 + 1 mang = 35 = 35 với 0 mang
  • 12 + 00 + 0 mang = 12 = 12 với 0 mang

Trên thực tế, đây là cách bổ sung thường hoạt động ở mức bit bên trong CPU của bạn.

Phép trừ tương tự (sử dụng phép trừ kiểu cơ số và mượn thay vì mang theo), phép nhân có thể được thực hiện với các phép cộng lặp lại (rất chậm) hoặc tích chéo (nhanh hơn) và phép chia phức tạp hơn nhưng có thể được thực hiện bằng cách chuyển và trừ các số liên quan (sự phân chia dài mà bạn đã học khi còn nhỏ).

Tôi thực sự đã viết các thư viện để thực hiện loại công việc này bằng cách sử dụng lũy ​​thừa tối đa của mười có thể vừa với một số nguyên khi được bình phương (để ngăn tràn khi nhân hai ints với nhau, chẳng hạn như 16 bit intđược giới hạn từ 0 đến 99 để tạo ra 9,801 (<32,768) khi bình phương hoặc 32-bit intsử dụng từ 0 đến 9,999 để tạo ra 99,980,001 (<2,147,483,648)), điều này đã làm giảm đáng kể các thuật toán.

Một số thủ thuật cần chú ý.

1 / Khi cộng hoặc nhân các số, hãy phân bổ trước khoảng trống tối đa cần thiết rồi giảm sau nếu bạn thấy quá nhiều. Ví dụ, thêm hai số 100- "chữ số" (trong đó chữ số là một int) sẽ không bao giờ cung cấp cho bạn nhiều hơn 101 chữ số. Nhân một số có 12 chữ số với một số có 3 chữ số sẽ không bao giờ tạo ra nhiều hơn 15 chữ số (cộng các chữ số).

2 / Để tăng tốc độ, hãy bình thường hóa (giảm dung lượng lưu trữ cần thiết cho) các số chỉ khi thực sự cần thiết - thư viện của tôi có điều này như một lệnh gọi riêng để người dùng có thể quyết định giữa các mối quan tâm về tốc độ và lưu trữ.

3 / Phép cộng một số dương là phép trừ, và trừ một số âm cũng giống như phép cộng với số dương tương đương. Bạn có thể tiết kiệm khá nhiều mã bằng cách để các phương pháp cộng và trừ gọi nhau sau khi điều chỉnh các dấu hiệu.

4 / Tránh trừ các số lớn với số nhỏ vì bạn luôn kết thúc bằng các số như:

         10
         11-
-- -- -- --
99 99 99 99 (and you still have a borrow).

Thay vào đó, hãy trừ 10 cho 11, sau đó phủ định nó:

11
10-
--
 1 (then negate to get -1).

Đây là những bình luận (được chuyển thành văn bản) từ một trong những thư viện mà tôi phải làm việc này. Rất tiếc, bản thân mã đã có bản quyền, nhưng bạn có thể chọn đủ thông tin để xử lý bốn thao tác cơ bản. Giả sử trong điều sau đây -a-bđại diện cho các số âm và ablà số 0 hoặc số dương.

Đối Ngoài ra , nếu có dấu hiệu là khác nhau, sử dụng phép trừ của phủ định:

-a +  b becomes b - a
 a + -b becomes a - b

Đối với phép trừ , nếu các dấu hiệu khác nhau, hãy sử dụng phép cộng phủ định:

 a - -b becomes   a + b
-a -  b becomes -(a + b)

Ngoài ra, xử lý đặc biệt để đảm bảo chúng tôi đang trừ số nhỏ cho số lớn:

small - big becomes -(big - small)

Phép nhân sử dụng toán học cấp đầu vào như sau:

475(a) x 32(b) = 475 x (30 + 2)
               = 475 x 30 + 475 x 2
               = 4750 x 3 + 475 x 2
               = 4750 + 4750 + 4750 + 475 + 475

Cách thức đạt được điều này bao gồm việc trích xuất từng chữ số trong số 32 một (ngược) sau đó sử dụng phép cộng để tính giá trị được thêm vào kết quả (ban đầu là số 0).

ShiftLeftvà các ShiftRightphép toán được sử dụng để nhanh chóng nhân hoặc chia a LongIntvới giá trị bọc (10 cho phép toán "thực"). Trong ví dụ trên, chúng ta cộng 475 với 0 2 lần (chữ số cuối cùng của 32) để được 950 (kết quả = 0 + 950 = 950).

Sau đó, chúng ta chuyển sang trái 475 để được 4750 và chuyển sang phải 32 để được 3. Cộng 4750 với 0 3 lần để được 14250 sau đó thêm vào kết quả của 950 để được 15200.

Dịch trái 4750 để có 47500, dịch phải 3 để nhận 0. Vì dịch sang phải 32 bây giờ bằng 0, chúng ta đã hoàn thành và trên thực tế 475 x 32 bằng 15200.

Phép chia cũng phức tạp nhưng dựa trên số học ban đầu (phương pháp "gazinta" cho "đi vào"). Hãy xem xét phép chia dài sau đây cho 12345 / 27:

       457
   +-------
27 | 12345    27 is larger than 1 or 12 so we first use 123.
     108      27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
     ---
      154     Bring down 4.
      135     27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
      ---
       195    Bring down 5.
       189    27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
       ---
         6    Nothing more to bring down, so stop.

Do đó 12345 / 27457với phần dư 6. Kiểm chứng:

  457 x 27 + 6
= 12339    + 6
= 12345

Điều này được thực hiện bằng cách sử dụng một biến kéo xuống (ban đầu là 0) để giảm từng phân đoạn của 12345 cho đến khi nó lớn hơn hoặc bằng 27.

Sau đó, chúng tôi chỉ cần trừ 27 từ đó cho đến khi chúng tôi nhận được dưới 27 - số lượng trừ là phân đoạn được thêm vào dòng trên cùng.

Khi không còn phân đoạn nào để gỡ xuống, chúng ta đã có kết quả của mình.


Hãy nhớ rằng đây là những thuật toán khá cơ bản. Có nhiều cách tốt hơn để thực hiện số học phức tạp nếu số của bạn đặc biệt lớn. Bạn có thể xem xét một thứ gì đó như Thư viện số học chính xác GNU - nó tốt hơn và nhanh hơn đáng kể so với các thư viện của riêng tôi.

Nó có một điểm sai khá đáng tiếc ở chỗ nó sẽ đơn giản thoát ra nếu hết bộ nhớ (theo ý kiến ​​của tôi là một lỗ hổng khá nghiêm trọng đối với một thư viện mục đích chung) nhưng, nếu bạn có thể xem qua, nó khá tốt về những gì nó làm.

Nếu bạn không thể sử dụng nó vì lý do cấp phép (hoặc vì bạn không muốn ứng dụng của mình thoát ra mà không có lý do rõ ràng), ít nhất bạn có thể lấy các thuật toán từ đó để tích hợp vào mã của riêng bạn.

Tôi cũng nhận thấy rằng những người tham gia MPIR (một nhánh của GMP) dễ dàng hơn trong việc thảo luận về những thay đổi tiềm năng - họ có vẻ là một nhóm thân thiện với nhà phát triển hơn.


14
Tôi nghĩ rằng bạn đã đề cập đến "Tôi chỉ muốn biết liệu có ai có hoặc có thể cung cấp một lời giải thích rất chi tiết, sâu sắc về số học có độ chính xác tùy ý" RẤT tốt
Grant Peters

Một câu hỏi tiếp theo: Có thể thiết lập / phát hiện mang và tràn mà không cần truy cập vào mã máy không?
SasQ

8

Trong khi việc phát minh lại bánh xe là cực kỳ tốt cho việc học tập và xây dựng cá nhân của bạn, nó cũng là một nhiệm vụ cực kỳ lớn. Tôi không muốn khuyên bạn vì đây là một bài tập quan trọng và là bài tập mà tôi đã tự làm, nhưng bạn nên biết rằng có những vấn đề phức tạp và tinh tế trong công việc mà các gói lớn hơn phải giải quyết.

Ví dụ, phép nhân. Một cách ngây thơ, bạn có thể nghĩ đến phương pháp 'học sinh', tức là viết một số lên trên số kia, sau đó thực hiện phép nhân dài như bạn đã học ở trường. thí dụ:

      123
    x  34
    -----
      492
+    3690
---------
     4182

nhưng phương pháp này rất chậm (O (n ^ 2), n là số chữ số). Thay vào đó, các gói bignum hiện đại sử dụng một phép biến đổi Fourier rời rạc hoặc một phép biến đổi Số để biến điều này thành một phép toán O (n ln (n)) cơ bản.

Và điều này chỉ dành cho số nguyên. Khi bạn tham gia vào các hàm phức tạp hơn trên một số kiểu biểu diễn số thực (log, sqrt, exp, v.v.) mọi thứ thậm chí còn phức tạp hơn.

Nếu bạn muốn có một số nền tảng lý thuyết, tôi thực sự khuyên bạn nên đọc chương đầu tiên của cuốn sách của Yap, "Các vấn đề cơ bản của đại số thuật toán" . Như đã đề cập, thư viện gmp bignum là một thư viện tuyệt vời. Đối với số thực, tôi đã sử dụng mpfr và thích nó.


1
Tôi quan tâm đến phần "sử dụng phép biến đổi Fourier rời rạc hoặc phép biến đổi Số để biến điều này thành phép toán O (n ln (n)) về cơ bản" - nó hoạt động như thế nào? Chỉ cần một tài liệu tham khảo là được :)
detly

1
@detly: phép nhân đa thức cũng giống như phép tích chập, nên dễ dàng tìm thấy thông tin bằng cách sử dụng FFT để thực hiện phép tích chập nhanh. Bất kỳ hệ thống số nào cũng là một đa thức, trong đó các chữ số là hệ số và cơ số là cơ số. Tất nhiên, bạn sẽ cần phải cẩn thận mang theo để tránh vượt quá phạm vi chữ số.
Ben Voigt

6

Đừng phát minh lại bánh xe: nó có thể trở thành hình vuông!

Sử dụng thư viện của bên thứ ba, chẳng hạn như GNU MP , được dùng thử và kiểm tra.


4
Tôi nếu bạn muốn học C, tôi sẽ đặt tầm nhìn của bạn thấp hơn một chút. Thực hiện một thư viện bignum là không tầm thường cho tất cả các loại lý do tế nhị mà sẽ đi lên một người học
Mitch Wheat

3
Thư viện của bên thứ 3: đã đồng ý, nhưng GMP có các vấn đề về cấp phép (LGPL, mặc dù nó hoạt động hiệu quả như GPL vì rất khó để thực hiện phép toán hiệu suất cao thông qua giao diện tương thích với LGPL).
Jason S

Tham khảo Nice Futurama (có chủ ý?)
Grant Peters

7
GNU MP kêu gọi vô điều kiện các abort()lỗi phân bổ, điều này nhất định xảy ra với một số tính toán cực kỳ lớn. Đây là hành vi không thể chấp nhận được đối với một thư viện và đủ lý do để viết mã có độ chính xác tùy ý của riêng bạn.
R .. GitHub DỪNG TRỢ GIÚP LÚC NỮA,

Tôi phải đồng ý với R ở đó. Một thư viện mục đích chung chỉ đơn giản là kéo tấm thảm ra khỏi chương trình của bạn khi bộ nhớ sắp hết là điều không thể tha thứ. Tôi thà rằng họ hy sinh một số tốc độ cho an toàn / khả năng phục hồi.
paxdiablo

4

Bạn làm điều đó về cơ bản giống như cách bạn làm với bút chì và giấy ...

  • Số phải được biểu diễn trong một bộ đệm (mảng) có thể có kích thước tùy ý (có nghĩa là sử dụng mallocrealloc) khi cần
  • bạn triển khai số học cơ bản nhiều nhất có thể bằng cách sử dụng các cấu trúc được ngôn ngữ hỗ trợ và xử lý việc mang và di chuyển điểm cơ số theo cách thủ công
  • bạn tìm kiếm các văn bản phân tích số để tìm các đối số hiệu quả để xử lý bằng hàm phức tạp hơn
  • bạn chỉ triển khai càng nhiều càng tốt.

Thông thường, bạn sẽ sử dụng đơn vị tính toán cơ bản của mình

  • byte chứa 0-99 hoặc 0-255
  • Các từ 16 bit có nghĩa là khô héo 0-9999 hoặc 0--65536
  • 32 từ bit chứa ...
  • ...

theo quy định của kiến ​​trúc của bạn.

Việc lựa chọn cơ số nhị phân hoặc cơ số thập phân tùy thuộc vào bạn mong muốn về hiệu quả không gian tối đa, khả năng đọc của con người và sự hiện diện của việc không hỗ trợ toán học thập phân mã hóa nhị phân (BCD) trên chip của bạn.


3

Bạn có thể làm điều đó với trình độ toán trung học. Mặc dù các thuật toán nâng cao hơn được sử dụng trong thực tế. Vì vậy, ví dụ để thêm hai số 1024 byte:

unsigned char first[1024], second[1024], result[1025];
unsigned char carry = 0;
unsigned int  sum   = 0;

for(size_t i = 0; i < 1024; i++)
{
    sum = first[i] + second[i] + carry;
    carry = sum - 255;
}

kết quả sẽ phải lớn hơn one placetrong trường hợp cộng để quan tâm đến các giá trị lớn nhất. Nhìn vào cái này:

9
   +
9
----
18

TTMath là một thư viện tuyệt vời nếu bạn muốn tìm hiểu. Nó được xây dựng bằng C ++. Ví dụ trên là một ví dụ ngớ ngẩn, nhưng đây là cách cộng và trừ nói chung được thực hiện!

Một tài liệu tham khảo tốt về chủ đề là Tính phức tạp của các phép toán . Nó cho bạn biết cần bao nhiêu dung lượng cho mỗi thao tác bạn muốn thực hiện. Ví dụ, nếu bạn có hai N-digitsố, thì bạn cần 2N digitslưu trữ kết quả của phép nhân.

Như Mitch đã nói, đây không phải là một nhiệm vụ dễ thực hiện! Tôi khuyên bạn nên xem qua TTMath nếu bạn biết C ++.


Việc sử dụng các mảng đã xảy ra với tôi, nhưng tôi đang tìm kiếm thứ gì đó tổng quát hơn. Cảm ơn vì sự trả lời!
TT.

2
Hmm ... tên của người hỏi và tên của thư viện không thể là trùng hợp, đúng không? ;)
John Y

LoL, tôi không nhận thấy điều đó! Tôi ước thực sự TTMath là của tôi :) Btw đây là một trong những câu hỏi của tôi về chủ đề này:
AraK


3

Một trong những tài liệu tham khảo cuối cùng (IMHO) là TAOCP Tập II của Knuth. Nó giải thích rất nhiều thuật toán để biểu diễn các số và các phép toán số học trên các biểu diễn này.

@Book{Knuth:taocp:2,
   author    = {Knuth, Donald E.},
   title     = {The Art of Computer Programming},
   volume    = {2: Seminumerical Algorithms, second edition},
   year      = {1981},
   publisher = {\Range{Addison}{Wesley}},
   isbn      = {0-201-03822-6},
}

1

Giả sử rằng bạn muốn tự mình viết một mã số nguyên lớn, điều này có thể thực hiện đơn giản một cách đáng ngạc nhiên, được nói như một người đã làm điều đó gần đây (mặc dù trong MATLAB.) Dưới đây là một vài thủ thuật tôi đã sử dụng:

  • Tôi đã lưu trữ từng chữ số thập phân riêng lẻ dưới dạng số kép. Điều này làm cho nhiều hoạt động trở nên đơn giản, đặc biệt là đầu ra. Mặc dù nó chiếm nhiều dung lượng hơn bạn có thể muốn, nhưng bộ nhớ ở đây rẻ và nó làm cho phép nhân rất hiệu quả nếu bạn có thể biến đổi một cặp vectơ một cách hiệu quả. Ngoài ra, bạn có thể lưu trữ một số chữ số thập phân ở dạng kép, nhưng hãy cẩn thận khi đó tích chập để thực hiện phép nhân có thể gây ra các vấn đề về số trên các số rất lớn.

  • Lưu trữ một bit dấu hiệu riêng biệt.

  • Phép cộng hai số chủ yếu là thêm các chữ số, sau đó kiểm tra xem có mang theo ở mỗi bước hay không.

  • Phép nhân một cặp số tốt nhất nên được thực hiện dưới dạng tích chập, sau đó là bước thực hiện, ít nhất là nếu bạn có mã tích chập nhanh.

  • Ngay cả khi bạn lưu trữ các số dưới dạng một chuỗi các chữ số thập phân riêng lẻ, có thể thực hiện phép chia (cũng có thể thực hiện mod / rem) để thu được khoảng 13 chữ số thập phân tại một thời điểm trong kết quả. Cách này hiệu quả hơn nhiều so với phép chia chỉ có 1 chữ số thập phân tại một thời điểm.

  • Để tính lũy thừa của một số nguyên, hãy tính biểu diễn nhị phân của số mũ. Sau đó sử dụng các phép toán bình phương lặp lại để tính lũy thừa khi cần thiết.

  • Nhiều hoạt động (bao thanh toán, kiểm tra tính nguyên thủy, v.v.) sẽ được hưởng lợi từ hoạt động powermod. Tức là, khi bạn tính mod (a ^ p, N), hãy giảm mod kết quả N ở mỗi bước của lũy thừa trong đó p đã được biểu diễn dưới dạng nhị phân. Không tính a ^ p trước, sau đó cố gắng giảm nó mod N.


1
Nếu bạn đang lưu trữ các chữ số riêng lẻ thay vì cơ số 10 ^ 9 hoặc cơ số 2 ^ 32 hoặc thứ gì đó tương tự, tất cả những thứ tích lũy cho phép nhân ưa thích của bạn chỉ đơn giản là lãng phí. Big-O khá vô nghĩa khi hằng số của bạn quá tệ ...
R .. GitHub NGỪNG TRỢ GIÚP LÚC NỮA,

0

Đây là một ví dụ đơn giản (ngây thơ) mà tôi đã làm trong PHP.

Tôi đã triển khai "Thêm" và "Nhân" và sử dụng nó cho một ví dụ về số mũ.

http://adevsoft.com/simple-php-arbitrary-pre precision-integer-big-num-example/

Đoạn mã

// Add two big integers
function ba($a, $b)
{
    if( $a === "0" ) return $b;
    else if( $b === "0") return $a;

    $aa = str_split(strrev(strlen($a)>1?ltrim($a,"0"):$a), 9);
    $bb = str_split(strrev(strlen($b)>1?ltrim($b,"0"):$b), 9);
    $rr = Array();

    $maxC = max(Array(count($aa), count($bb)));
    $aa = array_pad(array_map("strrev", $aa),$maxC+1,"0");
    $bb = array_pad(array_map("strrev", $bb),$maxC+1,"0");

    for( $i=0; $i<=$maxC; $i++ )
    {
        $t = str_pad((string) ($aa[$i] + $bb[$i]), 9, "0", STR_PAD_LEFT);

        if( strlen($t) > 9 )
        {
            $aa[$i+1] = ba($aa[$i+1], substr($t,0,1));
            $t = substr($t, 1);
        }

        array_unshift($rr, $t);
     }

     return implode($rr);
}
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.