/
Tags: языки программирования компьютерные технологии
ISBN: 978-5-9775-1882-6
Text
Павел Добряк
Санкт-Петербург
«БХВ-Петербург»
2024
УДК 004.43
ББК 32.973.26-018.1
Д57
Добряк П. В.
Д57
Python. Красивые задачи для начинающих. — СПб.: БХВ-Петербург,
2024. — 352 с.: ил. — (Для начинающих)
ISBN 978-5-9775-1882-6
В книге приведены примеры решения различных практических задач на языке
Python и предложено детальное пошаговое описание процесса написания программы для каждой из них. Подобраны задачи, которые имеют несколько вариантов
решений и формируют алгоритмическое мышление. Показаны основы структурного, динамического, объектно-ориентированного, функционального программирования. Приведены способы работы с функциями, алгоритмы поиска в длину, в ширину, бэктрекинга, рекурсии. Материал расположен по возрастанию сложности,
код программ снабжен комментариями, разъясняющими все языковые конструкции Python.
Для начинающих программистов
УДК 004.43
ББК 32.973.26-018.1
Группа подготовки издания:
Руководитель проекта
Зав. редакцией
Научный редактор
Редактор
Компьютерная верстка
Дизайн серии
Оформление обложки
Павел Шалин
Людмила Гауль
Михаил Бабич
Григорий Добин
Ольги Сергиенко
Марины Дамбиевой
Зои Канторович
"БХВ-Петербург", 191036, Санкт-Петербург, Гончарная ул., 20
ISBN 978-5-9775-1882-6
© Добряк П. В., 2024
© Оформление. ООО "БХВ-Петербург",
ООО "БХВ", 2024
Оглавление
Предисловие ..................................................................................................................... 7
На кого рассчитана эта книга?...................................................................................................... 10
Структура книги............................................................................................................................. 11
Благодарности ................................................................................................................................ 12
Глава 1. Условия ........................................................................................................... 13
1.1. Квадратное уравнение, комплексные корни ........................................................................ 13
Задача ...................................................................................................................................... 13
1.2. Кирпич и дыра в стене............................................................................................................ 18
Задача ...................................................................................................................................... 18
Версия 1 ......................................................................................................................... 19
Версия 2 ......................................................................................................................... 20
Версия 3 ......................................................................................................................... 21
Глава 2. Структурное программирование ............................................................... 23
2.1. Полупроходной балл .............................................................................................................. 23
Задача 1 ................................................................................................................................... 23
Версия 1 ......................................................................................................................... 24
Версия 2 ......................................................................................................................... 25
Задача 2 ................................................................................................................................... 28
Версия 3 ......................................................................................................................... 28
Версия 4 ......................................................................................................................... 29
Задача 3 ................................................................................................................................... 31
Версия 5 ......................................................................................................................... 32
2.2. Метеостанция .......................................................................................................................... 33
Задача ...................................................................................................................................... 33
Версия 1 ......................................................................................................................... 34
Версия 2 ......................................................................................................................... 37
2.3. Седловина матрицы ................................................................................................................ 40
Задача ...................................................................................................................................... 40
2.4. Максимальный квадрат в матрице, заполненный нулями................................................... 43
Задача ...................................................................................................................................... 43
2.5. Чемпионат по игре в тетрис ................................................................................................... 46
Задача ...................................................................................................................................... 46
4
Оглавление
2.6. Проверка правильности расстановки скобок ....................................................................... 51
Задача ...................................................................................................................................... 51
Глава 3. Функции .......................................................................................................... 57
3.1. Решето Эратосфена и числа-близнецы ................................................................................. 57
Задача ...................................................................................................................................... 57
3.2. Решето Сундарама .................................................................................................................. 65
Задача ...................................................................................................................................... 65
3.3. Лесенка .................................................................................................................................... 67
Задача ...................................................................................................................................... 67
3.4. Линейный и медианный фильтры ......................................................................................... 73
Задача 1 ................................................................................................................................... 73
Задача 2 ................................................................................................................................... 84
3.5. Алгоритм Евклида .................................................................................................................. 85
Задача ...................................................................................................................................... 85
3.6. Гипероператоры ...................................................................................................................... 91
Задача ...................................................................................................................................... 91
3.7. Ханойские башни.................................................................................................................. 102
Задача .................................................................................................................................... 102
3.8. Отображения списков........................................................................................................... 109
Задача .................................................................................................................................... 109
Глава 4. Поиск в длину и ширину, бэктрекинг,
динамическое программирование ........................................................................... 119
4.1. Лабиринт ............................................................................................................................... 119
Задача 1 ................................................................................................................................. 120
Версия 1 ....................................................................................................................... 120
Версия 2 ....................................................................................................................... 125
Версия 3 ....................................................................................................................... 130
Версия 4 ....................................................................................................................... 134
Задача 2 ................................................................................................................................. 139
4.2. Задача о восьми ферзях ........................................................................................................ 143
Задача .................................................................................................................................... 143
Версия 1 ....................................................................................................................... 143
Версия 2 ....................................................................................................................... 156
4.3. Поиск индекса элемента в списке ....................................................................................... 161
Задача .................................................................................................................................... 161
Версия 1 ....................................................................................................................... 161
Версия 2 ....................................................................................................................... 162
Версия 3 ....................................................................................................................... 163
4.4. Сжатие строки ....................................................................................................................... 167
Задача .................................................................................................................................... 167
4.5. Укладка рюкзака ................................................................................................................... 176
Задача .................................................................................................................................... 176
4.6. Подпоследовательность максимальной длины .................................................................. 180
Задача .................................................................................................................................... 181
Версия 1 ....................................................................................................................... 181
Версия 2 ....................................................................................................................... 184
Оглавление
5
Версия 3 ....................................................................................................................... 186
Версия 4 ....................................................................................................................... 191
4.7. Палиндром наибольшей длины ........................................................................................... 199
Задача .................................................................................................................................... 199
4.8. Гиперсфера ............................................................................................................................ 208
Задача .................................................................................................................................... 209
Глава 5. Объектно-ориентированное программирование .................................. 219
5.1. Графы с помощью словарей ................................................................................................219
Задача 1 ................................................................................................................................. 220
Задача 2 ................................................................................................................................. 220
Задача 3 ................................................................................................................................. 224
Задача 4 ................................................................................................................................. 225
Задача 5 ................................................................................................................................. 226
Задача 6 ................................................................................................................................. 227
Задача 7 ................................................................................................................................. 228
Задача 8 ................................................................................................................................. 228
5.2. Родословное древо................................................................................................................ 230
Задача 1 ................................................................................................................................. 230
Задача 2 ................................................................................................................................. 238
Задача 3 ................................................................................................................................. 243
5.3. Период в числовой последовательности ............................................................................ 252
Задача .................................................................................................................................... 252
Версия 1 ....................................................................................................................... 252
Версия 2 ....................................................................................................................... 253
Версия 3 ....................................................................................................................... 254
5.4. Треугольник Паскаля и сочетания ...................................................................................... 257
Задача 1 ................................................................................................................................. 257
Задача 2 ................................................................................................................................. 259
Отступление про функторы ....................................................................................... 262
5.5. Гиперкуб в многомерном пространстве ............................................................................. 266
Задача .................................................................................................................................... 266
Глава 6. Функциональное программирование ..................................................... 287
6.1. Интеграл ................................................................................................................................ 288
Задача .................................................................................................................................... 288
6.2. Отображения, сохраняющие внутреннюю структуру........................................................ 298
Общая задача ........................................................................................................................ 298
Задача 1 ................................................................................................................................. 299
Задача 2 ................................................................................................................................. 302
Задача 3 ................................................................................................................................. 304
6.3. Цепочки функций ................................................................................................................. 305
Задача .................................................................................................................................... 305
6.4. Монады .................................................................................................................................. 309
Задача .................................................................................................................................... 309
6.5. Карринг .................................................................................................................................. 319
Задача .................................................................................................................................... 319
6.6. Функторы .............................................................................................................................. 331
Задача .................................................................................................................................... 331
Выводы по главам 3, 4 и 6 .......................................................................................................... 342
6
Оглавление
Глава 7. Сюрреализм .................................................................................................. 345
7.1. Фрактальные списки............................................................................................................. 345
7.2. Фрактальный словарь ........................................................................................................... 346
7.3. Бесконечные вызовы функции ............................................................................................ 346
7.4. Функтор с бесконечными вызовами ................................................................................... 347
Заключение ................................................................................................................... 349
Предметный указатель .............................................................................................. 351
s
Предисловие
Язык программирования Python1 стремительно набирает популярность для разработки самых различных приложений, потеснив другие языки общего назначения.
А при обучении языкам программирования он стал безусловным лидером. Причина
этой популярности — лаконичность, простота и выразительность Python.
Многие вспомогательные действия (например, сортировка списка чисел по возрастанию), которые раньше занимали целый «абзац» кода, на Python укладываются
в одну строчку. В отличие от других языков программирования, на Python программисту не надо высматривать идею, спрятанную за множеством строк кода.
В результате, глядя на решение задач на Python, программист сразу видит суть алгоритма.
Кроме того, многие «технические детали» разработки программ можно теперь поначалу вообще не объяснять начинающим программистам или объяснять мимоходом. К ним относится, например, выделение памяти для списков объектов, работа
с большими числами и типы данных. В отличие от других языков, Python на самом
деле стал языком высокого уровня, спрятав глубоко эти технические нюансы.
А раньше на них приходилось тратить несколько уроков.
По правде говоря, перейдя на обучение Python, я теперь вспоминаю обучение на
С++, а тем более на Pascal, с содроганием. Но то, что школьники и студенты массово перешли на изучение Python, не означает, что они научились программировать...
По-прежнему остро стоит вечная проблема выбора методики обучения и учебников.
Есть много новомодных концепций обучения: проектное обучение (когда команду
учеников сразу кидают на решение большой практической задачи), обучение как
игра, наука как развлечение, обучение во сне... (последнее — шутка, я не знаю тех,
кто во сне научился программировать, но это не значит, что на этом нельзя сделать
деньги). Зная историю педагогики, могу сказать, что были попытки под другими
названиями внедрить эти концепции еще сто лет назад. Все они провалились и
были признаны «педагогическими извращениями». Но обучению программированию далеко не сто лет. Может быть, эти концепции победят в ИТ-сфере?
1
Правильно его читать как «Па́йтон» — с ударением на первом слоге.
8
Предисловие
Массовое обучение программированию проходило на моих глазах, когда я был
сперва школьником и студентом, а потом университетским преподавателем, репетитором и преподавателем на курсах повышения квалификации, получив опыт преподавания языков программирования как групповой, так и индивидуальный среди
школьников, студентов и взрослых людей. Информатика тесно связана с математикой, а в математике, как известно, «нет царских путей». В этом смысле я консерватор и считаю, что обучение программированию, как и математике, не может быть
легким, оно требует умственных усилий, а порой и очень серьезного напряжения
ума. А научившись программировать, вы станете уже другим человеком. Поэтому
если вам говорят, что вас обучат программировать легко и играючи, боюсь, что вас
дурачат.
Как же действительно научиться программировать? По моему мнению, только решая задачи: от простых к сложным, сначала учебные, а потом и практические.
Лучше еще ничего не придумали.
Что же я могу рекомендовать своим ученикам? Есть классическая книга создателя
языка программирования Pascal Никлауса Вирта «Алгоритмы и структуры данных»
и многотомник Дональда Кнута «Искусство программирования». «Вот бы перевести их на Python!» — подумал я. Но потом понял, что это плохая идея.
Многотомник не может стать учебником. Он может быть справочным пособием,
пособием для обучения олимпиадному программированию, книгой уже для профессионалов, которые хотят углубить свои знания по отдельным темам.
Теперь что касается перевода на Python «Алгоритмов и структур данных». Дело
в том, что современное программирование на языках высокого уровня — это не
только знание алгоритмов и структур данных, но еще и понимание разных стилей
(парадигм программирования). Python обладает таким набором средств выразительности, что один и тот же алгоритм можно запрограммировать очень поразному — так, что будет казаться, будто программа написана на разных языках.
И простой перевод программ в книге Вирта на Python познакомил бы читателя
только с одним стилем. Кажется, что этого уже было бы достаточно, ведь вы бы
научились программировать. Но программирование сегодня — это командная
работа. И труд программиста — это не только писать свой код «с нуля», но и разбираться в чужих программах. И кто знает, с какими стилями программирования
вам придется иметь дело!
Получается, что сейчас будущим программистам очень не хватает фундаментальной книги «Алгоритмы, структуры данных и парадигмы программирования». Но
такая книга противоречила бы «духу времени» и «духу Python». Дело в том, что она
была бы очень толстой! А программы на Python очень лаконичны. Должен ли быть
толстым учебник по Python?
Занимаясь преподавательской деятельностью, я понимал, что очень ограничен во
времени. У меня есть три месяца, полгода, год, максимум — два года, чтобы научить моего учащегося программировать. И я не могу долго объяснять ему одну
тему. Да и ученикам это будет не интересно. Чтобы соответствовать «духу времени», нужно очень быстро познакомить ученика со всем спектром возможностей
Предисловие
9
программирования на Python, причем так, чтобы он мог решать задачи. А потом
уже углублять знания, возможно, сделав еще несколько заходов по всем темам, но
на других задачах. То есть «духу времени» соответствует циклическое обучение.
Каждому циклу обучения нужна своя книга. Может быть, стоит посмотреть, какие
книги на Python есть в книжных магазинах, выбрать несколько из них и заниматься
по ним? Книг по Python большое разнообразие, и в этом есть свой смысл.
Просмотрев книги по программированию, ориентированные на решение задач,
и множество статей в Интернете, я понял, что у них есть один серьезный недостаток. В них, как правило, излагается идея алгоритма, а потом приводится код программы. Но в реальности код программы не возникает сразу. Программист пишет
код постепенно, по шагам, допуская типичные ошибки, отлаживая программу.
И очень хочется видеть не только готовый код с пояснениями, но и то, как он «рождался», чтобы научиться программировать самому.
Итак, мои требования к идеальной книге по программированию:
1. Циклическое обучение, каждому циклу — своя книга.
2. Небольшой объем, лаконичность книги.
3. Ориентир на решение задач.
4. Доходчивое объяснение идеи алгоритма.
5. Пошаговое изложение того, как создается программа.
6. Несколько алгоритмов, если задачу можно решить разными способами.
7. Для одного и того же алгоритма писать программу в разных стилях программирования.
Не удовлетворившись поисками книг по Python, которые полностью соответствовали бы моей преподавательской деятельности, я начал писать книги сам.
Для первой своей книги «Python. 12 уроков для начинающих»1 я выбрал основные
языковые конструкции Python, сгруппировал их по темам по возрастанию сложности и для каждой темы подобрал минимально необходимое количество задач, чтобы ученик усвоил тему.
Но у меня осталась подборка решений множества красивых задач, которые не вошли в эту книгу. Их я решал со своими учениками, если у нас было много времени,
задавал их как домашние задания или использовал для повторения изученного материала на следующий год (во второй цикл обучения). Кроме того, в первую книгу
не вошли такие важные алгоритмы, как поиск в длину и поиск в ширину. Эти задачи я перенес в свою вторую книгу «Python. Красивые задачи для начинающих»,
которую вы держите перед собой.
Но мне не хотелось писать простое дополнение или продолжение первой книги.
Я хотел написать самостоятельное произведение, сделав книгу доступной для начинающих. Поэтому я также расположил материал по темам и возрастанию слож1
См. https://bhv.ru/product/python-12-urokov-dlya-nachinayushhih/.
10
Предисловие
ности и снабдил код программ комментариями, вновь разъясняющими все языковые конструкции Python по мере их появления.
Я назвал книгу «Python. Красивые задачи для начинающих». Но какие задачи
я считаю красивыми? Те, которые:
1. Можно решить разными способами и в разных стилях программирования.
2. Можно обобщить, усложнить, придумать продолжение.
3. Идеи которых можно использовать при решении множества других задач.
4. Наконец, есть ведь и интуитивное представление о красоте. Разве не красивы,
например, Ханойские башни?
В общем, я приглашаю читателя насладился интеллектуальной красотой задач этой
книги — красотой постановки задач, идей алгоритмов и программ.
На кого рассчитана эта книга?
Книга написана для читателей разного уровня погружения в программирование.
Обучение программированию «с нуля».
Если вы в своей жизни до сих пор не написали ни одной самой простой учебной
программы (даже не знаете, что такое программа «Привет, мир!»), то, скорее
всего, эта книга не для вас. Но если у вас есть способности, например, к математике, если у вас развито логическое мышление или у вас есть инженерное или
техническое образование, то вы можете попробовать. Но рекомендую вам иметь
какой-нибудь простой учебник для самых-самых новичков — например, есть
бесплатный учебник на сайте https://pythonworld.ru/samouchitel-python. Вы
можете изучить какую-нибудь тему из этого учебника, а потом закрепить материал по первой моей книге «Python. 12 уроков для начинающих» или даже по
этой.
Уже есть представление/опыт изучения Python.
Если вы уже пробовали изучать Python самостоятельно, в школе, университете
или на курсах и знаете (хотя, возможно, уже подзабыли) основные языковые
конструкции языка, то эта книга вам хорошо подходит. С ее помощью вы освежите в памяти языковые конструкции Python (хотя для этого лучше подойдет
моя первая книга «Python. 12 уроков для начинающих») и научитесь писать
алгоритмы разного уровня сложности.
Умеете программировать на других языках программирования.
Книга хорошо для вас подойдет. Код программ снабжен комментариями, поясняющими синтаксис языка, и вы легко выучите Python. А разбирая задачи,
улучшите свои алгоритмические способности.
Хотите подготовиться к олимпиадам по программированию или к собеседованиям при приеме на работу.
Книга подойдет вам идеально. Задачи, в нее включенные, — это классика программирования, а их знание — это признак хорошего ИТ-образования. Приемы,
Предисловие
11
которые используются для решения этих задач, часто встречаются в олимпиадных задачах. Решение задач из этой книги — отличная тренировка ума.
Структура книги
Книга разбита на главы в соответствии с языковыми конструкциями, которые нужны для решения задач. При этом все то, что встречалось в предыдущих главах, становится нужным в следующих (рис. П.1).
Рис. П.1. Главы и языковые конструкции
Задачи также расположены с увеличением сложности:
1. Первая глава — это проверка, понимает ли читатель вообще, что такое алгоритм, и может ли он программировать условия.
2. Вторая глава — структурное программирование. Здесь используются циклы,
ветвления и встроенные в Python коллекции. Алгоритмы просты, и читатель,
у которого сформировано алгоритмическое мышление, способен написать их
самостоятельно.
3. В третьей главе к структурному программированию добавляются функции. Поначалу можно вполне обойтись без них (просто программа станет плохо читаемой), но потом без функций уже не написать программу никак. Алгоритмы здесь
не очень сложные, однако читатель уже может с ними самостоятельно не справиться, поэтому здесь перед программированием излагается идея алгоритма.
4. В четвертой главе приводятся задачи, которые решаются с помощью алгоритмов
поиска в длину, в ширину, бэктрекинга, рекурсии, динамического программирования. Здесь не добавляются новые языковые конструкции, а отрабатываются
крупные приемы программирования, которые можно назвать типами алгоритмов. Это хорошие задачи для подготовки к олимпиадам или к собеседованиям.
5. Пятая глава посвящена объектно-ориентированному программированию, но
многие задачи решаются самыми разнообразными средствами.
6. В шестой главе в первом разделе на примере вычисления интеграла детально
объясняются простые приемы функционального программирования. Остальные
же разделы сосредоточены на обманчиво простой задаче отображения коллек-
12
Предисловие
ции с сохранением ее сложной структуры, что делается с помощью вереницы
функций. На этой задаче отрабатываются такие сложные приемы функционального программирования, как функторы и монады.
7. Короткая седьмая глава абсурдна. Она посвящена даже не алгоритмам, а лаконичным программам, которые работают удивительными способами и при этом
совершенно бесполезны, по крайней мере, на современном этапе развития программирования. Цель главы — развлечь читателя.
Структура каждого раздела содержит формулировку задачи, языковые конструкции
и приемы программирования, далее излагается идея алгоритма и приводится пошаговое программирование — то, как программист реально пишет программу (это
является главным достоинством книги). Если в программе используются новые,
еще не описанные ранее языковые конструкции, то к коду приводится примечание
с разъяснением этих языковых конструкций.
Если строить обучение по этой книге, то, за рядом исключений, одному полуторачасовому занятию соответствует один раздел. Таким образом, на изучение материала книги понадобится минимум 36, а обычно, по опыту, до 50 занятий.
Благодарности
Я благодарю многих учеников, а особенно своих детей Андрея и Артема Добряков,
которым «досталось от меня» больше всех моих задач, Виктора Запорожца, занимавшегося со мной как школьником, так и студентом, и Александра Ефимова, который пришел ко мне уже во взрослом возрасте. Александр «заставил» меня объяснять алгоритмы еще более понятно, чем я делал до этого. И, конечно, спасибо моей
жене Евгении Хрущевой, которая не только поправила мой русский язык, вычитывая мои книги, но еще и нашла пару ошибок в коде. Я благодарен редактору Григорию Добину. До того, как он скрупулезно прочитал мою книгу, я и не представлял,
какой это огромный труд.
ГЛАВА
1
Условия
Эта глава — «разминочная». Мы решим две задачи на применение условий, чтобы
начать изучать Python, вспомнить его основы (если вы их уже знаете) или перейти
к программированию на Python с других языков программирования.
1.1. Квадратное уравнение, комплексные корни
Задача
Вводятся действительные числа a, b, c. Найти корни квадратного уравнения:
ax2 + bx + c = 0 .
Языковые конструкции: условия, вложенные и составные условия, математические формулы.
Ход программирования
Шаг 1. Квадратное уравнение решается по формуле
−b ± b 2 − 4ac
.
2a
Проблема задачи в том, что надо учесть все возможные случаи вводимых чисел.
Начнем анализ формулы. Видно, что при a = 0 происходит деление на ноль. Но это
не значит, что при a = 0 нет корней (просто уравнение станет линейным). В зависимости от a будут две ветки решения (листинг 1.1.1).
x1,2 =
Листинг 1.1.1
a=float(input())
b=float(input())
c=float(input())
if a!=0:
pass
else:
pass
14
Глава 1
П РИМЕЧАНИЯ К КОДУ
Здесь и далее в примечаниях будут делаться комментарии к коду (пояснения к синтаксису Python), рассчитанные на читателей, которые Python совсем не знают, — например, только начинают его изучать, возможно, зная какой-нибудь другой язык программирования. Если листинг вам понятен, смело можете примечания пропускать.
Здесь input() означает ввод текста с клавиатуры, а float — преобразование текста
в число с дробной частью. Таким образом, a=float(input()) означает то, что, когда
Python во время выполнения дойдет до этой строчки, он «попросит» пользователя
ввести текст, который затем преобразует в число (возможно, с дробной частью) и сохранит полученное значение в переменной a.
Конструкция if a!=0 означает, что весь следующий код, сделанный с отступом, в нашем случае pass, будет выполняться только, если выполняется условие a не равно 0.
В строчках с отступом после слова else («иначе») идет код, который будет выполняться, если условие, написанное в if, не выполняется (в нашем случае, если a равно 0).
pass означает, что, когда Python при выполнении программы дойдет до строчки со словом pass, ему ничего делать не нужно. Программисты используют этот оператор в заготовках программ. Вскоре мы заменим все pass на работающий код.
Чтобы самостоятельно написать и запустить эту программу на вашем компьютере вам
нужно установить какой-нибудь компилятор Python. Я рекомендую начать изучать программирование на Python со среды разработки IDLE Python. Это свободно распространяемое программное обеспечение, которое можно скачать с официального сайта:
https://www.python.org/downloads/. IDLE Python представляет собой простую среду
разработки, в которой вы легко сможете разобраться и научиться в ней создавать и
запускать программу.
Шаг 2. В зависимости от значения дискриминанта (подкоренного выражения) может быть несколько корней:
D = b 2 − 4ac .
При D = 0 один корень, при D > 0 два корня, при D < 0 действительных корней
нет, но зато есть комплексные (листинг 1.1.2).
Листинг 1.1.2
a=float(input())
b=float(input())
c=float(input())
if a!=0:
D=b*b-4*a*c
if D==0:
x=(-b)/(2*a)
print(x)
elif D>0:
pass
else:
pass
else:
pass
Условия
15
П РИМЕЧАНИЯ К КОДУ
Отступы в программе имеют важное значение. Обратите внимание на то, что после
условия if a!=0: идут восемь строчек с отступом. Это означает, что эти строчки будут
выполняться, только если а не равно 0. Две строчки: x=(-b)/(2*a) и print(x) — сделаны
с двойным отступом. Это означает, что они будут выполняться при дополнительном
условии D равно 0. Условия можно вкладывать друг в друга на неограниченную глубину (в нашем случае условие if D==0: вложено внутрь if a!=0:), увеличивая количество
отступов в начале строчек.
В программе появляется альтернативное условие elif D>0: (иначе если D больше нуля). Ветвление в программе может идти не только по двум веточкам с помощью конструкции if...else... (условие выполняется или не выполняется), но и по трем и
больше с помощью одного или нескольких альтернативных условий if...elif...elif...
elif...else.
print(x) означает «вывести значение переменной x на экран». С помощью print программа сообщает пользователю, который ее запустил, о результатах программы.
Обратите внимание, что в условии if D==0: оператор «равно» написан два раза. Это не
опечатка. Python, как и многие языки программирования, имеет два оператора «равно»: «равно как сравнение» и «равно как присваивание». В условии используется
«равно как сравнение», т. е. ==, а в строчке x=(-b)/(2*a) — «равно как присванивание»,
т. е. =.
Шаг 3. Рассмотрим случай D > 0. Для извлечения корня импортируем функцию
sqrt из библиотеки math. Типичной ошибкой при программировании формулы
x1, 2 =
− b ± b 2 − 4ac
2a
является отсутствие скобок. Чтобы не ошибиться, начнем формулу с последней
выполняемой операции — деления:
x1=/
Видно, что числитель и знаменатель дроби — сложные выражения. Поставим скобки:
x1=()/()
Далее напишем числитель и знаменатель:
x1=(-b-sqrt(D))/(2*a)
Очень часто начинающие программисты забывают скобки в знаменателе:
x1=(-b-sqrt(D))/2*a
получая ошибочный результат.
Программа приобретает окончательный вид в листинге 1.1.3.
Листинг 1.1.3
from math import sqrt
a=float(input())
b=float(input())
c=float(input())
16
Глава 1
if a!=0:
D=b*b-4*a*c
if D==0:
x=(-b)/(2*a)
print(x)
elif D>0:
x1=(-b-sqrt(D))/(2*a)
x2=(-b+sqrt(D))/(2*a)
print(x1,x2)
else:
pass
else:
pass
П РИМЕЧАНИЕ К КОДУ
from math import sqrt означает «импортировать функцию извлечения квадратного корня
sqrt из библиотеки math». Очень многие полезные функции, которые тем не менее
нужны далеко не в каждой программе, вынесены в отдельные библиотеки (файлы).
Чтобы их использовать, нужно импортировать их из соответствующих библиотек. Библиотека math содержит много полезных математических функций — например, тригонометрические.
Шаг 4. При D < 0, как учат в школе, действительных корней нет, зато, как учат
в университете, есть корни комплексные. Для тех, кто не знает, что это такое, поясню на примере.
Пусть a = 1, b = 1, c = 13, тогда
−b ± b 2 − 4ac −1 ± 1 − 4 −1 ± −3 −1 ± 3 ⋅ (−1)
=
=
=
=
2a
2
2
2
1
3
−1 ± 3 ⋅ −1
=
=− ±
−1.
2
2 2
Для любого уравнения можно получить похожую формулу, выделив в отдельную
позицию корень из минус единицы. Для него в математике вводится специальное
обозначение:
x1,2 =
Мы получаем ответ:
Таким образом, числа вида
i = −1 .
1
3
x1, 2 = − ±
i.
2 2
z = Re + Im⋅ i
называются комплексными (Re — действительная часть, Im — мнимая). Несмотря
на кажущуюся поначалу бессмысленность таких чисел, они заняли прочное место
в математике и оказались полезны как для развития самой математики, так и для
множества прикладных задач.
Условия
17
Если мы по ветке D < 0 напишем нашу обычную формулу через библиотечную
функцию sqrt:
x1=(-b-sqrt(D))/(2*a)
то Python выдаст ошибку.
Вспомним, что корень можно заменить на дробную степень:
n
1
a =a n,
и запишем формулу в программу (листинг 1.1.4).
Листинг 1.1.4
from math import sqrt
a=float(input())
b=float(input())
c=float(input())
if a!=0:
D=b*b-4*a*c
if D>0:
x1=(-b-sqrt(D))/(2*a)
x2=(-b+sqrt(D))/(2*a)
print(x1,x2)
elif D==0:
x=(-b)/(2*a)
print(x)
else:
x1=(-b-D**0.5)/(2*a)
x2=(-b+D**0.5)/(2*a)
print(x1,x2)
else:
pass
Результат выполнения программы:
(-0.5-0.8660254037844386j)
(-0.49999999999999994+0.8660254037844386j)
Python может работать с комплексными корнями! Только мнимую единицу обозначает не i, а j.
Шаг 5. Рассмотрим случай a = 0. Получим линейное уравнение:
bx + c = 0.
В общем случае получается один корень, вычисляемый по формуле
c
x=− .
b
Но при b = 0 получается деление на ноль.
18
Глава 1
Добавим вариант указанного линейного уравнения при b = 0. Уравнение примет
вид
c = 0.
В этом случае если с = 0, то x — любое число, если нет, то корней нет.
Значит, ветка a = 0 распадается на три случая. Мы получаем готовую программу
(листинг 1.1.5).
Листинг 1.1.5. Решение уравнений второй степени
a=float(input())
b=float(input())
c=float(input())
if a!=0:
D=b*b-4*a*c
if D==0:
x=(-b)/(2*a)
print(x)
elif D>0:
x1=(-b-sqrt(D))/(2*a)
x2=(-b+sqrt(D))/(2*a)
print(x1,x2)
else:
x1=(-b-D**0.5)/(2*a)
x2=(-b+D**0.5)/(2*a)
print(x1,x2)
else:
if b==0 and c==0:
print("x-любое")
elif b==0:
print("корней нет")
else:
x=-c/b
print(x)
П РИМЕЧАНИЕ К КОДУ
В if b==0 and c==0: мы использовали составное условие, соединив два условия: b равно 0 и с равно 0 с помощью оператора and («и»). Для связки условий также применяется оператор or («или»). Еще есть оператор отрицания условия not («не»). Какой из
операторов связки использовать, надо решать в каждом конкретном случае. От этого
выбора зависит ход выполнения программы.
1.2. Кирпич и дыра в стене
Задача
Вводятся размеры прямоугольной дыры в стене a, b и размеры кирпича x, y, z.
Можно ли кирпич просунуть в стену?
Условия
19
Рис. 1.1. Дыра в стене и кирпич
Толщина стены значения не имеет, дыра сквозная (рис. 1.1).
Способ просовывания по диагонали не рассматривайте.
Версия 1
Языковые конструкции: составные условия.
Ход программирования
Стороны дыры и кирпича не упорядочены. Проблемой является то, какими сторонами вставлять кирпич в стену. Это можно попробовать сделать тремя способами:
(x, y), или (x, z), или (y,z). В каждом из этих вариантов есть выбор из двух способов
просовывания — например: x расположить вдоль a, у — вдоль b или наоборот. Получается составное условие из шести частей, соединенных or (листинг 1.2.1)
Листинг 1.2.1. Кирпич и дыра в стене. Составные условия
print("введите 2 размера дыры")
a=int(input())
b=int(input())
print("введите 3 размера кирпича")
x=int(input())
y=int(input())
z=int(input())
if (x<=a and y<=b or y<=a and x<=b or
x<=a and z<=b or z<=a and x<=b or
z<=a and y<=b or y<=a and z<=b):
print("войдет")
else:
print("не войдет")
П РИМЕЧАНИЕ К КОДУ
Здесь в составном условии используются два оператора: and («и») и or («или»). Оператор and имеет больший приоритет, чем оператор or. То есть сперва выполняются все
and, и только потом уже or. В нашем случае вместо:
if x<=a and y<=b or y<=a and x<=b...
мы могли бы поставить дополнительные скобки:
if (x<=a and y<=b) or (y<=a and x<=b)...
20
Глава 1
Но мы не стали этого делать по той же причине, по которой в арифметической формуле 2*3+4*5 мы не ставим скобки, хотя могли бы это сделать: (2*3)+(4*5). Оператор or
играет роль сложения, and — умножения, not — возведения в степень. Впрочем, так же
как и в арифметических выражениях, во многих логических выражениях скобки нужны,
и их расстановка влияет на ход выполнения программы.
Версия 2
Языковые конструкции: условие, кортежи.
Ход программирования
Упорядочим размеры дыры и размеры кирпича с помощью функций min и max.
Тогда нам станет не нужно большое составное условие. Чтобы упорядочить три
размера кирпича, понадобятся в худшем случае три перестановки (листинг 1.2.2).
Листинг 1.2.2. Кирпич и дыра в стене. Кортежи, min и max
print("введите 2 размера дыры")
a=int(input())
b=int(input())
a,b=min(a,b),max(a,b)
print(a,b)
print("введите 3 размера кирпича")
x=int(input())
y=int(input())
z=int(input())
x,y=min(x,y),max(x,y)
y,z=min(y,z),max(y,z)
x,y=min(x,y),max(x,y)
print(x,y,z)
if x<=a and y<=b:
print("войдет")
else:
print("не войдет")
Результат
введите 2 размера дыры
2
1
1 2
введите 3 размера кирпича
2
5
1
1 2 5
войдет
П РИМЕЧАНИЯ К КОДУ
min — функция вычисления минимума из переменных, помещенных в скобки, max —
функция вычисления максимума.
В выражениях a,b=min(a,b),max(a,b)) и в трех выражениях вида x,y=min(x,y),max(x,y) используются кортежи — упорядоченные последовательности переменных для одновременных присваиваний. Эта конструкция отсутствует во многих языках программирования, поэтому поясним ее на следующем примере. Код:
a=1
b=2
c=3
можно заменить на
a,b,c = 1,2,3
Подобные упрощающие записи программисты называют синтаксическим сахаром.
Условия
21
Обратная замена, впрочем, не всегда верна и не всегда осуществляется просто.
В нашей программе, чтобы избавиться от кортежей, придется вводить дополнительные переменные — буферы обмена, которые будут хранить промежуточные результаты.
Версия 3
Языковые конструкции: сортировка списков.
Ход программирования
Сортировка трех чисел с помощью трех приравниваний не очень изящна. Поместим
числа в списки и отcортируем их с помощью функции sort (листинг 1.2.3).
Листинг 1.2.3. Кирпич и дыра в стене. Сортировка
print("введите 2 размера дыры")
a=int(input())
b=int(input())
L=sorted([a,b])
print(L)
print("введите 3 размера кирпича")
x=int(input())
y=int(input())
z=int(input())
M=sorted([x,y,z])
print(M)
if M[0]<=L[0] and M[1]<=L[1]:
print("войдет")
else:
print("не войдет")
П РИМЕЧАНИЕ К КОДУ
Здесь мы видим перечисления переменных, помещенных в квадратные скобки: [a,b] и
[x,y,z] — это списки (упорядоченные наборы элементов). Списку можно присвоить
имя, например L=[10,20,30], и далее через это имя обращаться к соответствующему
элементу по его номеру (элементы нумеруются с нуля) — например, чтобы добраться
до 20, надо написать L[1].
Итак, в этой главе мы написали несколько программ с использованием ввода/вывода, переменных, списков, математических формул и условий. Но это лишь
часть того, что есть в алгоритмах. Помимо ветвлений по условию, в алгоритмах
часто встречаются шаги, которые нужно повторить несколько раз (циклы). Им посвящена следующая глава.
22
Глава 1
ГЛАВА
2
Структурное программирование
Эта глава посвящена структурному программированию. В нем программа представляет собой вложенные друг в друга циклы и условия. Особенность Python
в том, что он позволяет очень просто оперировать числовыми и строковыми данными, а также данными, хранящимися в более сложных коллекциях, — одномерных
и двумерных списках, множествах и словарях. Задачи из этой главы имеют, как
правило, несколько решений.
Особенность приведенных в ней задач в том, что они имеют решения на уровне
«здравого смысла», и вам нужно иметь лишь немного алгоритмического мышления
(способности выстроить последовательность действий), чтобы их решить без моей
помощи. Поэтому, если вы считаете, что умеете программировать, сперва попробуйте написать программы самостоятельно.
2.1. Полупроходной балл
Решим задачу, которая имеет несколько модификаций и способов решений. Разберемся, когда надо использовать списки и в какой вариации, а когда — нет.
Задача 1
Вводится количество абитуриентов и количество мест. Затем вводятся баллы абитуриентов по убыванию. Вывести количество абитуриентов, имеющих полупроходной балл. Например:
количество абитуриентов: n = 12;
количество мест: m = 6;
баллы: a = 9, 9, 8, 8, 7, 7, 7, 7, 7, 6, 6, 5.
Видно, что:
a
Итог:
9
9
8
поступили
8
7
7
7
7
имеют полупроходной балл
7
6
6
не поступили
5
24
Глава 2
Версия 1
Языковые конструкции: списки.
Ход программирования
Шаг 1. Самая простая версия программы — со списком. Отсчитаем абитуриентов
по количеству мест:
Места:
1
2
3
4
5
6
a
9
9
8
8
7
7
Итог:
поступили
7
7
7
6
имеют полупроходной балл
6
5
не поступили
В списке нумерация (i) идет с нуля:
Места:
1
2
3
4
5
6
i
0
1
2
3
4
5
a
9
9
8
8
7
7
Итог:
поступили
7
7
имеют полупроходной балл
7
6
6
5
не поступили
Видно, что полупроходной балл равен a[m]. И нам осталось подсчитать количество
вхождений a[m] в список a. Сделаем это с помощью встроенного метода Python
count: a.count(a[m]) (листинг 2.1.1).
Листинг 2.1.1
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
print("баллы")
a=[]
for i in range(n):
a.append(int(input()))
print("кол-во полупроходников:",a.count(a[m]))
П РИМЕЧАНИЯ К КОДУ
Выражение a=[] создает пустой список.
Выражение for i in range(n): задает цикл — повторяющие действия. В нашем случае
это выражение можно понимать как «для переменной i, изменяющейся в диапазоне от
0 до n-1» делай следующую последовательность действий. То есть следующий после
for блок выражений с отступом будет выполняться n раз.
Выражение range(...) — это генератор диапазона. В общем случае он имеет вид
range(start,fin,step). Начало и шаг в range могут отсутствовать. По умолчанию шаг изменения равен 1, начало равно 0. Когда в range написано одно число n, то генератор
создает числа от 0 до n - 1. Например, при n = 5 сгенерированные числа будут 0, 1, 2,
3, 4.
Метод a.append(...) помещает значение в скобках в список a.
Метод a.count(...) подсчитывает количество вхождений того, что в скобках, в список a.
Структурное программирование
25
Шаг 2. Нужно проверить особый случай, когда «полупроходников» просто нет,
например:
количество абитуриентов: n = 12;
количество мест: m = 6.
a
9
9
Итог:
8
8
7
7
6
6
поступили
6
6
5
5
не поступили
Посмотрим на индекс в списке:
i
0
1
2
3
4
5
6
7
8
9
10
11
a
9
9
8
8
7
7
6
6
6
6
5
5
Итог:
поступили
не поступили
Видно, что ситуация, при которой «полупроходников» нет, возникает, когда:
a[m-1]!=a[m]
Модифицируем программу (листинг 2.1.2).
Листинг 2.1.2. Баллы упорядочены, список
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
print("баллы:")
a=[]
for i in range(n):
a.append(int(input()))
if a[m]==a[m-1]:
print("кол-во полупроходников:",a.count(a[m]))
else:
print("кол-во полупроходников:",0)
Версия 2
Языковые конструкции: условия.
Приемы программирования: запоминание предыдущего числа в потоке, счетчик
со сбросом.
Ход программирования
Шаг 1. Мы можем обойтись без списка. Рассмотрим работу по шагам для примера:
количество абитуриентов: n = 12;
количество мест: m = 6;
баллы: a = 9, 9, 8, 8, 7, 7, 7, 7, 7, 6, 6, 5.
26
Глава 2
Введем счетчик с — количество абитуриентов с полупроходным баллом.
1. Принимаем балл 9. Мы не знаем следующие баллы, поэтому 9 вполне может
быть полупроходным баллом: с = 1.
2. Принимаем балл 9. Количество людей с полупроходным баллом увеличивается:
с = 2.
3. Принимаем балл 8. Ясно, что предыдущие абитуриенты с 9 баллами поступили,
с = 1.
4. Принимаем балл 8, с = 2.
Мы имеем дело со счетчиком со сбросом и сравнением текущего балла a с предыдущим ap:
a
9
9
8
8
7
7
7
7
7
...
с
1
2
1
2
1
2
3
4
5
...
Получена заготовка программы (листинг 2.1.3).
Листинг 2.1.3
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
print("баллы:")
c=1
for i in range(n):
a=int(input())
if i>1:
if a==ap:
c=c+1
else:
c=1
ap=a
П РИМЕЧАНИЕ К КОДУ
Формулы вида c=c+1 называются рекуррентными формулами. В общем случае они означают: надо взять значение переменной, изменить его по написанной формуле и обновить сохраненное значение. В нашем случае берется значение переменной c, увеличивается на 1 и полученный результат сохраняется в той же самой переменной c.
Для таких формул в Python есть «синтаксический сахар» (более простая запись).
Вместо с=с+1 можно написать с+=1.
Шаг 2. Но мы продолжаем ввод и обработку баллов не до конца (иначе мы получим количество людей, имеющих минимальный балл):
a
9
9
8
8
7
7
7
7
7
6
6
5
с
1
2
1
2
1
2
3
4
5
1
2
1
Структурное программирование
27
Нам нужно сделать прерывание. Ясно, что это произойдет при i>m:
i>m
a
9
9
8
8
7
7
7
7
7
6
с
1
2
1
2
1
2
3
4
5
Break
6
5
Дополнение к программе показано в листинге 2.1.4.
Листинг 2.1.4
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
print("баллы:")
c=1
for i in range(n):
a=int(input())
if i>1:
if a==ap:
c=c+1
else:
if i>m:
break
c=1
ap=a
print("кол-во полупроходников:",c)
П РИМЕЧАНИЕ К КОДУ
Оператор break означает принудительный выход из цикла. Если циклов несколько (они
вложены друг в друга), оператор break прервет только один цикл.
Шаг 3. Есть еще случай, когда «полупроходников» нет:
i=m
a
9
9
8
8
7
7
6
c
1
2
1
2
1
2
0 break
6
6
6
5
5
Обнуляем счетчик и прерываем цикл. Получится готовая программа (листинг 2.1.5).
Листинг 2.1.5. Баллы упорядочены, счетчик со сбросом
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
print("баллы:")
c=1
28
Глава 2
for i in range(n):
a=int(input())
if i>1:
if a==ap:
c=c+1
else:
if i>m:
break
elif i==m:
c=0
break
c=1
ap=a
print("кол-во полупроходников:",c)
Задача 2
Уберем требование, чтобы баллы вводились по убыванию, — пусть баллы вводятся
вразброс. Итак, вводится количество абитуриентов и количество мест. Затем вводятся баллы абитуриентов по убыванию. Вывести количество абитуриентов,
имеющих полупроходной балл. Например:
количество абитуриентов: n = 12;
количество мест: m = 6;
баллы: a = 5, 7, 6, 7, 8, 9, 7, 6, 7, 8, 9, 7.
Версия 3
Языковые конструкции: список.
Приемы программирования: сортировка списка.
Ход программирования
Введем все баллы в список, затем отсортируем его по убыванию и воспользуемся
программой из листинга 2.1.2 (листинг 2.1.6).
Листинг 2.1.6. Сортировка
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
print("баллы")
a=[]
for i in range(n):
a.append(int(input()))
a=sorted(a)[::-1]
if a[m]==a[m-1]:
print("кол-во полупроходников:",a.count(a[m]))
else:
print("кол-во полупроходников:",0)
Структурное программирование
29
П РИМЕЧАНИЕ К КОДУ
В выражении сортировки a=sorted(a)[::-1] необычное выражение [::-1] означает «переворачивание» списка — запись его в обратном порядке. Это переворачивание
в нашем случае реализуется с помощью среза. Срез — это удобная «пайтоновская»
конструкция, отсутствующая во многих языках, она означает фрагмент списка. В общем случае срез задается в виде индексов начала и конца среза, а также шага, разделенных двоеточиями: L[start : fin : step]. Индексы и шаг могут отсутствовать.
В нашем случае в [::-1] нет индексов начала и конца — т. е. берется весь список, а
отрицательный шаг означает обратный порядок.
Версия 4
Языковые конструкции: список.
Приемы программирования: вставка в список, счетчик со сбросом.
Ход программирования
Шаг 1. Можно не хранить все баллы в списке — это неэкономно по памяти, а из-за
сортировки — неэкономно по времени. Можно создать список лишь по количеству
мест и хранить там баллы по убыванию, например:
[8,7,7,6,6,5]
Если мы примем a = 7, то увидим, что этот балл надо записать на первое место
(если считать нумерацию с нуля):
[8,7,7,7,6,6,5]
Урезаем список, удаляя 5. Ясно, что человек с 5 баллами не поступит:
[8,7,7,7,6,6]
Принимаем a = 9, добавляем 9 на нулевое место:
[9,8,7,7,7,6,6]
Мы не можем просто удалить последнюю 6. Вдруг 6 окажется полупроходным баллом? Введем счетчик с, равный количеству одинаковых с предпоследним элементом баллов, и урежем список:
[9,8,7,7,7,6] с = 1
Примем a = 8:
[9,8,8,7,7,7,6] с = 1
Поскольку последний элемент (6) не равен предпоследнему (7), то 6 перестает быть
полупроходным баллом, количество удаленных 6 уже не нужно помнить, удалим
последний элемент и обнулим счетчик:
[9,8,8,7,7,7] с = 0
В самом начале работы программы заполним список нереальными значениями баллов -1. Рассмотрим пошаговую работу для нашего примера:
a = 5, 7, 6, 7, 8, 9, 7, 6, 7, 8, 9, 7.
30
Глава 2
Начальное положение: L=[-1,-1,-1,-1,-1,-1] c=0
a=5:
a=7:
a=6:
a=7:
a=8:
a=9:
a=7:
a=6:
a=7:
a=8:
a=9:
a=7:
L=[5,-1,-1,-1,-1,-1] c=1
L=[7,5,-1,-1,-1,-1] c=2
L=[7,6,5,-1,-1,-1] c=3
L=[7,7,6,5,-1,-1] c=4
L=[8,7,7,6,5,-1] c=5
L=[9,8,7,7,6,5] c=0
L=[9,8,7,7,7,6] c=0
L=[9,8,7,7,7,6] c=1
L=[9,8,7,7,7,7] c=0
L=[9,8,8,7,7,7] c=1
L=[9,9,8,8,7,7] c=2
L=[9,9,8,8,7,7] c=3
Запрограммируем этот алгоритм (листинг 2.1.7).
Листинг 2.1.7
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
L=[-1]*m
c=0
print("баллы:")
for i in range(n):
a=int(input())
for j in range(m):
if a>=L[j]:
L.insert(j,a)
if L[-1]==L[-2]:
c=c+1
else:
c=0
L=L[:-1]
Break
П РИМЕЧАНИЯ К КОДУ
Выражение L=[-1]*m означает создание списка, состоящего из одних «-1» в количестве m.
В выражении L[-1]==L[-2] используется обратная индексация. Мы можем обращаться
к элементам списка, нумеруя их не только с начала, но и с конца. Для этого используются отрицательные индексы. В нашем случае L[-1] — последний элемент, а L[-2] —
предпоследний.
Метод L.insert(j,a) помещает значение а в список на место с номером j.
Часто встречающейся ошибкой является отсутствие break. В этом случае список
заполняется баллом a несколько раз до своего конца.
Структурное программирование
31
Шаг 2. После цикла приема и обработки данных i в том случае, если с не равно
нулю, остается подсчитать, сколько раз последний элемент списка в нем встречается, и увеличить с. Для нашего примера:
a=7: L=[9,9,8,8,7,7] c = 3
В конце идут две семерки, значит, с надо увеличить на 2, т. е. с = 5.
Мы получим готовую программу (листинг 2.1.8).
Листинг 2.1.8. Список по количеству мест
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
L=[-1]*m
c=0
print("баллы:")
for i in range(n):
a=int(input())
for j in range(m):
if a>=L[j]:
L.insert(j,a)
if L[-1]==L[-2]:
c=c+1
else:
c=0
L=L[:-1]
break
if c!=0:
c=c+L.count(L[-1])
print("кол-во полупроходников:",c)
П РИМЕЧАНИЕ К КОДУ
Выражение c=c+L.count(L[-1]) является накапливающейся суммой — вторым примером
рекуррентной формулы (первым был счетчик). В простом случае накапливающаяся
сумма имеет вид s=s+a, что означает взять значение переменной s, увеличить его на a
и сохранить полученное значение в этой же самой переменной s. Мы можем воспользоваться «синтаксическим сахаром» Python и записать эту формулу более кратко: s+=a.
Задача 3
Введем еще максимально возможный балл. Пусть баллы вводятся вразброс.
Итак, вводится количество абитуриентов, количество мест и максимально возможный балл. Затем вводятся баллы абитуриентов по убыванию. Вывести количество
абитуриентов, имеющих полупроходной балл. Например:
количество абитуриентов: n = 12;
количество мест: m = 6;
32
Глава 2
максимально возможный балл: А = 10;
баллы: a = 5, 7, 6, 7, 8, 9, 7, 6, 7, 8, 9, 7.
Версия 5
Языковые конструкции: список.
Приемы программирования: список счетчиков, накапливающаяся сумма.
Ход программирования
Шаг 1. Идея алгоритма заключается в том, что мы делаем список размером, равным максимально возможному баллу. В нем мы будем хранить количество абитуриентов, набравших балл, равный индексу элемента списка. То есть создадим список счетчиков (листинг 2.1.9).
Листинг 2.1.9
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
A=int(input("максимальный балл:"))
L=[0]*(A+1)
print("баллы:")
for i in range(n):
a=int(input())
L[a]=L[a]+1
Для нашего примера, где a = 5, 7, 6, 7, 8, 9, 7, 6, 7, 8, 9, 7, мы получим:
i
0
1
2
3
4
5
6
7
8
9
10
L[i]
0
0
0
0
0
1
2
5
2
2
0
Шаг 2. Будем просматривать список с конца и формировать накапливающуюся
сумму. Когда она превысит количество мест, L[i] и будет количеством полупроходников:
i
0
1
2
3
4
5
6
7
8
9
10
L[i]
0
0
0
0
0
1
2
5
2
2
0
9
4
2
0
s
Особым случаем является s, равная количеству мест m. При этом полупроходного
балла нет (листинг 2.1.10).
Листинг 2.1.10. Список счетчиков
n=int(input("кол-во абитуриентов:"))
m=int(input("кол-во мест:"))
A=int(input("максимальный балл:"))
Структурное программирование
33
L=[0]*(A+1)
print("баллы:")
for i in range(n):
a=int(input())
L[a]=L[a]+1
s=0
for i in range(1,A):
s=s+L[-i]
if s>m:
print("кол-во полупроходников:",L[-i])
break
elif s==m:
print("кол-во полупроходников:",0)
break
Мы решили важную задачу, в которой разобрались, когда нужно применять список,
а когда — нет. Кроме того, рассмотрели три версии, в которых список используется
по-разному, т. к. мы вкладывали разный смысл в значения элементов списка.
2.2. Метеостанция
Следующая задача интересна тем, что в ней задействовано несколько приемов на
потоке чисел: запоминание предыдущего числа, счетчик со сбросом, максимумы.
Программа будет иметь очень разветвленную структуру. Можно также написать
вторую версию программы — в ней исходный одномерный список мы преобразуем
в список списков.
Задача
Вводится количество дней. Метеостанция раз в день измеряет температуру. Найти:
1. Количество дней самого долгого периода возрастания температуры.
2. Вывести перепад между температурой первого и последнего дня в самом долгом
периоде возрастания. Если таких периодов несколько, то вывести максимальный
перепад.
Пример:
Температура t
2
3
1
3
4
3
2
5
7
1
9
Период
2
3
1
3
2
Максимальный период
x
v
x
v
x
Перепады
в макс. периодах
3
Макс. перепад
в макс. периодах
Напишем две версии программы: без списков и со списками.
5
5
34
Глава 2
Версия 1
Языковые конструкции: циклы, условия.
Приемы программирования: предыдущее число в потоке, максимумы.
Ход программирования
Шаг 1. Напишем версию алгоритма без списков. Для того чтобы узнать, возрастает
ли температура, нужно принимать текущее значение температуры и помнить предыдущее. Применим прием, который уже был использован в предыдущем разделе:
запоминание предшествующего числа в потоке (листинг 2.2.1).
Листинг 2.2.1
n=int(input())
for i in range(n):
t=int(input())
if i>0:
#здесь будет вся обработка
t1=t
П РИМЕЧАНИЕ К КОДУ
Текст в программе с «решеточкой» (#здесь будет вся обработка) означает комментарий — пояснения в программе для программиста. Строки с решеткой при выполнении
программы Python игнорирует. Комментарии также могут использоваться для отключения кода, когда программист не хочет его стирать. Например, в коде может быть
ошибка, и программист временно его отключает.
Шаг 2. Сделаем счетчик со сбросом, который будет возрастать при увеличении
температуры и сбрасываться в 1 при уменьшении. Обратите внимание, что начальное значение счетчика, так же как и сброс, происходит в 1, а не в 0:
Температура t
с
2
3
1
3
4
3
2
5
7
1
9
1
2
1
2
3
1
1
2
3
1
2
Модифицируем программу (листинг 2.2.2).
Листинг 2.2.2
n=int(input())
c=1
for i in range(n):
t=int(input())
if i>0:
if t>t1:
c=c+1
else:
c=1
t1=t
Структурное программирование
35
Шаг 3. Ищем максимальное значение счетчика (листинг 2.2.3).
Листинг 2.2.3
n=int(input())
c=1
m=0
for i in range(n):
t=int(input())
if i>0:
if t>t1:
c=c+1
if m<c:
m=c
else:
c=1
t1=t
print(m)
Мы получили работающую программу, правильно отвечающую на первый вопрос.
Шаг 4. Для определения максимального перепада температур надо помнить не
только текущую температуру t, температуру предыдущего дня t1, но и температуру
начала периода возрастания t0. Начальное ее значение устанавливается при i=0,
а обновление происходит при сбросе счетчика (листинг 2.2.4).
Листинг 2.2.4
n=int(input())
c=1
m=0
for i in range(n):
t=int(input())
if i>0:
if t>t1:
c=c+1
if m<c:
m=c
else:
c=1
t0=t
else:
t0=t
t1=t
print(m)
Шаг 5. Введем переменную d для хранения перепада температур. Поскольку мы
ищем перепад температур не среди всех периодов возрастания, а только у самого
36
Глава 2
длинного, обновление d будет происходить тогда, когда обновляется максимум
счетчика m (листинг 2.2.5).
Листинг 2.2.5
n=int(input())
c=1
m=0
for i in range(n):
t=int(input())
if i>0:
if t>t1:
c=c+1
if m<c:
m=c
d=t-t0
else:
c=1
t0=t
else:
t0=t
t1=t
print(m)
Шаг 6. Самых длинных периодов может быть несколько, и среди них нужно
выбрать максимальный перепад. Появляется еще одна ветка, когда m=c. Получаем
готовую версию программы (листинг 2.2.6).
Листинг 2.2.6. Версия без списков
n=int(input())
c=1
m=0
for i in range(n):
t=int(input())
if i>0:
if t>t1:
c=c+1
if m<c:
m=c
d=t-t0
elif m==c:
if d<t-t0:
d=t-t0
else:
c=1
t0=t
else:
t0=t
t1=t
Структурное программирование
37
print(m)
print(d)
Версия 2
Языковые конструкции: циклы, условия, списки списков.
Ход программирования
Шаг 1. Будем принимать температуры в список через пробел (листинг 2.2.7).
Листинг 2.2.7
t=[int(el) for el in input().split()]
print(t)
Результат:
2 3 1 3 4 3 2 5 7 1 9
[2, 3, 1, 3, 4, 3, 2, 5, 7, 1, 9]
П РИМЕЧАНИЕ К КОДУ
Разберемся, что означает выражение:
t=[int(el) for el in input().split()]
Это очень емкое выражение, заменяющее в других языках программирования много
строк кода и являющееся фирменным стилем Python.
Метод s.split() означает разбить строчку s на элементы по имеющимся в s пробелам.
Цепочка вызовов методов input().split() означает принять строчку и разбить ее по
пробелам на элементы, породив некоторую коллекцию.
Выражение for el in A — это одно из применений цикла for. До этого мы использовали for с range — генератором диапазона. Но мы можем перебирать и элементы любой
коллекции. В этом случае цикл можно понимать как «перебрать все элементы коллекции».
Циклы, состоящие из одного действия, можно записать в одну строчку без двоеточия,
поместив действие перед ключевым словом for.
Выражения вида [int(el) for el in input().split()] при создании списка, в которых
в квадратных скобках стоят сложные выражения с циклами и, возможно, с условиями,
называются списочными выражениями. Они порождают особый стиль (парадигму)
программирования Python.
То есть строчка t=[int(el) for el in input().split()] заменяет целый алгоритм:
A=input()
A=A.split()
t=[]
for el in A:
t.append(int(el))
Идея алгоритма заключается в том, что мы превратим список в список списков, т. е.
каждый период возрастания будем сохранять в подсписке:
T=[[2,3],[1,3,4],[3],[2,5,7],[1,9]]
38
Глава 2
Для этого перед циклом разбиения на подсписки поместим нулевой элемент
списка t в подсписок списка T:
T=[[t[0]]]
Далее в цикле перебираем следующие температуры. Если период возрастания продолжается, то мы дописываем последний период возрастания новым элементом:
T[-1].append(t[i])
А если начинается новый период, то создаем новый подсписок с одним элементом:
T.append([t[i]])
Так мы получим программу, приведенную в листинге 2.2.8.
Листинг 2.2.8
t=[int(el) for el in input().split()]
print(t)
T=[[t[0]]]
for i in range(1,len(t)):
if t[i]>t[i-1]:
T[-1].append(t[i])
else:
T.append([t[i]])
print(T)
Результат:
[2, 3, 1, 3, 4, 3, 2, 5, 7, 1, 9]
[[2, 3], [1, 3, 4], [3], [2, 5, 7], [1, 9]]
П РИМЕЧАНИЕ К КОДУ
Список не обязательно состоит из простых элементов: чисел или строк. Элемент может быть сам некоторой коллекцией — например, списком. В нашем примере видно,
что результат — это список списков. Соответственно T[i] — это «подсписок», T[-1] —
последний подсписок, T[-1].append(...) — добавить число к последнему подсписку,
T=[[...]] — сделать список списков.
Шаг 2. Сформируем список длин подсписков и найдем его максимум, получив ответ на вопрос № 1 (листинг 2.2.9).
Листинг 2.2.9
t=[int(el) for el in input().split()]
print(t)
T=[[t[0]]]
for i in range(1,len(t)):
if t[i]>t[i-1]:
T[-1].append(t[i])
else:
T.append([t[i]])
Структурное программирование
39
print(T)
m=max(len(p) for p in T)
print(m)
Результат:
2 3 1 3 4 3 2 5 7 1 9
[2, 3, 1, 3, 4, 3, 2, 5, 7, 1, 9]
[[2, 3], [1, 3, 4], [3], [2, 5, 7], [1, 9]]
3
П РИМЕЧАНИЕ К КОДУ
Функция len(p) вычисляет количество элементов коллекции. В нашем случае в списочном выражении max(len(p) for p in T) перебираются «подсписки» p двумерного списка T, для каждого «подсписка» вычисляется количество его элементов (длина «подсписка») и затем из этих длин вычисляется максимум.
Шаг 3. Сформируем новый список списков T1, куда добавим самые длинные периоды возрастания (листинг 2.2.10).
Листинг 2.2.10
t=[int(el) for el in input().split()]
print(t)
T=[[t[0]]]
for i in range(1,len(t)):
if t[i]>t[i-1]:
T[-1].append(t[i])
else:
T.append([t[i]])
print(T)
m=max([len(p) for p in T])
print(m)
T1=[p for p in T if len(p)==m]
print(T1)
Результат:
2 3 1 3 4 3 2 5
[2, 3, 1, 3, 4,
[[2, 3], [1, 3,
3
[[1, 3, 4], [2,
7 1 9
3, 2, 5, 7, 1, 9]
4], [3], [2, 5, 7], [1, 9]]
5, 7]]
П РИМЕЧАНИЕ К КОДУ
Выражение T1=[p for p in T if len(p)==m] — это пример списочного выражения «цикл
с условием». Оно означает отобрать элементы коллекции, удовлетворяющие условию. В нашем случае эта запись заменяет следующие строчки кода:
T1=[]
for p in T:
if len(p)==m:
T1.append(p)
40
Глава 2
Шаг 4. Для элементов T1 найдем разности между последним элементом и нулевым,
а среди полученных разностей — максимальную, что и даст ответ на вопрос № 2
(листинг 2.2.11).
Листинг 2.2.11. Список списков
t=[int(el) for el in input().split()]
print(t)
T=[[t[0]]]
for i in range(1,len(t)):
if t[i]>t[i-1]:
T[-1].append(t[i])
else:
T.append([t[i]])
print(T)
m=max([len(p) for p in T])
print(m)
T1=[p for p in T if len(p)==m]
print(T1)
d=max([p[-1]-p[0] for p in T1])
print(d)
Результат:
2 3 1 3 4 3 2 5
[2, 3, 1, 3, 4,
[[2, 3], [1, 3,
3
[[1, 3, 4], [2,
5
7 1 9
3, 2, 5, 7, 1, 9]
4], [3], [2, 5, 7], [1, 9]]
5, 7]]
2.3. Седловина матрицы
Задача
Вводится матрица. Найти ее седловину (клетку, значение которой является максимумом по строке и минимумом по столбцу или наоборот). Учесть, что седловин
может быть несколько.
Пример (выделенная клетка является седловиной):
1
2
7
3
2
4
5
3
4
5
6
6
7
8
8
9
Языковые конструкции: двумерные списки.
Структурное программирование
41
Ход программирования
Шаг 1. Для удобства отладки зададим матрицу в программе. Чтобы найти седловину матрицы, организуем перебор всех ее клеток (листинг 2.3.1).
Листинг 2.3.1
M=[[1,2,7,3],
[2,4,5,3],
[4,5,6,6],
[7,8,8,9]]
n=len(M)
for i in range(n):
for j in range(n):
#здесь будет определение седловины
Шаг 2. Определить, является ли клетка максимумом по строке, очень просто с помощью функции max (листинг 2.3.2).
Листинг 2.3.2
M=[[1,2,7,3],
[2,4,5,3],
[4,5,6,6],
[7,8,8,9]]
n=len(M)
for i in range(n):
for j in range(n):
if M[i][j]==max(M[i])
Шаг 3. Определить, является ли клетка минимумом по столбцу, сложнее. Чтобы
использовать функцию min, надо сперва сформировать список из элементов столбца. Понадобится еще один цикл (листинг 2.3.3).
Листинг 2.3.3
M=[[1,2,7,3],
[2,4,5,3],
[4,5,6,6],
[7,8,8,9]]
n=len(M)
for i in range(n):
for j in range(n):
if M[i][j]==max(M[i])==min([M[k][j] for k in range(n)])
Шаг 4. С использованием or добавим противоположное условие: минимум по строке и максимум по столбцу — и получим работающую программу (листинг 2.3.4).
42
Глава 2
Листинг 2.3.4. Циклы, минимумы и максимумы
M=[[1,2,7,3],
[2,4,5,3],
[4,5,6,6],
[7,8,8,9]]
n=len(M)
for i in range(n):
for j in range(n):
if (M[i][j]==max(M[i])==min([M[k][j] for k in range(n)])
or M[i][j]==min(M[i])==max([M[k][j] for k in range(n)])):
print("(",i,j,")=",M[i][j])
Результат:
( 1 2 )= 5
( 3 0 )= 7
Оказалось, в матрице есть вторая седловина:
1
2
7
3
2
4
5
3
4
5
6
6
7
8
8
9
Шаг 5. Заметим, что программа очень неэффективна. Возьмем любой столбец. Для
него программа ищет максимум и минимум столько раз, сколько в нем строк. Аналогично — для строк. Получается, что для нашего примера можно ускорить программу в четыре раза, если перед перебором всех ячеек матрицы сформировать
четыре списка: минимум и максимум по строкам, минимум и максимум по столбцам (листинг 2.3.5).
Листинг 2.3.5. Списки минимумов и максимумов
M=[[1,2,7,3],
[2,4,5,3],
[4,5,6,6],
[7,8,8,9]]
n=len(M)
maxrow=[max(M[i]) for i in range(n)]
minrow=[min(M[i]) for i in range(n)]
maxcol=[max([M[j][i] for j in range(n)]) for i in range(n)]
mincol=[min([M[j][i] for j in range(n)]) for i in range(n)]
for i in range(n):
for j in range(n):
if (M[i][j]==maxrow[i]==mincol[j] or
M[i][j]==minrow[i]==maxcol[j]):
print("(",i,j,")=",M[i][j])
Структурное программирование
43
2.4. Максимальный квадрат в матрице,
заполненный нулями
Задача
Пусть задана квадратная матрица. Нужно найти размер наибольшего квадрата
в ней, заполненного одними нулями. Например, для матрицы:
M=[[1,1,0,0,1],
[0,1,0,0,0],
[0,1,0,0,0],
[0,0,0,0,0],
[0,0,1,1,0]]
наибольший квадрат, заполненный одними нулями, имеет размер 3×3. Ответ: 3.
Языковые конструкции: двумерные списки.
Приемы программирования: вложенные циклы, флаг.
Ход программирования
В книге «Python. 12 уроков для начинающих» мы решали эту задачу методом
динамики по подотрезкам. Здесь мы решим ее самым примитивным способом —
перебором всех возможных квадратов, а с динамикой по подотрезкам мы еще
встретимся, когда будем решать обобщение этой задачи для многомерного пространства.
Шаг 1. Квадрат в матрице характеризуется тремя величинами: координатами углового квадрата y и x и стороной l. Квадрат будем строить для каждой возможной
угловой клетки, поэтому организуем два цикла для перебора координат с целью
выбора угловых клеток (рис. 2.1). Фрагмент программы приведен в листинге 2.4.1.
Листинг 2.4.1
...
for y in range(len(M)):
for x in range(len(M)):
pass
Рис. 2.1. Перебор координат
Шаг 2. Далее нужно организовать цикл перебора длин квадратов. Проблемой является вычисление самого большого потенциально возможного квадрата, поскольку
44
Глава 2
левый верхний угол квадрата может находиться на разных расстояниях от сторон
таблицы. Поэтому есть риск выйти за пределы таблицы. Вычисление самого большого возможного квадрата выделено в листинге 2.4.2 полужирным шрифтом.
Листинг 2.4.2
...
for y in range(len(M)):
for x in range(len(M)):
for l in range(len(M)-max(y,x)+1):
pass
Шаг 3. Далее организуем два цикла: i и j — для просмотра всех клеток квадрата
размера l. Поясняющая схема обхода приведена на рис. 2.2, фрагмент кода —
в листинге 2.4.3.
Листинг 2.4.3
...
for y in range(len(M)):
for x in range(len(M)):
for l in range(len(M)-max(y,x)+1):
for i in range(y,y+l):
for j in range(x,x+l):
pass
Рис. 2.2. Простой обход клеток
матрицы
Шаг 4. Для определения, заполнен ли квадрат одними нулями, используем флаг.
Размеры квадратов, заполненных нулями, сохраним в списке. Потом найдем максимум в списке и получим работающую программу (листинг 2.4.4).
Листинг 2.4.4. Максимальный квадрат в матрице
M=[[1,1,0,0,1],
[0,1,0,0,0],
[0,1,0,0,0],
[0,0,0,0,0],
[0,0,1,1,0]]
L=[]
for y in range(len(M)):
for x in range(len(M)):
for l in range(len(M)-max(y,x)+1):
f=True
Структурное программирование
45
for i in range(y,y+l):
for j in range(x,x+l):
if M[i][j]!=0:
f=False
break
if f==False:
break
if f==True:
L.append(l)
else:
break
print(max(L))
П РИМЕЧАНИЕ К КОДУ
Флаг — это самый распространенный прием программирования, применяющийся тогда, когда нужно проверить выполнение условия для всех или хотя бы для одного
случая (элемента коллекции). В нашем случае условие равенства нулю должно выполняться для всех элементов матрицы. Прием программирования «флаг» состоит из
использования цикла, вложенного в него условия и логической переменной. Для матрицы мы ищем хотя бы один случай, когда условие не выполняется, и «сбрасываем»
флаг.
Шаг 5. Наша программа работает неэффективно. Предположим, что квадрат, заполненный одними нулями:
является частью бо́льшего квадрата:
При увеличении l клетки малого квадрата будут проверяться заново!
Чтобы этого избежать, вместо вложенных циклов i и j, где цикл j находится внутри
цикла i, сделаем их независимыми, и пусть они проверяют еще не проверенные
клетки. Поясняющая схема обхода приведена на рис. 2.3, фрагмент кода — в листинге 2.4.5.
Листинг 2.4.5. Максимальный квадрат в матрице. Оптимизация
M=[[1,1,0,0,1],
[0,1,0,0,0],
[0,1,0,0,0],
[0,0,0,0,0],
[0,0,1,1,0]]
46
Глава 2
L=[]
for y in range(len(M)):
for x in range(len(M)):
for l in range(len(M)-max(y,x)+1):
f=True
for i in range(y,y+l):
if M[i][x+l-1]!=0:
f=False
break
for j in range(x,x+l):
if M[y+l-1][j]!=0:
f=False
break
if f==True:
L.append(l)
else:
break
print(max(L))
Рис. 2.3. Оптимизированный обход клеток матрицы
2.5. Чемпионат по игре в тетрис
Задача
Идет чемпионат по компьютерной игре. Вводятся количество сыгранных партий,
имена игроков и количество набранных баллов каждого игрока. Каждый игрок может сыграть несколько партий. Пример вводимых данных:
n=5
Иванов 10
Петров 20
Сидоров 30
Иванов 30
Сидоров 20
Структурное программирование
47
Определить, кто является победителем чемпионата. Для каждого игрока учитывается его лучшая игра. Если максимальное количество очков набрало несколько
игроков, то победителем считается тот игрок, который набрал максимум раньше
всех.
Языковые конструкции: словари, списки.
Ход программирования
Шаг 1. Будем сохранять результаты партии в словаре (листинг 2.5.1).
Листинг 2.5.1
n=int(input())
V={}
for i in range(n):
s=input().split()
name=s[0]
score=int(s[1])
V[name]=score
print(V)
Результат:
Иванов 10
Петров 20
Сидоров 30
Иванов 30
Сидоров 20
{'Иванов': 30, 'Петров': 20, 'Сидоров': 20}
П РИМЕЧАНИЕ К КОДУ
Здесь используется новая для этой книги коллекция — словарь — набор упорядоченных пар «ключ-значение» (в нашем случае в роли ключей выступают фамилии, а в роли значений — баллы). По ключу (т. е. по фамилии) можно узнать балл игрока
(V[name]), но не наоборот. В этом словари похожи на списки, только в списках в качестве индекса выступают порядковые номера элементов, а в словарях — ключи.
Шаг 2. Видно, что результаты хранятся неправильно: у Сидорова 20 очков, хотя
лучшая партия у него та, где он набрал 30 очков. Перед записью будем проверять,
сыграл ли игрок лучше своего прежнего результата (листинг 2.5.2).
Листинг 2.5.2
n=int(input())
V={}
for i in range(n):
s=input().split()
name=s[0]
score=int(s[1])
if name not in V or V[name]<score:
V[name]=score
print(V)
48
Глава 2
Результат:
5
Иванов 10
Петров 20
Сидоров 30
Иванов 30
Сидоров 20
{'Иванов': 30, 'Петров': 20, 'Сидоров': 30}
Шаг 3. Найдем максимальный балл. Воспользуемся методом словаря values, чтобы
получить список значений (листинг 2.5.3.).
Листинг 2.5.3
n=int(input())
V={}
for i in range(n):
s=input().split()
name=s[0]
score=int(s[1])
if name not in V or V[name]<score:
V[name]=score
print(V)
m=max(list(V.values()))
print(m)
Шаг 4. Выведем тех игроков, которые набрали максимальное количество очков
(листинг 2.5.4).
Листинг 2.5.4
n=int(input())
V={}
for i in range(n):
s=input().split()
name=s[0]
score=int(s[1])
if name not in V or V[name]<score:
V[name]=score
print(V)
m=max(list(V.values()))
for name in V:
if V[name]==m:
print(name)
Результат:
5
Иванов 10
Петров 20
Структурное программирование
49
Сидоров 30
Иванов 30
Сидоров 20
{'Иванов': 30, 'Петров': 20, 'Сидоров': 30}
Иванов
Сидоров
Максимальное количество очков набрали два человека: Иванов и Сидоров. Как же
определить победителя? Первым программа напечатала Иванова, т. к. он раньше
появился во вводимых данных. Но реальный победитель — Сидоров, т. к. он
набрал 30 баллов раньше Иванова.
Шаг 5. Выход заключается в том, что надо сохранять не только очки игроков, но и
время, в роли которого будет индекс игры i. И лучше всего это сделать в новом
словаре T (листинг 2.5.5).
Листинг 2.5.5
n=int(input())
V={}
T={}
for i in range(n):
s=input().split()
name=s[0]
score=int(s[1])
if name not in V or V[name]<score:
V[name]=score
T[name]=i
print(V)
print(T)
m=max(list(V.values()))
for name in V:
if V[name]==m:
print(name)
Результат:
5
Иванов 10
Петров 20
Сидоров 30
Иванов 30
Сидоров 20
{'Иванов': 30, 'Петров': 20, 'Сидоров': 30}
{'Иванов': 3, 'Петров': 1, 'Сидоров': 2}
Иванов
Сидоров
Шаг 6. Во втором цикле в новый словарь T1 сохраним время игроков, набравших
максимальный балл (листинг 2.5.6).
50
Глава 2
Листинг 2.5.6
n=int(input())
V={}
T={}
for i in range(n):
s=input().split()
name=s[0]
score=int(s[1])
if name not in V or V[name]<score:
V[name]=score
T[name]=i
print(V)
print(T)
m=max(list(V.values()))
T1={}
for name in V:
if V[name]==m:
T1[name]=T[name]
print(T1)
Результат:
5
Иванов 10
Петров 20
Сидоров 30
Иванов 30
Сидоров 20
{'Иванов': 30, 'Петров': 20, 'Сидоров': 30}
{'Иванов': 3, 'Петров': 1, 'Сидоров': 2}
{'Иванов': 3, 'Сидоров': 2}
Шаг 7. В словаре T1 найдем минимальное время, а потом игрока с этим минимальным временем (листинг 2.5.7).
Листинг 2.5.7
n=int(input())
V={}
T={}
for i in range(n):
s=input().split()
name=s[0]
score=int(s[1])
if name not in V or V[name]<score:
V[name]=score
T[name]=i
print(V)
print(T)
Структурное программирование
51
m=max(list(V.values()))
T1={}
for name in V:
if V[name]==m:
T1[name]=T[name]
print(T1)
t=min(list(T1.values()))
for name in T1:
if T1[name]==t:
print(name)
Результат:
5
Иванов 10
Петров 20
Сидоров 30
Иванов 30
Сидоров 20
{'Иванов': 30, 'Петров': 20, 'Сидоров': 30}
{'Иванов': 3, 'Петров': 1, 'Сидоров': 2}
{'Иванов': 3, 'Сидоров': 2}
Сидоров
Задача решена правильно.
2.6. Проверка
правильности расстановки скобок
Задача
Имеется строчка с разного вида скобками. Проверить правильность расстановки
скобок. Примеры:
1. Правильная строка:
s="(k*[(f+g*(a+b)*d+e)*k+l]+h*(<i>+j))*{m+n}"
2. Неправильная строка (выделенная скобка лишняя):
s="(k*[(f+g*(a+b)*d+e)*k+(l+o]+h*(<i>+j))*{m+n}"
3. Неправильная строка (выделенные скобки нарушают порядок):
s="(k*[(f+g*(a+b)*d+e)*k+{l+0]+h*(p+<i>}+j))*{m+n}"
Языковые конструкции: стек, словарь, множество, строка.
Эту задачу можно решить с помощью примитивных средств, занимаясь только обработкой строк, но алгоритм будет сложным. Гораздо проще решить ее с помощью
сложных структур данных: стека, словаря и множества.
52
Глава 2
Уникальность Python в том, что с помощью встроенных в него коллекций программа получается очень простой. Обычно эта задача в учебниках, посвященных другим языкам программирования, описывается в главе «Объектно-ориентированное
программирование». Хотел и я поместить эту задачу в ту главу. Но, написав код,
понял, что по простоте он ближе к структурному программированию.
Ход программирования
Шаг 1. Разберемся, что такое стек.
Рассмотрим коллекцию — стек, функционирующий по принципу LIFO (Last in,
first out, последним поместили, первым достали). Практический аналог стека — это
стопка тарелок: вы можете положить тарелку только сверху стопки или снять со
стопки только верхнюю тарелку.
В стеке (рис. 2.4) обычно задают два метода: push (поместить в стек) и pop (достать
элемент из стека).
Отдельной встроенной коллекции «стек» в Python нет, но мы можем воспользоваться обычной коллекцией «список». Метод push в нем заменяет append. А вот
метод pop в списке действительно есть — он считывает последний элемент списка,
одновременно удаляя его.
Посмотрим работу списка как стека, сперва добавив в него элементы, а потом прочитав. Выведем на экран результат работы pop и оставшееся содержимое стека
(листинг 2.6.1).
Листинг 2.6.1
L=[]
L.append("a")
L.append("b")
L.append("c")
print(L)
s=L.pop()
print(s,L)
s=L.pop()
print(s,L)
s=L.pop()
print(s,L)
Результат
['a', 'b', 'c']
c ['a', 'b']
b ['a']
Рис. 2.4. Стек
a []
Если список пустой, то pop возвратит ошибку, поэтому надо делать проверку списка
на то, что он не пустой (листинг 2.6.2).
Листинг 2.6.2
L=[]
L.append("a")
L.append("b")
L.append("c")
print(L)
Структурное программирование
53
if len(L)!=0:
s=L.pop()
print(s,L)
if len(L)!=0:
s=L.pop()
print(s,L)
if len(L)!=0:
s=L.pop()
print(s,L)
Шаг 2. Идея алгоритма состоит в следующем. Будем просматривать строку и игнорировать все символы, кроме скобок. Если мы встретим открывающую скобку, то
поместим ее в стек, если закрывающую — извлечем скобку из стека. При правильной расстановке скобок должна быть извлечена парная скобка к текущей. В конце
проверим размер стека — при правильной расстановке скобок он будет пуст.
Пример: ({}[()])
Строка (текущая скобка)
(
{
}
[
(
)
]
Стек для текущей скобки
(
({
(
([
([(
([
([
)
Шаг 3. Для удобства проверки парные скобки удобно поместить в словарь, в котором ключом будет открывающая скобка, а значением — закрывающая:
left={'(':')','[':']','{':'}','<':'>'}
Для удобства программирования сделаем отдельное множество для закрывающих
скобок, извлекая значения из словаря:
right=set(left.values())
Шаг 4. Организуем цикл перебора символов в строке. Определим, относится символ к открывающей или к закрывающей скобке (листинг 2.6.3).
Листинг 2.6.3
left={'(':')','[':']','{':'}','<':'>'}
right=set(left.values())
s="(k*[(f+g*(a+b)*d+e)*k+l]+h*(<i>+j))*{m+n}"
for i in range(len(s)):
if s[i] in left:
pass
elif s[i] in right:
pass
П РИМЕЧАНИЕ К КОДУ
Множество — это неупорядоченный набор элементов без повторений. Оно задается
перечислением элементов в фигурных скобках — например: {"a","ab","bsd","c"}. Два
главных отличия множества от списка состоят в том, что в списке элементы могут повторяться, а в множестве — нет. Кроме того, во множестве нет индексации. То есть
54
Глава 2
нельзя обратиться к элементу множества с помощью квадратных скобок. Список может быть преобразован во множество с помощью функции set: S=set(L) (в этом случае
дубликаты удалятся), а множество — в список с помощью list: L=set(S). Над множествами возможны операции объединения (|), пересечения (оператор &), разности (оператор -).
Шаг 5. Создадим стек. Если текущий символ строки — открывающая скобка, то
просто поместим ее в стек (листинг 2.6.4).
Листинг 2.6.4
left={'(':')','[':']','{':'}','<':'>'}
right=set(left.values())
s="(k*[(f+g*(a+b)*d+e)*k+l]+h*(<i>+j))*{m+n}"
stack=[]
for i in range(len(s)):
if s[i] in left:
stack.append(s[i])
elif s[i] in right:
pass
Шаг 6. Если скобка закрывающая, то ошибка будет в том случае, если стек пуст,
или последний элемент в нем не является парной скобкой для текущего символа
строки. Для общего вывода о правильности расстановки скобок введем переменную-флаг (листинг 2.6.5).
Листинг 2.6.5
left={'(':')','[':']','{':'}','<':'>'}
right=set(left.values())
s="(k*[(f+g*(a+b)*d+e)*k+l]+h*(<i>+j))*{m+n}"
stack=[]
f=True
for i in range(len(s)):
if s[i] in left:
stack.append(s[i])
elif s[i] in right:
if len(stack)==0 or s[i]!=left[stack.pop()]:
f=False
break
Шаг 7. После цикла сделаем проверку стека на то, что он не пустой, и выведем результат проверки на экран. Мы получили работающую программу (листинг 2.6.6).
Листинг 2.6.6
left={'(':')','[':']','{':'}','<':'>'}
right=set(left.values())
s="(k*[(f+g*(a+b)*d+e)*k+l]+h*(<i>+j))*{m+n}"
Структурное программирование
55
stack=[]
f=True
for i in range(len(s)):
if s[i] in left:
stack.append(s[i])
elif s[i] in right:
if len(stack)==0 or s[i]!=left[stack.pop()]:
f=False
break
if len(stack)>0:
f=False
print(f)
Итак, эта глава дополнила предыдущую (посвященную условиям) циклами и встроенными в Python коллекциями. Вместе они составляют структурное программирование, программы которого очень наглядны и понятны... до тех пор, пока не
становятся очень большими. А для больших программ нужна декомпозиция на небольшие фрагменты кода. Этому посвящена следующая глава.
56
Глава 2
ГЛАВА
3
Функции
Отличие задач этой главы от задач предыдущей — в том, что для их решения удобны (а для двух последних разделов и обязательны) функции. Здесь мы уже имеем
нетривиальные алгоритмы, которые тяжело придумать самостоятельно. Поэтому
читатель, считающий, что умеет программировать, может прочитать идею алгоритма, а потом попробовать написать программу самостоятельно.
Несколько особняком стоит последний раздел, посвященный функциональному
программированию. Он не дает новых алгоритмов и даже не решает какую-либо
хорошую задачу. С этой точки зрения его можно смело пропустить. Но он полностью меняет мышление, заставляя строить программы совершенно иным способом,
чем мы привыкли!
3.1. Решето Эратосфена и числа-близнецы
Задача
Вводится n. Найти:
1. Простые числа, меньшие или равные n, методом решета Эратосфена. Простыми
числами называются натуральные числа, которые делятся только на 1 и на самих
себя (не раскладываются на множители): 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31...
2. Из найденных простых чисел найти числа-близнецы (отличающиеся друг от
друга не более чем на 2): 2, 3, 5, 7 и 11, 13 и 17, 19 и 29, 31...
Языковые конструкции: списки, множества, циклы, функции.
Идея алгоритма
Задача была решена еще древнегреческим математиком Эратосфеном — он наносил числа на восковую дощечку (рис. 3.1) и выкалывал те, которые разлагаются на
множители (решето Эратосфена).
Выколем сначала числа, делящиеся на 2 (рис. 3.2). Следующее невыколотое число
после 2 — это 3. Выколем теперь числа, кратные трем (рис. 3.3). Далее выкалываем
числа, делящиеся на 5 (рис. 3.4). Затем — числа, делящиеся на 7 (рис. 3.5). И так
далее...
58
Глава 3
Рис. 3.1. Числа на «восковой дощечке»
Рис. 3.2. Выкалывание чисел, делящихся на 2
Рис. 3.3. Выкалывание чисел, делящихся на 3
Рис. 3.4. Выкалывание чисел, делящихся на 5
Рис. 3.5. Выкалывание чисел, делящихся на 7
Функции
59
Ход программирования
Шаг 1. Создадим список чисел от 0 до n (листинг 3.1.1).
Листинг 3.1.1
n=int(input())
L=list(range(n+1))
print(L)
Шаг 2. Перебирая список от числа 2, ищем «невыколотое» число (листинг 3.1.2).
Выкалыванию будет соответствовать обнуление.
Листинг 3.1.2
n=int(input())
L=list(range(n+1))
for i in range(2,n+1)
if L[i]!=0:
#здесь будет выкалывание чисел
print(L)
Шаг 3. Для каждого ненулевого L[i] надо выколоть все числа за ним с шагом, равным тому же L[i], — например, для L[i]=3 выкалываем 6, 9, 12... Для этого организуем вложенный цикл (листинг 3.1.3).
Листинг 3.1.3
n=int(input())
L=list(range(n+1))
for i in range(2,n+1):
if L[i]!=0:
for j in range(L[i]*2,n+1,L[i]):
L[j]=0
print(L)
Шаг 4. Осталось удалить выколотые числа, т. е. нули. Самый короткий программный способ — это удалить дубликаты с помощью преобразования во множество set
и обратно в список. Так мы получим список простых чисел (листинг 3.1.4).
Листинг 3.1.4
n=int(input())
L=list(range(n+1))
for i in range(2,n+1):
if L[i]!=0:
for j in range(L[i]*2,n+1,L[i]):
L[j]=0
L=sorted(list(set(L)))
print(L)
60
Глава 3
Результат:
100
[0, 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73,
79, 83, 89, 97]
Шаг 5. Задумаемся об оптимальности работы программы. Если n = 100, то у него
нет делителей со значением больше 50. То есть верхнюю границу цикла i можно
обозначить как n/2 (листинг 3.1.5).
Листинг 3.1.5
n=int(input())
L=list(range(n+1))
for i in range(2,n//2):
if L[i]!=0:
for j in range(L[i]*2,n+1,L[i]):
L[j]=0
L=sorted(list(set(L)))
print(L)
Шаг 6. Посмотрим на нижнюю границу цикла. Мы ее выбрали как L[i]+L[i]=L[i]*2.
Но это величина, делящаяся на 2, и мы ее уже выкололи при i = 2. Если мы возьмем
за нее L[i]*3, то она была выколота при i = 3. Значит, стартовать нужно с L[i]*L[i].
Мы получили программу для поиска простых чисел (листинг 3.1.6).
Листинг 3.1.6. Простые числа с помощью решета Эратосфена
n=int(input())
L=list(range(n+1))
for i in range(2,n//2):
if L[i]!=0:
for j in range(L[i]*2,n+1,L[i]):
L[j]=0
L=sorted(list(set(L)))
print(L)
Шаг 7. Из списка простых чисел выберем числа-близнецы — будем перебирать
список простых чисел и смотреть на соседние. Если разница с обоими соседями
больше 2, то число выкалываем (листинг 3.1.7).
Листинг 3.1.7
n=int(input())
L=list(range(n+1))
for i in range(2,n//2):
if L[i]!=0:
for j in range(L[i]*2,n+1,L[i]):
L[j]=0
L=sorted(list(set(L)))
Функции
61
print(L)
for i in range(len(L)):
if L[i]-L[i-1]>2 and L[i+1]-L[i]>2:
L[i]=0
L=sorted(list(set(L)))
print(L)
Шаг 8. Если мы сейчас запустим программу, то увидим ошибку: выход за пределы
списка. Это связано с тем, что у последнего элемента нет следующего соседа (у нулевого элемента нет предыдущего, но это не приводит к ошибке, т. к. Python сравнивает L[0] и L[-1] — последний элемент). Уменьшим правую границу цикла, сделаем отдельную проверку для последнего элемента и получим готовую программу
для чисел-близнецов (листинг 3.1.8).
Листинг 3.1.8. Числа-близнецы
n=int(input())
L=list(range(n+1))
for i in range(2,n//2):
if L[i]!=0:
for j in range(L[i]*2,n+1,L[i]):
L[j]=0
L=sorted(list(set(L)))
print(L)
for i in range(len(L)-1):
if L[i]-L[i-1]>2 and L[i+1]-L[i]>2:
L[i]=0
if L[-1]-L[-2]>2:
L[-1]=0
L=sorted(list(set(L)))
print(L)
Результат:
100
[0, 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73,
79, 83, 89, 97]
[0, 1, 2, 3, 5, 7, 11, 13, 17, 19, 29, 31, 41, 43, 59, 61, 71, 73]
Шаг 9. Числа-близнецы идут сплошным списком. Разместим каждую группу близнецов в подсписок (листинг 3.1.9), сделав список списков (так мы поступали в задаче про метеостанцию в разд. 2.2).
Листинг 3.1.9. Числа-близнецы сгруппированные
n=int(input())
L=list(range(n+1))
for i in range(2,n//2):
if L[i]!=0:
for j in range(L[i]*2,n+1,L[i]):
L[j]=0
62
Глава 3
L=sorted(list(set(L)))
print(L)
for i in range(len(L)-1):
if L[i]-L[i-1]>2 and L[i+1]-L[i]>2:
L[i]=0
if L[-1]-L[-2]>2:
L[-1]=0
L=sorted(list(set(L)))
print(L)
Результат:
100
[0, 1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73,
79, 83, 89, 97]
[0, 1, 2, 3, 5, 7, 11, 13, 17, 19, 29, 31, 41, 43, 59, 61, 71, 73]
[[0, 1, 2, 3, 5, 7], [11, 13], [17, 19], [29, 31], [41, 43], [59, 61], [71, 73]]
Шаг 10. Программа разрослась, и уместно декомпозировать ее в функции. Сделаем
функции формирования списков простых чисел, чисел-близнецов одним списком
и потом списком списков, а также вспомогательную функцию удаления лишних
нулей (листинг 3.1.10).
Листинг 3.1.10. Декомпозиция в функции
def squeeze(L):
return sorted(list(set(L)))
def prime(n):
L=list(range(n+1))
for i in range(2,n//2):
if L[i]!=0:
for j in range(L[i]*2,n+1,L[i]):
L[j]=0
L=squeeze(L)
return L
def twin(L):
for i in range(len(L)-1):
if L[i]-L[i-1]>2 and L[i+1]-L[i]>2:
L[i]=0
if L[-1]-L[-2]>2:
L[-1]=0
L=squeeze(L)
return L
def listoflists(L):
M=[[L[0]]]
for i in range(1,len(L)):
if L[i]-M[-1][-1]<=2:
M[-1].append(L[i])
Функции
63
else:
M.append([L[i]])
return M
print(listoflists(twin(prime(int(input())))))
П РИМЕЧАНИЕ К КОДУ
Выражение def имя_функции (список аргументов) задает важную языковую конструкцию —
функцию. Из примера видно, что функция — это законченный по смыслу и обособленный фрагмент кода. В функции можно помещать формулы, и тогда функции становятся функциями в математическом смысле. В общем же случае функции — это обособленные алгоритмы (последовательности действий с ветвлениями и повторениями).
Когда программа, разработанная в соответствии со структурным стилем (парадигмой)
программирования, становится большой и представляет собой вложенные друг в друга структурные блоки циклов и условий, она становится плохо читаемой и отлаживаемой. Поэтому стоит декомпозировать программу в маленькие обособленные кусочки
кода — функции. Каждую функцию надо отладить отдельно и потом можно уже забыть, как именно работает алгоритм внутри нее. Такая программа становится более
простой для понимания. И здесь мы переходим к следующему стилю (парадигме) программирования, когда программа представляет собой множество функций, которые
вызывают друг друга. С функциями связаны особые приемы программирования.
Шаг 11. Заметим, что удаление нулей-дубликатов в функции squeeze остроумно, но
не оптимально. Превращение списка во множество может нарушать порядок, поэтому приходится далее сортировать список. Это медленная операция. Так что
лучше построить новый список, поместив в него элементы старого списка, отличные от нуля. Сделать это можно в разных стилях (листинги 3.1.11–3.1.13).
Листинг 3.1.11. Структурное программирование
def squeeze(L):
M=[]
for el in L:
if el>0:
M.append(el)
return M
Листинг 3.1.12. Стиль Python
def squeeze(L):
return [el for el in L if el>0]
Листинг 3.1.13. Функциональное программирование
def squeeze(L):
return list(filter(lambda n:n>0,L))
Со списочными выражениями мы уже встречались. Программа в функциональном
стиле представляет собой вызов функции фильтрации filter, у которой два аргумента: второй аргумент — список, а первый аргумент — анонимная функция, которая задает критерий отбора. Эта функция задается с помощью ключевого слова
64
Глава 3
lambda, после которого указываются
мент n), затем идет двоеточие и тело
аргументы (в нашем случае — один аргуфункции n>0, которое возвращает истину для
положительных чисел и ложь для всех остальных.
Подробнее анонимные функции и приемы функционального программирования
будут рассмотрены в разд. 3.8 и в главе 6. Если вы не хотите в них вникать, то можете просто запомнить, как выглядит фильтрация:
list(filter(lambda n:n>0,L))
и использовать этот код, меняя n>0 на условия из ваших задач.
Приведем код программы полностью (листинг 3.1.14).
Листинг 3.1.14
def squeeze(L):
return list(filter(lambda n:n>0,L))
def prime(n):
L=list(range(n+1))
for i in range(2,n//2):
if L[i]!=0:
for j in range(L[i]*2,n+1,L[i]):
L[j]=0
L=squeeze(L)
return L
def twin(L):
for i in range(len(L)-1):
if L[i]-L[i-1]>2 and L[i+1]-L[i]>2:
L[i]=0
if L[-1]-L[-2]>2:
L[-1]=0
L=squeeze(L)
return L
def listoflists(L):
M=[[L[0]]]
for i in range(1,len(L)):
if L[i]-M[-1][-1]<=2:
M[-1].append(L[i])
else:
M.append([L[i]])
return M
print(listoflists(twin(prime(int(input())))))
Функции
65
3.2. Решето Сундарама
Задача
Вводится n. Вывести список простых чисел, не превышающих n. Найти их методом
решета Сундарама.
Языковые конструкции: списки.
Идея алгоритма
В отличие от решета Эратосфена, имеющего давнюю историю, этот алгоритм был
придуман индийским математиком Сундарамом в XX веке. Рассмотрим идею алгоритма.
Все простые числа, кроме числа 2, — нечетные. Нечетные числа можно получить
из натурального ряда путем умножения на 2 и прибавления 1:
Пусть m — натуральное число, тогда:
2m + 1 — нечетное число.
Но не все нечетные числа нам подойдут. Есть числа, являющиеся произведением
двух нечетных чисел. Пусть это будут числа 2i + 1 и 2j + 1, тогда
2m + 1 = (2i+1)(2j + 1).
Упростим это выражение и выразим m:
2m + 1=4ij + 2i + 2j + 1,
2m = 4ij + 2i+2j,
m=2ij + i + j.
Таким образом, алгоритм сводится к формированию ряда простых чисел и удалению тех из них, которые равны 2ij + i + j. Потом по сокращенному ряду натуральных чисел конструируются простые числа.
Ход программирования
Шаг 1. Построим список натуральных чисел от 1 до n (листинг 3.2.1).
Листинг 3.2.1
n=int(input())
L=list(range(n+1))
Шаг 2. Создадим два цикла с индексами i и j (j вложен в i), обнулим числа с индексами 2ij + i + j, удалим дубликаты, как мы это делали в предыдущем разделе,
и получим программу, приведенную в листинге 3.2.2.
Листинг 3.2.2
n=int(input())
L=list(range(n+1))
66
Глава 3
for i in range(1,n+1):
for j in range(1,n+1):
if 2*i*j+i+j<n:
L[2*i*j+i+j]=0
L=[el for el in L if el>0]
print(L)
Шаг 3. Каждый элемент списка умножим на два и прибавим 1, получив простые
числа (листинг 3.2.3).
Листинг 3.2.3
n=int(input())
L=list(range(n+1))
for i in range(1,n+1):
for j in range(1,n+1):
if 2*i*j+i+j<n:
L[2*i*j+i+j]=0
L=[el for el in L if el>0]
for i in range(len(L)):
L[i]=2*L[i]+1
print(L)
Шаг 4. Заметим, что в результате пропущено простое число 2 (оно единственное
является четным, сам же алгоритм работает только с нечетными числами). Добавим
к списку 2 и получим готовую программу (листинг 3.2.4).
Листинг 3.2.4. Решето Сундарама
n=int(input())
L=list(range(n+1))
for i in range(1,n+1):
for j in range(1,n+1):
if 2*i*j+i+j<n:
L[2*i*j+i+j]=0
L=[el for el in L if el>0]
for i in range(len(L)):
L[i]=2*L[i]+1
L.insert(0,2)
print(L)
Шаг 5. Программа нами написана в структурном стиле с элементами «пайтоновского» стиля (фильтрация с использованием списочного выражения):
L=[el for el in L if el>0]
Добавим элементы функционального программирования. Сделаем фильтрацию
с использованием функции filter так же, как мы это делали в предыдущем разделе:
L=list(filter(lambda x:x>0,L))
Функции
67
Но возможности функционального программирования этим не исчерпываются.
В конце программы мы преобразуем список, меняя каждый его элемент по формуле
2*L[i]+1. Такое преобразование списков с применением к каждому его элементу некоторой функции — типовая задача, и для ее решения в Python есть функция map:
L=list(map(lambda x:2*x+1,L))
Здесь так же, как и в filter, используется анонимная функция lambda. Подробнее об
анонимных функциях будет рассказано в разд. 3.8. Если же вы не хотите углубляться в функциональное программирование, то просто можете использовать
выражение map как шаблон, подставляя после двоеточия нужную вам формулу для
изменения элемента списка. Полный код программы приведен в листинге 3.2.5.
Листинг 3.2.5. Решето Сундарама с элементами функционального программирования
n=int(input())
L=list(range(n+1))
for i in range(1,n+1):
for j in range(1,n+1):
if 2*i*j+i+j<n:
L[2*i*j+i+j]=0
L=list(filter(lambda x:x>0,L))
L=list(map(lambda x:2*x+1,L))
L.insert(0,2)
print(L)
3.3. Лесенка
Задача
Вводится количество блоков. Нужно вывести возможное количество лесенок из
этих блоков. Блоки мы можем ставить друг на друга, но так, чтобы на каждом следующем уровне лесенки количество блоков было меньше, чем на предыдущем.
Языковые конструкции: списки, функции.
Ход программирования
Шаг 1. Прежде всего надо понять, как мы будем хранить результат. Поскольку
лестниц может быть очень много, создадим двумерный список, в котором первое
измерение — номер лестницы, а второе — уровни лестницы (листинг 3.3.1). Первая
лестница такова, что все блоки находятся на нижнем уровне. Пусть количество
блоков n = 10 (рис. 3.6).
Рис. 3.6. Десять блоков
68
Глава 3
Листинг 3.3.1
n=int(input())
L=[n]+[0]*(n-1)
print(L)
M=[L]
Результат
10
[10, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Шаг 2. Будем строить новые лестницы, модифицируя уже построенные, — перетаскивая блоки с нижних уровней на верхние. Нам нужен цикл для просмотра уже
построенных лестниц (листинг 3.3.1).
Листинг 3.3.2
n=int(input())
L=[n]+[0]*(n-1)
print(L)
M=[L]
for L in M:
# здесь будет модификация уже созданных лестниц
Посмотрим на первые шаги построения лесенки (рис. 3.7).
Рис. 3.7. Первые шаги
построения лесенки
Рис. 3.8. Подъем блока на разные уровни
Шаг 3. Следующий блок с нижнего этажа можно поднять как на второй уровень,
так и на первый, кроме того, можно предположить, что в будущем мы будем поднимать блоки не только с нижнего уровня (рис. 3.8).
Это означает, что нам придется в цикл перебора готовых лесенок включить еще два
вложенных друг в друга цикла. В первом цикле мы будем выбирать уровень, с которого поднимаем блок, а во втором — на какой уровень складываем. Перетаскиванию блока с i-го этажа на j-й соответствует следующий код:
Функции
69
L1[i]=L1[i]-1
L1[j]=L1[j]+1
Напишем эти два вложенных цикла. Заметим, что мы можем поднимать блоки
только с тех уровней, на которых количество блоков больше двух. Поскольку одну
и ту же лестницу можно модифицировать разными способами, скопируем лестницу L в список L1.
Простое приравнивание L1=L будет работать плохо. Реальная копия списка не создастся, фактически L1 — это другое имя для того же самого списка. Чтобы создавался
новый список с копиями элементов, воспользуемся функцией copy из одноименной
библиотеки:
from copy import copy
L1=copy(L)
и получим следующую заготовку программы (листинг 3.3.3).
Листинг 3.3.3
n=int(input())
L=[n]+[0]*(n-1)
print(L)
M=[L]
for L in M:
for i in range(len(L)):
if L[i]>2:
for j in range(i+1,len(L)):
L1=copy(L)
L1[i]=L1[i]-1
L1[j]=L1[j]+1
Шаг 4. Порождая новую лестницу, нам нужно убедиться, что она правильная, т. е.
что не нарушается принцип: на каждом следующем этаже блоков меньше, чем на
предыдущем. Напишем для этого отдельную функцию с применением флага (листинг 3.3.4).
Листинг 3.3.4
def check(L):
f=True
for i in range(1,len(L)):
if L[i]>=L[i-1]:
f=False
break
return f
Но этого мало. Допустим, есть лестница:
[3,2,1,0,0,0,0,0]
70
Глава 3
В конце идут одни нули, т. е. этажи с одинаковым количеством блоков. Кроме того,
есть риск построения лестницы с «подвешенными» блоками:
[3,2,1,0,0,1,0,0]
Найдя этаж с нулем блоков, сделаем срез списка с оставшимися этажами. Преобразуем его во множество (уберем дубликаты) и убедимся, что во множестве только
один элемент — 0 (листинг 3.3.5).
Листинг 3.3.5
def check(L):
f=True
for i in range(1,len(L)):
if L[i]>=L[i-1]:
f=False
break
if L[i]==0:
if len(set(L[i:]))>1:
f=False
break
return f
Проверим, правильная ли получилась лестница, и внесем ее в список M (листинг 3.3.6)
Листинг 3.3.6
...
n=int(input())
L=[n]+[0]*(n-1)
print(L)
M=[L]
for L in M:
for i in range(len(L)):
if L[i]>2:
for j in range(i+1,len(L)):
L1=copy(L)
L1[i]=L1[i]-1
L1[j]=L1[j]+1
if check(L1):
print(L1)
M.append(L1)
Шаг 5. Продолжим модифицировать левую нижнюю лесенку (см. рис. 3.8) и обнаружим, что в одной из своих модификаций она порождает уже созданную нами
лесенку — нижнюю правую (рис. 3.9).
Мы не можем сразу сохранять модифицированные лестницы в конце списка лестниц, т. к. модифицированная лестница может совпасть с уже имеющейся у нас
в списке.
Функции
71
Рис. 3.9. Повторное создание лестниц
Продолжая поднимать блоки, убеждаемся, что модифицированные лестницы часто
повторяются (рис. 3.10).
Условие добавления лестницы в список нужно дополнить проверкой, что ее нет
в этом списке:
if check(L1) and L1 not in M:
Итоговая программа приведена в листинге 3.3.7.
Листинг 3.3.7. Лесенки
from copy import copy
def check(L):
f=True
for i in range(1,len(L)):
if L[i]>=L[i-1]:
f=False
break
if L[i]==0:
if len(set(L[i:]))>1:
f=False
break
return f
n=int(input())
L=[n]+[0]*(n-1)
72
Глава 3
print(L)
M=[L]
for L in M:
for i in range(len(L)):
if L[i]>2:
for j in range(i+1,len(L)):
L1=copy(L)
L1[i]=L1[i]-1
L1[j]=L1[j]+1
if check(L1) and L1 not in M:
print(L1)
M.append(L1)
Рис. 3.10. Все лесенки из десяти блоков
Функции
73
3.4. Линейный и медианный фильтры
Решим две похожие друг на друга задачи из области обработки сигналов и изображений — линейный и медианный фильтры в нескольких вариантах.
Задача 1
Вводится сигнал в виде двумерного списка и апертура линейного фильтра (размер
окрестности точки). Запрограммировать линейный фильтр: новое значение каждого
элемента списка должно быть средним арифметическим его самого и соседних
элементов списка.
Например, пусть задана матрица (значения ячеек для наглядности зададим в буквенном виде):
⎛a
⎜
⎜e
⎜i
⎜
⎜m
⎝
b
f
j
n
c
g
k
o
d⎞
⎟
h⎟
.
l⎟
⎟
p ⎟⎠
Апертура здесь равна 3.
Чтобы подсчитать новое значение, например, в клетке f, нужно выбрать ближайшую окрестность клетки f — квадрат размером 3 на 3:
⎛a
⎜
⎜e
⎜i
⎝
b
f
j
c⎞
⎟
g⎟,
k ⎟⎠
и вычислить среднее арифметическое его элементов:
a+b+c+e+ f + g +i+ j+k
.
9
Поскольку у крайних элементов исходной матрицы окрестности 3 на 3 нет, полученная в результате линейной фильтрации матрица будет меньше исходной:
⎛ a+b+c+e+
⎜
⎜
⎜e+ f + g +i+
⎜
⎝
f + g +i+ j+k
9
j+k +m+n+o
9
b+c+d + f + g +h+ j+k +l ⎞
⎟
9
⎟.
f + g +h+ j+k +l +n+o+ p⎟
⎟
9
⎠
Языковые конструкции: списки, двумерные списки, циклы.
Ход программирования
Будем решать эту задачу, сначала предельно упростив ее задание и постепенно
усложняя. Таким образом, мы напишем несколько алгоритмов для частных случаев.
74
Глава 3
Шаг 1. Пусть дан одномерный список. Новое значение элемента списка вычислим
как среднее арифметическое ближайших соседей. При этом мы не станем обрабатывать крайние клетки (листинг 3.4.1):
Bi =
Ai −1 + Ai + Ai +1
.
3
Листинг 3.4.1. Линейный фильтр для одномерного списка с апертурой 3
L=[1,3,2,4,6,5,7,9]
print(L)
M=[]
for i in range(1,len(L)-1):
M.append((L[i-1]+L[i]+L[i+1])/3)
print(M)
Результат:
[1, 3, 2, 4, 6, 5, 7, 9]
[2.0, 3.0, 4.0, 5.0, 6.0, 7.0]
Шаг 2. Решим задачу для одномерного списка с задаваемой апертурой w (не обязательно равной 3). Для этого понадобится вложенный цикл (листинг 3.4.2).
Листинг 3.4.2. Линейный фильтр для одномерного списка с апертурой w
L=[1,3,2,4,6,5,7,9]
print(L)
w=5
M=[]
for i in range(w//2,len(L)-w//2):
s=0
for j in range(i-w//2,i+w//2+1):
s=s+L[j]
M.append(s/w)
print(M)
Результат:
[1, 3, 2, 4, 6, 5, 7, 9]
[3.2, 4.0, 4.8, 6.2]
Шаг 3. Программу предыдущего шага можно написать короче — с использованием
срезов, списочного выражения и функции sum (листинг 3.4.3).
Листинг 3.4.3
L=[1,3,2,4,6,5,7,9]
print(L)
w=5
M=[]
Функции
75
for i in range(w//2,len(L)-w//2):
M.append(sum(L[i-w//2:i+w//2+1])/w)
print(M)
Шаг 4. Или еще короче — с дополнительным списочным выражением (листинг 3.4.4).
П РИМЕЧАНИЕ К КОДУ
Оператор // означает целочисленное деление (с отбрасыванием дробной части).
Листинг 3.4.4
L=[1,3,2,4,6,5,7,9]
print(L)
w=5
M=[sum(L[i-w//2:i+w//2+1])/w for i in range(w//2,len(L)-w//2)]
print(M)
Шаг 5. Подумаем об оптимальности. Посмотрим, как вычисляются первые три
элемента:
L0 + L1 + L2 + L3 + L4
,
5
L + L2 + L3 + L4 + L5
,
M1 = 1
5
L + L3 + L4 + L5 + L6
.
M2 = 2
5
M0 =
Видно, что соседние суммы различаются первым и последним элементом. Это значит, что постоянно подсчитывать суммы всех элементов неоптимально. Достаточно
вычислить сумму для первых элементов:
s=sum(L[:w-1])
дальше добавлять новый элемент:
s=s+L[i]
и вычитать самый старый элемент из суммы:
s=s-L[i-w+1]
В итоге скорость программы не будет зависеть от размера апертуры (листинг 3.4.5).
Листинг 3.4.5. Линейный фильтр для одномерного списка оптимизированный
L=[1,3,2,4,6,5,7,9]
print(L)
w=5
M=[]
s=sum(L[:w-1])
76
Глава 3
for i in range(w-1,len(L)):
s=s+L[i]
M.append(s/w)
s=s-L[i-w+1]
print(M)
Шаг 6. Перейдем теперь к двумерному списку и рассмотрим список 5×5 с апертурой линейного фильтра 3 (рис. 3.11).
Рис. 3.11. Список 5×5
Рис. 3.12. Суммирование окрестности ячейки
с содержимым 7
Стартуем с ячейки с содержимым 7 и вычислим новое ее значение путем суммирования окрестности.
1. Для ячейки с содержимым 7 (рис. 3.12).
2. Для ячейки с содержимым 8 (рис. 3.13).
3. Для ячейки с содержимым 9 (рис. 3.14).
Рис. 3.13. Суммирование окрестности ячейки
с содержимым 8
Рис. 3.14. Суммирование окрестности ячейки
с содержимым 9
Видно, что суммы этих ячеек различаются на один столбец. То есть мы для ячейки
с содержимым 7 формируем сумму, а в дальнейшем вычитаем левый столбец и
прибавляем правый.
Но у нас двумерный случай, поэтому, закончив одну строчку, нам нужно перейти
к следующей. А там уже новые столбцы. Означает ли это, что нам нужно обнулять
сумму и формировать ее заново? Да, если мы сохраняем обычный способ обхода
двумерного списка с помощью цикла, вложенного в другой цикл. Но мы можем
выбрать другой способ обхода списка. Например, после ячейки с содержимым 9
перейти к клетке с содержимым 14, а дальше просматривать новую строчку уже
справа налево. Это еще один прием программирования — альтернативный обход
Функции
77
списка. В нашем случае он называется бустрофедон (от древнегреческого быка —
был такой способ письма, напоминающий движение быка с плугом на поле «змейкой»). Для нашего примера дальнейший обход будет выглядеть так:
1. Для ячейки с содержимым 14 (рис. 3.15).
2. Для ячейки с содержимым 13 (рис. 3.16).
Рис. 3.15. Суммирование окрестности ячейки
с содержимым 14
Рис. 3.16. Суммирование окрестности ячейки
с содержимым 13
3. Для ячейки с содержимым 12 (рис. 3.17).
4. Для ячейки с содержимым 17 (рис. 3.18).
Рис. 3.17. Суммирование окрестности ячейки
с содержимым 12
Рис. 3.18. Суммирование окрестности ячейки
с содержимым 17
5. Для ячейки с содержимым 18 (рис. 3.19).
6. Для ячейки с содержимым 19 (рис. 3.20).
Теперь видно, что в этом случае при переходе на новую строчку мы должны
вычесть из суммы верхнюю строчку и добавить нижнюю.
Рис. 3.19. Суммирование окрестности ячейки
с содержимым 18
Рис. 3.20. Суммирование окрестности ячейки
с содержимым 19
78
Глава 3
Напишем заготовку для программы, введя двумерные списки, дополнительные
индексы для обхода списка, и сделаем обход массива бустрофедоном, используя
переключатель — переменную, меняющую свое значение в цикле, как правило,
между двумя значениями (листинг 3.4.6).
Листинг 3.4.6
L=[[1,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20],
[21,22,23,24,25]]
w=3
n=len(L)
m=len(L[0])
t=1
for i in range(w//2,n-w//2):
for j in range(w//2,m-w//2):
if t==1:
print(L[i][j])
else:
print(L[i][-j-1])
t=-t
Результат
7
8
9
14
13
12
17
18
19
По результату вывода на экран убеждаемся, что обход списка бустрофедоном сделан правильно.
Шаг 7. Сформируем суммы для каждой клетки и проверим, что они вычисляются
верно (листинг 3.4.7).
Листинг 3.4.7
L=[[1,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20],
[21,22,23,24,25]]
w=3
n=len(L)
m=len(L[0])
s=sum([sum(L[i][:w-1]) for i in range(w-1)])
t=1
for i in range(w//2,n-w//2):
if t==1:
s=s+sum([L[i+w//2][k] for k in range(w-1)])
else:
s=s+sum([L[i+w//2][-k-1] for k in range(w-1)])
Функции
for j in range(w//2,m-w//2):
if t==1:
s=s+sum([L[k][j+w//2] for k in range(i-w//2,i+w//2+1)])
print(L[i][j],s)
s=s-sum([L[k][j-w//2] for k in range(i-w//2,i+w//2+1)])
else:
s=s+sum([L[k][-j-1-w//2] for k in range(i-w//2,i+w//2+1)])
print(L[i][-j-1],s)
s=s-sum([L[k][-j-1+w//2] for k in range(i-w//2,i+w//2+1)])
if t==1:
s=s-sum([L[i-w//2][-k-1] for k in range(w-1)])
else:
s=s-sum([L[i-w//2][k] for k in range(w-1)])
t=-t
Результат:
7 63
8 72
9 81
14 126
13 117
12 108
17 153
18 162
19 171
П РИМЕЧАНИЕ К КОДУ
Функция sum вычисляет сумму элементов коллекции.
Шаг 8. Подсчитаем средние арифметические и запишем в M (листинг 3.4.8).
Листинг 3.4.8
L=[[1,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20],
[21,22,23,24,25]]
w=3
v=w**2
n=len(L)
m=len(L[0])
s=sum([sum(L[i][:w-1]) for i in range(w-1)])
t=1
M=[[0]*m for i in range(n)]
for i in range(w//2,n-w//2):
if t==1:
s=s+sum([L[i+w//2][k] for k in range(w-1)])
79
80
Глава 3
else:
s=s+sum([L[i+w//2][-k-1] for k in range(w-1)])
for j in range(w//2,m-w//2):
if t==1:
s=s+sum([L[k][j+w//2] for k in range(i-w//2,i+w//2+1)])
M[i][j]=s/v
s=s-sum([L[k][j-w//2] for k in range(i-w//2,i+w//2+1)])
else:
s=s+sum([L[k][-j-1-w//2] for k in range(i-w//2,i+w//2+1)])
M[i][-j-1]=s/v
s=s-sum([L[k][-j-1+w//2] for k in range(i-w//2,i+w//2+1)])
if t==1:
s=s-sum([L[i-w//2][-k-1] for k in range(w-1)])
else:
s=s-sum([L[i-w//2][k] for k in range(w-1)])
t=-t
for i in range(n):
print(M[i])
Результат:
[0,
[0,
[0,
[0,
[0,
0, 0, 0, 0]
7.0, 8.0, 9.0, 0]
12.0, 13.0, 14.0, 0]
17.0, 18.0, 19.0, 0]
0, 0, 0, 0]
Шаг 9. Видим, что по краям результирующей матрицы — нули (для этих клеток
мы не вычисляем новое значение, т. к. для них не существует нужной окрестности).
Обрежем края и получим готовую версию программы (листинг 3.4.9).
Листинг 3.4.9. Линейный фильтр для двумерного списка
L=[[1,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20],
[21,22,23,24,25]]
w=3
v=w**2
n=len(L)
m=len(L[0])
s=sum([sum(L[i][:w-1]) for i in range(w-1)])
t=1
M=[[0]*(m) for i in range(n)]
for i in range(w//2,n-w//2):
if t==1:
s=s+sum([L[i+w//2][k] for k in range(w-1)])
else:
s=s+sum([L[i+w//2][-k-1] for k in range(w-1)])
Функции
81
for j in range(w//2,m-w//2):
if t==1:
s=s+sum([L[k][j+w//2] for k in range(i-w//2,i+w//2+1)])
M[i][j]=s/v
s=s-sum([L[k][j-w//2] for k in range(i-w//2,i+w//2+1)])
else:
s=s+sum([L[k][-j-1-w//2] for k in range(i-w//2,i+w//2+1)])
M[i][-j-1]=s/v
s=s-sum([L[k][-j-1+w//2] for k in range(i-w//2,i+w//2+1)])
if t==1:
s=s-sum([L[i-w//2][-k-1] for k in range(w-1)])
else:
s=s-sum([L[i-w//2][k] for k in range(w-1)])
t=-t
M=M[w//2:-w//2+1]
for i in range(len(M)):
M[i]=M[i][w//2:-w//2+1]
for i in range(len(M)):
print(M[i])
Результат:
[7.0, 8.0, 9.0]
[12.0, 13.0, 14.0]
[17.0, 18.0, 19.0]
Шаг 10. Еще больше усложним задачу. Почему обязательно окрестность должна
быть квадратной? Мы можем выбрать окрестность, например, в виде креста
(рис. 3.21).
Рис. 3.21. Окрестность в виде креста
Чтобы решить эту задачу, напишем версию предыдущей задачи без накапливающихся сумм — для каждой клетки будем складывать всю окрестность заново (листинг 3.4.10).
Листинг 3.4.10
L=[[1,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20],
[21,22,23,24,25]]
82
Глава 3
w=3
v=w**2
n=len(L)
m=len(L[0])
M=[[0]*(m) for i in range(n)]
for i in range(w//2,n-w//2):
for j in range(w//2,m-w//2):
s=0
for i1 in range(i-w//2,i+w//2+1):
for j1 in range(j-w//2,j+w//2+1):
s=s+L[i][j]
print(L[i][j],s)
M[i][j]=s/v
M=M[w//2:-w//2+1]
for i in range(len(M)):
M[i]=M[i][w//2:-w//2+1]
for i in range(len(M)):
print(M[i])
Шаг 11. Зададим форму окрестности матрицей, заполненной нулями и единицами
(1 — входит в окрестность, 0 — не входит):
A=[[0,1,0],
[1,1,1],
[0,1,0]]
Полученная программа приведена в листинге 3.4.11.
Листинг 3.4.11. Линейный фильтр для двумерного списка с апертурой сложной формы
L=[[1,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20],
[21,22,23,24,25]]
A=[[0,1,0],
[1,1,1],
[0,1,0]]
w=len(A)
v=sum([sum(A[i])for i in range(w)])
n=len(L)
m=len(L[0])
M=[[0]*(m) for i in range(n)]
for i in range(w//2,n-w//2):
for j in range(w//2,m-w//2):
s=0
for di in range(w):
for dj in range(w):
s=s+A[di][dj]*L[i+di-w//2][j+dj-w//2]
M[i][j]=s/v
Функции
83
M=M[w//2:-w//2+1]
for i in range(len(M)):
M[i]=M[i][w//2:-w//2+1]
for i in range(len(M)):
print(M[i])
Результат:
[7.0, 8.0, 9.0]
[12.0, 13.0, 14.0]
[17.0, 18.0, 19.0]
Шаг 12. Для большей универсальности программы (она нам потребуется в дальнейшем) поместим все элементы окрестности в список, а вычисление среднего
арифметического вынесем в отдельную функцию (листинг 3.4.12).
Листинг 3.4.12
def avg(L):
return sum(L)/len(L)
L=[[1,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20],
[21,22,23,24,25]]
A=[[0,1,0],
[1,1,1],
[0,1,0]]
w=len(A)
v=sum([sum(A[i])for i in range(w)])
n=len(L)
m=len(L[0])
M=[[0]*(m) for i in range(n)]
for i in range(w//2,n-w//2):
for j in range(w//2,m-w//2):
s=[]
for di in range(w):
for dj in range(w):
if A[di][dj]!=0:
s.append(L[i+di-w//2][j+dj-w//2])
M[i][j]=avg(s)
M=M[w//2:-w//2+1]
for i in range(len(M)):
M[i]=M[i][w//2:-w//2+1]
for i in range(len(M)):
print(M[i])
84
Глава 3
Задача 2
Для двумерного списка сделать медианный фильтр.
В отличие от линейного фильтра, медианный фильтр не усредняет окрестность,
а сортирует окрестность клетки, выбирая в качестве нового значения элемент клетки в середине отсортированного одномерного списка.
Приведем пример для одномерного списка:
Исходный сигнал
Отсортированная окрестность
Сигнал после применения фильтра
1
4
2
3
9
5
[1,2,4]
[2,3,4]
[2,3,9]
[3,5,9]
[4,5,9]
2
3
3
5
5
4
Видно, что фильтр убирает значения, резко отличающиеся от окружающих точек.
Такой фильтр полезен, если в изображении некоторые биты испорчены (например,
на фотографии есть белые точки). Недостатком фильтра может быть удаление из
изображения мелких деталей (например, зрачков).
Ход программирования
Модифицируем предыдущую программу, добавив функцию поиска медианы (листинг 3.4.13). Функция обработки окрестности (усреднение или медиана) будет
передаваться как аргумент в функцию фильтрации (перенесем весь наш код обработки в отдельную функцию filt).
Листинг 3.4.13. Медианный фильтр
def avg(L):
return sum(L)/len(L)
def median(L):
return sorted(L)[len(L)//2+1]
def filt(L,A,f):
w=len(A)
n=len(L)
m=len(L[0])
M=[[0]*(m) for i in range(n)]
for i in range(w//2,n-w//2):
for j in range(w//2,m-w//2):
s=[]
for di in range(w):
for dj in range(w):
if A[di][dj]!=0:
s.append(L[i+di-w//2][j+dj-w//2])
M[i][j]=f(s)
M=M[w//2:-w//2+1]
Функции
85
for i in range(len(M)):
M[i]=M[i][w//2:-w//2+1]
return M
L=[[1,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20],
[21,22,23,24,25]]
A=[[0,1,0],
[1,1,1],
[0,1,0]]
M=filt(L,A,median)
for i in range(len(M)):
print(M[i])
Здесь мы впервые в этой книге сами написали функцию высшего порядка (filt принимает в качестве аргумента не только списки L и A, но и функцию f). Этот пример
показывает, что приемы функционального программирования нередки и в обычных
практических задачах. В математике функции высшего порядка называют функционалами. А со встроенными в Python функционалами мы уже встречались — это
filter, reduce и map.
3.5. Алгоритм Евклида
Задача
Найти наибольший общий делитель двух чисел методом Евклида.
Языковые конструкции: функции.
Приемы программирования: рекурсия.
Идея алгоритма
Метод Евклида заключается в том, что из большего числа вычитается меньшее до
тех пор, пока числа не сравняются. Когда числа сравняются — это и будет наибольший общий делитель. Например:
Дано
Шаг 1
Шаг 2
Шаг 3
Шаг 4
Шаг 5
Шаг 6
Шаг 7
Шаг 8 —
результат:
a
132
132
54
54
30
6
6
6
6
b
210
78
78
24
24
24
18
12
6
86
Глава 3
Ход программирования
Шаг 1. Напишем программу, использующую цикл, пока числа не сравняются (листинг 3.5.1).
Листинг 3.5.1. Алгоритм Евклида с циклом while
a=int(input())
b=int(input())
while(a!=b):
if a>b:
a=a-b
else:
b=b-a
print(a)
П РИМЕЧАНИЕ К КОДУ
Здесь мы впервые в этой книге встретились со вторым видом цикла (первым был цикл
for) — циклом while (пока). В отличие от условия if, в котором блок после него выполняется однократно при выполнении условия, цикл while выполняется до тех пор, пока
истинно условие, в нем заданное (т. е. может выполняться много раз).
Шаг 2. Напишем программу (листинг 3.5.2), использующую рекурсивный вызов
функции с терминальным случаем, когда числа равны:
a,
если a = b
⎧⎪
nod (a, b) = ⎨ nod (a − b, b), если a > b .
⎪
⎩ nod (a, b − a ), если b > a
Листинг 3.5.2. Алгоритм Евклида рекурсивный
def nod(a,b):
if a==b:
return a
elif a>b:
return nod(a-b,b)
else:
return nod(a,b-a)
a=int(input())
b=int(input())
print(nod(a,b))
П РИМЕЧАНИЕ К КОДУ
Здесь мы впервые в этой книге встретились со случаем, когда функция вызывает саму
себя с измененными значениями аргумента. Для нашего примера nod(132, 210) вызовет
nod(138, 78), та, в свою очередь, вызовет nod(54, 78) и т. д. до nod(6, 6). Там, где аргументы равны, возвращается ответ (эта ветка условия, в которой не происходит вызова
той же самой функции, называется терминальным случаем). Сам вызов функцией
самой себя — это распространенный прием программирования, который называется
Функции
87
рекурсией. Он играет в функциональном программировании ту же роль, что и рекуррентные формулы — в структурном. Рекурсии бывают разных видов (один из примеров мы увидим в следующем разделе). Использование рекурсий может быть особым
стилем программирования — например, все циклы можно заменить на рекурсии.
Шаг 3. Количество условий можно сократить, использовав функции min и max (листинг 3.5.3).
Листинг 3.5.3
def nod(a,b):
if a==b:
return a
else:
return nod(max(a,b)-min(a,b),min(a,b))
a=int(input())
b=int(input())
print(nod(a,b))
Шаг 4. Можно объединить return, используя вторую форму if (листинг 3.5.4).
Листинг 3.5.4. Алгоритм Евклида в стиле Python
def nod(a,b):
return a if a==b else nod(max(a,b)-min(a,b),min(a,b))
a=int(input())
b=int(input())
print(nod(a,b))
Шаг 5. Обобщим нашу программу. Пусть надо найти НОД элементов целого списка. Делается это примерно так же, как и для двух чисел. Например, пусть дан список:
L=[63,21,35,77]
Чтобы не искать попарно наибольшие и наименьшие значения, отсортируем его:
L=[77,63,35,21]
Вычтем из каждого элемента следующий:
L=[14,28,14,21]
Уберем дубликаты и отсортируем:
L=[28,21,14]
Вычтем из каждого элемента следующий:
L=[7,7,14]
Уберем дубликаты и отсортируем:
L=[14,7]
88
Глава 3
Вычтем элементы:
L=[7,7]
Уберем дубликат:
L=[7]
Мы получили один элемент, который и есть НОД.
Напишем функцию, которая реализует однократный прогон списка и сортировку
(листинг 3.5.5).
Листинг 3.5.5
def nodlist(L):
L=sorted(L)[::-1]
print(L)
for i in range(len(L)-1):
L[i]=L[i]-L[i+1]
print(L)
Результат
63 21 35 77
[77, 63, 35, 21]
[14, 28, 14, 21]
L=[int(el) for el in input().split()]
nodlist(L)
Сделаем удаление дубликатов и многократный прогон и убедимся, что на каждом
шаге программа работает верно (листинг 3.5.6).
Листинг 3.5.6
def nodlist(L):
while len(L)>1:
L=sorted(set(L))[::-1]
print(L)
for i in range(len(L)-1):
L[i]=L[i]-L[i+1]
return L[0]
Результат
63 21 35 77
[77, 63, 35, 21]
[28, 21, 14]
[14, 7]
[7]
7
L=[int(el) for el in input().split()]
print(nodlist(L))
Удалим промежуточные выводы на экран и получим готовую версию программы
(листинг 3.5.7).
Листинг 3.5.7. НОД для списка
def nodlist(L):
while len(L)>1:
L=sorted(set(L))[::-1]
for i in range(len(L)-1):
L[i]=L[i]-L[i+1]
return L[0]
Функции
89
L=[int(el) for el in input().split()]
print(nodlist(L))
Шаг 6. Написанная программа никак не задействует нашу предыдущую наработку — поиск НОД двух чисел. Напишем версию алгоритма, который ею воспользуется. Идея алгоритма состоит в том, чтобы просматривать список чисел и находить
НОД текущего числа и полученного для предыдущих чисел НОД. Например:
Исходные числа
Шаг 1
63
21
35
77
НОД(63,21)=21
Шаг 2
Шаг 3
НОД(21,35)=7
НОД(7,77)=7
Мы будем сохранять значение НОД на каждом шаге в правом числе из каждой
пары (листинг 3.5.8).
Листинг 3.5.8
def nod(a,b):
return a if a==b else nod(max(a,b)-min(a,b),min(a,b))
def nodlist(L):
for i in range(1,len(L)):
L[i]=nod(L[i],L[i-1])
return L[-1]
L=[int(el) for el in input().split()]
print(nodlist(L))
Шаг 7. Нам не нужно хранить весь список. С каждым шагом мы можем удалять
один элемент (листинг 3.5.9).
Листинг 3.5.9
def nod(a,b):
return a if a==b else nod(max(a,b)-min(a,b),min(a,b))
def nodlist(L):
for i in range(1,len(L)):
L[0]=nod(L[0],L[1])
del L[1]
return L[0]
L=[int(el) for el in input().split()]
print(nodlist(L))
Шаг 8. Нам не обязательно самим делать подсчет НОД по списку с его сокращением. Это достаточно типичная задача, и в библиотеке functools есть функция reduce,
90
Глава 3
который «стягивает» список в одно число, последовательно применяя бинарную
операцию (листинг 3.5.10).
Листинг 3.5.10
from functools import reduce
def nod(a,b):
return a if a==b else nod(max(a,b)-min(a,b),min(a,b))
def nodlist(L):
return reduce(nod,L)
L=[int(el) for el in input().split()]
print(nodlist(L))
Шаг 9. Превратим функцию nodlist из функции, которая имеет один аргумент —
список, в функцию с переменным количеством аргументов. Теперь мы можем вызывать ее так, как показано в листинге 3.5.11.
Листинг 3.5.11
Результат
from functools import reduce
def nod(a,b):
return a if a==b else nod(max(a,b)-min(a,b),
min(a,b))
def nodlist(*a):
return reduce(nod,a)
L=[63,21,35,77]
print(nodlist(*L))
print(nodlist(63,21,35,77))
print(nodlist(63,21))
print(nodlist(63))
7
7
21
63
Шаг 10. Заметим, что nodlist прекрасно работает для двух аргументов и даже для
одного. Это значит, что самостоятельное использование nod теряет смысл. Спрячем
бинарную функцию nod и переименуем nodlist в nod. Это можно сделать тремя способами:
1. Спрятать бинарную nod внутрь функции работы со списком (листинг 3.5.12).
Листинг 3.5.12
from functools import reduce
def nod(*a):
def nod(a,b):
return a if a==b else nod(max(a,b)-min(a,b),min(a,b))
return reduce(nod,a)
Функции
91
L=[63,21,35,77]
print(nod(*L))
print(nod(63,21,35,77))
print(nod(63,21))
print(nod(63))
2. Превратить бинарную nod в терминальный случай (листинг 3.5.13).
Листинг 3.5.13
from functools import reduce
def nod(*a):
if len(a)==2:
return a[0] if a[0]==a[1] else nod(max(a)-min(a),min(a))
else:
return reduce(nod,a)
print(nod(63,21,35,77))
3. Записать код бинарной nod как анонимную функцию в reduce (листинг 3.5.14).
Листинг 3.5.14
from functools import reduce
def nod(*a):
return reduce(lambda x,y: x if x==y else nod(max(x,y)-min(x,y),min(x,y)),a)
print(nod(63,21,35,77))
print(nod(63,21))
print(nod(63))
Обратите внимание на интересный пример рекурсии: внутренний вызов nod
спрятан в анонимную функцию!
Подробнее анонимные функции будут рассмотрены в разд. 3.8.
3.6. Гипероператоры
Задача
Запрограммировать гипероператоры арифметики.
В школе меня всегда интересовал вопрос: есть ли какие-нибудь еще арифметические операции после возведения в степень? Вспомним, что такое умножение:
a ⋅ b = a + a + a + ... + a .
(b слагаемых)
92
Возведение в степень:
Глава 3
a b = a ⋅ a ⋅ a ⋅ ... ⋅ a .
(b множителей)
По аналогии мы можем продолжить этот ряд арифметических операций:
...a
aa
a
.
(b элементов)
В школе я не получил ответа на этот вопрос. В первой половине XX века этот вопрос занимал и ученых (ученики Гильберта: Аккеман, Гудстейн и др.). Такие операторы, по логике развития математики, были придуманы и исследованы. Вереница
возведений в степень называется тетрацией:
aa
a
a...
.
Вереница тетраций — пентацией, далее идет гексация и т. д. Общее их название —
гипероператоры.
Языковые конструкции: функции.
Приемы программирования: рекурсия, рекурсия высших порядков.
Ход программирования
Шаг 1. Напишем функцию сложения как обертку над оператором «плюс», на основе которой будут конструироваться остальные операторы (листинг 3.6.1).
Листинг 3.6.1
Результат
def summ(a,b):
return a+b
a=int(input())
b=int(input())
print(summ(a,b))
2
4
6
Шаг 2. На основе summ создадим умножение с помощью цикла (листинг 3.6.2).
Листинг 3.6.2
Результат
def summ(a,b):
return a+b
def mult(a,b):
r=a
for i in range(b-1):
r=summ(r,a)
return r
a=int(input())
b=int(input())
2
4
Функции
print(summ(a,b))
print(mult(a,b))
93
6
8
Шаг 3. На основе mult создадим возведение в степень (листинг 3.6.3).
Листинг 3.6.3
Результат
def summ(a,b):
return a+b
def mult(a,b):
r=a
for i in range(b-1):
r=summ(r,a)
return r
def power(a,b):
r=a
for i in range(b-1):
r=mult(r,a)
return r
a=int(input())
b=int(input())
print(summ(a,b))
print(mult(a,b))
print(power(a,b))
2
4
6
8
16
Шаг 4. Начнем конструировать тетрацию. Казалось бы, надо скопировать код power,
переименовать и заменить mult на power (листинг 3.6.4).
Листинг 3.6.4
def summ(a,b):
return a+b
def mult(a,b):
r=a
for i in range(b-1):
r=summ(r,a)
return r
def power(a,b):
r=a
for i in range(b-1):
r=mult(r,a)
return r
Результат
94
Глава 3
def tetra(a,b):
r=a
for i in range(b-1):
r=power(r,a)
return r
2
4
6
8
16
256
a=int(input())
b=int(input())
print(summ(a,b))
print(mult(a,b))
print(power(a,b))
print(tetra(a,b))
Но посмотрим пошагово, как производилась тетрация с результатом 256:
((22 ) 2 ) 2 = 256 .
Мы можем упростить это выражение:
3
((22 ) 2 )2 = 22⋅2⋅2 = 2( 2 ) .
Во введении новой арифметической операции таким способом нет никакого смысла, т. к. она заменяется степенью в степени:
aa
b −1
.
Но мы можем выполнить вычисления и в другом порядке:
22
( 22 ) ⎞⎟
⎛
⎜2
⎜
⎝
22 = 2
⎟
⎠
=2
( 24 ) = 216 = 65 536 .
И в таком способе вычисления (правоприоритетном) мы не будем иметь способа
упрощения выражения. Потому дальнейшие гипероператоры мы станем считать
правоприоритетными. Чтобы реализовать правоприоритетность, нам нужно поменять местами аргументы в рекуррентной формуле:
Было
r=power(r,a)
Стало
r=power(a,r)
Заодно изменим рекуррентные формулы и в других функциях. Проверим работоспособность программы (листинг 3.6.5).
Листинг 3.6.5
def summ(a,b):
return a+b
def mult(a,b):
r=a
Результат
Функции
95
for i in range(b-1):
r=summ(a,r)
return r
def power(a,b):
r=a
for i in range(b-1):
r=mult(a,r)
return r
def tetra(a,b):
r=a
for i in range(b-1):
r=power(a,r)
return r
a=int(input())
b=int(input())
print(summ(a,b))
print(mult(a,b))
print(power(a,b))
print(tetra(a,b))
2
4
6
8
16
65536
Шаг 5. Аналогично тетрации сконструируем пентацию (листинг 3.6.6).
Листинг 3.6.6
def summ(a,b):
return a+b
def mult(a,b):
r=a
for i in range(b-1):
r=summ(a,r)
return r
def power(a,b):
r=a
for i in range(b-1):
r=mult(a,r)
return r
def tetra(a,b):
r=a
for i in range(b-1):
r=power(a,r)
return r
Результат
96
Глава 3
def penta(a,b):
r=a
for i in range(b-1):
r=tetra(a,r)
return r
a=int(input())
b=int(input())
print(summ(a,b))
print(mult(a,b))
print(power(a,b))
print(tetra(a,b))
print(penta(a,b))
2
3
5
6
8
16
65536
Запустив программу с a = 2 и b = 4, мы не дождемся ответа — слишком большие
числа получаются. Поэтому запустим программу с a = 3 и b = 2.
Шаг 6. Продолжать вводить новые гипероператоры не имеет смысла, т. к. они похожи друг на друга. Нужно искать универсальное решение. Переименуем названия
функций следующим образом (листинг 3.6.7).
Было (листинг 3.6.6)
Стало (листинг 3.6.7)
def summ(a,b):
return a+b
def hyper1(a,b):
return a+b
def mult(a,b):
r=a
for i in range(b-1):
r=summ(a,r)
return r
def hyper2(a,b):
r=a
for i in range(b-1):
r=hyper1(a,r)
return r
def power(a,b):
r=a
for i in range(b-1):
r=mult(a,r)
return r
def hyper3(a,b):
r=a
for i in range(b-1):
r=hyper2(a,r)
return r
def tetra(a,b):
r=a
for i in range(b-1):
r=power(a,r)
return r
def hyper4(a,b):
r=a
for i in range(b-1):
r=hyper3(a,r)
return r
def penta(a,b):
r=a
for i in range(b-1):
r=tetra(a,r)
return r
def hyper5(a,b):
r=a
for i in range(b-1):
r=hyper4(a,r)
return r
Функции
a=int(input())
b=int(input())
print(summ(a,b))
print(mult(a,b))
print(power(a,b))
print(tetra(a,b))
print(penta(a,b))
97
a=int(input())
b=int(input())
print(hyper1(a,b))
print(hyper2(a,b))
print(hyper3(a,b))
print(hyper4(a,b))
print(hyper5(a,b))
Шаг 7. Напишем универсальную функцию — гипероператор, для этого номер гипероператора превратим в аргумент p:
def hyper(p,a,b):
Возьмем код гипероператора и сделаем все замены (листинг 3.6.8).
Было
def hyper5(a,b):
r=a
for i in range(b-1):
r=hyper4(a,r)
return r
Листинг 3.6.8
def hyper(p,a,b):
r=a
for i in range(b-1):
r=hyper(p-1,a,r)
return r
Мы получили рекурсивный вызов функции с понижением индекса гипероператора.
Полученный код описывает все наши гипероператоры, кроме самого первого (суммы). Сумма является терминальным случаем. Добавим ее в код (листинг 3.6.9).
Было
def hyper1(a,b):
return a+b
def hyper5(a,b):
r=a
for i in range(b-1):
r=hyper4(a,r)
return r
Листинг 3.6.9
def hyper(p,a,b):
if p==1:
return a+b
else:
r=a
for i in range(b-1):
r=hyper(p-1,a,r)
return r
Мы получили готовую программу (листинг 3.6.10).
Листинг 3.6.10. Гипероператор с помощью цикла
def hyper(p,a,b):
if p==1:
return a+b
else:
r=a
for i in range(b-1):
r=hyper(p-1,a,r)
return r
98
Глава 3
p=int(input())
a=int(input())
b=int(input())
print(hyper(p,a,b))
Шаг 8. Мы можем написать вторую версию программы. Раз уж нам пришлось
использовать рекурсию, то мы можем избавиться от цикла for, также заменив его
на рекурсию и этим сократив количество используемых языковых конструкций.
Посмотрим, как это можно сделать:
a ⋅ b = a + a + a + ... + a = a + (a + a + ... + a ) = a + a ⋅ (b − 1) .
(b слагаемых)
(b – 1 слагаемых)
Получается, что мы можем написать рекурсивный вариант умножения с терминальным случаем b = 1 (листинг 3.6.11).
Листинг 3.6.11
def mult(a,b):
if b==1:
return a
else:
return a+mult(a,b-1)
Или через функцию суммирования (листинг 3.6.12).
Листинг 3.6.12
def summ(a,b):
return a+b
def mult(a,b):
if b==1:
return a
else:
return summ(a,mult(a,b-1))
Или в гипероператорной форме (листинг 3.6.13).
Листинг 3.6.13
def hyper1(a,b):
return a+b
def hyper2(a,b):
if b==1:
return a
else:
return hyper1(a,hyper2(a,b-1))
Функции
99
И таким способом мы можем изменить все гипероператоры. Возьмем универсальный гипероператор, заменим в нем цикл на рекурсию и добавим второй терминальный случай. Мы получим вторую версию программы (листинг 3.6.14).
Листинг 3.6.14. Рекурсивный гипероператор
def hyper(p,a,b):
if b==1:
return a
elif p==1:
return a+b
else:
return hyper(p-1,a,hyper(p,a,b-1))
p=int(input())
a=int(input())
b=int(input())
print(hyper(p,a,b))
Заметим, что мы получили особый вид рекурсии, в которой повторный вызов осуществляется в аргументе, т. е. в круглых скобках. Такой вид рекурсии называется
рекурсией высшего порядка. Схематично покажем разницу между обычной рекурсией и рекурсией высшего порядка:
Обычная рекурсия
def f(...):
...
return f(...)
Рекурсия высшего порядка
def f(...):
...
return f(...f(...)...)
Шаг 9. Напишем еще одну версию гипероператора. Пойдем следующим путем.
Создадим список чисел a в количестве b:
L=[a]*b
Вычисление гипероператора означает последовательное применение гипероператора более низкого порядка к этому списку. Список как бы стягивается в одно число
путем последовательного применения одной и той же функции. Для этого есть
библиотечная функция reduce, с которым мы встретились в предыдущем разделе.
Напишем гипероператоры путем применения функции к списку (листинг 3.6.15).
Листинг 3.6.15
from functools import reduce
def summ(a,b):
return a+b
def mult(a,b):
return reduce(summ,[a]*b)
Результат
100
Глава 3
def power(a,b):
return reduce(mult,[a]*b)
def tetra(a,b):
return reduce(power,[a]*b)
a=int(input())
b=int(input())
print(summ(a,b))
print(mult(a,b))
print(power(a,b))
print(tetra(a,b))
2
4
6
8
16
256
Шаг 10. Проанализировав результат, мы увидим, что тетрация работает неверно.
Причина кроется в последовательном применении reduce:
2
2
2
2
2
2 =4
42 = 16
162 = 256
С таким выполнением мы уже сталкивались — это левоприоритетное выполнение:
3
((22 ) 2 )2 = 22⋅2⋅2 = 22 = 28 = 256 .
И оно бессмысленно с точки зрения введения новой арифметической операции.
Нам нужно правоприоритетное выполнение:
2
2
2
2
22 = 4
24 = 16
216 = 65 536
2
⎛
⎜⎜ 2
⎝
( 22 ) ⎞⎟
⎟
⎠
( 24 ) = 216 = 65 536 .
=2
Получается, что нам нужно поменять местами аргументы функции по типу:
имеется:
f(x,y)
а вызывать нужно:
f(y,x)
Сделать это можно с помощью анонимной функции:
lambda x,y: f(y,x)
Функции
101
Мы получим правильно работающий код (листинг 3.6.16).
Листинг 3.6.16
Результат
from functools import reduce
def summ(a,b):
return a+b
def mult(a,b):
return reduce(summ,[a]*b)
def power(a,b):
return reduce(mult,[a]*b)
def tetra(a,b):
return reduce(lambda x,y: power(y,x),[a]*b)
a=int(input())
b=int(input())
print(summ(a,b))
print(mult(a,b))
print(power(a,b))
print(tetra(a,b))
2
4
6
8
16
65536
Шаг 11. Унифицируем функции, добавив lambda во все reduce (листинг 3.6.17).
Листинг 3.6.17
from functools import reduce
def summ(a,b):
return a+b
def mult(a,b):
return reduce(lambda x,y: summ(y,x),[a]*b)
def power(a,b):
return reduce(lambda x,y: mult(y,x),[a]*b)
def tetra(a,b):
return reduce(lambda x,y: power(y,x),[a]*b)
a=int(input())
b=int(input())
print(summ(a,b))
print(mult(a,b))
print(power(a,b))
print(tetra(a,b))
102
Глава 3
Шаг 12. Теперь мы можем написать универсальную функцию-гипероператор (листинг 3.6.18).
Листинг 3.6.18. Гипероператор с помощью «стягивания» списка
from functools import reduce
def summ(a,b):
return a+b
def hyper(p,a,b):
if p==1:
return a+b
else:
return reduce(lambda x,y: hyper(p-1,y,x),[a]*b)
p=int(input())
a=int(input())
b=int(input())
print(hyper(p,a,b))
3.7. Ханойские башни
Задача
Написать алгоритм перекладывания колец в детской головоломке «Ханойские
башни». Головоломка заключается в следующем. Есть три стержня. На один из них
надеты кольца в порядке убывания размера. Нужно переложить кольца на третий
стержень, используя второй как вспомогательный (рис. 3.22). Единственное правило заключается в том, что нельзя укладывать кольцо большего размера на кольцо
меньшего размера.
Рис. 3.22. Начальное и конечное положение Ханойских башен
Это знаменитая задача древности. Есть легенда, что в городе Бенарес монахи перекладывают 64 кольца. Как только они закончат работу, наступит Конец света (чтобы вы не пугались, сразу скажу, что если на перекладывание одного кольца отвести
1 секунду, то работа займет несколько миллиардов лет). Теперь эта задача стала
классической задачей на рекурсию, которую часто предлагают программистам
решить при приеме на работу.
Функции
103
Идея алгоритма
Сперва попробуем поперекладывать пирамидки малых размеров. Случай с одним
кольцом тривиален (рис. 3.23).
Рис. 3.23. Перемещение Ханойской башни высотой 1
Переложим пирамидку высотой 2 (рис. 3.24).
Рис. 3.24. Перемещение Ханойской башни высотой 2
Переложим пирамидку высотой 3 (рис. 3.25).
Обратим внимание на особую промежуточную позицию при перекладывании пирамид высотой 2 и 3 (рис. 3.26).
0)
1)
2)
3)
4)
5)
6)
7)
Рис. 3.25. Перемещение Ханойской башни высотой 3
104
Глава 3
Рис. 3.26. Промежуточная позиция для Ханойских башен высотой 2 и 3
Было бы очень хорошо получить такую же позицию для ханойской башни высотой 4 (рис. 3.27).
Рис. 3.27. Промежуточная позиция для Ханойской башни высотой 4
Этой позиции мы легко достигнем с помощью тех же шагов, какими перекладывали башню высотой 3 (на нижнее кольцо можно просто не обращать внимания, оно
самое большое, на него на всех шагах алгоритма можно складывать любое кольцо).
Далее переложим большое кольцо на правый финишный стержень, и у нас опять
получится задача перекладывания башни, но только стартовый стержень будет
средний, промежуточный — левый, а финишный останется правым (рис. 3.28).
Рис. 3.28. Вторая часть перекладываний для Ханойской башни высотой 4
Таким образом, задача перекладывания башни высотой 4 сводится к двум задачам
перекладывания башни высотой 3 с разными стартовым и промежуточным стержнями. Аналогично в общем случае перекладывание башни высотой n сводится
к двум задачам перекладывания башни высотой n-1. Получается рекурсивный алгоритм.
Функции
105
Ход программирования
Шаг 1. Разберемся с сигнатурой рекурсивной функции. Первым аргументом будет
n, который можно интерпретировать как размер самого большого кольца или как
количество колец. Два других аргумента — это номера стартового и финишного
стержней (нумерацию сделаем с нуля). Функция будет рекурсивно вызывать саму
себя два раза с уменьшенным значением аргумента n-1. Терминальным случаем
будет n=0. Но тогда перекладывать ничего не нужно. Поэтому терминальный случай
писать не будем, а для рекурсивного случая напишем условие n>0. Заготовка программы приведена в листинге 3.7.1.
Листинг 3.7.1
def hanoj(n,start,fin):
if n>0:
pass
hanoj(2,0,2)
Шаг 2. По значениям стартового и финишного стержней нужно определить номер
промежуточного стержня temp. Заметим, что:
Таким образом:
start + temp + fin = 0 + 1 + 2 = 3 .
temp = 3 − start − fin .
Добавим вычисление промежуточного стержня в программу (листинг 3.7.2).
Листинг 3.7.2
def hanoj(n,start,fin):
if n>0:
temp=3-start-fin
hanoj(2,0,2)
Шаг 3. Рекурсивно запустим функцию два раза с уменьшенным аргументом n-1
и сменой стартового стержня во втором запуске. Между этими рекурсивными
вызовами выведем на экран, с какого стержня на какой мы перекладываем кольцо.
Мы получим работающую программу (листинг 3.7.3).
Листинг 3.7.3. Порядок перекладывания колец
def hanoj(n,start,fin):
if n>0:
temp=3-start-fin
hanoj(n-1,start,temp)
print("перенести",n,"c",start,"на",fin)
hanoj(n-1,temp,fin)
hanoj(2,0,2)
106
Глава 3
Результат:
перенести 1 c 0 на 1
перенести 2 c 0 на 2
перенести 1 c 1 на 2
Здесь самым сложным для понимания является то, что в функции мы выводим на
экран фактически промежуточную позицию. А остальные выводы на экран берут
на себя рекурсивные вызовы.
Шаг 4. Написанная функция является инструкцией по перекладыванию колец. Для
случая 2 видно, что программа работает правильно. Для больших n мысленно представлять все эти перекладывания станет затруднительно. Хочется видеть на экране
некоторое представление стержней на каждом шаге — например, в виде двумерного списка, где каждый вложенный список будет списком колец, надетым на
соответствующий стержень. Например: [[4,3,2,1],[],[]] — стартовая позиция, а
[[],[],[4,3,2,1]] — финишная. Для этого, помимо вывода на экран перекладываний, надо вести лог — запись состояния переменных функции в глобальных
переменных (в нашем случае — номера стержней, с которого и на который идут
перекладывания). Также для удобства переименуем функцию в instrHanoj (листинг 3.7.4).
Листинг 3.7.4
S=[]
F=[]
def instrHanoj(n,start,fin):
if n>0:
temp=3-start-fin
instrHanoj(n-1,start,temp)
print("перенести",n,"c",start,"на",fin)
S.append(start)
F.append(fin)
instrHanoj(n-1,temp,fin)
instrHanoj(2,0,2)
Шаг 5. Сделаем заготовку для функции showHanoj, которая создаст начальное положение башен в виде списка списков, — например: H=[[4,3,2,1],[],[]], и вызовет
функцию instrHanoj, чтобы получить инструкцию по перекладыванию колец (листинг 3.7.5).
Листинг 3.7.5
...
def showHanoj(n,start,fin):
H=[[],[],[]]
H[start]=list(range(n,0,-1))
print(H)
instrHanoj(n,start,fin)
Функции
107
Шаг 6. Дополним функцию showHanoj. В цикле будем перебирать значения списков S и F (S[i] — стержень, с которого берем кольцо, а F[i] — стержень, на который надеваем кольцо. Заметим, что списки внутри H (три стержня) ведут себя как
стеки LIFO (последним положили, первым взяли), поэтому взятие кольца будем
делать с помощью метода pop, а складывание — метода append. Мы получим работающую программу (листинг 3.7.6). Посмотрим результаты для количества колец 4
и убедимся, что программа работает верно.
Листинг 3.7.6
S=[]
F=[]
def instrHanoj(n,start,fin):
if n>0:
temp=3-start-fin
instrHanoj(n-1,start,temp)
print("перенести",n,"c",start,"на",fin)
S.append(start)
F.append(fin)
instrHanoj(n-1,temp,fin)
def showHanoj(n,start,fin):
H=[[],[],[]]
H[start]=list(range(n,0,-1))
print(H)
instrHanoj(n,start,fin)
for i in range(len(S)):
H[F[i]].append(H[S[i]].pop())
print(H)
n=int(input())
showHanoj(n,0,2)
Результат:
4
[[4, 3, 2, 1], [],
перенести 1 c 0 на
перенести 2 c 0 на
перенести 1 c 1 на
перенести 3 c 0 на
перенести 1 c 2 на
перенести 2 c 2 на
перенести 1 c 0 на
перенести 4 c 0 на
перенести 1 c 1 на
перенести 2 c 1 на
перенести 1 c 2 на
перенести 3 c 1 на
[]]
1
2
2
1
0
1
1
2
2
0
0
2
108
Глава 3
перенести 1 c 0 на 1
перенести 2 c 0 на 2
перенести 1 c 1 на 2
[[4, 3, 2], [1], []]
[[4, 3], [1], [2]]
[[4, 3], [], [2, 1]]
[[4], [3], [2, 1]]
[[4, 1], [3], [2]]
[[4, 1], [3, 2], []]
[[4], [3, 2, 1], []]
[[], [3, 2, 1], [4]]
[[], [3, 2], [4, 1]]
[[2], [3], [4, 1]]
[[2, 1], [3], [4]]
[[2, 1], [], [4, 3]]
[[2], [1], [4, 3]]
[[], [1], [4, 3, 2]]
[[], [], [4, 3, 2, 1]]
Шаг 7. Наша программа плохо структурирована: функция instrHanoj передает инструкцию по перекладыванию showHanoj через глобальные списки S и F. Если функция
instrHanoj отдельно не нужна, поместим обе функции в одну функцию Hanoj и спрячем в нее глобальные списки (листинг 3.7.7).
Листинг 3.7.7. Ханойские башни
def Hanoj(n,start=0,fin=2):
S=[]
F=[]
def instrHanoj(n,start,fin):
if n>0:
temp=3-start-fin
instrHanoj(n-1,start,temp)
print("перенести",n,"c",start,"на",fin)
S.append(start)
F.append(fin)
instrHanoj(n-1,temp,fin)
def showHanoj(n,start,fin):
H=[[],[],[]]
H[start]=list(range(n,0,-1))
print(H)
instrHanoj(n,start,fin)
for i in range(len(S)):
H[F[i]].append(H[S[i]].pop())
print(H)
Функции
109
showHanoj(n,start,fin)
n=int(input())
Hanoj(n)
Функция instrHanoj выглядит просто. Да и в showHanoj нет ничего сложного. Однако
задача «Ханойские башни» обычно вызывает затруднения у начинающих программистов даже в том случае, если они знают идею алгоритма. Я думаю, что это связано с тем, что декомпозиция алгоритма в две функции неочевидна. Этим задача
и ценна. Я сам сначала сразу пытался писать рекурсивную функцию, работающую
с двумерным списком, что сделать очень сложно.
3.8. Отображения списков
Задача
Дан список. Сформировать два новых списка, применив функцию сначала один раз,
потом еще раз.
Например:
L=[0,1,2,3,4,5]
Сформируем список квадратов:
M=[0,1,4,9,16,25]
К элементам M еще раз применить возведение в квадрат:
P=[0,1,16,81,256,625]
Казалось бы, это очень легкая задача. Но ее можно решить очень разными способами, в том числе функциональным программированием. На этом примере читатель вновь окунется в странный мир функционального программирования, где
функции могут обрабатывать не только числа и коллекции, но также и другие
функции.
Ход программирования
Шаг 1. Напишем задачу в стиле структурного программирования с применением
функций. Создадим функцию возведения в квадрат square. Новые списки будем
формировать в циклах перебором старых (листинг 3.8.1).
Листинг 3.8.1. Функция, структурное программирование
Результат
def square(n):
return n*n
L=[0,1,2,3,4,5]
print(L)
M=[]
[0, 1, 2, 3, 4, 5]
110
for el in L:
M.append(square(el))
print(M)
P=[]
for el in M:
P.append(square(el))
print(P)
Глава 3
[0, 1, 4, 9, 16, 25]
[0, 1, 16, 81, 256, 625]
Шаг 2. Заметим, что есть повторяющиеся куски кода. Поместим применение функции к списку внутрь особой функции (листинг 3.8.2).
Листинг 3.8.2. Декомпозиция в функции
def square(n):
return n*n
def squarelist(L):
M=[]
for el in L:
M.append(square(el))
return M
L=[0,1,2,3,4,5]
print(L)
M=squarelist(L)
print(M)
P=squarelist(M)
print(P)
Мы получили программу в самом распространенном стиле программирования,
когда алгоритм декомпозируется в функции, код которых — структурное программирование. Но код программы получился очень длинным.
Шаг 3. Философия Python — программировать так, чтобы код был коротким. Вернемся к программе из листинга 3.8.1 и заменим формирование списков списочными
выражениями (листинг 3.8.3).
Листинг 3.8.3. Стиль Python
def square(n):
return n*n
L=[0,1,2,3,4,5]
print(L)
M=[square(el) for el in L]
print(M)
P=[square(el) for el in M]
print(P)
Функции
111
Это стиль Python — особый стиль, используемый программистами, для которых
Python — «родной» язык, и нелюбимый программистами, для которых Python —
«второй» и «третий» языки.
Шаг 4. Вернемся к программе из листинга 3.8.2 и сделаем внутри функции
squarelist код в стиле Python (листинг 3.8.4).
Листинг 3.8.4. Декомпозиция в функции и стиль Python
def square(n):
return n*n
def squarelist(L):
return [square(el) for el in L]
L=[0,1,2,3,4,5]
print(L)
M=squarelist(L)
print(M)
P=squarelist(M)
print(P)
Шаг 5. Задумаемся над тем, как сделать программу более универсальной. Squarelist
применяет функцию возведения в квадрат к списку. Но что если попробовать
сделать так, чтобы эта функция применяла к списку любую другую функцию. Для
этого придется применяемую функцию передавать в качестве аргумента. Переименуем squarelist в funlist (листинг 3.8.5).
Листинг 3.8.5. Функция высшего порядка
def square(n):
return n*n
def funlist(f,L):
return [f(el) for el in L]
L=[0,1,2,3,4,5]
print(L)
M=funlist(square,L)
print(M)
P=funlist(square,M)
print(P)
Программа прекрасно работает! Здесь, как и в программе медианного фильтра
(см. разд. 3.4), получается, что аргументами функции могут быть не только числа,
строки, списки и другие контейнеры, но и другие функции. Напомню, что функции,
которые обрабатывают другие функции, называются функционалами, или функциями высших порядков.
112
Глава 3
Заметьте, что при передаче square в funlist мы имя функции square пишем безо всяких круглых скобок. В этом случае Python понимает, что в текущем месте не надо
вызывать функцию, а надо передавать ее имя как объект:
M=funlist(square, L)
Шаг 6. Применение функции к списку — весьма распространенная задача. Неужели в Python нет встроенного подходящего средства? Конечно, есть! Нам не нужно
писать funlist. Для этого есть встроенный функционал map, который делает то же
самое (листинг 3.8.6).
Листинг 3.8.6. Применение map
def square(n):
return n*n
L=[0,1,2,3,4,5]
print(L)
M=list(map(square,L))
print(M)
P=list(map(square,M))
print(P)
Шаг 7. Код предыдущего шага вызывает двойственное чувство. С одной стороны,
он экономен, но все портит громоздкая функция square. Можно ли обойтись без
нее? Можно, но для этого придется воспользоваться второй формой задания функции через ключевое слово lambda (листинг 3.8.7).
Листинг 3.8.7. Конструкция lambda
square=lambda n: n*n
L=[0,1,2,3,4,5]
print(L)
M=list(map(square,L))
print(M)
P=list(map(square,M))
print(P)
Шаг 8. Такую форму записи можно понимать как отображение n в n*n. Получается
даже небольшая экономия по сравнению с def — не нужно писать return. Но не
в экономии дело. Имя функции теперь отделено от его реализации. И можно реализацию функции использовать вообще без имени там, где нужен ее код. Здесь мы
имеем дело с анонимной функцией (листинг 3.8.8).
Листинг 3.8.8. Анонимные функции
L=[0,1,2,3,4,5]
print(L)
M=list(map(lambda n: n*n,L))
Функции
113
print(M)
P=list(map(lambda n: n*n,M))
print(P)
Сравним три программы:
Листинг 3.8.4.
Декомпозиция в функции
и стиль Python
Листинг 3.8.3.
Стиль Python: списочные
выражения
Листинг 3.8.8.
Функциональное
программирование
def square(n):
return n*n
def square(n):
return n*n
def squarelist(L):
M=[]
for el in L:
M.append(square(el))
return M
L=[0,1,2,3,4,5]
print(L)
M=[square(el) for el in L]
print(M)
P=[square(el) for el in M]
print(P)
L=[0,1,2,3,4,5]
print(L)
M=list(map(lambda n: n*n,L))
print(M)
P=list(map(lambda n: n*n,M))
print(P)
L=[0,1,2,3,4,5]
print(L)
M=squarelist(L)
print(M)
P=squarelist(M)
print(P)
Кажется, что программы вообще написаны на разных языках. Здесь мы имеем дело
с разными стилями программирования или, как говорят программисты, с разными
парадигмами программирования. Python же язык мультипарадигменный. Поэтому
надо не только уметь решать на нем задачи, но и разбираться в разных стилях программирования для того, чтобы понимать код, написанный другими программистами.
Шаг 9. Когда я написал, что к списку можно применить любую функцию, я погорячился. Пусть наша функция имеет два аргумента. Тогда применить ее к списку
мы не сможем. Первым аргументом функции будет элемент списка, а вторым нужно что-то подставить. Решение, приведенное в листинге 3.8.9, неизящно.
Листинг 3.8.9
def squareplus(x,y):
return x*x+y
def squareplusone(x)
return squareplus(x,1)
L=[0,1,2,3,4,5]
print(L)
M=list(map(squareplusone,L))
114
Глава 3
print(M)
P=list(map(squareplusone,M))
print(P)
Что делать, если мы хотим управлять тем, что подставляем во второй аргумент?
Напишем функцию, которая будет принимать значение того, что мы хотим прибавить, и возвращать в качестве ответа пригодную для применения в map функцию.
Для формирования функции-результата нам пригодится конструкция lambda — анонимная функция (листинг 3.7.10).
Листинг 3.8.10
def squareplus(x,y):
return x*x+y
def gensquareplus(y):
return lambda x: squareplus(x,y)
squareplusone=gensquareplus(1)
L=[0,1,2,3,4,5]
print(L)
M=list(map(squareplusone,L))
print(M)
P=list(map(squareplusone,M))
print(P)
Результат:
[0, 1, 2, 3, 4, 5]
[1, 2, 5, 10, 17, 26]
[2, 5, 26, 101, 290, 677]
Это важный пример по следующим причинам:
1. Мы научились не только принимать другие функции в качестве аргумента, но и
генерировать новые функции в качестве результата.
2. Заметим, что функция gensquareplus закончилась, но значение y не пропало. Оно
каким-то образом сохранилось в squareplusone. Это называется замыканием, или
захватом переменной.
3. Выполнение функции squareplus в нашей программе происходит в два этапа. На
первом этапе мы подставляем аргумент y, на втором этапе — аргумент x. Здесь
мы имеем дело с таким приемом программирования, как частичное применение
функции.
Шаг 10. Мы стремимся создавать более универсальные программы. Сделаем так,
чтобы частичное применение функции было возможно не только для squareplus. Будем передавать функцию, которую хотим «частично применить», в функционал
частичного применения функции, для чего переименуем gensquareplus в partapply
(листинг 3.8.11).
Функции
115
Листинг 3.8.11. Частичное применение функции
def squareplus(x,y):
return x*x+y
def partapply(f,y):
return lambda x: f(x,y)
squareplusone=partapply(squareplus,1)
L=[0,1,2,3,4,5]
print(L)
M=list(map(squareplusone,L))
print(M)
P=list(map(squareplusone,M))
print(P)
Функция f, переданная как аргумент в partapply, каким-то образом запомнилась
в squareplusone. Получается, что возможно не только замыкание переменной, но и
замыкание функции.
Шаг 11. Частичное применение функции — достаточно типовая задача, хотя и
более редкая, чем map. Поэтому она не встроена в Python, но есть в библиотеке инструментов для функционального программирования functools. Она называется
partial (листинг 3.8.12).
Листинг 3.8.12. Применение partial
from functools import partial
def squareplus(x,y):
return x*x+y
squareplusone=partial(squareplus,y=1)
L=[0,1,2,3,4,5]
print(L)
M=list(map(squareplusone,L))
print(M)
P=list(map(squareplusone,M))
print(P)
Шаг 12. Если мы в первом вызове хотим прибавлять 1, а во втором — 2, нам нужно
будет создать еще одну функцию squareplustwo (листинг 3.8.13).
Листинг 3.8.13
from functools import partial
def squareplus(x,y):
return x*x+y
116
Глава 3
squareplusone=partial(squareplus,y=1)
squareplustwo=partial(squareplus,y=2)
L=[0,1,2,3,4,5]
print(L)
M=list(map(squareplusone,L))
print(M)
P=list(map(squareplustwo,M))
print(P)
Результат:
[0, 1, 2, 3, 4, 5]
[1, 2, 5, 10, 17, 26]
[3, 6, 27, 102, 291, 678]
Или делать частичное применение прямо в map (листинг 3.8.14).
Листинг 3.8.14
from functools import partial
def squareplus(x,y):
return x*x+y
L=[0,1,2,3,4,5]
print(L)
M=list(map(partial(squareplus,y=1),L))
print(M)
P=list(map(partial(squareplus,y=2),M))
print(P)
Но есть и альтернативный путь. Применим прием, который называется каррирование (карринг). Посмотрите на код листинга 3.8.15.
Листинг 3.8.15. Карринг
Результат
def squareplus(x,y):
return x*x+y
def curry(f):
def res(y):
return lambda x: f(x,y)
return res
csp=curry(squareplus)
cspo=csp(1)
print(cspo(3))
10
Получается, что squareplus вызывается в три этапа:
1. На первом этапе функция «каррируется» — подготавливается к частичному
применению:
csp = curry(squareplus)
Функции
117
2. Далее подставляется 1 (собственно частичное применение — подстановка аргумента y)
cspo = csp(1)
3. На третьем этапе производится вызов функции с получением окончательного
результата:
print(cspo(3))
Функцию curry можно написать короче. Вместо внутренней именованной функции
res мы в return напишем анонимную функцию (листинг 3.8.16).
Листинг 3.8.16. Карринг с помощью анонимной функции
def squareplus(x,y):
return x*x+y
def curry(f):
return lambda y: lambda x: f(x,y)
csp=curry(squareplus)
cspo=csp(1)
print(cspo(3))
Применим теперь функцию карринга к списку (листинг 3.8.17).
Листинг 3.8.17
def squareplus(x,y):
return x*x+y
def curry(f):
return lambda y: lambda x: f(x,y)
csp=curry(squareplus)
L=[3,2,4,1,0,5]
print(L)
M=list(map(csp(1),L))
print(M)
P=list(map(csp(2),M))
print(P)
Шаг 13. Заметим, что карринг является обратимым (до того, как мы подставим
в него какой-либо аргумент). Напишем функционал превращения каррированной
функции в нормальную:
def uncurry(f):
Он будет возвращать обычную функцию от двух аргументов:
def uncurry(f):
return lambda x,y:
118
Глава 3
Внутри этой анонимной функции пошагово подставим значения аргументов в том
порядке, в каком мы запускали каррированную функцию:
def uncurry(f):
return lambda x,y: f(y)(x)
Проверим работу карринга и противоположного ему действия (листинг 3.8.18).
Листинг 3.8.18. Обратимость карринга
Результат
def squareplus(x,y):
return x*x+y
def curry(f):
return lambda y: lambda x: f(x,y)
def uncurry(f):
return lambda x,y: f(y)(x)
print(squareplus(3,1))
csp=curry(squareplus)
cspo=csp(1)
print(cspo(3))
newsquareplus=uncurry(csp)
print(newsquareplus(3,1))
10
10
10
В этой главе мы изучили возможности использования функций в Python — декомпозировали алгоритмы в функции, применяли приемы, связанные с функциями:
рекурсию, рекурсию высших порядков, использование функционалов, частичное
применение функций и карринг.
ГЛАВА
4
Поиск в длину и ширину, бэктрекинг,
динамическое программирование
Эта глава не вносит новые языковые конструкции. Но она важна тем, что развивает
приемы программирования из предыдущей главы, объединяя их в новые подходы
к решению задач: поиск в длину с бэктрекингом и рекурсией, поиск в ширину,
динамическое программирование с его разновидностью — динамикой по подотрезкам.
Эти подходы мы отработаем на ряде, казалось бы, различных задач. Мы будем искать выход из лабиринта, расставлять ферзей на шахматном поле так, чтобы они
друг друга не порубили, научимся укладывать рюкзак и т. д. В завершение мы
мысленно покинем наш трехмерный мир и перенесемся в бесконечномерный, пересчитывая, я хотел сказать, звезды на небе, нет, точки внутри бесконечномерной
сферы.
В общем, изучая алгоритмы этой главы, мы выведем наше алгоритмическое мышление на новый уровень.
4.1. Лабиринт
Задачи на поиск в длину и ширину начнем решать с лабиринта. Пусть лабиринт
задан двумерным списком, в котором 9 означает заполненное пространство (стена),
а 0 — пустое пространство (проход):
L=[[9,9,9,9,9,9,0,9,9,9],
[9,0,9,9,9,0,0,0,0,9],
[9,0,9,0,0,0,9,9,0,9],
[9,0,0,0,0,9,0,0,0,9],
[9,9,0,9,0,9,0,9,0,9],
[9,9,0,9,9,9,0,9,0,9],
[9,0,0,0,0,9,9,9,9,9],
[9,0,9,9,0,9,9,0,0,9],
[9,0,0,9,0,9,9,0,0,9],
[9,9,0,9,9,9,9,9,9,9]]
Для удобства восприятия напишем функцию вывода на экран, в которой нули в лабиринте будем выводить нулями, а девятки заменим на решетки (листинг 4.1.1).
120
Глава 4
Листинг 4.1.1
Результат
...
def show(L):
for i in range(len(L)):
s=""
for j in range(len(L)):
if L[i][j]==0:
s=s+"0"
elif L[i][j]==9:
s=s+"#"
print(s)
######0###
#0###0000#
#0#000##0#
#0000#000#
##0#0#0#0#
##0###0#0#
#0000#####
#0##0##00#
#00#0##00#
##0#######
show(L)
Задача 1
Разметить лабиринт стрелками в сторону выхода.
Языковые конструкции: двумерные списки, функции, циклы.
Напишем несколько версий программы.
Версия 1
Сначала используем самый примитивный алгоритм.
Ход программирования
Шаг 1. Прежде всего, определимся с обозначениями (табл. 4.1, рис. 4.1).
Таблица 4.1. Обозначения клеток в лабиринте
Смысл
В программе
Вывод на экран
Пустота
0
0
Стена
9
#
Движение вправо
1
>
Движение вверх
2
^
Движение влево
3
<
Движение вниз
4
v
Рис. 4.1. Вывод на экран обозначений клеток в лабиринте
Поиск в длину и ширину, бэктрекинг, динамическое программирование
121
Дополним функцию вывода на экран (листинг 4.1.2).
Листинг 4.1.2
def show(L):
for i in range(len(L)):
s=""
for j in range(len(L)):
if L[i][j]==0:
s=s+"0"
elif L[i][j]==9:
s=s+"#"
elif L[i][j]==1:
s=s+">"
elif L[i][j]==2:
s=s+"^"
elif L[i][j]==3:
s=s+"<"
elif L[i][j]==4:
s=s+"v"
print(s)
Шаг 2. Прежде всего нужно обойти края лабиринта, найти входы (они же выходы)
и проставить нужные стрелки. Будем каждый алгоритм помещать в отдельную
функцию (листинг 4.1.3).
Листинг 4.1.3
...
def simple(L):
for i in range(len(L)):
if L[0][i] == 0:
L[0][i] = 2
if L[-1][i] == 0:
L[-1][i] = 4
if L[i][0] == 0:
L[i][0] = 3
if L[i][-1] == 0:
L[i][-1] = 1
simple(L)
show(L)
Результат
######^###
#0###0000#
#0#000##0#
#0000#000#
##0#0#0#0#
##0###0#0#
#0000#####
#0##0##00#
#00#0##00#
##v#######
Шаг 3. Простой алгоритм разметки лабиринта заключается в том, что мы будем
просматривать внутренность лабиринта (циклом внутри цикла) в поисках еще не
размеченных клеток (там, где хранится 0). Для каждой нулевой клетки посмотрим
соседние («оглядимся по сторонам»). Если найдем соседнюю клетку со стрелочкой
122
Глава 4
(значениями 1, 2, 3, 4), то нарисуем стрелку в сторону этой соседней клетки (листинг 4.1.4).
Листинг 4.1.4
...
def simple(L):
for i in range(len(L)):
if L[0][i] == 0:
L[0][i] = 2
if L[-1][i] == 0:
L[-1][i] = 4
if L[i][0] == 0:
L[i][0] = 3
if L[i][-1] == 0:
L[i][-1] = 1
for i in range(1, len(L) - 1):
for j in range(1, len(L) - 1):
if L[i][j] == 0:
if 1<=L[i][j + 1]<=4:
L[i][j] = 1
elif 1<=L[i - 1][j]<=4:
L[i][j] = 2
elif 1<=L[i][j - 1]<=4:
L[i][j] = 3
elif 1<=L[i + 1][j]<=4:
L[i][j] = 4simple(L)
simple(L)
show(L)
Результат
######^###
#0###0^<<#
#0#000##^#
#0000#00^#
##0#0#0#^#
##0###0#^#
#0000#####
#0##0##00#
#0v#0##00#
##v#######
Шаг 4. Проанализируем результат выполнения программы на предыдущем шаге.
Мы видим, что стрелки появились далеко не во всех нужных клетках. Почему же
этого не произошло? Дело в том, что лабиринт мы просматривали справа налево
сверху вниз (как мы читаем книги), и многие клетки с нулями просмотрели еще до
того, как в соседних клетках появились стрелки.
Получается, что одного просмотра лабиринта мало. Нужно несколько просмотров.
Оценим максимально возможное количество просмотров. Если предположить, что
все пустые клетки вытянуты в одну длинную цепочку и за один просмотр лабиринта мы рисуем только одну стрелку, то количество просмотров будет равно
количеству клеток во всем лабиринте.
Поместим циклы просмотра лабиринта внутрь еще одного цикла — получим цикл
внутри цикла внутри цикла (листинг 4.1.5).
Поиск в длину и ширину, бэктрекинг, динамическое программирование
Листинг 4.1.5
...
def simple(L):
for i in range(len(L)):
if L[0][i] == 0:
L[0][i] = 2
if L[-1][i] == 0:
L[-1][i] = 4
if L[i][0] == 0:
L[i][0] = 3
if L[i][-1] == 0:
L[i][-1] = 1
for k in range(len(L)*len(L)):
for i in range(1, len(L) - 1):
for j in range(1, len(L) - 1):
if L[i][j] == 0:
if 1<=L[i][j + 1]<=4:
L[i][j] = 1
elif 1<=L[i - 1][j]<=4:
L[i][j] = 2
elif 1<=L[i][j - 1]<=4:
L[i][j] = 3
elif 1<=L[i + 1][j]<=4:
L[i][j] = 4
simple(L)
show(L)
123
Результат
######^###
#v###>^<<#
#v#>>^##^#
#>>>^#>>^#
##^#^#^#^#
##^###^#^#
#v<<<#####
#v##^##00#
#>v#^##00#
##v#######
Теперь алгоритм работает правильно — нулями осталась заполнена только изолированная область в правом нижнем углу.
Шаг 5. Можно ли сделать программу быстрее, прервав цикл k? Признаком того,
что лабиринт уже размечен, является то, что очередной просмотр лабиринта не
приводит к появлению новых клеток. Сделаем эту проверку с помощью флага (листинг 4.1.6).
Листинг 4.1.6
...
def simple(L):
...
for k in range(len(L)*len(L)):
f=1
for i in range(1, len(L) - 1):
for j in range(1, len(L) - 1):
if L[i][j] == 0:
if 1<=L[i][j + 1]<=4:
L[i][j] = 1
f=0
124
Глава 4
elif 1<=L[i - 1][j]<=4:
L[i][j] = 2
f=0
elif 1<=L[i][j - 1]<=4:
L[i][j] = 3
f=0
elif 1<=L[i + 1][j]<=4:
L[i][j] = 4
f=0
if f==1:
break
Мы получили первую версию программы. Приведем ее код полностью (листинг 4.1.7).
Листинг 4.1.7. Простой способ разметки лабиринта
L=[[9,9,9,9,9,9,0,9,9,9],
[9,0,9,9,9,0,0,0,0,9],
[9,0,9,0,0,0,9,9,0,9],
[9,0,0,0,0,9,0,0,0,9],
[9,9,0,9,0,9,0,9,0,9],
[9,9,0,9,9,9,0,9,0,9],
[9,0,0,0,0,9,9,9,9,9],
[9,0,9,9,0,9,9,0,0,9],
[9,0,0,9,0,9,9,0,0,9],
[9,9,0,9,9,9,9,9,9,9]]
def show(L):
for i in range(len(L)):
s=""
for j in range(len(L)):
if L[i][j]==0:
s=s+"0"
elif L[i][j]==9:
s=s+"#"
elif L[i][j]==1:
s=s+">"
elif L[i][j]==2:
s=s+"^"
elif L[i][j]==3:
s=s+"<"
elif L[i][j]==4:
s=s+"v"
elif L[i][j]==5:
s=s+"a"
elif L[i][j]==6:
s=s+"b"
Поиск в длину и ширину, бэктрекинг, динамическое программирование
125
elif L[i][j]==7:
s=s+"c"
elif L[i][j]==8:
s=s+"d"
print(s)
def simple(L):
for i in range(len(L)):
if L[0][i] == 0:
L[0][i] = 2
if L[-1][i] == 0:
L[-1][i] = 4
if L[i][0] == 0:
L[i][0] = 3
if L[i][-1] == 0:
L[i][-1] = 1
for k in range(len(L)*len(L)):
f=1
for i in range(1, len(L) - 1):
for j in range(1, len(L) - 1):
if L[i][j] == 0:
if 1<=L[i][j + 1]<=4:
L[i][j] = 1
f=0
elif 1<=L[i - 1][j]<=4:
L[i][j] = 2
f=0
elif 1<=L[i][j - 1]<=4:
L[i][j] = 3
f=0
elif 1<=L[i + 1][j]<=4:
L[i][j] = 4
f=0
if f==1:
break
simple(L)
show(L)
Версия 2
Разметим лабиринт стрелками методом поиска в длину с бэктрекингом.
Идея алгоритма
Надо действовать так, как если бы лабиринт размечал человек. Найдя вход, он идет
по лабиринту, рисуя стрелки в ту сторону, откуда пришел. Зайдя в тупик, человек
126
Глава 4
возвращается по своим стрелкам назад до ближайшего перекрестка и сворачивает
в проход, который еще не разметил.
Ход программирования
Шаг 1. Так же как и в предыдущем методе, нужно обойти внешний край лабиринта
и найти входы (выходы). Человек может зайти в любой из входов. Более того, ему
нужно зайти в каждый из еще не размеченных входов (лабиринт может состоять из
нескольких изолированных секторов). Пусть функция backtrack — это общая функция разметки методом поиска в длину. В нее заложен просмотр края лабиринта.
Внутрь нее поместим функцию bt, собственно реализующую метод — заход внутрь
лабиринта (листинг 4.1.8).
Листинг 4.1.8
...
def backtrack(L):
def bt(L, i0, j0):
...
for i in range(len(L)):
if L[0][i] == 0:
L[0][i] = 2
bt(L, 0, i)
if L[-1][i] == 0:
L[-1][i] = 4
bt(L, len(L)-1, i)
if L[i][0] == 0:
L[i][0] = 3
bt(L, i, 0)
if L[i][-1] == 0:
L[i][-1] = 1
bt(L, i, len(L)-1)
Шаг 2. Начнем реализовывать функцию bt. Пусть человек, перемещающийся по
лабиринту, имеет координаты (i,j). Начальные значения их будут равны i0 и j0
(листинг 4.1.9).
Листинг 4.1.9
...
def backtrack(L):
def bt(L, i0, j0):
i=i0
j=j0
...
Человек может перемещаться по клеткам лабиринта с количеством шагов, равным
размеру лабиринта. Каждый шаг будем делать внутри цикла (листинг 4.1.10).
Поиск в длину и ширину, бэктрекинг, динамическое программирование
127
Листинг 4.1.10
...
def backtrack(L):
def bt(L, i0, j0):
i=i0
j=j0
for k in range(len(L)*len(L)):
...
Шаг заключается в том, что человек оглядывается по сторонам (проверяем соседние клетки) в поисках прохода и делает шаг в проход — меняет координаты i и j
(листинг 4.1.11).
Листинг 4.1.11
...
def backtrack(L):
def bt(L, i0, j0):
i=i0
j=j0
for k in range(len(L)*len(L)):
if j + 1 < len(L) and L[i][j + 1] == 0:
L[i][j + 1] = 3
j = j + 1
elif i - 1 >= 0 and L[i - 1][j] == 0:
L[i - 1][j] = 4
i = i - 1
elif j - 1 >= 0 and L[i][j - 1] == 0:
L[i][j - 1] = 1
j = j - 1
elif i + 1 < len(L) and L[i + 1][j] == 0:
L[i + 1][j] = 2
i = i + 1
...
backtrack(L)
show(L)
Результат:
######^###
#0###0^<<#
#0#000##^#
#0000#>>^#
##0#0#^#0#
##0###^#0#
#v<<<#####
#v##^##00#
#>v#^##00#
##v#######
128
Глава 4
Шаг 3. Посмотрим на результаты выполнения программы. Видно, что человек заходил в лабиринт два раза (в каждый вход), доходил до тупика и... не возвращался!
Теперь нужно запрограммировать бэктрекинг — возврат человека по собственным
стрелкам. Сделаем прерывание цикла и закончим выполнение функции тогда, когда
человек вернется в ту же клетку, с которой стартовал. Полный текст программы
приведен в листинге 4.1.12 (бэктрекинг выделен полужирным шрифтом).
Листинг 4.1.12. Обход лабиринта поиском в длину с бэктрекингом
L=[[9,9,9,9,9,9,0,9,9,9],
[9,0,9,9,9,0,0,0,0,9],
[9,0,9,0,0,0,9,9,0,9],
[9,0,0,0,0,9,0,0,0,9],
[9,9,0,9,0,9,0,9,0,9],
[9,9,0,9,9,9,0,9,0,9],
[9,0,0,0,0,9,9,9,9,9],
[9,0,9,9,0,9,9,0,0,9],
[9,0,0,9,0,9,9,0,0,9],
[9,9,0,9,9,9,9,9,9,9]]
def show(L):
for i in range(len(L)):
s=""
for j in range(len(L)):
if L[i][j]==0:
s=s+"0"
elif L[i][j]==9:
s=s+"#"
elif L[i][j]==1:
s=s+">"
elif L[i][j]==2:
s=s+"^"
elif L[i][j]==3:
s=s+"<"
elif L[i][j]==4:
s=s+"v"
print(s)
def backtrack(L):
def bt(L, i0, j0):
i=i0
j=j0
for k in range(len(L)*len(L)):
if j + 1 < len(L) and L[i][j + 1] == 0:
L[i][j + 1] = 3
j = j + 1
Поиск в длину и ширину, бэктрекинг, динамическое программирование
elif i - 1 >= 0 and L[i - 1][j] == 0:
L[i - 1][j] = 4
i = i - 1
elif j - 1 >= 0 and L[i][j - 1] == 0:
L[i][j - 1] = 1
j = j - 1
elif i + 1 < len(L) and L[i + 1][j] == 0:
L[i + 1][j] = 2
i = i + 1
else:
if i==i0 and j==j0:
break
if L[i][j] == 1:
j = j + 1
elif L[i][j] == 2:
i = i - 1
elif L[i][j] == 3:
j = j - 1
elif L[i][j] == 4:
i = i + 1
for i in range(len(L)):
if L[0][i] == 0:
L[0][i] = 2
bt(L, 0, i)
if L[-1][i] == 0:
L[-1][i] = 4
bt(L, len(L)-1, i)
if L[i][0] == 0:
L[i][0] = 3
bt(L, i, 0)
if L[i][-1] == 0:
L[i][-1] = 1
bt(L, i, len(L)-1)
backtrack(L)
show(L)
Результат:
######v###
#v###v<<<#
#v#>v<##^#
#>v<<#>>^#
##v#^#^#^#
##v###^#^#
#v<<<#####
#v##^##00#
#>v#^##00#
##v#######
129
130
Глава 4
Версия 3
Разметим лабиринт стрелочками методом поиска в длину с рекурсией.
Мы можем модифицировать предыдущую программу (разметки в длину), заменив
бэктрекинг на рекурсию.
Шаг 1. Прежде всего уберем бэктрекинг (возвращение из тупика по размеченным
стрелкам). Это показано в листинге 4.1.13.
Листинг 4.1.13
...
def DFS(L):
def bt(L, i0, j0):
i=i0
j=j0
for k in range(len(L)*len(L)):
if j + 1 < len(L) and L[i][j + 1] == 0:
L[i][j + 1] = 3
j = j + 1
elif i - 1 >= 0 and L[i - 1][j] == 0:
L[i - 1][j] = 4
i = i - 1
elif j - 1 >= 0 and L[i][j - 1] == 0:
L[i][j - 1] = 1
j = j - 1
elif i + 1 < len(L) and L[i + 1][j] == 0:
L[i + 1][j] = 2
i = i + 1
else:
if i==i0 and j==j0:
break
if L[i][j] == 1:
j = j + 1
elif L[i][j] == 2:
i = i - 1
elif L[i][j] == 3:
j = j - 1
elif L[i][j] == 4:
i = i + 1
...
Шаг 2. Уберем перемещение с помощью рекуррентных формул (листинг 4.1.14).
Листинг 4.1.14
def DFS(L):
def bt(L, i0, j0):
i=i0
j=j0
Поиск в длину и ширину, бэктрекинг, динамическое программирование
131
for k in range(len(L)*len(L)):
if j + 1 < len(L) and L[i][j + 1] == 0:
L[i][j + 1] = 3
j = j + 1
elif i - 1 >= 0 and L[i - 1][j] == 0:
L[i - 1][j] = 4
i = i - 1
elif j - 1 >= 0 and L[i][j - 1] == 0:
L[i][j - 1] = 1
j = j - 1
elif i + 1 < len(L) and L[i + 1][j] == 0:
L[i + 1][j] = 2
i = i + 1
Заменим это перемещение на рекурсивный вызов этой же самой функции (листинг 4.1.15).
Листинг 4.1.15
...
def DFS(L):
def bt(L, i0, j0):
i=i0
j=j0
for k in range(len(L)*len(L)):
if j + 1 < len(L) and L[i][j + 1] == 0:
L[i][j + 1] = 3
bt(L, i, j+1)
elif i - 1 >= 0 and L[i - 1][j] == 0:
L[i - 1][j] = 4
bt(L, i-1, j)
elif j - 1 >= 0 and L[i][j - 1] == 0:
L[i][j - 1] = 1
bt(L, i, j-1)
elif i + 1 < len(L) and L[i + 1][j] == 0:
L[i + 1][j] = 2
bt(L, i+1, j)
...
Шаг 3. В случае перекрестка функция должна быть запущена по всем возможным
направлениям. Поэтому elif заменим на if (листинг 4.1.16).
Листинг 4.1.16
...
def DFS(L):
def bt(L, i0, j0):
i=i0
j=j0
132
Глава 4
for k in range(len(L)*len(L)):
if j + 1 < len(L) and L[i][j + 1] == 0:
L[i][j + 1] = 3
bt(L, i, j+1)
elif i - 1 >= 0 and L[i - 1][j] == 0:
L[i - 1][j] = 4
bt(L, i-1, j)
elif j - 1 >= 0 and L[i][j - 1] == 0:
L[i][j - 1] = 1
bt(L, i, j-1)
elif i + 1 < len(L) and L[i + 1][j] == 0:
L[i + 1][j] = 2
bt(L, i+1, j)
...
Шаг 4. Сохранение переменных i0 и j0 теряет смысл, уберем их (листинг 4.1.17).
Листинг 4.1.17
...
def DFS(L):
def bt(L, i0, j0):
i=i0
j=j0
for k in range(len(L)*len(L)):
if j + 1 < len(L) and L[i][j + 1] == 0:
L[i][j + 1] = 3
bt(L, i, j+1)
if i - 1 >= 0 and L[i - 1][j] == 0:
L[i - 1][j] = 4
bt(L, i-1, j)
if j - 1 >= 0 and L[i][j - 1] == 0:
L[i][j - 1] = 1
bt(L, i, j-1)
if i + 1 < len(L) and L[i + 1][j] == 0:
L[i + 1][j] = 2
bt(L, i+1, j)
...
Мы получим работающую версию программы (листинг 4.1.18).
Листинг 4.1.18. Разметка лабиринта поиском в длину с рекурсией
L=[[9,9,9,9,9,9,0,9,9,9],
[9,0,9,9,9,0,0,0,0,9],
[9,0,9,0,0,0,9,9,0,9],
[9,0,0,0,0,9,0,0,0,9],
Поиск в длину и ширину, бэктрекинг, динамическое программирование
[9,9,0,9,0,9,0,9,0,9],
[9,9,0,9,9,9,0,9,0,9],
[9,0,0,0,0,9,9,9,9,9],
[9,0,9,9,0,9,9,0,0,9],
[9,0,0,9,0,9,9,0,0,9],
[9,9,0,9,9,9,9,9,9,9]]
def show(L):
for i in range(len(L)):
s=""
for j in range(len(L)):
if L[i][j]==0:
s=s+"0"
elif L[i][j]==9:
s=s+"#"
elif L[i][j]==1:
s=s+">"
elif L[i][j]==2:
s=s+"^"
elif L[i][j]==3:
s=s+"<"
elif L[i][j]==4:
s=s+"v"
print(s)
def DFS(L):
def bt(L, i, j):
if j+1 < len(L) and L[i][j+1] == 0:
L[i][j+1] = 3
bt(L, i, j+1)
if i-1 >= 0 and L[i-1][j] == 0:
L[i-1][j] = 4
bt(L, i-1, j)
if j-1 >= 0 and L[i][j-1] == 0:
L[i][j-1] = 1
bt(L, i, j-1)
if i+1 < len(L) and L[i+1][j] == 0:
L[i+1][j] = 2
bt(L, i+1, j)
for i in range(len(L)):
if L[0][i] == 0:
L[0][i] = 2
bt(L, 0, i)
if L[-1][i] == 0:
L[-1][i] = 4
bt(L, len(L)-1, i)
133
134
Глава 4
if L[i][0] == 0:
L[i][0] = 3
bt(L, i, 0)
if L[i][-1] == 0:
L[i][-1] = 1
bt(L, i, len(L)-1)
DFS(L)
show(L)
Если мы посмотрим на результат работы обоих методов поиска в длину (бэктрекингом и рекурсией — их результаты одинаковы), то мы увидим странное:
######v###
#v###v<<<#
#v#>v<##^#
#>v<<#>>^#
##v#^#^#^#
##v###^#^#
#v<<<#####
#v##^##00#
#>v#^##00#
##v#######
У верхнего входа стрелка направлена не вверх, а вниз. Это связано с тем, что мы
зашли в лабиринт с нижнего входа. Человек ходил по лабиринту и рисовал стрелочки назад, в том числе дойдя до верхнего входа. Можно ли сделать разметку
лабиринта оптимальной, т. е. такой, чтобы стрелки были нарисованы к самому
близкому входу? Можно. Для этого нужно использовать другой алгоритм — поиск
в ширину.
Версия 4
Разметим лабиринт стрелками методом поиска в ширину.
Идея алгоритма
Заметим, что в рекурсивном поиске в длину на каждом перекрестке функция запускается сразу для всех разветвлений (как будто человек раздваивается, и дальше уже
разные люди идут по всем направлениям). Рекурсия дает ту же картинку, что и
бэктрекинг, так что этот параллелизм мнимый. Рекурсивный вызов осуществляется
«в длину».
Идея алгоритма поиска в ширину заключается в том, что вместо одного человека
мы запускаем по лабиринту во все входы много людей, которые идут по всем проходам. Рассмотрим работу по этапам:
Поиск в длину и ширину, бэктрекинг, динамическое программирование
Исходный лабиринт
Этапы 1, 2
Этапы 2, 3, 4
######0###
#0###0000#
#0#000##0#
#0000#000#
##0#0#0#0#
##0###0#0#
#0000#####
#0##0##00#
#00#0##00#
##0#######
######^###
#0###0^00#
#0#000##0#
#0000#000#
##0#0#0#0#
##0###0#0#
#0000#####
#0##0##00#
#0v#0##00#
##v#######
######^###
#0###>^<<#
#0#0>^##^#
#0000#000#
##0#0#0#0#
##0###0#0#
#v000#####
#v##0##00#
#>v#0##00#
##v#######
Этап 5
Этап 6
Этап 7
######^###
#0###>^<<#
#0#>>^##^#
#000^#00^#
##0#0#0#0#
##0###0#0#
#v<00#####
#v##0##00#
#>v#0##00#
##v#######
######^###
#0###>^<<#
#0#>>^##^#
#00>^#0>^#
##0#^#0#^#
##0###0#0#
#v<00#####
#v##0##00#
#>v#0##00#
##v#######
######^###
#0###>^<<#
#0#>>^##^#
#0>>^#>>^#
##0#^#0#^#
##v###0#^#
#v<<0#####
#v##0##00#
#>v#0##00#
##v#######
Этап 8
Этап 9
Этап 10
######^###
#0###>^<<#
#0#>>^##^#
#>>>^#>>^#
##v#^#^#^#
##v###0#^#
#v<<<#####
#v##0##00#
#>v#0##00#
##v#######
######^###
#0###>^<<#
#v#>>^##^#
#>>>^#>>^#
##v#^#^#^#
##v###^#^#
#v<<<#####
#v##^##00#
#>v#0##00#
##v#######
######^###
#v###>^<<#
#v#>>^##^#
#>>>^#>>^#
##v#^#^#^#
##v###^#^#
#v<<<#####
#v##^##00#
#>v#^##00#
##v#######
135
Ход программирования
Шаг 1. При просмотре края лабиринта мы станем не только рисовать стрелки, но и
запоминать клетки в списке. Точнее, запоминать координаты клеток в двух списках: I и J. Также введем счетчик количества запомненных клеток (листинг 4.1.19).
Листинг 4.1.19
...
def SBF(L):
I = []
J = []
k=0
136
Глава 4
for i in range(len(L)):
if L[0][i] == 0:
L[0][i] = 2
I.append(0)
J.append(i)
k=k+1
if L[-1][i] == 0:
L[-1][i] = 4
I.append(len(L)-1)
J.append(i)
k=k+1
if L[i][0] == 0:
L[i][0] = 3
I.append(i)
J.append(0)
k=k+1
if L[i][-1] == 0:
L[i][-1] = 1
I.append(i)
J.append(len(L)-1)
k=k+1
Шаг 2. В цикле будем по порядку перебирать запомненные клетки и осматривать
их окрестности. Если найдем клетки-проходы, запомним их координаты в этих же
списках I и J, обеспечив тем самым одновременность исследования лабиринта многими людьми. Цикл будет выполняться до тех пор, пока не перестанут пополняться
списки, — т. е. мы дойдем до конца I и J (листинг 4.1.20).
Листинг 4.1.20
def SBF(L):
I = []
J = []
k=0
...
i=0
while i<k:
if J[i] + 1 < len(L):
if L[I[i]][J[i] + 1] ==
L[I[i]][J[i] + 1] =
I.append(I[i])
J.append(J[i] + 1)
k=k+1
if I[i] - 1 > 0:
if L[I[i] - 1][J[i]] ==
L[I[i] - 1][J[i]] =
I.append(I[i] - 1)
0:
3
0:
4
Поиск в длину и ширину, бэктрекинг, динамическое программирование
J.append(J[i])
k=k+1
if J[i] - 1 > 0:
if L[I[i]][J[i] - 1] ==
L[I[i]][J[i] - 1] =
I.append(I[i])
J.append(J[i] - 1)
k=k+1
if I[i] + 1 < len(L):
if L[I[i] + 1][J[i]] ==
L[I[i] + 1][J[i]] =
I.append(I[i] + 1)
J.append(J[i])
k=k+1
i=i+1
0:
1
0:
2
Полный текст программы приведен в листинге 4.1.21.
Листинг 4.1.21
L=[[9,9,9,9,9,9,0,9,9,9],
[9,0,9,9,9,0,0,0,0,9],
[9,0,9,0,0,0,9,9,0,9],
[9,0,0,0,0,9,0,0,0,9],
[9,9,0,9,0,9,0,9,0,9],
[9,9,0,9,9,9,0,9,0,9],
[9,0,0,0,0,9,9,9,9,9],
[9,0,9,9,0,9,9,0,0,9],
[9,0,0,9,0,9,9,0,0,9],
[9,9,0,9,9,9,9,9,9,9]]
def show(L):
for i in range(len(L)):
s=""
for j in range(len(L)):
if L[i][j]==0:
s=s+"0"
elif L[i][j]==9:
s=s+"#"
elif L[i][j]==1:
s=s+">"
elif L[i][j]==2:
s=s+"^"
elif L[i][j]==3:
s=s+"<"
elif L[i][j]==4:
s=s+"v"
137
138
Глава 4
elif L[i][j]==5:
s=s+"a"
elif L[i][j]==6:
s=s+"b"
elif L[i][j]==7:
s=s+"c"
elif L[i][j]==8:
s=s+"d"
print(s)
def SBF(L):
I = []
J = []
k=0
for i in range(len(L)):
if L[0][i] == 0:
L[0][i] = 2
I.append(0)
J.append(i)
k=k+1
if L[-1][i] == 0:
L[-1][i] = 4
I.append(len(L)-1)
J.append(i)
k=k+1
if L[i][0] == 0:
L[i][0] = 3
I.append(i)
J.append(0)
k=k+1
if L[i][-1] == 0:
L[i][-1] = 1
I.append(i)
J.append(len(L)-1)
k=k+1
i=0
while i<k:
if J[i] + 1 < len(L):
if L[I[i]][J[i] + 1] ==
L[I[i]][J[i] + 1] =
I.append(I[i])
J.append(J[i] + 1)
k=k+1
if I[i] - 1 > 0:
if L[I[i] - 1][J[i]] ==
L[I[i] - 1][J[i]] =
I.append(I[i] - 1)
0:
3
0:
4
Поиск в длину и ширину, бэктрекинг, динамическое программирование
J.append(J[i])
k=k+1
if J[i] - 1 > 0:
if L[I[i]][J[i] - 1] ==
L[I[i]][J[i] - 1] =
I.append(I[i])
J.append(J[i] - 1)
k=k+1
if I[i] + 1 < len(L):
if L[I[i] + 1][J[i]] ==
L[I[i] + 1][J[i]] =
I.append(I[i] + 1)
J.append(J[i])
k=k+1
i=i+1
139
0:
1
0:
2
SBF(L)
show(L)
Задача 2
Разбить лабиринт на секторы — области достижимости.
Под сектором будем понимать все клетки, в которые можно дойти из заданной
клетки. Внесем в наш лабиринт одну перегородку (выделена):
######0###
#0###0000#
#0#000##0#
#0000#000#
####0#0#0#
##0###0#0#
#0000#####
#0##0##00#
#00#0##00#
##0#######
Видно, что лабиринт теперь состоит из трех секторов: верхнего, нижнего и изолированных клеток в правом нижнем углу.
Идея алгоритма
Будем просматривать лабиринт в поисках еще не размеченных клеток. Если найдем
неразмеченную, то для нее запустим рекурсивную функцию разметки (модифицируем ее так, чтобы она вместо стрелок проставляла метку лабиринта).
Ход программирования
Шаг 1. Добавим в функцию вывода лабиринта на экран еще четыре значения —
секторы при выводе будем обозначать буквами: a, b, c и d (листинг 4.1.22).
140
Глава 4
Листинг 4.1.22
def show(L):
for i in range(len(L)):
s=""
for j in range(len(L)):
if L[i][j]==0:
s=s+"0"
elif L[i][j]==9:
s=s+"#"
elif L[i][j]==1:
s=s+">"
elif L[i][j]==2:
s=s+"^"
elif L[i][j]==3:
s=s+"<"
elif L[i][j]==4:
s=s+"v"
elif L[i][j]==5:
s=s+"a"
elif L[i][j]==6:
s=s+"b"
elif L[i][j]==7:
s=s+"c"
elif L[i][j]==8:
s=s+"d"
print(s)
Шаг 2. начнем просматривать лабиринт в поисках нулевых (непомеченных) клеток.
Заметим, что, в отличие от всех предыдущих алгоритмов, мы осматриваем не граничные клетки лабиринта, а все клетки (чтобы изолированные области тоже были
помечены). Введем счетчик — маркер области (его значения 5, 6, 7, 8 соответствуют
областям a, b, c, d). Для каждой нулевой клетки станем запускать вложенную функцию поиска области достижимости (листинг 4.1.23).
Листинг 4.1.23
...
def sector(L):
def bt(L, i, j, a):
...
a = 5
for i in range(len(L)):
for j in range(len(L)):
if L[i][j] == 0:
bt(L,i,j,a)
a = a + 1
Поиск в длину и ширину, бэктрекинг, динамическое программирование
141
Шаг 3. Что же это за функция поиска области достижимости? Это уже знакомая
нам функция поиска в длину с рекурсией, только переделанная для разметки не
стрелочками, а маркером области (листинг 4.1.24)!
Листинг 4.1.24
...
def sector(L):
def bt(L, i, j, a):
if j+1 < len(L) and L[i][j+1] == 0:
L[i][j+1] = a
bt(L, i, j+1, a)
if i-1 >= 0 and L[i-1][j] == 0:
L[i-1][j] = a
bt(L, i-1, j, a)
if j-1 >= 0 and L[i][j-1] == 0:
L[i][j-1] = a
bt(L, i, j-1, a)
if i+1 < len(L) and L[i+1][j] == 0:
L[i+1][j] = a
bt(L, i+1, j, a)
...
Приведем полный код программы и результаты ее выполнения (листинг 4.1.25).
Листинг 4.1.25. Разметка лабиринта на секторы
L=[[9,9,9,9,9,9,0,9,9,9],
[9,0,9,9,9,0,0,0,0,9],
[9,0,9,0,0,0,9,9,0,9],
[9,0,0,0,0,9,0,0,0,9],
[9,9,9,9,0,9,0,9,0,9],
[9,9,0,9,9,9,0,9,0,9],
[9,0,0,0,0,9,9,9,9,9],
[9,0,9,9,0,9,9,0,0,9],
[9,0,0,9,0,9,9,0,0,9],
[9,9,0,9,9,9,9,9,9,9]]
def show(L):
for i in range(len(L)):
s=""
for j in range(len(L)):
if L[i][j]==0:
s=s+"0"
elif L[i][j]==9:
s=s+"#"
elif L[i][j]==1:
s=s+">"
142
Глава 4
elif L[i][j]==2:
s=s+"^"
elif L[i][j]==3:
s=s+"<"
elif L[i][j]==4:
s=s+"v"
elif L[i][j]==5:
s=s+"a"
elif L[i][j]==6:
s=s+"b"
elif L[i][j]==7:
s=s+"c"
elif L[i][j]==8:
s=s+"d"
print(s)
def sector(L):
def bt(L, i, j, a):
if j+1 < len(L) and L[i][j+1] == 0:
L[i][j+1] = a
bt(L, i, j+1, a)
if i-1 >= 0 and L[i-1][j] == 0:
L[i-1][j] = a
bt(L, i-1, j, a)
if j-1 >= 0 and L[i][j-1] == 0:
L[i][j-1] = a
bt(L, i, j-1, a)
if i+1 < len(L) and L[i+1][j] == 0:
L[i+1][j] = a
bt(L, i+1, j, a)
a = 5
for i in range(len(L)):
for j in range(len(L)):
if L[i][j] == 0:
bt(L,i,j,a)
a = a + 1
show(L)
print()
sector(L)
show(L)
Результат:
######a###
#a###aaaa#
#a#aaa##a#
#aaaa#aaa#
####a#a#a#
##b###a#a#
Поиск в длину и ширину, бэктрекинг, динамическое программирование
143
#bbbb#####
#b##b##cc#
#bb#b##cc#
##b#######
Задачи на исследование лабиринта — это наиболее наглядная демонстрация методов поиска в длину и ширину. Далее мы рассмотрим еще несколько задач, где эти
методы с успехом применяются.
4.2. Задача о восьми ферзях
Отработаем поиск в длину бэктрекингом и рекурсией еще на одном классическом
примере — задаче о восьми ферзях.
Задача
Вводится размер квадратного поля (шахматной доски) n. Надо расставить на доске n
ферзей так, чтобы они друг друга не порубили. Ферзь рубит все фигуры, которые
находятся с этим ферзем на одной строке, колонке и диагонали, независимо от расстояния.
Версия 1
Напишем программу поиска расположения ферзей бэктрекингом.
Идея алгоритма
Очевидно, в каждой строчке должно быть по ферзю. Будем искать место для ферзей построчно. Рассмотрим сначала маленькие поля.
Если размер поля 1×1, очевидно размещение одного ферзя:
Если поле 2×2 — оставим одного ферзя и отметим зону поражения:
Первый ферзь
Зона поражения
Очевидно, второго ферзя здесь разместить не получится.
Рассмотрим поле 3×3 — разместим первого ферзя в углу и отметим зону поражения:
Ф
Первый ферзь
Зона поражения
144
Глава 4
Поставим второго ферзя в свободную клетку второй строки:
Второй ферзь
Зона поражения
Третьего ферзя ставить некуда. Попробуем еще одно расположение — разместим
первого ферзя в центр первой строки и отметим зону поражения:
Ф
Второй ферзь
Зона поражения
Для ферзя на второй строке места не нашлось. Получается, что на поле 3×3 трех
ферзей не разместить.
Исследуем поле 4×4 — поместим первого ферзя в угол и отметим зону поражения:
Первый ферзь
Зона поражения
Поставим второго ферзя в свободную клетку второй строки и отметим зону поражения:
Второй ферзь
Зона поражения
Ферзя на третьей строчке ставить некуда. Попробуем еще одно расположение второго ферзя:
Второй ферзь
Зона поражения
Поиск в длину и ширину, бэктрекинг, динамическое программирование
145
Поставим третьего ферзя:
Третий ферзь
Зона поражения
Теперь для четвертого ферзя места не нашлось. Но мы можем попробовать сдвинуть первого ферзя:
Первый ферзь
Зона поражения
Второй ферзь
Зона поражения
Третий ферзь
Зона поражения
Разместим второго ферзя:
Разместим третьего ферзя:
Место для четвертого ферзя нашлось!
Мы получили расположение ферзей для поля 4×4:
146
Глава 4
Рассмотрим поле 5×5 — поставим ферзя на первую клетку первой строки:
× × × ×
× ×
×
×
×
×
×
×
Ф
Ф
Первый ферзь
Зона поражения
Второго ферзя поставим на вторую строку (первая и вторая клетки второй строки
не годятся, т. к. находятся в зоне поражения ферзя с первой строки):
× × × ×
× × Ф
×
×
×
×
×
×
Ф
Второй ферзь
× × × ×
× × Ф × ×
× × × ×
×
× × ×
×
×
×
Ф
Зона поражения
Третьего ферзя поставим в допустимую клетку третьей строки:
× × × ×
× × Ф × ×
× × × × Ф
×
× × ×
×
×
×
Ф
Третий ферзь
× × × ×
× × Ф × ×
× × × × Ф
×
× × ×
×
×
×
Ф
Зона поражения
Поставим четвертого ферзя и отметим его зону поражения:
× × × ×
× × Ф × ×
× × × × Ф
× Ф × × ×
×
×
×
Ф
Четвертый ферзь
× × × ×
× × Ф × ×
× × × × Ф
× Ф × × ×
× × ×
×
Ф
Зона поражения
Ставим еще одного (пятого) ферзя в единственную свободную клетку:
× × × ×
× × Ф × ×
× × × × Ф
× Ф × × ×
× × × Ф ×
Ф
Поиск в длину и ширину, бэктрекинг, динамическое программирование
147
И мы получили следующее расположение ферзей для поля 5×5:
Ф
Ф
Ф
Ф
Ф
Бэктрекинг будет заключаться в том, что, дойдя до последних строк и увидев, что
следующих ферзей ставить уже не получается, нам придется вернуться к предыдущим, уже расставленным ферзям, и сдвинуть их дальше. Посмотрим на работу бэктрекинга для поля 6×6:
Ф
Ф
Ф
Ф
Ф
×
Так, пытаясь найти место для шестого ферзя, мы увидим, что поставить его уже
просто некуда, — его срубит первый ферзь. Надо бы сдвинуть пятого ферзя, но некуда — он окажется под боем второго:
Ф
Ф
Ф
Ф
Ф
×
Видимо, надо двигать четвертого, но и ему больше места нет, поэтому двигаем
третьего. При новом расположении третьего ферзя мы можем расположить только
четвертого, а пятому не найдется места:
Ф
Ф
Ф
Ф
Третий уже стоит на последнем месте, поэтому двигаем второго, но при этом под
боем оказывается четвертый, так что двигаем и его:
148
Глава 4
Ф
Ф
Ф
Ф
Наконец, приходим к тому, что надо сдвинуть первого:
Ф
Ф
Ф
Ф
Это дает место пятому, но все еще нет места для шестого:
Ф
Ф
Ф
Ф
Ф
Ф
Тогда двигаем четвертого и ищем, куда подвинуть пятого, чтобы освободить место
для шестого:
Ф
Ф
Ф
Ф
Ф
Ф
Двигаем пятого и получаем хорошее расположение для шестого:
Ф
Ф
Ф
Ф
Ф
Ф
Ф
Поиск в длину и ширину, бэктрекинг, динамическое программирование
149
Мы решили типичную задачу на бэктрекинг: занимались перемещением последнего ферзя и, не находя нужной позиции, возвращались к предпоследнему расположению и более ранним.
Ход программирования
Шаг 1. Введем n — размер поля. Создадим поле F, заполненное нулями. Пусть символ Ф означает расположение ферзя. Напишем функцию вывода поля на экран (листинг 4.2.1).
Листинг 4.2.1
def show(F):
for s in F:
line="".join([str(el) for el in s])
print(line)
print()
n=int(input())
F=[[0]*n for i in range(n)]
F[2][1]="Ф"
show(F)
Результат:
8
00000000
00000000
0Ф000000
00000000
00000000
00000000
00000000
00000000
Шаг 2. Напишем функцию, которая определяет зону влияния одного ферзя, обозначив символом Х битое поле (листинг 4.2.2).
Листинг 4.2.2
def show(F):
for s in F:
line="".join([str(el) for el in s])
print(line)
print()
def f(i,j):
for k in range(len(F)):
A[i][k]="X"
A[k][j]="X"
150
Глава 4
if i+k<len(F) and j+k<len(F):
A[i+k][j+k]="X"
if i-k>=0 and j-k>=0:
A[i-k][j-k]="X"
if i+k<len(F) and j-k>=0:
A[i+k][j-k]="X"
if i-k>=0 and j+k<len(F):
A[i-k][j+k]="X"
n=int(input())
F=[[0]*n for i in range(n)]
F[2][1]="Ф"
A=[[0]*n for i in range(n)]
f(2,1)
show(A)
Результат:
8
0X0X0000
XXX00000
XXXXXXXX
XXX00000
0X0X0000
0X00X000
0X000X00
0X0000X0
Шаг 3. Напишем функцию, которая определяет зоны влияния всех ферзей. Имеющуюся функцию построения зоны влияния одного ферзя поместим внутрь новой
функции, т. к. функция построения зоны влияния одного ферзя нужна только в ней
(листинг 4.2.3).
Листинг 4.2.3
def show(F):
for s in F:
line="".join([str(el) for el in s])
print(line)
print()
def affect(F):
def f(i,j):
for k in range(len(F)):
A[i][k]="X"
A[k][j]="X"
if i+k<len(F) and j+k<len(F):
A[i+k][j+k]="X"
Поиск в длину и ширину, бэктрекинг, динамическое программирование
151
if i-k>=0 and j-k>=0:
A[i-k][j-k]="X"
if i+k<len(F) and j-k>=0:
A[i+k][j-k]="X"
if i-k>=0 and j+k<len(F):
A[i-k][j+k]="X"
A=[[0]*n for i in range(len(F))]
for i in range(len(F)):
for j in range(len(F)):
if(F[i][j]=="Ф"):
f(i,j)
return A
n=int(input())
F=[[0]*n for i in range(n)]
F[2][1]="Ф"
F[3][5]="Ф"
show(F)
A=affect(F)
show(A)
Результат:
8
00000000
00000000
0Ф000000
00000Ф00
00000000
00000000
00000000
00000000
0XXX0X00
XXXX0X0X
XXXXXXXX
XXXXXXXX
0X0XXXX0
0X0XXX0X
0XX00X00
0X000XX0
Шаг 4. Создадим пустое поле, в каждой строке которого постараемся разместить
ферзя. Для этого будем перебирать клетки каждой строки в поисках ячейки, свободной от ферзя и находящейся вне зоны поражения (каждый раз, размещая ферзя,
будем пересчитывать зону поражения). Фрагмент программы приведен в листинге 4.2.4.
152
Глава 4
Листинг 4.2.4
...
n=int(input())
F=[[0]*n for i in range(n)]
A=affect(F)
for i in range(n):
for j in range(n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
A=affect(F)
break
show(F)
Результат:
5
Ф0000
00Ф00
0000Ф
0Ф000
000Ф0
Для поля 5×5 программа сработала верно. Как мы помним, для такого размера поля
бэктрекинг не нужен. Запустим программу для поля 6×6:
6
Ф00000
00Ф000
0000Ф0
0Ф0000
000Ф00
000000
И увидим, что программа не смогла разместить последнего ферзя.
Шаг 5. Научимся определять ситуацию, когда нужен бэктрекинг. Для этого введем
флаг. Если во время цикла for ферзя разместить не получилось, то нужно делать
бэктрекинг (листинг 4.2.5).
Листинг 4.2.5
...
n=int(input())
F=[[0]*n for i in range(n)]
A=affect(F)
for i in range(n):
f=True
for j in range(n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
Поиск в длину и ширину, бэктрекинг, динамическое программирование
153
A=affect(F)
f=False
break
if f:
#здесь будет бэктрекинг
Шаг 6. Бэктрекинг будет заключаться в возвращении к предыдущей строке. Поэтому цикл for i заменим на while. В предыдущей строке найдем ферзя и обнулим
клетку, где он находится (листинг 4.2.6.)
Листинг 4.2.6
n=int(input())
F=[[0]*n for i in range(n)]
A=affect(F)
i=0
while i < n:
f=True
for j in range(n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
A=affect(F)
f=False
j0=0
break
if f:
i=i-1
j0=F[i].index("Ф")
F[i][j0]=0
A=affect(F)
i=i+1
show(F)
Шаг 7. Поиск нового места для ферзя, которого мы удалили, должен начинаться со
следующей клетки. Используем j0 как нижнюю границу цикла for j (листинг 4.2.7).
Листинг 4.2.7
...
n=int(input())
F=[[0]*n for i in range(n)]
A=affect(F)
j0=0
i=0
while i < n:
f=True
for j in range(j0,n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
154
Глава 4
A=affect(F)
f=False
j0=0
break
if f:
i=i-1
j0=F[i].index("Ф")
F[i][j0]=0
A=affect(F)
j0=j0+1
continue
i=i+1
show(F)
Результат:
6
0Ф0000
000Ф00
00000Ф
Ф00000
00Ф000
0000Ф0
Шаг 8. Мы получили пока еще не вполне работающую программу. Если мы ее запустим для поля, в котором не существует расположения ферзей, — например, для
поля 3×3, то она зависнет. Сделаем проверку и прерывание цикла, когда i станет
меньше нуля (листинг 4.2.8).
Листинг 4.2.8
...
n=int(input())
F=[[0]*n for i in range(n)]
A=affect(F)
j0=0
i=0
while i < n:
f=True
for j in range(j0,n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
A=affect(F)
f=False
j0=0
break
if f:
i=i-1
if i<0:
break
j0=F[i].index("Ф")
Поиск в длину и ширину, бэктрекинг, динамическое программирование
155
F[i][j0]=0
A=affect(F)
j0=j0+1
continue
i=i+1
show(F)
Результат:
3
000
000
000
Шаг 9. Для удобства поместим весь поиск расположения ферзей в функцию и получим окончательную версию программы (листинг 4.2.9).
Листинг 4.2.9. Расстановка ферзей бэктрекингом
def affect(F):
def f(i,j):
for k in range(len(F)):
A[i][k]="X"
A[k][j]="X"
if i+k<len(F) and j+k<len(F):
A[i+k][j+k]="X"
if i-k>=0 and j-k>=0:
A[i-k][j-k]="X"
if i+k<len(F) and j-k>=0:
A[i+k][j-k]="X"
if i-k>=0 and j+k<len(F):
A[i-k][j+k]="X"
A=[[0]*n for i in range(len(F))]
for i in range(len(F)):
for j in range(len(F)):
if(F[i][j]=="Ф"):
f(i,j)
return A
def show(F):
for s in F:
line="".join([str(el) for el in s])
print(line)
print()
def ferz(n):
F=[[0]*n for i in range(n)]
A=affect(F)
156
Глава 4
j0=0
i=0
while i < n:
f=True
for j in range(j0,n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
A=affect(F)
f=False
j0=0
break
if f:
i=i-1
if i<0:
return -1
j0=F[i].index("Ф")
F[i][j0]=0
A=affect(F)
j0=j0+1
continue
i=i+1
return F
n=int(input())
F=ferz(n)
if F==-1:
print("нет")
else:
show(F)
Результат:
8
Ф0000000
0000Ф000
0000000Ф
00000Ф00
00Ф00000
000000Ф0
0Ф000000
000Ф0000
Версия 2
Напишем программу поиска расположения ферзей рекурсией.
Идея алгоритма
Идея метода заключается в том, что для i-й строчки мы пытаемся расположить
ферзей в каждой удобной для этого клетке и рекурсивно запускаем эту же самую
Поиск в длину и ширину, бэктрекинг, динамическое программирование
Рис. 4.2. Схема вызовов функции для расстановки ферзей
157
158
Глава 4
функцию уже для следующих строк (схема вызовов функции приведена на
рис. 4.2).
Ход программирования
Шаг 1. Воспользуемся функциями вывода доски на экран и построением зоны поражения из предыдущей версии программы.
Шаг 2. Напишем заготовку для рекурсивной функции. Аргументами у нее будут
размер поля, само поле (полученное в предыдущих вызовах этой же функции) и
текущая строка поля (по умолчанию 0). В случае нулевой строки создаем поле
(листинг 4.2.10).
Листинг 4.2.10
...
def ferz(n,F=None,i=0):
if i==0:
F=[[0]*n for i in range(n)]
A=affect(F)
Шаг 3. Будем просматривать текущую строку и, если найдем подходящее место,
расположим там ферзя. Если строчка последняя, то это терминальный случай. Вернем построенное поле (листинг 4.2.11).
Листинг 4.2.11
...
def ferz(n,F=None,i=0):
if i==0:
F=[[0]*n for i in range(n)]
A=affect(F)
for j in range(n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
if i==n-1:
return F
Шаг 4. Если строка не является последней, рекурсивно запустим функцию для следующей строки (листинг 4.2.12).
Листинг 4.2.12
...
def ferz(n,F=None,i=0):
if i==0:
F=[[0]*n for i in range(n)]
A=affect(F)
for j in range(n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
Поиск в длину и ширину, бэктрекинг, динамическое программирование
159
if i==n-1:
return F
else:
r=ferz(n,F,i=i+1)
Шаг 5. Если рекурсивно запущенная функция вернет -1, значит, расположить ферзей не удалось — убираем ферзя из текущей клетки. Иначе — возвращаем получившееся расположение (листинг 4.2.13).
Листинг 4.2.13
...
def ferz(n,F=None,i=0):
if i==0:
F=[[0]*n for i in range(n)]
A=affect(F)
for j in range(n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
if i==n-1:
return F
else:
r=ferz(n,F,i=i+1)
if r==-1:
F[i][j]=0
A=affect(F)
else:
return F
Шаг 6. Наконец, если цикл for j дошел до конца без return, это значит, что в текущей строке не удалось расположить ферзей, — возвращаем -1 (листинг 4.2.14).
Листинг 4.2.14
...
def ferz(n,F=None,i=0):
if i==0:
F=[[0]*n for i in range(n)]
A=affect(F)
for j in range(n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
if i==n-1:
return F
else:
r=ferz(n,F,i=i+1)
if r==-1:
F[i][j]=0
A=affect(F)
160
Глава 4
else:
return F
return -1
Приведем полный код программы (листинг 4.2.15).
Листинг 4.2.15. Расстановка ферзей рекурсией
def affect(F):
def f(i,j):
for k in range(len(F)):
A[i][k]="X"
A[k][j]="X"
if i+k<len(F) and j+k<len(F):
A[i+k][j+k]="X"
if i-k>=0 and j-k>=0:
A[i-k][j-k]="X"
if i+k<len(F) and j-k>=0:
A[i+k][j-k]="X"
if i-k>=0 and j+k<len(F):
A[i-k][j+k]="X"
A=[[0]*n for i in range(len(F))]
for i in range(len(F)):
for j in range(len(F)):
if(F[i][j]=="Ф"):
f(i,j)
return A
def show(F):
for s in F:
line="".join([str(el) for el in s])
print(line)
print()
def ferz(n,F=None,i=0):
if i==0:
F=[[0]*n for i in range(n)]
A=affect(F)
for j in range(n):
if F[i][j]==0 and A[i][j]==0:
F[i][j]="Ф"
if i==n-1:
return F
else:
r=ferz(n,F,i=i+1)
if r==-1:
F[i][j]=0
A=affect(F)
Поиск в длину и ширину, бэктрекинг, динамическое программирование
161
else:
return F
return -1
n=int(input())
F=ferz(n)
if F==-1:
print("нет")
else:
show(F)
4.3. Поиск индекса элемента в списке
Отвлечемся от поиска в длину и ширину и изучим еще один важный тип алгоритма — динамику по подотрезкам.
Задача
Найти индекс элемента по его значению в упорядоченном по возрастанию списке
(если его нет, вернуть -1).
Версия 1
Решим задачу стандартными средствами Python.
Шаг 1. Зададим список и проверим, есть ли элемент в списке. Если его нет, выведем -1. Если есть, найдем его индекс с помощью метода index (листинг 4.3.1).
Листинг 4.3.1
L=[1,2,3,5,8,13,21,34,55]
print(L)
v=21
print(v)
if v not in L:
print(-1)
else:
print(L.index(v))
Результат
[1, 2, 3, 5, 8, 13, 21, 34, 55]
21
6
Шаг 2. Превратим поиск в функцию (листинг 4.3.2).
Листинг 4.3.2
def find(L,v):
if v not in L:
return -1
else:
return L.index(v)
162
Глава 4
L=[1,2,3,5,8,13,21,34,55]
print(L)
v=21
print(find(L,v))
Возникает вопрос, зачем я предлагаю читателю решить нестандартными методами
задачу, которая вполне решается стандартно? Дело в том, что будущий алгоритм
пригодится нам в слегка измененном виде для задачи разд. 4.6.
Версия 2
Решим задачу методом линейного поиска.
Шаг 1. Откажемся от стандартных средств Python и будем просматривать список
по порядку, пока не найдем нужный элемент (листинг 4.3.3).
Листинг 4.3.3
L=[1,2,3,5,8,13,21,34,55]
print(L)
v=21
j=-1
for i in range(len(L)):
if v==L[i]:
j=i
break
print(j)
Шаг 2. Поместим поиск элемента в функцию (листинг 4.3.4).
Листинг 4.3.4. Линейный поиск
def find(L,v):
j=-1
for i in range(len(L)):
if v==L[i]:
j=i
break
return j
L=[1,2,3,5,8,13,21,34,55]
print(L)
v=21
print(find(L,v))
Поиск в длину и ширину, бэктрекинг, динамическое программирование
163
Версия 3
Решим задачу методом бинарного поиска.
Идея алгоритма
Предыдущая версия не учитывает того, что список упорядочен. Поиск с самого начала списка нерационален.
Представьте, что вы играете в игру «Угадай возраст». Вряд ли вы будете задавать
вопросы по порядку:
— Вам 1 год?
— Вам 2 года?
— Вам 3 года?
— Вам 4 года?
— Вам 5 лет?
и т. д.
Это очень долго.
Скорее, вы прикинете средний возраст людей (допустим, 40 лет). И спросите:
— Вы моложе 40 лет?
— Нет.
— Вы моложе 60?
— Да.
— Вы моложе 50 лет?
— Да.
...
Так диапазон поиска будет быстро сокращаться.
Рассмотрим пример списка и числа для поиска:
L=[1,2,3,5,8,13,21,34,55,89,144,233,377]
v=55
Пусть левая граница поиска — это нулевой элемент, а правая — конец списка:
i
0
1
2
3
4
5
6
7
8
9
10
11
12
L
1
2
3
5
8
13
21
34
55
89
144
233
377
Шаг1
left
right
Проверим, не выходит ли 55 за пределы диапазона.
Найдем центральный элемент и посмотрим, больше он, меньше искомого числа 55
или равен ему:
164
Глава 4
i
0
1
2
3
4
5
6
7
8
9
10
11
12
L
1
2
3
5
8
13
21
34
55
89
144
233
377
Шаг1
left
center
21 < 55
right
Видим, что 21 < 55, значит, искать нужно в правой части списка. Суживаем диапазон поиска:
i
0
1
2
3
4
5
6
7
8
9
10
11
12
L
1
2
3
5
8
13
21
34
55
89
144
233
377
Шаг1
left
center
right
Шаг2
left
right
Заглядываем в центр суженного диапазона и видим, что искать нужно левее:
i
0
1
2
3
4
5
6
7
8
9
10
11
12
L
1
2
3
5
8
13
21
34
55
89
144
233
377
Шаг1
left
center
Шаг2
right
left
center
55 < 89
right
Еще сильнее суживаем диапазон:
i
0
1
2
3
4
5
6
7
8
9
10
11
12
L
1
2
3
5
8
13
21
34
55
89
144
233
377
Шаг1
Шаг2
Шаг3
left
center
right
left
center
right
left
right
center
Видно, что правая и левая границы поиска сошлись и мы нашли нужный элемент.
Алгоритм бинарного поиска относится к классу алгоритмов, которые называются
динамика по подотрезкам. В этих задачах имеется список, и большая задача сводится к маленькой задаче на урезанном списке, причем маленькая задача решается
аналогично большой.
Ход программирования
Шаг 1. Задачи на динамику по подотрезкам красивее всего решаются рекурсивной
функцией. Создадим функцию, аргументами которой будут обязательные список L
и число для поиска v, а также необязательные левая и правая границы left и right
(которые будут сужаться при рекурсивном вызове функции). Поскольку первый раз
Поиск в длину и ширину, бэктрекинг, динамическое программирование
165
функция будет запускаться без аргументов left и right, нужно сделать их начальную инициализацию (листинг 4.3.5).
Листинг 4.3.5
def find(L,v,left=0,right=-1):
if right==-1:
right=len(L)-1
...
L=[1,2,3,5,8,13,21,34,55,89,144,233,377]
v=55
print(find(L,v))
Шаг 2. Терминальными случаями, когда мы не нашли число, являются пустой список или выход числа за пределы диапазона (когда оно меньше числа по левой
границе или больше по правой). В этом случае пусть функция возвращает -1 (листинг 4.3.6).
Листинг 4.3.6
def find(L,v,left=0,right=-1):
if right==-1:
right=len(L)-1
if len(L)==0 or v<L[left] or v>L[right]:
return -1
Шаг 3. Еще тремя терминальными случаями являются совпадения значения поиска
со значением в ячейках по левой и правой границам. А если левая и правая границы — это соседние клетки, и число поиска не совпало с их содержимым, то число
не найдено (листинг 4.3.7).
Листинг 4.3.7
def find(L,v,left=0,right=-1):
if right==-1:
right=len(L)-1
if len(L)==0 or v<L[left] or v>L[right]:
return -1
elif v==L[left]:
return left
elif v==L[right]:
return right
elif left==right-1:
return -1
Шаг 4. Найдем центральную ячейку для заданного диапазона left и right и сравним
ее содержимое с искомым числом (листинг 4.3.8).
166
Глава 4
Листинг 4.3.8
def find(L,v,left=0,right=-1):
if right==-1:
right=len(L)-1
if len(L)==0 or v<L[left] or v>L[right]:
return -1
elif v==L[left]:
return left
elif v==L[right]:
return right
elif left==right-1:
return -1
else:
center=(left+right)//2
if v==L[center]:
return center
Шаг 5. Остались только рекурсивные вызовы, когда мы суживаем диапазон, ища
в правой или левой части. Потестируем программу для разных значений v (листинг 4.3.9).
Листинг 4.3.9. Бинарный поиск
def find(L,v,left=0,right=-1):
if right==-1:
right=len(L)-1
if len(L)==0 or v<L[left] or v>L[right]:
return -1
elif v==L[left]:
return left
elif v==L[right]:
return right
elif left==right-1:
return -1
else:
center=(left+right)//2
if v==L[center]:
return center
elif v<L[center]:
return find(L,v,left+1,center-1)
else:
return find(L,v,center+1,right-1)
L=[1,2,3,5,8,13,21,34,55,89,144,233,377]
print(L)
print(21,find(L,21))
print(2,find(L,2))
Поиск в длину и ширину, бэктрекинг, динамическое программирование
167
print(55,find(L,55))
print(90,find(L,90))
print(400,find(L,400))
Результат:
[1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377]
21 6
2 1
55 8
90 -1
400 -1
Нужно заметить, что «пайтоновская» функция index работает быстро. Там явно
используется вовсе не линейный поиск. Поэтому обычно в реализации бинарного
поиска нет необходимости. Но мы используем эту программу в модифицированном
виде для задачи из разд. 4.6.
4.4. Сжатие строки
Задача
Пусть дана строка, которая состоит из одних букв. Нужно ее сжать, найдя в ней закономерность по следующему типу:
Исходная строка:
Сжатая строка:
aaaaa
5a
abcabcabcabc
4(abc)
aaabbbbb
3a5b
aaabbbbbaaabbbbbaaabbbbbaaabbbbb
4(3a5b)
abababdefdefabababdefdefabababdefdef
3(3(ab)2(def))
Языковые конструкции: строки, множества.
Приемы программирования: взаимная рекурсия, динамика по подотрезкам.
Ход программирования
Шаг 1. Начнем с простейшего случая:
s="aaaaa"
Подсчитаем, сколько раз первый символ встречается в строке. Если количество
вхождений равно длине строки, то строка состоит из одного символа. В этом случае
запишем в сжатую строку размер множества и первую букву исходной строки (листинг 4.4.1).
168
Глава 4
Листинг 4.4.1
s="aaaaa"
print(s)
if len(set(s))==1:
r=str(len(s))+s[0]
else:
r=s
print(r)
Для случая:
s="a"
ответ будет:
1a
Сделаем проверку на то, что сжатая строка получилась больше, чем исходная (листинг 4.4.2).
Листинг 4.4.2
s="aaaaa"
print(s)
if s.count(s[0])==len(s):
r=str(len(s))+s[0]
else:
r=s
if len(r)>=len(s):
r=s
print(r)
Шаг 2. Пусть строка состоит из нескольких различных символов, которые повторяются с некоторой периодичностью:
s="abcdef"
Начнем искать в ней период в цикле от 2 до размера строки, проверяя, делится ли
строка на равные части этим периодом (листинг 4.4.3).
Листинг 4.4.3
s="abcdef"
print(s)
if len(set(s))==1:
r=str(len(s))+s[0]
else:
for t in range(2,len(s)):
if len(s)%t==0:
pass
Поиск в длину и ширину, бэктрекинг, динамическое программирование
169
Шаг 3. Проверим, сколько раз срез строки длиной t встречается в исходной строке.
Если он встречается len(s)/t раз, то вся строка состоит из этих повторяющихся
фрагментов (листинг 4.4.4).
Листинг 4.4.4
s="abcabcabcabc"
print(s)
if s.count(s[0])==len(s):
r=str(len(s))+s[0]
else:
r=s
for t in range(2,len(s)):
if len(s)%t==0 and s.count(s[:t])==len(s)/t:
r=str(len(s)//t)+"("+s[:t]+")"
break
if len(r)>=len(s):
r=s
print(r)
Результат:
abcabcabcabc
4(abc)
Шаг 4. Поместим выявление периода и сжатие в отдельную функцию (листинг 4.4.5).
Листинг 4.4.5
def period(s):
if s.count(s[0])==len(s):
r=str(len(s))+s[0]
else:
r=s
for t in range(2,len(s)):
if len(s)%t==0 and s.count(s[:t])==len(s)/t:
r=str(len(s)//t)+"("+s[:t]+")"
break
if len(r)>=len(s):
r=s
return r
s="abcabcabcabc"
print(s)
print(period(s))
Шаг 5. Рассмотрим теперь простейший случай, когда периода в строке нет:
s="aaabbbbb"
170
Глава 4
Видно, что строчку можно разбить на две части, сжать их по отдельности и результаты соединить.
Но как получить нужное оптимальное разбиение?
Напишем функцию, которая для строки делает все возможные разбиения на части,
применяя срезы (листинг 4.4.6).
Листинг 4.4.6
def compress(s):
for i in range(len(s)):
print(s[:i],’,’,s[i:])
s="aaabbbbb"
print(s)
compress(s)
Результат
aaabbbbb
, aaabbbbb
a , aabbbbb
aa , abbbbb
aaa , bbbbb
aaab , bbbb
aaabb , bbb
aaabbb , bb
aaabbbb , b
Шаг 6. Части каждого разбиения сожмем с помощью функции period, а результаты
сохраним в списке (листинг 4.4.7).
Листинг 4.4.7
def period(s):
if len(s)<=1:
r=s
elif s.count(s[0])==len(s):
r=str(len(s))+s[0]
else:
r=s
for t in range(2,len(s)):
if len(s)%t==0 and s.count(s[:t])==len(s)/t:
r=str(len(s)//t)+"("+s[:t]+")"
break
if len(r)>=len(s):
r=s
return r
def compress(s):
L=[]
for i in range(len(s)):
L.append(period(s[:i])+period(s[i:]))
print(L)
s="aaabbbbb"
print(s)
compress(s)
Поиск в длину и ширину, бэктрекинг, динамическое программирование
171
Результат:
aaabbbbb
['aaabbbbb', 'aaabbbbb', '2aabbbbb', '3a5b', 'aaab4b', 'aaabb3b', 'aaabbb2b', 'aaabbbbb']
Заметим, что в функцию period пришлось добавить условие проверки, состоит ли
строка из одного символа или вообще является пустой. Это особый случай — при
нем не работает проверка с обращением к s[0].
Шаг 7. Выберем из списка самую короткую строку и возвратим ее в качестве
результата функции (листинг 4.4.8).
Листинг 4.4.8
def period(s):
if len(s)<=1:
r=s
elif s.count(s[0])==len(s):
r=str(len(s))+s[0]
else:
r=s
for t in range(2,len(s)):
if len(s)%t==0 and s.count(s[:t])==len(s)/t:
r=str(len(s)//t)+"("+s[:t]+")"
break
if len(r)>=len(s):
r=s
return r
def compress(s):
L=[]
for i in range(len(s)):
L.append(period(s[:i])+period(s[i:]))
m=min([len(el) for el in L])
for el in L:
if len(el)==m:
r=el
break
return r
s="aaabbbbb"
print(s)
print(compress(s))
Результат:
aaabbbbb
3a5b
172
Глава 4
Шаг 8. Проверим программу на более сложном примере (листинг 4.4.9).
Листинг 4.4.9
...
s="abababcdecdecde"
print(s)
print(compress(s))
Результат:
abababcdecdecde
3(ab)3(cde)
Шаг 9. Проверим программу на еще более сложном примере (листинг 4.4.10).
Листинг 4.4.10
...
s="aaabbbbbaaabbbbbaaabbbbbaaabbbbb"
print(s)
print(compress(s))
Результат:
aaabbbbbaaabbbbbaaabbbbbaaabbbbb
4(aaabbbbb)
Программа нашла периодичность. Но в самом периоде можно «покопаться» еще и
обнаружить его структуру. Посмотрим на строчку из функции period:
r=str(len(L))+"("+L[0]+")"
Здесь функция в скобки помещает найденный период L[0]. Чтобы найти его внутреннюю структуру, нам нужно применить к L[0] функцию compress:
r=str(len(L))+"("+compress(L[0])+")"
Проверим работу программы — ее текст полностью приведен в листинге 4.4.11.
Листинг 4.4.11
def period(s):
if len(s)<=1:
r=s
elif s.count(s[0])==len(s):
r=str(len(s))+s[0]
else:
r=s
for t in range(2,len(s)):
if len(s)%t==0 and s.count(s[:t])==len(s)/t:
r=str(len(s)//t)+"("+compress(s[:t])+")"
break
if len(r)>=len(s):
r=s
return r
Поиск в длину и ширину, бэктрекинг, динамическое программирование
173
def compress(s):
L=[]
for i in range(len(s)):
L.append(period(s[:i])+period(s[i:]))
m=min([len(el) for el in L])
for el in L:
if len(el)==m:
r=el
break
return r
s="aaabbbbbaaabbbbbaaabbbbbaaabbbbb"
print(s)
print(compress(s))
Результат:
aaabbbbbaaabbbbbaaabbbbbaaabbbbb
4(3a5b)
В этой программе мы имеем дело со взаимной рекурсией: функция compress вызывает period, а функция period — compress. В целом же такой алгоритм относится к динамическому программированию (задача разбивается на подзадачи, которые решаются аналогично большой задаче), а точнее — к динамике по подотрезкам (подзадачи
решаются на фрагментах исходной строки).
Шаг 10. Мы отлаживали функцию compress на строке, состоящей из двух частей,
имеющих разную структуру. Проверим, справится ли она со строкой, состоящей из
трех частей (листинг 4.4.12).
Листинг 4.4.12
...
s="aaaabbbbbcccccc"
print(s)
print(compress(s))
Результат:
aaaabbbbbcccccc
aaaabbbbb6c
Обнаруживаем, что программа сжимает только самый большой фрагмент. Добавим
рекурсивные вызовы compress в вызов периода для двух частей. Вместо:
L.append(period(s[:i])+period(s[i:]))
Напишем:
L.append(compress(period(s[:i]))+ compress(period(s[i:])))
Это позволит дробить строку на большее, чем 2, количество частей.
При запуске обнаружим, что программа выводит ошибку поиска минимума по пустому множеству (один из срезов оказывается пустым). Чтобы исправить эту ошиб-
174
Глава 4
ку, организуем поиск самого короткого сжатия строки без хранения всех сжатий
в списке (листинг 4.4.13).
Листинг 4.4.13
def period(s):
if len(s)<=1:
r=s
elif s.count(s[0])==len(s):
r=str(len(s))+s[0]
else:
r=s
for t in range(2,len(s)):
if len(s)%t==0 and s.count(s[:t])==len(s)/t:
r=str(len(s)//t)+"("+compress(s[:t])+")"
break
if len(r)>=len(s):
r=s
return r
def compress(s):
r=s
for i in range(1,len(s)):
t=compress(period(s[:i]))+compress(period(s[i:]))
if len(t)<len(r):
r=t
return r
s="aaaabbbbbcccccc"
print(s)
print(compress(s))
Результат:
aaaabbbbbcccccc
4a5b6c
Шаг 11. Проверим теперь работоспособность программы на примере, в котором
три фрагмента строки имеют внутреннюю структуру:
s="abababcdecdecdeggggg"
Обнаруживаем, что программа зависла. Дело в том, что мы делаем очень много
одинаковых разбиений строки. Чтобы этого избежать, применим мемоизацию
(кеширование) в словаре (листинг 4.4.14).
Листинг 4.4.14. Сжатие строки
def period(s):
if len(s)<=1:
r=s
Поиск в длину и ширину, бэктрекинг, динамическое программирование
175
elif s.count(s[0])==len(s):
r=str(len(s))+s[0]
else:
r=s
for t in range(2,len(s)):
if len(s)%t==0 and s.count(s[:t])==len(s)/t:
r=str(len(s)//t)+"("+compress(s[:t])+")"
break
if len(r)>=len(s):
r=s
return r
D={}
def compress(s):
if s not in D:
r=s
for i in range(1,len(s)):
t=compress(period(s[:i]))+compress(period(s[i:]))
if len(t)<=len(r):
r=t
D[s]=r
return D[s]
s="abababcdecdecdeggggg"
print(s)
print(compress(s))
Результат:
abababcdecdecdeggggg
3(ab)3(cde)5g
Шаг 12. Мемоизацию (кеширование) можно сделать с помощью стандартного
«пайтоновского» средства — декоратора lru_cache из библиотеки functools (листинг 4.4.15).
Листинг 4.4.15
from functools import lru_cache
def period(s):
if len(s)<=1:
r=s
elif s.count(s[0])==len(s):
r=str(len(s))+s[0]
else:
r=s
for t in range(2,len(s)):
if len(s)%t==0 and s.count(s[:t])==len(s)/t:
r=str(len(s)//t)+"("+compress(s[:t])+")"
break
176
Глава 4
if len(r)>=len(s):
r=s
return r
@lru_cache
def compress(s):
r=s
for i in range(1,len(s)):
t=compress(period(s[:i]))+compress(period(s[i:]))
if len(t)<len(r):
r=t
return r
s="abababcdecdecdeggggg"
print(s)
print(compress(s))
4.5. Укладка рюкзака
Задача
Есть ряд предметов, которые имеют вес и стоимость. Мы можем унести ограниченное по весу множество предметов (поднять рюкзак заданного предельного веса).
Какова максимальная суммарная стоимость предметов, которые мы сможем
унести?
Это знаменитая задача оптимизации, которая имеет много разновидностей и способов решений — точных и приблизительных.
Идея алгоритма
Решим задачу методом динамического программирования. До сих пор мы решали
задачи динамического программирования рекурсией и, возможно, кешированием.
Но динамическое программирование не обязательно сводится к этим приемам.
В общем случае динамическое программирование разбивает задачу на подзадачи,
которые решаются аналогично большой задаче.
Мы будем решать задачу, последовательно исследуя сначала один предмет, потом
добавляя второй, затем третий и т. д.
Пусть М — максимальный вес предметов. Тогда подзадачи — это нахождение решения для меньших предельных весов. Будем последовательно решать задачу для
весов: 1, 2, 3, 4, 5, ...
Ход программирования
Шаг 1. Сделаем начальную инициализацию. Для примера возьмем три предмета
с весами:
m=[5,2,3]
Поиск в длину и ширину, бэктрекинг, динамическое программирование
177
и стоимостью:
v=[3000,2000,1500]
Пусть максимальный вес будет равен:
M=5
Создадим двумерный список, в котором первая координата будет соответствовать
номеру предмета, а вторая — исследованному предельному весу. В ячейках списка
будем хранить максимальную суммарную стоимость. Заполним список нулями:
S=[[0]*(M+1) for i in range(len(m))]
Например, S[2][3] будет хранить информацию о том, какова суммарная ценность
предметов с номерами 0, 1, 2, помещающихся в рюкзаке максимальной грузоподъемностью 3.
Код начальной инициализации приведен в листинге 4.5.1.
Листинг 4.5.1
m=[5,2,3]
v=[3000,2000,1500]
M=5
S=[[0]*(M+1) for i in range(len(m))]
Шаг 2. Организуем циклы перебора предметов j и перебора весов i (листинг 4.5.2).
Листинг 4.5.2
m=[5,2,3]
v=[3000,2000,1500]
M=5
S=[[0]*(M+1) for i in range(len(m))]
for j in range(1,M+1):
for i in range(len(m)):
pass
Шаг 3. Рассмотрим предмет под номером 0. Он имеет вес 5 и стоимость 3000. Очевидно, что он поместится только в рюкзак грузоподъемностью 5. Заполним нулевую строчку списка ценностей:
Вес
0
1
2
3
4
5
Стоимость наполненного рюкзака для предмета 0
0
0
0
0
0
3000
Этому соответствует код из листинга 4.5.3.
Листинг 4.5.3
m=[5,2,3]
v=[3000,2000,1500]
178
Глава 4
M=5
S=[[0]*(M+1) for i in range(len(m))]
for j in range(1,M+1):
for i in range(len(m)):
if i==0:
if m[i]<=j:
S[i][j]=v[i]
Шаг 4. Рассмотрим предмет 1 с весом 2 и стоимостью 2000. Его мы сможем положить в рюкзак с грузоподъемностью больше или равной 2:
Вес
0
1
2
3
4
5
Стоимость наполненного рюкзака для предмета 0
0
0
0
0
0
3000
Стоимость наполненного рюкзака для предмета 1
0
0
2000
2000
2000
???
Дойдя до последней ячейки ряда, мы увидим, что для веса 5 у нас есть выбор — что
положить: предмет 0 или предмет 1. И нам выгоднее положить предмет 0.
Вес
0
1
2
3
4
5
Стоимость наполненного рюкзака
для предмета 0
0
0
0
0
0
3000
Стоимость наполненного рюкзака
для предметов 0, 1
0
0
2000
2000
2000
3000
(2000 < 3000)
Запрограммируем наши рассуждения (листинг 4.5.4).
Листинг 4.5.4
m=[5,2,3]
v=[3000,2000,1500]
M=5
S=[[0]*(M+1) for i in range(len(m))]
for j in range(1,M+1):
for i in range(len(m)):
if i==0:
if m[i]<=j:
S[i][j]=v[i]
else:
if m[i]<=j:
S[i][j]=max(S[i-1][j],v[i])
else:
S[i][j]=S[i-1][j]
Шаг 5. Добавим третий предмет с весом 3 и стоимостью 1500. Он входит в рюкзак,
начиная с веса 3. Но его стоимость меньше, чем стоимость предыдущих предметов:
Поиск в длину и ширину, бэктрекинг, динамическое программирование
179
Вес
0
1
2
3
4
5
Стоимость наполненного
рюкзака для предмета 0
0
0
0
0
0
3000
Стоимость наполненного
рюкзака для предметов 0, 1
0
0
2000
2000
2000
3000
(2000 < 3000)
Стоимость
наполненного рюкзака
для предметов 0, 1, 2
0
0
2000
2000
(1500 < 2000)
2000
(1500 < 2000)
3000
(1500 < 3000)
Неправильное решение задачи начинается с ячейки для веса 5. Сейчас наша программа для текущего веса рюкзака выбирает только один самый ценный предмет
(нет ни одной формулы, содержащей сумму).
Если мы все-таки поместим в рюкзак весом 5 предмет № 2 с весом 3, то у нас еще
останется запас места весом 5 – 3 = 2 (программно вычисляется по формуле j-m[i]).
Заглянем в ячейку предыдущей строки, соответствующей весу 2:
Вес
0
1
2
3
4
5
Стоимость наполненного
рюкзака для предмета 0
0
0
0
0
0
3000
Стоимость наполненного
рюкзака для предметов 0, 1
0
0
2000
2000
2000
3000
(2000 < 3000)
Стоимость
наполненного рюкзака
для предметов 0, 1, 2
0
0
2000
2000
(1500 < 2000)
2000
(1500 < 2000)
3000
(1500 < 3000)
Мы видим, что в рюкзаке грузоподъемностью 2 можно разместить предметы стоимостью 2000 (это предмет № 1). Следовательно, альтернативой предмету № 0 со
стоимостью 3000 могут быть предметы № 1 и 2 с суммарным весом 3 + 2 = 5
и суммарной стоимостью 2000 + 1500 = 3500, что превышает стоимость предмета
№ 0. Дополним формулу:
S[i][j]=max(S[i-1][j],v[i]+S[i-1][j-m[i]])
и мы получим работающую программу (листинг 4.5.5).
Листинг 4.5.5
m=[5,2,3]
v=[3000,2000,1500]
M=5
S=[[0]*(M+1) for i in range(len(m))]
for j in range(1,M+1):
for i in range(len(m)):
if i==0:
if m[i]<=j:
S[i][j]=v[i]
else:
if m[i]<=j:
S[i][j]=max(S[i-1][j],v[i]+S[i-1][j-m[i]])
180
Глава 4
else:
S[i][j]=S[i-1][j]
for s in S:
print(s)
print("Ответ:", S[len(m)-1][M])
Результат:
[0, 0,
[0, 0,
[0, 0,
Ответ:
0, 0, 0, 3000]
2000, 2000, 2000, 3000]
2000, 2000, 2000, 3500]
3500
Шаг 6. Программу можно упростить, если нумерацию предметов вести не с 0, а с 1.
При этом в таблице S останется пустая нулевая строчка (фактически мы добавляем
нулевой предмет с весом, равным 0, и стоимостью, равной 0). Тогда исчезает первый терминальный случай.
Мы получили окончательную версию программы (листинг 4.5.6).
Листинг 4.5.6. Задача укладки рюкзака
m=[0,5,2,3]
v=[0,3000,2000,1500]
M=5
S=[[0]*(M+1) for i in range(len(m))]
for j in range(1,M+1):
for i in range(1,len(m)):
if m[i]<=j:
S[i][j]=max(S[i-1][j],v[i]+S[i-1][j-m[i]])
else:
S[i][j]=S[i-1][j]
for s in S:
print(s)
print("Ответ:", S[len(m)-1][M])
Результат:
[0, 0,
[0, 0,
[0, 0,
[0, 0,
Ответ:
0, 0,
0, 0,
2000,
2000,
3500
0, 0]
0, 3000]
2000, 2000, 3000]
2000, 2000, 3500]
4.6. Подпоследовательность
максимальной длины
Следующую задачу мы решим аж четырьмя способами: бэктрекингом, рекурсией,
динамическим программированием и бинарным поиском. Этим задача и ценна —
читатель может сравнить разные методы решения, а также способы мышления,
с ними связанные.
Поиск в длину и ширину, бэктрекинг, динамическое программирование
181
Задача
Для списка чисел найти возрастающую подпоследовательность наибольшей длины
из его элементов (элементы переставлять местами нельзя, вычеркивать лишние
можно). Например, для списка:
L=[3,1,2,9,4,7,5,6]
возрастающая подпоследовательность наибольшей длины будет:
M=[1,2,4,5,6]
Версия 1
Решим задачу методом бэктрекинга.
Идея алгоритма
Пример исходной последовательности: (3 1 2 9 4 7 5 6), результат (1 2 4 5 6).
1. Для первого элемента ищем числа, которые больше предыдущих: (3 9).
2. Делаем откат на одно число, ищем дальше: (3 4 7).
3. Делаем откат на одно число, ищем дальше: (3 4 5 6).
4. Делаем откат на четыре числа назад, начинаем со второго: (1 2 9).
5. Делаем откат на одно число, ищем дальше: (1 2 4 7).
6. Делаем откат на одно число, ищем дальше: (1 2 4 5 6).
И т. д.
При нахождении последовательности с длиной больше, чем запомненная, запоминаем эту последовательность.
Возможна оптимизация: когда осталось элементов меньше, чем длина запомненной
максимальной последовательности, прерываем поиск.
Приведем пример работы бэктрекинга для нашей последовательности (возрастающие подпоследовательности отмечены подчеркиванием):
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
--------------------(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
(3 1 2 9 4 7 5 6)
182
Глава 4
Ход программирования
Шаг 1. Поместим нулевой элемент в список P. Далее будем просматривать список L
и, найдя элемент больший, чем последний в P, добавим его в P (листинг 4.6.1).
Листинг 4.6.1
L=[3,1,2,9,4,7,5,6]
P=[]
for i in range(len(L)):
if len(P)==0 or P[-1]<L[i]:
P.append(L[i])
print(P)
Результат
[3, 9]
Шаг 2. Видно, что для получения более длинной подпоследовательности нужно 9
заменить на 2. Но мы не знаем сейчас, на каком месте находится 9 (функция index
вернет только первое местоположение числа, числа же теоретически могут повторяться). Поэтому для этой задачи перспективнее хранить не сами числа, а их индексы. Подпоследовательность по сохраненным индексам легко восстанавливается
(листинг 4.6.2).
Листинг 4.6.2
L=[3,1,2,9,4,7,5,6]
I=[]
for i in range(len(L)):
if len(I)==0 or L[I[-1]]<L[i]:
I.append(i)
print(I)
print([L[i] for i in I])
Результат
[0, 3]
[3, 9]
Шаг 3. Адаптируем программу под бэктрекинг. Поскольку индекс цикла будет ходить вперед-назад по списку, заменим цикл for на while. Кроме того, поскольку I —
это будут индексы текущей подпоследовательности, введем новый список M для
хранения подпоследовательности максимальной длины (листинг 4.6.3).
Листинг 4.6.3
from copy import copy
L=[3,1,2,9,4,7,5,6]
I=[]
M=[]
i=0
while i<len(L):
if len(I)==0 or L[I[-1]]<L[i]:
I.append(i)
i=i+1
Поиск в длину и ширину, бэктрекинг, динамическое программирование
183
if i==len(L):
if len(I)>len(M):
M=copy(I)
print([L[i] for i in M])
Обратите внимание, что копирование осуществляется с помощью библиотечной
функции copy, т. к. простое приравнивание списков не создает новый список (фактически мы получаем два имени для одного и того же списка при обычном приравнивании).
Шаг 4. Дойдя до конца списка, организуем удаление последнего элемента из подпоследовательности с продолжением просмотра списка L (листинг 4.6.4).
Листинг 4.6.4
Результат
from copy import copy
L=[3,1,2,9,4,7,5,6]
I=[]
M=[]
i=0
while i<len(L):
if len(I)==0 or L[I[-1]]<L[i]:
I.append(i)
i=i+1
if i==len(L):
if len(I)>len(M):
M=copy(I)
if len(I)>0:
i=I[-1]+1
del I[-1]
print([L[i] for i in M])
[3, 4, 5, 6]
Шаг 5. Мы получили последовательность — хоть и длиннее, но не самую большую. Это связано с тем, что откат производится только на один элемент, а не на
несколько. В правильно написанном бэктрекинге надо условие:
if len(I)>0:
заменить на цикл:
while len(I)>0:
Мы получили правильно работающую программу (листинг 4.6.5).
Листинг 4.6.5. Подпоследовательность максимальной длины,
полученная бэктрекингом
from copy import copy
L=[3,1,2,9,4,7,5,6]
I=[]
Результат
184
Глава 4
M=[]
i=0
while i<len(L):
if len(I)==0 or L[I[-1]]<L[i]:
I.append(i)
i=i+1
if i==len(L):
if len(I)>len(M):
M=copy(I)
while len(I)>0:
i=I[-1]+1
del I[-1]
if i<len(L):
break
print([L[i] for i in M])
[1, 2, 4, 5, 6]
Версия 2
Решим задачу методом рекурсии.
Идея алгоритма
Как и в задачах про лабиринт и восемь ферзей, бэктрекинг можно заменить на
рекурсию.
Рассмотрим предельный случай, когда все числа записаны по возрастанию, — например, последовательность: 1, 2, 3, 4, 5.
Рекурсивный поиск, как и бэктрекинг, рассматривает любые точки старта (рис. 4.3).
Рис. 4.3. Точки старта
Если мы начинаем с 1, то возможные продолжения последовательности: 2, 3, 4 или
5. Если начинаем с 2, то продолжения: 3, 4 или 5 и т. д. (рис. 4.4).
В свою очередь, каждый узел продолжает ветвиться похожим путем. Достроим дерево (рис. 4.5).
Рис. 4.4. Второй уровень поиска
Поиск в длину и ширину, бэктрекинг, динамическое программирование
185
Рис. 4.5. Следующие уровни поиска
Функция для каждой ветки должна возвращать самую длинную подпоследовательность.
Ход программирования
Шаг 1. Создадим рекурсивную функцию (листинг 4.6.6), которая принимает два
аргумента: исходный список и список с возрастающей подпоследовательностью,
сформированной на предыдущих этапах (при первом запуске он пуст).
Листинг 4.6.6
def sequence(L,M=[]):
...
L=[3,1,2,9,4,7,5,6]
print(L)
print(sequence(L))
Шаг 2. Для текущего положения может быть несколько возрастающих подпоследовательностей. Создадим список P, в котором все они будут храниться (листинг 4.6.7).
Листинг 4.6.7
def sequence(L,M=[]):
P=[]
Шаг 3. Просматриваем список L и, если находим элемент больший, чем последний
элемент списка M, добавляем этот больший элемент в список и потом рекурсивно
запускаем нашу функцию для правого среза списка (листинг 4.6.8).
Листинг 4.6.8
def sequence(L,M=[]):
P=[]
for i in range(len(L)):
if len(M)==0 or M[-1]<L[i]:
P.append(sequence(L[i+1:],M+[L[i]]))
186
Глава 4
Шаг 4. Далее анализируем список P. Если он пуст, то возвращаем M, иначе находим
в нем самую длинную подпоследовательность и возвращаем ее.
Мы получили работающую версию программы (листинг 4.6.9).
Листинг 4.6.9. Подпоследовательность максимальной длины бэктрекингом
def sequence(L,M=[]):
P=[]
for i in range(len(L)):
if len(M)==0 or M[-1]<L[i]:
P.append(sequence(L[i+1:],M+[L[i]]))
if len(P)==0:
return M
mp=max([len(p) for p in P])
for p in P:
if mp==len(p):
return p
L=[3,1,2,9,4,7,5,6]
print(L)
print(sequence(L))
Версия 3
Метод бэктрекинга, как и метод рекурсией, работает медленно. Рассмотрим здесь
более быстрый алгоритм и решим задачу методом динамического программирования.
Идея алгоритма
Для каждого числа с номером i будем просматривать все предыдущие числа j. Из
них выберем те, которые меньше текущего числа, и из них в свою очередь выберем
то, которое имеет самую большую длину подпоследовательности l. Для текущего
числа запишем длину подпоследовательности l+1. Чтобы можно было восстановить
подпоследовательность, в списке запомним также номер числа с максимальной
длиной подпоследовательности (предыдущее число в подпоследовательности для
текущего числа).
Например, пусть дана последовательность:
Номер числа i
0
1
2
3
4
5
6
7
Последовательность L
3
1
2
9
4
7
5
6
Создадим еще два списка: длину максимальной подпоследовательности для текущего числа и индекс предыдущего элемента в самой большой подпоследовательности:
Поиск в длину и ширину, бэктрекинг, динамическое программирование
187
Номер числа i
0
1
2
3
4
5
6
7
Последовательность L
3
1
2
9
4
7
5
6
Максимальная длина
подпоследовательности S
Номер предыдущего числа
в подпоследовательности I
Первые два элемента: 3 и 1 — могут быть началом возрастающей подпоследовательности, поэтому предыдущих элементов в подпоследовательности у них нет, а
максимальная длина подпоследовательности — 1 (т. е. они сами):
Номер числа i
0
1
2
3
4
5
6
7
Последовательность L
3
1
2
9
4
7
5
6
Максимальная длина
подпоследовательности S
1
1
Номер предыдущего числа
в подпоследовательности I
-1
-1
Перейдем к элементу 2. До него подходящий элемент возрастающей подпоследовательности — это 1, поэтому длина подпоследовательности для 2 равна 2:
Номер числа i
0
1
2
3
4
5
6
7
Последовательность L
3
1
2
9
4
7
5
6
Максимальная длина
подпоследовательности S
1
1
2
Номер предыдущего числа
в подпоследовательности I
-1
-1
1
Перейдем к элементу 9. Все предыдущие элементы последовательности меньше 9.
Выберем среди них элемент с самой большой длиной подпоследовательности
(т. е. 2):
Номер числа i
0
1
2
3
4
5
6
7
Последовательность L
3
1
2
9
4
7
5
6
Максимальная длина
подпоследовательности S
1
1
2
3
Номер предыдущего числа
в подпоследовательности I
-1
-1
1
2
188
Глава 4
Аналогично для элемента 4:
Номер числа i
0
1
2
3
4
5
6
7
Последовательность L
3
1
2
9
4
7
5
6
Максимальная длина
подпоследовательности S
1
1
2
3
3
Номер предыдущего числа
в подпоследовательности I
-1
-1
1
2
2
Номер числа i
0
1
2
3
4
5
6
7
Последовательность L
3
1
2
9
4
7
5
6
Максимальная длина
подпоследовательности S
1
1
2
3
3
4
4
5
Номер предыдущего числа
в подпоследовательности I
-1
-1
1
2
2
4
4
6
И т. д.:
По приведенным данным видно, что наибольшая длина среди возрастающих подпоследовательностей равна 6. По сохраненным индексам в I найдем остальные
элементы подпоследовательности, сформировав маску вхождения элемента в подпоследовательность M:
Номер числа i
0
1
2
3
4
5
6
7
Последовательность L
3
1
2
9
4
7
5
6
Максимальная длина
подпоследовательности S
1
1
2
3
3
4
4
5
Номер предыдущего числа
в подпоследовательности I
-1
-1
1
2
2
4
4
6
Маска вхождения в максимальную
подпоследовательность M
0
1
1
0
1
0
1
1
Ход программирования
Шаг 1. Создадим все необходимые для алгоритма списки с установленными начальными значениями (листинг 4.6.10).
Листинг 4.6.10
L=[3,1,2,9,4,7,5,6]
I=[-1]*len(L)
S=[1]*len(L)
M=[0]*len(L)
Поиск в длину и ширину, бэктрекинг, динамическое программирование
189
Шаг 2. Для каждого элемента последовательности просмотрим предыдущие элементы, выберем те из них, которые меньше текущего элемента, и сформируем список длин их подпоследовательностей S0 (листинг 4.6.11).
Листинг 4.6.11
...
for i in range(len(L)):
S0=[S[j] for j in range(i) if L[j]<L[i]]
Шаг 3. Для каждого элемента последовательности найдем предыдущий элемент
с самой большой подпоследовательностью и заполним списки I и S для текущего
элемента (листинг 4.6.12).
Листинг 4.6.12
...
for i in range(len(L)):
S0=[S[j] for j in range(i) if L[j]<L[i]]
if len(S0)>0:
s=max(S0)
for j in range(i):
if L[j]<L[i] and S[j]==s:
I[i]=j
S[i]=S[i]+S[j]
Шаг 4. В заполненном списке S находим индекс максимального элемента (листинг 4.6.13).
Листинг 4.6.13
...
s=max(S)
for i in range(len(L)):
if S[i]==s:
break
Шаг 5. Формируем маску M вхождения элемента в максимальную подпоследовательность (листинг 4.6.14).
Листинг 4.6.14
...
for j in range(len(L)):
M[i]=1
if I[i]!=-1:
i=I[i]
else:
break
190
Глава 4
Шаг 6. На основе маски получаем список — искомую максимальную подпоследовательность (листинг 4.6.15).
Листинг 4.6.15
...
P=[L[i] for i in range(len(L)) if M[i]==1]
Мы получили готовую программу. Приведем ее полностью в листинге 4.6.16.
Листинг 4.6.16. Подпоследовательность максимальной длины
методом динамического программирования
L=[3,1,2,9,4,7,5,6]
print(L)
S=[1]*len(L)
M=[0]*len(L)
for i in range(len(L)):
S0=[S[j] for j in range(i) if L[j]<L[i]]
if len(S0)>0:
s=max(S0)
for j in range(i):
if L[j]<L[i] and S[j]==s:
I[i]=j
S[i]=S[i]+S[j]
s=max(S)
for i in range(len(L)):
if S[i]==s:
break
for j in range(len(L)):
M[i]=1
if I[i]!=-1:
i=I[i]
else:
break
P=[L[i] for i in range(len(L)) if M[i]==1]
print(L)
print(I)
print(S)
print(M)
print(P)
Результат:
[3, 1, 2, 9, 4, 7, 5, 6]
[-1, -1, 1, 2, 2, 4, 4, 6]
[1, 1, 2, 3, 3, 4, 4, 5]
[0, 1, 1, 0, 1, 0, 1, 1]
[1, 2, 4, 5, 6]
Поиск в длину и ширину, бэктрекинг, динамическое программирование
191
Версия 4
Можно написать еще более быстрый алгоритм — в стиле динамического программирования с использованием бинарного поиска.
Идея алгоритма
Числа мы будем просматривать по порядку их ввода. Из метода бэктрекинга мы
знаем, что возрастающих последовательностей может быть несколько и они могут
быть разного размера. Предположим, у нас уже сформирована последовательность — скажем, длины 3. Далее в просмотре идут числа по убыванию. Какой элемент лучше выбрать в самую длинную подпоследовательность? Лучше наименьший, потому что после него больше будет шансов добавить в подпоследовательность еще числа. А что если для каждой возможной длины подпоследовательности
мы будем сохранять минимально возможный ее элемент? Тогда, обрабатывая очередной элемент, по списку минимально возможных элементов мы сможем находить возможный номер текущего элемента в большой последовательности. Быстрота алгоритма будет определяться тем, что поиск этого возможного номера в списке
минимально возможных элементов мы станем проводить методом бинарного поиска (в разд. 4.3 мы уже решали эту задачу).
Найдя максимально возможный номер, сохраним его в еще одном вспомогательном
списке, который по индексу привязан к исходному списку. После просмотра исходной последовательности чисел для каждого числа мы будем знать его номер
в потенциальной возрастающей последовательности — номер, сохраненный во
вспомогательном списке. Наибольшее число в этом списке — это длина самой
большой подпоследовательности. Затем по вспомогательному списку можно будет
найти возрастающую подпоследовательность (это потребует дополнительных разъяснений).
Описанный алгоритм иллюстрирует схема, показанная на рис. 4.6. У нас есть три
списка: a — исходная последовательность, I — индекс элемента a[i] в возрастающей подпоследовательности. Эти списки тесно связаны между собой по индексу.
И есть третий список — b, содержащий минимально возможные значения в возрастающей подпоследовательности длины j.
Поясним алгоритм на примере. Пусть дана последовательность, показанная на
рис. 4.7.
При i=0 список b пуст. Записываем a[i]=2 в b[0], а индекс j=0 элемента b[0] записываем в I[0] (рис. 4.8).
При i=1 берем a[i]=6, смотрим последний элемент в b и видим, что b[0] < a[i], т. е. 6
может быть уже не нулевым, а первым элементом в возрастающей подпоследовательности. Следовательно в возрастающей подпоследовательности минимальный
элемент под номером 1 должен быть 6. Дописываем 6 в конец списка b (рис. 4.9).
При i=2 берем a[i]=3, смотрим на список b=[2, 6] и видим, что 3 < 6. Это означает,
что 3 нельзя поставить в конец подпоследовательности, т. к. [2, 6, 3] не будет возрастать. Но 3 > 2, а это означает, что 3 может быть на первом месте (напомню, что
192
Глава 4
i
0
1
2
3
4
5
6
7
Исходная a
последовательность
Индекс в возрастающей
I
подпоследовательности
Ищем место j элемента a[i] в массиве b
методом дихотомии
или дописываем a[i] в конец b
Записываем
j в I[i]
Шаг 1
Минимальный элемент в b
возрастающей подпосл. длины j
j
Шаг 2
0
1
2
3
...
Рис. 4.6. Схема алгоритма
Рис. 4.7. Исходная последовательность
Рис. 4.8. Шаг 1
Поиск в длину и ширину, бэктрекинг, динамическое программирование
193
Рис. 4.9. Шаг 2
счет ведется с нуля) в возрастающей подпоследовательности вместо 6. Итак, у нас
сейчас две возрастающие подпоследовательности: [2, 6] и [2, 3]. Какая из них перспективнее, т. е. с большей вероятностью будет самой длинной возрастающей подпоследовательностью? Конечно, [2, 3]. Напомню, что в b[1] мы сохраняем минимально возможный элемент возрастающей подпоследовательности. Заменим 6 на 3
(рис. 4.10).
Рис. 4.10. Шаг 4
При i=3 берем a[i]=7, смотрим на список b=[2, 3] и видим, что 7 > 3. Это означает,
что 7 может быть вторым элементом возрастающей подпоследовательности (начиная с нулевого элемента). Дописываем 7 в конец b и получаем b=[2, 3, 7]
(рис. 4.11).
При i=4 берем a[i]=1. Очевидно, что 1 может быть только началом возрастающей подпоследовательности. Видим, что 1 меньше любого элемента в списке
b=[2, 3, 7]. Так как в списке b мы храним минимально возможный элемент подпоследовательности длины j, это означает, что вместо 2 нужно записать 1 (рис. 4.12).
При i=5 берем a[i]=8. Видно, что 8 больше последнего элемента в списке b=[1, 3, 7].
Этот случай аналогичен виденному при a[3]=7. То есть 8 вполне может идти на
194
Глава 4
Рис. 4.11. Шаг 5
Рис. 4.12. Шаг 6
третьем месте возрастающей подпоследовательности. Допишем 8 в конец списка:
b=[1, 3, 7, 8] (рис. 4.13).
При i=6 берем a[i]=4. Нужно найти место для 4 в списке b=[1, 3, 7, 8]. 4 > 3 — поэтому в максимальной подпоследовательности ее логично поставить после элемента 3. Но 4 меньше, чем 7. Это означает, что после 7 четверка идти не может, а вот
вместо 7 — может. То есть 4 должна быть на втором месте в возрастающей подпоследовательности вместо 7 (рис. 4.14).
Возникает вопрос: а как найти место в последовательности b? Самый простой способ — просматривать по порядку. Но гораздо интереснее сделать поиск в b методом
бинарного поиска.
Обработаем последний элемент a[7]=5 (рис. 4.15).
Напомню, что это еще не конец. Список b=[1, 3, 4, 5] — это не результат! Он хоть
и возрастает, но 1 ведь идет после 3! Нарушен порядок. Список b — это просто
вспомогательный список, который сыграл свою роль. Теперь о нем можно забыть
и обрабатывать оставшиеся два списка (рис. 4.16).
Поиск в длину и ширину, бэктрекинг, динамическое программирование
Рис. 4.13. Шаг 7
Рис. 4.14. Шаг 8
Рис. 4.15. Шаг 9
195
196
Глава 4
Рис. 4.16. Шаг 10
По списку I видно, под каким номером может быть элемент a в возрастающей подпоследовательности. По максимальному элементу списка I видно, что наибольшая
длина возрастающей подпоследовательности равна 4 (максимальный элемент I —
это 3, но нумерация у нас с 0). Поскольку 3 встречается два раза, в нашем примере
как минимум две наибольшие подпоследовательности. Надо сформировать одну из
них.
Будем просматривать списки с конца и выбирать последовательно элементы с индексом в возрастающей последовательности 3, 2, 1, 0 (рис. 4.17)
Рис. 4.17. Шаг 11
Так мы сформируем самую длинную возрастающую подпоследовательность [2, 3,
4, 5].
Ход программирования
Шаг 1. Адаптируем для нашей задачи функцию линейного поиска числа в списке
(используем ее для поиска места в списке b). У нас возможны три случая (листинг 4.6.17):
1. Если список пуст, то вернем 0.
2. Если в списке есть элементы бо́льшие, чем число для поиска, то вернем номер
первого из этих элементов.
3. Если число для поиска больше, чем все элементы списка, то вернем размер списка (т. е. число надо будет добавить в самый конец списка).
Листинг 4.6.17
def find(L,a):
if len(L)==0:
return 0
f=False
for i in range(len(L)):
if a<=L[i]:
Поиск в длину и ширину, бэктрекинг, динамическое программирование
197
f=True
break
if f:
return i
else:
return len(L)
Шаг 2. Зададим список и проинициализируем вспомогательные списки (листинг 4.6.18).
Листинг 4.6.18
a=[2,6,3,7,1,8,4,5]
I=[0]*len(a)
b=[]
Шаг 3. Просматриваем все элементы последовательности a, находим их место
в списке b и заполняем список индексов I (листинг 4.6.19).
Листинг 4.6.19
...
for i in range(len(a)):
j=find(b,a[i])
if j==len(b):
b.append(a[i])
else:
b[j]=a[i]
I[i]=j
print(a)
print(I)
Шаг 4. Организуем формирование максимальной последовательности, просматривая списки с конца (листинг 4.6.20).
Листинг 4.6.20
j=max(I)
S=[]
for i in range(len(a)-1,-1,-1):
if I[i]==j:
S.append(a[i])
j=j-1
S=S[::-1]
print(S)
Мы получили работающую версию программы. Ее код полностью приведен в листинге 4.6.21.
198
Глава 4
Листинг 4.6.21
def find(L,a):
if len(L)==0:
return 0
f=False
for i in range(len(L)):
if a<=L[i]:
f=True
break
if f:
return i
else:
return len(L)
a=[2,6,3,7,1,8,4,5]
I=[0]*len(a)
b=[]
for i in range(len(a)):
j=find(b,a[i])
if j==len(b):
b.append(a[i])
else:
b[j]=a[i]
I[i]=j
print(a)
print(I)
j=max(I)
S=[]
for i in range(len(a)-1,-1,-1):
if I[i]==j:
S.append(a[i])
j=j-1
S=S[::-1]
print(S)
Шаг 5. Заменим функцию линейного поиска на функцию бинарного поиска —
адаптируем ее под нашу задачу (листинг 4.6.22).
Листинг 4.6.22. Подпоследовательность максимальной длины. Бинарный поиск
def find(L,v,left=0,right=-1):
if right==-1:
right=len(L)-1
if len(L)==0:
return 0
elif v<L[left]:
return left
Поиск в длину и ширину, бэктрекинг, динамическое программирование
199
elif v>L[right]:
return right+1
elif left==right-1 and L[left]<v<L[right]:
return right
else:
center=(left+right)//2
if v<L[center]:
return find(L,v,left,center)
else:
return find(L,v,center,right)
a=[2,6,3,7,1,8,4,5]
I=[0]*len(a)
b=[]
for i in range(len(a)):
j=find(b,a[i])
if j==len(b):
b.append(a[i])
else:
b[j]=a[i]
I[i]=j
print(a)
print(I)
j=max(I)
S=[]
for i in range(len(a)-1,-1,-1):
if I[i]==j:
S.append(a[i])
j=j-1
S=S[::-1]
print(S)
4.7. Палиндром наибольшей длины
Задача
Из текстовой строки сформировать палиндром наибольшей длины путем вычеркивания лишних букв. Палиндромом называется строка, которая читается одинаково
слева направо и справа налево.
Поясню это на примере: пусть дана строка abcabba. Для неё палиндром равен:
abcabaa → abcba.
Идея алгоритма
В книге «Python. 12 уроков для начинающих» эта задача решалась с помощью метода динамики по подотрезкам. Решим здесь эту задачу динамическим программированием с использованием двумерного списка в виде косынки.
200
Глава 4
Для этого создадим двумерный список в виде косынки и расположим буквы исходной строки по предпоследней диагонали (последняя будет иметь вспомогательный
характер), как показано на рис. 4.18.
a
b
c
a
b
a
a
Рис. 4.18. Заготовка двумерного списка
в виде косынки для палиндрома наибольшей длины
В ячейки списка мы станем записывать палиндромы подстроки, на которую опирается эта ячейка. Например, для выделенной на рис. 4.19 ячейки будем искать
палиндром из подстроки, сформированной клетками, как показано на на рис. 4.20.
Порядок обхода клеток показан на рис. 4.21.
Начнем с первой диагонали. Если в правой соседней клетке и в нижней соседней
разные значения, то скопируем правую (рис. 4.22).
Если в правой соседней клетке и в нижней соседней одинаковые значения, то запишем оба символа (рис. 4.23).
На самом деле, в случае равенства, как мы увидим позже, между одинаковыми
символами вставляется еще содержимое из клетки по диагонали (рис. 4.24).
И именно для этого копирования мы сделали пустую диагональ после изначальных
букв (чтобы не было выхода за пределы списка).
Заполним все клетки первого обхода (рис. 4.25).
Поиск в длину и ширину, бэктрекинг, динамическое программирование
a
b
c
a
b
a
a
Рис. 4.19. Выделенная ячейка
a
b
c
a
b
a
a
Рис. 4.20. Палиндром
201
202
Глава 4
6
5
3
4
2
1
a
b
c
a
b
a
a
Рис. 4.21. Порядок обхода клеток косынки
a
a
b
Рис. 4.22. Шаг 1
Рис. 4.23. Шаг 2
Рис. 4.24. Шаг 3
Поиск в длину и ширину, бэктрекинг, динамическое программирование
a
b
c
a
bb
b
203
a
b
c
a
b
b
a
Рис. 4.25. Первый обход косынки
Перейдем ко второй диагонали обхода. Если клетка опирается на разные символы
по краям подстроки, то скопируем наибольшую строку из соседних клеток
(рис. 4.26).
aba
a
b
b
a
a
Рис. 4.26. Шаг 4
Рис. 4.27. Шаг 5
Если символы по краям одинаковые, то копируем и символ из диагональной клетки
(рис. 4.27).
Заполним все клетки второго обхода (рис. 4.28).
Аналогично заполним третью диагональ (рис. 4.29).
204
Глава 4
Рис. 4.28. Второй обход косынки
aba
a
a
bcb
b
b
b
aba
c
c
c
aba
aba
a
a
aa
b
b
aa
a
a
Рис. 4.29. Третий обход косынки
a
Поиск в длину и ширину, бэктрекинг, динамическое программирование
205
Рис. 4.30. Все обходы косынки
Заполнение всех остальных диагоналей показано на (рис. 4.30).
В клетке (0,0) получим ответ.
Ход программирования
Шаг 1. Создадим двумерный список-косынку и заполним его диагональ буквами
исходного слова (листинг 4.7.1).
Листинг 4.7.1
s=input()
n=len(s)
M=[]
for i in range(n+1):
M.append(['']*(n-i+1))
for i in range(n):
M[i][-2]=s[i]
for el in M:
print(el)
206
Глава 4
Результат:
abcabaa
['', '', '', '', '', '', 'a', '']
['', '', '', '', '', 'b', '']
['', '', '', '', 'c', '']
['', '', '', 'a', '']
['', '', 'b', '']
['', 'a', '']
['a', '']
['']
Шаг 2. При нестандартном обходе таблицы важно правильно рассчитывать индексы клеток. Научимся обходить таблицу по диагонали и проверим правильность
обхода, записав в ячейки порядок обработки (листинг 4.7.2).
Листинг 4.7.2
s=input()
n=len(s)
M=[]
for i in range(n+1):
M.append(['']*(n-i+1))
for i in range(n):
M[i][-2]=s[i]
t=1
for i in range(n-2,-1,-1):
for j in range(0,i+1):
M[j][i-j]=t
t=t+1
for el in M:
print(el)
Результат:
[21, 19, 16, 12, 7, 1, 'a', '']
[20, 17, 13, 8, 2, 'b', '']
[18, 14, 9, 3, 'c', '']
[15, 10, 4, 'a', '']
[11, 5, 'b', '']
[6, 'a', '']
['a', '']
['']
Шаг 3. Теперь важно правильно находить граничные клетки подстрок. В каждую
ячейку временно запишем содержимое граничных клеток ее подстроки (листинг 4.7.3).
Листинг 4.7.3
s=input()
n=len(s)
Поиск в длину и ширину, бэктрекинг, динамическое программирование
207
M=[]
for i in range(n+1):
M.append(['']*(n-i+1))
for i in range(n):
M[i][-2]=s[i]
for i in range(n-2,-1,-1):
for j in range(0,i+1):
M[j][i-j]=M[j][-2]+M[j+n-i-1][-2]
for el in M:
print(el)
Результат:
abcabaa
['aa', 'aa', 'ab', 'aa', 'ac', 'ab', 'a', '']
['ba', 'ba', 'bb', 'ba', 'bc', 'b', '']
['ca', 'ca', 'cb', 'ca', 'c', '']
['aa', 'aa', 'ab', 'a', '']
['ba', 'ba', 'b', '']
['aa', 'a', '']
['a', '']
['']
Шаг 4. Выполним проверки на равенство граничных клеток подстрок и запишем
соответствующие вычисления палиндромов. Мы получим готовую программу
(листинг 4.7.4).
Листинг 4.7.4
s=input()
n=len(s)
M=[]
for i in range(n+1):
M.append(['']*(n-i+1))
for i in range(n):
M[i][-2]=s[i]
for i in range(n-2,-1,-1):
for j in range(0,i+1):
if M[j][-2]==M[j+n-i-1][-2]:
M[j][i-j]=M[j][-2]+M[j+1][i-j+1]+M[j+n-i-1][-2]
else:
if len(M[j][i-j+1])>=len(M[j+1][i-j]):
M[j][i-j]=M[j][i-j+1]
else:
M[j][i-j]=M[j+1][i-j]
for el in M:
print(el)
print("Палиндром: ", M[0][0])
208
Глава 4
Результат:
abcabaa
['abcba', 'abcba', 'aba', 'aba', 'a', 'a', 'a', '']
['bcb', 'bcb', 'bcb', 'b', 'b', 'b', '']
['aba', 'aba', 'c', 'c', 'c', '']
['aba', 'aba', 'a', 'a', '']
['aa', 'b', 'b', '']
['aa', 'a', '']
['a', '']
['']
Палиндром: abcba
Если сравнить алгоритмы поиска самого длинного палиндрома из этой книги и из
книги «Python. 12 уроков для начинающих», то будет видно, что алгоритмы динамического программирования для одной и той же задачи очень разнообразны.
4.8. Гиперсфера
До сих пор решаемые нами математические задачи практически не выходили за
пределы двух- и трехмерного пространства. Рассмотрим теперь классическую задачу на многомерное пространство, в которой количество измерений пользователем
не задается и, по крайней мере теоретически, бесконечно.
Определим, что такое гиперсфера в бесконечном евклидовом пространстве.
Окружность — это множество точек, равноудаленных от центральной точки на
плоскости. Для простоты будем считать, что центр находится в начале координат
(рис. 4.31).
Рис. 4.31. Окружность
Поиск в длину и ширину, бэктрекинг, динамическое программирование
209
Подсчитаем по теореме Пифагора величину радиуса окружности:
x2 + y2 = r 2 .
Это же и будет уравнением окружности.
Аналогично для трехмерного случая — сферы:
x2 + y2 + z2 = r 2 .
Мы можем рассмотреть и четырехмерный случай:
x2 + y2 + z2 + t 2 = r 2 .
В общем случае n-мерного евклидова пространства:
n
∑x
i =1
2
i
= r2 .
Здесь xi заменяют по порядку в зависимости от значения i = 1, 2 ,3, 4 координаты x,
y, z, t и т. д.
Есть ли смысл рассматривать гиперсферу в бесконечномерном евклидовом пространстве?
∞
∑x
i =1
2
i
= r2 .
Смысл есть, т. к. можно привести пример точек, у которых будет бесконечное число ненулевых координат, но при этом не бесконечное расстояние от начала координат. Например,
⎛
⎜1,
⎝
1
,
2
1 1
1
⎞
,
,
, ... ⎟ .
4 8 16
⎠
Значения координат представляют собой бесконечную убывающую геометрическую прогрессию, сумма которой сходится к 2. Это значит, что сходится и сумма
квадратов координат (ряд бесконечен, но его сумма конечна).
А как выглядит гиперсфера в одномерном пространстве — на координатной оси?
Это две точки, равноудаленные от центра (рис. 4.32)
Рис. 4.32. Гиперсфера в одномерном пространстве
Задача
Найти все точки с целочисленными значениями координат бесконечномерного
евклидова пространства, находящиеся внутри гиперсферы.
Для двумерного случая достаточно подсчитать точки, как показано на рис. 4.33.
210
Глава 4
Рис. 4.33. Точки с целочисленными координатами
внутри круга
Рис. 4.34. Точки с целочисленными координатами
в первом квадранте
Чтобы не залезать в отрицательные числа, ограничимся первым квадрантом (рис. 4.34).
При решении этой задачи для бесконечномерного случая мы обнаружим, что решений бесконечно много, например:
(4)
(0, 4)
(0,0,4)
(0,0,0,4)
И т. д.
Чтобы уйти от бесконечного числа решений, оставим из приведенного примера
только одну точку — (4), как показано на рис. 4.35.
Рис. 4.35. Оставлена только точка (4)
Поиск в длину и ширину, бэктрекинг, динамическое программирование
211
Ход программирования
Шаг 1. Напишем заготовку. Очевидно, функция должна принимать радиус в качестве аргумента (листинг 4.8.1).
Листинг 4.8.1
def hypersphere(r):
pass
r=int(input())
hypersphere(r)
Шаг 2. Начнем решать задачу с одномерного случая. Пусть r = 4. Тогда нам подходят точки, удовлетворяющие неравенству:
x 2 ≤ r 2 , т. е.
(4), (3), (2), (1).
Запрограммируем этот случай (листинг 4.8.2).
Листинг 4.8.2
Результат
def hypersphere(r):
for i in range(r,0,-1):
print(i)
r=int(input())
hypersphere(r)
4
4
3
2
1
Шаг 3. Добавим второе измерение. Нужно, чтобы выполнялось условие
x2 + y2 ≤ r 2 .
Для каждой найденной на предыдущем этапе точки вычислим
y2 ≤ r2 − x2
и найдем уже двумерные точки (рис. 4.36).
Для трехмерного случая у каждой найденной двумерной точки вычислим
z2 ≤ r 2 − x2 − y2
и найдем эти точки (рис. 4.37).
Аналогично для четырехмерного пространства
(рис. 4.38).
Рис. 4.36. Порождение точек
в двумерном пространстве
212
Глава 4
(4)
(3,2)
(3)
(3,2,1)
(3,2,1,1)
(3,1,2)
(3,1,2,1)
(3,1)
(3,1,1)
(2,3)
(2,3,1)
(2,2,2)
(2)
(2,2)
(2,2,1)
(4)
(3,2)
(3)
(3,1)
(2,3)
(2)
(2,2)
(2,1,3)
(3,2,1)
(3,1,2)
(2,1)
(3,1,1)
(2,3,1)
(2,1,1)
(2,2,1)
(1,3)
(2,1,2)
(1,3,1)
(1,3,2)
(1,3,1)
(2,1,1)
(1,3)
(1,2,3)
(1)
(1,2)
(1,2,2)
(1,2)
(1,2,1)
(1,2,2)
(1,2,1)
(1,1,2)
(1,1,1)
Рис. 4.37. Порождение точек
в трехмерном пространстве
(2,2,2,1)
(2,2,1,2)
(2,2,1,1)
(2,1,3,1)
(2,1,2,2)
(2,1,2,1)
(2,1,1,2)
(1,3,2,1)
(1,3,1,2)
(1,3,1,1)
(1,2,3,1)
(1,2,2,2)
(1,2,2,1)
(1,2,1,2)
(1,2,1,1)
(1,1,3)
(1,1)
(2,2,2,2)
(1,2,1,3)
(1,2,3)
(1)
(2,3,1,1)
(2,1,1,1)
(2,2,2)
(1,3,2)
(3,1,1,1)
(2,1,1,3)
(2,1,3)
(2,1)
(2,1,2)
(3,1,1,2)
(1,1)
(1,1,3) ...
(1,1,2) ...
(1,1,1) ...
Рис. 4.38. Порождение точек
в четырехмерном пространстве
Обнаруживаем, что количество точек стремительно растет, и нам уже не хватает
места. При этом закончилась только одна ветка (2,2,2,2) и есть (4), которая и не
развивалась. Построим фрагмент дерева, чтобы понять, когда заканчиваются некоторые ветки (рис. 4.39).
Поиск в длину и ширину, бэктрекинг, динамическое программирование
213
(4)
(3,2)
(3)
(3,2,1)
(3,2,1,1) (3,2,1,1,1)
(3,1,2)
(3,1,2,1) (3,1,2,1,1)
(3,1)
(3,1,1)
(3,1,1,2) (3,1,1,2,1)
(3,1,1,1)
(2,3)
(2,3,1)
(2,2,2)
(2)
(2,2)
(3,1,1,1,2)
(3,1,1,1,1) (3,1,1,1,1,1) (3,1,1,1,1,1,1) (3,1,1,1,1,1,1,1)
(2,3,1,1) (2,3,1,1,1)
(2,2,2,2)
(2,2,2,1) (2,2,2,1,1) (2,2,2,1,1,1) (2,2,2,1,1,1,1)
(2,2,1,2) (2,2,1,2,1) (2,2,1,2,1,1) (2,2,1,2,1,1,1)
(2,2,1)
(2,2,1,1,2) (2,2,1,1,2,1) (2,2,1,1,2,1,1)
(2,2,1,1)
(2,2,1,1,1)
(2,1,3)
(2,1)
(2,1,2)
(2,2,1,1,1,2) (2,2,1,1,1,2,1)
(2,2,1,1,1,1)
(2,1,3,1) (2,1,3,1,1)
(2,2,1,1,1,1,2)
(2,2,1,1,1,1,1)
...
(2,2,1,1,1,1,1,1,1,1)
(2,1,2,2) (2,1,2,2,1) (2,1,2,2,1,1) (2,1,2,2,1,1,1)
(2,1,2,1,2) (2,1,2,1,2,1) (2,1,2,1,2,1,1)
(2,1,2,1)
(2,1,2,1,1)
(2,1,2,1,1,2) (2,1,2,1,1,2,1)
(2,1,2,1,1,1)
(2,1,2,1,1,1,2)
(2,1,2,1,1,1,1)
...
(2,1,2,1,1,1,1,1,1,1)
(2,1,1,3) (2,1,1,3,1)
(2,1,1,2,2) (2,1,1,2,2,1) (2,1,1,2,2,1,1)
(2,1,1)
(2,1,1,2)
(2,1,1,2,1)
(1) ...
(2,1,1,2,1,2) (2,1,1,2,1,2,1)
(2,1,2,1,1,1)
(2,1,1,1) ...
(2,1,2,1,1,1,2)
(2,1,2,1,1,1,1)
...
(2,1,2,1,1,1,1,1,1,1)
Рис. 4.39. Фрагмент дерева порождения точек в бесконечномерном пространстве
Очевидно, что решение по каждой ветке аналогично общему решению, значит,
применим рекурсию.
Поскольку при добавлении всё новых измерений мы вычисляем:
r 2 − x2 ,
r 2 − x2 − y2 ,
r 2 − x2 − y2 − z2 ,
214
Глава 4
то напишем вариант рекурсивного запуска (листинг 4.8.3).
Листинг 4.8.3
...
def hypersphere(r):
for i in range(r,0,-1):
print(i)
hypersphere(int((r*r-i*i)**0.5))
r=int(input())
hypersphere(r)
Шаг 4. Подумаем об организации вывода на экран. Пока координаты выводятся
в столбик, а точки не отделяются друг от друга. Поскольку «дочерние» точки добавляют дополнительную координату к родительской точке, то будем передавать
в рекурсивные вызовы список уже вычисленных координат (для этого нужно будет
копировать этот список), как показано в листинге 4.8.4.
Листинг 4.8.4
from copy import copy
def hypersphere(r,L=[]):
for i in range(r,0,-1):
L1=copy(L)
L1.append(i)
print(L1)
hypersphere(int((r*r-i*i)**0.5),L1)
r=int(input())
hypersphere(r)
Результат:
3
[3]
[2]
[2,
[2,
[2,
[1]
[1,
[1,
[1,
2]
1]
1, 1]
2]
1]
1, 1]
Шаг 5. Проанализируем результат. Для радиуса 3 программа нашла очень мало точек. В частности, нет точки с координатами [1, 1, 1, 1]. А она должна подходить:
12 + 12 + 12 + 12 = 4 ⋅1 = 2 < 3 .
Поиск в длину и ширину, бэктрекинг, динамическое программирование
215
Причина кроется в том, что мы округляем на каждом этапе корень до целого числа:
int((r*r-i*i)**0.5)
Уберем это округление и добавим округление в границу цикла (листинг 4.8.5).
Листинг 4.8.5
from copy import copy
def hypersphere(r,L=[]):
for i in range(int(r),0,-1):
L1=copy(L)
L1.append(i)
print(L1)
hypersphere((r*r-i*i)**0.5,L1)
r=int(input())
hypersphere(r)
Результат:
3
[3]
[2]
[2,
[2,
[2,
[2,
[2,
[2,
[2,
[1]
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
2]
2,
1]
1,
1,
1,
1,
2]
2,
2,
2,
2,
2,
1]
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1]
2]
1]
1, 1]
1, 1, 1]
2]
1]
1, 1]
1, 1, 1]
1, 1, 1, 1]
2]
2,
2,
2,
1]
1,
1,
1,
1,
1,
1]
1, 1]
1, 1, 1]
2]
2, 1]
2, 1, 1]
1]
1, 2]
216
[1,
[1,
[1,
[1,
[1,
[1,
Глава 4
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
2,
1]
1,
1,
1,
1,
1]
2]
1]
1, 1]
1, 1, 1]
И все-таки не хватает одной точки: [1, 1, 1, 1, 1, 1, 1, 1, 1]!
12 + 12 + 12 + 12 + 12 + 12 + 12 + 12 + 12 = 9 ⋅1 = 3 ≤ 3
Будем вызывать функцию с аргументом, равным не радиусу, а квадрату радиуса
(листинг 4.8.6).
Листинг 4.8.6
from copy import copy
def hypersphere(r,L=[]):
for i in range(int(r**0.5),0,-1):
L1=copy(L)
L1.append(i)
print(L1)
hypersphere(r-i*i,L1)
r=int(input())
hypersphere(r*r)
Результат:
3
[3]
[2]
[2,
[2,
[2,
[2,
[2,
[2,
[2,
[2,
[1]
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
2]
2,
1]
1,
1,
1,
1,
1,
2]
2,
2,
2,
2,
2,
1]
1,
1]
2]
1]
1, 1]
1, 1, 1]
1, 1, 1, 1]
2]
1]
1, 1]
1, 1, 1]
1, 1, 1, 1]
2]
Поиск в длину и ширину, бэктрекинг, динамическое программирование
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
2,
2,
2,
1]
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
217
1]
1, 1]
1, 1, 1]
2]
2,
2,
1]
1,
1,
1,
1,
1,
1,
1,
1,
1]
1, 1]
2]
2,
1]
1,
1,
1,
1,
1,
1]
2]
1]
1, 1]
1, 1, 1]
1, 1, 1, 1]
Шаг 6. Для дальнейшей работы — например, для подсчета количества точек, может понадобиться сохранить координаты этих точек в списке списков M. Создадим
этот список как аргумент, будем передавать его в рекурсивные вызовы и возвращать как результат. Вывод точек вынесем за пределы функции и получим финальную версию программы (листинг 4.8.7).
Листинг 4.8.7. Точки в гиперсфере
from copy import copy
def hypersphere(r,M=[],L=[]):
for i in range(int(r**0.5),0,-1):
L1=copy(L)
L1.append(i)
M.append(L1)
hypersphere(r-i*i,M,L1)
return M
r=int(input())
M=hypersphere(r*r)
for p in M:
print(p)
В этом разделе мы вышли в бесконечномерное пространство, но в программе использовали только одномерный список. В разд. 5.5 мы встретимся со списками
с произвольным числом измерений.
218
Глава 4
ГЛАВА
5
Объектно-ориентированное
программирование
В этой главе мы перейдем к объектно-ориентированному программированию, т. е.
к использованию языковой конструкции «класс».
Но начнем мы с задачи на графах, которую на других языках программирования
решают либо с помощью таблиц (двумерных списков), что весьма неизящно, либо
с помощью классов. А мы по контрасту с другими языками решим ее без того и
другого — только с помощью словарей.
Следующей мы решим задачу о поиске родственников в родословном древе. Здесь
мы напишем полноценный класс со множеством возможностей. Изложение объектно-ориентированного программирования будет идти «с нуля», хотя и лаконично.
Затем наступит очередь задачи о поиске повторяющегося фрагмента в списке. Казалось бы, мы уже решали похожую задачу при сжатии строки (см. разд. 4.4),
и можем прекрасно обойтись без классов. Мы и напишем несколько версий этой
программы без классов. А потом покажем алгоритм, где классы пригодятся.
До сих пор мы имели дело с двумерными списками. Сейчас же мы попробуем обратиться к многомерным спискам. Вы ведь уже познакомились с бесконечномерным
пространством в разд. 4.8? Но там пространство было бесконечномерным, а список — одномерным. А здесь мы встретимся с многомерным списком, количество
измерений которого заранее неизвестно. И самое удобное — поместить его в класс.
А потом решить с его помощью обобщенный вариант задачи поиска максимального
квадрата, заполненного одними нулями (мы ее уже решали двумя способами в этой
книге, а также в книге «Python. 12 уроков для начинающих»). То есть найдем
гиперкуб.
5.1. Графы с помощью словарей
Есть много разновидностей и определений графов. В простейшем случае графом
называется множество вершин, соединенных ребрами (рис. 5.1).
Графы находят много применений. Типичным примером практического их применения являются логистика и транспортные пути. На графах решаются задачи
подсчета стоимости пути, поиска оптимального пути, задачи построения области
достижимости.
Глава 5
y
220
x
Рис. 5.1. Пример графа: A, B, C, D — вершины, x, y, z, v — ребра
В программах графы обычно хранят в виде таблиц смежности или создают для
графовых структур класс.
В Python же есть коллекция, которая хорошо подходит для графов. Это словарь.
Мы решим несколько задач на графы различного вида, постепенно усложняя
модель данных.
Задача 1
Пусть вершины — это города, и задается рекомендуемое время для посещения
каждого города. Пользователь выбирает маршрут, вводя города. Нужно вывести
время путешествия.
Ход программирования
Для этой задачи граф не нужен. Сохраним в словаре названия городов и время посещения, а в цикле сложим время выбранных городов (листинг 5.1.1).
Листинг 5.1.1
V={'A':4,'B':2,'C':1,'D':3}
print(V)
s=input().split()
s=sum([V[i] for i in s])
print(s)
Результат
{'A': 4, 'B': 2, 'C': 1, 'D': 3}
A B C D
10
Задача 2
Усложним задачу. Допустим, мы можем перемещаться между городами не произвольным образом, а только по дорогам. Граф, показанный на рис. 5.1, можно рассматривать как пример дорожной сети.
Пусть вершины — это города, а ребра — дороги. Пользователь выбирает маршрут,
задавая дороги. Надо подсчитать время пребывания в городах.
Ход программирования
Шаг 1. Зададим дороги в виде словаря, в котором ключами будут имена дорог,
а значениями — список вершин, которые соединяет дорога (листинг 5.1.2).
Объектно-ориентированное программирование
221
Листинг 5.1.2
vertex={'A':4,'B':2,'C':1,'D':3}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D']}
Шаг 2. Спросим пользователя о дорогах и составим список списков вершин, которые соединят маршрут (листинг 5.1.3).
Листинг 5.1.3
...
s=input().split()
p1=[graph[i] for i in s]
print(p1)
Для введенного маршрута:
x y v
мы получим последовательность ребер:
[['A', 'B'], ['B', 'C'], ['C', 'D']]
Шаг 3. Соединим список списков в один список (листинг 5.1.4).
Листинг 5.1.4
p2=p1[0]
for i in range(1,len(p1)):
p2=p2+p1[i]
print(p2)
Для нашего примера получим результат:
['A', 'B', 'B', 'C', 'C', 'D']
Шаг 4. Составим последовательность вершин, удалив дубликаты (листинг 5.1.5).
Листинг 5.1.5
path=[p2[0]]
for i in range(1,len(p2)):
if path[-1]!=p2[i]:
path.append(p2[i])
print(path)
Для нашего примера получим:
['A', 'B', 'C', 'D']
Шаг 5. Теперь сложим числовые характеристики вершин, как мы это делали в предыдущей задаче:
s=sum([vertex[i] for i in path])
222
Глава 5
Мы получим программу (листинг 5.1.6).
Листинг 5.1.6
vertex={'A':4,'B':2,'C':1,'D':3}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D']}
print(vertex)
print(graph)
s=input().split()
p1=[graph[i] for i in s]
print(p1)
p2=p1[0]
for i in range(1,len(p1)):
p2=p2+p1[i]
print(p2)
path=[p2[0]]
for i in range(1,len(p2)):
if path[-1]!=p2[i]:
path.append(p2[i])
print(path)
s=sum([vertex[i] for i in path])
print(s)
Результат:
{'A': 4, 'B': 2, 'C': 1, 'D': 3}
{'x': ['A', 'B'], 'y': ['B', 'C'], 'z': ['C', 'A'], 'v': ['C', 'D']}
x y v
[['A', 'B'], ['B', 'C'], ['C', 'D']]
['A', 'B', 'B', 'C', 'C', 'D']
['A', 'B', 'C', 'D']
Шаг 6. Тестируя программу, обнаружим, что она работает плохо для очень многих
путей. Например, если мы идем в обратную сторону. Это связано с тем, что дубликаты вершин не удаляются.
Например, прямой путь:
x y z
[['A', 'B'], ['B', 'C'], ['C', 'A']]
['A', 'B', 'B', 'C', 'C', 'A']
['A', 'B', 'C', 'A']
Обратный путь:
z y x
[['C', 'A'], ['B', 'C'], ['A', 'B']]
['C', 'A', 'B', 'C', 'A', 'B']
Напрашивается решение: создать для каждого пути обратный путь, переставив
вершины в списках (листинг 5.1.7).
Объектно-ориентированное программирование
223
Листинг 5.1.7
vertex={'A':4,'B':2,'C':1,'D':3}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D']}
ng={}
for i in graph:
ng['n'+i]=[graph[i][1],graph[i][0]]
graph={**graph,**ng}
print(vertex)
print(graph)
Обратите внимание, как соединяются два словаря:
graph={**graph,**ng}
В новых версиях Python это можно сделать так:
graph= graph | ng
Теперь мы получим работающую программу (листинг 5.1.8).
Листинг 5.1.8
vertex={'A':4,'B':2,'C':1,'D':3}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D']}
ng={}
for i in graph:
ng['n'+i]=[graph[i][1],graph[i][0]]
graph={**graph,**ng}
print(vertex)
print(graph)
s=input().split()
p1=[graph[i] for i in s]
print(p1)
p2=p1[0]
for i in range(1,len(p1)):
p2=p2+p1[i]
path=[p2[0]]
for i in range(1,len(p2)):
if path[-1]!=p2[i]:
path.append(p2[i])
print(path)
s=sum([vertex[i] for i in path])
print(s)
Результат:
{'A': 4, 'B': 2, 'C': 1, 'D': 3}
{'x': ['A', 'B'], 'y': ['B', 'C'], 'z': ['C', 'A'], 'v': ['C', 'D'], 'nx': ['B', 'A'],
'ny': ['C', 'B'], 'nz': ['A', 'C'], 'nv': ['D', 'C']}
x y v nv z
[['A', 'B'], ['B', 'C'], ['C', 'D'], ['D', 'C'], ['C', 'A']]
['A', 'B', 'C', 'D', 'C', 'A']
15
224
Глава 5
Задача 3
В предыдущей задаче имело значение только время пребывания в городах. Допустим, что переходы по дорогам осуществляются пешком. Тогда общее время путешествия будет складываться из времени пребывания в городах и времени в пути.
Ход программирования
Сделаем новый словарь для хранения длины дорог:
edge={'x':6,'y':7,'z':8,'v':9}
Добавим расстояния для обратных дорог:
ne={}
for i in edge:
ne['n'+i]=edge[i]
Изменим формулу для расчета времени:
r=sum([vertex[i] for i in path])+sum([edge[i] for i in s])
И получим готовую программу (листинг 5.1.9).
Листинг 5.1.9
vertex={'A':4,'B':2,'C':1,'D':3}
edge={'x':6,'y':7,'z':8,'v':9}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D']}
ng={}
for i in graph:
ng['n'+i]=[graph[i][1],graph[i][0]]
graph={**graph,**ng}
ne={}
for i in edge:
ne['n'+i]=edge[i]
edge={**edge,**ne}
print(vertex)
print(edge)
print(graph)
s=input().split()
p1=[graph[i] for i in s]
print(p1)
p2=p1[0]
for i in range(1,len(p1)):
p2=p2+p1[i]
path=[p2[0]]
for i in range(1,len(p2)):
if path[-1]!=p2[i]:
path.append(p2[i])
print(path)
r=sum([vertex[i] for i in path])+sum([edge[i] for i in s])
print(r)
Объектно-ориентированное программирование
225
Результат:
{'A': 4, 'B': 2, 'C': 1, 'D': 3}
{'x': 6, 'y': 7, 'z': 8, 'v': 9, 'nx': 6, 'ny': 7, 'nz': 8, 'nv': 9}
{'x': ['A', 'B'], 'y': ['B', 'C'], 'z': ['C', 'A'], 'v': ['C', 'D'], 'nx': ['B', 'A'],
'ny': ['C', 'B'], 'nz': ['A', 'C'], 'nv': ['D', 'C']}
x y v nv z
[['A', 'B'], ['B', 'C'], ['C', 'D'], ['D', 'C'], ['C', 'A']]
['A', 'B', 'C', 'D', 'C', 'A']
54
Задача 4
Решим задачу подсчета времени для другого вида графа. В предыдущих задачах
для удобства расчетов мы создавали записи в словаре для обратных дорог. И при
этом считали, что по дороге можно ходить в обе стороны. Но это не всегда так. Бывают и односторонние дороги. Или в задаче может идти речь о авиарейсах в одну
сторону. В таком случае мы имеем дело с ориентированным графом. Упростим
задачу — уберем формирование обратных дорог. Кроме того, для простоты программы не будем удалять дубликаты нахождения в городах (листинг 5.1.10).
Также ничто не мешает нам реализовать еще одну разновидность графа, в котором
вершины могут быть связаны несколькими ребрами. Или даже ребро связывает
одну и ту же вершину саму с собой (рис. 5.2).
Рис. 5.2. Ориентированный граф
Листинг 5.1.10
vertex={'A':4,'B':2,'C':1,'D':3}
edge={'x':6,'y':7,'z':8,'v':9,'w':10,'u':11}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D'],'w':['D','C'],
'u':['B','B']}
s=input().split()
r=sum([edge[i]+vertex[graph[i][0]]+vertex[graph[i][1]] for i in s])
print(r)
226
Глава 5
Результат:
x u y v w z
77
Задача 5
Для ориентированных графов приобретает значение задача поиска области достижимости, потому что не из всех пунктов можно попасть во все пункты. Например,
для графа, показанного на рис. 5.2, из пункта А нельзя попасть в пункт E.
Ход программирования
Будем формировать область достижимости во множестве. Поместим во множество
стартовую вершину:
A={a}
В бесконечном цикле, перебирая элементы этого множества, сформируем новое
множество B смежных с А вершин:
while True:
l1=len(A)
for i in A:
B=set([graph[j][1] for j in graph if graph[j][0]==i])
Далее сложим А с B:
A=A|B
Будем проверять размер множества А до и после добавления B. Если размер множества перестал меняться, то прервем бесконечный цикл (листинг 5.1.11).
Листинг 5.1.11
vertex={'A':4,'B':2,'C':1,'D':3,'E':12}
edge={'x':6,'y':7,'z':8,'v':9,'w':10,'u':11,'t':12}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D'],'w':['D','C'],
'u':['B','B'],'t':['E','C']}
a=input()
A={a}
while True:
l1=len(A)
for i in A:
B=set([graph[j][1] for j in graph if graph[j][0]==i])
A=A|B
l2=len(A)
if l1==l2:
break
print(A)
Результат:
A
{'D', 'B', 'C', 'A'}
Объектно-ориентированное программирование
227
Задача 6
В списках, помещенных внутри словаря, до сих пор хранились только по два элемента (начальная и конечная вершины). А что если увеличить количество вершин
в списках? Получится, что ребро будет соединять более двух вершин (может быть,
стоит назвать такое ребро гранью?). Получившуюся структуру уже сложно назвать
графом, но она вполне может найти практическое применение. Добавим к нашему
графу два ребра, обозначенные пунктиром (рис. 5.3).
Рис. 5.3. Многовершинный граф
Построение области достижимости потребует дополнительного цикла (листинг 5.1.12).
Листинг 5.1.12
vertex={'A':4,'B':2,'C':1,'D':3,'E':12}
edge={'x':6,'y':7,'z':8,'v':9,'w':10,'u':11,'t':12}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D'],'w':['D','C'],
'u':['B','B'],'t':['E','C'],'s':['A','B','C'],'r':['C','D','E']}
a=input()
A={a}
while True:
l1=len(A)
for i in A:
B=[]
for j in graph:
if i in graph[j]:
B=B+graph[j]
B=set(B)
A=A|B
228
Глава 5
l2=len(A)
if l1==l2:
break
print(A)
Результат:
A
{'E', 'A', 'C', 'D', 'B'}
Задача 7
Вернемся к задаче времени пути и решим ее для многовершинного графа. Время
будет складываться из суммы характеристик ребер и входящих в них вершин (листинг 5.1.13).
Листинг 5.1.13
vertex={'A':4,'B':2,'C':1,'D':3,'E':12}
edge={'x':6,'y':7,'z':8,'v':9,'w':10,'u':11,'t':12,'s':13,'r':14}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D'],
'w':['D','C'],'u':['B','B'],'t':['E','C'],
's':['A','B','C'],'r':['C','D','E']}
s=input().split()
r=sum([edge[i]+sum([vertex[j] for j in graph[i]]) for i in s])
print(r)
Результат:
s r
50
Заметим, что хранить характеристики ребер и граней в разных словарях нет практического смысла. Поместим их в один словарь element (листинг 5.1.14).
Листинг 5.1.14
element={'A':4,'B':2,'C':1,'D':3,'E':12,'x':6,'y':7,'z':8,'v':9,'w':10,
'u':11,'t':12,'s':13,'r':14}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D'],'w':['D','C'],
'u':['B','B'],'t':['E','C'], 's':['A','B','C'],'r':['C','D','E']}
s=input().split()
r=sum([element[i]+sum([element[j] for j in graph[i]]) for i in s])
print(r)
Задача 8
После объединения вершин и ребер в одном словаре у нас появляется новая возможность добавления в граф ребер, соединяющих не вершины, а ребра! Например,
как показано на рис. 5.4.
Для такой графоподобной структуры важной становится задача выделения уровней:
Объектно-ориентированное программирование
229
1. Вершины в старом смысле (не связывают ничего).
2. Ребра, связывающие вершины.
3. Ребра, связывающие ребра 1-го уровня.
4. Ребра, связывающие ребра 2-го уровня.
И т. д.
Рис. 5.4. Многоуровневый граф
Ход программирования
Сначала найдем вершины и поместим их в нулевой элемент списка:
level=[set([i for i in element if i not in graph])]
Далее в бесконечном цикле будем искать ребра, связывающие элементы предыдущего уровня (листинг 5.1.15).
Листинг 5.1.15
element={'A':4,'B':2,'C':1,'D':3,'E':12,'x':6,'y':7,'z':8,'v':9,'w':10,'u':11,'t':12,
's':13,'r':14,'l':15,'m':16,'n':17}
graph={'x':['A','B'],'y':['B','C'],'z':['C','A'],'v':['C','D'],'w':['D','C'],
'u':['B','B'],'t':['E','C'],'s':['A','B','C'],'r':['C','D','E'],
'l':['x','y'],'m':['y','z'],'n':['z','x']}
level=[set([i for i in element if i not in graph])]
while True:
s=set([j for i in level[-1] for j in graph if i in graph[j]])
if len(s)!=0:
level.append(s)
230
Глава 5
else:
break
print(level)
Результат:
[{'C', 'A', 'D', 'E', 'B'}, {'v', 't', 'x', 'u', 's', 'z', 'w', 'r', 'y'}, {'n', 'l', 'm'}]
В этом разделе мы увидели, что с помощью словарей довольно легко решаются задачи с весьма сложными структурами данных. В следующем разделе мы перейдем
непосредственно к объектно-ориентированному программированию.
5.2. Родословное древо
Здесь мы создадим класс «Человек» с именем и годом рождения и зададим простейшие родственные отношения (мать, отец, ребенок). На основе этих родственных отношений напишем функции, находящие для заданного человека всех
братьев/сестер, предков, потомков, родственников (людей с общими генами).
Языковые конструкции: класс.
Приемы программирования: рекурсивные структуры данных, рекурсивные функции.
Задача 1
Сделаем класс «Человек» с именем и годом рождения, вычислением возраста. Добавим счетчик людей и ввод/вывод на экран.
Ход программирования
Класс «Человек» мы будем создавать постепенно, изучая, что такое объектноориентированное программирование.
Шаг 1. Создадим пустой класс «Человек» и два его экземпляра: Ивана и Марию
(листинг 5.2.1).
Листинг 5.2.1
Результат
class person:
pass
p1=person()
p1.name="Иван"
p1.year=2000
p2=person()
p2.name="Мария"
p2.year=2001
print(p1.name, p1.year)
print(p2.name, p2.year)
Иван 2000
Мария 2001
Объектно-ориентированное программирование
231
Здесь созданы три объекта: класс «Человек» (person) и два его экземпляра: p1 и p2.
Экземпляры класса можно рассматривать как переменные, а классы — как типы
данных наподобие строк, списков, словарей и т. д., только созданные не разработчиками Python, а программистами (пользовательские типы данных).
Названия переменных, которые пишутся через точку, называются свойствами:
p0.name
p0.year
Заметим, что хотя названия свойств и одинаковы, но значения у них разные для
разных экземпляров (Ивана и Марии).
Здесь реализуется принцип объектно-ориентированного программирования абстракция — выделение существенных свойств объекта и опущение несущественных.
Для нашей задачи из всего многообразия человеческих свойств пока нам нужны
только два: имя и год рождения.
Шаг 2. Задавать значения свойств в столбик неудобно, особенно если количество
свойств будет увеличиваться:
p1.name="Иван"
p1.year=2000
Добавим конструктор — функцию создания экземпляра класса (листинг 5.2.2).
Листинг 5.2.2
Результат
class person:
def __init__(self,name="noname",year=0):
self.name=name
self.year=year
p0=person()
p1=person("Иван",2000)
p2=person("Мария",2001)
print(p0.name, p0.year)
print(p1.name, p1.year)
print(p2.name, p2.year)
noname 0
Иван 2000
Мария 2001
Здесь __init__ — это конструктор (инициализатор) — вложенная в класс функция,
которая вызывается при создании объекта. Выполняя строку:
p1=person("Иван",2000)
Python вызывает функцию __init__. Это зарезервированное слово. При создании
экземпляра любого класса Python будет искать в соответствующем классе метод
__init__.
Первый аргумент init — это self. Он используется в конструкторе для того, чтобы
хранить на постоянной основе значения имени человека и год его рождения. Дело
в том, что name и year (второй и третий параметры конструктора) — это аргументы,
232
Глава 5
т. е. переменные, область жизни которых ограничена функцией. Но конструктор
закончит свою работу, а имя и год рождения человека останутся при нем. Глобальными мы эти переменные сделать не можем, т. к. в программе может быть много
людей с теми же именами и годами рождения. Здесь нам на помощь и приходит
self, который позволяет сохранить имена и годы рождения на постоянной основе:
self.name=name
self.year=year
В этом мы можем убедиться, выводя дальше на экран имена и годы рождения,
когда конструктор уже закончит работу:
print(p0.name, p0.year)
print(p1.name, p1.year)
print(p2.name, p2.year)
Заметим, что __init__ имеет три аргумента, а запускается с двумя. Первый аргумент self — это экземпляр класса, который записывается слева от точки.
Можно провести еще такую аналогию. У вас есть имя, но когда вы размышляете
о себе, то используете не имя, а личное местоимение «я», притяжательные местоимения «себя» и т. п. Можете понимать self как такое местоимение. Внутри класса
оно будет использоваться постоянно.
Заметим, что у аргументов конструктора есть значения по умолчанию:
name="noname", year=0
Это приводит к созданию человека:
noname 0
Шаг 3. Выводить информацию о человеке перечислением его свойств в аргументах
print неудобно:
print(p1.name, p1.year)
Хотелось бы вывод организовать так:
print(p1)
Сделаем функцию преобразования экземпляра класса в строчку (листинг 5.2.3) —
для этого также используется еще одно зарезервированное «пайтоновское» слово:
__str__ (обрамленное, как и __init__, двумя символами «земля» — нижним подчеркиванием).
Листинг 5.2.3
class person:
def __init__(self,name="noname",year=0):
self.name=name
self.year=year
def __str__(self):
return self.name+" "+str(self.year)
Результат
Объектно-ориентированное программирование
p0=person()
p1=person("Иван",2000)
p2=person("Мария",2001)
print(p0)
print(p1)
print(p2)
233
noname 0
Иван 2000
Мария 2001
Шаг 4. Научимся вводить данные о человеке с клавиатуры. Для этого внутри класса напишем функцию input (листинг 5.2.4).
Листинг 5.2.4
class person:
def __init__(self,name="noname",year=0):
self.name=name
self.year=year
def __str__(self):
return self.name+" "+str(self.year)
def input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
p1=person()
p1.input()
p2=person()
p2.input()
print(p1)
print(p2)
Результат:
Иван 2000
Мария 2001
Иван 2000
Мария 2001
Шаг 5. Мы уже привыкли к цепочечным вызовам методов:
input().split()
Хочется также вместо:
p1=person()
p1.input()
сделать так:
p1=person().input()
Для этого нужно, чтобы метод input() возвращал сам объект self. Добавим в input
оператор return (листинг 5.2.5).
234
Глава 5
Листинг 5.2.5
class person:
def __init__(self,name="noname",year=0):
self.name=name
self.year=year
def __str__(self):
return self.name+" "+str(self.year)
def input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
return self
p1=person().input()
p2=person().input()
print(p1)
print(p2)
Функции, находящиеся внутри класса, называются методами. Здесь мы имеем дело
со вторым принципом объектно-ориентированного программирования — инкапсуляцией — совместным определением данных (свойств) и методов их обработки.
У объектов есть свойства и методы, а общее их название — атрибуты.
Шаг 6. Напишем еще один метод — функцию вычисления возраста человека. Для
этого нам понадобится глобальная переменная currYear — текущий год (листинг 5.2.6).
Листинг 5.2.6
Результат
currYear=2022
class person:
def __init__(self,name="noname",year=0):
self.name=name
self.year=year
def __str__(self):
return self.name+" "+str(self.year)
def input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
return self
def age(self):
return currYear-self.year
p1=person("Иван",2000)
p2=person("Мария",2001)
print(p1,p1.age())
print(p2,p2.age())
Иван 2000 22
Мария 2001 21
Объектно-ориентированное программирование
235
Шаг 7. В нашем примере экземпляры класса имеют свойства «имя» и «год рождения» и метод «возраст». Но объектами являются не только экземпляры класса, но и
сам класс. А это значит, что у класса «Человек» также могут быть свои свойства и
методы! — т. е. свойства и методы, общие для всех экземпляров класса. Например,
нужно ввести счетчик количества людей. Имена — это индивидуальные свойства
экземпляров класса. А счетчик людей — общий для всех экземпляров. Напишем
счетчик, поместив в класс переменную с и модифицировав конструктор (листинг 5.2.7).
Листинг 5.2.7
currYear=2022
class person:
c=0
def __init__(self,name="noname",year=0):
self.name=name
self.year=year
person.c=person.c+1
def __str__(self):
return self.name+" "+str(self.year)
def input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
return self
def age(self):
return currYear-self.year
print(person.c)
p1=person("Иван",2000)
print(p1,p1.age())
print(person.c)
p2=person("Мария",2001)
print(p2,p2.age())
print(person.c)
Результат:
0
Иван 2000 22
1
Мария 2001 21
2
Обратите внимание: поскольку с — это свойство объекта по имени person, то и
обращение к нему идет через person, а не через self.
Шаг 8. Полезно все экземпляры класса поместить внутрь списка. Например, мы
будем создавать много людей, а выводить на экран их станем разом в конце. Чтобы
не делать много вызовов print, сделаем цикл перебора списка.
236
Глава 5
Если мы будем сохранять всех людей в списке, то счетчик количества людей нам
не потребуется, — мы будем использовать размер списка (листинг 5.2.8).
Листинг 5.2.8
currYear=2022
class person:
L=[]
def __init__(self,name="noname",year=0):
self.name=name
self.year=year
person.L.append(self)
def __str__(self):
return self.name+" "+str(self.year)
def input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
return self
def age(self):
return currYear-self.year
p1=person("Иван",2000)
p2=person("Мария",2001)
for p in person.L:
print(p,p.age())
print(len(person.L))
Результат:
Иван 2000 22
Мария 2001 21
2
Шаг 9. Чтобы в основной части программы много раз не писать person.L, мы можем
подменить класс на список (листинг 5.2.9).
Листинг 5.2.9
currYear=2022
class person:
L=[]
def __init__(self,name="noname",year=0):
self.name=name
self.year=year
person.L.append(self)
def __str__(self):
return self.name+" "+str(self.year)
def input(self):
s=input().split()
Объектно-ориентированное программирование
237
self.name=s[0]
self.year=int(s[1])
return self
def age(self):
return currYear-self.year
p1=person("Иван",2000)
p2=person("Мария",2001)
person=person.L
for p in person:
print(p,p.age())
print(len(person))
При таком решении у нас есть доступ к свойствам и методам элементов списка, но
вот новых людей создать мы уже не сможем, поскольку класс превратился в список.
Шаг 10. В классе сделаем метод вывода всех людей на экран. Это будет метод
именно класса, а не экземпляра класса. В таких методах отсутствует self как аргумент (листинг 5.2.10).
Листинг 5.2.10. Класс «Человек»
currYear=2022
class person:
L=[]
def __init__(self,name="noname",year=0):
self.name=name
self.year=year
person.L.append(self)
def __str__(self):
return self.name+" "+str(self.year)
def input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
return self
def age(self):
return currYear-self.year
def print():
for p in person.L:
print(p)
p1=person("Иван",2000)
p2=person("Мария",2001)
person.print()
print(len(person.L))
Результат
Иван 2000
Мария 2001
2
238
Глава 5
Задача 2
Добавить в класс «Человек» простейшие отношения: мать, отец, ребенок.
Ход программирования
Шаг 1. Добавим к классу «Человек» два свойства: «мать» и «отец». Поскольку они
не всегда заданы, установим значение по умолчанию None. Также добавим вывод
матери и отца в строку (листинг 5.2.11).
Листинг 5.2.11
currYear=2022
class person:
L=[]
def __init__(self,name="noname",year=0,mother=None,father=None):
self.name=name
self.year=year
self.mother=mother
self.father=father
person.L.append(self)
def __str__(self):
s=self.name+" "+str(self.year)
if self.mother!=None:
s=s+", мать: "+self.mother.name
if self.father!=None:
s=s+", отец: "+self.father.name
return s
def input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
return self
def age(self):
return currYear-self.year
def print():
for p in person.L:
print(p)
p1=person("Иван",2000)
p2=person("Мария",2001)
p3=person("Пётр",2021,p2,p1)
person.print()
print(p3.father.name)
Результат:
Иван 2000
Мария 2001
Пётр 2021, мать: Мария, отец: Иван
Иван
Объектно-ориентированное программирование
239
Теперь мы можем добраться до имени отца через:
print(p2.father.name)
Шаг 2. Здесь мы имеем дело с принципом объектно-ориентированного программирования, называемым агрегация, — возможностью создания составных объектов.
Но свойства «мать» и «отец» класса «Человек» также являются экземплярами класса «Человек». Получается, что это — рекурсивная структура данных.
Добавим в родословное древо дедушку (рис. 5.5).
Рис. 5.5. Родословное древо
Через цепочку точек у внука мы можем вывести деда (листинг 5.2.12).
Листинг 5.2.12
...
p0=person("Вова",1980)
p1=person("Иван",2000,father=p0)
p2=person("Мария",2001)
p2=person("Пётр",2021,p2,p1)
print("Люди:")
person.print()
print()
print("Человек:", p2.name)
print("Отец:", p2.father.name)
print("Дед:", p2.father.father.name)
Результат
Люди:
Вова 1980
Иван 2000, отец: Вова
Мария 2001
Пётр 2021, мать: Мария,
отец: Иван
Человек: Пётр
Отец: Иван
Дед: Вова
Обратите внимание, как мы задали отца Ивана:
p1=person("Иван",2000,father=p0)
То, что p0 — отец, прописано в явном виде. Если это убрать:
p1=person("Иван",2000, p0)
то p0 станет матерью. Это связано с особенностью использования значений по
умолчанию. Ведь мать Ивана мы не задали.
Шаг 3. Сейчас мы можем добраться до имени отца и матери через экземпляр класса «Человек». Но вот наоборот, узнать имена детей человека, мы не можем. Преду-
240
Глава 5
смотрим такую возможность, создав в конструкторе список детей. Добавим Надю — сестру Петра (рис. 5.6).
Когда мы создаем нового человека как ребенка, мы пополняем списки детей его
матери и отца (листинг 5.2.13).
Рис. 5.6. Семья растет
Листинг 5.2.13
currYear=2022
class person:
L=[]
def __init__(self,name="noname",year=0,mother=None,father=None):
self.name=name
self.year=year
self.children=[]
self.mother=mother
if mother!=None:
mother.children.append(self)
self.father=father
if father!=None:
father.children.append(self)
person.L.append(self)
def __str__(self):
s=self.name+" "+str(self.year)
if self.mother!=None:
s=s+", мать: "+self.mother.name
if self.father!=None:
s=s+", отец: "+self.father.name
return s
def input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
return self
def age(self):
return currYear-self.year
Объектно-ориентированное программирование
241
def print():
for p in person.L:
print(p)
p0=person("Вова",1980)
p1=person("Иван",2000,father=p0)
p2=person("Мария",2001)
p3=person("Пётр",2021,p2,p1)
p4=person("Надя",2020,p2,p1)
for p in p1.children:
print(p)
Результат:
Пётр 2021, мать: Мария, отец: Иван
Надя 2020, мать: Мария, отец: Иван
У такой реализации есть недостаток. Если мы человеку добавим отца и мать не
в конструкторе, а вне его, он не появится в списках детей этого отца и матери (листинг 5.2.14).
Листинг 5.2.14
...
p0=person("Вова",1980)
p1=person("Иван",2000,father=p0)
p2=person("Мария",2001)
p3=person("Пётр",2021,p2,p1)
p4=person("Надя",2020)
p4.mother=p2
p4.father=p1
for p in p1.children:
print(p)
Результат
Пётр 2021, мать: Мария, отец: Иван
Шаг 3. Объяснение указанного недостатка естественно — список детей пополняется в конструкторе. А нам нужно, чтобы список детей пополнялся при любом приравнивании значений к свойствам объекта. Мы можем вмешаться в процесс инициализации свойств. Для этого предназначен метод __setattr__. Перенесем код,
задающий детей, из конструктора в этот метод (листинг 5.2.15).
Листинг 5.2.15. Класс «Человек» с родителями и детьми
currYear=2022
class person:
L=[]
def __init__(self,name="noname",year=0,mother=None,father=None):
self.name=name
self.year=year
self.children=[]
self.mother=mother
242
Глава 5
def
def
def
def
def
self.father=father
person.L.append(self)
__setattr__(self,attr,value):
self.__dict__[attr] = value
if attr=="mother" and value!=None:
self.mother.children.append(self)
if attr=="father" and value!=None:
self.father.children.append(self)
__str__(self):
s=self.name+" "+str(self.year)
if self.mother!=None:
s=s+", мать: "+self.mother.name
if self.father!=None:
s=s+", отец: "+self.father.name
return s
input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
return self
age(self):
return currYear-self.year
print():
for p in person.L:
print(p)
p0=person("Вова",1980)
p1=person("Иван",2000,father=p0)
p2=person("Мария",2001)
p3=person("Пётр",2021,p2,p1)
p4=person("Надя",2020)
p4.mother=p2
p4.father=p1
for p in p1.children:
print(p)
Результат:
Пётр 2021, мать: Мария, отец: Иван
Надя 2020, мать: Мария, отец: Иван
Теперь всё здесь реализуется корректно.
Поясним, как работает наше решение.
При любом присваивании свойств — например, в конструкторе:
...
__init__(self,name="noname",year=0,mother=None,father=None):
self.name=name
self.year=year
Объектно-ориентированное программирование
243
self.children=[]
self.mother=mother
self.father=father
пять раз, т. е. для каждого присваивания, вызывается метод __setattr__.
Оказывается, у каждого класса есть словарь __dict__, в котором хранятся имена
свойств в виде ключей и их значений. В нашем методе мы сами прописываем помещение свойства в словарь:
def __setattr__(self,attr,value):
self.__dict__[attr] = value
Это происходит в том числе для имени человека.
Для свойств mother и father мы делаем дополнительные действия:
def __setattr__(self,attr,value):
self.__dict__[attr] = value
if attr=="mother" and value!=None:
self.mother.children.append(self)
if attr=="father" and value!=None:
self.father.children.append(self)
Задача 3
Написать методы поиска братьев (сестер), предков, родственников.
Ход программирования
Напишем теперь методы для более сложных родственных отношений.
Шаг 1. Добавим в родословное древо (рис. 5.7) Васю — еще одного брата Петра от
другой матери (Светы).
Рис. 5.7. И так случается
В классе «Человек» напишем метод поиска братьев/сестер (листинг 5.2.16).
Листинг 5.2.16. Братья/сестры
currYear=2022
class person:
...
244
Глава 5
def brothers(self):
R={p for p in self.mother.children if p!=self}
R=R|{p for p in self.father.children if p!=self}
return list(R)
p0=person("Вова",1980)
p1=person("Иван",2000,father=p0)
p2=person("Мария",2001)
p3=person("Пётр",2021,p2,p1)
p4=person("Надя",2020,p2,p1)
p5=person("Света",1999)
p6=person("Вася",2019,p5,p1)
print("Братья/сестры Петра:")
for p in p3.brothers():
print(p)
Результат:
Братья/сестры Петра:
Вася 2019, мать: Света, отец: Иван
Надя 2020, мать: Мария, отец: Иван
Здесь промежуточные результаты мы сохраняем во множестве. Оно позволит нам
избежать дубликатов общих братьев как по отцу, так и по матери.
Подобным образом можно запрограммировать поиск множества других родственных отношений: дяди, двоюродных братьев и т. п. Мы же перейдем к более сложным родственным отношениям.
Шаг 2. Для человека вывести его предков: родителей, бабушек и дедушек, прабабушек и прадедушек и т. д.
Добавим в родословное древо бабушку Петра Катю и прадедушку с прабабушкой — Пахома и Любу (рис. 5.8).
Функция будет работать рекурсивно — мы добавляем в список предков не только
родителей, но и предков родителей (листинг 5.2.17).
Пахом
Вова
Света
Люба
Катя
Иван
Вася
Мария
Пётр
Надя
Рис. 5.8. Дополненное родословное древо
Объектно-ориентированное программирование
245
Листинг 5.2.17. Предки
currYear=2022
class person:
...
def ancestors(self):
R=[]
if self.mother!=None:
R.append(self.mother)
R=R+self.mother.ancestors()
if self.father!=None:
R.append(self.father)
R=R+self.father.ancestors()
return list(set(R))
p7=person("Пахом",1958)
p8=person("Люба",1959)
p9=person("Катя",1981,p8,p7)
p0=person("Вова",1980)
p1=person("Иван",2000,p9,p0)
p2=person("Мария",2001)
p3=person("Пётр",2021,p2,p1)
p4=person("Надя",2020,p2,p1)
p5=person("Света",1999)
p6=person("Вася",2019,p5,p1)
print("Предки Петра:")
for p in p3.ancestors():
print(p)
Результат:
Предки Петра:
Мария 2001
Иван 2000, мать: Катя, отец: Вова
Катя 1981, мать: Люба, отец: Пахом
Люба 1959
Пахом 1958
Вова 1980
В конце мы делаем:
return list(set(R))
чтобы избежать дублирования в том случае, если один и тот же человек является
предком по нескольким линиям родства.
Шаг 4. Найдем всех потомков человека. Этот метод похож на поиск предков. Он
также рекурсивно добавляет в список не только детей, но и потомков детей (листинг 5.2.18).
246
Глава 5
Листинг 5.2.18. Потомки
currYear=2022
class person:
...
def descendants(self):
R=self.children
for p in self.children:
R=R+p.descendants()
return list(set(R))
p7=person("Пахом",1958)
p8=person("Люба",1959)
p9=person("Катя",1981,p8,p7)
p0=person("Вова",1980)
p1=person("Иван",2000,p9,p0)
p2=person("Мария",2001)
p3=person("Пётр",2021,p2,p1)
p4=person("Надя",2020,p2,p1)
p5=person("Света",1999)
p6=person("Вася",2019,p5,p1)
print("Потомки Пахома:")
for p in p7.descendants():
print(p)
Результат:
Потомки Пахома:
Вася 2019, мать:
Катя 1981, мать:
Иван 2000, мать:
Пётр 2021, мать:
Надя 2020, мать:
Света, отец: Иван
Люба, отец: Пахом
Катя, отец: Вова
Мария, отец: Иван
Мария, отец: Иван
Шаг 5. Выведем для человека его кровных родственников, добавив Ивану брата
Захара с женой Дуней и дочерью Лидой (рис. 5.9).
Пахом
Вова
Захар
Дуня
Лида
Люба
Катя
Света
Иван
Вася
Пётр
Мария
Надя
Рис. 5.9. Учитываем в родословном древе кровных родственников
Объектно-ориентированное программирование
247
Родственники — это люди с общими генами. Для этого найдем всех предков заданного человека и выведем всех потомков этих предков (листинг 5.2.19).
Листинг 5.2.19. Родственники
currYear=2022
class person:
...
def relatives(self):
A=self.ancestors()
B=[]
for p in A:
B=B+p.descendants()
return list(set(A+B))
p7=person("Пахом",1958)
p8=person("Люба",1959)
p9=person("Катя",1981,p8,p7)
p0=person("Вова",1980)
p1=person("Иван",2000,p9,p0)
p2=person("Мария",2001)
p3=person("Пётр",2021,p2,p1)
p4=person("Надя",2020,p2,p1)
p5=person("Света",1999)
p6=person("Вася",2019,p5,p1)
p10=person("Захар",1990,p9,p0)
p11=person("Дуня",1992)
p12=person("Лида",1990,p11,p10)
Результат
Родственники Ивана:
Вася 2019, мать: Света, отец: Иван
Захар 1990, мать: Катя, отец: Вова
Люба 1959
Лида 1990, мать: Дуня, отец: Захар
Катя 1981, мать: Люба, отец: Пахом
Вова 1980
Иван 2000, мать: Катя, отец: Вова
Пётр 2021, мать: Мария, отец: Иван
Надя 2020, мать: Мария, отец: Иван
Пахом 1958
print("Братья Петра:")
for p in p3.brothers():
print(p)
print("Предки Петра:")
for p in p3.ancestors():
print(p)
print("Потомки Пахома:")
for p in p7.descendants():
print(p)
print("Родственники Ивана:")
for p in p1.relatives():
print(p)
Шаг 6. Помимо родства есть еще понятие свойства. Свояки — это люди, у которых
общих генов нет, но их связывает цепочка общих родственников (жена, тесть, свекровь, шурин, деверь и т. д.).
Добавим в родословное древо Игоря и Веру — родителей Марии, жены Ивана, и их
сына Лёшу, а также Юлю и Руслана — не связанных никак с основным древом друзей семьи (рис. 5.10).
248
Глава 5
Рис. 5.10. Учитываем в родословном древе свояков и друзей
Вначале найдем родственников заданного человека, потом родственников его родственников, и т. д. в бесконечном цикле. Поиск будет осуществляться до тех пор,
пока будут находиться новые люди. Затем выведем всех, кроме кровных родственников (листинг 5.2.20).
Листинг 5.2.20. Свояки
currYear=2022
class person:
...
def inlaw(self):
R=self.relatives()
while True:
f=True
A=[]
for p in R:
A=A+p.relatives()
for a in A:
if a not in R:
R.append(a)
f=False
if f:
break
return list(set(R)-set(self.relatives()))
p13=person("Игорь",1970)
p14=person("Вера",1975)
p15=person("Лёша",1995,p14,p13)
p7=person("Пахом",1958)
p8=person("Люба",1959)
p9=person("Катя",1981,p8,p7)
p0=person("Вова",1980)
p1=person("Иван",2000,p9,p0)
p2=person("Мария",2001,p14,p13)
Объектно-ориентированное программирование
249
p3=person("Пётр",2021,p2,p1)
p4=person("Надя",2020,p2,p1)
p5=person("Света",1999)
p6=person("Вася",2019,p5,p1)
p10=person("Захар",1990,p9,p0)
p11=person("Дуня",1992)
p12=person("Лида",1990,p11,p10)
p16=person("Юля",1998)
p17=person("Руслан",2022,p16)
print("Свояки Ивана:")
for p in p1.inlaw():
print(p)
Результат:
Свояки Ивана:
Игорь 1970
Света 1999
Вера 1975
Лёша 1995, мать: Вера, отец: Игорь
Дуня 1992
Мария 2001, мать: Вера, отец: Игорь
Мы закончили программу с родословным древом. Ее полный код приведен в листинге 5.2.21.
Листинг 5.2.21. Родственные отношения
currYear=2022
class person:
L=[]
def __init__(self,name="noname",year=0,mother=None,father=None):
self.name=name
self.year=year
self.children=[]
self.mother=mother
self.father=father
person.L.append(self)
def __setattr__(self,attr,value):
self.__dict__[attr] = value
if attr=="mother" and value!=None:
self.mother.children.append(self)
if attr=="father" and value!=None:
self.father.children.append(self)
def __str__(self):
s=self.name+" "+str(self.year)
if self.mother!=None:
s=s+", мать: "+self.mother.name
250
Глава 5
def
def
def
def
def
def
def
def
if self.father!=None:
s=s+", отец: "+self.father.name
return s
input(self):
s=input().split()
self.name=s[0]
self.year=int(s[1])
return self
age(self):
return currYear-self.year
print():
for p in person.L:
print(p)
brothers(self):
R={p for p in self.mother.children if p!=self}
R=R|{p for p in self.father.children if p!=self}
return list(R)
ancestors(self):
R=[]
if self.mother!=None:
R.append(self.mother)
R=R+self.mother.ancestors()
if self.father!=None:
R.append(self.father)
R=R+self.father.ancestors()
return list(set(R))
descendants(self):
R=self.children
for p in self.children:
R=R+p.descendants()
return list(set(R))
relatives(self):
A=self.ancestors()
B=[]
for p in A:
B=B+p.descendants()
return list(set(A+B))
inlaw(self):
R=self.relatives()
while True:
f=True
A=[]
for p in R:
A=A+p.relatives()
for a in A:
if a not in R:
R.append(a)
f=False
Объектно-ориентированное программирование
251
if f:
break
return list(set(R)-set(self.relatives()))
p13=person("Игорь",1970)
p14=person("Вера",1975)
p15=person("Лёша",1995,p14,p13)
p7=person("Пахом",1958)
p8=person("Люба",1959)
p9=person("Катя",1981,p8,p7)
p0=person("Вова",1980)
p1=person("Иван",2000,p9,p0)
p2=person("Мария",2001,p14,p13)
p3=person("Пётр",2021,p2,p1)
p4=person("Надя",2020,p2,p1)
p5=person("Света",1999)
p6=person("Вася",2019,p5,p1)
p10=person("Захар",1990,p9,p0)
p11=person("Дуня",1992)
p12=person("Лида",1990,p11,p10)
p16=person("Юля",1998)
p17=person("Руслан",2022,p16)
print("Братья/сестры Петра:")
for p in p3.brothers():
print(p)
print("Предки Петра:")
for p in p3.ancestors():
print(p)
print("Потомки Пахома:")
for p in p7.descendants():
print(p)
print("Родственники Ивана:")
for p in p1.relatives():
print(p)
print("Свояки Ивана:")
for p in p1.inlaw():
print(p)
Результат:
Братья/сестры Петра:
Вася 2019, мать: Света, отец: Иван
Надя 2020, мать: Мария, отец: Иван
Предки Петра:
Игорь 1970
Вера 1975
Пахом 1958
Люба 1959
Катя 1981, мать: Люба, отец: Пахом
Вова 1980
Иван 2000, мать: Катя, отец: Вова
Мария 2001, мать: Вера, отец: Игорь
Потомки Пахома:
Пётр 2021, мать: Мария, отец: Иван
Надя 2020, мать: Мария, отец: Иван
Вася 2019, мать: Света, отец: Иван
Захар 1990, мать: Катя, отец: Вова
Лида 1990, мать: Дуня, отец: Захар
Катя 1981, мать: Люба, отец: Пахом
Иван 2000, мать: Катя, отец: Вова
Родственники Ивана:
Пётр 2021, мать: Мария, отец: Иван
Надя 2020, мать: Мария, отец: Иван
Вася 2019, мать: Света, отец: Иван
Захар 1990, мать: Катя, отец: Вова
Пахом 1958
Люба 1959
Лида 1990, мать: Дуня, отец: Захар
Катя 1981, мать: Люба, отец: Пахом
Вова 1980
Иван 2000, мать: Катя, отец: Вова
Свояки Ивана:
Игорь 1970
Света 1999
Вера 1975
Лёша 1995, мать: Вера, отец: Игорь
Дуня 1992
Мария 2001, мать: Вера, отец: Игорь
252
Глава 5
5.3. Период в числовой последовательности
Задача
Вводится количество чисел, затем вводятся сами числа. Нужно определить наименьшую длину периода в этой последовательности.
Пример: у последовательности 1 2 1 3 1 2 1 3 1 2 1 3 длина периода равна 4 (последний период может быть неполным).
Решим эту задачу тремя способами.
Версия 1
Языковые конструкции: список, вложенные циклы.
Ход программирования
Шаг 1. Идея алгоритма заключается в том, что мы перебираем потенциально возможные периоды длиной 1, 2, 3, ... и далее проверяем, является ли начальный отрезок такой длины периодом. Создадим цикл для изменения длины периода (листинг 5.3.1).
Листинг 5.3.1
L=[int(el) for el in input().split()]
for t in range(1,len(L)):
pass
Шаг 2. Для проверки переберем все числа в начальном фрагменте, длина которого
равна периоду. Например, при t=3 надо перебрать L[0], L[1], L[2] (листинг 5.3.2).
Листинг 5.3.2
L=[int(el) for el in input().split()]
for t in range(1,len(L)):
for i in range(t):
pass
Шаг 3. Каждое число из начального периода надо сравнить со всеми числами из
других периодов на соответствующих местах. Например, при t=3 нам нужно сравнить (листинг 5.3.3):
L[0]==L[3]==L[6]==L[9]==...
L[1]==L[4]==L[7]==L[10]==...
L[2]==L[5]==L[6]==L[11]==...
Листинг 5.3.3
L=[int(el) for el in input().split()]
for t in range(1,len(L)):
Объектно-ориентированное программирование
253
for i in range(t):
for j in range(i+t,len(L),t):
pass
Шаг 4. Чтобы считать t длиной периода, надо, чтобы все равенства выполнялись.
Применим прием программирования «флаг». Перед циклом for i установим флаг f
истинным. Если внутри цикла найдутся неравные числа, сделаем флаг f ложным.
Если к концу двух циклов он остался истинным, значит, мы нашли период (листинг 5.3.4).
Листинг 5.3.4. Период с помощью трех вложенных циклов
L=[int(el) for el in input().split()]
for t in range(1,len(L)):
f=True
for i in range(t):
for j in range(i+t,len(L),t):
if L[i]!=L[j]:
f=False
break
if f==False:
break
if f:
break
print(L[:t])
print(t)
Результат:
1 2 1 3 1 2 1 3 1 2 1 3
[1, 2, 1, 3]
4
Сложность этой версии для новичков заключается в том, что трудно догадаться,
что здесь организована «матрешка» из трех циклов.
Версия 2
Языковые конструкции: список, срезы с шагом, множества.
Мы можем избавиться от цикла j во вложенной версии, используя срезы исходного
цикла с шагом, равным длине периода, — например, как показано в листинг 5.3.5.
Листинг 5.3.5
L=[1,2,1,3,1,2,1,3,1,2,1,3]
print(L)
print(L[0::4])
print(L[1::4])
print(L[2::4])
print(L[3::4])
Результат
[1,2,1,3,1,2,1,3,1,2,1,3]
[1, 1, 1]
[2, 2, 2]
[1, 1, 1]
[3, 3, 3]
254
Глава 5
Если в каждом срезе все числа равны, то длина множества, полученного из среза,
будет равна 1 (листинг 5.3.6).
Листинг 5.3.6. Период с помощью срезов и множеств
L=[int(el) for el in input().split()]
for t in range(1,len(L)):
f=True
for i in range(t):
if len(set(L[i::t]))>1:
f=False
break
if f:
break
print(L[:t])
print(t)
Версия 3
Языковые конструкции: список списков, хешируемый список, множество.
Ход программирования
Шаг 1. Идея алгоритма заключается в том, что мы также используем срезы, но на
этот раз срезы без шага. Мы превратим список в список списков, порезав исходный
список на подсписки, длина которых равна предполагаемому периоду (листинг 5.3.7).
Листинг 5.3.7
L=[int(el) for el in input().split()]
for t in range(1,len(L)+1):
M=[]
L1=L
while len(L1)>t:
M.append(L1[:t])
L1=L1[t:]
M.append(L1)
print(M)
Результат:
1 2 3 1 2 3 1 2 3
[[1], [2], [3], [1], [2], [3], [1], [2], [3]]
[[1, 2], [3, 1], [2, 3], [1, 2], [3]]
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
[[1, 2, 3, 1], [2, 3, 1, 2], [3]]
[[1, 2, 3, 1, 2], [3, 1, 2, 3]]
[[1, 2, 3, 1, 2, 3], [1, 2, 3]]
[[1, 2, 3, 1, 2, 3, 1], [2, 3]]
[[1, 2, 3, 1, 2, 3, 1, 2], [3]]
[[1, 2, 3, 1, 2, 3, 1, 2, 3]]
Объектно-ориентированное программирование
255
Шаг 2. Списки мы можем сравнивать. Сделаем цикл, в котором текущий период
сравним с начальным, учтя при этом, что последний период может быть неполным
(листинг 5.3.8).
Листинг 5.3.8
L=[int(el) for el in input().split()]
for t in range(1,len(L)+1):
M=[]
L1=L
while len(L1)>t:
M.append(L1[:t])
L1=L1[t:]
M.append(L1)
f=True
for el in M:
if el!=M[0]:
if len(el)==len(M[0]) or M[0][:len(el)]!=el:
f=False
break
if f:
print(M[0])
break
Шаг 3. Аналогично предыдущей версии программы (со срезами), хочется применить set, удалив списки-дубликаты. Например:
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]
преобразовать в:
[[1, 2, 3]]
Проблема в том, что прямое использование set невозможно, т. к. список — нехешируемый тип. Но мы можем написать класс «Хешируемый список» и использовать его (листинг 5.3.9).
Листинг 5.3.9. Период с помощью хешируемых списков и множеств
class hlist:
maxh = 10
def __init__(self, a = []):
self.a = a
def __eq__(self, other):
return (self.a == other.a)
def __hash__(self):
s = 0
for i in range(len(self.a)):
s=s+self.a[i]*(self.maxh**i)
return s
256
Глава 5
def tolist(self):
return self.a
L=[int(el) for el in input().split()]
for t in range(1,len(L)+1):
M=[]
L1=L
while len(L1)>t:
M.append(hlist(L1[:t]))
L1=L1[t:]
M.append(hlist(L1))
M=set(M)
P=[i.tolist() for i in M]
if len(P)==1 or len(P)==2 and P[0][:len(P[1])]==P[1]:
print(P[0])
break
П РИМЕЧАНИЕ К КОДУ
Чтобы убирать дубликаты списков из множества, нам потребовалось на основе списка
сделать хешируемый тип. Для этого мы написали тривиальный метод сравнения списков, имеющий зарезервированное имя __eq__, и хеш-функцию с зарезервированным
именем __hash__. Эта функция для каждого экземпляра класса вычисляет уникальное
числовое значение (поскольку списки с разным содержимым при запуске хеш-функции
выдают разный результат). И именно это различие в числовых значениях использует
Python для удаления дубликатов. Экземпляры хешируемого типа могут также использоваться как ключи в словаре. Например, когда мы в качестве ключа брали текст,
Python на основе этого текста с помощью встроенной хеш-функции вычислял числовые значения и уже их задействовал как индекс в словаре.
Шаг 4. Класс hlist — это всего лишь обертка над встроенным классом списков list
с дополнительным методом. Нам при использовании hlist могут потребоваться все
методы, которые есть в list. В этом случае лучше задействовать не агрегацию, а
другой принцип объектно-ориентированного программирования — наследование.
Тогда экземпляры класса-наследника будут учитывать все свойства и методы родительского класса. Родительские классы указываются в скобках после имени класса:
class имя_класса (родитель1, родитель2,...)
После переделки программы методы __init__, __eq__ и tolist становятся ненужными, а программа — более лаконичной (листинг 5.3.10).
Листинг 5.3.10. Период с помощью хешируемых списков и множеств (уточнение)
class hlist(list):
maxh = 10
def __hash__(self):
s = 0
for i in range(len(self)):
s=s+self[i]*(self.maxh**i)
return s
Объектно-ориентированное программирование
257
L=[int(el) for el in input().split()]
for t in range(1,len(L)+1):
M=[]
L1=L
while len(L1)>t:
M.append(hlist(L1[:t]))
L1=L1[t:]
M.append(hlist(L1))
P=list(set(M))
if len(P)==1 or len(P)==2 and P[0][:len(P[1])]==P[1]:
print(P[0])
break
Период в числовой последовательности — это хороший пример того, как задача
может решаться множеством способов:
1. Мы использовали минимальный инструментарий Python в программе с тремя
вложенными циклами.
2. Задействовали хорошую возможность Python — срезы.
3. Применили разбиение списка на подсписки.
4. Настроили список так, чтобы к нему была применима функция создания множества set.
Последняя версия показывает, что объектно-ориентированное программирование
бывает удобно и в простых математических задачах.
5.4. Треугольник Паскаля и сочетания
В этом разделе мы решим две взаимосвязанные задачи: построения треугольника
Паскаля и создания функции вычисления количества сочетаний (она уже описывалась в книге «Python. 12 уроков для начинающих», здесь же мы рассмотрим другую
ее реализацию). Особенность этих задач в том, что для них можно использовать
разные языковые средства.
Задача 1
Запрограммировать построение треугольника Паскаля (рис. 5.11). Он представляет
собой пирамиду, в вершине которой находится 1, а каждая нижняя клетка равна
сумме двух верхних клеток, которые на нее опираются, боковые же клетки равны 1.
Треугольник Паскаля заполняется сверху.
Ход программирования
Шаг 1. Сперва научимся выделять память под треугольник Паскаля. Это двумерный неровный список, каждая следующая строка которого на одну клетку больше
предыдущей (рис. 5.12).
258
Глава 5
1
0
1
1
1
2
1
3
1
4
1
5
1
6
7
1
2
3
4
1
3
6
1
4
1
5 10 10 5
1
6 15 20 15 6
1
1
7 21 35 35 21 7
1
0
1
7
2
3
4
5
6
Рис. 5.11. Треугольник Паскаля
Рис. 5.12. Неровный двумерный список
для хранения треугольника Паскаля
Код выделения памяти под неровный список приведен в листинге 5.4.1.
Листинг 5.4.1
n=10
L=[]
for i in range(n):
L.append((i+1)*[0])
for row in L:
print(row)
Результат
[0]
[0,
[0,
[0,
[0,
[0,
[0,
[0,
[0,
[0,
0]
0,
0,
0,
0,
0,
0,
0,
0,
0]
0,
0,
0,
0,
0,
0,
0,
0]
0,
0,
0,
0,
0,
0,
0]
0,
0,
0,
0,
0,
0]
0,
0,
0,
0,
0]
0, 0]
0, 0, 0]
0, 0, 0, 0]
Шаг 2. Заполним боковые клетки единицами (листинг 5.4.2).
Листинг 5.4.2
n=10
L=[]
for i in range(n):
L.append((i+1)*[0])
L[i][0]=1
L[i][-1]=1
for row in L:
print(row)
Результат
[1]
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
1]
0,
0,
0,
0,
0,
0,
0,
0,
1]
0,
0,
0,
0,
0,
0,
0,
1]
0,
0,
0,
0,
0,
0,
1]
0,
0,
0,
0,
0,
1]
0,
0,
0,
0,
1]
0, 1]
0, 0, 1]
0, 0, 0, 1]
Шаг 3. Вычислим значения внутренних клеток и получим готовую программу
(листинг 5.4.3).
Объектно-ориентированное программирование
Листинг 5.4.3. Треугольник Паскаля
n=10
L=[]
for i in range(n):
L.append((i+1)*[0])
L[i][0]=1
L[i][-1]=1
for j in range(1,len(L[i])-1):
L[i][j]=L[i-1][j-1]+L[i-1][j]
for row in L:
print(row)
259
Результат
[1]
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
1]
2,
3,
4,
5,
6,
7,
8,
9,
1]
3, 1]
6, 4, 1]
10, 10, 5, 1]
15, 20, 15, 6, 1]
21, 35, 35, 21, 7, 1]
28, 56, 70, 56, 28, 8, 1]
36, 84, 126, 126, 84, 36, 9, 1]
У треугольника Паскаля есть много применений. Одно из них — вычисление количества сочетаний, использующихся в комбинаторике. Фактически клетки треугольника Паскаля хранят количество сочетаний (если нумеровать строки и столбцы
с нуля).
Задача 2
Написать функцию вычисления количества сочетаний на основе треугольника Паскаля.
Ход программирования
Шаг 1. Напишем функцию, возвращающую значение из ячейки треугольника Паскаля (листинг 5.4.4).
Листинг 5.4.4
n=10
L=[]
for i in range(n):
L.append((i+1)*[0])
L[i][0]=1
L[i][-1]=1
for j in range(1,len(L[i])-1):
L[i][j]=L[i-1][j-1]+L[i-1][j]
def comb(i,j):
return L[i][j]
i=int(input())
j=int(input())
print(comb(i,j))
Шаг 2. Приведенный код потенциально опасен — мы можем выйти за пределы вычисленного треугольника Паскаля. Поместим выделение памяти и расчеты внутрь
функции comb (листинг 5.4.5).
260
Глава 5
Листинг 5.4.5
L=[]
def comb(n,m):
global L
for i in range(len(L),n+1):
L.append((i+1)*[0])
L[i][0]=1
L[i][-1]=1
for j in range(1,len(L[i])-1):
L[i][j]=L[i-1][j-1]+L[i-1][j]
return L[n][m]
n=int(input())
m=int(input())
print(comb(n,m))
for row in L:
print(row)
Результат:
10
5
252
[1]
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
[1,
1]
2, 1]
3, 3, 1]
4, 6, 4, 1]
5, 10, 10, 5, 1]
6, 15, 20, 15, 6, 1]
7, 21, 35, 35, 21, 7, 1]
8, 28, 56, 70, 56, 28, 8, 1]
9, 36, 84, 126, 126, 84, 36, 9, 1]
10, 45, 120, 210, 252, 210, 120, 45, 10, 1]
П РИМЕЧАНИЕ К КОДУ
Ключевое слово global внутри функции означает, что объект после него является глобальным, т. е. объявленным вне функции, существующим не только во время запусков
этой функции, но и сохраняющим свое значение между запусками функции и доступным другим функциям программы.
Шаг 3. Заметим, что для вычисления количества сочетаний нам не обязательно вычислять все нижние строчки треугольника Паскаля полностью. Достаточно получать значения только тех ячеек, которые находятся сверху. Для их расчета превратим функцию comb в рекурсивную (листинг 5.4.6).
Объектно-ориентированное программирование
Листинг 5.4.6
L=[]
def comb(n,m):
global L
if n>=len(L):
for i in range(len(L),n+1):
L.append([0]*(i+1))
if L[n][m]==0:
if m==n==0:
L[n][m]=1
elif m==0:
L[n][m]=comb(n-1,m)
elif m==n:
L[n][m]=comb(n-1,m-1)
else:
L[n][m]=comb(n-1,m-1)+comb(n-1,m)
return L[n][m]
261
Результат
10
5
252
[1]
[1,
[1,
[1,
[1,
[1,
[0,
[0,
[0,
[0,
[0,
1]
2,
3,
4,
5,
6,
0,
0,
0,
0,
1]
3, 1]
6, 4, 1]
10, 10, 5, 1]
15, 20, 15, 6, 0]
21, 35, 35, 21, 0, 0]
0, 56, 70, 56, 0, 0, 0]
0, 0, 126, 126, 0, 0, 0, 0]
0, 0, 0, 252, 0, 0, 0, 0, 0]
n=int(input())
m=int(input())
print(comb(n,m))
for row in L:
print(row)
Шаг 4. Фактически в написанной программе применяется прием, который называется мемоизацией (кешированием) рекурсивной функции. Уберем мемоизацию списком L и получим рекурсивную функцию (листинг 5.4.7).
Листинг 5.4.7
def comb(n,m):
if m==n==0:
return 1
elif m==0:
return comb(n-1,m)
elif m==n:
return comb(n-1,m-1)
else:
return comb(n-1,m-1)+comb(n-1,m)
n=int(input())
m=int(input())
print(comb(n,m))
Шаг 5. Запуская программу, убеждаемся, что при больших значениях аргумента
она работает медленно (а с кешированием — быстро). Применим кеширование
с помощью библиотечного декоратора Python lru_cache (листинг 5.4.8).
262
Глава 5
Листинг 5.4.8
from functools import lru_cache
@lru_cache
def comb(n,m):
if m==n==0:
return 1
elif m==0:
return comb(n-1,m)
elif m==n:
return comb(n-1,m-1)
else:
return comb(n-1,m-1)+comb(n-1,m)
n=int(input())
m=int(input())
print(comb(n,m))
П РИМЕЧАНИЕ К КОДУ
Сейчас использование декоратора выглядит магией — мы написали волшебное слово
@lru_cache, и программа заработала быстро. Подробнее декораторы мы изучим в следующей главе. Пока лишь усвойте, что функцию иногда можно ускорить с помощью
этого декоратора.
Убеждаемся, что программа работает быстро. Но мы потеряли треугольник Паскаля! Он запрятан так глубоко, что мы не сможем к нему обратиться. Но это поправимо. Можно заставить функцию comb вести себя как двумерный список! Для этого
применим функторы.
Отступление про функторы
В объектно-ориентированном программировании есть понятие функтор — функция, маскирующаяся под список. Для этого надо написать класс с перегруженными
методами __call__ и __getitem__. Поскольку функторы редко встречаются в литературе, приведем полную цепочку рассуждений.
Шаг 1. Создадим обычную функцию — например, возведения в квадрат: square.
Напишем класс Square с перегруженными методами __call__ и __getitem__, которые
будут вызывать функцию возведения в квадрат. Создадим экземпляр класса sq, который будем использовать как функцию или как список (листинг 5.4.9).
Листинг 5.4.9
def square(n):
return n*n
class Square:
def __call__(self,n):
return square(n)
Объектно-ориентированное программирование
263
def __getitem__(self,n):
return square(n)
sq=Square()
print(sq(3))
print(sq[3])
Шаг 2. Сделаем класс универсальным — пусть он сможет работать не только
с функцией возведения в квадрат. Сделаем ему конструктор, который будет запоминать функцию для вызова (она станет его свойством). У свойства будет значение
по умолчанию — функция, которая возвращает свой аргумент. Зададим его в виде
анонимной функции (листинг 5.4.10).
Листинг 5.4.10
class functor:
def __init__(self,f=lambda n:n):
self.f=f
def __call__(self,n):
return self.f(n)
def __getitem__(self,n):
return self.f(n)
def square(n):
return n*n
sq=functor(square)
print(sq(3))
print(sq[3])
Шаг 3. Нет смысла для возведения в квадрат использовать два обозначения: square
и sq. Переопределим функцию, сделав замену:
Было
sq=functor(square)
Стало
square=functor(square)
и получим программу, приведенную в листинге 5.4.11.
Листинг 5.4.11
class functor:
def __init__(self,f=lambda n:n):
self.f=f
def __call__(self,n):
return self.f(n)
def __getitem__(self,n):
return self.f(n)
264
Глава 5
def square(n):
return n*n
square=functor(square)
print(square(3))
print(square[3])
Шаг 4. Для записей вида:
square=functor(square)
в Python есть языковая конструкция, которая называется декоратор (листинг 5.4.12).
Листинг 5.4.12. Функтор
class functor:
def __init__(self,f=lambda n:n):
self.f=f
def __call__(self,n):
return self.f(n)
def __getitem__(self,n):
return self.f(n)
@functor
def square(n):
return n*n
print(square(3))
print(square[3])
Продолжение хода программирования
Шаг 5. Вернемся теперь к треугольнику Паскаля. Переделаем функтор так, чтобы
он запускался для функций с двумя аргументами. Для этого нам понадобится частичное применение функции (листинг 5.4.13).
Листинг 5.4.13
from functools import lru_cache
from functools import partial
class functor:
def __init__(self,f):
self.f=f
def __call__(self,n,m):
return self.f(n,m)
def __getitem__(self,n):
return partial(self.f,n)
Объектно-ориентированное программирование
265
@functor
@lru_cache
def comb(n,m):
if m==n==0:
return 1
elif m==0:
return comb(n-1,m)
elif m==n:
return comb(n-1,m-1)
else:
return comb(n-1,m-1)+comb(n-1,m)
n=int(input())
m=int(input())
print(comb(n,m))
print(comb[n](m))
Шаг 6. Обратите внимание, что один из аргументов мы можем теперь помещать
в квадратные скобки. Чтобы помещать в квадратные скобки оба аргумента, нам
нужно к результирующей функции partial(self.f,n) также применить функтор, но
уже для одного аргумента (листинг 5.4.14).
Листинг 5.4.14. Треугольник Паскаля как функтор
from functools import lru_cache
from functools import partial
class functor1:
def __init__(self,f):
self.f=f
def __call__(self,n):
return self.f(n)
def __getitem__(self,n):
return self.f(n)
class functor2:
def __init__(self,f):
self.f=f
def __call__(self,n,m):
return self.f(n,m)
def __getitem__(self,n):
return functor1(partial(self.f,n))
@functor2
@lru_cache
def comb(n,m):
if m==n==0:
return 1
266
Глава 5
elif m==0:
return comb(n-1,m)
elif m==n:
return comb(n-1,m-1)
else:
return comb(n-1,m-1)+comb(n-1,m)
n=int(input())
m=int(input())
print(comb(n,m))
print(comb[n](m))
print(comb[n][m])
В этом примере показано, как изящно сочетаются в Python функциональное и
объектно-ориентированное программирование, причем используются стандартные
библиотечные средства.
5.5. Гиперкуб в многомерном пространстве
В разд. 4.8 — про гиперсферу — мы привыкали к многомерным пространствам.
В них точкам соответствовали списки координат. Здесь мы продолжим работать
с многомерными пространствами и решим задачу на многомерный список, количество измерений которого заранее неизвестно.
Задача
В многомерном списке найти размер гиперкуба, заполненного одинаковыми элементами.
Примеры:
Одномерный случай — задан список:
L=[1, 2, 3, 0, 0, 0, 4]
В нем есть фрагмент из трех нулей. Ответ: 3.
Двумерный случай — задана матрица:
M=[[1,2,3,4],
[5,6,0,0],
[7,8,0,0],
[9,1,2,3]]
В ней есть квадрат из нулей размером 2×2. Ответ: 2.
Трехмерный случай — задан гиперкуб:
M=[[],[],[]]
M[0]=[[1,2,3],
[4,5,6],
[7,8,9]]
Объектно-ориентированное программирование
267
M[1]=[[1,2,3],
[4,0,0],
[5,0,0]]
M[2]=[[6,7,8],
[9,0,0],
[1,0,0]]
В нем есть куб из нулей размером 2×2×2. Ответ: 2.
Задачу нужно решить для общего случая, когда пользователь задает не только длину ребра гиперкуба, но и размерность пространства.
Языковые конструкции: классы, списки, функции, множества, словари.
Приемы программирования: рекурсивные структуры данных, рекурсивные функции, хешированные списки, динамика по подотрезкам.
Идея алгоритма
Эта задача является обобщением задачи поиска длины квадратного фрагмента
в таблице, заполненного одними нулями. Я уже представил две версии этого алгоритма:
1. В разд. 8.2 книги «Python. 12 уроков для начинающих» — динамикой по подотрезкам.
2. В разд. 2.4 этой книги — с помощью вложенных циклов.
Соответственно, у нас есть выбор, какой из алгоритмов мы будем обобщать. Предлагаю остановиться на динамике по подотрезкам.
Но прежде чем начинать писать алгоритм, нужно определиться, как мы будем хранить этот многомерный список данных. Очевидно, наиболее удобный способ —
использовать класс. Ведь с этим объектом будет связано множество вспомогательных операций — например, ввод/вывод. А многомерность мы реализуем так, чтобы
класс представлял собой рекурсивную структуру данных: гиперкуб будет списком,
в котором каждый элемент окажется тоже гиперкубом, только размерностью на
единицу меньше исходного гиперкуба.
Идея алгоритма с использованием динамики по подотрезкам заключается в том, что
сначала мы проверяем, не заполнен ли вдруг весь гиперкуб одним и тем же значением? Тогда результатом выполнения функции будет размер гиперкуба n.
В противном случае мы урезаем гиперкуб, для каждой вершины удаляя слои по
каждой прилегающей к вершине грани. И проверяем, заполнены ли срезы одинаковыми элементами. Причем эти проверки будут делаться этой же самой функцией,
которая проверяла весь гиперкуб, — т. е. мы имеем дело с классической динамикой
по подотрезкам. Проиллюстрируем эти срезы для кубов различных размерностей.
1. В одномерном случае (рис. 5.13) — если список не заполнен одинаковыми элементами, рассматриваем два списка с удаленными правой или левой ячейками.
2. В двумерном случае (рис. 5.14) требуется рассмотреть четыре квадрата. От каждого угла мы удаляем крайние строчку и столбец.
268
Глава 5
Рис. 5.13. Одномерный случай
Рис. 5.14. Двумерный случай
Рис. 5.15. Трехмерный случай
3. Для трехмерного случая (рис. 5.15) требуется рассмотреть уже восемь урезаний
куба по каждой вершине.
Написав рабочую версию программы, убедимся, что она неэффективна — при срезах многомерного куба есть множество повторяющихся веток. Для исправления
Объектно-ориентированное программирование
269
ситуации станем кешировать результаты в словаре. Ключом словаря будет описание среза, которое само по себе является списком. А значит, придется использовать
список с хеш-функцией.
Ход программирования
Шаг 1. Создадим класс «Гиперкуб» с конструктором, индексатором и функцией
вывода на экран.
Конструктор будет принимать два аргумента: n — размер ребра и d — размерность
(количество измерений в пространстве) — и заполнять гиперкуб одними нулями.
Создаваемый класс — это рекурсивная структура, которая представляет собой список из n элементов, которые, в свою очередь, также являются гиперкубами, только
размерностью на единицу меньше. Терминальным случаем является куб с размерностью 1 — это список (листинг 5.5.1).
Листинг 5.5.1
def __init__(self,n=1,d=1):
self.n=n
self.d=d
if d==1:
self.A=[0]*n
else:
self.A=[hypercube(n,d-1) for i in range(n)]
Индексатор — это два метода для переопределения квадратных скобок. Он совершенно стандартен (листинг 5.5.2).
Листинг 5.5.2
def __getitem__(self,i):
return self.A[i]
def __setitem__(self,i,v):
self.A[i]=v
Функция вывода рекурсивна, как и конструктор (листинг 5.5.3).
Листинг 5.5.3
def print(self):
if self.d==1:
print(self.A)
else:
for i in range(self.n):
self[i].print()
print()
Проверим работу всей программы (листинг 5.5.4).
270
Глава 5
Листинг 5.5.4
class hypercube:
def __init__(self,n=1,d=1):
self.n=n
self.d=d
if d==1:
self.A=[0]*n
else:
self.A=[hypercube(n,d-1) for i in range(n)]
def __getitem__(self,i):
return self.A[i]
def __setitem__(self,i,v):
self.A[i]=v
def print(self):
if self.d==1:
print(self.A)
else:
for i in range(self.n):
self[i].print()
print()
n=int(input())
d=int(input())
cube=hypercube(3,3)
cube.print()
print(cube[0][0][0])
Результат:
3
3
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
[0, 0, 0]
0
Шаг 2. Напишем функцию ввода input (она по структуре аналогична print). Также
добавим в print и input оператор return self — для цепочки вызовов методов (листинг 5.5.5).
Объектно-ориентированное программирование
Листинг 5.5.5
class hypercube:
...
def print(self):
if self.d==1:
print(self.A)
else:
for i in range(self.n):
self[i].print()
print()
return self
def input(self):
if self.d==1:
self.A=[int(i) for i in input().split()]
else:
for i in range(self.n):
self[i].input()
print()
return self
cube=hypercube(3,3).input().print()
Результат:
1 2 3
4 5 6
7 8 9
11 12 13
14 15 16
17 18 19
21 22 23
24 25 26
27 28 29
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]
[11, 12, 13]
[14, 15, 16]
[17, 18, 19]
[21, 22, 23]
[24, 25, 26]
[27, 28, 29]
271
272
Глава 5
Шаг 3. Может потребоваться не только вводить гиперкуб с клавиатуры, но и задавать его программно. Для этого полезна функция копирования списка (листинг 5.5.6).
Листинг 5.5.6
class hypercube:
...
def copy(self,L):
for i in range(self.n):
self[i]=L[i]
cube=hypercube(2,3)
cube[0][0].copy([0,1])
cube[0][1].copy([2,3])
cube[1][0].copy([5,6])
cube[1][1].copy([7,8])
cube.print()
Результат:
[0, 1]
[2, 3]
[5, 6]
[7, 8]
Шаг 4. Чтобы проверить, заполнен ли фрагмент куба одними нулями, полезно многомерный список превратить в одномерный. С этой целью в классе hypercube мы
создадим рекурсивный метод linear, для описания фрагмента которого достаточно
знать координаты угла (будем хранить его в списке angle) и размер подкуба
(size) — например:
M=[[1,2,3,4,5],
[6,7,0,0,0],
[8,9,0,0,0],
[1,2,0,0,0],
[3,4,5,6,7]]
В этой таблице квадрат, заполненный нулями, однозначно определяется:
аngle=[1,2]
и
size=3
Фрагмент программы приведен в листинге 5.5.7.
Объектно-ориентированное программирование
Листинг 5.5.7
273
Результат
class hypercube:
def __init__(self,n=1,d=1):
self.n=n
self.d=d
if d==1:
self.A=[0]*n
else:
self.A=[hypercube(n,d-1) for i in range(n)]
def __getitem__(self,i):
return self.A[i]
def __setitem__(self,i,v):
self.A[i]=v
def print(self):
if self.d==1:
print(self.A)
else:
for i in range(self.n):
self[i].print()
print()
return self
def input(self):
if self.d==1:
self.A=[int(i) for i in input().split()]
else:
for i in range(self.n):
self[i].input()
print()
return self
def copy(self,L):
for i in range(self.n):
self[i]=L[i]
def linear(self,angle=[],size=-1):
if size==-1:
angle=[0]*cube.d
size=cube.n
if self.d==1:
return self[angle[0]:angle[0]+size]
else:
L=[]
for i in range(angle[0],angle[0]+size):
L=L+self[i].linear(angle[1:],size)
return L
cube=hypercube(3,2)
cube[0].copy([1,2,3])
cube[1].copy([4,5,6])
cube[2].copy([7,8,9])
print(cube.linear([1,1],2))
[5, 6, 8, 9]
274
Глава 5
Обратите внимание, что в функции linear значения угла и размера заданы по умолчанию. В случае, если при вызове они не заданы, функция работает не с фрагментом, а со всем гиперкубом.
Шаг 5. Теперь напишем фрагмент конечной функции, которая определит, заполнен
ли весь гиперкуб одним и тем же значением. Для этого:
1. Преобразуем гиперкуб в одномерный список.
2. Преобразуем одномерный список во множество, т. е. удалим дубликаты.
3. Вычислим размер множества. Если гиперкуб заполнен одним и тем же элементом, то размер множества будет 1.
4. Если размер множества 1, то функция вернет размер гиперкуба n.
Фрагмент программы приведен в листинге 5.5.8.
Листинг 5.5.8
Результат
...
def area(cube,angle=[],size=-1):
if size==-1:
angle=[0]*cube.d
size=cube.n
if len(set(cube.linear(angle,size)))==1:
return size
cube=hypercube(2,3)
cube[0][0].copy([0,0])
cube[0][1].copy([0,0])
cube[1][0].copy([0,0])
cube[1][1].copy([0,0])
2
print(area(cube))
Шаг 6. Займемся формированием срезов гиперкуба.
Введем маску обрезания, в которой 0 будет обозначать обрезание с левого края,
а 1 — с правого, если смотреть по оси координат.
Для одномерного случая маски приведены на рис. 5.16.
x
x=0
x=1
Рис. 5.16. Маски для одномерного «куба»
Объектно-ориентированное программирование
275
Для двумерного случая маски приведены на рис. 5.17.
y
x
x=1,y=0
x=0,y=0
x=1,y=1
x=0,y=1
Рис. 5.17. Маски для двумерного «куба»
Для трехмерного случая маски приведены на рис. 5.18.
x=0,y=1,z=1
x=1,y=0,z=1
x=1,y=1,z=1
z
y
x
x=1,y=1,z=0
x=0,y=1,z=0
x=0,y=0,z=1
x=0,y=0,z=0
x=1,y=0,z=0
Рис. 5.18. Маски для трехмерного «куба»
Фактически множество масок — это полный набор двоичных чисел, по количеству
разрядов равный количеству измерений. Всего масок 2d — столько же раз рекурсивно мы будем вызывать нашу функцию.
276
Глава 5
Перечислим маски для трехмерного случая:
000
100
010
110
001
101
011
111
Научимся генерировать эти маски с помощью функции инкремента по правилам
сложения двоичных чисел (листинг 5.5.9).
Листинг 5.5.9
def inc(L):
L[0]=L[0]+1
for i in range(len(L)):
if L[i]==2:
L[i]=0
if i+1<len(L):
L[i+1]=L[i+1]+1
return L
Результат
[0,
[1,
[0,
[1,
[0,
[1,
[0,
[1,
0,
0,
1,
1,
0,
0,
1,
1,
0]
0]
0]
0]
1]
1]
1]
1]
L=[0]*3
for i in range(8):
print(L)
L=inc(L)
Шаг 7. С помощью вспомогательной функции inc мы можем сформировать срезы и
написать рекурсивную ветку функции area. Для каждого среза рекурсивно запустим
функцию area и запишем результаты в список. Ответом будет максимальное значение списка (листинг 5.5.10).
Листинг 5.5.10
def area(cube,angle=[],size=-1):
if size==-1:
angle=[0]*cube.d
size=cube.n
if len(set(cube.linear(angle,size)))==1:
return size
else:
L=[0]*cube.d
R=[]
for i in range(2**cube.d):
angle1=[angle[i]+L[i] for i in range(cube.d)]
R.append(area(cube,angle1,size-1))
L=inc(L)
return(max(R))
Объектно-ориентированное программирование
277
Мы получили готовую программу. Приведем ее код полностью с результатами
выполнения (листинг 5.5.11).
Листинг 5.5.11. Гиперкуб в многомерном пространстве динамикой по подотрезкам
class hypercube:
def __init__(self,n=1,d=1):
self.n=n
self.d=d
if d==1:
self.A=[0]*n
else:
self.A=[hypercube(n,d-1) for i in range(n)]
def __getitem__(self,i):
return self.A[i]
def __setitem__(self,i,v):
self.A[i]=v
def print(self):
if self.d==1:
print(self.A)
else:
for i in range(self.n):
self[i].print()
print()
return self
def input(self):
if self.d==1:
self.A=[int(i) for i in input().split()]
else:
for i in range(self.n):
self[i].input()
print()
return self
def copy(self,L):
for i in range(self.n):
self[i]=L[i]
def linear(self,angle=[],size=-1):
if size==-1:
angle=[0]*cube.d
size=cube.n
if self.d==1:
return self[angle[0]:angle[0]+size]
else:
L=[]
for i in range(angle[0],angle[0]+size):
L=L+self[i].linear(angle[1:],size)
return L
278
def inc(L):
L[0]=L[0]+1
for i in range(len(L)):
if L[i]==2:
L[i]=0
if i+1<len(L):
L[i+1]=L[i+1]+1
return L
def area(cube,angle=[],size=-1):
if size==-1:
angle=[0]*cube.d
size=cube.n
if len(set(cube.linear(angle,size)))==1:
return size
else:
L=[0]*cube.d
R=[]
for i in range(2**cube.d):
angle1=[angle[i]+L[i] for i in range(cube.d)]
R.append(area(cube,angle1,size-1))
L=inc(L)
return(max(R))
cube=hypercube(4,3)
cube[0][0].copy([2,1,1,1])
cube[0][1].copy([1,1,1,1])
cube[0][2].copy([1,1,1,1])
cube[0][3].copy([3,1,1,1])
cube[1][0].copy([1,1,1,1])
cube[1][1].copy([4,1,0,0])
cube[1][2].copy([5,1,0,0])
cube[1][3].copy([1,1,1,1])
cube[2][0].copy([1,1,1,1])
cube[2][1].copy([1,1,0,0])
cube[2][2].copy([1,1,0,0])
cube[2][3].copy([1,1,1,1])
cube[3][0].copy([1,6,1,1])
cube[3][1].copy([1,1,1,1])
cube[3][2].copy([1,7,1,1])
cube[3][3].copy([1,1,1,1])
print(area(cube))
Результат:
2
Глава 5
Объектно-ориентированное программирование
279
Шаг 8. Программа работает неэффективно. Посмотрим, как происходит вызов
цепочки рекурсий для двумерного случая (рис. 5.19).
Как можно видеть, уже на третьем уровне имеется много дубликатов квадратов. То
есть наша функция один и тот же квадрат обсчитывает много раз. Так что нам надо
добиться сокращения количества проверок (рис. 5.20).
Это можно сделать с помощью кеширования. Посмотрим на функцию:
def area(cube,angle=[],size=-1):
Рис. 5.19. Полная цепочка рекурсивных вызовов
280
Глава 5
... ... ...
... ... ...
... ... ...
... ... ...
... ... ...
... ... ...
... ... ...
... ... ...
... ... ...
Рис. 5.20. Сокращенная цепочка рекурсивных вызовов
Параметрами кеша являются аргументы функции angle и size. Но angle представляет
собой список. Наиболее изящно для этого случая сделать кеширование с помощью
словаря. В нем ключом будет список angle с добавленным в конце элементом size,
а значением — результат выполнения функции.
Поскольку список не может быть ключом в словаре, воспользуемся уже написанным нами классом «Хешированный список» (листинг 5.5.12).
Листинг 5.5.12
class hlist:
maxh = 10
def __init__(self, a = []):
self.a = a
Объектно-ориентированное программирование
281
def print(self):
print(self.a)
def __eq__(self, other):
return (self.a == other.a)
def __hash__(self):
s = 0
for i in range(len(self.a)):
s=s+self.a[i]*(self.maxh**i)
return s
def tolist(self):
return self.a
Шаг 9. Преобразуем функцию area в кешированную функцию, для чего:
1. Добавим словарь в аргументы функции. На основе angle и size сформируем ключ
(листинг 5.5.13).
Листинг 5.5.13
def area(cube,angle=[],size=-1,V={}):
if size==-1:
angle=[0]*cube.d
size=cube.n
if len(set(cube.linear(angle,size)))==1:
return size
else:
L=[0]*cube.d
R=[]
for i in range(2**cube.d):
angle1=[angle[i]+L[i] for i in range(cube.d)]
R.append(area(cube,angle1,size-1,V))
L=inc(L)
return(max(R))
def area(cube,angle=[],size=-1,V={}):
if size==-1:
angle=[0]*cube.d
size=cube.n
key=hlist(angle+[size])
if len(set(cube.linear(angle,size)))==1:
return size
else:
L=[0]*cube.d
R=[]
for i in range(2**cube.d):
angle1=[angle[i]+L[i] for i in range(cube.d)]
R.append(area(cube,angle1,size-1,V))
L=inc(L)
return(max(R))
282
Глава 5
2. Прежде чем делать вычисления, проверим, возможно, мы уже нашли размер
фрагмента для этого ключа. В конце программы сделаем общий return (листинг 5.5.14).
Листинг 5.5.14
def area(cube,angle=[],size=-1,V={}):
if size==-1:
angle=[0]*cube.d
size=cube.n
key=hlist(angle+[size])
if key not in V:
if len(set(cube.linear(angle,size)))==1:
return size
else:
L=[0]*cube.d
R=[]
for i in range(2**cube.d):
angle1=[angle[i]+L[i] for i in range(cube.d)]
R.append(area(cube,angle1,size-1,V))
L=inc(L)
return max(R)
return V[key]
3. Заменим все return в вычислительной части на сохранение значений в словаре
(листинг 5.5.15).
Листинг 5.5.15
def area(cube,angle=[],size=-1,V={}):
if size==-1:
angle=[0]*cube.d
size=cube.n
key=hlist(angle+[size])
if key not in V:
if len(set(cube.linear(angle,size)))==1:
V[key]=size
else:
L=[0]*cube.d
R=[]
for i in range(2**cube.d):
angle1=[angle[i]+L[i] for i in range(cube.d)]
R.append(area(cube,angle1,size-1,V))
L=inc(L)
V[key]=max(R)
return V[key]
Мы получили итоговую программу. Ее код полностью приведен в листинге 5.5.16.
Объектно-ориентированное программирование
Листинг 5.5.16. Гиперкуб в многомерном пространстве
динамикой по подотрезкам и кешированием
class hypercube:
def __init__(self,n=1,d=1):
self.n=n
self.d=d
if d==1:
self.A=[0]*n
else:
self.A=[hypercube(n,d-1) for i in range(n)]
def __getitem__(self,i):
return self.A[i]
def __setitem__(self,i,v):
self.A[i]=v
def print(self):
if self.d==1:
print(self.A)
else:
for i in range(self.n):
self[i].print()
print()
return self
def input(self):
if self.d==1:
self.A=[int(i) for i in input().split()]
else:
for i in range(self.n):
self[i].input()
print()
return self
def copy(self,L):
for i in range(self.n):
self[i]=L[i]
def linear(self,angle=[],size=-1):
if size==-1:
angle=[0]*cube.d
size=cube.n
if self.d==1:
return self[angle[0]:angle[0]+size]
else:
L=[]
for i in range(angle[0],angle[0]+size):
L=L+self[i].linear(angle[1:],size)
return L
def inc(L):
L[0]=L[0]+1
283
284
Глава 5
for i in range(len(L)):
if L[i]==2:
L[i]=0
if i+1<len(L):
L[i+1]=L[i+1]+1
return L
class hlist:
maxh = 10
def __init__(self, a = []):
self.a = a
def print(self):
print(self.a)
def __eq__(self, other):
return (self.a == other.a)
def __hash__(self):
s = 0
for i in range(len(self.a)):
s=s+self.a[i]*(self.maxh**i)
return s
def area(cube,angle=[],size=-1,V={}):
if size==-1:
angle=[0]*cube.d
size=cube.n
key=hlist(angle+[size])
if key not in V:
if len(set(cube.linear(angle,size)))==1:
V[key]=size
else:
L=[0]*cube.d
R=[]
for i in range(2**cube.d):
angle1=[angle[i]+L[i] for i in range(cube.d)]
R.append(area(cube,angle1,size-1,V))
L=inc(L)
V[key]=max(R)
return V[key]
cube=hypercube(4,3)
cube[0][0].copy([2,1,1,1])
cube[0][1].copy([1,1,1,1])
cube[0][2].copy([1,1,1,1])
cube[0][3].copy([3,1,1,1])
cube[1][0].copy([1,1,1,1])
cube[1][1].copy([4,1,0,0])
Объектно-ориентированное программирование
285
cube[1][2].copy([5,1,0,0])
cube[1][3].copy([1,1,1,1])
cube[2][0].copy([1,1,1,1])
cube[2][1].copy([1,1,0,0])
cube[2][2].copy([1,1,0,0])
cube[2][3].copy([1,1,1,1])
cube[3][0].copy([1,6,1,1])
cube[3][1].copy([1,1,1,1])
cube[3][2].copy([1,7,1,1])
cube[3][3].copy([1,1,1,1])
print(area(cube))
В этой программе мы применили алгоритм из класса «динамика по подотрезкам»
для многомерных рекурсивных структур данных, а также кеширование в словаре.
Из языковых конструкций использовались многомерный список, множество,
словарь, класс. Поэтому рассмотренная здесь задача объединяет почти все наши
программистские знания. Но написанная нами программа — это еще не «вершина
программистского искусства» этой книги. То ли еще будет в следующей главе! Там
мы изучим множество приемов функционального программирования, а в конце соединим функциональное программирование с объектно-ориентированным.
286
Глава 5
ГЛАВА
6
Функциональное программирование
Функциональному программированию была посвящена глава в книге «Python.
12 уроков для начинающих». Кроме того, отдельные языковые конструкции и
приемы этого стиля встречались в главе 3 этой книги, где мы только начали использовать функции. В табл. 6.1 сделан обзор разделов этой главы с точки зрения
приемов функционального программирования.
Таблица. 6.1. Алгоритмы главы 3 и приемы функционального программирования
Раздел
Приемы функционального программирования
3.1. Решето
Эратосфена
и числа-близнецы
Удаление нулей из списка. Фильтрация осуществлялась с помощью
встроенного в Python стандартного функционала (функции, принимающей в качестве аргумента другие функции) filter. Условие
фильтрации задавалось с помощью анонимной функции lambda
3.2. Решето
Сундарама
Отображение одного списка в другой (был список [0,1,2,3,4,...,n],
стал [3,5,7,9,...,2n+1]) с помощью встроенного в Python
функционала map
3.4. Линейный
и медианный фильтры
Передача функции в качестве параметра другой функции (функция
фильтрации принимала в качестве аргументов исходное изображение, форму апертуры и функцию обработки)
3.5. Алгоритм Евклида
Стягивание списка в одно число с помощью библиотечного
функционала reduce (вычисление НОД списка осуществлялось путем
последовательного применения функции с двумя аргументами)
3.6. Гипероператоры
(третья версия
программы)
Стягивание списка с помощью reduce, причем в качестве функции
стягивания рекурсивно передавался гипероператор более низкого
порядка
3.8. Отображения
списков
Этот раздел уже целиком посвящен функциональному программированию: мы заменили библиотечную функцию map на собственную
и с помощью таких приемов, как «частичное применение» и «карринг», подготовили функцию для ее использования в отображении
Таким образом, мы применили функциональное программирование для задач всех
разделов, кроме двух. Практически всегда можно было обойтись без приемов
функционального программирования. Но с ним программы становились изящнее.
Перед прочтением этой главы рекомендую освежить в памяти функциональное
288
Глава 6
программирование, просмотрев последние версии программ из разделов, которые
приведены в табл. 6.1.
В этой главе в первом разделе, посвященном вычислению интеграла, мы вновь повторим функциональное программирование: основные языковые конструкции,
приемы и встроенные в Python функционалы.
Остальные же разделы будут развивать разд. 3.8, посвященный отображению списков. Там мы на основе исходного списка получали новый список путем применения функции к каждому элементу списка. В этой главе мы сделаем отображение
более сложных структур (например, вложенных списков с неограниченным количеством уровней вложенности). Наше отображение будет сохранять внутреннюю
структуру. Кроме того, отображение будет осуществляться не одной функцией, а
последовательностью функций. Здесь мы познакомимся с такими приемами программирования, как монады, функторы и карринг. Эти приемы пришли в программирование из очень абстрактного раздела математики, который называется теория
категорий.
В этой книге функциям посвящены три главы: 3, 4 и 6. Получается, что они разделены главой 5. «Объектно-ориентированное программирование». Это связано
с тем, что для функторов нам понадобятся языковые конструкции из объектноориентированного программирования.
6.1. Интеграл
Задача
Написать функционал приблизительного вычисления интеграла. В функционал передаются аргументы: функция для интегрирования и пределы интегрирования.
Геометрическим смыслом интеграла
right
S=
∫ f ( x)dx
left
является площадь криволинейной трапеции под функцией (рис. 6.1).
Рис. 6.1. Геометрический смысл интеграла
Функциональное программирование
289
Существуют точные методы вычисления интеграла по заданной функции, которые
составляют целый раздел математики. Мы же будем вычислять интеграл примерно.
Зададим шаг изменения переменной x как step и разобьем трапецию на n прямоугольников, где
n=
right − left
.
step
Чем меньше шаг step, тем значение суммы площадей прямоугольников ближе
к площади криволинейной трапеции (рис. 6.2 и 6.3).
y
left
right
step
x
n прямоугольников
Рис. 6.2. Приближенное
вычисление интеграла
Рис. 6.3. Более точное
вычисление интеграла
Заметим, что мы выбрали высоты прямоугольников так, чтобы длины их левых
сторон равнялись значению функции. Таким образом, мы можем примерно подсчитать значение интеграла как сумму площадей этих прямоугольников:
right
S=
∫
left
n −1
n −1
i =0
i =0
f ( x )dx ≈ ∑ f (left + step ⋅ i ) ⋅ step = step ⋅∑ f (left + step ⋅ i) .
Ход программирования
Шаг 1. Напишем функцию вычисления интеграла в соответствии с изложенным
математическим методом (листинг 6.1.1).
Листинг 6.1.1. Интеграл. Функция высшего порядка
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
290
Глава 6
def square(x):
return x*x
def cube(x):
return x**3
print(integral(square,0,2,0.001))
print(integral(cube,0,2,0.001))
Обратите внимание, что при передаче square и cube в integral после них нет круглых
скобок. Круглые скобки с параметрами ставятся при непосредственном вызове
функции. А здесь мы просто передаем имена этих функций, которые вызываются
внутри кода интеграла.
В функции «интеграл» три аргумента: left, right, step — числовые, а один аргумент, f — это функция. Функция integral, поскольку она принимает в качестве
аргумента другую функцию, называется функционалом, или функцией высшего
порядка.
Шаг 2. В нашем примере мы вычислили интегралы от двух функций от x: квадрата
и куба. Мы задали функции с помощью ключевого слова def. Но есть и вторая форма задания функций — c помощью ключевого слова lambda (листинг 6.1.2).
Листинг 6.1.2
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
square = lambda x: x*x
cube = lambda x: x**3
print(integral(square,0,2,0.001))
print(integral(cube,0,2,0.001))
Заметим, что в форме функции с lambda ключевое слово return не нужно.
Шаг 3. Какое преимущество дает использование lambda вместо def? С помощью
lambda-выражения имя функции отделяется от ее кода. А это значит, что в случае,
если не предполагается повторного использования функции в разных частях программы, мы можем код функции без имени сразу прописывать там, где он нужен.
То есть мы используем анонимные функции (листинг 6.1.3).
Листинг 6.1.3. Интеграл. Анонимные функции
def integral(f, left, right, step):
s=0
Функциональное программирование
291
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
print(integral(lambda x: x*x,0,2,0.001))
print(integral(lambda x: x**3,0,2,0.001))
Шаг 4. Вычислим интеграл от функции xn, предварительно задав значение n = 10
(листинг 6.1.4).
Листинг 6.1.4. Интеграл. Захват переменной
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
n=10
print(integral(lambda x: x**n,0,2,0.001))
Заметим, что в коде анонимной функции используется переменная x, которая передается в анонимную функцию как аргумент, а переменная n берется из внешнего
окружения (это называется захват переменной).
Шаг 5. Допустим, что, помимо вычисления интеграла, нам нужна функция возведения в степень:
def power(x,n):
return x**n
В этом случае попытка передать ее в вызов интеграла, как это мы делали с функциями возведения в куб и квадрат:
print(integral(power,0,2,0.001))
приведет к ошибке.
Ведь в интеграле используются функции с одним аргументом, а у возведения в степень их два.
Правильно будет использовать анонимную функцию (листинг 6.1.5).
Листинг 6.1.5. Интеграл. Захват функции
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
292
Глава 6
def power(x,n):
return x**n
print(integral(lambda x: power(x,10),0,2,0.001))
Заметим, что в этой программе из внешнего окружения в анонимную функцию
берется уже не число, а другая функция, т. е. происходит захват функции.
Шаг 6. Вынесем код анонимной функции из вызова интеграла, присвоив этой
функции имя decpower. Так мы на основе функции возведения в степень создадим
новую функцию — возведения в степень 10 (листинг 6.1.6).
Листинг 6.1.6
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
def power(x,n):
return x**n
decpower = lambda x: power(x,10)
print(integral(decpower,0,2,0.001))
Шаг 7. В этой программе мы на основе функции возведения в степень с помощью
анонимной функции получили новую функцию — «возведение в десятичную степень». Но что если нам на основе функции возведения в степень понадобятся и
другие именованные функции — например, квадрат и куб? Логично код с анонимной функцией вынести в отдельную функцию partpower, которая будет порождать
эти новые функции (листинг 6.1.7).
Листинг 6.1.7
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
def power(x,n):
return x**n
def partpower(n):
return lambda x: power(x,n)
Функциональное программирование
293
square = partpower(2)
cube = partpower(3)
decpower = partpower(10)
print(integral(square,0,2,0.001))
print(integral(cube,0,2,0.001))
print(integral(decpower,0,2,0.001))
Заметим, что функция partpower также является функцией высшего порядка, но
только, в отличие от интеграла, который принимает в качестве аргумента другую
функцию, ее аргумент — числовой, при этом она возвращает функцию как результат своего выполнения.
Шаг 8. Функция power теперь используется только внутри функции partpower и не
задействуется в основном коде программы. Перенесем ее код внутрь функции
partpower (листинг 6.1.8).
Листинг 6.1.8. Интеграл. Вложенные функции
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
def partpower(n):
def power(x,n):
return x**n
return lambda x: power(x,n)
square = partpower(2)
cube = partpower(3)
decpower = partpower(10)
print(integral(square,0,2,0.001))
print(integral(cube,0,2,0.001))
print(integral(decpower,0,2,0.001))
Программа работает без ошибок — получается, что мы можем использовать вложенные функции.
Шаг 9. После того как мы вложили функцию power внутрь partpower, исчезла необходимость в длинном имени partpower. Переименуем partpower в power (листинг 6.1.9).
Листинг 6.1.9
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
294
Глава 6
for i in range(n):
s=s+f(left+step*i)
return s*step
def power(n):
def power(x,n):
return x**n
return lambda x: power(x,n)
square = power(2)
cube = power(3)
decpower = power(10)
print(integral(square,0,2,0.001))
print(integral(cube,0,2,0.001))
print(integral(decpower,0,2,0.001))
Заметим, что теперь названия внешней и внутренней функций совпадают! Но программа прекрасно работает. Фактически речь идет о замене функции на вложенную
функцию.
Шаг 10. Функции square, cube, decpower используются только один раз. Перенесем
код их объявления непосредственно туда, где это происходит, т. е. в вызов интеграла (листинг 6.1.10).
Листинг 6.1.10. Интеграл. Частичное применение функций
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
def power(n):
def power(x,n):
return x**n
return lambda x: power(x,n)
print(integral(power(2),0,2,0.001))
print(integral(power(3),0,2,0.001))
print(integral(power(10),0,2,0.001))
Заметим, что реальная бинарная функция запускается теперь в два этапа: на первом
этапе мы задаем показатель степени, а на втором — основание степени. Такой прием программирования называется частичное применение функции.
Шаг 11. Вернемся на два шага назад — к листингу 6.1.8. Заметим, что функция
partpower подготавливает функцию power к тому, чтобы ее можно было интегриро-
Функциональное программирование
295
вать. То есть она сокращает количество ее аргументов с 2 до 1. Но это же самое
можно проделать и с любой другой функцией от двух аргументов. Напишем универсальную функцию partapply, которая будет проводить эту подготовку для любой
бинарной функции (листинг 6.1.11).
Листинг 6.1.11. Интеграл. Функционал частичного применения функции
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
def power(x,n):
return x**n
def partapply(f,n):
return lambda x: f(x,n)
square = partapply(power,2)
cube = partapply(power,3)
decpower = partapply(power,10)
print(integral(square,0,2,0.001))
print(integral(cube,0,2,0.001))
print(integral(decpower,0,2,0.001))
Заметим, что функция высшего порядка partapply не только принимает другую
функцию как аргумент, но и возвращает другую функцию как результат.
Шаг 12. Избавимся от функции power, сделав анонимную функцию в вызове
partapply, оставив только функцию возведения в десятую степень (листинг 6.1.12).
Листинг 6.1.12
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
def partapply(f,n):
return lambda x: f(x,n)
decpower = partapply(lambda x,n: x**n,2, 10)
print(integral(decpower,0,2,0.001))
296
Глава 6
Шаг 13. Перенесем вызов partapply туда, где он используется, — т. е. в функцию
integral (листинг 6.1.13.)
Листинг 6.1.13. Интеграл в функциональном стиле
def integral(f, left, right, step):
s=0
n=int((right-left)/step)
for i in range(n):
s=s+f(left+step*i)
return s*step
def partapply(f,n):
return lambda x: f(x,n)
print(integral(partapply(lambda x,n: x**n,2,10),0,2,0.001))
Код стал более непонятным, и я бы остановился на предыдущем шаге. Но здесь
видно функциональное программирование «во всей красе»: программа представляет собой некоторую комбинацию вызовов функций.
Шаг 14. «Питонизируем» программу. Интеграл можно запрограммировать в три
шага:
1. Породить множество значений X как список:
[left+step*i for i in range(n)]
2. Отобразить этот список в новый список, содержащий значения Y.
3. Суммировать значения списка Y.
4. Получившийся код приведен в листинге 6.1.14.
Листинг 6.1.14
def integral(f, left, right, step):
n=int((right-left)/step)
X=[left+step*i for i in range(n)]
Y=[f(X[i]) for i in range(n)]
return sum(Y)*step
def partapply(f,n):
return lambda x: f(x,n)
def power(x,n):
return x**n
decpower = partapply(power,10)
print(integral(decpower,0,2,0.001))
В нашем примере сочетаются списочные выражения Python с функциональным
программированием. Это придает программам на Python лаконичность.
Функциональное программирование
297
Шаг 15. Вынесем отображение списка X в Y в отдельную функцию mapping (листинг 6.1.15).
Листинг 6.1.15. Интеграл. Сочетание функционального программирования и стиля Python
def mapping(f,L):
return [f(el) for el in L]
def integral(f, left, right, step):
n=int((right-left)/step)
X=[left+step*i for i in range(n)]
Y=mapping(f,X)
return sum(Y)*step
def partapply(f,n):
return lambda x: f(x,n)
def power(x,n):
return x**n
decpower = partapply(power,10)
print(integral(decpower,0,2,0.001))
Наши две функции весьма универсальны: integral считает интеграл от любой подходящей функции, а partapply подготавливает функцию к интегрированию, убирая
у нее лишние аргументы. К этим функциям добавилась еще одна универсальная
функция — mapping. Функциональное программирование подталкивает кодировщика к написанию универсальных функций.
Шаг 16. Python богат встроенными и библиотечными функциями. На самом деле
у нас нет необходимости писать функции mapping и partapply. Вместо них можно использовать встроенную в Python функцию map и функцию partial из библиотеки
functools, в которой есть и много других полезных функций (листинг 6.1.16).
Листинг 6.1.16
from functools import partial
def integral(f, left, right, step):
n=int((right-left)/step)
X=[left+step*i for i in range(n)]
Y=map(f,X)
return sum(Y)*step
def power(x,n):
return x**n
decpower = partial(power,n=10)
print(integral(decpower,0,2,0.001))
298
Глава 6
Шаг 17. Заметим, что X — это тоже отображение range (т. е. списка [0,1,2,...,n-1])
с помощью формулы left+step*i. Вынесем отображение в отдельную функцию. Но,
чтобы ее применить, надо будет сократить количество ее аргументов. Сделаем это
с помощью функционала partial (листинг 6.1.17).
Листинг 6.1.17. Интеграл. Библиотечные функционалы и стиль Python
from functools import partial
def shift(left, step, n):
return left+step*n
def integral(f, left, right, step):
n=int((right-left)/step)
partshift=partial(shift,left,step)
X=map(partshift,range(n))
Y=map(f,X)
return sum(Y)*step
def power(a,n):
return a**n
decpower = partial(power,n=10)
print(integral(decpower,0,2,0.001))
В этом разделе мы вновь увидели функциональное программирование на вполне
жизненном примере — вычислении интеграла. Из написанных нами программ видно, как на основе всего двух языковых конструкций Python — именованной и анонимной функций — естественным путем (как бы сами собой) получаются вложенные функции и функции высших порядков, захват переменных и функций. Мы
написали также функционал для более сложного приема — частичного применения
функций. Встроенные в Python функционалы в сочетании со списочными выражениями делают функциональное программирование более удобным по сравнению
с другими языками программирования общего назначения.
6.2. Отображения,
сохраняющие внутреннюю структуру
Сформулируем задачу, общую для всех остальных разделов этой главы.
Общая задача
Пусть задана сложная коллекция чисел (например, с вложенными списками разных
уровней). Надо отобразить ее в новую коллекцию с сохранением прежней структуры, применяя к каждому простому элементу исходной коллекции сложную формулу, состоящую из композиции функций.
Функциональное программирование
299
Например, пусть дана коллекция:
[1, 2, [3, 4, [5, 6], 7], [8, 9]]
К каждому элементу x из этой коллекции надо применить формулу
fib( x 2 + 2 x ) ,
где fib — функция, вычисляющая число Фибоначчи. Напомню, что числа Фибоначчи — это последовательность, в которой первые два числа равны 1, а каждое следующее равно сумме двух предыдущих:
Номер числа
1
2
3
4
5
6
7
8
9
10
Число Фибоначчи
1
1
2
3
5
8
13
21
34
55
Подсчитаем значения выражения «вручную» по шагам:
1. Возведем каждый элемент исходной коллекции в квадрат:
[1, 4, [9, 16, [25, 36], 49], [64, 81]]
2. Удвоим каждый элемент исходной коллекции:
[2, 4, [6, 8, [10, 12], 14], [16, 18]]
3. Поэлементно сложим результаты предыдущих шагов:
[3, 8, [15, 24, [35, 48], 63], [80, 99]]
4. Далее вычислим числа Фибоначчи, воспользовавшись готовой программой:
[2, 21, [610, 46368, [9227465, 4807526976], 6557470319842],
[23416728348467685, 218922995834555169026]]
Кажется, что алгоритмически это не очень сложная задача. Но в ней есть много
подводных камней. Решая эту задачу, мы изучим сложные приемы функционального программирования.
Задача 1
Написать программу, отображающую исходную коллекцию в новую, применяя поэлементно функцию и сохраняя внутреннюю структуру исходной коллекции.
Ход программирования
Шаг 1. Предельно упростим задачу. Пусть дан одномерный список чисел. Подсчитаем их квадраты с помощью встроенного функционала map (листинг 6.2.1).
Листинг 6.2.1
Результат
def square(x):
return x*x
L=[1,2,3,4,5]
print(L)
[1, 2, 3, 4, 5]
300
M=list(map(square,L))
print(M)
Глава 6
[1, 4, 9, 16, 25]
Попытка применить эту программу к сложной коллекции:
[1, 2, [3, 4, [5, 6], 7], [8, 9]]
приведет к ошибке, ведь map выполняется поэлементно, а функция возведения
в квадрат работает только с отдельными числами, а не со списками.
Шаг 2. Стандартный функционал map нам не подходит. Напишем свой функционал — bind. И добьемся, чтобы он работал как для отдельных чисел, так и для
функций. Для этого нам нужно определить тип аргумента (список или нет?). Воспользуемся встроенной в Python функцией isinstance (листинг 6.2.2).
Листинг 6.2.2
Результат
def square(x):
return x*x
def bind(f,M):
if isinstance(M, list):
R=[]
for m in M:
R.append(f(m))
return R
else:
return f(M)
a=5
print(bind(square,a))
L=[1,2,3,4,5]
print(bind(square,L))
25
[1, 4, 9, 16, 25]
Шаг 3. Чтобы bind обрабатывал не только одномерные списки, но и вложенные
списки, вызовем его рекурсивно к каждому элементу исходной коллекции (листинг 6.2.3).
Листинг 6.2.3
def square(x):
return x*x
def bind(f,M):
if isinstance(M, list):
R=[]
for m in M:
R.append(bind(f,m))
return R
Результат
Функциональное программирование
301
else:
return f(M)
a=5
print(bind(square,a))
L=[1,2,3,4,5]
print(bind(square,L))
M=[1,2,[3,4,[5,6],7],[8,9]]
print(bind(square,M))
25
[1, 4, 9, 16, 25]
[1, 4, [9, 16, [25, 36], 49], [64, 81]]
Шаг 4. Можно сделать функцию bind еще более универсальной. Ее можно применять не только для списков, но и для любых других итерируемых коллекций. Чтобы
проверить, является ли коллекция итерируемой, импортируем класс Iterable из
библиотеки collections.abc. Кроме того, когда мы создадим новый список, нужно
его преобразовать в тип исходной коллекции с помощью функции type (листинг 6.2.4).
Листинг 6.2.4
Результат
def square(x):
return x*x
from collections.abc import Iterable
def bind(f,M):
if isinstance(M, Iterable):
R=[]
for m in M:
R.append(bind(f,m))
return type(M)(R)
else:
return f(M)
a=5
print(bind(square,a))
L=[1,2,3,4,5]
print(bind(square,L))
M=[[1,2,3],[4,5,6],[7,8,9]]
print(bind(square,M))
S={1,2,3}
print(bind(square,S))
T=[1,2,[3,4,[5,6],7],[8,9]]
print(bind(square,T))
25
[1, 4, 9, 16, 25]
[[1, 4, 9], [16, 25, 36], [49, 64, 81]]
{1, 4, 9}
[1, 4, [9, 16, [25, 36], 49], [64, 81]]
Шаг 5. Напишем функцию bind в стиле Python (листинг 6.2.5).
Листинг 6.2.5. Отображение, сохраняющее внутреннюю структуру
def square(x):
return x*x
302
Глава 6
from collections.abc import Iterable
def bind(f,M):
if isinstance(M, Iterable):
return type(M)(bind(f,m) for m in M)
else:
return f(M)
a=5
print(bind(square,a))
L=[1,2,3,4,5]
print(bind(square,L))
M=[[1,2,3],[4,5,6],[7,8,9]]
print(bind(square,M))
S={1,2,3}
print(bind(square,S))
T=[1,2,[3,4,[5,6],7],[8,9]]
print(bind(square,T))
Задача 2
Найти сумму всех элементов сложной коллекции.
Ход программирования
Шаг 1. Для простой коллекции сумму мы обычно находим с помощью встроенной
в Python функции sum (листинг 6.2.6).
Листинг 6.2.6
L=[1,2,3,4,5]
print(sum(L))
Подготовим программу к более универсальному варианту: заменим sum на применение библиотечного функционала reduce, который последовательно стягивает список в одно число, применяя к его элементам бинарную функцию (листинг 6.2.7).
Листинг 6.2.7
from functools import reduce
L=[1,2,3,4,5]
print(reduce(lambda x,y: x+y,L))
Применение функционала reduce к сложной коллекции вызовет ошибку.
Шаг 2. За основу новой функции: deepreduce — возьмем написанную нами рекурсивную функцию bind. В ветке общего случая нам нужно стянуть некоторые элементы в одно число (применим библиотечную reduce). Но поскольку эти элементы
могут иметь сложную структуру, к ним также нужно применить deepreduce. Отобразим deepreduce на коллекцию с помощью map (листинг 6.2.8).
Функциональное программирование
Листинг 6.2.8. Стягивание сложной коллекции
303
Результат
from collections.abc import Iterable
from functools import reduce
def deepreduce(f,M):
if isinstance(M, Iterable):
return reduce(f,map(lambda x: deepreduce(f,x),M))
else:
return M
a=5
print(deepreduce(lambda x,y:x+y,a))
L=[1,2,3,4,5]
print(deepreduce(lambda x,y:x+y,L))
M=[[1,2,3],[4,5,6],[7,8,9]]
print(deepreduce(lambda x,y:x+y,M))
T=[1,2,[3,4,[5,6],7],[8,9]]
print(deepreduce(lambda x,y:x+y,T))
S={1,2,3}
print(deepreduce(lambda x,y:x+y,S))
5
15
45
45
6
Шаг 3. Может возникнуть другая идея для реализации deepreduce — рекурсивно
превратить коллекцию в одномерный список («выпрямить список»). Этот вариант
хорош для коммутативных операций (где порядок операндов не имеет значения —
например, для суммы и произведения). Но если речь идет о возведении в степень,
от того, как мы применяем функцию, зависит результат (листинг 6.2.9).
Листинг 6.2.9
from collections.abc import Iterable
from functools import reduce
def deepreduce(f,M):
if isinstance(M, Iterable):
return reduce(f,map(lambda x: deepreduce(f,x),M))
else:
return M
print(deepreduce(lambda x,y: x**y,[2,3,4]))
print(deepreduce(lambda x,y: x**y,[[2,3],4]))
print(deepreduce(lambda x,y: x**y,[2,[3,4]]))
Результат:
4096
4096
2417851639229258349412352
304
Глава 6
В первых двух случаях мы вычислили:
(23 ) 4 = 84 = 4096 .
В последнем случае:
2
(34 ) = 281 = 2417851639229258349412352 .
Получается, что квадратные скобки в списке послужили скобками в математической формуле.
Задача 3
Применить функцию к сложной коллекции и найти сумму её элементов.
Ход программирования
Шаг 1. Применим написанные нами bind и deepreduce в одной программе (листинг 6.2.10).
Листинг 6.2.10. Отображение, сохраняющее внутреннюю структуру,
и стягивание сложной коллекции
from collections.abc import Iterable
from functools import reduce
def bind(f,M):
if isinstance(M, Iterable):
return type(M)(bind(f,m) for m in M)
else:
return f(M)
def deepreduce(f,M):
if isinstance(M, Iterable):
return reduce(f,map(lambda x: deepreduce(f,x),M))
else:
return M
L=[1,2,[3,4,[5,6],7],[8,9]]
print(L)
M=bind(lambda n:n*n,L)
print(M)
print(deepreduce(lambda x,y:x+y,M))
Результат:
[1, 2, [3, 4, [5, 6], 7], [8, 9]]
[1, 4, [9, 16, [25, 36], 49], [64, 81]]
285
Функциональное программирование
305
6.3. Цепочки функций
Задача
Применить к сложной коллекции цепочку вызовов функции.
Научившись в предыдущем разделе применять функцию к сложной коллекции,
здесь мы начнем применять к коллекции цепочку функций.
Ход программирования
Шаг 1. Создадим несколько функций и последовательно применим их к числу. Например, возьмем число 2, возведем его в квадрат: square(2)=4. Найдем число Фибоначчи от 4: fib(4)=3. Далее подсчитаем факториал: fact(3)=6. И, наконец, возведем
полученное число в куб: cube(6)=316 (листинг 6.3.1).
Листинг 6.3.1
def square(x):
return x**2
def cube(x):
return x**3
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
print(cube(fact(fib(square(2)))))
Код применения функций к числу экономен, но плохо воспринимается из-за большого количества скобок. Да и читать его приходится справа налево (именно так
применяются эти функции в математике и в программировании).
Шаг 2. Будем вызывать функции по шагам (листинг 6.3.2).
Листинг 6.3.2
...
a=2
a=square(a)
306
Глава 6
a=fib(a)
a=fact(a)
a=cube(a)
print(a)
Шаг 3. Код легко читается, но содержит повторяющиеся действия. Логично поместить его в цикл, но что мы будем в цикле перебирать? Конечно, функции (листинг 6.3.3)!
Листинг 6.3.3
...
a=2
for f in (square,fib,fact,cube):
a=f(a)
print(a)
Шаг 4. Вынесем цикл применения программы в отдельную функцию (листинг 6.3.4).
Листинг 6.3.4. Функционал вызова цепочки функций
...
def pipeline(a,*F):
for f in F:
a=f(a)
return a
print(pipeline(2,square,fib,fact,cube))
Обратите внимание, что мы передаем функции в pipeline не как список, а как последовательность аргументов. Это возможно с помощью особой языковой конструкции — функции с переменным числом аргументов. В ее списке аргументов (сигнатуре функции) сначала приводятся именованные аргументы, а дальше идет со
звездочкой имя, общее для всех остальных аргументов, которые мы можем перебирать в цикле.
Шаг 5. Применим цепочку функций к сложной коллекции. Полный текст программы приведен в листинге 6.3.5.
Листинг 6.3.5
from collections.abc import Iterable
def bind(f,M):
if isinstance(M, Iterable):
return type(M)(bind(f,m) for m in M)
else:
return f(M)
def square(x):
return x**2
Функциональное программирование
307
def cube(x):
return x**3
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
def pipeline(a,*F):
for f in F:
a=f(a)
return a
print(pipeline(2,square,fib,fact,cube))
T=[1,[2,[3]]]
print(T)
M=bind(lambda x: pipeline(x,square,fib,fact,cube),T)
print(M)
Результат:
216
[1, [2, [3]]]
[1, [216, [25733200984803950416205135028918966933548556223107595490343969725486893172557
695684023269982208000000000000000000000]]]
Шаг 6. Упростим основной код программы, перенеся вызов bind внутрь функции
pipeline (листинг 6.3.6).
Листинг 6.3.6
...
def pipeline(a,*F):
for f in F:
a=bind(f,a)
return a
print(pipeline(2,square,fib,fact,cube))
T=[1,[2,[3]]]
308
Глава 6
print(T)
M=pipeline(T,square,fib,fact,cube)
print(M)
Шаг 7. Заметим, что функции bind и pipeline делают, в сущности, общее дело:
pipeline связывает вместе несколько функций, а bind применяет функцию к коллекции со сложной структурой. Поместим функцию bind внутрь pipeline и переименуем pipeline в bind. Проверим различные варианты вызовов итоговой общей функции
(листинг 6.3.7).
Листинг 6.3.7. Применение цепочки функций к сложной коллекции
from collections.abc import Iterable
def square(x):
return x**2
def cube(x):
return x**3
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
def bind(a,*F):
def bind(f,M):
if isinstance(M, Iterable):
return type(M)(bind(f,m) for m in M)
else:
return f(M)
for f in F:
a=bind(f,a)
return a
print(bind(10,fib))
print(bind(2,square,fib,fact,cube))
Функциональное программирование
309
L=[1,[2,[3]]]
print(bind(L,square))
print(bind(L,square,fib,fact,cube))
Результат:
55
216
[1, [4, [9]]]
[1, [216, [25733200984803950416205135028918966933548556223107595490343969725486893172557
695684023269982208000000000000000000000]]]
6.4. Монады
В предыдущем разделе функции были достаточно хорошими в том смысле, что они
принимали один аргумент и вычисляли ответ для любого входного значения. На
практике это далеко не всегда бывает так. Например, функция может выдать ошибку при извлечении квадратного корня из отрицательного числа. В общем, функции
могут иметь побочный эффект. И это не обязательно ошибка.
Уже при отладке наших первых цепочек функций мы столкнулись с тем, что нужно
проверять вычисления. Хорошо бы видеть не только конечный результат, но и все
промежуточные. И здесь нам поможет то, что функция может запоминать свои действия, собирать статистику своих запусков.
Задача
Для цепочки функций вывести все промежуточные результаты вычислений.
Ход программирования
Шаг 1. Будем запускать функции последовательно, выводя их результаты на экран
(листинг 6.4.1).
Листинг 6.4.1
def square(x):
return x**2
def cube(x):
return x**3
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
def fib(x):
a=1
Результат
310
Глава 6
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
a=2
print(a)
a=square(a)
print(a)
a=fib(a)
print(a)
a=fact(a)
print(a)
a=cube(a)
print(a)
2
4
3
6
216
Шаг 2. В этой цепочке вызовов результат выводится на каждом этапе. Это может
быть неудобно, если вычислительная цепочка не сосредоточена в одном месте,
а разбросана по разным частям программы. В этом случае лучше помещать все
вычисления в список и выводить его потом разом (листинг 6.4.2).
Листинг 6.4.2
...
L=[]
a=2
L.append(a)
a=square(a)
L.append(a)
a=fib(a)
L.append(a)
a=fact(a)
L.append(a)
a=cube(a)
L.append(a)
print(L)
Результат
[2, 4, 3, 6, 216]
Шаг 3. Каждый раз после вызова функций мы помещаем их результат в список.
Почему бы не поручить это дело самим функциям?
Мы можем передавать список с промежуточными результатами в качестве дополнительного аргумента функции. Но как передавать его на выход? Хранить список
глобально — плохая идея. При одновременном запуске нескольких вычислительных цепочек списки могут быть испорчены. Воспользуемся тем, что функции
Функциональное программирование
311
в Python могут выдавать несколько результатов за один запуск (точнее, один результат, но в виде кортежа, что равносильно сразу нескольким результатам). Простой пример приведен в листинге 6.4.3.
Листинг 6.4.3
Результат
def f(x):
return x**2, x**3
a,b=f(5)
print(a,b)
25 125
Пусть функции возвращают дополненный список как второй ответ (второй элемент
кортежа-результата) — это показано в листинге 6.4.4.
Листинг 6.4.4
def square(x,L=[]):
r=x**2
L.append(r)
return r,L
def cube(x,L=[]):
r=x**3
L.append(r)
return r,L
def fact(x,L=[]):
r=1
for i in range(1,x+1):
r=r*i
L.append(r)
return r,L
def fib(x,L=[]):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
L.append(r)
return r,L
a=2
a,L=square(a)
a,L=fib(a,L)
Результат
312
a,L=fact(a,L)
a,L=cube(a,L)
print(a)
print(L)
Глава 6
216
[4, 3, 6, 216]
Шаг 4. В этой версии программы мы потеряли начальное значение a. Чтобы этого
не происходило, напишем функцию unit, которая будет инициализировать начальное значение a и помещать его в список (листинг 6.4.5).
Листинг 6.4.5
Результат
...
def unit(x,L=[]):
r=x
L.append(r)
return r,L
a,L=unit(2)
a,L=square(a,L)
a,L=fib(a,L)
a,L=fact(a,L)
a,L=cube(a,L)
print(a)
print(L)
216
[2, 4, 3, 6, 216]
Шаг 5. Если мы попробуем вызвать функции так, как это делается в математике:
cube(fact(fib(square(2))))
то произойдет ошибка. Ведь функции возвращают и, следовательно, передают
дальше по вычислительной цепи уже не отдельные числа, а кортежи. Понадобится дополнительная функция bind, которая послужит связующим звеном (листинг 6.4.6).
Листинг 6.4.6
...
def bind(f,a):
r=f(a[0])
a[1].append(r[0])
return r[0],a[1]
a,L=bind(cube,bind(fact,bind(fib,bind(square,unit(2)))))
print(a)
print(L)
Шаг 6. Код стал плохо читаемым. Напишем функцию pipeline, чтобы можно было
задавать цепочку функций путем их перечисления (листинг 6.4.7).
Функциональное программирование
313
Листинг 6.4.7
def square(x,L=[]):
r=x**2
L.append(r)
return r,L
def cube(x,L=[]):
r=x**3
L.append(r)
return r,L
def fact(x,L=[]):
r=1
for i in range(1,x+1):
r=r*i
L.append(r)
return r,L
def fib(x,L=[]):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
L.append(r)
return r,L
def unit(x,L=[]):
r=x
L.append(r)
return r,L
def pipeline(a,*F):
log=[]
for f in F:
a,log=f(a,log)
return a,log
a,L=pipeline(2,unit,square,fib,fact,cube)
print(a)
print(L)
Шаг 7. После переделки, нужной для сохранения промежуточных результатов, наши функции утратили прежнюю «чистоту» и стали похожими друг на друга. А что
если мы вернемся к исходным функциям и доверим такую их переделку новой
314
Глава 6
функции logging? Она будет принимать исходные функции как аргумент и возвращать новые функции (листинг 6.4.8).
Листинг 6.4.8
def square(x):
return x**2
def cube(x):
return x**3
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
def unit(x):
return x
def logging(f):
def res(x,L=[]):
r=f(x)
L.append(r)
return r,L
return res
logsquare=logging(square)
logcube=logging(cube)
logfact=logging(fact)
logfib=logging(fib)
logunit=logging(unit)
def pipeline(a,*F):
L=[]
for f in F:
a,L=f(a,L)
return a,L
Функциональное программирование
315
a,L=pipeline(2,logunit,logsquare,logfib,logfact,logcube)
print(a)
print(L)
Шаг 8. Если исходные функции не нужны, то мы можем не вводить новые имена
функций, а использовать старые (листинг 6.4.9).
листинг 6.4.9
...
square=logging(square)
cube=logging(cube)
fact=logging(fact)
fib=logging(fib)
unit=logging(unit)
def pipeline(a,*F):
L=[]
for f in F:
a,L=f(a,L)
return a,L
a,L=pipeline(2,unit,square,fib,fact,cube)
print(a)
print(L)
Шаг 9. Вспомним, что для записей вида:
square=logging(square)
есть более короткая форма — декоратор. Код программы с его использованием
приведен в листинге 6.4.10.
Листинг 6.4.10
def logging(f):
def res(x,L=[]):
r=f(x)
L.append(r)
return r,L
return res
@logging
def square(x):
return x**2
@logging
def cube(x):
return x**3
316
Глава 6
@logging
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
@logging
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
@logging
def unit(x):
return x
def pipeline(a,*F):
L=[]
for f in F:
a,L=f(a,L)
return a,L
a,L=pipeline(2,unit,square,fib,fact,cube)
print(a)
print(L)
Декораторы являются удобной языковой конструкцией, и мы с ними еще неоднократно встретимся в этой главе.
Шаг 10. Скопируем функцию отображения, сохраняющего структуру, из предыдущего раздела и запустим цепочку функций для составного списка (листинг 6.4.11).
Листинг 6.4.11
def logging(f):
def res(x,L=[]):
r=f(x)
L.append(r)
return r,L
return res
@logging
def square(x):
return x**2
Функциональное программирование
317
@logging
def cube(x):
return x**3
@logging
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
@logging
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
@logging
def unit(x):
return x
def pipeline(a,*F):
L=[]
for f in F:
a,L=f(a,L)
return a,L
from collections.abc import Iterable
def deepmap(f,M):
if isinstance(M, Iterable):
return type(M)(deepmap(f,m) for m in M)
else:
return f(M)
T=[1,[2,[3]]]
print(T)
M=deepmap(lambda x: pipeline (x,unit,square,fib,fact,cube), T)
print(M)
Результат:
[1, [2, [3]]]
[(1, [1, 1, 1, 1, 1]), [(216, [2, 4, 3, 6, 216]), [(2573320098480395041620513502891
8966933548556223107595490343969725486893172557695684023269982208000000000000000000000,
[3, 9, 34, 295232799039604140847618609643520000000, 25733200984803950416205135028918966
933548556223107595490343969725486893172557695684023269982208000000000000000000000])]]]
318
Глава 6
Проанализировав результат, мы увидим, что элеменами структуры стали кортежи
из значений и списков, сохраняющих промежуточные результаты, — например:
(216, [2, 4, 3, 6, 216])
Шаг 11. Упростим финальный вызов:
M=deepmap(lambda x: pipeline (x,unit,square,fib,fact,cube), T)
введя функцию bind, которая свяжет вызовы deepmap и pipeline. Сами же эти функции сделаем вложенными в bind. Полный код программы приведен в листинге 6.4.12.
Листинг 6.4.12. Монада
def logging(f):
def res(x,L=[]):
r=f(x)
L.append(r)
return r,L
return res
@logging
def square(x):
return x**2
@logging
def cube(x):
return x**3
@logging
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
@logging
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
@logging
def unit(x):
return x
Функциональное программирование
319
from collections.abc import Iterable
def bind(M,*F):
def pipeline(a,*F):
L=[]
for f in F:
a,L=f(a,L)
return a,L
def deepmap(f,M):
if isinstance(M, Iterable):
return type(M)(deepmap(f,m) for m in M)
else:
return f(M)
return deepmap(lambda x: pipeline(x,*F),M)
T=[1,[2,[3]]]
print(T)
M=bind(T,unit,square,fib,fact,cube)
print(M)
Такой тип организации вычислений: задание функции unit, цепочки функций, отображение, сохраняющее структуру, — называется монадой (термин пришел из раздела математики, который называется теория категорий).
6.5. Карринг
В программах разд. 6.3 в цепочке функций участвовали удобные функции —
с одним аргументом и возвращаемым результатом, без побочных эффектов. В предыдущем разделе мы научились выстраивать цепочку из функций, возвращающих
кортеж. Но наиболее распространенным случаем является ситуация, когда функции
имеют много аргументов.
Задача
Научиться встраивать в вычислительную цепочку функции от многих аргументов
путем подстановки значений во все аргументы, кроме одного.
Ход программирования
Шаг 1. Возьмем цепочку из разд. 6.3:
bind(2,square,fib,fact,cube)
и заменим функции возведения в куб и в квадрат на одну функцию с двумя аргументами — возведение в степень. Мы умеем сокращать количество аргументов
с помощью функционала partial (листинг 6.5.1).
320
Глава 6
Листинг 6.5.1
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
def bind(a,*F):
for f in F:
a=f(a)
return a
def power(a,n):
return a**n
from functools import partial
square=partial(power,n=2)
cube=partial(power,n=3)
print(bind(2,square,fib,fact,cube))
Шаг 2. Чтобы не делать специальные функции square и cube, можно организовать
частичное применение прямо в bind (листинг 6.5.2).
Листинг 6.5.2. Использование цепочки функций с частичным применением
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
def fib(x):
a=1
b=1
r=1
Функциональное программирование
321
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
def bind(a,*F):
for f in F:
a=f(a)
return a
def power(a,n):
return a**n
print(bind(2,partial(power,n=2),fib,fact,partial(power,n=3)))
Но так цепочка функций утратила свою лаконичность.
Шаг 3. Более изящным решением будет применить карринг — преобразование
функции от двух аргументов в функцию от одного аргумента, результат которой —
функция от одного аргумента. На этом шаге мы разберемся, что такое карринг (или
вспомним, что это такое, т. к. он уже встречался нам в разд. 3.8).
Карринг очень похож на частичное применение функции, поэтому приведу простой
пример вычисления третьей степени числа 5 в вариантах с каррингом и частичным
применением (для частичного применения возьму не стандартный функционал
partial, а нашу разработку).
Программа с каррингом
Программа с частичным применением
def curry(f):
return lambda y: lambda x: f(x,y)
def partapply(f,a):
return lambda x: f(x,a)
def power(a,n):
return a**n
def power(a,n):
return a**n
curpower = curry(power)
decpower = curpower(3)
print(decpower(5))
decpower = partapply(power,3)
print(decpower(5))
Результат:
Результат:
125
125
Пока преимуществ не видно. Более того, если в частичном применении вычислительная функция формируется в два этапа, то в карринге — в три (сначала power
каррируется, потом каррированная функция запускается с показателем степени,
а затем уже — в вычислении).
322
Глава 6
Но мы можем сделать каррирование более лаконичным. Если нам не нужна некаррированная функция, то заменим curpower на power (листинг 6.5.3).
Листинг 6.5.3
def curry(f):
return lambda y: lambda x: f(x,y)
def power(a,n):
return a**n
power = curry(power)
decpower = power(3)
print(decpower(5))
Вспомним, что для записей вида:
power = curry(power)
в Python есть особая языковая конструкция — декоратор, и мы получим лаконичный код (листинг 6.5.4).
Листинг 6.5.4
def curry(f):
return lambda y: lambda x: f(x,y)
@curry
def power(a,n):
return a**n
decpower = power(3)
print(decpower(5))
Теперь по лаконичности код с каррингом сравнялся с кодом частичного применения. Поскольку decpower встречается в программе один раз, перенесем ее значение
в место вызова (листинг 6.5.5).
Листинг 6.5.5
def curry(f):
return lambda y: lambda x: f(x,y)
@curry
def power(a,n):
return a**n
print(power(3)(5))
Функциональное программирование
323
Подобный вызов функции для нас экзотичен. Мы привыкли к тому, что функция
вызывается по имени и с аргументами в круглых скобках:
f(a,b)
Познакомившись с первыми программами функционального программирования,
мы привыкли к использованию имени функции без скобок, если речь идет о ее
передаче для дальнейшего запуска в другую функцию:
g(f, a)
Теперь получается, что списков вызовов может быть несколько, и они могут идти
друг за другом:
f(b)(a)
Этим не исчерпываются возможности работы с функциями (с другими возможностями мы познакомимся в следующем разделе).
Кажется, что, применив декоратор, мы навсегда утратили функцию от двух аргументов. Но карринг обратим, и мы, при желании, можем восстановить исходную
функцию (листинг 6.5.6)!
Листинг 6.5.6. Обратимость карринга
Результат
def curry(f):
return lambda y: lambda x: f(x,y)
def uncurry(f):
return lambda x,y: f(y)(x)
@curry
def power(a,n):
return a**n
print(power(3)(5))
power = uncurry(power)
print(power(5,3))
125
125
Шаг 4. Применим карринг в вычислительной цепочке (листинг 6.5.7)
Листинг 6.5.7
def curry(f):
return lambda y: lambda x: f(x,y)
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
324
Глава 6
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
def bind(a,*F):
for f in F:
a=f(a)
return a
@curry
def power(a,n):
return a**n
print(bind(2,power(2),fib,fact,power(3)))
Сравним вычислительные цепочки в программе с частичным применением и каррингом:
bind(2,partial(power,n=2),fib,fact,partial(power,n=3))
bind(2,power(2),fib,fact,power(3))
Лаконичность и понятность карринга несомненны.
Шаг 5. У каррируемой функции аргументов может быть больше двух. В этом случае наша функция карринга не будет работать. Добавим символ * к переменной y
(теперь y означает переменное количество аргументов):
Стало
def curry(f):
return lambda *y: lambda x: f(x,*y)
Было
def curry(f):
return lambda y: lambda x: f(x,y)
Добавим функцию с тремя аргументами и проверим ее работу (листинг 6.5.8).
Листинг 6.5.8
Результат
def curry(f):
return lambda *y: lambda x: f(x,*y)
def f(a,b,c):
return a**b+c
print(f(3,2,1))
g=curry(f)
print(g(2,1)(3))
10
10
Функциональное программирование
325
Шаг 6. Мы получили то, что нам нужно: в функцию на первом этапе вызова подставляются все аргументы, кроме одного. Теперь она готова к тому, чтобы ее включить в вычислительную цепочку. Добавим функцию с тремя аргументами в наш
пример (листинг 6.5.9).
Листинг 6.5.9. Применение цепочки каррированных функций к числу
...
def bind(a,*F):
for f in F:
a=f(a)
return a
@curry
def power(a,n):
return a**n
@curry
def trif(a,b,c):
return a*b+a*c+b*c
print(bind(2,power(2),fib,fact,power(3),trif(1,1)))
Результат:
433
Шаг 7. Применим теперь вычислительную цепочку с подготовленными с помощью
карринга функциями к сложной структуре данных (листинг 6.5.10).
Листинг 6.5.10. Применение цепочки каррированных функций к сложной коллекции
def curry(f):
return lambda *y: lambda x: f(x,*y)
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
326
Глава 6
@curry
def power(a,n):
return a**n
@curry
def trif(a,b,c):
return a*b+a*c+b*c
from collections.abc import Iterable
def bind(M,*F):
def pipeline(a,*F):
for f in F:
a=f(a)
return a
def deepmap(f,M):
if isinstance(M, Iterable):
return type(M)(deepmap(f,m) for m in M)
else:
return f(M)
return deepmap(lambda x: pipeline(x,*F),M)
T=[1,[2,[3]]]
print(T)
M=bind(T,power(2),fib,fact,power(3),trif(1,1))
print(M)
Результат:
[1, [2, [3]]]
[3, [433, [514664019696079008324102700578379338670971124462151909806879394509737863451
15391368046539964416000000000000000000001]]]
Шаг 8. Кажется, мы достигли поставленной цели, но продолжим эксперименты
с каррингом, — применим карринг два раза к функции с тремя аргументами (листинг 6.5.11).
Листинг 6.5.11
Результат
def curry(f):
return lambda *y: lambda x: f(x,*y)
def f(a,b,c):
return a**b+c
print(f(3,2,1))
g=curry(f)
print(g(2,1)(3))
h=curry(curry(f))
print(h(1)(2)(3))
10
10
10
Функциональное программирование
327
Возникает идея: вызов функции от любого числа аргументов превратить в последовательность вызовов от одного аргумента:
f(a) -> f(a)
g(a,b) -> g(b)(a)
h(a,b,c) -> h(c)(b)(a)
...
Но для этого внутри функции карринга надо узнать количество аргументов (длину
сигнатуры) функции. Сделать это можно, применив библиотечную функцию
signature (с ее помощью мы получаем доступ к кортежу аргументов), как показано
в листинге 6.5.12.
Листинг 6.5.12
Результат
from inspect import signature
def f(a):
pass
def g(a,b):
pass
def h(a,b,c):
pass
sf=signature(f)
sg=signature(g)
sh=signature(h)
print(sf)
print(sg)
print(sh)
(a)
(a, b)
(a, b, c)
Сделав преобразования, найдем длину сигнатуры (листинг 6.5.13).
Листинг 6.5.13
from inspect import signature
def f(a):
pass
def g(a,b):
pass
def h(a,b,c):
pass
sf=list(signature(f).parameters)
sg=list(signature(g).parameters)
Результат
328
sh=list(signature(h).parameters)
print(sf)
print(sg)
print(sh)
print(len(sf))
print(len(sg))
print(len(sh))
Глава 6
['a']
['a', 'b']
['a', 'b', 'c']
1
2
3
Анализ сигнатуры функции является разновидностью интроспекции — возможности во время выполнения программы анализировать сам ее код. С другими разновидностями интроспекции — функциями type и isinstance, мы уже встречались.
Превратим теперь вызов функции с многими аргументами в последовательность
вызовов с одним аргументом, написав функцию deepcurry (листинг 6.5.14).
Листинг 6.5.14
from inspect import signature
def curry(f):
return lambda *y: lambda x: f(x,*y)
def deepcurry(f):
length=len(signature(f).parameters)
if length==1:
return f
else:
for i in range(length-1):
f=curry(f)
return f
def f(a,b,c):
return a**b+c
print(f(3,2,1))
g=curry(f)
print(g(2,1)(3))
h=curry(curry(f))
print(h(1)(2)(3))
q=deepcurry(f)
print(q(1)(2)(3))
Если однократное каррирование в основном коде программы уже не нужно, можно
эту функцию спрятать внутри deepcurry, а название deepcurry сократить до curry
(листинг 6.5.15).
Функциональное программирование
Листинг 6.5.15
329
Результат
from inspect import signature
def curry(f):
def curry(f):
return lambda *y: lambda x: f(x,*y)
length=len(signature(f).parameters)
if length==1:
return f
else:
for i in range(length-1):
f=curry(f)
return f
def f(a,b,c):
return a**b+c
g=curry(f)
print(f(3,2,1))
print(g(1)(2)(3))
10
10
Шаг 9. Применим теперь глубокое каррирование в программе с цепочкой функций
и сложной структурой данных. Поскольку глубокое каррирование применительно
к функции с одним аргументом возвращает ее саму, мы можем декоратор curry
применить ко всем функциям в вычислительной цепочке (листинг 6.5.16).
Листинг 6.5.16. Глубокое каррирование функций в цепочке,
применяющейся к сложной коллекции
from inspect import signature
def curry(f):
def curry(f):
return lambda *y: lambda x: f(x,*y)
length=len(signature(f).parameters)
if length==1:
return f
else:
for i in range(length-1):
f=curry(f)
return f
@curry
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
330
Глава 6
@curry
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
@curry
def power(a,n):
return a**n
@curry
def trif(a,b,c):
return a*b+a*c+b*c
from collections.abc import Iterable
def bind(M,*F):
def pipeline(a,*F):
for f in F:
a=f(a)
return a
def deepmap(f,M):
if isinstance(M, Iterable):
R=[]
for m in M:
R.append(deepmap(f,m))
return type(M)(R)
else:
return f(M)
return deepmap(lambda x: pipeline(x,*F),M)
T=[1,[2,[3]]]
print(T)
M=bind(T,power(2),fib,fact,power(3),trif(1)(1))
print(M)
Результат:
[1, [2, [3]]]
[3, [433, [5146640196960790083241027005783793386709711244621519098068793945097378634511
5391368046539964416000000000000000000001]]]
Функциональное программирование
331
Декорирование @curry функций с одним аргументом излишне, но пригодится нам
в следующем разделе, где мы добавим в функцию curry дополнительные действия.
Принудительное глубокое каррирование всех функций, даже тех, которые в этом не
нуждаются, приводит к тому, что мы в принципе отказываемся от сигнатур функций как кортежей из нескольких переменных. Это приводит к своеобразному стилю
программирования, в котором все функции вызываются поэтапно, — с количеством
этапов, равным общему числу аргументов.
6.6. Функторы
В предыдущих разделах мы научились применять цепочку функций к коллекциям
со сложной структурой. При этом мы подготавливали функции с несколькими
аргументами или результатами к тому, чтобы они могли были быть использованы
в цепочке. Но сами цепочки были простыми, линейными и представляли собой
композицию функций (последовательное применение функций к результатам вызова предыдущих функций). Например, в цепочке:
bind(T,power(2),fib,fact,power(3),trif(1)(1))
факториал применялся к результату функции вычисления чисел Фибоначчи, т. е.
мы вычисляли:
fact(fib(x))
где x — результат предыдущего вычисления.
Если же нам надо подсчитать, например,
fib(x)+fact(x)
то это простое действие делает цепочку функций нелинейной.
Задача
Сделать возможным применение к коллекциям со сложной структурой нелинейных
цепочек функций.
Ход программирования
Шаг 1. Подумаем, как в принципе можно вписать в цепочку сумму факториала и
чисел Фибоначчи. Для этого нам придется создать новую функцию (листинг 6.6.1).
Листинг 6.6.1
...
def fibplusfact(x):
return fib(x)+fact(x)
print(bind(2,power(2),fibplusfact,power(3),trif(1)(1)))
или скомпоновать функции прямо в цепочке с помощью лямбда-выражения (листинг 6.6.2).
332
Глава 6
Листинг 6.6.2
print(bind(2,power(2),lambda x: fib(x)+fact(x), power(3), trif(1)(1)))
Первый способ требует создания новой функции, что противоречит лаконичности,
к которой мы стремимся. Второй способ уничтожает простоту восприятия цепочки.
Чтобы соединять функции не только с помощью последовательной композиции,
воспользуемся возможностями объектно-ориентированного программирования, а
именно — функторами. С примером применения функтора мы уже познакомились
в разд. 5.4 (про треугольник Паскаля), где он использовался для других надобностей.
Шаг 2. Ещё раз разберемся, что такое функторы. Превратим функцию «Факториал»
в класс (листинг 6.6.3).
Листинг 6.6.3. Класс «Факториал»
class fact:
def __call__(self,x):
r=1
for i in range(1,x+1):
r=r*i
return r
Функция «Факториал»
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
print(fact(5))
f=fact()
print(f(5))
В нашей программе fact — это уже не функция вычисления факториала, а класс.
Строкой f=fact() создается экземпляр класса fact с именем f.
Далее при использовании экземпляра класса как функции:
f(5)
в классе вызывается зарезервированный метод __call__ .
Таким образом, функтор — это класс с реализацией служебного метода __call__.
Именно он запускается тогда, когда Python у объекта видит круглые скобки.
Пока нам еще непонятна польза от такого превращения функции в функтор, но
продолжим изменение кода.
В нашем случае, чтобы вычислять факториал, достаточно одного экземпляра класса. Чтобы не создавать других экземпляров факториала с дополнительными именами, сделаем у экземпляра класса то же самое имя, что и у класса. То есть подменим
класс на его экземпляр (листинг 6.6.4).
Листинг 6.6.4
class fact:
def __call__(self,x):
r=1
Функциональное программирование
333
for i in range(1,x+1):
r=r*i
return r
fact=fact()
print(fact(5))
После такой подмены разница между функцией и классом уже не видна.
Но возникает та же проблема, что и в разд. 6.3. А именно — нам надо переделывать
все имеющиеся у нас функции в функторы.
Шаг 3. Мы не будем менять все функции, а напишем класс «Функтор» с конструктором, в который будем передавать функции. Таким образом, экземплярами класса «Функтор» будут отдельные функции: «Факториал» и «Фибоначчи» (листинг 6.6.5).
Листинг 6.6.5
Результат
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
class functor:
def __init__(self,f):
self.f=f
def __call__(self,x):
return self.f(x)
funfact=functor(fact)
funfib=functor(fib)
print(funfact(5))
print(funfib(10))
120
55
Шаг 4. Естественно, чтобы не множить наименования функций, теперь нужно
подменить старые функции на соответствующие экземпляры классов. То есть
вместо:
334
Глава 6
...
funfact=functor(fact)
funfib=functor(fib)
print(funfact(5))
print(funfib(10))
напишем:
...
fact=functor(fact)
fib=functor(fib)
print(fact(5))
print(fib(10))
Но для таких записей у нас есть особая языковая конструкция — декоратор (листинг 6.6.6).
Листинг 6.6.6
class functor():
def __init__(self,f):
self.f=f
def __call__(self,x):
return self.f(x)
@functor
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
@functor
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
print(fact(5))
print(fib(10))
Получается, что декораторами могут быть не только функции, но и классы. При
декорировании функции классом она поступает в конструктор. Мы написали уже
третий декоратор в этой главе (другими были logging и curry). Получается, что
декораторы активно применяются в функциональном программировании.
Функциональное программирование
335
Шаг 5. Теперь перейдем к тому, ради чего мы превратили функции в функторы.
Помимо оператора «круглые скобки» мы можем реализовать в классе другие операторы — например, сложение и умножение (для них есть зарезервированные слова __add__ и __mul__). Будем складывать функции с помощью оператора + и умножать с помощью оператора *. Еще нам понадобится операция композиции функции
(взятия функции от результата выполнения другой функции). Используем для нее
оператор >>. Сравним старые и новые обозначения:
Новая возможность:
Старая реализация:
(f + g)(x)
f(x) + g(x)
(f * g)(x)
f(x) * g(x)
(f >> g)(x)
g(f(x))
Запрограммируем эти операторы и запустим функции в старом и новом стиле (листинг 6.6.7).
Листинг 6.6.7
class functor():
def __init__(self,f):
self.f=f
def __call__(self,x):
return self.f(x)
def __add__(self,other):
return functor(lambda x: self.f(x)+other.f(x))
def __mul__(self,other):
return functor(lambda x: self.f(x)*other.f(x))
def __rshift__(self, other):
return functor(lambda x: other.f(self.f(x)))
@functor
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
@functor
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
Результат
336
x=5
print(fact(x)+fib(x))
print((fact+fib)(x))
print(fact(x)*fib(x))
print((fact*fib)(x))
print(fact(fib(x)))
print((fib>>fact)(x))
print(fib(fact(x)))
print((fact>>fib)(x))
Глава 6
125
125
600
600
120
120
5358359254990966640871840
5358359254990966640871840
Шаг 6. Сейчас функтор работает только с функциями, имеющими один аргумент.
Добавим оператор * перед всеми x в функторе, и мы получим функтор, который
работает с функциями от любого количества аргументов. Проверим это на функциях сложения и умножения (листинг 6.6.8).
Листинг 6.6.8
Результат
class functor():
def __init__(self,f):
self.f=f
def __call__(self,*x):
return self.f(*x)
def __add__(self,other):
return functor(lambda *x: self.f(*x)+other.f(*x))
def __mul__(self,other):
return functor(lambda *x: self.f(*x)*other.f(*x))
def __rshift__(self, other):
return functor(lambda *x: other.f(self.f(*x)))
@functor
def summ(x,y):
return x+y
@functor
def mult(x,y):
return x*y
print(summ(2,3))
print(mult(2,3))
print((summ+mult)(2,3))
Поясню, что за формулу мы написали:
(summ+mult)(x,y) = sum(x,y)+mult(x,y) = x+y+x*y
Шаг 7. В записях вида:
(fact>>fib)(x)
5
6
11
Функциональное программирование
337
желательно перенести x в начало строки (как у нас и было в программах с цепочкой
функций). То есть хотелось бы делать такие записи:
x>>fact>>fib
Чтобы это стало возможным, сделаем еще один класс — «Монада», в котором реализуем оператор >> (листинг 6.6.9).
Листинг 6.6.9
class functor():
def __init__(self,f):
self.f=f
def __call__(self,*x):
return self.f(*x)
def __add__(self,other):
return functor(lambda *x: self.f(*x)+other.f(*x))
def __mul__(self,other):
return functor(lambda *x: self.f(*x)*other.f(*x))
def __rshift__(self, other):
return functor(lambda *x: other.f(self.f(*x)))
@functor
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
@functor
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
class monada:
def __init__(self,x):
self.x=x
def __rshift__(self, other):
return monada(other(self.x))
def __str__(self):
return str(self.x)
x=monada(5)
print(x>>fact)
338
Глава 6
print(x>>fib)
print(x>>(fact+fib))
print(x>>(fact*fib))
print(x>>fib>>fact)
print(x>>fact>>fib)
Для вывода на экран пришлось в монаде реализовать еще один метод: __str__, преобразующий ее в строку.
Шаг 8. Превратим в монаду не отдельные числа, а списки аргументов. Но если мы
вернемся к примеру цепочки:
(summ+mult)(x,y)
и заменим ее на:
monada(x,y)>>(summ+mult)
то получим ошибку, т. к. конструктор монады — помимо self — принимает только
один аргумент. Добавим звездочку к этому аргументу, сделав возможной работу
монады с переменным количеством аргументов. Звездочки также нужно добавить
при вызове функций от монады (отмечены в коде листинга 6.6.10 полужирным
шрифтом).
Листинг 6.6.10
class functor():
def __init__(self,f):
self.f=f
def __call__(self,*x):
return self.f(*x)
def __add__(self,other):
return functor(lambda *x: self.f(*x)+other.f(*x))
def __mul__(self,other):
return functor(lambda *x: self.f(*x)*other.f(*x))
def __rshift__(self, other):
return functor(lambda *x: other.f(self.f(*x)))
class monada:
def __init__(self,*x):
self.x=x
def __rshift__(self, other):
return monada(other(*self.x))
def __str__(self):
return str(*self.x)
@functor
def summ(x,y):
return x+y
Функциональное программирование
339
@functor
def mult(x,y):
return x*y
print(summ(2,3))
print(mult(2,3))
print(monada(2,3)>>(summ+mult))
Шаг 9. Добавим карринг, чтобы можно было подключать функции с уменьшенным
количеством аргументов. Поскольку мы каррируем каждую функцию (включая
функции с одним аргументом), уберем декоратор @functor у функций, но зато внутри функционала curry каждую анонимную функцию превратим в функтор (изменения отмечены в листинге 6.6.11 полужирным шрифтом).
Листинг 6.6.11. Нелинейные цепочки функций в виде функторов
class functor():
def __init__(self,f):
self.f=f
def __call__(self,*x):
return self.f(*x)
def __add__(self,other):
return functor(lambda *x: self.f(*x)+other.f(*x))
def __mul__(self,other):
return functor(lambda *x: self.f(*x)*other.f(*x))
def __rshift__(self, other):
return functor(lambda *x: other.f(self.f(*x)))
from inspect import signature
def curry(f):
def curry(f):
return functor(lambda *y: functor(lambda x: f(x,*y)))
length=len(signature(f).parameters)
if length==1:
return functor(lambda x: f(x))
else:
for i in range(length-1):
f=functor(curry(f))
return f
class monada:
def __init__(self,*x):
self.x=x
def __rshift__(self, other):
return monada(other(*self.x))
340
Глава 6
def __str__(self):
return str(*self.x)
@curry
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
@curry
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
@curry
def power(a,b):
return a**b
@curry
def trif(a,b,c):
return a*b+a*c+b*c
print(monada(2)>>power(2)>>(fib+fact)>>power(3)>>trif(1)(1))
Шаг 10. Применим теперь последовательность функций к коллекции со сложной
структурой (листинг 6.6.12).
Листинг 6.6.12
class functor():
def __init__(self,f):
self.f=f
def __call__(self,*x):
return self.f(*x)
def __add__(self,other):
return functor(lambda *x: self.f(*x)+other.f(*x))
def __mul__(self,other):
return functor(lambda *x: self.f(*x)*other.f(*x))
def __rshift__(self, other):
return functor(lambda *x: other.f(self.f(*x)))
Функциональное программирование
from inspect import signature
def curry(f):
def curry(f):
return functor(lambda *y: functor(lambda x: f(x,*y)))
length=len(signature(f).parameters)
if length==1:
return functor(lambda x: f(x))
else:
for i in range(length-1):
f=functor(curry(f))
return f
from collections.abc import Iterable
def deepmap(f,M):
if isinstance(M, Iterable):
R=[]
for m in M:
R.append(deepmap(f,m))
return type(M)(R)
else:
return f(M)
class monada:
def __init__(self,*x):
self.x=x
def __rshift__(self, other):
return monada(deepmap(other,*self.x))
def __str__(self):
return str(*self.x)
@curry
def fact(x):
r=1
for i in range(1,x+1):
r=r*i
return r
@curry
def fib(x):
a=1
b=1
r=1
for i in range(3,x+1):
r=a+b
a=b
b=r
return r
341
342
Глава 6
@curry
def power(a,b):
return a**b
@curry
def trif(a,b,c):
return a*b+a*c+b*c
M=[1,[2,[3]]]
print(monada(M)>>power(2)>>(fib+fact)>>power(3)>>trif(1)(1))
Выводы по главам 3, 4 и 6
Мы достигли поставленной цели, решив обобщенную задачу, сформулированную
в разд. 6.2. А именно — сделали отображение, сохраняющее внутреннюю структуру коллекции. Отображение является цепочкой специально подготовленных для
этого (каррированных) функций, соединенных вместе с помощью алгебраических
формул.
Кажется, что программа выглядит сложной. Но на самом деле, чтобы оценить
сложность программы, нужно смотреть на последние две строки: на коллекцию со
сложной структурой и на формулу соединения функций. Они выглядят просто. Все
остальное: функтор, глубокое отображение, карринг, монаду — можно импортировать из готовых библиотек для функционального программирования.
Сами не заметив, мы погрузились в очень абстрактный раздел математики, который
называется теория категорий. Этот раздел изучают только на хороших математических специальностях в вузах (и, между нами говоря, эту теорию мало кто понимает). Решив обобщенную задачу, мы на интуитивном уровне усвоили некоторые
понятия теории категорий, а именно: функторы и монады.
Но приемы программирования, которые мы здесь изучили, нужны не только математикам. Они применяются во вполне реальных технических проектах. Представьте распределенную систему. В ней управляющему узлу на обработку передаются
данные, которые могут иметь самую разнообразную структуру. А управляющий
узел соединяет узлы-обработчики в цепочку, по которой будут передаваться и изменяться данные, общая же структура данных останется неизменной.
Функциям в этой книге, как и в книге «Python. 12 уроков для начинающих», были
посвящены три главы. Введение всего двух языковых конструкций: именованных и
анонимных функций — породило несколько стилей программирования, в которых
программы выглядят так, как будто они написаны на разных языках:
1. Программы, декомпозированные в функции. Программа представляет собой
множество функций, которые вызывают друг друга. Сами же функции — это
либо математические формулы, либо законченные короткие алгоритмы в виде
иерархии вложенных блоков условий и циклов.
2. Динамическое программирование. Программа не только является множеством
функций, но эти функции вызывают друг друга и самих себя (рекурсии, взаим-
Функциональное программирование
343
ные рекурсии, удаленные рекурсии). Циклы в программе могут отсутствовать
вообще, их заменяют рекурсивные вызовы функций.
3. Функциональное программирование. Программа в идеале является одной строкой — формулой, в которой функции обращаются друг к другу, причем функции обрабатывают не только числа и другие типы данных, но и другие функции
(а также самих себя).
4. Программирование в стиле теории категорий. Программа — это множество каррированных функций, объединенных в вычислительные цепочки алгебраическими формулами. С помощью одной и той же цепочки могут обрабатываться
данные, структурированные по-разному.
В последнем разделе этой главы мы вышли за пределы функционального программирования и использовали объектно-ориентированное программирование (классы
«Функтор» и «Монада»). Здесь объектно-ориентированное программирование
играло подчиненную функциональному программированию роль — мы задействовали в качестве коллекций со сложной структурой списки с разными уровнями
вложенности. Но не всегда это так: объектно-ориентированное программирование
и функциональное — это две равноправные части мультипарадигменного программирования, и вы можете сами придумать задачи на отображение сложных коллекций (гиперкуба, родословного древа, матрицы, сложной геометрической фигуры),
с которыми познакомились в этой книге или в книге «Python. 12 уроков для начинающих».
344
Глава 6
ГЛАВА
7
Сюрреализм
Решив задачу поиска гиперкуба в многомерном пространстве, причем решив ее
с использованием почти всего арсенала языковых конструкций Python, и сделав
отображения сложных коллекций с помощью функторов, можно было бы и закончить эту книгу. Но в конце я хочу показать вам абсурдные, непонятно зачем нужные, но вполне работоспособные программы. Программисты тоже умеют шутить.
7.1. Фрактальные списки
Создадим список из двух элементов: left и right:
left=int(input())
right=int(input())
L=[left,right]
Добавим в середину списка новый элемент — этот же самый список!
L.insert(1,L)
Запустим программу (листинг 7.1.1) и посмотрим, что получится.
Заметим, что к граничным элементам мы всегда имеем доступ.
Листинг 7.1.1. Фрактальный список
left=int(input())
right=int(input())
L=[left,right]
L.insert(1,L)
print(L)
print(L[1])
print(L[1][1])
print(L[1][1][1])
print(L[0])
print(L[1][0])
print(L[1][1][0])
print(L[1][1][1][0])
Результат
1
2
[1,
[1,
[1,
[1,
1
1
1
1
[...],
[...],
[...],
[...],
2]
2]
2]
2]
346
Глава 7
7.2. Фрактальный словарь
Мы уже знаем, что значением в словаре может быть сложная структура данных —
например, список или даже другой словарь. Поместим в словарь этот же самый
словарь (листинг 7.2.1)!
Листинг 7.2.1. Фрактальный словарь
V={'V':'V'}
V['V']=V
print(V)
print(V['V'])
print(V['V']['V'])
print(V['V']['V']['V'])
Результат
{'V':
{'V':
{'V':
{'V':
{...}}
{...}}
{...}}
{...}}
7.3. Бесконечные вызовы функции
В функциональном программировании мы имеем дело с функциями высших порядков, которые принимают в качестве аргумента другие функции или возвращают
функции в качестве ответа.
Сделаем функцию, которая возвращает как ответ саму себя:
def f():
return f
f()()()
Теперь мы можем ее вызывать бесконечное число раз подряд.
Чтобы функция не была совсем бесполезной, пусть она помещает свой аргумент
в список (листинг 7.3.1).
Листинг 7.3.1. Функция с неограниченным количеством вызовов
Результат
L=[]
def f(n):
L.append(n)
return f
f(1)
f(2)(3)
f(4)(5)(6)
print(L)
[1, 2, 3, 4, 5, 6]
Просто хранить число в списке не интересно. Добавим в функцию еще один аргумент — другую функцию, которую мы будем применять к первому аргументу (листинг 7.3.2).
Сюрреализм
347
Листинг 7.3.2
Результат
L=[]
def f(n=0,g=lambda n:n):
L.append(g(n))
return f
f()(2,lambda n:n**2)(2,lambda n:n**3)(2,lambda n:n**4)
print(L)
[0, 4, 8, 16]
7.4. Функтор с бесконечными вызовами
Доработаем функтор из разд. 5.4 так, чтобы его можно было запускать с неограниченным количеством квадратных и круглых скобок.
Пусть функция square возвращает саму себя, а результат сохраняет в списке, как это
делалось в предыдущем разделе (листинг 7.4.1).
Листинг 7.4.1. Функтор с неограниченным
количеством вызовов
Результат
class functor:
def __init__(self,f=lambda n:n):
self.f=f
def __call__(self,n):
return self.f(n)
def __getitem__(self,n):
return self.f(n)
Square=[]
@functor
def square(n):
Square.append(n*n)
return square
square(1)
square[2]
square(3)(4)
square[5][6]
square(7)[8](9)[10]
print(Square)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Так что же такое square: список, функция или класс?
348
Глава 7
Заключение
Если вы перед чтением этой книги познакомились с моей книгой «Python. 12 уроков для начинающих» или занимались этим уже с некоторым предварительным
опытом изучения Python, то вы, можно сказать, учились программировать на
Python второй раз. Мы вновь прошлись по всем разделам языковых конструкций,
вспомнили старые приемы программирования и изучили несколько новых. Мы
решили старые задачи новыми методами и новые задачи старыми методами. Как я
уже упоминал, процесс обучения итеративен. И мы с вами сделали второй заход
в изучении программирования, пройдя большой путь: от поиска корней квадратного уравнения до поиска гиперкуба в многомерном пространстве, от расчета, можно
ли кирпич просунуть в дыру, до многоэтапных отображений коллекций, сохраняющих внутреннюю структуру.
Сколько же нужно сделать заходов, чтобы научиться программировать на Python?
Я назвал эту книгу «Python. Красивые задачи для начинающих» и упомянул, какие
же задачи я считаю красивыми — те, которые имеют несколько вариантов решений
и предлагают несколько вариантов усложнений, чем формируют алгоритмическое
мышление. Вы сможете воспользоваться приобретенными навыками в дальнейшем.
Какую же задачу я считаю самой красивой?
А вот ее я в этой книге привел только мимоходом (это числа Фибоначчи)... потому
что ей целиком будет посвящена моя третья книга по программированию на Python.
В ней на примере всего одной задачи мы сделаем третий заход изучения программирования. Она тем и уникальна, что ее можно решить множеством способов с использованием разнообразных приемов программирования и практически всех языковых конструкций. Это действительно самая красивая программистская задача.
Предметный указатель
А
Абстракция 231
Агрегация 239
Анонимная функция 63, 67, 112, 290, 342
Апертура линейного фильтра 73
Аргументы 231
Атрибут 234
Б
Бустрофедон 77
Бэктрекинг 119, 125, 128, 134, 143, 155,
180, 186
В
Взаимная рекурсия 173
Вложенная функция 293
Г
Гексация 92
Гиперкуб 266
Гипероператоры 91, 92
Гиперсфера 208
Глобальный объект 260
Граф 219
Д
Декоратор 261, 264, 315, 322
Динамика по подотрезкам 164, 173
Динамическое программирование 173, 176
З
Замыкание 114
◊ функции 115
Захват
◊ переменной 114, 291
◊ функции 292
И
Индексатор 269
Инкапсуляция 234
Интроспекция 328
К
Карринг 321
Каррирование (карринг) 116
Класс 231
Коллекция 23
Комментарий 34
Комплексные числа 16
Конструктор 231
Кортеж 20
Л
Лог 106
Лямбда-выражение 331
М
Мемоизация (кеширование) 174, 261
Метод 234
◊ бинарного поиска 163
◊ линейного поиска 162
352
Множество 53
Монада 319, 338
Н
Накапливающаяся сумма 31
Наследование 256
О
Обратная индексация 30
Объект 231
Ориентированный граф 225
Отступы 15
П
Палиндром 199
Парадигма программирования 63, 113
Пентация 92
Переключатель 78
Р
Рекуррентная формула 26
Рекурсивная структура данных 239
Рекурсия 87, 119, 130, 132, 134, 141, 143,
156, 160, 176, 180, 186
◊ высшего порядка 99
Решето
◊ Сундарама 65
◊ Эратосфена 57
С
Свойство 231
Седловина матрицы 40
Сигнатура функции 306
Синтаксический сахар 20, 26
Словарь 47, 220
Список 21
Списочное выражение 37
Предметный указатель
Среда разработки IDLE Python 14
Срез 29
Стек 52
◊ LIFO 52
Структурное программирование 23
Счетчик со сбросом 26
Т
Теория категорий 288, 319, 342
Тетрация 92
Треугольник Паскаля 257
Ф
Факториал 331
Флаг 45, 253
Функтор 262, 332, 347
Функционал 85, 111, 290
Функциональное программирование 297
Функция 63
◊ высшего порядка 85, 111, 290
◊ с переменным числом аргументов 306
Х
Ханойские башни 102
Хеш-функция 256
Ц
Цикл 24
Ч
Частичное применение функции 114, 294
Числа Фибоначчи 331
Э
Экземпляр класса 231