Теория чисел и язык Haskell: различия между версиями

Содержимое удалено Содержимое добавлено
вычитка, правки
Строка 1:
<small>Исходный текст опубликован в [[Журнал «Потенциал»|журнале «Потенциал»]] ([[Участник:Dark_Magus|Душкин Р. В.]] «Теория чисел и язык Haskell»)</small>
 
Эта статья продолжает рассматривать функциональный язык программирования [[w:Haskell|Haskell]], изучение которого начато в статье А.&nbsp;В.&nbsp;Ворожцова «[[Язык Haskell: О пользе и вреде лени|«Язык&nbsp;''Haskell'': О&nbsp;пользе и&nbsp;вреде лени»]]». В статье рассматриваются некоторые интересные задачи из теории чисел и предлагаются численные способы их решения. Методология представления функций на языке ''Haskell'' основана на их представление в виде, наиболее близком к математическим формулам.
 
==Введение==
 
[[w:Теория чисел|Теория чисел]] &mdash; это одно из направлений математики, которое иногда называют «высшей арифметикой». Данная наука изучает натуральные числа и некоторые сходные с ними объекты, рассматривает различные свойства (делимость, разложимость, взаимосвязи и&nbsp;т.&nbsp;д. так далее), алгоритмы поиска чисел, а также определяет ряд достаточно интересных наборов натуральных чисел.
 
Так, к примеру, в рамках теории чисел рассматриваются вопросы делимости целых чисел друг на друга, [[w:алгоритмАлгоритм Евклида|алгоритм Евклида]] для поиска наибольшего общего делителя, поиск наименьшего общего кратного, [[w:малаяМалая теорема Ферма|малая]] и [[w:Великая теорема Ферма|большая]] теоремы Ферма. В качестве самых известных рядов натуральных чисел можно привести [[w:Числа Фибоначчи|ряд Фибоначчи]], простые числа, совершенные и дружественные числа, степени и суперстепени натуральных чисел.
 
С другой стороны, в рамках функционального программирования существуют различные методы для вычисления значений сложных формул. А если рассмотреть мощь и выразительность современных функциональных языков программирования, то становится очевидным, что изучать на практике различные аспекты теории чисел можно при помощи программирования формул на каком-нибудь функциональном языке.
 
В этой статье в качестве функционального языка программирования, при помощи которого можно рассматривать теорию чисел, предлагается использовать ''Haskell'' как уже зарекомендовавший себя язык для использования в науке и прикладных технологиях. Более того, тот язык используется в качестве первого языка программирования в некоторых университетах мира, поэтому его рассмотрение для решения задач из теории чисел имеет ещё и практическую цель &mdash; дать читателю навыки работы с этим языком.
 
Для работы с функциями, приведёнными в этой статье, необходимо использовать интерпретатор языка ''Haskell'' [[HUGS 98|HUGS&nbsp;98]], бесплатную версию которого можно достать на http://www.haskell.org/hugs/. Все рассмотренные в статье функции протестированы на этом интерпретаторе, поэтому правильность их определения гарантируется автором. Использование других трансляторов языка ''Haskell'' (например, компилятора GHC) также возможно, однако для их использования, возможно, придётся вносить в определения функций незначительные изменения.
 
Предполагается, что читатель прочитал и понял основы языка ''Haskell'', изложенные в статье «[[Язык Haskell: О пользе и вреде лени]]», поэтому далее синтаксис поясняется редко. Если вам трудно понять синтаксис или смысл приводимых определений, можно обратиться к курсу «[[Основы функционального программирования]]», находящемуся в Викиучебнике и основанному на курсе лекций [[w:Московский инженерно-физический институт|МИФИ]].
 
Автор будет признателен любым отзывам и комментариям к статье, которые помогут сделать дальнейшие статьи более интересными и полезными для читателей. Отзывы можно посылать электронной почтой по адресу [mailto:darkus.14@gmail.com darkus.14@gmail.com].
 
==Простейшие задачи==
Строка 23:
Прежде чем рассматривать сложные вопросы теории чисел, следует понять и подготовить основной набор функций, требуемых для вычисления более сложных формул: в первую очередь, функции для нахождения наибольшего общего делителя&nbsp;(НОД) и наименьшего общего кратного&nbsp;(НОК).
 
