Опубликован: 09.12.2017 | Доступ: свободный | Студентов: 737 / 31 | Длительность: 02:06:00
Специальности: Программист
Лекция 8:

Простые числа

< Лекция 1 || Лекция 8
Аннотация: Простые числа На этом уроке обсуждается понятие простого числа, алгоритм, позволяющий определить, является ли число простым. Рассматривается интуитивно понятный алгоритм нахождения всех простых чисел в заданном диапазоне. Подробно обсуждается эффективный алгоритм поиска простых чисел "Решето Эратосфена", придуманный более двух тысяч лет тому назад выдающимся древнегреческим ученым Эратосфеном. Как обычно показан программный проект, реализующий алгоритмы, рассмотренные в лекции.

Взгляд на задачи с вершины горы

Нахождение всех делителей числа, определение задуманной комбинации в игре "быки и коровы" можно рассматривать как частные случаи общей задачи, часто встречающейся в нашей жизни в самых разных вариациях. В чем суть этой задачи – среди множества объектов необходимо найти один или несколько объектов, обладающих необходимыми свойствами. Эта задача зачастую решается в два этапа. На первом - формируется множество кандидатов, то есть множество объектов, которые могут обладать заданными свойствами. Второй этап – это фильтр. На этом этапе устраивается перебор по множеству кандидатов, каждый из которых проходит фильтр – удовлетворяет ли кандидат заданным свойствам.

В чем сложность задачи?

Если множество кандидатов конечно, то решение задачи можно получить полным перебором, рассмотрев всех кандидатов в поисках лучшего. Множество кандидатов может быть большим. Тогда полный перебор может быть практически неосуществим. Если число кандидатов задается экспонентой, например, функцией 2n, то для человека обычно перебор невозможен уже при n, равном 10, современный обычный компьютер может справляться с задачами при n, равном 30. Но многие, самые важные для человечества задачи – создание новых образцов техники, обладающих уникальными свойствами, поиск новых эффективных лекарств, другие задачи требуют перебора куда большего количества вариантов. Вот почему так важно для страны иметь в своем арсенале суперкомпьютеры, которые могут справляться с задачами при n, равном 60, но возможности суперкомпьютеров тоже ограничены, поскольку и в обозримом будущем ни один компьютер не сможет справиться с перебором при n, равном 100.

Непростой является и задача фильтрации. Классическим примером является притча о "Буридановом осле". Буридан любил своего осла и, желая порадовать осла, поставил перед ним две торбы, - одну с ароматным сеном, другую – с отборным зерном. Оба яства были так аппетитны, что осел не мог сделать выбор и умер от голода.

Стоит перечесть "Женитьбу" Гоголя, где как нельзя лучше описана вариация данной задачи и возникающие здесь проблемы.

Сваха, отбирающая множество женихов (кандидатов в мужья), говорит невесте: "Так ухлопоталась! Зато уж каких женихов тебе припасла! То есть, и стоял свет и будет стоять, а таких еще не было!"

Невеста, решая проблему выбора, - "Право, такое затруднение — выбор! Если бы губы Никанора Ивановича да приставить к носу Ивана Кузьмича, да взять сколько-нибудь развязности, какая у Балтазара Балтазарыча, да, пожалуй, прибавить к этому еще дородности Ивана Павловича — я бы тогда тотчас же решилась".

В задачах, появляющихся в программировании, возникают подобные проблемы.

В игре "Отгадай число" задуманное число находится в интервале [0, N], где N может быть велико, что не позволяет человеку применять полный перебор. Эффективный фильтр в этой задаче, основанный на дихотомии множества кандидатов, позволяет с каждым вопросом вдвое уменьшать число кандидатов, которое достаточно скоро сводится к единственному элементу.

В игре "Быки и коровы" при задуманной комбинации длины 6, выбранной из 12 фигур, число возможных вариантов равно 126, что также исключает для человека возможность полного перебора. В данной игре построение эффективного фильтра совсем не простое дело. Магистр игры может разгадать задуманную комбинацию за меньшее число вопросов чем компьютер, использующий не самый эффективный фильтр.

В задаче о нахождении всех делителей числа N в качестве множества кандидатов естественно выбрать множество чисел из интервала [1, N]. Фильтр IsDivisor в этой задаче довольно прост, но уменьшение перебора требует хорошего математического и алгоритмического мышления. Нетривиальной догадкой является то, что это множество можно существенно сократить и свести к интервалу $[3, \sqrt{N}]$ , рассматривая в этом интервале только нечетные числа.

В задаче о разложении числа на простые делители опять-таки требуется алгоритмическое мышление для понимания того, что задачу можно свести к поиску минимального делителя. Опять-таки требуется быть немного математиком, чтобы доказать, что минимальный делитель является простым числом.

