Keyfi hassasiyette aritmetik Açıklama


92

C'yi öğrenmeye çalışıyorum ve GERÇEKTEN büyük sayılarla (yani, 100 hane, 1000 hane, vb.) Çalışamama durumuyla karşılaştım. Bunu yapacak kütüphaneler olduğunun farkındayım, ancak bunu kendim uygulamaya çalışmak istiyorum.

Sadece herhangi birinin keyfi-kesinlikli aritmetiğin çok detaylı, basitleştirilmiş bir açıklamasına sahip olup olmadığını bilmek istiyorum.

Yanıtlar:


163

Sayıları daha küçük parçalar olarak ele almak, yeterli depolama ve algoritmalar meselesidir. Bir derleyicinizin intyalnızca 0'dan 99'a kadar olabileceği ve 999999'a kadar olan sayıları işlemek istediğinizi varsayalım (burada basit tutmak için yalnızca pozitif sayılar için endişeleneceğiz).

Bunu, her bir numaraya üç ints vererek ve toplama, çıkarma ve diğer temel işlemler için ilkokulda öğrendiğiniz (sahip olmanız gereken) aynı kuralları kullanarak yaparsınız.

Rasgele bir hassas kitaplıkta, sayılarımızı temsil etmek için kullanılan temel türlerin sayısında sabit bir sınır yoktur, tam da belleğin tutabildiği şey.

Ekleme, örneğin 123456 + 78::

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

En önemsiz uçtan çalışmak:

  • ilk taşıma = 0.
  • 56 + 78 + 0 taşıma = 134 = 34 1 taşıma ile
  • 34 + 00 + 1 taşıma = 35 = 35 ile 0 taşıma
  • 12 + 00 + 0 taşıma = 12 = 12 ile 0 taşıma

Aslında bu, eklemenin genellikle CPU'nuzdaki bit seviyesinde nasıl çalıştığıdır.

Çıkarma benzerdir (taban türünün çıkarılması ve taşıma yerine ödünç alma kullanılarak), çarpma, tekrarlanan eklemelerle (çok yavaş) veya çapraz ürünlerle (daha hızlı) yapılabilir ve bölme daha zordur, ancak sayıların kaydırılması ve çıkarılmasıyla yapılabilir. dahil (çocukken öğreneceğiniz uzun bölüm).

Aslında, karesi alındığında bir tamsayıya sığabilen on'un maksimum güçlerini kullanarak bu tür şeyler yapmak için kitaplıklar yazdım (iki ints'yi birlikte çarparken taşmayı önlemek için , örneğin 16 bitin int0 ile 99 arasında sınırlanması gibi) karesi alınırken 9,801 (<32,768) veya int99,980,001 (<2,147,483,648) oluşturmak için 0 ila 9,999 kullanarak 32 bit üretin ve bu da algoritmaları büyük ölçüde kolaylaştırdı.

Dikkat edilmesi gereken bazı püf noktaları.

1 / Numaraları eklerken veya çarparken, gereken maksimum alanı önceden ayırın ve daha sonra çok fazla bulursanız azaltın. Örneğin, iki adet 100 "basamaklı" (basamak bir intsayıdır) sayı eklemek size asla 101 basamaktan fazlasını vermez. 12 basamaklı bir sayıyı 3 basamaklı bir sayı ile çarptığınızda hiçbir zaman 15 basamaktan fazlasını oluşturmayacaksınız (basamak sayılarını ekleyin).

2 / Daha fazla hız için, sayıları yalnızca kesinlikle gerekliyse normalleştirin (gereken depolamayı azaltın) - kütüphanem bunu ayrı bir arama olarak yaptı, böylece kullanıcı hız ve depolama sorunları arasında karar verebilir.

3 / Pozitif ve negatif bir sayının toplanması çıkarma işlemidir ve negatif bir sayının çıkarılması, eşdeğer pozitifin toplanmasıyla aynıdır. İşaretleri ayarladıktan sonra toplama ve çıkarma yöntemlerinin birbirini çağırmasını sağlayarak oldukça fazla kod kaydedebilirsiniz.

4 / Büyük sayıları küçük sayılardan çıkarmaktan kaçının çünkü her zaman aşağıdaki gibi sayılarla karşılaşacaksınız:

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

Bunun yerine, 11'den 10'u çıkarın, sonra onu olumsuzlayın:

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

İşte bunu yapmak zorunda olduğum kütüphanelerden birinin yorumları (metne dönüştürülmüş). Kodun kendisi maalesef telif hakkıyla korunmaktadır, ancak dört temel işlemi idare etmek için yeterli bilgiyi seçmeniz mümkün olabilir. Aşağıdakileri varsayalım -ave -bnegatif sayıları temsil eder ave bsıfır veya pozitif sayılardır.

