[math] 임의 정밀도 산술 설명

저는 C를 배우려고하는데 정말 큰 숫자 (즉, 100 자리, 1000 자리 등)로 작업 할 수 없다는 것을 알게되었습니다. 이 작업을 수행 할 라이브러리가 있다는 것을 알고 있지만 직접 구현하려고합니다.

나는 누군가가 임의 정밀도 산술에 대한 매우 상세하고 멍청한 설명을 가지고 있는지 또는 제공 할 수 있는지 알고 싶습니다.



답변

숫자를 더 작은 부분으로 취급하는 것은 적절한 저장과 알고리즘의 문제입니다. an int이 0에서 99까지만 될 수 있는 컴파일러가 있고 999999까지의 숫자를 처리하고 싶다고 가정 해 봅시다 (여기서는 간단하게 유지하기 위해 양수에 대해서만 걱정할 것입니다).

int덧셈, 뺄셈 및 기타 기본 연산을 위해 초등학교에서 배웠어야했던 동일한 규칙을 사용하여 각 숫자에 3을 부여 합니다.

임의 정밀도 라이브러리에서는 숫자를 나타내는 데 사용되는 기본 유형의 수에 고정 된 제한이 없습니다.

예를 들어 추가 : 123456 + 78:

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

최하위 단계에서 작업 :

  • 초기 캐리 = 0.
  • 56 + 78 + 0 캐리 = 134 = 34 (1 캐리 포함)
  • 34 + 00 + 1 캐리 = 35 = 35, 0 캐리
  • 12 + 00 + 0 캐리 = 12 = 12, 0 캐리

사실 이것은 추가가 일반적으로 CPU 내부의 비트 수준에서 작동하는 방식입니다.

뺄셈은 비슷하고 (기본 유형의 뺄셈을 사용하고 캐리 대신 빌려 사용) 반복 덧셈 (매우 느림) 또는 교차 곱 (빠름)으로 곱셈을 수행 할 수 있으며 나눗셈은 더 까다 롭지 만 숫자를 이동하고 빼서 수행 할 수 있습니다. 관련 (어렸을 때 배웠을 긴 부문).

저는 실제로 제곱 할 때 정수에 들어갈 수있는 최대 10의 거듭 제곱을 사용하여 이러한 종류의 작업을 수행하는 라이브러리를 작성 int했습니다 (16 비트 int가 0에서 99로 제한되는 것과 같이 두 s를 곱할 때 오버플로를 방지하기 위해 제곱 할 때 9,801 (<32,768) int을 생성하거나 0에서 9,999를 사용하여 32 비트 를 생성하여 99,980,001 (<2,147,483,648))을 생성하여 알고리즘을 크게 완화했습니다.

주의해야 할 몇 가지 트릭.

1 / 숫자를 더하거나 곱할 때 필요한 최대 공간을 미리 할당하고 너무 많으면 나중에 줄이십시오. 예를 들어 100 자리 숫자 (숫자는 int) 두 개 를 추가해도 101 자리를 넘지 않습니다. 12 자리 숫자에 3 자리 숫자를 곱하면 15 자리 이상이 생성되지 않습니다 (숫자 개수 추가).

2 / 속도를 높이려면 절대적으로 필요한 경우에만 숫자를 정규화 (필요한 저장 공간 줄이기)합니다. 내 라이브러리에는 사용자가 속도와 저장 문제 사이에서 결정할 수 있도록 별도의 호출이있었습니다.

3 / 양수와 음수를 더하는 것은 빼기이고, 음수를 빼는 것은 동등한 양수를 더하는 것과 같습니다. 기호를 조정 한 후 add 및 subtract 메서드가 서로 호출하도록하여 코드를 상당히 절약 할 수 있습니다.

4 / 다음과 같은 숫자로 끝날 수 있으므로 작은 숫자에서 큰 숫자를 빼지 마십시오.

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

대신 11에서 10을 뺀 다음 부정합니다.

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

다음은 내가이 작업을 수행해야하는 라이브러리 중 하나의 주석 (텍스트로 바뀜)입니다. 불행히도 코드 자체는 저작권이 있지만 네 가지 기본 작업을 처리 할 수있는 충분한 정보를 선택할 수 있습니다. 그 다음에 가정 -a-b음수를 나타내고 a그리고 b제로 또는 양의 수이다.

대한 또한 , 표시가 다른 경우, 부정 사용 공제 :

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

들어 뺄셈 , 표시가 다른 경우, 부정 사용 추가 :

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

또한 큰 수에서 작은 수를 뺄 수 있도록 특수 처리합니다.

small - big becomes -(big - small)

곱셈 은 다음과 같이 엔트리 레벨 수학을 사용합니다.

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