Каждая задача имеет свои особенности, свою специфику решения связанных с ней проблем. Но общий взгляд, сама постановка вопросов: "как сократить множество кандидатов", уменьшая перебор вариантов?", "как построить эффективный фильтр отбора кандидатов?", способствует нахождению эффективного решения задачи.

Простые числа

Рассмотрим алгоритмы, связанные с простыми числами. Число N простое, если у него только два делителя – 1 и само число N.

Из определения следует, что функцию IsPrime(int N) можно написать в одну строчку, если использовать ранее написанные функции.

Работа у доски

        /// <summary>
        /// n - простое число?
        /// </summary>
        /// <param name="n">число</param>
        /// <returns>true, если n простое</returns>
        public static bool IsPrime(int n)
        {
            List<int> list = AllDivisors(n);
                return list.Count == 2;
          //  List<int> list = PrimeDivisors(n);
          //  return list.Count == 1;
        }

Это решение можно записать в одну строчку, не вводя дополнительной переменной:

public static bool IsPrime(int n)
        {
            
                return AllDivisors(n).Count == 2;
          
        }

А как найти все простые числа? Постановка задачи некорректна, поскольку множество простых чисел бесконечно. Можно ли доказать этот факт? Да, и доказательство довольно простое.

Используем метод доказательства "от противного".

Предположим, что множество простых чисел конечно. Тогда существует самое большое простое числоpk. Рассмотрим число N, представляющее произведение всех простых чисел, начиная от двух до pk. Прибавим к этому произведению 1. Полученное число $N ( 2 \cdot 3 \cdot 5 \cdot \hdots \cdot pk + 1)$ является простым числом. Как ранее было показано, всякое составное число M может быть разложено на произведение простых делителей, больших 1 и меньших M. Число N нацело не делится ни на одно простое число, следовательно, оно простое и больше чем pk. Пришли к противоречию. Наше предположение о конечности множества простых чисел неверно. Множество простых чисел бесконечно.

Корректная постановка задачи: найти все простые числа в интервале [3, N]. Есть смысл искать их в этом интервале, поскольку тогда можно вести поиск только среди нечетных чисел, вдвое сокращая множество кандидатов. Возникает очевидный алгоритм. Множеством кандидатов является множество нечетных чисел в интервале [3, N]. Фильтром является построенная функция IsPrime. Перебор элементов множества кандидатов и отбор кандидатов, прошедших фильтр, дает решение задачи.

Работа у доски:

Приведу вариацию этого алгоритма, вычисляющую все простые числа в интервале [min, max].

        /// <summary>
        /// Все простые числа в интервале [min, max]
        /// </summary>
        /// <param name="min">нижняя граница</param>
        /// <param name="max">верхняя граница</param>
        /// <returns>список всех простых чисел в заданном интервале</returns>
        public static List<int> Prime_Iterator(int min, int max)
        {
            List<int> list = new List<int>();
            int cand;
            if(IsEven(min))
                cand = min + 1;
            else cand = min;
            while (cand <= max)
            {
                if (IsPrime(cand)) list.Add(cand);
                cand += 2;
            }
            return list;
        }

В нашем алгоритме довольно сложный фильтр, требующий для каждого кандидата вычисления всех его делителей. Возникает законный вопрос, а можно ли предложить лучший по эффективности алгоритм. Такой алгоритм был придуман примерно 2300 лет тому назад греческим ученым Эратосфеном.

Эратосфен считается отцом науки географии, придумал сам термин "география". Первый, кто достаточно точно вычислил размер Земли. (Как можно вычислить размер Земли?)

Руководил знаменитой библиотекой в Александрии, дружил и много лет вел переписку с Архимедом. Нашел все простые числа до 1000, придумав метод, получивший название "Решето Эратосфена".

В чем его суть. Выпишем подряд все числа от 2-х до N. (для простоты можно выписать только нечетные числа). Первое число в этом ряду (3) – простое. Пройдем по ряду с шагом, равным найденным простым числом и заменим числа нулями. Это означает, что мы удаляем из множества кандидатов, числа кратные данному простому числу. Число, следующее за ранее найденным простым числом, не равное нулю, будет следующим простым числом. Будем повторять этот процесс, пока ряд не будет исчерпан. Алгоритм основан на том факте, что минимальное число, которое не делится на предшествующие ему простые числа, является простым.

Алгоритм назван "Решетом", поскольку Эратосфен записывал числа на восковой табличке и, удаляя кратные числа, просто прокалывал их, оставляя дырки. В результате "просеивания" в решете оставались только простые числа.

