Автор: Порублев И.Н.   Ставровский А.Б.  

Теги: компьютерные технологии  

ISBN: 978-5-8459-1244-2

Год: 2007

Текст
                    И.Н. Порублёв, А.Б. Ставровский
АЛГОРИТМЫ
И ПРОГРАММЫ
РЕШЕНИЕ ОЛИМПИАДНЫХ ЗАДАЧ
пввавиа и  и
1ааааваааааав«вааааааав8а
 - -да* ш
йшлЕктши
dialektika.com
АЛГОРИТМЫ И ПРОГРАММЫ РЕШЕНИЕ ОЛИМПИАДНЫХ ЗАДАЧ
И.Н. Порублёв, А.Б. Ставровский
Москва • Санкт-Петербург • Киев 2007
ББК 32.973.26-018.2.75
П60
УДК 681.3.07
Компьютерное издательство “Диалектика” Зав. редакцией А.В. Слепцов
По общим вопросам обращайтесь в издательство “Диалектика” по адресу: info@dialektika.com, http://www.dialektika.com 115419, Москва, а/я 783; 03150, Киев, а/я 152
Порублев, И.Н., Ставровский, А.Б.
П60 Алгоритмы и программы. Решение олимпиадных задач — М. : ООО “И.Д. Вильямс”, 2007. — 480 с.: ил.
ISBN 978-5-8459-1244-2 (рус.)
Данная книга ориентирована на старшеклассников и студентов младших курсов, желающих подготовиться к олимпиадам или экзаменам по программированию. Ее могут использовать и учителя информатики, и все те, кого интересует решение нестандартных алгоритмических задач.
В книге обсуждаются методы решения различных задач полтрограммированию, знание которых будет полезно во многих ситуациях. Затронуты также технические вопросы: структурное кодирование и использование подпрограмм, элементы стиля, отладки и тестирования, использование режимов компиляции, организация' ввода данных. Особое внимание уделено анализу сложности алгоритмов.
Книга будет полезна всем, кто учится программировать — именно учится программировать, а не изучает языки программирования.
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и запись на магнитный носитель, если на это нет письменного разрешения издательства “Диалектика”.
Copyright © 2007 by Dialektika Computer Publishing.
AU rights reserved including the right of reproduction in whole or in part in any form.
ISBN 978-5-8459-1244-2 (pyc.)
© Компьютерное изд-во “Диалектика”, 2007, текст, оформление, макетирование
Оглавление
Предисловие	13
Глава 1. Разминка (понемногу о разном)	17
Глава 2. Однопроходные алгоритмы	47
Глава 3. Рекурсия	79
Глава 4. Нестандартная обработка чисел	97
Глава 5. Бинарный поиск, слияние и сортировка	127
Глава 6. Вычислительная геометрия на плоскости	159
Глава 7. Выметание	195
Глава 8. Графы	211
Глава 9. Графы клеток и графы с нагруженными ребрами	243
Глава 10. Комбинаторика	279
Глава И. Перебор вариантов	309
Глава 12. Жадные алгоритмы	333
Глава 13. Динамическое программирование	347
Глава 14. Игры двух лиц	387
Глава 15. Японский кроссворд	403
Приложение А. Указания по решению упражнений	427
Список литературы	469
Предметный указатель	471
Содержание
Предисловие
От издательства “Диалектика”
Глава 1. Разминка (понемногу о разном)
1.1.	Три простые задачи
1.1.1.	Совпадения стрелок часов
1.1.2.	Последовательности с одинаковыми суммами
1.1.3.	Ребус
1.2.	Знакомство со сложностью алгоритмов
1.2.1.	Простые и составные числа
1.2.2.	Понятие сложности алгоритма
1.2.3.	Характер возрастания сложности
1.2.4.	Алгоритм Евклида и его современная версия
1.2.5.	Бинарный алгоритм
1.2.6.	Понятие сложности задачи
1.2.7.	Что выбирать?
1.3.	Несколько технических вопросов
1.3.1.	Проектирование сверху вниз, подпрограммы и структурное кодирование
1.3.2.	Когда уместны безусловные переходы
1.3.3.	Несколько замечаний о стиле
1.3.4.	Отладка программы
1.3.5.	Директивы компилятору
1.3.6.	Проверка программы
1.4.	Ввод последовательностей данных
1.4.1.	Организация данных и вид цикла ввода
1.4.2.	Изменение источника данных
Упражнения
Глава 2. Однопроходные алгоритмы
2.1.	Попутные вычисления
2.1.1.	Три простых примера
2.1.2.	Максимальная сумма отрезка числовой последовательности
2.1.3.	Инопланетная армия
2.1.4.	Стрельба из двуствольной пушки
2.2.	Чтение и обработка символов
2.2.1.	Удаление пробелов
2.2.2.	Удаление комментариев
СОДЕРЖАНИЕ
7
2.2.3.	Чтение и вычисление многочлена	59
2.2.4.	Языки скобок	67
2.2.5.	Линейный поиск подстроки в тексте	72
Упражнения	76
Глава 3. Рекурсия	79
3.1.	Основные понятия	79
3.1.1.	Рекурсивные определения	79
3.1.2.	Простейший пример рекурсивной подпрограммы	80
3.1.3.	Глубина рекурсии и общее количество рекурсивных вызовов	81
3.1.4.	Косвенная рекурсия	83
3.2.	Быстрое возведение в степень	85
3.3.	Рисование самоподобных ломаных	88
3.3.1.	Снежинка Коха	89
3.3.2.	Треугольник Серпиньского	90
3.3.3.	Драконова ломаная	93
Упражнения	95
Глава 4. Нестандартная обработка чисел	97
4.1.	Длинная целочисленная арифметика	97
4.1.1.	Представление длинных чисел	98
4.1.2.	Сравнение, сложение и вычитание длинных целых	100
4.1.3.	Организация ввода-вывода	102
4.1.4.	Умножение длинных целых	103
4.1.5.	Деление длинных целых	105
4.1.6.	Целая часть квадратного корня длинного числа	107
4.2.	Два магических числа	109
4.2.1.	Число е	109
4.2.2.	Число л	112
4.3.	Остатки от деления	112
4.3.1.	Плиты в треугольнике	113
4.3.2.	Кратное число с одинаковыми цифрами	115
4.4.	Отслеживание циклических повторений	116
4.4.1.	Десятичное представление дроби и />алгоритм	116
4.4.2.	Остатки от деления чисел Фибоначчи	119
4.5.	Нули в конце факториала	121
Упражнения	124
Глава 5. Бинарный поиск, слияние и сортировка	127
5.1.	Бинарный поиск	127
5.1.1.	Идея бинарного поиска	127
5.1.2.	“Оптический танк”	128
5.2.	Слияние упорядоченных последовательностей	130
5.2.1.	Слияние двух участков массива	130
5.2.2.	Слияние файлов	131
8
СОДЕРЖАНИЕ
5.3.	Основные способы сортировки	135
5.3.1.	Два простейших алгоритма	135
5.3.2.	Сортировка слиянием	137
5.3.3.	Быстрая сортировка	140
5.3.4.	Пирамидальная сортировка	142
5.3.5.	Линейная сортировка подсчетом	145
5.3.6.	Поразрядная сортировка	148
5.4.	Применение сортировки	150
5.4.1.	Проверка уникальности	150
5.4.2.	Проход в заборе	151
5.4.3.	Транзитивность	153
Упражнения	154
Глава 6. Вычислительная геометрия на плоскости	159
6.1.	Точки, векторы, прямые, отрезки	159
6.1.1.	Точки, векторы, углы и ориентированная площадь	159
6.1.2.	Представление прямых и отрезков	162
6.1.3.	Взаимное расположение прямых, отрезков и точек	164
6.1.4.	Две задачи о треугольниках	169
6.2.	Многоугольники (полигоны)	173
6.2.	J. Основные определения	173
6.2.2.	Площадь полигона	174
6.2.3.	Принадлежность точки полигону	176
6.2.4.	Принадлежность точки выпуклому полигону	177
6.2.5.	Построение полигонов	178
6.2.6.	Сумма и разность полигонов	182
6.3.	Окружности и круги	185
6.3.1.	Прямая и круг	185
6.3.2.	Отрезок и окружность	186
6.3.3.	Общие касательные	187
6.3.4.	Пересечение двух кругов	188
Упражнения	191
Глава 7. Выметание	195
7.1.	Интернет-провайдер	195
7.2.	Мера объединения отрезков	199
7.3.	Линия горизонта	201
7.4.	Мера объединения треугольников	205
Упражнения	209
Глава 8. Графы	211
8.1.	Графы и способы их представления	211
8.1.1.	Неориентированные графы: основные понятия	211
8.1.2.	Ориентированные графы	213
8.1.3.	Представления графа	214
СОДЕРЖАНИЕ
9
8.1.4.	Пример: задача о центре дерева	215
8.2.	Алгоритмы обхода графов	221
8.2.1.	Обход в глубину	221
8.2.2.	Обход в ширину	224
8.2.3.	Реализация очереди	225
8.3.	Применение алгоритмов обхода	226
8.3.1.	Построение остовного дерева и остовного леса	226
8.3.2.	Расстояния между вершинами	228
8.3.3.	Проверка ацикличности и топологическая сортировка ациклического орграфа	230
8.3.4.	Эйлеровы циклы и цепи	233
8.3.5.	Обход графа достижимых состояний	236
Упражнения	240
Глава 9. Графы клеток и графы с нагруженными ребрами	243
9.1.	Графы на клетчатых полях	243
9.1.1.	Фигуры на клетчатом поле	243
9.1.2.	Минимальный путь в лабиринте	246
9.1.3.	Подсчет клеток в областях	252
9.2.	Остовное дерево минимального веса	258
9.3.	Алгоритм Дейкстры и его применение	261
9.3.1.	Задача с одним источником и положительным весом ребер	261
9.3.2.	Максимальный груз	264
9.3.3.	Зал Круглых Столов	265
9.4.	Скоростная алхимия	270
Упражнения	274
Глава 10. Комбинаторика	279
10.1.	“Амебы” комбинаторики	280
10.1.1.	Правила суммы и произведения	280
10.1.2.	Перестановки, размещения и сочетания без повторений	281
10.1.3.	Перестановки, размещения и сочетания с повторениями	282
10.1.4.	Размещения и сочетания как отображения	284
10.1.5.	Биномиальные коэффициенты	285
10.2.	Рекуррентные соотношения и таблицы	286
10.2.1.	Пути в квадратных кварталах	286
10.2.2.	Правильные скобочные выражения	288
10.2.3.	Счастливые билеты	289
10.2.4.	Белые и черные кубики	292
10.2.5.	Велосипедные гонки	295
10.3.	Рекурсия в задаче о русском лото	296
10.4.	Включения и исключения	299
10.4.1.	Принцип включений и исключений	299
10.4.2.	“Батарея, огонь!”	301
10.4.3.	Беспорядок в шляпах	303
10.5.	Количество раскладок и разбиений	304
10
СОДЕРЖАНИЕ
10.5.1.	Разбиения множества	304
10.5.2.	Разбиения множества с учетом порядка классов	305
10.5.3.	Разбиения числа на слагаемые	306
Упражнения	307
Глава 11. Перебор вариантов	309
11.1.	Порождение подмножеств	309
11.1.1.	Все подмножества	309
11.1.2.	Подмножества с заданным числом элементов	313
11.2.	Порождение последовательностей	315
11.2.1.	Размещения ферзей	315
11.2.2.	Дерево размещений и его обход	317
11.2.3.	Обходдерева с помощью магазина	318
11.2.4.	Порождение всех перестановок	320
11.3.	Попытки сократить перебор	322
11.3.1.	Подмножества положительных чисел с заданной суммой	323
11.3.2.	Псевдополиномиальный приближенный алгоритм поиска подмножества	324
11.3.3.	Идея метода ветвей и границ в задаче коммивояжера	325
11.3.4.	Решение задачи коммивояжера методом ветвей и границ	326
11.3.5.	Упрощенный алгоритм	327
11.4.	Послесловие	328
Упражнения	329
Глава 12. Жадные алгоритмы	333
12.1.	Знакомство с жадными алгоритмами	333
12.1.1.	Быстрый выбор упорядоченных вариантов	333
12.1.2.	Сортировка и выбор в динамическом множестве	335
12.1.3.	Понятие жадного алгоритма	338
12.2.	Матроиды и жадные алгоритмы	339
12.2.1.	Понятие матроида	339
12.2.2.	Жадный поиск допустимого подмножества с максимальным весом 340
12.2.3.	Взвешенный матроид и жадный алгоритм	340
12.2.4.	Матричный матроид	341
12.3.	Некорректная “жадность” вместо перебора	342
12.3.1.	Поспешная укладка рюкзака	342
12.3.2.	Распределение заданий	343
Упражнения	344
Глава 13. Динамическое программирование	347
13.1.	Принцип оптимальности	347
13.1.1.	Путь по клеткам с максимальной суммой	347
13.1.2.	Общие замечания по методологии динамического программирования 351
13.1.3.	Количество путей с суммой, близкой к максимальной	353
13.2.	Монотонная подпоследовательность	358
СОДЕРЖАНИЕ
13.2.1.	Поиск монотонной подпоследовательности
13.2.2.	Бинарный поиск начала подпоследовательности
13.2.3.	Вложенные коробки
13.3.	Табличная техника и рекурсия с запоминанием
13.3.1.	Расстановка скобок в произведении матриц
13.3.2.	Минимальное количество монет
13.3.3.	Разбиение алфавита
13.3.4.	Абзац с блоками разной высоты
13.3.5.	Максимальное значение выражения
13.3.6.	Вычеркивание из строки
Упражнения
Глава 14. Игры двух лиц
14.1.	Анализ позиций и выбор хода
14.1.1.	Выигрышные и проигрышные позиции
14.1.2.	Золотое сечение
14.1.3.	Ним
14.1.4.	Таблица ходов
14.2.	Оценивание позиций: максимальная сумма
Упражнения
Глава 15. Японский кроссворд
15.1.	Итерационный анализ линий
15.1.1.	Постановка задачи и основные идеи решения
15.1.2.	Ввод, вывод и основные структуры данных
15.1.3.	Реализация итерационного анализа линий
15.2.	Анализ линии на основе конечного автомата
15.2.1.	Описание линии в виде конечного автомата
15.2.2.	Обработка линии и уточнение клеток
15.2.3.	Реализация
15.3.	Решение задачи с помощью перебора
15.3.1.	Итерационный анализ линий не решает задачу
15.3.2.	Перебор и исследование состояний клеток с помощью ИАЛ
15.3.3.	Решение задачи и анализ решения
Приложение А. Указания по решению упражнений
Глава 1
Глава 2
Глава 3
Глава 4
Глава 5
Глава 6
Глава 7
Глава 8
Глава 9
12	СОДЕРЖАНИЕ
Глава 10 Глава 11 Глава 12 Глава 13 Глава 14	450 455 459 461 464
Список литературы	469
Предметный указатель	471
Предисловие
Для кого предназначена эта книга
Данная книга ориентирована, в основном, на старшеклассников и студентов младших курсов, желающих подготовиться к соревнованиям по программированию. Ее могут использовать учителя информатики в школе, которых интересует решение нестандартных алгоритмических задач. Она также может быть полезной всем, кто учится программировать. Именно учится программировать, а не изучает языки программирования.
О чем эта книга
Главная тема данной книги — построение и анализ программ, работающих по возможности рационально и быстро. Этой теме, фундаментальной для всего программирования, посвящено немало книг (например, классические [3, 9, И, 13, 34, 35] и их аналоги [4,10,18-23, 31, 32, 36, 37, 42], изданные в последние годы).
Эффективное программирование играет ключевую роль в решении подавляющего большинства занимательных задач, особенно на соревнованиях. Занимательные задачи по программированию — не новая тема в литературе (см., например, [2, 7]). Интерес к ней не убывает — наоборот, книги, посвященные решению занимательных задач, в последние годы стали издаваться чаще (см., например, [15, 16, 26, 27, 33, 38]). Авторы надеются, что данная книга достойно продолжит этот ряд.
Структуру и содержание книги определяют, в первую очередь, методы решения задач, знание которых полезно во многих ситуациях. Затронуты также технические вопросы: структурное кодирование и использование подпрограмм, элементы стиля, отладки и тестирования, использование режимов компиляции, организация ввода данных. Особое внимание уделено анализу сложности алгоритмов.
Многие из задач, представленных здесь, встречались на соревнованиях по программированию разных лет, мест, уровней и форматов проведения, но указаны авторы только некоторых из них. Мы старались указать авторство задачи, когда были достаточно в нем уверены, а сама задача не публиковалась прежде в известных книгах. Установить истинных авторов олимпиадных и занимательных задач обычно очень трудно (каждый из авторов данной книги не однажды, придумывая задачи, обнаруживал, что они уже были придуманы кем-то еще...). Поэтому заранее приносим извинения за возможные неточности и отсутствие ссылок на олимпиады, на которых “играли” те или иные задачи.
Распределение материала по главам отражено в оглавлении и содержании. Авторы старались размещать материал по порядку от простого к сложному. Это касается как глав в целом, так и их разделов. Поэтому обычно нестрашно, если при первом чтении отдельные фрагменты покажутся слишком сложными — многие из них можно пропустить и спокойно переходить к следующей главе (разделу или подразделу главы). Если пропущенный материал понадобится в дальнейшем, читатель увидит это по перекрестным ссылкам. Но лучше все-таки стараться читать все по порядку.
14
ПРЕДИСЛОВИЕ
Для записи алгоритмов в основном использован язык Turbo Pascal, доступный практически всем, изучающим программирование, но это не значит, что читателю рекомендуется только его и использовать. Применение более нового компилятора, например Free Pascal, существенно облегчает решение некоторых задач. Как правило, в книге об этом сказано и описано, в чем состоит выигрыш (объем используемой памяти, представимость чисел в стандартных типах, возможность перегрузки операций, возможность выдачи предупреждений при компиляции и т.д.). Указаны и ситуации, в которых более удачные решения получаются при использовании C++.
Невозможно научиться программировать, только читая готовые чужие программы, даже с объяснениями. Необходимо еще писать программы самому. Поэтому в конце каждой главы приведены упражнения для самостоятельной работы. Кроме того, некоторые задачи в основном тексте имеют задания, связанные с модификацией постановок задач или алгоритмов их решения. Будьте внимательны, глядя на перекрестные ссылки, — они могут указывать как на задачи, так и на упражнения. Для всех упражнений в конце книги есть указания по решению (различные по степени подробности).
Благодарности
Авторы благодарны многим людям, с которыми в разные годы случалось взаимодействовать в связи с олимпиадами, занимательными задачами и нестандартными алгоритмами. Среди них: Вячеслав Гальперин и Виктор Бардадым — идейный и организационный вдохновители одного из авторов; Сергей Жук — неформальный учитель другого автора; Виталий Бондаренко, Шамиль Ягияев, Сергей Раков и другие члены жюри УОИ; Юрий Пасихов, Галина Кравец и другие организаторы и члены жюри NetOI; Юрий Зайцев и Валентин Нечаев — финалисты командных студенческих соревнований под эгидой АСМ. Авторы также благодарны главному редактору газеты “Информатика” (г. Киев) Наталии Вовковинской за опубликование предыдущих материалов и Александру Шеню (автору работы [42]) за конструктивную критику раннего прототипа данной книги.
Авторы также выражают благодарность: участникам олимпиад — без них эта работа во многом теряет смысл; создателям Интернет-ресурсов [43—51] — за возможность обсуждать алгоритмы и быть в курсе новостей и событий; коллегам и учителям из разных городов и стран, готовящим учеников к олимпиадам — при встречах они делятся своим бесценным практическим опытом.
Обратная связь
Возможно, у вас возникнут замечания, предложения или пожелания, адресованные авторам. Сообщите о них! Не исключено, что, вопреки всем стараниям авторов, в книгу вкрались смысловые ошибки и опечатки. Тем более сообщите! Этим вы внесете свой вклад и в улучшение данной книги, и в наше общее дело — ‘обучение одаренной молодежи алгоритмам и программированию.
Предлагаем сосредоточить обсуждение данной книги на форуме booksfo-rum.olymp.vinnica.ua. Разумеется, если вы уверены, что нашли ошибку, сначала посмотрите предыдущие сообщения на форуме — вдруг эта ошибка уже была найдена.
ПРЕДИСЛОВИЕ
15
Условные обозначения
В книге использованы стили и отметки, акцентирующие внимание на некоторых моментах материала.
Определения терминов и некоторые формулировки. Определяемые термины выделены курсивом.
• Информация, играющая особо важную роль.
* Технические подробности, связанные с реализацией алгоритмов.
► Доказательства утверждений. <
Н Задания по самостоятельному завершению работы над программой или модификации уже решенной задачи и алгоритма ее решения.
От издательства “Диалектика”
Вы, читатель этой книги, и есть главный ее критик. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересны любые ваши замечания в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится ли вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас.
Отправляя письмо или сообщение, не забудьте указать название книги и ее авторов, а также свой обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию новых книг.
Наши электронные адреса:
E-mail: info@dialektika.com
WWW: http://www.dialektika.com
Наши почтовые адреса:
в России: 115419, Москва, а/я 783
в Украине: 03150, Киев, а/я 152
Глава 1
Разминка (понемногу о разном)
В этой главе...
♦	Простые задачи, одни программы решения которых выполняются долго, а другие — быстро
♦	Размер данных, временная сложность алгоритма и характер возрастания сложности
♦	Сложность определения, является ли число простым
♦	Алгоритм Евклида и другие алгоритмы вычисления НОД
*	Несколько слов о технике разработки программы
♦	Что помогает проводить отладку и проверку программы
♦	Организация циклов ввода данных из текстов
1.1. Три простые задачи
1.1.1. Совпадения стрелок часов
Задача 1.1. Стрелки часов движутся с постоянными угловыми скоростями и показывают h часов т минут. Найти число полных минут до ближайшего момента, в который стрелки совпадут.
Вход. Два целых числа h и т (на клавиатуре).
Выход. Целое число минут (на экране).
Пример. Вход'. О 0; выход: 0. Вход: 1 1; выход: 4.
Анализ и решение задачи. Вначале выясним, как часто стрелки совпадают: ровно в 0:00, затем приблизительно в 1:05, в 2:11, и т.д., в 10:55 — всего одиннадцать положений. Угловые скорости стрелок постоянны, поэтому совпадения наступают через равные промежутки времени. Следовательно, между ними проходит 12/11 часа, или (12-60)/11 минут.
Промежуток времени от момента первого совпадения 0:00 до момента h:m (h часов т минут) равен 60Л+т минут и является промежутком времени от одного из совпадений стрелок до момента Л: т. Причем это совпадение последнее, если 60Л+т<(12-60)/11. Если же 60-Л-Ьт > (12-60)/11, то можно вычислить 60 Л+т-(12-60)/11 (время после второго совпадения), 60/i+m-2(12-60)/l 1 (после третьего) и т.д.
На некотором шаге получим разность t в пределах от 0 до (12-60)/11. Это и будет промежуток времени после последнего совпадения до момента h: т. Значит, до еле-
18
ГЛАВА 1
дующего совпадения осталось (12-60)/11-г минут, и остается только взять целую часть этого числа.
И последнее замечание. За единицу времени примем не минуту, а одну одиннадцатую минуты. Тогда все числа будут целыми, и t можно вычислить не в цикле, а с помощью операции mod, как в следующей программе:
program clock;
var h, m, t : integer;
begin
readln-(h, m) ;
t := ll*(60*h+m) mod 720;
if t <> 0 then t := 720 - t;
writeln(t div 11); {целое число минут} end.
1.1.2. Последовательности с одинаковыми суммами
Задача 1.2. Разбить последовательность чисел от 1 до № на N подпоследовательностей так, чтобы все они состояли из N чисел и имели равные суммы. Если решении несколько, вывести любое из них.
Вход. Целое число N от 1 до 200 (на клавиатуре).
Выход. N строк, содержащих по N возрастающих чисел, разделенных пробелами (на экране). Порядок, в котором выводятся подпоследовательности, роли не играет.
Примеры. Вход: 1; выход: 1. Вход: 3; выход:
15 9 2 6 7 3 4 8.
Анализ задачи. Сумма всех чисел от 1 до № равна №-(№+1)/2. Значит, сумма каждой подпоследовательности должна быть равна 7У-(№+1)/2. Это подсказывает вид одной из них — арифметическая прогрессия с первым элементом 1 и последним V. Ее разность равна (№—1)/(А/—1), т.е. N+1. Если числами от 1 до № заполнить строки квадратной таблицы, получится, что эта арифметическая прогрессия находится на главной диагонали. Например, для N=3 таблица выглядит так (числа прогрессии выделены): 2	2	3
4	5	6
7	8	5
Заметим, что числа справа от главной диагонали (их ДМ) тоже образуют прогрессию, и каждое на 1 больше своего соседа слева. Их сумма равна 2+6+ ...+(N>-N) = =(2+У-Л0(ЛМ)/2. Это меньше “необходимой” суммы N-(1/+\)I2 на rf-N+\. Но в левом нижнем углу таблицы находится именно N*-N+V. Добавив его, получим вторую подпоследовательность — числа над диагональю плюс число в левом нижнем углу. Например, полученная подпоследовательность выделена приМ=3: 15	3
4 5	5
7 8	9
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
19
Как видим, оставшиеся три числа 3, 4, 8 расположены аналогично: 3 на диагонали, следующей за диагональю (2,6), а 4 и 8 — на следующей за 7.
Итак, первая подпоследовательность находится на главной диагонали матрицы, а остальные — на диагоналях, которые начинаются числами первой строки 2, N, доходят до правого края таблицы и продолжаются в первом столбце следующей строки до нижней строки. Формальное доказательство этого утверждения предоставляется в качестве упражнения.
Решение задачи. Можно заполнить числами матрицу VxV и вывести подпоследовательности, совершив описанные проходы по отрезкам диагоналей матрицы. Однако при #=200 матрица будет иметь 40000 элементов. Для чисел от 1 до 40000 достаточно типа word, но и это потребует 80000 байт. Но в действительности матрица вообще не нужна!
Найдем закон, по которому изменяются числа в подпоследовательностях. В пределах отрезка диагонали каждое число больше предыдущего на N+1, а при переходе из последнего столбца в первый — на 1. Но числа в правом столбце, и толькб они, кратны N, т.е. на 1 увеличивается каждое число, кратное N. Ясно также, что сумма любого числа последней строки с N+1 больше М, поэтому вывод подпоследовательности прекращается, когда получено число больше №. Итак, основная часть решения имеет следующий вид:
for i := 1 to n do begin {перебор диагоналей}
k := i; write(k); {печать числа в первой строке}
while k <= n*n do begin
if k mod n = 0
then inc(k) {переход в первый столбец}
else inc(k, n+1);
if k <= n*n then write(1 k)
end;
writein
end;
►» Напишите всю программу.
1.1.3. Ребус
Задача 1.3. Составить программу поиска всех решений ребуса	|
VOLVO + FIAT = MOTOR.	I
Разным буквам соответствуют разные цифры, одинаковым — одинаковые. I Старшая цифра каждого числа отличается от нуля.	|
Анализ и решение задачи. Программа поиска всех решений по определению должна делать перебор. Вопрос в том, как его организовать, чтобы это было правильно и по возможности удобно и эффективно.
Есть девять букв. Можно написать девять вложенных циклов for, со значениями параметра в каждом от 0 до 9, чтобы генерировать все возможные наборы значений девяти переменных, проверяя для каждого набора, является ли он решением ребуса. Но общее количество наборов равно 10’, и для каждого необходимо проверить и условие ребуса
20
ГЛАВА 1
(104V+ Ю3О+ 102L + 10V+ О) + (103F+ 102/ + 10A + T) = = (104M + 103O + 102r + 100 + R),
и что соответствующие разным буквам цифры отличаются, и что числа не начинаются с нуля. Миллиард таких проверок — это очень много.
•	Дополнительные ограничения могут сократить перебор, если они позволяют отбрасывать не отдельные конкретные наборы, а большие их совокупности.
Начнем с простейшего условия: первые цифры должны быть не равны 0. Записав for V: = l to 9 вместо for V: = 0 to 9, сократим общий объем работы на 10%.
Следующее условие: разным буквам соответствуют разные цифры. Предположим, программа имеет вложенные циклы, внешний из которых содержит перебор возможных значений А, следующий по вложенности (внутренний для цикла по А и внешний для остальных) — перебор по Т. Тогда, переместив условие А < > Т с уровня, внутреннего для всех циклов, на уровень, внутренний для этих двух и внешний для остальных, мы сократим количество выполнений остальных циклов также на 10%. Программа не будет перебирать значения V, О, L и других при выбранных равных А и Т, а решит, что такой перебор не нужен, ведь значения уже не являются разными.
Указанные сокращения перебора реализованы в следующей программе (листинг 1.1).
Листинг 1.1. Решение ребуса с минимальными оптимизациями перебора
var V, О, L, F, I, А, Т, М, R : byte;
used : set of 0..9;
BEGIN
used : = [] ;
for V := 1 to 9 do begin
used := used + [V];
for 0 := 0 to 9 do if not (0 in used) then begin used := used + [0];
for L := 0 to 9 do if not (L in used) then begin
used := used + [L];
for F := 1 to 9 do if not (F in used) then begin used := used + [F];
for I := 0 to 9 do if not (I in used) then begin used := used + [I];
for A := 0 to 9 do if not (A in used) then begin used := used + [A];
for T := 0 to 9 do if not (T in used) then begin used := used + [T];	*
for M := 1 to 9 do if not (M in used) then begin used := used + [M];
for R := 0 to 9 do if not (R in used) then if (((longint(V)*10+0)*10+L)*10+V)*10+0 +
((longint(F)*10+1)*10+A)*10+T =
(((longint(M)*10+0)*10+T)*10+0)*10+R then
writein(V,0,L,V,0,1+',F,I,A,T,	M,O,T,O,R);
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
21
used := used - [М]
end;
used := used - [T]
end;
used := used - [A]
end;
used := used - [I]
end;
used := used - [F]
end;
used := used - [L]
end;
used := used - [0]
end;
used := used - [V]
end;
END.
Выполнение этой программы на современном ПК требует меньше минуты, поэтому, если нужно только один раз получить решение, тратить еще 20 минут на дальнейшую оптимизацию нецелесообразно. Но если нужна как можно более быстрая программа, то резервы для сокращения перебора еще есть. В частности, пока не использован ввд самого условия VOLVO+ FIAT = MOTOR. Рассмотрев его, заметим следующее.
•	M=V+1, поскольку слагаемое FIAT четырехзначное, а пятый разряд суммы MOTOR не равен пятому разряду VOLVO, т.е. имел место перенос. Это позволяет избежать перебора по М, вместо этого с каждым значением V отмечая, что значение V+1 также использовано.
•	F=9, L+/>10, поскольку четвертые разряды VOLVO и MOTOR совпадают, а четвертый разряд F слагаемого FIAT является старшим и не может быть равен 0; следовательно, F=9, а третий разряд дает перенос. Значит, все остальные переменные не могут быть равны 9.
•	0*0, Т*0, R=(О+7) mod 10, поскольку в самый младший разряд переноса быть не может, а младшая цифра суммы отличается от младших цифр обоих слагаемых. Таким образом, R вычисляется без перебора.
Поскольку V определяет две занятые цифры (V иЛТ), а две переменные О и Т— три занятые цифры (/?, О и 7), циклы для V, О и Т целесообразно вынести на внешние уровни, чтобы отбрасывать как можно большие совокупности значений как можно раньше.
Описанные сокращения реализованы в следующей программе (листинг 1.2). Эксперименты показывают, что она работает существенно быстрее первой — приблизительно в 500 раз.
Листинг 1.2. Оптимизированный перебор
var V, О, L, F, I, А, Т, М, R : byte;
used : set of 0..9;
BEGIN
F := 9;
22
ГЛАВА 1
used := [?] ;
for V := 1 to 7 do begin
M := v+l; used := used + [V,M]; for 0 := 1 to 8 do if not (0 in used) then begin used := used + [0]; for T := 1 to 8 do if not (T in used) then begin used := used + [T]; R := (О + T) mod 10; if not (R in used) then begin used := used+[R]; for L := 2 to 8 do if not (L in used) then begin used := used+[L]; for I := 10-L to 8 do if not (I in used) then begin used := used + [I] ; for A := 0 to 8 do if not (A in used) then if longint(V*10+O)*1001+L*100 + ((longint(F)*10+1)*10+A)*10+T = ((longint(M)*100+T)*10+0*101)*10+R then writein(V,0,L,V,0,'+1,F,I,A,T, M,O,T,O,R); used := Used - [I] end; used := used - [L] end; used := used - [R] end; used :=? used - [T] end; used := used - [0] end; used := used - [V] end
END.
Реализованные способы сокращения перебора вполне удовлетворительны по соотношению выигрыша от сокращения перебора и затрат на реализацию. Однако в основе программы лежит учет конкретного условия ребуса. При решении другого ребуса реализованные оптимизации потеряют смысл, поэтому придется вновь искать аналогичные соотношения и существенно изменять программу. Вместе с тем к оптимизации возможен более общий подход.
Например, обобщим соображение, по которому 7? вычислялось, а не перебиралось. Если в самых внешних циклах перебирать значения крайних справа разрядов слагаемых, в следующих по вложенности — значения вторых справа разрядов и т.д., то значения соответствующих разрядов суммы оказываются определенными и их не нужно перебирать. Основное достоинство этой идеи в том, что ее можно применить не только к условию нашей задачи, но и к любому другому. Идея пригодна, если даже задача имеет более высокий уровень универсальности — известно, что ребус имеет вид слагаемое1 + слагаемое2 = сумма, а конкретные слагаемые и сумма задаются во время выполнения программы.
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
23
*	В представленных программах использована переменная used типа set of о.. э. Вместо этого можно было бы объявить массив used •. array [0.. 91 of boolean. Тогда инициализировать пустое множество (used := []) означает присвоить всем элементам массива значение false, добавить элемент а к множеству — присвоить true элементу used [а], удалить элемент — присвоить false, проверить принадлежность a in used — взять значение used [а].
*	Операции для типа set приблизительно так,и выполняются; его преимущество в том, что он реализован в самом языке Паскаль и на каждый элемент базового множества использует один бит, а не байт. Но есть и недостаток: во всех известных реализациях языка Паскаль базовое множество не может иметь больше, чем 256 элементов.
♦	Множество, представленное с помощью типа sat или с помощью массива, использует объем памяти, пропорциональный не количеству элементов множества в данный момент, а количеству элементов базового множества. Во многих ситуациях (но не в данной задаче) это может быть существенным недостатком.
Другие задачи, переборные по своей природе, представлены в главе 11. Оптимизация перебора была популярной темой на олимпиадах по программированию до начала 1990-х годов, но затем на первый план вышли задачи, для решения которых нужно найти и реализовать существенно более эффективный алгоритм.
1.2. Знакомство со сложностью алгоритмов
1.2.1.	Простые и составные числа
Для знакомства с понятием сложности рассмотрим две фундаментальные задачи, связанные с обработкой натуральных чисел. Без их решения, например, не обходится ни одна современная система шифрования.
Задача 1.4. Определить, является ли натуральное число простым.
Анализ и решение задачи. Число и, п> 1, называется простым, если имеет только два положительных делителя — 1 и п. Иначе число называется составным. Любое четное число больше 2 является составным, 2— простым. Нечетное п, п>2, является простым, если не делится без остатка ни на одно из чисел 2, 3, ..., п-1. Таким образом, нужно перебрать все делители к от 2 до п-1 и проверить, делится ли п на очередное к.
Однако вычисления можно ускорить. Если и составное, то n=ktk2, где и к}, и кг больше 1, а меньшее из них обязательно не больше Jn . Поэтому, чтобы узнать, является ли простым число п, достаточно проверить, что оно не делится на числа от 2 до [ 4п ]. Это позволяет уменьшить количество проверок делимости приблизительно в 4п раз. Ясно также, что достаточно проверить делимость только на нечетные к.
Эти соображения реализованы в следующей функции is Prime (prime — простой):
function isPrime{n : integer) : boolean;
var k, t : integer; {очередной делитель, верхний предел} begin
if not odd(n) then begin
isPrime := (n = 2) ; exit
end;
k := 3; {первый делитель 3}
24
ГЛАВА 1
t := round(sqrt(n)); {round - для гарантии} while (к <= t) and (n mod к <> 0) do inc(k, 2);
{ (k > t) or (n mod к = 0) }
isPrime := к > t {если к > t, то число простое, иначе составное}
end;
►► Для проверки функции напишите программу с несколькими вызовами функции, аргументы в которых 2, а также несколько других простых и составных чисел, которые заставляли бы выполнять цикл один и несколько раз. Обязательно проверьте работу на составных числах, которые являются квадратами других целых чисел (простых и составных). Результаты в виде 0 (аргумент функции не простой) или 1 (простой) можно вывести, например, таю
writein (ord (isPrime (...) ) ).
Задача 1.5. Как известно, каждое натуральное число и, п>1, однозначно раскладывается в произведение простых сомножителей, например, 13 = 13, 105= 3-5-7, 72= 2-2-2-3-3. Разложить натуральное число типа integer на простые сомножители {факторизовать его).
Анализ задачи. Чтобы построить разложение произвольного числа, найдем его наименьший делитель (больше 1; очевидно, он простой), запишем его и разделим на него число. Дальнейшие сомножители разложения получаются точно так же, пока в результате делений не останется 1. Например, 36= 2x18 (выписали 2), 18= 2x9 (2), 9=3x3 (3), 3=3x1 (3).
Очевидно, что наименьший делитель частного от деления не может быть меньше, чем наименьший делитель делимого. Поэтому после деления поиски наименьшего делителя можно не начинать с 2, а продолжать с последнего делителя.
Решение задачи. Алгоритм печати простых делителей натурального п оформим в виде процедуры primeDivisors с параметром n (divisor — делитель). Возможные делители будут значениями переменной к.
Вначале к=2. Как и в задаче 1.4, определим верхний предел t для делителей — round (sqrt (n)). Если п делится на очередное значение к, делим и печатаем к. Затем, пока п делится на к, делим и печатаем к.1 После этих делений новое значение п может стать простым. Если оно составное, то имеет делитель среди чисел от к+1 до round (sqrt (n)). Поэтому значение t вычисляется вновь при новом значении п. Если к больше round (sqrt (n) ), значение п стало простым, и оно печатается как последний множитель. Итак, условие k<=t становится условием продолжения поиска следующего делителя:
procedure primeDivisors(n : integer);
var t, k : integer; {верхний предел, очередной целитель} begin
write(n, ' = 1) ;
If n <= 1 then write(' 1, n) else begin
t := round(sqrt(n));
1 Заметим, что, если n делится на к, то к не может быть составным, поскольку все простые делители меньше к уже “изъяты” из п при предыдущих делениях.
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
25
к := 2;
while к <= t do begin
if n mod к = 0 then begin
n := n div k; write(1 k);
while n mod к = 0 do begin
n := n div k; write(' k)
end; •
t := round(sqrt(n));
end;
к := k+1
end;
{k > t -- простых делителей меньше n уже нет}
if n > 1 then write(' n)
end end;
►► Для проверки процедуры обеспечьте ее выполнение с аргументами 2, 3 и несколькими другими простыми и составными числами, при которых цикл выполняется один и несколько раз и проходит по разным ветвям вычислений.
Насколько эффективны алгоритмы в задачах 1.4 и 1.5? Чтобы ответить на этот вопрос, определим понятие сложности алгоритма.
1.2.2.	Понятие сложности алгоритма
Для каждой задачи можно говорить о приемлемом времени ее решения. Для одних задач это десятые доли секунды, для других — минуты и часы. Часто одну и ту же задачу можно решать с помощью разных алгоритмов, и некоторые из них дают результат за приемлемое время, а некоторые — нет. В таких ситуациях правильный выбор алгоритма является решающим.
Обычно решения задачи программируют так, чтобы с помощью программы решить любой возможный экземпляр задачи. Например, экземпляры задачи “является ли заданное число простым?” — это задачи с конкретными числами: “является ли 997 простым?”, “является ли 2007 простым?” и т.д.
Экземпляр определяется конкретными входными данными, а они, как правило, характеризуются некоторым числовым параметром — количеством цифр в числе, количеством элементов в массиве и т.п. Этот параметр имеет натуральные значения и называется размером экземпляра задачи.
•	Обычно размером экземпляра задачи является число битов, которыми представлен экземпляр входных данных.
•	Однако в задачах, где входные данные образуют последовательность значений, размером считается не число битов, а длина последовательности.
Обобщим операции над значениями скалярных типов (присваивание, сравнение, сложение, умножение и т.п.) термином элементарное действие. Предположим, что длительность выполнения любого элементарного действия не зависит от его операндов и самого действия. Тогда время работы программы прямо пропорционально числу выполняемых элементарных действий, т.е. измеряется количеством действий.
26
ГЛАВА 1
Главную роль в понятии сложности алгоритма играет не само по себе число элементарных действий, а характер его возрастания при увеличении размера экземпляров задачи. Уточним это утверждение.
Пусть А — алгоритм решения задачи. При решении экземпляра задачи по этому алгоритму выполняется некоторое количество элементарных действий. Каждому возможному размеру экземпляров п сопоставим количество элементарных действий, наибольшее для экземпляров этого размера. Обозначим это количество FA(n).
Функция FA(n), определенная как наибольшее количество элементарных действий при решении экземпляров задачи размера п с помощью алгоритма А, называется сложностью алгоритма А.
Сложность алгоритмов решения практически всех реальных задач является неубывающей функцией. Аналитическое выражение функции FA(n) для реальных алгоритмов, как правило, невозможно и не нужно. Практическое значение имеет порядок возрастания FA(n) относительно п. Он задается с помощью другой функции, которая имеет простое аналитическое выражение и является оценкой для FA(n).
Функция G(n) называется оценкой сверху для функции F(n), если существуют положительное число с2 и натуральное т, для которых F(n)<cfi(ri) при п>т. Данная связь между функциями обозначается с помощью знака “О”: F(n)=O(G(n)). Запись “О(...)” читается “О большое от... ”.
Функция G(n) называется оценкой снизу для функции F(ri), если существуют положительное число с, и натуральное т, для которых ctG(n)< F(n) при п>т. Данная связь между функциями обозначается знаком Q (омега): F(n)=Q(G(n)).
Функция G(«) называется оценкой для функции F(n), или F(n) является функцией порядка G(ri), если существуют положительные конечные числа ср с2 и натуральное т, для которых c,G(«)< F(ri)<c2G(n) при п>т. Данная связь между функциями обозначается с помощью знака 0 (тэта): F(n)=0(G(n)).
Иногда используют такие определения.
Функция F(n) называется функцией порядка G(ri) при больших п, если lim = С, »-»- G(n)
где 0< С<°°. Нетрудно убедиться, что это определение аналогичноF(«)=0(G(n)).
Функция F(n) называется функцией порядка меньше G(n) пру больших п, если lim - ^п- = 0. Такое соотношение обозначается буквой о (о малое): F(n)=o(G(n)). "-*» G(n)
Для оценки сложности реальных алгоритмов достаточно логарифмической, степенной и показательной функций, а также их сумм, произведений и подстановок. Все они монотонно возрастают и пррсто выражаются. Например, п(п-1) = 0(и2), по
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
27
скольку 0,5 л2 < п(п-1)<п при п>2. Аналогично нетрудно убедиться, что л3+1ООл2= = 0(л3)= о(пл)= о(2"), 1001ogn+ 10000= 0(logn)= 0(lgn)=o(n). Очевидно также, что любая положительная константа с имеет оценки 0(1) и 0(1).
Для вычисления оценок сложности алгоритмов обычно используют два правила, следующие из приведенных определений.
Правило сумм. Предположим, что алгоритм можно разбить на части, не имеющие общих действий. Если сложность одной части алгоритма есть 0(Дл)), а другой — 0(g(n)), то их суммарная сложность есть ©(maxg(ri)}).
Правило произведений. Если часть алгоритма имеет сложность 0(Дп)) и выполняется 0(§(л)) раз, то общая сложность выполнения есть 0(/(n)xg(n)).
Например, в задаче об определении простоты в качестве размера п возьмем само число п. Если п — простое число, цикл в функции isPrime выполняется 0( >/л) раз, а если составное — О(4п) раз. Каждое повторение цикла требует 0(1) действий, поэтому сложность этого алгоритма (при выбранном “размере” л) есть 0( 4п) или О(л1/2).
Однако в действительности размером является не число, а число битов его двоичного представления (длина). Для представления числа п нужно т= Llog2nJ+1 бит, т.е. л=0(2"), и сложность приведенного алгоритма в действительности есть 0(2”/2).
Аналогично, в приведенном выше алгоритме факторизации при простом п цикл с условием продолжения k < = t выполняется [ Jn ] раз, т.е. сложность этого алгоритма также имеет оценку 0(л1/2), или в действительности 0(2"/3).
• Задачи определения простоты и факторизации — одни из основных в шифровании с открытыми ключами. Размеры ключей измеряются сотнями и тысячами битов, поэтому приведенные алгоритмы проверки простоты и факторизации неприемлемы. Для решения этих задач применяются принципиально иные алгоритмы (см., например, [22]).
1.2.3.	Характер возрастания сложности
Предположим, что есть некоторая задача и две программы ее решения с оценками 7’1(л)=0(л2) и Г2(л)=0(л). Очевидно, что T2(n)=o(Ti(n)), т.е. порядок Т2(п) меньше, чем порядок Tt(n). У этих оценок могут быть разные константы. Предположим, что Г,(и)=л2, а Т2(л)=100л, т.е. Т1(п)/Т2(п)=п/100. При л<100 первая программа работает быстрее, чем вторая, но при л порядка 106 — медленнее в десять тысяч раз. Для решения задачи такого размера, очевидно, вторая программа лучше.
Другая причина выбора программы с меньшим порядком сложности связана с максимальным размером экземпляров задачи, решаемых за приемлемое время. Понятие “приемлемое время” неоднозначно, тем не менее, для любой конкретной задачи можно зафиксировать отрезок времени, в течение которого она должна быть решена. Отсюда определяют и максимальный размер экземпляров, решаемых с помощью того или иного алгоритма.
Очевидно, чем меньше порядок сложности алгоритма, тем больше максимальный размер решаемых задач. Важно также, что при больших порядках сложности максимальные размеры с ростом быстродействия компьютеров увеличиваются мало.
28
ГЛАВА 1
Для иллюстрации предположим, что есть два компьютера: один выполняет 10б элементарных действий в секунду, второй — 10’, т.е. работает в 100 раз быстрее. Приемлемое время примем равным 10 с. Обозначим через и М2 максимальные размеры задачи, достижимые на компьютерах при выполнении программ данного порядка сложности. Размеры и характер их увеличения (отношение с ростом быстродействия приведены в табл. 1.1.
Таблица 1.1. Порядок сложности и максимальные размеры задачи			
Порядок сложности	Mi	М2	М2/М1
п	107	10®	100
г?	=>3000	« 30000	= 10
п3	= 220	1000	« 5
2П	21	27	-1,3
lit	10	12	1,2
Как видим, ускорение работы в 100 раз позволяет увеличить максимальный размер для задачи, решение которой имеет сложность порядка п, также в 100 раз, а для задачи с решением сложности п — в 10 раз. Размеры же задач, решаемых со сложностью большого порядка (2" и и!), возрастают незначительно.
Алгоритмы и их сложности порядка п, где к — натуральное число, называются полиномиальными, а алгоритмы и их сложности порядка к' — экспоненциальными.
Из табл. 1.1 видно, что экспоненциальные алгоритмы работают очень долго уже при решении задач, размеры которых несколько десятков. Отметим также, что ускорение работы в 100 раз — это очень много в большинстве практических задач. Однако это — ничто, если для выполнения экспоненциального алгоритма на данных вполне реального размера нужны тысячи и миллионы лет.
Рассмотрим еще одно понятие. Оценивая сложность алгоритмов определения простоты и факторизации, в качестве размера мы взяли сначала само число и, а не длину log л его двоичного представления. Так была получена оценка сложности 0( 4п ), которая одновременно есть О(п), т.е. как будто полиномиальная.
/
Алгоритмы, сложность которых имеет полиномиальную оценку в терминах входных числовых значений, а не размеров их двоичного представления, называются псевдополиномиальными.
1.2.4.	Алгоритм Евклида и его современная версия
Наибольший общий делитель двух целых чисел — это наибольшее натуральное число, которое делит оба числа без остатка. Обозначим наибольший общий делитель чисел а и b через НОД(а, Ь).
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
29
Чтобы вычислить НОД(а,6), можно попробовать разложить а и b на простые множители и выбрать из них общие в нужном количестве. Например, 40= 2-2-2-5, 60= 2-2-3-5, и НОД(40,60) = 2-2-5. Однако есть способы получше.
Алгоритм Евклида основан на том, что:
НОД(а, Ь)=а, если b=0,
НОД(а, Ь) = а, если а=Ь,
НОД(а,6) = НОД(6, а), если а <Ь,
НОД(а,6) = НОД(а-6, 6), если Ь*0.
Например, НОД(21,15)= НОД(6,15)= НОД(15,6)= НОД(9,6)= НОД(3,6)= НОД(6,3)= = НОД(3,3) = 3. Однако при большом а и малом b количество применений правила НОД(а,6) = НОД(а-6,6) окажется сравнимым с а, т.е. сложность (относительно log а) окажется экспоненциальной).
Современная версия алгоритма Евклида основана на том, что НОД(а,6) = = НОД(6,ато46), если b±0 (a mod 6 — остаток от деления а наб). Алгоритм имеет следующий вид (предполагаем, что вначале а>Ь):
while Ь > 0 do begin
С := a mod Ь; а := Ь; Ь := с
end; {а - искомое}
Нетрудно убедиться, что через два выполнения тела цикла меньшее из чисел гарантированно уменьшается более чем вдвое, поэтому цикл выполняется O(logo) раз. Неприятный момент —; вычисление остатка от деления. Известный всем алгоритм деления m-цифровых чисел в столбик имеет сложность О(т2), поэтому вычисление НОД(а,6), где а > Ь, имеет полиномиальную оценку сложности <9(log3a).
1.2.5.	Бинарный алгоритм
Рассмотрим эффективный алгоритм поиска НОД, не требующий делений. Его можно записать следующими формулами:2
(а) НОД(2а, 2Ь)=2-НОД(а, 6),
(67) НОД(а, б) = НОД(б, а), если a < б,
(62) НОД(2а, 6) = НОД(а, 6), если 6 нечетно,
(62) НОД(а, 26) = НОД(а, 6), если а нечетно,
(с) НОД(а, 6) = НОД(а-6, 6), еслиа>Ь,
(4) НОД (а, а)=а,
(е) НОД(а, 1) = 1.
► Формулы (67), (ф и (е) очевидны. Формула (с) используется в “классической версии” алгоритма Евклида. Корректность (а) следует1 из корректности поиска НОД через разложение на множители: если 2 — общий множитель 2а и 26, то при выборе всех общих множителей он попадет и в НОД(2а, 26); причем именно эта двойка не попадает в НОД(а, 6). Наконец, формулы (62) и (65): если одно из чисел нечетно, то НОД тоже нечетный и отбрасывание множителя 2 в другом числе ничего не меняет. <
2 См. также [22,42].
30
ГЛАВА 1
Рассмотрим последовательность применения этих формул. Вначале проверим условия выхода (d) и (е). Скорее всего, они не сработают, поэтому начнем с формулы (а). Применяем ее, пока возможно, причем не строим сразу произведение 22...-2=2к, а только запоминаем количество к применений этой формулы. Как только окажется, что формула (о) неприменима, ее гарантированно не удастся применить и в дальнейшем, поскольку формулы (Ь) и (с) оставляют одно из чисел нечетным.
Затем, пока возможно, применяем формулы (6). Если они неприменимы, оба значения аиЬ нечетны. Тогда, если условия выхода (d), (е) не выполнены, однократно применим формулу (с), получим четную разность а-b, и опять начнем применять формулы (Ь).
Наконец, дойдя до условия выхода (d) или (е), полученное значение НОД к раз умножим на 2 (к — количество применений формулы (а)).
Оценим количество шагов применения формул. Формула (а) уменьшает а-b вчетверо, формулы (Ь2) и (ЬЗ) — вдвое. Поэтому количество применений этих формул не более чем log2ah=О(п), где п — суммарное количество цифр в начальных значениях а и Ъ. Формула (с), которая была причиной неэффективности “классической версии” алгоритма Евклида, благодаря четности а-b, применяется не большее количество раз, чем (Z>2) и (ЬЗ), т.е. тоже О(п) раз. Формула (Ы) применяется не чаще, чем формулы (Ь2) и (с), уменьшающие значение а. Итого суммарное количество шагов — О(п).
Количество элементарных действий на каждом шаге, очевидно, есть О(п), ведь нужны только проверка четности (0(1) при любом четном основании системы счисления), сравнение с константой 1 (0(1)), сравнение и вычитание чисел, а также деление и умножение на константу 2 (все по О(п)). Итак, получаем оценку общего количества действий О(п), что не больше оценки одной операции mod.
Н Реализуйте представленный алгоритм.
1.2.6.	Понятие сложности задачи
Задача может иметь алгоритмы решения различной сложности. Неформально под сложностью задачи понимают наименьшую из сложностей алгоритмов ее решения.
Задача имеет сложность порядка G(ri), если существует алгоритм ее решения со сложностью G(ri) и не существует алгоритмов со сложностью o(G(n)).
Примеры. Рассмотрим задачу вычисления НОД двух натуральных чисел (см. предыдущие подразделы). За размер п примем суммарное количество цифр в исходных значениях чисел. Тогда современная версия алгоритма Евклида имеет оценку сложности О(и3), а бинарный алгоритм — О(п). Поэтому сложность задачи имеет оценку сверху О(п). Однако этим не утверждается, что алгоритма с меньшей оценкой нет.
В задаче 1.2 за размер примем число N и будем считать, что каждое арифметическое действие “стоит” 0(1). В задаче нужно вывести N2 чисел, поэтому сложность любого алгоритма ее решения имеет оценку снизу О(№). Но предложенный алгоритм имеет оценку сложности 0(№), т.е. ее-то и можно считать оценкой сложности данной задачи. 
Сложность задач, как правило, оценить гораздо труднее, чем сложность алгоритмов. Существует много задач, сложность которых до сих пор неизвестна, например, приведенные выше задачи о простоте и разложении натурального числа.
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
31
1.2.7.	Что выбирать?
Минимальный порядок сложности — это не единственный критерий выбора алгоритмов, и не всегда нужен самый эффективный из них. В любом случае важно, чтобы выбор был осознанным.	'
Эффективные алгоритмы имеют, как правило, большие константы в оценке и дают лучшие результаты только при больших размерах задач. Поэтому для небольших по размерам задач неэффективные алгоритмы могут оказаться быстрее эффективных.
Как правило, наиболее эффективные алгоритмы сложны, и их реализация требует намного больше времени, чем реализация неэффективных, но простых алгоритмов. Если программу нужно выполнить всего несколько раз, нет смысла гоняться за эффективностью — время разработки программы может намного превысить суммарное время ее выполнения. Кроме того, понять и при необходимости изменить сложную программу намного труднее, чем простую, поэтому использование простых и “прозрачных” алгоритмов существенно уменьшает длительность программирования. Но если задача при выполнении программы должна решаться много раз, то эффективность необходима.
Введенное выше понятие сложности алгоритма связано со сложностью наихудшего случая — сложность определялась как наибольшее количество действий для всех экземпляров данного размера. Во многих реальных задачах важнее оказывается сложность алгоритма в среднем — усреднение и оценка числа действий по всем экземплярам данного размера. Например, один из алгоритмов сортировки массивов {алгоритм быстрой сортировки — подробнее в главе 5) имеет оценку сложности в среднем O(niogw), а в наихудшем случае — О(п). Тем не менее, его используют чаще других, поскольку он работает практически всегда быстрее, чем алгоритмы, имеющие оценку сложности наихудшего случая О(п log л).
Однако оценить сложность алгоритма в среднем, как правило, очень непросто, поэтому в качестве меры эффективности чаще используется все-таки сложность наихудшего случая.
1.3.	Несколько технических вопросов
Программы для занимательных задач, рассматриваемых в данной книге, существенно меньше по размерам, чем программы для реальных задач. Однако и эти “игрушечные” программы пишутся быстрее и получаются более надежными, если при их создании придерживаться правил, отчасти представленных в данном разделе. Намного основательнее технические вопросы кодирования представлены в [17].
1.3.1.	Проектирование сверху вниз, подпрограммы и структурное кодирование
Проектирование сверху вниз. Реальные задачи, связанные с программированием, обычно сначала четко не формулируются, поэтому, решая задачу, приходится начинать с уточнения ее постановки. Результатом этой работы является спецификация задачи — точное описание требований к программе и данных, которые она получает и создает. В этом смысле условия занимательных и олимпиадных задач проще, поскольку обычно являются уже готовыми спецификациями.
Уточнив задачу, начинают проектировать программу — определять основные понятия задачи и связи между ними, выделять подзадачи и разделять проектируе
32
ГЛАВА 1
мую программу на отдельные программные единицы (модули). Занимательные задачи просты в проектировании, поскольку обычно содержат немного подзадач и редко нуждаются в использовании модулей. Однако и здесь очень важно уметь выделять подзадачи и уточнять их решение с помощью подпрограмм.
В программировании, как и в других областях инженерной деятельности, обычно применяют метод проектирования сверху вниз (пошаговой детализации или нисходящего проектирования). Решение задачи вначале представляют в общих чертах, в виде словесного описания или, возможно, одного оператора, и проектирование представляет собой последовательность шагов уточнения. При этом задачу разбивают на подзадачи до столь простых, что их решение можно описать на языке программирования в нескольких десятках строк.
Подпрограммы. Разбивая программу на подпрограммы, желательно придерживаться следующего, хотя и нечеткого, но все-таки правила: выделять подпрограммы, пока это целесообразно. Обычно размер подпрограммы ограничивают несколькими десятками строк кода на языке высокого уровня. Считается, что малая подпрограмма лучше большой, поскольку с увеличением размеров понятность и отладка подпрограмм усложняются ускоренными темпами. Кроме того, большие подпрограммы часто оказываются взаимозависимыми, и изменения в одной из них приводят к необходимости изменений в других.
Многочисленные примеры использования подпрограмм читатель найдет в дальнейших главах.
Существует общая рекомендация — стараться использовать как можно меньше глобальных переменных. В ситуациях, когда их могут изменять многие подпрограммы, разработка этих подпрограмм требует особенной аккуратности. Однако существует немало ситуаций, в которых эта рекомендация неприменима. Например, объявление массивов в рекурсивных подпрограммах угрожает переполнением программного стека. Можно возразить, что объем программного стека регулируется в настройках компилятора. Однако предел увеличения относительно невелик, тогда как глобальные (точнее, статические) массивы благодаря современным 32-битовым компиляторам могут занимать сотни мегабайтов (конечно, если позволяет конкретный компьютер).
Структурное кодирование. Читатель наверняка знает, что код программы должен иметь определенную структуру и за счет этого быть удобным для восприятия, проверки и внесения изменений. Для этого используют структурные операторы, правильность которых легко проанализировать и установить. Структурность операторов состоит в том, что каждый оператор имеет один вход и один выход. Всем хорошо известны структурные операторы begin-end, if-then-else, while, repeat-until или for.
Структурные операторы часто применяют уже в начале проектирования программы, записывая предложения естественного или математического языка внутри структурных управляющих операторов. Такая форма записи алгоритмов называется псевдокодом. Она облегчает выделение подзадач, упрощает создание подпрограмм и естественным путем приводит к структурированным программам.
1.3.2.	Когда уместны безусловные переходы
Как известно, действие оператора goto (безусловного перехода на метку) состоит в том, что “нормальное” последовательное выполнение программы обрывается и происходит переход на метку, указанную в операторе. Оператор goto нарушает
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
33
структурированность программы, а злоупотребление этим оператором является источником множества ошибок и приводит к очень запутанным, ненадежным и трудноизменяемым программам3.
Из-за “антитехнологичности” goto в свое время у многих алгоритмистов возникло желание вообще исключить безусловный переход из средств программирования. В начале 1960-х годов возникла знаменитая дискуссия о том, возможно ли “программирование без goto”. Было даже математически доказано, что от любого goto можно избавиться путем введения новых переменных и преобразования разветвлений и циклов. Кстати, “побочным продуктом” той дискуссии стало понятие структурного программирования. Примечательно: разрабатывая язык Паскаль, Ник-лаус Вирт вначале планировал вообще не включать в него метки и оператор goto.
В современных языках программирования фактически под держивается компромисс: goto существует, но “не популярен”, а такие “скандальные” его использования, как безусловный переход внутрь тела подпрограммы и аналогичный выход из него, запрещены.
По мере развития программирования были выделены определенные “типичные” ситуации, в которых безусловные переходы не приводят ни к каким проблемам и при этом оказываются удобнее, чем обычные разветвления. В первую очередь, это переход к завершению подпрограммы и выход из цикла — соответственно переход к слову end, закрывающему подпрограмму, и к первому оператору после тела цикла. Благодаря фирме Borland, современные версии языка Паскаль обеспечивают д ля этих ситуаций специальные операторы4: exit завершает выполнение подпрограммы (если записан в программе, то работу программы), a break — выполнение цикла.
Несколько реже используется оператор continue, обрывающий текущую итерацию цикла — операторы, записанные ниже в теле цикла, не выполняются. Дальше выполняется то, что следует за выполнением тела цикла — проверка условия продолжения (оператор while) или завершения (оператор repeat) цикла. В цикле for проверяется, не стало ли значение параметра цикла равным его верхней (нижней) границе. В зависимости от результата этой проверки цикл завершается или начинается его следующая итерация (в цикле for предварительно увеличивается или уменьшается значение параметра цикла).
В язык C/C++ соответствующие средства были внесены изначально его разработчиками. Для немедленного завершения подпрограммы используется return (если функция не типа void, то после return должно быть задано значение, которое возвратит функция). Для завершения цикла и его текущей итерации используются все те же break и continue.
Наконец, рассмотрим ситуацию, в которой использование явных безусловных переходов goto можно считать целесообразным. В программах довольно часто, особенно при обработке двумерных массивов, возникают вложенные циклы следующего вида:
for i := iMin to iMax
for j := jMin to jMax do begin
end
3 В легкости внесения изменений заключается гибкость программирования.
4 В действительности это процедуры.
34
ГЛАВА 1
Оператор break, записанный в теле цикла, приведет к завершению только внутреннего цикла (с параметром j), а внешний, с параметром i, будет продолжен. Если же нужно прервать весь цикл, то придется работать с дополнительными переменными, ухудшающими ясность программы. В этой ситуации лучше использовать не break, a goto — переход на метку, поставленную сразу после тела цикла.
1.3.3.	Несколько замечаний о стиле
Под стилем программирования обычно понимают набор приемов и методов, применяемых с целью получить правильные, эффективные, удобные для восприятия, тестирования, применения и модификации программы. Четкого определения хорошего стиля нет, но существуют неформальные рекомендации по записи выражений, операторов и других элементов программы. Использование структурных операторов — это один из элементов хорошего стиля, но есть и другие.
Правильные значения. Не забывайте присваивать переменным начальные значения. Многие языки позволяют присваивать значения непосредственно в объявлении. Если эта возможность есть — пользуйтесь ею5.
Начинающие программисты легко привыкают к тому, что начальные значения переменных равны 0. Однако во многих средах нулевые начальные значения гарантированы только для глобальных переменных. Поэтому, если не указать начальное значение локальной переменной подпрограммы, то результаты работы программы могут быть не то что неправильными, а вообще разными при разных ее запусках с одними и теми же входными данными!
Старайтесь избегать разнотипных числовых операндов в выражениях, особенно в сравнениях, и не присваивать переменным “коротких” целых типов значения “длинных” типов.
Перед вызовом подпрограммы проверьте, принадлежат ли значения аргументов в вызове подпрограммы предполагаемому диапазону значений. Иногда такую проверку помещают в саму подпрограмму.
Если выражение может иметь N известных значений, которым соответствуют N ветвей вычислений, добавьте проверку, не отличается ли его значение от всех .предусмотренных, и еще одну ветвь вычислений, соответствующую непредвиденным значениям.
Имена. Выбирайте имена так, чтобы они явно отражали представленные ими понятия, т.е. имели подходящую мнемонику. Не используйте короткие имена или слишком лаконичные и непонятные сокращения. Правильно выбранные имена уменьшают потребность в комментариях6.
Имена должны ощутимо отличаться, по крайней мере, больше, чем одной буквой в конце. Если вместо одного имени написано другое, похожее на него, обнаружить эту ошибку нелегко.
Не используйте имена, определенные в системе программирования, с другой целью.
Операторы и выражения. Лучше придерживаться правила: odha строка — один оператор. Запись нескольких операторов в одной строке усложняет чтение программы
5 В Turbo Pascal и некоторых других языках “семейства Паскаля” инициализируемая переменная объявляется как типизированная константа.
6 В условиях соревнований эти рекомендации не очень реалистичны, но при неторопливой разработке программы весьма важны.
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
35
и скрывает детали ее пошагового выполнения. Если в одном из операторов есть ошибка, компилятор или отладчик укажет строку, а не конкретный оператор.
Вместе с тем, в одной строке часто задают несколько логически связанных присваиваний с простыми выражениями, например, в начале тела подпрограммы или перед циклом.
Программа воспринимается гораздо легче, если операторы и выражения записаны с отступами. Отступы подчеркивают вложенность операторов и помогают отследить порядок выполнения операторов; структурность операторов дополняется структурностью вида самого текста.
Если значение некоторого выражения используется в программе несколько раз без его повторного вычисления, можно присвоить его вспомогательной переменной и затем использовать ее имя вместо выражения.
Комментарии. Участники соревнований практически никогда не пишут комментарии для экономии времени. Однако при недостатке или ошибочности комментариев дальнейшая работа с программой значительно усложняется.
Лучше писать комментарий вместе с программой, поскольку именно тогда программист сосредоточен на ней больше всего и ему не приходится вспоминать забытые детали.
В операторе i£-then-else после слова else полезно добавить комментарий, содержащий отрицание условия, указанного после if, особенно когда само if записано намного выше.
После оператора while очень полезно записать комментарий с отрицанием условия продолжения цикла. Часто это дает подсказку о состоянии памяти после завершения цикла.
1.3.4.	Отладка программы
Термин “правильная программа” неоднозначен. Синтаксически правильная программа, которая выдает совершенно “не те” результаты, нуждается в анализе и изменениях. Программа, правильно работающая на некоторых экземплярах входных данных, на соревнованиях школьников имеет шансы получить какие-то баллы (впрочем, для победы их, скорее всего, не хватит).
В большинстве студенческих и “сетевых” соревнований программа должна выдавать правильные результаты для всех возможных входных данных, удовлетворяющих спецификации задачи. Как правило, чтобы этого достичь, приходится тщательно и неоднократно проверять программу на специально подобранных данных и исправлять обнаруженные при этом ошибки (отлаживать ее).
Программы для реальных задач еще должны сохранять свою работоспособность при входных данных с произвольными ошибками (быть живучими) и обеспечивать диагностику и безопасную обработку ошибок.
Написанная и набранная программа, даже небольшая, почти всегда содержит ошибки, и приходится их искать и устранять, т.е. отлаживать. Процесс отладки — непростая работа, которая иногда занимает гораздо больше времени, чем написание кода. Главная причина трудностей отладки заключается в психологической установке: разум видит то, что он хочет и ожидает увидеть, а не то, что есть в действительности.
Как когда-то пошутил один из классиков-алгоритмистов Уильям Огден, отлаженная программа — это программа, для которой пока еще не найдены условия ее
36
ГЛАВА 1
неработоспособности. Тем не менее отлаженные программы для “игрушечных” занимательных задач — реальность.
Лучший способ облегчить отладку — свести к минимуму ее необходимость. Используйте структурное нисходящее проектирование и хороший стиль программирования, обдумывайте каждый шаг. И не спешите садиться за клавиатуру.
Рассмотрим несколько приемов, облегчающих отладку.
Добавляйте в программу специальные отладочные операторы (и необходимые для них объявления). Они называются стопорами ошибок и облегчают их поиск. Удалить средства отладки в дальнейшем гораздо проще, чем добавить их в уже набранную программу, когда выяснится, что она содержит ошибки. Вот несколько рекомендаций по их применению.
•	Добавляйте к программе операторы вывода полученных входных данных.
•	Используйте отладочные блоки, структура которых позволяет в дальнейшем удалить их без внесения ошибок в программу. В них можно выборочно выводить значения переменных и указывать, что началось или закончилось выполнение той или иной подпрограммы.
•	Работой отладочных блоков можно управлять с помощью набора булевых переменных, которые специально объявляются в программе и удаляются в конце отладки. Они инициализируются в начале программы и своими значениями указывают, выполнять ли соответствующий блок и выводить ли значение той или иной переменной. Аналогичного эффекта можно достичь с помощью условной компиляции (подробнее в решении задачи 2.9, подраздел 2.2.3).
•	Используйте счетчики, позволяющие узнать количество выполнений циклов или вызовов подпрограмм.
Использование отладочных средств при разработке программы называется защитным стилем программирования. Его часто игнорируют, в основном, из-за излишней самоуверенности и нежелания тратить усилия на средства отладки, хотя миллионы раз проверено, что затраты времени на защитное программирование многократно окупаются при отладке программы.
Отладке помогают средства среды программирования. Советуем изучить возможности отладчика среды и использовать средства, задающие различные режимы компиляции. В частности, в следующем подразделе представлены некоторые из директив компилятору Turbo Pascal.
Не пренебрегайте также предупреждениями (warnings), которые выдаются компилятором и указывают на мелкие недочеты — неинициализированные или лишние (объявленные и не используемые) переменные, и тщ. Иногда эти недочеты ни на что не влияют, поэтому, обнаружив их, компилятор создает выполнимую программу. Программист может просмотреть предупреждения и решить, нужно ли изменять программу. Рекомендуем использовать режим компилятора, в котором он выдаем предупреждения.
Наконец, еще один хороший способ облегчения отладки — имитация программы на бумаге. Для этого нужно уметь внимательно читать ее и стараться как можно глубже вникнуть в алгоритм. Это особенно полезно, если ошибка локализована в небольшой области текста и нужно найти ее точное расположение.
И не забудьте: любое исправление может внести в программу новую ошибку и требует дополнительной проверки.
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
37
1.3.5.	Директивы компилятору
Директивы компилятору подробно здесь не объясняются — для этого есть справка Help в составе среды программирования. Наша цель — напомнить об этом средстве и объяснить, зачем оно нужно.
Для примера предположим, что массив mass объявлен как array [1. . NMax] of.... Если при вычислении условия (i>l) and (mass[i-1]=1)
значение i равно 1, то попытка взять значение элемента mass [0] может вывести за границь! массива и привести программу к аварийному завершению. А может и не привести. Это зависит от режима вычисления операций and и or, а также от того, проверяется ли при вычислениях выход индекса массива за пределы его диапазона.
В выражении (i>l) and (mass[i-l]=l) можно сначала найти результаты обоих сравнений, а потом выполнить над ними операцию and (полное вычисление). Но можно сначала вычислить только (i>1) — если его значение false, то результат всей операции and будет false независимо от значения второго операнда. Поэтому второй операнд можно вообще не вычислять (сокращенное вычисление). Аналогично, если первый операнд операции or имеет значение true, результатом ее будет true, иначе нужно вычислить второй операнд.
Способ вычисления логических выражений можно выбрать перед компиляцией Паскаль-программы. В Turbo Pascal можно выставлять или снимать крестик в пункте меню
Options/Compiler/Complete boolean evaluation
Turbo-среды: если флажок выставлен, в сгенерированных компилятором программах будет происходить полное вычисление, если снят — сокращенное.
Таким образом, результат компиляции программы может зависеть от настроек Turbo-среды, что не всегда желательно. Указания нужных режимов компиляции удобнее включить в текст программы в виде директив компилятору, имеющих общий вид {$...}.
Директива “проводить сокращенные вычисления логических выражений” имеет вид {$В -}, а “проводить полные вычисления” — {$В+}. Директива “проверять выход за пределы диапазонов индексов массива” имеет вид {$R+}, а противоположная ей “не проверять...” — {$R-}. Таким образом, вычисление условия (i>l) and (mass [i-1] =1) приведет к аварийному завершению, если при его компиляции действовали директивы {$в+} и {$R+}.
Если программный код скомпилирован при действии {$R-}, то при его выполнении выход за границы массивов не проверяется. Вообще-то, проверки тормозят выполнение программы, поэтому обычно их убирают перед окончательной компиляцией. Но на время отладки настоятельно рекомендуем ставить {$R+}. Если программа выходит за границы массива, то она, скорее всего, неправильная, а цель отладки как раз и состоит в поиске и исправлении ошибок! Так что можно только радоваться такой автоматизации поиска. Кроме того, при выходе за границы массива возможен выход за границы вообще всей памяти, отведенной программе. На эту ошибку, возможно, укажет операционная система, сообщив “Программа вызвала ошибку и будет снята”, причем снята вместе со средой программирования. Так что сообщение “самоконтролирующейся”
38
ГЛАВА 1
программы “Range-check error at line ...”, в котором указано место ошибки, гораздо информативнее...
Напомним еще две директивы, которые задают проверки, вряд ли нужные в полностью правильной программе, но полезные при отладке. Директива {$Q+} задает проверку арифметических переполнений, {$S+) — переполнений программного стека.
Наконец, Turbo (Borland) Pascal позволяет автоматически сохранить в программе все использованные директивы компилятору — с помощью <Ctrl+O+O> (удерживая клавишу <Ctrl>, дважды нажать <О>). Однако рекомендуем пользоваться этой возможностью, только если есть гарантия, что программа всегда будет транслироваться компилятором этой же версии. Если же программа будет компилироваться в разных средах (например, Turbo Pascal и Free Pascal), лучше применять только проверенные директивы, для которых известно, что их смысл одинаков для всех используемых компиляторов.
Сказанное о директивах компилятору касается только Turbo (Borland) Pascal. Если читатель использует другие системы программирования, советуем выяснить аналогичные вопросы по справочной информации этих систем или другим источникам.
Иногда, чтобы гарантировать безопасное выполнение программы независимо от способа вычисления булевых операций, принцип “проверять второе условие, только если выполнено первое” используют явно. Например,
if i>l then if mass[i-l] = 1 then ....
К сожалению, такой прием уместен в приведенном простом случае, но очень неудобен, например, если оператор разветвления с условием, содержащим and, имеет else-часть или условие касается цикла, а не разветвления.
1.3.6.	Проверка программы
Вообще-то, проверка, или тестирование, программы — это ее выполнение с целью установить наличие ошибок. Нужно заставить программу сбиться, т.е. отнестись к ней деструктивно. Автору программы это сделать трудно, поэтому тестирование реальных программ проводят специалисты, которые вообще не занимаются их проектированием и кодированием.
В наших условиях проверка программы является составной частью отладки, и проводить ее приходится автору программы. И цель здесь — не только установить, что ошибки есть, но и облегчить их поиск. Вместе с тем, участники соревнований иногда отлаживают программу, чтобы она успешно работала на входных данных (тестах), которые, предположительно, может использовать жюри.
Итак, несколько общих замечаний,
•	Планируя тестирование, предполагайте, что в программе есть ошибки.
•	Создавая набор тестов, для каждого теста предварительно определите результат, который должен быть получен.
•	Проверяйте, выполняет ли программа то, для чего она предназначена, а также не делает ли она того, чего не должна делать (например, выводит не только то, что Нужно).
•	Если набор тестов велик, то сначала проверьте программу на более простых тестах, способных выявить простейшие ошибки.
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
39
•	Ошибки имеют свойство скапливаться. Отсюда парадоксальный вывод — чем больше ошибок найдено в некоторой части программы, тем больше шансов, что там есть еще. Поэтому данную часть программы нужно проверить с усиленным вниманием.
•	Не предполагайте, что тест будет использован только один раз.
•	Получив результаты тестирования, внимательно их изучите. Каждое несоответствие полученных и ожидаемых выходных данных дает информацию для отладки программы.
К созданию набора тестов есть два подхода: один использует спецификацию программы, другой — ее логику. Жюри на соревнованиях, естественно, использует первый способ, ^тестируя программу как черный ящик, о внутреннем устройстве которого ничего не известно. Этот способ еще называют тестированием, управляемым данными. Его крайний вариант — исчерпывающее входное тестирование, в котором тестами являются все возможные экземпляры данных. Практически этот вариант неосуществим, поэтому нужно искать ограниченные, но одновременно достаточно представительные наборы входных данных.
Реайьные способы тестирования, управляемого данными, основаны на разбиении множества входных данных на классы так, что все экземпляры из некоторого класса дают в определенном смысле одинаковые результаты. Например, при вычислении корней квадратного уравнения ах +bx+c=Q по его действительным коэффициентам все тройки чисел (а,Ь,с), для которых Ь2-4ас<0, должны давать один и тот же ответ (корней нет), поэтому программу достаточно проверить только на одной из таких троек. Таким образом, используется набор тестов, представляющих свои классы эквивалентности.
Итак, для тестирования важен отбор тестов, дающих максимальную отдачу с точки зрения выявления ошибок. В условиях соревнований обычно готовят тесты для каждой возможности, описанной в спецификации, учитывая границы областей допустимых значений входных и выходных данных. Если по условию задачи есть отдельные особые значения входных данных, проверяют и их. Если правильность входных данных не гарантирована, добавляют тесты с недопустимыми значениями.
Создавая реальные программы, используют также тестирование, управляемое логикой программы. Программа рассматривается как белый (прозрачный) ящик. Здесь анализируется логика программы, а спецификация во внимание не принимается. Этот подход предназначен для выявления путей вычисления, на которых программа дает сбой. Крайний (и практически неосуществимый) вариант — исчерпывающее тестирование путей. В действительности же используют различные условия, которым должны удовлетворять наборы тестов. Например, выполняется ли каждый оператор, проходится ли каждая ветвь в операторах ветвлений, выполняется ли тело каждого цикла минимальное, максимальное и какое-нибудь промежуточное число раз.
Формируя тестовый набор, как правило, вначале используют подход черного ящика, а затем изучают программу и добавляют тесты, связанные с логикой программы.
40
ГЛАВА 1
1.4.	Ввод последовательностей данных
1.4.1.	Организация данных и вид цикла ввода
Входные данные во многих задачах образуют последовательность числовом и других констант; для их ввода нужен цикл. От того, как организована последовательность, в частности, как задан ее конец, зависит вид цикла. Рассмотрим три способа указания конца последовательности и три способа организации циклов ввода и обработки данных.
• Входные данные ниже в книге везде считаются корректными.
В задачах типична ситуация, когда отдельный экземпляр тестовых данных (тест) содержит последовательность значений, записанных в одной или нескольких строках. Проще всего ввести тест, если в нем вначале задано количество п последующих значении или строк, а затем сами значения или строки в количестве п (в некоторых задачах это количество кратно п или отличается от п на небольшую константу). Как правило, значение п можно представить в типе integer, word или longint и для ввода теста использовать for-оператор.
Входные данные могут содержать несколько тестов; их последовательность организуют тремя основными способами.
1.	Количество тестов задано вначале. Используем внешний цикл £ог.
2.	Конец данных обозначен специальным значением. Напишем “бесконечный” цикл, который прервем, получив это значение.
3.	Признаком конца является окончание текста. Также используем “бесконечный” цикл, но прервем, используя функцию eof.
Еще один способ задать конец последовательности — повторить первое или предыдущее значение последовательности. Схема решения аналогична приведенной в п. 2, только “особое значение” становится известным в процессе чтения (нужно хранить значение, или прочитанное первым, или предыдущее).
Рассмотрим три приведенных варианта на примере одной популярной задачи.
Задача 1.6. По целому пил положительным целым числам типа integer определить, можно ли из них образовать подмножество, сумма элементов которого делится на п без остатка; если можно, напечатать любое из таких подмножеств.
Вход. В файле input. txt первая строка каждого теста содержит количество чисел и (1 < п< 104), вторая — натуральные числа а,, а2, ..., ап типа integer, разделенные пробелами.
Вариант 1. Тестам предшествует строка с количеством ^тестов (не больше 20).
Вариант 2. Признаком конца является значение и=0.
Вариант 3. Признак конца — окончание текста.
Выход. Для каждого теста вывести в одну строку файла output. txt найденное подмножество чисел или сообщение “Подмножества нет”.
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
41
Пример
Вход (вариант 1)	Вход (вариант 2)	Вход (вариант 3)	Выход
2	1	1	1
1	1	1	1 2
1	3	3	
3	14 2	14 2	
14 2	0		
Решение задачи. Вариант 1. Тесты введем во внешнем цикле for, числа каждого из них— во внутреннем. Для хранения чисел достаточно массива А : array [1. . 10000] of integer. После связывания и открытия входного текста F обработка тестов будет иметь такой общий вид: readLn(F, m); { количество тестов } for t := 1 to m do begin
{ ввод теста }
readln(F, n) ; { количество чисел }
for i := 1 to n do read(F, A[i]);
readln(F);
Обработка массива A;
Вывод /результата для очередного теста; end;
Вариант 2. Образуем “бесконечный” цикл обработки последовательности тестов, который прервем, когда будет прочитано значение и=0. Соответствующий фрагмент программы будет иметь такой вид:
{ количество тестов заранее неизвестно }
while true do begin
readln(F, n); { количество чисел } if n = 0 then break; { прерываем цикл } for i := 1 to n do read(F, A[i]);
readln(F);
Обработка массива A;
Вывод результата для очередного теста; end;
Вариант 3. “Бесконечный” цикл обработки последовательности тестов прервем, если перед чтением значения п оказалось, что файл закончен.
{ количество тестов заранее неизвестно }
while true do begin
if eof(F) then break; { прерываем цикл }
readln(F, n); { количество чисел }
if n = 0 then break; { на всякий случай... }
for i := 1 to n do read(F, A[i]);
readln(F);
Обработка массива A;
Вывод результата для очередного теста; end;
42
ГЛАВА 1
В последнем варианте “на всякий случай” оставлена проверка условия п = 0. Вспомним особенность чтения в системе Turbo Pascal. Если после последней строки с тестовыми данными до конца текста есть еще пустые символы (пробелы или символы конца строки), то из вызова eof (f) возвращается false, и будет прочитано именно значение п=0.
Перейдем собственно к решению задачи — обработке массива А. Первое, что приходит в голову, — перебрать все возможные подмножества, но их может быть до 2""“, т.е. существенно больше, чем 1О3000. Даже предположив, что ежесекундно компьютер обрабатывает 1010 подмножеств, получим, что на это пойдет больше Ю3000 лет...
Ключ к решению дает математика. Суммы ар а1+а2, ..., а2+а2+ —+ая имеют остатки гр г2,.... гж. Если среди них есть г(=0, то {ар а2, а) — искомое подмножество. Иначе остатками гр г2, гя являются числа 1, 2, ..., п-1, и среди них обязательно есть одинаковые, пусть rt и г., где i < j. Тогда остаток от деления аРх + ...+а} на п равен 0, и {аРр ал,..., Oj} — искомое подмножество.
Если каждая из п величин принимает одно из п-1 значений, то хотя бы две из них равны. Это простое правило называют принципом Дирихле и часто формулируют так: “Как бы ни сажали п кроликов в п-1 клетку, хотя бы в одной клетке будет не меньше двух кроликов”.
Объявим вспомогательный массив В, целые элементы которого индексированы остатками 0,..., п-1 от деления на п и инициализированы значением 0. Далее последовательно вычислим остатки гр г2, ..., гп от деления на и сумм ар а2+а2, ..., at+a2+ +...+ая и номера 1, 2, ..., п этих слагаемых присвоим переменным B[rJ, В[г2] _В[гя]. Если на i-м шаге сумма имеет остаток 0, выведем ар а2,аг Если на i-м шаге вычислен остаток г и оказалось, что значением В[г] является к*0, то выведем а^, ...,аг
Очевидно, что нужно вычислить не более п остатков и запомнить не более п номеров, поэтому максимальное общее количество действий с числами прямо пропорционально п.
Итак, объявления программы имеют следующий вид:
const МахА = 10000;
var F, G : text;
А : array [1..МахА] of integer; {числа}
В : array [0..MaxA-l] of integer; {номера}
S ; longint; { сумма остатков может не поместиться в integer} m, t, {количество тестов, номер теста (вариант 1)} n, i, {количество чисел, номер числа} bs, es : longint; {начало и конец последовательности}
В начале тела программы должны быть вызовы подпрограмм, которые готовят файлы к обработке.
assign(F, 'input.txt'); reset(F);
assign(G, 1 output.txt1); rewrite(G);
После этого читаются входные данные. Собственно решение задачи и вывод результата, т.е. указанные выше пункты
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
43
Обработка массива А;
Вывод результата для очередного теста
будут иметь следующий вид:
{ Обработка массива А }
S := 0;
for i := 1 to n do begin
S := S + A[i] ;
If S mod n = 0 then
begin bs := 1; es := i; break end
else if В [S mod n] <> 0 then
begin bs := В[S mod n]+l; es := i; break end;
B[S mod n] := i;
end;
{ Вывод результата для очередного теста }
for i := bs to es do write(G, A[i]); writein(G);
В конце программы не забудьте Закрыть файлы.
close(F); close(G);
Замечание. Формула (a+6) mod п = (a mod и + b mod n) mod п позволяет вычислить г( как г = (/>,+ azmodn)modn. Поэтому можно вычислять не суммы чисел, а суммы их остатков от деления на и, и от этих сумм брать остатки. Таким образом, вместо S : = S + A [i] можно записать S : = (S + А [i] modn) modn и ниже вместо выражений Smodn использовать просто S. Тогда, поскольку п<104, для S достаточно типа integer.
Н Запишите программу полностью.
• Условие задачи может не уточнять, как именно данные разбиты по строкам. Поэтому для чтения чисел, чтобы не пропустить ни одного из них, лучше использовать процедуру read. Однако чтение текста в строковые переменные имеет свои особенности и для этого, как правило, используют процедуру readin.
1.4.2. Изменение источника данных
В некоторых задачах по условию данные должны вводиться с клавиатуры. Если в процессе отладки программу приходится перезапускать много раз, то ввод с клавиатуры становится проклятием, поскольку входные данные приходится каждый раз набирать на клавиатуре. Конечно, можно ввести файловую переменную, скажем, f v типа text, заменить все операторы read(...) на read(fv, ...), а после отладки убрать в них добавленные символы “fv, ”. Но если таких операторов ввода много, их переписывание из одного формата в другой — нудное занятие, к тому же чреватое досадными техническими ошибками. Однако для того, чтобы указать нужный источник данных, не изменяя самих операторов ввода, есть два способа.
Первый способ. Вначале свяжем стандартное имя input с нужным файлом на диске, скажем, 1 source. txt', и откроем его для чтения.
assign(input, 'source.txt');
reset(input);
44
ГЛАВА 1
В программе для ввода используем операторы read без имени файловой переменной, поскольку в действительности имя input используется в операторах ввода по умолчанию. Когда программа отлажена и нужно вводить данные с клавиатуры, просто уберем или “спрячем” в комментарий вызовы процедур assign и reset, указанные выше.
Второй способ. Используем файловую переменную, скажем, fv типа text. Вначале свяжем имя f v с нужным файлом на диске и откроем его для чтения.
assign(fv, 1 source.txt1);
reset(fv);
Для ввода используем операторы вида read (fv, ...). После отладки, когда понадобится ввод данных с клавиатуры, изменим лишь связывание имени fv: вместо 1 source. txt' запишем 1 con1 — имя клавиатуры, под которым она известна в операционной системе.
Упражнения
1.1.	Найти все делители заданного натурального числа п типа longint.
1.2.	Задан действительный радиус г круга на плоскости. Известно, что центр круга — точка с целочисленными координатами. Найти количество узлов целочисленной координатной сетки внутри круга. Предложите одно решение с оценкой сложности 0(г2), другое — 0(г).
1.3.	Числа от 1 до п (2<п< 106) нужно разбить на максимально возможное количество пар так, чтобы суммы чисел в парах были простыми числами. Напечатать количество пар. Например, при л = 3 получается одна пара (1,2), при п=1 — три: (1,2), (3,4), (6,7).
1.4.	От прямоугольника с целочисленными размерами пхт отрезают квадраты максимального размера, пока не останется квадрат. Найти количество отсечений.
1.5.	По двум натуральным а и b найти наименьшее положительное число d и какие-нибудь целые и и v, при которых au+bv=d.
1.6.	По заданным натуральным числам а,, а2,..., ап, где и<210’, найти наибольшее натуральное число d, при котором остатки от деления заданных чисел на d равны.
1.7.	В одном ящике находится а морковок, в другом — Ъ морковок, всего не больше 2-109. Каждый ящик может вместить всю морковь. За один раз из одного ящика можно переложить в другой столько морковок, сколько лежит в другом ящике. Определить, можно ли в результате таких перекладываний освободить один из ящиков. Например, при а=9, й=3 это возможно, прр а=6, Ь=3 — нет.
1.8.	Концы отрезка на плоскости имеют целочисленные координаты. Вычислить, сколько всего точек с целочисленными координатами принадлежат отрезку.
1.9.	У Аси есть неограниченный запас монеток достоинством а копеек, у Васи — b копеек. Нужно найти максимальную сумму, которую Ася и Вася не смогут набрать своими монетками. Если такие суммы не ограничены, выдать ответ 0. Например, при а = 2, Ь = 5 ответ 3, а при а = 2, Ь=4 нельзя набрать ни одной нечетной суммы, поэтому ответ 0.
РАЗМИНКА (ПОНЕМНОГУ О РАЗНОМ)
45
1.10.	Прямоугольник состоит из XxY клеток единичного размера. Из него вырезан прямоугольник размером (Х-2)х(У-2) так, что осталась рамка шириной в одну клетку. Определить, можно ли покрыть всю рамку плитками размером Ах 1. Запас плиток не ограничен, плитки не накладываются одна на другую и за пределы рамки не выходят. Вход. Последовательность тестов в тексте. В первой строке теста записаны X и Y, во второй — последовательность размеров А, для которых нужно проверить возможность покрытия (через пробел, за последним числом пробела нет). Гарантировано, что 3< X, Y, А<2109. Признак окончания входа — конец текста. Выход. Последовательность строк, соответ-ствуюпйос тестам. Каждая строка состоит из 0 и 1, соответствующих проверяемым размерам плиток (1 — покрыть можно, 0 — нельзя). Например, при Х=5и У=3 размерам 2,3,4 соответствует строка 110.
1.11.	Прочитать последовательность неизвестной длины (признак окончания — конец текста), состоящую из натуральных чисел, которые не больше 104. Найти минимальное натуральное число, которого нет в последовательности.
1.12.	Дана последовательность чисел, представимых в типе longint; ее длина не больше 104. Найдите минимальное натуральное число, которого нет в последовательности.
1.13.	Напечатать в порядке возрастания первые N чисел (1^У<104), имеющие из простых делителей только 2, 3, 5 (последовательность Хэмминга). Например, первые 10 чисел таковы: 2,3,4,5,6, 8, 9,10,12,15.
Глава 2
Однопроходные алгоритмы
В этой главе...
♦ Задачи, в Которых нужно читать из текста и обрабатывать как угодно длинные последовательности значений, используя фиксированное количество скалярных переменных, не зависящее от длины последовательностей
* Чтение и обработка последовательностей символов с помощью конечных автоматов
♦	Распознавание “языков вложенных скобок” с помощью магазина
♦	Алгоритм поиска в тексте вхождений подстроки, имеющий линейную оценку времени работы и допускающий “однопроходную” реализацию
2.1. Попутные вычисления
2.1.1. Три простых примера
Задача 2.1. В последовательности целых чисел типа longint найти минимальное число и количество его повторений.
Вход. Числа в тексте разделены пробелами и концами строк, между последним из них и концом текста пробелов и иных символов нет. Гарантировано, что последовательность не пуста и количество чисел можно представить в типеlongint.
Выход. Максимальное число и количество его повторений.
Примеры. Вход: -2 -1; выход: -2 1.Вход:3 10 3; выход: 3 2.
Анализ и решение задачи. Для решения задачи достаточно хранить только число min, минимальное среди прочитанных, и количество его повторений ent. Эти два значения дают всю необходимую информацию о состоянии прочитанной последовательности. Очередное число а сравним с min. Возможны три ситуации. Если а < min, выполним min := a; ent := 1. Если а = min, увеличим ent. Если а > min, состояние не меняется. Первое число обрабатывается отдельно, поскольку до его чтения состояние не сформировано.
Реализуем приведенные рассуждения в следующем фрагменте программы (f — текст):
48
ГЛАВА 2
read(f, min); ent := 1;
while true do begin
if eof(f) then break;
read(f, a);
if a < min then begin
min := a; ent := 1;
end else
if a = min then inc(ent);
end;
writeln(min, 1 1, ent);
H Дополните этот фрагмент до программы.
Задача 2.2. На плоскости задан круг с центром в начале координат и набор I точек. Его радиус, количество точек и их координаты вводятся с клавиатуры. I Найти точку вне круга, ближайшую к нему.	|
Анализ и решение задачи. Объединим известную формулу расстояния от точки до начала координат (d ; - sqrt (х*х+у*у)) со стандартным алгоритмом поиска минимума. Однако минимум ищется только среди расстояний, больших радиуса R, поэтому инициализацию минимума придется усложнить.
Рассмотрим один из возможных способов. Используем флажок found с начальным значением false, которое превращается в true при первом появлении точки вне круга. Чтобы не записывать изменение минимума расстояний min_d для двух разных ситуаций (первый и не первый раз), используем следующее условие:
if (d > R) and (not found or (d < min_d)) then ....
Когда появляется первая точка вне круга, истинно not found, а когда новая ближайшая точка — d < min_d.
►► Реализуйте приведенный алгоритм.
Задача 2.3. Вдоль координатной прямой размещены N отрезков; каждый отрезок задается координатами начала и конца xrain и х^. Нужно найти какую-либо точку, принадлежащую всем отрезкам, или сказать, что таких точек нет.
Вход. На стандартном входе задается число N (2 <N£ 100), затем по два числа в каждой из Nследующих строк (сначалах^, затем х^ очередного отрезка).
Выход. Если точки, принадлежащей всем отрезкам, не существует, вывести на стандартный выход слово No. Иначе в первой строке вывести слово Yes, во второй — координату точки (если точек много, то координату любой из них).
Примеры
Вход	Выход Вход	Выход
2	No	3	Yes
0	2	0 5	2.5
3	7	-1.5 3
2 9
Анализ задачи. Отрезок образован точками, координата х которых удовлетворяет неравенствам х^^х^х^. Значит, точка принадлежит нескольким отрезкам на прямой, если ее координата не меньше всехх^ и не больше всехх^.
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
49
Таким образом, достаточно найти максимум maxmin среди всех xmin и минимум minmax среди всехх^. Если maxmin< minmax, то в качестве искомой точки можно взять любую от maxmin до minmax включительно, иначе таких точек нет.
►► Реализуйте описанное решение.
Н На координатной плоскости заданы N прямоугольников, стороны которых параллельны осям координат; такие прямоугольники называются изотетичными. Каждый прямоугольник задан диапазонами координат [xmin; xj и	Нужно или найти какую-нибудь
точку, принадлежащую всем прямоугольникам, или сказать, что таких точек нет. Указание. Достаточно дважды решить задачу 2.3, для х-координат и у-координат (т. е. удобно оформить основную часть решения задачи 2.3 функцией и дважды вызвать ее). Прямоугольники пересекаются, если оба ответа о пересечении всех отрезков положительны.
2.1.2.	Максимальная сумма отрезка числовой последовательности
Задача 2.4. Отрезок последовательности целых чисел образуют числа, идущие в ней подряд. Напечатать номера чисел, которыми начинается и заканчивается первый отрезок с максимальной суммой, а также эту сумму.
Вход. Текст, в котором записана последовательность чисел, не равных 0. Признак окончания — 0, не входящий в последовательность. Гарантировано, что последовательность не пуста и что сумму и количество чисел любого отрезка можно представить в типе longint.
Выход. Номера первого и последнего числа и сумма чисел отрезка.
Примеры. Вход'. -2 -1 о; выход: 2 2 -г. Вход: 1 2-3 3 О', выход: 1 2 3.
Анализ задачи. Сразу заметим: если все числа последовательности отрицательны, то искомые отрезок и сумма — это первое максимальное число. Поэтому, если последовательность начинается отрицательными числами, запомним максимальное из них и его номер. Если положительных чисел нет, этим все и закончится.
Предположим, что хотя бы одно положительное число есть. Как организовать поиск отрезка с максимальной суммой? Вспомним, как ищется максимум числовой последовательности. Запоминаем первое число как максимум. Затем просматриваем последовательность и, если очередное число больше максимума, запоминаем его. Таким образом, в любой момент просмотра есть два значения— максимальное и очередное.
Аналогично используем в нашей задаче два отрезка в просмотренной части последовательности — с максимальной суммой maxS и текущей temps. Начало и окончание этих отрезков (номера чисел) обозначим begMaxS, f inMaxS, begTempS и f inTempS. Ясно, что первое положительное значение максимальной суммы набирается на первом отрезке, состоящем только из положительных чисел. Отрезок с неотрицательной текущей суммой начинается некоторым положительным числом и заканчивается последним прочитанным.
Предположим, что прочитана часть последовательности до элемента аг1 и запомнен отрезок с положительной максимальной суммой maxS и отрезок с текущей суммой 5, началом а. и окончанием аг1.
’	О	I 1
50
ГЛАВА 2
Если $>0, то положительный а, ее увеличит. Если S+a( >maxS то нужно изменить maxS, begMaxS и finMaxS. Отрицательный at уменьшит S, поэтому сравнение с maxS не нужно. Если же 5 <0, то текущий отрезок вообще не нужен — начнем его с at, если а,>0.
Решение задачи. Реализуем приведенные рассуждения в следующем фрагменте программы, где f — текст, а — очередное число, t — его номер в последовательности: read(f, а); t := 1;
maxS := a; begMaxS := t; finMaxS := t;
while a < 0 do begin
if a > maxS then begin
maxS := a; begMaxS := t; finMaxS »= t;
end;
read(f, a) ;
if a = 0 then exit
inc(t) ;
end;
{ a > 0 }
tempS := a; begTempS := t; finTempS := t;
maxS := a; begMaxS := t; finMaxS := t;
while true do begin
read(f, a); if a = 0 then exit else inc(t);
if tempS >= 0 then begin
temps := tempS+a;
if a > 0 then begin
finTempS := t;
if tempS > maxS then begin
begMaxS := begTempS; finMaxS := finTempS;
maxS :» tempS;
end;
end;
end
else { tempS < 0 }
if a > 0 then begin
tempS := a; begTempS := t; finTempS := t;
if tempS > maxS then begin
begMaxS :== begTempS; finMaxS := finTempS;
maxS := tempS;
end;
end;
end;
H Дополните приведенный фрагмент до программы.
2.1.3.	Инопланетная армия
Солдаты инопланетной армии перед походом строятся в колонну, поворачиваясь к командиру или правым, или левым боком. По команде они начинают готовиться к движению. Если двое соседних солдат стоят лицом друг к другу, оба за одну секунду разворачиваются на 180°. Развороты разных пар солдат происходят одновременно. Армия сможет начать движение, если в колонне не будет солдат, стоящих лицом друг к другу.
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
51
По начальному расположению солдат нужно определить, отправится ли когда-нибудь армия в поход, и если да, то через сколько секунд и какое общее количество разворотов выполнят солдаты.
Вход. Последовательность символов < и > длиной до 9104 в одной строке текста; < означает, что солдат стоит лицом налево, > — направо.
Выход. Время и общее количество разворотов (через пробел), если армия сможет отправиться в поход, иначе слово infinite.
Примеры
Вход	Выход
>><<	3	4
о	0	0
>><><	3	5
Анализ задачи. Первое, что приходит в голову — прочитать текст в массив символов и затем моделировать развороты, т.е. многократно проходить по массиву, разворачивая солдат, стоящих лицом друг к другу. Однако по условию солдат может быть почти 10s, поэтому такое решение проблематично. Есть и другие вопросы.
•	Могут ли развороты не заканчиваться? Как, моделируя их, узнать это?
•	ДОожно ли не имитировать развороты, а действовать более эффективно? Ведь в задаче нужны не отдельные развороты, а только их количество.
Попробуем ответить. Длина колонны конечна, и каждый солдат может находиться в одном из двух состояний (лицом налево или направо), поэтому множестве всех возможных состояний колонны также конечно. Предположив, что развороты происходят бесконечно, придем к выводу, что некоторое состояние колонны (не обязательно начальное) повторится. Однако заметим, что развороту одной пары соответствует перестановка символов < и >: < перемещается на одну позицию влево, а > — вправо. При перестановках знаки < “дрейфуют” влево, > — вправо, поэтому состояния не повторяются, т.е. процесс разворотов обязательно закончится. Перестановки продолжаются, пока где-нибудь есть пара символов ><, и прекращаются, когда все символы < будут собраны слева, а все > — справа. Это наблюдение дает ключ к эффективному решению задачи.
Начнем с общего количества разворотов. Рассмотрим какой-нибудь символ <. При каждой перестановке он меняется местами с символом > слева и прекращает свое движение, обменявшись со всеми символами >, вначале находившимися слева от него. Итак, количество перестановок, в которых принимает участие символ <, — это количество символов > слева от него.
Читая входную последовательность, нетрудно подсчитывать символы > и с каждым символом < добавлять к общему числу разворотов текущее количество прочитанных символов >. Например, если на входе 45 тысяч символов > и за ними столько же символов <, то общее количество разворотов будет равно 450002= 2,025-109. Нетрудно убедиться, что это число — максимальное количество разворотов при длине входа до 90 тысяч.
Вопрос о количестве секунд менее прозрачен. Символ < может простаивать, т.е., не закончив своего движения влево (в частности, не начав двигаться), в некоторые моменты оставаться на месте. Это происходит, когда его сосед слева — символ <, ко
52
ГЛАВА 2
торый со временем должен “отдрейфовать” влево. Будем говорить, что символ имеет простой п, если в общей сложности он должен простоять п секунд.
Рассмотрим последний (самый правый) символ <. Однотипные символы не могут перепрыгнуть друг через друга, поэтому именно он примет участие в последнем развороте. Итак, общее время равно числу символов > слева от последнего символа < плюс простой этого символа.
Чтобы понять, как вычислить простой последнего символа <, рассмотрим примеры. В последовательности >><< есть два символа <. Простой первого из них равен О, второго — на 1 больше, т.е. простой последнего символа < равен 1, время его движения — 2 (два символа > левее его), поэтому всего тактов 3.
В >><>< нет простоев: после первого < идет >, не позволяющий второму символу < “догнать” первый. Общее количество тактов 3 определяется только временем движения (3 символа > левее последнего <).
В »««»< четыре символа < идут подряд, и простой каждого на 1 больше, чем простой предыдущего, — соответственно 0, 1, 2,3. Пятый символ <, если бы шел подряд за первыми четырьмя, имел бы простой 4. Однако перед ним идут два символа >, поэтому он может начать двигаться, когда предыдущие < еще стоят, и его простой на два меньше, т.е. 2. Он должен двигаться 4 такта, поэтому всего тактов 6.
Из этих примеров можно сделать такие выводы.
•	Символы < в начале последовательности, идущие подряд, вообще не участвуют в перестановках, и на них можно не обращать внимания.
•	Один или несколько символов >, идущих подряд, и один символ < после них не создают простоев.
•	Если символы < идут подряд, каждый следующий должен дождаться, пока сдвинется с места предыдущий и на его месте появится >. Поэтому для пары символов « простой правого из них на 1 больше простоя левого (при условии, что левее был хотя бы один символ >).
•	Пусть начало строки заканчивается символом < с простоем п (и>0), затем идут т (mSl) символов >, а после них — символ <. Обозначим первый из упомянутых символов < через р, второй — через а. Найдем простой символа а, который остановится, догнав р. Если а догонитр уже после того, как тот займет свое окончательное место, простой а будет равен 0. В противном случае из п тактов простоя р символ а “использует” т, чтобы догнать р. Поэтому с учетом предыдущего пункта простой а будет равен п-т+\.
Итак, простой первого символа < равен 0; каждый символ < увеличивает возможный простой на 1 (если ранее был хотя бы один символ >), а каждый символ > — уменьшает на 1 (если простой был положительным).	»
Решение задачи. Вычисление простоя и общего количества поворотов реализовано в следующей программе (листинг 2.1).
Листинг 2.1. Подсчет тактов и разворотов
program Aliens;
var fv : text;
N_left, { количество символов > слева }
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
53
N_turn, { общее количество разворотов }
N_tact, { общее количество тактов } stay : longint;
{ простой следующего символа <, если он появится } begin
assign('Alierts.dat'); reset(fv); read(fv, a);
N_left := 0; N_turn := 0; N_tact := 0; stay := 0;
repeat
if a = then begin
N_left := N_left+1;
if stay > 0 then stay := stay-1 end
else { a = '<' } begin
N_turn := N_turn+N_left;
N_tact := N_left+stay;
if N_left > 0 then stay := stay+1;
end;
if eof(fv) then a := #27
else read(fv,a);
until not (a in ['<', ’>']);
writein(N_tact, 1 N_turn);
end.
Как видим, для каждого символа входа выполняется ограниченное число необходимых действий, поэтому других решений, существенно лучших по времени, просто не может быть. Количество скалярных переменных в программе не зависит от длины входа, которая ограничена лишь тем, что количество разворотов необходимо представлять в компьютере.
Читатели, которых заинтересовало, как отследить возможные зацикливания, могут познакомиться с одним из методов такого отслеживания в подразделе 4.4.1.
2.1.4.	Стрельба из двуствольной пушки
Задача 2.5. Вам нужно разбить бронированные плиты на оборонной башне | противника, которая в плане имеет вид правильного jV-угольника. Для каждой | стороны известно количество прикрывающих ее плит. Стрельба ведется из I специальной двуствольной пушки — она ездит по рельсам вокруг башни и за I один выстрел разбивает (по вашему желанию) или две плиты на одной стороне I башни, или по одной плите на двух соседних сторонах. Найти наименьшее I число выстрелов, необходимых для разрушения всех плит.	I
Вход. В первой строке текста — количество тестов М, в каждой из следующих М строк — тест. Тест задает число сторон N (3 <N< 107) и N целых неотрицательных чисел — количества плит на сторонах. Гарантировано, что общее количество плит на башне можно представить в типе longint.
Выход. В одной строке количества выстрелов, разделенные пробелами.
Примеры
Тесту 3 13 1 соответствует выход 3, тесту 3 12 1— выход 2, тесту 3 1 0 1 — выход 1.
54
ГЛАВА 2
Анализ и решение задачи. Возможное количество сторон башни подсказывает, что запомнить все количества плит на сторонах башни нельзя и нужно обрабатывать числа по мере чтения текста. Заметим сразу: если сумма всех чисел нечетна, то хотя бы один выстрел будет разбивать только одну плиту.
Для начала разберем ситуацию, в которой все числа ар а2, ..., ая ненулевые. Если a^lk, т.е. четно, то с помощью к выстрелов разобьем все плиты на первой стороне — суммарная четность числа оставшихся плит не изменится. Если a,=2fc+l, т.е. нечетно, то с помощью к выстрелов разобьем все плиты на первой стороне, кроме одной, а еще одним выстрелом разобьем ее и одну плиту на второй стороне. Затем аналогично поступаем с оставшимися плитами на второй стороне. И так далее, пока не дойдем до последней стороны. Если общее количество плит S четно, “холостых” выстрелов не будет, иначе он будет один. Итак, общее число выстрелов выражается какТ= (S+l)div2.
Если среди вр аг, ..., ая есть нулевые, то нужно выделить отрезки, состоящие из ненулевых чисел, для каждого найти общее число плит и количество выстрелов. Особенная ситуация — когда отрезок начинается (ненулевым) а,, заканчивается перед некоторым нулем, а последний отрезок заканчивается ал. Однако башня — многоугольник, поэтому первый из этих отрезков является продолжением второго. И если на обоих оказались нечетные суммы, то один выстрел лишний. Значит, нужно запомнить признак нечетности суммы на первом отрезке и учесть его, подсчитывая выстрелы на последнем отрезке.
Программа, реализующая описанные действия, представлена в листинге 2.2).
Листинг 2.2. Подсчет количества выстрелов
program WallCrsh; var f ; text; a : integer; totShots, sum : longint; fZero, isZero,
очередное число}
общее количество выстрелов количество плит на отрезке признак того, что а(1) = О признак того, что есть нули}
1walltst.txt1); reset(f); nTest);
:= 1 to nTest do begin
oddSuml : boolean; {нечетность первой суммы}
n, i , nTest, iTest : longint;
begin
assign(f, readln(f, : for iTest read(f, : totShots := 0; sum := 0;
fZero := false; isZero := false; oddSuml := false; read(f, a); i := 1;
if a = 0 then begin
fZero := true; isZero := true;
end
else inc(sum, a);
while (i < n) do begin inc(i); read(f, a); if a = 0 then begin
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
55
if not isZero then begin
{сохраняем признак нечетности первой суммы} isZero := true; oddSuml := odd(sum);
end;
inc(totShots, (sum+1) div 2);
sum := 0; {перед началом следующего отрезка}
end
else inc(sum, a);
end;
{i = n}
if a<>0 then begin {учет последнего отрезка}
if not fZero and isZero
{первый отрезок - продолжение последнего}
then inc(totShots, (sum+1-ord(oddSuml)) div 2)
else inc(totShots, (sum+1) div 2);
end;
if iTest > 1 then write(' '); write(totShots);
end;
end.
2.2. Чтение и обработка символов
2.2.1.	Удаление пробелов
Задача 2.6. В строке текста записаны слова, разделенные пробелами в произвольном количестве. Сжатие текста состоит в том, что между словами оставляется по одному пробелу, а после последнего слова пробелы удаляются (пробелы перед первым словом Сохраняются). Сжатый текст записать в другой файл. Если строка содержит только пробелы, то все они копируются.
Вход. Строка в тексте Despace. txt неограниченной длины.
Выход. Строка в тексте Despace. sol.
Анализ и решение задачи. На этой простой задаче мы познакомимся с тем, как в обработке текстов применяются конечные автоматы.
По условию задачи:
•	пробелы перед первым словом копируются в другой текст;
•	после каждого слова, кроме последнего, из серии пробелов выводится только один;
•	после последнего слова они вообще не выводятся.
Как видим, реакция на пробел зависит от того, в какой части входной строки находится прочитанный символ. Ясно, что этими частями строки являются, как минимум, часть перед первым словом и часть после него.
Кроме того, если после слова появился пробел, его нельзя сразу копировать — неизвестно, было ли это слово последним. Будем выводить пробел, идущий после слова, только при появлении следующего слова. Значит, нужно по-разному реагировать на очередную литеру, когда слово продолжается, и когда литера появляется после пробела и слово не первое. В первой ситуации выводится прочитанная литера, во второй — пробел и литера.
56
ГЛАВА 2
Итак, у нас есть три различные части строки: “перед первым словом”, “после литеры слова”, “после пробела”. Назовем их состояниями. По текущему состоянию можно определить, в какое состояние переходит текст после чтения очередного входного символа. Например, из состояния “перед первым словом” литера переводит в состояние “после литеры слова”, а пробел оставляет в том же состоянии. Изменение состояния в зависимости от текущего состояния и прочитанного символа называется переходом.
Изобразим переходы между состояниями. Обозначим состояния, указанные выше, цифрами соответственно 0,1,2. Добавим еще состояние “конец текста”, возникающее при окончании входной строки, и обозначим его цифрой 3. В этом состоянии работа завершается.
Переходы изобразим на диаграмме переходов. Круги обозначают состояния, а стрелки, отмеченные символами, — переходы. Переход по стрелке происходит тогда и только тогда, когда текущим является состояние в начале стрелки и на входе прочитан отмечающий ее символ. Символы, отличные от пробела, обозначим буквой а. Окончание текста обозначим “символом” eof. При некоторых переходах в выходной текст выводятся символы — укажем их справа от косой черты если при переходе символы не выводятся, отсутствует (рис. 2.1).
Рис. 2.1. Диаграмма состояний при сжатии пробелов
Зададим переходы также в следующей таблице переходов (табл. 2.1). В состоянии 3 переходов нет, поэтому в таблице его нет.
Таблица 2.1. Переходы между состояниями
	Символ		
Состояние	а	1 * (пробел)	eof
0	1/ а	0/ 1 1	3
1	1/ а	2	3
2	1/ 1 ' ,а	2	3
По диаграмме или таблице переходов легко написать программу чтения и обработки символов текста. В основе программы лежит цикл, в котором очередной символ считывается и обрабатывается с учетом текущего состояния. Представим состояние значением целой переменной state, очередной символ — переменной с типа char. Начальным состоянием (перед обработкой входа) является 0.
Внутри основного цикла программы, по сути, реализована таблица переходов. Текущее состояние определяется с помощью case state of .... В вариантных
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
57
операторах указано, как обрабатывается символ и изменяется состояние. Состояние 3 в программе не представлено — переходу в него при появлении eof соответствует прерывание цикла и завершение работы (листинг 2.3).
Листинг 2.3. Программа сжатия пробелов
program Despace;
type tState = 0..2;
var f, g : text; c : char; state : tState;
begin
assign(f, ’Despace.txt'); reset(f);
assign(gt 'Despace.sol'); rewrite(g);
state j= 0;
while true do begin { цикл выполнения переходов } if eof(f) then break; read(f, c);
case state of
0 : begin write(g, c);
if с <> ' ' then state := 1 end;
1	Г* if c <> 1 ' then write(g, c) else state := 2;
2	: if c <> ' ' then begin write(g, ' ', c); state := 1
end;
end; { оператор case }
end; { достигнут конец текста } close(f); close(g);
end.
Неформально, конечная система состояний и правил переходов между ними под воздействием входных символов называется конечным автоматом. Автомат функционирует, читая входные символы и изменяя свои состояния. С помощью состояния автомата, можно, например, представить множество цепочек входных символов, приводящих в это состояние. Тогда своими состояниями автомат распознает множества цепочек.
2.2.2.	Удаление комментариев
Задача 2.7. Комментарий в Паскаль-программе — это последовательность символов, которая начинается “ (*”, заканчивается “*)” и не содержит “*) ” внутри. Прочитать текст Паскаль-программы, удалить в нем комментарии и вывести в другой текст. Разбиение нового текста по строкам не имеет значения.
58
ГЛАВА 2
Анализ задачи. Как и в предыдущей задаче, выделим состояния в тексте:
out — “вне комментария”,
begc — “начало комментария” (прочитана открывающая скобка), comm — “внутри комментария” (прочитана * после скобки), endc — “окончание комментария” (прочитана * внутри комментария).
В записи комментария в тексте используются символы (, ), *; любой другой символ обозначим через а. Переходы между состояниями и действия при обработке текущего символа с представим в таблице. В данной задаче нужно только выводить символы в новый текст, поэтому после знака вместо обозначения действия укажем только выводимые символы. Если состояние при переходе не изменяется, записывать его не будем.
	Символ			
Состояние	(	*	)	a
out	begc/	/ *	/ )	/ a
begc	(	comm	out/ (, )	out/ (,a
comm		endc		
endc	comm		out	comm
н Реализуйте в программе представленный автомат для удаления комментариев.
Задача 2.8. В тексте Паскаль-программы нужно удалить комментарии вида (* ... *) (разбиение текста по строкам не имеет значения), но с учетом того, что “скобки” (* и *) могут находиться внутри литералов. Литерал — это последовательность символов в апострофах. Апострофы также могут быть внутри комментариев.
Анализ задачи. Задача аналогична предыдущей. К “особым” символам (, ), * добавлен апостроф “1 ”, а обозначает любой другой символ. Добавим состояние ltr, означающее, что в тексте начался литерал. Переходы между состояниями при обработке текущего символа с представим в следующей таблице (после знака указаны только выводимые символы).
	Символ				
Состояние	(	•	)	1	a
out	begc/	/ *	/ )	ltr/ '	/ a
begc	/ (	comm/	out/ (, )	ltr/ (	out / (,a
comm	/	endc/	/	/	/
endc	comm/	/	out/	comm	comm/
ltr	/ (	/ *	/ )	out/ '	/ a
Отметим, что двойные апострофы внутри литералов обрабатываются этим автоматом правильно, хотя эта ситуация и не выделена как особая. Однако если бы литералы нужно было не просто копировать, а преобразовывать, действия пришлось бы усложнить...
н Реализуйте представленный автомат в программе.
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
59
2.2.3.	Чтение и вычисление многочлена
Задача 2.9. Многочлен с переменной х записывают, соблюдая такие правила:
•	многочлен состоит из слагаемых со знаками + или - между ними; количество слагаемых — от 1 до 100;
•	каждое слагаемое представляет собой или произведение натурального коэффициента на степень х (например, 5*хА2), или только степень х (например, хА3), или только коэффициент (например, 7);
•	степень записывается в виде: буква х (строчная латинская), знак А, показатель степени (натуральное число);
•	в выражении нет пробелов; после него в конце строки записан один пробел.
Вычислить значение многочлена при заданном действительном значении х.
Вход. Первая строка текста polynom. dat содержит значение х, вторая — многочлен, записанный По указанным правилам. Коэффициенты и показатели степени представимы в типе longint.
Выход. Результат (число) в строке текста polynom. sol. Точность, обеспеченную типом real, считать достаточной, результат не округлять.
Призеры
Вход	Выход	Вход	Выход
0.7 ХаЗ-5*ха2+7	4.8930000000е+00	2 хА12-1000*хА2+7	1.0300000000е+02
Анализ и решение задачи.
В общем виде решение задачи таково: читаем слагаемые, умножаем коэффициенты в них на степени переменной х и накапливаем их в сумме, учитывая знаки перед слагаемыми.
Для вычисления степени х" можно использовать цикл for i : = 1 to n do p : = p*x. Однако в многочлене могут быть десятки слагаемых и показатели порядка миллиона и больше, поэтому такие циклы будут “крутиться” слишком долго. Формула х’’=в’"1”1 дает ответ быстро, но применима только к положительным значениям х. Поэтому при х# 0 вычислим модуль степени |х>’| = /ta|x и учтем знак х. Для возведения в степень можно использовать и другие способы, например, “индийский” алгоритм (подробности — в главе 3).
Однако основная сложность в этой задаче — правильно прочитать и проанализировать многочлен, выделив его коэффициенты и степени в слагаемых и знаки операций между ними. Рассмотрим два подхода к организации анализа.
Анализ на основе процедур. Основная структурная часть многочлена — слагаемое (одночлен, терм). Чтение терма оформим отдельной подпрограммой ReadTerm, вызывая ее в цикле для обработки всего многочлена. Терм может содержать коэффициент и показатель степени — натуральные числа; их чтение опишем отдельной процедурой Readlnt.
В некоторые моменты неизвестно, какого вида символ должен быть в выражении дальше. Например, терм может начинаться числом (коэффициентом) или обозначе
60
ГЛАВА 2
нием переменной ' х'. После числа может идти ' * 1 (продолжение терма), ' +1, ' -' (переход к следующему одночлену) или пробел, обозначающий окончание всего многочлена. Чтобы решить, как дальше обрабатывать вход, нужно знать следующий символ. Но для этого его нужно заранее прочитать из текста. Для хранения очередного символа, прочитанного из текста, но еще не обработанного, используем глобальную переменную ch типа char.
Организуем программу и процедуры так, чтобы первый символ терма и натурального числа был прочитан перед соответствующим вызовом ReadTerm и Readlnt, а символ, следующий за термом или числом (' + ',	или пробел), — в преде-
лах этого вызова.
Процедура Readlnt читает из текста запись целого числа и возвращает его через параметр-переменную. Первая цифра числа прочитана в ch перед вызовом процедуры; далее в цикле читаются следующие цифры, пока в ch не появится символ, идущий после числа.
Рассмотрим чтение терма. По условию терм может быть или коэффициентом, умноженным на степень х, или только коэффициентом, или только степенью х. Процедура ReadTerm читает терм, возвращая его значение при заданном значении х.
В терме может не быть степени х, т.е. терм содержит только коэффициент, после которого идет не ’ * 1. Для учета этой возможности используем булеву переменную do_read_deg: она инициализируется значением true, которое заменяется на false только в указанной ситуации..
Условие ch in DIGITS (“ch является цифрой”) выясняет, начинается ли терм коэффициентом. Если это так, читаем коэффициент с помощью процедуры Readlnt. Чтение степени х состоит в том, что символы ' х' и ' л ’ пропускаются, а показатель степени читается с помощью Readlnt (листинг 2.4).
Листинг 2.4. Вычисление полинома (процедурный вариант)
program Polynom_Procedures;
const DIGITS = [1 0 ' . . ' 9 ' ] ;
var fv : text;
x, { переменная }
term, sum : real; { значение слагаемого и сумма } sign : longint; { знак перед термом } ch : char;	{ прочитанный символ }
procedure ERROR;
begin
writein('ERROR!!!');
readln
end;
function power(a : real; n : longint) : real;
var res : real;
begin
if a = 0 then power := 0 else begin res := exp(n*ln(abs(a)));
if (a<0) and odd(n) then power := -res
else power := +res
end
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
61
end;
procedure Readlnt(var fv : text; var n : longint);
begin { чтение целого числа в параметр п }
п : = 0 ;
while ch in DIGITS do begin
n := n*10 + ord(ch) - ord('O'); read(fv, ch);
end; end;
procedure ReadTerm(var fv : text; var value : real);
{ чтение и вычисление значения терма }
var koef, deg : longint; { коэффициент и степень } do_read_deg : boolean; { признак чтения показателя }
begin
do_read_deg := true;
i£ ch in DIGITS then begin
Readlnt(fv, koef); 1
if ch = '* 1 then read(fv, ch)
else do_read_deg i= false end else laoef := 1;
if doreaddeg then begin
if ch о 'x1 then ERROR;
read(fv, ch);
if ch <> |X| then ERROR;
read(fv, ch);
Readlnt(fv, deg);
end
else deg := 0;
term := koef*power(x, deg) end;
begin
assign(fv, 'polynom.dat'); reset(fv);
readln(fv, x);
sign := +1; sum := 0;
repeat
read(fv, ch);
ReadTerm(fv, term); sum := sum + sign*term;
if ch = '+' then sign := 1 else
if ch = then sign := -1 else
if ch = 1 1 then break
else ERROR;
until ch = ' 1 ; close(fv);
assign(fv, 'polynom.sol'); rewrite(fv);
writein(fv, sum); close(fv) end.
62
ГЛАВА 2
•	Если система программирования позволяет смотреть очередной символ текста, не считывая его из файла, или возвращать текущую позицию файла на символ назад, программу можно сделать проще и красивее.
♦	Для хранения символов выражения мы сознательно не использовали тип string. Во-первых, этот тип не нужен для решения подобных задач. Во-вторых, длина строк этого типа не более чем 255 (впрочем, в последних версиях Delphi это ограничение снято).
♦	Предполагается, что входные данные корректны, т.е. строка текста содержит только правильно записанный многочлен. Поэтому процедура error и все ветки, где она вызывается, в принципе не нужны. Однако эти ветки облегчают проверку, следует ли программа правилам построения выражения.
Уточним несколько терминов, связанных с анализом выражений.
Отдельные части текста, которые обозначают некоторое содержание и рассматриваются как неделимые (слова, имена, константы, знаки операций, разделители и т.п.), называются лексемами. Выделение лексем в тексте называется лексическим анализом.
Лексемы в многочлене — это коэффициенты и показатели степени (они являются целыми константами), знаки операций л и буква х. В частности, процедура Readlnt выполняет лексический анализ целых констант и получение соответствующих им чисел.
Однако, обрабатывая многочлен, мы не только выделяем лексемы, но и анализируем его структуру, описанную в условии. Например, весь многочлен является суммой слагаемых, а слагаемое — произведением коэффициента на степень.
Структуру выражений называют синтаксисом, а анализ структуры — синтаксическим анализом.
Таким образом, процедура ReadTerm выполняет синтаксический анализ и вычисляет значение отдельного слагаемого в многочлене, а анализ всего многочлена происходит при выполнении основного цикла программы.
Анализ на основе автоматов. Аналогично задаче 2.6, определим состояния, представляющие части входного выражения, в которые приводят прочитанные символы. Все части выражения и состояния, обозначенные номерами от 0 до 5, приведены в табл. 2.2 (к ним добавлено состояние 9 для окончания многочлена).
Таблица 2.2. Состояния автомата и их описание
Состояние	Часть входного выражения
0	Сейчас начнем читать терм
1	Внутри коэффициента
2	Прочитали символ *, ожидаем х
3	Прочитали символ х, ожидаем А
4	Сейчас начнем читать показатель степени
5	Внутри показателя степени
9	Окончание многочлена
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
63
По правилам записи многочлена нетрудно определить переходы нашего автомата. Например, из состояния 0 (“сейчас начнем читать терм”) входная цифра переводит в состояние 1 (“внутри коэффициента”), а символ х — в 3 (“прочитали символ х, ожидаем*”). Все переходы между состояниями представлены на диаграмме (рис. 2.2).
Представим переходы также в табл. 2.3.
Таблица 2.3. Переходы при чтении многочлена
	Символ					
Состояние	0-9	*	X	А	+, -	пробел
0	1		3			
1	1	2			0	9
2			3			
3				4		
4	5					
S	5				0	9
9						
Аналогично задаче 2.6, по диаграмме или таблице переходов напишем программу чтения и обработки полинома. В ней нужно не только выполнять переходы, но и проводить семантическую (смысловую) обработку прочитанных символов. По правилам записи многочлена, перед первым термом не может быть знака поэтому вначале запомним знак слагаемого sign-. = +l. Начальное значение суммы, как всегда, sum -. = 0.
Переходы реализуем в основном цикле программы. Текущее состояние определяется с помощью “большого case” (case STATE of...), а очередной символ в большинстве случаев — с помощью соответствующих “малых case”. В вариантных операторах указано, как изменяется состояние и как обрабатывается символ. Проиллюстрируем программный код, реализующий смысловую обработку символов в состояниях 1 и 2.
В состоянии 0 осмысленный переход возможен, если очередной символ — цифра или 'х'. Цифру (старшую цифру коэффициента) переводим в число— koef : = ord (ch) -ord (1 0 1). Поскольку в кодовых таблицах символы 'О', ' 11, ..., '9'
64
ГЛАВА 2
идут подряд, ord (ch) -ord (’О') будет нужным числовым значением цифры. Затем собственно переходим в состояние 1 — STATE : = 1. Если очередной символ — 1 х', то, прежде чем перейти в состояние 3, запоминаем значение коэффициента — koef : = 1. Если же в состоянии 0 очередной символ — и не цифра, и не ' х1, выведем сообщение об ошибке и “досрочно” перейдем в заключительное состояние 9.
В состоянии 1 очередной символ 1 +1 или 1 - ' означает завершение текущего слагаемого, не содержащего степени х. Вычислим слагаемое как sign*koef и добавим к общей сумме sum. Затем запомним в sign прочитанный знак следующего слагаемого и перейдем в состояние 0. Если текущий символ — пробел, также выполним sum : = sum+sign*koef, но перейдем в заключительное состояние 9.
• Если правильность входного выражения гарантирована, то контроль ошибок в нем не обязателен, но желателен, поскольку помогает создать правильную программу. В реальных же программах, связанных с обработкой выражений, контроль ошибок (как правило, более подробный) совершенно необходим.
В программе (листинг 2.5) использованы средства отладки, прокомментированные ниже. Они не имеют никакого отношения к алгоритму решения задачи, но помогают при отладке программы.
Листинг 2.5. Вычисление полинома (автоматный вариант) *
program Polynom_Automat;
($define ECHO}
{$define STRING_ECHO}
var fv : text;	{входной текст}
x, sum : extended; {переменная и значение многочлена) koef, deg, sign : longint; {коэффициент, степень, знак} ch : char; {текущий символ} STATE : integer; {текущее состояние}
{$ifdef STRING_ECHO}
s : string; {строка для отладки}
{$endif}
function power(a ; extended; n : longint) ; extended;
var res : extended;
begin
res := exp(n*ln(abs(a)));
if (a<0) and odd(n) then power := -res
else power := +res end ;
begin
assign(fv, 'polynom.dat'); reset(fv) ;
readln(fv, x);	t
{$ifdef ECHO} writein(1 restart...'); {$endif}
{$ifdef STRING_ECHO} s := 11; {$endif}
sign := +1; sum := 0; STATE := 0;
while true do begin
if not eoln(fv) then read(fv, ch) else ch := ' ';
{$ifdef ECHO} write(ch); {$endif}
{$ifdef STRING_ECHO} s := s+ch; {$endif}
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
65
case STATE of
О : case ch of
' 0' . . 1 9 ' : begin koef := ord(ch)-ord('01) ; STATE := 1;
end;
1x1 : begin koef := 1; STATE := 3;
end;
else begin
{$ifdef ECHO} writein('ERROR. STATE := 9
end
end; {case ch}
1 : case ch of
); {$endif}
' 0'..'91 : begin
koef := koef*10+ord(ch)-ord(101) ;
STATE := 1;
end;
: begin
sum := sum + sign*koef;
if ch=1 -1 then sign := -1
else sign : = +1;
STATE := 0;
end;
J*' : STATE := 3;
1 ' : begin
sum := sum + sign*koef;
STATE := 9;
end;
else begin
{$ifdef ECHO} writein('ERROR.; {$endif| STATE := 9
2
end
end; {case ch}
case ch of
'x' : STATE := 3;
else begin
{$ifdef ECHO} writein('ERROR..
STATE := 9
{$endif}
end
end; {case ch}
3	: case ch of
,x' : STATE := 4;
else begin
{$ifdef ECHO} writein(1 ERROR.; {$endif
STATE := 9 end end; {case ch}
4	: case ch of
' 0 ' . . ' 9 ' : begin
deg := ord(ch)-ord('01); STATE . a 5;
66
ГЛАВА 2
end;
else begin
{$ifdef ECHO} writein('ERROR...') ; {$endif}
STATE := 9
end
end; {case ch}
5	: case ch of
101..1	91 : begin
deg := deg*10+ord(ch)-ord('0');
STATE := 5;
end;
begin
sum := sum + sign*koef‘power(x, deg);
if ch='-' then sign := -1
else sign := +1;
STATE := 0;
end;
' * : begin
sum := sum + sign*koef*power(x, deg);
STATE := 9;
end;
else begin
{$ifdef ECHO} writein('ERROR...1); {$endif}
STATE := 9
end
end; {case ch}
9 : break; {выход из цикла while}
end; {case STATE}
end; {while true}
close(fv);
assign(fv, 'polynom.sol1); rewrite(fv);
writein(fv, sum); close(fv) end.
Данная программа организована настолько специфично, что при ее отладке трудно отследить, “где мы сейчас находимся”, т.е. какой символ обрабатывается на том или ином шаге выполнения и какие символы были перед ним. Для облегчения этой задачи использованы средства отладки.
В начале программы записаны определения имен компилятора {$def ine ECHO} и {$define STRING_ECHO}. Первое позволяет в указаниях условной компиляции {$ifdef ECHO}...{$endif} выводить каждый прочитанный символ на экран, второе — дописывать в конец вспомогательной строки s, на которую можно посмотреть в окне отладчика. Возможность увидеть прочитанные символы на экране или в окне отладчика существенно облегчает процесс отладки.
Однако по условию программа должна выводить только окончательный числовой ответ и больше ничего. В принципе достаточно, закончив отладку программы, удалить вспомогательные фрагменты. Одно плохо — как узнать, что отладка программы закончена?
В этой ситуации весь “вспомогательный инвентарь”, не нужный в окончательном тексте программы, можно разместить внутри указаний условной компиляции {$ifdef имя} ... {$endif}. Смысл этих указаний таков: если имя, записанное
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
67
после ifdef (в программе это имена STRING_ECHO и ECHO), определено, то компилятор воспринимает текст между командами {$if def имя} и {$endif} как обычную часть текста программы. Если это имя не определено, то текст между командами {$if def имя} и {$endif} при компиляции игнорируется.
Чтобы определить имя в момент компиляции, нужно выше по тексту программы написать {$def ine имя}. Затем, когда понадобится, чтобы компилятор игнорировал вспомогательные операторы, достаточно вставить в директивы {$def ine имя} пробел между символами { и $. Компилятор воспримет текст { $def ine имя} как комментарий, а не определение имени, и проигнорирует операторы между {$ifdef имя} и{$endif}.
Для управления компиляцией можно использовать имена, определенные не только пользователем, но и самим компилятором. Например, имя VER70 определено в Borland и Turbo Pascal 7.0 и не определено в Free Pascal, даже когда включен режим совместимости с Turbo Pascal 7.0. Предположим, что окончательный вариант программы должен компилироваться в Free Pascal с большими массивами, а для отладки применяется привычная среда Bprland Pascal. Тогда фрагмент программы {$ifdef VER70] const MAXN=500;
{$else}	const MAXN=10000; {$endif}
без каких бы то ни было изменений можно компилировать как в Borland Pascal (получая const maxn=500), так и в Free Pascal (получая const MAXN=10000).
» Модифицируйте обе приведенные программы, чтобы вместо хА1 можно было писать просто х.
2.2.4.	Языки скобок
Задача 2.10. Последовательность открывающих и закрывающих скобок ( и ) называют скобочным выражением. Скобочное выражение называют правильным, если:
а)	скобки сбалансированы и в каждой паре скобка вначале открывается, а затем закрывается;
б)	скобка закрывается после того, как закрыты все скобки, открытые внутри нее.
Выяснить, образуют ли скобки правильное выражение.
Вход. Каждая строка текста balance. txt содержит отдельный тест; количество строк не ограничено, а признаком окончания является конец текста. Длины строк от 1 до 210’. Гарантировано, что в строках нет символов, отличных от скобок.
Выход. Строка (в файле balance. sol) из символов 0 и 1, соответствующих тестам (1, если выражение в тесте правильно, иначе 0).
Пример
Вход 0 0 Выход НОО
(0)0
(О))
ОН
68
ГЛАВА 2
Решение задачи. Используем счетчик NOpen: его значением будет количество открытых и еще не закрытых скобок в прочитанной части строки. В начале каждой строки NOpen = 0. Строку со скобочным выражением будем читать по одному символу, увеличивая NOpen на 1 с каждой • (’ и уменьшая на 1 с ') *. Если появилась закрывающая скобка без предыдущей открывающей, получим NOpen < о. В этой ситуации выражение неправильно — остаток строки можно пропустить, не анализируя дальше. Если по окончании строки NOpen# 0, количество скобок несбалансирова-но, т.е. выражение неправильно. Признак правильности выражения запомним в булевой переменной ок.
После обработки строки выведем результат— ord (ок). Описанные действия уточним следующей программой:
program BinLang;
var f, g	:	text;	{файлы входа и выхода}
с	:	char;	{текущий	символ}
NOpen	:	longint;	{счетчик	скобок}
ok	:	boolean;	{признак	правильности}
begin assign(f, 'balance.txt'); reset(f);
assign(g, 'balance.sol'); rewrite(g);
while true do begin
if eof(f) then break;
{обработка новой строки}
NOpen := 0; ok := true; {скобок нет, все o'k} while true do begin
if eoln(f) then begin {строка закончилась} ok := (NOpen = 0); {признак правильности} break
end;
read(f, c);
if c = '(1 then inc(NOpen)
else dec(NOpen);
if NOpen < 0 then begin
ok := false; break {баланс нарушен} end;
end;
writefg, ord(ok)); {вывод результата}
readln(f);	{пропуск остатка строки}
end; close(f); close(g);
end.
Задача 2.11. В скобочном выражении (см. условие предыдущей задачи) могут быть скобки четырех типов: (и), [ и ], { и }, < и >. Скобочное выражение называют правильным, если скобки в нем сбалансированы и правильно вложены, причем скобка закрывается скобкой того же типа (круглая — круглой, фигурная — фигурной и т.д.). Выяснить, образуют ли скобки правильное выражение.
Вход. Текст balance. txt содержит тесты; последовательность символов теста расположена в нескольких строках, тесты отделены одной или несколь
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
69
кими пустыми строками. Количество строк не ограничено, а признаком окончания является конец текста. Каждый тест содержит не больше 105 символов, Гарантировано, что в строках нет символов, отличных от скобок.
Выход. Строка (в файле balance. sol) из символов 0 и 1, соответствующих тестам (1, если выражение в тесте правильно, иначе 0).
Пример *
Вход (((({}[] Выход 1°
))))
<{>}
Решение задачи. Построим программу, постепенно уточняя необходимые действия.
Входной текст f представляет собой последовательность выражений (тестов), отделенных несколькими пустыми строками. Следовательно, пока в тексте не прочитаны все выражения, нужно пропускать пустые строки, останавливаться в начале следующего выражения и обрабатывать его.
Пропускать пустые строки перед следующим выражением будем с помощью функции newTest. Она останавливается, обнаружив начало выражения или конец файла, и возвращает признак того, что следующее выражение есть.
function newTest(var f : text) : boolean;
begin
while eoln(f) and not eof(f) do readln(f);
newTest := not eof(f) end;
Обработку выражения оформим функцией procTest, возвращающей булев признак его правильности. Этот признак в виде 1 или 0 нужно вывести в текст д. Итак, обработка текста будет иметь такой общий вид:
while newTest(f) do writein(g, ord(procTest(f)));
Уточним функцию обработки выражения procTest. Обработка выражения состоит в том, что последовательные символы выражения вводятся и обрабатываются.
Получение символа с из текста f оформим функцией с таким заголовком: function getC(var f : text; var c : char) : boolean;
Она присваивает своёму параметру следующий символ выражения или константу #0, если выражение закончилось, и возвращает признак того, что получен символ выражения. Уточним функцию getC.
Выражение записано в нескольких строках, поэтому перед чтением нового символа проверим, не закончилась ли текущая строка. Если не закончилась, прочитаем и возвратим новый символ.
Если строка закончилась, проверим, не закончилось ли выражение, т.е. закончился текст или следующая строка пуста. Если выражение закончилось, возвратим признак того, что символа нет и #0 в качестве с. Если выражение не закончилось, прочитаем новый символ.
function getC(var f : text; var c : char) : boolean;
begin
getC := true;
if eoln(f) then begin
70
ГЛАВА 2
readln(f); if eoln(f)
then begin c := #0; getC := false end
else read(f, c)
end
else read(f, c) ;
end;
С использованием getC цикл обработки выражения внутри функции procTest выглядит так:
while getC(f, с) do begin
обработка символа с;
end;
Уточним обработку символа. Из примера в условии ясно, что четырех счетчиков, каждый для своего типа скобок, недостаточно. Чтобы отследить, правильно ли закрываются скобки, необходимо помнить, в каком порядке они открывались.
Итак, нам Необходим список открытых и еще не закрытых скобок. Новую открывающую скобку дописываем в конец списка. Когда появляется закрывающая, смотрим, которой была последняя открывающая. Если эти скобки различных типов или список пуст, то выражение неправильно, иначе удаляем последнюю открывающую скобку из списка. По окончании правильного выражения список пуст. Пример изменений в списке приведен в следующей таблице.
Прочитаны символы	Список еще не закрытых скобок
Л Л	Л Л Л V V	V V Л Л V	( ( [ ( (< ( <
Список еще не закрытых скобок представляет собой стек (stack) или магазин — новая открывающая скобка помещается на его верхушку, закрывающая выталкивает с верхушки “свою” открывающую, а “чужая” открывающая на верхушке магазина свидетельствует о том, что выражение неправильно.
Реализуем магазин с помощью массива символов stack (его размер уточним ниже) и целой переменной top с начальным значением 0. Массив представляет элементы списка, top — их количество, а элемент stack [top] — последнюю еще не закрытую скобку.
Добавить открывающую скобку в магазин означает
inc(top); stack[top] := с,
удалить скобку — dec (top). Магазин пуст, если top = 0 (перед циклом обработки выражения нужно присвоить top : = о).
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
71
Если новый символ — закрывающая скобка, а магазин пуст или на его верхушке “чужая” скобка, пропустим остаток текущего выражения с помощью следующей процедуры skipTest:
procedure skipTest;
begin
readln(f); {пропускаем текущую строку и}
while not eoln(f) do readln(f); {остаток выражения} end;
Определим размер магазина stack. Заметим: если длина списка еще не закрытых скобок больше /Головины максимально возможной длины теста (50000), баланса скобок в выражении быть не может, и остаток выражения можно пропустить. Поэтому положим размер массива stack равным 50001 (с запасом в один элемент).
Итак, обработка выражения уточняется следующей функцией:
function procTest(var f : text) : boolean;
const halfMax = 50000;
var c : char; {текущий сцмвол}
stack : array [1.. halfMax+1] of char; {магазин}
top : longint; {номер элемента-верхушки}
begin
top := 0; procTest := true;
while ge£C(f, c) do begin
if c in [1 (', ' [1 , '{', '<']
then begin
inc(top); stack[top] := c;
if top > halfMax then begin
procTest : = false; skipTest(f); break {выход из цикла обработки выражения} end
end
else {на входе закрывающая скобка}
if (top	= 0)	or
(с =	')')	and	(stack[top]	<>	'(')	or
(c =	']')	and	(stack[top]	<>	*[’)	or
(c =	'}')	and	(stack[top]	< > '{') or
(c =	'>')	and	(stack[top]	<>	1 <:1)
then begin
procTest := false; skipTest(f); break
{выход из цикла обработки выражения} end
else dec(top); {"своя" скобка удаляется из магазина}
end;
if с = #0 then procTest ;= (top = 0);
end;
Наконец, приведем программу в сокращенном виде.
program BinLang2;
var f, g : text; {файлы входа и выхода}
подпрограммы newTest, skipTest, getc, procTest (cut. выше) begin
assign(f, 'balance, txt'); reset(f);
72
ГЛАВА 2
assign(g, 'balance, sol'); rewrite(g) ;
while newTest(f) do write(g, ord(procTest(f)));
close(f); close(g);
end.
H Восстановите полный текст программы с подпрограммами.
2.2.5.	Линейный поиск подстроки в тексте
Задача 2.12. Заданы две последовательности символов и нужно определить все вхождения первой из них (назовем ее строка-образец) во вторую (строка-текст).
Вход. Первая строка текста pattsrc. txt является образцом и имеет длину от 1 до 255. Вторая строка является текстом и имеет длину от 1 до 2-109.
Выход. В одну строку текстового файла pattsrc.sol вывести последовательность номеров позиций в тексте, начиная с которых в него входит образец (нумерация начинается с 1). Числа разделить пробелами. Если вхождений нет, выход пуст.
Пример
Вход аа Выход 12 5 Вход аа Выход aaabaa	abababa
Решение задачи. Данная задача — упрощенный вариант задачи поиска подстроки'. задана строка, и нужно найти все ее вхождения в текст. Такая задача (в более сложных вариантах) традиционна в текстовых редакторах или в системах поиска информации.
Из условия ясно, что образец можно запомнить в массиве символов, а текст — нельзя. Однако вначале предположим, что и образец, и текст запомнены в массивах символов put соответственно. Пусть т — длина образца, п — длина текста. Естественно предполагать, что т<п.
Первое, что приходит в голову, — сравнить с образцом каждую из подстрок текста длиной т, начинающихся в позициях 1, 2,..., п-т+1. Однако общее количество сравнений символов будет иметь оценку О(т(п-т+1)). Например, если р=a, t=d, то придется провести как раз m(«-zn+1) сравнений, что при т порядка 103 и п порядка 105 сравнимо с 108. Многовато, тем более что большинство сравнений в действительности лишние. Мы убедимся в этом, познакомившись с другим способом поиска подстроки, имеющим линейную оценку числа сравнений.
Представленный ниже способ называется методом Кнута-Морриса—Пратта (его придумали Дж. Моррис и В. Пратт и независимо от них Д. Кнут). Будем называть его сокращенно КМП. Он подробно описан в книгах [3, 22, 39] и др.
Начнем сравнивать символы образца p=pv..pm с символами текста t=tl...tri с начала. Предположим, что ..., trl=prv tj*Pj, где j<m, т.е. образец не входит в текст с первой позиции. Можно попробовать начать проверку со второй позиции, но совсем не обязательно. Например, если p=ababb и t=ababababbab и после того, как оказалось, что t5=a* b=p5, то начинать следующую проверку с t2 бессмысленно, поскольку t2 не является началом образца (рис. 2.3).
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
73
Проверка с позиции 2: f2*pr
Проверка с позиции 3: ^=рр t4=p2.
ababababbab ababb ababb
Рис. 2.3. Сдвиги образца относительно текста
Заметим: символами Ц4=аЬ одновременно заканчивается и начинается часть образца pj>j>j>4, поэтому следующее вхождение образца возможно только тогда, когда pj>2 займут место PjP4, т.е. образец “сдвинется” относительно текста сразу на две позиции. После этого можно продолжить проверку от символа Г,, т.е. без возвращения назад в тексте t.
Далее будет обнаружено г7*р3, и образец можно сдвинуть на две позиции, чтобы рр2 вновь заняли место pj>4, совпадая при этом ct5t6. Теперь t2-p3, ts=p4, t9=p5, и вхождение начиная с позиции 5 найдено.
Таким образом, пусть проверяется вхождение образца начиная с позиции i-j и при этом рР..р.= а pj+i не совпадает с tt. В этой ситуации нужно найти самое блинное начало рх—рк образца, которое одновременно является концом его подстроки р ...pj. Оно также будет концом текста
Переход от проверенного начала образца длины j к проверенному началу меньшей длины к Означает сдвиг образца относительно текста t сразу на j-k позиций. Но на меньшее количество позиций сдвигать образец нет смысла, поскольку рх...рк — это самое длинное начало образца, совпадающее с концом текста
Если pk+x = tp то можно продолжать сравнения с символа Если рк+х**р нужно отыскать самое длинное начало pv-pkl образца, совпадающее с концом рх...рк (и с концом	сравнить pt;+1 с tp и т.д.
Итак, если для каждой позиции j образца известна наибольшая длина Д/) <7 начата образцаpx...pnv совпадающего с концом подстрокирх...рр то первое вхождение образца находится без возвращений в тексте t. Для определения возможного начала следующего вхождения нужно знать лишь flj) и продолжать поиск без возвращений в тексте*. Именно отсутствие возвращений в тексте позволяет не запоминать его в массиве и дать общему количеству сравнений символов линейную оценку 0(ги+п) — ниже она будет строго доказана.
Функция Ду) называется функцией возвратов. Она показывает, к какому символу нужно возвратиться в образце, когда р^х не совпал с очередным символом tt текста, чтобы продолжить поиск со сравнения г с рм+х. Этот возврат равносилен сдвигу образца на наименьшее возможное количество позиций j-flj).
Займемся вычислением функции возвратов. Очевидно, Д1) = 0. Пусть все значения Д 1),..., flj-1) уже вычислены, и Ду-1)=к. Если ру=р*+1, то конец строкирх...рГ^ совпадает с ее же началом длины к+1, поэтому Ду)=Л+1. Если р^р^, то следующим концом” строки рГ..ргхр} может быть строка Рр-РЛ4)РЛ4)+р поскольку именно рГ-рЛк) является самым длинным концом pv..pk. Если и эта строка не годится, то следующим может быть4р1...рЛЛ4))+|, и т.д. Таким образом, или будет найдено
74
ГЛАВА 2
начало длины г, при котором pv..pr является концом pv..pj, и тогда fij) = г, или не будет найдено, и тогда fij)=0.
Представим образец и функцию возвратов массивами символов patt и целых f и опишем ее вычисление в следующем фрагменте программы: f [1] := 0; к := 0;
for i := 2 to m do begin
while (k > 0) and (patt [i] <> patt[k+1]) do к : = f [к] ;
if patt[i] « patt[k+1] then inc(k);
f[i] := к end; ,
Оценим общее количество сравнений символов, выполняемых по этому алгоритму. Обозначим через w(i) количество выполнений тела цикла while при соответствующем значении i= 2, ..., т. Заметим, что каждое выполнение тела цикла while уменьшает значение к не меньше, чем на 1. Отсюда
ЯП <; л»-1] - но +1, т.е. но <Я/-1] -ЯП +1.
Тогда
Н2) + НЗ) + ... + Нт) *ЯИ -Я2] + 1 +Я2] -ЯЗ] + 1 + ... +Ят-1] ~Ят] + 1 = =ЯЛ _Ят] + т-1<т-1.
При каждом значении i сравнений символов происходит на 2 больше, чем выполнений тела цикла — одно дополнительное при вычислении условия в заголовке цикла и одно в операторе разветвления. Отсюда общее количество сравнений символов не больше 3(т-1), т.е. прямо пропорционально т. Итак, общее количество сравнений символов при построении функции возвратов имеет линейную (относительно длины образца) оценку Q(m).
Реализуем алгоритм решения задачи в виде процедуры (листинг 2.6). Образец представлен в массиве patt типа aChar = array [1. . 255] of char, функция возвратов/—в массиве f типа aPos = array [1..255] of byte. Номера текущих позиций в тексте и образце хранятся в переменных tp и рр; перед началом поиска они равны 0. Очередной символ текста вводится в переменную с, которая сравнивается с patt [рр+1]. Предполагается, что входной и выходной файлы fin и f Out предварительно установлены в соответствующие начальные состояния.
Листинг 2.6. Поиск всех вхождений подстроки в тексте по методу КМП procedure run(var fin, fOut : text; patt : aChar; m : byte; f : aPos) ;
var c : char; очередной символ текста }
рр : byte;	текущая позиция в образце }
tp : longint;	текущая позиция в тексте }
begin
рр := 0; tp := 0;
while true do begin
if eof(fln) then break;
read(fin, c); inc(tp); { вводим очередной символ текста }
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ
75
{*} while (рр > 0) and (patt [рр+1] о с) do
рр := f [рр]; { следующий "суффикс-префикс" }
if patt[рр+1] = с then inc(pp);
if рр = m then begin { найден последний символ образца }
write(fOut, '	tp-m+1);
рр := f[рр])	{ подготовка к поиску следующего вхождения }
end
end;
end;
Анализ решения. Оценим теперь количество сравнений символов при выполнении приведенной процедуры. Заметим: при каждом выполнении тела внешнего цикла tp увеличивается на 1, а рр увеличивается на 1 и, возможно, уменьшается как минимум на 1 за счет присваивания f [рр]. Обозначим через fe(tp) начальное значение рр при очередном значении tp (от 1 до и), а через w(tp) — количество уменьшений рр при одной и той же позиции tp в тексте. Тогда fe(tp) < b(tp-l)-w(tp) +1 при tp> 1, откуда w(tp) b(tp-l)-Z>(tp) +1. Следовательно,
w(l) + w(2) + ... + w(n) < 1 + й[1] - b[2] + 1 + b[2] - />[3] + 1 + ... + fe[n-l] - fc[n] + 1 =
= n + £[1] - b[n] < n+1.
Из реализации алгоритма ясно, что при каждом значении tp символы сравниваются не' более чем дважды1, если не учитывать выполнений цикла {*}, каждое из которых добавляет еще одно сравнение. Но tp увеличивается п-1 раз, а общее количество выполнений цикла {*}, т.е. уменьшений значения рр, не больше п+1, поэтому общее число сравнений не больше 2п-2+n+1, т.е. имеет оценку О(п). Поскольку каждый символ текста сравнивается хотя бы один раз, получаем оценку 0(п).
С учетом построения функции возвратов приходим к выводу: данная реализация поиска всех вхождений образца длиной т в текст длиной п требует 0(т+л) сравнений символов. Суммарная сложность прямо пропорциональна количеству сравнений, поэтому общей оценкой сложности также является 0(т+и).
Замечание. Алгоритм КМП обеспечивает линейную сложность в самом худшем случае, но в среднем он не является эффективным. Существуют другие алгоритмы, которые в худшем случае имеют квадратичную оценку, но в среднем работают быстрее, чем алгоритм КМП (приблизительно в два-три раза). Эти алгоритмы, основанные на других идеях, подробно представлены в [39], а один из них — в [22]. Для однопроходной реализации они не пригодны.
•» Написать полную программу решения задачи.
м Решить задачу при условии, что образцов может быть несколько, например, от одного до 20. Указание. Для каждого образца построить функцию возвратов и отслеживать ее значение на очередном символе текста.
» Решить задачу при условии, что следующее вхождение образца в текст может начинаться только после окончания предыдущего. Например, образец аа входит в текст ааа только один раз, начиная с позиции 1. Указание. После того как найдено вхождение образца, текущую позицию в образце положить равной 0.
1 За счет “ленивого” вычисления and.
76
ГЛАВА 2
Упражнения
2.1.	В последовательности значений типа longint, длина которой не более чем 1О‘°, все значения, кроме одного, встречаются четное число раз, а одно — нечетное число раз. Определить это значение.
2.2.	Подсчитать количество появлений каждой буквы во входном тексте.
2.3.	Из последовательности целых чисел выбрать три числа, произведение которых максимально. Вход. В первой строке текста — количество чисел и, 108, в следующих п строках — целые числа типа integer. Выход. Три числа в произвольном порядке. Если решений несколько, вывести любое из них.
2.4.	Заданы 2п положительных целых чисел, которые можно разбить на пары так, что суммы чисел во всех парах одинаковы. Определить, можно ли их разбить на пары так, что произведения чисел в всех парах одинаковы.
Вход. Первая строка текста содержит количество чисел п (1^ п< 2-10’). В следующих п строках записаны натуральные числа а,, аг,..., а*.
Выход. 1, если искомое разбиение существует, иначе 0.
2.5.	В тексте записана последовательность натуральных чисел типа longint.
а)	Известно, что некоторое число встречается в ней чаще, чем все остальные числа, вместе взятые. Найти это число.
6)	Предположительно, некоторое число встречается в ней чаще, чем все остальные числа, вместе взятые. Установить, так ли это, и если так, найти число.
2.6.	Клиенты приходят к парикмахеру, занимают очередь, если она есть, и стригутся в порядке прихода. Мастер, освободившись, сразу готов к стрижке следующего клиента. Для каждого клиента известна длительность его стрижки г. клкет уходит через t единиц времени после начала стрижки. Вход. В первой строке текста — количество клиентов т (1 <т< 10б). В каждой из следующих т строк записана пара целых положительных чисел — момент прихода клиента и длительность его стрижки. Моменты прихода заданы относительно начального момента времени и образуют неубывающую последовательность. Определить моменты ухода.
2.7.	В положительном целом числе вычеркнуть цифру так, чтобы оставшееся число было как можно больше. Вход и выход. Строка текста с числом длиной не более чем 10s. Например, входу 321 соответствует выход 32,123 — выход 2 3.
2.8.	Текст содержит слова в алфавите а, ..., z, разделенные пробелами и знаками пунктуации в произвольном количестве. Слова со строки на строку не переносятся. Знаки пунктуации — “: ”,	”. Написать программу печати тек-
ста с удалением однолитерных слов, лишних пробелов и знаков пунктуации.
2.9.	Текст содержит символы 0 и 1, последовательность которых считается непрерывной независимо от разбиения текста на строки. Написать программу определения, содержит ли текст образцы из 0 и 1, заданные в отдельных строках длиной не больше 1000. Например, текст, образованный строками 00 и 11, содержит образцы 001и011ине содержит образцов 10 и 111.
Вход. Первая строка текста patterns. txt содержит натуральное п — количество образцов (от 1 до 20). Следующие п строк длиной не больше 1000 со
ОДНОПРОХОДНЫЕ АЛГОРИТМЫ	77
держат образцы. Строки текста text. txt в количестве не больше 210’ имеют длину не больше 1000 и представляют последовательность 0 и 1, в которой нужно найти вхождения образцов.
Выход. Последовательности строк, соответствующих образцам. Если образец не входит в текст, в строке записан символ 0, иначе символ 1 и два целых числа — номер строки в тексте и номер позиции в ней, начиная с которой образец входит в текст. Например, для указанных выше строк текста и образцов в первой строке будет 1 1, во второй — 1 2, в третьей и четвертой — 0.
Глава 3
Рекурсия
В этой главе...
*	Простые примеры рекурсивных определений и рекурсивных подпрограмм
•	“Подводные камни” рекурсии — глубина рекурсии и количество вложенных рекурсивных вызовов — и их влияние на размер занятой части программного стека и длительность вычислений
•	Косвенная рекурсия и ее реализация
♦	Быстрый алгоритм возведения в степень и его нерекурсивная (итеративная) реализация
•	Построение самоподобных ломаных с использованием рекурсии
3.1.	Основные понятия
3.1.1.	Рекурсивные определения
Рекурсивное определение задает элементы некоторого множества с помощью других элементов этого же множества. Использование таких определений называютрекурсией.
Например, значения функции “факториал” задаются выражением л!=пх(п-1)!, причем 0!=1. Тогда 1!= 1x0!=1, 2=2х1!=2, 3!=3х2!=6 и т.д. Все значения, кроме первого, определяются рекурсивно.
•	В рекурсивном определении не должно быть “заколдованного круга”, когда в определении объекта используется он сам или другие объекты, заданные с его помощью.
Например, определение функции “факториал” изменим так: п!=пх(п-1)! при л>0, 0! = 1!. “Заколдованный круг”: значение функции от 1 выражается через ее же значение от 0, которое, в свою очередь, — через значение от 1. По этому “определению” невозможно узнать, чему равно 1!.
Избежать подобных “заколдованных кругов” можно, обеспечив следующие условия:
•	множество определяемых объектов является частично упорядоченным;
•	каждая последовательность элементов, убывающая по этому упорядочению, заканчивается некоторым минимальным элементом;
•	минимальные элементы определяются нерекурсивно;
80
ГЛАВА 3
•	неминимальные элементы определяются с помощью элементов, которые меньше их по этому упорядочению.
Для тех, кому не знакомы термины частично упорядоченное множество и минимальный элемент, дадим небольшое неформальное пояснение.
Предположим, что элементы некоторого множества (не обязательно все) можно сравнивать, т.е. устанавливать, что а<Ъ (“а не больше Ь”). Если при этом а и Ь различны, то говорят “а меньше Ь”. Элементы могут быть и несравнимыми — когда не выполняется ни а<,Ь, ни Ь<,а. Например, если в качестве “не больше” взять отношение “делит” между натуральными числами, то 1 меньше любого другого натурального числа, а 2 и 3 между собой несравнимы.
Отношение между элементами множества называется отношением частичного порядка, если обладает следующими свойствами.
1.	Для каждого а верно а<,а (элементы не больше самих себя).
2.	Не существует разных а и Ь, для которых одновременно а Ъ и b <а.
3.	Для любых элементов а, Ь, с, если верно а£Ь и Ь<.с, то верно и а£с. Различных таких элементов в множестве может и не быть — лишь бы не было того, что а меньше b, Ь меньше с и при этом неверно, что а меньше с.
Первое свойство называется рефлексивностью, второе— антисимметричностью, третье — транзитивностью.
Множество элементов, отношение между которыми является частичным порядком, называется частично упорядоченным. Его элемент а называется минимальным, если в множестве нет элементов меньше а.
Рассмотрим примеры. Множество натуральных чисел частично упорядочено по отношению “делит”. У него есть один минимальный элемент — число 1, но если его исключить, то минимальными элементами будут все простые числа. Значения функции л! упорядочены по возрастанию значений аргумента, и минимальным является 0!. Заметим, что 0! определено без рекурсии, а остальные значения — с помощью меньших значений, т.е. значений от меньших аргументов.
3.1.2.	Простейший пример рекурсивной подпрограммы
Вызов подпрограммы, записанный в ее же теле, называется рекурсивным, как и подпрограмма с такими вызовами.
Рассмотрим, например, функцию для вычисления л!.
function f(n : integer) : integer; (* n >=0 *)
begin	*
if n « 0 then f := 1
else f :» n*f(n-1) end;
Имитируя вызовы рекурсивных подпрограмм, их локальные переменные будем обозначать так. Если подпрограмма 5 вызвана из программы, ее локальную переменную X обозначим S.X. При выполнении каждого рекурсивного вызова подпро
РЕКУРСИЯ
81
граммы S появляется новая переменная X. Для ее обозначения префикс “S.” дописывается к имени X в предыдущем вызове: S.S.X, S.S.S.X и т.д.
Имитацию вызова f (2) функции f представим в табл. 3.1.
Таблица 3.1. Имитация вызова f (2)
1	,	-	-	.	.	« Что выполняется	Состояние памяти	|					
Вызов f (2)	f.n	f.f				
	2	?				
Вычисление n<0: false	2	?				
Начало f := n‘f(1)	2	?				
Вызов f(1)	2	?	f.f.n	f.t.f		
	2	?	1	?		
Вычисление n=0: false	2	?	1	?		
Начало f := n‘f(O)	2	?	1	?		
Вызов f(0)	2 '	?	1	?	f.f.f.n	f.t.f.f
	2	?	1	?	0	?
Вычисление n=0: true	2	?	1	?	0	?
f:=1	2	?	1	?	0	1
Возвращение из вызова f(0)	2	?	1	?		
Окончание f :=n‘f(O)	2	?	1	1		
Возвращение из вызова f(1)	2	?				
Окончание f := n‘f(1)	2	2				
Возвращение из вызова f(2)						
Как видно из таблицы, с каждым рекурсивным вызовом растет программный стек, уменьшаясь по окончании вызова.
* Используя рекурсию, мы перекладываем проблемы работы с программным стеком на компилятор. Именно он добавляет в генерируемую программу команды обработки стека и позволяет нам считать, что стек обрабатывается автоматически.
3.1.3.	Глубина рекурсии и общее количество рекурсивных вызовов
С рекурсивными подпрограммами связаны два важных понятия — глубина рекурсии и общее количество вызовов, порожденных вызовом рекурсивной подпрограммы.
Глубина рекурсии вызова подпрограммы — это максимальное количество незаконченных рекурсивных вызовов при выполнении данного вызова.
Например, вызов функции вычисления л! с аргументом 3 заканчивается лишь после окончания вызова с аргументом 2, а тот после вызова с аргументом 1. Такие вызовы называются вложенными. Очевидно, что вызов с аргументом п порождает
82
ГЛАВА 3
еще п вызовов, поэтому максимальное число незаконченных вызовов при вычислении и!, т.е. глубина рекурсии, достигает п+1.
Когда выполняется вызов с глубиной рекурсии т, при выполнении самого глубокого вызова одновременно “существуют” т экземпляров локальной памяти. Каждый экземпляр имеет определенный размер, и, если глубина будет слишком большой, то автоматической памяти, предоставленной процессу выполнения программы, может не хватить.
♦	Напомним: размер автоматической памяти- можно устанавливать в настройках среды или с помощью директивы компилятору {$М ...}. Обратите внимание: синтаксис именно этой директивы в Turbo Pascal, Free Pascal и Delphi различен.
♦	Отслеживая пошаговое выполнение рекурсивной подпрограммы в Turbo-среде, обращайте внимание не только на позицию в исходном тексте, но и на содержимое программного стека, используя команду Call stack (клавиши <Ctrl+F3>). Учтите, что по этой команде среда отображает не все, что реально находится в программном стеке.
♦	Выполняя команду step Over (клавиша <F8>) для рекурсивных подпрограмм, Turbo-среда отображает состояние программы после завершения вызова рекурсивной подпрограммы (как и должно быть). Однако возможно, это не тот вызов, который начал выполняться при нажатии <F8>, а другой, вложенный по отношению к нему, т.е. отображено не то, что ожидалось.
Общее количество вложенных вызовов, порожденных данным вызовом рекурсивной подпрограммы, — это количество вызовов, выполненных между началом и завершением данного вызова.
Пример. Биномиальные коэффициенты определяются рекурсивно: С£ =1, если 0<т< 1, или п=0, или п=т; иначе = C^+C^-i . Рассмотрим рекурсивную функцию вычисления биномиального коэффициента С" по т и и, где 0< п<т.
function C(m, n : integer) : longint;
begin
if (m <= 1) or (n = 0) or (n = m) then C := 1
else C := C(m-1, n-l)+C(m-l, n) end;
Каждый вызов, в котором значения аргументов /п>1, 0< п<т, порождает два вложенных вызова. В результате одни и те же величины вычисляются повторно. Например, выполнение вызова с аргументами (5,2) приводит к тому, что вызов с аргументами (3,1) выполняется дважды, вызовы с аргументами (2,1), (1,0) и (1,1) — трижды, а общее число вложенных вызовов достигает 18.
Чем больше т и чем ближе и к т!2, тем больше вложенных вызовов. Не определяя точно зависимость их количества от аргументов, скажем лишь, что при п= mdiv2 или л= mdiv2+1 оно больше 2п/2 (при т=60 это 230, или приблизительно 10’). Если предположить, что за секунду можно выполнить 10б вызовов, то для вычисления нужно больше 1000 секунд, т.е. более четверти часа. Однако нетрудно написать рекурсивную функцию на основе определения С" = хш/и. Ее вызов с аргументом т порождает не больше т/2 вложенных вызовов.
РЕКУРСИЯ
83
Функция С приведена выше только как пример бездумного использования рекурсии. Причина лавинообразного возрастания количества вложенных вызовов — два вызова в теле функции, совсем не обязательные в этой задаче.
Вычислять биномиальные коэффициенты и элементы других рекуррентных последовательностей лучше с помощью циклов, а не рекурсии. Выполнение каждого вызова подпрограммы требует дополнительных действий по подстановке аргументов, поэтому “циклический” вариант, как правило, выполняется быстрее, чем соответствующий ему рекурсивный. 
•	Глубина рекурсии влияет на размер выделяемой автоматической памяти, а общее количество вложенных вызовов — на длительность выполнения. При большой глубине рекурсии существует опасность исчерпания автоматической памяти и аварийного завершения программы.
•	Рекурсия “прячет” в себе огромные объемы вычислений гораздо легче, чем их “прячут” вложенные циклы. Используя рекурсивные подпрограммы, умейте оценить возможную глубину рекурсии и общее количество вызовов.
•	Не спешите писать рекурсивную Подпрограмму непосредственно по рекурсивному определению — простая и короткая подпрограмма может выполняться очень долго. Не применяйте рекурсию для вычисления элементов рекуррентных последовательностей, особенно если порядок соотношения больше 1.
•	Тем не менее разумное применение рекурсии позволяет легко и быстро создавать ясные и естественные программы.
3.1.4. Косвенная рекурсия
Задача3.1. Строка состоит из клеток, пронумерованных от 1 дол. Состояние клетки можно изменить — если она пуста, поставить в нее шашку (занять ее), иначе убрать из нее шашку (освободить ее). Вначале строка пуста. Нужно занять все клетки, соблюдая следующее правило. Изменение клетки допустимо, если она имеет номер 1 или расположена непосредственно после занятой клетки, имеющей минимальный номер среди занятых клеток.
Вход. Целое л, 1£л<15.
Выход. Последовательность элементов вида +i или -г, обозначающих соответственно занять клетку i и освободить клетку i.
Пример. Прил=3 выход имеет вид+1+2-1+3+1.
Анализ задачи. При л = 1 все ясно: нужно вывести +1; при л = 2 — вывести +1+2.
Пусть л >3. Чтобы заполнить л клеток, необходимо в какой-то момент занять л-ю клетку. Правила позволяют это сделать, если (л-1)-я клетка занята, а все клетки с номерами от 1 до л-2 свободны. Значит, нужно сделать так, чтобы заполненной оказалась только (л- 1)-я клетка, потом поставить шашку в клетку л, а затем заполнить клетки с номерами от 1 до (л-2). Итак, появилась подзадача “заполнить только клетку л-1” и рекурсивная подзадача “заполнить л-2 клетки”.
Обозначим задачи “заполнить л клеток” и “заполнить только клетку л” как fill(n) HfillOnly(n) соответственно. Тогда fill(n) при л S3 означает следующее.
fillOnly(n-1);
write('+', n);
fill(n-2)
84
ГЛАВА 3
Займемся задачей fillOnly (п). Если л = 1, нужно вывести+1. Пусть п >2. Как и в задаче fill(n), нужно сначала решить задачу fillOnly(n-l) и занять клетку п. Однако далее нужно освободить (п-1)-ю клетку. Обозначим эту новую подзадачу free(n-l). Таким образом, fillOnly(и) при п>2 означает следующее.
fillOnly(п-1);
write('+', n);
free (д-1)
Рассмотрим подзадачу free(ri). При п= 1 нужно вывести-1, а при п>2, чтобы освободить клетку п, вновь нужно вначале решить задачу fill Only (п-1), но затем не занять, а освободить клетку п, после чего рекурсивно освободить (п-1)-ю клетку. Итак, free(n) при п>2 означает следующее.
fillOnly(п-1);
write(1 - 1, п);
free(п-1)
Решение задачи. Три рекурсивные процедуры fill, fillOnly и free уже почти написаны. Однако они отличаются от предыдущих рекурсивных подпрограмм тем, что вторая из них вызывает третью, а третья — вторую.
Подпрограммы, содержащие вызовы друг друга, называются взаимно рекурсивными. Такую рекурсию еще называют косвенной, в отличие от прямой, когда подпрограмма содержит вызовы самой себя.
В каком порядке разместить взаимно рекурсивные процедуры fillOnly и free! Если одну из них записать первой, то в ней будет вызываться записанная второй, а это противоречит тому, что имя должно использоваться после его объявления. Разрешить это затруднение можно с помощью директивы forward1.
•
* Заголовок одной (или нескольких) из подпрограмм, содержащих вызовы друг друга, разместим первым, но вместо блока с объявлениями и телом напишем слово forward (это указание компилятору, что блок “впереди”, т.е. ниже по тексту). Затем разместим под- программы так, чтобы они содержали вызовы подпрограмм, заголовки которых записаны выше (возможно, со словом forward).
Итак, вначале запишем заголовок процедуры free, а затем все процедуры с блоками (листинг 3.1).
Листинг 3.1. Взаимно рекурсивные подпрограммы___________________________
program Cells;
var n : byte;
procedure free(n : byte); forward; { заголовок без блока }
procedure fillOnly(n : byte);
begin
1 Два других способа — объявить имя подпрограммы, т.е. записать ее заголовок, в интерфейсном разделе модуля или в классе.
РЕКУРСИЯ
85
if n = 1 then write('+1')
else begin
fillOnly(n-1);
write('+1t n);
free(n-1)
end
end;
procedure free(n : byte);
begin
if n = 1 then write('-1')
else begin
fillOnly(n-1);
write ('-', n);
free(n-1)
end
end;
procedure fill(n : byte);
begin
lfn*=l then write ('+1') else if n = 2 then write(1+1+21) else begin
fillOnly(n-1);
writef1+1, n);
fill(n-2)
end end;
begin
readln(n); fill(n);
end.
3.2.	Быстрое возведение в степень
Задача 3.2. Даны a, N. Вычислить а". Числа а и а1 могут быть представлены в I типе extended, N — неотрицательное целое типа longint. Использовать ло- I гарифм и экспоненту запрещено.	|
Анализ задачи. Логарифмы и экспоненты упомянуты, поскольку /= е”*", но пользоваться этой формулой как раз нельзя. Почему? Главным образом потому, что в степень приходится возводить не только числа, но и другие объекты, например, квадратные матрицы. Или целые числа, но не в обычной арифметику, а в кольце по одулюр, да еще и очень большие, например 1024-битовые (это необходимо в современных методах шифрования). В этих ситуациях пытаться логарифмировать возводить число е в степень неуместно.
Смысл нашей задачи — реализовать возведение чисел в степень так, чтобы, заменив операцию умножения чисел подпрограммой умножения соответствующих объектов, можно было получить реализацию их возведения в степень. Вдобавок, по возможности, эффективную. Для начала рассмотрим очевидный вариант вычисления а .
es := 1.0;
for i := 1 to N do res := res*a
86
ГЛАВА 3
Однако возведите таким образом 1,00000012000000000 = 7,2259-1086 (одна целая одна десятимиллионная в степени два миллиарда). Например, на AMD486-DX4-100 это считалось почти час2. И это — числа с плавающей точкой, где умножение отвечает одной команде математического сопроцессора. А если каждое умножение требует выполнения сложной подпрограммы? Кроме того, в современных алгоритмах шифрования показатель степени может быть не типа longint, т.е. Не больше 2-10’, а, например, целый 1024-битовый, т.е. порядка Ю310 (причем не для взлома шифра, а для законного шифрования-дешифрования, которое обязано быть быстрым).
Итак, нужен способ возведения в степень N с помощью умножений, но умножений должно быть существенно меньше N.
Основная идея. Пусть есть переменная а со значением а и переменная b со значением а1000. Сколько нужно умножений, чтобы получить значение а20”? И как получить значение а2001? Ответ очевиден.
а2000 = (а1000)-(а1(ХЮ) = b*b.
о2001 = (^ооо^кюо) а = ь*ь*а.
Итак, за одно-два умножения можно увеличить показатель вдвое. Количество умножений M(N), необходимое для возведения в степень N, оказывается в пределах от Llog/zJ до 2|_log2ArJ. Чтобы возвести а в степень 2-10’, достаточно 43 умножения...
Решение задачи. Алгоритм, в сущности, записан в следующих формулах:
a2N=(a2)N;a^ = (a2)N-a-,a°=l.	(3.1)
Осталось реализовать это в виде программы. Приведем две реализации. Первая — непосредственная запись этих формул в виде рекурсивной функции.
Листинг 3.2. Рекурсивная реализация деления пополам
function power(а : extended; N : longint) : extended; begin
if N = 0 then power := 1.0
else if odd(N)
then power := a*power(a*a, N div 2)
else power := power(a*a, N div 2) end;
Чтобы получить результат, нужно сделать вызов res : =power (a, N). Промежуточные степени а хранятся в локальной памяти вызовов функции, точнее в переменных, которые ставятся в соответствие имени power.
Итак, в каждом следующем вложенном вызове значение аргумента п меньше предыдущего значения, как минимум, вдвое. При п< 1 происходит возвращение из вызова, поэтому указанных уменьшений значения аргумента п не больше, чем log/i, и глубина рекурсии вызова с аргументом п тоже не превышает log2n. При каждом выполнении вызова происходит не больше одного деления, возведения в квадрат и умножения, поэтому общее число арифметических операций не больше 3xlog2n.
2 Такие машины — прошлый век, но если читатель отсюда делает только этот вывод, просим его дальше не читать и сразу подарить кому-нибудь эту книжку.
РЕКУРСИЯ
87
Заметим, что при некоторых л приведенный алгоритм не дает наименьшего количества умножений, необходимых для вычисления n-й степени. Например, при п=15 по алгоритму выполняется 6 умножений, хотя можно с помощью трех умножений вычислить а и затем умножить его на себя дважды (всего 5 умножений). Впрочем, эта неоптимальность не бывает действительно большой, так что авторы не склонны улучшать этот простой и красивый алгоритм.
Вторая реализация формул (3.1) — итеративная, т.е. циклическая (листинг 3.3).
Листинг 3.3. Итеративная реализация деления пополам_____________________
function pdwer(a : extended; N : longint) : extended;
var res : extended;
begin
if odd(N) then res := a else res := 1.0;
while N > 1 do begin
N := N div 2;
a := a*a;
if odd(N) then res := res*a;
end;
power := res end;
Основное преимущество рекурсивного варианта состоит в простоте перехода от формул к программе на реальном языке программирования, а для более эффективной работы лучше итерация.
Анализ решения. Итеративный алгоритм легче понять, если при его имитации записать все показатели в двоичной системе. Пример имитации при N=50 представлен в следующей таблице. В значениях переменной а показатель степени возрастает согласованно с уменьшением N; в моменты, когда N нечетно, res домножается на нужное значение, которое в этот момент хранится в а.
N	a	res
110010	a	1.0
11001	10 a	10 a
1100	too a	10 a
110	1000 a	10 a
11	10000 a	10010 a
1	100000 a	110010 a
Формулы (3.1) часто записывают в виде aw=(a'Vdi’2)2-a"nKKl2J а=1 и называют индийским алгоритмом возведения в степень (этот алгоритм был известен еще древним индусам). Алгоритм, выражающий умножение через сложение так же, как индийский алгоритм выражает степень через умножение, в англоязычной литературе часто называют русским народным алгоритмом умножения (Russian peasant's algorit! m).
Замечание. В листинге 3.2 рекурсивные вызовы можно записать как sqr (power (а, Ndiv2)) Ha*sqr (power (a, Ndiv2)) — это и правильно, и эффективно. Однако если выражение power (a*a, Ndiv2) заменить выражением power(a, N div 2)*power(a, N div 2),
88
ГЛАВА 3
то функция будет работать очень медленно. Причина кроется в том, что с увеличением глубины вложенности рекурсии количество вызовов функции power растет в геометрической прогрессии (см. подраздел 3.1.3). В частности, вызов power (а, 0) выполняется приблизительно 210вЛГ=Араз.
Н Задание для любителей C/C++. Выше указано, что рекурсивные вызовы в листинге 3.2 можно, не теряя эффективности, записать как sqr(power(a, Ndiv2)) и a*sqr(power(а, N div 2)). А что будет, если написать аналогичные вызовы на языке C/C++, при этом реализовав sqr как:
а)	функцию double sqr (double х) {return х*х,-};
б)	макроопределение #def ine sqr (х) ((х) * (х) ) ?
Указание. Функция (п. а) особых последствий не повлечет, но макроопределение (п. б) приведет к ужасной потере эффективности. Если вы еще не поняли, почему, перечитайте последнее замечание и (в своей любимой книге по C/C++) сведения о том, как работают макроопределения.
3.3.	Рисование самоподобных ломаных
Самоподобные ломаные состоят из нескольких частей, каждая из которых подобна ломаной в целом. Построение таких ломаных естественно описывается с помощью рекурсивных подпрограмм.
Для вывода изображений будем использовать (в разных программах) “обычную” графику модуля graph и так называемую черепашью графику. В простейшем варианте черепашьей графики можно рисовать только отрезки на плоскости, получаемые при движениях “черепашки”. Движения задаются командами вида “вперед”, “назад”, “повернуть на такой-то угол” и тай далее. Команды черепашьей графики действуют относительно текущего положения (включающего координаты и направление). Это существенно отличается от большинства графических библиотек (в том числе и модуля graph), в которых команды обычно привязаны к абсолютной системе координат и имеют вид “нарисовать отрезок от точки (хр У]) до точки (х2, у2)”.
В приведенных ниже программах используем следующие Процедуры черепашьей графики:
ForWd (d) — переместиться на d единиц вперед;
Back (d) — переместиться на d единиц назад;
TurnLef t (а) — повернуть на а градусов влево (против часовой стрелки).
TurnRight (а) — повернуть на а градусов вправо (по часовой стрелке).
PenUp — поднять перо (чтобы черепашка при движении не оставляла следа).
PenDown — опустить перо (чтобы черепашка при движении оставляла след).
Используем также SetPosition и SetHeading — установки начальных координат и начального направления черепашки.
В системе Borland Pascal в модуле Graphs реализована готовая черепашья графика, хотя и не очень красивая. Во-первых, она требует, чтобы расстояния и углы были
РЕКУРСИЯ
89
целыми, тогда как для многих задач весьма желательны дробные значения3. Во-вторых, она использует графический режим 320x200 — мало того, что разрешение очень низкое, так еще и пиксели не квадратные. Поэтому разумно поискать какой-нибудь другой “движок” черепашьей графики или написать свой. Можно использовать какую-нибудь реализацию языка Лого (“родного” языка черепашьей графики) — но тогда придется изучить его, а он весьма отличается от языка Паскаль.
Отметим также, что в большинстве реализаций черепашьей графики команда “вперед” называется forward или fd, “назад” — back или bk, “влево” — left или It, “вправо” — right или rt, а приведенные выше имена используются именно в модуле Graph3.
3.3.1.	Снежинка Коха
Ломаные “снежинки Коха” порядка 0,1, 2 и 3 изображены на рис. 3.1. Формально их определяют двумя способами.
1.	Снежинка Коха порядка 0 — это отрезок, а снежинка порядка п (п — целое положительное) получается из снежинки порядка (п-1) заменой каждого ее отрезка ломаной по правилу: отрезок делится на три равные части, на средней строится равносторонний треугольник и его основание удаляется.
2.	Снежинка Коха порядка 0 — это отрезок, а снежинка порядка п (п — целое положительное) состоит из четырех снежинок порядка п-1 втрое меньших линейных размеров, причем вторая повернута относительно первой на 60° влево, третья относительно второй — на 120° вправо, а четвертая относительно третьей — на 60° влево, т.е. ориентирована так же, как первая.
л = 0	л=1	л = 2	л = 3
Рис. 3.1. Снежинки Коха порядков 0, 1,2иЗ
Задача 3.3. Написать программу, рисующую снежинку Коха порядка п.
Анализ и решение задачи. Возможно, определение 1 удобнее для восприятия или ручного построения. Но для написания программы безусловно лучше определение 2, поскольку его структура уже отражает организацию программы.
Из определения 2 понятен список аргументов рекурсии: порядок ломаной и расстояние от начала до конца ломаной.
Согласно определению, снежинка Коха порядка 0 рисуется непосредственно, а порядка и (и>1)— через ломаные предыдущего порядка. Поэтому рекурсивная подпрограмма должна начинаться с проверки п= 0. А что делать в случаях п=0 и п * 0, написано в определении 2. Получаем программу, приведенную в листинге 3.4.
3 Если единицей длины является пиксель и нарисовать перемещение на дробную длину нельзя, то дробные перемещения можно по крайней мере помнить, округляя только при выводе на экран. Например, при выполнении многих подряд шагов длины 0,2 гораздо лучше, когда каждое пятое перемещение черепашки сдвигает ее изображение на один пиксель, чем когда черепашка вообще перестает двигаться.
90
ГЛАВА 3
Листинг 3.4. Программа построения снежинки Коха л-го порядка uses graph3, crt;
procedure Koch(d : integer; n : integer);
begin
if n=0 then
ForWd(d)
else begin
Koch(d div 3,n-l);
TurnLeft(60);
Koch(d div 3,n-l);
TurnRight(120);
Koch(d div 3,n-l);
TurnLeft(60);
Koch(d div 3,n-l);
end;
end;
var n : integer;
begin
readln(n);
graphcolormode;
SetHeading(90);
SetPosition(175, 0);
Koch(243, n);
readkey;
textmode(co80);
end.
♦ Упомянутые выше печальные особенности черепашьей графики модуля Graphs не позволяют запускать эту программу при л > 5. Но с более адекватным “движком" черепашьей графики и большим разрешением экрана она работает при значительно больших л.
3.3.2.	Треугольник Серпиньского
Как известно, любой треугольник разбивается средними линиями на четыре треугольника, подобных начальному.
Треугольник Серпиньского порядка 0 — это равносторонний треугольник; порядка 1 — равносторонний треугольник, в котором проведены средние линии; для целого положительного п треугольник Серпиньского (и+1)-го порядка получается дроблением трех “не центральных” из каждых четырех треугольников, полученных при построении л-го порядка треугольника Серпиньского (рис. 3.2).
Рис. 3.2. Треугольники Серпиньского порядков отОдоЗ
РЕКУРСИЯ
91
Задача 3.4. Написать программу, рисующую треугольник Серпиньского л-го Порядка.
Анализ и решение задачи. Вновь переформулируем правило построения самоподобной ломаной порядка л, чтобы в нем использовались только ломаные порядка л-1.
Это несложно. Треугольник Серпиньского порядка 0 — это равносторонний треугольник со стороной d; треугольник Серпиньского порядка л (л> 1) — это объединение трех треугольников Серпиньского (л-1)-го порядка с длиной стороны d/2; левые нижние углы треугольников Серпиньского (л-1)-го порядка размещены в левом нижнем углу, в середине нижней стороны и в середине левой стороны треугольника Серпиньского л-го порядка.
Итак, количество случаев уменьшено с трех (0,1 и л > 2) до двух (0 и л > 1). Это позволяет существенно сократить рекурсивную процедуру.
Первый способ. Заметим, что три меньших треугольника Серпиньского образуют весь треугольник текущего порядка, включая его внешние стороны. Рекурсивная процедура при л> 1 должна выполнять следующие действия (начинаем в левой нижней вершине, смотрим вправо).
1.	Нарисовать треугольник Серпиньского (л-1)-го порядка размера d/2.
2.	Переместиться на d/2 вперед, попав таким образом на середину нижней стороны.
3.	Нарисовать треугольник Серпиньского (л-1)-го порядка размера d/2.
4.	Выполнить повороты и движения, чтобы попасть на середину левой стороны (смотря вправо).
5.	Нарисовать треугольник Серпиньского (л-1 )-го порядка размера d/2.
Однако этого недостаточно. После прорисовки третьего треугольника Серпиньского (л—1)-го порядка необходимо еще вернуться в начальное положение '“левый нижний угол треугольника л-го порядка, смотрим вправо”. Естественно, нужно обеспечить, чтобы при л=0 после прорисовки обычного треугольника черепашка также возвращалась в начальное положение. Итак, получим подпрограмму в листинге 3.5.
Листинг 3.5. Построение треугольника Серпиньского с помощью черепашьей графики и одинаково ориентированных треугольников порядка л-1
procedure Sierp_Tr(d : integer; n : integer);
begin
if n=0 then begin (* простой треугольник *)
ForWd(d); TurnLeft(120); (* трижды рисуем *)
ForWd(d); TurnLeft(120); (* сторону и *)
ForWd(d); TurnLeft(120); (* поворачиваем, *)
(* возвратившись в исходное положение *)
end else begin
Sierp_Tr(d div 2, n-1); (* левый нижний треугольник *)
ForWd(d div 2); (* сдвиг в середину основания *)
Sierp_Tr(d div 2, n-1); (* правый нижний треугольник *)
TurnLeft(120); (* поворот и *)
ForWd(d div 2); (* сдвиг в середину левой стороны *)
92
ГЛАВА 3
TurnRight(120); (* подготовительный поворот *)
Sierp_Tr(d div 2, n-1); (* верхний треугольник *)
TurnRight(120); (* подготовка к сдвигу *)
ForWd(d div 2) ; (* сдвиг в нижний левый угол *)
TurnLeft(120); (* и "смотрим вправо" *)
end;
end;
Второй способ. В предыдущем алгоритме треугольники порядка л-1 были одинаково ориентированы. Мысленно повернем второй (правый нижний) треугольник порядка л-1 относительно первого влево на 120°. Тогда его левый нижний угол окажется в правом нижнем углу треугольника л-го порядка.
Итак, чтобы нарисовать треугольник Серпиньского порядка л (л > 1), достаточно трижды выполнить такие действия.
1.	Нарисовать треугольник Серпиньского (л-1)-го порядка с длиной стороны J72.
2.	Переместиться вперед на d.
3.	Повернуть на 120°.
Реализуем их в подпрограмме (листинг 3.6), которая оказывается короче и проще предыдущей (см. листинг 3.5).
Листинг 3.6. Построение треугольника Серпиньского с помощью черепашьей графики и по-разному ориентированных треугольников порядка л-1
procedure Sierp_Tr(d : integer; n : integer);
begin
if n=0 then begin (* простой треугольник *)
ForWd(d); TurnLeft(120); (* трижды рисуем *)
ForWd(d); TurnLeft(120); (* сторону и *)
ForWd(d); TurnLeft(120); (* поворачиваем, *) (* возвратившись в исходное положение *) end else begin
Sierp_Tr(d div 2,n-l); (* левый нижний треугольник *)
ForWd(d);	(* сдвиг в правый нижний угол *)
TurnLeft(120); (* и поворот *)
Sierp_Tr(d div 2,n-1); (* правый нижний треугольник *)
ForWd(d);	(* сдвиг в верхний угол *)
TurnLeft(120); (* и поворот *)
Sierp_Tr(d div 2,n-1); (* верхний треугольник *>
ForWd(d);	(* возвращение в *)
TurnLeft(120); (* начальное положение *)
end;
end;
Третий способ. Изобразим треугольник Серпиньского с помощью “обычной” (не черепашьей) графики. Более удобным оказывается первый способ, где все три треугольника Серпиньского (л-1)-го порядка одинаково ориентированы.
Рекурсивная процедура, работающая с “обычной” графикой, содержит гораздо меньше “связующего материала”. Чтобы нарисовать треугольник Серпиньского по
РЕКУРСИЯ
93
рядка п (п > 1) с левой нижней вершиной в точке (х, у), Достаточно выполнить три рекурсивных вызова, каждый из которых рисует треугольник Серпиньского (n-l)-ro порядка с длиной стороны d!2. Левые нижние вершины этих треугольников должны находиться в точках (х,у), (x+d/2, у), (x+d/4, y+d7з /4).4
Ясно, что у процедуры, кроме параметров а и п, должны быть дополнительные параметры х и у — ведь их значения для каждого вызова свои. Процедура приведена в листинге 3.7.
Листинг 3.7. Программа построения треугольника Серпиньского с помощью “обычной” графики__________________________________________________
uses graph, crt;
procedure Sierp_Tr(x, у : extended; d : extended; n : integer); begin
if n=0 then begin (* простой треугольник *) line(round(x), round(y), round(x+d/2), round(y-d*sqrt(3)/2)) ;
line(round(x+d/2), round(y-d*sqrt(3)/2), round(x+d), round(y));
line(round(x), round(y), round(x+d), round(y));
end else begin
Sierp_Tr(x, y, d/2, n-1);
Sierp_Tr(x+d/2, y, d/2, n-1);
Sierp_Tr(x+d/4, y-d*sqrt(3)/4, d/2, n-1);
end; end;
var n : integer;
gd, gm : integer;
begin
readln(n);
gd := detect;
InitGraph(Gd, Gm, 1c:\lang\bp\bgi');
if GraphResult <> grOk then exit;
Sierp_Tr(70.0, 475.0, 500.0, n);
readkey;
closegraph; end.
3.3.3.	Драконова ломаная
Драконова ломаная нулевого порядка — это отрезок. Драконова ломаная порядка л (п> 1) получается из драконовой ломаной порядка п-1 следующим образом: линия “расщепляется” на две, и одна ее часть остается на месте, а вторая поворачивается относительно хвоста ломаной (n-l)-ro порядка на 90° против часовой стрелки (рис. 3.3).
4 В программе пишем y-d*sqrt (3) / 4, поскольку в экранной системе координат ось Оу направлена сверху вниз.
94
ГЛАВА 3
Рис. 3.3. Построение драконовой ломаной порядка п по ломаной порядка п-1
Драконовы ломаные порядков 0,1,2,3,4 изображены на рис. 3.4.
Задача 3.5. Напишите программу построения драконовой ломаной.
Анализ задачи. По определению, ломаная порядка 0 — это просто вертикальный единичный отрезок (нарисованный снизу вверх), а ломаная порядка и состоит из двух ломаных (n-l)-ro порядка. Можно сделать вывод, будто для построения ломаной порядка п (п > 1) нужно выполнить последовательность действий dragon(n-1); TurnRight(90); dragon(п-1).
Однако такой наивный подход не приводит к успеху (попытайтесь сначала сами понять, что получится в результате такой рекурсии, а затем читайте дальше).
Ломаные порядка 0 и 1 строятся правильно, но с порядка 2 начинается не то, что нужно. А именно, при п>2 получаем обведенный 2"~2 раз единичный квадрат. Впрочем, этого следовало ожидать: в описанном наивном подходе есть только правые повороты, а в правильных ломаных порядка п > 2 — и правые, и левые.
Чтобы выяснить, когда поворачивать направо и когда налево, присмотримся к рис. 3.3. Прорисовка ломаной n-го порядка происходит в порядке от А кА'через В, т.е. состоит из двух этапов: от А к В и от В кА'. Введем очевидные термины голова и хвост ломаной; направление от головы к хвосту будем называть прямым, от хвоста к голове — обратным.
Итак, уточним описанный раньше наивный подход: для построения (в прямом порядке) ломаной порядка п (п>1) нужно нарисовать ломаную дорядка п-1 от головы к хвосту, повернуть направо и нарисовать обратную ломаную порядка п-1.
Выясним, чем отличаются построения от головы к хвосту и от хвоста к голове. На рис. 3.3 эти построения отличаются одним поворотом, причем в точке соединения ломаных порядка п-2.
Убедимся, что ситуация будет такой же для всех порядков и >2. Один и тот же поворот траектории при движении в одном направлении оказывается правым, а в противо
РЕКУРСИЯ
95
положном — левым. Поэтому повороты в точках соединения ломаных порядка л-2 всегда будут противоположными. Остальные части отличаться не будут, поскольку ломаная любого порядка п (и>2) разбивается на четыре части порядка п-2. Первая из них проходится от головы к хвосту, вторая — наоборот. Разберемся с третьей.
С третьей ломаной порядка п-2 начинается обратная ломаная (л-1)-го порядка. Но ломаная (л-1)-го порядка, если пройти ее от головы к хвосту, заканчивается обратной ломаной порядка л-2, значит, рассматриваемая третья часть является обратной к обратной, т.е. проходится от головы к хвосту. Аналогично и четвертая часть является обратной.
Итак, составляющие ломаной порядка л-1 (и >2) отличаются только направлением поворота, связующего ломаные порядка л-2.
» По приведенным рассуждениям запишите алгоритм и реализуйте его. Проверьте его правильность по рис. 3.4 и 3.5.
Рис. 3.5. Драконова ломаная порядка 10
Наконец, рассмотрим два подхода к решению с помощью “обычной” графики. Проще всего эмулировать черепашью графику, работая с глобальными переменными, хранящими х-, у-координаты и направление.
В “обычной” графике также возможен принципиально иной подход: построить АВ, перескочить в точку А' и построить А'В от головы А' к хвосту В. Но для этого нужно вычислить координаты А' и направление первого отрезка ломаной А'В', это называется сложнее, чем эмулировать черепашью графику.
» Реализуйте оба способа.
Упражнения
3 1. Дать рекурсивное определение функции, выражающей сумму значений цифр десятичной записи натурального л.
3 2. Написать рекурсивную процедуру печати десятичных цифр числа типа integer: в обратном порядке, начиная с младших разрядов;
в обычном порядке, начиная со старших разрядов.
3 3. Дать нерекурсивное определение “91-функции Мак-Карти” F, заданной так: Г(л)= л—10 при л>100, F(n) = F(F(n+ll)) при л<100. Написать функцию (на языке Паскаль) вычисления F(n) при л <200.
96
ГЛАВА 3
3.4.	Заданы две строки а и b с длинами тип. Нужно найти максимальную длину их общих подпоследовательностей символов (для строк “козёл” и “осёл” — “оёл” длины 3). Рассмотрим следующий рекурсивный способ вычисления искомой длины как величины Lmj< (LtJ означает искбмую длину для начал строк axa2...at и bxb2...ty. LtJ=0 при i=0 илиj=0. Иначе, если а=Ьр то Ltj= 1 +Lrxj-x, иначе L/J=max{Lrly, Почему непосредственная реализация этого способа рекурсивной функцией — безумие!
3.5.	Решить задачу 3.1, не используя рекурсию.
3.6.	Рассмотрим подпрограммы в листингах 3.5 и 3.6. В них есть фрагменты, где одни и те же действия повторяются по три раза. Можно ли заменить эти фрагменты на циклы вида fori: =1 to3do...? Требуются ли для этого какие-то дополнительные условия?
3.7.	Предложите способ построения драконовой ломаной (см. рис. 3.3), требующий объема памяти 0(2"), зато не рекурсивный и очень простой.
Глава 4
Нестандартная обработка чисел
В этой главе...
♦	Нестандартные представления целых чисел и операции с ними
♦	Представление и обработка действительных чисел с повышенной точностью
•	Вычисление и использование остатков от деления нацело
*	Использование систем записи чисел с недесятичными основами
•	Применение ро-алгоритма
4.1.	Длинная целочисленная арифметика
Стандартные числовые типы данных в языках программирования обычно имеют ограниченные диапазоны значений, и для решения некоторых задач их оказывается недостаточно. Чтобы повысить точность вычислений, увеличить диапазон или улучшить какую-нибудь еще характеристику, программист сам разрабатывает типы для хранения чисел и подпрограммы для арифметических действий с ними1.
Чаще всего используют длинную целочисленную беззнаковую арифметику. Рассмотрим средства, позволяющие обрабатывать точные значения неотрицательных целых чисел с большим количеством знаков. “Большое количество” зависит от конкретной задачи и ограничений по памяти и по времени работы программы. Это может быть, например, и 100 десятичных знаков, и 1000, и 100000. Обработка этих чисел также зависит от потребностей конкретной задачи. В одной задаче числа нужно только складывать и вычитать, в другой — умножать и делить, и т.д.
Напомним разницу между числом и цифрой. Число 123 (в десятичной записи) состоит из трех цифр: “1” в разряде сотен, “2” — десятков и “3” — единиц. В системе счисления с основанием В есть В цифр со значениями от 0 до В-1, например, в десятичной — от 0 до 9, в шестнадцатеричной — от 0 до F (F имеет значение 15) и т.д. Обычно цифры — это знаки, которые легко записываются на бумаге. Для представления чисел в компьютере знаки не нужны, поэтому можно использовать системы основаниями 100,10000, 65 536 и т.д., не беспокоясь, что для них знаков нет. Главное, что в 100-ричной системе используются цифры со значениями от 0 до 99, 65 536-ричной — от 0 до 65 535, и т.д.
1 Впрочем, дроби с потенциально бесконечными числителями и знаменателями есть в язы-Lisp, потенциально бесконечные целые числа — в языке Python, и т.д.... Их ограниченность визана только с размерами памяти, предоставляемой программе.
98
ГЛАВА 4
4.1.1.	Представление длинных чисел
Для работы с числами, имеющими много разрядов, логично использовать массивы. В простейшем случае каждый элемент массива байтов может содержать одну десятичную цифру числа, но это неэкономно по памяти: байт может иметь 256 значений, и лишь 10 из них оказываются “разумными”. Другая крайность — трактовать массив байтов как число в системе счисления с основанием 256: любое значение байта является “разумным” значением 256-ричной цифры, и память используется максимально экономно. К тому же, удается ускорить обработку за счет использования специальных команд (типа shl, shr, побитовый and и т.д.). Но ввод-вывод такого числа в десятичной форме оказывается нетривиальной задачей (подраздел 4.1.3). Поэтому систему счисления с основанием 256 (или 65 536) используют, когда есть жесткие требования по памяти или известно, что почти вся работа относится к “внутренним” действиям над числами и лишь малая часть — к вводу-выводу.
Часто разумным оказывается компромиссный вариант: массив элементов типа byte, представляющих числа от 0 до 99 (цифры по основанию 100), или тйпа word (цифры по основанию 10000). По сравнению с хранением одной десятичной цифры в byte, удается приблизительно вдвое сэкономить память, почти не усложнив ввод-вывод. При этом еще и ускоряются некоторые арифметические действия — за счет того, что у числа, записанного в 10000-ричной системе, вчетверо меньше цифр, чем у записанного в десятичной.
Еще нужно решить, в каком порядке хранить цифры в массиве. На первый взгляд — как пишутся на бумаге, так и хранить. Однако лучше порядок, когда в первом элементе массива хранится младшая цифра (количество единиц), во втором — предпоследняя по старшинству (количество “десятков” системы счисления) и т.д. При таком порядке цифры одинаковых разрядов находятся в элементах с одинаковыми индексами. Кроме того, если после арифметического действия изменяется количество разрядов (например, 98+5 = 103), то изменяется только содержимое старших разрядов и сдвигать цифры не приходится.
Говоря о массиве, нужно определить количество его элементов. Наиболее опытным читателям советуем не фиксировать размер массива в программе, а выделять под него свободную память по мере надобности2. А остальным скажем, что обычно максимальное количество цифр длинного числа или задано в постановке задачи, или его нетрудно оценить. Поэтому объявим константу MAXN и будем хранить длинное число в массиве с размером 1. . MAXN.
Однако это не значит, что нужно заполнить все незанятые элементы нулями и в дальнейшем выполнять все арифметические действия надо всеми MAXN цифрами, не отличая занятые от незанятых. Часто оказывается, что, выполняя действия только над нужными разрядами, можно существенно выиграть во времени работы программы (например, если при сравнении чисел в одном из них больше знаков, то оно и будет больше —i- можно даже не смотреть на цифры).
2 С помощью GetMem / FreeMem для языка Pascal, new / delete для C++.
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
99
Поэтому часто целесообразно представлять длинное целое не просто массивом, а записью с двумя полями: массивом и целочисленной переменной — количеством реально занятых элементов массива3,
• Любая дополнительная информация полезна лишь при условии, что она правильно отражает реальное положение вещей; в частности — своевременно и правильно модифицируется при выполнении каждого действия над числами.
Итак, для представления длинных целых беззнаковых чисел используем такую структуру:
type ULong = record
used : IDX_TYPE;
mass : array [1..MAXN] of DIG_TYPE;
end;
Разумеется, где-то выше должны быть объявления наподобие
const MAXN = 1000; const BASE = 100;
type IDX_TYPE = 0..MAXN; DIG^TYPE = 0..BASE-1.
♦ Количество реально занятых цифр при желании можно разместить не в отдельном поле записи, а в нулевом элементе самого массива (аналогично длине строки в типе string языка Turbo Pascal)4. Однако это не лучшее решение, поскольку в одном массиве смешиваются величины разной природы. К тому же, записи с несколькими полями позволяют легйо менять тип (и, следовательно, диапазон) величин одной природы, не трогая величины другой.
Говоря о количестве занятых цифр, решим также вопрос: “Сколько цифр в числе (Л”. 1ожно принять любое из двух решений — нуль цифр или одна цифра 0. Приняв это решение, в дальнейшем его необходимо четко придерживаться (иначе может оказаться, что один 0 строго больше другого или еще что-то в этом роде...). В дальнейшем окажется удобнее считать, что в числе 0 нуль цифр.
Нужно также решить, что делать с незанятыми старшими элементами массивов: размещать там принудительно нули или считать, что там может находиться любой “мусор”. Обычно разумнее допускать “мусор”, поскольку многократные заполнения сех MAXN разрядов нулями стоят слишком дорого. Поэтому нужно не забывать заводнять нулями отдельные цифры или диапазоны там, где без этого возникает риск принять “мусор” за реальное значение.
И последнее замечание. В данной главе изучается эффективность алгоритмов выполнения арифметических действий с числами, представленными не в базовых скалярных типах, а в виде массивов. В выражениях для асимптотических оценок параметр п обычно характеризует размер входных данных, поэтому при рассмотрении алгоритмов длинной арифметики (не только целочисленной) под п будем понимать шишчество цифр.
3 При использовании динамической памяти — записью с тремя полями: указателем на ассив, объемом памяти, выделенной под динамический массив, и количеством реально за-шпых разрядов.
4 В старших версиях Delphi тип string организован иначе, а возможность использования Трбо-Паскалевского” способа оставлена исключительно для совместимости.
100
ГЛАВА 4
4.1.2.	Сравнение, сложение и вычитание длинных целых
Начнем со сравнения длинных чисел, представленных в типе ULong. Подпрограмме передаются два длинных числа (обозначим их L1 и L2). В качестве результата разумно возвращать отрицательное число, положительное или нуль соответственно ситуациям: первый аргумент меньше, больше или равен второму5. Достоинство этого решения в том, что возвращается больше информации, чем просто “да/нет”. Например, если в некотором алгоритме для каждой из трех ситуаций L1<L2, L1=L2, L1>L2 нужно выполнять свои действия, то достаточно вызвать подпрограмму сравнения один раз и запомнить результат в целой переменной, с ее помощью различая эти три ситуации.
Как уже сказано, если LI,used*L2.used, то большее число то, у которого больше цифр. Если же количества цифр равны, то будем сравнивать значения цифр, начиная со старших (больших индексов), пока не наступит одно из двух событий — цифры разные (большее число то, у которого больше цифра) или цифры закончились (числа равны).
Подпрограмма сравнения представлена в листинге 4.1. Асимптотическая оценка количества ее действий очевидна — О(п).
Листинг 4.1. Функция сравнения длинных целых
function UL_cmp const LI, L2 : ULong) : integer;
{ результат меньше 0 при L1<L2, 0 при L1=L2, больше 0 при L1>L2 }
var i : IDX_TYPE;
begin
if LI.used > L2.used then
UL_cmp j= BASE { у LI больше цифр }
else if LI.used < L2.used then
UL_cmp := -BASE { у L2 больше цифр }
else begin
i := LI.used;
while (i >= 1) and (LI.mass[i] = L2.mass[i]) do dec(i);	{ пропускаем одинаковые- цифры }
if i = 0 then
UL_cmp := 0	{ все цифры равны => числа равны }
else
UL_cmp := integer(LI.mass[i])-integer(L2.mass[i]);
{ больше число, у которого цифра больше }
end; end;
I ♦ Напомним: модификатор const в заголовке функции иъ_сггр неформально означает | “передавать ссылки на ы и L2 (не копируя их в программный стек), но следить, чтобы подпрограмма их не изменяла”.
Перейдем к сложению. Реализуем хорошо известное сложение в столбик — складываются отдельные цифры, начиная с младших; если сумма больше основания сис
5 Так же делает функция сравнения строк int strcmp (char*, char*) в C/C++.
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
101
темы счисления, то происходит перенос в следующий разряд. Однако есть один вопрос. Аргументы-слагаемые подставляются в подпрограмму на место параметров L1
L2 типа ULong, но куда записать сумму?
Нередко проблема решается сама собой, например, если в задаче нужна не “настоящая” сумма, 4 только операции вида s : = s+a.6 Тогда можно записать результат в параметр L1 (объявив его с модификатором var).
Однако бывает и так, что нужно именно сложение, “не портящее” аргументы. Можно было бы запомнить сумму в локальной переменной типа ULong и возвратить ее значение как результат функции сложения, например, с таким заголовком: function UL_add(const LI, L2 : ULong) : ULong;
Так можно поступить в С, C++, Delphi, Free Pascal... Но в Turbo (Borland) Pascal функция может возвращать только скалярные типы, строки и указатели.
Поэтому реализуем арифметические подпрограммы (в том числе сложение) в ви-ж процедур с тремя параметрами типа ULong: первые два задают слагаемые, третий — место для результата (листицг 4.2).
Листинг 4.2. Процедура сложения длинных целых
procedure UL_add(const LI, L2 : ULong; var LS : ULong);
(* Тип wide_val = O..2*BASE-1 нужно определить заранее *) var t :^ide_val; (* сумма очередной пары цифр *)
i : IDX_TYPE;
begin
t := 0; i := 1;
while (i <= LI.used) and (i <= L2.used) do begin
t := t + LI.mass[i] + L2.mass[i];
LS.massfi] := t mod BASE;
t := t div BASE;
inc(i);
end; (* собственно сложение LI и L2 *)
* переписывание старших цифр более длинного числа *)
if Ll.used >= L2.used then begin
LS.used := Ll.used;
while i <= Ll.used do begin
t := t+Ll.mass[i];
LS.mass[i] := t mod BASE;
t := t div BASE;
inc(i);
end;
end
else begin
LS.used := L2.used;
while i < = L2.used do begin
t := t + L2.mass[i];
LS.mass[i] := t mpd BASE;
t := t div BASE;
inc(i);
end;
6 Выразительнее s+=a в терминах C/C++/FreePascal.
102
ГЛАВА 4
end;
if t > 0 then begin (* перенос за край суммы *)
inc(LS.used);
LS.mass[LS.used] := t;
end;
End;
_	л	-	1
* В процедуре не предусмотрена ситуация переполнения, в которой результат не по- < мешается в массиве (ls , used становится равным maxn+1). Чтобы избежать неприятно-стей (аварийного завершения или “залезания” в чужую память), процедуру нужно модифицировать.	(
►> Реализуйте вычитание длинных целых чисел. Указание. Учтите, что при вычитании количество цифр результата может быть намного меньше количества цифр уменьшаемого (например, 10002-9915 = 87). Кроме того, уменьшаемое может быть меньше вычитаемого. Для начала можно считать, что такого не будет. Затем доработать программу, чтобы в этой ситуации она выдавала свое сообщение об ошибке. Наконец, можно и “по-честному” посчитать отрицательный результат: добавить к типу Ulong поле для знака и учитывать знак, вычитая из большего по модулю числа меньшее. Но тогда придется переписать все подпрограммы арифметических действий (включая сравнения и сложения), ведь “честный” числовой тип должен не только представлять отрицательные числа, но и обеспечивать арифметические операции с ними!
4.1.3.	Организация ввода-вывода
Организация ввода-вывода десятичных чисел зависит от основания внутренней системы счисления: 10, 10* (чаще всего 102 или 104) или основание другого вида (обычно 256 или 65 536).
Для десятичной системы вывод вовсе примитивен:
for i := L.used downto 1 do write(L.mass[i])
(если считается, что в числе 0 нуль цифр, перед этим нужно добавить проверку if L. used = 0 then write (' 0 ')). Ввод тоже не сложен, но, поскольку количество цифр в числе заранее не известно, придется сначала прочитать все знаки в некоторый буфер (например, массив символов), а потом уже перемещать их в массив L. mass в обратном порядке.
Для систем с основанием 10* общая логика та же, что для десятичной, но добавляются новые заботы. При выводе нужно не забыть ведущие нули для всех цифр, кроме старшей — например, число 10012 при основании внутренней системы 10000 должно получиться все-таки в виде 10012, а не 112 или 1012 (в его представлении used=2, mass[l] =12, mass [2] =1). При вводе опять-таки нужно сначала все цифры прочитать в дополнительный буфер, а затем, разбив ‘на группы по к цифр, начиная от младшей, заполнить начало массива L. mass.
Очевидно, что в описанных случаях количество действий процедур ввода-вывода есть 0(и).
Чтобы разобраться с вводом-выводом для произвольного основания внутренней системы счисления, вспомним суть позиционных систем счисления. Запись 123 в десятичной системе означает “одна сотня, два десятка, три единицы”, т.е. 1Ю2+
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
103
+ 2-10+3-1. Иначе говоря, (110+2)10+3. Следовательно, чтобы прочитать число, записанное в десятичной системе, в переменную L типа ULong, можно действовать по такому алгоритму:
инициализировать L := 0;
while (еще есть црфры) do begin
L *= 10; (* L := L*10 *)
L += a;	(* L := L+a; a -- значение очередной десятичной
цифры *)
перейти к следующей десятичной цифре end;
Вообще-то, это известный алгоритм перевода между системами счисления, просто обычно его применяют для перевода в десятичную систему, а здесь он применяется для перевода из десятичной системы во внутреннюю.
♦	Разумеется, этот алгоритм ввода пригоден и для основания вида 10*. Более того, он проще для реализации, чем описанный раньше. Но, как увидим ниже, он менее эффективен (0(и2) против 0(л)).
При выводе будем действовать строго в обратном порядке: поделим (целочисленно) начальное число на 10; остаток будет последней десятичной цифрой, а частное соответствует всем остальным (старшим) цифрам. Далее делим полученное на предыдущем шаге частное на 10, получая в остатке десятичную цифру разряда десятков, а в частном — общее количество сотен и т.д. Для вывода этих цифр в общепринятом порядке придется сначала записать их в некоторый буфер, а потом вывести в обратном порядке.
Сложность десятичного ввода-вывода с помощью указанных алгоритмов состав-тяет 0(и2), поскольку для обработки каждой из 0(и) десятичных цифр приходится умножать или делить на 10, а умножение или деление на малую константу требует 0(п) действий.
♦	Попытка использовать для реализации ввода алгоритм умножения из листинга 4.3 не приведет к особо плачевным последствиям, но все же разумнее использовать более простой и быстрый алгоритм умножения длинного числа на малое (он все равно нужен для реализации самого умножения в листинге 4.3).
♦	Существенно, что нужно использовать именно умножение или деление на малую константу, а не пытаться работать с константой 10 как с длинным числом. Ведь если при выводе использовать алгоритм деления из листинга 4.4 или 4.5, каждое деление потребует порядка О(я2) действий, а весь вывод — &(п3). Более чем странно, если вывод уже посчитанного числа окажется самым трудоемким этапом...
4.1.4.	Умножение длинных целых
Вспомним школьный алгоритм умножения в столбик: первое число умножается на каждую цифру второго; полученные произведения записываются со сдвигами, соответствующими положениям цифр, и складываются (рис. 4.1).
Итак, если оформить умножение длинного целого числа на обычное целое например, типа word; этц зависит от основания внутренней системы счисления) процедурой UL_mul_digit, сдвиг цифр — процедурой UL_shift_left, то умножение длинных целых чисел можно реализовать, как в листинге 4.3.
ГЛАВА 4
104
1	2	3
4	5	6
7	3	8
6	1	5
4	9	2
5	6	0	8	8
Рис. 4.1. Умножение в столбик
Листинг 4.3. Процедура умножения длинных целых
procedure UL_mul(const LI, L2 : ULong; var res : ULong);
var i : IDX_TYPE;
tmp : ULong;
Begin
res.used := 0;
(* * начинаем co значения res = 0 (где количество цифр нуль); дальнейшие исправления res.used и необходимые заполнения res.mass нулями производятся автоматически в вызовах UL_add *)
for i := 1 to L2.used do begin
(* записываем произведение LI на i-ю цифру L2 в tmp *) UL_mul_digit(LI, L2.mass[i], tmp);
(* сдвигаем полученное произведение *)
UL_shift_left tmp, i-1);
(* прибавляем сдвинутое произведение *)
UL_add(res, tmp, res);
end;
End;
Основное отличие этой реализации от рис. 4.1 — на рис. 4.1 сначала вычисляются все произведения 123 на каждую цифру 4, 5, 6 и затем складываются, а в процедуре UL_mul сложение происходит после каждого умножения L1 на очередную цифру L2.
В процедуре в листинге 4.3 есть вызов UL_add (res, tmp, res) (иначе говоря, res : = tmp+res). Если пользоваться процедурой UL_add (см. листинг 4.2), этот вызов будет работать правильно. Но далеко не каждая подпрограмма умеет правильно разобраться с ситуацией, когда одна и та же переменная передается в качестве сразу нескольких аргументов! Например, вызов UL_mul (а, Ь, а) приведет не к а : = а*Ь, а к безвозвратной потере старого значения а (на его месте вначале окажется 0).
* Реализация в листинге 4.3 правильно обрабатывает вызов UL add(res, tmp, res), однако делает это не самым эффективным образом. Ведь если количество цифр в tmp значительно меньше, чем в res, то можно сложить только разряды, которые есть в tmp, обработать возможные переносы за пределы tmp, а к еще более старшим цифрам res вообще не обращаться. Это наблюдение не очень важно для умножения, но оказывается существенным, например, при делении (листинг 4.5). Но, чтобы использовать это наблюдение, нужно реализовать отдельные подпрограммы для операций + и +=,
Сложность алгоритма умножения в листинге 4.3 очевидно выражается как Q(mn), где тип — длины множителей.
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
105
4.1.5.	Деление длинных целых
Перейдем к наиболее неприятному действию — делению. Для деления известны различные алгоритмы, но мы ограничимся несколькими вариациями известного всем деления “уголком” (рис. 4.2).
6 5 4 3 2 1 3 2 1
6 4 2	2 0 3 5
1 1 3
0
113 2 9 6 3
1 6 9 1
16 0 5 8~6
Рис. 4.2. Деление уголком
♦	Напомним, как работает этот алгоритм. Первая цифра частного равна 2, поскольку в 654 множитель 321 помещается 2 раза; остаток 11 используем далее. Следующее уменьшаемое 113 получается из остатка предыдущего шага (11) и снесенной очередной цифры делимого. 113 <321, Поэтому в частное идет цифра 0, и очередное уменьшаемое равно 1132. В него слагаемое 321 входит трижды, отсюда очередные цифра частного 3 и остаток 169. Аналогично получаем последнюю цифру частного 5 и окончательный остаток 654321 mod 321 = 86.
♦	Этот алгоритм, как и большинство алгоритмов деления целых, находит и целое частное, и остаток. Причем очень часто, например, в десятичном выводе для произвольной системы счисления (см. подраздел 4.1.3), нужны именно оба результата. Поэтому напишем процедуру, которая получает два входных аргумента (делимое и делитель) и возвращает два выходных— целое частное и остаток.
Этот алгоритм программируется довольно просто. Нужны сдвиги, сравнения, вычитания и подбор значения цифры. Все этапы, кроме последнего, уже реализованы. Подбор цифры тоже не очень сложен. В процедуре из листинга 4.4 подбор реализован так: переменная L2_sh содержит (сдвинутый на нужное количество цифр) делитель L2, а в переменных eubjrev и sub_this строятся значения L2_sh, 2xL2_sh, 3xL2_sh, ..., пока не окажется, что sub_prev< remain^sub_this. Когда это произойдет, множитель при L2_sh (хранимый в переменной v) как раз и будет значением цифры частного.
Листинг 4.4. Процедура деления длинных целых (для произвольной системы счисления)
procedure UL_div(const LI, L2 : ULong; var frac, remain : ULong);
var i : IDX_TYPE;
V : DIG_TYPE;
L2_sh, sub_jprev, sub_this : ULong;
cmp : integer;
Begin
remain : = LI;
106
ГЛАВА 4
if UL_cmp(Ll, L2) < 0 then begin
(* LI < L2 => frac=0, remain=Ll *)
UL_init(frac, 0); exit
end;
frac.used := LI.used - L2.used + 1;
for i := frac.used downto 1 do begin
L2_sh := L2;
UL_shift_left(L2_sh, i-1);
sub_this := L2_sh;
UL_init(sub_prev, 0);
v := 0; (* очередная цифра частного *)
cmp := UL_cmp(remain, sub_this);
(* Цикл while обязательно заканчивается при sub_prev < remain <= sub_this.
Это и обеспечивает подбор очередной цифры частного *)
while cmp >= 0 do begin
sub_jorev := sub_this;
UL_add sub_prev, L2_sh, sub_this);
inc(v ;
cmp := UL_cmp(remain, sub_this);
end;
frac.mass[i] := v;
UL_sub(remain, sub_prev, remain)
end;
if (frac.used > 0) and (frac.mass[frac.used] = 0) then dec(frac.used);
End;
Однако в этом алгоритме возникает пренеприятнейшая зависимость количества шагов по подбору цифры частного от основания системы счисления BASE (в худшем случае может понадобиться перебрать все значения L2_sh, 2xL2_sh, 3xL2_sh, ..., BASExL2_sh).
Теоретически, BASE=<?(1) (конкретное константное значение BASE выбирается при написании программы и не зависит от и), поэтому умножение на BASE не влияет на асимптотическую оценку количества действий. Однако это теоретическое рассуждение не отменяет того практического факта, что увеличение внутреннего основания с 10 до 10000 замедляет работу процедуры из листинга 4.4 в 20-40 раз (хотя для сложения, вычитания и умножения ускоряло в 3-4 раза за счет уменьшения количества цифр).
Желательно, чтобы количество действий алгоритма перестало существенно зависеть от основания системы счисления. Можно начинать перебирать v не с нуля, а с более близкой оценки, например, целого частного одной-двух старших цифр остатка remain на старшую цифру делителя. Ничего принципиально сложного тут нет, но необходимо позаботиться о некоторых тонких моментах: следить*, когда брать одну цифру remain, а когда две; как округлять частное старших цифр, чтобы оно не оказалось больше “настоящего” значения цифры частного, полученной по всем цифрам, и т.п.
Другой способ ускорения длинного деления основан на наблюдении, что для двоичной системы подбор цифры частного особо прост. Нужно выбрать всего из двух возможностей (0 или 1), что можно сделать одним сравнением UL_cmp (remain, L2_sh) >= 0. Попытаемся при большом основании BASE выразить подбор цифры частного через не
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
107
сколько аналогичных сравнений. Можно сначала сравнить remain с ГBASE/2"|xL2_sh; в зависимости от его результата, получаем v>Fbase/2~| или v<FbaseZ2~|; в первом случае сразу уменьшаем remain на ГBASE/2"|x L2_sh. Теперь можно сравнивать текущее значение remain с ГBASE/4"|x L2_sh, и т.д.
Описанная идея в1 принципе применима для любой системы счисления. Но ее реализация проще и красивее, когда BASE является степенью двойки (листинг 4.5).
Листинг 4.5. Оптимизированная процедура деления длинных целых aTWO_POWER%
(для основания 2	)
procedure UB_div(const LI, L2 : ULong; var frac, remain : ULong);
(* Тип IDX_TYPE_WIDE = 0..MAXN нужно определить заранее*) var L2_sh : ULong;
div_shift, num_bits : IDX_TYPE_WIDE;
Begin
remain := LI; UL_init(frac, 0);
if UL_cmp(Ll, L2) >= 0 then begin
L2_sh := L2;
diV_shift := (IDX_TYPE_WIDE(Ll.used)-
IDX_TYPE_WIDE(L2.used)+1)*TWO_POWER - 1;
UL_shl_binary(L2_sh, div_shift);
numbits := 0;
while div_shift >= 0 do begin
if UL_cmp(remain, L2_sh) >= 0 then begin
UL_sub(remain, L2_sh, remain);
UL_shl_binary(frac, num_bits);
UL_add_assign_digit(frac, 1);
num_bits := 0;
end;
inc(num_bits);
dec(div_shift);
UL_shr_binary(L2_sh, 1);
end;
UL_shl_binary(frac, num_bits-l);
end;
End;
4.1.6. Целая часть квадратного корня длинного числа
Задача 4.1. По длинному числу L найти целую часть квадратного корня L 4l J. Входные данные и результат записываются в десятичной системе.
Анализ задачи. Чтобы определить одну старшую цифру квадратного корня, достаточно разбить входное число на группы по два разряда (начиная от младших) и посмотреть на старшую группу. Старшая цифра корня dk равна L y]gt J, где gk — значение старшей группы аргумента. Иначе говоря, старшая цифра корня — это максимальное значение dk, при котором dk<gk. Например, число 987 654 321 при разбиении на группы в десятичной системе) имеет вид 9'87'65'43'21; старшая группа —9, поэтому старшая цифра квадратного корня равна 3.
108
ГЛАВА 4
Аналогично, вторая по старшинству цифра корня определяется только двумя старшими группами аргумента. Ее можно искать как наибольшее значение dt_15 при котором (Wdl+dk_l')l<Ek_l, где = lOOg^+g^, — значение двух старших групп; например, Для числа 987 654321 получаем Е4=987.
Эту стратегию несложно продолжить — искать dk_2 как наибольшее значение, при котором 100^+ \0dki+dk2<Ek_v затем аналогично dk__3, dk_* и тд. Чтобы найти каждую из этих цифр, достаточно просто перебрать значения от 0 до 9; чтобы принять одно из решений “подходит текущее d”, “вернуться к предыдущему значению d” или “пробовать дальше”, можно каждый раз возводить в квадрат длинное число dkdk_x ...dt, в котором цифры dk, dk_x, ...,dM найдены раньше, a dt подбирается, и сравнивать с Ег
Нетрудно убедиться, что общее количество действий такого алгоритма можно оценить как О(п), где п — количество цифр во входном числе. Действительно, количество цифр в корне равно Гп/21= ©(«), при поиске каждой необходимо несколько раз возводить в квадрат число длиной О(п), возведение в квадрат имеет оценку О(п); итого получаем О(п).
Приведенный алгоритм можно существенно оптимизировать. Известную формулу (a+Z>)2= а+2аЬ+Ьг перепишем в виде
(a+b)2 = а2 + (2a+b)b.	(4.1)
Если в (4.1) вместо а подставить число dkdk_x...dM0 (обозначим его А.), вместо b — подбираемую цифру dp то формулы, употребляемые для поиска значения dp можно переписать в виде А/+(2А(.+d) d<Ep перенеся А2 вправо, получим
(2А,+di)-di <Е,~А2.	(4.2)
Правая часть (4.2) не зависит от dr Значения же левой части при d=0, 1, 2, 3, ... выразим как
0,
2А,+1,
(2А, + 2)-2 = (2А,- +1) + 2А, +1 + 2,
(2А,-+3)-3 = (2А, + 2)-2+2А,-+2 + 3
и так далее, т.е. каждое следующее значение левой части можно получить по предыдущему и значению 2А, с помощью только сложений.
Более того, ничего кроме сложений, вычитаний и сдвигов не нужно и при переходе от одного i к следующему. Ведь изЕ.= 100Еж+^, А= 10(Аж+Ji+1) и (4.1) следует
Е(-А2 =100 ( Ем -А,2+1 - (2А,+1 +dM)dM )+gi	(4.3)
»	V---•	*------V-------'	>
предыдущая пра— выбранное значение преды— воя часть (4.2) дущей левой части (4.2)
Из всего перечисленного следует алгоритм, пример применения которого приведен на рис. 4.3. В средней части вверху записан разбитый на группы аргумент 987654321, в следующих строках средней части — значения Е-А* и выбранные значения (2А/+1+	В правой части указано, почему выбираются именно эти
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
109
значения dr В левой части записаны последовательности 2А. и dr Результат — 31426; можно получить как последовательность использованных на разных шагах цифр
кш как число 62 852 (слева внизу), поделенное на 2.
3
3
б 1
1
6~2 4
4
6 2 8 2
_________2
6 2 8 4
4
6 2 8 5 2
9 87 *65 43 21 ?
87 61
26 65
24 96
1 69 43
1 25 26
43 79 21 38 30 76
(ЗхЗ = 9^9< 4x4 = 16)
(61x1 = 61 s87< 62x2 = 124)
(624 х 4 = 2496 <: 2665 < 625 х 5 = 3125)
6282 х 2 = 12564 <. 16943 < 6283 х 3 = 18849)
(62846 6 = 383076 £ 437921 < 62847 х 7 = 440069)
Рис. 4.3. Пример извлечения округленного вниз квадратного корня
Оценим коЛиество действий нового алгоритма. Количество цифр в корне не изменилось (Гл/2"|=0(п)), но теперь при подборе значения d, используются не возведения в квадрат стоимостью О(п~), а сложения стоимостью О(п). Переход между соседними значениями i согласно (4.3) также требует О(п) действий. Итого, общая оценка равна О(п).
* Количество длинных сложений при поиске d/оказывается зависимым от основания системы счисления base (см. аналогичное замечание для деления).	.
Наконец, для квадратного корня можно применить бинарный поиск (подробнее в разделе 5.1). Начальный интервал можно получить, посчитав несколько старших цифр корня по старшим цифрам входного числа (с помощью обычной eqrt) и округлив это значение рниз и вверх с некоторым “запасом”. Сам поиск тоже очевиден: возводить каждое пробное число в квадрат и смотреть на соотношение этого квадрата и входного числа, в зависимости от результата сужать диапазон поиска влево или вправо.
Сложность такого алгоритма О(п), т. е. он не оптимален. Однако такой подход оказывается разумным, если главная цель — быстро написать работающую программу на основе уже имеющихся подпрограмм основных арифметических действий.
4.2.	Два магических числа
4.2.1.	Число е
Задача 4.2. Вычислить значение основания натурального догарифма е= lim(l + l/n)" с заданным количеством М десятичных знаков после запятой, л-и»
Программа должна работать для как можно больших М.
110
ГЛАВА 4
Анализ задачи. Значение е можно вычислить с помощью формулы
1 — + п\
е =
(4.4)
2
Готовая целочисленная длинная арифметика может весьма облегчить решение данной задачи — достаточно найти в ней числитель и знаменатель суммы (4.4). Как нетрудно убедиться, при переходе от суммы 1/2!+ 1/3!+ ... + 1/(/-1)! к сумме 1/2! + 1/3!+...+ 1/(/-1)!+1//! знаменатель умножается на/, а числитель умножается на/ и складывается с 1.
Когда требуемая точность будет достигнута (подробности ниже), останется вывести полученный результат в виде десятичной дроби, что также не очень сложно. Действительно: сумма (4.4), если начать ее со слагаемого 1/2!, меньше единицы. Заметим: значение одной цифры после запятой (количество десятых) дроби A/В равно [10хА/В], значение двух цифр (десятые и сотые) равно [100хА/В], и т.д. Поэтому для вывода М знаков после запятой можно умножить числитель длинной дроби на 10м и вывести [10 хА/В]. Можно также получать эти цифры по одной: М раз умножаем дробь на 10, выводим целую часть как очередную десятичную цифру, а дробную используем для следующего умножения (см. раздел 4.1.3, алгоритм десятичного вывода при использовании произвольной системы счисления).
Но если готового длинного целочисленного деления нет, гораздо удобнее вычислять частичные суммы (4.4) непосредственно в виде приближенной десятичной дроби. Используем следующее представление длинной десятичной дроби: массив элементов типа word, основание системы счисления — 10000; первый элемент массива содержит первую цифру (по этому основанию) после запятой, второй — вторую цифру, и т.д.
В нашем представлении запятая (точка), отделяющая целую часть числа от дробной, всегда находится перед первым элементом массива, т.е. фиксирована. В частности, если число довольно близко к нулю (например, 0,0000123), какое-то количество начальных элементов массива будет занято нулями, а значимые цифры начнутся в дальнейших элементах. Такой способ хранения дробей называют представлением с фиксированной точкой (fixed point).
Представление чисел в машинных “действительных” типах (real, extended ит.д.) аналогично математической записи вида 1,23x10 ’, т.е. в мантиссе сразу идут значимые разряды, а порядок (информация о том, где относительно этих разрядов находится запятая) хранится отдельно. Таким образом, запятая “плавает” относительно старшего значимого знака, и представление называют с плавающей точкой (floatingpoint).
Обычно используют дроби с плавающей точкой, поскольку для этого способа гораздо меньше риск переполнения и потери точности. Однако в данной задаче для арифметических действий (особенно сложения) удобнее фиксированная точка.
Будем поддерживать две переменные типа “длинная дробь”: curr — слагаемое 1/и! для текущего п, и sum — текущее значение частичной суммы 1/2! + ... + 1/и!.
Согласно (4.4), для перехода от l/(n-1)! к 1/м! нужно разделить длинную дробь на (обычное) целое и сложить две длинные дроби. В обычных типах это можно записать как curr : = curr/п и sum : = sum+ curr. Другие арифметические операции с длинными дробями не нужны, но есть еще программистская операция инициализации — sum и curr нужно присвоить начальное значение 1/2.
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
111
* Операции деления длинной дроби на обычное целое и сложения дробей можно реализовать со сложностью О(М), где М — количество цифр в длинном числе.
В условии есть скользкий момент — задано не число слагаемых, а количество знаков после запятой. Сколько слагаемых нужно взять, чтобы получить заданное количество знаков после запятой, неизвестно. Примем такое решение: остановиться, когда очередное слагаемое станет меньше, чем требуемая точность 10 * Просим поверить на слово, что для формулы (4.4) этот критерий остановки верен, но вообще-то, чтобы утверждать подобные вещи, нужен серьезный математический анализ. Ведь есть даже ряд 1+1/2+ 1/3+ ...+ 1/п+..., в котором слагаемые монотонно стремятся к нулю, а сумма — к бесконечности...
Сколько знаков после запятой нужно обеспечить в наших длинных числах? В условии задано “количество М десятичных знаков после запятой”, поэтому, на первый взгляд, достаточно Пи/4~| знаков по основанию 10000. Однако в представлении с фиксированной точкой присутствует округление. Значит, есть и погрешность. Хуже того, погрешности накапливаются в процессе работы алгоритма (погрешность суммы “вбирает в себя” погрешности слагаемых).
К счастью, при подсчете по формуле (4.4) погрешности накапливаются очень медленно. Все значения curr (начиная с 1/6) являются приближенными, но мы сами выбираем точность этого приближения. Пусть выбрана точность к знаков после запятой (в системе с основанием 10000). При вычислении нового значения curr старое (приближенное) значение делится на целое (точное) текущее значение?!. Благодаря этому, сколько бы curr ни делилось, абсолютная погрешность не возрастет и не превысит единицы в it-м разряде после запятой. Следовательно, сложив п слагаемых, получим абсолютную погрешность не больше nxlOOOO’4.
Выясним, с какими п и М можно будет работать. Если исходить из ограничения на размер массива 64Х, налагаемого DOS-овскими компиляторами, мы не сможем получить больше =131000 десятичных знаков. Чтобы найти значения п, соответствующие этому, будем в цикле вычислять 1п(и!) = £"=1lni, пока не достигнем 131000х1п 10. Получим, что ««32000, т.е. для его представления можно использовать word или integer. Если же нужна большая точность (миллионы десятичных знаков), то (в 32-битовом компиляторе) придется представить п в longint и написать деление длинной дроби на число типа не word, a longint.
При таких значениях п достаточно проводить все внутренние вычисления с точностью на 2 знака (системы с основанием 10000) большей, чем точность, с которой нужно вывести результат. Чтобы не морочиться с округлением М/4, используем USED_CELLS:= (Mdiv4)+3.
Величина слагаемых curr постоянно уменьшается, и при больших п значительная часть старших разрядов curr содержит нули. Поэтому, во избежание ненужной работы, будем хранить в длинной дроби, кроме массива цифр и индекса USED_CELLS (см. выше о точности), еще и наименьший индекс ненулевого элемента.
Н Реализуйте описанные действия.
112
ГЛАВА 4
4.2.2.	Число ТС
Задача 4.3. Написать программу, которая вычисляет значение п с заданным количеством М десятичных знаков после запятой. Программа должна работать для как можно больших М.
Анализ задачи. Опишем довольно простой (хотя и не наилучший) способ вычисления л. Он основан на формуле для arctgx:
arctgx = х-х3/3 +х5/5 -х7/7 + ... при |х| 1.	(4.5)
При х= 1 имеем jr/4=arctg 1 = 1 - 1/3 + 1/5 - 1/7 + ..., но слагаемые в этой сумме убывают очень медленно, и нам пришлось бы суммировать неимоверное количество слагаемых. Однако, чем ближе х к 0, тем быстрее сходится сумма в формуле (4.5).
Подберем а и Ь, при которых tga и tgb строго меньше 1 и tg(a+Z?) = 1, т.?. л/4 = = arctg(tg(a+Z>))=a+/>. Например, npHa=arctg(l/2), Z>=arctg(l/3) имеем
tg (a+b) = (tg а + tgb)/(l - tga • tg b) = (1/2 + l/3)/(l - (l/2) ( 1/3)) = 1.
Итак, a=arctg(l/2) и b=arctg(l/3) можно вычислить с помощью формулы (4.5), а затем сложить — получим л/4. Однако лучше использовать другие разложения л/4, например
л/4 = 4 arctg(l/5) - arctg( 1/239),
л/4= 8arctg(l/10) - 4arctg(l/515) т arctg( 1/239),
л/4= 3arctg(l/4) + arctg(l/20) + arctg(l/1985),
у которыхx в формуле (4.5) имеет меньшие значения.
Итак, основное в решении — вычисления по формуле (4.5). Как и в предыдущей задаче, нужны два массива для представления дробных цифр очередного слагаемого и накапливаемой суммы. При переходе от (л-1)-го слагаемого к л-му нужно умножить на х2 и на (л-2) и разделить на л.
Учтем, что в данной задаче х — это дробь вида 1/г, где г — короткое целое число. Тогда вместо умножения на дробное число х2 можно использовать все то же деление на короткое целое г2. Таким образом, к операциям деления длинной дроби на короткое целое число и сложения дробей (см. предыдущую задачу) придется добавить только умножение на короткое целое число.
►► Реализуйте представленное решение.
4.3.	Остатки от деления
Во многих задачах приходится вычислять остатки от дедения сумм, разностей или произведений некоторых чисел на заданное натуральное числор, не изменяемое в процессе решения. Обычно для этого не обязательно вначале искать сумму и т.д., а затем вычислять остаток — можно использовать следующие тождества:
(а + b) mod р = ((a mod р) + (b mod р)) mod р, (a-b) mod р = (р + (а mod р) - (b mod р)) mod р при а > Ь,	(4.6)
(а • b) mod р = ((а mod р) х (b mod р)) mod р.
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
113
Например, для вычисления (a+b)modp даже не обязательно знать числа а и Ь — нужны только amodp и femodp. Докажем тождество для сложения (доказательства для других операций аналогичны):
(й + b) mod р = ((<2 div р)р + a mod р + (b div р)р + b mod р) mod р = (слагаемые, кратные р, не влияют на остаток)
= ((a mod р) + (b mod р)) mod р.
Формула для вычитаний несколько отличается от других из-за того, что в большинстве компиляторов остаток от деления отрицательного числа на натуральное отрицателен: например, (-13 mod 10) будет -3, а не 7.
Аналогичной формулы для делений нет и быть не может, поскольку вначале брать остаток, а потом делить — неправильно. Например,
(160 div 2) mod 100 = 80 # 30 = ((160 mod 100) div 2) mod 100.
4.3.1.	Плиты в треугольнике
Задача 4.4. Великая Треугольная Область (ВТО) представляет собой прямоугольный треугольник. Его катеты имеют целые длины типи лежат на осях координат. Нужно покрыть как можно большую часть территории ВТО квадратными плитами размером 1x1. Плиты должны плотно прилегать одна к другой и к катетам ВТО, не выходя за пределы области. Резать плиты нельзя.
Плиты поставляются только контейнерами по р штук; используется необходимый минимум количества контейнеров. Вычислить, сколько плит из последнего контейнера останется после покрытия ВТО.
Вход. Три целых числа: т, п (2< т, п < 2000000000) ир (100<р< 10000).
Выход. Количество оставшихся плит (целое число меньше р).
Пример. Вход: 4 3 100. Выход: 97.
Анализ задачи. Вначале рассмотрим длины катетов, имеющие общий делитель t, т е. числа вида т=pt, п=qt. Количество используемых плит описывает формула
K(pt, qt) = pq + t-K(p, q).	(4.7)
> Рассмотрим рис. 4.4. В границы треугольника попадают (/-1)+(t-2)+... +1 = f(r-l)/2 прямоугольников размерами pxq и t одинаковых прямоугольных треугольников с катетами р и q, что и доказывает формулу (4.7). <
114
ГЛАВА 4
Если длины катетов взаимно просты, имеет место формула
К(т, л) = (mn - (т + п - 1)) div 2
(4.8)
► Для доказательства рассмотрим рис. 4.5. Два прямоугольных треугольника с катетами т и л образуют прямоугольник размерами тхп. Числа т и л взаимно просты, поэтому диагональ прямоугольника (гипотенуза треугольника) не проходит через вершины клеток. Докажем, что она проходит через т+п-1 клеток.
Рис. 4.5. Иллюстрация для взаимно простых чисел
Для упрощения предположим, что т>п, длинная сторона горизонтальна, а короткая вертикальна (остальные ситуации рассматриваются аналогично). Диагональ спускается на л клеток, пересекая л-1 “внутреннюю” горизонталь, причем каждую в двух разных клетках, поскольку т>п. Поэтому в (л—1)-м столбце будут заняты диагональю (заштрихованы) две клетки, а в остальных столбцах (их т - (л -1)) — по одной. Итак, диагональ проходит через 2(л-1) + т—(л-1) = т+л-1 клеток.
Остальные клетки прямоугольника целиком принадлежат двум одинаковым прямоугольным треугольникам, откуда и получаем формулу (4.8). <
Итак, для решения задачи вычислим НОД(т,л). Если он равен 1, то применим формулу (4.8), а если больше 1, то применим формулу (4.8) к “меньшей” задаче и подставим ее результат в формулу (4.7). Получив общее количество клеток, целиком попадающих в ВТО, легко найти и количество оставшихся плит.
Остается одна проблема. При длинах катетов до 210’ общее количество клеток не помещается в longint, а типы с плавающей точкой не обеспечивают необходимой точности7. Можно использовать “длинную арифметику”, но общее количество клеток в действительности не нужно. Достаточно выполнять все действия в формулах (4.7) и (4.8), используя “арифметику по модулюр” и формулы (4.6).
Чтобы избежать переполнений, необходимо подробно расписать отдельные действия в формулах (4.7) и (4.8) и брать остаток от деления во всех ситуациях, в которых не исключено переполнение. Однако, поскольку вначале брать остаток, а потом делить — неправильно (см. начало данного раздела), придется вначале брать остаток от деления на 2р и только после деления на 2 искать “правильный” остаток (по модулю р) и выполнять действие.
Наконец, не забудем, что в задаче спрашивают, сколько плит останется, поэтому, найдя результат res, в самом конце нужно еще вычислить (р - res) modp.
7 При использовании типа QWord проблема решается, но эта задача предлагалась на УОИ-2003, когда в последний раз использовались только DOS-компиляторы.
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
115
Оценивая общее количество действий по приведенному алгоритму, видим, что основная часть работы приходится на вычисление НОД. Как известно, количество действий при вычислении НОД(/п,п) по алгоритму Евклида (в его современной версии) имеет оценку O(log(m+n)). Остальные шаги решения задачи (ввод, вывод и реализация формул (4.7) и/4.8)) требуют константного количества действий.
Наконец, вспомним принципиально иной алгоритм подсчета количества клеток. Его можно сформулировать так: “пройти по всем столбцам и в каждом подсчитать искомые клетки, находя точки пересечения и округляя”. Этот алгоритм будет правильным, но существенно менее эффективным практически при всех входных данных. Ведь только перебор столбцов требует количества действий порядка min(zn, и), что намного больше, чем log(m+«).
4.3.2.	Кратное число с одинаковыми цифрами
Задача 4.5. Найти наименьшее число с одинаковыми десятичными цифрами, кратное заданному натуральному числу К (типа integer). Вывести цифру и количество цифр в найденном числе. Если решения не существует, вывести о о. Например, при К= 3 7 нужно вывести 1 з, а при АГ=ю —о о.
Анализ задачи. Нужно найти число, кратное К, среди чисел следующей таблицы.
1	2	3	4	5	6	7	8	9
11	22	33	44	55	66	77	88	99
111	222	333	444	555	666	777	888	999
11...1	22...2	33...3	44...4	55...5	66...6	77...7	88...8	99...9
Число кратно К, если остаток от деления его на К равен 0, поэтому нам нужны не столько числа таблицы, сколько остатки от деления их на К. Обозначим число в т-й строке (т> 1) иj-м столбце (/ = 1,.... 9) через а остаток от деления его на АГ — через г^.. Нетрудно убедиться в правильности следующих утверждений.
1.	Первое число первой строки равно 1 и дает остаток гп = 1.
2.	Следующее число в первом столбце получается из предыдущего как АГ= 1(W , + 1, а остаток от его деления на К — как г . = (Юг , + l)modAT.
3.	Остатки	где т> 1, 2< j<9, получаются из остатка гт1 домножением на соот-
ветствующую цифру: r^fj-r^modK, где 2<j<9.
На основе этих правил нетрудно считать остатки построчно, пока не встретится 0. Однако как решить, что нужно остановиться, если 0 все еще не встретился? Ответим на этот вопрос.
Заметим, что различных ненулевых остатков всего АГ-1. Тогда аналогично задаче 1.6 в любом столбце j среди	гу,..., есть 0 или г*=г* при п < т < к.
Из правила 2 следует, что если гл1 = ги|, то гл+11 = rm+11, ^+21=r„,+2,i и т.д. Но аналогично правилу 2 при т>2, 2<j<9 получим, что
Nmj= lONm-ij+j и rmj=Nmj mod К= (lOr^ij+j) mod АГ.
116
ГЛАВА 4
Поэтому в каждом столбце повторение остатка приводит к зацикливанию последовательности остатков этого столбца. Но это значит, что если остатка 0 не было в первых К строках, то и дальше не будет. Итак, достаточно вычислить остатки не более чем К первых строк (с помощью правил 1—3).
Замечание. Числа указанного вида при делении на К не дают остатка 0 тогда и только тогда, когда К кратно 10,16 или 25 (советуем в качестве упражнения это доказать).
4.4.	Отслеживание циклических повторений
4.4.1.	Десятичное представление дроби и р-алгоритм
Задача 4.6. Натуральные кит, где 1 <к <т, задают правильную дробь к/т.
Вывести кратчайшую последовательность цифр XX...X, предшествующих периоду, и период УТ... У десятичного представления дроби в виде 0гХХ...Х(1Т...У). Если длина периода больше 100, вывести его первые 100 цифр и многоточие, а после периода — его длину в скобках.
Техническое ограничение: т< 200000000.
Примеры
Вход	Выход
3 4	0,75(0)
1 6	0,1(6)
1 199999999 0, (00000000500000002500000012500000062500000312500001562 50000781250003906250019531250097656250488281252...) (12343056)
Анализ задачи. Чтобы получить цифры d}, d2,... р-ичного представления правильной дроби к/т, нужно умножать и делить с остатком: dx = kxpdwm, r,= fcxpmodm, J2= i\xpdivm, r2= rtxp mod m и т.д. (d= r^xpdivm, r= r)Xxpmodm). Если очередной остаток r(=0, то все дальнейшие цифры будут нулями, т.е. представление будет иметь вид 0,d1d2...d/(0). Если же все остатки ненулевые, то р-ичное представление бесконечно и периодично (возможно, не с самого начала): Q,dldr..d[dl^dtl'Y..d), где i>0, j-te 1. Нужно найти минимальное i, после которого начинается период.
В условии задачи р=10, т<200000000, поэтому для вычислений достаточно стандартной арифметики типа longint. Очевидное решение состоит в том, чтобы очередной остаток сравнивать с предыдущими — первое повторение укажет на начало периода. Пусть — очередной остаток. Если гу=г при некотором i < J, то при d=dt периодом будет d^.-d^, иначе — d^...dj. Для хранения предыдущих остатков понадобится массив чисел типа longint.
Оценим необходимую длину массива остатков. Количество различных ненулевых остатков от деления на т равно т-1, и все они могут появиться в периоде. Кроме того, первый повторенный позже остаток может иметь номер не менее 1 и не более т-1. Итак, размер массива должен быть в пределах от т-1 до 2т-2. Однако т может достигать 2-108, и массив с элементами типа longint общим размером не менее чем 800 Мбайт, по которому нужно проходить с каждым новым остатком — это ужасно!
Теоретически, можно предположить, что память такого размера найдется, и вспомнить решение задачи 1.6. Инициализируем массив нулями, и будем присваи
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
117
вать номер шага, на котором получен остаток г, элементу с индексом г. Тогда проверка повторения остатка потребует 0(1) времени, что позволит достичь суммарной оценки сложности 0(т). Однако практически все это невозможно и, главное, не нужно, поскольку можно вообще обойтись без массивов.
Решение задачи. Во многих задачах фигурируют последовательности, которые, начиная с некоторого места, становятся циклическими. Их зацикливание можно изобразить в виде греческой буквы р (ро): есть начало линии, а конца будто и нет — линия закручивается в овал и циклически повторяется. Для поиска повторений и циклов в таких последовательностях используют так называемый р-алгоритм.
Суть р-алгордтма такова. Пусть значения элементов последовательности a,, av ... состояния рекуррентного процесса) принадлежат конечному множеству, вычисляются с помощью рекуррентного соотношения и, начиная с некоторого номера, образуют цикл. С помощью рекуррентного соотношения алгоритм образует пары значений (fl],a2), (а2,й4), (а3,а6) и т.д. “Расстояние” между элементами в парах все время увеличивается, поэтому рано или поздно оно обязательно окажется кратным длине уже начавшегося цикла, т.е. при некотором i значения а, и аъ совпадут. Итак, равенство at и аъ означает, что обнаружен цикл, который начинается не позже ;-го элемента и имеет длину не более чем I.
В такой формулировке р-алгоритм считает циклом и ситуацию, когда, начиная с некоторого шага, значения элементов вообще перестают изменяться. Но, с одной стороны, это нетрудно проверить, а с другой — часто, как и в данной задаче, выделять эту ситуацию и не нужно.
Разумеется, р-алгоритм корректен, только если очередное состояние рекуррентного процесса полностью определяется одним лишь предыдущим. Например, если значение последовательности зависит от двух предыдущих, “состоянием” нужно считать значения двух подряд идущих элементов.
• Основное достоинство р-алгоритма — возможность узнать о цикле, потратив очень мало памяти: хранятся лишь два-три состояния. Основной недостаток — он не дает точной информации о длине и начале цикла. При особо неудачном соотношении этих величин он может довольно долго “не замечать” уже начавшийся цикл.
Используем р-алгоритм для построения десятичного представления правильной дроби. Вначале найдем i, при котором r=rv Затем найдем последний номер last, при котором г/ля=г2Г Длиной периода periodLength будет гъ-гы. На следующем этапе используем последовательности остатков гр г2, ... и r1+jieWIeert,	...,
а также цифр d}, d2, ... и	d^^^, ..., чтобы найти начало периода. Циф-
ры “отстающей” последовательности выводим как предшествующие периоду. Равенства d=d^, „ , и г.=г^	,, сигнализируют о начале периода. Далее выводим
период или, если он слишком длинный, его начало (листинг 4.6).
Листинг 4.6. Десятичное представление правильной дроби____________
procedure fraction(numerator, denumerator s longint);
{numerator, denumerator -- числитель и знаменатель} last : longint; {индекс последнего элемента, равного r(2i)} i, i2 : longint; {индексы в парах (r(i), r(2i))} r, r2 : longint; {текущие остатки}
118
ГЛАВА!
d, d2 : longint; {текущие цифры} periodLength, outputLength : longint;
{длина периода и его части для печати} begin г := numerator;
i 1; i2 := 2;
г := 10*r mod denumerator; (r(l)}
r2 := 10*r mod denumerator; {r(2)j
{Вычисление i, при котором r(i) = r(2i)}
while r <> r2 do begin
r := 10*r mod denumerator; {r(i) -->r(i+l), r(2i) -->r(2i+2)} r2 := 10*(10*r2 mod denumerator) mod denumerator;
inc(i); inc(i2, 2) ;
end;
{r(2 i) = r(i)}
{Поиск последнего номера last, при котором r(last) = r(2i)} last :» i;
while (i < i2-l) do begin
r := 10*r mod denumerator;
' inc(i);
if r - r2 then last := i;
end;
periodLength := i2 - last;
{Значения r2 и d2 сдвигаем на periodLength относительно г и d} d := 10*numerator div denumerator; {d(l)} r := 10‘numerator mod denumerator; {r(l)} r2 : = r ;
{Сдвиг r2 и d2 на periodLength относительно г и d}
for i f= 1 to periodLength do begin
d2 := 10*r2 div denumerator;
r2 := 10*r2 mod denumerator;
end;
{Печать части представления, предшествующей периоду} write(10,1);
while (г о r2) or (d <> d2) do begin write(d);
d := 10*r div denumerator;
r := 10*r mod denumerator;
d2 := 10*r2 div denumerator;
r2 := 10*r2 mod denumerator;
end;
!r = r2 and d = d2}
Печать периода в представлении или его начала} write(1(1, d);
if periodLength <= 100 then outputLength := periodLength else outputLength := 100;
for i :» 1 to outputLength-1 do begin
d := 10*r div denumerator;
r i= 10*r mod denumerator; write(d);
end;
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
119
if outputLength <> periodLength then write
write(1)');
if outputLength <> periodLength then write (' (', periodLength, 1) 1) ; readin;
end;
Анализ решения. Оценим общее количество действий по приведенной процедуре. Все циклы, кроме первого, имеют очевидную оценку сложности О(т). Точно оценить сложность первого цикла весьма непросто, но в действительности она тоже равна О(т). Отсюда сложность решения — О(т), причем речь везде идет именно об О(т), а не о @(m)i количество действий для особо плохих пар к и т будет порядка т, но для большинства пар — гораздо меньше.
Поскольку Лит — значения входных чисел (а не длины их двоичных представлений), рассмотренный алгоритм является лсевдополиномиальным.
4.4.2.	Остатки от деления чисел Фибоначчи
Задача 4.7. В известной рекуррентной последовательности, называемой I числами Фибоначчи, каждый элемент (кроме двух начальных) является сум- I мой двух предыдущих: F0=O; Fj = 1; Fn= F^ + F^ при п>2. Найти остаток от I деления л-го числа Фибоначчи на натуральное число р.	I
Ограничения: 0 < п < 2 000 000 000, 1 <р <32 000.	|
Примеры	I
N	р	результат	I
0	7	0	I
6	16	8	I
12	10	4	I
Анализ и решение задачи. Формула Бине Гл=((1+>/5 )/2)”- (1—Vs )/2)")/Т5 позволяет найти n-е число Фибоначчи непосредственно по его номеру. Однако для программирования пользы от нее никакой — на реальных компьютерах выполнять точные действия с иррациональными числами нельзя, а приближенные вычисления не позволят найти остаток отделения.
Вспомним “обычный” алгоритм вычисления чисел Фибоначчи и формулы (4.6) (без них никак— F20MW00M>ю400 000000, т.е. даже при использовании длинной арифметики” размеры массивов будут огромными, а время работы — астрономическим).
Е0 := 0; fl := 1;
for i := 1 to n do begin
f2 := (fO + fl) mod p;
fO := fl; fl := f2;
•nd;
Задача почти решена. Однако пока нехорошо, что придется выполнять цикл до двух миллиардов раз. Рассмотрим три способа, как от этого избавиться.
Первый способ. Перейдя к вычислениям по модулю р (где р<32000), мы сделали множество возможных значений переменных f0, fl, f2 довольно небольшим. При
120
ГЛАВА 4
достаточно долгой работе цикла обязательно появится остаток числа Фибоначчи, уже вычислявшийся раньше. Более того, когда-нибудь обязательно будут получены два подряд идущих числа, уже появлявшихся раньше именно подряд. Но тогда дальше обязательно начнутся точные циклические повторения всей последовательности. Ведь если эти два числа такие же, как были раньше, то и следующее, определяемое как (F(_| +F._2) mod/?, окажется таким же, и т.д.
Однако от факта, что на каком-то (неизвестно, каком) i-ом шаге начнется цикл какой-то (неизвестно, какой) длины с, все же мало пользы. Поэтому обратим внимание, что из (n-l)-ro и n-го чисел Фибоначчи можно получить не только (и+1)-е, но также и (n-2)-e: Fn_2= Fn-FH_V Поскольку вычисления ведутся по модулю/?, Fn_2 можно найти, используя соотношение
F„_2 mod /?=(/? + Fn mod p - Fn_i mod p) mod p.
Отсюда
Fi mod p = Fi+C mod p mod p = FMt<. mod p
mod p = Fj_2+c mod p .
Применив этот прием i-2 раза, получим F0mod р = Fcmodp. Итак, в данной задаче цикл начнется обязательно с повторения начальных значений. Это позволяет написать программу со следующим фрагментом (для п > 0).
Фрагмент программы, проводящей сокращения с учетом зацикливаний
f0 := 0; fl := 1; С := 0;
repeat
f2 := (fO+fl) mod p;
fO := fl; fl := f2;
C := c+1
until (c >= N) or (fO = 0) and (fl = 1) ;
if c < N then begin
N := N mod c;
for i s= 1 to N do begin
f2 := (fO+fl) mod p;
fO := fl; fl := f2
end end; writeln(fO) ;
Анализ решения. Оценить количество действий в представленном решении довольно сложно: очевидно, оно пропорционально minpV,2c}, но как оценить длину цикла с? Теоретически, с<р\ нор2 — это очень много... Тем не менее в действительности min{Af, 2с} относительно невелик (при указанных ограничениях на р он практически всегда не больше 105).
Второй способ. Нетрудно убедиться в следующем:
i ил+ол-J
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
121
Отсюда
1 ’Y П Г1
J oj (л-.Д! оД1
С ( ' 0Ж-1,
1
1 0, <	/ V Ы >
и так далее, т.е.
I1 °J
^к+п
Fk+n-1
♦
Если в этом равенстве вместо Fk и F^l подставить Ft = 1 и Fo=0, а вместо л — п-1, получим
1 1Y’1 ГПГ Fn 'I
1 о J
Итак, в нашей задаче для вычисления Fnmodp достаточно возвести указанную матрицу в степень л-1, выполняя все действия в арйфметике по модулю р. Для этого нужно реализовать возведение матриц 2x2 в степень на основе алгоритма из раздела 3.2 с оценкой сложности O(log л).
Третий способ. Д ля чисел Фибоначчи справедливо тождество Fn= FJ,+1-Fn_t+F(t-Fn-lt_1. сложив fc=nmod2, можно за константное количество действий увеличить номер ела Фибоначчи вдвое. Однако здесь не удается выразить Fn через одно число Фи-наччи с приблизительно вдвое меньшим номером. Поэтому придется писать ре-рсию с запоминаниями (подробности в главе 13), да еще используя изощренную структуру данных. Так что до этого способа додуматься легче, чем до возведения матрицы в степень, но реализовать его гораздо сложнее.
Отметим, что описанные “быстрые” методы вычисления чисел Фибоначчи не всегда являются таковыми. Оценки O(logn) для возведения матрицы в степень и О(и) для “школьного” метода получены в предположении, что стоимость любой арифметической операции составляет 0(1).
Если же считать точные значения чисел Фибоначчи с помощью длинной арифметики из раздела 4.1, ситуация совершенно иная. Возводя матрицу в степень, придется выполнить O(logn) умножений длинных чисел. Согласно формуле Бине, длина к-ro числа Фибоначчи составляет 0(к). Следовательно, одно только последнее умножение будет “стоить” 0(л2). Вместе с тем нетрудно убедиться, что все умножения, вместе взятые, тоже “стоят” 0(и2), а не 0(n2 logn), как кажется на первый взгляд, ... азатем увидеть, что “школьный” метод имеет такую же оценку 0(п2)!!! Ведь он требует 0(п) сложений суммарной стоимостью ®(0 = ®(и2) • Следовательно, “школьный” метод выигрывает за счет своей простоты.
Впрочем, можно возводить матрицу в степень, пользуясь алгоритмом умножения элементов из упражнения4.3. Тогда оценка будет 0(п*^) (log23 = 1,585), и очень большие числа Фибоначчи (начиная приблизительно с F50000> 1О10 450) будут считаться быстрее, чем по школьному” алгоритму...
122
ГЛАВА 4
4.5.	Нули в конце факториала
Задача 4.8. Заданы натуральные числа N и р (оба в диапазоне от 2'до 10’).
Найти количество нулей в конце числа № (А факториал), записанного в системе счисления с основанием р.
Пример. Вход: 7 10. Выход: 1. (7! записывается в десятичной системе как
5040, в конце этой записи один нуль.)
Анализ задачи. Нетрудно заметить, что число NI не только не поместится в обычных целых типах, но и не поместится в разумном объеме памяти при использовании длинной арифметики. Очевидно также, что ни от приблизительной, ни от модулярной арифметики в задаче нет никакой пользы. Так что следует искать какую-то закономерность, характерную именно для этой задачи.
Значения факториалов чисел от 1 до 10 запишем в таблицу. В первой паре столбцов укажем N и его разложение на множители, во второй паре — запись А! в двоичной системе и количество нулей в этой записи (в скобках), в третьей и четвертой — то же самое для десятичной и двенадцатеричной систем.
N		р=2	р=10	р = 12
1	1	1 (0)	1 (0)	1 (0)
2	2	Ю (1)	2 (0)	2 (0)
3	3	110 (1)	6 (0)	6 (0)
4	22	11000 (3)	24 (0)	20 (1)
5	5	1111000 (3)	120 (1)	АО (1)
6	2-3	1011010000 (4)	720 (1)	500 (2)
7	7	1001110110000 (4)	5040 (1)	2800 (2)
8	23	1001110110000000 (7)	40320 (1)	1В400 (2)
9	Зг	1011000100110000000 (7)	362880 (1)	165000 (3)
10	25	1101110101111100000000 (8)	3628800 (2)	1270000 (4)
Рассмотрим двоичную систему. Сначала нулей не было вообще, при умножении на 2 добавился один нуль, на 4=22— два нуля, на 6=2-3 — один, на 8=23 — три, на 10=2-5 — один.
Видим, что при умножении на число, не содержащее множителя 2, количество нулей в конце двоичной записи не меняется; при умножении на число, содержащее 2*, количество нулей увеличивается на к. Эту закономерность нетрудно доказать и строго (используя единственность разложения на простые множители).
На основании этого наблюдения несложно написать фрагмент программы, решающий задачу при р=2:
к := 2; NF_2 := 0;	(4.9)
while к <= N do begin
NF_2 := NF_2 + N div k;
к := k*2;
end;
Объясним его. Здесь N — число Айз входных данных, в переменной NF_2 строится ответ (количество нулей), переменная к используется как вспомогательная. Под
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
123
счет количества нулей происходит не в том порядке, как они появляются при пошаговом вычислении факториала, а довольно хитрым и более эффективным образом. Сначала, при к = 2, учитывается, что все четные множители (количество которых N div 2) дают хотя бы один нуль. Потом, при к = 4 — что все множители, кратные четырем, дают хотя бы два нуля; но один из этих нулей уже был посчитан, поскольку кратное четырем число уже было учтено как четное, поэтому к NF_2 прибавляется просто количество множителей, кратных четырем (Ndiv4). И так далее. Процесс продолжается, пока не окажется, что k > N, т.е. настолько больших степеней двойки среди множителей нет.
Итак, с двоикной системой разобрались. Но, продолжив рассмотрение таблицы, видим, что для десятичной системой ситуация несколько сложнее. А именно, нули в конце ЛП появляются при умножениях не только на числа, кратные 10, а и на кратные 5.
Понять, при чем тут число 5, несложно. 10=2-5; поэтому, если в предыдущем значении произведения уже есть “свободный” множитель 2, то умножение на 5 приводит к появлению новой пары множителей (2; 5) и, следовательно, нового нуля в конце произведения.
Это наблюдение нетрудно обобщить. Если основание системы р разлагается на простые множители как р=рк' -pf2-... рткт, то подсчитаем для каждого из простых де-тителей pt, р2, ••,ря (отдельно) количество таких множителей в №. (NF(p) по аналогии с NF_2 в фрагменте 4.9). Для появления одного нуля в конце числа нужны множителей р{, к2 — р2 и т.д. Окончательный ответ (количество нулей в конце IV!) определяется как
min{WF(pi) divк\, NF(p2) div Л2.NF(p^ divim).
На основании именно этой формулы работает следующая программа. В ней использованы типы Dword и QWord — 4- и 8-байтовые беззнаковые целые, реализуемые языком системы программирования Free Pascal.
Листинг 4.7. Решение задачи 4.8_______________________________________
▼ar N, р, р_, q, k : QWord;
primes : array[1..16] of record
val_, (* значение простого множителя (p_i) *) num_in_p : DWord; (* степень множителя (k_i) *) num_in_fact : QWord (* количество этих множителей в N!
(NF(p_i)) *)
end;
i, N_prime : integer;
N_zeroes : QWord; (* окончательный ответ *)
BEGIN
read(N, p) ;
p_ := p; (* при разложении на множители р_ будет уменьшаться, *) N_prime := 0;	(* количество различных простых множителей
в разложении *)
q := 2;	(* число, на которое пробуем делить при
разложении *)
while р_ > 1 do begin
if р_ mod q = 0 then begin
(* q -- новый делитель p *)
124
ГЛАВА 4
inc(N_prime);
primes[N_prime].val_ := q; (* записываем его в список множителей *)
primes[N_prime].num_in_p := 0;
repeat (* сразу ищем степень этого множителя *) р_ s= р_ div q;
inc(primes[N_prime].num_in_p);
until p_ mod q <> 0;
end;
inc(q);	(* переходим к следующему q; если q*q>p_,
то p_ простое, *)
if q*q > p_ then q := p_;
(* но его тоже нужно занести в список *)
end;
for i := 1 to Njprime do begin
primes[i].num_in_fact := 0;
k := primes[i].val_;
(* считаем NF(p_i) *)
while k <= N do begin
inc primes[i].num_in_fact, N div k);
k := k*primes[i].val_;
end;
(* и ищем минимум NF(p_i)/k_i *)
q := primes[i].num_in_fact div primes[i].num_in_p;
if (i = 1 or (q < N_zeroes)
then N_zeroes :=1q;
(* первое выполнение c i = 1 использует "ленивое" вычисление or *)
end;
writein(N_zeroes);
END.
* Можно обойтись без массива primes, объединив разложение на множители, вычисление NF(pj) и поиск min(NF(p/) div к/}.
В заключение предостережем от одной ложной идеи, которая может прийти в голову при анализе задачи для десятичной системы. Поскольку 2<5, при последовательном вычислении 1-2-3-... всегда будут “свободные” множители 2, и никогда — “свободные” множители 5. Так что для десятичной системы “критическим” простым множителем является 5, и для решения задачи при р=10 можно просто заменить в фрагменте (4.9) оба вхождения 2 на 5. Но идея искать конкретный “критический” простой множитель для произвольного возможного основания р не проходит. Например, при р=12 количество нулей попеременно определяется то количеством множителей 2, то множителей 3.
Упражнения
4.1.	В первой строке записано число а от 1 до 1000, во второй — число Ь от 1 до 1О1000. Вычислить остаток от деления b на а.
НЕСТАНДАРТНАЯ ОБРАБОТКА ЧИСЕЛ
125
4.2.	Найдите компилятор, позволяющий возвращать сложные структуры как результаты функций, и реализуйте арифметические подпрограммы в виде функций. Обязательно проверьте (хотя бы экспериментально), не приводит ли ваша реализация к “утечке памяти”, когда память выделяется, но не освобождается (при этом может заканчиваться память или очень сильно разрастаться файл подкачки).
4.3.	Известны асимптотически более эффективные алгоритмы умножения, чем с квадратичной оценкой. Основная идея одного из них такова: если есть два числа А] и А2, состоящие из 2к цифр каждое, их можно представить как A=<tBfCt (d— основание системы счисления, В, и С,— числа, состоящие из к цифр каждое). Тогда
АрА2 =	+ <^’(В\С2 + В2С1) + С1С2.
При “лобовом” подходе нужно выполнить четыре умножения чисел длины к. Но вместо этого можно посчитать В^СХ, В2+С2, потом три произведения B' B2, С,С2 и (Bj+QMBj+Cj), а недостающее выражение В]С2+В2С[ получить как разность (В|+С|) (В2+С2)-В1В2-С1С2. Уменьшение количества рекурсивных вызовов существенно сокращает общее количество действий, в данном случае — от О(п) до O(nk*23) (log23 = 1,585).
Реализуйте этот алгоритм умножения и убедитесь, что для очень больших (например, больше 1О1000) чисел он действительно работает быстрее, чем обычное умножение в столбик.
4	4. Реализуйте вычисление НОД длинных чисел.
4	5. Числовые множества Ао, Ар А2,... образованы следующим образом: Ао = [0; 1 ], А1 состоит из отрезков [0; 1/3] и [2/3; 1], т.е. из первой и последней трети отрезка Ао, А2 — из первой и последней трети каждого отрезка в А], и т.д. Точки, принадлежащие всем множествам Ао, Ар А2, ..., образуют множество А_Р Нужно определить, принадлежит ли точка т/п множеству Ак, где к>-1. Например, точка 1/6 принадлежит Ар но не принадлежит А2 и всем остальным, а точка 3/10 принадлежит Ак при любом к>0, т.е. принадлежит А_,. Вход. Три целых числа т, п, к, где 0< т< п< 108, -1 < к< 106. Выход. 1 или 0 — точка принадлежит Ак или нет.
4	6. Есть п гирь с массами 1, 3,9,..., З"-1 г. На левую чашку весов кладут предмет с заданной целой положительной массой т и произвольную комбинацию гирь, на правую — только гири. Найти подмножества гирь на левой и правой чашках, при которых достигается равновесие, если это возможно.
Вход. В первой строке текста — количество гирь п, во второй — число т.
12п 20, т типа longint;
1 <n<300, m< 1О100.
Выход. Если равновесие невозможно, вывести в текст-1. Иначе в первой строке текста — т и возрастающие массы гирь на левой чашке, во второй строке — возрастающие массы гирь на правой чашке (все числа через пробел).
126
ГЛАВА 4
4.7.	Числовое множество А строится следующим образом: его элементами являются заданные различные натуральные числа а и Ь; если х и у — его элементы, то х+у+ху — также его элемент, других чисел в нем нет. Определить, принадлежит ли заданное длинное натуральное число с множеству А.
4.8.	В Стране Дураков сравнивают целые положительные числа кит следующим образом. Сначала вычисляют к"" и т, затем находят окончательные суммы цифр этих чисел (если сумма цифр числа больше или равна 10, находят сумму цифр этой суммы цифр, и т.д., пока не получат сумму цифр меньше 10) Большим считается то число из к и т, степень которого имеет большую окончательную сумму цифр. Например, 2<5, поскольку 2s=32 с окончательной суммой цифр 5, а 52 = 25 с суммой?. Аналогично 4>5, а 2=4. Помогите Дуракам.
Вход. В первой строке — количество тестов, каждый тест — пара чисел типа longint в отдельной строке.
Выход. Последовательность знаков <, = или > (первое число меньше, равно или больше второго по указанному способу сравнения).
4.9.	Задано целое число N (0<N<2147483 647). Вывести наименьшее положительное целое число, произведение цифр которого равно N, или 0, если такого числа нет. Например, при N=0 это число 10, при W= 5 — 5, при N= 21 — 37, при №11 — 0.
4.10.	По заданному к найти какое-нибудь Л-значное десятичное число, которое состоит из цифр 1 и 2 и без остатка делится на 2* (если существует). Вход. Число к, 1 < к< 1000. Выход. Строка с fc-значным числом или 0 (если числа нет).
4.11.	Найти наименьшее натуральное число, которое при переносе младшей десятичной цифры N в старший разряд и сдвиге остальных цифр вправо увеличивается в Nраз (если при #= 2,..., 9 такие числа вообще существуют).
4.12.	Бильярдное поле в плане имеет целочисленные размеры К в ширину (по горизонтали) и М в длину (по вертикали). Начало координат находится в левом нижнем (юго-западном) углу поля. В точке поля с целочисленными коорАи-натами (х; у) находится шар. От удара кием шар начинает двигаться в северо-восточном направлении (под углом 45° к горизонтали). Он проделывает путь длиной L 42 , возможно, несколько раз отражаясь от бортов и гарантированно не попадая в лузы. Вычислить координаты шара в конце пути. Вход. Три целых числа К, М, Lb диапазоне от 1 до 1О100. Выход. Два целых числа х, у.
4.13.	По заданной сумме S определить, можно ли оплатить ее, использовав ровно N монет достоинством 1,3, или 5 копеек.
4.14.	Последовательность строк Ао, Ар ..., состоящих из 0 и 1, образуется следующим образом: Ао = 1; Ал+1 получается из Ап заменой каждой 1 на 01 и каждого 0 на 10. По заданным пит найти т-ю цифру Ап, где 0 £ и £ 31, 0 < т < 2"-1.
Глава 5
Бинарный поиск, слияние и сортировка
В этой главе...
•	Бинарный поиск в упорядоченном массиве и его применение в задачах
•	Слияние участков упорядоченного массива. Слияние упорядоченных последовательностей в файлах
•	Наиболее популярные алгоритмы сортировки массивов и их использование
5.1.	Бинарный поиск
5.1.1.	Идея бинарного поиска
Когда нам нужно найти слово в словаре, мы раскрываем словарь приблизительно посередине. Если слово должно быть в словаре дальше, ищем его только во второй половине словаря. Середина словаря становится началом, и мы раскрываем его на середине второй половины. Аналогично, если слово должно быть в первой половине, ищем только в ней. Каждый раз, заглядывая в словарь, мы делим “пространство поиска” пополам, уменьшая его приблизительно вдвое. Такой поиск называется дихотомическим, или двоичным (бинарным).
Рассмотрим бинарный поиск заданного значения в упорядоченном массиве. ПустьА[0] < А[1] <...<А[п—1], т.е. массив А отсортирован по неубыванию. Сначала пространство поиска — это элементы с индексами от low = 0 до up=n -1. Индекс середины равен (low+up) div2. Если элемент с этим индексом равен ключу, поиск успешно завершен, иначе изменяется или верхняя граница (up •. = i-1), или нижняя low : = i+1). Поиск прекращается, если нужный элемент найден или пространство поиска исчерпалось (листинг 5.1).
Листинг 5.1. Бинарный поиск по ключу
function BinSearch(const А : alnt; n : integer; key : T) : integer;
(* alnt - тип массива целых; нижний индекс 0 *)
var i,	(текущий индекс}
up, low : integer; {верхняя и нижняя границы поиска} begin
low := 0; up := n-1;
i := (low+up) div 2;
128
ГЛАВА
while (low <= up) do begin
{пространство поиска не исчерпано}
if A[i] = key then break {нашли} else
if A[i] > key
then up := i-1 {key слева от A[i]}
else low := i+1; {key справа от A[i]}
i := (low+up) div 2
end;
{или low > up, или A[i] = key}
if low <= up then BinSearch := i
else BinSearch := -1 end;
Однократное выполнение тела цикла while требует постоянного количества элементарных действий независимо от значений переменных A, key, i, low и up поэтому общее количество действий прямо пропорционально числу повторений те ла цикла while. При каждом повторении разность up- low уменьшается, как минимум, вдвое. Сначала up-low= n-1, поэтому тело цикла выполняется не более че log2n раз и время выполнения фуйкции thiI1= O(log2n). Благодаря этому двоичный поиск еще называется логарифмическим1.
* Идея бинарного поиска очевидна, но неаккуратная ее реализация легко приводит к подпрограмме, которая только кажется правильной. Например, если в операторе up i-i забыть “-1”, то на некоторых входных данных выполнение подпрограммы зациклится.
5.1.2.	“Оптический танк”
Задача 5.1. Танк должен выехать с базы, пересечь пустынную и болотистую местность и прибыть на пост. Препятствий по пути нет, танк может двигаться в любом направлении. Известно, что прямая, соединяющая базу и пост, проходит по обеим территориям. Определите путь, по которому танк приедет с базы на пост быстрее всего.
Вход. Первая строка текста содержит два числа — скорости танка по пустыне и болоту (м/с). Вторая строка содержит координаты базы, третья — координаты поста. Известно, что ось Ох разделяет пустынную и болотистую территории (пустынная наверху, болотистая внизу), координата у базы положительна, поста — отрицательна. Все числа действительные.
Выход. Вывести два числа: абсциссу координаты места, в котором танк пересекает границу пустынной и болотистой местности, и время движения от базы до поста. Оба числа выводить как дробные, с возможной ошибкой не больше 10~5.
Пример
Вход 3 5 Выход 15.7465
20 10	1
8 -9	5.99728
Анализ задачи. Рассмотрим схему пересечения территорий (рис. 5.1). Нетрудно заметить, что картинка может быть или именно такой, или отраженной слева напра
1 Логарифм по основанию 2 обычно обозначают, не указывая основания — log л.
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
129
во. Вычисления проведем согласно рисунку, а поправку на возможную отражен-ность введем позже.
Рис. 5.1. Схема пересечения территории
Видим, что у, и у2 непосредственно заданы во входных данных, a DX можно определить как Ixp-xJ. Неизвестна лишь величина х.
Вычислим суммарное время движения танка по пустынной и по болотистой местности:
t(x) = t\(x) + Г2(х) = jy2+(DX-x')1 /vi + jy2+x2 !v2	(5-0
(расстояние s вычислено по теореме Пифагора, время — как s/v). Итак, остается найти значение х— точку минимума функции (5.1). Докажем, что точка минимума только одна.
► Рассмотрим слагаемое t/x) = ^у2 +х2 /v, для упрощения опустив нижний индекс при у и v. Найдем его вторую производную:
Видим, что она строго положительна при любом х. Функция г, (х) с помощью замены x-DX=w приводится к такому же виду ^у2 + w2 /у, поэтому ^"(х) тоже всегда строго положительна. Отсюда и строго положительна, т.е. первая производная t'(x) монотонно и строго возрастает, поэтому /(х) не может иметь различных точек локального минимума. <
Отметим, что элементы высшей математики использованы здесь только для доказательства корректности; для написания программы они не нужны.
Итак, выражение (5.1) имеет одну точку минимума, но при чем здесь бинарный поиск? Ведь значения функции t(x) не образуют монотонной последовательности и искать нужно не заданное значение, а оптимум... Однако можно применить модификацию бинарного поиска.
130
ГЛАВА 5
Дело в том, что значения выражения (5.1) имеют единственный минимум. Значит, проверяя “пробное” значение хр нужно посмотреть, убывает или возрастает t(x). Для этого достаточно взять некоторое малое е (поскольку требуемая точность ответа 10~5, разумно взять е=10”*) и посмотреть, какое из значений больше — z(x.-e или t(xt+z). Если г(х.-е) < ?(х.+е), то t(x) возрастает в точке х=х, и оптимум находится слева; если г(х -е) > г(х.+е), то оптимум справа; если г(х.-е) = t(x+£.), то точка xi сама будет оптимумом (возможно, не абсолютно точным, но с достаточной точностью).
Разумеется, не стоит предполагать, что при любых входных данных вычисления приведут к ?(х-е) = г(х,+е), поэтому нужны два условия выхода из бинарного поиска — достигнуто это равенство или ширина области поиска меньше требуемой точности.
В данной задаче можно использовать и обычный бинарный поиск, применив его к первой производной выражения (5.1). При анализе первой производной на каждом шаге бинарного поиска нужно вычислять одно значение t’(x) (а не два значения г(х;-е) и г(х;.+е)), но выражение для производной сложнее, чем выражение t(x).
В обоих способах происходит потеря точности: при анализе производной — когда складываются (близкие к противоположным) г/(х) и t2'(x), при анализе самой г(х) — когда сравниваются близкие значения ?(х(-е) и г(х.+е). Так что разница между этими решениями несущественна, хотя описанная модификация бинарного поиска применима и к функциям, от которых нельзя взять производную.
И последнее. Задача “Оптический танк” эквивалентна задаче преломления света на границе двух однородных сред — свет распространяется именно по “быстрейшему” пути. Впрочем, эта аналогия не дает нового способа решения, поскольку закон преломления света sina/sinP = v1/v2 эквивалентен уже известному уравнению “первая производная функции (5.1) равна нулю”.
Н Реализуйте представленное решение (оба способа). Проведите сравнительные эксперименты с точностью и ее влиянием на скорость решения.
5.2.	Слияние упорядоченных последовательностей
5.2.1.	Слияние двух участков массива
Идея слияния проста. Представим себе, как две колонны учеников, выстроенных по росту, перестраиваются в одну. Ученики, первые в своих колоннах, сравниваются, и более рослый становится последним в новую колонну, а другой остается первым в своей. Так они действуют, пока одна из колонн не исчерпается — тогда остаток другой добавляется к новой колонне.
Начнем с простейшей ситуации — сливаются два упорядоченных по неубыванию соседних участка с заданными границами в числовом массиве (роль более высокого ученика играет меньшее число). Пусть это участки в массиве А с индексами 1..т (и длиной т-/+1) и с индексами т+l..r (длиной r-m). Например, длина следующих участков равна т-1+1=3 и r-m=3.
| 1	| 3	| 13	| 2	| 5	| 19	|
I	т	т+1	г
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
131
Они объединяются в такой участок длиной r-1+l =6 во вспомогательном массиве В.
| 1	I 2	| 3	| 5	| 13	| 19	|
Z	т	т+1	г
Количество перемещаемых элементов известно заранее, поэтому запрограммируем слияние с помощью цикла for. В следующей процедуре смежные участки массива X (участок слева содержит элементы с индексами 1..т, справа — т+l..r) сливаются в участок массива Z с индексами 1..г (листинг 5.2).
Листинг 5.2. Слияние двух упорядоченных участков массива
procedure merge(var X, Z : aT; left, mid, right : integer);
left = 1, mid = m, right = r }
var k : integer; {индекс в целевом массиве}
i,	j : integer; {индексы левой и правой половин}
begin i := left; j := mid+1;
for k := left to right do {заполнение элементов Z [left], ...
Z[right]}
if i > mid { левый участок пройден }
then begin Z [k] := X[j] ; inc(j) end else
if j > right { правый участок пройден }
then begin Z[k] : = X[i]; inc(i) end else
if X[i] < X [j]
then begin Z [k] := X[i]; inc(i) end
else begin Z[k] := X[j]; inc(j) end end;
Тело цикла выполняется r-Z+1 раз и имеет сложность 0(1), поэтому сложность выполнения вызова процедуры merge есть 0(r-Z+l).
5.2.2.	Слияние файлов
Когда упорядоченные последовательности записаны в файлах, их длина, как правило, заранее неизвестна, поэтому их слияние программируется иначе.
Задача 5.2. В тексте записана неубывающая последовательность положительных чисел. Два таких текста нужно слить в один, т.е. получить новый текст, содержащий все исходные числа в порядке неубывания.
Вход. Каждый текст состоит из нескольких строк или является пустым. Числовые константы могут находиться в нескольких строках и внутри строки разделены пробелами.
Выход. Строка текста, в которой константы разделены пробелами.
Пример
Входные тексты Выходной текст
14	1	12347
7	2 3
Решение задачи. Реализуем действия, описанные в первичном алгоритме — последовательности сливаются, пока обе не пусты, а затем оставшийся “хвост” одной аз них прибавляется к результату. С каждым входным текстом свяжем две перемен
132
ГЛАВА 5
ные. Первая из них будет хранить очередное число, прочитанное из текста, вторая — признак того, что последнее число из текста уже скопировано в выходной текст Тогда ложность этого признака является условием того, что сливаемая последовательность не пуста. Заметим, что если входная последовательность пуста, этот признак должен быть истинным.
Каждый раз, когда есть два очередных числа, нужно найти минимальное из них, вывести его в новый текст и попытаться прочитать из соответствующего входного текста следующее число. Если прочитать невозможно, значит, последовательность закончилась.
Реализуем описанные действия процедурой merge2 (листинг 5.3); ее параметры — имена файловых переменных. Отметим, что, в отличие от слияния двух соседних участков массива, длина сливаемых последовательностей заранее неизвестна, поэтому используются циклы while, а не for.
Листинг 5.3. Слияние двух упорядоченных участков массива
procedure merge2(var fl, f2, f3 : text);
{два входных и выходной файлы }
var al, а2 : integer; { очередные считанные числа }
isUsedl, isUsed2 : boolean;
{ признаки того, что последние считанные числа обработаны } begin
reset(fl); reset(f2);
rewrite(f3);
isUsedl := true; istfsed2 := true;
if not eof(fl) then begin
read(fl, al); isUsedl := false
end;
if not eof(f2) then begin
read(f2, a2); isUsed2 := false end;
while not isUsedl and not isUsed2 do begin
if al < a2 then begin
write(f3, 1 1, al) ;
if not eof(fl)
then read(fl, al)
else isUsedl := true ;
end
else begin
write(f3, ' ', a2);
if not eof(f2)
then read(f2, a2)
else isUsed2 := true;
end;
end;
{ isUsedl or isUsed2 }
if isUsedl then
while(not isUsed2) do begin
write(f3, ' 1, a2);
if not eof(f2)
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
133
then read(f2, а2)
else isUsed2 := true ;
end;
i£ isUsed2 then
while(not isUsedl) do begin
write(f3, 4 1, al);
if not eof(fl)
then read(fl, al)
else isUsedl : = true;
end;
close(fl); close(f2); close(f3);
end;
* Между последней константой во входном тексте и его концом не должно быть пробелов, символов табуляции или концов строк. Иначе из-за особенностей чтения текстов в системе Turbo Pascal выходной файл будет содержать одну или две лишние константы О, т.е. результат будет ошибочным.
♦
И Программу, в которой связываются файловые переменные, вызывается процедура Merge2 и закрываются файлы, напишите самостоятельно.
Задача 5.3. Решить задачу 5.2 при условии, что количество входных текстов I задается на клавиатуре и может достигать девяти.	|
Анализ и решение задачи. Как и в задаче 5.2, с каждым текстом свяжем две переменные. В первой будем хранить последнее прочитанное число, во второй — признак того, что последнее число текста уже выведено. Схема наших действий такова.
Из каждого текста прочитать число.
Пока осталось хотя бы одно невыведенное число, выполнять найти минимальное из прочитанных и невыведенных чисел; запомнить, из какого текста оно прочитано;
вывести это число в новый текст,-
из соответствующего входного текста прочитать новое число
Уточним эту схему с учетом условий задачи. Для обработки нескольких текстов естественно использовать массивы. Обозначим максимальное количество текстов через MaxN и используем массивы текстов f, чисел а и признаков окончания последовательности isUsed с индексами [1. .MaxN].
Опишем слияние в процедуре mergeTexts. Ее параметры — массив текстов, их количество и новый текст. Предположим, что исходные тексты хранятся в файлах с именами ordsql.txt, ordsq2.txt и т.д., а новый текст создается в файле ordsq.txt (листинг 5.4).
Листинг 5.4. Слияние числовых последовательностей
program MergeT;
const rnaxN = 9;
type aText = array [l..maxN] of text;
alnt = array [l..maxN] of integer;
aBool = array [1..maxN] of boolean;
▼ar f : aText; res : text; { входные и выходной файлы } n : byte; { количество входных файлов }
134
ГЛАВА 5
procedure init(var f : aText; var n : byte; var res j text);
{ связывание файловых переменных } var i : byte;
begin
assign(res, 'ordsq.txt');
writein(1 задайте количество текстов (1..9)'); readin(n);
for i := 1 to n do
assign(f[i], 'ordsq'+chr(i+ord('01)) + 1.txt1); end;
procedure done; { завершение работы с текстами } begin
close(res);
for i := 1 to n do close(f[ij); end;
procedure mergeTexts(var f : aText; n : byte; var res : text); var a : alnt; { массив последних прочитанных чисел }
isUsed : aBool; {массив признаков того, что последние прочитанные числа уже обработаны }
empty : boolean; { признак того, что все числа обработаны } i, first, { индексы текущего, первого найденного }
m : byte; { и минимального из текущих значений } minVal : integer; { минимальное из текущих значений } begin rewrite(res);
for i := 1 to n do reset (f [i] ) ;
{ первичное чтение значений из текстов } empty := true;
for i := 1 to n do
if not eof(f[i]) then begin read(f [i], a[i]);
isUsed[i] false; empty := false end else isUsed[i] := true;
{ поиск минимального из текущих значений } while not empty do begin
{ поиск первого значения }
first := 1;
empty := true; { значение пока не найдено }
while (first <= n) and empty do if isUsed[first] then inc(first) else empty := false;
if not empty then begin { значение найдено } m := first; minVal := a[m]; for i := first+1 to n do begin if not isUsed [i] a,nd (minVal > a[il) then begin m := i; minVal ;= a[m] end; end;
write(res, 1 ’, a[m]);
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
135
{ пытаемся читать значение из соответствующего текста } if not eof (f [tn])
then read(f[m], a[m])
else isUsed[m] := true;
end;
end;
{ все прочитанные числа выведены } end;
begin
init(f, n, res);
mergeTextslf, n, res);
done ;
end.
♦ Учтите замечание, сопровождающее листинг 5.3.
» Обеспечьте работу с большим числом входных текстов.
5.3. Основные способы сортировки
Большая часть алгоритмов этого раздела использует сравнения и обмены скалярных значений элементов массива. В реальных же задачах часто нужно сортировать массивы не скалярных значений, а структур данных (записей), состоящих из нескольких, вообще говоря, разнотипных полей. Как правило, эти структуры сортируются по значениям одного из полей, которое называется ключевым.
Таким образом, нужно сравнивать значения не элементов массива, а их ключевых полей, но менять местами целые структуры, зачастую большого размера. Обмен значениями такшГ структур означает копирование всех их полей, а это может быть слишком длительным. Сортировку можно ускорить, уменьшая по возможности количество обменов. Другой способ ускорения заключается в создании дополнительного массива индексов или указателей на элементы основного массива структур. Тогда меняются местами не большие структуры, а короткие скалярные значения, указывающие на них.
Однако, чтобы не загромождать изложение, почти везде в данном разделе рассматриваются массивы типа аТ, элементы которых имеют индексы, начиная от О, значения типа Т, допускающего сравнения.
5.3.1.	Два простейших алгоритма
Рассмотрим простейший способ сортировки массива с элементами А [0], А [1], , А [п-1 ]. Последовательно сравниваем А[0] с А [1], А [1] с А [ 2 ] и т.д., меняя естами A [i] и A [i+1], если A [i] >А [i+1]. Тогда А [п-1] получит наибольшее значение. Например, последовательность <3,4,2,1> превратится в <3,2,1,4>. Аналогичным способом переместим второе по величине значение в А [п-2 ], превратив, например, <3,2,1,4> в <2,1,3,4>. Затем третье по величине значение переместим А [п-3] ит.д.
136
ГЛАВА 5
Описанный способ называется пузырьковой сортировкой— если значения эле ментов уподобить размерам пузырьков, то сравнения и обмены похожи на то, как самый большой пузырек всплывает наверх, оттесняя остальных.
Заметим, что если на некотором проходе по массиву обменов не было, то массив уже отсортирован, и дальнейшие проходы не нужны. Для выявления этой ситуации запомним, обменивались ли на проходе значения, и если нет, закончим сортировка (листинг 5.5).
Листинг 5.5. Пузырьковая сортировка
procedure bubbleSort(var А : аТ; n : integer);
{ bubble - пузырек }
{ exchange - процедура обмена значений ее аргументов } var i, k: integer; { текущий индекс и правая граница } var nExchange : boolean; { индикатор обменов на проходе } begin
for k := n-1 downto 1 do begin
nExchange :?= false;
for i := 0 to k-1 do
if A[i] > A[i+1] then begin
nExchange s= true;
exchange(A[i], A[i+1]) end;
if not nExchange then break end
end;
Очевидно, что наибольшее число элементарных действий прямо пропорционально общему числу сравнений, которых в худшем случае (л-1)+ (п-2)+ ...+1 = л(л-1)/2. Поэтому сложность такой сортировки л-элементного массива — 0(л2).
Рассмотрим еще один способ — сортировку выбором. Просмотрим элементы массива от А [0] до А [п-1], найдем элемент с наименьшим значением и это значение поменяем местами сА[0]. Затем выберем наименьшее значение среди А[1], ..., А [п-1] и поменяем его с А [1] ит.д. (листинг 5.6).
Листинг 5.6. Сортировка выбором
procedure selectSort(var А : аТ; n : integer);
{ см. комментарии в предыдущем листинге }
var i, к,	{текущий индекс и левая граница}
irnin : integer; {индекс минимального значения} min : Т;
begin
for k := 0 to n-2 do begin
min := A[k]; imin := k;
for i := k+1 to n-1 do
if A[i] < min then begin
min := A[i]; imin := i end;
if imin > k
then exchange(A[k], A[imin])
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
137
end end;
Число обменов в этой сортировке гарантированно не более чем л-1, но сложность также 0(и2).
Кроме простоты и скорости написания, в приведенных процедурах ничего хорошего нет. При больших п они работают слишком медленно, поэтому на практике применяются другие, существенно более быстрые алгоритмы. Начнем их изучение.
5.3.2.	Сортировка слиянием
Слияние упорядоченных участков максимальной длины. Под упорядоченным участком (отрезком иди серией) будем понимать последовательность неубывающих элементов, которая не является частью другой упорядоченной последовательности. Например, в последовательности значений 2, 4, 6, 1, 3, 5, 3 есть три отрезка: (2,4,6), 1,3,5) и (3). Рассмотрим алгоритм сортировки, использующий слияние отрезков.
Сортировка состоит в повторении шагов слияния пар отрезков. На каждом шаге ищется пара соседних отрезков, которая объединяется в один отрезок вспомогательной последовательности. Затем ищется и объединяется следующая пара и т.д. Возможно, в конце останется участок, не имеющий пары, — он копируется во вспомогательную последовательность без изменений. На следующем шаге проис-одит такое же слияние пар Отрезков вспомогательной последовательности в основную.
Шаги повторяются, пока одна из последовательностей не окажется упорядочен-ой. Если это вспомогательная последовательность, она копируется в основную.
Рассмотрим сортировку последовательности А = <7, 6, 5, 4, 3, 2,1> длиной п=7 с вспомогательной последовательностью В. Отрезки выделим скобками О, пары сливаемых участков разделим знаком |.
А = <7> <б> | <5> <4> | <3> <2> | <1>,
В = <6, 7> <4, 5> | <2, 3> <1>,
А = <4,5, 6, 7> | <1, 2, 3>,
В = <1,2, 3,4, 5, 6, 7>.
Как видим, для сортировки понадобилось три шага слияния. После этого вспомогательная последовательность копируется в основную:
А = <1,2,3,4,5,6,7>.
Предположим, что последовательности хранятся в виде массивов, и оформим описанные действия в виде процедуры sortByMrg (листинг 5.7). Шаг слияния участков одного массива в другой оформим в виде функции mergeStep. Она возвращает признак того, что на шаге слияния была найдена хотя бы одна пара упорядоченных участков. Если пара не найдена, значит, исходный для этой функции массив от-ртирован. На нечетных шагах слияния функция mergeStep выполняет слияние астков основного массива А во вспомогательный массив В, на четных — наоборот.
Пару соседних упорядоченных участков n-элементного массива (первый из них чинается индексом left) ищет функция findPair. Она возвращает признак го, что пара найдена. Правые границы участков сохраняются в ее параметрах mid
138
ГЛАВА 5
и right. Если после ее вызова оказалось, что (lef t = 0) and (right = n-1), значит пара не найдена, т.е. массив отсортирован.
Для слияния используем процедуру merge (см. листинг 5.2 в подразделе 5.2.1) Вспомогательная процедура копирования соруАг очевидца.
Листинг 5.7. Сортировка с помощью слияний
function findPair(var X : аТ; n, left : integer; var mid, right : integer): boolean;
begin findPair := false;
if left > n-1 then exit; mid := left;
while (mid < n-1) and (X[mid] <= X[mid+1]) do inc(mid);
{ mid = n-1 or X [mid] > X[mid+1] } if mid = n-1 then begin right := mid; exit
end;
findPair := true;
right := mid+1;
while (right < n-1) and (X[right] <= X[right+1]) do inc(right);
{ right = n-1 or a[right] > a[right+1] } end;
function mergeStep(var x, у : аТ; n : integer) : boolean; var left, mid, right : integer;
begin mergeStep := true; left := 0;
while findPair(x, n, left, mid, right) do begin merge(x, y, left, mid, right); left := right+1
end;
{ из последнего вызова findPair возвращено false}
if (left = 0) and (right = n-1) then begin mergeStep := false; exit { массив x отсортирован }
end;
if left <= n-1 then copyAr(x, y, left, n-1); end;
procedure sortByMrg(var a : aT; n : integer);
var b : аТ;	{ вспомогательный массив }
step : integer;	{ номер шага }
notSorted : boolean; { признак неотсортированное™ }
begin
step := 0; notSorted := true;
while notSorted do begin inc(step);
if odd(step) then notSorted ;= mergeStep(a, b, n) else notSorted mergeStep(b, a, n)
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
139
end;
{ после слияния один из массивов отсортирован }
if not odd(step) then copyAr(b, a, 0, n-1) end;
Анализ сложности. После первого шага слияния длина упорядоченных участков не меньше 2 (за исключением, возможно, “хвоста” длиной 1), после второго — не меньше 4 (кроме, возможно, “хвоста” меньшей длины) и т.д. После i-ro шага длина упорядоченных участков, кроме, возможно, “хвоста”, не меньше 2'. Пусть п — число элементов массива. На последнем, fc-м, шаге объединяются два участка; первый из них имеет длину*не меньше 2*”1, причем 2*_|<л. Отсюда число шагов fc<logn+l. За счет возможного дополнительного копирования число шагов нужно увеличить на 1, но оценка числа шагов ©(log л) сохранится. На каждом шаге общее число элементарных действий есть ©(л), поэтому сложность алгоритма — 0(л log л).
• В приведенном алгоритме элементы обрабатываются в порядке их расположения в последовательности. Поэтому на практике файлы и связанные списки эффективно сортируют на основе именно алгоритмов слияния.
Рекурсивная бинарная сортировка со слиянием. Рассмотрим следующий алгоритм сортировки, использующий слияние. Массив делится на две части, длина которых отличается не более чем на 1; эти части рекурсивно сортируются и затем сливаются с помощью дополнительного массива. Если длина части уменьшается до 1, происходит возвращение из рекурсии.
Уточним алгоритм в виде процедуры mergeSort_r. Она имеет два параметра-массива (основной и вспомогательный) и два параметра 1 и г, задающих начало и конец сортируемой части основного массива. Вначале вычисляется середина сортируемого участка т. Затем для сортировки половин участка рекурсивно вызывается процедура mergeSort_r. Для их слияния используется процедура merge. После слияния отсортированная часть из вспомогательного массива копируется в основной (листинг 5.8).
Листинг 5.8. Рекурсивная бинарная сортировка со слиянием
procedure mergeSort_r(var А, В : а; 1, г : integer);
var m,	{индекс середины участка}
k : integer; {текущий индекс} begin
if 1 >= г then exit;
m := (1+r) div 2;
mergeSort_r(А, В, 1, m); {сортировка левой половины} mergeSort_r(A, В , m+1, r); {сортировка правой половины} merge(А, В, 1, m, r); {слияние упорядоченных половин} for k s= 1 to r do A[k] := B[k]; {копирование}
end;
Сложность этой простой процедуры также имеет оценку 0(«logn). Если входные данные имеют длинные упорядоченные участки, она их “не замечает”, в отличие от процедуры sortByMrg (см. листинг 5.7), которая в этой ситуации имеет некоторое преимущество.
140
ГЛАВА 5
Сортировки слиянием при большом размере массива п работают существенн быстрее, чем алгоритмы “пузырька” или выбора. Кроме того, в некоторых задачах весьма желательно, чтобы при сортировке элементы с одинаковыми ключами (значениями, по которым происходит упорядочение) не переставлялись один относительно другого. Это свойство сортировки называется устойчивостью. Сортировки слиянием хороши тем, что их очень легко сделать устойчивыми.
Недостаток сортировок слиянием— необходимость дополнительного массива размером л. Алгоритмы, представленные ниже, требуют существенно меньше дополнительной памяти, хотя сделать их устойчивыми весьма проблематично.
Н Напишите рекурсивный вариант сортировки слиянием, избавленный от излишних копирований вспомогательного массива в основной. Основной и вспомогательный массивы должны попеременно (на разных уровнях вложенности рекурсивных вызовов) сливаться один в другой. Указание. В вызове с заголовком mergeSort r (var А, В : аТ; ... должны выполняться рекурсивные вызовы mergeSort_r(В, А, ...). Кроме того, программа вызывает не процедуру mergeSort_r, а вспомогательную процедуру, параметры которой — массив и его длина (количество элементов). В этой процедуре объявляется вспомогательный массив и происходит “внешний” вызов mergeSort_r.
5.3.3.	Быстрая сортировка
Идея быстрой сортировки заключается в следующем. Определенным образом выбирается опорное значение v. Значения элементов массива А обмениваются так, что массив разбивается на две части — левую и правую. Элементы в левой части имеют значения не больше v, а в правой — не меньше. После этого достаточно рекурсивно отсортировать эти две части по отдельности.
Существует простой, но достаточно эффективный способ выбора v в участке массива A [/zm], ..., A[/astJ: v =А[(/?мг+/шт) div2]. Для разбиения используются два индекса —. “левый” left и “правый” right.
Вначале left = first, right=last; далее они двигаются навстречу один другому. При этом значения меньше опорного (“хорошие”) в левой части пропускаются, а на “плохом” движение останавливается. Аналогично после этого в правой части пропускаются значения больше опорного. Если left еще не стал больше right, это означает, что оба они указывают на “плохие” значения. Эти значения меняются местами, а индексы сдвигаются навстречу один другому.
Движение индексов продолжается, пока left не станет больше right. Тогда все элементы от A[/zm] до A[right] имеют значение не больше v, а все от A[left] до A[last] — не меньше. Каждый из этих участков, если его длина больше 1, разбивается и сортируется рекурсивно. Если же участок пуст или состоит из одного элемента, то он уже отсортирован.
• В зависимости от конкретных значений в сортируемом массиве индексы left и right могут “встретиться” далеко от середины массива.
Опишем сортировку части массива А[/гт], ..., A[Zasf] в виде рекурсивной процедуры (листинг 5.9).
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
141
Листинг 5.9. Процедура быстрой сортировки
procedure quicksort(var А : аТ; first, last : integer);
var v : T; {опорное значение}
left, right : integer; {“левый" и “правый" индексы}
begin	,
left := first; right := last;
v := A[(left+right) div 2];
while left <= right do begin
while A[left] < v do inc(left);
while A[right] > v do dec(right);
if left (= right then begin
exchange(A[left], A[right]);
inc(left); dec(right);
end;
end;
{left > right};
if first < right {рекурсивно сортировать левый участок }
then quicksort(A, first, tight);
if left < last {рекурсивно сортировать правый участок }
then quicksort(A, left, last);
end;
Идея быстрой сортировки довольно проста, но неаккуратная ее реализация легко приводит к подпрограмме, которая только кажется правильной. Например, идея “не перебрасывать в другую половину элементы, значение которых равно опорному” на первый взгляд разумна. Для ее воплощения можно заменить строгие неравенства A [left] <vnA[right] >v на нестрогие. Однако это приводит к тому, что индексы “перескакивают” друг через друга и могут выходить за пределы нужной части массива или приводить к совершенно ненужным (неправильным) обменам. Аналогично, попытка увеличивать left и уменьшать right при условии left «right (ане 1е^£г1д11С)можетпривестикзацикливанию.
Анализ сложности. Пусть массив А имеет л элементов. В худшем случае в каждом вызове процедуры quicksort опорным значением является наименьшее на участке от A[/irsf] до А[1ам], и разбиение участка массива длиной т дает участки длиной 1 т-1. Поскольку т=п, л-1, ..., 3, глубина рекурсии достигает 0(л), и при каждом значении т разбиение участка длиной т имеет сложность 0(т). Тогда суммарная сложность имеет оценку 0(л)+ 0(л-1)+ ... + 0(3) = 0(и2).
Однако вероятность описанного худшего случая в реальных данных очень мала. Для подавляющего большинства данных разбиение участка массива дает два участка с приблизительно равными длинами. Поэтому размеры сортируемых массивов при переходе на следующий уровень рекурсии уменьшаются примерно вдвое. Отсюда число уровней рекурсии имеет оценку ©(log л). Очевидно, что на каждом уровне рекурсии суммарная сложность есть О(л), поэтому общая сложность имеет оценку O(nlogn).
Итак, описанный способ сортировки имеет оценку сложности в худшем случае 0(л). Однако средняя по всем возможным упорядочениям длины и сложность ценивается как О(л log л)доказательство можно найти в работах [3,4, 20,22]. Более того, эмпирические исследования (согласно [20]) свидетельствуют, что быстрая сортировка в среднем требует меньше элементарных действий, чем другие алгоритмы сортировки сравнениями со сложностью худшего случая О(п log л).
142
ГЛАВА 5
Для выбора опорного значения существует много способов (работы [3, 4, 22]) В процедуре из листинга 5.9 важно, что в качестве опорного значения выбирается некоторое значение из сортируемого массива, поскольку такой выбор блокирует выход за пределы индексного множества массива без явной проверки индекса. Иногда опорное значение ищут с помощью генераторов случайных чисел, что сводит шансы израсходовать 0(и2) действий практически к нулю.
* В процедуре quicksort вместо каждого вызова процедуры обмена exchange можно написать три соответствующих оператора присваивания. Подобные замены вызовов некоторых простых подпрограмм их телами (с учетом необходимых изменений) практикуются в задачах вычислительного характера и иногда ощутимо ускоряют выполнение программы.
♦ При использовании C++ или Free Pascal можно объявлять подпрограммы с модификатором inline. Вызов такой подпрограммы происходит по “упрощенному” способу, причем заметно быстрее, чем обычный вызов. Однако при этом возможности подпрограммы ограничены например, в ней запрещены рекурсивные вызовы и некоторые виды циклов. В языке Borland Pascal inline-подпрограмм в описанном смысле нет.
5.3.4. Пирамидальная сортировка
Представленная здесь сортировка в среднем медленнее, чем быстрая, но имеет сложность худшего случая O(nlogzi). Кроме того, она использует структуру данных “пирамида”, которая оказывается полезной при решении ряда других задач (подробности — в разделе 7.3, подразделе 9.3.1, разделе 9.4 и упражнении 5.6).
Структуру данных “пирамида” в литературе (особенно в переводах с английского) называют сортирующим деревом или кучей. Эти термины имеют несколько значений, поэтому здесь использоваться не будут.
Расположим элементы массива с индексами 0. . n -1 по строкам, удваивая их количество: в первой строке — первый элемент (с индексом 0), во второй — с индексами 1 и 2, в третьей — с индексами 3-6, дальше — 7-14 и т.д. Последняя строка может оказаться неполной. Например, при количестве элементов п=12 получится следующая пирамида индексов:
О 1	2
3	4	5	6
7	8	9	10	11
Представим пирамиду в виде дерева; оно называется сортирующим, а элемент пирамиды — узлом. От каждого узла£, где &<ndiv2, проведем стрелки к узлам 2£+1 и 2к+2 (рис. 5.2). При четном п от узла ndiv2-1 проводится стрелка только к узлу п-1. Узлы 2к+1 и 2к+2 называются сыновьями узла к, а он — их 'отцом. Таким образом, ветви дерева — это стрелки от родителей к сыновьям.
Каждый узел является вершиной пирамиды, образованной им самим и его потомками. Например, узел 1 — вершина пирамиды из узлов 1, 3,4, 7, 8, 9, 10, узел 2 — пирамиды из узлов 2, 5, 6,11 (см. рис. 5.2). Узел 0 является вершиной всей пирамиды или корнем дерева.
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
143
о
7	8 9	10 11
Рис. 5.2. Пирамида индексов, или сортирующее дерево
Рассмотрим упорядочение значений элементов массива А, при котором значение каждого элемента-отца не меньше значений его сыновей: А[0]> А[1] и А[0]> А[2], А[1]> А[3] и А[1] > А[4], т.е. при каждом k= 1, 2, .... ndiv2-l выполняются следующие неравенства (при четном п элемент А [и div 2-1] имеет только сына А [п-1]).
АИ > А[2к+1], АИ £ А[2к+2]	(5.2)
Рассмотрим пример пирамиды, имеющей свойство (5.2):
30
12	30
12	5	29	2
11	10	3	2	28
Ей соответствует последовательность значений <30,12,30,12,5,29,2,11,10,3,2,28>.
В пирамиде со свойством (5.2) каждый элемент имеет значение, наибольшее  пирамиде, для которой он является вершиной. Например, значение А[1] — наибольшее среди элементов с индексами 1, 3, 4, 7, 8, 9, 10, а значение А[0] — во всей пирамиде.
Перейдем к сортировке. Если поменять местами значения А[0] и А[п-1], то элемент А[п-1] будет иметь наибольшее значение. О нем “можно забыть” и сосредоточиться на сортировке А[0], А[1], ..., А[и-2]. Условие А[0] >А[1], А[0]>А[2] после обмена может оказаться нарушенным. Для его восстановления нужно поменять местами значение А[0] и того из А[1], А[2], значение которого максимально. Пусть это будет А[2]. В примере после обмена значениями А[0] и А[11] на вершине пирамиды будет 28, и А[0]<А[2], т.е. 28<30. После их обмена условие (5.2) будет нарушено  пирамиде с вершиной А [2]. Восстановим его так же, как и для вершины А[0], опустившись при этом к узлуА[5] или А [6] и т.д.
После восстановления условия (5.2) в пирамиде можно поменять местами значения первого и предпоследнего элементов, “забыть” о двух последних элементах, вновь восстановить условие и переместить первое значение в конец и т.д.
Описанные действия называются пирамидальной сортировкой, или сортировкой с помощью дерева.
Реализуем йостроение первоначальной перестановки, удовлетворяющей условию (5.2), в виде процедуры build со следующим заголовком:
procedure build(var А : аТ; n : integer);
144
ГЛАВА 5
Для восстановления условия (5.2) используем процедуру rebuild с таким заголовком:
procedure rebuild(var А : аТ; first, last : integer);
Ее второй и третий параметры задают начало и конец части массива, в начале которого, возможно, нарушено условие (5.2).
С помощью этих процедур реализуем пирамидальную сортировку (листинг 5.10).
Листинг 5.10. Процедура пирамидальной сортировки
procedure treeSort(var А : аТ; n : integer);
var j : integer; {индекс последнего элемента} begin
build(A, n);	{начальная перестановка}
for j := n-1 downto 1 do begin
exchange A[0], A[j]); {"забыть” о максимальном} rebuild A, 0, j-1) {восстановить условие (5.2)} end
end;
Уточним процедуру rebuild, которая восстанавливает свойство (5.2) в части массива А[/], ...» А[/] для произвольных/ и/. Если 2/+25/, нужно восстановить условие (5.2), возможно, нарушенное в начале: А[/]< тах{А[2/+1], А[2/+2]}. При условии А[2/+1]>А[2/+2] положим t=2/+l, иначе k=2f+2. Поменяем местами значения А[/] иА[А:], а затем, если необходимо, аналогичным способом восстановим условие (5.2) в части массива A[fc],..., A[Z], для чего/положим равным к. Если же А[/] > шах{А[2/+1], А[2/+2]}, то в начале части массива условие (5.2) не нарушено, и работа закончена.
Если 2f+2>l, но 2/+1=/ и А[/] <А[2/+1], то поменяем местами А[/] и А[/+1], иначе в части массива А[/],..., А[(] условие (5.2) не может быть нарушено.
procedure rebuild(var А : аТ; first, last : integer);
var k : integer; {индекс большего сына узла f} begin
while 2*first+2 <= last do begin
{найти большего сына узла first} if A[2*first+1] > A[2*first+2] then k := 2*first+l
else k :» 2*first+2;
if A[first] < A[k] then begin {сын больше отца} exchange(A[first], A[k]); {поменяем их местами } first := k {и дальше перестроим пирамиду сына} end
else break; { отец не меньше сыновей - перестройка закончена } end;	»
{ 2*first+2 > last или A[first] >= A[k] }
if J2*first+1 = last then { у A[first] есть один сын }
if A[first] < A[last] then exchange(A[first], A[last]); end;
После выполнения вызова rebuild (A, first, last) при каждом first< tosrdiv2-l элемент A[/m] имеет максимальное значение среди A[/m], A[2/rsr+l],
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
145
A[2-/ir.«+2]. Используем это для построения первоначального массива со свойством (5.2): сначала “восстанавливается” часть массива A[n div 2-1], ..., А[п-1], затем часть A[ndiv2-2],..., А[л-1] ит.д. Это описано в следующей процедуре build: procedure build(var А : аТ; n : integer);
var i : integer;* {индекс начала части массива} begin
for i :» n div 2-1 downto 0 do
rebuild(A, i, n-1) {перестройка части массива} end;
Анализ сложности. Очевидно, что сложность прямо пропорциональна количеству вызовов rebuild. При выполнении build процедура rebuild вызывается ndiv2 раз, а при выполнении цикла for процедуры treesort — еще п-2 раза, т.е. общее количество вызовов процедуры rebuild из других процедур есть0(и). Оценим сложность выполнения rebuild. Каждое выполнение цикла в ней увеличивает значение /не менее чем в 2 раза, а верхняд граница массива I не больше п, поэтому тело цикла выполняется O(logn) раз. Отсюда общая сложность имеет оценку O(nlogn),
5.3.5.	Линейная сортировка подсчетом
Представленные выше алгоритмы сортировки массивов основаны на сравнении и перестановке значений элементов. В некоторых задачах условие позволяет непосредственно по ключевым значениям элементов определять их номера в отсортированной последовательности. На этом основаны способы сортировки, которые не используют сравнений и имеют линейную оценку сложности.
Рассмотрим простейшую ситуацию: нужно отсортировать массив записей А с индексами от 0 до и -1 по значениям ключевого поля key, причем все возможные значения поля от 0 до и -1 встречаются по одному разу2.
Если условия позволяют использовать дополнительный массив В, однотипный с А, то задача решается очень просто:
for i :« 0 to n-1 do
В [A[i].key] := A[i]
Усложним ситуацию. Предположим, что возможные значения ключевых полей в элементах массива А — числа от 0 до К, каждое из которых встречается произвольное количество раз. Предположим также, что условия позволяют использовать дополнительный массив В, однотипный с А, и дополнительный массив С с индексами от 0 до К и целыми неотрицательными значениями не больше п.
Вначале в массиве С подсчитаем количества повторений ключевых значений в элементах А. Затем пройдем по массиву Сив каждом его к-м элементе запомним количество ключевых значений, не превосходящих к. Тогда значение С[£] -1 — это индекс места, которое в отсортированном массиве должен занять последний из элементов А, имеющих ключевое значение к, С[&]-2 — предпоследний из них и т.д.
2 В этом подразделе подчеркнута разница между значениями ключей и элементов, поскольку использование ключевых значений существенно отличается от предыдущих алгоритмов.
146
ГЛАВА 5
Реализуем описанные действия в следующем алгоритме (константа кМах представляет указанное выше максимальное значение ключа X):
{ подсчет количеств повторений ключевых значений }
for к := 0 to кМах do С[к] := 0;
for i := 0 to n-1 do inc(C[A[i]].key);
for к := 1 to kMax do C [k] := C[k-1]+C[k];
{ C[kJ - количество ключевых значений от 0 до k }
for i := n-1 downto 0 do begin
В [C [A [i] .key]-1] :=A[i];
dec (C [A[i] .key] ) end
Этот алгоритм называется сортировкой подсчетом. Очевидно, что его сложность имеет оценку Q(n+K), а при К порядка О(п) — оценку 0(л).
• Сортировка подсчетом сохраняет относительный порядок элементов с одинаковыми ключевыми значениями, т.е. является устойчивой (за счет дополнительного массива, однотипного с исходным).
Наконец, рассмотрим способ сортировки массива “на месте”, т.е. без помощи дополнительного массива, однотипного с исходным. Платой за эту экономию памяти является неустойчивость, т.е. относительный порядок элементов с одинаковыми ключами может измениться.
Для представляемого способа нужны два дополнительных массива — уже знакомый массив С с индексами отО до/Г и массив Sc индексами от 0 до п-1. Значение S[z] показывает, находится ли значение A[i] на “своем” месте в отсортированном массиве.
Вначале вычислим все значения C[fc] — количества ключевых значений, не превосходящих к. Всем элементам S[i] присвоим 0 (ни один элемент массива А не находится на “своем” месте). Затем начнем с А[л-1]: пусть А[л-1].кеу=к. Этот элемент в отсортированном массиве должен иметь индекс ij=C[A:]-l. Но элемент A[i,] имеет некоторое значение V, которое нужно сохранить, прежде чем записывать на его место А[и-1]. По значению V.key с помощью элемента массива CfV.key] найдем “законный” для V индекс i2, аналогично сохраним значение A [i2] и запишем на его место V и т.д.
Каждый раз, поместив V на нужное место i, присвоим S[«] значение 2, указывающее, что A[z] уже получил нужное значение. Кроме того, когда с помощью CfV.key] найдено место для V, С[ V.key] уменьшается на 1, чтобы дальше C[V.key] указывал на место для следующего значения с этим же ключом V.key.
В этом процессе есть одна деталь. Рано или поздно некоторое значение должно быть записано в А[л-1]. Чтобы старое значение A[n-1] “не пошло по второму кругу”, в самом начале присвоим S[n-1] значение 1. Это позволит определить, что произошло возвращение к элементу с индексом л-1 — тогда запишем в А[л-1] новое значение и остановимся.
После этого начнем “следующий круг”. Найдем ближайший к л-1 индекс i, для которого S[i] =0. Начав со значения А [г], пройдем путь, аналогичный пути из А[л-1] в А[л-1]. Так будем действовать, пока не останется элементов S[i]=0.
Уточним описанные действия. Предположим, что действуют следующие объявления типов (RecType — тип записей, в. которых есть ключевое поле с именем key):
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
147
type aRT = array [O..nMax] of RecType;
aC = array [O..kMax] of O..nMax;
aS = array [O..nMax] of byte;
Представленный алгоритм реализован в следующей процедуре (параметр п задает количество используемьрс элементов в массиве А):
procedure sortCnt(var A ; aRT; n : integer);
var C : aC; S : aS; { массивы количеств и признаков }
k, { значение ключа }
i, { старый индекс перемещаемого значения }
idx : integer; { новый индекс }
vln, { значение, размещаемое на новом месте }
vOut : RecType; { сохраняемое значение }
Begin
for k := 0 to kMax do С[к] := 0;
for i ;= 0 to n-1 do inc(C[A[i].key]);
for к := 1 to kMax do C [k] := C[k-1]+C[k];
for i := 0 to n-1 do S[i] := 0;
i := n-1; S [i] := 1; vln := A[i] ;
while i >| -1 do begin
k ;= vln.key;
idx := C[k]-1; dec(C[k]);
vOut := A[idx];
A [idx] : = Vln; S[idx] : = 2;
i := C[vOut.key]-1;
if S[i] = 0
then vln := vOut
else { S[i] = 1 } begin
A[i] := vOut; S[i] := 2;
dec (C[A[i] .key] ) ;
while (i > -1) and (S[i] = 2) do dec(i);
if i > -1 then begin
S[i] := 1; vln := A[i] ;
end;
end;
end; {i = -1}
End;
Оценим сложность описанных действий, предполагая, что К=О(п). Очевидно, что четыре цикла в начале тела процедуры имеют сложность 0(и). При каждом выполнении внешнего цикла while переменная idx получает новое значение, поскольку после присваивания idx: = С [к] -1 значение С [к] уменьшается. Но таких уменьшений для каждого к может быть столько, сколько элементов исходного массива имели ключевое значение к. Но суммарное по всем значениям к количество элементов равно п, поэтому тело внешнего цикла выполняется п раз. Все операторы в этом теле, кроме ветви, связанной с условием S [ i ] =1, имеют константную сложность.
Если S [ i ] = 1, то выполняется внутренний цикл с телом dec (i), уменьшающим значение i. Но в начале каждого выполнения всего цикла, кроме первого, i имеет значение, оставшееся от предыдущего выполнения. Первое выполнение цикла начинается со значения п-1, поэтому общее количество выполнений тела цикла равно п. Отсюда следует и итоговая оценка сложности 0(и).
148
ГЛАВА
5.3.6.	Поразрядная сортировка
Поразрядная сортировка была изобретена в 1920-х годах как побочный результат использования сортирующих машин [20]. Такая машина обрабатывала перфокарты имевшие по 80 колонок. Каждая колонка представляла отдельный символ. В колонке было 12 позиций, и в них для представления того или иного символа пробива отверстия. Цифру от 0 до 9 кодировали одним отверстием в соответствующей по ции (еще две позиции в колонке использовали для кодировки букв).
Запуская машину, оператор закладывал в ее приемное устройство стопку перфокарт и задавал номер колонки на перфокартах. Машина “просматривала” эту колонку на картах и по цифровому значению 0, 1, ..., 9 в ней распредели, (“сортировала”) карты на 10 стопок.
Несколько колонок (разрядов) с закодированными цифрами представляли натуральное число, т.е. номер. Чтобы получить стопку карт, упорядоченных по номерам, оператор действовал так. Вначале он распределял карты на 10 стопок по значению младшем разряде. Эти стопки в порядке возрастания значений в младшем разряде он складывал в одну и повторял процесс, но со следующим разрядом, и т.д. Получи® стопки карт, распределенных по значениям в старшем разряде, оператор складывал их по возрастанию этих значений и получал то, что нужно3.
Рассмотрим пример. Последовательность трехразрядных номеров <733, 877, 323 231, 777, 721, 123> распределяется по младшей цифре на стопки <231,721 <733,323,123>, <877,777>, которые в порядке возрастания последней цифры складываются в одну:
<231,721, 733, 323,123, 877,777>.
На втором шаге номера (обрабатываемые именно в этой последовательности распределяются по второй цифре на стопки <721,323,123>, <231,733>, <877,777> из которых образуется одна:
<721, 323, 123, 231, 733, 877, 777>.
Обратим внимание: перед последним шагом все номера с числом сотен 7 благодаря предыдущим шагам расположены один относительно другого по возрастанию. На последнем шаге номера распределяются по старшей цифре на стопки <123>, <231> <323>, <721,733,777>, <877> и образуется окончательная последовательности <123, 231, 323, 721,733, 777, 877>.
Значения в разрядах номеров заданы цифрами, поэтому поразрядную сортировку еще называют цифровой. Заметим: цифры от 0 до 9 упорядочены по возрастанию, поэтому цифровая сортировка располагает числа в лексикографическом порядке.
Реализуем цифровую сортировку для простой ситуации, в которой многоразрядный номер — это массив чисел 0 до в-1, где В£256, представляющих цифры. Предположим, что номера имеют D разрядов (разряд 0 — старший, D-1 — младший) и представлены в типе Т = array [ 0.. D-1 ] of byte. ПредпсЬюжим, что п номеров записаны в массиве Data типа List = array [0. .nMax-1] of Т. Номера могут повторяться, и нужно выдать их в порядке неубывания.
3 Отсюда глагол “сортировать” (распределять, классифицировать) приобрел смысл “упорядочивать”, хотя сортирующая машина не упорядочивала, а только распределяла карты по значению, закодированному в колонке.
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
149
Как организовать данные для поразрядной сортировки? Сначала на каждом шаге есть общий список номеров; номера распределяются в В списков, соответствующих “цифрам” от 0 до в—1. Эти в списков затем рассматриваются в порядке возрастания “цифр” и сцепляются в один. Списки можно реализовать в свободной памяти, однако рассмотрим другой fпособ.
И исходную последовательность номеров, и формируемые в списков будем хранить с помощью одного и того же дополнительного массива PQnext. Значением PQnext [i] является индекс многоразрядного номера, следующего после Data [i] водном из В списков или в общем списке. Вначале значением PQnext [i] становится i+1, т.е. на первом шаге исходные номера рассматриваются в порядке их расположения в массиве Data. Индекс первого элемента списка запомним в переменной first. Перед первым шагом first = 0.
Далее, каждый k-й шаг начинается с элемента, имеющего индекс i = first. Значение Datafi, к] определяет список (от 0 доВ-1), в который нужно поместить Data [i]. Если это значение первое в списке, i запоминается как индекс и первого, и последнего элемента списка. Иначе i запоминается как индекс следующего за последним элементом этого списка и нового последнего элемента.
Списки формируются с помощью двух массивов pFirst и pLast типа array [О. .В-1} of integer; pFirst [к] указывает на первый элемент списка, соответствующего “цифре” k, pLast — на последний.
Чтобы сформированные непустые списки сцепить в общий список, последнему элементу очередного непустого списка присвоим индекс первого элемента в следующем непустом списке. Кроме того, начало первого непустого списка (а значит, всего общего списка) запомним в first.
Действия одного шага, на котором многоразрядные номера сортируются по цифрам” в k-м разряде, уточнены в следующей процедуре:
procedure sortD(k : byte);
var newL, { номер нового непустого списка }
tempL : byte; { номер текущего непустого списка }
i, nextl : word; { индексы очередного и следующего элемента } begin
{ инициализация списков }
for tempL := 0 to D-l do begin
pFirst[tempL] := n; { n означает, что список пуст }
pLast[tempL] := n;
end;
проход по общему списку и формирование списков }
i := first;
while i <> n do begin
tempL := Datafi, k] ;
nextl ;= PQNext[i];
PQNext[i] := n;
if pFirst[tempL] = n	{ список tempL пуст }
then pFirst[tempL] := i
else PQNext[pLast[tempL]] := i; { или не пуст }
pLast[tempL] := i;
i := nextl;
end;
150
ГЛАВА
{ формирование общего списка }
tempL := 0;
{ ищем первый непустой список }
while (tempL < D) and (pFirst[tempL] = n) do
inc(tempL);
first := pFirst[tempL] ;
while tempL < D-l do begin
newL := tempL + 1;
{ ищем следующий непустой список }
while (newL < D) and (pFirst[newL] = n) do inc(newL);
if newL < D then { следующий непустой список есть, }
PQNext[pLast[tempL]] := pFirst[newL]; { прицепляем его к текущему } tempL := newL;
end;
end;
Для того чтобы упорядочить D-разрядные номера по неубыванию, выполню сортировку по цифрам, начиная с младшего разряда и заканчивая старшим, как 1 следующем цикле:
for k := D-l downto 0 do sortD(k)
После этого выведем номера в соответствии со списком индексов в массиве PQNext Это выполняется следующей процедурой done (процедура outDigs выводит номе| из массива Data, индекс которого задан ее аргументом).
procedure done;
var i : word;
begin
i := first;
repeat
outDigs(i);
i := PQNext[i];
until i = n;
end;
Оценка сложности поразрядной сортировки (количество цифр В считаем константой) очевидна — &(Dn). При D=o(logn) это лучше, чем сложность сортировки, основанной на обменах.
» Реализуйте всю программу.
Н Реализуйте последовательности номеров с помощью связанных списков в свободной памяти.
5.4. Применение сортировки
5.4.1.	Проверка уникальности
Задача 5.4. Есть массив, содержащий набор чисел в произвольном порядке. Нам обещают, что все его значения различны. Написать программу, которая проверяет, так ли это, и если не так, удаляет из набора повторные вхождения чисел.
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
151
Анализ задачи. Очевидный алгоритм требует сравнить каждое число с каждым, так что оценка суммарного количества его действий — О(п2).
Однако, если отсортировать набор эффективным алгоритмом (O(nlogn) действий), то одинаковые значения станут соседними. Очевидно, что провести сравнения соседних значений мо^кно за О(п) действий (разумеется, и этот, и последующие приведенные фрагменты следует применять к уже отсортированному массиву а): all_diff := true;
for i : = 1 to n-1 do
if a[i] = a[1+1] then begin
al^_diff := false; break
end;
Итак, получить ответ “да”/“нет” можно за max{O(nlogn), О(п)} = O(nlogn) действий. Но удастся ли выполнить “сжатие”, т.е. удалить повторные вхождения, не ухудшив этой оценки?
Очевидный подход — каждый раз при нахождении к (к>2) одинаковых значений удалять повторения, сдвигая весь “хвост” массива на £-1 позиций “влево”. Но каждый сдвиг хвоста требует выполнения цикла, т.е. имеет оценку О(п). При особо плохих входных данных (например, все значения появляются по два раза) нужно провести О(п) сдвигов, и суммарное количество действий достигает О(п2).
Рассмотрим подробно, как “сжать” массив с оценкой О(п). Сначала используем дополнительный массив Ь, однотипный с а. Перебираем элементы массива а и, если a[i-l] < а [ 1], то продвигаемся по массиву Ь (увеличиваем j) и копируем значение a [i] в b [ j ]. Если же значение в а повторяется, то продвигаемся дальше по а, не меняя Ь.
Ь[1] :=а[1];
] : = 1 ;
for i := 2 to n do
if a[i] > a[i-l] then begin inc(j) ; b[j] := a [i] ;
end; { j - количество элементов в массиве b }
Однако в действительности массив b вообще не нужен: можно все время копировать из а в сам а, поскольку всегда j<i, т.е. “позиция записи” не обгоняет “позицию чтения”.
5.4.2.	Проход в заборе
Задача 5.4. Участок г-на Чудакова выходит на улицу одной прямолинейной стороной. Г-н Чудаков пожелал отгородить его, но решил, что капитальный забор ему ни к чему, достаточно и отдельных столбиков. Сначала этих столбиков было только два (по краям участка). Потом г-н Чудаков несколько раз убеждался, что такой забор недостаточно надежен, и добавлял к нему новые промежуточные столбики. Найдите самый широкий на данный момент проход в заборе г-на Чудакова.
Вход. В первой строке текста hole. dat записано количество столбиков У (3< У<5000). Каждая из следующих W строк содержит координату столбика —
152
ГЛАВА 5
целое число, которое по модулю не больше 10б. Порядок координат в тексте соответствует тому порядку, в котором г-н Чудаков устанавливал столбики.
Выход. В первой строке текста hole. sol — ширина искомого прохода, во второй — координаты столбиков, ограничивающих этот проход, в третьей — номера этих столбиков (по порядку установки столбиков). Первым вывести столбик с меньшей координатой, вторым — с большей.
Пример Вход 4 Выход	5 5
О	15 70
100	Л 3
70 15
Анализ задачи. Понятно, что от “хронологического порядка” столбиков толку никакого, входные данные приходится считать неупорядоченными. Ясно также, что после сортировки столбиков по возрастанию их координат проходам будут соответствовать соседние элементы. Так что нужно сначала отсортировать, затем найти максимальную разность а [ i+1 ] - а [ i ] —и, казалось бы, все.
Однако в ответе нужны начальные номера столбиков, а они при “обычной” сортировке теряются. Поэтому представим каждый столбик не просто числом, а структурой (записью) с двумя полями: х — координата, idx — начальный номер. Соответственно немного изменим подпрограмму сортировки, например вместо “A[left] <v” будем писать “A [left] . х <. v. х” и т.д.
Окончательно, основная часть решения может иметь вид как в листинге 5.11.
Листинг 5.11. Основная часть решения задачи “Проход в заборе” assign(fv, 'hole.dat'); reset(fv); read(fv, N); for i ;= 1 to N do begin read(fv,a[i].x); a[i].idx := i { при чтении заодно запоминаем начальные номера } end; close(fv);
{ тут вызываем алгоритм эффективной сортировки }
max := а[2].х - а[1].х; max_idx := 1; for i := 2 to N-1 do if a[i+1].x - a[i].x > max then begin max := a[i+l].x - a[i].x; max_idx := i;
end;
assign(fv, 'hole.sol'); rewrite(fv); writein(fv, max);
writeln(fv, a[max_idx].x, ' ', a[max_idx+l].x);
writein(fv, a[max_idx].idx, 1 ’, a[max_idx+l].idx); close(fv);
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
153
5.4.3.	Транзитивность
Задача 5.6. Задан набор пар натуральных чисел (пара (а, Ь) не равна паре (Ь,а)). Проверить, выполняется ли для него свойство транзитивности', если в наборе есть пары (а, Ь) и (Ь, с), то он обязательно содержит и пару (а, с).
Вход. В тексте первая строка содержит количество пар N (3 < 7V< 1000), каждая из следующих N строк содержат по паре положительных чисел типа Longint.
Выход. Если свойство транзитивности выполнено, то выводится строка со словом “Yes”, иначе в первой строке выводится слово “No”, во второй — три числа а, Ь, с, при которых пары (а,Ь) и (Ь,с) есть в наборе, а пары (а, с) нет. Если есть несколько троек а, Ь, с, показывающих нетранзитивность, вывести любую из них.
Примеры
Вход 4 Выход Yes Вход 2 Выход No
2 2	2 3	2 3 2
2 3	3 2
3 5	I
2 5	I
Анализ задачи. Ограничимся наиболее очевидным подходом: перебирать пары а;Ь) и (Ь;с) (первый компонент второй пары равен первому компоненту второй) смотреть, есть ли в наборе пара (а; с).
Приведем два решения, по-разному использующих эту общую идею. Первое из них простое, второе сложнее, но эффективнее.
Простое решение. Условие задачи позволяет запомнить все пары в массиве. Реализуем идею “перебирать пары (а; Ь) и (Ь; с) и смотреть, принадлежит ли набору пара а; с)”. Никаких сортировок, пары записаны в массив в порядке их чтения.
Во внешнем цикле перебираем пары, играющие роль (а; Ь), в среднем (втором по ровню вложенности) — пары, пробуемые на роль (Ь; с). Проверяем, равен ли второй элемент первой пары первому элементу второй пары. Если не равен, переходим следующей паре в среднем цикле. Если равен, во внутреннем цикле проверяем, есть ли в наборе пара (а; с).
Работу можно несколько сократить. Например, найдя в наборе пару (а; с), сразу прервем выполнение внутреннего и среднего циклов, а найдя значения а, b и с, доказывающие нетранзитивность набора, сразу прервем все циклы и выведем результат. Программу написать довольно легко, и во многих ситуациях ее эффективности будет достаточно, хотя сложность худшего случая все равно будет О(№).
Более эффективное решение. Отсортируем набор пар лексикографически ((a,; bt) 5 а?; Ь2), если а, < а2 или а^=а2 и bt <Ь2) и применим бинарный поиск (см. раздел 5.1).
Для каждой пары (а; Ь) нужно найти и исследовать все пары (6; с) с первым компонентом Ь, расположенные в отсортированном массиве подряд. Первую из них найдем с помощью функции бинарного поиска пары (Ь;1), остальные — путем сдвига по массиву. Для текущих пар (а; Ь) и (Ь;с) с помощью бинарного поиска выясним, есть ли в массиве пара (а; с).
Функцию бинарного поиска организуем так, чтобы она вычисляла признак того, что искомая пара есть в массиве (да/нет), а через дополнительный параметр-
154
ГЛАВА 5
переменную возвращала индекс первого элемента, который больше искомого. При выяснении, есть ли в наборе пара (а; с), используется признак наличия, а при поиске первой пары (£; с) — найденный индекс.
Оценим сложность этого алгоритма. Для каждой пары (а;Ь) бинарный поиск первой пары (Ь; с) имеет оценку O(loglV), а общее количество сдвигов по массиву для всех пар (a;b) — O(N). Поиск (а; с) также имеет оценку OflogN). Итак, общая сложность имеет оценку O(Nlog2N).
* Данный алгоритм требует применять сортировку и поиск к более сложным элементам, чем числа. Можно, конечно, взять алгоритм сортировки и алгоритм бинарного поиска и заменить каждое “a [i] <a[j]*Ha
• a[i] [1] <a[j] [1]) or ((a[i] [1] -a[j] [1]) and (a[il [2] <a[j] [2]) )*.
Но лучше (если это возможно, т. е. поддерживается компилятором) перегрузить4 используемые операции сравнения и не трогать хрупкие реализации бинарного поиска и быстрой сортировки. Перегруженные операции можно объявить с модификатором inline.
Упражнения
5.1.	По целому с, 1 £с<210’, найти целое а, при котором максимально.
5.2.	Множество целых чисел типа longint представлено текстом, в котором записаны целые числа, упорядоченные по возрастанию. По двум таким файлам создать третий файл, который также упорядочен и представляет операцию с исходными множествами:
а)	объединение (содержит числа, принадлежащие хотя бы одному из множеств);
б)	пересечение (числа, принадлежащие обоим множествам);
в)	разность (числа, принадлежащие первому множеству, но не второму); г) симметричную разность (объединение разностей множеств).
5.3.	Ученики школы живут в двух домах. Чтобы дойти к школе от первого дома, нужно г, времени, от второго —t2. Известны моменты времени выхода каждого школьника из дома. Написать программу выяснения, в каком порядке они придут в школу.
Вход. Первые строки текстов housel. txt и house2 . txt содержат длительность перехода от дома к школе, следующие строки — неотрицательные моменты выхода, упорядоченные по возрастанию. Конец обозначен числом-1. Выход. Данные о моментах прихода учеников в школу (в порядке возрастания) в тексте school. res. Каждая строка должна иметь вид: момент прихода ученика в школу, номер дома, где он живет, номер, в котором по порядку он выходит из своего дома. Если несколько учеников приходят в школу одновременно, данные выводятся в разных строках (в произвольном порядке).
4 Иными словами, реализовать в виде подпрограмм специальной структуры, чтобы компилятору было понятно, что, например, вместо кода для a[i] < a[j] нужно вызвать соответствующую подпрограмму. Подробности можно найти в справке по соответствующему языку; ключевые слова — перегрузка операций (operator overloading).
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
155
Пример
Входные файлы	Выход
10	15	10 1 1
047	03	14 12
15 2 1
17 1 3
18 2 2
5.4.	Служебный автобус вмещает не более чем М рабочих5. Он отвозит их на завод,
совершая один рейс по установленному маршруту, и, если есть свободные места, подбирает рабочих, ожидающих на остановках. Автобус также может ожидать на остановке еще не пришедших рабочих. Известен момент прихода каждого рабочего на свою остановку и длительность проезда автобуса от каждой остановки до следующей. Автобус приходит на первую остановку в момент времени 0. Длительность посадки рабочих в автобус считается нулевой.
Нужно минимизировать момент прибытия автобуса на завод при условии, что доставлено максимально возможное количество рабочих (М или все, если их количество меньше Л/).
Вход. Входной текст bus. dat содержит: в первой строке — количества остановок N и мест в автобусе Af; каждая из следующих N строк содержит t. — длительность проезда от текущей остановки до следующей (для последней остановки — до завода), затем количество Kt рабочих, приходящих на эту остановку, затем ^моментов прихода рабочих в порядке возрастания.
Выход. Искомое время — одно число в тексте bus . sol.
Ограничения. 1<М<2103, l<N,X,<2105, все моменты времени, включая искомый, помещаются в типе longint. Программа должна помещаться В DOS-овский объем памяти и читать вход однократно; использовать какие-либо дополнительные файлы запрещено.
Пример
Вход 2 5	Выход 19
5 3 1 4 5
7 3 2 8 12
_5. Заданы попарно различные точки на координатной числовой прямой (целые числа типа longint в количестве не больше 5000). Определить все пары точек, расстояние между которыми минимально.
6	. Найти к-& по порядку значение в массиве из п элементов (не сортируя массив полностью).
7	Написать процедуру вычисления N (N< 104) наименьших значений последова-
тельности действительных чисел, которая вводится из файла и имеет неограниченную длину, при условии:
а)	число учитывается столько раз, сколько встречается в файле (но не больше чем N)",
б)	независимо от количества повторений числа учитываются по одному разу.
5 Автор задачи — Павел Аксенов. Предыдущая задача о школе придумана как упрощение иной задачи.
156
ГЛАВА 5
5.8.	Информация о сотруднике фирмы, существующей с 1990 года, включает его идентификационный код и дату поступления на работу (день, месяц и год) В 2029 году руководитель фирмы захотел получить список сотрудников, упорядоченный по возрастанию дат. Помогите составить этот список.
Вход. Текст содержит не больше 5-103 строк. В каждой строке записаны четыре целых числа — код (типа integer), день, месяц (оба типа byte) и год (типа integer). Числа разделены пробелами.
Выход. Выходные данные имеют аналогичный вид, только они упорядочены по годам, в году — по месяцам, в месяце — по дням. Порядок данных с одинаковыми датами роли не играет.
5.9.	Даны две последовательности натуральных чисел а,, а2, ..., ап и Ьх, Ьг, ..., Ья Найти перестановку ip i2,.... in чисел 1, 2, ..., п, при которой достигается минимум суммы a, + а2-Ь,2 + ... + а„ Ьы.
5.10.	Даны а,, а2,..., ал и bv b2, ..., Ья — целые положительные числа, не обязательно различные. Найти перестановку ix, i2, ...,in номеров чисел bx,b2, ..., Ья, при которой величина а1‘*'],+а2д<'2,+...+а*'п} максимальна.
5.11.	Заданы N(N<5000) попарно различных длин отрезков» Вычислить количество способов, которыми из отрезков можно сложить треугольник.
5.12.	М пронумерованных гирь имеют попарно различные массы, которые заданы натуральными числами. Есть слово длины М, составленное из букв L и R. Буква обозначает результат взвешивания на чашечных весах: L — левая чашка тяжелее, R — правая. Построить, если это возможно, последовательность номеров гирь, в которой гири поочередно ставятся на чашки и потом не снимаются, а последовательность результатов взвешиваний соответствует заданному слову.
Вход. В первой строке текста записано количество гирь М (1 < М< 104) в следующих М строках — массы гирь не больше 10б.
5.13.	В каждой строке текста записано слово длиной не больше 30. Строк не больше 1000. Слова в тексте не повторяются и неявно нумеруются в порядке записи в тексте. Некоторые слова образуют анаграммы — их можно получить друг из друга, переставляя буквы. Например, “автор”, “тавро” и “отвар”. Разбить все слова на группы так, чтобы внутри каждой группы все слова были анаграммами, а слова разных групп — не были. Для каждой группы слов вывести их номера в порядке возрастания через пробелы, отделяя группы числом -1 в отдельной строке. Порядок вывода групп значения не имеет.
5.14.	Большое количество слов и словосочетаний русскогр, украинского и беларус-ского языков необходимо отсортировать в лексикографическом (словарном порядке, считая алфавитом следующую последовательность:
АБВ(ГГ)Д(ЕЁе)ЖЗ(ИХ1Й)КЛМНОПРСТ(УУ)ФХЦЧШЩЫ(ЪЬ}ЭЮЯ.
Скобками выделены совокупности букв, которые условно считаются одинаковыми при упорядочении. Например, слова СДИНХСТЬ, ЕДИНСТВЕННОСТЬ ЁЖ и ЕНОТ должны идти именно в таком порядке, поскольку их первые буквы
БИНАРНЫЙ ПОИСК, СЛИЯНИЕ И СОРТИРОВКА
157
считаются одинаковыми, и КС, Д<Ж, Ж<Н; вместе с тем, заменять Е, Ё и С на один и тот же символ нельзя. Аналогично, прописные и строчные буквы в словах тоже считаются равноценными, но не заменяются. Дефисы и апострофы в словах не учитываются, например,
чтобы < что «бы ни случилось, Прип'ять = Припять.
Названия заданы в кодировке Win 1251.
Глава 6
Вычислительная геометрия на плоскости
В этой главе...
•	Представление точек и векторов плоскости. Угол наклона вектора, угол поворота между векторами и ориентированная площадь треугольника, образованного векторами
•	Способы представления прямых и отрезков. Определение, принадлежит ли точка прямой и отрезку. Расстояние от точки до прямой
« Условия параллельности и перпендикулярности прямых. Углы между прямыми
•	Вычисление общих точек отрезков
•	Способы определения, лежит ли точка в треугольнике
♦	Площади треугольников и их пересечений
•	Представление многоугольников (полигонов) и вычисление их площади. Построение полигонов. Сумма и разность полигонов
*	Принадлежность точки полигону и выпуклому полигону
•	Длина отрезка прямой внутри окружности. Общие касательные двух окружностей. Площадь пересечения двух кругов
В некоторых задачах этой главы вид входных и выходных данных сознательно не уточнен, поскольку эти задачи часто являются подзадачами других, более сложных задач, которые и определяют организацию входных и выходных данных.
6.1.	Точки, векторы, прямые, отрезки
6.1.1.	Точки, векторы, углы и ориентированная площадь
Точки плоскости (двумерного пространства) обычно задают в декартовой прямоугольной системе в виде пар координат (х;у). В программах точки обычно представляют таким типом записей:
type TPoint = record
х, у: extended
rad;
160
ГЛАВА!
Расстояние между точками А и В с координатами (х0',у0) и (хрУ,) определяют каа d(A,B) =	+(У1"3'о)1 2 •
Вектор а задают также, как и точку, т.е. парой координат для его представления часто используют тот же тип, что и для точек. По любым двум точкам Р Q с координатами (Рх; Ру) и (бх; Qy) можно построить вектор PQ с началом в Р концом в Q. Координаты такого вектора равны (Q-Pj Qy-Ри-
длина вектора — это расстояние между его началом и концом. Длину вектора будем обозначать 1а. Очевидно, 1а = ^ах + ау .
Произведение числа к на вектор а — это вектор длины |£|х/о с координатами (к ах; к ау). Если говорить о векторе как о направленном отрезке, то при Jt>0 вектор к- а направлен так же, как а, а при к<0 — в противоположную сторону.
Если совместить (перенести в одну и ту же точку плоскости) конец вектора а начало вектора b, то суммой векторов а и Ь будет вектор, который начинается в начале а и заканчивается в конце b . Координаты вектора-суммы равны (ах+Ьх; ау+Ь}
♦ Многие, хотя и не все, геометрические задачи решаются особенно красиво с помощью объектно-ориентированного программирования и/или перегрузки операций (сложение, вычитание, умножение на число, и т. д.). Хорошие примеры такого подхода описаны, например, в [23].
Угол наклона вектора — это угол а между осью Ох и вектором; он отсчитывается от оси Ох против часовой стрелки. Углы, отличающиеся на 2я, 4я и т.д., по сути совпадают, например, и Зл/2, и -я/2 задают направление вертикально вниз. Поэтому углы обычно приводят к диапазону 0<а<2л или к диапазону -п<а<п. В последне ситуации говорят о положительных (0<а<л) и отрицательных (-л<а<0) углах. Вектор а с длиной la и углом наклона а имеет координаты (Zecos a; /esin а).
Для вычисления угла наклона вектора по его координатам удобно использовать функцию arctan2 (dy, dx : extended) : extended. Ее аргументы — координаты вектора (сначала у, потомх!), результат— угол его наклона (от -л доя В Borland Pascal функции arctan2 нет. В некоторых других компиляторах она находится в дополнительном модуле (обычно math), которого может не оказаться в “урезанной” поставке. Поэтому желательно уметь реализовывать “свой” arctan2 используя разветвления и стандартную функцию arctan от одного аргумента.
Угол между векторами а и b определяется при кратчайшем движении от а к b (он отрицателен, если движемся по часовой стрелке, и положителен, если против Если а и b имеют углы наклона а и р, то угол между ними равен р~а.2 С помощью известного тригонометрического тождества	*
1
1 Подразумевается свободный вектор — его начало можно переносить в любую точку плоскости, не изменяя длины и направления, а значит, и координат.
2 Разность P-а. может выйти за пределы диапазона, в котором заданы сами аир. Часто этим можно пренебречь, но в некоторых случаях, когда нужны сами значения углов (а не их sin и cos необходимо приводить результат к нужному диапазону, “поправляя” разность р~а на ±2я.
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
161
cos(P-a) = cosacosP + sinasinP
cos(P-a) выражается через координаты (ах; ау) и (bx; Ьу) и длины la, 1Ь: a b “I- а b	\
cos(P-a) =-------— .	(6-1)
Величина ax bx+ay by называется скалярным произведением векторов с координатами (ax;a7) и (bx',by). Оно равно 0, если (и только если) векторы перпендикулярны, поэтому равенство ах Ьх+ау Ьу=0 является признаком перпендикулярности векторов.
Векторы, угол между которыми равен 0 или л, называются коллинеарными. Если угол равен 0, они называются однонаправленными, а если п — противоположными.
Из тригонометрического тождества
sin(P-a) = sinPcosa - cospsina
легко выразить sin(P-a) через координаты (ох; ау) и (Ьх; Ьу) и длины /а, Z4:
a b -ah	,л.
sin(p-a) =	.	(6-2)
Выражение ахЬ -а Ьх играет весьма важную роль. Если оно отрицательно, угол поворота от первого вектора а ко второму b направлен по часовой стрелке вправо), а если положительно — против (влево). Если оно равно 0, векторы коллинеарны.
Из равенства axby-aybx=laltsiii(^-6d следует, что ах-Ъ -а -Ьх выражает ориентированную площадь параллелограмма, образованного векторами а и b. Она отрицательна, вели кратчайшее движение от вектора а к вектору b направлено вправо, иначе положительна (рис. 6.1). Ориентированная площадь треугольника, образованного векторами (см. рис. 6.1), равна половине площади параллелограмма, т. е. l/2-(xe.yt-ye-xd); ее как определяется так же, как для параллелограмма.
a)S>0
Рис. 6.1. Кратчайшее движение от а к b
а
6)S<0
162
ГЛАВА
Формулу а -Ь - а -Ь иногда называют векторным произведением векторов (а ; а) и (b ; b ), м
* У У *	* У * У ।
это не совсем правильно. Расположим декартовы координаты х и у трехмерного простра* ства в плоскости, содержащей векторы а и Ь . Тогда векторным произведением являета вектор, перпендикулярный этой плоскости и имеющий координаты (0,0, ax-b -a -bj.
Рассмотренные формулы позволяют найти угол между векторами: абсолютную величину его можно вычислить как арккосинус правой части (6.1), знак — как зна ориентированной площади. Но вряд ли этот способ существенно проще, чея “лобовое” определение через разность углов наклона векторов, пусть даже с приво дением кдиапазону -ж а< п.
Результатом поворота вектора а длины 1в с углом наклона а на угол <р являета вектор длины 1а с углом наклона а+ф.
Итак, если вектор задан длиной и углом, выразить поворот очень просто. А есш вектор задан координатами? Выразим координаты повернутого вектора через коор динаты заданного:
а,	= /a cos(a+<p) = /a(cos acos <р - sin asin (p) =
= /acos acos <p - /asin asin ф = axcos ф — aysin ф. Аналогично
ay^ = Za(sin acos ф + cos asin ф) = a?cos ф + axsin ф.
Таким образом, из “тригонометрических операций” нужны только совф и втф.
Отдельно рассмотрим важный частный случай — поворот на +л/2. Формулы приобретают вид х/“”;=-уа; у.6*"'*»*,.
« "Если координаты входных точек — целые числа от о до юооо, то они должны быть типа integer". Такие рассуждения — типичная ошибка, ведь результаты операций с координатами далеко не всегда целые. Например, расстояние между точками. Так что внимательно анализируйте все вычисления или представляйте точки и векторы в типе "с большим запасом”.
• Числа в действительных типах представлены приближенно, и это может существенно влиять на результаты. Если важные решения принимаются в зависимости от результатов сравнений, чаще всего, проверок на равенство-неравенство, то алгоритм может оказаться ненадежным.
« Сложения, умножения и вычитания сами по себе увеличивают погрешность в представлении чисел незначительно, но вместе с делениями и извлечениями корней, и особенно, с тригонометрическими функциями могут приводить к заметным погрешностям.
6.1.2.	Представление прямых и отрезков
Способы представления прямой. Существует несколько способов аналитического представления прямой на плоскости. В одних задачах удобны одни представления,! в других — другие. Рассмотрим три из них: общее уравнение, уравнение с угловым коэффициентом, система с направляющим вектором.
Уравнение вида
Ах + By + С = О,
где А и В не равны одновременно 0, называется общим уравнением. Чтобы задать конкретную прямую на плоскости общим уравнением, нужна тройка коэффициентов
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
163
(А,В,С). Если А=0, то прямая горизонтальна, если В=0, вертикальна, если оба не равны 0, наклонна.
Одной и той же прямой могут соответствовать разные тройки (А, В, С), поскольку при умножении коэффициентов А, В и С на одно и то же ненулевое число получается уравнение, задающее туЪке самую прямую. Поэтому среди всех возможных общих уравнений иногда выделяют так называемое нормированное, удовлетворяющее условиям Va2 +В2 = 1 и А>0.
Представление прямой в задачах обычно приходится строить по двум точкам, через которые она проходит. По заданным точкам (х0;у0) и (Хру^ и произвольной точке прямой (х; у) с помощью пропорции (x-x^/iy-y^ = (x^-x^/iy^-y) получается уравнение
х-(уо-У1) + У (ч-х0) + (хоу 1-Х1у0) = 0.
Если прямая не вертикальна, т.е. х0#хр можно разделить общее уравнение почленно Haxj-xo и прийти к уравнению с угловым коэффициентом'.
Г
X] Хо	Х1 Хд
В уравнении вида y=kx+d угловой коэффициент к выражает тангенс угла наклона прямой к горизонтали, смещение d — значение у при х=0. Угол наклона а отсчитывается от оси Ох; диапазоном значений а можно считать либо 0< а<л (исключая /2), либо -тс/2< ас л/2.
Как видим, представление прямой уравнением с угловым коэффициентом требует всего двух действительных чисел. Это экономно и не позволяет представить одну  ту же прямую разными парами чисел. Тем не менее, такие уравнения используются редко, поскольку этим способом нельзя представить вертикальные прямые, а также на это представление сильнее всего влияют погрешности.
Прямую можно представить системой с направляющим вектором. Задаются точка (^Уд) на прямой и ненулевой вектор (ах;ау), параллельный прямой. Система уравнений х=хъ+ах1, y=yo+ay-t
задает координаты точек, образующих прямую. Точки получаются, когда в уравнения подставляется одно и то же конкретное значение параметра t — действительное число от —до +°°.
В частности, если прямая задана двумя точками (х0;у0) и (хру^, то одну из них, например (х0;у0), можно рассмотреть как начальную, а вектор (х^х^у-уд) — как направляющий. Тогда система примет вид
х=х0+ (xi-xo)-r, у=уо+ (У1—Уо)-Г.
Чтобы задать конкретную прямую, нужны четыре числа — координаты начальной точки и вектора. Это неэкономно и допускает бесконечно много представлений мной и той же прямой. Однако именно этот способ представления прямой нередко оказывается самым удобным.
Способы представления отрезка. Рассмотрим два способа. Первый, наиболее популярный — отрезок задан координатами его концов (х0;у0) и (Xpyj).
164
ГЛАВА 6
Второй способ — заданы координаты одного из концов отрезка (х0;у0) и вектор (ах;ау) = (х,-х0; зу-Уд), идущий из этого конца отрезка в противоположный. Координаты точек отрезка, как и в системе с направляющим вектором для прямой, можн выразить с помощью параметра г:
x=xo + ax t, y=y0 + Oyt.
Только теперь он принимает значения в отрезке [0; 1]. Например, при t=0 получаем точку (х0;у0), при /= 1 — точку (х,;у,), при /=0.5 — середину отрезка.
Некоторые свойства представлений. Рассмотрим общее уравнение прямой, считая известной некоторую (“начальную”) ее точку (х0;у0). Перепишем его как
0=Ах+Ву + С = А(х-хо+хо) + Я(у-уо+Уо) + С = = (Ахо + Вуо + Q + А(х—хо) + В(у—уо).
Точка (х0; у0) принадлежит прямой, поэтому первая скобка в последнем выражении равна 0. Но если и первая скобка, и сумма равны 0, то для любой точки (х;у), принадлежащей прямой, справедливо тождество
А(х-хо) + В(у-уо) = 0.
Левая часть тождества представляет собой скалярное произведение векторов (А; В) и (х-х0;у-у0); начало и конец второго из них принадлежат рассматриваемой прямой. Отсюда получаем следующие выводы.
•	Вектор с координатами (А; В) перпендикулярен прямой, заданной общим уравнением Ах+ Ву+ С= 0.
•	Если прямая задана общим уравнением Ах+Ву+С=0 и для нее нужно получить систему с направляющим вектором, то в качестве вектора можно взять (-В; А); остается только подобрать начальную точку.
•	Если прямая задана системой с направляющим вектором (ах; аД то в общем уравнении этой прямой в качестве коэффициентов можно взять А=-ау и В=ах; остается подобрать С так, чтобы прямая проходила через начальную точку.
6.1.3. Взаимное расположение прямых, отрезков и точек
Принадлежность точки прямой. Пусть даны точка А с координатами (Ах; Ау) и прямая. Нужно выяснить, лежит ли эта точка на этой прямой, дав ответ “да” или “нет”.
Если прямая задана общим уравнением или уравнением с угловым коэффициентом, достаточно подставить координаты Ах и Ау вместо переменных х и у, и посмотреть, выполнилось ли равенство.	,
Если прямая задана начальной точкой (х0;у0) и направляющим вектором (bx;b то рассмотрим два вектора — направляющий (Ьх; Ьу) и вектор (А-х0;А-у0) из начальной точки прямой в проверяемую. Они коллинеарны тогда и только тогда, когда проверяемая точка лежит на прямой. Следовательно, условие принадлежности точки прямой можно записать как Ьх(А-у0) - Ь-(А-х^=0.
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
165
Расположение точки относительно прямой. Эта задача является обобщением предыдущей: если точка лежит на прямой, нужен тот же ответ “принадлежит”, иначе нужно выяснить, по какую сторону и на каком расстоянии от прямой она находится. Решим эту задачу с помощью расстояния от точки до прямой.
Вообще, расстояние между протяженными объектами определяют как длину кратчайшего возможного отрезка, их соединяющего. В частности, расстоянием от точки до прямой является длина перпендикуляра, опущенного из точки на прямую.
Прямая задана точкой (х0;у0) на ней и ненулевым направляющим вектором (bjby). Определить, по какую сторону от прямой находится точка (Ах;Ау), можно по знаку значения ориентированной площади bx (A-у0)- b -(A-xJ. Если оно равно 0 — точка принадлежит прямой; если положительно — точка находится слева от прямой, отрицательно — справа. “Слева” и “справа” тут имеют смысл “если смотреть вдоль прямой так, чтобы направляющий вектор был направлен вперед, то точка окажется слева или справа”. Если, например, угол наклона направляющего вектора равен -л/4, то слева от прямой” означает “в правой-верхней полуплоскости” (рис. 6.2).
Рис. 6.2. Расположение точки относительно прямой
Площадь треугольника, образованного векторами (bjby) и (А^-у0;Ах-х0), можно вычислить также по формуле “половина основания на высоту”. Основанием является направляющий вектор, высотой — исследуемый перпендикуляр (рис. 6.3). Отсю-за расстояние от точки до прямой равно
*1(Ау-у0)-^(Ах-х0) я =------—, - --------.
нак этого выражения определяется числителем; следовательно, значение h несет в себе и расстояние от точки до прямой, м направление. Поэтому его еще называют ориентированным расстоянием.
Рис. 6.3. Два способа вычисления площади треугольника
166
ГЛАВ1
Прямая задана общим уравнением. Ориентированное расстояние от точки (ХрУ,) прямой Ал + Ву+ С=0 равно
g _ Ах, + Ву} + С у/а2+В2
Доказательство этой формулы и выяснение, какой стороне от прямой какой знак о ответствует, опустим.
Прямая задана уравнением y=kx+d. Пусть (ХрУ,) — координаты точки. Точка с к ординатами (х,;Ах,+</) принадлежит прямой. Значит, если yl>kxl+d, точка (x,;j выше прямой, если меньше — ниже. Разность у,- (kxl+d) выражает “ориентирова] ное расстояние по вертикали”; а “обычное” ориентированное расстояние (по nq пендикуляру к прямой) выражается как
g_ у,-(Ах,+</) у/к2+1
Я Докажите эту формулу самостоятельно.
Как видим, понятие “по какую сторону от прямой” для разных способов пред ставления прямой выражается по-разному. Тем не менее, прямая всегда разбивав плоскость на две полуплоскости, поэтому для любых двух точек, не лежащих на этсй прямой, осмысленно говорить по одну и ту же сторону и по разные стороны.	;
Итак, если ориентированные расстояния от точек до прямой одного знака, точки ц одну сторону от прямой, если разных знаков — по разные стороны. Чтобы проверить, чп числа dl и d2 одного знака, можно использовать условие (dl>0) and (d2 >0) 01 (dl<0) and(d2<0) или условие dl*d2 > 0.
Каким должен быть ответ, если хотя бы одна из точек лежит на прямой, зависит от задачи. Например, можно условно отнести саму прямую к одной (и только одной] из полуплоскостей — тогда условие “по одну сторону” принимает вид
(dl >= 0) and (d2 >= 0) or (dl < 0) and (d2 < 0), а “по разные стороны” —
(dl >= 0) and (d2 < 0) or (dl < 0) and (d2 >= 0).
Иногда предполагают, что возможны три ответа: “точки строго по одну сторону* (dl*d2>0), “строго по разные стороны” (dl*d2<0) и “определиться нельзя* (dl*d2 = 0).
* Во всех полученных формулах ориентированного расстояния есть знаменатели, необходимые для правильного числового ответа, но не влияющие на знак. Поэтому, чтобы только узнать, находятся ли точки по одну сторону от прямой, знаменатели можно не вычислять.
Принадлежность точки отрезку. Предположим, задан отрезок с концами в точках Р и б с координатами (Рх',Ру) и (Qx; Qy)', нужно определить, принадлежит ли ему точка А с координатами (Ах;Ау). Сначала проверим, принадлежит ли точка прямой PQ, т. е. рав
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
167
на ли нулю ориентированная площадь, определяемая векторами PQ и РА. Если это так, то нужно еще проверить, что А находится между Р и Q.
Поскольку принадлежность точки прямой PQ установлена, достаточно убедиться, что координата Ах находится между Рх и Qx. Но не известно, что из них больше, поэтому придется проверить громоздкое условие
(Р.х<=А.х) and (A.x<=Q.X) or (Q.x<=A.x) and (A.x<=P.x).
Кроме того, если Px= Qx, то понадобится еще аналогичное условие по у-координате.
Другой способ рснован на том, что А находится между Р и Q тогда и только тогда, когда направления векторов AQ и АР противоположны, следовательно, их скалярное произведение отрицательно. Еще нужно учесть, что А может совпадать с Р или Q такая точка тоже принадлежит отрезку). Но это просто: вместо отрицательности AQ  АР проверим его неположительность.
Расстояние от точки до отрезка. По общему определению расстояния между протяженными объектами, приведенному выше, расстоянием от точки до отрезка может оказаться или длина перпендикуляра к отрезку (если он попадает на отрезок, а не на его продолжение), или расстояние от точки до ближайшего конца отрезка. Основание перпендикуляра попадает на отрезок, если и только если ни один из углов ZAPQ и ZAQP не тупой, т.е. оба скалярные произведения QAQP и PA PQ неотрицательны.
м Остальные детали алгоритма доработайте самостоятельно.
Условия параллельности и перпендикулярности прямых. Предположим, что уравнения с угловыми коэффициентами имеют вид y=klx+dl и y=k2x+d2, общие уравнения — Alx+Bly+Cl=0 и А^+В^у+С^О, а в задании с помощью направляющего вектора одна прямая имеет начальную точку (х01;у01) и направляющий вектор (ах,а^, другая — точку (х02; у02) и вектор (bx, by). С помощью этих представлений выразим соотношения между прямыми в следующей таблице.
	Представление		
Соотношение	С угловым коэффициентом	Общее уравнение	С направляющим вектором
Параллельны или совпадают	к=к2	A^Bj-AyB^O	
Совпадают (дополнение к предыдущему условию)	dK — d2	Aj-Cj-AjC^O	*01) ~ 0
Перпендикулярны	= 		A/Ao+ В^В^О	<hbr+a„b„=0
Приведем краткие доказательства этих формул.
► Системы уравнений с направляющим вектором. Условия параллельности и перпендикулярности это просто соответствующие условия для направляющих векторов. Условие совпадения выражает, принадлежит ли первой прямой начальная точка второй прямой.
168
ГЛАВА 6
Общие уравнения. Условия перпендикулярности и параллельности следуют из того, что:
—	они совпадают с условиями перпендикулярности и параллельности векторов (АрД) и (А2;В2), — эти векторы перпендикулярны рассматриваемым прямым;
—	угол между перпендикулярами к двум прямым на плоскости равен углу между самими прямыми.
Условие совпадения прямых (А(В2- А2Я1 = 0) и (Aj-C2- А2С,=О) эквивалентно условию пропорциональности коэффициентов = = но позволяет избежать опасности деления на нуль.
Уравнения с угловыми коэффициентами. Все следует из того, что угловой коэффициент является тангенсом угла наклона. Если k^k^, то углы наклона двух прямых совпадают. При этом, если dl=d1 (прямые проходят через одну и ту же точку на оси Оу), то прямые совпадают, а если Д *<1? то параллельны и не совпадают. Наконец, условие перпендикулярности следует из того, что tg(-| — а) = , tg(-a)=-tg а, а значения углов наклона заключены между -ТО2 и я/2. <
Угол между двумя прямыми. Пересекаясь, две прямые образуют две пары вертикальных углов. Если значения углов одной пары составляют <р, то значения второй пары — к-ф (угол рассматривается здесь в “чисто геометрическом” смысле, а не “тригонометрическом”). Углом между двумя прямыми будем считать меньшее из значений <р и л-ф. Такой угол находится между 0 (прямые совпадают) и л/2 (прямые перпендикулярны).
Формулы для вычисления угла между прямыми связаны со способом представления прямых. Если прямые заданы с помощью направляющих векторов, общих уравнений и угловых коэффициентов, то формулы имеют вид соответственно
arcsin		, arcsin	- ад-яд	, arctg	&2 Д
	1Л		/а,2+в2/а^+в22		1 + к^к2
Чтобы получить ориентированный угол между прямыми (знак указывает, в каком направлении нужно повернуть первую прямую, чтобы она стала параллельной второй), в приведенных формулах достаточно убрать модули. При этом для общих уравнений должно соблюдаться дополнительное условие (А, >0) и (Д2>0).
►> Докажите приведенные утверждения самостоятельно.
Точка пересечения прямых. Установить факт пересечения прямых на плоскости очень просто: прямые пересекаются, если они не параллельны, а условия параллельности приведены выше. Так что сразу предположим, что прямые не параллельны, и перейдем к поиску точки их пересечения. Напомним: если точка пересечения принадлежит обеим линиям, то ее координаты являются решением системы уравнений, задающих линии.
Если прямые заданы уравнениями с угловым коэффициентом, то из системы
у = Дх + й?,, у = k2x+d2
следует уравнение Дх+Д=и его решение х0=	. Чтобы вычислить у-координату
точки пересечения, подставим х0 в уравнение любой из двух прямых.
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
169
Для общих уравнений нужно решить систему
Дх+Bjj + Cj =0, ад + В2у+ С2 =0.
Убедитесь (возможно, с*помощью учебника по линейной алгебре), что ее решение ас,-ад ас,-ад п л „ п
имеет вид х=	---—L, у= —— -----—. Заметим, что А.В,-А,В, #0, поскольку
0 ад-ад 0 ад-ад	1221’
равенство AlB2-A2Bi=0 является как раз условием параллельности прямых.
Для представления с направляющим вектором не столь очевидно, какую систему решать. Для начала запишем ее в виде
'х = х01+ад У = У01+а/1’ Х = Хо2+*Л, У = Уи+^2-
Из неизвестных х, у, tt и t2 нас интересуют только х и у. Вопреки идее “исключить ненужные переменные”, разумной в большинстве ситуаций, приравняем правые части первого и третьего, а также второго и четвертого уравнений:
=Л02+^2-
Уо1 +V1 =	+ЬУ*2-
Эта система очевидно преобразуется к системе
ах?1 ~ ЬхЧ + (*01 ~ 'то)=
а/1-6/2 + (Ую-Уо1) = О,
аналогичной рассмотренной выше системе общих уравнений и решаемой по тем же формулам. Впрочем, для поиска точки пересечения прямых достаточно найти только и, подставив в первые два уравнения начальной системы, найти х и у.
Общие точки отрезков. Если отрезки заданы точкой и вектором, то система для поиска точки их пересечения имеет такой же вид, как и для прямых с направляющим вектором. Здесь нужно найти и tt, и tv а также проверить, принадлежат ли они отрезку [0; 1].
Указанную систему можно решать, если отрезки принадлежат пересекающимся прямым. Но если прямые совпадают, система становится неопределенной. Если прямые горизонтальны или вертикальны, можно вспомнить задачу 2.3. Но что делать, если они наклонны?
Рассмотрим способ проверки, имеют ли отрезки произвольных прямых общие точки (без поиска самих точек). Пусть концами одного отрезка являются точки Р2 и Ср другого — точки Р2 и Q2.
Если точки Р2 и Q2 не принадлежат прямой PQ и лежат по разные ее стороны и если точки Рх и Q} так же расположены относительно Р2б2, то отрезки пересекаются. Если обе точки хотя бы одной из этих пар лежат строго по одну сторону от “чужой” прямой, то отрезки не пересекаются. Иначе для каждой из четырех точек проверим,
170
ГЛАВА 6
принадлежит ли она “чужому” отрезку (т.е. Р, — отрезку Р2б2 и т.д.). Если хотя бы одна принадлежит, то отрезки пересекаются, иначе нет.
6.1.4.	Две задачи о треугольниках
Задача 6.1. По координатам точки Р и трех вершин треугольника А,, Л2 и А3 определить, лежит ли точка в треугольнике.
Анализ задачи. Рассмотрим два способа решения задачи.
Первый способ — проверка полуплоскостей. Если хотя бы одна из сторон треугольника “разводит” противолежащую ей вершину и точку по (строго) разным полуплоскостям, точка лежит вне треугольника. Иначе, если точка принадлежит хотя бы одной из прямых, содержащих стороны треугольника, то она находится на границе треугольника. Если и этого не произошло, точка лежит внутри треугольника.
В реализации описанных действий некоторые из ориентированных площадей используются дважды, поэтому целесообразно вычислить их один раз и запомнить в дополнительных переменных.
» Реализуйте все приведенные рассуждения в виде программы или подпрограммы.
Второй способ— проверка площадей. Если сумма площадей (причем не ориентированных, а “обычных”) АРА^, &PAfi3 и APAjA, больше площади АА^^, точка лежит вне треугольника. Если же сумма первых трех площадей равна четвертой, то проверим, не равна ли нулю одна из первых трех площадей. Если это так, точка лежит на границе треугольника, иначе — внутри.
Сравним приведенные способы. Второй опирается только на школьный курс геометрии: площади треугольников можно считать по формуле Герона л]р(р-а)(р-Ь)(р-с). Но только этим он и хорош. Первый способ лучше и тем, что получается более короткий текст программы, и тем, что эта программа быстрее выполняется, и тем, что в нем гораздо меньше риск принятия неверного решения из-за влияния погрешностей.
►> Предположим, что ориентированная площадь произвольного ААВС вычисляется как площадь треугольника, образованного векторами АВ и АС (именно в таком порядке). Докажите, что сумма ориентированных площадей АРА{А2, АРА2А3 и АРА3А1 независимо от расположения точки Р равна ориентированной площади AAjAjAj.
Задача 6.2. У двух прямоугольных треугольников на плоскости катеты параллельны осям координат, а прямой угол размещен внизу слева. Найти площадь пересечения треугольников.
Вход. Каждый треугольник задается четырьмя числами: х^, у^,
Все координаты целые, по модулю не больше 10‘. Первая строка текста содержит данные об одном треугольнике, вторая — о другом.
Выход. Одно число (с шестью знаками после десятичной точки).
Пример
Вход 0 0 4 3 Выход 0.166667
2 15 2
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
171
Анализ задачи. Обозначим входные данные как x_min_l, y_min_l, x_max_l, y_max_l, x_min_2, y_min_2, x_max_2, y_max_2.
Очевидно, что левее max_x_min= max{x_min_l, x_min_2} и ниже max_y_min= max{y_min_l, y_min_2) точек пересечения треугольников нет. Поэтому сразу отсечем “лишние” части треугольников, размещенные левее max_x_min и ниже max_y_min. Отсечение левой части треугольника представлено на рис. 6.4 — ЛАВС заменяется ЛА'ВС.
Рис. 6.4. Отсечение части треугольника левее max_x_min
Обозначим диапазон координат текущего (для которого проводим отсечение) треугольника как x_min, y_min, x_max, y_max. Координаты точки С очевидны — max_x_min, y_rnin); х-координата точки А' также равна max_x_mxn, а ее
у-координату найдем из подобия МВС-ДА ВС; |А'С|= |АС| |СВ|/|СВ|, поэтому
(А1)^ y_min+ (y_max-y_min) х (x_max-max_x_min)/(x_max-x_min).
Перед отсечением по этим формулам проверим, не пуст ли результат отсечения, т.е. max_x_min>x_max. В этой ситуации условно будем считать, что вершины А’, В и С находятся в одной точке с координатами (max_x_min, y_min).
Нижняя часть треугольника отсекается аналогично — для этого целесообразно вызвать ту же процедуру, переставив местами х- и у-координаты.
Итак, произвольная допустимая по условию пара прямоугольных треугольников искусственно сведена к паре прямоугольных треугольников с общим прямым углом. Для еще большего удобства перенесем в эту точку начало координат. Тогда треугольники полностью задаются длинами своих катетов хр ур х2 и у2. Кроме того, ниже будем считать, что горизонтальный катет первого треугольника не длиннее горизонтального катета второго, т.е. х, <х2. Это предположение не приводит к утрате общности: если это не так, поменяем местами значения х, и х2 (обеспечив х, < х2), а также у, и у2. При этих обменах треугольники меняются местами, что не влияет на результат.
Итак, возможны только две ситуации — если у^Ур то весь первый треугольник находится внутри второго, иначе гипотенузы треугольников пересекаются (рис. 6.5). В первой ситуации площадь пересечения очевидна: 0.5-Xj-y,.
172
ГЛАВА в
Рис. 6.5. Две ситуации, к которым сводятся все остальные
Рассмотрим вторую ситуацию. Найдем точку пересечения гипотенуз (сх,с} “Скорости уменьшения высот” равны kl=yi/xi и к2=у2/х2, поэтому для того, чтобы разница высот (у,~у2) стала равна.0, нужно сместиться по оси Ох на (у-у^/^-к^ вправо. Смещение по вертикали (для второго треугольника) составляет kyly-y^/tk-k^ вниз от у2.3 Итак, сх= (угу^Д*,-*,), с=у2-к2(у -у2)/(кгк2).
Чтобы вычислить площадь пересечения, разобьем его на две части отрезком (сх, су)—(сх, 0). Слева от этого отрезка находится прямоугольная трапеция с вертикальными основаниями у2 и ус, горизонтальной высотой сх и площадью О.б ^+с,) ^. Справа — прямоугольный треугольник с катетами (хх-с) и су и площадью
Программа, реализующая описанное решение, представлена в листинге 6.1. Отсечение левой (или нижней) части треугольника задает процедура CutLef t. Роли переменных и фрагментов текста очевидны из решения, поэтому комментарии не приведены.
Листинг 6.1. Площадь пересечения треугольников
program TrianglesIntersection; var fv : text;
x_min_l, y_min_l, x_max_l, y_max_l, x_min_2, y_min_2, x_max_2, y_max_2, max_x_min, max_y_min, x__1, у___1, x__2, у__2,
t, x_cross, y_cross, kl, k2, S : extended;
procedure CutLeft(var x_min, y_min, xmax, y_max : extended; const max_x_min : extended);
begin
if x_min < max_x_min then begin
if xjnax <= max_x_min then begin
x_min := max_x_min;
x_max : = max_x_min ;
y_max := y_min;
end
else begin
3 Конечно, cx и cy можно вычислить и формально, решив систему уравнений, но приведенная аналогия нагляднее.
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
173
у_тпах := y_min + (у_тах - y_min)*
(х_тах - max_x_min) / (х_тах - x_tnin) ; x_min := max_x_min;
end end; end;
begin
assign(fv, 1 triangle.dat'); reset(fv);
readln(fv, x_min_l, y_min_l, x_max_l, y_max_l); readln(fv, x_min_2, y_min_2, x_tnax_2, y_max__2) ; close(fv);
if x__min_l > x_min_2 then max_x_min := x_min_l else max_x_min := x_min_2;
if y_min_l > y_min_2 then max_y_min := y_min_l else max_y_min := y_min_2;
CutLeft(x_min_l, y_min_l, x_max_l, y_max_l, max_x min); CutLeft(y_min_l, x_min_l, y^max_l, x_max_l, max_y"min); CutLeft(x_min_2, y_min_2, x_max_2, y_max_2, max_x^min); CutLeft(y_min_2, x_min_2, y_max_2, x_max_2, max_/jnin);
x__1 := x_max_l - max_x_min;
у__1 := y_max_l - max_y_min;
x__2 := x_max_2 - max_x_tnin;
у__2 := y_max_2 - max_y_min;
if x__1 > x___2 then begin
t := X 1; X 1 := X 2; x 2 := t;
t := у 1; у 1 := у 2; у 2 := t; end;
if у__1 <= у__2 then S := 0.5*x___l*y__1
else begin
kl := у 1/x 1;
k2 := у 2/x 2;
x_cross := (y__1 - у___2)/(kl-k2);
y_cross := у___1 - kl*x_cross;
S := 0.5*x__cross*(y___2 + y_cross) +
0.5*(x__1 - x_cross)*y_cross
end; assign(fv, 'triangle.sol1); rewrite(fv); writeln(fv, S:0:6); close(fv) end.
6.2.	Многоугольники (полигоны)
6.2.1.	Основные определения
Многоугольником (он же полигон)4 будем называть либо замкнутую ломаную, либо часть плоскости, ограниченную такой ломаной (что именно, будет понятно из контекста). Термины вершина,ребро (или сторона), граница и внутренность полигона
4 Чаще будем употреблять слово “полигон”, поскольку оно короче.
174
ГЛАВА 6
считаем общеизвестными. Будем рассматривать исключительно простые полигоны — у них в каждой вершине сходятся в точности две соседние стороны и других общих точек у сторон нет, т.е. они образованы несамопересекающимися ломаными.
На рис. 6.6, а и 6.6, б изображены два полигона. Фигура на рис 6.6, в не является простым полигоном и вообще не считается полигоном. Полигон на рис. 6.6, а является выпуклым. Выпуклые полигоны характеризуются тем, что при последовательном продвижении от вершины к вершине (его называют обходом) либо все повороты неположительны, либо все неотрицательны5. В отличие от школьного курса геометрии, если сказано “полигон”, то он обязательно простой, но не обязательно выпуклый.
а)	б)	в)
Рис. 6.6. Примеры полигонов
Казалось бы, для представления полигона идеально подходит массив вершин Однако полигон является замкнутым (после “последней” N-й вершины следует первая), а непосредственное представление вершин в элементах массива с индексами 1.. N эту замкнутость не отражает.
В некоторых задачах элемент с индексом 0 делают копией N-ro элемента. Например, в цикле
for i := 1 to N do обработать ребро (a[i—l], a[i])
будут обработаны все ребра. Но существуют алгоритмы, использующие тройки последовательных вершин, и Тогда нужны две копии. А некоторым алгоритмам и этого мало...
Другой способ — запоминать вершины в элементах с 0-го по (ы-1)-й у обращаться не к i-му элементу, а к (imodN)-My. Например, цикл, аналогичный предыдущему, может выглядеть следующим образом.
for i := 0 to N—1 do обработать ребро (a[i], a[(i+l) mod N]) .
* Учитывая свойства операции mod, вместо (l-i) modN приходится писать (i+N-i) modN.
Иногда проблему решают кардинально, представляя полигоны не массивами, а “закольцованными” одно- или двусвязными списками6. Но, по нашему мнению, обычно все-таки проще использовать массивы.
6.2.2.	Площадь полигона
Задача 6.3. Полигон задан последовательностью вершин. Вычислить его площадь.
5 Неположительность или неотрицательность означают, что некоторые углы полигона могут быть развернутыми, т.е. равными л.
6 Это одна из динамических структур данных, обычно реализуемых в свободной памяти с помощью указателей.
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
175
Вход. Последовательность пар действительных координат вершин, гарантированно образующая простой полигон. Признак окончания ввода — повторение координат первой вершины.
Выход. Положительное действительное число — площадь полигона.
Пример. Вход: 0 0 1 0 1 1 0 1 0 0; выход: 1.
Анализ задачи. Площадь полигона найдем как модуль его ориентированной площади, которую вычислим одним из двух способов — как сумму ориентированных площадей треугольников или как сумму ориентированных площадей трапеций.
Сумма ориентированных площадей треугольников. Если в выпуклом полигоне провести отрезки из какой-Либо вершины (например, А^ во все остальные, то получатся п-2 треугольника АдА Д, AyljAj,..., АД_Д_р сумма площадей которых как раз равна площади всего полигона. Йо оказывается, при использовании ориентированных площадей (ррплощадей) треугольников это равенство выполняется для любого простого полигона, а не только выпуклого. Например, на рис. 6.7 заштрихованная в полоску область учитывается сначала как часть орплощади ДАДА3 со знаком а затем как часть орплощади ДАдАД со знаком “+” и в результате не учитывается (что и нужно). Аналогично, область, заштрихованная в клеточку, прибавляется дважды (как часть ДАдА,А2 и ДАдА^ и вычитается один раз (как часть ДА ДА3) и в результате учитывается один раз.
Рис. 6.7. Площадь как сумма треугольников
Итак, вводим координаты вершин Ао, Ар А2, А3, ... и складываем ориентированные площади треугольников А ДА2, АдАД,... — пока не повторится вершина Ао.
Сумма ориентированных площадей трапеций. Вначале определим ориентированную площадь “трапеции”, ограниченной отрезком АВ, его проекцией CD на ось Ох и перпендикулярами АС и BD как (хв-хя) (ул+ул)/2 (рис. 6.8). Площадь положительна, если хЙ>хл и ул+Уд>0 или хв<хл и уА+ув<0, иначе отрицательна или равна 0. В частности, если отрезок вертикален (хв~хА) или ул=-у,, площадь равна 0.
Рис. 6.8. Трапеция и ее ориентированная площадь
176
ГЛАВА
Нетрудно убедиться, что ориентированная площадь полигона АдАр.А^-Д рав^ сумме ориентированных площадей “трапеций” АдАДД,, АДВД, .... А^АдВдВ (рис. 6.9). Заметим, что она положительна, если полигон обходится по часов« стрелке, и отрицательна, если против.
Рис. 6.9. Площадь как сумма трапеций
Итак, площадь простого полигона можно вычислить по следующей формуле:

(6.3J
где (xt,y) — координаты i-й вершины полигона в порядке обхода, п — количество) вершин (n-я вершина совпадает с начальной).
• Вычисления по каждой из указанных формул можно проводить по мере чтения йхода, т. е. организовать алгоритм как однопроходный.
6.2.3.	Принадлежность точки полигону
Задача 6.4. На плоскости заданы Полигон (не обязательно выпуклый) и точка. Нужно определить, находится ли точка вне полигона, внутри или на границе.
Анализ задачи. Рассмотрим два подхода к решению — подсчет точек пересечения луча со сторонами полигона и вычисление суммы углов.
Первый способ— подсчет точек пересечения. Чтобы выяснить, лежит ли точка Р с координатами (Рх; Р^ внутри полигона, можно пустить луч из Р, например горизонтально влево, и подсчитать количество пересечений этого луча со сторонами АдАр А,А2, ..., А(|_2Ая_р А^Зд полигона. Если количество пересечений нечетно, точка лежит внутри, а если четно (в том числе 0) — снаружи.
Проверка пересечения стороны с горизонтальным лучом может состоять, например, в том, чтобы:
•	проверить, попадает ли Ру в диапазон у-координат, покрываемых стороной;
•	найти х-координату пересечения стороны с горизонтальной прямой у=Р, и сравнить ее с Рх (формула для х-координаты строится из соображений пропорциональности координат).
При анализе очередной стороны АД^ возможны особые ситуации:
•	точка попадает в вершину или на сторону полигона — тогда закончим процесс с соответствующим результатом;
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
177
•	луч проходит через вершину А(+1. Если At и А/+2 лежат по разные стороны луча, пересечение есть, иначе нет;
•	луч накладывается на сторону А Д+|. Если Ап и А/+2 лежат по разные стороны луча, пересечение есть, иначе нет.
♦	Напомним: при обращениях к Ам> Ам и Ан-2 нужно учитывать “зацикленность" полигона.
Участники олимпиад часто используют следующее упрощение описанного метода. Вместо горизонтального луча рассматривают отрезок из точки Р в случайную точку, которая гарантированно находится за пределами полигона. Это значительно уменьшает вероятность появления последних двух особых ситуаций, и их вообще не учитывают. То, что “луч” не горизонтален, не усложняет кода, поскольку для проверки, пересекаются ли отрезки, используют “упрощенную” версию.
Этот способ действительно проще реализовать, и риск, что он будет “пойман” при тестовой проверке, действительно очень мал. Но полной гарантии правильности все-таки нет....
Второй способ — сумма углов. Вначале выявим (с помощью n-кратной проверки принадлежности точки отрезку) и обработаем ситуации, когда точка Р попадает на сторону или в вершину полигона.
В остальных ситуациях вычислим сумму углов между векторами РД и РД , между РД и PAj, ..., между РД,! и PAq (трактуя их как значения от -л до л, см. раздел 6.1.1). Эта сумма может принять только одно из трех значений:
•	2л — точка Р внутри полигона и номера вершин соответствуют обходу полигона против часовой стрелки;
•	-2я — точка Р внутри полигона и вершины пронумерованы по часовой стрелке;
•	0 — точка Р снаружи полигона.
Разумеется, не стоит проверять вычисленную сумму на равенство этим значениям, поскольку использование обратных тригонометрических функций почти обязательно создаст погрешности. Но вряд ли эти погрешности окажутся настолько большими, чтобы нельзя было с уверенностью различить числа, близкие кО и близкие к2л. Разве что используемые координаты представлены с огромными погрешностями.
•	Существенным достоинством второго способа является то, что он не имеет особых ситуаций (кроме попадания точки на сторону или в вершину полигона, которое рассматривается в самом начале алгоритма).
6.2.4.	Принадлежность точки выпуклому полигону
Задача 6.5. Задача отличается от предыдущей только тем, что полигон гарантированно выпуклый.
Анализ задачи. Эта задача — частный случай предыдущей, более общей задачи, ее можно решить тем же способом, что и предыдущую. Однако и в математике, в программировании для решения частных случаев многих, хотя и не всех, задач существуют более эффективные способы.
Рассмотрим два алгоритма, применимые только к выпуклым полигонам. Первый из них проще, чем рассмотренные выше, а второй имеет лучшую асимптотическую
178
ГЛАВА
оценку количества действий. К сожалению, в данной ситуации эти преимущества сочетаемы.
Первый способ—проверка полуплоскостей. Проверим, в левой или правой полуплос кости относительно каждой из прямых А^, АХА2, ..., А^^ находится точка Р. Зада| эти прямые нужно с помощью начальной точки Ао и направляющего вектора АдА,1 точки А] и вектора AAj и т.д., нигде не изменив порядка вершин. Если эти п провера дают все ответы “строго слева” или все “строго справа”, точка находится внутри nd лигона. Иначе, если есть хотя бы по одному ответу и “строго слева”, и “строго справа* точка вне полигона. В противном случае точка принадлежит границе.
Хранить и анализировать информацию о том, какие результаты были и каких в было, удобно, на наш взгляд, с помощью булевых переменных found_left found_right и found_straight. Они инициализируются значением false при каждом определении положения точки относительно прямой соответствующе! переменной присваивается true.
Этот способ решения проще реализовать, чем любой из способов для предыду щей более общей задачи (особенно, если нет ни готовой функции вычисления упв между векторами, ни функции arctan2). К тому же, он работает в несколько рас быстрее, чем способы предыдущей задачи, хотя и имеет такую же оценку количества действий 0(л).
Второй способ — бинарный поиск сектора. Возьмем некоторую точку О внутри полигона и мысленно рассмотрим лучи ОА0, ОАХ,..., ОАп1. Точка Р попадает в один из “секторов”, образованных соседними лучами OAt и OA.+V Чтобы найти i (в какой именно сектор попала точка), можно применить бинарный поиск, поскольку каждый следующий из векторов ОАд , OAi,...., ОАП_1 направлен левее предыдущего (или правее, в зависимости от направления обхода АдАр.-А^). Когда сектор найден, остается только посмотреть, по одну ли сторону от А оказались точки Р и О.
н Реализацию этого способа предлагаем в качестве упражнения, причем довольно сложного поскольку придется решить немало технических проблем, сознательно не упомянутых здесь. Убедитесь, что реализация имеет оценку количества действий O(log2«) в худшем случае, т. е. что главное достоинство способа не утрачено при реализации.
Из всего сказанного напрашивается вывод, что в обычных ситуациях целесообразнее использовать первый способ. Но если понадобится проверять принадлежность тысяч точек одному и тому же выпуклому полигону, имеющему тысячи вершин, реализация второго способа будет работать существенно быстрее.
6.2.5.	Построение полигонов
Задача б.б. На координатной плоскостй заданы координаты N различных точек. Ситуация, в которой все они лежат на одной прямой, гарантированно исключается. Вывести координаты этих точек в таком порядке, чтобы проведенная через них ломаная была простым полигоном.
Анализ задачи. Разумеется, для большинства входных данных эта задача имеет много разных решений; следовательно, вывести нужно любое одно из них. Один из
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
179
способов упорядочения, дающий правильный результат для всех возможных входов, использует понятие монотонной ломаной.
Ломаная называется монотонной относительно прямой а, если любая прямая, перпендикулярная прямой а, пересекает ломаную не более чем в одной точке.
Ломаные, монотонная и немонотонна^ относительно горизонтальной прямой, представлены на рис. 6.10.
Рис. 6.10. Монотонная и немонотонная ломаные
Чтобы провести через заданные точки ломаную, монотонную относительно горизонтальной прямой, достаточно отсортировать эти точки по х-координате и соединить в полученном порядке. Монотонная ломаная очевидно не имеет самопересечений.
На рис. 6.11 представлен полигон, состоящий из двух ломаных, монотонных относительно горизонтальной прямой. Так возникает идея выделить крайние точки (слева и справа) и соединить их двумя монотонными ломаными (верхней и нижней), ожидая, что получится полигон.
Рис. 6.11. Две монотонные ломаные образуют полигон
Очевидно, верхние точки должны образовать верхнюю монотонную ломаную, а нижние — нижнюю. Однако еще нужно правильно разбить точки на верхние и нижние, иначе ломаные могут пересечься (рис. 6.12).
Рис. 6.12. Монотонные ломаные могут пересекаться
Примем меры, чтобы ломаные не пересекались. В качестве концов монотонных томаных возьмем, как уже сказано, одну из крайних слева и одну из крайних справа точек. Они определяют прямую, которая разбивает плоскость на две полуплоскости. Разобьем точки на группы верхних и нижних так, чтобы верхние лежали в верхней полуплоскости, а нижние — в нижней. Тогда проведенные через них монотонные
180
ГЛАВА 6
ломаные тоже целиком принадлежат соответствующим полуплоскостям и не пересекаются. Их объединение проходит через все заданные точки и не содержит пересечений ни в пределах ломаных, ни разных ломаных между собой, т.е. получен искомый полигон. Итак, к нужному результату приводят следующие действия:
отсортировать точки по х-координате и найти прямую, которая соединяет одну из крайних слева и одну из крайних справа точек;
разбить точки на верхние и нижние относительно полученной прямой;
из верхних и из нижних точек построить верхнюю и нижнюю ломаные, монотонные относительно горизонтальной прямой;
объединить монотонные ломаные в полигон (нижнюю ломаную пройдем слева направо, верхнюю — справа налево).
Для реализации приведенного алгоритма нужно выбрать метод сортировки и разобраться с двумя особыми ситуациями.
Точки нужно разбить на две непересекающиеся группы соответственно полуплоскостям. Но может оказаться, что некоторые точки (кроме выбранных крайних лежат на самой прямой. Будем считать, что сама прямая относится к одной (и только одной) из полуплоскостей — например, верхней.
Если несколько (не меньше трех) разных точек лежат на одной вертикали, могут возникнуть наложения вертикальных отрезков. Для решения этой проблемы достаточно упорядочить точки с одинаковыми х-координатами по их у-координатам. Ломаные теперь могут оказаться немонотонными относительно горизонтальной прямой, но это не влияет на построение полигона. Кстати, если крайних слева точек несколько, следует выбирать самую нижнюю из них (и самую верхнюю среди крайних справа).
Данная задача весьма специфична, но два свойства ее решения типичны для задач вычислительной геометрии:
•	основная идея проста, но есть специальные случаи, требующие отдельного рассмотрения и аккуратности при программировании;
•	используется сортировка точек по одной из координат.
Задача 6.7. Задано множество точек плоскости. Нужно построить его выпуклую оболочку — выпуклый полигон, охватывающий все заданные точки, внутри которого нет других выпуклых полигонов, также охватывающих все точки.
Вход. Сначала задается количество точекп (3<и<1000), затем, парами координат, сами точки.
Выход. Количество вершин выпуклой оболочки, затем последовательность номеров точек (согласно порядку во входных данных), задающая обход выпуклой оболочки против часовой стрелки.
Пример
Вход 5	Выход 4
0022433017	1435
Анализ задачи. Смысл выпуклой оболочки (сокращенно ВО) иногда объясняют с помощью следующей аналогии. Забьем гвозди во все заданные во входных данных точки плоскости. Возьмем резиновое кольцо, растянем его так, чтобы оно охватило все точки, и отпустим. После “схлопывания” кольцо охватит некий выпуклый по
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
181
лигон, вершинами которого будут “наиболее выступающие наружу” точки набора. Это и есть выпуклая оболочка.
Отсортируем точки7 по неубыванию х-координаты и обозначим их, уже отсортированные, как Ао, Ар ..., Ал1. Точки Ао (крайняя слева)8 и Ал1 (крайняя справа), очевидно, принадлежат ВО.«Теперь построим две “половинки” ВО (точнее говоря, границы ВО), идущие “по низу” слева направо и “по верху” справа налево.
Хранить ВО (точнее, цепочку, которая в конце концов станет границей ВО) будем в виде отдельной последовательности, т.е. элементами Со, С,,... будут некоторые из точек9 Ао, А,.
Построение нижней части цепочки начинаем с того, что включаем в нее Ао и А, Сд :=Ад, С, :=At). Затем рассматриваем точки А2, А3,... по порядку. Пусть Ск_, и Ск — предпоследняя и последняя точки цепочки, А. текущая точка. Если угол поворота от Ск_1Ск к Ск Д отрицателен или равен 0, точка Ск удаляется из цепочки. Причем, если удаляется Ск, то аналогично проверяем знак поворота от Ck_2Ck_t к Сд_]Д и т.д. В конце концов или две последние точки в цепочке образуют с новой точкой левый поворот, или от цепочки остается одна точка Са. После этого точка А,, добавляется к цепочке. Нетрудно убедиться, что крайняя справа точка А^ всегда будет включена в цепочку.
Верхняя часть цепочки строится аналогично, только точки рассматриваются в обратном порядке: Ал_2, Ал_3,..., Ао. Точки, вошедшие в нижнюю часть цепочки, можно не рассматривать, поскольку они, за исключением Ао, не могут входить в верхнюю часть цепочки. Но можно и рассматривать, поскольку они все равно будут удалены при анализе последующих направлений поворотов. Построение заканчивается, когда в верхнюю часть цепочки включается начальная точка С0=А0. В действительности это включение лишнее, и его можно не выполнять. Однако все действия, связанные с проверкой угла поворота от Ск_{Ск к , выполнить необходимо.
Оценим сложность представленного алгоритма. Каждая точка включается в це-ючку не больше двух раз (именно двух, поскольку нижняя и верхняя части строятся сдельными проходами) и не больше двух раз удаляется. Значит, этап выделения раницы ВО имеет сложность 0(п), поэтому сложность всего алгоритма определяет-я сортировкой —O(nlogn).
Другие алгоритмы построения ВО представлены, например, в работах [22, 23, 34].
7 При выводе результата нужны начальные номера, поэтому сортировать придется струк-уры, состоящие из собственно точки и ее начального номера (см. также задачу 5.5).
8 Если крайних слева несколько, желательно выбрать нижнюю из них. Это можно обеспе-ить, если при каждом сравнении точек при равенстве х сравнивать у. Однако дополнитель-ые сравнения замедлят сортировку, которая и так является узким местом алгоритма. Так что ортировать будем только по х. Все крайние слева точки “соберутся” в начале — переберем ж, найдем нужную и переставим на нулевое место (одним обменом). Аналогично поступим и верхней из крайних справа.
9 Можно хранить не сами точки (плюс номера согласно входным данным), а номера этих »чек в последовательности Ао, Ар ..., А^.
182
ГЛАВА 6
6.2.6.	Сумма и разность полигонов
Задача 6.8. Подъемный кран “Мостовой—Глуповский” позволяет переместить крюк в любую точку части плоскости, ограниченной выпуклым полигоном. Верхняя сторона полигона находится под балкой крана и в плане горизонтальна, а внутренние углы при ней острые. Действие двух кранов такого типа можно комбинировать: область достижимости крюка составного механизма такая, как если бы балку второго крана подвесили в определенной ее точке на крюк первого, сохранив горизонтальность в плане (рис. 6.13).
С2
Рис. 6.13. Сумма двух областей достижимости
Задача 1. По областям достижимости двух кранов найти область достижимости их комбинации.
Задача 2. По области достижимости одного крана и заданной области выяснить, каким должен быть второй кран, чтобы область достижимости комбинации кранов совпала с этой областью (или выяснить, что второй кран подобрать невозможно).
Вход. В первой строке текста — 1 или 2 (обозначение решаемой задачи), в двух следующих строках — две области. Если решается задача 1, то это области достижимости первого и второго кранов, а если 2, то сначала нужная область, а затем область первого крана. Области задаются в таком формате: целое N (3<jV< 1000) — количество вершин в полигоне, а затем N пар чисел ху и у( (координат вершин области) в порядке возрастания х-координаты. Первая вершина имеет координаты (0;0), ось у направлена сверху вниз. Все входные координаты — целые неотрицательные числа не больше 106. Числа разделены пробелами.
Выход. Если решается задача 2 и подобрать второй кран нельзя, то выводится о, иначе построенная область достижимости в формате входных данных. Если какие-то координаты окажутся нецелыми, округлить их до ближайшего целого.
Примеры
Вход	Выход
2	3 0 0 10 40 20 0
5 0 0 10 40	20 50 30 40 40 0
3 0 0 10 10	20 0
2	0
3 0 0 10 10	20 0
3 0 0 10 10	40 0
1	4 0 0 20 20 50 10 60 0
3 0 0 10 10	20 0
3 0 0 10 10	40 0
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
183
Анализ и решение задачи. Сначала разберем сложение областей из последнего примера в условии — 3 0 0 10 10 20 ОплюсЗ 0 0 10 10 40 0.
На рис. 6.14 показано, как результат сложения (см. рис. 6.13) получается путем скольжения (параллельных переносов) второй области вдоль сторон первой. Сторона С£2 образована как щкэдолжение стороной BtB2 параллельной ей стороны А(Аг, т.е. как результат переносов этих сторон. Сторона С2С} — это перенос В2В3, С3С4 — перенос AjA3.
Рис. 6.14. Стороны суммы как параллельные переносы
Следовательно, для решения задачи 1 нужно включить в результат параллельные переносы всех сторон обоих входных полигонов — в таком порядке, чтобы результирующий полигон оказался выпуклым. С учетом “перевернутости” системы координат, полигон является выпуклым тогда и только тогда, когда угловой коэффициент каждого (не первого) ребра строго меньше предыдущего (угловой коэффициент стороны vvf+1 вводим обычным образом, как (у^-у^х^-х)).
Итак, задачу можно решить, используя алгоритм слияния упорядоченных последовательностей (см. раздел. 5.2). Его придется модифицировать, поскольку на входе и на выходе — последовательности вершин, а сравнивать нужно угловые коэффициенты сторон. Первая вершина и в обеих входных, и в выходной последовательности имеет координаты (0;0). Начиная с i=2, когда текущей в какой-либо последовательности вершин является i-я, рассматривается сторона из (/-1)-й вершины в /-ю. Естественно, при сравнении угловых коэффициентов нужно выбирать одну из трех возможностей: меньше, больше, равно.
Рассмотрим реализацию вычисления суммы полигонов. Представим полигон ассивом вершин а и переменной num, хранящей их количество; объединим их в структуру. В элементе массива запомним х- и у-координаты вершины. В структуры si и s2 прочитаем вершины полигонов-слагаемых. Сумму сформируем в s3 с помощью модифицированного слияния.
11 := 2; i2 := 2; i3 := 1;
il, 12 - номера текущих вершин в слагаемых, i3 - в сумме }
З.а[1].х := 0; s3.a[l].y := 0; { первая вершина суммы -- (0,0) } while (il <= si.num) and (i2 <= s2.num) do begin
пока ни одно из слагаемых не закончилось }
dxl := (sl.a[il].x - si.a[il-1].х);
dyl := (sl.a[il].y - si.a[il-1].у);
dx2 := (s2.a[i2].x - s2.a[i2-l].x);
dy2 := (s2.a[i2].y - s2.a[i2-l].y);
kl := dyl/dxl; { угловые коэффициенты }
k2 := dy2/dx2;
if abs(kl-k2) < EPS then begin
{ параллельность (kl = k2) проверяем с запасом
на погрешности вычислений, EPS=le-15 }
184
ГЛАВА 6
{ в результат добавляем сумму параллельных ребер } inc(i3) ;
s3.a[i3].x := s3.a[i3-l].x + dxl + dx2;
s3.a[i3].y := s3.a[i3-l].y + dyl + dy2;
inc(il); inc(i2);
end else
if kl > k2 then begin
{ добавим сторону первого полигона }
inc(i3);
s3.a[i3].x := s3.a[i3-l].x + dxl;
s3.a[i3].y := s3.a[i3-l].y + dyl; inc(il);
end else begin
{ добавим сторону второго полигона }
inc(i3);
s3.a[i3].x := s3.a[i3-l].x + dx2;
s3.a[i3].y := s3.a[i3-l].y + dy2;
inc(i2);
end;
end;
{ Допишем хвост оставшейся последовательности.
Из следующих двух циклов выполняется только один }
while (il <= si.num) do begin
dxl := (sl.a[il].x - si. a [il-U .x) ;
dyl :•= (sl.a[il].y - si .a [il-1] .y) ;
inc(i3);
s3.a[i3].x := s3.a[i3-l].x + dxl;
s3.a[i3].y := s3.a[i3-l].y + dyl;
inc(il);
end;
while (i2 <= s2.num) do begin
dx2 := (s2.a[i2].x - s2.a[i2-l].x);
dy2 := (s2.a[i2].y - s2.a[i2-l].y);
inc(i3);
s3.a[i3].x := s3.a[i3-l].x + dx2;
s3.a[i3].y := s3.a[i3-l].y + dy2;
inc(i2);
end;
s3.num := i3; { количество вершин в сумме }
Область достижимости комбинации двух кранов (задача 1), т.е. сумма двух полигонов, называется их суммой Минковского. Область достижимости второго крана, дающая в сумме с областью первого крана суммарную область (задача 2), т.е. разность двух полигонов, называется разностью Минковского.
Нетрудно (по крайней мере для выпуклых полигонов частного вида, используемых в данной задаче) убедиться в том, что сумма Минковского существует для любых выпуклых множеств-слагаемых10. Разность существует не всегда, но, если существует, является единственной. Это дает ключ к решению задачи 2.
10 Полигон здесь рассматривается как множество точек.
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
185
Для решения задачи 2 достаточно двигаться вдоль суммарной области (полигона-уменьшаемого) и области заданного крана (полигона-вычитаемого). Если очередная сторона уменьшаемого есть в вычитаемом, то пропускаем ее (продвигаясь и по уменьшаемому, и по вычитаемому). Если стороны в вычитаемом нет, значит она должна быть в полигона-разности. Еще возможно, что сторона уменьшаемого параллельна стороне вычитаемого и длиннее ее — тогда в разность идет параллельная им сторона, длина которой равна разности длин.
Если же наступает какая-либо из ситуаций:
•	угловой коэффициент у текущей стороны уменьшаемого меньше, чем у вычитаемого;
•	текущая сторона уменьшаемого параллельна и короче текущей стороны вычитаемого;
•	стороны уменьшаемого уже закончились, а вычитаемого — остались,
то для данных полигонов разность не существует (ответ 0). Если же первыми закончились стороны вычитаемого, то оставшиеся стороны уменьшаемого просто идут в полигон-разность.
И последнее замечание. Сумма и разность Минковского определены для произвольных выпуклых множеств, поэтому задачу можно немного обобщить — найти сумму-разность произвольных выпуклых полигонов (не обязательно с горизонтальной верхней стороной). Поскольку стороны могут быть вертикальными, использовать уг-товые коэффициенты нельзя. Однако, чтобы определить, какая из сторон “меньше повернута влево”, можно посмотреть на знак “ориентированной площади” аД-оД.
6.3. Окружности и круги
Напомним; окружность — это линия, все точки которой находятся на одном том же расстоянии (оно называется радиус) от точки-центра, а круг — часть плос-ости, ограниченная окружностью. Чтобы задать окружность или круг, указывают оординаты центра и радиус.
Любые три точки, не лежащие на одной прямой, тоже определяют окружность эти точки определяют треугольник, а вокруг него можно описать окружность). Если заданы три точки и нужно получить окружность (в виде центра и радиуса), повторяют построения “классической” геометрии: строят серединные перпендикуляры двум сторонам треугольника и ищут точку пересечения перпендикуляров (из подразделов 6.1.1 и 6.1.3 должно быть понятно, как это сделать численно).
» Реализуйте соответствующие вычисления в подпрограмме.
6.3.1.	Прямая и круг
Задача 6.9. На плоскости заданы круг с центром в точке (Ох;Оу) и радиусом А и прямая, проходящая через точки (Ах; Ау) и (Вх;Ву). Найти длину части прямой, принадлежащей кругу.
Анализ задачи. Рассмотрим рис. 6.15. Если расстояние 5 от центра О до прямой •альте или равно R, то прямая не пересекается с кругом или пересекается только
186
ГЛАВА I
в точке касания, так что ответ 0. Иначе нетрудно убедиться, что искомая длина равн| 21 = bjR2-d2 .
Рис. 6.15. Прямая пересекает круг
6.3.1.	Отрезок и окружность
Задача 6.9. На плоскости заданы окружность с центром в точке (Ох; Оу) и радиусом R и отрезок с концами (Ах; А^) и (Вх; Ву). Найти количество точек пересечения окружности и отрезка.
Анализ задачи. Сначала сравним расстояние 3 от центра О до прямой АВ с радиу сом R. Если 8>R, то с окружностью не пересекается даже прямая АВ, а отрезок А1 тем более.
Если это не так, посмотрим, находятся ли точки А и В внутри окружности (дм этого достаточно вычислить длины отрезков ОА и ОВ и сравнить их с R). Если обе точки строго внутри окружности, то пересечений нет. Если одна точка внутри, а другая снаружи или на самой окружности, то точка пересечения одна.
Остаются только ситуации, когда прямая пересекает окружность и каждая из точек А и В или снаружи, или на самой окружности. Эти ситуации можно разделить на две группы: а) точки лежат “по одну сторону” от окружности, т. е. прямая ее пересекает, а отрезок — нет; б) точки находятся “по разные стороны” от окруж-i ности, т.е. пересечения прямой с окружностью являются также пересечениями отрезка с окружностью.
Распознать группы ситуаций можно по величине углов Z.OAB и ZOBA: если один из них тупой (соответствующее скалярное произведение отрицательно), имеем си туацию б, иначе ситуацию а. Наконец, в ситуации а нужно вновь сравнить 8 и R: если 8=R, точка пересечения одна (точка касания), а если 8<R, то две. В ситуации нужно проверить, принадлежат ли окружности сами точки А и В (сравнив расстояния О А и ОВр R): если хоть одна принадлежит, то она является единственной точко пересечения, иначе точек пересечения нет.
К решению можно подойти иначе: найти в явном виде координаты точек пересечения ок ружности с прямой, а затем исследовать, в каком порядке точки пересечения и концы от резка лежат на прямой. Этот подход может дать больше информации (например, с его помощью легко найти длину части отрезка, принадлежащей кругу), хотя в нем рассматривается приблизительно такое же, если не меньшее, количество случаев. Однако здесь нужна значительно больше арифметических операций, что приводит к большему риску принять неверное решение из-за погрешностей.
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
187
6.3.2.	Общие касательные
Задача 6.10. Даны две окружности с центрами О} и О2 и радиусами иЯ2. Найти все их общие касательные.
Анализ задачи. Предположим, что Я, >R2 (если это не так, поменяем окружности местами). Через |О,О2| обозначим расстояние между центрами, т.е. длину отрезка Ор2.
Рассмотрим особые ситуации. Если окружности совпадают (10^1=0, /?1=/?2), то все их касательные являются общими; их бесконечно много, поэтому нужно выдать особое сообщение. Если одна из окружностей полностью, не касаясь, лежит внутри другой (при Р{>\ор]+Р2), то общих касательных нет. Если окружности касаются “внешним образом”	+Д), то общих касательных три: две “внешние одно-
сторонние” и еще одна, проходящая через точку касания окружностей перпендикулярно ОХО2. Если окружности касаются “внутренним образом” (Д=|О,О2|+Д), то касательная одна и проходит она через точку касания окружностей перпендикулярно Ор2.
Остаются две “главные” ситуации: а) окружности не пересекаются (|0]02|>Д+Д); 6) окружности пересекаются в двух точках (не выполнилось ни одно из предыдущих условий). В ситуации а общих касательных четыре (рис. 6.16), в ситуации б—две (остаются только “односторонние” касательные АД и ДД).
Рис. 6.16. Общие касательные двух окружностей
Рассмотрим подробно поиск точек Aj и А2. Поскольку АД — касательная, углы и ZO2A2A1 прямые. Точку К построим (на бумаге, а не в алгоритме) как основание акрпендикуляра, опущенного из О2 на 0^. Тогда ОД4Д — прямоугольник, а О2ОХК— прямоугольный треугольник, у которого известны длина	гипотенузы ОХО2 и длина
-R2 катета ОХК. Отсюда, обозначив через а величину угла ХОр^К, получим
а = arcsin	.
J(Olx-O2x)2 + (Oiy-O2J
188
ГЛАВА 6
Угол наклона вектора O2OY (обозначим его <р) тоже по сути известен. Тогда углы наклона векторов О2А, и ОД равны ф-а-л/2, векторов О2В2 и ОД — (р+а+л/2. Теперь несложно вычислить декартовы координаты этих векторов, а По ним — координаты точек Ар А2, и В2.
Нетрудно убедиться, что рассуждения предыдущего абзаца применимы в обеих “главных” ситуациях.
Для нахождения точек Ср С2, Dt и D2 (если они существуют) повторяются почти те же рассуждения, только длина катета равна Rx+Rr
6.3.4.	Пересечение двух кругов
Задача 6.12. Два соседа-фермера получили во владение по участку в виде круга. Возникло подозрение, что часть территории принадлежит обоим фермерам. Они не стали судиться, а объединились в кооператив, чтобы совместно использовать всю полученную землю. Какая площадь оказалась в распоряжении кооператива?
Вход. В строке текста шесть вещественных чисел через пробел: координаты центра и радиус одного круга, затем то же — другого. Все числа по модулю не больше 106 и содержат не больше трех знаков после запятой.
Выход. Одно число в экспоненциальной форме без округления и с возможной погрешностью не больше 10-3.
Пример. Вход-. О О 17 О 21 10. Выход-. 1.15575235315894Е+0003.
Анализ задачи. Ясно, что важны только радиусы кругов и расстояние между центрами (ХрУ,) и (х2;у2). Поэтому вычислим D= y](xl -Xj/ +(у, -у2У и “забудем” координаты. Для удобства предположим, что RtkR2 (если это не так, поменяем местами значения R} и R2).
Рассмотрим тривиальные ситуации.
•	Круги не пересекаются или пересекаются в одной точке, т.е. Rl+R1<D- можн сразу выдать окончательный ответ n(R2+R2).
•	Один из кругов полностью лежит в другом. Поскольку Я, >R2, второй круг лежит в первом. Тогда R}>D+R2, и площадь объединения равна площади первого круга, т.е. itR*.
Итак, остается ситуация, когда круги пересекаются частично. В сумме площаде кругов пересечение учтено дважды, и от суммы достаточно отнять площадь пересечения. Поэтому займемся вычислением площади пересечения, используя обозначения, представленные ра рис 6.17.
AOjAB и АО^АВ прямоугольные, а сумма расстояний ОгВ и равна D, поэтому: d12+h2 = /?12,	(6.4
dy+h2 = R2\	(6.5
d\+d2 = D.	(6-6
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
189
Из (6.5) и (6.4) получим d2-d2= R2-R2\ учитывая тождество d2-d2 = (d^djxtd-dj и уравнение (6.6), получим
(6.7)
di-d2=(Ri2-R22)fD.
Система уравнений (6.6) и (6.7) позволяет легко найти dj и d2; зная их, можно по 6.4) вычислить h; также можно найти а, и а2 — величины ZAO,C и ZAO2C;
а, = 2 ZAOiB = 2arctg(A/d,), (i = 1,2).	(6.8)
Рассмотрим подробнее сектор и сегмент круга, определяемые точками А и С. Сек-р круга — это часть плоскости, ограниченная двумя радиусами и дугой; на рис. 6.18 сектор ОАС выделен редкой штриховкой. Сегмент круга — это часть плоскости, ограниченная хордой и дугой; на рис. 6.18 сегмент АС выделен частой штриховкой.
Рис. 6.18. Сектор и сегмент круга
190
ГЛАВА!
Очевидно, что сектор О АС состоит из двух областей — сегмента АС и АО АС.
Площадь сектора ОАС равна
1/27?2сх,	(6.9J
где а — радианная мера ХАОС. Это известная формула, но, даже не зная ее, лети догадаться, что площадь сектора прямо пропорциональна центральному углу, а сектор полного угла 2л совпадает с кругом, площадь которого л/?2.
Площадь АОАС как площадь треугольника, определяемая по двум сторонам и углу между ними, равна
(6.Ш
Отсюда площадь сегмента АС равна разности площадей сектора и треугольника, т.е
l/2/Jz(a-sina).	(6.1 Г
Пересечение кругов состоит из двух сегментов: рассмотренного сегмента первого круга и аналогичного сегмента второго. Казалось бы, осталось только применил формулы (6.8) и (6.11) еще и ко второму кругу.
Однако еще нужно исследовать, всегда ли пригодны уравнения, построенные пл рис. 6.17. Расположение кругов на плоскости несущественно, поэтому общносп не теряется, если считать, что центр второго круга размещен горизонтально спраа от центра первого. Но нужно выяснить, имеют ли смысл используемые уравнения, если центр второго круга О2:
1)	совпадает с крайней справа точкой первого круга;
2)	находится между крайней справа точкой первого круга и точкой В;
3)	совпадает с точкой В;
4)	находится между точками В и О,.
Строго говоря, нужно еще убедиться, что все эти ситуации возможны. Проверьте самостоятельно, что ситуации 1 и 2 возможны и для них годятся все предыдущие математические выкладки.
В ситуации 3 вычислить а, по формулам (6.11) нельзя, поскольку </2=0. Но здеса можно или сказать, что сегмент является полукругом и имеет площадь nR2fl, илл заметить, что =л, и применить формулу (6.11).
Ситуацию 4 рассмотрим подробнее (рис. 6.19). Если решить систему уравнений (6.4)—(6.6), результат отличается от ожидаемого согласно рис. 6.17, но является осмысленным: dx — длина |<?jB|, h — длина |АВ|, а d2 — отрицательное число, модуль которого равен длине |OjB|. Использование отрицательного d2 в (6.8) приводит к отрицательному значению о^, что явно не соответствует ни формуле (6.9), ни рис. 6.19 Однако нетрудно заметить, что нужное значение — это значение, полученное п формуле (6.8) и увеличенное на 2л.
На первый взгляд, даже после этого исправления остается различие в ситуаци ях на рис. 6.18 и 6.19. На рис. 6.18 площадь сегмента равна площади сектора минус площадь треугольника, а на рис. 6.19 — площади сектора плюс площадь треугольни ка. Но в действительности, подставляя о^ж в формулу (6.11), получаем отрицатель
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
191
ную площадь треугольника, поэтому выражение (6.11) является при а^л разностью площадей, а при а^>п — суммой.
Рис. 6.19. Сектор и сегмент круга с углом больше л
Итак, для решения задачи в ситуации 4 перед применением (6.11) к о^ нужно прибавить 2л.
В итоге приходим к следующему алгоритму.
D sqrt((xl-x2)*(xl-x2) + (yl-y2)*(у1-у2)).
Обеспечить, что Rl > R2.
SI = X*R1*R1, S2 = n*R2*R2.
Отбросить тривиальные случаи:
если R1+R2 < D, закончить с результатом S1+S2;
если Rl £ D+R2, закончить с результатом S1.
Вычислить dl и d2 по (6.6)-(б.7), h по (6.4).
Вычислить (Xj по (6.8) и S_segml по (6.11).
Вычислить а2:
если d2 = О, то а2 = л,-
если d2 > 0, то вычислить а2 по (6.8) ;
если d2 < 0, то а2 равен результату (6.8) плюс 2л.
Вычислить S_segm2 по (6.11).
Возвратить результат Sl+S2-S_segml-S_segm2.
192
ГЛАВА!
Упражнения
6.1.	Даны два вектора а = (а. х; а. у) и с = (с. х; с. у). Их необходимо повернуть вместе (угол между а и с остается неизменным) так, чтобы с стал сонапра®-лен оси Ох. Напишите фрагмент программы, выполняющий такой поворот эффективнее, чем по формулам в конце раздела 6.1.1.
6.2.	Точки плоскости А, В, С, D заданы координатами. Вычислить длину кратчайшего пути из А в В с учетом того, что отрезок CD пересекать нельзя.
6.3.	Ввести координаты точек А, В, С, D и определить, является замкнутая ломаная ABCDA:
а)	четырехугольником, т.е. отрезки не пересекаются;
б)	выпуклым четырехугольником;
в)	невыпуклым четырехугольником.
6.4.	Два отрезка, расположенные в первом квадранте плоскости, заданы координатами концов. Написать процедуру определения, какие части отрезков видно из начала координат при условии, что отрезки могут пересекаться и закрывать друг друга. Процедура должна выдавать сообщения вида: Первый отрезок виден целиком, второй не виден, Первый отрезок виден от точки ... до точки и от точки ... до точки ..., второй виден целиком и т.п.
6.5.	Написать программу проверки, принадлежит ли точка плоскости заданно полигону.
6.6.	Реализовать проверку, является ли заданный полигон выпуклым.
6.7.	Задан выпуклый полигон на плоскости и пара точек вне его. Вычислить длину кратчайшего пути между точками при условии, что полигон иересе кать нельзя.
6.8.	На плоскости задан выпуклый полигон АД...А*-,. В нем выделены две вершины А;. и Аг Найти вершину Ак, образующую с заданными треугольник мак симальной площади. Предложите наиболее эффективный алгоритм.
6.9.	Два соседа-фермера получили во владение по участку земли в виде полигона (углы не равны 180°) без самопересечений. Граница участка задан® координатами вершин полигона в порядке его обхода или по часовоА стрелке, или против. Позже выяснилось, что эти земли имеют общую часть, но ни одна из вершин полигона, ограничивающего один из участ ков, не попадает на границы другого. Фермеры решили огородить общую землю — некоторое множество полигонов (также без самопересечений углов в 180°). Для этого нужно поставить колья во все вершины полигонов, образующих пересечение земель.
Вход. В первой строке текста два целых числа (от 3 до 100) — количества вершин участков, во второй — координаты вершин первого участка, в третьей — второго (целые числа не больше 1000 по модулю, разделенные пробелами).
Выход. Искомое количество кольев.
ВЫЧИСЛИТЕЛЬНАЯ ГЕОМЕТРИЯ НА ПЛОСКОСТИ
193
Примеры
Вход	Выход
5 6 2253618447 275 10 78856455	5
6 6 14476442604 -2 416-3 12 4774482	8
6.10.	Треугольник на плоскости задан координатами вершин. Найти (с точностью 1е-4) минимальную длину стороны квадрата, в который можно поместить этот треугольник так, чтобы все вершины треугольника находились внутри квадрата или на его сторонах.
6.11.	Даны две окружности. Найти координаты точек их пересечения.
6	12. На плоскости заданы круг с центром в точке (Ох; Оу) и радиусом R и отрезок с концами (Ах; А^) и (Вх; Ву). Найти длину части отрезка, принадлежащей кругу.
Глава 7
Выметание
В этой главе...
•	Упорядоченные точки событий
•	Отслеживание статуса выметания
•	Решение одно- и двухмерных геометрических задач методом выметания
7.1. Интернет-провайдер
Задача 7.1. Мелкий Интернет-провайдер предоставляет клиентам многоканальный телефон. Когда модем клиента звонит на этот номер, АТС соединяет его с любым из свободных каналов (если такие есть).
На каждом канале ведется журнал событий. В нем указаны упорядоченные по возрастанию моменты времени, в которые канал занимался или освобождался: первое число означает момент присоединения клиента к каналу, второе — момент его отсоединения, третье — момент присоединения следующего и т.д. Время считается в секундах, прошедших от начала работы провайдера. Длительность соединения не меньше 1 с; канал готов к подключению клиента через 1 с после отключения предыдущего; если на нескольких каналах происходят одновременные подключения или отключения, в этот момент все эти каналы считаются занятыми.
Провайдеру нужно выяснить, в какие промежутки времени все каналы были свободны и в какие все были заняты.
Вход. Журнал каждого канала представлен отдельным текстом; в каждой его строке записано одно целое число — момент времени. Количество строк в тексте предварительно не указано, но последняя строка завершается символом перевода строки, сразу после которого следует конец файла. Информация о журналах помещается во входном тексте provider.dat: первая строка содержит число N (2< У<50)' — количество каналов, следующие N строк — имена файлов, представляющих журналы.
Выход. Текст allfree. sol должен содержать (в хронологическом порядке) промежутки времени, когда все каналы свободны, allbusy. sol — когда все заняты. Каждый промежуток представлен отдельной строкой, в которой через про-
1 При использовании DOS-среды программирования — 2 <N < 10; это связано с системным ограничением на количество одновременно открытых файлов.
196
ГЛАВАХ
бел записаны его начало и конец. Если на момент анализа все каналы заняты или все каналы свободны, вместо конца промежутка следует вывести слово now.
Пример 1
Provider.dat	n1.dat	n2.dat	1 allfree.sol	allbusy.aol
2	1	3	0 1	3 4
nl.dat	4	5	5 6	7 8
n2.dat Пример 2	6 11	7 8 9 10	11 now	9 10
Provlder.dat	сМ	ch2	ch3	allfree.sol allbusy.sol
3	1	1	4	0 1	4 4
chi ch2 ch3	2 4 6 10	4 9	6 8	68	10 now
Анализ задачи. Представим			приведенные	примеры входа и выхода графически
(рис. 7.1). Ясно, что подключения и отключения изменяют состояния каналов, т.е. являются точками событий в каналах. Заметим: одновременные подключения или от ключения будут разными точками событий. Между точками событий в каналах ничего не изменяется, поэтому для анализа состояний каналов важны только эти точки.
01 23456789 10 11 12
____I_I_I_I—1—1	I_I_1—1_I	I_I_► канал 1_»»> -_____ч
।	к	1-х. J ।_E	l- « 1 «а_।_
канал 2	f11 з и
।	।	 ь-4.1	।_11 11 ।	।	,
все свободны	___
ЕД । । । та । |_| । Еа_». все заняты _
। । I Ш—I I — ст।।„
01 234567 89 10 11 12
Д__1_I_I_I_I_I_I_I_1_1_I_1_
канал 1
____L
канал 2
канал3	г*"^
___I_I__I_ t	»	i ___It	jji	ь „
все свободны	_____
едя__।_|__|_।__________I_I__I__।	»
все заняты
____I___I__I___I__L
Рис. 7.1. Графическое изображение примеров входа и выхода
Чтобы определить, в какие моменты времени заняты или свободны все каналы, достаточно отследить, как изменяется количество занятых каналов в точках событий, т.е. это количество характеризует состояние системы.
Для решения задачи используем метод выметания2 (sweeping). Обычно он включает в себя следующие этапы.
1.	Выделить точки событий.
2.	Отсортировать точки событий.
3.	Обработать точки событий, передвигаясь последовательно от точки события к следующей согласно проведенной сортировке.
2 В литературе часто используют термин "метод заметания".
ВЫМЕТАНИЕ
197
•	Выметание используют, когда при последовательной обработке точек событий удается отслеживать некоторую важную для задачи характеристику, изменяемую в точках событий. Ее называют статусом выметания.
•	Определение точек событий и статуса зависит от конкретной задачи. Например, алгоритм решения задачи 2.10 (см. подраздел 2.2.4) можно рассматривать как особый случаи метода выметания. Точки событий в этой задаче — символы-скобки, статус — количество открытых и еще не закрытых скобок.
В нашей задаче статусом естественно считать колйчество занятых каналов. Такой статус помогает ответить на основной вопрос задачи: все каналы заняты, если статус равен N, и все свободны, если он равен 0. Кроме того, этот статус нетрудно поддерживать, двигаясь по точкам событий: если это подключение, статус нужно увеличить на 1, если отключение — уменьшить.
По условию в момент одновременного подключения и отключения нескольких каналов все они считаются занятыми. Поэтому будем считать, что подключения происходят, говоря неформально, “в начале секунды”, а одновременные с ними отключения — “в конце секунды”.	,
Точки событий обычно сортируют, чтобы проходить их в определенном порядке, например, по неубыванию моментов времени. Однако в данной задаче каждый журнал упорядочен и прохождение точек событий аналогично слиянию журналов, поэтому нужно двигаться одновременно по всем журналам.
Решение задачи. Работу с большим количеством файлов удобно программировать, собрав файловые переменные в массив; назовем его chns (листинг 7.1).
Чтобы начать прохождение по точкам событий, нужно выбрать первую точку. Она соответствует первому моменту, записанному в каком-то из файлов; но в каком именно — неизвестно. Поэтому прочитаем первые числа всех файлов и найдем минимальное; оно и задает первую точку события.
Если что-то читается из текстового файла (не имеющего прямого доступа), текущая позиция в файле смещается и эффективно прочитать то же самое еще раз нельзя. Поэтому нужно помнить значения, прочитанные из файлов последними. Для их хранения используем массив curr_val.
Найдя в массиве curr_val минимальное значение mintime и канал minidx, в котором произошло соответствующее событие, и обработав точку события, нужно продвинуться по файлу chns [minidx]. Для этого прочитаем из него следующее значение и вернемся к поиску минимального значения С обновленным curr_val[minidx].
Для обработки точки события нужно знать, что задает число в файле — подклю-ение или отключение. Числа на нечетных местах в файле задают моменты подклю-ения, на четных — отключения. Признаки того, что места последних прочитанных чисел нечетны, будем хранить в массиве i s_odd.
Наконец, нужно учесть ситуацию, в которой все числа файла уже обработаны. Для этого присвоим соответствующему элементу curr_val “машинную бесконечность” INFTY, гарантированно большую, чем разумные значения3.
3 Другой способ представить “бесконечность” — использовать отрицательное значение. Конечно, при этом придется слегка модифицировать сравнения.
198
ГЛАВА
Листинг 7.1. Решение задачи “Интернет-провайдер”
program Provider;
const MAX_N_CH = $40;
INFTY = $7FFFFFFF;
var heads s text;	{ текст с именами файлов }
fn : string; { имя очередного файла-журнала } chns : array[1..MAX_N_CH] of text;
allfree, allbusy : text; { выходные файлы }
curr_val : array[1..MAX_N_CH] of longint;
is_odd : array[1..MAX_N_CH] of boolean;
N_Ch, i, minidx, used : byte;
mintime : longint;
begin
assign(heads, 'provider.dat'); reset(heads);
readln(heads, N_Ch);
used := 0;
for i := 1 to N_Ch do is_odd[i] := true;
for i := 1 to N_Ch do begin
readln(heads, fn); assign(chns[i], fn); reset(chns[i]); if eof(chns[i])
then curr_val[i] := INFTY
else readln(chns[i], curr_val[i]);
end;
{ открываем файлы с журналами и читаем первые значения } close(heads);
assign(allfree, allfree.sol'); rewrite(allfree);
assign(allbusy, 'allbusy.sol'); rewrite(allbusy);
write(allfree, '0');
{ вначале все каналы свободны }
repeat
{ находим очередную точку события как минимальное значение в curr_val с учетом правила одновременных подключений и отключений } mintime := curr_val[1]; minidx := 1;
for i := 2 to N_Ch do
if (mintime > curr_val[i]) or
(mintime = curr_val[i]) and (is_odd[i])
then begin
mintime := curr_val[i]; minidx := i;
end;
if mintime = INFTY then break; { все файлы закончились }
if eof(chns[minidx]) then { отмечаем конец файла или } curr_val[minidx] := INFTY
else	{ читаем очередное значение }
readln(chns[minidx], curr_val[minidx]);
if is_odd[minidx] then begin { подключение в канале minidx } used := used+1;
if used = N_Ch then { начало интервала, когда все заняты } write(allbusy, mintime);
if used = 1 then { конец интервала, когда все свободны } writeln(allfree,' ', mintime^
ВЫМЕТАНИЕ
199
end else begin	{ отключение в канале minidx }
used := used-1;
if used = 0 then { начало интервала, когда все свободны } write(allfree, mintime);
if used = Nj_Ch-l then { конец интервала, когда все заняты } writein(allbusy, ' 1 ,mintime)
end; is_odd[minidx] := not is_odd[minidx]; until mintime = INFTY;
{ все журналы обработаны } if used = 0 then writeln(allfree, ' now');
if used = N_Ch then writein(allbusy,1 now'); close(allfree); close(allbusy);
for i := 1 to N_Ch do close(chns[i]) end.
Анализ решения. Общее количество действий приведенного решения составляет Q(N K), где X— количество каналов, К — суммарное по всем входным потокам количество точек событий. Объем используемой оперативной памяти не зависит от К и имеет оценку 6(N).
Если исходить из того, что N невелико, т.е. ^=O(1), можно сказать, что общее количество действий имеет оценку 0(К). Все точки событий должны быть обработаны, поэтому существенно улучшить алгоритм нельзя.
Если же учесть, что количество действий зависит от N, обнаружим неоптималь-ность. На каждом из Х-1 шагов (кроме первого) в массиве curr_val изменяется значение одного элемента, а для поиска минимального просматриваются все.
7.2.	Мера объединения отрезков
Метод выметания особенно часто применяют в одно- и двухмерных геометрических задачах и называют еще сканированием плоскости [23] или методом движущейся прямой [21]. Ниже представлены две одномерные геометрические задачи и одна двухмерная.
Задача 7.2. На прямой заданы отрезки. Найти меру их объединения, т.е. суммарную длину всех частей прямой, покрытых хотя бы одним отрезком; части, покрытые несколькими отрезками, учитываются один раз.
Вход. Первая строка текста содержит число N (2<Л(<2500) — количество отрезков, каждая из следующих N строк — по две координаты концов отрезка, заданных числами с плавающей точкой. Неточностями вычислений с плавающей точкой пренебречь.
Выход. Строка с числом — мерой объединения.
Пример
Вход 3	Выход 10.2
-2 5 12 13.2 7 4.5
200
ГЛАВА 1
Анализ и решение задачи. Эту задачу легко решить методом выметания. Точки событий — это левые и правые концы отрезков. Как известно, точки событий нужно упорядочить. Поэтому сортировать нужно именно концы отрезков, а не отрезки в целом. Например, для приведенных входных данных порядок таков: -2, 4.5, 5, 12,13.2.
Статус выметания такой же, как в предыдущей задаче: количество “активных 1 отрезков в данный момент, т.е. при текущей координате. Левый конец отрезка озн чает увеличение статуса на 1, правый — уменьшение. Считать сумму длин нужно п всем промежуткам, для которых статус положителен.
Значит, чтобы отличать в упорядоченном списке точек событий левый конец от резка от правого, нужно сортировать по х не просто координаты, а записи из дв> полей — х-координаты и флажка, указывающего, левым или правым концом она является. Пусть этот флажок типа short int хранит +1 для левого конца и -1 для правого. Эти значения упрощают изменение статуса (листинг 7.2).
Листинг 7.2. Вычисление меры объединения отрезков
{ объявления переменных и подпрограмма сортировки }
begin
assign(fv, 'segm.dat'); reset(fv);
readln(fv, N);
for i := 1 to N do begin
readln(fv, tl, t2);
if t2 < tl then begin
t := tl; tl := t2; t2 := t end;
mass[2*1-1].x := tl; mass[2*i-l].kind := 1;
mass[2*i].x := t2; mass[2*i].kind := -1;
end;
close(fv);
{ вызов эффективной процедуры сортировки массива mass с диапазоном индексов от 1 до 2*N; при этом элементы массива (записи) упорядочиваются по неубыванию значений х-координаты } status := 0;
ans : = 0;	{ найденная мера объединения }
for i := 1 to N*2 do begin
status := status + Data[i].kind;
if (status=l) and (Data[i].kind=l) then
{ начался отрезок объединения } segm_begin ; = Data[i].x;
else if (status=0) and (Data[i].kind=-l) then { закончился отрезок объединения, начатый в segm_begin } ans := ans+(Data[i].x-segm_begin);
end;
writein(ans);
end.
ВЫМЕТАНИЕ
201
Анализ решения. Очевидно, количество действий в приведенном основном фрагменты программы (см. листинг 7.2) имеет оценку 0(jV), поэтому общее количество действий определяется сортировкой и составляет O(NlogN).
» Завершите программу, приведенную в решении (с обязательным использованием эффективного алгоритма сортировки).
н Отрезки (арЬ) на координатной прямой заданы парами концов, i= 1, ...,л. Написать программу, которая проверяет, является ли отрезком объединение отрезков и, если это так, указывает концы этого отрезка. Сложность программы должна быть О(п log п).
7.3.	Линия горизонта
Задача 7.3. Вдоль прямой улицы стоят дома — прямоугольные параллелег пипеды. Нужно найти линию горизонта — ломаную, “отделяющую дома от неба” при взгляде сбоку.
Вход. Первая строка текста horizon. dat содержит число N (2<, N<20000, реализация в 32-битовой среде) — количество домов, следующие N строк — по три числа (координата начала дома, его высота и длина). Все размеры и координаты — целые числа от 1 до 10’.
Выход. Описание линии горизонта в тексте horizon. sol должно состоять из упорядоченных слева направо описаний отдельных горизонтальных фрагментов — троек вида начало, высота, конец. Линия горизонта левее самого левого и правее самого правого дома не описывается. В результатах не должно быть перекрывающихся фрагментов; соприкасающиеся фрагменты одинаковой высоты следует объединять.
Пример
Вход 5	Выход 1 10 20
20	30	10	20	30	30
1	10	40	30	10	50
30	10	20	50	0	60
80	10	20	60	20	90
60	20	30	90	10	100
Анализ задачи. Решим задачу тремя способами. Первые два из них используют метод выметания “в полном объеме”, а последний использует точки событий, но без последовательного продвижения по ним.
Первый способ. Точки событий выделить просто — это координаты начал и концов домов. Параметры (поля) точки события— это х-координата, высота и тип начало или конец дома). Аналогично задаче об объединении отрезков, отсортируем именно точки событий, а не дома. Чтобы учесть замечание о соприкасающихся фрагментах одинаковой высоты, достаточно в случае одинаковых х-координат сна-ала обрабатывать начала домов, а потом концы.
Со статусом ситуация сложнее. Несомненно, он должен хранить максимальную данный момент (при текущей х-координате) высоту дома. Но только ее недостаточно, поскольку такой статус бесполезен в точках событий типа “конец дома”. Когда заканчивается самый высокий в данный момент дом, линия горизонта должна перейти на меныпую высоту — высоту самого высокого из активных (продолжающихся) до
202
ГЛАВА
мов. Пока дом не закончился, не известно, не станет лй он позже “наивысшим активных”. Следовательно, чтобы не потерять полезную информацию, статус да жен хранить совокупность высот всех активных в данный момент домов.
Самый простой для реализации (но не оптимальный по времени обработки) сп соб хранения такого статуса — в виде структуры, состоящей:
•	из максимальной в данный момент высоты дома;
•	из количества активных домов;
•	из массива, в котором записаны высоты всех активных домов.
В каждой точке типа “начало дома” проверим, изменилась ли максимальная вй сота дома, увеличим количество активных домов и допишем высоту нового дома конец массива высот активных домов. Итого, 0( 1) действий.
В каждой точке типа “конец дома” уменьшим количество активных домов и уда лим высоту закончившегося дома из массива (сдвигая “хвост” массива); если высоя закончившегося дома была наибольшей, то ищем в массиве высот самый высокий  оставшихся домов. Итого, О(к) действий, где к— количество активных домен к=О(п).
Кроме того, в любой точке события, если высота линии горизонта изменилася следует вывести предыдущий фрагмент в выходной файл и запомнить х-координат начала нового фрагмента. Это требует всего 0(1) действий.
Итак, решение задачи по этому алгоритму требует О(пк) = О(п) действий, где к -среднее количество активных домов. Здесь и до конца задачи подразумеваете! к > log л; точнее было бы везде писать не пк, а тах{л£, nlogn}, но это громоздко...
й Напишите программу, реализующую описанные действия.
Второй способ. Одним из “основных тормозов” предыдущего алгоритма был про( смотр высот всех активных домов при поиске максимальной. Как известно, если нужно только добавлять произвольные элементы и выбирать максимальный, то для эффективной реализации можно использовать пирамиду (см. подраздел 5.3.4). Таким образом, статус включает в себя пирамиду, которая позволяет быстро, за <?(log£), найти максимальную высоту дома и переупорядочить пирамиду.
В каждой точке типа “начало дома” новый элемент добавляем в пирамиду. Но что делать в точках типа “конец дома”? Ведь закончиться может и не самый высокий из активных домов, и тогда нужно быстро удалить из пирамиды немаксимальный элемент.
К решению этой задачи есть, как минимум, два подхода. Первый — разрешить, чтобы пирамида содержала, кроме активных домов, также некоторые из уже закончившихся низких домов. В точках событий типа “начало дома” проверяем, не выше ли новый дом текущей высоты горизонта. Если выше, выводим очередной фрагмент В выходной файл. Независимо от результата проверки добавляем высоту нового дома в пирамиду. В точках типа “конец дома” проверяем, находится ли закончившийся дом в корне пирамиды. Если нет, то ничего не делаем. Если да, то исключаем его и корня пирамиды и запускаем цикл: пока в корне пирамиды уже закончившийся дом удалить его (перестраивая пирамиду).
Этот подход позволяет не модифицировать операции с пирамидой, но она, как правило, будет содержать ненужные элементы.
ВЫМЕТАНИЕ
203
Другой подход — когда какой-либо дом (возможно, не самый высокий) заканчивается, именно удалять его из пирамиды за время O(logfc). Этим можно добиться суммарной сложности O(n logfc)=O(n logn), почти не изменив остальную часть алгоритма (собственно выметание).
Предположим, чтд индекс удаляемого из пирамиды элемента известен. Организуем удаление аналогично стандартному удалению элемента из корня: поменяем местами значения удаляемого и последнего элементов; уменьшим размер пирамиды; восстановим основное свойство пирамиды.
Значение последнего элемента “перепрыгивает” ближе к корню пирамиды и может оказаться меньше своих сыновей (рис. 7.2, д, б). Тогда его нужно поменять местами с большим из сыновей и так далее вниз по дереву, т.е. пирамида переупорядочивается, как при удалении максимума.
последний	последний	последний
а)5^3,5^4
6)5 <6
в) 5 < 6
Рис. 7.2. Три ситуации при перемещении значения последнего элемента пирамиды на произвольное место: а — перемещение элемента не нарушило основное свойство пирамиды; б — перемещенный элемент меньше своего сына; в — перемещенный элемент больше своего отца
Однако, в отличие от удаления из корня, возможна еще одна ситуация. Если удаляемый элемент не является предком последнего элемента, т.е. принадлежит другому поддереву, перемещенное значение последнего элемента может оказаться больше нового родителя (рис. 7.2, в). Поэтому нужно проверить (и, возможно, восстановить) основное свойство пирамиды еще и в направлении от перемещенного элемента к корню.
Очевидно, что указанные нарушения основного свойства пирамиды не могут появиться одновременно. Поэтому достаточно двух фрагментов кода, каждый из которых проверяет и исправляет соответствующее нарушение. Как известно, выполнение этих фрагментов требует O(logfc) операций.
Таким образом, зная индекс элемента в пирамиде, можно быстро его удалить. А чтобы знать этот индекс, введем глобальную нумерацию домов, например, в порядке чтения входных данных. В точках событий будем хранить, кроме х-координаты, высоты и типа точки, еще и номер дома.
type EvPoint = record
х, h : longint; (* х-координата и высота *) kind : shortint; (* начало/конец дома *) num : integer; (* номер дома в глобальной нумерации *) end;
204
ГЛАВА 7
В элементе пирамиды, хранящем высоту активного дома, будем хранить два поля-heap . eLems [•] . h — высота дома, heap. elems [•] . num — его глобальный номер Наконец, основной прием: используем еще один массив heap_place, индексы которого — глобальные номера домов, а значения элементов — индексы соответствующих домов в пирамиде.
Заметим, что для активных (входящих в пирамиду) домов heap_place [heap, elems [i] .num] равно i, поскольку heap.elems [i] .num- глобальный номер дома, хранимый в /-м элементе пирамиды. Аналогично, heap.elems [heap_place [k] ] . num равно к, поскольку heap_place [k] — место в пирамиде, на котором находится дом с глобальным номером к.
Чтобы поддерживать эти соотношения, придется добавить операторы обработки heap__place во все подпрограммы работы с пирамидой. Это не очень сложно, но требует аккуратности и хорошего понимания сути действий с пирамидой. Зато после этих модификаций можно быстро искать только что закончившийся (в i-й точке события) дом: его глобальный номер равен evjpnts [i] . num, а индекс в пирамиде — heap__place [ev_j?nts [i] .num].
►► Реализуйте приведенный алгоритм.
►► Убедитесь, что для достаточно больших п программа по второму способу работает быстрее чем по первому.
Третий способ. Введем те же точки событий (начала и концы домов), но с небольшим отличием: различные начала или концы домов с одной и той же х-координатой объединим в одну точку события (см. также подраздел 5.4.1). Как обычно, отсортируем точки событий и пронумеруем по порядку промежутки между точками событий. По определению точек событий внутри каждого такого промежутка высота линии горизонта постоянна.
Линию горизонта будем строить в отдельном массиве как последовательность высот на выделенных промежутках. Сначала, пока не рассмотрен ни один дом, считаем, что линия горизонта проходит по земле. Дома обрабатываются в порядке их задания во входном файле. Рассматривая очередной дом, проверяем высоты всех покрываемых этим домом текущих промежутков: если высота текущего промежутка меньше высоты дома, изменяем высоту промежутка, иначе не трогаем ее. Пример работы по этому алгоритму приведен в табл. 7.1.
Таблица 7.1. Построение горизонта для примера из условия
Номер промежутка	1	2	3	4	5	6	^7	8
х-координаты	1-20	20-30	30-41	41-50	50-60	60-80	80-90	90—100
Исходный горизонт	0	0	0	0	0	0	0	0
Обработан 1 -й дом	0	30	0	0	0	|0	0	0
Обработан 2-й дом	10	30	10	0	0	0	0	0
Обработан 3-й дом	10	30	10	10	0	0	0	0
Обработан 4-й дом	10	30	10	10	0	0	10	10
Обработан 5-й дом	10	30	10	0	0	20	20	10

ВЫМЕТАНИЕ
205
После обработки всех домов остается объединить последовательные промежутки одинаковой высоты.
Очевидно, что порядок рассмотрения домов не влияет ни на ответ, ни на количество действий.
Самый тонкий момент в этом алгоритме — способ определения диапазона номеров промежутков, занятых домом. Естественно использовать отсортированный массив точек событий, построенный по х-координатам начал и концов домов.
Для каждого дома можно просто пройти по массиву точек событий, пока не дойдем до точки с номером sp координата которой равна координате начала дома, т.е. первый промежуток, занятый домом, — это промежуток номер sr Затем пройдем по массиву до точки события номер координата которой равна координате конца дома (последний промежуток, занятый домом, — промежуток номер/-1).
Однако для домов, находящихся справа, придется просмотреть почти весь массив точек событий, чтобы просто найти номер начального промежутка, даже если дом занимает всего один-два промежутка. Этого можно избежать, если номер начального промежутка s{. искать по массиву координат точек событий с помощью бинарного поиска.
Разумеется, для каждого дома все равно придется просматривать промежутки от s-ro по (£-1)-й, поэтому при “длинных домах” оптимизация поиска ^дает мало. Однако она позволяет снизить оценку сложности с О(п) в “чистом виде” до O(nfe'), где к' — средняя длина дома, измеренная в количестве промежутков. Нетрудно убедиться, что обычно к'~2к, где к — среднее количество активных домов, поэтому работа программы в среднем ускоряется.
►► Реализуйте приведенный алгоритм. Убедитесь в следующем: если большинство домов длинные, применение бинарного поиска практически не ускоряет работу, но если длинных домов мало, работа существенно ускоряется.
7.4. Мера объединения треугольников
Задача 7.4. На плоскости заданы треугольники. Найти меру их объединения, I т.е. суммарную площадь всех частей плоскости, покрытых хотя бы одним тре- I угольником; части, покрытые несколькими треугольниками, учитываются один раз. Неточностями вычислений в стандартных действительных типах можно пренебречь.
Вход. Первая строка текста содержит число п (2^ п < 103; для DOS-реализаций л £ 100) — количество треугольников, затем каждая из п строк — по шесть координат трех вершин треугольника (х- и у-координаты первой вершины, затем второй и третьей). Координаты задаются как числа с плавающей точкой.
Выход. Строка с числом — мерой объединения.
Пример
Вход 2
Выход 15.5
0 0 6 0 -2 6 0-1216-1
Анализ задачи. Несомненно, точками событий должны быть все вершины треугольников. Но не только: в примере из условия на промежутке (в вертикальной по-
ГЛАВА 7
лосе между х=0 и х=2 вершин треугольников нет, однако между х= 1 и х=2 два
треугольника пересекаются, а между х—0 и х— 1 — нет.
Ситуация в вертикальных полосах изменяется из-за пересечения сторон тре-
тьников. Поэтому естественно считать точками событий все вершины и все точка
пересечений сторон треугольников. Тогда промежутки от точки события до точки события (вертикальные полосы) содержат только треугольники и трапеции, не имею-
«е общих областей ненулевой площади (пересечения по отрезкам или точкам не-
hi
ественны). Треугольники и трапеции для примера входных данных из условия
изображены на рис. 7.3.
(-2,4)
Рис, 7.3. Треугольники и трапеции для примера входных данных
Площадь объединения непересекающихся фигур равна сумме их площадей. Площадь трапеции равна половине произведения суммы оснований на высоту; площадь треугольника— половине произведения основания на высоту (треугольник можно считать частным случаем трапеции, у которой одно из оснований имеет длину 0). Высота всех фигур одной вертикальной полосы равна ширине этой полосы. Вынеся высоту за скобки, получим, что суммарная площадь фигур одной полосы равна
И2(Общая высота)(Сумма длин всех “левых” и всех “правых” оснований).
Нетрудно также заметить, что каждую (отдельно “левую”, отдельно “правую”) сумму длин оснований можно вычислить как меру объединения отрезков (см. раздел 7.2). Например, будем вычислять сумму длин оснований (для точки события х=х), двигаясь снизу вверх. При этом каждое пересечение нижней стороны треугольника с х=х. соответствует началу отрезка, верхней — концу.
Строго вертикальные стороны треугольников можно игнорировать; площадь соответствующего треугольника все равно будет учтена за счет анализа двух других его сторон.
Таким образом, статус выметания целесообразно представлять как последовательность (при движении снизу вверх) пересечений с невертикальными сторонами треугольников (учитывая для каждой стороны, верхняя она или нижняя).
Решение задачи. Итак, точки событий — это и вершины треугольников, и пересечения сторон. Однако возможна ситуация, когда почти все треугольники взаимно пересекаются. В худшем случае каждая сторона треугольника пересекает по две сто
ВЫМЕТАНИЕ
207
роны остальных треугольников, давая 6(л-1) точек пересечения. Сумма по всем треугольникам дает 6п(п-1) точек пересечения. Каждая точка при этом учтена дважды, т.е. всего точек пересечения не больше Зп(п-1), или О(и2).
Хранить все точки в оперативной памяти проблематично. Поэтому вершины треугольников будем считать гарантированными точками событий, запоминая и сортируя их, а точки пересечений сторон — динамическими; их будем вычислять и обрабатывать, не запоминая.
В реализации используем следующие структуры данных:
•	VERTICE — набор всех вершин треугольников; каждая вершина задается записью с полями х и у;
•	EDGES — набор всех ребер, т.е. сторон треугольников; для каждого ребра хранятся его концы (индексы в наборе vertice) и тип (верхнее-нижнее). Вертикальные стороны в этот набор не включаются;
•	status статус выметания, т.е. последовательность номеров ребер (в наборе EDGES), полученная при движении снизу вверх;
• evjpnts— последовательность гарантированных точек событий (вершин треугольников). Точка события характеризуется такими данными: р — номер вершины в наборе VERTICE; е — номер ребра в массиве EDGES; kind — признак начала (+1) или окончания (-1) ребра в этой вершине.
Последовательности и наборы (VERTICE, EDGES, status_this, status_next, ev_pnts) будем хранить в виде записей с полями num (количество элементов) и а (собственно массив элементов).
Собственно выметание реализуется в следующем фрагменте.
var ev_p_i : integer; { номер гарантированной точки события }
curr_x, I х-координата текущей вершины }
next_x, { х-координата следующей вершины }
cro8S_х : extended; { х-координата ближайшего пересечения ребер }
ml_left, ml_right : extended; { меры вертикальных отрезков, проходящих через соседние точки событий } cross__idx, t : integer; { используются для перестановок ребер в статусе при нахождении пересечений }
RES—SQUARE : extended; { ответ -- площадь объединения треугольников в выметенной части }
RES_SQUARE := 0;
ev_p__i : = 1 ;
status.num := 0;
curr_x := VERTICE. a [ev_jpnts . a [1] .p] .x;
repeat { инициализируем статус, обрабатывая ВСЕ точки событий с минимальной х-координатой }
InsertEdge (status, evjints.a [ev_p_i] .e, curr_x) ;
inc (ev_p_i) ;
until VERTICE.a [ev_jonts.a [ev_p_i] .p] .x > curr_x;
while ev_p_i <= ev_jonts.num do begin
{ пока есть точки событий }
ml_left := LinearMeasure(status, curr_x);
жя
ГЛАВА 7
nextx *= VERTICE. a [evjpnts .a [ev_p_i] .р] .х;
crossjc := NearestCross(status, curr_x, next_x, cross_idx); находим ближайшую к curr_x точку пересечения ребер статуса; функция NearestCross возвращает х-координату пересечения как результат, а через параметр-переменную cross_idx --позицию в статусе, в которой произошло пересечение }
while cross_x < next_x do begin
{ если пересечение левее следующей гарантированной точки события, изменяем порядок ребер в статусе и вычисляем площадь в полосе от curr_x до cross__x }
t := status.a[cross_idx];
status.a[cross_idx] := status.a[cross_idx-l];
status.a[cross_idx-l] := t;
ml_right := LinearMeasure(status, cross_x);
RESJSQUARE := RESJSQUARE +
(ml_left + ml_right)*(cross_x - curr_x)/2;
curr_x := cross_x; { площадь левее cross_x уже вычислена, } ml_left := ml_right; { далее‘нужно будет вычислять только в полосе между cross_x и next_x }
cross_x := NearestCross(status, curr_x, next_x, cross_idx) ; end;
ml_right := LinearMeasure(status, next_x);
RESJSQUARE :« RESJSQUARE +
(ml_left + ml_right)*(nextjc - curr_x)/2;
{ вычисляем площадь в полосе от последней обработанной точки события (гарантированной или динамической) до next_x }
while (ev_jo_i <= ev_pnts.num) and
(VERTICE.a[ev_pnts.a[evjo_i].p].x = next_x) do begin
{ модифицируем статус, обрабатывая ВСЕ точки событий с х-координатой next_x }
if ev_pnts.a[ev_p_i] .kind = -1 then
DeleteEdge(status, events, a[ev_p_i].e) else
InsertEdge(status, evjpnts.a[evjp_i].e, next_x);
inc(ev_p_i);
end;
curr_x :® next_х;
end;
►► Реализуйте программу полностью. Нужно объявить подпрограммы, используемые в данном фрагменте, добавить “интеллектуальный” ввод данных (который разделял бы стороны треугольников на верхние и нижние, а концы этих сторон — на левые и правые) и сортировку точек событий.
Анализ решения. Обозначим количество точек пересечений сторон треугольников через к, к=О(п2). Текущее (и среднее) количество ребер в статусе т имеет оценку О(п). Общее количество точек событий равно п+к, а обработка каждой из них требует О(т) действий, поэтому сложность приведенного алгоритма составляет О(т(п+к)) = О(п). Это существенно меньше, чем экспоненциальная в худшем случае оценка, которая получилась бы, если использовать принцип включений и исключений (подробности — в главе 10).
ВЫМЕТАНИЕ
209
♦ Не исключено, что данная задача допускает более эффективные реализации выметания. Например, можно попытаться ускорить нахождение координаты cross_x, учитывая предыдущие результаты поиска. Однако попытка реализовать эту идею показала, что скорость увеличилась незначительно, но влияние погрешностей вычислений с плавающей точкой недопустимо возросло. Причем настолько, что из-за неточных значений координат оптимизированный алгоритм иногда неправильно определял порядок точек событий и считал площадь совершенно неправильно. Приведенная же реализация не будет настолько “хрупкой”.
Упражнения
7.1.	Реализуйте решение задачи “Интернет-провайдер^’ с оценками количества действий O(Klog2V), объема оперативной памяти 0(7V).
7.2.	Решите задачу “Интернет-провайдер”, держа одновременно открытыми не более четырех файлов. Оценка количества действий должна быть 0(NK), объема оперативной памяти — 0(2V).,
7.3.	Реализуйте по возможности эффективную программу, решающую задачу “мера объединения отрезков” с помощью явного построения объединения. Сравните эффективность вашего алгоритма и алгоритма, использующего выметание, на различных типах входных данных: отрезки короткие и почти не пересекаются, отрезки длинные и мера пересечения очень велика, что-то среднее между этими крайними ситуациями.
7.4.	На координатной прямой заданы и точек. Найти размещение отрезка длиной L на этой прямой, при котором отрезок накрывает максимально возможное количество точек. Сложность подпрограммы должна быть O(nlogn).
7.5.	Вычислить площадь объединения прямоугольников на плоскости, стороны которых параллельны координатным осям. Вид входных данных уточните самостоятельно,
7.6.	Из деревянных палочек одинаковой очень малой толщины сделан жесткий выпуклый многоугольник. Он размещен в вертикальной плоскости так, что опирается одной из своих сторон (назовем ее основанием) на дно сосуда, у-координата которого равна 0. В сосуд медленно наливают воду и на погруженную часть многоугольника начинает действовать архимедова сила. Когда достигается некоторый критический уровень воды, многоугольник отрывается от дна и всплывает.
Основание многоугольника достаточно велико, чтобы он не опрокидывался в процессе доливания воды, но не настолько, чтобы он плавал, когда водой накрыто только основание.
Напишите программу, которая находит критический уровень воды.
Вход, Первая строка текста содержит количество вершин многоугольника N (3 ^N< 500). Каждая из следующих N строк — по два числа: х- и у-координаты вершин (в порядке обхода по часовой стрелке, начиная с левой вершины основания). Значения координат действительные и по модулю не больше 106. Последняя строка содержит число р— плотность древесины (0,3<р<0,98). Плотность воды считается равной 1.
ГЛАВА 7
Выход. Одно действительное число с точностью не меньше двух знаков после запятой.
Пример
Вход	Выход Иллюстрация
Глава 8
Г рафы
В этой главе...
♦	Графы и спосдбы их представления в программах
♦	Работа со списочным представлением графа
♦	Обходы вершин графа в глубину и в ширину
♦	Алгоритмы, основанные на обходах графов
ф Построение остовного дерева и остовного леса
ф Вычисление расстояний между вершинами
>	Проверка ацикличности и топологическая сортировка орграфов
ф Проверка эйлеровости графа
Графы — это модели систем, образованных из элементов и связей между ними, например, городов и дорог или узлов компьютерной сети и связей между ними. Эффективные алгоритмы работы с графами имеют большое практическое значение. В этой главе представлены простейшие задачи и алгоритмы обработки графов.
Отметим, что графы иногда возникают в задачах, в которых, на первый взгляд, никаких элементов и связей между ними нет (например, задача 8.6 и весь первый раздел Следующей главы).
В этой главе для записи алгоритмов используется псевдокод на основе операторов языка Паскаль, к которым добавлен такой оператор:
for (х € X) do оператор
Оператор выполняется для всех элементов х из множества^. Порядок перебора элементов определяется способом представления множества.
8.1.	Графы и способы их представления
8.1.1.	Неориентированные графы: основные понятия
Неформально граф выглядит как диаграмма, т.е. конечное множество точек плоскости {вершин, узлов), соединенных между собой линиями (ребрами) — связями между вершинами.
Петлей называется ребро, соединяющее вершину саму с собой. Ребра, соединяющие одну и ту же пару вершин, называются кратными. Под простым графом обычно понимают граф, в котором нет ни петель, ни кратных ребер. В мулътиграфе могут быть кратные ребра, но петли не допускаются. Если есть и петли, и кратные
212
ГЛАВА 8
ребра, говорят о псевдографе. Примеры графов этих трех видов представлены на рис 8.1, а—в. Ниже под словом граф, как правило, будем понимать простой граф.
а) псевдограф	б) мультиграф	в) граф	г) орграф
Рис. 8.1. Виды графов
Уточним понятие графа. Пусть V — непустое конечное множество, V(2> — множество всех двухэлементных подмножеств множества V, а Е — произвольное подмножество Пара (V.E) называется (неориентированным) графом, элементы множества V — вершинами, или узлами, а элементы множества Е—ребрами.
Ребра будем обозначать в виде {v, w}, где v и vy — вершины. Граф с п вершинами и т ребрами называется (п.т)-графом. Пусть e={v, w} — ребро графа. Вершины у и w называются концами ребра е. а также смежными между собой и инцидентными ребру е.
Степень вершины v в графе G — это число инцидентных ей ребер, обозначаемое 5(у). Множество вершин, смежных с у, будем обозначать N(v). Очевидно, что для простых графов |У(у)|=8(у). Вершина степени 0 называется изолированной, а степени 1 — концевой. или висячей.
Полезно помнить следующее очевидное утверждение.
Лемма о рукопожатиях. Сумма степеней вершин равна удвоенному числу ребер.
С помощью этой леммы, в частности, нетрудно убедиться, что число вершин графа, имеющих нечетную степень, обязательно четно.
Рассмотрим понятия, связанные с переходами между вершинами по ребрам графа.
Маршрут в графе — это последовательность вершин и ребер вида (v0, ev ур уя_р
ея,уя), где у0, ..., vn — вершины, ^{y^vj, 1< i<n. — ребра. Указанный маршрут соединяет вершины v0 и уя. В графе без кратных ребер маршрут, как правило, обозначают как последовательность вершин (vft, v., ..., v ). Длина маршрута — это число ре-
Хл	Л	W
бер в нем, т.е. маршрут (v0, ур ..., уя) имеет длину п. Маршрут, в котором вершины не повторяются, называется простым. Маршрут, образованный одной вершиной, имеет длину 0 и называется тривиальным.
Если для двух вершин графа существует путь с концами в этих вершинах, они называются связанными. Граф называется связным, если любые две его вершины связа
к
ны. Граф, не являющийся связным (несвязный), состоит из ^нескольких компонент
связности. Компоненту связности (КС) образует подмножество связанных между со
бой вершин (с инцидентными им ребрами), ни одна из которых не связана ни с од
ной вершиной вне этого подмножества.
Маршрут (v0, vp ...,vn) называется замкнутым, если у0=уя. Нетривиальный замкнутый маршрут, в котором не повторяются ребра, называется циклом. Цикл (у0, ур ... у^, ул) называется простым, если его вершины ур ..., уя_1, уя попарно различны. Граф не имеющий циклов, называется ациклическим.
ГРАФЫ
213
Связный ациклический граф называется деревом. Дерево, которое содержит все вершины и некоторые ребра графа G, называется его остовным деревом.
Деревья имеют два важных свойства. Первое — дерево с п вершинами имеет п-1 ребро. Второе — добавление к дереву любого ребра приводит к появлению цикла. Таким образом, деревья являются связными графами с минимальным числом ребер при данном количестве вершин.
Расстояние между вершинами связного графа — это длина кратчайшего маршрута, их соединяющего. Эксцентриситетом вершины называется максимальное расстояние от нее до других вершин, радиусом графа — минимальный эксцентриситет его вершин, а диаметром — максимальный.
8.1.2.	Ориентированные графы
В некоторых задачах связи между объектами несимметричны, например, в плане проведения работ один вид работ обязательно должен предшествовать другому или по какой-нибудь дороге между перекрестками есть движение только в одну сторону. Такую несимметричность представляют, придавая ребрам графа ориентацию; ребра изображают стрелками и называют дугами. Граф с дугами вместо ребер называется риентированным или орграфом (см. рис. 8.1, г).
Некоторые из понятий, связанных с неориентированными графами, для орграфов изменяют или теряют свой смысл. Дуги в орграфе — это не подмножества, а (упорядоченные) пары, обозначаемые в виде <v, w>. Вершина v называется началом дуги, w — концом. Вершину v еще называют смежной	смежной с v.
Полустепень выхода 3"(v) вершины v — это число дуг, выходящих из нее, полустепень входа 5+(v) — входящих в нее, степень 8(v) — инцидентных ей1. В орграфе без петель, очевидно, 8(v) — 8*(v)+8"(v).
Дуги <v,w> и <w,v> орграфа называются симметричными. Орграф, не имеющий ар симметричных дуг, называется направленным. Пару симметричных дуг между различными вершинами v и w иногда рассматривают как одно ребро.
Маршрут в орграфе проходит по дугам в соответствии с их ориентацией (от начала к концу каждой дуги маршрута) и ведет из первой вершины маршрута в последнюю. Маршрут, в котором вершины и дуги не повторяются, называется путем.
Если в орграфе существует путь с началом в вершине v и концом в w, то w называется достижимой U3V. Длина кратчайшего такого пути называется расстоянием v, vv) от вершины v до вершины w. Отношение достижимости, вообще говоря, не является симметричным, поэтому расстояния d(v, к) и d(w, v) могут отличаться.
Вершину v, из которой не достижима ни одна другая вершина, называют тупика-вой (8"(v)=0), а недостижимой — вершину, в которую нет путей из других вершин 8*(v)=0).
Орграф называется сильно связным, если любые две его вершины достижимы одна жз другой. Соответственно, в компоненте сильной связности любые две вершины достижимы друг из друга и ни одна из них не является взаимно достижимой ни с одной ершиной вне этой компоненты.
1 Полустепени входа и выхода иногда обозначают противоположным образом.
214
ГЛАВА 8
8.1.3.	Представления графа
Рассмотрим несколько способов представления графов и орграфов; они часто влияют на оценку сложности программ, реализующих алгоритмы.
В программах графы представляют с помощью матриц или комбинаций массивов со связанными списками. Выбор представления зависйт от числа вершин и ребер, а также особенностей конкретных алгоритмов и задач. Вершины графа ниже будем представлять целыми числами либо 0,1,2, ..., либо 1,2,3,....
Графу с п вершинами 0, 1, ..., п-1 соответствует матрица смежности А размерами пхп: А. .= 1, если вершины i и j смежны, иначе Atf=0 (в частности, Ай=0). Для мультиграфов Дч — это число ребер, соединяющих вершины i и j. Для орграфов Ау= 1, если дуга имеет начало i и конец у, иначе Ау.=0. Например, трафам на рис. 8.1 соответствуют следующие матрицы смежности. Буквам А, В, С, D на рис. 8.1, а, б соответствуют номера строк и столбцов матриц 0,1, 2, 3.
г0 1 О Р 1112 0 111 к1 2 1 0;
2 2 Р
2 0 0 1 2 0 0 1 к1 1 1 °>
л0 1 1 1 Р 10 110 110 0 0 110 0 0
J 0 0 0 0;
г0 1 0 Р 0 0 10 10 0 1 к0 о о 0;
Список ребер графа — это множество S- {{v., у.}, {vpvj}, образованное парами смежных вершин. Обычно это представление используют для хранения графа во внешней памяти и преобразуют в другое для его обработки. Например, граф на рис. 8.1, в представляется списком смежности {{0,1}, {0,2}, {0,3}, {0,4}, {1,2}, {1,3}} Списки смежности для орграфов определяются аналогично.
Структура смежности для графа с п вершинами образуется массивом или списком из п элементов, соответствующих вершинам. Каждый элемент массива или списка содержит указатель на список номеров вершин, смежных с “его” вершиной. Каждая дуга орграфа представлена в этой структуре один раз, каждое ребро графа — дважды. Например, графам на рис. 8.1, в, г соответствуют структуры смежности на рис. 8.2.
а) граф	а) орграф
Рис, 8.2. Структуры смежности графа и орграфа
Не стоит думать, что массив списков и список списков взаимозаменяемы. Как увидим дальше, во многих алгоритмах необходимо быстро (прямым доступом) обращаться к вершинам по их номерам, а при “обыкновенном” использовании списков это невозможно.
ГРАФЫ
215
Каждый способ представления графов имеет свои достоинства и недостатки. Например, чтобы узнать, есть ли ребро между вершинами i и j, в матрице смежности достаточно обратиться к элементу А., а в структуре смежности нужно искать j в списке соседей i. Но если нужно перечислить все вершины, смежные с i, то в структуре смежности достаточно иройти по готовому списку именно этих вершин, а в матрице смежности придется перебирать все вершины графа и для каждой проверять, смежна ли она с i.
Подобных примеров можно привести немало, но вместо них сразу сформулируем выводы. При работе с большими разреженными графами (у них количества вершин и ребер — величины одного порядка) обычно выигрывают структуры смежности. При работе с насыщенными графами, содержащими “рочти все” возможные ребра, — матрицы смежности. Причем в обеих ситуациях выигрыш обычно достигается одновременно как по скорости работы, так и по объему памяти.
•	Отметим без доказательства, что если граф можно изобразить на плоскости так, чтобы его ребра не пересекались (такие графы называют планарными), то /п<3и-6. Следовательно, планарные графы являются разреженными.
Существуют и ситуации, в которых способ представления графа определяется самим алгоритмом. Например, алгоритму Флойда—Уоршалла (подраздел 8.3.2) нужна матрица смежности, а алгоритму Краскала (раздел 9.2) — список ребер.
Учтем также, что перед решением практически всех задач обработки графов по внешнему представлению графа нужно строить внутреннее, а после решения — наоборот. Например, построение матрицы смежности независимо от истинного числа ребер требует 0(л2) времени и сводит на нет эффективность алгоритмов, сложность которых меньше 0(п2).
•	Выбирая способ представления графа, необходимо учитывать особенности задачи и алгоритмы ее решения, а также, какие графы, близкие к разреженным или к насыщенным, типичнее для задачи.
8.1.4.	Пример: задача о центре дерева
На следующем примере рассмотрим -использование списочного представления графа.
Задача 8.1. В офисе фирмы Megasoft установлены У компьютеров с номерами от 1 до N, некоторые из них соединены между собой. Сообщение между соединенными компьютерами проходит в любом из двух направлений за 1 с. Компьютер, получив сообщение, сразу отправляет его всем соединенным с ним компьютерам. Компьютерная сеть устроена так, что между любыми двумя компьютерами есть путь, причем только один.
Найти номера всех компьютеров, с которых главный программист Гилл Бейтс может отправить сообщение, чтобы максимальная задержка в по. учении сообщения была как можно меньше.
Вход. Количество компьютеров п (1< л <10000) и п-1 пара чисел, обозначающая соединение.
Выход. Номера искомых компьютеров (в порядке возрастания).
Пример. Вход'. 4 1 2 4 3 2 3. Выход: 2 3.
216
ГЛАВА 8
Анализ задачи. Компьютеры —» это вершины (неориентированного) дерева с ребрами одинаковой длины, а время передачи сообщений между компьютерами — это расстояние между вершинами. Однако вычислять расстояния в этой задаче не будем — есть способ получше.
Удалим из дерева одновременно все концевые вершины вместе с ребрами, ведущими к ним. Удаление концевых вершин не нарушает связности, поэтому граф остается деревом. Заметим: любые две вершины, наиболее отдаленные одна от другой в дереве, являются концевыми. Значит, расстояние между вершинами, наиболее отдаленными одна от другой, после удаления концевых вершин уменьшается на 2. Далее операция одновременного удаления концевых вершин повторяется, пока не останется одна вершина (если расстояние между наиболее отдаленными вершинами в исходном дереве четно) или две (если нечетно). Это и есть центр дерева.
Как видим, алгоритм прост, но его реализация требует особого внимания к представлению дерева. Например, при удалении концевой вершины и и ребра {и,У} нужно вычеркнуть и из списка смежности вершины v. Если для этого придется просмотреть список смежности v, оценка сложности сразу станет, как минимум, квадратичной. Значит, нужно обеспечить, чтобы на удаление концевой вершины и ребра, ведущего в нее, тратилось константное количество действий.
Заметим также, что при удалении концевых вершин нужно избежать их поиска среди всех вершин. Значит, перед удалением их нужно как-то “собрать вместе”, а в процессе удалений аналогично “собирать вместе” вершины, которые в результате удалений становятся концевыми.
Решение задачи. Организация данных. Определим структуры данных и переменные программы. Представим дерево с помощью структуры смежности (см. рис. 8.2, а), добавив к элементам списков компоненты, избавляющие при удалении ребер от поиска ребер в списках. Ребро {u, v} дерева представим в двух “симметричных” элементах в списках смежности вершин и и v, содержащих v и и соответственно и указывающих друг на друга. Кроме того, элементы содержат еще по два указателя на следующий и предыдущий элементы в своих списках.
type PNode = ANode;
Node = record
idx : word; {вершины}
edgeCopy, {ссылка на " симметричный11 элемент}
next, prev : PNode
{ссылки на следующий и предыдущий}
end;
Граф представим с помощью массива указателей на списки смежности сдедую-щего типа:
const maxN = 10000;
type TGraph = array [l..maxN] of PNode;
var T : TGraph;
В дальнейшем нам придется изменять порядок значений в массиве Т и одновременно работать с исходными номерами вершин, поэтому, чтобы отслеживать соответствие номеров вершин и списков, используем еще один массив. Индекс в этом массиве — (исходный) номер вершины, а значение элемента — индекс (переставленного) элемента в массиве Т, представляющего ее список смежности.
ГРАФЫ
217
type aldx = array [l..maxN] of word; var NodeIdx : aldx;
♦ Идея “хранить массив индексов, позволяющий быстро переходить от одной нумерации к другой” нам уже знакома. В задаче 5.5 в подразделе 5.4.2 определялся исходный номер по переставленному, а в данной задаче — наоборот.
Наконец, нужны файловая переменная, количество вершин и количество центральных вершин: var f : text;
n f NCentar : word;
Ввод и инициализация данных. В процедуре init, приведенной ниже, вначале создадим “пустые” списки, состоящие из одного элемента, хранящего исходный номер вершины. Этот элемент позволяет по индексу в массиве Т получить исходный номер вершины и значительно упрощает дальнейшую обработку списка. В частности, при удалении ребра всегда удаляется непервый элемент списка смежности. Индексы в массиве Т вначале совпадают с исходными номерами вершин. Собственно добавление ребра {/, Л} выполняет вызов процедуры addEdge (j / к), представленной ниже, procedure init(var n : word);
var i, j, к : word;
begin
assign(f, 1tree2.txt'); reset(f);
readlnff, n);
for i := 1 to n do begin
new(T [i] ) ;
T[i]A.idx := i;
T[i]A.next := nil;
T[i]A.prev := nil;
T[i]A.edgeCopy := nil;
Nodeldx[i] := i;
end;
for i ;= 1 to n-1 do begin
readlnff, j , k);
addEdge(j, k) ;
end;
end;
Добавление ребра состоит в том, что создаются два новых элемента типа Node, в них записываются номера вершин и ссылки друг на друга. Собственно вставку элемента в список выполняет процедура insNode.
procedure insNode(р : PNode; i ; word);
var pp : PNode;
begin
pp := T[i]A.next;
T [i]A.next := p;
pA.next := pp;
pA.prev := T[i];
if pp <> nil then ppA-prev := p;
218
ГЛАВА 8
procedure addEdge(j, к : word);
var pl, p2 : PNode;
begin
j заносится в список смежности вершины к, к—в список j}
new(pl); new(p2);
plA.idx := k;
p2A.idx := j;
pl*.edgeCopy := p2;
p2 *.edgeCopy := pl;
insNode(pl, j);
insNode(p2, k) ;
end;
Для проверки правильности построенной структуры можно применить Следующую процедуру printTree печати содержимого списков. Она не нужна в окончательной программе, но может оказаться полезной при отладке.
procedure printTree(n : word);
var i ; word; p : PNode;
begin
for i := 1 to n do begin
write(T[i]*.idx, ':');
p := T[i]*.next;
while(p <> nil) do begin
write(' 1 , p*.idx); p := pA.next
end;
writein;
end;
end;
Реализация основного алгоритма. Наша цель — добиться, чтобы каждая кбнцевая вершина удалялась за константу действий. Для этого нужно избежать поиска концевых вершин и поиска в списке элемента, представляющего удаляемое ребро.
Для начала за один проход по массиву Т упорядочим его так, чтобы все концевые вершины были представлены его первыми элементами. После этого вершины можно удалить также за один проход по этим первым элементам. Список смежности концевой вершины и состоит из одного элемента и он указывает на элемент в списке вершины v, хранящий и, поэтому ребро {м, v} удаляется из обоих списков без каких-либо проходов по спискам, т.е. с помощью константного количества действий.
После этих удалений некоторые прежде неконцевые вершины становятся концевыми. Удалив ребро из списка неконцевой вершины, можно сразу выяснить, стала ли она концевой. Если стала, поменяем ее местами с первой (в массиве Т) и неконцевых вершин. Таким образом, исключив на очередном шаге все концевые вершины, получим, что концевые вершины для следующего этапа представлены в массиве т элементами, идущими подряд.
Итак, первичную перестановку вершин опишем в следующей процедуре reorder При ее выполнении определяется индекс firstNL первого элемента в массиве Т представляющего неконцевую вершину.
procedure reorder(n : word; var firstNL : word);
var i : word;
ГРАФЫ
219
begin
firstNL := 1; {пропустим первые концевые вершины}
while (firstNL < n) and
(T[firstNL]A.nextA.next = nil)
do inc(firstNL);
for i := firatNL+1 to n do begin
if T[i]*.next*.next = nil then
begin {концевая и первая неконцевая вершины} exchNode(firstNL, i); {меняются местами} inc(firstNL);
end;
end; end;
Обмен вершин местами (значений элементов в массивах Т и NodeIdx) реализуем с помощью следующих очевидных процедур2.
procedure exchPNdfvar х, у : PNode);
var t : PNode;
begin t := x; x := у; у := t end;
procedure exchldx(var x, у : word);
var t : word;
begin t : = x; x := у; у := t end;
procedure exchNode(j, k : word);
begin
exchPNd(T[j] , T[k]) ;
exchldx (Nodeldx [T [ j ] *. idx] , Nodeldx [T [k] *. idx] ) ; end;
Удаление элемента, представленного указателем р, из списка смежности выполняет следующая функция de INode. Она возвращает признак того, что после удаления список смежности состоит из одного элемента. С помощью флажков isFirst и isLast запоминаем, был ли удаляемый элемент первым или последним в своем списке, и определяем, стала ли вершина концевой.
function deiNode(р : PNode) : boolean;
var ppr, pnx : PNode;
isFirst, isLast : boolean;
begin
deiNode := false;
pnx := p*.next;
ppr := p*.prev;
isFirst := ppr*.prev = nil; {первый в списке}
ppr*.next := pnx;
if pnx <> nil then
begin pnx*.prev ;= ppr; isLast := false; end
else isLast := true; {последний в списке}
{условия того, что вершина стала концевой}
2 В C++, используя шаблон (template), можно обойтись одной подпрограммой. Паскаль экого, увы, не позволяет (по крайней мере, пока).
ГЛАВА 8
deiNode := (isFirst and (pnxA.next = nil)) or
(isLast and (pprA.prevA.prev » nil));
dispose(p);
end;
Основной алгоритм реализуем следующей процедурой run, вычисляющей количество центральных вершин NCenter (1 или 2):
procedure run(n : word; var NCenter ; word);
var firstNL, {индекс первой неконцевой вершины} firstLf, {индекс первой концевой вершины} NoldLvs, {количество удаляемых концевых вершин} NnewLvs, {количество появившихся концевых вершин} к,	{исходный номер неконцевой вершины}
i : word; {индекс удаляемой вершины в Т} р : PNode; {указатель удаляемого элемента}
/begin
reorder(n, firstNL);
Ncenter := n; firstLf := 1;
{итерации этапов удаления концевых вершин}
while Ncenter > 2 do begin
NoldLvs := firstNL - firstLf;
NnewLvs i= 0;
for i := firstLf to firstNL-1 do begin
k := T[i]A.nextA.idx;
p := T[i]A.nextA.EdgeCopy;
if deiNode(p) then begin inc(NnewLvs);
exchNode(Nodeldx[k], firstNL);
inc(firstNL);
end;
end;
inc(firstLf, NoldLvs);
dec(Ncenter, NoldLvs);
end; end;
Результаты выводит процедура done.
procedure done(n, NCenter : word);
begin
write(NCenter, 1 1);
if Ncenter = 1 then write(T[n]A.idx) else
if T[n]A.idx < T[n-l]A.idx
then write(T[n]A.idx, 1 T[n-1]A.idx)
else write(T[n-1]A.idx, 1 J, T[n]A.idx); end;
Наконец, тело программы имеет следующий вид:
begin
init(n);
run(n, NCenter);
done(n, NCenter); end.
ГРАФЫ
221
Анализ решения. Ввод и первичная перестановка вершин имеют очевидную оценку сложности 0(и). В процедуре run происходит несколько итераций, но в сумме массив т проходится один раз, а обработка каждой концевой вершины и удаление инцидентного ей ребра имеют сложность 0(1). Это и обеспечивает общую оценку сложности 0(и).
н Восстановите полный текст программы.
8.2.	Алгоритмы обхода графов
Пройти по графу, просмотреть граф — это не совсем элементарная задача. Из вершин исходят по несколько ребер, поэтому обход графа может “расползаться в разные стороны”... Итак, чтобы уметь правильно обойти произвольный граф, нужно знать методы обхода.
Неформально говоря, поиск в графе — это алгоритмический метод обхода графа, гарантирующий, что все вершины и ребра будут рассмотрены и при этом не слишком большое число раз.	*
Стандартными являются два метода — поиск в глубину (DFS — Depth First Search) и поиск в ширину (BFS — Breadth First Search). Основная идея поиска в глубину — когда возможные пути по ребрам, выходящим из вершины, разветвляются, нужно сначала полностью исследовать одну ветку и только потом переходить к другим веткам (если они останутся нерассмотренными).
При поиске в ширину сначала исследуются все вершины, смежные с начальной вершиной (из нее в них идут ребра или дуги). Эти вершины находятся на расстоянии от начальной. Затем исследуются все вершины на расстоянии 2 от начальной, затем все на расстоянии 3 и т.д. Заметим: при этом для каждой вершины сразу находится длина кратчайшего маршрута от начальной вершины.
Алгоритмы обхода в глубину и в ширину лежат в основе решения различных задач обработки графов, например построения остовного леса, проверки связности, ацикличности, вычисления расстояний между вершинами и других.
Алгоритмы данного раздела применимы также к орграфам; вместо ребер рассматривают дуги, а отношение достижимости вершин не симметрично.
8.2.1.	Обход в глубину
Вначале рассмотрим связные графы. Обход в глубину традиционно задается рекурсивным алгоритмом dfs (depth-first search — поиск первого в глубину). Вершина, с которой начинается обход, называется начальной. Через F обозначим список вершин, который строится в порядке их обхода. Вначале он пуст, что обозначим как о, а затем вершины добавляются в его конец. Представление множеств и списков пока не уточняем.
Обход связного графа в глубину (рекурсивный алгоритм)
Вход. Неориентированный связный граф G = (V,£) с неотмеченными вершинами.
Выход. Список вершин F в порядке их посещения.
Алгоритм dfs (v).
{1} отметить v;
{2} добавить v к F;
ГЛАВА 8
{3} for (w e N(v)) do
4} if v не отмечена then 5} dfs (w) .
Пример 8.1. Рассмотрим применение алгоритма df s к графу, изображенному на рис. 8.1,0, с множеством ребер {{0,1}, {0,2}, {0,3}, {0,4}, {1,2}, {1,3}} и начальной вершиной 0. Пусть вершины множества	в заголовке цикла for обрабатываются
в порядке возрастания номеров. Применение алгоритма dfs(O) порождает следующую последовательность рекурсивных вызовов; dfs(l) при обработке N(0), dfs(2) при обработке Ml), dfs(3) при обработке МО), dfs(4) при обработке МО). Результатом является список F=<0,1, 2, 3,4>. 
► Докажем, что сложность алгоритма df s для связного (л,т)-графа имеет оценку 0(т). Граф связен, поэтому m> п-1. Очевидно, что сложность алгоритма определяется общим количеством выполнений тела цикла. Вызовы df s происходят только для неотмеченных вершин, при каждом вызове вершина-аргумент отмечается, поэтому повторных вызовов для вершины нет. Тогда общее количество выполнений тела цикла является суммой количеств элементов в множествах №(у), т.е. 0(т). ◄
Рассмотрим, как структуры данных для представления графа влияют на сложность реализации алгоритма. Предположим, граф представлен матрицей смежности. Сложность ее заполнения — 0(п2). Кроме того, реализация заголовка цикла for (wgN (v) ) do требует просмотра всей строки матрицы, что при суммировании по всем вершинам также имеет оценку 0(и2). Сложность реализации остальных операций имеет меньшую оценку. Итак, оценка сложности является квадратичной не зависит от числа ребер и не совпадает со сложностью алгоритма.
Используем д ля представления ребер структуру смежности. Сложность ее заполнения имеет оценку 0(т). Отметки вершин представим с помощью массива, индексированного вершинами. Тогда отметка всех вершин (строка 1 алгоритма) требует 0(и) элементарных действий, поскольку процедура df s с одной и той же вершиной в качестве аргумента вызывается один раз. Список F представим связным списком с указателями на начало и конец, чтобы сформировать его, добавляя номера вершин в конце, за 0(и) времени.
Перебор вершин в каждом списке смежности (заголовок в строке 3) выполняется за 0(5(v)), проверка, отмечена ли вершина, — за 0(1). Сумма 5(v) по всем v равна 2?и поэтому циклы в строках 3-5 при выполнении всех рекурсивных вызовов имеют сложность 0(ти). Итак, суммарная сложность имеет оценку 0(п)+0(т), что для связных графов равно 0(/п), т.е. совпадает с оценкой для алгоритма.
Для обхода произвольных, не обязательно связных, графов достаточно применить алгоритм dfs ко всем вершинам графа, не отмеченным после предыдущих применений. Эти вершины становятся начальными в своих компонентах связности Для реализации алгоритма нужны те же структуры данных, что и для алгоритма df s
Обход произвольного графа в глубину
Вход, Неориентированный граф G = (V, Е).
Выход, Список вершин F в порядке их посещения.
Алгоритм
{1} for (v е V) do установить v как неотмеченную;
{2} F := о;
ГРАФЫ
223
{3} for (v e V) do
{4} if v не отмечена then { 5	df s (v) .
Сложность алгоритма очевидна: 0(n) плюс суммарная сложность обхода компонент связности, т.е. 0(л)+ 0(т) = в(п+т).
При обходе в глубину можно не только добавлять вершины к списку F, а присваивать им последовательные номера (нумеровать их). Из возможных нумераций выделим прямую и обратную.
Начало нумерации задается в алгоритме обхода произвольного графа в глубину — в строке 2 после йли вместо F : = < > записывается к : = -1.
Прямая нумерация получается, если в алгоритме df s в строке 2 после или вместо “добавить v к F” записать
к := к+1; присвоить v номер к.
Таким образом, вершина v получает меньший номер, чем все вершины w, для которых при обработке v происходит рекурсивный вызов df s (w).
Обратная нумерация получается, если вершина v нумеруется не в начале ее обработки (строка 2 в алгоритме df s), а в конце. В строке 2 алгоритма df s сохраним действие “добавить v к F”, а после строки 5 добавим
к := к+1; присвоить v номер к.
Тогда номер вершины v больше номеров всех вершин w, для которых происходит рекурсивный вызов df s (w) .
В примере 8.1 вершины 0-4 по прямой нумерации получают номера соответственно 0-4, а по обратной — 4-0.
Нерекурсивный вариант алгоритма. Вначале рассмотрим обход связного графа. Используем магазин S: вершины добавляются к S и удаляются из него с помощью процедур Push и Pop (процедуры не уточняем).
Обход связного графа в глубину (нерекурсивный алгоритм)
Вход. Неориентированный связный граф G = (V, Е) и начальная вершина v0.
Выход. Список вершин F в порядке их посещения.
Алгоритм
1} S := о; F := о;
2} for (v g V) do установить v как неотмеченную;
3} Push(S, v0) ; отметить v0; добавить v0 к F;
4} while S / <> do begin
5} Pop (S, v) ;
6} for (w e N (v)) do
7}	if w не отмечена then begin
8}	Push(S, w) ; отметить w; добавить w к F;
9	end
10} end
j Приведенный алгоритм нетрудно обобщить для обхода произвольного графа. Естественно, здесь начальная вершина не задается. Вершина, с которой начинается обход компоненты связности, становится начальной в своей компоненте.
224
ГЛАВА 8
Обход произвольного графа в глубину (нерекурсивный алгоритм)
Вход. Неориентированный граф G = (V, Е).
Выход. Список вершин F в порядке их посещения.
Алгоритм
{ 1} S := о; F := о;
{ 2} for (v g V) do установить v как неотмеченную;
{ 3} for (v G V) do begin
{4} if v не отмечена then begin
5} Push(S, v) ; отметить v; добавить v к F;
6} end ;
7}	... { стрбки 4-10 предыдущего алгоритма для связного графа }
{14} end
• Порядки обхода вершин по рекурсивному и нерекурсивному алгоритмам могут не совпадать. Дело в том, что в рекурсивном варианте вершины-соседи рассматриваются в порядке, порождаемом циклом for (we N(v)), а в нерекурсивном они сначала помещаются в магазин (стек), а затем обрабатываются по мере извлечения из него, т.е. в обратном порядке.
8.2.2.	Обход в ширину
Обход в ширину традиционно задается алгоритмом bf s (breadth-first search — поиск первого в ширину). В нем используется вспомогательный список S. Первой отмечается и добавляется к 5 начальная вершина. Когда вершину посещают, она извлекается из 5, а все смежные с ней вершины, которые еще не отмечены, добавляются в конец 5, т.е. S — очередь. Таким образом, после начальной вершины посещаются все ее соседи. Обход заканчивается, когда очередь 5 становится пустой.
Обход вершин связного графа в ширину
Вход. Неориентированный связный граф G = (V, Е). Выход. Список вершин F в порядке их посещения.
Алгоритм bf s
{ 1} for (v G V) do установить v как неотмеченную;
{ 2} S := о; F := <>;
{ 3} выбрать в множестве V начальную вершину v;
{ 4} отметить v; добавить v к S;
{ 5} while S # о do begin
{ 6} извлечь из очереди S ее первую вершину и;
for (w G N (u) ) do
if w не отмечена then begin отметить w; добавить w к S;
end;
добавить u к F;
end.
Очевидно, что результат выполнения алгоритма bf s зависит от порядка, в котором рассматриваются вершины в заголовке цикла for (weN (u)) do (строка 7). Предполо-
жим, что цикл при обходе в ширину выполняется
по возрастанию номеров вершин.
ГРАФЫ
225
Пример 8.2. При обходе графа с множеством ребер {{0,1}, {0,4}, {1,2}, {2,3}, {3,4}} после выполнения строки 4 и нескольких выполнений строки 11 образуются следующие состояния списков 5 и F.
S	
<0>	о
<1,4>	<0>
<4, 2>	<0, 1>
<2, 3>	<0,1,4>
<3>	<0,1,4,2>
о	<0, 1, 4, 2, 3>
Сложность алгоритма bf s для (и, т)-графа также имеет оценку 0(/п). К очереди S
добавляются только неотмеченные вершины, поэтому каждое ребро вида {х,у} рас
сматривается дважды. В первый раз вершина х отмечена и является значением пере
менной и, а у — w. При этом у либо уже отмечена, либо отмечается, поэтому во вто
й раз, когда у является значением u, а х — w, обе они отмечены, и в третий раз реб-
{х, у} рассматриваться не может.
Обход в ширину связного графа обобщается для несвязных графов аналогично об
ходу в глубину.
Для орграфов алгоритмы обхода в глубину и в ширину аналогичны, только вместо ребер рассматриваются дуги.
Чтобы реализация алгоритма сохраняла его оценку сложности, отметки вершин представим в массиве, а ребра — в структуре смежности. Для реализации очереди существует несколько способов (см. работы [10, 21, 40, 43] и другие источники). Два
из них представлены в следующем подразделе.
8.2.3.	Реализация очереди
Для реализации очереди можно использовать массив Q с диапазоном индексов 0. .N-1 и две переменные head и free — индексы головного элемента очереди и первого свободного элемента массива, следующего после нее. Необходимое количество N элементов массива Q определяется, исходя из условия конкретной задачи.
Занесение в очередь самого первого значения задают операторы head: = 0 ;
free : = 1; Q [0] : =дальнейшее включение нового элемента — Q [free] inc (free), удаление головного элемента Q [head] — inc (head). Равенство зна
чений head и free означает, что очередь пуста.
При добавлении элементов будет достигнут конец массива, поэтому, дойдя до онца массива, продолжим его заполнять с начала. Голова к этому моменту уже должна сместиться вправо (разумеется, если максимально возможная длина очереди не больше, чем размер массива). Для “зацикливания” очереди значения head
free вычисляют и хранят по модулю N.
Если нет уверенности, что очередь всегда поместится в массиве, можно ввести
к
[е одну переменную — длину очереди, проверяя ее значение перед добавлением и
удалением и изменяя после них.
226
ГЛАВА 8
При обходе графа в ширину работа с очередью имеет свою специфику. На каждом £-м шаге в нее попадают все вершины, находящиеся на расстоянии к от начальной, и только они. На следующем — все они удаляются, а вслед за последней из них добавляются все вершины на расстоянии А+1, и только они. Таким образом, в любой момент работы в очереди находятся вершины только одного или двух последних “слоев”. Вместо “зацикливания” очереди вершины каждого из “слоев” можно хранить в отдельном массиве, на каждом шаге заполняя один из них с начала.
Используем два массива — SO и S1. Вначале, на “нулевом” шаге, запомним начальную вершину в массиве SO. На очередном it-м шаге (к> 1) рассмотрим уже достигнутые вершины в массиве SO и запомним их еще неотмеченных соседей в массиве S1.
Новые вершины на следующем, (it+l)-M, шаге должны играть роль уже достигнутых, поэтому для подготовки следующего шага нужно скопировать S1 в SO. Однако копировать не обязательно — на каждом шаге достаточно менять местами роли массивов достигнутых и новых вершин. Для этого можно рассмотреть эти массивы как S [ 0 ] и S [ 1 ] и на каждом шаге менять местами роли их индексов. А именно, на к-м шаге (&>1) уже достигнутые вершины хранятся в массиве S [1 - kmod2], а новые запоминаются в массиве S [kmod 2].
Очередь также несложно реализовать с помощью односвязного списка в свободной памяти, представленного с помощью указателя на последний элемент. Последний элемент очереди содержит указатель на первый — это позволяет добавлять элемент в конце очереди и извлекать из ее начала за константное время (см., например работы [10, 31, 40] и другие источники).
8.3.	Применение алгоритмов обхода
8.3.1.	Построение остовного дерева и остовного леса
В связном графе к каждой вершине ведет хотя бы один простой маршрут от начальной вершины v0. В алгоритмах df s и bf s (см. раздел 8.2), отмечая вершину w, используем в качестве отметки P(w) номер уже отмеченной вершины v, из которой мы попадаем в w. Поскольку каждая вершина отмечается один раз, ребра {w,P(w)} {v,P(v)}, ... {u,P(u)}, где P(w)=v0, образуют простой маршрут, ведущий из w в v0. При этом P(w) является непосредственным предшественником вершины w в маршруте, ведущем из Уо в
Простые маршруты из начальной вершины в данную, построенные по алгоритмам df s и bf s, вообще говоря, отличаются3. Рассмотрим алгоритм обхода в ширину bfs. В строках 7-9 для вершины w предыдущей является и, указанная в строках 6, 7,11, т.е. P(w)=u. Например, при обходе в ширину графа с ребрами {{Ъ2}, {1,5}, {2,3}, {2,4}, {3,4}, {4,5}} и начальной вершиной 1 получи^ Р(2)^=Р(5)=1, Р(3)= = Р(4)=2, т.е. к вершине 3 ведет маршрут (1, 2,3)= (Р(Р(3)), Р(3),3), а к 4 — маршрут (1, 2,4)= (Р(Р(4)), Р(4),4) (рис. 8.3, а). При обходе этого же графа по алгоритму df s получится Р(2)=1, Р(3)=2, Р(4)=3, Р(5)=4; например, к вершине 4 ведет маршрут (1, 2, 3,4)= (Р(Р(Р(4))), Р(Р(4)), Р(4),4) (см. рис. 8.3, 6).
3 Они зависят еще и от порядка, в котором обрабатываются списки смежных вершин.
ГРАФЫ
227
а)	б)
Рис. 8.3. Маршруты, ведущие к вершинам
Для реализации указанной разметки вершин уточним действия в алгоритмах dfs bf s. Отметки вершин будем хранить в массиве mark, индексированном номерами вершин 1, 2, п. Действие “установить v как неотмеченную" уточним оператором mark [v] : = -1, условие “v не отмечена" — mark [v] = -1. Начальную вершину отметим 0 (mark [v0J : = 0), а вершину w в списке смежности вершины v отметим номером V —mark [w] :±v.	*
Используем указанную разметку вершин для построения остовного дерева графа. Другие задачи, решаемые с помощью такой разметки, приведены в упражнениях.
Нетрудно убедиться, что множество ребер вида {P(w),w} для всех вершин w, кро-е начальной, полученное после обхода, образует остовное дерево связного графа. Впримере, приведенном выше, обход в ширину дает ребра {1,2}, {1,5}, {2,3}, {2,4}, выделенные на рис. 8.3, а, а обход в глубину — ребра {1,2}, {2,3}, {3,4}, {4,5}, выделенные на рис. 8.3, б.
По алгоритму bf s (см. подраздел 8.2.2) нетрудно получить алгоритм построения остовного дерева связного графа. Для этого достаточно в строке 2 изменить F на Г, дописать в строке 9 оператор добавить {u, w} к Т
удалить строку 11. Это дерево, по существу, имеет корень (начальную вершину) и является ориентированным (если ребра вида {P(w),w} рассматривать как дуги Р(и),и’>).
Построение остовного дерева на основе алгоритма bf s распространяется на произвольные графы в виде построения остовного леса. Структуры данных для реализации алгоритма выбираются, как описано в предыдущем разделе.
Построение остовного леса
Вход. Неориентированный граф G = (V, Е).
Выход. Список ребер Т остовного леса графа.
Алгоритм
1} for (v е V) do установить v как неотмеченную;
2 S := о; Т := о;
3} for (v е V) do
4} if v не отмечена then begin
5} выбрать v как начальную в своем дереве;
6} отметить V; добавить v к S;
228
ГЛАВА 8
while S о do begin
извлечь из списка S его первую вершину и;
for (w G N (и) ) do
if w не отмечена then begin
отметить w; добавить w к S; добавить {u, w} к T; end;
end end
Подчеркнем, что множество ребер Т представляет остовный лес, а окончание цикла while в строках 7-13 означает конец построения очередного остовного дерева с начальной вершиной у. Список 5 при этом пуст.
Пример. Для графа со множеством вершин {1, 2, ...,7}иребер {{1,2}, {1,3}, {2,3 {4,5}, {5,6}, {6,7}, {4,7}} приведенный алгоритм строит остовный лес в виде списка ребер <{1,2}, {1,3}, {4,5}, {4,7}, {5,6}>.
Алгоритм построения остовного леса на основе обхода в глубину получается аналогично— в алгоритме dfs перед рекурсивным вызовом dfs (w) нужно добавить ребро {v, w} к Г.
• Построенное дерево может зависеть от того, какой вариант алгоритма dfs использован — рекурсивный или нерекурсивный (см. подраздел 8.2.1).
8.3.2.	Расстояния между вершинами
Задача 8.2. В офисе фирмы Megasoft установлены п компьютеров с номерами от 1 до и, некоторые из них соединены между собой. В отличие от задачи 8.1, соединения несимметричны, поэтому сообщение может пройти за 1 с только от компьютера-источника к компьютеру-приемнику. Компьютер, ho-лучив сообщение, сразу отправляет его всем присоединенным к нему компьютерам-приемникам. Найти номера всех компьютеров, которые можно сделать центральными, чтобы сообщение от них доходило до всех других компьютеров, а до наиболее отдаленных от них как можно быстрее.
Вход. Количество компьютеров п (3 < п<200), количество соединений т и т пар чисел, обозначающих соединение (источники приемник).
Выход. Номера возможных центральных компьютеров (в порядке возрастания); если ни один компьютер не подходит на роль центрального, выведите 0.
Пример. Вход: 4 3 1 2 3 4 2 3; выход: 1. Вход: 4 3 2 1 4 3 2 3; выход: 0.
Анализ задачи. Ясно, что компьютеры — это вершины орграфа с дугами-соединениями, время передачи сообщений между компьютерами — это расстояние между вершинами. Для решения задачи нужно найти время передачи сообщения от каждого компьютера к каждому, т.е. расстояния между любыми двумя вершинами орграфа. Зная все расстояния, для каждой вершины найдем максимальное расстояние от нее до остальных вершин и затем из этих максимумов выберем минимум. Вершины, на которых достигается этот минимум, и будут искомыми.
Итак, основная проблема — найти все расстояния в орграфе. Для ее решения ис пользуют, как правило, один из двух способов. По первому каждую вершину берут
ГРАФЫ
229
качестве начальной и вычисляют расстояния всех вершин от нее с помощью обхода в ширину. По второму способу вычисляют длины кратчайших путей для всех пар вершин, постепенно добавляя вершины, которые могут быть промежуточными в этих путях.
Первый способ. Это конкретный вариант вычисления расстояний на основе обхода в ширину. Не уточив структур данных для представления орграфа, опишем вычисление расстояний следующим алгоритмом.
Вычисление расстояний от заданной вершины на основе обхода в ширину
----  . _ _ _ . . - - — - . —.	— . - 
Вход. Орграф G = (V, Е) и начальная вершина v0.
Выход. Для каждой вершины v ее расстояние d(y) от v0.
Алгоритм
1} for (v е V) do d(v) : = <»;
2} d(vO) := 0;
{ 3} S := <>;
{4} добавить vO к S;
{ 5} while S * <> do begin
{ 6} извлечь u из S;
7} for (w G N(u)) do
8}	if d(w) = 00 then begin
9}	d(w) := d(u)+l; добавить w к S;
10}	end;
11} end
Очевидно, что сложность данного алгоритма для (м,т)-орграфа имеет оценку О(т). В нашей задаче нужно выбрать в качестве начальной каждую вершину и для нее запустить алгоритм, приведенный выше. Отсюда сложность вычисления всех попарных расстояний между п вершинами равна 0(тп).
Если после выполнения алгоритма для выбранной начальйой вершины остаются
вершины, недостижимые из нее, т.е. имеющие расстояние «>, эта вершина не может
быть центральной и для нее нет смысла искать максимум расстояний до других вер-
in
ян. Если ни одна вершина не может быть центральной, выдается ответ 0. С учетом
этих дополнительных действий сложность описанного решения задачи имеет оцен-
ку O((m+ri)ri).
Второй способ. По этому способу рассматривают все пары вершин и представляют расстояния в виде матрицы D. Значения ее элементов выражают длину кратчай-
in
его простого маршрута между соответствующими вершинами. Непосредственно по
графу известны смежные вершины, т.е. все расстояния длины 1, поэтому вначале
[м'1=0, D[i,j] = l, если различные вершины i и j смежны, D[i,	если несмежны
“оо” представляет отсутствие маршрута и считается большим любого другого значения). Затем матрица D изменяется по следующему алгоритму с очевидной сложностью 0(л3).
Вычисление расстояний с помощью добавления промежуточных вершин
Вход. Орграф G = (V,Е) с вершинами 1,2,..., и в виде матрицыD[L.л, 1..л].
Выход. Матрица D, у которой D[i,j] — это расстояние между вершинами i и j в орграфе.
230
ГЛАВА 8
Алгоритм (Флойда—Уоршалла)
1} for к := 1 to n do
2}	for i := 1 to n do
{3} for j := 1 to n do
{4}	D[i, j] := min(D[i, j], D[i, k]+D[k, j])
Далее в нашей задаче нужно найти максимальные значения в строках измененной матрицы D, не содержащих среди этих максимумов выбрать минимум и номера строк, в которых он достигается. Если в каждой строке есть выдается ответ 0. Сложность описанных дополнительных действий очевидна — 0(и2).
Сравнивая два способа вычисления всех расстояний, видим, что для разреженных графов 0(тп) значительно меньше ОСи3), т.е. n-кратный поиск в ширину в такой ситуации работает быстрее. Но для насыщенных графов алгоритм Флойда— Уоршалла и проще программируется, и работает быстрее. Кроме того, алгоритм Флойда—Уоршалла может находить длины минимальных путей в графах, ребрам или дугам которых приписаны различные веса (подробности — в главе 9), тогда как поиск в ширину в этой ситуации неприменим.
С помощью алгоритма Флойда—Уоршалла решают и некоторые другие задачи, например вычисляют транзитивное замыкание графов или анализируют конечные автоматы (см. работы [22, 24, 30]).
8.3.3.	Проверка ацикличности и топологическая сортировка ациклического орграфа
При строительстве дома есть несколько видов работ; некоторые из них можно выполнять только после завершения других, а некоторые не зависят одна от другой Например, нельзя возводить стены, цока не закончен фундамент, но электротехнические и водопроводные работы можно выполнять одновременно. Таким образом, отношение зависимости частично упорядочивает виды работ. Представив работы вершинами графа, а зависимости между ними — дугами, направленными от более ранних работ к более поздним, получим ациклический орграф4.
Задача 8.3. Дан орграф, возможно, ациклический. Нужно либо подтвердить ацикличность, либо указать любую вершину, принадлежащую какому-нибудь циклу.
Анализ задачи. Нетрудно убедиться в справедливости следующего утверждения, связывающего проверку цикличности с обходом в глубину.
Если в орграфе есть цикл, содержащий вершину v, то при поиске в глубину, запущенном из вершины v, обязательно будет рассмотрена дуга цикла, входящая в у.
Например, обход орграфа на рис. 8.4, начатый в вершине 2, пройдет по вершинам 4, 6 и 8. Затем обход вернется в 4, пройдет 7, 5, 3 и проверит дугу <3,2>. Ясно, что проходить дальше по этой дуге нельзя, а нужно запомнить, что вершина 2 принадлежит циклу.
4 Между работами не может быть “циклической” зависимости u—>v->...—>и, иначе выполнить их невозможно.
ГРАФЫ
231
Рис. 8.4. Дуга цикла при обходе в глубину
Итак, в алгоритме dfs (см. подраздел 8.2.1) Нужно изменить тело цикла for (we N (v) ): if w не отмечена then dfs(w) else запомнить, что нашли цикл.
Однако этого недостаточно. Обход может повторно привести в вершину после
того, как произошел “откат” из рекурсии. Например, в вершину 8 ведет путь (4,6,8), а после возвращения в 4 — путь (4,7,8), но вершина 8 никакому циклу не принадлежит. Итак, нужно отличать, когда обход повторно приводит в вершину после возвращения к предыдущим вершинам, а когда вершина в порядке обхода предшествует сама себе”.
Чтобы отличать эти ситуации, используем отметки вершин 1 и 2 (0 означает, что
вершина не отмечена). Если рекурсивный обход вершин, достижимых из вершины, еще продолжается, она будет иметь отметку 1, а если этот обход завершен, то отметку 2.
Следующий алгоритм проверяет, достижима ли из вершины v какая-нибудь вер-
и
ина, принадлежащая циклу; N(v) обозначает множество вершин, в которые из
вершины v ведут дуги.
Проверка, достижима ли из вершины v какая-нибудь вершина в цикле орграфа I Ч	»	I	..... I. 
Алгоритм df s_cycle (v)
1} присвоить вершине v отметку 1;
2} for (w g N(v)) do
3} if w не отмечена then
4}	dfs_cycle(w)
5} else if w отмечена 1 then
6}	запомнить, что w принадлежит циклу;
7} присвоить вершине V отметку 2;
Возможны ситуации, когда цикл в орграфе есть, но однократный запуск подпрограммы его не находит, например, если в примере, приведенном йыше, в качестве начальной взять вершину 6. Поэтому возникает предположение, что для полного исследования орграфа подпрограмму нужно запустить с каждой вершиной в качестве начальной.
1} for (v G V) do begin
2} for (w g V) do присвоить w отметку 0;
3} dfs_cycle(v)
4} end.
ГЛАВА 8
Однако заметим: если вершина имеет отметку 2, то все вершины, достижимые и нее, уже рассмотрены и, если среди них есть вершина, принадлежащая циклу, она уже обнаружена. Поэтому работу по запускам обхода в глубину можно сократить, как в следующем алгоритме решения задачи:
for (v € V) do присвоить v отметку 0;
for (v g V) do begin
if v имеет отметку 0 then
df s_cycle (v) ;
if найдена вершина w, принадлежащая циклу then begin
вывести w; break end
end;
if вершин, принадлежащих циклам, не было then подтвердить, что Ьраф ацикличен
►► Реализуйте приведенный алгоритм.
►► Измените приведенный выше алгоритм df s_cycle (v), чтобы, как только обнаружена вершина w, принадлежащая циклу, исследование графа прекращалось. Указание. Первый способ. Перед рекурсивным вызовом проверять не только то, что w не отмечена, но и то что цикл еще не найден. При этом не должен измениться смысл else-ветки, в которой и запоминается как вершина, принадлежащая циклу. Второй способ. Используйте нерекурсивный вариант поиска в глубину, который можно оборвать с помощью break.
Задача 8.4. Дан ациклический орграф. Нужно пронумеровать его вершины так, чтобы номер каждой вершины был больше номеров вершин, из которых в нее ведут дуги.
Анализ задачи. Указанная нумерация вершин называется топологической сортировкой. Она задает линейное упорядочение вершин и может быть не единственной, например, орграф с множеством дуг {<0,1 >, <0,2>} допускает линейные упорядочения <0,1,2>и<0, 2,1>.
Топологическую сортировку связного ациклического орграфа Gen вершинами можно провести на основе обхода в глубину, используя обратную нумерацию и присваивая номера в порядке убывания (см. подраздел 8.2.1).
Начав с произвольной вершины, обойдем и отметим вершины, достижимые из нее. Первая вершина, при обработке которой нет рекурсивных вызовов dfs, получит наибольший номер. Поскольку очередная вершина нумеруется после своих потомков, ее номер меньше номеров потомков. Если после очередного обхода в графе остались неотмеченные вершины, берется следующая начальная вершина, и обход повторяется. Возможно, из новых вершин достижимы старые, отмеченные на предыдущих обходах. Но номера новых вершин меньше, поэтому правильность нумерации не нарушается.
Вместо алгоритма dfs можно применить для обхода аналогичный алгоритм df sr с обратной нумерацией вершин. Вместо убывающей нумерации будем добавлять вершины в начало списка F, который инициализируется как пустой. Таким образом, алгоритм топологической сортировки ациклического орграфа аналогичен алгоритму обхода произвольного графа.
ГРАФЫ
233
Топологическая сортировка ациклического орграфа
Вход. Ациклический орграф G = (V, Е).
Выход. Список вершин Г, линейно упорядочивающий вершины.
Алгоритм
il} for (v е V) do4установить v как неотмеченную;
2} F := о;
{3} for (v е V) do
{4} If v не отмечена then
{5}	dfs_rev_ord(v) .
Алгоритм df sr_rev_ord (v)
{1} отметить v;
{2} for (w € N(v)) do
{3} if w не отмечена then
4)	dfs_rev_ord(w);
{5} добавить v в начало F.
Сложность данного алгоритма, очевидно, равна сложности алгоритма обхода произвольного графа, т.е. 0(п)+0(т) = 0(м+ш).
и Реализуйте приведенный алгоритм.
►► Добавьте обработку ситуации, в которой исходный орграф оказывается циклическим.
8.3.4. Эйлеровы циклы и цепи
Задача 8.5. Есть система двухсторонних дорог, каждая из которых соединяет два поселка, через другие поселки не проходит и вне поселков не имеет перекрестков. Из каждого поселка выходит не меньше одной дороги5. Между любыми двумя поселками или только одна дорога, или дороги нет. Дорожная бригада должна разметить на всех дорогах осевую линию, не проезжая по ним дважды. Направление движения по дороге значения не имеет. Начать и закончить работу бригада должна на своей базе в начальном поселке. Построить какой-нибудь из возможных маршрутов или указать, что их нет.
Вход. Количество поселков и, количество дорог т, затем m пар чисел от 1 до л, задающих дороги (3<n<200,3<т<и(и-1)/2). Начальный поселок — 1.
Выход. Последовательность номеров поселков длиной т+1, которая начинается и заканчивается номером 1, если указанный маршрут существует, иначе одно число 0.
Анализ задачи. Очевидно, поселки — это вершины неориентированного графа, дороги — ребра, а искомый маршрут является циклом, который содержит все ребра графа по одному разу и начинается и заканчивается в заданной начальной вершине.
5 Поселений, в которые можно добраться только на вертолете, нет.
234
ГЛАВА 8
Рассматривая эту задачу, нельзя не вспомнить исторически первую задачу теории графов — задачу о кенигсбергских мостах, которую в 1736 году сформулировал и решил Эйлер. В Кенигсберге было два острова, соединенных семью мостами с берегами реки Прегель и друг с другом (рис. 8.5, а). Эту карту представляет неориентированный мультиграф, вершины которого — острова и берега, ребра — мосты (рис. 8.5, б). Говоря современными терминами, задача состояла в поиске цикла, который проходил бы по каждому ребру только один раз.
а) Карта	б) Граф
Рис. 5.5. Карта и граф в задаче о кенигсбергских мостах
Решая задачу, Эйлер установил и доказал необходимое и достаточное условие существования такого цикла в мультиграфе (естественно, в терминах, современных Эйлеру). С помощью этого критерия Эйлер доказал, что в “кенигсбергском” графе такого цикла нет. Позже такой цикл в графе назвали эйлеровым, как и связный мультиграф, содержащий эйлеров цикл.
Теорема (критерий эйлеровости графа). Неориентированный граф является эйлеровым, если, и только если, он связен и степени всех его вершин четны6.
► Необходимость (“только если”). Пусть граф является эйлеровым. По определению, он связен и в нем существует эйлеров цикл. Начав с произвольной вершины, пройдем по ребрам этого цикла, вычеркивая пройденные ребра. Придя в вершину по ребру, выходим нее п другому ребру, поэтому каждое прохождение вершины вычитает 2 от ее степени (из начальной вершины цикла мы выходим, а в конце приходим в нее). Удалив все ребра, получим пустой граф с вершинами степени 0, поэтому их исходные степени были четными.
Достаточность (“если”). Связность графа обеспечена условием. В качестве доказательства того, что он содержит эйлеров цикл, приведем алгоритм построения этого цикла. Пусть граф связен и степени всех его вершин четны. Из начальной вершины v0 начнем обход в глубину на каждом шаге добавляя очередную вершину в магазин S и удаляя пройденные ребра.
Заметим, что пройденный маршрут сохранен в магазине. Если удалять вершины из магазина по одной и выводить, получится обращенный пройденный маршрут. Будем говорить, что магазин представляет именно этот обращенный маршрут.
При прохождении вершины удаляются два инцидентных ей ребра (входное и выходное). Если вершина достигнута по входному ребру, то, поскольку степени вершин четны, выходное у нее обязательно есть. Первый шаг был сделан по ребру, выходному для г0, поэтому, если достигнута вершина, у которой больше нет ребер, то это v0. Возможны две ситуации.
1. Все ребра, инцидентные пройденным вершинам, уже удалены, т.е. в магазине хранится эйлеров цикл. Выведем его (в порядке, обратном пройденному).
6 В теореме и ее доказательстве под графом подразумевается мультиграф.
ГРАФЫ
235
2. Непройденные ребра остались, но инцидентные не v0, а какой-то другой вершине, пройденной и запомненной в магазине. Тогда можно удалить из магазина и вывести вершины без инцидентных им ребер, пока на верхушке не появится какая-нибудь вершина vp у которой еще есть ребра. В этот момент магазин представляет путь из в v0, обратный пройденному. В оставшемся графе, не обязательно связном, степени всех вершин четны. Значит, можно ббойти компоненту связности, содержащую vr Теперь эта вершина играет роль v0. Аналогично обойдем оставшийся граф в глубину до возврата в vp но уже без ребер. Выведем обращенное окончание пройденного пути, пока на верхушке магазина не появится вершина, у которой остались ребра. И так далее. <
Оценим сложность приведенного алгоритма. Каждое ребро рассматривается, по сути, дважды -- при удалении из графа и при удалении вершины из магазина. Определить, имеет ли вершина инцидентные ребра, и удалить ребро можно за константу. Поэтому сложность алгоритма имеет оценку &(т).
Решение задачи. Используем приведенный алгоритм для решения нашей задачи, учитывая, что условие не гарантирует эйлеровости графа. Из доказанного следует, что если хотя бы одна вершина имеет нечетную степень или граф несвязен, то эйлерова цикла нет. Для проверки первого условия достаточно вначале просмотреть степени вершин, второго — найти количество компонент связности, обойдя граф в ширину или глубину.
Вместо предварительной проверки связности, т.е. обхода графа, запомним общее количество ребер т, построим цикл из ребер компоненты связности, содержащей начальную вершину, подсчитаем количество ребер в компоненте и сравним с т. Однако в этой ситуации, пока не построен весь цикл, нельзя выводить часть обращенного цикла. Поэтому вершины, удаляемые из основного магазина, будем заталкивать в дополнительный магазин R, который в конце работы будет представлять цикл. Если длина цикла равна т, выведем его, иначе выведем 0.
Уточним структуру данных. Вершины представим индексами 1-200 массива, каждый элемент которого хранит указатель на ее список смежности. Для номера вершины достаточно одного байта. Из условия следует, что длина эйлерова цикла меньше 19 900, поэтому реализуем магазины массивами байтов.
Рассмотрим представление графа. Если реализовать граф матрицей смежности, то поиск ребра {v, w}, следующего в маршруте за {и, v}, требует просмотра v-й строки матрицы и имеет оценку в худшем случае 0(п). Отсюда сложность реализации — &(тп).
Представим граф структурой смежности (см. подраздел 8.1.3). В задаче при переходе из вершины и нужно удалить любое инцидентное ей ребро. Проще всего удалить ребро {и, г}, где v — первая вершина в списке смежности и. Однако нужно еще удалить и из списка смежности v, а для этого понадобится просмотр списка. Чтобы избежать просмотров, используем в списке смежности элементы с тремя указателями. Элемент для и в списке смежности v хранит указатель на элемент для v в списке смежности и, и наоборот. Еще два указателя — на предыдущий и следующий элементы списка, позволяющие удалить элемент без просмотра списка с начала. Напомним: такая структура данных встречалась в задаче о центре дерева (см. подраздел 8.1.4).
С помощью описанной структуры обеспечивается константная оценка обработки каждого ребра, поэтому сложность описанного решения задачи — 0(/и).
ГЛАВА 8
Из действий, описанных выше, рассмотрим только построение цикла.
Построение эйлерова цикла
S := о; R := о;
Vo :» 1;	{ 1 - начальная вершина }
занести v0 в S;
while S не пуст do
if вершина v на верхушке S имеет ребра then begin u : = вершина в первом элементе списка смежности v; удалить первый элемент из списка смежности v и "парный" к нему элемент из списка смежности и; занести и в магазин S
end
else begin
удалить вершину v на верхушке S; занести v в магазин R
end
{ цикл представлен в магазине R }
►► Уточните решение (ввод данных и создание структуры смежности, построение цикла и вывод результата).
►► Решите задачу при условии, что между поселкам^ может быть не одна, а несколько дорог т.е. поселки и дороги образуют мультиграф.
В заключение приведем понятие эйлеровой цепи — так называется маршрут, проходящий через все ребра графа по одному разу. Нетрудно убедиться, что в графе есть эйлерова цепь, если он эйлеров или он состоит из одной нетривиальной компоненты связности и только две его вершины имеют нечетную степень — тогда цеп^ начинается в одной из них и заканчивается в другой.
8.3.5. Обход графа достижимых состояний
Задача 8.6. На болоте была прямая дорожка из L бугорков, расположенных на расстоянии одного метра друг от друга. Некоторые бугорки провалились, и их осталось только М (M<L). Известно, что первый и последний (L-й) бугорки сохранились.
На первом бугорке находится попрыгунчик. Он может прыгать только вперед, начав с длины прыжка 1 м. Длина каждого следующего прыжка или равна длине предыдущего, или на 1 м больше, или на 1 м меньше. Попав в болото (на провалившийся бугорок), попрыгунчик гибнет.
Выяснить, может ли попрыгунчик добраться до последнего бугорка, и если может, то как он должен прыгать, чтобы общее количестве прыжков было как можно меньше. Если есть несколько разных способов допрыгать за минимальное количество прыжков, найти любой один из них. “Добраться до бугорка” означает, что попрыгунчик должен попасть на него, а не перепрыгнуть через него, но длина последнего прыжка роли не Играет.
Вход. Текст содержит в первой строке количество оставшихся бугорков М (М< 500), во второй — М порядковых номеров оставшихся бугорков.
ГРАФЫ
237
Выход. Текст должен содержать число 0 (если допрыгать нельзя) или минимальное число прыжков и последовательность номеров бугорков, на которых должен побывать попрыгунчик.
Анализ задачи. Из условия “прыгать разрешается только вперед” не ясно, можно ли прыгать на месте (с длиной 0). Однако, как нетрудно убедиться, оптимальное решение не будет содержать таких прыжков, поэтому будем считать их запрещенными.
По условию, первый прыжок с бугорка 1 имеет длину 1, поэтому добраться до бугорка 2 можно только за один прыжок. До бугорка 3 можно добраться только за два прыжка длиной 1, до бугорка 4 — за два прыжка длиной 1 и 2 или за три прыжка длиной 1 (если бугорки 2 и 3 не провалены) и т.д.
Ясно, что минимальное число прыжков, приводящих на бугорок з, не только зависит от бугорка, но и как-то (пока непонятно, как именно) связано с длиной последнего прыжка V. Поэтому будем рассматривать пары (j, v), называя их состояниями. Через N(s, v) обозначим минимальное возможное количество прыжков, приводящих в состояние (s, v).
Для вычисления N(s, v) достаточно рассмотреть три возможных предыдущих состояния (j—v, V—1), (s—v, v) и (s-v,v+l), выбрать из них то, в котором значение N минимально, и учесть прыжок из него, приводящий в состояние (s,v). Обозначим N(s, v) = оо, если состояние (j, v) недостижимо.
Итак, приходим к следующей рекурсивной формуле:
N(s, v) = min{ N(s-v, v-1), N(s-v, v), N(s-v, v+1) }+l, если бугорок j остался, (8.1) N(s, v) = co иначе.
Начальные условия — N(2,1) = 1 и N(s, v) = оо при s£0 и любом v.
Приведенная формула позволяет вычислить значения N(L, v) при всех возможных v и выбрать минимальное из этих значений. Однако вместо рекурсии используем обход графа достижимых состояний в ширину.
Из условия следует, что N(2,1) = 1, причем (2,1) — единственное состояние, достижимое за 1 прыжок. Используя его, найдем все состояния, достижимые за 2 прыжка; опираясь на них — все, достижимые за 3 прыжка, и т.д. Обозначим множество состояний, достижимых за i шагов (но не за меньшее число шагов), через S(. Опишем, как построить S^, опираясь на известное Sr
Пусть N(s, v)=i. Если длина текущего прыжка равна v, следующий прыжок может иметь длину v-1 (при условии v>2), v или v+1. Рассмотрим состояния (s+(v-l), v-1), s+v, v) и (y+(v+l), v+1), обозначив их для унификации (s+(v+Av), v+Av), Ave {-1, 0,1}. V(j, v)=i, поэтому через состояние (j, v) до них можно добраться за i+1 шаг.
ГЛАВА 8
При этом возможные следующие ситуации, и только они:
•	бугорок s+(v+Av) провален: добираться на него не нужно — ничего не делаем;
•	s+(v+Av) > L: переход в такое состояние не может дать решения — ничего не делаем;
•	MA+(v+Av), v+Av)<i+l: раньше уже был найден способ, как достичь состояния (A+(v+Av), v+Ay) не более чем за i+1 шагов, поэтому только что найденный способ не лучше уже известного — ничего не делаем;
•	мы впервые анализируем состояние (s+(v+Av),v+Av), поэтому Af(s+(v+Av), v+Av) := i+1,5+] := u {(^+(v+Av), v+Av)}.
Выполнив эти действия для всех состояний (A, v)g 5., получим все состояния из *^+г
Подчеркнем, что состояния из 5j+1 строятся после того, как полностью построено S Отсюда следует:
•	правильные значения МА, у) вычисляются сразу, т.е. каждая клетка таблицы или еще не обработана в процессе построения, или содержит правильное значение 7V(j,v) (ситуаций, когда сначала установлено "промежуточное” значение, а потом меньшее, не бывает);
•	при первом достижении (L, у) (неважно, при каком у) вычислено минимальное число шагов, необходимое для того, чтобы прибыть на бугорок L.
До сих пор рассматривался лишь вопрос, как найти минимальный путь. Но один из вопросов задачи — выяснить, существует ли путь. Рассмотрим работу нашего метода на входных данных 3 12 8. Получим {(2,1)}, S2 = 0 (состояний, Достижимых за два шага, нет). Из 52 = 0 следует, что S3= S4 = ... = 0, т.е. других достижимых состояний, кроме элементов А,, нет.
Итак, описанный процесс нужно закончить, если достигнуто какое-либо из состояний (L,у) или оказалось, что S=0, но ни одно состояние (L,у) так и не достигнуто. Во второй ситуации нужно выдать сообщение, что пути нет, в первой — вывести минимальное количество прыжков и восстановить путь, для чего нужна процедура обратного хода. Рассмотрим ее.
Найдя N(L, v), получим состояние (одно из равнозначных) (L, у), на котором достигается минимальное количество прыжков. Следовательно, последний прыжок был с бугорка L-v, а длиной предпоследнего прыжка было v-1, v или v+1. Чтобы выяснить, какой из этих вариантов имел место, достаточно просмотреть поля N(L-v, v-1 (при у >2), N(L-v, v) и N(L~v, v+1) и найти, какое из их значений равно N(L, v)-l (если таких несколько, взять любое). “Отступать” нужно, пока не придем к j = 1. Поскольку обратный ход начинается с конца (s=L), а вывести путь нужно с начала, понадобится вспомогательный массив.
Наконец, оценим “ширину таблицы” М(5, у) — количество различных значений у. Очевидно, что длина прыжка достигает максимального значения х, если с каждым
Графы
239
прыжком увеличивается. Таким рбразом, 1 + 2+ ... + x<L-l, т.е. (1+х)х<2(£-1). От-
-1 + V1+8(L-1) г—
сюдах<------2--------< V2L .
2
Решение задачи. Ниже будет представлен фрагмент программы, описывающий только построение N(s *v) с помощью обхода в ширину. Опишем структуры данных,
использованные в нем.
Данные об оставшихся бугорках запомним в массиве iRes с индексами от 1 до Л и элементами типа boolean (true, если бугорок остался, и false, если провалился). Значения N^s, v) будем запоминать в массиве N.
Номер итерации (количество прыжков) является значением переменной min_steps. Множества S. и представим двумя связными списками с указателями на их головы в массиве curr_steps : array [0. .1] of PSitList. На итерации с номером min_steps уже известное множество S( представлено в списке curr_steps [min_steps mod 2], а второй список, для множества создается. На следующей итерации они меняются ролями.
Если достигнуто условие s=L, то выводится количество прыжков, вызывается процедура Out Sol, которая восстанавливает и выводит оптимальный путь, и работа завершается. Если же curr_steps [min_steps mod 2] =nil, т.е. S=0, то выводится 0.
Значения N(s,v) можно представить в типе integer, поэтому “оо” реализована в программе как $7FFF (максимальное значение этого типа).
Последовательное построение совокупностей достижимых состояний
currjsteps[0] := nil;
new(curr_steps[1]); curr_steps[1]A.next := nil;
curr__steps [1] A .pos : = 1; curr_steps [1] A . speed : = 1;
min_steps := 1;
for j := 1 to round(sqrt(2*L)) do
for i := 1 to L do N[i,j] := $7FFF;
mass[1,1] := 1;
while curr_steps[min_steps mod 2] <> nil do begin p := curr_steps[min_steps mod 2];
min_steps := min_steps+l;
while p <> nil do begin
for k := 1 downto -1 do begin
s := p^.pos; V := pA.speed + k
if s+v > L then continue;
if not iRes[s+v] then continue;
if v <- 0 then break;
if N[s+v,v] = $7FFF then begin
N[s+v,v] := min_steps;
if s+v = L then begin writein(fv, min_steps); OutSol(fv, min_steps, s+v, v); close(fv); exit
end;
new(q); qA.pos := s+v;
240
ГЛАВА 8
qA.speed := v;
qA.next := curr_steps [min__steps mod 2] ;
curr_steps[min_steps mod 2] := q;
end;
end;
q := p; p := pA.next; dispose(q);
end;
curr_steps[(min_steps+l) mod 2] := nil;
end;
writein(fv, '0'); close (fv);
H Реализуйте программу.
Упражнения
8.1.	Для тех, кто знаком со свойствами умножения матриц: пусть А — матрица смежности графа. Что выражают значения элементов матрицы А”, где п > 1 ?
8.2.	Между городами проложены дороги. Проверить, из каждого ли города можно попасть в любой другой. Вход. Количество городов п (п< 104) и список дорог (пар городов, заданных номерами от 1 до и). Выход. 1, если можно, или О, если нельзя.
8.3.	Выделить и пронумеровать компоненты связности графа. Для всех вершин указать номера компонент, которым принадлежат вершины.
8.4.	Проверить ацикличность неориентированного графа (нужен только ответ “Да” или “Нет”).
8.5.	Заданы две вершины графа. Найти какой-нибудь маршрут минимальной длины из первой вершины во вторую.
8.6.	Построить список вершин, образующих какой-либо цикл орграфа (если он есть).
8.7.	Определить, принадлежит ли заданная вершина какому-либо циклу неориентированного графа, и если это так, вывести вершины цикла.
8.8.	Модифицируйте алгоритм df s_cycle (v) (см. задачу 8.3) так, чтобы проверялось наличие циклов длиной не меньше 3, а петли и пары противоположных дуг циклами не считались.
8.9.	В рыцарском войске некоторые рыцари враждуют между собой. Нужно разбить их на два отряда, чтобы в каждом из отрядов был хотя бы один рыцарь, но не было врагов.
Вход: количество рыцарей п (п< 104) и список пар врагов, заданных номерами от 1 до п. Выход: два списка номеров, если разбиение возможно (оно может быть не единственным), или 0, если невозможно.
Замечание. Эта задача равносильна проверке, можно ли вершины графа правильно раскрасить в два цвета, т.е. так, чтобы любые две смежные вершины были разных цветов. Проверка правильной раскрашиваемости в большее число цветов — переборная задача (такие задачи представлены в главе 11).
8.10.	Города заданы номерами 1,2,..., п и связаны дорогами, которые имеют длину 1 и пересекаются только в городах. Размерами городов можно пренебречь.
ГРАФЫ
241
Есть три робота; каждый из них может двигаться по дорогам с постоянной скоростью или 1, или 2 (единиц расстояния в секунду), не меняя направления. Находясь в некоторых городах, они начинают движение, чтобы за минимальное время прибыть в одно и то же место. Вычислить это время.
Вход. В первой строке — количество городов п и количество дорог /и (1 £ и£50, 1 < ли < 100). В следующих т строках — пары городов. В следующей строке три скорости роботов и три города, в которых они находятся.
Выход. Время, округленное до двух дробных десятичных цифр, или -1, если встреча невозможна.
Примеры. При п = 2, т = 1, дороге {1,2}, скоростях 1, 1, 2 и исходных городах соответственно 1,1,2 получится время 0.33, а при исходных городах 1,2,2—1.
8.11.	Вычислить эксцентриситеты вершин связного графа, его диаметр и радиус.
8.12.	Транзитивное замыкание графа — это граф, вершины которого смежны, если, и только если, в исходном графе они связаны. Транзитивное замыкание орграфа — это орграф, в котором есть дуга <v, w>, если, и только если, в исходном графе вершина w достижима из v. Построить транзитивное замыкание заданного графа или орграфа.
8.13.	Для двух заданных вершин корневого ориентированного дерева найти их наименьшего (ближайшего к ним) общего предка. Например, в дереве с дугами < 1,2>, <1,3>, <3,4> у вершин 2 и 4 наименьший общий предок — 1, а у вершин 3 и 4 — 3.
8.14.	Слова состоят из строчных букв латинского алфавита; длина слов не больше 50. Можно ли из заданного набора слов в количестве до 103 сложить замкнутый чайнворд (первая буква следующего слова совпадает с последней буквой предыдущего, а следующим за последним словом является первое). Должны быть использованы все слова по одному разу.
8.15.	Изменим условие задачи 8.5: по каждой дороге нужно проехать по одному разу в обе стороны. Вывести какой-либо маршрут или указать, что его нет.
8.16.	Как известно, кости домино — это прямоугольные пластинки размером 2х 1; на их половинки нанесены точки, обозначающие числа от 0 до 6. Если числа равны, кость называется дублем (число дублей равно 7), иначе два числа образуют сочетание (их количество равно 7-6/2=21). Обобщенные кости домино — это дубли и сочетания двух чисел от 0 до п (0£п£ 100). Например, при п = 3 полный набор содержит 10 костей: {0,0}, {0,1}, {0,2}, {0,3}, {1,1}, {1,2}, {1,3}, {2,2}, {2,3}, {3,3}.
Из костей можно выкладывать цепочки, соединяя их короткими сторонами, если на соединяемых половинках числа равны.
Задан набор обобщенных костей, возможно, неполный.
а)	Проверить, можно ли все кости этого набора выложить в одну цепочку (не указывая ее саму).
242
ГЛАВА 8
б)	Определить, какое минимальное количество цепочек можно выложить из костей набора, чтобы каждая кость была в одной из цепочек7.
Пример. Набор костей {0,1}, {0,2}, {0,3}, {1,2}, {1,3}, {2,3} нельзя выложить в одну цепочку, но можно выложить в две: <{0,1}, {1,2}, {2,3}> и <{1,3 {3,0}, {0,2}>.
7 Автор задачи (в несколько другой редакции) — Андрей Стасюк.
Глава 9
Графы клеток и графы с нагруженными ребрами
В этой главе...
♦	Графы смежности на клетчатых полях и применение алгоритмов их обхода
♦	Остовные деревья минимального веса и их построение по алгоритмам Прима и Краскала
♦	Вычисление расстояний от вершины-источника до остальных вершин. Алгоритм Дейкстры
♦	Вычисление максимальной грузоподъемности путей от вершины-источника до остальных вершин на основе алгоритма Дейкстры
Данная глава состоит из двух частей. Задачи первой части связаны с графами, вершинами которых являются элементы клетчатых полей, а ребра образуются клетками, имеющими общую сторону. Во второй части представлены графы, ребра которых отмечены числами. Во многих практических задачах эти отметки задают длину дороги, стоимость перевозки груза между двумя городами, длину проводов, соединяющих элементы электрической схемы, и т.д. Ребра или дуги таких графов называются нагруженными, а числовые отметки на них — весом.
9.1. Графы на клетчатых полях
9.1.1. Фигуры на клетчатом поле
Задача 9.1. В каждой клетке прямоугольной таблицы, имеющей М строк и N I столбцов, находится о или 1. Единицы в этой таблице образуют произвольные 1 “фигуры”. Две единицы принадлежат одной и той же фигуре, если между ними I существует путь, в котором каждый шаг — это переход между двумя соседними I единицами. Соседство клеток считается по вертикали или горизонтали, но не I по диагонали. Нужно подсчитать количество фигур в заданной прямоугольной I таблице.	|
Анализ и решение задачи. Попытаемся определить общий порядок прохождения таблицы. Например, начиная с левого верхнего угла, пройти первую строку слева направо, затем пройти следующую строку и т.д. Но фигуры могут быть сложными, и построить решение, где таблица проходится только так, проблематично. Например, если фигура имеет форму буквы Н, верхние части ее вертикальных частей сна
244
ГЛАВА 9
чала будут восприняты как две разные фигуры. Можно разработать метод, как с этим бороться, но проще изменить стратегию обхода.
Можно искать фигуры так: проходить поле в указанном порядке, пока не найдем единицу — клетку, принадлежащую какой-то фигуре. После этого будем отслеживать всю фигуру, заменяя единицы во всех ее клетках двойками (чтобы избежать повторного рассмотрения этой же клетки), а затем продолжать просмотр с той клетки, где нашли единицу. Единицы теперь имеют смысл не “элемент фигуры”, а “еще не просмотренный элемент”.
Результат будет правильным: последовательный просмотр не пропустит ни одного поля, поэтому все фигуры будут учтены, как в следующем фрагменте программы. При обнаружении 1 вызывается процедура ProcessFigure (о ней ниже), прослеживающая и обозначающая двойками всю фигуру, благодаря чему фигура не учитывается больше одного раза.
Основной проход и подсчет количества фигур
N_fig := 0;
for i := 1 to М do
for j := 1 to N do
if mass[ir j] = 1 then begin
ProcessFigure(i, j);
N_fig := N_fig+1;
end;
Итак, основной проблемой задачи является “прослеживание фигуры”. Для решения этой проблемы посмотрим на нее с точки зрения графов. Клетки с единицами можно считать вершинами, их соседство — смежностью, а фигуру — компонентой связности. Таким образом, нужно подсчитать компоненты связности — используем для этого обход в глубину.
Реализуем нерекурсивный вариант обхода из подраздела 8.2.1. Неотмеченные вершины — это клетки с 1, отмеченные — с 2. Координаты вершин (записи с полями i Hj), соседних с отмечаемыми, запомним в (глобальном) массиве stack (магазин). Количество занятых элементов магазина хранится в переменной top (листинг 9.1).
Листинг 9.1. Обход фигуры в глубину с помощью магазина
procedure ProcessFigure(start^i, start_j : byte);
var i, j : byte;
begin
top := 1;
stack[1].i := start_i; stack[1].j := start_j;
mass[start_i, start_j] := 2;
while top <> 0 do begin
i :=. stack [top] . i; j := stack[top].j;
top := top-1;
if (i>l) and (mass[i-1, j] = 1) then begin
mass[i-l/ j] := 2;
top := top+1;
stack[top].i ;= i-i; stack[top].j := j;
end;
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
245
if (i < М) and (mass[i+1, j] =1) then begin
mass [i+1, j] := 2;
top := top+1;
stack [top].i := i+1; stack [top].j := j;
end;
if (j > 1) artd (mass[i, j-1] = 1) then begin
mass[i, j-1] := 2;
top := top+1;
stack[top].i := i; stack[top].j := j-1;
end;
if (j<N| and (mass[i, j+1] = 1) then begin
mass [i, j+1] := 2;
top := top+1;
stack[top].i := i; stack[top].j := j+1;
end
end end;
Рекурсивный алгоритм обхода фигуры намного проще и короче, хотя он может переполнить программный стек (листинг 9.2). Подчеркнем, что в рекурсивной реализации вершина отмечается до выполнения рекурсивных вызовов.
Листинг 9.2. Рекурсивный вариант обхода фигуры
procedure ProcessFigure(i, j : byte);
begin
mass[1, j] := 2;
if (i>l) and (mass[i-l, j] = 1) then
ProcessFigure(i-1, j);
if (i<M) and (mass[i+1, j] = 1) then
ProcessFigure(i+1, j);
if (j>l) and (mass[i, j-1] = 1) then
ProcessFigure(i, j-1) ;
if (j<N) and (mass[i, j+1] * 1) then
ProcessFigure(i, j+1);
end;
Приведенные варианты отличаются способом запоминания (глобальный массив или программный стек). Кроме того, в нерекурсивной реализации текущая клетка удаляется из магазина перед тем, как будут рассмотрены все ее соседи, а в рекурсивной — после того. Это отличие на некоторых входных данных (например, длинные линии без разветвлений) существенно влияет на количество клеток, которые одновременно хранятся в памяти.
Технические замечания
1. Поле клеток представлено в двумерном массиве
mass : array [1..ММах, 1..NMax] of byte.
Значения ММах и NMax — константы, заданные в тексте программы, а М и N — переменные, указывающие, какая часть из этого массива (1.. мх 1.. N) реально используется.
2. Обе реализации обхода фигур имеют по четыре практически одинаковых составных оператора, ведь для левой, правой, верхней и нижней клеток-соседей нужно сделать одно и то же. Унифицируем эти операторы. Параметризировать их действия
246
ГЛАВА 9
легко — параметр будет указывать, какая именно соседняя клетка обрабатывается. С п мощью массивов-констант
di : array [0..3] of shortint = (0, 1, 0, -1) и
dj : array [0..3] of shortint = (1, 0, -1, 0)
перебор четырех соседей можно записать довольно коротко: for к := 0 to 3 do
где координаты текущего соседа клетки (i, j ) выражаются как i+di [k], j +dj [к] Другой способ объединить просмотры всех соседей в один цикл —
for di := -1 to 1 do for dj := -1 to 1 do if abs(di) <> abs(dj) then ... .
Здесь координаты соседа — i+di, j +dj.
Основное преимущество унифицированной записи — если в процессе отладки понадобится изменить обработку соседей клетки, то исправить придется тольк один оператор. Проблема, что для части соседей операторы модифицированы правильно, а для части неправильно, вообще не возникнет. Кроме того, унификация уменьшает объем программного кода.
Здесь приведена не унифицированная реализация, поскольку действия над клетками-соседями довольно просты, а унификация усложняет и замедляет проверил выхода за границы поля, ь
►► Реализуйте всю программу.
►► Реализуйте унифицированный рекурсивный и нерекурсивный варианты, а также попробуйте решить вариант задачи, в котором соседними считаются клетки еще и по диагонали
9.1.2. Минимальный путь в лабиринте
Задача 9.2. Карта лабиринта задана прямоугольной таблицей из 0 и 1. Единицы означают стены, нули т- свободные клетки. Из свободной клетки можно пройти в любую соседнюю свободную клетку. Соседними считаются клетки, имеющие общую сторону.
Клетки лабиринта имеют координаты. Первая координата увеличивается по направлению сверху вниз, вторая — слева направо. В клетке с координатами (f0,j0) находится Робот. По карте нужно найти кратчайший путь, выводящий Робота из лабиринта (если есть несколько равноценных путей, то найти любой из них). Выходами из лабиринта считаются все свободные клетки во внешних стенах.
Вход. Текст содержит в первой строке через пробел М fe N — соответственно количество строк и столбцов (4<М<100, 4<#<100), дальше М строк, каждая из которых состоит из N символов 0 и 1, дальше (в (А/+2)-й строке) через пробел координаты i0 и у0 (нумерация по обеим осям начинается с 1)
Выход. Текст должен содержать одну строку с числом -1, если выйти из лабиринта невозможно, иначе первая строка должна содержать количество шагов минимального пути, вторая — сам путь в форме последовательности пар
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
247
<длина, направление^ направление вверх кодируется буквой N, вниз — S, вправо — Е, влево — Ж Последовательность выводится без пробелов. Путь заканчивается в клетке-выходе.
Примеры
Вход 5 9	1 Выход 4 Вход 5 5 Выход -1
101111111	2W2N 11101
101111111	10100
100000000	10110
111101111	10010
111111111	11111
3 4	2 2
Анализ задачи. Стратегию “пока можно, двигаться по направлению к выходу, а если случится преграда, обходить” мы не рассматриваем не потому, что в лабиринте может быть много выходов, а потому, что она может не дать оптимального пути, если даже выход один.
Для представления карты лабиринта используем двумерный массив с типом элементов word, вначале прочитав в него карту из входного файла.
Для построения кратчайшего пути будем постепенно расширять множество достижимых клеток. За 0 ходов можно достичь только клетки (i0 j0). Занесем в соответствующий элемент массива значение 2. Далее значения элементов массива будут трактоваться так: 1 — стена, 0 — свободная нерассмотренная клетка, значение п > 2 — до клетки можно дойти из (i0, j0) за л-2 шага.
Найдем все свободные клетки, достижимые за один ход, т.е. соседние с (г0,у0). Соответствующим элементам массива присвоим 3.
Найдем все клетки, в которые можно дойти за два хода, — соседние с какой-либо клеткой, достижимой за один ход. Перебрав все свободные клетки, соседние с содержащими 3, найдем все клетки, достижимые за два хода, и запишем в эти элементы значение 4. Среди Них будет и клетка (f0,j0). Если бы нас интересовали все возможные блуждания Робота, пришлось бы придумать, как запомнить, что клетка (z0, i0) достижима за нуль или за два хода. Но нам нужен только кратчайший путь, поэтому в процессе расширения не нужно заносить большее значение туда, где уже есть меньшее.
Каждая свободная клетка имеет начальную метку 0. Когда до нее доходит очередь, она получает новую метку. Сначала находим все клетки, достижимые за один ход, потом все, достижимые за два хода, и т.д. Поэтому полученное значение количества ходов сразу является окончательным, т.е. не может быть ситуации, когда сначала для клетки нашли какую-то оценку числа ходов, а потом — меньшую.
Процесс расширения останавливается, как только достигнута какая-либо клетка на внешней стороне лабиринта (кратчайший путь существует) или при очередной попытке расширения не получено ни одной новой оценки (дошли до в с достижимых клеток, но до выхода не добрались).
Описанный алгоритм иногда называют алгоритмом числовой волны.
Решение задачи. Решение задачи оформим в стиле “Init-Run-Done”. Тело программы — это вызовы процедур с указанными именами, обрабатывающих глобальные данные (массив-лабиринт maze и другие). Процедура Init очевидна.
248
ГЛАВА 9
procedure Init; { чтение входных данных в массив maze } var i, j : integer; a : char;
fv : text;
остальные переменные глобальны }
begin
assign(fv, 'maze, dat'); reset(fv);
readln(fv, M, N);
for i := 1 to M do begin
for j : = 1 to N do begin
read(fxr, a); maze[i, j] := ord (a)-ord ( ' 0 1) end;
readln(fv)
end;
readln(fv, iO, jo);
close (fv) ;
end;
Реализуем расширение множества достижимых клеток, запоминая клетки, достижимые на каждом шаге. Рассмотрим лабиринт как граф (вершины — свободные клетки) и обойдем его в ширину, используя очередь (см. подраздел 8.2.2).
Используем реализацию очереди с помощью двух массивов S [ 0 ] и S [ 1 ] (см подраздел 8.2.3). Необходимую длину этих массивов определим ниже.
Вначале, на “нулевом” шаге, занесем 2 в maze [ i01 j 0] и запомним координаты клетки (Zo Jo) {достигнутой на последнем шаге) в массиве S [ 0 ]. На очередном к-м шаге (£>1) рассмотрим соседей всех клеток из массива S [1 -kmod2], занесем в нужные элементы массива maze значение к+2 и запомним координаты новых клеток в другом массиве S [k mod 2 ].
Оценим необходимую длину массивов S [ 0 ] и S [ 1 ]. Представим себе лабйринт без стенок — количество достижимых клеток будет максимальным среди всех лабиринтов с теми же размерами. Нетрудно убедиться, что ширина волны достижимых клеток при любом соотношении сторон такого “лабиринта” и любом положении начальной клетки строго меньше M+N. Пусть в тексте программы объявлены кднстан-ты ММах и NMax, задающие максимальные размеры лабиринта. Сделаем MMax+NMax (избыточной) длиной массивов S [0] и S [1] (листинг 9.3).
Листинг 9.3. Расширение с помощью очереди клеток в виде двух массивов procedure Run; { расширение множества достижимых клеток } const di : array[О..3] of shortint = (0,1,0,-1);
dj : array[0..3] of shortint = (1,0,-1,0);
type Elem = record i, j : integer end;
var S : array [0..1] [0..MMax+NMax] of Elem;
k,	{ номер шага }
ti, tj,	индексы в массиве maze }
sFrom, sTo, номера массивов очереди }
nFrom, nTo, { количество достигнутых и новых клеток } tFrom	{ индекс в массиве достигнутых клеток }
: integer;
dir : byte; { направление }
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
249
{ остальные переменные глобальны }
begin
found__way := false; { пока путь не найден }
maze[io, jO] := 2;
k := о; номер шага }
sTo := 0; клетка i0, jO помещается в S [0] }
пТо := 1;
S [0] [0] .i := i0; S [0] [0] . j := jO;
repeat
k ;= k+1;	{ переход к следующему шагу }
sFrom := sTo; { роли массивов меняются местами }
sTo := 1 - sFrom;
nFrom := ПТо; пТо := 0;
tFrom := 0; { просмотр массива достигнутых клеток }
while tFrom < nFrom do begin
ti := S[sFrom][tFrom]. i;
tj := S[sFrom][tFrom]. j;
if (ti=l) or (tj=l) or (ti=M) or (tj=N) then begin i_exit := ti; j_exit := t j ; found_way := true; exit
end;
{ унифицированная обработка всех соседей текущей }
{ некрайней клетки (1 < ti < М, 1 < tj < N) } for dir := 0 to 3 do
if maze[ti+di[dir], tj+dj[dir]] = 0 then begin maze[ti+di[dir], tj+dj[dir]] := k+2;
S[sTo][nTo].i := ti+di[dir];
S [sTo] [nTo] .j := tj+dj[dir];
inc(nTo);
end;
inc(tFrom);
end;
{ tFrom = nFrom - массив S [sFrom] пройден }
until nTo = 0
{ новые клетки не добавлены или найден выход } end ;
Каждая свободная клетка, достижимая из начальной, попадает в один из массивов S только один раз, а ее обработка требует константы действий, поэтому общая сложность приведенной реализации — 0(MN).
Перейдем к процедуре Done. Рассмотрим, как с помощью измененного массива maze восстановить кратчайший путь. Кратчайший путь к клетке (z,j) на предпоследнем шаге проходит через одну из соседних с ней клеток. Если к (i,j) можно дойти за i=maze [i, j ] -2 шага, то к соседней, предшествующей ей на кратчайшем пути, — за к-1 шаг. Итак, предшествующей клеткой должна быть соседняя, у которой значение на 1 меньше (если их несколько, то существует несколько равноценных путей и можно выбрать любой из них).
Отступая таким образом от клетки-выхода (проводя обратный ход), дойдем до начальной клетки (i0,J0) со значением 2.
250
ГЛАВА 9
Путь строится от конца к началу, но вывести его нужно от начала к концу, по-
этому для временного хранения пути нужен дополнительный массив way. В боль-
HKI
ястве “разумных” лабиринтов длина кратчайшего пути к выходу приблизительн
пропорциональна линейным размерам лабиринта M+N. Однако в лабиринте вроде “змеики” или “спирали” длина минимального пути приближается к MN/19 поэтому массив way сделаем довольно большим (листинг 9.4).
Листинг 9.4. Обратный ход и вывод пути
procedure Done; { обратный ход и вывод пути } var fv : text;
way : array [O..M*N] of char;
N_Steps, ( длина пути } step, ( отметка и } i, j, { координаты текущей клетки пути } d, { длина прямого отрезка пути } t { длина пройденной части пути } : integer;
{ остальные переменные глобальны }
begin
assign(fv, 1 maze.sol'); rewrite(fv);
if not found_way then begin { путь не найден } writein(fv, 1-1'); close(fv); exit
end;
N_Steps := maze [i_exit, j_exit]-2;
writein(fv, N_Steps ;
if N_Steps = 0 then begin writeln(fv); close(fv); exit end;
{ обратный ход - путь запоминается от конца к началу } i := i_exit; j := j_exit; step := maze[i, j];
while step > 2 do begin
if (j>l) and (maze[i, j-1] = step-1) then begin j := j-1; way[step-2] := ' E1 end
else if (j<N) and (mazed, j+1] = step-1) then begin j := j+1; way[step-2] := 'W' end
else if (i>l) and (maze[i-l, j] = step-1) then begin i : = i-1; way[step-2] := 'S' end
else {(i<M) and (maze[i+l, j] = step-1)} begin i := i + 1; way[step-2] := 'N' end;
dec (step) end;
{ вывод пути от начала к концу в требуемом формате } t := 1;
repeat
d := 1;
while (t < N_Steps) and (way[t] = way[t+l]) do begin t :« t+1; d := d+1 end;
write (fv, d, way[t]);
t := t+1;
until t > N_Steps;
writein(fv); close(fv) end;
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
251
Если бы по условию выход из лабиринта был один, дополнительный массив был бы не нужен — проведем расширение, начиная с выхода, пока не достигнем Тогда результаты обратного хода получились бы сразу в нужном порядке, и их можно было бы вывести, не запоминая1.
н Реализуйте всю программу.
Задача 9.3. Прямоугольный в плане лабиринт размерами ЕхА/ состоит из одинаковых комнат. Комната в северо-западном углу лабиринта имеет координаты (1,1), в юго-западном — (L, 1). В каждой из комнат есть от одной до четырех дверей в соседние комнаты. Все двери одинаковы, на них есть ручки с обеих сторон и нет замков.
Путник вошел в одну из комнат лабиринта, записал ее координаты и затем, блуждая, нашел выход. Он записал свой маршрут в виде последовательности букв, обозначающих направления проходов через двери: У — север, Е — восток, S — юг, W — запад. Последняя буква последовательности обозначает переход в комнату, в которой есть выход из лабиринта.
По маршруту нужно составить кратчайший путь, ведущий от входа к выходу и проходящий по комнатам, в которых побывал Путник.
Вход. В первой строке текста через пробел записаны размеры лабиринта с севера на юг и с запада на восток (не больше 50). Во второй строке — координаты комнаты, в которую вошел путник. В третьей — последовательность букв У, Е, S и W длиной не более 104.
Выход. В первой строке текста — длина найденного кратчайшего пути, во второй — задающая его последовательность букв У, Е, S и Ж
Пример
Вход 3 3 Выход 2 3 1	ЕЕ
EWNNESSE
Анализ задачи. Очевидно, нужен поиск в ширину на графе, вершины которого — осещенные комнаты, ребра — двери, через которые проходил Путник. Основной вопрос в этой задаче — представление графа. Каждая вершина имеет не более четырех соседей, поэтому использование списков смежности вершин явно неэкономно, е говоря уже о матрице смежности.
Естественно представить граф “матрицей комнат”, но нужны дополнительные данные о том, в каких стенах каждая комната имеет двери, а в каких — нет. Для каждой комнаты можно использовать четыре поля типа Boolean, соответствующих направ-ениям, но поступим иначе, представив данные о дверях числом С в одном байте.
Вначале положим С=0, затем, обнаружив при анализе маршрута дверь в восточном направлении, к С прибавим 1, в северном — 2, в западном •— 4, в южном — 8. Таким образом, каждое направление представлено одним битом2. Естественно, каж-ое слагаемое нельзя прибавлять повторное Определить, было ли добавлено слагае-
1	Этот прием можно применить и в нашей задаче, если начать процесс расширения одновременно от всех выходов.
2	Старший полубайт в действительности не используется, так что у читателя есть простор для деятельности.
252
ГЛАВА &
мое. несложно. Признак наличия восточной двери— Cmod2 = l, северной — Cmod4 >=2, восточной — С mod 8 >=4, южной — С >= 8.
Указанные признаки определяют смежность вершин, и с их помощью нетруднс реализовать обход графа в ширину.
н Доведите решение задачи до программы.
9.1.3. Подсчет клеток в областях
Задача 9.4. На квадратной доске размером NxN играли в интеллектуальную игру, закрашивая клетки в белый, черный или зеленый цвет. Известно, что все клетки верхней строки белые, а нижней — черные.
Для определения победителя игры нужно подсчитать количество клеток в белой и в черной области. Белая область — это наибольшая по количеству клеток часть квадрата, ограниченная верхней стороной квадрата и белой границей. Белая граница — это последовательность соседних белых клеток (имеющих общую сторону), в которой клетки не повторяются. Концы границы — левая и правая верхние клетки квадрата. Черная область аналогична: она ограничена нижней стороной квадрата и границей, проходящей по черным клеткам с концами в левой и правой нижних клетках квадрата.
Найти количества клеток в белой и в черной областях.
Вход. Первая строка текста содержит одно целое число N— размер квадрата (5 < 7V<250). В каждой из следующих N строк записаны N символов G, w или В (без пробелов), обозначающих зеленый, белый и черный цвет соответственно.
Выход. В первой строке текста количество клеток в белой области, во второй — в черной.
Пример
Вход	Выход
7	22
WWWWWWW 15 WGWWBWG WWWWGWW BBGWWWB GWBBWGB BBBBGBB ВВВВВВВ
Иллюстрация
Анализ задачи. В самых общих чертах решение таково: границы областей найдем с помощью правила правой руки (в варианте “с возвращениями”); площадь вычислим, обходя границу и добавляя ориентированные площади прямоугольников-“столбцов”. образованных клетками.
Поиск границы области. Во избежание двусмысленностей используем слова “влево-вправо” в их относительном смысле (“влево” означает “против часовой стрелки относительно текущего направления”). Абсолютные направления будем называть западным, восточным, северным и южным.
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
253
Припишем границам Hanpat тение, указанное на рис. 9.1. Тогда вся область, заданная границей, находится слева от нее, остальное поле — справа. Поэтому, когда при построении границы появляется выбор (есть различные соседние клетки нужного цвета), вначале нужно рассмотреть правое из возможных направлений. Ведь правее границы клетки нужного цвета ведут в тупики, которые нетрудно распознать, отбросить и перейти к рассмотрению следующего (в порядке справа налево) варианта пути. Но если пойти “слишком влево”, можно получить путь, который в действительности проходит внутри нужной области.
1	2	3	4	5	6	7
WWW	W W
W G W	W В ,W G
	W G ,^..W
В В G	.в
G W	м • • • о со
00 сс,	ли. ф со
В В В 1	00 S. д. я.
Рис. 9.1. Направление областей
Выясним, как распознать тупик и когда отступать в предыдущую клетку. Тупик — это клетка, у которой нет соседей нужного цвета (кроме клетки, из которой только что при-дли). Из тупика нужно отступать в предыдущую клетку. Кроме того, если все возможные продолжения пути нужного цвета оказались тупиковыми, тоже нужно отступать.
Наконец, заметим, что требование, чтобы клетки не повторялись, нарушается при попадании не только в клетку, где побывали только что, но и в клетку, где вообще когда->ибо бывали. Отрезок границы до повторения клетки нужно исключить (рис. 9.2).
W	.W	.W		,w	,уу		,w	,w
								
								
В	в		W	в	в		в	в
В	в		W	в	в		в	в
в	в					W	в	в
								
в	в		W	,w		W	в	в
								
в	в	в		в	в		в	в
в	в	в		в	в		в	в
1 2 6 7
1
2 3
4
5
6
7
Рис. 9.2. Исключение границы с повторением вершин
Реализация алгоритма выделения области. Рассмотрим основные идеи реализации. Чтобы не проверять, есть у клетки все соседи или она крайняя, целесообразно
ГЛАВА 9
добавить к массиву дополнительные 0-й и (ЛЧ-1)-й столбцы, заполнив их зелеными
клетками.
Закодируем абсолютные направления: 0 — “на юг”, 1 — “на восток”, 2 — “на север”,
3 — “на запад”. Изменения абсолютных координат (i,j) при переходе на одну клетку в со
ответствующем направлении запомним в массивах-константах di : array [0.. 3] of
integer= (1, 0,-1, 0) и dj : array [0..3] of integer= (0, 1,0,-1). Значения i возрастают пр направлению с севера на юг, j — с запада на восток.
С помощью абсолютных направлений легко получить повороты: выражение (d+1) mod 4 (или (d+5) mod4) задает направление “влево относительно d”; (d+2) mod4 — “назад”, (d+3) mod 4 — “вправо относительно d” (очевидное выражение (d-1) mod4 не работает при d= 0). Выражение (d+4) mod4 задает движение вперед, поэтому направления “вправо”, “вперед”, “влево” можно перебрать в цикле
for dd := 3 to 5 do исследовать направление (d+dd) mod 4
Предыдущие рассуждения о продолжениях пути и отступлениях легко реализовать рекурсивно: переход к следующей клетке границы — рекурсивный вызов, отступление — возврат на один уровень вверх, при достижении конечной клетки (северо-восточной для белой области, юго-западной для черной) нужно вычислить площадь.
Однако рекурсивная реализация создает несколько неудобств.
•	Дойдя до концевой клетки, желательно “выйти совсем”, а не подниматься на несколько уровней и углубляться в другие ветки рекурсии, ведущие к бесцельному перебору путей внутри области.
•	Глубина рекурсии определяется длиной границы, которая в наихудшем случае может достигать почти 2/з№.
•	Для вычисления площади области нужно знать всю ее границу, запомнив ее в отдельном массиве. В дополнение к этому рекурсивная реализация запоминает границу еще и в программном стеке.
►► Если использовать 32-битовый компилятор (и установить достаточный размер программного стека), то реализовать описанный алгоритм рекурсивно нетрудно. Сделайте это самостоятельно.
Однако итеративное решение все же будет более эффективным. Представим его подробно.
Использование “стандартной” нерекурсивной версии поиска в глубину (см. подраздел 8.2.1) также создает серьезные неудобства. Наибольшее из них состоит в том, что граница области в виде, удобном для ее дальнейшего анализа, отсутствует! Те клетки, по которым действительно прошла граница, уже извлечены из магазина, а остаются в магазине только некоторые из их соседей, которые не очень-то и нужны.
Поэтому в данной задаче лучше всего эмулировать рекурсивный обход в глубину нерекурсивными средствами: найдя некоторого соседа, исследовать его, “даже не взглянув” на других соседей. Напомним: стандартная нерекурсивная реализация заносит в магазин сразу всех соседей, а этого-то и нужно избежать.
Следовательно, в нашем “магазине наручном управлении” придется хранить, помимо прочего, текущее направление curr, по которому пытаемся перейти из данной клетки в следующую (точный аналог локальной переменной w из стандарт
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
255
ной рекурсивной реализации поиска в глубину). Кстати, именно последовательность этих направлений и представляет собой необходимое описание границы, удобное для подсчета площади области. Кроме того, диапазон перебора значений curr зависит от того, откуда пришли в данную клетку, поэтому используем значение back, задающее направление, противоположное тому, по которому пришли в данную клетку. Когда Значение curr становится равным back, значит, все возможные значения curr исчерпаны.
Таким образом, магазин будет массивом структур следующего вида:
Record
curr, bac)$ : byte;
end;
Собственно координаты текущей вершины (“основной” параметр рекурсии) хранить не обязательно, поскольку их нетрудно вычислять по направлениям переходов3.
Разумеется, необходимо правильно эмулировать не только рекурсивные вызовы, но и откаты рекурсии. И не забыть то, ради чего вообще переходили от рекурсивной реализации к итеративной: обеспечить обрыв поиска в момент нахождения конечной клетки.
Реализация приведена ниже в листинге 9.5. Столь большое количество параметров обусловлено тем, что эту подпрограмму нужно запускать и для белой, и для черной областей. Параметры имеют следующий смысл:
•	the_i, the_j — координаты начальной клетки;
•	start-direction— первоочередное направление выхода из начальной клетки;
•	srch_ch — буква, задающая нужную область (“В” или “W”);
•	act_ch — буква, которой выделяются клетки, в данный момент считающиеся частью границы (при рекурсивной реализации можно было бы сказать, что рекурсивные вызовы именно с этими клетками в качестве параметров начались и не закончились);
•	dend_ch — буква, которой выделяются тупики (при рекурсивной реализации из этих клеток произошел откат рекурсии).
Значения act_ch и dend_ch записываются прямо в массив, отображающий поте игры, и по сути исполняют обязанности тех пометок, по которым поиск в глубину принимает решение, переходить ли в данную соседнюю клетку.
Вызовы функции select_area для поиска и обработки белой и черной областей имеют соответственно следующий вид:
select_area (1, 1, 0, 1, N, ' W' , 'w', ’.'J
select__area (N, N, 2, N, 1, ' B1, 'b',
Здесь N — линейный размер поля.
3 Эта экономия памяти кажется не принципиальной, но при плохой реализации алгорит-а оказывается необходимым 32-битовый компилятор, да еще с увеличенным размером программного стека, а при экономной — все помещается в DOS-овскую память.
256
ГЛАВА 9
Листинг 9.5. Выделение границы области
function select_area (the__i, the_j : byte; start_direction : byte; end_i, end_j : byte; s rch_ch, act_ch, dend_ch : char) : word;
var t : word;
Begin
{ Следующие 4 строки заполняют значения для начальной клетки и являются аналогом первичного вызова рекурсии из главной программы }
STACK[1].curr := start_direction;
STACK[1].back := (start_direction+2) mod 4;
map[the_i, the_j] := act—ch;
top := 1;
{ Поиск области }
while not((the_i=end_i) and (the_j=end_j)) and (top>0) do begin { условия выхода — достигнута конечная клетка или (на всякий случай) магазин пуст (закончился бы рекурсивный dfs) } { Поиск направления, по которому можно пройти дальше.
Если подходит направление curr, будет выбрано оно; если нет будут перебираться дальнейшие в порядке справа налево.
Если перебор дойдет до направления back, цикл обрывается } while -(STACK [top] .curr <> STACK [top] .back) and (map[the_i+di[STACK[top].curr], the_j+dj[STACK[top].curr]] <> srch_ch)
do STACK[top].curr := (STACK[top].curr +1) mod 4;
if STACK[top].curr = STACK[top].back then begin
{ Хорошего направления для продолжения не нашли, эмулируем откат из рекурсии } map[the_i,the_j] := dend_ch;
the_i := the__i+di [STACK[top] .back] ;
the_j := the__j+dj [STACK[top] .back] ;
dec(top);
end else begin
{ Нашли хорошее продолжение, эмулируем рекурсивный вызов} the__i := the_i+di [STACK[top] .curr] ;
the_j := the_J+dj [STACK[top] .curr] ;
map [the_i, the_j ] : = act_ch;
inc(top);
STACK[top].curr := (STACK[top-1].curr +3) mod 4;
{ вначале рассмотрим направление направо относительно текущего }
STACK[top].back := (STACK[top-1].curr +2) mod 4; end;
end; { Конец поиска области )
{ Вычисление площади области }
if start_direction о 0 then
for t := 1 to top do
STACK[t].curr := (STACK[t].curr+4-start_direction) mod 4; { При необходимости "поворачиваем" все направления, чтобы площадь любой области независимо от ориентации можно было считать так же, как площадь белой области }
{ Площадь считаем сразу; подробнее об этом ниже }
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
257
select_area : = Countsquare(top-1);
End;
Вычисление площади. Используем формулу (6.3), приведенную в конце подраздела 6.2.2. В нашей задаче формула (6.3) упрощается. Каждая сторона или вертикальна, т.е.	и слагаемое равно 0, или горизонтальна, т.е. (гл+1+/л)/2 равно
текущей /-координате. Однако номера клеток— это не совсем координаты. На рис. 9.3, а граница белой области выделена ломаной линией, проходящей по центрам клеток. Сдвинем эту границу параллельно на полклетки на юг и на восток, чтобы она проходила по линии между клетками (рис. 9.3, б). Теперь клетки, по которым граница направлялась на юг или на запад, оказались вне области (на рис. 9.3, б они выделены серым). Среди них находятся и /V клеток северной строки, по которой граница неявно направляется на запад, чтобы “замкнуть полигон”.
Рис. 9.3. Граница белой области и ее смещение
После того как граница построена, становится известным количество Т ее клеток (“сторон полигона”). Клетки северной стороны, неявно пройденные на запад, в границу не включены. Граница задает суммарное смещение на N-1 клеток на восток. Значит, N-1 сторона границы направлена на восток, а остальные T-(N-1) сторон дают нулевое суммарное смещение. Поэтому суммарное количество сторон, направленных на юг или на запад, равно (T-(N-1))/2.
Приведенные рассуждения реализованы во фрагменте программы (листинг 9.6). Предполагается, что выделение области реализовано как в листинге 9.5, т.е. элементы массива STACK в своих полях cur г хранят “абсолютные” направления сторон границы. В отличие от формулы (6.3), здесь нет “замыкания” области, но это не влияет на ответ, поскольку в начале и в конце границы /-координата the_i = о.
Листинг 9.6. Вычисление площади области по ее границе
function Countsquare(Т : word) : word;
var res : word; { накапливаемая площадь }
the i : byte; { i-координата текущей клетки границы }
258
ГЛАВА 9
begin
Вначале в площади res учтены клетки северной строки и суммарное число клеток, пройденных на юг или на запад } res := N + (T-(N-l)) div 2;
the_i := 0;
for k := 1 to T do begin
the_i := the_i + di[STACK[k].curr];
res := res + the_i*dj[STACK[k].curr] end;
Countsquare := res
end;
Оценки времени работы и объема памяти. Количество действий в алгоритме выделения области имеет оценку ©(Т^) = О(№) (где Т\ — суммарное число клеток границы тупиковых клеток); в алгоритме вычисления площади области — 0(7)=<?(№) (где Г— число клеток границы). Впрочем, эти оценки перекрываются оценкой ©(№) чтения входных данных.
Алгоритм выделения границы требует хранить все входные данные в массиве, т.е нужен объем памяти ©(№). Так что “узкое место” — это объем памяти...
►► Реализуйте всю программу.
9.2. Остовное дерево минимального веса
Задача 9.5. Есть несколько городов и дорог между некоторыми из них. Из любого города можно попасть в любой другой, проехав, возможно, по нескольким дорогам. Все дороги пришли в негодность, и их необходимо ремонтировать. Известна стоимость ремонта каждой дороги. Нужно обеспечить проезд из любого города в любой другой, затратив на ремонт как можно меньше средств.
Вход. Количества городов п и дорогт, где*2<п<100, п-1<т<л (л-1)/2, а также т строк с данными о ремонте дорог — тройками чисел вида u v d, где и и v - номера городов от0дол-1^ — стоимость ремонта дороги.
Выход. Список дорог (пар номеров городов) в произвольном порядке. Решений может быть несколько — вывести одно из них.
Пример. При л=4, т=5 и данных о стоимости ремонта дорог (0,1,6), (0,3,8), (0,2,6), (1,2,5), (2,3,5) есть два решения: {0,1}, {1,2}, {2,3}, а также {0,2}, {1,2}, {2,3}.
Анализ и решение задачи. Сформулируем задачу в терминах теории графов.
Города — это вершины графа, дор
ги — ребра, а стоимость ремонта дороги — вес
ребра. По связному графу с положительными весами ребер нужно построить остов
ное дерево с минимальным суммарным весом ребер. Оно называется остовным деревом минимального веса (ОДМВ).
• Задача построения ОДМВ имеет смысл только для неориентированных графов с нагруженными ребрами.
Ниже представлены два различных способа построения ОДМВ, но оба они основаны на следующем утверждении.
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
259
Теорема. Пусть в графе G=(V,E) выделено подмножество вершин U. Пусть среди ребер, соединяющих U и У\ U, наименьший вес имеет ребро {и, у}, где ие [/, ve V\ [7. Тогда для графа G существует ОДМВ, содержащее ребро {и, у}.
► Обозначим вес произвольного ребра {и, у} через J(u, у) и докажем методом “от противного”.
Предположим, что существует остовное дерево Г, не содержащее указанного ребра {и, у}, причем его вес меньше веса любого остовного дерева, содержащего {и,у). Добавим ребро {и, v} к дереву Т и получим цикл. Он содержит еще одно ребро, скажем {мр vj, соединяющее Un V\U, причем d(u,v)<d(uvvx). Удалив ребро {ир vj, получим остовное дерево, вес которого не больше веса Т. Это противоречит предположению, что вес Т меньше веса любого остовноео дерева, содержащего {и, у}. Значит, предположение о существовании Т
ошибочно, т.е. ОДМВ обязательно содержит ребг
{и, у}. 1
Первый способ — алгоритм Прима, или алгоритм ближайшего соседа. Для графа G=(V,E) с вершинами 0, 1, ..., п-1 строится подмножество вершин U, вначале состоящее из одной вершины, например 0. На каждом шаге выбирается ребро {и,у}, где и е U, v е V\ U, имеющее минимальный вес среди ребер с одним концом в U и другим в V\ U. Ребро добавляется к дереву, а вершина v (ближайший сосед) — Kt/. Дерево наращивается описанным способом, пока U не станет равным V.
Алгоритм Прима построения ОДМВ_______________________
{1} и :=i {0} ; Т ;= о;
{2} while U / V do begin
{3}	среди ребер, соединяющих Un V\U, найти ребро {uf w}
минимального веса;
{4} добавить ребро {u, w} к Т;
5} U := U U {v};
6} end
Рассмотрим реализацию алгоритма, которая позволяет быстро находить нужное ребро {и, >у} и обеспечивает сложность порядка О(п). Используем два массива clos -est и minw, индексированных вершинами. Для вершины w из множества V\Uзначением closest [w] является вершина и из U, ближайшая к и7, а значением minw [w] — вес ребра {closest [w], w}.
Вначале массив minw строится по списку ребер, инцидентных вершине 0, — для каждой вершины w значением minw [w] становится d(0,w). Если w и 0 несмежны, вес ребра полагается равным “машинной оо”, которую можно представить каким-нибудь очень большим числом. Например, если стоимости представляются в типе real, этим числом может быть 1.701е+38.
На каждом шаге в массиве minw находится минимальное значение и его индекс, скажем, г. Ребро {closest [г], г} добавляется к списку Т, а вершина г — к U. После этого необходимо изменить массивы closest и minw. Для вершины г, перешедшей в U, minw [г] =“+оо”. Для вершин и7, смежных с г, minw[iy] становится равным min{minw [w], J(w, г)}, а для несмежных ничего не изменяется. Соответственно пересчитывается и значение closest [w].
Оценим сложность описанной реализации. Первичное вычисление и изменение массивов closest и minw имеет порядок О(п), поскольку для этого нужен один
260
ГЛАВА .-
проход по множеству вершин. Поиск ребра (строка 3 алгоритма) происходит с помощью массива minw и также имеет порядок О(и). Цикл в строках 2-6 выполняется л-1 раз, поэтому общая сложность — О(п).
Второй способ — алгоритм Краскала. Ребра связного графа с п вершинами и т ребрами упорядочиваются и затем обрабатываются в порядке неубывания веса. Вначале вершины рассматриваются как отдельные компоненты связности (КС). На каждом шаге выбирается новое ребро с наименьшим весом, не образующее циклов с уже выбранными, т.е. соединяющее вершины в разных КС. Эти КС объединяются. Так пре-должается, пока выбранные ребра (в количестве л-1) не образуют остовное дерево Т.
► Докажем, что дерево Т, построенное таким образом, является ОДМВ. Пусть по представленному алгоритму к Тдобавлялись ребра eve2, ..., ел в порядке неубывания веса. Предположим, что существует остовное дерево Tmjn с меньшим весом, включающее ребра е2, .. еГ1 при наибольшем возможном /. Если добавить е. к Tmjri, образуется цикл, содержаши-некоторое ребро z, причем z*e. при 1 <j<i, посколькуер е2, ..., е не образуют цикла в Т. Н : в Т ребра е,, е2, eri, z также не образуют цикла, поэтому вес ребра z не меньше веса t иначе при построении дерева Т вместо е2 выбиралось бы z.
Рассмотрим ТСл=(Ти.л\{г})и{е.}. Т 'mjn является остовным деревом, поскольку замена в указанном выше цикле одного ребра другим не изменяет связанности вершин. Если вес : больше веса е, то вес T'min меньше веса Т — противоречие. Если веса z и et равны, то равны и веса T'mtn и Tmln, но T'mjn включает ребра ер е2,..., е., что противоречит предположеник о максимальности /. Поэтому Т — ОДМВ. <
Уточним алгоритм. Компоненту связности обозначим номером одной из его вершин и свяжем этот номер с каждой вершиной в виде отметки cmp (v). Номер очередного ребра обозначим через i, а количество КС, образованных выбранными ребрами, — через ncomp.
Алгоритм Краскала построения ОДМВ
{ 1} Упорядочить множество ребер Е по неубыванию веса;
{ 2} for (v G V) do cmp (v) := v;
{ 3} ncomp := 0; i := 0; T := < >;
{ 4} while ncomp > 1 do begin
{5} i := i+1; выбрать E[i] с концами u и w;
{6} if cmp(u) cmp(w) then begin
{ 7}	присоединить меньшую из КС, содержащих и и w,
к большей;
{ 8}	добавить ребро {u, w} к Т;
{9} ncomp := ncomp-1;
{10} end
{11} end
Рассмотрим возможную реализацию алгоритма. Распределение вершин по КС представим с помощью пяти массивов, индексированных номерами вершин 0, 1, ...,и-1. Каждая КС хранится в виде связанного списка в этих массивах.
В массивах first и last хранятся номера первых и последних вершин в списках. Вначале для всех j установим first [j] = last [j] = j. Если список с номером j становится пустым, first [j] и last[j] присваиваем-1. Номера КС, которым
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
261
принадлежат вершины, заданы в массиве стр. Вначале для всех j устанавливаем cmp [ j ] = j. Значением элемента массива cmpnum [ j ] является число вершин в КС, содержащей вершину j. Вначале для всех j устанавливаем cmpnumtj] =1. Наконец, в элементе массиве next[j] хранится номер вершины, следующей за вершиной j в списке КС. Вначале вместо номера элементу присваиваем -1.
Оценим сложность описанной реализации. Упорядочение ребер требует O(mlogm) времени. Цикл в строках 4-11 выполняется О(т) раз, однако объединение КС происходит только п-1 раз. С помощью массива cmpnum определяется, какая из объединяемых КС меньше, и меньшая КС присоединяется к большей. Пусть к — число вершин в меньшей КС, содержащей вершину и. Номера КС являются значениями стр [и] и стр [w]. С помощью first [стр [и] ] за 0(1) находится начало списка с вершиной и, а с помощью last [стр [w] ] — конец списка с вершиной w. Присоединение заключается в том, что начало списка с и присоединяется к концу списка с w за 0(1), а затем при движении по списку вершин меньшей КС изменяются соответствующие значения стр и cmpnum. После этого за 0(1) изменяются first [cmp [u] ], last [cmp [u] ] и last [cmp [w] ]. Таким образом, сложность присоединения — ©(к).
При каждом присоединении все вершины из КС, которая присоединяется и имеет размер к, попадают в КС, размер которой, как минимум, вдвое больше. Поскольку в итоге каждая вершина попадает в КС размером п, она участвует не более чем в logn присоединениях. Поэтому общее количество изменений номеров КС и количеств вершин в списках ограничено O(nlogn), откуда общая сложность выполнения цикла в строках 4-11 равна тах{О(т), O(«logn)}. Итак, сложность описанной реализации определяется сложностью сортировки весов ребер и равна 0(m log т).
• Из полученных оценок сложности алгоритмов Краскала и Прима следует, что при количестве ребер 0(п) алгоритм Краскала лучше. Однако если число ребер близко к О(п2), предпочтительнее алгоритм Прима.
н Реализуйте представленные алгоритмы самостоятельно.
9.3. Алгоритм Дейкстры и его применение
9.3.1.	Задача с одним источником и положительным весом ребер
Во многих задачах вес ребра рассматривают как его длину, а длиной маршрута считают сумму длин ребер. Традиционная задача — найти длины кратчайших путей, соединяющих все пары вершин (длины маршрутов рассматривают как расстояния между вершинами). Эту задачу можно решить, постепенно добавляя возможные промежуточные вершины (см. подраздел 8.3.4).
Часто нужны не все попарные расстояния, а только расстояния от заданной вершины (источника) до всех остальных (задача с одним источником). Для графов с неотрицательным весом ребер эта задача имеет более эффективные способы решения.
Задача 9.6. Есть города с номерами от 0 до п -1 и дороги между некоторыми из них. Из любого города можно попасть в любой другой, проехав, возможно, по нескольким дорогам. Известны длины всех дорог (естественно, положительные).
262
ГЛАВА 9
Путешественник желает узнать минимальные длины путей из “своего” города с номером 0 во все другие.
Вход. Количества городов п и дорогт, где 2<л<100, и-1<т<п (п-1)/2, а также т строк с данными о дорогах — тройками чисел вида u v 1, где и и v — номера городов от 0 до п -1,1 — длина дороги.
Выход. Последовательность расстояний до городов 1,..., л -1.
Пример. При л = 5, т=6 и данных о дорогах (0,1,6), (0,4,7), (1,3,3), (4,3,1), (1,2,6), (3,2,1) (рис. 9.4) получим последовательность 6,9, 8,7.
Рис. 9.4. Граф с нагруженными ребрами
Анализ и решение задачи. В терминах графов задача выглядит так. Найти минимальные длины маршрутов, соединяющих заданную вершину s связного графа со всеми остальными (для упрощения считаем, что s=0).
В задаче вес ребер графа положителен. В этом частном, но практически важном случае одним из наиболее эффектив|гых является алгоритм Дейкстры. Рассмотрим его
Пусть G= (V, Е) — граф с источником з. Каждое ребро е нагружено неотрицательным весом 1(e) — длиной. По своей идее алгоритм Дейкстры похож на алгоритм Прима. Постепенно строится множество вершин U, для которых расстояния уже найдены. Вначале U= {л}, затем на очередном шаге ко множеству U добавляется Ya из оставшихся вершин, расстояние до которой от источника минимально. Для каждой вершины i хранится ее текущее расстояние d(i) от $ — расстояние, полученное на предыдущих шагах. Вначале вершина s имеет расстояние 0, смежные с ней удалены от нее на длину ребра, остальные — на бесконечность.
Описанная идея уточняется в следующем алгоритме (s=0).
Алгоритм Дейкстры для графа с неотрицательным весом ребер
{ 1} и := {0}; d(0) := 0;
{ 2} for v := 1 to n-1 do
{ 3} if {0, v} G E then d(v) := 1(0, v)
{ 4} else d(v) := °°;
{ 5} while U # V do begin
{ 6} найти в V\U вершину w с минимальным расстоянием d(w);
{ 7} добавить w к U;
{ 8} for (v g N(w)) do
{ 9} if vgU then d(v) := tnin(d(v), d(w)+l(w, v) ) ;
{10} end
Проимитируем алгоритм Дейкстры на примере из условия задачи (см. рис. 9-4). Состояние данных перед началом очередного цикла while отображено в табл. 9.1.
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
263
Таблица 9.1. Имитация алгоритма Дейкстры
и	W		d(2)	d(3)	d[4)
{0}	-	6	оо	оо	7
{0,1}	1	6	12	9	7
{0,1,4}	4	6	12	8	7
{0, 1,4,3}	3	6	9	8	7
{0,1,4, 3,2}	2	6	9	8	7
Анализ решения. Докажем правильность алгоритма.
► Пусть на некотором шаге вершина w добавляется к U. Расстояние d(yv) вычислено как длина кратчайшего маршрута, проходящего от s к w только по вершинам из U. Оно в этот момент не больше, чем расстояние от j до остальных вершин из V\U. Поэтому длина маршрутов из s в w, проходящих через какую-нибудь еще вершину из V\U, не меньше d(w),4 т.е. в графе нет маршрута от s к w, длина которого меньше d(w). Итак, расстояния вычисляются правильно. <
1
Оценим сложность алгоритма. На каждом шаге к U добавляется одна вершина, поэтому цикл в строках 5-9 выполняется я -1 раз. Поиск вершины с минимальным расстоянием и пересчет остальных расстояний имеют сложность О(п). Поэтому сложность алгоритма не больше О(л2).
Для разреженных графов с числом ребер т порядка 0(л), например, планарных или близких к ним, оценку сложности О(л2) можно снизить за счет организации данных. Представим граф с помощью структуры смежности. Граф вводится как последовательность троек вида (вершина, вершина, длина), поэтому построение структуры смежности имеет оценку 0(ш).
Принадлежность вершин множеству U представим в булевом массиве U. Для работы с расстояниями вершин используем два массива. В массиве D с индексами от О до л-2 хранятся расстояния от источника до вершин с номерами от 1 до и -1. В массиве Т с индексами от 0 до л-2 в виде сортирующего дерева (оно же пирамида, ей. подраздел 5.3.4) хранятся номера вершин от 1 до л-1, частично отсортированные так, что при к= 0,1,2,..., ndiv2-1 выполняются следующие неравенства.
£>[71*]] £>[712* +1]] и £>[71*]] < £>[ Д2£+2]]	(9.1)
Сортирующее дерево в массиве Т обеспечивает быстрый поиск минимального расстояния среди вершин из V\U. По мере добавления вершин к U дерево уменьшается, занимая все более короткий участок в начале массива Т. Последний элемент дерева указывается переменной last.
Вначале расстояния вычисляются (строки 2-4) и вершины сортируются в массиве Г с соблюдением условий (9.1). При этом last-n-2. Сложность этого этапа — 0(nlogn).
Вершина w с минимальным расстоянием является значением элемента ДО], а само расстояние — £>[710]]. ДО] и Hlast] меняются местами, а затем last уменыпа-
4 Если вес ребер может быть отрицательным, этого утверждать нельзя.
264
ГЛАВА»
ется на 1. Это может нарушить свойство (9.1), и его нужно восстановить (см. подраздел 5.3.4). Сложность этих действий — O(logn).
Пересчитывая расстояния для вершин v из множества V\U (строка 8), использу ем список смежности вершины и> и массив U. Использование списка смежносп вместо перебора всех вершин гарантирует, что каждая пара смежных вершин {v, рассматривается только дважды — при добавлении к U вершин w и у. Тогда пересчет расстояния по всем шагам происходит 0(ш) раз.
При каждом изменении расстояния в массиве D восстанавливается свойство (9. в массиве Т. При этом новое значение может подняться в дереве, проделав путь н верх длиной O(logn). Поскольку расстояния пересчитываются 0(лт) раз, общая сложность всех пересчетов расстояний есть O(mlogn).
Учитывая оценки сложности, полученные для всех этапов реализации алгоритма, приходим к выводу, что ее сложность имеет оценку O(m\ogn). При лг=0(л) это лучше, чем О(л2).
» Реализуйте алгоритм Дейкстры и решите задачу самостоятельно.
►> В приведенном псевдокоде алгоритма Дейкстры предполагается, что граф является связным. Однако на практике иногда приходится искать кратчайшие пути в нагруженных графах, для которых такой гарантии нет. Модифицируйте алгоритм Дейкстры, чтобы он правильно работал и в этой ситуации (для всех достижимых вершин находил кратчайшие расстояния, для не достижимых устанавливал факт недостижимости). Указание. Например, можно добавить условие: если поиск в строке 6 не нашел минимального расстояния меньше то основной цикл нужно оборвать. Недостижимы те и только те вершины, для которых 4=°°.
Н Оптимизируйте алгоритм Дейкстры для частного случая, когда нужно искать не расстоя ния от начальной вершины до всех остальных, а расстояние между двумя заданными вершинами. Указание. Можно добавить условие досрочного выхода из цикла, когда вершина, выбранная в строке 6, совпадает с целевой. Однако среднее ускорение работы при этом невелико, а объем полезных результатов алгоритма уменьшается существенно. Так что используйте этот прием только тогда, когда уверены в его целесообразности.
►» Поэкспериментируйте с различными структурами данных для представления графа и тем, как и$ выбор влияет на скорость работы программы.
• Ставить задачу о кратчайших путях и решать ее с помощью алгоритма Дейкстры можно как для неориентированных графов, так и для орграфов.
9.3.2.	Максимальный груз
Задача 9.7. Есть города с номерами от 0 до л -1 и дороги между некоторыми из них. Из любого города можно попасть в любой другой, проехав, возможно, через некоторые другие города. Известно, какой максимальный груз можно провести по каждой из дорог (положительная величина). Йужно узнать, какие максимальные грузы можно доставить из города 0 в остальные города.
Вход. Количества городов л и дорог гл, где 2<и<100, и-1<т<л(л-1)/2, а также гп строк с данными о дорогах — тройками чисел вида u v 1, где и и v — номера городов от 0 до л -1, 1 — грузоподъемность дороги.
Выход. Максимальные грузы до городов 1,..., л -1.
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
265
Пример. При п=5, т—6 и данных о дорогах (0,1,6), (0,4,7), (1,3,3), (4,3,1), I (1,2,6), (3,2,1) (см. рис. 9.4) получим последовательность 6,6, 3,7.	|
Анализ и решение задачи. Вспомним алгоритм Дейкстры. После очередного выбора вершины w (“ближайшего соседа” множества U) она может стать предшественником на кратчайшем «маршруте из источника в вершины вне U. С учетом этого пересчитываются минимальные расстояния до вершин вне U:
for (v е V\U) do d(v) := miil(d(v) , d(w) +1 (w,v)) .
В нашей задаче роль длины дороги играет ее грузоподъемность. Длина маршрута — это сумма длин его дорог, а грузоподъемность маршрута — минимум грузоподъемностей его дорог, т.е. в роли суммы длин дорог выступает минимум их грузоподъемностей. Минимальному (по всем маршрутам) расстоянию до города v соответствует максимальная грузоподъемность d(y) (в частности, длине маршрута 0 — грузоподъемность °°).
Итак, для решения нашей задачи достаточно лишь слегка изменить алгоритм Дейкстры.	।
Модификация алгоритма Дейкстры для вычисления максимальной грузоподъемности
{ 1} U ;= {Of; d(0) :=
{ 2} for v 1 to n-1 do
{ 3} if (0, v) G E then d(v) := 1(0, v)
{ 4} else d(v) := 0;
{ 5} while U V do begin
{ 6} найти в V\U вершину w с максимальной грузоподъемностью d(w);
{ 7} добавить w к U;
{ 8} for (v g N(w)) do
{9} if vgU then d(v) := max{d(v), min{d(w), l(w,v)}};
{10} end
H Завершите решение задачи.
9.3.3.	Зал Круглых Столов
Задача 9.8. В Зал Круглых Столов можно попасть, только пройдя через коридор. Вертикальные в плане стены коридора изображены на карте прямыми линиями, параллельными оси OY системы координат. Вход в коридор находится на плане внизу, выход в зал — вверху. В коридоре есть круглые в плане колонны одинакового радиуса Ra.
По данным о размерах коридора и размещении колонн нужно найти максимальный диаметр круглого стола, который можно пронести через коридор, сохраняя поверхность стола горизонтальной.
Вход. В первой строке входного текста TABLE. DAT записаны XL и Хй — х-координаты левой и правой стен коридора. Во второй строке — целое число Ra (радиус колонн), 1 <7?0< 10б. В третьей — целое число N (количество колонн), 1<7V<2OO. Дальше в каждой из N строк записаны по два числа — х- и у-
266
ГЛАВА 9
координаты центра соответствующей колонны. Все координаты — целые числа, которые по модулю не больше 106.
Выход. Искомый диаметр — действительное число с точностью три знака после десятичной точки (даже если оно окажется целым). Если столы вообще нельзя пронести, вывести 0.000.
Пример
Вход	Выход
0 90	47.000
3 4 10 10 70 10 50 50 10 90
Анализ и решение задачи. Решать задачу можно несколькими способами. Вначале рассмотрим “основной” способ (с нашей точки зрения, наилучший), а затем еще Два.
Анализ кругов и анализ линий. Круглый стол диаметром D можно пронести через коридор тогда и только тогда, когда существует путь снизу вверх, на котором все проходы (расстояния между колоннами слева и справа от пути) имеют ширину не меньше D.
Стены и виртуальные колонны. Ограничение на движение стола накладывают как колонны, так и внешние боковые стены (левая и правая). Но боковые стены создают ограничение для движения стола только тогда, когда необходимо пронести стол между колонной и боковой стеной. Если стена слева (с х-координатой Х£) и колонна имеет координаты (хру), то вместо стены можно рассматривать новую (виртуальную) колонну. Удобно считать, что у виртуальных колонн тот же радиус, что и у реальных, заданных на входе, поэтому она будет иметь радиус Ro и центр в точке (XL-R0,y). Вместо правой стены можно рассматривать виртуальную колонну с центром в (XR+R0,y).
Вопрос, где именно добавлять виртуальные колонны, можно решать по-разному. Простейший способ — ввести левую и правую виртуальные колонны для каждой реальной. Общее количество колонн при этом увеличивается втрое.
Попробуем уменьшить количество колонн. Рассмотрим условия, при которых левая виртуальная колонна не нужна (для правой условия аналогичны). Пусть реальная колонна имеет центр в точке (хру). Расстояние от нее до левой стены равно a =x-Xl-R0. Если есть Другая реальная колонна с центром в (ху,уу), причем х;<х( и расстояние между точками (хру) и (х^у) меньше а+27?0 (ширина прохода между этими колоннами меньше а), то проход между стеной и колонной (хру) шире, чем проход между колоннами (хру) и (ху,уу) и проход между стеной и колонной (xfyj). Поэтому левая виртуальная колонна с координатой х не нужна, поскольку ограничивает диаметр
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
267
стола в меньшей степени, чем соседние реальные проходы или другая виртуальная колонна. Если же “в окрестностях” (хру) колонны Ц,у;) с указанными свойствами нет, виртуальная колонна с координатой х нужна.
Уменьшение диаметра колонн. Если, не перемещая центров колонн (в том числе виртуальных), уменьшить их диаметры на величину &d<2R0, то искомый диаметр стола увеличится на bd.
► Если диаметр колонн уменьшается на Az/, то ширина всех проходов между колоннами увеличивается на AzZ (на М!2 за счет каждой из двух колонн). Значит, и все проходы на том пути, по которому можно пронести стол наибольшего диаметра, увеличиваются на М. <
Поэтому будем решать задачу, считая колонны точками, а затем из полученного решения вычтем 27?0.
Графы преград и заборы. Увеличим диаметр колонн так, чтобы некоторые колонны касались или даже пересекались. Если ширина прохода между какими-то двумя колоннами была d, а диаметр увеличился на Az/, то при М < d между колоннами остается проход шириной d-M, а при &d>d проход исчезает. Будем говорить, что в этой ситуации появляется ребро преграды. Его концы — это центры данных двух колонн, длина — начальное расстояние d между ними.
Зафиксировав Az/, получим некоторую совокупность ребер преград и их концов — центров колонн. Концы ребер, т.е. некоторые из центров колонн, будут вершинами графа преград. Подчеркнем: разные значения Az/ дают разные графы преград.
Маршрут в графе преград, который не имеет повторений вершин и связывает левую и правую виртуальные колонны, назовем забором (он соединяет левый край коридора с правым). Граф преград, в котором есть такой маршрут, назовем перегораживающим.
Параметром забора в перегораживающем графе назовем наибольшую из длин ребер преград, входящих в забор, а параметром перегораживающего графа — наименьший из параметров всех его заборов.
• При увеличении М появляются новые преграды с большими длинами и новые заборы, параметры которых только увеличиваются. Поэтому, если при некотором пороговом значении Az/o появляется забор (его параметр как раз и равен Az/o), параметр перегораживающего графа с ростом Az/ остается равным этому Az/o.
Параметр графа преград и максимальный диаметр стала
Утверждение. Круглый стол диаметра D нельзя пронести через коридор тогда и только тогда, когда в графе преград существует забор, параметр которого строго меньше D.
► Достаточность (если есть забор, то стол пронести нельзя) очевидна, поскольку, пронося стол, нужно пересечь каждый забор.
Необходимость (если стол пронести нельзя, то существует забор). Рассмотрим движение стола фиксированного диаметра D. Пусть он вначале двигается снизу вверх, постоянно касаясь левой стены. Так продолжается, пока стол не коснется точки колонны Ар расстояние от которой до левой стены меньше D. Тогда начнем поворачивать стол вокруг точки А! как центра вращения против часовой стрелки.
268
ГЛАВА 9
Стол может или сделать оборот вокруг точки А] так, чтобы вновь коснуться левой стены (тогда точку А, можно в дальнейшем не рассматривать) или коснуться точки А2 (A}A2<D В этой ситуации начнем поворачивать стол вокруг точки А2 по тем же правилам. Если при таких последовательных вращениях вокруг Ар А2, Ая стол вернется в некоторую предыдущую точку А( (1И£п), то точки А^рА^.Ая можно в дальнейшем не рассматривать
продолжить вращения стола вокруг А(.
По предположению, стол не может дойти до верхнего края коридора. “Зациклиться” он тоже не может. Следовательно, движение должно прерваться, когда нельзя будет поворачивать стол вокруг колонны против часовой стрелки. А это возможно, если только стол упрется в стену. Если это левая стена, то снова начинаем весь процесс, который снова не может продолжаться вечно. Значит, в конце концов стол должен упереться в правую стену. А это означает, что в процессе движения стола «ак раз и построен забор с параметром меньше D. <
Переформулируем доказанное утверждение: круглый стол диаметра D можно пронести через коридор тогда и только тогда, когда все заборы в графе преград имеют параметр не меньше D.
Последнее утверждение позволяет свести вычисление максимального диаметра стола к вычислению наименьшей длины ребер преград, при которой в графе преград существует забор. Эту величину назовем критическим параметром графа.
Вычисление диаметра. Вначале образуем граф преград со всеми левыми виртуальными колоннами в качестве вершин и пустым множеством ребер. Затем, пока не появится забор, будем пошагово добавлять вершины (и ведущие к ним ребра) к графу преград следующим образом. Каждый раз к графу добавляем вершину (центр реальной или правой виртуальной колонны), расстояние которой от уже построенного графа минимально. Это обеспечивает, что наибольшая длина ребер преград маршрутах, начинающихся в левых виртуальных колоннах, остается минимальной. Поэтому, когда при добавлении вершин образуется забор, его параметр является минимальным среди параметров всех возможных заборов.
Расстояние от построенного графа преград до еще не включенной в него вершины v( считаем как расстояние от v( до ближайшей к ней вершины графа.
Алгоритм вычисления диаметра
1.	Инициализация:
а)	построить виртуальные колонны;
б)	d := 0;
в)	образовать граф преград изо всех левых виртуальных колонн;
г)	для всех реальных и правых виртуальных колонн вычислить расстояния от графа преград.
2.	Пока граф преград не достиг правой стороны коридора, повторять: а) среди вершин, еще не включенных в граф преград, выбрать
вершину с минимальным расстоянием d_curr_min от графа и включить ее в граф;
б)	если d_curr_min > d, то d := d_curr_min, иначе d не изменяется;
в)	для всех вершин, еще не включенных в граф преград, уточнить расстояние до графа как минимум текущего расстояния до графа и расстояния до вершины, только что
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
269
добавленной к графу.
3.	Окончательным результатом является d-2*R0, где R0 - радиус колонн.
» Обоснуем измейение значений d в п. 26. Если наименьшее расстояние до новых вершин d_curr_min больше^, то при текущем значении d невозможно расширить связный граф преград, а минимальное значение, при котором можно продолжить расширение,— d_curr_min. Следовательно, чтобы продолжить построение, нужно увеличить параметр графа преград d, положив его равным d_curr_min. Если же d_curr_min^d, значит, раньше в процессе построения графа уже возникала необходимость увеличить параметр до d, поэтому уменьшать его сейчас нет оснований. <
Поскольку нужно только значение d, можно не поддерживать граф как структуру, а хранить лишь признаки того, что вершины принадлежат построенному графу преград.
Представленный алгоритм имеет немало сходства с алгоритмом Прима построения минимального остовного дерева и с известной модификацией алгоритма Дейкстры. Он не является ни одним из этих алгоритмов, хотя и основан на них.
Анализ объема памяти и количества действий. Помнить нужно координаты всех колонн, признаки принадлежности колонн графу и расстояния от него (для всех колонн, кроме левых виртуальных). Таким образом, не считая отдельных переменных, для работы алгоритма нужен объем памяти порядка O(JV), но с довольно большим константным множителем.
Время работы алгоритма, очевидно, составляет О(№). Именно столько действий нужно для выполнения п.п. 1г, 2а, 2в (весь цикл 2 повторяется O(N) раз) и 1а (с описанным способом выбора виртуальных колонн); остальные пункты требуют или O(N), или 0(1) времени.
Краткий анализ других методов решения. Вычисление критического параметра перегораживающего графа преград с помощью бинарного поиска. Приведенные выше рассуждения подталкивают к бинарному поиску критического параметра: выбираем “пробную” длину ребер преград и исследуем, получается ли забор (например, с помощью поиска в глубину). В качестве начальных приближений можно взять, например, ^min=0 и	Условие выхода из бинарного поиска— достижение
точности КГ3, указанной в условии.
Этот алгоритм проще, чем приведенный выше “основной”, но заметно менее эффективен. Ведь запускать вспомогательный алгоритм поиска забора, пусть и более простой, нужно на каждом шаге бинарного поиска. Кроме того, любой алгоритм бинарного поиска принципиально не может дать точного ответа.
Применение модификации алгоритма Дейкстры к триангуляции Делоне. Предположим, нужно найти путь, по которому проходит стол наибольшего диаметра. Как указано в работе [21] (упражнение 26.4-6), эту задачу можно решить, модифицировав алгоритм Дейкстры, но используя оценки, как в задаче о максимальном грузе (см. подраздел 9.3.2). Это значит, что нужна не минимальная сумма длин ребер, а максимальный груз, лучшей считается не минимальная оценка, а максимальная, при подсчете оценки вершины вычисляется не сумма оценки предыдущей вершины и длины ребра, а их минимум.
270
ГЛАВА 9
Граф, по которому проходит путь стола, строится на основе триангуляции Делоне (подробности изложены в работе [23, 34] и других источниках, связанных с вычислительной геометрией).
Триангуляция — это разбиение области на треугольники, вершинами которых являются заданные точки. Триангуляция Делоне — это такая триангуляция, у которой ни одна вершина не попадает в круг, описанный вокруг любого треугольника триангуляции.
Для триангуляции Делоне (но не для произвольной триангуляции) выполняется следующее утверждение.
Утверждение. Пусть АВ, ВС viCA — стороны треугольника триангуляции Делоне Пусть максимальный возможный диаметр стола, который можно пронести через отрезок АВ внутрь ААВС, равен D' (D'<AB). Тогда самые большие возможные диаметры столов, которые можно пронести через отрезки ВС и СА из ААВС определяются как min{Z/, ВС} и ппп{/У, СА} соответственно. (Без доказательства.)
Данное утверждение позволяет определить вершины и ребра графа. Верщинами графа, к которому применяется модифицированный алгоритм Дейкстры, являются ребра триангуляции Делоне. Эти вершины смежны в графе тогда и только тогда, когда принадлежат одному треугольнику триангуляции.
Оценки, как всегда, сопоставляем вершинам графа (ребрам триангуляции), длин ребер графа как таковых нет; при построении оценки вершины графа берется минимум оценки предыдущей вершины и длины ребра триангуляции, соответствующей данной вершине.
Разумеется, этот алгоритм существенно сложнее, чем алгоритм вычисления критического параметра перегораживающего графа. При правильной реализации он также будет работать быстро. Точней, его не очень сложно реализовать с оценками памяти O(N) и времени О(№). Однако в принципе возможна (технически очень сложная) реализация с оценкой времени работы ©(WlogAf).
Кроме того, из всех упомянутых алгоритмов только алгоритм “Делоне+Дейкстры” с очень небольшими модификациями позволяет искать не только максимальный диаметр стола, но и путь, по которому нужно нести стол. Это не нужно в нашей задаче, но может оказаться существенным в других условиях.
9.4. Скоростная алхимия
Задача 9.9. Алхимику известны несколько веществ и алхимических реакций5 с ними. Каждая реакция по набору входных веществ порождает набор выходных. Реакция возможна при наличии всех ее входных веществ (неважно, в каком количестве).
Проведение каждой реакции имеет определенную длительность. Все вещества по окончании реакции можно выделить в чистом виде для дальнейшего использования. Одновременно может протекать произвольное количество реакций.
5 Ниже слово “алхимический” для краткости не пишем.
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
271
Помогите Алхимику узнать, за какое минимальное время он, имея некоторый набор исходных веществ, сможет получить целевое вещество. Нужно также составить план запуска реакций. В плане могут быть избыточные реакции (без которых в действительности можно обойтись).
Вход. В перво^ строке текста записаны количество веществ к, реакций п, исходных веществ т (3 < £<250,1 < п <500, т<к), а также номер целевого вещества. Исходные вещества имеют номера от 1 до т, остальные — от т+1 до к. Далее следуют п блоков по три строки, описывающих реакции. В первой строке блока указана длительность реакции, во второй — количество ее входных веществ и список номеров этих веществ, в третьей — количество выходных
веществ и список их номеров.
Все числа в строках разделены пробелами. Входных и выходных веществ в каждой строке не более чем /=10. Суммарная длительность всех реакций не больше 2-10’.
Выход. В первую строку текста записать момент получения целевого вещества, в следующие строки момент начала и номер реакции. Отсчет времени начинается с 0; реакции нумеруются от 1 до п в соответствии с порядком на входе. Моменты начала реакций не убывают; при равенстве моментов возрастают номера реакций. Решение может быть не единственным; вывести любое из них. Если решения нет, вывести -1.
Пример
Вход 6 5 2 3
4 1 2 2 2 6 5 1 1 2 4 2 10 1 6 2 2 5 4 2 4 6 2 15 10 2 15 2 3 6
Выход
19
0 1 0 2
5 4 9 5
Заметим, что реакция 3 оказалась ненужной. Кроме того, если бы реакция 2 имела вид не 1-> 2 4, al—>4 6, то реакция 1 в плане стала бы лишней.
Анализ задачи. Представим реакции и вещества вершинами орграфа. От реакции дуги ведут к ее выходным веществам, а к ней ведут дуги от входных веществ. Отметки дуг означают длительность. Дуги, ведущие от реакций к веществам, отмечены длительностью реакций, а от веществ к реакциям — нулем (вещество сразу доступно для использования в реакции).
Вершины-вещества, имеющиеся вначале, являются стартовыми. Из них можно достичь вершин-реакций, все входные вещества которых находятся среди стартовых. Из полученных вершин-реакций достижимы вершины-выходные вещества и т.д.,
272
ГЛАВА 9
пока не будет достигнуто целевое вещество или не окажется, что новые вершины не добавляются. Нужно построить расписание, позволяющее как можно быстрее достичь целевого вещества6.
Заметим сразу, что уже проведенную реакцию запускать еще раз бессмысленно поскольку все ее выходные вещества уже есть. Поэтому реакция используется тальке один раз. Естественно запускать ее, как только появились все ее входные вещества.
Если целевого вещества нет среди исходных, а набор исходных веществ не позволяет запустить ни одной реакции, задача не имеет решения.
Итак, пусть в начальный момент времени некоторые реакции возможны. Запустив их, получим несколько моментов времени их окончания и, возможно, новые вещества, образуемые к моментам окончания реакций. Запомним моменты времени в порядке возрастания. Затем обработаем их, запустив реакции, которые еще не использованы и допустимы наборами веществ (исходных или полученных) в эти моменты. Запущенные реакции дадут новые моменты и вещества. И так далее.
Реакция, запущенная позже, может закончиться быстрее, чем запущенная раньше. Поэтому необработанные моменты окончания реакций придется организовать в очередь, поддерживая в ней упорядоченность элементов по возрастанию (очередь с приоритетами).
Запуск реакций прекращается, когда использованы все реакции, допустимые в свои моменты времени, или получено целевое вещество.
Для того чтобы выдать ответ, нужно восстановить данные о том, какие реакции привели к целевому веществу, если оно получено. Реакция входит в план обязательно вместе с предшествующими ей реакциями (породившими ее входные вещества к моменту ее начала). Заранее не известно, какая реакция войдет в план, поэтому нужно знать для каждой реакции, какие ее входные вещества появились к моменту ее начала и в результате каких реакций.
Итак, для каждого вещества будем хранить наименьший момент его получения и номер реакции, первой породившей его. (В действительности, эта реакция может оказаться избыточной.) Для исходного вещества момент получения определим равным 0 — порождающая его реакция не нужна.
Для формирования плана проведем обратный ход. Включим в план одну из реакций, породивших целевое вещество. Включая реакцию, определим предшествующие ей реакции и тоже включим в план. Действуем так, пока есть предшествующие реакции. В полученном плане упорядоченность, указанная в условии, может не соблюдаться, поэтому перед выводом отсортируем его.
Вопросы реализации. Структуры данных. Начнем с представления веществ. Анализируя, можно ли запустить реакцию, нужно знать, получены ли все ее входные вещества. Значит, нужно уметь определять, получено ли заданное вещество.
Для представления веществ используем массив Substance, индексированный номерами от 1 до250. Элемент массива— это запись, полй которой created и byReaction представляют момент образования вещества и номер порождающей реакции (типа integer). Моменты образования исходных веществ инициализируем значением 0, остальных веществ — значением-1/. Номера реакций вначале по-
6 Описанный способ передвижения по орграфу нельзя назвать стандартным, поэтому данная задача не является типично “графовой”. Но, как увидим, ее решение имеет много общего с некоторыми алгоритмами для графов с нагруженными ребрами.
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
273
жим равными 0 и в дальнейшем используем 0 как признак отсутствия порождающей реакции.
По условию, количество реакций не больше 500, поэтому для их представления подойдет массив Reaction с индексами от 1 до 500. Элемент массива будет хранить момент начала реакции и два массива из 10 номеров веществ каждый (по условию, реакция имеет не больше 10 входных и 10 выходных веществ). Инициализировав омент начала значением -1, можно в дальнейшем считать -1 признаком того, что реакция не использована. Массивы номеров инициализируем нулями.
Массив реакций заполняем при вводе. Номера веществ запоминаем в массивах номеров веществ. Если входных или выходных веществ меньше 10, в соответствующем массиве номеров останутся нули — первый из них будет признаком окончания списка входных или выходных веществ в реакции.
Очередь моментов окончания реакций можно организовать в виде пирамиды (см. одразделы 5.3.4 и 9.3.1). Моментов окончания реакций не больше, чем самих реакций, т.е. не больше п, поэтому пирамида обеспечит оценку O(logn) для вставки и удаления элемента.
Выходную последовательность пар вида (момент, реакция) длиной не больше п нужно сортировать по возрастанию моментов. Поэтому запомним ее в массиве TimeTable из 500 элементов.
Уточнение алгоритма. Обрабатывая строки, представляющие реакцию, номера веществ занесем в элемент массива реакций. Номера исходных веществ определяют элементы в массиве Substance, полю created которых нужно присвоить 0.
Обрабатывая момент времени t, начальный или удаляемый из очереди, нужно определить, какие реакции запускаются в этот момент. Просмотрим массив реакций и для каждой неиспользованной реакции определим, все ли ее входные вещества имеют моменты образования от 0 до t. Если это так, запомним t как момент начала реакции, а момент окончания (сумма t и длительности реакции) занесем в очередь. Для выходных веществ в массиве Substance пересчитаем значения поля created и, возможно, изменим поле byReaction.
Когда целевое вещество получит положительный момент образования /, начнем обратный ход. Момент/и номер реакции к, породившей целевое вещество, запишем в первый элемент массива TimeTable. По номерам входных веществ реакции к определим реакции, породившие эти вещества, и моменты их запуска. Запишем их номера и моменты запуска в TimeTable. Далее используем массив TimeTable как очередь, растущую в конце и исследуемую, но не уменьшаемую в начале.
Добавление реакций в TimeTable чревато тем, что одна и та же реакция, будучи предшественницей различных реакций, может попасть в расписание несколько раз. Можно не обращать на это внимания и убрать дубли после сортировки TimeTable. Однако лучше перед добавлением реакции проверить, не была ли она добавлена раньше, и если была, то не добавлять ее. Для этого можно, например, добавляя реакцию, изменить момент ее начала (в соответствующем элементе массива Reaction) на -1 и далее использовать -1 как признак того, что реакция добавлена.
Анализ сложности. Ввод данных имеет очевидную оценку О(пГ)+<Э(т). Каждая реакция запускается не более чем один раз, поэтому количество моментов окончания реакций ограничено оценкой О(п). В каждый из моментов нужно учесть появившиеся вещества и определить запускаемые реакции (они не были запущены раньше
274
ГЛАВА9
и стали допустимыми). Суммарная (по всем шагам) сложность добавления вещест» имеет оценку О(п1), определение запускаемых реакций — О(п\ удаление реакций из очереди — О(п). Если очередь с приоритетами реализовать сортирующим деревом, то суммарная сложность запуска реакций (помещения их в очередь) имеет оценку O(nlogn). Итак, суммарная сложность имитации реакций — О(п).
Рассмотрим обратный ход. Поскольку каждая реакция добавляется в расписание не более чем один раз, суммарная сложность добавлений О(п). Исследуя реакцию в расписании, нужно просмотреть ее входные вещества, найти реакции-предшественницы и определить, какие из них должны быть добавлены в расписание. Суммарная сложность этих действий — О(п1). Сортировка расписания имеет сложность O(nlogn), поэтому сложность решения всей задачи — О(п).
Упражнения
9.1.	Граф имеет не больше 100 вершин. Длины ребер — целые положительные числа (не обязательно одинаковые). Найти максимальное расстояние между верши-
. нами графа и хотя бы одну пару вершин, находящихся на этом расстоянии.
9.2.	Высоты, на которых находятся клетки поля размером пхп, где 2<п< 100, заданы целыми числами типа integer. Соседними считаются клетки, имеющие смежную сторону. Заданы координаты начальной клетки и нужно найти:
а)	один из кратчайших путей из начальной клетки в еще одну заданную клетку при условии, что подъем между соседними клетками возможен, если разность высот не превышает заданного порога;
б)	количество достижимых клеток (при условии в п. а).
9.3.	В задаче о лабиринте (см. подраздел 9.1.2) заданы начальная и конечная клетки. Построить какой-нибудь из кратчайших путей, который их соединяет (если существует).
9.4.	В задаче о лабиринте (см. подраздел 9.1.2) подсчитать количество различных кратчайших путей между двумя заданными клетками.
9.5.	Найти путь в лабиринте с минимальным количеством поворотов (количество пройденных клеток несущественно).
9.6.	Найти путь в лабиринте, имеющий минимальную сумму количества пройденный клеток и количества поворотов.
9.7.	Есть город со строго квадратными кварталами, некоторые перекрестки закрыты. Найти кратчайший путь между двумя заданными перекрестками (какой-либо, если вообще существует).
9.8.	Прямоугольный белый лист имеет размеры АхВ, егц стороны параллельны координатным осям плоскости хОу, а левый нижний угол находится в начале координат. На листе лежат цветные прямоугольники; их стороны параллельны его сторонам и не выходят за его пределы. Одноцветные прямоугольники, имеющие хотя бы одну общую точку, считаются частями одной видимой фигуры. Найти площади всех одноцветных фигур.
Вход. Первая строка текста — координаты верхней правой вершины листа и количество цветных прямоугольников п. Каждая из следующих п строк со
’^АФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
275
держит данные о прямоугольнике: координаты левой нижней и правой верхней вершины, а также цвет. Порядок данных соответствует порядку помещения прямоугольников на лист. Все координаты — целые числа в пределах от О до 100, цвет задан целым числом от 1 до 100 (1 — белый цвет).
Выход. Каждая строка вывода соответствует цвету, присутствующему на листе. В строке указан номер цвета, количество и площади фигур этого цвета. Порядок площадей значения не имеет. Цвета упорядочены по возрастанию.
Пример
Вход	Выход
333	13131
0022	15	312
00111	15 12
112 3 3
9.9.	Лабиринт состоит из NxN квадратных клеток; его внешние стороны и некоторые из внутренних сторон клеток закрыты стенами. По лабиринту нужно пройти к источнику живой воды. Путь к источнику существует не всегда, но можно проходить сквозь стены, правда, не более чем К раз. Выходить за пределы лабиринта нельзя.
Хедом считается перемещение в соседнюю по горизонтали или по вертикали клетку. Вычислить минимальное количество ходов, за которое из клетки с координатами (1,1) можно дойти до источника с координатами (P,Q). Верхняя левая клетка лабиринта имеет координаты (1,1), нижняя левая — (N, 1).
Вход. В первой строке текста записаны числа N, К, Р, Q (2< 7V<200, 0< А?<250, \ <P<N, \ <Q<N). В следующих TV-1 строках— по TV чисел 0 или 1, обозначающих горизонтальные в плане стены между клетками (1 — стена есть, 0 — нет). Следующие N строк содержат TV-1 обозначений вертикальных стен между клетками.
Выход. Найденное количество ходов или -1, если пути нет.
Пример
Вход
3 12 3
0 0 0
0 10
1 0
1 0
0 0
Выход
3
Вид лабиринта
9.10.	Решите задачу 8.1 (см. подраздел 8.1.4) при условии, что длительности прохождения сообщений по различным соединениям могут быть различными (целыми положительными).
276
ГЛАВА
9.11.	Точки на плоскости (не более чем 104) заданы парами координат. Соединить их отрезками с концами в этих точках так, чтобы по отрезкам можно бьь пройти из любой точки в любую другую, а суммарная длина отрезков была минимальной.
9.12.	В алгоритме Дейкстры при добавлении вершины w ко множеству U можно дописывать ребро {/’[w], w} к списку ребер, представляющему остовное дере во. Является ли оно ОДМВ?
9.13.	В условиях задачи 9.6 построить кратчайший путь до заданного города.
9.14.	Узлы сети связаны соединениями, имеющими неотрицательные задержки Маршрут образуется цепью соединений; максимум их задержек является задержкой маршрута. Найти маршрут между двумя заданными узлами сети, имеющий минимальную задержку.
9.15.	Строители нефтепровода привезли на лесную поляну трубы и бросили их там в беспорядке, причем так, что они не соприкасаются. Гном может подходить как угодно близко к лежащей трубе, но не может перелезть через нее. Помогите гному найти кратчайший путь между двумя заданными точками поляны. Толщиной труб можно пренебречь.
9.16.	Как известно, Луна покрыта кратерами. Луноход может двигаться между ними или по их краям. Определить длину кратчайшего пути лунохода между двумя заданными точками на лунной поверхности между кратерами. Кратеры представляют собой правильные окружности. Поверхность считается плоской, а не сферической. Ни одна из двух заданных точек не находится строго внутри какого-либо кратера.
Вход. Первая строка — пары координат заданных точек. Вторая строку — количество кратеров и тройки чисел, задающих координаты центров кратеров и их радиусы. Все числа действительные. Выход. Длина пути — действительное число.
9.17.	Некоторые компании являются совладельцами других компаний, поскольку приобрели часть их акций. Компания А контролирует компанию В, если выполнено хотя бы одно из следующих условий:
А = В;
А владеет более чем 50% акций В;
А контролирует несколько компаний Q, С2, .... С*, владеющих соответственно Xi, Х2,..,, Хк процентов акций В, и при этом Х\ + Х2 +... + Хк > 50.
Известно, какими количествами процентов акций компаний владеют другие компании. Определить все пары компаний, в которых первая контролирует вторую.
Вход. В первой строке текста записано количество ^компаний N, представленных номерами от 1 до N, и количество М следующих строк. Далее в каждой строке указаны три целых числа i, j, р, означающих, что компания i владеет р% акций компании/ (1 < i<N, 1 < j£N, 1 <р< 100).
Выход. Последовательность строк с парами номеров компаний k, I, где к*1 и к контролирует I, расположенные по возрастанию к, а при равных к — по возрастанию I.
ГРАФЫ КЛЕТОК И ГРАФЫ С НАГРУЖЕННЫМИ РЕБРАМИ
277
9	18. Решить задачу 9.9 с учетом того, что количество веществ не ограничено, а сами вещества задаются названиями из двух латинских букв (заглавной и строчной). Реакцию описывает строка следующего вида.
Список_входных_веществ = Список_выходных_веществ; Длительность
Названия веществ и знаки “=” разделены пробелами в произвольном количестве; длительность задана после знака и пробела целым положительным числом типа integer. Кроме того, количество входных и выходных веществ в строке реакции может быть произвольным.
Глава 10
Комбинаторика
В этой главе...
♦	Правила суммы и произведения и их применение
♦	Основные комбинаторный объекты: размещения, перестановки и сочетания без повторений и с повторениями
♦	Биномиальные коэффициенты и некоторые их свойства
♦	Рекуррентные соотношения, вычисления на их основе, вычисления с помощью таблиц
« Принцип включений и исключений и его применение для вычислений
*	Разбиения множеств на подмножества, разбиения чисел на слагаемые и раскладки
Под термином комбинаторика будем подразумевать раздел математики, который изучает способы подсчета количества различных объектов, удовлетворяющих определенным правилам. При программировании такого подсчета обычно требуется, чтобы он происходил существенно быстрее, чем перебор всех возможных конкретных объектов.
В задачах комбинаторики часто встречаются стандартные комбинаторные объекты — перестановки, размещения и сочетания. Для количеств этих простейших комбинаторных объектов (“амеб”) существуют довольно простые формулы. Эти и многие другие формулы комбинаторики выводятся с помощью основных правил — правил суммы, произведения и еще нескольких безымянных правил.
В реальных задачах комбинаторные объекты, как правило, строятся по более сложным правилам, чем перестановки, размещения или сочетания, и простых формул для их подсчета нет и быть не может. Чаще всего количество объектов определенного размера выражается с помощью количества объектов меньших размеров, т.е. в виде рекуррентных соотношений.
В данной главе представлены примеры задач, решение которых задается рекуррентными соотношениями. Для вычислений на основе соотношений применяются таблицы промежуточных значений и/или рекурсивные подпрограммы.
Количества комбинаторных объектов велики и, как правило, не представимы в стандартных целочисленных типах. В данной главе для упрощения изложения эта проблема игнорировалась, но для реализации большинства представленных здесь решений необходима “длинная арифметика”. Ее применение оставлено читателям для самостоятельной работы.
280
ГЛАВА 10
10.1. “Амебы” комбинаторики
10.1.1. Правила суммы и произведения
Правило суммы. Пусть все способы выполнить действие делятся на п групп так, что каждый конкретный способ относится к одной и только одной из этих групп. Пусть известны количества способов в каждой группе: кх,к2, кп. Тогда общее количество способов выполнить действие равно кх +к2+ ...+кп.
Простейший пример. Есть две зеленые и три красные чашки. Сколькими способами можно выбрать чашку (либо красную, либо зеленую)? Всего способов 2+3, н складываются не количества чашек, а количества способов выбрать чашку среди зеленых и среди красных. Более содержательные примеры представлены ниже.
Правило произведения. Пусть сложное действие состоит из и последовательных этапов, и первый этап можно выполнить любым из кх способов, второй — любым из к2 способов независимо от способа выполнения первого этапа и т.д. Тогда действие в целом можно выполнитькхк2-... кп способами.
Примеры. Есть две тарелки и три ложки. Можно собрать 2-3=6 наборов, состоящих из тарелки и ложки. Их полный перечень —
<тарелка 1, ложка 1>, старелка 1, ложка 2>, <тарелка 1, ложка 3>, <тарелка2, ложка 1>, <тарелка2, ложка 2>, старелка 2, ложка 3>.
Из города А в город D можно проехать либо через город В, либо через город С Проехать из А в В можно кх способами, из А в С — к2 способами, из В в D — к3 способами, из С в D — к4 способами. Сколькими способами можно проехать из А в D1
Для подсчета используем оба правила. Поскольку все дороги проходят либо через В, либо через С, справедливо правило суммы: подсчитать количества путей через В и через С и результаты сложить. По правилу произведения есть кхк3 различных путей из А в D через В, поскольку любой способ проезда из А в В комбинируется с любым способом проезда из В в D. Аналогично подсчитывается количество путей из А в D через С. Итого кхк3+к2к4 путей. 
Правило произведения имеет еще одну формулировку.
Пусть последовательность собирается из п элементов, причем в качестве первого можно взять любой из кх элементов, в качестве второго — любой из к2 элементов независимо от выбора первого элемента и т.д. Тогда всю последовательность можно собратькхк2-... кп способами.
КОМБИНАТОРИКА
281
10.1.2. Перестановки, размещения и сочетания без повторений
Перестановки. Переставляя элементы 1, 2, 3, получим последовательности <1,2,3>, <1,3,2>, <2,1,3>, <2,3,1>, <3,1,2>, <3,2,1>.
Перестановки (без повторений) — это последовательности, которые содержат все элементы одного и того же множества по одному разу, но отличаются порядком.
Количество перестановок элементов n-элементного множества обозначается Ря и определяется следующей формулой:
Р„ = п(п-1)-...-21 = и!	(10.1)
► Будем строить перестановки, сначала выбирая элемент на первое место, затем на второе, и т.д. На первое место можно поставить любой из п элементов, на второе — любой из п-1 оставшихся (кроме уже использованного на первом месте) и т.д., пока не дойдем до последнего места, для которого остается только один элемент. Выбор элементов для начальных мест не влияет на количество элементов, остающихся для заполнения последующих мест, поэтому по правилу произведения получаем (10.1). 1
Размещения. Из элементов 1, 2, 3 можно образовать все возможные последовательности длиной 2 без повторения элементов: <1,2>, <1,3>, <2,1>, <2,3>, <3,1>, <3,2>.
Размещениями (без повторений) из ппо к называются последовательности, образованные к попарно различными элементами из п возможных.
Количество размещений по к элементов из и обозначается А*, (п)к или пк и определяется следующими равенствами:
А* = п(п-1)-...(п-А:+1) = п!/(п-Л)!	(10.2)
Для аналитических преобразований удобнее частное факториалов, а для вычислений — произведение.
► Доказательство аналогично предыдущему, только останавливаемся на к-м месте, где можно разместить любой из п-Л+1 остающихсяюлементов. <
Классический пример размещений — тройки призеров в следующей простой задаче: В соревнованиях принимают участие восемь команд. Сколько может быть различных результатов, т.е. распределений команд по призовым местам (первое, второе, третье)?Очевидно, что результат — это тройка призеров, т.е. размещение из 8 по 3, аих Aj3 = 8-7-6=336.
Сочетания. Из элементов 1, 2, 3 можно образовать все двухэлементные подмножества: {1,2}, {1,3}, {2,3}. Их называют сочетаниями (изтрех по два).
Сочетания (без повторений) из п по к — это подмножества, образованные к элементами из п возможных.
282
ГЛАВА 10
В отличие от размещений, у сочетаний не важен порядок элементов. Классический пример задачи на количество сочетаний: Сколькими способами из п учеников можно выбрать к учеников в экскурсионную группу (порядок выбора не имеет значения
_	t	(п>\
Количество сочетаний по к элементов из п обозначается С„ или и определя-'Л J
ется равенствами
С* = ”	_ п-	(Ю з
"	1-2‘...-к	(п-к)’.кГ
Для аналитических преобразований удобнее вторая формула (с факториалами а для вычислений — первая. Обычно целесообразно вычислять так: сначала найта (n(n-l))div2, затем умножить его на (п-2) и разделить целочисленно наЗ и тл_ Это позволяет, оставаясь в рамках целочисленной арифметики, в значительно мере избежать больших промежуточных значений при не очень большом оконча тельном результате.
► Доказательство формулы (10.3) опирается на формулу (10.2), поскольку нужно доказать, что = А* /к!. Рассмотрим все сочетания и все размещения для одних и тех же л и к. Одному и тому же сочетанию соответствует несколько различных размещений (например, сочетанию {2,3} — размещения <2,3> и <3,2>). Их количество равно к!, поскольку они образуются как перестановки элементов этого сочетания. Таким образом, каждому сочетанию соответствует группа из к! размещений и каждое размещение входит в одну йз эта групп. Поэтому количество сочетаний в к! раз меньше количества размещений. <
* Подобные рассуждения применяются в комбинаторике довольно часто. Их можно было бы назвать правилом частного, но этот термин не стал общепринятым.
10.1.3. Перестановки, размещения и сочетания с повторениями
Размещения с повторениями. Из элементов 1, 2, 3 можно образовать последовательности длиной 2, в которых элементы могут повторяться: <1,1>, <1,2>, <1,3> <2,1>, <2,2>, <2,3>, <3,1>, <3,2>, <3,3>.
Размещения с повторениями из п пот — это последовательности длины т, в которых п возможных элементов могут повторяться.
На каждой из т позиций в последовательности независимо от других может находиться любой из п элементов, поэтому по правилу произведения общее количество размещений — л".
Перестановки с повторениями. Сколько различных слов можно образовать, переставляя буквы слова АБРАКАДАБРА! Рассмотрим два способа вычисления.
Первый способ. Ясно, что каждое такое слово взаимно однозначно представляется набором позиций, на которых стоят неотличимые между собой буквы А, набором позиций с буквами Б и т.д. Всего позиций 11 (длина слова), и пять из них для буквы А выбираются С,5, способами. Независимо от выбора остается шесть позиций, из ко
КОМБИНАТОРИКА
283
торых две выбираются для буквы Б (С% способов), и т.д. Итого получается С,3, х С2 х С42 х С] х С} способов выбрать позиции для букв разных видов, т.е. способов образовать слова. С помощью формулы (10.3) легко получить, что это произведение 11' ..
равно----------, т.е. 83ч 60.
5!-2!-2!1И!
Второй способ. Каждой букве присвоим дополнительный номер так, чтобы одинаковые буквы стали различными. Из этих пронумерованных букв можно образовать 11! перестановок. Но есть 5! перестановок номеров букв А, независимо от них есть 2! перестановок букв Б и т.д. Поэтому количество перестановок непронумерованных 11'
букв в 5!-2!-2!1!1! раз меньше, чем пронумерованных, т.е. -------.
5!-2!-2М!-1!
Все перестановки букв слова АБРАКАДАБРА имеют один и тот же состав, т.е. последовательность (5,2,2,1,1) количеств повторений букв Л, Б, Р, К, Д.
В общем случае есть к2 неотличимых между собой элементов первого типа, к2 — второго,..., кя — n-готипа.
Последовательность чисел (кх,к2,...,к^, у которой kv kv .... fcn>0, ki+k2+.,.+kn=m, называется (п,т)-составом. Перестановки с повторениями состава (fcp kv ...,кп) — это последовательности длины к{ + к2+ ...+кп, которые содержат kt элементов первого типа, к2 — второго,..., кп — л-го типа и отличаются порядком следования элементов разных типов.
Количество перестановок с повторениями состава (kt, к2, ...,кп) обозначается через P(kt, к2.кп) и выражается формулой
(10-4)
*!• *2- •••
Сочетания с повторениями. Рассмотрим пример: есть неограниченный запас шариков, отличающихся только цветом — белым (Б) или черным (Ч). Взяв любые три шарика, можно получить один из таких наборов их цветов: {Б, Б, Б}, {Б,Б,Ч}, {Б,Ч,Ч1, {Ч, Ч, Ч}. Этим наборам цветов соответствуют (2,3)-составы (3,0), (2,1), 1,2), (0,3).
Обобщим пример: вместо цветов рассмотрим типы элементов rp t2...tn, вместо
шариков — элементы, которые отличаются один от другого только этими типами, т е. однотипные элементы считаются одинаковыми.
Сочетание с повторениями из п (типов) по т (элементов) — это множество, содержащее т элементов, каждый из которых имеет один из типов rp t2,..., ta.
- ______________________________________
Указанные выше четыре набора двух цветов у трех шариков являются примером всех возможных сочетаний с повторениями из двух (типов) по три (элемента).
Элементы одного типа считаются одинаковыми, поэтому сочетания отличаются только количествами элементов каждого типа, т.е. составами.
284
ГЛАВА 1
Количество различных сочетаний с повторениями из и по т обозначим// и докажем, что/"= С", = С"7* .. " J ц	т+я—1	т+п-1
► Количество элементов i-ro вида (i=l, 2, ...,п) в произвольном сочетании обозначив через к.. Закодируем сочетание следующим образом: запишем сначала кг цифр 1, затем разделитель 0 (если = 0, сразу запишем 0), затем к2 цифр 1, затем разделитель 0 (если к2 = в коде будут два 0 подряд) и т.д.
Нетрудно убедиться, что все коды, состоящие из т цифр 1 и п-1 цифры 0, взаимно однозначна соответствуют всем сочетаниям с повторениями цз п по т. Но любой такой код взаимно однозначно представляется подмножеством позиций, на которых стоят, например, цифры 1. Веете позиций zi+m-1, поэтому количество кодов равно С”+яЧ или, что то же,	.<
Количество (п, ш)-составов также равно//. Заметим, что каждый (и,т)-состаи является решением уравнения х1+х2+ ...+xb=s=wi (n> 1) в целых неотрицательных чис лах и наоборот, каждое решение этого уравнения является (п, ш)-составом. Поэтом» количество решений такого уравнения равно//.
10.1.4.	Размещения и сочетания как отображения
Рассмотрим представление размещений и сочетаний в виде отображений, т.е функций, всюду определенных на некотором множестве. Пусть Р={1, 2,..., т А = {а{, а2,..., ая], причем множество А упорядочено: at <а2<... <ап. Отображенйя Р в А называют конфигурациями (см., например, [30]). Множество А — это множество элементов, из которых образуются размещения и сочетания, Р — множество номеров, поэтому конфигурации, по сути, являются последовательностями.
Все возможные конфигурации — это последовательности длиной т, состоящие из элементов А, т.е. размещения из п по тс повторениями. Другим видам размещении и сочетаниям соответствуют конфигурации, удовлетворяющие тем или иным условиям.
Инъективное отображение Р в А — это последовательность длиной т с попарн различными элементами. Такие последовательности существуют при т<п соответствуют размещениям без повторений. При т = п эти размещения являются перестановками (без повторении).
Зафиксируем (и,/и)-состав (kx,k2, ...,кя) и рассмотрим все возможные отображения, у которых а, как образ встречается к2 раз, а2 — к2 раз, ..., ая — кп раз. Эти отображения взаимно однозначно соответствуют перестановкам с повторениями состава (к^,к2,...,к1).
Будем говорить, что отображение С'.Р->А строго монотонно возрастает, если С(г) < C(j) при i<j. При т<п строго монотонно возрастающее»отображение взаимн однозначно соответствует m-элементному подмножеству множестваА, т.е. сочетанию (без повторений).
Если C(i) < C(j) при i < j, то отображение С называется монотонно неубывающим Такому отображению взаимно однозначно соответствует (п, т)-состав (kvk2,...,k определяющий количества повторений элементов ар а2, ...,ая как образов в отображении С. Но (и, »г)-составы взаимно однозначно соответствуют сочетаниям с повто
КОМБИНАТОРИКА
285
рениями из п по т, поэтому монотонно неубывающие отображения можно рассматривать как представления таких сочетаний.
Представление размещений и сочетаний в виде последовательностей дает основу для общего подхода к их порождению, описанному в главе 11.
10.1.5.	Биномиальные коэффициенты
Бином Ньютона — это выражение вида (а+Ь)". Бином раскладывается в сумму произведений некоторых степеней его слагаемых а и Ь, например, при л=2 и л = 3: (а+Ь)2=a2+2ab+b2, (а+Ь)3 = а3+За2Ь+ЗаЬ2+Ь3.
Разложим (а+Ь)" в многочлен при произвольном л > 0, пронумеровав скобки числами от 1 до л. Очевидно, каждое слагаемое содержит л сомножителей — некоторое количество к сомножителей а и п-к множителей Ь, т.е. имеет вид ab"~*', где 0<к<«. Каждое такое слагаемое взаимно однозначно сбответствует подмножеству номеров скобок, из которых для образования слагаемого взяты сомножители а. Поэтому и!
слагаемых акЬ',~к столько, сколько таких подмножеств, т.е. С* =-:. Отсюда
"	к\(п-к)\
(а+Ь)п= ХСк„акЬя-*. л=о
Коэффициенты С* при называются биномиальными, поскольку записываются в разложении бинома (а+Ь)". Биномиальные коэффициенты доопределяются при к<0 ик>п как С* =0. Воспользуемся этим доопределением ниже, рассматривая суммы, в которых сложение ведется по коэффициенту к в выражениях вида С*. Это позволит не записывать границы, в которых изменяется к.
Нетрудно убедиться в правильности следующих тождеств:
С‘ = С”~к = —-—, "	"	к!(п-к)!
(«+!)"=	(1+1)” = 2я = ^С‘,(-1+1)я = 0я = £(-1)*С*.
*=0	к=0	к=0
Последнее тождество, в частности, позволяет естественно определить 0° как 1 (следуя работе [12]). Из него следует тождество £С„2* = ^С2к+1 =2"“'. к	к
Запишем биномиальные коэффициенты для начальных значений л = 0, 1,..., 5 в треугольную таблицу, которая называется треугольником Паскаля:
1
1 1
1 2 1
13 3 1 1 4 6 4 1 1 5 10 10 5 1
Эта таблица иллюстрирует еще одно тождество:
С* = C»*-i + <£? при л > 0.	(Ю.5)
286
ГЛАВА 1
Оно легко доказывается с помощью формулы для С*:
(и-1)!	(п —1)!	_(п-1)1(п-к+к) _ п!
(п-1-к)!к!+ (и-*)!(Л-1)!	к\(п-к)\	к\(п-к)\'
Из формулы (10.5) несложно получить полезные тождества
у- гчк _	'V-' (^к-т _ (~>к
''n+1 » Zj'-'п-м ''я+1 • m=k	ш=0
Тождество (10.5) позволяет говорить о биномиальных коэффициентах как о треугольных числах. Записанные по строкам, они образуют треугольник, причем элементы любой его строки (кроме первой) рекуррентно выражаются с помощью элементов предыдущей строки. Другие примеры треугольных чисел представлены в разделе 10.5.
Докажем еще одно тождество — £ СкС*~к = C”+s , называемое тождеством Коши к
► Используем следующую наглядную интерпретацию. Пусть есть г девушек и s юноше Справа в тождестве записано количество способов выбрать из них всех п лиц. Каждое слагаемое в сумме слева задает количество способов выбрать п лиц так, чтобы среди них было к девушек из г и п-к юношей из s. Сумма этих количеств по всем возможным значениям дает количество всех способов выбрать из них всех п лиц. Поэтому значения выражений слева и справа равны. <
Если заменить к на к+т, а л на п+т, то получим тождество Вандермонда уч ^лт+к ^jn—k _Qtnt+л
к
10.2.	Рекуррентные соотношения и таблицы
10.2.1.	Пути в квадратных кварталах
Задача посвящается генеральному плану застройки города Черкассы 1826 года.
Задача 10.1. Город образован квадратными кварталами (рис. 10.1, а). В таком городе обычно есть много различных путей одинаковой минимальной длины, соединяющих одну и ту же пару перекрестков (рис. 10.1, б).
а)	б)
Рис. 10.1. Пути в городе с квадратными кварталами
Улицы, вертикальные на карте, пронумерованы слева направо, горизонтальные — снизу вверх. Координаты перекрестка задаются сначала номером
КОМБИНАТОРИКА
287
вертикальной улицы, затем горизонтальной. Некоторые перекрестки закрыты, т.е. через них проходить нельзя.
Программа должна по координатам начального, конечного и закрытых перекрестков найти количество различных допустимых кратчайших путей.
Гарантируется, что начальный и конечный перекрестки не закрыты. Если все кратчайшие путй перекрыты, ответом должен быть 0 (даже если существуют более длинные допустимые пути).
Вход. Первая строка текста содержит координаты начального перекрестка, вторая — конечного, третья — количество закрытых перекрестков N, следующие N строк — координаты закрытых перекрестков.
Выход. Количество путей.
Примеры
Вход	1	1	Выход 3 Вход	5	5	Выход О
2	3	5	9
О	1
5 7
Анализ задачи. Очевидно, кратчайшими являются только те пути, в которых все одноквартальные перемещения происходят в “правильных” направлениях. Например, если конечная клетка выше и правее начальной, каждый “шаг” должен быть направлен или вправо, или вверх. Дальнейшие рассуждения проводятся именно для движения “вправо вверх”, но в программе нужно учесть все возможные взаимные размещения старта и финиша.
Итак, для каждого перекрестка (внутри прямоугольника, образованного начальным и конечным перекрестками) найдем количество способов попасть к нему, двигаясь от начального только вправо или вверх.
Все допустимые пути, ведущие к данному перекрестку, естественно разбиваются на две группы — пришедшие на последнем шаге от перекрестка, соседнего слева, и от перекрестка, соседнего снизу. Поэтому применимо правило суммы: количество способов прийти к перекрестку равно сумме количеств способов прийти к перекресткам, соседним слева и снизу.
Если для некоторого перекрестка левый или нижний сосед оказывается левее или ниже начального, соответствующее слагаемое равно 0; если сосед — закрытый перекресток, “его” слагаемое также равно 0. Для начального перекрестка количество способов равно 1.
Применение описанных правил на примере города с начальным и конечным перекрестками (0,0) и (6,5) приведено в следующей таблице (закрытые перекрестки 1,1), (1,2) и (6,4) обозначены знаком х).
5	1	3	7	16	36	77	77
4	1	2	4	9	20	41	X
3	1	1	2	5	11	21	36
2	1	X	1	3	6	10	15
1	1	X	1	2	3	4	5
0	1	1	1	1	1	1	1
	0	1	2	3	4	5	6
288
ГЛАВА 1
Итак, для решения задачи нужен двухмерный массив, который заполняется, например, по строкам, от начального перекрестка к конечному или в противополож ном направлении.
В частном случае, когда закрытых перекрестков нет, построенная таблица оказывает ся частью повернутого треугольника Паскаля и ответ выражается простой формулой с£+д>, где Ах и — расстояния между начальным и конечным перекрестками по горизонтали и по вертикали. Формулу С£+4х также получим, если рассмотрим путь кж последовательность Ах+Ау перемещений, из которых нужно выбрать Ах горизонтальных.
Н Реализуйте описанные вычисления с использованием стандартных типов данных.
Н Реализуйте описанные вычисления в длинной арифметике, стараясь использовать кж можно меньше памяти. Указание. Для вычисления значений в текущей строке достаточно одной предыдущей строки, поэтому двумерный массив длинных чисел не нужен.
10.2.2.	Правильные скобочные выражения
Задача 10.2. Найти количество правильных слов длиной 2л, состоящих из открывающих и закрывающих скобок (см. задачу 2.10). Например, при п=1 это слово (),прии=3 — ((О)), (О О), (О) О, О (0), О О ().
Вход. Количество пар символов в слове.
Выход. Количество различных правильных слов.
Примеры. Вход: 1; выход: 1. Вход: 3; выход: 5.
Анализ задачи. Правильные слова будем называть правильными скобочными выражениями (ПСВ). Используем термины позиция в слове (номер символа, начиная с 1 и \i..j]-nodaioeo (слово изо всех подряд букв с i-й noji-ю включительно).
Очевидно, на первой позиции ПСВ может находиться только “(”. Рассмотрим, на какой позиции в слове может находиться парная к ней “)”. Поскольку скобки парные, между ними записано ПСВ1, поэтому “)” находится на четной позиции. Пос. скобки “)” может быть записан “остаток” слова, который тоже Должен быть ПСВ Итак, ПСВ длиной 2 л можно записать в виде
( ПСВ длиной 2(i-l) ) ПСВ длиной 2(n-i),
Т1	t 2i
где 1< i<n.
Разобьем все ПСВ на л групп в соответствии со значением i. К этим группам применимо правило суммы, а внутри каждой группы — правило произведения, п скольку любое “левое” ПСВ длиной 2(i-l) можно комбинировать с любым “правым” ПСВ длиной 2(л-г). Таким образом, приходим к следующей формуле:
К(в)= ^,я=1К(/-1) K(n-i)
Надеемся, что для программирования по этой формуле читатель не станет применять рекурсию, а использует что-то вроде следующего фрагмента, дополнив его надлежащим образом:
1 ПСВ может быть пустым, т.е. последовательностью, в которой нет ни одной буквы. Иначе говоря, это строка '', имеющая длину 0.
КОМБИНАТОРИКА
289
а[0] := 1;
for i := 1 to THE_N do begin
a[i] := 0;
for j := 1 to i do a[i] := a [i]+a[j-1]*a[i-j]; end;
и Реализуйте представленные вычисления.
Количество К(п) правильных скобочных выражений длиной 2и называется п-м числом Каталана. Отметим без доказательств, что для нахождения чисел Катала-
на можно использовать формулу К(п)= —— С2"л, а для быстрой оценки порядка л + 1
4"
их возрастания — формулу К(л)=0,564—=. n-Jn
10.2.3.	Счастливые билеты
Задача 10.3. В городе Глупове общепринята р-ичная система счисления (вместо десятичной), а номера троллейбусных билетов состоят из 2k разрядов (каждый разряд — одна р-ичная цифра). Билет считается счастливым, если сумма первых к разрядов равна сумме последних к разрядов.
Вход. Значения р и к.
Выход. Количество счастливых билетов.
Примеры. Вход'. 2 2; выход: 6. Вход: 10 3; выход: 55252.
Анализ задачи. По условию сумма левых к разрядов и сумма правых к разрядов должны совпадать между собой, но обе суммы могут принимать Любое значение от 0, когда все цифры равны 0, до к(р-Г), когда все цифры — максимальные р-йчные. Если разбить все счастливые билеты на группы соответственно значениям сумм, то к этому разбиению можно применить правило суммы.
Займемся подсчетом количества номеров внутри групп. Рассмотрим для примера группу с суммой s=2 при к=3,р= 10. Полный перечень счастливых билетов представим следующим образом:
002'		002
011		011
020 101	X'	020 101
ПО		ПО
200		200
Здесь слева и справа записаны все трехцифровые последовательности с суммой 2. Все пары этих последовательностей дают номера с суммами 2, и каждый счастливый билет с суммами 2 является парой каких-то из этих последовательностей.
290
ГЛАВА 10
Итак, внутри каждой группы действует правило произведения, поэтому
Nlotal=£^\N(S))2,	(Ю.6
где N(s) — количество ^-цифровых р-ичных последовательностей с суммой s. Чтобы применить формулу (10.6) к решению задачи, нужно построить алгоритм вычисления N(s).
Решение задачи. Для начала рассмотрим такой алгоритм. Заведем массив счетчиков с индексами от 0 до к(р-1). Переберем все it-цифровые р-ичные последовательности и учтем каждую в том счетчике, у которого индекс равен сумме цифр последовательности. Эта первичная идея реализована для it=3 в очевидном фрагменте программы2, к := 3;
for i ;= 0 to к*(р-1) do N[i] := 0;
for il := 0 to p-1 do
for i2 := 0 to p-1 do
for i3 := 0 to p-1 do
N[il+i2+i3] := N[il+i2+i3] + 1;
N_tot := 0 ;
for i := 0 to k*(p-l) do
N_tot := N_tot+sqr(N[i]);
Однако нам нужно работать с произвольными значениями к, а не только с 3. Следующая идея — перебрать в порядке возрастания it-цифровые р-ичные числа, представив каждую их цифру в отдельном элементе массива. Как для произвольного к организовать перебор в порядке возрастания всех it-цифровых р-ичных чисел, станет ясно из главы 11. Такой путь вполне возможен, причем перебор р‘ половин номера билета происходит существенно быстрее, чем перебор вообще всех номеров рт 0 до ри-1 и проверка каждого на “счастливость”. Но все же р* — многовато. Тем более что, решая комбинаторную задачу, желательно избегать подсчета объектов с помощью их перебора. А пока перебор (половин номеров) есть.
Покажем, как найти количества N(s) комбинаторно. Дадим ему его настоящее обозначение N(s,p,k), поскольку N(s) зависит от р ик. Любая последовательность р-ичных цифр заканчивается какой-то из цифр 0, 1, ...,р-1. Используем этот признак, чтобы разбивать числа на группы и применять правило суммы; k'-значная последовательность (к'>2) имеет сумму s, если она.заканчивается числом i (0< i<p-l), а остальные к'-1 разрядов имеют сумму s-i:
N(s,p,V)= ^N(s-i,p,k'-l).	(Ю-7)
При kf= 1 и s<p все значения N(s,p, 1)=1; опираясь на них, по формуле (10.7) можно найти значения N(s,p, 2) для всех s от 0 до 2(р-1), затем значения N(s,p, 3) и тд. до заданного к. Итак, нужно завести массив и заполнить его согласно этим рассуждениям.
Вычисления по формуле (10.7) таят “подводные камни”. Например, для вычисления Д17,10,2) нужно значение N(17,10,1), т.е. количество последовательностей деся
2 Просим менее опытных читателей обратить внимание на полноценное использование прямого доступа в выражении N [ii+i2+i3].
КОМБИНАТОРИКА
291
тичных цифр длиной 1, имеющих сумму 17, а для вычислении N(7,10,2) — значение М(-2,10,1). Удивляться не будем: раз такие последовательности невозможны, значит, их количество равно нулю3. В следующей программе эти ситуации учтены с помощью дополнительных проверок, находится ли индекс в пределах возможных значений. Другой способ — расширить массив и заполнить его “лишние” элементы нулями.
Оценим необходимые размеры массива. В массиве должны вычисляться и храниться значения N(s,p, к) для всех возможных s, получаемые при пошаговом увеличении к, ap=const, поэтому достаточно массива, индексированного s и к. Поскольку 0< s<k(p-i), “ширина” массива должна быть А(р-1)+1 ~рк. Однако при построении значений используются только значения N(...,p,k'-l), поэтому достаточно массива раз-ерами не ~кхрк, а =2^рк. Общий объем памяти пропорционален рк, т.е. затраты памяти по сравнению с предыдущими программами существенно не увеличиваются.
Программа, использующая формулу (10.7), приведена в листинге 10.1; в ней предполагается, что к(р-1)+1 <5000.
Листинг 10.1. Программа вычисления количества “счастливых” билетов
▼ar N : array[0..1] of array[0..5000] of QWord;
При вычислениях по формуле (10.7) из N[0] берутся данные
для к'-1 и для к' запоминаются в N[l] }
N_tot : QWord;
s,	s_, к, k_, p : integer;
BEGIN
write(’Enter p, к > ’); readin(p, k);
for S_ := 0 to p-1 do N[0][s_] ;= 1;
for k_ := 2 to к do begin
for s := 0 to k_*(p-l) do begin
{ Применение формулы (10.7), Данные предыдущего шага, т.е. к'-1, берутся из N[0], а результаты для значения к_, т.е. к', сохраняются в N[l] } N[l] [s] ;= 0;
for s_ := 0 to p-1 do
if (s-s_ >= 0) and (s-s_ <= (k_-l)*(p-1))
then N[l] [s] : = N [1] [s]+N [0] [s-s_] ;
end;
N[0] := N[l]; { подготовка к следующему шагу }
end;
N_tot := 0;
for s := 0 to k*(p-l) do
N_tot := N_tot+sqr(N[0][s]);
writein(N_tot);
END.
3 Указанное количество N(17,10,1) аналогично, например, количеству сочетаний без повторений по пять элементов из трех возможных, которое, естественно, равно 0 (вспомните договоренность в подразделе 10.1.5).
292
ГЛАВА 10
♦ Программу можно несколько ускорить, чередуя роли массивов n[ о] ии[1] при четных и нечетных значениях к_. Можно также создать массивы с помощью GetMem(N[il, (к* (р-1) +1) * eizeof (QWord)) при i = 0 и i » 1.
Анализ решения. Оценим общее количество действий по приведенной программе. Для каждого к' от 2 до к находятся =р£' чисел, т.е. всего О(рк2) чисел. Каждое из них является суммой р чисел, поэтому общее количество действий имеет полиномиальную оценку О(р2&2),
Весьма поучительны результаты экспериментальных сравнений скорости работы всех упомянутых методов. В 2000 году получилось, что разница между перебором номеров и пе ребором их'половин заметна, а между перебором половин и программой в листинге 10.1 — не очень (для всех входных данных, при которых результат помещается в максимальным для Turbo Pascal тип longint, обе программы работали на машине с частотой СР 60 МГц практически мгновенно). В 2006 году использовались Free Pascal, тип QWord машина с частотой CPU 2 ГГц. В этих условиях было нетрудно найти входные данные, пре которых программа из листинга 10.1 выполняется практически мгновенно, а перебор п ловин — в течение несколько минут. Так что развитие технических средств только под черкнуло преимущество более эффективного алгоритма.
Отметим: если реализовать формулу (10.7) рекурсивной функцией, то это будет ярким примером на тему “как не стоит обращаться с рекурсией” (см. главу 3).
Отметим также, что основные идеи эффективного алгоритма, реализованного в листинге 10.1, изложены в математическом пособии [8].
10.2.4.	Белые и черные кубики
Задача 10.4. Бросают кх белых и кг черных кубиков. На каждом выпадает число от 1 до 6. Считают суммы чисел, выпавших на белых и на черных кубиках, и полученные суммы перемножают. Нужно найти количество разных способов получить в результате произведение А.
Кубики считаются различимыми, т.е. получить на первом белом кубике 2 и на втором белом кубике 5 — не то же, что 5 на первом и 2 на втором.
Вход. Числа Л,, к2 и А вводятся в одной строке в указанном порядке через пробел.
Выход. Одно число — количество способов.
Примеры. Вход: 2 2 8; выход: 6. Вход: 2 3 29; выход: 0.
Анализ задачи. Вначале рассмотрим еще один конкретный пример: кх=к2=2 А = 36. Произведение 36 можно получить как 3x12, 4x9, 6x6, 9x4, 12x3. Остальные варианты разложения 36 на два множителя (например, 2x18) не учитываем, поскольку на двух кубиках нельзя набрать сумму больше 12. »
• Каждое разложение на два множителя порождает свою группу вариантов, причем группы не пересекаются и полностью покрывают все возможные варианты, поэтому применимо правило суммы.
Рассмотрим подробнее какое-нибудь разложение на два множителя, например 4x9. Каждый из трех способов набрать сумму 4 на белых кубиках (1+3, 2+2,3+1 можно комбинировать с каждым из четырех способов получить сумму 9 на черных
КОМБИНАТОРИКА
293
кубиках (3+6, 4+5, 5+4, 6+3). Поэтому применимо правило произведения: количество способов получить произведение 36, набрав сумму 4 на двух белых кубиках и сумму 9 на двух черных, равно 3x4=12. Аналогично количество способов получить 36 как 3x12 равно 2x1=2, как 6x6— 5x5=25.
♦ Если = кг, то количества способов для 9x4 и 12x3 совпадают с количествами для 4x9 и 3x12, но при *1 *кг таких равенств уже не будет. Для ручных подсчетов это наблюдение сокращает работу, но при написании программы оно будет больше запутывать, чем помогать, так что этой оптимизацией пренебрежем.
Количества способов набрать на kt кубиках одного цвета сумму s., разумеется, тоже следует получать комбинаторно. Однако эта часть задачи уже решена — см. формулу (10.7) в задаче 10.3 о счастливых билетах.
Итак, для каждого разложения А на два множителя atxa2 берем значения N(a2, 6, к) и N(a2,6,k2) из таблицы, заранее посчитанной по формуле (10.7), умножаем их и добавляем произведение к общему количеству. Осталось выяснить, как лучше искать разложения а1ха2.
Используем простую идею: перебрать все возможные значения яр проверяя, действительно ли А кратно ар если кратно, то выполнить указанные выше действия.
Сузим диапазон перебора аГ Во-первых, не нужны ai меньше к2 и больше 6kt. Во-вторых, если произведение к2 и максимально возможной суммы черных кубиков 6fc2 меньше А, то можно начать со значения а1 **А/6к2, а точнее — округленного вверх частного ГА/б^Д Аналогично, если 6ktk2>A, то можно остановиться на (округленном вниз) частном La/£2J. В-третьих, очевидно, что при обмене местами значений к2 и к2 ответ не изменяется, а область перебора описанного цикла меняется: она уже там, где меньше кубиков. Поэтому вначале, если kt >к2, поменяем к2 и к2 местами.
Приведенные соображения реализованы в листинге 10.2, большую часть которого занимает построение таблиц N(aif 6,к{) и N(a2,6,к2) по формуле (10.7).
Листинг 10.2. Решение задачи о черных и белых кубиках
const МАХ_К = 250;
type Tval = extended;
var n_l, n_2, f таблицы количеств вариантов для белых }
{ и для черных кубиков, индексированные суммами } n_prev, n_this :	{ вспомогательные таблицы }
array[-5..6*МАХ_К] of TVal;
k_l, k_2, { исходные количества кубиков }
i, 1» j_ : longint;
Prod, { исходное произведение }
min_a_l, max_a_l, a_l, a_2 : longint;
result : TVal;
function max(x, у : longint) : longint;
begin
if x > у then max := x else max := y; end;
function min(x, у : longint) : longint;
294
ГЛАВА 1
begin
if х < у then min := x else min := y;
end;
BEGIN
readln(k_l, k_2, Prod);
if k_l > k_2 then begin
i := k_2; k_2 := k_l; k_l := i;
end;
{ первичная таблица, соответствующая одному кубику }
for j := -5 to 0 do
n_this[j] := 0;
for j := 1 to 6 do
n_this[j] := 1;
for j := 7 to 6*k_2 do
n_this[j] := 0;
if k_l = 1 then n_l ;= n_this;
{ заполнение таблиц N(..., 6, kl) и N(..., 6, k2)
по формуле (10.7) }
for i := 2 to k_2 do begin
n_prev := n_this;
for j := 1 to 6*i do begin
n_this[j] := 0;
for j_ := 1 to 6 do n_this[j] := n_this[j] + n_prev[j-j_] ;
end;
if i = k_l then n_l := n_this;
end;
n_2 := n_this;
{	границы для подходящих делителей числа Prod } min_a_l := max(k_l, (Prod+6*k_2-l) div (6*k_2));
{	ceil(Prod/(6*k_2)) }
max_a_l := min(6*k_l, Prod div k_2); { floor(Prod/k_2) } result := 0;
{	перебор возможных делителей числа Prod }
for а_1 := min_a_l to max_a_l do
if Prod mod a_l = 0 then begin
a_2 := Prod div a_l;
result := result + n_l[a_l]*n_2[a_2];
end;
writein(result);
END.
« Если Jti « кг и 6^1 кг £ A £ ЗбЛ^г. использованные приемы сужают область перебора для at незначительно, зато существенной будет следующая оптимизация. Разница между достаточно большими (больше Va ) последовательными делителями числа А довольно велика, поэтому целесообразно перебирать значения at только до Г -Ja 1, а дальше перебирать значения аг до L Va J, определяя at как А/аг-
♦ См. также замечания к листингу 1Q. 1.
КОМБИНАТОРИКА
295
10.2.5. Велосипедные гонки
Задача 10.5. На праздновании Дня города проводятся уличные гонки велосипедистов по следующим правилам4.
1.	Участвуют М гонщиков <3< Л/< 100).
2.	Нужно проехатьNкругов (2<N< 100, Nчетное).
3.	Гонщик, первым закончивший круг с нечетным номером, получает одно очко.
4.	Четыре гонщика, первыми закончившие круг с четным номером (кроме последнего), получают очки (первый— 5, второй— 3, третий— 2, четвертый *-1).
5.	На последнем круге все очки удваиваются относительно предыдущего пункта (10,6,4 и 2).
Определить количество различных способов, которыми можно набрать сумму очков К. Способы считаются различными, если у них отличается последовательность очков по кругам.
Вход. В первой строке три целых числа — К, N и М через пробел. Выход. Одно число — количество способов.
Примеры. Вход'. 10 4 7; выход'. 6. Вход'. 8 2 5; выход'. 0.
Анализ задачи. Поставим серию подзадач следующие образом: сколькими способами можно набрать сумму очков s, проехав круги с первого по n-й? Обозначим такую задачу как (п, я)-подзадачу, а ее ответ как Т(п, s).
При и = 1 подзадачи тривиальны: Т(1,0) = 1, Т(1,1) = 1, 71(1,0=0 при i>2, поскольку за один (нечетный) круг можно набрать только 0 или 1 очко. Для дальнейших кругов выразим результаты через результаты предыдущих кругов.
Следующим идет второй круг (четный). Однако вначале рассмотрим более простую задачу — как получить ответ для нечетного круга, имея результаты предыдущего четного.
На нечетном круге можно набрать только 0 или 1 очко. Поэтому, чтобы набрать общую сумму очков s, можно или набрать 5-1 очко на предыдущих кругах и одно очко на данном, или набрать s на предыдущих. Эти ситуации покрывают все случаи и взаимно исключаются. Следовательно,
Т(п, s) = Т(п -l,s-l) + Т(п -1,5) при нечетном п.
Из этой формулы есть исключение: при 5=0 оказывается 5-1=-1, т.е. (п-1,5-1)-подзадача не имеет смысла. Трактуем это так, что в сумме для Т(п, s) второе слагаемое равно 0.
Действуем аналогично для четных кругов. Здесь можно приехать первым (5 очков), вторым (3 очка), третьим (2 очка), четвертым (1 очко) или “не менее чем пятым” (0 очков).
Т(п, s) = Т(п-1, 5) + Т\п-1, 5-1) + T(n-i, 5-2) + Т(п-1, 5—3) + T(n-l, s-5) при четном п.
4 Этот способ подсчёта очков действительно используется в велогонках типа “критериум”. Впрочем, вряд ли данная задача имеет значение для реальных гонщиков...
296
ГЛАВА 1
Применяя эту формулу, также нужно учитывать только слагаемые, у которых сумма очков неотрицательна. Кроме того, если общее количество участников гонки М равн 4, то приехать “не менее чем пятым” нельзя, а при М=3 нельзя приехать и четвертым. Итак, при таких М исчезают соответствующие слагаемые T(n-l,s) и
В последней N-й строке считаем только T(N, К) по аналогичной формуле (с учетом всех замечаний к предыдущей формуле):
ДМ К) = T(N-1, К) + ДМ-1, К-2) + ДМ-1, К-4) + ДМ-1, К-6) + ДМ-1, К-10).
Во всех приведенных формулах используются только результаты предыдущей строки (номер круга — номер строки, количество набранных очков — номер столбца). Поэтому держать в памяти всю таблицу размером NxK не нужно; достаточно 2хК чисел.
Поскольку формулы для четных и для нечетных кругов разные, удобнее хранить в 0-й строке результаты для очередного четного круга, а в 1-й — нечетного.
В условии не указано явное ограничение на количество очков К. Однако за Л кругов невозможно набрать больше ЗМ+5 очков. Поэтому, если K>3N+5, сразу сообщаем результат 0, иначе проводим вычисления.
Нетрудно убедиться, что при максимальных ограничениях придется использовать “длинную арифметику”. Впрочем, здесь нужны лишь следующие довольно простые операции: присвоить длинному целому малое целое значение, присвоить (копировать длинное целое, сложить два длинных целых (записав сумму на место одного из слагаемых), вывести длинное целое значение.
10.3.	Рекурсия в задаче о русском лото
Задача 10.6. Для игры “Русское лото” используются карточки, удовлетворяющие следующим условиям:	»
•	карточка состоит из трех строк и девяти столбцов;
•	пять клеток в каждой строке заняты целыми числами (остальные свободны);
•	в первом столбце могут находиться только числа из диапазона 1. . 9, во втором — из диапазона 10. .19, в третьем — 20. .29 ит.д., в восьмом— 70.. 79, вдевятом— 80. .90;
•	ни одно число на карточке не повторяется.
Найти количество различных карточек.
Пример карточки
8		25	30		57			81
3		26		43			79	85
	15	29	35		50	68		
« В данной задаче нет входных данных, и ниже это будет использовано. Тем не менее, зададим размеры именованными константами: n_cols - 9 — количество столбцов, N_Rows - з — количество строк, n_used «5 — количество клеток в строке, занятых числами. Это целесообразно хотя бы потому, что при решении данной задачи приходится проверять правильность написанного кода, используя “уменьшенное лото”, например, размерами n_cols = з, N_rows  2, n_used » 2. Настоятельно рекомендуем читателям в подобных ситуациях поступать так же!
КОМБИНАТОРИКА
297
Анализ и решение задачи. Первое число в первой строке может находиться в первой, второй и т.д. или пятой клетке — соответственно этому все карточки можно разбить на пять групп. Для этого разбиения справедливо правило суммы.
Внутри каждой из этих групп можно выделить подгруппы в соответствии с тем, где находится второ^ число первой строки. Затем ситуации дробятся в соответствии с тем, где находятся следующие числа первой строки, затем — с тем, где находятся числа второй строки, и т. д., пока не дойдем до тривиальной подзадачи — позиции всех чисел во всех строках уже выбраны.
Описанный перебор вариантов можно реализовать рекурсивной функцией. Сформулируем более четко, что нам нужно в качестве результата этой функции. Удобно, чтобы это было количество способов дальнейшего размещения чисел, если зафиксированы некоторые начальные размещения (где “начальные размещения” выбраны внешними в данный момент рекурсивными вызовами).
Подчеркнем, что внешние вызовы должны фиксировать, какие клетки были заняты числами, но не фиксировать сами числа. Уместно вспомнить замечание к доказательству формулы для Рп (см. подраздел 10.1.2): “Выбор элементов для начальных мест не влияет на количество элементов, остающихся для заполнения последующих мест...”. Так и здесь: если во второй клетке первой строки размещается число, то для него возможны 10 вариантов, а если после этого число размещается во второй клетке второй строки, то независимо от первого числа для второго остается 9 вариантов.
В предыдущей фразе существенны слова “после этого”. Ведь если в первой строке вторая клетка пуста, и очередное число размещается во второй клетке второй строки, то для него возможны 10 вариантов. Поэтому в рекурсивный вызов целесообразно передавать в качестве аргумента массив из девяти элементов, соответствующих столбцам; смысл его значений — количество вариантов размещения чисел в столбце5. Согласно условию, начальное значение массива — (9, 10, 10, 10, 10, 10, 10, 10, 11); каждый раз, когда рекурсивный вызов размещает очередное число в столбце the_col, соответствующий элемент локальной копии массива уменьшается на 1 (dec (left_num [the_col]) в листинге 10.3).
Итак, выяснен полный список параметров рекурсии: the_row — номер строки; the_col — номер клетки в строке; num_in_l ine — номер числа в строке; lef t_num — массив количеств остающихся вариантов для чисел в столбцах.
Если вызов размещает последнее число в последней строке, т.е. при условии (the_row = N_ROWS) and (num_in_row = N_USED), то можно немедленно возвратить количество вариантов — один (если все числа уже размещены, то разместить дальнейшие можно только одним способом — ничего не делая).
Если размещаемое число не будет последним в строке, то нужно перебрать все возможные места i для размещения следующего числа: от the_col +1 (оно должно быть размещено после текущего) до N COLS - N_USED + num_in_row +1 (в строке должны поместиться все N_USED чисел).
Если размещаемое число последнее (но не в последней строке), то опять-таки нужно перебрать все возможные места для размещения следующего числа. Разница только в том, что эти места находятся в следующей, (the_row + 1)-й, строке.
5 Для экономии стека можно поддерживать один глобальный экземпляр, модифицируя его каждый раз после входа в рекурсию и вручную возвращая предыдущее значение перед выходом. Однако массив размером всего 9 байт можно передавать как параметр-значение.
298
ГЛАВА 1
Как уже указано выше, результаты, возвращаемые рекурсивными вызовами, нужно умножать на left_num[i] — количества чисел, оставшихся возможными в соответствующем столбце.
Листинг 10.3. Решение задачи о русском лото 
const N_COLS = 9; N_USED = 5; N_ROWS = 3;
type TLeftNum = array[1..N_COLS] of byte;
const init_left_num : TLeftNum = (9,10,10,10,10,10,10,10,11); var result : extended; i : byte;
function count_all(the_row, the_col, num_in_row : byte;
left_num : TLeftNum) : extended;
var result : extended; i : byte;
Begin
if (the_row = N_ROWS) and (num_in_row = N_USED)
then begin count_all := 1; exit; end;
dec(left_num[the_col]);
result := 0;
if (num_in_row = N_USED) then begin
for i := 1 to N_COLS - N_USED + 1 do result := result +
count_all(the_row +1, i, 1, left_num)*left_num[i];
end
else begin
for i := the_col+l to N_COLS - N_USED + num_in_row + 1 do result := result + count_all(the_row, i, num_in_row+1, left_num)*1e ft_num[ i ] ;
end;
count_all := result;
End;
BEGIN
result := 0;
for i := 1 to N_COLS - NJJSED + 1 do
result = result +
count_all(l, i, 1, init_left_num)*init_left_num[i];
writein(result:25:0);
END.
♦ Ответ программы— 817687046204481600000. Несмотря на то, что использован тип extended, этот ответ точен (для его проверки пришлось переписать программу, применив “длинную арифметику”),
►> Карточки, у которых одинаковые наборы строк размещены в различных порядках, можно считать одинаковыми. Как вычислить количество различных карточек при этом изменении условия? Указание. Достаточно, не меняя программу, разделить ее результат на N_ROWS\=3!=6. Поскольку числа не повторяются, одинаковых строк в карточке не может быть. Поэтому корректность указанного действия обосновывается аналогично формуле для С*.
Н Предположим, что карточки считаются одинаковыми, если у них заняты одни и те же клетки (неважно, какими числами). Выведите и обоснуйте формулу количества различных
КОМБИНАТОРИКА
299
карточек. Ответ.	• Д™ каждой из N_ROWS строк есть C^ cols способов
заполнить ее клетки независимо от других строк.
» Совместим предыдущие вопросы. Строки карточек не различаются, если у них заняты одни и те же клетки (неважно, какими числами). Карточки, у которых один и тот же набор строк записан в различных порядках, считаются одинаковыми. Найти формулу для количества различных карточек. Указание. Пытаться в этой ситуации применить правило частного и давать ответ
(f N _ VSED Ч N _ ROWS
'''-'N_COLS'	, _	-	.
----------------грубая ошибка!
N_ROWS\
Например, если в наборе карточек две строки одинаковы, а третья отличается, то карточек, различных в смысле предыдущего вопроса, будет только 3, а не 6. Приведем правильное решение. Есть r= Сcols способов заполнить строку. Из N_ROWS строк х, заполняется первым способом, хг — вторым,..., х(-р t-м способом, х1 + х2+ ... + xt=N_ROWS. Отсюда ответ —	(см. подраздел 10.1.3, сочетания с повторениями).
10.4.	Включения и исключения
10.4.1.	Принцип включений и исключений
Пусть есть три круга А, В, С, которые могут пересекаться (рис. 10.2). Все, что попало в эти круги, закрашивается (плотность закрашивания одинакова и для просто кругов, и для их пересечений). Тогда площадь закрашенной части плоскости можно вычислить так:
сначала найдем сумму площадей кругов (S := 5(A) + 5(B) + 5(C));
пересечения кругов были подсчитаны дважды, поэтому их нужно вычесть (5 := 5 - S(AnB) - 5(AnC) - 5(BnQ);
пересечение всех трех кругов сначала трижды прибавили, а затем трижды вычли, т.е. не учли ни разу, поэтому его нужно прибавить (5 := 5 + S(ArBr\Cy).
Приведенные рассуждения представляют собой частный случай так называемого принципа включений и исключений. В более общей формулировке он имеет следующий вид.
Если есть п множеств А,, А2, ..., Ая, то мера (количество элементов, площадь и т.п.) объединения этих множеств A1<jA2\j...\jAii равна сумме мер всех отдельно взятых множеств, минус меры всех возможных попарных пересечений, плюс меры всех возможных пересечений группами по три, минус меры всех возможных пересечений группами по четыре и т.д., до меры пересечения всех п множеств включительно.
300
ГЛАВА К|
Рис. 10.2. Пример к формуле включений и исключений
Сказанное словами выражается следующей формулой:
|Ai иА2и ... оА„| = £|А,| - £ k nAj + ...+ (-1)"+1|А1 пА2П ... п
(=1	K/cjSr»1	’
An|.
Как видим, знаки чередуются (+,	+,	...), поэтому мера пересечения всех п мно-
жеств прибавляется, если п нечетно, и вычитается, если четно.
Вычислить меру объединения множеств А,, А2,..., Ая можно следующим способом.
• Найдем меру части Ai, которая не включается ни в одно из множеств А2,.... Ап-Затем найдем меру части А2, которая не включается ни в одно из множеств Аз, ..., Ап и т.д. Последней будет мера А№ Сложив полученные меры, получим меру объединения.
Описанные вычисления довольно просто программируются с помощью рекурсивной функции. Пусть TMySet — тип, представляющий множество (неважно, как именно), для которого реализована подпрограмма вычисления пересечения; количество множеств представлено в глобальной переменной N : integer, сами множества — в глобальном массиве А : array [1. . MAXN] of TMySet.
Построим функцию с таким заголовком:
function Count(X ; TMySet; i : integer) : extended;
Она вычисляет меру части множества х, не пересекающейся с множествами, номера которых строго больше i.
Многократные вызовы этой функции
res := 0;
for i : = 1 to N do
res := res + Count(A[i], i);
приведут к вычислению меры А! иА2и... иАя. Это утверждение проиллюстрировано на рис. 10.3. Count (А [1] , 1) вычислит меру всего того, что принадлежит только множеству А, и ни одному другому множеству (область 1 на рис. 10.3, б); Count (А[2] ,2) — меру того, что принадлежит А2 и не принадлежит А3 или А4 (область 2) и т.д.
Функция Count может иметь следующий схематический вид.
КОМБИНАТОРИКА
301
function Count(X : TMySet; i : integer) : extended; var j : integer;
гёа : extended;
Begin
if X = 0 then begin { если множество пусто, то }
Count := 0; eJtit { пустыми будут и его пересечения } end;
res ;= |х|;	{ вычисляем меру множества }
for j := i+1 to N do
{ вычитаем пересечения с “дальнейшими” множествами }
res := res - Count (X n A [j] , j);
Count := res;
End;
a)	6)
Puc. 10.3. Мера объединения множеств
Отметим: приведенная функция обеспечивает чередование знаков слагаемых (см. принцип включений и исключений) тем, что значения, возвращаемые рекурсивными вызовами, вычитаются.
10.4.2.	“Батарея, огонь!”
Задача 10.7. Космическая станция состоит из одинаковых кубических модулей, образующих большой прямоугольный параллелепипед размерами XxFxZ. Ее испытывают на живучесть, имитируя удары метеоритов выстрелами из пушки по трем направлениям. Снаряды попадают в центр грани какого-либо модуля перпендикулярно его поверхности и прошивают станцию насквозь, не меняя направления. Сколько модулей остались неповрежденными после Р выстрелов?
Вход. В первой строке текста — числа X, Y, Z и Р (1S X,Y,Z £ 1000, 0< Р£ 150). Далее Р строк по три координаты х, у, z в каждой описывают выстрелы. Нулевое значение координаты определяет ось, параллельно которой произведен выстрел, а две другие — соответствующие координаты простреленных модулей. Например, при Х=3, У=4, Z=5 тройка (1,0,3) означает, что пробиты модули (1, 1,3), (1,2,3), (1,3,3) и (1,4,3). Известно, что “снаряд дважды в одну воронку не падает”, т.е. ни одна тройка не повторяется. Числа в строках отделены пробелами.
Выход. Одно число — количество неповрежденных модулей.
Пример
Вход 3 4 5 2 Выход 52
2 2 0
2 0 1
302
ГЛАВА К
Анализ задачи. Самое первое (неправильное) приближение к решению — считать) что выстрелы вдоль оси Ох выбивают X модулей, выстрелы вдоль Оу — Y модулей^ вдоль Oz — Z модулей. Однако здесь не учтено, что непервый выстрел выбивает меньше модулей, если какие-то из них на пути этого выстрела уже выбиты. Это наблюдение подталкивает к тому, чтобы использовать принцип включений и исключений.
В первом приближении считаем, что каждый выстрел выбивает соответственна своему направлению X, Y или Z модулей. Затем перебираем возможные пары вы-^ стрелов: если их траектории пересекаются, уменьшаем количество выбитых модулей на 1. Если же пересекаются траектории трех выстрелов, то количество выбитых мен дулей нужно увеличить на 1.
Ограничимся тройками выстрелов, не рассматривая группы по 4, 5 и т.д., поскольку по условию задачи все выстрелы разные и делаются параллельно координатным осям. Таким образом, более чем три траектории не могут пересечься в одной точке. По этой же причине любое пересечение траекторий может только либо быть пустым, либо состоять из одного модуля.
* Для удобства перебора таких пар и троек целесообразно при чтении входных данных сразу распределить выстрелы по трем последовательностям — параллельные осям Ох, Оу и Oz.
Сложность приведенного алгоритма равна О(Р*), поскольку приходится перебирать тройки выстрелов, но в среднем она может быть ближе к Р2, чем к Р3. Дело в том, что поиск пересечений трех выстрелов (ix-ro вдоль Ox, iy-ro вдоль Оу и i‘z-ro вдоль Oz логично организовать тремя вложенными циклами (например, внешний по ix, средний по i, внутренний по Q, и, если для некоторых ix, iy соответствующая пара выстрелов не пересекается, то для них не нужно запускать цикл по iz.
При заданных в условии ограничениях (X, Y, Z< 1000, Р< 150) трудно нацти что-нибудь лучшее, чем описанное применение принципа включений и исключений. Однако рассмотрим эту же задачу, но с меньшими диапазонами для X, Y, Z (например, “до 200”) и без явного ограничения Р. (Поскольку никакие два выстрела не проходят по одной траектории, Р ограничено неявно величиной XY+XZ+YZ.)
В этой ситуации целесообразнее действовать иначе. Будем отслеживать проекции на плоскости Оху, Oxz и Oyz. На каждой проекции отмечаем выстрелы, пробивающие параллелепипед насквозь перпендикулярно этой плоскости (например, проекция Oyz соответствует выстрелам вдоль оси Ох). Рассматривая (читая) каждый новый выстрел, отмечаем его на перпендикулярной к нему проекции, и, анализируя две другие проекции, выясняем, какие модули выбиваются данным выстрелом, а какие уже были выбиты.
Оценка количества действий по этому способу — O(D2+PD), где £>= max{X, K,Z}. Основной недостаток — нужно много памяти, O(XY+ XZ+ YZ) = O(D2).
* Предположим, что в условии задачи нет гарантии, что “снаряд дважды в одну воронку не падает*. Тогда при использовании принципа включений и исключений проще всего свести задачу к версии, где такая гарантия есть, предварительно исключив повторные выстрелы. В задаче 5.4 описано, как сделать это за время О(Р log Р). Если же решать задачу через проекции, то проверять повторения выстрелов лучше в самом алгоритме, перед модификацией проекции.
►> Реализуйте оба представленных способа решения.
КОМБИНАТОРИКА
303
10.4.3.	Беспорядок в шляпах
Задача 10.8. Джентльмены пришли в клуб и сдали в гардероб свои шляпы.
Уходя, джентльмены перепутали шляпы так, что каждый ушел в чужой шляпе. Сколько вариантов такого события может быть, если клуб посещают п джентльменов?
Вход. Целое положительное число п.
Выход. Целое неотрицательное количество вариантов.
Примеры. Вход'. 1; выход'. 0. Вход: 2; выход: 1. Вход: 5; выход: 44.
Анализ задачи. Вместо распределений п шляп среди п джентльменов рассмотрим перестановки чисел от 1 до и. Перестановка р, у которой p(i) * i при каждом i от 1 до п, называется инверсией, или беспорядком. Итак, требуется найти количество инверсий чисел от 1 до в.
Обозначим через А, множество всех перестановокр, у которых p(i) ~ i (а остальные числа могут быть как на своих местах, так и на чужих). Тогда не-инверсии, т.е. перестановки, в которых хотя бы одно число находится на своем месте, образуют множество Atu А2и ... иАл. Следовательно, по принципу включений и исключений, количество не-инверсий равно сумме мер множеств А(, минус меры всех множеств вида А'Г\ Aj (перестановки, в которых два числа i и j находятся на своих местах) и т.д.:
|AiuA2u...uA„|= £|А,.| - £ |a,haJ + ... + (-1)"+1|Ai пА2 n ... n
A„|.
Найдем, чему равно количество перестановок, в которых i и j находятся на своих местах. При конкретных i и j можно как угодно переставлять остальные п-2 числа по оставшимся и-2 местам, так что количество таких перестановок равно (п-2)!. Формула включений и исключений требует вычесть меры всех попарных пересечений, поэтому (п-2)! нужно умножить на количество способов выбрать такие i и j, т.е. на С„. Получаем
<^(«-2)!=д^-(п-2)! = -£.
Проведя аналогичные рассуждения для перестановок, у которых на своих местах стоят не 2, а к чисел, получим формулу для их количества . Подставив полученные выражения в формулу включений и исключений, получим
|AiUA2u...uA„|= £(-1)‘4^.
t=i kl
Количество инверсий равно разности между количествами всех перестановок и не-инверсий, т.е.
t=i kl 1!	1!	*=2 kl	kl
» Запрограммируйте вычисления по приведенной формуле.
304
ГЛАВА 10
10.5.	Количество раскладок и разбиений
10.5.1.	Разбиения множества
Задача 10.9. Вовочка раскладывает п попарно различных карандашей по к коробочкам, не оставляя пустых коробочек (к<п). Вычислить количество способов раскладки при условии, что коробочки неотличимы.
Вход. Целые положительные количества карандашей и коробочек.
Выход. Количество раскладок.
Примеры. Вход1, з 2; выход'. 3. Вход'. 4 3; выход: б.
Анализ задачи. Пронумеруем карандаши числами от 1 до п. Для первого примера раскладками являются (1)(2,3), (2)(1,3), (3)(1,2), для второго — (1)(2)(3,4), (1)(3)(2,4 (2)(3)(1,4), (1)(4)(2,3), (2)(4)(1,3), (3)(4)(1,2). Вместо раскладок п карандашей по к коробочкам рассмотрим разбиения множества {1, 2.п} на к непересекающихся
подмножеств (блоков). Обозначим их количество через S(n, к). Очевидно, что
S(n, п) = 1, S(n, 1) = 1, S(n, 0) = 0 при п > 0.
Построим для чисел S(n, к) рекуррентную формулу. Из всех разбиений на к блоков выделим те, которые содержат одноэлементный блок {л}. Очевидно, их количество равно количеству разбиений множества {1,2, ...,л—1} Hafc-1 блок, т.е. 5(л-1,к-1). В остальных разбиениях число п находится в блоке с некоторыми другими числами, и его можно удалить, получив разбиение {1, 2, ...,л-1} на к блоков. Каждое разбиение {1, 2...л-1} на к блоков можно получить из к различных разбиений {1, 2,
..., л}, удаляя число л из блоков 1, 2,..., к. Таким образом, число оставшихся разбиений равно Ъ<5(л-1, к), поэтому	1
5(л, к) = S(n-l,k-l) + kxS(n-l, к) при 1 < к < п.	(10.8)
Числа S(n, к) называются числами Стирлинга второго рода. Очевидно, что сумма 5(и, 1) + S(n, 2)+ ... +S(n, п) выражает количество всевозможных разбиений множества, состоящего из л различных элементов. Это количество называется числом Белла Вл;
В„ =	НРИ л > 0-
Количество разбиений пустого множества считается равным 1, т.е. Во= 1. По тем же причинам 5(0,0) = 1.
Для чисел Белла несложно вывести другую рекуррентную зависимость. Все разбиения множества {1, 2, ...,л} можно разделить на непересекающиеся труппы в зависимости от блока, содержащего число л. Если этот непустой блок зафиксирован, то оставшиеся к чисел (0£ £<и-1) можно разбить Вк способами. Выбор блока соответствует выбору оставшихся чисел, поэтому при каждом к есть способов выбрать блок размером п-к. Таким образом, получаем формулу
Вп=	.
» Реализуйте вычисления по представленной формуле.
КОМБИНАТОРИКА
305
10.5.2.	Разбиения множества с учетом порядка классов
Задача 10.10. Условие отличается от задачи 10.9 только тем, что коробочки попарно различны.
Примеры. Вход'. 3 2; выход'. 6. Вход'. 4 3; выход'. 36.
4
Анализ задачи. Для первого примера искомыми раскладками являются (1)(2,3), (2,3)(1), (2)(1,3), (1,3)(2), (3)(1,2), (1,2)(3). Коробочки попарно различны, поэтому раскладку по ним можно рассматривать как перестановку к блоков, на которые разбиты числа от 1 до л. Тогда количество раскладок равно klxS(n, к). Обозначим эту величину через Ди, к) и рассмотрим еще два способа ее подсчета.
Построим для fin, к) рекуррентную формулу. Все разбиения на к упорядоченных блоков можно разделить на те, которые содержат одноэлементный блок {л}, и те, которых число п находится в блоке с некоторыми другими числами. Количество первых, очевидно, равноkxfin-l,k-l), поскольку блок {л} может иметь любой из номеров от 1 до к. Аналогично количество вторых равно ЪДл-1, к), поэтому
Ди, к) = kx(fin-l, fc-1) +fin-1, к)) при 1 < к < л.
Это же соотношение легко получить из (10.8) и равенства fin, к)= k'.xS(n,k). Очевидно также, чтоДл, 1) = 1,Дл,0)=0 при л >0,ДО, 0) = 1.
Еще одна формула для fin, к) основана на принципе включений и исключений. Присвоим коробочкам номера от 1 до к. Раскладки представляют собой размещения с повторениями из к по л, поэтому их общее количество (возможно, с пустыми коробочками) равно Г. Обозначим через A, (i= 1, ...,fc) множество раскладок, у которых пуста i-я коробочка (и, возможно, некоторые другие). Тогда
Л ид “ искомая м
величина.
Для произвольного т от 1 до к-1 множество A(jn А^г\ ...пА^, где 1	i2<... <
im<k, содержит все возможные раскладки, у которых пусты коробочки i2...im (и,
возможно, некоторые другие). Эти раскладки можно рассматривать как размещения с повторениями из к-т пол, поэтому их количество равно (к-т/. Выбрать т-элементное подмножество пустых коробочек можно С” способами, поэтому
fin,k) = k!'- С'(Л-1)"+ С^(к-2)п-... +(-l)*“1Ct‘-1(H^-l))',=
Н Реализуйте описанные вычисления.
« Раскладки карандашей по коробочкам с точки зрения математики являются функциями (карандашу ставится в соответствие коробочка). Эти функции определены на всем множестве карандашей, поэтому представляют собой отображения. Пустых коробочек нет, т.е. карандаши отображаются на множество коробочек. Такие отображения называются сюръективными (сюръекциями). Таким образом, fin, к) выражает количество сюръекций, отображающих n-элементное множество на ^-элементное.
А/ — это количество раскладок с пустыми коробочками, а Л"-
306
ГЛАВА 10
10.5.3.	Разбиения числа на слагаемые
Задача 10.11. Вовочка раскладывает т одинаковых винтиков по к неотличимым коробочкам (к<т). Он может поместить в одну коробочку все винтики, а может положить только часть, распределив остальные так, что в каждой следующей коробочке не больше винтиков, чем в предыдущей. Вычислить количество способов раскладки.
Вход. Целые положительные количества винтиков и коробочек.
Выход. Количество раскладок.
Примеры. Вход'. 3 2; выход'. 2. Вход'. 5 3; выход'. 5.
Анализ и решение задачи. Вместо раскладки т винтиков по указанным в условии правилам будем говорить о разбиении числа т не более чем на к слагаемых, последовательность которых не убывает. Из примеров несложно убедиться, что возможными разбиениями числа 3 являются 3 и 2+1, числа 5 — 5,4+1, 3+2, 3+1+1 и 2+2+1.
Все допустимые разбиения очевидным образом разделяются на группы по числт слагаемых (для числа 5 и не более чем трех слагаемых это группы {5}, {4+1, 3+2} и {3+1+1, 2+2+1}), поэтому можно применить правило суммы. Обозначим количество всех разбиений произвольного числа и не более чем на к слагаемых через R(n,k а количество всех разбиений числа п в точности на к слагаемых — через Р(п, к). Тогда
R(m, к) = Р(т, 1) + Р(т, 2) + ... + Р(т, к).	(10.9
Займемся числами Р(п,к). Очевидно, что P(n,n) = P(n,n-1) = Р(и, 1) = 1 при любом и>0. Пусть л>3 и 1 < к<л-1. Все разбиения с к слагаемыми можно разбить на две группы — с последним слагаемым 1 и с последним слагаемым больше 1. Любое разбиение первой группы взаимно однозначно получается из разбиения числа zj-1 на Л-1 слагаемых, если дописать к нему к-е слагаемое 1. Любое разбиение второй группы взаимно однозначно получается из разбиения числа п-к на к слагаемых, если прибавить к каждому слагаемому по 1. Например, разбиение 4+1 получается из разбиения 4, а 3+2 — из 2+1. Итак, по правилу суммы получим следующую формулу:
Р(п,к)=Р(п-1,к-1) + Р(п-к,к).	(10.10
Она и будет основной при решении задачи. Перед ее применением добавим лишь, что Р(п, к)=0 при л < к.
По формуле (10.10) нетрудно построить таблицу значений P[n,s] с помощью перебора значений п от 1 до m и s от 1 до Л, а затем по формуле (10.9) вычислить R(m, к) Сложность такого алгоритма очевидна— О(тк). Разумеется, часть этой таблицы (при n<s) можно не заполнять, используя условие n<s как признак того, что Р[т, s]=0.
В формуле (10.10) можно выразить Р(л-1,к-1) как Р(п-2,к-2)+ Р(п-к, Л-1), затем аналогично выразить Р( л-2, Л-2) и т.д. В итоге получим формулу Р(п,к) = ’E^Pln-k, г) Если нужно найти только Р(л, Л), т.е. количество разбиений числа в точности на Л слагаемых, то при Л> л/2 согласно этой формуле достаточно заполнить меньше половины таблицы.
й Реализуйте описанные вычисления.
КОМБИНАТОРИКА
307
Упражнения
10.1.	Доказать, что количество способов распределить и одинаковых предметов по т различным ячейкам так, что ни одна не остается пустой, равно С”^1.
10.2.	Лотерею проводят по следующим правилам:
на игровой карточке записаны числа (номера) от 1 до N (10<#£ 100);
игрок выбирает и зачеркивает любые К чисел данного набора (2 < К< N/2);
• во время розыгрыша тиража на лототроне выпадают произвольные М чисел данного набора (K<M<N/2)‘
выигрыш игрока устанавливается в зависимости от количества “угаданных” номеров; возможны выигрыши за любое количество угаданных номеров (как большое, так и малое).
Напишите программу, вычисляющую математическое ожидание выигрыша в такой лотерее. Предйолагается, что игрок равновероятно выбирает произвольный набор чисел, а на лототроне также равновероятно выпадает произвольный набор.
Вход. Количества N, К, М, затем К+1 величина выигрыша за угаданные К, К-1, ..., 1, 0 номеров (некоторые из них равны 0, остальные положительны). Выход. Одно число — математическое ожидание выигрыша.
Справка. Под событием в теории вероятностей понимают любой факт, который в результате опыта может произойти или не произойти. Результат опыта, влекущий за собой событие, называется благоприятным этому событию. Например, в лотерее результатом является любой набор из К номеров, а событию “угадано три номера” благоприятны результаты, содержащие любые три из выпавших М номеров.
Под вероятностью события понимают отношение количества благоприятных результатов к количеству всех возможных результатов. Математическое ожидание некоторой величины равно сумме произведений значений величины на вероятности появления этих значений. Пусть, например, возможны следующие значения и вероятности выигрыша: 10000 и вероятность 0,000001, 200 и 0,001, 5 и 0,04. Тогда математическое ожидание выигрыша будет равно
10000x0,000001 + 200x0,001 + 5x0,04 = 0,01 + 0,2 + 0,2 = 0,41.
10.3.	Друзья переговариваются по телефону. Каждый разговор длится одну минуту. Один из друзей хочет передать остальным новость как можно быстрее, но так, чтобы каждый сделал не больше двух звонков. По заданному количеству друзей п (не более чем210’) найти минимальное время, через которое новость будет известна всем.
10.4.	Подсчитать количество способов покрытия прямоугольника 2хи, где 0< п<65 536, прямоугольниками 2x1. Покрытия, получаемые одно из другого симметричными отображениями, считаются различными.
10.5.	У дорожных рабочих есть плитки для тротуаров 1x1 и 1x2. Сколькими разными способами они могут вымостить дорожку 2хп? Плитки 1x2 сделаны так, что они ложатся вдоль дорожки только широкой стороной. Учесть, что я < 103.
10.6.	Каждая из т точек на одной из двух параллельных прямых соединена отрезком с каждой из п точек на другой прямой так, что никакие три отрезка не пересекаются в одной точке. Найти число точек пересечения.
308
ГЛАВА 10
10.7.	Докажите тождество f	“ 1)(и ~ ~ 1) = Сп •
10.8.	Полоса состоит из п последовательных клеток. Шашка стоит на первой клетке полосы. За один ход шашку можно передвинуть вперед на любое число клеток в пределах от 1 до к. По заданным п и к вычислить количество последовательностей ходов, после которых шашка достигнет последней клетки. Технические ограничения: 2< п< 100,1 < к<п.
10.9.	Верно ли, что для решения задачи “Сколькими разными способами гонщик Иванов мог набрать Kt очков, а гонщик Петров— Кр очков?” (в условиях задачи 10.5) достаточно решить задачу 10.5 отдельно для Кр отдельно для Кр и результаты перемножить? Если это не так, то почему и как решать такую задачу?
10.10.	В задаче о русском лото добавить в рекурсивную подпрограмму дополнительные ограничения, чтобы исключить из подсчета карточки, у которых хотя бы в одном столбце заняты все три клетки.
10.11.	В задаче о русском лото добавить в рекурсивную подпрограмму дополнительные ограничения, чтобы исключить из подсчета карточки, у которых хотя бы •в одном столбце нет занятых клеток.
10.12.	Буквы латинского алфавита кодируются натуральными числами, начиная с 1, т.е А — 1, В — 2,..., Z—26. Слово кодируется заменой каждой его буквы на соответствующее число, например, строка “ABBA” кодируется как “1221”, a“RAZE” — как “181265”. Декодирование при этом неоднозначно, например, код “1221” можно получить по любой из строк “ABBA”, “ABU”, “AVA”, “LBA” или “LU”. Напишите программу, которая по входному коду (последовательности цифр длиной не более чем 100 с первой цифрой, не равной 0) определяет число способов его декодирования.
10.13.	В строке записаны все подряд натуральные числа от N до М включительно, N<M. Сколько раз в этой строке встречается каждая из цифр 0,1,2,3,4,5,6,7,8,9? Например, при N=2,M= 12 ответом является 1,4,2,1,1,1,1,1,1,1, т.е. четыре единицы, две двойки и по одной каждой из других цифр (включая 0).
10.14.	Число является произведением п попарно различных простых чисел. Сколькими способами его можно представить в виде произведения т сомножителей (т<л)?
10.15.	Все л! перестановок чисел от 1 до л записаны в лексикографическом (словарном) порядке. Найти перестановку номер k (1 <к<,п\).
10.16.	Подстановка s множества А = { 1, 2,..., л) — это взаимно однозначное отображение А на себя. Циклом подстановки s длиной I, где />1, называется подмножество {ар а2........а), для которого 5(0^ = а2, s(a2)=а}, ..., s(a)=л,. Постро-
ить рекуррентное соотношение для количества перестановок л элементов, имеющих к циклов.
10.17.	“Чистый” математик мог бы сказать, что меру каждого объединения необходимо считать на основе принципа включений и исключений (см. подраздел 10.4.1). Запрограммируйте его, по возможности эффективно, в задачах о мере объединения отрезков и треугольников (см. задачи 7.2 и 7.4). Какова будет сложность такого алгоритма в худшем случае? На каких входных данных он будет работать быстрее, чем в худшем случае?
Глава 11
Перебор вариантов
В этой главе...
•	Порождение подмножеств и последовательностей
•	Перебор вариантов как обход корневого дерева
•	Сокращение перебора — метод ветвей и границ
•	Пример псевдополиномиального алгоритма
В данной главе начинается знакомство с порождением комбинаторных объектов множеств и последовательностей), структура которых определена в каждой конкретной задаче. Количество объектов зависит от их размера, как правило, экспоненциально, поэтому алгоритмы перебора, т.е. порождения всех или всех допустимых в задаче объектов обычно имеют экспоненциальную сложность.
В первом разделе представлен перебор подмножеств данного множества, во втором — последовательностей заданной длины, образованных элементами множества. Основной вопрос в этих задачах — как от очередного объекта перейти к следующему, чтобы ни один допустимый вариант не был пропущен и не рассматривался несколько раз.
В третьем разделе рассмотрен поиск не всех объектов, а только тех, которые удовлетворяют условиям, определенным в задаче. В таких задачах возможно сокращение перебора. Один из путей сокращения — отсечение вариантов, т.е. отбрасывание целых подмножеств объектов, которые наверняка не подходят.
Еще один способ “борьбы с перебором” состоит в том, что варианты вообще не перебирают, а ищут решение в небольшом их подмножестве, которое содержит полиномиальное число легко порождаемых объектов. Полученное при этом решение, как правило, отличается от наилучшего, т.е. является некоторым приближением к нему. Такого рода алгоритмы также называют приближенными. Их часто используют в реальных задачах, для которых точные алгоритмы из-за большого размера задачи работают недопустимо долго.
11.1. Порождение подмножеств
11.1.1.	Все подмножества
Задача 11.1. Пусть А = {а0, ах,..., ап_,} — множество целых чисел (положительных I или отрицательных). Построить все его непустые подмножества.	I
Вход. В первой строке задано количество чисел п, л<20, во второй — л це- I лых чисел типа longint.	|
310
ГЛАВА 11
Выход. Последовательность строк, в каждой из которых через пробел указаны числа, образующие подмножество. Порядок строк и чисел в них значения не имеют.	1
Анализ и решение задачи. Нужно перебрать 2" подмножеств множества А. Рассмотрим два различных порядка перебора.
Первый способ. Опишем перебор рекурсивно. Каждое подмножество или не содержит а0, или содержит его. Для образования всех множеств достаточно перебрать все подмножества множества {ар а затем снова перебрать их, добавив к каждому а0. Вообще, если есть некоторое текущее подмножество В, Bq {а0, ар ...,0^,}, то перебор всех множеств, включающих его, означает перебор всех подмножеств множества {а(..для чего нужно перебрать все подмножества {аж, ...,0^} и затем
повторно перебрать их, добавив аг Итак, имеем рекурсивный алгоритм перебора всех подмножеств.
Представим «-элементное множество А, где п < maxn, maxn = 20, массивом А типа alnt= array [O..maxn-1] of longint. Представим текущее подмножество В где Bq {а0, а,..массивом типа aByte= array [0. .maxn-1] of byte
В Ci] =1, если A[i] принадлежит В, иначе B[i] =0. Пусть очевидная процедура writeSet печатает текущее подмножество, представленное массивами А и В. Подмножества строятся следующей рекурсивной процедурой subSets: procedure subSets(var A : aLong; var В : aByte; n, i : byte); begin if i < n-1 then subSets(A, B, n, i+1) else writeSet(A, B, n); В[i] := 1; if i < n-1 then subSets(A, B, n, i+1)
else writeSet(A, B, n);
B[i] := 0; end;
Программа с необходимыми объявлениями, инициализацией переменных и вызовом subSets (А, В, п, 0) очевидна.
Приведенный алгоритм перебирает подмножества в порядке, который выражен следующим списком значений массива в (кодов подмножеств):
о	о	...	о	О
О	О	...	О	1
О	О	...	1	о
О	О	...	1	1
1	1	...	1	о
1	1	...	1	1
Очевидно, этот порядок кодов является лексикографическим (предполагаем, что О < 1).
ПЕРЕБОР ВАРИАНТОВ
311
Переход от подмножества к следующему в некоторых ситуациях требует больших изменений подмножества, многочисленных возвратов из рекурсии и углублений в нее. Например, приА = {1,2,3,4} следующим после подмножества {2,3,4} будет {1}. Чтобы перейти к нему, нужно удалить три элемента и добавить один, выйти из трех рекурсивных вызовов и*в три углубиться.
Нетрудно написать нерекурсивный аналог приведенной процедуры, по которому подмножества перебираются в том же порядке. Чтобы перейти от текущего подмножества к следующему, нужно проимитировать добавление 1 к коду подмножества как представлению целого числа (его младший разряд справа) и соответственно изменить текущую сумму. Однако ситуации с большим количеством изменений при переходе от текущего подмножества к следующему остаются.
Второй способ. Рассмотрим порядок перебора множеств, по которому, чтобы перейти от любого подмножества к следующему, нужно либо удалить, либо добавить только один элемент. Впрочем, дополнительные вычисления понадобятся и здесь, но их объем несколько меньше, чем при первом способе.
Начнем с двух примеров. Пусть множество А состоит из одного элемента. Коды его подмножеств — 0 и 1. Ясно, что для перехода от пустого подмножества к А нужно изменить код в единственном разряде 0. Пусть А содержит два элемента. Рассмотрим коды его подмножеств в следующем порядке: 00, 10, 11, 01. Соседние коды отличаются в одном разряде, т.е. при переходе к следующему подмножеству один элемент или добавляется, или удаляется. При проходе по этим кодам изменяются разряды с номерами 0, 1,0. Заметим также, что в первой паре кодов h, = 0, во второй паре ^ = 1, а последовательности значений разряда Ъо противоположны — 0, 1 и 1,0.
ПустьА= {а0, ар Предположим, что СД0...0,..., С^ОО.,.0, Си00...0 — последовательность кодов всех его подмножеств, содержащих только элементы а0, ар ..., at_p где m=2k, к<п, причем соседние коды С отличаются только в одном разряде. Тогда в последовательности кодов
С100...0..Cm_i00...0, Cm00...0, Q10...0, СтЧ1О...О,..., С110...0
каждый следующий также отличается от предыдущего только в одном разряде. Новые коды представляют все подмножества элементов а0, at,..., ak_t, ак.
Итак, начав при к=0 с кода 00...0 и “удваивая” описанным способом для к- 1, 2, ...,п-1 последовательность кодов, получим последовательность всех подмножеств, в которой каждое следующее отличается от предыдущего только одним элементом. Выясним, в каком порядке для этого нужно изменять разряды в кодах.
Обозначим через Sk (к> 1) последовательность номеров разрядов s2, ..., яи_р отличающих Сг от Ср С3 от С2, ..., Ст отСт_р Длина Sk равна 2*-1. Ясно, что для “удвоенной” последовательности кодов Sk,l имеет вид Sk, к, S*, где S* — обращенная длина SM равна 2*+1-1. Например, начав с последовательности 5,=0, получим
S2=0,1,0,
53 = 0,1,0,2,0,1,0,
= 0,1, 0,2, 0, 1, 0, 3, 0, 1, 0, 2, 0,1, 0.
312
ГЛАВА 11
Рассмотрим последовательность номеров разрядов St= (sp s2,	s^,), где m=2*,
найдем зависимость s. от i. По построению Sk, s=0 при нечетных i, s=k-l при i=2‘"‘ Аналогично s = l-\ при любом i=2w, где 1 < 1<к. Возникает гипотеза, что s. равен количеству нулей, которыми заканчивается двоичное представление числа/, т.е. максимальному показателю степени числа 2, которая делит /.
► Докажем эту гипотезу, проведя индукцию по показателю степени / числа 2. База очевидна. Для обоснования перехода предположим, что утверждение верно при любом i = 2‘-j, где J=l, 2, ...,2-1. Поскольку (2-J)+ (2'+j)=2M, в представлениях чисел (2-J) и (2'+у) номера младших разрядов, равных 1, совпадают. Поэтому для /= 2'+j= 2l+'-(2'-j) гипотеза также подтверждается. 1
Итак, начав с первого кода 00...О, по номеру / очередного кода (;= 1, 2,...,2") вычислим номер разряда s как максимальный показатель степени числа 2, которая делит /. Затем инвертируем s-й разряд в коде и получим код следующего подмножества. При /=2" получим s=n — признак окончания.
Последовательность кодов подмножеств «-элементного множества, получаемая описанным способом, называется бинарным кодом Грея порядка п.
Реализуем описанный способ, основанный на построении кода Грея, следующей процедурой (смысл некоторых имен объяснялся выше): procedure subSetsGrey(var А : aLong; var В : aByte; n ; byte); var k, s : byte;
i, icopy : longint;
begin
for k := 0 to n-1 do B[k] := 0;
i : = 0; S : = 0 ;
while s < n do begin writeSet(A, B, n); i := i+1; s := 0; icopy := i; while icopy mod 2 = 0 do begin icopy := icopy div 2; S := s+1;	(***)
end;
if s < n then B[s] := 1-B[s];
end; end;
Анализ решения. Сколько раз выполняется тело цикла (***)? В наихудшем случае s=n (при i = 2"). Однако в среднем s невелико и вообще не зависит от л. Убедимся в этом. При выполнении внешнего цикла repeat значения s вычисляются как элементы последовательности Sn, в конце которой добавлено п. Например, при л = 3 это последовательность 0, 1, 0,2,0, 1,0, 3. С помощью индукции нетрудно доказать, что при любом п сумма 2" чисел такой последовательности равна 2"-1, т.е. тело цикла (*•*) в среднем выполняется меньше одного раза!
Замечание. Представим подмножества как вершины графа; смежными будут подмножества, отличающиеся одним элементом. Например, {1} и {1,2} смежны, {1} и {2} — нет. Граф подмножеств, представленных последовательностями из 0 и 1 дли
ПЕРЕБОР ВАРИАНТОВ
313
ной п, образует л-мерный двоичный куб. Код Грея представляет гамильтонов мар-шрут в этом кубе (маршрут, проходящий через все вершины по одному разу). Пример для п = 3 приведен на рис. 11.1.
Рис. 11.1. Гамильтонов маршрут в трехмерном двоичном кубе
11.1.2. Подмножества с заданным числом элементов
Задача 11.2. Напечатать все fc-элементные подмножества множества {1, 2, ...,п}, где 1<£<л, л<30. Порядок строк и чисел в них значения не имеют.
Анализ и решение задачи. Можно использовать решения предыдущей задачи, добавив работу со счетчиком количества элементов в текущем подмножестве. Однако это неэффективно — например, во втором способе вместо С(п, к) возможных подмножеств придется рассмотреть все подмножества, а их 2". Подойдем к решению иначе и рассмотрим два алгоритма решения.
Нерекурсивный алгоритм. Рассмотрим ^-элементные подмножества как возрастающие последовательности чисел от 1 до п длиной к (например, подмножество 4,1,2} рассматривается как последовательность <1,2,4>). Переберем эти последовательности в лексикографическом порядке. Например, при п = 5 и к=3:
12	3	14	5
1	2	4	2	3	4
1	2	5	2	3	5
1	3	4	2	4	5
1	3	5	3	4	5
Основная задача — по очередной последовательности <ах,...,ак> найти следующую. Заметим: вначале изменяется последний элемент. Когда он “добирается” до л, увеличивается предпоследний, последний становится на 1 больше предпоследнего, и затем снова возрастает последний. Обобщим это наблюдение: если в конце последовательности <а,, ...,ак> несколько элементов достигли своих предельных значений, т.е. ак=п, ак_1=п-1, ..., aM=n-k+r¥l, но аг<п-к+г, то следующей будет последовательность
<ai,..., ar-i, аг+1, аг+2,..., аг+Л-т+1>.
314
ГЛАВА 11
Например, после <1,4,5> идет <2,3,4>. Если же а* < л, то ^увеличивается на 1. Дей ствия в этих двух ситуациях, по сути, не отличаются: в текущей последовательное™ найдем первый справа номер г, при котором а< n-k+r, и присвоим элементам аг, а^. ..., ak значения аг+1, а +2, ..., а+к-r+l. Если при этом ак стало равным п, то все элементы аг, аж, ..., также получили свои предельные значения и следующим значением г будет г-1.
Очевидно также, что последовательность предельных значений <п-к+1, п-к+2, ...,п является последней и дает г=0; это условие используем как признак завершения.
Описанные действия реализованы в следующем фрагменте программы, гае writeAr(a, к) —вызов процедуры вывода значений первых к элементов массива А. for i := 1 to к do a [i] : = i; {первая последовательность} г := к; {сначала увеличивается последний элемент} while (г > 0) do begin writeAr(a, к); writein; if а[к] < n then r := к else r := r-1; if r > 0 then for i := к downto r do a[i] := a[r]+i-r+l end
й Дополните фрагмент до программы. Указание. Не забудьте, что при к=п нужно вывести только одну последовательность < 1,2, ...,£> и закончить работу.
Рекурсивный алгоритм. Вернемся к списку подмножеств в их лексикографическом порядке при л = 5 и к=3. Первое значение а1 в элементах списка изменяется медленнее всего, и при каждом а1 значения <а2,...,ак> упорядочены также леквйко-графически. Это наводит на мысль о рекурсивном порождении элементов списка.
Рассмотрим произвольную позицию г в последовательности <а1,...,ак>. Из построения нерекурсивного алгоритма очевидно, что при г>1 и значениях <а1,...,аг_ > предыдущих элементов минимальное значение аг равно а^+1, максимальное — п-к+г.
Таким образом, на глубине г, имея начало последовательности <ар ...,аж>, нужно перебрать значения аг от а^+1 до п-к+r и для каждого рекурсивно перейти на глубину г+1. Глубина 1 отличается только тем, что минимальное значение at равно 1 а глубина к — тем, что после присваивания очередного значения ак нужно выдать полученное подмножество.
Приведенные соображения реализованы в следующей рекурсивной процедуре для выполнения которой нужен вызов subR (1).
procedure SubR(г : integer);	*
var i, lb : integer; {lb - минимальное значение a[г] } begin
if r ® 1 then lb := 1
else lb := a[r-l]+l;
for i ;= lb to n-k+r do begin a[r] := i; if r = k then writeAr(a, k)
ПЕРЕБОР ВАРИАНТОВ
315
else SubR(r+l)
end;
end;
H Напишите всю программу (возможно, параметризовав приведенную процедуру массивом и его длиной).
11.2. Порождение последовательностей
11.2.1. Размещения ферзей
Задача 11.3 (о ферзях). Шахматная доска имеет размеры пхп. По правилам шахмат ферзь атакует все поля и фигуры на одной с ним вертикали, горизонтали и диагонали. Расположение нескольких ферзей на шахматной доске назо- I вем их размещением. Размещение назовем допустимым, если ферзи не атакуют I друг друга. Размещение п ферзей на шахматной доске пхп называется полным. I Построить все полные допустимые размещения п ферзей, где 4 < п < 20.	|
Анализ и решение задачи. Допустимые полные размещения существуют не при каждом значении п. Например, при п=2 или 3 их нет. При п=4 их два, и они симметричны (рис. 11.2).
Рис. 11.2. Два полных допустимых размещения на доске 4x4
Ясно, что в допустимом размещении каждый ферзь занимает отдельную вертикаль и горизонталь. Пронумеруем вертикали и горизонтали номерами 1, 2..п.
В шахматах первой обозначается вертикаль, а второй — горизонталь, поэтому <НХ, Н2, ...,Н> обозначает последовательность номеров горизонталей, занятых ферзями в вертикалях 1,2.i (1 < i<n). Пустое размещение о соответствует i=0.
Есть п способов разместить ферзя в первой вертикали, т.е. перейти от пустого размещения к непустому. Этот переход обозначим стрелкой (рис. 11.3, о). При каждом расположении ферзя в первой вертикали есть п положений во второй вертикали; из них сразу отбросим недопустимые, отметив знаком 1 *1 (рис. 11.3,6).
Опишем построение всех допустимых размещений.
Пусть есть допустимое размещение ферзей в первых i-1 вертикалях S(i-1) = <Я1, ...»	Для построения всех допустимых размещений
с началом S(i-l) перебрать все допустимые размещения S(i) с ферзем в i-й вертикали и для каждого построить Все допустимые размещения с этим началом S(i) .
316
ГЛАВА 11
Рис. 11.3. Начальные размещения ферзей на доскепхп
По этому рекурсивному алгоритму поиск допустимых расположений ферзей в n-i+ последних вертикалях сводится к поиску заполнений n-i последних вертикалей. Реализуем его рекурсивной процедурой Search. Пусть MaxSize = 20— максимальный размер доски. Номера вертикалей и горизонталей представим диапазоном Number = 1. .MaxSize, а размещение — массивомн типа aHoriz = array [Number] of Number.
Процедура Search ищет все полные размещения, начиная с i-Й вертикали доски размером и, при фиксированных допустимых Я[1],	Z/[z-l]. Допустимость разме-
щения <Я[1],..., H[i~ 1], Я[(]> проверяется функцией Test; полное допустимое размещение печатается процедурой WriteSet.
procedure Search(var H : aHoriz; n, i s Number);
var j, k : Number;
begin
for k := 1 to n do begin
H[i] := k;
if Test(H, i) then
if i = n then WriteSet(H, n) {печать полного размещения} else Search(H, n, i+1) {рекурсивный вызов} / end
end;
Функция Test проверяет допустимость размещения <W[1],...,	при ус-
ловии, что <//[1], ...,Н[/-1]> допустимо. В допустимом размещении ферзь в поле О',Н[(]) занимает собственные горизонталь и диагонали, т.е. Я[[]*//[/] и	i-
при всехj= 1, 2,..., i-l.
function Test(var H ; aHoriz; i : Number) : boolean;
var j : Number;
begin
{проверка, не лежит ли поле (i, Н [i]) в занятой} {горизонтали или занятых диагоналях} j := 1;
while (j < i) and (H[i] о H[j] )
and (abs (H[i]-H[j] ) <> i-j) do inc(j);
Test := (j = i) end;
Программа с необходимыми объявлениями, подпрограммами и телом с вызовом Search (Н, n, 1) очевидна и не приводится.
ПЕРЕБОР ВАРИАНТОВ
317
При размере доски, например 4, печатаются два размещения, а при размере 8 — 92. При этом происходит соответственно 60 и 15720 вызовов функции Test. Отметим, что количество всех возможных размещений (полных и неполных, допустимых и недопустимых) выражается через размер доски л как л+ л2+... +и". При л=4 это 340, а при и = 8 — больше 16-106. Как видим,
• своевременное отбрасывание недопустимых неполных вариантов позволяет существенно ускорить поиск.
11.2.2.	Дерево размещений и его обход
Размещения ферзей на шахматной доске, которые строятся в процессе выполнения процедуры Search, можно представить узлами корневого ориентированного дерева (рис. 11.4). Каждый узел <Я[1],..., 7/[z]>, где 0< i<n, имеет сыновей
<Я[1], , ЯШ, 1>, <Я[1], ..., ЯШ, 2>.<Я[1].ЯШ, л>
и соответственно называется их отцом. Сыновья узла, сыновья его сыновей и так далее называются его потомками, а он — их предком. Пустое размещение о является корнем дерева, полные и недопустимые размещения — его листьями, а допустимые неполные — промежуточными узлами. Каждый узел дерева имеет глубину в дереве. Корень — глубину0, его сыновья— 1 и т.д.; глубина узла дерева равна длине размещения, представленного узлом. Полным размещениям соответствуют
<1,3,5,..., Н[л]>
Рис. 11.4. Дерево размещений
Обобщим и несколько изменим алгоритм, реализованный процедурой Search. Дополнительно предположим, что корень дерева может быть его единственным листом. Пусть А обозначает лист или промежуточный узел с сыновьями А,, А2, ..., Ая; Обход(х) — обход дерева или поддерева с корнем х. Тогда Обход(А) имеет следующую схему:
if А является листом then обработка листа А else
for k ;= 1 to n do begin
переход к узлу Ak;
if Ak является допустимым then Обход(Ak) end
318
ГЛАВА 11
Как видим, обход дерева с некоторым корнем заканчивается только после того, как закончен обход всех потомков корня. Кроме того, рано или поздно достигается каждый допустимый узел дерева.
Описанный обход называется обходом дёрева в глубину — попав в промежуточный узел, мы сразу переходим к одному из его потомков, углубляясь в дерево. При этом происходит перебор узлов дерева с возвращениями, используемый в решении многих задач.
11.2.3.	Обход дерева с помощью магазина
Реализуем обход дерева в глубину с помощью магазина узлов. С каждым узлом свяжем данные о размещении, которые добавляются в магазин при переходе к этому узлу. Корневой узел соответствует пустому размещению и не содержит данных. При переходе от узла <Я[1], ...,Я[/-1]> к узлу <Я[1], ..., H[i-1],k> в поле (i,k) ставится ферзь, С новым узлом свяжем пару чисел (i,fc), т.е. пару номеров вертикали и горизонтали, и добавим к магазину. Магазином является массив н; при добавлении новой пары (i, к) увеличиваем номер вертикали i (переходим к следующему элементу массива) и присваиваем н [ i ] : = к.
Цикл с заголовком for к : = 1 to n do в процедуре Search задает перебор узлов-братьев
<Я[1]...H\i-1], 1>,<Я[1], ...»Я[<—1], 2>,.... <Я[1], ..., Я[/-1], л>,
т.е. последовательное удаление из магазина предыдущего брата и добавление следующего.
Рассмотрим переходы, связанные с узлами дерева. От допустимого узла-листа переходим к его отцу, от недопустимого — к его брату, если тот существует, или к отцу Переходы, связанные с промежуточным узлом, представлены на рис. 11.5.
1. От узла-отца или левого брата
4. К правому брату или отцу
/	3. От правого сына
2. К левому сыну
Рис. 11.5. Переходы, связанные с промежуточным урюм
Промежуточный узел посещается дважды — в начале и в конце обхода дерева, корнем которого он является. Чтобы отличить эти две ситуации, нужны дополнительные данные. При размещении ферзей переходу от узла к его правому брату соответствует увеличение Я[г] на 1 (узел выталкивается из стека, а вместо него добавляется его правый брат). Поэтому, когда обрабатывается узел глубины i, магазин содержит по одному узлу каждой глубины т, где m<i, и для каждой глубины достаточно
ПЕРЕБОР ВАРИАНТОВ
319
одной дополнительной переменной. Ее значение показывает, закончен ли обход дерева, корнем которого является текущий узел на данной глубине. Представим эти переменные в булевом массиве F: F[»1 = false, если к узлу на глубине i мы приходим сверху или слева, и F[j] = true — если снизу.
Возвращение к отцу в задаче о ферзях представим условием /ф]=и. Возвращение к корню дерева (i=0) означает конец обхода.
i := 1; H[i] := 1; F[i] := false;
while (i <> 0) do begin
if i = n then	{ обработка узла-листа}
if Test(H, i) then begin
{печать полного допустимого размещения и}
{возвращение к отцу независимо от наличия братьев} WriteSet(Н, n); dec(i); F[i] := true
end
else
if H[i] < n then inc(H[i]) {переход к правому брату}
else begin {возвращение к отцу; поддерево,}
{в котором он является корнем, уже обошли} dec(i); F[i] :=* true
end
else	{обработка промежуточного узла}
if not F[i] and Test(H, i)
then begin {движение в глубину}
inc(i); H[i] := 1; F[i] := false end
else	{движение вправо или вверх}
if H[i] < n
then begin	{движение вправо}
inc(H[i]); F[i] := false
end
else begin	{движение вверх}
i : = i -1;
if i > 0 then F[i] := true
end end
» Напишите программу с нерекурсивной процедурой поиска.
Обобщим приведенный алгоритм, предполагая, что корневой узел, как и другие узлы, содержит некоторые данные, а узлы-листья могут иметь допустимых братьев.
затолкнуть корневой узел в стек;
while магазин не пуст do begin
пусть А - узел на верхушке стека;
if А является листом then begin
обработать лист А;
вытолкнуть А из стека;
if А не является правым сыном своего отца then затолкнуть в стек правого брата А; end
320
ГЛАВА 1
else {А - промежуточный узел}
If А является допустимым и дерево с корнем А не обработано then затолкнуть в стек левого сына А
else begin
{дерево с корнем А обработано или А недопустим} вытолкнуть А из стека;
if А не является правым сыном своего отца и не является корнем
then затолкнуть правого брата А в стек;
end end.
Представленные алгоритмы не означают, что для нерекурсивного обхода необходимо явно строить дерево перебора в виде некоторой структуры данных. Обычно дерево не хранят, а строят и “забывают” по мере продвижения, как и в рекурсивных подпрограммах. Речь здесь идет главным образом о том, в какой последовательности обычно происходит перебор (в том числе и рекурсивный) и как его запрограммировать.
11.2.4. Порождение всех перестановок
Задача 11.4 (о перестановках). Построить все возможные перестановки чисел 1,2,..., п.
Анализ и решение задачи. Отвлечемся от условия задачи и вернемся к рекурсивному алгоритму поиска размещений ферзей (см. подраздел 11.2.1). Функция Jest проверяет, не лежит ли поле (i,H [i]) в занятой горизонтали или занятых диагоналях, т.е. верно ли, что для всех j < i
(H[iJ <> H[j] ) and (abs (H[iJ -H[j] ) <> i-j).
Если для всех j<i выполняется условие (Н [i] <>Н [j ]), то горизонтальН [i]» свободна. Заменив условие продолжения цикла в функции Test условие (Н [ i ] < > Н [ j ]), получим решение задачи о полных допустимых размещениях ладей (ладья атакует поля по горизонтали и вертикали).
Представим размещение ладей в вертикалях 1, 2, ..., п последовательностью номеров занятых горизонталей <ЯР Н2, ...,НЛ>. Очевидно, что все эти последовательности <Ht, Н2, —,Н> являются всевозможными перестановками чисел 1, 2,..., п. Таким образом, процедура Search с упрощенной функцией Test порождает все перестановки чисел 1, 2,..., п.
По этому алгоритму п! перестановок порождаются в лексикографическом порядке:
1, 2, 3,..., п—2, п-1, п,
1, 2, 3,..., п—2, п, п—1,
1, 2, 3,..., п—1, п—2, и,
1, 2, 3,..., п—1, п, п-2,
и, и—1, п—2, .... 3, 2, 1.
Основной недостаток алгоритма — дополнительные расходы времени, связанные с переходом к следующей перестановке (выходы из рекурсивных вызовов и углубления в них).
ПЕРЕБОР ВАРИАНТОВ
321
Рассмотрим другой алгоритм порождения всех перестановок, по которому для перехода к каждой следующей перестановке проводится транспозиция (обмен местами) только двух соседних элементов предыдущей.
Для примера рассмотрим перестановки чисел 1, 2, 3 в таком порядке. Вначале зафиксируем порядок элементов 2, 3, добавим 1 перед ними и, меняя 1 местами с 2 и 3, переместим 1 в конец? Затем породим следующую перестановку элементов 2 и 3, поменяв их местами, и переместим 1 в начало (переставляемые элементы выделены):
(7,2,3), (2,1,3), (2,3,1), (3,2,1), (3,1,2), (1, 3,2).
Для произвольного п последовательность всех перестановок 1, 2, 3, ..., п образуется аналогично. Строим последовательность всех перестановок элементов 2, 3, .., п, к каждой добавляем 1 поочередно слева или справа с помощью и обменов пе-
ремещаем 1 в противоположный конец перестановки. Начало выглядит так.
1,2,3,..., л-1, п первая перестановка 2,3,...»п
2,1,3,..., n-1, п 1 перемещается вправо...
2,3,4,..., 1,п
2,3,4,..., л, 1
3,2,4,..., л, 1
3,2,4,..., 7, л
1 переместилась вправо; 2 делает шаг вправо
следующая перестановка 2,3,..., п 1 перемещается влево...
3,1, 2,4, ...,л
, 3,2,4,..., л	1 переместилась влево; 2 делает шаг вправо
1,3,4,2,..., п	следующая перестановка 2,3,..., и
1 перемещается вправо...
Последовательность всех перестановок элементов 2, 3,..., л порождается таким же способом, т.е. рекурсивно. “На дне рекурсии” находится транспозиция л-1 и л, т е. перемещение л-1 в конец перестановки л.
С помощью индукции по л нетрудно убедиться, что таким способом порождаются все перестановки элементов 1,2,3.л.
В описанном построении выделим следующие важные моменты.
•	Каждая транспозиция — это, по сути, шаг перемещения некоторого элемента i (i= 1, 2, ...,л-1) влево или вправо относительно очередной перестановки элементов i+1, i+2, ...,п. Относительные позиции при перемещении i представим числами 1, 2, ...,n-i+l; позицию 1 назовем стартовой, л-г+1 — финишной (i стартует вправо слева от перестановки элементов 7+1, i+2, ...,п, влево — справа).
•	Элемент i перемещается только тогда, когда все элементы 1,2, ..., i-1 достигли своего финиша, находясь слева или справа от очередной перестановки элементов i, i+1, i+2, ...,п. Поэтому i — наименьший из элементов, не достигших финиша.
•	После перемещения i финишные позиции элементов 1,2,..., i-1 превращаются в стартовые, а направления движения изменяются на противоположные.
322
ГЛАВА 11
Чтобы описать процесс построения перестановок, зафиксируем их представление. Очередную перестановку представим значением массива Р, переход к следующей — обменом значений в некоторой паре его соседних элементов.
Чтобы совершить очередной обмен, для каждого i (i= 1, 2, ..., n-1) нужно знать относительную позицию и направление перемещения. Позиции представим массивом целых R, направления — массивом D значений -1 (влево) или 1 (вправо). П этим данным несложно найти наименьший элемент i, не достигший финиша, и его абсолютное положение к в перестановке Р.
Начиная с i= 1, пропустим все, у которых 7ф] = п-i+l. При этом превратим их финишные позиции в стартовые (/?[/] := 1) и изменим направление (Z)[i] :=-D[i] Кроме того, подсчитаем количество L тех из них, которые оказались в начале перестановки Р, т.е. на старте для движения вправо (D[i] = 1). Тогда у наименьшего элемента имеющего /ф] < п-i+l, при движении вправо абсолютная позиция к в перестановке Р равна А+7ф], а при движении влево — L+(n-i+l-(/?[i]-l)) = L+(n-i+2-R[i]).
Транспозиция заключается в обмене значений Р[£] и Р[Л+1]; чтобы учесть относительный сдвиг элемента i, прибавим 1 к /ф].
Если для всех i-1, 2, ...,п-1 оказалось /ф] = п-i+l, значит, все элементы 1, 2. ..., п-1 достигли финишных позиций, и процесс построения завершен.
Приведенные рассуждения уточним следующим алгоритмом:
for i := 1 to n do begin
P[i] := i; R[i] := 1; D[i] := 1;
end;
R[n] := 0; { обеспечим R[i]<n-i+l при i=n } вывод массива P;
i : = 1 ;
while i < n do begin {переход к следующей перестановке }
i := 1; L := 0; { вычисление наименьшего i }
while R[i] = n-i+1 do begin
R[i] := 1; D[i] := —D[i]; { старт в новом направлении }
if D[i] =1 then L := L+l;
i := i+1;
end;
{ R[i] < n-i+1 }
if i < n then begin
if D[i] = 1 then k := L+R[i]
else k := L+n-i+l-R[i]; { на 1 меньше позиции i в P } транспозиция P[к] и Р[к+1];
вывод массива Р;
R[i] := R[i]+1;
end
end {i = n - элементы 1, 2, ..., n-1 в финишных позициях}
Анализ сложности. Оценим “накладные расходы”, связанные с циклом вычисления наименьшего i, при котором /ф] < п-i+l. Это i равно количеству раз вычисления условия в заголовке при однократном выполнении цикла.
Элемент i проходит от старта до финиша с помощью n-i транспозиций, а количество проходов равно числу перестановок элементов i+1, i+2, ..., п, т.е. (n-i)!. Значит
ПЕРЕБОР ВАРИАНТОВ
323
суммарно элемент i сдвигается (n-i)-(n-z)! раз. Отсюда условие R [i] =n-i+l вычисляется 1(п-1)(и-1)! + 2(л-2)(п-2)!+ ... + (п-1)11!раз. Но
Ь(л-1)(и-1)! + 2-(л-2)(л-2)! + ... +(л-1)11! = = 1(л! - £л-1)!)4- 2 ((л-1)! - (л-2)!) + ... + (л-1) (2! - 1!) = = л! + (л-1)! + (л-2)! + ... + 1! - л = О(л!).
Итак, собственно порождение всех перестановок имеет оценку сложности О(л!). Однако ее ухудшает очевидная оценка сложности вывода всех перестановок 0(л л!).
11.3.	Попытки сократить перебор
В задачах предыдущих разделов нужно было строить все возможные варианты подмножеств или последовательностей. Рассмотрим примеры задач, в которых нужно найти один или несколько вариантов, удовлетворяющих заданным условиям.
11.3.1.	Подмножества положительных чисел с заданной суммой
Задача 11.5. Пусть А = {«0, а{, ...,0^} — множество положительных чисел. Найти все его непустые подмножества с заданной суммой элементов М.
Вход. В первой строке — количество чисел л, л <30, во второй — л целых чисел, «^третьей — целое число М. Все числа и их суммы представимы в типе longint.
Выход. Последовательность строк, в каждой из которых через пробел указаны числа, образующие нужное подмножество. Порядок строк и чисел в них значения не имеют.
Пример
Вход 3	Выход 2 1
2 3 1	3
3
Анализ и решение задачи. Сразу заметим, что при М> S= а0+ а,+ ...+a„-j решения нет, а при M=S оно очевидно, поэтому предположим, что M<S. Переберем подмножества множества А, выводя только те, которые имеют сумму М. Перебор можно несколько сократить за счет того, что элементы положительны — если сумма подмножества В больше М, то все подмножества, включающие В, рассматривать нет смысла.
Используем решение задачи 11.1, выводя подмножества с суммой М и отсекая с большими суммами.
procedure subsets(var А : aLong; var В : aByte; n, i : byte;
M, S : integer);
begin
запускаем дальнейший перебор, HE включая i-й элемент
в множество }
if (i < n-1) then
subsets(A, B, n, i+1, M, S) ;
включаем i-й элемент в множество }
B[i] := 1; S := S+A[i] ;
324
ГЛАВА 11
S ? 8
if S = М then writeSet(A, B, n);
if (i < n-1) and (S < M)
then subsets(A, B, n, i+1, M, S);
B[i] := 0;
end;
Обратите внимание: массив В — параметр-переменная, поэтому значение В [i], изменяется на 1 перед вторым рекурсивным вызовом и после него “возвращается на место”. Однако s — параметр-значение, поэтому “возвращать на место” сумму выбранных элементов, вычитая A[i] из S, необязательно. В подобных ситуациях иногда удобнее даже изменять параметр прямо в вызове:
subsets(А, В, n, i+1, М, S+A[i]).
Как и в задаче 11.1, программа очевидна, только вызов subSets может иметь вид subSets (А, В, п, О, М,0).
►> Добавьте отсечение не только тех наборов подмножеств, суммы которых слишком велики, итех, суммы которых слишком малы. Указание. Если S+A[i+1] +A[i+2] +...+A[n-l] < выполнять первый рекурсивный вызов (не включив A [i] в сумму) бесполезно. Чтобы считать суммы вида A [i+1] +A[i+2] +...+А[п-1] каждый раз, используйте (глобальный массив, значениями элементов которого будут эти суммы, вычисленные вначале за один проход по массиву А
11.3.2.	Псевдополиномиальный приближенный алгоритм поиска подмножества
Задача 11.6. Изменим условие задачи 11.5 о подмножествах. Предположим, что значение М и значения элементов не очень велики (не больше 106). Нужно найти не все под множества с заданной суммой М, а хотя бы одно, но>если такого не существует, найти подмножество с суммой, максимально близкой к М снизу.
Анализ задачи. Можно использовать решение предыдущей задачи — перебирать подмножества и хранить максимальную сумму, не превосходящую М, пока не получена сумма М и не исследованы все подмножества. Однако подойдем к решению иначе — с помощью построения таблицы Т размером пхМ. Индексами ее строк являются номера чисел а0, at, ..., а^, расположенных по возрастанию, а индексами столбцов — числа 1,... М.
•	Значением T[i, Jt] будет сумма, максимально близкая к к снизу (в частно-сти, равная к), которую можно набрать с помощью только ао, а-\,а,.
Заполним таблицу на основе индукции. Ясно, что значения Т[0Д]=0 при к<а Т[0, к}=аа при к>ай удовлетворяют условию (*). Предположим, что при i> 1 значение T[i-1, fc] удовлетворяет условию (*), т.е. является суммой, максимально близкой к снизу и набранной с помощью а0, а1г..., u_r Заполним i-ю строку слева направо.
При k<at число а, использовать для суммирования нельзя, поэтому Tp', fc]=7{(-1,к T[i,k]-ai при к=а,. При к>а. вычислим разность г=к-а.. По предположению индукции 7I/-1, г] — это сумма, максимально близкая к к-a. снизу и набранная с помощью
ПЕРЕБОР ВАРИАНТОВ
325
, ах.arv Тогда Т[г-1, г]+а. — это максимально близкая к к сумма, в которой есть
слагаемое ар а Т[/-1, Л] — в которой нет. Остается выбрать
7]i, к] = max{ T[i-1, к-а,] + я„ Т[г-1, Л]}.
Отметим: подобные соотношения являются главной составляющей динамического программирования (подробности— в главе 13 и, в частности, в задаче 13.6). Алгоритм построения по таблице Т одного из подмножеств с максимальной суммой Дп-1, Л/] разработайте самостоятельно.
Анализ сложности. Из построения таблицы Т очевидно, что сложность вычисления Т[п-1, М] имеет оценку Q(nM), которая выглядит линейной относительно количества элементов и. Однако множитель М является частью входных данных задачи. При М порядка п, п, п, ... сложность полиномиальна, но если М сравнимо с 2", сложность становится экспоненциальной. Поэтому приведенный алгоритм называют псевдополиномиальным.
•	Условие задачи предполагает приближенное решение — сумма подмножества может отличаться от заданного числа М, но быть “достаточно близкой” к нему. Поэтому приведенный алгоритм является приближенным. Другие примеры приближенных алгоритмов приведены в главе 12.
11.3.3.	Идея метода ветвей и границ в задаче коммивояжера
Обход всех узлов дерева вариантов может оказаться очень длинным. Например, если в дереве с глубиной л все узлы допустимы и каждый промежуточный узел имеет  сыновей, т.е. все листья расположены на расстоянии л от корня, то всего в дереве + т+т + ...+т* узлов. Уже при т = 10 и л = 10 это больше, чем 1О10.
Во многих практических задачах требуется отыскать или построить какой-нибудь изо всех возможных вариантов, имеющий наименьшую стоимость, определенную в идаче. Общая идея решения таких задач — сократить обход дерева вариантов, отбрасывая ветви, о которых можно утверждать, что они не содержат вариантов, более дешевых, чем уже найденные. Рассмотрим пример.
Задача 11.7 (коммивояжера). Есть несколько городов; между некоторыми из них есть дороги с известной положительной стоимостью проезда. В одном из городов находится торговый агент (коммивояжер), который должен посетить каждый город по одному разу и возвратиться в начальный пункт. Ему нужно составить маршрут с минимальными дорожными затратами.
Анализ задачи. Переформулируем задачу. Есть граф, ребра которого нагружены положительными числами. Гамильтоновым циклом в связном графе называется пропой цикл, проходящий через каждую вершину. Стоимость гамильтонова цикла — это сумма стоимостей его ребер. Итак, задача коммивояжера состоит в том, чтобы шйти гамильтонов цикл наименьшей стоимости.
Очевидно, что граф не может иметь гамильтонова цикла, если он несвязен или имеет концевые вершины, поэтому в дальнейшем предположим, что граф связен и тепени всех вершин не меньше двух.
Рассмотрим решение задачи коммивояжера, состоящее в переборе подмножеств ребер графа. Искомый гамильтонов цикл строится как подмножество ребер; очевид
326
ГЛАВА 11
но что количество ребер в нем равно числу вершин. Некоторые промежуточные узлы дерева поиска могут быть недопустимыми, поэтому соответствующие поддеревья можно не рассматривать.
Есть еще один источник сокращения обхода дерева. Каждая вершина графа должна иметь два инцидентных ребра в гамильтоновом цикле. Отсюда возможную стоимость искомого цикла можно оценить снизу — сложить по всем вершинам по две наименьшие стоимости ребер, инцидентных каждой вершине. Очевидно, что стоимость любого гамильтонова цикла не меньше этой суммы.
Аналогично, имея некоторое допустимое подмножество ребер В, можно оценить снизу возможную стоимость гамильтонова цикла, включающего В. Каждая вершина v графа имеет r(v) (это 0,1 или 2) инцидентных ребер в В. Вычислим Е(В) — сумму стоимостей ребер в В. Для каждой вершины v прибавим к Е(В) стоимости 2-r(v) “самых легких” ребер среди тех, которые инцидентны v и не вошли в В (вычисление оценки будет уточнено в следующем подразделе). Очевидно, что стоимость гамильтонова цикла, включающего подмножество В, не меньше, чем Е(В), т.е. оценка Е(В) является нижней границей для стоимости потомков узла, соответствующего подмножеству В.
Отсюда, если при обходе дерева поиска раньше был получен гамильтонов цикл, стоимость которого меньше Е(В), то узел, соответствующий В, и всех его потомков рассматривать уже нет смысла. Оценка поддеревьев (ветвей) и отбрасывание явно неперспективных во многих случаях сокращает перебор и составляет суть метода ветвей и границ.
• В общем случае метод ветвей и границ не избавляет от перебора.
11.3.4.	Решение задачи коммивояжера методом ветвей и границ
Предположим, что вершины графа G представлены номерами от 0 до и -1. Ребра графа пронумерованы от 0 до т-1, где т>п, и рассматриваются в порядке возрастания номеров — е0, et,..., em-v но сама нумерация не уточняется.
Пусть В — допустимое подмножество ребер из множества {е0, ер ....е^,}, которое не образует гамильтонова цикла и имеет стоимость С=С(В), a Cmin обозначает текущую минимальную стоимость уже полученных циклов (если их не было, то Cmin = °°).
Рассмотрим обработку очередного ребра ек. Вначале ек не добавляется к В и при условии к<т-1 рекурсивно обрабатывается ребро ek+v Затем ребро ек добавляется к В. Если множество Ви{еД допустимо, то анализируется, образует ли оно гамильтонов цикл. Если образует, этот цикл запоминается лишь при условии, что стоимость Ви{ек} меньше Cm. Тогда же стоимость этого цикла становится значением С^. Если Bu{eJ не образует гамильтонова цикла, то вычисляется нижняя оценка Е = Е(Ви{ек}) возможной стоимости гамильтонова цикла, включающего Ви{ек}. Ребро ek+l рекурсивно анализируется лишь при условии к<.т-1 иЕ< С^.	‘
Дополнительно сократим перебор за счет следующих соображений. Если к текущему подмножеству В, содержащему |В| ребер, добавляется ребро ек, то с помощью т-1 -к еще не проанализированных ребер можно получить гамильтонов цикл лишь при условии (|В|+1)+ (т-1 -к)>п, т.е. \В\+т~к>п. Аналогично, если ек не добавляется кВ, цикл можно получить лишь при условии |B|+m-1 -к>п.
Указанные действия описываются следующим рекурсивным алгоритмом.
ПЕРЕБОР ВАРИАНТОВ
327
Алгоритм Searchcycle (к, В)
( Вначале е* не включается в В и анализируются дальнейшие ребра } if (к < m-1) and (|в|+т-1-к > п)
then SearchCycle(к+1, В);
В := В и {е*};	*( Затем е* включается в В }
if В является допустимым подмножеством then
if В образует гамильтонов цикл then begin
вычислить С = С(В);
if С < Cmin then begin
запомнить гамильтонов цикл В;
Cmin := С;
end
end
else begin { В не является гамильтоновым циклом } вычислить оценку Е = Е(В) ;
{ дальнейшие ребра анализируются, только если оценка Е(В) меньше стоимости Cmin уже полученного цикла }
if (k < m-1) and (|В|+m-k > n) and (Е < Cmin)
then SearchCycle(к+1, В)
end;
В := В \ {ек};
Уточним условия допустимости подмножества ребер. Подмножество ребер допустимо, если включается в множество ребер некоторого гамильтонова цикла. Очевидно, что каждое такое подмножество:
содержит не более чем п ребер;
не содержит цикла, образованного не всеми вершинами графа;
не содержит трех ребер, инцидентных одной и той же вершине графа.
Если хотя бы одно из этих условий нарушается, подмножество ребер недопустимо.
Уточним вычисление оценки стоимости Е(В) цикла, включающего текущее подмножество В ребер с номерами из множества {0, 1.£}.	Ребра с номерами не боль-
ше к уже проанализированы и добавляться к подмножеству В уже не будут. Таким образом, для каждой вершины v к Е(В) прибавляются стоимости r(v) ребер из В, инцидентных v, и стоимости 2-r(v) “самых легких” ребер среди тех, которые инцидентны v и имеют номера больше к. Если среди ребер с номерами больше к некоторая вершина имеет меньше, чем 2-r(v) инцидентных, образовать цикл невозможно.
Алгоритм и структуры данных для его реализации уточните самостоятельно.
11.3.5.	Упрощенный алгоритм
Упростим алгоритм решения задачи коммивояжера. В алгоритме SearchCycle тторядочим ребра по невозрастанию стоимости. К текущему подмножеству ребер в первую очередь будем добавлять легчайшие ребра (с наибольшими номерами), а решением считать первый полученный цикл.
Используем обозначения из предыдущего подраздела и предположим, что ребра , ер ..., упорядочены по невозрастанию стоимости. Булев признак ready озна
328
ГЛАВА 11
чает, что гамильтонов цикл получен (его начальным значением является false) при выполнении следующего рекурсивного алгоритма.
Алгоритм SimpleCommy (к, В)
{ Вначале ек не включается в В и анализируются дальнейшие ребра } if (k < m-1) and (|в|+т-1-к > п)
then SimpleCommy(к+1, В);
В := В и {ejt};	{ Затем ек включается в В }
if В является допустимым подмножеством then
if В образует гамильтонов цикл then begin
ready := true;
выдать цикл В;
end
else begin	{ В не образует гамильтонова цикла }
if (k < m-1) and not ready and (|в|+т-к > n)
then SimpleCommy(k+1, B)
end;
В := В \ {ek};
На рис. 11.6, а представлен граф, возле ребер которого указаны их стоимости. П алгоритму SimpleCommy для этого графа получим следующие подмножества ребер {2}, {2,3}, {2, 3,4}, {2, 3,5}, {2, 3, 5,6}, {2, 3, 5,7}, {2, 3, 5, 7,8}. Цикл, образованный ребрами 2, 3, 5, 8, 7, является оптимальным. Однако применение алгоритма к графу на рис. 11.6, б, дает подмножество {2, 3, 7, 8,15} с суммой 35, тогда как оптимальным является цикл {3, 4, 8, 7,6} с суммой 28.
Рис. 11.6. Два графа для задачи коммивояжера

11.4.	Послесловие
В этой главе представлены задачи перебора вариантов, для которых не найдены полиномиальные алгоритмы. По крайней мере, пока. Такие задачи называют труднорешаемыми. Например, у задач о коммивояжере и о подмножестве с заданной суммой пока нет и, предположительно, не будет точных полиномиальных алгоритмов решения.
Причина кроется в том, что обе задачи являются NP-полными. Рассмотрим это понятие неформально (строгое изложение см. в работах [3, 14, 22, 35, 42]).
В привычных нам алгоритмах варианты (комбинаторные объекты) порождаются и обрабатываются последовательно, один за другим. Если количество объектов экспоненциально, то, как минимум, такой же будет и сложность алгоритма. Вместе с тем существует понятие так называемого недетерминированного алгоритма, в котором предполагается, что нужный вариант можно “угадать” (отсюда и недетерминизм, т.е. неопределенность).
ПЕРЕБОР ВАРИАНТОВ
329
Если “угадывание” (порождение) и обработка любого объекта имеют полиномиальную сложность, то недетерминированный алгоритм называется полиномиальным. Задачи, для которых существуют полиномиальные недетерминированные алгоритмы, образуют класс, обозначаемый NP. В нем тысячи задач. Одна из них — задача коммивояжера: нетрудно убедиться, что породить любое подмножество вершин и проверить, образует ли оно гамильтонов цикл, мокно за полиномиальное время.
Класс задач, для которых существуют обычные полиномиальные алгоритмы, обозначают Р. Ясно, что PcNP. Однако все попытки решить, что верно — P=NP или P^NP — пока безуспешны, т.е. неизвестно, существуют ли в принципе для задач класса NP обычные полиномиальные алгоритмы. Тем не менее, специалисты предполагают, что P*NP. И один из аргументов — существование класса NP-полных задач.
Задача называется NP-полной, если она принадлежит классу NP и любая задача из класса NP является “не более сложной” чем она, в следующем смысле. Если есть алгоритм решения некоторой NP-полной задачи А, то для любой задачи В из класса NP можно построить алгоритм решения, который включает в себя алгоритм решения А и имеет полиномиальный “довесок” для преобразования исходных данных и результатов. Отметим, что полиномиальность “довеска” обеспечивает, что задача В является не более сложной чем задача А. При этом говорят, что задача В полиномиально сводится к NP-полной задаче А.
Важнейшее свойство NP-полных задач состоит в том, что все они полиномиально сводятся одна к другой. Значит, если бы для одной из них нашелся полиномиальный алгоритм решения, то он, за счет сводимости, был бы, по сути, единым алгоритмом для всех задач класса NP. Однако понятие “NP-полная задача” появилось в 1971 году, и с тех пор ни одна из таких задач полиномиально не решена. Это и наводит на мысль, что полиномиальных алгоритмов для точного решения NP-полных задач нет и не будет.
Существует немало задач, похожих на труднорешаемые, но в действительности имеющих полиномиальные алгоритмы решения. Например, задача о том, содержит ли граф гамильтонов цикл (простой цикл, содержащий все вершины графа), тесно связана с задачей коммивояжера и является NP-полной. Однако задача о том, содержит ли граф эйлеров цикл (проходящий по каждому ребру графа один раз), имеет решение, сложность которого прямо пропорциональна числу ребер графа. Аналогично NP-полной является задача определения, содержит ли граф простой маршрут длиной не меньше заданного числа. В то же время, внешне похожая задача об определении кратчайших путей между вершинами решается полиномиально.
На олимпиадах очень часто встречаются задачи, которые решать путем перебора можно, но не нужно, поскольку в действительности они имеют существенно более эффективное решение. Поэтому, прежде чем программировать перебор, необходимо убедиться, что никакого достаточно очевидного более эффективного метода нет и что время работы на входных данных нужного размера будет измеряться секундами, а не миллиардами лет.
Два метода полиномиального решения задач, условия их применимости и примеры использования в задачах рассмотрены в следующих двух главах.
Упражнения
11.1.	Поля шахматной доски размерами 8x8 отмечены целыми положительными числами. Нужно найти размещение ферзей, в котором они не атакуют Друг
330
ГЛАВА 1
друга и занимают поля с максимальной суммой отметок. Учесть, что тестовые данные могут содержать очень много тестов.
11.2.	Есть куча камней с заданными массами (положительными целыми числами Нужно разделить ее на две кучи так, чтобы суммарные массы камней в них отличались как можно меньше.
11.3.	Задано и-элементное множество целых чисел (положительных или отрица тельных) и число к, 1 < к<п. Найти все к-элементные подмножества множест ва А, сумма чисел которых равна заданному М.
Вход. В первой строке задано количество чисел п, п <30, во второй — и це лых чисел, в третьей — целые числа к и М. Все числа и их суммы представимы в типе integer.
Выход. Последовательность строк, в каждой из которых через пробел ука заны номера чисел от 1 до п, образующих нужное подмножество. Порядок строк и номеров в них значения не имеют.
Пример Вход	Выход
4	12
2	12 1	14
2	3	2 3
3 4
11.4.	Цифры четырехзначного числа попарно различны. Цифры переставляют, полученное число вычитают из исходного и получают число, цифры которого также образуют перестановку цифр исходного числа. Найти все такие числа.
11.5.	Реализовать программу с рекурсивной версией генерации перестановок чисел 1,2, ...,п.
11.6.	Построить все размещения чисел 1,2,..., п по к без повторений. 11.7. Задача о ферзях, доминирующих на доске. Построить все возможные размещения минимального количества ферзей, атакующих все свободные поля доски размером лхи. Например, при п = 3 достаточно занять центральное поле Ь2, при и=4 ответов несколько, и один из них — al и сЗ.
11.8.	Задача о доминирующих ферзях, которые не атакуют друг друга. Построить все возможные размещения минимального количества ферзей, атакующих все свободные поля доски размером пхл и не атакующих друг друга.
11.9.	Найти какую-нибудь строку длиной N, состоящую из символов А, в или С, у которой любые две соседние подстроки различны. Например, в строке АВ АС АВ А нет одинаковых соседних подстрок, а в строках АВААСАВ, АВАВ-САС, АВСАВСА есть.
Вход. Длина искомой строки N (N< 2-103). Выход. Строка текста с искомой последовательностью. Например, при
N= 7 это может быть АВАСАВА или АВАСАВС.
11.10.	В подразделе 10.5.3 подсчитывалось количество разбиений заданного натурального числа п на положительные слагаемые, расположенные по невозрастанию. Запрограммировать порождение всех таких разбиений двумя способами — рекурсивно и без Использования рекурсии.
ПЕРЕБОР ВАРИАНТОВ
331
11.11.	С рассказов в произвольном порядке размещаются в сборнике, состоящем из В томов (В<С). Сформировать сборник так, чтобы максимальная толщина тома (сумма количеств страниц вошедших в него рассказов) была как можно меньше. Каждый рассказ начинают с новой страницы, поэтому толщина тома есть сумма длин рассказов, входящих в него. Разрывать рассказы нельзя. Если есть несколько равноценных оптимальных решений, вывести любое из них.
Вариант 7. В = 2,2 < С< 20, суммарное число страниц не более чем 103.
Вариант 2. 3 <В < 5, В < С< 10, суммарное число страниц не более чем 105.
Вариант 3. 10<В<50, В<С<200, суммарное число страниц не более чем 2104. Решение может быть приближенным, т.е. допустимым (каждый рассказ включен только в один том), но неоптимальным (в действительности существует другое допустимое разбиение с меньшей толщиной самого толстого тома).
Вход. В первой строке текста— количества С и В, в следующих С строках — длины рассказов.
Выход. В первой строке — максимальная толщина тома, в следующих В строках — списки номеров рассказов (в произвольном порядке), вошедших в соответствующий том. Например, если пять рассказов имеют длины 300, 500, 300, 300, 300 и распределяются по трем томам, то максимальная толщина тома — 600, первый том содержит рассказы 1 и 3, второй — 2, третий — 4 и 5.
11.12.	Задача об укладке рюкзака. Есть неограниченные запасы предметов п типов. Каждый предмет с номером i, где i= 1, ...,п, имеет заданные положительные вес wj и стоимость сг Нужно уложить рюкзак так, чтобы общая стоимость предметов в нем была как можно больше, а вес не превышал заданного Т. Форма предметов значения не имеет.
11.13.	Три станка обрабатывают детали с одинаковой скоростью. Заданы длительности обработки нескольких деталей; порядок обработки неважен. Обработка детали не прерывается. Станок переключается на обработку следующей детали мгновенно. Нужно так распределить детали между станками, чтобы обработка последней из них закончилась как можно раньше.
Вход. Количество деталей п, 1 <п<20, затем и натуральных чисел не больше 1000 — длительности их обработки. Числа разделены пробелами.
Выход. Общая длительность и последовательность пар вида (а; р), где а — номер детали от 1 до и; р — номер станка от 1 до 3. Пары упорядочены по номерам деталей. Если распределений с минимальной общей длительностью несколько, вывести любое из них.
Пример. Вход'. 6 7 8 9 10 11 12. Выход: 19 (1 1) (2 2) (3 3) (4 3) (5 2) (6 1).
11.14.	Задача о почтовых марках. Почтовая служба выпускает марки п различных достоинств и запрещает наклеивать на письмо более чем т марок. Стоимость почтового отправления, которую нужно оплатить марками, может быть любым натуральным числом. По заданным пит нужно найти наибольшее целое число В и все возможные наборы достоинств марок, позволяющие оплатить любую стоимость от 1 до В при указанных условиях. Например, при п = 2, т=4 любую сумму от 1 до 10 можно оплатить с помощью наборов {1,3}
332
ГЛАВА 1
или {1,4} (чтобы оплатить 11, нужно по пять марок этих наборов). Заметим: в наборе обязательно должна быть стоимость 1, наборы {1,5} и {1,2} не позволяют оплатить 9, а имея набор вида {1, к}, где к>6, нельзя оплатить 5.
11.15.	Ребра графа имеют длины. Построить остовное дерево минимального диаметра.
Глава 12
Жадные алгоритмы
В этой главе...
*	Примеры жадных алгоритмов
*	Матроиды и жадные алгоритмы
♦	Применение жадных алгоритмов в задачах переборного характера — быстрое, но не всегда правильное решение
12.1.	Знакомство с жадными алгоритмами
12.1.1.	Быстрый выбор упорядоченных вариантов
Задача 12.1. Перед праздниками Шеф получает очень много приглашений I на торжественные заседания. Чтобы лучше планировать свое время, Шеф ввел правило, чтобы в каждом i-м приглашении был четко указан отрезок времени заседания [а^Ь]. Шеф не любит половинчатых решений, поэтому или находится на заседании все указанное время, или не приходит на него. Между посещениями заседаний должен быть хотя бы минимальный перерыв, т.е. Шеф может успеть на j-e (по списку приглашений) после i-ro, если а} > bt.
Напишите программу, помогающую Шефу посетить как можно больше заседаний. Если различные расписания позволяют посетить одинаковое максимальное количество заседаний, найти любое из них.
Вход. В первой строке записано количество заседаний N, где 2< 7У<5000, в следующих строках — по два целых числа а, и Ь, (0 < а, < b. < 109).
Выход. Строка с N символами 0 и 1, обозначающими, согласен ли Шеф приехать на i-e (в порядке входных данных) заседание.
Пример
Вход 5 Выход 11001
2 17
26 50
17 20
10 15
20 25
Анализ задачи. В данной задаче правильным оказывается неожиданно простой алгоритм: на первом шаге выбрать заседание с наименьшим значением Ъ; на каждом
334
ГЛАВА 12
следующем шаге — с наименьшим значением Ь, но только среди тех заседаний, которые начинаются после конца предыдущего выбранного. Иными словами,
fl = arg min{fey}, 4 = arg min{b;} при k > 2.
J
Докажем, что применение этой идеи правильно решает задачу. Требование, что любое непервое посещаемое заседание нужно выбирать среди j, для которых заложено в условии, поэтому достаточно доказать только корректность стратегии “выбрать минимальное b среди допустимых”.
Предположим, что существует оптимальное расписание посещения заседании, где в качестве некоторого £-го (fc>l) по порядку посещения заседания выбрано ле заседание с наименьшим йз допустимых Ь. Тогда ничто не мешает построить расписание, в котором все пункты, кроме fc-ro, совпадают, а в качестве k-го выбрано заседание с наименьшим из допустимых Ь. Действительно, все выбранные ранее заседания (если такие есть) сохраним, поскольку выбрано Ь, наименьшее среди допустимых', все выбранные позже — тоже, ведь, не пересекаясь со “старым” Ь с минимальным b они тем более не пересекутся.
Провести обратную замену, т.е. заседание с наименьшим из допустимых b заменить заседанием с большим Ь, можно не всегда. Такая замена может привести к пересечению с более поздними заседаниями и в итоге уменьшить количество посещений.
•	Итак, никакое расписание посещения заседаний не может быть лучше расписания, построенного по указанному выше алгоритму.
•	Это не значит, что любое отступление от алгоритма приведет к неоптимальному расписанию. Утверждается лишь, что алгоритм дает одно из лучших решений. В примере из условия ответ правилен, но отличается от полученного по этому алгоритму.
Решение задачи. Для реализации представленного алгоритма сначала отсортируем массив интервалов времени по неубыванию значений Ь. Поскольку для окончательного ответа нужны исходные номера заседаний, целесообразно организовать данные в записи с полями а, b и idx (границы интервала заседания и его номер в начальном списке) и сортировать записи по значениям поля Ь. Реализация представлена в листинге 12.1.
Листинг 12.1. Быстрый выбор вариантов на основе сортировки
const MAXN = 5000;
type Inv = record a, b : longint; idx : word end;
alnv = array [1..MAXN] of Inv;
aAns = array [1..MAXN] of byte;
... { процедура эффективной сортировки массива записей }
var invit : alnv; { интервалы приглашений } i, N : word;
res : aAns; { массив для ответа }
b_prev : longint; { момент b последнего выбранного заседания }
ЖАДНЫЕ АЛГОРИТМЫ
335
BEGIN
read(N);
for i := 1 to N do begin { заполняем массив интервалов приглашений: }
read(invit[i].a, invit[i].b); { интервал приглашения } invit [i] . idx« := i; { его начальный номер }
end;
...{ вызов процедуры эффективной сортировки, упорядочивающей записи массива по возрастанию b }
fillchar(res, N, #0);	{ массив ответа инициализируем нулями }
b_jprev := -1;	{ -1 меньше любого допустимого а,- такая
инициализация позволяет не выделять выбор первого приглашения }
for i := 1 to N do begin
{ первый интервал, не пересекающийся с предыдущим, принимается }
if invit[i].а > b_prev then begin
res[invit[i].idx] := 1; { указываем его в ответе и } b_prev := invit [i] .b;	{ изменяем последнее b }
end;
end;
for i := 1 to N do { вывод ответа } write(res[i]);
END.
Анализ решения. Сортировка позволяет в качестве z\ взять первый элемент отсортированной последовательности, а для поиска всех следующих iM просто идти по последовательности, пропуская заседания, для которых a.<b.k. Поэтому сам выбор заседаний требует всего лишь 0(л) действий, а общая оценка сложности определяется сортировкой— O(nlogn) для “обычных” быстрых методов или 0(л) для поразрядной сортировки (см. раздел 5.3.6).
Если же массив не упорядочить по возрастанию Ь, то очевидная реализация той же основной идеи — для каждого выбора очередного заседания пересматривать список интервалов — означает 0(и2) действий.
12.1.2.	Сортировка и выбор в динамическом множестве
Задача 12.2. Шеф уделяет посетителям равные промежутки времени (например, каждому по пять минут). Чтобы попасть на прием, посетитель заранее записывается у секретаря, указывая начало и конец интервала времени [a.; £ ], в течение которого он желает зайти на прием. Целые числа а( и А. выражают количество интервалов приема, прошедших от начала рабочего дня Шефа. Помогите секретарю обработать собранные записи и составить расписание приема.
Вход. В первой строке текста записано количество посетителей (2< п< 104), в следующих п строках по два числа а. и bt> 0< а < Ь:<2п. Номера посетителей явно не заданы; они определяются номерами строк от 1 до л.
Выход. Число 1 (если установить расписание приема можно) или 0 (если нельзя); если выведена 1, то в следующей строке через пробел записываются |
336
ГЛАВА 12
номера посетителей, находящихся на приеме на последовательных интервалах приема. Если на интервале никто не заходит на прием, выводится -1.
Примеры
Вход	Выход	Вход	Выход	Вход	Выход
3	1	3	0	3	1
2 2	2 3 1	1 2		1 2	-12 13
0 1		1 2		1 2	
1 2		1 2		2 4	
Анализ задачи. Ясно, что из-за ограничения п< 104 какие-либо переборные реше ния невозможны. Рассмотрим три идеи построения простого алгоритма, избавленного от перебора.
Первая идея. Вначале предположим, что секретарь игнорирует записи и руководит процессом в приемной по мере прихода посетителей. (Для читателей, не имеющих бюрократического опыта: Шеф принимает посетителей в кабинете, а приемная — это комната, через которую проходят в кабинет; если приходят сразу несколько п сетителей, то один проходит в кабинет, а остальные ожидают в приемной.) Буде считать, что в момент t сначала в приемную заходят посетители, для которых а = (если такие есть), секретарь сразу решает, кого пропустить из приемной в кабинет а посетители, ожидающие в приемной, определяют, не закончилось ли их допустимое время, т.е. не стало ли t равным Ьг
Попробуем приглашать посетителей на прием в порядке возрастания значений а (“обычная очередь”). В примерах из условия так и есть: в первом примере приглашен сначала второй посетитель (а2=0), затем третий (а3 = 1) и последним первый (^ = 2). Получено упорядочение, позволяющее принять всех. В третьем примере все аналогично, только первый интервал приема пропущен, поскольку посетителей нет Во втором примере принять всех невозможно.
Однако этот способ не будет правильным, например, для интервалов [0; 2], [0; 2] [1; 1]. У третьего посетителя наибольшее значение а} = 1, но если пригласить его после первых двух, он не попадет на прием, поскольку b3= 1 < 2. Хотя в действительности принять всех посетителей можно, например, в порядке 13 2.
Третьего посетителя плохо приглашать последним потому, что у него малое значение Z>3 = 1. И вообще, посетителей с малыми значениями bt необходимо приглашать как можно раньше, потому что позже они не смогут попасть на прием. Однако сортируя по ар мы не обращаем на это внимания. Легко видеть, даже на этом примере входных данных, что и остальные разумные способы сортировки (сортировка по Ь‘, сортировка сначала по ар а при равных а( — по Ьр сортировка сначала по bt, затем по а) не дают правильного решения.
Вторая идея. Вспомним, что список в действительности ^аранее известен. Попробуем назначать время приема в порядке возрастания b-а., т.е. в первую очередь тем, у кого эта разница мала. Этот подход позволяет разобраться с упомянутым примером [0; 2], [0; 2], [1; 1]: наименьшее у третьего посетителя, поэтому сначала назначаем ему момент времени t = 1, затем назначаем первому момент 0, а второму — 2.
Однако приведенные рассуждения — это вообще не алгоритм, поскольку не установлено четко, на какое время записывать человека, если есть несколько возможностей. Если конкретизировать рассуждения, например, чтобы каждому посетителю
ЖАДНЫЕ АЛГОРИТМЫ
337
назначался наименьший возможный момент времени, получим неправильный способ. Например, при интервалах [0; 1], [1;2], [2;3], [0;2], если сначала назначать как можно более ранний прием всем, у кого b-ai = 1, то распределим первых трех посетителей, а затем, перейдя к большим b-а,, увидим, что четвертый не попадет на прием. Однако в действительности установить порядок можно, например 1 4 2 3.
Третья идея. В каждый момент времени t приглашаем посетителя с наименьшим А, но только среди уже находящихся в приемной, т.е. тех, у кого a<t (при одинаковых bt выберем посетителя с меньшим номером). Если наименьшее Ь, меньше t, закончим работу и выдадим “основной” ответ 0. Выведем в расписание -1, если в момент t в приемной посетителей нет, но позже будут. Объявлять, что все расписание построено успешно, можно только после того, как всем посетителям назначено время приема.
Это описание позволяет однозначно определить момент посещения каждого посетителя, т.е. является алгоритмом. Применим его к приведенным выше “проблемным” примерам.
Пример с интервалами [0; 2], [0; 2], [1; 1]. В момент 0 присутствуют первый и второй посетители с одинаковыми Ьр выберем любого из них, например первого. Первый посетитель уходит из приемной. В момент 1 входит третий, т.е. присутствуют второй и третий посетители с минимальным у третьего, поэтому он идет на прием. Второй посетитель успевает на прием в момент 2.
Пример с интервалами [0; 1], [1; 2], [2; 3], [0; 2]. В момент 0 присутствуют первый и четвертый посетители, из них по значению bt выбираем первого. В момент 1 присутствуют второй и четвертый посетители, выбираем кого-либо из них, например четвертого. В момент 2 из второго и третьего выбираем второго; в момент 3 идет третий.
Правильность приведенного алгоритма интуитивно ясна, но ее еще нужно доказать, тем более, что две подобные идеи уже оказались ошибочными.
Достаточно доказать два утверждения.
1.	Если алгоритм строит расписание, то оно допустимо.
2.	Если алгоритм сообщает, что принять всех посетителей нельзя, значит, допустимого расписания не существует.
► 1. Если алгоритм построил расписание приема, то в нем указаны все посетители и в каждый момент t или никто не заходит, или заходит посетитель, для которого в соответствии с алгоритмом а< t<bp т.е. расписание допустимо.	•
2. Пусть i — посетитель с интервалом [а(; Ь(] и минимальным Ьр которому алгоритм не присвоил момент приема, т.е. момент bf+1 оказался недопустимым для посетителя i. Это значит, что каждый момент t, где а.< t<bp по алгоритму был присвоен другим посетителям, имевшим интервалы [а; А] с a<t и Ь<Ье Поэтому, если вместо одного из них назначить посетителя i, “вытесненному” посетителю тем более нельзя назначить момент Ау+1, т.е. допустимого расписания не существует. <
• Нарушение правил именно этого алгоритма совсем не обязательно приводит к неправильному решению. Утверждается лишь, что, применив этот алгоритм, получим одно из правильных решений.
Сортировка посетителей по at не приводит к готовому правильному результату, но является полезным этапом: после нее несравненно легче поддерживать множество “присутствующих в приемной”. Поскольку в окончательном ответе нужны исходные
338
ГЛАВА 12
номера посетителей, целесообразно сортировать по а. записи, состоящие из полей а, и idx — начала и окончания интервалов посетителей и их номера в исходном списке
Множество присутствующих в приемной будет динамическим, непостоянным, поскольку к нему применяются следующие операции.
1.	Добавить новый элемент — посетителя, зашедшего в приемную в момент (значение Ь. при этом не ограничено сверху).
2.	Найти и удалить посетителя с минимальным значением Ьс
Множество может быть достаточно большим, поэтому позаботимся о структуре данных, позволяющей быстро выполнять указанные операции. Простые массив вряд ли позволят получить оценку количества действий существенно меньше п каждый из 0(п) моментов нужно просмотреть 0(и) элементов). Согласно многим источникам, например [21, 22], для указанных операций вполне подходит пирамида (см. подраздел 5.3.4), обеспечивающая оценку сложности O(n logn).
♦ Наш алгоритм предварительно сортирует посетителей по а/, поэтому для сортировки также необходим эффективный алгоритм.
►► Реализуйте представленный алгоритм с помощью простых массивов и пирамиды. Проведите сравнительные эксперименты с временем выполнения.
Задача о Шефе, принимающем посетителей, является примером задач теории расписаний (schedule theory). Введение в эту теорию, полезное для начинающих, содержится в [6].
12.1.3. Понятие жадного алгоритма
Алгоритмы, представленные в задачах 12.1 и 12.2, принадлежат к ширцкому классу так называемых жадных (greedy) алгоритмов.
Жадный алгоритм на каждом шаге выбирает локально наилучший вариант (тот который кажется наилучшим в данный момент); этот выбор является окончательным и далее не пересматривается.
Вообще говоря, придумать какой-то жадный, т.е. быстро выполняемый, способ выбора вариантов, на первый взгляд разумно связанный с поставленной задачей — не проблема. Труднее разобраться, правильно ли этот способ решает задачу при всех допустимых входных данных.
• Жадный алгоритм быстро дает решение, но оно может быть не наилучшим, поскольку далеко не для каждой задачи существует правильный жадный алгоритм (см. главы 11 и 13).
Чтобы обосновать жадный алгоритм, обычно доказывают, что использование только локально лучших выборов не может привести на дальнейших шагах к “плохим последствиям”. Смысл “плохих последствий” и аргументы, доказывающие их отсутствие, для разных задач бывают весьма различными.
Жадные алгоритмы иногда применяют, не доказывая их корректность — когда известно, что выгоднее решить задачу неточно, но быстро, чем не решить вообще или решать очень долго. Примеры такого применения приведены в разделе 12.3.
Вопросы применимости жадных алгоритмов весьма подробно изложены в [22]. При решении многих задач для выяснения, существует ли для них правильный жадный алгоритм, применяют теорию матроидов (подробнее см. [22, 24]).
ЖАДНЫЕ АЛГОРИТМЫ
339
12.2.	Матроиды и жадные алгоритмы
12.2.1.	Понятие матроида
Пусть Е — конечное множество, / — некоторое непустое семейство подмножеств множества Е, т.е. Iq 0(E). Пара (Е, /) называется матроидом, если удовлетворяет следующим условиям:
(Ml) 0 е Г, если А е I и В с А, то В е Г,
(М2) если Ael, B<zl и |А|<|Е|, то существует такой элемент е, для которого Аи{е}е.1.
Условие Ml называется свойством наследования, М2-1- свойством замены. Множества семейства I называются независимыми, остальные подмножества из 0(E) — зависимыми.
Пример 12.1. Пусть Е— множество ребер графа, I— семейство подмножеств ребер, не образующих циклы. Свойство наследования для (Е,Г) очевидно. Докажем свойство замены.
►	Вспомним алгоритм Краскала: остовное дерево графа (V, Е) строится наращиванием подмножеств ребер, образующих остовные леса. Сначала множество ребер пусто (остовный лес состоит из |V| одновершинных деревьев). На каждом шаге добавляется ребро и из двух деревьев образуется одно, т.е. количество деревьев уменьшается на 1. Поэтому остовный лес, содержащий t ребер, состоит из |V| -1 деревьев.
Пусть А и В — ациклические подмножества ребер и |А| < |В|. Они образуют остовные леса Ga и GB, причем количество деревьев М~|В| в GB меньше, чем число деревьев |У|-|А| в Ga. Тогда в лесу GB существует дерево, вершины которого принадлежат, как минимум, двум различным деревьям леса GA. Это дерево связно, поэтому оно содержит ребро, соединяющие вершины из двух разных деревьев леса GA, т.е. отсутствующее в GA. Добавление его к лесу GA не нарушает ацикличности множества А, поэтому свойство замены выполняется. <

Заметим, что в приведенном примере понятия независимости и допустимости подмножеств совпадают.
Утверждение. Если (Е,Г) — матроид, F — непустое подмножество множества Е, то максимальные независимые подмножества произвольного множества F имеют поровну элементов.
►	Предположим противное: для некоторого непустого множества F существуют максимальные независимые его подмножества А и В, причем |А <|В. Тогда в силу условия Ml существует независимое подмножество Bv |Bjдля которого = |А|+1. Тогда по свойству М2 существует такой элемент ее В{\А, для которого Аи{е) g I. НоАи{е)сС, что противоречит максимальности множества А. 4
Следствие. Максимальные независимые подмножества всего множества Е имеют поровну элементов.
Например, любое остовное дерево графа содержит |V|-1 ребро.
340
ГЛАВА 12
12.2.2.	Жадный поиск допустимого подмножества с максимальным весом
Рассмотрим два типа задач, для решения которых применяются жадные алгоритмы.
Пусть£={е,, ev еп} — конечное множество элементов и известны условия того, что те или иные элементы могут одновременно принадлежать некоторому подмножеству множества Е. Подмножества, удовлетворяющие этим условиям, назовем допустимыми. Например, подмножества попарно непересекающихся временных интервалов в задаче 12.1 или подмножества ребер графа, образующие ациклические подграфы в задачах, связанных с остовными деревьями и лесами (см. главы 8 и 9).
Типична задача поиска максимальных допустимых подмножеств, т.е. не являющихся подмножествами других допустимых множеств. Обычно требуются подмножества с максимальным числом элементов, как, например, в задаче 12.1.
В задачах второго типа для каждого элемента е множества Е определен действительный вес w(e), как правило, положительный. Требуется найти какое-нибудь допустимое подмножество S с максимальным весом (весом подмножества считается сумма весов его элементов).
Для задач второго типа жадный алгоритм имеет следующий общий вид (листинг 12.2).
Листинг 12.2. Общая схема жадных алгоритмов
Упорядочить Е = {elt в2, еп} по убыванию веса элементов, обеспечив w(ei) > w ег) > ... w(en) ;
S : = 0;
for i := 1 to n do
if Su{ei} является допустимым
then S :=
Как видим, на каждом шаге i к подмножеству 5 добавляется элемент et, имеющий наибольший вес среди необработанных"элементов е,, е^х, ..., еп, и в дальнейшем из S не удаляется.
В качестве примера рассмотрим решение задачи 12.1. По условию интервалы не имеют веса, но в решении упорядочиваются по возрастанию моментов окончания. Однако, чем раньше заканчивается интервал времени, тем больше времени остается после него до конца последнего интервала. Именно этот остаток времени неявно используется как вес интервала. Таким образом, интервал с наибольшим остатком является наилучшим, а полученное подмножество интервалов имеет наибольшую сумму весов.
12.2.3.	Взвешенный матроид и жадный алгоритм
Достаточное (но не необходимое!) условие того, что можно построить жадный алгоритм, правильно решающий задачу поиска допустимого подмножества с максимальным весом, связано с особенностями структуры допустимых множеств. А именно, алгоритм существует, если множество элементов Е и множество допустимых подмножеств I образуют так называемый взвешенный матроид. Рассмотрим это понятие.
Матроид (Е,Г) называется взвешенным, если для него определена функция w.E^R" (Я+ — множество положительных действительных чисел), задающая вес элементов.
ЖАДНЫЕ АЛГОРИТМЫ
341
Взвешенный матроид обозначим как тройку вида (E,I,w). Вес подмножества определяется как сумма весов его элементов.
Утверждение. Если (E,I,w) — взвешенный матроид, то множество S, найденное по алгоритму из листинга 12.2, является допустимым множеством с максимальным весом.
► Непосредственна из алгоритма следует, что множество S допустимо. Докажем, что среди допустимых множеств его вес максимален. Пусть S = {sp ..., sj, причем nfs}) > ... > w(sk). По алгоритму каждый элемент е, не включенный на некотором шаге в S, образовывал зависимое подмножество с некоторым подмножеством множества S, а значит, и с самим S. Поэтому 5 максимально.
Пусть Т= {гр ..., t,} — независимое множество, причем w(t,)> ...>w(tz). Поскольку S максимально, по следствию (см. предыдущий подраздел) получим, что к>1. Докажем, что w(s) > w(t) для всех i < I.
Предположим противное. Пусть i — наименьший индекс, при котором ) < н>((). Рассмотрим независимые подмножества А = {sp ...,sn} и В = {(,..., trp t}. По условию М2 существует элемент г, где j<i, не принадлежащий А, для которого множество {s,,..., srl, t} независимо. Но w(tp > w(t) > w(az), поэтому на i-м шаге алгоритма к S должен быть добавлен не sz, a tr Полученное противоречие доказывает, что w(s) > Цг) для всех i < I.
Отсюда следует, что w(S) >w(T). 4
Пример 12.2. Алгоритм Краскала строит остовное дерево не максимального, аминимального веса (см. главу 9). В отличие от представленной схемы, ребра вначале упорядочиваются не по убыванию, а по возрастанию веса. Заметим, что в графе с и вершинами каждое остовное дерево содержит п-1 ребро, поэтому за счет уменьшения количества ребер вес максимального допустимого подмножества уменьшить нельзя. На каждом шаге из оставшихся ребер, не образующих цикла с уже выбранными, выбирается легчайшее и далее из остовного леса не удаляется. Отсюда лес, образуемый после каждого шага, имеет наименьший вес среди лесов с данным числом ребер. 
Пример 12.3. Пусть Е — множество интервалов в задаче 12.1, I— семейство допустимых множеств интервалов. Свойство наследования для (Е,1) очевидно, но свойство замены не выполняется. Например, при Е'={[1,3], [2,5], [4,6]} положим А = {[2,5]} и В = {[1,3], [4,6]}. Очевидно, что добавление любого интервала к А нарушает допустимость.
Тем не менее, жадный алгоритм, представленный в решении задачи 12.1, работает корректно, ведь “матроидность” — это достаточное, но не необходимое условие! 
12.2.4.	Матричный матроид
Рассмотрим матрицу с действительными коэффициентами
а11	••• а1п
Пусть Е— множество ее строк, I— семейство множеств линейно независимых строк. Докажем, что М=(Е,Г) — матроид.
342
ГЛАВА 12
► Свойство наследования очевидно — подмножество любого линейно независимого множества строк также линейно независимо. Докажем свойство замены. Пусть А и В — линейно независимые подмножества строк матрицы и |А| < |В|. Если бы все строки множества В были зависимы от строк множествах, то любое их подмножество из |А|+1 строки было бы линейно зависимым, но это не так. Поэтому в множестве В есть строка, линейно независимая от строк множества А. Добавление ее к А образует независимое подмножество строк. <
Максимальное количество линейно независимых строк можно найти, например, приводя матрицу к треугольному виду на основе алгоритма Гаусса. Оформим эти вычисления в виде следующей функции:
function rank (var а : matr; tn, n : byte) : byte;
{a - матрица действительных чисел (m строк, n столбцов) }
var i, j, k : byte; t : real;
begin
for i := 1 to n do begin
if a[i,i] =0 then begin
k := i+1;
while (k <= m) and (a[k,i] = 0) do inc(k);
if k > m then begin
rank := i-1; exit;
end;
for j := i to n do begin
t := a[i,j]; a[i,j] : = a[k,j]; a[k,j] := t;
end;
end;
for k := i+1 to m do begin
for j := i+1 to n do
a[k,j] := a[k,j] - a[i,j]*a[k,i]/a[i,i];
a[k,ij := 0;
end;
end;
if m > n then rank := n else rank := tn;
end;
H На основе представленной функции напишите подпрограмму, которая по весам строк, заданным дополнительно, ищет линейно независимую систему строк наибольшего веса.
Аналогичный матроид, связанный с матрицей, можно получить, рассматривая в качестве Е множество столбцов.
12.3.	Некорректная “жадность” вместо перебора
12.3.1.	Поспешная укладка рюкзака
Рассмотрим еще один вариант задачи об укладке рюкзака (см. упражнение 11.12). Он отличается тем, что есть только один предмет каждого вида, его можно или положить в рюкзак, или не положить.
Задача 12.3. Для каждого из п предметов заданы положительные вес tv. и стоимость с, (z = 1, ...,п). Нужно уложить рюкзак так, чтобы общая стоимость
ЖАДНЫЕ АЛГОРИТМЫ
343
предметов в нем была как можно больше, а вес не превышал заданного Т. I Форма предметов значения не имеет.	|
Предположим, что нам приходится укладывать рюкзак, очень торопясь. Как мы действуем? В первую очередь кладем самое ценное, а потом — “что под руку попадется”1. Естественнд, результат далеко не всегда окажется наилучшим. Слегка уточним этот способ укладки с помощью алгоритма из подраздела 12.2.2.
Отсортируем предметы по невозрастанию отношения стоимости к весу r=c/w. Рассмотрим предметы в этом порядке. Если очередной предмет помещается в оставшейся части рюкзака, кладем его туда, иначе пропускаем.
Оценка сложности этого способа очевидна— O(nlgn). Ясно также, что он не всегда дает наилучшую укладку. Пусть, например, рюкзак вмещает вес не больше Т=20. При Wj = 15, w2= 10, w3 = 10, С!=45, с2=25, с3 = 24 получим ^ = 3, г2 = 2,5, г3=2,4. Выбрав первый предмет с максимальным г, получим стоимость 45, но добавить в рюкзак уже ничего нельзя. Вместе с тем, пропустив первый предмет, можно получить сумарную стоимость 49.
Н Предположим, что с увеличением веса предметов уменьшается их стоимость. Докажите, что при этом условии жадный алгоритм позволяет решить задачу.
12.3.2.	Распределение заданий
Вернемся к упражнению 11.13 (см. также указания к его решению).
Задача 12.4. Три станка обрабатывают детали с одинаковой скоростью. Из- I вестны длительности обработки п деталей; порядок обработки неважен. Обра- | ботка детали не прерывается. Станок переключается на обработку следующей | детали мгновенно. Нужно так распределить детали между станками, чтобы об- I работка последней из них закончилась как можно раньше.	•	|
Попробуем решить эту задачу с помощью жадного алгоритма. Пусть распределены все детали, кроме последней, и (S3,S2,S3) — моменты окончания работы станков. Тогда последнюю деталь лучше всего обработать на станке, который освободится раньше других, прибавив длительность ее обработки к наименьшему из чисел 5,, S2, S3.
Распространим это правило на все детали. Отсортируем детали по убыванию длительности их обработки и будем распределять их по правилу передать очередную деталь на наименее загруженный станок.
S[l] := 0; S [2] := 0; S[3] := 0;
for i := 1 to n do begin
вычислить k - номер одного из минимальных S[l], S[2], S[3];
прибавить T[iJ к S[k]
end
По этому алгоритму детали с длительностью обработки 12, 8, 7, 5, 4 распределятся по станкам так: 12, 8+4, 7+5. Очевидно, лучше не может быть. Однако, если дли
1 В жизни, кстати, самые ценные предметы (документы, драгоценности, деньги), как правило, имеют небольшой вес.
344
ГЛАВА 12
тельности 12, 8, 7, 5,4, 2, то алгоритм даст распределение 12+2, 8+4, 7+5 с моментом окончания 14, хотя распределение 12, 8+5,7+4+2 имеет момент окончания 13.
Итак, с помощью жадного алгоритма можно очень быстро решить задачу, тре бующую перебора, но это решение может быть не наилучшим.
Упражнения
12.1.	Даны отрезки, длины которых — натуральные числа. Нужно выбрать три от резка, образующих треугольник с максимальной площадью.
Вход. Первое число текста — количество отрезков п (п< 103), затем п натуральных чисел (длин отрезков), каждое не больше 103.
Выход. Площадь и длины сторон треугольника (действительное число с тремя десятичными цифрами в дробной части и три длины отрезков) или О если образовать треугольник нельзя.
Примеры. Вход: 4 1 2 3 5; выход: 0.
Вход: 4 3 5 3 4; выход: 6 . ООО 3 4 5.
12.2.	Денежная система предоставляет монеты, номиналы которых — степени целого числар>2: 1 =рй, р\ ...,р", где п> 1. Ясно, что любую сумму можно выдать этими монетами, если иметь их в неограниченном запасе. Жадный алгоритм выдачи монет состоит в том, чтобы выдать как можно больше монет с наибольшим номиналом, для оставшейся суммы использовать максимум монет со следующим номиналом и т.д. Докажите, что жадный алгоритм позволяет минимизировать общее количество монет2.
12.3.	Бычкам дают пищевые добавки, чтобы ускорить их рост. Каждая добавка содержит некоторые из N действующих веществ. Соотношения количеств веществ в добавках могут отличаться. Воздействие добавки определяете^ как CjO]+с2а2+..,+с^^ где а( — количество i-ro вещества в добавке, с. — неизвестный коэффициент, связанный с веществом и не зависящий от добавки.
Чтобы найти неизвестные коэффициенты ср Биолог может измерить воздействие любой добавки, использовав один ее мешок. Известна цена мешка каждой из М (М>М) различных добавок. Помогите Биологу подобрать самый дешевый набор добавок, позволяющий найти коэффициенты сг Возможно, соотношения веществ в добавках таковы, что определить коэффициенты невозможно.
Вход. В первой строке текста — целые числа М и N. В каждой из следующих М строк записаны N чисел, задающих соотношение количеств веществ в ней, а за ними — цена мешка добавки. Порядок веществ во всех описаниях добавок один и тот же. Все числа — неотрицательные целые не больше 50.
Выход. -1, если определить коэффициенты невозможно, иначе набор добавок (их номеров по порядку во входных данных). Если вариантов несколько, вывести какой-либо из них.
2 Жадный алгоритм не всегда минимизирует количество монет (подробности — в подразделе 13.3.2).
ЖАДНЫЕ АЛГОРИТМЫ
345
Пример
Вход	Выход Вход Выход
33	-1	32	13
1 0 2 3	2 1 2
1 0 2 4	1 2 9
2 0 12	12 3
12.4.	Фирма занимается мелкотиражным изготовлением полиграфической продукции. Перед праздниками набралось столько заказов, что пришлось прекратить прием новых и задуматься, как поскорее разобраться с уже принятыми. Для каждого заказа известны длительности его печати и доставки др клиента. Печать всех заказов происходит на одном и том же устройстве и поэтому выполняется последовательно, а доставку можно распараллелить, наняв любое нужное количество курьеров. Как организовать печать и доставку, чтобы момент, когда все клиенты получили свои заказы, наступил как можно раньше?
12.5.	Цех выполняет задания по очереди в течение равных, промежутков времени (например, по одному дню), не прерывая начатого задания. Для каждого из п заданий известен срок исполнения dp отсчитываемый от нулевого дня, и штраф рр который цех должен заплатить, если не выполнит задание в срок (неважно, на сколько дней он опоздает). Нужно найти порядок выполнения заданий с минимальным штрафом.
Вход. В первой строке текста записано количество заданий (2< и<103), в следующих п строках по два числа J и р;, 0< dp р.< 103. Номера заданий явно не указаны; они определяются номерами строк от 1 до п.
Выход. В первой строке штраф, во второй — через пробел номера заданий.
Примеры
Вход	Выход	Вход	Выход
3	0	3	10
2 10	2 13	1 10	2 3 1
1 50		1 50-	
3 20		2 20	
12.6.	Автомобилист выезжает с полным баком топлива из Ростова-на-Дону в Петропавловск-Камчатский, имея при себе план маршрута, на котором обозначены все заправки и расстояния между ними. Известно расстояние D, которое может проехать автомобиль с полным баком. Найти заправки, на которых нужно заполнять бак, чтобы количество остановок на заправках было как можно меньше. Учесть, что расстояние между соседними заправками может оказаться больше D.
12.7.	Группа студентов хочет пройти на выставку, имея только два приглашения. Они могут сделать это, если будут заходить по двое и один из вошедших тут же будет выходить, вынося оба приглашения. Для каждого студента известна длительность его прохода на выставку. Длительность выхода равна длительности входа, а длительность прохода пары студентов определяется по менее расторопному из них. Определить, как всей группе проникнуть на выставку за минимальное время. Если вариантов несколько, найти какой-либо из них.
Вход. В первой строке текста задано натуральное число N (2<7V<103).
346
ГЛАВА 12
В следующих N строках по одному в строке записаны натуральные числа гр tv длительности прохода студентов на выставку (в секундах), 1 <t< 104 По порядку записи этих чисел определяются номера студентов.
Выход. В первой строке текста — время (в секундах), через которое все студенты окажутся на выставке. В следующих строках — тройки целых чисел, из которых первые два — номера студентов, проходящих на выставку, третье — номер студента, выносящего два приглашения. В последней строке — два номера студентов, проходящих последними.
12.8.	Задан упорядоченный по невозрастанию список весов wp wv ..., wn предметов, которые нужно распределить по ящикам, способным выдерживать вес V. Объем и форма предметов не имеют значения; V>wr Нужно определить наименьшее количество ящиков, необходимых для распределения всех предметов. Используйте жадные алгоритмы, Приведите примеры весов предметов и “грузоподъемности” ящиков, при которых жадные алгоритмы дают наименьшие количества ящиков (и, наоборот, не дают).
Глава 13
Динамическое программирование
В этой главе...
*	Применение метода динамического программирования в задачах
♦	Каким условиям должны удовлетворять задачи, решаемые методом динамического программирования
♦	Техника заполнения таблиц для получения оптимального решения * Применение рекурсии с запоминанием
13.1. Принцип оптимальности
13.1.1. Путь по клеткам с максимальной суммой
Задача 13.1. За долгую и верную службу Рыцарю позволено набрать сокровищ в сокровищнице своего сеньора. Сокровищница имеет форму прямоугольника, состоящего из отдельных “клеток” — прямоугольных комнат. В каждой комнате хранятся сокровища известной стоимости. Рыцарь может вынести сколько угодно сокровищ, но пройдя через сокровищницу только один раз. Он может начать с любой комнаты вдоль внешней северной стейы сокровищницы (выбор комнаты — за рыцарем). На каждом шаге он может переходить в одну из трех “южно-соседних” комнат: южную, юго-восточную или юго-западную. Из комнат, граничащих с восточной или западной внешней стеной, возможны только два направления выхода. Закончить путь Рыцарь должен в любой из комнат на южной внешней стороне сокровищницы.
У Рыцаря есть план сокровищницы — прямоугольная таблица, в которой обозначены стоимости сокровищ каждой комнаты. Направлению с севера на юг соответствует направление сверху вниз на карте.
По заданной карте нужно найти один из допустимых путей, обеспечивающих наибольшую возможную сумму сокровищ.
Вход. Первая строка в тексте treasury.dat содержит два числа N и М, обозначающие “ширину” и “высоту”, далее М строк по N неотрицательных целых чисел в каждой — стоимости сокровищ соответствующих комнат. Размеры сокровищницы не более чем 80x80 комнат.
348
ГЛАВА 13
Выход. Оптимальный путь в тексте treasury. sol. В первой строке указывается номер (по порядку с запада на восток) комнаты северного ряда, из которой нужно начать движение, во второй — строка символов, означающих направление очередного перехода (S — на юг, Е — на юго-восток, W — на юго-запад); в третьей — полученная максимально возможная суммарная стоимость. Если есть несколько путей с максимальной суммой, вывести любой из них.
Пример
Вход 5 4
О 12 10
0 20 10
7	5	2
Выход 2
0 5	SWE
5 2	49
3	0
9 10 10 2	0
Анализ задачи. Можно найти оптимальный путь, перебрав все возможные пути, но их неимоверно много. Оценим их количество. Если отбросить элементарные случаи М= 1 или N= 1, то из каждой клетки каждого ряда, кроме нижнего, есть два или три выхода; следовательно, для каждой из N начальных клеток путей явно больше, чем 2*^'. Попробуйте оценить, сколько часов понадобится современному компьютеру для перебора такого количества вариантов при размерах сокровищницы 50x50 (и сколько миллиардов лет при размерах 80x80).
Приведенный в условии пример подталкивает к (ошибочному'.) предположению: изо всех чисел верхней строки выбрать максимальное, а затем каждый раз выбирать ту из трех (или двух) соседних снизу клеток, где записано наибольшее число, и в результате будет найден наилучший путь. Но, действуя таким образом, мы даже не узнаем, какие числа записаны в “дальних” от выбранного пути столбцах. Однако в клетках тех столбцов могли оказаться миллионы, а мы туда вообще не пошли.
4
* Если некоторая недоказанная идея позволяет получить правильный результат для примера, приведенного в условии задачи, она не обязательно даст правильный результат для всех входных данных.
Итак, крайности — полный перебор и выбор варианта, максимального из возможных на каждом шаге, — в данной задаче не годятся. Рассмотрим правильный и эффективный алгоритм.
Прочитаем входные данные в двумерный массив (таблицу данных) и заведем еще одну таблицу аналогичных размеров — таблицу оценок. Первую строку таблицы данных просто копируем в таблицу оценок; значение клеток каждой следующей строки таблицы оценок строим так: берем максимум из трех верхних соседних клеток (для крайних клеток из двух верхних соседних) и прибавляем значение соответствующей клетки таблицы данных. Так делаем для всех строк до последней включительно (рис. 13.1).
0	12	10 0 5	о	12	10	0	5
0	20	10 5 2	12	32	22	15	7
?	230	39	37	34	25	15
9 10 10 2 0
48 49 47 36 25
Рис. 13.1. Таблица данных и таблица оценок для примера из условия; в таблице оценок выделен наилучший путь
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
349
После этого в каждой клетке таблицы оценок находится сумма, которую можно набрать, пройдя наилучший допустимый путь (или один из нескольких равноценных наилучших путей) до данной клетки. Индукцией по номеру строки докажем, что это действительно так.
► База индукции. Прцйти в клетку первой строки можно лишь одним способом — начать путь из нее. Наилучшая сумма, которую можно при этом собрать, — это просто число из данной клетки. Поэтому, скопировав первую строку, получим правильные оценки для всех ее клеток.
Шаг индукции. Уже построены правильные оценки для (m-1 )-й строки, и строятся оценки для m-й (2<т<М) согласно описанному правилу. Предположим противное: существует клетка (m,j) и “сверхоптимальный” путь в нее, сумма которого больше оценки, построенной описанным способом. Стоимость клетки (m,j) обозначим у , оценку ее суммы — ет], а сумму “сверхоптимального” пути —j. Предположим, что s>e^ Прийти в любую клетку любой (непервой) строки можно одним из трех способов — сверху, слева сверху или справа сверху (на краях таблицы — одним из двух). “Сверхоптимальный” путь тоже проходит на предпоследнем шаге через одну из этих трех (двух) клеток. Рассмотрим “сверхоптимальный” путь без последнего перемещения. Он заканчивается в одной из клеток (m-l,j-l), (m-1,j) или (m-1,j+1). Обозначим ее (m-1,j'). На этом пути в клетку (m-1 ,j') набирается суммаs-v^ По предположению индукции, все оценки (т-1)-й строки построены правильно, т.е.
максимально возможная сумма, поэтому
em_Vj'>s-vmj.	(130
Но оценка строилась как
=	1,J-1»	^m-l,j+l} + Vmp
поэтому
emj>em.Aj' + vm}.	(13.2)
Из неравенств (13.1) и (13.2) следует emy>em_1/+vmj>s, что противоречит предположению s > ет. Противоречие доказывает шаг индукции и все утверждение. 1
Доказательство нехорошо тем, что использует индукцию и метод от противного, но анализ именно его, надеемся, облегчит восприятие следующего утверждения.
• Утверждение. Приведенный способ построения оценок правилен, поскольку оптимальный путь содержит в себе только оптимальные подпути.
Решение задачи. Построив оценки для всех клеток, искомую сумму получить легко: достаточно просмотреть клетки последней строки таблицы оценок и выбрать наибольшее значение. Однако нам, кроме суммы, нужен путь.
Определяя по последней строке таблицы оценок максимальную возможную сумму, мы автоматически получаем клетку, в которой завершается оптимальный путь (если таких клеток несколько, берем любую из них). При построении ее оценки выбиралась максимальная из оценок верхних соседних клеток. Следовательно, предыдущей на оптимальном пути является верхняя соседняя клетка с максимальной оценкой. Умея восстанавливать предыдущую клетку, можно найти весь путь: начав из известной последней клетки, двигаемся назад, пока не дойдем до клетки в первой строке (обратныйход — см. также задачу 8.6).
350
ГЛАВА 13
Для обратного хода заведем еще одну таблицу (таблицу выборов) и в каждой ее клетке будем хранить последний оптимальный выбор. В данной задаче выбор можн указать в клетке одним из трех чисел -1, 0 или 1, обозначающим, какая из верхних соседних клеток дала текущую оценку.
Итак, нам нужны три таблицы; размер каждой из них аналогичен размеру входных данных. Однако, если размер входа велик, каждая лишняя таблица может оказаться критической. Выясним, какие из таблиц действительно необходимы. В решении нужно вычислить оценки и восстановить путь. Если программу построить так, чтобы оценки вычислялись по мере чтения входного файла, то таблица данных вообще не нужна — стоимость клетки используется только при вычислении ее оценки Предшествующие значения оценок нужны, но только в пределах предыдущей строки. Поэтому для вычисления оценок нужны лишь текущая и предыдущая строки, а не вся таблица. Для восстановления пути нужно хранить или всю таблицу оценок, или последнюю строку таблицы оценок и всю таблицу выборов. Итак, достаточно одной двумерной таблицы.
Приведенные рассуждения реализованы в листинге 13.1.
Листинг 13.1. Программа решения задачи о сокровищнице, заполняющая текущую строку таблицы оценок и таблицу выборов
{$В-} { требуется короткое вычисление булевых операций } const МАХХ = 80; МАХУ = 80;
dir_labels : array [-1..1] of char = (' W,'S ', ' E');
{ значки для выдачи строки перемещений }
var XSz, YSz, { размеры }
i,	j, { номера текущих строки и столбца }
m { х-координата клетки с максимальной оценкой в текущей строке }
: byte;
est : array [1..2, 1..МАХХ] of longint;
{ предыдущая и текущая строки таблицы оценок }
choice : array [2..МАХУ, 1..МАХХ] of shortint;
{ таблица выборов }
path : array [2..МАХУ] of char; { восстанавливаемый путь } fv : text;
v, e : longint;
BEGIN
assign(fv, 1treasury.dat'); reset(fv);
readln(fv, XSz, YSz);
{ первая строка данных становится первой строкой таблицы оценок }
for j := 1 to XSz do read(fv, estfl, j]);
{ остальные строки обрабатываются с учетом предыдущих }
for i := 2 to YSz do begin	ь
{ обработка очередной строки входных данных } readln(fv);
for j := 1 to XSz do begin readffv, v);
e := -1;
{ Все клетки неотрицательны, поэтому -1 меньше любой оценки } If (j > 1) and (est[l, j-1] > e) then begin
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
351
е := est[l, j-1]; choice[i, j]:= -1
end;
if est[l, j] > e then begin
e := est[l, j]; choice[i, j] := 0
end;
if (j < XS^) and (est[l, j+1] > e) then begin e := est[l, j+1]; choice[i, j] := 1
end;
est [2, j] := v+e
end;
{ текущая строка станет предыдущей на следующем шаге } for j := 1 to XSz do est[l, j] := est [2, j];
end;
{ обратный ход }
close(fv);
e : = -1 ;
{ m - это х-координата клетки в текущей строке с максимальной оценкой }
{ в последней строке ищется клетка с наибольшей оценкой }
for j := 1 to XSz do
if est[l, j] > e then begin
e := est [1, j]; m := j
end;
{ восстановление пути на основе таблицы выборов }
for i := YSz downto 2 do begin
path[i] := dir_labels[-choice[i, m] ] ;
m := m+choice[i, m];
end;
assign(fv, 1 treasury.sol'); rewrite(fv);
writein(fv, m);
for i ;= 2 to YSz do write(fv, path[i]);
writeln(fv); writeln(fv, e); close(fv); END.
Анализ решения. Ввод данных имеет оценку сложности 0(Л/А). При построении оценки каждой клетки просматриваются не больше трех клеток таблицы оценок, поэтому построение всей таблицы оценок также имеет оценку 0(М-N). Количество действий обратного хода пропорционально N+M (нужно просмотреть последнюю строку таблицы оценок длиной N и воспроизвести М-1 шагов, каждый из которых требует константы действий). Итак, количество действий пропорционально М-N. Правильного решения с существенно меньшим количеством действий быть не может, поскольку один лишь ввод данных имеет оценку 0(M 7V).
13.1.2.	Общие замечания по методологии динамического программирования
Надеемся, что читатель увидел, как эффективно решить задачу о сокровищнице. Однако пока неясно, как мы догадались действовать именно таким методом и почему таблица оценок позволяет эффективно получить правильное решение.
Догадались потому, что нам известен этот метод и условия его применимости. Обычно он применим к задачам, в решении которых возникают различные допус
352
ГЛАВА 1
тимые варианты и из них нужно выбрать оптимальный (в разобранной задаче и всех путей, отклоняющиеся от вертикали на каждом шаге не более чем на 1, нуже путь с наибольшей суммой). Этот метод имеет также существенные общие черты ос способами решения некоторых комбинаторных задач (например, задачи 10.1, 1 _ и особенно 10.6).
Метод решения, описанный выше или ему подобный, скорее всего, будет эффек тивным1, если задача удовлетворяет всем следующим условиям.
1.	В задаче можно разумно выделить подзадачи аналогичной структуры меньшего размера. В задаче о сокровищнице подзадачами были “Найти наибольшие возможные суммы, которые можно собрать, дойдя до клеток т-й строки. (1< т<М)”. Иногда бывает, что исходная задача не является одной из зада серии, но ее можно легко решить, опираясь на решения одной или нескольких задач серии. Именно так было в задаче о сокровищнице: исходная задача легко получается из решения подзадачи при т=М.
2.	Среди выделенных подзадач есть тривиальные, т.е. имеющие “малый размер и очевидное решение. Тривиальная подзадача в задаче о сокровищнице — . прит=1.
3.	Оптимальное решение подзадачи большего размера можно построить из оптимальных решений подзадач (классическая формулировка: у оптимально решенной задачи все подзадачи решены оптимально). Именно это доказывал ос в шаге индукции.
4.	При решении различных подзадач приходится многократно решать одни и те же (нетривиальные) подзадачи меньшего размера (подзадачи перекрываются Действительно, только при этих условиях есть смысл запоминать какие-либо промежуточные результаты. В задаче о сокровищнице результату подзадачи “С какой максимально возможной суммой можно прийти в клетку (m-^lj)” нужен трижды (возможно, дважды)1 2: при решении подзадач для клеток (m,j-V) (m,j) и (m,j+l).
5.	Таблицы для запоминания ответов подзадач имеют разумные, не слишком большие, размеры.
Описывая условия применимости метода, часто приводят такой признак: различные подзадачи меньшего размера, из которых состоит подзадача большего размера, независимы одна от другой. Этот признак можно считать объяснением к условию 3, но они не равноценны, поскольку независимость подзадач не обеспечивает, что из решений этих подзадач можно построить решение задачи.
Условия 1-3 выражают принципиальную возможность правильного применения метода к задаче, 4—5 — его целесообразность. Если условия 1—3 выполняются, а 4 — нет, то, вероятно, задачу можно успешно решить рекурсивно.
Если же выполняются условия 1—4, но не 5, то или нужно выделить другую серию подзадач, или эффективное решение не имеет ничего общего с разбиением на подзадачи, или задача вообще не имеет эффективного решения.
1 Вопрос, будет ли он самым эффективным, каждый раз нужно исследовать отдельно.
2 О том, почему запоминание результата сокращает общее количество действий существенно, а не втрое, см. подраздел 3.1.3.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
353
Описанный метод решения задач называют динамическим программированием, применением принципа оптимальности (принципа Веллмана) или табличной техникой динамического программирования. Под словом “программирование” здесь не имеется в виду процесс составления компьютерных программ. “Динамическое программирование” — короткое и общепринятое название этого метода, хотя сам Р. Веллман развивал теорию динамического программирования применительно к оптимальному управлению динамическими системами, описанными с помощью дифференциальных уравнений в частных производных.
Наиболее ясно и строго, по мнению авторов, динамическое программирование изложено в [21,22] (см. также [3, 35,43]).
Для решения ряда задач, удовлетворяющих условиям динамического программирования, технически удобнее применить модификацию метода — рекурсию с запоминанием (memoized recursion). Суть ее в следующем: решая подзадачу, начинать с проверки, не была ли она решена раньше; если не была, решить рекурсивно и запомнить результат в таблице, а если была, взять ответ из таблицы. Примеры таких задач представлены в разделе 13.3 и в упражнениях.
13.1.3.	Количество путей с суммой, близкой к максимальной
Задача 13.2. Прямоугольная таблица имеет М строк и N столбцов. В каждой ее клетке записано натуральное число не больше 200. Нужно пройти из левого верхнего угла таблицы в правый нижний, на каждом шаге перемещаясь на 1 клетку вправо или вниз. Очевидно, таких путей много, и для каждого можно найти сумму чисел в пройденных клетках. Ясно, что среди этих сумм есть максимальная.
“Хорошими” считаются не только пути с максимально возможной суммой, но и пути, сумма которых отличается от максимальной не более чем на К. Количество “хороших” путей гарантированно не больше 10’.
Найти максимально возможную сумму и количество “хороших” путей.
Вход. Первая строка входного текста содержит три целых числа М (2<М<200), N (2<7V<200) и К (0<К<200). Каждая из следующих М строк задает N чисел в соответствующих клетках.
Выход. Первая строка текста должна содержать максимально возможную сумму, вторая — количество маршрутов, сумма чисел которых отличается от максимальной не более чем на К.
Пример
Вход 2 3 3 Выход 2 0
19 7	2
2 5 3
Анализ задачи. Вначале рассмотрим, как можно вычислить максимальные суммы, которые можно набрать на путях в клетки, и количества путей, на которых эти суммы набираются. Прочитаем вход в таблицу данных v, заведем таблицу оценок сумм е и таблицу количеств путей nw. В каждую клетку первого столбца и первой строки ведет один путь, поэтому все элементы в первой строке и первом столбце таблицы nw равны 1, а в таблице е вычисляются так:
354
ГЛАВА 13
е[1,11 := v[l,l] ;
е[1, jJ := е[1,j-1]+v[1,j] при 2 < j < N;
e[i,l] := e [i-1,1]+v[i, 1] при 2 < i < M.
Остальные элементы таблицы вычисляются построчно в соответствии с формулой et= max{eMy,	Максимальную сумму можно набрать на разных путях,
если e^j и е f при вычислении ev равны. В этой ситуации количество путей, приводящих в клетку (i,j) с оценкой е/у, равно сумме количеств аналогичных путей, приводящих в клетки (г-1,у) и (i,j—1). Таким образом, оценки сумм и количества путей пр i> 1 ,j> 1 можно вычислить так:
if е [i-1,j] > е [i-1,j1
then e [i,j] := e [i-1,j] + v [ i, j ] else	e[i,j] := e[i,j-l] + v[i,j];
if e [i-1,j] > ё [i-1,j]
then nw[i,j] := nw[i-l,j] else if e [i-1,j] < e[i,j-1]
then nw[i,j] := nw[i,j-l]
else nw[i,j] := nw[i-l,j] + nw[i,j-l].
Ответ — это значения e [M, N] и nw [M,N].
Перейдем к вычислению количества “хороших” путей, на которых отставание суммы пути от максимально возможной не превышает заданного К. Очевидно, что для всех клеток первой строки и первого столбца количество “хороших” путей равно 1. Рассмотрим клетку (i,j) при i> l,j> 1:
•	при e[i-l J] > e[i,J-l]+K все “хорошие” пути в клетку (i,j) проходят через (z-1 ,j);
•	при e[i-l,J] < е[г,у-1]-К — через (г,у-1);
•	при |e[i-l,j]- e[i,j- 1]|<АГ среди “хороших” путей есть пути и <?ерез и через (ij-1).
Можно сделать вывод, будто количество “хороших” путей для клетки (i,j) равно их количеству или для клетки (г-1,у), или для (ij-1), или сумме этих чисел. Однако последняя “ветка” вывода неверна.
Пример. Пусть таблица данных имеет вид
13 7 5 3 4
и К= 3. Шаг по таблице вправо обозначим R, вниз — D. В клетку (1; 3) можно прийти одним “хорошим” путем (RR), в (2; 2) — двумя (DR с суммой 9 и RD с суммой 7). Но если продолжить эти пути в клетку (2; 3), то пути RRD (сумма 15) и DRR (сумма 13) окажутся “хорошими”, a RDR (сумма 11)— нет: отставание суммы этого пути от максимально возможной больше К.	*
Из таблицы данных ясно, что путь RD имеет отставание 2, а пути DR и RR — 0. У пути RDR на части RD было отставание 2, и к нему прибавилась неоптимальность перехода (2; 2)-(2; 3) с величиной е[1,3]- е[2,2]=2.
Итак, введем понятие задержки каждого перехода (R или D). Если переход только один (R в первой строке или D в первом столбце), задержка равна 0. При г>2, j>2 для вычисления задержек достаточно вычислить:
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
355
•	большую из предыдущих оценок тах_е := max{e[i-l,j], e[i,J-1]},
•	новую оценку e[i,J] := тах_е + v[i,j],
•	задержки переходов D и R: add_sh_U := max_e-e[i-l,J] и add_sh_L'.= тах_е-
Иными словами, если данный переход оптимален, задержка равна 0; иначе его оценка строго меньше оценки другого возможного перехода и задержка равна разнице оценок. Очевидно следующее утверждение.
Лемма 1 об отставании пути. Отставание любого пути равно сумме задержек всех его переходов.
Решение задачи. Рекурсия с запоминанием. Введем величину Т(i, j, d) — количество путей из клетки (1; 1) в клетку (i;j), отставание которых не больше d (при 05 d<K). При приходе в клетку сверху отставание путей увеличивается на add_sh_U, при приходе слева — на add_sh_L. Поэтому
T(i,j, d) = T(i-l,j, d-add_sh_U) + T(i,j-l,d-add_sh_R).
Естественно, полагаем T(i,j, d)=0 при d < 0 и T(i,j, d) = 1 при i = 1 или j = 1 и любом d>0.
Формулу вычисления T(i,j, d) можно реализовать рекурсивной функцией, но это решение слишком неэффективно (время работы будет приблизительно пропорционально произведению числа “хороших” путей на размеры поля). Если использовать рекурсию с запоминанием, т.е. запоминать ответы уже решенных подзадач и не решать их заново, решение будет очень эффективным по времени.
Однако этот алгоритм должен работать с большим /и/>ехмерным массивом, т.е. весьма неоптимален по памяти. Поэтому ниже представлен алгоритм, значительно более эффективный по памяти и не менее эффективный по времени работы.
Итеративный алгоритм. На основе леммы 1 об отставании пути нетрудно сформулировать лемму, дающую ключ к эффективному подсчету количества “хороших” путей.
Лемма 2 об отставании пути. Пусть для клеток (i-l,j) и (i,j-l) известны оценки е[-,-] и распределения количеств “хороших” путей: сколько имеют отставание 0, сколько — 1 и так далее до К включительно. Обозначим эти последовательности как NWay_U и NWay_L. Тогда, чтобы получить распределение количеств “хороших” путей для клетки (i,;), достаточно выполнить следующие действия:
•	вычислить величины add_sh_U и add_sh_L (см. правила перед леммой об отставании пути);
•	сдвинуть распределения соответственно на вычисленные задержки, т.е. для элементов NWay_U увеличить отставание на add_sh_U, элементов NV/ay_L — на add_sh_L‘,
• “объединить” сдвинутые NWay_U и NWay_L, сложив количества путей с одинаковыми задержками и отбросив отставания, после увеличения ставшие строго больше К.
Доказательство очевидно из предыдущих рассуждений.
356
ГЛАВА 13
Итак, нам известны и распределения “хороших” путей для первой строки и первого столбца, и способ построения распределений при i>2, j>2. Значит, можно построить распределения для клеток второй строки (слева направо), затем третьей и т.д. При этом полные таблицы не нужны — достаточно двух соседних строк.
Наконец, вычислив распределение для клетки сложим количества путей для всех отставаний (не превышающих К).
Существенные улучшения итеративного алгоритма.
Сдвинутое слияние. Вначале рассмотрим структуры данных для хранения количеств путей и отставаний. Ситуация, когда в клетку ведут пути со всеми значениями отставаний от 0 до К, возможна. Но для значительной части клеток будет иначе: неравными 0 будут количества путей только для довольно небольшой части отставаний. Поэтому для очередной клетки целесообразно хранить количество значении отставаний, для которых есть “хорошие” пути.
type Ways = record
sh : byte; { отставание }
w : longint; { количество путей с этим отставанием } end;
WaysArr = array [0..MAXK] of Ways;
CellE = record
e : longint; оценка клетки }
num : byte; • количество различных отставаний }
nn : WaysArr; распределение отставаний } end;
Распределения NWayJU и NWay_L лучше хранить не в виде массивов, индексированных отставанием, а в виде указанной структуры. В CellE поле е хранит оценку клетки, num — количество значений отставаний, для которых есть “хорошие” пути, а массив пп (точнее, его элементы с 0-го по (пит-1)-й) — сами количества йутей; массив упорядочен по возрастанию sh. В примере, приведенном выше, для клетки (2;2): е = 9, num=2, nn [0] =(sh: 0, w: 1), nn [1] =(sh : 2, w : 1).
Слияние упорядоченных последовательностей в итеративном алгоритме имеет особенности:
•	во-первых, сливаются не сами значения входных последовательностей, а их “сдвиги” на add_sh_U и add_sh_L',
•	во-вторых, результирующая последовательность заканчивается не только тогда, когда заканчиваются обе входные последовательности, но и когда “сдвинутое” значение отставания больше К, т.е. пути перестают быть “хорошими”.
Отсечение “безнадежных” путей с помощью встречных оценок. По условию количество “хороших” путей гарантированно не больше 10’. При этом» количество всех путей из (1,1) в (M,N) составляет С^~^_2, что при Л/=А=200 будет порядка 10118. А это означает, что при больших М и N подавляющее большинство путей не “хорошие”.
Чтобы как можно быстрее избавляться от “безнадежных” путей, используем встречные оценки — наибольшие суммы, которые можно набрать на путях из клетки (i,j) в (М, N):
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
357
e2[M,j] = v[Af, j] + ... + v[M, TV],
e2[i, AQ = v[i, TV] + ... + v[M, TV], e2[z,j] = max {e2[i+l, j], e2[i,j+l]} + v[i,j] при
Рассмотрим величину eeefij] = e[i,j] + e2[i,j] - v[i,j]. Она задает максимальную сумму, которую можно найрать, проходя из (1,1) в (М,Ы) через Тогда eee[ij]< e[M,N], причем равенство достигается только для тех клеток, через которые проходит хотя бы один максимальный путь. Величину е[М, ?V] - eeefij] обозначим D[i,j].
Лемма о встречных оценках
1.	При Dfi,j] > К пути через (ij) можно не исследовать, поскольку среди них нет “хороших”.
2.	При £)[;,;] <К для клетки (/,» достаточно проводить “сдвинутое слияние” для значений отставания, не превышающих K-DfiJ],
► Наибольшая сумма, которую можно набрать, проходя через данную клетку, хуже вообще наилучшей наР[«,у], т.е. общая сумма задержек на “неоптимальных переходах” составит£)[<',у]. Значит, пути с текущим отставанием больше все равно будут отброшены. <
Утверждения леммы позволяют уменьшить объем работы при слияниях и сократить время работы (конечно, в разной степени для разных входных данных).
В следующем фрагменте программы распределение cres количеств путей текущей клетки (i,J) строится по известным распределениям CU и CL для клеток и (ij-l) с помощью “сдвинутого слияния” и оптимизации отсечений по встречным оценкам. Стоимость текущей клетки vv обозначена the_v, оценки суммы e[z,j], e2[ij], eeefij] и Dfi J] — соответственно the_e, the_e2, the_eee и D.
{ строим задержки и оценки }
if CU.e >= CL.e then begin
add_sh_U := 0;
add_sh_L := CU.e - CL.e;
cres.e := CU.e + the_v;
end else begin
add_sh_U := CL.e - CU.e; add_sh_L := 0;
cres.e := CL.e + the_v;
end;
the_eee := the_e + the_e2 - the_v;
D := e2[l,l] - the_eee;
if D > К then begin
cres.num := 0; exit { см. лемму о встречных оценках } end;
local_K := К - D; {см. лемму о встречных оценках }
i := 0; j := 0; i_new := 0;
while((i < CU.num) and (j < CL.num) and
{ пока ни одна из входных последовательностей не закончилась } (CU.nn[i].sh + add_sh_U <= local_K) and (CL.nn[j].sh + add_sh_L <= local_K))
{ и значения отставаний с учетом новых задержек }
{ не превысили "запаса" для "хороших" путей }
do begin
if CU.nn[i].sh + add_sh_U = CL.nntj],sh + add_sh_L
358
ГЛАВА 13
then begin
{значения отставаний с новыми задержками одинаковы, } количества путей складываются }
cres.nn[i_new].sh := CU.nn[i].sh + add_sh_U;
cres.nn[i_new].w := CU.nn[i].w + CL.nn[j].w;
inc(i); inc(j); inc(i_new);
end else
if CU.nn[i).sh + add_sh__U < CL.nn[j].sh + add_sh_L then begin
{ отставание плюс задержка верхней клетки }
{ меньше, чем левой, - записываем в результат текущий элемент CU } cres.nn[i_new].sh := CU.nn[i].sh + add_sh_U;
cres.nn[i_new].w := CU.nn[i].w;
inc(i); inc(i_new);
end
else begin
{ аналогично для левой }
cres.nn[i_new].sh := CL.nn[j].sh + add_sh_L;
. cres.nn[i_new].w := CL.nn[j].w;
inc(j); inc(i_new);
end;
end;
while((i < CU.num) and (CU.nn[i].sh + add_sh_U < local_K)) do begin { последовательность nn левой клетки закончилась, } {ay верхней клетки еще есть "хорошие" пути } cres.nn[i_new].sh := CU.nn[i].sh + add_sh_U;
cres.nn[i_new].w := CU.nn[i].w;
inc(i); inc(i_new);
end;	j
while((j < CL.num) and (CL.nn[j].sh + add_sh_L <= local_K)) do begin { аналогично }
cres.nn[i_new].sh := CL.nn[j].sh + add_sh_L;
cres.nn[i_new].w := CL.nn[j].w;
inc(j); inc(i_new);
end;
cres.num := i_new;
Оценки времени работы и объема памяти. Как уже упоминалось, время работы на разных входных данных одного и того же размера может сильно отличаться. Поэтому для самых быстрых алгоритмов (рекурсивного с запоминанием и итеративного, использующего “встречные оценки”) количество действий в худшем случае оценивается как O(M N K), но для многих входных данных оценка ненамного превышает O(MN).
Объем памяти рекурсии с запоминанием имеет оценку O(Af N K); для итеративных алгоритмов — Q(N(M+K)).
13.2. Монотонная подпоследовательность
13.2.1. Поиск монотонной подпоследовательности
Начнем с примера. Из последовательности <1,2,3,4> можно взять отдельные элементы и, не меняя их взаимного порядка, образовать из них новую последова
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
359
тельность, например <2>, или <1,3>, или <1,2,4>, или <1,2,3,4>. То, что при этом получается, называется подпоследовательностью исходной последовательности. Дадим формальное определение.
Последовательность Z=<z1,z2, ...,zt> называется подпоследовательностью последовательности Х=<х1,х2,...,х>, если существует строго возрастающая последовательность индексов <i,, i2,..., /*>, для которой z=x при всехj = 1,2,..., к.
Как видим, из исходной последовательности берутся любые элементы и не обязательно подряд.
Числовая последовательность монотонно не убывает, если каждый следующий элемент последовательности не меньше чем предыдущий.
Например, <1,2,4,4,7> является монотонно неубывающей, а <5,9,0> — нет.
Задача 13.3. Из заданной числовой последовательности выделить монотонно неубывающую подпоследовательность максимально возможной длины. Если таких несколько, то из них нужно выбрать ту, у которой наибольшая сумма чисел.
Вход. Первая строка текста sequence. dat содержит длину последовательности N (1 <jV<5000), вторая — N целых чисел (элементы последовательности, среди которых могут быть одинаковые). Значения элементов целые, от 0 до 50000.
Выход. В первой строке текстового файла sequence. sol должна быть выбранная подпоследовательность, во второй — сумма ее элементов.
Пример
Вход 9	Выход 5 6 8 9
531687249	28
Анализ задачи. Перебор всех подпоследовательностей практически невозможен: последовательность длиной N имеет 2* различных подпоследовательностей. Значительную часть из них можно отбросить как немонотонные, но возможность такой оптимизации зависит от вида последовательности и будет улучшением в среднем, а не для наихудшего случая.
Пример из условия может привести к гипотезе: начать с первого элемента и включать по порядку все, что можно включить, не нарушая монотонности. Но этот алгоритм не всегда дает правильный результат (пример вновь подобран коварно). Действительно, рассмотрим вход, где третий элемент — 4 вместо 1. Ответом будет 3 4 6 8 9 (длина 5, сумма 30), а значение 5 не войдет в результат. Как видим, на решение, включать или не включать в ответ первый элемент, влияет значение третьего элемента.
Решение задачи. Разобьем задачу на подзадачи. Может прийти в голову поставить серию подзадач: Какую наилучшую (согласно условию) монотонно неубывающую подпоследовательность можно выделить из последовательности <а{, (т начальных элементов данной последовательности)? Тогда целевая задача была бы просто по
360
ГЛАВА 13
следней из подзадач. Но если подзадачи формулировать так, то непонятно, как из решений меньших подзадач правильно собирать решения больших.
Поставим подзадачи иначе: Какую наилучшую (согласно условию) монотонно неубывающую подпоследовательность, начинающуюся элементом ат, можно выделить из последовательности <ат, ...,а^>?Наилучшую подпоследовательность подзадачи для т будем называть т-наилучшей3 4.
Исходная задача не является одной из подзадач, поэтому убедимся, что, решив все подзадачи для т от N до 1, мы сможем получить решение нужной.
Пусть первым элементом искомой наилучшей подпоследовательности является некоторый элемент аг Отметим следующее свойство: если подпоследовательность наилучшая среди всех возможных, то она наилучшая и среди тех из них, которые начинаются элементом аг Таким образом, искомая наилучшая подпоследовательность является решением подзадачи для некоторого i. Поэтому, выбрав наилучшее из оптимальных решений всех подзадач от тп=1 до m=№, получим наилучшее решение задачи.
Покажем, как из решений меньших подзадач собрать решение большей. Пусть есть N-, N-1-, ..., (т+1)-наилучшие подпоследовательности и нужно найти т-наилучшую. Одноэлементная последовательность <ап> может быть т-наилучшей лишь при условии, что в <аи+1, ...,«^> все элементы были меньше ат. Если же m-наилучшая подпоследовательность содержит не только ап, то ат предшествует некоторому элементу ak (т< k<N, ат<ак), причем т-наилучшая последовательность без ат в начале равноценна к-наилучшей*. Докажем это.
► Урезанная в начале m-наилучшая является одной из монотонных подпоследовательностей, начинающихся с ак, и поэтому она не лучше, чем fc-наилучшая. С другой стороны, она не может быть строго хуже: предположив это, получим, что добавление ат в начало it-наилучшей дает монотонную подпоследовательность, начинающуюся на ат, строго лучшею, чем т-наилучшая. <
Сравните приведенное доказательство с доказательством в задаче 13.1. Они одинаковы в своей цели — показать, что задача удовлетворяет условие 3 из подраздела 13.1.2.
Итак, для нахождения оптимального решения подзадачи для т можно перебрать те подзадачи меньшего размера, у которых первый элемент ак удовлетворяет неравенства т< k<N и ат<ак, выбрать наилучшее из их решений и дописать перед ним ат.
Выясним, что и как запоминать. Нужно многократно выбирать наилучшее из решений меньших подзадач (среди тех, у которых am<at). Согласно критериям в условии задачи, подпоследовательность лучше, если она длиннее или имеет то же количество элементов и большую сумму. Для такого сравнения удобнее работать не с самими подпоследовательностями, а с их длинами и суммами. Эти характеристики легко поддерживать: если к подпоследовательности добавляетйя элемент, ее длина
3 Выделение подзадач именно в таком порядке (малые — в конце последовательности, большие “растут” в сторону ее начала) выгодно, главным образом, для более удобной организации обратного хода. Подзадачи вполне можно поставить и о наилучшей подпоследовательности, заканчивающейся ат.
4 В действительности совпадает, но, чтобы это утверждать, нужно доказать, что в данной задаче не может быть различных наилучших решений одной подзадачи.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
361
увеличивается на1, а сумма— на величину элемента. Итак, нужны массивы длиной N, в которых хранятся: значения элементов ар длины i-наилучших подпоследовательностей и их суммы.
Наилучшая подпоследовательность должна быть выведена, поэтому нужен обратный ход. Для обратного хода используем “таблицу выборов” — одномерный массив; значением его т-то элемента является то к, для которого ак был вторым элементом в т-наилучшей подпоследовательности.
Приведенное решение реализовано в листинге 13.2.
Листинг 13.2. Решение задачи о монотонной подпоследовательности
{ Подзадачи строятся с конца, чтобы облегчить обратный ход }
const	MAX_N = 5000;
var a	: array [l..max_N] of word;
	{ значения элементов последовательности
len	: array [l..max_N] of word;
	{ длины наилучших подпоследовательностей
sum	: array [l..max_N] of longint;
	{ суммы наилучших подпоследовательностей
next	: array [l..max_N] of word;
	{ "таблица выборов": следующие элементы :
	наилучших подпоследовательностях }
fv : text;
N, m, k : word;
curr_len : word; curr_sum : longint; curr : word;
BEGIN
{ ввод данных и инициализация массивов } assign(fv, 'sequence.dat1); reset(fv); readln(fv, N);
for m := 1 to N do read(fv, a[m]);
close(fv);
for m := 1 to N do begin
next[m] := 0; len[m] := 0; sum[m] := 0;
end;
{ Решение подзадач поиска m-наилучших подпоследовательностей } for m := N downto 1 do begin
curr_len := 0; curr_sum := 0;
Ищется начало k-наилучшего решения для к от m+1 до N, } т.е. одной из подзадач меньшего размера, } и запоминается в next[m]. В len[m] и sum[m] } запоминаются длина и сумма т-наилучшего решения }
for k := m+1 to N do if а[к] >= а[т] then .if (len[к] > curr_len) or (len[к] = curr_len) and (sum[k] > curr_sum) then begin
curr_len := len[kJ; curr_sum := sum[k];
next[m] := к;
end;
lenfm] := curr_len+l; sum[m] := curr_sum+a[m];
end;
362
ГЛАВА 13
{ Поиск начала к наилучшей подпоследовательности } curr_len := 0; curr_sum := 0;
for к := 1 to N do
if (len[k] > curr_len) or
(len[k] = curr_len) and (sum[k] > curr_sum)
then begin
curr_len := len[k]; curr_sum := sum[k];
curr := к;
end;
assign(fv, 1 sequence.sol1); rewrite(fv);
к := curr;
{ Вывод наилучшей подпоследовательности }
while к о 0 do begin
write(fv, a[к], 1 '); к := next[к];
end;
writein(fv); writein(fv, sum[curr]); close(fv);
END.
Содержимое таблиц для примера из условия задачи показано на рис. 13.2.
i	1	2	3	4	5	6	7	8	9
«,	5	3	1	6	8	7	2	4	9
1еп[	4	4	4	3	2	2	3	2	1
sumt	28	26	24	23	17	16	15	13	9
next.	4	4	4	5	9	9	8	9	0
Рис. 13.2. Содержимое таблиц на примере из условия
Анализ решения. Оценим общее количество действий и объем памяти. При решении каждой подзадачи просматриваются все результаты уже решенных подзадач, т.е. всего не более чем У,^!(1У -т-Г) = (N-l)N/2 раз, что требует 0(1/) действий. Используются четыре массива размером W каждый, т.е. объем памяти пропорционален N. Отметим, что в явном виде решения всех подзадач не сохранялись (для их явного хранения был бы нужен объем памяти 0(1/)),
♦ Алгоритмы динамического программирования, приведенные в задачах 13.1 и 13.3, используют объем памяти, пропорциональный размеру входных данных. Однако в большинстве алгоритмов динамического программирования требуется память, объем которой на порядок больше размера входных данных.
13.2.2. Бинарный поиск начала подпоследовательности
В действительности задачу о подпоследовательности можно решить с оценкой времени не 0(1/), а гораздо меньшей: O(N\ogN).
Решение. Будем решать задачу от последнего элемента последовательности aN к первому ар но, в отличие от подраздела 13.2.1, для каждого т будем искать лучшую
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
363
подпоследовательность среди всех монотонных подпоследовательностей последовательности ат, ...,aN (не обязательно начинающихся с ат).
Чтобы определить лучшую монотонную подпоследовательность, начинающуюся с ат, нужно знать, какими элементами начинаются подпоследовательности всех возможных длин, полученные при исследовании йя+|, ..., aN, Для этого достаточно двух массивов, f_val и f_idx, индексированных длинами подпоследовательностей: значения f_val[i]	— это значение и номер элемента, которым начинается лучшая
подпоследовательность длиной i. Пусть к — максимальная длина монотонных подпоследовательностей. Тогда
/_vaZ[l] >f_val[2] > .. .>f_val[k],	(13.3)
поскольку, если есть более длинная подпоследовательность, начинающаяся с некоторого значения, то гарантированно можно найти и более короткую, но не наоборот.
Пусть при исследовании ап обнаружено, что ат может предшествовать лучшей подпоследовательности St длиной i, но не может предшествовать лучшей подпоследовательности длиной Z+1. Это означает, что am<f_val[i] и a„>/_vaZ[i+l]. Тогда 5, с добавленным в начале ат по формуле (13.3) лучше, чем SM, а ее первый элемент будет следующим после ат в новой лучшей подпоследовательности длины i+1. Отразим этот факт, изменив f_val[i+l] на ат Hf_idx[i+1] нам. В частном случае, если i=k, то дополнительно увеличивается максимальная длина к.
Для запоминания элемента, следующего после ат в лучшей подпоследовательности, введем “таблицу выборов” — массив next. Если ат нельзя продолжить никакой подпоследовательностью, то nextfm]=0.
Источником ускорения работы является упорядоченность (13.3). Благодаря ей найти i, для которого f_val[i]> am>/_vaZ[i+l] (или 0, если nm>/_vaZ[l]), можно с помощью бинарного поиска за время O(log&). Остальные действия по обработке ат требуют константного времени. Поскольку к не больше размера подзадач N-m, суммарное количество действий по обработке всего массива а имеет оценку O(NlogN).
Описанное решение реализовано в листинге 13.3. Отметим, что из-за отсутствия массива sum обратный ход отличается от программы в листинге 13.2, хотя роль массивов next в обеих программах одинакова.
Листинг 13.3. Бинарный поиск в задаче о монотонной подпоследовательности
const MAX_N = 5000;
var а : array[1..max_N] of word;
{ исходная последовательность }
next : array[1..max_N] of word;
{ next[i] - номер элемента, следующего после a[i] в наилучшей подпоследовательности }
f_val : array[1..max_N] of word;
{ f_val[i] -- значение элемента, которым начинается лучшая подпоследовательность длины i }
f_idx : array[1.,max_N] of word;
{ f_idx[i] -- номер элемента, которым начинается лучшая подпоследовательность длины i }
sum : longint; { сумма значений в подпоследовательности }
364
ГЛАВА 13
fv : text;
N,	длина последовательности }
m,	номер исследуемого элемента }
к, текущая длина наилучшей подпоследовательности } left, right, mid : word;
BEGIN
{ ввод данных и инициализация массивов } assign(fv, 'sequence.dat'); reset(fv); readln(fv, N);
for m := 1 to N do read(fv, a [m] ) ;
close(fv);
к := 1; f_val[1] := a[N]; f_idx[l] := N;
{ Решение подзадач поиска m-наилучших подпоследовательностей } for m := N-1 downto 1 do begin
left := 0; right := k+1;
{ найти i, для которого f_val[i] > a[m] > f_val[i+1] }
while right-left > 1 do begin
mid := (left + right) div 2;
if a[m] > f_val[mid] then right := mid else left : = mid
end;
if left = k then begin
{ a[m] удлиняет подпоследовательность } inc(k);
f_val [k] : = a [m] ;
f_idx[k] := m;
next[m] := f_idx[k-l];
end
else begin
{ a[m] замещает начало подпоследовательности } f_val[left+1] := a [m] ;
f__idx [left+1] := m;
if left > 0
then next[m] ;= f_idx[left] else next[m] := 0;
end end;
{ Вывод наилучшей подпоследовательности } assign(fv, 'sequence.sol1); rewrite(fv); writein(fv, k);
sum := 0;
m := f_idx[k];
repeat.
write(fv, a[m], J '); sum := sum+a[m];
m := next[m];
until m = 0;
writeln(fv); writeln(fv, sum);
close(fv);
END.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
365
Задача о подпоследовательности предлагалась на студенческих соревнованиях по программированию, проводимых АСМ, в следующей остроумной формулировке (технические условия для краткости опущены).
На вооружении некоторого государства есть секретный противоракетный бульдозер. Когда радары замечают, что приближаются вражеские ракеты, этот бульдозер поднимают в воздух, где он может функционировать самостоятельно. Встретив на одной высоте любую ракету противника, он уничтожает ее, да так, что она бесследно исчезает. Однако бульдозер был сделан очень давно и немного износился. Он может парить на любой нужной высоте как угодно долго, может как угодно быстро снизиться и повиснуть на меньшей высоте, но самостоятельно подняться на большую высоту он уже не может. Буксировка бульдозера вверх требует большой подготовки, и это не позволяет поднимать его несколько раз.
Написать программу, которая будет руководить процессом падения бульдозера так, чтобы он сбил как можно больше ракет. На момент, когда бульдозер отбуксировали вверх, уже известно, когда и на каких высотах будут пролетать вражеские ракеты.
Логичен вопрос: зачем вообще нужен алгоритм поиска подпоследовательности с оценкой сложности №, если есть существенно более эффективный с оценкой NlogN! Основных причин две. Во-первых, идея алгоритма “NlogN” весьма специфична, тогда как идеи алгоритма типичны для динамического программирования. А во-вторых, есть задачи, которые можно решить весьма очевидным обобщением алгоритма но не ‘WlogA”. Пример представлен в следующем подразделе.
13.2.3.	Вложенные коробки
Задача 13.4. Даны N коробок в форме прямоугольных параллелепипедов размерами а^Ь^сг Некоторые из них, как правило, можно вложить в другие. Толщина стенок коробок пренебрежимо мала, но строго больше нуля. Коробки разрешается вращать, но только на углы, кратные 90° (иначе говоря, вкладывать коробки можно только “ровно”, а не “наискось”). Коробки вкладываются “как матрешки”, т.е. меньшая в среднюю, а средняя в большую, но нельзя вложить в большую коробку несколько маленьких рядом.
Среди заданных коробок необходимо выбрать такие, которые можно последовательно вложить одну в другую, причем их суммарный объем должен быть как можно больше. Написать программу построения такой “цепочки” коробок. Если есть различные решения с одним и тем же максимальным суммарным объемом, вывести любое из них.
Анализ задачи. Ясно, что при некоторых входных данных все коробки вложить одну в другую невозможно. Например, ни одну из коробок 10x10x10 и 100x100x2 нельзя вложить в другую. Поэтому ниже слова “большая” и “меньшая” коробка будут означать, что “меньшую” можно разместить внутри “большей” (возможно, после поворотов). Очевидно, что для ускорения проверки, помещаются ли коробки одна в другой, можно предварительно отсортировать линейные размеры каждой коробки, например, по неубыванию.
Эта задача уже упоминалась при сравнительном обсуждении достоинств алгоритмов решения задачи 13.3 (о подпоследовательности). Поэтому будем решать ее,
366
ГЛАВА 13
опираясь на алгоритм решения задачи 13.3. Ясно, что при модификации ука занного алгоритма сумму значений элементов следует заменить на сумму объемов коробок, а условие “ат<а” — на “т-я коробка помещается в к-ю”. Но этого мало.
Пусть коробки таковы, что их можно последовательно вложить одну в другую. И пусть в первый раз они задаются во входных данных в порядке от наименьшей до наибольшей, а во второй раз — наоборот. Ответы в этих ситуациях должны совпа дать, однако в первом примере все коробки окажутся вложенными, а во втором будет взята одна наибольшая коробка. Ведь для задачи о монотонной подпоследовательности важен порядок начальной последовательности, а для коробок — нет.
Естественно предположить: если порядок неважен, то при переборе ^-подзадач в процессе решения «i-подзадачи используются к не только от т+1 до N, но вообще все возможные от 1 до N. Но что делать, если при решении текущей подзадачи нужен ответ еще не решенной подзадачи? Можно запускать рекурсию с запоминанием, но где гарантия, что она не будет бесконечной?
Главная причина всех этих вопросов и сомнений кроется в том, что пока не выяснено, как изменилась постановка серии подзадач*.
Итак, сформулируем серию подзадач, в которой «/-подзадача будет иметь вид. Найти Цепочку коробок с наибольшим суммарным объемом среди цепочек, в которых самой внешней коробкой является т-я, а в качестве внутренних разрешается брать какие угодно коробки набора.
После этого нетрудно заметить, что при решении любой «/-подзадачи могут понадобиться результаты решения только тех ^-подзадач, у которых k-я коробка помещается в т-й.
Очевидно, что отношение вложенности коробок не может иметь циклов (когда /л-я коробка помещается в /„-j-й, ..., г2-я в ^-й, а г^-я — в /л-й). Это позволяет переупорядочить коробки так, чтобы “меньшие” всегда были размещеЦЫ перед “большими”, и после такой предобработки описанная выше модификация алгоритма из задачи 13.3 гарантированно даст правильный ответ.
Указанное переупорядочение является топологической сортировкой (см. подраздел 8.3.3). Любое правильное решение этой задачи, основанное на рекурсии с запоминанием, будет неявно проводить топологическую сортировку, но в данной задаче достаточно просто упорядочить коробки по неубыванию их объемов и затем решить задачу итерационно.
13.3. Табличная техника и рекурсия с запоминанием
13.3.1.	Расстановка скобок в произведении матриц
Рассмотрим задачу о расстановке скобок в произведении матриц — одну из наиболее известных задач динамического программирования. Например, с ее помощью методология динамического программирования описывается в замечательных книгах [3, 21].
Задача 13.5. Краткая формулировка. Задана последовательность матриц, для которой определено произведение. Расставить скобки так, чтобы произведение вычислялось с минимально возможным количеством элементарных умножений.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
367
Подробная формулировка. Матрицы — это прямоугольные таблицы, элементами которых, как правило, являются числа или скалярные переменные. В размерах матриц первое число означает количество строк, второе— количество столбцов. Произведение двух матриц АхВ определено, если они имеют размеры тхр и рхп (количество столбцов первой равно количеству строк второй), и результатом умножения будет матрица С размерами тхп, элементы су которой определяются по формуле су =	•
Описанное умножение матриц требует тпр умножений элементов. Произведение матриц не коммутативно (значения АхВ и ВхА могут не совпадать и даже одно из них может быть определено, а второе — нет), но ассоциативно, т.е. если одно из выражений (АхВ)хС, Ах(ВхС) определено, то второе тоже определено и их значения равны).
Пусть нужно найти произведение последовательности N матриц размерами а0Ха|( а,хп2, ..., а^ха*. Оно определено, поскольку число строк каждой матрицы (кроме первой) равно числу столбцов предшествующей и это будет матрица размерами аохап. Благодаря ассоциативности можно по-разному расставлять скобки внутри произведения, всегда получая один и тот же результат. Но от расстановки скобок может зависеть количество элементарных действий, необходимых для получения результата. Например, пусть размеры матриц — 5x2, 2x100, 100x2. При расстановке скобок (А,хА2)хА3 сначала с помощью 5-2-100=1000 элементарных умножений находится промежуточная матрица размерами 5x100, которая затем умножается на А3 с помощью 5' 100-2 = 1000 умножений, т.е. всего умножений 2000. При расстановке скобок А,х(А2хА3) сначала 2-100-2 =400 умножений, размер промежуточной матрицы 2x2, затем еще 5-2-2=20 умножений, т.е. всего 420 элементарных умножений.
Итак, нужно найти расстановку скобок в выражении А1хА2х...хАя с размерами матриц aoxalf а1ха2, ..., а^ха^ при которой общее число элементарных умножений во время вычисления было минимальным. Если таких расстановок несколькр, найти любую из них.
Вход. Первая строка текста matrmul. dat содержит число п (3 <и<50), вторая — через пробелы л+1 число (размеры а0, at,..., ап).
Выход. Текст matrmul. sol должен содержать две строки: первая — минимально возможное количество умножений, вторая — оптимальная расстановка скобок, в которой матрицы обозначены индексами, а умножения — звездочками"*”. Выражение должно быть правильным (строки типа (1*2) (*3) не допускаются). Лишние скобки, не влияющие на порядок вычисления, например ((1*(2))*(3)), допускаются; отсутствие скобок, например 1*2*3, не допускается.
Примеры
Вход	Выход
3	140
2 4 10	3	(1*2)*3
5	606
6 12	3	5 10 5 (1*2) * ( (3*4) *5)
368
ГЛАВА 13
Анализ задачи. “Перебрать все возможные расстановки скобок” не будет хорошей идеей: не ясно, как это запрограммировать, к тому же количество всех возможных расстановок зависит от п экспоненциально (см. задачу 10.2), т.е. полный перебор будет слишком неэффективным.
Покажем, что задача является задачей динамического программирования. Подзадачами будут: найти оптимальную в описанном смысле расстановку скобок для выражения где i<j. При i=j или i+1 =j имеем тривиальные подзадачи — выражение содержит соответственно 0 или 1 матричное умножение, поэтому скобки не нужны. При i= 1, j=n имеем конечную задачу.
Пусть обозначает минимальное количество умножений, ALJ — оптимальную расстановку скобок для подзадачи А^.-.хА^.
Рассмотрим (нетривиальную) подзадачу нахождения AtJ. При оптимальном вычислении этого произведения какое-то из умножений будет происходить последним: или Ajx(AH.1x„.xAy), или (AJxAH.1)x(A/+2x...xA/), или какой-либо другой вариант до (Ap<....xAJ_l)xAJ включительно— всего j-i вариантов последнего умножения (A/x...xAt)x(Ab_1x...xAJ), где i<, k<j.
Предположим, что именно k=k0 является последним в оптимальной расстановке А(/ Чтобы найти (оптимальную) ALJ, достаточно рассмотреть только (оптимальные) A, t и АЖ7 для iS k<j. Действительно, чтобы вычислить “большее” произведение Ах...хАр сначала вычисляем “меньшие” Ах...хАк и А^х...хА} и умножаем их значения. Поэтому стоимость вычисления “большего” состоит из стоимостей вычисления “меньших” и стоимости умножения результатов.
Пусть мы сначала использовали неоптимальный способ вычисления А^х...хАк и на основе этого произведения вычислили A;x...xAJt а затем улучшили способ вычисления Ajx...xAt (и только его) и вычислили А^х...хА} уже на его основе. Стоимость ры-числения A^...xAj от этого могла только уменьшиться, поскольку из трех ее составляющих первая (стоимость вычисления левого произведения) уменьшилась, а прочие остались неизменными. Аналогично можно рассмотреть и Ажх...хАг
Итак, рост эффективности вычисления каждого меньшего произведения повышает эффективность вычисления большего, и минимум операций достигается тогда, когда достигнут минимум в обеих меньших подзадачах.
Если бы мы знали, при каком именно к^ достигается минимальное количество умножений, мы могли бы сразу свести задачу нахождения А.; к двум меньшим подзадачам A,t и A. f Но мы его не знаем, поэтому нужно еще перебрать возможные к.
'“min
Mij= 4 min {Mik + <Ч-гака} + MMj}	(13.4)
* Терминологическое замечание. Математическая сторона динамического программирования на этом завершена, дальше идут вопросы реализации. Распространена, хотя и не общепринята, терминология, согласно которой то, что было до сих пор — динамическое программирование, а то, что будет дальше, — табличная техника (см. также подраздел 13.1.2).
Решение задачи. Забудем на некоторое время, что нужна расстановка скобок, и будем считать, что требуется только М1п.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
369
Если реализовать соотношение (13.4) рекурсивной функцией M(i, j : byte) : longint, эго будет правильно, но очень неэффективно, поскольку подзадачи здесь перекрываются, причем с довольно большой кратностью (см. подраздел 3.1.3).
Для эффективного решения используем таблицу. Ясно, что это должна быть двумерная таблица размерами (приблизительно) пхп и ее (1 ,;)-й элемент должен содержать М^ Заполнять таблицу в данной задаче естественнее всего по диагоналям, начиная с главной, пока не будет достигнут угловой элемент М1п. Благодаря такому порядку прохождения на каждом шаге используются только уже построенные элементы таблицы (возможны и другие порядки прохождения, имеющие это свойство).
Заполним таблицу вручную для второго примера из условия (п = 5, размеры 6, 12, 3, 5,10,5). Для удобства под главной диагональю в скобках обозначим размеры матриц (которые фактически там не хранятся).
На главной диагонали стоят нули, поскольку в соответствующих подзадачах умножений нет. Элементам диагонали, соседней с главной, соответствуют последовательности длиной 2 — проблемы выбора расстановки скобок все еще нет, Mt ,н=а,_}арм.
М12 = 6-12-3 = 216, М23 = 12-3-5 = 180,М34 = 3-5-10 = 150, М45 = 5-10-5 = 250.
В следующих диагоналях используются ранее построенные элементы таблицы.
М\3 = min{0+6-12-5+180, 216+6-3-5+0} = min{540, 306} = 306,
Ми = min{0+12-3-10+150,180+12-5-10+0} = min{510,780} = 510,
Л/35 = min{0+3-5-5+250,150+3-10-5+0} = min{325, 300} = 300,
1И14 = min{6+6-12-10+510, 216+6-3-1O+-150, 306+6-5-10+0} = min{1230, 546, 606} = 546, M2S = min{0+12-3-5+300,180+12-5-5+250,510+12-10-5+0} = min{480,730,1110} =480, Mi5 = min{0+6-12-5+480, 216+6-3-5+300, 306+6-5-5+250,546+6-10-5+0} =
= min{ 840,606,706, 846} = 606.
		1	2	3	4	5
1	(ff)	0	216	306	546	606
2		(12)	0	180	510	480
3			(5)	0	150	300
4				(5)	0	250
S					(10)	0
						(5)
В приведенных ниже фрагментах программы предполагается, что размеры матриц at уже прочитаны в массив sz : array [0. .MAXN] of longint, массив est объявлен как array [1. .MAXN, 1. .MAXN] of longint, все массивы глобальные. Заполнение таблицы описано в листинге 13.4.
Листинг 13.4. Итеративное заполнение таблицы
for iO := 1 to n do est [iO, iO] := 0;
for iO := 1 to n-1 do
370
ГЛАВА 13
estLiO, iO + 1] := sz [iO-1]*sz [iO]*sz [iO + 1];
for jO := 2 to n-1 do
for iO := 1 to п-jO do begin
i := iO; j := iO+jO;
estti, j] := $7FFFFFFF;
for к := i to j-1 do begin
e := est[i, k] + sz [i-1]*sz[k]*sz[j] + est[k+l, j];
if e < est[i, j] then begin
est[i, j] := e;
est [ j, i] := к; { для обратного хода (см. ниже по тексту) } end;
end;
end;
res := est [1, n] ;
Вернемся к расстановке скобок. В соответствии с формулой (13.4) нужно выбрать значение к, при котором достигается минимальное значение выражения. Следовательно, для восстановления (обратного хода) нужно сохранять к. Для этого можно или завести дополнительную таблицу размерами (приблизительно) пхп, или использовать такой прием: Мц вычисляем при i < j, а левая нижняя половина таблицы оценок свободна; используем ее для хранения к: est[i, j] будет содержать aest[j,i] — то jt, при котором достигнуто М..
Используя эту информацию, построим расстановку скобок. Берем значение k: = est[n, 1], означающее место последнего умножения при вычислении всего выражения, т.е. оптимальная расстановка скобок имеет вид (А1..Л4)-(Аш...А/1). Если какое-либо из подвыражений A1.._Aifc, Ам...Ап имеет длину 1 или 2, расставлять в нем скобки не нужно, иначе для построения расстановки скобок внутри подвыражений нужно аналогичным образом использовать значения est [k, 1] и est [п, к+1] соответственно. Описанный способ по своей природе рекурсивен, поэтому реализуем его рекурсивно (листинг. 13.5); для вывода результата нужен вызов restpars (1, п).
Листинг 13.5. Восстановление структуры скобок_________________________
procedure restpars (i, j : byte)
Begin
if i = j then begin write(i); exit end;
if i+1 = j then begin write(i, 1*', j); exit end;
write(1(1) ;
restpars(i, est [j, i]);
write(')*(');
restpars(est[j, i]+l, j);
write(1)1);
End;
Реализация, основанная на рекурсии с запоминанием (см. подраздел 13.1.2), представлена в листинге 13.6. Для корректной реализации рекурсии с запоминанием необходимо знать, была ли подзадача решена раньше. Здесь для этого используется поле est [ j , i], в котором должен храниться оптимальный выбор (число к): -1 означает, что подзадача еще не решалась, а другое значение — что уже решалась, и можно взять значение est [i, j ] в качестве результата.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
371
Листинг 13.6. Заполнение таблицы на основе рекурсии с запоминанием
Program Matrixes;
function M(i, j : byte) : longint;
var k : byte;
e : longint;
begin
if j = i then begin M := 0; exit end;
if est[j, i] о -1 then
begin M := est[i, j]; exit end;
if j = i+1 then begin
est[i, j] := sz[i-1]*sz [i]*sz [j] ;
est [j, i] := i;
M := est [i, j] ; exit
end;
for k := i to j-1 do begin
e := M(i, k)+sz[i-1]*sz[k]*sz[j]+M(k+1, j);
if (e < est[i, j]) or (est [j, i] = -1) then begin est[i, j] := e; est [j, i] := k end
end;
M := est [i, j] ;
end; { конец рекурсивной функции }
BEGIN { главная программа }
for i := 1 to n-1 do
for j : = i+1 to n do
est [ j, i] :=-l ;
res := M(l, n)
END.
Анализ решения. Сравнивая итеративное заполнение таблицы (см. листинг 13.4) и рекурсию с запоминанием (см. листинг 13.6), видим, что переход от формулы (13.4) к рекурсии проще, поскольку не нужно заботиться о правильном порядке прохождения клеток. Однако итеративное заполнение таблицы лучше тем, что оно работает быстрее, чем рекурсия с запоминанием, хотя асимптотические оценки и совпадают. Кроме того, при рекурсивной реализации необходимо хранить в программном стеке оценку е и счетчик к каждого вызова.
Оценим объем памяти и количество действий. Объем памяти определяется размерами таблицы и составляет О(п). Легко видеть, что общее количество действий определяется процессом заполнения таблицы: все другие действия (ввод данных, восстановление структуры скобок) требуют О(п) действий. Приближенно можно сказать, что нужно заполнить таблицу размерами порядка п и для заполнения каждой клетки нужно перебрать порядка п значений уже построенных клеток, итого — п-п = п.
Более строго, при заполнении первой диагонали (следующей за главной) перебора уже заполненных клеток нет, а есть лишь умножение трех чисел, т.е. количество действий на каждую из п-1 клетки константно. Во второй диагонали для каждой из п-2 клеток перебирается по два варианта, в третьей — п-3 клетки по три варианта и так далее до угловой клетки — для Нее нужен п-1 вариант. Таким образом,
372
ГЛАВА 13
(n-1) + 2(n-2) + 3(n-3) +... + (n— l)(n—(n—1)) = = n-1 + 2n—2-2 + 3n -3-3 +... + (n-l)n - (n-l)-(n-l) = = n(l + 2 + 3 +... + (n-1)) - (12+ 22+ 32+... + (n-1)2) = = n(n—l)n/2 — (n—l)n(2n—1)/6 = (n3—n)/6 = 0(n).
* Оценки n2 памяти и п3 времени типичны для алгоритмов динамического программирования. Эти алгоритмы достаточно хороши, но неочевидно, являются ли они наилучшими из всех возможных. Как правило, критическим фактором для них оказывается объем памяти.
И Приведенная в листинге 13.5 реализация берет в скобки обозначение одиночной матрицы, например (1) * (2*3).Модифицируйте процедуру restpars так,чтобы избежать лишних скобок.
» Сократите количество операторов процедуры restpars (см. листинг 13.5) за счет несущественного недостатка в расстановке скобок: все выражение в целом тоже можно брать в скобки.
13.3.2.	Минимальное количество монет
Задача 13.6. Денежная система некоторой страны предоставляет монеты номиналом с, = 1, с2, Как выдать сумму S с помощью минимального числа монет?
Вход. В первой строке — сумма S и количество номиналов N, во второй — значения номиналов; 1 < 7V<20,1 = с, < с2 < ... < ^<50000, S< 100000.
Замечание. Программа реализуется в современной 32-битовой среде программирования.
Выход. В первой строке — минимальное количество монет, во второй — N чисел (количества монет каждого номинала).
Пример
Вход 17 3 Выход 3
1 5 15	2 0 1
Анализ задачи. Данная задача весьма известна, но многие неправильно представляют себе способ ее решения. На первый взгляд, задача решается просто: каждый раз берем монету наибольшего допустимого номинала. В частности, этот алгоритм даст правильный ответ для примера при заданных условиях. Чтобы выдать 17, берем наибольший номинал 15 и получаем остаток 2; номиналы 15 и 5 больше этого остатка, поэтому выдаем его двумя монетами с номиналом 1.
Этот алгоритм находит правильный ответ не только для 17, но и для любой суммы, если номиналы монет — 15, 5 и 1. Он также находит правильные ответы для многих других наборов монет (возможно даже, что для всех наборов, используемых на нашей планете). Тем не менее этот алгоритм не всегда правилен! Например, нужно выдать сумму 50 монетами номиналом 1, 10 и 23. По этому алгоритму, взяв две монеты по 23, получим сумму 46 и остаток 4, а затем наберем 4 по единичке. Общее число монет — 2+4=6 (шесть), хотя 50 можно набрать как 5 (пять) раз по 10.
Решение задачи. Рассмотрим серию подзадач: Каким наименьшим количеством монет можно выдать сумму г, используя монеты только 1-го, 2-го,..., i-го номиналов? (Не обязательно использовать монеты всех этих номиналов, главное — не брать другие.) Будем называть такие подзадачи (i, ^-подзадачами — тогда целевая задача будет (N, 5)-подзадачей.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
373
Ответы (г, г)-подзадач будем записывать в прямоугольную таблицу Т, строки которой соответствуют номиналам монет, столбцы — суммам.
Первую строку заполнить очень легко: если разрешено использовать только монеты номиналом 1, то необходимое количество монет совпадает с нужной суммой.
Заполнять любук? непервую строку можно на основе того, что в набор монет можно или не включать монеты этого нового номинала (включить их нуль штук) либо включить одну такую монету, либо две и т.д. Эти варианты соответствуют наборам из T(i-1, г) монет, T(i-1, г-с.)+1 монет (здесь T(i-1, r-с) монет позволяют набрать сумму г-c., плюс одна монета номиналом с), T(i-l,r-2-c)+2 монет и т.д. Это можно записать формулой
T(i, f) = min	г-к-сд+к}.	(13.5)
Однако несложно разработать более эффективную формулу. В действительности невозможна ситуация, когда оптимум какой-либо (/, г)-подзадачи достигается только без монет i-ro номинала, а оптимум (i, /ч-с,)-подзадачи — обязательно с более чем одной такой монетой. Ведь решение (/,г)-подзадачи таково, что выполняются все неравенства T(i, г) < T(i, r-cj+l, T(i, г) < T(i, r-2c)+2, ... (иначе решение (i, г)-подзадачи просто не будет оптимальным). Но тогда при анализе (г,/ч-с)-подзадачи окажется, что ни одно из значений T(i, r-c)+2, T(i, г-2с)+3,... не может быть строго меньше, чем T(i, г)+1.
Итак, перебирать все варианты, указанные в формуле (13.5), не нужно — достаточно сравнить только T(i-l,r) и T(i,r-c)+l. Поэтому формулу (13.5) можно упростить до вида
T(i, f) = min{7V-l, г), T(i, г-с,)+1}	(13.6)
и на ее основе написать несложную программу.
Чтобы определить число монет каждого номинала, нужно провести обратный ход. И, хотя строить таблицу удобнее по формуле (13.6); при обратном ходе удобнее считать, что она построена по формуле (13.5) (которая, напоминаем, дает те же результаты, но подразумевает больший перебор). Если T(i,r)=T(i-l,r), это означает, что для оптимального решения целесообразно не брать ни одной монеты i-ro номинала; если это не так, но T(i,r)= T(i-l,r-c)+l, то целесообразно взять одну монету i-ro номинала, если T(i,r) = = T(i-l, r-2c)+2 — две монеты и т.д. Процесс этот, как обычно, начинаем с (N,S)-подзадачи и продолжаем, пока не придем в первую строку или в нулевой столбец.
Для примера из условия заполненная таблица имеет следующий вид.
	0	1	2	3	4	5	6	7	8	9	10	11	12	13	14	15	16	17
1 (С1 - 1)	0	1	2	3	4	5	6	7	8	9	10	11	12	13	14	15	16	17
2 (С2 = 5)	0	1	2	3	4	1	2	3	4	5	2	3	4	5	6	3	4	5
3 (с3 . 15)	0	1	2	3	4	1	2	3	4	5	2	3	4	5	6	1	2	3
Анализ решения. Очевидно, что и количество действий, и объем памяти имеет оценку Q(N S). Но что такое 5? До сих пор мы выражали количество действий и объем памяти как асимптотические оценки от количества входных данных. Но здесь S
374
ГЛАВА 13
является не количеством, а значением одного из входных данных, поэтому алгоритм псевдополиномиален (см. также подраздел 11.3.2).
Принципиально иначе эту задачу можно решить с помощью метода ветвей и границ (см. раздел 11.3).
Есть еще один способ, мало известный олимпиадникам, но наиболее очевидный с точки зрения “классической” науки. Задачу можно рассмотреть как задачу иулочислешюй линейной ошпимиза-ции (минимизации целевой функции х2+ ...+xN при ограничениях с^ч- с^с2+ ...+ £^=5 и целочисленности х2,..., х^. Для решения этой задачи известны специальные методы, применяемые в линейном программировании, например симплекс-метод плюс метод Гомори (см. литературу по методам оптимизации и математическому программированию).
Сравнивая все эти методы, отметим, что псевдополиномиальный алгоритм безусловно проще для реализации и при указанных технических ограничениях эффективнее по времени. Но с возрастанием S (при неизменном N) псевдополиномиальный алгоритм проигрывает другим подходам. Кроме того, псевдополиномиальныи алгоритм требует гораздо больше памяти.
Важно также, что только псевдополиномиальный алгоритм требует, чтобы номиналы монет были не очень большими натуральными числами. Иными словами, он менее универсален, т.е. решает более узкую задачу.
13.3.3.	Разбиение алфавита
Задача 13.7. Существует следующий способ набора букв на мобильном телефоне. Клавише 2 сопоставлены буквы abc, клавише 3 — def и т.д. При наборе текста одно нажатие на клавишу 2 порождает символ а, два подряд нажатия — символ Ь, три подряд — символ с; аналогично, одно нажатие на 3 порождает d, два подряд— е и т.д. Если же нужно набрать две букры а, то нажимают на 2, немного ждут и снова нажимают на 2.
Обобщим ситуацию. Пусть есть алфавит из N символов, который нужно сопоставить М клавишам (М <N). Для каждого символа алфавита известна частота его использования. Нужно задать соответствие символов алфавита клавишам так, чтобы символы с первого по некоторый kt-vi соответствовали первой клавише, с (Jt,+l)-ro по некоторый &2-й — второй клавише и т.д., а среднее количество нажатий на клавиши (исходя из известных частот) было минимальным. Формально говоря, нужно минимизировать характеристику
=fyi +fyi + • • • +/nIn,	(13.7)
где/ — частота использования j-ro символа согласно входным данным, г, — количество нажатий, нужное для набора i-го символа согласно построенному разбиению алфавита.
Вход. В первой строке текста abodefg. dat записаны N и М, в следующих N строках — по одному целому числу, пропорциональному частоте использования символа (^<Л/< 100, М< ЛГ<250).
Выход. Первая строка текста abcdefg.sol должна содержать найденную минимальную характеристику (13.7), а каждая из следующих М строк — два числа (между ними пробел), которые задают диапазон символов, сопоставленных данной клавише.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
375
Пример
Вход 5 3 Выход	21
3	12
2	3 3
5	,	4 5
7
1
Анализ задачи. Задачу легко разбить на подзадачи аналогичной структуры меньшего размера. Такими подзадачами будут как оптимально (в смысле (13.7)) сопоставить символы алфавита с первого по q-й клавишам с первой по р-ю? Назовем такую задачу (р, д)-подзадачей, а значение оптимальной оценки количества нажатий в (р, q)-. подзадаче обозначим 7^. Тогда нужная задача является одной из задач серии, а именно (Л7,Л0-подзадачей.
Покажем, что выполняется основное свойство динамического программирования (у оптимально решенной задачи все подзадачи решены оптимально).
► Пусть у нас есть оптимальное решение (р, ^(-задачи для некоторыхр и q (ISpiq), а последней буквой из назначенных (р-1)-й клавише будет некоторая k-я (р-1< k<q). Тогда оценка количества нажатий может быть выражена как
Т_д=Т . . + £.,'1 +/,,-2 + ... +f (q-к), pq	"'Arri •'KT2	Jq v” '
При изменении способа разбиений для (р-1,Л)-задачи в этой формуле изменяется лишь величина Г , к, а остальные слагаемые неизменны, т.е., чем меньше к, тем меньше и Т^- А отсюда следует: чтобы “собрать” оптимальное решение большей подзадачи, достаточно рассматривать только оптимальные решения меньших подзадач. <
Решение. Аналогично другим задачам динамического программирования, приведенные рассуждения связаны с конкретным числом к, которое в действительности не известно, поэтому нужно перебрать возможные значения к и выбрать то, для которого искомая величина 7^ минимальна. Таким образом, уравнение динамического программирования для данной задачи имеет следующий вид:
7}^ = min {7^1,*+ (/*-ц-1 +/t+2-2+ ... +fq(q-k))}	(13.8)
к: р 4 к < у
Остается запрограммировать эту формулу. Очевидно, что здесь много перекрывающихся подзадач, поэтому нужна не рекурсия, а итерационное заполнение таблицы. Вначале вычисляются все значения первой строки Г1?, затем с помощью известных Tlq — все значения второй строки и так далее до Л7-й строки. При построении оценок р-й строки используются лишь оценки (р-1)-й, поэтому для экономии памяти можно хранить не всю таблицу оценок, а только две ее строки — предыдущую и текущую5.
Впрочем, вся таблица выборов все равно необходима. Ведь нужно не олько найти минимальную оценку числа нажатий, но и восстановить само разбиение, а оно определяется именно тем к, которое выбирается в уравнении (13.8). Поэтому, чтобы
5 Казалось бы, что можно даже хранить только одну строку, поскольку используются только значения из меньших по номеру столбцов предыдущей строки. Но эта оптимизация по памяти мешает более важным оптимизациям по скорости, описанным ниже.
376
ГЛАВА 13
восстановить оптимальное разбиение, нужно хранить таблицу выборов и действовать по обычной схеме обратного хода. Сначала возьмем выбор klasl целевой задачи, восстановив тем самым диапазон букв, сопоставленных М-й клавише, затем возьмем выбор (Af-l,fcto()-задачи и восстановим диапазон (А/-1)-й клавиши и т.д.
Отметим две довольно существенные оптимизации, которые требуют, чтобы перебор значений к в уравнении (13.8) проводился от больших значений к меньшим.
1.	При переходе в формуле (13.8) от значения к к значению к-l слагаемое в скобках превращается H3/t+l-l+/J+2-2+ ...+fg(q-k) в/ь-1+/ж-2+ ...+fg(q-k+V), т.е. увеличивается mfk+fk+l + ... +f. Используем для хранения частот символов не только массив freq частот, заданных входными данными, но и массив sum_freq с суммами частот символов с первого по текущий: sum_freq[i] = J\+f2+ ••+fr Тогда “добавку” fk+	можно выразить как разность surti_f req [q] -
sum_f req [ k -1 ] и избавиться от лишнего цикла.
2.	В соответствии с формулой (13.8) значения к изменяются от наибольшего р-1 до наименьшего q. Будем хранить текущее значение искомого минимума в переменно min_est, а текущее значение4ц'1+/м-2+ ... +fq {q-k) — в loc_sum_f req и сравнивать их при каждом значении к. Как только окажется, что min_est < loc_sum_f req цикл по к можно прервать — при уменьшении к значение loc_sum_f req может только возрасти, поэтому сумма loc_sum_f req+T j к наверняка будет больше текущего значения min_est.
Эти оптимизации (особенно первая) позволяют заметно сократить количество действий именно в том вложенном цикле, который выполняется очень много pai Как следствие, получаем асимптотическую оценку времени работы программы O(MN2), т.е. порядка MN2 в худшем случае; без этих оптимизаций оценка^получилас бы 0(Л/№), т.е. порядка MN3 для всех входных данных.
13.3.4.	Абзац с блоками разной высоты
Задача 13.8. Есть абзац текста, в котором много слов (блоков) с разными высотами, например обычные слова и математические формулы. Абзац достаточно длинный, поэтому его нужно разбить на строки. Высота строки определяется по наивысшему из блоков в ней. Высота абзаца определяется как сумма высот всех строк. Длина каждой строки определяется как суммарная ширина блоков, включенных в эту строку (пробелы не учитываются). Возможность разбиения блока для переноса со строки на строку не рассматривается. Изменять порядок следования блоков нельзя.
Нужно найти такое разбиение абзаца на строки, чтобы высота абзаца была минимальной.
Вход. Первая строка текста paragr. dat содержит ширину области печати (т.е. максимальную допустимую длину строки) и число N (количество блоков в абзаце), где 5<W<200; следующие N строк — по два числа (ширину и высоту блока); все размеры — натуральные числа не больше 1000000.
Выход. В первую строку текста paragr. sol вывести полученную высоту абзаца, во вторую — количество строк М, на которые нужно разбить абзац, в следующие N строк — количества блоков в соответствующих строках абзаца.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
377
Пример
Вход 7 6	Выход 5
3 1	3
2 1	2
3 3	♦ з
1 1	1
3 3	
3 1	
Анализ и решение задачи. Задача разбиения текста на строки в похожих постановках возникает во многих приложениях, и на практике она обычно (за исключением компилирующей верстки в TgX-системах) решается по принципу “размещать в строку все, что в нее помещается, а остальное переносить дальше”. Однако этот способ не всегда дает наилучший результат — и в данной задаче, и в некоторых других разумных ее постановках. Он только самый быстрый.
Покажем неоптималыюсть указанного алгоритма подробнее. Пусть входные данные отличаются от приведенных в условии только тем, что оба блока высоты 3 имеют ширину 2. Тогда алгоритм решит, что к первой строке нужно отнести первый, второй и третий блоки (они как раз имеют суммарную ширину 7, равную ширине области печати), а во вторую — оставшиеся, т.е. с 4-го по 6-й. Но тогда обе строки будут иметь высоту 3, и суммарная высота равна 6. Однако для этих входных данных ничто не мешает разбить абзац на строки так же, как для примера из условия; тогда будут три строки, но зато их высоты будут равны 1, 3 и 1, что в сумме дает 5, а 5 < 6.
Данная задача интересна тем, что при попытке применения динамического программирования для нее можно поставить разные серии подзадач. Легко прийти к серии подзадач, аналогичной серии из задачи 13.7 (о разбиении алфавита): Как оптимально разбить часть абзаца от первого по q-й блок нар строк?Такая серия подзадач будет корректной, на ее основе можно построить правильный и достаточно быстрый алгоритм, и все же такая серия подзадач — не лучшее, что можно придумать.
Поставим подзадачи так: Как оптимально разбить на строки часть абзаца от первого по i-й блок?, не фиксируя в постановке подзадачи количества строк — пусть оно само подберется в процессе поиска оптимального решения.
Тривиальными подзадачами будут несколько первых подзадач: если сумма ширин блоков от первого по i-й позволяет разместить их в одну строку, то нужно так и сделать. Ведь если все блоки помещаются в одну строку, то высота абзаца равна высоте строки, т.е. максимальной высоте блока. При любом разбиении на строки блок с максимальной высотой будет определять высоту одной из строк, и высота абзаца окажется равной высоте этого блока плюс высоты остальных строк.
Обозначим ширину области печати через TW, ширину и высоту блока — через wt и Л,, оптимальное решение /-подзадачи (минимальная высота части абзаца с первого по i-й блок) — как Тг Тогда решения тривиальных подзадач можно записать как
Т, = max hk при УА-.м'р TW	(13.9)
Перейдем к нетривиальным подзадачам. При размещении i блоков в более чем одной строке некоторый £-й блок оказывается последним в предпоследней строке; очевидно, k должно удовлетворять условиям к< I и »уж+ ...+ w<TW (все блоки
378
ГЛАВА 13
от (it-bl)-ro по i-й должны поместиться в одну строку). Высота последней строки равна max h . Очевидно, что эта высота не зависит от способа разбиения по строкам t< pSi р
предыдущих блоков, поэтому, чем лучше решена ^-подзадача, тем лучше будет и решение i-подзадачи, т.е. принцип оптимальности для задачи выполняется.
Итак, уравнение оптимальности имеет вид
Tj= min (Тк + max hp) при условиях к< i и £ w <TW .	(13.10)
* р=к+1
Значения к легче всего перебирать в цикле while от (i-1) в сторону уменьшения.
13.3.5.	Максимальное значение выражения
Задача 13.9. Дано арифметическое выражение, состоящее из одноцифровых (от 0 до 9) чисел, между которыми записаны знаки операций +, - и *. Нужно вставить в это выражение круглые скобки так, чтобы получить правильное арифметическое выражение, имеющее наибольшее возможное значение. Если таких расстановок несколько, найти и вывести любую одну из них.
Вход. Одна строка текста max. dat содержит выражение указанного вида с количеством операций от 2 до 50.
Выход. Текст max. sol должен содержать одну строку с выражением, в котором расставлены скобки. Строка обязательно должна быть правильным арифметическим выражением. Если в выведенной строке не будет хватать скобок, порядок действий при проверке будет определяться старшинством операций.
Пример. Вход: 1+2-3*4; выход: ((1+2) -3) *4.
Анализ задачи. Попытки использовать “жадные идеи” не приводят к успеху. Например, к стратегии “собирать вместе части выражения от знака умножения до знака умножения, поскольку произведения обычно больше сумм” можно подобрать контрпример
(4+4)*(8-6)*(4+2) = 96 < 348 = ((4+4)*8-6)*(4+2).
Будем нумеровать числа, начиная с 0, знаки операций — с 1, так что /-я операция будет между (i-l)-M и i-м числами:
d0 opi di ор2 d2 ору... opn dn.
Решим задачу аналогично задаче 13.5 (об оптимизации расстановки скобок в произведении матриц). Покажем, что задачу можно решить методом динамического программирования.
Разбить задачу на подзадачи меньших размеров просто — ^ми будут задачи поиска оптимальной расстановки скобок для частей заданного выражения, имеющих вид </,орж ...op.dj, где 0< i< j<n. При i=j и i+1 =j подзадачи тривиальны — если выражение является числом или содержит лишь одну арифметическую операцию, расставлять скобки не нужно. При i = 0, j=n получаем целевую задачу.
Аналогично задаче об умножении матриц, заметим, что при вычислении dop^ ... opjdj последней оказывается какая-то из opk, i <k<j.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
379
Однако неясно, как использовать оптимальные решения меньших подзадач для эффективного построения оптимального решения большей подзадачи. Если бы только складывались и умножались натуральные числа, достаточно было бы формулы, ана-
логичной (13.4) из задачи о матрицах, что-то наподобие Mf=
(хотя
и она не нужна, поскольку в этой ситуации сработала бы стратегия “складывать от умножения до умножения”). Однако в выражении возможны вычитания, для которых максимумы меньших подзадач не дают максимума большей. Чтобы получить в результате вычитания как можно больше, уменьшаемое должно быть как можно больше, но вычитаемое — наоборот, как можно меньше.
Стоп! Именно это наблюдение и позволяет применить динамичешЛе программирование. Вместо задачи Найти расстановку скобок, при которой выражение максимально будем рассматривать задачу Найти пару расстановок скобок: ту, при которой выражение максимально, и ту, при которой оно минимально. Обозначим мини
мальное и максимальное значения выражения для подзадачи (i, j) как ЕМах^ и Оптимумы для сложения и вычитания имеют вид
maxfTjjt-i+T’fcj} = EMaXjk-i + Emaxkj, = EMinlk^ + Eminkj, max{7’(-.jb-i-l*..j} = EMax^i - Eminkj, - Emaxkj,
(13.11)
где Tt j обозначает подвыражение </,opi+1... орД, в котором еще не выбрана расстановка скобок, и оптимум (min или шах) берется по всем расстановкам.
С умножением сложнее. Если сомножители положительны, то max{7’/
= ЕМах^-ЕМах^ Если же оба минимальные значения отрицательны и велики по модулю, то может быть и max{r(JMTtJ = EMinik_x-EMinkj, поскольку “минус на минус дает плюс”. Если же значение Tt при любой расстановке скобок положительно, а значение Тк отрицательно, то все возможные произведения будут отрицательными, и нужно минимизировать абсолютное значение, а это будет EMin.^-EMax^. Аналогична и симметричная ситуация, когда max{T. k_\Tk]f = EMaxjk_l-EMinkj.
Можно было бы расписать все возможные варианты значений оценок меньших подзадач и для каждого выяснить, чему именно будет равен тах{Т Однако при использовании этих результатов в тексте программы будет много разветвлений, смысл которых — выяснить, какой из вариантов имеет место. Возможно, этот способ немного эффективнее, чем приведенный ниже, но он требует очень большой аккуратности как при математическом выражении всех возможных вариантов, так и при их программировании.
Докажем следующее утверждение.
Чтобы найти шах{Г/„*-1-7к..Д, достаточно рассмотреть четыре пары значений: EMirtiji-yEMinig, EMinjk-vEMaxty ЕМахц^-ЕМтц, EMaxi^-vEMaxkj.
► Рассмотрим произвольную пару <е. ^vekj>, где eej.— значение Те .при некоторой расстановке скобок. Может быть или е. >0, или е, < 0. Если е, >0, то
>0 so
so
380
ГЛАВА 13
т.е. ЕМах.^-е^> elkri-ekJ; если же ек .< 0, то
EMinilrl-ek.-eIJriek = (EMini.k-i	>
so
£0
т.е. EMin1к-х-ек>еккгх ек/
В обоих случаях удается построить произведение, которое не меньше, чем eL k_x-ekJ> и имеет в качестве первого сомножителя ЕМах)к_у или EMinik_v Аналогично, в любом из рассмотренных случаев переход к произведению, где вторым сомножителем является EMaxkJ или EMin^ (в за-висимост^хг знака первого множителя) также не уменьшит значения произведения.
Поскольку эти преобразования можно провести при любом выборе <et ни одно из значений е( k-l-ek J не будет строго больше всех четырех приведенных в условии произведений, т.е. среди этих четырех есть максимально возможное. <
Утверждение, что min{Tz Д достаточно искать среди тех же четырех пар, доказывается симметрично, т.е. в случае ekj>0 делается переход к£Л/т/1_1 и т.д.
Благодаря этому запишем
	EMaxi k_i  EMaxk j EMaxi k_{  EMink EMini k^ • EMaxk j EMin,^ • EMink j	
тах{7;.*-г7*..Л = max		»
	EMaxl k_i  EMaxk j EMaxl k_i  EMink j EMinl k_t  EMaxk j EMinik_x  EMink j	(13.12)
min{7’i.Jk_r7’t.J} = min-		
Отсюда уравнение динамического программирования, аналогичное (13.4) из задачи 13.5, имеет вид
Етахц= max {тах{Т|..£_] орк 7*..у}},
(13.13
Erring ^ тш {т1п{Г,..*_1 орк Tfcj}},
где выражения opt{T( k_lopk TkJ} разворачиваются согласно (13.11) или (13.12) в зависимости от знака операции орк (“+”, или “*”).
Решение задачи. Табличный способ. Уравнение (13.13) сложнее, чем (13.4), но задает тот же порядок использования подзадач, поэтому реализация заполнения таблицы будет похожей на приведенную в подразделе 13.3.1, только гораздо более громоздкой.
Для любой подзадачи (i,j) нужно сохранять уже не один оптимум, а два {EMin и ЕМах^, а для дальнейшего восстановления расстановки скобок — по два выбора, приведшие к минимальному и к максимальному значениям выражения.
Здесь острее, чем в задаче о матрицах, стоит проблема объема памяти, поэтому еще важнее склеить две треугольные таблицы в одну квадратную. Но, поскольку здесь подзадачи нужно решать и min и для max, лучше “склеивать” половинки таблиц не так, как это сделано в задаче о матрицах (est [i, j] — оценка, est [j, i] — выбор),
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
381
а иметь для оценок и выборов отдельные таблицы, в которых элемент с индексами [i, j ] соответствует max{T/y}, ас [ j , i] — min{Ti;}. Такое представление лучше еще и тем, что оптимальные оценки и оптимальные выборы — величины различного происхождения, имеющие разные типы.
Впрочем, любое “склеивание” является вынужденным компромиссом и не может быть идеальным. Если не пытаться уменьшить вдвое объем памяти, то наилучшим будет представление таблиц в трехмерных массивах размерами [ 0.. 1, 1.. MAX_N, 1.. MAX_N], где первый индекс определяет, с min или с max мы имеем дело.
Наконец, учтем необходимость восстановления скобок. Восстановление упростится, если считать, что каждый выбор определяется не только числом к, но также двумя булевыми значениями, указывающими, минимизировать или максимизировать левый и правый аргументы операции орк. В частности, если орк является умножением, при поиске оптимума согласно (13.12) перебираются четыре варианта; если не сохранить, какой из них дал оптимум, то при обратном ходе придется перебирать их снова.
Итак, выбор представляется составной структурой, но с технической точки зрения числа к удобнее хранить в одной таблице, а указания, минимизировать или максимизировать соответствующие подвыражения, — в других. Однако таблицы указаний для левых и для правых подвыражений все-таки можно совместить, используя элементы типа byte: старший шестнадцатеричный разряд соответствует выбору для левой подзадачи (0 — min, 1 — max), младший шестнадцатеричный — для правой.
Рекурсия с запоминанием. Вычисление EMin и ЕМах по формулам (13.11)—(13.13) разумно реализовать в разных функциях, вызывающих друг друга. Для записи этих косвенно рекурсивных функций используем конструкцию forward (см. подраздел 3.1.4). Например, вначале запишем заголовок EMin со словом forward вместо блока функции, затем заголовок и блок функции ЕМах, затем функцию EMin (при этом ее заголовок может быть сокращенным — function EMin;).
Модификация задачи. Что будет, если в условие данной задачи добавить действие деления “/”? Очевидно, что некоторые расстановки скобок могут стать недопустимыми, приводя к делению на нуль, а значения подвыражений перестают быть гарантированно целыми. Однако кардинальное усложнение задачи кроется в другом.
Рассмотрим тах{Т к_{/Т^. Результат деления может быть наибольшим, если делимое имеет как можно большее неотрицательное значение, а делитель — как можно меньшее строго положительное или делимое — как можно большее по модулю неположительное значение, а делитель — как можно меньшее по модулю строго отрицательное... Перечень случаев еще не закончен, но уже создана серьезнейшая проблема: как искать указанные оценки для делителя!
Если бы внутри подвыражения TkJ были только умножения и деления, такие оценки (обозначим их EMinPos^ и EMaxNeg^, минимальное положительное и максимальное отрицательное) можно было бы построить, опираясь на ЕМах, EMin, EMinPos и EMaxNeg меньших подзадач. Но предположим, что в подзадаче {k,j) операция орр, где к< p<j, является вычитанием. Чтобы построить EMinPosи EMaxNeg нужно рассмотреть среди других и ситуацию, когда скобки расставлены как	).
Разность будет ближайшей к нулю, но не нулем, когда уменьшаемое и вычитаемое максимально близки, но не равны. По данному критерию нельзя проводить оптимиза
382
ГЛАВА 13
ции меньших подзадач (к,р-l) и (p,j) независимо, т.е. не выполнены условия применимости динамического программирования (см. подраздел 13.1.2).
Ситуация со сложением аналогична вычитанию: чтобы сумма была как можн ближе к нулю, слагаемые должны быть максимально близкими к противоположным (равным по модулю разного знака), т.е. условие независимости подзада вновь нарушено.
Из-за нарушения условия независимости подзадач вряд ли можно предложить что-нибудь существенно более эффективное, чем полный перебор всех возможных расстановок скобок и вычисления полученных выражений. Для реализации такого переборного способа решения отсылаем желающих к работе [43], раздел 2.6.
13.3.6.	Вычеркивание из строки
Задача 13.10. Задана строка, символы которой могут повторяться. За один ход разрешается вычеркнуть в любом месте строки один или несколько одинаковых символов, идущих в строке подряд. Нужно удалить все символы строки с помощью наименьшего количества вычеркиваний.
Вход. Строка длиной не больше 255.
Выход. Минимальное количество операций, с помощью которых можно удалить все символы строки.
Пример. Вход'. 1343223; выход'. 4.
Анализ задачи. В строке 1343223 из примера можно вычеркнуть 22 (получится 13433), затем 1 и 4, а затем сразу три символа 3, т.е. всего четыре вычеркивания.
Предварительно “сожмем” слово, заменив каждую последовательность одинаковых символов, идущих подряд, одним таким символом, например, из 111223114 получим 12314. Очевидно, что ответ от этого не изменится, как и вреК<я работы в худшем случае. Однако в среднем выигрыш будет существенным за счет уменьшения числа подзадач (сложность “сжатия” линейна и не влияет на общую оценку — см задачу 5.4). Кроме того, “сжатие” гарантирует, что все соседние символы различны.
Пусть а [1] ...а [т] — строка, т — ее длина. Часть строки a [i] ...а [ j ], состоящую из всех символов с i-ro по /-Й включительно, будем называть i. ./-подстрокой Серия подзадач очевидна: За какое минимально возможное количество вычеркивании можно удалить i..j-nodcmpoKy? Нужная задача будет одной из задач серии, а именно задачей размером (1, т).
Тривиальные подзадачи, решение которых очевидно — это подзадачи вида (i, i (одиночный символ удаляется за один ход) и вида (i, i+1) (двухсимвольные подстроки, состоящие из разных символов, удаляются за два хода).
Обозначим минимальное количество действий для удаления i. ./-подстроки через Тц. Оптимальным решением нетривиальной (ц/)-подзадйчи может быть такое: перебрать все к, удовлетворяющие i< k<j, удалить i..fc-подстроку, а затем k+l..j-подстроку. Значение, полученное этим способом, обозначим через
КУ= ^АТ^}.	(13.14)
Однако это решение не обязательно оптимально. Если а [ i ] = а [ j ], вычеркивания можно ускорить: сначала удалить подстроку a [i+1] . .a [j-1], т.е.
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
383
a [i] . .a [j] без крайних символов, а затем за один ход вычеркнуть одинаковые крайние символы. Значение, полученное этим способом, обозначим через Ку:
Ку = Fi+ij-j+l при a [i] = a[j].	(13.15)
Таким образом, при a^i] [ j ] окончательный оптимум О’,у)-подзадачи достаточно искать как Tv= Kv (согласно (13.14)), а при a [i] = а [j] следует рассмотреть Ку и К* и выбрать из них меньшее.
Но и этого мало. Если одинаковые крайние символы i..j-подстроки повторяются внутри нее, то может оказаться, что выгоднее всего сначала удалить все подстроки, разделяющие эти вхождения, а затем зачеркнуть все эти символы за один ход (например, слово 1213141 лучше всего обработать, вычеркнув 2, 3, 4, а затем все единицы за один ход).
Однако “может оказаться” не означает “непременно окажется”. Рассмотрим слово 1231413215161. Любое его оптимальное решение (например, сначала вычеркнуть 4, затем новообразуемые подстроки 11, 33, 22, затем 5, б, и, наконец, оставшиеся 1111) требует удалять вторую и третью справа единицы (в позициях 9 и 11) вместе с крайними (в позициях 1 и 13), а единицы в позициях 4 и 6 должны быть вычеркнуты раньше. Таким образом, на последний ход нужно оставить много единиц, но не все.
• Нужно позволять вычеркивать за один ход больше двух вхождений буквы, но не обязывать удалять все вхождения.
Может показаться, что для реализации этой идеи нужен полный перебор подмножеств вхождений символов, одинаковых с а [ i ] и а [ j ]. Однако это не так благодаря следующей идее.
Пусть для некоторой /..у-подстроки крайние символы a[i], a [j] совпадают с некоторым внутренним а [к]. Тогда удаление сначала i+l..fc-1-подстроки, затем k+1..j-1 -подстроки, затем трех одинаковых символов a[i]a[k]a[j] можно условно свести к тому, что удаляется /..^-подстрока (но символ а [к] при этом загадочным образом остается), а затем Е.у-подстрока. Из суммы количеств ходов по удалению этих подстрок вычитается 1, поскольку при вычислении Т^+Т^ вычеркивание символа а [к] считается дважды:
*7 = мЧЧ rJW1’	(13-16)
Очевидно, что формула (13.16) позволяет собирать под одно вычеркивание много одинаковых символов (если оптимальное решение хотя бы одной из и (k,j)-подзадач получено по этой же формуле), не требуя, чтобы все одинаковые символы вычеркивались обязательно за один раз.
Окончательный оптимальный ответ z..y-подзадачи считается по формуле
Ту = пйп{Ку, К', К^}	(13.17)
(учитывая значения, имеющие смысл для данной подстроки).
Как и в задачах об умножении матриц и о максимальном значении выражения, формулы (13.14)—(13.17) следует реализовать либо с помощью рекурсии с запоминанием, либо итеративно заполнить таблицу для Ту, двигаясь от главной диагонали по линиям, параллельным ей, пока не придем в угловой элемент Tlm.
384
ГЛАВА 13
Вспомогательные величины Ку, K~tj и К~ используются только для вычисления Тп (при тех же i иу) и в дальнейшем их помнить не обязательно. Однако, реализуя алгоритм с помощью рекурсии с запоминанием, эти вспомогательные значения все-таки придется хранить (в массивах или в программном стеке). Так что преимущества итеративного заполнения таблицы проявляются в данной задаче несколько ярче чем в предыдущих.
Упражнения
13.1.	Предположим, что в задаче 13.1 нужно найти только максимальную сумму, а путь не нужен. Сколько и какого размера массивов достаточно для решения?
13.2.	Пути в числовом треугольнике начинаются от верхнего числа. От любого числа можно перейти к одному из двух соседних чисел в следующей строке Вычислить максимальную среди сумм чисел, расположенных на путях, заканчивающихся каким-нибудь числом в основании треугольника, и найти один из путей с этой суммой. В следующем треугольнике он образован выделенными числами.
7
3	8
5	10
2	7	4	4
4	5	2	6	5
13.3.	Триангуляция многоугольника — это разбиение его на треугольники непере-секающимися диагоналями (рис. 13.3). Стоимость триангуляции определяется как сумма длин проведенных в ней диагоналей. Задан выпуклый многоугольник. Найти его триангуляцию с наименьшей стоимостью. 1
Рис. 13.3. Две триангуляции выпуклого шестиугольника
Вход. Координаты вершин выпуклого многоугольника, заданные в порядке обхода против часовой стрелки.
Выход. Совокупность диагоналей, принадлежащих к оптимальной триангуляции (целесообразно также вывести изображения оптимальной триангуляции на экран в графическом режиме).
13.4.	Заданы две строки длиной не больше 104. Нужно найти^:
а)	самую длинную из их общих подстрок (например, для строк гавгав и кваква это будут а и в). Если таких несколько, найти наиболее “прижатую влево” в первой строке (а);
б)	самую длинную из их общих подпоследовательностей символов (ава или вав для строк гавгав и кваква). Если их несколько, найти наиболее “прижатую влево” в первой строке (ава);
ДИНАМИЧЕСКОЕ ПРОГРАММИРОВАНИЕ
385
в)	“расстояние” между строками — минимальное количество вставок и удалении символов, необходимых для преобразования одной строки в другую (гавгав и кваква имеют расстояние 6: удалить г, г и в в конце и вставить к, ей к).
13.5.	Роман состоит из С глав. Нужно, не переставляя главы, разбить его на В (В<С) томов так, чтобы максимальная толщина тома (сумма количеств страниц вошедших'в него глав) была как можно меньше. Каждую главу начинают с новой страницы, поэтому толщина тома есть сумма длин глав, вводящих в него. Разрывать главы нельзя. Если есть несколько равноценных оптимальных решений, вывести любое из них.
Вариант 1.В=2, 2<С<500, суммарное число страниц не больше 104.
Вариант 2. 3 <В<40, В< С<250, суммарное число страниц не больше 3-104.
Вход. В первой строке текста— количества С и В, в следующих С строках — длины глав.
Выход. В первой строке — максимальная толщина тома, в следующих В строках — номера первой и последней глав каждого тома. Например, если пять глав имеют длины 300, 300, 500, 300, 300 и распределяются по трем томам, то максимальная толщина тома — 600, первый том начинается главой 1 и заканчивается 2, второй включает только главу 3, третий — главы 4 и 5.
13.6.	Входной текст состоит из слов с известными длинами (количеством символов) /р 1г,..., 1п и представляет абзац. Его нужно “правильно отформатировать” и вывести в несколько строк длиной М символов (M>maxl). Форматирование заключается в следующем. Если в строке размещаются слова с /-го по j-e, то между ними вставляется по одному пробелу и вычисляется остаток М- j+ i- (lt+ ...+Г), который должен быть неотрицательным. Нужно минимизировать сумму кубов остатков по всем строкам, кроме последней.
13.7.	“Дыра” и отрезки на прямой заданы целыми координатами своих концов. “Дыру” нужно закрыть с помощью отрезков; их суммарная длина должна быть минимальной.
Вход. “Дыра” и отрезки задаются в тексте: его первая строка содержит числа L и U (координаты левого и правого концов “дыры”), следующие строки — пары чисел А, и Bi (0< L< U< 1000,0< А,< В< 1000). Отрезков не больше 100.
Выход. Если “дыру” можно закрыть, то в первую строку текста вывести сумму длин использованных отрезков, а в следующие строки — пары координат в порядке возрастания координат левых концов использованных отрезков. Если дыру закрыть нельзя, то в первую строку вывести 0. Если решений несколько, вывести любое.
13.8.	Строку S, состоящую из десятичных цифр, можно разделить на непустые последовательные подстроки 50, SltS2, ... (если записать их подряд, получим S). Каждая подстрока St задает записанное ею число аг Из чисел, заданных подстроками, образуется многочлен
а0 + Д1*х + а2*^ + ...
Нужно разбить строку S так, чтобы при заданном значении х этот многочлен имел минимальное значение (решений может быть несколько).
386
ГЛАВА 13
Вход. Первая строка текста содержит строку S длиной не больше 100, вторая— значение* (0,01<х<99,9). Точность, которую обеспечивает тип extended, считать достаточной.
Выход. В первой строке текста — минимальное значение многочлена, во второй — последовательность подстрок, разделенных пробелом.
13.9.	В условиях задачи 13.8 докажите, что, если нужно просто минимизировать число строк, то жадный алгоритм гарантированно дает наилучший ответ и будет оптимальным по времени работы.
13.10.	Шефу нужно выбрать, какие заседания он посетит (см. задачу 12.1). Каждое заседание имеет интервал и “важность” с' нужно максимизировать сумму важностей выбранных заседаний. Если возможны разные наборы с одинаковой суммарной важностью, выбрать тот, где меньше суммарная длина заседаний. Если одинаковы и сумма важностей, и сумма времен, выбрать любой из наборов.
Вход: число заседаний N, затем Nтроек (a,, Ьр с).
Выход: в первой строке через пробел суммарные важность и время вы-• бранных заседаний, во второй — сами заседания (формат их представления см. в задаче 12.1).
13.11.	Решите методом динамического программирования задачу 8.6.
' Глава 14
Игры двух лиц
В этой главе.
♦	Примеры антагонистических игр с полной информацией
♦	Игры с известной стратегией
« Выигрышные и проигрышные позиции
♦	Использование рекурсии и таблиц ходов
♦	Числовое оценивание позиццй
Игры, в которых участвуют два игрока, знакомы нам с детства — от крестиков-ноликов на поле 3x3 до шашек и шахмат. Как правило, такие игры являются антагонистическими — выигрыш одного игрока означает проигрыш другого. Игры также бывают с полной и с неполной информацией — в зависимости от того, все ли известно игроку о позиции своей и соперника. Примеры игр с полной информацией — варианты крестиков-ноликов, шахматы или шашки, с неполной — домино или карточные игры, в которых соперник скрывает, что у него “на руках”.
Серьезные игры вроде шахмат с самых первых ходов имеют очень много вариантов продолжения. Из-за этого для них очень трудно сформулировать “алгоритм игры”, придерживаясь которого, игрок может выиграть. Именно отсутствие таких алгоритмов делает эти игры интересными для соревнования людей и чрезвычайно сложными для программирования. Хотя нельзя не отметить, что уже в 2005 и 2006 годах наилучшие шахматные программы в матчах против чемпионов мира убедительно переигрывали людей.
В процессе игры ее участники, как правило, по очереди совершают ходы, благодаря которым образуются позиции в игре. Варианты процесса любой игры обычно можно представить корневым ориентированным деревом (иногда — направленным графом). Узлы в этом дереве представляют позиции (корневой узел — исходную позицию, узлы-листья — заключительные позиции, из которых нельзя сделать ход). Дуги дерева представляют ходы.
В реальных играх деревья позиций разветвляются очень широко, поэтому поиск хода с помощью перебора, т.е. обхода такого дерева, оказывается очень длительным. Для сокращения перебора существуют общие способы, аналогичные методу ветвей и границ, однако не они будут предметом нашего внимания.
Игры с полной информацией, представленные в данной главе, являются играми с известной стратегией. Это значит, что один из игроков, выбирая право первого хода и каждый свой ход некоторым не очень сложным способом, может гарантировать себе выигрыш при любой игре соперника (естественно, в пределах правил). Главное в этих играх — найти методику выбора ходов, гарантирующих выигрыш, и реализовать ее в программе по возможности эффективно.
388
ГЛАВА 14
В решениях задач данной главы отсутствует защита от ходов игрока, нарушающих правила игры. Добавьте такую защиту к решению каждой задачи самостоятельно — в большинстве ситуаций это довольно просто. Защита должна, как минимум, игнорировать ход соперника, нарушающий правила игры.
14.1. Анализ позиций и выбор хода
14.1.1. Выигрышные и проигрышные позиции
Задача 14.1. Касса содержит С копеек. Два игрока поочередно забирают из кассы от 1 до М копеек. Выигрывает игрок, после хода которого касса станет пустой. Программа должна читать положительные числа С и М (от 1 до 104), выбирать право первого хода и выигрывать.
Пример. При С= 17 и М=9 последовательность ходов (первый — 5, второй —
6, первый — 6) означает выигрыш первого игрока, поскольку 17-5-6-6=0.
Анализ и решение задачи. В любой игре присутствует такое понятие, как позиция Неформально говоря, это совокупность текущих параметров игры, которую изменяют игроки своими ходами и по которой в конце концов определяется выигрыш, кроме того, позиция должна однозначно задавать возможные ходы из нее.
В данной задаче позицией естественно считать остаток в кассе R и номер игрока, имеющего право хода.
Предположим, что сейчас наш ход. Начнем с ситуаций, в которых остаток R мал. Если R=Q, мы уже проиграли. Если R=1, R=2, R=M, у нас есть победный ход — число R, после которого сумма станет равной С. Если R=М+1, то любой наш ход сделает R равным одному из чисел 1,2,..., М, и тогда победный ход сделает соперник.
Итак, остатки R=0 и R=M+1 характеризуют проигрышную позицию. Каким бы ни был наш ход, соперник выиграет (конечно, если не допустит ошибки, но не стоит на это надеяться...). Вто же время, в позициях с R = l,2,... или М есть победный ход.
Продолжим анализ. Если R=M+2, М+3, ... или А/+Л/+1, то наш соответствующий ход 1, 2, ..., М приведет соперника к проигрышной позиции с R=M+1. Значит, в этих позициях можно выиграть. Но если R=2M+2, любой наш ход приведет в позицию с R в пределах от М+2 до 2М+1, начиная с которой выиграет соперник...
По этим наблюдениям нетрудно догадаться, что позиции с R=0, М+1, 2(М+1), 3(М+1), ... являются проигрышными, а во всех остальных существуют ходы, “загоняющие” соперника в одну из этих проигрышных позиций, т.е. остальные позиции — выигрышные.
Итак, при Cmod(A/+l)^0 исходная позиция является выигрышной, и наш первый ход — число Cmod(A/+l). Иначе позиция проигрышная, и пусть начинает соперник. На каждый ход А соперника нужно отвечать ходом М-Р1-А. Программа, реализующая эти соображения, приведена в листинге 14.1.
Листинг 14.1. Простейшая игра в сумму
program SimpleGame;
var с,	{ исходная сумма }
m : integer; { максимальный ход }
ИГРЫ ДВУХ лиц
389
г,	{ текущий остаток }
move : integer; { ход }
ml : integer; { m+1 }
begin
write(1 Введите сумму и максимальный ход (integer)»');
readln(c, m); «
г := c; ml := m+1; { инициализация }
writein('Макс, ход: 1, m, ', остаток = ', г);
if г mod ml о 0 then begin
move := r mod ml;
decfr, move);
writein('Мой ход>', move);
writein('Макс. ход: ', m, ', остаток = ', г) ;
end;
while r > 0 do begin
write('Ваш ход»'); readln(move);
dec(r, move);
writein('Макс. ход: ', m, ', остаток =	г);
move := ml - move;
dec(r, move);
writein('my move»', move);
writein('Макс. ход: ', m, остаток = ', г);
end;
writein('Простите, вы проиграли.'); end.
Уточним понятия, возникшие в рассмотренной задаче. Они относятся к общей теории, помогающей в построении выигрышных стратегий для довольно широкого класса игр.
Выигрышная позиция — это позиция, начиная с которой можно, играя правильно, гарантированно выиграть при любой игре соперника. Проигрышная позиция — позиция, начиная с которой выиграть невозможно (если соперник не допустит ошибки). Иными словами, в выигрышной позиции либо игрок уже выиграл, либо хотя бы один ход приводит в проигрышную позицию, обеспечивая выигрыш при любой игре соперника. В проигрышной же позиции либо игра уже проиграна, либо нет ни одного хода, обеспечивающего выигрыш (при правильной игре соперника), т.е. любой ход приводит в выигрышную позицию. ---------------------------------------------------------------------
Наконец, рассмотрим общую схему, которую может иметь программа или подпрограмма для любой антагонистической игры с известными выигрышными и проигрышными позициями (сравните ее с листингом 14.1).
создать начальную позицию; отобразить позицию;
if позиция выигрышная then begin найти и выполнить свой ход; отобразить ход;
отобразить позицию;
end;
390
ГЛАВА 14
while позиция незаключительная do begin
получить ход от игрока;
выполнить ход игрока;
отобразить позицию;
найти и выполнить свой ход;
отобразить свой ход;
отобразить позицию;
end;
отобразить сообщение о своей победе.
Главное — уметь определять, выигрышна ли текущая позиция, и находить ход, после которого сопернику каждый раз достается проигрышная позиция. А уточнить пункты этой схемы конкретными операторами и подпрограммами совсем несложно.
Проверять, правилен ли ход игрока, необходимо при его задании игроком. Отображать заданный игроком ход, как правило, не нужно — это происходит во время задания. В некоторых задачах ход программы определяется как результат анализа, является ли позиция выигрышной. Тогда пункт найти и выполнить свой ход должен включать анализ позиции. Примеры приведены в следующих подразделах.
Еще одно замечание. Если вам все-таки досталась проигрышная позиция, а возможности отдать ход сопернику нет — как ходить? В игре с идеальным соперником любой ход приведет к проигрышу, но можно пытаться “продержаться как можно дольше”. Если же соперник не является идеальным, то перед каждым ходом нужно проверять, не досталась ли нам выигрышная позиция, и в этой ситуации “перехватывать инициативу”.
Однако представленной выше теории проигрышных и выигрышных позиций не всегда достаточно. Существуют антагонистические игры (в том числе и с “бинарным” результатом “выиграл/проиграл”!), анализ которых требует оценивать позиции точнее, чем просто как выигрышные или проигрышные. В частности, могут оказаться полезным^ так называемые числа Спрэга—Гранди (Sprag, Grundy). Определение и примеры применения этих чисел можно найти, например, в работе [2], однако в ней подробно рассмотрены только игры, для анализа которых в действительности достаточно “бинарной” проигрышности или выигрышное™.
В данной книге этот материал также не представлен, да и источников, где он был бы разъяснен достаточно обширно и понятно, на момент написания книги авторы, к сожалению, указать не могут. Можем лишь посоветовать сначала изучить игры, разобранные тут, а затем поискать материалы с более глубокой теорией в Интернете...
14.1.2. Золотое сечение
Задача 14.2. Есть две кучки спичек. Два игрока берут из них спички цо очереди. За один ход игрок берет из неменьшей кучки ненулевое число спичек, кратное числу спичек в другой кучке. Выигрывает тот, ктц взял последнюю спичку в одной из кучек. Программа должна реализовывать выигрышную стратегию, в частности, решать, кто должен ходить первым.
Пример. Если в кучках 1 и 3 спички, выигрывает первый игрок, забрав 3 спички. Если в кучках 2 и 3 спички, то первый игрок может взять только 2 спички из второй кучки, после чего в позиции (2,1) второй игрок забирает 2 списки и выигрывает.
ИГРЫ ДВУХ лиц
391
Вход и выход. На клавиатуре задаются два положительных целых чисел (типа integer), обозначающих количества спичек в кучках. Ход задается числом, кратным меньшему из чисел в текущей позиции. После каждого хода выводится полученная позиция (два числа).
Анализ задачи. Поищем способ определения, является ли заданная позиция выигрышной. Пусть в позиции (а, Ь) первое число не меньше второго. Если Ь=0, то позиция является проигрышной, иначе, если а кратно Ь, — выигрышной.
Пусть а не кратной, т.е. a = kb+r, где r= amodb>0. Из позиции (а,Ь) можно перейти в позиции (r,b), (b+r,b), (2b+r,b), ..., ((£-1)й+г,й). По определению, позиция (а, Ь) является выигрышной тогда и только тогда, когда хотя бы одна из этих позиций проигрышная. Но это можно определить рекурсивно! Поскольку во всех этих позициях первое число строго меньше а, рекурсия обязательно достигнет “дна”, на котором одно из чисел равно 0 или одно из них кратно другому.
Однако такая рекурсия очень неэффективна из-за многократных рекурсивных вызовов. Можно оптимизировать время работы за счет памяти, используя табличную технику (см. главу 13 и дальнейшие задачи в данной главе). Но для этой игры есть способ получше. Докажем, что позиции (2й+г, й),..., ((Л-1) й+г, й) обязательно выигрышные, т.е. их можно не анализировать.
► Предположим, что одна из указанных позиций, скажем, (/й+-г,й), где 2</<йи г<й, проигрышная. Но из проигрышной позиции любой ход ведет в выигрышную позицию. Значит, и (г, й), и (й+r, й) являются выигрышными. Но тогда из выигрышной позиции (й+г, й) единственный допустимый ход й приводит в выигрышную же позицию (г, й), что невозможно по определению выигрышной позиции. Полученное противоречие доказывает, что позиция вида (lb+r, b) при />2 является выигрышной. Отсюда следует: при а/Ь> 2 позиция (а, Ь) выигрышная. Кроме того, из двух позиций (г, й) и (й+r, й) одна, и только одна, является выигрышной. <
Итак, если й<а<2 й (неравенства строгие), то достаточно только рекурсивно выяснить, является ли позиция (г,й), где r=amodb>0, проигрышной. Если является, то позиция (а, й) выигрышная, иначе проигрышная.
Нужно еще правильно выбрать ход при а/Ь>2. Если позиция (г,й) проигрышная, то в нее ведет ход а-атос!й, иначе проигрышной обязательно будет позиция (й+r, й), в которую ведет ход a -a mod й - й.
Эти соображения избавляют от повторных рекурсивных вызовов и существенно сокращают работу.
Решение задачи. Оформим решение задачи с помощью трех подпрограмм. Процедура makeMove реализует собственно изъятие спичек из большей кучки. Функция detwin получает при вызове количества а и b спичек в кучках, возвращает признак того, что данная позиция выигрышная, и в параметре-переменной сохраняет выигрышный ход (или наименьший возможный ход, если позиция проигрышная).
Процедура game получает исходную позицию и вызывает функцию detwin, чтобы определить, является ли эта позиция выигрышной. Если это так, она делает первый ход. Далее в цикле соперник и программа делают по одному ходу. Поскольку соперник все время загоняется в проигрышную позицию, в которой у него есть только один допустимый ход, дальнейшие вызовы detwin не нужны.
392
ГЛАВА 14
Решение представлено в листинге 14.2.
Листинг 14.2. Решение, основанное на рекурсивном анализе var a, b : integer; { исходные числа } procedure makeMove(var a, b, с : integer); Begin if a > b then dec(a, c) else dec(b, c);
End;
function detWin(a, b : integer; var c : integer) : boolean; var t : integer;
Begin
if a < b then begin t:=a;a:=b;b:=t end;
{ гарантированно a >= b } if b = 0 then begin detWin := false; c := 0 end else if (a mod b = 0) then begin detWin := true; c := a end else if (a div b >= 2) then begin detWin := true;
if not detWin(b, a mod b , c) then c := a - a mod b else c := a - a mod b - b end else if not detWin(b, a mod b , c) then begin c := a - a mod b; detWin := true end else begin c := b; detWin := false;
end;
End;
procedure game(a, b : integer); var c : integer;
Begin
writein('Позиция: ', a, 1 ', b);
if detWin(a, b, c) then begin makeMove(a, b, c); writein(’Мой ход>', с);
writein(1 Позиция: 1, a, 1 1, b);
end;
while (a <> 0) and (b <> 0) do begin write('Ваш ход>1); readln(c);
{ Здесь нужно добавить проверку правильности хода } makeMove(а, b, с);
writein(1 Позиция: ', а, ' ', Ь); if detWin(a, b, с) then begin
ИГРЫ ДВУХ лиц
393
makeMove(a, Ь, с);
writein('Мой ход>', с);
writein(1 Позиция: ', а, 1 1, Ь);
end;
end;
writeln(1 Проспите, вы проиграли.');
End;
Begin
readln(a, b); game(a, b) ;
End.
H Добавьте проверку правильности хода соперника.
Решение без рекурсии. Рассмотрим выигрышную позицию (2,1); большее число будем указывать в позиции первым. Эта позиция достигается ходом 2 из проигрышной позиции (3,2), а та, в свою очередь, ходом 3 из выигрышной позиции (5,3). Следующей в этом движении назад будет проигрышная позиция (8,5), затем выигрышная (13,8). Стоп! Каждая из них, имея вид (а,Ь), достигается ходом а из позиции (а+b, а). Числа 2 и 1 — это числа Фибоначчи, а для любых последовательных пар (с, а) и (а,Ь) верно, что с=Ь+а. Значит, в парах находятся последовательные числа Фибоначчи.
Нетрудно убедиться, что отношение соседних чисел Фибоначчи через раз больше и через раз меньше золотого сечения Ф1, равного
1,618034:2/1 >Ф, 3/2 <Ф, 5/3>Ф, 8/5<Ф и т.д.
2
Значит, в выигрышных позициях с соседними числами Фибоначчи отношение чисел больше Ф, а в проигрышных — меньше Ф. Этот пример подводит нас к такому предположению.
• Позиция (а, Ь), где a S Ь, является выигрышной тогда и только тогда, когда а = Ъ или а/Ь > Ф.
Докажем это утверждение.
► (=>) Вспомним первое решение. На “дне” рекурсии находилась выигрышная позиция (а, Ь), в которой а/b > 2, т.е. а/Ь > Ф (ситуация а=Ь в качестве выигрышной позиции невозможна в рекурсивных вызовах). За один ход а до этой позиции, т.е. в предыдущем вызове рассматривалась проигрышная позиция (а+Ь,а), за два хода — выигрышная и т.д. Заметим: если а/Ь>Ф, то (а+Ь)/а<Ф. Действительно,
(а+Ь)/а = 1+Ыа < 1+1/Ф = Ф.
Аналогично, если а/Ь<Ф, то (а+Ь)/а>Ф. Отсюда для всех проигрышных позиций (а,Ь) выполняется а/Ь < Ф, а для выигрышных — а/Ь > Ф.
(4=) Пусть а/Ь>Ф в позиции (а,Ь). Если а/Ь>2, то позиция выигрышная (это доказано в первом решении). Пустьа = Ь+г, где r=emodb>0. В позиции (а,Ь) есть только один ход Ь,
1 Обозначение Ф связано с именем древнегреческого скульптора Фидия — он использовал золотое сечение в пропорциях многих своих скульптур.
394
ГЛАВА 14
и он приводит в позицию (Ь, г) с Ыг<Ф. Из такой позиции (Ь, г) есть только один ход в позицию (b-r,r), в которой (Ь-г)/г>Ф. Таким образом, из позиции с отношением чисел больше Ф переходим в позиций) с отношением меньше Ф, а еще через шаг — в позицию с отношением больше Ф.
Каждый раз одно из чисел уменьшается, поэтому на некотором шаге получим позицию вида (к-b, Ь) с к>2. У нее отношение чисел больше Ф, поэтому она получена через четное число шагов. Но она выигрышная, поэтому и исходная выигрышная. <
Для реализации описанного способа не обязательно использовать само число Ф. Нетрудно убедиться, что для позиции (а,Ь) условие а/Ь>Ф равносильно тому, что a/b>b/(amodb) или a/b > b/(b+a mod Ь). В программе от неравенств с дробями перейдем и неравенствам с произведениями. Эти соображения реализованы в следующей процедуре.
function detWin(a, b : longint; var с : longint) : boolean;
var t : longint;
begin
toSwap := false;
if a < b then begin
t := a; a := b; b := t; toSwap := true;
end;
{ a >= b }
if b = 0 then begin
detWin := false; c := 0
end
else
if a mod b = 0 then begin
detWin := true; c := a
end
else
if (a*(a mod b) > b*b) or (a*b > b*(b + a mod b)) then begin detWin := true;
if (a mod b)*(b + a mod b) > b*b then c := a - a mod b
else c := a - a mod b - b;
end
else begin
C := b; detWin := false;
end;
end;
►> Напипщте программу с учетом того, что в первой строке текста ' heapprop. txt1 записано количество тестов nTest, а следующие nTest строк содержат псепаре целых положительных целых чисел (типа integer), обозначающих количества спичек в кучках. Для каждого теста выводится исходная позиция, а далее после каждого хода выводится текущая позиция (два числа).
14.1.3.	Ним
Полный анализ этой игры в 1901 году опубликовал профессор Гарвардского университета Чарлз Бутон. По одной из версий, он назвал эту игру в честь старинного
ИГРЫ ДВУХ лиц
395
французского города. Однако авторам кажется более правдоподобным, что Ч. Бутон использовал устаревший английский глагол nim (брать, красть) или повелительное наклонение nimm немецкого глагола nehmen (брать).
Задача 14.3. На столе лежат несколько кучек камешков. Два игрока берут из них камешки по очереди. За один ход игрок выбирает любую кучку и берет из нее любое ненулевое число камешков. Выигрывает тот, кто взял самый последний камешек. Программа должна реализовывать выигрышную стратегию, в частности, решать, кто должен ходить первым.
Пример. Если в кучках 1 и 3 камешка, выиграет первьш игрок. Он берет 2 камешка из второй кучки и оставляет позицию (1,1). Второй игрок может взять из любой кучки только 1 камешек, после чего второй заберет последний камешек из другой и выиграет. Если в кучках по 2 камешка, то выиграет второй игрок. Если первый возьмет 2 камешка из какой-нибудь кучки, то второй заберет 2 из другой и выиграет. Если же первый возьмет 1 камешек из какой-нибудь кучки, то второй — 1 камешек из другой кучки, а с позицией (1,1) уже все ясно.
Вход и выход. От клавиатуры вводится количество кучек п, 1 <п< 10, затем п положительных чисел (типа integer) — количества камешков в кучках. Ход задается номером кучки и количеством забираемых камешков. После каждого хода выводится полученная позиция (и чисел).
Анализ задачи. Анализ позиций с двумя равными кучками прост: сколько бы камешков ни брал первый игрок из какой-либо кучки, второй будет брать столько же из другой кучки и в конце концов выиграет.
Если же кучки не равны, то цель ясна — своим ходом сделать их равными. В этой ситуации выигрывает первый игрок — первым ходом он заберет из большей кучки разницу количеств камешков и оставит второму игроку две равные кучки.
В общем случае решение связано с двоичным представлением чисел. Заметим: если числа равны, то их двоичные представления одинаковы, иначе они отличаются хотя бы в одном разряде. Поэтому поразрядная сумма по модулю 2 (“исключающее или”) двух равных чисел имеет во всех разрядах 0, т. е. равна 0, а разных чисел — отлична от 0.
Результат поразрядного сложения по модулю 2 для краткости будем называть ним-суммой, а само это сложение — ним-сложением. В языке Turbo Pascal оно реализовано операцией хог: 10 хог 5 = 15, 5 хог 6 = 3. Итак, позиция с двумя числами at и а2 является выигрышной тогда и только тогда, когда аг хог а2 * 0. Но если чисел не два, а больше, то этот критерий остается в силе!
• Позиция с числами ai, аг, ...,ап является выигрышной тогда и только тогда, когда а-\ хог аг хог ... хог апФ 0.
К сожалению, доказательство критерия выходит за рамки этой книги.
Рассмотрим примеры. Позиция (1,2,3) проигрышная — двоичные представления чисел 01,10,11 дают в каждом разряде четное число единиц, из-за чего их ним-сумма равна 0. Позиция (5,2,4) выигрышная — эти числа с двоичными представлениями 101,010,100 дают ним-сумму 3 с представлением 011.
Итак, чтобы определить, является ли позиция выигрышной, достаточно вычислить ним-сумму чисел в позиции и убедиться, что она не равна 0. Но нужно еще определить, из какой кучки и сколько камешков взять, чтобы получилась проигрышная позиция.
396
ГЛАВА 14
Вначале найдем, сколько камешков нужно взять. Если выбрать одно из чисел, скажем, а,, и ним-прибавить к нему ним-сумму всех чисел S = a] хог а2 хог... хог ап, то ним-сумма (а хог S) хог а2 хог... хог ап, очевидно, равна 0. Значит, чтобы новая ним-сумма стала равной 0, из а, камешков должно остаться хог S. Например, в позиции (5,2,4) с ним-суммой 3 выберем число 2; 2 хог 3 = 1 — это количество камешков, которые должны остаться во второй кучке. Значит, возьмем из нее 2-1 = 1 камешек и получим позицию (5,1,4) с ним-суммой 0.
Но как выбрать число для ним-сложения с 5? Вообще говоря, ним-прибавление S может увеличить число, например 4 хог 3 = 7 или 5 хог 3 = 6. Однако число должно уменьшиться — ведь камешки нужно не добавлять к кучке, а брать из нее! Но оказывается, что среди чисел а{, а2, ..., ап обязательно найдется хотя бы одно, которое при ним-прибавлении S уменьшится. Убедимся в этом.
Заметим: если двоичные представления двух ним-слагаемых имеют 1 в одном и том же разряде, то в их ним-сумме этот разряд равен 0. Например, 2 хог 3 = 1 (2, 3 и 1 имеют двоичные представления 10,11 и 01 соответственно).
Найдем разряд со старшей единицей ним-суммы S. Тогда, по построению S, среди ар а2, ..., ап есть нечетное количество чисел, у которых в этом разряде 1. Обозначим любое из них через х. В результате ним-прибавления S к х в указанном разряде будет 0, старшие разряды х, если есть, останутся без изменений, а возможные изменения в младших разрядах гарантированно “не перекроют” уменьшения в указанном разряде. Следовательно, ним-сумма х и S окажется меньше х.
Итак, искомым может быть любое из чисел, имеющих 1 в том разряде, в котором находится старшая 1 ним-суммы 5. Чтобы найти его, переберем числа ар а2, ..., ап, пока не найдем ар для которого а хог S<a..
Решение задачи. Приведем только функцию de twin, которая возвращает признак выигрышности позиции, сохраняя в параметрах-переменных номер кучки и количество камешков, представляющих ход из выигрышной позиции. Если позиция проигрышная, ход вычисляется так же, как и для выигрышной позиции, но программа в такой позиции ходить не должна.
Размеры кучек представим массивом num типа alnt = array [l..maxN] of integer, где maxN — имя константы 10. Количество кучек представим параметром п, ход — параметрами-переменными пНеар и decrem. Функция приведена в листинге 14.3.
Листинг 14.3. Функция определения позиции и хода в игре ним function detWln(var num : alnt; n : byte;
var ПНеар : byte; var decrem : integer) : boolean;
var sum,
numCopy : integer;
k : byte;
begin
sum := 0;
for k := 1 to П do sum := sum xor num[k];
detwin := (sum <> 0) ;
k := 1;
while num[k] xor sum >= num[k] do inc(k);
ИГРЫ ДВУХ лиц
397
пНеар к;
numCopy := num[пНеар];
num (пНеар] := num[пНеар] xor sum;
decrem := numCopy - num[пНеар];
end;
H Напишите программу, аналогичную программе в листинге 14.2.
14.1.4.	Таблица ходов
Задача 14.4. Касса содержит С копеек (С> 3). Два игрока по очереди берут из кассы по целому числу копеек. За ход можно взять не меньше одной копейки и не больше удвоенного числа копеек, взятых соперником на предыдущем ходу. Первый ход — одна или две копейки. Выигрывает тот, кто своим ходом опустошит кассу. Программа должна читать С (от 1 до 300), выбирать право первого хода и выигрывать.
Пример. При С*= 3 выигрывает второй игрок — если первый взял 1 копейку, второй берет 2, если первый взял 2 копейки, второй берет 1. При С=4 выиграет первый игрок — взяв 1 копейку, он оставит в кассе 3 копейки перед ходом | второго игрока.	|
Анализ задачи. Ясно, что позиция в данной задаче — это не просто остаток в кассе, а пара (остаток, последний ход). Обозначим эти величины соответственно г и т.
Позиция (г,т) является выигрышной, если существует ход к (от 1 до 2т) в проигрышную позицию (r-к, к), и является проигрышной, если при любом к от 1 до 2т позиция (г-к,к) выигрышная. Это рекурсивное определение имеет “дно”: позиции (1,к) и (2, к) при любом возможном к являются выигрышными, (0, к) — проигрышными.
Чтобы решить, является ли текущая позиция выигрышной или проигрышной, нетрудно реализовать это определение “В лоб” с помощью рекурсивной функции. Однако из-за кратных рекурсивных вызовов такое решение неэффективно. Рекурсия с запоминанием может улучшить его, но проще один раз заполнить таблицу вариантов ходов и использовать ее во время игры.
Используем таблицу tab, строки которой индексированы остатками, столбцы — последними ходами. Значением элемента, соответствующего выигрышной позиции, является один из возможных ходов, которые ведут к выигрышу. Если позиция проигрышная, значением элемента должно быть то, что не может быть ходом — положим его равным 0. Тогда о будет признаком проигрышной позиции, а положительное значение — признаком выигрышной.
Построение таблицы. Очевидно, все элементы первой строки должны иметь значение 1, второй — 2. Заполним остальные строки, инициализировав все их элементы значением 0. Пусть теперь п>3 ит>1. Если n<2w, то в позиции возможен ход п, опустошающий кассу, поэтому tab [n, m] = п. При п>2т переберем все возможные ходы к от 1 до 2т и используем элементы предыдущих строк таблицы. Если tab [n-k, k] = о хотя бы для одного значения к, значит, ход к приводит в проигрышную позицию (п-к,к); тогда tab[n, m] =к. При этом естественно выбрать максимальное значение к, ускоряющее игру. Если же все tab[n-k, к] *0 при всех к от 1 до 2т, tab [n, т] представляет проигрышную позицию и остается равным 0.
398
ГЛАВА 14
Выбор хода с помощью таблицы. Предположим, текущей является позиция (г, т). Если г<2т, то победный ход г является допустимым, и для его определения таблица вообще не нужна. При г>2т, если tab [г, т] > 0, ходом будет tab [г, т], иначе 1 (такой ход в действительности программа никогда не выполняет). Если оформить выбор хода в виде функции, возвращающей булев признак выигрышное™ позиции, то в первых двух ситуациях возвращается true, в третьей — false.
Ширина таблицы. Остается уточнить, какой должна быть ширина таблицы, т. е. каким может быть максимальный ход. Заметам: если последний ход равен т, то все выполненные ходы уменьшили исходную сумму С не менее чем на 2ти-1 копеек (касса уменьшается в точности на 2/п-1, если первый ход равен 1 и каждый следующий ход вдвое больше предыдущего). Отсюда, если достигнута позиция (г,т), то г<С-2т+\. Но при г<2т для определения следующего хода (одного из чисел от 1 до 2т) таблица не нужна. Но таблица нужна, поэтому г>2т. Тогда 2т<С-2т+1, или 2ти<(С+1)/2. Итак, достаточно таблицы шириной (С+1)/2.
н Реализуйте представленное решение.
14.2. Оценивание позиций: максимальная сумма
Задача 14.5. N золотых слитков с различными ценами разложены в ряд. Два игрока по очереди берут несколько слитков в левом конце ряда. Первый ход — один или два слитка, а далее за каждый ход можно взять не меньше одного слитка и не больше удвоенного числа слитков, взятых перед этим соперником. Цель каждого игрока — получить как можно большую сумму цен взятых слитков. Программа должна вычислять, какие суммы могут обеспечить себе первый и второй игроки, как бы ни играл их соперник. Затем она должна играть за игрока, максимальная сумма которого больше, и набирать слитки с суммой не меньше этой максимальной.
Вход. В строке текста записано число N (1 <М< 2 0 0), затем N цен слитков от 1 до 2 0 0 в порядке их расположения в ряду. Числа разделены пробелами.
Примеры. Вход: 4 1 2 4 8. Первый игрок не позволит набрать второму сумму больше 6, если сделает первый ход 1. Тогда второй игрок возьмет два слитка ценой 2 и 4. Первый игрок заберет последний слиток и получит сумму 9. Если первый игрок сделает первый (ошибочный!) ход1 2, то второй заберет слитки 4 8. Итак, играя правильно, первый набирает не меньше 9, второй — не меньше 6, поэтому программа должна ходить первой и набирать сумму не меньше 9. Вход: 4 1111. Каждый игрок может набрать не меньше 2, поэтому не важно, за какого игрока будет играть программа.
Анализ задачи. В данной задаче главной является сумма, которую можно набрать, исходя из позиции. Сумма зависит как от оставшихся слитков, так и от последнего хода, поэтому под позицией будем понимать пару (г, т), где г — номер первого из оставшихся слитков, т — последний ход.
Максимум суммы, которую можно набрать, начиная с позиции (г,т), обозначим через S(r,m), а сумму цен всех слитков, начиная с г-го, — Т(г). Оба игрока стремятся увеличить свою сумму, поэтому игрок получит все, что не сможет забрать его сопер
ИГРЫ ДВУХ лиц
399
ник. Значит, ходить нужно так, чтобы соперник мог набрать как можно меньше. Иными словами, первый ход к, где 1 <,к<2т, приводящий к позиции (г+к,к), нужно выбрать так, чтобы минимизировать максимум суммы, набираемой начиная с позиции (г+к, к). Таким образом,
S(r, т) = Т(г) - min S(r+k,k),	(14.1)
а ход — это значение к, при котором достигается rmn S(r+k, к).
Как видим, максимальная сумма S(r, т) рекурсивно выражается с помощью максимальной суммы с большим аргументом г+к. На “дне” рекурсии находятся ситуации, в которых игрок может взять все оставшиеся слитки. Естественно, он должен их взять, иначе часть возьмет соперник и уменьшит сумму игрока, поэтому S(r, т) = Т(г) при 2m >W+l-r.
Первым ходом первого игрока может быть 1 или 2, поэтому можно считать, что он начинает в позиции (1,1). Вначале нужно определить, кто делает первый ход. Для этого нужно знать 5(1» 1) и Г(1). Если 5(1,1)>Т(1)/2, программа будет ходить первой, иначе отдаст это право сопернику.
Решим задачу, не используя рекурсию. Значения Т(г) для r= 1, 2, ..., N вычислим сразу и запомним в массиве т, поскольку они понадобятся в дальнейшем. Для вычисления 5(1,1) используем таблицу S размерами Nx£, где L — ширина таблицы, которую определим ниже. Заполним таблицу построчно, начав с последней строки — все значения в ней равны цене последнего слитка 7’(2V)- Затем в очередной строке г для каждого возможного значения т, если 2m<N+\-r, то 5(r,m) вычисляется по формуле (14.1), иначе S(r,m) = T(r).
Определив 5(1,1) и право первого хода, программа должна играть, т.е. для каждой позиции (г,т) находить значение к, при котором достигается минимум S(r+k,k) по к от 1 до 2m. Для решения этой задачи достаточно построенной таблицы S.
Найдем необходимую ширину L таблицы при #<200. На первом шаге ход — 1 или 2, на втором — от 1 до 4, на третьем — от 1 до 8 и т.д. С каждым ходом необходимая ширина таблицы удваивается и образуются позиции с максимальной шириной (3,2), (7,4), ..., (63,32). Затем возможные ходы т от 1 до 64 приводят в позиции вида (63+m,m). Из позиции (63+m,m) возможен ход к от 1 до 2m, но при этом 63+т+к должно быть не больше 200. Найдем наибольшее возможное к из того, что
63 + m + 2m > 200 и 63 + (m-1) + 2(m-l) < 200.
Отсюдат=46, 63+т=109, т.е. £^=200-109=91.
Итак, при #<200 достаточно таблицы шириной £=91. Поскольку суммарная цена слитков не больше 200x200 = 40000, элементы таблицы могут иметь тип word и занимать по два байта, а вся таблица уместится в пределах 40 Кбайт.
И Реализуйте представленное решение.
В задаче 14.5 позиции не являются выигрышными или проигрышными. Вместо этого позиция (r,m) имеет числовую оценку S(r,m). Очередным ходом игрок “загоняет соперника” в позицию с минимальной оценкой максимального выигрыша,
400
ГЛАВА 14
которого может достичь соперник своим ходом. Такой способ оценивания позиций и выбора ходов является примером применения метода минимакса, предложенного в 1945 году О. Моргенштерном и Дж. фон Нейманом (см., например, [25, 28]).
Упражнения
14.1.	Предположим, что в игре в сумму (см. задачу 14.1) игрок проигрывает, если своим ходом опустошает кассу. Реализовать выигрышную стратегию.
14.2.	Шахматная доска имеет т горизонталей и и вертикалей; координаты ее нижней левой клетки — (1,1), верхней правой — (m,n). На нижней левой клетке стоит король. Игроки по очереди перемещают его на одно поле вправо, вверх или по диагонали вправо-вверх. Выигрывает тот, кто поставит короля в верхний правый угол доски. Программа должна получать числа тип, определять
право первого хода и выигрывать.
Вход и выход. Вначале на клавиатуре задаются количества горизонталей и вертикалей на доске (два числа от 1 до 20) и выводится начальное положение короля. Ход задается числом 1,2 или 3 — соответственно вправо, вверх или по диагонали. После каждого хода выводится текущее положение короля (двачисла).
14.3.	Первый игрок задает начальное значение переменной А, второй — переменной В (числа 0 или 1). Затем игроки заполняют 10 строк программы, по очереди выбирая номер свободной строки и записывая в нее один оператор следующего вида:
1.	А:=1-А	4. В:=1-В
2.	if В=1 then А:=1-А	5. if А=1 then В:=1-В
3.	if В=0 then А:=1-А	6. if А=0 then В:=1-В
После заполнения строк операторы выполняются. Если в результате А #В, выигрывает первый игрок, иначе — второй. Программа должна играть за второго игрока и выигрывать.
Вход и выход. Игрок указывает номер свободной строки (от 1 до 10) и номер оператора (от 1 до 6), ответный ход программы отображается в таком же виде. После заполнения строк вся программа вместе с начальными присваиваниями выводится на экран и имитируется; при этом отображаются выполненные операторы и значения переменных А и В.
14.4.	Начиная от заданной даты, игроки по очереди называют число и месяц, каждый раз увеличивая или число в месяце, или месяц. Считается, что в феврале 28 дней. Кто назвал 31 декабря, тот: в) выиграл; б) проиграл.
14.5.	Игра в произведение. Два игрока по очереди называют натуральные числа от 2 до 9; на это число умножается произведение всех ранеё названных игроками чисел. Вначале произведение равно 1. Выигрывает игрок, после хода которого произведение станет больше заданного целого числа С. Программа должна читать С (2 < С< 1012), определять право первого хода и выигрывать.
14.6.	Игра в произведение—2. В условия предыдущей игры (игра в произведение) внесены два изменения. Первое: для выигрыша нужно получить произведение, лежащее в диапазоне от С до Л (включительно), а произведение больше R оз
ИГРЫ ДВУХ лиц
401
начает проигрыш. Второе: число очередного хода должно быть взаимно простым с числом предыдущего хода. Программа должна читать два целых числа С и R (2 < C<R 5103), определять право первого хода и выигрывать.
14.7.	Игра Норткотта. Мост состоит из т дорожек по п клеток в каждой. В левой и правой клетках* каждой дорожки находятся два соперничающих барана. За один ход баран проходит вперед по своей дорожке или отступает на любое число клеток, но не может пройти своего соперника или сойти с моста. Бараны ходят по очереди слева и справа. Проиграет та шеренга баранов, у которой нет хода.
14.8.	Игра “Нимбы”. Игровое поле разделено на клетки, образующие прямоугольник. Вначале все клетки поля свободны. Игрок по имени Горз своими ходами занимает клетки по горизонтали (в строках), по имени Верт — по вертикали (в столбцах). На каждом ходу Горз выбирает любую свободную клетку и занимает ее и все клетки в строке, достижимые без пересечения вертикали, занятой Вертом. Верт аналогично занимает клетки в столбцах, не пересекая занятые горизонтали. Начинает Горз. Выигрывает тот, кто займет последнюю свободную клетку.
Программа должна получать размеры поля (количество строк и затем количество столбцов), выбирать игрока, за которого она играет, и выигрывать.
Пример. На следующем рисунке представлены два варианта игры на поле 2x3. В клетках указаны номера ходов, на которых эти клетки были заняты (нечетные обозначают ходы Горза, четные — Верта). На рис. а третьим ходом выиграл Горз, на рис. б — четвертым ходом Верт.
1	1	1		1	1	1
2	3	3		3	2	4
а	б
14-9. в рдц положены к спичек. Два игрока по очереди каждым своим ходом берут К спичек, расположенных рядом (К<Н). Игрок, который не может сделать очередной ход, проигрывает. Программа должна определять право первого хода и выигрывать.
Вход и выход. Вначале задаются N и К (1 <£<№^40). Ход обозначается минимальным из номеров забираемых спичек. На экране отображаются исходная позиция, затем ходы соперников и позиции после ходов. Позиция задается в виде двух строк — первая представляет спички и состоит из символов 1 и О, обозначающих спички и пустые позиции, вторая состоит из цифр 0,1,..., 9, указывающих младшие цифры номеров позиций в строке. Например, при 7V=4, К=2 первый игрок, забрав две средние спички, оставляет второму позицию 10 01, в которой хода нет.
14.10	Тик-так-ту. У первого игрока есть три знака “х”, у второго — три “0”. На первом этапе игры игроки по очереди ставят по три своих знака на клетки поля 3x3. На втором этапе игроки по очереди перемещают свои знаки на одну клетку по вертикали или горизонтали. Выигрывает тот, кто первым займет тремя своими знаками одну из горизонталей, вертикалей или диагоналей. Повторение позиции рассматриваете^ как ничейный результат. Программа должна играть крестиками и выигрывать.
Глава 15
Японский кроссворд
В этой главе...
*	Постановка задачи решения японского кроссворда и основные идеи ее решения
♦	Поочередный анализ строк и столбиков кроссворда
*	Анализ линии на основе конечного автомата
♦	Использование анализа линии при переборе состояний клеток
15.1.	Итерационный анализ линий
15.1.1.	Постановка задачи и основные идеи решения
Читателю наверняка знакома популярная головоломка Японский кроссворд (или Paint by Numbers — картинка, заданная числами). Решением кроссворда является прямоугольная картинка “в монохромном ВМР-формате” — каждая клетка прямоугольника или зарисована, или нет.
Последовательность зарисованных клеток, расположенных подряд в строке илй столбике прямоугольника, называется блоком. Строки и столбики описываются последовательностями длин блоков (считая слева направо для строк и сверху вниз для столбиков). Например, если строка описана числом 3, то в ней зарисованы ровно три клетки подряд, а если столбик задан последовательностью 2 5 3, то он содержит блоки длиной 2,5 и 3, расположенные именно в этом порядке. Между блоками есть хотя бы одна незарисован-ная клетка. Пример решенного японского кроссворда приведен на рис. 15.1.
Задача 15.1. Написать программу, решающую японские кроссворды.
Вход. В первой строке текста japan.dat записано YSz — количество строк прямоугольника. Каждая из следующих YSz строк содержит описание соответствующей строки прямоугольника — количество блоков N. в строке, затем Nt чисел — длины блоков. В следующей, (YSz+2)-ft строке записано количество столбиков XSz, а далее XSz строк содержат описания соответствующих столбиков таблицы (в таком же формате, как для строк).
Гарантируется: YSz и XSz не больше 100, количество строк текста соответствует указанным значениям YSz и XSz, в каждом описании количество длин блоков равно и блоки всегда могут поместиться в строке или столбике. Однако неизвестно, соответствует ли описание реальному японскому кроссворду, в котором решение существует и является единственным.
404
ГЛАВА 15
							2	1		1	1	
			1	3	5	. 7	3	3	6	5	5	6
		6					X	X	X	X	X	X
2	1	1				X	X		X			X
1	1	1				X			X			X
		9		X	X	X	X	X	X	X	X	X
		9		X	X	X	X	X	X	X	X	X
		10	X	X	X	X	X	X	X	X	X	X
	2	2		-	X	X				X	X	
	2	2			X	X				X	X	
Рис. 15.1. Пример решенного кроссворда
• Выход. YSz строк текста japan, sol длиной XSz символов каждая, в которых зарисованные клетки обозначены символом “*” (звездочка), не зарисованные — “. ” (точка). Если входным данным соответствует несколько различных картинок, нужно вывести все решения. После последнего решения вывести строку <end>, после каждого из остальных — строку <next>. Если входным данным не соответствует никакая картинка, вывести строку <no solutions».
Примеры. (Пример слева соответствует рис. 15.1.)
Вход	Выход	Вход	Выход
8	__ ******	2	* .
1 6	** * *	1 1	. *
321 1		1 1	<next>
3111		2	. *
1 9	ф★★★★★★★★★	1 1 4 4	* w
1 10	**********		<end>
222	• * * . , ,		
222	* * * *		
10	<end>		
1 1
1 3
1 5
1 7
223 2 1 3 1 6
215 21 5 1 6
* Эта задача предлагалась в 1992 году на IV Всемирной олимпиаде по информатике с ограничениями XSz £ 8, YSz £ 8. Однако нам нужно, чтобы программа работала быстро при размерах поля до 100x100.
ЯПОНСКИЙ КРОССВОРД
405
Поиск способа решения. В первую очередь в голову приходит мысль о переборе вариантов. Ее простейшее применение выглядит так. Пытаясь разместить зарисованные клетки в первой строке, сразу проверяем, соответствуют ли они описанию первой строки. К попыткам разместить клетки во второй строке перейдем лишь тогда, когда найдено даовлетворительное размещение в первой; к попыткам разместить в третьей — только после того, как найдены размещения в первой и второй строках и т.д. до последней строки. Каждый раз, рассмотрев все возможные размещения в следующих строках, необходимо возвращаться к первой строке, пробовать найти другое допустимое размещение клеток и снова рассматривать все возможные размещения в следующих строках.
Описанный способ позволяет разгадывать картинки, соразмерные с рис. 15.1, но не более. Разумнее учесть обе совокупности ограничений (описания строк и описания столбиков) и по мере построения картинки по строкам проверять, соответствует ли построенная часть картинки ограничениям по столбикам (еще не достроив эти столбики до низа).
Реализация алгоритма такого рода привела одного из авторов к несколько лучшим результатам: из более чем 80 кроссвордов размерами приблизительно 30x30 программа разгадала около 70 менее чем за секунду, большинство остальных — за время от минуты до получаса... Но нашлись кроссворды, которые за четыре часа так и не были решены!
Итак, перебор вариантов зарисовывания не дает нужного результата. Обратимся к приемам “ручного” разгадывания.
Пример 15.1. Картинка на рис. 15.1 состоит из 10 столбиков и 6-я строка содержит блок длиной 10. Значит, в этой строке зарисованы все клетки; 4-я и 5-я строки содержат каждая блок длиной 9, и указать точное размещение этого блока пока нельзя. Но все возможные размещения блока (клетки или 1-9, или 2-10) имеют то общее свойство, что клетки 2-9 гарантированно зарисованы. Пока правильный вид всей строки неизвестен, но уже точно известно: эти клетки зарисованы. Аналогично можно зарисовать отдельные клетки в 4-м, 7-м и 10-м столбиках.
Теперь используем предыдущие выводы. Так, в 1-м столбике один блок длиной 1 и одна зарисованная клетка. Несомненно, она и является этим блоком, а остальные клетки гарантированно не зарисованы. Ситуация с блоком длины 3 во 2-м столбике аналогична. В 3-м столбике точное положение блока пока не известно. Но он один и имеет длину 5, а 6-я клетка столбика зарисована. Значит, 1-я клетка не может быть зарисованной. В 1-й строке есть один блок длины 6, а клетки 1-3 гарантированно не зарисованы. Значит, блок должен находиться в пределах с 4-й по 10-ю клетки, т.е. клетки с 5-й по 9-ю гарантированно зарисованы...
На этом остановимся — увлекшись мелочами, можно “за деревьями не увидеть леса”. 
Прежде чем двигаться дальше, введем несколько терминов. Каждая клетка может находиться в одном из трех состояний: или зарисованная, или гарантированно не зарисованная, или такая, о которой пока ничего не известно. Состояния “зарисованная” и “незарисованная” окончательны, а состояние “пока неизвестно” в процессе решения должно быть изменено на одно из первых двух.
Строки и столбики кросворда равноправны с точки зрения их обработки, поэтому и строки, и столбики будем называть линиями; столбик и столбик или строку и строку — однотипными линиями, столбик и строку—разнотипными линиями.
406
ГЛАВА 15
• Равноправность строк и столбиков будет учтена в процессе разработки программы — отдельных подпрограмм для работы со строками и для работы со столбиками не будет, подпрограммы будут работать с линиями.
Из примера 15.1 извлечем идеи, пригодные для алгоритмизации.
1.	По последовательности длин блоков отдельно взятой линии обычно нельзя определить окончательные состояния всех клеток этой линии, но нередко можно выяснить состояния некоторых клеток.
2.	Чтобы начать решать японский кроссворд таким способом, необходимо иметь линии, у которых окончательные состояния некоторых клеток можно найти по длинам блоков только этой линии.
3.	Для продолжения решения дополнительно можно использовать линии, у которых на предыдущих шагах найдены окончательные состояния некоторых клеток.
4.	Линии, которые нельзя использовать согласно п. 2, могут стать пригодными к использованию согласно п. 3 только при условии, что окончательное состояние клетки найдено за счет анализа линии другого типа, проходящей через эту клетку.
На основе этих соображений можно провести последовательный анализ отдельных линий кроссворда. Не уточняя пока, в чем заключается анализ линии, опишем общую схему наших действий.
Вначале проанализируем строки, например, сверху вниз. При этом, возможно, появятся столбики, в которых изменились состояния некоторых клеток. Учитывая эти изменения, проанализируем столбики, например, слева направо. Возможно, в некоторых строках изменятся состояния клеток, поэтому появится смысл эти строки проанализировать вновь. Выполнив этот анализ, получим уточненные состояния клеток в некоторых столбиках и т.д.
Каждый анализ линии или приводит к изменению состояния хотя бы одной клетки, или не приводит. Если на некотором шаге — проходе по строкам или по столбикам — происходят изменения, то состояние хотя бы одной клетки становится окончательным, поэтому общее количество шагов не больше количества клеток XSz*YSz.
Если на некотором шаге ни одна линия не изменилась, значит, следующий шаг анализа линий другого типа даст такие же результаты, как и предыдущий шаг их анализа. В этой ситуации можно останавливаться — никаких новых уточнений уже быть не может.
Возможно также, что при анализе некоторой линии обнаружено противоречие, говорящее о том, что решения нет, — тогда остановимся и выдадим соответствующее сообщение.	»
Если противоречия не были обнаружены, то по окончании работы уточнены состояния либо всех клеток, либо не всех. В первой ситуации получено решение. Вторая ситуация возможна, например, если кроссворд имеет более чем одно решение (и не только при этом условии, как мы увидим ниже). Тогда придется перебирать состояния клеток, оставшиеся неуточненными. Но если таких клеток немного, перебрать их состояния существенно легче, чем провести перебор “с чистого листа”.
Уточним представленную схему решения в следующих подразделах.
ЯПОНСКИЙ КРОССВОРД
407
15.1.2	. Ввод, вывод и основные структуры данных
• В первую очередь обеспечим чтение входа и вывод решения, уточнив при этом часть структур для представления данных в программе.
Линейный размер поля по условию не больше 100, поэтому введем константу MAXSZ = 100. Максимальное количество блоков достигается, когда линия “вплотную заполнена” блоками длины 1, поэтому их максимальное число maxblocks = (MAXSZ +1) div2. Можно было бы написать и MAXBLOCKS = 50, но тогда при возможном изменении MAXSZ пришлось бы изменять вручную MAXBLOCKS, а так соответствие поддерживается автоматически. Размеры поля, заданные на входе, запомним в переменных XSz и YSz типа byte.
Рассмотрим представление информации о блоках в линиях. Линию представим парой значений — булевым признаком kind (true для строки, false для столбика) и номером number.
♦ Булев признак линии позволяет легко выразить понятие “линия другого типа” (см. п. 4 выше): если тип данной линии — kind, то “другой тип" — not kind.
В представлении линии нужно хранить информацию о ее блоках. Линии могут иметь различные количества блоков, поэтому представим линию записью, поля которой — количество блоков N и массив длин блоков линии Ь1_1еп. Таким образом, представим линии с помощью следующего типа:
type LineDescript = record { представление линии }
N : byte;
Ы_1еп : array[1..MAXBLOCKS] of byte;
end;
Информацию о линиях представим в двумерном массиве Lines; его элементы индексированы признаками (строка или столбец) и номерами.
var Lines : array [boolean, 1..MAXSZ] of LineDescript;
Продолжим изучение условия и схемы решения задачи (см. предыдущий подраздел). После шага анализа, например, строк нужно знать, в каких столбиках произошли изменения. Информацию о линиях, которые нужно анализировать, будем запоминать в следующем массиве:
var need_refresh : array [boolean, 1..MAXSZ] of boolean;
Если линию [kind, i] нужно анализировать, элемент need_refresh [false, i] должен иметь значение true, иначе значение false.
Ясно также, что нам нужно помнить и изменять состояния клеток всего кроссворда. Для этого естественно использовать следующий массив:
var pict : array [1..MAXSZ,1..MAXSZ] of byte;
В этом массиве состояния клеток (“не зарисована”, “зарисована”, “неизвестно”) представлены значениями элементов 0,1 и 2 соответственно.
Наконец, рассмотрим вывод решений. По условию, их может быть больше одного, и неясно, как оценить их количество. Поэтому проще выводить их по мере нахождения, а не после того, как найдены все.
408
ГЛАВА 15
После решения нужно вывести строку <end> или <next> в зависимости от того, последнее оно или нет. Однако, когда решение найдено, не известно, последнее ли оно. Поэтому после решения не будем выводить ничего, перед каждым решением, кроме первого, выведем строку <next>, а перед завершением программы, если хотя бы одно решение было найдено, выведем <end>, иначе <no solutions:».
Для реализации этого плана нам понадобится булев признак sol_found того, что решение найдено. Кроме того, выходной файл будет использоваться не только в подпрограмме вывода, поэтому переменная f out типа text будет глобальной, как и все переменные, указанные выше.
Итак, все готово, чтобы запрограммировать чтение входа, инициализацию клеток кроссворда и печать решения (листинг 15.1).
Листинг 15.1. Инициализация
procedure Init;
var fv : text; { входной текст }
i, j : byte; { номер столбика и строки }
Begin
assign(fv, 'japan.dat'); reset(fv);
readln(fv, YSz); { ввод данных о строках }
for i := 1 to YSz do begin
read(fv. Lines[true, i],N);
for j := 1 to Lines[true, i].N do
read(fv, Lines[true, i].bl_len[j]);
need_refresh[true, i] := true; { строку будем анализировать } readln(fv);
end;
readln(fv, XSz); { ввод данных о столбиках }
for i := 1 to XSz do begin
read(fv, Lines[false, i] .N);
for j := 1 to Lines[false, i].N do
read(fv, Lines[false, i].bl_len[j]);
need_refresh[false, i] := true; { столбик будем анализировать } readln(fv);
end;
close(fv);
{ инициализация состояний клеток кроссворда }
for j := 1 to YSz do
for i := 1 to XSz do
pict[j, i] := 2;	( вначале все клетки не определены, }
sol_found := false; { а решение не найдено } assign(fout, 'japan.sol'); rewrite(fout);
End;
В процедуре вывода решения предусмотрим возможность, не указанную в условии — вывод знака ?, если состояние клетки не определено (листинг 15.2).
Листинг 15.2. Вывод решения
procedure Outputsolution;
var i, j : byte; { номер столбика и строки } outchar : char; { выводимый символ }
ЯПОНСКИЙ КРОССВОРД
409
Begin
if sol_found then writein(fout, '<next>');
sol_found := true;
for j := 1 to YSz do begin
for i := 1 to XSz do begin
case pict bj , i] of
1 : outchar :=
0 : outchar := 1.1 ;
else outchar :=
end;
write(fout, outchar);
end;
writein(fout);
end;
End;
н Напишите программу с необходимыми объявлениями и представленными выше подпрограммами, которая читает вход и отображает исходное состояние поля кроссворда.
15.1.3	. Реализация итерационного анализа линий
Реализуем итерационный анализ линий (ИАЛ), описанный в подразделе 15.1.1. Обязанность анализировать линию возложим на процедуру AnalyzeLine, параметризованную линией (точнее, ее признаком и номером). AnalyzeLine вызывается для каждой линии на первых двух шагах, т.е. проходах по всем строкам и по всем столбикам. На каждом следующем шаге она вызывается для тех линий, у которых на предыдущем шаге изменилось состояние хотя бы одной клетки.
•	Изменение состояния i-й клетки линии означает, что изменяется клетка в i-й линии противоположного типа. Это изменение при выполнении процедуры AnalyzeLine запомним в глобальном массиве need_refresh. Его элемент, соответствующий i-й линии другого типа, получает значение true и используется на следующем шаге, т.е. проходе по линиям противоположного типа (см. п. 4 выше).
•	Когда процедура AnalyzeLine анализирует линию (kind, i), она присваивает false элементу массива need refresh [kind, i], поскольку нет смысла повторно анализировать линию, из которой только что получена вся информация (см. также п. 4 выше).
Пары шагов (по строкам и по столбцам) запишем в общем цикле repeat-until. Каждый раз, анализируя линию, будем присваивать значение true переменной si и используем условие not si в качестве условия выхода из цикла (листинг 15.3).
Листинг 15.3. Процедура итерационного анализа линий
procedure IterateLineLook;
var i : byte; { номер строки или столбика }
si : boolean; { признак анализа линий на шаге }
Begin
repeat
si := false;
410
ГЛАВА 15
{ шаг анализа строк }
for i := 1 to YSz do
if need_refresh[true, i] then begin AnalyzeLine(true, i);
si := true;
end;
{ шаг анализа столбиков }
for i := 1 to XSz do
if need_refresh[false, i] then begin AnalyzeLine(false, i);
si := true;
end;
until not si;
End;
Оценка сложности НАЛ. Обозначим через L максимальное из чисел XSz и YSz. Шаги алгоритма повторяются, пока на очередном шаге появляется хотя бы одна клетка с выясненным состоянием. Количество клеток O(L2), поэтому и число шагов имеет оценку 0(1?). На каждом шаге просматриваются O(L) значений в массиве need_ref resh и для некоторых из них анализируются линии. Значит, сложность всех действий без анализа линий имеет оценку 0(1?).
Каждая линия анализируется на одном из первых двух шагов и дальше, когда на предыдущем шаге в ней появились клетки с выясненным состоянием. Любая из O(L) линий имеет O(L) клеток, поэтому она анализируется O(L) раз. Обозначим T(L) сложность анализа линии. Тогда суммарная сложность анализа линий — O(l?T(L)).
В следующем разделе представлен алгоритм анализа линии со сложностью T(L) = O(L2), который обеспечивает ИАЛ суммарную полиномиальную оценку сложности 0(1?). Разумеется, все перечисленные оценки являются оценками сйерху; ^ля очень многих конкретных входных данных количество действий оказывается значительно меньше L?.
15.2.	Анализ линии на основе конечного автомата
15.2.1.	Описание линии в виде конечного автомата
Как анализировать линию? Ответ “так же, как в примере, представленном на рис. 15.1” не говорит почти ничего, ведь в примере упомянуты частные случаи, тогда как возможности анализа линии намного богаче, например, если линия содержит несколько блоков большой длины.
Может возникнуть желание сесть и выписать как можно больше различных приемов анализа, а потом их все запрограммировать. Однако, во-первых, такая работа требует уж очень большой аккуратности. Во-вторых, желательно получать из линии всю возможную информацию о ее клетках: если состояние некоторой клетки не выяснено, то не потому что этого не позволяет используемый метод, а потому, что при данных ограничениях возможно как то, что клетка зарисована, так и то, что не зарисована.
Рассмотрим один из методов анализа линии, который на основе известных состояний ее клеток дает всю возможную информацию об остальных клетках. Об этом
ЯПОНСКИЙ КРОССВОРД
411
методе одному из авторов в 2003 году сообщили Илья Посов и Петр Громов, в то время студенты Санкт-Петербургского университета. Метод не требует перебора состояний клеток и имеет полиномиальную оценку сложности.
В основе метода лежит описание линии с помощью конечного автомата (см. раздел 2.2). Пусть линия содержит N блоков, и их длины заданы массивом Ы_1еп. Между каждой парой соседних блоков должна быть хотя бы одна незарисованная клетка, поэтому всего в линии должно быть не меньше, чем М= bl_len[k] +N-1 клеток поскольку между N блоками есть N-1 промежуток). Будем говорить о позициях 1,2,..., М в описании линии.
Определим конечный автомат, состояния которого 1,..., М соответствуют позициям, а состояние о — положению перед началом описания, т.е. оно означает “можно начинать первый блок”. Состояние 1 означает “пройдена первая клетка первого блока”, состояние Ы_1еп[1] — “пройдена последняя клетка первого блока”, Ь1_1еп [1]+1 — “можно начинать второй блок” и т.д. Состояние (^.=lbl _len[k} + N-Y) означает, что пройдена последняя клетка последнего блока. Начальным состоянием автомата является 0.
•	В нашем анализе присутствует понятие “состояние клетки”, поэтому, чтобы избежать путаницы, состояния автомата будем называть вершинами.
Входные символы 1 и 0 обозначают зарисованные и незарисованные клетки. Переходы между вершинами автомата описывают степень продвижения по позициям в описании. Переходы по символу 1 обозначают продвижение по зарисованным клеткам, по символу 0 — по незарисованным клеткам между блоками. Соответственно определим переходы.
•	Изо всех вершин i, кроме означающих, что пройдена последняя клетка блока, есть переход в следующую вершину i+1 по символу 1 (внутри блока клетки должны быть зарисованы).
•	Из вершин, означающих, что пройдена последняя клетка не последнего блока, есть переход в следующую вершину по символу 0 (клетка после блока должна быть незарисованной).
•	Из вершин, означающих “можно начинать блок”, есть переход в них же (петля) по символу 0 (незарисованных клеток между блоками может быть несколько, и они оставляют в вершине “можно начинать блок”).
•	Из последней вершины М исходит петля, отмеченная символом 0 (если пройдена последняя клетка последнего блока, возможные дальнейшие незарисованные клетки оставляют автомат в этой вершине).
Пример графа переходов, построенного по этим правилам, приведен на рис. 15.2.
Q°...	.......	-Q"
1	2	3	4	5	6	7	8	9
Рис. 15.2. Автомат для линии, содержащей три блока с длинами 1, Зи 2
412
ГЛАВА 15
15.2.2.	Обработка линии и уточнение клеток
Конечный автомат, построенный по описанию линии, считывает входные символы, последовательность которых находится в массиве. Вначале автомат находится в вершине 0 (она активна). Он берет значение первого элемента массива, в зависимости от него совершает переход в новую вершину (делает ее активной), берет следующее значение, совершает переход и т.д.
Возможно, переход по очередному входному символу о или 1 в вершине не определен, как, например, переход по символу 1 в вершине, обозначающей, что пройдена последняя клетка блока. Тогда конечный автомат “замирает”, т.е. процесс считывания символов и изменения активных вершин обрывается.
Однако значением клетки может быть и 2, обозначающее “возможен как 0, так и 1”. Переходы по входному символу 2 в автомате не определены, но нам совершенно ни к чему, чтобы при виде 2 автомат “замирал”. Изменим его поведение так, чтобы оно отвечало смыслу символа 2.
Если в активной вершине определен переход только по 0 или только по 1, то автомат обрабатывает символ 2 так, как будто это соответственно 0 или 1. Если в активной вершине определены переходы и по 1, и по о, то входной символ 2 означает, что автомат должен “расщепиться”, т.е. совершить переходы и по 1, и по 0, сделав активными одновременно две различные вершины. Если автомат имеет две активные вершины одновременно, то после следующего символа 2 количество вершин, активных одновременно, может достичь, например, трех, и т.д.
Итак, входной символ 2 обрабатывается одновременно и как 1, и как 0. Поэтому применение автомата можно представить двумерной картинкой (пример на рис. 15.3). Сверху изображен граф переходов автомата (см. рис. 15.2), слева в столбик записан его вход — состояния 10 клеток линии (значения в массиве cel|s). Эти строка и столбик являются заголовками для основного двумерного поля, в котором стрелки представляют процесс работы автомата. Перед анализом клетки линии имеют состояния 1222220222 (столбик слева), а после него — 102112021 2 (столбик справа).
Сначала активна вершина 0 и считывается значение 1 элемента cells [1]. Автомат делает активной вершину 1, что указано наклонной стрелкой. На следующем шаге активна вершина 1 и обрабатывается значение 2 элемента cells [2]. В вершине 1 определен переход только по 0, поэтому 2 рассматривается как 0, и активной становится вершина 2. Но на следующем шаге автомат “расщепляется”, поскольку обрабатывается символ 2 и из активной вершины исходят две разные дуги. Активными становятся две вершины — 2 и 3. На следующем шаге получаем три активные вершины: 2, 3 и 4. И так далее, пока не завершится последовательность клеток линии (см. рис. 15.3).
Для дальнейшего анализа обратимся к понятию пути в автомате. Путь — это последовательность вида (q0, хр qx, х2, q2.хя, qn), где для всех i>0 в вершине qt опре-
делен переход по символу х^., в вершину q^v Говоря неформально, путь — это последовательность стрелок между вершинами, в которой стрелки обозначают переходы, определенные в автомате, и каждая следующая стрелка начинается там, где закончилась предыдущая.
ЯПОНСКИЙ КРОССВОРД
413
Рис. 15.3. Процесс обработки линии с помощью автомата
Итак, путь определяется вершиной qa и последовательностью входных символов х^2.. .хп, отмечающих стрелки. Однако в нашем автомате путь может разветвляться — если в активной вершине qt определены переходы и по 0, и по 1, а символом х+1 является 2. Путь может также обрываться — если в qt не определен переход по х+1, равному 0 или 1.
Заметим: путь продолжается, не разветвляясь, если есть только один способ согласовать состояние очередной клетки с достигнутой позицией в описании линии. Путь разветвляется, если таких способов несколько, и обрывается, когда согласование невозможно. В частности, обрыв пути в последней вершине М соответствуют тому, что пройдены все блоки, а в линии есть еще одна зарисованная клетка.
Нас интересуют только полезные пути, которые начинаются в вершине о и после чтения всей входной последовательности заканчиваются в последней вершине М. Остальные пути обрываются на том или ином шаге из-за противоречия между достигнутой позицией в описании линии и состоянием очередной клетки. Анализ этих путей не позволит правильно уточнить состояния клеток, поэтому они бесполезны.
Часть бесполезных путей можно распознать и отбросить еще до их обрыва. На каждом шаге работы автомата считывается один входной символ, поэтому в поле, изображающем процесс работы, горизонтальных стрелок нет. Отсюда следует, что путь, который привел под диагональ, ведущую в нижний правый угол поля, бесполезен.
Содержательно, если линия длиной L содержит N блоков, а ее описание имеет М позиций, то N-1 символов 0 обязаны разделять блоки, а остальные L-М нулей “свободно” распределены между блоками. Значит, путь выходит под указанную диагональ, если среди прочитанных символов 0 (или 2, принимаемых за о) “свободных” больше, чем L-М. Эта ситуация отражена на рис. 15.3 зачеркнутыми вертикальными
414
ГЛАВА 15
стрелками, ведущими из вершины 2 по пятому входному символу и из вершины 6 по девятому. Как видим, все пути с этими стрелками обрываются.
•	В поле, изображающем процесс работы автомата, полезные пути лежат в пределах полосы шириной L-М, образованной диагоналями, начиная от выходящей из верхнего левого угла поля и заканчивая ведущей в нижний правый угол.
•	Каждая диагональ этой полосы соответствует количеству прочитанных “свободных” символов о или символов 2, принятых за о.
После того как построены пути, по которым проходит обработка входных символов, можно уточнить состояния некоторых клеток линии. Для этого используем обратный ход по стрелкам.
Сначала выделим стрелки, входящие в правый нижний угол, затем стрелки, которые могут быть для них предыдущими и т.д. На рис. 15.3 стрелки, составляющие полезные пути, выделены и отмечены символами, по которым были проведены соответствующие переходы.
Выделив полезные пути, восстановим окончательные (1 или о) состояния как можно большего количества клеток, находившихся до анализа в состоянии “неизвестно” (входной символ 2), с помощью следующих правил.
•	Если в некоторой строке все стрелки полезных путей отмечены одним и тем же символом (или о, или 1), то он и обозначает окончательное состояние этой клетки.
•	Если же в одной строке встречаются стрелки с символами и о, и 1, значит, есть как полезные пути, означающие “эта клетка может быть не зарисована”, так и полезные пути, означающие “может быть зарисована”. Поэтому без дополнительной информации окончательное состояние такой клетки выяснить невозможно.
15.2.3.	Реализация
Представленные выше построение автомата, построение карты путей и уточнение состояний клеток линии реализуем процедурой AnalyzeLine. Ее схема представлена в листинге 15.4.
Листинг 15.4. Общий вид процедуры анализа линии
procedure AnalyzeLine(kind : boolean/number : byte);
{ kind - вид линии (строка/столбик), number - ее номер }
const nondef = -1; { неопределенное состояние }
var N, L : byte; { количество блоков и длина линии }
М : byte; { количество позиций в описании линии }
i,	j : byte; { номер символа в линии и вершина автомата }
f : byte; { номер строки в массиве тар }
next : array [O..MaxSz, 0..1] of shortint; { таблица переходов }
map : array [O..MaxSz, O..MaxSz] of byte; ( карта путей } can_zero, can_one : boolean;
jMin, jMax : byte;
Begin
формирование линии и массива длин блоков;
построение таблицы переходов;
построение карты путей по таблице автомата и текущему
ЯПОНСКИЙ КРОССВОРД
415
состоянию линии;
обратный ход по карте путей и уточнение состояний клеток; End;
Пункгы, указанные в теле процедуры, уточним по отдельности.
Формирование лищри и массива длин блоков. Линия (строка или столбик в массиве pict) задана параметрами процедуры. Значение true параметра kind задает строку, false — столбик. В соответствии с этим по массиву pict строим линию в массиве cells. В массиве need_refresh запоминаем, что данная линия будет проанализирована. По описанию линий формируем массив длин блоков Ы_1еп. { подготовка массивов клеток линии и длин блоков } if kind then L := XSz else L := YSz; if kind then for i := 1 to L do
cells [i] := pict[number, i] else
for i := 1 to L do
cells [i] := pict[i, number]; need_refresh[kind, number] := false; N := Lines[kind, number].N; for i := 1 to N do
bl_len[i] := Lines[kind, number] .bl_len[i];
Построение таблицы переходов. Для реализации автомат удобнее представлять не графом, а таблицей переходов, т. е. двумерным массивом, индексы в котором — входные символы и вершины автомата (рис. 15.4). Значение элемента массива указывает, в какую вершину переходит автомат из данной, прочитав данный символ.
	Состояние								
Символ	0	1	2	3	4	5	б	7	8
0	0	2	2	-	-	6	6	-	8
1	1	-	3	4	5	-	7	8	-
Рис. 15.4. Таблица переходов автомата, заданного графом на рис. 15.2
Реализуем таблицу в двумерном массиве Next и заполним ее по описанию линии в соответствии с правилами из подраздеДа 15.2.1. Заметим, что в вершинах автомата переходы по некоторым символам не определены. Однако, чтобы работать с таблицей, нужно определить значения всех ее элементов. Поэтому “пустым” элементам таблицы присвоим значение -1, отличное от возможных номеров вершин о, 1, ..., М. В процедуре дадим этому значению имя nondef.
Для подсчета позиций используем переменную М. После обработки очередного блока ее значение увеличивается на длину этого блока с учетом незарисованной клетки после него. За последним блоком незарисованной клетки в описании линии нет.
{ заполнение таблицы переходов NEXT }
М := 0;
for i : = 1 to N do begin
{ в первой клетке блока определены переходы по 0 и по 1 } next[М, 0] := М; next[М, 1] := М+1;
416
ГЛАВА 15
for j := 1 to bl_len[i]-l do begin
{ внутри блока определены переходы только по 1 }
next[М+j, 0] := nondef; next[М+j, 1] := M+j+1;
end;
inc(M, bl_len[i]+l);
{ последняя клетка блока... }
if i < N
then next[M-1, 0] := M	{ ••• последнего }
else next[M-1, 0] := M-1; { ... непоследнего }
next[M-1, 1] := nondef;
end;
M := M-1; { количество позиций в описании линии }
Построение полезных путей. Представим пути, по которым проходит процесс обработки линии, в двумерном массиве тар. Столбцы этого массива с индексами от О до м представляют вершины автомата, т.е. позиции в описании линии с добавлением вершины 0. Строки массива имеют индексы от 0 до L-М и соответствуют количествам прочитанных “свободных” символов 0 линии (или символов 2, принятых за 0), т.е. диагоналям полосы, указанной в подразделе 15.2.2.
Например, путям на рис. 15.3 соответствуют пути между элементами массива, изображенные на рис. 15.5. “Вертикальные” переходы между элементами массива тар соответствуют переходам по вертикали на рис. 15.5, а “горизонтальные” — переходам по диагонали. Как видим, любой путь из элемента тар [0,0] в элемент map [L-М, М] имеет длину L.
Рис. 15.5. Пути обработки линии «массиве тар
Достижимость вершин после чтения символов представим, присваивая положительные значения элементам массива тар. Вначале всем элементам присвоим значение 0, а тар [0,0] (“исходной позиции”) — значение 2.
Далее изменим элементы массива, моделируя процесс обработки линии в автомате. Сначала рассмотрим, какие индексы имеет элемент массива тар, соответствующий вершине с координатами [i, j] в карте путей автомата (см. рис. 15.3). Строка массива с индексом 0 соответствует диагонали, ведущей из верхнего девого угла карты путей, т.е. вершину с координатами вида [j , j] представляет элемент тар[0, j]. Аналогично произвольной вершине [i, j] пути (в пределах полосы, описанной выше) соответствует элемент map [ i - j , j ].
ЯПОНСКИЙ КРОССВОРД
417
Таким образом, если после чтения входного символа cells [i] автомат достигает позиции j, тр элементу map[i-j, j] присваивается положительное значение. Рассмотрим, какое именно.
Если в вершине j автомата определен переход по символу 0, то переход по символу cells [i], равному 0 или 2, соответствует вертикальной стрелке в карте путей (состояние автомата не изменяется). В этой ситуации к значению map[i-j, j] прибавим 4, запомнив тем самым, что переход произошел по символу 0.
Проанализируем переходы из вершины в следующую вершину. Пусть в вершине j -1 определен переход в вершину j по символу 0. Тогда, находясь в вершине j -1 и прочитав символ cells [i], равный 0 или 2, автомат переходит в вершину j. Этому соответствует достижение элемента map [ i - j , j ]; к его значению прибавим 2.
Если же в вершине j -1 определен переход в вершину j по символу 1, то он происходит при чтении cells [i], равного 1 или 2. Тогда к значению map [i, j] прибавим 1.
Таким образом, map [f, j ] = 0 является признаком того, что после чтения (f+j) -го входного символа вершина j недостижима, нечетное значение map [f, j ] — что вершина достижима по символу 1, а значение больше 1 — что достижима по символу 0.
Рассмотрим, в каком порядке достигаются элементы массива тар. При i = l можно достичь элементов тар[0,1] и тар[1,0], при i = 2— тар[0,2], тар [1,1] и тар [2,0] и т.д. Точнее, у элементов тар, достижимых после чтения cells [i], минимальное значение j (обозначим его jMin), при i<L-M равно о, иначе i+M-L (рис. 15.6, а). Аналогично максимальное значение jMax при i <М равно i, иначе равно М (рис. 15.6, б).
jMin = О
i< М => jMax=i
jMax=M

а)	б)
Рис. 15.6. Минимальные и максимальные значения j
Итак, работа автомата моделируется в цикле по i от 1 до L с помощью обработки элементов массива тар с индексами [ i - j , j ], где j Min < j < j Max.
{ построение карты путей по таблице переходов и текущему состоянию линии }
for f ;= 0 to L-M do
for j := 0 to M do map[f, j] := 0;
map[0, 0] := 2;	{ "исходная позиция" достижима }
for i := 1 to L do begin
if i < L-M then jMin := 0
else jMin := i+M-L;
if i < M then jMax := i
else jMax := M;
for j := jMin to jMax do begin
if i > j then { "вертикальный" переход возможен }
if (map[i-j-l, j] <> 0) and
418
ГЛАВА 15
(next[j, 0] = j) and (cells [i] mod 2 = 0) then inc(map[i-j, j], 4);
if j > 0 then { "горизонтальный" переход возможен }
if map[i-j, j-1] <> 0 then begin
if (next[j-1, 0] = j) and (cells[i] mod 2=0) then inc(map[i-j, j], 2);
if (next[j-l, 1] = j) and (cells[i] > 0) then inc(map[i-j, j], 1) ;
end;
end;
end;
Уточнение состояний клеток. Реализуем правила, приведенные в конце подраздела 15.2.2. Если после чтения L символов вершина М недостижима, т.е. map [L-М, М] =0, то состояния клеток линии противоречат ее описанию, иначе проводим обратный ход по элементам массива тар.
Клеткам линии соответствуют диагонали в таблице тар: клетке 1 — элементы тар [0,1] и тар [1, 0], клетке 2 — тар [0, 2], тар [1,1] и тар [2,0] и т.д. На обратном ходе диагонали обрабатываются от map [ L - м, М ] к тар [ 0, 0 ].
В каждой диагонали нужно сначала “обнулить” элементы, из которых недостижим map [L-М, М]. Для этого пройдем по диагонали и присвоим 0 тем элементам, у которых значения элементов, соседних справа и снизу, равны 0.
Диагональ, соответствующая клетке с состоянием 2, проходится еще раз. На этом проходе формируются признаки достижимости клетки по символам о и 1 — переменные can zero и сапопе. Перед проходом они получают значение false. Если в диагонали есть элемент, достижимый по символу 0, can zero присваивается true, а если есть элемент, достижимый по символу 1, сап_опе становится равной true. Напомним, что map [f, j ] достижим по символу 0, если map [f, j ] > 1, и достижим по символу 1, если map [ f, j ] нечетно.
Если после обработки диагонали массива тар, соответствующей клетке с состоянием 2, только одно из значений can zero и сап опе стало истинным, состояние клетки соответственно изменяется. Если же оба признака истинны, значит, клетка достижима как по 0, так и по 1, и ее состояние уточнить невозможно.
Что делать, если состояния клеток линии противоречат ее описанию? Ниже нам понадобится вне процедуры AnalyzeLine отличать успешный анализ линии от неуспешного. Для этого можно поступить одним из двух способов.
Первый способ (многие считают его лучшим в плане совершенства, красоты и стиля) — сделать подпрограммы AnalyzeLine и IterateLineLook функциями, которые в ситуациях успеха или неуспеха возвращали бы разные значения.
Второй способ менее красив, но на практике более популярен. Используем признак неуспешности анализа строки — глобальную nepeMeHHyro*ErrorLevel. Перед началом итерационного анализа линий присвоим ей 0, а если при анализе линии обнаружено противоречие, изменим ее значение на 1.
Описанные действия реализованы в следующем фрагменте кода:
if map[L-M, М] = 0 then ErrorLevel := 1 else begin
{ обратный ход и уточнение клеток }
ЯПОНСКИЙ КРОССВОРД
419
for i := L downto 1 do begin
if i < L-M then jMin := 0
else jMin s = i+M-L;
if i < M then jMax := i else jMax : = M;
{ "обнулени^" элементов, из которых недостижим map[L-M, М] } for j := jMin to jMax do
if map[i-j, j] <> 0 then begin
{ проверка, продолжаются ли пути }
{ в последнем столбце }
if (j = М) and (i-j < L-M) and (map[i-j+l, j] = 0) then map[i-j, j] ;= 0;
{ не в последнем столбце, но в последней строке } if (j < М) and (i-j = L-M) and (map[i-j, j+1] = 0) then map[i-j, j] := 0;
{ внутри таблицы }
if (j < M) and (i-j < L-M) and
(map[i-j, j+1] = 0) and (map[i-j+l, j] = 0) then map[i-j, j] : = 0 ;
end;
{ проверка достижимости неопределенной клетки по 0 и по 1 } if cells [i] = 2 then begin
can_one := false; can_zero' := false;
for j := jMin to jMax do begin
if map{i-j, j] >1 then can_zero := true;
if odd(map[i-j, j]) then can_one := true; end;
{ уточнение состояния клетки }
if can_zero <> can_one then begin need_refresh[not kind, i] := true; if can_one then cells [i] := 1 else cells [i] := 0;
if kind then pict[number, i] := cells [i] else pict[i, number] := cells[i] ;
end;
end; { конец обработки клетки с состоянием 2 }
end; { конец обратного хода (цикла по номеру клетки i) } end;
Сложность реализованного анализа линии. Очевидно, что линия (массив cells) формируется за время 0(L), а автомат (таблица Next) — за время 0(М) = O(L). Карта путей автомата в массиве тар имеет размеры (L-A/)xAf и заполняется за время O(L(L-M)) = O(L2). Такую же оценку сложности имеет обратный ход по карте путей и уточнение состояний клеток. Итак, общая сложность анализа линии длиной L имеет полиномиальную оценку 0(1}).
» Запишите полный текст процедуры AnalyzeLine самостоятельно. Напишите программу, которая решает кроссворды с помощью приведенных подпрограмм IterateLineLook и AnalyzeLine.
420
ГЛАВА 15
15.3.	Решение задачи с помощью перебора
15.3.1.	Итерационный анализ линий не решает задачу
Итак, подпрограммы IterateLineLook и AnalyzeLine (см. подразделы 15.1.3 и 15.2.3) реализуют итерационный анализ линий (ИАЛ). Как уже говорилось, ИАЛ не может решить задачу, если, например, кроссворд “низкокачественный” и имеет несколько решений. В таких ситуациях ИАЛ завершает просмотры, когда состояния некоторых клеток еще не выяснены.
Хуже того, существуют кроссворды, имеющие одно решение, но не решаемые с помощью ИАЛ, например, кроссворд на рис. 15.7.
					5	7	2	1	1	1	7	11	4	1	7	6	2	4	4	4	5	6	2	5	7	12	5	1	3
					6	3	7	6	3	3	4	6	4	6	4	5	2	4	7	4	3	5	2	1	1	1	6	10	8
					4	5	5	6	6	6	7		1	1	3	2	3	1	2	1	1	1	4	1	1	1	3	1	
									6	7			4	3			1	1		2	1		1		1				
																	2												
1	5	11	4		X		X	X	X	X	X		X	X	X	X	X	X	X	X	X	X	X			X	X	X	X
3	3	9	2	1	X	X	X				X	X	X		X	X	X	X	X	X	X	X	X			X	X		X
2	8	5	5		X	X			X	X	X	X	X	X	X	X		X	X	X	X	X			X	X	X	X	X
2	14	5			X	X		►	X	X	X	X	X	X	X	X	X	X	X	X	X	X		X	X	X	X	X	
2	4	4	2	6	X	X			X	X	X	X		X	X	X	X			X	X			X	X	X	X	X	X
2	6	5	2			X	X				X	X	X	X	X	X						X	X	X	X	X		X	X
11	7				X	X	X	X	X	X	X	X	X	X	X								X	X	X	X	X	X	X
6	3	3	6		X	X	X	X	X	X		X	X	X				X	X	X				X	X	X	X	X	X
1	7	5	5		X		X	X	X	X	X	X	X				X	X	X	X	X				X	X	X	X	X
8	7	4			X	X	X	X	X	X	X	X				X	X	X	X	X	X	X				X	X	X	X
8	9	4			X	X	X	X	X	X	X	X			X	X	X	X	X	X	X	X	X			X	X	X	X
12	1	8			X	X	X	X	X	X	X	X	X	X	X	X			X			X	X	X	X	X	X	X	X
2	1	2													X	X			X			X	X						
9	3														X	X	X	X	X	X	X	X	X						
2																											X	X	
9																			X	X	X	X	X	X	X	X	X		
6															X	X	X	X	X	X									
6												X	X	X	X	X	X												
6										X	X	X	X	X	X														
7								X	X	X	X	X	X	X								•							
8						X	X	X	X	X	X	X	X									4							
8					X	X	X	X	X	X	X	X																	
8					X	X	X	X	X	X	X	X																	
7					X	X	X	X	X	X	X																		
7					X	X	X	X	X	X	X																		
Рис. 15.7. Кроссворд, имеющий одно решение, но не решаемый с помощью ИАЛ
ЯПОНСКИЙ КРОССВОРД
421
Результат применения ИАЛ к этому кроссворду выглядит так, как показано на рис. 15.8.
*'*****'*****«*****,_**♦* *** * *,**♦,******♦**, * ** * * ** * * ***************
** * ********************** *** *********** * ********** * ** * * *******.********
***********......*******
*********** * ***** * ******* ********** * ******** ****** *********.??*****??,,**** ********* *********** ***** ************* * * * *********
** * **........
.........??*******???*??. ???????.????.....????.??.
????????????????????????. ????????????????????????. ????????????????????????. .????♦*????...............
.??*****??...............
.?????***?????...........
????????????????????????. ????????????????????????. ????????????????????????. ????????????????????????.
Рис. 15.8. Результат применения ИАЛ к кроссворду на рис. 15 7
15.3.2.	Перебор и исследование состояний клеток с помощью ИАЛ
Итак, ИАЛ не позволяет решать некоторые кроссворды, поэтому вернемся к перебору состояний клеток, но запускать его придется, только если ИАЛ оставил состояния некоторых клеток невыясненными.
Основная идея перебора очевидна: найдя клетку с невыясненным состоянием, сначала предположим, что она не зарисована, и посмотрим, что из этого получится, затем предположим, что зарисована, и вновь посмотрим. Например, в ситуации на рис. 15.8 предположение pict [10, 11] = 0 приводит к (единственному) решению, а предположение pict [10, 11] = 1 — к противоречию.
Предположение о состоянии клетки увеличивает количество ограничений по строке и столбику, которым она принадлежит. Отсюда, возможно, будут следовать окончательные состояния других клеток, из их окончательных состояний — еще какие-то окончательные состояния и т.д. Поэтому, чтобы “посмотреть, что получится”, целесообразно использовать ИАЛ.
ИАЛ, проводимый вначале, назовем первичным, а проверяющий следствия предположения — анализом предположения.
422
ГЛАВА 15
Результаты анализа любого предположения являются следствиями этого предположения, поэтому, проверяя второе предположение после того, как проверено первое, нужно опираться на результаты только первичного анализа, но никак не на результаты анализа первого предположения. Таким образом, после проверки предположений нужно восстановить результаты первичного анализа. Это можно сделать двумя способами.
Первый способ — завести еще один массив, соразмерный pict, и хранить в нем данные о том, когда был сделан вывод об окончательном состоянии клетки — в процессе первичного анализа или анализа предположений.
Второй способ — перед запуском анализа первого предположения сделать копию массива pict, чтобы использовать ее для анализа второго предположения. Этот способ проще программировать, поэтому выберем его. Однако он создает следующую проблему.
После анализа предположений, связанных с некоторой клеткой, могут остаться клетки с состоянием “неизвестно”. Так будет, например, если кроссворд имеет больше двух решений. Тогда “внутри”, как минимум, одного из предположений, связанных с клеткой, придется делать дополнительные предположения относительно еще хотя бы одной клетки.
Число таких “вложенных” предположений заранее неизвестно, поэтому анализ предположений естественно реализовать рекурсивной подпрограммой. Для анализа двух предположений об окончательном состоянии клетки порождаются два рекурсивных вызова подпрограммы.
При этом перед каждым рекурсивным вызовом нужно сохранять копию массива pict, причем в локальной переменной, т.е. в программном стеке. Но если массив имеет размеры 100x100, стек будет переполнен при совсем малой глубине рекурсии.
Для решения проблемы будем располагать копии массива в свободней памяти, а локальными сделаем указатели на них. Используем следующие типы массивов и указателей на них:
type PAByte = *AByte;
Abyte = array [1..MAXSZ*MAXSZ] of byte;
Рекурсивный анализ предположений о состояниях клеток, использующий ИАЛ, реализуем рекурсивной процедурой Try (листинг 15.5). Вначале она запускает ИАЛ, в результате чего либо находится противоречие, либо нет. Эти ситуации различаются по значению глобальной переменной ErrorLevel, установленному при анализе линий. В первой ситуации вызов Try завершается. Во второй Try проверяет, нет ли клеток с неокончательным состоянием.
Если клеток с неокончательным состоянием нет, значит, получено решение; его нужно вывести и закончить работу. Иначе найдена клетка с неокончательным состоянием и некоторыми координатами [ j , i]. Тогда делаются^предположения о состоянии клетки [ j , i] и анализируются с помощью двух рекурсивных вызовов Try. Перед первым вызовом создается копия картинки в свободной памяти, а перед вторым она замещает картинку, возможно, изменившуюся при выполнении первого вызова. Таким образом, предположения анализируются, начиная с одной и той же картинки (листинг 15.5).
ЯПОНСКИЙ КРОССВОРД
423
Листинг 15.5. Анализ предположений о состояниях клеток
procedure Try(у, х : byte);
var i, j, i_, j_ : byte;
p : PAByte;
Begin
ErrorLevel := *0;
IterateLineLook;	{ первичный анализ линий }
if ErrorLevel <> 0 then exit; { получено противоречие } i := у; j := x;
{ определим, есть ли клетки с неокончательным состоянием } while (i <= Ysz) and (pict[i, j] <> 2) do
if j = XSz
then begin j := 1; i := i+1 end
else j := j+1;
if i > YSz then { определены окончательные состояния всех клеток }
Outputsolution { вывод найденного решения и выход } else begin
{клетка [i, j] имеет неокончательное состояние } копирование картинки в свободную память }
GetMem(p, XSz*YSz);
for i_ := 1 to YSz do
for j_ := 1 to XSz do pA[XSz*(i_-l)+j_l :=pict[i_, j_];
{ предположим, что клетка [i, j] не зарисована } pict[i, j] :=0;
need_refresh[true, i] := true;
need_refresh[false, j] := true;
Try(i, j); { анализ предположения } { восстановление картинки }
for i_ := 1 to YSz do
for j_ := 1 to XSz do
pict[i_, j_] := pA [XSz*(i_-l)+j_];
FreeMem(p, XSz*YSz);
{ предположим, что клетка [i, j] зарисована }
pict[i, j] := 1;
need_refresh[true, i] := true;
need_refresh[false, j] : = true;
Try(i, j); { анализ предположения } end;
End;
15.3.3.	Решение задачи и анализ решения
В предыдущих разделах были определены все структуры данных и процедуры, необходимые для решения задачи. Соберем их в следующей программе (листинг 15.6).
Листинг 15.6. Программа решения задачи
const MAXSZ = 100;	{ максимальный размер поля }
MAXBLOCKS =50; { максимальное число блоков В линии }
424
ГЛАВА 15
type LineDescript = record { представление линии } N : byte; { количество и длины блоков } bl_len : array[1..MAXBLOCKS] of byte;
end;
PAByte = xAByte; { тип указателя на копию картинки } Abyte = array [1..MAXSZ*MAXSZ] of byte;
var pict : array [1..MAXSZ, l.^MAXSZ] of byte; { картинка } Lines : array [boolean, 1..MAXSZ] of LineDescript; { данные о линиях } need_refresh : array [boolean, 1..MAXSZ] of boolean;
{ признаки изменения состояний в линиях }
cells : array [1..MAXSZ] of byte; { клетки анализируемой линии } bl_len : array [1..MAXBLOCKS] of byte; { длины блоков } XSz, YSz : byte; { размеры поля } sol_found : boolean; { признак того, что решение найдено } font : text; { выходной файл } ErrorLevel : byte; { признак противоречия }
procedure Init;
... { см. подраздел 15.1.2 }
End;
procedure Outputsolution;
... { см. подраздел 15.1.2 }
End;
procedure AnalyzeLine kind : boolean;number : byte); ... { см. подраздел 15.2.3 }
End;
procedure IterateLineLook;
... { см. подраздел 15.1.3 }
End;
procedure Try(y, x : byte);
... { см. подраздел 15.3.2 }
End;
BEGIN
Init;
Try(l, 1) ;
if sol_found then writein(fout, '<end>') else writeln(fout, '<no solutions:»1);
close(fout);
END. k
Анализ. Представленная реализация не оптимальна, как минимум, в трех технических вопросах.
1.	Размер всех массивов задан в их объявлении. Вместе с тем, например, для массива тар (карты путей в автомате) целесообразнее выделять память нужного объема динамически. Если максимальная длина линии равна L и автомат имеет состояния 0,1,..., М, то карта содержит M(L-M) элементов, что не больше, чемЬ2/4. Таким образом, для динамичес
ЯПОНСКИЙ КРОССВОРД
425
кого массива тар нужно вчетверо меньше памяти, чем для статического или автоматического. В некоторых ситуациях такая экономия может оказаться решающей.
2.	Все массивы определены как глобальные, хотя разумнее было бы ограничить область видимости, например Массива cells, процедурой AnalyzeLine.
3.	При каждом анализе одной и той же линии ее автомат строится заново. Если задаться целью максимально увеличить скорость (в том числе и за счет увеличения расходов памяти), автоматы всех линий следует построить один раз и запомнить. Впрочем, затраты на многократные построения автоматов относительно невелики.
Эти нюансы не учтены в программе, чтобы сократить и без того громоздкое изложение и оставить желающим простор для усовершенствования программы.
Н Напишите полный текст программы, возможно, собрав подпрограммы в модуль. Реализуйте указанные выше усовершенствования. Попробуйте обрабатывать кроссворды, размер которых больше, чем 100x100.
►> “Естественный” анализ линии заключается в том, чтобы перебрать размещения блоков как целостных элементов. Размещения блоков, не противоречащие состояниям клеток линии перед анализом, определяют, какими могут быть состояния клеток линии. Если после перебора у ранее не уточненной клетки может быть только одно состояние, оно становится для нее окончательным.
Разработайте и реализуйте рекурсивный алгоритм перебора позиций, с которых в линии могут начинаться блоки (при рациональной реализации такой алгоритм в действительности работает достаточно быстро). Реализуйте программу, которая решает кроссворды с помощью описанного анализа линии.
Указание. Нужно перебрать все возможные размещения первого блока линии, просматривая (рекурсивно) для каждого из них размещения всех следующих блоков. На “дне” рекурсии находится исследование размещений последнего блока. В качестве параметров рекурсивной подпрограммы возьмите номер блока и номер клетки, с которой он начинается. Целесообразно, чтобы подпрограмма была функцией, возвращающей признак того, что, начиная с данной клетки, можно разместить данный блок и после него все следующие блоки. Если такое размещение получено, функция должна зафиксировать возможные состояния клеток блока и промежутка после него.
Учтите, что блок не может включать гарантированно незарисованную клетку, а промежуток между блоками состоит не менее чем из одной клетки и ни одна клетка промежутка не может быть окончательно зарисованной (как и клетка за последним блоком). Не менее важно, что различных вызовов с одними теми же параметрами может быть много, т.е. имеют место перекрывающиеся подзадачи. Поэтому реализуйте рекурсию с запоминанием (см. подраздел 13.1.2) с помощью двумерного массива, индексированного номерами блоков и номерами клеток.
Приложение А
Указания по решению упражненйй
Глава 1
1.1.	Используйте цикл с параметром i, который изменяется от2до|_7й_|-1,и учтите, что каждому найденному делителю i соответствует делитель ndivi. Значение L Jn J нужно рассмотреть отдельно. Если 4п целый, то он является делителем (например, л = 25 или 49). Иначе \_-Jn J может как быть делителем (п = 12 или 35), так и не быть (л= 10 или 26).
1-2. Пусть г — радиус круга. Для решения с оценкой О(г) достаточно перебрать значения х от 1 до LrJ и просуммировать числа вида 2|_ Vr2 -х2 J+1, полученную сумму удвоить и прибавить 21_rJ+1.
1.3.	Попробуйте доказать, что следующий способ разбиения на пары даст максимальное количество пар. Найдем минимальное число х, при котором х+п — простое. Тогда все пары (х+1,п-1), (х+2,и-2), ... будут “хорошими”. Затем найдем минимальное число в пару кх-1 и т.д.
1.4.	Отрезание квадрата равносильно вычитанию в алгоритме Евклида (см. подраздел 1.2.4).
1.5.	Заметим, что каждое число, на которое делятся и а, и Ь, должно также делить и любую сумму вида au+bv. Поэтому искомая натуральная сумма au+bv не меньше НОД(а, Ь). Значит, если найти числа и и v, при которых au+bv = НОД(а, Ь), задача будет решена.
Найти такие числа миг можно с помощью современной версии алгоритма Евклида. Цикл с операторами с :=amodb; a : = b; Ь : = с задает вычисление последовательности остатков г. от деления: гй = а, r} = b, r.^r^ mod гг1 при i>2. Кроме остатков, рассмотрим частные qt от деления и коэффициенты и, и v( при а и Ь, с помощью которых остатки выражаются через а и Ь. Очевидно, и0=1, v0=O, и,=0, v, = l. На первом шаге вычисляется r2=r0modrp т.е. a=qlrl+r2, где ^^div^. Отсюда r^a-q^, т.е. и2 = и0-^1и1, v2 = v0-^Iv1. Аналогично из формулы г,=r.-j mod гГ1 следуют формулы для вычисления частных и коэффициентов
q, = гы div г,-, и,- = м,_2 - qi-iUi-i, v,=v,-2 - qt-iVt-i при всех i > 2.
Искомыми значениями являются и. и v, при последнем i, для которого г(*0 (это г. и есть d).
428
ПРИЛОЖЕНИЕ А
1-6. Пусть ^=9]*/+г, a2=q2-d+r итак далее при наибольшем возможном d и некотором r<min{ap а2, ..., ап]. Отсюда a2-al = (q2-q1)-d, a2~a2—{q2-q^ dит.д., т.е. ^=HO4(|a2-aJ, |а3-а2|, ..., |ал-ал_,|). Заметим, что НОД можно вычислять по мере чтения ар а2,..., ап, из прочитанных чисел храня в памяти только два последних.
1.7.	Попробуйте доказать, что переложить морковь можно, если, и только если, а+Ь = 2* НОД(а,Ь) при некотором £ > 0.
1.8.	Пусть (хр^), (х2, у2) — координаты концов отрезка. Переместим отрезок и, если нужно, отразим его относительно вертикали и горизонтали так, чтобы его левый нижний находился в точке (0,0), а второй имел координаты х=|х-х2| и у = [Vj—у2|. Очевидно, что количество “целочисленных” точек на отрезке при этих перемещениях не изменяется. Найдем </=НОД(х,у). Тогда x=kd, у—Id, и точки (к, I), (2к, 21), ..., ((d-\)k, (d-l)l) принадлежат отрезку, поэтому всего “целочисленных” точек (d-1 )+2, т.е. d+1.
1.9.	Ясно, что если d = НОД (a, b) > 1, то линейные комбинации вида ка+1-b кратны d, поэтому все числа, не кратные d, нельзя выразить в виде ка+1-b. Если НОД (a, b) = 1, то искомым числом является ab-a-b. Докажем это.
1.	Предположим, что ab-a-b=ка+1-b при некоторых неотрицательных ки1. Ясно, что к<Ь-1,1<а-1, т.е. Jt+1 <b, /+1 <а. Перенесем слагаемые в равенстве: ab= (к+\)а+(1+\)Ь. Отсюда оба слагаемых в сумме справа должны иметь делители а и Ь. Тогда, поскольку а и b взаимно просты, а делит /+1, b делит fc+1, что невозможно, ведь Z+1 <а, Л+1 <Ь. Значит, ab-a-b нельзя представить в виде kq+1-b.
2.	Докажем, что любое число больше ab-a-b можно представить в виде ка+1-b. Не ограничивая общности, предположим, что a>b na=qb+r, 1 < r<b. Рассмотрим числа а, 2а, ..., (b-l)a, Ьа. Они дают попарно различные остатки от деления на Ь, причем остаток 0 дает число Ьа. Действительно, предположив, что kla=qJb+s, kji = q2b+s при k2<k2<b, получим (k2-kl)a = (q2-q)b, т.е. b делит k2-kt, а это невозможно.
Рассмотрим любое из чисел вида ab+s, где j=l, 2, ..., b-1. Представим ab+s в виде суммы qb+ s+ (a-q)b, где qt— число меньше а, при котором qb+s=ka, kt<b. Значит, ab+s= ka+(a-q)b, причем fc>0, a-qt>0. Отсюда следует, что все числа ab-a-b+s с положительным з можно представить в виде линейной комбинации а и Ь.
1.10.	Рамку можно покрыть плитками размером Ах1, если А = 1 или А = 2 или одна из сторон кратна А, а вторая при делении на А дает остаток 2, или каждая при делении на А дает остаток 1.
1.11.	Инициализируем нулями целочисленный массив А с индексами от 1 до 1 000 1. Прочитав очередное числом, выполняем А [к] : = 1. Затем просматриваем массив А от начала, пока не встретится элемент со значением 0 (а он встретится обязательно: в крайнем случае, им будет А[10001]). Индекс этого элемента и является результатом.
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
429
1.12.	В отличие от предыдущей задачи, если очередное прочитанное число к принадлежит диапазону от 1 до 10001, выполняем А [к] : = 1, а если не принадлежит, пропускаем его. Поскольку не более чем 104 элементов не могут “покрыть” собой все 10001 значений, среди значений элементов массива останется хотя бы один 0. К тому же, если в последовательности встречаются числа вне указанного диапазона, это только уменьшает количество единиц в массиве А.
1.13.	Рассмотрим “наивный” алгоритм: пока не будет выведено N чисел, проверять все натуральные числа подряд (удалить все делители 2, 3, 5; если осталось 1, число “подходит”). Однако этот алгоритм в действительности является экспоненциальным относительно номера числа, поскольку с ростом N такие числа встречаются всё реже и реже. Используем другой способ.
Будем находить числа в порядке возрастания и запоминать, чтобы с их помощью определять число, следующее за последним из полученных. Пусть последним получено число т=а[к]. Ясно, что следующее число a[fc+l] является минимальным из результатов умножения некоторых предыдущих чисел на 2, 3 или5. Можно найти эти числа как первые элементы последовательности, которые соответственно больше, чем т/2, т/3, т/5. Однако вместо их вычисления можно хранить их номера i2> iJt is в последовательности. Тогда следующим числом будет r=min{2 a[i2], 3a[i3], 5a[i3]}. После его вычисления нужно пересчитать номера i2, i}, is: если t=ja[ij], то увеличивается на 1 (/’= 2, 3,5). Например, после вычисления т = 5 имеем i2=2 (номер числа3), i3=l, i5=l (номер числа 2). Тогда 2 a[i2]= 3 a[i3]=6, 5-a[is] >6, поэтому после запоминания t=6 увеличим i2 и »3 на 1: /2=3 (номер числа 4), i3=2 (номер числа 3). Чтобы не вычислять всякий раз произведения можно хранить эти числа Ь2, b}, bs и вычислять заново только с увеличением соответствующих ij.
Последовательность чисел удобно хранить в массиве, добавив нулевой элемент 1. Тогда i2, iy i5 инициализируются значением 0, a b2, b}, bs — значением 1.
Заметим, что 035=32768 (непредставимо в типе integer), а а1(В0=2147483648 (в longint). Поэтому, работая с современными 32-битовыми компиляторами, используйте для чисел тип Int64 или Qword, а работая с Turbo Pascal — тип Comp. Кстати, в Turbo Pascal возникнет еще одна проблема: 10000 элементов типа Comp не помещаются в 64 Кбайта, и придется либо “закрутить” массив1, либо выделять ему свободную память “по кусочкам”. Это еще раз подтверждает: Turbo Pascal (как и Borland Pascal), оставаясь неплохой средой для отладки, уже не очень годится для окончательной компиляции программ при “настоящих” больших ограничениях.
1 Достигнув конца массива, продолжить записывать в его начало. Это возможно, поскольку хранимые в начале значения уже выведены, а для дальнейших вычислений необходимы только элементы с индексами не меньше i5.
430
ПРИЛОЖЕНИЕ А
Глава 2
2.1.	Это “сумма” всех чисел с хог в качестве сложения.
2.2.	Использовать массив amount : array [char] of longint, значение c-го элемента — количество появлений соответствующего символа с. Учет очередного символа, прочитанного в переменную с, — это inc (amount [с] ). Естественно, инициализировать массив нулями.
2.3.	Если в тексте есть не меньше трех положительных чисел или хотя бы одно положительное и не меньше двух отрицательных, то решением будет тройка наибольших положительных а, Ь, с или наибольшее положительное и пара наименьших отрицательных (наибольших по модулю) d, е. Для определения тройки нужны произведения Ьс и d e типа longint. В противном случае текст содержит не более двух положительных чисел. Возможны две ситуации — положительных чисел нет или отрицательных не более одного. В первой решением является тройка наименьших по модулю отрицательных чисел, но если есть 0, то 0 и любые другие два числа. Во второй ситуации отрицательных чисел нет или оно одно. Но положительных чисел не более двух, поэтому, если отрицательных нет, то остальные числа — нули, и решением будет 0 и любые другие два числа. Если же отрицательное число одно, то, если есть 0, решением будет 0 и любые другие два числа, а если нет, то решение — тройка чисел, образующих текст (два положительных и одно отрицательное).
Итак, нужно прочитать текст один раз, запомнить три наибольших положительных числа, три наименьших по модулю отрицательных, два наибольших по модулю отрицательных (или столько, сколько их было, и их количества) и запомнить, был ли 0. Затем по этим данным найти решение, как описано выше.
2.4.	Мысленно расположим числа по неубыванию. Тогда пары с одинаковыми суммами образованы числами, равноотстоящими от концов этой последовательности. Но равные произведения могут дать только эти же пары, поэтому все они должны быть одинаковыми, г)то возможно, если все числа равны между собой или есть п повторений одного числа и п — другого. Проверить указанное условие нетрудно, прочитав вход один раз.
2.5.	а) Используйте массив С : array [0. .3, 0. .255] of longint, в котором значение C[i, к] указывает, сколько раз встречалось число к (от 0 до 255) в качестве i-ro байта прочитанных чисел. После ввода всех чисел искомое число восстанавливается по индексам максимальных значений в строках массива.
6)	Примените описанное в п. а восстановление числа по значениям байтов. Однако этого недостаточно: например, в числах 1, 1, 256,^256, 257, 257, 257 значение 1 встречается по пять раз в оббих младших байтах, но собранное из этих байтов число 257 повторяется только трижды. Чтобы установить, сколько раз восстановленное число встречалось в последовательности, придется еще раз пройти по входным данным. Получаем двупроходный алгоритм', он позволяет не хранить все входные данные в памяти, но требует просмотреть их дважды. Программа, реализующая такой алгоритм, может обработать сколь угодно большой файл на диске. Однако, если последовательность данных по
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
431
ступает от внешнего источника (от клавиатуры, антенны и т.п.) и повторный ее ввод проблематичен, придется запомнить ее всю в оперативной памяти или в файле.
2.6.	Обозначим моменты прихода х,, х2, ..., а искомые моменты ухода — ур у2, .... Введем понятие “момент начала стрижки i-ro клиента” и обозначим его Ьг Введем искусственный момент ухода “нулевого” посетителя уо=О. Тогда при i> 1 нетрудно вычислить b = тах^рхД, у = bi+tr
2Л. Нетрудно убедиться, что нужно вычеркнуть первую слева (самую старшую) цифру, следующая за которой больше ее, а если такой нет, Вычеркнуть крайнюю справа (самую младшую). Запоминать все цифры не нужно. Читаем символы по одному, сохраняя предыдущую цифру. Выводим ее, если она не меньше следующей. Если предыдущая цифра меньше следующей, выводим следующую, а затем читаем и выводим цифры до конца строки. Если цифры не возрастают, то не выводим последнюю.
2.8.	Состояния и переходы между ними, а также действия, связанные с обработкой очередного символа текста tc, предст/влены таблицей. Если состояние не изменяется, оно не указано. Действия указаны после знака “/’. Предыдущий символ хранится в дополнительной переменной рс. Ввод следующего символа и присваивание pc : = tc происходят на каждом шаге и в таблице не указаны. Для вывода символа вызывается put, для определения, является ли он буквой, — It г.
	Буква	Знак пунктуации	1 1 (пробел)	<eol>
out	bw/	/if Itr(pc) then put(1 1)	/if Itr(pc) then put(1 1)	/put(tc)
bw	word/put(pc); put(tc)	out/	out/put(' ')	out/put(tc)
word	/put(tc)	out/put(1 1)	out/put(tc)	out/put(tc)
2.9.	Использовать посимвольное чтение текста, при котором конец строки пропускается, а номер строки увеличивается. Для каждого образца построить и использовать свою собственную функцию возвратов. Учесть, что после того, как вхождение образца найдено, его следующие вхождения можно не отслеживать.
Глава 3
3.1.	5(и)=п, еслип<10; S'(«)=«modl0+5(ndivl0), еслии>10.
3.2.	Младшая цифра числа У равна /Vmod 10, число без младшей цифры — Ndiv 10. Решения пп. а и 6 должны отличаться только взаимным расположением оператора вывода и рекурсивного вызова.
3.3.	Докажем, что F(n) = л+10 при л > 100, иначе F(ri)=91. Первая часть очевидна. Рассмотрим значения п от 1 до 100. F(100)= F(F(lll))=F(101)=91. При л>90 имеем F(ri)= F(F(n+ll))=F(n+l). Тогда F(91) = F(92)= ...= F(100)=91. При
432
ПРИЛОЖЕНИЕ А
и <91 доказательство индуктивно, но не по возрастанию, а по убыванию п. База. F(90)= F(F(101))=91. Предположение индукции', при всех п, 100> п>к, F(n)=91. По определению и по предположению индукции получим F(fc) = F(F(fc+ll))=F(91), т.е. 91.
3.4.	Ответ. При длине строк, измеряемой десятками символов, получается астрономическое количество вложенных вызовов.
3.5.	Вначале изменяется клетка 1, затем клетка 2. Первой занятой остается клетка 1, но освобождать только что занятую 2 бессмысленно, поэтому освобождается 1. Клетка 2 стала первой занятой, и занимается клетка 3. Аналогично теперь изменяется клетка 1, становясь первой занятой. Далее освобождается 2, и можно только освободить клетку 1 и т.д. Очевидно, клетка 1 изменяется через один шаг. Как только она занята, изменяется клетка 2, а как только свободна, изменяется клетка, номер которой на 1 больше номера первой занятой.
Таким образом, приходим к циклу, в первой части которого изменяется клетка 1, а во второй — клетка, следующая за первой занятой (этой занятой через одно выполнение цикла будет клетка 1). После каждого выставления шашки нужно проверить, не оказались ли все п клеток занятыми. Если оказались, прервать цикл. Цикл естественно сделать бесконечным, т.е. использовать тождественно истинное условие продолжения.
Представить состояния клеток проще всего с помощью массива со значениями 0 и 1. Теоретически, он плох тем, что каждый раз первую занятую клетку придется искать (связный список избавил бы от этого). Однако практически эти поиски через раз ограничатся клеткой 1, да и всего-то клеток не больше 15...
3.6.	Ответ. Можно, но только при условии, что i будет локальной переменной рекурсивной подпрограммы Sierp_Tr.
Разберемся, почему не будет работать (иначе говоря, будет “работать неправильно”) с глобальным i модификация подпрограммы в листинге 3.6. Рассмотрим вызов при n=1. Попадаем в else-ветвь и пытаемся перебрать в ней, начиная с i=l, три треугольника Серпиньского порядка 0. Внутри вызова при п=0 попадаем в then-ветвь, в ней рисуем (обычный) треугольник, выходим из внутреннего уровня рекурсии на более внешний (п=1)... и обнаруживаем, что значение i, в котором должны перебираться треугольники 0-го порядка, безвозвратно испорчено. Ведь если переменная i глобальна, то для прорисовки треугольника во внутреннем вызове было использовано то же самое i.
Если же i — локальная переменная рекурсивной подпрограммы, то для каждого рекурсивного вызова будет своя переменная i, и описанная накладка не возникнет.	*
Если использовать цикл по глобальному i в подпрограмме в листинге 3.5, то очень велики шансы, что все случайно получится правильно, поскольку i используется лишь на самых глубоких уровнях рекурсии, и следующее использование начинается гарантированно позже предыдущего. Но если это же глобальное i используется не только в подпрограмме Sierp_Tr, то и в случае подпрограммы в листинге 3.5 тоже может возникнуть накладка.
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
433
Искать ошибки такого рода очень тяжело. Поэтому говорят, что по-хорошему в циклах следует пользоваться только локальными переменными. Раньше считалось, что это “правило хорошего тона”, но ради экономии программного стека его можно нарушить. Сейчас, после перехода к 32-битовым программам, это правило уже «тало “почти категорическим”. Например, современные версии компилятора Object Pascal из среды Delphi в случае использования в цикле for нелокальных переменных выдают предупреждение.
3.7.	Зададим драконову ломаную последовательностью букв R и L, обозначающих направление текущего поворота. Ломаная порядка 0 задается пустой строкой, порядка 1 — строкой R, порядка 2 — RRL, порядка 3 — RRLRRLL ит.д. Дописав к (п-1)-й строке сначала букву R, а затем — всю (п-1)-ю строку в обратном порядке с взаимозаменой R и L, получим п-ю.
Построить последовательность поворотов для ломаной порядка л можно, используя существенно меньше памяти (не более чем 10 целых переменных и ни одного массива). Будем для каждого i-ro поворота (1 < i<2"‘1) вычислять, какой он (левый или правый), смотря по тому, оказывается ли он в последовательности поворотов текущего уровня точно посередине, в левой половине или в правой (если точно посредине, поворот правый или левый соответственно тому, в левую или в правую половину переходили на предыдущем шаге; если не посредине, переходим к рассмотрению в качестве последовательности левой или правой половины).
Алгоритм сформулирован “весьма рекурсивно”, но его можно реализовать, используя не рекурсивные подпрограммы, а циклы.
Глава 4
4.1. Делим в столбик: “подкачиваем” цифры длинного числа и оставляем остаток от деления (он в integer).
4.4.	Используйте бинарный алгоритм (см. подраздел 1.2.5).
4.5.	Дроби 1/3 и 2/3 в условии подталкивают к тому, чтобы рассмотреть троичное представление дроби т/п. Его цифры dv d2, ... получаются так: dt = (3 m) div п, m1 = (3m)modn, d2=(3-m,)divn, т2 = (3т)modп ит.д. Цифра d, указывает на положение точки в отрезке: если dt = 0, то точка в первой трети, если d, = 1 — во второй, а если d}=2 — в третьей. Однако при dl = 1 есть две различные ситуации. Если остаток (3m)modn=0, то т/п-1/3, и точка принадлежит всем множествам Ак, а если (3 m)modп>0, то 1/3< т/п<213, т.е. точка не принадлежит уже А(. Аналогично d2 указывает на положение точки в пределах трети отрезка: в ее первой, второй или третьей трети.
Итак, чтобы определить, принадлежит ли точка множеству Ак, где к>0, получим троичные цифры dp d2,..., dt_p dk дроби т/п (не запоминая их в массиве!). Если одна из них dt оказалась равной 1 при ненулевом остатке от деления тр точка mln не принадлежит Ак, иначе принадлежит. Естественно, если на некотором шаге остаток mt оказался нулевым, ответ положителен и без следующих цифр.
При jt=-l для ответа достаточно получить п цифр — либо один из остатков т. окажется равным 0 (ответ “да”), либо один из п-1 ненулевых остатков по
434
ПРИЛОЖЕНИЕ А
вторится. Если при этом цифры 1 не было, то не будет и дальше, поскольку последовательность цифр повторится после повторения остатка (ответ “да”). Если же появилась цифра 1 при ненулевом остатке, ответ “нет”. Текст программы должен оказаться заметно короче приведенных объяснений.
4.6.	Рассмотрим троичное представление числа т:
т = do'3° + dj-31 + ... + dr-3r = т= d$ + 3-(di + 3-(d2 +... + 3*(dr-i + 3- dr)...)).
Ясно, что d0 = wmod3. Если d0=O, то т кратно 3 и уравновешивается гирями с массами 3, 9, .... При da= 1 на правую чашку весов положим одну гирю с массой 1 и уравновесим массу т-\, кратную 3, с помощью остальных гирь. Если d0=2, значит, на правую чашку весов нужно положить 2 гири массой 1, но это невозможно. Положим гирю с массой 1 вместе с предметом — далее нужно уравновесить массу т+1, кратную 3, с помощью остальных гирь.
Итак, если d0=2 изменить на dQ=-1, то при любом d0 далее нужно уравновесить массу m-d0, кратную 3. Сократив ее на 3, получим dx как т mod 3 и т.д.
По последовательности d0, dx, ... определим: при d.= 1 гиря массой 3' кладется на левую чашку, а при d(=-l — на правую.
Не забудьте: масса предмета может превышать общую массу гирь. Кроме того, в варианте б придется работать с длинными числами.
4.7.	Представим числа а и b как (а+1)-1 и (Ь+1)-1. Нетрудно убедиться, что для любых неотрицательных к, 1,т,пи х=(а+1)‘(/н-1)'-1, y=(a+l)'”(fe+l)”-l число вида х+у+ху равно (a+l)*+"(b+l) "-1 и что А содержит все числа вида (а+1)т(й+1)л-1. Итак, достаточно проверить, верны ли оба условия cmod(a+l) = a, с mod (£+1) =6.
4.8.	Возводить в степень — безумие, ведь для этого понадобятся миллиарды цифр. Вместо этого рассмотрим, как образуется сумма цифр. В числе ал10’+ а 140"'|+ +...+ а^Ю'+йо выделим сумму десятичных цифр: ал(10”-1) + a_|(10',1-l)+ +...+ а1(101-1)+ (ал+ ал-,+ ...+ а1+а0). Все сомножители вида 10-1 делятся на 9, следовательно, окончательная сумма цифр любого числа равна 9, если число делится на 9, иначе остатку от деления на 9.
Кроме того, достаточно возводить в степень не само число, а его остаток от деления на 9 — сумма цифр не изменится. Однако при возведении в последовательные степени 1, 2, 3, ... остатки от деления на 9 повторяются. Составим таблицу остатков степеней чисел от 2 до 9 с показателями от 1 до 6 (остатки всех степеней 1 равны 1).
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
435
	1	2	3	4	5	6
2	2	4	8	7	5	1
3	3	0	0	0	0	0
4	4	7	1	4	7	1
S	5	7	8	4	2	1
6	6	0	0	0	0	0
7	7	4	1	7	4	1
8	8	1	8	1	8	1
9	0	0	0	0	0	0
Отсюда все степени числа 9 имеют окончательную сумму цифр 9, чисел 3 и 6 (кроме первой) — тоже 9, для 8 период повторения остатков равен 2, для 4 и 7 — 3, для 2 и 5 —6.
Итак, остается вместо исходных чисел взять их остатки от деления на 9 и реализовать приведенную таблицу. Сложность решения имеет константную оценку!
4.9.	Число N должно быть произведением цифр, поэтому оно не должно иметь простых делителей больше 10. Искомое число должно быть наименьшим, поэтому количество его цифр минимально, а сами цифры расположены в порядке неубывания. Итак, нужно начать с максимального делителя 9, делить N на 9, пока возможно, и накапливать в массиве цифры 9 от младших к старшим. Затем перейти к делителю 8 и т.д. Если после деления на 2 окажется, что Л/> 1, искомого числа нет. Размер массива определяется максимальным числом сомножителей, а оно не больше максимального log//, т.е. 32.
4.10.	С помощью индукции по к убедимся, что искомое число пк существует при любому. Ясно, что л, = 2. Пусть	Приписав слева 1 или 2, получим
число 10ы+21''г или 210^’+2*’'-r. При нечетном г, r=2m-l, 10tl+2t'lr= = 2w(5w+2m-l)= 2*((5“-l)/2+m). Число в скобках целое, поскольку 5*-1 нечетно. При четном г, г=2т, 210*1+2*'| г= 2-2*'1-5*'1+2‘'l>2m= 2‘ (5* '+wi).
Из полученных равенств следует, что очередная цифра <4=1 при нечетном иначе dk=2. Кроме того, ri=(5t l+rifc_1)div2 при нечетном rw, rt=5*'l+ + div 2 при четном г^. Итак, нужны длинные числа — значения г и степени числа 5. Сложность каждого действия с ними (сложение, деление на 2, умножение на 5 и на 10) линейна относительно количества цифр, поэтому общая сложность имеет оценку 0(Л2).
4.И.	Рассмотрим решение для N=5; для других N решение аналогично. Предположим, что число существует, и обозначим его х„хп_1...х2х15. По условию, хлхл_1...х15х5 = 5хлхл_1...х2х1 . Проведем это умножение, вычисляя цифры и переносы в следующий разряд: х,= 5x5 mod 10 = 5, .s, = 5x5 mod 10 = 2, х2= = (s1+5x1)modl0=7, ..., xk= (s^+Sx^/modlO, sk= (j^+Sx^/divlO ит.д. Выясним условие, при котором нужно остановиться. Общее количество цифр при умножении на 5 не увеличивается, поэтому хл=1. При этом 5^=0, иначе
436
ПРИЛОЖЕНИЕ А
старшая цифра произведения не равна 5. Итак, умножение заканчиваем, как только получим очередную цифру 1 и перенос 0; меньшего числа с указанным свойством быть не может.
Однако заранее неизвестно, существует ли искомое число, и, если это так, то сколько у него цифр. Тем не менее различных пар вида (х, s) может быть получено не более чем 99, и их несложно запомнить. Повторение некоторой пары (не более чем на сотом шаге) означает, что последовательность пар зациклится и пара (1,0) уже не появится. Итак, для запоминания цифр и контроля зацикливания понадобятся два массива размером в 100 байт. (При N= 5 искомое число имеет 42 цифры, а контроль зацикливания в действительности не нужен).
4.12.	(Смещение шара под углом 45° имеет равные по модулю горизонтальную и вертикальную составляющие, которые можно рассмотреть по отдельности. Отражение шара от вертикального борта изменяет горизонтальную составляющую на противоположную, а вертикальная при этом не изменяется. При отражении от горизонтального борта наоборот, изменяется вертикальная составляющая.
Рассмотрим изменение горизонтальной составляющей. После отражения от правого борта шар движется влево. Представим это как движение вправо в поле, зеркально отраженном относительно правого борта. Тогда отражение от левого борта представим как переход шара в следующее отражение поля, ориентированное также, как исходное поле. И так далее. Таким образом, если 0< L+x<K, 2К< L+x<3K, ..., 2пК< L+x<(2n+l)K, то шар остановится в поле с исходной ориентацией, иначе — с зеркальной (борт относим к полю с исходной ориентацией).
Итак, вычислим xl = (L+x)mod2K. Если х{<К, то х, — х-координата шарА, иначе х-координатой является 2К-х,. Координата шара по вертикали вычисляется аналогично.
4.13.	Ясно, при S<Nили S>5Nрешения нет. Пусть ир п3, и5 — количества монет по 1, 3 и 5 копеек. По условию nl+ni+ns=N, nl+3ni+5ns=S. Отсюда 2n3+4n5= S-N. Число слева четное, поэтому, если 5 -N нечетно, решения нет. Любое четное число можно представить в виде некоторой суммы четверок и двоек, поэтому при любом четном 5 -X решение есть.
4.14.	Не пытайтесь заполнять массив цифрами 0 и 1: два миллиарда цифр, даже двоичных, — это слишком много. По первым строкам Ао = 1, А,=01, А2 = 1001, А3=01101001 нетрудно понять, что вторая половина каждой строки является обращением ее первой половины (0 изменен на 1, 1 — на 0), вторая четверть — обращением первой четверти ит.д. Через А(п,т) обозначил! т-ю цифру строки Ап. Тогда А(и, т) = 1-А(и, т-2к), где к — максимальный показатель, при котором zn>2‘. Перейти от номера т к номеру zn-2* означает зачеркнуть 1 в к-м разряде двоичного разложения т. Поэтому A(n,zn)=A(n,0), если число 1 в двоичном разложении т четно, иначе А(п,т)= 1-А(п,0). Ясно также, что А(л,0)=0 при нечетном п и А(и, 0) = 1 при четном. Итак, если г — количество 1 в двоичном разложении т, то искомое значение равно (n+1+r) mod 2.
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
437
Глава 5
5.1.	Логарифмируем по основанию е, берем производную и получаем условие максимума а+а \па=с. Поскольку а+д1пд монотонно возрастает по а, применим бинарный поиск по целым значениям а. Само по себе равенство в целых точках может и не выполняться.
5.2.	Использовать слияние упорядоченных числовых последовательностей (см. подраздел 5.2.2), но различать не две, а три ситуации: al < а2, al > а2, al = а2. Для различных множественных операций действия в этих ситуациях отличаются, главным образом, решением о выводе текущего элемента в текст-результат.
Если бы множества были представлены в массивах, а не файлах, слияние все равно пришлось бы проводить в циклах while (а не for), поскольку количество элементов в множестве-результате заранее не известно.
5.3.	Применить слияние двух последовательностей чисел — моментов выхода из дома, к которым прибавлена длительность перехода.
5.4.	Для каждого рабочего можно найти максимальное время, на которое автобус должен задержаться, чтобы забрать его. Для рабочего на первой остановке эта задержка равна моменту его прихода, на остальных остановках — моменту прихода минус время проезда автобуса до остановки (или 0, если эта разница отрицательна). В примере из условия автобус приедет на вторую остановку не раньше момента 5, поэтому максимальные задержки, создаваемые рабочими на ней, равны 0, 3 и 7. Общее число рабочих (Kt + Кг = 6) на 1 больше количества мест (М = 5), поэтому нужно не взять одного рабочего — последнего на первой остановке, ведь его задержка 9 является наибольшей.
Таким образом, чтобы отобрать рабочих с М наименьшими задержками, используем идею “сдвинутого” слияния из предыдущей задачи, но, в отличие от нее, сливать нужно последовательности задержек, из которых вычтена длительность проезда автобуса к соответствующей остановке.
Однако непосредственно сливать файлы нельзя, поскольку все моменты прихода заданы в одном тексте и дополнительные файлы использовать запрещено. Поэтому придется сливать вход из массива и вход из текста в другой массив.
Если количество остановок N > 2, то “сдвинутые” слияния применяются многократно: результат слияния первой и второй остановок сливается с третьей и т.д. Начать можно с пустого массива — это позволит не описывать обработку первой остановки отдельно и сократит код.
Размеры массивов можно ограничить вместимостью автобуса М, поскольку подобрать на остановке больше, чем М рабочих, все равно нельзя. Поэтому, если в строке остались непрочитанные элементы, их можно пропустить. Самый простой и эффективный способ сделать это в большинстве реализаций языка Паскаль — вызвать readln (f).
Заметим, что технически удобнее не вычитать длительность проезда до очередной остановки из моментов прихода на нее, а прибавлять эту длительность к элементам массива, хранящего результат слияния данных по предыдущим остановкам. Обработав последнюю остановку, не забудьте сложить длительность проезда от нее до завода с максимальным элементом итогового массива.
438
ПРИЛОЖЕНИЕ А
5.5.	Отсортировать точки по возрастанию. Минимальным является расстояние между некоторыми из соседних точек. Их можно определить за один проход, запоминая в дополнительном массиве. Сложность — O(nlogn).
5.6.	Первый способ. Поскольку сортировать массив нельзя, можно с помощью процедуры treeSort (см. подраздел 5.3.4) построить начальную перестановку и к-1 раз выполнить тело цикла. В результате А[0] будет иметь fc-e по порядку значение. Это быстрее, чем полная сортировка массива, но оценка сложности все равно 0(n Zog п).
Второй способ. Для решения этой задачи существует алгоритм с оценкой сложности в среднем O(ri) (доказательство см. в [3,4]). Случайным образом выберем опорное значение в массиве. Как в алгоритме быстрой сортировки, в начале массива соберем значения, меньшие опорного, следом за ними — опорные значения, а в конце — большие опорного. Пусть их соответственно /Ир т2, ту Если т3>к, продолжим поиск среди первых т3 значений. Иначе, если т3+т2>к, опорное значение является искомым. Иначе продолжим поиск (к-m^-m^-ro значения среди последнихт3 значений.
5.7.	о) Если в процессе добавления чисел поддерживать упорядоченный по неубыванию массив, то с помощью бинарного поиска можно быстро найти место для нового числа. Однако необходимость “раздвигания” массива для вставки нового числа приведет к оценке сложности О(№). Используем пирамиду, которая имеет объем N и удовлетворяет условию (*): А[£]>A[2Zt+1], А[&] >А[2к+2]. Отсюда наибольшее из сохраняемых в пирамиде чисел находится в ее первом элементе А [0].
Если пирамида содержит меньше, чем N значений, то новое число добавляется к пирамиде как последнее и затем, пока оно нарушает условие (*), “всплывает” наверх. Пусть пирамида содержит N элементов. Если новое число больше значения А[0], добавлять его нет смысла, иначе нужно сделать его значением элемента А[0] и “спустить” вниз, пока оно нарушает условие (*). По окончании работы нужно отсортировать массив по неубыванию. Нетрудно убедиться, что суммарная сложность описанных действий — O(N\ogN).
б)	Определить по произвольному значению, содержит ли его пирамида, нельзя быстрее чем за линейное время. Поэтому придется “быстро искать” в упорядоченном массиве и затем его “долго раздвигать”. Кстати, если не заботиться о кроссплатформенности программы, раздвигание можно ускорить за счет использования процедуры move.
Можно также реализовать какую-либо из сложных структур данных (скажем, сбалансированные деревья поиска с надстройкой, позволяющей искать значение по номеру — см., например, [22]). Однако пррт указанном ограничении на N и с учетом затрат на чтение данных полученный эффект вряд ли будет стоить потраченных усилий.
5.8.	Примените поразрядную сортировку, в которой младший разряд задает день, средний — месяц, старший — год.
5.9.	Нетрудно убедиться, что сумма albi+ а2Ь2 + ...+ап Ьи минимальна, если числа а, упорядочены по возрастанию, bt — по убыванию. Используем массивы а_ и
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
439
Ь_ записей, содержащих (аналогично задаче 5.5) значение и его исходный индекс. После сортировки исходные номера элементов, образующих пары, равны: а_[1] . idxnb_[n] . idx, а—[2] . idxnb_[n-l] .idxит.д. Вывести их нужно в другом порядке: сначала исходный номер элемента из Ь, которому соответствует изначально первый элемент массива а и т.д. Поэтому построим дополнительный массив чисел a rev, k-й элемент которого хранит переставленный номер изначально k-го элемента массива а.
for i:=l to n do a_rev[a_[i].idx] := i
Тогда it, указанное в условии, равно b_ [n+1 -a_rev [k] ] . idx.
5.10.	Докажите, что если ap a2, ..., an и blt b2, bn упорядочены по убыванию, то сумма а^+а^Ч- •• +«/" максимальна. Для вычисления перестановки индексов учтите указание к предыдущему упражнению. Отличие в том, что обе последовательности нужно сортировать одинаково (обе по возрастанию или обе по убыванию).
5.11.	Введем числа в массив А и отсортируем по возрастанию. Используем три индекса /, т и I, которые будут удовлетворять условию f< т<1. Переберем тройки чисел А[/], А[т], А [/] так: внешний цикл — по f, внутренний — по I. Для каждой пары (ДО ищется минимальное значение т, для которого А [/]+А [ли] > A [Z]. Тогда все тройки вида A[f], А[/], А[/], где т< j<l, образуют треугольники, количество которых 1—т подсчитывается за 0(1). При переходе от А[/] к А[/+1] значение т не уменьшается, поэтому общее количество изменений индексов т и I при каждом/— O(N). Это дает оценку сложности всего алгоритма О(№).
5.12.	.Отсортируем гири по убыванию массы (сохранив информацию о начальных номерах) и разделим их на две группы. В первую группу включим самую тяжелую гирю, третью по массе, пятую и т.д., во вторую — вторую по массе гирю, четвертую и т.д. Очевидно, что суммарная масса первой группы больше, чем второй. Если строка заканчивается буквой R, поставим все гири первой группы на правую чашку, иначе на левую. Это будет окончательное расположение гирь. Предыдущие расположения образуются в обратном порядке с помощью анализа соседних букв слова с его конца и соответствующих снятий гирь с чашек. Например, пусть последняя буква L, т.е. первая группа гирь на левой чашке. Если предыдущая буква тоже L, снимаем самую легкую гирю из тех, что остались на чашках (на какой бы чашке она ни была). Если предыдущая буква R, снимаем самую тяжелую гирю с левой чашки — теперь вторая группа тяжелее, чем первая. И так далее.
5.13.	Используем массив строк и вспомогательный массив их номеров в порядке записи. Заполним эти массивы, прочитав текст. Символы каждой строки упорядочим по неубыванию. Отсортируем измененные строки, соответственно переставляя их номера. После этого строки-анаграммы идут подряд. Отсортируем номера в каждом множестве строк-анаграмм по возрастанию и выдадим их.
Оценить сложность этого алгоритма проблематично, цоскольку одно сравнение двух строк требует О(к) операций, где к — минимальная из длин строк. Впрочем, это оценка худшего случая, а в среднем она меньше.
440
ПРИЛОЖЕНИЕ А
Для сортировки внутри каждой строки удобно использовать сортировку подсчетом, а для сортировки измененных строк (она и составляет основную часть работы), по-видимому, в среднем лучше методы, основанные на сравнениях.
Если бы заранее было известно, что групп анаграмм мало, а размеры групп большие, то было бы целесообразно сортировать список измененных строк с помощью слияний, причем одновременно объединяя одинаковые измененные строки и сливая списки исходных номеров этих строк.
5.14.	Один из способов — переделать каждое слово и словосочетание в запись из двух полей: “настоящее” его значение и ключевое, в котором убраны пробелы, дефисы и апострофы, все буквы приведены к одному регистру (например, верхнему), и Г, Ё, е, I, I, Й, У и ъ заменены на г, Е, Е, И, и, и, У и Ь соответственно. После этого записи можно отсортировать по ключам, используя “обычное” сравнение строк.
У этого способа есть два серьезных недостатка. Один из них — размер данных “на ровном месте” увеличивается вдвое. Другой — если хоть немного изменить порядок (например, потребовать сортировать, считая, что ...<В<Г<Г<Д<Е<Ё<е<Ж<...), то свести все к порядку, задаваемому стандартной кодовой страницей, не удастся в принципе.
Поэтому более универсальный способ — написать свою функцию сравнения строк, похожую на stricmp языка C/C++, но пропускающую пробелы, дефисы и табуляции и сравнивающую не коды символов напрямую, а значения из вспомогательного массива ord_. В этом массиве, в частности, ord_ [' А' ] = ord_ [' а' ] =1, ord_ [' Б' ] = ord_ [' б' ] =2, ..., ord_ [' Г' ] = ord_[' г1 ] =ord_ [' Г' ] = ord_ [' г1' ] =4,...).
Если бы по условию не нужно было пропускать пробелы, дефисц и апострофы, можно было бы применить также поразрядную сортировку (значение элемента массива ord_ определяет, в какую “стопку” идет данное слово).
Глава 6
6.1.	Координаты вектора с после поворота будут (/с;0). Для вектора а видоизменим обычные формулы поворота с учетом того, что угол поворота противоположен (начальному) углу наклона с , следовательно, cos<p и sin ф можно получить как сД и -сД соответственно. Таким образом, тригонометрические операции заменены арифметическими, более быстрыми и менее рискованными в плане потерн точности.
6.2.	Если отрезки АВ и CD имеют одну точку пересечения, которая является внутренней для них, то длина кратчайшего пути — это минимальная из сумм длин АС+СВ и AD+DB. В любой другой ситуации это длина АВ.
6.3.	а) Ломаная ABCDA образует четырехугольник тогда и только тогда, когда отрезки АВ и CD (а также AD и ВС) не имеют общих точек (см. конец подраздела 6.1.'3).
б)	и в) Четырехугольник является выпуклым, если его диагонали пересекаются.
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
441
6.4.	Можно соединить точку (0,0) с концами отрезков и использовать углы, образуемые полученными отрезками с осью Ох, а также длины полученных отрезков.
6.5.	Реализовать один из способов, представленных в подразделе 6.2.3.
6.6.	См. определение в подразделе 6.2.1.
6.7.	Если прямая^ соединяющая заданные точки А и В, пересекает полигон, то нужно выбрать минимальную из длин путей, обходящих полигон слева и справа. “Левый” путь начнется отрезком АС, где С— вершина полигона, имеющая наибольшее (в угловом выражении) отклонение влево от АВ. Затем в пути будет, возможно, несколько соседних ребер, Пока не придем в вершину D, для которой следующее ребро отклоняется от DB вправо. “Правый” путь строится аналогично.
6.8.	В качестве Ак следует брать вершину, наиболее отдаленную от АД. (как прямой, а не отрезка). Для этого можно использовать один из следующих вариантов бинарного поиска.
1.	Обозначим угол наклона вектора А,Ау через <р. Среди углов наклона векторов AqA, , А,А2,... найти значения, ближайшие к <р и к <р±л. В этом случае ищется значение угла в упорядоченной последовательности углов.
2.	Вычислять расстояния до прямой АД от пробной вершины А( и от соседней с ней А(+1; в зависимости от того, что больше, принимается решение, среди вершин с какими номерами искать дальше (подобно тому, как в задаче 5.1 сравнивались значения 1(х-е) и г(х+е)).
Таким образом, в каждом способе бинарный поиск нужно запускать дважды, отдельно для каждой полуплоскости. Учтите также “зацикленность” полигона, приводящую сразу к нескольким мелким проблемам.
6.9.	По условию, ни одна вершина ни одного из полигонов не лежит на границе другого (ни на стороне, ни в вершине), поэтому остаются только возможности “строго внутри” и “строго снаружи”. По этой же Причине, если есть пересечения сторон разных полигонов, то точки пересечения лежат на сторонах, но не в вершинах. Значит, общее количество вершин полигонов пересечения равно сумме количеств:
вершин первого полигона, лежащих внутри второго;
вершин второго полигона, лежащих внутри первого;
пересечений сторон первого полигона со сторонами второго (как отрезков, а не прямых, содержащих отрезки).
Итак, нужно перебрать все точки первого полигона и подсчитать те из них, которые находятся внутри второго. Затем аналогично подсчитать точки второго полигона, попавшие в первый. Для этого удобно написать и использовать функцию проверки, лежит ли точка внутри полигона. Еще одна функция — проверки, пересекаются ли отрезки, — понадобится для подсчета точек пересечения сторон полигонов. Поскольку эти функции применяются для подсчета точек, удобно, чтобы они возвращали значения 0 или 1.
6.10.	Нетрудно убедиться, что одна из вершин треугольника совпадает с одной из вершин минимального квадрата (если это не так, то одна из сторон квадрата не
442
ПРИЛОЖЕНИЕ А
содержит вершин треугольника, и треугольник можно сдвинуть к этой стороне). Это позволяет сделать следующее. Поместим наименьший угол треугольника, скажем, А в начало координат с наибольшей стороной АВ на оси Ох и сторонами АС и ВС в пределах первой четверти. При повороте треугольника против часовой стрелки, пока сторона АВ не достигнет вертикального положения, проекция треугольника на ось Ох уменьшается, а на ось Оу — увеличивается. Значит, искомый квадрат получится, когда проекции сравняются. Для поиска нужного угла наклона стороны АВ можно использовать деление угла наклона пополам.
6.11.	Сначала обработать частные случаи, когда окружности совпадают или не пересекаются (находясь далеко одна от другой или одна внутри другой). В остальных ситуациях: найти dx и h (обозначения см. в задаче 6.12); зная расстояние dx и направление ОХВ (совпадающее с ОХО7 ), найти координаты точки В; зная расстояние h и направления В А и ВС (противонаправленные перпендикуляры к ОХО2), найти искомые координаты точек А и С.
6.12.	См. предыдущее упражнение и примечание к решению задачи 6.10.
Глава 7
7.1.	Используйте дополнительную структуру данных в виде пирамиды, в корне каждого поддерева которой, в отличие от подраздела 5.3.4, находится не максимальное, а минимальное значение.
7.2.	В двух вспомогательных (временных) файлах можно запомнить промежутки, когда одновременно были заняты и были свободны первый и второй каналы. С помощью этих файлов и третьего журнала можно определить, когда были заняты и когда были свободны все три канала и т.д.
Если промежутков, на которых все каналы заняты или все свободны, мало, такой алгоритм может оказаться самым быстрым — даже быстрее, чем оптимизация с помощью пирамиды (см. предыдущее упражнение). Но если таких промежутков много, то этот алгоритм оказывается медленным.
7.3.	Можно поддерживать “объединение всех отрезков с первого по i-й (последний рассмотренный)”, обязательно упрощая эту структуру при каждом нахождении пересечений. Список отрезков объединения можно представлять по-разному; по-видимому, наилучший способ — с помощью связного списка в свободной памяти.
Количество действий этого алгоритма можно оценить как О(№), поскольку для обработки очередного отрезка из входных данных приходится просматривать весь уже построенный список объединения, длина которого в худшем случае равна количеству обработанных отрезков. Поэтому для “плохих” входных данных этот алгоритм значительно хуже выметания.
Но для большинства “разумных” входных данных список, представляющий объединение, будет упрощаться за счет пересечений отрезков, поэтому его длина будет заметно меньше. Это и ускоряет выполнение алгоритма, и уменьшает объем используемой памяти. При большом количестве пересече
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
443
ний этот алгоритм оказывается лучше выметания, поскольку работает почти так же быстро, используя гораздо меньше памяти.
7.4.	Упорядочим точки на прямой по возрастанию координаты. Совместим левый конец отрезка с первой точкой и найдем, сколько точек накрыто отрезком. Да-Лее, пока правый конец отрезка не накроет последнюю точку, пошагово сдвигаем левый конец на одну точку и подсчитываем, как при соответствующем сдвиге правого конца изменяется количество накрытых точек. Запоминаем, при каком размещении левого конца это количество максимально. Сложность этого алгоритма определяется сложностью сортировки координат точек.
7.5.	См. задачу 7.3,7.4.
7.6.	Физика заканчивается на том, что многоугольник всплывет, когда отношение суммы длин погруженных ребер и их частей ко всему периметру станет равно р. Точками событий будут моменты, когда вода достигает очередной вершины многоугольника. В каждой такой точке считаем сумму погруженных частей. Если оказывается, что она превышает критическую, отрыв произошел на промежутке между предыдущей точкой события и данной; искомый уровень воды находим из того, что внутри отрезка между соседними точками событий зависимость размера погруженной части от уровня воды линейная. Подобно задаче “Интернет-провайдер”, вместо сортировки используется аналог слияния. Убедитесь, что ваша реализация работает и в вырожденном случае, когда в многоугольнике есть довольно большая горизонтальная верхняя сторона и отрыв происходит только после ее покрытия.
Для решения задачи в действительности существенна не выпуклость многоугольника, а монотонность “левой” и “правой” ломаных относительно вертикали.
Глава 8
8.1.	Ответ. Количество путей длиной п.
8.2.	Провести обход в Глубину или в ширину из произвольной вершины и проверить, остались ли непосещенные вершины.
8.3.	В алгоритме обхода произвольного графа (например, в глубину) использовать счетчик КС, увеличивая его перед вызовом dfs. В алгоритме df s отмечать вершины текущим значением этого счетчика.
8.4.	Неориентированный граф ацикличен, если, и только если, он является лесом (т.е. каждая его КС — дерево). Поэтому проще всего подсчитать количество КС к и проверить условие т=п-к, где п — количество вершин, т — ребер (в частности, для дерева т = п-1). Разумеется, для решения задачи можно и модифицировать проверку ацикличности орграфа, но это сложнее...
8.5.	Используйте обход в ширину и обратный ход по вершинам-предшественникам.
8.6.	Сделать это на основе dfs, запоминая для каждой вершины предыдущую ей на пути от начальной.
8.7.	Запустим рекурсивный обход в глубину с заданной вершиной v в качестве начальной, в котором учитывается глубина рекурсии. Длина цикла не меньше
444
ПРИЛОЖЕНИЕ А
трех, поэтому v принадлежит циклу тогда и только тогда, когда появляется в списке смежности некоторой вершины w, достигнутой на глубине рекурсии больше 1. Вывести цикл можно, возвращаясь из рекурсивных вызовов.
8.8.	Для каждой вершины хранить предшественника P(v), как при выделении остовного дерева. Условие “w отмечена 1” в строке 5 алгоритма dfs_cycle(v) (см. задачу 8.3) расширить до следующего:
(ы отмечена 1) и (w * v) и (w * Р(v)).
Заметим: эта модификация позволит применять алгоритм к неориентированным графам.
8.9.	Вершины графа — рыцари, пары враждующих рыцарей — ребра. Начав обход в глубину с какой-либо вершины, будем отмечать вершины, достижимое за четное количество шагов, нулем, а за нечетное — единицей. Если окажется, что в списке смежности очередной вершины v есть вершина w, отмеченная так же, как у, то разбиение невозможно. Учтите, что граф может иметь несколько компонент связности.
8.10.	Рассмотрим граф, образованный городами и дорогами, и определим, возможна ли встреча. Если роботы находятся в разных компонентах связности, встреча невозможна. Далее предположим, что они в одной компоненте.
Если два робота с одинаковыми скоростями находятся в одном и том же городе, то ясно, что, двигаясь вместе, они встретятся с третьим роботом, причем длины их маршрутов не больше числа городов. Пусть два робота с одинаковыми скоростями находятся в разных городах. Есть две возможности — между этими городами есть маршрут четной длины или его нет.
1.	Маршрут четной длины есть. Нетрудно убедиться, что роботы встретятся в одном городе, причем длины их маршрутов до встречи также не больще числа городов. Тогда встреча всех трех роботов произойдет.
2.	Маршрута четной длины нет. Тогда роботы с одинаковыми скоростями могут встретиться только на середине дороги. Если третий робот имеет другую скорость, он не может встретиться с первыми двумя на середине дороги, поэтому встреча всех роботов невозможна. Если его скорость такая же, то от его исходного города существует маршрут четной длины хотя бы до одного из других двух городов, поэтому встреча произойдет.
Выяснив, что встреча произойдет, перейдем к поиску минимального времени встречи. Для этого понадобится дополнительный граф.
Если все скорости равны 1 (или 2), то суммарная скорость сближения роботов равна 2 (или 4), поэтому встреча всех роботов возможна через количество секунд, кратное 1/2 (или 1/4), в городе или на середине дороги. Образуем Граф, вершины которого — города и середины дорог. За единицу времени примем 1/2 (или 1/4) секунды.	*
При наборах скоростей (1,1,2) или (1,2,2) встреча всех роботов возможна только в городе или на расстоянии 1/3 от одного из городов. Вершинами графа будут города и точки дорог на расстоянии 1/3 и 2/3 от городов. Время будем измерять в 1/3 с.
Проимитируем все возможные одновременные перемещения роботов, учитывая, что робот может возвращаться в уже пройденные города и, начав
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
445
двигаться по дороге, не меняет направления.
В процессе имитации будем заполнять таблицу А, строки которой индексированы номерами вершин, столбцы — моментами времени. Значение Аи представляет посещение роботами вершины i в момент г. Это можно реализовать разныци способами, например, сначала А.=0, а при посещении вершины i роботом 1 в момент t к Аи прибавляется 1 (если не было прибавлено раньше), роботом 2 — 2, роботом 3 — 4. Тогда условие Аи=1 является признаком встречи трех роботов, условия Au mod2=0, A;imod 4div 2 = 0, Ai(div4=0 — что появление робота соответственно 1,2, 3 в А.гне отмечено.
С учетом оценок длин маршрутов, приведенных выше, необходимое количество столбцов таблицы не больше удвоенного числа вершин графа.
8.11.	Эксцентриситеты вершин определяются на основе алгоритмов вычисления расстояний; диаметр и радиус вычисляются по эксцентриситетам за линейное время.
8.12.	Рассмотрим неориентированные графы; для орграфов рассуждения и алгоритмы аналогичны. Используем алгоритмы вычисления расстояний из задачи 8.2 (см. подраздел 8.3.2), только “расстоянием” между вершинами будет достижимость, которую представим Значением 1. Недостижимость вершин (расстояние “<»”) представим значением 0. В алгоритме, основанном на обходе в ширину, если обход, запущенный с начальной вершиной v, привел в вершину w, то “расстояние” d(v,w) полагается равным 1. В алгоритме Флойда—Уоршалла вместо сложения использовать умножение, а вместо вычисления минимума — вычисление максимума. Значение D[i,j] окажется равным 1 тогда и только тогда, когда между вершинами i и j есть маршрут.
8.13.	В процессе чтения списка дуг обращать их, чтобы вершины-сыновья ссылались на вершину-отца. Такой граф целесообразно представить с помощью одномерного массива, в котором отец вершины i хранится как значение i-ro элемента. Например, дерево из условия будет представлено последовательными значениями -1113 (-1 соответствует корню дерева).
Затем можно просто выписать пути от каждой из двух заданных вершин до корня и выделить их общую часть (она начинается в искомом минимальном общем предке и заканчивается в корне). Можно также, ради экономии памяти, пройдя от каждой вершины до корня, узнать разницу их глубин М, затем из более углубленной вершины подняться на Ad вершин вверх, далее от полученной вершины и другой заданной вершины, пока не оказались в одной вершине, подниматься по дереву одновременно.
8.14.	Рассмотрим мультиорграф, вершины которого — буквы латинского алфавита, которые встречаются как первые и последние в словах (и только они), дуги — слова. Чайнворд можно сложить, если в этом мультиорграфе есть ориентированный цикл, содержащий все дуги. Нетрудно убедиться, что такой цикл есть, если орграф сильно связен и у каждой вершины поровну входящих и выходящих дуг. Построение цикла аналогично построению эйлерова цикла неориентированного мультиграфа, только вместо “входных” и “выходных” ребер вершины рассматриваются входящие и выходящие дуги.
Реализуя алгоритм, заметим, что в структуре смежности для дуг орграфа, в отличие от ребер графа, не нужны “парные” элементы в списках.
446
ПРИЛОЖЕНИЕ А
8.15.	Условие обеспечивает, что поселки и стороны дорог образуют орграф, в котором у каждой вершины поровну входящих и выходящих дуг. Этот орграф сильно связен, если связен неориентированный граф поселков и дорог. См. также указание к предыдущему упражнению.
8.16.	а) Те из чисел 0, 1.п, которые встречаются на костях набора, — это вер-
шины графа, кости — ребра (дубли являются петлями). Цепочка, состоящая из всех костей, — это эйлеров цикл или эйлерова цепь в неориентированном графе. Таким образом, нужно построить граф и проверить, что он связен и что все вершины (кроме, может быть, двух) имеют четные степени. Не забудьте, что петля добавляет к степени вершины не 1, а 2.
б) Проверим критерий эйлеровости по каждой компоненте связности отдельно. Поскольку в каждой компоненте число вершин нечетной степени обязательно четно, то количество цепочек равно половине этого числа (и равно 1, если все степени вершин четны). Учтите, что изолированная вершина с петлей образует отдельную компоненту связности.
Глава 9
9.1.	Максимальное расстояние выбирается среди расстояний, т.е. длин кратчайших маршрутов между вершинами. Поэтому найдите все расстояния между вершинами с помощью алгоритма Флойда—Уоршалла.
9.2.	о) Аналогично задаче 9.2 использовать поиск в ширину. Отношение достижимости может быть несимметричным. Для восстановления пути понадобится обратный ход, поэтому нужно запоминать предшествующие вершины.
б)	Отмечать и подсчитывать достигнутые узлы (см. п. а).
9.3.	В задаче о лабиринте изменить условие выхода из процедуры Run.
9.4.	Взяв одну из заданных клеток в качестве начальной, запустим обход лабиринта в ширину (см. подраздел 9.1.2). Отмечать клетки будем парами чисел — номер шага, на котором клетка достигнута, и количество различных кратчайших путей из начальной клетки; ее отметка — (0,1). На Л-м шаге (£>1) просматриваем клетки, достигнутые на (А:-1)-м шаге и запомненные в очереди. Для каждой такой клетки v с отметкой (к-1,т) просматриваем всех ее соседей w. Если клетка w до сих пор не достигнута, она получает отметку (к, т) и заносится в очередь. Если w уже имеет отметку (k,s), то к j прибавляем m {не добавляя w в очередь, так как она там уже есть). Если w имеет отметку (р, s), где р<к,с ней ничего не делаем. Так действуем, пока не будет достигнута целевая клетка (вторая из заданных). Если на каком-то шаге очередь окажется пустой, значит, целевая клетка недостижима.
9.5.	Образуйте граф роседства клеток, считая смежными клетки, которые видны одна из другой (они находятся на одной вертикали или горизонтали и между ними нет перегородок). Перебор всех соседей вершины реализуется четырьмя циклами (вверх, вниз, влево и вправо от данной клетки до появления преграды или выхода за пределы лабиринта). При желании эти циклы можно унифицировать (внешний цикл for перебирает четыре направления, внутренний while — клетки в текущем направлении).
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
447
9.6.	Считать вершиной графа не клетку лабиринта, а пару “клетка-направление”. Поскольку начальное направление во входных данных не задано, начальными для поиска в ширину будут сразу четыре вершины графа (одна и та же клетка, все направления).
9.7.	Вершины графа— перекрестки, ребра— улицы. Закрытые перекрестки и улицы, ведущие к ним, в графе отсутствуют. Применить поиск в ширину, запоминая вершину-предшественника. Представление графа, стандарное для клетчатых полей, — двумерный массив. Клетки, не являющиеся вершинами графа, будут также представлены в нем, поэтому обеспечьте, чтобы при поиске в ширину они не посещались.
9.8.	Прочитать вход и заполнить матрицу, представляющую лист, номерами цветов. В грофе клеток, в отличие от задачи 9.1, смежными считать соседние одноцветные клетки (по горизонтали, вертикали или диагонали). В таком графе одноцветные фигуры будут компонентами связности.
9.9.	Для решения попытаемся модифицировать поиск в ширину. Рассмотрим две такие модификации. Первая из них довольно проста, но требует много памяти, вторая использует специальный прием, но позволяет получить весьма экономный по памяти алгоритм.
Первый способ. Можно хранить расстояния от начальной клетки в трехмерном массиве: два измерения — “обычные” координаты, третье — количество применений магии. Таким образом, элемент Di st [i, j , m] должен содержать минимальное количество ходов, которое необходимо, чтобы дойти до клетки (i, j j, использовав ровно m магических прохождений сквозь стены.
Тройки (i, j ,m) также будем хранить в очереди. Рассмотрим, что нужно делать, удалив из головы очереди тройку (i, j , m). Для каждой из четырех соседних клеток (i+di [d] , j+dj [d]) проверим, находится ли она внутри лабиринта и есть ли стена между нею и текущей клеткой (i, j). Если стены нет и элемент Dist [i+di [d], j +dj [d] , m] еще не заполнен, присвоим ему значение Dist [i, j , m] +1 (как при обычном поиске в ширину). Если стена есть, но запас магии еще не исчерпан (т< К), то аналогично проверим (и, возможно, заполним) элемент Dist [i+di [d] , j +dj [d] , m+1]. Заполняя любой из упомянуты^ соседних элементов, тройку его координат занесем в конец очереди (одной и той же для обычных переходов к соседям и проходов сквозь стены).
Кратчайший согласно условию путь найден, когда впервые достигнута целевая клетка (Р, Q) (неважно, при каком tn). Легко видеть, что оценки и объема памяти, и времени работы этого алгоритма равны O(KN2).
Как видим, рассмотренный способ весьма затратен по памяти. А данная задача (именно с такими ограничениями) была предложена2 на олимпиаде в 2001 году, когда использовались исключительно DOS-овские среды Borland Pascal и Borland C++, поэтому в тех условиях представленное решение было неприемлемым.
Второй способ. В действительности третье измерение массива является избыточным. Используем двумерный массив записей, которые представляют
2 Автор задачи (в несколько иной формулировке) — Сергей Диденко.
448
ПРИЛОЖЕНИЕ А
клетки и включают минимальное количество шагов и остаток запаса магии при достижении клетки. Значение элемента массива не только присваивается при первом достижении клетки, но и изменяется, если новый путь приводит в клетку с большим запасом магии (пусть даже с большим количеством ходов). Когда впервые достигается целевая клетка (Р, Q), ее значение расстояния является ответом задачи.
Не очевидно, что такое решение правильно. Тем более, что оно противоречит основному свойству поиска в ширину "'первое же посещение вершины определяет минимальный путь”. По-хорошему, следовало бы доказать правильность этого алгоритма, но мы лишь подробно рассмотрим пример.
Пусть стены размещены так, как указано на рисунке ниже, цель находится в клетке (2; 4), исходный запас магических прохождений К= 1. Клетки (1; 2) и (2; 1) получают значения (1; 1), затем клетка (1;3) — значение (2;0) (единица магии израсходована), а клетки (2; 2) и (3; 1) — значения (2; 1). Клетка (1; 3) достигнута без запаса магии, поэтому из нее можно пойти только в клетку (2; 3), которая получает значение (3;0). И так далее. Наконец, в клетке (2; 3) значение (3;0), не позволявшее пройти сквозь стену справа, изменяется на (5; 1). Это позволяет пройти сквозь стену в целевую клетку (2; 4), получив тем самым кратчайший по условию путь.
В то же время, с начальным запасом магии К>2 целевая клетка достижима раньше, поскольку можно, например, пройти сквозь стену из (1;2) в (1; 3) и из (1; 3) в (1;4).
Отметим, что прием с заменой меньшего запаса магии большим существенно сокращает расход памяти. Однако при этом теряется возможность восстановить путь с помощью обратного хода.
9.10.	См. задачу 8.1. Вначале, удаляя листья дерева, припишем каждому их соседу и расстояние от листьев D(u) — максимальную из длин удаляемых ребер, инцидентных и. Затем будем удалять вершины, ставшие листьями, по одной, выбирая на каждом шаге вершину с наименьшим D. Если удаляется лист v и ребро (w,v) длиной I, то вычисляется новое D(u) = max{D(u), D(v)+Z}. После некоторого шага останется две вершины. Если их расстояния от листьев равны, они образуют центр, иначе центром является вершина с большим D.
Если использовать в основном такие же структуры данных, как в задаче 8.1, а также пирамиду для выбора вершины с наименьшим D, сложность алгоритма должна иметь оценку О(п log и).
9.11.	Примените алгоритм Прима, начав с произвольно выбранной точки. Учтите, что хранить все длины ребер (расстояния между точками) нереально, но на
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
449
каждом шаге для точек, вошедших в дерево, желательно знать “ближайших соседей” из числа не вошедших.
9.12.	Ответ. Нет. Например, рассмотрим граф с ребрами (1,2,9), (1,3,9), (2,4,1), (3,4,1), где первые два числа — номера вершин, а третье — длина ребра. Если источником Является вершина 1, то по алгоритму Дейкстры строится дерево с ребрами, вес которых 9,9 и 1, тогда как ОДМВ имеет ребра с весом 1,1,9.
9.13.	На основе алгоритма Дейкстры нетрудно написать алгоритм построения кратчайших путей, а не только расстояний. Используем массив Р, который для каждой вершины v хранит предыдущую вершину на кратчайшем пути от s к v (см. подраздел 8.3.5). К циклам в строках 2-4 и 8 добавим соответствующие присваивания элементу P[v], После этого кратчайший путь к любой вершине v представляет собой последовательность v, P[v], P[P[vJ], ..., s, прочитанную справа налево.
9.14.	Примените алгоритм Дейкстры, вычисляя текущее расстояние от источника, т.е. задержку, как d(v) := min{d(v), max{d(w), l(w, v)}}.
9.15.	Рассмотрим граф, вершины которого — заданные точки и концы стволов. Его ребра — это отрезки прямых, которые соединяют вершины, не пересекая стволов. Для этого графа применим алгоритм Дейкстры с одной из заданных точек в качестве источника. Для экономии памяти можно не строить ребра графа в одной из структур данных, а вместо перебора соседей (цикл forwe N (v)) определять, какие пары вершин создают ребра, а какие — нет (используя геометрические соотношения). Этот подход аналогичен определению смежности клеток в клетчатых полях, где “геометрия” значительно проще.
9.16.	Проведем всевозможные касательные, общие для пар кругов (кратеров) и не пересекающие другие круги (см. задачу 6.11). Точки старта и финиша можно условно считать окружностями радиуса 0. Найдем координаты точек касания. Точки касания на разных кругах можно соединить отрезками, на одном круге — дугой.
По координатам точек касания вычислим длины этих отрезков и дуг, а также длины отрезков, которые соединяют заданные точки с точками касания, не пересекая заданные круги. Все эти точки образуют вершины графа, по которому может происходить движение. Ребра графа нагружены длинами соответствующих отрезков и дуг. Далее используем алгоритм Дейкстры.
Граф можно как сначала построить, а затем анализировать, так и находить его вершины и ребра динамически, по мере надобности. В первой реализации раздельность геометрии и “графового” алгоритма значительно упрощает отладку, во второй — значительно экономится память и немного уменьшается время работы.
9.17.	Ясно, что отношение “владеет акциями” можно представить орграфом. По входным данным построим структуру смежности орграфа с дугами (i,j), вес которых р. Для каждой компании i от 1 до N построим и выведем множество компаний, контролируемых ею. Для этого вначале найдем компании, контролируемые компанией i согласно пункту б в условии. Отметим их как контролируемые и добавим в очередь. Остальные компании будут получать по-
450
ПРИЛОЖЕНИЕ А
ложительный вес w, вначале равный 0. При обработке компании j в очереди, если j владеет р% акций неконтролируемой пока компании к, то w(k)=w(k)+p. Если w(k) стало больше 50, отметим к как контролируемую и добавим в очередь. После обработки всех вершин в очереди выведем их как контролируемые компанией i.
9.18.	Для чтения исходных данных используйте технику, описанную в разделе 2.2. Превратите названия веществ в номера в массиве данных о них. Лабинский алфавит имеет 26 букв, поэтому названий не более 26x26=676 (вот почему в условии количество названий не ограничено). Пусть С, и С2 — заглавная и строчная латинские буквы. По названию СхСг можно вычислить номер как: (ord(Ci) -ord(1А'))*26 + (ord (С2)-ord(1 а1))•
Ясно, что такие номера от 0 до 26x26-1 соответствуют названиям взаимно однозначно и являются, по сути, значениями хеш-функции. Для представления веществ используем массив Substance, индексированный номерами от 0 до 26x26 -1. Название восстанавливается по номеру к так:
Ci = chr(k div 26 + ord('A'));
С2 = chr(k mod 26 + ord(1 a1)) •
Поскольку количества входных и выходных веществ реакций не ограничены, вместо массивов веществ используйте связанные списки в свободной памяти.
Глава 10
10.1.	Распределению взаимно однозначно соответствует состав (&,, кгЛ • •-,&„), в котором ^i+fc2+ ...+кт=п, а все к>\. Каждое число kt в составе представить как последовательность из к. цифр 1, разделить последовательности цифрами 0 и вычеркнуть одну цифру 1 из каждой последовательности.
10.2.
Общее количество способов заполнения карточки, при которых угадано ровно i номеров, равно С'м  С^,~'м (г угаданных номеров — любые среди М выпавших, a K-i не угаданных — среди N-М не выпавших). Общее же количество способов выбрать К номеров равно . Отсюда математическое
ожидание равно
„к Zi=ovi ’С’м Cn-m ''N
где v( — выигрыш за угадывание i номеров.
10.3. Для малых значе'Ний и=1, 2,... найдем минимальное время и запишем его в следующей таблице.
к
п	1	2	3	4	5	6	7	8	9	10	11	12	13
Т(п)	0	1	2	2	3	3	3	4	4	4	4	4	5
Присмотримся к значениям Т(п). Пронумеруем друзей 1, 2, .... При п=2 нужен звонок 1->2, при п=3 — звонки 1—>2, 1->3 или 1—>2, 2—>3. В первом из этих случаев можно добавить звонок 2—>4, а во втором — 1—>4, не увеличи
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
451
вая общего времени. Схема звонков с указанием моментов их окончания при л=4 представлена на рис. А. 1, а.
а)л = 4	б)л = 5,6,7
Рис. А. 1. Звонки для четырех и семи друзей
Как видим, звонок пятому другу увеличивает время, кто бы из друзей 2, 3 или 4 ему ни звонил. В то же время добавление всех звонков от 2, 3 и 4 увеличивает время также на 1 и дает схему звонков, как на рис. А.1, б. Заметим, что левая ветвь новой схемы совпадает со схемой для четырех друзей, а правая — с ее левой ветвью (схемой для двух друзей). В новой схеме появляется пять возможностей звонков, любой из которых и все они вместе увеличивают время на 1.
Приведенные наблюдения подводят к понятию заполненной схемы — добавление любого возможного звонка к ней увеличивает общее время на 1. Из таблицы ясно, что эти схемы должны существовать при п=1,2,4,7,12. Чтобы продолжить этот ряд, заметим, что две последовательные заполненные схемы становятся правой и левой ветвями следующей схемы и, по сути, являются рекурсивными. Отсюда же получим рекуррентное соотношение для количеств друзей Vk (к — общее время “работы схемы”) в заполненных схемах:
Vk = 1 + Vjm + Vjt-2 при к > 2, Уо = 1, Vi = 2.
По этому рекуррентному соотношению будем итеративно вычислять значения Ук, пока не получим наименьший номер к, при котором Vt>«. С учетом условия Vt может не поместиться в типе longint, поэтому для получения нужного к используем равносильное условие 1 + VM + V^2>n или V^ >п-1 - V^.
Отметим, что числа Vk связаны с числами Фибоначчи fk не только сходством рекуррентных соотношений (вспомним, чтопри Л>3,/, = 1, /2=1). Из приведенной выше таблицы видим: V0=/3-l, Vl=f4-1, V2=/5-l-Доказать, что V^f^-1, нетрудно с помощью индукции. Тогда 7*-^,= А+з“Л+2 =А+1> откуда V = V^+Уж- Поскольку Уй = \=^, отсюда следуют равенства vk= и =Ач-1-
10.4.	Обозначим через gn искомое количество способов. Ясно, что g, = 1, g2=2. Пусть
л>3. Покрытия прямоугольника 2хи разбиваются на два подмножества: покрытия, у которых две последние плитки расположены горизонтально, и покрытия,
452
ПРИЛОЖЕНИЕ А
у которых последняя плитка вертикальна. Тогда gn= g/r.2+S„~f Таким образом, g„ — это число Фибоначчи/^,. Используйте “длинную арифметику”.
10.5.	Нетрудно убедиться, что х,= 1, х2=4. Поскольку плитки 1x2 кладутся только “вдоль” дорожки, укладки плиток на левой и правой половинах дорожки не зависят одна от другой, т.е. х„=ул2, где уп — количество способов уложить плитки в ряд шириной 1. Ясно, у, = 1, у2—2. Рассмотрим ряд длиной и>3. Его последняя плитка может иметь длину 1 или 2, продолжая ряд длиной соответственно п-1 или п-2. Отсюда ул=ул_1+ул-2, т.е. уп является числом Фибонач-чи/л+1. Итак, xn=fn+2. По условию, п может доходить до 103, поэтому придется использовать “длинную” арифметику, ведь для представления /10012 нужно больше, чем 400 десятичных цифр. Рассмотрим два способа вычислений.
Первый способ. Можно вычислить fn+l с помощью рекуррентного соотноше-ния/„=Л-1+Л-2 и затем возвести в квадрат. Придется выполнить 0(и) сложений и одно умножение “длинных” чисел (не считая вспомогательных действий). При условии, что сложение и умножение Л-значных чисел имеют сложность соответственно <Э(к) и &(к2), этот способ вычисления хл имеет сложность 0(и2).
Второй способ. Используем при л>3 соотношение	обозначив
через рп произведение/,/,,,:
Хп =fn+l2 = (fn +fn_v)2 =fn +fn_l2 + 2fnfn_i = X„-1 + X„-2 + 2pn.
HoP=fnf^=(f^ +fn_2)f^ =fj+fr.2-f^ = x^2+prt_l. Отсюдахл_2=рл-рл_1 или x„_, - рл+1 -р„. Окончательно для вычисления х„ получаем систему рекуррентных соотношений:
Р2= 1,Х] = 1,х2 = 4;
Рп = Хп-2 +Рп-1, хп = Х„-2 +р„+1 +Рп при п > 3.
Как видим, на каждом шаге вычислений по этой системе нужны только три сложения (не считая присваиваний). Оценка сложности этого способа также 0(п2), но он не требует умножений и поэтому программируется проще.
Вместо хл= ^2+P„+i+P„ можно использовать и более очевидное соотношение хл= х^ч-хл_,+2р„, не требующее “опережающего” вычисления рп+}. Но тогда придется увеличить количество длинных арифметических операций на каждом шаге (с трех до четырех).
10.6.	Пронумеруем точки слева направо на прямых 1,..., т и 1......п. Начнем
подсчет с точки 1 на первой прямой. Отрезки этой точки пересекают отрезки, выходящие из точек 2, ..., т. Очевидно, отрезок, ведущий в любую точку I на второй прямой, имеет (/-l)-(m-l) точек пересечения. Для точки к, где к> 1, точки пересечения с отрезками точек 1,..., £-1 уже подсчитаны, а количество точек пересечения отрезка, ведущего в точку I, с отрезками точек к+1,...,т равно (/-1 )(т-к). Итак, общее число точек пересечения равно
SZiSLG-D(m-k) = £Г=1(т-ВД=1(/-1) =
= £”=,(zn-fc)x«(n-l)/2 = т(т-1)п(п-1)/4.
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
453
Эту формулу можно вывести иначе. Каждой точке пересечения отрезков взаимно однозначно соответствует трапеция (вершинами трапеции являются концы отрезков, диагоналями — сами отрезки). Количество таких трапеций определяется количеством пар вершин на каждой прямой (и С2 соответственно). Отсюда получаем С2 • С2 = m("l~1>fl(f|-|>.
Кстати, на олимпиадах условия подобных задач иногда запутывают: гарантируют, что никакие три отрезка не пересекаются в одной точке, но при этом во входных данных указывают координаты всех точек. И участнику психологически непросто сообразить, что указанные координаты не нужны...
10.7.	Рассмотрим задачу “Найти количество точек пересечения диагоналей выпуклого n-угольника, если известно, что никакие три диагонали не пересекаются в одной точке” и решим ее двумя способами, аналогичными способам решения упражнения 10.6. Рассуждения по первому способу дают левую часть тождества, по второму — правую.
10.8.	Обозначим через х. искомое количество для клетки i. Ясно, что х0 = 1, Xj = 1.
Все последовательности ходов можно разбить по длине последнего хода. Тогда x=xt , + х 2+... +xi Jt, где х =0 при i<0. Нетрудно также убедиться, что х1=2 х/ ] -хИспользуйте табличную технику и длинную арифметику.
10.9.	Этот подход ошибочен, поскольку учитывает способы (в действительности невозможные), при которых Иванов и Петров заняли одно и то же место на одном и том же круге. Возможное правильное решение: поставить серию задач “сколькими способами Иванов и Петров могли набрать s и sp очков соответственно, проехав круги с первого по n-й?” и использовать трехмерную таблицу промежуточных значений и значительно более громоздкие рекуррентные формулы.
10.10.	Достаточно добавить в самом начале функции проверку
if (the_row = N_ROWS) and
(init_num_left[the_col] - num_left[the_col] = N_ROWS-1) then { вернуть нуль }
10.11.	Несмотря на схожесть с предыдущим вопросом, изменения гораздо сложнее. Например, можно заменить циклы, в которых происходят рекурсивные вызовы, следующим кодом:
If (num_in_row = N—USED) then begin
for i := 1 to N_COLS-N_USED+1 do begin
result := result + count_all(the_row+l, i, 1, left_num)*left_num[i];
if (the_row+l = N_ROWS) and (left_num[i] = init_num__lef t [i] ) then break
end;
end
else begin
for i := the_col+l to N_COLS - N_USED + num_in_row+l
454
ПРИЛОЖЕНИЕ А
do begin
result := result + count_all(the_row, i, num_in_row+l, left_num)*left_num[i];
if (the_row = N_ROWS) and (left_num[i] = init_num_left[i])
then break
end;
end;
Суть этой модификации: если в последней строке есть некоторый i-й столбец, в котором все клетки свободны (left_num[i] = init_num_left [i]), то дальнейшие (с большими значениями i) рекурсивные вызовы пропускаются.
Нужно также отсечь ситуации, когда пустой столбец окажется правее последнего N_USED-ro числа в последней N_ROWS-ft строке. Для этого придется изменить условие выхода для тривиальной подзадачи следующим образом: if (the_row = N_ROWS) and (num_in_row = N_USED) then begin
for i := the_col+l to N_COLS do
if num_left[i] = init_num_left[i] then begin count_all := 0; exit;
end;
count_all :=1 ; exit;
end;
10.12.	Обозначим входную строку jjS2.. .$я, a T(sis2.. ,sn) — искомое количество. Очевидно, что TX-s,)31, 7’(s,52) = 1+m, где m=l, если sts2 представляет число не больше 26, иначе 0. При п>2 очевидна рекуррентная формула искомого количества: T(sls2...si)= T(s2...sn)+ m-T(s3...si). Используйте табличную технику, наращивая длину подстрок вида	где 1 < i, j < п.
10.13.	Рассмотрим частный случай N= 1 и обозначим ответ для такой задачи через Т(М), т.е. Т(М) — это массив из 10 чисел. Будем работать с такими массивами как с векторами, поэлементно складывая их друг с другом и умножая на скаляры (целые числа). С помощью этих операций Фи® запишем для Т(М) рекуррентное соотношение. Для этого представим М в виде 10А+В, где В=А/mod 10.
Если А=0, то, очевидно, T(Af)= (0,1,...,1,0,...,0).
В 9-В
Если же А > 0, то всю строку разобьем на три части. Первая часть имеет вид 123456789; ответ для нее — (0,1,1,1,1,1,1,1,1,1). Вторая часть при А = 1 пуста, а при А> 1 содержит записи чисел от 10 до 10(А-1) + 9, т.е. по десять экземпляров записи каждого из чисел от 1 до А -1 * к которым дописаны цифры 0,1,..., 9. Например, при А = 2 записи чисел от 10 до 19 образованы десятью единицами, к каждой из которых дописана одна из десятичных цифр. Этой части соответствует вектор-слагаемое
10® ПА-D Ф(А-1) ® (1,1,1,1,1,1,1,1,1,1),
где 10 и А-1 — числа, Г(А-1) и (1,1,..., 1) — векторы-решения подзадач.
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
455
В третью часть входят записи чисел от 10А до 10А+В; они представляют собой В+1 экземпляр числа А, к которому дописаны цифры 0, 1,..., В. Этой части соответствует слагаемое
(В+1)®Т(А)Ф (Ц,...1,0,...,0) •
4	В+1	9-е
Три полученных выражения (или два, если А = 1) являются слагаемыми в рекуррентном соотношении для Т(М).
Чтобы получить ответ для исходной задачи (числа записаны, начиная не с 1, а с некоторого N), достаточно покомпонентно отнять T(N-1) от Т(М).
Реализация алгоритма будет более ясной и лучше соответствующей приведенным выкладкам, если использовать компилятор, который допускает перегрузку операций (лучше всего C++, как компромиссный вариант — Free Pascal).
10.14.	$(п, m) способов. См. задачу о разбиении на подмножества.
10.15.	Убедитесь, что первый элемент перестановки равен округленному вверх частному Г 7^17 "I. Когда первый элемент найден, выяснение второго аналогично: к заменяется на остаток от деления к на (п-1)!, п уменьшается на 1 и т.д.
Дополнительную трудность создает то, что при выборе “некрайних” элементов остающиеся числа уже не идут друг за другом (например, если л=4 и на первое место выбран элемент 2, то на последующие остаются 1,3 и 4). Эта проблема решается применением вспомогательного массива, который инициализируют последовательностью 1, 2,.... и, а каждый раз при выборе некоторого элемента он удаляется из массива (со сдвигом “хвоста” влево).
Этот алгоритм эффективен, когда нужно найти одну перестановку по заданному номеру к. Но если нужно генерировать перестановки подряд друг за другом, лучше использовать какой-либо из методов главы 11.
Кстати, если начинать нумерацию не с 1, а с 0, вместо “потолка” частного можно использовать более простое целочисленное деление.
10.16.	Ответ. с(п,к) = с(п-1,к-1)+ (n-l)xc(n-l,fc). См. задачу о разбиении на подмножества.
10.17.	Используйте способ вычисления меры объединения, приведенный в подразделе 10.4.1.
Глава 11
11.1.	Поскольку тестов может быть очень много, целесообразно вначале породить и запомнить все возможные размещения ферзей (их 92). Это избавит от “основного” перебора (построения всех размещений ферзей) при обработке каждого теста. Останется только мини-поиск размещения, наилучшего для данных отметок, среди 92 запомненных.
11.2.	Это частный случай задачи 11.6 (сумма масс в одной из куч должна равнять-
ся или быть как можно ближе к половине общей массы камней).
Впрочем, если массы камней очень велики, а их количество довольно мало, лучше использовать идеи решения задачи 11.5, заменив условия отсе
456
ПРИЛОЖЕНИЕ А
чения бесперспективных вариантов на более мягкие: помнить наилучшие на данный момент м_ир и M_down (суммы, ближайшие к М сверху и снизу) и отсекать только варианты, суммы которых или меньше M_down, или больше М_ир.
11.3.	Организуйте перебор подмножеств как в решении задачи 11.2. К рекурсивному алгоритму несложно добавить отсечение неподходящих вариантов (см. задачу 11.5).
11.4.	Количество четырехзначных чисел с неповторяющимися цифрами равно 9-9-8-7=4536, т.е. более половины из 9000 четырехзначных чисел. Поэтому переберем все числа от 1234 до 9876 и для каждого числа с неповторяющимися цифрами (первая из них не наименьшая!) образуем 24 перестановки цифр (см. подраздел 11.2.4). Если перестановка представляет число, меньшее исходного, вычтем его из исходного числа и проверим, является ли разность перестановкой цифр исходного числа. В противном случае перейдем к следующей перестановке.
11.5.	См. подразделы 11.2.1 и 11.2.4.
11.6.	Простейший для написания “с чистого листа” способ — рекурсивная процедура, которой передаются как параметры количество элементов, которые еще нужно выбрать, и множество, из которого их еще можно выбирать. Начальный вызов этой процедуры из главной программы будет иметь вид gen_k_perm к, [1.,п] , условие вывода текущего размещения, построенного в глобальном массиве, и выхода из рекурсии — к = 0. Но это не наилучший способ, особенно если к ненамного меньше п, поскольку внутри каждого рекурсивного вызова придется просматривать все п элементов и для каждого определять, принадлежит ли он множеству еще не выбранный.
Если есть готовые реализации описанных в главе алгоритмов, несложно скомпоновать более эффективную программу, перебирая размещения как перестановки элементов всех возможных fc-элементных подмножеств (используйте нерекурсивные версии обоих переборов).
11.7.	Используйте матрицу, индексированную полями, значение каждого элемента которой — количество ферзей, атакующих данное поле. На каждом уровне дерева перебора (он же уровень рекурсии при рекурсивной реализации) выбирается место для очередного ферзя, причем не только в границах очередной вертикали, а по всей доске. Поскольку все ферзи одинаковы, нет смысла строить размещения, отличающиеся только тем, что ферзи обменялись местами. Поэтому при поиске мест для каждого непервого ферзя достаточно рассматривать только клетки, находящиеся после последнего из поставленных ферзей. Не забудьте также отсечение по числу выставленных ферзей: если уже найдено решение, в котором к ферзей атакуют все свободные поля, а в текущей ветке перебора к ферзей еще не атакуют все поля, перебирать дальше бессмысленно. Имеет смысл также инициализировать это к значением п.
11.8.	См. указание к предыдущей задаче.
11.9.	Строку длиной N можно построить в массиве А из 2000 символов с помощью рекурсивной процедуры с целым параметром Num. Если Num больше N,
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
457
в массиве А уже построена строка длиной N; скопируем ее во вспомогательный массив в и сформируем признак того, что строка длины N построена.
Если Num не больше N, присвоим значение ' А1 переменной A [Num] и проверим, правильна ли строка А [1.. Num], т.е. нет ли в ней одинаковых соседних подстрок (вторая из них заканчивается символом A [Num]). Если строка правильна, выполним рекурсивный вызов с аргументом Num+1.
Если строка с последним символом A [Num] неправильная или рекурсивный вызов закончился без построения строки длиной N, переменной A [Num] присвоим ' В' и аналогично проверим правильность и выполним рекурсивный вызов. Если и этот вызов не привел к успеху, переменной A [NUm] присвоим ' С1, выполним проверку и рекурсивный вызов.
11.10.	В рекурсивной версии параметрами будут число, разбиваемое на слагаемые, и максимальное допустимое значение слагаемого.
Основной вопрос нерекурсивной реализации — как от очередного разбиения перейти к следующему — решается просто, если разбиения порождать в порядке, противоположном к лексикографическому. Например, начало последовательности разбиений п=7 имеет вид 7, 6+1, 5+2, 5+1+1, 4+3, 4+2+1, 4+1+1+1, 3+3+1, 3+2+2, конец - 2+2+1+1+1, 2+1+1+1+1+1, 1+1+1+1+1+1+1. Нетрудно догадаться, что при переходе к следующему разбиению последнее слагаемое I, большее 1, уменьшается на 1, а следующие за ним должны быть максимальными, но не больше Z-1.
Итак, для перехода к следующему разбиению удалим все 1 в конце разбиения вместе с последним неединичным слагаемым I и найдем их сумму s. Значение sdiv(Z-l) есть количество слагаемых 1-1, которыми заменяются удаленные слагаемые; последнее слагаемое, возможно, равно т- smod(Z-l) (при т*0). К следующему разбиению переходим, пока в очередном не все слагаемые равны 1.
Для реализации представим разбиение с помощью двух массивов (слагаемых А и их количеств В) и счетчика с количества элементов, используемых в этих массивах. Например, разбиение 7=7 представлено значениями с=1, А[1]=7, B[l]=l, 2+2+1+1+1 — с=2, А[1]=2, А[2] = 1, В[1]=2, В[2]=3. Массив А все время должен быть строго убывающим (A[z]>A[i+lJ при 1< f<c-l). Полезно также поддерживать индекс последнего неединичного слагаемого.
Размер массивов А и В, т.е. максимальное количество т разных слагаемых, определяется из условия 1+ 2+ ...+ оценим т сверху величиной 1 +ы.
11.11.	Без доказательства утверждаем, что данная задача является	полной.
Вариант 1. Задача идентична упражнению 11.2.
Вариант 2. Используйте метод ветвей и границ. Одна из оптимизаций — ’ начинать перебор не с тривиального “включить все рассказы в один том”, а с решения, найденного алгоритмом в варианте 3 (см. ниже). Такое улучшение во многих ситуациях существенно усилит отсечение и сузит область перебора.
Нетрудно также заметить, что если все рассказы, которые были в первом томе, включить во второй, а все, которые были во втором — в первый, то по
458
ПРИЛОЖЕНИЕ А
сути ничего не меняется, и лучше не расходовать время на анализ обоих этих разбиений. Избежав подобного дублирования, можно ускорить работу приблизительно в В! раз. Опишем, как реализовать такую оптимизацию. Пусть первый рассказ обязательно входит в первый том, выбран текущий вариант вхождений рассказов в тома и начинается выбор рассказов, входящих в i-й том. Тогда рассказ с минимальным номером, еще не включенный ни в один том, включается именно в i-й том. Например, из двух разбиений (1,3), (2,4) и (2,4), (1,3), по сути совпадающих, второе вообще не будет рассматриваться, поскольку в нем первый рассказ не входит в первый том.
На практике полезно написать перебор так, чтобы каждый раз, получив результат, который лучше, чем найденные ранее, стирать из выходного файла старый результат и записывать новый. Когда пользователю надоест ждать и он прервет выполнение программы, в выходном файле будет некоторое приближение к оптимальному решению; причем, чем дольше работала программа, тем лучше может оказаться результат. Однако на олимпиадах такой прием категорически не рекомендуется, поскольку практически всегда, если программа не завершила работу вовремя, выходной файл вообще не анализируют!
Вариант 3. Вначале упорядочим рассказы по убыванию длины и затем будем включать рассказы в тома, придерживаясь правила “очередной рассказ — в самый тонкий том”. Этот алгоритм работает очень быстро и дает решения, часто “достаточно близкие” к оптимальным.
11.12.	Реализовать метод ветвей и границ, упорядочив предметы по убыванию отношения стоимости к весу r=c/w. Целесообразно вначале пытаться уложить как можно больше предметов с большими значениями г.
11.13.	В примере указано распределение деталей с общей длительностью (стоимостью) 19 = шах{7+12, 8+11,9+10}.
Представим распределение деталей 1, 2, ..., к последовательностью (р,, р2, номеров станков, на которых они обрабатываются. При к=п распределение назовем полным, иначе неполным. Корень дерева распределений— пустое распределение (), его сыновья — (1), (2), (3) ит.д. Вообще, сыновьями распределения у = (рр.... р^) при i-1 < п являются распределения v,= (р,, ..., рм,1), v2= (рр ..., рм,2), v3 = (рр .... ри,3). Полные распределения — это листья вида (рр ...,р„).
Распределение v= (plt .^p^, где 1-1<п, определяет длительности работы станков (Sp S2, S}) и стоимость распределения max{Sp S2,S3}. Сыновья vp v2, v3 распределения v при i-1 < п имеют длительности
(51+Т|, Si, S3), (Si, S2+T1, S3), (Si, S2, Зз+Тд.
При i=n выбираем наименьшую из стоимостей vp v2,»v3. При i<n нужны не стоимости этих неполных распределений, а нижние оценки стоимости тех полных распределений, которые из них можно получить.
Рассмотрим простейший способ оценивания. Очевидно, что потомки неполного распределения у первых i-1 заданий с тройкой (Sp S2, S3) имеют стоимость не меньше
E(v) = max{5i, S2, S3, nrinfSj, S2, S3} + 7/},
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
459
т.е. Е(у) — нижняя граница стоимости потомков распределения v. При i=n эта величина является стоимостью полного распределения — “лучшего из сыновей” узла v.
Используем оценки стоимости при обходе дерева распределений в глубину так, чтобы узлы-братья рассматривались в порядке возрастания вычисленных оценок, а потомки узла с оценкой, большей стоимости уже полученного полного распределения, вообще не посещались.
Текущее распределение можно хранить в массиве D с индексами от 1 до п (номерами деталей) и значениями 1, 2, 3 — номерами станков; £>[i] =к, если деталь i распределена на станок к. По мере обхода дерева массив D используется как магазин.
На глубине п вычислим стоимость узла с. Если с меньше найденной ранее стоимости какого-либо полного распределения С^, изменим на с и скопируем распределение в дополнительный массив F.
На глубине меньше и, если узел v имеет оценку Е(у) меньше Cmin, рекурсивно переходим к его потомкам, иначе возвращаемся на предыдущий уровень.
Обход дерева заканчивается при возвращении в корень дерева. Распределение, сохраненное в F, выводится.
Полезно учесть, что вначале из трех распределений (1), (2), (3) достаточно исследовать одно. Вообще, если обрабатывается узел с одинаковыми длительностями Sp S2, S3, то из трех его сыновей нужен только один. Если же две из трех длительностей в узле равны, то из двух сыновей, отличающихся порядком длительностей, нужен только один.
Отметим, что данная задача— частный случай варианта! упражнения 11.11 и обобщение упражнения 11.2.
11.14.	Реализуйте перебор n-элементных числовых множеств. Учтите, что первое число в множестве равно 1, а каждое следующее не больше тк+1, где к — предыдущее.
11.15.	Простой (но далеко не самый эффективный) способ — перебрать остовные деревья аналогично тому, как перебираются гамильтоновы циклы в подразделе 11.3.4. Идея более эффективного алгоритма представлена в [35].
Глава 12
12.1.	“Самая жадная” идея “взять три длиннейших отрезка” не работает: возможно, они не образуют треугольник, а более короткие образуют, например, если длины отрезков равны 220 100 90 70.
Разумным кажется проверить три длиннейших отрезка; если они не образуют треугольник, то наибольший отрезок “выбросить”, а вместо него взять четвертый по длине; если треугольник опять не получается — “выбросить” второй и взять пятый и т.д., пока не получим треугольник (или не закончатся отрезки). Но и этот “алгоритм” неправилен! Например, если взять отрезки 147 98 50 49 48, то треугольники (147, 98,50) и (98,50,49) оказываются длинными, но очень узкими, а треугольник (50,49,48) — почти равносторонним, и за счет этого наибольшим по площади!
460
ПРИЛОЖЕНИЕ А
Тем не менее задачу можно решить “жадно”. Для этого достаточно не останавливаться на первом же треугольнике, полученном согласно рассуждениям предыдущего пункта, а просмотреть их все и выбрать максимум.
Итак, отсортируем отрезки по невозрастанию длины, рассмотрим все возможные тройки отрезков а., и а^, и среди соответствующих площадей треугольников найдем наибольшую.
Докажите самостоятельно, что такой алгоритм правильно находит наибольшую площадь для любой возможной совокупности длин отрезков.
12.2.	Нет смысла брать больше, чем р-1 монету любого номинала, кроме наибольшего, иначе р монет с номиналом р можно заменить одной монетой с номиналом р'+1. Но тогда все суммы, которые можно набрать монетами с номиналами от 1 до р, строго меньше р'+1.
12.3.	Упорядочить строки по возрастанию цены добавки. Искомым будет подмножество У линейно независимых строк, построенное жадным алгоритмом.
12.4.	Докажите, что оптимальное решение — печатать (и сразу же доставлять) заказы в порядке невозрастания времени доставки.
12.5.	Рассмотрите пару (Е,Г), где Е— множество заданий, I — семейство множеств заданий, для которых можно построить расписание без превышения сроков исполнения (семейство независимых множеств).
Пусть А — независимое множество и Nt(A) обозначает количество заданий, срок выполнения которых не больше t. Докажите следующий критерий независимости.
Множество независимо А тогда и только тогда, когда Nt(A)<t для всех t=0, 1, 2, ..., и.
С помощью условия, указанного в критерии, докажите, что (Е, Г) — матроиД.
Очевидно, что сумма штрафов минимальна тогда и только тогда, когда максимальна сумма штрафов, которой удалось избежать. На этом основании реализуйте жадный алгоритм, расположив задания в порядке убывания штрафа, т.е. используя штраф в качестве веса.
В этом алгоритме, когда уже выбрано некоторое независимое подмножество А и обрабатывается очередной элемент е, проверка условия Aue е I и, если это так, добавление элемента е требует дополнительных усилий. Докажите следующее утверждение.
Если задания независимого множества расположены в порядке возрастания сроков исполнения, то ни одно из них не является просроченным.
На основании этого утверждения задание добавляется к расписанию так, что порядок возрастания сроков исполнения сохраняется. Затем проверяется критерий независимости (см. выше) и, если он нарущен, задание перемещается в конец расписания.
12.6.	Заправляться нужно на наиболее отдаленной заправке, до которой можно доехать, имея полный бак.
12.7.	Поскольку в задаче идет речь о быстрых и медленных студентах, упорядочим студентов по неубыванию длительности их прохода на выставку. Первое соображение: если два самых медленных (последних) студента проходят вместе,
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
461
то экономится время на проход предпоследнего. Второе: приглашение выносит самый быстрый из тех, кто находится на выставке. Отсюда следует способ провести двух самых медленных студентов. Сначала проходят первые два (самые быстрые) и первый выносит приглашения. Затем проходят два последних, второй, оставшийся на выставке, выносит приглашения. Таким образом, вся группа, кроме двух последних, находится на улице, т.е. задача свелась к меньшей задаче, и ее можно решить тем же способом.
Однако, если время прохода второго студента велико, то лучшим может оказаться другой способ, а именно: первый проводит по одному всех остальных студентов, каждый раз вынося приглашения. Заметим, что при этом порядок прохода студентов не играет роли.
Рассмотрим условия, при которых^ лучше первый или второй способ. Пусть Г], /2, ..., tN], tN— длительности прохода первого, второго, ..., последнего студентов. Для прохода двух последних студентов и сведения задачи к меньшей (первый способ) нужно время t2+ г,+ tN+t2, а для проведения двух последних студентов с возвращением первого (второй способ) — tN+ r,+ Отсюда, если 2t2< tx + tN,, то лучше первый способ, иначе — второй. Заметим также: если остались студенты 1, 2, ..., К и 2г2> то 2t2> t^t^, ведь tK3<tr г Значит, после перехода ко второму способу не будет возвращения к первому.
Для решения задачи понадобится массив, позволяющий по номеру, полученному студентом после сортировки длительностей по возрастанию, определять номер студента во входной последовательности (см. главу 5).
12.8.	Первый способ (первый подходящий ящик). Первый груз кладем в первый ящик. Второй также, если он там уместится. Если не уместится, то кладем его во второй ящик. Вообще, очередной груз кладем в ящик с наименьшим номером, в котором он уместится. Второй способ (наиболее подходящий ящик). Очередной груз кладем в тот ящик, в котором остается наименьший допустимый свободный вес. Если таких ящиков несколько, то из них выбираем ящик с наименьшим номером.
Глава 13
13.1.	Один линейный массив размером N плюс несколько вспомогательных скалярных переменных в дополнение к указанным в листинге 13.1. Текущее значение клетки можно обрабатывать сразу после чтения, т.е. достаточно одной целой переменной; для таблицы оценок достаточно хранить конец предыдущей и начало текущей строки. Точнее говоря, при обработке (m,j)-ro элемента (м>2) значения еи1у и ет, берутся из j-го и (/+1)-го элементов линейного массива, а етЛ И — из заранее сохраненной вспомогательной целой переменной. Результат (emJ) записывается bj-й элемент массива, но перед этим ея1 у сохраняется во вспомогательную переменную.
13.2.	См. задачу 13.1.
13.3.	Триангуляция многоугольника имеет глубокую внутреннюю связь с оптимизацией расстановки скобок и решается аналогично.
462
ПРИЛОЖЕНИЕ А
13.4.	а) Пусть A = a]aY..an и B=b]br..bm — строки. Рассмотрим прямоугольную таблицу Х\ значением	является длина общей подстроки, которая заканчива-
ется символом at в строке А и символом Ь в В. Ху=1, если а^Ь^ иначе 0; Хй = 1, если а=Ь19 иначе 0. В следующих строках таблицы X,=XMJ4 + 1, если а=Ь}, иначе 0. Вычислим таблицу по строкам, запоминая индекс в строке А, при котором достигается текущее максимальное значение Ху, и само это значение. По этим данным найдем начало подстроки. Вместо таблицы достаточно одной ее строки.
б)	Пусть A=aiar..an и В — ЬхЬг„Ьт — строки. Рассмотрим прямоугольную таблицу X: значением Хц является максимальная длина общей подпоследовательности в начале строки А (от at до а.) и в начале В (от Ь{ до b^. Ху= 1, если ах — Ьк при некотором к, k<j, иначе 0; Хя = 1, если ак=Ь1 при некотором к, к<Л, иначе 0. В следующих строках таблицы £ =XW ,+1, если а=Ь}, иначе max{X._M, Xt_ц}. Вычислим таблицу по строкам, запоминая в отдельном списке каждое добытые (когда а=Ь}) в виде тройки (i,j, /), где /=ХГ Вместо таблицы X достаточно одной ее строки длиной т.
Например, для строк A=bacbac и B=abcab таблица X имеет следующий
По таблице видно, что общими подпоследовательностями максимальной длины 3 строк bacbac и abcab являются bab, bcb, bca, abc, aba, аса и acb. Список событий образован следующими тройками:
(1, 2,1), (2,1,1), (2,4, 2), (3, 3,2), (4, 2, 2), (4,5, 3), (5,4, 3), (6, 3, 3)
На тройках можно ввести отношение непосредственного предшествования <:	Q < Op Л’ У’ если *!<	Этому отношению соответ-
ствуют дуги в орграфе, вершинами которого будут события. Он является направленным ацикличным, а все максимальные общие подпоследовательности получаются обходом его путей максимальной длины. Подпоследовательность, наиболее “прижатая влево” в первой строке, получается, если изо всех событий (г, j, /) с данным значением I запоминать событие с наименьшим L
в)	“Расстояние” — это сумма длин строк минус удвоенная длина максимальной общей подпоследовательности.
13.5.	Вариант 1. Найти суммарное число страниц Sum, определить, для какой главы сумма длин глав с первой по текущую становится больше Sum/2,
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
463
и решить, в какой том поместить эту главу.
Вариант 2. Рассмотрим серию задач: Какова минимальная возможная толщина самого толстого тома, если все главы с первой по j-ю издаются в i томах? Назовем такую задачу (/,^-подзадачей; (В, С)-п°дзадача — это исходная задача.
Решение любой (1J)-подзадачи тривиально — главы 1..J помещаются в первый том, и он “самый толстый”. При произвольных i и j, где 1 < i<B, j< C-B+i, в оптимальном решении (ij)-подзадачи (i-l)-fl том должен закончиться некоторой fc-й главой (чему равно к, пока не известно), а все главы с (£+1)-й по;-ю должны быть включены в /-й том.
Самым толстым из i томов может быть или какой-то из томов 1..(/-1), или i-й. Максимальная толщина томов l..(z-l) — это решение (i-l,k)-подзадачи, а толщина i-ro тома — сумма длин глав (Ач-1).•/. Максимум этих двух чисел и есть максимальная толщина всех i томов.
Когда используется решение (z-1, ^-подзадачи, заранее не известно, чему равно Л, поэтому нужно перебрать возможные значения к. Итак, при 1>2
Ту= nun {тах{7мЛ, szm.j]},
где Т& — максимальная толщина тома в оптимальном решении (/,;)-подзадачи, — сумма длин глав (k+l)..j. Диапазон перебора к должен быть (i-l)..(/-!), но его можно немного сократить, проводя перебор от и на каждом шаге уменьшая на 1, пока ^+1 у не станет больше уже найденной оценки Ту.
Для реализации вычислений можно использовать рекурсию с запоминанием или заполнять таблицу оценок по строкам (/=2, 3,..., В).
Чтобы построить разбиение глав на тома, можно завести таблицу выборов, в клетках которой запоминать, какое именно значение к привело к оптимальному решению (z,»-подзадачи. Тогда в клетке (В, С) берем к — номер последней главы в (В-1)-м томе, затем в клетке (В-1, С-к) — номер последней главы в (В-2)-м и так далее до первого тома. Этот обратный ход строит результаты от последнего тома к первому. На выходе они нужны в обратном порядке, поэтому придется запомнить их в дополнительном массиве.
13.6.	Решение похоже на задачу 13.8, но, поскольку остаток последней строки не учитывается, целевая задача не будет одной из задач серии.
13.7.	Рассмотрите координаты концов “дыры” и всех отрезков Ср С2,..., Ся, упорядоченные по возрастанию. Исходная задача является одной из (г,у)-подзадач: Какова наименьшая суммарная длина отрезков, покрывающих отрезок [С, С.]?
13.8.	Рассмотрим серию (г,у)-подзадач Какое наименьшее значение может иметь многочлен со старшей степенью х, заданный подстрокой, которая начинается первым символом строки и заканчивается j-м? Целевая задача не является одной из задач серии, но ее решение можно получить из решений (/,/)-подзадач, где I — длина строки, для всех i от 0 до I-1.
13.9.	Жадный алгоритм минимизирует число блоков, остающихся после формирования очередной строки.
464
ПРИЛОЖЕНИЕ А
13.10.	Простой, но неэффективный способ. Отсортируйте интервалы по возрастанию начала и рассмотрите (г,/)-подзадачи: Какую максимальную важность можно получить на заседаниях с i-го по j-e? Исходной задачей является (1, АО-подзадача.
Сложный, но существенно более эффективный способ. Решение упражнения получается из решений задач 12.1, 13.3 и 13.4. Отсортируем заседания по b и поставим серию/-подзадач Какую максимальную важность можно получить на заседаниях с 1-го по j-e?3. Здесь для решения больших подзадач нужны только решения меньших (по номерам отсортированных заседаний).
Если следовать задаче о коробках и рассматривать в /-подзадаче только расписания, в которых само /-е заседание обязательно выбрано, получим алгоритм со сложностью О(№).
Лучше понимать /-подзадачу так: Какую максимальную важность можно набрать, начиная с начала и освободившись не позже конца J-го заседания? При такой постановке задачи последовательность решений подзадач Тр Tv ..., Тг j будет монотонно неубывающей, и для нахождения ответа достаточно выбрать максимум из Тм и Tk+vJ9 где у,— важность /-го заседания, к — номер последнего (согласно сортировке по Ь) из заседаний, заканчивающихся строго раньше, чем начинается /*-е (если таких заседаний нет, считаем Тл=0). Обратите внимание: в отличие от большинства задач динамического программирования, к не перебирается, а ищется бинарным поиском. Что и дает редкостно быструю оценку сложности O(ATlogTV)-
13.11.	Чтобы выяснить, можно ли добраться до бугорка, достаточно проверить несколько вариантов: можно ли прибыть на него с длиной прыжка 1, с длиной прыжка 2 и т.д. Обозначим утверждение “можно прибыть на бугорок j с длиной прыжка v” как P(s, v). Пары (s, v) будем называть состояниями. Чтобы попасть в состояние (s,v), необходимо, чтобы предыдущим был бугорок 5-v. А для этого нужно, чтобы бугорок s-v не провалился и на него можно было прибыть со скоростью v-1, v или v+1. Отсюда получаем рекурсивное выражение для P(s9 v):
P(s, v) = (бугорок j остался) and (P(s-v, v-1) or P(s-v9 v) or P(s-v, v+1)). Чтобы построить по этой формуле рекурсивную функцию, достаточно добавить условия выхода из рекурсии — например, Р(2,1)= true и P(j,v) = false при и любом v. Для хранения значений Р можно использовать двумерный массив с индексами л и v.
С помощью рекурсивного выражения для P(j, v) легко выяснить, можно ли достичь последнего бугорка. Однако найти минимальное количество прыжков также несложно. Построим функци ю #($, v) — минимум возможного
3	По условию нужно учитывать не только важность, но и время, если важность заседаний одинакова. И важности, и длительности складываются, поэтому под конец написания программы, после отладки собственно реализации алгоритма, нужно будет переделать тип для хранения ответов подзадач с числового на структуру с двумя полями и перегрузить операции сравнения и сложения для этого типа. А пока “забудем” об этом.
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
465
количества прыжков, приводящих в состояние (s,v). Для вычисления N(s,v) достаточно рассмотреть три возможных предыдущих состояния (5-v,v-l), (s-v, v) и (s-v, v+1), выбрать из них то, в котором значение W минимально, и учесть прыжок из него на (s, v). Обозначим N(s, v)=оо, если P(s, v) = f alse, т.е. состояние недостижимо.
Вычисление N(s, v) можно объединить с проверкой достижимости P(s, v): N(s, v) = min{ N(s-v, v-1), N(s-v, v), N(s-v, v+1) }+l, если бугорок s остался, N(s, v) = оо, если не остался.
Начальные условия — N(2,1) = 1 и N(s, v) = оо при s<0 и любом v.
Подзадачи в данной задаче перекрываются, поэтому здесь понадобится рекурсия с запоминаниями.
Глава 14
14.1.	Проигрышными являются позиции с остатком Я=1, 1+(Л/+1)5 1+2(М+1),.... Отсюда, если r=(C-l)mod(Af+l)#0, первый ход программы — г, иначе пусть первым ходит противник, а задача программы — обеспечить перед ходом противника остаток вида 1+Л-(Af+l).
14.2.	Проигрышными являются те позиции, у которых обе разницы координат с верхней правой клеткой четные. Остальные выигрышные.
14.3.	Разобьем номера строк на пары (1,2), (3,4), ..., (9,10). Вначале зададим значение В, равное значению А. Затем каждый ход совершается в строке, парной к строке, которую только что заполнил соперник, и повторяет его ход.
14.4.	а) Позиция в игре — число и месяц, названные перед очередным ходом. Представим позиции таблицей из 31 строки и 12 столбцов. 31.12 — проигрышная позиция, любой другой день в декабре — выигрышная, поскольку, если назван этот день, следующим можно назвать 31.12, увеличив число в декабре. 31 число любого месяца — также выигрышная позиция, поскольку можно увеличить месяц до 12. 30.11 — проигрышная позиция, поскольку из нее есть только один ход — в выигрышную позицию 30.12. Аналогично все 30-е числа и все остальные дни в ноябре — выигрышные. Продолжив, получим, что проигрышными являются 31.12, 30.11, 29.10, 28.09, ..., 20.01, а остальные дни выигрышные.
6)	Рассмотрите таблицу, аналогичную п. а, но в которой 31.12 — выигрышная позиция. Убедитесь, что проигрышными являются 30.12, 31.10, 30.11, азатем 28.09, 27.08,..., 20.01, и только они.
14.5.	Позиция — это произведение, полученное перед очередным ходом. Ясно, что позиция С — проигрышная, позиции от ГС79~| до С-1 — выигрышные, от ГГС/9~|/2~| до ГС791-1 — проигрышные. Отсюда позиции от ГП~С/91/23/91 до ГГ679^21-1 — выигрышные ит.д. Итак, последовательность нижних границ выигрышных и проигрышных интервалов можно найти и запомнить, попеременно деля границы на 9 и на 2 и вычисляя целую часть с избытком.
Последняя нижняя граница равна 1. Если она достигнута при делении на 9, то первый ход выполняет программа, иначе — соперник.
466
ПРИЛОЖЕНИЕ А
В языке Turbo Pascal для представления целых значений из диапазона, указанного в условии, удобно использовать тип сотпр, а для вычисления целой части — функцию int. Тогда, если int (х) =х, то Гх~1 выражается как int (х), иначе — как int (х) +1.
14.6.	Рассмотрите в качестве позиций пары вида (Р, Л), где Р — произведение, накопленное перед ходом, к — последний сомножитель. Позиции (Р,к)9 где C<P<R, являются проигрышными. Выигрышные позиции — это позиции (Р,к)9 ддя которых существует х от 2 до 9, взаимно простое с к, причем (Рх,х) — проигрышная позиция. Позиция вида (Р9к) проигрышная, если для каждого х от 2 до 9, взаимно простого с к9 позиция (Рх, х) — выигрышная.
14.7.	Число т соответствует числу кучек в игре Ним, число клеток л-2 между баранами — числу камешков в кучке (см. подраздел 14.1.3). Отличие от игры Ним состоит в том, что камешки можно возвращать в кучку — этому соответствует отступление барана.
14.8.	Ясно, что первый ход либо отрезает от свободного поля строку (если строка одна, поле “уничтожается”), либо разбивает его на две прямоугольные области. Каждый из следующих ходов также либо уменьшает свободную область на одну строку или столбец (возможно, уничтожая ее), либо разбивает ее по горизонтали или вертикали на две. Таким образом, после каждого хода имеется одна или несколько прямоугольных свободных областей.
Пусть т — количество строк, п — столбцов. Главным в анализе игры является четность и нечетность тип. Назовем свободную область ЧЧ- или ЧН-областъю9 если число строк в ней четно, а число столбцов соответственно четно или нечетно. Аналогично определим НН- и НЧ-область.
Утверждение. Если исходное поле является НН-, НЧ- или ЧЧ-областыо, тб существует выигрышная стратегия для Горза, если ЧН-областью—для Верта.
► 1. Рассмотрим первые три ситуации. Цель Горза — ходить так, чтобы после любого хода Верта оставалось нечетное число НН-областей. Тогда у Горза всегда будет возможность хода.
Рассмотрим первый ход Горза, когда поле является НН-областью. При т=1 Горз выигрывает первым же ходом, иначе занимает вторую строку, оставив Верту Две НН-области. Если п = 1, то ход Верта уничтожает одну из областей, но оставляет другую. Иначе Верт может в одной из областей либо образовать НЧ-область, заняв крайний столбец, либо разбить ее на две НН- или на две НЧ-области, заняв в ней четный или нечетный столбец.
Если поле является НЧ-областью, Горз занимает вторую строку, разбивая поле на две НЧ-области (или выигрывает при т — 1). Ход Верта в одной из них может либо отрезать крайний столбец и превратить ее в НН-область, либо разбить ее на две — НЧ-область и НН-область.
В исходной ЧЧ-области Горз отрезает крайнюю строку и образует НЧ-область. Верт может образовать либо НН-область, либо НЧ-область и НН-область.
Как видим, в любой из рассмотренных ситуаций количество НН-областей, оставшихся после хода Верта, нечетно, причем ЧН- и ЧЧ-областей нет.
Далее Горз совершает каждый ход в НН-области, либо уничтожая ее, либо ходом в ее вторую строку разбивая на две НН-области. Тем самым он оставляет
УКАЗАНИЯ ПО РЕШЕНИЮ УПРАЖНЕНИЙ
467
Верту четное число НН-областей и некоторое количество НЧ-областей. Но после любого хода Верта, как уже было показано, образуется нечетное число НН-областей. Поэтому Горз, играя указанным образом, необходимо выиграет.
2.	Рассмотрим стратегию Верта, когда исходное поле — ЧН-область. Первый ход Горза образует либо НН-область, либо НН-область и ЧН-область.
В обеих ситуациях Верт делает ход в НН-области. Если п = 1, НН-область уничтожается, иначе Верт ходит во второй столбец и оставлет Горзу две НН-области, т.е. четное их число. Затем Горз ходом в одну из них либо уничтожает ее, либо образует ЧН-область, либо две НН-области, либо две ЧН-области. Как видим, при любом ходе Горза в НН-области четность числа НН-областей обязательно изменяется, и во всех этих ситуациях у Верта для хода есть нечетное число НН-областей и некоторое число ЧН-областей. Поэтому Верт всегда имеет возможность хода, после которого Горзу достается четное число НН-< областей. Значит, Верт, играя указанным способом, обязательно выиграет. Аналогично предыдущему, НЧ- и ЧЧ-областей в ходе игры не будет. 1
14.9.	При АГ=1 важна только четность или нечетность?/, поэтому ниже предположим, что К>2. Позиция в игре — это набор длин участков, образованных оставшимися спичками. Заметим, что участок длиной меньше К можно считать пустым. Из ограничений в условии следует, что количество участков не больше 13 (при К=2). Заметим также, что порядок следования участков значения не имеет, поэтому многие позиции являются неотличимыми, например, 10011 и 11001 (обе они неотличимы еще и от позиций 11000 и 00011).
На основе приведенных фактов можно убедиться, что количество отличимых позиций не больше 3500. Приведенное ограничение на число позиций позволяет использовать их рекурсивный перебор с запоминанием (с учетом неотличимости). На “дне” рекурсии будут находиться проигрышные позиции, в которых нет участков длиной не меньше К. Подробнее анализ аналогичной задачи см. в [26], задача ЮС.
14.10.	Ограничимся позициями, которые получаются, если игроки не делают ошибочных ходов, в результате которых либо второй игрок проигрывает быстрее, либо получается ничья, либо проигрывает первый игрок (рис. А.2). Подробнее см. [25].
468
ПРИЛОЖЕНИЕ А
Первый ход крестиков
Первый ход ноликов
Второй ход крестиков (наилучший)
Второй ход ноликов (вынужденный)
Третий ход крестиков (вынужденный)
Третий ход ноликов (вынужденный)
Второй этап игры
Четвертый ход крестиков (наилучший)
XI о ю
X
о
х х
Четвертый ход ноликов (один из возможных, но уже бесполезных)
Выигрыш крестиков
Рис. А. 2. Позиции в игре “Тик-так-ту”
Список литературы
1.	Андреева Е. В., Егоров Ю. Е. Вычислительная геометрия на плоскости. — “Информатика”, № 39-44,2004 г.
2.	АрсакЖ. Программирование игр и головоломок. М.: Наука, 1990. — 224 с.
3.	Ахо А., ХопкрофтДж,, Ульман Дж, Построение и анализ вычислительных алгоритмов. — М.: Мир, 1979. — 536 с.
4.	Ахо А., Хопкрофт Дж.у Ульман Дж, Структуры данных и алгоритмы. — М.: Издательский дом “Вильямс”, 2000. — 384 с.
5.	Ахо А., СетиР., Ульман Дж, Компиляторы: принципы, технологии и инструменты. — М,: Издательский дом “Вильямс”, 2001.
6.	Бондарев В. М,, Рублинецкий В.И., Качко Е. Г. Основы программирования.— Харьков: “Фолио”: Ростов-на-Дону: “Феникс”, 1998. — 368 стр.
7.	Брудно А,Л.у Каплан Л,И. Московские олимпиады по программированию/Под ред. акад. Б.Н.Наумова. — 2-е изд., доп. и перераб. — М.: Наука, 1990. — 208 с.
8.	Виленкин Н.Я. Комбинаторика. — М.: Наука, 1969. — 356 с.
9.	Вирт Н. Алгоритмы + Структуры данных = Программы. — М.: Мир, 1985. — 368 с.
10.	Вирт Н, Алгоритмы и структуры данных. — СПб.: Невский диалект, 2005. — 352 с.
11.	ГрисД. Наука программирования. — М.: Мир, 1984. — 412 с.
12.	Грэхем Р,, КнутД.у Паташник О, Конкретная математика. — М.: Мир, 1998. — 703 с.
13.	Гудман С,,Хидетниеми С, Введение в разработку и анализ алгоритмов. — М.: Мир, 1981. — 368 с.
14.	Гэри М., Джонсон Д. Вычислительные машины и труднорешаемые задачи. — М.: Мир, 1982.-416 с.
15.	Долинский М,С, Алгоритмизация и программирование на Turbo Pascal: от простых до олимпиадных задач. — СПб.: Питер, 2005. — 237 с.
16.	Долинский М.С, Решение сложных и олимпиадных задач по программированию. — СПб.: Питер, 2006.
17.	Керниган Б., Пайк Р. Практика программирования. — М.: Издательский дом “Вильямс”, 2004.
18.	КнутД. Искусство программирования. Том 1: Основные алгоритмы. — М.: Издательский дом “Вильямс”, 2000.
19.	КнутД. Искусство программирования. Том 2: Получисленные алгоритмы. — М.: Издательский дом “Вильямс”, 2000.
20.	КнутД. Искусство программирования. Том 3: Поиск и сортировка. — М.: Издательский дом “Вильямс”, 2000.
21.	Кормен Т.уЛейзерсон Ч,9 Ривест Р, Алгоритмы: построение и анализ. — М.: МЦНМО, 2001.
22.	Кормен Т.у Лейзерсон Ч., Ривест Р., Штайн К Алгоритмы: построение и анализ. — М.: Издательский дом “Вильямс”, 2005. — 1296 с.
23.	Ласло М, Вычислительная геометрия и компьютерная графика на C++, М.: Binom, 1997.-304 с.
24.	Липский В. Комбинаторика для программистов. М.: Мир, 1988. — 213 с.
25.	ЛорьерЖ.—Л. Системы искусственного интеллекта. — М.: Мир, 1991. — 568 с.
470
СПИСОК ЛИТЕРАТУРЫ
26.	Меньшиков Ф. Олимпиадные задачи по программированию. — СПб.: Питер, 2005. — 320 с.
27.	Мозговой М, Занимательное програмирование. Самоучитель. — СПб.: Питер, 2004. — 208 с.
28.	Нильсон Н. Принципы искусственного интеллекта. — М.: Радио и связь, 1985.
29.	Нйсолъський Ю.В., Пас1чник В.В., Щербина Ю.М. Дискретна математика. — К.: BHV, 2007.
30.	Новиков ФА. Дискретная математика для программистов. — СПб.: Питер, 2000. — 304 с.
31.	Окулов С.М. Основы программирования. — М.: БИНОМ. Лаборатория знаний, 2004. — 440 с.
32.	Окулов С.М. Программирование в алгоритмах. — М.: БИНОМ. Лаборатория знаний, 2004. - 341 с.
33.	ПасЬсов Ю.Я., Омонов К.К., Кравець Г.П., Непомнящий Г.1., Порубльов I.M. Всеукрашсью 1нтернет-ол1мшади з шформатики NetOI. — УН1ВЕРСУМ-Вшниця, 2006.
34.	Препарата Ф., Шеймос М, Вычислительная геометрия. Введение. — М.: Мир, 1989.
35.	Рейнгольд Э., Нивергелып Ю.,Део Н. Комбинаторные алгоритмы. Теория и практика. — М.: Мир, 1980. - 476 с.
36.	Романовский В.И. Дискретный анализ. — СПб., Невский диалект, 2004. — 320 с.
37.	Седжвик Р. Фундаментально алгоритмы на C++. — М.;СПб.;К.: DiaSoft, 2002.
38.	Скиена С., Ревилла М. Олимпиадные задачи по программированию. Руководство по подготовке к соревнованиям. — М.: Кудиц-Образ, 2005. — 416 с.
39.	Смит Б. Методы и алгоритмы вычислений на строках. — М.: ООО “И.д. Вильямс”, 2006. - 496 с.
40.	Ставровский А.Б., Карнаух ТА. Первые шаги в программировании. Самоучитель. 2-е издание. — М.: Издательский дом “Вильямс”, 2006. — 400 с.
41.	Хопкрофт Дж., Мотвани Р., Ульман Дж. Введение в теорию автоматов, языков и вычислений. — М.: Издательский дом “Вильямс”, 2002. — 528 с.
42.	ШеньАХ. Программирование. Теоремы и задачи. — М.: МЦНМО, 2001.
43.	acm. timus.ru.
44.	algolist .manual. ru.
45.	дбргод. narod. ru.
46.	neerc. if mo. ru.
47.	neerc. if mo. ru/school.
48.	http:I/www.olymp.vinnica.ua.
49.	http://www.olympiads.ru.
50.	http://www.ttb.by.
51.	http: //www.uoi.kiev.ua.
Предметный указатель
#
/^алгоритм, 117
А
Алгоритм
возведения в степень, 86 индийский, 87
Гаусса, 342
Дейкстры, 262 динамического программирования, 353
Евклида, 29
жадный, 338; 340
Краскала, 260
недетерминированный, 328 обхода
графа в глубину, 222; 223 дерева поиска, 318 связного графа
в глубину, 227
в ширину, 224 однопроходный, 47 поиска зацикливания, 117 логарифмический, 127 полиномиальный, 28 приближенный, 324 Прима, 259 псевдополиномиальный, 28; 324; 373
сортировки быстрый, 140 выбором, 136 деревом, 142 пирамидальный, 142 пузырьком, 136 слиянием,137
Флойда—Уоршалла, 229 числовой волны, 247 экспоненциальный, 28
Арифметика
длинная
дробная, 110
целая, 98
Б
Бином Ньютона, 285
Биномиальные коэффициенты, 285
в
Вектор, 160
Выметание, 196
Вычисление булевых выражений полное и сокращенное, 37
г
Глубина рекурсии, 81
Граф, 211
ориентированный, 213
ациклический, 230
эйлеров, 234
Графика, 88
черепашья, 88
д
Действие элементарное, 25
Дерево, 213
остовное, 213; 227
минимального веса, 258
сортирующее, 142
Динамическое программирование,
353
Директива компилятору, 37
Директивы компилятору, 37; 67
Дробь
длинная, 110
представление
с плавающей точкой, НО с фиксированной точкой, НО
472
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ
3
Задача
NP-полная, 328
коммивояжера, 325
о выпуклой оболочке, 180
о вычеркивании из строки, 381
о кенигсбергских мостах, 234
о количестве
правильных скобочных
выражений, 288
о количестве сюръекций, 305
о коробках, 365
о кратчайших путях в графе, 262
о максимальном
грузе, 264
значении выражения, 378
о минимальном числе монет, 344;
372
о площади полигона, 174
о подмножествах, 309; 313
о подмножестве, 323; 324; 329
о подпоследовательности, 359; 365
о почтовых марках, 331
о разбиении
алфавита, 374
множества, 304
числа, 306
о размещении ферзей, 315
о расстановке скобок, 366
о русском лото, 296
о сокровищнице, 347; 353
о счастливых билетах, 289
о триангуляции выпуклого многоугольника, 384
о центре дерева, 215
о черных и белых кубиках, 292
об абзаце с блоками разной высоты, 376
об инверсиях, 303
об остовном дереве минимального веса, 258
об укладке рюкзака, 331; 343
поиска подстроки, 72
с одним источником, 261
топологической сортировки, 232
труднорешаемая, 328
факторизации, 24
Золотое сечение, 393
и
Игра Ним, 395
к
Код Грея, 312
Конечный автомат, 57; 411
Конструкция forward, 84
л
Лексический анализ, 62
м
Маршрут
в графе, 212
в орграфе, 213
Матрица, 367
оптимизация умножений, 367
смежности, 214
Матроид, 339
взвешенный, 340
Метод
ветвей и границ, 326
выметания, 196
Кнута—Морриса—Пратта, 72
минимакса, 399
Многоугольник, 173
Множество
тип данных, 23
частично упорядоченное, 80
Монотонная ломаная, 179
н
Направление поворота, 161
о
Обратный ход, 249; 349; 370
Обход
графа
в глубину, 221
в ширину, 224
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ
473
дерева
в глубину, 317
Определение
рекурсивное, 79 Отображение, 305 < Оценка
позиции, 399
функции, 26
Очередь, 224
с приоритетами, 272
п
Перебор
с возвращениями, 317
сокращение, 20
Перестановка, 281; 303
с повторениями, 283
Площадь
полигона, 174
Подзадачи
аналогичной структуры, 352
независимые, 352
оптимальные, 352 перекрывающиеся, 352 тривиальные, 352
Подпоследовательность, 359
Подпрограмма
рекурсивная, 80
Подстановка, 308
Позиция в игре
выигрышная, 389
проигрышная, 389
Поиск
в графе, 221
дихотомический, 127
логарифмический, 128
Полигон, 173
Порядок
лексикографический, 148
Правило
произведения, 280
суммы, 280
Принцип
включений и исключений, 299
Дирихле, 42
оптимальности динамического
программирования, 352; 353
сокращения перебора с помощью ограничений, 20
Прямая, 162
Псевдокод, 32
р
Разбиение
множества, 304
числа, 306
Размер экземпляра задачи, 25
Размещение, 281
с повторениями, 282
Разность Минковского, 184
Расстояние, 160
от точки до прямой, 165
Рекурсия, 79
косвенная, 84; 381
прямая, 84
с запоминанием, 353; 370; 425
с
Самоподобная ломаная, 88
Синтаксический анализ, 62
Скалярное произведение векторов, 161
Скобочные выражения
оптимальная расстановка скобок, 367; 378
Слияние упорядоченных
последовательностей, 130
Сложность
алгоритма, 26
задачи, 30
полиномиальная, 28
Снежинка Коха, 89
Событие
в методе выметания, 196
Сортировка
массива
быстрая, 140
выбором, 136
деревом, 142
пирамидальная, 142
пузырьковая, 136
474
ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ
слиянием, 130; 138
подсчетом, 146
поразрядная, 148
топологическая, 232; 366
устойчивая, 140
цифровая, 148
Состав, 283
Сочетание, 281
с повторениями, 283
Список
ребер, 214
Средства отладки, 64
Статус выметания, 197
Структура смежности, 214
Сумма Минковского, 184
Сюръекция, 305
т
Табличная техника, 368
динамического
программирования, 352
Тождество
Вандермонда, 286
Коши, 286
Точка плоскости, 160
Точки событий, 196
Транзитивное замыкание, 241
Треугольник
Паскаля, 285
Серпиньского, 90
Треугольные числа, 286
Триангуляция, 269
выпуклого многоугольника, 384
Делоне, 269
У
Умножение матриц, 366
Уравнение прямой, 162
Условная компиляция, 67
Ф
Формула
включений и исключений, 300
ориентированной площади, 161
Функция
91 Мак-Карти, 95
возвратов, 73
ц
Цепь
эйлерова, 236
Цикл, 212
гамильтонов, 325
подстановки, 308
эйлеров, 234
ч
Числа
Фибоначчи, 119; 393; 450; 451
Число
Белла, 304
Каталана, 289
простое, 23
составное, 23
Стирлинга второго рода, 304
я
Японский кроссворд, 403
Научно-популярное издание
Илья Николаевич Порублёв Андрей Борисович Ставровский
Алгоритмы и программы
Решение олимпиадных задач
Литературный редактор Л.Н. Важенина Верстка О.В. Романенко Обложка С.П. Мягков
Издательский дом “Вильямс” 127055, г. Москва, ул. Лесная, д. 43, стр. 1
Подписано в печать 8.05.2007. Формат 70x100/16.
Гарнитура Newton. Печать офсетная.
Усл. печ. л. 38,7. Уч.-изд. л. 26,9.
Тираж 3000 экз. Заказ № 1049.
Отпечатано по технологии CtP в ОАО “Печатный двор” им. А. М. Горького 197110, Санкт-Петербург, Чкаловский пр., 15.
И.Н. По рубле в А.Б Ставровскии
. АЛГОРИТМЫ. И ПРОГРАММЫ РЕШЕНИЕ ОЛИМПИАДНЫХ ЗАДАЧ
Эта книга — для старшеклассников и студентов, желающих лоД|ОТовиться к олимпиадам или экзаменам по программиров >нию, но она будет полезна и всем тем, кого интерссутт решение нестандартных алгоритмических гадач
Подборка задач в книге охватывает самые разные темы:
•	однопроходные и жадные алгоритмы
*	перебор вариантов, рекурсия и нестандартная работа с числами
•	поиск, слияние и сортировка
•	вычислительная геометрия графы и комбинаторика
•	динамическое программирование и игры
Многие из задач, представленных здесь,1 встречались на соревнованиях по программированию разных лет мест, уровней и форматов проведения. Помимо обсуждения мэтодов решения задач, в книге затронут) • также и технические вопросы: структурное кодирована и использование подпрограмм, элементы стиля, отладки и тестирования, использование режимов компиляции, организация bi юда данных. Особое внимание уделено анализу сложности алгоритмов
В конце каждой главы книги приведены упражнения, а некоторые задачи в основном тексте имеют задания, связанные с модификацией постановок задач или алгоритмов их решение Книга, безусловно, будет полезна всем, кто учится
программиоовать — именно учите г программировать а не изучает языки программирования.
Ik дидлаиаигы www dial) ‘"‘ikaxom