Text
                    И.Н. Порублёв, А.Б. Ставровский
АЛГОРИТМЫ
И ПРОГРАММЫ
РЕШЕНИЕ ОЛИМПИАДНЫХ ЗАДАЧ
пввавиа и  и
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)}. Итак, сложность описанной р