İçin ek işaretler farklı ise, olumsuzlama kullanımı çıkarma:

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

İçin çıkarma , işaretler farklı ise, olumsuzlama kullanımı ilavesi:

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

Ayrıca küçük sayıları büyük sayılardan çıkardığımızdan emin olmak için özel işlem:

small - big becomes -(big - small)

Çarpma , aşağıdaki gibi giriş düzeyinde matematik kullanır:

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

Bunun elde edilme yolu, 32 basamaktan her birinin bir seferde (geriye doğru) çıkarılmasını ve ardından sonuca eklenecek bir değeri (başlangıçta sıfır) hesaplamak için add işlevinin kullanılmasını içerir.

ShiftLeftve ShiftRightişlemler a'yı LongIntsarma değeriyle hızlıca çarpmak veya bölmek için kullanılır ("gerçek" matematik için 10). Yukarıdaki örnekte, 950 elde etmek için 475'i sıfıra 2 kez (32'nin son rakamı) ekliyoruz (sonuç = 0 + 950 = 950).

Sonra 4750'ye kaydırarak 475'i bıraktık ve 3'ü elde etmek için sağa 32 kaydırdık. 14250'yi elde etmek için 4750'yi sıfıra 3 kez ekledikten sonra 15200 elde etmek için 950'nin sonucuna ekleyin.

4750'yi 47500 almak için sola kaydırın, 0 almak için sağa kaydırın. Sağa kaydırılan 32 şimdi sıfır olduğundan, işimiz bitti ve aslında 475 x 32, 15200'e eşittir.

Bölme de zordur ancak erken aritmetiğe dayanır ("içine girer" için "gazinta" yöntemi). Aşağıdaki uzun bölmeyi düşünün 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.

Bu nedenle 12345 / 27bir 457geri kalanı ile 6. Doğrulayın:

  457 x 27 + 6
= 12339    + 6
= 12345

Bu, 12345 segmentlerini 27'ye eşit veya daha büyük olana kadar teker teker aşağı çekmek için bir aşağı çekme değişkeni (başlangıçta sıfır) kullanılarak gerçekleştirilir.

Sonra 27'nin altına inene kadar bundan 27 çıkarırız - çıkarma sayısı en üst satıra eklenen segmenttir.

Aşağı indirilecek daha fazla segment kalmadığında, sonucumuz var.


Bunların oldukça basit algoritmalar olduğunu unutmayın. Sayılarınız özellikle büyük olacaksa, karmaşık aritmetik yapmanın çok daha iyi yolları vardır. GNU Çoklu Duyarlı Aritmetik Kitaplığı gibi bir şeye bakabilirsiniz - bu benim kitaplıklarımdan önemli ölçüde daha iyi ve daha hızlıdır.

Oldukça talihsiz bir yanlışlığa sahiptir, çünkü hafızası biterse basitçe çıkacaktır (bence genel amaçlı bir kütüphane için oldukça ölümcül bir kusurdur), ancak bunun geçmişine bakabilirseniz, yaptığı işte oldukça iyidir.

Lisanslama amacıyla kullanamıyorsanız (veya uygulamanızın görünürde bir neden olmadan çıkmasını istemiyorsanız), en azından algoritmaları oradan kendi kodunuza entegre etmek için alabilirsiniz.

Ayrıca, MPIR'daki (bir GMP çatalı) gövdelerin potansiyel değişikliklerle ilgili tartışmalara daha uygun olduğunu buldum - daha geliştirici dostu bir grup gibi görünüyorlar.


14
Sanırım "Sadece herhangi birinin keyfi-kesinlikli aritmetiğin çok detaylı, basitleştirilmiş bir açıklamasına sahip olup olmadığını bilmek istiyorum" ÇOK iyi
Grant Peters

Takip eden bir soru: Makine koduna erişim olmadan taşımaları ve taşmaları ayarlamak / tespit etmek mümkün müdür?
SasQ

8

Tekerleği yeniden icat etmek kişisel eğitiminiz ve öğrenmeniz için son derece iyi olsa da, aynı zamanda son derece büyük bir görevdir. Kendim yaptığım ve önemli bir alıştırma olarak sizi caydırmak istemiyorum, ancak daha büyük paketlerin ele aldığı iş yerinde ince ve karmaşık sorunlar olduğunu bilmelisiniz.

Örneğin çarpma. Naif bir şekilde, 'okul çocuğu' yöntemini düşünebilirsiniz, yani bir sayıyı diğerinin üzerine yazın, sonra okulda öğrendiğiniz gibi uzun çarpma yapın. misal:

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

ancak bu yöntem son derece yavaştır (O (n ^ 2), n basamak sayısıdır). Bunun yerine, modern bignum paketleri, bunu temelde O (n ln (n)) işlemine dönüştürmek için ayrı bir Fourier dönüşümü veya bir Sayısal dönüşüm kullanır.

Ve bu sadece tamsayılar içindir. Sayının bir tür gerçek temsilinde (log, sqrt, exp, vb.) Daha karmaşık işlevlere girdiğinizde işler daha da karmaşık hale gelir.

Biraz teorik arka plan istiyorsanız, Yap'ın "Algoritmik Cebirin Temel Sorunları" adlı kitabının ilk bölümünü okumanızı şiddetle tavsiye ederim . Daha önce belirtildiği gibi, gmp bignum kitaplığı mükemmel bir kitaplıktır. Gerçek sayılar için mpfr kullandım ve beğendim.


1
"Bunu esasen bir O (n ln (n)) işlemine dönüştürmek için ayrık bir Fourier dönüşümü veya bir Sayısal dönüşüm kullanın" ile ilgili bölümle ilgileniyorum - bu nasıl çalışır? Sadece bir referans iyi olur :)
detly