НОД двух целых чисел <math>m</math> и <math>n</math> — это такой общий делитель <math>d</math> (то есть <math>d \mid m</math> и <math>d \mid n</math>), который делится на любой другой общий делитель исходных чисел. НОД определён, если хотя бы одно из чисел <math>m</math> или <math>n</math> не ноль. Обозначение — <math>(m, n)</math>. Для вычисления этого числа можно воспользоваться функцией <code>gcd</code> (от английского наименования «greatest common divisor»):
НОД двух целых чисел <math>m</math> и <math>n</math> &mdash; это такой общий делитель
<math>d</math> (т.&nbsp;е.: <math>d \mid m</math> и <math>d \mid n</math>, который
делится на любой другой общий делитель исходных чисел. НОД
определён, если хотя бы одно из чисел <math>m</math> или <math>n</math> не ноль.
Обозначение &mdash; <math>(m, n)</math>. Для вычисления этого числа
можно воспользоваться функцией <tt>gcd</tt> (от английского
наименования «greatest common divisor»):
 
<code>gcd :: Integral a => a -> a -> a
Строка 37 ⟶ 31 :
gcd' m n = gcd' n (rem m n)</code>
 
В этом определении использованы функция <code>abs</code>, вычисляющая модуль заданного целого числа, а также функция <code>rem</code>, которая возвращает остаток от целочисленного деления первого аргумента на второй. Данная функция реализует алгоритм Евклида вычисления НОД, разработаный ещё в древней Греции.
В этом определении использованы функция <tt>abs</tt>, вычисляющая
модуль заданного целого числа, а также функция <tt>rem</tt>,
которая возвращает остаток от целочисленного деления первого
аргумента на второй. Данная функция реализует алгоритм Евклида
вычисления НОД, разработаный ещё в древней Греции.
 
Необходимо напомнить, что строка с символами (<code>::</code>) является определением типа функции, тело которой определяется в следующей строке. То есть такая строка определяет ''сигнатуру функции''. В ней используются два специальных символа: <code>=></code> и <code>-></code>. Первый задаёт контекст использования переменных типа <code>a</code> в дальнейшей записи (в указанном примере — переменная&nbsp;<code>a</code>). В функции <code>gcd</code> аргументы могут быть любого типа <code>a</code>, являющегося экземпляром класса <code>Integral</code>, то есть классом чисел, для которых определены операции целочисленного деления и взятия остатка от деления.
Необходимо напомнить, что строка с символами (<tt>::</tt>) является
определением типа функции, тело которой определяется в следующей
строке. То есть такая строка определяет ''сигнатуру функции''. В ней используются
два специальных символа: <tt>=></tt> и <tt>-></tt>. Первый
задаёт контекст использования переменных типа <tt>a</tt> в дальнейшей записи (в
указанном примере &mdash; переменная&nbsp;<tt>a</tt>). В функции <tt>gcd</tt>
аргументы могут быть любого типа <tt>a</tt>, являющегося экземпляром
класса <tt>Integral</tt>, то есть классом чисел, для которых
определены операции целочисленного деления и взятия остатка от
деления.
 
Стрелка <ttcode>-></ttcode> используется для определения типа функций. Так, к примеру, запись типа функции «<code>Integer -> Bool</code>» гласит, что функция принимает на вход один параметр типа
<code>Integer</code>, а возвращает результат типа <code>Bool</code>. В свою очередь, запись типа «<code>Integer -> Char -> Bool</code>» говорит, что у функции есть два аргумента: первый типа
Так, к примеру, запись типа функции «<tt>Integer -> Bool</tt>»
<code>Integer</code>, а второй — типа <code>Char</code>. Возвращает функция значение типа <code>Bool</code>.
гласит, что функция принимает на вход один параметр типа
<tt>Integer</tt>, а возвращает результат типа <tt>Bool</tt>. В свою
очередь, запись типа «<tt>Integer -> Char -> Bool</tt>»
говорит, что у функции есть два аргумента: первый типа
<tt>Integer</tt>, а второй &mdash; типа <tt>Char</tt>. Возвращает
функция значение типа <tt>Bool</tt>.
 
Подробно о типизации функций, классах типов и параметрических переменных типов написано в лекциях по функциональному программированию. Детальное рассмотрение этих аспектов функционального программирования выходит за рамки рассмотрения этой статьи.
переменных типов написано в лекциях по функциональному
программированию. Детальное рассмотрение этих аспектов
функционального программирования выходит за рамки рассмотрения этой
статьи.
 
НОК двух целых чисел <math>m</math> и <math>n</math> &mdash; это такое наименьшее целое число,
которое делится на <math>m</math> и <math>n</math> без остатка. Обозначение &mdash; <math>[m,
n]</math>. Для вычисления этого числа можно воспользоваться функцией
<tt>lcm</tt> (от английского наименования «least common
multiple»):
 
НОК двух целых чисел <math>m</math> и <math>n</math> — это такое наименьшее целое число, которое делится на <math>m</math> и <math>n</math> без остатка. Обозначение — <math>[m, n]</math>. Для вычисления этого числа можно воспользоваться функцией <code>lcm</code> (от английского наименования «least common multiple»):
 
<code>lcm :: Integral a => a -> a -> a
Строка 81 ⟶ 48 :
lcm m n = abs ((quot m (gcd m n)) * n)</code>
 
Здесь также встречается уже рассмотренная функция <code>abs</code>, а также функция <code>quot</code>, возвращающая значение целочисленного деления первого аргумента на второй. Как видно, НОК вычисляется достаточно просто — необходимо разделить первый аргумент на НОД двух чисел, а потом результат деления умножить на второй аргумент.
Здесь также встречается уже рассмотренная функция <tt>abs</tt>, а
также функция <tt>quot</tt>, возвращающая значение целочисленного
деления первого аргумента на второй. Как видно, НОК вычисляется
достаточно просто &mdash; необходимо разделить первый аргумент на НОД
двух чисел, а потом результат деления умножить на второй аргумент.
 
Написанные функции можно использовать для построения бесконечного списка взаимно простых чисел. Два целых числа называются взаимно простыми, если их НОД равен 1. Для вычисления такого списка чисел можно воспользоваться следующей функцией:
списка взаимно простых чисел. Два целых числа называются взаимно
простыми, если их НОД равен 1. Для вычисления такого списка чисел
можно воспользоваться следующей функцией:
 
<code>reciprocals :: Integral a => [(a, a)]
reciprocals = [(m, n) | m <- [1..], n <- [1..m], gcd m n == 1]</code>
 
Как видно, это определение полностью соответствует математической формуле, по которой можно было бы вычислить множество пар взаимно простых чисел:
формуле, по которой можно было бы вычислить множество пар взаимно
простых чисел:
 
<math>\mathbb{N}_{reciprocals} = \{ \langle m, n \rangle \mid m \in \mathbb{N}, n \in \mathbb{N}, (m, n) = 1 \}</math>
 
Данная функция не очень интересна с точки зрения её реализации. Всё довольно типично — определитель списка с двумя генераторами (выражения со знаком (<code><-</code>)) и одним охраняющим выражением (условное выражение <code>gcd m n == 1</code>). Здесь интересно другое. Ведь приведённое определение функции, хотя и является таким простым, на самом деле содержит два скрытых вложенных цикла и одну проверку. Циклы соответствуют генераторам, при этом первый цикл выполняется от единицы до бесконечности, а второй — от единицы до значения переменной первого цикла. Это важно, так как если бы в определении функции стояло бы два одинаковых генератора: <code>m <- [1..]</code> и <code>n <- [1..]</code>, то результаты работы были бы не так интересны — первый элемент любой пары в полученном бесконечном списке всегда был бы равен единице. Читателю предлагается самостоятельно подумать, почему это так.
Данная функция не очень интересна с точки зрения её реализации. Всё
довольно типично &mdash; определитель списка с двумя генераторами
(выражения со знаком (<tt><-</tt>)) и одним охраняющим выражением
(условное выражение <math>gcd m n == 1</math>). Здесь интересно
другое. Ведь приведённое определение функции, хотя и является таким
простым, на самом деле содержит два скрытых вложенных цикла и одну
проверку. Циклы соответствуют генераторам, при этом первый цикл
выполняется от единицы до бесконечности, а второй &mdash; от единицы до
значения переменной первого цикла. Это важно, т.&nbsp;к. если бы в
определении функции стояло бы два одинаковых генератора:
<tt>m <- [1..]</tt> и <tt>n <- [1..]</tt>, то
результаты работы были бы не так интересны &mdash; первый элемент любой
пары в полученном бесконечном списке всегда был бы равен единице.
Читателю предлагается самостоятельно подумать, почему это так.
 
Необходимо отметить, что функции <code>gcd</code> и <code>lcm</code> определены в стандартном модуле <code>Prelude</code>, поэтому их определение здесь приведено исключительно в познавательных целях. При разработке собственных программ эти функции можно использовать непосредственно без дополнительного определения.
Необходимо отметить, что функции <tt>gcd</tt> и <tt>lcm</tt>
определены в стандартном модуле <tt>Prelude</tt>, поэтому их
определение здесь приведено исключительно в познавательных целях.
При разработке собственных программ эти функции можно использовать
непосредственно без дополнительного определения.
 
Одним из самых ключевых понятий теории чисел является понятие делителя. Очень многие целочисленные последовательности, в том числе и те, которые будут рассмотрены далее, определяются через делители. Поэтому было бы интересно иметь функцию для получения списка делителей заданного числа. Пусть такая функция называется <code>divisors</code>:
Одним из самых ключевых понятий теории чисел является понятие
делителя. Очень многие целочисленные последовательности, в том числе
и те, которые будут рассмотрены далее, определяются через делители.
Поэтому было бы интересно иметь функцию для получения списка
делителей заданного числа. Пусть такая функция называется
<tt>divisors</tt>:
 
<code>divisors :: Integral a => a -> [a]
divisors n = [x | x <- [1..(n - 1)], rem n x == 0]</code>
 
Необходимо отметить, что данная функция возвращает список так называемых собственных делителей числа <math>n</math>, то есть такие, которые строго меньше самого числа <math>n</math>. Таким образом, в результат этой функции не входит само число <math>n</math> — это свойство будет использоваться в некоторых случаях в последующих определениях функций.
Необходимо отметить, что данная функция возвращает список т.&nbsp;н.
собственных делителей числа <math>n</math>, т.&nbsp;е. такие, которые строго
меньше самого числа <math>n</math>. Таким образом, в результат этой функции
не входит само число <math>n</math> &mdash; это свойство будет использоваться в
некоторых случаях в последующих определениях функций.
 
==Такие непростые простые числа==
 
Очень широкую известность в рамках теории чисел имеют простые числа, то есть такие, в списке собственных делителей которых находится только один делитель — 1. Такие числа нашли самое широкое применение во многих прикладных областях, в том числе и в современных методах и алгоритмах шифрования информации. Кроме того, простые числа успешно используются в хеш-таблицах и для генерации
Очень широкую известность в рамках теории чисел имеют простые
числа, т.&nbsp;е. такие, в списке собственных делителей которых
находится только один делитель &mdash; 1. Такие числа нашли самое
широкое применение во многих прикладных областях, в том числе и в
современных методах и алгоритмах шифрования информации. Кроме того,
простые числа успешно используются в хеш-таблицах и для генерации
псевдослучайных чисел.
 
К сожалению, в математике не придумано простой формулы для нахождения заданного по порядку простого числа, поэтому построение списка простых чисел делается перебором с применением всевозможных эвристических правил проверки на простоту. К множеству таких правил относится, например, решето Эратосфена — алгоритм нахождения при помощи перебора всех простых чисел до некоторого заданного <math>n</math>.
К сожалению, в математике не придумано простой формулы для
нахождения заданного по порядку простого числа, поэтому построение
списка простых чисел делается перебором с применением всевозможных
эвристических правил проверки на простоту. К множеству таких правил
относится, например, решето Эратосфена &mdash; алгоритм нахождения при
помощи перебора всех простых чисел до некоторого заданного <math>n</math>.
 
Проще всего находить простые числа перебором:
 
<code>primes = [n | n <- [1..], isPrime n]
where isPrime x = (divisors x == [1])</code>
 
Однако данный алгоритм весьма несовершенен. Незачем перебирать все числа, тем более, что из чётных чисел только число&nbsp;2 является простым. Однако далее получается, что и все числа, кратные трём, не являются простыми, а там и кратные пяти тоже и так далее. Это и есть известное «решето» Эратосфена — хорошо бы было его внедрить для построения бесконечного списка простых чисел. Читателю предлагается самостоятельно подумать над этой проблемой.
Однако данный алгоритм весьма несовершенен. Незачем перебирать все
числа, тем более, что из чётных чисел только число&nbsp;2 является
простым. Однако далее получается, что и все числа, кратные трём,
не являются простыми, а там и кратные пяти тоже и&nbsp;т.&nbsp;д. Это
и есть известное «решето» Эратосфена &mdash; хорошо бы было его внедрить для
построения бесконечного списка простых чисел. Читателю предлагается
самостоятельно подумать над этой проблемой.
 
Описанная функция <code>primes</code> вполне подходит для решения многих задач в рамках теории чисел. Хотя она работает долго, но зато вполне надёжно, вычисляя действительно бесконечный список простых чисел. Чтобы получить ограниченный список простых чисел, не превышающих заданного <math>n</math>, необходимо воспользоваться стандартной функцией <code>take</code>, которая возвращает заданное количество элементов с начала списка (например, команда «<code>take 1000 primes</code>» вернёт список из тысячи первых простых чисел). А для того, чтобы получить определённое простое число, можно воспользоваться функцией (<code>!!</code>), возвращающей заданный элемент списка: «<code>primes !! 1000</code>» вернёт тысячное простое число.
Описанная функция <tt>primes</tt> вполне подходит для решения многих
задач в рамках теории чисел. Хотя она работает долго, но зато вполне
надёжно, вычисляя действительно бесконечный список простых чисел.
Чтобы получить ограниченный список простых чисел, не превышающих
заданного <math>n</math>, необходимо воспользоваться стандартной функцией
<tt>take</tt>, которая возвращает заданное количество элементов с начала
списка (например, команда «<tt>take 1000 primes</tt>» вернёт
список из тысячи первых простых чисел). А для того, чтобы получить
определённое простое число, можно воспользоваться функцией
(<tt>!!</tt>), возвращающей заданный элемент списка:
«<tt>primes !! 1000</tt>» вернёт тысячное простое число.
 
Всем вышеперечисленным можно воспользоваться для того, чтобы написать функцию, возвращающую разложение заданного натурального числа на простые делители. По основной теореме арифметики такое разложение существует, и оно единственное (с точностью до порядка следования простых делителей). Такое представление натурального числа в виде произведения простых называется факторизацией. На настоящий момент неизвестно алгоритмов факторизации чисел с полиномиальной сложностью, хотя и не доказано, что таковых алгоритмов нет. На гипотезе о том, что факторизовать произвольное число не так просто, основан алгоритм шифрования с открытым ключом RSA.
Всем вышеперечисленным можно воспользоваться для того, чтобы
написать функцию, возвращающую разложение заданного натурального
числа на простые делители. По основной теореме арифметики такое
разложение существует, и оно единственное (с точностью до порядка
следования простых делителей). Такое представление натурального
числа в виде произведения простых называется факторизацией. На
настоящий момент неизвестно алгоритмов факторизации чисел с
полиномиальной сложностью, хотя и не доказано, что таковых
алгоритмов нет. На гипотезе о том, что факторизовать произвольное
число не так просто, основан алгоритм шифрования с открытым ключом
RSA.
 
Таким образом, функция для факторизации заданного числа хотя и будет весьма неоптимизированной, но, тем не менее, вполне будет работать, особенно для несложных составных чисел, которые равны произведению преимущественно маленьких простых чисел. Её определение выглядит так:
будет весьма неоптимизированной, но, тем не менее, вполне будет
работать, особенно для несложных составных чисел, которые равны
произведению преимущественно маленьких простых чисел.
Её&nbsp;определение
выглядит так:
 
<code>expansion :: Integer -> [Integer]
Строка 204 ⟶ 96 :
x = head [y | y <- primesBN, mod n y == 0]</code>
 
Данная функция будет работать медленнее для чисел, которые раскладываются на большие простые числа. Так, к примеру, число <math>1\;000\;000</math> (миллион) раскладывается на простые множители за доли секунды (<math>[2, 2, 2, 2, 2, 2, 5, 5, 5, 5, 5, 5]</math>), а вот следующее за ним число <math>1\;000\;001</math> (миллион один) факторизуется примерно за полминуты (<math>[101, 9, 901]</math>). Читателю предлагается самостоятельно изучить зависимость времени исполнения приведённого алгоритма факторизации от величины аргумента.
Данная функция будет работать медленнее для чисел, которые
раскладываются на большие простые числа. Так, к примеру, число
<math>\mbox{1 000 000}</math> (миллион) раскладывается на простые множители за
доли секунды (<math>[2, 2, 2, 2, 2, 2, 5, 5, 5, 5, 5, 5]</math>),
а вот следующее за ним число <math>\mbox{1 000 001}</math> (миллион один)
факторизуется примерно за полминуты (<math>[101, 9 901]</math>).
Читателю предлагается самостоятельно изучить зависимость времени
исполнения приведённого алгоритма факторизации от величины
аргумента.
 
Доказано, что ряд простых чисел бесконечен. Однако среди всех таких чисел имеются так называемые числа-близнецы, то есть такие, которые отличаются друг от друга на&nbsp;2. Неизвестно, сколько таких пар, и бесконечно ли их количество вообще. Простейшее описание функции, вычисляющей такие числа, выглядит так:
чисел имеются т.&nbsp;н. числа-близнецы, т.&nbsp;е. такие, которые
отличаются друг от друга на&nbsp;2. Неизвестно, сколько таких пар, и
бесконечно ли их количество вообще. Простейшее описание функции,
вычисляющей такие числа, выглядит так:
 
<code>twins :: [(Integer, Integer)]
twins = [(p, p + 2) | p <- primes, isPrime (p + 2)]</code>
 
Кроме чисел-близнецов можно вводить ряды пар простых чисел, отличающихся друг от друга на 4, на 6 и так далее. Такие ряды имеют обобщённое наименование родственных простых чисел. Для поиска пар родственных чисел можно написать функцию, параметризуемую разницей, которая должна быть между родственными числами. Определение такой функции может выглядеть так:
Кроме чисел-близнецов можно вводить ряды пар простых чисел,
отличающихся друг от друга на 4, на 6 и&nbsp;т.&nbsp;д. Такие ряды имеют
обобщённое наименование родственных простых чисел. Для поиска пар
родственных чисел можно написать функцию, параметризуемую
разницей, которая должна быть между родственными числами.
Определение такой функции может выглядеть так:
 
<code>kins :: Integer -> [(Integer, Integer)]
kins n = [(p, p + n) | p <- primes, isPrime (p + n)]</code>
 
В этом случае определение функции для поиска простых чисел-близнецов выглядит очень просто:
выглядит очень просто:
 
<code>twins :: [(Integer, Integer)]
twins = kins 2</code>
 
Однако несмотря на всю кажущуюся простоту простых чисел, математики очень любят их. И в связи с этим постоянно ищут различные свойства, которые характеризуют простые числа. Более того, для некоторых целей выделяются особые простые числа, которые получают свои собственные имена, часто по имени своего первооткрывателя. У всех таких подмножеств простых чисел имеются собственные области применения.
Однако несмотря на всю кажущуюся простоту простых чисел, математики
очень любят их. И в связи с этим постоянно ищут различные свойства,
которые характеризуют простые числа. Более того, для некоторых
целей выделяются особые простые числа, которые получают свои
собственные имена, часто по имени своего первооткрывателя. У всех
таких подмножеств простых чисел имеются собственные области
применения.
 
===Числа Мерсенна===
 
Так, к примеру, в XVII&nbsp;17-м веке французский математик М.&nbsp;Мерсенн определил последовательность чисел вида:
определил последовательность чисел вида:
 
<math>M_{n}M_n = 2^{n} - 1</math>
 
Эта последовательность получила наименование «чисел Мерсенна». Сама по себе она не так интересна, но в ней существуют так называемые простые числа Мерсенна, которые получили свою известность в связи с эффективным критерием простоты Люка — Лемера, благодаря которому простые числа Мерсенна давно удерживают лидерство как самые большие известные простые числа. На данный момент самым большим известным простым числом является число Мерсенна <math>M_{30402457} = 2^{30402457} - 1</math>, найденное в декабре 2005&nbsp;года. Оно содержит <math>9152052</math> десятичных цифры.
Эта последовательность получила наименование «чисел Мерсенна».
Сама по себе она не так интересна, но в ней существуют т.&nbsp;н. простые
числа Мерсенна, которые получили свою известность в связи с
эффективным критерием простоты Люка&nbsp;&mdash;&nbsp;Лемера, благодаря
которому простые числа Мерсенна давно удерживают лидерство как самые
большие известные простые числа. На данный момент самым
большим известным простым числом является число Мерсенна
<math>M_{30402457} = 2^{30402457} - 1</math>, найденное в декабре
2005&nbsp;года. Оно содержит <math>9152052</math> десятичных цифры.
 
Эффективный тест простоты (тест Люка&nbsp;—&nbsp;Лемера) для чисел Мерсенна был предложен в 1878&nbsp;году и базируется на том наблюдении, что простота числа Мерсенна <math>M_p = 2^p - 1</math> влечёт простоту его индекса <math>p</math>, а также на следующем утверждении: «для простого <math>p</math> число <math>M_p</math> является простым тогда и только тогда, когда оно делит число <math>L_{p - 1}</math>, где числа <math>L_k</math> определяются рекуррентным соотношением — <math>L_1 = 4</math>, <math>L_{k + 1} = L_k^2 - 2</math>».
Эффективный тест простоты (тест Люка&nbsp;&mdash;&nbsp;Лемера) для чисел
Мерсенна был предложен в 1878&nbsp;году и базируется на том наблюдении,
что простота числа Мерсенна <math>M_{p} = 2^{p} - 1</math> влечёт
простоту его индекса <math>p</math>, а также на следующем утверждении: «для
простого <math>p</math> число <math>M_{p}</math> является простым тогда и только тогда,
когда оно делит число <math>L_{p - 1}</math>, где числа <math>L_{k}</math> определяются
рекуррентным соотношением &mdash; <math>L_{1} = 4</math>, <math>L_{k + 1} =
L_{k}^{2} - 2</math>».
 
Для установления простоты <math>M_p</math> последовательность чисел <math>L_1,\ L_2,\ \ldots,\ L_{p - 1}</math> достаточно вычислять по модулю числа <math>M_p</math> (то есть вычислять не сами числа <math>L_k</math>, длина которых растёт экспоненциально, а остатки от деления <math>L_k</math> на <math>M_p</math>, длина которых ограничена <math>p</math> битами). Последнее число в этой последовательности <math>L_{p - 1} \mod M_p</math> называется вычетом Люка&nbsp;— Лемера. Таким образом, число Мерсенна является простым тогда и только тогда, когда число <math>p</math> — простое и вычет Люка&nbsp;— Лемера равен нулю.
Для установления простоты <math>M_{p}</math> последовательность чисел
<math>L_{1}, L_{2}, \ldots, L_{p - 1}</math> достаточно вычислять по
модулю числа <math>M_{p}</math> (т.&nbsp;е. вычислять не сами числа <math>L_{k}</math>, длина
которых растёт экспоненциально, а остатки от деления <math>L_{k}</math> на
<math>M_{p}</math>, длина которых ограничена <math>p</math> битами). Последнее число в
этой последовательности <math>L_{p - 1} \mod M_{p}</math>
называется вычетом Люка&nbsp;&mdash;&nbsp;Лемера. Таким образом, число
Мерсенна является простым тогда и только тогда, когда число <math>p</math> &mdash;
простое и вычет Люка&nbsp;&mdash;&nbsp;Лемера равен нулю.
 
Для вычисления последовательности простых чисел Мерсенна можно воспользоваться следующими несложными функциями (необходимо в очередной раз заметить, что данные определения весьма далеки от оптимизированного варианта — они лишь показывают, насколько определения функций на языке Haskell похожи на математические формулы):
Для вычисления последовательности простых чисел Мерсенна можно
воспользоваться следующими несложными функциями (необходимо в
очередной раз заметить, что данные определения весьма далеки от
оптимизированного варианта &mdash; они лишь показывают, насколько
определения функций на языке ''Haskell'' похожи на математические
формулы):
 
<code>lucas :: (Num a, Num b) => b -> a
lucas 1 = 4
lucas n = (lucas (n - 1))^2 - 2
 
mersenne :: [Integer]
mersenne = [m p | p <- primes, rem (lucas (p - 1)) (m p) == 0]
where m p = 2^p - 1</code>
 
При помощи функции <ttcode>mersenne</ttcode> во время написания автором данного подраздела статьи вычислены первые семь простых чисел Мерсенна:
подраздела статьи вычислены первые семь простых чисел Мерсенна:
 
<ttcode>[7, 31, 127, 8 191, 131 071, 524 287, 2 147 483 647]</ttcode>.
 
===Числа Ферма===
 
Эксцентричный учёный П.&nbsp;Ферма, придумавший малую и большую теоремы своего имени, которые долгое время мучили математиков (а большая и вовсе до 1994 года не была доказана {{Ref|Note}}, также интересовался простыми числами, в связи с чем пытался разработать формулу, при помощи которой можно было бы оные вычислять. После некоторых усилий он получил такое соотношение:
Эксцентричный учёный П.&nbsp;Ферма, придумавший малую и
большую
теоремы своего имени, которые долгое время мучили математиков (а
большая и вовсе до 1994&nbsp;г. не была доказана {{Ref|Note}},
также интересовался простыми числами, в связи с чем пытался разработать
формулу, при помощи которой можно было бы оные вычислять.
После некоторых усилий он получил такое соотношение:
 
<math>F_{n}F_n = 2^{2^{n}} + 1</math>
 
Сам П.&nbsp;Ферма смог проверить простоту чисел из данной последовательности только до <math>n = 4</math>. Далее он просто предположил, что остальные числа из этого ряда тоже простые. Однако в 1732&nbsp;году Л.&nbsp;Эйлер нашёл разложение числа <math>F_5</math>. На сегодняшний день известно, что все числа Ферма для <math>5 \leqslant n \leqslant 32</math> являются составными. Бо́льшие числа из этого ряда на простоту пока не проверены.
Сам П.&nbsp;Ферма смог проверить простоту чисел из данной
последовательности только до <math>n = 4</math>. Далее он просто
предположил, что остальные числа из этого ряда тоже простые.
Однако в 1732&nbsp;году Л.&nbsp;Эйлер нашёл разложение числа <math>F_{5}</math>. На
сегодняшний день известно, что все числа Ферма для <math>5 \leq
n \leq 32</math> являются составными. Б\'ольшие числа из этого ряда на
простоту пока не проверены.
 
Для проверки простоты чисел Ферма используется тест Пепина, являющийся [[w:Полиномиальная вычислимость|полиномиальным]]. Данный тест утверждает, что число Ферма <math>F_n</math> является простым тогда и только тогда, когда <math>3^{\frac{F_n - 1}{2}} \equiv -1 (\mod F_n)</math>.
являющийся [[w:Полиномиальная вычислимость|полиномиальным]]. Данный тест утверждает, что число Ферма
<math>F_{n}</math> является простым тогда и только тогда, когда
<math>3^{\frac{F_{n} - 1}{2}} \equiv -1 (\mod F_{n})</math>.
 
Читателю рекомендуется самостоятельно реализовать функцию или набор функций для осуществления теста Пепина для простых чисел Ферма.
функций для осуществления теста Пепина для простых чисел Ферма.
 
===Числа Софи Жермен===
 
Софи Жермен доказала большую теорему Ферма для показателей <math>n</math>, являющихся простыми числами <math>p</math> такими, что числа <math>2p + 1</math> также простые. Тем самым подмножество таких простых чисел получило наименование чисел Софи Жермен. Неизвестно, является ли эта последовательность бесконечной, хотя предполагается, что это так.
являющихся простыми числами <math>p</math> такими, что числа <math>2p + 1</math>
также простые. Тем самым подмножество таких простых чисел получило
наименование чисел Софи Жермен. Неизвестно, является ли эта
последовательность бесконечной, хотя предполагается, что это так.
 
Для вычисления пар чисел Софи Жермен можно воспользоваться следующей функцией:
функцией:
 
<code>germain :: [(Integer, Integer)]
Строка 347 ⟶ 164 :
===Другие последовательности простых чисел===
 
Существует ещё большое количество различных подмножеств простых чисел, которые используются как для развлечения, так и для некоторых прикладных аспектов науки и техники. Так, к примеру, выведены формулы для простых чисел имени Вильсона (на сегодняшний день известно три таких числа) и имени Вольстенхольма (известно два таких числа). В англоязычных математических справочниках вводится до шестидесяти различных типов простых чисел, многие из которых используются в доказательствах тех или иных теорем.
Существует ещё большое количество различных подмножеств простых
чисел, которые используются как для развлечения, так и для некоторых
прикладных аспектов науки и техники. Так, к примеру, выведены
формулы для простых чисел имени Вильсона (на сегодняшний день
известно три таких числа) и имени Вольстенхольма (известно два
таких числа). В англоязычных математических справочниках вводится
до шестидесяти различных типов простых чисел, многие из которых
используются в доказательствах тех или иных теорем.
 
Особый интерес у учёных, занимающихся теорией чисел, вызывают так называемые факториальные простые числа. Факториальным называется такое простое число, которое отличается на единицу в ту или иную сторону от факториала некоторого натурального числа: <math>n! \pm 1</math>. Эти числа интересны тем, что сигнализируют своим присутствием о начале или конце длинной последовательности составных чисел. Для получения бесконечного списка таких простых чисел можно воспользоваться следующими функциями:
Особый интерес у учёных, занимающихся теорией чисел, вызывают т.&nbsp;н.
факториальные простые числа. Факториальным называется такое
простое число, которое отличается на единицу в ту или иную сторону
от факториала некоторого натурального числа: <math>n! \pm 1</math>.
Эти числа интересны тем, что сигнализируют своим присутствием о
начале или конце длинной последовательности составных чисел. Для
получения бесконечного списка таких простых чисел можно
воспользоваться следующими функциями:
 
<code>fact :: (Num a, Enum a) => a -> a
fact n = product [1..n]
 
fp :: [Integer]
fp = [p | n <- test, p <- n, isPrime p]
where test = [[x-1, x+1] | x <- map fact [1..]]</code>
 
Функция <ttcode>product</ttcode> из стандартного модуля <ttcode>Prelude</ttcode> возвращает произведение элементов переданного ей в качестве аргумента списка.
возвращает произведение элементов переданного ей в качестве
аргумента списка.
 
С другой стороны простые числа можно использовать и для развлечения. Например, в ряду простых чисел можно искать такие, которые читаются одинаково в обе стороны (в десятичной системе счисления) — числа-палиндромы. Такие палиндромы можно также искать и среди простых чисел в различных системах счисления. Читателю предлагается самостоятельно разработать функцию для проверки того, что заданное число является палиндромом и реализовать бесконечный список простых чисел-палиндромов.
С другой стороны простые числа можно использовать и для
развлечения. Например, в ряду простых чисел можно искать такие,
которые читаются одинаково в обе стороны (в десятичной системе
счисления) &mdash; числа-палиндромы. Такие палиндромы можно также
искать и среди простых чисел в различных системах счисления.
Читателю предлагается самостоятельно разработать функцию для
проверки того, что заданное число является палиндромом и реализовать
бесконечный список простых чисел-палиндромов.
 
==Совершенству нет предела==
 
Другим широким классом чисел являются так называемые совершенные числа, которые равны сумме всех своих собственных делителей. Совершенных чисел очень мало. В натуральном ряду до одного миллиона встречается только четыре таких числа, а до триллиона — всего шесть. Проще всего искать такие числа при помощи следующей функции:
Другим широким классом чисел являются т.&nbsp;н.
совершенные числа, которые равны сумме всех своих собственных
делителей. Совершенных чисел очень мало. В натуральном ряду до
одного миллиона встречается только четыре таких числа, а до
триллиона &mdash; всего шесть. Проще всего искать такие числа при
помощи следующей функции:
 
<code>perfects :: [Integer]
perfects = [n | n <- [1..], sum (divisors n) == n]</code>
 
Функция <code>sum</code> из стандартного модуля <code>Prelude</code> возвращает сумму элементов переданного ей в качестве аргумента списка. Однако данное определение весьма несовершенно. Если обратиться к теории чисел, то в ней можно найти доказательство того, что чётные совершенные числа и числа Мерсенна, рассмотренные в предыдущем разделе, связаны друг с другом простым соотношением:
Функция <tt>sum</tt> из стандартного модуля <tt>Prelude</tt>
возвращает сумму элементов переданного ей в качестве аргумента
списка. Однако данное определение весьма несовершенно. Если
обратиться к теории чисел, то в ней можно найти доказательство того,
что чётные совершенные числа и числа Мерсенна, рассмотренные в
предыдущем разделе, связаны друг с другом простым соотношением:
 
<math>P_{p}P_p = 2^{p - 1} *\cdot (2^{p} - 1)</math>,
 
где число <math>2^{p} - 1</math> является простым (числом Мерсенна). Таким образом, каждому чётному совершенному числу соответствует число Мерсенна и наоборот.
Мерсенна). Таким образом, каждому чётному совершенному числу
соответствует число Мерсенна и наоборот.
 
Это соотношение нашёл ещё древнегреческий математик Евклид, а строго доказал Л.&nbsp;Эйлер. Однако не доказано, существуют ли нечётные совершенные числа, потому данное соотношение необходимо использовать с осторожностью. Известно лишь, что если нечётное совершенное число существует, то оно должно превышать <math>10^{300}</math>.
доказал Л.&nbsp;Эйлер. Однако не доказано, существуют ли нечётные совершенные
числа, потому данное соотношение необходимо использовать с
осторожностью. Известно лишь, что если нечётное совершенное число
существует, то оно должно превышать&nbsp;<math>10^{300}</math>.
 
Используя указанное соотношение, можно реализовать функцию, вычисляющую совершенные числа не медленным перебором, а достаточно быстро:
вычисляющую совершенные числа не медленным перебором, а достаточно
быстро:
 
<code>perfects :: [Integer]
Строка 425 ⟶ 201 :
m n = 2^n - 1</code>
 
Дополнительно проверить получаемые числа можно при помощи предиката <code>isPerfect</code>:
предиката <tt>isPerfect</tt>:
 
<code>isPerfect :: Integral a => a -> Bool
isPerfect n = sum (divisors n) == n</code>
 
Однако математики не остановились на такой формулировке. В обиход было введено понятие дружественных чисел, которые связаны друг с другом таким соотношением, при котором первое число в паре равно сумме собственных делителей второго числа, а второе — сумме собственных делителей первого соответственно. Таким образом совершенные числа являются дружественными по отношению к самим себе. Список пар дружественных чисел можно получить при помощи следующей функции:
Однако математики не остановились на такой формулировке. В обиход
было введено понятие дружественных чисел, которые связаны друг с
другом таким соотношением, при котором первое число в паре равно
сумме собственных делителей второго числа, а второе &mdash; сумме
собственных делителей первого соответственно. Таким образом
совершенные числа являются дружественными по отношению к самим себе.
Список пар дружественных чисел можно получить при помощи следующей
функции:
 
<code>friends :: [(Integer, Integer)]
Строка 445 ⟶ 213 :
sum (divisors n) == m]</code>
 
Как уже упоминалось, совершенные числа очень редки. Для того, чтобы поле для исследований в этом направлении было немного шире, математики ввели некоторые дополнительные определения целочисленных последовательностей: недостаточное число и избыточное число. К недостаточным относятся такие натуральные числа, сумма собственных делителей которых меньше самого числа. Соответственно, к избыточным относятся числа, сумма собственных делителей которых больше самого числа. Таким образом, весь класс натуральных чисел может быть разделён на три непересекающихся подмножества — недостаточных, совершенных и избыточных чисел. А каждое натуральное число в свою очередь находится в одном из этих трёх подмножеств.
Как уже упоминалось, совершенные числа очень редки. Для того,
чтобы поле для исследований в этом направлении было немного шире,
математики ввели некоторые дополнительные определения целочисленных
последовательностей: недостаточное число и избыточное число. К
недостаточным относятся такие натуральные числа, сумма собственных
делителей которых меньше самого числа. Соответственно, к
избыточным относятся числа, сумма собственных делителей которых
больше самого числа. Таким образом, весь класс натуральных чисел
может быть разделён на три непересекающихся подмножества &mdash;
недостаточных, совершенных и избыточных чисел. А каждое натуральное
число в свою очередь находится в одном из этих трёх подмножеств.
 
Но и этого адептам математики показалось мало. Были введены слегка недостаточные и слегка избыточные числа. Такие числа отличаются от совершенных в ту или иную сторону. Однако при определении таких множеств математиков ждало некоторое разочарование. Если использовать следующие функции для поиска таких чисел:
недостаточные и слегка избыточные числа. Такие числа отличаются
от совершенных в ту или иную сторону. Однако при определении таких
множеств математиков ждало некоторое разочарование. Если
использовать следующие функции для поиска таких чисел:
 
<code>imperfects :: [Integer]
imperfects = [n | n <- [1..], sum (divisors n) == n - 1]
 
excesses :: [Integer]
excesses = [n | n <- [1..], sum (divisors n) == n + 1]</code>
 
то будет ясно, что первая функция возвращает список степеней числа 2, а вторая функция не выдаёт вообще никакого результата. Так и получилось — в математике до сих пор неизвестно, существуют ли иные слегка недостаточные числа, кроме степеней двойки, а также существуют ли в принципе слегка избыточные числа.
2, а вторая функция не выдаёт вообще никакого результата. Так и
получилось &mdash; в математике до сих пор неизвестно, существуют ли
иные слегка недостаточные числа, кроме степеней двойки, а также
существуют ли в принципе слегка избыточные числа.
 
Читателю рекомендуется самостоятельно разработать функции для генерации бесконечных списков недостаточных и избыточных чисел, а также разработать функцию высшего порядка, при помощи которой можно получить все пять перечисленных классов натуральных чисел — совершенные, избыточные и слегка избыточные, недостаточные и слегка недостаточные числа.
Читателю рекомендуется самостоятельно разработать функции для
генерации бесконечных списков недостаточных и избыточных чисел, а
также разработать функцию высшего порядка, при помощи которой можно
получить все пять перечисленных классов натуральных чисел &mdash;
совершенные, избыточные и слегка избыточные, недостаточные и слегка
недостаточные числа.
 
==Заключение==
 
Теория чисел — занимательная наука. На ней основаны многие интересные аспекты прикладных технологий в самых различных областях науки и техники. Знание основ теории чисел помогает разрабатывать более оптимизированные алгоритмы в вычислительных задачах, а также успешно применять численные методы при решении различных задач при помощи вычислительной техники. Более того, теория чисел позволяет быть более внимательным к различным числовым последовательностям, развивает умение находить скрытые взаимосвязи в казалось бы хаотических множествах чисел. Всё это, в свою очередь, самым благотворным образом сказывается на развитии интеллектуальных способностей человека.
Теория чисел &mdash; занимательная наука. На ней основаны многие
интересные аспекты прикладных технологий в самых различных областях
науки и техники. Знание основ теории чисел помогает разрабатывать
более оптимизированные алгоритмы в вычислительных задачах, а также
успешно применять численные методы при решении различных задач при
помощи вычислительной техники. Более того, теория чисел позволяет
быть более внимательным к различным числовым последовательностям,
развивает умение находить скрытые взаимосвязи в казалось бы
хаотических множествах чисел. Всё это, в свою очередь, самым
благотворным образом сказывается на развитии интеллектуальных
способностей человека.
 
В интернете можно найти [http://www.research.att.com/njas/sequences/ энциклопедию целочисленных последовательностей] (на английском языке), в которой представлена информация более чем о ста тысячах различных конечных и бесконечных последовательностей, состоящих из целых чисел. Данную энциклопедию можно использовать, в том числе, и для самостоятельного создания новых задач в рамках теории чисел для последующего решения методами функционального программирования.
В интернете можно найти
[http://www.research.att.com/njas/sequences/ энциклопедию целочисленных последовательностей] (к сожалению, на английском
языке), в которой представлена информация более чем о ста тысячах различных
конечных и бесконечных последовательностей, состоящих из целых чисел. Данную
энциклопедию можно использовать, в том числе, и для самостоятельного создания
новых задач в рамках теории чисел для последующего решения методами
функционального программирования.
 
Все перечисленные в этой статье определения функций сведены в отдельный модуль, который каждый желающий может получить, послав письмо с запросом автору статьи по адресу электронной почты
[mailto:darkus.14@gmail.com darkus.14@gmail.com].
отдельный модуль, который каждый желающий может получить, послав
письмо с запросом автору статьи по адресу электронной почты
darkus.14@gmail.com.
 
==Примечания==
 
#{{Note|Note}} Данная теорема утверждает, что для любого целого <math>n > 2</math> уравнение <math>a^{n} + b^{n} = c^{n}</math> не имеет положительных целых решений <math>a</math>, <math>b</math> и <math>c</math>. Сам П.&nbsp;Ферма доказал теорему для <math>n = 4</math>, затем Л.&nbsp;Эйлер доказал её для <math>n =
#{{Note|Note}} Данная теорема утверждает, что для любого целого <math>n > 2</math> уравнение <math>a^n + b^n = c^n</math> не имеет положительных целых решений <math>a</math>, <math>b</math> и <math>c</math>. Сам П.&nbsp;Ферма доказал теорему для <math>n = 4</math>, затем Л.&nbsp;Эйлер доказал её для <math>n = 3</math>, а позже И.&nbsp;Дирихле привёл доказательство для <math>n = 5</math>. Окончательно доказали теорему только в 1994&nbsp;году для любого <math>n</math>.
 
==Ссылки==
* [http://www.haskell.org www.haskell.org]
* [http://potential.org.ru/bin/view/Home/JourDt200601000000PHJ1 Журнал «Потенциал», №1№ 1, 2006]
 
[[Категория:Журнал_Журнал «Потенциал»]]
[[Категория:Функциональное программирование]]
[[Категория:информатикаИнформатика в журнале «Потенциал»]]