이를 달성하는 방법은 32의 각 자릿수를 한 번에 하나씩 (뒤로) 추출한 다음 add를 사용하여 결과에 추가 할 값 (처음에는 0)을 계산하는 것입니다.

ShiftLeft그리고 ShiftRight연산은 a LongInt를 랩 값 ( “실제”수학의 경우 10) 으로 빠르게 곱하거나 나누는 데 사용됩니다 . 위의 예에서 475를 0에 2 번 더해 (32의 마지막 숫자) 950을 얻습니다 (결과 = 0 + 950 = 950).

그런 다음 475를 왼쪽으로 이동하여 4750을 얻고 32를 오른쪽으로 이동하여 3을 얻습니다. 4750을 0에 3 번 더하여 14250을 얻은 다음 950의 결과에 더하여 15200을 얻습니다.

47500을 얻으려면 4750을 왼쪽으로 시프트하고 0을 얻으려면 3을 오른쪽으로 시프트합니다. 오른쪽으로 시프트 된 32는 이제 0이므로 완료되었으며 실제로 475 x 32는 15200과 같습니다.

나누기 도 까다 롭지 만 초기 산술 ( “goes into”에 대한 “gazinta”방법)을 기반으로합니다. 에 대한 다음 긴 나눗셈을 고려하십시오 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.

따라서 12345 / 27입니다 457나머지 6. 확인:

  457 x 27 + 6
= 12339    + 6
= 12345

이는 드로 다운 변수 (처음에는 0)를 사용하여 12345의 세그먼트가 27보다 크거나 같을 때까지 한 번에 하나씩 가져옴으로써 구현됩니다.

그런 다음 27 미만이 될 때까지 27을 빼면됩니다. 빼기 횟수는 맨 윗줄에 추가 된 세그먼트입니다.

삭제할 세그먼트가 더 이상 없으면 결과가 있습니다.


이것은 매우 기본적인 알고리즘이라는 것을 명심하십시오. 숫자가 특히 클 경우 복잡한 산술을 수행하는 훨씬 더 좋은 방법이 있습니다. GNU Multiple Precision Arithmetic Library 와 같은 것을 살펴볼 수 있습니다. 이것은 제 라이브러리보다 훨씬 더 좋고 빠릅니다.

그것은 메모리가 부족하면 단순히 종료된다는 점에서 다소 불행한 잘못된 기능이 있습니다 (제 생각에는 범용 라이브러리의 경우 다소 치명적인 결함).

라이센스 이유로 사용할 수없는 경우 (또는 명백한 이유없이 응용 프로그램이 종료되는 것을 원하지 않기 때문에) 적어도 자체 코드에 통합하기위한 알고리즘을 얻을 수 있습니다.

나는 또한 MPIR (GMP의 포크)의 이사회 가 잠재적 인 변경 사항에 대한 토론에 더 적합 하다는 것을 발견했습니다 . 개발자 친화적 인 무리처럼 보입니다.


답변

바퀴를 재창조하는 것은 개인적인 교화와 학습에 매우 좋을뿐만 아니라 매우 큰 작업이기도합니다. 나는 당신이 중요한 운동이고 내가 직접 해본 것이라고 단념하고 싶지는 않지만, 더 큰 패키지가 해결하는 미묘하고 복잡한 문제가 작업에 있다는 것을 알고 있어야합니다.

예를 들어, 곱셈. 순진하게도 ‘schoolboy’방법을 생각할 수 있습니다. 즉, 다른 숫자 위에 하나의 숫자를 쓴 다음 학교에서 배운 것처럼 긴 곱셈을 수행합니다. 예:

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

그러나이 방법은 매우 느립니다 (O (n ^ 2), n은 자릿수). 대신 현대의 bignum 패키지는 이산 푸리에 변환 또는 숫자 변환을 사용하여이를 본질적으로 O (n ln (n)) 연산으로 변환합니다.

그리고 이것은 정수만을위한 것입니다. 숫자의 실제 표현 (log, sqrt, exp 등)에 대해 더 복잡한 함수를 사용하면 상황이 더욱 복잡해집니다.

이론적 배경이 필요하다면 Yap 책의 첫 번째 장을 읽는 것이 좋습니다. “Fundamental Problems of Algorithmic Algebra” . 이미 언급했듯이 gmp bignum 라이브러리는 훌륭한 라이브러리입니다. 실수의 경우 mpfr을 사용했고 좋아했습니다.


답변

바퀴를 재발 명하지 마십시오. 사각형으로 판명 될 수 있습니다!

시도되고 테스트 된 GNU MP 와 같은 타사 라이브러리를 사용하십시오 .


답변