1
@detly: polinom çarpımı, evrişim ile aynıdır, hızlı evrişimi gerçekleştirmek için FFT'yi kullanma hakkında bilgi bulmak kolay olmalıdır. Herhangi bir sayı sistemi, rakamların katsayılar ve tabanın taban olduğu bir polinomdur. Elbette, basamak aralığını aşmamak için taşıma işlemlerine dikkat etmeniz gerekir.
Ben Voigt

6

Tekerleği yeniden icat etmeyin: kare olabilir!

Denenmiş ve test edilmiş GNU MP gibi bir üçüncü taraf kitaplığı kullanın .


4
C'yi öğrenmek istersen, gözlerini biraz daha aşağı çekerim. Bir bignum kitaplığı uygulamak, bir öğrenciyi ayağa kaldıracak her türlü ince nedenden ötürü önemsiz değildir
Mitch Wheat

3
3. taraf kitaplığı: kabul edildi, ancak GMP'nin lisans sorunları var (LGPL, etkili bir şekilde GPL gibi davranıyor çünkü LGPL uyumlu bir arayüz aracılığıyla yüksek performanslı matematik yapmak biraz zor olduğundan).
Jason S

Güzel Futurama referansı (kasıtlı mı?)
Grant Peters

7
GNU MP, kayıtsız şartsız abort(), delicesine büyük bazı hesaplamalarda meydana gelmesi muhtemel olan tahsis hatalarını çağırır . Bu, bir kitaplık için kabul edilemez bir davranış ve kendi keyfi hassasiyetli kodunuzu yazmak için yeterli bir nedendir.
R .. GitHub BUZA YARDIM ETMEYİ DURDUR

Orada R ile aynı fikirdeyim. Bellek azaldığında programınızın altından halıyı çıkaran genel amaçlı bir kitaplık affedilemez. Güvenlik / kurtarılabilirlik için biraz hızdan fedakarlık etmelerini tercih ederdim.
paxdiablo

4

Temelde kalem ve kağıtla yaptığınız gibi yaparsınız ...

  • Sayı, gerektiğinde keyfi bir boyut alabilen bir tamponda (dizi) temsil edilecektir (bu, mallocve kullanılması anlamına gelir realloc)
  • Dil destekli yapıları kullanarak mümkün olduğunca temel aritmetik uygularsınız ve taban noktasını manuel olarak taşır ve hareket ettirirsiniz
  • Daha karmaşık işlevlerle başa çıkmak için verimli argümanlar bulmak için sayısal analiz metinlerini araştırırsınız
  • sadece ihtiyacınız olduğu kadar uygularsınız.

Tipik olarak temel hesaplama birimi olarak kullanacaksınız

  • 0-99 veya 0-255 içeren baytlar
  • 0-9999 veya 0-65536 içeren 16 bit sözcükler
  • 32 bit kelime içeren ...
  • ...

mimarinizin belirlediği gibi.

İkili veya ondalık taban seçimi, maksimum alan verimliliği, insan tarafından okunabilirlik ve çipinizde İkili Kodlu Ondalık (BCD) matematik desteğinin bulunmamasına bağlıdır.


3

