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

Алгоритм Эвклида

< Лекция 1 || Лекция 9
Аннотация: Алгоритм Эвклида В начале урока дается задание на "выпускную работу", которая должна быть представлена через две недели на последнем занятии в этом учебном году. Темой урока является алгоритм нахождения наибольшего общего делителя и наименьшего общего кратного двух целых чисел. Вначале рассматривается интуитивно понятный алгоритм, использующий ранее построенные функции нахождения делителей числа. Затем объясняется эффективный алгоритм нахождения НОД двух чисел, придуманный и обоснованный Эвклидом более двух тысяч лет тому назад. Этот алгоритм считается старейшим алгоритмом, отвечающим всем требованиям, которые предъявляются к описанию алгоритма, - строго обоснованным и точно сформулированным.

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

Наши занятия в этом году закончатся 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

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

НОД и НОК. Алгоритм Эвклида

Как следует из названия, наибольшим общим делителем двух целых чисел N и M является наибольшее целое число d, которое является делителем как числа N, так и числа M. Как найти НОД?

Естественными представляются два алгоритма, назовем их Standart и StandartA. Алгоритм Standart использует возможность создания списка всех простых делителей числа N. Определяем два списка listN и listM для чисел N и M. Функция "Пересечение" списков позволяет создать список list_com, элементы которого принадлежат как списку listN так и списку listM. Функция "Произведение" в качестве результата вернет НОД, перемножив все элементы списка list_com.

Алгоритм StandartA можно построить на основе функции AllDivisors, получив делители числа N. Эти делители являются кандидатами на звание НОД. Учитывая тот факт, что при построении всех делителей они строятся упорядоченным образом. Первая пара содержит минимальный и максимальный делители – 1 и N. Следующая по построению пара (d, b) содержит делитель d, больший чем делитель d предыдущей пары, и делитель b, меньший чем делитель b предыдущей пары. Фильтр по поиску НОД устроен просто, - в цикле по числу пар проверяются делители очередной пары числа N. Если делитель b очередной пары является делителем числа M, то он и является НОД. Если среди делителей b нет делителей M, то НОД является максимальный делитель d. Таковой всегда имеется, поскольку 1 является делителем всех чисел.

Если НОД(N, M) равен 1, то числа называются взаимно простыми. У них нет общих делителей, отличных от 1.

Приведу реализацию алгоритма Standart определения НОД. Этот алгоритм видимо более сложен и менее эффективен, чем алгоритм StandartA. Он интересен тем, что построен на основе функции пересечения списков, которая может быть полезна при решении других задач, связанных со списками.

        /// <summary>
        /// Наибольший общий делитель N1 и N2
        /// </summary>
        /// <param name=&quot;N1&quot;>первое число</param>
        /// <param name=&quot;N2&quot;>второе число</param>
        /// <returns>Наибольший Общий Делитель</returns>
        public static long Standart_NOD(int N1, int N2)
        {
            //НОД является произведением (Product) элементов списка, 
            //представляющего пересечение (ListIntersect) элементов
            //списка простых делителей чисел N1 и N2
            return Product(ListIntersect(PrimeDivisors(N1), 
                PrimeDivisors(N2)));
        }

Хотя тело функции состоит из одной строчки, но простота кажущаяся, поскольку для получения результата вызываются различные функции. Эта запись представляет пример функционального стиля программирования, когда при вызове функции ее аргументы представляют вызовы других функций, которые в свою очередь могут в качестве аргументов содержать вызовы функций.

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

Альтернативой является стиль "снизу – вверх" (down up), когда вначале создаются функции нижнего уровня и на их основе создаются функции следующего уровня.

Чаще всего проектирование идет с двух сторон – снизу и сверху навстречу друг другу. В данном случае имеет место именно такая ситуация, поскольку функция PrimeDivisors уже нами создана, а функции Product и ListIntersect еще предстоит создать.

Начнем с создания простой, алгоритмически знакомой функции Product, которая находит произведение элементов списка.

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

Вот заголовок этой функции

        /// <summary>
        /// Произведение элементов списка
        /// </summary>
        /// <param name=&quot;list&quot;>список</param>
        /// <returns>произведение его элементов</returns>
        public static long Product(List<int> list)
        {
            int n = list.Count();
            long res = 1;
            for (int i = 0; i < n; i++)
                res *= list[i];
            return res;
        }

Более сложной является реализация функции Intersect, которая создает список общих элементов двух списков. Операция "Пересечение" списков является обобщением операции "Пересечение", определенной над множествами. Следует проработать лекцию из курса "Введение в логику", посвященную множествам и операциям над ними.

        /// <summary>
        /// Пересечение списков - операция Intersection
        /// </summary>
        /// <param name=&quot;list1&quot;>первый список</param>
        /// <param name=&quot;list2&quot;>второй список</param>
        /// <returns>пересечение списков</returns>
        public static List<int> ListIntersect(List<int> list1, List<int> list2)
        {
            List<int> list_res = new List<int>();
            int n1 = list1.Count;
            int item;
            for (int i = 0; i < n1; i++)
            {
                item = list1[i];		//кандидат на общий элемент
                if (list2.Contains(item))	//фильтр
                {
                    list_res.Add(item);
                    list2.Remove(item);
                }
            }
            return list_res;
        }

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