기본적으로 연필과 종이로하는 것과 같은 방식으로합니다.

  • 수는 사용되는 수단 (임의의 크기에 대해 수행 할 수있는 버퍼 (배열)로 표현 될 mallocrealloc 필요)
  • 언어 지원 구조를 사용하여 가능한 한 기본 산술을 구현하고 기수 점을 수동으로 운반하고 이동하는 작업을 처리합니다.
  • 더 복잡한 기능을 처리하기위한 효율적인 인수를 찾기 위해 숫자 분석 텍스트를 샅샅이 뒤집니다.
  • 필요한만큼만 구현합니다.

일반적으로 기본 계산 단위로 사용합니다.

  • 0-99 또는 0-255를 포함하는 바이트
  • 0-9999 또는 0-65536이 위축되는 16 비트 단어
  • 32 비트 단어 포함 …

아키텍처에 따라 결정됩니다.

바이너리 또는 10 진수베이스의 선택은 최대 공간 효율성, 사람의 가독성 및 칩에 BCD (Binary Coded Decimal) 수학 지원이 없는지에 대한 요구에 따라 다릅니다.


답변

고등학교 수준의 수학으로 할 수 있습니다. 실제로는 더 고급 알고리즘이 사용됩니다. 예를 들어 두 개의 1024 바이트 숫자를 추가하려면 다음을 수행하십시오.

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

one place최대 값을 처리하기 위해 추가하는 경우 결과가 더 커야 합니다. 이것 좀봐 :

9
   +
9
----
18

TTMath 는 배우고 싶다면 훌륭한 라이브러리입니다. C ++를 사용하여 빌드되었습니다. 위의 예는 어리석은 일이지만 일반적으로 덧셈과 뺄셈이 수행되는 방식입니다!

주제에 대한 좋은 참조 는 수학 연산의 계산 복잡성입니다 . 구현하려는 각 작업에 필요한 공간을 알려줍니다. 예를 들어, 두 개의 N-digit숫자가 2N digits있는 경우 곱셈 결과를 저장 해야 합니다.

미치가 말했다, 그것은 구현하는 것은 쉬운 일이 멀지 않은 것입니다! C ++을 알고 있다면 TTMath를 살펴 보는 것이 좋습니다.


답변

궁극적 인 참조 (IMHO) 중 하나는 Knuth의 TAOCP Volume II입니다. 이 표현에 대한 숫자와 산술 연산을 표현하는 많은 알고리즘을 설명합니다.

@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},
}


답변

큰 정수 코드를 직접 작성하고 싶다고 가정하면, 이것은 놀랍도록 간단 할 수 있습니다. 이것은 최근에 (MATLAB에서했지만) 누군가처럼 말하면서 할 수 있습니다. 제가 사용한 몇 가지 트릭은 다음과 같습니다.

  • 나는 각각의 10 진수를 이중 숫자로 저장했다. 이것은 많은 작업, 특히 출력을 단순화합니다. 원하는 것보다 더 많은 저장 공간을 차지하지만 여기에서는 메모리가 저렴하며 한 쌍의 벡터를 효율적으로 컨볼 루션 할 수 있다면 곱셈을 매우 효율적으로 만듭니다. 또는 여러 십진수를 double로 저장할 수 있지만 곱셈을 수행하는 컨볼 루션은 매우 큰 숫자에서 숫자 문제를 일으킬 수 있습니다.

  • 기호 비트를 별도로 저장하십시오.

  • 두 개의 숫자를 더하는 것은 주로 숫자를 더한 다음 각 단계에서 캐리를 확인하는 문제입니다.

  • 한 쌍의 숫자의 곱셈은 적어도 탭에 빠른 회선 코드가있는 경우 회선 다음에 캐리 단계를 수행하는 것이 가장 좋습니다.

  • 숫자를 개별 십진수 문자열로 저장하는 경우에도 결과에서 한 번에 약 13 개의 십진수를 얻기 위해 나누기 (mod / rem ops)를 수행 할 수 있습니다. 이것은 한 번에 십진수 1 자리에서만 작동하는 나누기보다 훨씬 효율적입니다.

  • 정수의 정수 거듭 제곱을 계산하려면 지수의 이진 표현을 계산하십시오. 그런 다음 반복 제곱 연산을 사용하여 필요에 따라 거듭 제곱을 계산합니다.

  • 많은 작업 (팩터링, 소수 테스트 등)은 powermod 작업의 이점을 누릴 수 있습니다. 즉, mod (a ^ p, N)을 계산할 때 p가 이진 형식으로 표현 된 지수화의 각 단계에서 결과 mod N을 줄입니다. 먼저 a ^ p를 계산하지 말고 mod N을 줄이십시오.