Реферат: "Длинная" арифметика
"Длинная"
арифметика
Известно, что
арифметические действия, выполняемые компьютером в ограниченном числе разрядов,
не всегда позволяют получить точный результат. Более того, мы ограничены
размером (величиной) чисел, с которыми можем работать. А если нам необходимо
выполнить арифметические действия над очень большими числами, например,
30! =
265252859812191058636308480000000?
В таких случаях
мы сами должны позаботиться о представлении чисел в машине и о точном
выполнении арифметических операций над ними.
Числа, для
представления которых в стандартных компьютерных типах данных не хватает
количества двоичных разрядов, называются "длинными". Реализация
арифметических операций над такими "длинными" числами получила
название "длинной арифметики".
Организация
работы с "длинными" числами во многом зависит от того, как мы
представим в компьютере эти числа. "Длинное" число можно записать,
например, с помощью массива десятичных цифр, количество элементов в таком
массиве равно количеству значащих цифр в "длинном" числе. Но если мы
будем реализовывать арифметические операции над этим числом, то размер массива
должен быть достаточным, чтобы разместить в нем и результат, например,
умножения.
Существуют и
другие представления "длинных" чисел. Рассмотрим одно из них.
Представим наше число
30! =
265252859812191058636308480000000
в виде:
30! = 2 *
(104)8 + 6525 * (104)7 + 2859 * (104) + 8121 * (104)5 + 9105 * (104)4 + 8636 *
(104)3 + 3084 * (104)2 + 8000 * (104)1 + 0000 * (104)0.
Это
представление наталкивает на мысль о массиве, представленном в табл. 1.
Таблица 1
Номер
элемента в массиве А
|
0
|
1
|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
Значение
|
9
|
0
|
8000
|
3084
|
8636
|
9105
|
8121
|
2859
|
6525
|
2
|
Мы можем
считать, что наше "длинное" число представлено в 10000-10 системе
счисления (десятитысячно-десятичная система счисления, приведите аналогию с
восьмерично-десятичной системой счисления), а "цифрами" числа
являются четырехзначные числа.
Возникают
вопросы. Что за 9 в А [0], почему число хранится "задом наперед"?
Ответы очевидны, но подождем с преждевременными объяснениями. Ответы на вопросы
будут ясны из текста.
Примечание. Мы
работаем с положительными числами!
Первая задача.
Ввести "длинное" число из файла. Решение задачи начнем с описания
данных.
Const MaxDig = 1000; {Максимальное
количество цифр — четырехзначных!}
Osn = 10000; {Основание нашей системы
счисления,
в
элементах массива храним четырехзначные числа}
Type Tlong
= Array[0..MaxDig] Of Integer;
{Максимальное количество десятичных цифр в
нашем числе}
Алгоритм ввода
"длинного" числа из файла рассмотрим на конкретном примере.
Пусть в файле
записано число 23851674 и основанием (Osn) является 1000 (храним по три цифры в
элементе массива А). Изменение значений элементов массива А в процессе ввода
(посимвольного в переменную Ch) отражено в табл. 2.
Таблица 2
А[0]
|
А[1]
|
А[2]
|
А[3]
|
Ch
|
Примечание
|
3
|
674
|
851
|
23
|
-
|
Конечное
состояние
|
0
|
0
|
0
|
0
|
2
|
Начальное
состояние
|
1
|
2
|
0
|
0
|
3
|
1-й шаг
|
1
|
23
|
0
|
0
|
8
|
2-й шаг
|
1
|
238
|
0
|
0
|
5
|
3-й шаг
|
2
|
385
|
2
|
0
|
1
|
4-й
шаг: старшая цифра элемента А [1] перешла в пока "пустой" элемент
А[2]
|
2
|
851
|
23
|
0
|
6
|
5-й шаг
|
2
|
516
|
238
|
0
|
7
|
6-й шаг
|
3
|
167
|
385
|
2
|
4
|
7-й шаг
|
3
|
674
|
851
|
23
|
|
|
Проанализируем
таблицу (и получим ответы на поставленные выше вопросы).
1. В А[0]
храним количество задействованных (ненулевых) элементов массива А — это уже
очевидно.
2. При
обработке каждой очередной цифры входного числа старшая цифра элемента массива
с номером i становится младшей цифрой числа в элементе i+1, а вводимая цифра
будет младшей цифрой числа из А[1]. В результате работы нашего алгоритма мы
получили число, записанное "задом наперед".
Примечание
(методическое): Можно ограничиться этим объяснением и разработку процедуры
вынести на самостоятельное задание. Можно продолжить объяснение. Например,
выписать фрагмент текста процедуры перенос старшей цифры из A[i] в младшую
цифру А[i+1], т.е. сдвиг уже введенной части числа на одну позицию вправо:
For i := A[0] DownTo 1 Do
Begin
A[i+l] := A[i+l] + (Longint(A[i]) *
10) Div Osn;
A[i] := (LongInt(A[i]) * 10) Mod
Osn;
End;
Пусть мы вводим
число 23851674 и первые 6 цифр уже разместили "задом наперед" в
массиве А. В символьную переменную считали очередную цифру "длинного"
числа — это "7". По нашему алгоритму эта цифра "7" должна
быть размещена младшей цифрой в А[1]. Выписанный фрагмент программы
"освобождает" место для этой цифры. В таблице отражены результаты
работы этого фрагмента.
i
|
А[1]
|
А[2]
|
А[3]
|
ch
|
2
|
516
|
238
|
0
|
7
|
2
|
516
|
380
|
2
|
|
1
|
160
|
385
|
2
|
|
После этого
остается только добавить текущую (считанную в ch) цифру "длинного"
числа к А[1] и изменить значение А[0].
В конечном
итоге процедура должна иметь следующий вид:
Procedure ReadLong(Var A : Tlong);
Var ch : char;
i : Integer;
Begin
FillChar(A, SizeOf(A), 0) ;
Read(ch);
While Not(ch In ['0'..'9']) Do
Read(ch);
{пропуск не цифр во входном файле}
While
ch In ['0'..'9'] Do
Begin
For i := A[0] DownTo 1 Do
Begin
{"протаскивание" старшей
цифры в числе из A[i]
в младшую цифру числа из A[i+l]}
A[i+l] := A[i+l] +
(LongInt(A[i]) * 10) Div Osn;
A[i]
:= (LongInt(A[i]) * 10) Mod Osn
End;
A[1] := A[l] + Ord(ch) - Ord('0');
{добавляем младшую цифру к числу из А[1]}
If
A[A[0]+1] > 0 Then Inc(A[0]);
{изменяем длину, число задействованных элементов массива А}
Read(ch)
End
End;
Вторая задача.
Вывод "длинного" числа в файл или на экран.
Казалось бы,
нет проблем — выводи число за числом. Однако в силу выбранного нами
представления "длинного" числа мы должны всегда помнить, что в каждом
элементе массива хранится не последовательность цифр "длинного"
числа, а значение числа, записанного этими цифрами. Пусть в элементах массива
хранятся четырехзначные числа. Тогда "длинное" число 128400583274
будет в массиве А представлено следующим образом:
A[0]
|
A[1]
|
A[2]
|
A[3]
|
3
|
3274
|
58
|
1284
|
При выводе
"длинного" числа из массива нам необходимо вывести 0058, иначе будет
потеря цифр. Итак, незначащие нули также необходимо выводить. Процедура вывода
имеет вид:
Procedure WriteLong(Const A :
Tlong);
Var ls, s : String; i : Integer;
Begin
Str(Osn Div 10, Is);
Write(A[A[0]]; {выводим старшие цифры числа}
For i
:= A[0] - l DownTo 1 Do
Begin
Str(A[i], s);
While Length(s) < Length(Is) Do s
:= '0' + s;
{дополняем незначащими нулями}
Write(s)
End;
WriteLn
End;
Третья задача.
Предварительная работа по описанию способа хранения, вводу и выводу "длинных"
чисел выполнена.
У нас есть все
необходимые "кирпичики", например, для написания программы сложения
двух "длинных" положительных чисел. Исходные числа и результат храним
в файлах. Назовем процедуру сложения SumLongTwo.
Тогда программа
ввода двух "длинных" чисел и вывода результата их сложения будет
иметь следующий вид:
Var A, B, C : Tlong;
Begin
Assign(Input,
'Input.txt'); Reset(Input);
ReadLong(A); ReadLong(B) ;
Close(Input);
SumLongTwo(A, B, C);
Assign(Output, 'Output.txt');
Rewrite(Output);
WriteLong(C);
Close(Output)
End.
Алгоритм
процедуры сложения можно объяснить на простом примере. Пусть А=870613029451,
В=3475912100517461.
i
|
A[i]
|
B[i]
|
C[1]
|
C[2]
|
C[3]
|
C[4]
|
1
|
9451
|
7461
|
6912
|
1
|
0
|
0
|
2
|
1302
|
51
|
6912
|
1354
|
0
|
0
|
3
|
8706
|
9121
|
6912
|
1354
|
7827
|
1
|
4
|
0
|
3475
|
6912
|
1354
|
7827
|
3476
|
Алгоритм
имитирует привычное сложение столбиком, начиная с младших разрядов. И именно
для простоты реализации арифметических операций над "длинными"
числами используется машинное представление "задом наперед".
Результат: С =
3476782713546912.
Ниже приведен
текст процедуры сложения двух "длинных" чисел.
Procedure SumLongTwo(A, B : Nlong;
Var C : Tlong);
Var i, k :
Integer;
Begin
FillChar(C, SizeOf (C), 0) ;
If A[0] > B[0] Then k := A[0]
Else k : =B[0];
For i := l To k Do
Begin С [i+1]
:= (C[i] + A[i] + B[i]) Div Osn;
C[i] := (C[i] + A[i] + B[i]) Mod Osn
{Есть ли в этих операторах ошибка?}
End;
If
C[k+l] = 0 Then C[0] := k Else C[0] := k + l
End;
Четвертая
задача. Реализация операций сравнения для "длинных" чисел (А=В,
А<В, А>В, А<=В, А>=В).
Function Eq(A, B : TLong) :
Boolean;
Var i :
Integer;
Begin
Eq := False;
If A[0] <> B[0] Then Exit
Else Begin
i := l;
While (i <= A[0]) And (A[i] =
B[i]) Do Inc(i);
Eq := i = A[0] + l
End
End;
Реализация
функции А > В также прозрачна.
Function More(A, B : Tlong) :
Boolean;
Var i :
Integer;
Begin If A[0]
< B[0] Then More := False
Else
If A[0] > B[0] Then More :=
True
Else Begin
i :=
A[0];
While (i
> 0) And (A[i] = B[i]) Do Dec(i);
If i = 0
Then More := False
Else
If A[i] > B[i] Then More := True
Else
More:=False
End
End;
Остальные
функции реализуются через функции Eq и More.
Function Less(A, B : Tlong) :
Boolean; {A < B}
Begin
Less := Not(More(A, B) Or Eq(A, B))
End;
Function
More_Eq(A, B : Tlong) : Boolean; {A >= B}
Begin
More_Eq := More(A, B) Or Eq(A, B)
End;
Function
Less_Eq(A, B : Tlong) : Boolean; {A <= B}
Begin
Less_Eq := Not More(A, B)
End;
Для
самостоятельного решения может быть предложена следующая, более сложная,
задача. Требуется разработать функцию, которая выдает 0, если А больше В, 1,
если А меньше В, и 2 при равенстве чисел. Но сравнение должно быть выполнено с
учетом сдвига. О чем идет речь? Поясним на примере. Пусть А равно 56784, а В —
634. При сдвиге числа В на 2 позиции влево функция должна сказать, что В больше
А, без сдвига, что А больше В. Другой пример. При А равном 56700, а В — 567 и
сдвиге 2 функция должна "сказать", что числа равны. Решение может
иметь следующий вид:
Function More(Const А, В : Tlong; Const sdvig : Integer) : Byte;
Var i : Integer;
Begin
If A[0] >
B[0] + sdvig Then More := 0
Else
If A[0] < B[0] + sdvig Then More := l
Else Begin
i :=
A[0];
While
(i > sdvig) And
(A[i]
= B[i-sdvig]) Do Dec(i);
If i =
sdvig Then Begin
More:=0;
{совпадение
чисел с учетом сдвига}
For i
:= 1 To sdvig Do
If A[i] > 0 Then Exit;
More := 2;
{числа
равны, "хвост" числа А равен нулю}
End
Else
More := Byte(A[i] < B[i-sdvig])
End
End;
Пятая задача.
Умножение длинного числа на короткое. Под коротким понимается целое число типа
LongInt.
Процедура очень
походит на процедуру сложения двух длинных чисел.
Procedure Mul(Const A : TLong;
Const К :
Longlnt; Var С :
TLong);
Var i :
Integer;
{результат - значение переменной С}
Begin
FillChar (С, SizeOf(С), 0);
If K = 0 Then Inc(С[0]){умножение на ноль}
Else
Begin
For i:= l To A[0] Do
Begin
C[i+l]
:= (LongInt(A[i]) * K + C[i]) Div Osn;
C[i]
:= (LongInt(A[i])* K + C[i]) Mod Osn
End;
If C[A[0]+1] > 0 Then C[0]:= A[0]
+ 1
Else C[0]:= A[0]
{определяем длину результата}
End
End;
Шестая задача.
Вычитание двух длинных чисел с учетом сдвига
Если понятие
сдвига пока не понятно, то оставьте его в покое, на самом деле вычитание с
учетом сдвига потребуется при реализации операции деления. В начале выясните
логику работы процедуры при нулевом сдвиге.
Введем
ограничение: число, из которого вычитают, больше числа, которое вычитается.
Работать с "длинными" отрицательными числами мы не умеем.
Процедура была
бы похожа на процедуры сложения и умножения, если бы не одно "но" —
заимствование единицы из старшего разряда вместо переноса единицы в старший
разряд. Например, в обычной системе счисления мы вычитаем 9 из 11 — идет
заимствование 1 из разряда десятков, а если из 10000 вычитаем 9 — процесс
заимствования несколько сложнее.
Procedure Sub (Var A : TLong; Const
B : TLong; Const sp : Integer);
Var i, j :
Integer;
{из А вычитаем В с учетом сдвига sp, результат вычитания в А}
Begin
For i := l To B[0] Do
Begin Dec(A[i+sp], B[i]);
j: = i;{*}
{реализация
сложного заимствования}
while
(A[j+sp] < 0) and (j <= A[0]) Do
Begin{*}
Inc(A[j+sp],
Osn) ;
Dec(A[j+sp+l]);
Inc(j); {*}
end; {*}
{Реализация
простого заимствования.
Если
операторы, отмеченные *, заменить
на
нижеприведенные операторы в фигурных скобках, то,
по
понятным причинам, логика не будет работать
при
всех исходных данных. Можно сознательно сделать
ошибку
и предложить найти ее — принцип "обучение через ошибку"}
{If
A[i+sp]<0 Then Begin Inc(A[i+sp], Osn);
Dec (A[i+sp+l]);End;}
End;
i := A[0];
While (i > l) And (A[i] = 0) Do
Dec(i);
A[0] := i
{корректировка
длины результата операции}
End;
Рекомендуется
выполнить трассировку работы данной процедуры, например, для следующих исходных
данных. Число А равно 100000001000000000000, число В — 2000073859998.
Седьмая задача.
Деление двух длинных чисел, т.е. нахождение целой части частного и остатка.
Написать
исходную (без уточнений) часть логики не составляет труда. Это:
Procedure
Long_Div_Long(Const А, В : TLong; Var Res, Ost : TLong);
Begin
FillChar(Res, SizeOf(Res), 0);
Res[0] := 1;
FillChar(Ost, SizeOf(Ost), 0);
0st[0] := 1;
Case More(A, B, 0) Of
0: MakeDel(A, B, Res, Ost);
{А больше В, пока не знаем, как выполнять операцию -
"выносим" в процедуру}
1:
Ost:=A; {А меньше В}
2:
Res[l] := l; {А равно В}
End;
End;
А дальше?
Дальше начинаются проблемы. Делить столбиком нас научили в школе. Например,
1000143123567 |73859998
- 73859998 |----------
--------- |13541 (Целая часть частного)
261543143
- 221579994
----------
399631495
- 369299990
---------
303315056
- 295439992
----------
78750647
- 73859998
--------
4890649 (Остаток)
Что мы делали?
На каждом этапе в уме подбирали цифру (1, 3, 5 и т.д.), такую, что произведение
этой цифры на делитель дает число меньшее, но наиболее близкое к числу...
Какому? Это трудно сказать словами, но из примера ясно. Зачем нам это делать в
уме, пусть делает компьютер. Однако упростим пример, оставим его для
тестирования окончательной логики процедуры, тем более что и числа
"длинные". Пусть число А будет меньше В*10, тогда в результате (целой
части деления) будет одна цифра. Например, А равно 564, а В — 63 и простая
десятичная система счисления. Попробуем подобрать цифру результата, но не
методом прямого перебора, а методом деления отрезка пополам. Пусть Down —
верхняя граница интервала изменения подбираемой цифры, Up — нижняя граница
интервала, Ost равен делимому.
Down
|
Up
|
С = В * ( (Down + Up)
Div 2)
|
Ost = 564
|
0
|
10
|
315 = 63 * ( (0 + 10) Div 2)
|
C < Ost
|
5
|
10
|
441 = 63 * ( (5 + 10) Div 2)
|
C < Ost
|
7
|
10
|
504 = 63 * ( (7 + 10) Div 2)
|
C < Ost
|
8
|
10
|
567 = 63 * ( (8 + 10) Div 2)
|
C > Ost
|
8
|
9
|
504 = 63 * ( (8 + 9) Div 2)
|
C < Ost
|
Итак, результат
— целая часть частного — равен (Up + Down) Div 2, остаток от деления — разность
между значениями Ost и С. Нижнюю границу (Down) изменяем, если результат (С)
меньше остатка, верхнюю (Up), — если больше.
Усложним
пример. Пусть А равно 27856, а В — 354. Основанием системы счисления является
не 10, а 10000.
Down
|
Up
|
С
|
Ost = 27856
|
0
|
10000
|
1770000
|
C > Ost
|
0
|
5000
|
885000
|
C > Ost
|
0
|
2500
|
442500
|
C > Ost
|
0
|
1250
|
221250
|
C > Ost
|
0
|
625
|
110448
|
C > Ost
|
0
|
312
|
55224
|
C > Ost
|
0
|
156
|
27612
|
C < Ost
|
78
|
156
|
41418
|
C > Ost
|
78
|
117
|
34338
|
C > Ost
|
78
|
97
|
30798
|
C > Ost
|
78
|
87
|
29028
|
C > Ost
|
78
|
82
|
28320
|
C > Ost
|
78
|
80
|
27966
|
C > Ost
|
78
|
79
|
27612
|
C <
Ost
|
Целая часть
частного равна 78, остаток от деления — 27856 минус 27612, т.е. 244.
Пора приводить
процедуру. Используемые "кирпичики": функция сравнения чисел (More) с
учетом сдвига и функция умножения длинного числа на короткое (Mul) описаны
выше.
Function FindBin(Var Ost : Tlong; Const В : TLong; Const sp : Integer) :
Longint;
Var Down, Up : Word; C : TLong;
Begin
Down := 0;Up
:= 0sn;
{основание системы счисления}
While Up - l > Down Do
Begin
{Есть
возможность преподавателю сделать
сознательную
ошибку. Изменить условие
цикла
на Up>Down. Результат - зацикливание программы.}
Mul(В, (Up + Down) Div 2, С);
Case More(Ost, C, sp) Of
0: Down := (Down + Up) Div 2;
1: Up := (Up + Down) Div 2;
2: Begin Up := (Up + Down) Div 2;
Down := Up End;
End;
End;
Mul(B, (Up +
Down) Div 2, C);
If More (Ost,
C, 0) = 0 Then Sub(Ost, C, sp)
{находим остаток от деления}
Else begin Sub (C, Ost, sp); Ost :=
C end;
FindBin := (Up
+ Down) Div 2;
{целая часть частного}
End;
Осталось
разобраться со сдвигом, значением переменной sp в нашем изложении. Опять
вернемся к обычной системе счисления и попытаемся разделить, например, 635 на
15. Что мы делаем? Вначале делим 63 на 15 и формируем, подбираем в уме первую
цифру результата. Подбирать с помощью компьютера мы научились. Подобрали — это
цифра 4, и это старшая цифра результата. Изменим остаток. Если вначале он был
635, то сейчас стал 35. Вычитать с учетом сдвига мы умеем. Опять подбираем
цифру. Вторую цифру результата. Это цифра 2 и остаток 5. Итак, результат (целая
часть) 42, остаток от деления 5. А что изменится, если основанием будет не 10,
а 10000? Логика совпадает, только в уме считать несколько труднее, но ведь у
нас же есть молоток под названием компьютер — пусть он вбивает гвозди.
Procedure MakeDel(Const А, В : TLong; Var Res, Ost : TLong);
Var sp : Integer;
Begin
Ost := A; {первоначальное значение остатка}
sp := А[0] - В[0];
If More(А, В, sp) = l Then Dec(sp);
{B * Osn > A, в результате одна цифра}
Res[0] := sp + l;
While sp >=
0 Do
Begin
{находим очередную цифру результата}
Res[sp
+ 1] := FindBin(Ost, B, sp);
Dec(sp)
End
End;
Методические
рекомендации. Представленный материал излагается на четырех занятиях по
известной схеме: 10-15-минутное изложение идей, а затем работа учащихся под
руководством преподавателя.
1-е занятие.
Ввод, вывод и сложение длинных чисел (задачи 1, 2, 3).
2-е занятие.
Функции сравнения (задача 4).
3-е занятие.
Умножение и вычитание длинных чисел (задачи 5, 6).
4-е занятие.
Деление длинных чисел (задача 7). Безусловно, эта схема не догма. В зависимости
от уровня подготовки учащихся на самостоятельное выполнение может быть вынесена
значительная часть материала. Замечу только, что в силу сложившейся традиции в
ряде случаев допускаются при изложении сознательные ошибки. В результате работы
каждый учащийся должен иметь собственный модуль для работы с
"длинными" числами.
Темы для
исследований
1. Решение
задач: поиск наибольшего общего делителя двух "длинных" чисел; поиск
наименьшего общего кратного двух "длинных" чисел; извлечение
квадратного корня из "длинного" числа и т.д.
2.
"Длинные" числа могут быть отрицательными. Как изменятся описанные
выше операции для этого случая?
3. Для хранения
"длинных" чисел используется не массив, а стек, реализованный с
помощью списка. Модифицировать модуль работы с "длинными" числами.
Список
литературы
С.М. Окулов/
"Длинная" арифметика/
Для подготовки
данной работы были использованы материалы с сайта http://www.comp-science.narod.ru/
|