Вот возможная реализация этого алгоритма:

        /// <summary>
        /// Решето Эратосфена
        /// Формирование списка всех простых чисел
        /// в интервале [3,N]
        /// </summary>
        /// <param name="N"></param>
        /// <returns>список простых чисел в заданном интервале</returns>
        public static List<int> Eratosfen_Sieve(int N)
        {
            List<int> simple_numbers = new List<int>();
            int m = (int)((N - 3) / 2) + 1; //размер массива нечетных чисел
            int[] odd_numbers = new int[m];
            simple_numbers.Add(2);      //первое простое число
            int i = 0;
            while (i < m)
            {
                odd_numbers[i] = 2 * i + 3; //заполняем массив нечетными числами
                i++;
            }

            int k = 0;
            int last;
            int j;
            while (k < m)        //цикл по числу простых чисел
            {
                while (k < m && odd_numbers[k] == 0) k++; //пропускаем нули
                if (k < m)
                {
                    last = odd_numbers[k];  //следующее простое число
                    simple_numbers.Add(last);
                    //просеивание кратных last
                    j = k + last;
                    while (j < m)
                    {
                        odd_numbers[j] = 0;
                        j += last;
                    }
                    k++;
                }
            }
            return simple_numbers;
        } 

Работа в классе:

Создайте проект по следующему образцу:


Обратите внимание, пользователю выводится не только список простых чисел в заданном интервале, но и плотность простых чисел и время, затраченное алгоритмом, вычисляющим список простых чисел. Оба алгоритма строят один и тот же список, но алгоритм Эратосфена работает в 200 раз быстрее алгоритма Итератора! (Молодец Эратосфен!)

Приведу обработчик события "Click" для кнопки "Eratosphen Sieve":

        private void buttonSieve_Click(object sender, EventArgs e)
        {
            DateTime start, finish;
            double sieve_time;
            textBoxMessageSieve.Text = "";
            int max = int.Parse(textBoxMax.Text);
            int min = int.Parse(textBoxMin.Text);
            start = DateTime.Now;
                List<int> simple_numbers = 
                Numbers.Eratosfen_Sieve(max);
            finish = DateTime.Now;
            sieve_time = (finish - start).TotalMilliseconds;
            listBoxSimple.Items.Clear();
            int N = simple_numbers.Count;
            int n = 0;
            for (int i = 0; i < N; i++)
                if (simple_numbers[i] >= min)
                {
                    listBoxSimple.Items.Add(simple_numbers[i]);
                    n++;
                }

            double density = Convert.ToDouble(n) / (max- min);
            string mes1 = "В интервале [" + min.ToString()  +
              " - " +   max.ToString() +
                "] простых чисел - " + n.ToString() + "\r\n";
            string mes2 = "Плотность простых чисел = " + 
                density.ToString() + "\r\n";
            string mes3 = "Время вычислений Решетом Эратосфена (миллисекундах) = " 
                + sieve_time.ToString();
            textBoxMessageSieve.Text = mes1 + mes2 + mes3;
        }

Домашнее задание: Закончить проект, начатый в классе.

Выпускная работа

Наши занятия в этом году закончатся 25 апреля через две недели. На последнем занятии желательно рассмотреть выпускную работу, которую нужно попытаться выполнить. Работа не простая, и не у всех она может получиться. Но попытаться выполнить ее следует.

Задание 1

Написать игру "One-armed gangster" - "Однорукий бандит".

Суть игры: При нажатии кнопки "Игра" появляются 3 картинки из заданного набора 12 картинок. Картинки могут быть те же, что и в игре "Быки и коровы". Выбор картинок генерируется случайным образом. Выигрыш игрока возможен в трех случаях:

  • Все три картинки совпадают (цена выигрыша – N, например, 100 рублей);
  • Совпадают цвета у всех картинок (цена выигрыша – M, например, 20 рублей);
  • Совпадают геометрические фигуры – три круга или три ромба ((цена выигрыша – M, например, 20 рублей);

За каждую игру с игрока взимается плата P (например, 10 рублей).

Вначале игрок вносит некоторую сумму и играет до тех пор, пока не проиграет все внесенные деньги или не нажмет кнопку Stop.

Указание: Разбор игры "Быки и коровы"" поможет написать игру "однорукий бандит". Тем не менее задача остается довольно сложной для начинающих программистов.

Задание 2

Написать функцию GoodPupil. На вход функции подаются два массива Fam и Marks. Массив Fam содержит фамилии учеников, массив Marks – их оценки по математике и информатике. Функция GoodPupil в качестве результата возвращает список учеников, которые имеют оценки 4 или 5 по обоим предметам.

Приведу заголовок функции GoodPupil:

List<string> GoodPupil(string[] Fam, int[]Marks]

(Пример исходных данных: Fam = {"Петров В. А", "Михайлов Н. В", "Чистяков А. П."} Marks = {5, 3, 4, 4, 5, 4}. В результате должен быть сформирован список, содержащий двух учеников – Михайлова и Чистякова)

Задание 3

Реализовать один из проектов, рассмотренных в течение курса

< Лекция 1 || Лекция 8