2300 лет тому назад такой алгоритм предложил Эвклид – создатель оснований науки "Геометрия". Этот алгоритм считается старейшим алгоритмом, описание и обоснование которого соответствует всем современным требованиям, предъявляемым к понятию "алгоритм".

Можно ли предложить алгоритм поиска НОД, лучший, чем алгоритм Standart или StandartA?

Эвклид заметил, что функция НОД(N, M) обладает следующими свойствами:

НОД(N, 0) = N; НОД(N, M) = НОД(M, N % M)

Сам Эвклид использовал операцию вычитания из большего числа меньшего. Он использовал немного другие аксиомы:

НОД(N, N) = N; НОД(N, M) = НОД(M, N); НОД(N, M) = НОД(M, N - M).

Достоинство варианта Эвклида в том, что здесь не нужно выполнять сложную операцию взятия остатка от деления нацело. Но при работе с компьютером проще выполнять эти операции, сокращая число итераций цикла.

Докажем, как это сделал Эвклид, справедливость этих свойств. Первое свойство очевидно. Первое число имеет наибольший делитель, равный N. Он и является наибольшим общим делителем, поскольку любое число является делителем числа 0.

Докажем справедливость второго свойства. Пусть НОД(N, M) = d. Докажем, что и остаток от деления N % M имеет тот же делитель d. Для операций деления нацело и остаток от деления нацело справедливо следующее утверждение:

$$N=M \cdot (N/M)+ N % M$$

Число N нацело делится на d, первое слагаемое нацело делится на d за счет M. Но тогда и второе слагаемое должно нацело делиться на d. Это следствие известного утверждения. Если целое число С представимо в виде суммы целых слагаемых (C = A + B) и сумма C и слагаемое A имеют общий делитель k, то и слагаемое B имеет тот же делитель k. Для доказательства достаточно разделить обе части равенства на k. Чтобы сумма слагаемых была целым числом, когда одно из слагаемых целое, то и второе слагаемое должно быть целым числом, а значит делиться на k.

Отсюда следует алгоритм Эвклида. Начинаем с пары (N, M), заменяем ее парой (M, N % M). Продолжаем этот процесс, пока второй элемент пары не станет равным 0. В этот момент первый элемент пары и будет равен НОД исходных чисел N и M. Нетрудно доказать, что поскольку пары уменьшаются, оставаясь положительными, то процесс уменьшения пар обязательно завершится.

Пример: Найти НОД(105, 84). НОД(105, 84) = НОД(84, 21) = НОД(21, 0) = 21.

Что, если первое число меньше второго?

НОД(84, 105) = НОД(105, 84) = 21.

В этом случае пара элементов меняется местами и первым становится максимальный элемент пары, после чего процесс продолжится, приводя к уменьшению чисел пары.

Реализация алгоритма достаточно прозрачна, но в данном варианте использована конструкция цикла, которая ранее у нас не встречалась:

        /// <summary>
        /// Наибольший общий делитель N1 и N2
        /// </summary>
        /// <param name=&quot;N1&quot;>первое число</param>
        /// <param name=&quot;N2&quot;>второе число</param>
        /// <returns>Наибольший Общий Делитель</returns>
        public static int Euclid_NOD(int N1, int N2)
        {
            int p = 0;
            do
            {
              p = N1 % N2; N1 = N2; N2 = p;
            } while (N2 != 0);
            return (N1);
        }

В реализации использован вариант цикла, называемый циклом dowhile. В цикле while вначале проверяется условие завершения и при условии его истинности выполняется тело цикла. В цикле do - while вначале выполняется тело цикла, а потом проверяется условие завершения и при условии его истинности тело цикла выполняется повторно.

Замечу, что наша функция будет правильно работать при условии, что N2 не равно 0. Конечно, можно написать реализацию с обычным циклом while и она всегда будет корректно работать.

А как найти НОК(N, M) – наименьшее общее кратное двух чисел? Если мы умеем вычислять НОД(N, M), то вычислить НОК(N, M) достаточно просто, используя следующее соотношение, справедливость которого нетрудно доказать:

НОК(N, M) * НОД(N, M) = N * M;

Это позволяет записать вычисление НОК(N, M) в одну строчку:

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

        /// <summary>
        /// Наименьшее общее кратное N1 и N2
        /// </summary>
        /// <param name=&quot;N1&quot;>первое число</param>
        /// <param name=&quot;N2&quot;>второе число</param>
        /// <returns>Наименьшее Общее Кратное</returns>
        public static long Euclid_NOK(int N1, int N2)
        {
            return N1 / Euclid_NOD(N1, N2) * N2; 
        }

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

Построить Windows проект по образцу:


Домашняя работа

Закончить проект. Написать реализацию НОД для алгоритма StandartA

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