Text
                    Michael J. Laszlo
Computational Geometry ---------and----------- Computer Graphics in C++
Prentice Hall
Майкл Ласло
Вычислительная геометрия ------------ и------------ компьютерная графика на C++
Перевод с английского
В. Львова
Москва БИНОМ 199 7
УДК 004.4 Л 26
ББК 32.973
Перевод с английского Львова В. А.
Майкл Ласло. Вычислительная геометрия и компьютерная графика на C++-Пер. с англ.— М.: Издательство БИНОМ*, 1997. — 304 с.: ил.
В книге описываются основные проблемы, возникающие в компьютерной графике и вычислительной геометрии. Рассмотрены практические задачи и представлены относительно простые способы их решения. Приведены основные приемы разработки и анализа алгоритмов, обсуждаются элементарные структуры данных (списки и деревья) и способы их использования.
Для математиков-прикладников, аспирантов и студентов вузов как учебное пособие по машинному проектированию, машинной графике, распознаванию образов.
Все права защищены. Никакая часть этой книги не может быть воспроизведена в любой форме или любыми средствами, электронными или механическими, включая фотографирование, магнитную запись или иные средства копирования или сохранения информации без письменного разрешения издательства.
ISBN 0-13-290842-5
ISBN 5-7989-0008-8
Authorized translation from the English language edition.
® prentice-Hal|, Inc.,
A Simon & Schuster Company, 1996
J И?да11ие па русском языке.
•Издательство БИНОМ», 1997
Содержание
Предисловие
ЧАСТЬ I. Основы
Глава 1. Введение..........................
1.1.	Обзор задач.........................
1.2.	Применение языка программирования С++
1.3.	Надежность.........................
i
а
Глава 2. Анализ алгоритмов...................
2.1.	Модель вычислений.....................
2.2.	Мера сложности........................
2.2.1.	Время работы в наихудшем случае.
2.2.2.	Время работы в среднем..........
2.2.3.	Средневзвешенное время работы...
2.3.	Асимптотический анализ.............•	• •
2.3.1.	Функции скорости роста.......
2.3.2.	Асимптотическая нотация.........
2.4.	Анализ рекурсивных алгоритмов.........
2.4.1.	Поэтапное суммирование ......•	•
2.4.2.	Способ подстановки.........
2.5.	Сложность задачи...........>..........
2.5.1.	Верхние границы............
2.5.2.	Нижние границы................. •'
2.6.	Замечания по главе.....•.........  •	* •
2.7.	Упражнения............................
Глава 3. Структуры данных.....
3.1.	Что такое структуры данных?
3-2. Связанные списки.......
3.3.	Списки.................

геометрия И компьютерная графика на С-н-
Вычислительная геометр---------_
3	3.1. Конструкторы и деструкторы
3.3.2.	Изменение списков....................
3.3.3.	Доступ к элементам списка................
3.3.4.	Примеры списков....................
3.4.	Стеки......................................
3.5.	Двоичные деревья поиска..........................
3.5.1.	Двоичные деревья.........................
3.5.2.	Двоичные деревья поиска...................................
3.5.3.	Класс SearchTree (дерево поиска)..........................
3.5.4.	Конструкторы и деструкторы................................
3.5.5.	Поиск.....................................................
3.5.6.	Симметричный обход........................................
3.5.7.	Включение элементов........................................
3.5.8.	Удаление элементов................................
50
50
52
53
54
57
57
59
60
61
61
63
64
65
3.6.	Связанные двоичные деревья поиска..................................68
3.6.1.	Класс BraidedNode..........................................
3.6.2.	Класс BraidedSearchTree.................................... 70
3.6.3.	Конструкторы и деструкторы................................. 70
3.6.4.	Использование ленты........................................ 70
3.6.5.	Поиск...................................................... 72
3.6.6.	Включение элементов........................................ 72
3.6.7.	Удаление элементов......................................... 73
3.7.	Деревья со случайным поиском......................................75
3.7.1.	Класс Random!zedNode..................................... . 77
3.7.2.	Класс RandomizedSearchTree................................. 80
3.7.3.	Конструкторы и деструкторы..................................81
3.7.4.	Использование ленты......................,	• 82
3.7.5.	Поиск...................................................... 83
3.7.6.	Включение элементов........................................ 84
3.7.7.	Удаление элементов.......
3.7.8.	Ожидаемые характеристики..................................  86
3.7.9.	Словарь АТД. .............................................. 87
3.8.	Замечания........................................................ д?
...................................................
•9. Упражнения. . . .
Глава 4. Структуры геометрических данных
А 1	...... ......................................................
4.2.		...............................................
4’22 ^accFoint(To4Ka)^.............................
 Конструкторы	............. О,
4 2.3. Векторная арифиетака; ’ ’;  ..7.	7. . 7. 7..
<•2.6.	.....7 ...................... 94
«••• парные ко»zzrпчы-: _•.......................£
....................... 97
Содержание
4.3.	Полигоны (многоугольники)........
4.3.1.	Что такое полигон?.......
4.3.2.	Выпуклые полигоны..........
4.3.3.	Класс Vertex (Вершина).....
4.3.4.	Класс Polygon (Полигон).......
4.3.5.	Точки, входящие в состав выпуклого полигона
4.3.6.	Определение наименьшей вершины в полигоне
4.4.	Ребра......................................
4.4.1.	Класс Edge (Ребро).................
4.4.2.	Поворот ребер....................
4.4.3.	Определение точки пересечения двух прямых линий
4.4.4.	Расстояние от точки до прямой линии
4.4.5.	Дополнительные утилиты.........
4.5.	Геометрические объекты в пространстве
4.5.1.	Точки......................
4.5.2.	Треугольники...................
4.5.3.	Ребра..........................
4.6.	Определение пересечения прямой линии и треугольника
4.7.	Замечания .
4.8.	Упражнения
ЧАСТЬ II. Применение
129
131
132
133
134
135
137
•л • •
Глава 5. Пошаговый ввод
5.1.	Сортировка при вводе..........
5.1.1.	Анализ................* *
5.2.	Образование звездчатого полигона
5.2.1.	Что такое звездчатый полигон?
5.2.2.	Построение звездчатого полигона
5.3.	Поиск выпуклой оболочки.......
5.3.1.	Что такое выпуклая оболочка? .
5.3.2.	Ввод оболочки..............
5.3.3.	Анализ....................
5.4.	Принадлежность точки: метод луча.
5.5.	Принадлежность точки: метод углов ....
5.6.	Отсечение линий: алгоритм Цируса-Бека.
5.7.	Отсечение полигона: алгоритм Сазерленда-Ходжмана
5.8.	Триангуляция монотонных полигонов.
5.8.1.	Что такое монотонный полигон? ..•
5.8.2.	Алгоритм триангуляции
5.8.3.	Корректность.......
5.8.4.	Анализ.............
. 99
99 100 101 103 108 108 109 ПО ПО 112 118 116 117 117
118 122 123 125 126
14* 151
# «Htr» * 155
. .U.. 156
ibo
• -
ВигшгАмтельная геометрия и компьютерная графика на C++ ------------- -
5.9.	Замечания по главе
5.10.	Упражнения . . . .
162
163
Глава 6. Пошаговая выборка..................................
6.1.	Сортировка выборкой.................................
6.1.1.	Автономные и оперативные программы............
6.2.	Построение выпуклой оболочки способом «заворачивание подарка........►.............................*...........
6,2.1.	Анализ.......................»........ ......
6.3.	Построение выпуклой оболочки: метод обхода Грэхема............. . 156
6.4.	Удаление невидимых поверхностей: алгоритм сортировки по глубине...........................................................174
6.4.1.	Предварительные замечания................................ 174
6.4.2.	Алгоритм сортировки по глубине.......................     175
6.4.3.	Сравнение двух треугольников............................. 180
6.4.4.	Уточнение списка треугольников.........................   182
6.5.	Пересечение выпуклых полигонов..................................184
6.5.1.	Анализ и корректность................................     190
6.5.2.	Затруднения.............................................. 192
6.6.	Определение триангуляции Делоне.................................193
6.6.1.	Поиск сопряженной точки для ребра ....................... 199
6.7.	Замечания по главе..............................................202
6.8.	Упражнения . ...................................................203
Глава 7. Алгоритмы сканирования на плоскости..........................
7.1.	Определение точек пересечения отрезков прямых линий...........
7.1.1.	Представление точек-событий....
7.1.2.	Программа верхнего уровня ......
7.1.3.	Структура сканирующей линии. ................
7.1.4.	Переходы...........
7.1.5,	Анализ..........
7.2.	Поиск выпуклой оболочки (построение оболочки при вводе) . . . .
7.2.1.	Анализ . , ........
7.3.	Контур объединения прямоугольников
7.3,1.	Представление прямоугольников . .
7.3.2.	Программа верхнего уровня	................’.........
7.3.3.	Переходы . 4. . .	*...........*.......*...........
7.3.4.	Анализ.....	.................9.................
7.4.	Декомпозиция полигонов ия '...................................
7 4 1 гт	монотонные части
7.4.1.	Программа верхнего уривя ... ЧаСТИ.......................
•4.2. Структура скыиру„щей	...........................
7.4.3. Переходы
7.4.4. Анализ . .
205
206
206
208
209
210
213
214
215
216
217
219
221
224
225
226
229
232
235
Содержание
7.5.	Замечания по главе
7.6.	Упражнения....
235
236
Глава 8. Алгоритмы типа «разделяй и властвуй»	238
8.1.	Сортировка путем слияния..................
8.2.	Вычисление области пересечения полуплоскостей. . ” . . . .	241
8.2.1.	Анализ..............................................   '
	243
8.3.	Определение ядра полигона...............................   243
8.3.1.	Анализ. ........................................ ’	а._
8.4.	Определение многоугольника Вороного........................245
8.4.1.	Диаграммы Вороного.................................. 240
8.4.2.	Анализ ............................................  д^а
8.5.	Слияние оболочек ..........................................g^g
8.5.1.	Программа верхнего уровня.......................     248
8.5.2.	Слияние двух выпуклых оболочек....................   250
8.5.3.	Анализ..........................................     ggg
8.6.	Ближайшие точки........................................    252
8.6.1.	Программа верхнего уровня.......................     258
8.6.2.	Обработка точек в пределах вертикальной полосы..	255
8.6.3.	Анализ..........................................     256
8.7.	Триангуляция полигона.................................  .	. 257
8.7.1.	Программа верхнего уровня........................    258
8.7.2.	Поиск вторгающейся вершины.......................... 269
8.7.3.	Анализ.........................................      260
8.8.	Замечания по главе..................................261
8.9.	Упражнения......................................26Я,
Глава 9. Способы пространственного разделения
9.1.	Задача поиска по области..............
9.2.	Метод сетки...........................
9.2.1.	Представление...................
9.2.2.	Конструкторы и деструкторы......
9.2.3.	Выполнение запросов по области..
9.2.4.	Анализ.....................»•••
9.3.	Квадрантное дерево....... .............
9.3.1.	Представление...................
9.3.2.	Конструкторы и деструкторы......
9.3.3.	Выполнение запросов по области..
9.3.4.	Обеспечивающие функции • • •....
9.3.5.	Анализ..........................
9.4.	Двухмерные деревья поиска.............
264
. 265
. 267 .
. 267 .
. 257 .
269
. 269	/
. 270
. Й7Т
и-В "а '
...28^ П ... 288
9.4.1. Представление..... • • ............................. i	284
9.4.2. Конструкторы и деструкторы ..«••••
10	Вычислительная геометрия и компьютерная графика на с
9 4 3. Выполнение запроса по области.............................
9 4.4. Вспомогательные функции...............
Q 4 5. Анализ......................
9.5.	Удаление невидимых поверхностей: деление пространства...........
9.5.1.	Представление.........................
9.5.2.	Конструкторы и деструкторы............
9.5.8.	Выполнение удаления невидимой поверхности.................
9.5.4.	Анализ.................................
9.6.	Замечания по главе..............................................
9.7.	Упражнения......................................................
285
286
287
288
290
291
293
293
294
294
Литература
Х^сновная цель книги заключается в описании некоторых основных проблем, возникающих в задачах компьютерной графики и вычислительной геометрии с представлением ряда практических и сравнительно простых способов их решения. Мы не будем анализировать сложные ситуации в этих областях, ограничившись рассмотрением только узловых проблем и примеров их решения, что может оказаться более интересным и приемлемым для читателя.
Другое назначение этой книги — подвести читателя к проектированию и анализу алгоритмов (алгоритм — это средство или способ решения вычислительной задачи). Тем самым определяются границы рассматриваемых вопросов при изучении алгоритмов. В число обсуждаемых тем включены вопросы описания элементарных структур данных (списки и деревья поиска), алгоритмических парадигм (деление и захват) и методов анализа характеристик алгоритмов и структур данных.	.
Рассматриваемые задачи вытекают из областей применения компьютера ной графики и вычислительной геометрии. Возникшая из потребностей рынка и развития компьютерной техники (и не только персональных компьютеров) компьютерная графика стала очень быстро развиваться после ее появления в 1960-х годах. Напротив, вычислительная геометрия имеет очень древнее происхождение, уходя корнями к построениям Эвклида. Но и она подверглась стремительному развитию в течение последних десятков лет под воздействием науки об алгоритмах и растущим пониманием ее ши-рокой применимости.	. .,	ичод
Компьютерная графика исследует методы для моделирования и построения сцен. Моделирование используется для построения описания сцен, природа которых может быть самой различной: обычные геометрические объекты в двух- и трехмерном пространстве, естественные явления облака или деревья), страницы текста, огромные массивы чисид^, додучае-мых от устройств чтения изображений или в процессе исследований, гне другие. Процесс отображения используется для преобразования описания сцен в картинку или в кинофильм.	• ’	,
В этой книге мы рассмотрим некоторые задачи компьютерной графшмф относящиеся к отображению. Для определения частей геометрического обт£ акта, выходящих аа пределы заданной области (иля окна) иепольауаи^опе-Рация отсечения, которая должна выполняться перед формир бражек,,Я. Операция удаления невидимых поверхностей пршаеня«е« ин определения таких объектов (или их частей) в пространстве, котор ры

Р1...мгАмтеднаи геометрий и компьютерная графика на C++
«гтпгмм объектами, расположенными ближе к наблю-ты для наблюдения'	,и ЙСКЛючевию перед отображением объекта,
дателю, и поэтому под' рассматривает алгоритмы для решения геомет-
™не мдачи'которьге
ричесюи зада i. Зде относякя к простым геометрическим объектам: Х^“.«м\ХТолХ^п О^жносткм на плоскости, точкам, линиям и тр^львпкам и пространстве. Будут также исследованы задачи декомпоэипий" многоугольников на треугольники, определения поверхностей (многоугольников, их декомпозиций и выпуклых оболочек), .скрытых, среди конечного набора точек я образующих линии пересечения различных геометрических объектов, поиска проекции на плоскость для геометрических объектов, удовлетворяющих определенным условиям.
Связь между компьютерной графикой и вычислительной геометрией не ограничивается только тем фактом, что они рассматривают геометрические объекты. Хотя вполне очевидно, что некоторые методы явно относятся к той или иной области, ио существуют и обшпе методы. Более того, некоторые методы вычислительной геометрии были мотивированы и плодотворно использованы в компьютерной графике. Ярким примером этого является задача удаления невидимых поверхностей, которая еще совсем недавно представляла очень сложную проблему, в последнее время успешно решается точными методами вычислительной геометрии.
Предпосылки
Излагаемый материал основан на самых последних достижениях вычислительной математики. Некоторые понятия тригонометрии и линейной алгебры были использованы при определении классов для геометрических объектов ° ГЛаВС этом наложены лишь самые необходимые математические понятия, достаточные для восприятия материала читателями со слабыми ri4«vMT”4CtKnMK зканиями* Обладание опытом работы с основными струк-м « в^«,»ма”“п”" К8К “язаввые и двоичные деревья, полезно, в>щьем в сахых oomnx'tepVx ^“еедетаГТо',0’Ь СТ₽У’7’Р данны* и р83‘ остальных rm	сведеипя* которые потребуются при чтении
R -утпй	н* стеки и двоичные деревья.
лого	язв—с++ -в
ВЫХ. Существуют две основные нрнч, Вы дй."“ммувмой «РУКтуры давших программ в книгу об алготЛах Т включения текстов работаю-дополняет сухое изложен™.	‘ ЙХ‘ ВопеРвыХ, каждая реализация
и формальные основы алгоритма" поел^”™®’ выепе’ГИЙая ключевые идеи э сухом изложения. Во-вторы» читат-,Тазляя детали’ которые отсутствуют дафицировать ее и эксперимемпгпоРяг^Л0*^ НСПОЛ,П1ТЬ программу, мо-описываемый алгоритм ведет гебя ° Н6Ю’ чтобы лУчше понять, как стартовую площадку для развития твооч^^Г^’ Программы обеспечивают
Читатели, уже знакомые с языком r-IL T8d И иэобРетательности. программ, приведенных в тексте Опп™ ’ полу',Вт преимущества в изучении работы с языком с, при изучении аги* К° ” чг,татели, которые имеют опыт полезных сведений, если опи*хг^°П1аМ‘М TflKHw могут почерпнуть очень не имеющие опыта и поогпамм,,^ ? расшиРить г-вои зияния. Читатели, адацвя 11 «се-таки получить удонА?0***”1”’ Могут иротгустить конкретные «ть удовольствие от чтения книги.
Предисловие
Обзор рассматриваемых задач
Книга состоит из двух частей. Первая часть .Основы. (главы с 1 во 4) дает обшес представление о структурах данных „ алгоритмах. в ней щж водятся необходимые определения геометрических ковцепдий и средств Во второй части «Применение, ставятся задачи и описываются возможные пути их решения.
В главе 1 формулируются общие контуры рассматриваемых вопросов, дается определение таких терминов, как алгоритм. структура данных, ана лиз. Здесь же обосновывается применение языка C++ и показывается надежность нашей реализации. В главе 2, которая касается анализа алгорП1МОв> приводятся концепции и методы пмялмвд характеристик алго* ритмов и структур данных. В главе 3 представлены классы языка C++, которые охватывают абстрактные типы данных, применяемые нами в последующем изложении, и структуры данных для реал идя пвм: связанные списки, стеки, несколько вариантов двоичных деревьев поиска. В главе 4 описаны классы для представления и обработки базовых геометрических объектов на плоскости я в пространстве. Кроме всего прочего, эти классы обеспечивают средства для вычисления точек пересечения двух прямых линий на плоскости и для классификации точек отялептельял прямой линии на плоскости или треугольника в пространстве.
Вторая часть книги организована по принципу алгоритмической пярядиг-мы — в каждой главе рассматриваются алгоритмы, относящиеся к данной парадигме. Так в главе 5 рассматриваются пошаговые методы ввода, в которых при вводе обработка каждого вводимого элемента производится отдельно, без анализа всего массива ввода целиком. В число рассматриваемых алгоритмов входят определения выпуклой оболочки для конечного набора точек, алгоритмы отсечения прямой линии и произвольного полигона по границе выпуклого полигона, алгоритм декомпозиции на треугольники специального класса полигонов, известных под названием монотонных полигонов.
Глава 6 содержит описания способов пошаговой выборки, в которых весь вводимый массив данных сканируется перед обработкой. Здесь налагаются еще два способа для поиска выпуклой оболочки конечного набора точек (способ «заворачивание подарка» и сканирование по методу Грэхема), алгоритм вычисления пересечения двух выпуклых полигонов за линейное время и пошаговый способ триангуляции набора точек на плоскости.
В главе 7 раскрываются алгоритмы сканирования на плоскости, в процессе работы которых сканирующая прямая линия перемещается по плоскости слева направо и решается подзадача для области, лежащей слева от сканирующей прямой линки. В одном из алгоритмов сканирования плоскости находится объединение коллекции прямоугольников на плоскости, в другом выполняется декомпозиция произвольного полигона на монотонные части.
В главе В описываются алгоритмы < разделяй и властвуй*, Решения задачи осуществляется путем деления ее на две пода„	'
каждой из которых примерно вдвое меньше, решения их °® исходной ’«тем объединения полученных результате. . И™
’«дачи. Алгоритмы такого типа применяются ь спос е . декомпо-й	("способ слияния оболочек), для декомпо
',ки для конечного набора точек (спосоо сл пиабнения плоскости ив Зиции произвольного полигона на треугольн , РЛ . Вороною, "«•иго,,,,.,участк„, известные под названием ойтадт.й Вороно.о.
ШИН-
54
P1 -uur Аительная геометрия и компьютерная графика на C++
основанные на пространственном иод-в главе 9 представлены ^“Подразделения - сетки, квадрантные ® Мы опишем три способа поди	по области на плоскости;
и 2-0 de^eM '	нТплоскости и прямоугольника со сто-
X заданных конечного наборе.точек	вайти те точки. которые лежат
ровами, параллельными «»°Р»ИВ’Х рассмотрены пространственные двои,-Хёрёвья длТрЬешення Шачи удаления невидимых поверхностен в про-странстве.
Благодарности
Автор благодарен целому ряду лиц, которые прочитали отдельные части этой книги на разных этапах ее написания и сделали полезные замечания: Давиду Донкину, Эрику Бриссону, Барту Розенбургу, Деборе Силвер и Шай Симонсон и другим рецензентам. Автор также благодарит факультет математики и вычислительной техники Университета Майами (Корал Габлс, Флорида) за предоставленное время и ресурсы для написания этой книги, а также Школу вычислительной техники и информатики Нова Саузистерн Университета (Фт. Лодердейл) за поддержку на стадии разработки этого проекта. Автор благодарит за энтузиазм и поддержку редакторов издательства Прентис-Холл Билла Зобрис (на этапе написания книги), Розу Кернан и Алана Алта и их помощников Филлис Морган и Ширли МакГури.
Автор чрезвычайно признателен за любовь и поддержку жены Элизы, без чьем помощи он даже не попытался бы написать эту книгу. Он также благодарен маленькой дочери Арианне и своим родителям Морису и Филлис.
Часть I
Введение
Введение
м этой главе мы представим основы для изучения алгоритмов, будь они геометрическими или какими-либо иными. Будут рассмотрены такие вопросы: что такое алгоритмы, что такое структуры данных, что подразумевается под анализом алгоритма? В нашу задачу входит правильно ориентировать читателя, показать основные концепции и сформулировать вопросы, относящиеся к задаче. Ответы и определения, представленные в данной главе, будут сжатыми и эскизными. Они будут подробно проанализированы в последующих главах, где будут даны более детальные объяснения со многими примерами.
Будет обосновано применение языка C++ и показано, насколько млтцным может быть его применение.
1.1.	Обзор задач
Рассмотрим вкратце область наших интересов. Вычислительная задача — это задача, которая может быть решена путем выполнения согласованного набора команд. Вычислительная задача формулируется в виде постановки задачи с описанием всей необходимой входной информации и желаемого результата как функции этой входной информации. Например, рассмотрим задачу, которую назовем ПЕРЕСЕЧЕНИЕ ГРАНИЦ: для двух заданных на плоскости многоугольников определить, пересекаются ли их границы. Требуемая входная информация содержит описания двух многоугольников в некотором заданном формате. Для двух заданных многоугольников результат будет «ДА», если их границы пересекаются, и «НЕТ» „в противном случае.	____
Алгоритмом называется способ решения вычислительной задачу Алгоритмы всегда абстрактны — данный алгоритм может быть реализова^Д^ личными путями, например, в виде компьютерной программы на различных языках программирования и выполнен на различных компьютер а ж,7д^адая компьютерная программа основана на алгоритме и, если программа хор<А1В составлена, заложенный алгоритм может быть понятен из текста yonyfe у мм. В качестве примера алгоритма можно привести алгоритм рещен радами ПЕРЕСЕЧЕНИЯ ГРАНИЦ: для заданных на входе двух многоут ельников каждая сторона первого многоугольника сравниваетсяс М»до£’ “ поромой второго. Положительный результат получается » 1аа’ какая-либо пара сторон имеет общую точку пересечения, и отрицательный
Глам 1
11
. ^тивном OTW. Заметам, что при этом потребуется еще дру. результат о противном i у	Q пересечении двух сторон многоугодъ-
гой алгоритм для принятия I е
ииков-	„ ппплЛи,мл является некоторой конструкцией, на осно-
Ллгорот,™.	рлзличные программы могут быть ее-
од'доЛ том же алгоритме, так и различные алгоритмы могут Z. кованы К» одной парадигме. Знакомство с явлением парадигмы по-могает нам попять п объяснить алгоритм, упомянутый ранее и помогает в разработке новыз алгоритмов. Алгоритм, приведенный для решения задачи ПЕРЕСЕЧЕНИЯ ГРАНИЦ, интерактивно использует парадигму пошагового выбора', па каждом шаге выбирается сторона первого многоугольника, ко-торая затем последовательно сравнивается с каждой стороной второго многоугольника. В этой книге будут применяться и другие алгоритмические парадигмы.
Алгоритмы используют объекты для упорядоченных данных, называемых структурами данных. Алгоритмы и структуры данных всегда используются совместно: алгоритмы мотивируют разработку л научение структур данных, а структуры данных служат в качестве строительных блоков, из которых формируются алгоритмы, Наш алгоритм ПЕРЕСЕЧЕНИЯ ГРАНИЦ основан на структурах данных для представления многоугольников и представления его сторон.
Абстрактные типы данных (ЛТД) дают общее представление структуры данных, независимо от ее применения. АТД охватывают набор операций, которые поддерживаются структурой данных и он служит в качестве интерфейса между алгоритмом и его структурами данных: алгоритм выполняет операции, поддерживаемые ЛТД, а структура данных реализует эти операции. Если эти действия рассматривать раздельно, то каждый АТД ярвастГ1Вляется а виде вычислительной задачи (набора операций), а каждая дтп ДйН”ых’ ко™₽ая обеспечивает эти операции, выдает решения для АТД. Наш алгоритм ПЕРЕСЕЧЕНИЯ ГРдттыгт г	и
дТп. дтп	иэГЬ;ьпы,И)> 1РАНИЦ требует использования двух
ступа ZSZT lUtI ,10Шг0,,й)' К°Т°РЬ1Й включает операции до-:*ТД	-ногоуго—. который
данных отрезков прямой линии. " ° Взапмном пересечении двух за-
Для большинства задач существует „
мов, а для многих типов абе-гоактцых л™ конкурирующих алгорит-тур данных. Как же выбрать напб/ . * ЫХ еСТЬ несколько вплов струк-горитмов позволяет выбрать г , Л<а Э^ОКТИВНО(? решение? Анализ ал-алгоритмов к структур данных А 1°°° описапия и измерения свойств решения заданной задачи к оцени? Да<!Т Основу Для выбора нанлучшего пример, анализ позволяет устаптт возм°жного лаилучшего решения. На-ритма ПЕРЕСЕЧЕНИЯ ГРАНИН к ’ ЧТ° Яремя аь»полиения нашего алго-““’«угодмиков (О;ЛГ ^•’"^«иоимыю произведению раз-числом образующих его сторон) т-L'?™ мпогоУгольиика определяется представлено решение, котовое ш 8Лгори™ !1е оптимален, в главе 6 сумме размерностей многоугольника °ЛКЯется зп !’Рсмя, пропорциоиаль-
Введение
1.2.	Применение языка программирования C++
В этой книге для каждого описываемого алгоритма и структуры данных будут представлены работающие программы на языке C++. Это позволило значительно повысить качество изложения материала книги. Во-первых, каждая программа дополняет сухое описание работы заложенного в нее алгоритма, что помогает раскрывать ключевые идеи формального описания алгоритма и представить детали, обычно опускаемые при сухом перечислении свойств алгоритма. Во-вторых, читатель может исполнить программу, модифицировать ее и провести с ней некоторые эксперименты для лучтлего понимания поведения заложенного в нее алгоритма но практике.
В приводимых программах будет использован целый ряд специфических свойств языка C++, отсутствующих в более раннем языке С. Мы будем применять классы, компонентные функции, управление доступом, конструкторы и деструкторы, операции-функции, механизм перегрузки стандартных операции и ссылочные типы. Мы также будем создавать новые классы на основе других (базовых) классов, когда новые классы наследуют атрибуты и свойства своих базовых классов. Также будут использованы шаблоны классов и функций, определяющие правила построения семейства классов или функций, которые могут работать с различными типами данных, передаваемых им в момент обращения, но в остальном ведущих себя аналогич
но.
Данная книга не имеет своей целью изучение языка C++. Объяснение некоторых языковых свойств дается лишь попутно для пояснения причин принятия определенных решений или в качестве напоминания читателю, но не для обучения языку. Хорошее введение в язык C++ изложено в книгах (23, 79], а также в аннотированном руководстве [26].
Также эта книга не может служить учебником по объектно-ориентированному программированию (ООП). Программы, представленные в книге, составлены так, чтобы их можно было легко понять и проследить их работу. Хотя обычно на практике универсальность сопровождается строгим соблюдением требований ООП, мы иногда будем отступать от этой практики ради простоты и краткости (без потери общности). При реализации ООП обычно тратится много времени и усилий на программирование при разработке классов с тем, чтобы впоследствии можно было сократить время на составление прикладных программ, в которых эти классы применяются. В атом смысле некоторые из приведенных в книге классов могут оказаться ♦бросовыми», предназначенными для одного-двух применений. В таких случаях мы далеки от намерения создавать класс для удовлетворения универсальных запросов. Объектно-ориентированное программирование освещается в jrie6-виках [11, 13, 15, 32].	„
Теперь нп очереди замечания относительно эффективности. Наши про-‘Тнммы в принципе эффективны. Однако мы намеренно пошли на некоторый компромисс ради ясности, нашему основному условию, то может при~ “ест*, например, к добавлению совсем не необходимой локальной веремсниой для соответствия концепции, излагаемой в тексте, веник» излишнего обращения к некоторой процедуре, вместо ППН(М» оп. "хранить результат предыдущего обращении, пдп	лнкпии которою
Ределскпой функции вместо более специализированной функции, которую
Глава!
^плбйтывать. Конечно, приветствуются любые нам почему-то не хотелось	чветие таких программ.
усилия читателя, направлен	опробовавы на компьютере Macintosh
все программы а этой	Symantec C++. При разработке про-
LCIII с использованием ко.	» описанная в Аннотированном
грамм учитывалась реялизаци паботают в том виде, как они приведены руководстве [26].	классов Point и Polygon, при-
в книге, только с одн»заменены н0 Point2D и PolygonZD. "„"было сделано 'псЛючительво во избежание конфликтов имен.
1.3.	Надежность
Алгоритм надежен, если он всегда работает правильно, даже если на входе встречаются вырожденные л специальные случаи. Конечно, одной из возможных причин сбоев может быть просто ошибка в алгоритме. Но если алгоритм верен, он может давать ошибку из-за округления при вычислениях с вещественными числами. Ошибка округления возникает из-за представления чисел с использованием ограниченного размера памяти (например, 8 байт для типа double). Так что все вещественные числа, которых бесконечно много, выражаются конечным числом представлений. Ошибка округления равна разности между вещественным числом и его приближенным представлением в формате с плавающей точкой.
Ошибки округления могут стать серьезной помехой для геометрических алгоритмов, подобных рассматриваемым в этой книге, которые работают в непрерывном пространстве, например, на плоскости или в трехмерном объеме. Идеальные точки могут быть описаны только приближенно, поскольку их компоненты являются вещественными числами. Идеальные отрезки прямых линии также могут быть лишь приближенными, так как они определяются своими концевыми точками и т, д. Ошибки округления сказываются наиболее сильно при осуществлении операций ввода, а в некоторых особых случаях они даже имеют тенденцию к относительному возрастанию, что вынуждает алгоритм выдавать возможное неправильное решение. Например, эта ппям'>НЫХ идеально” прямой линии и идеальной точке, через которую 4W то™ Ра_Т°ДПТ' КОМПЫО”₽ЯЯЯ программа может ’определить, на ней я зависимости	ПРЯМ°Й ЛИНН“’ Л"б° ЛеЖ"Т
линия и точка, прппадлежащая ей Из «пРедставлень1 сам“ П₽Я““Я даваемое программой решение может бь™	некоторых случаях выложенный а основу прогоаммо	неверным даже тогда, когда по-
вает идеальные геометрические объекты” С0Ве₽ШВ,га0 правильно обрабаты-
® ^тои книге были введены нр'-а
данных) для исключения возможны*0*^! ограничепня (например, на ввод мы становились менее универсалы особых слУчаев. Хотя такие програм-поскольку именно общие случаи Ыми’ ио Это не наносило особого вреда, того, обработка специальных случа/11*4ЯЮТСл И£1иболее интересными. Более лого ряда дополнительных опепатгЛ М0ГЛа потребовать включения пе-
Исключаемые особые случаи и»В' *П° затУ‘Мпиипаст общую идею.
редко. Тем не менее некоторые иа пр‘‘ктикс встречаются срнпиитслыЮ • Рассматриваемых и книг- 11р.,грамм спс-

Введение
— _______
циально были переработаны для вт
в которых иначе пришлось „ О3можности работы в
к обеспечению надежности работ™7ЯТЬ Другне программы Хгой'Х”’ исключении путем некоторой> XeLXТ С"~*
мененяя длины отрезка прямой пп У^овий задачи- НР«п„.
бесконечно малое расстонние, пере™"^,™еп’сц"я прямой линии адево ад мои линии, до совпадения с ней „ т ~«*«Щей очень близко от пря аении условии, чтобы исключить двусмыс^ Мключ"«ся я таком „X зависимым от представления геометрХко™ Г” " зделлть Решение не-
Задаче обеспечения надежности »₽ кого объекта.	н
очень много внимания. Хотя приняХТ?”? ” П0Слелвне годы уделялось щепринятым, но его нельзя считать окоадТ’ К“"ге Подход « являет™об приемы повышения надежности, но пх оадс Сг“"ст,да “ Д₽Х кш,ги-	°ПИСание аыходит за рамки донв™
- • п
1
‘ •“ сг .
Глава 2
Анализ алгоритмов
Анализ алгоритмов означает измерение и описание их характеристик. Понимание, насколько хорошо работает алгоритм, показывает, будет ли алгоритм практичен, будут ли достаточны предоставляемые ресурсы. Более того, анализ алгоритмов предоставляет основу для сравнения различных алгоритмических решений одной и топ же задачи, ранжирования их по характеристикам и выявления, какие из них наиболее эффективны.
При анализе алгоритмов обычно наибольшее внимание уделяется необходимому объему памяти я затратам времени на выполнение алгоритма. В этой главе основное внимание мы уделим затратам времени па выполнение алгоритма. Очень часто время является самым критичным ресурсом. Системы реального времени, подобные автоматам управления полетом или перемещения робота, должны срабатывать моментально, чтобы избежать, возможно катастрофической, критической ситуации. Даже в более спокойных системах, подобных текстовым редакторам, ожидается, что ответ на запрос оператора должен появиться практически немедленно, а другие программы, например. для анализа медицинских изображений, должны выполняться не более, чем за несколько часов. Иногда полагают, что время обработки становится менее критичным по мере разработки более высокоскоростных компьютеров. Но это пе совсем так. Задачи, возлагаемые гораздо сложнее, чем раньше, л рост их сложности компьютерных технологий, от которых часто решения.
Объем требуемой памяти
на компьютер сегодня, опережает возможности зависит получение быстрого
Многие алгоритмы, используемые ^a^^'Чйе жнее критичен, чем время. вхомыДаЯНОК книге’ кспользуют объем*паТ,,КС' также почти все, описан-загяч * ДаН_НЫх« так что если имеется л МЯТИ’ пР°поРЧИональный размеру пХя’ Т° °бЫЧН° ее	к	Памяти- “тобы загрузить
к	может 6ыть	больш"я чис”
Память к время к	Мя 0,‘“л,па требования
Х3Х“ ~ЫмП°₽^^™ "еЭвВ,,еИМЫН" . Часто яачво.тх ° в::да ,да“""«тсн □:днога *» -«<>" ресурсов зя
~Та\~	— ,.ро.
^1₽8КТ,ИЙ модели аХЛ’,..и"УЛ'ТПТП' "Ср,,ЫЯ ’’ни, (ггп|юм - и опредс-
Анализ алгоритмов
леЮПт зависимости времени работы от объема входных данных. третий -в выражении времени работы в виде простой функции	J в ПОС-
леднем разделе обсудим проблему сложности, которая определяет сколько времени необходимо и достаточно для решения заданной задачи. ’
2.1.	Модель вычислений
Для анализа алгоритма нам сначала нужно идентифицировать операции, которые в нем используются, и стоимость каждой из них. Это определяется моделью вычислений. В идеале нам нужно выбрать такую модель вычислений. которая абстрагирует наиболее существенные операции алгоритма. Например, чтобы проанализировать алгоритм, работающий с полигонами, мы могли бы подсчитать, сколько раз происходит обращение к вершинам полигона. Числовые алгоритмы обычно анализируются путем подсчета арифметических операции. Алгоритмы сортировки и поиска анализируются путем подсчета количества сравнений пар элементов.
Стоимость каждой операции, учитываемой моделью вычислений, устанавливается в абстрактных единицах времени, называемых шагами, В некоторых моделях вычислений каждая операция стоит определенное постоянное число шагов, стоимость некоторых операций может зависеть от значений аргументов, с которыми выполняется данная операция. Время работы алгоритма для указанных входных данных равно общему числу выполняемых шагов.
Хотя принятие абстрактной модели вычислений упрощает анализ, модель не должна быть настолько абстрактной, чтобы она игнорировала существенную часть работы, выполняемой алгоритмом. В идеале время между последовательными операциями, учитываемыми моделью вычислений, постоянно, так что его можно игнорировать. Тогда время работы — общая стоимость операций, учитываемых моделью вычислений — пропорциональ
но фактическому времени выполнения алгоритма.
Поскольку время работы зависит от модели вычислений, то было бы естественным принять одинаковую модель вычислений для различных решений одной и той же задачи. Тогда путем сравнения их времени работы мы могли бы выявить наиболее эффективное решение (если бы модель вычислений менялась, то нам бы пришлось сравнивать яблоки с апельсинами). По этой причине в основном мы будем применять модель вычислении — Хотя довольно часто только неявно — как часть формулировки постанпнхн
задачи. Чтобы показать эту идею более конкретно, рассмотрим задачу почина заданного целого числа х в отсортированном массиве целых чисел. Присвоим этой задаче имя ПОИСК:	*
заданного целого числа л и отсортированного мзеси&з а различных целых чихл сообщить "нла-с числа х внутри лвссива а, а если rawxo числа в массиве а нет. то еоэврэша^-л. лечение быть равно -1
Целое число х здесь служит в качестве ключа поиска.
Модель вычислений, которую мы примем для задачи ПОИСК, <юдг/...(
Т0,1ько пксрцщпо типи проба: сравнение ключа поиска х с ’““••лом о массиве, В предположении, что целые числа ограничены
личине, одиночная проба может быть выполнена за постоянное время. Так что мы можем сказать, что стоимость пробы составляет один шаг.
Простейшее решение ПОИСКА может быть реализовано путем последовательного поиска: пошаговый просмотр массива а с пробой каждого целого числа. Если число х найдено (поиск завершен), то возвращается его индекс в массиве а, в противном случае возвращается число -1, показывающее, что искомое число не обнаружено. Возможная реализация может быть следую щей:
int aeguanti alSearch(int x, int a(J, int n) I
for (int 1 = 0; i < n; i*+)
if (afij = x)
return i;
retum -1;
Если время работы программы sequentialSearch обозначить в функции от введенных данных через f(x, о, п) , то имеем
k + 1 если число х обнаружено в позиции k п в противном случае (поиск неудачен)
Заметим, что выражение (2.1) определяет время работы — общее число проб, или шагов как функцию всех допустимых входных данных. Заметим также, что поскольку программа sequentialSearch затрачивает только постоянное время на операции между последовательными пробами, ™ Э™ выразкен1,е может служить хорошея мерой для фактической характеристики программы.
2.2.	Мера сложности
Выбор достаточно абстрактной мл
ЛИЗД. Для дальнейшего упрощения аЬПО,слений Упрощает задачу ана-ОТЫ как функцию только размера вхс™"38 будем выражать время разводимых значений. Этому есть две п °Г° массиаа* а не всех допустимых проще выражение для определения впАх.П°ВНЫе причины« Во-первых, чем divZt7** Врем<?нз Работы различных алго™^" рнботы’ тем легче будет срав-спг -1ЦИК НпСХ допУСТГгмых вводимых ? , ™оа’ Описание времени работы в ритмов ^вЖе выражет’е (2.1) нескш3 ,еНИ” МОжет быть громоздким и оказаться3^ Ж° Простых- как последова^0 Неудо6но’ а очень мало алго-допустимых »»едим«°ТаН“'<	Во ВТ°ры*' М0”“‘Т
ствительио к «пл эн»чевий. Поведвми.» 1 Работы в терминах всех проследить мяриа1^ЫМ ’На',еи«ям и было f иоги* алгоритмов очень чув-ЛИЧНЫХ входных t Путей» ПО которым ОЫП J практ,,чески невозможным ь-р	пр.. рМ.
ватуральтлм^рХ^”^ С"°"16 “«'"ни рад™ ‘Шмпти' “««олыуемый лля — толика мпг.гип чисел, то
диализ алгоритмов
ег„ размер определяется длиной массива, еелв вводится полигон то в ка честэе размера ввода может быть взято ввело вершив,	Г состав
полигона. Здесь подойдет любая разумвая ехема. если он. yZXop^ а0 применима для всех решений данной задачи	хдмш.створитель
Выражение времени работы в зависимости от* размера ввода уменьшает количество учитываемых параметров до одного. Поскольку сто может быть реализовано самыми различными способами, то определение конкретного значения времени работы становится несущественным. Мы будем применять его оценку лишь в качестве определенной меры сложности, описывающей только тот пли ивои аспект оцениваемой характеристики. Мера сложности обычно определяется для наихудшего случал, в среднем пли при средневзвешенном анализе.	*
2.2.1.	Время работы в наихудшем случае
Время работы в наихудшем случае определяется для самого длительного выполнения алгоритма для любых входных данных при каждом размере ввода. Это очень полезная мера, поскольку производится оценка работы программы в самой плохой ситуации. Например, поскольку программа seguentialSearah осуществляет п проб в наихудшем случае (когда работа завершается безуспешно), то время ее работы Т(п) в наихудшем случае равно Т(п) = п.
Вычислить время работы в наихудшем случае иногда сравнительно просто, поскольку нет необходимости рассматривать каждую возможную ситуацию, а только самую неблагоприятную для каждого размера ввода. Более того, время работы в наихудшем случае часто представляется в виде точно определяемой границы, поскольку наихудшие характеристики явно определяются видом входных данных, используемых для анализа.
2.2.2.	Время работы в среднем
Время работы в среднем определяет усредненное значение времени работы для всех видов входных данных для каждого размера. Время работы в среднем для программы sequential Search равно в предположении, что каждый из п элементов может быть выбран о равной вероятностью и что попек завершается успешно. Бели второе предположение не удовлетворяется, то время работы в среднем лежит в диапазоне [-у^л] в зависимости от причины.	______	 ' '
Анализ ситуации в среднем обычно более трудный по сравнению с анализом наихудшего случая, поскольку он существенно зависят от определения, что считать типичным вводом. Даже если мы примем сравнительно простые предположения (например, о том, что равновероятно постусловие на вход данных заданного размера), то все равно вычислении ОвВЯЧИЖ кожными и, кроме того, если программе придется работать с данными,для которых не обеспечены предполагаемые условия, то анализ может дахедй 3Ультат, сильно отличающийся от действительности.
Глава 2
2.2.3.	Средневзвешенное время работы
пПнИОйша₽. алгоритмом, обычно взаимозависимы. Парамет-Операдяи, вы по _ сто^МОсть в частности — зависят от того, что ры каждой операци „ нвК0Т0пых алгоритмах невозможно обеспечить ХХ'иеУ<= для всех операций одновременно. В более общем случае, XX сравнительно дорогих операций может быть перекрыта большим числом недорогих операций. При средневзвешенном анализе делается по-пытка учесть такую ситуацию: учитывается средняя стоимость каждой операции в худшем случае.
Рассмотрим алгоритм, в котором выполняются п операции с многодоступным стеком 5. (Более подробно стеки рассмотрены в разделе 3.4). В этой структуре данных выполняются такие две операции:
•	s.push(x) — занесение элемента х на верх стека s.
•	s.pop(i) — выборка i самых верхних элементов из стека s. Операция не определена, если стек в содержит менее i элементов.
Как часть нашей модели вычислений предположим, что операция s.push(x) выполняется за постоянное время (или за один шаг), а операция s.pop(i) — за время, пропорциональное i (т, е. за I шагов).
Средневзвешенный анализ может быть выполнен несколькими способами. Мы будем использовать подход, основанный на бухгалтерской метафоре, в которой различным операциям присваиваются некоторые «веса». При так называемой взвешенной стоимости операции каждый такой вес может быть больше или меньше фактической стоимости операции. Недорогим операци-ям обычно приписывается взвешенная стоимость, которая превышает их действительную стоимость, а наиболее дорогим — меньше. Когда взвешен-пп₽пгтаПМ0СГЬ.°г еРэщш превышает ее фактическую стоимость, то разность ния попышрмСи 01 кредпт* который может быть использован для погаше-Идея заключается д8Т₽аТ Hd_ ВЫПОляен»е последующих дорогих операций, идея заключается в том, чтобы аазначать так, чтобы накопленные кредиты за стоимость дорогих операций.
В отношении стека фактические » енться следующим образом:	извещенные стоимости могут соотно-
взвешенные стоимости операциям счет недорогих операций перекрывали
Операция	Фактическая СТОИМОСТЬ	Взогшеииа» стоимость
ЗрАК»)	1	2
5-РЧХг)	1	0
		
несения	у“М™'"'гая стоимость
Пр» каждой мпвси ,леДХ“а8га* • ПЛеМе^в\=1т“- «Нии™ коплены в резерве _
для каждой операции за-Лля 0ПсРашп1 выборки pop — нулю-o_ek,e.	-	-----кадится R СЧ<?1 °ПЛП™ ОШфЛЦИИ ОТНОСИТСЯ
Т°В СТОИМость операции будет г 01Ж’’ ^огля при выборке* из стека “° ХГГ '	-тОР...С Г.,..ли ««•
элементе» в	°б'“ИЙ к^ит. «рнквдшйУ’	:1Л,,М1’"1- D
-ке-	^"ящийсн в ррзррщ,, |Ш1И.И числу
диализ алгоритмов
17
Обшая взвешенная стоимость достигает максимума при непрерывности последовательности п операций записи в стек и составляет 2л шагов. Следовательно, в среднем в наихудшем случае каждая операция выполняется 30 два шага, т. е. каждая операция выполняется за постоянное время при оценке в смысле взвешенной стоимости.
Ключ к такому бухгалтерскому’ подходу лежит в выборе взвешенной стоимости, а это, исключая предыдущий пример,— довольно трудная задача. Необходимо найти определенный компромисс. Во-первых, общая взвешенная стоимость любой последовательности операций должна быть не больше верхней оценки общей действительной стоимости этой последовательности, Тем самым обеспечивается, что при анализе будет получена некоторая оценка. соответствующая общей фактической стоимости. В нашем случае общий доступный кредит первоначально нулевой и никогда не может быть меньше нуля, так что общая взвешенная стоимость никогда не может оказаться меньше общей действительной стоимости. Во-вторых, необходимо обеспечить, чтобы принятое решение было как можно более строгим п выбор навешенной стоимости был таковым, чтобы она наиболее близко соответствовала верхней оценке. В нашем примере общая взвешенная стоимость превышает фактическую стоимость на число элементов в стеке, поэтому она никогда не может быть более, чем удвоенная фактическая стоимость. В третьих, общий кредит никогда не должен быть отрицательным, иначе могла бы появиться ситуация, когда было бы нечем платить за некоторую последовательность операций.
Взвешенный анализ часто применяется для поиска верхней границы времени работы алгоритма в наихудшем случае: устанавливается зависимость количества операций, выполняемых алгоритмом, от размера ввода и затем вычисляется взвешенная стоимость каждой операции. Если затраты времени между двумя последовательными операциями постоянны, то время работы алгоритма в наихудшем случае ограничено сверху общей взвешенной стоимостью. Анализ такого вида часто обеспечивает получение более точной оценки для верхней оценки времени работы в наихудшем случае, чем остальные более простые виды анализа. Относительно нашего последнего примера многократной выборки из стека мы могли бы наивно утверждать следующее: поскольку в наихудшем случае операция выборки pop требует до л шагов в наихудшем случае, то на выполнение последовательности п операций могло бы потребоваться шагов. Однако взвешенный анализ выявляет, что такая последовательность операций невозможна к последовательность п операций может быть выполнена не более, чем за 2п. шагов.
1111
t
я
S
..
2-3. Асимптотический анализ
“ л
fr«
2.3.1. Функции скорости роста
До сих пор мы применяли два известных подхода к у1 паботы ‘'-пользование абстрактной модели вычислений и продета плтаола оассмат-г1г°ритма в виде зависимости от размера ввода, рп тРе™’ изменяется время Пишется асимптотическая эффективность алгоритмов.	Пусть время
Работы алгоритма при возрастании размера ввода до
Глава 2
II
,	в наихудшем случае) выражается функцией
работы алгоритма (напрР.	функции f и мы будем ее опре-
f(n). Нас будет лптеРесова^^ СКорости роста Т(н).
делать через	дл) = ап2 + Ъп + с при некоторых констан-
Иапрх^р пред ’а параметра п влияние членов малого порядка ™* а> Ь “ *	__ ИРНМИ можно пренебречь, т. е. функцию ftn) можно счи-
t4PS а/А Более того, даже коэффициент а в главном члене станочек Сравнительно незначащим при увеличении размера вводатак что функцию /(п) можно вполне заменить простои функцией Т(п) - п .
Нетрудно убедиться в том, что коэффициентом при главном члене можно пренебречь. Если принять, что Т(п) = ап2, то каждое удвоение параметра п приводит к увеличению времени работы в зависимости от Т’(п), а не от а. Действительно, мы имеем
Г(2л) = fl(2n)z
= 4а п2
= 4Т(л)
Т. е. время работы учетверяется при каждом удвоении размера ввода и не зависит от величины коэффициента а.
В более общем случае, если Т(л) является функцией скорости роста, состоящей из главного члена с коэффициентом а, то Т(2п) будет функцией от Т(п) и, возможно, с компонентом более низкого порядка, зависящим от л и а. Это показано в следующей таблице, в которой функции упорядочены по скорости роста.
Т(п)	Т(2л)
а	Т(п)
alogn	Т(л)+а
аг»	21(п)
ал log п	2Т(п) + 2ал
Л ап	4Т(п)
ал3	8Т(п)
Г	ИСп)]®
Различные функции T(ni пор
рис. 2.1 при а = 1 и 1о₽ л г noJa3aHU в логаРифмическом масштабе на Время работк в нХдшем ~"вМ 2‘
этой книге, пропорционально одной АЛЯ алгоР11™°в» рассматриваемых в Ций скорости роста. В этом списке ннжеслеДУющих простейших функциям, почему отнесенные к дяйпп^ТРаЖеНЬ1 так>ке наиболее общие при-таким образом:	‘ у ВИДУ алгоритмы работают именно
1 (постоянное время) — такИр а
висимое от размера ввода. ' 1гори™ы выполняются за время, неза-log п (логарифмическое время» _
яутем многократного разбиения залач^₽ИТМЫ этого класса выполняются на подзадачи фиксированного ра3'
Аилдиз алгоритмов
1*
п (линейное время) — TaKMe яп
время на обработку каждого элем<ш^И™Ы обычно затрачивают п log п (время, пронорционадьн» „“"loe"' ,
лежат алгоритмы типа .разделяй „ „ " л> ~ * этому классу декомпозиции исходной задачи на более м»' К0Т0₽“е ’“"Олияю^сХ^ отдельности и последующего объедини” Л™ Подзал«->. решен™ ™ в
" (квадратичное время) _ большиХо	₽и™™х>в.
постоянное время на обработку всех "ар вхо„ "ЛГ0Р1'™°" затрачивает " (кубическое время) - а больпп.Х ДНЫХ эле«евтов.
каждой парь, входных элементов	“*
2.3.2- Асимптотическая нотация
аотапия используется для выражения времен., работы алгоритмов ХсХ аостн задачи, она часто применяется в доказательствах, предусХХ щих ограничения количества или размера объектов. Зде ь L осудим™ новные элементы такой нотации.	ос
"4
f “
ппя обозначения iMfffti О-нотация
m/шил) используется дл	. запиСь
О-нотация (или большое о « икЦИй. Если есть ФУ не быстревъ чем «их оценок скорости P0CJ* всех функций»	более, чем» нек0Т?^>
0(/(п)) обозначает множест	n/f(n)), если ^('0 большом Более фор-
1(п). Функция д(л) принадле	прН дОСтаточн
постоянное число раз превыше
Глава a
10
„„„ мркотопого вещественного числа с > 0 су. мально, М Е О(Кп)), если д	< с	всех п > nQ
шествует целое число по - 1	констант а н ь. Чтобы убедиться в
Например, ал + Ь	«	б иметь
этом, выберем с - а + 1 И ло
ап + b = (а + 1)л + Ь - 1 = СП + ло ~ Л
< СП
Выоажеш.е времени работы с помощью О-нотацип часто применяется без указания меры сложности. Когда говорится, например, что алгоритм вы-□олняется за время О(/(я)), то это означает, что время его выполнения ог-раннчено сверху значением О(/(л)) для всех входов с размером л. При этом предполагается, естестветствепно, что время выполнения в наихудшем случае также ограничено сверху величиной O(f(n)).
о-нотация
о-нотация (или малое о-нотация) используется для обозначения точных верхних оценок скорости роста функций. Если есть функция f(n), то запись о(/(л)) обозначает множество всех функций, которые определенно растут медленнее, чем /(л). Более формально, g(ji) принадлежит o(f(n)), если для любой константы с > 0 существует целое число по £ 1 такое, что g(n) S с f(n) для всех л > ло. Легко видеть, что o(f(n)) С О(/(л)) для любой функции /.
о-нотация привлекает внимание к главным составляющим функции. Например, для выделения главного члена функции f(n) = anlog2 п + Ьп + с ее можно переписать в виде /(л) = anlogo п + о(л log л). Это означает, что член низшего порядка существует и он может быть опушен.
П-нотация
оценокскопогтч?1 г0Лиг°',!УШа<<МЛ) ,,СЦ0льзУется Для обозначения нижних
ЖЛ"» обозначает	ФУНКЦ"Я 'М‘ Г°
ленно чем Ппк»»- « ФУНКЦИЙ, которые растут не более мед-
функция Цп) пряпадляжит ад„^ЛХСХМпР"ЧНП оп₽еделению
С„;по°	"0^
6-нотация
6-нотация и П-нотация могут бы
деления множество функций дл ** нспользованы совместно для определенных пределах. Нацримеп эГп К°Т гР,ЫХ СКОР°СТЬ роста лежит в опре-ций, которые обладают скооос'тьи '** J П f Г' обозначает класс фуик-л, 9-нотация (или тэта-цотаиии\ ** Менсе’ »», но нс более, чем между оценками уменьшается по^„^"^ьзуетсл тогда. когда разность хеаговдо;т--ифункиилбУд^?;,;1со^т''роптп м°)кет б,лгь °пре’ <hnnM^,yHKtUl^' KOTOI)we имеют такую • Т°	будет обозначать класс
Формально эту нотацию можно опрсдпл» CKopf,CT,‘ Р^«. что и ((н). Более <Th ка«	(ИЦ,,)) г. H(/(«)h
диализ алгоритмов
2.4.	Анализ рекурсивных алгоритмов
Очень многие алгоритмы решают большую задачу путем ее декомпозиции на более мелкие подзадачи того же вида, решения их и последующего объединения полученных решении для формирования решения исходной задачи. Поскольку подзадачи относятся к тому же виду, что и исходная задача, пни решаются рекурсивно тем же самым алгоритмом. Для обеспечения безусловного завершения процесса подзадача в конце концов должна стать настолько малой или достаточно простой, чтобы ее можно было решить непосредственно. Такой общин подход известен как рекурсивная декомпозиция.
Алгоритмы, основанные на таком подходе, могут быть проанализированы с использованием рекуррентных отношений. Рекуррентная формула представляется в виде последовательности целочисленных членов, причем каждый член последовательности определяется через предыдущие члены той же функции. В качестве примера можно привести известную функцию для вычисления факториала n! = n*(n - 1)*. . . *2*1*1, которая описывается рекуррентным отношением:
Л1 _ п*(п - 1)! если п > 1
1	в противном случае (при п = 0)
Для анализа алгоритма, основанного на рекурсивной декомпозиции, мы будем выражать время работы при вводе данных размера п в виде рекуррентного отношения. Например, рассмотрим сортировку путем выборки, при которой выполняется сортировка п целых чисел выделением из набора сначала наименьшего числа, и затем применения этого же алгоритма к оставшимся п - 1 числам:
void selectionsort (int а[], int n) (
if (n > 0) (
int i = positionOfSmallest (a, n) ;
swap(a[0], a[i]);
selectionsort (a+1, n-1) ;
I
При обращении к функции positionOfSmallest(аг п) возвращается индекс наименьшего целого числа в массиве о с длиной п. Функция выполняет сканирование массива, сохраняя индекс позиции, в которой до те-кугцего момента было обнаружено наименьшее число, и время ее работы составляет 0(п). Функция swap выполняет взаимную замену двух целых чисел за постоянное время.
Время работы сортировки выборкой как функция (я) при размере п выражается рекуррентным отношением -	•	 • «.Oil
2Т (л - 1)+ ап если п > 0
в противном случав (л - 0)
T(n) =

где а и /> — константы. В выражении 2.2 член г (n Имости рекурсивного вызова, ап — i—_ лого числа и взаимного обмена его с а[0] и b — 0.
стоимости обнаружения наименьшего це-стоимости возврати, если
Глава 2
„пптппоаки выборкой с другими алгоритмами сор. Для целей	еобразов«ть выражение 2.2 в закрытую форм£
тпровк.. п₽м”0™'?”“ Рв терминах а, а и 0. Мы вкратце рассмотрим т. е. выразить Т(п) т	рекуррентных отношений в закрытой
два способа получении решения Р ^ Р	подстановки.
форме: способ поэтапного суммирование
2.4.1.	Поэтапное суммирование
Идея способа поэтапного суммирования заключается в постепенном раскрытии рекуррентного отношения до тех пор, пока оно не будет выражено суммой членов, которые зависят только от л и начальных условий. Практически для этого бывает необходимо выполнить раскрытие только несколько раз, пока зависимость не станет очевидной. Например, выражение 2.2 может быть решено так.
Т(п) = Т (л - 1) + ап
= Т (п - 2) + а(п - 1) + ап
= Т (л - 3) + а(п - 2) + а(п - 1) + ап
= Т (0) + а + 2а + ... + па
= b + а(1 + 2 + ... + и)
= b + а[л(л + 1)/2]
а о , а
= г" +гл + ь
Следовательно, Т(л) 6 О(п^). Откуда получаем, что сортировка выбор-к°й а также все другие алгоритмы, описываемые выражением 2.2 — выполняется за время О(л2).
Заметим, что в этом выводе используется тот факт, что V " I = п(пЯ). Суммирование не всегда выполняется так просто. В некоторых случаях^мы nni^eM сделать ато не лУ-ше, чем ограничить суммирование сверху, что приводит к верхней оценке для Т(л).
2.4.2.	Способ подстановки
“ вЛШтв!/й'- »сп°лм>тог собов в режиме «разделяй и marmvc В большинстве простейших спо-подзадачи, каждая из которых приметно 3адаЧЭ Раэделяется н0 ДВС исходная задача. Предположим, 7то кажлыйТ Р838 МеНЬШе °° размерУ’ ЧвМ композиции исходной задачи разме Д ДВУХ пР°цессов ~ процесс денации их решений в общее пргнр Р3 Л На ДВе П0дзаДачи и процесс комби-О(п). Примером этого может служит^ ИСХ0ДН0^ заДачи — занимают время тировкн массива длиной л при norm 3аДача сйРпшРовки слиянием. Для сор-левый и правый подмассивы и	сиянием рекурсивно сортируются
подмассива:	-ате« сливаются вместе оба отсортированных
void -Wrtdnt <(], n) i* <n > 1) {
int a  n I 2;
«•rgeSortfa, }.
"*rg«Sort(*+ttf n^) ;

дивлиз Алгоритмов

merge(at mt n) ;
Шаг слияния выполняется при обращении к функции merge (а, т, и), которая сливает два отсортированных подмасспва а[0..т-1] и a[m..n-l] в единый массив а [ 0 . . п-1 ]. (Здесь мы используем запись а [ 1. . и] для обозначения подмассива а между нижним индексом 1 и верхним индексом и включительно.) Функция merge работает путем сохранения указателя на наименьший еще не слитый элемент в каждом из двух отсортированных подмассивов. Указатели итеративно перемещаются слева направо до тех пор, пока не будет выполнено слияние. Работа шага слияния, выполнение которого занимает линейное время, во многом похожа на складывание вместе двух отсортированных частей колоды игральных карт. (Сортировка слиянием более подробно будет рассмотрена в главе 8.)
Время работы Т(л) сортировки слиянием описывается следующим рекуррентным соотношением, в котором для простоты мы предположим, что число п выражается степенью числа 2:
2Т (|) + ап если п > 1
Ъ	в противном случае (л = 1)
t
✓


1


Г(п) =
(2.3)
d

Для решения уравнения 2.3 воспользуемся способом подстановки. Идея заключается в выборе некоторой закрытой формы решения рекуррентного уравнения и последующем применении математической индукция, чтобы убедиться в правильности решения/ Для решения уравнения 2.3 путем индукции мы покажем, что

T(n) < ci n log n + C2
(2Л)
при соответствующим образом выбранных константах ci и сг. Пусть
С2 = b
будут пригодны для нашего случая.	____
На индуктивном шаге предположим в качестве индуктивной гипотезы, что выражение 2.4 является решением уравнения 2.3 для всех степеней числа 2, меньших, чем п. Тогда будем иметь
Т(п) = 2Т (•||+ ш»
С)П . п ~Tlog2
' М-т.мтг.еек,, „нлухцч. чрелстдыдет собой	““7®/“ZZS
спрппедл|щОСТИ послсдоватсльиостн утверждений РмА ( /• *	„ет.„ prt) Ня
........лук.ч.к.) 0.КЛЮЧ00ТО1 о доклэлтельетм	М?’ ’'“чутцеи. инЯуклишпш. шаге,	« ^ерждишл*?!*^” ЗиЛ'
тпгдп, ггЛИ р(П) перпо, то должно быть	шпотехСь.	.а»
11к,рЛ5кг|ц|(. ппдуктипного шага Р(п) называется У
‘Mkr i 27’11
«8
Глава 2
= ап log у + 2с2 + ап
= an log п - ап + С2 + ь - Ьп s an log п + сг
Это и будет решением индуктивного шага.
Основной шаг индукции также следует из нашего выбора а и с2. Ниже показано, что уравнение 2.4 удовлетворяется при п — 1:
Т(1) = b
= С2
S ci (1 log 1) + С2
Из приведенного обсуждения следует, что время работы для сортировки слиянием равно 0(n log п) в наихудшем случае.
2.5.	Сложность задачи
Если нам известны параметры алгоритма, то мы можем сравнить его с другими алгоритмами решения такой же задачи. Но предположим, что нам нужно было бы определить, будет ли алгоритм оптимальным, и не существует ли более эффективное решение. Одним из способов решения, является ли данный алгоритм оптимальным, заключается в сравнении его с другим алгоритмом, который по какой-то причине считается оптимальным, но это срабатывает только в том случае, если его оптимальность действительно можно доказать. Необходим другой подход, если мы хотим избежать такой бесконечной последовательности. Как же определить, будет ли алгоритм оптимальным? Или, в более общей постановке, как можно определить, сколько максимально потребуется времени на поиск любого решения для данной задачи?
здтрвт времени для решен,,» Мдач„ Есл ,7 °6х0Д"МЫХ " a°CTaTO4HUlt меня меньше, чем необходимо то это можёт ° 3ад0ЧИ аятр0',ен0 а₽е' дозрения, что задача решена невегш- ч служить основанием для понять эффективность конкретного Ушенин* ДОСТаточное вРемя’ можно оце-усовершенствования.	’ ния 11 опРеДелить возможные пути
Когда сложность задачи
0(/{л)). Сложность большинства заХ ™ЧН0' ее М°>«КО выразить а форме нако в ряде случаев единственнор u °°Ы’,Н0 известна довольно точно. Одине и верхние границы сложности „° МОЖно сДелять, это установить ниж-оцевкл до тех пор, пока они не совпал3™* попытаться сближать эти две му I •
2.5.1.	Верхние границы
Л’обое решение заданной
W(«»X.X‘eХШреше Л°лЧу"'"’Ра™ 77еТяН0СТ1' COe₽Xy: “7!
’*	буде; т"„ ЛаН!"№ ИШ«„ио „„ляетея опта-
• В протинпом случпл, ослн ре-
лилдиз алгоритмов
тенпе почти оптимальное, то верхняя оценка может быть снижена по мере открытия более эффективного способа решения.
Напомним о задаче ПОИСК, в которой поиск числа п отсортированном массиве О(п) различных чисел осуществлялся на основе проб. Поскольку последовательный поиск выполняется за время О(л) в наихудшем случае, то значение О(п} представляет собой верхнюю оценку для этой задачи. Однако, О(п) не будет точным решением, поскольку существуют более эффективные решения. При двоичном поиске используется то преимущество, что массив уже отсортирован. При заданном подмассиве а[1,.ц) в процессе двоичного поиска ключ поиска х сравнивается с элементом а[т], расположенным в позиции m примерно посередине между позициями 1 и и. Если х не больше, чем а (т), то двоичный поиск рекурсивно продолжается в левом подмассиве а(1..т], в противном случае (х больше, чем a [mJ) рекурсивный двоичный поиск применяется для правого подмассива а[т+1..и]. В конечной ситуации (1==и) задача решается непосредственно. Приведенная ниже функция возвращает индекс х в подмассиве а[1. .и) или -1, если поиск закончился неудачно:
int binarySearch (int х, int а[], int 1, int u) (
if (1 = u)
return (x — a[u]) ? u : -1;
int m = (1 + u) /2;
if (x <= a [m])
return binarySearch(x, a, 1, m) ; else
return binarySearch(x, a, m+1, u) ; I
Чтобы найти число x в массиве а длиной п, обращение к этой функции на верхнем уровне должно быть binarySearch(х, а, 0, п-1).
Время работы двоичного поиска в наихудшем случае определяется рекуррентным соотношением:
Т(п) = 
2Т (?) + а Ъ
если л > 1
в противном случае (n = 1)
(2.5)
Нетрудно показать, используя любой из описанных ранее способов, что Пл) е O(log л).	______
Из нашего обсуждения следует, что O(log п) будет верхней оценкой для задачи ПОИСК. Будет ли эта оценка точной или может существовать более эффективный способ, чем двоичный поиск? На самом деле эта оценка точная — двоичный поиск оптимален. Чтобы убедиться почему, обратимся к
определению нижней границы сложности задачи.
2.5.2.	Нижние границы
Определение хороших нижних границ может оказаться сложным делом. Значение адя» для нижней границы сложности некоторых задач предпо-. адгает, что для их решения необходимо время, равное по крайней мер». Но можем ян мы быть уверенными, что каждое решение требует
По кр.йое» мере «Я«» еР«ме»и " -
“™ Т -
KeZZoTn” мощью О'Р«* Р™"™ “ Доказательство “У™ Р^укции.
деревья решений	Любой алгоритм> основанный на своей
Вновь вернемся к задаче пип	временем выполнения) может рас-
модели вычислений (проба с “°-	в дереве решения отражаются все
смвтриватьсл в качестве дере Р данного размера (для каждого раз-возможные вычисления для в	Каждый узел дерева решения со-
мера входа имеется свое дер выходяшая пз каждого узла, соответствует ответствует пробе. Пари Р Р«	ченпому на основе результата пробы,
оператору
И вычисления продолж. .	_ КОнечные узлы, не имеющие
результату. На рис. 2.2
показано дерево решения для программы binaty^earch, применяемой для поиска в отсортированном массиве длиной 3. Фактически дерево решения является двоичным деревом, в котором пробы ассоциированы с каждым внутренним узлом (изображенным в виде овала), а принимаемые решения ассоциированы с каждым внешним узлом (изображенным в виде квадрата). Двоичные деревья более подробно ассоциируются в разделе 3.5.
Дерево решения можно использовать при определении нижней границы для задачи ПОИСК. Каждый путь от самого верхнего узла (или корня) вниз до некоторого внешнего узла отображает возможные вычисления и длина пути равпа числу проб, выполняемых при этих вычислениях. (Здесь под длиной пути понимается количество ребер, содержащихся в нем.) Следовательно, длина самого длинного пути в дереве решения представляет коли-
АН4АИЗ алгоритмов
>7
честно проб, выполняемых в наихудшем случае. Насколько коротким может быть этот самый длинный путь?
Для задачи ПОИСК с массивом длиной п дерево решения должно содержать по крайней мере п внешних узлов: каждое их п отдельных решений (нли п. + 1, если мы включаем отрицательный результат поиска) связано по крайней мере с одним уникальным внешним узлом. Покажем, что в любом двоичном дереве, содержащем п внешних узлов, существует некоторый путь с длиной, ^равной по крайней мере log л, При высоте двоичного дерева, определенной как длина самого длинного пути от корня до внешнего узла, можно утверждать, что вам необходимо показать следующее’
Теорема 1. Дроичное дерево, содержащее п внешних узлов, имеет высоту по крайней мере logs л.
Доказательство этой теоремы можно выполнить путем индукции числа а. В качестве основы индукции берем na 1, в этом случае дерево состоит из единственного внешнего узла. В этом случае единственный путь имеет длину 0 = log2 1, т, е. базисное условие соблюдено.
Для индуктивного шага предположим в качестве индуктивной гипотезы,
что теорема удовлетворяется для всех двоичных деревьев с числом внешних
узлов меньше, чем л. Нам нужно показать, что она удовлетворяется для
каждого двоичного дерева Т, содержащего п вне
нит
узлов. Корневой увел
дерева Т соединяется с двумя меньшими двоичными деревьями Ti и Tjj,
содержащими деревьями ni я П2 внешних узлов соответственно. Поскольку Л1 + Л2 = н, то либо ль либо П2 (или оба) больше или равны Предположим без потери общности, что ni > Кроме того, поскольку пг > 0 (почему?), имеем П1 < п, так что можем применить индуктивную гипотезу. Обозначив через высота (Т) длину самого длинного пути в двоичном дереве Т, будем иметь
высота (Г) = 1 + шах ( высота (Ti), высота (Тг)} > 1 + высота (Ti) 5 1 + log2 «1
5 1+ logz 5 = log п
Применим этот результат к нашему обсуждению нижних границ. Любой алгоритм для решения задачи ПОИСК соответствует (при размере входа п) дереву решения, содержащему по крайней мере л внешних узлов. Из теоремы 1 следует, что дерево решения будет иметь высоту не менее, чем logg л. Следовательно, алгоритм выполняет по крайней мере logs п проб для некоторого входа с размером п. Поскольку это соблюдается для всех возможных алго ритмов решения задачи ПОИСК, то ilflog п) будет нижней границей для это^; задачи. И конечно, двоичный поиск будет оптимальным решением.
Редукция	» -
При другом способе определения нижней границы для некоторой задаче, используется известная нижняя граница для некоторой другой задачи Идея заключается в том, чтобы показать, что задача Рв м°**в**
Решена не быстрее, чем задача Рд и поэтому нижнюю границу для Рд
Главе 2
, показать такое соотношение между двумя можно отмести и к ?в- ктиш1Ый алгоритм А для решения задачи задачами, выберем такой W	раз вызывает процедуру,
Рл, который не более, чем п	* называть редукцией от Рл к Рв.
шлющую задачу Рв-	качестве примера рассмотрим сначала
Перед Форм^иза^й	^еи в ю
задачу УНИКАЛЬНОСТЬ •	„ п чисел решить, будут ли одшщ.
Для заданного неупорядоченного наоора
’‘° ПоХ'	реДУК-и» ” ““™ УНИКАЛЬНОСТЬ ЭЛЕМЕНТА
К ^Гзы®п™Р№>КпоР"«оче«иого набора » чисел перестроить числа в не. >6 Дробей*1 задач будем предполагать, что модель вычислении основана на сравнении-, единственной операцией, которую мы будем учитывать, яв-ляетсп операция сравнения, в которой два числа сравниваются за постоянное время (за один шаг).	___
Простейшая редукция А от задачи УНИКАЛЬНОСТЬ ЭЛЕМЕНТА к задаче СОРТИРОВКА работает следующим образом: для определения, будет ли некоторое число появляться в массиве из п чисел более одного раза, сначала отсортируем массив и затем пошагово будем просматривать его, сравнивая все пары последовательных чисел. Число считается появляющимся в исходном массиве дважды, если и только если оно обнаруживается в соседних позициях только что отсортированного массива.
Существовппие такой редукции показывает, что известная нижняя граница Q(n log я) для задачи УНИКАЛЬНОСТЬ ЭЛЕМЕНТА относится и к задаче СОРТИРОВКА. Предположим, что должен существовать некоторый алгоритм В для задачи СОРТИРОВКА с временем работы о(л log п). Тогда редукция А решила бы задачу УНИКАЛЬНОСТЬ ЭЛЕМЕНТА за время о(л log л), если бы в ней для сортировки применялся алгоритм В и тогда на сортировку потребовалось бы время о(л log я) в на проверку соседних чисел в только 'сто отсортированном массиве потребовалось бы время О(п) с о(п log л). Но это VMin?AnLuTTtD-nn’U0* поскольку нижняя граница П(л log л) для задачи ^КАЛ^Н°СТЬ ’ЗЛЕМЕНТА предполагает, что невозможно существование какого-либо решения с временем выполнения о(п log л)
Теперь формализуем эти пдеи.
Алгоритм А для задачи Pa Gvtpt тип	л
л "Ягвремсннои редукцией от Рд к Рв, если
- а а..Де
2» А выполняется за ноемя ОГт/мп
к В составляет один шаг ’ ркЧем стои*ость каждого обращения >*гч1**« txtQj %
Б результате мы заменим
постоянное число раз обращение к	ДЛЯ задачи рА на некоторое
дачи Pg. Мы вполне можем	Р Щ,н °’ 470 пРн°ОД»т к решению за-
мулировки решения задачи Ря fMnJ ОПеРпцн1о В без конкретной фор-нашем случае задачей Рл ямяетея Ы??тг?,Н1КС и “с :,ная его совсем). В дачей Рв - задача СОРТИРОВКА УИИКлльНОСТЬ ЭЛЕМЕНТА, эо-иейную по времени) редукцию ггг реАСтавляет собой л-кратиую (или ли-
• ^х^хихьность к ’°-
izirunKA.	’-'гнческим алгоритмом для задачи
LJKiil
диллмэ Алгоритмов
п
Предположим, что для задачи РА известия пижняя граница Q(f (п)) х что *ы можем составить г(л)-кратную по времени редукцию А от Рд к Рв-Тогда» предполагая, что цл) G о(/ (л)), задача Pg также должна иметь нижнюю границу / (и)), ели бы существовал некоторый алгоритм В для Рв, который выполняется за время о(/ (л)), то алгоритм А также выполнял, ся бы за время o(f (л)), если бы он использовал алгоритм В и тогда бы А не соответствовал бы нижней границе для задачи Рд. Этот аргумент зависит от предположения, что 1(л) G off (л)). Если бы это предположение не было удовлетворено, то алгоритм А выполнялся бы за время Q.(f (л)), независимо от свойств алгоритма В, и известная нижняя граница для Рд была бы нарушена. В этом и заключается причина, по которой редукция должна быть эффективной, чтобы быть полезной (см. рис. 2.3).
Эффективная редукция от задачи Рд к задаче Рв также может быть использована для переадресации известной нижней границы для Рд к задаче рд. Вернемся к нашему примеру. Задача СОРТИРОВКА имеет верхнюю границу О(п log п). Сортировка слиянием является единственным алгоритмом, который решает задачу за время О(п log л). Основываясь на любом алгоритме сортировки, выполняемом за время О(п log л), редукция А приводит к решению задачи УНИКАЛЬНОСТЬ ЭЛЕМЕНТА за время О(п log л). Следовательно, задача УНИКАЛЬНОСТЬ ЭЛЕМЕНТА наследует верхнюю границу 0(д log л) для задачи СОРТИРОВКА.
В общем случае предположим, что задача Рв имеет известную верхнюю границу и мы можем предложить O(g (л)), редукцию по времени от задачи Ра к задаче Рв< Тогда, полагая т(л) е O(g (л)), задача РА должна иметь такую же верхнюю границу O(g (л)).
Верхняя граница
Нижняя граница
2.3. т(п Хграгная по времени редуэдия от задачи Рд к задаче Рв
2.6.	Замечания по главе
В книгах [2, 20, 56, 73, 83, 89, 90] излагаются вопросы анализа алгоритмов и анализируются приведенные в них алгоритмы. Можно также рекомендовать статью [82], в которой описывается взвешенный анализ и пред-
ставлено несколько примеров.
Кнут использовал О-нотацию и асимптотический анализ алгоритм
“ 1973 году [481 >. ааел разновидности О-. Я-. 9- » « нотаций тр"
" 119|. В обоих источниках описывается история асимптотической котацип.
Глава}
40
Etunvxitxfl определена и описана в [57], а основы для раз.
Матемагическая ивд^«ия^ Р Д	жмы * [И]
работки алгоритмов и их авали
2.7.	Упражнения
1.	Примените математическую индукцию для доказательства X "= 1 ' =
2.	Примените математическую индукцию, чтобы показать, что двоичное дерево с п внутренними узлами должно иметь п + 1 внешних узлов (см. раздел 3.5 для получения более подробных сведении о двоичных деревьях).
3.	Обсудите преимущества и недостатки оценки времени работы программы в единицах времени при ее выполнении в компьютере. Является ли такая мера надежной характеристикой?
4.	Исследуйте функции sequentialSearch и binarysearch и постройте графики зависимости времени их выполнения от размера ввода. Поскольку обе программы имеют параметры наихудшего случая при неудачном поиске, то вам вероятно захочется использовать ключи поиска, не встречающиеся во входном массиве. Подтверждают ли ваши графики времени выполнения в наихудшем случае О(п) для последовательного поиска O(log п) для двоичного поиска?
5.	Исследуйте алгоритмы сортировки insertionSort (выполняемый за время О(п )), selectionsort (выполняемый за время Ofn2)) и mergeSort (выполняемый за время О(л log п)) и постройте графики зависимости времени их выполнения от размера ввода. Подтверждают ли ваши графики их времена выполнения? (Эти программы обсуждаются в начале глав 5, б и 8.)
6.	Решите рекуррентное уравнение
Т’(п) = • (п ~ 1) + а если л > О в противном случае (л = 0) для констант а и Ь.
Решите рекуррентное уравнение
Лл) =
Г(|) + в ь
для констант а и Ь.
если л > 1
в противном случае (л = 1)
8. Решите рекуррентное уравнение
9.
10.
Т(л) =. 2 Т (и - 2) + а
Для констант а и Ь.
Покажите, что е qi„ /_п	,
O(g(n)), если (Л+/21(л) = /(/)2	пРИв°Дят к (/1 +/г) (л) 6
Покажите, что f\ £ q/	/2(п).
G ' B2> l-n»> если (/i . (дД	приводят к (f\ • /2) (л)
если л > 1
ь противном случае (л = 0 или л = 1)
/2(Л).
дндаиз алгоритмов
11.	Покажите, что f(n\ е O(afn\\ -	<
О(Л(п)).	е °(Л(л)) приводят к /(п) G
12.	Покажите, что log» я е 0(logc п) ыя всех b с > j
13.	Покажите, что атпт + а» ш"'1 и. .	„
„акт а,.	+ "+ °" 6 О(п") для любых кои-
14.	Покажите, что log п е О(л») для всех k > 0.
15.	Покажите, что п* в 0(2") для всех * > п
16.	Покажите, что Дп) S O(g(n)) приводит к Яя) £ Одл)).
17.	Покажите с помощью деревьев dpiupumh wm
н	м н св решения, что сортировка на основе ппр-
OM loe пНпТг <ЭаЛаЧа С0Р™Р0ВКА) имеет’ иижиюю граищу fl(nlogn)5 П	М0ЖН0 исполмов»ть СВОЙСТВО log(nl) £
18.	Покажите с помощью деревьев решения, что будет определена нижняя граница Q(log п) решения задачи ПОИСК для версии "да/нет": для заданного отсортированного массива а из п различных целых чисел и ключа поиска х, определить, имеется ли в массиве а число х. (Под-
сказка: хотя возможны только два варианта ответа, утверждается, что дерево решения должно содержать по крайней мере п внешних узлов.)

Структуры данных
Vтруктуры данных яаляютея главными строительными блоками, из кото-рых формируются алгоритмы. Структуры данных (т. е. способы описания упорядоченных данных), используемые алгоритмом, влияют иа эффективность алгоритма, а также на простоту его понимания и программной реализации. Поэтому очень важно изучить особенности структур данных.
Язык программирования C++ имеет ряд предопределенных структур данных. В качестве примера можно назвать целые числа вместе с операциями по их обработке: арифметические операторы, например, сложение и умножение, операторы отношений, операторы присвоения и т. д. В качестве других примеров можно назвать числа с плавающей точкой, символы, типы указателей, типы ссылок. Предопределенные структуры могут быть использованы в программах в »чпстом виде» или комбинированно через такие средства, как классы и массивы для образовании структур данных более высокой сложности.
В этой главе мы представим и будем использовать такие структуры данных: списки, стеки и деревья двоичного поиска. Эти структуры данных хотя и элементарны, но достаточно мощные, а их реализация является стандартно и пррктичной. Мы не будем пытаться давать в этой главе сложной трактовки структур данных нашей целью в данном случае является подготовка применению структур данных, необходимых для последующих программ.
3.1.	Что такое структуры данных?
модяфи^апт.Р» Памяти для хранения данных, состоит mi ’ достула к Данным. Более фор-И3 ~ледую1иих трех компонентов:
Уляции над специфическими типами аб-
которок хранятся абстрактные объекты-
□ операций в терминах структуры памяти, компонента определения — набор операций вид обстраКт11ЫМ1’ — называется абстрактным
л— '»*%•***•“-'
Структура данных состоит из способов ее формирования, мально структура данных
1.	Набор операций для страктных объектов.
2.	Структура памяти, в
3.	Реализация каждой из
Первая компонента тоеть^гомпонеиты пм™	типом данных (ЛТД). Вторая и
мер, массив АТД обеспечивав’CTpyKTyp,d A°«”wx- НйПр”0 и того же типа посредством операций ”ПД ”яборпм значений одно'
Р и зй доступа и моднфикпцни лночснни.
задаваемых с помощью индекса. Реализация, используемая в языке C++, хранит массив в непрерывном блоке помята (структура памяти) я использует арифметику указателей для преобразования индекса в адрес внутри блока (реализация операций).
Тип абстрактных данных определяет, что делает структура данных — какие операции она поддерживает — не раскрывая, как опи выполняются. Поэтому при составлении программы программист может думать о необходимых для программы типах абстрактных данпых. а работу по их реализации отложить па некоторое время. 1акая практика программирования, заключающаяся в разделении алгоритма и его структуры данных так, что они могут рассматриваться раздельно, называется абстракцией данных. Абстракция данных предполагает различные уровни абстрактного представления. Так в целочисленной арифметике абстрактный тип данных, предусмотренный в большинстве языков программирования (включал C++), позволяет нам размышлять на уровне операций сложения, умножения и сравнения целых чисел без необходимости рассмотрения как эти целые числа представлены, как они складываются или умножаются и т. д. АТД стека позволяет думать на уровне организации стека, откладывая вопрос (более низкого уровня) о применении стека.
Использование абстрактного типа данных приводит к применению модульного программирования, т. е. к практике разбиения программы во отдельные модули с хорошо проработанным интерфейсом. Модульность имеет целый ряд преимуществ. Модули могут быть написаны и отлажены отдельно от остальной программы, другим программистом и в другое время. Модуля допускают многократное применение, так что они могут быть загружены из библиотеки или скопированы из другой программы. Кроме того модуль может быть заменен другим модулем, который функционально эквивалентен, но более эффективен, надежен или, по каким-либо другим соображениям, просто лучше исходного.
Кроме этих преимуществ, не каждая структура данных должна рассматриваться как абстрактный тип данных. АТД массив обеспечивает индексный способ доступа и модификации данных для манипуляций с набором данных, но часто в такой абстрактной трактовке массива нет никаких преимуществ. В этих случаях нарушается принцип использования для массива непрерывного блока памяти п вызывает часто излишний уровень абстракции. Это и неэффективно, поскольку дополнительный вызов функции может привести к значительному удорожанию и нельзя использовать эффективный способ применения указателей, который основан на жесткой связи между указателем и массивом.	_____ _____
Первой нашей задачей в этой главе будет представить абстрактный тип данных, используемых алгоритмами в этой книге и эффективно их применить. Знакомство с этими АТД позволит нам описывать наши алгоритмы на высоком, более абстрактном уровне, в терминах абстрактных операций. Знакомство с вопросами применения АТД — которые интересны сами по себе позволит нам применить алгоритмы, использующие их. Кроме того, зная, как при меняются АТД, мы сможем применить структуры данных и в тех случаях, когда они лишь отдаленно соответствуют нашим нуждам.
3.2.	Связанные списки
«mnnfain массивам, используются для манипуляций
Связанные	элементы хранятся в виде узлов связанных спис-
со списками элементе.	ьн0 в вяде списка. Каждый узел обладает
ХаУтелОемЯОвае“Ые^уюИий У™ С™ст° иозываемЫМ ««лкой). Последив» узе^отличается специальным значением в его поле ссылки (нулевая ссылка на рис. 3.1) или ссылкой на самого себя.
При обработке списков связанные списки имеют два преимущества перед массивами. Во-первых, изменение порядка элементов в связанном списке выполняется очень просто и быстро, обычно реализуется путем редактиро-вания небольшого числа ссылок (например, для включения нового узла b после некоторого узла а устанавливается ссылка от а к Ь, и ссылка от b на тот элемент, на который только что была ссылка от а). Для сравнения выполняемых действий внесем один элемент в массив а, представляющий список п элементов. Для внесения элемента в позицию i каждый элемент от а (i ] до а[п-1] должен быть сдвинут на один элемент вправо, чтобы образовать «дырку» для нового элемента в позиции i.
В отличие от массивов связанные списки динамичны — они могут сжиматься и расти в течение их жизни. При работе со связанными списками нет необходимости следить за переполнением отведенного участка памяти.
Существует несколько видов связанных списков. Каждый узел для односвязных списков имеет ссылку на следующий узел, его последователя. Каждый узел двухсвязных списков имеет ссылку как на следующий, так и на предшествующий узел, его предшественника. В кольцевых связанных списках последний узел связан с первым узлом, если кольцевой список является двухсвязным, то первый узел также имеет ссылку на последний. Например, на рис. 3.1 показан простой односвязный список, на рис. 3.2 — кольцевой BBvxcnniwwp (СПИС0К’ Здесь в основном будут рассматриваться кольцевые ным в этой книгой ПОСКОЛЬКу они лучше подходят к алгоритмам, описан-списками. Мы Краткостп мы их бУдем называть просто связанными угольников, гдеУкаждый°узелВаТЬ например’ для представления много-многоугольника а v-ии ’ В связанном списке соответствует вершине ника.	’ упорядочены в порядке обхода вершин многоуголь-
В реализации связанных списков л*»
точек, узел является объектом кЛ ’ ,ор,’ептирова»ных на представление класса »Ioqg*
Структуры данных
41
class Node (
protected:
Node *_next;	/ / связь к последующему узлу
Node * prev;	// связь к предшествующему узлу
public:
Node (void);
virtual -Node (void);
Node ♦next(void);
Node *prev(void);
Node ‘insert(Node*);
Node *remove(void);
void splice(Node*); );
Конструктор класса Node формирует новый узел, который имеет двойную ссылку на самого себя — новый узел представляет собой одноузловой связанный список.
Node::Node(void) :
next(this), _prev(this) (
1
Внутри компонентной функции переменная this автоматически указывает на принимающий объект, объект, для которого инициируется компонентная функция. В конструкторе переменная this указывает на объект, который подлежит внесению. Соответственно при обсуждении компонентных функций мы будем ссылаться на принимающий объект как на текущий объект (объект с именем this). Деструктор -Node отвечает за удаление этого объекта. При описании класса он объявлен виртуальным (virtual), так что удаляемый объект (объект класса, полученного из класса Node) будет удален корректно:	• .

Node: : -Node (void)
•г?

Компонентные функции next и prev используются для перемещай^.. из этого узла к последователю или предшественнику:
Node *Node::next(void)
return next;
Node *Node::prev(void)
। return _prev;
Компонентная функция insert включает узел Ь сразу же уалн (рис. 3.3):
Рис. 3.3. Включение узла в связанный список
Node ♦Node::insert(Node *b) {
Node *c = _next;
b->_next = c;
b->_prev = this;
next = b;
c->_prev = b;
return b;
)
Узел можно удалить из списка. Компонентная функция remove удаляет текущий узел из данного связанного списка (рис. 3.4). При этом возвращается указатель на текущий узел, так что впоследствии он может быть исключен:
Node *Node::remove(void) {
_prev->_next = _next; _next->_prev = jprev; _next = _prev = this; return this;
)
Компонентная а и b. Операция принадлежности
ункция splice используется для соединения двух узлов соединения приводит к разным результатам, зависящим от двух узлов к одному и тому же списку. Если принад
Рис. 3.4. Удаление узла из связанного списка
Ь
Структуры данных
лежат, то связанный список разделяется на два меньших списка. И наоборот, если узлы а и b принадлежат разным спискам, то оба списка обведи-няются в один более крупный список.
Для слияния двух узлов а и b мы устанавливаем ссылку из а на Ь-> next (на последователя узла Ь) и из b на a->_next (на последователя узла а). Также необходимо изменить и соответствующие ссылки на предшественника. Для этого для нового последователя узла а устанавливаем обратную ссылку на а, а для нового последователя узла Ь — обратную ссылку на Ь. Эта операция схематически отображена на рис. 3.5. Из рисунка очевидно, что операция соединения splice взаимно обратна; соединение узлов а и Ь на левой схеме приводит к образованию правой схемы, а повторное соединение узлов а и b (на правой схеме) приводит к левой схеме.
В следующей реализации компонентной функции splice текущий узел играет роль узла айв качестве аргумента функции указывается только узел Ь. Переменная а введена в программу исключительно для отображения симметрии операции: для заданных указателей узлов а и Ь операции a->splice(b) и b->splice(a) дают одинаковый результат.
void Node::splice (Node *b) (
Node *a = this;
Node *an = a->_next;
Node *bn = b->_next; a->_next = bn; b->_next = an; an->_prev = b; bn->_prev = a;
I
ГЙ1К
Заметим, что в случаях, когда узел а предшествует узлу Ь в связанном списке, то операция слияния приводит к удалению узла Ь. Слияние одноузлового связанного списка Ь с узлом а некоторого другого связанного списка приводит к фактическому включению узла b после узла а. Это яв.:шнйв приводит к выводу, что операции включения узла в связанный
удаления узла из связанного списка фактически являются частными случаями операции слияния. Но это только замечание компонентные insert и remove предусмотрены для удобства.	.
И, наконец, заметим, что слияние узла с самим собой, нацрдЦ^р^ЧДу обращении a->splice (а), не приводит ни к каким изменениям, ртолегко проверить по реализации функции splice.
3.3.	Списки
В этом разделе для представления списков введен новый/^/списка сок - это упорядоченный набор конечного числа элементов. Д равна числу элементов, содержащихся в списке, спи зывается пустым списком.	___
При нашем подходе каждый элемент списка заним первый элемент и первой позиции, второй элем	оасполагается перед
того существует головная позиция, которая одновр	..
. . «мк lAPUPHioa Элементы в списке располагаются с 1 по 7 позицию. Голое-
“• окм'rao6f™ 8
рата, в данный момент находится в положении 2
первой и после последней позиции. Объект List обеспечивает доступ к его элементам через окно (window), которое в любой данный момент относится к некоторой позиции в списке. Большинство операции со списками выполняется либо с окном, либо с элементом в окне. Например, мы можем вызвать элемент, находящийся в окне, сдвинуть окно на следующую или предыдущую позицию или передвинуть его на первую или последнюю позицию. Мы также можем выполнить операцию удаления из списка элемента, находящегося в окне, или внести новый элемент в список сразу же после окна. На рис. 3.6
отражен наш подход к организации списка.
Для реализации класса List будем использовать связанные списки. Каждый узел соответствует позиции в списке и содержит элемент, сохраняемый в этой позиции (узел, соответствующий головной позиции, называется головной узел). Узел представляется объектом класса ListNode, который получается из класса Node. Класс Li st Node обладает компонентом данных _val, который указывает на фактический элемент.
Список является коллекцией элементов некоторого заданного типа. Однако нет необходимости встраивать специфический тип элемента в описание класса _^stNode или List, поскольку операции над списками действуют совершенно независимо от типа элемента. По этой причине мы определим эти классы как шаблоны класса. Тип элемента задается в виде которого спетпРииплСЛеДСТВИП’ когда вам потребуется список элементов не-типом этементя Г° типа’ мы обратимся к шаблону класса с заданным фактического ХаТ^^м^Г ~3°°аН С°ЗДаШ“
Шаблон класса ListHode определяется следующим образом:
: public Node (
параметра
public:
Т _val;
LiatNodefT val) ;
Здесь через Т обозначен папамет , ализации Listllode вместо папамртт |^ля °^ъявлсиия конкретной Ре’ типа- апример, объявление 8 необходимо подставить название
Liat Noda <int *>
определяет а и b как объекты ListNode	_
X на_целое (pointer_to_int). ’	“ нвх “«РЖ» указа-
Конструктор ListNode может быть определен подобно
teinplate<class Т> ListNode::ListNode (Т val) • _val(val)
< )
Конструктор ListNode неявно инициирует конструктор для базового класса Node, поскольку последний конструктор не имеет аргументов.
Мы не определяем здесь деструктора класса ListNode. Как только удаляется объект ListNode, автоматически инициируется деструктор базового класса Node:: -Node, объявленный как виртуальный. Заметим, что элемент, указанный элементом данных ListNode: :_val не разрушается. Было бы безопасно разрушить элемент только в том случае, если бы было известно, что он создан как новый (new), но нет никакой гарантии такой ситуации.
Вернемся к определению шаблона класса List. Класс List содержит три компонента данных: header указывает на головной узел, соответствующий головной позиции; win указывает на узел, который находится в позиции текущего окна; параметр length содержит длину списка. Шаблон класса определяется следующим образом:
template<class Т> class List { private:
ListNode<T> *header;
Lis tNode<T> * win ;
int _length;
public:
List (void) ;
-List (void) ;
T insert(T);
T append (T) ;
List «append(List *) ;
T prepend(T);
T remove(void);
void val(T);
T val(void);
T next(void);
T prev(void);
T first(void);
T last(void); int length(void); bool isFirst(void); bool isLast(void); bool isHoad(void);
I:
•Д'ОЧ H i
упрощения нашей реализации будем предполагать, что объекты данного типа. Тогда с
Для
списка являются указатели на
tiat<Polygon •> p;
'	 „ s»
-So, __________________________________________________________________________________________________________________________ глава з
определяет, что p будет списком указателей_на_полигоны (Polygons), тогда как объявление
List<Polygon> q;
будет ошибочным.
3.3.1. Конструкторы и деструкторы
Конструктор List создает и инициализирует пустой список, представленный одним головным узлом, со ссылками на самого себя:
template<class Т> List<T>::List(void) :
_length(0) (
header = new ListNode<T> (NULL);
win = header;
Деструктор класса разрушает узлы связанного списка:
template<class Т> List<T>::~List (void) (
while (length () > 0) (
first () ;
remove () ;
)
delete header;
)
Заметим, что деструктор не разрушает сами элементы данных.
3.3.2. Изменение списков
Компонентные функции insert, г--включения в список новых элементов Ни изменяет позиции окна. Функция insert -и возвращает указатель на новый элемент-
prepend и append используются для J одна из этих трех функций не заносит новый элемент после окна
insert(T val)
Компонентные функции preoend , начало или н конец списка 'UU",,
в ।----
новый элемент:
вставляют новый элемент возвращают указатель на
hwd.r->in..rt (naw
Prepend(T val)
Структуры данных
return val; )
template<class T> T List<T>:: append (T val) ( „ .
header->prev() ->insert (new ListNode<T> (val) ) •
++_length;
return val;
)	II
I
Вторая версия компонентной функции append используется для добавления списка 1 в конец текущего списка — первый элемент списка 1 по- I мещается после последнего элемента текущего списка. В процессе выполнения список 1 становится пустым. Возвращается указатель на текущий список:
template<class Т> List<T>* List<T>: :append(List<T> *1) (
ListNode<T> *a = (ListNode<T>*) header->prev () ;
a->splice(l->header);
length += l-> _length;
l->header->remove();
l->_length = 0;
l->win = header;
return this;
}

Компонентная функция remove удаляет элемент, находящийся в окне, перемещает окно в предыдущую позицию и возвращает указатель на только что удаленный элемент. Если окно находится в головной позиции, то таких действий не производится:
template<class T> T List<T>:: remove (void)
if (win = header) return NULL;

win = (ListNode<T>*) win->prev () ;
delete (ListNode<T>*) win->next()->remove() ;
——length;
return val;
я я* ш
В 6 ".



головной
Если вызывается компонентная функция val с мента V, то происходит замена элемента, находящегося элемент v. Если окно находится в головной позиции, не производится.

void List<T>::val (Т v)
if (win •= header)
и
13 3 Доступ к элементам списка
3.3.3. Досту!	дикции val происходит без аргумея.
Если обращение к «“Х" находящийся в окне, или куль NOLL, если то», то возвращается элемент, в^ окно располагается
tM,pl.t.<01aaa т> т Li.t<T>: :val (void)
return win->_val;
1	и orev перемещают окно в следующую или
Компонентные функции next н	3 них возвращает указатель
предыдущую позиции	заметим, что класс List
на элемент, расположенный в	Например, если окно находится в
Обеспечивает операцию	операции next переместит окно в го-
лХГпоз“щшо.”еще одно обращение к next переместит окно в первую позицию.
template<class Т> Т bist<T>::next(void)
1 win = (ListNode<T>*) win->next();
return win->_yal;
)
template<class T> T List<T>::prev(void) (
win = (ListNode<T>*) win->prev(); return win-> val;
)
Компонентные функции first и last переносят окно в леднюю позиции соответственно, они не производят никаких лист пустой. Каждая из этих функций записанный в новом положении окна: tentplate<class Т> Т List<T>: :first(voi <
win = (ListNode<T>*) header->next(); return win->_val; 1
первую и пос-действий, если возвращает указатель на элемент,
Компонентная функция l^nnth
- ч-«• возвращает значение длины списка.
Ъ-pl.t^cl... Т> int bl.t<T>:..l.„9th(vold|
return _length;
a
Компонентные функции isFirst, isLast и isHead возвращают знаке вне TRUE (истина) только в случаях, если окно находится а Первой not ледней или головной позициях соответственно.	’
templateCclass Т> bool ListCT>: : isFirst (void)
* return ( (win == header->next() ) (_length >0) ); )
templateCclass T> bool ListCT>: : isLast(void)
return ( (win = header->prev() ) fifi (_length >0) );
I
templateCclass T> bool ListCT>::isHead(void)
( return (win == header);
I
3.3.4. Примеры списков «к
Два простых примера иллюстрируют использование списков. В первом примере шаблон функции arrayToList загружает п элементов массива а в список и возвращает указатель на этот список:
templateCclass Т> List<T> *arrayToList(Т а[], int п)	Л >
(
ListCT> *s = new ListCT>; for (int i = 0; i < n; i++) s->append(a[i]);
return s; )
int(*cap)
как
по-
Например, если а представляет собой массив строк, то мент программы преобразует массив а в список строк s
char *a[20];

11 здесь должен быть определен массив а
Шаблон функции arrayToList мы будем впослвдрНЙИ Утилиту в некоторых программах.
В качестве второго примера рассмотрим шаблон ‘ торая возвращает наименьший элемент в списке s. в&ются с помощью функции стр, в результате ра ются значения -1, 0 или 1, если первый аргумент Рапец Мли больше второго аргумента:
, .. .< л
••ч -“К •
меньше.
int i;
if (a.length() = 0)
return NULL;
T v = a.first() ;
for (s.next(); Is.isHeadO;
if (cmp (s.val(), v> < 0) v = s.val() ;
s.nextO
return v; )
списков строк появляется раньше в алфавит-
Для определения, какой я *	leastitem с функцией срал-
ном порядке, следует обр 6в6лиотечяая функция языка C++, которая нения st гетр, «это стандар -ия«РИяя -1, 0 или 1 в зависимости от при обработке двух стр0КтВ“Дменьше| раВна или больше второй строки в того, будет ли перваяi р _ результате работы следующего фрагмента алфавитном порядке. Например, в резул^ н программы будет напечатана строка ant.
List<char*> s;
s.append("bat");
s.append("ant") ;
s.append("cat") ;
cout « leastitem(s, stremp);
3.4.	Стеки
ствуют также и такие списковые структуры дГнны °ГраНИЧИВается- Но суще‘ ментам ограничен, наиболее важная ™ Д ’ в КОТОРЫХ Доступ к эле-стек (stack). В стеке доступ ограничен m Стек™ыи список, пит просто в него самым последним. По этой причине ои° К ЭЛементу’ КОТОРЫЙ внесен по принципу * послед ним поите,	°Н имеет также название списка
Основными операд„я.ч„Р““ СТека "/“ШгЛ‘ <СШ,СКИ ™па L,F0^
(push) и выталкивания (роо) Пп- ЛЯ1°тся операции проталкивания элемент заносится в стек, а опеоаппа ВЫполнении операции проталкивания мент, записанный в него самым пппо ВЫТалкивания выбирает из стека эле-как укладывание элементов одного^ М’ ото можно представить доступ только к самому верхнему эл₽мД др^м 'аким образом, что имеется вычыМЫЙ В^₽Х’ 8 Операция pop удаляет^ Опорация Г’1"П заносит элемент »1гп^ТЩеЙ программе- Из операций г Верхний элемент и возвращает его КОТ0₽ал етТяЛ0 СТеК0М мож"° назвать операция, (см“Г7)е °Пе₽аЦ"“ “-о»»	(ИСТИ‘,а>- ПУСТ'
а.{).	‘’лементов из стека без их удаления
Рис. 3.7. Стег.  прсчесое рйоты
Самая простейшая реализация стека использует массив. Отек из п элементов хранится в элементах некоторого массива s от s СО] до з[п-1], при этом число элементов (п) записывается в некоторой целочисленной переменной top- Элемент массива s[0] содержит самый нижний элемент стека, а s[top-U самый верхний элемент. Список элементов растет до самого верхнего индекса по мере выполнения операций проталкивания (push) и уменьшается при выполнении операций выталкивания. Практически для записи элемента х в стек выполняется оператор s[top++]ex, а для выталкивания возвращается значение s[—top). Единственной проблемой при такой реализации остается то, что она нединамична — длина массива ограничивает размер стека. Далее мы рассмотрим реализацию стека на основе шаблона класса List из предыдущего раздела. Тогда наши стеки будут динамичными, поскольку объекты класса List динамичны.
Шаблон класса Stack содержит собственный элемент данных s, который указывает на объект List, представляющий собой стек. Список упорядочен с верха стека до его низа, в частности верхний элемент стека находится в первой позиции списка, а нижний элемент — в последней позиции списка.
template<class T> class Stack {
List<T> *s;
public:
Stack(void) ;
-Stack(void); void push(T v);
T pop(void); bool empty(void); int size(void);
top(void); nextToTop(void); bottom(void);
Т т т
Реализация компонентных функций очевидна. Конструктор Stack ООВД»-
ет объект List и приписывает его элементу дан:
template<class T> Stack<T>::Stack (void) : s (new List<T>)
:• чгз’
Деструктор -Stack удаляет объект List, на который указывает элемент Данных s:
tamplate<class Т> Stack<T>: ;-Staek (void)
delete s
Компонентная функция push протал -екущего стека п компонентная функция pop выбирает	, ,
’'Демент и позвращаст его:
г -
54
t»pl.t.<ol..s T> void Stack<T>::pu’b(T v) (
s->prepend(v);
I
template<class T> I Stack<T>::pop(void) (
s->first() ;
return s->remove();
1
Компонентная функция empty возвращает значение TRUE (истина) в том случае, когда текущий стек пустой, а компонентная функция size возвращает число элементов в текущем стеке:
tenplate<class Т> bool Stack<T>: : empty (void) (
return (a->length() = 0);
)
template<class T> int Stack<T>::size(void) (
return s->length();
I
Полезность операции «вызова» будет очевидна из следующих глав. Функция top возвращает самый верхний элемент в стеке, функция nextToTop возвращает элемент, лежащий непосредственно под самым верхним, а функция bottom возвращает самый нижний элемент. Ни одна из этих функций вызова не изменяет состояния стека (ничто не заносится и не выбирается).
teaplateCclass Т> Т Stack: .top(void) return 8->firat();
)
T> T Stack:: nextToTop (void)
~>first() ; return s->next();
t-pX.t^c!„. T> T Steek: :bottoe(vold)
return 8->lait();
На простейшем примере исппг
следующей функции можно изменив	’П0Кажсм- как с помощью
void reverse(char ♦.[] int }	СЛ'’Д0ПППИЯ Л МЛССИВС a:
Яtack<char♦> •;
tor (int i - 0; 1 <
гтоукгурь1 данных
s.push(a[i]); for (i = i < n;
a[i] = s.popO ;
Класс Stack является хорошим примером абстрактного типа данных Класс обеспечивает оощедоступный интерфейс, состоящий из возможных операций push, pop, empty, конструктора и деструктора класса п one-раций вызова. Стеком можно манипулировать только через этот общий интерфейс. Структура памяти (список) скрыта в собственной части определения класса и не может быть изменена или доступна иным путем, кроме как через интерфейс. Программам, использующим стеки, совсем не нужно ничего знать о реализации стека. Например, функция reverse будет работать соверхенно одинаково, независимо от того, будет ли стек реализован на основе списка или массива.
Заметим, что мы реализовали АТД стека в терминах АТД списка. Такая реализация гораздо проще, чем реализация, основанная непосредственно на таких программных блоках низкого уровня, как связанные списки или массивы. Опасность реализации АТД в терминах другого АТД заключается в том, что первый АТД наследует характеристики второго, реализация которого может быть неэффективной. Но в данном случае, применяя свою собственную реализацию АТД списка, мы хорошо представляем ее свойства и поэтому можем легко показать, что каждая из операций, поддерживаемая классом Stack, выполняется за постоянное время.
3.5.	Двоичные деревья поиска
3.5.1.	Двоичные деревья
Хотя списки очень эффективны для включения и удаления элвмыид^ переустройства их порядка, они не эффективны для целей поиска, конкретного элемента х требует пошагового просмотра всего списка, тов на предмет их соответствия х. Поиск соответствия может xUhywMMffP обращения ко многим элементам, и если список не содержит заданного эле мента х, то и ко всем элементам списка. Даже если список то при поиске приходится обратиться к каждому элементу, преддхиутвуж»-
Щему искомому.	м	т
Двоичные деревья представляют более эффективны» спос	' ь
ичное дерево представляет собой структурированную колле лекция может быть пустой и в этом случае мы имеем пус ре«о. Если коллекция непуста, то она подразделяется ипА1 семейства узлов: корневой узел я. (или просто корень), зываемос левым поддеревом для п, и двоичное деР^в^’ . поддеревом для п. На рис. 3.8а узел, обозначенный буквой, новым, узел Л называется левым потомком / »	Являе
ПоДдеренл А, узел С называется правым потомком а  правого поддерева А.	внитпенних
Двоичное дерево на рис. 3.8и состоит из чет~*" а"""0„„ых	|>„г. кружками) и пяти аяешких (конешых, у
кор
лов (обо-
О о
Главаз
Рис. 3.8. Двоичное дерево с показанными внешними узллами (а) и без них (6)
чены квадратами). Размер двоичного дерева определяется числом содержащихся в нем внутренних узлов. Внешние узлы соответствуют пустым двоичным деревьям. Например, левый потомок узла В непустой (содержит узел D), тогда как правый потомок узла В — пустое дерево. В некоторых случаях внешние узлы обозначаются каким-либо образом, в других — на них совсем не ссылаются и они считаются пустыми двоичными деревьями (на рис. 3.86 внешние узлы не показаны).
Основанная на генеалогии метафора дает удобный способ обозначения узлов внутри двоичного дерева. Узел р является родителем (или предком) узла л, если л — потомок узла р. Два узла являются братьями, если они принадлежат одному и тому же родителю. Для двух заданных узлов Л1 и лк, таких, что узел Лк принадлежит поддереву с корнем в узле ль говорят, что узел лк является потомком узла ль а узел л| — предком узла пь Существует уникальный путь от узла щ вниз к каждому из потомков ль а именно, последовательность узлов п\ и Л2,... лк такая, что узел п{ является родителем узла /ц+i для i = 1, 2,..., k-1. Длина пути равна числу ребер (А 1), содержащихся в нем. Например, на рис. 3.8а уникальный путь от УЗЛ?Д2'МУ ° СОСТОИТ из последовательности А, В, D и имеет длину 2.
1 лубина узла п определяется рекурсивно:
О
1 + глубина (родителя (л))
Глубина узла равна длине уникального пути о, узел А имеет глубину 0, а узел D имеет гХ”
В«еолш узла я также определяется рекурсИВН0: i о . 1 + max (мювп (м„ (п))_ >ысота
глубина (п) =
высота (л)
если П — корневой узел и противном случае
от корня к узлу. На рис. 3.8а . j равную 2.
если П — внешний узел п лрстзппюм случае
где через лев(п) обозначен левый
потомок узла п. Высота узла л павл**^ УЗЛа П “ ЧсРсз прав(п) — правый п вниз до внешнего узла поддерева п. саыс,гп Длинного пути от узла -Ота ДВОИЧНОГО ДГ'рРИП ОПр»,Д<’ля<*ТСЯ
Гтоуктуры данных
S9
,х г——• —.. ....
templateCclass т> class TreeNode { protected:
TreeNode *_lchild;
TreeNode *_rchild;
T val;
public:
TreeNode(T) ;
virtual -TreeNode(void);
friend class SearchTree<T>;
friend class BraidedSearchTreeCT>;
) >
Элементы данных _lchild и _rchild обозначают связи текущего узла с левым и правым потомками соответственно, а элемент данных val содержит сам элемент.
Конструктор класса формирует двоичное дерево единичного размера — единственный внутренний узел имеет два пустых потомка, каждое из которых представлено нулем NULL :
templateCclass Т> TreeNode<T>: : TreeNode (Т v) : val (v) , _lchild(NULL), _rchild(NULL)
(
I
Деструктор -TreeNode рекурсивно удаляет оба потомка текущего узла (если они существуют) перед уничтожением самого текущего узла:
 . *
templateCclass Т> TreeNodeCT>: : -TreeNode (void)	..JR
if (_lchild) delete _lchild;	...	n,;
if (_rchild) delete _rchild;	... ,.t „

3.5.2.	Двоичные деревья поиска
Основное назначение двоичных деревьев заключается в повышениц^^фчИв. тивности поиска. При поиске выполняются такие операции, как нахождение заданного элемента из набора различных элементов, определение наибздД шего °ли наименьшего элемента в наборе, фиксация факта, что набор содержит заданный элемент. Для эффективного поиска внутри двоичного дерева его элементы должны быть организованы соответствующим образом. HiiQgtf&P. дво. t п’нсое дерево будет называться двоичным деревом поиска, вв^Г Расположены так, что для каждого элемента п все элементнСЖлВНК^^^^. Дереве п будут меньше, чем л, а все элементы в правом поддереву будут больше, чеМ л. цй рпс. 3.9 изображены три двоичных дерева доиска>_***’ из которых содержит один и тот же набор целочисленных элементов.
° • ’ ’ °
•й
в обшем случае существует огромное число двоичных деревьев поиска (раз-лиЗй формы) длл любого заданного набора элементов.
Предполагается, что элементы располагаются в линейном порядке и, следовательно, любые два элемента можно сравнить между собой. Примерами линейного порядка могут служить ряды целых или вещественных чисел в порядке возрастания, а также слов или строк символов, расположенных в лексикографическом (алфавитном, или словарном) порядке. Поиск осуществляется путем обращения к функции сравнения для сопоставления любых двух элементов относительно их линейного порядка. (Напомним о разделе 3.3, где была описана функция, которой передаются два элемента и которая возвращает значения -1, 0 или 1 в зависимости от того, будет ли первый элемент меньше, равен или больше второго соответственно). В нашей версии деревьев поиска действие функции сравнения ограничено только явно определенными объектами деревьев поиска.
Также очень полезны функции для обращения к элементам дерева поиска и воздействия на них. Такие функции обращения могут быть полезны для вывода на печать, редактирования и доступа к элементу или воздействия на него каким-либо иным образом. Функции обращения не принадлежат деревьям поиска, к элементам одного и того же дерева поиска могут быть применены различные функции обращения.
3.5.3.	Класс SearchTree (дерево поиска)
Определим шаблон нового класса SearchTree ноте дерева поиска. Класс содержит элемент «ии м7нГ “ К°РеПЬ ДВ0ИЧН0Г0 дерева поиска (объект мент данных стар, который указывает
для представления двоич-данных root, который ука-—’ класса TreeNode) и эле-на функцию сравнения.
TraaWoda ‘root;
МОаЛ	.
SearchTree (int(*) (T,T) );
-SearchTree(void);
int isEmpty(void);
T find (T) ;
T findMin(void) ;
void inorder(void(*)	(T) );
void insert(T);
void remove(T) ;
T removeMin(void) ;
Для упрощения реализации предположим, что элементами в дереве поиска являются указатели на объект заданного типа, когда шаблон класса SearchTree используется для создания действительного класса. Параметр 1 Т передается в виде типа указателя.	J
3.5.4.	Конструкторы и деструкторы
I i *
Конструктор SearchTree инициализирует элементы данных стр для функции сравнения и root для пустого дерева поиска:	-.iJ
• I) J
template<class T> SearchTree<T>: : SearchTree (int (*c) (T,T) ) :
anp(c) , root (NULL)	. •	?.
° a °	 J
Дерево поиска пусто только, если в элементе данных root содержится нуль (NULL) вместо разрешенного указателя:
template<class T> int SearchTree<T>::isEmpty (void)
return (root = NULL);
______- •, -згл*
Деструктор удаляет все дерево путем обращения к деструктору корня. teaplate<class Т> SearchTree<T>::-SearchTree (void)
с ле-
if (root) delete root;

3.5.5.	Поиск
•	 • J
Чтобы найти заданный элемент val, мы начинаем с корня ДУвм вдоль ломаной линии уникального путл вниздо УМИЧ ”аЬ В каждом узле п вдоль этого пути используем функцию Данного дерева на предмет сравнения val с элементом в п. Если val меньше, чем n->val, то поиск продолжается, аого потомка узла п, если val больше, чем n->val, то ”а’<иная с правого потомка п, в противном случае 1 '7(* I (и задача решена). Путь от корневого узла поиска для val.
еле-
для
Глава 3
_ояпи-и/ется в компонентной функции find, кото-
Этот алгоритм поиска реал у казатеЛь на элемент или NULL, если рая возвращает обнаруженный ею ук такой элемент не существует в дереве поиска.
т> Т SearchTree::find(T val)
TreeNode *n = root, while (n) (	•
int result = (♦cmp) (val, n->val), if (result < 0)
n = n->_lchild;
else if (result > 0) n = n->_rchild;
else return n->val;
) return NULL;
Этот алгоритм поиска можно сравнить с турниром, в котором участвуют некоторые кандидаты. В начале, когда мы начинаем с корня, в состав кандидатов входят все элементы в дереве поиска. В общем случае для каждого узла п в состав кандидатов входят все потомки п. На каждом этапе производится сравнение val с n->val, Если val меньше, чем n->val, то состав
кандидатов сужается до элементов, находящихся в левом поддереве, а элементы в правом поддереве п, как и сам элемент n->val, исключаются из соревнования. Аналогичным образом, если val больше, чем n->val, то состав кандидатов сужается до правого поддерева п. Процесс продолжается до тех пор, пока не будет обнаружен элемент val или не останется ни одного кандидата, что означает, что элемент val не существует в дереве поиска.
Для нахождения наименьшего элемента мы начинаем с корня и прослеживаем связи с левым потомком до тех пор, пока не достигнем узла п, левый потомок которого пуст — это означает, что в узле п содержится наименьший элемент. Этот процесс также можно уподобить турниру. Для каждого узла п состав кандидатов определяется потомками узла п. На каж-Z	_с°става каидпдатов удаляются те элементы, которые больше
нового П Ппопргг' И левыи потомок П будет теперь выступать в качестве котопый vi₽n продолжается до тех пор, пока не будет достигнут не-дидатов меньше,0чТмТ^^Гк "7°МК0М полагая- что не осталось кан-
Компонентная функция findMin ном дереве поиска, ции _findMin, ная с узла п :
и удет возвращено значение n->val.
—j возвращает наименьший элемент к компонентной алгоритм поиска,
в дан-функ-начи-
в ней происходит обращение которая реализует описанный ранее
Tre«Nod»<T> return (n ?
1 s««<a'Tr..<I>::flrtdHtn(void)
n - .findMxn (root) • n->val : HULL);
teaplat«<claas T>
TreeNoda<T> •S«*rchTree<T>
•: «FindMin

•n)
гтоуктуры данных
if (n = NULL) return NULL;
while (n—>_lchild) n = n->_lchild;
return n;
)
Наибольший элемент в дереве поиска может быть найден аналогично только отслеживаются связи с правым потомком вместо левого.
3.5.6. Симметричный обход
Обход двоичного дерева — это процесс, при котором каждый узел посещается точно только один раз. Компонентная функция inorder выполняет специальную форму обхода, известную как симметричный обход. Стратегия заключается сначала в симметричном обходе левого поддерева, затем посещения корня и потом в симметричном обходе правого поддерева. Узел посещается путем применения функции обращения к элементу, записанному
в узле.
Компонентная функция inorder служит в качестве ведущей функции. Она обращается к собственной компонентной функции _inorder, которая
выполняет симметричный обход от узла п и применяет функц
плз
каждому достигнутому узлу.
template<class Т> void SearchTree<T>:: inorder (void(‘visit) (T) ) (
_inorder (root, visit);
template<class T>
• - > 'Г.
void SearchTree: : inorder (TreeNode<T> ‘n, void (‘visit) I
if (n) (
_inorder(n->_lchild, visit);
(‘visit) (n->val);
—inorder(n->_rchild, visit);
(I)
-----------------1 поиска, пока-возрастающем порядке: 2, 3, 5,JT, -
его элементы посещаются в возрастающем п°Рядке* Чтобы выполнении симметричного обхода в некотором
из двоичных деревьев в i _	______
любого двоичного дерева поиска все
При симметричном обходе каждого данных на рис. 3.9, узлы посещаются 8. Конечно, при симметричном обходе Г“
370 так, заметим, что при
Узле п элементы меньше, чем n->val посещаются до п, иадлежат к левому поддереву п, а элементы больше, че“ Ютсн после п, поскольку они принадлежат правому п0 ^ т* льно, узел п посещается в правильной последовательн пРоизвольный узел, то это же правило соблюдается для кажоого Компонентная функция inorder обеспечивает т и Двоичного дерена поиска в отсортированном порядк
_ =^итгяе для строк, то эти строки можем напечатать ляется деревом поиск.олнХомавдой а. inorder (printstring). д * в лексикографическом Р	£ может быть определена как:
этого функция обращения printbirir у
void printstring(char *s) <
cout « 8 « "\n";
I
При симметричном обходе двоичного дерева узел, посещаемый после некоторого узла п, называется последователем узла п, а узел, посещаемый непосредственно перед п, называется предшественником узла п. Не существует никакого предшественника для первого посещаемого узла и никакого последователя для последнего посещаемого узла (в двоичном дереве поиска эти узлы содержат наименьший и наибольший элемент соответственно).
3.5.7. Включение элементов
Для включения нового элемента в двоичное дерево поиска вначале нужно определить его точное положение — а именно внешний узел, который должен быть заменен путем отслеживания пути поиска элемента, начиная с корня. Кроме сохранения указателя п на текущий узел мы будем хранить указатель р на предка узла п. Таким образом, когда п достигнет некоторого внешнего узла, р будет указывать на узел, который должен стать предком нового узла. Для осуществления включения узла мы создадим новый узел, содержащий новый элемент, и затем свяжем предок р с этим новым узлом (рис. 3.10),
Компонентная функция insert включает элемент дерево поиска:
val в это двоичное
templateCclass Т> void SearchTree<T>insert (Т val) if (root = NULL) {
root = new TreeNode<T>(val)• return;
} else (
int result;
TreeNode<T> *p, *n = roofe.
while (n) ( p = n; result = (*cmp) (val, p->val); if (result < 0)
n = p->_lchild;
else if (result > 0)
n = p->_rchild; else
return;
}
if (result < 0) p->_lchild = new TreeNode<T>(val) ;
else p->_rchild = new TreeNode<T> (val) ;
)
3.5.8. Удаление элементов
Удаление элемента из двоичного дерева поиска гораздо сложнее, чем включение, поскольку может быть значительно изменена форма дерева. Удаление узла, у которого есть не более чем один непустой потомок, является сравнительно простой задачей — устанавливается ссылка от предка узла на этот потомок. Однако ситуация становится гораздо сложнее, если у подлежащего удалению узла есть два непустых потомка: предок узла может быть связан с одним из потомков, а что делать с другим? Решение может заключаться не в удалении узла из дерева, а скорее в замене элемента, содержащегося в нем, на последователя этого элемента, а затем в удалении узла, содержащего этот последователь.
Чтобы удалить элемент из дерева поиска, вначале мы отслеживаем путь поиска элемента, начиная с корня и вниз до узла п, содержащего элемент. В этот момент могут возникнуть три ситуации, показанные на рпс. 3.11;	 • -	
1. Узел п имеет пустой, левый потомок. В этом случае ссылка на n (at писанная в предке п, если он есть) заменяется на ссылку на правого потомка п.
₽Ис'3,11 • Три ситуации, вотникающис при удалении цемента из двоичного дерева поиска 3 Звквп 2794
2 У вала п есть непустой левый потомок, но правый потомок nycnv_ В атом ^учае ссылка вниз на п заменяется ссылкой на левый noTOMl,
3. Уил п имеет два непустых потомка. Найдем последователя дад. (назовем его т), скопируем данные, хранящиеся в т, в узел п и зате* рекурсивно удалим узел т из дерева поиска.
Очень важно проследить, как будет выглядеть результирующее двоичв» дерево поиска в каждом случае. Рассмотрим случаи 1. Если подлежать удалению узел п, является левым потомком, то элементы, относящиеся ? правому поддереву п будут меньше, чем у узла р, предка узла п. При удд. лепии узла п его правое поддерево связывается с узлом р и элементы, хра нящиеся в новом левом поддереве узла р конечно остаются меньше элемент? в узле р. Поскольку никакие другие ссылки не изменяются, то дерево ос тается двоичным деревом поиска. Аргументы остаются подобными, есл? узел п является правым потомком, и они тривиальны, если п — корней? узел. Случай 2 объясняется аналогично. В случае 3 элемент и, записанный в узле п, перекрывается следующим большим элементом, хранящимся » узле m (назовем его iv), после чего элемент ш удаляется из дерева. В получающемся после этого двоичном дереве значения в левом поддереве узд? л будут меньше ш, поскольку они меньше о. Более того, элементы в право* поддереве узла п больше, чем ш, поскольку (1) они больше, чем и, (2) нет ни одного элемента двоичного дерева поиска, лежащего между пиши (3) из них элемент ш был удален.
Заметим, что в случае 3 узел m должен обязательно существовать, поскольку правое поддерево узла п непустое. Более того, рекурсивный вызов для удаления m не может привести к срыву рекурсивного вызова, поскольку хСДпЯУ«г» Ш Не И«еЛ ЛеВ0Г° потомка’ то был бы применен случай 1, когда его нужно было бы удалить.
топай вочнпкя^тП0Ка3аНа последовательн°сть операций удаления, при ко-каждого депева в	снтуации- Напомним, что симметричный обход
пчроход,,т °се уэлы -воз₽зси пека.	В каждом случае это двоичные деревья во
Компонентная (Ьункпия «-а™»..
функцией для удален,JсодсржХ™” °6шед°ступ“°й к°м°°«еВТО* ется к собственной компонентноГт тическую
Фунхшп» Зададный элемент. Она обрат* _гегло’/е, которая выполняет фак
работу:
(6)
-	улмента (ади(б)- Случаи 1 улпм’»»**^'7'^3
элемента В; (б)и(в)-СлучайЗ; умлтс элемеига Ь; (ej и(г, - О./».* J у^»м»<ие
rfflVyyP»1 *анны*.
t-pl.t.<cl.ee T> void Se.rchTr,.<T>: ir„ remove(val, root);
tamplate<class T>
v3id SearchTree<T>: :_renove(T val, TreeNode<T> ♦ fin)
if (n = NULL)
return;
int result = (*cmp) if (result < 0)
(val, n->val);
remove(val, n—>_lchild);
else if (result > 0)
remove(val, n->_rchild);
else {	// случай 1
if (n->_lchild — NULL) (	'
TreeNode<T> *old = n; n = old->rchild; delete old;
1
else if (n->_rchild = NULL) (	// случай 2
TreeNode<T> *old = n; n = old->lchild;	ij
delete old;
)	h]
else (	// случай 3
TreeNode<T> *m = _findMin(n->_rchild); n->val = m->val;
remove (m->val, n->_rchild) ; ) )	t
I
Параметр n (типа ссылки) служит
KU, которое содержит ссылку вниз> »	У	(в предке уала old),
подлежащего удалению (old), п обозн - команда n=old->rchild за-содержащее ссылку вниз на old. Следовательно команду меняет ссылку на old ссылкой на прав	дерева поиска наямень-
Компонентная функция removeMin удаляет из дерева
пшй элемент и возвращает его:	.. •	*«—°
templateCclass Т> Т SearchTree<T>: :гипогеИ-)"(vo^- )
Т v = f indMin () ;	•	. - V О**
remove (v) ;	' S MOt
return v;	"
1
сносов сортировки ^ассива зле^ Древовидная	" "раммы. «сполъ’УХо поиска
реализуется в виде просто Р элементов в Д Р пока Идея заключается в занесен.Ш	элемента
в интерактивном удалении	heapSort сортир
Удалены все элементы. ПРогр°* п11еНИЯ схпр : ментов, используя функцию ср
«8
t«nplae«<cla»B T> void heapsort (T at),
( /1-
SearchTree<T> t(cmp),
for (int i = 0; i < n; i++) t.insert(s[i]) ;
for (i = 0; i < n; i++) s[i) = t.removeMin();
int n, int(*cmp)
(T,T) )
3.6.	Связанные двоичные деревья поиска
Существует одна задача, которая не может быть эффективно решена с по-мощью двоичных деревьев поиска: для заданного элемента в двоичном де-реве поиска найти ближайшие больший и меньший элементы. Хотя при симметричном обходе все элементы выстраиваются в порядке увеличения, это не позволяет эффективно определить предшественника и последователя для произвольного элемента. В этом разделе мы рассмотрим связанные двоичные деревья поиска, которые являются двохгчными деревьями поиска со связным списком, охватывающим все узлы в порядке возрастания. Для краткости связанное двоичное дерево поиска будем называть связанны* деревом поиска, а охватывающий его связный список — лентой.
Рис 3.13. Связанное двоичное дерево поиска дугами
с головным узлом. Связи ленты представлены более светлыми
Де₽еВ° П°ИСКа РеаЛИЗуеМ в виде >«асса BraidedSearchTree. существенным^пизиак! КЛВСса SearchTree из предыдущего раздела по трем обладают связным спи Во’пеРвых, объекты класса BraidedSearchTree вает ссылки на предшественника и п^ РЫИ ДЛЯ каждого Узла УстанавЛ* BraidedSearchTree поддерживают	Во'втоРых‘ объекты класса
располагается над каким-тиЯпкно’ К0Т0Р°е в любой момент временя что и в классе List: существуетВ Дереве' Окпо служит той же пели, том в окне. В-третьих, элемент ЛЛ & РЯД 0Пераций с окном или с элемен на головной узел — «псевдокооневлй КЛЙССа BraidedScarchTrce указывает ется фактическим корнем связа * УЗел*’ пРавь’й потомок которого явля ленту, то узел, содержащий наимЛ?^ Лерева поиска. Если посмотреть в« ным узлом, а узел, содержащий	“ дерсис’ следует за голов-
ловкому узлу. Следовательно голпп»»» в ^льшой элемент, предшествует го которая располагается одновпемош.г УЗОЛ соотвстствует колонной позии1111, ней (рис. 3.13).	Р енно перед первой полицией и после послеД
I
дапуй!^!-------------------------------------- ЯНК" "1
— """"" ""
3.6.1-	Класс BraidedNode
Узлы связанного дерева поиска являются объектами класса BraidedNode. скОльку эти узлы действуют одновременно как узлы дерева и как узлы П0С то шаблон класса BraidedNode составим на основе классов TreeNode П Node:
।
teB1plate<claas Т>
class BraidedNode : public Node, public TreeNode<T> { public:
BraidedNode (T) ;
BraidedNode<T> *rchild(void);
BraidedNode<T> *lchild (void);
BraidedNode<T> *next (void) ;
BraidedNode<T> *prev (void) ;
friend class BraidedSearchTree<T>;
I;
Конструктор класса BraidedNode явно инициализирует базовый класс TreeNode по своему списку инициализации, а базовый класс Node инициализируется неявно, поскольку его конструктор не имеет параметров:
। template<class Т> BraidedNode<T>: : BraidedNode (Т val) :
TreeNode<T> (val) (
I
Компонентные функции rchild, lchild, next и prev устанавливают четыре ссылки для текущего узла — первые два для дерева поиска, а последние два — для ленты:	•
teniplate<class Т>	-з 4 .(ЕМ
BraidedNode<T> *BraidedNode<T>:: rchild (void)
return (BraidedNode<T> *)_rchild, I
template<clase T>
BraidedNode<T> *BraidedNode<T>: :lchild(
return (BraidedNode<T> *) )_lchild; i
ta»plate<class T>
3r»idedNodo<T> *BraidedNode<T>: : next (void) ( return (BraidedNode<T> -)_next;
tenplata^ciaaa T>	,
®raidadNoda<T> *BraidedNode<T>: :prev(voiai
( return (BraidedNode<T> *)_pr«v;
70
3.6.2.	Класс BraidedSearchTree
Шаблон класса BraidedSearchTree определяется следующим обРаз01(:
templ«te<cla»» Т> cl... BrsldedS«rchTree (
private:	// головной узел
BraidedNode<T> *гоо ,	// текущее окно
BraidedNode<T> *win;	фун|ЩИЯ
сравнения
int (*cmp) (Т,Т);
void _remove(T, TreeNode<T> S)»
public: BraidedSearchTree(int(*)	(T,T) )/
^BraidedSearchTree(void);
T next(void);
T prev(void); void inorder(void(*) (T) );
T val(void);
bool isFirst (void);
bool isLast(void);
bool isHead(void) ;
bool isEmpty(void);
T find(T);
T findMin(void);
T insert(T);
void remove(void);
T removeMin (void) ; };
3.6.3.	Конструкторы и деструкторы
Конструктор класса BraidedSearchTree инициализирует элементы данных стр для функции сравнения и root для пустого дерева, представленного в виде изолированного головного узла:
template<class Т>
BraidedSearchTree<T>: :BraidedSearchTree (int (*с) (Т.Т) ) стр (с)
(
win = root = new BraidedNode<T>(NULL) ;
Деструктор класса ловного узла:
удаляет дерево целиком, обращаясь к деструктору г0-
template<clas8 Т>
Br«idedSearchTr«.<T>; : -Br.id.d3..rchTr.e (void) delete root;
I
ЗМ. Использование ленты
зывает на узел, над которым пагпгКМ° ДЛЯ деРепп ~ Т. с. элемент win Ук> летается окно. Компонентные фУ,,кЦ”
•itiM
X4T
i.^Siae
return win->val;
tempiate<class
T>
return (win
T>
return (win
root->prev
teaPlate<cl
T>
BraidedNode<T> *n = root->next() ; while (n != root) ( (♦visit) (n->val);
n = n->next() ;
гтуктуры данных

next и prew перемещают окно на гп»
Если окно находится в головной Л следУ»Щую или на первую позицию, а функция рг™*™' Т° Функ«»я^е^^пУЮ поа*Ции. цю, возвращают ссылку на	"ози^Тук?
template<class Т> т B«i<iedSearehIree<T>. .	°КВа:
f	•»next(void)
win = Win->nextо; return win->val;
template<class T> T BraidedSearchTree<T>: :prev(void) win = win->prev() ;
return win->val;
Компонентная функция val возвращает ссылку на элемент внутри окна. Если окно находится в головной позиции, то возвращается значение NDLL.
template<class Т> Т BraidedSearchTree<T>: : val (void)
Симметричный обход выполняется путем прохода по ленте от первой позиции до последней, применяя функцию visit для каждого элемента на своем пути:
template<class T>
void BraidedSearchTree<T>: : inorder (void (*visit) (T) )
Компонентные функции isFirst, isLast и isHead возвращают знатям TRUE (истина), если окно находится в первой, последней или гсигавш^ЯО" зициях соответственно.
bool BraidedSearchTree<T>:: isFirst (void) root->next() ) (root I
bool BraidedSearchTree<T>:: isLaat (void)
bool BraidedSearchTr«a<T>::isHead(void)
Главаз
return (win == root);
Функция isEmpty возвращает значение TRUE (истина) только если теку, щее дерево пусто и состоит только из одиночного головного узла:
template<class Т> bool BraidsdSearchTree<T>::isEmpty () (
return (root == root—>next() );
J
3.6.5.	Поиск
Для поиска элемента используется компонентная функция find. Эта функция аналогична функции SearchTree: :find за исключением того, что она начинает свой поиск с root->rchild (), фактического корня. Если элемент найден, то окно устанавливается на этом элементе.
template<class Т> Т BraidedSearchTree<T>::find(Т val) {
BraidedNode<T> *n = root->rchild();
while (n) { int result = (*cmp) (val, n->val); if (result < 0)
n = n->lchild() ;
else if (result > 0) n = n->rchild();
else (
win=n; return n->val;
}
) return NULL;
}
Наименьший элемент в связанном дереве поиска располагается в первой позиции ленты. Компонентная функция findMin перемещает окно на этот наименьший элемент и возвращает указатель на элемент. Если дерево поиска пусто, то возвращается NULL :
template<class T> Т BraidedSearchTree<T>::findMin (void)
win = root->next();
return win->val;
}
3.6.6.	Включение элементов
Новый элемент должен быть внесен в его правильной позиции как в дерево поиска, так и в ленту. Если новый элемент становится левым потомком, то тем самым он становится предшественником предка в ленте. Если элемент становится правым потомком, то он становится последователем предка в ленте. Реализация функции insert одинакова с функцией SearchTree::insert за ис-
„ючением дополнительного включен...
нветить. что в функции insert "Довог° Ила в леиту
„устое дерево, поскольку головной у3е ^*од"м«тп проверки " СЛеяует „магвет окно над только что внесен’'» °Р"сут"»т всегда ф “’°',енл" “
“ элемент, если включение произошло^Хно™ “	~
:^plate<class Т> Т BraidedS©archTroe<T>: :insert(Т val)
int result = 1;
BraidedNode<T> *p = root;
BraidedNcde<T> *n = root->rchild();
while (n) ( p = n; result = (*cmp) (val, p->val); if (result < 0) n = p->lchild();
else if (result > 0) n = p->rchild()t else return NULL;
I win = new BraidedNode<T> (val) ; if (result < 0) { p->_lchild = win; p->prev()->Node: .’insert(win) ;
I else { p->_rchild = win; p->Node::insert(win);
J return val;
)
3.6.7.	Удаление элементов
Компонентная функция remove удаляет элемент, находящийся в окне Я перемещает окно в предыдущую позицию.	.	..
template<class Т> void BraidedSearchTree< Т>:. remove (void) (
*•£ (win != root)
_remove (win->val, root->_rchild);
- .СТ. У
пск ЛеМент Va^’ подлежащий удалению, и указатель п на корень дерева по-cofi3' В КОТОР°М находится этот элемент, передаются в качестве параметров ЛогСТВенно” Функции _remove. Эта функция работает подобно своему ана-,(Ь/У с тем же именем в классе SearchTree. Однако при работе со связан-ппп ДеРев°м поиска элемент, конечно, должен быть удален как из дерева
’ гак и из ленты:	й

te®pl.ate<class T>	___
’'old BraidedSearchTree<T>: • r
74
int. result = (*cnnp) (val, n—Xval) ;
if (result < 0)
_remove(val, n->_lchild);
else if (result > 0)
remove(val, n-> rchild);
else (	“	// случаи 1
if (n->_lchild “ NULL) { BraidedNode<T> *old = (BraidedNode<T>*)n;
if (win = old) win = old->prev();
n = old->rchild();
old->Node::remove() ;
delete old;
)
else if (n->_rchild == NULL) (	// случай 2
BraidedNode<T> *old = (BraidedNode<T>*) n;
if (win — old) win = old->prev();
n = old->lchild();
old->Node::remove();
delete old; )
e^se	// случай 3
BraidedNode<T> *m = ( (BraidedNode<T>*) n)->next () • n->val = m->val;
„remove(m->val, n->_rchild);
)
вателя узла n в ^случае	использУет ле«ту для поиска последо-
непустых потомка. Отметим тазХ	УДаленню Узел имеет два
тип TreeNode*, а не на тип в 4J° параметР п является ссылкой на п указывает на ссылку, хранящуюся в °пое‘ Происходит П0Т0МУ- w нпю, а эта ссылка имеет тип ТгееЫокЛ рузла’ п°ллежаЩего УД«в-п был присвоен тип BraidedNode* ’ ЬСЛИ бы вместо этого параметру анонимный объект. При этом ’ Т° ЭТ° ®ыла бы ошибочная ссылка на ступно.	ЭТ0М поле «ьики в узле предка было бы ведо-
Компонентная функция те
мент и возвращает указатель на neit? Удаляет из Дерева наименьший эле-если дерево пусто. Если окно солрп^’ <рункция возвращает значение NULL, то окно перемещается в головную nnnt^0 удаляемый наименьший элемент, изменения:	вн>ю позицию, в ином случае оно остается без
:renoveMin(void)
root-> rch;' . return val •	—	»
ГгруИУРылаНН —
3.7.	Деревья со случайным поиском
Поиск осуществляется тем быстрее, чем ближе к корню расположен яс комый элемент. В идеале двоичное дерево поиска сбалансировано, т. е. его форма такова, что каждый элемент располагается сравнительно близко к корню. Сбалансированное двоичное дерево поиска размера п имеет высоту O(log п), в предположении, что путь от корня до каждого узла имеет длину не более, чем O(log п). Но совсем несбалансированное дерево поиска имеет высоту поиск в таком дереве чуть более эффективен, чем в связанном списке (рис. 3.9в).
Двоичные деревья поиска, сформированные совместным использованием функций insert, remove и removeMin имеют высоту П(л) в наихудшем случве. Однако, если предположить, что из этих трех операций применялась только одна функция insert и что п вводимых элементов поступали в случайном порядке и что все л! перестановок одинаково вероятны, тогда двоичное дерево в среднем будет иметь высоту O(log л). Это очень важный результат, хотя и основанный на предположениях с серьезными
ограничениями, в результате чего полученные двоичные деревья поиска не всегда эффективны на практике. Во-первых, во многих приложениях входной поток элементов не обязательно будет случайным (в крайнем случае эле
менты вводятся в возрастающем или убывающем порядке и получающееся двоичное дерево в большинстве случаев оказывается несбалансированным). Во-вторых, порядок операций поиска (перемежающихся с операциями ввода) может быть непредсказуем (поиск недавно введенных элементов
•Щ
может оказаться особенно дорогим, поскольку элемент может оказаться на самом нижнем уровне двоичного дерева). В третьих, ожидаемые свойства деревьев поиска, получившихся после выполнения операций удаления remove, если она допустима, не всегда хорошо понятны.
В этом разделе мы рассмотрим деревья случайного поиска. Идея заключается в том, чтобы сделать поведение дерева поиска зависимым от некоторого числа, полученного от генератора случайных чисел, а не от порядка ввода. При вводе элемента в дерево случайного поиска этому элементу присваивается приоритет — некоторое вещественное число с равномерным рас^ пределением в диапазоне [0, 1] (вероятность диапазоне одинакова). Приоритеты элементов
выбора любого числа в заданном в дереве случайного поиска оп-
₽ЧС 3.
Главаз
ределяют их положение в дереве в соответствии с правилом: приоритет каждого элемента в дереве не должен быть более приоритета любого из его последователей. Правило двоичного дерева поиска также остается справедливым: для каждого элемента х элементы в левом поддереве х будут меньше чем х, а в правом поддереве — больше, чем х. На рис. 3.14 приведен при-мер такого дерева случайного поиска.
Как же осуществить ввод некоторого элемента х в дерево случайного по-иска? Он выполняется в два этапа. На первом этапе будем учитывать приоритет и введем элемент х уже известным нам способом: проследуем по пути поиска элемента х от корня вниз до внешнего узла, которому принадлежит х, и затем заменим внешний узел новым внутренним узлом, содержа-
ли Ю-(6) " л,'вдя Р0’0*” ****”"
дим х. На втором этапе мы изменим форму дерева в соответствии о приоритетами элементов. Сначала случайным образом зададим приоритет для х. В простейшем случае, приоритет х может быть больше и равен приоритету предка, тогда задача оказывается решенной. В противном случае, если приоритет х оказывается меньше приоритета его предка у, то правило приоритета для у оказывается нарушенным. Предположим, что элемент х оказался левым наследником для у. В этом случае применим правую ротацию для элемента у, переместив элемент х в дереве на один уровень выше (рис. 3.15). Теперь правило приоритета может оказаться нарушенным для нового предка элемента х, Если это так, то применим ротацию для предка — правую ротацию, если х является левым потомком, или левую ротацию, если х является правым потомком, переместив таким образом элемент х еще на один уровень вверх. Такой подъем элемента х будем осуществлять до тех пор, пока он не станет либо корневым, либо его приоритет будет не меньше, чем у его предка.
На рис. 3.16 показан процесс внесения элемента. На этой схеме показано, как элемент 6 заменяет соответствующий внешний узел, а затем поднимается вверх в соответствии с его приоритетом (.10) и приоритетами его текущих предков.
Определим теперь соответствующие классы. Несколько позже рассмотрим, как удалить элемент из дерева случайного поиска.
3.7.1.	Класс RandomizedNode
Узлы в дереве случайного поиска являются объектами шаблона класса Randomi zedNode:
template<class Т>
class RandomizedNode : public BraidedNode<T> {
protected:
RandomizedNode *_parent; double _priority;
void rotateRight(void) ; void rotateLeft(void) ;
void bubbleUp(void); void bubbleDown(void);
public:	,. .
RandomizedNode (T V, int seed = -1),
RandomizedNode *lchild(void);
RandomizedNode *rchild(void) ;
RandomizedNode *next(void),	_
RandomizedNode *prev( void),	 *
RandomizedNode *parent(void) ,	*
double priority(void);	-*
friend class RandomizedSearchTree<T>, );
Класс RandomizedNode наследует пять элементов данных из его оа класса: val, Ichild, _rchild, _prev и _next. В класс включен Дополнительных элемента данных: _parent, который указывает на ’’редок текущего узла, п _priority, приоритет текущего узла.
71
Конструктор присваивает новому узлу Randomi^edNode случайное дВа ние приоритета, используя стандартную функцию языка C++ rand, котоп-. генерирует случайное число в пределах от 0 до RAND_MAX :
RandomizedNode<T>: :Random!zedNode (T v, int seed) :
BraidedNode<T>(v) (
if (seed != -1) grand(seed);
priority = (rand() % 32767) / 32767.0;
изменным порядок следования элементов
Наибольший интерес представляют компонентные функции класса RandomizedNode, объявленные как private (собственные). Первые две выпад, няют два вида ротации — правую и левую. Правая ротация является ло-калькой операцией, которая изменяет форму дерева поиска, оставляя не до и после выполнения
операции предшественник и последователь для каждого узла остаются без изменения. Правая ротация для узла у приводит к повороту поддерева с корнем в узле у вокруг связи от узла у к его левому потомку х (рис. 3.15), Операция выполняется путем изменения небольшого числа связей: связь с левым потомком узла у заменяется на связь с поддеревом Тг, связь с правым потомком узла х заменяется на связь с узлом у, а связь с узлом и (от его предка) заменяется на связь с узлом х.
Компонентная функция rotateRight выполняет правую ротацию для текущего узла, который играет роль узла у. При этом узел х должен был левым потомком узла у. Для надежности предполагается, что предок узла у существует, поскольку головной узел должен иметь минимальный прнорп-тет ни одного другого узла не должно быть на уровне головного узла, поэтому все остальные узлы должны иметь своего предка.
template<clasa Т> void Random!zedNode<T>::rotateRight (void)
Random!zedNode<T> *y = this;
RandomizedNode<T> »x = y->lchild() •
Random zedNode<T> *p = y->parent()• y->_lchild = x->rchild();
if (y->lchild() ! = NULL)
—• • —. —. —
)
показано на рис, 3.15^° on^onn*1” rctatrjLeft Для левой ротации таК*е пределяется симметрично:
t—platXcl... Т> void Random! M*dNod^
(	«h<T>: JrotateLef t (void)
^уктурыланньос
7»
Random!zedNode<T> *х = this;
BandonizedNode<T> *y = x->rchild() ;
BandomizedNode<T> *p = x->parent();
x_> rchild = y->lchild() ;
if 7x->rchild() ! = NULL) x->_rchild()->_parent = x;
if (p->lchild() == x) p->_lchild = y;
else
p->_rchild = y;
y->_parent = p;
y->_lchild = x;
x->jparent = y;
I
Компонентная функция bubbleUp переносит текущий узел вверх по направлению к корню путем многократных ротаций до тех пор, пока приоритет текущего узла не станет больше или равным, чем у его предка. Функция применяется в тех случаях, когда элемент в дереве случайного поиска располагается ниже, чем это следует в соответствии со значением его приоритета. Отметим, что ротация применяется к предку текущего узла.
templateCclass Т> void RandomizedNode<T>: :bubbleUp (void) (
Random!zedNode<T> *p = parent();
if (priority () < p->priority {) ) (
if (p->lchild() — this)
p->rotateRj.ght () ;
else p->rotateLeft();
bubbleUp();	k
•	ce
Компонентная функция bubbleDown переносит текущий узел вниз по, направлению к внешним узлам дерева путем многократных ротаций до тех'
пор, пока приоритет текущего узла не станет меньше или равным приоритетам обоих его потомков. Для ротации ее направление (вправо или вдфо) зависит от того, какой из потомков текущего узла имеет меньший приори->
тет. Если, например, приоритет левого потомка меньше, то правая ротация переносит левый потомок вверх на один уровень, а текущий узел переме-
няется на один уровень вниз. Каждому внешнему узлу приписывается при-°ритет 2.0, достаточно большой, чтобы ошибочно не переместить его вверх.
Ункция bubbleDown пршченяется в тех случаях, когда СлР«лйного поиска располагается выше, чем это следует в
3,,аченпем его приоритета. Эта функция будет > ля удаления элемента из дерева поиска.	. t
^•>«plate<claes Т> void Random!cedNode<T>: :bubbleDown(void)
float IcPriority - IchildO ? IchildO->priority() : 2-.WW float rePriority - rchildO ? rchild()-priority() : 2.0| . . float minPriority « (IcPriority < rePriority) ?	'	3
IcPriority : rePriority;
•о
if (priority() <ж minPriorxty)
return;
if (IcPriority < rePriority) rotateRight();
else
rotateLeftO ;
bubbleDown();
)
Общедоступные компонентные функции rchild, Ichild, next, prey и parent определяют связи текущего узла с его правым и левым потомками, с последующим и предшествующим узлами и с предком соответственно:
template<class Т>
RandomizedNode<T> *RandomizedNode<T>: : rchild (void) (
return (RandomizedNode<T>*) _rchild;
}
templateCclass T>
RandomizedNode<T> *RandomizedNode<T>: : Ichild (void) {
return (RandomizedNode<T>*) Ichild;
)
template<class T>
RandomizedNode<T> *RandomizedNode<T>: :next(void) return (RandomizedNode<T>*) next;
template<class T>
RandomizedNode<T> *RandomizedNode<T>: :prev(void)
return (Randomiz6dNode<T>*) nrav*
} V'
Randomi xedNode<T> -Random, zedHode<T>; :patent (void)
Компонентная функция
У возвращает приоритет текущего узля:
templateCclass Т>
doubl. Randcmi2ed1)od.<T>:;ptiocitY(vo,d)
return _priority;
3.7.2.	Класс ^ndomizedSeatchTree
Деревья случайного поиска
RandonizedSearchTroo. Во ми,, Идста|1л"»тся И80гих “тоошениях
в виде объектов класса шаблон класса нппомиваеТ
класс BraidedSearchTree: элемент данных root указывает узел, win представляет окно, а элемент стр указывает на нения деревьев.
на головной
функцию срав-
template<class Т> class RandomizedSearchTree {
private:
RandomizedNode<T> *root;	//	головной узел
RandomizedNode<T> *win;	//	окно
int (*cmp) (T,T);	//	функция сравнения
void _remove(RandomizedNode<T>*) ;
public*
RandomizedSearchTree(int(*) (T,T) , int = -1) ;
-RandomizedSearchTree(void);
T next(void);
T prev(void);
void inorder(void(*) (T) );
T val (void);
bool isFirst(void) ;
bool isLast(void);
bool isHead(void);
bool isEmpty(void);
T find(T) ;
T findMin (void) ;
T locate(T);
T insert(T) ;
void remove (void) ;
T remove(T);
T removeMin(void);
J;
3.7.3.	Конструкторы и деструкторы
новое
Конструктор RandomizedSearchTree инициализирует случайного поиска, представляемое изолированным головным нимальным приоритетом, равным -1,0:
RandomizedSearchTree<T>: :RandomizedSearchTree (int (*с) (Т,Т) int seed) :
стр (с) О 
win = root = new Randomi zedNode<T> (NULL, seed)
Деструктор удаляет дерево поиска:
^ndomizedSearchTreeO
3.7.4.	Использование ленты
Компонентные функции next, prev, val, inorder, isFirst, isLast, isHead и isEmpty определяются подобно их аналогам в клаие BraidedSearchTree :
template<class Т> Т RandomizedSearchTree<T>: : next (void) (
win = win->next(); return win->val;
)
template<class T> T RandomizedSearchTree<T>: :prev (void) (
win = win->prev(); return win->val;
)
template<class T> T RandomizedSearchTree<T>: : val (void) {
return win->val; )
template<class T>
void RandomizedSearchTree<T>:: inorder (void (*visit) (T) )
RandomizedNode<T> *n = root->next() ;
while (n != root) ( (♦visit) (n->val); n = n->next();
} )
template<class T>
bool RandomizedSearchTree<T>: {
isFirst(void)
return (win = root->next() ) st (root ! =
root->next ()
template<class T>
bool RandomizedSearchTree<T>::isLast(void)
return
(win = root->prev() ) ti (root
1= root->next()
template<class T>
bool RandoaizedSearchTree<T>::isHead(void) return (win — root);
1
teeplate<class T>
bool RandomizedSearchTree<T>:: i,Empty (void) return (root -• root->next() )♦
Структуры данных
3.7.5. Поиск
return NULL;
R^ndomizedNode<T> *b = root;
RandomizedNode<T> *n = root->rchild() ;
win = root->next(); return win->val;
= n->lchild() ; if (result > 0) = n->rchild() ;
<
int result = (*cmp) (val, n->val), if (result < 0)
n = n->lchild() ;
else if (result > 0) <
templateCclass Т> Т RandomizedSearchTree<T>• -F4 а tm
।	:find(T val)
RandomizedNode<T> *n = root->rchilrf м while (n) (
int result = (*cmp) (val, n->vai) •
if (result < 0) n else n else
win = n; return n->val;
Компонентные функции find и finHLu аналогам в классе BraidedSearchTree ; ПРИМеняю™ также подобно их
template<class Т> Т RandomizedSearchTree<T>:: findMin (void)
Теперь мы введем новую операцию поиска locate. При обращении к функции locate с аргументом val возвращается наибольший элемент в дереве, который не больше, чем значение val. Если в дереве присутствует элемент со значением val, то возвращается значение val. Если такого значения нет, то возвращается значение меньше, чем val, если же в дереве и такого значения нет, то возвращается NULL.	..
Для выполнения этой операции будем отслеживать путь по дереву поиска До значения val. По мере продвижения вниз запоминается последний (самый нижний) узел Ь, в котором поворачиваем вправо — узел Ь является самым нижним узлом, обнаруженным на пути поиска, для которого правый потомок также лежит на пути поиска. Если в дереве находится значение val, то оно просто возвращается. В противном случае, если мы не найдем значение val в дереве — путь поиска завершается на внешнем узле -^->то" возвращается элемент, запомненный в узле Ь.	- - .	>
Операция реализуется компонентной функцией locate, в кото перемещается на обнаруженный элемент и возвращается этот tamPlate<class Т> Т RandomizedSearchTree<T>:: locate (Т

ь  n;
n  n->rchild();
) ( win  n; return win->val;
I ) win  b; return win->val; )
Каким же образом получается правильный результат? Он вполне очевиден, если значение val точно находится в дереве. Ио предположим, что такого значения в дереве нет, поэтому поиск значения val завершается на внешнем узле. Поскольку указатель п сигнализирует об окончании поиска, значение b показывает на наибольший элемент, который меньше всех остальных, содержащихся в поддереве с корнем в узле п. Легко видеть, что это условие сохраняется всегда. Каждый раз, когда в процессе поиска в узле п мы переходим на левую ветвь, условие продолжает сохраняться, поскольку каждый элемент в левом поддереве узла п меньше элемента в узле п. При переходе в узле п на правую ветвь присваивание Ь значения, равного п, восстанавливает условие, поскольку: (1) каждый элемент в правом поддереве узла п больше элемента, хранящегося в п и (2) элемент, хранящийся в узле п больше, чем элемент, на который указывало значение b до его изменения. И, наконец, когда п указывает на внешний узел, то Ь будет наибольшим элементом в дереве, который меньше любого другого элемента, который может легально заменить внешний узел, среди которых находится также и val.
3.7.6. Включение элементов
Для внесения нового элемента в дерево случайного поиска сначала найдем внешний узел, к которому он принадлежит (этап 1), а затем будем его поднимать по дереву по направлению к корню в соответствии с его приоритетом (этап 2). Компонентная функция insert заносит элемент val в текущее дерево случайного поиска и затем перемещает окно на этот элемент и возвращает указатель на этот элемент:
template<clas8 Т> Т RandomizedSeaгchTree<T>insert(Т val)
int result = 1;
RandomizedNode<T> *p  root;
RandomizedNode<T> *n = root->rchild while (n) {
P  n;
result  (»сзпр) (val, p->val) •
if (result < 0)
n  p->lchild() ;
else if (result > 0)
n • p~>rchild();
// этап 1
return NULL;
лруктур^мхых
win - new Randomiz.dNodQ<T>(va !» win—>_parent - p;	ival) ;
if (result < 0) (
p->_lchild - win;
P->prav() ->Node: .-insert (win) ;
p->_rchild  win;
p->Node::insert(win) ;
win- >bubblaUp (); return val;
// этап 2
i
I
I
I
I
I
I
3.7.7. Удаление элементов
Компонентная функция remove удаляет элемент в окне и затем перемешает окно в предшествующую позицию. Компонентная функция removeMin удаляет наименьший элемент и возвращает его. Если окно находилось на этом элементе, то оно перемещается в головную позицию:
tenplateCclass Т> void RandomizedSearchTr«»<T>::ramova(void) (
if (win != root)
_remove(win);
template<class T> T RandomizedSearchTree<T>::removeMin(void) (
T val = root->next()->val;
if (root != root->next() )
^remove(root->next() ) ;
return val;
Обе эти функции основаны на внутренней компонентной функции . remove. Для удаления узла п функция _remove увеличивает приоритет Узла п до 1.5 и затем опускает его вниз до тех пор, пока он не станет внеш- * ним узлом. Приоритет 1.5 превышает приоритет любого элемента в дереве, "о меньше, чем приоритет внешних узлов (2.0). После этого функция УДД-ляет уэСЛ п из дерева случайного поиска. Если окно было на узле, подлежащем удалению, то окно перемещается на предшествующую позицию. Ал-,оритм реализуется в виде функции _remove, которая удаляет узел ТСкУЩего дерева поиска.
с*пР1ае«<с1авв Т> void	"Mg.
RandomizodSearchTreo<T>: : remove (RandomizedNodo<T> #n)
n~>_priority  1. 5;
n->bubbleDown();
RandomizedNode<T> *p  n->parent(); if (p->lchild() “ n)

Глава 3
р-> Ichild = NULL; else
p->_rchild = NULL; if (win == n)
win = n->prev() ; n->Node::remove (); delete n;
На рис. 3.16, если его анализировать от (г) до (а), показано удаление элемента 6 из самого правого дерева поиска, для которого приоритет был увеличен до 1.5. Процесс удаления обратен процессу внесения элемента 6 в дерево (а), поскольку правая и левая ротации взаимно обратны.
Для удобства введем вторую компонентную функцию remove, которая, при передаче ей элемента, удаляет его из дерева случайного поиска. Если окно находится на элементе, подлежащем удалению, то окно перемещается в предыдущую позицию, в противном случае окно не перемещается:
template<class Т> Т Random!2edSearchTree<T>::remove (Т val) {
Т v = find(val) ;
if (v) {
remove () ,♦ return v; ) return NULL;
)
3.7.8.	Ожидаемые характеристики
В дереве случайного поиска размера п средняя глубина узла равна O(log л). Это справедливо независимо от изменений в порядке ввода элементов или положения в дереве, куда эти элементы вводятся или из которого удаляются. Поскольку выполнение каждой из операций ввода и удаления insertr remove и removeMin, а также операций поиска find и findMin занимает время, пропорциональное глубине обрабатываемого элемента, то в среднем затраты времени равны O(log л).
Почему же средняя длина поиска равна Ofiog л) 7 Чтобы ответить на этот вопрос, найдем ожидаемую глубину элемента н дереве случайного поиска Т. Переименуем элементы а дереве поиска Т по увеличивающемуся приоритету как Xi, Хп- Для определения ожидаемой глубины элемента Xfc предположим, что дерево Т строилось путем занесения элементов в указанном порядке в первоначально пустое дерево случайного поиска Т '. По мере ввода каждого элемента Х( определим вероятность того, что оно лежит на пути по-иска элемента хь в дереве Т.
Прежде, чем продолжать дальше, обсудим три момента. Во-первых, процесс имеет место в дереве Т, поскольку дерево случайного поиска полностью определяется его элементами и их приоритетами (если даже некоторые лна-имя приоритетов встречаются более одного роза, то элементы все равно могут быть упорядочены таким образом, что дерево Т существует). По-вторых. вследствие принятого порядка включения элементов не требуется ни-каких операций ротаций. В-третьих, каш вналип требует, чтобы были вне-
гтоуюУР**1 Данных
•7
сеяы только элементы xi,..., xki, поскольку элемент хк всегда обязательно заходится в конце своего пути поиска и внесение элементов xx+i.. Хп не
может увеличить глубину элемента хи, поскольку ни один из этих элементов не может быть предшественником элемента хь
Начнем с пустого двоичного дерева Т представленного единственным внешним узлом. Для внесения элемента xi в Т ' заменим внешний узел на Х1. Поскольку элемент xi находится в корне дерева Г то он будет предшественником любого элемента хч, так что вероятность того, что элемент xi лежит на пути поиска элемента хь, равна единице. Внесем теперь в дерево Т' элемент Х2. Очевидно, что элемент хг может заменить любой из двух внешних узлов, которые являются последователями элемента xi, так что только одна из этих двух позиций приведет к тому, что элемент хг будет лежать на пути поиска элемента хь Поэтому вероятность того, что элемент хг будет лежать на этом пути поиска, равна В общем случае, при вводе в дерево Т ' элемента хь где i изменяется от 1 до k, элемент х» с равной вероятностью замещает один из i внешних узлов. Поскольку только одна из этих позиций приводит к тому, что элемент xi будет лежать на пути поиска элемента хь, то вероятность того, что элемент Xi лежит на пути поиска элемента хк будет равна Из этого следует, что ожидаемая глубина элемента хк равна 1 + Ем р что соответствует O(log я) для k £ п.
3.7.9.	Словарь АТД
Словарь АТД используется для манипулирования набором элементов, выстроенных в линейном порядке. Он поддерживает динамическое редактирование элементов (создание, внесение и удаление) и различные формы поиска внутри набора (например, поиск заданного элемента, наименьшего пли наибольшего элемента, предшественника или последователя элемента). Конкретная реализация операции поиска и ее поведение лишь слегка отличаются в разных версиях. Для целей данного изложения в качестве словаря будем использовать дерево случайного поиска.
С помощью директивы препроцессора tdefine для словаря зададим идентификатор RandomizedSearchTree:
•define Dictionary RandomizedSearchTree
Применение я наших программах идентификатора Dictionary для обо-значения словаря более очевидно определяет его назначение, чем иденти-. фикатор RandomizedSearchTree. Более того, это дает нам возможность заменять одну реализацию словаря на другую путем простой замены соответствующей директивы tdefine.	•
3.8.	Замечания
Структуры данных хорошо описаны в роботах [2, 20, 78, 78» 83, 89. 90]. В монографии Роберта Тарьи [83], более полной по сравнению с другим*, деликатно описана взаимосвязь между структурами данных. *
Ритмами.
Из совместного
^““значение о определяется размером дерева. Деревья слт игГХка обсуждаются в [58]. Ротации, на которых они основаны, та^, Х=лись в ранних схемах сбалансированных деревьев поиска, когорт ___________ юй9 п пт к н nnvrnx деревьях (о, joj.
относятся к 1962 г. [1], и в других деревьях
3.9.	Упражнения
1.	Доказать, что двоичное дерево размера п содержит п + 1 внешних узла.
2.	Доказать, что двоичное дерево высоты h имеет размер по крайней мере 21' - 1. (Подсказка: выполните индукцию по значению Л.)
3.	Доказать, что двоичное дерево размера п имеет высоту по крайней мере [ Iog(n + 1)1. (Подсказка: использовать предыдущее упражнение.)
4.	Напишите компонентную функцию для вычисления высоты объекта класса SearchTree.
5.	Определите конструктор копирования для класса SearchTree.
6.	Измените определение Random!zedNode и Random! zedSearchTree так, чтобы каждый объект Random!zedSearchTree содержал свой собственный генератор случайных чисел, который бы срабатывал однажды при инициализации объекта.
7.	Покажите, что набор элементов с различными приоритетами полностью определяет дерево случайного поиска.
8.	Покажите, что ротация сохраняет порядок узлов в двоичном дереве.
9.	Для двух данных двоичных деревьев Т и Т ' размера п показать, что существует последовательность левых и правых ротаций, которая трансформирует Т в Т'.
10.
11.
12.
ЧТ0 МЫ опРеделил1’ двоичное дерево поиска, узлы ко-пр’дкох ZZ Т°аЬК° СВЯЗЯМИ ' Ле"им "	потомкам.. « ‘
для заданного узлГлиПод"* эмХ^»Г°₽‘Я °предоляет последователя то его наследник является предков y”a ” "РаВОГ° Показать, что 1	’’
нем равна 2.99 1од2”Г₽°В|а случойного поиска размера п в сред-периментально, проанализипг», П)’ ПопР°бУйге проверить это экс-ка разного размера. Кахлг< Р В нссколько деревьен случайного понс Oflog2 log2 л)?	числовой коэффициент скрыт в выражения
13. В утверждении, что ожидаемло
поиска размера п равна O(lo₽” ^',ИНа ПУ’П1 поиска в дереве случайной элемента xj эквивалентно ом ИСП0ЛЬ:1Уртся тот флкт. что внесение узлов I дерева Т \ Почему ^Н°ВерОЯТ’,Г|й ппмр’(е плиого из виеш*"* это так?
Глава 4
Структуры геометрических данных
D этой главе определим классы, которые вам потребуются прп работе с гео-метрическими объектами в двух- и трехмерном пространствах. В двухмерном пространстве эти классы будут обеспечивать выполнение таких операций, как разбиение полигона хордой на два меньших полигона, вычисление точки пересечения двух непараллельных прямых линий, определение положения точки относительно прямой линии. В трехмерном пространстве будем определять положение точки относительно плоскости и искать точку пересечения прямой линии и треугольника. Здесь же приводятся минимально необходимые сведения из линейной алгебры.
4.1.	Векторы

Определение положения точки на плоскости производится с помощью системы координат. В декартовой системе координат плоскость содержит две координатные оси с общим началом (точка их пересечения), имеющие одинаковые единицы длины. Оси перпендикулярны друг другу и ориентированы так, как показано на рис. 4.1а. Это устанавливает однозначное соответствие между упор ядочрняыми парами чисел (х, у) и точками на плоскости. Первая координата х точки показывает ее смещение вдоль горизонтальной осп, а вторая координата у — ее смещение вдоль вертикальной оси.
Упорядоченная пара чисел (х, у) может также рассматриваться как век-wop, что показано на рис. 4.16. Геометрически вектор (х, у) вто направленный отрезок прямой линии, начинающийся в точке (0, 0) и заканчивающийся в точке (х, у). Точка начала координат (0, 0) иногда обозначается как 0 и называется нулевым вектором.	'
Для работы с векторами существуют две основных операцщ:. —-— яек/пороа и скалярное умножение (рис. 4.2). Для двух заданпых векторов ° * (*0, Уп) и Ь - (хь, уь) сложение векторов определяется как а + b =
+ хь, рл + t/ь). Геометрически векторы а и b определяют с вершинами О, а, b и а + Ь.	___ _
Скалярное умножение заключается в умножении вектора на
лярнос умножение определяется как tb
°ПеРащги длина вектора b изменяется в t раз. Направлено ’^нщ'тся, если / > 0 и изменяется на обратное, если
ч,,сло. скаляр (рис. 4.2). Для заданных скаляра t я вектора о > - (txb, tyb)- При
1W
с тем же на-
Поскольку вектор начинается в точке начала координат, он полностью описывается точкой, в которой он оканчивается. И наоборот, вектор может быть описан его длиной и направлением. Длина вектора а = (ха, уа) обозначается через ||а|| и определяется как ||а|| =	+ У а • Длина вектора равна расстоянию от
начала координат 0 до точки а. Вектор называется единичным вектором, если его длина равна единице. Умножение ненулевого вектора а на величину, обратную его длине, приводит к получению единичного вектора правлением. Эта операция известна под названием нормализация.
Направление вектора а описывается его полярным углом 0а — углом, образуемым между положительной осью х и этим вектором. Полярный угол измеряется в направлении против часовой стрелки, начиная от положительного направления оси х, и лежит в диапазоне 0 < 0а < 360 (углы мы будем всегда измерять в градусах). На рис. 4.3 приведено несколько примеров.
Вычитание векторов определяется в терминах сложения векторов и скалярного умножения. Для заданных векторов а и b будем иметь b - а = b + Практически такая операция выполняется путем вычитания значений координат. Ь а (хь ха, уъ~уа). Геометрически операция вычитания определяет направленный отрезок прямой линии ab, начинающийся в точке а и заканчивающийся в точке Ь, эквивалентный вектору b - а (рис. 4.4).
Структуры геометрических данных
b-a
V
1
(1.5,200)
(1.315)
рис. 4.3. различные векторы, заданные в полярных координатах а = (Hall, 0 а)
Рис. 4.4. Вычитание векторов
Направленный отрезок прямой	*______
ным на плоскости. Концевая тонненных отрезка прямой "Г**” ‘ концевая точка b — концом. Два авлеНИе, могут быть совмвй®£* cl, имеющие одинаковые длину «	_ вектором b
ределены одним и тем же направл- средство для решения	дть.
Векторная арифметика предост йств направленных огре - — включающих в себя использование свойс^	следующемпрнмере.^
ся неизменными при их перено ’ не лежащие на одной	ориенти-
Пусть даны три точки ро. Р	называется п0*°*	- отрица
ределяют треугольник ДроР1Р2.	q от отреЭка прямо Р Р • цр^л*
рованным, если точка Р2 лежи	лежит справа	опое
телъно ориентированным, если	в описании пРои,0Д^_^1 ввк?
Ж (рис. 4.5). Проблема заключ	проблему с
ления ориентации. Рациональ Р	меняется при
поскольку ориентация треуголь тся к определению, “ =Р1 ’ Ро » b = Р2 " РО и 31^ яравлении против час°в°’ р имеет авторами, измеренными в на Р < 180| тогда Др(№ °т направления вектора а. Если
Рис. 4.5. треугольник ориентирован положительно (а) и отрицательно (6)
Рис. 4.6. Четыре возможных случая взаимного расположения векторов для определения ориентации треугольника
тельную ориентацию, в противном случае (180 < 6а& < 360) треугольник имеет отрицательную ориентацию.
Предполагается, что существуют четыре варианта расположения векторов а и Ь относительно друг друга (рис. 4.6). В случаях 1 и 3 имеем 0 < Qab < 180, а в случаях 2 и 4 — 180 < Qab < 360. В случаях 1 и 2 положительная ось х проходит внутри угла еа&, а в случаях 3 и 4 — нет. Четыре возможные конфигурации соответствуют четырем возможным диапазонам, в пределах которых находится угол £? = 0& - 6а:
Случай	Диапазон Q=0b-01	Ориентация треугольника Д po Pi Ps	sin Q
1	-360<Q<-180	+	+
2	-180<Q<0	—	
3	0<Q<180	— +	+
4	180<Q<360		
“Г-Яв0₽ГаЦИИ т₽еугольника нам нужно было бы »ьг№ каком же на четыоех	получить ответ на основе определения. 1
проще реш^ ZJ^npTaX’ Л„еЖЯТ ™ ^л. Однако значит.^" что знак синуса этого угла sin(O) м" Заметного из таблицы соотношу” ' ника. Поскольку	‘ совпадает со знаком ориентации треуголь
Bin<et-0O)= аьхеы со.(во) . со>(вь, в1п(()о)
Структуры геометрических данных
coseo = §, sinea = g, смвб=х>. sineb = ^
будем иметь
8111(04 - 0a) = Дрд (Xa yb - X> y„)
Длины векторов всегда положительны, следовательно sign(sin(0/, - 0а)) = sign(x0 yb - хь уа).
Отсюда следует, что выражение ха уь — хь уа имеет тот же знак, что и ориентация треугольника. В следующем разделе мы запишем это соотношение в виде функции языка C++, определяющей ориентацию треугольника. Стоит отметить, что выражение Ха уь ~ хь у а имеет простую геометрическую интерпретацию — оно равно площади (со знаком) параллелограмма с вершинами 0, а, b и а + Ь.
4.2.	Точки
4.2.1.	Класс Point (Точка)
KaaccPoint содержит элементы данных х и у для хранения координат точки. Компонентные функции обеспечивают выполнение операций по определению положения точки относительно заданного отрезка прямой линии и вычисления расстояния от заданной точки до прямой линии. Дополнитель
ные компонентные функции рассматривают текущую точку как вектор и определяют функцию специального вида — «операцию-функцию» (operator function) для реализации векторной арифметики, используя ключевое слово operator. Включены также компонентные функции, возвращающие значения полярного угла
•111
и длины.
Г
class Point (
public:
double x;
double у;
Point (double x = 0.0, double _y = 0.0);,
Point operator* (Points);
Point operator- (Points);
friend Point operator* (double, Points), double operator!] (int);
int operator^ (Points) ;
int operator!= (Points);
int operator< (Points);
int operator> (Points);
int classify(Points, Points);
int classify(Edges);
double polarAngle(void);
double length(void);
double distance (Edges);
4.2.2.	Конструктор	с координатами х и у:
Конструктор инициализирует но
Point:: Point (double double _У> ’• x(_x), y(_y) ( }
on имолчанию инициализируется точка в Если аргумент не указан, то по умолча
начале координат (0, 0).	дубль другой точки. Например, об-
Можно инициализировать то у	точку р с теми же коорди.
ращение Point p(q) ин”ц”^ сл^ае инициализация осуществляется ко-натами, как и точка q. » это ’ (существующем в компиляторе пирующим конструктором п°рУтМХю со всеми элементами.
языка C++), который выполняет копию
4.2.3.	Векторная арифметика
Векторное сложение и векторное вычитание выполняется с помощью операций-функций со знаками операций + и - соответственно:
Point Point::operator-*- (Point &p) (
return Point(x + p.x, у + p.y); )
Point Point:: operator- (Point &p)
return Point(x - p.x, у - p.y);
I умножения реализована в виде дружественной
Операция скалярного —— ----------------------------------—— r-”^“*uvuana о	Дру 7TVCV4 dcb«vm функции классу Point, а не члена класса, поскольку ее первый операнд не относится к типу Point. Оператор определен следующим образом:
Point operator*(double s, Point bp) (
return point(a * p.x, s • p.y); i
Операция-функция operator ГI
^С'ЧИ B об₽ащении в качестве иидекгЯЗВРаП1ает кооРДИнату х текущей точки, координату у при указании	UKUrnbl ^ыло указано значение0'
значения индекса ];
^uble ₽oi„t: ;operatoru
return (1 - 0} ? х ; у.
I	У *
Структуры геометрических данных
4.2.4.	Операции отношения
Операции отношения == и ности двух точек:
юпольз^ются для определения эквивалент*
int Point: :operator— (Point &p)
1 return (x == p.x) && (y == p.y); I
int Point::operator!= (Point fip) return !(*this == p);
I
Операции < и > реализуют лексикографический порядок отношений, когда считается, что точка а меньше точки Ь, если либо а.х < Ь.х, либо а.х — Ь.х и а.у < Ь.у. Для двух данных точек сначала сравниваются их х-координаты, и, если они равны, то сравниваются «/-координаты. Такой порядок иногда называется словарным порядком отношений, поскольку точно по такому же правилу располагаются в словаре двухбуквенные слова.
int Point::operator< (Point &p)
I
return ( (x < p.x) II ( (x == p.x) && (y < p.y))) ; I
int Point::operator> (Point &p) (
return ( (x > p.x) || ( (x = p.x) && (y > p.y))); I
Упорядочение точек на плоскости может быть выполнено бесконечным числом


способов. Тем не менее удобно использовать операции < и > для канонического упорядочения, поскольку нам часто придется сохранять точки в словарях и эти опера-
Прежде, чем переходить к остальным компонентным функция! Point, рассмотрим следующий простой пример, который показыв менение объектов класса Point. Функция orientation возвращав ние 1, если обрабатываемые три точки ориентированы положительно, они ориентированы отрицательно, или 0, если они коллинеарны. ЕГ Реализован способ, обсуждавшийся в конце предыдущего раздела.
.а « л •,
lnt orientation (Point ipO, Point fipl, Point fip2) I	*	1
Point a = pl — pO;
Point b = y2 - P0;	* „.'"/If
’.st
return 1;
Htf’l -
return -1;
r®turn 0;
4.2.5. Относительное положение точки и прямой линии
Одна из важнейших операций заключается в определении положен,, точки относительно направленного отрезка прямой линии. Результат полнения операции показывает, лежит ли точка слева или справа от м. правленного отрезка прямой линии или, если она совпадает с прямой ли-ни ей, располагается она после конца направленного отрезка этой прямой линии, до начала, а если ни то, ни другое, то не совпадает ли она с на. чалом или с концом или лежит между ними. Направленный отрезок прямой линии четко разделяет плоскость на семь непересекающихся областей и эта операция сообщает, какой из этих областей принадлежит точка (рис. 4.7).
Для определения положения текущей точки относительно отрезка прямой линии popi, направленного от точки рО к точке pl, используется компонентная функция classify. Возвращается значение типа перечисления указывающее на положение текущей точки:
С1руктурь1 геометрических данных
—----------------------- ”
return ORIGIN;
if (pl == P2> return DESTINATION
return BETWEEN;
располагается ли точка p2 слева или справа или п« определить, р	——* ту ------------ ” справа, или она коллинеарна с от-
—। вычисления, направление, то
Вначале проверяется ориентация точек рО, pl и о2 чтбк, располагается ли точка Р2 слева или справа, резком popi- В последнем случае необходимы дополнительные Если векторы а=р1-р0 и Ь=р2-р0 имеют противоположное точка Р2 лежит позади направленного отрезка ррр?, если вектор а короче вектора Ь, то точка р. расположена после отрезка рйрь В противном случае с точками рО и pl для определения, совпадает ли она с одной из этих концевых точек или лежит между ними
Для удобства предусмотрена несколько иная версия комповеитпой Функ-цаи classify, которой в качестве аргумента передается ребро вместо пары точек.
вис. 4.7. Радеке	01ракм
int Point::classify(Edge &e)
I
return classify(e.org, e.dest); )
Определение относительного положения точки и прямой линии в этой книге используется очень часто. В некоторых применениях достаточна более грубая классификация (т. е. достаточно индикации положения точки слева или справа от прямой линии, или просто фиксации совпадения с ней). В других же применениях требуется полная схема этой классификации.
4.2.6. Полярные координаты
I
I I
<
Система полярных координат обеспечивает второй способ задания положения точки на плоскости. Полярная ось выходит из точки начала коор-
динат 0 и направлена в виде горизонтального луча вправо, как показано не рис. 4.8. Точка а представляется парой чисел (га, 0а)« Принимая точку а за вектор, начинающийся в точке начала координат, число га определяет
int Point::Classify(Point fipO, Point fipl)
Point p2 = *this;
Point a = pl - pQ;
Point b = p2 - p0;
double sa > a.x *'b.y - b * ♦
if (»a > 0.0) У Ь a
return LEFT;
return RIGHT;
((ах • b.x < о.О) | | (а return BEHZND;
С. length» < Ь. length») return BEYOND;
(р0 — Р2)
Длину вектора» а 0О — его полярный угол (угол» образуемый между вектором а и полярной осью и отсчитываемый в направлении вращения против часовой стрелки).	мГ-й-ЛЙЙУ 
Соответствие между парами чисел (Гд, 0а) и точками не является однознач-,гым; множество пар могут представлять одну и ту же точку. Пара.(0, 0) соответствует началу координат для любого значения 0. Более того, дц^^чисел ,г7 9 + 360/г) соответствуют одной и той же точке для любого ।
Точка может быть представлена как в декартовых, так Ж-зурярта^ К00РДинатах и иногда бывает необходимо переходить от
Другой. Как это очевидно из рис. 4.8, два выражения	;	•
х = г cos 0, у = г sin 0	>
ОсУщестлляют преобразования из полярных коорд:
К00РДинаты (х, у), 1 '(акл:| 279 |
• -rtbKr
Рис.«.«. Точи р описывав поаяриь». координатами (Г, 0) и декартоаьмт координата™ (х. у)
Для обратного преобразования координата расстояния определяется как IH = М + У2
Чтобы выразить полярный угол 0 в функции от х и у заметим, что существует соотношение tan 0 = из которого следует
0 = arctan х Ф 0	(4.1)
Для использования выражения 4.1 в функции polarAngle необходимо различать квадранты на плоскости и учитывать случай возможного равенства нулю координаты х:
double Point::polarAngle(void) (
if ((x = 0.0) (y = 0.0)) return -1.0;
if (x = 0.0)
return ((y > 0.0) ? 90 : 270);
double theta = atan(y/x) ;
theta ♦= 360 / (2 * 3.1415926);
if (x > 0.0)
return ((y >= 0.0) ? theta : 360 else
return (180 + theta);
)
// в радианах
// перевод в градусы
// 1 и 4 квадранты
+ theta);
// 2 и 3 квадранты
куший вектоп яЛоНКЦИЯ ро1агАп91е возвращает значение -1.0, если те-ется неотоипательн^рТСЯ Нулев“^' вектоР°м (в противном случае возврата-ствии для уппошения ЗНачение^' ^то обстоятельство мы используем впослед-полярного угла. писания функции сравнения, основанной на задавил Компонентная функция length возвращает длину текущего вектора
double Point::length(void)
return sqrt(x*x + y*y).
Компонентная функция d i s t
ком) от текущей точки до ребг авозвращает значение расстояния (со зн« ' У функцию определим и разделе 4.6-
Струхтуры геометрических данных
4 3. Полигоны (многоугольники)
Полигоны (многоугольники) являются просто очаровательными объектами, причем очень удивительно, что они чрезвычайно просты концептуально. В этом разделе представим основные определения и концепции, чтобы можно было обсуждать свойства полигонов и средства для их обработки.
4.3.1.	Что такое полигон?
Полигоном называется замкнутая кривая на плоскости, образуемая отрезками прямых линии. Отрезки называются ребрами или сторонами полигона, концевые точки отрезков, совпадающие для двух соседних ребер, называются вершинами. Количество вершин (пли сторон, что эквивалентно), содержащихся в полигоне, определяет его размер. Для краткости мы иногда будем использовать название п-гон (или п-уголъник) для обозначения полигона размера п п обозначение (Р| для размера некоторого полигона Р.
Полигон будет простым, если он не пересекает самого себя. Простой полигон охватывает непрерывную область плоскости, которая считается внутренней, частью полигона. Неограниченная область, окружающая простой полигон, образует внешнюю часть, а набор точек, лежащих на самом полигоне, образует границу между этими двумя частями. В этой книге термин полигон будет использоваться в смысле простого заполненного полигона, т. е. объединения границы и внутренней части простого полигона. Например, если говорится, что точка находится в полигоне, то это означает принадлежность точки либо границе простого полигона, либо его внутренней
части.
Вершины упорядочены циклически вдоль границы полигона. Две вершины, являющиеся концевыми точками одного и того же ребра, являются соседями и про них также говорят, что они смежные. Вершина, соседняя при обходе по часовой стрелке, называется последователем, а соседняя при обходе против часовой стрелки — ее предшественником. Цепи вершин,, идД^ просто цепь — это часть границы полигона. Обход полигона заключается, в перемещении вдоль цепи от одной вершины к соседней к ней в надра»-.
Граница
Выпуклые вершины
Внешняя область
Внутренняя область
Вогнутые вершимы.
вогнутые о^минны, хорда
ленки по движению часовой стрелки или в противоположном Обход часто делает полный круг вдоль всей границы полигона, если необходимо посе. тить все вершины полигона.
Вершины полигона подразделяются на выпуклые и вогнутые. Вершина называется выпуклой, если внутренний угол при этой вершине (находящий-ся в пределах внутренней области) меньше или равен 180 градусов. В противном случае вершина считается вогнутой (внутренний угол более 180 градусов).
Отрезок прямой линки между двумя не соседними вершинами называется диагональю. Диагональ называется хордой или внутренней диагональю, если она лежит внутри полигона, не захватывая внешнюю область полигона. Добавление к полигону хорды делит его на два меньших подполигона. На рис. 4.9 проиллюстрированы некоторые определения, относящиеся к полигонам.
Точку или отрезок прямой линии иногда удобно рассматривать как вырожденный полигон. 1-гон состоит из единственной вершины и имеется единственное ребро нулевой длины, соединяющее вершину саму с собой. 2-гон содержит две вершины и два совпадающих ребра, соединяющих две вершины. Кроме других преимуществ использование вырожденных полигонов часто упрощает формирование полигона: начиная с 1-гона, вносим вторую вершину для образования 2-гона, добавление последующих вершин приводит к формированию обычных полигонов размера 3 и более. Если считать точки и отрезки прямых линий полигонами, то начальные этапы формирования полигона ничем не отличаются от последующих, поскольку каждый этап заключается в модификации полигонов.
4.3.2.	Выпуклые полигоны
точек
Область на плоскости считается выпуклой^ если для любых двух внутри области отрезок прямой линии между этими двумя точками полностью лежит внутри области. На рис. 4.9 полигон (а) является выпуклым в отличие от полигона (б), который таковым не будет, поскольку отрезок прямой линии Ру выходит за пределы полигона. Заметим, что граница выпуклого полигона не является выпуклой областью, тогда как внутренняя часть выпуклого полигона будет выпуклой.
Выпуклость приводит к появлению целого ряда полезных свойств, которые приводят к упрощению работы с выпуклыми полигонами по сравнению с произвольными полигонами. Например, любая диагональ в выпуклом полигоне является хордой. Кроме того, любая вершина в выпуклом полигоне будет выпуклой (в невыпуклом полигоне по крайней мере одна вершина вогнутая), з этого следует, что при обходе границы выпуклого поли-"° часоиой грелке в каждой вершине каждое вправо ₽ б₽О ЛИб° ПроД0ЛЖается по ПРЯМОЙ линии, либо поворачивает
Другое полезное свойство заключается в том что любых двух выпуклых областей А и В является ся в этом, предположим, г— -обе точки р и q лежат в А, а область А i линии Я лежит , сОл.т, А. Аиалогичко,
перрсрчгнир ДПВ выпуклым. (Чтобы убедить-что р н у любые две точки в ДПЛ. Поскольку выпуклой, то отр<?иок прямой
Гтрухтуры геометрических данных
101
Следовательно, отрезок рп лежи* »
выпуклой). Из этого следует, что п/УТрИ ЛПВ и область ДПВ пп
ляется выпуклым и факт.счески Г)Г ",рсссчение Wx вьгцу1О1Ых пп„ ° быть рождввкый). Более того, ПоХьТС„ГУКЛиЙ "°™™ Su литью, пересечение прямой лпиш, Т " ливн" ""яетея еы„^®« Областью - в виде отрезка прямой ли.ГпиХ^" ДОЛЖН° Оыть XX линия только касается полигон „ и одинокой точки У ” друпю свойства, данной книге мы «удам «L*1*”""- **•’• "W пуклыми полигонами.	удем чап*е всего работать имптт «
•	41A1U1IHO С ПБТ-
4.3.3.	Класс Vertex (Вершина)
Мы будем представлять полигон в виде кольца вершин, хранящегося в кольцевом списке с двойными связями. Каждый узел соответствует вершине и связан со своими двумя соседями, Следуя связям, мы можем обойти границу полигона в любом направлении, а включая или удаляя узлы (редактируя связи соответствующим образом), мы можем создавать полигоны и динамически их модифицировать.
Эту схему обеспечивают классы Vertex и Polygon. Полигон хранится в виде циклического списка объектов класса Vertex с двойными связями. Поскольку вершины полигона существуют одновременно как точки на плоскости и узлы в связном списке, класс Vertex формируется на основе класса Point и класса Node. Класс Polygon содержит элемент данных, указывающий на некоторую вершину в связном списке, представляющем полигон. Класс Polygon служит как общедоступный (public) интерфейс для полигонов.
Класс Vertex наследует элементы данных _next и _prev из базового класса Node и х и у из базового класса Point. По соглашению _next указывает на последователя текущей вершины (ее соседа в направлении по часовой стрелке), a prev — на предшественника текущей вершины (соседа в направлении против часовой стрелки).
class Vertex: public Node, public 01 public:
Vertex(double X, double y)»
Vertex(Pointfi);
Vertex *cw(void);
Vertex ♦ccw(void);	t
Vertex ‘neighbor(int rotation);
Point point(void);
Vertex ♦ insert(Vertex*) ;
Vertex ‘remove(void);
void splice(Vertex*) ;
Vertex ‘split(Vertex*);
friend class Polygon;
!
Объект класса Vertex может быть сформирован координатам х и у.
t-
на основе точки или !
Vortex::Vertex(double х, double у) •’ Point (x, у)
( )
Vertex::Vertex(Point &p) : Point (p)
( }
Компонентные функции cw и ccw возвращают указатели на последователя и предшественника текущей вершины соответственно.
Vertex *Vertex::cw(void) ( return (Vertex*)_next; }
Vertex ‘Vertex::ccw(void) ( return (Vertex*)_prev; }
Компонентная функция neighbor сообщает, какой из соседей определен параметром rotation, возвращая одно из значений типа перечисления CLOCKWISE или COUNTER_CLOCKWISE
Vertex ‘Vertex::neighbor(int rotation) (
return ((rotation = CLOCKWISE) ? cw() : ccw());
Компонентная функция point возвращает точку на плоскости, в которой находится текущая вершина
Point Vertex::point(void) (
return *((Point*)this);
}
Компонентные функции insert , remove и splice соответствуют своим аналогам, определенным в базовом классе Node.
Vertex *Vertex::insert(Vertex *v) <
return (Vertex *)(Node::insert(v));
Vertex *Vertex::remove(void)
return (Vertex *)(Node::remove());
void Vertex::splice(Vertex *b)
Mode::splice(b);
Стпуктуры геометрических данных
юз
WjMt
Заметим, что в функциях insert и remove перед выводом производится преобразование возвращаемых значений к типу указатель_на_7е^ех. Явное преобразование необходимо здесь потому, что язык C++ не может автоматически преобразовать указатель из базового класса, чтобы указать на объект производного класса. Причина заключается в том, что компилятор языка C++ не может быть уверенным в том, что имеется объект производного класса, на который нужно указать, поскольку объект базового класса не обязательно должен быть частью объекта производного класса (но, с другой стороны, компилятор языка C++ автоматически преобразует указатель на производный класс для указания на объект базового класса, поскольку каждый объект производного класса включает внутри себя объект базового класса).
Последняя компонентная функция Vertex: :split будет определена несколько ниже.
4.3.4.	Класс Polygon (Полигон)
ШИ
пш
Полигон представляется в виде объекта класса Polygon. Класс содержит два элемента данных. Первый из них, _v, указывает на некоторую вершину полигона — текущую позицию окна полигона. Большинство операций с полигонами относятся либо к текущему окну, либо к вершите в окне. Иногда мы будем ссылаться на вершину в окне как на текущую вершину. Во втором элементе данных, _size, хранится размер полигона.
class Polygon ( private:
Vertex *_v; int _size; void resize(void);
public:
Polygon(void);
Polygon(Polygons);
Polygon(Vertex*);
-Polygon(void);
Vertex *v(void); int size (void);
Point point (void);
Edge edge(void);
Vertex Vertex Vertex


*cw(void);
♦ccw(void);
♦neighbor(int rotation)»
♦advance(int rotation);
*setV(Vertex*);
♦insert(Points) ;

Vertex Vertex void remove(void); Polygon *split(Vertex*) ;
Конструкторы и деструкторы
Имеется несколько конструкторов для класса Polygon, имеющий аргументов, инициирует пустой полигон
Polygon::Polygon(void) _v(NULL), _size(0)
I
)
Копирующий конструктор берет некоторый полигон рис его помощью инициализирует новый полигон. Он осуществляет полную копию, дублируя связный список, в котором хранится полигон р. Окно нового полигона по мещается над вершиной, соответствующей текущей вершине полигона р;
polygon::Polygon(Polygon £р) (
_size = p._size;
if (_size = 0)
_v = NULL;
else {
_y = new Vertex(p.point());
for (int i = 1; i < _size; i++) (
p.advance(CLOCKWISE);
_v = _v->insert(new Vertex(p.point())); )
p.advance(CLOCKWISE);
_v = _v->cw () ;
)
Третий конструктор инициализирует полигон с кольцевым двухсвязным списком вершин:
Polygon::Polygon(Vertex *v) : _v(v)
( resize();
)
Конструктор вызывает собственную компонентную функцию resize для изменения значения элемента .size. В принципе, функция resize должна вызываться всегда, когда к полигону добавляется или из него удаляется це-гбпл^л^еРШИН неизвестной длины- Эта функция определяется следующим оор ИЗ ОМ •
void Polygon::resize(void) (
if (_y = NULL)
_size = 0;
else {
Vertex *v = _y->cw() ;
for (_size = 1; v ?= _v; ++_size r
Деструктор ' Polygon освобождает вершины текущего полигона, прежде чем удалить сам объект Polygon:
Polygon::-Polygon (void)
Структуры геометрических данных
105
if (_v> (
Vertex *w = _v->cw() ;
while(_v ! = w) (
delete w->remove();
W = _v->cw();
}
delete _v;
функции доступа
Следующие несколько компонентных функций позволяют получить некоторые сведения о текущем полигоне. Функция v возвращает текущую вершину данного полигона, а функция size — его размер:
Vertex *Polygon::v(void) {
return _v;
)
int Polygon::size(void) {
return _size;
)
Указатель, возвращаемый функцией v, может быть использован в виде дополнительного окна для полигона как дополнение к неявному окну полигона. В некоторых применениях требуется одновременное использование нескольких окон для одного и того же полигона — единственного окна, обес
печиваемого в классе неявно, не всегда оказывается достаточно.
Компонентная функция point возвращает точку на плоскости, которая соответствует текущей вершине. Компонентная функция edge возвращает
текущее ребро. Текущее ребро начинается в текущей вер вается в следующей после нее вершине:
IIIU
* - *
Point Polygon::point(void) (
return v->point(); }
Edge Polygon::edge(void)
return Edge (point () , _v->cw () ->point ()) ;
Класс Edge (ребро) будет определен в следующем разделе. Компонентные функции cw и ccw возвращают ственника для текущей вершины, оставляя без изменения Функция neighbor (сосед) возвращает указатель на Явствен ника текущей вершины в зависимости от значения торым она вызывается (CLOCKWISE или COUNTER_CLOCKWISB; стрслке или против):
заканчи-
окна
с ко-
Vertex *Polygon::cw(void)
I
return _v->cw();
>
Vertex *Polygon::ccw(void)
{
return _v->ccw() ;
I
Vertex *Polygon::neighbor(int rotation) <
return _v->neighbor(rotation);
)
Функции редактирования
Компонентные функции advance и setV перемещают окно на различные вершины. Функция advance (продвинуть) перемещает окно на последователя или предшественника текущей вершины в зависимости от заданного аргумента:
Vertex *Polygon::advance(int rotation) (
return _y = _v->neighbor(rotation);
Компонентная функция setV перемещает окно на вершину v, указанную в качестве аргумента:
Vertex *Polygon::setV(Vertex *v) (
return _v = v;
}
В программе должно быть обеспечено, чтобы вершина-v принадлежала текущему полигону.
Компонентная функция insert вносит новую вершину после текущей и затем перемещает окно на новую вершину:
Vertex ‘Polygon::insert(Point 6p)
if (_size++ = 0)
_v = new Vertex(p);
_v ~ _v->insert(new Vertex(p));
return _v;
ша^Гна Xme^ZT remOVe УЛвЛЯеТ текущую	перемо-
ягаитоя	ника или остается неопределенным, если полигон ста-
новится пустым:
void Polygon::remove(void)
Гтоуктурь1 гсометримеских данных
107
Vertex *v = _v;
v = (—_size = 0) ? NULL : _v->ccw(); delete v->remove();
Расщепление полигонов
Расщепление полигонов заключается в делении полигонов на два меньших полигона. Разрезание осуществляется по некоторой хорде. Чтобы разрезать полигон вдоль хорды об, вначале внесем после вершины а ее дубликат, а дубликат вершины b внесем перед вершиной Ь (эти дубликаты вершинах а и Ьр. Этот
назовем ар и bp). Затем произведем разделение процесс проиллюстрирован на рис. 4.10.
в
Рис. 4.10. Разрезание полигона вдоль хорды ab. Текущие вершины (в окне каждого полигона) обведены кружком
Компонентная функция Polygon::split определена в терминах функции Vertex: :split. Эта последняя функция разделяет полигон вдоль хорды, соединяющей текущую вершину (которая играет роль вершины а) с вершиной Ь. Она возвращает указатель на вершину Ьр, являющуюся дублем вершины Ь:
ЩИ
Vertex *Vertex::split(Vertex *b) (	// занесение bp перед вершиной b
Vertex *bp = b->ccw()->insert(new Vertex(b->point())); insert(new Vertex(point())) }	______
// занесение ар после текущей вершины
%
splice(bp);
return bp;
Компонентная функция Polygon::split разрезает теку, вдоль хорды, соединяющей ее текущую вершину с вершШИ вращает указатель на новый полигон, окно которого размещено НОЙ Ьр, являющейся дубликатом вершины Ь. Положение окна hi полигоном не изменяется.
Polygon ♦Polygon:J split(Vertex *b)
Vertex *bp ш _v~>split( resize () ;
return new Polygon(bp);

Глава 4
108
i • .4nlit должна использоваться с некото-Компонентная функция Polygon - . р последователем текущей вер. рой осторожностью. Если вершина	полигон остается неизменным.
ХШ.НЫ _v. то после выполнения	ие являющейся хордой, то
Если разрезание производится д пплИгона» могут оказаться взаимно один или даже оба результирующих .полигоиа^мо у	полигонаМ1
пересекающимися. Если верш, соедИНению двух полигонов по двум со-то операция разделения приводит кед	прпшины
впадающим ребрам, которые соединяют эти две верил -
4.3.5.	Точки, входящие в состав выпуклого полигона
В этом и последующем подразделах будут представлены две простые программы для работы с полигонами. Программа pointInConvexPolygon обсчитывает точку s и полигон р и возвращает значение TRUE только в том случае, если точка лежит внутри полигона р (в том числе и на его границе):
bool pointlnConvexPolygon(Point &s, Polygon &p) <
if (p.sizeO = 1)
return (a == p.point () );
if (p.sizeO « 2) (
int c = s.classify(p.edge());
return ((c=BETWEEN) || (c==ORIGIN) || (c==DESTINATION)); }
Vertex *org = p.v();
for (int i = 0; i < p.sizeO; i++, p. advance (CLOCKWISE) )
if (s.classify(p.edge()) = LEFT) (
p.setV(org);
return FALSE; }
return TRUE; )
® функщш вначме анализируются специальные случаи на привад-лежность полигона р вырожденным полигонам - 1-гону или 2-гону В ^Т^окнТ'пеоемет»^006 ₽°°°ТЫ аЛГ0₽итма обходится граница полигона, поочередный анали-i я?я °Т вершиыы к соседней вершине и производится поочередный анализ взаимного положения точки s и каждого пр6пп Поскольку предполагается, что полигон р выпуклый* то точка s дежит вне £Тпе°гелТ0М СЛУЧЭе’ еСЛ” °На °КаЖеТСЯ СЛева от какого-либо узла, положение окна "в полигоне р °Р0Грамме восстанавливается первоначальное
4.3.6.	Определение наименьшей вершины в полигоне

В следующую функции? в качестве аргументов п,™
функция сравнения сто о и и май гт™ Ромеитов передаются полигон р и  полигоне р. Здесь под термине м ЗВОЛИТСЯ поиск наименьшей вершины аерхиииа.	M.Z.X"
точек » смысле, задаваемом функцией 4.р Фуикции"'''’''?, У"",”,Л0"'”""'
Структуры геометрических данных
Ю?
щает окно полигона р на эту наименьшую вершину и возвращает указатель
на эту вершину:
Vertex *leastVertex (Polygon £р, int (*cmp) (Point*,Point*)) (
Vertex *bestV = p.v();
p.advance(CLOCKWISE) ;
for (int i = 1; i < p.sizeO; p. advance (CLOCKWISE) , i++)
if ( (*cmp) (p.v() , bestV) < 0)
bestV = p.v() ;
p.setV(bestV); return bestV;
Например, для обнаружения самой левой вершины следует обратиться к функции leastvertex со следующей функцией сравнения:
int leftToRightCmp(Point *а, Point *b) {
if (*a < *b) return -1;
if (*a > *b) return 1;
return 0;
Для нахождения самой правой вершины будем использовать такую функцию сравнения:
int rightToLeftCmp(Point *а, Point *b) (
return leftToRightCmp(b, a);
В этой книге функции pointlnConvexPolygon и least Ver tex. (ЭУДОТ использоваться довольно часто. Также будут применяться и приведенные здесь две функции сравнения, другие функции сравнения будут определены по мере необходимости.
4.4. Ребра
Большинство из рассматриваемых здесь алгоритмов затрагивает прямую линии в той или иной форме. Отрезок прямой линии popi состоит из концевых точек ро и pi вместе с точками, лежащими между ними-ЖСогда Л важен порядок следования точек ро и pi, то мы говорим о отрезке прямой линии popl* Концевая точка ро является	.
ленного отрезка прямой линии, а точка pi его концом» ленный отрезок прямой линии будем называть ребром, если он представляет собой сторону некоторого полигона. Ребро направлено та внутренняя часть полигона располагается всегда справа от нег0< дсдиМг 4 ноя (направленная) прямая линия определяется лона от первой точки ко второй. Луч является полубесконечной прямо ли Нией, начинающейся в точке начала и проходящей через вторуЮЯ|ЯШш
-..Я
110
4.4.1.	Класс Edge (Ребро)
Класс Edge, применяется для представления всех форм прямых линий. Он определяется следующим образом.
class Edge (
public:
Point org;
Point dest;
Edge(Point S_org, Point S_dest);
Edge(void);
Edge Srot(void);
Edge (flip(void);
Point point(double);
int intersect(Edgefi, doubles);
int cross(Edges, doubles);
bool isVertical(void);
double slope(void);
double у(double);
>;
Концевые точки начала и конца хранятся в элементах данных org и dest соответственно. Эти элементы инициализируются конструктором Edge:
Edge::Edge(Point S_org, Point S_dest) : org(_org), dest(_dest)
( )
Полезно также иметь конструктор для класса Edge, не требующий аргументов:
Edge::Edge(void) :
org(Point(0,0)), dest(Point(1,0)) { )
4.4.2.	Поворот ребер
Под термином поворот ребра подразумевается вращение ребра на 90 градусов в направлении по часовой стрелке вокруг его средней точки. Два пос-ледова ильных поворота ребра приводят к его перевороту, поскольку при этом происходит смена направления ребра на противоположное. Три последовательных поворота ребра соответствуют повороту ребра на 90 градусов в направлении против движения часовой стрелки вокруг его середины. Четыре последовательных поворота оставляют ребро неизменным. Это иллюстрируется рис. 4.11.
На рис. 4.12 показано, как путем поворота ребра at> формируется ребро 5 * вектор а — (х, у), то вектор п, перпендикулярный вектору «РеДвЛЯеТСЯ КЯК Г ~	~Х}' Средняя точка ™ между точками а и b
определяется как /п=1<а + 6). Тогда точки с и d определяются как реаЛИЗустся компонентной функцией rot
гауктуры геометрических данных
111
dest - org;
Edge SEdge::rot(void)
1 point m - 0.5 * (org + dest); point Point org =
dest = m + 0.5 * n; return *this;
m - 0.5 * n
ь
Рис. 4.12. Применение векторов при повороте ребра ab. Повернутое ребро cd имеет c = m-|n и c=m+|n

Заметим, что функция rot является разрушающей. Она изменяет Ду-щее ребро вместо того, чтобы формировать новое ребро.
; функции rot может на-
Щает ссылку на текущее ребро, поэтому обращение К ф! стоять непосредственно в более сложных выражениях, i пример, записать следующее краткое определение комп И ip для изменения направления текущего ребра на об]
Глава 4
Edge AEdge::flip(void) {
return rot().rot(); }
При таком определении компонентной функции flip первое обращение к функции rot (слева от оператора доступа к элементу) поворачивает текущее ребро, а затем второе обращение поворачивает его еще раз.
4.4.3.	Определение точки пересечения двух прямых линий
Бесконечная прямая линия ab, проходящая через две точки а и Ь, может быть записана в параметрической форме как
P(t) = a +t(b - а)
(4.2)
где параметр t может принимать любое значение в диапазоне вещественных чисел (если значения t ограничены диапазоном 0 < t < 1, то уравнение 4.2 описывает отрезок прямой линии а5). Параметрическая форма описания прямой линии устанавливает соответствие между вещественными числами и точками на прямой линии. На рис. 4.13 показаны точки на прямой линии, соответствующие различным значениям параметра t.
Компонентные функции Edge::intersect и Edge: :point предусмотрены для совместной работы при поиске точки пересечения двух бесконечных прямых линий е и f. Если е и f являются объектами класса Edge, то фрагмент программы
double t;
Point р;
if (е.intersect(f,t) == SKEW) p = e.point(t);
определяет t как параметрическое значение (вдоль прямой линии е) точки, в которой пересекаются прямые линии е и f и затем эта точка р становится текущей точкой. Функция intersect возвращает значения типа перечис-гпТТтмрпр (ШХКЛОН)’ если пР^ые линии пересекаются в некоторой точке, OLL.NEAR, если прямые лилии коллинеарны, или PARALLEL, если они па-Компоневтаая функция point обрабатывает параметрическое , 'Х . ё ‘ в03вРащает соответствующую точку. Задача решается обраще-“^Х₽к„ИН“,Н“М ФУВКЦИЯМ Вместо °®₽в'«™ия к одной общей значение* точки пепелИНОГДа ыам Достаточно определить параметрическое значение точки пересечения, а не саму точку пересечения.
Ьс. 4.1 X Йпяимс тем» ив гстлюй
'*•**' лроходяиир -егрет го<и а и b
Структур*1
геометрических данных
111
реализация компонентной функции
параметра t подставляв™ в параметр.^ урХпи^
Point Edge: :point (double t) (
return Point (org + t * (dest - org));
Реализация компонентной функции intersect основана на записи скалярного произведения а • b для двух векторов а = (х0, уа) и b = (хь, уь), которое определяется как а b = хахь + уауь- Скалярное произведение имеет несколько важных свойств, включая основные:
1,	Если	а, b	и с — векторы, то а	b - b •	а и
2.	а (Ь	+ с) =	а  b + а • с = (Ь + с)  а.
3.	Если	s —	скаляр, то (sa) • b = s(a	• b) и	а • (sb) = s(a	•	b).
4.	Если	а —	нулевой вектор, то а •	а = 0,	в противном	случае a • a > 0.
5.	||a||2 = а • а.
На основании этих основных свойств можно получить очень важное свойство, на котором основан целый ряд операций с прямыми линиями: два вектора а п b перпендикулярны, если, и только если их скалярное произведение равно нулю а • b = 0. Чтобы доказать это положение, заметим, что два вектора а и b перпендикулярны, если, и только если
Ila - 6|| = ||а + Ь|
Это проиллюстрировано на рис. 4.14а. Возводя в квадрат правую и левую части, получим
(а - Ь) • (а - Ь) = (а + Ь) • (а + Ь)
Учитывая ранее упомянутые свойства с 1 по 3, получим после раскрытия скобок
a«a-2a-6 + &’b = <i,fl + 2a-d + &,b
или, сокращая подобные члены,	.. иу
а На - Ы1
Ис-4.14. Угол между векторами а и Ь:
(а) - ровен 90 градусов, если II а - Ы1  II а + Ы1;
(6) - меньше 90 градусов, если II а - Ы1 < II а + b II;
(в) - больше 90 градусов, если II а - Ы1 > II а + b II;
11а +
I
4a • b = 0
отсюда
ab = 0
Следовательно условие a • b = 0 будет удовлетворяться, если, и только если векторы а и b перпендикулярны.
Мы можем сказать даже больше. Если угол между векторами а и b меньше 90 градусов, то ||а - д|| < ||а + &|| (рис. 4.146). Подобные аргументы могут быть использованы для того, чтобы показать, что это эквивалентно условию а b > 0. Аналогично можно доказать, что угол между векторами а и Ь будет больше 90 градусов, если, и только если а • b < 0 (рис. 4.14в). Эти резуль-таты обобщены в следующей теореме:
Теорема 2. (Теорема о скалярном произведении). Пусть а и b — векторы и 0 — угол мекду ними. Тогда
а • b
0, если, и только если 0
90 градусов.
Теорема о скалярном произведении может быть использована для нахождения точки пересечения двух прямых линий db и cd . Если прямая линия db описывается уравнением = а<+ ^(6 - а), то будем искать значение t такое, что прямые линии ab и cd пересекаются (в точке P(t). Поскольку вектор Р(/) - с должен .совпадать с прямой линией cd , то и вектор Р(£) - с и прямая линия cd должны быть перпендикулярны одному и тому же вектору п. Следовательно, на основании теоремы о скалярном произведении нам необходимо решить относительно t уравнение
п • (Р(£) - с) = 0	(4.3)
I
Поскольку Р(о = а + ЦЬ - а), уравнение 4.3 можно переписать в виде
п • ((а + t(b - а)) - с) = 0
Учитывая основные свойства скалярного произведения, получим
п • (а - с) + п • (t(b - а)) = 0
Выводя за скобки параметр t, имеем
п  (а - с) + £[(п • (Ь - а)] = 0
Откуда следует
Уравнение 4.^ удовлетворяется, если линии db к cd не параллельны точке. Если две прямые линии
п • (а - с)
------—------п • (Ь - а) * 0 п • (Ь - а)
(4.4)
и только если бесконечные прямые и они пересекаются и единственной параллельны или совпадают, то этот фякт вия п • (Ь - а) = о, поскольку оба лектора
Структуры геометрических данных
115
(b - и (d пеРпенДикУлярны одному и тому же вектору п. В результате получаем следующую реализацию компонентной функции intersect*.
enum { COLLINEAR, PARALLEL, SKEW, SKEW_CROSS, SKEWING CROSS };
int Edge:-.intersect (Edge &e, double it)
(
Point a = org;
Point b = dest;
Point c = e.org;
Point d = e.dest;
Point n = Point((d-c).y, (c-d).x);
double denom = dotProduct(n, b-a) ;
if (denom = 0.0) {
int aclass = org.classify(e) ;
if ( (aclass=LEFT) | | (aclass=RIGHT))
return PARALLEL;
else
return COLLINEAR;
double num = dotProduct(n, a-c) ;
t = -num I denom;
return SKEW;
)
Реализация функции dotProduct (скалярное произведение) очень проста:
double dotProduct(Point 4p, Point &q) {
return (p.x * q.x + p.y * q.y) ;
)
Компонентная функция Edge::cross возвращает значение SKEW_CROSS (наклонены и пересекаются), если и только если текущий отрезок прямой линии пересекает отрезок прямой линии е. Если отрезки прямой пересекаются, то возвращается значение параметра t вдоль этого отрДиа прямой линии, соответствующее точке пересечения. В противном случае функция возвращает одно из следующих подходящих значении СОЩНЕДК (коллинеарны), PARALLEL (параллельны) или SKEW_NO__CROSS ны, но без пересечения).	 "• •*
int Edge::cross(edge &e, double &t) < double s;
int crossType = e.intersect(*this, a); _«e«TT-T4i if ((crossType=COLLINEAR) II (crossType=PARALLEL)) return crossType;
if ((s < 0.0) || (s > 1.0)) return SKEW_NO_CROSS; intersect(e, t);
if ((0.0 <» t) && (t <= 1.0)) return SKEW_CROSS;
- -iaM.1 о
else
return SKEW NO_CROSS; I

4.4.4. Расстояние от точки до прямой линии
n -nt---distance иллюстрирует некоторые из Определение функции Pointy онентную функцию класса Point только что описанных свойств, ь э У ие расст0Яния от текущей точки передается ребро е и в°звра1?*е™ ном расстОяние от точки р до ребра до ребра е со знаком. Здесь под Р Р до некоторой точки на е понимается минимальное расстояние от
е. Расстояние со знаком бесконечной прямой линии, определяем р ава от ребра е, отрицатель-будет положительным, если точ р	либо нулевым, если точка р
ным, если точка р лежит слева Р Р
принадлежит прямой	определяется следующим образом:
Компонентная функция distan - р
double Point:‘.distance (Edge &e) (
Edge ab = e;
ab.flipO .rot() ;
Point n(ab.dest - ab.org);
n = (1.0 / n. length ()) * n;
Edge f(*this, *this + n);
double t;
f.intersect(e, t);
return t;
)
// поворот ab на 90 градусов
// против часовой стрелки
// п = вектор, перпендикулярный
// ребру е
// нормализация вектора п
// ребро f = п позиционируется
// на текущей точке
// t = расстоянию со знаком
// вдоль вектора f до точки, // в которой ребро f пересекает // ребро е
е, имеет единичную точки е.
В функции сначала формируется единичный вектор п такой, что он перпендикулярен ребру е и направлен влево от ребра е. Затем вектор п переносится до совпадения его начала с текущей точкой, образуя ребро f. И, наконец, вычисляется параметрическое значение точки пересечения ребра f с ребром е. Поскольку ребро f перпендикулярно ребру с, длину и начинается в текущей точке, то параметрическое значение t пересечения равно расстоянию со знаком от текущей точки до ребра
4Л.5. Дополнительные утилиты
Последние три компонентные функции класса Edrp- введены для ства. Компонентная функция isVettical возвращает значение TRUE (истина) только в том случае, если текущее ребро вертикально:
bool Edge::i«V«rtical (void) (
удоб-
Компонентная функция -slop»- возвращает величину наклона текущего ЯЛЖ значение LF:L MAX, если текущее ребро вертикально:
структуры геометрических данных
117
double Edge: : slope (void)
1 if (org.x != dest.x)
return (dest.y - org.y) / (dest.x -return DBL_MAX;
Для компонентной функции у задается значение х и она возвращает значение у, соответствующее точке (х, у) на текущей бесконечной прямой линии. Функция действует только в том случае, если текущее ребро не вертикально.
double Edge: :у (double х) (
return slope () * (х - org.x) + org.y; I
4.5.	Геометрические объекты в пространстве
illlj
Хотя в основном мы будем проводить исследования объектов на плоскости, несколько последующих разделов этой книги будут посвящены объектам в трехмерном пространстве. В данном разделе представим классы Point3D, Triangle3D и Edge3D для работы с точками, треугольниками п ребрами, расположенными в пространстве. Определение классов будет дано очень схематично, что обеспечивает лишь те функциональные свойства, которые нам будут необходимы. Более того, ради краткости многие из компонентных функций будут определены внутри определения их классов с немногословным объяснением их работы. Это не должно помешать ясности изложения, поскольку большинство аналогичных концепций уже объяснялось в связи с двухмерными объектами на плоскости, более детально будут описаны лишь новые ситуации.
нШ
4.5.1.	Точки
, ' .ч' •
В декартовой системе координат точка в трехмерном пространстве представляется упорядоченной триадой (х, у, z) вещественных чисел. Класс Point3D содержит компонентные данные х, у и z для хранения координат точки, конструктор, операции-функции для выполнения основных операции с векторами, операцию-функцию [] для обеспечения доступа к координатам, компонентную еще одну функцию кости.

функцию для вычисления скалярного произведения и для определения положения точки относительно плос-
claas Point3D ( Public:
double x;
double y;
double z;
Point3D(double
х, double _у, double __«) :
Point3D(void)
о
Point3D operator* (Point3D ip)
{ return Point3D(x + p.x, у * p.y, x + p.x); }
Point3D operator- (Point3D fip)
{ return Point3D(x - p.x, у - РУ» » “ P-x); 1 friend Point3D operator*(double, Point3D fi);
int operator= (Point3D fip)
{ return ((x = p.x) fifi (y == p.y) fi& (x = p.x)); ) int operatorI=(Point3D fip)
{ return !(*this == p) ; ) double operator[](int i)
( return ((i = 0) ? x : ((i = 1) ? у : x)) ; ) double dotProduct(Point3D fip)
( return (x*p.x + y*p.y +• z*p.z); } int classify(Triangle3D fit);
};
Скалярное умножение реализуется функцией
Point3D operator* (double s, Point3D fip) (
return Point3D(s ♦ p.x, s * p.y, s * p.z);
)
Компонентная функция classify сообщает, с какой стороны плоскости, задаваемой треугольником t, располагается текущая точка. Ее определение дается в следующем подразделе.
4.5.2.	Треугольники
Треугольник полностью описывается своими тремя вершинами. Для работы с треугольниками в пространстве для каждого треугольника кроме его вершин полезно отслеживать ограничивающий параллелепипед и его вектор нормали. Ограничивающий параллелепипед геометрического объекта — это наименьший параллелепипед, охватывающий объект, со сторонами, параллельными главным координатным осям. На рис. 4.15 приведено несколько примеров.
Вектор, перпендикулярный заданной плоскости Р называется нормалью к Р. Для любых заданных трех неколлинеарных точек р0, р} и р2, лежащих в плоскости Р, нормаль к Р определяется векторным произведением axt> для векторов а = Р1-Р0 „ Ь = Рг-Р0. Пусть а = (хо. Уа. ад я О - [ХЬ. Уь. гь), тогда векторное произведение векторов определяется как
а х д = (уагь - 2ауь, гахь - хагь, xayb - уаХЬ)	(4.5)
П|гонэввдеиие ««торов а и Ь вычисляется и возвращается
Foint3D «©..Product (Point3D 4а, Point3D fib) return Point3D(«.у ♦ b.z -	• b v
•-» • b.x - а.х . ь.ж, ex • b.y - у • b x).
Структуры геометрических данных
11»
(а)	(6)	()
Рис. 4.15. Ограничивающий прямоугольник (а) для кривой на плоскости; (6) для треугольника на плоскости;
(в) ограничивающий параллелепипед для треугольника в пространстве
Чтобы доказать, что вектор векторного произведения а х b перпендикулярен плоскости, определяемой векторами а и Ь, необходимо только показать, что а • (а х Ь) = 0 и b • (а х &) = 0. Мы имеем
а • (а X &) = (Ха, Уа, 2а)  (уа2Ь - 20уь, 2aXb - Ха2Ь, Хауь “ УаХЬ) = 0
поскольку все члены равны нулю. Равенство b • (а х Ь) = 0 доказывается аналогично.
Направление вектора векторного произведения показано на рис. 4.16. Треугольник ДОаЬ ориентирован положительно, если на него смотреть из точки а х b в пространстве. Вектор нормали, имеющий такую же длину, но противоположное направление, определяется как -а х b = Ь х а .
Заметим, что если векторы а и b лежат в плоскости ху, то длина вектора их векторного произведения равна ||а х Ь|| = |хауь - Уа*ь| — площади параллелограмма с вершинами 0, а, b и а + Ь.
.►Ajrfc
Рис 4.16. Векторное произведение а х b векторов а и b
Обсудив понятия ограничивающего параллелепипеда и Дать определение класса Triangle3D:
с^ави Triangle3D { Prxvato:
Point3D _v[3];
Edge3D _boundingBox;
Point3D _n;
public:
int id;
int mark;	.	,
Triangle3D(Point3D fivO, Point3D fcvl, Point3D fiv2, int id);
Triangle3D(void)
()
Point3D operator[] (int 1)
( return _v[i]; )
Edge3D boundingBox()
( return _boundingBox; )
Point3D n(void)
( return _n; }
double length(void)
{ return sgrt(x*x + y*y + z+z) ; ) );
Вершины текущего треугольника хранятся в массиве _v. Ограничивающий параллелепипед представляется в виде ребра _boundingBox между углом с минимальными значениями координат и углом с максимальными значениями координат. Единичная нормаль к треугольнику записывается в элементе данных _п, она равна вектору векторного произведения (_v[l]-_v[0]) х (_v[2]-_v[0]), деленному на его длину. Элемент данных id является идентификатором этого треугольника.
Первый конструктор Triangle3D требует определения макрофункций глахЗ и min3 для выбора наибольшего и наименьшего числа из трех чисел:
fdefine min3 (А, В, С )	\\
((А)<(В) ? ((А) < (С) ? (А) : (С)) ; ((В)<(С)?(В) : (С)))
fldefine тахЗ (А, В, С )	\\
((А) > (В) ? ( (А) >(С) ? (А.) ; (С)) : <(В)>(С)?(В) : (С)))
Triangle3D::Triangle3D(Point3D fivO, Point3D £vl, Point3D &v2 int id)	’
(
id = _id;
mark = 0;
v[0] = v0;
_v[l] = vl;
_v[21 = v2;
_boundingBox.org.x = min3(v0.x, vl.x, v2.x) •
-boundingBox.org.у = min3(v0.y, vl.y, v2*y);
_boundingBox.org.z =min3(v0.z, vl.z, v2.z);
—boundingBox.dest.x « max3(v0.x, vl.x, v2 x) •
.boundingBox. de st. у =. max3(v0. у, vlV v2 v) •
.boundingBox.dest.z = max3(u0.z, vl.z, v2 z) •
_n • crossProduct(vl - v0, v2 - v0);
_n « (1.0 / —n.length()) • n;
представляет собой объект Trinngl
Вершины объекта TriangLel' _	_
i ** » то I f> J соотпетстпугт rro перлон
доступны через оперпцию-функцию [1»
Структуры геометрических данных
111
вершине. Доступ к ограничивающему параллелепипеду и единичному вектору нормали осуществляется через компонентные функции boundingBox и соответственно. Элементы геометрических данных объявлены как собственные, так что такое определение класса обеспечивает его надежность.
Плоскость, заданная треугольником, делит все трехмерное пространство на два полупространства. Полупространство, на которое указывает вектор нормали треугольника, называется положительным полупространством треугольника, поскольку треугольник кажется положительно ориентированным, если на него смотреть из этого полупространства. Другое полупро-
странство называется отрицательным полупространством треугольника.
Имея определение класса Triangle3D, можно приступить к определеь

компонентной функции Point3:: classify. Напомним, что эта функция ус
танавливает, в каком полупространстве относительно заданного треугольника р лежит текущая точка. Функция возвращает значение POSITIVE или NEGATIVE в зависимости от того, лежит ли точка в положительном или отрицательном полупространствах плоскости р. Возвращается значение ON, если текущая точка лежит в плоскости, определяемой р:
«define EPSILON1 1Е-12 enum ( POSITIVE, NEGATIVE, ON };
int Pointsclassify(Triangle3 &p) (
Point3 v = *this - p[0);
double len = v.length();
if (len =0.0)
return ON;
v = (1.0 / len) * v;
double d = v.dotProduct(p.n(>);
if (d > EPSILON1) return POSITIVE;
else if (d < -EPSILON1)
return NEGATIVE;
else
return ON;
Вектор v представляет собой напре—* о=п₽
торый начинается в некоторой то 1ке (р	Пля ппинятия ре-
в точке, которая должна быть классифицирована C*tbxs). ,Цля принятая ре шения следует	’ и векторомп нормали
зволяет определить, будет ли угол ме ДУ ,	.	..«
плоскости меньше, равен или больше гр *	^г± располага-
В функции предполагается, что плоско	лежащяе внутри этого
ется внутри слоя толщиной 2 Е -	*	сделаво для того, чтоАр
слоя, считаются принадлежащими	огоаничений Мредстав-
избежать возможной ошибки ления значений координат точки, р ад	,
классифицированы как находящиеся вне	,,
4.5.3.	Ребра
Класс Edge3D определен следующим образом:
class Edge3D (
public:
PointlD org;
Point3D dest;
Edge3D(Point3D fiorg, Point3D &_dest) org(_org), dest(_dest) ()
Edge3D(void) ()
int intersect(Triangle3D fip, double fit) ;
Point3D point(double t);
>;
Первый конструктор инициализирует ребро с концевыми точками начала и конца, хранящимися в элементах данных org и dest. Компонентные функции intersect и point играют ту же роль, что их аналоги в классе Edge. Функция intersect находит значение параметра для точки на бесконечной прямой линии, определяемой текущим ребром, в которой эта прямая линия пересекает плоскость треугольника р. Если такая точка пересечения прямой линии и плоскости существует, то нвйденное параметрически значение передается через параметр t и функция возвращает значение SKEW (наклон), в противном случае могут быть выданы возвращаемые значения PARALLEL или COLLINEAR. Как и ее аналог Edge: :intersect компонентная функция intersect работает на основе уравнения 4.4:
int Edge3D::intersect(Triangle3D fip, double fit) (
PointlD a = org;
Point3D b = dest;
Point3D c = p[0]; fl произвольная
Point! n = p.n() ;
double denom » n.dotProduct(b - a); if (denom — 0.0) (
int aclass - org.classify(p);
if (aclass ! = ON) return PARALLEL;
else
точка на плоскости
return COLLINEAR,
double num = n.dotProduct(a - c); t = -num / denom;
return SKEW;
)
Компонентная функция point возвращает точку на текущей прямой линия, соответствующую параметрическому значению t ;
Point3D Edge3D:;point(double t) (
return org ♦ t ♦ (dest - org) •
*
геометрических данных
4,6.	Определение пересечения прямой линии и треугольника
В в в
В этом разделе мы обсудим решение одной интересной проблемы с использованием уже описанных средств - проходит ли прямая линия в пространстве сквозь треугольник, Решение такой задачи окажется очень полезным в последующем изложении.
Проекцией называется отражение пространства более высокого порядка па пространство более низкого порядка. Проекция часто используется для преобразования задачи в пространство более низкого порядка, где существуют способы ее решения. Рассмотрим проблему. Существует задача: определить, проходит ли данная бесконечная прямая линия внутри заданного треугольника р. На рис. 4.17 показан один из подходов к решению этой задачи. Вначале вычисляется точка q, в которой бесконечная прямая линия пересекает плоскость треугольника р, Затем выполняется ортогональное проецирование треугольника р и точки q на плоскость ху, что дает треугольник р' и точку q'. Получающаяся задача в двухмерном пространстве — принадлежит ли точка q' треугольнику р’ — эквивалентна исходной задаче: ответ для двухмерного пространства будет да, если и только если ответ да будет исходном пространстве. Преимущество такого преобразования заключается том, что поиск решения в двухмерном пространстве гораздо проще, чем исходном трехмерном пространстве.
При проецировании иногда возникает неоднозначность и из-за потери ин
формации могут возникнуть сложности. Например, на рис. 4.176 два треугольника вырожденно проецируются на плоскости ху в один и тот же отрезок прямой линии. Нетрудно видеть, почему эта двухмерная задача не эквивалентна исходной задаче. Для двух заданных треугольников р\ и рг» проецирующихся в одпн и тот же отрезок прямой линии, и бесконечной прямой линии С в пространстве, существуют две разных задачи в трехмерном пространстве (одна относится к определению взаимного соотношения £ и pi, другая — к £ и рз) и обе преобразуются в одинаковую задачу в двухмерной плоскости ху. Если, скажем, прямая линия I проходит сквозь треугольник pi, но не проходит сквозь треугольник р2, то решение двухмерной задачи в одном из двух случаен даст неправильный результат.
Чтобы оставить алгоритм решения задачи неизменным, перед проецированием необходимо выполнить проверку на вырождение. Треугольник р проецируется в отрезок прямой линии на плоскости ху, если вектор нормали п треугольника перпендикулярен оси г. Этот тест должен быть выполнен до проецирования и, если окажется, что вектор нормали перпендикулярен оси г, то проецирование будем выполнять на плоскость Если эта проекция также окажется вырожденной, то будем использовать проекцию па плоскость 2Х. Поскольку вектор нормали не может быть перпендикулярен одновременно всем трем осям, по крайней мере одна на проекций будет невырожденной.
Алгоритм реализуется в следующей функции, в которой возвращаемое значение типа перечисления PARALLEL, COLLINEAR, SKEW_CROSS или SKEW NO CROSS показывает соотношение между бесконечной пршмсЛ^ляжМ г' и треугольником р КОпы иаютцео, что
[.ц> СКО’ЛЗ,
реализуется в следующей функции, в которой возвращаемое - перечисления PARALLEL, COLLINEAR, SKEW_CRO3S или
I, Если функция возвращает значение SKEW_CROSS, по», прямая линия проходит внутри треугольника,, ха показывающее, что прямая линия пересекает плоскость:
I
Рис, 4.17. (a) - будет ли прямая линии проходить внутри треугольника р?; (6) - оба треугольника вырожденно проецируются в один и тот же отрезок прямой линии
треугольника, но точка пересечения лежит вне треугольника, то параметрическое значение точки пересечения, лежащей на прямой линии е, передается через ссылочный параметр t:
int lineTrianglelntersect (Edge3D fie, Triangle3D fip, double fit) (
Point3D q;
int aclass - e.intersect(p, t);
if ((aclass==PARALLEL) | | (aclass=COLLINEAR) ) return aclass;
q = e.point(t);
int h, v;
if (p.n().dotProduct (Point3D(0, 0, 1)) != 0.0) { h = 0;
v = 1;
) else if (p.n().dotProduct(Point3D(1, 0, 0)) != 0.0) { h = 1;
v = 2;
) else (
h = 2;
v = 0; )
Polygon *pp = project(p, h, v);
Point qp a Point(q[h], q[v]);
int answer = pointlnConvexPolygon(qp, *pp) ;
delete pp;
.	return (answer ? SKEW CROSS : SKEW NO CROSS) :
i	—	—	—
При обращении к функции project (р, h, v) возвращается полигон, представляющий собой проекцию треугольника р на плоскость hv. Аргументы h и v являются индексами осей: например, оператор project, (р, 0, 1) определяет проецирование треугольника р на плоскость ху. В функции project
СМ»
руруктуры геометрических данных
1М
предполагается, что проекция треугольник, р пе шарождева, иоатому его що-екиия также является треугольником. Функция определяется следующим об-разом*
polygon *project(Triangle3D Sp, int h, int v) {
// проецирование вершин треугольника p Point3D a;
Point pts[3];
for (int i = 0; i < 3; i++) ( a = p.v(i) ;
pts[i] = Point (a[h], a[v]); I
// сначала включение в полигон двух спроецированных вершин Polygon *рр = new Polygon;
for (i = 0; i < 2; i++) pp->insert(pts[i));
// включение в полигон третьей спроецированной вершины if (pts[2].classify(pts(0), pts[l]) «LEFT)
pp->advance(CLOCKWISE);
pp->insert(pts[2]); return pp;
I
Здесь есть очень интересный момент, заключающийся во включении последней из трех проецируемых вершин (pts (2]) в состав полигона при его формировании. Если три проецируемые вершины ориентированы отрицательно, то pts [2] включается после pts[l], если же они ориентированы положительно, то pts [2] включается после pts[0). Этим обеспечивается, что внутренняя часть результирующего полигона рр лежит всегда справа от каждого образующего его ребра — так что последующее обращение к оператору pp->advance (CLOCKWISE) соответствует обходу по часовой стрелке. Проталкиванием окна для полигона рр, если проецируемые вершины ориентированы положительно, и обеспечивается выполнение этой операции.
4.7.	Замечания
I
Большинство математических операций, описанных в этой главв^ Относится к элементам линейной алгебры» Векторы являются элементами вдгеб-раической структуры, известной под названием векторного .пространства. Хотя большинство аспектов линейной алгебры подразумевает геометрическую интерпретацию (наше представление и было направлено именно на такую интерпретацию), все результаты линейной алгебры могут быть получены на основе обычной алгебры, не обращаясь к геометрическому представлению. Введение в линейную алгебру содержится в книгах
В ряде других книг описываются геометрические свойства и рабоМЖЖЦрф программы для их использования в геометрических алгоритмах [3> ЗОв-ЛЖ 66. 731. В них можно найти обоснование некоторых идей, изложенных в Данной главе.	’•*
4.8.	Упражнения
1.	Показать, что выражение хауь - хьУа равно площади (со ^знаком) да-раллелограмма, определяемого векторами а — (ха, у а) и — (хъ, уъ),
2.	Для заданных ненулевых векторов а и b показать, что а • b = ||а|| Цд|| cos 0, где 0 - угол между векторами а и Ь.
3.	Показать, что sin (а - 0) = sin а cos 0 — cos а sin 0
4.	Показать, что выпуклый полигон с вершинами У1, ..., Uk состоит из набора точек вида р = итог + ••• + O-kVk, где oti + ... + ctk — 1 и каждый oq > О (это выражение известно как выпуклая комбинация точек i?l, ..., 1>л).
5.	Показать, что теорема о скалярном произведении остается справедливой для векторов в трехмерном пространстве.
6.	Почему копирующий конструктор Polygon:: Polygon (Polygons) выполняет полную копию? (Подсказка: что плохого в том, что два объекта типа полигона ссылаются на один и тот же связанный список вершин?).
7.	В чем заключаются преимущества и недостатки представления различных видов прямых линий (бесконечных прямых линий, отрезков прямых линий и лучей) при использовании единственного класса Edge?
8.	Напишите версию функции Polygon: :split, в которой осуществляется проверка ошибочных ситуаций.
9.	На основе операции расщепления двухсвязного кольцевого списка напишите (разрушающую) функцию join (Polygon &р, Polygon &q), которая сливает полигоны р и q в один и возвращает указатель на новый полигон.
10.	Напишите функцию, которая будет проверять полигон на выпуклость.
11.	Дайте структуру данных для представления выпуклого л-гона, позволяющую определить за время O(log п) принадлежность полигону заданной точки.
12.	Напишите функцию для определения, будет ли хордой заданная диагональ в заданном полигоне.
13.	Напишите функцию, которая будет определять, не является ли объект класса Polygon недопустимым л-угольником, пересекающим сам себя. ( евидный подход, который проверяет все пары ребер, занимает время (л ). Подумайте об алгоритме, на выполнение которого требуется время О(п log л).
14.	Придумайте алгоритм определения принадлежности точки произвольному (выпуклому или не выпуклому) л-угольнику, работающий за время
15.
алгори™' Ра6этающий за время Ofnlogn). для опреде-размеров	пересечения двух полигонов, где н равно сумме их
ы геометрических данных
117
16	ПриДУма”те алгоритм Для определения взаимного пересечения двух выпуклых полигонов, выполняемый за время О(п), где л равно сумме размеров этих полигонов.
17	Напишите функцию для нахождения точки пересечения прямой линии и треугольника в пространстве без использования их проекций на плоскость.
18	Напишите функцию для определения факта пересечения двух треугольников в пространстве.

ч
f
-•i
Л&.
Часть II
Применение
Пошаговый ввод
Г
Существует особый подход к разработке алгоритма ввода, носящий название пошаговый ввод, при котором весь процесс ввода сводится к обработке вво-димых элементов по одному, сохраняя принятое состояние всех ранее введенных элементов. На каждом следующем шаге анализируется и обрабатывается очередной вводимый элемент и текущее состояние модифицируется для восприятия нового элемента. После обработки всех введенных элементов задача оказывается выполненной.
Здесь можно привести аналогию с детективным романом. Читателю сообщается рабочая гипотеза о потенциальном убийце и как и почему это убийство произошло. Каждая новая улика либо подтверждает гипотезу, либо требует ее уточнения, либо вообще исключения ее и выдвижения новой. К
ш

концу книги, когда приведены все улики, читатель сможет раскрыть преступление, если он или она достаточно умны, а писатель был талантлив.
Может оказаться, что в некоторых случаях алгоритм способен поддерживать только текущее состояние, в противоположность текущему решению, поскольку введенные на текущий момент данные могут оказаться далеко не полными, чтобы представить логически связную ситуацию. Например, такая ситуация часто возникает при решении задач с полигонами: если для задания границы полигона вершины вводятся по одной, то нельзя анализировать даже простой полигон, пока не будут введены все вершины. Возвращаясь к нашей аналогии с детективной историей, этот пример может быть аналогичен ситуации, когда мнение читателя формируется еще до того, как произошло убийство — хотя еще и нечего раскрывать, но уже существуют улики и подозрения как первые сигналы для тревоги.	4,1
Наиболее очевидный способ поиска наименьшего целого числа в массиве заключается в пошаговом просмотре массива, с сохранением ранее полученной последовательность наименьших чисел — это вычислительный пример пошагового ввода. Процесс ввода включает условное присвоение значения переменной, хранящей текущий минимум. На каждом этале эта перел содержит решение проблемы на основании тех чисел, которые уже bi и обработаны. Однако в общем случае процесс пошагового ввода -НЯ! так прост. В этой главе будет рассмотрен ряд ситуации (иногда, весь? тересных), в которых используется рассматриваемая стратегия. Сннмал смотрим сортировку при вводе, хорошо известный способ сортировки более полезный для сортировки сравнительно небольших списков^JJJPI Остальные алгоритмы будут применяться для решения геометрия задач: определение звездчатого полигона среди конечного набора точе

и«к» для набора точек, определение пр„.
SSJbSSffb—
„уклого полигона, триангул
5.1.	Сортировка при вводе
Сортировка при вводе работает так же, как игрок в карты держит g руках карты. Вначале колода карт лежит на столе, лицом вниз. По мере взятия карт из колоды игрок располагает их в руке, помещая в опреде. ленном порядке. Перед взятием очередной карты все уже взятые карты от. сортированы по порядку.
Рассмотрим, как использовать сортировку при вводе для упорядочения элементов массива а [0],	а [п-1] в порядке увеличения. (Для крат
кости этот набор элементов будем обозначать как а [0. . .п-1].) Для каждого i от 1 до п- 1 в начале итерации i подмассив a[0...i-l] считается отсортированным. Наша задача на шаге итерации i заключается в сортировке массива а [ 0. . . i ] путем занесения элемента а [ i ] в его соответствующую позицию. Чтобы выполнить это, мы запишем элемент a[i] в некоторой переменной v и затем по очереди будем перемещать элементы a[i-l), a[i-2),... на один элемент вправо до тех пор, пока не найдем первый элемент a[j-l), не больший, чем v. Наконец скопируем значение vb «дырку», которая образовалась в позиции j. На рис. 5.1 показано, как этот алгоритм сортирует короткий массив целый чисел.
Сортировка при вводе массива из шести целых чигрл
подлежащие вводу	ц чисел Кружком отмечены очередные числа,
Алгоритм реализован шаблоном функции inserti.onSort, которая сортирует массив a[0...n-lJ. Аргумент ептр устанавливает функцию сра®,,е ния, которая возвращает значения -1, 0 пли 1, если ее первый аргумент меньше, равен или больше второго аргумента соответственно: template<class Т> void insertionsort (Т
а[], intn, int (*anp)(Т.Т))
for (int
с
133
Пг.ШДГОВЫИ ввод
while ( (j >0) && ((*cmp) (v,
Ha каждой итерации i от l до n - 1 цикл while нводит элемент a(i] а сортируемый подмассив а [0. . .1-1]. Проверка j > 0 в цикле while гарантирует, что работа программы не нарушится при достижении начального элемента массива а в процессе ввода.
В представленной здесь программе сортировки предполагается, что параметр типа Т в шаблоне является типом указателя. Тем не менее шаблон функции insertionSort, может быть использован для сортировки объектов любого типа, для которых существует оператор присвоения = и копирующий конструктор. Например, следующий фрагмент программы считывает 100 строк в массив s и затем сортирует их с помощью стандартной библиотечной программы st гору языка C++, которая сравнивает строки символов в словарном порядке.
char buffer [80] ;
char *s [ 100] ;
for (int i = 0; i < 100 ; i++) (
cin » buffer;
s[x] = new char[strlen (buffer)+1] ; strcpy(s[i], buffer);
)
insertionsort(s, 100, strcmp);
Заметим, что в этом фрагменте программы сортируется скорее массив указателей_на_строки (массив s), чем сами строки. Очень часто оказывается, что гораздо эффективнее сортировать указатели вместо самих объектов, на которые они указывают. Если объекты не очень малы (четыре байта или меньше), то сортирующая программа может перемещать указатели быстрее, чем сами объекты.
5.1.1.	Анализ
Для анализа процесса сортировки при вводе достаточно сосчитать коли-чсство обращений к функции сравнения (предполагая, что сравнение выполняется за постоянное время). Время работы функции insertionSort равно °*)= Id), где через 1(1) обозначено время, необходимое для ввода х-того элемента. Поскольку 1(1) соответствует по крайней мере х сравнениям, то !1а выполнение сортировки при вводе потребуется Т(п) = у , i = сравнении, иди около п3/2 сравнений в худшем случае. Наихудший случай фак-Т1,,<ески получается тогда, когда вводимый массив отсортирован в обратном Г10Рядке (в порядке уменьшения).
® среднем х-тый элемент сравнивается примерно с х/2 элементами, преж-д“ чем будет найдено его положение. Следовательно, в среднем в процессе г'Ртировки при вводе будет выполнено около пг/2 сравнений, т. е. вдвое Мр,1ьще> Чем в ХуДШем случае. Если входной массив вначале почти отсор-; “Рован, то ожидаемое время работы программы почти линейное, поскольку
Главеj
0 с постоянным числом элемев
„„„нем сравнивается тол ^яедовательно, сортировка при BS°enSoM жировки входного „асе™, о М.
ХдХя-" очень -pc»-тором известно, что
5 о образование звездчатого полигона
5.2.	быть разЛичным образом
- точек на плоскости м ЛппмИг>Ование каждого такого Конечный набор обра3ования полигона. т0₽ек. В
этом разделе мы единен ребрами_ д"я	полигонизацией наоо	.соединения, точек
полигона можно “»3’’"э„атой полигонизации. т° ₽в3ра6Т«оСМПО“обы волг-ить звездчатый полигон.
таким образом,
5.2.1.	Что такое звездчатый полигон?
Предположим, что точки р и q лежат внутри некоторого полигона. Були. говорить что точка , eui)„a из точки р, если отрезок прямой линия „ лежит внутр., полигона. Здесь мы предполагаем, что граница полигона Р составлена из непрозрачных стен, а его внутренность заполнена прозрачно., средой подобной воздуху. Каждая точка может видеть другую точку только в том случае, если между ними нет никаких стен. Операция видимости симметрична (если точка р видит точку q, то и точка q видит р), но Г" ™’ зитивна (если р видит q, а (см. рис. 5.2).
। не трав-q видит г, то из этого не следует, что р видит г)
Рис. 5.2. Точки р и q, как и точки q и г, видят друг друга, на прямой видимости между точками р и г нет
ОН
Рис 5.3. Полигона с выдл»енн*г ядром г», - »ып/г/кй гу,лигам, 'Г,) «с»т»77«‘Н« 'И
(•) - звездчятъМ полигон, гг)- нпзпд«тый полкан
Пошагово ввод
111
Набор точек внутри полигона, из которых видны все точки полигона, называется ядром полигона. Говорят, что полигон звездчатый (рис. 5.3), если его ядро не пустое. Полигон называется веерообразным, если его непустое ядро содержит одну или более вершин (каждая такая вершина называется корнем полигона). Каждый выпуклый полигон является веерообразным, поскольку его ядро включает несколько вершин (на самом деле все вершины). Каждый веерообразный полигон является звездчатым, поскольку его ядро не пустое.
5.2.2.	Построение звездчатого полигона
Для заданного набора S точек so, si,..., sn_j на плоскости задача заключается в выполнении звездчатой полигонизации набора S. Нетрудно видеть, что может существовать более одного такого полигона. Но мы ограничимся поиском одного такого, ядро которого содержит первую точку so-
Алгоритм работает итеративно, формируя текущий полигон из точек набора S. Вначале текущий полигон является одноугольником so. На каждой итерации i от 1 до п-1 в текущий полигон добавляется следующая точка в/. По окончании обхода всех точек получается искомый звездчатый полигон.
Чтобы ввести новую точку s, в текущий полигон, будем выполнять обход текущего полигона по часовой стрелке, начиная с вершины so- Обход совершается по часовой стрелке вдоль границы полигона до тех пор, пока не будет достигнута вершина, которая должна стать последователем точки я/, тогда точка si вставляется перед этой вершиной. Если завершен полный круговой обход и произошел возврат в точку so, тогда si вставляется перед so. Таким образом, вершина so служит в качестве стража, гарантирующего
5.4. f Ьхлросниг звездчатого полигона по набору точек
к НА оис 5.4 показано несколько моментов завершение обходя* На ри •
горитма с небольшим числ^” бывает массив s из п точек и Функция atarPolygon обра .ппрпжИт точку s ГО] •
звездчатый полигон, ядро которого содержит точку s[U].
Работы ал. в°звраща?1
Point originPt; // global: originPt = з[0]
Polygon *starPolygon(Point s[], int n) (
Polygon *p e new Polygon;
p->insert(e[O]);
Vertex *origin = p->v();
originPt = origin- >point();
for (int i = 1; i < n; i++) (
p->setV(origin);
p->advance(CLOCKWISE);
while (polarCmp(ts[i], p->v()) < 0) p->advance(CLOCKWISE);
p->advance(COUNTER_CLOCKWISE);
p->insert(s[i]) ;
) return p;
)
Какие же действия нужно выполнить при каждой итерации i, чтобы определить, в какое место границы текущего полигона вставить точку 8(? Здесь применяется правило, что вершины звездчатого полигона радиально упорядочены вокруг любой точки его ядра. Поскольку точка so должна принадлежать ядру, то определим функцию polarCmp, основанную на вычислении полярных координат точек относительно точки so (т. е. точка so считается началом координат). В этих условиях точка р — (Гр, On) считается меньше точки q = (rq, 0если (1) 0p<9g или (2) 0р = 67 и гр < г7.Прн таком упорядочении обход текущего полигона по часовой стрелке производится от больших точек к меньшим точкам.
В функцию сравнения polarCmp передаются две точки р и q, которые сравниваются по их радиальному упорядочению относительно точки
--f ’ являЮ1уе,,ся глобальной переменной. Функция возвращает зна-мриктпо па» ИЛ" « зависимости от того, будет ли ее первый аргумент р меньше, равен или больше второго
аргумента q:
int polarCmp(Point *р, Point *q)
Point vp = *p - originPt;
Point vq = *q - originPt; double pPolar = vp.polarAngle(); double qPolar = vq.polarAngle( if (pPolar < qPolar) return if pPolar > qPolar) return 1; if (vp. length () < vq lennf-ь м » if (VF.length!) >V,:i.”gS	-1
return 0;	° ' return 1;
С точки зрения функции ос •	д
точки на плоскости, Эго ппоисгпн^ ^ Т°,,ИП orlrlinPi меньше любой ДРУ1^ происходи погому> ,,то фуик1ш11 :;|,,,larAngle
ПОШАГОВЫЙ ввод
137
возврашает значение I, если текущая точка совпадает с точкой originPt. п возвращает значение в диапазоне [0, 360] в противном случае. Это позволяет использовать точку я[0] (-originPt), совпадающую с началом координат, в качестве стража. Время работы функции starPolygon составляет О(па). Итерация i требует выполнения i сравнений и должно быть выполнено л 1 иге раций (здесь анализ аналогичен анализу сортировки при вводе).
Алгоритм построения звездчатого полигона очень похож па сортировку при вводе. При работе обоих алгоритмов текущее решение, представляемое в виде упорядоченных элементов, постоянно растет до полного решения. При занесении нового элемента в текущую упорядоченную последовательность в обоих алгоритмах производится последовательный обход упорядоченного списка от большего значения к меньшему до тех пор, пока не будет найдено соответствующее место для нового элемента. Более того, оба алгоритма выполняются за квадратичное время в худшем случае.
5.3.	Поиск выпуклой оболочки
Алгоритм поиска выпуклой оболочки для набора точек, который будет рассмотрен в этой главе, значительно сложнее алгоритмов сортировки при вводе и построения звездчатого полигона. Во-первых, определение правильного положения каждого нового элемента дело более сложное. Во-вторых, иногда требуется удаление элементов из текущего решения, так что в процессе работы алгоритма текущее решение растет и сжимается.
5.3.1.	Что такое выпуклая оболочка?
Пусть S будет конечным набором точек на плоскости. Выпуклая оболочка набора S, обозначаемая как 6¥(S), равна пересечению всех выпуклых полигонов, которые содержат S. Иными словами, 6W(S) — это выпуклый полигон минимальной площади, содержащий все точки S. Существует и другое определение, утверждающее, что CH(S) равно объединению всех треугольников, определяемых точками набора S.
Вообразим, что плоскость представляет собой деревянную доску, утыканную гвоздями в каждой точке из набора S. Теперь натянем вокруг гвоздей
Невыступающие точки
^ис. 5.5. Конечный набор точек и его выпуклая оболочка

кольцо Резиновая нить образует выпуклую обо. резиновую ""^^“’кмаи на рис. 5.5. полезвыми в целом ряде геоап. Л°ЧВыпуСкль.е оболочки оквзы.»к«я •> представляют способ аппроксимШИ1, „Лс^х приложений, поскольку ок ^&M4eK. Например, при распоав,. S точек или других «'Хма может быть представлена ее выпукло!, ИНИН Образов неизвестная фирма	ек, которые затем могут быть со
„“почкой или набором выпуклы»10другого примеро мювд поставлены с набором известных фор	векоторых объектов знача,
назвать моделирование д“"'Х“"”п1,сывоется своей выпуклой оболочкой, тельно упрощается, если робот от псс11(1нциротань1 относнтельно ВЬШукл0, Точки из набора S могу	граничными точками, если они лежи
Оболочки Crt(S). Точ»и назывпюте	?	ми точками, если они ара-
„а границе выпуклой об.°Х? внешней оболочки. Граничные точки, обр.-подлежат внутренней области вн о6олочки, можно назвать ныеюрас зующие .угловые, вершины. выпу ямяются выступающими, если они «с ЮЩ1ЫШ точками. Гранин	другими точками из наоора S. Эи
лежат на одной прямой между ДВУя м_ чт0 такая схема классифика-замечания отображены на рис. о... • *	находить саму выпуклую
ПИИ точек применима и тогда, когда не тре у
оболочку.
5.3.2.	Ввод оболочки
собой специальный подход к поиску выпук* _________________________________) ввод?, точек, введен-
Ввод оболочки представляет i “ “
пГолной0™'» КОНечяого яабо₽а гочск s путем "последовательного по одной точке и поддержание выпуклой оболочки для всех ---------- -
КеТек^’Т' ТаКУЮ °б0Л0ЧКУ будам ЯЯаияать "^==. зХХГиКХ^7“че~ Т0ЧК"' п₽—ежащей S. П» выпуклой оболочке	кущая оболочка становится эквивалентной
и\клон ооолочке CW(S) п аадача оказьгвается решенной При вводе каждой новой точки s в TPRvrnvr решенноЯ’
две ситуации. В первом случае точка	оболочкУ могут возникнуть
лочке (находиться на ее граница и™	прннадлежать текущей обо-
изменять не требуется.	-ли внутри) и тогда текущую оболочку
Во втором случае точка «
модифицировать, как показан,- о ? В"' текущей оболочки и ее требуете» к показано, например, „в рис. 5.6. Через точку » могут
быть проведены две вспомогательные прямые линии, каждая из которых образует касательную к текущей оболочке. (Линия является касательной к выпуклому полигону Р, если она проходит через вершину Р и внутренняя область полигона Р полностью лежит по одну сторону от этой прямой линии). Левая (правая) касательная яг проходит через некоторую вершину ((г) текущей оболочки и лежит слева (справа) от текущей оболочки. Если наблюдатель находится в точке s л смотрит на выпуклую оболочку, то левая кясательнля будет видна слева, а правая касательная — справа.
Две вершины С и а, через которые проходят касательные, делят границу текущей оболочки на две цепочки вершин: ближняя цепочка, которая расположена ближе к точке s. и дальняя цепочка, которая находится дальше от точки s. (Ближняя цепочка расположена с той же стороны от отрезка прямой /г . что и точка я. дальняя цепочка располагается по другую сторону от отрезка ? г ). Для модификации текущей оболочки сначала необходимо найти две вершины I и г. определяющие границу между ближней и дальней цепочками. Затем мы должны удалить вершины ближней цепочки (за исключением вершин Г и г) и внести точку я в нужном месте.
Следующая программа insert ionHul 1 возвращает текущую оболочку для массива в из п точек:
Faint somePoxnt;
// global
Polygon *insertionHull (Point s[) , int n) (
Polygon *p = new Polygon;
p->inaert(s[0]) ;
for (int i=l; i < n; £♦+) (
if (pointlnConvexPolygon (s [i] , *p)) continue;
somePoint = s[i];
leastVertex (*pt closestToPolygonCmp) ; supportingLine(s[i] , p, LEFT);
Vertex *1 = p->v();
supportingLine(s[i] , p, RIGHT) ;
delete p->split(l);
p->insert(sIi]) ;
) return p;
I
uki;
На шаге 1 точка s (1 ] вносится в текущую оболочку р. Обращение к функции leastvertex перемещает окно полигона р на вершину, которая расположена ближе всего к точке s(i]. При этом осуществляется подготовка к последующему обращению supportingLine (s [i]» Р» LEFT) « переносяще МУ окно на вершину Г, через которую проходит левая касательная. При втором обращении к функции supportingLine окно перемещается на вершину
Операция расщепления split применяется для удаления полигона р.по его диагонали ТУ, отделяя таким образом ближнюю цепочку от дальней. Часть полигона, содержащая ближнюю цепочку, возвращается функцией split и Удаляется. После этого точка з [1] вставляется в полигон р, который, после наполнения функции split, состоит только из дальне *К”г’пр_рлр1НПЯ
Рассмотрим функцию supportingLine более детальН°’	некоторой
першины через которую проходит левая касательная, начнем с некоторой
будем совершать обход по чамвм ближней цепочке и затем оуд	де дойдем д0 пе|)яой
паршивы в ®л«*“ й оболочки до «х п Р	ди направленна
стрелке вдоль теку которой яе лажи	вершиЯои t, которую «,
шины о. послед01	Вершина » >	вы и (например, верш,
отрезка примой лик.» посдаяо„„тель ДЛI в Р
поиска продолжается, по-Т")' Р3аспо^я ’° Xбыть крзй»"« ТОЧКОЙ' еСЛ" °““ ЛеЖ“Т скольку вершина и
т  е в качестве аргументов задается точками я и и>. ^„кции suppottinqLin^ пппаметров типа перечисления При обраше.^ к ФУ’-“лнго„„ р „ один из парам^	((
полигон р, то а или справа), показь	Находящаяся в окне для поли
LEFT или RI	( полвГается, что веРш	’беспечивается предварительным
п8рХХ — ХХ-Г перемешает окно полигона р
<‘ - " d,
r .UWO^.<^ - еоху^р.	ISE!
' int rotation - <вМе~иГП » CLOOTISB
Xt=: >b ’ P^Ubbos (rotation) ;
int c o ь->=1а«з‘У<»' bey0№) (I (C = BETWEBH)) (
while ((c - side) H c p->advance(rotation) , a s p->v 0;
ь = p->neighbor(rotation), c = b->classify(s, *a);)
)
мой insert ion^h i'.erLtл' описанная в подразделе 4.3.6, используется програм-ближе всего к tourЛЛЯ нахо'КДРИИЯ в полигоне р вершины, расположенной ЦИЯ сравнения -1 - 4Х??^о?Г’1СЯ ° глобальной переменной somePoint. Функ-leastvertex соявнмпяр >"ЗопСгоР» вместе с которой вызывается функция ^Х?9о"еР0ТпГ ДВе ТОЧКИ Н ВЫбНРаеТ’ КаКЭЯ И3
int closestToPolygonCmp(Point *a, Point ♦b}
double dxstA = (somePoint - *a).length () double dxstB = (somePoint - *b).length() if (distA < distB) return -1;
else if (distA > distB) return 1;
return 0;
5.3.3.	Анализ
процессе m
7	Mo«" построить
чва~--^^ J''~pnMxzz°»
₽SW,! ’“^nnor. ХСЛ:Д''^ трех“C;0XK";	-чек р. ч и г.
До тех пОр * г,рпнодит к исключению
0,<и не останется один тре
угольник (рис. 5.76 г). Очевидно, что если бы эти три точки р, » и г были днесены первыми, перед всеми остальными точками, то треугольная оболочка была бы сформирована с самого начала и внесение каждой из последующих точек было бы выполнено быстрее, так как потребовалось бы только определять, что точки находятся внутри треугольника. Таким образом, порядок включения точек может очень сильно влиять на эффективность.
Следует отметить, что стоимость выполнения операций по формированию выпуклой оболочки лшло зависит от операции insert и split, используемых при включении и исключении точек в процессе отслеживания текущей оболочки. Кроме того, каждая точка может быть включена и исключена не более одного раза. Из этого следует, что общая цена операций insert и split при реализации алгоритма ограничена сверху величиной 0(л).
Аналогично и обращения к функции supportingLine сравнительно недорогие: оба обращения к ней на каждой итерации занимают время, пропорциональное длине ближней цепочки, затрачиваемое на обработку вершин в ближней цепочке, которые затем удаляются на этом же шаге. Поскольку каждая вершина может быть удалена только однажды, стоимость выполнения всех обращений к функции support ingLine в процессе работы алгоритма ограничена сверху величиной О(п).
Оказывается, что функция insertionHull тратит основное время на выполнение функций pointlnConvexPolygon и leastvertex. Для обработки /-той точки я, обращение к каждой из этих двух функций занимает время, пропорциональное числу I в худшем случае (когда выпуклая оболочка содержит i вершин). Этот случай относится ко всем точкам я/, если они являются внешними, поскольку текущая оболочка увеличивается на одну вершину при занесении каждой последующей точки. Следовательно, в худшем случае функция insertionHull работает в течение времени О(пг).	____
Несколько позже в этой книге мы рассмотрим два других алгоритма сканирование Грэхема и слияние оболочек, которые формируют выпуклую оболочку из п точек в течение оптимального времени О(п log л).
Рис. 5.7. Три точки р, q и г внесены последшили
5.4. Принадлежность
точки: метод луча
В главе 4 был рассмотрен простой алгоритм для решения проблемы прп. надлежности точки выпуклому полигону. Этот алгоритм определял, лежПт ли заданная точка внутри, вне или на границе выпуклого полигона р. оа работает по принципу поочередной проверки положения точки а относптель. но каждого ребра р. Если обнаруживается, что точка а находится в отрй. дательной области какого-либо ребра, то считается, что она находится вне полигона, в противном случае она считается принадлежащей полигону р Алгоритм использует тот факт, что внутренняя часть полигона лежит щ». ликом с одной и той же (положительной) стороны каждого ребра, так что любая точка, расположенная с другой стороны хотя бы одного ребра, не может находиться внутри полигона. Но этот алгоритм не может правильно решить задачу принадлежности точки в более общем случае произвольного полигона (когда он может быть как выпуклым, так и звездчатым). Например, на рис 5.8 показан случай, когда внутренняя часть полигона расположена с той и другой стороны от ребра, обозначенного буквой е, и поскольку точка а лежит с «другой» стороны от ребра е, то упомянутый алгоритм будет считать, что точка а находится вне полигона.
Проблема определения принадлежности точки к выпуклому полигону сводится к задаче анализа списка неотсортированных чисел — для выпуклого полигона они должны быть больше нуля или равны нулю. Для решения такой задачи следует поочередно проверить каждое число и при обнаружении первого отрицательного числа выдается ответ «нет», если такого числа не обнаруживается, то выдается ответ «да». Ответ «да» будет получен только в том случае, если каждое из ряда проверяемых условий оказывается истинным.
С другой стороны, проблема определения принадлежности точки произ вольному полигону подобна задаче определения, будет ли сумма чисел в неотсортированном списке больше нуля или равна нулю. Задача не может считаться выполненной, пока не будут просуммированы все числа. >ммированне только нескольких чисел, даже всех, но хотя бы без одного, можХЖпппПР11ВеСТИ К правильномУ результату, поскольку оставшееся число довании по * изменить ситуацию. Точно так же, при частичном иссле-удаленную ZX "тпТГ	ЧТ° ™
ребер образуют своего ^одТ^п^цТ	ЧТ? несколько последних
части полигона, и он включает otv'v^	”'СЯ далск0 °Т °':Н°
ложности точки произвольному полигонуТ0',КУ' ШеНИе ° "Р нове одного условия, рассматривающего
может быть принято только ня ос-полигон целиком.
пошаговый ввод
143
Рис. 5.9. Каждый луч, исходящий из точки а, пересекает границу нечетное число раз, а каждый луч, исходящий из точки Ь, имеет четное число пересечении с границей полигона
В этом разделе для решения проблемы принадлежности точки произвольному полигону мы рассмотрим метод трассировки луча. Предположим, что нам необходимо определить принадлежность точки а полигону р. Для этого из некоторой удаленной точки проведем прямую линию в точку а. На этом пути может встретиться нуль или несколько пересечений границы полигона: при первом пересечении мы входим внутрь полигона, при втором — выходим из него, при третьем пересечении снова входим внутрь и так до тех пор, пока не достигнем точки а. Таким образом каждое нечетное пересечение означает попадание внутрь полигона р, а каждое четное — выход из него. Если мы попадаем в точку а с нечетным числом пересечений границы полигона, то точка а лежит внутри полигона, а если получается четное число пересечений, то точка находится вне полигона р. Например, на рис. 5.9 луч Гд пересекает границу только однажды, поскольку единица число нечетное, то точка а находится внутри полигона. Относительно точки Ь мы прийдем к заключению, что она лежит вне полигона, поскольку луч 7ь пересекает границу четное число раз (дважды).
Построение алгоритма на основе этой идеи базируется на двух особенностях. Во-первых, для решения задачи подходят любой луч, начинающийся в анализируемой точке а (ряс. 5.9). Поэтому для прпстоты выберем правый горизонтальный луч Га, начинающийся в точке а и направленный вправо параллельно положительной полуоси х.
Во-вторых, порядок расположения ребер, пересекаемых лучом Гд безразличен, имеет значение лишь парность (четное или нечетное количество) их общего числа. Следовательно, вместо моделирования движения вдоль луча Гд для алгоритма достаточно определить все пересечения ребер в любом порядке, определяя четность по мере продвижения. Простейшим
Рис- 5.10. Mipo с являете* ncpctcwMOuvw ребром, ребро d - касательное, ребро е - безразличное
решением будет обход границы
ПРИОт№е1кГДРри»ХьноГО луча £, направленного вправо, будем разд. чать три типа ребер полигона: касательное ребро, содержащее точку о; перг секающее ребро, не содержащее точку а; безразличное ребро, которое совсем й имеет пересечения с лучом Га. Например, на рис 5.10 ребро с является пере секающим ребром, ребро d - касательным, а ребро е - оезразличвым.
Функция pointlnPolygon решает задачу о принадлежности для точки и полигона р. В процессе работы алгоритма обходится граница полигона и переключается переменная parity для каждого обнаруженного факта пересечения ребра лучом. Возвращается значение INSIDE типа перечисления, если окончательное значение переменной parity равно 1 (означающее нечетное число), или возвращает OUTSIDE, если это конечное значение равно О (означающее четность). Если обнаруживается касательное ребро, то алгоритм немедленно возвращает значение типа перечисления BOUNDARY (граница).
enum ( INSIDE, OUTSIDE, BOUNDARY	// положение
// ВНУТРИ, ВНЕ, НА ГРАНИЦЕ
enum ( TOUCHING, CROSSING, INESSENTIAL );	// положение
КАСАТЕЛЬНОЕ, ПЕРЕСЕКА
;W
НЕСУЩЕСТВЕННОЕ
F
точки
ребра
int pointlnPolygon(Point 6а, Polygon &р} (
int parity =0;
for (int i = 0; i < p.sizeO; i++, p. advance (CLOCKWISE) ) { Edge e = p.edge () ; switch (edgeType(a, e)) (
case TOUCHING:
return BOUNDARY;
case CROSSING:
parity = 1 - parity;
1
return (parity ? INSIDE : OUTSIDE);
торого уточнения схемы • Ребро e — • Ребро e —
Обращение к функции edaeTvno / -	~,
ребра е относительно горизонтального	класси*иц,,Рует положение
вращая значения TOUCHING	Han₽0MeHHOro вправо, воз-
пересекающее или безразличное)’т паTn»”™ :fiESS~NTIAL (касательное. edgeType несколько хитроумное о " ₽еч“сления- Определение фувкшге должна правильно обрабатывать	льку Функция pointlnPolygon
хождении луча % точно через BPr,°6b'e ™Зуации’ возникающие при про-(а) бит четности должен быть перекТюче^ Р“ССМОТРИМ ₽“ В слу№ однажды, хотя при этом на Ря« ЮЧеН ~ Луч пеРесекает границу только (б) и (в) четность не изменяетсяДеЛ° пеРесекает два ребра. В случаях ---------------- ««ется Это может быть достигнуто путем неко-
—J классификации ребер: касательное ребпо рш- « пересекаю '	' °Н° С0^еРжит точку а.
(2) луч пересекает ребоо ’ 6Сли ребро е не горизонтальное я нижней конечной точки, некоторой точке, отличающейся от его
• Ребро е - безразличное, если онп
J не касательное и но пересекяю®^*

Обращаясь к рис. 5.11, мы „ожем в_
parity должна переключиться однажды Т'’ Что 8 слУча“ (а) перемен., егся, в случае (в) parity перекли,.,, “ ’ СЛУЧ““ (б) parltv ,.»₽ „имения. Заметим, что горизонталь^ Х'Г " СИТуа”'я' остается^ гчлтается безразличным и оно игнорХЛ ₽°’ ве “'“'охающее точку о Поэтому случаи (г), (д) „ (е, обрабатывая 1ФуНКЦ,гей ₽о1«1пРо?Хп’ „тощие случаи (а), (б) и (в).	“ются точно так же. как соотнёс?
Итак, функция edgeTvoe а.	*
CROSSING или INESSENTIAL (лвлесе^ИЦИРУеТ ₽ебро е как TOUCHINr относительно точки а:	^екающее, касателмм
int edgeType (Point ia, Edge &e)
I Point v = e.org; Point w = e.dest; switch (a.classify(e)) ( case LEFT :
return ( (v.y<a.y) &S(a.y<=w.x) ) ? CROSSING : INESSENTIAL-case RIGHT:
return ((w.y<a.y)&&(a.y<=v.y)) ? CROSSING : INESSENTIAL-case BETWEEN :	'
case ORIGIN : case DESTINATION :
return TOUCHING; default :
return INESSENTIAL;
Обратите внимание, как функция edgeType обнаруживает пересекающие ребра. Если точка а лежит слева от ребра е, то ребро является пересека* ющим, только если вершина начала ребра v(=e.org) лежит ниже луча fa, а вершина конца w(=e.dest) расположена на луче или лежит выше его. Тогда ребро не может быть горизонтальным и луч Гд должен пересекать ребро в некоторой точке, отличающейся от его нижнего конца. Если точка а находится справа от ребра е, то роли конечных точек v и w взаимно меняются.
Ис 5.11. Специальные случаи.
(а) и (г) - переменная parity переключается однажды;
(6)н(л)-раГ1еунеи1менястся;
(о) и (е) - pa rl t у переключается дважды и ситуация остается без изменения
R	случае (когда точка а не принадлежит границе полигон,
время nSnennn программы point InPolygon пропорционально рази^, полигона.
5.5.	Принадлежность точки: метод углов
Рассмотрим другой подход к решению проблемы принадлежности точки При этом подходе требуется определить понятие угла со знаком. Пусть даяь; направленный отрезок прямой линии Ьс и некоторая точка а и предполс, жим, что угол между векторами	равен п. Тогда угол со знаком
для точки а относительно отрезка Ьс равен 0, если точка с лежит слева от вектора ab или коллинеарна с ним, или -0, если точка с лежит справа от вектора al>. Заметим, что угол со знаком и ориентация треугольника babe имеют одинаковые знакп.
Расширим определение угла со знаком для цепочки вершин. Угол со знаком для точки а относительно цепочки вершин равен сумме углов со знаком для точки а относительно ребер, входящих в эту цепочку. На рис. 5.12 показан ряд примеров.
Функция siqnodAngle вычисляет и возвращает значение угла со знаком для точки а относительно ребра е. После исключения случая, когда точка а коллинеарна с ребром е, функция различает ситуации, показанные на рис. 4.6:
double signedAngle (Foxnt &а, Edge &е| (
Point v = e.org - a;
Point w = e.dest — a;
double va = v.polarAngle();
double wa = w.polarAngle();
if ((va — -1.0) || (wa = -1.0))
return 180.0;
double x = wa - va;
ii ((x = 180.0) || (x = -180.0))
return 180.0;
else if (x < -180.0)
return (x + 360.0);
else if (x > 180.0)
return (x - 360.0);
else
return x;
)
определения положения точки а отно ’ быть ,1СП0ЛЬ30ваНЫп ДеД положим, что точка а пе лежит на	заданного полигона р. Пред
А величину угла со знаком в точке Полигона Р- Обозначим букве причем обход полигона р совепп.п<Л относительно границы полигона р< Значение угла А позволяет °Я 8 иапРавлении по часовой стрел*  А = -360 градусов, то точка и °Пределить принадлежность точки: если а расположена вне полигона. ОчГ'П ИНутри полигона, и А = 0, если точка верждения для выпуклых полиг^ Ле1Ко пока*чать справедливость этого ут пуклого полигона р, то ппи °° Если точка а находится внутри |,!>I при обходе границы полигона р совершается поворот
Пэи*3,,ОВЫй ввод
I
а
₽ИС. 5.13. (а)-
(6)
V»
Рис 5.12. (а) УГОЛ СО знагом мя точки а равен 20, Гб) Угол со jm«qm м» two, b рлен -90 (.) угпа со для точки с равен 20-90+40^-30	001И*ОМ
на полные 360 градусов. В противном случае, если точка а расположена вне полигона р, то граница полигона может быть расщеплена на две цепочки, ближнюю и дальнюю к точке а (определение ближней и дальней цепочки было дано в разделе 5.3). Если через Ап обозначить угол со знаком относительно ближней цепочки, а через А/ — угол со знаком относительно дальней цепочки, то будем иметь Ап = -А/, откуда следует иметь
Менее очевидно, что это условие сохраняется и для невыпуклых полигонов. Вначале предположим, что точка а находится вне полигона. Вообразим проведение лучей из точки а через каждую вершину полигона, разбивая таким образом полигон на несколько треугольников и выпуклых четырехугольников Р1, р2.Pk (рис. 5.13а). Поскольку точка а лежит вне каждого
из Pi, а каждый р, является выпуклым, то угол со знаком А для точки a сдельно границ Р, равен нулю (т. е. Л = 0). Но А =	~’о
ку при суммировании каждое ребро исходного полигона р учи один раз Обратите внимание, что новые ребра, образованные при включении лучей добавляют лишь нулевые значения к А. Из этого следует, что А - 0, ” Ш, если точка о -ходится внутри по™-гона р. Как и раньше, предположим разделение полигона лучами, исходя
р'
Рз
(б)
(а)
• точка а вне полигона, поэтому А = Ат+Аг + >^з = О + О + О = О; (6) — точка а внутри полигона, поэтому A^Aj + Ai+Ab + Aj + Ajs -360 + 0 + О + О + О = -360
14t
г
i 1
I » I
)
I
I I
I
9
i
►
I
I
шжв „э точив n. го ва этот раа предусмотрим небольшой выпуклый Т? окоестпостп точки п (на ряс. ов обозначен как ро). Поскольку „ _ ™о ^»ый полигон в внутри него содержится точка а, то А = ^еГпоскольку точка а располагается вне оставшихся (выпуклых) Вдла. гонов Р1, то будем иметь А = 0) для всех 1 = 1.2.....*.	Из этого следу,,
что А — Ao + А1 + ••• + -Ал = “360.
Функция pointInPolygon2 решает задачу принадлежности для точки а я полигона р. Угол со знаком для точки а относительно полигона накал ливается в переменной total по мере обхода каждого ребра по очереди. Если будет обнаружено, что точка а располагается на каком-либо ребр; (угол со знаком относительно такого ребра будет составлять 180 градусов), то функция немедленно возвращает значение типа перечисления BOUNDAfly (граница). В противном случае после обработки всех ребер полигона возвра. щается одно из значений INSIDE или OUTSIDE (внутри или снаружи) в 34. висимости от окончательного значения,
накопленного в переменной total.
int pointln₽olygon2(Point Sa, Polygon Sp) (
double total - 0.0;
for (int i = 0; i < p.sizeO; i+*, p. advance(CLOCKWISE)) { Edge e = p.edge() ;
double x = signedAngle(a, e); if (x = 180.0) return BOUNDARY;
total += x; )
return ((total < -180.0) ? INSIDE : OUTSIDE); )
В худшем случае время работы программы pointInPolygon2 находится в линейной зависимости от размера полигона.
5.6.	Отсечение линий: алгоритм Цируса-Бека
®Ыделения той частп геометрического объекта, которая лежит меняется в магпи^™* наз“ваетси осенением. Отсечение очень часто при-для выделения небольт₽“*ИКв’ В окоуных системах окно может служить во все стороны Пои иппг Ч8СТИ общеи панорамы, далеко простирающейся объектов, которые не попадают	необходимо отсечь все те чаСТЯ
докторах необходимо отсечь те литтоы’кото°' В аеКОТО₽ых текстовьи £ длины строки. В тоехмрпмпй , J Р ’ кото₽ые не умещаются в пределах объему до их проеадрования нГрафИКе объекты отсекаются по заданному нужных затрат на ZeConaX Л°СК0СТЬ пРоек^и, чтобы избежать невидны.	проецирование тех частей, которые все равно не будут
В этом разделе будет оггмла»
ков прямых линий по границама,?Г°1>И™ ЦиРУса-Бека для отсечения отрез-раскроем алгоритм Сазерленлй.ХЫПУКЛ°Г0 полнгона- в следующем разделе полигона по границам выпик-™™ 0Джмаиа Для отсечения произвольного Обозначим через з	по™гонз.
по границам которого должен лииии и Р — выпуклый л-угольник> отсечен заданный отрезок. Здесь р н°
^веется отсекающим полигоном, в з является субъектом действия. Мы будем искать я_А р, т. е. ту часть в, которая лежит внутри р.
Пусть через я будет обозначена одна из двух направленных полубеско-a^nn*ix прямых линий, определяемых заданным отрезком *. Предположим, мы как бы продляем каждое из п ребер полигона до бесконечности в ()болх направлениях. Прямая линия .< пересекает вое атв продолженные ребра полигона р не более, чем в л различных точках пересечения. (Вели полупрямая s параллельна или коллинеарна некоторому ребру полигона р, то ребро не образует точки пересечения, и, более того, если прямая а* про* ходят через вершину полигона р, то две точки пересечения совпадают).
Отсекающий алгоритм Цируса-Бека находит эти точки пересечения я классифицирует каждую из них либо как потенциально входящую (ПВ). либо как потенциально покидающую (ПП). Предположим, что линия Т пересекает продолжение ребра е в точке i. Точка i будет типа ПВ, если полупрямая з* переходит из области слева от е в область справа от е. Учитывая наше соглашение о том, что внутренняя часть полигона лежит справа от каждого из его ребер, полупрямая 7 «потенциально входит» внутрь поли* гона в точке пересечения I. Поэтому если часть в А р не пуста, то в А р должна лежать после точки i (см. рис. 5.14). Точка пересечения i будет типа ПП, если полупрямая з проходит из области справа от е в область слева от е. В этом случае полупрямая г «потенциально покидает» полигон р и тогда если часть з А р не пуста, то з А р должна лежать позади точки
ш
пересечения t.
Классифицировать точку пересечения по ее типу ПВ или ПП сравнительно легко. Пусть вектор п будет перпендикулярен к некоторому ребру е отсекающего полигона и направлен вправо от ребра е. Определим вектор v*= b - а, где И* = а&. Тогда точка пересечения I (в которой полупрямая s пересекает ребро е) будет типа ПВ, если угол между векторами п и и будет меньше 90 градусов, или типа ПП, если этот угол больше 90 градусов. (Если угол равен точно 90 градусов, то точки пересечения не существует, поскольку в этом случае полупрямая ~з* должна быть параллельной или кол-
после
„	чп -mnvrnwAX скалярного произведения точка t
лквеарпой с р^ром е). В тврм«я« скаляр	„ . „ < 0>	« ЗД»
К.Л7Х мпольэоить такую классификацию точки вересе,^, .ЧТО точки пересечения упорядочены вдоль полупрямой Тотм полупрямая 7 пересекает отсекающий полигон р только тогда. ХИщ после последовательности точек пересечения типа ПВ следует последовать^ в^ь тХ пересечения типа ПП. Более того, искомый отсекаемый сегме„ отрезка прямой линии а О р начинается в последней точке типа ПВ в Хкащпя до первой точки типа ПП (отрезки а.1 п аг на рис 5.14).
С другой стороны, если тючки пересечении типов ПВ и ПП встречаю^ вдоль полупрямой в* поочередно, то отсекаемый сегмент а П р будет пустьа (отрезок ва на рис. 5.14). Поскольку в этом случае на прямой s долж^ существовать некоторая точка в такая, что можно найти точку типа ПЛ, лежащую позади от точки а и точку типа ПВ, расположенную после точка а. Но поскольку отсекаемая часть з П р находится сзади от точки типа ПП, сегмент в Л р должен также находиться после точки а, и, кроме того, поскольку сегмент я Л р должен быть после точки типа ПВ, то сегмент я Л р также должен лежать после точки о. Это возможно только в тох случае, если сегмент я Л р пустой.
Вместо непосредственной работы с точками пересечения алгоритм Циру. са-Бека работает с параметрическими величинами, соответствующими положению этих точек пересечения вдоль полупрямой s*. Алгоритм обеспечивает работу в диапазоне параметрических значений [to. Ъ]» соответствующих mt кущему отрезку прямой линии, который преобразуется в искомый отсеченный сегмент отрезка прямой линии я Л р по мере выполнения алгоритма. Вначале текущий сегмент отрезка прямой линии s задается равным исходному отрезку прямой линии с диапазоном параметрических значений [0, 1]. По ходу обработки алгоритмом каждого ребра е отсекающего полигона текущий диапазон либо остается неизменным, либо усекается (т. е. его нижняя граница увеличивается или верхняя граница уменьшается). На самом деле при каждом появлении точки пересечения типа ПВ с параметром t изменяется нижняя граница to текущего диапазона to = max(to» t). Аналогично, при обнаружении точки пересечения типа Ш! с параметрическим значением t требуется заменить верхнюю границу текущего диапазона: ti = min(ti, 0-осле обработки всех ребер (т. е. после вычисления всех точек пересечения) текущий диапазон [to, tj] соответствует искомому отсеченному сегменту $ Лр-
f “ Ь Т° отсеченяый сегмент существует, в противном случае (to > ti) он пуст.	г
линии^ч^Дй^^1^111111 аыполняет отсечение заданного отрезка прямой (истина) если в пр™* отсекающего полигона р и возвращает значение TROE в SXbhThГслХТаТе Ц0ЛУЧеН !е ПУСТОЙ сегмеат‘ или значение FALSE резок прямой линий яп-» отсечевяы4 отрезок не пустой, то отсеченный оттай параметр result: ВрЭЩается в вызывающуЮ программу через ссылоч-
bool clipLineSegnent(Edge Ьа I
double tO - 0.0;
double ti » 1.0;
double t;
Point v «= a. de at - a.org;
for (int i • 0; i < o.ai»
Polygon bp, Edge bresult)
P-advance (CLOCKWISE) ) (
..-•СвЬГй В*ОА
^23.—---------------------------------------------------------------1>1
Edge • « Р•edge() ;
if (s.intersect(е, t)=SKEW) { // в м . переоекажхмж Edge f = а;
f.rot();
Point n » f.dest - f.org;
if (dotProduct(n, v) >0,0) ( if (t > tO) tO = t;
1 else ( if (t < tl) tl = t;
)
) else {	//яма параллелью) или коллмжеаркн
if (s.org.classify(•) «= LEFT) return FALSE;
)
)
if (tO <= tl) [ result = Edge(s.point(tO), s.point(tl)) ; return TRUE ;
)
return FALSE;
Обратите внимание на то, как функция clipLineSegment обрабатывает случай, когда прямая линия s параллельна некоторому ребру е отсекающего полигона. Если прямая линия s расположена слева от ребра е, то функция немедленно возвращает значение FALSE и прекращает свою работу. В про* тпвном случае это ребро игнорируется и функция переходит к обработке следующего ребра. Очевидно, что время работы алгоритма линейно прапор* цпоналъно размеру отсекающего полигона.
5.7.	Отсечение полигона: алгоритм Сазерленда-Ходжмана
Отсечение полигона, т. е. процесс отсечения частей заданного полигона по границам отсекающего полигона,— более интересная задача, чем отсечение отрезка прямой линии, поскольку в результате получается не просто набор отрезков прямой линии, а коллекция полигонов. Более того, задача отсечения полигонов заставляет нас использовать структуры, заключенные н заданном полигоне и интерпретировать их не только как простые коллекции отрезков прямых линий. В этом разделе мы опишем алгоритм Са-зерленда-Ходжмана для отсечения полигонов. По заданным выпуклому отсекающему полигону р и исходному произвольному полигону 8 алгоритм Формирует область з П р в виде коллекции полигонов, содержащей куль Или несколько полигонов.
Алгоритм Сазерленда-Ходжмана выполняет отсечение исходного полигона относительно каждого ребра отсекающего полигона поочередно. Исходный ПОЛ1’гон сначала отсекается по первому ребру отсекающего полигона, затем ПолУченный новый полигон отсекается по следующему ребру к так далее: полученный в результате выполнения операции новый полигон «пересыдв-ется» в следующую операцию отсечения. Задача считается выполненной, когда исходный полигон будет отсечен по каждому ребру отсекающего вопи
.»-
11»
_____________	on пне. 5.15, где в качестве m пптиа яллостряруи» в Р
гопа. Работа	квадро e „i<«Polygon, которой передает^
ноющего полигона мят к фуикцяи clP^J отсекающий полили р.
Не "***
bool OU₽B=X«»"(MW’°" “
1	, non *4 - neW ?о1УдОП(9)'
poiyg°n 4 PolV9on *r*
int flag -	< p.sizeO ’’ i+“
f ОГ SoH^W-To^” delete q;
Lp, Polygon
(CLOCKWISE)) (
return FALSE;
result = q;
return TRUE;
delete q;
flag = FALSE;
break;
Рис. 5.15. Алгоривл отсечения СаэерленАв-Хоммана
На каждой итерации работы функции clipPolygon переменим q указывает на текущий обрабатываемый полигон, а переменная г-на полигон, полученный в результате отсечения полигона q по ребру е отсекающего полигона. Вначале переменная q устанавливается как указатель на копию исходного полигона s (она указывает на копию полигона з, так что поли* гоП s не разрушается). Отсечение полигона q по ребру е выполняется путем обращения к функции clipPolygonToEdge, которая выдает значение TRUE, если получаемый в результате полигон г непуст. Если оказывается, что полигон г не пустой, то он снова передается для выполнения следующей итерации отсечения путем операции присваивания q-r, В противном случае осуществляется выход из функции clipPolygon с возвращением .значения false.
Функция clipPolygonToEdge отсекает исследуемый полигон в по правой стороне ребра е отсекающего полигона. Результирующий полигон постепенно превращается в отсеченный полигон, который нам и нужен. Идея заключается в попарном поочередном сравнении каждого ребра в с каждым ребром е. В зависимости от результата каждого сравнения в создаваемый полигон добавляются две, одна или ни одной вершины.
На рис. 5.16 показаны четыре различных случая взаимного расположения ребра е и ребра s. Если вектор at соответствует текущему ребру а, то могут возникнуть такие ситуации при построении результирующего полигона:
1.	Ребро at полностью лежит справа от е. На выходе добавляется вершина Ь.
2.	Ребро at пересекает е, переходя с правой стороны на левую. На выходе добавляется точка I, в которой вектор ао пересекает ребро е.
3.	Ребро at лежит полностью слева от е. На выход ничего не посылается.
4.	Ребро at пересекает е, переходя с левой стороны на правую от ребра е. На выходе добавляется точка I, в которой пересекаются ребро ей* вектор at, а затем точка Ь.	'
Функция clipPolygonToEdge выполняет отсечение исходного подщюнд^ по ребру е. Она возвращает результирующий полигон через ссылочный параметр result и возвращает значение TRUE только в том случае, еслл полигон result не пустой.	.
5.16. Возможное взаимное расположение ребра и отсекающей полуплоскости
bool clipFolygonToEdge (Polygon is, Edgs f, Polygon* «result)
Polygon *p " new Polygon;
Point croesingPt;
for (int i  0; i < a.aixet); a. advance (CLOCKWISE) , i++) Point org в a.point();
Point da at « в. cw() ->point();
int orglslnsids « (org.classify(e) !« LEFT) ;
int dsstlslnsids « (dost.classify(e) !« LEFT);
if (orglslnsids !» dsstlslnsids) {
double t;
a.intersect(s.edge(), t);
crossingPt = e. point (t);
)
if (orglslnside £& destlslnside) p->inssrt(dast);
else if (orglslnside Sa !destlslnside) if (org 1= crossingPt)
p->insert(crossingPt);
I
else if ((orglslnside SS (destlslnside) I
else (
p->insert(crossingPt);
if (dost != crossingPt) p->insert(dest);
)
)
// случай 1
( // случай 2
// случай 3
// случай 4
result = р;
return (p->size() > 0) ;
такой полигон ,WWa Imporo^nocae отсеч^вмдо^^ Д0ЛЖНа обРябота" самостоятельных полигонов? R Чения Должно появиться несколько создаст единственный полигон сппр” СЛуч?е Ф^ЦИЯ clipPolygonToEdge Ситуация поясняется на рис. 5 17 ₽^а1?ии БЫР°жДенные граничные ребра, вырожденные части, сначаля Т° Ы разделить такой полигон на не-ребер вдоль общей прямой линии Руются концевые точки вырожденных изводится многократное обращения и *°ТОрои они коллинеарны. Затем про-чения вырожденных ребер. ПоскопкК?УЫКЦИИ Vertex: •* splice для пскли-ионкретного применения, описании™У Такал “Рифовка не потребуется для с ipPolygonToEdge, то нет смысля ° Главе 8 и использующего функции во-	МЫСЛа описывать эту операцию более пода*
Проанализируем время работы „
ишговв з и размера |р| отсекающего пД,/ теР«о1вах раамеоа U ..
„амаяется за время О(|а|) и ока вызХХ₽„ФуИКЦПЯ
Хвждого ребра отсекающего полигона т «Райней мере одпя программа Функция clipPolvnnn к ’ т* е- не более Ы раа Спв Для худшем случае.	РР°1удоп выполнена за вр^я
5 8. Триангуляция монотонных полигонов
Триангуляцией полигона называется декомпозиция полигона в набор треугольников. Триангуляция часто используется для упрощения реглет» я задачи в пределах области со сложной конфигурацией, и сведения ее к более простой задаче в пределах треугольника, поскольку треугольник относится к простейшей области и в этом случае задачу можно решить гораздо проще. Например, для определения, лежит ли точка внутри невыпуклого полигон», можно осуществить триангуляцию полигона и ответить «да» только в том случае, если точка принадлежит по крайней мере хотя бы одному троуголь-впку. Или при анализе сложных поверхностей, расположенных в пространстве, можно эти поверхности аппроксимировать сеткой треугольников, проанализировать которые значительно легче.
В этом разделе рассмотрим алгоритм для триангуляции полигонов специального типа, известных под названием монотонные полигоны. Этот алгоритм появился в 1978 году и он впервые дал возможность осуществить триангуляцию произвольного л-угольника за время О(л log п) путем выполнения двух операций:
1. Декомпозиция полигона на монотонные части за время О(п log п).
2. Триангуляция монотонных частей за общее время О(л).
Алгоритм декомпозиции полигона на монотонные части, работающий за. время О(п log п), будет представлен в главе 7.
Совсем еще недавно была сформулирована задача: возможно ли разр&бо-тать всеобщий алгоритм триангуляции, работающий быстрее, чем log п). Выли созданы достаточно быстрые алгоритмы, но либо одни на яих были пригодны только для специальных случаев обрабатываемых полигонов, либо улучшенные характеристики других зависели от свойств полигонов (например, от количества вогнутых вершин). Но несколько лет назад появилось несколько общих алгоритмов триангуляции, которые выполнялись 38 время о(н log л). В 1991 году Бернард Чезелле разработал оптимальный алгоритм с временем работы О(я).

5-8.1. Что такое монотонный полигон?
оворят, что цепочка вершин монотонная, если любая вертикальная е”НИя пеРесекяет ее не более одного раза. Начиная с самого левого конца, вершины монотонной цепочки расположены в порядке увеличения их координаты х.	;qr
м Л°лигон называется монотонным, если его граница составлена на двп оИотонных цепочек вершин: верхней и нижней цепочек вершин полигона.
• лая цепочка заканчивается в самой левой и в самой правой вершинах
Ill
полигона и содержит диво ни оддой, либо несколько вершин между Не рис 6.18 показано несколько примеров. Заметим, что (обязательно Ве. пустое) пересечение вертикальной прямой линииимовотонвого полигона » стоит либо ив отрезка прямой вертикальной линии, либо из точки.
(>)
Рис. 5.18. Два монотонных полигона Верхняя цепочка полигона (6) состоит из единственного ребра
5.8.2.	Алгоритм триангуляции
Пусть р будет монотонный полигон и переименуем его вершины ках 01, иг,..., vn в порядке увеличения координаты х, поскольку наш алгоритм будет обрабатывать эти вершины именно в таком порядке. Алгоритм формирует последовательность монотонных полигонов р = pi, рг»*... рп =0. Полигон pi, как результат обработки вершины vi, получается путем отсечения нуля или нескольких треугольников от предыдущего полигона рц. Алгоритм заканчивает свою работу после выхода с рп, пустым полигоном, а коллекция треугольников, накопленная в процессе обработки, представляет собой триангуляцию исходного полигона р.
Алгоритм хранит стек з вершин, которые были проверены, но еще не обработаны полностью (возможно, обнаружены еще не все треугольники, связанные с этой вершиной). Перед началом обработки вершины Ui в стеке содержатся некоторые вершины, принадлежащие полигону pi-i. По мере выполнения алгоритма сохраняются определенные инварианты стека.* Пусть вершины в стеке обозначены как sь 82>...» st в порядке снизу вверх, тогда могут встретиться четыре ситуации:
«1. «2...., st упорядочены по возрастанию координаты х и содержат каждую из вершин полигона p^i, расположенную справа от и слева от St.
2.
3.
St я si:
а. щ соседняя с а<, но не с з1(
в. vi Все эти
«1. «2».... sf являются последовательными вершинами либо в верхней, либо в нижней цепочках полигона рц.
Вершины si, аг,..., 8g являются вогнутыми вершинами полигона ввлк^ина внутреннего угла при каждой из них превышает 180 гра* дусов).	г
о6’’а6отке вершина Ш в полпгове Pl-i »»<-дятся в следующем соотношении с вершинами a. vi соседняя с в<| но не с зь б. и/ соседняя с si, но не с вц соседняя с обеими вершинами si и et.
Действия^" ^^еСТршины°7абЭаНЫ Р"С' 5Л 9*	их
трех^учадв. отражающих тонущее ^тояпи^’ХТ ДЛ" КаЯ<Д0Г0 "
* Инвариантом называется состояние г™
алгоритма, палример,  начале каждой	на определенной стадии
*ДОй итерации дллного ,шклл.
ыЯ ввод
519 Три случая, которые могут возникнуть при триангуляции монотонного полигона Вершина и (а) - со-' \едНЯяс5Г1нонес51;(б)-сосеАняяс51,нонеС5С(в)-со<ХАЗиссйеммивершинамн51Н^
Случай 4а. Вершина щ соседняя с af, но не с зу. Если t > 1 и виугрвг угол ZWe-1 меньше 180 градусов, то отсекается треугольник Др/Хв«_1 н J, исключается из стека. После этого в стек заносится щ. В алгоритме используется тот факт, что угол ZuiStSt-! < 180 только в том случае, когда либо s/-i лежит слева от вектора если и/ принадлежит полигону pt—i из верхней цепочки, либо sr-i лежит справа от векторе u$t, если щ принадлежит нижней цепочке.
Случай 46. Вершина щ соседняя с л, но не с st: Отсекаем полигон, определяемый вершинами и<, 3j, аг,..., St, очищаем стек и затем заносим в него сначала St, потом о/. Полигон, определяемый вершинами У/, ц, аг,,.., фактически является веерообразным с узлом в точке У/ (т. е. вершина У/ принадлежит корню веера). После этого алгоритм выполняет триангуляцию по-

лученного полигона.	„
Случай 4в. Вершина и{ соседняя с обеими вершинами si и 8f. В этом случае ui = ип и полигон pi-i, определяемый вершинами «1, «а,..., «ь янля ется веерообразным с узлом в точке ип. Алгоритм иепосред
тает триангуляцию этого полигона изакрешвкни простой На рис. 5.20 показав процесс работы алгоритм р PDaBQi. на мж-эдачи (порядок выполнения шагов сверху ,в”’' вершины в стеке ®м шаге обрабатываемые вершизы отмечены кружком, а в.рши
а
обозначены последовательностью si, в2,’”'/г' lateMonotonaPolygon зада-Для приведенной ниже программы=	список треугольников, пред-
ется монотонный полигон р и она в 15	р программе предполагает-
стазляющий результат триангуляции п • самой левой вершине.
СЯ. что окно для полигона р расположено на его сам
nil-/-]
«num ( upper, LOWER ) ;
, .^onoton.eoW"tpl Uat<Polygon*> ♦triangul»teM
I
Stack<Vertex*> в;	_ Lxst<Polygon*>;
List<Polyyon*> *txiang 08
Vertex *vr *vu, *vl;	:
leastVertex (pf	tT°Rie
v = vu = vl e p»v() ?
s.push(v);	vu,v);
int chain = advancePtrt •
u - -и:
j	h
'’’a^
e
. • . у.-н
“ push (v) ;
while (1) (	„a .
chain = advance₽tr(vl, yu, if (adjacantfv, e.topO)
// акашнмй цяхл while
Ш1
!adjacent(v, e.bcttceO)) (	// случай
int aide « (chain = UPPER) ? LEFT : RIGHT; Vertex *a •* s.topO;
Vertex *b  s.nextToTopO ;
while ((a.sized > 1) &£
(b->classify (v->point () ,a->point ()) == if (chain “ UPPER) (
p.setV(b);
triangles->append(p.split (v)) ;
} else (
p.setv(v) ;
triangles->append(p.split (b)) ;
}
s.popO ;
a = b;
b « s.nextToTop();
e.push (v) ;
______ caujacenciv, S.topO)) ( Polygon *q;
Vertex *t = s.popO;
if (chain = UPPER) {
p.setV(t);
q = p.split(v);
I —4 - -	-
// случай 46
р.setV(v);
q = р.split(t);
q->advance(CLOCKWISE); )
triangulateFanPolygon(*q, while (!s.empty())
s.popO ;
s.push(t);
s.push(v);
} }
)
P-setV(v) .
// случай 4a
II конец внешнего
Цикла while
Две указанные в обращении aenmwu Ие TRUE только в том случае, если ы» являются соседними
bool adjacent (Vo-t
return ((„ ==
Программа trianoui
°Р«имущеХОаВуже вь°	о6равот«е поливом Р
яые^а6иаТеИУЮ Т К^ДОЙ XX ?' —»
и zJ-» которые уКя? В ПрогРамме rhn Ц И Переме»ная у указывает Указывав на *орм«РУются еще две перемер-
10 °°Рабатываемую вершину
Ый ВВОД
Vertex *vun = vu—>cw(), Vertex *vln ж vl*^cc^*

Рис. 5.20. Триангуляция небольшого монотонного полигона. Треугольники, обнаруженные на каждол* шаге, выделены толстыми линиями. Порядок выполнения шагов сверху вниз, слева направо
соответственно. По мере в верхней и нижней цепочках п0"™^°,vanceptr продвигаются слева программы эти указатели ФУ®'?” и advancePtr она изменяет 31 во. При каждом обращении к ФУ тель v в соответс либо vu, либо vl и корректируй ным изменением:
Int advancePtr(Vertex* &vuf Vertex
if (vun->point() < vln->poxnt())	(
v • vu « vxm;
return UPPTR;
) I
v  vl » vln;
return LOWER;
)
Функция advancePtr возвращает значение UPPER или LOWER, показываю щее, к какой из двух цепочек вершин v относится произведенное действие. Это значение используется в программе triangulateHonotonePolygon ддя контроля, чтобы ее последующее обращение к функции spl i t вызывало возврат части, выделенной из основного полигона, а не основного полигона, пз которого эта часть исключена.
При триангуляции веерообразного полигона последовательно находятся все треугольники, имеющие одну корневую вершину и. Для этого полигон обхо-дится, начиная с вершины и, и в каждой вершине и), не являющейся соседней с п, отсекается треугольник по хорде, соединяющей вершины и и ш. Эго выполняется функцией triangulateFanPolygon, которая деструктивно разбивает л-угольник р на п - 2 треугольников и добавляет их в список треугольников . В функции предполагается, что полигон р веерообразный с окном, расположенным на его корне:
void triangulateFanPolygon (Polygon fip, List<Polygon*> ♦triangles)
(
Vertex *w = p.v()->cv()->cw() ;
int size = p.size();
for (int i = 3; i < size; i++) ( triangles->append(p.split(w)); w = w->cw();
I
trianglee->append(Sp);
}
На рис. 5.21 показана триангуляция, полученная с помощью описанного алгоритма. Монотонный полигон содержит 35 вершин.
Рмс 5.21. Триану/лцмя монотонного 35-угольж*а
5.8.3. Корректность
Нам необходимо доказать алгоритм находит на шаге i	---- ---------
И-И (2) алгоритм восстанавливает"1-70” внутренней Х°РД°Й Для П0ЛИГ°^
Два^положения: (1) каждая диагональ, которую четыре состояния стека при переходе &
Ппшагоамй мол
„„о* итерации к следующей. (То, что авйдеввые хорды поеобмтпг » ходяый полигон в треугольники, очевидно из ряс. 6.19).
Сначала рассмотрим диагональ на рис. 5.19а (здесь t - б) Обо-л^чим треугольник Т =	и заметим, что ни одна из вершинполигона pi-1 не может лежать внутри Т: вершины я0.... расположены слева
от самой левой вершины K/-i треугольника Т, а вершины и/ для j > I лежат справа от самой правой вершины щ треугольника Т. Следовательно любое ребро, пересекающее диагональ у^_ь должно выходить из треугольника Т через одно из ребер st-ist или sfui, что невозможно, поскольку они является граничными ребрами полигона рм. Таким образом, диагональ является хордой. Отсечем треугольник Т от полигона р{_ь Те же аргументы показывают, что оставшиеся диагонали, получаемые в случае 4а, также являются хордами.
Теперь рассмотрим случай 46, показанный на рис. 5.196. Согласно условиям 2 и 3 для стека, полигон Т, определяемый вершинами aj, aj,..,, it, является веерообразным с корнем в вершине щ. Заметим, что ня о дня па вершин полигона pi-i не может лежать внутри полигона Т — если бы там оказалась одна или несколько вершин, то самая правая из таких вершин должна бы оказаться записанной в стеке и следовательно и я ходиться на граниту» полигона pi-i, Поскольку внутри Т нет ни одной вершины, то любое ребро, пересекающее vi3t, должно также пересекать одно из ребер o/aj, ijej » . . . » jf-ist , что невозможно, поскольку они являются граничными ребрами полигона pi-i. Аналогично также можно показать, что в случае 4в (рве. 5.19в) полигон pi-i является веерообразным с корнем в вершине ад и внутри него нет ни одной другой вершины.
Затем докажем, что условия стека сохраняются от одной итерации к другой. По крайне:*! мере две вершины щ в должны быть записаны в стеке с обязательным учетом порядка их расположения по горизонтали. Тем самым удовлетворяется условие 1. Вершины образуют цепочку вершив в р/-1 по индукции (в случае 4а) или вследствие того, что стек сокращен до двух соседних вершин (в случае 46), тем самым оказывается удовлетворенным условие 2. Вершины стека являются вогнутыми (за исключением верхней х вижней вершин), поскольку вершина vi записывается в стек только в том случае, если верхняя вершина стека (над которой будет записана новая вершина) станет вогнутой. Следовательно, в этом случае удовлетворяется условие 3 (при этом безусловно удовлетворяется случай 46, поскольку в стеке содержатся лишь две вершины). Наконец, вершина 8t должна быть соседней либо si, либо St, поскольку монотонность полигона рм гарантирует, что вершина Uj имеет соседа слева, а все вершимы в стеке за исключением si и *г уже имеют двух соседей. Следовательно, удовлетворяется условие 4 для отека.	' _
ШИПП*
К.»',
5.8.4. Анализ	. .. ‘
. •* . М
Программа triangulateMonotonePolygon выполняется за аремя в(л), если задаваемый на входе полигон содержит л вершим. Для получедхх ®ей границы О(л) заметим, что при каждой итерация двух внутретим л°в while из стека исключается одна вершина (в случаях 4а ж4Цк *аЖдвя вершина записывается в стек только однажды (при перво Рвботке) и, следовательно, может быть выбрана из стека тоже только од-fi Запад 2704
наждь. Поскольку алгоритм вьшолвяет ООО операдии со стеком одивако =n o^ н затрачивает одинаковое время между двумя такими послг. доваХТми операциями, то время работы программы проиорцио^ О(л). Нижвяя граница определяется тем, что должва быть обработана ка». дая из п вершин.
5.9. Замечания по главе
Необходимость сортировки возникает в самых различных ситуациях и такая задача возникла задолго до появления компьютеров и даже возможно раньше появления письменности. И неудивительно, что многие из рассматриваемых в книге задач основаны на той или иной сортировке. Здесь были рассмотрены три способа сортировки, используемые в различных применениях компьютерных программ: сортировка при вводе, сортировка выборки, сортировка при слиянии. Эти способы настолько фундаментальны, что они не требуют специальной атрибуции. Однако, несмотря на их кажущуюся простоту, до сих пор продолжается их исследование и разрабатываются новые базовые алгоритмы сортировки.
Количество полигонов, образуемых набором из п вершин, возрастает экспоненциально в зависимости от числа п, однако количество звездчатых полигонов ограничено сверху числом О(п4). Если в наборе точек нет ни одной коллинеарной тройки точек, то ядро различных частичных звездчатых полигонов для такого набора точек имеет выпуклую оболочку. Алгоритм формирования такого разделения в течение времени О(л4) представлев в [22], он приводит к созданию алгоритма поиска звездчатых полигонов, выполняемого за время О(на). Алгоритм определения ядра любого л-стороа-него полигона за время О (п) описан в [53]. В главе 8 мы предложим более простой, во менее эффективный алгоритм, выполняемый за время О(л log п). В прекрасной книге [60] исследуются проблемы видимости внутри полигона.
Определение выпуклой оболочки позволяет расширить это понятие на набор точек в любом d-мерном пространстве при d > 1: выпуклая оболочка представляет собой пересечение всех выпуклый политопов, содержащих этп точки. Некоторые способы поиска выпуклой оболочки для набора точек на плоскости формулируются в терминах d-мерного пространства. Формирование оболочки, например, может рассматриваться как частный случай ж* щюс^анХве^ [45]° ыахождеппя выпуклой оболочки точек в d-мерном алгппитм* °^ечеиия °рлмых линии, описанный в разделе 5.6, известен как алгоритм отсечения Цируса-Века л гплплА	г>витс*
пя s 7 u-qtz апг	а способ отсечения полигонов из
ь™ сц=^о7Х™Як:^Ха п„°Д>КМаНа [75)- Из Д₽У™Х Х0Р0Ш°,цх линий Кл-»иа Га-.вгхп '	’ едУст Упомянуть алгоритм отсечения прямы
Гго“°1СеТ"°	™ ° г"
горитм является общим nJ Вейлера-Атертона [88]. Этот последюгй ал кающих полигонов, К(порыеДнеУобяз1тОбРЯбОТКУ ЛЮбЫХ ИСХ0ДИЬГ* ” JJh могут включать отверстия. Таил J Ja . Ь"° Д0ЛЖИЫ 61>1ТЬ выпуклый 11 алгоритме удаления невидимых °J4’,0CTb позволяет использовать его в в [88].	Д поверхностей, также прсдстпвлони°
ввод
Алгоритм для триангуляции монотонных полигонов описан в Г311 мв даВо более общее определение монотонности, чем предполагалось в папГ1 представленни: цепочка вершин с является монотонной относительно веко торой прямой ЛИВИИ f. если любая прямая линия, перпендикулярная поя „ОЙ линии I, пересекает ломаную линию с пе более, чем в одной точке Полигон является монотонным, если его граница сформирована на двух цепочек вершин, являющихся монотонными относительно одной и той же прямой линии. В нашем случае рассматривался полигон, который являлся монотонным относительно горизонтальной оси. В статье [67] описан алгоритм, выполняемый за линейное время, решающий, существует ли прямая линия' относительно которой полигон будет монотонным.
Алгоритм триангуляция Хазеля [19] выполняется за оптимальное время О(п).
Обзор методов разбиения полигонов представлен в [60, 61].
5.10. Упражнения . ,
• о ». &
I.	Программа starPolygon находит только веерообразные полигоны. Обобщите программу так, чтобы при обработке любой точки в, лежащей внутри выпуклой оболочки набора точек, она находила звездчатый полигон, содержащий точку з внутри его ядра.
2.	Определите алгоритм, выполняемый за время О(п log л), для определения звездчатого полигона в заданном наборе точек.
6.
8.
9.
3.	Докажите, что ядро полигона является выпуклым.
4.	Напишите версию программы ввода оболочки, выполняемой за время О(п log п). (Подсказка: предварительно отсортируйте точки, чтобы не было необходимости обращаться к функциям pointlnConvexPolygon и closestVertex).
5.	Покажите, что для решения задачи пояска выпуклой оболочки необходимо время Q(n log п). (Подсказка: найдите эффективный способ перехода от сортировки к поиску выпуклой оболочки, используя тот факт, что время П(п log л) определяет нижнюю границу для сортировки).
Покажите, что точка р принадлежит выпуклой оболочке Crt(S), если и только если существуют такие три точки в наборе S, которые образуют треугольник, включающий точку р.
Следующий вопрос относится к алгоритму отсечения прямых линий Цируса-Бека: покажите, что прямая линия в пересекает выпуклый полигон р только в том случае, если точки пересечения,, упорядоченные вдоль прямой линии V, состоят из последовательности точек пересечения типа ПВ, за которой следует последовательность точек пересечения типа 1111.
Измените программу clipbineSegment так, чтобы она заданному выпуклому полигону бесконечную прямую линию, резок прямой линии.	*.•
Разработайте алгоритм для удаления вырожденных ребер, киТм^ы» иногда формируются программой clipPoly9on ° 9е*
некоторой точки а внутри р разработайте sS«'“	:№ —' ™' »
риге радиальную сортир	иавгуляции n-утолышка неволь.
И. Покажите, -“д"^ «“композиции полигона в п - 2 треугольника.
зуются п ~ а хирА
Гла»а 6
Пошаговая выборка
^^пособы пошаговой выборки решают задачу постепенно, понемногу за каж-дым шаг. При этих способах входной поток обрабатывается по собственным правилам, а не в том порядке, в каком данные следуют во входном потоке. При пошаговой выборке производится просмотр всего входного потока для выбора «наилучшего» элемента, который будет обрабатываться следующим.
В некоторых приложениях пошаговой выборки порядок обработки элементов может быть определен заранее. В таких случаях входной поток может быть подвергнут предварительной сортировке. В других приложениях порядок следования нельзя предвидеть и выбор следующего элемента для обработки зависит от полученного предыдущего результата.
В этой главе мы рассмотрим оба этих вида приложений. Начнем с сортировки выборки как примера пошаговой выборки. Затем проанализируем еще два способа для формирования выпуклой оболочки конечного набора точек: способ «заворачивания подарка» и метод обхода Грэхема. Третьим геометрическим алгоритмом, который будет нами описан, является способ сортировки по глубине для реализации задачи удаления невидимых поверх-ностей из заданного набора треугольников в пространстве. Следус: горитм вычисляет пересечение двух выпуклых полигонов в плоскости. И последний алгоритм выполняет специальную триангуляцию конечного набора точек на плоскости, известную как триангуляция Делоне

6.1. Сортировка выборкой
нг,г,

Сортировка	Я°
путем многократной выборки найм	массива
пока набор не окажется
дится наименьший элемент и пр	наименьший элемент
вой позиции массива. Затем находится на^шеньшй шпхся элементов (справа от первой поз^ш^* замена с элементом во второй позпци  v
пор, пока не будет от=о₽™рОВаЕВ 'сортирует элементы в массиве Шаблон функции selertio i вбирается наимеиьт=±
Для каждого 1 от 0 до п 1 на	замена с элементом а [Иг
a [i. .п-1] и затем выполняется взаимная замен

tamplateCclass T> void aaleotionSort(T a[]
int n, int (*cmp)
(T,T))
min = j;
swap(a[i], a[min]);
1
Тело внутреннего цикла for осуществляет операцию выбора на каждом шаге 1. В переменной min хранится индекс наименьшего элемента, обнаруженного в ходе текущей проверки. Здесь лучше сохранять индекс элемента, а не сам элемент, чтобы можно было осуществить последующую замену.
Функция swap выполняет взаимную замену для указанных ей двух аргументов
template <claas Т> void swap(T Sa, T &Ь) (
Т t - а;
а = Ь;
Ь = t;
Время выполнения сортировки выборкой составляет Т(п) = У
где I(i) — время, необходимое для выбора i-того наименьшего элемента. Поскольку для выбора этого элемента необходимо сделать п - i сравнении, то всего в программе выполняется Т(л) = (п - 1) + (п - 2) + ... + 1 = п(п-1)/2 или всего около л2/2 сравнений. Хотя это время работы сравнимо со временем работы сортировки при вводе, в общем случае применение сортировки выборкой предпочтительнее: здесь будет сделано только п замен, тогда как для выполнения сортировки при вводе потребуется около п2/2 полузамен в худшем случае (понимая под полузаменой сдвиг элемента на одну позицию вправо).
вЛЛ • Томные и оперативны. „
Сортировка вы6орк„ я НЫв "₽ОГР^'
’-и X'Z -	мпой
того, как	«ли иаХ*’? ®ЫТЬ «°«Упны с самого №
«лможвость DOM«Ci ₽“2₽>ТЯе болХТ® Элемеиг поступит только !^епа* Все Мет®ДЫ пош^МеаЬПи1Й алемент ае П Элвмеиты уже выбраяы, то ами’ поскольку правила °ыбора neajl[ ераУ1° позицию была бы ут-
«е все элемеХ ДХЫЙ ВЫбо₽	проком-
РШровка при Ва тупны в Каждый л, * Мон<ет быть гарантирован, «ся примером опеРа^ ' Подоб«о Дру™ма»»ьгй момент.
Оперативные нро^6	(рД^68*1 пошагового ввода, явля-
ооо^кае,	“е
ао вРемецИ и г еР°Д- Это означает, что на °йсем не обязательно, чтобы
пе вполне воз-
весь поток был получен к моменту обработки. Хотя в принципе вполне нм-«°*во> ЛО°МогГиачТ» lnSe“iOnSott М0жет 6ыть «о^Хеь месспн с самого начала, во предварительный просмотр массиве в ней не про-изводится и каждый элемент анализируется поочередно, не заглядывая впе-
ред.
Оперативные программы наиболее полезны для работы в системах рояль вого времени, когда входные данные постоянно поступают в процессе ра боты. Текстовые редакторы и системы моделирования полетов самолета яв
лаются примерами оперативных программ, поскольку входные данные для таких систем генерируются пользователем в реальном времени и его действия предсказать нельзя. Иногда программы реального времени должны работать нэ основе входных данных, которые поступят только впоследствии (зачастую даже слишком поздно), что приводит к напрасным затратам. В
качестве примера вспомним программу формирования выпуклой оболочки insertLonHull в предыдущей главе, которая может создавать большие текущие оболочки только для того, чтобы потом их отбросить. Так что рассмотрим теперь способ формирования выпуклой оболочки, в которой исключается подобная ненужная работа, поскольку она основана на подходе, характерном для пошаговой выборки.
6.2.	Построение выпуклой оболочки способом «заворачивание подарка»
Один из способов формирования выпуклой оболочки конечного набора точек S на плоскости напоминает действия по вычерчиванию выпуклой оболочки с помощью линейки и карандаша. Вначале выбирается некоторая точка a G S такая, что она явно должна принадлежать границе выпуклой оболочки — для этого годится самая левая точка. Затем вертикальный луч поворачивается по направлению движения часовой стрелки вокруг этой точки до тех пор, пока он не наткнется на некоторую другую точку b в наборе S; отрезок будет ребром выпуклой оболочки. Для поиска следующего ребра будем продолжать вращение луча по часовой стрелке, на этот раз вокруг точки Ъ до встречи со следующей точкой с из набора Я, отрезок 5Е будет следующим ребром выпуклой оболочки. Этот процесс продолжается До тех пор, пока не будет достигнута точка а. Процесс изобразим на рис. 6.1, ои получил название способ .заворачивание подарка.. J
Процесс поворота луча вокруг каждой точки является .выбирающей, частью алгоритма. При выборе точки, следующей за точкой а на.г₽а^в выпуклой оболочки, ищем такую точку 6, для которой вег щ.однойт™ лежащей слева от луча Л. Все точки анализируются по очереди, пока < горптм не проследит весь путь, вплоть до самой левой точки, об р К этому моменту При этом проверке подвергаются
вествые точки, которые должны принадлежать границ-	ттеяетяВ*
Следующая программа giftwrapHull возвращает поляив жймь лающий выпуклую оболочку п точек в массиве а. Массив s
Длину П+1, поскольку в элементе s[n) программа храни огрышчиввжда» элемент (*стрпж»):
Polygon *giftwrapHull(Point s[], int n) (
int a# i;	.
for (a - o, i - 1; i < a; i**)
if («[i] < 8[a]) a » i;
o[n]  a[a];
Polygon *p - new Polygon;
for (int m = 0; m < n; m++) (
swap(a[a], a[mJ);
p->inaert(a[m]);
a = m+1;
for (int i = m + 2; i <= n; i++) ( int c = a[i].classify(a[mj, s[aj); if (c = LEFT || c = BEYOND)
a = i;
)
if (a = n) return p;
)
return NULL;
Вращение луча вокруг некоторой точки s [m] моделируется внутренним циклом for. Точка s [а] должна быть самой левой, которая встречается на пути луча. Если новая точка s [ i ] лежит слева от луча, начинающегося в точке s [т] и проходящего через точку s[a], тогда вращаемый луч должен поместить точку s[i] перед точкой s[a], так что точка а будет изменена.
Поиигоаая выборка
Переменная а также изменяется, если -гл
точка s [а] не может быть вершиной выпуклой оболов? П03вДИ ~ между точками s[m] и a[i]	“куклой оболочки, если она лежит
Заметим, что функция giftwrapHull реорганизует точки в массиве в
В конце шага гл подмассив s(0. ,mJ содержит изд оболочки, упорядоченные в направлении по движению часовой	а
подмассив s[m+l..n 1] содержит остальные точки, которые пра^^кат пли нет вершинам выпуклой оболочки. Именно эти последние точки и должны проверяться на последующих шагах.
6.2.1.	Анализ
Чтобы проанализировать способ заворачивания подарка, ваметим, что поворот вокруг /и-той точки требует классификации п - т - 2 точек, каждая из которых занимает одинаковое время. Поскольку выполняется тоаьмп Л. поворотов (где Л — число вершин в выпуклой оболочке 6W(S) ), то общее время работы составляет О(Лп) . Если каждая из п точек является вершиной выпуклой оболочки CH (S) (т. е. Л = л), то время работы составляет О(л’), сравнимое с временем работы функции insertionHull. С другой стороны, если величина Л мала по сравнению с л, то способ заворачивания подарка работает быстрее, чем способ ввода оболочки.
Говорят, что время работы, определяемое как О(Лк)> зависит от результата, поскольку формула содержит коэффициент Л, зависящий от длины выходного массива. С точки зрения анализа программы, она работает бы» стрее, если на выходе будет сформировано меньше данных (но так работают отнюдь не все программы). При учете результата работы получаются более правильные оценки поведения программы. Так в случае анализа программы, работающей по принципу «заворачивания подарка», оценка О(Лл) означает, что программа более эффективна, если выпуклая оболочка определяется наибольшим числом вершин — в атом заключается отмячио от оценки О(п2), когда учитывается величина только входного файла и не принимается во внимание объем выходных данных.	.. •“
•* •
• J
6.3.	Построение выпуклой оболочки: метод обхода Грэхема . . г в
В этом разделе мы раскроем сущность способа построения выпуклой оболочки по методу обхода Грэхема, названному так по имени его создателе .В методе обхода Грэхема выпуклая оболочка конечного набора точек S находится в два этапа. На первом этапе предварительной сортировки алгоритф выбирает экстремальную точку ро G S и сортирует остальные точкй 8 ж ОЙН ответствии с углом направленного к ним луча относительно точки ро. этапе построения выпуклой оболочки алгоритм выполняет пошаговую Обработку отсортированных точек, формируя последовательность текущих ныпук лых оболочек, которые в конце концов образуют искомую вьщукяфЛ «У 6W(S). Предварительная сортировка упрощает выполнений 'мала построения выпуклой оболочки — каждая обрабатываемая на этом ЛИЙ», точка включается в текущую оболочку без дополнительных условий^ того, при этом оказывается очень легко обнаружить те точки, к р
I
Рис. 6.2. Обозначение точек на основе их полярной координаты относительно точки ро
дует удалить из текущей оболочки. Это даст положительный результат по сравнению со способом ввода оболочки, описанным в предыдущей главе, при котором обрабатываются все точки и каждый раз принимается решение о включении точки в текущую оболочку и, если она включается, то просмат-ривается вся граница текущей оболочки для определения вершин, подлежащих удалению.
Для заданного набора точек S при методе обхода Грэхема сначала на-ходится некоторая экстремальная точка ро Е S. Выберем в качестве такой точки точку ро из S с минимальной координатой у или самую правую в случае сильно вытянутого набора. Затем остальные точки сортируются в порядке полярного угла относительно точки ро- Если две точки имеют одинаковый полярный угол, то точка, расположенная ближе к точке ро, считается меньшей, чем более дальняя точка. Это обычное словарное правило для определения соотношения точек по их полярной координате относительно точки ро, которое реализуется функцией сравнения polarCmp, приведенной в разделе 5.2. Переименуем теперь оставшиеся точки Р1. Р2.»-.. Рп в соответствии с их упорядочением, как показано на рис. 6.2.
На этапе построения выпуклой оболочки при методе Грэхема текущая выпуклая оболочка строится на основе уже обработанных точек. На рис. 6.3 показан процесс работы алгоритма. Рассмотрим обработку точки рт (рис. 6.Зе). Поскольку точки упорядочены в порядке возрастания полярного утла относительно точки ро. то очевидно, что точка р7 должна быть включена в состав оболочки и что точка ро должна быть ее предшественницей, но какая точка будет включена после точки р?? Для ответа на этот вопрос используем тот факт, что каждая вершина должна соответствовать повороту влево при обходе границы выпуклой оболочки в направлении против движения часовой стрелки. Рассмотрим сначала первого кандидата — точку ре. Поскольку Угол ^Р5РОР7 представляет не-левый поворот (ру лежит справа от ребра рбРб)» точ1'а ре удаляется из текущей оболочки. Затем рассмотрим точку Р5- Поскольку угол Z.p5pep7 также представляет не-левый поворот, то удаляется и точка Р5. Аналогичным образом удаляется точка р^. Ситуация изменяется при Д°‘ стижеиии точки рз: угол Др1рзр7 обозначает поворот плево и таким образом находится последователь для точки р7 в текущей оболочке (точка рз)-
В программу grahamScan передается массив pts из п точек и возвращается полигон, представляющий собой выпуклую оболочку для точек pts.
Программа выполняется за пять шагов: первые два образуют эта» пуклад₽оболочк1;СО₽ТИРОВКИ’ “ ’	оставшихся трех находятся
(Ж)
Рис. 6.3. Робота по методу обхода Грэхема
1. Поиск экстремальной точки ро*
2, Сортировка остальных точек по точки ро.
•- t *:
* ’ • -= • * I
ИХ полярному углу относительно
m
з.
uii
ализация текущей оболочки.
Формирование текущей оболочки, пока она не станет равной оболочке для всех л элементов.
5. Преобразование текущей оболочки в объект типа полигон и возврат его.
вьшуклой
(Polygon)
Программа выглядит следующим образом: Point originPt;
Polygon *grahamScan(Point pts[], int n) {
// шаг 1
int m = 0;
fox (int i = 1; i < n; i++)
if ((pts[i].y < pts[m].y)
((pts[ij.y = pts[m].y) ££ (pts[i].x < pts[m].x))) m = i;
swap(pts[0], pts[a]);
originPt = pts[0];
Point **p = new (Point*) [n];	// шаг 2
for (i = 0; i < n; i++)
plij = £pts[ij;
selectionsort(£p[l], n-1, polarCmp);
// или любой другой способ сортировки
// шаг 3
for (i = 1; p[i+l]-Classify (*p[0) , *p[i]) = BEYOND; i++)
Stack<Point*> s;
s.push(p[0]);
s.push(p[ij) ;
for (i = i+l; i < n; i++> (	// щаГ 4
while (p[i]->classify(*s.neKtToTop(), *s.top()) ’= LEFT) s.popf) ;
s.push(p[ij) ; )
Polygon *q = new Polygon; // mr 5
while (!s.empty())
q->inaert(♦a.pop()) ;
delete p;
return q;
Первый шаг ясен м
^^изируются равным/тп1аге 2 Соадается масси и
у/сазв/пелей> чтобы м	Point в массив™ Р “ его эле“енты ини-
сортировки Зат^« было Использовать nn PtS> Н°М Пзебуется массив вения polarCmp котпп Массив Р сортируй Д"У И3 обобЩснных подпро-э«злч.гого поЛгоХРо’:а:П₽ед'»™ " рХе” 5С2ПОМО“’ЬЮ ФУИКВНЯ Ср“-переменная orlainP^ заданному na6onv m ^'2 8 Контексте построения P’larCmp “»чал1.иу1о'тДС"°ЛМуетс" А»я тогое1‘^ПОМИИМ' что гло6альв"'
Н« 3 я 4 тагах , у ~ в даии0м еду,’ т бЫ ,,срелпть ° функцию 8‘ = 1/>1. рг.. I и « ««даетсв теку.,° то',ку />п-
прелстааляет текуТу" °^Л°ЧКП- Пуст'- имеем набор
,ую клочку сн (S) следующим
р0ц15говзя выборка
образом. Если точки в стеке обозначены как 8i, s2... в порядке от низа
стека к его верху, то стек удовлетворяет таким условиям стека:
1. Точки ро = si» 82,..., st = pi являются вершинами текущей оболочки 6W (S) в направлении обхода против часовой стрелки;
2. Ребро S1S2 является ребром окончательной выпуклой оболочки (№(3).
На шаге 3 эти условия определяются предварительно. В цикле for осуществляется пошаговое продвижение по лучу pnpi до тех пор, пока не будет достигнута последняя (самая дальняя) точка р/, после чего точки ро и pi будут записаны в стек. Первое условие стека будет удовлетворено по той причине, что отрезок прямой линии popi является выпуклой оболочкой для Si поскольку точки pi, р2,...» pi-i расположены между ро и рь Второе условие выполнено потому, что popi является ребром для (M(S) .
На шаге 4, проиллюстрированном на рис. 6.4, обрабатывается точка pt для формирования текущей оболочки tW(Si). Программа grahamScan выбирает из стека точки st, st-i...s*-i до тех пор, пока не будет достигнута
точка Sk , самая верхняя точка в стеке такая, что угол Lak-iakpi представляет левый поворот. Поскольку все эти вызванные точки лежат внутри треугольника Apop/Sfc или вдоль одного из ребер popt или pish, то ни одна из них не может быть вершиной выпуклой оболочки CM (Si). Так как из стека будут удалены только эти точки и никакие другие, оставшиеся точки вместе с pi будут вершинами выпуклой оболочки CW (S/). Вследствие того, что первое условие стека гарантирует корректность упорядочения точек «ь ая,..., внутри стека, а точка pi следует за ними в порядке увеличения полярного угла, то новое содержимое стека 8i, S2,..»» s>, pi правильно упорядочено в порядке обхода против часовой стрелки. Из этого следует, что первое условие соблюдено.
Назначение условия 2 заключается в гарантии существования точки 8Д. Поскольку ребро S1S2 является ребром оболочки 6W (S), то каждая точка рь прошедшая обработку, лежит слева от отрезка 8182. Поскольку угол Zois^pt представляет левый поворот, то из этого следует, что точка за существует для некоторого k > 2. Более того, так как исходные точки 81 и 82 никогда не вызываются из стека, условие 2 сохраняется.	___
На шаге 5 программа grahamScan формирует объект q типа Polygon путем итеративного вызова точки из стека и записи ее в q.	ус-
1 точки вызываются из стека в порядке обхода по часовой стрелке.
S1S2 является ребром оболочки £*# (S), то каждая точка рь прошедшая обработку, лежит слева от отрезка sisfc. Поскольку угол Zoispj?/
ловию
₽мс. 6.4. Включение точки а лая обрвюннт* тотуикй обоочхм СЛ($д
в 1*1
174

Что касается времени работы, то легко видеть, что каждый из шагов 1, 3 и 5 выполняется за время О(п). На шаге 4 тело цикла while выполняется не более одного раза для каждой точки (будучи вызванной из стека, точи, никогда вторично в стек не записывается). Следовательно, выполнение mara 4 также занимает время О(п) и общее время выполнения в основном зависит от начальной сортировки на шаге 2, поэтому метод обхода Грэхема выпад, няется за время О(п log п), если используется подходящий способ сортиров-ки. Стоит еще раз напомнить, что метод обхода Грэхема будет выполняться за линейное время, если известно, что набор точек изначально отсортирован.
6.4.	Удаление невидимых поверхностей: алгоритм сортировки по глубине
6.4.1.	Предварительные замечания
В трехмерной машинной графике рассматриваются задачи моделирования сцен в пространстве и затем формируется изображение сцен, отображая их на плоскость путем закрашивания. Для осуществления отображения выбирается точка в пространстве, из которой наблюдается сцена и, на основе задания этой точки зрения и ряда дополнительных параметров, сцена проецируется на плоскость, где и формируется искомое изображение.
Формирование изображения часто осложняется тем, что некоторые объекты в сцене или их части закрыты для наблюдателя и поэтому они не должны появляться в окончательном изображении. Некоторые из объектов могут находиться вне поля зрения (обнаружение таких объектов относится к проблеме отсечения). Кроме того, некоторые объекты (или их части) могут быть закрыты другими непрозрачными объектами, расположенными между ними и наблюдателем. Задача идентификации таких закрытых объектов известна как проблема удаления невидимых поверхностей.
В этом разделе будем решать проблему удаления невидимых поверхностей путем сортировки по глубине. Сцена будет представлена в виде набора треугольников, расположенных в пространстве. Такая модель получила очень широкое распространение в основном потому, что она охватывает очень большое разнообразие сцен. Например, любая поверхность может быть аппроксимирована сеткой треугольников и, если такая сетка достаточно хорошо составлена, то с ее помощью можно представить любую поверхность с необходимой точностью. Даже достаточно грубая сетка полезна на практике, поскольку методы сглаживания, применяемые в процессе отображения, могут значительно улучшить представление о кривизне поверхности.
Будем использовать способ проецирования, при котором точки в про-ХаТ ГГ₽УЮТСЯ На	°Д0ль лучей, параллельных оси Г.
^^2™Л’.^™РОеЦИРУ'!ТСЯ ° Т0ЧКу <х’ °) • Такой способ, известны» под названием ортогональное параллельное проецирование обеспечивает сохранение универсальности: дли заданного набора парамстров прмцироваяи» зедо°Хз “по^, п^ГО ИЗО6₽Т“"П Может	некоторая пос-
выполнению оптогонял^а1ЗПВаПН *’ которая СЯ°ЛИТ проблему отобрпженИЯ * дожить, что точка наблюдения лежит » Л «''лагиеиию можно преДОО а няя лежит в отрицательной части пол у простри»
ррщцгоядя выборка
171
pjja относительно оси г (позади плоскости хр), а сцена расположена в положительной части полупространства по оси г	плоскости ху) и что
глубина увеличивается (т. е. объекты расположены дальше) по мере увеличения координаты г.
В дальнейшем будем также предполагать, что треугольники в сцене оря-ентпрованы так, что они наблюдаются со стороны их отрицательного полу-пространства — их вектор нормали направлен в противоположную сторону от точки наблюдения (рис. 6.5). Такое предположение не очень ограничивает ситуацию, как это может показаться вначале. При использовании сетки треугольников для моделирования поверхности твердого тела треугольники всегда одинаково ориентированы относительно внутренней части твердого тела, например, нормаль направлена внутрь твердого тела. На предварительном шаге, известном как отбор задних поверхностей, такие треугольники, нормали которых направлены в сторону точки наблюдения, исключаются, поскольку они не могут быть видны — между ними и тачкой наблюдения лежит непрозрачное тело. Будем оставлять только такие треугольники, нормали которых направлены в другую сторону от точки наблюдения. Даже когда сетка треугольников используется для представления «свободных» поверхностей, не являющихся границей твердого тела (т. е. не существует ие-прозрачной субстанции, закрывающей треугольники), треугольники должны быть переориентированы, чтобы обеспечить направления их нормали в сторону от точки наблюдения.
6.4.2.	Алгоритм сортировки по глубине
Удаление невидимых поверхностей осуществляется очень просто для набора треугольников, которые не перекрывают друг друге'
Сначала треугольники сортируются по уменьшению к°°р и*	ом
ке от более далеких к более близким) и затем они
порядке. Если треугольник виден, то-	Z '—
п ничто не закрывает его самого. Т«кои nofinT_ художника который моя закрашивания, “““^«“рнТуТЛ^Дяие планы и поверх всего в». в~ T^TeZ-износится поверх предыдущего, более далекого слоя.	,.,„„„„чется тот факт, что совершенно беэ-
В алгоритме закрашивания исп	У	ничего и да, что будет
опасно нарисовать что-либо, если он Р
4»
_ «то СПИСОК треугольников упорядочен в ааоясовано поздвее. roB“pj“; „ вих можно нарисовать а установлю ЖмЛ»"“- если «Хмрнсованных треугольников не закрывав, ном порядке, т. е. »“<“” Хея позже. Более формально список т>
и» «*•'"° уХялокен В порядке B^n«°CT“ Х“7“Т Хльвяков Р1 ч Ра к -	если соблюдается правило, если Р, ч р(,
наблюдения р. если я	m точки р. Сортировкой по «убак,
” Р, не закрывает Р) при п"Х>а треугольников в порядке видим, называется процесс оргавизац
„опускают более одного порядка вид,.
Некоторые наборы треугольников д	даа	ве закры-
мости, в простейшем примере это	м(Жет существовать единственный
ваюшяе друг ДРУга' ^Р ' п ве существует вовсе.
порядок видимости, а ииогдае«	тей становится более сложным да.
Процесс удаления невидимых no iк перекрь1вающпхся по координате г. реаХация ври наличии	нуЖНо было определить, какой из
На основе алгоритма закрапшваяи	сраввевяя каионическнх
двух данных ^УголвХ°ХвеХотяжеввостВ треугольника по оси г (про значений, полученных па основе пр, диапазону координат л. занимав-тлжеииость треугольника по оси - к	она равва ортоговальвоЙ
мому этим ТРе'тол“ХляХ'го прямоугольника). Однако, это иравцло ра-проекции на ось г обрамляюш Р псп0ЛЬ30вать величину г -ботает ве всегда. Если, ваприм р.	каждого треуголь
симальвое значение прог’же^по	А ч В. поскаль-
кГгТ^о^орадочения по видимости при этом не достигается, так № *» у д •*
•ми
мак-
«а, то
6.1.	п>/егцм* цжугоры^гЛ не мг11Г_ г
^ябойюпоадмммл^с^	y’7x»/z/ie»pj no аимитути, если в »гем с<^
Рис. 6.8. Уточнение исходных наборов треугольников ю рис. 6 7 (а) — Л, ч В ч Аз •< Ai; (6) - Ст ч D ч Е ч Gt < Сь
даст порядок видимости.
А закрывает В и поэтому не может быть нарисован первым (их действительны! порядок видимости равен В ч А). Этот пример показывает, что сортировка по глубине в общем случае требует, чтобы заданный список треугольников был реорганизован, даже еслп исходный список был предваритегсьип упорядочен от более далеких к более близким треугольникам. Ниже мы представим алгоритм, который реорганизует предварительно упорядоченны* список путем выполнения последовательности операций перетасовки.
Некоторые наборы треугольников совсем не допускают никакого упорядочения по видимости. Если два треугольника пересекают друг друга, как для примера показано на рис. 6.7а, то невозможно осуществить какое-либо упорядочение по видимости — ни один из треугольников ве может предшествовать другому ни в каком допустимом порядке видимости, поскольку каждый из них закрывает часть другого. Определение порядка видимости может оказаться невозможным и тогда, когда треугольника взаимно не пересекаются. Ни один из треугольников, показанных на рис. 6.76, не может считаться находящимся перед другими двумя, поскольку каждый на
них закрывает часть остальных двух.
Выход из этого затруднительного положения может быть найден в пере-
определении исходного набора треугольников путем деления некоторых тре-
угольников на треугольные части таким образом, чтобы подученный новый
набор треугольников мог быть отсортирован по глубине. Бели треугольник А на рис. 6.7а делится плоскостью треугольника В на части Ai, Аэ  А$ (как показано на рис. 6.8а), то полученный набор треугольников может быть упорядочен в порядке видимости как Ai -< В -< Aj -< A3. Если треуголь-ник С на рис. 6.76 делится плоскостью D на части Ci, С2 и Сэ (рис. 6.86), то получим порядок видимости Cl -< D Ч Е -< Cj -Ч С3. Получение набора треугольников путем разбиения треугольников на части можно назвать лере-определением (уточнением) исходного набора.	_
Описанные идеи могут послужить основой для создания алгоритма сортировки по глубине для набора треугольников. Сначала сортируем треугольники в соответствии с их максимальной глубиной 2м от более долекнх к более близким. Полученный список дает первое приближение (нлп предварительное) упорядочения по видимости. Затем этот список преобразуется в действительно упорядоченный по видимости путем пошагов перегаси списка и, при необходимости, уточнением списка.
Алгоритм работает следующим образом. Пусть удет предв ~ ~ отсортированный в порядке видимости список и пусть р Лцпппгио наоисо Угольник в списке S. Нам нужно определить, будет лн безопасно норноо-
DftTb n _т. в. не будет ли p закрывать какой-либо q для каждого q g s
Для этого мы будем сравнивать р с каждым треугольником q в списке / для которого протяженность по оси г перекрывает протяженность р Цо ‘ же оси. Для каждого треугольника q мы будем проверять, не будет лк « закрывать q. Если окажется, что р не закрывает ни одного из треугольай₽ ков q, то треугольник р можно безопасно нарисовать, следовательно, му ВЬ1 бираем р из S с удалением, рисуем его и снова запускаем алгоритм, йс. пользуя первый элемент в списке S в качестве нового р.
В противном случае, если случилось так, что р закрывает какой-нибудь из треугольников q, то мы должны проверить, не будет ли q также закры. вать р, или не был ли треугольник q уже однажды перетасован. Если об. наружена любая из этих ситуаций, то треугольник q разбивается на части плоскостью р и затем внутри списка S треугольник q заменяется его соот-ветствующими частями (т. е. выполняется операция уточнения). Проверка на имевшую место перетасовку треугольника q необходима для того, чтобы исключить бесконечное зацикливание, которое может появиться при обработке ситуации, подобной изображенной на рис. 6.7. Если не было ни одной такой ситуации, то положение треугольников р и q в списке S взаимно заменяется (операция перетасовки) и работа алгоритма возобновляется, но теперь в качестве первого элемента списка S используется треугольник qt играющий роль нового р.
Программа depthSort осуществляет сортировку по глубине для массива tri из п указателей_на_треугольники и возвращает список треугольников, упорядоченных в порядке видимости. Локальная переменная s указывает на предварительно упорядоченный список треугольников, а окончательно упорядоченный по глубине список определяется переменной result;
Liat<Triangle3D*> ‘depthSort (Triangle3D	int n)
[n] ;
(t, n) ;
цикла while */
List<Triangle3D‘> ‘result = new List<Triangle3D*>;
Triangle3D “t = new (Triangle3D‘) for (int i = 0; i < n; i++) t[i] = new Triangle3D(*tri[i]); insertionsort(t, n, triangleCmp); List<Triangle3D‘> ‘s = arrayToList while (s->length() >0) ( /♦ начало Triangle3D *p = s->first(); Triangle3D ‘q = s->next()• int hasShuffled = FALSE;
£°\г;	;4=s->n«to>
if (q->mark || mayObscure(q, p)) refineList(a, p);
else (
ehuffleList(s, p); haaShuffled = TRUE• break;
) )
if (!hasShuffled) ( e->fir.t(); e->reaove ();
r«sult->append(p)•
выборка
)
|	/* конец цикла while */
return result;
)
Сортировка применяется для сЬоомпппЯОИ«и
упорядоченного списка. Функция’ сравнения trilnTlX™ преЛ8а₽ятвльв“ треугольника Trian9le3D в соответствии !их'
int triangleCmp (Triangle3D ♦a, Triangle3D *b)
< b~>boundingBox()
else
return 0;
I
После этого используется функция arrayToLiat, определенпая ранее в главе 3, для преобразования отсортированного массива указателей в список.
Обращение к функции overlappingExtent (р, q, 2) возвращает значение TRUE, если треугольники р и q перекрываются по координате г (третий аргумент определяет координатную ось через соответствующие индексы О, 1 или 2). При реализации функции overlappingExtent использован тот факт, что два интервала вещественных чисел пересекаются, если и только если левая точка одного из интервалов содержится внутри другого интервала:
bool overlappingExtent (Triangle3D *р, Triangle3D *q, int 1) (
Edge3D pbox = p->boundingBox();
Edge3D qbox = q->boundingBox();
return (((pbox.org[i] <= qbox.org[i]) fit
(qbox.org[i] <= pbox.dest[i]))
( (qbox.org[i] <= pbox.org(i])
(pbox.org[i) <= qbox.dost[i])));
Список s тасуется с помощью функции shuffleList, которая осущест-ьписок S тасуе	элемента р в списке с элементом q, нахо-
вляет взаимную замену первого элемент р дящимся в окне списка:
void shuffleList (bi.t<rri«K>l^*>	*₽’
)	I- »• I
Triangle3D *q = s->val0 J	•	e:;
q->mark - TRUE;	)
s->val(p);	- j 
s->first();	.	•
s->val(q);
\уу урСуГОЛЬНИКОВ
6.4.3.	Сравнение дву	использовано обращение к фуикзд
« программе depthsort был	лЯ треуг0ЛЬЯик р потенад.
ранее в пр гр	определения, _avObscure выполняет пять про-
mayObscure (Р, ^ольвих q. ФУ^”5 EJn какая-либо
из проверок за-ально закрывать РеУдиченяя сложности. Б
то считается, что р не верок в поряд пУ0Л0>кительвым Рюул1’ “ одйа яз проверок ве удовлепю. канчиввет противном случае, есл	Эти пять проверок следу».
Э0Х " потенциально р может закрывать ,
тие: ’	зояы расположения треугольников р « ,
! Не перекрываются ля зовы Р
оои Х’	„„ зоны расположения треугольников р и , п«
2 Не перекрываются ли зоны Р
°™ ™ ожеи ли треугольник ₽ полностью позади или в плоскости тре-3 Расположен да
’ угольника q?	полностью перед или в плоскости тре-
4. Расположен ли треугольник q
угольника р?	ЛЛАКПЯИ треугольников р и q?
5 Не перекрываются ли проекци Р }
5. не пер р	показаны на рис. 6.9.
Ситуации для тестов 3 и 4 пока
Рис. 6.9, Два теста из Ф/н>дии mayObacure; Тест 3 - р находится позади плоскости q, Тест 4 - Q находи* перед плоскостью р
Большинство соелстп
дле готово-Тесты 1 я 2 ”* трех вершив треугогьни С°Отаетств™но. Тест З к™.Р"!ь“'Н"" пх п₽отяжм' полненным если J ЬНика Р относительно ппп классиФицирует положение (иначе говоря в °АИа из аеРШин не скости Я п он оказывается вы-гично, тест 4 ока-? рИцатель«ом полупростпл°КаЗЫВаеТСЯ пеРеД плоскостью ««а , вГв1хоХсХЯ. вЫИ-«а«ньХ е?л7,шеоТРеУГОЛЬНИКа странстве тпеуголвит 0заДИ плоскости п (т °Ди° ка веРпп1Н треуголь-P»jeeti0X™XK^ Тест 5 “М"оля'1тсел ‘ °~»«ительном полупи мые ей два треугпп,* ’ КОгорая возвращает -з С Использ°в«нием функции В функшш „X а ,,Ме,от "£Хош аЧеННе TRUE- «₽«”«•
»«"> Расположеаия ',OCЛ^я<>»зтeль^o‘Ц"eC’,
ив окажется уДОВ еуголь»иков р и г Пь,п°Л"яются все тесты взалм-
•“«икным. Если да ТОХ пор, пок„ один из тестов удет выполнен пи один из них,

ИГ.»ов,Тй- - *
гуровая выборка
то возвращается значение TRUE, показывающее, что возможно закрыт треугольника q треугольником р.
bool mayObscure (Triangle3D *р, Triangle3D *q)
int i;
// тест 1
if («overlappingExtent (p, q, 0)) return FALSE;
11 тест 2
if (!overlappingExtent(p, q, 1)) return FALSE;
// тест 3
for (i = 0; i < 3; i++)
if ((*p)[i].classify(*q) = NEGATIVE) break;
if (i — 3) return FALSE;
// тест 4
for (i = 0; i < 3; i+4)
if ((*q) [i] .classify (*p) = POSITIVE) break;
if (i = 3) return FALSE;
// тест 5
if («projectionsOverlap(p, q)) return FALSE;
return TRUE;
Рассмотрим более подробно тест 5. Чтобы определить наличие перекрытия проекций треугольников р и д, осуществим сначала проецирование треугольников на плоскость ху и получим треугольники Р я Q на плоскости. Затем выполним для них три проверки для обнаружения факта перекрытая. Если какая-либо из этих проверок окажется удовлетворенной, значит^существует взаимное перекрытие проекций р и д. Эти три теста заключаются., в следующем:
1. Находится
l
пГЛ
какая-нибудь вершина P внутри Q?
2. Находится какая-нибудь вершина Q внутри Р?
3. В
которая часть Р, а во втором Перекрытие из-за любых других тестом (хотя некоторые из воз» при выполнении первого'	треугольники р и q н ао>
В функцию projectionsoveriap и	ттоекпии nep°"^TeftWVrMr-
вращается значение TRUE,	(описанная в разделе 4.6) Ж
Внутри используется функция proj I лучения проекций р и q:
bool proj.ctlon.Ov.rlap <Trl.ngl.3D *Р, Tri.ngl.3O *g> (
int answer = TRUE;
Polygon *P = projsct(*p, 0» *>»
Пересекает ли какое-нибудь ребро Р какое-нибудь ребро Q? . первом тесте обнаруживает»
взаимных положений определяется третьим

Polygon *Q » project(*q, 0, 1) ;
for (int i = o; i < 3; P->advance(CLOCKWISE))
if (pointInConvexPolygon(P->point() , *Q)) goto finish;
for (i = 0; i < 3; i++, Q->advance (CLOCKWISE))
if (pointlnConvexPolygon(Q->point(), *P)) goto finish;	_____
for (i = 0; i < 3; i++, P->advance(CLOCKWISE)) (
double t;
Edge ep = P->edge();
for (int j =0; j <3; j++, Q->advance (CLOCKWISE)) ( Edge eq = Q->edge();
if (ep.cross (eq, t) = SKEW_CROSS) goto finish;
) ) answer - FALSE; finish:
delete P;
delete Q;
return answer; )
6.4.4. Уточнение списка треугольников
В алгоритме сортировки по глубине уточнение списка s заключается в разбиении треугольника q плоскостью рассматриваемого полигона р на две или три части и последующей замене треугольника q (внутри списка s) на эти части. Это реализуется функцией refineList, которой передается текущий список s и рассматриваемый треугольник р. Предполагается, что треугольник q является текущим элементом в списке s:
void refineList (List<Triangle3D*> *s, Triangle3D *p) {
Triangle3D q = s->val();
Triangle3D *ql, *q2, *q3;
int nbrPieces = splitTriangleByPlane (*q, *p, ql, q2, q3) ;
if (nbrPieces > 1) (
delete s->reaove();
s->insert(ql);
s->insert(q2);
if (nbrPieces = 3)
s->insert(q3) ;
} »
Разбиение треуг
““ q.	B К*ЧКп^‘п^ны  °л₽еделяемой ниже функцией
тп паРаметры Результате разбмп Роугольиик р. Части тре-Х^львя« Ч делится ’X " 43 (па₽а3м6"е“ИЯ’ яод»Р"Щвются через ссы-лыдается количестве ° дос чести). й *' "° |,сп°льзуется, если ПолУчеиных частей Качсстве Н03прощаемого зна-
Пзшдговзя выборка
int splitTriangleByPlane(TrianalH4r г
Tri.ngl.3D. tgl, Tri«19l’30“t‘’’	«Р.
(	У JO fiq2, Triangl.3D* fcq3)
Point3D crossingPts[2] •
int edgelds[2], cl[3];' double t;
int nbrPts = 0;
for (int i = 0; i < 3; i++)
cl[i] = q[i].classify(p);
for (i = 0; i < 3; i++)
if ( ( (cl [i] «POSITIVE) (cl [ (i+1) %3) «NEGATIVE)) || ((cl [i] «NEGATIVE) £6 (cl [(i+l)%3] «POSITIVE))) ( Edge3D e(q[i], q[(i+l)%3J);
a.intersect(p, t);
crossingPts[nbrPts] =e.point(t);
edgelds[nbrPts++] = i;
if (nbrPts « 0) return 1;
Point3D a = q[edgeIds[0] ] ;
Point3D b = q[(edgelds[0]+1) % 3] ;
Point3D c = q[(edgelds[0]+2) % 3];
if (nbrPts « 1) (
Point3D d = crossingPts[0];
ql = new Triangle3D(d, b, Q, q.id);
q2 = new TriangleSD(a, d, c, q.id);
) else {
Point3D d = crossingPts[0];
Point3D e = crossingPts[1] ;
if (edgelds [1] — (edgelds[0]+1)%3) { ql =	new	Triangle3D(d,	b,	e,	q.id);
q2 =	new	Triangle3D (a,	d,	e,	q.id);
q3 =	new	Triangle3D (a,	a,	c,	q.id);
) else (
ql =	new	Triangle3D(a,	d,	e,	q.id);
q2 =	new	Triangle3D(b,	e,	d,	q.id);
q3 =	new	Triangle3D(c,	e,	b,	q.id);
)
)
return (nbrPts + 1) ; I
г
В первых двух фазах функция splltTriangleByPlane
в которых плоскость Р пересекает ребра	Содержащие точки
сечения сохраняются в массиД® ^edoelds Ребра здесь идентифицируются пересечения, хранятся в массиве edge	_,тггПтп.няке а.
по индексу вершины начала (°,’ 1	вычисляет части тре-
Во второй фазе функция split 9	буквами а. b и с относн-
угольника q. Обозначим верш”"Ь1ТР^ доказано на рис. 6.10. При такой тельно первой точки пересеченияс!, азбиваетСя в соответствии со схе-спстеме обозначений треугольник g	пеоесечения d, или в соответ-
мой (а) рис. 6.10. если 7^ХеуХз^”е X пере^ечення d ж П. ствии со схемами (б) и (в), если	глубине, то в некоторых слул
Что касается характеристик сорт р - от алгоритма разбить весь, чаях п треугольников в пространстве потребуют от алг Р
v
Рис. 6.10. Разбиение треугольника- (а)—на две части; (6) и (в) на три части
список на 0(п2) частей. Поскольку список s теперь будет иметь длину 0(л2), то обработка каждого возможного треугольника р может занять время, пропорциональное длине списка и в худшем случае сортировка по глубине будет выполнена за время О(л^). Однако такие ситуации возникают редко и практически алгоритм работает достаточно хорошо. Кроме того, список полигонов, полученных в результате сортировки по глубине, может быть передан по линиям связи непосредственно в любую графическую систему для отображения. Этого нельзя сказать о всех других методах удаления невидимых поверхностей, поскольку работа некоторых из них зависит от разрешающей способности устройства отображения.
6.5.	Пересечение выпуклых полигонов
Теперь рассмотрим проблему вычисления области пересечения Р A Q двух выпуклых полигонов Р и Q. За исключением особо оговоренных случаев будем предполагать, что два полигона пересекаются невырожденно: пересечение двух ребер происходит в одной единственной точке, не являющейся вершиной какого-либо полигона. Учитывая такое предположение о невырожденности, всегда получим, что полигон Р A Q состоит из попеременных цепочек из Р и Q. Каждая пара последовательных цепочек соединяется в точке пересечения, в которой пересекаются границы полигонов Р и Q (рис. 6.11).
Существует несколько решений этой задачи с линейной зависимостью времени выполнения от суммарного числа вершин. Описываемый здесь алгоритм обладает особым изяществом и его легко применять. Для двух заданных на входе выпуклых полигонов Р и Q алгоритм определяет окно иа ребре полигона Р и еще одно окно на ребре полигона Q. Идея заключается
О
Рис 6.11. Стр/гт/рь nooroe пересеченна ? п О
выборке
» продвижении этих окон вдоль границ полигона по мере формирования полигона пересечения Р П Q: окна как бы проталкивают др^д^^Х границы своих соответствующих полигонов в направлении по часовой сгоел ке для поиска точек пересечения ребер. Поскольку точки пересечения rf. наруживаются в том порядке, а котором они располагаются вокруг политом р П Q, полигон пересечения оказывается сформированным, когда некоторая точка пересечения будет обнаружена вторично. В противном случае, если не будет обнаружено ни одной точки пересечения после достаточного числа итераций, то значит границы полигонов не пересекаются. В этом случае потребуется дополнительный простой тест, не содержится ли один пг>дягой внутри другого или они вовсе не пересекаются.
Для объяснения работы оказывается весьма полезным ввести приятие серпа.. На рис. 6.12 серпами будут шесть затененных полигонов. Каждый из них ограничен цепочкой, взятой от полигона Р, п цепочкой от полигона Q, ограниченных двумя последовательными точками пересечения. Внутренней цепочкой серпа будет та часть, которая принадлежит полигону пересечения. Отметим, что полигон пересечения окружен четным числом серпов, внутренние цепочки которых попеременно являются частями границ полигонов Р и Q.
₽мс. 6.12. Серпы, окружающие полигон пересечет»
В терминах серпов алгоритм поиска полигона пересечения проходит две фазы. В процессе первой фазы окно р полигона Р и окно q полигона Q перемещаются в направлении по движению часовой стрелки до тех пор, пока они не будут установлены на ребрах, принадлежащих одновременно одному и тому же серпу. Каждое окно начинает свое движение с произвольной позиции. Здесь для краткости будем использовать один и тот же символ р для обозначения как окна полигона Р, так и ребра в этом окне. Тогда термин «начало р» будет относиться к точке начала ребра в окне полигона Р» а команда «продвинуть р» будет означать, что необходимо полигона Р на следующее ребро. Аналогичным образом буквой дбудем oto-значать как окно полигона Q, так п ребро в окне. Иногда ребра.р . q считать текущими ребрами,	__тт
Во время фазы 2 р и q продолжают перемещаться в вапрамеикнпо ч совой стрелке^ но на этот раз они двигаются в унисон от одного £рпа н соседнему серпу. Перед тем как любое окно перейдет на соседний, ребра р и q пересекутся в точке "е’(се’'™
серпа. Полигон пересечения строится именно во аре.. » Р фниь ДММ|
каждым перемещением р конечная точка ребра р заносится в полигон пе сечения, если ребро р принадлежит внутренней цепочке текущего сеп^ Аналогичным образом перед перемещением q фиксируется конечная топ° ребра q, если q принадлежит внутренней цепочке текущего серпа. При кЛа дом пересечении ребер р и q точка пересечения, в которой они Пересе • ются, записывается в полигон пересечения.
Для принятия решения, какое из окон должно перемещаться, в алгол ме используется правило перемещения. Это правило основано на следуют замечаниях: говорят, что ребро а нацелено на ребро Ь, если бесконечв * прямая линия, определяемая ребром Ь, расположена перед а (рис. 6 13)
Рис. 6.13. Только показанные толстыми линиями ребра нацелены на ребро q, остальные - нет
Ребро а нацелено на Ь, если выполняется одно из условий:
•	"а х > 0 и точка a.dest не лежат справа от 1) или
•	"а х 7? < 0 и точка a.dest не лежат слева от 1) .
Заметим, что соотношение ~а х 7? > 0 соответствует случаю, при котором угол между векторами а п b меньше 180 градусов
Функция aimsAt возвращает значение TRUE, если и только если ребро а нацелено на ребро Ь. Параметр aclass указывает на положение конечной точки a.dest относительно ребра Ь.
Параметр crossType принимает значение COLLINEAR, если и только если ребра а и b коллинеарны.
bool aimsAt(Edge Sa, Edge Sb, int aclass, int crossType) (
Point2 va = a.dest a.org;
Foint2 vb = b.dest b.org;
if (crossType != COLLINEAR) (
if ((va.x ♦ vb.y) >= (vb.x • va.y))
return (aclass RIGHT);
else
return (aclass ! = LEFT);
) else (
return (aclass != BEIOND);
1 )
Если ребра w	---------- ии<-'тоятельство используется для tui««
чтобы продвинуть а вместо Ь, когда два ребра пересекаются пырождеи^
неИлежи-^п^гТ₽ьЫк2° ребр0 a иа1*елс1’° ira b. если конечная »уть а имргтл 1	° °®стоятелестно используется для того.
^ее, чем в одной точке. Позволяя а «догонять» Ь, мы обеспечиваем что ни одна точка пересечения не будет пропущена.	*
Возвратимся к обсуждению правил перемещения. Они сформулированы таким образом, чтобы не пропустить следующую точку пересечения. Правила отличают текущее ребро, которое может содержать следующую точку пересечения, от текущего ребра, которое возможно не может содержать следующей точки пересечения, причем в этом случае окно переносится вполне безопасно. Правила перемещения различают четыре ситуации, показанные на рис. 6.14. Здесь ребро а считается находящимся вне ребра Ь, если конечная точка a.dest расположена слева от Ь.
(а)	(б)
Рис. 6.14. Четыре правила перемещения: (а)- продвинуть р; б-продвинуть р,- в-продджулр; г—продвинуть р
1.	р и q нацелены друг на друга', перемещается окно, соответствующее
тому ребру (р или д), которое находится снаружи другого. В сптуацз
рис. 6.14а должно быть перенесено окно на ребре р. Следующая ючзгя
пересечения не может лежать ня р, поскольку ребро р находится вне
полигона пересечения.
2.	р нацелено на q, но q не нацелено на р; конечная концевая точка ребра р заносится в полигон пересечения, если р не находится снаружи q и затем окно р переносится. На рис. 6.146 ребро р не может содержать следующей точки пересечения (хотя оно может содержать некоторую точку пересечения, если р находится не снаружи от q). На рис* показана ситуация, в которой ребро р, окно которого должно быть перенесено, не находится снаружи от ребра q.
3.	q нацелено на р. но р не нацелено на qx конечная концевая точна ребра q заносится в полигон пересечения, если q не находится снаружи от р, после чего переносится окно q (рис. 6.14в). Этот случай спммырн-чен предыдущему. На рис. показана ситуация, при которой ребро окно которого должно быть перенесено, находится снаружи от ребра
Р-	________
4.	р и q не нацелены друг на друга-, переносится то окно, носится к ребру, расположенному снаружи от ДРУГОГО-Согласно рис. 6.14 необходимо перенести окно р. поскольку ребро р находится снаружи от ребра q.	_
Работа алгоритма показана на рис. 6.15. Каждое ребро имеет метку если оно обрабатывается на шаге i (у некоторых Р	оебоа имели
метка, поскольку они обрабатываются ДВШЩДО)»
8
Рис. 6.15. Поиск полигона пересечения. Для ребра указана метка i, если оно обрабатывается на шаге i д -чальных ребра обозначены меткой 0	Два на-
метку 0 На этом рисунке фаза 2 (когда два текущих ребра оказываются принадлежащими одному и тому же серпу) начинается после трех итераций. Алгоритм реализован в программе convexPolygonlntersect. Программе
list
передаются полигоны Р и Q. она возвращает указатель на результирующий полигон пересечения R. Обращение к функции advance использовано для переноса одного из двух текущих ребер и для включения конечной конце, вой точки ребра в полигон R в соответствии с выполнением определенных условий. Используются окна, существующие внутри класса Polygon.
enum ( UNKNOWN, P_IS_INSIDE, Q_IS_INSIDE);
Polygon *convexPolygonIntersect(Polygon &P, Polygon &Q) (
Polygon *R;
Point iPnt, startPnt;
int inflag = UNKNOWN;
int phase = 1;
int maxltns = 2 * (P.sizeO + Q.sizeO);
// начало цикла for
for (int i = 1; (i<=maxltns) | | (phase=2) ; i++) (
Edge p = P.edgeO;
Edge q = Q.edgeO;
int pciass = p.dest.classify(q);
int qclass = q.dest.classify(p);
int crossType = crossingpoint(pf q, iPnt) ;
if (crossType == SKEW_CROSS) (
if (phase = 1) { ~
phase =2;
R = new Polygon; R->insert(iPnt); startPnt = iPnt;
) else if (iPnt ! R->point()) if (iPnt !« startPnt) R->insert(iPnt);
—
if (pclass—RIGHT) inflag  P_I8_INSIDE;
•Ise if (qolass—RIGHT) inflag  Q IS INSIDE; else inflag  UNKNOWN;
else if ((crossType—COLLINEAR)
(pclass ! BEHIND) &&
(qclass != BEHIND)) inflag = UNKNOWN;
bool pAIMSq = aimsAt(p, q, pclass, crossType);
bool qAIMSp = aimsAt(q, p, qclass, crossType) ;
if (pAIMSq && qAIMSp) {
if ( (inflag = Q_IS_INSIDE) | |
((inflag = UNKNOWN) £& (pclass — LEFT))) advance(P, *R, FALSE);
else advance(Q, *R, FALSE);
} else if (pAIMSq) ( advance (P, *R, inflag = P_IS_INSIDE) ;
) else if (qAIMSp) ( advance (Q, *R, inflag = Q_IS_INSIDE) ;
) else ( if ((inflag = Q_IS_INSIDE)
((inflag = UNKNOWN) && (pclass = LEFT))) advance (P, *R, FALSE);
else advance(Q, *R, FALSE);
}
}	// конец цикла for
if (pointlnConvexPolygon (P.point () , Q)) return new Polygon(P);
else if (pointlnConvexPolygon(Q.point (), P))
return new Polygon(Q) ;
return new Polygon;
I
Если после выполнения 2(|P| + |Q|) итераций не одной точки пересечения, то основной цикл завершу _ *’ обращения ж начает, что границы полигона не пересекаются.
— p-ntxnco—^n	=—“
некоторая точка пересеченнаi IPntкиГточва полигона пересечения R и остан авллн	.	м»
iPnt будет обнаружена вторично.	входных полигонов».
Переменная inflag показывает, какой из двух	ПОЛШ?(ОД
ходится в данный момент внутри	внутренней цепочки текущего
текущее ребро которого нах°*и* принимает значение UNKNOWN (шшз-серпа. Более того, переменная int g Р	когда оба текущих ребра
вестно) во время первой фазы, а	Значение этой переменной
коллинеарны или перекрывают дру тпчкп пересечения. • 'л *4 обнаружении новой точки Переса**»* ^а1Т~Яятттп^ обнаружен* _екущее ребро полигона А, представляю
—г—----------- -	вносит конечную концевую точку
щее либо Р, либо Q. Эта же ПРОЦ^Р иахОдится внутри другого полигона.
В полшип	--— - -	я R-	; ' Л»М.
была последней точной, записанной в R.
void advance (PolygonZ ДА, PolygonZ SR, lot
няется при каждом
Процедура advance продвигает
1 ________ ж ____
ребра г в полигон пересечения Rt
и х не I

A.advance(CLOCKWISE);
if (inside &£ (R.pointO !“ A.point ())) R. insert (A.point 0 ) ;
}
6.5.1. Анализ и корректность
Доказательство корректности показывает наиболее важные моменты ра. боты алгоритма — один и тот же набор правил продвижения работает в течение обеих фаз. Правило продвижения заносит р и q в один и тот же серп и затем передвигает р и q в унисон из одного серпа в другой.
Корректность алгоритма следует из двух утверждений:
1. Если текущие ребра р и q принадлежат одному и тому же серпу, тогда можно будет найти следующую точку пересечения, в которой закая-чипяется текущий серп, и эта точка будет именно следующей.
2. Если гряттцы полигонов Р и Q пересекаются, то текущие ребра р и q пересекутся в некоторой точке пересечения после не более, чем 2(|Р| + |Q[) итераций.
Утверждение 2 обеспечивает, что алгоритм найдет некоторую точку пере-сечения, если таковая существует. Поскольку ребра р и q принадлежат одно
му и тому же серпу, если они пересекаются, то из утверждения 1 следует, что остальные точки пересечения будут найдены в нужном порядке.
Рассмотрим сначала утверждение 1. Предположим, что р и q принадле
жат одному и тому же серпу и q достигает некоторой точки пересечения раньше, чем р. Мы покажем, что тогда q останется неподвижным, пока р не достигнет точки пересечения после ряда последовательных продвижений. Могут возникнуть две ситуации. Сначала предположим, что р находится снаружи от q (рис. 6.1 ба). При этом q останется фиксированным, пока р будет продвигаться согласно нулю пли нескольким применениям правила 4, затем нулю или нескольким применениям правила 1 и потом нулю или нескольким применениям правила 2. Во второй ситу я пип предположим, что р не находится снаружи от q (рис. 6.166). Здесь q будет оставаться фиксированным, пока р будет продвигаться путем нуля или нескольких применений правила 2. В симметричной ситуации, когда р достигает точка пересечения раньше, чем q, ребро q остается фиксированным, а ребро q продвигается до точки встречи. Это можно показать аналогичным образом, только меняется роль р и q и правило 3 заменяет правило 2. Из этого следует утверждение 1.
Чтобы показать утверждение 2, предположим, что границы Р и Q пересекаются. После |Р| + |<?| итераций либо р, либо q должны совершить полный оборот вокруг своего полигона. Предположим, что это произошло с р. В некоторый момент времени ребро р должно расположиться так, что оно будет содержать точку пересечения, в которой полигон Q переходит извне полигона Р внутрь его. Это происходит потому, что существуют по кр не мере две точки пересечения и направление пересечения меняется. конГ/Л бУАСТ РебР°М виутри окна полигона Q в момент обнаружения тб-
На рис. 6.17 границе полигона Q разбита на две цепочки Сг и С,. Первая мГХтвь пмнгонГГ" ’ Р*6ре ’ Тои р"бр" •«лигонп Q. которое »» ДИТ внутрь полигон. Р через его ребро р. Дру,„„	с, з,1КП..чио.е««
fwnfoa&t выборка
в ребре qs, конечная точка которого лежит справа от бесконечной прямой дпнпи, определяемой ребром р, и она наиболее удалена от этой прямой линии. Следует рассмотреть два случая в зависимости от того, какой из двух цепочек принадлежит ребро q:
Случай 1 |q Е Сг|. Здесь р остается фиксированным, тогда как q продвигается согласно нулю или нескольким применениям правила 3, затем правила 4, потом правила 1 и, наконец, правила 3, когда обнаруживается точка пересечения
Случай 2 [q, G Сг]. Здесь q остается фиксированным, а р продвигается согласно нулю или не* скольких применений правила 2, затем правила 4, потом оравила 1 и, наконец, правила 2 в мо-гент, когда р окажется внутри q. С этого момента оба ребра р и q могут продвигаться несколько раз, однако ребро q не может быть продвинуто за его следующую точку пересечения, пока р сначала не достигнет предыдущей точки пересечения ребра q (если этого уже не произошло с ребром р). Поскольку р и q заканчиваются в одном и том же серпе, утверждение 1 гарантирует, что после некоторого числа дополнительных продвижений они пересекутся в точке пересечения, в которой заканчивается текущий серп.
Чтобы показать, что достаточно 2(|Р| + |Q|) итераций для поиска некоторой точки пересечения, заметим, что при доказательстве утверждения 8 (о том, что граница полигона Q входит внутрь полигона Р через рерро р при произвольном положении ребрв q) начальные позиции р и q достигались при выполнении не более |Р| + |Q| итераций. Фактически такая ситуация. либо симметричная ей, в оторой роли р и q взаимно заменяются. Достигается за меньшее число итераций. Поскольку после этого ни р, q не будут продвинуты на полный оборот прежде, чем будет первая точка пересечения, потребуется не более |Р| + |Q| Д продвижений.
прежде, чем будет достигнута
6.5.2. Затруднения
Ня пт алгоритм определения пересечения двух выпуклых полигонов очевь чувствителен к ошибкам округления, когда два полигона пересекаются в точке, являющейся вершиной одного или обоих полигонов. Одна из проблем заключается в том, что такая точка может быть пропущена. В одном случаев на рве. 6.13 ребра р и q пересекаются в точке х, являющейся ко. вечной концевой точкой ребра р. При использовании точной арифметика параметрическое значение х вдоль отрезка р равно единице. Однако н ариф. метике с плавающей точкой фактически вычисленное значение может не сколько превышать единицу, что соответствует положению точки х после конца ребра р и такая точка может оказаться необнаруженной.
С целью обойти подобное затруднение при вычислении точки пересечения двух ребер в программе convexPolygonlntersect применяется функция crossingPoint. Для заданных двух ребер е и f функция сначала вычисляет точку, в которой пересекаются бесконечные прямые линии, определяемые ребрами е и f. Если эта точка оказывается в непосредственной близости одного из четырех концов упомянутых ребер, то в качестве точки пересечения берется ближайшая концевая точка. В силу особенностей применения функция работает с параметрическими значениями, а не с самими точками. Путем расширения диапазона параметрических значении вдоль ребра ( ребро фактически удлиняется на величину EPSILON2 в обоих направлениях. Если точка пересечения при вычислении находится в пределах EPSILON2 от одной из концевых точек ребра f, то точка пересечения принудительно •привязывается» к этой концевой точке. То же самое правило применяется и к ребру е.
Функция crossingpoint возвращает одно из значений COLLINEAR, PARALLEL, SKEW_NO_CROSS или SKEW_CROSS (коллинеарны, параллельны, не пересекаются, пересекаются), показывающее взаимное положение ребер е и 1.. Если возвращается значение SKEW_CROSS, показывающее, что ребра имеют точку пересечения, то эта точка пересечения возвращается через ссылочный параметр р:
♦define EPSTLON2 12-10
int orossingPoint(Edge &e, Edge fcf, Point bp)
double s, t;
int clasee = e.intersect(f, s);
if ( (cluse — COLLINEAR) | | (classe = PARALLEL) ) return classe;
double lene - (a.dest-e.org) length();
if ((» < -EPSIIZW2»lene) (((s > 1.0+EPSILON2*lene)) return SKEW_N0_CR0S3;
. .----------—-	M 6b (t < 1.0+EP5ILON2 * lenf) )
if (t < EP8ILON2*lenf) p • f.org;
• if (t. >• 1
-•«MiK.'Knti p s ш . org, ее if (» >- 1.0-EPSIIXJN2*lene) n - -
return SKEW_CROSS;
) else
return SKEW NO CROSS;
I
Если в качестве основы для вычисления точек пересечения брать функцию crossingPoinто наша программа определения пересечения выпуклых полигонов будет устойчиво работать даже тогда, когда полигоны пере-секаются в вершинах. Это особенно важно для нас. так как в главе 8 эту программу будем использовать в приложениях, в которых неизбежно может встретиться такая ситуация. Но заметим, что наша программа может дать ошибку, если вершины одного из полигонов располагаются очень близко (в пределах EPSILON2) от границ другого полигона, но практически не касаясь границы.
6.6. Определение триангуляции Делоне
Триангуляция для конечного набора точек S является задачей триангуляции выпуклой оболочки 6W(S), охватывающей все точки набора 8. Отрезки прямых линий при триангуляции не могут пересекаться — они могут только встречаться в общих точках, принадлежащих набору S. Поскольку отрезки прямых линий замыкают треугольники, мы будем считать ях реб-
рами. На рис. 6.18 показаны два различных варианта триангуля ции для одного и того же набора точек (временно проигнорируем окружности, про
веденные на этих рисунках).
Для данного набора точек S мы можем видеть, что все точки из набора S могут быть подразделены на граничные точки — те точки, которые лежат на границе выпуклой оболочки CH (S), и внутренние тачки лежащие внутри выпуклой оболочки CH (S). Также можно классифицировать и ребра, полученные в результате триангуляции S, как ребра оболочки и внутренние ребра. К ребрам оболочки относятся ребра, расположенные вдоль границы выпуклой оболочки tW(S), а к внутренним ребрам - все остальные ребра, образующие сеть треугольников внутри выпуклой оболочки. Отметим, что каждое ребро оболочки соединяет две соседние граничные точки, тогда как внутренние ребра могут соединять две точки любого типа. В частности, если внутреннее ребро соединяет две граничные точки, то оно является выпуклой оболочки CW(S). Заметим также, что каждое ребро триангуляции
’’чс. 6.1 в. дм pavjг<мых мрнанта тр^внгуляцин М* одного набора точек
7 Зпкпл 27П
, „.ждое внутр«ш« ребро ааходие, . двух областей к»*» о6олочкн - между треуг^. границей ДВУ* каждое рео₽°
является П угодьвикаМИ’ а
между ДВУМ* ^Х°ой плоскостью. я,которых тривиальных случаев, ». никои и 6е«в“еч“° 38 исключением век Р^ этом сущестаует
Любой иаб0₽ Хо сдособа	данного набора опре.
пускает более од^. яю6ой способ трн^	„3 теоремы;
нательное “ алело треуголен	^ддаим. что набор точек S сом»
деляет одина	и набора точек), npw	точек из них являюта
3.	°*® TOfW П₽"
житлгЗ’оче»	внутри выпукл	п + 1_2 треугольники
5 бУАЯ
Для доказательства теоремы рассмотрим сначала триангуляцию n~i граничных точек. Поскольку все они являются вершинами выпуклого полигона, то при такой триангуляции будет получено (и - I) - 2 треугольников. (В этом нетрудно удостовериться и, более того, в главе 8 мы покажем, что любая триангуляция произвольного m-стороннего полигона — выпуклого или невыпуклого — содержит т - 2 треугольника). Теперь про верим, что будет происходить с триангуляцией при добавлении оставшихся I внутренних точек, каждый раз по одной. Мы утверждаем, что добавление каждой такой точки приводит к увеличению числа треугольников на два. При добавлении внутренней точки могут возникнуть две ситуация, показанные на рис. 6.19. Во-первых, точка может оказаться внутри некоторого треугольника и тогда такой треугольник заменяется тремя новыми треугольниками. Во-вторых, если точка совпадает с одним из ребер триангуляции, то каждый из двух треугольников, примыкающих к этому ребру, заменяется двумя новыми треугольниками. Из этого следует, что после добавления всех i точек, общее число треугольников составит (л - I — 2) + (21), или просто п - I - 2.
РМС 6.19. две опуад*. Бсзииисиие w тр^г/л,^ до6звАениа ноеой	точки
В этом Риделе мы ппелгтл
хХТсбГЛЯДИй’ КЗа™ Х\Х°РИ™ фор^рован„я специального мятся к па аисировяиа в том смыслТТ"^^ Эта триангуляция Рис. 6.18а “°Г°ЛЬНОСТИ- Так напримео форм,,РУсмые треугольники стре-ТРмЛлл^я°П"!СП “ ™пуТри"в^««»к>. ..зобрпже.шую » нельзя отнести содержит несколько сильиг7ЛЯЦИИ ^слонс’ ° но Р«*с. 6.186 Делоне для иабооГк?, ^ело,,с- На рис, 6° £,итянУтых треугольников л ее Р большого число точек покплл” пример триангуляции
lull
рис. 6.20. Триангуляция Делоне для 250 гачек, выбранные случайным образом в пределах прхмоугольни» Всего образовано 484 треугольника
Для формирования триангуляции Делоне нам потребуется несколько новых определений. Набор точек считается круговым, если существует некоторая окружность, на которой лежат все точки набора. Такая окружность будет описанной для данного набора точек. Описанная окружность для треугольника проходит через все три ее (не коллинеарные) вершины. Говорят, что окружность будет свободной от точек в отношении к заданному набору точек S, если внутри окружности нет ни одной точки на набора S. Но, однако, точки из набора S могут располагаться на самой свободной от точек окружности.	_
Триангуляция набора точек S будет триангуляцией Делоне, если описанная окружность для каждого треугольника будет свободна от точек. На схеме триангуляции рис. 6.18а показаны две окружности, которые явно не содержат внутри себя других точек (можно провести окружности иДЛЯ ДРУ" гих треугольников, чтобы убедиться, что они	поовед^н-
бора). Это правило не соблюдается на схеме рис. .
ной окружности попала одна точка другого треугольника,	*
триангуляция не относится к типу Делоне.	8 чтобы
Можно сделать два предположена-™™бы
упростить алгоритм триангуляции. Во пер »	мепе
триангуляция. L должпы полагать что
три точки и они не коллинеарны. Во-втор , ДО набора 3 не лежал* Пии Делоне необходимо, чтобы“^ХвадетГчто без такого предположения «а одной описанной окружности. Лег Д	на одной описанной,
триангуляция Делоне не будет
окружности позволяют реализовать две р i •	~ текущей триан-
Наш алгоритм работает путем постоянного наращ^^теку1да^_
гуляции по одному треугольнику за одп® ’ оконча ляция состоит из единственного ре ра о тпвангул'"™* Горитма текущая триангуляция станов	*
каждой итерации алгоритм ищет ковы < тр уголь стен к границе текущей триангуляции.
Определение границы зависит от следующей схемы классификации w6e триангуляции Делоне относительно текущей триангуляции. Каждое может быть спящим, живым или мертвым.
спящие ребра: ребро триангуляции Делоне является с еще не было обнаружено алгоритмом,
если Ойо
•	живые ребра: ребро живое, если оно обнаружено, но известна только одна примыкающая к нему область;
•	мертвые ребра: ребро считается мертвым, если оно обнаружено и известны обе примыкающие к нему области#
Вначале живым является единственное ребро, принадлежащее выпуклой оболочке — к нему примыкает неограниченная плоскость, а все остальные ребра — спящие. По мере работы алгоритма ребра из спящих становятся живыми, а затем мертвыми. Граница на каждом этапе состоит из набора живых ребер.
На каждой итерации выбирается любое одно из ребер е границы и оно подвергается обработке, заключающейся в поиске неизвестной области, которой принадлежит ребро е. Если эта область окажется треугольником (, определяемым концевыми точками ребра е и некоторой третьей вершиной и, то ребро е становится мертвым, поскольку теперь известны обе примыкающие к нему области. Каждое из двух других ребер треугольника t пере
водятся в следующее состояние: из спящего в живое или из живого в мертвое. Здесь вершина и будет называться сопряженной с ребром е. В противном случае, если неизвестная область оказывается бесконечной плоскостью, то ребро е просто умирает. В этом случае ребро е не имеет сопряженной вершины.
На рис. 6,21 показана работа алгоритма, где действие происходит сверху вниз и слава направо. Граница на каждом этапе выделена толстой линией.
Алгоритм реализован в программе delaunayTriangulate. Программе задается массив s из п точек и она возвращает список треугольников, представляющих триангуляцию Делоне:
Liat<Polygon*> ♦ (Point в[], int n)
Point p;
List<Polygon*> ‘triangles = new List<Polygon*>-
Dictionary<Edge‘> frontier(edgeCmp);
Edge *e = hullEdgefs, n) ;
frontier.insert(e) ;
while (!frontier.isEmpty()) {
e = frontier.removeMin();
if (mate(*6, s, n, p)) {
U2a^rOntier(frOntier* P' e->org);
e->dest, p);
,	(tIia„91e (e_>ot' p'L><Jeot
delete e;
)
return triangles;
^-“'П-ляцию, записываются в список
langlea. граница представлена словарем frontier живых ребер. Ка«Д«
Рис. Ml. Наращивание гривиугииииДемие. Ребра, .ходящие в сое» -рани». вше^квягЛаН*»*».
ребро направлено, так что неизвестная область для него (подлежащая Оц. ределению) лежит справа от ребра. Функция сравнения edgeCmp испод^у. ется для просмотра словаря. В ней сравниваются начальные точки двух ребер, если они оказываются равными, то потом сравниваются их концевые точки:
int edgeCmp(Edge *а, Edge *b) (
if (a->org < b->org) return 1;
if (a->org > b->org) return 1;
if (a->de8t < b->dest) return -1;
if (a->deet > b->dest) return 1; return 0;
}
Как же изменяется граница от одного шага к другому и как функция update Frontier изменяет словарь ребер границы для отражения этих изменений? При подсоединении к границе нового треугольника t изменяются состояния трех ребер треугольника. Ребро треугольника t, примыкающее к границе, из живого становится мертвым. Функция updateFrontier может игнорировать это ребро, поскольку оно уже должно быть удалено из словаря при обращении к функции removeMin. Каждое из двух оставшихся ребер треугольника t изменяют свое состояние из спящего на живое, если они уже ранее не были записаны в словарь, или из живого в мертвое, если ребро уже находится в словаре. На рис. 6.22 показаны оба случая. В соответствии с рисунком мы обрабатываем живое ребро а/ и, после обнаружения, что точка Ь является сопряженной ему, добавляем треугольник Aafb к текущей триангуляции. Затем ищем ребро fo в словаре и, поскольку его там еще нет и оно обнаружено впервые, его состояние изменяется от спящего к живому. Для редактирования словаря мы повернем ребро fb так, чтобы примыкающая к нему неизвестная область лежала справа_от него и запишем это ребро в словарь. Затем отыщем в словаре ребро Ьа — поскольку оно есть в нем, то оно уже живое (известная примыкающая к нему область — треугольник Дп&с). Так как неизвестная для него область, треугольник bafb, только что была обнаружена, это ребро удаляется из словаря.
Функция updateFrontier редактирует словарь frontier, в котором изменяется состояние ребра из точки а в точку Ь:
6.2S. Псмд/осмие тре^&г^мса Да/О г	лГ
void updateFrontier (Dictionary<Edge*> Point 4a, Point fib)
1 Edge *e = new Edge(a, b) ; if (frentier.find(e)) frontier.remove(e) ;
else (
e->flipO ; frontier.insert(e) ; )
«frontier,
Функция hullEdge обнаруживает ребро оболочки среди п точек массива 5. В этой функции фактически применяется этап инициализации и первой итерации метода заворачивания подарка:
Edge *hullEdge (Point з[], int n) (
int m = 0;
for (int i = 1; i < n; i++) if (s[i] < s[mj)
m = i;
swap(a[0], s[m]);
for (m = 1, i = 2; i < n; i++) {
int c = s [i].classify(s[0] , s[m]);
if ((c = LEFT) || (c == BETWEEN)) m = i;
)
return new Edge(s[0], s[m]);
)
Функция triangle просто формирует и возвращает полигон для трех точек, передаваемых ей в качестве параметров:	•
Polygon * triangle (Point fia, Point fib, Point fic)	. .
(	...’ft
Polygon *t = new Polygon;	'ч •.
t->insert(a) ;	.... уд-ру
t->insert (b);	,4
t->insert(c) ; return t;	* ’ ‘ .
।	*• •*. *-*41 "Д
6.6.1, Поиск сопряженной точки для ребра
Обратим внимание на задачу, решаемую функцией mate, которая О1фе-"	-------------чка к, если
определяет
деляе'т. существует ли	ЗГре”р“
она есть, находит ее. Рассмотрим „походящих через его концевые точки бесконечное семейство	’в00тай через C(a.W (Р«с. 6.23). .......
а и Ь. Обозначим это семейство о ру	прямой лшшв. .
Центры окружное^ “«'«"“шей через его центр, « ДИ » Дикулярной отрезку об и	т0₽чкамп на этом пернида
установить однозначное соот ссмейотва параметризуем перпевдик Для спецификации окружностей семейства пар
too
Рис. 6.23. Четыре окружности из семейства Г(а,Ь), определяемого ребром аб и их параметрические значок
припишем каждой окружности параметрическое значение положения ее центра. Средства, описанные в главе 4, позволяют определить естественную параметризацию: ребро ab поворачивается вокруг своей средней точки на 90 градусов до совпадения с перпендикуляром и затем можно использовать параметрические значения точек вдоль этого ребра. На рис. 6.23 используется запись С г для обозначения окружности, соответствующей параметрическому значению г.
Как же найти сопряженную точку для некоторого живого ребра об среди множества точек набора S? Предположим, что окружность Сг является описанной окружностью для известной области ребра ab (на рис. 6.24 такой известной областью будет треугольник &abc). Если известная область для ребра ab является неограниченной, то г = -« и Сг представляет собой полуплоскость, лежащую слева от ао. Нам нужно найти такое наименьшее значение t > г, чтобы некоторая точка из набора S (отличная от точек а и Ь), принадлежала окружности Ct. Если не существует такого значения t, то ребро ао не имеет сопряженной точки. Более образноэто соответствует надуванию двухмерного пузыря, привязанного к отрезку ао. Если такой пузырь достигает некоторой точки из набора S, то эта точка является сопряженной отрезку ab (точка d на рис. 6.24). В противном случае, если не встретится ни одной такой точки из S, то пузырь разрастается до заполнения всей бесконечной полуплоскости справа от отрезка ао и тогда отрезок ао не имеет сопряженной точки.
Почему же работает такой алгоритм? Пусть Сг обозначает описанную окружность известной области отрезка ab и Ct — описанную окружность не-
ГИС- 6.14. СПРСДДАСММ!

Пошаговая выборка
известной области отрезка al. Здесь t > г и t = », если отрезок не им орт сопряженной точки. Будет ли окружность С/ свободной от точек, что кам нужно? Слева от отрезка ао окружность Ct должна быть свободной от точек, поскольку Сг свободна от точек, а часть С«, лежащая слева от ребра ао, находится внутри Сг. Справа от ребра al Ct также должна быть свободна от точек, поскольку если бы некоторая точка q попала бы внутрь этой окружности, то она бы принадлежала окружности Ct G С(а,Ь), где г <  < t, что противоречило бы нашему выбору I, В нашей аналогии о пузырем расширяющийся пузырь достиг бы точки q до того, как он достиг сопряженной точки ребра ао.
При поиске сопряженной точки для ребра al мы будем рассматривать только те точки р Е S, которые лежат справа от al, Центр окружности, опи-санной вокруг любых трех точек а, Ь, и с, лежит на дересечении перпендикуляров, проведенных через середины отрезков ао и Ьр. (Здесь используется тот факт, что перпендикуляры в серединах ребер треугольников пересекаются в центре описанной окружности треугольника.) Вместо вычисления положения центра окружности мы будем вычислять его параметрическое значение вдоль перпендикуляра к середине ребра al. Таким образом мы можем осуществить поиск наименьшего параметрического значения.
Эта методика реализована в функции mate, которая возвращает значение TRUE, если ребро е имеет сопряженную точку, и FALSE, если такой точки нет. Если сопряженная точка существует, то она возвращается через ссылочный параметр р:
bool mate (Edge £е, Point s(], int n, (
Point *beatp = NULL;
double t, bestt = FLT_MAX;
Edge f = e;
f.rot(); // f - перпендикуляр for (int i  0; i < n; i++)
if (s(i).classify(e) =* RIGHT) Edge g(e.dest, s[ij);
g.rot() ; f.intersect(g, t); if (t < bestt) ( bestp = &s[i]; bestt = t;
) ) if (bestp) ( p ° ‘bestp; return TRUE;
Point &p)
в середина отреака e
{	.	4 j
; «-J

return FALSE;
на самую bestt
В функции mate переменная bestp указывает найденную к данному моменту, а в переменно ияпм BTV рическос значение для окружности, которое пр А помним, что при атом анализируются только те точки, что
от ребра о.
Этот алгоритм для вычисления триангуляции Делоне по набору ва точек выполняется ва время О(л2), поскольку при каждой итерации из гр,, шщы исключается одно ребро. Поскольку каждое ребро исключается иа гр,. вицы точно однажды - каждое ребро относится к границе однажды в исключается из нее, никогда ве возвращаясь - число итераций ранее чяМу ребер в триангуляции Делоне. Согласно теореме о триангуляции цабор, точек любая триангуляция содержит не более, чем О(п) реберt поэтому ал. горитм пыполняет 0(н) итераций. Поскольку на каждую итерацию тратится время О(л). то полностью алгоритм выполняется за время О(л )г
6.7. Замечания по главе
Метод заворачивания подарка (известный также как обход по методу Джарвиса) назван по способу обхода вокруг границы выпуклой оболочки, он описан в [43]. Диалогичная основная идея может быть применена для поиска выпуклой оболочки точек более высоких размерностей [17]. В трехмерном пространстве этот способ как раз и напоминает нам действие по заворачиванию подарка. Сканирование по методу Грэхема представлено в [33].
Существует много алгоритмов для поиска выпуклой оболочки и некоторые пз них описаны в этой книге: построение оболочки прп вводе, выполняемой за время О(л2), где п — количество точек; сканирование плоскости в течение времени О(п log л); сканирование по методу Грэхема за время О(п log л); заворачивание подарка за время О(лй), когда выпуклая оболочка содержит й < п вершин; слияние оболочек за время О(п log п). Здесь не был
рассмотрен один из интереснейших алгоритмов быстрого построения оба лочки. Подобно алгоритму быстрой сортировки, на основе которой он создан, быстрое построение оболочки занимает время О(л2) в худшем случае, но наиболее вероятно время О(л log п) [14, 25, 34]. Оптимальный алгоритм поиска выпуклой оболочки был сформулирован Киркпатриком и Зайделем [46]. Если формируемая в этом алгоритме оболочка содержит й вершин, то он выполняется за время О(л log й) в худшем случае.
Поскольку алгоритмы удаления невидимых поверхностей обязательно присутствуют при реалистичном отображении сцен в трехмерной графике, то эта проблема была в центре многих исследований, что привело к различным решениям. Эти решения зависят от применяемой системы моделирования трехмерной сцены, эффективности, требуемой степени реализма и других факторов. Способ сортировки по глубине, описанный в данной книге, взят из [ ]. Другие хорошо известные методы включают буферизацию и° оси г [16], метод деления областей Варнока [86], метод Вейлера-Атертонэ «слоеной булочки» [88], метод построчного сканирования [50, 87] и трассировки луча. Описания некоторых алгоритмов можно найти в различных учебниках по компьютерной графике [28, 39, 68]. При буферизации по оси ° v^KTa’ °тобрйЖаемого в виде пиксела, сохраняется в буфере для аавТя	называеМ04 ^'буфере). При необходимости рисо-
ся — новый пЛт.₽еКТа* Значение каждого пиксела селективно редактируй более удаленио»™Кпк'г1еРеКРЬ,ВаРТ ТОЛЬКо те пикселы, которые принадлежу для более бли^ипгл ” П *'бу<^ер новое значение записывается ТОЛЬКО для более близкого объекта. Поскольку этот способ наиболее простой и
обший (С точки зрения обеспечения работы с широким диапазоном моделей сцен), то -.-буферизация была реализована аппаратным путем в нескольких вовейших графических системах. При трассировке луча, другом распростри ненпом способе удаления невидимых поверхностей, па сцену направляется как бы луч света, отражение которого обрабатывается в компьютере. Трассировка луча может быть использована для формирования изображений, включающих гикие свойства, как прозрачность, зеркальное отряжение в затенение.
Алгоритмы для нахождения пересечения двух выпуклых полигонов Р и Q представлены в [61, 62], хотя наше описание более точно соответствует [66]. Более ранний алгоритм для решения такой же задачи был опубликован п [75]. При этом способе через каждую вершину проводится вертикальная прямая линия, разделяющая плоскость на две части, а каждый полигон на треугольники и трапецоиды. Задача пересечения затем решается по очереди внутри каждой полуплоскости, а результирующие полигональные куски суммируются. Поскольку пересечение двух полигонов ограниченного размера может быть вычислено за постоянное время и может быть произведено не более, чем |Р| + |Q| делений по вертикали, то этот алгоритм, подобно описанному в этой книге, будет выполнен за время О(|Р| + |Qj).
Триангуляция Делоне является дуальной диаграмме Вороного, полигональной декомпозиции плоскости, которая играет очень важную роль в вычислительной геометрии. Эта связь будет использована в главе В, где будет описан алгоритм формирования диаграммы Вороного. Представленный в этой главе алгоритм триангуляции Делоне основан на работах [5, 56]. Алгоритм расширен на трехмерное пространство в [24], а структура данных, пригодная для расширения до d-мерного пространства, дана л [12], Алгоритм для построения триангуляции Делоне на плоскости за время О(п. log п), использующий метод «разделяй и властвуй», описан в [36]. Обзор методов формирования диаграмм Вороного и триангуляций Делопе дан в [4].
6.8. Упражнения
1.
2.
з.
Измените программу giftwrapHull так, чтобы выпуклая оболочка которую она формирует, включала все граничные точки набора
S,	а не только экстремальные точки.
Измените программу grahawScan для выполнения условий, аналогичных предыдущему примеру.
Глубшш точки р в конечном наборе S определяется числом мшукии оболочек, которые требуется удалять до тех пор. пока р ве стаял гра Хой точкой Например, граничные точки набора S имеют тбику ноль, а точки, которые станут граничнымипосле удаления точе£ пр.,надлежащих Гранине набора S.	^к^^яст^я
грубый подход к определению глубшш всех точек многократном определении выпуклой оболочки набора точек нию граничных точек из набора, пока он не станет пустым. Измените

8.
9.
10.
12.
функцию giftwrapHull так, чтобы ода вычисляла глубнву каЖадй точки за время О(л ).
4.	Диаметр набора точек равен максимальному расстоянию между любоц парой точек.
а)	покажите, что диаметр набора точек определяется парой экстре, мильных точек.
б)	дайте алгоритм, выполняемый за время О(п log л), для вычисления диаметра набора п точек на плоскости.
5.	Опишите конфигурацию п треугольников в ^пространстве, которую функция depthSort может расщепить на П(я ) кусков.
в. В программе depthSort при обращении к функции mayObscure (р, q) возвращается значение TRUE, если возможно, что треугольник р зд. крывает треугольник q. Что произойдет с программой depthSort. если сделать функцию mayObscure более строгой, например, чтобы она возвращала значение TRUE, если и только если р закрывает q. Какие при этом получаются преимущества и недостатки?
7. Отметим, что второе обращение к функции depthSort в программе mayObscure является неэффективным, поскольку повторные тесты 1, 2 и 5 не нужны. Перепишите программу, чтобы исключить эту неэффективность.
Рассмотрим следующее утверждение относительно полигона пересечение двух выпуклых полигонов Р и Q: если границы двух полигонов Р и Q пересекаются, то алгоритм находит все точки их пересечения не более, чем за 2(|Р| + |Q|) итераций. Либо докажите это утверждение, либо измените соответствующим образом программу convexPolygonlntersect, либо опровергните утверждение, дав контрпример.
Опишите возможные входные данные, при обработке которых программой convexPolygonlntersect будет получен неправильный результат в терминах значения EPSIL0N2.
Покажите на примере, как в доказательстве корректности программы convexPolygonlntersect используется предположение, что входные полигоны Р и Q являются выпуклыми.
Покажите, что триангуляция Делоне является уникальной, если никакие четыре точки набора S (при (|8| > 3)) не принадлежат одной описанной окружности.
Покажите, что при любой триангуляции конечного набора точек S ;
1
получается 3|S| - 3 - Л ребер, если граница 6¥(S) содержит Л ребер. Из этого, кстати, следует, что триангуляция содержит O(|S|) ребер -факт, использованный н нашем доказательстве выполнения алгоритма триангуляции Делове за время О(л2).
Покажите, что из всех триангуляций конечного набора точек S при триангуляции Делоне максимизируется минимальное значение внут* ревних углов.
13.

j
Алгоритмы сканирования на плоскости
ъ
I
1
2.
I • 1», <
в
I
н
II
8
I
I €»
Сканирование на плоскости является очень полезным инструментом рсгпо-ния задач обработки геометрических объектов на плоскости. Воображаемая вертикальная линия сканирования перемещается слева направо, пересекая объекты. В процессе обработки линии сканирования решаются задачи, относящиеся только к частям объектов, лежащих слева от линии сканирлмкии В структуре, линии сканирования, представляющей состояние алгоритма для каждого положения линии сканирования, собирается информация об условиях слева от линип сканирования, которые должны учитываться при обработке объектов, располагающихся справа от нее. Когда линия сканирования продвинута достаточно далеко, обычно эд все объекты, то вся исходная задача оказывается решенной полностью.
Линия сканирования продвигается шагами, останавливаясь в позиция, носящей название точка-событие. Точки-события это те точки на плоскости, достижение которых линией сканирования вызывает изменение состояния алгоритма. Это могут быть, например, вершины полигонов, концевые точки отрезков прямых линий или точки пересечений в зависимости от решаемой задачи. Точки-события хранятся в перечне точек-событий, который упорядочен по возрастанию координаты х и используется для принятия решения о виде точки-события, которая должна будет обнаружена линией сканирования. В некоторых применениях перечень точек-событий является статичным, поскольку можно заранее установить все виды точек -событий, в других он динамичный, поскольку будущая точка-событие обнаруживается только по мере выполнения сканирования.
В процессе выполнения алгоритма при каждом появлении точки-события происходит переход. Переход заключается в выполнении трех видов действий:


Редактирование перечня точек-событий: занесение только что обнаруженных точек-событий, удаление текущей точки-события, а также всех других, которые стали ненужными.
Редактирование структуры линии сканирования для представления жв-менений состояния в алгоритме из-за нового положения линия сканирования.
3. Решение остальных вопросов задачи.
При сканировании задача на плоскости сводится к серил аналогичных задач о одномерном пространстве (на линии сканирования). Эго достаточно мощный подход, поскольку возшпсяющие одномерные задачи обычно решить горвддц
проще, чем исходную двухмерную задачу. Более того, поскольку измрНС8л? задач на каждом шаге сканирования бывает очень малым и в больцщНо> случаев их изменение от одной линии сканирования к следующей бь^ предсказуемым, то одномерные задачи могут рассматриваться как серил сВя, ванных задач, позволяющих получить общее решение с помощью абстра^. кых типов данных (часто типовых).
В этой главе мы рассмотрим несколько случаев применения сканиро^. ния плоскости. В первом примере найдем все пары пересекающихся отреэ-ков прямых линий лэ заданного набора отрезков на плоскости. Во втором случае найдем выпуклую оболочку для набора точек на плоскости с использованием способа, аналогичного способу ввода оболочки в разделе 5.3, но гораздо более эффективного. В третьей реализация вычисляется граница объединения набора прямоугольников, стороны которых параллельны двуц координатным осям. В четвертом и последнем случае выполняется деком позиция произвольного полигона на монотонные части. Если эту последнюю реализацию использовать совместно с алгоритмом триангуляции монотонного полигона, описанным в разделе 5.8, то в результате будет получен эффективный способ триангуляции произвольных полигонов.
7.1.	Определение точек пересечения отрезков прямых линий
В этом разделе мы коснемся следующей задачи: для заданной коллек-
ции из п отрезков прямых линий найти все пары взаимопересекаю
сл

отрезков. При наиболее грубом подходе для обнаружения факта пересечения проверяются все возможные пары отрезков. Это занимает время 0(пг), поскольку имеется п(п2~1- пар, а каждая проверка выполняется за фиксированное время. В этом разделе применим способ сканирования на плоскости для решения этой задачи за время O((r + я) log п) , где г равно числу обнаруженных пар. Способ сканирования по сравнению со способом «грубой силы» имеет большое преимущество (особенно при малом г), за счет уменьшения числа проверок на пересечения: полностью исключается проверка таких пар, составные части которых расположены далеко друг от друга и поэтому их пересечение невозможно.
Для упрощения обсуждения сделаем несколько допущений. Во-первых, ни один из отрезков прямой линии не должен быть вертикальным (в любом конкретном случае такой ситуации можно избежать путем некоторого поворота плоскости). Во-вторых, любые два отрезка прямой линии могут пересекаться только в единственной точке. В-третьих, никакие три отрезка прямой линии не пересекаются в одной и той же точке.
7.1.1.	Представление точек-событий
В процессе сканирования плоскости линия сканирования при своем пере' HCT^nu^BranfiHanPaB0 "ересекает п отрезков прямой линии, останавлдаа-событий- ъгтях‘ данном примере могут появиться три вида точек-вЫС т°ЧКи' Правые ,соп^еаые точки и пересечения (или, более точно, точки пересечения). Точки-события будем представлять
ддгормтмы сканирования на плоскости
гремя классами, получаемыми из общего базового абстрактного класса Event Point:
class Eventpoint (
public .*
Point p;
virtual void handl.Tran.itiontDictlonarytMg..^, DictionAry<Bv«ntPointe>€, List<£ventPoint*>*) • 0;
) •
Элемент данных p содержит точку-событие. Абстрактная компонентная функция handleTransition будет конкретизирована в классах, полученных из EventPoint.
Класс LeftEndpoint запоминает левую концевую точку отрезка прямой линии, в элементе данных р, наследуемом из базового класса. Сам отрезок прямой линии сохраняется в элементе данных е:
class LeftEndpoint: public EventPoint (
public:
Edge e;
LeftEndpoint(Edge*) ;
void handleTransition (Dictionary<Edge*>4,
Dictionary<EventPoint*>6, List<EventPoint*>*);
If
Конструктор LeftEndpoint инициализирует элементы данных:
LeftEndpoint::LeftEndpoint(Edge *_e) : e(*_e)
I
p = (e.org < e.dest) ? e.org : e.dest;
Класс RightEndpoint определяется аналогично:
class RightEndpoint; public EventPoint ( public;
Edge e;
RightEndpoint(Edge*) ;
void handleTransition (Dictionary<Edge*>fc, Dictionary<EventPoint*>4, List<EventPoint*>*)
RightEndpoint::RightEndpoint (Edge *_e) :
«.*
Класс Crossing запоминает в элементах данных el н е2 оба пересека* кицихся отрезка прямой линии, а в элементе данных р — нх точку пере?
сечения:
class Crossing: public BvcntPoint ( public:
Edge •!, «2;
Crossing(Edge*, Edge*, Pointfc);
void handleTranoition (Dictionary<Edge*>fi,
Dictionary<EventPoint*>6,
Его конструктор инициализирует элементы данных:
Crossing::Crossing(Edge *_el, Edge *_e2, Point t_p) •l(*_el), e2(* e2) (
P = jp;
7.1.2.	Программа верхнего уровня
Целиком задача решается программой intersectSegments. Программе передается массив s из п отрезков прямых линий и она возвращает список объектов Crossing, представляющих точки пересечения, обнаруженные программой. В программе используется глобальная переменная curx для хранения текущей координаты х сканирующей линии:
double curx; // текущая координата х на сканирующей линии
List<Event₽oint*> *intersectSegments (Edge s[] , int n) (
Dictionary<EventPoint*> schedule = buildschedule (s, n) ;
Dictionary<Edge*> sweepline(edgeCmp2);
List<EventPoint*> ‘result = new List<EventPoint*>;
while (!schedule.isEmpty()) (
EventPoint *ev = schedule.removeMin () ;
curx = ev->p.x;
ev->handleTransition(sweepline, schedule, result) ;
) return result; )
Программа intersectSegments инициализирует перечень точек-событий и структуру сканирующей линии и затем пошагово выполняет переход. Поскольку функция handleTransition объявлена виртуальной, то версия этой функции на каждом шаге цикла while зависит от типа события ev*
Перечень точек-событии нами представлен в виде словаря, поскольку перечень изменяется динамически: по мере перемещения сканируют®1”1 линии точки пересечения должны заноситься в перечень точек-событий 11 удаляться иэ него. Но перед началом сканирования в него должны быть предварительно занесены все л левых и все п правых концевых точек. Функция buildSchedule заносит эти 2п точек-событий в перечень точек событий, который затем и возвращается:
Dictionary<EvsntPoint*> fibuildSchedule (Edge s(], int n)
Алгоритмы сканирования на плоскости
М*
Diction*ry<EventPoint*> ‘schedule -
new ^ctionary<Event₽oint*>(.ventCam) ;
for (int i a 0; 1 < n; 14-4) (	*
schedule . insert (new LeftEndpoint(Ss[1])) -> schedule. insert (new RightEndpoint (£s [1]))' return •schedule;
Функция сравнения eventCmp, используемая функцией buildSchedule для инициализации перечня точек-событий, упорядочивает точки-события и порядке возрастания координаты х, а в случае совпадения учитывается взрастание координаты у:
int eventCmp (EventPoint ‘a, EventPoint »Ь) (
if (а->р < b->p) return -1;
else if (a->p > b->p) return 1;
return 0;
I
7.1.3.	Структура сканирующей линии

Набор всех отрезков прямых линий может быть классифицирован относительно любого данного положения сканирующей линии. У спящих отрез* ков обе концевые точки располагаются справа от сканирующей линии. Активные отрезки имеют по одной концевой точке с каждой стороны сканирующей линии. У мертвых отрезков обе концевые точки находятся слева от сканирующей линии. Структура сканирующей линия sweepline фактически является словарем, содержащим активные отрезки прямых линий, т. е. таких, которые в данный момент пересекаются сканирующей линией, упорядоченным в соответствия с координатой у точек их пересечения со сканирующей линией. Например, для случая, показанного на рис. 7.1, активные сегменты записываются в порядке а < b < с, когда сканирующая линия находится в позиции xj (поскольку отрезок прямой линии d не является активным, то в данный момент он игнорируется).
Словарь sweepline инициализируется с помощью функции сравнения edge Стр2, которая попользует текущее вертикальное упорядоч уже упоминалось, в глобальной переменной curx хранится текущДЯ -ХДОДОг дината х сканирующей линии:
ЭТИ
*1
Рис. 7.1. Активные отрезки упорядочены по вертикали w в позиции хь маи как b < а < с, если в позиции л®
fdefinm EPSILON3 1Ж-10
int edgsC&pZ (Edge *«, Edge *b)
double ye = a->y(curx EPSILON3); double yb = b->y(curx EP5ILOH3); if (ye < yb) return -1;
else if (ya > yb) return 1; double ma ° a->slope();
double mb “ b->slope();
if (ma > mb) return -1;
else if (ma < mb) return 1;
return 0; )
Функция сравнения edgeCmp2 оказалась неожиданно сложной, поскольку она может выполнять сравнение любых пар отрезков прямых линий, даже тех, которые пересекаются со сканирующей линией в совпадающей точке. Если обнаруживается такая ситуация, то функция сравнивает наклоны отрезков прямых линий и считает, что отрезок прямой линии с наибольшим наклоном находится ниже всех остальных отрезков. Это моделируется небольшим сдвигом сканирующей линии влево. Во избежание появления ошибок округления применяется константа EPSILON3.
7.1.4.	Переходы
Следующее наблюдение оказывается очень важным для нашего алгоритма. Предположим, что отрезки прямых линий а и b пересекаются в точке р и эта точка р находится справа от сканирующей линии. Тогда, если между точкой р и сканирующей линией нет никаких других концевых точек или точек пересечения, т. е. точка р является следующей точкой-событием, обнаруживаемой при перемещении сканирующей линии, тогда отрезки прямых линии а и b должны быть соседними при их вертикальной сортировке в структуре сканирующей линии.
Почему это так? Легко видеть, что оба отрезка а и b должны быть активными отрезками. Чтобы удостовериться, что они должны располагаться рядом по вертикали в структуре сканирующей линии, предположим противное на следующем шаге сканирующая линия обнаружит их пересечение, но они не являются соседними по вертикали. В этом случае некоторый другой активный отрезок прямой линии с должен пересекать
Рис 7.й (а)—права* гонцевая точка отрезка с с пересыхает отрезог а
ЛСгит между сканирующей линией и точкой р. (б) - атрезо* »ле*ду сканирующей линией и точгой р
ддгормтмы сканирования на плоскости
сканирующую линию между а и Ь. Эп означает, что либо правая концевая точка отрезка с оказывается между точкой р и сканирующей линией (рис. 7.2а), либо отрезок с пересекает а или b в некоторой точке, лежащей между Р и сканирующей линией (рис. 7.26). В любом случае ск титрующая линия обнаружит концевую точку или точку пересечения до того, как она достигнет точки р, что противоречит нашему предположению.
В алгоритме это наблюдение используется в форме инварианта, включающего перечень точек-событий: для каждой пары соседних по вертикали ребер, имеющих точку пересечения справа от сканирующей линии, в перечне точек-событий содержится точка их пересечения. Эго гарантирует, что перед тем, как сканирующая линия достигнет точки пересечения, эта точка уже должна быть в перечне точек-событий. Никакая точка пересечения ве будет пропущена. Основная работа по выполнению перехода направлена на реализацию этого инварианта.
Рассмотрим три вида перехода, начиная с левой концевой точки (рис. 7.3а). Когда сканирующая линия обнаруживает левую концевую точку отрезка прямой линии 6, то активные отрезки а и с (ниже и выше отрезка b соответственно) перестают быть соседними по вертикали вдоль линии сканирования. Если существуют оба отрезка а н с (а их может и не быть) и они пересекаются справа от сканирующей линии, то их точка пересечения удаляется из перечня точек-событий. Более того, поскольку теперь отрезки а и b стали соседними по вертикали и необходимо проверить, не пересекаются ли они, и, если пересекаются, то нужно занести их точку пересечения в перечень точек-событий. Точно также, если пересекаются отрезки b и с, то их точка пересечения заносится в перечень точек-событий. Итак, сформируем функцию LeftEndpoint:;handleTransition:
void LeftEndpoint: :handleTransition(Dictionary<Edge*> fisweepline, Dictionary<EventPoint*> (schedule, List<EventPoint*> *result)
Edge »b = sweepline, insert (fie) ;
Edge *c = sweepline.next() ;
sweepline.prev();
Edge *a = sweepline.prev() ;
double t;
if (a && c fifi (a->cross (*c, t) =SKEW_CROS3)) ( Point p » a->point(t);
(-)	' -r
7.3. - перекос	-«И-	"Ti
ТОЧКИ
if (cues < p.x) ( Creasing cev(a, c, p); dalete schedule.remove(6cev);
if (c ££ (b->cross(*c, t) “ SKEW_CROSS))
schedule.insert(new Crossing(b, c, b->point(t)) ) ;
if (a ££ (b->oross(*a, t) == 5KEW_CROSS))
schedule.insert(new Crossing(a, b, b->point(t)));
Теперь рассмотрим случай перехода для пересечения. Когда сканирую, щая линия достигает точки пересечения отрезков прямых линий Ь и с, оба отрезка должны поменяться местами при их упорядочении по вертикали (рис. 7.36). Поскольку отрезок b пересекает вышележащий отрезок с, мы должны проверить, не будут ли теперь отрезок b и активный отрезок d (те-перь располагающийся над Ь) пересекаться справа от сканирующей линии и если да, то их точка пересечения должна быть занесена в перечень. Аналогично, если отрезок с и активный отрезок а (теперь оказавшийся ниже отрезка с) пересекаются справа от сканирующей линии, то их точка пересечения также заносится в перечень. Кроме того, мы должны удалить из перечня точек-событий точки пересечения отрезков а и Ь, а также с и d если они имеются. Это приводит к следующему определению функции:
void Crossing::handleTransition(Dictionary<Edge*> Dictionary<EventPoint*> fischedule, List<EventPoint*> ‘result)
(
Edge *b = sweepline.find(£el);
Edge *a = sweepline.prev();
Edge *c = sweepline.find(fie2);
Edge *d = sweepline.next();
double t;
if (a £6 (a->cross(*c, t)==SKEW_CROSS)) ( Point p = a->point(t);
if (cunt < p.x)
schedule.insert(new Crossing (a, c, p));
)
if (d ££ (d->cross (*b, t) ==SKEW_CROSS) ) (
Point p = d->point(t);
if (cunt < p.x)
schedule.insert(new Crossing(b, d, p)) ; )
if (a ££ (a->cross(*b, t)»=SKEW_CROSS)) (
Point p = a->point(t);
if (curx < p.x) (
Crossing cev(a, b, p);
delete schedule.remove(£cev);
>
)
if (d ££ (d->croas(*c, t) =SKEW_CROSS) ) (
Point p  d->point(t);
if (curx < p.x) (
Crossing cev(c, d, p);
delete schedule.remove(£cev> *
Asweepline,
дуоритмы сканировании на плоскости
sweepline.remove(b) ; curx +« 2*EPSILON3; eweepline.insert(h) ; curx -= 2*EPSILON3; result->append(this) ;
Несколько последних операторов функции осуществляют взаимную заме* ну ребер Ь и с в структуре сканирующей линии. Это выполняется путем удаления ребра Ь и его повторного занесения после предварительного небольшого смещения сканирующей линии вправо.
Наконец, рассмотрим переход для правой концевой точки. Пря достижении сканирующей линией правой концевой точки отрезка прямой линии Ь, активные отрезки а и с (ниже и выше отрезка b соответственно) станут соседними по вертикали вдоль сканирующей линии (рис. 7.Зв). Если отрезки а и с существуют и пересекаются справа от сканирующей линии, то их точка пересечения записывается в перечень. Эта операция осуществляется функцией RightEndpoint:: handleTransition:
void RightEndpoint: :handleTransition(Dictionary<Edge*> bsweepline, Dictionary<EventPoint*> bschedule, List<EventPoint*> *result)
Edge *b = sweepline.find(£e);
Edge *c = sweepline.next();
sweepline.prev() ;
Edge *a = sweepline.prev(); double t;
if (a ££ c £5 (a->cross(*c, t) — SKEW_CROSS)) ( Point p = a->point(t);
schedule.insert (new Crossing(а, а, р)) :
у
»’д* .

7.1.5.	Анализ
Если г определяет общее число пересечений среди п отрезков прямых линий, то всего выполняется г + 2п операций перехода. При каждой ЦИИ перехода выполняется постоянное (небольшое) число действий над сгрук турой сканирующей линии и перечнем точек-событий. Каждое действие над структурой занимает время O(log л), поскольку все п отрезков прямых линя могут оказаться активными одновременно. Такое же время O(Iog.n) У9МД занимать и каждое действие над перечнем точек-событий. Действительно,; можно определить, что перечень точек-событий может содержать точек-событий типа пересечения в некоторый момент времени, поскольку существует не более п — 1 пар вертикально соседних активных элементов» чень точек-событий может также содержать до 2л концевых точек. След * , пательио, перечень точек-событий содержит О(л) элементов, поэтхлцг д^стпад^ над ним выполняются за время О(1о£ л). Поскольку ня ждя® из
•14
перехода занимает время O(log »). алгоритм целиком будет выполиев „ ”Р Характеристики этого алгоритма отражают его глубокую стратегию: потр,. тить дополнительное время на исключение пар отрезков прямых линий, торые возможно не могут пересекаться, чтобы сократить время на их рованле. Пары отрезков, расположенные далеко друг от друга так, что Сх проекции на ось х взаимно не перекрываются я не тестируются, поскольку такие отрезки не могут оказаться активными одновременно. Пары отрезков, расположенные далеко друг от друга по вертикали в том смысле, что между ними могут оказаться другие отрезки, также не подвергаются тестированию, поскольку они не могут оказаться соседними в структуре сканирующей линии. Расплата за исключение таких неперспективных пар отрезков прямых линий учитывается коэффициентом замедления log п.
Такая стратегия оправдывается, когда число г достаточно мало: если г G о(_2_.), то алгоритм выполняется за время о(л2), что лучше параметров прямолинейного грубого подхода к решению задачи. Однако, если на самом деле будут пересекаться много отрезков прямых линий (например, г Е П(п2)), то такой алгоритм менее эффективен, чем прямолинейный подход, ком
поскольку цена исключения неперспективных пар становится cj высокой и такие действие непродуктивны.

Поиск выпуклой оболочки (построение оболочки при вводе)
1
разделе 5.3 была описана программа insertionHull, которая опреде-выпуклую оболочку набора из п точек на плоскости путем включения
В ляет их по очереди в текущую оболочку. Анализ этой программы показал, что ее параметры определяются главным образом двумя операциями; (1) определением, не будет ли каждая из вновь вводимых точек р попадать внутрь текущей оболочки (т. е. «абсорбироваться» ею), и (2) поиском некоторой вершины в ближней цепочке текущей оболочки, если точка р лежит вне текущей оболочки. Поскольку выполнение каждой из этих операций занимает время, пропорциональное размеру текущей оболочки и, в худшем случае, размер оболочки увеличивается на одну вершину за каждую итерацию, можно заключить, что программа insertionHull выполняется за время О(п’)- В этом разделе мы кратко рассмотрим версию алгоритма ввода оболочки, работающую в автономном режиме (off-line) и не требующую выполнения этих двух операций, что в результате дает более эффективную программу, выполняемую за время О(п log п).
Идея заключается в предварительной сортировке точек слева направо в ™?ючении их в текущую оболочку именно в этом порядке-о BHVTDb Bkinvxnnf Х°ДИМ0СТЬ Б нроверке попадания каждой вводимой точки оболочки: поскольку точка р находится справа от всех точки) то эта точка	П° к₽айней меР₽ выше последней введенной
^Гто^ р находя 3аТеЛЬН0 ЛеЖИТ ВНе текУ“’ей оболочки. Определив, ее в состав текушеГг ТеКущей оболо«кн, нам нужно будет включить торуто вершину в ближне^цеп^” Ч°Г° пот1,ебУи'сл определить немочке текущей оболочки. Но вследствие Ус
поскольку точка р находится справа от в'-**
1
": i
Алгоритмы сканирования на плоскости	Т ’’ '
—---------------------------------------------------------------------------------til
 в
хг	-т™-
Программа insertionHull? возвращает выпуклую ^лоч^₽^*^,.к Н массиве pts. Для практики весьма полезно сравнить эту новую программу с программой insertionHull 1га раздела 5.3.	У» программу
Polygon ♦insertionHullZ(Point pti[), int n)
Point **9 * new (Point*) (n) ;
I

। •!
Polygon *p = new Polygon; p->insert(new Vertex(*s[0])) ;
continue;
supportingLine(*s[i] , p, LEFT);
Vertex *1 = p—>v();
supportingLine(*s[i] , p, RIGHT); delete p->split(l) ;
p->insert(new Vertex(♦s[i])>;
return p;
Заметим, что новая версия ввода оболочки является примером сканирования на плоскости, хотя при ее объяснении не были использованы термины, принятые для метода сканирования на плоскости. Действительно, отсортированный массив точек здесь выступает в роли перечня точек-событг* (в данном случае статического), а текущая оболочка служит в качестве структуры сканирующей линии. По мере обработки каждой точки изменяется текущая оболочка, тем самым неявно редактируя как структуру сканирующей линии, так и полного решения задачи для всех точек, дели ,‘Z слева от сканирующей линии.
7.2.1. Анализ
Проанализируем программу insertionHullZ. В пределах одного i----
цикла for выполнение только двух обращений к функции supportingLine занимает время больше, чем константа — вместе они выполняются за время, пропорциональное длине ближней цепочки текущей оболочки. Для упрощения анализа приравняем затраты на обработку каждой верппгьы ближней цепочки затратам на выполнение двух обращений к функция supportingLine. Поскольку эти вершины удаляются при последующем обращении к функции split, то такое приравнивание для каждой точки потребуется сделать только один раз — точка, однажды удаленная из оболочки, вторично в нее заноситься не будет. Всего имеется п точек, псс тому стоимость всех обращений к функции supportingLine за все п-1 итерации составит по времени О(п).	___
Отсюда следует, что время выполнения функции insertionHullZ в мовном определяется временем начальной сортировки. При
Л
1- «
оптимального метода сортировки, например, mergeSort, который будет опи-сан в следующей главе, программа insertionHull^ выполняется за врвМн О(л log п).
7.3.	Контур объединения прямоугольников
Параллельный осям прямоугольник — это такой прямоугольник, сторону которого параллельны координатным осям на плоскости, две его стороны вертикальны и две — горизонтальны. В этом разделе будем использовать метод сканирования плоскости для решения задачи поиска контура', вы. числения границы (или контура) области, образованной объединением коллекции параллельных осям прямоугольников. Такая задача возникает при проектировании интегральных схем (ИС) в электронике при использовании масок ИС в виде параллельных осям прямоугольников. Подобные алгоритмы применяются при раскладке масок ИС для обеспечения определенных ограничений, например, экономических (минимизация общей площади) или электрических (изоляция или контакт).
Контур формируется из одной или нескольких петель, каждая из которых образуется различным числом горизонтальных и вертикальных сегментов контура. Петли не пересекают друг друга, но могут быть вложенными до любой глубины. На рис. 7.4 показаны шесть прямоугольников и контур их объединения. Контур состоит из четырех петель: первая охватывает вторую, которая в свою очередь охватывает третью; четвертая лежит в стороне от остальных. На рис. 7.5 показан контур 120 прямоугольников, расположенных случайным образом в прямоугольной области на плоскости.
Для решения задачи определения контура воспользуемся методом сканирования плоскости и будем продвигать сканирующую линию слева направо через все п прямоугольников при соблюдении следующего инварианта: в каждом положении сканирующей линии все сегменты контура, целиком лежащие слева от сканирующей линии, полностью определены. Горизонтальный сегмент контура, пересекающий сканирующую линию, не определен до тех пор, пока сканирующая линия не достигнет его правого конца. Инвариант должен быть пересмотрен при обнаружении сканирующей линией каждого из 2п вертикальных ребер, что означает необходимость пояска таких сегментов контура, которые теперь впервые оказываются слева от ска-
РИС.7Л. Гхжтур объели,
Алгоритмы сканирования на плоскости
М7
%
□
и
«а
I'
С
h
Рис. 7.5. Контур объединения 120 прямоугольников
нирующей линии. Таким образом, левые и правые ребра прямоугольников служат в качестве точек-событий. На рис. 7.6 показаны различные ситуации с двумя прямоугольниками с выделением толстой линией сегментов контура, выявленных в последовательных позициях сканирующей линии. Для ясности рисунка сканирующая линия вправо.
на каждой схеме
несколько смещена
Рис. 7.6. Сегменты контура, выявленные в каждой позиции сканирующей линии, выделены толстой линией

7.3.1.	Представление прямоугольников
Прямоугольники будем представлять следующим классом:

class Rectangle public;
Point sw;
Point ne;
I/ юго-западный (нижний левый) угол // северо-восточный (верхний правый) угол // идентификатор
.nt & sw, Point fi_ne, int _id = -1) ;
Rectangle(void) ( )
Прямоугольник определяется двумя
и ему приписывается идентификатор id. трук
как:	Л
—. •• «£ *' Ч|1
Rectangle::Rectangle (Point <*, ₽oint fi-ne' int -id> : sw(_sw) , ne(_ne), id(_id)
( )
Стороны прямоугольника представим в виде класса AxisParallelEdge;
enum ( LEFT SIDE, RIGHT_SIDE, BOTTOM_SIDE, TOP_SIDE) ; // левая", правая , нижняя , верхняя стороны
class AxisParallelEdge { public:
Rectangle *r;	И прямоугольник, обладающий ребром
int count;
double m; int type;	// тип стороны прямоугольника
AxisParallelEdge(Rectangle*, int) ;
double pos(void);
double min(void);
double max(void); void setMin(double); void handleLeftEdge (Dictionary<AxisParallelEdge*>&, List<Edge*>*); void handleRightEdge (Dictionary<AxisParallelEdge*>&, List<Edge*>*) ;
I;
Конструктор класса может быть определен как:
AxisParallelEdge::AxisParallelEdge (Rectangle * г, int _type) : r( r), count(0), m(-DBL_MAX), type(_type)	~
{ “ )
Компонентная функция pos возвращает позицию текущего ребра по оси, к которой оно перпендикулярно, а компонентные функции min и max возвращают значения минимальной и максимальной координат концевых точек ребра вдоль оси, которой оно параллельно. Например, для вертикального ребра с концевыми точками (1, 2) и (1, 4) функция pos возвращает значение 1, функция min возвращает 2 и функция max возвращает 4. Эти функции определяются следующим образом:
double AxisParallelEdge::pos(void)
switch (type) {
case LEFT-SIDE:
return r->sw.x; break;
case RIGHT_SIDE:
return r->ne.x; break;
case TOP_SIDE:
return r->ne.y; break;
case BOTTOM_SIDE: default:
return r—>sw.y; break;
J
double AxisParallelEdge::min(void)
Алгоритмы сканирования на плоскости
К»
1
return m;
switch (type) (
case LEFT_SIDE:
case RIGHT_SIDE:
return r—>sw.y; break;
case TOP_SIDE: case BOTTOM_SIDE: default:
return r->sw.x; break;
J
1
1

i i
double AxisParallelEdge::max(void)
switch (type) ( case LEFT_SIDE: case RIGHT_SIDE;
return r->ne.y; break;
case TOP_SIDE: case BOTTOM_SIDE: default:
return r->ne.x; break;
При инициализации верхнего или нижнего ребер оно совпадает с верхней или нижней стороной прямоугольника, которому принадлежит это ребро. Однако, наш алгоритм должен иногда изменить левую концевую точку горизонтальных ребер. Это выполняется компонентной функцией setMin. Она используется для изменения значения, возвращаемого последующими обращениями к компонентной функции min. Компонентная функция setMin определяется как
void AxisParallelEdge: : setMin (double f)
ИЗ
7.3.2.	Программа верхнего уровня	 * .
Программа findContour решает задачу определения контура массива г п прямоугольников. Функция накапливает обнаруживаемые сегменты контура в списке сегментов segments, который возвращается по окончании работы:	" ' '*’*Я
List<Edge*> * findContour (Rectangle	r[], intn)	. •.‘"ЗеДИ
(	• г jAtff
AxisParallelEdge “schedule = buildSchedule(r, n) ;	-rtGH a
List<Edge‘> ‘segments = new List<Edge*>;
Dictionary<AxisParallelEdge*> sweepline'
Rectangle ‘sentinel = new Rectangle(Point(-DBL MAX,-DBLJttX),	.
Point (DBL_MAX, DBL_MAX) , -1);	-
sweepline. insert (new AxisParallelEdge (sentinel, BOTTCMSIM) ) f , jg

for (int i  0; i < 2*n;
switch (echedule[i]
°“h.“tldgaTr^.ltion (.«oplioe, sogm.nt,) , break;
“cheSls’l)->handleRightEdgaTransition (sweepline, segments) ; break;
)
return segments;
)
Функция buildSchedule создает перечень точек-событий в виде массива из 2п вертикальных ребер, отсортированных в соответствии с увеличением координаты х. Функции задается массив г из п прямоугольников и она воз. вращает перечень точек-событий:
ttyi ^Рят*я 11 el Edge **buildSchedule (Rectangle r[], int n) (
AxisParallelEdge «^schedule = new AxisParallelEdgePtr[2*n]; for (int i = 0; i < n; i++) (
schedule [2*i] = new AxiaParallelEdge (fir [i] , LEFT_SIDE) ; scheduler 2 *i+l]=new AxisParallelEdge(fir[i] , RIGHT_SIDE) ;
) insertionsort(schedule, 2*n, axisParallelEdgeCmp) ; return schedule;
)
Функция buildSchedule сортирует перечень точек-событий, используя функцию сравнения axisParallelEdgeCmp:
int axisParallelEdgeCmp(AxiaParallelEdge *a, AxiaParallelEdge *b)
if (a->pos() < b->poa()) return -1;
else if (a->pos() >b->poa()) return 1;
else if (a->type < b->type) return -1;
else if (a->type > b->type) return 1;
else if (a->r->id < b->r->id) return -1;
else if (a->r->id > b->r->id) return 1;
return 0;
)
Функция сравнения axisParallelEdgeCmp сравнивает два вертикальных ребра по величине их координаты х и, если они одинаковы, то ребра сравниваются по типу (левые ребра считаются меньшими, чем правые). И, наконец, ®сли и типы одинаковые, то ребра сравниваются по отношению к идентификаторам их соответствующих прямоугольников. Горизонтальные Г»™™? НИ“аются “алогично, во роль х-коордннат играют координаты у. когяТм сп™««ЧИТ“Ю'":Я Меньтими' чем верхние ребра. (Алгоритм W
дли лся™ пяВп" веРтикальыые ребра с горизонтальными.) моуго^ьникГя, ИМС№ оаК0Т01>0Г0 прямоуголь,,ика R, и правое ребро '2 ЯР* ^^оо е. меньше L°OTH«K0By” к00РЛинпту X. то почему нужно считать.
’ ребро ег? Причина заключается в том, что ребро а
Алгоритмы сканирования на плоскости
должно быть занесено а перечень перед е2. чтобы сканирующая лиш» лжо-днла в прямоугольник Л, до того, как опа покинет прямоугольник Ла. Вели этого нс делать, то алгоритм ошибочно сочтет вертикальное ребро, по ко-торому соприкасаются два прямоугольника, за сегмент контура.
Структура сканирующей линии строятся по словарному принципу и содержит активные горизонтальные ребра, т. е. такие горизонтальные ребра, которые пересекают сканирующую линию в текущей полиции. Функция сравнения axisParallelEdgeCmp используется не только для формирования перечня точек-событий, но и для создания и поддержания структуры сканирующей линии.
7.3.3.	Переходы
Переходы, возникающие при каждом обнаружении сканирующей линией вертикального ребра, обрабатываются по-разному для левого и правого ребер. В обоих случаях, однако, мы должны воспользоваться полем счет-
чика count активных горизонтальных ребер. Параметр count ребра равен числу прямоугольников, которые перекрывают участок сканирующей линии от данного ребра до ближайшего следующего ребра сверху. Эти числа показаны на рис. 7.7 и рис. 7.8.
Рассмотрим сначала обработку левых ребер, когда сканирующая линия
входит в некоторый прямоугольник R. Для объяснения ситуации обратимся
к рис. 7.7а. Для начала внесем в структуру сканирующей л

ризонтальных ребра а и Л прямоугольника R. Затем используем структуру
сканирующей линии для анализа последовательности активных горизонтальных ребер на промежутке от а до h. Вдоль этого пути можно обнаружить три вида сегментов контура. Во-первых, мы найдем горизонтальный сегмент контура вдоль каждого нижнего ребра, счетчик для которого равен 1 (ребра b и d). Во-вторых, найдем горизонтальный сегмент контура вдоль каждого верхнего ребра, счетчик для которого равен 0 (ребра с я g). Поскольку оба вида сегментов контура далее будут закрываться прямоугольником Я, то их пересечение со сканирующей линией определяет их правые концевые точки.
О
h
9
b
d
с
2
3
2
> »
(б)
ММ
Л* И
левого ребра с указанием значений счетчика. Найдем сегменты контура
Рис. 7.7. Обработка ситуации для выделены толстой линией
В-третьих, мы можем иайти вертикальные сегменты контура, которые м. канчпваются в нижнем горизонтальном ребре со значением счетчика 1 (между ребрами а и Ь), или которые заканчиваются на верхнем ребре а значением счетчика 0 (между ребрами £ и Л), или которые ограничиваются обоими типами ребер (между ребрами end). Все такие вертикальные ребра располагаются вдоль левого ребра прямоугольника /?. Полученный результат показан на рис. 7.76.
В добавление к определению сегментов контура мы должны отредактировать значения счетчиков, ассоциированных с активными горизонтальными ребрами. Счетчики всех активных горизонтальных ребер, лежащих между а и Л, получают приращение на единицу к значению, имевшему место до встречи с прямоугольником В. Значения счетчиков для а и Л инициализируются с учетом значений счетчиков активных горизонтальных ребер, которые располагаются непосредственно под каждым из них.
Левое ребро обрабатывается компонентной функцией handleLeftEdge. Этой функции передается структура сканирующей линии и список уже известных сегментов контура, к которому она добавляет новые обнаруженные сегменты:
void AxisParallelEdge: :handleLeftEdge ( Dictionary<AxisParallelEdge*> &sweepline, List<Edge*> *segs)
<
sweepline.insert(new AxisParallelEdge(r, TOP_SIDE)) ;
AxisParallelEdge *u — sweepline.val ();
sweepline.insert(new AxisParallelEdge(r, BOTTOM_SIDE)) ;
AxisParallelEdge *1 = sweepline.val();
AxisParallelEdge *p = sweepline.prev();
float curx = pos();
l->count = p->count + 1;
p = sweepline.next();
1 = sweepline.next();
for ( ; 1 != u; p = 1, 1 = sweepline.next()) {
if ((l->type==BOTTOM_SIDE) && (l->count++ =1)) ( segs->append(new Edge(Point(curx, p->pos () ) , Point(curx, l->pos ()))) ;
segs->append(new Edge (Point(l->min(), l->pos{)) ,
.	,	_	Point (curx, l->pos () ) )) ;
} else if ((l->type == TOPJ5IDE} U (l->count++ — 0) ) segs->append(new Edge(Point(l->min () , l->po9 ()) ,
}	Point(curx, l->pos())));
if ((l->count = p->count 1) = 0)
egs->append(new Edge(Point(curx, p->po8()) Pointfcurx, l->poe()))) ;
Теперь рассмотрим процесс обработки правого ребра когда сканирую^8* “Тб₽“Й П₽ЯМ°™ы”'к « <₽ис 7P8i. Сючалз Х™»-линии (а и g на рисуше’) €^4™!'“'“’'“ Л “ стру"т>'рс скавируюше" ныл горизонтальных ребер от а ааерх^лпТ1'^ послвл°вательяость ментов контура. Во-пепамх	Р Д А и б™ем искать два вида сег
• Риаоитальные ребра прямоугольника Я могут
Алгоритмы сканирования на плоскости
>0
I3
*2
12
a
(а)
(6)
2
h
1
0
d
с
Ь
Рис 7.8. Обработка ситуации для правого ребра с указанием значений сметчика. Сегменты контура выделены толстой линией
содержать сегменты контура — нижнее ребро, если его счетчик равен 1 к верхнее ребро, если его счетчик равен 0 (на рисунке а является сегментом контура, a g — нет). Во-вторых, найдем вертикальные сегменты контура, соединяющие каждое верхнее горизонтальное ребро со значением счетчика 1 с нижним горизонтальным ребром, счетчик которого равен 2 (между ребрами а и b я между ребрами с и d). Для каждого из рассмотренных на этом пути ребер (между а и g), выходящих из пределов прямоугольника R, счетчики уменьшаются на единицу.
Попутно, просматривая список от а до g, решим дополнительную задачу: для верхнего горизонтального ребра с уменьшенным значением счетчике, равным 0, и для нижнего горизонтального ребра с уменьшенным значением счетчика, равным 1, применим функцию setMin для смещения левой концевой точки вправо до положения сканирующей линии. На рис. 7.8 таким образом смещены левые концевые точки для горизонтальных ребер &, с ж d. Чтобы убедиться, что это необходимо, отметим, что каждое из ребер cud содержат сегменты контура, которые еще предстоит обнаружить, ж что левые концевые точки каждого из этих сегментов располагаются на правом ребре прямоугольника R, совпадающем с текущей позицией сканирующей ЛИНИИ.	_____•!'
Чтобы закончить вычисления, связанные с правым ребром, удалим из структуры сканирую щей линии горизонтальные ребра прямоугольника R (о и g) и тем самым будет завершена обработка прямоугольника R.	~	
Компонентная функция handleRightEdge предназначена для обработки правого ребра. Обнаруженные ею сегменты контура будут добавлены в сш* сок segs:
void AxisParallelEdge::handleRightEdge(
Dictionary<AxisParallelEdge*> fisweeplxne, List<Edga*> ‘sags)
AxisParallelEdge uedge(r, TOP_SIDE);
AxisParallelEdge ledgefr, BOTTOM SIDBJ;
AxisParallelEdge •« - sweepline.find ‘uedgeb AxisParallelEdge *1 = sweepline.find(Sledge1,
Е
н”»
float curx * pos();
if (l->count “ 1)
sgs->appand(naw Edge(Point(l->min(), l->poa ()) , Point(curx, l->pos ())));
if (u->count = 0)
segs->append(new Edge(Point(u->min(), u->pos()) , Point(curx, u->pos())));
AxisPaxallelEdge *initl = 1;
AxieParallelEdge *p 3 1/
1 3 sweepline.next();
for ( ; 1 !« u; p = 1, 1 = sweepline.next()) (
if ((l->type = BOTTOM_3IDE) ££ (—l->count — 1) ) ( segs->append(new Edge(Point(curx, p->pos()), Point (curx, l->pos ())));
l->setwin(curx);
) else if ((l->type = TOP_SIDE) £& (—1->count == 0)) l->setMin(curx);
)
if (l->count — 0)
aegs->append(new Edge(Point(curx, p->pos()), Point (curx, l->pos ())));
sweepline.remove(u);
sweepline.remove(initl) ;
7.3.4.	Анализ
Для заданных n прямоугольников рассмотрим затраты на выполнение переходов при обработке некоторого прямоугольника R. Переход любого типа требует выполнения небольшого постоянного числа операции со структурой сканирующей линии с затратами времени O(log и) плюс время, пропорциональное числу активных горизонтальных ребер, расположенных между нижним и верхним ребрами прямоугольника R. Поскольку число таких ребер может составлять П(л), то в худшем случае обработка этих активных горизонтальных ребер и будет занимать основное время. Следовательно, каждый переход будет занимать время до О(л). Поскольку может потребоваться до 2л переходов, то полностью алгоритм выполняется за время О(л2) в худшем случае.
В самом плохом случае у высоких и узких прямоугольников и у широких прямоугольников образуют сетку, в которой каждый высокий прямоугольник пересекает каждый широкий прямоугольник. Поскольку контур такой сетки содержит П(л2) сегментов контура, то потребуется время О(л2) только на сообщение о каждом сегменте контура.
Можно попытаться выразить время работы в функциональной зависимости от числа л и размера формируемого контура. Однако, даже если контур составлен из очень небольшого числа сегментов, па его формирование может потребоваться время П(л2). Рассмотрим работу программы findContout. если в качестве входных данных для нее задана сетка из л прямоугольников, описанная в предыдущем абзаце плюс большой прямоугольник Я, ватывающий эту сетку. Хотя контур объединения этих л + 1 прямоугольников состоит только из четырех ребер прямоугольника R, сГ0 выявления потребуется время £2(л2). По мере продвижения сканирую®^
Алгоритмы сканирования на плоскости
линии слева направо алгоритм должен осуществлять редактирование стотк туры сканирующей линии в соответствии с обнаруженными £бр^ даже если эта сетка перекрывается прямоугольником Я. Это оказывается необходимым потому, что если бы сканирующая линия достигла бы правой границы прямоугольника Я чуть раньше, чем кончилась бы сетк“. то со стояние алгоритма должно оказаться в сильной зависимости от структуры сетки. Хотя в нашем примере сканирующая линия сначала проходит всю сетку полностью и только после этого выходит за пределы прямоугольника Я, но знать заранее этой ситуации алгоритм не может.
7.4.	Декомпозиция полигонов на монотонные части
В этом разделе представим алгоритм для декомпозиции произвольного полигона на монотонные подполигоны, этот процесс иногда называют регуляризацией. Напомним из раздела 5.8, что полигон называется монотонным, если его граница состоит из монотонных верхней и нижней цепочек. Алгоритм использует сканирование на плоскости для регуляризации л-уголь-ника за время О(п log п). Используя этот алгоритм совместно с описанным в разделе 5.8 способом триангуляции монотонных полигонов, вышит яемыы за линейное время, мы получим алгоритм для триансуляции произвольного полигон за время О(п log л): сначала выполним декомпозицию заданного полигона на монотонные подполигоны, затем осуществим триангуляцию каждого из них по очереди.
Для упрощения описания ниже в этом разделе будем предполагать, что никакие две вершины в полигоне не имеют одинаковой координаты х.
Наш алгоритм основан на определении понятия излома. Вогнутая вершина — вершина, внутренний угол которой превышает 180 градусов — является изломом, если обе ее соседние вершины лежат либо слева, либо справа от нее (рис. 7.9). Легко видеть, что никакой монотонный полигон не’ может содержать излом. Это правило обратимо и выражается следующей теоремой, лежащей в основе алгоритма:
Теорема 4. (Теорема о монотонности полигона): любой полигон, не содержащий излома, является монотонным
Теорема доказывается от противного: если полигон Р немонотонный, тогда в нем есть излом. Предположим, что полигон Р немонотонным и его немонотонность является следствием того, что его верхняя цепочка на является монотонной. Обозначим вершины вдоль верхней цепочки полигона Р как щ, иг...У* и пусть будет первой вершиной вдоль цепочки такой,
что вершина им будет лежать слева от щ (рис. 7.10). Если вершина с»н-1 лежит над ребром ПИЙ. тогда т будет изломом, так что мы предположим, что вершина им находится под ймщ. Пусть uj будет самой левой цепочки от и/ до и*, прежде чем цепочка пересечет ребро щи*.
о/ является вогнутой, поскольку она локально самая левая и внутренность полигона лежит слева от нее, отв же вершина и) является взломом, поакмь-ку ОПП погнутая и обе соседние с ней вершины находятся сорив от и«. Таким образом теорема доказана.
н :1пкп;1 2791
7А1. Программа верхнего уровня
Няттт алгоритм выполняется путем декомпозиции полигона Р на подполигоны без изломов. В первой из двух фаз по мере продвижения сканирующей линии по прямоугольнику R слева направо алгоритм удаляет изломы, направленные влево, формируя набор подполигонов Pi, Рг,--., Рщ, ни один из которых не содержит изломов, направленных влево. Во время второй фазф алгоритм удаляет изломы, направленные вправо, продвигая сканирующую линию справа налево по подполигонам Р/ по очереди. Полученная коллекция полигонов и будет декомпозицией исходного полигона Р на подполигоны, в которых нет ни право-, ни лево-направленных изломов и, следовательно, совсем никаких изломов.
В программу regularize передается полигон р и она возвращает список монотонных полигонов, представляющих регуляризацию исходного полигона р. В процессе работы программы исходный полигон р разрушается:
enum ( LEFT_TO_RIGHT, RIGHT_TO_LEFT);
// слева-направо, справа-налево
List<Polygon*> *regularize(Polygon Ьр) (
// фаза 1
I*ist<Polygon*> «polysl = new List<Polygon*>;
semiregularize(р, LEFT_TO_RIGHTr polysl);
_. .	// фаза 2
List<Polygon*> *polys2 = new List<Polygon*>: polysl->last();
while (!polysl->isHead()) (
Polygon *q = polysl->remove();
semiregularize(*q, RIGHT_TO_LEFT, polys2) ;
return polys2;

Гис. 7.10. kU/jocrpeuHi	D
Алгоритмы сканирования на плоскости
В этой программе полигоны, создаваемые в первой фазе, накапчиваются в списке polysl, а формируемые во второй фазе - в списке polyaz"^
Основное назначение функции semiregularize заключается в исключении всех изломов, направленных в одну сторону. Например, при обращении semiregularize(р, LEFT_TO_RIGHT, polysl) выполняется сканирование полигона р слева направо и удаляются изломы, направленные влево, а получающиеся подполигоны добавляются в список polysl. Каждый раз, когда сканирующая линия обнаруживает направленные влево изломы и, полигон разбивается вдоль хорды, соединяющей и с некоторой вершиной ш, расположенной слева от и.~ Чтобы отрезок прямой линии иш был действительно хордой (т. е. внутренней диагональю) полигона, выбор вершины и> должен быть оправданным. Если вершина и лежит между активными ребрами а к d в качестве вершины ш выбирается самая правая из тех вершин, что лежат между ребрами а и d и расположены слева от сканирующей линии (рис. 7.11а),
Выбор вершины w можно пояснить и другим способом: рассмотрим трапецоид, образованный сканирующей линией, ребром а, ребром d и прямой линией, параллельной сканирующей линии и проходящей через точку X. Здесь в качестве точки х можно выбрать левую концевую точку ребер а или d, лежащую правее другой. Вершина ш должна быть самой правой вершиной, лежащей внутри этого трапецоида, но если внутри трапецоида нет других вершин, то такой вершиной ш будет вершина х. Вершину ш будем называть целевой вершиной пары ребер ad (вертикально соседней пары ребер and).
На рис. 7.116 показаны действия, предпринимаемые, когда сканирующая линия обнаруживает излом и: полигон расщепляется по хорде иш. Ни в одном из полученных новых полигонов вершина и уже не будет изломом — в каждом новом полигоне одна из соседних с и вершин лежит слева, другая — справа. Конечно, такая ситуация обеспечивается при перемещении сканирующей линии слева направо: ни одна из вершин, лежащих слева от сканирующей линии, не может быть изломом, указывающим влево.
Переход выполняется каждый раз как только сканирующая линия обнаруживает вершину полигона. Тип перехода зависит от типа вершины, если вершины классифицировать в соответствии с положением их двух соседей относительно направления сканирования:
• Начальная вершина: обе соседние вершины для вершины v располагаются после сканирующей линии, т. е. в направлении перемещения сканирующей линии.
Ilf И
Рис. 7.11. действия, выполняемые при обнаружении излома V, направленного влево
Глам7
Вершина перегиба: одВВ _______линия, ДРУГ®*
V лпошин находится сзади от ска-соседних верш»
итт О Д	_
нируюшай ляяЯЯ\Д"‘~ двие с V вершины расположены позади ска.
« Концевая вершина, о
пирующей линии.	начальный переход, переход при пере.
р „опатшых перехода - нача^ 7 12, где предполагается, что ТрН взаимосв д __ доказаны на Р ’ ‘ а наПраво. Типы перехода гибе и КОПЦе ществляется в направлен!	так что функция
сканирование о	направления ск Р	рованИи в любом на-
определены в теР“^*Хбыть ИСпользована при сканир semiregularize	сдается произвольный поли-
ПР В^епии к функции	LEFT„TO RIGHT для об-
= " Называется направление скввиР RIGHTT0_LEFT для обнару. напужевня изломов, указывающих влев	ющиеся „ результате
ж’ния изломов. УК-“’а^ХоПо“ув. в функции используются три подполигоны добавляются в спи глобальные переменные.
int eweepdirection, double curx;
int curtype;
,. текущее направление сканирования // текущая позиция сканирующей линии // текущий тип перехода
int direction,
sweepdirection = direction;
int (*cmp)(Vertex*, Vertex*);
if (sweepdirection=LEFT_TO_RIGHT) cmp = leftToRightCmp; else cmp = rightTobeftCmp;
Vertex **schedule = buildSchedule(p, cmp);
Dictionary<ActiveElement*> sweepline = buildSweepline () ;
for (int i = 0; i < p.sizeO; i++) (
Vertex *v = scheduled];
curx = v->x;
switch (curtype = typeEvent(v, cmp)) (
case START_TYPE:
---' л-» .
break;
case BEND_TYPE: bendTransition(v, sweepline);
break;
(•)
Fmc. 7.1 i. Три tova переноса, ю»*«лощие при перолешем*», r.ra^p/оцги /и»»<и гу г.м нлп»
Алгоритмы сканирования на плоскости
case END_TYPE:
endTransition(v, swasplins, polys);
}
p.setV(NULL);
Точки-события возникают при обнаружении вершин полигона. Поскольку все они известны заранее, то перечень точек-событий записан в массиве вершин, предварительно отсортированном в порядке увеличения координаты х (поэтому нет необходимости в динамической пересортировке словаря). Перечень формируется функцией buildSchedule, которой передается одна из функций сравнения точек leftToRightCmp или rightToLeftCmp в зависимости от направления сканирования:
Vertex **buildSchedule(Polygon 6p, int(♦cmp) (Vertex*, Vertex*))
Vertex **schedule = new (Vertex*) [p. size()];
for (int i = 0; i < p.sizeO; i++, p. advance (CLOCKWISE)) schedule[i] = p.v();
insertionsort (schedule, p.sizeO, cmp);
return schedule;
4
Чтобы определить, какой тип перехода должен быть соотнесен с данной вершиной, в функции semiregularize используется функция typeEvent. Для классификации вершины и функция typeEvent проверяет положение двух соседних с ней вершин относительно направления сканирования:
int typeEvent(Vertex
if ((а <= 0) &£ (Ь <= 0)) return END_TYPE;
else if ((a > 0) &£ (b > 0)) return START_TYPZ; else return BEND_TYPE;
7.4.2.	Структура сканирующей линии
Структура сканирующей линии представляется в виде словаря активных ребер, т. е. тех ребер полигона, которые пересекают сканирующую: линию в данный момент, упорядоченных по увеличению координаты у.^Жроэй того, структура сканирующей линии содержит точку с минимальной коор* динатой у (лежащую ниже всех ребер), служащую в качестве строжа. Мы будем представлять структуру сканирующей линии в виде словаря обьаяов ActiveElement.
class ActiveElement (
₽UintCtype; // ACTIVE_EDGE (ребро) или ACTIVE_FOINT (точка) ActiveElement(int type)/ virtual double у(void) a 0;
virtual Edge edge(void) { return Edge(), b
virtual double slope(void) ( return 0.0» I/ );
Конструктор ActiveElement инициализирует элемент данных type, который показывает, является ли элемент ребром или точкой:
ActiveElement; ;ActiveElement(int t) type(t)
(
1
Остальные компонентные функции класса ActiveElement являются виртуальными и они будут обсуждены несколько позже, в контексте двух производных классов.
Активные ребра представляются в виде объектов класса ActiveEdge:
class ActiveEdge : public ActiveElement ( public:
Vertex *v;
Vertex *w;
int rotation;
ActiveEdge(Vertex *_v, int _r, Vertex *_w) ;
Edge2 edge(void);
double у(void);
double slope(void);
);
Элемент данных v указывает на одну из концевых точек текущего ребра, a v->cw () — на другой конец. Элемент данных w является целевой вершиной пары ребер, состоящей из текущего ребра и активного ребра, расположенного непосредственно над ним. Элемент данных rotation используется для отслеживания связи от текущего ребра к ребру, которое может пересекать текущее ребро за сканирующей линией: если такое ребро существует, то v->neighbor(rotation) определяет окно на ребре.
Конструктор ActiveEdge определяется непосредственно:
ActiveEdge::ActiveEdge(Vertex *_v, int r, Vertex ♦ w) • ( ActiveElement (ACTIVE_EDGE) , v(_v) , rotation (_r) ,~w (_w) )
Компонентная функция у возвращает координату у точки пересечения текущего ребра со сканирующей линией. Компонентные функции edge и “	~ возвращают текущее ребро и наклон текущего ребра соответственно.
<пи три функции определяются как:
double ActiveEdge::у(void)
return edge () .у (curx) ; )
Edge ActiveEdge: :edge (void)
Алгоритмы сканирования на плоскости
—
return Edge (v->point () , v->cw () >point ()) ;
double ActiveEdge::slope(void) (
return edge() . slope () ;
)
Структура сканирующей линии формируется так, чтобы она могла содержать точки наряду с ребрами, поскольку она должна обеспечивать операции определения положения точки в такой форме: для данной точки на сканирующей линии наити пару активных ребер, между которыми няходится данная точка. Вот поэтому мы и определили класс ActiveElement так, чтобы получить из него производные классы, один для представления ребер и второй — для представления точек. Для целей поиска внутри структуры сканирующей линии мы будем представлять точку в виде объекта класса ActivePoint:
class ActivePoint : public ActiveElement { public:
Point p;
ActivePoint (Pointfi);
double у (void);
);
Конструктор класса определим как:
ActivePoint: :ActivePoint (Point &_p) :
ActiveElement (ACTIVE_POINT) , p(_p) (	.. — . .’F
Компонентная функция у просто возвращает координату у текущей.«очки
. .. -«ЬгТШ
double ActivePoint::у(void) (
return p.y;
)
'•I
Определив необходимые классы, перейдем к рассмотрению стру тур сканирующей линии. Структура сканирующей линии создается с помощью функции buildSweepline. Функция инициализирует нов храняет активную точку, служащую в качестве стража-^ женную ниже любого ребра, которое будет записано в ело р
Dictionary<ActiveElement*> tbuildSweepline ()
ActivePoint (Point (0.0, -DBLJOX) ) > J

new
Функция сравнения активных элементов:
activeElementCmp применяется для сравнения двух
int activeElementOnp(ActiveElement *af ActiveElement *b)
<
double ya = a->y() ;
double yb = b->y();
if (ya < yb) return -1;
else if (ya > yb) return 1;
if ((a->type == ACTIVE_POINT) £& (b->type — ACTIVE_POINT) ) return 0;
else if (a->type == ACTIVE_POINT) return -1;
else if (b->type = ACTIVE_POINT) return 1;
int rval = 1;
if ((sweepdirection==IEFT_TO_RIGHT£Ccurtype=START_TYPE) | | (sweepdirection—RIGHT_TO_LEFT&Scurtype=END_TYPE))
rval "= -1;
double ma = a->slope() ;
double mb = b->slope();
if (ma < mb) return rval;
else if (ma > mb) return -rval;
return 0;
)
Функция activeElementCmp вначале сравнивает активные элементы а и b на основе координат у их точек пересечения со сканирующей линией. Если они пересекаются в одной и той же точке, то считается, что активная точка расположена под активным ребром. Если оба элемента а и b являются активными ребрами, то для определения, какое из них лежит ниже другого, используются их соответствующие параметры наклона. Если предположим, что сканирование осуществляется слева направо, то ребро с меньшим коэффициентом наклона расположено ниже другого, если точка-событие является начальной вершиной, и выше другого, если точка-событие является конечной вершиной. Если предположим, что сканирование производится справа налево, то роли начальной и конечной вершин взаимно меняются местами (например, начальная вершина при сканировании слева направо становится конечной вершиной при сканировании в противоположном направлении).
7.4.3.	Переходы
Рассмотрим теперь, как к
Когдэ сканирующая линияисходы, начиная с первого ных п₽бпяЫ Сна^ала отыскать в структур0™™07 начальн°й вершины и, то ляется Д U п Между которыми лежи сканиРУ,°щей линии два актив-DVJomeG ЧК° 1 встречи Ребер Ь к с то h	и. Если вершина и яа-
ребео а-А»ЛлЫИИ ” НоРшина и будет счит ” закосптся в структуру скани-чающее что С<*' К₽Оме того, если вешл^ целевой “ершиной для пар шина и, расщепляя” ”ЭЛОМом)’ то п<Хгон° “ ,,UJIncTc« “огнутой (оэна-аультате получается л ®дол». хорды, соеЛи?яИ;.г!^О₽ОМу «Ринадлежит вер-юлучадтся функция ctartTr^.	и .. ш. В ре-
t. юг/:
Алгоритмы сканирования на плоскости
void startTransition(Vertex* v, Dictionary<ActiveEleaient*> Св weep line)
ActivePoint ve (v->point () ) ;
ActiveEdge *a  (ActiveEdge*) sweepline. locate (fcve) ;
Vertex *w = a->w;
if (!isConvex(v)) (
Vertex *wp = v->split(w) ;
sweepline.inaert(new ActiveEdge(wp->cw() ,
CLOCKWISE,wp->cw ()));
sweepline.insert(new ActiveEdge(v->ccw() , COUNTER-CLOCKWISE, v)) ;
a->w = (sweepdirection==LEFT_T0_RIGHT)?wp->ccw() :v;
) else (
sweepline.insert(new ActiveEdge(v->ccw() , COUNTER_CLOCKWISE,v));
sweepline.insert(new ActiveEdge(vr CLOCKWISE, v)) ;
a->w = v;
)
)
Разбиение полигона v в функции startTransition выполняется с помощью функции Vertex: :split, вместо Polygon::split, поэтому пет необходимости знать, какому полигону принадлежит вершина и. По мере перемещения сканирующей линии в сторону вершины о в результате вы-
полнения операции split формируются новые полигоны и вершина и
может мигрировать из одного полигона в
другой, Работая на уровне вер:
•НИХ
а не погигонов, мы можем избежать необходимости отслеживать, какому
полигону принадлежит каждая вершина.
При обращении к функции isConvex (v) возвращается значение TRUE, если вершина v выпуклая, или значение FALSE в противном случае (вершина v вогнутая).
Функция определяется в виде:	•
bool isConvex(Vertex *v) {
Vertex *u = v->ccw();
Vertex *w = v->cw();
int c = w->classify(*u, *v) ;
return ((c " BEYOND) |I (c == RIGHT)) ;
Для обработки перехода для точки перегиба в вершине перегиба у сначала требуется найти в структуре сканирующей линии ребра а, б и с и принять вершину и в качестве целевой вершины для пар ребер аЬ п (рис. 7.126). Затем ребро b в структуре сканирующей линии зам:^“ ребро Ь', которое встречается с ребром Ь в вершине у. Таким образ j лучаем функцию bendTransition:
void bendTransition(Vertex *v,	£eweeoline)
Dictionary<ActiveElemont*> fisweepllne) (
ActivePoint vo (v->point ()) ,’
Гм**)
ActiveEdge *а  (ActiveEdge*) sweepline.locate (five) ;
ActiveEdge *b « (ActiveEdge*) sweepline. next () ;
В концевой вершине о сначала в струн-
Для обработки концевого перех	ребра а, b) с к d (рис. 7.12в). С
туре сканирующей линии пып7Клая тогда и должна быть самой край-одной стороны, если вершин	ЭТо ’не так, то полигон, которому при-
вей правой вершиной пол^°нбыл бы обладать изломом, указывающим влево в^сТоложен^м^’ева от и, что противоречило бы условию сканирования.

Поскольку вершина и является самой правой вершиной в полигоне, то на этот раз полигон добавляется в список polys. С другой стороны, если вер-за о вогнутая, то и становится целевой вершиной для пары ребер а-d, а ребра Ь и с удаляются из структуры сканирующей линии. Функцию endTransition получаем в следующем виде:
void endTransition(Vertex *v, Dictionary<ActiveElement*> Ssweepline, List<Polygon*> *polys)
(
ActivePoint ve(v->point ());
ActiveElement *a = sweepline.locate(6ve);
ActiveEdge *b = (ActiveEdge*)sweepline.next();
ActiveEdge *c = (ActiveEdge*)sweepline.next(); if (iaConvex(v))
polys->append(new Polygon(v))• else
((ActiveEdge*)a)->w = v;
sweepline.remove(b);
sweepline.remove(c);
)
нирТва^и слеааХ	-
И один - направленный вправо (одна из опепл, яаправленных влев0’ ляет два излома одновременно) Ппи «« операцик Расщепления split удалено удаляются два оставшихся и1лг^СЛ<?ДУЮЩеМ сканиР°аании справа на-ются семь монотонных полигонов КажлыйНа,П₽аВЛеННЫХ ВПраВ0> и получа* триангуляции с использованием ал гори*	ЭТИХ полигонов подвергается
Рис 7.13.	раЛмеии.	* ctw. монетой обмосй г. ни гухлслуглиен
Алгоритмы сканирования на плоскости
7.4.4.	Анализ
Проведем анализ работы программы regularise для входного полигона имеющего л вершин. По мере выполнения программы после рада последовательных обращений к функции semiregularise общее число вершив™, личиваетья на две вершины после каждого выполнения операции расщепления split. Однако, поскольку не более, чем | вершин исходного полигона могут оыть изломами, то может быть добавлено не более, чем 2(2) — л вершин, что дает в результате не более 2л вершин. Так как каждая вершина может возбудить не более двух переходов (по одному на каждое направление сканирования), то всего может потребоваться выполнить не более 4л переходов.
Время выполнения перехода каждого типа определяется в основном небольшим постоянным числом операций со словарем, O(log л). Поскольку всего выполняется О(л) переходов, полняется за время О(п log л).
что занимает время то весь алгоритм вы-
7.5.	Замечания по главе
Алгоритмы из раздела 7.1 для точек пересечения линий описан в [9].
Основа нашего алгоритма определения контура объединения п параллельных осям прямоугольников заимствована из [54]. Путем представления сканирующей линии с более сложной структурой поиска (с деревьями сегментов), Липски и Препарата получили алгоритм, выполняемый за время 0{п log п 4- г log(n2/r)), если контур состоит из г ребер. Дерево сегментов было использовано для представления «пустых» участков вдоль сканирующей линии, т. е. таких интервалов, которые не перекрываются ни одним из прямоугольников. (В нашей терминологии «пустые» участки определяются интервалом между активным горизонтальным ребром со значением счетчика 0 и другим ребром со значением счетчика 1). Время, затрачиваемое на поиск внутри структуры сканирующей линии, будет зависеть от числа обнаруженных вертикальных ребер контура. Гютинг [37] впоследствии разработал оптимальный алгоритм, который выполняется за время O(r + п log л). Наш алгоритм регуляризации полигона взят из [52], а формулировка и доказательство теоремы о монотонности полигона — из [31]. Хертел и Мелхорн [40] описали алгоритм сканирования на плоскости для триангуляции произвольного полигона за время О(л + г log г), где г равно числу вогнутых вершин в полигоне. Сканирующая линия перемещается слева направо по частям (а не целиком в виде единственное вертикальное линии) и останавливается ве более, чем на О(г) вершстах Из други соо-собоо триангуляции полигонов стоит отметить способ Тарьяна и оакВика (84],который выполняется .за время О(п log log я), я недвяо пймг способ Хезеля. [19]. выполняемый за	время О(я). Обзор ме-
тодов разбиения полигонов представлен в [60, 61J.
л отрезков прямых
7.6.	Упражнения
1.	Измените программу поиска точек пересечения в коллекции отрезков прямых линий, чтобы она осталась корректной, даже если опустить предположение, что в одной точке могут пересекаться только два отрезка
ВШИ
2.	Разработайте алгоритм сканирования на плоскости для вычисления площади объединения коллекции прямоугольников, параллельных осям координат.
3.	Используя нотацию «большого О*,.выразите пространственную сложность нашего алгоритма поиска пересечения отрезков прямых линий в функции от числа отрезков прямых линий и количества найденных пересечений. Какова была бы пространственная сложность, если функ-ция обработки переходов была модифицирована так, чтобы точка-событие удалялась из перечня точек-событий только при необходимости ее обработки и не удалялась в противном случае. Изменится ли сложность времени выполнения алгоритма?
4.	Разработайте алгоритм для поиска всех точек пересечения в наборе из п горизонтальных и вертикальных отрезков прямых линий. Алгоритм должен выполняться за время О(п log п + г) , где г — число сообщений о пересечениях.
5.	Для двух заданных выпуклых полигонов Р и Q составьте алгоритм сканирования на плоскости для вычисления пересечения Р и Q за время, пропорциональное |Р| + |Q|. (Подсказка: границы полигонов Р и Q совместно могут служить в качестве перечня точек-событий. По мере обнаружения точек пересечения они могут служить для объединения границ Р П Q ).
6.	Для чего предназначен элемент Rectangle: :id в алгоритме поиска контура?
7.	Покажите, что задача поиска контура для п прямоугольников имеет нижнюю границу Q(n log л). (Подсказка: функция сортировки должна быть адаптирована к условиям задачи. Для ряда чисел xi, Х2»..«» подлежащего сортировке, каждое число х/ соответствует прямоугольнику с параллельными осям сторонами с координатами углов (0, 0) и (xj.M), где М = тахДхД.)
8.	Составьте алгоритм сканирования на плоскости для триангуляции произвольного полигона, выполняемый за время О(л log л). (Подсказка: триангуляция полигона должна осуществляться в процессе его регуляризации.)
9.
Для заданного полигона Р триангуляция набора вершин полигона Р является триангуляцией вершин, которые считаются точками на плоскос-ппи^Рп7₽1М^ГРаНИЧеНИеМ ЗДеСЬ будет то’ что кажД<* ребро Р должно кости ямппгта триа^гуляцин' Составьте алгоритм сканирования на плос-шин любого пппМЫ 1 ЗЭ °Ремя °(п	л)« Для триангуляции набора вер-
щего упражнения.)НЭ одсказка: модифицируйте алгоритм предыду-
Алгоритмы сканирования на плоскости	му
и
10. Для заданного набора п дисков переменного радиуса на плоскости со* ставьте алгоритм сканирования на плоскости для определения за время О(п log п) будут ли пересекаться любые два диска.
11. Говорят, что точка р = (р*,ру) доминирует над точкой q = (?x>9y)t если рх > qx и ру > qy. Когда р принадлежит набору точек S, то точка р называется максимальной, если ни одна из точек в S не доминирует над р. Составьте алгоритм сканирования на плоскости для сообщения о всех максимальных точках в наборе из п точек за время O(n log л).
Глава 8
Алгоритмы типа «разделяй и властвуй»
При реализации подхода «разделяй и властвуй» задача делится на несколько подзадач, которые решаются рекурсивно и затем достигнутые результаты объединяются для получения решения исходной задачи. По своему виду подзадачи сходны с исходной задачей, но они меньше по размеру и, естественно, сумма размеров подзадач равна размеру исходной задачи. Если задача достаточно мала или проста, она может быть решена непосредственно и нет необходимости ее дальнейшего деления — это условие является критерием для окончания процесса деления. В наиболее обычной форме при использовании принципа «разделяй и властвуй» задача делится на две подзадачи, каждая из которых по размеру вдвое меньше исходной задачи.
Рассмотрим алгоритм «разделяй и властвуй» для нахождения минимального значения в массиве целых чисел. Идея заключается в делении массива на левый и правый подмассивы и в рекурсивном применении к ним алгоритма поиска минимального значения в каждом из подмассивов, после чего возвращается наименьшее из полученных двух значений. Базовая ситуация возникает при длине подмассива, равной единице — тогда единственное значение, содержащееся в нем и возвращается в качестве решения. Алгоритм реализуется в функции fMin, которая возвращает наименьшее целое значение в подмассиве а[1..г]. Функция findMin является ведущей функцией, которая на верхнем уровне осуществляет обращение к функции fMin;
int findMinfint а[], int n) (
return fMin(a, 0, n-1);
)
int fMin(int a[], int 1, int r) (
if (1 = r)
return a[l];
else (
int a = (1 ♦ r) / 2;
int i • fMin(a, 1, m); int j - fMin(а, o+l, r) ; return (i < j) ? i - j.
)
в *-------—
рост просмотр .массива слева направо с сохранением те‘
ВмЫПО™ЯРМЫх сравнений функция findMin также эф-
Алгоритмы типа «разделяй и властвуя»
кущего найденного наименьшего значения (при обоих способах выполняется п 1 сравнении). Практически рекурсивный подход менее эффективен из-за возможного переполнения при рекурсивном обращении к функции, но он служит хорошим примером использования метода «разделяй и властвуй»
Эту главу мы начнем с описания способа сортировки путем слияния — классического алгоритма сортировки методом «разделяй и властвуй». Затем представим способ формирования области пересечения коллекции полуплоскостей и применим этот способ к задачам определения ядра полигона и построения полигонов Вороного (определение которых будет дано ниже). Далее мы вернемся к задаче построения выпуклой оболочки для планарного набора точек. Затем применим метод «разделяй и властвуй» в эффективном алгоритме поиска пары ближайших точек для набора точек на плоскости. И, наконец, используем метод «разделяй и властвуй» для триангуляции произвольного полигона.

8.1.	Сортировка путем слияния
Сортировка путем слияния является единственным алгоритмом сортировки, который выполняется за оптимальное время О(п log л) в наихудшем случае. Заданный для сортировки массив элементов при сортировке путем слияния делится на левый и правый подмассивы примерно равного размера, рекурсивно сортируется каждый из двух подмассивов и затем оба только что отсортированных подмассива сливаются в один отсортированный массив (рис. 8.1). Поскольку отсортированные подмассивы могут быть объединены за линейное время, время работы определяется рекуррентным выражением Т(п) = 2Т (£) 4- ап, которое сводится к T(n) е О(п log п).
Рис. 8.1. Сортировка путем слияния
Шаблон функции mSort выполняет сортировку путем слияния подмассивов а(1. .т], используя функцию сравнения стр. Шаблон функции mer-
ge Sort определяет ведущую функцию:
templateCclass Т>
void mergeSort(Т а[), int n, int (*сгар) (
mSort(а, 0, п-1, стр);
void sSortCT a[], int 1, int r, int («cap) (T,T)) {
i£ (1 < r) ( int  - (1 ♦ r) / 2; Sort(a, I, , cap); Sort(a, ♦!, r, cap); serge(a, 1, a, r, cap);
При обращении к функции merge (а,	1, m, г, стр) происходит
сортировка путем слияния двух подмассяпов afl-.-m] и а[т+1..г]. Рассмотрим, как можно соединить два отсортированных массива а и b в третий массив с достаточной длины. Для реализации этого будем поддержи вать индекс aindx, который указывает на наименьший (самый левый) элемент в массиве а, еще не скопированный в массив с, п индекс bindx, определенный аналогично для массива Ь, а также индекс cindx, указывающий па следующую (самую левую) свободную позицию в массиве с. На каждом шаге будем копировать меньший из двух элементов a [aindx; и b[bindx] в с[cindx] и затем увеличивать на единицу cindx, а также либо aindx, либо bindx в зависимости от того, какой элемент был только что скопирован. Если aindx выходит за пределы самого правого элемента массива а, то оставшиеся элементы в массиве b (начиная с индекса bindx) копируются в оставшиеся позиции массива с. Аналогично, если индекс bindx выходит за пределы самого правого элемента массива Ь, оставшиеся элементы массива а копируются в массив с.
Следующий шаблон функции осуществляет слияние отсортированных подмассивов х[1..ш] и х[т+1..г] в отдельный массив с, который затем копируется обратно в х [ 1.. г ]:
templateCclass Т>
void merge (Т х[], int 1, int m, int r, int (*cmp) (T,T)) {
T *a = x+1;
T *b = x+n+1;
T *c = new T[r-1+1];
int aindx = 0, bindx = 0f cindx =0;
int alim = ш-1+l, blun = r-m;
while ((ain<hc < alim) && (bindx < blim)) if (<*cmp) (a[aindx), b[bindx]) < 0) c[cindx++] = a[aindx++];
else
c[cindx++] = b[bindx++) ;
while (aindx < al is)	// копирование остатка a
c[cindx++] = a[aindx++);
while (bindx < blim)	// копирование остатка b
c[cindx++] = b[bindx++];
for (aindx=cindx=0; aindx <= r-1; a[aindx-HJ = c(cindx-Hj) „f	if обратное копирование
delete c;
)
Сортировка путем слияния оказывается эффективной потому, что два отсортированных массива сливаются за время. пропорциояал^оаТу^ ЖХ размеров. Функция гетре выполняется за время О(г-П. поскольку ждждый из г - I ♦ ] элементов, вохтеж.щкх слиянию, пересылаетсяХо у«жды: сначала в массив z и потом обратно в массив х. Если черв, Т(л) обозначить время, затрачиваемое на сортировку путем слияния массива из п элементов, то на выполнение программы потребуется время
7*(Л> _ [ 2Т (£) . ап если п > 1
j b	если л = 1
при подходящих константах а рекурсивной сортировки двух последующее слияние. Таким
и Ь. Здесь элемент 2Т (£) представляет время подмассивов, а элемент ал — время на их образом мы имеем Т(л) е О(л log я).
8.2.	Вычисление области пересечения полуплоскостей
Прямая линия делит плоскость на две полуплоскости, по одной с каждой стороны прямой линии. Полуплоскости являются самыми простейшими ив выпуклых планарных областей, но путем выполнения операций nepcceqemt* я объединения соответствующим образом выбранных полуплоскостей ив них может быть сформирован любой полигон. В этом разделе мы ограничимся задачей формирования области пересечения для коллекции полуплоскостей.
Пусть Н будет коллекцией полуплоскостей и через /(Я) обозначим об-
ласть их пересечения. Если она непуста, то область /(Я) называется выпук-
лым политопом. Область 1(H) безусловно является выпуклой, поскольку
пересечение выпуклых областей всегда выпуклое. На рис. 8.2 изображены два выпуклых политопа, образованных пересечением полуплоскостей. На
рис. 8.2а это выпуклый полигон, тогда как на рис. 8.26 получился неограниченный выпуклый политоп. Полуплоскость в Н является лишней, если
ее удаление не изменит 1(H). На рис. 8.2 л
«.••it:
яя только полуплоскость.
определяемая прямой линией е.
Один из способов формирования пересечения п полуплоскостей заключается в их поочередной обработке. Самая непосредственная реализация та-
8.2. два выпуклых помпона, образованных пересечением полупмхэтаей
4 Заказ 2794
IM
Глава 8
кого подхода будет выполняться за время О(п2). Но здесь с успехом можно примачитъ способ «разделяй и властвуй», поскольку набор пересечений ассоциативен — область пересечения л полуплоскостей может быть получена вычислением пересечения пар полуплоскостей е любом порядке. Следова-
тельно, при любом делении набора Н на два непустых поднабора Hi и Hg будем иметь 1(H) e /(Hi) Г) /(На). Для вычисления 1(H) с использованием способа «разделяй и властвуй» мы будем разбивать коллекцию полу-
плоскостей Н на два непустых поднабора Hi и Нг примерно одинакового
размера, затем рекурсивно вычислять /(Hi) и /(На), после чего комбинировать их для получения /(Hi) П /(На). В конечном случае (п = 1) будем
просто возвращать чистую полуплоскость из Н.
Возвратимся к реализации. Нам необходим способ представления границ
выпуклых политопов, получаемых в процессе выполнения алгоритма, а также окончательного политопа 1(H). К сожалению, для этого мы не можем использовать объекты класса Polygon, поскольку границы неограниченных выпуклых политопов не замкнуты. Но вместо определения нового класса мы будем отсекать 1(H) по границам выпуклого ограничивающего прямоугольника В. Тем самым будет обеспечено, что каждый политоп, выполняемый
по мере выполнения алгоритма, будет ограничен и определен как пересечение некоторого выпуклого политопа и ограничивающего прямоугольника В,
В программе halfplanelntersect мы будем представлять полуплоскость в виде объектов класса Edge и считать, что полуплоскость лежит справа от ребра. Программе задается массив Н из п ребер и выпуклый ограничивающий прямоугольник box. Программа возвращает выпуклый полигон, равный пересечению прямоугольника box и п полуплоскостей в наборе Н:
Polygon ‘halfplanelntersect(Edge H[], int п, Polygon Sbox) (
Polygon *c;
if (n = 1)
clipPolygonToEdge(box, H[0) , c) ;
else (
int m = n / 2;
Polygon ‘a = halfplaneZntersect(H, m( box);
Polygon *b = halfplaneZntersect(H+m, n-m, box) ;
c = convexPolygonlntersect(*a, *b);
delete a;
delete b;
1 return c;
>
В программе используются две функции, которые были определены ранее. Функция clipPolygonToEdge из раздела 5.7 применяется для выделения части ограничивающего прямоугольника, лежащего справа от ребра (для каждой полуплоскости). Функция convexPolygonlntersect из раздела 6.5 формирует область пересечения для двух выпуклых полигонов.
Ограничивающий прямоугольник, который будет передаваться в программу halfplanelntersect, должен быть достаточно большим, чтобы не потерять нужную информацию. В идеале полуплоскости, образованные ребрами
. ограничивающего прямоутолышка. яаляются лншаиия так _ возвращаемое программой, равно ЦН). Если яго яеьозможког сечение /(Я) безграиичио, ограяпчпвшнй прямоугольник’ до^охьХ ить асе вершины, принадлежащие ДИ) „ , помелу^ только неинтересная часть 1(H). Ответственность а. устанооку подхДягам гранил ограничивающего прямоугольника возлагается на прХщ^^ грамму, которая вызывает функцию halfplanelntersect
8.2.1.	Анализ
При размере входного массива, равном п, время работы Т(п) программы halfplanelntersect определяется рекуррентным выражением
2Т (у) + ап если л > 1
b	в противном случае (п = 1).
T(n) =
Здесь элемент ал соответствует временя, которое затрачивается функцией convexPolygonlntersect для формирования области пересечения двух выпуклых полигонов. Отсюда следует, что программа halfplanelntersect выполняется за время О(л log л).
Этот алгоритм оптимален, поскольку для подобных задач нижний предел составляет П(п log л). Чтобы показать справедливость этого высказывания, выполним переход от задачи сортировки к рассмотренной задаче формирования выпуклого полигона, что можно осуществить за линейное время, Для заданного ряда чисел xi, хг,..., хл, подлежащих сортировке, отобразим каждое число xi в точку с координатами (xj, хр), располагающуюся на параболе у = х%. Пусть Hi будет полуплоскость, ограниченная снизу прямой л
касательной к параболе в точке (х/, хр). Затем сформируем выпуклый политоп 1(H), равный пересечению полуплоскостей	Здесь ребра 1(H)
упорядочены по их наклону, а точки их контакта с параболой (х/, хр) упорядочены вдоль оси абсцисс. В результате получим значения координат ребер xi в порядке обхода 1(H).

8.3.	Определение ядра полигона

Напомним из раздела 5.2, что ядро полигона определяется как множество точек, из которых видны все точки внутри полигона. Если оно непустое, то ядро полигона Р представляется в виде выпуклого полигона, лежащетовнут-ри полигона Р. при этом говорят, что
РИС. 5.3). В этом разделе мы будем применять функцию Ьа1^Р^1"ее”емя в алгоритме построения ядра полигона, который вьш
О(л log л).
Наш алгоритм основан на следующей теореме.
Теорема 5. (Теорема о построении ядра). Предположим^^	равно перс-
набор п полуплоскостей, определяемых ребрами Р. Тогда ядро для ра семению КН) этих полуплоскостей.
Глава 8
Здесь предполагается, что полуплоскость, задаваемая ребром, ограничена этим ребром и располагается справа от него.
Теорема следует из такой цепи эквивалентных утверждений: (1) точка q принадлежит ядру Р, (2) точка q видит каждую точку полигона Р, (3) ни одно из ребер Р не нарушает видимости точки q, (4) точка q не может лежать слева от какого-либо ребра, (5) точка q гхртщимжт каждой полу-плоскости, определяемой ребром из набора Я, (6) точка q принадлежит 1(H) (см. рис. 8.3).
Рис. 8.3. Ядро полигона Р равно пересечению полуплоскостей, определяемых ребрами Р
Основываясь на этой теореме, мы можем сконструировать ядро полигона путем применения функции halfplanelntersect к ребрам Р. Любой ограничивающий прямоугольник, содержащий внутри себя полигон Р, будет достаточен, поскольку ядро располагается внутри Р. В приводимую ниже функцию kernel передается полигон р. Она возвращает полигон, представляющий ядро полигона р, или пустой полигон, если р не является звездчатым полигоном:
Polygon *kemel (Polygon &р) {
Edge *edges = new Edge [p. size ()] ;
for (int i = 0; i < p.size(); i++, p.advance(CLOCKWISE)) edges [i] - p.edge();
Polygon box;
box.insert(Point <-DBL_MAX, -DBL_MAX));
box.insert(Point(-DBL_MAX, DHL MAX));
box.insert(Point(DBL_MAX, DBL_MAX));
box.insert(Point(DBL_MAX, -DBL_MAX));
Polygon *r = halfplanelntersect(edges, p.sizeQ, box); delete edges;
return r;
)
в

Алгоритмы типа «разделяй и властвуй»
й •
I
1
1
8.3.1.	Анализ
»
Программа kernel вычисляет ядро п-угольника за время О(п 1оя я) ™ время в основном определяется обращением к функции ЬаИЪ1апХ.гео1Г Интересно, что программа не оптимальна - V, р,^	«
главе упоминается оптимальный алгоритм определевия ядра, который вы , полняется за время О(д), Можно было бы ожидать, что существует ли-нейное сведение задачи поиска области пересечения полуплоскостей к за* даче определения ядра и таким образом трансформировать нижнюю границу оценки Я(и log л) предыдущей задачи к пашей последней задаче. Однако такого сведения не существует. Ребра полигона расположены не произвольно, поскольку они соединяются в вершинах. Оказывается, что легче сформировать область пересечения полуплоскостей, определяемы? ребрами полигона, чем вычислять пересечение произвольной коллекции полуплоскостей.

8.4.	Определение многоугольника Вороного
Пусть S будет конечным набором точек на плоскости и есть еще точка р, не принадлежащая S. Многоугольник Вороного, соответствующий точке р относительно множества S, обозначается как ffls(p) , он состоит из локусов точек на плоскости, которые расположены ближе к точке р, чем к любой другой точке из множества S. Для каждой точки q Е S локус точек, лежащих ближе к р, чем к д, равен полуплоскости, которая ограничена перпендикуляром, проведенным через середину отрезка прямой линии рд, и которая содержит точку р. Многоугольник Вороного Ms(p) равен пересечению всех таких полуплоскостей, образованных точками д множества S (см. рис. 8.4а). Этот факт можно зафиксировать в виде теоремы: •
Теорема 6. (Теорема о многоугольнике Вороного). Для двух заданных различных точек р и q многоугольник KJ2[q)(p) равен полуплоскости, ограниченной перпендикуляром к середине отрезка pq и расположенной со стороны от перпендикуляра, в которой находится точка р. Более того, для заданных непересекающиха lawyw точек А и В и точки р A U В будем иметь (ЗДив (Р) = ЕЧл(р) П КВДр).
<•> <в’
б.с.в.4.	,
И двойственная ей триангуляция Делоне
Первое утверждение теоремы следует из того факта, что все точки перпендикуляра к середине отрезка pq находятся на равном расстоянии от точек р и q. Что же касается второго утверждения теоремы, то нетрудно видеть, что (р) С Е^л(р) П К^в(р). Для доказательства ^(р) П К^в(р) С Юлив (р) предположим, что точка в принадлежит К^ч(р) А ^в(р). Тогда точка « будет ближе к р, чем к любой точке из множества А, а также ближе к р, чем к любой точке из множества В. Следовательно, точка в должна быть ближе к р, чем к любой точке из A U В, т. е. а Е (р)»
Как следствие этой теоремы мы можем вычислить путем образования пересечения полуплоскостей Kfc/qjtp) для всех точек q из множества S. Полуплоскость ИЯ(д)(р) ограничена перпендикуляром к середине отрезка прямой линии pq, определяемым ребром Edge(p,q) . rot(). Интересующая нас полуплоскость находится справа от этого ребра.
Программе voronoiRegion задается точка р, массив s из п точек и ограничивающий прямоугольник box. Программа возвращает многоугольник Вороного для точки р относительно набора точек з:
Polygon *voronoiRegion(Point fip, Point s[], int n, Polygon &box)
(
Edge «edges = new Edge[n];
for (int i = 0; i < n; i++) (
edges [i] =Edge(p, s[i]); edges[i].rot();
)
Polygon *r = halfplanelntersect(edges, n, box);
delete edges; return r;
}
Конечно, в случае неограниченного многоугольника Вороного его части, выходящие за пределы ограничивающего прямоугольника, будут отсечены, это делается обязательно. В идеале ограничивающий прямоугольник должен охватывать все вершины многоугольника Вороного, так что будут отсечены только неинтересные части. Однако, для заданной фиксированной точки р совсем несложно сформировать такой набор точек S, что вершины ffls(p) окажутся произвольно далеко от нее.
8.4.1.	Диаграммы Вороного
Для заданного набора точек S диаграмма Вороного, обозначаемая как K3(S) состоит из коллекции многоугольников Вороного для каждой из точек набора S относительно остальных точек из этого набора. Т. е. многоугольники Вороного в ИВД определяются в форме	для каждой точки
р в наборе S. Диаграмма Вороного И3(5) разбивает всю плоскость на выпуклые политопы (рис. 8.46 и 8.5).
Можно привести очень интересную аналогию из области кристаллографии. Предположим, что точки представляются в виде ядер зерен кристалла, которые растут с одинаковой постоянной скоростью во всех направлениях. Предположим также, что рост зерен кристалла продолжается до тех порядок, пока два или более зерен не встретятся. Через некоторое достаточное
Рис 8.5 Диаграмма Вороного для набора из 80 то-ос
IMS
время каждое выросшее зерно будет представлено в виде многоугольника Вороного для своего ядра (вполне очевидно, что рост крайних неограничен' ных областей будет продолжаться бесконечно). В результате будет получена диаграмма Вороного для набора S. (Весьма забавно представить более быстрый процесс роста в одном из направлений и при расположении ядер о фиксированных точках в пространстве. Полученная в результате диаграмма Вороного в трехмерном пространстве будет состоять из ограниченных и неограниченных полиэдральных областей.)
Для краткости мы будем писать ЕЯ(р) вместо И^5-{р)(р). С целью упрощения рассуждений будем предполагать, что никакие четыре точки исходного множества S не лежат на одной окружности. (Напомним из раздела 6.6, что набор точек считается принадлежащим окружности, если все точки лежат на этой окружности, и если такая окружность существует и она уникальна, то она называется описанной окружностью для этих точек.)
Ребро диаграммы Вороного KZ(S), лежащее между двумя многоугольниками Вороного ЮТ(р) и ^(д), состоит из точек на плоскости, эквидистантных точкам р и q, и расположено ближе к этим точкам, чем к остальным точкам из набора S. Каждая вершина о диаграммы Вороного K3(S) является точкой встречи трех многоугольников Вороного V$(p), W(q) н КЯ(г). Будучи эквидистантной от трех точек р, д и г, вершина о является центром описанной окружности для этих трех точек. Более того, поскольку точка и является ближайшей к точкам р, д и г, чем к остальным точкам из набора S. то внутри этой описанной окружности нет никаких других точек иэ заданного набора. Из этого следует, что треугольник Apqr является треугольником Делоне для набора S. (Триангуляция Делоне была описана в разделе 6.6.) Таким образом, каждой вершине диаграммы Вороного соответствует треугольник триангуляции Делоне, а каждому многоугольнику Вороного cwr-ветствует вершина триангуляции Делоне (точки из набора S). Гово^т, что диаграмма Вороного и триангуляция Делоне двойственны друг к ДРУГУ* показано на рис. 8.4в, где две схемы наложены друг
Программа voronoiDiagram формирует диаграмму Вороного дм з из п точек. Она возвращает список многоугольников Вороного, же которых состоит диаграмма.
Глан8
List<Polygon*> «voronoiDiagram(Point (], int n, Polygon Sbox) (
List<Polygon*> *regions » new List<Polygon*>;
for (int i - 0; i < n; i++) (
Point p  s[i];
a[i] - s[n-l];
r«gion8->append(voronoiRegion(p, s, n-1, box));
 [i] - p;
>
return regions;
8.4.2.	Анализ
Затраты времени на выполнение программы voronoiRegion в основном определяются обращениями к функции halfplanelntersect, так что она выполняется за время О(п log л). Конечно, это оптимальный результат. Чтобы убедиться, что задача определения многоугольников Вороного имеет нижний предел £1(п log л), рассмотрим следующую линейную по времени редукцию задачи сортировки. Пусть для сортировки задана последовательность чисел Xi, ха»..., хп, отобразим каждое из этих чисел xi на точки р(х;), расположенные на окружности так, чтобы точки p(xj) оказались упорядоченными вдоль окружности в соответствии с возрастанием х/. Затем построим многоугольник Вороного для точки центра окружности относительно этих л точек. Многоугольник Вороного в данном случае будет представлен в виде выпуклого л-угольника, каждое ребро которого расположено между центром окружности и одной из точек р(х/) на окружности. Теперь можно составить список точек p(Xi) в порядке расположения ребер вокруг многоугольника Вороного.
Хотя для вычисления многоугольника Вороного по набору S из л точек потребуется время П(л log л), но в пределах тех же самых временных границ можно выполнить гораздо больше. Так в замечаниях по главе будет упомянут алгоритм, позволяющий полностью определить диаграмму Вороного для S за время О(л log л). Это позволяет улучшить время выполнения программы voronoiDiagram в л раз.
8.5.	Слияние оболочек
В этом разделе мы представим алгоритм < разделяй и властвуй* для вычисления выпуклой оболочки CH (S) для набора точек S. Идея заключается в разделении массива S воображаемой вертикальной прямой линией на два равных по размеру подкабора S/J и Зя, затем рекурсивно сформировать из них CH(Sl) и CH (Sb) и затем объединить их для образования 6W(S). В конечном счете набор будет состоять из одной точки (|S| = 1) и будет просто
возвращаться одноугольник, единственная вершина которого принадлежит набору S. Эта точка является собственной выпуклой оболочкой.
Алгоритм асимптотически эффективен, поскольку выпуклые оболочки CH (Sl) и CW (5я) могут быть эффективно объединены за время O(|S|). Для выполнения слияния нужно удалить правую цепочку CH(Sl) и левую цепоч-ку CH (Sr) и заменить их на верхний мостик и нижний мостик (рис. 8.6). Такие мостики обязательно существуют, поскольку 6W(Sl) и СИ (Зя) разъ-
Алгоритмы типа «разделяй и властвуя.
Рис. В.6. Слияние двух раздельных выпуклые оболочек
единены — они лежат по разные стороны воображаемой прямой линия. Подобное слияние представляет самую сложную задачу в алгоритме и мы обсудим ее несколько ниже.
8.5.1.	Программа верхнего уровня
Программе mergeHull передается массив pts на п точек и она возвращает полигон, представляющий выпуклую оболочку для этих точек:
Polygon *mergeHull(Point pta[], int n) (
Point **p = new (Point*)[n] ;
for (int i = 0; i < n; i++)
p[i] = 6pta[i];
msrgeSort(p, n, leftToRightQnp) ; return inHull (p, n) ;
I
Программа mergeHull осуществляет предварительную сортировку точек слева направо, чтобы впоследствии наборы точек можно было бы быстро разделить на правый и левый поднаборы.
В функции mHull реализуется рекурсивная часть алгоритма:
Polygon *mHull(Point *p[J, int n) (
if (n » 1) (
Polygon *q = new Polygon;
q->ineert(*p(0)); return q;
) else (
int m = n / 2;
Polygon *L = mHull(p, m) ;
Polygon *R = mHull(р+т, n-m) ; return merge(L, R)>
)
конечной ситуации
В
В конечной ситуации in—-м ж/---общем случае функция делит набор точек
на левый и правый поднаборы
точек p[0..m-l) п p[m..п-1], затем формирует для них выпуклые оболочки L и R и выполняет слияние полученных двух оболочек.
8.5.2.	Слияние двух выпуклых оболочек
Обращение к функции merge(L, R), которая объединяет две выпуклые оболочки L и R, зависит от способа записи мостика. Напомним из раздела 5.3 определение касательной прямой линии', прямая линия £ будет касательной прямой линией выпуклого полигона Р, если прямая линия € проходит через вершину полигона Р и внутренняя часть полигона Р целиком располагается по одну сторону прямой линии (. Прямая линия £ будет мостиком для выпуклых полигонов Р и Q, если £ является касательной прямой линией для обоих Р и Q. Прямая линия £ будет верхним мостиком, если оба полигона расположены ниже прямой линии £, или нижним мостиком — если они оба расположены над прямой линией £.
Для слияния полигонов L и R нам нужно найти верхний мостик, соединяющий некоторую вершину li Е L с некоторой вершиной и. G Я, а также нижний мостик, соединяющий вершины 1% Е L и Г2 G R. Вершины h и 12 делят границы полигона L на левую и правую цепочки и, аналогично, вершины ri и Г2 делят границы полигона R на левую и правую цепочки. Для слияния L и R в CH. (S) нам потребуется заменить правую цепочку полигона L и левую цепочку полигона R на верхний и нижний мостики.
Функция merge определяет общую выпуклую оболочку для выпуклых полигонов L и R и возвращает ее по завершении работы функции. Предполагается, что полигон L расположен слева от полигона R. Параметры UPPER и LOWER имеют тип перечисления:
Polygon ‘merge(Polygon *L, Polygon *R) <
Vertex *11, *rl, *12, *r2;
Vertex *vl = leastvertex(*L, rightToLeftCmp);
Vertex *vr = leastvertex(*R, leftToRightCmp); bridge (L, R, 11, rl, UPPER);
L—>eetV(vl);
R->setV(vr);
bridge(L, R, 12, r2, LOWER);
L—>setV(ll);
Lr->eplit(rl) ;
R->satV(r2);
delete R->split(12);
return R;
При обращениях к функции leastvertex находятся самая правая вершина полигона L и самая левая вершина полигона R, Эта функция была определена в разделе 4.3.6.
Две операции split в функции merge осуществляют замену правой цепочки полигона L и левой цепочки полигона R на верхний и нижний мостики. Как это делается, показано на рис. 8.7,


*

Рис. В.7. Слияние выпуклых оболочек L и R с помощью двух операций split
Посмотрим, как функция bridge находит верхний мостик полигонов L и к. Функция находит некоторую касательную прямую ливню для полигона L, затем для полигона R и последовательно их проверяет до тех пор, пока не будет найдена одна общая касательная прямая линия, т. е. верхний мостик (рис. 8.8а). Для этого функция bridge сначала размещает окно vl па самой правой вершине полигона L и окно ог на самой левой вершине ггплп.

s
(2 к полигону L, которая
1
8
£1 к S, которая проходит S.
S, которой касается пря-
•“ >
j-i м
‘til 9.1
’./I
. - I
•-НВ•
. -I
(а)	(б)
Рис. 08. Последовательность поиска верхнего и ниснего мостиков для выпухльа полисное


гона R. Затем она итеративно выполняет следующую группу операций до тех пор, пока нельзя будет переместить ни окпо (Я, ни окно vr.
1.	Найти такую касательную прямую линию через вершину vl и лежит выше полигона
2.	Установить окно иг на вершине полигона мая fi.
3.	Найти такую касательную прямую линию
располагается выше полигона L и проходит через вершину иг.
4.	Установить окно vl на вершину полигона L, которой касается прямая £г.
Нижний мостик для полигонов L и R находится аналогично, только с "выше па ниже. На рис. 8.8 показана последовательность касательных прямых линий в процессе поиска верхнего я ниж-мостих между выпуклыми полигонами L я В.

заменой параметра определения L-
него мостиков.
Функция bridge находит мостик мелкду	""~^аахпся через
d	млгтмк касается полигонов L я л. возвращаются
Вершины, в которых мостик касаешл	гтпг,а«атп tune указывает и
ссылочные параметры vl иип'	гажнег0 LOWER. В функции
Х^вго^иГп^Тнаходится слева от полигона Я и что окно т!
первоначально расположено на некоторой вершине правой цепочки полиго-
на L, а окно vr — на некоторой вер:
Я:
?в левой цепочки полигона

void bridge(Polygon *L, Polygon *R, Vertax* fivl, Vertex* fivr, int type) {
int aides[2] « ( LEFT, RIGHT };
int indx » (type “= UPPER) ? 0 : 1; do (
vl " L->v() ;
vr * R->v() ;
supportingLine(L->point(), R, sides[indx]);
supportingLine(R->point()t L, sides[1-indx]); ) while ((vl != L->v()) || (vr l=R->v()));
Функция supportingLine была определена в разделе 5.3 как часть алгоритма ввода оболочки.
8.5.3.	Анализ
Нетрудно видеть, что полигоны L и R сливаются за время, пропорциональное |L| + |Н|. Чтобы найти каждый мостик, функция bridge попеременно проверяет каждую вершину в полигонах L и Я. Если вершина просмотрена, то снова к ней не возвращаются. Следовательно, каждый из двух мостиков находится за время О (|L| + р?|), так ’что оба обращения из функции merge к функции bridge занимают линейное время. Более того, функция merge затрачивает линейное время на обращение к функции leastvertex и постоянное время на свои другие операции. Это означает, что функция merge выполняется за линейное время.
Из сказанного следует, что обращение на верхнем уровне к функции mHull выполняется за время Т(л), где
Т(п) = 2Г Ф + ап если п > 1
&	в противном случае (n = 1)
В результате получаем решение Т(п) Е О(п log л).
На параметры алгоритма не влияет начальная сортировка точек, поскольку она также занимает время О(п log л). Следовательно, в худшем случае программа слияния оболочек выполняется за время О(п log л).
8.6.	Ближайшие точки
В этом разделе мы рассмотрим задачу поиска ближайших пар точек: для заданного набора S из л точек найти такие две точки, расстояние между которыми меньше (или равно) расстояния в любой другой паре точек. Такие две точки будем называть ближайшей парой в наборе S, а расстояние между ними расстоянием ближайшей пары. Если л = 1, то расстояние ближайшей пары в наборе S равно бесконечности.
Самым грубым решением этой задачи является непосредственное вычисление расстояний для каждой пары точек, отслеживая значение минимлль-
Алгоритмы типа «разделяй и властвуй»
кого расстояния (для краткости под термином расстояние будем понимать «расстояние для некоторой пары точек.). Поскольку существует пар то поиск решения при таком подходе займет время 9(п2) . Здесь ьпд пред-ставпм решение, основанное на методе «разделяй и властвуй», которое вы* полняется за время O(n log л),
В общем случае наш алгоритм работает следующим обраоом. При ведан* ном наборе точек S размера |S| > 1 мы будем делить S воображаемой вер* тикальной прямой линией f на такие два набора Sl и Sr примерно оди* ненового размера, что все точки в Sl лежат слева от прямой линяя t, а все точки в S# справа от нее. Затем алгоритм рекурсивно применяется к обоим наборам, чтобы найти ближайшую пару в Sl и ближайшую пару в Sr. Может оказаться, что либо одна из этих пар является ближайшей парой для всего набора S, либо ближайшую пару будут образовывать разнесенные точки, одна из которых принадлежит Sl, а другая — Sr. Вов* пикшая ситуация анализируется с помощью операции слияния.
Пусть и будут означать расстояния ближайших пар в подндборах S/, и Sr соответственно, обозначим также 5 = min (5д, 6д). Если каждая ближайшая пара в S состоит из точек, разнесенных между Sl и Sr, тогда расстояние в каждой такой паре точек должно быть обязательно меньше, чем 5. Поэтому мы должны ограничиться рассмотрением только тех точек, которые лежат внутри вертикальной полосы шириной 26, расположенной симметрично вдоль разделяющей прямой £. Если какая-либо точка из Sl лежит вне этой полосы (а именно левее ее), то это означает, что она находится по крайней мере на расстоянии 5 от прямой линии I, т. е. не ближе этого расстояния от любой точки из Sr. Аналогично не потребуется рассматривать точки из Sr, также лежащие вне этой полосы (ряс. 8.9а).
Для обработки точек в пределах полосы мы будем хранить текущее значение расстояния ближайшей пары D, вначале установленное равным 5. Значение D будет изменяться каждый раз, когда внутри полосы будет обнаружена пара точек, расстояние между которыми точно меньше D. Инте-ресно, что совсем нет необходимости вычислять расстояние для каждой пары точек в полосе. Так как, если расстояние по вертикали между двумя точками превышает D, то и фактическое расстояние между ними будет больше D.	______
Переобозначим точки в полосе в порядке возрастания координаты у в виде ряда pi, Р2>—. Рт- Для каждой точки pi будем вычислять расстояние от текущей точки pi до каждой р/ для j e i 4-1, i 4- 2, . . ., Л пока не будет достигнут конец последовательности (т. е. k = m), либо координаты у ддя точек pi я рю будут отличаться на D или более. Т. е. будут анализироваться только те пары точек, для которых расстояние по вертикали меньше, чем D.

i i
8.6.1.	Программа верхнего уровня
Программе closest Points передается массив s из п точек. Она ^вращает расстояние для ближайшей пары точек из массива s, а также некоторую ближайшую пару через ссылочный параметр с:
double closestPoints (Point s[], int n, Edge &c) {
Point *‘x = new (Point*)[n];
Глава В
(а)	(б)
Рис. 8.9. (а) - Полоса шириной 28, где 8 = min (8i, 8/?). (6) - В полосу 28 х 8, могут попасть не более восьми точек из массива S (здесь две точки - одна из St, другая из 5к- могут совпадать в точке а, две другие могут совпадать с точкой Ь)
Point **у = new (Point*)[п]; for (int i = 0; i < n; i++) x[i] = y[i] = Ss[i];
mergeSort(x, n, leftToRightCmp); mergeSort(y, n, bottomToTopCmp); return cPoints(x, y, n, c);
)
Программа closest Points осуществляет предварительную сортировку точек в соответствии с их координатами по оси х для возможности разделения на поднаборы точек и в соответствии с их координатами по оси у для обработки точек в пределах вертикальной полосы. Сортировка по увеличению координаты у выполняется с использованием следующей функции сравнения:
int bottomToTopCmg(Point ‘a, Point *b) <
if ((a->y < b->y) || ((a—>y «= b->y) c& (a->x < b->x))) return -1;
else if ((a->y > b->y) ||((a->y = b->y) ti (a->x > b->x))) return 1;
return 0; )
В функ
ll'.w
ePoints передается массив x из
n точек, отсортированных по
координате х, и массив у тех же самых точек, отсортированных по координате у. Функция возвращает расстояние для ближайшей пары из п точек, а также через ссылочный параметр с возвращает некоторую ближайшую пару:
double ePoints (Point *х[] , Point *у[] , int n, Edge fic)
if (n == 1) return DBL_MAX;
«разделяй и властвуй»
Point **yL « new (Point*)[m];
Point **yR = new (Point*) [n-el • splitY (y, n, x(a], yL, yR) ;
Edge a, b;
double deltaL = cPoints(x, yL, a a) • double deltaR = ePoints(x+m, yR, n-a, b) delete yL;
delete yR;
double delta;
if (deltaL < deltaR) {
delta = deltaL;
c = a;
} else (
delta = deltaR;
c = b;
)
return checkstrip(y, n, x[m], delta, a);
Вертикальная разделительная прямая линия ( проходит через точку х[т], медиану набора точек относительно их координат по оси х. Функция splitY делит набор точек относительно вертикальной прямой линии ( в качестве подготовки к двум рекурсивным обращениям к функции ePoints.
8.6.2.	Обработка точек в пределах вертикальной полосы
Функция splitY делит массив у на два массива yL и yR. Точка р является делящей точкой: после выполнения всех операций массив yL будет содержать те точки из массива у, которые меньше р (иначе говоря, лежащие слева от р), отсортированные по их координатам у, а массив yR будет
содержать точки, которые больше или равны р, также отсортированные по
выполняет действия, обратные опера
координате у. Функция
слияния
I
при сортировке методом слияния:
void splitY(Point *у[], int n, Point *p, Point *yL[], Point *yR[J)
(
int i, lindx, rindx;
i = lindx = rindx = 0;
while (i < n)
if (*y[il < *P) yL[lindx++] =y[i++l;
else
yR[rindx++] = y[i++)»'
rheckStrip осуществляет проверку тех точек массива у, ко-Функция checkStrip> осущ	2*delta, симметричной относительно
торые лежат внутри полосы шириной	„ Если она находит,
вертикальной прямой линии, проходящей чер • е точками мель-что наилучшая пара анутри полосы ₽“с^Хе
ше, чем delta, то W “звршца-	ф„ в0за₽ашает
пару через ссылочный параметр с. р
только значение delta, без изменения с.
IM
Fami8
double oheckStrip(Point *y[)( int n, Point *p, double delta, Edge 6o)
(
int i, «tripion;
Point  new Point[nJ;
for (i  tripion • 0; i < n; !♦*) if ((p->x delta < y(i]->x) £6 (y[i)->x < p->x ♦ delta)) •(tripl«n++) • *y[i];
for (i  0; i < tripion; I**) for (int j  i+1; j < stripion; j++) ( if (a[j)>y s[i].y > delta) break;
if ((•[!]	•[}]).length() < delta) (
delta  (a[i)	 [j] )• length () ;
о - Edge(o[i], [j]); )
I delete a; return delta;
8.6.3a	Анализ
Мы здесь покажем, что функция ePoints выполняется за время О(п log л) при задании п точек. Поскольку этап предварительной сортировки программой closestPoints также занимает время О(п log д), то из этого следует, что программа целиком выполняется за время О(п log п).
Время работы Т(п) функции ePoints определяется известным рекуррентным соотношением Т(п) = 2Т (£) 4 ад, следовательно Т(д) е О(л log л). Выражение ад представляет собой время, необходимое для слияния результата, которое меньше, чем затраты на обращение к функциям splitY и checkstrip. Ясно, что функция splitY выполняется за линейное время.
Рассмотрим функцию checkstrip. Тело этой функции состоит из двух последовательных циклов for. Первый цикл for накапливает в массиве s точки, которые лежат внутри полосы, и очевидно, что он выполняется за линейное время О(л). Внешний второй цикл for выполняет одну итерацию для каждой точки в полосе, всего не более д итераций. А сколько времени тратится на выполнение внутреннего цикла for? Рассмотрим полосу в окрестности точки Si, а именно полоску размером 25 х 6, простирающуюся от вверх до 8i.y -4- 5 (рис. 8.96). Эта полоска делится вертикальной прямой линией F. на левый квадрат и правый квадрат Вд, каждый из них с размером 5x5. Поскольку никакие две точки слева от прямой линии не могут быть ближе друг к другу, чем 5, то квадрат BL может содержать не более четырех точек из массива S. Аналогично, квадрат Вд не может содержать более четырех точек из массива S. Следовательно, вся полоска может содержать не более восьми точек из массива S, одна из которых является самой точкой а/. Поскольку в пределах расстояния по вертикали, равном 5, над точкой з/ может находиться не более семи точек, число итераций во внутреннем цикле ограничено сверху константой. Из этого следует, что агорой внешний цикл for выполняется за линейное время, это же утверждение относится и ко всей функции checkStrip.
В общем алгоритм целиком выполняет осмо.шлп
^₽ихстГкХ~Тяе:₽х: ™и
чала вычисляется а “ Z	'£
□начел.,е о затем используется для ограничения eaepxyZjZl ™ ZmZ	”''₽Тпка-'ш'оП п₽”тоЯ линяя I. „я которых лотжио
вычисляться расстояние.	и «- ‘жми
8.7.	Триангуляция полигона
В этом разделе используем метод «разделяй и властвуй» для гриаягул*. ции произвольного полигона Р. Идея заключается в расщеплении полигона Р вдоль некоторой хорды и затем в рекурсивной триангуляции подучивших* ся двух подполигонов. В конце возникает ситуация, когда подложигций триангуляции полигон является треугольником. Алгоритм менее аффективен, чем способ, описанный в разделе 7.4, использующий расщепление полигона на монотонные части и их поочередной триангуляции, но его гораздо проще запрограммировать. Алгоритм основан па непосредственном доказательства следующей теоремы:
Теорема 7. (Теорема о триангуляции полигона): гт-угольннк может быть разбит но П - 8 треугольника проведением п - 3 хорд.
• I: I ’
Почувствовать справедливость этой теоремы можно путем выполнения триангуляции для некоторого придуманного полигона. Наиболее просто можно осуществить триангуляцию выпуклого полигона. Нужно выбрать одну из п вершин полигона и соединить ее с каждой из л - 3 Hecooeczzz вершин, получив л - 2 треугольников. Другой способ триангуляции выпуклого полигона основан на том факте, что каждая диагональ выпуклого полигона является его хордой: необходимо расщепить полигон вдоль любой диагонали и затем рекурсивно выполнить триангуляцию полученных двух
выпуклых полигонов.
верждение, что можно осуществить триангуляцию любого полигона, не так очевидна, если учитывать вогнутые полигоны. Мы будем доказывать теорему о триангуляции как для того, чтобы убедиться в справедливости теоремы, так и для мотивации разработки алгоритма триангуляции.
Доказательство теоремы ведется на основе индукции числа я, количества сторон полигона. Теорема тривиально справедлива для п “ 3, что является основой для индукция. Так что пусть Р будет полигон счислом^сторон п > 4 и предположим (в качестве гипотезы индукции), что условия теоремы удовлетворяются для всех полигонов с числом сторон меньше, чем л, Будем искать хорду полигона Р.	,	ла.
Пусть b будет некоторая выпуклая вершина и пусть а « ‘
седи. Могут возникнут‘ПЯ“Т°”УВ6^"‘Гьсторокш^'по^н, получающий-ис является хордой У	ТТп гипотезе квдукцни суще-
ся в результате отсечения^^“яГ^вТаТ’з ствует побор из л * хорд» пи р
*м
Глава 8
Рис. В.10. Две сигуации, рассматриваемые при доказательстве теоремы о триангуляции
itt;> iii;is
•ни
Добавляя к ним хорду ас и треугольник Да&с, мы получим набор из п - 2 хорд, которые делят исходный полигон Р на л - 2 треугольника.
Во втором случае диагональ ас не является хордой Р, полагая, что внутрь треугольника &abc должна попасть по крайней мере одна вер всех вер дальше всех от прямой лг вершиной. При таком выборе вершины d диагональ bd должна лежать внутри Р — ибо если бы какое-либо другое ребро полигона вторглось бы между b и d, то по крайней мере один из его ^оуцов должен находиться внутри треугольника Дяде на расстоянии от прямой са дальше, чем вершина d, что противоречило бы нашему выбору d. Следовательно, отрезок bd должен быть хордой. Теперь хорда bd делит полигон Р на два меньших полигона Р\ и Р2 с гц и n% сторонами соответственно. Поскольку гц + П2 = п + 2 и ni, П2 3, то каждое из п\ и П2 должно быть меньше п. Теперь мы можем применить гипотезу индукции к Pi и к Р2. Подсчет числа хорд приводит к (ni - 3) + (лг - 3) + 1 = п - 3 хордам, а подсчет треугольников — к общему числу - 2) + (лг - 2) = л - 2 треугольников. Теорема о триангуляции доказана.
ia полигона Р. Из
а внутри треугольнику Дяде пусть d будет вершиной, расположенной инии са . Эту вершину d будем называть вторгающейся
8.7.1.	Программа верхнего уровня
Вышеприведенное доказательство теоремы прямо подводит к следующей программе для триангуляции полигонов. Программе передается полигон р и она возвращает список треугольников, представляющих его триангуляцию. Треугольники накапливаются в списке triangles:
List<Polygon*> ^triangulate (Polygon &p) (
List<Polygon*> *triangles = new List<Polygon*>;
if (p.sizeO — 3) triangles->append(bp);
•Ise {
findConveXVertex(p);
Vertex *d  findlntrudingVertex(p);
if (d = HULL) ( /f нет вторгающихся вершин Vertex *c » p.neighbor(CLOCKWISE);
p.advance(COUNTER_CLOCKWISE);
Polygon *q « p.split(c);
triangles->append(triangulate(p)) ; triangles->append(q);
) else (	// d - вторгающаяся вершина
Polygon *q " p.split(d);
Алгоритмы типа «разделяй и властвуя»
119
triangles->append (triangulate («ой * triangle s->append (triangulate(р))' ’
)
return triangles;
8.7.2.	Поиск вторгающейся вершины
Используются функции findConvexVertex и findlntrudingVertex. При обработке полигона р функция findConvexVertex перемещает окно в полигоне р на некоторую выпуклую вершину. Это реализуется путем пере* мещения трех окон а, Ь и с, расположенных над тремя соседними верши-нами, в направлении по часовой стрелке вокруг полигона до тех пор, пока не будет обнаружен поворот направо в вершине Ь:
void findConvexVertex (Polygon Sp) (
Vertex *a = p.neighbor (COUNTERCLOCKWISE) ;
Vertex *b = p.v() ;
Vertex *c = p. neighbor (CLOCKWISE);
while (c->classify(*a, *b) != RIGHT) ( a = b;
b = p.aduance(CLOCKWISE) ;
c = p. neighbor(CLOCKWISE) ;
1
)
функцию findlntrudingVertex передается полигон з, для которого ’	________псппл/ппЛ. Фимкиня ВОЗ-
предполагается, что его текущая вершина является выпук''’°^Д>ДД^^Д вращает указатель на вторгающуюся вершину, если существу
NULL в противном случае.
Vertex *findIntrudingVertex(Polygon fip)
Vertex Vertex Vertex Vertex double
Edge ca(c->point()
♦	a = p. neighbor (COUNTER_CI^CKWISE) ;
*	b = p.v() ;	 _,гдд1 .
♦	c = p. advance (CWCKWT^J кжйдидат на давний момент
♦	d = NULL;	// лучшим	лучшего кандидата
bestD = -1.0;	// расстояние до лг»
a->point()>;
★	v = р • advance (CLOCKWISE) ,
while (v I» a) <	*o)) (
if (diet > bestD) (
)
i = p. advance (CLOCKWISE); )
P. setV (b) ;
return d;
Глава 8
Функция pointlnTriangle проверяет точки на попадание внутрь треугольника: функция возвращает значение TRUE, если точка р находится внутри треугольника с вершинами в точках а, b я с, в противном случае возвращаемое значение равно FALSE.
bool pointlnTriangle(Point p, Point a, Point b, Point c) (
return ((p. classify (a, b) !» LEFT) ££
(p.classify(b, c) !« LEFT) &£
(p.classify(c, a) lr LEFT));
)
Заметьте, что триангуляция, формируемая программой triangulate, зависит как от вида передаваемого ей полигона, так я от начальной текущей точки этого полигона. На рис. 8.11 представлены три различных результата триангуляции одного и того же полигона путем изменения его начальной текущей вершины. Эти результаты триангуляции полезно также сравнить с триангуляцией, показанной па рис. 7.13.
Рис. 8.11. Три различных результата триангуляции, формируемых программой t гiangulate
8.7.3.	Анализ
При обработке n-угольника каждая из функций findConvexVertex и findlntrudingVertex затрачивает время О(п). Если не найдено ни одной вторгающейся вершины, то одиночный треугольник удаляется из н-угольника и подвергается рекурсивной триангуляции результирующий л-1-угольник. В худшем случае вторгающаяся вершина никогда не обнаруживается в про-цессе выполнения программы и размер полигона уменьшается на единицу на каждом последующем шаге. Это приводит к времени работы в худшем случае Т(п) = п + (п - 1) + ... + 1, что равно О(п2). Такой самый плохой показатель получается при триангуляции выпуклого полигона.
*	Похоже, что программа будет работать значительно быстрее, если обна-
руживается вторгающаяся вершила, поскольку в этом случае задача разби-| вается на две подзадачи, вполне возможно, примерно равного размера. Поскольку только вогнутая вершина может вторгаться в треугольник, то i	программа работает быстрее, если полигон имеет много вогнутых вершин.
4	Однако не представляет труда составить такой полигон с С1(п) вогнутыми
вершинами, для триангуляции которого потребуется время £1(л2), хотя во-j гнутые вершины и существуют, но алгоритм их не обнаруживает или на-
ходит такие «плохие», что они проводят к разбиению на очень неравные части, одну большую, а другую маленькую.
8.8.	Замечания по главе
Оптимальный алгоритм для вычисления ядра полигона, выполняемый за время 0(л), разработали Ли и Препарата [53], его описание имеется в [67]. При обходе границ полигона этот алгоритм постепенно формирует Пересе* чение полуплоскостей, определяемых ребрами полигона. В нем используются преимущества, представляемые структурой полигона для достижения линейных характеристик.
Метод «разделяй п властвуй» может быть использован для формирования диаграммы Вороного для набора точек S за время О(п log л), где |S| - п. Идея заключается в разделении всего набора точек S воображаемой мртж-кальной прямой линией на два набора точек Sl и Sr, рекурсивного формирования диаграмм Вороного ВД£) к ЩМ с последующим ® яля обоазования V&Sr). Такое слияние занимает линейное время, что делает XXZ интер^м « эфф=к~ Ю. в®. «]• рации слияния диаграммы Борового представлю™ .
Lx, более сложной, чем простой список полигонов, применяемый
П₽ иХ”трХяПда^Ха Вороного. основанная
фии, приведена в [61], к0™ра* В р®^^Тпо методу «разделяй и вла-
Существует еще один	Р	планарного набора точек S,
ствуй» для формирования рьШ-'кл°“_ S снаЧала разбивается на два который работает следуют	проазвольиым образом
поднабора Si и S2, причем разбиение	примерно одинакового
за исключением того, что оба полна р_*' оболочки C#(Si) и СЛ(8г), ко-размера. Затем рекурсивно находятся вьшую	W(S) [67]. Поскольку
торые затем сливаются для обр*3°® общем случае пересекаются, шаг ели-выпуклые оболочки 4W(S1) и С*№) ’гяаю. Но поскольку этот яния значительно отличается от они ейвое время, полностью алгоритм вы шаг слияния также выполняется опяСанному в данной главе, полняется за время О(п log я), под й парЫ точек, представленное
Решение задачи поиска	задач» было
главе, впервые было °впм“° 5/де было помаав“\ 7°эХпгеши также d-мерного пространства в 11и-Ь давно числу точек, «ет р
решена за время О(п log л). гд	ятгоовтм триангуляции,
были приведены в [67].	кОТОром основавt	хороШей хорды
Доказательство теоремы. л1гнейвый способ п	поднгон
взято из [60]. Чазелле [18] опи	Дуется время О(л	иметь раэ-
(но на предварительную обра поЛучаюхляеся два П°тпнаягуляции будет вы-р расщепить такой хордой.	главе алгоритм<	вторгающейся вер-
мер не менее Описанный в это^ способ поиска подняться за время О(л log • nQ меТОду Чаз шины заменить на поиск хо
Mft
Глава 8
8.9.	Упражнения
При доказательстве теоремы о триангуляции полигона мы предполагали, что каждый полигон содержит по крайней мере несколько вы-
пукл
вершин. Докажите это.
2.	Покажите, что любой полигон может быть сформирован комбинацией соответствующим образом выбранных полуплоскостей путем выполнения операций пересечения и объединения.
3.	Определите новый класс для представления ограниченных и неограниченных двухмерных выпуклых политопов. Какие будут получены преимущества при представлении выпуклых политопов с использованием этого класса вместо класса Polygon, как в этой книге?
4.	Задача поиска самой удаленной пары формулируется следующим образом: для заданного набора точек S на плоскости найти некоторую пару точек такую, расстояние для которой больше или равно расстоянию между точками в любой другой паре. Покажите, что расстояние самой удаленной пары определяется двумя экстремальными точками (вершинами CW(S)).
5.	Для заданного планарного набора S из л точек разработайте алгоритм для решения задачи поиска самой удаленной пары в S за время О(п log л). (Подсказка: сначала постройте выпуклую оболочку CH(S). Затем создайте два окна, размещенных на противоположных вершинах набора S, и перемещайте их вокруг полигона до тех пор, пока не будут рассмотрены все противоположные пары. Две вершины выпуклого полигона будут противоположными, если через них можно провести параллельные касательные прямые линии, между которыми располагается полигон.)
6.	Создайте л-угольник, обладающий П(л) вогнутыми вершинами, для ко-
торого алгоритм триангуляции из раздела время П(л2).
8.7 будет выполняться за
7.	Предположим, что вторгающуюся вершину мы определим следующим образом: для заданной выпуклой вершины Ъ и ее соседних вершин а и с вершина d будет вторгающейся, если (1) вершина d лежит внутри треугольника babe и (2) вершина d находятся ближе к вершине Ь, чем все другие вершины, попавшие внутрь треугольника babe. Будет ли корректным алгоритм триангуляции при таком пересмотренном определении? Докажите Ваш ответ.
8.	Создайте алгоритм по методу «разделяй и властвуй» для поиска всех максимальных точек в заданном наборе за время О(п log л) (понятие максимальной точки для набора было определено в упражнении 11 раздела 7.6).
9.	Создайте алгоритм по методу «разделяй и властвуй* поиска всех точек пересечения для заданного набора п горизонтальных и вертикальных отрезков прямых линий на плоскости. Алгоритм должен выполняться за время О(п log л + г) , где г — число обнаруженных точек пересечения.
дуоритмы типа «разделяй и властвуй»
Ш
10.	Разработайте пошаговый алгоритм для вычисления ядра п-угольника за время О(п).
11.	Пусть задан набор точек 8 на плоскости.
а)	покажите, что если р G S и q — ближайшая к р точка в наборе S, то ИЖр) и ИТ?(<7) имеют общее ребро в диаграмме Вороного K2KS).
б)	для заданной K3(S) разработайте алгоритм решения задачи поиска ближайшей пары в S. Каким будет время выполнения Вашего алгоритма?
12.	Разработайте алгоритм сканирования плоскости для решения задачи поиска ближайшей пары. (Подсказка: когда сканирующая линия достигает точки р, определите, нет ли некоторой точки слева от сканирующей линии, которая вместе с точкой р образует ближайшую пару.)
Способы пространственного разделения
При реализации способа пространственного разделения выполняется де-композиция некоторого пространственного домена на более мелкие части. Этот способ известен также под названием расщепление, или просто деление. Он позволяет свести решение задачи, относящейся ко всему домену или геометрического объекта, лежащего внутри домена, к более простым или к меньшим подзадачам. Например, площадь полигона может быть легко вычислена путем суммирования площадей треугольников, полученных в результате триангуляция полигона.
Способы пространственного разделения очень широко варьируются по своей форме и структуре. Иерархическое деление получается очень легко путем рекурсивного разделения домена на меньшие и меньшие части и оно является очень мощным средством, поскольку обеспечивает реализацию рекурсивных способов решения задач. Хорошо известным примером одномерной задачи является двоичное дерево для набора вещественных чисел. Каждый уровень дерева соответствует разделению строки вещественных чисел: корневой уровень соответствует неразделенной строке вещественных чисел, а каждый следующий уровень — уточняет разделение соответствующего более высокого уровня. Самый нижний уровень дерева соответствует наименьшему делению из всех возможных (см. рис. 9.1).
В этой главе мы рассмотрим три способа иерархического разделения. Дня из них — квадрантное дерево и двухмерное дерево — будут применены для решения задач поиска на плоскости. Эти же задачи будут также исследованы с применением сетки — неиерархического деления домена на более мелкие кня/гряты.
6
------------------------1-------------
4	6	8
--------------1-------1-------1-----
2	4 5 6 7 8 9
-*------1-------1---1---1---1---1---г
Fwc. 9.1. Дю»мй€ дерем) поиска представляет иерзржкског разделение строки вещественны/' чисел
0вСобы пространственного разделения
 в заключение рассмотрим третий способ иерархически пвжж
^ЩЯЙСЯ 8 ТТ лво11чпого nPoeTpSXZa™u^ «» применяется при решении практической задив , XZT' °” ^e: для заданной статической сцены реанещения тр,уго^н™ХХ „ристве при перемещении точки наблюдения найти упомХХ, .X кость треугольников для каждого последовательного положекм точи, наблюдения. Получаемое при этом решение может быть использовано для генерации последовательности изображений почти в реальном времени для сцен различной сложности.
9.1,	Задача поиска по области
Для заданного набора точек иногда требуется определить, какие из них лежат в пределах заданной области. Например, может потребоваться узнать, какие города находятся между указанными значениями широты и долготы. Или мы можем запросить названия всех звезд, расположенных на заданном расстоянии от некоторой заданной звезды или точки в пространстве. Такие вопросы называются запросами по области.
Если точки принадлежат плоскости, а область определяется прямоугольником со сторонами, параллельными координатным осям, то запрос по области будет называться запросом в двухмерной области, в прямоугольник будем называть просто областью. На рис. 9.2 показан запрос по области на плоскости. Для выполнения запроса мы будем использовать тот факт, что точка лежит в указанной области, если я только если точка одновре-менно находится в заданном диапазоне области как по координате х, так П по координате у. На рис. 9.2 точки е. f, g и Л попадают в диапазон области по координате х, но из них только точки f и Л также находятся Диапазоне области и по координате у.
^9.2. Запрос по области на плоскости
л	™ функция pointlnRectangle
Стратегия весьма проста для реализац Х^очка р лежит в пределах °38Ращает значение TRUE, если и только
п₽я,14оуголъника R:
PointlnRectangle (Point 6р» Rectangle 6R)
R.ne.y))»
Запросы по области часто возникают в таких ситуациях, которые непосредственно не относятся к геометрическим задачам, но могут быть интерпретированы как таковые. В этих случаях в качестве «точек» выступают некоторые количественные параметры. Например, может потребоваться узнать имена всех звезд, яркости и температуры которых лежат в пределах некоторых диапазонов. Областью в этом случае будет прямоугольник со сто
ронами, параллельными координатным осям на плоскости в системе коор-
Mil:
ат яркость-температура. Системы баз данных поддерживают такие запро-
сы, в которых точки представлены в виде записей, а их компонентами
являются поля.
До сих пор имелись в виду запросы по области в виде «одноразовой» задачи: для заданных набора точек S и области R сообщить о точках из набора S, попадающих в область R. Однако во многих ситуациях бывает необходимо выполнить серию запросов по области для некоторого фиксированного набора точек S. В таких ситуациях в общем случае лучше сначала организовать точки в S в структуру данных, которая бы обеспечила выполнение последующих серий запросов по области, чем отвечать на каждый запрос независимо. Такая «многоразовая» версия известна как задача поиска области. Задача поиска в двухмерной области и определяется статическим набором S точек на плоскости. Проблема заключается в организации точек набора S в пространственной структуре данных, которая эффективно обеспечивает повторяющиеся запросы по области. Пространственные структуры данных обычно используются для выполнения серий операций и применяются для реализации геометрических абстрактных типов данных (АТД). Тогда стоимость их образования, которая может быть значительной, окупается при выполнении большого числа операций.
При самом упрощенном подходе к поиску области набор точек S мог бы быть организован в виде списка. Тогда для запроса по области R можно было бы проверять каждую точку в списке на попадание в область R. Это занимает время, пропорциональное |S|, т. е. размеру S, что оказывается неэффективным, когда число выбранных точек оказывается малым по сравнению с |S|. Проблема здесь в том, что не делается никаких попыток связать поиск с близостью к области и обрабатываются даже те точки, которые находятся далеко от заданной области.
В следующих трех разделах мы представим более эффективные решения, в которых используются пространственные структуры данных для локализации поиска. Для упрощения описания будем предполагать, что точки набора S лежат в положительном квадранте на плоскости, так что они могут быть заключены в квадратный домен левый нижний угол которого совпадает с началом координат. Следовательно будет достаточно рассматривать не всю плоскость, а только выделенный домен S), который может подвергнуться делению.
Хотя мы всегда будем говорить о задаче поиска области, но очень важно понимать, что исследуемые нами пространственные структуры данных могут быть использованы также для решения многих других задач. В упражнениях в конце главы обращается внимание на некоторые из них.
Способы Пространственного разделения
9.2.	Метод сетки
9.2.1.	Представление
Простейший способ деления квадратного домена заключается ж раобжв-нии его на сетку из малых квадратов, пли ячеек. Для подготовки сетки для поиска по области в наборе точек S «бросим* каждую точку ка набора S в соответствующую ячейку — для кпждой ячейки построим список точек, лежащих в ячейке (рис. 9.3). Заметим, для выполнения запроса по области Я будем рассматривать только те ячейки, которые пересекаются с Я и для каждой такой ячейки будем проверять входящие в нее точки на предмет включения в R.
Рис. 9.3, Сетка
Сетка из т X т ячеек
«Ха V которые зежат a =оот.етет»яоИе« ячейке:
class Grid (
private:	. дасяо „чаек к* сторону домепа
infc m;	иар сторокм ячейки
double cellSire;	'' ^,етка я х и элементе»
void^Grid^doubledomainSire. *°int
PUGiid(double domainsite, Point s[], ££ “• ^le M , 1.0); Grid (double domainSire, Point su* "Grid (void);	/Rectangle brange) ;
List<Point*> *rangeQ3Jery(R friend class Quadtree;
1;
9.2.2.	Конструкторы и деструкгорь
массив s из n 1
Первому конструктору переда^ разМер сетки _®; стороны квадратного домена
•	- г1. int •
•„q;Poxnt siJ Г
Grid:’Grid(double domainSiг ’
Главе 9
(
_Grid(domainSize, в, n); }
Фактическая работа конструктора выполняется собственной компонентной функцией _Grid:
void Grid::Grid(double donainSizo, Point h[], int n) (
cellsize > domainSize / m; g  new (Liot<Point*>**)[m]; for (int i = 0; i < m; i++) { g[i] a new (Liet<Point*>*)[m]; for (int j « 0; j < m; j++) g[i)(j) “ new Liet<Point*>;
I for (i = 0; i < n; i++) ( int a = int(a[i].x / cellsize); int b = int(s[i).у I cellsize); g[a][b]->append(new Point(в[i]));
) )
Сетка хранится в массиве g указателей на списки размером т X т. Поскольку g реализуется в виде массива массивов, то можно использовать стандартную нотацию. Так запись g[i] [ j ] указывает на список в строке i столбца j сетки. После инициализации нового списка для каждой ячейки сетки конструктор в цикле просматривает все п точек, добавляя каждую из них в соответствующий список.
Размер сетки т влияет на эффективность. Если т выбрано слишком большим, то ячейки сетки окажутся настолько малыми, что многие из них окажутся без точек. Если же т будет выбрано очень малым, то ячейки сетки получатся настолько большими, что они будут переполненными, содержащими неразумно большое количество точек. Один из путей регулирования размера сетки может заключаться в определении коэффициента заполняемости ячейки, вещественного числа М, устанавливающего среднее желаемое число точек на одну ячейку.
Если S содержит п точек, то будем иметь М = Чтобы выразить значение т в функции от М, перепишем это выражение в виде т - Г^д1- Это выражение приводит нас ко второму конструктору Grid, который по заданному коэффициенту заполняемости М вычисляет по этой формуле размер сетки:
Grid::Grid(double domainSize, Point в[], int n, double M) : m (int (coil (aqrt(n / M))))
(
_Grid(domainSize, a, n);
Деструктор -Grid удаляет все точки из сетки, списки, содержащие эти точки, и массивы, из которых была сформирована сама сетка:
Grid::-Grid(void)
Способы пространственного разделения
for (int 1 = 0; i < В; £♦+) ( for (int j =0; j < B. .
g[i] [j]->last() •
while (g[i] [j]->length() > 0) delete g[i] [j]->r«nove{);
delete g[i][j] ;
) delete g[i]
}
delete g;
9.2.3.	Выполнение запросов по области
Для выполнения запроса по заданной области R компонентная функция rangeQuery преобразует координаты углов области R d целочисленные индексы сетки, используемые для ограничения области пояска по строкам и столбцам сетки. Например, области протяженности области R по координата х составляет от столбца int (R.sw.x/cellSize) сетки до столбца int (R.sw.y/cellSize). Для каждой ячейки, которую будет обрабатывать функция rangeQuery, проверяется каждая точка в списке ячейки для включения в область R. Обнаруженные точки накапливаются в списке result:
List<Point*> *Grid: :rangeQuery (Rectangle 6R)
List<Point*> *result = new Liet<Point*>; int ilimit = int(R.ne.x / cellSixe); int ilimit = int(R.ne.y I cellsize); -	k	—
Liet<Point*> *pts = g[i][jj;
for (pts->firet (); !pta->i«HeadO; pts->naxt()) { Point *p = pte->val();
if (pointInRectangle(*p, R))
result->append (p) ;
I
return result; )
jjq fl точкам за время
9-2.4. Анализ
Конструктор строит сетку с размером т х т О(т2 + в самом худшем случае одиночный aai. »Ремя	не представляет сложное™
процедуру запроса по области, которая проверяет Ж» Ж без обнаружения хотя бы единственной точкя^^ эффективны в среднем:
Тем не менее, запросы по области гр выполвяется в среднем за время прос по области, определяющий • „тобы убедиться в этом, за-<г). когда точки распределены равн0* J; ‘ 1оиально ожидаемому числу мстим, что время работы в среднем Р еряемых точек. В каждой проверяемых ячеек плюс ожидаемое ч>
проверяемой ячейке в среднем обнаруживается небольшое постоянное число таких точек, которые включаются в список, следовательно ожидаемое число проверяемых ячеек равно О(г). В среднем отбирается только некоторая фиксированная часть точек в проверяемых ячейках, следовательно, ожидаемое число проверяемых точек также равно О(г).
Свойства сетки несколько хуже, если точки в наборе S распределены неравномерно. Если имеется слишком много пустых ячеек, то ожидаемое число проверяемых ячеек в процессе выполнения запроса по области теперь уже не будет ограничено сверху величиной О(г). Это происходит потому, что приходится проверять много пустых ячеек, а в результате нужные точки не будут обнаружены. И наоборот, если очень многие ячейки будут перегружены или заданный области будет узким и длинным, как бы вытянутым между точек в наборе S, тогда ожидаемое число проверяемых точек не будет ограничено сверху величиной О(г). Если ячейки содержат случайное большое число точек из набора S, то нельзя предположить, что в каждой проверяемой ячейке будет обнаружена фиксированное число точек.
9.3.	Квадрантное дерево
Сетки сравнительно неэффективны, если точки в наборе S распределены неравномерно — плотно в некоторых частях домена -Э п редко в других. В таких случаях никакой выбор размера сетки не гарантирует идеала, когда каждая ячейка содержит некоторое небольшое число точек из набора S. Деление с помощью квадрант кого дерева позволяет разрешить это затруднение путем изменения размера ячеек в соответствии с распределением точек» обеспечивая деление на мелкие ячейки там, где точки расположены плотно, и более крупные ячейки — в разреженных местах.
Квадрантное деление получается путем рекуррентного деления квадратного домена -Э на квадранты. Данный квадрант делится путем проведения двух секущих прямых линий, одна из которых вертикальная, другая — горизонтальная, пересекающихся в центре квадранта и таким образом получаются четыре одинаковых по размеру квадратных подквадранта. Так на рис. 9.4а домен 2? поделен на северо-восточный, юго-восточный, юго-западный и северо-западный подквадранты. Северо-восточный и юго-западный квадранты, в свою очередь, подвергнуты дальнейшему делению.
Гис. 9А (•) - деление Армена на авадрзяты. (б) - представление в виде гмдрамгного дерева
Способы пространственного разделемха
Квадрантное деление предстаппм п
торого каждый узел имеет четыре вепцГкХ”й	у “
дравтиого дерева ассоциируется с областью Жи) и L УМЯ " К“ что он накрывает область: корневой v», „„„	* умл ™*0₽«т.
целиком, а каждый некорвевой узел вакрыаа^ X	*
=т°=н^ коХГе.ВНУТРе,ПЯК ™ ’-«рХт^^ре™^ накрывают квадранта, не подлежХеМдХвеСХ°а“'““ю во. мы будем ссылаться на квадрант как на
внутреннее деление, и на внешний, если он больше во делятся.
На рис. 9.46 ветви, исходящие из каждого внутреовего ума квадрант. НОТО дерева, обозначены цифрами 0, 1, 2 и 3. относящимися к северо-вос-точному, юго-восточному, юго-западному и северо-западному подквадрантам соответственно. Обозначения ветвей могут быть использовады в качестве простои схемы для обозначения узлов в квадрантном дереве н, следовательно, соответствующих подквадрантов, получаемых в результате деления квадранта. Корневой узел обозначается меткой 0. Метка некорневого узла, который связан с ветвью, обозначенной меткой I, получается путем добаалпяяч i к метке своего родителя. Таким образом для получения метки любого у» л л мы начинаем с 0 и последовательно добавляем метку ветви вдоль уникального пути от корня вниз до нужного узла. Ня рис. 9.4а показаны метки некоторых
квадрантов.
Какова реальная глубина квадрантного дерева? Если снова обратиться к
точкам, то чем нам нужно руководствоваться при построении квадрантного
дерева в процессе деления квадрантов? Построение квадрантного дерева ос-
новано на идее адаптивного деления — квадранты подвергаются делению до тех пор, пока не будет выполнено некоторое условие. Для целей решения задач поиска по области вам необходимо контролировать количество
точек из набора S, попадающих в квадрант. В данном случае мы можем использовать такой критерий деления: квадрант подвергается дальнейшему делению только в том случае, если в нем содержится более М точек из набора S. Здесь целое число М, коэффициент заполняемости ячейки, задается как условие формирования квадрантного дерева.	me„»o
•р	__ гтлп’гппаяо лирик глубоки то стоимость поиски
Если квадрантное дерево построено очень у	я-прнстя мя.
по области будет значительной. К сожалению, наш р Р ,. ,.НАД. « отдельно" недостаточен для
разумной глуоиной^Ес. 1	го получаемоев результате
оказался произвольно глубоким, чтобы отделить глубины квадрантного дерева введем понятие которой квадрантное дерево расти не может.
ячейки М и предельная глубина D исполь-Я квадрантного дерева, удовлетворяющего внешнего узла п:
рева с парованы произвольно квадрантное дерево может все точки. Для ограничения предельной глубины D, ниже
Коэффициент заполняемости зуются совместно для получения бедующим условиям для каждого п лежит не глубже, чем D. Если глубина п определенно более, чем Л/ точек из S. Если п нс является корне1*1’ чем М точек из набора S.
2.
3,
меньше, чем Д тогда п накрывает не „ родитель узле « накрывает больше,
•J
•>* I
г I I t
*, ••
"»» 1
И”;
I I
I

I
о
5
I
I

р
I
I
9
’«

а
d

т
Глава 9
Совместно все эти свойства определяют уникальное квадрантное дерево для любого заданного набора точек S, содержащегося в домене ефор мально можно сказать, что деление квадрантов производится до тех пор. пока в них будет попадать не более, чем М точек, или не будет достигнута
Рис. 9.5. (а) - набор из 16 точек; (6) - квадрантное дерево для этих точек при М = 2 и D = 3
9.3.1.	Представление
Применим квадрантное дерево для решения задачи поиска по области. Определим класс Quadtree:
class Quadtree ( private:
QuadtreeNode ‘root;
Rectangle domain;
QuadtreeNode *buildQuadtree(Grid &G, int M, xnt D, int level,int, int, int, int); public:
Quadtree (Grid SG, int M, int D);
-Quadtree();
List<Point*> *rangeQuery(Rectangle firange); };
Узлы квадрантного дерева представляются в виде объектов класса QuadtreeNode:
class QuadtreeNode ( private:
QuadtreeNode *child[4];
List<₽oint*> *pts; // точки в S, если узел внешний // NULL, если узел внутренний int size;	// число точек в S, накрываемых узлом
Liat<₽oint*> *rangeQuery(Rectangle Grange, Rectangle ^quadrant);
Rectangle quadrant(Rectangles, int); int isExternal () ;
public:
QuadtreeNode(List<Point*>*);
QuadtreeNode (void) ,*
-QuadtreeNode(void);
friend class Quadtree;
Способы пространственного разделения
Если текущий узел квадрантного дерева DHPrTTn„ft
представляется в виде списка точек «Ln,.то цемент данных pts пример, точек, лежащих в квадранте. ипкпывя^аРЬ“П,0ТС? 9™М У"10* (яа’ ренний, то элемент данных child указывает ип У3*0*)’ Еслл У0®4 >нУт* a pts равен NULL. Заметим, что нет необходим^™*0"* четырсх потомков, внутренних узлов, поскольку эти же точки будут запп™ Хранеш,я точвк
ZX7 - ™й в~ -
9.3.2.	Конструкторы и деструкторы
Конструктору Quadtree передается сетка 6, коэффициент заполняемости ячеек М и предельная глубина D. Он формирует квадрантное дерево по точкам в сетке G, обеспечивая ограничения, налагаемые значениями И и D. В процессе работы точки удаляются из сетки G. Предполагается, что количество ячеек вдоль каждой стороны сетки G равно степени 2, т. е. G.m - 2* для некоторого целого числа k.
QuadtreeQuadtree (Grid SG, int M, int D) (
root = buildQuadtree (G, M, D, 0, 0, G.m-1, 0, G.n-1);
domain = Rectangle (Point (0,0) ,
Point (G.m*G.cellsize, G.m*G.callsize));
Компонентная функция buildQuadtree строит квадрантное дерево по сетке G снизу вверх, от уровня отдельной ячейки сетки до нулевого уровня, при котором домен 3) рассматривается как единая ячейка. На самом нижнем уровне — уровне fe, когда сетка имеет размер 2* X 2* каждая ячейка сетки представлена своим собственным одно-узловым квадрантным деревом. Узел содержит список тех точек из набора S, которые попадают в его ячейку. Для построения квадрантного дерева, корни которого лежат на уровне с, будем компоновать группы из четырех квадрантных деревьев, корня которых лежат на следующем более низком уровне I + 1. гда достигнем уров вя 0, самого верхнего уровня, то получим единое торое накрывает весь домен 3) рълимм. ажды и „ которые квадрантного дерева будет содержать список тех	•
он накрывает.	OWTr
Для заданных четырех квадрантных
п3 на уровне С + 1 нам ----------- . - ™>ялизчетсл
квадрантное дерево с корнем п на уровне одного из следующих двух действий.
•	Привязать мюврттти вереаья * ~
пз представляются в виде ПО*°”К°	; объедиНить точки из набора
•	Слить квадрантные деревья в * у в единый список точек и ас-s. накрываемые узлами по, пи пз • удалить узлы ло, ль «2 и лз-социировать этот список с узлом	предпринять? Цель
Как решить, какое из этих дну* д	удовлетворяющего трем ус-
заключается в построении квадрантного дерева,
10 Закал 2794
------: ло, Л1, ля и
«об^мо иайти способ объедавения их в единое ВСООЛОА. _____ веалиоуется о помощью
ПО. Ш. «2 И
Глава?
ловиям, упомянутым ранее. Для выполнения этого мы будем сливать квадрантные деревья по, «1. лз и пз в единый узел л, если (1) область, накрываемая узлом п будет содержать не более, чем М точек набора S, или (2) узел п лежит на уровне D или более. В противном случае узлы no, ni, П2 и пз связываются с узлом п.
Процесс формирования квадрантного дерева по сетке может быть реализован следующим образом. Начиная с самого мелкого возможного деления сетки G, когда каждая ячейка сетки представляется в виде отдельного квадранта, мы производим обработку, последовательно переходя с уровня на уровень, объединяя группы из четырех квадрантов, когда это необходимо, исходя из значений коэффициентов заполняемости ячеек М и предельной глубины D. Этот процесс показан на рис. 9.6. В самом начале процесса наиболее мелкие ячейки имеют уровень 4. Уровень 3 получается путем слияния всех групп из четырех квадрантов, поскольку предельный уровень задан равным 3 и все узлы на уровне 4 должны быть объединены. Остальные уровни (2, 1 и 0) получаются в результате слияния только тех групп из четырех квадрантов, которые после их объединения будут содержать не более М точек из набора S.
В компонентную функцию Quadtree::buildQuadtree передаются сетка G, коэффициент заполняемости ячейки М и предельная глубина D. Она формирует квадрантное дерево для подсетки G[imin. .imax, jmin. . jinax] и возвращает его корневой узел. Параметр level указывает уровень, на котором расположен этот корневой узел внутри основного квадрантного дерева верхнего уровня.
Рис. 9.6. Формирсваис «зддрашного дерева по сетке. Здесь М = 2 и 0 = 3
Способы пространственного разделения
QuadtreeNode «Quadtree: -.buildQuadtree(Grid MJ, int int level, int imin, int iaax, int jmin, ' if (imin = imax) ( QuadtreeNode *q  G.g[imin][jmin] = return g; ) else ( QuadtreeNode *p =
new new
QuadtreeHodB(G.g[imin][jmin]);
new
QuadtreeNode;
int iraid = (imin + imax) /2;
int jmid = (jmin + jmax) f 2;
p->child[0] =	// северо-восток
buildQuadtree (G, M, D, level+l, imid*l, imax, jald+l, jaax) ;
p->child[l] =	// юго-восток
buildguadtree (G,M,D, level+l,imid>l,imax, jmin, jmid);
p->child[2] =	// юго-запад
buildguadtree (G,M,D,level+l,imin,imid, jmin, jmid);
p->child[3] =	// северо-запад
buildQuadtree(G,M,D, level+1, imin, imid,jaid+l, jmax);
for (int i = 0; i < 4; i++)
p->sxze += p->child[i]->size;
if ((p->size <= M) || (level > = D)> ( //слияние потомков
p->pts = new List<Point*>;
p->pts->append (p->child[i] ->pts) ; delete p->child[i);
p->child[i] = NULL;
// завершение слияния потомков
return р;
общем случае функция buildQuadtree рекурсивно образует корень р
re-BW Я—
четырех потомков ври они удаляются. Заметим, что iimxn—imax), функция «изымает» списки точек из сетку пустой, но в таком состоянии, что она может быть
в L___......,—	----------в -	ом p. При необходимости mi-
для четырех потомков и связыва	_----------
полняется слияние ’ в базовом случае (imin= сетки G, оставляя i безопасно удалена.
Деструктор -Quadtree образуется тривиально:
Quadtree: :-Quadtree О (
delete root;
n adtreeNode	““
Первый конструктор Qu списка точек: них класса при передаче ему
QuadtreeNode: : QuadtreeNode (L }
pts( pts), size (_pts-?J-e у
I
(int i = 0; i < 4; i++) child[i] = NULL;
т
Глава 9
Конструктор QuadtreeNode, не имеющий аргументов, используется при формировании списка точек внутреннего узла внутри компонентной функции buildQuadtree:
QuadtreeNode: : QuadtreeNode (void) : pts(NULL), sire(O)
(
for (int i = 0; i < 4; i++) child[i] « NULL;
Деструктор -QuadtreeNode исключает список точек текущего узла, если текущий узел является внешним, или рекурсивно удаляет потомков текущего узла, если текущий узел внутренний:
QuadtreeNode::-QuadtreeNode() (
if (isExternal()) (
pta->laat() ;
while (pts->length() > 0) delete pta->remove();
delete pta;
} elae
for (int i = □; i < 4; i++) delete child[i];
// узел внешний
// узел внутренний
9.3.3.	Выполнение запросов по области
Для осуществления запроса по заданной области R будем выполнять запрос к корневому узлу квадрантного дерева:
List<Point*> *Quadtree:zrangeQuery(Rectangle &R) (
return root->rangeQuery(R, domain);
)
Для запроса по области R, начинающейся с узла п, необходимо сначала проверить, пересекается ли область R с областью Л(л). Если нет, то выполняется возврат. Если п — внешний узел, то каждая точка в его списке проверяется на предмет включения в Я. В противном случае (л — внутренний узел) запрос по области рекурсивно применяется к каждому из четырех потомков узла п, объединяя вместе все обнаруженные точки в единый список, который и возвращается:
List<Point*> «QuadtreeNode::rangeQuery(Rectangle fiR, Rectangle Cspan) (
List<Point*> «result = new List<Point*>;
if (Iintersect(R, span)) return result;
else if (isExternal()) Ц узел внешний for (pts->fir8t() ; !pts->isHead() ; pts->next()) (
Point *p » pts->val() ;
Способы пространственного разделения
if (pointlnRectangle (*р, R)) result->append (р) ;
I
for (int i = 0; i < 4;	{
Liat<Point*> *1 a
child [i]->rangeQuery( R, quadrant («pan, 1 J) .
result->append(1) ;	**“»*» I.
J
return result;
Параметр span определяет квадрант, накрываемый текущим узлом Вместо того, чтобы явно хранить квадрант каждого узла в виде элемента данных класса QuadtreeNode, будем вычислить квадранты «на ходу»: когда узел запрашивает каждого из своих потомков, потомку сообщается имя его подквадранта. Подквадранты вычисляются с помощью компонентной функ-дни quadrant, определяемого в следующем разделе.
9.3.4.	Обеспечивающие функции
Компонентная функция QuadtreeNode::quadrant используется во время выполнения запроса по области для вычисления квадранте, накрываемого каждым узлом. Функция возвращает квадрант, накрываемый потомком i текущего узла, где предполагается, что текущий узел накрывает квадрант s:
Rectangle QuadtreeNode:: quadrant (Rectangle int i) <
Point c = 0.5 * (s.sw + s.ne) ;
switch (i) ( case 0:
return Rectangle(c, s.ne);
return Rectangle (Point (c.xrs.sw«y) # Point (a. na.x, o.yllr case 2:
return Rectangle(s.sw, c);
return Rectangle (Point (s.sw.x, a.y), Point(c.x,o.na.y>) I I
_	«a fivffVT тя пересекаться два прямо-
Функция intersect определяет, не оудут	есди одновремешю
Угольника а и Ь. Они пересекаются толь а ^^^g оси х и у: •••• налагаются друг на друга их проекции	. «ч .
Ьо°1 intersect (Rectangle fi®< Rectangle fib)
return (overlappingExtent (az b, X). fi overlappingExtent (a, bz *)	'
рели левая концевая точка одного
Два интервала налагаются, если и то	функция overlappingr,xten
Интервала лежит в пределах другого инте
возвращает значение TRUE, если и только если проекции прямоугольников а и b налагаются по координате 1:
bool overlappingExtant (Rectangle &в, Rectangle fcb, int i) (
return ((a.ew[i] <«b.aw(i]) tt (b,sw(i] <“ a.ne[i])) II ((b.swfi] <• a.ew[i]) 6S (a.sw[i] <»b,ne[i)));
>
Из других обеспечивающих функций следует упомянуть функцию QuadtreeNode:: isExternal, которая возвращает значение TRUE, если текущий узел является внешним узлом:
int QuadtreeNode:'.isExternal() (
return (pts I= NULL);
)
9.3.5.	Анализ
Рассмотрим время работы конструктора Quadtree, которому передается сетка размером т х т. Время работы Т(т) определяется рекуррентным выражением
T(m) = [ 4r<f) + 0 еит т>1 b	если (т = 1)
»
для констант а и Ь. Нетрудно показать» что Т(т) G О(т\ Заметим, что Т(т) не зависит от размера набора точек S. Это происходит из-за того, что функция buildQuadtree занимает только постоянное время, не считая рекурсивных вызовов. Каждый раз, когда функция выполняет слияние четырех узлов в узел их общего предка, четыре списка точек сливаются за постоянное время с помощью функции List:: append. При каждом завершении рекурсивных обращений, функция <изымает» списки точек из сетки также за постоянное время.
Рассмотрим затраты на выполнение запроса по области. Запрос с областью R занимает время, пропорциональное числу проверяемых точек плюс число посещаемых узлов. В наихудшем случае будут проверяться все п точек набора S, но ни одна из них может не попасть в заданную область. Это может произойти, например, когда все точки из S случайно попадут в один и тот же внешний квадрант, а область Я пересекается с этим квадрантом, но не захватывает ни одной точки, расположенной в нем. Т. о. в худшем случае на запрос по области будет затрачено время О(л), что не лучше, чем груоый непосредственный поиск по области. Однако, использование квадрантных деревьев в среднем дает существенное преимущество перед непосредственным поиском. Мы здесь не будем анализировать ожидаемые характеристики при использовании квадрантных деревьев для поиска по области, поскольку эта задача сложна, а ответ очень сильно зависит от определения, что понимать под ожидаемым распределением точек ня плоскости.
Но все-таки стоит узнать, а сколько же узлов будет обработано в процессе выполнения запроса по области» При заданной области Л, каждый узел я
Способы пространственного рвтдслеиия
» квадрантном дереве может быть отвееев х оджЛ „ т™,	_
горче определяются соотношением »>»•.. о Z " тр” “тегорн». жо-
. Иесое^ненные у,лы характер,tXД	**>•
Здесь Жп) лежит полностью вне области ™W^™,ГС,' П П *<"> ~ »• . Окруженные узлы характеризуются выражением R п им • .
Здесь Щи) лежит полностью внутри области Я. * ’ * ДЛ)’ . Пересекающиеся узлы характеризуются выражением О С ЯП г
Здесь ^п) и R частично пересекаются.	и с « п Дп) С Дл).
Оказывается, что мы можем игнорировать несоедмнсшше и окруженные узлы. Процесс поиска не продолжается после песоедпвенных узлов: при до-стяжении несоединенного узла нет смысла обрабатывать его последователей С другой стороны, когда достигается окруженный узел, то это озиачает. что должны быть обработаны все его последователи. Это происходит потому, что каждая точка из набора S, принадлежащая последователю окруястшого узла по необходимости, должна лежать внутри области Я. Тем не менее, мы можем легко модифицировать нашу реализацию квадрантного дерева так, чтобы точки из набора S хранились не только во внешних узлах, но также и во внутренних: мы допустим, чтобы каждый уаел п — будь ок внутренним или внешним — мог обладать списком точек, которые лежат в области Л(л). Тогда при обнаружении окруженного узла в процессе вы* волнения запроса по области, его список точек выводится непосредственно, что делает ненужной обработку последователей этого узла.
Таким образом, мы можем предположить, что при поиске не обрабатываются последователи ни несоединенных узлов, ни окруженных узлов. Мы все-таки должны учитывать сами окруженные и несоединенные узлы п, но это значительно легче. Если узел п является корнем, то остальные узлы квадрантного дерева не обрабатываются. В противном случае, если узел п не корневой, тогда стоимость обработки этого узла может быть отнесена к стоимости обработки его предка, который должен быть пересекающимся узлом. Поскольку этот узел-предок имеет только четырех потомков, то таких отнесенных долей может быть не более четырех. Следовательно общее число узлов, обрабатываемых в процм.о.птгпых°пеоесе* запроса по области пропорционально общему числу о руж “К решить задачу подсчета числа	nt
рабатываемых в процессе выполнения	уала я опраде_
казаны возможные пять типов пересек	области J0fn).
ляется видом взаимного пересечения области Я я области т
взаимного пересечения области
МС»,<
R и области Л(л).
_	тип а	Тип 3
Тип 0	Тин 1
„	-г-> псостсчо101 обчюпей R и Дл>-
Рис 9.7 Гкп	1ИГНЬ
.ж"*
> «»<
। <
\н|
I
ЕС
‘1
Глава 9
Для пересекающегося узла п каждого типа (от 0 до 4) область R может
пересекать четыре подквадранта для Я(л) только неболь
не.
м числом спосо-
бов, показанных на рис. 9.8. Под каждой схемой указан тип узла, полу
чающегося в результате такого пересечения и лежащего на один уровень ниже. Я п штрихов а иные подквадранты охватываются областью, так что они соответствуют окруженным узлам. При каждом посещении узла типа I, обработка переходит на одного или нескольких его потомков. Но во всех случаях, по крайней мере два обрабатываемых потомка будут также типа i, а остальные относятся к типу, более высокому, чем i. Если узел имеет тип 4, то возможно только двух-вариантное ветвление. Тип узла соответст-
вует возможности для трех-вариантного или четырех-вариантного ветвления во время выполнения запроса по области. Когда происходит трех-вариантное
или четырех-вариантное ветвление, то возможность для последующего трехвариантного или четырех-вариантного ветвления уменьшается для всех, кроме, может быть, двух потомков. Поиск продолжается вниз подобно как
	
L-	
	
	
ТипЗ
Рис. 9.8. Способы, готогамч об*лсп, Р может педосеппь четъ<ж полиуидони а/я >л.дг,г<1 гмгн пп гмгчш’ го уме п (от О до 4)
□„кобы пространсгееяиого рамеленил
чество посещаемых узлов пропорционально 2°. если квадрантное дерево имеет глубину £>.
Это же можно показать более формально. Каждая строка ва рис. 9.8 приводит к возрастанию рекуррентного соотношения, характеризующего T^D) — число узлов, посещаемых в квадрантном дереве с глубиной D, корень которого имеет тип i. Например, будем иметь
Ta(D) = {
2Т3 (D - 1)
4Т4 (D - 1)
если в строке 3 обнаружен первый случай если в строке 3 обнаружен второй случай
Поскольку рекуррентное выражение для Т| содержит влемент Т/ для то эти рекуррентные выражения будем решать в порядке Ti, Тз,.»и То- Методом индукции можно показать, что
т4(В) < 2£ T3(D) S 2?
ад S
ГКО) s 2М
TotD) s 2
«||:Н
iUIHM'
Из этого следует, что независимо от типа корневого узла в процессе выполнения запроса по области посещаются по крайней мере 2^3 = 8(2D) G 0(2°) узла.
В свете нашего анализа, как бы следовало наилучшим образом выбирать допустимую глубину D. Мы хотим выбрать D достаточно большим, чтобы только несколько внешних узлов содержали больше, чем М точек, и достаточно малым, чтобы квадрантное дерево не было слишком глубоким.
При наиболее мелком делении квадрантное дерево с глубиной D содержит 4° внешних узла. Чтобы каждый внешний квадрант содержал в среднем М точек, потребуется квадрантов, т. е. 4° = ^.	г я
Решение относительно D приводит к получению выражения D = На основе этого значения D получим, что число посещаемых узлов при вьшол-нении запроса по области пропорционально 2 м, что приводит к 0(v—).
Мы здесь показали, что при выполнении запроса по области посещаются не более 0(^п) узлов при предположении, что для каждого узла как внутреннего, так и ’внешнего - хранится список точек, которые он накрывает, к сожалению, как мы видели, в худшем случае потребуется обработать Я (л) точек и это может произойти даже в том случае, если ни одна из точек не попадает в заданную область. Причина заключается в том, что деление с помощью квадрантного дерева является пространственным: положение и ориентация отсекающих линий зависит от пространственного домена Д а не । ингыция отсекн^ищ	посещаемых узлов в процессе выполне-
ОТ точек набора S Хотя кол.пество	ва овра6отку
»ия запроса по оолясти огр ,|П„11тельиые затраты — в них может ока-ЭТ..Х узлов могут	™’ вХ.“ а	из них только
аяться большое количество точек л оЩцМ разделе мы рассмотрим за-несколько, или вообще 11,1	‘ двухмерных деревьев, которые otfwm-
лачу поиска по области i	ппиентация отсекающих линий зависят как
ноарцгцти/ншины: положение Р оледовйГелъно, мы можем выбирать от набора точек -S’, так и от до	нпбора § совершенно обдуманно
ОТС1ЧОИ.ЧЦИГ линии для выделения	‘ узлов было недорогим — об-
и таким образом обеспечить, чтобы посещение
рабатываемым практически за постоянное время. В результате получим пространственную структуру данных, которая обеспечивает выполнение запроса по области в наихудшем случае за время O(r + VrT) , где г равно числу обнаруженных точек.
9.4.	Двухмерные деревья поиска
Двухмерное двоичное дерево поиска, или 2-d дерево, является двоичным деревом, в котором выполняется рекурсивное деление плоскости вертикальными и горизонтальными отсекающими линиями. Направление отсекающих линий попеременно меняется по мере продвижения вниз по дереву: четный уровень (начиная с корня) содержит вертикальные линии, нечетный уровень — горизонтальные линии деления. Для целей поиска по области в наборе точек S каждый узел соответствует точке в наборе S и отсекающей линии, проходящей через эту точку.
На рис. 9.9 показано разбиение плоскости на соответствующее 2-d дерево. Вертикальная отсекающая прямая линия, проходящая через точку а и соответствующая корню дерева, разделяет набор точек S на два поднабора Sl и Sr, располагающиеся слева и справа от вертикальной линии соответственно. Горизонтальная отсекающая линия через точку Ь, левого потомка узла а, делит набор Sl на два набора, располагающиеся сверху и снизу от этой отсекающей прямой. Горизонтальная прямая линия, проходящая через точку с, отражающую правый потомок узла а, аналогичным образом делит набор Sr. Остальные отсекающие прямые линии выполняют дальнейшее более мелкое деление. Заметьте, что каждая отсекающая линия разделяет остающиеся точки на два набора примерно равного размера.
Как и в случае с квадрантными деревьями, полезно рассмотреть области, ассоциированные с каждым узлом. В 2-d дереве корень охватывает весь домен 2>, а каждый некорневой узел накрывает часть области своего предка, лежащей с той или иной стороны отсекающей прямой линии, относящейся к предку. На рис. 9.96 узел а относится ко всему домену, а узел b накрывает полудомен, расположенный слева от вертикальной отсекающей прямой
Рис 9.9. (в)—асломс n/ocxociw отселяющими прямыми лилиями, Гб) — соответствующее 9 лерено
Способы пространственного разделения
линии, проходящей через точку а, тогда как vs.. я
X™	ЛеЖВТ П0Л ГО₽—» ~ яиРн”Т
На рис. 9.10 показано 2-d дерево для набора вх 200 нения приведено также
точек. Для срав-квадрантное дерево для этого же набора точек.
(б)
0)
•,ь
‘ §
Рис. 9.10, (а) - набор S из 200 точек; (6) - 2-d дерево для наборе точа S; (в) - квадрантное дерево для набора S
9.4.1. Представление
2-d дерево будем представлять в виде объектов класса TwoDTree.
. t
I» I
L-
class TwoDTree (
private:
TwoDTreeNode *root;	. r,
TwoDTreeNode *buildTwoDTree(Point *x[], Poxnt y[J, int n, int cutType);
I • Д S  •
c *
I
H>=';
public:
TwoDTree (Point р[] , int n) ;
^TwoDTree(void) ;	__ . .
List<Point*> *rangeQuery (Rectangle r g
объектов класса TwoDTreeNode: Узлы представляются в виде о
class TwoDTreeNode (
private:	. точкв| ассоциировавшая с узлом
Point ♦pnt;	сяава под линией оесечеяиж
TwoDTreeNode *lchxld, II	ияп линией отсечения
TwoDTreeNode *rchi1^ (Sctangle firange, int List<Point*> *rangeQuery (Re
отсечения cutType);
..q:
«Х.О.В*
!!>
t0 « j
TwoDTreeNode(Point*) .
TwoDTreeNode (void) ; friend class TwoDTree;
I
&
вертикальна, то узел.
rrfftttvn текущего узла
Если отеоклклцля пряма" м«™ ’cMld), накрывает область слева укааыпаемыК параметром 1С"‘	. Л11И1!В; если отсевающая прямая лпшя
(или еир,иш) от отсекающей прямой
>
4
8
„ 8
I
8
I
*
> •
„ 8
«
?
!
1 I I ?
‘ ' ъ
г
»
Глава 9
горизонтальна, то узел Ichild (или rchild) накрывает область ниже (или выше) отсекающей прямой. Элемент данных pnt является точкой текущего у ала.
9.4.2.	Конструкторы и деструкторы
Конструктор TwoDTree строит 2-d дерево по массиву р из п отдельных точек. Конструктор выполняет предварительную сортировку точек независимо по х-координатам и по ^-координатам, а затем вызывает компонентную функцию buildTwoDTree:
TwoDTree::TwoDTree(Point p[], int n) (
Point **x = new (Point*)[n];
Point **y = new (Point*)[n];
for (int i = 0; i < n; 1++)
x[i) = y[i] = new Point(p[ij);
mergeSort(x, n, leftToRightCmp);
mergeSort(y, n, bottomToTopCmp);
root = buildTwoDTree(x, y, n, VERTICAL);
)
Функции сравнения n leftToRightCmp и bottomToTopCmp выполняют упорядочение точек на плоскости по увеличению координаты х и координаты у соответственно. Первая из них была определена в разделе 4.3.6, а вторая — в разделе 8.6.1.
Для построения 2-d дерева компонентная функция buildTwoDTree (которая будет определена несколько позже) инициализирует корневой узел дерева и затем рекурсивно строит два поддерева для этого корня, по одному для каждой стороны отсекающей прямой линии, соответствующей корню. Если т = п/2, то корневой узел ассоциируется с точкой х[/п] и с вертикальной прямой линией, проходящей через эту точку. Поскольку массив х был предварительно отсортирован, то точка х[ш] будет средней точкой по координате х, а вертикальная отсекающая прямая линия через точку х[т] разделяет оставшиеся л — 1 точек набора S на левый и правый поднаборы примерно равного размера.
Перед рекурсивным формированием двух поднаборов корня функция buildTwoDTree разделяет оба массива х и у на два отсортированных подмассива, по одному для каждой стороны от вертикальной отсекающей прямой линии. Массив х делится на х[0. . т - 1] и х[т +1. . п - 1]. Деление массива у выполняется функцией splitPointSet, которая будет определена ниже. Имея два разделенных массива, функция строит левое (правое) поддерево для корня путем рекурсивного обращения к самой себе с отсортированными подмассивами х и у точек, расположенных слева (справа) от отсекающей прямой линии.
Для получения горизонтальной отсекающей линии на следующем более низком уровне взаимно меняется роль массивов х и у. В общем, по мере продвижения по дереву на следующий уровень, каждый раз меняются роли массивов х и у для получения вертикальной и горизонтальной отсекающих прямых линий.
В функцию buildTwoDTree передаются массивы х и у для п точек, отсортированных по координатам х и у соответственно. Параметр cutTypo указывает иа тип отсекающей прямой линии с помощью значений типа
Способы пространственного разделения
перечисления VERTICAL (вертикальная) или нойтупытлг /	_
фуккцпя возвращает указатель ва корень сформвроаавв^ТТГ^
enum ( VERTICAL = О, HORIZONTAL » 1) ;
TwoDTreaNode ‘TwoDTree i: buildTwoDTree (Faint •«[), Point *y[] int n, int cutType)
if (n == 0)
return NULL;
else if (n = 1)
return new TwoDTreeNode(x[OJ) ;
int m = n / 2;
int (*cmp) (Point*, Point*);
if (cutType = VERTICAL) cmp » leftToRightCap;
else cmp = bottomToTopCmp;
TwoDTreaNode *p = new TwoDTreeNode (x [m]) ;
Point **yL = new (Point*) (m) ;
Point **yR = new (Point*) [n-m];
splitPointSet(y, n, x[m], yL, yR, cmp);
p->lchild « buildTwoDTree (yL, x, a, 1-cutType);
p->rchild = buildTwoDTree (yR, x+a+1, n-a-1, 1-cutType) ;
delete yL;
delete yR;
return p;
)
Деструктор -TwoDTree тривиален:
TwoDTree::-TwoDTree() (
delete root;
)
Конструктор TwoDTreeNode инициализирует элементы данных для уела:
TwoDTreeNode: :TwoDTreeNode (Point *_pnt) : pnt (_pnt) , Ichild(NULL) , rchild(NULL) ( )
m	nowvncHBHO удаляет оба потомка текущего
Деструктор -TwoDTreeNode рекурсивно уд«^ узла, после чего удаляет точку текущего узла.
TwoDTreeNode: : -TwoDTreeNode ()
if (Ichild) delete Ichild; if (rchild) delete rchild; delete pnt;
)
9.4.3.	Выполнение запроса по области
запрос с областью Я, начинающейся о не-
Рассмотрим, как °сУщеС™"™ ДР веряМ| будет ли точка, связанная с которого узла 2-d дерева. Сн
узлом, лежать в области R— и, если да, то эта точка выводится. Затем, если часть области R лежит слева (или снизу) от вертикальной (горизонтальной) отсекающей прямой линии узла, то будем рекурсивно выполнять запрос из левого потомка узла. И симметрично, если часть области R расположена справа (выше) от вертикальной (горизонтальной) отсекающей прямой линии узла, то будем рекурсивно выполнять запрос из левого потомка узла. Цель заключается в ограничении обращения запроса к одной из сторон от отсекающей прямой линии узла, пока это возможно сделать — т. е. пока область R не перестанет пересекать отсекающую прямую линию.
На самом верхнем уровне запрос с областью R применяется к корню 2-d дерева:
List<Point*> *TwoDTree::rangeQuery(Rectangle £R) {
return root->rangeQuery(R, VERTICAL);
>
Функция TwoDTreeNode: : rangeQuery выполняет запрос с областью R, начиная с текущего узла. Предполагается, что отсекающая прямая линия через текущий узел будет иметь тип cutType, либо VERTICAL, либо HORIZONTAL. Функция возвращает список тех точек, которые находятся в области R:
List<Point*> ♦TwoDTreeNode::rangeQuery(Rectangle &R, int cutType)
{
Liat<Point*> *result = new List<₽oint*>;
if (pointlnRectangle(*pnt, R)) result->append(pnt) ;
int (*cmp)(Point*, Point*);
cmp = (cutType=VERTICAL) ? leftToRightCmp ; hottomToTopCmp; if (Ichild it ((*cmp) (fiR.sw, pnt) < 0>)
result->append(lchild->rangeQuery(R, 1-cutType));
if (rchild it ((*cmp)(&R.ne, pnt) > 0)) result->append(rchild->rangeQuery(R, 1-cutType)); re turn result;
}
Функция накапливает точки, которые она находит, в списке result. Обратите внимание, как в функции используются углы области для определения взаимного расположения области R и отсекающей прямой линии» Например. часть области R лежит слева (ниже) от вертикальной (горизонтальной) отсекающей прямой линии только в том случае, если юго-западный угол области Р меньше, чем pnt. Здесь смысл понятия меньше, чем зависит от ориентации отсекающей прямой линии и определяется функциями сравнения leftToRightCmp или hottomToTopCmp.
9.4.4,	Вспомогательные функции
Функция split Point Set используется для разделения точек отсекающей прямой линией. Предположим, что отсек а юш а я линия вертикальная. Если •jHb проходит через точку р, а массив у для точек отсортирован по увели-
I
Способы пространственного разделения
ченшо координаты у, то в	<
массива: массив yL содержит те тОяю1Р”,аЭТ*я,.™'ии'П" вуяут 00лУче»Ь‘ Ж» от р, отсортированные по координате у,“а ХТуГод^Х массива у, которые лежат справа от р. также
ч
void splitPointSet (Point *у(), int П/ Point * Point *yL[], Point *yR[), int (*cmp) (Point*, Point*))
int lindx = 0, rindx = 0;
for (int i = 0; i < n: i++) i
yR[ гindx++]
y[i], p) > 0) y[i]r
I
Функция выполняет действия, обратные операции слияния при сортировке сем и отличается от функции splitY из раздела 8.6 только двумя моментами: во-первых, точка р не включается ни в один из массивов yL и уй, даже если точка р присутствует в массиве у. Во-вторых, функция split Pointset разделяет точки в соответствии с передаваемой ей функцией сравнения стр. Мы будем передавать ей функцию сравнения leftToRightCmp для рааделе-ния точек с помощью вертикальной отсекающей прямой линия, проходящей через точку р, или функцию hottomToTopCmp для деления относительно горизонтальной линии, проходящей через точку р.
О . ;
I; !
j.:
• ‘и:
9.4.5. Анализ
J г ! о ♦* £1 > se?! ’ I
i:i-s н:?
Рекурсивная функция TwoDTree: :buildTwoDTree строит 2-d дерево по набору из п. точек за время ражением
Т(п), где T(n) определяется рекуррентным вы-
2Т (|) + ап если п > 1 если (л = 1)
а и Ь. Следовательно, Т(л) 6 0(л log л). По-предварительная сортировка, осуществляемая
также занимает время 0(п log я), то построение 2-d дерева вы-
Т(л) =
с подходящими константами скольку TwoDTree полняется за время 0(л log я). Запрос за время О(г + тех же самых । деле при анализе ХТТаГ^соодиненный. окруженный шш пересекакь быть классифицирован как неи м	_ ------ п »«-
щийся на основании его : показать, что число не1 ионии запроса по < езчцлемых окруженных узлов
по области, в результате которого выдаются г точек, выполняется а самом худшем случае. Эго можио показать с-помощью пвссужлеиий, которые были попользованы в предыдущем раз-₽ ХХпптиых деревьев. Каждый из узлов г^е^в..может „ак несоедпвенный, окруженный пли пересекаю-положения относительно данной области Я. Можно пересекающихся узлов, посещаемых " “Воп““
области ограничено сверху заачеяием O(Vn), а число ио-ограничено сверху значением 0(г).
1Й





9.5.	Удаление невидимых поверхностей: деление пространства
В этом разделе мы обратимся к задаче, тесно связанной с проблемой удаления невидимых поверхностей, рассматривавшейся п разделе 6,4. Нашей целью будет составить пространственную структуру данных по статичной коллекции треугольников в пространстве для реализации следующих операций: для заданной точки зрения р в пространстве вычислить упорядочение в порядке видимости треугольников относительно точки р. (Напомним из раздела 6.4 важность упорядочения в порядке видимости: если при таком упорядочении треугольник Р предшествует треугольнику Q, то треугольник Р не должен закрывать треугольник Q при его наблюдении из точки р — рисование треугольников согласно этому порядку приводит к получению изображения, видимого из точки р с удалением невидимых поверхностей.) Такое «многоплановое* решение проблемы удаления невидимых поверхностей весьма часто применяется в компьютерной графике, поскольку это очень хорошо подходит к случаю перемещения точки наблюдения относительно статичной сцены. Для каждого положения точки наблюдения структура данных обеспечивает генерацию изображения почти в реальном времени с удалением невидимых поверхностей.
Наша структура данных основана на двоичном делении пространства. Для визуализации того, что происходит, мы будем рассматривать двоичное
деление в пространстве, на одну размерность меньше, т. е. на плоскости. При этом общность рассматриваемых вопросов при концентрации внимания на плоскости не будет потеряна, поскольку анализируемая идея пригодна для пространства любой размерности. Конечно, когда мы будем рассматри
вать конкретную реализацию, мы вернемся к трехмерному пространству, так что полученные программы могут быть использованы в компьютерной графике.
Двухмерный аналог нашей задачи заключается в следующем: заданный набор отрезков прямых линий на плоскости должен быть организован в
виде структуры данных для вычисления упорядочения в порядке видимости отрезков прямых линий на плоскости относительно любой точки наблюдения р на плоскости. Вычерчивание отрезков прямых линий в порядке их
видимости предполагает их одномерную проекцию из
точки р с удаленными предполагаются непро-
11ми частями. Здесь отрезки прямых линий
неви.

зрачными, а видимость ограничена плоскостью.
Рассмотрим структуру данных, которая будет применяться. Двухмерное дерево двоичною пространственною деления, или дерево ДПД, является двоичным деревом, которое рекурсивно делит пространство путем проведения отсекающих прямых линий. Каждый узел дерева ДПД связан одновременно с отсекающей прямой линией и областью. Как и в случае с 2-d деревом, область. соответствующая корню, представляет всю плоскость, а каждая область некорневого узла — часть области своего предка с той или другой стороны отсекающей прямой линии. Однако, в отличие от 2-d дерева, отсекающие прямые линии в дереве ДПД имеют произвольные ориентацию и положение, На рис. 9.11 изображены двоичное деление пространства и соответ с гр у ющее дерево ДПД. Отметим, что отсекающие прямые линии слева от данной отсекающей прямой линии лежат в левом поддереве для /. в те. что находятся справа от t — в его правом поддереве.
Рис. 9.11. (а) - длоичное деление прсстрнк^, (б) - соопитовугшет лпхю АЛЛ
Теперь посмотрим, как нужно строить дерево ДПД для решения кашей задачи видимости на плоскости. Для заданного набора S отрезков прямых лилий сначала выберем из S произвольный отрезок прямой линии и ассоциируем его с корневым узлом дерева. Корневая отсекающая прямая лилия определяется зтпм отрезком прямой линии путем продолжения отрезка с обеих сторон. Эта прямая линия разделяет оставшиеся отрезки прямых линий ид две группы: Sl, расположенную слева от отсекающей прямой линии, и справа. Любой отрезок, пересекаемый отсекающей прямой линией, разделяется на две части и часть, лежащая слева от отсекающей прямой линии, добавляется к Si, а его другая часть — к Затем будем рекурсивно строить левое поддерево корпя по набору Sl и правое поддерево — по набору Sr. На рис. 9.126 отсекающая прямая линия по отрезку а делит отрезок прямой линии d на dj и da. Набор Sl содержит отрезки прямых линий Ъ, с и dj, в набор Sr— da, е я f. Левое и правое поддеревья узла а также будут деревьями ДПД, построенными по наборам Sl и Sr соответственно.
Для любой точки наблюдения р на плоскости мы используем модифицированный порядок обхода дерева ДПД. чтобы организовать отрезки прямых линий в порядке видимости. Рассмотрим, как обходить дерево ДПД на рис. 9.13, начиная с его корня. Поскольку точка р лежит справа от отреека прямой линии а, то ни один из отрезков прямых линий слева от а пе может закрывать а. они не могут также закрывать ни одного из отрезков прямых линий, лежащих справа от а. Следовательно, все отрезки прямых линий, лежащие слева от а, мы должны вычерчивать в первую скольку отрезок о не может закрывать какие-либо <^езки лежащвдеправа от него следующим будет вычерчен отрезок прямой лилии о. И, наконец. ОТ него, следующие	которые лежат справа от отрезка а.
вычертим все отрезки прямых	6ы слм„ „ оршюй
бы пришлось все вычерчивать в обратном порядке: сначала линий лежащие справа от отрезка а, затем сам отрезок линий, расположенные слева от а. Этот моди-—, обхода дерева ДПД, начинающийся в узле п, от-
ЦЛ-1ЦН ришшшл •» liVK******-*	'	М..
слеживается следующей псевдопрограммой.
Конечно, если бы точка линии с, го нам I все отрезки прямых а и потом отрезки прямых фицнрошшиый порядок •
removGHxddenLinos (node если р лежит справа
п, от
viewpoint р) отрезка узла п, то
Глава 9
Рис. 9.12. (а) - набор отрезков прямых линий S; (б)—двоичное деление пространства для набора S; (в) - соответствующее дерево ДПД
Рис. 9.13. (а) - двоичное деление пространства и точка наблюдения р; (б) - части отрезков прямых линий, видимых из точки р, выделены толстой линией; (в) — соответствующее дерево ДПД, в котором отмечен порядок обхода узлов
removeHiddenLines(leftchild(n), р) draw(n)
removeHiddenLines(rightchild(n), p) иначе
removeHiddenLines (rightchild(n), y) drax(n)
removeHiddenLines(leftchild(n) , p)
Заметим, что блок ♦иначе* выполняется в том случае, если точка наблюдения р коллинеарна с отрезком прямой линии а. Но и в этом случае было бы вполне корректно выполнять блок ♦если», поскольку отрезок а был бы виден со стороны его конца.
9.5.1.	Представление
После этих предварительных объяснений вернемся к задаче реализации деревьев ДПД. Будем анализировать задачу сортировки по видимости для статичной коллекции треугольников в пространстве и перемещения точки наблюдения. Расширение описанных способов на трехмерное пространство
Способы пространственного разделения
Дерево ДПД предствовя” Хсо'х ’в^Г" ~™-
class BspTree { private:
BspTreeNode *root;
BspTreeNods ♦buildBapTree (List<Triangle3D*>*) • public:
BspTree (Triangle3D *t[]r int n) ;
-BspTree(void);
List<Triangle3D*> *viaibilitySort(Point3D p) ;
Узлы представляются объектами такого класса:
class BspTreeNode {
private:
BspTreeNode *poschild;
BspTreeNode *negchild;
Triangle3D *tri;
BspTreeNode (Triangle3D*) ;
-BspTreeNode (void) ;
List<Triangle3D*> *visibilitySort (Point3D) ; friend class BspTree;
};
Треугольник текущего узла — указываемый элементом данных tri — делит все пространство на положительное и отрицательное полупространства, о чем говорилось в разделе 4.5. Элемент данных poschild указывает на потомок текущего узла в положительном полупространстве tri, a negchild на потомки в его отрицательном полупространстве.
9.5.2.	Конструкторы и деструкторы
Конструктор BspTree создает новое дерево ДПД по массиву t из п тре-угольников. Он просто преобразует t в список, который передается в функцию buildBspTree:
BspTree :: BspTree (Triangle3D *t[], intn)
' Liafc<Trlangla3D*> *trls = new Li5t<Triang1.30*>; for (int i = 0; i < n; i++)
tris->append(new Triangle3D(*t[ij>) .
root = buildBspTree(tris);
delete tris;
.	~ к.,; iHRqnTree формирует дерево ДПД по списку
Компонентная функция bu	- Р'	ф функция возвращает указатель
s треугольников, который ему передается, w на корневой узел дерева:
,	• 1 яввпТгее (bist<Triangle3D*> *s)
BspTreeNode *BspTree: :buildBspTreelьх
tn
Глава 9
if (8->length() == 0)
return NULL;
if (s->length() = 1)
return new BspTreeNode(8->firet()) ;
List<Triangle3D*> *sP « new List<Triangle3D*>;
Liet<Triangle3D*> *sN = new List<Triangle3D*>;
Triangle3D *p = 3->first();
for (a->next(); !a->isHead() ; s->next()) {
Triangle3D *q = s->val();
int cl[3) '
for (int i = 0; i < 3; i++)
cl[i) = (*q)[i].classify(*p);
if ((cl[0] != NEGATIVE) &&
(cl[l) != NEGATIVE) &C
(cl[2] != NEGATIVE)) s₽->append(q);
else if ((cl[0] ! = POSITIVE) &&
(cl[l) ! = POSITIVE) &&
(cl[2] != POSITIVE))
sN->append(q);
else
refineList(s, P) ;
)
BspTreeNode *n = new BspTreeNode(p);
n->poschild = buildBspTree(sP);
n->negchild = buildBspTree(sN);
delete sP;
delete sN;
return n;
}
В общем случае, когда список s содержит более одного треугольника, первый треугольник р в списке s запоминается в качестве корневого узла, а все остальные треугольники q добавляются либо в список sP, либо в список sN, в зависимости от того, лежит ли q в положительном, либо в отрицательном полупространстве от р. Если плоскость р пересекает треугольник q, то путем обращения к функции refineList осуществляется расщепление треугольника q плоскостью р и, в пределах списка s, треугольник q заменяется его частями. Функция refineList была определена в разделе 6.4.
Деструктор -BspTree удаляет корень дерева:
BspTree::~BspTree() (
delete root;
)
Конструктор BspTreeNode инициализирует элементы данных:
BspTreeNode::BspTreeNode(Triangle3D * tri)
tri( tel), poschild(NULL), negchild(NULL) <
)
Деструктор класса рекурсивно удаляет оба потомка текущего узла и затем удаляет его треугольник:
Способы пространственного разделения
BspTreeNode:: -BspTreeNode () {
if (poschild) delete poschild; if (negchild) delete negchild; delete tri;
9.5.3.	Выполнение удаления невидимой поверхности
В компонентную функцию BspTree: .-visibllitySort передается точка наблюдения и она возвращает упорядоченный по видимости список треутт». ников:
List<Triangle3D*> *BspTree: :visibilitySort(Foint3D р) (
return root->vi3ibilitySort(p) ;
I
Компонентная функция BspTreeNode:: visibilitysort реализует описанный ранее модифицированный порядок обхода для дерева ДПД, имеющего корень в текущем узле, и для точки наблюдения р. Функция возвращает упорядоченный по видимости список треугольников:
List<Triangle3D*> *BspTreeNode::visibilitysort (Foint3D p) (
List<Triangle3D*> *s = new List<Triangle3D*>;
if (p. classify (*tri) — POSITIVE) {
if (negchild) s->append(negchild->vi3ibilitySort(p)); s->append(tri);
if (poschild) s->append(poschild->visibilitySort(p));
if (poschild) s->append(poschild->visibilitySort(p)); s->append(tri);	,
if (negchild) s->append(negchild->vxsxbilitySort(p)), ) return s;
9.5.4.	Анализ
—— -•=	“:.s
время O(n2 log n). Ожи^®““чен₽е „о видимости вычисляется в среднем за предположении, что упорядоченп
время О(п log л).	—ттпплгяртся что п треугольников в простран-
Для трехмерного случая пРе’ ожидаемый размер дерева ДПД состав-етве взаимно не пересек»^Т°™’^“рования - 0(n* log я).
ляст О(л^), а ожидаемое врем
1*4
Глава 9
9.6.	Замечания по главе
Широкий области пространственных структур данных, а также огромное* число их вариантов и применений, описан Хапаном Саметом а [69, 70, 711. Обзор структур данных для решения задач поиска по области можно найти в [8]. Применение многомерных деревьев поиска исследовано в [7J.
Термин квадрантное дерево иногда используется в связи с любой пространственной структурой данных, основанной на рекурсивной декомпози ции пространства. Такие структуры данных могут варьироваться как в отношении типов данных, представленных в них, так и по принципу отслеживания декомпозиции. В свете такого широкого определения, квадрантные деревья применялись в течение длительного времени. Некоторые ил самых ранних применений включают удаление невидимых поверхностей [86J, статистический анализ [47], поиск по области [27], обработку изображений и определение свойств [42, 81]. В актантном дереве, трехмерном аналоге квадрантного дерева, куб рекурсивно делится ив восемь октантов. Октантные деревья используются в компьютерной графике для описания твердых тел в пространстве и ускорения их обработки [28].
В статьях [29, 30] рассматривается применение деревьев ДПД для удаления невидимых поверхностей при заданной статичной сцене и перемещении точки наблюдения. Нош вероятностный алгоритм формирования деревьев ДПД описан и проанализирован в [58, 63], в последней работе также поясняется, как улучшить характеристики деревьев ДПД в наихудших условиях за счет упрощения (методика становится детерминистической), Деревья ДПД применяются также для представления полиэдров в пространстве и манипуляций с ними [64, 85].
9.7.	Упражнения
1.	Примените актантное дерево, трехмерный аналог квадрантного дерева, для решения задач поиска в трехмерном пространстве.
2.	Примените 3d дерево, аналог 2d дерева в трехмерном пространстве для решения задачи поиска а трехмерном пространстве.
3.	В конструктор, который был определен г in квадрантного дерена, передается сетка и он строит квадрантное дерево снизу вверх. Определите новый конструктор, которому передается список точек нп плоскости и который будет строить квадрантное дерево для точек сверху вина. Квадрантное дерево должно обладать теми же тремя свойствами, которые были приведены в начале раздела 9.3. Каковы преимущества и недостатки построения Квадрантного дорсни сверху вниз?
4.	Модифицируйте нашу реализацию сетки так. чтобы ипициидизвния сетки размером т ? т по и точкам занимала время (Нт « ль
5.	Реализуйте конструктор СериН. подобный тому, что (нал определен г, этой главе! ио который ке разрушает передаваемую ему (В!*псу.
6-	Для поиска по области среди наборы точек Я им плоскости реализуйте такое квадрантное дерево. чтобы каждый его у:юл Пудг. то внешний
Способы пространственного разделении
8.
9.
10.
или внутренний - содержал список точек к, ч
Насколько больше памяти
Ш.К, с кпалр»,™^ в.	6шо (ч.‘
7’	™ ‘	— "о по-
I ухлои квадрантного дерева с глубмппЛ Л » предположении. что точки храШ1Тся как	"“°* f ’ "
внешних узлах.	утренних, так и во
Почему конструктор T*Wr,, для 2-d деревьев будет работать пли-нилысо, если на его вход будет подай список точек, содержащий дубликаты; Модифицируйте конструктор так, чтобы он работал и с такой ситуации. Будут ли работать наши реализации квадрантных деревьев и сеток с дубликатами точек?
Реализуйте версию запроса но области в 2-d деревьях, в которой проверяется, не будет ли узел окруженным, и если так, то исключается проверка его последователей для включения в область.
Задача поиска диска требует от нас организовать набор S точек на плоскости так, чтобы обеспечить поиск диска: для любой заданной точки р и положительного вещественного число г войти те ТОЧКИ КЗ набора S. расстояние которых до точки р не превышает г. Решите эту задачу с применением сеток, квадрантных деревьев и 2-d деревьев.
Квадрантное дерево можно использовать для представления планарной области Л следующим образом. Каждый внешний узел п содержит бит, показывающий, будет ли квадрант Л(п) полностью находиться внутри области R или он не соединен с Я. Область Я равна объединению всех ♦ включенных» квадрантов, которые содержатся в Я, а). Реализуйте такую схему.
б). Составьте функцию для классификации точки относительно представленной в виде кввдрантов области Я, т. е. нужно определить, будет ли данная точка р лежать внутри Я.
в). Напишите функцию для формирования объединения областей, представленных квадрантными деревьями: для двух заданных квадрантных деревьев, представляющих области А и В, необходимо построить третье квадрантное дерево, представляющее область A U В. г). Напишите функцию, которая при передаче ей двух квадрантных деревьев, представляющих области А и В, строят третье квадрантное дерево, представляющее их пересечение А Л В,
В нашей реализации деревьев ДПД каждая отсекающая плоскость определяется одним ИЗ треугольников в сцене. Примените версию дере-пм-1. ЛИД п которых отсскмоиие плоскости определяются по коко-пь в д.	1	п п чем заключается преимущество
хг»яд - ........
данных, обе	- доскости определить, попадает ли она
для андниио.	f предСтаВлекы два способа для выполнения
внутрь /*. В главе .> был р	попытойтесь сделать это же
подобное запроса зп время U{n). icnep
.чи время О(Ь»к rth
[1]	G. M. Adelson-Velskii and E. M. Landis. An algorithm for the organization of information, Soviet Mathematics Doklady, 3, 1259-1262 (1962).
[2]	A. V. Alio, J. E. Hopcroft, and J. D. Ullman, The Design and Analysis of Computer Algorithms. Addison-Wesley, Reading, MA, 1974. Русский перевод: Ахо А., Хопкрофт Дж. Построение и анализ вычислительных алгоритмов.— М.: Мир, 1979,
[3]	L. Ammeraal, Programming Principles in Computer Graphics, John Wiley and Sons, New York, 1992. Русский перевод: Аммерал Л. Принципы программирования в машинной графике.— М.: Сол Систем, 1992
[4]	F. Aurenhammer, Voronoi diagrams: A survey of a fundamental geometric data structure, ACM Computing Surveys, 23(3), 345-405 (1991).
[5]	D. Avis and В. K. Bhattacharya, Algorithms for computing d-dimen-sional Voronoi diagrams and their duals, in Advances in Computing Research, edited by F. P. Preparata, JAI Press, 159-180 (1983).
[6]	R. Bayer, Symmetric binary В-trees: Data structure and maintenance algorithms, Acta Informatlca, 1, 290-306 (1972).
[7]	J. L. Bentley, Multi-dimensional binary search tree used for associative searching, Communications of the ACM, 18(9), 509-517 (1975).
[8]	J. L. Bentley and J. H. Friedman, Data structures for range searching, ACM Computing Surveys, 11(4), 397-409 (1979).
[9]	J. L. Bentley and T. A. Ottmann, Algorithms for reporting and counting geometric intersections, IEEE Transactions on Computers, 28, 643-647 (1979).
[10]	J. L. Bentley and M. I. Shamos, Divide-and-conquer in multidimensional space. Proceedings of the Eighth ACM Annual Symposium on Theory of Computation, 220-230 (1976),
[11]	G. Booch, Object-Oriented Design with Applications, Benjamin Cummings, Redwood City, CA, 1990.
[12]	E. Brisson, Representing geometric structures in d dimensions: Topology' and order, Proceeding of the 5th Annual ACM Symposium on Computational Geometry, 218-227 (1989).
[13]	T. Budd, An Introduction to Object Oriented Programming. Addison-Wesley, Reeding, MA, 1991.
Литература
[14]
[15]
[16]
[17]
[18]
[19]
[20]
A.	Bykat, Convex hull of a finite
formation Processing Utter,. 7. 296-298 П9781 d"nen,ic"ul' >*
T^CaegiU, c++ Programming Slylt. Addi™.^,
fae^/phl T^йЙ to -UP-y of curved mm-nical Report UTEC-CSc-74-133 (1974) pl* f ComPuter Science, Tech-“Лв xrihm for ”nvra po,yiup”'
>" Pruceed-
IEEE Computer Society, 339-349 (1982).	°f ComP‘“f Science.
B.	Chazelle, Triangulating a simple polygon in linear time. Discrete and Computational Geometry. 6, 485-524 (1991).
T‘ H‘ ^T^eS’J3, Leiser9on’ and R- L. Rivest, Introduction to Algorithms. The MIT Press, Cambridge, MA. 1990.
[21]	M. Cyrus and J. Beck, Generalized two- and three-dimensional clipping Computers and Graphics. 3(1), 23-28 (1978),
[22]	L. Deneen and G. Shute, Polygonizations of point sets in the plane, Discrete and Computational Geometry, 3, 77-87 (1988).
[23]	S. C. Dewhurst and К. T. Stark, Programming in C++, Prentice Hall, Englewood Cliffs, NJ, 1989.
[24]	D. P. Dobkin and M. J. Laazlo, Primitives for the manipulation of three-dimensional subdivisions, Algorithmica, 4, 3-32 (1989).
[25]	W. Eddy, A new convex hull algorithm for planar seta, ACM Transactions on Mathematical Software, 3(4), 398-403 (1977).
[26]
[27]
[28]
[29]
[30]
[31]
[32]
[33]
M. A. Ellis and B. Stroustrup, The Annotated C++ Reference Manual, Addison-Wesley, Reading, MA, 1990.
R. A. Finkel and J. L. Bentley, Quad trees; A data structure for retrieval on composite keys, Acta Information, 4, 1-9 (1974).
J. D. Foley, A. van Dam, S. K. Feiner, and J. F. Hughes, Computer Graphics: Principles and Practice. Addison-Wesley, Reading, MA, 1990. H. Fuchs. G. D. Abram, and E. D. Grant, Near real-time shaded display of rigid objects. Computer Graphics. 17(3), 65-72 (1983).
H. Fuchs. Z. M. Kedem. and B. F. Naylor, On
by a priori tree structures. Computer Graphics. 14(3), 124-133 (1980).
M. R. Garey, D. S. Johnson, F. P. Preparata. and R. B. Тш-jan. Tn-। i.rcrnn infnrmation Processing Letters, 7(4), 175 angulnting a simple polygon, Injorn
180 (1978).	. ЛА_
л ।	p я Plexico. Data Abstraction and Ub-
jrcl/irh'rtln/1 Pniffnimmlng* In Cti. WUey. Chichester,
..... AU
of n finite plnnnr set, Informal!
Вычислительная геометрия и машинная графика на яэыие
[34]	Р. J. Green and В. W. Silverman, Constructing the convex hull of a finite planar set, Information Processing Letters. 22, 262—266 (1979).
[36]	L. J. Guibns and R. Sedgewick, A dichromatic framework for balanced trees, in Proceedings of the 19th Annual Symposium on Foundations of Computer Science., IEEE Computer Society, 8-21 (1978).
[36]	L. Guibas and J. Stolfi, Primitives for the manipulation of general subdivisions and the computation of Voronoi diagrams, ACM Transactions on Graphics, 4(2), 74-123 (1985).
[37]	R. H. Giiting, An optimal contour algorithm for isooriented rectangles, Journal of Algorithms, 5(3), 303-326 (1984).
[38]	D. Harel, Algorilhmics: The Spirit of Computing. Addison-Wesley, Reading, MA, 1992.
[39]	D. Hearn and M. P. Baker, Computer Graphics, Prentice Hall, Englewood Cliffs, NJ, 1994,
[40]	S. Hertel and K. Mehlhorn, Fast triangulation of simple polygons, Proceedings of the Conference on Foundations of Computing Theory, Sprin-ger-Verlag, New York, 207-218 (1983).
[41]	J. A. Hummel, Inlroduclion to Vector Functions, Addison-Wesley, Reading, MA, 1967.
[42]	G. M. Hunter and K. Steiglit?., Operations on images using quad trees, IEEE Transactions on Pattern Analysis and Machine Intelligence, 11(2), 125-153 (1979).
[43]	R. A. Jarvis, On the identification of the convex hull of a finite set of points in the plane, Information Processing Letters, 2, 18-21 (1973).
[44]	P. J. Kalin, Introduction to Linear Algebra, Harper & Row, New York, 1967,
[45]	M. Kallay, Convex hull algorithms in higher dimensions, unpublished manuscript, Dept, of Mathematics, Univ, of Oklahoma, Norman, Oklahoma, 1981,
[46]	D. G. Kirkpatrick and R. Seidel, The ultimate planar convex hull algorithm? Tech. Rep. 83-577, Dept, of Computer Science, Cornell University, 1983.
[47]	A. Klinger, Patterns and search statistics, in Optimizing Methods in Sta tistics, edited by J. S. Rustagi, Academic Press, New York, 303-337 (1971).
[48]	D. E. Knuth, Fundamental Algorithms, Vol. 1 of The Art of Computer Programming. 2nd edition, Addison-Wesley, Reading, MA. 1973. Русский перевод: Кнут Д. Искусство программирования для ЭВМ. Т. 1. Основные алгоритмы.— М.: Мир, 1976.
[49]	D. Е. Knuth, Big omicron and big omega and big theta, ACM SfGACT News, 8(2), 18-23 (1976).
[50]	J. M. Lane, L. C. Carpenter, T. Whitted, and J. F, Blinn, Scan line methods for displaying parametrically defined surfaces, Communications of the ACM. 23. 23-34 (1980).

Литература
[51J
[52]
[531
[54]
[55]
.nd iu «Рр11«йоп»₽^м,и°r°m^n6. 'XSX’ ж %	x ?r * *•
’W». ~ '(г98оГ -D. T. Lee and B. Sehnchlor, Tw algorithm» for constructing Delaunay en“ 9(3) ^’в-Ш uSeZ' ''"n'”‘lm “"d '"'0™а“»п Sc'
[56]
U. Manber, Introduction to Alcorithmx: A c Wesley, Reading, MA, 1989.
rcatiw Approach, Addiaon-
[57]	S. B. Maurer and A. Ralston, Discrete Algorithmic Mathematics, Addison-Wesley, Reading. MA. 1991.
[58]	K. Mulmuley, Computational Geometry: An Introduction Through Randomized Algorithms, Prentice Hall, Englewood Cliffs, NJ, 1994.
[59]	M. E. Newell. R. G. Newell, and T. L. Sandia, A solution to the hidden surface problem, in Proceedings of the ACM National Conference 1972, 443-450 (1972).
[60]	J. O’Rourke, Art Gallery Theorems and Algorithms, Oxford University Press, 1987,
[61]	J. O’Rourke, Computational Geometry in C, Cambridge Univerelty Press, 1994.
[62]
[63]
[64]
[65]
J. O’Rourke, C.-B Chien, T. Olson, and D. Naddor, A new linear algorithm for intersecting convex polygons, Computer Graphics and Image Processing, 19, 384-391 (1982).
M. S. Paterson and F. F. Yao, Efficient binary span partitions for hidden surface removal and solid modelling, Discrete and Computational Geometry, 5, 485-503 (1990).
D Peterson Halfspace representations of extrusions, solids of revolution, and pyramids, SAN-DIA Report 84-0572, Sandia National Laboratory, 1984.
F. P. Preparala and S. J. Hong. Convex hulls of finite eels of pointe in two and three dimensions. Communications of the ACM. 2(20). 87 93
[66]
[67]
[68]
(1977).	л
F. P. Preparala and M. I. Shamos. Computational. Gmnet™*“'n‘™ duello,,. Springer-Verlag. New York. 1985. Русский перевод: Препарата ф„ Шоймос М. Вычислительная геометрия: Введение,- М.. Мир. 1989.
F. Р. Preporata and К. J. Supowit. Teeting a staple ₽JW»" mon°-tonicily. Information Processing Letters. 12(4), 161-164
D. F. Rogers. Procedural Element, for Computer Graphs. McGraw-Hill. New York, New York, 1985.
300
Вычислительная геометрия и машинная графика на языке Си++
[69]	Н. Samet, The quadtree and related hierarchical data structures, ACM Computing Surveys, 16(2), 187-260 (1984).
[70]	H. Samet, Applications of Spatial Data Structures, Addison-Wesley, MA, 1990.
[71]	H. Samet, Design and Analysis of Spatial Data Structures, Addison-Wesley, MA, 1990.
[72]	B. Schaudt and R. L. Drysdale, Multiplicatively weighted crystal growth Voronoi diagrams, Proceedings of the 7th Annual ACM Symposium on Computational Geometry, 214-223 (1991).
[73]	R. Sedgewick, Algorithms in C++, Addison-Wesley, Reading, MA, 1992.
[74]	M. I. Shamos and D. Hoey, Closest-point problems, Sixteenth Annual IEEE Symposium on Foundations of Computer Science, 151-162 (1975).
[75]	M. I. Shamos and D. Hoey, Geometric intersection problems, Seventeenth Annual IEEE Symposium on Foundations of Computer Science, 208-215 (1976).
[76]	J. S. Shapiro, A C++ Toolkit, Prentice Hall, Englewood Cliffs, New Jersey, 1991.
[77]	R. F. Sproull and I. E. Sutherland, A clipping divider, AFIPS Fall Joint Computer Conference, Thompson Books, Washington, DC, 765-775 (1968).
[78]	T. A. Standish, Data Structure Techniques, Addison-Wesley, Reading, MA, 1980.
[79]	B. Stroustrup, The C++ Programming Language, Addison-Wesley, Reading, MA, 1991.
[80]	I. E. Sutherland and G. W. Hodgman, Reentrant polygon clipping, Communications of the ACM, 17(1), 32-42 (1974).
[81]	S. Tanimoto and T. Pavlidis, A hierarchical data structure for picture processing, Computer Graphics and Image Processing, 4(2), 104-119 (1975).
[82]	R. E. Tarjan, Amortized computational complexity, SIAM Journal on Algebraic and Discrete Methods, 6(2), 306-318 (1985).
[83]	R. E. Tarjan, Data Structures and JVetwork Algorithms, Volume 44 of BMS-NSF Regional Conference Series in Applied Mathematics, Society for Industrial and Applied Mathematics, Philadelphia, PA, 1983.
[84]	R. E. Tarjan and C. J. van Wyk, An O(n log log n)-time algorithm for triangulating a simple polygon, SIAM Journal of Computing, 17(1), 143-178 (1988),
[85]	W. C. Thibult and B. F. Naylor, Set operations on polyhedra using binary space partitioning trees, Computer Graphics, 8(7), 153-162.
[86]	J. Warnock, A hidden-surface algorithm for computer generated halftone pictures, University of Utah Computer Science Dept. Technical Report TR 4-15, NTIS AD 753 671 (1968).
Литература
Я
[87]	G. S. Watkins, A real-time visible surface algorithm, University of Utah Computer Science Dept. Technical Report TR UTEC-CSc-70-101 NTIS AD 762 004 (1970).	NTIS
[88]
[89]
[90]
K. Weiler and P. Atherton, Hidden surface removal using polygon area sorting, Computer Graphics, 11, 214-222 (1977).
M. A. Weiss, Data Structures and Algorithm Analysis, Benjamin Cummings, Redwood City, CA, 1992.
D. Wood, Data Structures, Algorithms, and Performance, Addison* Wesley, Reading, MA, 1993.