Bunu lise düzeyinde matematikle yapabilirsiniz. Gerçekte daha gelişmiş algoritmalar kullanılsa da. Örneğin iki 1024 baytlık sayı eklemek için:

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 placemaksimum değerlere dikkat etmek için ilave yapılması durumunda sonucun daha büyük olması gerekecektir . Şuna bak :

9
   +
9
----
18

TTMath , öğrenmek istiyorsanız harika bir kütüphanedir. C ++ kullanılarak oluşturulmuştur. Yukarıdaki örnek aptalcaydı, ancak genel olarak toplama ve çıkarma bu şekilde yapılır!

Konu hakkında iyi bir referans , matematiksel işlemlerin Hesaplamalı karmaşıklığıdır . Gerçekleştirmek istediğiniz her işlem için ne kadar alan gerektiğini size söyler. Örneğin, iki N-digitsayınız varsa 2N digits, çarpmanın sonucunu kaydetmeniz gerekir .

As Mitch söyledi projesinin hayata geçirilmesi kolay bir iş uzak değildir gereğidir! C ++ biliyorsanız TTMath'a bir göz atmanızı tavsiye ederim.


Dizilerin kullanımı aklıma geldi, ancak daha genel bir şey arıyorum. Cevabınız için teşekkürler!
TT.

2
Hmm ... soruyu soranın adı ve kütüphanenin adı tesadüf olamaz, değil mi? ;)
John Y

LoL, bunu fark etmedim! Keşke gerçekten TTMath benim olsaydı :) Btw konuyla ilgili sorularımdan biri:
AraK


3

Nihai referanslardan biri (IMHO) Knuth'un TAOCP Volume II'sidir. Bu temsiller üzerinde sayıları ve aritmetik işlemleri temsil etmek için birçok algoritmayı açıklar.

@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

Kendi kendinize büyük bir tamsayı kodu yazmak istediğinizi varsayarsak, bunu yapmak şaşırtıcı derecede basit olabilir, son zamanlarda bunu yapan biri olarak söylenebilir (MATLAB'da olsa da.) İşte kullandığım püf noktalarından birkaçı:

  • Her bir ondalık basamağı çift sayı olarak sakladım. Bu, birçok işlemi, özellikle çıktıyı basitleştirir. İstediğinizden daha fazla depolama alanı kaplasa da, burada hafıza ucuzdur ve bir çift vektörü verimli bir şekilde birleştirebiliyorsanız çarpmayı çok verimli hale getirir. Alternatif olarak, birkaç ondalık basamağı ikili olarak saklayabilirsiniz, ancak çarpma işlemini yapacak evrişimin çok büyük sayılarda sayısal sorunlara neden olabileceğine dikkat edin.

  • Bir işaret parçasını ayrı olarak saklayın.

  • İki sayının eklenmesi esas olarak rakamların eklenmesiyle ilgilidir, ardından her adımda bir taşıma olup olmadığını kontrol edin.

  • Bir çift sayının çarpımı en iyi şekilde evrişim olarak yapılır ve ardından bir taşıma adımı gelir, en azından dokunma sırasında hızlı bir evrişim kodunuz varsa.

  • Sayıları tek tek ondalık basamak dizisi olarak sakladığınızda bile, sonuçta bir seferde kabaca 13 ondalık basamak elde etmek için bölme (ayrıca mod / rem işlemleri) yapılabilir. Bu, bir seferde yalnızca 1 ondalık basamakta çalışan bir bölmeden çok daha etkilidir.

  • Bir tamsayının tamsayı kuvvetini hesaplamak için üssün ikili temsilini hesaplayın. Daha sonra, güçleri gerektiği gibi hesaplamak için tekrarlanan kare alma işlemlerini kullanın.

  • Pek çok işlem (faktoring, asallık testleri, vb.) Bir powermod işleminden faydalanacaktır. Yani, mod'u (a ^ p, N) hesapladığınızda, p'nin ikili biçimde ifade edildiği üslemenin her adımında sonuç mod N'yi azaltın. Önce a ^ p'yi hesaplamayın, sonra onu mod N'yi azaltmaya çalışın.


1
Baz-10 ^ 9 veya taban-2 ^ 32 veya benzeri bir şey yerine tek tek rakamları depoluyorsanız, tüm fantezi çarpma için evrişim şeyleriniz boşa harcanır. Sabitiniz o kadar kötü olduğunda Big-O oldukça anlamsız ...
.. GitHub BUZA YARDIM ETMEYİ DURDUR

0

İşte PHP'de yaptığım basit (saf) bir örnek.

"Ekle" ve "Çarp" uyguladım ve bunu üslü bir örnek için kullandım.

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

Kod kesme

// 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);
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.