Текст
                    М. С. Долинский
Решение сложных
и олимпиадных зедеч
Москва • Санкт-Петербург - Нижний Новгород • Воронеж
Ростов-на-Дону • Екатеринбург • Самара • Новосибирск
Киев • Харьков • Минск
2006


ББК32.973-018я7 УДК 004.42@75) Д64 Долинский М. С. Д64 Решение сложных и олимпиадных задач по программированию: Учебное пособие. — СПб.: Питер, 2006. — 366 с: ил. ISBN W69^0794^ В книге рассматриваются решения оригинальных задач международных и национальных олимпиад по информатике и программированию для школьников и студентов. Задачи сгруппированы по темам: максимальный поток, минимальное остовное дерево, деревья, скрытые графы, стратегические игры, табло Янга. В начале каждой главы лаконично, но доступно излагается необходимый теоретический материал по теме, затем для каждой задачи приводятся условие, идея решения и описание конкретной реализации на языке программирования Паскаль. Для школьников, студентов и их преподавателей. ББК32.973418я7 УДК004.42@75) Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. ISBN 5~469407944 © ЗАО Издательский дом «Питер», 2006
Краткое содержание Введение 8 Глава 1. Максимальный поток 12 Глава 2. Минимальное остовное дерево 45 Глава 3. Решение задач на деревьях и с помощью деревьев 76 Глава 4. Скрытые графы 185 Глава 5. Стратегические игры 296 Глава 6. Диаграмма Юнга 333 Литература 363 Алфавитный указатель 364
Содержание Введение 8 От издательства 11 Глава 1. Максимальный поток 12 1.1. Примеры задач на максимальный поток 12 1.2. Формальная постановка задачи 14 1.3. Задача «Новогодняя вечеринка» 16 1.4. Задача «Кубики» 18 1.5. Задача «Игра» 21 1.6. Пример максимального потока на графе 25 1.7. Алгоритм Форда-Фалкерсона 28 1.8. Решения задач 33 1.9. Замечания по реализации 44 Глава 2. Минимальное остовное дерево 45 2.1. Формальная постановка задачи 45 2.2. Алгоритм Прима 47 2.3. Алгоритм Крускала 51 2.4. Быстрая сортировка 54 2.5. Задача «Secret Pipes» 55 2.6. Задача «Метро» 59 2.7. Задача «Network» 61 2.8. Решения задач 63
Содержание Глава 3. Решение задач надеревьях и с помощью деревьев 76 3.1. Практические примеры деревьев 78 3.1.1. Деревья отношений 78 3.1.2. Деревья попиксельного представления плоских цветных образов 78 3.1.3. Деревья представления сложных композиций трехмерных объектов 79 3.1.4. Деревья кодирования символов 79 3.1.5. Деревья сортировки 81 3.1.6. Деревья сумм 82 3.1.7. Перечисление деревьев 82 3.1.8. Представление деревьев в памяти компьютера 83 3.1.9. Порядок обхода деревьев 84 3.1.10. Организация материала и технология работы с ним 84 3.2. Задачи на основные свойства деревьев 84 3.2.1. Задача «Is it a tree?» 85 3.2.2. Задача «Strategic game» 89 3.2.3. Задача «Оппозиция» 92 3.2.4. Задача «Erdos Numbers» 94 3.2.5. Задача «Closest Common Ancestor» 96 3.3. Задачи на представление образов 98 3.3.1. Задача «Unscrambling Images» 99 3.3.2. Задача «BSP Trees» 106 3.4. Задачи на двоичные деревья сортировки 110 3.4.1. Задача «Дерево» 110 3.4.2. Задача «Parliament» 113 3.4.3. Задача «Falling Leaves» 117 3.5. Кодирование последовательностей символов методом Хаффмана 120 3.5.1. Задача «Кодирование» 120 3.5.2. Задача «Entropy» 124
6 Содержание 3.6. Перечисление деревьев 126 3.6.1. Задача «Nextree» 126 3.6.2. Задача «Trees Made to Order» 132 3.7. Битово-индексированные деревья 137 3.7.1. Задача «Мобильные телефоны» 137 3.7.2. Структура данных BIT 141 3.8. Задачи для самостоятельного решения 145 3.8.1. Задача «Knockout Tournament» 145 3.8.2. Задача «Split Windows» 147 3.8.3. Задача «Huffman Trees» 149 3.8.4. Задача «Pre-Post-erous!» 151 3.9. Решения задач 153 Глава 4. Скрытые графы 185 4.1. Инцидентность областей 186 4.1.1. Задача «Тетраэдр» 186 4.1.2. Задача «Стены» 192 4.1.3. Задача «Блокада» 198 4.1.4. Задача «Мудрый правитель» 203 4.1.5. Задача «Ременная передача» 207 4.2. Отношения других видов 211 4.2.1. Задача «Currency Exchange» 211 4.2.2. Задача «Exchange Rates» 215 4.2.3. Задача «Sorting It All Out» 220 4.2.4. Задача «Проверка веб-страниц» 225 4.2.5. Задача «Play On Words» 230 4.3. Задачи на множествах отрезков 234 4.3.1. Задача «Падение» 235 4.3.2. Задача «The Doors» 241 4.3.3. Задача «Борозды» 246 4.3.4. Задача «N-Credible Mazes» 250 4.4. Задачи для самостоятельного решения 254 4.4.1. Задача «Door Man» 254 4.4.2. Задача «This Sentence is false» 255
Содержание 4.4.3. Задача «Will Indiana Jones Get There?» 256 4.4.4. Задача «I hate SPAM, but some people love it» 257 4.5. Решения задач 260 Глава 5. Стратегические игры 296 5.1. Задача «Алиса и Боб» 296 5.2. Задача «Ладья и конь» 300 5.3. Задача «Нечестная игра» 305 5.4. Как играть победно? 308 5.5. Задача «Игра loiwari» 308 5.6. Задача «Игра Score» 314 5.7. Задача «Игра-2» 319 5.8. Решения задач 321 Глава 6. Диаграмма Юнга 333 6.1. Введение в диаграмму Юнга 333 6.2. Вставка и удаление элементов диаграммы 333 6.3. Количество возможныхдиаграмм заданной формы (n1, n2 nM) 335 6.4. Задача «Склад» 336 6.5. Задача «Twofive» 341 6.6. Решения задач 353 Литература 363 Алфавитный указатель 364
Введение В настоящее время «олимпиадное программирование» переживает настоящий бум среди студентов и школьников. Тому есть множество причин. Перечислим наи- наиболее важные из них. 1. Программирование — чрезвычайно увлекательная сфера деятельности, в то же время обеспечивающая достойный уровень оплаты труда во всем мире, в том числе и в странах СНГ. Последнее связано со стремительной компьютериза- компьютеризацией всех сфер жизнедеятельности человека и открытием крупнейшими фир- фирмами (Intel, IBM, Motorola и т. д.) центров исследований и коммерческих раз- разработок в области программного обеспечения на территории России, Белару- Беларуси и других стран СНГ. 2. При изучении программирования и других компьютерных наук молодой че- человек получает «конвертируемые» знания, позволяющие ему работать не толь- только на родине, но и практически в любой стране мира. 3. Олимпиады по программированию для студентов и школьников имеют более чем двадцатилетнюю историю. При этом высокие результаты предыдущих поколений студентов и школьников стимулируют занятие олимпиадным про- программированием все новых и новых молодых людей. Приведем только несколько цифр. С 1997 года Россия была представлена на — Международной олимпиаде школьников по информатике (International Olym- Olympiad in Informatics, IOI) тридцатью двумя школьниками (восемь олимпиад, по 4 школьника в команде на каждой олимпиаде). Все эти школьники завоевали медали, причем подавляющее большинство — золотые или серебряные. Ана- Аналогичный результат имеет только одна страна в мире — Китай. С 1996 года вузы России и других стран СНГ начали принимать участие в команд- командном студенческом первенстве мира по программированию, проводимом под пат- патронажем ACM (Association for Computing Machinery) — одной из крупнейших и старейших ассоциаций компьютерных профессионалов. Результаты и здесь вы- выглядят впечатляюще. В течение последних шести лет команды Санкт-Петербург- Санкт-Петербургских вузов (СПбГУ и СПбГУТМО) трижды становились чемпионами мира по программированию. При этом в финале 2005 года второе, третье и девятое места
Введение 9 (а в финале 2004 года — первое, четвертое и восьмое) были заняты командами рос- российских вузов. Такими стабильно высокими результатами своих студентов не может похвастаться ни одна страна в мире. Понятно, что эти успехи достигнуты благодаря педагогическим кадрам Санкт- Петербурга, Москвы, Саратова, Нижнего Новгорода, Ижевска, Петрозаводска, Перми и других городов России. В них знания передаются «изустно» от ведущих преподавателей к олимпиадникам, а также от одного поколения олимпиадников к другому. В то же время во множестве городов России и других стран СНГ наблюдается острейшая нехватка как педагогических кадров, так и литературы по подготовке к олимпиадам и решению сложных олимпиадных задач по программированию для студентов и школьников, — и это на фоне огромного интереса к олимпиадам со стороны школьников, студентов и преподавателей. Достаточно сказать, что в 2004 году за право попасть в Северо-Восточный полуфинал студенческого первенства мира по программированию (NEERC) в четырнадцати (!!!) четверть- четвертьфиналах соревновалось более 630 команд более чем из 210 вузов. Сопоставимы по числу участников в олимпиадах и соревнования для школьников по инфор- информатике. Данная книга написана с целыо восполнить указанный пробел в отечественном книгоиздании. Она ориентирована на следующие категории читателей: а участников командных чемпионатов мира но программированию для студен- студентов, проводимых иод эгидой ACM; ? тренеров таких студенческих команд; ? преподавателей вузов, ведущих учебные дисциплины «Теория алгоритмов», «Методы алгоритмизации», «Структуры данных», «Олимпиадные задачи» и т. д.; ? участников региональных и Всероссийских олимпиад по информатике для школьников, а также русскоязычных участников национальных олимпиад в странах СНГ и Балтии; ? тренеров региональных команд по информатике; О учителей школ, ведущих занятия по информатике на углубленном и профиль- профильном уровнях; ? школьников, изучающих информатику на углубленном и профильном уров- уровнях. Автору представляется полезным знакомство читателя с материалом его преды- предыдущей книги «Алгоритмизация и программирование на Turbo Pascal от простых до олимпиадных задач», включающим, в частности, следующие темы: динамиче- динамическое программирование и рекуррентные соотношения, графы, рекурсия, очередь, генерация комбинаторных объектов, элементы теории чисел, полезные сведения из аналитической геометрии. Задачным материалом для данной книги послужили задачи международных и на- национальных олимпиад по информатике и программированию для школьников и студентов. Для повышения эффективности книги задачи в ней сгруппированы по темам. Последовательно рассматриваются следующие темы: максимальный
Ю Введение поток, минимальное остовное дерево, деревья, скрытые графы, стратегические игры, диаграммы Юнга1, дихотомия. Во всех случаях вначале лаконично, но до- доступно (даже для школьников среднего звена) излагается необходимый теорети- теоретический материал по теме, затем приводятся условие задачи, идея решения и опи- описание конкретной реализации решения на языке программирования Паскаль. Каждый, кто работает с книгой, может проверить авторские и собственные реше- решения на сайте дистанционного обучения Гомельского государственного универси- университета им. Ф. Скорины http://dl.gsu.unibel.by в разделах «Методы алгоритмизации», «Олимпиады по информатике», «Тренировочный курс АСМ». К важным достоинствам данной книги, по мнению автора, можно отнести: ? наличие материалов, неизменно отсутствующих в литературе, представленной на книжном рынке (см. главы «Скрытые графы», «Решение задач на деревьях и с помощью деревьев», «Стратегические игры»); ? актуальность всех рассмотренных тем: максимальный поток, минимальное ос- остовное дерево, дихотомия, табло Янга, скрытые графы, стратегические игры, решение задач на деревьях и с помощью деревьев; ? многослойность изложения материала, позволяющую успешно работать с кни- книгой читателям с различным уровнем подготовки и различными темперамента- темпераментами, с возможностью пропуска отдельных пунктов для тех, кто уже все понял, и углубления в детали материала для тех, кому он показался сложным; ? поддержку круглосуточного автоматического тестирования решений предло- предложенных в книге задач на сайте дистанционного обучения Гомельского госу- дарственногоуниверситета им. Ф. Скорины http://dl.gsu.unibel.by; О поддержка форумов и консультаций по задачам на сайте http://dl.gsu.unibel.by. Проект «Дистанционное обучение» функционирует на серверах ГГУ им. Ф. Ско- Скорины с октября 1999 года. Обучаемый может, используя Интернет или электрон- электронную почту (dlrobot@gsu.unibel.by), отправить на проверку собственное решение любой из приведенных в книге (и многих других) задач в любое удобное для него время! Система автоматической проверки присылаемых решений работает круг- круглосуточно без праздников и выходных и, как правило, обеспечивает проверку ре- решений в течение нескольких минут. Автор надеется, что читатель сможет сам убедиться в достоинствах данной книги и предлагаемого подхода к обучению и самообучению основам алгоритмизации и программирования. Тем не менее автор будет благодарен за все отклики и замечания (в том числе — и прежде всего — критические), присланные на адрес dolinsky@gsu.unibel.by. ЗАМЕЧАНИЕ Условия задач no правилам студенческих олимпиад выдаются участникам на английском языке. Мы перевели все условия на русский, однако всем тем, кто собирается принимать участие в студенческих соревнованиях по программированию, настоятельно рекомендуем потрени- потренироваться в решении англоязычных задач как на сайте http://dl.gsu.unibel.by, так и на других тестирующих системах, например, на http://acm.timus.ru или на http://acm.uva.es. 1 Другое используемое название - «табло Янга».
От издательства От издательства Седьмая глава книги, содержащая описание шести задач, решаемых методом ди- дихотомии, помещена (в качестведополнения) на сайт издательства http:/Avww.piter.com в виде pdf-файла. Алфавитный указатель содержит термины из этой главы с ссыл- ссылкой на нумерацию страниц этого файла. Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Подробную информацию о наших книгах вы найдете на веб-сайте издательства http://www.piter.com.
Глава 1. Максимальный поток Данная глава посвящена теме «Максимальный поток» и имеет следующую струк- структуру. Вначале на простейших примерах вводится понятие «максимальный поток». Затем приводится формальная постановка задачи, примеры олимпиадных задач и методики их сведения к задаче о «максимальном потоке». Далее объясняется алгоритм Форда-Фалкерсонадля построения максимального потока и описыва- описывается его реализация, позволяюшая решить все приведенные задачи. 1.1. Примеры задач на максимальный поток Ориентированный взвешенный граф служит математической моделью широкого класса задач на нахождение потоков в сетях. Для примера рассмотрим граф с че- четырьмя вершинами, пронумерованными от 1 до 4: 2 4 Рис. 1.1. Граф с четырьмя вершинами Веса C[i,j] дуг графа: 1 2 3 4 1 0 0 0 0 2 0 0 0 0 3 10 10 0 0 4 10 0 0 0
1.1. Примеры задач на максимальный поток 13 Пусть веса дуг (от вершин 1 к вершинам 3 и 4 и от вершины 2 к вершине 3) рав- равны 10. Реальная постановка задачи, сводящейся к такому графу, может быть, на- например, такой: имеются две буровые вышки, добывающие нефть (вершины 1 и 2 нарис. 1.1), и два нефтеперерабатывающих завода (вершины 3 и 4 на рис. 1.1), которые эту нефть потребляют. Между вышками и заводами проложена сеть труб. В нашем примере от первой вышки (вершина 1) проведены трубы к обоим заво- заводам (вершины 3 и 4), а от второй вышки (вершина 2) — только к первому заводу (вершина 3). Заданы также пропускные способности каждой трубы — по 10 т/ч. Требуется узнать, какое максимальное количество нефти в час может поставляться на переработку. Многие читатели уже знают правильный ответ для приведенных исходных данных: 20 тонн. Нужно, чтобы из вершины 1 поставлялось 10 т/ч в вершину 4 (по дуге 1-4), а из вершины 2 поставлялось 10 т/ч в вершину 3 (по дуге 1-3). В общем случае пропускные способности разных дуг, безусловно, могут разли- различаться. Более того, вместо потоков жидкого вещества (нефти) можно рассматри- рассматривать движение тока по проводам и многое другое. Для корректности применения описанного ниже метода нахождения максималь- максимального потока в сетях необходимо, чтобы в физической постановке задачи соблюда- соблюдались следующие условия. 1. Ограничение пропускной спосо6ности:/[^, V] < c[U, V], где/[У, V] — поток от вершины UK вершине V, a c[ U, V] — пропускная способность дуги от вершины U к вершине V. 2. Граф должен иметь ровно одну вершину-истиок (в нее нет входящих дуг, а есть только исходящие из нее), ировно одну вершину-сшж (нет исходящих из нее дуг, а есть только входящие в нее). 3. Сохранение потока: для любой вершины графа, кроме истока и стока, сумма потоков по всем входящим в вершину дугам равна сумме потоков по исходя- исходящим из нее дугам Для нашей задачи о нефтедобывающих вышках и нефтеперерабатывающих за- заводах не выполнено условие 2 — у нас два истока (вершины 1 и 2) и два стока (вершины 3 и 4). Для того чтобы обеспечить выполнение условия 2, в таких слу- случаях искусственно добавляют исток и сток. Например, для нашей задачи исток — вершина 0 — общий (подземный) резервуар нефти, а сток — вершина 5 — общее «виртуальное» хранилище потребленной нефти. Тогда граф имеет вид, пока- показанный на рис. 1.2: 2 4 Рис. 1.2. Граф сдобавленными вершинами (истоком и стоком)
14 Глава 1 * Максимальный поток Важно еще правильно определить веса введенных дуг, то есть «пропускные спо- способности» труб от вершины 0 к вершинам 1 и 2 и от вершин 3 и 4 к вершине 5. В данной задаче корректно ввести понятие «неограниченных» весов. На практике эта переменная получает максимально возможное значение (maxlongint, напри- например, в случае целочисленных весов c[iJ]). Классической задачей, сводящейся к задаче о потоках в сетях, является также задача о максимальном паросочетании в двудольном графе. Пусть имеется граф (рис. 1.3): Рис. 1.3. Пример графа для задачи о максимальном паросочетании Веса C[iJ] дуг графа: 1 2 3 4 1 0 0 1 1 2 0 0 1 0 3 1 1 0 0 4 1 0 0 0 Пусть вершины 1 и 2 соответствуют женихам, вершины 3 и 4 — невестам, a c[iJ] ж 1 в том случае, если i uj готовы стать супругами. Требуется узнать максимально возможное количество супружеских пар. Правильный ответ в данных условиях — 2. Сочетаться браком могут жених 1 с невестой 4 и жених 2 с невестой 3. 1.2. Формальная постановка задачи В примерах, приведенных выше, граф задавался матрицей весов своих дуг. Для формальной записи алгоритма удобно, кроме матрицы весов c[i,j], оперировать также списками дуг графа из каждой вершины, которые представлены в следую- следующем виде: ? ka[i] — количество дуг из вершины i; ? a[iJ] — номер вершины, в которую ведет^'-я дуга из вершины i. Например, для графа из 6 вершин массивы ka[i] и a[iJ] таковы: / *a[/] 4 1 5 0
1.2. Формальная постановка задачи 15 2 4 Рис. 1.4. Граф с шестью вершинами Таблица 1.1. Значения a[/, j] j=1 j=2 0 1 2 1 3 4 2 3 3 5 4 5 5 здесь i — номер строки (вершины),; — номер столбца (дуги). Пусть задан граф cN+ 2 вершинами (вершина 0 — исток, вершина N+ 1 — сток и «промежуточные* вершины от 1 до N). Результат решения задачи о максимальном потоке — это построение такого мас- массива/^'] if[hj] — поток по дуге из вершины i в вершину;), что сумма/[0,;] (для всех; от 1 до N + 1) (исходящий поток) максимальна. Понятно, что то же максимальное значение имеет и другая величина — входящий поток — сумма/[г, N+ 1] (для всех i от 0 до N). Прежде чем перейти к изложению алгоритма решения задач о максимальном по- потоке, рассмотрим несколько конкретных задач с реальных олимпиад и сведем их к задаче о максимальном потоке. Необходимо также отметить, что при решении задач основная трудность часто заключается именно в формализации задачи и организации ввода исходных дан- данных таким образом, чтобы можно было применять стандартный алгоритм нахож- нахождения максимального потока. Для решения всех трех задач, условия которых приведены далее («Новогодние вечеринки», «Кубики», «Игра»), тело программы выглядит следующим образом: begin InputData: MaxFlow: OutputData: end. При этом если процедуры InputData и OutputData отражают специфику зада- задачи, то процедура MaxFlow абсолютно идентична для всех трех решений.
16 Глава 1 * Максимальный поток 1.3. Задача «Новогодняя вечеринка» USA Computing Olympiad, 2002 Новогодняя вечеринка (party.in / party.out)) Группа из N C<N<200) коров устраивает новогоднюю вечеринку. Каждая корова может приготовить несколько различных видов пищи, измеряемой в единицах, называемых «блюдо». Всего имеется D E < D < 100) различных видов пищи. Каждый вид пищи обозначается чис- числом в диапазоне от 1 до D. Координатор коровьей вечеринки хочет максимизировать общее количе- количество блюд, которые будут принесены на вечеринку, но имеет установлен- установленный лимит на количество блюд каждого типа. Каждая корова может прине- принести К A < К < 5) блюд, но они должны отличаться друг от друга. К примеру, одна корова не может принести 3 пирожка с говядиной, но может принести пирожок, хлеб и вкусную люцерну в апельсиновом соусе. Каково макси- максимальное количество пищи, которую коровы могут принести на вечеринку? Ввод: Строка 1: Три целых числа: N, К, D Строка 2: D неотрицательных чисел — предел суммарного количества для каждо- каждого из различных блюд, которые могут быть принесены на вечеринку. Строки 3..N+ 2: Каждая строка содержит начальное целое ZA <Z<D), обозна- обозначающее количество типов различных блюд, которое может приготовить одна ко- корова; остаток строки содержит Z целых чисел — идентификаторов типов пищи, которую может приготовить корова, соответствующая текущей строке (в строке 3 — корова 1, в строке 4 — корова 2, и т. д.). Пример ввода [файл party.in]: 4 3 5 2 2 2 2 3 4 1 2 3 4 4 2 3 4 5 3 1 2 4 3 1 2 3 Пояснения к примеру: Данные Пояснения 4 3 5 4 коровы, каждая до 3 блюд, всего 5 различных типов пищи 2 2 2 2 3 max — 2 блюда типов 1..4 и 3 блюда типа 5 4 1 2 3 4 1-я корова может приготовить 4 различных блюда A, 2, 3, 4) 4 2 3 4 5 2-я корова может приготовить 4 различных блюда B, 3, 4, 5) 3 1 2 4 3-я корова может приготовить 3 различных блюда A, 2, 4) 3 1 2 3 4-я корова может приготовить 3 различных блюда A, 2, 3)
1.3. Задача «Новогодняя вечеринка» 27 Вывод: Одна строка содержит одно целое число — максимальное количество блюд, кото- которое может быть принесено на вечеринку. Пример вывода [файл party.out]: 9 Пояснения: Корова 1 принесет блюда 3 и 4: Корова 2 принесет блюда 3. 4 и 5: Корова 3 принесет блюда 1 и 2; Корова 4 принесет блюда 1 и 2. Рассмотрим граф, состоящий из N + D вершин. Вершины с номерами от 1 до N соответствуют коровам, вершины с номерами от N+1 до N+D соответствуют раз- различным видам пищи (блюдам). Прежде всего мы должны добавить в граф вершины исток (вершину с номером 0) и сток (вершину с номером Finish - N + D + 1). Понятно, что от истока должны быть дуги ко всем вершинам 1..#(коровам), а от вершин N+l..N+D (блюда) должны быть дуги к стоку. Каковы же должны быть веса введенных дуг от истока и к стоку? Для дуг «от истока»: c[0, i] - К (для всех i от 1 до N), поскольку каждая корова может принести К блюд. Соответствующий фрагмент программы выглядит сле- следующим образом: {Добавляем вершину-исток} ka[O]:-N; {Из истока - N дуг к каждой корове} for i:-l to N do begin a[O.i]:-i: {Номер вершины, соединенной с истоком} c[O.i]:-K: {ee вес - максимальное кол-во блюд для коровы} end: Для дуг «к стоку»: c[i, Finish] - Lim[i-N] (для всех i от N + 1 до N + D), поскольку имеется лимит на количество блюд каждого типа. Соответствующий фрагмент программы выглядит следующим образом: for i:-l to D do read (Lim[i]): {Пределы блюд каждого типа} {Добавляем вершину-сток} for i:-N+l to N+D do begin ka[i] :-l: a[i.l]:-Finish : {одна дуга от каждого блюда к стоку} c[i.Finish]:-Lim[i-N]; {ee вес - предел числа блюд данного типа} end: Мы строим дугу от коровы i к блюдут в том и только в том случае, если корова i может приготовить блюдо/ Вес каждой такой дуги должен получить значение 1,
J8 Глава 1 * Максимальный поток поскольку блюда, которые принесет корова, должны отличаться друг от друга. Соответствующий фрагмент программы выглядит следующим образом: {Добавляем вершины от коров к блюдам} for i:-l to N do {Для каждой коровы} begin read(Z); ka[i]:-Z; {Количество блюд, которые она готовит} for j:-l to Z do {Для каждого такого блюда} begin read(FoodID): {Читаем его номер} a[i.j]:-N+FoodID: {Добавляем дугу от коровы к блюду} c[i.a[i.j]]:-l: {Bec такой дуги - 1} end end; Мы имеем граф, заданный списками дуг {ka[i], a[iJ]} и матрицей весов c[i,j], на котором требуется решить задачу о построении максимального потокаД^]. Пос- После чего ответ на вопрос, поставленный в задаче («Каково максимальное количе- количество пищи, которую коровы могут принести на вечеринку?»), можно получить, например, следующим образом: {Поток, входящий в вершину-сток, с номером Finish} Max:-0: for i:-N+l to N+D do Max:4tex+f[i.Finish]; writeln(Max): ЗАМЕЧАНИЕ Поскольку по построению связь с вершиной-стоком (номер Finish) имеют только вершины, соответствующие блюдам (с номерами от N+1 до N+D), то суммируем только их. Потоки из вершин 0..N в вершину-сток будут равны нулю, поскольку в графе для данной задачи вообще нет дуг, соединяющих эти вершины со стоком. Полный текст решения задачи приведен в конце главы. 1.4. Задача «Кубики» Всероссийская командная олимпиада школьников по программированию, 2000 Кубики (input.txt / output.txt /10 с) Родители подарили Пете набор детских кубиков. Поскольку Петя скоро пойдет в школу, они купили ему кубики с буквами. На каждой из шести граней каждого кубика написана буква. Теперь Петя хочет похвастаться перед старшей сестрой, что научился читать. Для этого он хочет сложить из кубиков ее имя. Но это оказалось довольно сложно сделать — ведь разные буквы могут находиться на од- одном и том же кубике, и тогда Петя не сможет использовать обе буквы в сло- слове. Правда, одна и та же буква может встречаться на разных кубиках. По- Помогите Пете!
1.4. Задача «Кубики» Дан набор кубиков и имя сестры. Выясните, можно ли выложить ее имя с по- помощью этих кубиков, и если да, то в каком порядке следует выложить кубики. Ввод: В первой строке входного файла находится число N(\ <N< 100) — количество кубиков в наборе у Пети. Во второй строке записано имя Петиной сестры — сло- слово, состоящее только из больших латинских букв, не длиннее 100 символов. Сле- Следующие ЛГстрок содержат по 6 букв (только большие латинские буквы), которые написаны на соответствующем кубике. Вывод: В первой строке выходного файла выведите YES, если выложить имя Петиной сестры данными кубиками можно, и N0 — в противном случае. Если ответ YES, то во второй строке выведите М различных чисел из диапазона l..N, где М — количество букв в имени Петиной сестры, i-e число должно быть но- номером кубика, который следует положить на i-e место при составлении имени Пети- Петиной сестры. Кубики нумеруются с 1, в том порядке, в котором они заданы во входном файле. Если решений несколько, выведите любое. Разделяйте числа пробелами. Пример ввода Пример вывода ~~4 N0 ANN ANNNNN BCDEFG HIJKLM NOPQRS 5 YES HELEN 2 1 3 5 4 ABCDEF GHIJKL MNOPQL STUVWN EIUOZK Составим для данной задачи граф следующим образом: в качестве базовых вер- вершин будем рассматривать заданные кубики, а также вершины, соответствующие буквам, которые могут быть написаны на заданных кубиках B6 заглавных букв латинского алфавита). Добавим также вершину-исток (ее соединим со всеми вершинами, соответствующими кубиками) и вершину-сток (с ней соединим все вершины, соответствующие буквам). Фрагменты программы, которые обеспечат необходимый ввод исходных данных и построение графа, приведены ниже: readln (N): {Количество кубиков} readln (Name): {Имя Петиной сестры} for i:-l to N do readln(Qubics[i]): {Надписи на кубиках} Finish :- N+26+l: {Кубики + алфавит + сток}
20 Глава 1 • Максимальный поток Поскольку с каждого кубика может быть использована только одна буква, то веса дуг от истока к кубикам равны 1. {Добавляем вершину-исток} ka[O]:-N; {Из истока - N дуг к каждому кубику} for i:-l to N do begin a[O.i]:-i; {Номер вершины, соединенной с истоком} c[O.i]:-l; {вес соответствующей дуги} end; Фрагмент графа от вершин, соответствующих заглавным буквам латинского ал- алфавита, к стоку определяется именем Петиной сестры (переменная Name), кото- которое по условиям задачи требуется составить из исходных кубиков: {Добавляем вершину-сток} for i:-l to Length(Name) do begin NomLet:= Ord(Name[i]) - Ord('A') + 1; ka[N+NomLet]:-l: a[N+NomLet.l]:-Finish : {Одна дуга на каждую букву из имени} Inc(c[N+NomLet.Finish]) {ee вес - количество таких букв в имени) end: ЗАМЕЧАНИЕ Поскольку имя Петиной сестры может включать повторяющиеся буквы, то при формировании веса соответствующей дуги используется инкрементирование — lnc(c[N+NomLet,Finish]). Наконец для каждой буквы на каждом кубике строится дуга от соответствующего кубика к соответствующей букве. Заметим, что на одном кубике буквы могут повторяться, и мы можем для таких дуг либо увеличивать их вес, либо добавлять новую дугу (с весом 1 — именно та- такой вариант и реализован в приведенном фрагменте программы). Алгоритм по- построения максимального потока, который будет изложен далее, работает и в гра- графах, содержащих более одной дуги между одной и той же парой вершин. {Добавляем дуги от кубиков к буквам} for i:-l to N do {Для каждого кубика} begin for j:-l to 6 do {Для каждой буквы на кубике} begin NomLet:-Ord(Qubics[i.j])-Ord('A')+l; {Вычисляем номер буквы} a[i.j]:^N+NomLet; {Добавляем дугу от кубика к букве} c[i.a[i.j]]:-l: {Bec такой дуги - 1} NomKub[Nomlet]:-l: inc(ka[i]) end: end;
1.5. Задача «Игра» Чтобы получить ответ на вопрос, поставленный в задаче («Выясните, можно ли выложить ее имя с помощью этих кубиков и если да, то в каком порядке следует выложить кубики»), после построения максимального потока назаданном графе следует высчитать максимальный исходящий поток Мах: Max:-0: for i:-N+l to N+26 do Max:-Max+f[i.Finish]; Если значение Мах не равно длине имени Name, то ответ отрицательный (N0), иначе — положительный ( YES). Для того чтобы вывести номера использованных кубиков в порядке, обеспечивающем построение имени, нужно для каждой бук- буквы имени {NomLet) найти такую вершину k в графе (и вывести ее номер), 4iof[k, Nomlet] - 1. Фрагмент программы, выводящий ответ задачи, приводится ниже: If Max<>Length(Name) then writelnCNO') else begin writeln('YES'): for i:-l to Length(Name) do begin NomLet:-Ord(Name[i])-Ord('A')+l; {Вычисляем номер буквы} for k:-l to n do if f[k.N+NomLet]-l then begin write(k.' '): f[k.N+NomLet]:-0: break; end; end: Полный текст решения задачи приведен в конце главы. 1.5. Задача «Игра» Белорусская олимпиада по информатике, 2001 Игра (game.in / game.out) Известная на весь Могилев компания выпустила игру, для которой необхо- необходима конструкция, состоящая из маленьких платформ и труб. Платформы разделяются на стартовые (их М штук), финишные (N3 штук) и промежу- промежуточные (N2 штук). Все стартовые платформы находятся на одинаковой вы- высоте. Финишные платформы также находятся на одинаковой высоте. Все высоты промежуточных платформ различны. Они меньше высоты старто- стартовых, но больше высоты финишных. Каждой платформе соответствует уникальный номер от 1 до Nl+N2+N3. Нумерация следующая: сначала перечислены все стартовые платформы, за- затем промежуточные и, наконец, финишные. Все промежуточные платформы
22 Глава 1 * Максимальный поток пронумерованы по убыванию высоты. То есть если высота промежуточной платформы А больше высоты платформы В, то номер А меньше номера В. На каждой из стартовых платформ находится шарик. Шарик может скатить- скатиться с платформы А на платформу В, если они соединены трубой и высота А больше высоты В. На каждой из финишных платформ может оказаться не более одного шарика. Если шарик находится на некоторой платформе, то игрок может выбрать направление дальнейшего пути шарика, то есть вы- выбрать платформу, на которую шарик скатится. Также для каждой проме- промежуточной платформы задано число С, равное максимальному количеству шариков, которые могут прокатиться по ней за время игры. Цель игры за- заключается в том, чтобы на финишных платформах оказалось как можно больше шариков. Вам нужно узнать, какое максимальное количество шариков может оказать- оказаться на финишных платформах в результате игры. Ввод: Во входном файле Game.in находится информация о конструкции в следующей форме: N1 N2 N3 CN1+1 CNl+N2 K1 A[1.1] : A[1.K1] K2 A[2.1] : A[2.K2] KNl+N2A[Nl+N2.1] : A[Nl+N2.KNl+N2] где числа N1, M2, N3 — соответственно количество стартовых, промежуточных и фи- финишных платформ. Cj — максимальное количество шариков, которые могут прока- прокатиться по промежуточной платформе с номером,;' (N\ +1 <j ? N1 +M2) за все время игры. Ki — количество труб, выходящих из платформы с номером i A ? i й N\ +N2). Элементы массиваЛ, перечисленные в строке, являются номерами платформ, на которые может скатиться шарик с соответствующей платформы. Ограничения: Все числа на вводе целые. 0 < N1. N2. N3 < 51 1 < Nl+N2+N3 < 201 0 J Cj J 50 Не существует труб между стартовыми платформами. Не существует труб между финишными платформами. Вывод: В первой строке выходного файла Game.out должно находиться единственное число, равное максимальному количеству шариков, которые могут оказаться на финишных платформах в результате игры.
1.5. Задача «Игра» 23 Пример ввода Пример вывода 343 2 3 2 1 2 1 4 1 4 1 4 256 1 7 1 7 3 89 10 Стартовые, промежуточные и финишные платформы естественно представлять вершинами графа, а трубы между ними — дугами. Однако данная задача слегка отличается по постановке от классической, поскольку здесь задан вес вершины: «...для каждой промежуточной платформы задано число С, равное максимально- максимальному количеству шариков, которые могут прокатиться по ней за время игры». Для перехода к классической постановке достаточно заменить такие вершины на дуги. При этом введенным дугам присваивается вес замененных вершин. Понятно, что должны появиться дополнительные вершины. Например, пусть в вершину 3 с ве- весом X входит две дуги A ^ 3 и 2 -^> 3) и выходит две дуги C ^ 4 и 3 ^> 5), как показано на рис. 1.5: ЗХ / \ 2 5 Рис. 1.5. Исходное состояние вершины 3 Мы добавляем вершину 6 и дугу 3 ^ 6 с весом X (рис. 1.6): 1 4 \ 2 5 Рис. 1.6. Преобразованный фрагмент графа
24 Глава 1 • Максимальный поток Покажем теперь, как такой подход может быть реализован в данной задаче. По- Поскольку промежуточные платформы превращаются в дуги, то всего вершин ста- становится на N2 больше, где N2 — количество промежуточных платформ. readln (Nl.N2.N3): {Количества платформ} All :- Nl+N2+N3: Finish :» Nl+2*N2+N3+1: {Платформы + сток} Добавляем исток и связываем его с вершинами, соответствующими стартовым платформам. Пропускная способность дуг равна 1, поскольку по условию задачи на каждой из стартовых платформ находится шарик. {Добавляем вершину-исток} ka[O]:-Nl: {Из истока - N дуг - к каждой стартовой платформе} for i:-l to N1 do begin a[O.i]:-i: {Номер вершины, соединенной с истоком} c[O.i]:-l: {на каждой из стартовых платформ - 1 шарик} end: Для каждой промежуточной платформы с номером /добавляем две вершины с но- номерами N1 + i и all + i (где all = N\ + N2 + N3) соответственно. Добавляем также дугу между этим вершинами с пропускной способностью вершины. {Добавляем дуги "внутри" промежуточных платформ} for i:-l to N2 do {Для каждой промежуточной платформы} begin readln (cn); {Читаем сколько шариков она выдержит} a[Nl+i,l]:-all+i; {Добавляем дугу "внутри" этой платформы} c[Nl+i.all+i]:-cn: {Пропускная способность добавленной дуги} ka[Nl+i]:-l; {Такая дуга всегда одна} end: Все входящие дуги от стартовых платформ будем строить в вершины M+i. По условиям задачи их пропускная способность не ограничена. {Добавляем дуги от стартовых платформ к "началу" промежуточных} for 1:-1 to N1 do begin read(k); {Количество труб от платформы} ka[i].*k: {Количество дуг из вершины i} for j:=l to k do {Для каждой трубы} begin read(a[i.j]): {Дуга из i в a[i.j]} c[i.a[i.j]]:-MaxP; {Ee пропускная способность не ограничена} end; end: Все дуги от промежуточных платформ будем строить от вершин all + i — их про- пропускная способность также не ограничена. {Добавляем дуги от "конца" промежуточных платформ к финишным платформам} for i:-l to N2 do
1.6. Пример максимального потока на графе 25 begin read(k); {Количество труб от платформы} ka[all+i]:-k: {Количество дуг из вершины all+i} for j:-l to k do {Для каждой трубы} begin read (np); {Номер "целевой" платформы} a[all+i.j]:-np; {Дуга из Nl+2*i в a[i.j]} c[all+i.a[all+i.j]]:-MaxP: {Ee пропускная способность не ограничена} end: end: Добавляем вершину-сток с дугами к ней от всех финишных платформ. Веса :>тих дуг равны 1, поскольку на каждой из финишных платформ может оказаться не более одного шарика. {Добавляем вершину-сток} N:-Nl+N2: {Стартовый номер финишных платформ} for i:-l to N3 do {Для каждой финишной платформы} begin ka[N+i]:-l: a[N+i.l]:-Finish : {Одна дуга на каждую финишную платформу} c[N+i.Finish]:-l: {Ha каждой из финишных платформ - не более 1 шарика} end: Для вывода ответа на поставленный в задаче вопрос «Какое максимальное коли- количество шариков может оказаться на финишных платформах в результате игры?» после построения максимального потокадостаточно просуммировать количество шариков, выкатившихся из истока на стартовые платформы: Max:-0: for i:-l to N1 do Max:-Max+f[O.i]: writeln(Max): Полный текст решения задачи приведен в конце главы. 1.6. Пример максимального потока на графе Вернемся к задаче о паросочетании: 1 2 4 Рис. 1.7. Пример графа для задачи о максимальном паросочетании
26 Глава 1 * Максимальный поток Добавляем вершины — исток и сток: 1 Рис. 1.8. Граф с добавленными вершинами (истоком и стоком) Сначала проиллюстрируем неформально на данном примере алгоритм Форда- Фалкерсона, изложенный в следующем пункте данной главы. Все начинается с нулевого noTOKa/[i,;] e 0 для всех iJ. Далее строится остаточ- остаточная пропускная способность cf[i,j] (из вершины i в вершину,;') для всех дуг: cf[iJ}-c[iJ]-f[i,j}. Напомним, что в этой задаче c[i,j] e 1, если есть дуга из вершины i в вершину/ Затем строится произвольный дополняющий путь из истока @) в сток E), увели- увеличивающий поток. Путь называется увеличивающим поток, если он состоит только из дуг с неотрицательной остаточной пропускной способностью cf[iJ]. Пусть, например, это путь 0-1-3-5. Далее выбирается минимальное значение из весов всех дуг увеличивающего пути. В нашем случае минимальное значение ищется из величин: c/[0, 1] * неограниченно c/[1.3]-l c/[3, 5] = неограниченно Минимальноезначение - 1. Далее для каждой дуги из увеличивающего пути увеличиваем поток по этой дуге на найденное минимальное значение и получаем (рис. 1.9): 2 4 Рис. 1.9. Граф с отмеченным увеличивающим путем Затем — это ключевой момент алгоритма — для каждой дуги (w, v) из увеличиваю- увеличивающего пути СТРОИМ ОБРАТНЫЕДУГИс ОТРИЦАТЕЛЬНЫМ потоком:/[а, u] - -/[м, о], в частности,/[3,1] - -/[1,3] - -1 (рис. 1.10):
1.6. Пример максимального потока на графе 27 Рис. 1.10. Граф с обратной дугой с отрицательным потоком При вычислении остаточной пропускной способности дуги с отрицательным по- потоком появляется НОВАЯ дуга с ПОЛОЖИТЕЛЬНОЙ остаточной пропускной способностью: c/[3,l]- c[3,l] -/[3,1] ¦ 0 - (-1) - 1. Теперь при попытке постро- построения нового увеличивающего пути нам удается найти путь 0 -^ 2 ^ 3 ^ 1 ^ 4 ^ 5. Для всех дуг этого пути cf[iJ] > 0. Минимальное значение увеличивающего пути из всех cf[i,j] опять равно 1. Снова прибавляем это минимальное значение ко всем дугам увеличивающего пути, по- получаем c[3,1] - -1 + 1 -0 (рис 1.11): 2 4 Рис. 1.11. Граф с новым увеличивающим путем И снова для каждой дуги (u,v) из увеличивающего пути строим обратные дороги сотрицательнымпотоком:Л^«] e -Лм^вчастностиД^З] ж -/[3i 1] в0иокон- чательно получаем (рис. 1.12): Рис. 1.12. Граф с новыми обратными дугами с отрицательным потоком
28 Глава 1 * Максимальный поток Теперь найти увеличивающий путь невозможно. Алгоритм закончил свою рабо- работу. Осталось проинтерпретировать результаты. Мы построили следующий поток: О из вершины 0 (истока) — в вершину 1 и вершину 2:Д0,1] e l,f[0, 2] e l; Q из вершины 1 в вершину 4:Д1,4] e 1; ? из вершины 2 в вершину 3:f[2,3] e 1; ? из вершины 3 в вершину 5:ДЗ, 5] e 1; ? из вершины 4 в вершину 5:f[4, 5] = 1. МаксимальныйпотокравенмаксимальномуисходящемупотокуДО, 1] +Л0,2] e 2 и равен максимальному входящему потоку/^З, 5] +Л^> 5] e 2. 1.7. Алгоритм Форда-Фалкерсона Неформально этот алгоритм может быть записан следующим образом: Формируем нулевой поток Пока существует путь из истока в сток, увеличивающий поток Выбираем Cmin - минимальную из остаточных пропускных способностей найденного пути Для каждой дуги (u.v) пути (из вершины u в вершину v) f[u.v] :- f[u.v] + Qnin f[v.u] :- -f[u.v] Далее приводится описание одного из вариантов программной реализации мето- метода Форда-Фалкерсона. Для решений всех трех описанных ранее задач тело про- программы выглядит следующим образом: begin InputData: MaxFlow; OutputData: end. Тело процедуры MaxFlow выглядит следующим образом: begin Init: {Инициализация нулевого потока} While ExistPath(KR.Cmin) do {CMin - минимальный из cf(u.v)-c(u.v)-f(u.v))} for i:-KR downto 1 do {Для каждой дуги увеличивающего пути} begin u:- path[i+l]; {U->V - дуга с номером i в пути} v:- path[i]: f[u.v]:-f[u.v]+Cmin; {Увеличиваем поток на минимальную величину} f[v.u]:-f[u.v]: {f(v.u)-f(u.v)} cf[u.v]:-c[u.v]-f[u.v]: {Перевычисляем остаточную пропускную способность} cf[v.u]:-c[v.u]-f[v.u]: {на дугахувеличивающего пути} end: end:
1.7. Алгоритм Форда-Фалкерсона 29 Как можно заметить, в процедуре MaxFlow вызываются процедура Init для ини- инициализации нулевого потока и функция ExistPath, которая возвращает значе- значение true, если увеличивающий путь существует, и значение false в противном слу- случае. Очевидно, что если функция ExistPath возвращает значение false, то про- процедура MaxFlow завершает свою работу (увеличивающих путей больше нет). Если же ExistPath возвращает true (найден увеличивающий путь), то через свои па- параметры она возвращает также Cmin (минимальное значение из остаточных про- пропускных способностей дуг увеличивающего пути) и KR (количество дуг в увели- увеличивающем пути). Кроме того, в глобальном для процедуры MaxFlow массиве^А возвращаются номера вершин увеличивающего пути, перечисленные в порядке от стока к истоку. Если путь найден, то в соответствии с алгоритмом Форда-Фалкерсона для всех дугувеличивающего пути пересчитывается поток/[м, v], строятся обратныедуги с отрицательным потоком/[я, и] и пересчитываются остаточные пропускные спо- способности cf[u, v] и cf[v, и]. Рассмотрим подробней содержание процедур Init и ExistPath. Procedure Init: begin for i:*0 to Finish do for j:=0 to Finish do f[i.j]:*O: for i:-0 to Finish do for j:-0 to Finish do cf[i.j]:-c[i.j]: {построение списков входящих дуг} for i:-0 to Finish do kia[i]:-0: for i:-0 to Finish do {По всем вершинам} for j:-l to Finish do ia[i.j]:--l: {no всем исходящим дугам} for i:-0 to Finish do {По всем вершинам} for j:-l to ka[i] do {no всем исходящим дугам} begin inc(kia[a[i.j]]): {инкремент к-ва дуг. входящих в a[i.j]} ia[a[i.j].kia[a[i.j]]]:-i: {запоминаем вершину i. как входящую в a[i.j]} end: end: Здесь вначале строится нулевой поток for i:-0 to Finish do for j:-0 to Finish do f[i.j]:-0: Затем — начальная остаточная пропускная способность for i:-0 to Finish do for j:-0 to Finish do cf[i.j]:-c[i.j]: Далее для более удобной работы с «обратными» дугами увеличивающего пути строится «инвертированный» граф таким образом, что номера вершин в «ин- «инвертированном» графе остаются прежними, а направления дуг меняются на про- противоположные. Тем самым мы для каждой вершины получаем список входящих в нее дуг.
30 Глава 1 * Максимальный поток ? kia[i] — количество дуг, входящих в вершину i ? ia[i,j] — номер вершины, из которой ведет^-я по счету дуга, входящая в вер- вершину i {построение списков входящих дуг} {Инициализация} for i:=0 to Finish do kia[i].-O: for i =0 to Finish do {По всем вершинам} for j-«l to Finish do ia[i.j]:--l: {no всем исходящим дугам} Заметим, что величины ia[i,j] инициализируются значением -1, поскольку ну- нулем инициализировать нельзя — в графе есть дуги, исходящие из вершины 0. for i:-0 to Finish do {По всем вершинам} for j:-l to ka[i] do {no всем исходящим дугам} begin inc(kia[a[i.j]]): {инкремент к-ва дуг. входящих в a[i.j]} ia[a[i.j].kia[a[i.j]]].-i: {запоминаем вершину i. как входящую в [i.j]} end: Наконец рассмотрим функцию ExistPath. Function ExistPath(var KR.Cmin:integer):boolean; { Поиск в ширину } Const { Breadth-First Search } WHITE - 1: GRAY - 2: var color, back : array [O..MaxV] of integer: Q • дггау [O..MaxV] of integer; EndQ. BegQ : integer: Find : Boolean: Procedure Put(x:integer): begin inc(EndQ): Q[EndQ]:-x; • end: Procedure Get(var x:integer): begin x:-Q[BegQ]: inc(BegQ): end; begin for i:-0 to Finish do color[i]:-WHITE: {Bce вершины свободны} for i:-0 to Finish do Q[i]:--1: {Bce вершины свободны} color[0]:-GRAY: {Начальная вершина - обработана} back[O]:=-l: BegQ:-l: {Начало очереди} EndQ:-O: {Очередь пуста}
1.7. Алгоритм Форда-Фалкерсона 31 KR:-O; {Количество дуг в пути » 0} Put@): {Поместить в очередь начальную вершину} Find:-false: while (BegQ<-EndQ) and not Find do begin Get(i): j:-l: while (a[i.j]>0) and not Find do begin if (color[a[1.j]]-WHITE) and (cf[i.a[i.j]]>0) then begin Put(a[i.j]): back[a[i.j]]:-i: I color[a[i.j]]:-GRAY: Find :» a[i.j]-Finish: end: inc(j): end: {Пока очередь не пуста и путь не найден} {Взять вершину i из очереди} {номер дуги из вершины i} {пока дуги не кончились} {если вершина a[i.j] свободна} {поставить ее в очередь} [в вершину a[i.j] - из вершины i} {пометить вершину a[i.j]} {как использованную} {взять следующую дугу} {Проход по отрицательным ребрам} {номер дуги из вершины i} {пока дуги не кончились} {если вершина a[i.j] свободна} j:-l: while (ia[i.j]>-0) and not Find do begin if (color[ia[i.j]]-WHITE) and (cf[i.ia[i.j]]>0) then begin Put(ia[i.j]): {поставить ее в очередь} back[ia[i.j]]:-i: {в вершину a[i.j] - из вершины i} color[ia[i.j]]:-GRAY: {пометить вершину a[i.j]} {как использованную} Find :- ia[i.j]-Finish: end: inc(j): {взять следуюш^ю дугу} end: end: {Восстановление пути из очереди} KR:-0: i:-Finish: {Начинаем с конца} Cmin:- maxint: while (i<>0) do {Пока не начало} begin Inc(KR): {Увеличиваем к-во дуг в пути} path[KR]:-i: {Заносим номер предыдущей вершины}
32 Глава 1 • Максимальный поток i:-back[i]: {Меняен текущую вершину} if Cmin>cf[i.path[KR]] then Cmin:^f[i.path[KR]]: {Минимальный добавляемый поток} end: path[kR>l]:-0: ExistPath:-Find: end: Для построения увеличивающего пути используется механизм очереди от истока Put@): {Поместить в очередь начальную вершину} while (BegQ<-EndQ) and not Find do {Пока очередь не пуста и путь не найден} begin Get(i): {Взять вершину i из очереди} Вначале идет попытка увеличить путь за счет прямых дуг j:-l: {номер дуги из вершины i} while (a[i.j]>0) and not Find do {пока дуги не кончились} begin if (color[a[i.j]]-WHITE) and (cf[i.a[i.j]]>0) {если вершина a[i.j] свободна} then begin Put(a[i.j]); {поставить ее в очередь} back[a[i.j]]:-i; {в вершину a[i.j] - из вершины i} color[a[i.j]]:45RAY: {пометить вершину a[i.j]} {как использованную} Find :- a[i.j]-Finish: end: inc(j): {взять следующую дугу} end: Здесь: ? массив Color[i] хранит пометки для всех вершин (включена/нет в увеличива- увеличивающийся путь); ? массив back[k] хранит номер вершины, из которой пришли в вершину k в уве- увеличивающем пути; Q переменная Find получает значение true, если увеличивающий путь достиг стока (a[i,j] = Finish). Далее делаем попытку увеличить путь за счет обратных дуг {Проход по отрицательным ребрам} j:-l; {номер дуги из вершины i} while (ia[i.j]>-0) and not Find do {пока дуги не кончились}
1.8. Решения задач 33 begin if (color[ia[i.j]]-WHITE) and (cf[i.ia[i.j]]>0) then begin Put(ia[i.j]): badc[ia[i.j]]:-1: color[ia[i.j]]:-GRAY: Find :- 1a[1.j]-F1nish: end: inc(j): end: {если вершина a[i.j] свободна} {поставить ее в очередь} {в вершину a[i.j] - из вершины i} {пометить вершину a[i.j]} {как использованную} {взять следующую дугу} После выхода из цикла построения увеличивающего пути остается только вос- восстановить увеличивающий путь в массив path, используя массив back. Попутно подсчитываются количество дуг в увеличивающем пути (KR) и минимальный добавляемый поток (Cmin): {Восстановление пути из очереди} KR:-0: i:-Finish: {Начинаем с конца} Cmin:- maxint: while (i<>0) do {Пока не начало} begin Inc(KR): {Увеличиваем к-во дуг в пути} path[KR]:-i: {Заносим номер предыдушей вершины} i:-back[i]: {Меняем текущую вершину} if Gnin>cf[i.path[KR]] then Cmin:-cf[i.path[KR]]: {Минимальный добавляемый поток} end: path[kR+l]:-0: ExistPath:-Find: Для получения цельного представления о решении задачи методом Форда-Фал- керсона в конце главы приведено полное решение задачи «Новогодняя вечерин- вечеринка» (листинг 1.4). 1.8. Решения задач Листинг 1.1. Текст программы к задаче «Новогодняя вечеринка» const MaxN - 200: MaxD - 100: MaxV - MaxN+MaxD+l: продолжение
34 Глава 1 • Максимальный поток Листинг 1.1 (продолжение) var N К D Lim Z FoodID Мах 3..MaxN: 1..5: 5..MaxD; аггау [l..MaxD] of integer: integer; integer, integer: a : array [O..MaxV. l..MaxV] of Ka : array [O..MaxV] of integer: c.f.cf : array [O..MaxV. O..MaxV] of i.j. Finish : integer: ia : array [O..MaxV. l..MaxV] of Kia : array [0. MaxV] of integer; {Количество коров} {Количество блюд на одну корову} {Количество видов пищи} {Предел количества блюд на вид пищи} {Количество типов блюд, которое корова может приготовить} {Идентификатор блюда, которое корова может приготовить} {Максимальное количество блюд, которое может быть принесено на вечеринку} integer; {Граф исток-коровы-блюда-сток} {Количество ребер из вершины} byte: {Beca вершин в графе A} integer: {Список входящих вершин} {Количество входящих ребер} Procedure InputData; begin assign( input.'party.in'); reset(input): read(N.K.D): {Прочитали N-коров. D-блюд. К-предел блюд} Finish :- N+D+1: for i:-0 to N*D+1 do {Инициализируем пустой граф} begin {исток-коровы-блюда-сток} for j:-l to MaxD do a[1.j]:-0: ka[i]:-0: end. ka[O]:-N: for i.-l to N do begin a[0.i]:-i: c[0.1]:-K: end: for i:-l to D do read (Lim[i]): for i--N+l to N+D do begin ka[i] .-l: a[i.l]:-fHM : c[i.Finish]:-Lim[i-N]: end: for i:-l to N do {Добавляем вершину-исток} {Из истока - N дуг - к каждой корове} {Номер вершины, соединенной с истоком} {ee вес - максимальное кол-во блюд для коровы} {Пределы блюд каждого типа} {Добавляем вершину-сток} {одна дуга от каждого блюда к стоку} {ee вес - предел числа блюд данного типа} {Добавляем вершины от коров к блюдам} {Для каждой коровы}
1.8. Решения задач 35 begin read(Z): ka[i]:-Z: for j:-l to Z do begin read(FoodID): a[i.j]:-N+FoodID; c[i.a[i.j]]:-l: end end; close(input): end; {Количество блюд, которые она готовит} {Для каждого такого блюда} {Читаем его номер} {Добавляем дугу от коровы к блюду} {Bec такой дуги - 1} Procedure OutputData; begin assign(output.'party.out'); rewrite(output): Max:-0: for i:-N+l to N+D do Max:-Max+f[i.Finish]: writeln(Max); close(output): end: ПРИМЕЧАНИЕ Некоторые наиболее «востроглазые» читатели, наверное, уже нервничают — где это автор нашел столько оперативной памяти... Такое количество массивовдостаточно больших размеров никак не могут разместиться в статической памяти Turbo Pascal. Автор с этим согласен, но... Дело в том, что по правилам этой олимпиады программы компилировались с помощью компилятора Free Pascal, который разрешал использоватьдо 15 Мбайтоперативной памяти и стекдо 1 Мбайт. Так что приведенное решение далеко не полностью используетдоступную оперативную память. Листинг 1.2. Текст программы к задаче «Кубики» const 100; MaxN+26+l; MaxV div 20; MaxN • MaxV • МахС • var N а Ка c.f.cf {Макс {Макс, к-во вершин} к-во дуг из вершины} i.j. Finish : integer; ia Kia Мах Name Qubics Lim 3..MaxN: аггау [0..MaxV. l..MaxN] of byte; аггау [0..MaxV] of integer; аггау [0..MaxV. 0..MaxV] of byte; {Количество кубиков} {Граф исток-кубики-буквы-сток} {Количество ребер из вершины} {Beca вершин в графе A} аггау [0..MaxV. l..MaxC] of shortint; аггау [0..MaxV] of integer; longint; string[100]: аггау [1..100] of string [6]; аггау [1..26] of integer; {Список входящих вершин} {Количество входящих ребер} {Макс, поток} {Количество букв в имени} продолжение
36 Глава 1 * Максимальный поток Листинг 1.2 (продолжение) NomKub : аггау [1..26] of integer; {Номер кубика с такой буквой} NomLet : byte: s.k : byte: Procedure InputData: begin assign( input.*input.txt'): reset(input): readln (N); {Количество кубиков} readln (Name): {Имя Петиной сестры} for i:-l to N do readln(Qubics[i]); {Надписи на кубиках} Finish :- N+26+l; {Кубики + алфавит + сток} for i:-0 to Finish do {Инициализируем пустой граф} begin {исток-кубики-буквы-сток} for j:-0 to Finish do a[i.j]:-0: ka[i]:-0: end: {Добавляем вершину-исток} ka[O]:-N: {Из истока - N дуг - к каждому кубику} for i:-l to N do begin a[O.i]:-i: {Номер вершины, соединенной с истоком} c[O.i]:-l: {вес соответствующей дуги} end: {Добавляем вершину-сток} for i:-l to Length(Name) do begin NomLet:- Ord(Name[i]) - OrdCA') + 1: ka[N+NomLet]:-l; a[N+NomLet.l]:-Finish ; {Одна дуга на каждую букву из имени} Inc(c[N+NomLet.Finish]) {ee вес - количество таких букв в имени} end: {Добавляем дуги от кубиков к буквам} for i:-l to N do {Для каждого кубика} begin for j:-l to 6 do {Для каждой буквы на кубике} begin NomLet:4)rd(Qubics[i.j])-Ord('A')+l. {Вычисляем номер буквы} a[i.j]:-N+NomLet: {Добавляем дугу от кубика к букве} c[i.a[i.j]]:-l: {Bec такой дуги - 1} htomKub[Nomlet]-l: inc(ka[i]) end:
1.8. Решениязадач 37 end: close(input); end: {Вычисляем номер буквы} Procedure OutputData: begin assign(output.'output.txt'): rewrite(output): Max:-0: for i:-N+l to N+26 do Max:-Max+f[i.Finish]: If Max<>Length(Name) then writeln('NO') else begin writelnCYES'); for i:-l to Length(Name) do begin NomLet:-Ord(Name[i])-Ord('A')+l. for k:-l to n do if f[k.N+NomLet]-l then begin write(k.' '); f[k.N+NomLet]:-0: break: end: end: end: close(output); end: Листинг 1.3. Текст программы к задаче «Игра» const MaxNl - {50} 20: {Макс, кол-во стартовых платформ} MaxN2 - {100} 20: {Макс, кол-во промежуточных платформ} MaxN3 - {50} 20: {Макс, кол-во финишных платформ} MaxV - MaxNl+2*MaxN2+MaxN3: {Макс, к-во вершин} {Макс, к-во дуг из вершины} {Граф исток-кубики-буквы-сток} {Количество ребер из вершины} : {Beca вершин в графе A} {Список входящих вершин} {Количество входящих ребер} {Макс, поток} МахС - 't var а Ка c.f.cf i.j. Fir ia Kia Max const MaxP - I >*MaxN2 array array array iish : i array array [0..MaxV. [0..MaxV] CO..MaxV. nteger: [0..MaxV. [0..MaxV] longint: L27: 1. of 0. 1. of .MaxV] of i nteger; .MaxV] of .MaxV] of integer: byte: { shortint shortint продолжение
38 Глава 1 * Максимальный поток Листинг 1.3 (продолжение) var N1 : O..MaxNl: N2 : 0..MaxN2: N3 : 0..MaxN3: cn, np. N. k. tn. all : byte: {Кол-во стартовых платформ) {Кол-во промежуточных платформ) {Кол-во финишных платформ) Procedure InputData. begin assign( input.'game.in1 readln (N1.N2.N3): Finish :- N1+2*N2+N3+1: All :- Nl+N2+N3: for i:-0 to Finish do begin for j:-l to MaxC do ka[i]:-0: end: ): reset(input): {Количество платформ) {Платформы + сток) {Промежуточные платформы превращаются в дуги) {Инициализируем пустой граф) {исток-задачи-ресурсы-сток} a[i.j]:-0: {Добавляем вершину-исток) ka[0]:-Nl: {Из истока - N дуг - к каждой стартовой платформе) for i:-l to Nl do begin a[0.1]:-i: c[0.i].-l: end: for 1:-1 to N2 do begin readln (en): a[Nl+1.l]:-all+i: c[Nl+i.all+i]:-cn: ka[Nl+i]:-l; end: for i:-l to N1 do begin read(k): ka[i]:-k: for j:-l to k do begin read(a[i.j]): {Номер вершины, соединенной с истоком) {на каждой из стартовых вершин - 1 шарик) {Добавляем дуги "внутри" промежуточных платформ) {Для каждой промежуточной платформы) {Читаем сколько шариков она выдержит) {Добавляем дугу "внутри" этой платформы) {Пропускная способность добавленной дуги) {Такая дуга всегда одна) {Добавляем дуги от стартовых платформ к промежуточных) {Количество труб от платформы) {Количество дуг из вершины i} {Для каждой трубы) {Дуга из i в a[i.j]} c[i.a[i.j]]:-MaxP: {Ее пропускная способность не ограничена) end: end: 'началу"
1.8. Решения задач 39 {Добавляем дуги от "конца" промежуточных платформ к финишным платформам} for i:-l to N2 do begin read(k): {Количество труб от платформы} ka[all+i]:-k: {Количество дуг из вершины all+i} for j:-l to k do {Для каждой трубы} begin read (np); {Номер "целевой" платформы} a[all+i.j]:-np: {Дуга из Nl+2*i в a[i.j]} c[all+i.a[all+i.j]]:-MaxP; {Ee пропускная способность не ограничена} end: end: {Добавляем вершину-сток} N:-Nl+N2; {Стартовый номер финишных платформ} for i:-l to N3 do {Для каждой финишной платформы} begin ka[N+i]:-l: a[N+i.l]:-Finish ; {Одна дуга на каждую финишную платформу} c[N+i.Finish]:-l: {Hp каждой из финишных платформ - не более 1 шарика} end: close(input): end: Procedure OutputData: begin assign(output.'game.out'): rewrite(output): Max:-0: {Количество шариков из истока на стартовые платформы} for 1:-1 to N1 do Max:-Max+f[O.i]: writeln(Max); close(output): end: Листинг 1.4. Текст программы к задаче «Новогодняя вечеринка» (метод Форда-Фалкерсона) program const MaxN - MaxD - MaxV - var N К us02win2: 200: 100: MaxN+MaxD+1 : 3..MaxN: : 1..5: {Количествокоров} {Количество блюд на одну корову} продолжение
40 Глава 1 * Максимальный поток Листинг 1.4 (продолжение) D Lim Z FoodID Мах 5..MaxD: аггау [l..MaxD] of integer: integer: integer: integer: a : array [O..MaxV. l..MaxV] of Ka : array [O..MaxV] of integer: c.f.cf : array [O..MaxV. O..MaxV] of i.j. Finish : integer: ia : array [O..MaxV. l..MaxV] of Kia : array [O..MaxV] of integer: {Количество видов пищи} {Предел количества блюд на вид пищи) {Количество типов блюд, которое корова может приготовить) {Идентификатор блюда, которое корова может пригото- приготовить) {Максимальное количество блюд.которое может быть принесено на вечеринку) integer: {Граф исток-коровы-блюда-сток) {Количество ребер иэ вершины) byte: {Beca вершин в графе A} integer: {Список входящих вершин) {Количество входящих ребер) Procedure Init: begin for i:-0 to Finish do for j:-0 to Finish do f[i.j]:-O: for i:-0 to Finish do for j:-0 to Finish do cf[i.j]:-c[i.j]: for i:-0 to Finish do kia[i]:-O: for i:-0 to Finish do for j:-l to Finish do ia[i.j]:-l: for i:-0 to Finish do for j:-l to ka[i] do begin inc(kia[a[i.j]]): ia[a[i.j].kia[a[i.j]]]:-i: end: end: {Нулевой поток f(u.v)} {Построение списков входящих дуг) {По всем вершинам) {no всем исходящим дугам) {По всем вершинам) {no всем исходящим дугам) {инкремент к-ва дуг. входящих в a[i.j]} {запоминаем вершину i. как входящую в a[i.j]} Procedure InputData: begin assign( input.'party.in'); read (N.K.D); Finish :- N+D+1: for i:-0 to Finish do begin for j:-0 to Finish do a[i.j]:-0 ka[i]:-O: end: reset(input): {Прочитали N-KopoB. D-блюд. К-предел блюд) {Инициализируем пустой граф) {исток-коровы-блюда-сток}
1.8. Решения задач 41 {Добавляен вершину-исток} {Из истока - N дуг - к каждой корове} {Номер вершины, соединенной с истоком} {ee вес - максимальное кол-во блюд для коровы} {Пределы блюд каждого типа} {Добавляем вершину-сток} ka[O]:-N: for i:-l to N do begin a[O.i]:-i; c[O.i]:-K; end: for i:-l to D do read (Lim[i]): for i:-N+l to N+D do begin ka[i] :-l: a[i.l]:-Finish : {одна дуга от каждого блюда к стоку} c[i.Finish]:-Lim[i-N]; {ee вес - предел числа блюд данного типа} end: {Добавляем вершины от коров к блюдам} {Для каждой коровы} for i:-l to N do begin read(Z): ka[i]:-Z; for j:-l to Z do begin read(FoodID): a[i.j]:-N+FoodID; c[i.a[i.j]]:-l: end end: close(input): end: {Количество блюд, которые она готовит} {Для каждого такого блюда} {Читаем его номер} {Добавляем дугу от коровы к блюду} {Bec такой дуги - 1} Procedure OutputData: begin assign(output.'party.out'): rewrite(output): Max:-0: for i:-N+l to N+D do Max:-Max+f[i.Finish]; writeln(Max); close(output): end: Procedure MaxFlow: var KR. Cmin. u. v : integer; path : array [O..MaxV] of integer: Function ExistPath(var KR.Cmin:integer):boolean; {Поиск в ширину} продолжение
42 Глава 1 * Максимальный поток Листинг 1.4 (продолжение) const {c помощью очереди} WHITE - 1; GRAY - 2: var color, back : аггау [O..MaxV] of integer; Q : аггау [O..MaxV] of integer: EndQ. BegQ : integer: Find : Boolean: Procedure Put(x:integer): begin inc(EndQ): Q[EndQ]:=x: end: Procedure Get(var x:integer); begin x:-Q[BegQ]: inc(BegQ): end: begin for i--0 to Finish do color[i]:-WHITE: {Bce вершины свободны} for i:-0 to Finish do Q[i]:--1; {Bce вершины свободны} co1or[0]:-GRAY; {Начальная вершина - обработана} back[O]:-l; BegQ:-l: {Начало очереди} EndQ:-0; {Очередь пуста} KR:-0: {Количество дуг в пути - 0} Put@): {Поместить в очередь начальную вершину} Find:-false: while (BegQ<-EndQ) and not Find do {Пока очередь не пуста и путь не найден} begin Get(i); {Взять вершину i из очереди} j:-l: {номер дуги из вершины i} while (a[i.j]>0) and not Find do {пока дуги не кончились} begin if (color[a[i.j]]-WHITE) and (cf[i.a[i.j]]>0) {если вершина a[i.j] свободна} then begin Put(a[i.j]): {поставить ее в очередь} back[a[i.j]]:-i: {в вершину a[i.j] - из вершины i} color[a[i.j]]:4aRAY: {пометить вершину a[i.j]} {как использованную}
1.8. Решения задач 43 Find :- a[i.j]-Finish; end: inc(j): end: J:-l: while (ia[i.j]>-O) and not Find do begin if (color[ia[i.j]]-WHITE) (cf[i.ia[i.j]]>0) then begin Put(ia[i.j]): back[ia[i.j]]:-i color[ia[1.j]]:- Find :- ia[i.j]- end: inc(j): {взять следующую дугу) {Проход по отрицательным ребрам) {номер дуги из вершины i} {пока дуги не кончились) and {если вершина a[i.j] свободна) {поставить ее в очередь) : {в вершину a[i.j] - из вершины i} GRAY: {пометить вершину a[i.j]} {как использованную) finish: {взять следующую дугу) end: end: KR:-0: i:-Finish: Cmin:- maxint: while (i<>0) do begin Inc(KR): path[KR]:-i: i:-back[i]: if Cmin>cf[i.path[KR]] then Cmin:-cf[i.path[KR]]: end: path[kR+l]:-0; ExistPath:-Find: end: begin Init: While ExistPath(KR.Cmin) do for i:-KR downto 1 do begin u:- path[i+l]: {Восстановление пути из очереди) {Начинаем с конца) {Пока не начало) {Увеличиваем к-во дуг в пути) {Заносим номер предыдущей вершины) {Меняем текущую вершину) {Минимальный добавляемый поток) {Нулевой поток f(u.v)} {CMin - минимальный из cf(u.v)-c(u.v)-f(u.v))} {Для каждой дуги увеличивающего пути) {U->V - дуга с номером i в пути) продолжение
44 Глава 1 • Максимальный поток Листинг 1.4 {продолжение) v:- path[i]: f[u.v]:-f[u.v]+Cmin: {увеличиваем поток на минимальную величину} f[v.u]:-f[u.v]: {f(v.u)-f(u.v)} cf[u.v]:-c[u.v]-f[u.v]: cf[v.u]:-c[v.u]-f[v.u]: end: end: begin InputData; MaxFlow: OutputData: end. 1.9. Замечания по реализации Возможно, некоторым читателям покажется чрезвычайно неэкономным исполь- использование оперативной памяти в приведенных выше решениях. Ничуть не пытаясь оспаривать это, автор тем не менее отмечает следующее. 1. Современные компиляторы и операционные системы, в которых они работа- работают, снимают привычные для DOS-приложений (например, Turbo Pascal) ограничения на статический размер сегмента данных F4 Кбайт). Более того, они поощряют использование массивов данных больших размеров оптимиза- оптимизацией работы с оперативной и дисковойпамятью на уровне приложений (стра- (страничная организация памяти, динамическая подгрузка страниц, оптимальные алгоритмы вытеснения страниц и т. д.). 2. В международных олимпиадах по информатике для школьников (IOI) и сту- студенческих командных олимпиадах по программированиюдля студентов (ACM ICPC) приветствуется использование 32-битных компиляторов (Free Pascal, Delphi, GNU C и др.). 3. Основной целыо книги является максимально простое и понятное изложение идей, сводящих олимпиадные задачи к задачам на построение максимального потока и применение алгоритма Форда-Фалкерсона. Потому и были выбра- выбраны наиболее простые, а возможно, и избыточные структуры данных. 4. Как уже упоминалось, задача «Новогодние вечеринки» могла решаться с использо- использованием 32-битного компилятора FreePascal. Задача «Кубики» решается и с ис- использованием компилятораТигЬо Pascal в операционной системе DOS при пра- правильном подборе величины MaxC. Наконец, модификация авторского реше- решения задачи «Игра», чтобы оно могло быть выполнено в DOS с помощью Turbo Pascal (как это и предлагалось на белорусской республиканской олимпиаде в 2001 году) является неплохим домашним заданием для вдумчивых читателей. 5. Автор полагает, что методы оптимального использования оперативной памяти и использование сложно организованных структур хранения данных заслуживают специального раздела, посвященного рассмотрению этих и только этих вопросов.
Глава 2. Минимальное остовное дерево Данная глава посвящена теме «Минимальное остовное дерево» и имеет следую- следующую структуру. Вначале приводится формальная постановка задачи построения минимального остовного дерева заданного графа. Затем на примере решения достаточно простой задачи «Веревочный телеграф» объясняются два основных алгоритма (Прима и Крускала), применяемых для построения минимального ос- остовного дерева. Дальше приводятся полные решения трех олимпиадных задач: «Secret Milk Pipes», «Метро» и «Network». 2.1. Формальная постановка задачи Взвешенный граф служит математической моделью широкого класса задач на нахождение минимального остовного дерева. Для примера рассмотрим граф с че- четырьмя вершинами, которые пронумерованы от 1 до 4 (рис. 2.1). Рис. 2.1. Полный граф с четырьмя вершинами Пусть веса C[ij] всех ребер графа равны 1: 1 2 3 4 1 0 1 1 1 2 1 0 1 1 3 1 1 0 1 4 1 1 1 0
46 Глава 2 • Минимальное остовное дерево Примерами минимального остовного дерева для данного графа могут быть его подграфы, показанные на рис. 2.2, я-г: 1 1 1 1 1 1 2 4 2 4 б в Рис. 2.2. Минимальные остовные деревья Если же веса ребер исходного графа не равны единицам, а заданы следующим образом: 1 2 3 4 1 0 1 1 1 2 1 0 5 5 3 1 5 0 5 4 1 5 5 0 то соответствующие остовные графы примут вид, показанный на рис. 2.3, а-г (из- (изменились веса некоторых входящих в остовные графы ребер): 1 1 1 1 1 1 2 5 а б в г Рис. 2.3. Примеры остовных деревьев с весами Теперь суммы весов, приведенных выше четырех деревьев, будут 3, 7, 7, 7 соот- соответственно, и минимальное остовное дерево будет единственным (рис. 2.4): Заметим также, что в остовном дереве для графа из N вершин всегда содержится N-l соединяющих их ребер. Формально минимальное остовное дерево определяется следующим образом: пусть имеется связный неориентированный граф G=(V,E ), где V— множество
2.2. Алгоритм Прима 47 его вершин, а Е — множество его ребер. И пусть для каждого ребра графа (и, v) (соединяющего вершины и и v) задан его неотрицательный вес P(w, v). Задача нахождения минимального остовного дерева заключается в нахождении такого подмножества Гребер исходного графа, которое связывает все вершины графа G и — для которого суммарный вес всех ребер минимален. 1 1 2 4 Рис. 2.4. Минимальное остовное дерево Далее приводятся формулировки реальных олимпиадных задач, которые сводят- сводятся к нахождению минимального остовного дерева, описываются два алгоритма построения минимального остовного дерева — алгоритм Прима и алгоритм Kpyc- кала и показывается их применение для решения приведенных задач. 2.2. Алгоритм Прима Гомельская городская олимпиада, 1999 Веревочный телеграф Тимур и его друзья, приехав летом на свои старые дачи, решили устроить на время своего отдыха игру. Они организовали команду, чтобы тайно по- помогать жителям дачного городка в их повседневных делах. Дачный поселок довольно большой, и дома, в которых живут друзья Тиму- Тимура, расположены далеко друг от друга. Как быстро передавать друг другу сообщения? Как собирать ребят на совет? Тимур решил проложить веревочный телеграф, который связал бы все домики, в которых живут ребята из его команды. Всего домиков N. По карте ребята вычислили координаты каждого домика (Л7, Yi) в целых числах и выписали их на бумаге. За единицу измерения координат они взяли один метр. Однако возник вопрос: какиедомики нужно соединять веревочным телеграфом, чтобы связь была между всеми домиками (воз- (возможно, через другие домики), а общая длина всех веревок была как мож- можно меньше? Требуется написать программу, которая по координатам домиков опреде- определяла бы, какова минимальная общая длина всех веревок, соединяющих все домики между собой (возможно, через другие домики).
48 Глава 2 • Минимальное остовное дерево Порядок ввода Порядок вывода ~Ы >Ooc X1 Y1 X2Y2 XnYn где Xjoc — минимальная общая длина веревки с точностью до двух знаков в дроб- дробной части. Пример ввода Пример вывода ~~5 623.61 100 200 200 200 300 400 400 200 400 100 Ограничения: 0<W<100 -32 000<Xi,Kf<32 000 Рассмотрим домики тимуровцев как вершины графа, веревки между ними — как ребра графа, а длины веревок — как веса ребер. Теперь перед нами задача о мини- минимальном остовном дереве. Тело программы, решающей эту задачу, может выгля- выглядеть следующим образом: begin InputData; {Ввод исходных данных} MinTree: {Построение минимального остовного дерева} OutputData: {Вывод результата} end. В данном случае исходный граф — полный, то есть существует ребро между лю- любыми его двумя вершинами, поскольку по условиям задачи веревки можно про- протянуть между любыми двумя домиками. В данной задаче удобно представлять граф в виде матрицы весов ребер: D[i,j] — расстояние между вершинами i vij. Ввод данных, обеспечивающий вычисление D[i,j] по исходным данным задачи, представлен ниже и не требует комментариев. procedure InputData; begin assign (mput.'input.txt'): reset(input): readln(N); for i:-l to N do readln(x[i].y[i]): close(input):
2.2. Алгоритм Прима 49 for i:-l to N do for j:-l to N do D[1.j]:-sqrt(sqr(x[i]-x[j])*sqr(y[1]-y[j])). end: Результат работы алгоритма построения остовного дерева методом Прима будет представлен в виде массива Pred [l..N]: Pred [i] -7, то есть предком вершины i в остовном графе будет вершина;, или, другими словами, минимальное остовное дерево будет содержать ребро (iJ). У вершины 1 предка не будет (Pred[l] - 0). Ответ задачи по построенному остовному дереву получается следующим образом: procedure OutputData: var Answer : single; begin Answer:-0: for i:-2 to N do Answer:-Answer+D[i.Pred[i]]: assign (output.'output.txt'): rewrite(output): writeln(Answer:0:2); close(output); end; Рассмотрим теперь собственно алгоритм Прима для построения минимального остовного дерева. Идея алгоритма Прима заключается в следующем: пусть А — это множество ребер части остова, растущей от пустого множества ребер к мини- минимальному остовному дереву. Формирование множестваЛ может начинаться с про- произвольной вершины, называемой корневой. Для определенности в реализации в ка- качестве корневой выбирается вершина 1. На каждом шаге в множество (дерево) А добавляется ребро наименьшего веса среди ребер, соединяющих вершины из дерева А с вершинами не из дерева А. После вы- выполнения N - 1 раз таких добавлений мы получаем минимальное остовное дерево. Для эффективной организации выбора нужного для добавления ребра использу- используется очередь с приоритетами, в которой хранятся все вершины, еще не попавшие в деревоЛ. При этом каждая вершина i снабжена также приоритетом — специаль- специальной характеристикой Key[i], которая равна минимальному весу ребер, соединяю- соединяющих вершину i с вершинами из А. Более формально алгоритм Прима может быть записан следующим образом: Ставим в очередь Q все вершины исходного графа Устанавливаем для всех вершин Key[i] - бесконечность (MaxD) Заносим в дерево А вершину 1: key[l] -0 - расстояние от нее до вершин дерева А Pred[l]-0 - у корневой вершины нет предка Пока очередь Q не пуста Выбираем из очереди вершину U такую, для которой расстояние от нее до вершин из множества А минимально
50 Глава 2 * Минимальное остовное дерево Для каждой вершины V. соседней с U (то есть в графе имеется ребро (U.V)). если V находится в очереди и D[U.V]<key[V] то Pred[V]-U key[V]=D[U.V] Ниже приведена реализация алгоритма Прима: procedure MinTree. Const MaxD -lel6: {"бесконечность"} Free «0: Used -1: var Lbl : array [l..MaxN] of byte: Key : array [l..MaxN] of single: u.v : byte: Min : single. begin for i -1 to N do begin key[i]:-MaxD: Lbl[i]:-Free: end: Key[l]:-0: Pred[l]:-O: for i:-l to N Do begin Min.=MaxD: for j:-l to N Do { Все вершины в очереди} { Начнен построение с остова с 1-й вершины} { У 1-й вершины нет предка} { Для всех вершин } { Находим вершину. } if (Lbl[j]-Free) and (Key[j]<Min) { ближайшую к остову } then begin u:-j: Min:-Key[j] end: Lbl[u]:-Used: { Помечаем ее как использованную} for v:-l to N do { Для всех оставшихся в очереди вершин} if (Lbl[v]-Free) and (D[u.v]<Key[v]) then { Пересчитываем } { расстояние до растущего остова } { Переустанавливаем предка} begin Key[v]:-D[u.v]: Pred[v]:-u: end: end: end: Далее приводится полное решение задачи «Веревочный телеграф» алгоритмом Прима.
2.3. Алгоритм Крускала 51 2.3. Алгоритм Крускала Для решения приведенных далее задач «Secret Pipes» , «Метро» и «Network» удобнее применять метод Крускала, и поскольку придется его модифицировать, то рассмотрим вначале его применение в «чистом виде» для решения задачи «Вере- «Веревочный телеграф». В тело главной программы внесено существенное изменение: begin InputData: Qu1ckSortR(l.N*(N-l) div 2): MinTree_Kruska1: OutputData: end. Алгоритм Крускала работает на множестве УПОРЯДОЧЕННЫХ ПО ВОЗРАСТА- ВОЗРАСТАНИЮ весов ребер R[k]. Поэтому перед началом его работы НЕОБХОДИМО про- произвести переупорядочивание ребер по возрастанию весов. Эту работу и выполня- выполняет рекурсивная процедура QuickSortR, которой при начальном вызове передаются номера начального (номер 1) и конечного (номер M(N -l)/2 ) ребер. В данном случае рекомендуется применять алгоритм быстрой сортировки, который ввиду своей важности рассматривается отдельно в следующем пункте. Ввод данных тоже приходится немного изменить. procedure InputData: begin assign (input.'input.txt'): reset(input): readln(N): for 1:-1 to N do readln(x[1].y[i]): close(input); k:-0; for i:-l to N-1 do for j:-i+l to N do begin inc(k); R[k]:-sqrt(sqr(x[1]-x[j])+sqr<y[1]-y[j])): B[k]:-i: E[k]:-j: end; end: Здесь граф представлен как набор из ff(N -1)/2 ребер, где: ? R[k] — вес ребра (длина веревки) с номером k; ? B[k] — начальная вершина ребра; Q E[k] — конечная вершина ребра.
52 Глава 2 * Минимальное остовное дерево Ответ будет накапливаться в переменной Answer непосредственно в процессе до- добавления ребер в минимальное остовное дерево, поэтому процедура вывода ре- результата предельно упростится: procedure OutputData: begin assign (output.'output.txt'); rewrite(output); writeln(Answer:0:2); close(output); end: Процедура же построения минимального остовного дерева методом Крускала выглядит следующим образом: procedure MinTree_Kruskal: begin for i:-l to N do Makeset(i): Answer:-0: for i:-l to k do if FindSet(B[i])<>FindSet(E[i]) then begin Answer:-Answer+R[i]; Union(B[i].E[i]): end: end: Алгоритм Крускала работает следующим образом: for i:-l to N do Makeset(i): Вначале строятся N (по числу вершин) непересекающихся множеств, каждое из которых хранит номера вершин, в него входящих (вначале каждое такое множе- множество хранит только одну вершину — с номером вершины, соответствующим но- номеру множества). Далее осуществляется цикЛ по всем ребрам (B[i], E[i]) с весом R[i] (в порядке возрастания весов R[i]). Если ребро (B[i], E[i]) соединяет два разных множества (FindSet(B[i]) x FindSet(E[if), то это ребро включается в минимальный остовный граф, его вес добавляется к ответу: Answer, - Answer + R[i], а сами множества объ- объединяются (как связывающиеся данным ребром): Union(B[i],E[i]); Рассмотрим подробнее процедуры, которые реализуют работу с множеством не- непересекающихся подмножеств вершин в процессе построения минимального ос- остовного дерева. Все они работают с двумя массивами: Pred[ 1 ..N] и Rank[ 1 ..N]. Мас- Массив Pred[i] содержит номер вершины — предка. Массив Rank[i] содержит ранг вер- вершины. Понятиеранга вводится для ускорения обработки. Как уже говорилось, процедура MakeSet осуществляет начальное построение сис- системы независимых множеств вершин: procedure MakeSet(х:1ongi nt):
2.3. Алгоритм Крускала 53 begin Pred[x]:-x: {вначале вершина ссылается сама на себя} rank[x]:-0: {ранг ее равен 0} end; Рекурсивная функция FindSet{x) отвечает на вопрос: «Какому множеству из по- построенной системы подмножеств принадлежит вершина*?» Каждое из подмно- подмножеств фактически представляет собой поддерево, и функция FindSet возвращает номер вершины, которая является корнем этого поддерева. funct i on Fi ndSet(х:1ongi nt):1ongi nt: begin if x<>predfx] then Pred[x]:-FindSet(pred[x]); FindSet:-Pred[x]: end: Процедура Union(x, у) вызывается в том случае, если вершины х и у лежат в разных поддеревьях, и служит для объединения этих поддеревьев. При этом она находит корни соответствующих поддеревьев, вызывая рекурсивную функцию FindSetRBa раза (Findset(x) и FindSet(y)), а затем вызывает процедуру Link для объединения деревьев с заданными корнями: Procedure Union(x.y:longint); begin Link (FindSet(x).FindSet(y)): end: И, наконец, процедура Link(x, у) осуществляет объединение деревьев с корнями х и у и перестроение рангов вершин, входящих в эти деревья. Procedure Link(x.y:longint); begin if rank[x]>rank[y] then pred[y]:-x else begin pred[x]:-y; if rank[x]-rank[y] then inc(rank[y]); end: end: Напомним, что ранги вершин обеспечивают сокращение количества операций (и соответственно времени), затрачиваемых на обработку системы непересекаю- непересекающихся поддеревьев. Полное решение задачи «Веревочный телеграф» методом Крускала приводится далее.
54 Глава 2 • Минимальное остовное дерево 2.4. Быстрая сортировка Быстрая сортировка участка массива R от р до rr происходит следующим обра- образом: ? элементы массива R переставляются так, что бы любой из элементов R[p],..., R[q] был не больше любого из элементов Д[#+1],..., R[rr]. Эта операция назы- называется разделением ^partition). Здесь q — некоторое число в интервалер й q < rr; ? процедура сортировки рекурсивно вызывается для массивов R^),.q] и #[#+l, rr]. procedure QuicksortR(p.rr:longint); var q : longint: begin if p<rr then begin q:* Partition(p.rr); QuicksortR(p.q): QuicksortR(q+l.rr); end: end: Рассмотрим теперь подробнее реализацию функции Partition(p, rr). function Partition(p.гг-longint):longint: var i.j.z : longint: x.t • single: begin x:- R[p]: 1.-p-l: j:-rr+l: while i<j do begin repeat j:=j-l until R[j]<=x: repeat i:-i+l until R[i]>=x: if i<j then begin t:-R[i]. R[i]:-R[j]: R[j]:-t: z -B[i]: B[i]-B[j]: B[j]:-z: z-E[i]. E[i]-E[j]: E[j]:-z. end: end: Partition:*j: end:
2.5. Задача «Secret Milk Pipes» 55 В качестве «граничного элемента» принимается X = Щр\- Далее заводим два ука- указателя: i — при просмотре элементов с начала сортируемого участка, и j — при просмотре элементов с конца сортируемого участка. Пока i <j, выполняется сле- следующее: находим R[j] — первый с конца элемент меньше Ху и R[i] — первый с на- начала элемент, больший X. Если i<j, то меняем местами элементы R[i] и R[j]: t:-R[i],R[iI: = R[j];R[j]: = t; Поскольку в данной задаче нужно синхронно с перемещением весов ребер R[k] перемещать также вершины их начала и конца, выполняются также соответству- соответствующие операции: z:-B[i]: B[i]:-B[j]: B[j]:-z: z:-E[i]; E[1]:-E[j]: E[j]:-z: По завершении цикла «пока i <у» мы и получаем границу «разделения» Partition: =j. Понятно, что вызывать алгоритм быстрой сортировки нужно, передавая ему вна- вначале номера начального и конечного элементов массива, например, для задачи «Веревочный телеграф»: QuickSortR(X,N*(N-X) div 2). 2.5. Задача «Secret Pipes» USACO Open 2002, Green Division Secret Pipes Фермер Джон хочет как можно дешевле организовать свою систему рас- распределения воды, но он не хочет, чтобы его конкурент фермер Плуто мог предсказать маршруты, которые он выбирает. ФД знает, что такая задача обычно требует самого дешевого способа прокладки труб поэтому он ре- решил использовать второй по стоимости способ. Дан список всех двунаправленных труб, которые могут соединять множе- множество из WC < W< 2 000) станций с водой (каждая из которых может быть встроена в колодец). Ваша задача — найти второй из самых дешевых спосо- способов соединить насосные станции, используя не более чемР(Р < 20 000) труб с заданной стоимостью каждой трубы. Не должно быть трубы, соединяю- соединяющей станцию саму с собой. Не должно быть двух труб, соединяющих дваж- дважды одну и ту же пару станций. Гарантируется, что есть только один самый дешевый способ распределшъ во- воду, и что существует, как минимум, два способа распределить воду. Все сто- стоимости — положительные числа, помещающиеся в 16-битное целое. Водная станция идентифицируется своим номером — целым числомвдиапазоне l..U7 Ввод: Q строка 1- два разделенных пробелом целых числа, WuP, Q строки 2..P + 1 — каждая строка описывает одну трубу и содержит 3 числа, раз- разделенных пробелом, — номера станций начала и конца трубы, а также стоимость этой трубы.
56 Глава 2 * Минимальное остовное дерево Пример ввода: 57 123 234 147 2411 259 545 358 Вывод: Одна строка, содержащая целое число — вторая минимальная стоимость кон- конструирования системы распределения воды. Пример въ:*юда: 20 В данной задаче мы можем рассматривать насосные станции как вершины; тру- трубы, соединяющие насосные станции — как ребра, а стоимость труб — как веса ре- ребер. Тогда по условию задачи от нас требуется построить второе по весу остовное дерево. Прежде всего опишем ввод исходных данных. procedure InputData: begin assign (input.'secret.in'); reset(input): readln(N.P): for i:-l to Р do readln(B[i].E[i].R[i]); close(input): end: Теперь решение задачи можно обеспечить следующим образом. 1. Найдем минимальное остовное дерево. 2. «Удаляя* в цикле по очереди одно из найденных ребер минимального остов- остовного дерева и достраивая оставшийся нецельным остов до минимального ос- остовного дерева, вычисляем стоимости получающихся деревьев и запоминаем меньшую. Для нахождения минимального остовного дерева используем алгоритм Крускала: procedure MinTree_Kruska1_l: begin Answer:-0: j:-0: for i:-l to N do Makeset(i); for i:-l to Р do if (Findset(B[i])<>FindSet(E[i])) then begin inc(j); Key[j]:-i;
2.5. Задача «Secret Milk Pipes» 57 Union(B[i].E[i]): end; end; Легко заметить, что в процедуру поиска минимального остовного дерева вместо строкиувеличенияответаЛяздаег: =Answer + R[i] введенастрокашсО');#е#^]: = i, которая сохраняет в массиве Кеу номера всех ребер, включенных в минимальное остовное дерево. Тогда тело главной программы может выглядеть следующим образом: begin InputData: QuickSortR(l.P): MinTree_Kruskal_l: for i;-l to Р do NotMasked[i]:-true; CurAnswer:^naxint: for v:-l to N-1 do begin NotMasked[key[v]]:-false; MinTree_Kruskal_2; if Answer<CurAnswer then CurAnswer:-Answer; NotMasked[key[v]]:-true; end: assign (output.'secret.out'); rewrite(output); writeln(CurAnswer); close(output); end. Здесь первые три строки InputData; QuickSortR(l.P): MinTree_KruskalJ: обеспечивают построение минимального остовного дерева, где Key[i] (для i от 1 RoN- 1) — номера ребер остовного дерева. Затем отмечаем все ребра как сво- свободные : for i:-l to Р do NotMasked[i]:-true: и инициализируем поиск минимального ответа: CurAnswer:^naxint: Далее в цикле по всем ребрам, включенным в минимальное остовное дерево, for v:-l to N-1 do помечаем текущее ребро минимального остовного дерева как занятое, NotMasked[key[v]]:-false; вызываем алгоритм построения минимального остовного дерева, который учиты- учитывает, можно ли использовать при построении текущего дерева данное ребро.
58 Глава 2 • Минимальное остовное дерево MinTree_Kruskal_2; Среди получемых ответов Атшегищем минимальный. if Answer<CurAnswer then CurAnswer:-Answer; В конце цикла возвращаем «исключенное» ребро в список. NotMasked[key[v]]:-true: Модифицированный алгоритм Крускаладля поиска минимального остова на под- подмножестве ребер выглядит следующим образом: procedure MinTree_Kruskal_2: begin Answer:=0; j:=0: for i:-l to N do Makeset(i); for i:-l to Р do if (FindSet(B[i])<>FindSet(E[i])) and (NotMasked[i]) then begin Answer:-Answer+R[i]: Union(B[i].E[i]): end: end: Как легко заметить, отличие заключается в операторе сравнения, if (FindSet(B[i])<>FindSet(E[i])) and (NotMasked[i]) то есть если вершины находятся в разных поддеревьях и текущее ребро не маскиро- маскировано. Однако такое решение на тестах максимальной размерности B000 вершин, 20 000 ребер) не проходит по времени исполнения. Задумаемся: а можно ли сокра- сократить объем работы, выполняемой в процедурахMinTree_Kruskal_l и MinTree_Krus- kal__27 Да, можно — из следующих соображений. ? В обеих процедурах мы осуществляем перебор по всем ребрам/оггв1 toPdo ..., в то время как достаточно перебиратьДО НАХОЖДЕНИЯ {N - 1 )-ro ребра ми- минимального остовного дерева: while (J<>N - 1) do, где,/ — номер последнего ребра, найденного для минимального остовного дерева ? В процедуре MinTreeJKruskal_2 можно сделать несколько изменений с целью ее оптимизации: 1) начинать с остовного дерева из N - 2 ребер. Для этого достаточно в главной программе без перебора включить в минимальное остовное дерево N - 2 найденных в процедуре MinTree_Kruskal_l ребер (поочередно исключая из рассмотрения одно из найденных ребер); 2) не вызывать процедуру Union; 3) просмотр ребер из множества заданных начинать с ребра, следующего за исключенным из просмотра ребром минимального отстовного дерева; 4) оператор z/можно разбить на два, чтобы для помеченного ребра не вызыва- вызывалась рекурсивная процедура FindSet.
2.6. Задача «Метро» 59 Полный текст программы, решающей задачу «Secret Pipes», приводится в конце главы. 2.6. Задача «Метро» Белорусская республиканская олимпиада, 2002. Метро В некотором городе есть метро, состоящее из NA <N< 1000) станций и M@ ? М <i 500 000) линий, соединяющих их. Каждая линия обеспечивает проезд между какими-то двумя станциями в обе стороны. Между любой парой станций проведено не более одной линии. Сеть метро построена таким образом, чтобы с каждой станции можно было проехать на каждую (возможно, через промежуточные станции). Назовем это свойство связ- связностью метро. В связи с изобретением принципиально нового вида транспорта метро ста- стало убыточным, и его работу решили прекратить. На заседании мэрии горо- города было постановлено закрывать каждый год по одной станции, но так, что- чтобы связность метро каждый раз сохранялась. При закрытии какой-либо стан- станции линии, ведущие от этой станции к другим, естественно, тоже перестают функционировать. Задание: По введенной информации о сети метро разработать какой-либо порядок закры- закрытия станций, при котором метро всегда будет оставаться связным. Например, пусть метро выглядит так, как показано на рисунке. Тогда станции можно закрывать, например, в порядке 1,2,4,3,5. А порядок 3,1,2,4,5 — не подходит, так как после закрытия 3-й станции метро распадется на четыре не связных части. Ввод: Первая строка входного файла будет содержать числа Ми M. В следующих Мстро- ках находится информация о линиях. Каждая из этих строк содержит через про- пробел числа Ai и Bi (Ai, Bi) — две станции, которые соединяет i-я линия. Вывод: Выходной файл должен состоять из N строк. Каждая строка должна содержать одно число — номер станции. Вывести станции нужно в порядке их закрытия. Пример ввода 54 31 32 34 35 Пример вывода 1 2 4 3 5 Естественно представить станции метро вершинами графа, а линии, их соединя- соединяющие, — ребрами графа. По условиям задачи длины линий не имеют значения,
60 Глава 2 • Минимальное остовное дерево поэтому мы можем задать значения весов всех ребер равными 1. Ввод исходных данных осуществляется следующим образом: procedure InputData: begin assign (input.'input.txt'): reset(input): readln(N.P); for i:-l to Р do begin readln(B[i].E[i]): R[1]:-1: end: close(input): end: Теперь нам достаточно найти остовное дерево (минимальность не требуется, сор- сортировку вызывать нет необходимости), а затем обойти исходный граф поиском в глубину, используя только ребра, включенные в остовное дерево. Тело главной программы будет выглядеть следующим образом: begin InputData: MinTree_Kruskal: assign (output.'output.txt'): rewrite(output); for i:-l to N do Color[1]:-FREE: DFSA): close(output): end. Здесь MinTree_Kruskal- процедура построения остовного дерева, сохраняющая найденный подграф с помощью двух массивов: ? kG [U] — количество ребер, связанных с вершиной U; ? G[u,j] e v — из вершины Сребро с номером.;' ведет в вершину V. Ниже приведен ее текст: procedure MinTree_Kruska1; begin for i:-l to N do kG[i]:-O; for i:-l to N do Makeset(i): for i:-l to Р do if (Findset(B[i])<>FindSet(E[i])) then begin inc(kG[B[i]]): G[B[i].kG[B[i]]]:-E[i]: inc(kG[E[i]]): G[E[i].kG[E[i]]]:-B[i]; Union(B[i].E[1]): end: end:
2.7. Задача «Network» СП Массив Color[i] обеспечивает работу процедуры DFS — обхода графа в глубину и вывода номеров вершин в порядке «от листьев к корням». Procedure DFS(U:longint): var j : longint: begin color[U]:-USED: for j:-l to kG[U] do if Color[G[u.j]]-FREE then DFS(G[U.j]): writeln(U): end: Неформально поиск в глубину с помощью массива Со/огможно пояснить так: при заходе в очередную вершину мы выясняем, есть ли из нее ребро к неокрашенным (непосещенным) вершинам. Если есть — идем туда. Если же у текущей вершины больше нет «потомков», значит, это «листовая» вершина, и ее можно выводить в результат. Полный текст решения задачи «Метро» приводится в конце главы. 2.7. Задача «Network» NEERC, Северный четвертьфинал, 2001 Network Андрей работает системным администратором и планирует создание но- новой сети в своей компании. Всего будет Мхабов, они будут соединены друг с другом с помощью кабелей. Поскольку каждый сотрудник компании должен иметь доступ ко всей сети, каждый хаб должен быть достижим от любого другого хаба — возможно, через несколько промежуточных хабов. Поскольку имеются кабели различ- различных типов и короткие кабели дешевле, требуется сделать такой план сети (соединения хабов), чтобы максимальная длина одного кабеля была как можно меньшей. Есть еще одна проблема — не каждую пару хабов можно непосредственно соединять по причине проблем совместимости и геометри- геометрических ограничений здания. Андрей снабдит вас всей необходимой инфор- информацией о возможных соединениях хабов. Вы должны помочь Андрею найти способ соединения хабов, который удо- удовлетворит всем указанным выше условиям. Ввод: Первая строка входного файла содержит два целых числа: N — количество хабов в се- сети B < N< 1000) и М — количество возможных соединений хабов A й М < 15 000). Все хабы пронумерованы от 1 до N Следующие М строк содержат информацию о возможных соединениях — номера двух хабов, которые могут быть соединены, и длина кабеля, который требуется, чтобы соединить их. Эта длина — натураль- натуральное число, не превышающее 106. Существует не более одного способа соединить
62 Глава 2 * Минимальное остовное дерево каждую пару хабов. Хаб не может быть присоединен сам к себе. Всегда существу- существует хотя бы один способ соединить все хабы. Вывод: Сначала выведите максимальную длину одного кабеля в вашем плане соедине- соединений (это величина, которую вы должны минимизировать). Затем выведите свой план: сначала выведите Р — количество кабелей, которые вы использовали, затем выведите Р пар целых чисел — номера хабов, непосредственно соединенных в ва- вашем плане кабелями. Пример: Пример ввода 46 1 21 1 3 1 1 42 231 34 1 24 1 Пример вывода 1 4 см 1 3 23 34 Приведем краткую формализацию постановки задачи. Для заданного взвешенного графа найти такое остовное дерево, чтобы максимальный из весов ребер этого oc- товного дерева был минимальным, и вывести этот вес, количество ребер в остовном дереве и сами ребра (номера вершин, соединенных соответствующим ребром). По построению минимального остовного дерева алгоритмом Крускала получается, что оно всегда является и решением поставленной задачи. Таким образом, решение задачи сводится к нахождению минимального остовного дерева алгоритмом Крус- Крускала, а затем к нахождению максимального по весу из входящих в него ребер. Рассмотрим подробнее некоторыедетали реализации. Поскольку количества вер- вершин (до 1000) и ребер (до 15 000) таковы, что требуемые алгоритму Крускала структуры данных не поместятся в статической памяти Турбо-Паскаля, массивы начал, концов и весов ребер (В, Е и R, соответственно) размещаются в динамиче- динамической памяти. Тело главной программы выглядит следующим образом: begin InputData: {Ввод исходных данных} QuickSortR(l.M). {Сортировка ребер по возрастанию весов} MinTree_Kruskal: {Построение минимального остовного дерева} Max:=R^[Key[l]]; {Нахождение максимального ребра в ост. дереве} for i:=1 to j do if fT[Key[i]]>Max then Max:-R^[Key[i]]: OutputData: {Вывод результата в требуемом формате} enG Процедуры QuickSortR, MinTree_KruskalnoRBeprHyjbi минимальным изменениям, вызванным переходом со статических массивов В, ?, R на динамические массивы: BA, ?A, R^. Полный текст решения задачи приведен в следующем разделе.
2.8. Решения задач 63 2.8. Решения задач Листинг 2.1. Текст программы к задаче «Веревочный телеграф» (алгоритм Прима) program gg99dlt2: Const MaxN - 100: var N.i.j : l..MaxN: X.Y : array [l..MaxN] of longint: D : array [l..MaxN.l..MaxN] of single: Pred : array [l..MaxN] of byte: procedure InputData: begin assign (input.'input.txt'): reset(input): readln(N): for i:-l to N do readln(x[i].y[i]): close(input): for i:-l to N do for j:=l to N do D[1.j]:-sqrt(sqr(x[1]-x[j])+sqr(y[i]-y[j])): end: procedure OutputData: var Answer : single: begin Answer:-0: for 1:-2 to N do Answer:-Answer+D[i.Pred[i]]: assign (output.'output.txt'); rewrite(output): writeln(Answer:0:2): close(output): end: procedure MinTree: Const MaxD -lel6: Free -0: Used -1: var Lbl : array [l..MaxN] of byte: Key : array [l..MaxN] of single: продолжение
64 Глава 2 * Минимальное остовное дерево Листинг 2.1 {продолжение) u.v : byte: Min : single; begin for i:-l to N do begin key[i]:-MaxD: Lbl[i]:-Free: end: Key[l]:-0: Pred[l]-0: for i:-l to N Do begin Min:-MaxD: for j:-l to N Do if (Lbl[j]-Free) then begin u:-j Lbl[u]:-Used: for v:-l to N do if (Lbl[v]-Free) then begin Key[v]:-D[u Pred[v]:-u: end; end: end: begin InputData: MinTree: OutputData: end. { Начнем построение с остова с 1-й вершины} { У 1-й вершины нет предка} { Для всех вершин } { Находим вершину. } and (Key[j]<Min) { ближайшую к остову } : Min:-Key[j] end: { Помечаем ее как использованную} { Для всех оставшихся вершин} and (D[u.v]<Key[v]) { Пересчитываем } .v]; { расстояние до остова } { Переустанавливаем предка} Листинг 2.2. Текст программы к задаче «Веревочный телеграф» (алгоритм Крускала) program Const MaxN var N.i.j X.Y Pred. k.t.m gg99dlt2: - 100: Rank : .p.rr : : longint. array [1. array [1. longint: .MaxN] .MaxN] of of longint: longint:
2.8. Решения задач 65 R : array [0. 4950] of single: B.E : array [1..4950] of 0..MaxN: Answer.Min : single: procedure InputData: begin assign (input.'input.txt'): reset(input): readln(N). for i:-l to N do readln(x[i].y[i]). close(input): k:-0: for i:-l to N-1 do for j:-i+l to N do begin inc(k). R[k].-sqrt(sqr(x[i]-x[j])+sqr(y[i]-y[j])). B[k]:-i: E[k]:-j: end: end: procedure OutputData: begin assign (output.*output.txt'): rewrite(output). writeln(Answer.0 2): close(output). end: procedure MakeSet(x:longint): begin Pred[x]:=x. rank[x]=0: end: function FindSet(x:longint):longint: begin if x<>pred[x] then Pred[x]:-FindSet(pred[x]). FindSet:-Pred[x]: end: Procedure Link(x.y:longint): begin if rank[x]>rank[y] then pred[y]-=x else продолжение &
66 Глава 2 * Минимальное остовное дерево Листинг 2.2 (продолжение) begin pred[x];-y; if rank[x]-rank[y] then inc(rank[y]): end: end; Procedure Union(x.y:longint); begin Link (FindSet(x).FindSet(y)): end: procedure MinTree_Kruskal: begin for i:-l to N do Makeset(i): Answer:-0; for i:-l to к do if Findset(B[i])<>FindSet(E[i]) then begin Answer:-Answer+R[i]; Union(B[i].E[i]): end: end: function Partition(p.гг:longint):longint: var i.j.z : longint: x.t : single: begin x:- R[p]: 1:-p-l: j:-rr+l: while i<j do begin repeat j:-j-l until R[j]<-x: repeat i:-i+l until R[i]>-x; if i<j then begin t:-R[1]: R[i]:-R[j]: R[j]:-t: z:-B[i]: B[i]:-B[j]: B[j]:-z: z:-E[1]: E[1]:-E[j]: E[j]:-z: end: end:
2.8. Решения задач 67 Partition:-j: end: procedure QuicksortR(p.rr:longint); var q : longint: begin if p<rr then begin q:- Partition(p.rr): QuicksortR(p.q): QuicksortR(q+l.rr): end: end: begin InputData: QuickSortR(l.N*(N-l) div 2): MinTree_Kruskal: OutputData: end. Листинг 2.З. Текст программы к {$E+.N+.R+.O+} program us02ope2: Const MaxN - 2000: MaxP - 20000: Free -0: Used -1: var N P i.j Pred.Rank B. E NotMasked Key l..MaxN: l..MaxP: integer: array [l..MaxN] array [l..MaxP] array [l..MaxP] array [l..MaxN-l] задаче «Secret Pipes» of of of of integer: integer: boolean: integer: Answer.MinAnswer.CurAnswer : longint: u.v : integer: R : array[l..MaxP] of integer: procedure InputData: begin assign (input.'secret.in'): reset(input): продолжение
68 . Глава 2 • Минимальное остовное дерево Листинг 2.3 (продолжение) readln(N.P): for i:-l to Р do readln(B[1].E[i].R[i]>: close(input): end: function Partition(p.гг:1ongint):longint: var i.j.z : longint; х.у : integer: begin x.- R[p]: 1:-p-l: j:-rr+l: while i<j do begin repeat j:-j-l until R[j]<-x; repeat i:-i+l until R[i]>-x: if i<j then begin y:-R[1]: R[1]:-R[j]: R[j]:-y: z:-B[1]: B[i]:-B[j]: B[j]:-z: z:-E[i]; E[i]:-E[j]: E[j]:-z: end: end; Partition:=j: end: procedure OuicksortR(p.rr:longint); var q : longint: begin if p<rr then begin q:- Partition(p.rr); QuicksortR(p.q); QuicksortR(q+l.rr); end. end; procedure MakeSet(x:longint): begin Pred[x]:-x:
2.8. Решения задач 69 rank[x]:-O: end; function FindSet(x:1ongi nt):1ongi nt. begin if x<>pred[x] then Pred[x]=FindSet(pred[x]): FindSet:=Pred[x]: end; Procedure Link(x.y:longint). begin if rank[x]>rank[y] then pred[y]:=x else begin pred[x]:-y; if rank[x>rank[y] then inc(rank[y]): end; end; Procedure Union(x.ylongint): begin Link (FindSet(x).FindSet(y)); end; procedure MinTree_Kruskal_l: begin Answer:-0; j:=0: for i:-l to N do Makeset(i); i:-l: While J<>N-1 do begin if (Findset(B[i])<>FindSet(E[i])) then begin Answer:=Answer+R[i]; inc(j): Key[j];-i; Union(B[i].E[i]): end; inc(i); end; end; procedure MinTree Kruskal 2; л продолжение &
70 Глава 2 * Минимальное остовное дерево Листинг 2.3 (продолжение) var к : longint; FindSetConst:array [l..MaxN] of integer; begin While (J<>N-1) do begin if (NotMasked[i]) then if (Findset(B[i])<>FindSet(E[i])) then begin Answer:-Answer+R[i]: inc(j); end: inc(i) end; end: begin InputData: QuickSortR(l.P): MinTree_Kruskal_l; for i:-l to P do NotMasked[i]:-true: CurAnswer:-maxlongi nt: Mi nAnswer:-Answer: for v:-l to N-1 do begin NotMasked[key[v]]:-false; Answer:-MinAnswer-R[key[v]]: for i:-l to N do Makeset(i): for u:-l to N-1 do if NotMasked[key[u]] then begin Union(B[key[u]].E[key[u]]): NotMasked[key[u]]:-false: end: i:-key[v]+l: j:-N-2: MinTree_Kruskal_2; if Answer<CurAnswer then CurAnswer:-Answer: for u:-l to N-1 do NotMasked[key[u]]:-true: end: assign (output.'secret.out'): rewrite(output): writeln(CurAnswer): close(output): end
2.8. Решения задач 71 Листинг 2.4. Текст программы к задаче «Метро» program us02ope2; Const MaxN - 1000: МахР - 500000: MaxShortInt - 32767: Free «0: Used -1: var N : l..MaxN: Р : l..MaxP: i.j : integer: Pred.Rank.Color : array [1 В. Е R kG G .MaxN] of integer: аггау [l..MaxP] of integer: array[l..MaxP] of integer: array[l..MaxN] of integer: array[l..MaxN.l..MaxN] of integer: procedure InputData: begin assign (input.'input.txt'): reset(input): readln(N.P): for i:-l to P do begin readln(B[i].E[i]): R[1]:-1: end: close(input): end: procedure MakeSet(x:longint): begin Pred[x]:-x: rank[x]:-0: end: function FindSet(x:longint):longint: begin if x<>pred[x] then Pred[xJ:-FindSet(pred[x]): F1ndSet:-Pred[x]: end: Procedure Link(x.y:longint): продолжение
72 Глава 2 * Минимальное остовное дерево Листинг 2.4 (продолжение) begin if rank[x]>rank[y] then pred[y]*x else begin pred[x]:-y: if rank[x]-rank[y] then inc(rank[y]): end: end: Procedure Union(x.y:longint): begin Link (FindSet(x).FindSet(y)); end: procedure MinTree_Kruskal: begin for i:-l to N do kG[i]:-0: for i:-l to N do Makeset(i); for 1--1 to Р do if (Findset(B[i])<>FindSet(E[i])) then begin inc(kG[B[i]]): G[B[i].kG[B[i]]]:-E[i]: inc(kG[E[i]]): G[E[i].kG[E[i]]]:-B[i]: Union(B[i].E[i]): end: end: Procedure DFS(U:longint): vdr j . longint: begin color[U]:-USED: for j:-l to kG[U] do if Color[G[u.j]]-FREE then DFS(G[U.j]): writeln(U): end: begin InputData: MinTree Kruskal:
2.8. Решения задач 73 assign (output.'output.txt'): rewrite(output); for i:-l to N do Color[i].-FREE: DFSA): close(output): end. Листинг 2.5. program Const MaxN MaxP Type TMas PMas var N M i.j Key Pred. В. Е. Max n401dltC - 1000 - 15000 - array - ^TMas: Rank R Текст программы к задаче «Network» 1; [l..MaxP] of longint; l..MaxN: l..MaxP: integer; array [l..MaxN-l] of integer: array [l..MaxN] of integer: PMas: longint: procedure InputDat3: begin assign (input.'mput.txt'): reset(input). readln(N.M): B:=New(PMas); E:-New(PMas): R:-New(PMas): for i:-l to M do readln(B^[i].E^[i].R^[i]): close(input): end. function Partition(p.rr:longint):longint: var i.j.z : longint: x.y : longint: begin x:- R^[p]: 1:-p-l: j:-rr+l; while i<j do begin repeat j:-j-l until R^[j]<-x: repeat i:-i+l until R^[i]>-x: продолжение
74 Глава 2 * Минимальное остовное дерево Листинг 2.5 (продолжение) if i<j then begin y:-R^[i]: R^[i]:-R^[j]: R^[j]:-y: z:-B^[i]: B^[i]:-B^[j]: B^[j]:-z: z:«EA[i]: E^[i]:-E^[j]: E^[j]:-z: end: end: Partition:-j: end: procedure QuicksortR(p.rr:longint): var q : longint: begin if p<rr then begin q:- Partition(p.rr): QuicksortR(p.q): QuicksortR(q+l.rr): end: end: procedure MakeSet(x:longint): begin Pred[x]:-x: rank[x]:-0: end: function FindSet(x:1ongint):longint: begin if x<>pred[x] then Pred[x]:-FindSet(pred[x]): FindSet:-Pred[x]: end: Procedure Link(x.y:longint): begin if rank[x]>rank[y] then pred[y]:-x else begin pred[x]:-y:
2.8. Решения задач 75 if rank[x]-rank[y] then inc(rank[y]); end: end: Procedure Union(x.y:longint): begin Link (FindSet(x).FindSet(y)): end: procedure MinTree_Kruskal; begin J:-0: for i:-l to N do Makeset(i): i:-l: While J<>N-1 do begin if (Findset(B^[i])<>FindSet(E^[i])) then begin inc(j): Key[j]:-i: Union(BA[i].EA[i]): end: inc(i): end: end: Procedure OutputData: begin assign (output.'output.txf): rewrite(output): writeln(Max): write1n(J): for i:-l to J do writeln (B^[Key[i]].' '.E^[Key[i]]): close(output): end: begin InputData: QuickSortR(l.M): MinTree_Kruskal: Max:-R^[Key[l]]: for i:-l to j do if RA[Key[i]]>Max then Max:-RA[Key[i]]: OutputData: end.
Глава 3. Решение задач на деревьях и с помощью деревьев В ранее выпущенной книге автора ([2]) уже рассматривались некоторые методы решения задач на графах, в том числе метод Флойда, методДейкстры, поиск в глу- глубину, поиск в ширину. В первых двух главах этой книги рассмотрены построение минимального остовного дерева и построение максимального потока. Материал этой главы посвящен методам и средствам решения задач на деревьях и с помощью деревьев. Дерево, являясь разновидностью графа, имеет множество эквивалентных определений. Возьмем в качестве базового такое определение: «Деревом называется связный графбе.ч циклов». ПРИМЕЧАНИЕ Важно отметить также, что пустой граф (то есть граф, не содержащий ни вершин, ни ребер) также считаегся деревом. Это дополнение введено для удобства обобщения многих алгорит- алгоритмов на деревьях. В качестве аналогии хочется привесги пример определения факториала: факториал натураль- натурального числа N определяется как AH-l*2M*...iV, а 0! доопределен искусственно и равен 1. Рассмотрим основные свойства деревьев, вытекающие из приведенного опреде- определения, иллюстрируя их на примере, показанном на рис. 3.1. Рис. 3.1. Пример дерева с пятью вершинами ? Связность дерева обеспечивает наличие пути из любой вершины в любую. ? Отсутствие циклов в дереве гарантирует ЕДИНСТВЕННОСТЬ такого пути. ? Количество ребер (?) и вершин ( V) в дереве связаны соотношением Е - V- 1 (в примере 5 вершин и 4 ребра).
Решение задач на деревьях и с помощью деревьев 77 Важным классом деревьев является дерево с предопределенным корнем и уста- установленным отношением «предок-потомок» (ancestor, descendant). Такие деревья называются корневьши деревьями (rooted tree). Вершины, имеющие потомков, называются внутренними. Вершины, не имеющие потомков, называются внешни- внешними, или листьями. Например, корневое дерево может иметь вид, показанный на рис. 3.2, или вид, показанный на рис. 3.3: 4( Рис. 3.2. Пример корневого дерева, вариант 1 или о Рис. 3.3. Пример корневого дерева, вариант 2 Здесь вершины 1 и 3 — внутренние, вершины 2 и 4 — листья. Заметим, что определением корневых деревьев ЗАПРЕЩЕНО «множественное наследование». То есть, например, граф на рис. 3.4 не является корневым деревом (если считать, что направленные дуги идут сверху вниз — от вершин 1 и 3 к вер- вершине 2, от вершины 2 — к вершинам 4 и 5). 4 5 Рис. 3.4. Пример графа, не являющегося корневым деревом У корневых деревьев свойство единственности пути может быть переформули- переформулировано следующим образом: для любой вершины дерева существует единствен- единственный путь к ней из корневой вершины. В дальнейшем в этой главе мы будем употреблять название дерево, имея ввиду именно корневое дерево. Заметим, что корневые деревья активно используются в реальной жизни и программистской практике.
78 Глава 3 * Решение задач на деревьях и с помощью деревьев Очевидно, что каждый читатель, хотя бы на интуитивном уровне, многократно сталкивался с подобным отображением реалий. Прежде всего вспоминается «ге- «генеалогическое древо» («Авраам родил Исаака; Исаак родил Иакова; Иаков родил Иуду и братьев ero..>). Заметим, что, в отличие от реальности, родителем в таком дереве выбирается только отец. Другое часто употребляемое применение корневых деревьев — это оглавление кни- книги. Книга состоит из глав, главы состоят из пунктов, пункты состоят из подпунк- подпунктов. При желании или необходимости детализацию можно и продолжить, напри- например, так: подпункты состоят из абзацев, абзацы состоят из предложений, предло- предложения состоят из слов, слова состоят из букв. Обобщая такой подход, можно заметить, что дерево — удобное средство представ- представления сложной системы, с помощью которого последовательно детализируются иерархические составляющие (компоненты) такой системы. 3.1. Практические примеры деревьев В этом разделе приведены примеры отображения деревьями сложных систем. Эти примеры важны для понимания условий и решений задач, разбираемых в следу- следующих пунктах. 3.1.1. Деревья отношений 1. Организация (партия), построенная по принципу единоначалия: во главе орга- организации стоит один человек, ему непосредственно подчиняются несколько руководителей. Каждому из них подчиняются руководители более низкого уровня, и т. д. Руководителю самого низкого уровня подчиняются несколько рядовых членов организации. 2. Ученые, имеющие совместные публикации с Эрдёшем, получают «номер Эр- Эрдёша», равный 1. Ученые, имеющие совместные публикации с учеными, име- имеющие номер Эрдёша 1, получают номер Эрдёша 2, и так далее. Ученые, не име- имеющие публикаций ни с кем из тех, кто имеет установленный номер Эрдёша, получают признак infinity1. 3.1.2. Деревья попиксельного представления плоских цветных образов Деревья с четырьмя или менее дочерними вершинами применяются для свертки информации о графическом образе, представленном квадратом из Nx ^пиксе- ^пикселов (где N — степень двойки). Цвет каждого пиксела кодируется собственным чис- числом (в например, от 0 до 255) примерно так: если весь квадрат имеет один тот же цвет, то для кодирования информации используется единственное число (дерево 1 Подробности см. па страиичкс «Thc Erdos Number Projcct> http://www.oakland.edu/enp/. — При- Примеч. ред.
3.1. Практические примеры деревьев 79 с одной вершиной). Иначе квадрат разбивается на 4 равных квадратных части. Если какая-то из частей имеет единый цвет для всех пикселов в ней, то она коди- кодируется этим числом. Для всех частей, не имеющих единого цвета для составляю- составляющих ее пикселов, процедура деления квадрата на четыре меньших квадрата про- продолжается. Например, для такого образа D х 4 пиксела) 12 1.2 12 100 12 12 12 100 136 136 12 12 136 136 12 12 получается дерево, показанное на рис. 3.5: 12 136 12 12 100 100 Рис. 3.5. Дерево представления графического образа рисунка 3.1.3. Деревья представления сложных композиций трехмерных объектов BSP (Binary Space Partition) Trees — деревья двоичного разбиения пространства — используются для хранения информации о расположении объектов на трехмер- трехмерной сцене относительно обозревателя. Это полезно в процессе прорисовки объек- объектов с тем, чтобы сначала нарисовать объекты, наиболее удаленные от наблюдате- наблюдателя, затем более близкие и т. д. Алгоритм построения BSP-дерева может быть описан следующим образом. Меж- Между наблюдателем и сценой проводится секущая плоскость (не пересекающая ни одного из объектов). Тогда часть объектов окажется между наблюдателем и секу- секущей плоскостью, а часть — между секущей плоскостью и сценой. Процесс прове- проведения таких секущих плоскостей может быть продолжен. После проведения каж- каждой из них мы имеем дерево объектов, в котором иерархия внутренних вершин обозначает последовательность проведения секущих плоскостей, сами внутрен- внутренние вершины хранят информацию о секущих плоскостях, а листовые вершины включают совокупности отображаемых объектов в одной (ограниченной прове- проведенными секущими плоскостями) области пространства. 3.1.4. Деревья кодирования символов Хаффман предложил сжатие последовательности символов за счет кодирования символов переменным количеством битов: те символы, которые встречаются в этой
80 Глава 3 * Решение задач на деревьях и с помощью деревьев последовательности символов чаще, кодируются меньшим количеством битов, а символы, встречающиеся в этой последовательности реже, кодируются большим количеством битов. Для кодирования символов строится дерево, листьями которого являются пары «символ + частота встречаемости», а иерархия в дереве выстраивается так, чтобы: ? каждая внутренняя вершина имела РОВНО двух потомков; Q в первую очередь объединялись потомки, имеющие наименьшие частоты встре- встречаемости; G при объединении двух потомков их родитель (предок) получал частоту встре- встречаемости, равную сумме частот встречаемости потомков. Например, пусть в символьной строке из 100 символов встретились только сим- символы д, b, с, d, e,fco следующими частотами: Символ Частота А 45 раз В 13 С 12 D 16 Е 9 F 5 Тогда соответствующее дерево кодирования будет иметь вид, показанный на рис. 3.6. Заметим, что каждая дуга дерева нагружена цифрой 0 (дуга к левому потомку) или 1 (дуга к правому потомку). Таблица кодировки получается при анализе пути по дугам дерева от корня к листу соответствующего символа: Символ А В С D Е F Код 0 101 100 111 1101 1100 Оценим теперь степень сжатия. Исходный текст, как следует из таблицы встре- встречаемости, состоял из 100 символов (a — 45, b — 13, с — 12, d — 16, е — 9,/- 5). Если закодировать символы способом, указанным выше (на a — 1 бит, на 6, с, d — по 3 бита, на е и/— по 4 бита), то для представления исходной строки из 100 символов потребуется 224 бита: 45 х 1 + 13 х 3 + 12 х 3 + 16 х 3 + 9 х 4 + 5 х 4 - 224. При обычном (равномерном) кодировании требовалось бы минимум три бита на каждый символ, то есть всего нужно 300 битов A00 х 3). Коэффициент сжатия 300/224 = 1,34.
3.1. Практические примеры деревьев 100 / \ a:45 (b,c,d,e,f:55) o/ \ (b,c:25) (e,d,f:30) 0/ N^J о/ ^ c:12 b:13 (f.e:14) d:16 0/ \ f:5 e:9 Рис. 3.6. Дерево кодирования символов исходной строки 3.1.5. Деревья сортировки Для примера рассмотрим дерево: 10 / 5 /\ 1 7 \ 20 \ 25 / \ 22 27 21 Рис. 3.7. Пример дерева сортировки Такое дерево является деревом сортировки, поскольку для любой вершины этого дерева выполняется следующее свойство: в левом поддереве данной вершины находятся все числа, меньшие числа в самой вершине (и только они). А в правом поддереве данной вершины находятся все числа, большие числа самой вершины (и только они). Заметим также, что каждая вершина такого дерева имеет не более двух потомков. Для поиска наименьшего элемента в этом дереве потребуется анализ всего трех вершин A0, 5, 1). Для поиска наибольшего элемента — анализ четырех вершин A0, 20, 25, 27). В то же время для поиска минимального (максимального) числа в одномерном массиве из этих же девяти чисел обычным способом потребовался бы анализ всех девяти элементов. Аналогичным образом сокращается трудоемкость ответа на вопрос «имеется ли в данном дереве заданное число?». В дерево сортировки относительно несложно добавляется новый элемент с сохранением основного свойства дерева. И, нако- наконец, обходом «левое_поддерево -> корень -^ правое_поддерево» выводится по- последовательность всех чисел в порядке возрастания.
82 Глава 3 * Решение задач на деревьях и с помощью деревьев 3.1.6. Деревья сумм Один пример деревьев сумм представлен выше при описании дерева составлен- составленного для оптимального кодирования символов по методу Хаффмана, Другой при- пример, «битово-индексированное дерево» (Bit-Indexed Tree), ввиду своей сложнос- сложности объясняется далее — непосредственно при описании решения задачи «Мобиль- «Мобильные телефоны». 3.1.7. Перечислениедеревьев Особый интерес представляеттема «Перечислениедеревьев». Алгоритмы подоб- подобного вида обычно отвечают на вопросы: «сколько существует деревьев заданного типа?», «каково следующее дерево заданного типа?». Ответы на такие вопросы интересны потому, что можно сопоставить деревья реальным объектам, и тогда мы получим ответы на вопросы «сколько сущестует реальных объектов?» и «ка- «какой следующий по порядку реальный объект?». Например, в одной из задач рас- рассматриваются деревья, в которых каждая из вершин имеет РОВНОДВУХ потом- потомков или не имеет НИ ОДНОГО. Для нумерации используется следующая система:. 1. Каждой вершине назначается число — количество листьев в поддереве, кото- которое имеет эту вершину в качестве корневой. 2. Дерево кодируется последовательностью чисел, ассоциированных с корневой вершиной и всеми левыми потомками. •4 /\ *1 3 /\ *2 1 /\ 3 /\ •1 2 /\ •1 1 •1 1 Рис. 3.8. Пример бинармого дерева Так, дерево, изображенное на рис. 3.8, кодируется последовательностью чисел 7, 4,1,2,1,1,1. Символом «*» отмечены числа, вошедшие в кодовую числовую пос- последовательность. Деревья упорядочены по лексико-графическому возрастанию представляющих их числовых последовательностей. Требуется по кодовой пос- последовательности текущего дерева узнать кодовую последовательность следующего дерева. В другой задаче рассматриваются все деревья, у каждой из вершин которых мо- может быть НИ ОДНОГО, ОДИН или ДВА потомка. Они упорядочены в порядке возрастания вершин в дереве, а в случае равенства вершин в двух деревьях — в по- порядке возрастания количества вершин в левом поддереве, в случае равенства у двух
3.1. Практические примеры деревьев 83 деревьев и вершин в левом поддереве — в порядке возрастания количества вер- вершин в правом поддереве. По заданному номеру дерева требуется вывести в специ- специальном виде само дерево. 3.1.8. Представлениедеревьев в памяти компьютера tf-ичные деревья (то есть деревья, про которые заведомо известно, что они имеют не более К вершин) удобно хранить в одномерном массиве, используя индексы этого массива для выстраивания иерархии дерева. Каждой вершине дерева соот- соответствует ОДИН элемент массива. Индекс корневой вершины равен 1. Индексы вершин — потомков корневой — равны 2,3,..., К + 1. Индексы вершин — потомков вершины 1 — равны К + 2, К + 3,..., 2K + 1. Индексы вершин — потомков вершины 2 — равны 2K + 2, 2K + 3,..., ЗК + 1 и т. д. В общем случае, если вершина расположена в этом массиве на позиции f, то ин- дексыеевершин-потомковтаковы:(*- l)*K + 2,(i- l)*# + 3,...,(i- l)*K+ 1 + K Легко также найти и предка любой вершины. Пусть вершина расположена в мас- массиве на позиции i (или, говоря другими словами, вершина имеет индекс i в масси- массиве). Тогда индекс (расположение в массиве) вершины, которая является предком вершины i, есть 1 + (i div К), то есть, увеличенное на 1 частное от целочисленного деления i на К. Например, пусть К - 4. То есть в дереве каждая вершина может иметь 0, 1, 2, 3 или 4 наследника. Нам нужно узнать позицию предка вершины, находящейся в массиве на позиции 23. Вычисляем: 1 + B3 div 4) - 6. Значит, предком вершины, которая находится в массиве на позиции 23, является верши- вершина на позиции 6. Действительно, выпишем для проверки последовательно потом- потомков всех вершин до 6-й включительно: 1: 2: 3: 4: 5: 6: 2 6 10 14 18 22 3 7 11 15 19 23 4 8 12 16 20 24 5 9 13 17 21 25 Для того чтобы отметить отстутствие потомков у какой-то из вершин, соответ- ствующемуэлементу массива присваивается специальноезначение @, -1, пустая строка и т. д.). Основное достоинство такого хранения деревьев — упрощение алгоритмов обра- обработки деревьев. Основной недостаток — неэффективное использование памяти для несбалансированных деревьев, то есть, деревьев, для которых количества по- потомков от предка к предку сильно различаются. Если по ограничениям задачи массив может быть размещен в оперативной памяти, то такое представление де- дерева вполне приемлемо.
84 Глава 3 * Решение задач на деревьях и с помощью деревьев Бывают задачи, в которых такое размещение невозможно. Тогда для хранения деревьев могут быть использованы другие варианты. Например, те, которые ис- использовались ранее для хранения графов (в частности списки дуг из вершин). Приведем для примера один вариант хранения двоичного дерева (дерева, в кото- котором у каждой вершины-предка имеется не более двух потомков), использующий три массива: A[i] служит для хранения содержимого вершин (количество элемен- элементов в нем должно быть не меньше максимального числа вершин дерева для дан- данной задачи). Еще нужны два специальных массива (Left и Right) такой же размер- размерности для хранения ссылок на левого и правого наследника: Left[i] указывает ин- индекс в массиве А элемента, который является левым потомком элемента i, Right[i] — указывает индекс в массиве А элемента, который является правым потомком эле- элемента i. Один из элементов массива А объявляется корнем дерева (Root). 3.1.9. Порядок обхода деревьев Во многих задачах требуется обойти все вершины дерева. В литературе наиболее распространены три порядка обхода деревьев — нисходящий (pre-order), фланго- фланговый (in-order) и восходящий (post-order), в зависимости от того, в какой момент мы посещаем вершину-предка относительно посещения ее потомков: ? прямой, или нисходящий ^>re-ordertraversat), — такой порядок обхода, при ко- котором вначале посещается (анализируется) вершина, потом ее левое поддере- поддерево, затем ее правое поддерево; ? фланговый (in-ordertraversal) — это порядок обхода, при котором вначале по- посещается (анализируется) левое поддерево, потом сама вершина, а затем ее пра- правое поддерево; ? восходящий Q)ost-order traversat) — это такой порядок обхода, при котором вначале посещается (анализируется) левое поддерево, потом — правое подде- поддерево, и только в последнюю очередь сама вершина. 3.1.10. Организация материала и технология работы с ним Далее приводятся условия и описания решений 15 задач. Задачи разбиты на груп- группы по темам и, по мнению автора, представлены в порядке возрастания сложно- сложности. Последний раздел включает задачи для самостоятельного решения. 3.2. Задачи на основные свойства деревьев В данном разделе приведены условия и решения задач, не требующих никаких специальных знаний и навыков, кроме понимания определения деревьев и спосо- способов их хранения.
3.2. Задачи на основные свойства деревьев 85 3.2.1. Задача «Is it a tree?» NCNA, 1997 Is It a Tree? (B.IN / B.OUT / 10 с) Дерево — это широко известная структура данных, которая или пуста, или является множеством из одной или нескольких вершин, соединенных ори- ориентированными ребрами (дугами), удовлетворяющим следующим свой- свойствам: 1)существует ровно одна вершина, называемая корневой, в которую не приходят дуги; 2) в каждую из вершин, кроме корневой, приходит ровно одна дуга; 3) существует ровно одна последовательность дуг, приводящая из корня в каждую вершину. В данной задаче вам будет дано несколько описаний наборов вершин, со- соединенных дугами. Для каждого из них вы должны определить, удовлетво- удовлетворяет ли данный набор определению дерева или нет. Ввод: Ввод состоит из последовательности описаний (тестов) за которой следует пара отрицательных чисел. Каждый тест состоит из последовательности описаний дуг, за которой следует пара нулей. Описание каждой дуги состоит из пары целых чисел. Первое число определяет вершину, из которой дуга выходит, второе число определяет вершину, в которую дуга входит. Номера вершин всегда больше нуля. Вывод: Для каждого теста выведите одну из двух фраз: «Case k is a tree> или «Case k is not a tree>, гдс k соответствует номеру теста. Тесты нумеруются последовательно, начиная с номера 1. Пример ввода: 6 8 5 6 5 3 0 0 5 2 6 4 8 1 7 4 7 3 7 8 6 2 7 6 8 9 0 0 7 5 3 8 5 3 -1 6 8 5 6 -1 6 4 5 2 0 0 Пример вывода: Case 1 is a tree. Case 2 is a tree. Case 3 is not a tree. Итак, ориентированный граф задается наборами ребер. Требуется выяснить, яв- является ли этот граф деревом. При этом пустой граф (не имеющий вообще ни вер- вершин, ии дуг) считается деревом по определению. Известно, что для того, чтобы
86 Глава 3 - Решение задач на деревьях и с помощью деревьев непустой граф был деревом, необходимо и достаточно, чтобы выполнялись два свойства: ? количество дуг должно быть ровно на 1 меньше количества вершин; ? граф должен быть связным. С учетом того, что по условиям задачи во входном наборе данных может быть несколько тестовых наборов, разделяемых парой 0 0, главная программа может выглядеть так: begin assign(input.'b.in'); reset(input): assign(output.'b.out'): rewrite(output): i:-0; x:-l: while x<>0 do begin inc(i): InputData: if (Nodes<>O) and ((Edges<>Nodes-l) ог (not Connected)) then writeln ('Case '.i.' is not a tree.1) else writeln CCase ' .i.' is a tree.') end; end. Здесь в процедуре InputData вводятся данные об очередном графе. Там же одно- одновременно вычисляются величины: Nodes — количество вершин в графе, и Edges — количество дуг в графе. Если граф не пустой (Nodes<>O) и (несоответствие количеств вершин и ребер (Edges<>Nodes-l) или граф несвязный (это определяется функцией Connected)) T0 данный граф не является деревом ИНАЧЕ данный граф является деревом Рассмотрим подробнее процедуру ввода InputData. procedure InputData: var i.j. Holes : TInt: begin MaxNode:-0: for i:-l to MaxN do for j:-l to MaxN do G[i.j]:-GREAT; for i:-l to MaxN do V[i]:-0: read(x.y). Nodes:=O: Edges:-O: if х-1 then begin close(input): close(output):
3.2. Задачи на основные свойства деревьев 87 halt@): end: while (x<>0) do begin g[x.y]:-l: v[x]:-l: v[y]:-l: Max(MaxNode.x.y): inc(Edges); read(x.y) end; Holes:-0: for i:-l to MaxNode do if v[i]-0 then inc(Holes): Nodes:-MaxNode-Holes: x:-l: end: В условиях данной задачи не оговорены размерности, но анализ оригинального наборатестов показал, что можно принять 100 в качестве наибольшего возможного количества вершин. Тем не менее введены специальный целый тип {TInt - ShottInt), и константы MaxNn GREAT, изменяя которые, можно увеличивать размерность обрабатываемых данной программой графов. Основной цикл ввода очередного теста выглядит так: while (x<>0) do begin g[x.y]:-l: v[x]:-l: v[y]:-l; Max(MaxNode.x.y): inc(Edges): read(x.y) end: Здесь: a C*i у) — очередная дуга в графе из вершины х в вершину у; ? g[x, у] = 1, если есть дуга из х в у, GREA Т — в противном случае; ? v[x] *= 1, если вершина х присутствует в графе, 0 — в противном случае; ? Edges — количество обработанных дуг; ? MaxNode — встреченный максимальный номер вершины; ? Мах — процедура, выбирающая максимальное значение из трех величин Мах- Node, x, у. Условиями задачи не запрещаются такие входные данные, в которых для обозначе- обозначения вершин используется не последовательная их нумерация. Поэтому после ввода данных необходимо подсчитать Nodes — реальное число вершин в графе — как разность между максимальным встреченным номером MaxNode и количеством пропусков {Holes) Holes:-0: for i:-l to MaxNode do
88 Глава 3 * Решение задач на деревьях и с помощью деревьев if v[i>0 then inc(Ho1es): Nodes:-MaxNode-Holes; Далее необходимо обрабатывать конец ввода исходных данных: (-1 -1) if х-1 then begin close(input): close(output): halt@): end. И, наконец, необходимо в начале процедуры инициализировать граф Сотсутстви- ем всех дуг, а список вершин V— отсутствием всех вершин: for i:-l to MaxN do for j.-l to MaxN do G[i.j]:-6REAT: for i:-l to MaxN do V[i]:-0: Теперь рассмотрим функцию Connected, которая определяет, является ли исход- исходный граф связным. function Connected : boolean; var i.j.k : TInt; begin for i:-l to MaxNode do if v[i]-0 then for j:-l to MaxNode do begirt g[i.j] -1. g[j.i]:-l: end: for k:-l to MaxNode do for i:-l to MaxNode do for j:-l to MaxNode do G[i.j]:-min(G[i.j].G[i.k]+G[k.j]): Connected:*true; for i:-l to Nodes do for j:-l to Nodes do if G[i.j]=GREAT then begin Connected:-false: exit: end: end: Здесь вначале в граф добавляются «пропущенные вершины» и их дуги ко всем вершинам графа:
3.2. Задачи на основные свойства деревьев 89 for i:-l to MaxNode do if v[i]-0 then for j:=l to MaxNode do begin g[i.j]:-l: g[j.i]:-l: end: Далее с помощью алгоритма Флойда вычисляется кратчайшее расстояние от каж- каждой вершины до других: for k:-l to MaxNode do for i:-l to MaxNode do for j:-l to MaxNode do G[i.j]:=min(G[i.j].G[i.k]+G[k.j]): И, наконец, определяется связность графа: Connected:=true: for i:-l to Nodes do for j:-l to Nodes do if G[i.j]-GREAT then begin Connected:=false: exit; end. Если имеется хотя бы одна такая пара (iJ) для которой g[i,j] = GREAT, то есть вершина; не достижима из вершины i, то граф не связный. Полный текст реше- решения задачи приведен в конце главы. 3.2.2. Задача «Strategic game» Southeastern European Regional Contest, 2000 Strategic game (input.txt / output.txt / 10 c) Боб любит играть в компьютерные игры, особенно стратегические. Но ино- иногда он не находит решение достаточно быстро — и тогда сильно огорчается. Сейчас перед ним стоит следующая задача. Он должен защитить средневе- средневековый город, дороги которого образуют дерево. Бобу надо расставить ми- минимальное количество стражников в вершины так, чтобы они могли обо- обозревать все дуги. Можете ли вы ему помочь? Ваша программа должна найти минимальное количество стражников, ко- которое нужно расставить в вершиныданного дерева. Входной файл содержит несколько наборов тестов в текстовом формате. Каждый тест представляет дерево следующим образом: 1) количество вершин; 2) описание каждой вершины в следующем формате: номер_вершипы:{ко- личество_дорог) номер_вершины1 номер_вершины2 ... номер_вершины_ко- личество_дорог или номер_вершины:@). Номера вершин — целые числа от 0
90 Глава 3 * Решение задач на деревьях и с помощью деревьев до п - 1 для п вершин @ < п < 1500). Каждая дуга появляется во входном тесте ровно один раз. Например, для дерева, показанного на рис. 3.9, ответ — один стражник (в верши- вершине 1). Вывод для каждого теста должен содержать одно целое число (каждое в отдельной строке) — минимальное количество стражников для этого теста. 1 /\ 2 3 Рис. 3.9. Пример дерева для задачи «Strategic game» Пример вывода 1 2 Пример 4 0:(D 1:B) 2:@) 3:@) 5 3:C) 1:A) 2:@) 0:@) 4:@) ввода 1 2 1 0 Алгоритм решения таков: Подсчитываем степени вершин. Пока максимальная степень не равна 0 Удаляем вершину с максимальной степенью и ее ребра (попутно уменьшая степени вершин, ребра к которым удалены) Инкрементируем количество удаленных вершин Напомним, что степенью вершины называется количество связанных с ней ребер. Заметим, что в случае нескольких вершин с одинаковой максимальной степенью нужно выбирать вершину, максимально далекую от корня дерева. На рис. 3.10 и 3.11 показаны примеры, иллюстрирующие это положение. С учетом возможности нескольких тестов во входном файле тело главной про- программы может быть записано на Паскале так: begin assign(input.'input.txt'): reset(input); assign(output.'output.txt'): rewrite(output): while not EOF(input) do
3.2. Задачи на основные свойства деревьев begin InputData: Deleted:-0: while MaxPower(Node)<>O do begin inc(Deleted): Annigilate(Node): end: writeln(Deleted): end: close(input): close(output): end. 1* 2* /\ /\ 3* 4* 5* 6# /\ /\ /\ /\ 7 8 9 10 11 12 13 14 Рис. 3.10. Поиск максимума от корня к листьям (количество удаленных вершин — 6) 0* 1 2 /\ /\ 3* А* 5* 6* /\ /\ /\ /\ 7 8 9 10 11 12 13 14 Рис. 3.11. Поиск максимума от листьев к корню (количество удаленных вершин — 6) Здесь: Q InputData — процедура, обеспечивающая ввод дерева; a MaxPower — функция, находящая максимальную степень MaxPower и соответ- соответствующую вершину Node; Q Annigihte — процедура, удаляющая из дерева вершину Node со всеми ее ребрами; ? Deleted — количество удаленных вершин. Рассмотрим подробнее процедуру Annigilate: procedure Annigilate(Node:integer): begin j:-l: for i:-l to Power[Node] do
92 Глава 3 * Решение задач на деревьях и с помощью деревьев begin while Power[G[Node.j]]-0 do inc(j): dec(Power[G[Node.j]]): inc(j) end: Power[Node]:-0: end: Исходное дерево представлено в данной задаче следующим образом: Q Powet{i] — степень вершины i ? G[i,j] — номер вершины, в которую есть ребро из вершины i (для Bcexj от 1 до Power[i]) Процедура Annigilate сначала уменьшает на 1 степени всех вершин, связанных ребрами с текущей, а затем обращает в 0 степень самой текущей вершины. Процедура InputData выглядит несколько громоздко. Это связано с неудобным для вводаформатом исходныхданных, когдаприходится «выковыривать» нужные данные из текстовой строки. Кроме того, в качестве разделителей данных в ори- оригинальных тестовых примерах использовались как пробелы, так и символы табу- табуляции. Полный текст решения приведен в конце главы. 3.2.3. Задача «Оппозиция» ХШВсероссийская олимпиада школьников по информатике, 2001 Оппозиция (org.in / org.out / 5 с) В некоторой стране полиция выявила разветвленную сеть оппозиционной партии. Партия сильно законспирирована и состоит из рядовых членов и ру- руководителей различных уровней. Во главе партии стоит один главный ру- руководитель — лидер партии. До начала арестов приказ лидера может быть до- доведен до любого члена партии. Все члены партии пронумерованы от 1 до № Каждый член партии знает только своего непосредственного руководителя (ровно одного) и своих непосредственных подчиненных (руководитель не знает подчиненных своего подчиненного и наоборот). Естественно, что с на- началом арестов членов партии она распадется на мелкие, не связанные друг с другом группы. Например, с арестом члена партии № 2 партия развалива- разваливается на 4 группы. Полицмейстер уверяет, что группа, состоящая из менее чем Лчленов партии, идеологически вырождается и не представляет угро- угрозы для государства. Стремясь не уронить престиж страны в глазах мирового общественного мнения, полицмейстер поставил задачу произвести минимальное количе- количество арестов членов партии так, чтобы от нее остались только йдеологиче- ски вырождающиеся маленькие группы. Требуется написать программу, которая бы по входным данным, описыва- описывающим структуру подпольной партии, выводила количество арестов и но- номера членов партии, которых нужно арестовать.
3.2. Задачи на основные свойства деревьев 93 Ввод: Входной файл с именем org.in содержит три строки. В первой записано число KA <K< 10 000), во второй строке — число N A <N< 10 000), определяющее коли-чество членов партии. Третья строка содержит набор из N - 1 числа. В этой строке для каждого члена партии, кроме лидера, задается номер его непосредственного руководителя. Номер руководителя всегда меньше, чем но- номер подчиненного. При этом первое число задает номер руководителя второго члена партии, второе — третьего и так далее. Числа в строке разделяются одним пробелом. Вывод: Выходной файл с именем org.out состоит из двух строк. В первую строку необхо- необходимо поместить количество арестов, а во вторую — номера членов партии, подле- подлежащих аресту. Эти номера разделяются одним пробелом. При наличии несколь- нескольких решений выведите одно из них. 3 /\ 8 6 4 5 7 /1\ \ /\ 9 10 11 13 12 14 Рис. 3.12. Пример дерева для входного файла Пример ввода Пример вывода 3 4 14 6 27 8 1122323666747 Математическая постановка задачи выглядит следующем образом: в заданном дереве удалить минимальное количество вершин (вместе с инциндентными ребрами) так, чтобы в получившемся графе не оказалось связных компонент с числом вершин К или более. Алгоритм решения (похожий на алгоритм решения предыдущей задачи) таков: Подсчитываем степени вершин. Пока максимальная степень >- К-1 Удаляем вершину с максимальной степенью и ее ребра (попутно уменьшая степени вершин, ребра к которым удалены) Инкрементируем количество удаленных вершин Запоминаем номера удаленных вершин Также по сравнению с предыдущей задачей существенно упрощен ввод дерева, а в выводе надо вывести не только количество удаленных вершин, но и их номера. Полный текст решения задачи приведен в конце главы.
94 Глава 3 * Решение задач на деревьях и с помощью деревьев 3.2.4. Задача «Erdos Numbers» NWERC, 2000 Erdos Numbers (input.txt / output.txt / 10 с) Венгерский математик Пал Эрдёш A913-1996) был не только одним из са- самых сильных математиков XX века, но и одним из самых известных. Он часто публиковал свои работы в соавторстве с другими учеными, и каждый математик считал за честь стать соавтором Эрдёша. Однако мало кому выпадала честь стать соавтором работ Эрдёша, поэтому многие были рады, если им удавалось опубликовать совместную работу с кем-то, кто был соавтором Эрдёша. Так появилось понятие «номер Эрдёша». Автор, который имел совместные с Эрдёшем публикации, получал номер Эрдёша 1. Автор, который не имел совместных публикаций с Эрдёшем, но имел совместную публикацию с кем- то, кто имел номер Эрдёша 1, получал номер Эрдёша 2, и т. д. Сегодня по- почти каждый хочет знать, каков его номер Эрдёша. Ваша задача — написать программу, которая вычислит номера Эрдёша для заданного множества ученых. Ввод: Первая строка ввода содержит количество сценариев. Ввод для каждого сцена- сценария состоит из списка описаний статей и списка имен. Он начинается со строки Р N (Р и N — натуральные числа). Далее следует Р строк, содержащих описания ста- статей. Каждая статья описывается в одной строке следующим образом: Smith. M.N.. Martin, G.. Erdos. P.: Newtonian forms of prime factors matrices Вы можете предполагать, что авторами одной статьи будут не более 20 авторов, а полное описание статьи всегда короче, чем 2 000 символов. За списком статей следуют Л^строк списка имен авторов, по одному в строке, для которых вы должны вычислить номера Эрдёша. Вывод: Для каждого сценария вы должны вывести строку, содержащую текст «Scenario i» (где i — номер сценария) и имена авторов вместе с их номерами Эрдёша для всех авторов из заданного списка имен. Авторы должны выводиться в том же порядке, в котором они появились в списке имен во вводе. Номера Эрдёшадолжны вычис- вычисляться по заданному списку статей этого сценария. Автор, для которого невоз- невозможно вычислить номер Эрдёша (то есть не имевший отношения к работам Эрдё- Эрдёша через заданный список статей), получает номер Эрдёша «infinity». Пример ввода: 2 1 1 Hahn. P.M. My hat is the best Hahn. P.M.
3.2. Задачи на основные свойства деревьев 95 4 3 Smith. M.N.. Martin. G.. Erdos. P.: Newtonian forms of prime factor matrices Erdos. P.. Reisig. W.: Stuttering in petri nets Smith. M.N.. Chen. X.: First oder derivates in structured programming Jablonski. T.. Hsueh. Z.: Se1fstabilizing data structures Smith. M.N. Hsueh. Z. Chen. X. Пример вывода: Scenario 1 Hahn. P.M. infinity Scenario 2 Sm1th. M.N. 1 Hsueh. Z. infinity Chen. X. 2 Задача решается поиском в ширину от СПЕЦВЕРШИНЫ. Заметим, что основная сложность в этой задаче — организация ввода исходных данных. С учетом того, что по условиям задачи исходные данные могут содержать указанное число тес- тестов, тело главной программы может выглядеть так: begin Authors[l]:-'Erdos. P.': assign(input.'input.txt'): reset(input): assign(output.'output.txt'); rewrite(output); readln(NT); for t:-l to NT do begin writelnCScenario '.t): readln(P.N): a:-l: for i:-l to Р do GetPaper; BuildErdosNumbers: for i:-l to N do GetAuthorPrintResult: end: close(input): close(output): end. То есть циклом по числу тестов: О вводим все статьи (процедура GetPaper), по ходу ввода статей пополняем граф отношений соавторства; ? для всех вершин графа строим номера Эрдёша (уровни вершин относительно вершины Эрдёша), это делается в процедуре BuildErdosNumbers поиском в ши- ширину от вершины ЭрдёшаA); ? вводим авторов и выводим ответы для каждого из них.
96 Глава 3 * Решение задач на деревьях и с помощью деревьев В программе использованы следующие структуры данных: ? G — граф отношений соавторства; ? Power — степени вершин; ? Authors — список фамилий авторов; ? СА — номера авторов, написавших текущую статью; ? Erdos — номера Эрдеша для всех авторов. Полный текст решения задачи приведен в конце главы. 3.2.5. Задача «Closest Common Ancestor» Southeastern European Regional Contest, 2000 Closest Common Ancestors (input.txt / output.txt / 10 c) Напишите программу, которая возьмет иа входе корневое дерево и список пар вершин. Для каждой пары (и, v) программа должна определить бли- ближайшего общего предка вершин и и v в этом дереве. Ближайший общий предок вершин и и v — это такая вершина w% которая яв- является предком для обоих вершин и и v и имеет наибольшую глубину в дереве. Вершина может считаться своим предком. Например, для дерева, показанно- показанного на рис. 3.13, вершина 2 является ближайшим общим предком вершин 2 и 5. Набор данных, который нужно прочитать из текстового файла, начинается с описания дерева, заданного в виде: количество_вершин вершина:(количество_потомков) потомок1 потомок2 ... потомокК 4 \ Рис. 3.13. Пример дерева для задачи «Closest Common Ancestor» Вершины представляются целыми числами от 1 до N. За описанием дерева следует описание пар вершин в виде количество_пар (u v) .. (х у) Входной файл содержит множество тестов (не менее одного). Заметим, что разделители (знаки табуляции, пробелы и переводы строк) могут встречать- встречаться в любом месте входного файла. Для каждого ближайшего общего предка в отдельной строке (в порядке возрастания ближайших общих предков) про- программа должна вывести его и количество пар, для которых он был ближай- ближайшим общим предком в виде: предок:количество_раз
3.2. Задачи на основные свойства деревьев 97 Пример для графа, показанного на рис. 3.13: Пример ввода Пример вывода ~ъ гл 5:C) 1 4 2 5:5 1:@) 4:@) 2:AK 3:@) 6 A 5) A4) D 2) B 3) A3) D 3) Математическая постановка задачи такова: в заданном дереве найти ближайшего общего предка для каждой пары вершин, указанных на входе. Решение заключается в построении всех предков для каждой из вершин и на- нахождении ближайшего общего предка (по определению дерева, предок всегда есть, в частности одна из двух вершин пары может быть общим предком). Кро- Кроме того, на входе задается множество запросов о предках и требуется интеграль- интегральный ответ: сколько раз каждая вершина была найдена в качестве ближайшего общего предка. Выводить предлагается только вершины, которые хоть раз по- побывали ближайшим общим предком. С учетом вышеизложенного и множествен- множественности тестов во входном файле тело главной программы может выглядеть сле- следующим образом: begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'): rewrite(output); while not EOF(input) do begin InputTree: InputProcessVerticePairs: OutputResults; end; close(input); close(output): end. Здесь: ? lnputTree — процедура ввода дерева. Заметим, что при вводе строится инверсный граф (то есть для каждой вершины указывается ее единственная предшествен- предшественница); ? InputProcessVerticePairs — процедура, которая вводит очередную пару вершин, на- находит для этой пары ближайшего общего предка, вызывая процедуру FindCCA,
98 Глава 3 * Решение задач на деревьях и с помощью деревьев инкрементирует в массиве А количество раз, когда соответствующая вершина была ближайшим общим предком; ? OutputResults — процедура форматированного вывода номеров вершин и ко- количеств раз, когда она была ближайшим общим предком. Заметим, что ввод исходных данных выполняется аналогично тому, как это де- делалось в задаче из пункта 2 данной главы. Рассмотрим подробнее процедуру FindCCA procedure FindCCA(x.y:integer); var i : integer: begin for i:=1 to N do begin Ax[i]-0: Ay[i]:-0: end; Ax[x]:-1: Ay[y]:-1: while (Power[x]-l) and (Ax[g[x.l]]<>l) do begin x:-g[x.l]; Ax[x]:-1: end: while (Power[y]-l) and (Ay[g[y.l]]<>l) do beginy:-g[y.l]: Ay[y]--1; end: 1:-1: while (Ax[i]<>l) or (Ay[i]<>l) do inc(i); inc(A[i]); end: Здесь: a Ax — массив предков вершины лг, ? Ax[i] - 1, если вершина i является предком вершины х; ? Ау — массив предков вершины у; ? Ay[i] - 1, если вершина i является предком вершины у\ ? А — массив для хранения найденных ближайших общих предков (вершина i была ближайшим общим предком A[i] раз). В Ax[x] и Ax[y] вписываются единицы по мере последовательного продвижения вверх по дереву. A[i] инкремеитируется, если Ax[i] и Ay[i] одновременно равны 1. Полный текст решения задачи приведен в конце главы. 3.3. Задачи на представление образов В данном разделе приведены условия и решения двух задач на представление обра- образов. Первая задача, «Unscrambling Images», посвящена свертке попиксельных иЗоб- ражений, а вторая, «BSP Trees», связана с представлением деревьями сложных трехмерных сцен.
3.3. Задачи на представление образов 99 3.3.1. Задача «Unscrambling Images» Pacific Northwest Programming Contest, 1999 Unscrambling Images (D.txt / вывод на экран / 5 с) Деревья квадрантов широко используются для компактного кодирования цифровых образов. Пусть мы имеем изображение NxN пикселов (где N— степень числа 2, 1 <N< 16). Тогда оно кодируется следующим образом: Начинаем с дерева квадрантов, имеющего ровно 1 узел — корень, и ассоциируем эту вершину со всем образом целиком — регион Nx Мпикселов. Затем рекурсивно вы- выполняем следующие действия: 1) если цвет каждого пиксела в регионе, ас- ассоциированном с текущей вершиной, имеет значение р, то вершина стано- становится листовой и с ней ассоциируется значениер; 2) иначе к этой вершине добавляются четыре вершины в качестве потомков. Ее регион разбивается на четыре одинаковые квадратные части (квадранты), и каждый квадрант становится регионом соответствующей вершины-потомка. Алгоритм рекур- рекурсивно применяется к каждому потомку. Когда процесс завершается, мы получаем дерево квадрантов, в котором каж- каждая внутренняя вершина имеет четыре потомка. С каждым листом ассоци- ассоциирована величина, представляющая цвет всех пикселов соответствующей части изображения. В качестве исходного изображения (размером 8 х 8) может быть, напри- например, такое: 12 12 12 100 100 100 100 100 12 12 12 100 100 100 100 100 136 136 12 12 100 100 100 100 136 136 12 12 100 100 100 100 136 136 0 0 48 48 100 100 136 136 0 0 48 48 100 100 0 0 0 0 48 48 48 48 0 0 0 0 48 48 48 48 Предположим, что потомки (слева направо) представляют соответственно квадранты: верхнийлевый, верхний правый, нижнийлевый, нижний правый. Чтобы было легче идентифицировать вершину в дереве квадрантов, мы на- назначим каждой вершине номер в соответствии со следующими правилами: 1) корневая вершина получает номер 0; 2) если номер вершины k, то ее потомки (слева направо), получают номера 4?+1,4? + 2,4? + 3,4? + 4. Образы, закодированные деревьями квадрантов, могут быть зашифрованы с помощью паролей так: после того как определены подрегионы, потомки переупорядочиваются. Переупорядочение может различаться для каждой вершины, но оно полностью определяется паролем и номером вершины.
100 Глава 3 * Решение задач на деревьях и с помощью деревьев К несчастью, некоторые люди используют один и тот же пароль для коди- кодирования разных образов. В этом случае, увидев результат кодирования специально подобранного тестового образа, можно легко раскодировать любой образ, закодирован- закодированный тем же паролем. В этом тестовом образе каждый пиксел имеет различ- различное значение цвета — от 0 до N2- 1, размещенные слева направо, сверху вниз в порядке возрастания значения цвета. Вам дан результат кодирования такого тестового образа. Напишите программу, которая, используя его, сможет раскодировать лю- любой образ, закодированный тем же паролем. Ввод: Во вводе задается множество тестов. Первая строка содержит их количество. Каж- Каждый тест начинается со строки, содержащей число N, за которой следует описание дерева квадрантов — результат кодирования тестового образа, а затем еще одно дерево квадрантов — оно задает образ, который требуется раскодировать. Описание каждого дерева квадрантов начинается с положительного целого чис- числа А/, обозначающего количество листьев в дереве. Следующие т строк имеют вид k с Это означает, что вершина k является листовой, а ее цвет — с. Вершины не встречаются на вводе, если они являются внутренними или их нет в дереве вообще. Вы можете предполагать, что все значения цветов находятся в диа- диапазоне от 0 до 255 включительно и что каждое четверичное дерево — корректный результат кодирования по алгоритму, описанному выше. Вывод: Для каждого теста выведите его номер, пустую строку и цвета пикселов в декоди- декодированном образе по одной строке пикселов в строке текста. Выводите цвета в че- четырех колонках с выравниванием по правой стороне. Добавьте пустую строку между тестами. Пример ввода Продолжение ввода Пример вывода Case 1 253 40 123 23 Case 2 10 10 20 20 10 10 20 20 41 42 30 30 43 44 30 30 3 2 4 1 3 22 30 4 1 4 1 23 2 123 3 253 4 40 4 16 58 69 7 13 8 12 90 104 11 1 125 132
3.3. Задачи на представление образов 101 Пример ввода Продолжение ввода Пример вывода 4 1 6 58 69 7 13 8 12 90 104 11 1 125 132 14 3 157 166 17 10 18 11 19 15 20 14 7 2 10 3 20 4 30 5 41 6 42 7 44 8 43 14 3 157 16 6 17 10 18 11 19 15 20 14 10 2 255 4 140 5 40 6 40 7 42 8 41 13 30 14 30 15 28 16 28 Case 3 255 255 30 30 255 255 28 28 40 40 140 140 41 42 140 140 Сокращенно постановка задачи заключается в следующем: по заданному зашиф- зашифрованному изображению и ключу к расшифровке построить расшифрованное изображение. В табл. 3.1. указаны подрегионы, построенные по приведенной выше таблице данных. Таблица 3.1. Подрегионы пикселов 12 12 12 100 100 100 100 100 12 12 12 100 100 100 100 100 136 136 12 12 100 100 100 100 136 136 12 12 100 100 100 100 136 136 0 0 48 48 100 100 136 136 0 0 48 48 100 100 0 0 0 0 48 48 48 48 0 0 0 0 48 48 48 48 Дерево, представляющее такой образ, будет иметь вид, показанный на рис. 3.14.
102 Глава 3 • Решение задач на деревьях и с помощью деревьев 12 136 0 0 0 48 48 100 48 12 12 100 100 Рис. 3.14. Дерево образа 16 х 16 пикселов Это дерево квадрантов, каждая вершина которого имеет не более четырех потом- потомков. Для хранения полного такого дерева в памяти достаточно иметь одномерный массив, имеющий 340 элементов. Элементы 1-4 хранятдереводля п - 2 (рис. 3.15). 1 3 4 Рис. 3.15. Дерево для n = 2 Элементы 5-20 хранят дерево для п e 4 (рис. 3.16). 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Рис. 3.16. Дерево для n = 4 Элементы21-84хранятдереводляя = 8,иэлементы85-340хранятдереводляя- 16. При этом соответствие двумерным изображениям выглядит следующим образом: n = 2 1 2 34 n = 56 78 13 15 4 9 11 14 16 10 12 17 19 18 20 n = 21 23 29 31 8 22 25 26 ... 24 27 28 ... 30... 32... n = 16 85 86 89 87 88 91 93 94 ... 95 96 ... 90... 92... Для обхода дерева квадрантов используется алгоритм вида: Безусловный цикл Если можно. то идем вниз. (j=4*i+l) иначе - если можно, то идем вправо, (j-i+l)
3.3. Задачи на представление образов 103 иначе - если можно. то идем вверх, (j-i div 4) иначе, конец Здесь i — индекс в массиве текущей вершины,.;' — индекс в массиве следующей вершины. В данной задаче дело осложняется еще тем, что образ был зашифрован, а для его расшифровки дается специальный ключ. Для иллюстрации рассмотрим второй тест из примера ввода. Зашифрованный образ задается так: 7 2 10 3 20 4 30 5 41 6 42 7 44 8 43 Соответствующее дерево образа имеет вид, показанный на рис. 3.17. 10 20 30 41 42 43 %4 Рис. 3.17. Дерево зашифрованного образа Зашифрованный образ имел вид: 41 42 10 10 44 43 10 10 20 20 30 30 20 20 30 30 Теперь ключ к расшифровке задан: 4 16 5 8 6 9 7 13 8 12 9 0 10 4 11 1 12 5 13 2 14 3
104 Глава 3 * Решение задач на деревьях и с помощью деревьев 15 7 16 6 17 10 18 11 19 15 20 14 Для расшировки рассмотрим два массива. А 8904 131215 231011 7 61514 В 4142 10 10 44 43 10 10 20 20 30 30 20 20 30 30 По условиям задачи массив расшифрованного изображения строится по ним сле- следующим образом: находим самый маленький элемент в массиве A@) и переносим элемент A0) из такой же позиции массива В в начальную позицию массива С и так далее по возрастанию значений в массиве Л. Построенный таким образом массив С будет иметь вид: 10 10 20 20 10 10 20 20 4142 30 30 43 44 30 30 При программировании этого фрагмента выгодно в момент считывания ключа переупорядочивать его так, чтобы сразу получать позицию, с которой нужно брать элемент из В для занесения в очередную позицию массива С. Рассмотрим детальнее реализацию решения. С учетом обработки нескольких те- тестов в одном входном файле тело главной программы может выглядеть так: begin assign(input.'D.txt'): reset(input); read1n(NT): for t:-l to NT do begin InputData: OutRI; end: close(input); end. Здесь: ? lnputData — процедура ввода зашифрованного образа и ключа к расшифровке. Одновременно с вводом частей образа сразу формируется представление в па- памяти зашифрованного образа; {Количество тестов} {Ввод и формирование образа} {Раскодирование и вывод}
3.3. Задачи на представление образов 105 ? OutRI — процедура вывода расшифрованного образа (в ней же одним операто- оператором осуществляется расшифровка). Рассмотрим подробнее некоторые фрагменты процедуры InputData. Запись в мас- массив 77 порядка расшифровки при считывании эталонного кода осуществляется так: for i:-l to М do begin readln(x.y): TI[y+G[N2]+l]:-x; {Кодирование эталонного образа} end: «Разворачивание» зашифрованного изображения в массив RI в памяти делается так: for i:-l to S do begin readln(SiP.SiC); {Читаем зашифрованный образ} if SiP>G[N2] then RI[SiP]:-SiC {Выставляем введенный цвет листу} else Fi11SubTree: {Закрашиваем все поддерево} end: При этом для заполнения целого поддерева одним цветом вызывается процедура FillSubTree: procedure FillSubTree: {Обход четверичного дерева} var i.j : integer; begin i:-SiP: while 4*i+l <- G[N] do i:-4*i+l; {Спускаемся к листьям} while true do begin for j:-0 to 3 do RI[i+j]:-SiC: {Закрашиваем 4 листа} inc(i.4); if ((i div 4) < G[SiP div 4]) and {Есть куда наверх ?} ((i div 4) > SIP) then i:- (i div 4) {Подымаемся вверх} else exit: {Выходим} end: end: Эта процедура осуществляет обход «четверичного» дерева изображения и при достижении листьев этого дерева заносит номер соответствуюшего цвета в мас- массив образа в памяти. Полный текст решения задачи приведен в конце главы.
106 Глава 3 * Решение задач на деревьях и с помощью деревьев 3.3.2. Задача «BSP Trees» East CentralNorth America Programming Contest, 2000 BSP Trees (lnput.txt / output.txt / 10 c) При прорисовке сцен с множеством объектов на экране очень важен поря- порядок, в котором объекты прорисовываются. В общем случае чем дальше объект от наблюдателя, тем раньше он должен быть нарисован, чтобы ближ- ближние объекты рисовались поверх дальних. Если два объекта не перекрыва- перекрываются, то порядок прорисовки не существен. Деревья двоичного разбиения пространства (BSP-деревья) — одна из структур данных, которые исполь- используются для упрощения представления порядка следования объектов. Это делается следующим образом. Предположим, что экран лежит в плоскости XY и в его центре находится основание оси Z, а сама ось 2уходит от наблю- наблюдателя, смотрящего на экран. Пусть для простоты наблюдатель находится на оси Z в точке «минус бесконечность». Предположим, что все объекты лежат «за экраном», то есть для всех них z > 0. BSP-дерево строится прове- проведением серии плоскостей параллельно плоскости XY. Первая плоскость делит пространство на два региона — регион, содержащий наблюдателя, и регион, не содержащий наблюдателя. Мы разделяем все объекты в прос- пространстве на два подмножества — те объекты, которые попали в один реги- регион с наблюдателем, и те объекты, которые не попали в один регион с на- наблюдателем. Понятно, что все объекты, попавшие в один регион с наблюда- наблюдателем должны прорисовываться после всех объектов, которые не попали в один регион с наблюдателем. В данный момент BSP-дерево может рас- рассматриваться как дерево, имеющее корень и двух потомков. Каждый пото- потомок содержит объекты соответствующего множества. Теперь мы можем добавить вторую плоскость, которая разделит пространство опять. Мы раз- разделяем каждое из двух разбиений на два, получая тем самым четыре разби- разбиения, и в результате BSP-дерево будет иметь три уровня с разбиениями в ли- листьях. Заметим, что часть этих разбиений могут содержать несколько объек- объектов, а некоторые могут не содержать ни одного. Этот процесс продолжается до тех пор, пока каждое разбиение не станет содержать не более одного объекта или пока не будет проведено предопределенное заранее количество секущих плоскостей. Если BSP-дерево построено корректно, то простой обход дерева даст вам порядок, в котором нужно прорисовывать объекты. Ввод: Первая строка содержит положительное число N< 20 — количество объектов на сцене. Все объекты, полагаются лежащими в одной плоскости. Следующие Мстрок содержат описание этих объектов в виде m xl zl x2 z2 ... xmzm, где т — количество вершин объекта, а оставшиеся величины — вершины пересе- пересечения объекта с плоскостью ZX. У каждого объекта от 3 до 6 вершин, и они имеют идентификаторы Л, В, С,... в порядке ввода. Далее следует строка с положитель-
3.3. Задачи на представление образов 107 ным числом P<> 10, обозначающим количество плоскостей, использованных для создания BSP-дерева. Последние Р строк входа содержат описание каждой плос- плоскости в виде xl zl x2 z2, представляющее две точки линии пересечения этой плоскости с плоскостью ZX. Вы можете считать, что нет линий, пересекающих объекты (их ребра и вершины), и что нет плоскостей, параллельных оси Z. Все координаты — целые числа. Вывод: Вывод состоит из одной строки, содержащей идентификаторы объектов в поряд- порядке, в котором они должны прорисовываться в соответствии с построенным BSP- деревом. В случае если разбиение содержит один или более объектов, выводите их идентификаторы в алфавитном порядке. Пример ввода (продолжение ввода) Пример вывода ~~7о Ю BCGEJFIHDA 3 65 5 66 5 65 6 159 165 -131 -177 3 65 123 66 123 65 124 -153 -192 -197 158 3 122 176 123 176 122 177 -77 -86 -98 30 3 56 23 57 23 56 24 -177 59 146 63 3 11 49 12 49 11 50 192-117 92 43 3 167 111 168 111 167 112 121 -67-62-134 3 57 123 58 123 57 124 41 -81 130 196 3 130 6 131 6 130 7 95 -185 -89 154 3 100 85 101 85 100 86 -163 -179 93 175 3 11 28 12 28 11 29 113 41 -92-28 Вначале приведем краткое изложение постановки задачи. BSP-деревья применя- применяются для представления пространственных сцен. Чем дальше изображаемый объект от экрана, тем раньше он должен быть нарисован. BSP-дерево строится с помощью серии плоскостей, разбивающих пространство на подпространства. Первая плос- плоскость делит пространство на два региона — ближний к обозревателю, и дальний от него. Соответственно все подлежащие изображению объекты делятся на два региона. Все объекты в ближнем регионе должны быть нарисованы ПОСЛЕ всех объектов дальнего региона. Затем проводим следующую плоскость, она делит каж- каждый из имеющихся регионов на подрегионы. Продолжаем процесс для всех име- имеющихся плоскостей. Если регион содержит не более одного объекта, то он далее не делится. Таким образом получается BSP-дерево пространственной сцены. В данной задаче предлагается упрощенный вариант — построить BSP-дерево для плоских объектов. Задано некоторое количество объектов (многоугольников) на плоскости и некоторое количество прямых, разделяющих эти объекты на подмно- подмножества. Гарантируется, что ни один объект не имеет общих точек ни с одной пря- прямой. Кроме того, все прямые не параллельны оси ординат. Объекты обозначаются латинскими буквами Л, fi, С,... (всего объектов не более 20, а прямых — не более 10). Нужно вывести последовательность порядка отрисовки объектов.
108 Глава 3 * Решение задач на деревьях и с помощью деревьев Идея решения: по условиям задачи прямые не пересекают фигуры и, следователь- следовательно, для определения положения всей фигуры относительно прямой достаточно анализировать положение любой (например, первой) ее точки. Разбиваем мно- множество всех первых точек фигур на подмножества по разные стороны от очеред- очередной прямой. В левое подмножество включаем точки, лежащие выше прямой. После завершения процессадля всех прямых выводим получившиеся листы дерева в по- порядке обхода слева направо. Рассмотрим подробнее реализацию этого алгоритма. Тело главной программы выглядит так: begin assign(input.'input.txt*): reset(input): assign(output.'output.txt'): rewrite(output): InputData: T[l]:-copy(alf.l.NP); for i:-l to NL do for j:-Pof2[i] to Pof2[i+1]-1 do if length(T[j])>l then Split(i.T[j].T[2*j].T[2*j+l]): WriteTree(l): close(input): close(output); end. Здесь процедура InputData обеспечивает ввод первых точек каждой фигуры (ос- (остальные игнорируются) и секущих прямых. Для каждой прямой по условиям да- даются ее 2 точки. В процедуре ввода сразу строятся коэффициенты уравнения пря- При заданных ограничениях на количества объектов и прямых двоичное дерево можно хранить в виде массива Г, в котором сыновья вершины с индексом.;' имеют индексы 2/' и 2j + 1. Вершина T[j] хранит строку букв, обозначающих объекты, находящиеся в данном регионе. В T[1] помещаются буквы всех объектов. Далее, циклом по количеству прямых (i) и по номеру региона j делим регионы Т [ j], (в которых еще болееодного объекта) на подрегионы Т [2 *j] и Т [2 *j + 1]. Разбиение на подрегионы осуществляет процедура Split. Рекурсивная процедура WriteTree(l) обеспечивает обход дерева по листам слева направо и вывод ответов. Рассмотрим подробнее реализацию процедуры InputData, procedure InputData: begin read(NP): for i:-l to NP do readln(m.x[i].y[i]): read(NL): for i:-l to NL do begin readln(xl.yl,x2.y2): A[i]:-y2-yl:
3.3. Задачи на представление образов 109 B[i]:-xl-x2; C[i]:-yl*(x2-xl)-xl*(y2-yl): end: Pof2[l]:-1: for i:-2 to NL+2 do pof2[i]:-2*Pof2[i-l]: for i:-l to Pof2[Nl+2] do T[i]:-": end; Здесь: ? NP — количество фигур (или точнее, точек — мы берем только координаты первой точки каждой фигуры x[i], y[i]); ? NL — количество секущих прямых jri, y\, x2, y2 — две точки, через которые проходиттекущая прямая.Л[г],Я[1], C[i] — коэффициенты каноническогоурав- нения этой прямой; ? Pof2 — массив степеней двойки; О T[i] — массив, который будет содержать дерево разбиения. Теперь обратимся к процедуре Split. procedure Split(i:integer: var S.Sl.S2:string20): begin for k:-l to length(S) do begin m:-ord(S[k])-ordCA')+l: if y[m]>(-(x[m]*a[1]+c[1])/b[i]) then Sl:-Sl+S[k] else S2:-S2+S[k]: end; end: т — номер точки, определяемый по букве фигуры, которую представляет эта точ- точка. Если точка (x[mj#[m]) лежит выше прямой (A[i]x+B[i]y+C=0), то она зано- заносится в левое поддерево, иначе — в правое. И, наконец, рекурсивная процедура левостороннего обхода листьев дерева: procedure WriteTree(j:integer); begin if (j>2048) then exit: if (j<-1024) and (T[2*j]<>") then WriteTreeB*j); if (j<-1024) and (T[2*j+l]<>") then WriteTreeB*j+l); if (j>1024) and (T[j]<>") or (T[2*j]-") and (T[2*j+l]-") then write(T[j]): end: Нам нужно выводить содержимое только тех элементов массива, у которых нет «потомков», а содержимое — есть. Полный текст решения задачи приведен в конце главы.
110 Глава 3 * Решение задач на деревьях и с помощью деревьев 3.4. Задачи на двоичные деревья сортировки В этом разделе представлены задачи «Дерево», «Parliament», «Falling Leaves», при решении которых дерево используется как сортирующая структура. 3.4.1. Задача «Дерево» ХУБелорусская олимпиада по информатике, 2002 Дерево (lnput.Txt / Output.Txt / 1 с) Пете Булочкину крупно повезло: он наконец устроился на работу в фирму Macrohard. Он хочет показать себя с самой лучшей стороны, поэтому к пер- первому своему заданию отнесся весьма ответственно. Задание состоит в том, чтобы написать поисковую систему. Пете заранее известен набор чиселЛ1, Л2,..., Ak (все Ai — различные целые числа) — на- назовем их ключами. Система должна обрабатывать запросы типа: «Содер- «Содержится ли среди ключей число s?» Известно, что число s может быть любым целым числом от 1 до п (п < 109). Руководство фирмы сказало Пете, что ему нужно использовать как можно меньше памяти. Поразмыслив, Петя решил, что оптимальным решением поставленной задачи будет использование двоичных деревьев поиска, опи- описание которых приведено далее. Описание: Двоичное дерево может быть пустым или состоять из вершины, к кото- которой присоединены два двоичных дерева, то есть левое и правое поддеревья (в этом случае вершину, к которой присоединяются деревья, называют корнем). Если в каждую вершину поместить по ключу, причем так, чтобы в разных вершинах были различные ключи, то получим двоичное дерево для заданного набора ключей. Будем говорить, что дерево является двоич- двоичным деревом поиска, если левое и правое поддеревья являются двоичными деревьями поиска, а также любой ключ из левого поддерева, выходящего из корня, меньше ключа, записанного в корне, а любой ключ из правого подде- поддерева — больше. На рис. 3.18 показаны различные двоичные деревья для на- набора ключей 1, 3, 7, 11, из них деревья б и в — двоичные деревья поиска. Корни деревьев изображены сверху. Корни деревьев а и б — вершина с клю- ключом 1, корень дерева в — вершина с ключом 7. Для того чтобы проверить, содержится ли в заданном двоичном дереве поиска ключ 5, используется следующий алгоритм: 1) положить текущую вершину рав- равной корню дерева; 2) проверить, совпадает ли s с ключом, записанным в тек- текущей вершине. Если да, то ключ s найден. Иначе перейти к шагу 3; 3) если s меньше ключа, записанного в текущей вершине, то положить текущую вер- вершину равной корню левого поддерева, иначе — равной корню правого под- поддерева (если соответствующие поддеревья отсутствуют, то алгоритм закан- заканчивает работу, выдавая, что ключ s в дереве отсутствует). Перейти к шагу 1.
3.4. Задачи на двоичные деревья сортировки 111 Стоимостью поиска ключа s назовем количество выполненных шагов вы- вышеописанного алгоритма. Например, для дерева на рис. 3.18, в стоимости поиска различных ключей указаны в табл. 3.2. 11 \ ) / a б в Рис. 3.18. Примеры двоичных деревьев поиска Таблица 3.2. Стоимости поиска ключей Ключ Стоимость поиска 1 2 2 3 3 3 4 3 5 3 6 3 7 1 8 2 9 2 10 2 11 2 Стоимостью заданного двоичного дерева для диапазона поиска от 1 до п назовем сумму стоимостей поиска каждого из ключей от 1 до п в этом дере- дереве. Например, стоимость дерева на рис. 3.18, в для диапазона поиска от 1 до 11 равна 26. Задание\ Петя хочет построить для своей поисковой системы двоичное дерево поис- поиска минимальной стоимости. По введенным числам я, k, Л1,..., Ak опреде- определить минимальную стоимость Сдвоичного дерева поиска для набора клю- ключей Л1, Л2,..., Ak и диапазона поиска от 1 до я. Ввод: Входные данные состоят из k + 2 строк: 1-я строка содержит число я, 2-я строка — число k, строки с 3-й по (k + 2)-ю — числаЛ1,..., Ak по одному в строке. Вывод: Выходной файл должен содержать единственное число С. Прммвр ввода Пример вывода ~1о 22 4 9 3 7 4
112 Глава 3 * Решение задач на деревьях и с помощью деревьев Оптимальное дерево для заданного примера показано на рис. 3.19. А 7 Рис. 3.19. Оптимальное дерево для заданного примера Описание решения: По условиям задачи нам нужно найти минимальную стоимость двоичного дерева поиска для набора ключей Л[1],..., A[K] и диапазона поиска от 1 до N. Будем ис- искать эту минимальную стоимость как минимальную из всех стоимостей, который могут получаться для ВСЕХ вариантов двоичных деревьев поиска для заданного множества ключей. Прежде всего упорядочим ключи A[i] по возрастанию. Положим также A[0] - 0, Введем обозначение F(i, j) — минимальная стоимость двоичного дерева поиска длянабораключейЛ*,... ,Л/'идиапазонапоискаотЛ[2-1] + 1доЛ^' + 1]-1.Тогда по условиям задачи нам фактически требуется найти FA, k). Будем вычислять минимальную стоимость F(iJ) рекурсивно через минимальную из возможных стоимостей при выборе вершины s (s изменяется в диапазоне от i Roj) в качестве корня и минимального левого поддерева (из элементов от i до s-1) и минимального правого поддерева (из элементов от 5+1 доД Пусть f0, если / > j F('>j) = )A[j + l}-A[i-l]-l+mn(F(i,s-l) + F(s + lj)),ecmi<j Для ускорения рекурсивных вычислений заведем также массив FA/, в котором бу- будем хранить вычисленные значения F(iJ) для устранения потерь времени на повтор- повторные вычисления F(i,j). Значение -1 в массиве FM[i,j] будем трактовать как невычис- ленное ее значение функции F(i,j). Тело главной программы может выглядеть так: begin assign(input.'input.txf); reset(input); assign(output.'output.txt'); rewrite(output); read(N.K); for i:-l to k do read(a[i]): SortA: A[0]:-0: A[K+1]:-N+1: for i:-0 to К+1 do for j:-0 to К+1 do FM[1.j]:-l: writeln(F(l.k)): close(input): close(output); end.
3.4. Задачи иа двоичные деревья сортировки 113 Рекурсивная функция F(i,j) может быть реализована так: function FA.j:longint).longint: var fs.s : longint; begin if fm[i.j]<0 then begin if i>j tnen Fm[i.j]:-0 else begin fs:-max1ongint: for s:-i to j do fs:-min(fs.FO.s-l)+F(s+l.j)): fm[i.j]:-a[j+l]-a[i-l]-l+fs end; end; F:-fm[i.j] end; Полный текст решения задачи приведен в конце главы. 3.4.2. Задача «Parliament» NEERC, центральный четветьфинал, 2001 Parliament (lnput.txt / output.txt / 3 с) В государстве ММММ выбран новый парламент. При регистрации каж- каждый член парламента получил уникальный идентификатор (положитель- (положительное целое число). Идентификаторы раздавались в случайном порядке, воз- возможны и разрывы в последовательности чисел. Кресла в парламенте орга- организованы в виде «дерева». Когда члены парламента входят в аудиторию, они занимают места в следующем порядке. Первый вошедший занимает место председателя. Каждый следующий делегат возглавляет левое дерево, если его идентификатор меньше идентификатора председателя, или справа в противном случае. После чего он садится в пустое кресло и объявляет себя председателем крыла. Если кресло председателя крыла уже занято, то алгоритм распространяет- распространяется далее — делегат отправляется налево или направо в зависимости от ре- результатов сравнения своего идентификатора с идентификатором председа- председателя крыла. Рисунок 3.20 показывает пример рассаживания членов парламента, если они входили в аудиторию в следующем порядке: 10,5,1,7,20,25,22,21,27. Во время первой сессии парламент принял решение не изменять порядок рассаживания. Был также принят порядок выступлений. Если номер сессии нечетный, то парламентарии высказывались в следую- следующем порядке: левое крыло, правое крыло, председатель. Если в крыле более
114 Глава 3 * Решение задач на деревьях и с помощью деревьев одного парламентария, то их порядок выступления был такой же: левое крыло, правое крыло, председатель. Если номер сессии четный, то порядок выступлений менялся: правое кры- крыло, левое крыло, председатель. Enter 10 /\ 5 20 /\ \ 1 7 25 /\ 22 27 / 21 Рис. 3.20. Пример дерева для задачи «Parliament» Для приведенного примера порядок выступлений для нечетных сессий — 1, 7, 5, 21, 22, 27, 25, 20, а для четных сессий - 27, 21, 22, 25, 20, 7,1, 5,10. Ваша задача — определить порядок выступления для четной сессии, если известен порядок выступления для нечетной сессии. Ввод: Первая строка входного файла содержит количество парламентариев N Следую- Следующие строки содержат Л^целых чисел — идентификаторы парламентариев в порядке их выступления на нечетных сессиях. Вывод: Выходной файл должен содержать идентификаторы парламентариев в порядке их выступления на четных сессиях. Ограничения: Общее количество членов парламента не превысит 3 000. Идентификаторы не превысят 60 000. Пример ввода: 9 1 7 5 21 22 27 25 20 10 Пример вывода: 27 21 22 25 20 7 1
3.4. Задачи на двоичные деревья сортировки 115 5 10 Итак, дан «левосторонний» обход двоичного отсортированного дерева. Нужно построить его «правосторонний обход». Под «левосторонним» обходом здесь понимается такой обход, при котором вершины посещаются в порядке «левое поддерево - правое поддерево - корень». Под «правосторонним», соответствен- соответственно, понимается обход в порядке «правое поддерево - левое поддерево - корень». Решение задачи заключается в выполнении двух шагов. 1. Построение двоичного отсортированного дерева по заданному «левосторон- «левостороннему» обходу. При этом элементы массива добавляются в отсортированное дерево в обратном порядке (начиная с последнего). 2. «Правосторонний» обход построенного дерева. Тело главной программы может выглядеть следующим образом: begin assign(input.'input.txt'); reset(input): assign(output.'output.txt'): rewrite(output): read(N): for i:-l to N do read(a[i]): for i:-l to MaxD do d[i]:-0: for 1:-N downto 1 do AddToTree(a[i]); WriteTree(l): close(input): close(output): end. Простейший способ представления двоичного дерева — хранить его в массиве (D), используя индексы массива в качестве ссылок к сыновьям. Корень дерева имеет индекс 1. Для вершины i левый потомок имеет индекс 2i. а правый потомок — индекс 2i + 1. ПроцедураЛ^7о7гее(ф']) добавляет в двоичное отсортированноедерево элемент a[i]. Процедура WriteTree(l) выводитэлементы дерева, корнем которого является вершинаномер 1. Решение задачи приведено в конце главы. Оно имеет, к сожалению, недостаток, связанный с ограничениями, установленными авторами данной задачи. Три ты- тысячи чисел (по условию их может быть столько) могут не поместиться в памяти, если использовать такое представление дерева. Поэтомудалее приводится аналогичное по содержанию решение, которое исполь- использует другое представление дерева. А именно: в дополнение к массиву введенных чисел a[1..3000] для хранения дерева используются еще два массива Left[ 1 ..3000] и #igftf[1..3000]. Корнем дерева (Root) объявляется элемент N Left[i] указывает индекс в массиве а элемента, который является левым потомком элемента i. Right[i] указывает индекс в массиве а элемента, который является правым потомком эле- элемента i. При прохождении текущего узла необходимо также знать номер элемен- элемента, который является его предком. Для этого используется переменная pred.
116 Глава 3 * Решение задач на деревьях и с помощью деревьев Тело главной программы нового решения выглядит так: begin assign(input.'input.txt'): reset(input); assign(output.'output.txt'): rewrite(output): read(N); for i:-l to N do read(a[i]): for i:-l to N do begin Left[i]:-0: Right[i]:-0; end: Root:-N: for i:-N-l downto 1 do AddToTree(i); WriteTree(Root.Root.2): close(input); close(output): end. Вначале инициализируется пустое дерево (Left[i] e 0, Right[i] - 0). Затем опре- определяется корень дерева Root. Процедуры AddToTree (добавить элемент в дерево) и Write Tree (вывести дерево) несколько видоизменены, рассмотрим их подробнее. procedure AddtoTree(i:word): begin Tek:-Root: while ((a[i]>a[Tek]) and (Right[Tek]<>O)) ог ((a[i]<a[Tek]) and (Left [Tek]<>O)) do if a[i]>a[Tek] then Tek:-Right[Tek] else Tek:-Left[Tek]: if (a[i]>a[Tek]) and (Right[Tek]-O) then Right[Tek]:-i else Left [Tek]:-i: end: Процедура AddToTree(i) перемещается по дереву, сравнивая значение помещае- помещаемого в дерево элемента a[i] с имеющимимся вершинами дерева, выбирая направ- направление «вправо», если a[i] больше значения текущего узла дерева и «влево» — в про- противном случае. Добравшись до свободной позиции для текущего элемента, про- процедура устанавливает нужную ссылку (Right[Tek] или Left[Tek]). procedure WriteTree(j.pred.Dir:word): begin if Right[j]<>O then WriteTree(Right[j].j.2): if Left [j]<>0 then WriteTree(Left [j].j.l): writeln(a[j]); if Dir-2 then Right[pred]:-0 else Left[pred]:-O; end; Процедура WriteTree имеет три входных параметра:
3.4. Задачи на двоичные деревья сортировки 117 j — номер элемента в текущей вершине дерева; pred — номер элемента, который ссылался на текущий; Dir — направление A — влево, 2 — вправо), по которому шли от предыдущего эле- элемента к текущему. В соответствии с «правосторонним» обходом вначале обходится правое дерево (WriteTree(Right[j]J, 2)), затем neuoejiepeBo(WriteTree(Left [j],jA))- Затем выво- выводится текущий узел и обнуляется ссылка, по которой мы в него пришли. Полный текст решения задачи приведен в конце главы. 3.4.3. Задача «Falling Leaves» Mid-Central USA Programming Contest, 2000 Falling Leaves (leaves.in / leaves.out / 5 c) /\ C H M Y /\ \ B D P Рис. 3.21. Пример дерева для задачи «Falling Leaves» Рисунок 3.21 показывает графическое представление двоичного дерева из букв. Двоичное дерево букв — либо пустое, либо имеет букву в качестве данных и ссылки на левое и правое поддеревья. Левое и правое поддеревья также являются двоичными деревьями букв. В графическом представле- представлении двоичных деревьев букв пустые деревья опускаются, каждая вершина идентифицируется своей буквой, отрезком линии вниз-влево к левому под- поддереву (если оно непустое), и отрезком линии вниз-вправо к правому под- поддереву (если оно непустое). Листом в двоичном дереве называется верши- вершина, оба поддерева которой пусты. Например, в изображенном выше дереве листами являются вершины с данными В, D, H, P, Y. Прямой порядок об- обхода деревьев удовлетворяет следующим свойствам: 1) если дерево пусто, то и порядок его обхода пуст; 2) если дерево непустое, тогда прямой обход выполняется в следующем порядке: корень дерева, прямой обход левого под- поддерева, прямой обход правого поддерева. Например, порядок прямого об- обхода дерева на рис. 3.21 - KGCBDHQMPY. Дерево, представленное на рис. 3.21, является также и деревом двоичного поиска букв. Двоичное дерево поиска букв — это двоичное дерево букв, в ко- котором каждая вершина удовлетворяет следующим свойствам: 1) буква в кор- корневой вершине встречается в алфавите после всех букв в вершинах левого поддерева; 2) буква в корневой вершине встречается в алфавите перед все- всеми буквами в вершинах правого поддерева.
/ с к /\ G Q / м к /\ G 118 Глава 3 * Решение задач на деревьях и с помощью деревьев Задание: Рассмотрим следующую последовательность операций на двоичном дереве поиска из букв: удалим все листья и выпишем все удаленные данные; по- повторим эту процедуру, пока дерево не станет пустым. Если начинать с дере- дерева, представленного на рисунке, мы получим последовательность деревьев, показанную на рис. 3.22, и такие удаляемые данные (листья): BDHPY СМ GQ К К о/Чо /\ /\ С Н М Y /\ \ В D Р Рис. 3.22. Иллюстрация «опадания листьев» Ваша задача — обработав такую последовательность строк листьев, удаленных из буквенного двоичного дерева поиска, вывести порядок прямого обхода этого дерева. Ввод: Входной файл содержит один или более тестов. Каждый тест — последователь- последовательность из одной или более строк с заглавными латинскими буквами. Они содер- содержат листья, удаленные из буквенного двоичного дерева поиска по описанным стадиям. Буквы на строке представлены в алфавитном порядке. Тесты разделяются строкой, содержащей только символ '*'. За последним тестом следует строка, содержащая только символ '$'. Вывод: Для каждого теста выведите одну строку — прямой обход соответствующего дерева. Пример ввода Пример вывода BDHPY KGCBDHQMPY СМ ВАС GQ К * АС В $
3.4. Задачи на двоичные деревья сортировки 119 Итак, требуется построить обход дерева в порядке «корень ^ левое_поддерево ^ правое_поддерево». Как и в предыдущей задаче, решение включает в себя два шага. 1. Восстановление дерева. 2. Вывод дерева в нужном порядке. С учетом особенностей формата ввода и того, что элементами деревьев являются символы (а не числа, как в предыдущей задаче), тело главной программы выгля- выглядит так: begin assign(input.'leaves.in'): reset(input): {assign(output.'leaves.out'): rewrite(output);} readln(s); while not EOF(input) do begin a:-'': while (s<>'*') and (s<>'$')do begin a:-a+s: readln(s) end; N:-length(a); for i:-l to N do begin Left[i]:-0; Right[i]:-0: end: R00t:-N; for i:-N-l downto 1 do AddToTree(i): WriteTree(Root); writeln; readln(s); end: close(input): close(output): end. Как и в предыдущей задаче, AddToTree — процедура добавляющая очередной эле- элемент к дереву, WriteTree — процедура обхода дерева и вывода его элементов в нуж- нужном порядке. Рассмотрим детальнее реализацию процедуры WriteTree, которая претерпела не- некоторые изменения по сравнению с предыдущей задачей. procedure WriteTree(j:integer): begin write(a[j]); if Left [j]<>0 then WriteTree(Left [j]): if Right[j]<>0 then WriteTree(Right[j]); end: Вначале выводится корневой элемент, потом выполняется обход левого дерева (WriteTree(Left[j])) и затем обход правого дерева (WriteTree(Right[j])). Полный текст решения задачи приведен в конце главы.
120 Глава 3 * Решение задач на деревьях и с помощью деревьев 3.5. Кодирование последовательностей символов методом Хаффмана В данном разделе приводятся две задачи: «Кодирование» и «Entropy», решение кооторых основывается на кодировании символов по методу Хаффмана. 3.5.1. Задача «Кодирование» Гомельская городская олимпиада школьников по информатике, 2002 Кодирование (input.txt / output.txt / 5 с) Необходимо произвести оптимальное кодирование исходного текста, представляющего собой набор символов из множества М e {':7,7.7 ', '0'..'9', 'A'..'Z', 'a'..'z', 'A'..'H', 'a'..V} в двоичное представление. Это зна- значит, что каждый символ входного предложения должен быть закодиро- закодирован уникальным набором нулей и единиц. Символы, не принадлежащие множеству Л/, кодировать и помещать в выходной файл не надо. Вы- Выходные данные, представляющие собой закодированный текст, также должны однозначно декодироваться. В данном случае оптимальным ко- кодом называется код, содержащий наименьшее суммарное число единиц и нулей. Ограничения: Гарантируется, что размер входного файла не будет превышать 170 Кбайт. Алгоритм построения оптимального кодадля некоторой последовательности сим- символов состоит в том, чтобы кодировать символы, встречающиеся наиболее часто, последовательностью минимальной длины, и наоборот. 1. Вычисляем для каждого символа количество раз, которое он встретился в пос- последовательности, и записываем это число в список. 2. В данном списке находим два самых маленьких числа. Суммируем их, добав- добавляем сумму в список ивычеркиваем их из списка. 3. Пока в списке больше двух чисел, продолжаем выполнение пункта 2. 4. Ставим в соответствие первому числу из списка 0, а второму — 1. 5. Начинаем последовательно выполнять возврат к исходному состоянию спис- списка (то есть как бы разъединяя суммы и добавляя элементы в список). При этом, если мы разъединяем элемент списка, которому сопоставлено двоичное число X, то необходимо первому из создаваемых элементов сопоставить число X0,a второмуХ1. 6. Продолжаем выполнение пункта 5 до тех пор, пока не исчерпаем все суммы, которые необходимо разъединить. В итоге мы получим двоичные коды для чисел из последовательности.
3.5. Кодирование последовательностей символов методом Хаффмана Пример: Последовательность: ABCAD. Символы: 121 А В С D 2\ 1\ 1\ 1- 2\ 1\ 2- 2- 3- Сопоставим 0 Сопоставим 1 /-- // /10 -11 0 10 но 111 Оптимальный код для последовательности: код для А код для В код для С код для D 0101100111 Ввод: Входной файл содержитМ(Мне дано) строк с текстом. Вывод: Выходной файл состоит из двух частей. 1. Коды символов, встречающихся во входном тексте, выписанные в произволь- произвольном порядке. [символ][пробел][код] [символ][пробел][код] 2. Одна строка, содержащая закодированный текст. Пример ввода: А В А. Пример вывода: 001 . 000 А 1 В 01 1010011000 Фактически в условии задачи описано ее решение. Рассмотрим практическую реализацию. Тело главной программы выглядит так: begin InputData; SumUp; BuildBits; OutputData: end. Здесь: ? InputData — процедура ввода исходных данных; ? SumUp — процедура построения сумм;
122 Глава 3 * Решение задач на деревьях и с помощью деревьев ? BuildBits — процедура кодирования входных символов двоичными строками; ? OutputData — процедура кодирования и вывода исходного текста. Рассмотрим их детальнее. procedure InputData; var с : char; s : аггау [0..255] of longint: begin assign(input.'input.txt'): reset(input): for i:-0 to 255 do s[i]:-0: while not EOF(input) do begin read(c); if (c<>#10) and (c<>#13) then inc(s[ord(c)]): end: j:-0: for i:-0 to 255 do if s[i]<>0 then begin 1nc(j): k[j]:-s[i]: sk[j]:-chr(i) end: kj:-j: close(input): end: По условию задачи символы с кодами #10 и #13, разделяющие строки, кодиро- кодировать не нужно, поэтому их частоты не считаются. В результате ввода: ? kj — количество различных сиволов во входном файле; ? sk[j] — массив различных символов; ? k[j] — частоты этих символов. procedure SumUp: var sl. s2. ml. m2 : longint: begin if kj-l then begin assign(output.'output.txf): rewrite(output): writeln(sk[l].' '.0): writeln@): close(output): halt(O) end: for i:-l to kj do Free[i]:-true; kkl:-kj; kk2:-kj:
3.5. Кодирование последовательностей символов методом Хаффмана 123 while kkl>2 do begin sl:-MinK: ml:-m; s2:-MinK: m2:-m: inc(kk2): k[kk2]:-sl+s2: a[kk2]:-ml: b[kk2]:-m2: Free[kk2]:-true: dec(kkl); end: end: В этой процедуре отдельно обрабатывается случай «текст из одного символа», поскольку основной алгоритм предполагает наличие двух или более различных символов. Для поиска минимального элемента в массиве k[j] используется функция MinK. В массиве Free хранятся отметки об использованных (как минимальные) элементах массива К. В массивах а и b запоминаются номера элементов массива k> просум- просуммированных в соответствующий элемент массива k, начиная с номера kj + 1 и до номера kk2. procedure BuildBits: begin bits[kk2]:-T; Free[kk2]:-false: b1ts[a[kk2]]:-bits[kk2]+'0': bits[b[kk2]]:-bits[kk2]+T: i:-MinK: bits[m]:-'0': Free[m]:-true: bits[a[m]]:-bits[m]+'O'; bits[b[m]]:-bits[m>T; for i:- kk2-l downto 1 do if not Free[i] then begin b1ts[a[1]]:-b1ts[i]+'0': bits[b[i]]:-bits[i]+T: end: end: Процедура BuildBits присваивает элементу с номером kk2 префикс 1. Затем нахо- находит в массиве k второй по величине элемент (после k[kk2]) и присваивает ему префикс 0. Далее для всех оставшихся элементов массива k строит их префиксы, используя номера элементов (a[i] и b[i]), составивших очередной элемент k[i]. procedure OutputData: var str : string:
124 Глава 3 * Решение задач на деревьях и с помощью деревьев begin assign(output.'output.txt'); rewrite(output): for i:- 1 to kj do writeln(sk[i].' '.bits[i]); for i:-l to kj do res[ord(sk[i])]:-bits[i]; assign(input.'input.txt'): reset(input): while not EOF(input) do begin readln(str): for i:-l to length(str) do write(res[ord(str[i])]); end: close(input): close(output); end: Процедура OutputData вначале выводит символы и их кодировки. Затем для каж- каждого символа входного файла выводит в выходной его файл построенный двоич- двоичный код. Для ускорения используется массив res из 256 элементов типа string, в ко- котором хранятся полученные коды символов. Полный текст решения задачи при- приведен в конце главы. 3.5.2. Задача «Entropy» GreaterNew York Regional Contest, 2000 Entropy (input.txt / output.txt / 10 c) Энтропийное кодирование — это метод кодирования данных, который обес- обеспечивает компрессию данных за счет удаления избыточной информации. Например, английский текст, закодированный с помощью таблицы ASCII, — это пример сообщения с высокой энтропией. В тоже время сжатые сообще- сообщения, например zip-архивы, имеют очень маленькую энтропию, и потому попытки их энтропийного кодирования не принесут пользы. Английский текст, закодированный с помощью ASCII, имеет высокую сте- степень энтропии, потому что для кодирования всех символов используется одно и тоже количество бит — восемь. В то же время известный факт состо- состоит в том, что буквы ?, L, N, R, S и Гвстречаются со значительно более высо- высокой частотой, чем другие буквы английского алфавита. Если мы найдем способ закодировать только эти буквы четырьмя битами, то закодированный текст станет существенно меньше и при этом будет содержать всю исход- исходную информацию и иметь меньшую энтропию. Однако как нам различить при декодировании, четырьмя или восемью битами закодирован очередной символ? Эта проблема решается с помощью префиксного кодирования. В такой схеме кодирования любое количество битов может быть использо- использовано для конкретного символа. Однако для того чтобы иметь возможность восстановить информацию, запрещено, чтобы последовательность битов, кодирующая некоторый символ, была префиксом битовой последовательно- последовательности, используемой для кодирования любого другого символа. Это позволя-
3.5. Кодирование последовательностей символов методом Хаффмана 125 ет читать входную последовательность бит за битом, и как только встрече- встречено обозначение символа — его декодировать. Рассмотрим текст AAAAABCD. Кодирование, использующее ASCII, требу- требует 64 бит. Если же мы символ А закодируем битовой последовательно- последовательностью 00, символ В — последовательностью 01, символ С - последователь- последовательностью 10, a D — последовательностью 11,то для кодирования потребуется всего 16бит. Результирующий поток битов будет такой: 0000000000011011. Но это все еще кодирование с фиксированной длиной, мы просто исполь- использовали для каждого символа два бита вместо восьми. Символ А встречается чаще, чем другие, можем ли мы его закодировать с по- помощью меньшего количества битов? Да, можем, тогда мы закодируем символы такими последовательностями битов: Л-0 J3 — 10 С— 110 D-U1 Используя такое кодирование, мы получим только 13 бит в закодирован- закодированном сообщении: 0000010110111. Коэффициент сжатия в этом случае равен 4.9 к 1. Это означает, что каждый бит в последнем закодированном сообще- сообщении содержит столько же информации, сколько и 4.9 бит в первом закоди- закодированном сообщении (с помощью ASCII). Попробуйте читать сообщение 0000010110111 слева направо — и убедитесь, что «префиксное» кодирование обеспечивает простое декодирование текста, даже несмотря нато, что символы кодируются различным количеством битов. В качестве другого примера рассмотрим текст THE CATIN THEHAT. В этом тексте символы Т и пробел встречаются чаще других. Поэтому их нужно кодировать меньшим количеством битов. А символы С, / и N встре- встречаются только по одному разу, потому будут кодироваться самыми длин- длинными кодами. Например, так: пробел — 00 Л-100 С— 1110 ?-1111 Я-110 /— 1010 W-1011 Г-01 При таком кодировании исходного предложения потребуется только 51 бит против 144, которые необходимы, чтобы закодировать исходное сообщение с по- помощью 8-битного ASCII-кодирования. Коэффициент сжатия равен 2.8 к 1.
126 Глава 3 * Решение задач на деревьях и с помощью деревьев Ввод: Входной файл будет содержать список текстовых сообщений, по одному в строке. Сообщения будут состоять только из больших английских букв, цифр и симво- символов подчеркивания (вместо пробелов). Конец файла обозначается строкой END. Эту строку не нужно обрабатывать. Вывод: Для каждого входного сообщения выведите количество бит в восьми-битовом ASCII-кодировании, количество бит при оптимальном префиксном кодировании и коэффициент сжатия с точность до одного знака после десятичной точки. Пример ввода: AAAAABCD THE_CAT_IN_THE_HAT END Пример вывода: 64 13 4.9 144 51 2.8 В данной задаче, как и в предыдущей, предлагается провести кодирование текста алгоритмом Хаффмана. Отличиями являются представление входных и выход- выходных данных. Каждую входную строку нужно кодировать по отдельности. Строка «END» обозначает конец ввода, ее кодировать не нужно. На выходе нужно указать три числа: 1) длину сообщения в битах при стандартном восьми-битном кодировании; 2) длину сообщения в битах при выполненном оптимальном кодировании; 3) коэффициентсжатия. Полный текст решения приведен в конце главы. 3.6. Перечисление деревьев В данном разделе приведены задачи «Nextree» и «Trees Made to Order», связан- связанные с тематикой «Перечисление деревьев». 3.6.1. Задача «Nextree» USA Computing Olimoiad, 2000 Nextree (nextree.in / nextree.out) На ферме Джона растет большое количество деревьев. После обучения в компьютерном классе корова Бетси заметила, что все эти деревья являются двоичными деревьями, у которых каждая вершина, не являющаяся листом, имеет ровно две вершины-наследницы. Бетси назначила каждой вершине число — ко- количество листьев в поддереве, которое имеет эту вершину в качестве корневой.
3.6. Перечисление деревьев 127 Затем Бетси расположила числа, ассоциированные с вершинами, в соот- соответствующем порядке, однако она указала только числа, ассоциированные с корневой вершиной и всеми левыми наследницами всех вершин. Для уточ- уточнения рассмотрим дерево, показанное на рис. 3.23. •4 /\ •1 3 /\ *2 1 /\ 3 /\ *1 2 /\ •1 1 Рис. 3.23. Пример дерева для задачи «Nextree» Звездочками помечены вершины, указанные Бетси. Набор чисел, представ- представляющих такое дерево — 7 4 1 2 1 1 1. После представления каждого дерева таким образом Бетси заметила, что все деревья имеют одинаковое количество листьев; все деревья имеют раз- различное числовое представление; все возможные двоичные деревья имеют- имеются на ферме. Поэтому, будучи творческой коровой, она решила отсортировать коды этих деревьев. Решите эту же задачу и вы. Вам задано числовое представление (код) дерева, найдите код, который следует непосредственно за ним, в списке Бетси. Ввод: Строка 1: L — длина кода A < L < 1000); Строка 2: L разделенных пробелами целых чисел, которые представляют код де- дерева в списке Бетси. Пример ввода: 5 5 3 2 1 1 Вывод: Одна строка с разделенными пробелом целыми числами кода, который следует за данным кодом в лексико-графическом порядке. Если входной код является по- последним в списке Бетси, выведите 0. Помните: деревья, указанные на вводе и пред- представленные в выводе, должны иметь одно и то же количество листьев. Пример вывода: 5 4 1 1 1
128 Глава 3 * Решение задач на деревьях и с помощью деревьев Описание решения: Прежде всего заметим, что при таком способе нумерации деревьев (слева напра- направо) самые ранние изменения произойдут в самом правом «нетупиковом» подде- поддереве. «Нетупиковым» мы здесь называем поддерево, которое может быть измене- изменено так, что его корневая вершина не изменится, а некоторые из поддеревьев изме- изменятся. Рассмотрим это положение на примере. Входной строке 5 3 2 1 1 соответствует дерево на рис. 3.24. •5 / \ •3 2 /\ /\ •2 1 *1 1 /\ *1 1 Рис. 3.24. Дерево с кодом 5 3 2 1 1 Символом '*1 помечены вершины, вошедшие в запись кода дерева. Здесь правое поддерево не может быть изменено, и потому изменяем левое поддерево (рис. 3.25). •5 /\ •4 1 /\ •1 3 /\ •1 2 /\ •1 1 Рис. 3.25. Дерево с кодом 5 4 1 1 1 Было: 5 3 2 1 1 Ответ: 5 4 1 1 1 Рассмотрим еще один пример (рис. 3.26). •6 3 /\ /\ *2 4 *1 2 /\ /\ /\ •1 1 *2 2 *1 1 /\/\ *1 1 *1 1 Рис. 3.26. Дерево с кодом 9 6 2 1 2 1 1 1
3.6. Перечисление деревьев 129 Код представленнного дерева — 9 6 2 1 2 1 1 1 1. Самое правое его «нетупиковое» поддерево показано на рис. 3.27: 3 /\ •1 2 /\ •1 1 Рис. 3.27. Самое правое «нетупиковое» поддерево Оно имеет код 1 1 ( в записи кода всего дерева). Это поддерево может быть изме- изменено на поддерево, изображенное на рис. 3.28. 3 /\ •2 2 /\ •1 1 Рис. 3.28. Преобразованное «нетупиковое» поддерево Его код — 2 1. В результате все дерево примет такой вид, как на рис. 3.29. •9 /\ *6 3 /\ /\ •2 4 *2 2 /i /\ /\ *1 1 *2 2 *1 1 /\/\ •1 1 *1 1 Рис. 3.29. Дерево с кодом 9 6 2 1 2 1 1 Его код 9 6 2 1 2 1 1 2 1. Теперь самое правое «нетупиковое» поддерево (рис. 3.30) имееткод2 1 1. 4 /\ •2 2 /\ /\ *1 1 *1 1 Рис. 3.30. Новое самое правое «нетупиковое» поддерево
130 Глава 3 * Решение задач на деревьях и с помощью деревьев Следующее по счету дерево (рис. З.31)будет иметь код 3 1 1. 4 Л *3 1 /\ *1 2 /\ *1 1 Рис. 3.31. Следующее по счету поддерево Целиком следующее дерево будет иметь вид, показанный на рис. 3.32, и код 9 6 2 13 1 1 1 1. •9 /\ •6 Зч /\ /\ *2 4 *1 2 л i\ л *1 1 *3 1 *1 1 л *1 2 л •1 1 Рис. 3.32. Дерево с кодом 9 6 2 1 3 1 1 1 1 Заметим, что после реорганизации самого правого «нетупикового» дерева, все более правые «тупиковые» деревья преобразуются к «стартовой позиции» (рис. 3.33). /\ *1 х-1 /\ *1 х-2 /\ *1 Рис. 3.33. «Стартовая позиция» нетупикового поддерева Таким образом, очевидно, что для нахождения следующего кода дерева нужно най- найти самое правое «нетупиковое» поддерево, перестроить его (в сторону минимального увеличения кода), преобразовать все более правые «тупиковыедеревья» к «стар- «стартовой» позиции и вывести код получившегося дерева.
3.6. Перечисление деревьев 131 При реализации достаточно работать с фрагментом массива исходного кода, пред- представляющего найденное тупиковое дерево. Алгоритм изменения этого фрагмента кода таков: с правой стороны фрагмента массива ищем первый элемент, который меньше предыдущего на 2 или более. Увеличиваем его на 1, а все позиции справа от него заполняем единицами. Рассмотрим конкретную реализацию программы, решающей поставленнуюзадачу. Тело главной программы выглядит следующим образом: begin assign(input,'nextree.in'): reset(input): assign(output.'nextree.out'):rewrite(output): read(L): for i:-l to L do read(a[i]): Done:-fa1se: CheckTree(l.L): if not Done then begin writeln@): exit end: for i:-l to L-1 do write(a[1].' '): writeln(a[L]): close(input): close(output): end. Здесь: ? Done — логическая переменная, которая устанавливается в true (истина), если мы нашли нетупиковое дерево и построили код нового дерева. Если Done так и не установилось в true, значит, нам на вход поступил код «последнего» дере- дерева, и по условию задачи нужно вывести 0. В противном случае — выводим эле- элементы массива д, которые хранят код результирующего дерева; ? CheckTree — рекурсивная процедура поиска и проверки «нетупиковых» дере- деревьев «справа налево». procedure CheckTree(p.r:integer); begin if Done then exit: if (a[p]>a[p+l]+2) and (r-P>-2) then begin q:-p+a[p+l]: inc(k); b[k]:=q: c[k]:-a[q]: a[q]:-a[p]-a[p+l]: CheckTree(q.r): for i:-l to k do a[b[i]]:-c[1]: if (not Done) and (a[p+l]>2) then CheckTree(p+l.p+a[p+l]). end else begin if (not Done) and (a[p+l]>2) then CheckTree(p+l.p+a[p+l]):
132 Глава 3 * Решение задач на деревьях и с помощью деревьев if Done then exit: i:-r; while (i>p) and ((a[i-l]-a[i]<2)) do dec(i); if i>p then begin inc(a[i]); for j:-i+l to L do a[j]:-l: Done:*true: end end; end: Здесь: а (аЬА > аЬ> + 1] + 2) — основное условие наличия правого дерева; ? q — номер позиции в массиве, на которую нужно внести отсутствующую в ко- коде вершину правого дерева; ? a[q] e aty] - а^р + 1] — количество вершин в правом дереве; О b и с — используются для сохранения/восстановления затираемых элементов в массиве; Q CheckTreety + 1, р + а^р + 1]) — вызов проверки левого поддерева. Полный текст решения приведен в конце главы. 3.6.2. Задача «Trees Made to Order» East Central North America Programming Contest, 2001 Trees Made to Order (input.txt / output.txt / 10 c) Мы можем перенумеровать двоичные деревья по следующей схеме: пустое дерево получает номер 0; дерево с одной вершиной получает номер 1; все двоичные деревья с т вершинами получают номера меньше, чем все дво- двоичные деревья с т+\ вершиной; любое двоичное дерево с т вершинами и ле- левым и правым поддеревьями L и R получает такой номер л, что все деревья с т вершинами и номером, большим я, обладают одним из следующих свойств: 1) левое поддерево имеет номер больший, чем L; 2) левое дерево имеет тот же номер, что и I, а правое дерево имеет номер, больший, чем R. Первые 10 деревьев, а также дерево с номером 20 изображены на рис. 3.34. Ваша задача — вывести двоичное дерево по его номеру. Ввод: Ввод состоит из множества тестов. Каждый тест — одно целое число N A <N< < 500 000 000). Значение Afe 0 означает конец ввода. Вам не нужно выводить пу- пустое дерево. Вывод: Для каждого теста выведите одну строку, содержащую дерево, соответствующее введенному номеру. Для вывода дерева используйте следующую схему:
3.6. Перечисление деревьев 133 ? дерево без потомков обозна 0 1 X 2 3 X X \ / чается как X; 4 X \ 5 6 X X \/\ 7 X / 8 X / 9 X \ 20 X / XX X XX XX \l W \/\ XX XX XX X \ Рис. 3.34. Первые 10 двоичных деревьев и двоичное дерево с номером 20 Q дерево с левыми и правыми поддеревьями L и R нужно выводить как (L')X(R'), где L' и R' — представления деревьев L и R\ Q если L пусто, выводите просто X(R'); ? если R пусто, выводите просто (I')X. Пример ввода: 1 20 31117532 0 Пример вывода: х ((X)X(X))X (X(X(((X(X))X(X))X(X))))X(((X((X)X((X)X)))X)X) Итак, среди деревьев с одинаковым количеством вершин меньший номер получа- получает то дерево, у которого меньший номер имеет левое поддерево, а при одинаковых левых деревьях — то, у которого меньше номер правого поддерева. На входе задается номер дерева, нам нужно вывести его представление в виде соответствующего скобочного выражения.Хуказывает на корень поддерева; (..)X указывает на наличие левого поддерева; X(...) — указывает на наличие правого поддерева. Например, для дерева с номером 20 (рис. 3.35) скобочная запись имеет вид: ((X)X(X))X. X / \ X X Рис. 3.35. Двоичное дерево с номером 20
134 Глава 3 • Решение задач на деревьях и с помощью деревьев Описание решения: Прежде всего мы можем подсчитать, сколько существует двоичных деревьев с за- заданным количеством вершин К. Известно, что b[0] = b[\] = 1 по определению. (Пустое дерево — одно, и дерево с одной вершиной тоже одно). Для любого К > 2 b[K] - b[O]*b[K-l] + b[l]*b[K-2]+...+ b[K-l]*b[k]. Действительно, дерево с К вершинами имеет левое и правое поддерево соответ- соответственно с 0 и К-\ вершинами, 1 и K-2,..., K-l и 0. Далее мы можем получить абсолютный номер последнего дерева с К вершинами: a[k]:-b[O]+b[l]+...+b[k] Теперь, получив на входе номер дерева Nt мы можем с помощью a[i] узнать, сколько вершин k в этом дереве, исходя из выполнения соотношения a[#-l] <N<a[K], а также узнать, какой номер этого дерева среди деревьев, имеющих ровно К вер- вершин: N - <z[tf-l]. Далее можно узнать, сколько вершин в левом (!) и правом (R) поддеревьях текущего дерева. L выбирается как максимальное такое, что a[k-l]+b[O]*b[K-l]+ b[l]*b[K-2]+...+ b[L]*b[k-l-L]<N Тогда R - К - 1 - L (одна вершина — в корне дерева). И, наконец, нужно вычис- вычислить какие абсолютные номера имеют левое (NL) и правое поддеревья (NR). Для этого сначала вычислим относительные номералевого и правого поддеревьев среди деревьев с соответствующим количеством вершин. Если в левом или правом под- поддереве 0 A) вершин, то это поддерево имеет номер 0 A). Противоположное дере- дерево имеет номер N-a[k-l]+b[O]*b[K-l]+b[l]*b[K-2]+...+b[L]*b[k-l-L] Если же в левом и правом поддеревьях более одной вершины, то номера левого и правого поддеревьев находятся в результате исполнения следующего алгоритма: NT - a[k-l] . NL - 0 Пока (NT ¦ b[R])< N ' NT - NT + b[R] NL - NL ¦ 1 NR - N - NT То есть пока добавление к текущему номеру дерева (NT) количества всех деревь- деревьев с R вершинами(В[Я]) не превышает номер дерева N, увеличиваем на 1 номер левого поддерева (NL). Когда добавить количество всех правых деревьев нельзя, поскольку будет превышен заданный номер дерева N, вычисляем номер правого поддерева (NR - N - NT). В завершение к полученным относительным номерам деревьев (NL, NR) приба- прибавим количества всех деревьев с L - 1 и R - 1 вершинами соответственно: NL - NL + a[L-l] NR - NR + a[R-l]
3.6. Перечисление деревьев 135 Далее работу по разложению дерева на поддеревья нужно рекурсивно продол- продолжать с обоими поддеревьями до тех пор, пока не получим дерево с одиой или ну- нулем вершин. По выходу из рекурсии формируем строку ответа по правилам: Если число вершин - 0 то S - 'пустая строка' Если число вершин - 1 то S - X Если SL (строка левого поддерева) не пуста то S - (SL)X иначе S - X Если SR (строка правого поддерева) не пуста то S - S + (SR) Рассмотрим подробнее реализацию. Тело главной программы имеет вид: begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'); rewrite(output): BuildB: SumUpB: while true do begin readln(N); if N-0 then break: BuildS(N.S): writeln(S); end: close(input): close(output); end. Здесь: ? BuildB — процедура, которая строит массив b[i] (количества деревьев с i вер- вершинами); ? SumUpB — суммирует элементы fi, строя массив a[j] (абсолютный номер по- последнего дерева cj вершинами); ? BuildS — рекурсивная процедура, которая по заданному номеруМстроит сим- символьное представление дерева 5. procedure BuildS(N:longint: var S:string); var NL.NR : longint: SL.SR : string. begin if Ne0 then begin S:*": exit: end: if N-1 then begin S:-'X', exit: end: Find(N.NL.NR): BuildS(NL.SL): BuildS(NR.SR):
136 Глава 3 * Решение задач на деревьях и с помощью деревьев if SL<>" then S:-T+SL+')' + 'X' else S:-'X'; if SR<>" then S:-S+'C+SRO': end: Процедура BuildS вызывает процедуру Find, которой передается абсолютный но- номер текущего дерева N, а она возвращает абсолютные номера левого и правого поддеревьев этого дерева: NL и NR. procedure Find(N:1ongint: var NL.NR.longint): begin k:-Place(N): {К-во вершин в дереве} NT:-a[k-l]: i:-0: While (NT+b[1]*b[k-l-i]<N) do begin 1nc(NT.b[i]*b[k-l-1]): inc(i): end: L:-1: R:-K-l-L; {К-во вершин в правом поддереве} NL:-1: NR:--1: if (L-0) ог (L-1) then NL:-L: if (R-0) ог (R-1) then NR:=R: if NL<0 then NL:-N-NT; if NR<0 then NR:-N-NT; if (L<>0) and (L<>1) and (R<>0) and (R<>1) then begin NL:-0: while (NT+b[R]<N) do begin inc(NT.b[R]): inc(NL) end: NR:*N-NT: end: inc(NL.a[L-l]): inc(NR.a[R-l]): end: Процедура Яя^реализует описанный выше алгоритм. Вначале вызывается функ- функция Place, которая находит место Ымежду a[i] и, соответственно, количество вер- вершин в текущем дереве. Затем операторы NT:-a[k-l]: i:-0: While (NT+b[i]*b[k-l-i]<N) do begin inc(NT.b[1]*b[k-l-i]): inc(i): end: L:-i: R:-K-l-L; находят количества вершин в левом (I) и правом (R) поддеревьях. А операторы NL:-0: while (NT+b[R]<N) do
3.7. Битово-индексированные деревья 137 begin inc(NT.b[R]). inc(NL) end; NR:-N-NT; находят относительные номера левого и правого поддеревьев (NL, NR) среди де- деревьев с соответствующим количеством вершин для общего случая (I <> 0, L <> 1, R <> 0, R <> 1). В конце главы приведен полный текст решения задачи. 3.7. Битово-индексированные деревья В данном разделе приводится условие задачи «Мобильные телефоны», а затем несколько попыток решить эту задачу. В завершение приводится авторское ре- решение задачи, опирающееся на структуру данных «битово-индексированные деревья», и необходимая для понимания решения информация о самих битово- индексированных деревьях. 3.7.1. Задача «Мобильные телефоны» Международная олимпиада по информатике, 2001 Мобильные телефоны Предположим, что базовые станции для мобильных телефонов 4-го по- поколения, расположенные в районе Тампере, функционируют следующим образом: район поделен на квадраты. Квадраты образуют матрицу 5 х 5, в ко- которой строки и столбцы нумеруются от 0 до 5 - 1. Каждый квадрат содер- содержит базовую станцию. Количество активных мобильных телефонов внутри квадрата может изменяться, потому что телефоны перемещаются из квад- квадрата в квадрат или потому, что телефоны включаются и выключаются. Вре- Время от времени каждая базовая станция сообщает изменения в своем коли- количестве активных телефонов на главную базовую станцию, единственную для всех строк и столбцов матрицы. Напишите программу, которая получает эти отчеты и отвечает на запросы о текущем общем количестве активных мобильных телефонов в любой об- области прямоугольной формы. Ввод и вывод: Ввод необходимо читать из стандартного ввода как целые числа, и ответы на за- запросы нужно выводить в стандартный вывод как целые числа. Каждый ввод зада- задается на отдельной строке и содержит одну инструкцию и некоторое количество параметров (все — целые числа), в соответствии с табл. 3.3. Таблица 3.3. Инструкции ввода Инструкции Параметры Значения 0 S Инициализировать матрицу размером S x S так, чтобы она содержала все нули. Эта инструкция дается только один раз и будет первой инструкцией продолжение &
138 Глава 3 * Решение задач на деревьях и с помощью деревьев Таблица 3.3 {продолжение) Инструкции Параметры Значения 1 X Y А Прибавить к количеству активных телефонов в квад- квадрате (X, Y). А может быть как положительным, так и отрицательным 2 L В R Т Выдать текущее количество активных телефонов в квадратах (X, Y) из прямоугольной области L<X<R,B<Y<T 3 Завершает работу программы. Эта инструкция дается только один раз и будет последней инструкцией Все значения всегда будут в указанных диапазонах, поэтому нет необходимости их проверять. В частности, если Л отрицательное, можно полагать, что оно не уста- установит количество мобильных телефонов в квадрате в отрицательное значение. Ивдексация начинается от 0. То есть для таблицы размером 4 х 4 мы имеем 0 < X < 3, 0 < У< 3. Ваша программа не должна отвечать на инструкции, кроме инструкции 2. Если же инструкция — 2, то программа должна вывести в стандартный вывод ответ на запрос, как одно целое число в строке. Инструкции no программированию: В примерах, приведенных далее, целое last — это последнее целое, которое вы про- прочитали из входной строки, a answer— целая переменная, которая содержит ваш ответ. Если ваша программа написана на Паскале, вы должны использовать сле- следующий способ чтения из стандартного ввода и записи в стандартный вывод: Read(last): . . Readln. WrHeln(answer): Пример ввода Пример вывода Пояснение 0 4 Инициализировать таблицу 4 х 4 1 1 2 3 Добавить 3 в позицию A, 2) 2 0 0 2 2 Запрос на сумму прямоугольника 0 <, X <, 2, 0 <, Y ? 2 3 Ответ на запрос 1 1 1 2 Добавить 2 в позицию A, 1) 1 1 2 -1 Добавить -1 в позицию A, 2) 2 1 1 2 3 Запрос на сумму прямоугольника 1 <, х < 2,1 < Y ? 3 4 Ответ на запрос 3 Завершить работу программы Ограничения: Размер таблицы SxS 1 x t<5x S< 1024 х 1024 Значение ячейки V в любое время 0 < V < 215 -1 Величина изменения Л ~21Г) <A < 215-1 Количество U инструкций на вводе 3 < U <, 60 002 Максимальное количество телефонов во всей таблице М - 230
3.7. Битово-индексированные деревья 139 Из 20 тестовых примеров— 16 таких, в которых таблица не превышает 512x512. Описание решения: Первое решение — самое простое — реализовать непосредственно хранение дан- данных в двумерном массиве и суммировать количества двойным циклом: program ioOldltl: const MaxS - 100: var SS : array [-l..maxS-l.-l..MaxS-l] of longint; Cm.X.Y.A.B.T.L.R.i.j.S : integer: Ans : longint: procedure Init: beg1n readln(S): for y:--l to S-1 do for x:-l to S-1 do SS[y.x]:-0; end: procedure Add: begin readln(X.Y.A): inc(SS[Y.X].A): end: procedure Result: begin readln(L.B.R.T); Ans:-0: for i:-B to T do for j:-L to R do 1nc(Ans.SS[1.j]): writeln(Ans): end: begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'): rewrite(output): Cm:-1; while (cm<>3) do begin read(Cm):
140 Глава 3 • Решение задач на деревьях и с помощью деревьев case 0 : 1 : 2 : 3 : end; end: end. Cm of Init: Add: Resul Понятно, что сложность получения ответов на запросы имеет порядок 5 х 5. На ори- оригинальных тестах такое решение выдает правильные ответы только на первых пяти тестах, но не успевает по времени на шестом (размер массива 250 х 250). Другой вариант решения — хранить для каждой клетки суммарное количество мобильных телефонов в текущей строке от первой колонки до текущей, а для выда- выдачи ответов суммировать соответствующие разности. Такой алгоритм имеет сложность порядка 5. То есть при изменении количества телефонов в клетке {X, Y) мы де- делаем одни и те же изменения во всех клетках от клетки (X, Y) до клетки E - 1, У). В результате процедура Add6ynpv выглядеть следующим образом: procedure Add: begin readln(X.Y.A): for j-X to S-1 do inc(SS[Y.j].A): end: При получении ответа накапливаем разности, соответствующие прямоугольной области запроса. Для чего процедура #е$и&записызается следующим образом: procedure Result: begin readln(L.B.R.T); Ans -0: for 1:-B to Т do inc(Ans.(SS[i.R]-SS[1.L-l])): writeln(Ans); end: Такое решение, имеющее сложность 5, выдает правильные ответы на первых 8 тестах, не успевая по времени начиная с теста 9 (размер массива 441 х 441). Третье решение предлагает хранить дерево сумм в одномерном массиве 55(рис. 3.36): 55[0] — хранит сумму чисел во всем квадрате; 55[1] — хранит сумму чисел в первом квадранте; SS[2] — хранит сумму чисел во втором квадранте; SS[3] — хранит сумму чисел в третьем квадранте; SS[4] — хранит сумму чисел в четвертом квадранте; 55[5] — 55[8] — хранят суммы подквадрантов 1 -го квадранта; 55[9] — 55[12] — хранят суммы подквадрантов 2-го квадранта;
3.7. Битово-индексированные деревья 141 2 1 3 4 I—> 2 1 3 4 @,0) X @,0) Рис. 3.36. Порядок квадрантов для хранения сумм 55[13]- 55[16] — хранят суммы подквадрантов 3-го квадранта; 55[17] — 55[20] — хранят суммы подквадрантов 3-го квадранта. И так далее. При изменении количества телефонов в клетке (X, У) мы проводим соответствую- соответствующие изменения во всех квадрантах, содержащих данный квадрант. Очевидно, что их не более чем log 5. При получении запроса на подсчет числа мобильников на задан- заданной прямоугольной территории, мы вычисляем ответ на вопрос как СУММУ коли- количеств телефонов во всех непересекающихся квадрантах, участвующих в покрытии заданной территории. В конце главы приводится текст соответствующего решения. Это решение, к сожалению, не прошло все тесты. Полное решение автор книги придумать не смог, потому пришлось разбираться с авторским решением, кото- которое вместе с другими материалами IOI-2001 выложено по адресу ftp://ftp.cs.uta.fi/ pub/reports/pdf/A-2001 -7.pdf. 3.7.2. Структура данных BIT Автор задачи предлагает использование такой структуры хранения данных, как Б-деревья {Binary Indexed Tree, далее мы будем использовать аббревиатуру BIT). Рассмотрим вначале упрощенный вариант задачи в случае размерности 1. То есть, если требуется искать различные суммы подряд идущих элементов одномерного массива. Вместо хранения самих элементов предлагается хранить предвычисленные сум- суммы следующим образом. Пусть m[i] — элементы массива, q[i] — предвычисленные суммы. Таблица 3.4 показывает, сумму каких элементов массива m[i] будет хранить эле- элемент q[i] (в примере размерность обоих массивов — от 1 до 16). Факт вхождения элемента m[i] в сумму q[j] отображается символом X в элементе таблицы на пересечении строки i и столбца/ Например, в таблице отображено, что элемент m[ 1 ] входит в суммы q[ 1], q[2], q[4], q[8] и q[16], и что q[12] — это сумма элементов m[9], m[10], m[ll] и m[12]. В общем случае элемент q[i] содержит сумму элементов m[i] из интервала [i-2 ^k+ +1, i], где k — количество «хвостовых» нулей в двоичном представлении числа i. Например, вычислим интервал для вышеупомянутого
142 Глава 3 • Решение задач на деревьях и с помощью деревьев Таблица m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 q 1 X 3. 2 X X 4. 3 X Элементы массива 4 X X X X 5 X 6 X X 7 X 8 X X X X X X X X 9 X 1 0 X X 1 1 X 1 2 X X X X 1 3 X 1 4 X X 1 5 X 1 6 X X X X X X X X X X X X X X X X Двоичное представление 12 — 1100. Количество «хвостовых» нулей равно 2. Сле- Следовательно, к = 2. Тогда левая граница интервала: 12 - 22 + 1 = 9. Рассмотрим, как поддерживать массив q в корректном состоянии при модифика- модификации (увеличении/уменьшении на величину Л) элемента m[i]). Нужно соответствующим образом изменить элементы массива q, которые имеют не- некоторые индексыу в интервале от i до 16. При этом индекс следующего элементами находитсячерезиндекспредыдущегоэлементаурследующимобразом:^.* e jp + 2^k, где к — количество «хвостовых» нулей в двоичном разложении числа^р. Пусть мы изменили элемент a[l]. Начинаем cjp = 1, к = 0 (количество «хвосто- «хвостовых» нулей в i).jn =jp + 2^0 = 1 + 1 = 2. Пустьreuepbjp = 2, тогдаk = 1, ajn - 4. Продолжая процесс, мы получим ряд элементов массива q, которые нужно моди- модифицировать при изменении m[lJ: cy[lJ, #[2], ^[4], <7[8], <7[l6]. Построенный таким образом массив q позволяет легко находить суммы элементов массива т с но- номерами от 1 до Р. Обозначим такую сумму F(P). Тогда для нахождения суммы элементов массива т с номерами от L до Ядостаточио найти F(R)-F(L-\). Уточним, как с помощью массива S находить суммы типа F(T). Для этого до- достаточно просуммировать элементы массива q, начиная от Ги уменьшая индекс на величину 2* до тех пор, пока i > 0. Здесь, как и везде раиее, k — количество «хво- «хвостовых» нулей в двоичном представлении текущего индекса. Например, пусть мы хотим получить сумму элементов от 1 до 12. Значит, мы долж- должны сложить элементы с индексами 12 и 8. Как это получается: первый индексA2) имеет двоичное представление 1100. k e 2. 2* = 4. 12 - 4 = 8. Двоичное представ- представление 8 - 1000. k - 3, 2* = 8. 8 - 8 - 0. Процесс закончен.
3.7. Битово-индексированные деревья 143 Теперь применим данный подход к исходной задаче. То есть в элементе двумер- двумерного массива SS(R, Т) мы будем хранить частичные суммы значений элементов исходного массива таким образом, чтобы функция Sum(R, 7} могла легко под- подсчитать значение суммы элементов исходного массива в прямоугольниках вида [1..ДД..7]. Тогда сумма элементов в прямоугольнике [L..R, B..T] находится как: Ans:-Sum(R.T)-Sum(L-l.T)-Sum(R.B-l)+Sum(L-l.B-l) Элемент двумерного массива SS[X, Y\ хранит сумму элементов прямоугольного подмассива, в котором Хи Усоответствуют описанным правилам изменения ин- индексов для одномерного массива. Рассмотрим конкретную реализацию этого подхода. Главная программа не пре- претерпела никаких изменений ио сравнению со всеми предыдущими версиями. begin assign(input.'input.txt'): reset(input); assign(output.'output.txt'); rewrite(output); Cm:-1: while (cm<>3) do begin read(Cm); case Cm of 0 : Init: 1 : Add: 2 : Result: 3 : ; end; end: end. Процедура Init используется для ввода и инициализации данных. procedure Init: begin readln(S): for 1:-1 to MaxS do for j:-l to MaxS do SS[1.j]:-O: S2:-NearestP2(S): for 1:-1 to S2 do begin k:-l: j:-1: while (j and l)-0 do begin j:-j shr 1: k:-k shl 1: end: ad[i]:-k: end: end: Очевидно, обнуляется двумерный массив SS. Кроме того, величина S2 получает значение «ближайшая большая или равная по отношению к 5 степень числа 2».
144 Глава 3 * Решение задач на деревьях и с помощью деревьев И далее для i от 1 до S2 заранее заполняем массив ad[i] константами перехода от предыдущего индекса к следующему (ad[i] — количество «хвостовых» нулей в дво- двоичном представлении числа i). Функция NearestP2 вычисляет 52, используя оператор выбора: function NearestP2(s:integeD:integer: var Temp : integer; begin case S of 1 2..4 .8 .16 .32 5. 9. 17. Temp:-1: Temp:-4: Temp:-8: Temp:-16: Temp:-32; 33..64 : Temp:-64; 65..128 : Temp:-128: 129..256 : Temp:-256: 257..512 : Temp-512: 513..1024 : Temp:-1024; end: NearestP2:-Temp: end: Рассмотрим теперь процедуру добавления количества А мобильных телефонов в квадрат [X, Y]: procedure Add: begin readln(X.Y.A): i:-X+l: while К- S2 do begin j:-Y+l: while j<- S2 do begin inc(SS[i.j].A); inc(j.ad[j]): end: inc(i.ad[i]); end: end: Здесь выполняется именно то, о чем говорилось выше, — инкрементируется не- некоторое количество элементов массива SS[i,j]. При этом индексы i nj выбирают- выбираются исходя из описанных выше правил. Процедура вывода ответа Resuk реализует описанный подход: procedure Result: begin readln(L.B.R.T):
3.8. Задачи для самостоятельного решения 145 1nc(L): inc(B): inc(R): inc(T): Inc(N): Ans:-Sum(R.T)-Sum(L-l.T)-Sum(R.B-l)+Sum(L-l.B-l): writeln(Ans): end: Вызывая функцию Sum: function Sum(R.T:integer):long1nt: var Ans: long1nt: beg1n i:-R: Ans:-0; while i>0 do begin J:-T: while j>0 do begin inc(Ans.SS[i.j]): dec(j.ad[j]): end: dec(i.ad[i]): end: Sum:-Ans: end: Функция Sum вычисляет сумму элементов массива SS[i, j], индексы которых под- подчиняются описанным выше закономерностям. Полный текст решения приводит- приводится в конце главы. 3.8. Задачи для самостоятельного решения Ничто так не проверяет приобретенные навыки, как выполнение заданий для са- самостоятельного решения. Здесь приводятся четыре таких задачи: «Knockout Tournament>, «Split Windows*, «Haffman Trees», «Рге-Post-erous!*. Свои реше- решения, как обычно, можно проверить на сайте http://dl.gsu.unibel.by в разделе «Тре- «Тренировочный курс ACM>. 3.8.1. Задача «Knockout Tournament» North America Programming Contest, 2002 Knockout Tournament (tour.ln / tour.out / 5 c) В турнире с выбыванием участвуют 2" игроков. Каждый проигравший выбывает из турнира. Победители играют попарно, новые победители проходят в следующий круг и так до тех пор, пока не останется только один победитель. Если мы перенумеруем игроков от 1 до 2я, и в первом раунде игра- ютшроки2& - 1 против2А,гдеА- l,2,..., 2л~\товесьтурнирможнопредставить
146 Глава 3 * Решение задач на деревьях и с помощью деревьев полным двоичным деревом. Победитель каждой пары указывается во внут- внутренних вершинах. На рис. 3.37 приведен пример турнира при п - 3. 1 8 /\ /\ 1 3 5 8 /\ /\ /\ /\ 1 2 3 4 5 6 7 8 Рис. 3.37. Пример дерева для задачи «Knockout Tournament» После турнира некоторые репортеры спорили относительно рейтинга игро- игроков по результатам этого турнира. Предполагается, что если игрок А выиграл у игрока В, который в свою очередь выиграл у игрока С, то это означает, что игрок А сильнее игрока С (победа транзитивна). Нет сомнений, кто самый лучший игрок. Однако для всех остальных игроков все не так однозначно. Например, игрок 2, проигравший победителю, может быть вторым по силе игроком (рейтинг 2), а может быть и самым плохим игроком турнира (рей- (рейтинг 8). Игрок 5 может быть 3-м по силе игроком (поскольку точно нам известно, что он слабее 1-го и 8-го), но не может иметь рейтинг меньше 7 (поскольку он обыграл одного игрока в первом раунде). Вам необходимо определить максимальный и минимальный возможные рейтинги игроков по заданным результатам турнира. Ввод: В файле содержится несколько тестов. Каждый тест состоит из трех строк. Пер- Первая строка содержит положительное число п (п < 8), указывающее, что в турнире приняли участие 2" игроков с номерами от 1 до 2" (разбитые вначале на пары, как описано ранее), п = 0 означает конец ввода. Следующая строка содержит резуль- результаты каждого раунда турнира, выписанные слева направо, начиная с первого ра- раунда. Например, представленный турнир задается так: 1 3 5 8 1 8 1 Последняя строка содержит положительное целое число т} за которым следуют числа kx%..., km — номера игроков, для которых нужно определить диапазон рейтинга. Вывод: Для каждого k. выведите одну строку в виде: Player ki can be ranked as high as h or as low as 1. в которую нужно подставить подходящие числа. Строки должны появляться в том же самом порядке, в котором k. задаются на входе. Результаты для разных тестов должны разделяться пустой строкой. Пример ввода: 3 1 3 5 8 1 8 1
3.8. Задачи для самостоятельного решения 147 2 2 5 4 2 3 6 7 9 11 14 15 3 6 9 15 6 9 6 4 1 15 7 6 0 Пример вывода: Player 2 can be ranked as high as 2 ог as low as 8. Player 5 can be ranked as high as 3 ог as low as 7. Player 1 can be ranked as high as 4 ог as low as 16. Player 15 can be ranked as high as 3 ог as low as 13. Player 7 can be ranked as high as 2 ог as low as 15. Player 6 can be ranked as high as 1 ог as low as 1. 3.8.2. Задача «Split Windows» USA Programming Contest, 2001 Split Windows (split.ln / split.out) Компания Dotty Software разрабатывает программы, обеспечивающие ввод на недорогой текстовый терминал. Одно из приложений для этой системы имеет вид главного окна, которое может разделяться на подокна. Ваша задача: по заданному описанию расположения окон нарисовать ми- минимальную решетку окон, соответствующую заданному описанию. В данной задаче мы сосредоточимся на границах окон. Поэтому содержа- содержание всех символов внутри окон — пробелы. Каждое окно, которое не имеет подокон, получает метку — одну прописную букву английского алфавита. Все метки различны. Границы окон должны отрисовываться символами, выбираемыми по следующим правилам: Мет- Метка ставится в левый верхний угол каждого неразделенного окна. Символ '•' ставится во все углы окон, где нет метки. Символы *—' используются для обозначения верхних и нижних границ окон, не являющихся углами. Сим- Символ '|' используется для обозначения вертикальных границ окон, не являю- являющихся углами. Например, экран на рис. 3.38 можно получить таким образом: вначале было окно с меткой M, затем оно было разделено вертикально, на левую и правую половины, и правая половина получила метку R. Затем левая половина была разделена на верхнюю и нижнюю части и нижняя часть получила метку С. М — R—* i i i c-* | I I I * * __ * Рис. 3.38. Пример отображения окон для задачи «Split Windows»
148 Глава 3 * Решение задач на деревьях и с помощью деревьев Для каждой последовательности разделений существует двоичное дерево символов, которое описывает эту последовательность. Ниже приводятся общие правила для построения разбиений и соответствующего двоичного дерева. 1. Окно может быть неразделенным прямоугольником. Такое окно имеет заглав- заглавную английскую букву в качестве метки. Дерево для такого окна содержит толь- только эту метку. 2. Окно может быть разделено на верхнее и нижнее подокно или на левое и пра- правое подокно. Соответствующие деревья в качестве корня граничный символ разделения:f-' или '|\ Корень имеет левое и правое поддеревья соответствую- соответствующие верхнему/нижнему или левому/правому подокнам. Такие деревья могут быть представлены с помощью прямого (нисходящего) обхода. Каждое нераз- неразделенное окно должно иметь как минимум один символ внутри. Следователь- Следовательно, с каждым деревом разделений может быть ассоциирован минимальный размер внешнего окна. Очевидно, что даже при минимальном размере внеш- внешнего окна, не все неразделенные окна содержат только один символ. Ввод: Первая строка ввода содержит одно целое число - общее количество заданных обходов, описывающих вложенные оконные структуры. За этой строкой следуют строки, каждая из которых описывает один нисходящий обход и содержит симво- символы '|\ '-' и от 1 до 26 заглавных английских букв. Вывод: Для каждого нисходящего обхода выведите номер обхода, а за ним минимальное окно, которое может представлять заданный обход. Гарантируется, что общее ко- количество строк и столбцов вывода не будет превышать 53. Пример ввода: 3 I-MCR |-|-ABC-D|E-FG-P-|Q|RST -|-|ABC|D-E|FG|P|-Q-RST A — C—P I I I ! I I ' D— *—Q—R—S — * I I I I I I -F-* I I I I T-*-*-* G-* I I Рис. 3.39. Пример отображения окон для задачи «Split Windows»
3.8. Задачи для самостоятельного решения 149 Пример вывода: 1 — изображение, показанное на рис. 3.38; 2 — изображение, показанное на рис. 3.39; 3 — изображение, показанное на рис. 3.40. А — В —D — E * I I I I I С—*—* F—G-* I I I I I р Q-* Т * — * I I R * I I S * I I Рис. 3.40. Пример отображения окон для задачи «Split Windows» 3.8.3. Задача «Huffman Trees» North Western European, Programming Contest 2002 Huffman Trees (trees.in / trees.out / 25 c) Относительно простой метод сжатия данных работает созданием так назы- называемого дерева Хаффмана для файла и использованием его для компресси- ,и и декомпрессии данных, которые содержит это файл. В большинстве слу- случаев используется двоичное дерево Хаффмана (то есть каждая вершина дерева либо является листом, либо имеет ровно двух потомков). В общем случае дерево Хаффмана может быть с любым количеством поддеревьев. Дерево Хаффмана для файла, содержащего7различных символов, имеет Z листьев. Путь от корня к листу, который представляет этот символ, опреде- определяет кодирование этого символа. Каждый шаг к листу определяет кодиру- кодирующий символ (который может быть 0,1,..., JV- 1 в случае N-арного дерева Хаффмана). Размещая наиболее часто используемые символы ближе к корню, а менее часто используемые символы дальше от него, мы достигаем желаемого уров- уровня сжатия. Строго говоря, такое дерево будет деревом Хаффмана только если в резуль- результате такого кодирования мы используем минимальное количество N-арных символов для кодирования всего файла. В данной задаче мы рассмотрим только деревья, каждая вершина которых или внутренняя, или лист, кодирующий символ. Рисунок 3.41 показывает при- пример троичного дерева Хаффмана. Символы а и е кодируются с использовани- использованием только одного троичного символа. Менее часто встречающиеся символы
150 Глава 3 * Решение задач на деревьях и с помощью деревьев s и р кодируются двумя троичными символами. Наконец, наиболее редко встречающиеся символы x, q, у кодируются тремя троичными символами каждый. s 00 X 010 I I * I q 011 p 02 У 012 Рис. 3.41. Пример троичного дерева Конечно, если мы хотим декодировать поток N-арных символов, нам необ- необходимо знать дерево, которое использовалось для кодирования исходного текста. Это может быть сделано несколькими способами. В данной задаче потоку закодированных символов предшествует заголовок — поток коди- кодирования Z символов, встреченных в файле, в алфавитном порядке. Задание: Вам дано количество исходных символов Z, значение N, обозначающее «-арность» дерева Хаффмана, и заголовок. Постройте отображение исходных символов в за- закодированные символы. Ввод: Ввод начинается с целого числа Г, обозначающего количество тестов. Далее для каждого из тестов следуют три строки. 1. Количество Z различных символов в файле B < Z < 20). 2. Число N, обозначающее «арность» дерева Хаффмана. 3. Строка, представляющая заголовок полученного сообщения; каждый символ будет цифрой в диапазоне от 0 до (ЛМ). Эта строка будет содержать не более 200 символов. Теоретически возможны заголовки с неоднозначной интерпретацией (например, заголовок 010011101100 при Z- 5 и N- 2). Однако вам гарантируется, что все представленные в тесте заголовки имеют однозначное решение. Вывод: Для каждого из Гтестов вывeдитeZcтpoк, дающие представление кодирования каждого из Zcимвoлoв в порядке возрастания. Используйте формат исходный -> кодирование, где «исходный» - число в интервале от 0 до Z-1, а кодирование — строка цифр, кодирующих этот символ, каждая цифра неотрицательна и меньше N.
3.8. Задачи для самостоятельного решения 151 Пример ввода: 3 3 2 10011 4 2 000111010 19 10 01234678950515253545556575859 Пример вывода: 0 н 1 н 2 н 0 н 1 н 2 н 3 н 0 н 2 н 3 н 4 н 5 н 6 н 7 н 8 н 9 н 10 - 11 - 12 - 13 - 14 - 15 - 16 - 17 - 18 - > 10 > 0 > 11 > 00 > 011 > 1 > 010 > 0 > 2 > 3 > 4 > 6 > 7 > 8 > 9 > 50 -* 51 -» 52 -* 53 -> 54 -> 55 -> 56 + 57 -* 58 ¦> 59 3.8.4. Задача «Pre-Post-erous!» East Central North America Programming Contest, 2002 Pre-Post-erous! (post.in / post.out / 5 c) Известны стандартные варианты обхода двоичных деревьев (нисходящий, фланговый, восходящий). Часто встает задача найти нисходящий обход двоичного дерева, если заданы его фланговый и восходящий обходы.
152 Глава 3 * Решение задач на деревьях и с помощью деревьев Или вы можете найти восходящий обход двоичного дерева, если известны его фланговый и нисходящий обходы. Однако в общем случае невозможно определить фланговый порядок об- обхода двоичного дерева, если даны его нисходящий и восходящий обхо- обходы. Для примера рассмотрим четыре двоичных дерева, показанные на рис. 3.42. Рис. 3.42. Примеры двоичных деревьев для задачи «Pre-Post-erous» Все эти деревья имеют одинаковые нисходящий и восходящий обходы. Однако, очевидно, они различны и имеют различные фланговые обходы. Такой феномен присущ не только бинарным, но и N-арным деревьям. Ввод: Ввод содержит множество тестов. Каждый тест состоит из одной строки вида: m sl s2 указывающей, что мы рассматриваем m-арные деревья, sl — нисходящий обход дерева, s2 — восходящий обход этого же дерева. Все обходы задаются маленьки- маленькими латинскими буквами. Для всех тестов 1 < т < 20, длины sl и 52 — от 1 до 26. Если длина51 равна k (длина 52, очевидно, тоже равна k), то первые k букв алфа- алфавита используются в строке s\ (и 52). Строка, содержащая число 0, означает ко- конец файла. Вывод: Для каждого теста Вы должны вывести строку, содержащую количество возмож- возможных деревьев, которые имеют такие же нисходящий и восходящий обходы. Гаран- Гарантируется, что все выводимые значения не превысят 32-битного знакового числа. Также гарантируется существование хотя бы одного дерева с заданными нисхо- нисходящим и восходящим обходами. Пример ввода: Пример вывода: 2 abc cba 4 2 abc bca 1 10abcbca 45 13 abejkcfghid jkebfghicda 207352860 0
3.9. Решения задач 153 3.9. Решения задач Листинг 3.1. Текст программы к задаче «Is it a tree?» program nc97b: Type TInt - ShortInt: const MaxN-100: GREAT-120: var G : аггау [l..MaxN.l..MaxN] ofTInt: V : аггау [l..MaxN] of TInt: N.x.y.i.j.Nodes.Edges.MaxNode : TInt: procedure Max(var Nodes.x.y:TInt); begin if x>Nodes then Nodes:-x; if y>Nodes then Nodes:-y; end: procedure InputData: var i.j. Holes : TInt: begin MaxNode:-0: for i:-l to MaxN do for j:-l to MaxN do G[i.j]:-GREAT: for i:-l to MaxN do V[i]:-0: read(x.y): Nodes:-0: Edges:-O: if х-1 then begin close(input): close(output): halt@): end: while (xo0) do begin g[x.y]:-l: v[x]:-l: v[y]:-l: Max(MaxNode.x.y): inc(Edges): read(x.y) end: Holes:-0: for i:-l to MaxNode do А продолжение &
154 Глава 3 * Решение задач на деревьях и с помощью деревьев ЛистингЗ.1 (продолжение) if v[i]-0 then inc(Holes); Nodes:-MaxNode-Holes: x:-l: end: function min(x.y:Tint):Tint: begin if x<y then min:-x else min:-y: end: function Connected:boolean: var i.j.k : TInt: begin for i:-l to MaxNode do if v[i]-0 then for j:-l to MaxNode do begin g[i.j]:-l: g[j.1]:-l: end: for k:-l to MaxNode do for i:-l to MaxNode do for j «1 to MaxNode do G[i.j]:^nin(G[i.j].G[i.k]+G[k.j]): Connected:-true: for i:-l to Nodes do for j:-l to Nodes do if G[i.j]-GREAT then begin Connected:-false: exit: end: end: begin assign(input.'b.in'): reset(input): assign(output.'b.out'); rewrite(output): i:-0: x:-l: while x<>0 do begin inc(i): InputData:
3.9. Решения задач 155 if (Nodes<>O) and ((Edges<>Nodes-l) ог (not Connected)) then wnteln ('Case '.i.' is not a tree.') else writeln ('Case '.i.' is a tree ') end: end. Листинг 3.2. Текст программы к задаче «Strategic game» program seeOOa: const MaxN-1500: MaxE-20: var G : array [O..MaxN-l.l .MaxE] of integer; Power : array [O..MaxN-l] of integer: Deleted. Node. N. i. j. m : integer; procedure InputData: const Tab : char - #9: var k.j.b.e.p.m.o.t : integer; s : string: begin readln(N): for i:=0 to N-1 do Power[i]:-0: for 1:-0 to N-1 do begin readln(s): k:-pos(':'.s): m:-pos(')'.s): val(copy(s.l.k-l).b.j): val(copy(s.k+2.m-k-2).p.j): delete(s.l.m+l): s:=s+' '; for o:-l to p do begin while (s[lK ') or (s[l]-Tab) do delete (s.1.1); k:=pos(Tab.s); if k-0 then k:-pos(' '.s): val(copy(s.l.k-l).e.j); inc(Power[b]): g[b.Power[b]]:-e; inc(Power[e]): g[e.Power[e]]:-b: delete(s.l.k): end: end: end: function MaxPower(var Node:integer):integer; продолжение
156 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.2 (продолжение) begin m:-Power[N-l]: Node:-N-1: for i:-N-2 downto 0 do if Power[i]>m then begin m:-Power[i]: Node:-i; end: MaxPower:-m; end: procedure Ann1gilate(Node:integer); begin j:-l: for i:-l to Power[Node] do begin while Power[G[Node.j]]-0 do inc(j): dec(Power[G[Node.j]]): inc(j) end: Power[Node]:-0: end: begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'): rewrite(output): while not EOF(input) do begin InputData: Deleted:-0: while MaxPower(Node)<>O do begin inc(Deleted): Annigilate(Node): end: writeln(Deleted): end: close(input): close(output): end. Листинг 3.3. Текст программы к задаче «Оппозиция» program ruOlt2: const MaxN-10000: МахЕ-1000:
3.9. Решения задач 157 var G : array [l..MaxN.l..MaxE] of integer: Power : array [l..MaxN] of integer; DeletedNodes : array [l..MaxN] of integer; Deleted. Node. N. i. j. m. K : integer; procedure InputData: var i.b.e : integer; begin assign(input.'org.in'); reset(input): readln(K); readln(N); for i:-l to N do Power[i];-0: for e;-2 to N do begin read(b); inc(Power[b]); g[b.Power[b]]:-e: inc(Power[e]); g[e.Power[e]];-b; end; close(input): end: procedure OutputResults; begin assign(output.'org.out'); rewrite(output): writeln(Deleted); if Deleted<>0 then begin for i:-l to Deleted-1 do write(DeletedNodes[i].' '): writeln(DeletedNodes[Deleted]); end: close(output); end: function MaxPower(var Node.M:integer):integer; begin m:-Power[N]: Node:-N; for i:-N-l downto 1 do if Power[i]>m then begin m:-Power[i]; Node:-i: end; MaxPower:-m; en°: продолжение тР
158 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.3 (продолжение) procedure Annigilate(Node:integer): begin j:-l: for i:-l to Power[Node] do begin while Power[G[Node.j]]-0 do inc(j): dec(Power[G[Node.j]]); inc(j) end: Power[Node]:-0; DeletedNodes[Deleted]:-Node; end; begin InputData; Deleted:-0: while (MaxPower(Node.m)>=K-l) and (m<>0) do begin inc(Deleted): Annigilate(Node): end; OutputResults: end. Листинг 3.4. Текст программы к задаче «Erdos Numbers» program nweOOf; const MaxN-202: {32000} MaxE-100; MaxA-20: var G : array [l..MaxN.l .MaxE] of integer; Authors : array [l..MaxN] of string[MaxA]; Power : array [l..MaxN] of integer: CA : array [l..MaxN] of integer; Erdos • array [l..MaxN] of integer: i.T.P.N.NT.a.c.ii.jj : integer: s : string; procedure AddAuthorIfNew(s:string): var i : integer:
3.9. Решения задач 159 begin 1:-1: while (i<-a) and (Authors[i]<>s) do inc(i): if i>a then begin inc(a); Authors[a]:=s: end: inc(c); CA[c]:-i: end; procedure GetPaper: var i.j.k.q.b.e : integer: begin readln(s); i:-l: q:=*0; b:-l: c:=0; while (s[i]<>':') do begin if s[i] - *.' then inc(q): if q-2 then begin AddAuthorIfNew(copy(s.b.i-b)). b:-i+2: q:-0: end: inc(i): end; AddAuthorIfNew(copy(s.b.i-b)): for i:-l to c do for j:-l to c do if i<>j then begin ii:-ca[i]; jj:-ca[j]: k:-l: while (k<-Power[ii]) and (G[ii.k]<>jj) do inc(k); if k>Power[ii] then begin inc(Power[ii]); G[ii.k]:=jj: end: end: end: procedure BuildErdosNumbers: {BFS-Breadth-First Search} var n.j.BegQ.EndQ : longint: продолжение &
160 Глава 3 • Решение задач на деревьях и с помощью деревьев Листинг 3.4 (продолжение) Q : array [l..MaxN] of integer; Procedure Put(x:longint): begin inc(EndQ): Q[EndQ]:-x; end; Procedure Get(var x:longint); begin x:-Q[BegQ]: inc(BegQ); end; begin for i:-l to a do erdos[i]:--l; erdos[l];-0; BegQ:-l: EndQ:-O; Put(l); while (BegQ<-EndQ) do begin Get(i); for j:-l to Power[i] do if erdos[G[i.j]]<0 then begin Put(G[i.j]): erdos[G[i.j]]:-erdos[i]+l; end; end; end; procedure GetAuthorPrintResult: var i : integer; begin readln(s): write(s+' '): i;-l: while (i<-a) and (Authors[i]<>s) do inc(i); if (i>a) or (Erdos[i]<0) then writeln('infinity') else writeln(Erdos[i]): end; begin Authors[l];-'Erdos. P.'; assign(input.'input.txt'); reset(input):
3.9. Решения задач 161 assign(output.'output.txt'): rewrite(output): readln(NT): for t:-l to NT do begin writeln('Scenario '.t): readln(P.N): a:-l: for i:-l to P do GetPaper: BuildErdosNumbers: for i:-l to N do GetAuthorPrintResult: end: close(input): close(output): end. Листинг З.5. Текст программы к задаче «Closest Common Ancestor» program seeOOa: const MaxN-802: MaxE-20: var G : array [l..MaxN.l..MaxE] of integer: Power : array [l..MaxN] of integer: Ах.Ду.А : array [l..MaxN] of integer: B kB array [l..MaxN.l..MaxE] of integer: array [l..MaxN] of integer: N. i. j. m : integer: procedure InputTree: const Tab : char - #9: var k.j.b.e.p.m.o.t : integer: s : string: begin readln(N): for i:-l to N do Power[i]:-0: for i:-l to N do begin readln(s): k:-pos(':'.s): m:-pos(')'.s): val(copy(s.l.k-l).b.j); val(copy(s.k+2.m-k-2).p.j): delete(s.l.m+l); s:-s+' ': продолжение
162 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.5 (продолжение) for o:-l to р do begin while (s[l]-' ') ог (s[l]-Tab) do delete (s.l.l): k:-pos(Tab.s): if k-0 then k:-posC '.s): val(copy(s.l.k-l).e.j): inc(Power[e]): g[e.Power[e]]:-b: delete(s.l.k): end; end: end; procedure FindCCA(x.y:integer); var i : integer; begin for i:-l to N do begin Ax[i]:-0: Ay[i]:-O: end: Ax[x]:-1: АуСу]:-1: while (Power[x]-l) and (Ax[g[x.l]]<>l) do begin x:-g[x.l]: Ax[x]:-1: end: while (Power[y]-l) and (Ay[gCy.l]]<>l) do begin y:-g[y.l]; AyCyD:~1: end: i:-l: while (Ax[i]ol) or (Ay[i]<>D do inc(i): inc(A[i]): end: procedure InputProcessVerticePairs: var p.i.x.y.j.k.m : integer; s : string; begin for i:-l to N do A[i]:-0: readln(P): for i:-l to P do begin readln(s): k:-pos(' '.s): m:-posC)'.s): val(copy(s.2.k-2).x.j): val(copy(s.k+l.m-k-l).y.j):
3.9. Решения задач 163 FindCCA(x.y): end: end: procedure OutputResults: begin for i:-l to N do if a[i]<>0 then writeln (i. end: V.A[1]): begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'); rewrite(output): while not EOF(input) do begin InputTree: InputProcessVerticePai rs: OutputResults: end: close(input): close(output): end. Листинг З.6. Текст программы к задаче «Unscrambling Images» program pnw99d: var TI. SI. RI. RR : array [1..512] of integer: t.NT.N.M.k.i.S.j.SiP.SiC.N2 : integer: 6 : array[0..16] of integer: procedure FillSubTree: var i.j : integer: begin i:-SiP: while 4*i+l <- G[N] do i:-4*i+l: while true do begin for j:-0 to 3 do RI[1+j]:-SiC: inc(i,4): if ((i div 4) < G[SiP div 4]) and ((i div 4) > SIP) then i:- (i div 4) else exit: end: end: {Обход дерева квадратнов} {Спускаемся к листьям} {Закрашиваем 4 листа} {Есть куда наверх ?} {Подымаемся вверх} {Выходим} продолжение
164 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.6 (продолжение) procedure InputData; var х.у : integer; begin readln(N); {размерность картинки N*N} readln(M): {количество листьев в дереве тестового образа} G[0]:-0: G[2]:-4: G[4]:-20: G[8].-84: G[16]:-340: for i:-l to G[N] do begin TI[i]:-0: SI[i]:-O; RI[i]:-0 end: N2:-N div 2: for i:-l to М do begin readln(x.y); TI[y+G[N2]+l]:-x; {Кодирование эталонного образа} end: read1n(S): {количество листьев в дереве зашифрованного образа} if N-1 then begin readln(SiP.SiC); writeln ('Case '.t): writeln: writeln (SiC:4); writeln; exit; end: for i:-l to S do begin readln(SiP.SiC); {Читаем зашифрованный образ} if SiP>G[N2] then RI[SiP]:-SiC {Выставляем введенный цвет листу} else FillSubTree: {Закрашиваем все поддерево} end: end: procedure OutRI: var j.c.ci.cj : integer: begin if n-1 then exit: writeln CCase '.t): writeln: for i:43[N2]+l to G[N] do RR[1]:-RI[TI[i]]: {Расшифровываем образ} for i:-l to n do {Выводим ero} begin for j:-l to n do write (RR[G[N2]+A-l)*n+j]:4): writeln; end; writeln: end:
3.9. Решения задач 165 begin assign(input.'D.txt'): reset(input); readln(NT): {Количество тестов} for t:-l to NT do begin InputData: {Формирование образа} OutRI: {Раскодирование и вывод} end: close(input): end. Листинг 3.7. Текст программы к задаче «BSP Trees» program ecnaOOg: const alf : string - * ABCDEFGHIJKLMNOPQRSTUVWXYZf: NS - 20: NT - 2049: type string20 - string[NS]: var T : array [l..NT] of string20: Pof2 : array [1..12] of longint: A.B.C : array [1..10] of real: x.y : array [1..20] of real: i.j.NP.NL.xl.yl.x2.y2.m.k : longint: procedure InputOata: begin read(NP): for i—l to NP do readln(m.x[i].y[i]): read(NL): for i:-l to NL do begin readln(xl.yl.x2.y2): A[i]:-y2-yl: B[1]:-xl-x2: C[i]:-yl*(x2-xl)-xl*(y2-yl); end: Pof2[l]:-1: for i:-2 to NL+2 do pof2[i]:-2*Pof2[i-l]; for i:-l to Pof2[NL+2] do T[i]:-"; end: procedure WriteTree(j:integer): продолжение &
166 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.7 (продолжение) begin if (j>2048) then exit: if (j<-1024) and (T[2*j]o") then WriteTreeB*j): if (j<-1024) and (T[2*j+l]o"> then WriteTreeB*j+l): if (j>1024) and (T[j]o") ог (T[2*j]-") and (T[2*j+l]-"> then write(T[j]): end; procedure Split(i:integer; var S.Sl.S2:string20): begin for k:-l to length(S) do begin m:-ord(S[k])-ordCA')+l: if y[m]>(-(x[m]*a[1]+c[i])/b[1]) then Sl:-Sl+S[k] else S2:-S2+S[k]: end: end: begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'): rewrite(output): InputData; T[l]:-copy(alf.l.NP); for 1:-1 to NL do for j:-Pof2[i] to Pof2[i+1]-1 do if length(T[j])>l then SplitA.T[j].T[2*j].T[2*j+l]): WriteTree(l): close(input); close(output): end. Листинг 3.8. Текст программы к задаче «Дерево» {$R+.M 65000.0.8192} program byO2d2t3: const МахК - 300: var а : аггау [0..301] of 1ongint: i.j.K.N.s.g : longint; fm : аггау [0..MaxK+1.0..MaxK+l] of longint:
3.9. Решения задач 167 procedure SortA: var i.j.s.t : longint: begin for j:-l to К-1 do begin s:-a[j]: t:-j: for 1:-j+l to К do if a[i]<s then begin s:-a[i]: t:-i end: a[t]:-a[j]: a[j]:-s: end: end: function min(a.b:longint):longint: begin if a<b then min:-a else min:-b: end: function F(i.j:longint):longint: var fs.s : longint: begin if fm[i.j]<0 then begin if i>j then Fm[i.j]:-0 else begin fs:^axlongint: for s:-i to j do fs:-min(fs.F(i.s-l)+F(s+l.j)): fm[i.j]:-a[j+l]-a[i-l]-l+fs end: end: F:-fm[i.j] end: begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'); rewrite(output): read(N.K): for i:-l to k do read(a[i]): SortA: A[0]:-0: A[K+1]:-N+1; продолжение &
168 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.8 (продолжение) for i:-0 to К+1 do for j:-0 to К+1 do FM[i.j]:-l: writeln(F(l.k)): close(input); close(output); end. Листинг 3.9. Текст программы к задаче «Parliament» {$R+.M 65000.0.8192} program nee4c01d: const MaxN - 3000: МахО - 25000: var а : array [l..MaxN] ofword: d : array [l..MaxD] ofword: i.j.K.N.s.g : integer: procedure AddtoTree(x:word): begin J:-1: while d[j]<>O do if x>d[j] then j:-2*j+l else j:-2*j; d[j]:-x: end: procedure WriteTree(j:word): begin if d[2*j+l]<>0 then WriteTreeB*j+l): ifd[2*j ]<>0thenWriteTreeB*j ): if (d[2*j]-0) and (d[2*j+l]-0) then begin writeln(d[j]): d[j]:-0 end end: begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'); rewrite(output): read(N): for i:-l to N do read(a[i]): for i:-l to MaxD do d[i]:-0: for i:-N downto 1 do AddToTree(a[i]): WriteTree(l): close(input): close(output): end.
3.9. Решения задач 169 Листинг 3.10. Текст программы к задаче «Parliament» {$R+.M 65000.0.8192} program nee4c01d: const MaxN - 3000: var а : array [l..MaxN] of word: Right. Left : array [l..MaxN] ofword: i.j.K.N.s.g. Tek. Root : integer: procedure AddtoTree(i:word): begin Tek:-Root: while ((a[i]>a[Tek]) and (Right[Tek]<>O)) ог ((a[i]<a[Tek]) and (Left [Tek]<>0)) do if a[i]>a[Tek] then Tek:-Right[Tek] else Tek:-Left[Tek]: if (a[i]>a[Tek]) and (Right[Tek]-O) then Right[Tek]:-i else Left [Tek]:-i: end: procedure WriteTree(j.pred.Dir:word); begin if Right[j]<>O then WriteTree(Right[j].j.2): if Left [j]<>0 then WriteTree(Left [j].j.l): writeln(a[j]): if Dir-2 then Right[pred]:-0 else Left[pred]:-0: end: begin assign(input.'input.txt'); reset(input): assign(output.'output.txt'): rewrite(output): read(N): for i:-l to N do read(a[i]): for i:-l to N do begin Left[i]:-0: Right[i]:-O: end: Root:-N: for i:-N-l downto 1 do AddToTree(i): WriteTree(Root.Root.2): close(input): close(output): end- продолжение^
170 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.11. Текст программы к задаче «Falling Leaves» {$R+.M 65000.0.8192} program mc00d; const MaxN - 3000; MaxD - 25000: var a. s : string: d : array [l..MaxD] of char: i.j.N.Tek. Root : integer: Right. Left : array [l..MaxN] ofword; procedure AddtoTree(i:word); begin Tek:-Root; while ((a[i]>a[Tek]) and (Right[Tek]<>O)) or ((a[i]<a[Tek]) and (Left [Tek]<>0)) do if a[i]>a[Tek] then Tek:-Right[Tek] else Tek:-Left[Tek]: if (a[i]>a[Tek]) and (Right[Tek]-O) then Right[Tek]:-i else Left [Tek]:-i: end: procedure WriteTree(j:integer): begin write(a[j]): if Left [j]<>0 then WriteTree(Left [j]): if Right[j]<>O then WriteTree(Right[j]): end; begin assign(mput.'leaves.in'): reset(input): assign(output.'leaves.out'); rewrite(output): readln(s): while not EOF(input) do begin a:-''; while (s<>'*') and (s<>'$')do begin a:-a+s: readln(s) end: N:-length(a):
3.9. Решения задач 171 for i:-l to N do begin Left[i]:-0; Right[i]:-0: end: Root:-N; for i:-N-l downto 1 do AddToTree(i): WriteTree(Root): writeln; readln(s): end: close(input); close(output): end. Листинг 3.12. Текст программы к задаче «Кодирование» {*R+} program ggO2t3: const МахК - 1000: var k.a.b : array [l..MaxK] of longint: bits : array [0..MaxK] of string[20]: sk : array [l..MaxK] of char: Free : array [l..MaxK] of boolean: res : array [0..255] of string[20]: kj.i.j.m.kkl.kk2 : longint: procedure InputData: var с : char: s : array [0..255] of longint: begin assign(input.'input.txt'): reset(input): for i:-0 to 255 do s[i]:-0: while not EOF(input) do begin read(c): if (c<>#10) and (c<>#13) then inc(s[ord(c)]): end: j:-0: for i:-0 to 255 do if s[i]<>0 then begin inc(j): k[j]:-s[1]: sk[j]:-chr(i) end: kj:-j: . продолжение &
172 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.12 (продолжение) close(input): end: function MinK:longint: var min : longint: begin i:-l: while (not Free[i]) do inc(i): min:-k[i]; m:-i; for i:^n+l to kk2 do if Free[i] and (k[i]<min) then begin min:-k[i]; m:-i; end; Mink:^nin; Free[m]:-false: end: procedure SumUp: var sl. s2. ml. m2 : longint: begin if kj-l then begin assign(output.'output txt'): rewrite(output): writeln(sk[l].' '.0): writeln@): close(output): halt@) end: for i:-l to kj do Free[i]:-true: kkl:-kj; kk2:-kj; while kkl>2 do begin sl:-MinK: ml:^n; s2:H^linK: m2.-m; inc(kk2): k[kk2]:-sl+s2: a[kk2]:^nl: b[kk2]:^n2: Free[kk2]:-true: dec(kkl): end: end: procedure BuildBits: begin bits[kk2]:-T: Free[kk2]:-false:
3.9. Решения задач 173 b1ts[a[kk2]]:-bits[kk2]+'0': bits[b[kk2]]:-b1ts[kk2]+'l': i:-MinK: bits[m]:-'0': Free[m]:-true; bits[a[m]]:-bits[m]+'O'; bits[b[m]]:-bits[m]+T; for i:- kk2-l downto 1 do if not Free[i] then begin bits[a[i]]:-bits[i]+'O': b1ts[b[i]]:-bits[i]+'l': end: end; procedure OutputData; var str : string: begin assign(output.'output.txt'): rewrite(output): for i:- 1 to kj do writeln(sk[i].' '.bits[i]): for i:-l to kj do res[ord(sk[i])]:-b1ts[1]: assign(input.'input.txt'): reset(input): while not EOF(input) do begin readln(str); for i:-l to length(str) do write(res[ord(str[i])]): end; close(input): close(output): end; begin InputData; SumUp: BuildBits: OutputData: end. Листинг 3.13. Текст программы к задаче «Entropy» program gnyOOf: const MaxK - 1000: var k.a.b : аггау [l..MaxK] of longint: продолжение &
174 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.13 (продолжение) bits : array [O..MaxK] of string[20]; sk : array [l..MaxK] of char; Free : array [l..MaxK] of boolean: res : array [0..255] of string[20]; kj.i.j.m.kkl.kk2.n : longint: str : string; procedure InputData: var с : char; s : array [0..255] of longint: begin for i:-0 to 255 do s[i]:-0; readln(str): if str-'END' then halt: for n:-l to length(str) do begin c:-str[n]; inc(s[ord(c)]): end; j:-0: for i:-0 to 255 do if s[i]<>0 then begin inc(j): k[j]:-s[i]: sk[j]:-chr(i) end: kj:-j: end: function MinK:longint; var min : longint: begin i:-l: while (not Free[i]) do inc(i): min:-k[i]; m:-i: for i:-m+l to kk2 do if Free[i] and (k[i]<min) then begin min:-k[i]: m:-i; end; Mink:^nin: Free[m]:-false: end: procedure SumUp: var sl. s2. ml. m2 : longint:
3.9. Решения задач 175 begin 1f kj-l then begin writeln(8*length(str).' '.length(str).' ' .8.0:0:1): exit: end: for 1:-1 to kj do begin Free[i]:-true; a[i]:-0: b[1]:-0: end: kkl:-kj; kk2:-kj: while kkl>2 do begin sl:-flinK; ml:-m; s2:-MinK: m2:-m; inc(kk2): k[kk2]:-sl+s2: a[kk2]:^nl: b[kk2]:^n2: Free[kk2]:-true: dec(kkl): end: end: procedure BuildBits: begin b1ts[kk2]:-'l': Free[kk2]:-false: bits[a[kk2]]:-bits[kk2]+'0'; b1ts[b[kk2]]:-bits[kk2]+'l': i:-MinK: bits[m]:-'0': Free[m]:-true: bits[a[m]]:-bits[m]+'0'; bits[b[m]]:-bits[m]+T: for i:- kk2-l downto 1 do if not Free[i] then begin bits[a[i]]:-bits[i]+'O': b1ts[b[1]]:-b1ts[1]+'l': end: end: procedure OutputData: var b8. bh : longint: begin for i:-l to kj do res[ord<sk[i])]:-bits[i]: продолжение *
176 Глава 3 • Решение задач на деревьях и с помощью деревьев Листинг 3.13 (продолжение) b8:-8*length(str); bh:-O: for i:-l to length(str) do begin inc(bh.length(res[ord(str[i])])); end: writeln(b8.' '.bh.' '.b8/bh:0:l) end; begin assign(input.'input.txf): reset(input): assign(output.'output.txt'); rewrite(output): while not EOF(input) do begin InputData: SumUp: if kj<>l then BuildBits: if kj<>l then OutputData: end; close(input): close(output): end. Листинг 3.14. Текст программы к задаче «Nextree» {$m 65000.0.0} program usO2dl: var a.b.c : array[1..1000] of longint: L.i.j.k.p.q : integer: Done : boolean: procedure CheckTree(p.г:integer); begin if Done then exit: if Ca[p]>a[p+l]+2) and (r-P>-2) {Если есть правое дерево} then begin q:-p+a[p+l]: {Сделал правое дерево} inc(k): b[k]:^: c[k]:-a[q]: a[q]:-a[p]-a[p+l]; CheckTree(q.D: for i:-l to k do a[b[i]]:-c[i]: if (not Done) and (a[p+l]>2) then CheckTree(p+l.p+a[p+l]); end
3.9. Решения задач 177 else begin if (not Done) and (a[p+l]>2) then CheckTree(p+l.p+a[p+l]); if Done then exit: i:-r; while (i>p) and ((a[1-l]-a[1]<2)) do dec(i); if i>p then begin inc(a[i]): for j:-1+l to L do a[j]:-l: Done:-true: end end; end: begin assign(input.'nextree.in'); reset(input); assign(output.'nextree.out'):rewri te(output): read(L): for i:-l to L do read(a[i]): Gone:-false: CheckTree(l.L): if not Done then begin writeln@): exit end: for i:-l to L-1 do write(a[i].1 '); writeln(a[L]): close(input): close(output): end. Листинг 3.15. Текст программы к задаче «Trees Made to Order» program ecnaOld: const MaxB - 19: var B.A : array [-1 .MaxB] of longint; S : string: N.i.j.sb.k.L.R.M.NT.z : longint: procedure BuildB: begin b[O]:-l: b[l]:-l; for i:-2 to MaxB do begin sb:-0: продолжение &
178 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.15 (продолжение) for j--0 to i-1 do 1nc(sb.b[j]*b[i-l-j]): b[i]:-sb: end: end: procedure SumUpB; begin a[0]:-0; a[-l]:-0: for i.- 1 to MaxB do a[i]:-a[i-l]+b[i]: end; function Place(M:longint).longint; begin i-0: while M>a[i] do inc(i); Place:=i; end: procedure Find(N:longint. var NL.NR:longint); begin k:*P1ace(N): {К-во вершин в дереве} NT:-a[k-l]: i.-0: While (NT+b[i]*b[k-l-i]<N) do begin inc(NT.b[i]*b[k-l-i]): inc(i): end; L -i: R:=K-l-L: {К-во вершин в правом поддереве) NL:-1: NR--1: if (L-0) ог (L-1) then NL:-L: if (R-0) ог (R-1) then NR:-R; if NL<0 then NL:-N-NT; . if NR<0 then NR:-N-NT: if (L<>0) and (L<>1) and (R<>0) and (R<>1) then begin NL:-0: while (NT+b[R]<N) do begin inc(NT.b[R]): inc(NL) end; NR:-N-NT: end: 1nc(NL.a[L-l]): inc(NR.a[R-l]): end:
3.9. Решения задач 179 procedure BuildS(N:longint: var S:string): var NL.NR : longint: SL.SR : string: begin if N-0 then begin S:-": exit; end: if N-1 then begin S:-'X': exit: end; Find(N.NL.NR): BuildS(NL.SL): BuildS(NR.SR); if SLo" then S:-'C+SL+T + 'X1 else S:-'X': if SR<>" then S:-S+'('+SR+')': end: begin assign(input.'input.txf): reset(input): assign(output.'output.txt'); rewrite(output); BuildB: SumUpB: while true do begin readln(N): if N-0 then break; BuildS(N.S); writeln(S): end: close(input): close(output): end. Листинг 3.16. Текст программы к задаче «Мобильные телефоны» {$M 65500.0.0} program io01dltl: const MaxS - 100: МахК - 1048576: var SS : array [0..MaxK] of longint: Cm.X.Y.A.B.T.L.R.i.j.S : integer: Ans : longint: procedure Init; продолжение &
180 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг 3.16 (продолжение) begin readln(S): for y:-0 to Maxk do SS[y]:-O: end: procedure Sum(L.B.R.T.X.Y.A.i:integeD: var XN. YN : integer; begin if (x<L) ог (x>R) ог (y<B) ог (y>T) then exit: inc(ss[i].A): if (x-L) and (x-R) and (y-B) and (y-T) then exit: i:-4*i: XN:-(R+L) div 2; YN:-(B+T) div 2; if (x<-XN) and (y<-YN) then Sum(L.B.XN.YN.X.Y.A.i+l): if (x<-XN) and (y> YN) then Sum(L.YN+l.XN.T.X.Y.A.i+2): if (x> XN) and (y> YN) then Sum(XN+l.YN+l.R.T,X.Y.A.i+3): if (x> XN) and (y<-YN) then Sum(XN+l.B.R.YN.X.Y.A.i+4); end: procedure Add: begin readln(X.Y.A): Sum(l.l.S.S.X+l.Y+l.A.O): end: function min(a.b:integer):integer: begin if a<b then min:-a else min:-b; end: function max(a.b:integer):integer: begin if a>b then max:-a else max:-b: end: procedure Res(L.B.R.T.LC.BC.RC.TC.i:integer): var XN.YN.XM.YM : integer; begin if (R<L) or (L>R) ог (T<B) ог (B>T) then exit;
3.9. Решения задач 181 if (L-LC) and (R-RC) and (B-BC) and (T-TC) then begin inc(Ans.SS[i]): exit; end; i:-4*i; XN:-(RC+LC) div 2; YN:-(BC+TC) div 2; Res(L.B,min(R.XN).min(T.YN).LC.BC.XN.YN.i+l): {(x<-(R+L)div2)and(y<-(B+T)div2)} Res(L.max(B.YN+l).min(R.XN).T.LC.YN+l.XN.TC.i+2): {(x<-(R+L)div2)and(y> (B+T)div2)} Res(max(L.XN+l).max(B.YN+l).R.T.XN+l.YN+l.RC.TC.i+3): {(x> (R+L)div2)and(y> (B+T)div2)} Res(max(L.XN+l).B.R.min(T.YN).XN+l.BC.RC.YN.i+4): {(x> (R+L)div2)and(y<-(B+T)div2)} end: procedure Result: begin readln(L.B.R.T): Ans:-0; Res(L+l.B+l.R+l.T+l.l.l.S.S.O): writeln(Ans); end; begin assign(input.'input.txt'): reset(input): assign(output.'output.txf); rewrite(output): On:-1: while (cm<>3) do begin read(Cm): case Cm of 0 : Init: 1 : Add; 2 : Result; 3 : : end; end; end. Листинг 3.17. Текст программы к задаче «Мобильные телефоны» program ioOldltl; const MaxS - 1024; var продолжение &
182 Глава 3 • Решение задач на деревьях и с помощью деревьев Листинг 3.17 (продолжение) SS : array [l..MaxS.l..MaxS] of longint: On.X.Y.A.B.T.L.R.i.j.k.S : integer: Ans : longint: s2 : integer: ad : array [l..MaxS] of integer: function NearestP2(s:integer):integer: var Temp : integer: begin case S of 1 : Temp:-1: 2..4 : Temp:-4; Temp:-8: Temp:-16: Temp:-32: Temp:-64: Temp:-128: Temp--256: Temp:-512: Temp:-1024: 5..8 9..16 17..32 33. 64 65..128 129..256 257..512 513..1024 end: NearestP2:-Temp: end: procedure Init: begin readln(S): for i:«l to MaxS do for j:-l to MaxS do SS[i.j]-O: S2:-NearestP2(S): for i:-l to S2 do begin k:-l: j:-i: while (j and l)-0 do begin j:-j shr 1: k:-k shl 1: end: ad[i]:-k: end: end: procedure Add: begin readln(X.Y.A):
3.9. Решения задач 183 1:-X+1: while i<- S2 do begin j:-Y+l: while j<« S2 do begin 1nc(SS[i.j].A): inc(j.ad[j]): end: inc(i.ad[i]); end: end: function Sum(R.T:integer):longint: var Ans: longint: begin i:-R: Ans:-0: while i>0 do begin j:-T: while j>0 do begin inc(Ans.SS[i.j]): dec(j.ad[j]): end: dec(i.ad[i]): end: Sum:-Ans: end: procedure Result: begin readln(L.B.R.T); inc(L): inc(B): inc(R): inc(T): Inc(N): Ans:-Sum(R.T)-Sum(L-l.T)-Sum(R.B-l)+Sum(L-l.B-l): writeln(Ans): end: begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'): rewrite(output): продолжение
184 Глава 3 * Решение задач на деревьях и с помощью деревьев Листинг On:-1: 3.17 (продолжение) while (cm<>3) do begin read(Cm); case 0 1 2 3 end: end: end. Cm of : Init: : Add: : Result:
Глава 4. Скрытые графы Материал этой главы посвящен способам расширения области применения приве- приведенных методов решения задач на графах. Существует множество классов задач, в ко- которых для непосвященного совсем не очевидно, что эта задача может быть сведена к задаче на графе. Прежде всего, попытаемся дать некоторые общие рекомендации. Начнем с известного в математике понятия «отношение инцидентности». В геоме- геометрическом смысле две области (фигуры) на плоскости называются инцидентными (смежными) если они имеют общую границу. Для перехода к графам вершинам графа сопоставляют сами области, а ребрам графа — состояние инцидентности. То есть ребра проводятся между вершинами, соответствующими смежным облас- областям. Такими областями могут быть треугольники (задача «Тетраэдр»), многоуголь- многоугольники (задача «Стены»), наборы смежных квадратных клеток (задача «Блокада»). Более того, смежность может быть нестандартной, например клетки шахматной доски могут быть смежными в том смысле, что из одной клетки можно попасть в другую ровно за один ход шахматного коня (задача «Мудрый правитель»), или смежными будут считаться валы, связанные ременной передачей (задача «Ремен- «Ременная передача»). Все эти задачи разобраны в первом разделе. Во второй раздел включены задачи, в которых между негеометрическими объек- объектами существуют какие-то отношения. Например, коээфициенты соотношения валют (задачи «Currency Exchange» и «Exchange Rates»), отношение порядкамеж- ду величинами (задача «Sorting It All Out»), отношение ссылки из одного файла на другой (задача.«Проверка веб-страниц»), отношения вершины-буквы, ребра- слова, начинающиеся и заканчивающиеся на соответствующие буквы (задача «Play On Words»). В третий раздел включены задачи, в которых исходными данными являются мно- множества отрезков. Тогда в качестве вершин принимаются точки (как правило, кон- концы отрезков), а в качестве ребер — отрезки между точками, принятыми в качестве вершин графа (задачи «Падение», «The Doors», «Борозды», «N-Credible Mazes»). Последний раздел включает задачи для самостоятельного решения. Все рассмат- рассматриваемые задачи взяты с официальных соревнований по информатике и програм- программированию.
186 Глава 4 * Скрытые графы 4.1. Инцидентность областей В данном разделе собраны задачи «Тетраэдр», «Стены», «Блокада», «Мудрый правитель» и «Ременная передача», в которых граф возникает вследствие приме- применения абстракции «инцидентность (соседство) областей». 4.1.1. Задача «Тетраэдр» Республиканская олимпиада по информатике, Минск, 1999 Тетраэдр (input.txt / output.txt) Дано треугольное поле в виде равностороннего треугольника. Оно разби- разбито на маленькие одинаковые равносторонние треугольники со сторонами, в М раз меньшими, чем сторона большого треугольника. Рис. 4.1. Общий видтреугольного поля для задачи «Тетраэдр» Маленькие треугольники пронумерованы подряд с верхнего ряда вниз по рядам, начиная с 0. Числами показаны номера треугольников. 7-му тре- треугольнику приписана пометка Pi. Имеется также тетраэдр (правильная треугольная пирамида) с ребром, равным длине стороны маленького треугольника. Тетраэдр установлен на 5-м треугольнике. Все грани тетраэдра пронумерованы следующим об- образом: 1) основание тетраэдра; 2) правая грань тетраэдра, если смотреть сверху тетраэдра в направлении стороны АВ перпендикулярно ей; 3) левая грань тетраэдра, если смотреть сверху тетраэдра в направлении сто роны АВ перпендикулярно ей; 4) оставшаяся грань. Например, при 5 e 2 жирной линией выделено нижнее ребро третьей гра- грани, а при 5 е 3 жирной линией выделено нижнее ребро второй грани./-я грань тетраэдра имеет пометку Rj.
4.1. Инцидентность областей 187 Имеется возможность перекатывать тетраэдр через ребро, но при каждом перекатывании взимается штраф, равный квадрату разности между по- пометками совмещаемой грани тетраэдра и треугольника. Требуется пере- перекатить тетраэдр с треугольника 5 на D с наименьшим суммарным штра- штрафом E * D). Входные данные находятся в текстовом файле INPUT.TXT. Первая строка содержит целые числа 5, D и М (М < 90). Каждая из следующих M2 строк содержит пометку соответствующего треугольника. В последней строке за- записаны пометки граней тетраэдра. Пометки (как граней, так и треугольни- треугольников) — целые неотрицательные числа, не превосходящие 300. Числа в од- одной строке разделены пробелами. В выходной файл OUTPUT.TXT должно быть записано одно число — мини- минимально возможный штраф. Пример ввода Пример вывода 043 9446 4 3 8 100 7 3 2 49 9 7 50 100 8 Описание решения: Перейдем к графу следующим образом: вершина — маленький треугольник. Реб- Ребро — наличие возможности перекатить тетраэдр через ребро из одного треуголь- треугольника в другой. Тогда, например, поле, изображенное на рис. 4.2, превратится в граф на рис. 4.3. Рис. 4.2. Пример треугольного поля для задачи «Тетраэдр»
188 Глава 4 * Скрытые графы 1 — 2 — 3 i i 4 — 5 — 6 — 7—8 Рис. 4.3. Начальное положение развертки тетраэдра На этом графе требуется найти путь минимальной стоимости из одной верши- вершины в другую. Поскольку веса ребер в этом графе зависят от того, какой именно гранью тетраэдр придет на соответствующий треугольник, то воспользуемся поиском в ширину. Но прежде проясним процесс перекатывания тетраэдра. В со- соответствии с условиями задачи начальное положение развертки тетраэдра пока- показано на рис. 4.4. w Рис. 4.4. Перекатывание тетраэдра Обозначим его \и (в основании грань номер 1, повернутая вверх). Очевидно, что всего существует 8 возможных различных состояний тетраэдра: 1ы, 2u, Зи, 4и, \dy 2d, 3d, 4d. На рис. 4.5 и 4.6 приведены соответствующие развертки: 2u Зи Ли w w w Рис. 4.5. Развертки перекатывания тетраэдра вверх 2d 3d 44 Рис. 4.6. Развертки перекатывания тетраэдра вниз Составим теперь таблицу, отображающую, в какое из состояний переходит тетра- тетраэдр при перекатывании его вниз, вверх, вправо, влево из текущего состояния.
4.1. Инцидентность областей 189 Таблица 4.1. Состояния тетраэдра Вниз Вверх Вправо Влево Id 2G 2d 3u 3d Аи Ad Ad X 3d X 2d X ^d X X Аи X Зи X 2и X 1G 3d 2G Ad 1G 1d Аи 2d Зи 2d Зи Id Аи Ad 1G 3d 2G Для удобства использования этой информации введем следующее кодирование: X 0 \и 1 и 2 2и 3 2d 4 Зи 5 3d 6 Аи 7 Ad 8 Вниз 1 Вверх 2 Вправо 3 Влево 4 Получаем двумерный массив Г(8 строк, 4 столбца), который описывает все воз- возможные перекатывания тетраэдра. |т 1 2 3 4 5 6 г 8 1 8 0 6 0 4 0 2 0 2 0 7 0 5 0 3 0 1 3 6 3 8 1 2 7 4 5 4 4 5 2 7 8 1 6 3 Основная идея решения заключается в следующем: Заносим в очередь стартовую позицию S. Пока очередь не пуста. берем из очереди очередную позицию ставим в очередь позиции, в которые тетраэдр может попасть за одно перекатывание. При установке в очередь очередной элемент включает номер вершины на графе, тип прихода {iu.Ad)y текущий штраф после перехода в эту вершину. Элемент не нужно ставить в очередь, если текущий штраф больше ранее запомненного для этой вершины графа.
190 Глава 4 * Скрытые графы Перейдем к рассмотрению реализации. Тело главной программы выглядит сле- следующим образом: begin InputData: InitGraph; QEnd:-O: QBegin:-l: Put(S.TS.O); while (QBegin<-QEnd) do begin 6et(V.TV.CV); PutAll(V.TV.CV): end; OutResult: end. Здесь: ? InputData — процедура, обеспечивающая ввод исходных данных; ? InitGraph — процедура, обеспечивающая создание графа по исходным данным; ? Put(V,TV,CV) — постановка в очередь одной вершины графа (позиции на тре- треугольном поле); ? V— номер вершины (позиции); ? TV — тип вершины; ? CV— текушее значение штрафа (при прохождении от начальной вершины 5 до текущей); ? Get{ V,TV,CV) — взятие из очереди очередной вершины графа (позиции на тре- треугольном поле); ? PutAll — постановка в очередь всех вершин, смежных с текущей (СУприбавля- ется к штрафу за текущий переход); ? OutResult — вывод результата — минимального штрафа при путешествии от позиции5до позиции D. Рассмотрим подробнее реализацию процедуры InitGraph: procedure InitGraph: begin Pw[O]:-l: G[0.1]:-2; for i:-l to М-1 do {Горизонтальные ребра} for j:-i*i to A+l)*A+l)-2 do begin inc(Pw[j]): G[j.Pw[j]]:-j+l: inc(Pw[j+l]): G[j+l.Pw[j+l]]:-j: end: a:-4: TS:-1 {1 Up}: for i:-l to М-2 do {Вертикальные ребра}
4.1. Инцидентностьобластей 191 begin j:-i*i: while j<-(i+l)*(i+l)-l do begin 1nc(Pw[j]): G[j.Pw[j]]:-j+a: inc(Pw[j+a]); G[j+a.Pw[j+a]]:-j: if S-j then TS;-2 {1 Dn}; inc(j.2): end: inc(a.2): end; for i:-0 to m*m-l do cp[i]:-maxint: end; В данной задаче граф представляется двумя массивами: ? Pw[i] — количество дуг из вершины i; Q G[ij] — вершина, в которую идет дуга номеру из вершины i. Из условий задачи очевидно, что из каждой вершины идет не более трех ребер к другим верши- вершинам. Кроме того, для каждой вершины i хранится cp[i] — текущее значение штрафа до этой вершины от исходной вершины 5. В процессе инициализации устанавливается начальное положениететраэдра(Г5) — одно из двух возможных: \и или \d. И, наконец, рассмотрим подробнее процедуру PutAll: procedure PutAll(V.TV.CV;longint): var NV. NTV. Dir. Base. NCV : longint: begin for i:-l to Pw[V] do begin NV:-G[V.i]; if NV-V+1 then Dir:-3 {Right} else if NV-V-1 then Dir:-4 {Left} else if NV>V then Dir;-1 {Down} else Dir:-2: {Up} NTV:-T[TV.Dir]; Base;- (NTV+1) div 2: NCV:-CV+SQR(p[NV]-R[Base]):
192 Глава 4 • Скрытые графы if NCVV then Dir:-1 {Down} else Dir:=2: {Up} NTV:-T[TV.Dir]; Base:- (NTV+1) div 2: NCV:=CV+SQR(p[NV]-R[Base]): if NCV<cp[NV] then Put(NV.NTV.NCV): end: end: Для всех вершин (NV), смежных с текущей, выясняется направление движения (Dir), тип прибытия (NTV), грань прибытия (Base), штраф попадания в вершину NVno текущему маршруту с учетом накопленного штрафа (CV) и штрафа за по- последний перекат (SQRQ)[NV]-R[Base])). Очередная вершина ставится в очередь, только если новый штраф для этой вершины меньше прежнего (NCV< cp[NVY). Полный текст решения задачи приведен в конце главы. 4.1.2. Задача «Стены» Международная олимпиада по информатике, 2000 Стены (walls.in/walls.out) В некоторой стране стены построены таким образом, что каждая стена соеди- соединяет ровно два города, и стены не пересекают друг друга. Таким образом, страна разделена на отдельные части. Чтобы добраться из одной области в другую, необходимо либо пройти через город, либо пересечь стену. Два лю- любых города А и В могут соединяться не более чем одной стеной (один конец стены в городе Л, другой — в городе В). Более того, всегда существует путь из города А в город В, проходящий вдоль каких-либо стен и через города. Существует клуб, члены которого живут в городах. В каждом городе может жить либо один член клуба, либо вообще ни одного. Иногда у членов клуба возникает желание встретиться внутри одной из областей, но не в городе. Чтобы попасть в нужную область, каждый член клуба должен пересечь ка- какое-то множество стен, возможно, равное нулю. Члены клуба путешествуют на велосипедах. Они не хотят въезжать ни в один город из-за интенсивного движения на городских улицах. Они также хотят пересечь минимально воз- возможное количество стен, так как пересекать стену с велосипедом довольно трудно. Исходя из этого, нужно найти такую оптимальную область, чтобы суммарное количество стен, которые требуется пересечь членам клуба, на- называемое суммой пересечений, было минимальным из всех возможных. Города пронумерованы целыми числами от 1 до N, где N — количество го- городов. На рис. 4.7 пронумерованные вершины обозначают города, а линии, соединяющие вершины, обозначают стены. Предположим, членами клуба являются три человека, которые живут в городах с номерами 3,6 и 9. Тогда оптимальная область и соответствующие маршруты движения членов клу- клуба показаны на рис. 4.8. Сумма пересечений равна 2, так как членам клуба
4.1. Инцидентность областей 193 требуется пересечь две стены: человеку из города 9 требуется пересечь сте- стену между городами 2 и 4, человеку из города 6 требуется пересечь стену между городами 4 и 7, а человек из города 3 не пересекает стен вообще. Рис. 4.7. Карта городов и стен для задачи «Стены» Рис. 4.8. Оптимальная область и маршруты движения Требуется написать программу, которая по заданной информации о горо- городах, областях и местах проживания членов клуба находит оптимальную область и минимальную сумму пересечений. Ввод: Первая строка содержит одно целое число: количество областей Af, удовлетворя- удовлетворяющее неравенствам 2 ? М < 200. Вторая строка содержит одно целое число: коли- количество городов N, причем 3 < N< 250. Третья строка содержит одно целое число: количество членов клуба I, причем 1 < L < 30 и L < N. Четвертая строка содержит L различных целых чисел в возрастающем порядке: номера городов, где живут члены клуба. Оставшаяся часть файла содержит 2Мстрок, по паре строк для каждой из Л/обла- стей. Первые две строки описывают первую область, вторые две строки — вторую область, и т. д. В каждой паре первая строка содержит количество городов на гра- границе области; вторая содержит номера этих / городов в порядке обхода границы области по часовой стрелке. Единственным исключением является последняя область — это находящаяся снаружи (внешняя) область, окружающая все города и другие области, и для нее порядок следования городов на границе задается про- против часовой стрелки. Порядок описания областей во входном файле задает целые номера этим областям. Область, описанная первой во входном файле, имеет номер 1, описанная второй —
194 Глава 4 * Скрытые графы номер 2, и т. д. Обратите внимание, что входной файл содержит описание всех областей, образованных городами и стенами, включая находящуюся снаружи (вне- (внешнюю) область. Вывод: Первая строка этого файла содержит одно целое число — минимальную сумму пересечений. Вторая строка содержит одно целое число — номер оптимальной области. Если существует несколько различных решений, вам необходимо найти лишь одно из них. Пример ввода и вывода: Walts.in (продолжение ввода) Walls.out 10 10 3 369 3 1 23 3 1 37 4 2473 3 467 3 486 3 687 3 458 4 78 109 3 5108 7 79 10 54 2 1 Описание решения: Основная идея заключается в том, чтобы преобразовать данную задачу к задаче на графе следующим образом: пусть вершины — это области, образованные стенами. Наличие ребра означает смежность областей. Для перехода в смежную область требуется перелезть ровно одну стену. Например, для данных из примера ввода получим граф из 10 вершин A0 областей), связанных ребрами (смежные облас- области) следующим образом: Область Смежные области 1 2 3 4 5 6 7 8 9 10 23 10 1 3 10 1 24 10 3 56 10 467 458 59 10 6 9 10 78 10 1 234789
4.1. Инцидентность областей 195 Нужно найти такую вершину на графе, чтобы сумма расстояний к ней из городов, где живут члены клуба, была минимальной. Каждый город может быть гранич- граничным для нескольких областей. Поэтому для каждого города нужно выбирать наи- наиболее выгодную область, среди его граничных областей. Рассмотрим реализацию решения. Тело главной программы выглядит следующим образом: begin InputData: Init6raph: Floyd; FindBest: OutResult: end. Здесь: ? InputData — процедура ввода исходных данных; a InitGraph — процедура инициализации графа; ? Floyd — процедура поиска алгоритмом Флойда кратчайшего расстояния меж- между всеми парами вершин; ? FindBest — процедура поиска оптимального места встречи; ? OutResuU — процедура вывода результатов. Рассмотрим подробнее реализацию этих процедур. Начнем с процедуры InputData: procedure InputData: begin assign(input.'walls.in'); reset(mput): read(M.N.L): for i:-l to L do read(c[i]); for i:-l to М do begin read(k[i]): for j:-l to k[i] do read(t[i.j]): end; close(input): end; Здесь: Q c[i] — города, в которых живут члены клуба; ? k[i] — количество городов, ограничивающих область i; ? t[i] — список городов, ограничивающих область i (t[i] состоит из t[iJ] дляу от 1 zok[i]).
196 Глава 4 * Скрытые графы Теперь рассмотрим процедуру IninGraph: procedure InitGraph: var wl.w2 : array [l..MaxM] of byte: s.p : byte: begin for i:-l to М do for j:-l to М do begin G[i.j]:-GREAT: if i-j then continue: for x:-l to М do begin wl[x]:=0: w2[x]-0: end: for x:-l to k[i] do wl[t[i.x]]:-l: for x:-l to k[j] do w2[t[j.x]]:-l: s:-0; for x:-l to М do inc(s.wl[x]*w2[x]): if s>-2 then begin 6[1.j]:-l: G[j.i]:-1: end: end: end: Здесь г. i ГСЛ?ЛГ(максимально возможная константа), если области не смежные; [1, если области смежные. Для установления факта смежности областей используется следующий алгоритм. Если списки городов, ограничивающих области (t[i] и ф']), включают 2 одинако- одинаковых города, значит, эти области являются смежными, поскольку они имеют об- общую стену, соединяющую эти два города. Для реализации установки факта наличия двух общих чисел используются два массива w\ и w2. w\ содержит 1 на позициях городов, ограничивающих первую область, и 0 на всех остальных позициях. Аналогично w2 содержит 1 на позициях городов, ограничивающих вторую область, и 0 на всех остальных позициях. Если единицы встречаются в обоих городах на одних и тех же позициях более двух раз, то области смежные. Процедура Floyd находит кратчайшие расстояния G[i, j] на построенном графе между всеми парами вершин (i,j). procedure Floyd: begin for x:=l to М do for i:-l to М do for j:-l to М do
4.1. Инцидентность областей 197 begin if i-j then G[i.j]:-0: if (G[i.x]<>GREAT) and (G[x.j]<>GREAT) then G[i.j]:- min(G[i.j].G[i.x]+G[x.j]): end; end; То есть в результате выполнения алгоритма G[i, j] будет содержать количество стен, которые нужно перелезть, чтобы попасть из области i в область/ Заметим, что в соответствии с семантикой задачи G[i, i] полагается равным нулю для всех i. Теперь обратим свое внимание к процедуре FindBest: procedure FindBest; var ВМ : longint; byte; CurS. tc begin {Строим массив город - список областей} {i - номер области} {x - номер города} for 1:-1 to М do kR[i]:-O: for i:-l to М do for j:-l to k[i] do begin х :- t[i.j]; inc(kR[x]); R[x.kR[x]]:-i; end; MinS:-maxlongint; NumM:-0; for i:-l to М do begin CurS;-0; for j;-l to L do begin tc:-c[j]; BM;-GREAT; for x:-l to kR[tc] do BM:=min(BM.G[r[tc.x].i]); inc(CurS.BM); end; if CurS<MinS then begin MinS:-CurS; NumM:-i end; end; end; Вначале в ней строится массив R[i] — для города i список смежных областей, то есть тех областей, для попадания в которые из этого города не требуется переле- перелезать стены. {Цикл по областям} {Цикл по членам клуба} {Город с членом клуба} {Лучший для члена}
198 Глава 4 * Скрытые графы Затем циклом по всем областям находим минимальную стоимость проведения сбора членов клуба именно в ней и среди этих значений находим минимальное (MinS) и номер соответствующей области (NumM). Заметим, что для каждого члена клуба в качестве области, из которой он отправится в путь, нужно последователь- последовательно выбирать все области, смежные с городом, запоминая ту, которая имеет крат- кратчайший путь к текущему месту сбора (i). Наконец, процедура OutResult выводит найденные величины MinS и NumM: procedure OutResult: begin assign(output.'wal1s.out'); rewrite(output); writeln(MinS): writeln(NumM); close(output); end: Полный текст решения задачи приведен в конце главы. 4.1.3. Задача «Блокада» Всероссийская командная олимпиада школьников по программированию, 2001 Блокада (input.txt / output.txt / 2 с) Государство Флатлавдия представляет собой прямоугольник размером М x N, состоящий из единичных квадратиков. Флатландия разделена на К провин- провинций B <K< 100). Каждая провинция представляет собой связное множе- множество квадратиков, то есть из каждой точки провинции можно дойти до любой другой ее точки, при этом разрешается переходить с квадратика на квадра- квадратик, только если они имеют общую сторону (общей вершины недостаточ- недостаточно). Во Флатландии нет точки, которая граничила бы более чем с тремя провинциями (то есть четыре квадратика, имеющие общую вершину, не могут принадлежать четырем разным провинциям). Каждая провинция имеет свой символ. Столица Флатландии находится в провинции, имеющей символ А (заглавная латинская буква А). Провин- Провинция называется пограничной, если она содержит граничные квадратики. Провинция, в которой находится столица Флатландии, не является погра- пограничной. Король Ректилании, соседнего с Флатландией королевства, решил завое- завоевать Флатландию. Для этого он хочет захватить столицу Флатландии. Од- Однако он знает, что сил его армии недостаточно, чтобы сделать это сразу. Поэтому сначала он хочет окружить центральную провинцию, ослабить силы противника долгой блокадой, а потом захватить столицу. Чтобы окружить провинцию, требуется захватить все провинции, с кото- которыми она граничит. Две провинции граничат, если существует два квадра- квадрата, имеющие общую сторону, один из которых принадлежит первой из них, а другой — второй. Чтобы захватить провинцию, надо чтобы выполнялось одно из двух условий: либо она пограничная, либо граничит с какой-либо уже захваченной провинцией.
4.1. Инцидентность областей 199 Чтобы сберечь силы своей армии, король Ректилании хочет установить блокаду центральной провинции, захватив как можно меньше провинций. Помогите ему выяснить, сколько провинций потребуется захватить. Захва- Захватывать саму центральную провинцию нельзя, поскольку для этого сил ар- армии Ректилании пока недостаточно. Ввод: Первая строка содержит М и N C < M, N< 200). Следующие М строк содержат N символов каждая и задают карту Флатландии. Символ, находящийся в (i + 1)-й строке входного файла на^'-м месте, представляет собой символ провинции, кото- которой принадлежит квадратик (iJ). Все символы имеют ASCII-код, больший 32. Вывод: Выведите в выходной файл единственное число — количество провинций, кото- которые требуется захватить. Если установить блокаду невозможно, выведите «-1». Пример ввода Пример вывода 56 4 BBBBBZ BCCCBZ BCAbbZ BDDDbZ 33333Z Описание решения: Переход к графам осуществляем следующим образом: вершина — область (обозна- (обозначается символом с возможным кодом от 32 до 255). Ребро отображает смежность двух областей. При вводе исходных данных анализируются две смежные строки: соседние (горизонтально) символы во второй строке и соседние (вертикально) символы первой и второй строк. По этим данным пополняются ребра графа. Области, смежные с областью Д которую нужно блокировать, сразу включаем в спи- список областей, которые нужно захватить. Их количество обозначаем переменной А. Затем алгоритмом Дейкстры находим кратчайшие расстояния от внешней облас- области, обозначаемой пробелом (код пробела — 32), до всех заданных областей. Если какая-то из областей, смежных с областью Л, недостижима (при вычисле- вычислении расстояния в d[i] так и осталось GREAT), то выводим ответ «-1» и прекраща- прекращаем работу программы. Циклом по количеству смежных с А областей: ? все новые вершины, вошедшие в оптимальный путь, добавляем в список всех различных вершин, необходимых для захвата области А; ? обнуляем веса всех ребер, связанных с вершинами, вошедшими в оптималь- оптимальный путь; Q алгоритмом Дейкстры находим новые кратчайшие расстояния до всех вер- вершин.
200 Г*лава 4 * Скрытые графы После завершения этого цикла выводим количество вершин в созданном списке. Рассмотрим подробнее реализацию. Тело главной программы выглядит следую- следующим образом: begin InputDataInitGraph: Dejkstra: for i:-33 to 255 do if (G[i.ordCA')]-l) and (i<>ordCA')) and (d[i]-GREAT) then BadStop: A:-K; for p.-l to A do begin ZeroG: Dejkstra: end; assign(output.'output.txt');rewnte(output): writeln(K): close(output); end. Здесь: О InputDataInitGraph — процедура, которая обеспечивает ввод и формирование графа; ? G[32..255,32..255] — матрица смежности построенного графа; ? Dejkstra — процедура, реализующая поиск кратчайших расстояний алгоритмом Дейкстры; ? BadStop — процедура вывода -1 в случае недостижимости хоть одной из обла- областей, смежных с областью А от внешней области; ? ZeroG — обнуление весов ребер в графе, связанных с вершинами, вошедшими в оптимальный путь; ? К — количество областей, смежных с областью А (после первого выполнения алгоритма Дейкстры), и количество областей, требуемых для захвата (после последнего выполнения алгоритма Дейкстры). Рассмотрим подробнее процедуру ввода исходных данных и формирования графа: procedure InputDataInitGraph; begin assign(input.'input.txt'): reset(input); readln(M.N); sl:-"; for i:-l to N+2 do sl:-sl+' ': for i:-32 to 255 do for j:-32 to 255 do g[i.j]:-GREAT: for i:-l to М do begin readln(s2): s2.-* *+s2+' ': for j:-2 to N+2 do
4.1. Инцидентностьобластей 201 begin g[ord(s2[j]).ord(s2[j-l])]:-l: g[ord(s2[j-l]).ord(s2[j])]:-l: g[ord(sl[j]).ord(s2[j])]-l: g[ord(s2[j]).ord(sl[j])]:-l: end: sl-s2: end; s2:-": for i:-l to N+2 do s2:-s2+' '; for j:-2 to N+1 do begin g[ord(s2[j]).ord(s2[j-l])]:-l: g[ord(s2[j-l]).ord(s2[j])]:-l: g[ord(sl[j]).ord(s2[j])]:-l; g[ord(s2[j]).ord(sl[j])]:-l: end: K:-0: for i:-33 to 255 do if (G[ordCA').1]-l) and A<>ord('A')) then begin G[ord('A').i]:-GREAT: {Через А идти нельзя) ^uCvK). {Спежчые с А сразу включаем} diff[K] -i: end. close(input); end: Слева и справа к каждой входной строке добавляется ио символу пробел (''), обо- обозначающему внешнюю область. Кроме того, в качестве первой и последней строк также используем строки, состоящие изМ+ 2 пробелов. - ., _ Jl, если во вводе есть соседние символы с кодами i и;', [СД?у4Г,впротивномслучае; a diff — массив, содержащий все различные символы областей, соседствующих с областью «А»; О К — количество элементов в этом массиве. Теперь рассмотрим реализацию алгоритма Дейкстры. procedure Dejkstra: const FREE - 0: DONE - 1: var Lab : array [32..255] of byte:
202 Глава 4 • Скрытые графы Bliz. MinD : byte: begin {Инициализация данных} for i:-33 to 255 do begin Lab[i]:-FREE: d[i].-6[32.i]; {Расстояния от 0-й до остальных) pred[1]:-32: end: d[32]:-0: Lab[32]:-DONE: pred[32]:-0: {У нее нет предка} for j:-33 to 255 do begin {Поиск ближайшей из необработанных} MinD:-GREAT: for i:-33 to 255 do if (Lab[i]-FREE) and (d[i]255) and (d[i]>d[bliz]+G[Bliz.i]) then begin d[i]:-d[bliz]+6[B1iz.i]: pred[i].-Bliz; end: end: end: В данном случае реализация несколько отличается от обычной, поскольку граф представлен матрицей смежности, а не списком ребер, исходящих из каждой вер- вершины графа. Кроме того, в связи со спецификой задачи вершины нумеруются от 32до255. Теперь рассмотрим процедуру ZeroG: procedure ZeroG: begin j:-ord('A'): G[pred[j].j]:-O: j:-pred[j]: while pred[j]<>O do begin for 1:-33 to 255 do ifG[i.j]-l then begin G[1.j]:-0: G[j.i]:-O:end: 6[pred[j].j]:-0:
4.1. Инцидентностьобластей 203 G[j.pred[j]]:-O: AddNew(j): j:-pred[j]: end: end; Эта процедура обнуляет все ребра вершин графа, вошедших в текущий оптималь- оптимальный путь от внешней области к области «Л». Заметим, что на первый взгляд по семантике задачи нужно искать кратчайшие пути до вершин, смежных с «Л». Однако поскольку алгоритм Дейкстры ищет кратчайшие пути одновременно до всех вершин, то он справляется с нужной нам задачей. Здесь: ? pred — массив, хранящий номера вершин-предков, для тех вершин, который вошли в оптимальный путь; ? AddNew(j) — процедура, добавляющая в массив <##вершину, если ее там еще не было. procedure AddNew(j:byte); var i : byte; begin i:-l: while A<-K) and (diff[i]<>j) do Inc(i); if i>K then begin inc(K); diff[K]:-j: end: end; Полный текст решения задачи приведен в конце главы. 4.1.4. Задача «Мудрый правитель» Командный чемпионат школьников Санкт-Петербурга по программированию, 2000 Мудрый правитель (INPUT.TXT / OUTPUT.TXT / 10 с) Известно, что о создателе шахмат ходит множество легенд. Недавно в од- одной древней библиотеке была обнаружена еще одна. В ней утверждается, что когда создатель игры рассказал правителю о своем изобретении и по- попросил скромную награду, правитель сказал ему следующее: «Возьми шах- шахматную доску и поставь на нее коня. На ту клетку, на которую ты поста- поставишь коня, я положу 2N золотых монет. На те клетки, на которые ты смо- сможешь дойти конем за 1 ход, я положу 2N~1 золотых монет. И вообще, если с клетки, на которую ты поставишь коня, ты сможешь дойти до некоторой клетки самое меньшее за Р < N ходов, я положу на нее 2N ~p золотых монет. Но если ты проявишь чрезмерную жадность и не сможешь унести все моне- монеты, которые я выложу на доску, то я прикажу отрубить тебе голову!»
204 Глава 4 * Скрытые графы Ученым известно, что создатель шахмат был очень умным человеком. Он знал, что сможет унести не более М монет. Поэтому он поставил коня на такую клетку, чтобы получить как можно больше монет и остаться живым. Если бы такой клетки не было, то он бы тихо покинул город. Помогите уче- ученым узнать, сколько монет заработал создатель шахмат своим изобретени- изобретением и на какую клетку он поставил коня. ПРИМЕЧАНИЕ Напоминаем, что шахматная доска имеет форму квадрата, поделенного на клетки; столбцы называются латинскими буквами от а до h, а строки — цифрами от 1 до 8, клетка имеет название в виде пары буква-цифра, в зависимости от того, на пересечении какого столбца и какой строки она находится. Конь ходит буквой «Г» — на 2 клетки в горизонтальном или вертикальном направлении и затем на одну клетку в перпендикулярном направлении. Разу- Разумеется, конь не может выходить за пределы доски. Ввод: На первой строке находятся числа @ <N<25) и A < М < 109). Вывод: На первой строке выходного файла выведите число 5 — количество монет, кото- которое получил создатель шахмат (если ему не удалось заработать ни одной монеты, то 0). Если 5 > 0, на второй строке выведите в любом порядке, но без повторений, все возможные клетки, в которые он мог поставить коня. Разделяйте имена кле- клеток пробелами. Пример ввода 1 5 221 3 92 24 Пример вывода 5 а2а7Ы Ь8 g1 g8 h2 h7 17 a1 a8 h1 h8 91 ЬЗ Ь6 c2 c7 f2 П дЗ дб 0 Описание решения: Для перехода к задаче на графе вводим следующие аналогии: вершина — клетка доски, ребро — наличие возможности хода шахматного коня из одной клетки в дру- другую. Ребрам присваиваем веса: 0 — для ребра к самой себе; 1 — для ребра к клетке, в которую можно добраться ровно за 1 ход; -1 — в остальных случаях. Затем алгоритмом Флойда ищем кратчайшие расстояния от каждой клетки до каждой. Далее для каждой клетки строим и запоминаем сумму, которую можно получить по условиям задачи, если начать с этой клетки, параллельно выбираем максимальную из этих сумм, не превышающую М. Выводим найденную сумму. Для каждой клетки, имеющей такую же сумму, выводим координаты этой клетки в шахматной нотации. Рассмотрим подробнее реализацию. Тело главной програм- программы выглядит следующим образом:
4.1. Инцидентность областей 205 begin InputData; InitGraph: Floyd; FindTheBest: OutResults: end. Здесь используются процедуры, обеспечивающие отдельные этапы решения: ? InputData — ввод исходных данных; ? InitGraph — формирование графа; ? Floyd — вычисление кратчайшего пути от каждой вершины до каждой алго- алгоритмом Флойда; ? FindTheBest — нахождение оптимальной суммы; ? OutResults — вывод результатов. Процедура ввода данных очевидна: procedure InputData: begin assign(input. 'input.txt'); reset(input); read(N. M); close(input): end; Рассмотрим процедуру формирования графа: procedure InitGraph: begin for i :» 1 to 8 do for j :» 1 to 8 do for k :- 1 to 8 do for 1 :- 1 to 8 do begin a[i.j][k.l]:-l: if abs((i-k)*(j-l)) - 2 then a[i.j][k.l] :- 1: if (i=k) and (j=l) then a[i.j][k.l] :« 0; end: end: Заметим, что матрица смежности графа шахматной доски хранится в виде четы- четырехмерного массива (по два измерения на вершину). Элегантно выглядит при таком подходе и условие существования хода шахматного коня из клетки (i,j ) в клетку (k, 1): abs((i - Ar)*O* - /)) e 2.
206 Глава 4 * Скрытые графы Отметим также тот факт, что синтаксис Паскаля допускает различные способы записи обращения к элементу четырехмерного массива с индексами iJ, k, /, в том числе a[i,j, k, /], a[2][7][ife]l/] и a[i,j][k, /]. В решении выбран последний вариант, как наиболее адекватный реалиям задачи. Теперь рассмотрим процедуру, реали- реализующую алгоритм Флойда: procedure Floyd; begin for р :- 1 to 8 do for q :» 1 to 8 do for i :- 1 to 8 do for j :- 1 to 8 do for k := 1 to 8 do for 1 :- 1 to 8 do if (a[i.j][p.q] <> -1) and (a[p.q][k.l] <> -1) and ((a[i.j][k.l] - -1) ог (a[i.j][p.q] + a[p.q][k.l] < a[i.j][k.l])) then a[i.jHk.l] :- a[i.j][p.q] + a[p.q][k.l]: end: Кратчайшее расстояние от клетки (i,j) до клетки (k, /) заменяется путем через клетку ty, q), если: ? уже существует путь из клетки (i,j) в клетку Q), q); (a[i.j][p.q] о -1) ? уже существует путь из клетки Q), q) в клетку (k, /); (a[p.q][k.l] о -1) ? это расстояние еще не вычислялось (a[i,j][k, /] - -1) или новое расстояние (через клетку fa q)) короче прежнего: (a[i.j][p.q] + a[p.q][k.l] < a[i.j][k.l])) Обратимся теперь к процедуре FindTheBest: procedure FindTheBest: begin best :- 0: for i :- 1 to 8 do for j :* 1 to 8 do begin р :- 0: for k :- 1 to 8 do for 1 :- 1 to 8 do if a[i.j][k.l] <- N then р :- р + A shl (N - a[i.j][k.l])); if (р > best) and (р <- m) then best :- р:
4.1. Инцидентность областей 207 s[i.j]:-p: end; end: В этой процедуре используются величины a[i,j][k, /] — кратчайшие расстояния от каждой клетки (i,j) до каждой клетки (k, /). При фиксированной клетке (i,j) просматриваются все клетки доски и суммируются соответствующие количества монет, учитывающие количества ходов до этой клетки. Если до клетки доходим не более чем за Мходов (a[i,j ][k, 1 ] < N), то к сумме прибавляем 2^(N - a[i,j ][k, 1 ]). Напомним, что SHL — это операция сдвига влево. А сдвиг влево двоичного целого числа на одну позицию как раз и обес- обеспечивает его умножение на 2. Сдвиг влево двоичного целого числа на X пози- позиций обеспечивает его умножение на 2Л. Соответственно, 1 sA/Хобеспечивает константу 2х. Теиерь рассмотрим процедуру OutResults: procedure OutResults; begin assign(output. 'output.txt'): rewrite(output): writeln(best): for i :- 1 to 8 do for j :- 1 to 8 do if S[i.j] - best then write(chr(ordCa') + i - 1). j. ' '); close(output); end: После вывода оптимального количества монет (best) выводятся все клетки, начи- начиная с которых можно получить такую же сумму (S[iJ] = best). Полный текст решения приведен в конце главы. 4.1.5. Задача «Ременная передача» NEERC, Северный четвертьфинал, 1998 Ременная передача (input.txt / output.txt / 10 с) На плоскости расположены п < 100 валов (Xi, Yi — координаты центров, Ri — радиусы). Для каждого i такого, что 1 < i < я, Xi, Yi — целые числа в диапа- диапазоне [-10 000, 10 000], Ri — целые числа в диапазоне [1, 10]), связанные К ременными передачами. Валы, имеющие общий центр вращения (одинако- (одинаковые Xi и Yi — координаты центра), жестко связаны, то есть при всех усло- условиях имеют одинаковую угловую скорость (количество оборотов в едини- единицу времени). При этом задаваемая система геометрически правильна, то - есть валы не пересекаются между собой и валы, имеющие общий центр вращения, не могут быть соединены ремнем. Будем считать вал с номером 1 < т й N вращающимся по часовой стрелке с угловой скоростью 1000 оборотов в минуту. Требуется для каждого вала найти количество полных оборотов, которое он совершит за 1 час.
208 Глава 4 * Скрытые графы Ввод: Первая строка входного файла содержит три числа N, К, т, разделенные пробела- пробелами. Затем следуют N строк, каждая из которых содержит тройку чисел Xi, Yi, Ri. Следующая группа состоит из К строк вида Uj Vj, где Uj и Vj — номера валов, свя- связанных ремнем^ A ^j ^ К). Вывод: Выходной файл должен содержать Мстрок, где i-я строка содержит число полных оборотов i-го вала, которое он совершит за 1 час или строку «IDLE», если вал не вращается. В случае если заданная система противоречива, то есть на один вал передается вращение с разными угловыми скоростями от нескольких источни- источников, выходной файл должен содержать строку «DO NOTTURNITONh. Пример ввода Пример вывода 52 1 001 10 103 10 10 1 20 20 3 30 30 10 1 2 34 60000 20000 20000 6666 IDLE Onucanue решения: Для перехода к задаче о графе сопоставим вершинам валы, а ребрам — ременные передачи или жесткое сцепление между ними. Тогда с помощью очереди от за- заданного исходного вала можно рассчитать количество полных оборотов в час всех валов. Тело главной программы выглядит следующим образом: begin InputDataInitGraph; QEnd:-O: QBeg-l: for i-1 to N do A[i]:-1: Put(m.60000): while (QBeg<=QEnd) and not Stop do begin Get(V.W): PutAll(V.W): end; OutResu1ts; end. Здесь: О InputDataInitGraph — процедура ввода исходных данных и построения графа;
4.1. Инцидентность областей 209 ? OutResuUs — процедура вывода результатов в заданном формате. А остальные строки реализуют очередь начиная от вершины т для вычисления количества оборотов за час всех валов; Q QBeg — переменная начала очереди; Q QEnd — переменная конца очереди; ? Stop — логическая переменная, получает значение «истина», если в результате вычислений получилось противоречние (новое количество оборотов не совпа- совпадает с ранее вычисленным); ? Get{ Vt W) — процедура, берущая из очереди вал номер Vc количеством оборо- ? Put{ V, W) — процедура, помещающая в очередь вал номер Vc количеством обо- оборотов W; ? PutAll( V, W) — процедура, помещающая в очередь все валы, связанные с валом номер Vc количеством оборотов W. Рассмотрим процедуру ввода данных и построения графа. procedure InputDataInitGraph; begin assign(input.'input.txt'): reset(input); readln(N.K.m); for i:-l to N do readln(x[i].y[i].r[i]): for i:-l to N do for j:-l to N do G[i.j]:-O: for i:-l to К do begin read(u.v); G[u.v]:-1: G[v.u]:-1: end; for i:-l to N do for j:-i+l to N do if (x[i]-x[j]) and (y[i]-y[j]) then begin G[i.j]:-1: G[j.i]:-1: end: close(input): end; Здесь: Q x[i], y[i]> t{i] — координаты центра и радиус вала номер i; , если валы i и j связаны, > - в противном случае;
210 Глава 4 * Скрытые графы ? К — количество ременных передач; ? м, v — очередная ременная передача связывает валы и и v. Теперь рассмотрим процедуры работы с очередью. procedure Put(V:longint; Wreal): begin inc(QEnd); Q[QEnd]:-V: a[V].-W: end; procedure Get(var V:longint: var W:real): begin V-Q[QBeg]: W:-A[V]: inc(QBeg): end. ? Q — хранит номера вершин, поставленных в очередь; f-l, если количество оборотов вала i еще не вычислялось, ? a[i] = [вычисленное кол-во оборотов в противном случае. Процедура пополнения очереди PutAll: procedure PutAll(V:1ongint: W:real); begin for i:-l to N do if (G[V.i]<>O) then begin if (x[v]<>x[i]) or (y[v]<>y[i]) then WN:-W*r[v]/r[i] else WN:-W; if a[i]-l then Put(i.WN) else Stop:- abs(a[i]-WN)>E: end: end; Для всех валов i, связанных с очередным валом V(io есть при G[ V, i] <> 0), если валы на ременной передаче, то вычисляем количество оборотов зависимого вала WN(w[i]) из соотношения U^*]*#[i] e И^[о]*Л[У]. Если же валы нажестком сцеп- сцеплении, то количества оборотов одинаковы ( WM m W). Далее, если количество оборотов этого вала еще не вычислялось, то добавляем в очередь текущий вал i; иначе (a[i] уже вычислялось ранее), если новое (WN) и старое (a[i]) значения не совпадают, (abs(a[i] - WN) > ?, здесь ?— принятая
4.2. Отношения других видов 211 константа различения вещественных чисел, например 0.000001), то переменная stop получает значение true, что прекращает дальнейшую обработку очереди. И, наконец, процедура вывода результатов: procedure OutResults: begin assign(output.'output.txt'): rewrite(output); if stop then begin writeln('D0 NOT TURN IT 0N!1): halt; end: for i:-l to N do begin a[1]:-trunc(a[1]): if a[1]-l then writeln('IDLE') else writeln(a[i]:0:0): end: close(output); end: Заметим, что по условиям задачи требуется выводить количество полных оборо- оборотов, а если на вал вращение не передается, то IDLE. Решение приведено в конце главы. 4.2. Отношения других видов В данном разделе собраны задачи на соотношение валют («Currency Exchange» и «Exchange Rates»), отношение порядка («Sorting It All Out»), отношение «ссылка на файл» («Проверка веб-страниц») и отношение «слово определяет дугу между вершинами, соответствующими начальной и конечной букве слова» («Play On Words»). 4.2.1. Задача «Currency Exchange» NEERC, Северный четвертьфинал, 2001 Currency Exchange (input.txt / output.txt / 2 с) В нашем городе работает несколько пунктов обмена валют. Каждый пункт специализируется на обмене двух конкретных валют и выполняет обмен- обменные операции только над ними. Может быть несколько пунктов, специали- специализирующихся на обменах одной и той же пары валют. Каждый пункт имеет свой собственный обменный курс. Обменный курс валюты А к валюте В есть количество валюты J3, которое выдается за единицу валюты А. Кроме того, каждый пункт обмена назначает «комиссионные» — сумму, которую вы должны заплатить за операцию обмена. Комиссия всегда собирается в ис- исходной валюте.
212 Глава 4 * Скрытые графы Например, если вы хотите обменять 100 долларов США на российские рубли в обменном пункте, в котором курс 29.75, а комиссия 0.39, то вы получите A00 - 0.39) * 29.75 - 2963.3975 российских рублей Вы точно знаете, что всего есть N различных валют, по которым выполня- выполняются обмены в нашем городе — и они обозначаются номерами от 1 до N. Тогда каждый обменный пункт может быть описан 6 числами: целыеЛ и В — номера валют, с которыми работает данный обменный пункт; и веществен- вещественные RABy CAB, RBA и СВА — курсы обмена и комиссионные для обмена валюту А на В и В на А соответственно. Николай имеет некоторое количество денег в валюте 5 и хочет узнать, мо- может ли он некоторой последовательностью операций обмена увеличить свой капитал. Конечно, он хочет иметь в конце операций деньги в той же исход- исходной валюте 5. Помогите ему ответить на этот трудный вопрос. Николай все- всегда должен иметь неотрицательную сумму денег во время выполнения опе- операций обмена. Ввод: Первая строка входного файла содержит четыре числа: N— количество валют, М — количество обменных пунктов, 5 — первоначальный номер валюты у Нико- Николая и V — первоначальное количество валюты у Николая. Каждая из последующихМстрок содержит 6 чисел — описание соответствующе- соответствующего пункта обмена в указанном выше порядке; числа разделены одним или более пробелами. 1 <S<N< 100, 1 <M< 100, V-действительное число, 0 < V< 1000. Во всех обменных пунктах «курс» и «комиссия» — вещественные числа, заданные с двумя знаками после десятичной точки, 0.01 < курс < 100, 0 < комиссия й 100. Назовем некоторую последовательность обменных операций простой, если в це- цепочке операций обмена каждый обменный пункт использовался не более одного раза. Вы можете полагать, что коэффициент отношения сумм в конце и начале простой последовательности обменных операций будет меньше, чем 10 000. Вывод: Если Николай может увеличить свой капитал, выведите «YES», иначе — выведи- выведите «NO». Пример ввода Пример вы&ода 3 2 1 10.0 NO 1 2 1.00 1.00 1.00 1.00 2 3 1.10 1.00 1.10 1.00 3 2 1 20.0 YES 1 2 1.00 1.00 1.00 1.00 2 3 1.10 1.00 1.10 1.00 Коротко постановка задачи может быть переформулирована следующим образом: имеется N видов валют и М пунктов, в которых можно обменивать валюту. Каж-
4.2. Отношения других видов 213 дый пункт работает с валютами РОВНО двух видов. То есть если он работает с ва- валютами А и В, то в нем можно поменять валюту А на валюту В и наоборот. Каж- Каждый валютный пункт задается 6 числами: А В RAB CAB RBA СВА Первые два числа (номера видов валют) — целые, остальные четыре числа — ве- вещественные. Обмен VA единиц валюты А на VB единиц валюты В производится по формуле VB:-(VA - CAB)*RAB. Аналогично обмен VB единиц валюты В на VA единиц валюты А производится по формуле VA:-(VB - CBA)*RBA Требуется узнать, можно ли увеличить сумму V, заданную в валюте 5, с помощью имеющейся системы обменных пунктов. При этом обмен каждого типа можно производить не более одного раза. В условиях оговорено также, что недопустимы обмены, при которых получается отрицательная сумма. Описание решения: Перейдем к графу следующим образом. Пусть вершина — вид валюты, а дуга — наличие валютной операции от одной валюты к другой. Фактически каждый обменный пункт задает две дуги между соответствующими вершинами. Начав с вершины, соответствующей валюте 5, необходимо найти цикл (завер- (завершающийся в этой же вершине), при котором сумма Уувеличится в результате конвертации, соответствующих циклу на графе. Если такой цикл есть — ответ «YES», иначе — «NO». Цикл можно строить поиском в глубину. Рассмотрим подробнее реализацию. Тело главной программы выглядит следующим образом: begin InputDataInitGraph; VNew:-V: DFS(S.V): assign(output.'output.txt');rewri te(output): if VNew>V then writeln('YES') else writelnCNO'): close(output): end. Здесь: ? InputDataInitGraph — процедура ввода исходных данных и построения соот- соответствующего графа; Q V — исходная сумма;
214 Глава 4 * Скрытые графы ? VNew — новая сумма, если ее удалось увеличить при конвертировании; ? DFS — рекурсивная процедура, реализующая «поиск в глубину». Рассмотрим подробнее процедуру InputDatalnitGraph procedure InputDataInitGraph: begin assign(input.'input.txt'): reset(input): readln (N.M.S.V): if V-0 then begin assign(output.'output.txt'):rewrite(output): writeln('NO'); close(output): halt: end: for i:-l to М do ReadAddEdges: close(input): end: Если исходная сумма равна 0, то ее увеличить нельзя. ReadAddEdges — процедура считывания данных об очередном обменном пункте и добавления в граф соответствующих дуг. procedure ReadAddEdges: begin read(f.t.RAB.CAB.RBA.CBA); inc(kG[f]): G[f.kG[f]]:-t: P[f.kG[f].l]:-RAB: P[f.kG[f].2]:-CAB: inc(kG[t]): G[t.kG[t]]:-f: P[t.kG[t].l]:-RBA: P[t.kG[t].2]:-CBA: color[f,kG[f]]:-FREE; color[t.kG[t]]:-FREE: end: Здесь: ? f- номер вида валюты A tfrom — откуда); ? t — номер вида валюты В (to — куда); ? kG[i] — количество дуг из вершины i; ? G[iJ] — номер вершины, в которую ведет дуга,;' из вершины i; ? P[iJ] — вес дуги из вершины i в вершину G[ij]; ? P[i,j, 1] — коэффициент обмена (Rft); Q P[iJ, 2] — комиссия (Cft); U coloj\iJ] — пометка об использовании дуги; из вершины i при построении цикла во время поиска в глубину; ? FREE — константа, обзначающая, что дуга не использовалась. Рассмотрим теперь процедуру DFS, реализующую поиск в глубину: procedure DFS(u:integer: V:real): var
4.2. Отношения других видов 215 j : byte; VCur : real. begin for j:-l to kG[u] do if color[u.j]-FREE then begin color[u.j]:-DONE: VCur:=Count(u.j.V): if VCur<-0 then exit: if (G[u.j]-S) then begin if VCur>VNew then VNew:-VCur: exit: end; DFS(G[u.j].VCur): color[u.j]:-FREE: end; end: Здесь: ? U — вершина, от которой продолжаем поиск в глубину; ? Count (u,j, V) — функция, осуществляющая обмен суммы Vc валюты типа U на валюту, в которую ведет дуга;; ? VCur — текущая сумма денег, получившаяся в результате последнего обмена. Если цикл замкнулся (G[u,j] = S), то выходим из процедуры DFS, предваритель- предварительно поменяв VNew, если найдена большая величина. Реализация функции конвертирования валют Count такова: function Count(F.T:integer; V:real):real: begin RAB:-P[f.t.l]: CAB:-P[f.t.2]: Count:-(V-CAB)*RAB: end: Полный текст решения задачи приведен в конце главы. 4.2.2. Задача «Exchange Rates» USA Programming, 1999 Exchange Rates (exchange.in / exchange.out / 5 c) Использование денег для оплаты за товары и услуги обычно упрощает жизнь, но иногда люди предпочитают обмениваться непосредственно това- товарами. В таком случае торговцы устанавливают коэффициенты обменов между товарами. Коэффициент обмена между двумя товарами А и В выра- выражается двумя положительными целыми числами т и я. В этом случае гово- рят, что т предметов товараЛравноценны п предметам товара В. Например, 2 печи могут быть равноценны 3 холодильникам.
216 Глава 4 * Скрытые графы Ваша задача - написать программу, которая по заданному списку обмен- обменных коэффициентов вычислит обменный коэффициент между двумя за- заданными товарами. Входной файл содержит одну или более команд, за которой следует строка, начинающаяся с символа «точка», которая сигнализирует о конце входно- входного файла. Каждая команда находится на отдельной строке и является или утверждением, или вопросом. Утверждение начинается с восклицательно- восклицательного знака и имеет вид «/ m itema = п itemb», где itema и itemb — различные имена товаров, а т и п - натуральные числа, меньшие 100. Эта команда говорит, что т предметов товара itema равноценны п предме- предметам товара itemb. Запрос начинается со знака вопроса и имеет вид « ? itema = itemb* — требу- требуется узнать обменный курс между itema и itemb, где itema и itemb — различ- различные товары, каждый из которых уже появлялся в предыдущих утвержде- утверждениях (хотя и не обязательно в одном и том же). Для каждого запроса выведите обменный курс между itema и itemb, осно- основываясь на всех утверждениях, обработанных до текущего запроса. Числа, задающие обменный курс, должны быть целыми и взаимно простыми. Если в этой точке невозможно определить обменный курс между данными това- товарами, используйте знак вопроса вместо целых чисел. Форматируйте вывод, как указано в примерах вывода. Примечания: Названия товаров будут иметь длину не более 20 символов и содержать только маленькие латинские буквы. Для всех названий будет использоваться единствен- единственное число. Всего будет не более 60 товаров. Для каждой пары товаров будет использовано не более одного утверждения. Не будет противоречивых утверждений. Например, противоречивы утверждения «2 pig e 1 cow*, «2 cow e 1 horse*, «2 horse e 3pig>. Утверждения во вводе не обяза- обязательно взаимно просты, но на выводе они должны быть такими. Хотя утверждения используют числа меньше 100, ответы на вопросы могут выра- выражаться большими числами, которые не превысят 10 000 после их сокращения. Пример ввода: ! 6 shirt - 15 sock ! 47 underwear - 9 pant ? sock - shirt ? shirt - pant ! 2 sock - 1 underwear ? pant - shirt Пример вывода: 5 sock - 2 shirt ? shirt - ? pant 45 pant - 188 shirt
4.2. Отношения других видов 217 ЗАМЕЧАНИЕ По условию на вводе соотношения могут задаваться не взаимно простыми числами, тем не менее ответы нужно приводить к взаимно простым коэффициентам отношения товаров. Описание решения: Для перехода к графу сопоставим товарам вершины, а соотношениям — дуги. Каж- Каждая строка типа «!» добавляет две дуги в граф и, если есть новые товары, то одну или две вершины. По каждой строке типа «?» алгоритмом Флойда устанавливаем соотношения между всеми товарами и отвечаем на поставленный вопрос. Рассмотрим реализацию подробнее. Тело главной программы выглядит следую- следующим образом: begin assign(input.'exchange.in'): reset(input): assign(output.'exchange.out'): rewrite(output): KV:-0: for i:-l to MaxN do for j:-l to MaxN do G[i.j]:-0: readln(s): while s<>'.' do begin if s[l]-'l'then AddGraph else Answer; readln(s): end: close(input):close(output): end. Здесь: ? KV— количество вершин (различных наименований товаров); ? G[iJ ] — указывает, установлено ли соотношение между товаром i uj; ? AddGraph — процедура добавления дуг в граф G; ? Answer — процедура формирования ответа на вопрос; ? s — вводимая строка. Рассмотрим процедуру AddGraph: procedure AddGraph: begin val(copy(s.l.k-l).nl.j): sl:-copy(s.l.k-l): val(copy(s.l.k-l).n2.j): s2:-s: delete(s.l.2): delete(s.l.k): delete(s.l.k+2) delete(s.l.k): ProcessName(sl. ProcessName(s2. Reduce(nl.n2): AddEdges(kl.k2. к к : к kl) к2) nl. :=pos(' :=pos(' :-pos(' п2): '.S) '.S) '.S)
218 Глава 4 * Скрытые графы Floyded:-False: end: Здесь: ? sl, s2 — названия товаров во введенной строке типа «!»; ? я1, n2 — количества товаров в соотношении; ? ProcessName(S, К) — процедура, которая по названию товара S выясняет его номер К\ ? Reduce(nl, ri2) — процедура сокращения п\ и я2 до состояния взаимной про- простоты; ? AddEdges(kl, k2, n\, n2) — добавить в граф дуги между вершинами kl и k2 с весами п\ и я2; ? Floyded — переменная, которая сбрасывается в /д&е при добавлении в граф очередного ребра и устанавливается в true после обработки графа алгоритмом Флойда. Ее наличие позволяет отвечать на вопросы без лишних запусков ал- алгоритма Флойда (пока граф не менялся от последнего запуска). Процедура ProcessName: procedure ProcessName(s:string: var i:longint): begin i:-l: while (i<-KV) and (SList[i]<>s) do inc(i): if i>KV then begin inc(KV); SL1st[KV]:-s: end: end: просматривает список SList уже имеющихся наименований товаров. Если 5 в нем содержится, то i получает соответствующий номер, иначе количество наименова- наименований ^^увеличивается на 1, a i автоматически имеет номер нового товара после исполнения оператора while. Процедура Reduce: procedure Reduce(var nl.n2:l6ngint); begin for j:-2 to min(nl.n2) do while ((nl mod j)-0) and ((n2 mod j)-0) do begin nl:- nl div j: n2:- n2 div j: end: end: последовательно делит на все общие множители числа nl и я2, в результате чего числа nl и n2 становятся взаимно простыми.
4.2. Отношения других видов 219 Процедура AddEdges добавляет в граф две дуги с взаимнообратными соотноше- соотношениями товаров. G[kl, k2] = 1, если между товарами к\ и k2 введено соотношение. Это соотношение есть P[kl, k2, l]/P[ki, k2, 2]. procedure AddEdges(к1.k2.nl.n2:1ongint): begin G[kl.k2]:-1: P[kl.k2.1]:-nl; P[kl.k2.2]:-n2: G[k2.kl]:-1: P[k2.kl.l]:-n2: P[k2.kl.2]:-nl: end; Теперь рассмотрим, как обрабатываются строки типа «?». Процедура Answer выделяет в s\ и 52 наименования товаров, которые нужно со- сопоставить. Затем с помощью процедуры ProcessName находятся соответствующие номера товаров И и k2. Далее, если процедура Floyd еще не вызывалась после до- дополнения графа, то она вызывается, строя все соотношения между товарами. Функ- Функция Exist отвечает на вопрос, установлено ли соотношения между товарами kX и k2. Если соотношение установлено, то вя1 и n2 находятся соответствующие ко- коэффициенты. procedure Answer: begin delete(s.l.2): k:-posC '.S): sl:-copy(s.l.k-l); delete(s.l.k+2): s2:-s: ProcessName(sl.kl): ProcessName(s2.k2): If Not Floyded then Floyd: if Exist(kl.k2.nl.n2) thenwriteln(nl.' '.SList[kl].1 - '.n2.' '.SList[k2]) elsewritelnC? '.SList[kl].' -? '.SList[k2]): end: Процедура Floyd пересчитывает все соотношения между товарами и устанавли- устанавливает признак Floyded=true (обработано алгоритмом Флойда). procedure Floyd: begin for k:-l to KV do for i:-l to KV do for j:-l to KV do if (G[i.k]-l) and (G[k.j]-l) and (i<>j) then begin G[1.j]:-1: nl:-P[i.k.l]*p[k.j.l]; n2:-P[i.k.2]*p[k.j.2]: P[i.j.l]:-nl:
220 Глава 4 * Скрытые графы P[i.j.2]:-n2; end; Floyded:-true; end: Функция Exist получает значение/я&е, если сотношение не установлено, и true — в противном случае. Кроме того, если соотношение между товарами kl и k2 уста- установлено, оно упрощается с помощью процедуры Reduce. function Exist(kl.k2:longint; var nl.n2:longint):boolean; begin Exist:- G[kl.k2]-1: if (G[kl.k2]-l) then begin nl:-P[kl.k2.1]: n2:-P[kl.k2.2]: Reduce(nl.n2): end: end: Полный текст решения задачи приведен в конце главы. 4.2.3. Задача «Sorting It All Out» East CentralNorth America Programming Contest, 2001 Sorting It AU Out (input.txt / output.txt / 10 c) Отсортированная в порядке возрастания последовательность различных величин может быть сформирована, если над этими величинами установ- установлено отношение порядка (<). Например, отсортированная по возрастанию последовательность Л, В, С, D означает, что А < В, В < С и С < D. Вам дается множество отношений видаЛ < В, и требуется установить, можно ли сфор- сформировать возрастающую последовательность. Ввод: Ввод состоит из множества тестов. Каждый тест начинается со строки, которая содержит два положительных целых числа п и m. Первое число указывает коли- количество объектов, которые нужно отсортировать B ^ п < 26). Объекты, подлежа- подлежащие сортировке, обозначаются первыми п большими латинскими буквами. Вто- Второе число т обозначает количество отношений вида А < В, которые даются в те- текущем тесте. Далее следуют М строк, задающих одно такое соотношение тремя символами: большая буква, символ «<» и другая большая буква. Значение п =* m e 0 обозначает конец ввода. Вывод: Для каждого теста выведите одну строку. Эта строка может быть одного из следу- следующих видов: Sorted sequence determined after xxx relations: yyy...y. Sorted sequence cannot be determined. Inconsistency found after xxx relations.
4.2. Отношения других видов 221 Здесь ххх — количество отношений, обработанных к моменту построения отсор- отсортированной последовательности или к моменту обнаружения противоречия (что встретится раньше), a yyy...y — последовательность, отсортированная в порядке возрастания. Пример ввода: 4 6 A<B A<C B<C C<D B<D A<B 3 2 A<B B<A 26 1 A<Z 0 0 Пример вывода: Sorted sequence determined after 4 relations: ABCD. Inconsistency found after 2 relations. Sorted sequence cannot be determined. Коротко говоря, в задаче требуется установить один из трех возможных фактов: 1. Между величинами построено отношение порядка («меньше»). 2. Обнаружено противоречие в отношении порядка. 3. Отношение порядка недоопределено. Причем третий факт можно обнаружить только после обработки всех отношений, а любой из первых двух может возникнуть до конца обработки всех отношений, поэтому требуется выводить еще и номер отношения, при обработке которого об- обнаружен соответствующий факт. Кроме того, в случае построения отношения по- порядка (например, Л < В < C< D), нужно его вывести (опустив знак «меньше»: ABCD). В примере ввода приведены варинты исходных данных для всех трех воз- возможных случаев. Описание решения: Переход к графу осуществим следующим образом: вершина — это величина, дуга — отношение «меньше» между двумя величинами. Тогда факт 2 означает, что в гра- графе существует цикл (то есть нашлись две величины такие, что X< Уи Y<X). Факт 3 означает, что граф не связный (то есть существуют величины, между ко- которыми не установлено отношение порядка). Факт 1 означает, что граф связный (между всеми величинами установлено отношение порядка). Нам также требует- требуется вывести порядок величин (от самой меньшей к самой большей), установлен- установленный отношением «меньше».
222 Глава 4 * Скрытые графы Рассмотрим подробнее реализацию. Тело главной программы таково: begin assign(input.'input.txt'); reset(input): assign(output.'output.txt'): rewrite(output): readln(N.M). while (N<>0) do begin for i:=l to М do readln(s[i]): InitGraph; for i:»1 to М do begin AddEdge(i.Sorted.Cycle); if Cycle then writelnCInconsistency found after '.i.' relations.1): if Sorted then writeln('Sorted sequence determined after '.i. 1 relations: '.R.'.'): if Sorted or Cycle then break: end: if not (Sorted or Cycle) then writeln('Sorted sequence cannot be determined.1): readln(N.M): end: close(input): close(output): end. Здесь: ? InitGraph — процедура инициализации графа; ? AddEdge — процедура добавления очередной введенной дуги к графу. Она же устанавливает, обнаружен ли в графе цикл (Cycle=true) и обнаружен ли полный порядок (Sorted=true). Если обнаружена истинность одного из условий Cycle или Sorted, то выводится соответствующее сообщение. В случае Sorted=true прямо из оператора writeln вызывается функция R. ? R — функция, которая возвращает строку символов в порядке, установленном введенным отношением. Если все строки отношения обработаны, а Sorted и Cycle не были ни разу установле- установлены в состояние true, то выводится сообщение, соответствующее факту 3 (отношение порядка не достроено). Заменить внутри цикла оператор break на оператор halt нель- нельзя, поскольку нам нужно обрабатывать множество тестов в одном входном файле. Рассмотрим процедуру InitGraph: procedure InitGraph: begin for i:=l to 26 do
4.2. Отношения других видов 223 for j:-l to 26 do G[i.j]:-O: for i:-l to 26 do a[i]:-0: end: , если есть дуга из вершины i в вершину;, > в противном случае; ? a[i] — количество вершин, с которыми связана вершина i. Теперь рассмотрим процедуру AddEdge procedure AddEdge(i:integer: var Sorted.Cycle:boolean): var p.k.q : integer: begin j:-ord(s[i.l])-ord('A')+l: k:-ord(s[i.3])-ord('A')+l: if G[j.k]<-0 then inc(a[j]): G[j.k]:-1: for p:-l to N do if G[p.j]-1 then begin if (G[p.k]-0) and (p<>k) then inc(a[p]): G[p.k]:-1: end: for p:-l to N do if G[k.p]-1 then begin if (G[j.p]-0) and (j>p) then inc(a[j]): G[j.p]:-1: end: Cycle:-False; for p:-l to N do Cycle:-Cycle or (G[p.p]-l): for p:-l tO N do b[p]:-0: for p:-l to N do b[a[p]+l]:-l: Sorted:-True: for p:-l to N do Sorted:-Sorted and (b[p]-l): end: Здесь выполняется следующая работа. Операторы j:-ord(s[i.l])-ord('A')+l: k:-ord(s[i.3])-ordCA')+l: обеспечивают переход от букв к соответствующим числам (вместо буквы А — чис- число 1, вместо буквы В — число 2 и т. д.).
224 ; Глава 4 * Скрытые графы if G[j.k]<-0 then inc(a[j]): G[j.k]:-1: Если от вершины j к вершине к еще не было пути, то увеличиваем количество вершин a[j], достижимых от вершины/ И отмечаем достижимость вершины k от вершины^(С[/?]: - 1;) for p:-l to N do if G[p.j]-1 then begin if (G[p.k]-0) and (p<>k) then inc(a[p]); G[p.k]:-1: end; Для всех вершин р, от которых была достижима вершина/ теперь устанавливаем достижимость и вершины k. Если ранее вершина k была недостижима отр, то уве- увеличиваем количество (fl[p]) вершин, достижимых от вершины р. for p:-l to N do if 6[k.p]-l then begin if (G[j.p]-O) and (j<>p) then inc(a[j]): G[j.p]:-1: end; Для всех вершин р, которые были достижимы от вершины к, теперь устанавлива- устанавливаем достижимость и от вершины/ Если ранее вершинар была недостижима от/ то увеличиваем количество (a[j]) вершин, достижимых от вершины/ Cycle:-False; for p:-l to N do Cycle:-Cycle ог (G[p.p]-l): Если для какого-то р существует путь из вершины р в нее же (G[p, р] - 1), то обнаружен цикл {Cycle получает значение true) for p:*l to N do b[p]:-0: for p:-l to N do b[a[p]+l]:-l: Sorted:-True: for p:-l to N do Sorted:-Sorted and (b[p]-l); Если отношение порядка полностью установлено, то a[i] принимает все различ- различные значения от 0 до N - 1 и соответственно все b[i] e 1. Таким образом, если все b[i] равны 1, то 5от?е^устанавливается в true, иначе — ъ/аЬе. Теперь рассмотрим процедуру R: function R:string; var b : array [1..26] of byte:
4.2. Отношения других видов 225 i.j.k.s.p : byte: Т : string: begin for j:-l to 26 do b[j]:-j: for j:-l to N-1 do begin s:-a[j]: k:-j: for i:-j+l to N do if a[i]>s then begin s:-a[i]: k:-i end: a[k]:-a[j]: a[J]:-s: p:-b[k]: b[k]:-b[j]: b[j]:-p: end: T:-": for i:-l to N do T:H>chr(b[i]+ord('A')-l): R:-T: end: Здесь вначале массив а сортируется по убыванию. Параллельно сортируется мас- массив Ь — номеров элементов в массиве а. Затем в строку Гобъединяются символы в порядке, соответствующем построенному соотношению. Полный текст решения приведен в конце главы. 4.2.4. Задача «Проверка веб-страниц» Всероссийская олимпиада школьников по программированию, 1999 Проверка веб-страющ (kiput.txt / Output.txt /10 с) Многим из тех, кому приходилось работать в Интернете, случалось сталки- сталкиваться с неправильными ссылками, то есть ссылками на несуществующие документы. Ваша задача — реализовать упрощенную проверку страницы на корректность ссылок. Ввод: Входной файл содержит название одного или нескольких документов и их содер- содержимое. Содержимое документов имеет следующий вид: <HTML> Текст <END> В тесте могут присутствовать ссылки на другие документы на данном сервере. Они имеют следующий вид: <A HREF-nvu* файла»>. Первая строка входного файла содержит число N — количество файлов (N < 100). Далее, с новой строки, идет название 1 -го документа, за которым следует содержи- содержимое 1-го документа, потом начиная с новой строки название 2-го документа, потом содержимое 2-годокумента, и т. д. Размер входного файла не превышает 100 Кбайт.
226 Глава 4 • Скрытые графы ПРИМЕЧАНИЯ Ваша программа должна игнорировать различие между строчными и прописными буквами. Любое ненулевое количество пробелов считается эквивалентным одному пробелу. Гаран- Гарантируется, что в теле документа не будут встречаться последовательности символов <html> и <end>. Имена файлов имеют длину не более 32 символов и содержат только латинские буквы, цифры и точки. Длина каждой строки входного файла не превышает 100 символов. В каждом документе не более десяти ссылок. Вывод: Поместите в выходной файл два числа, общее количество ссылок на несуществу- несуществующие документы и количество документов, до которых нельзя добраться, начав с первого документа и переходя по ссылкам. Пример ввода: 4 index.html <html> Index l>A HREF-T> 2<A href-">yTB <end> 1 <html> This picture shows an example <A HREF-."> <end> 2 <html> This problem is very simple <A href-"Sol.pas"> <end> 3 <html> Information about other contests is unavailable Look there <A HREF-"hehe.html"> <end> Пример вывода: 3 1 Описание решения: В качестве вершин графа возьмем имена заданных в исходном файле документов, а также имена файлов, на которые есть ссылки из заданных документов. В каче- качестве дуг на этом графе примем ссылку из файла на файл.
4.2. Отношения других видов 227 Тогда общее количество ссылок на несуществующие документы — это сумма всех ссылок в графе на вершины, соответствующие файлам, имена которых не заданы в исходном файле как имена документов. Количество документов, на которые нельзя попасть по ссылкам, начиная с первого, можно вычислить следующим об- образом: вначале методом Флойда построим матрицу достижимости, а затем посчи- посчитаем количество вершин, недостижимых от первой. Рассмотрим подробнее реализацию. Тело главной программы выглядит так: begin ReadDataInitGraph: assign(output.'output.txf): rewrite(output); writeln(DeadLinks.' '.UnReachDocs): close(output); end. Здесь: Q ReadDataInitGraph — процедура, которая вводит исходные данные и строит по ним граф; ? DeadLinks — функция, вычисляющая общее количество ссылок на несуществу- несуществующие документы; Q UnReachDocs — функция, вычисляющая количество документов, до которых нельзя добраться по ссылкам, начиная с первого документа и переходя по ссылкам. Рассмотрим теперь процедуру ReadDataInitGraph: procedure ReadDataInitGraph: begin assign(input.'input.txf): reset(input): readln(N): for i:-l to 2*N do for j:-l to 2*N do G[i.j]:-O: for i:-l to 2*N do RealDocs[i]:-false: k:-0: for t:-l to N do begin readln(s): UpCaseCompressStr(s): AddName(s.kl): RealDocs[kl]:-true: readln(s): {<HTML>} UpCaseCompressStr(s); while (s<'<END>') do begin j:-posC<A HREF-"'.s): m:-posC">' .s): while (j<>0) and (m<>0) and (j<m) do
228 Глава 4 * Скрытые графы begin p:-copy(s.j+9.m-(j+9)): AddName(p.k2): inc(G[kl.k2]): delete(s.l.m+l): j:-pos('<A HREF-*".s): m:-pos('">'.s): end: readln(s): UpCaseCompressStr(s): end: end: close(input): end: Здесь: 1, если файл i ссылается на файл j, 0 в противном случае. Заметим, что мы вводим последовательную нумерацию всех имен файлов — в порядке их появления в качестве имени существующего документа или в ка- качестве ссылки; RealDocs[f] = true, если номер вершины i соответствует существующему документу, fahe в противном случае; ? N — количество документов; ? К — общее количество имен файлов (вершин в графе — документов и ссылок); ? UpCaseCompressStr(s) — процедура приведения всех символов входной строки 5 к большим буквам, а также замены в ней всех последовательностей двух и бо- более пробелов на один пробел; О AddName(s, k) — процедура, которая добавляет в список файлов DocNames но- новый файл с именем s или находит номер существующего в этом списке имени. В обоих случаях k — номер имени файла s в списке всех файлов. Строки readln(s); UpCaseCompressStr(s): AddName(s.kl): RealDocs[kl]:-true: обеспечивают обработку имен документов (существующих файлов). При этом kl — номер текущего обрабатываемого документа. Строки while (s<>'<END>') do begin j:-posC<A HREF-"'.s):
4.2. Отношения других видов 229 m:-pos('">'.s): wh1le (j<>0) and (m<>0) and (j<m) do begin p:-copy(s.j+9.m-(j+9)); AddName(p.k2): inc(G[kl.k2]): delete(s.l.m+l): j:-pos('<A HREF-"'.s): m:-pos('">'.S); end: readln(s): UpCaseCompressStr(s): end: обеспечивают обработку тела документа: в каждой строке ищутся определения ссылок (<A НЯЕР=«файл»>), файлу-ссылке устанавливается номер k2 (если он но- новый, то одновременно пополняется список имен). Строка incF[kl.k2]): обеспечивает увеличение количества ссылок из файла с номером kl на файл с но- номером k2. Оператор whUe обеспечивает обработку нескольких ссылок в одной строке. Рассмотрим теперь процедуру UpCaseCompressStr. procedure UpCaseCompressStr(var s: string): begin for i :- 1 to Length(s) do s[i] :« UpCase(s[i]): p:-s[l]: J:-1: for i:-2 to length(s) do if (pCj]o' ') ог (s[i]<>' ') then begin p:-p+s[i]: inc(j) end; s:-p: end: С помощью встроенной в Паскаль функции UpCase все символы переводятся в за- заглавные. Далее в новую строку р переносим символ очередной из строки s, если последний символ в строке р — не пробел (или текущий символ в строке s — не пробел). Таким образом, результат выполнения — строка, в которой выброшены все лишние пробелы, а все маленькие латинские буквы заменены на большие. Процедура AddName обеспечивает поиск строки s в массиве DocNames и возвра- возвращение в переменной т соответствующего номера вне зависимости от того, суще- существовала ли строка или это строка новая и ее пришлось добавить в массив DocNames. procedure AddName(s:string; var m:integer): begin m:-l: while (пк-k) and (s<>DocNames[m]) do inc(m):
230 Глава 4 * Скрытые графы if m>k then begin inc(k); DocNames[k]:-s: end: end: Для обнаружения «мертвых ссылок» и вычисления их количества использова- использовалась функция DeadLinks. function DeadLinks:integer; begin t:-0; for j:-l to К do if (not RealDocs[j]) then for i:-l to К do inc(t.G[i.j]); DeadLinks:-t; end: Для всех номеров^нереальных документов (not RealDocsy]) суммируем все ссылки Наконец, для вычисления количества «недостижимых* файлов использовалась функция UnReachDocs: function UnReachDocs:integer; begin for t:-l to k do for i:-l to k do for j:-l to k do if (G[i.t]<>O) and (G[t.j]<>O) then 6[i.j]:-l: t:-0; for i:-2 to k do if (G[l.i]-0) and RealDocs[i] then inc(t): UnReachDocs:-t: end: Здесь первый тройной цикл — это реализация алгоритма Флойдадля построения матрицы достижимости. Затем идет цикл подсчета всех реальных документов (RealDocs[i] = true), которые не достижимы от первого (G[1, i] - 0). Полный текст решения задачи приведен в конце главы. 4.2.5. Задача «Play On Words» Central Europe Programming Contest, 1999 Play on Words (input.txt / output.txt) Некоторые из кодовых дверей содержат очень интересные словесные голо- головоломки. Команда археологов должна решить одну из них, поскольку нет другого способа открыть двери.
4.2. Отношения других видов 231 На каждой двери имеется большое количество магнитных плат. На каждой плате написано ровно одно слово. Платы могут быть переупорядочены в по- последовательность таким образом, что каждое слово начинается на ту букву, на которую предыдущее слово заканчивается. Например, за словом +acm* может следовать слово *motorola*. Ваша задача — написать программу, ко- которая прочитает список слов и определит, можно ли их переупорядочить таким образом, чтобы они составили последовательность (в соответствии с описанным выше правилом), и тем самым открыть дверь. Ввод: Ввод состоит из Гтестовых случаев. Их количество задается в первой строке входного файла. Каждый тест начинается со строки, содержащей одно целое число N, которое задает количество плат A ? Nu 100 000). Каждая из последующих Мстрок содержит ровно одно слово. Каждое слово состоит из строчных латинских букв. Длина слова — от 2 до 1000 букв. Одно и то же слово может появиться в списке несколько раз. Вывод: Ваша программа должна определить, возможно ли переупорядочить заданные слова таким образом, чтобы первая буква каждого слова совпадала с последней буквой предыдущего слова. При этом каждое слов из входного файладолжно быть использовано, и только один раз. Слово, которое на вводе встретилось несколько раз, должно быть использовано столько же раз. Если существует такой порядок слов, ваша программа должна вывести фразу «Ordering is possible». Иначе выведите фразу «The door cannot be opened». Пример ввода Пример вывода 3 The door cannot be opened 2 Ordering is possible acm The door cannot be opened ibm 3 acm malform mouse 2 ok ok Onucanue решения: Перейдем к графу следующим образом: буква — вершина, слово — дуга из началь- начальной буквы слова к конечной букве. Тогда ответ на вопрос задачи будет утверди- утвердительным, если выполняются три нижеследующих условия. 1. Граф связен: существует вершина, от которой доступна любая из остальных вершин. 2. Не существует вершин, у которых количество входящих дуг отличается от количества исходящих на 2 или больше.
232 Глава 4 * Скрытые графы 3. LT< 1 и GT< 1, где ZT— количество вершин, у которых количество входящих дуг на 1 меньше количества исходящих дуг, GT — количество вершин, у кото- которых количество входящих дуг на 1 больше количества исходящих дуг. Рассмотрим реализацию подробнее. Тело главной программы выглядит так: begin assign (input.'input.txf): reset(input): assign (output.'output.txt'): rewrite(output): readln(NT): for t:-l to NT do begin readln(NW): for i:-l to 26 do for j:-l to 26 do G[i.j]:-O: for i:-l to 26 do Letter[i]:-0: for w:-l to NW do begin ReadWord(b.e): Letter[b]:-1: Letter[e]:-1: if b<e then inc(G[b.e]): end: if CorrNumInOut and Connected then writelnCOrdering is possible.1) else writeln('The door cannot be opened.'): end: close(input): close(output): end. Здесь: ? t — номер теста во входном наборе данных; ? NT — количество тестов; если есть дуга из вершины i в вершину;, [0 в противном случае; ? Letter[i] -1, если буква, соответствующая вершине i была во входных данных первой или последней хотя бы в одном из слов; Q ReadWord(b, е) — процедура, которая умеет считывать слова длиной до 1000 сим- символов и возвращает b и е — номера, соответствующие первой и последней бук- букве прочитанного слова; ? CorrNumInOut — функция, которая получает значение true, если степени коли- количества входных и выходных дуг для всех вершин графа удовлетворяют выше- вышеописанным правилам B и 3); ? Connected — функция, которая получает значение true, если граф связный (условие 1).
4.2. Отношения других видов 233 Рассмотрим процедуру ReadWord procedure ReadWord(var B.E:byte): var s.c : char; begin read(c): b:-ord(c)-ordCa')+l: while (c<>#13) do begin s:-c: read(c); end: e:-ord(s)-ord('a')+l: read(c); {дочитываем #10} end: Строки в файле разделяются парами символов с кодами #13 и #10. Поэтому про- процедура читает первый символ, строит по нему номер b. Затем сохраняет прочи- прочитанный символ в s и читает следующий до тех пор, пока не доберется до ограничи- ограничителя строки — символа #13. Затем по символу s, содержащему последнюю букву введенного слова, строится величина е. После чего делается еще один оператор read, чтобы дочитать второй символ — разделитель строк (#10). Теперь рассмотрим функцию CorrNumInOut function CorrNumInOut:boolean; var LT. GT : byte; begin for i:-l to 26 do begin kGIn[i]:-0: kGOut[i]:-0: for j:-l to 26 do begin inc(kGIn [i].G[j.i]): inc(kGOut[i].G[i.j]): end: end: CorrNumInOut:-false: LT:-O: GT:-0: for i:-l to 26 do if (Letter[i]-l) then begin if abs(kGIn[i]-kGOut[i])>l then exit: if kGIn[i]-kGOut[i] - 1 then inc(GT): if kGIn[i]-kGOut[i] -1 then inc(LT): end:
234 Глава 4 * Скрытые графы CorrNumInOut:- (LT<-1) and FT<-1); end: Здесь kGIn[i] и kGOut[i] — количества входящих и исходящихдуг вершины i. Если они отличаются более чем на 1, то выход и CorrNumInOut - faise. Для всех реаль- реально введенных букв (Letter[i] - 1) считаем величины GTn LT, если они обе не пре- превышают 1, то CorrNumInOut e true, иначе -fabe. Наконец, рассмотрим функцию Connected: function Connected: boolean: begin for k:-l to 26 do for i:-l to 26 do for j:-l to 26 do if (G[i.k]<>0) and (G[k.j]<>O) then G[i.j]:-1: A11:-O: for i:-l to 26 d0 if Letter[i]-1 then inc(All); Connected:-true: for i:-l to 26 do begin if (Letter[i]-0) then continue: k.-0: for j:-l to 26 do if (Letter[j]-l) and (i<>j) and (G[i.j]<>O) then inc(k): if k-All-1 then exit: end: Connected:-false: end: Вначале с помощью алгоритма Флойда строим матрицу достижимости. Затем счи- считаем величину All — количество реально введенных букв (в качестве начальных или конечных всех введенных слов). Если существует вершина, из которой до- достижимы все остальные (k - All - 1), то Connected получает значение truet ина- иначе — значение/aZse. Полный текст решения задачи приведен в конце главы. 4.3. Задачи на множествах отрезков В данном разделе собраны задачи, в исходных формулировках которых фигури- фигурируют множества отрезков, но которые позволяют переформулирование в терми- терминах графов.
4.3. Задачи на множествах отрезков 235 4.3.1. Задача «Падение» Центральноевропейская олимпиада, 2000 Падение (FALL.IN / FALL.OUT) Рассмотрим игру, основанную на устройстве, изображенном на рис. 4.9. Рис. 4.9. Схема устройства для задачи «Падение» Устройство состоит из множества горизонтальных платформ различной длины, размещенных на различных высотах. Самая нижняя платформа - уровень земли (она расположена на высоте 0 и имеет бесконечную длину). С заданной позиции отпускают мяч в свободное падение в момент време- времени 0. Мяч падает с постоянной скоростью 1 м/с. Когда мяч достигает плат- платформы, он начинает катиться по направлению к одному из концов плат- платформы, по выбору игрока, с той же скоростью — 1 м/с. Когда он достигает конца платформы, он продолжает вертикальное свободное падение. Мячу не позволено свободное падение более чем на МАХ метров в одно падение (между двумя платформами). Напишите программу, которая найдет мини- минимальный по времени путь мяча к земле. Ввод: Первая строка: NX YMAX — четыре целых числа, разделенных пробелом: количество платформ (не учитывая землю), начальные координаты мяча, и установленное максимальное расстояние «свободного падения» между двумя платформами. Платформы нумеруются от 1 до N. Следующие Мстрок: X\i X2i Hi — 3 целых числа разделенных пробелами; i-я платформа расположена на высоте Hi между горизонтальными координа- координатами XXi и X2i включительно (Xli < X2f, i - l..N). ЗАМЕЧАНИЯ Игнорируйте диаметр мяча и толщину платформы. Если мяч падает точно на край платфор- платформы, он рассматривается как упавший на эту платформу. Никакие две платформы не имеют общих точек. Для заданных тестовых данных решение всегда существует. Все величины из- измерены в метрах. Вывод: Первая строка: целое число — время, через которое мяч коснется земли в соот- соответствии с вашим решением.
236 Глава 4 * Скрытые графы Оставшиеся строки до конца файла: Р TD, три целых числа, разделенных про- пробелами. Мяч коснется платформы Рв момент времени Ги покатится в направ- направлении D @ — влево, 1 — вправо). ? касание земли не должно появиться в этих строках. ? порядок вывода должен быть таким, что величины Т приводятся в порядке возрастания. Если существует несколько решений, выведите только одно. Ограничения: -20 000 <, Xii, X2i ? 20 000 (i - l.JV) 0<#i<y<20 000 Пример ввода 3 8 17 20 0 10 8 0 10 13 4 14 3 3 16 1 Прммершытода 23 241 1 11 1 Описание решения: Переход к графу осуществляем следующим образом: вершины — это начальное расположение шарика, все начала и концы горизонтальных отрезков (платформ), а также две специальных конечных вершины, описывающие платформу «земля*. Ребра — время, потребное шарику, чтобы добраться от фдеой вершины до другой. С учетом того, что скорость и горизонтального и вертикального движения 1 м/с, а все расстояния измерены в метрах, это время просто равно сумме вертикальных и горизонтальных расстояний между вершинами. Результат задачи может быть получен с помощью алгоритма Дейкстры, который ищет кратчайшие расстояния от заданной вершины (начальной точки располо- расположения шарика) до всех остальных. В качестве ответа нас интересует кратчайшее расстояние (время) от начальной вершины до вершины «земля». Рассмотрим подробнее реализацию этого решения. Тело главной программы вы- выглядит следующим образом: begin InputData: InitGraph; Dejkstra: QutResults: end. Здесь: ? InputData — процедура ввода исходных данных; ? InitGraph — процедура построения графа;
4.3. Задачи на множествах отрезков 237 ? Dijkstra — процедура, исполняющая алгоритм Дейкстры для поиска кратчай- кратчайших расстояний на графе от начальной до всех остальных. Параллельно, для каждой вершины оптимального пути запоминается предыдущая вершина. ? OutResults — процедура вывода результатов в соответствии с форматом вывода. Рассмотрим подробнее процедуру InputData procedure InputData: begin assign(input.'fall.in'); reset(input): readln(N.X.Y.MAX); for i:-l to N do readln(xl[i].x2[i].H[i]): close(input); end: Здесь: ? N—количествоплатформ; ? Х,К—начальныекоординатышарика; ? xl[i], x2[i], Щг] — описание платформы номер i (начало и конец поХи высота); ? МАХ — максимальное вертикальное расстояние, которое шарику разрешается пролететь между платформами. Теперь перейдем к процедуре InitGraph procedure InitGraph: begin SortPlatforms: for i:-0 to 2*N+1 do kG[i]:-0: Buildl(O.X.Y): for i:-l to N do Build2(i): end; Она вызывает процедуру сортировки платформ SortPlaforms. Затем последова- последовательно дополняет граф с помощью процедур Buildi и Build2. Процедура SortPlatforms приведена далее: procedure SortPlatforms: var s.k : integer; begin for i:-l to N do b[1]:-1: for j:-l to N-1 do begin s:-H[j]: k:-j: for i:-j+l to N do if H[i]>s then begin s:-H[i]: k:-i end: H[k]:-H[j]; H[j]:-s:
238 Глава 4 * Скрытые графы s:-b[k]: b[k] :-b[j]: b[j] :-s: s:-xl[k]; xl[k]:-xl[j]: xl[j]:-s: s:-x2[k]: x2[k]:-x2[j]: x2[j]:-s: end: H[N+l]:-O: xl[N+l]:-20000: x2[N+l]:-20000: end: Можно заметить, что ключом сортировки по убыванию является высота платформ H[i]. Одновременно синхронно переносятся начала (*l[t]) и концы (#2[i]) платформ, а также первоначальные номера платформ (b[i]). Массив b[i] будет использован при выводе в ответ исходных номеров платформ. Кроме того, в конец массива добавляется плафторма «земля» на позициюМ + 1 осортированного массива. Процедура Buildl(i, X, Y) заносит в граф расстояния от вершины (X, У) до бли- ближайшей платформы: procedure Buildl(i.X.Y:integer): begin j:-((i+l) div 2)+l: while ((X<xl[j]) ог (X>x2[j])) and (Y-H[j]<-MAX) do inc(j); if Y-H[j]>MAX then exit: G[1.kG[1]+l]:-2*j-l: G[i.kG[i]+2]:-2*j; P[i.kG[i]+l]:-(Y-H[j]) + (X-xl[j]): P[1.kG[i>2]:-(Y-H[j]) + (x2[j]-X): if j-N+l then begin P[i.kG[i>l]:-(Y-H[j]): P[i.kG[i>2]:-(Y-H[j]); end: inc (kG[i].2): end: Поиск начинается с ближайшей нижней платформы. j:-(A+l) div 2)+l Платформа считается найденной, если координата вершины поХнаходится меж- между координатами платформы, ((X<xl[j]) ог (X>x2[j])) и свободный полет шарика еще не превысил допустимый предел ( Y - H[j] < МАХ). Граф хранится в виде двух массивов: О kG[i] — количество ребер из вершины i; ? G[ij] — номер вершины, в которую ведет ребро номер.;' из вершины i.
4.3. Задачи на множествах отрезков 239 Если вершина не «Земля» Q * N + 1), то суммируются вертикальный и горизон- горизонтальный пути, иначе — только вертикальный. Процедура Build2 вызывает процедуру Buildl для обоих концов очередной плат- платформы: procedure Build2(i:integer): begin BuildlB*i-l.xl[i].H[i]): Bu1ldlB*1.x2[1].H[1]): end: Рассмотрим теперь процедуру Dijkstra procedure Dejkstra: const FREE - 0: DONE - 1: var Lab : array [0..M2] of integer: MinD. Bliz : longint: begin for i:-0 to 2*N+1 do {Инициализация данных} begin Lab[i]:-FREE; pred[i]:-0: d[i]:^naxlongint: end: d[0]:-0: Lab[0]:-DONE: pred[0]:--l: {y нее нет предка} for i:-l to kG[0] do d[G[0.i]]:-P[0.i]: {расстояния от 0-й до остальных} for j:-0 to 2*N+1 do begin {Поиск ближайшей из необработанных} MinD:-maxlongint: for i:-0 to 2*N+1 do if (Lab[i]-FREE) and (d[i]<MinD) then begin MinD:-d[i]: Bliz:-i end: Lab[Bliz]:*Done; { Пересчет кратчайших расстояний через ближайшую} for i:-l to kG[Bliz] do if Lab[G[Bliz.i]]-FREE then if d[G[Bliz.i]]>d[bliz]+P[Bliz.i]
240 Глава 4 * Скрытые графы then begin d[G[Bliz.i]]:-d[bliz]+P[Bliz.i]: pred[G[Bliz.i]]:-Bliz: end: end: end; Эта процедура, как обычно, инициализирует начальные данные, а затем в цикле находит ближайшую к текущей вершину {Bliz) и пересчитывает через нее рассто- расстояния до всех оставшихся вершин. Попутно в pred[i] хранится номер вершины, являющейся предыдущей для вершины i на оптимальном маршруте из начальной вершины в конечную. Теперь рассмотрим подробнее процедуру вывода результатов. procedure OutResults: var res : array [l..MaxN] of integer: t.kp. NumP.D1r.Time.Last.Curr : integer: begin assign(output.'fal1.out*): rewrite(output): writeln(d[2*N+l]): t:-2*N+l: kp:-O: while (pred[t]<>O) do begin inc(kp): res[kp]:-pred[t]: t:-pred[t]; end: Last:-X: for i:-kp downto 1 do begin NumP:-(Res[i]+l) div 2; Dir :-l -(Res[i] mod 2): if Dir-0 then Curr:-xl[NumP] else Curr:-x2[NumP]: Time:-D[res[i]]-abs(Curr-Last): writeln(b[NumP].' '.Time.' '.Dir): Last:-Curr; end: close(output): end: Вначале выводится минимальное время полета шарика (d[2 *N + 1]). Далее по мас- cmypred восстанавливается оптимальный маршрут Res. И затем циклом по всем
4.3. Задачи на множествах отрезков 241 элементам оптимального маршрута выводятся результаты в соответствии с фор- форматом вывода. Полный текст решения приведен в конце главы. 4.3.2. Задача «The Doors» Mid-Central USA Programming Contest, 1996 The Doors (doors.ln / doors.out / 30 c) Вы должны найти длину кратчайшего пути в комнате, в которой располо- расположены препятствия. Комната всегда имеет стороны х - 0, х - 10, у - 0, у e 10. Начальная и конечная точки пути всегда @,5) и A0,5). Может быть от 0 до 18 вертикальных стен внутри комнаты, каждая с двумя дверями. Ввод/вывод: Первая строка содержит количество внутренних стен. Затем идет строка, описы- описывающая каждую стену пятью вещественными числами. Первое число — ж-коор- дината стены @ < х < 10). Оставшиеся четыре числа — это у-координаты концов дверей в этой стене. Х-координаты стен задаются в порядке возрастания. Для каж- каждой стены у-координаты дверей также задаются в порядке возрастания. Входной файл содержит не менее одного такого набора данных. Конец входного файла обо- обозначается числом -1 на месте количества стен. Выходной файл должен содержать одну строку для каждого теста. Эта строка долж- должна содержать длину минимального пути, округленную до двух знаков после деся- десятичной точки. Пример ввода: 1 5 4 6 7 8 2 4 2 7 8 9 7 3 4.5 6 7 -1 Пример вывода: 10.00 10.06 Фактически в задаче требуется найти кратчайший путь из точки @,5) в точку A0,5), учитывая наличие задаваемых препятствий в виде вертикальных стен. 8 каждой стене есть две двери, через которые можно преодолевать стену. Перейдем к графу следующим образом. Пусть начальная точка, конечная точ- точка и границы дверей — это вершины графа. Ребро связывает две вершины, если одна точка находится в зоне прямой видимости другой. В качестве веса ребер примем расстояния по прямой между вершинами. Найти кратчайшее расстояние
242 Глава 4 * Скрытые графы на заданном графе от начальной вершины до конечной можно с помощью алго- алгоритма Дейкстры. Рассмотрим подробнее реализацию решения. С учетом формата ввода (с возможностью наличия нескольких тестов в одном файле) тело главной программы выглядит следующим образом: begin assign(input.'doors.in'):reset(input): assign(output.'doors.out'):rewrite(output): read(N): while N<>-1 do begin if N-0 then writelnA0.0:0:2) else begin InputData; InitGraph; Dejkstra; writeln(d[4*N+l]:0:2): end: read(N): end: close(input): close(output): end. Здесь отдельно обрабатывается ситуация отсутствия препятствий (N - 0). ? InputData — процедура ввода исходных данных; ? InitGraph — процедура построения графа; ? Dijkstra — процедура, реализующая поиск кратчайшего пути от вершины 0 до всех остальных вершин алгоритмом Дейкстры. Все найденные кратчайшие расстояния находятся в массиве D[1..4 *N + 1]. ? D[A *N + 1 ] — кратчайшее расстояние от вершины 0 до вершины AN + 1 (послед- (последней вершины). procedure InputData: begin for i:- 1 to N do begin read(x[i]): for j:-l to 4 do read (y[i.j]): end: end: Препятствия вводятся в виде массива горизонтальных координат стен x[i] и верти- вертикальных координат дверей y[i,j] — по 4 координаты на каждую вертикальную стену.
4.3. Задачи на множествах отрезков 243 Рассмотрим подробнее процедуру построения графа InitGraph: procedure InitGraph: var i.j : integer: begin for i:-0 tO (N-l)*4 do kG[i]:-4; for i:-4*(N-l)+l to 4*N do kG[i]:-l; for i:-l to N-1 do for j:-l to 4 do for k:-l to 4 do begin G[4*(i-l)+j.k]:-4*i + k: P[4*A-l)+j.k]:-D1st(x[1].y[1.j].x[1+l].y[1+l.k]): end: for i:-l to 4 do begin G[O.i]:-i: {Начальнаявершина} P[0.1]:-D1st@.0.5.0.x[l].y[l.1]): G[4*(N-l)+i.l]:-4*N+l: {Конечная вершина} P[4*(N-l)+i.l]:-Dist(x[N].y[N.i].10.0.5.0): end: for j:-5 to 4*N+1 do if Seen(O.j.Dij) then Add(O.j.Dij): if N>-2 then for j:-l to 4*(N-1) do if Seen(j.4*N+l.Dij) then Add(j.4*N+l.Dij): if N>-3 then for i:-l to N-2 do for j:-i+2 to N do for k:-l to 4 do for m:-l to 4 do if Seen((i-l)*4+k.(j-l)*4+m.Dij) then Add((i-l)*4+k.(j-l)*4+m.Dij): end: Граф, как обычно, хранится в следующем виде: О kG[i] — количество ребер из вершины i; ? G[i,j] — номер вершины, которую с вершиной i соединяет ребро;; ? P[i,j] — вес ребра от вершины i к вершине G[i,j]. Построение графа проводится в несколько этапов. Прежде всего формируем ко- количества «обязательных ребер»: for i:-0 to (N-l)*4 do kG[i]:-4: for i:^*(N-l)+l to 4*N do kG[i]:-l:
244 Глава 4 * Скрытые графы то есть каждая вершина, кроме вершин из последней стены, имеет по 4 ребра к вер- вершинам следующей стены. Вершины последней стены связаны только с одной вер- вершиной (последней). Затем формируем элементы массивов G и R for i:-l to N-1 do for j:-l to 4 do for k:-l to 4 do begin G[4*(i-l)+j.k]:-4*i + k: P[4*(i-l)+j.k]:-Dist(x[i].y[i.j].x[i+l].y[i+l.k]): end: Каждая из вершин стены i должна быть связана ребром с каждой из вершин стены (i + 1). Вес ребра — расстояние между соответствующими точками находится с по- помощью процедуры Dist, параметрами которой являются координаты точек (jrl, у\) и (x2, г/2) между которыми нужно найти расстояние: function Dist(xl.yl.x2.y2:real):real; begin Dist:-sqrt(sqr(xl-x2)+sqr(yl-y2)); end: Затем добавляем в граф ребра от вершины 0 к ближайшим (ничем не заслонен- заслоненным) вершинам 1-4. И одновременно добавляем в граф ребра от вершин из пос- последней стены D*(N - 1) + i) до последней вершины D *N + 1). for i:-l to 4 do begin G[O.i]:-i: {Начальная вершина} P[0.i]:-Dist@.0.5.0.x[l].y[l.i]): G[4*(N-l)+1.l]:-4*N+l: {Конечная вершина} P[4*(N-l)+i.l]:-Dist(x[N].y[N.i].10.0.5.0): end: Далее добавляем в граф все ребра от вершины 0 до остальных вершин, находя- находящихся в зоне прямой видимости: for j:-5 to 4*N+1 do if Seen(O.j.Dij) then Add(O.j.Dij): Здесь используется функция Seen(iJ, Dij), которая отвечает на вопрос видна ли напрямую вершинау из вершины i, и, если ответ утвердительный, то в Dij возвра- возвращается кратчайшее расстояние от вершины i до вершины/ Также здесь используется nponeRypbAdd(i,j, Dij), которая добавляет в граф ребро от вершины i к вершине,;' с весом Dij. Конкретную реализацию функции Seen и процедуры Add мы рассмотрим чуть позже. Следующий шаг в процессе формирования графа — внесение в него ребер к по- последней вершине D*N + 1) от всех вершин графа, которые могут иметь преграды
4.3. Задачи на множествах отрезков 245 на своем пути (если таковые имеются:#? 2), то есть всех, кроме тех, которые на- находятся в последней стене: if N>-2 then for j:-l to 4*(N-1) do if Seen(j.4*N+l.Dij) then Add(j.4*N+l.Dij): И, наконец, на последнем этапе в граф вносятся ребра от каждой вершины из стены i к каждой вершине стены;, если они находятся в зоне прямой видимости. При этом i nj рассматриваются такими, чтобы между ними была по меньшей мере одна стена: if N»3 then for i:-l to N-2 do for j:-i+2 to N do for k:-l to 4 do for m:-l to 4 do if Seen((i-l)*4+k.(j-l)*4+m.Dij) then Add((i-l)*4+k.(j-l)*4+m.Dij): Здесь, как и прежде, используются функция Seen и процедура ADD. Рассмотрим реализацию функции Seen: function Seen(i.j:integer; var Dij:rea1):boolean: var k.m : integer; begin Seen:-false: if i-0 then begin ixl:-0; xl:-0: yl:-5.0: end else begin ixl:- ((i-l) div 4)+l; 1yl:-A mod 4): if iyl-0 then iyl:-4: xl:-x[ixl]: yl:-y[ixl.iyl]: end: if j-4*N+l then begin ix2:-N+l: x2:-10.0: y2:-5.0: end else begin ix2:- ((j-l) div 4)+l: iy2:-(j mod 4): if iy2-0 then iy2:-4: x2:-x[ix2]: y2:-y[ix2.iy2]: end: A:-y2-yl: B:-xl-x2: C:-yl*(x2-xl)-xl*(y2-yl); for k:-ixl+l to ix2-l do begin CurY:-(A*x[k]+C)/B:
246 Глава 4 * Скрытые графы if (CurY<y[k.l]) ог (CurY>y[k.4]) ог ((CurY>y[k.2]) and (CurY<y[k.3])) then exit: end: Dij:-Dist(xl.yl.x2.y2): Seen:-true: end; Прежде всего по номерам вершин iJ находятся номера стен (irl и ix2) и относи- относительные номера вершин в стене от 1 до 4 (iyl, iy2) Затем находятся соответствую- соответствующие реальные координаты вершин i(xl, y\) nj(x2,y2). Отдельно обрабатываются номера вершин, не укладывающиеся в схему общей нумерации: начальная @) и ко- конечная D*JV + 1). Далее через найденные точки (xl, у\) и (x2, y2) проводится прямая A*x + Затем для всех стен между стенами с этими вершинами проверяем, не закрывает ли какая-то из них «линию прямой видимости». Это делается следующим обра- образом. Для горизонтальной координаты очередной стены (*[?]) выясняем на какой высоте проходит наша прямая CurY:-(A*x[k]+C)/B Если текущая высота не на уровне ни одной из дверей, то есть ниже нижней двери (CurY < y[k, 1]), или выше верхней двери (CurY > y[k, 4]), или между верхом ниж- нижней и низом верхней дверей ((CurY>y[k, 2]) и (CurY<y[k, 3])), то выходим из функции со значением Seen =fake. Иначе (если вершины i и j находятся в зоне прямой видимости) — вычисляем расстояние между ними с помощью функции Dist и выходим из функции со зна- значением Seen = true. Процедура Л^добавляет очередное ребро в граф: procedure Add(i.j:integer.Dij:real): begin inc(kG[i]): G[i.kG[i]]:-j: P[i.kG[i]]:-Dij: end: Процедура Dejkstra, реализующая поиск всех кратчайших растояний на графе от вершины 0 до всех остальных реализована точно так, как при решении предыду- предыдущей задачи. Полный текст решения задачи приведен в конце главы. 4.3.3. Задача «Борозды» Всероссийская олимпиада по программированию, 1999 Борозды (lnput.txt / Output.txt / 10 с) На паркетном полу физико-математической школы № 932 какой-то хули- хулиган стамеской проделал несколько борозд, идущих параллельно стенам.
4.3. Задачи на множествах отрезков 247 Администрация школы решила закрасить все борозды синей краской. При- Приготовив все необходимое для покраски, главный маляр задумался: а можно ли закрасить все борозды, не отрывая валика от пола и, более того, не закра- закрашивая одну и ту же борозду дважды. Ваша задача будет состоять в том, чтобы определить, возможно ли такое окрашивание, и если это так, то вывести координаты какой-нибудь точки, с которой можно начинать покраску. Ввод: В первой строке входного файла находится число N(\ <N< 100) — количество борозд, проделанных хулиганом. За ним следуют 4Мцелых чисел — координаты концовкаждой изборозд (-1000^#l,yl,*2,#2 < 1000). Вывод: В первую строку выходного файла выведите «NO», если такого окрашивания не существует, и «YES», если оно существует. В последнем случае во вторую строку выведите координаты точки, с которой можно начинать красить. Пример ввода 8 363 13 9 1 916 18 1 18 16 9 118 1 3696 96186 3 13 18 13 9 16 18 16 пример вывода YES 186 Описание решения: Задача к графам сводится следующим образом: вершины графа — точки (концы отрезков), ребра — наличие отрезка между соответствующими точками. Граф мож- можно будет полностью обойти указанным в задаче образом (пройдя по каждому реб- ребру РОВНО 1 раз) только при выполнении следующих двух условий/ 1. Графсвязный. 2. Степени всех его вершин четны (и тогда обход можно начинать с любой из его вершин) или степени ровно двух вершин нечетны (тогда обход можно начи- начинать с любой из вершин с нечетной степенью). Рассмотрим реализацию решения. Тело главной программы выглядит так: beg1n InputData: InitGraph; Count(EvenV.xO.yO): assign(output.'output.txt'); rewrite(output); if Connected and ((EvenV-K) ог (EvenV-K-2))
248 Глава 4 * Скрытые графы then begin writeln('YES'): writeln(xO.' '.yO) end else writeln('NO'): close(output): end. Здесь: ? InputData — процедура ввода исходных данных об отрезках; Q InitGraph — процедура формирования графа; ? Count — процедура, возвращающая количество вершин с четной степенью (Everi) и координаты точки (x0, y0), с которой нужно начинать обход; ? Connected — функция, возвращающая значение true, если граф связный, и/аке — в противном случае; Q К — количество всех вершин в графе (количество всех различных точек из тех, которые введены в качестве концов отрезков). Рассмотрим детальнее процедуру InputData: procedure InputData; begin assign(input.'input.txf): reset(input); readln(N): for i:-l to N do readln(x[2*i-l].y[2*i-l].x[2*i].y[2*i]); close(input): end: Все введенные точки удобнее хранить в виде пары массивовХи Y. Тогда процеду- процедура формирования графа выглядит следующим образом: procedure InitGraph; begin k:-2; AddNode(l.l); AddNodeB.2); AddEdgeA.2): for i:-2 to N do begin if not FoundB*i-l.vl) then begin inc(k): AddNode(k.2*i-l): end; if not FoundB*i.v2) then begin inc(k): AddNode(k.2*i); end: AddEdge(vl.v2) end: end: Здесь: ? List — массив номеров различных точек из тех, что поданы на вход в качестве концов отрезков; Q AddNode(i,j) — процедура добавления к графу вершины с номером i (исполь- (используя при этом входную точку с номеромД
4.3. Задачи на множествах отрезков 249 Концы первого отрезка сразу заносятся в граф вместе с соответствующим реб- ребром. Затем в цикле по всем остальным отрезкам делается следующее: ? если первый конец отрезка отсутствует в графе, то он туда добавляется, vl — номер найденной или добавленной вершины; ? если второй конец отрезка отсутствует в графе, то он туда добавляется, v2 — номер найденной или добавленной вершины. С помощью процедуры AddEdge в граф добавляется ребро между вершинами v\ и v2. Ниже представлена процедура AddNode: procedure AddNode(i.j:integer); begin List[i]:-j; kG[i]:-0: end: Процедура AddNode запоминает в список List номер добавленной вершины и устанавливает степень добавленной вершины (kG[i]) равной нулю. Ниже представлена процедура AddEdge: procedure AddEdge(i.j:integer); begin inc(kG[i]): G[1.kG[1]]:-j: inc(kG[j]): G[j.kG[j]]:-i: end: nponeRypaAddEdge(ij) увеличивает степени вершин i nj и модифицирует граф G, внося ссылки из вершины i на вершину^ и наоборот. Ниже представлена функция Found. Она анализирует, есть ли в списке List вер- вершина с координатами, равными координатам очередной точки (x[t], y[t]). Если есть, то v получает значение, равное номеру этой точки в списке List, иначе v полу- получает значение на 1 больше, чем количество точек в списке List. function Found (t:integer: var v:integer):boolean; begin v:-l: while (v<-K) and ((x[List[v]]<>x[t]) ог (y[L1st[v]]oy[t])) do inc(v); Found :- v<-k; end: Теперь рассмотрим процедуру Count: procedure Count(var EvenV.xO.yO:integer): begin EvenV:-0: xO:-x[l];yO:-y[l]; for i:-l to k do if Odd(kG[i])
250 Глава 4 * Скрытые графы then begin xO:-x[List[i]]: yO:-y[List[i]] end else inc(EvenV); end. Она возвращает EvenV- количество вершин с четной степенью, а также (яО, yO). Если все вершины имеют четную степень, то (jcO, yO) — это координаты первой точки (в таком случае любая вершина графа является правильной начальной точкой). Если же есть вершины с нечетной степенью, то (*0, yO) — координаты одной из них (в данном случае — последней). Осталось рассмотреть функцию Connected, которая определяет, является ли граф связным. function Connected:boolean; begin for i:-l to К do Free[i]:-true: BegQ:-l; EndQ:-O: Put(l); while (BegQ<-EndQ) do begin Get(i): for j:-l to kG[i] do if Free[G[i.j]] then Put(G[i.j]): end: Connected:-false: for i:-l to N do if Free[i] then exit: Connected:-true: end; Это делается с помощью поиска в ширину от первой вершины. Если все вершины достигнуты (Free[i] =fabe для всех вершин), то граф связный, иначе — нет. Полный текст решения задачи приведен в конце главы. 4.3.4. Задача «N-Credible Mazes» GreaterNew York Programming Contest, 2000 N-Credible Mazes (input.txt / output.txt / 10 c) Точку в я-мерном пространстве назовем я-тересной, если все ее коорди- координаты — неотрицательные числа. Например A, 2, 3) — З-тересная точка (я-тересная в трехмерном пространстве. Две я-тересных точки назовем соседними, если их координаты отличаются ровно на 1 и только в одном измерении. Например A, 2,3) — соседняя с @, 2,3), B,2,3) и A,2,4), но не соседняя с B, 3, 3) или с C, 2, 3). Определим я-тересное пространство как коллекцию путей между соседни- соседними я-тересными точками. Наконец, я-тересным лабиринтом назовем я-те-
4.3. Задачи на множествах отрезков 251 ресное пространство с указанными в нем двумя я-тересными точками — начальной и конечной. Ввод: Входной файл состоит из описаний одного или более лабиринтов. Первая строка описания всегда задает п — размерность пространства. В данной задаче п всегда меньше 10. Вторая строка содержит 2я неотрицательных чисел. Первые п из них описывают начальную позицию начиная с меньшей размерности, а следующие п описывают конечную позицию. Далее идет неотрицательное количество строк, указывающих путь между соседними я-тересными точками в этом пространстве. Этот список завершается строкой, содержащей только число -1. В одном файле может быть задано несколько таких описаний. Конец файла сиг- сигнализируется строкой, задающей 0 в качестве размерности пространства. Вывод: Для каждого лабиринта выводите его номер во вводе, например, для первого ла- лабиринта выводите «Maze 1», для второго «Maze 2» и т. д. Если для этого лабирин- лабиринта возможно добраться от начальной точки к конечной, выведите в той же строке «can be travelled», иначе выведите «cannot be travelled». Пример ввода: 2 0 0 2 2 0 0 0 1 0 1 0 2 0 2 1 2 1 2 2 2 -1 3 1 1 1 1 2 3 1 1 2 1 1 3 1 1 3 1 2 3 1 1 1 1 1 0 1 1 0 1 0 0 1 0 0 0 0 0 -1 0 Пример вывода: Maze #1 can be travelled Maze #2 cannot be travelled Описаниерешения: К графу переходим очевидным образом: точки — вершины, отрезки — ребра. Тре- Требуется узнать, существует ли маршрут в графе от одной вершины до другой. Это можно сделать, например, поиском в ширину.
252 Глава 4 * Скрытые графы Расмотрим подробнее реализацию. Тело главной программы с учетом формата ввода (с несколькими тестами в одном входном наборе данных) выглядит следу- следующим образом: begin assign(input.'input.txf):reset(input): assign(output.'output.txt');rewrite(output): readln(N); t:-0; while(N<>0) do begin inc(t): InputDataInitGraph: if PathExists then writeln('Maze #*.t.' can be travelled') else writelnCMaze #'.t.1 cannot be travelled1): readln(N): end; close(input): close(output): end. Здесь: ? InputDataInitGraph — процедура, которая формирует граф непосредственно в процессе ввода исходных данных; ? PathExists — функция, которая возвращает true, если путь существует, и/аке — в противном случае. Рассмотрим подробнее процедуру InputDataInitGraph: procedure InputDataInitGraph: begin Take(VFrom): Take(Vto): K:-0: AddNode(l.Vfrom): AddNodeB.Vto): Take(i): while i<>-l do begin Take(j): if not Found(i.vl) then AddNode(vl.i): if not Found(j.v2) then AddNode(v2.j): AddEdge(vl.v2); Take(i) end: end:
4.3. Задачи на множествах отрезков 253 Здесь: ? Take(i) — процедура ввода очередной точки Af-мерного пространства и преоб- преобразования ее к соответствующему числу (по условиям задачи координаты — числа от 0 до 9); Q AddNode(i,f) — добавить в граф вершину i, которая имеет соответствующее ей число^'; Q AddEdge(vl, v2) — добавить в граф ребро между вершинами vl и v2; ? Found(i, v) — поискать вершину с кодовым числом i в списке вершин графа Li$t; Q v — номер найденной вершины, если она существует, или номер на 1 больше имеющегося количества вершин в противном случае. Вершина «откуда идти» вносится в граф под номером 1, вершина «куда идти» вносится в граф под но- номером 2. Рассмотрим процедуру Take: procedure Take(var VFrom:longint): var k.s.i.j : longint: begin k:-l: s:-0; Vfrom:-1: for i:-l to N do begin read(j): if j—1 then exit; s:-s+k*j; k:-k*lO end: VFrom:-s: end: В результат Vfrvm попадает сумма взвешенных координат точки. Первая коорди- координата умножается на 1, вторая — на 10, третья (если есть) — на 100, и т. д. Работа процедур AddNode, AddEdge и функции Found описана при объяснении решения предыдущей задачи. Рассмотрим теперь реализацию функции PathExists: function PathExists:boolean; begin for i:-l to К do Free[i]:-true: BegQ:-l; EndQ:-O; Put(l); PathExists:-true: while (BegQ<-EndQ) do
254 Глава 4 • Скрытые графы begin Get(i): for j:-l to kG[i] do 1fG[1.j]-2 then exit else if (Free[G[i.j]]) then Put(G[i.j]): end: PathExists:-false; end; Здесь выполняется поиск в ширину от вершины 1 к вершине 2. Если вершина 2 достигнута, поиск прекращается. Если поиск завершен (очередь пуста), а верши- вершина 2 так и не достигнута, значит, она недостижима. Полный текст решения приводится в конце главы. 4.4. Задачи для самостоятельного решения 4.4.1. Задача «Door Man» South Central USA Programming Contest, 2002 Door Man (клавиатура / экран / 5 с) Вы — дворецкий в огромном доме. В нем так много комнат, что они просто перенумерованы @, 1, 2, 3,...). Ваш хозяин постоянно оставляет двери от- открытыми. Ваша задача — определить, существует ли такой путь, что: 1) дверь закры- закрывается сразу после того, как вы в нее прошли; 2) закрытая дверь никогда не открывается; 3) завершается путь в той же комнате, где начинался (с номе- номером 0), причем все двери уже закрыты. В этой задаче вам дается список комнат и открытых дверей между ними. Нет необходимости определять маршрут, нужно только выяснить, суще- существует ли он. Ввод: Ввод стоит из серии (до 100) тестов. Каждый тест содержит 3 компонента: ^стар- ^стартовая строка *START М N*, где М обозначает номер комнаты, из которой не- необходимо начать обход, N — количество комнат в доме A й N < 20); 2) список ком- комнат с открытыми дверями (из N строк). В каждой строке для текущей комнаты приводятся номера открытых дверей в комнаты с большим номером. Например, если комната 3 имеет открытые двери в комнаты 1, 5, 7. то строка для комнаты 3 будет содержать числа 5 и 7. Первая строка представляет комнату 0, вторая стро- строка представляет комнату 1, и т. д., последняя строка представляет комнату с но- номером (N - 1). Некоторые строки могут быть пустыми (в частности, последняя строка будет пустой всегда, поскольку имеет самый большой номер). В каждой строке номера дверей приводятся в порядке возрастания. Комнаты могут соеди- соединяться множеством дверей; 3) конечная строка - «END».
4.4. Задачи для самостоятельного решения 255 За последним набором данных идет строка «ENDOFINPUT». В одном наборе дан- данных может быть не более 100 дверей. Вывод: Для каждого входного набора данных выведите ровно одну строку. Если суще- существует требуемый путь — выведите « YES X», где X — количество дверей, которые придется закрыть, иначе выведите «NO». Пример ввода: START 1 2 1 END START 0 5 1 2 2 3 3 4 4 END START 0 10 1 9 2 3 4 5 б 7 8 9 END ENDOFINPUT Пример вывода: YES 1 N0 YES 10 4.4.2. Задача «This Sentence is false» South America Programming Contest, 2002 This Sentence is False (sentence.in / sentence.out / 25 c) В руки секретной службы короля Xeon 2.4 попал документ с утверждения- утверждениями вида «Предложение X истинно/ложно». Вам приказано проверить, является ли этот набор предложений противо- противоречивым. Если нет, то какое максимальное количество предложений в нем может быть истинным. Ввод: Входной файл содержит несколько документов. Каждый документ начинается со строки, содержащей целое число N, которое обозначает количество предложений
256 Глава 4 * Скрытые графы в документе A ? N ? 1000). Каждая из следующих Мстрок содержит одно утверж- утверждение. Утверждения нумеруются последовательно начиная с 1 в том порядке, в ко- котором они появляются во вводе. Утверждения имеют вид <SentenceXistrue> или *Sentence X isfake*, где 1 ? X <> N. Значение N - 0 означает конец ввода. Вывод: Для каждого документа ваша программа должна вывести одну строку. Если доку- документ состоятелен, нужно вывести максимальное количество истинных предло- предложений. Иначе выведите слово *Inconsistent> («Противоречие»). Пример ввода: 1 Sentence 1 is false. 1 Sentence 1 is true. 5 Sentence 2 is false. Sentence 1 is false. Sentence 3 is true. Sentence 3 is true. Sentence 4 is false. 0 Пример вывода: Inconsistent 1 3 4.4.3. Задача «Will Indiana Jones Get There?» South America Programming Contest, 2002 WU1 IndianaJones Get There? (get.in / get.out / 25 c) Индиана Джонс находится в городе, разрушенном войной. Крыши всех до- домов снесены, стоят только части стен. В земле столько мин, что единствен- единственный безопасный путь — по уцелевшим стенам. Для перемещения между сте- стенами Индиана Джонс пользуется доской от одной стены к другой. Его цель — спасти друга, попавшего в ловушку. Начальные позиции Индианы Джонса и друга — в некоторых секциях стен. Стены расположены в направлениях Юг—Север или Запад—Восток. Вам дана карта того, что осталось от города. Вы должны найти минималь- минимальную длину доски, требуемой Индиане Джонсу, чтобы добраться до друга. Ввод: Ваша программа должна обрабатывать несколько тестов. Каждый тест начинается с целого числа N, указывающего количество стен, остав- оставшихся в городе B^N^ 1000). Каждая из следующих ДОстрок описывает стену.
4.4. Задачи для самостоятельного решения 257 Первая стена — та, на которой находится Индиана Джонс вначале. Вторая сте- стена — та, куда он должен добраться. Каждая стена описывается тремя целыми чис- числами X, Уи L ЧислаХи Копределяют самую южную точку стены (для стен, иду- идущих в направлении Юг—Север) или самую западную точку стены (для стен в на- направлении Запад—Восток). L определяет длину и направление стены: При L > 0 стена идет с запада на восток, а при L < 0 —в направлении Юг-Север с длиной |1|. Концу ввода соответствует N - 0. Вывод: Для каждого теста ваша программа должна вывести одну строку — содержащую ве- вещественное число — длину доски, которую должен нести с собой Индиана Джонс. Длина должна выводиться с точностью два знака после запятой. Последняя циф- цифра должна округляться. Пример ввода: 14 1 1 5 6 8 2 7 2 -2 5 3 3 2 5 2 2 3 2 2 3 -2 4 3 -2 0 7 1 1 8 2 3 6 -2 4 7 2 6 6 1 6 6 -2 3 -10 0 20 -5 1 10 50 50 100 0 Пример вывода: 1.41 1.00 4.4.4. Задача «I Hate SPAM, But Some People Love K» South America Programming Contest, 2002 I Hate SPAM, But Some People Love It (spam.in / spam.out / 25 c) В наши дни, к сожалению, спамерские письма получают все большее рас- распространение. В некоторых из них авторы просят вас переслать письмо всем
258 Глава 4 * Скрытые графы вашим друзьям. Некоторые такие письма желают вам удачи, другие обеща- обещают богатство, третьи просто напоминают вам, как важно сообщить вашим друзьям, что вы дорожите их дружбой. Вот пример спама: От.Алиса Кому: Боб, Мэри, Юлия, Джон Привет, это письмо счастья. Яжелаю тебе статьмиллионером, но всезави- сит от тебя самого. Если ты * пошлешь это письмо 10 или более друзьям, ты станешьмиллионером, * пошлешь это письмо 5 или более друзьям, ты станешь богатым, * пошлешь это письмо меньше чем 5 друзьям, ты станешь бедным. Алиса Получив спам, люди реагируют одним из двух способов: либо удаляют та- такое письмо не читая (если ненавидят спам), либо пересылают такое письмо всем, кого знают (если любят спам). В данной задаче мы предположим, что все люди любят спам, но никто не пересылает одно и то же письмо дважды. Каждое спам-письмо имеет уни- уникальные свойства, зависящие от количества друзей, которым вы отправите его. Например, письмо может сообщать, что вы будете бедным, если пошле- пошлете его 5 друзьям, богатым — если 10, и самым богатым, если 20, и т. д. В данной задаче мы будем рассматривать только спам-письма, подобные описанным. Точнее, спам-письмо будет определять две пороговых величи- величины П и T2 и три атрибутаЛ1, A2 и A3. Человеку присваивается атрибут (один из указанных трех) в зависимости от числа отправленных им писем для соответствующего спам-письма. Если он отправит Гписем, причем T< П,он получает атрибутЛ1, если 71 < 7<72, он получает атрибут Л2, иначе получает атрибут A3. Вам даются имена группы людей и для каждого из них множество друзей, чьи электронные адреса он знает. Вам также дается множество различных спам-писем, и для каждого письма пороговые числа 77, T2 и атрибуты A1, A2, A3, а также персона, первая пославшая это письмо. Вы должны написать программу, которая определит для каждого челове- человека в группе, какие атрибуты он получит в зависимости от пересланных им спам-писем. Вы можете полагать, что первый, кто отсылает спам-письмо, пошлеткак минимум одно письмо, и что человек не шлет писем сам себе. Ввод: Ваша программа должна обрабатывать несколько тестовых случаев. Первая стро- строка теста содержит целое число N, определяющее количество человек в группе B <N< 20). Во вводе люди обозначаются номерами от 1 до N. Каждая из следую- следующих NcTpoK содержат список друзей каждого человека. Список друзей персоны i описывает друзей, адреса которых он знает, и состоит из списка целых чисел Fi A < Fi < N, Fi * i); последнее в списке число равно 0.
4.4. Задачи для самостоятельного решения 259 После списка друзей идет описание спам-писем (не более 100). Каждое описание занимает отдельную строку и содержит целое число Р, указывающее номер чело- человека — источника письма B < Р й N)', два целых числа П и 72 и три атрибутаЛ1, Л2, A3 (каждый атрибут — слово, стоящее не более чем из 20 символов). Список описаний спам-писем заканчивается строкой, содержащей только число 0. Следующие N строк содержат имена — по одному слову в строке (длиной не бо- более 20 символов). Имя в строке i — это имя человека с номером i. Конец списка обозначается строкой с N - 0. Вывод: Для каждого теста ваша программа должна вывести список имен с полученными атрибутами. Выводить нужно в порядке ввода в формате имя : атрибуты (в порядке разосланных ими писем) Пример ввода: 5 2 3 0 1 3 5 4 0 5 0 0 4 1 0 1 2 4 poor rich millionaire 5 3 10 sad normal happy 0 Bob Paul Mary Alice Julia 6 2 0 1 3 0 1 2 4 0 1 2 3 5 0 1 2 3 4 0 1 3 4 0 1 2 4 red green blue 1 2 4 dumb normal smart 6 3 5 ugly bad good 0 Peter Paul Victoria John
260 Глава 4 * Скрытые графы Julia Anne 0 0 Пример вывода: Bob: rich sad Paul: millionaire normal Магу: poor sad Alice: poor sad Julia: rich sad Peter: red dumb ugly Paul: green normal ugly Victoria: green normal bad John: blue smart bad Julia: blue smart bad Anne: red dumb bad 4.5. Решения задач Листинг 4.1. Текст программы к задаче «Тетраэдр» program by99d2tl: type Table - array [1..8.1..4] of byte: const МахМ - 10: {90} Т :Table - ((8.0.6.4). @.7.3.5). F.0.8.2). @.5.1.7). D.0.2.8). @.3.7.1). B.0.4.6). @.1.5.3)): MaxQ - MaxM*MaxM*MaxM: var р.ср : array [0..MaxM*MaxM-l] of integer: Pw : array [0..MaxM*MaxM-l] of byte: g : array [0..MaxM*MaxM-1.1..3] of integer: Q : array [l..MaxQ.1..3] of integer: R : array [1..4] of longint: i.S.D.M.j.a.TS.QEnd.QBegin.V.TV.CV.Last : longint:
4.5. Решения задач 261 procedure InputData: begin assign(input.'input.txt');reset(input): read(S.D.M); for i:-l to M*M do read(p[i-l]): for i:-l to 4 do read(R[i]): close(input): end; procedure OutResult: begin assign(output.'output.txt');rewrite(output): writeln(Cp[D]): close(output); end; procedure InitGraph; begin Pw[O]:-l: G[0.1]:-2; for i:-l to M-1 do { Горизонтальные ребра } for j:-i*i to (i+l)*(i+l)-2 do begin inc(Pw[j]); G[j.Pw[j]]:-j+l; inc(Pw[j+l]); G[j+l.Pw[j+l]]:-j; end; a:-4; TS:-1 {1 Up}; for i:-l to M-2 do { Вертикальные ребра } begin j:-i*i; while j<-(i+l)*(i+l)-l do begin inc(Pw[j]): G[j.Pw[j]]:-j+a; inc(Pw[j+a]): G[j+a,Pw[j+a]]:-j: if S-j then TS:-2 {1 Dn}; inc(j.2): end; inc(a.2); end: for i:-0 to m*m-l do cp[i]:-maxint: end: function min(a.b:longint):longint: begin if a<b then min:-a else min:-b: end: продолжение &
262 Глава 4 * Скрытые графы Листинг 4.1 (продолжение) procedure Put(V.TV.CV:longint): begin inc(QEnd): Q[QEnd.l]:-V: Q[QEnd.2]:-TV: Q[QEnd.3]:-CV: cp[v]:-cv: end: procedure Get(var V.TV.CV:longint): begin V:-Q[QBegin.l]: TV:-Q[QBegin.2]; CV:-Q[QBegin.3]: inc(QBegin): end: procedure PutAl1(V.TV.CV:1ongi nt): var NV. NTV. Dir. Base. NCV : longint: begin for i:-l to Pw[V] do begin NV:-G[V.i]: if NV-V+1 then Dir:-3 {Right} else if NV-V-1 then Dir:=4 {Left} else if NV>V then Dir:-1 {Down} else Dir:-2: {Up} NTV:-T[TV.Dir]: Base:- (NTY+1) div 2: NCV:-CV+SQR(p[NV]-R[Base]): if NCV<cp[NV] then Put(NV.NTV.NCV): end: end: begin InputData: InitGraph: QEnd:-0: QBegin:-l: Put(S.TS.0): while (QBegin<-QEnd) do
4.5. Решения задач 263 begin Get(V.TV.CV): PutAll(V.TV.CV): end; OutResult: end. Листинг 4.2. Текст программы к задаче «Стены» program io02d2t3: const { Количество областей} { Количество городов } { Количество членов клуба } { Количество городов на границе области} { Количество смежных областей для города} МахМ - 200: MaxN - 250: MaxL - 30: МахТ - MaxN: MaxR - 10: GREAT- 255; var аггау [1 аггау [1 array [1 аггау [1 аггау [1 аггау [1 с к t G R kR .MaxL] of byte; .MaxM] of byte: .MaxM.l..MaxT] of byte: .MaxM.l..MaxM] of byte; .MaxN.l..MaxR] of byte; .MaxN] of byte: { Города с членами клуба } { Количество городов на границе области} { Города на границе области} { Матрица инцидентности областей} { Области, смежные с городом} { К-во областей, смежных с городом} M.N.L.i.j.x.NumM : byte; MinS : longint: procedure InputData: begin assign(input.'walls.in'); reset(input): read(M.N.L): for i:-l to L do read(c[i]): for i:-l to М do begin read(k[i]); for j:-l to k[i] do read(t[i.j]); end: close(input): end: procedure InitGraph: var wl.w2 : array [l..MaxM] of byte: продолжение
264 Глава 4 * Скрытые фафы Листинг 4.2 (продолжение) s.p : byte: begin for i:-l to М do for j:-l to М do begin G[i.j]:^REAT; if i-j then continue; for x:-l to М do begin wl[x]:-0: w2[x]:-0: end: for x:-l to k[i] do wl[t[i.x]]:-l; for x:-l tO k[j] do w2[t[j.x]]:-l; s:-0: for x:-l to М do inc(s.wl[x]*w2[x]): if s>-2 then begin G[i.j]:-1: 6[j.1]:-l: end; end; end: function min(a.b:byte):byte: begin if a<b then min:-a else min:-b; end: procedure.Floyd: begin for x:-l to М do for i:-l to М do for j:-l to М do begin if i-j then G[i.j]:-O: if (G[i.x]<>GREAT) and (G[x.j]<>GREAT) then G[1.j]:- min(G[i.j].G[i.x]+G[x.j]): end; end: procedure FindBest: var CurS. ВМ : longint: tc : byte: begin { Строин массив город - список областей} for 1:-1 to М do kR[i]:-0: for i:-l to М do {i - номер области} for j:-l to k[i] do
4.5. Решения задач 265 begin х :- t[i.j]: {x - нонер города} inc(kR[x]): R[x.kR[x]]:-i: end: MinS:-maxlongint; NumM:-0; for i:-l to М do { Цикл по областян} begin CurS:-0: for j:-l to L do { Цикл no членаи клуба} begin tc:-c[j]: { Город с членом клуба} BM:-GREAT; { Лучший для члена клуба} for x:-l to kR[tc] do BM:-min(BM.G[r[tc.x].i]); inc(CurS.BM); end; if CurS<MinS then begin MinS:-CurS: NumM:-i end; end: end; procedure OutResult; begin assign(output.'wa11s.out'); rewrite(output): writeln(MinS): writeln(NumM); close(output); end; begin InputData: InitGraph; Floyd: FindBest; OutResult; end. Листинг 4.3. Текст программы к задаче «Блокада» program spbOlb: const GREAT - 255; var g : array [32..255.32..255] of byte: d : array [32..255] of byte: продолжение &
266 Глава 4 * Скрытые графы Листинг 4.3 (продолжение) pred : аггау [32..255] of byte: diff : аггау [ 1..255] of byte: M.N.1.j.K.A.p : byte: sl.s2 : string: procedure InputDataInitGraph: begin assign(input.'input.txt'): reset(input): readln(M.N); sl:-": for i:-l to N+2 do sl:-sl+' ': for i:-32 to 255 do for j:-32 to 255 do g[i.j]:HiREAT: for i:-l to М do begin readln(s2): s2:-' '+s2+' ': for j:-2 to N+2 do begin g[ord(s2[j]).ord(s2[j-l])]:-l; g[ord(s2[j-l]).ord(s2[j])]:-l; g[ord(sl[j]).ord(s2[j])]:-l: g[ord(s2[j]).ord(sl[j])]:-l: end: sl:-s2: end: s2:-": for i:-l to N+2 do s2:-s2+* ': for j:-2 to N+1 do begin g[ord(s2[j]).ord(s2[j-l])]:-l; g[ord<s2[j-l]).ord(s2[j]>]:-l: g[ord(sl[j]).ordCs2[j])]:-l; g[ord(s2[j]).ord(sl[j])]:-l: end: K:-0: for i:-33 to 255 do if (G[ordCA').1]-l) and (i<>ord('A')) then begin G[ord('A').i]:-GREAT: {Через А идти нельзя} inc(K): {Снежные с А сразу включаем} diff[K]:-i: end: close(input): end:
4.5. Решения задач 267 procedure Dijkstra; const FREE - 0: DONE - 1: var Lab : array [32..255] of byte: Bliz. MinD : byte; begin (Инициализация данных) for i:-33 to 255 do begin Lab[i]:-FREE; d[i]:-G[32.i]: (расстояния от 0-й вершины до остальных) pred[i]:-32: end; d[32]:-0: Lab[32]:-DONE: pred[32]:-0: {y нее нет предка) for j:-33 to 255 do begin (Поиск ближайшей из необработанных) MinD:^REAT: for i:-33 to 255 do if (Lab[i]-FREE) and (d[i]255) and (d[i]>d[bliz]+G[Bliz.i]) then begin d[i]:^[bliz]+G[Bliz.i]: pred[i]:-Bliz: end: end: end: procedure AddNew(j:byte); var i : byte: begin 1:-1: while (i<-K) and (diff[i]<>j) do Inc(i): if i>K then begin inc(K): d1ff[K]:-j: end: end: procedure ZeroG: продолжение &
268 Глава 4 * Скрытые графы Листинг 4.3 {продолжение) begin j:-ord('A'): G[pred[j].j]:-0: j:-pred[j]: while pred[j]<>0 do begin for i:-33 to 255 do if G[i.j]-1 then begin G[i.j]:-O; G[j.i]:-O;end; G[pred[j].j]:-0: G[j.pred[j]]:-0; AddNew(j); j:-pred[j]; end: end; procedure BadStop: begin assign(output.'output.txt'):rewrite(output); writeln(-l): close(output): halt@): end: begin InputDataInitGraph; Dejkstra: for i:-33 to 255 do if (G[i.ordCA')]-l) and (i<>ordCA')) and (d[i]-GREAT) then BadStop: A:-K: for p-1 to A do begin ZeroG: Dejkstra: end; assign(output.'output.txt'):rewrite(output); writeln(K): close(output): end. Листинг 4.4. Текст программы к задаче «Мудрый правитель» program spbOOc; var z. i. j. k. 1. p. q. n. m, best : longint: а : аггау [1..8. 1..8. 1..8. 1..8] of longint:
4.5. Решения задан 269 s : array [1..8. 1..8] of longint: procedure InputData: begin assign(input. 'input.txt'): reset(input): read(N. M); close(input): end: procedure InitGraph: begin for i :- 1 to 8 do for j :- 1 to 8 do for к :- 1 to 8 do for 1 :- 1 to 8 do begin a[i.j][k.l]:-l: if abs(<i-k)*<j-l)) - 2 then a[i.j][k.l] :- 1: if (i-k) and (j-1) then a[1.j][k.l] :- 0; end: end; procedure Floyd: begin for р :- 1 to 8 do for q :- 1 to 8 do for i :- 1 to 8 do for j :- 1 to 8 do for к :- 1 to 8 do for 1 :- 1 to 8 do if (a[i.j][p.q] <> -1) and (a[p.q][k.l] <> -1) and ((a[i.j][k.l] - -1) or (a[i.j][p.q] ¦ a[p.q][k.l] < a[i.j][k.l])) then a[i.j][k.l] :- a[i.j][p.q] + a[p.q][k.l]; end: procedure FindTheBest: begin best :- 0: for i :- 1 to 8 do for j :- 1 to 8 do begin р :-0: for к :- 1 to 8 do продолжение &
270 Глава 4 * Скрытые графы Листинг 4.4 (продолжение) for 1 :- 1 to 8 do if a[i.j][k.l] <- n then р :- р + A shl (n - a[i.j][k.l])): if (р > best) and (р <- m) then best :- р; s[1.J]:-p: end: end; procedure OutResults: begin assign(output. 'output.txt'): rewrite(output): writeln(best): for i :« 1 to 8 do for j :« 1 to 8 do if S[i.j] - best then write(chr(ord('a') + i - 1). j. ' '): close(output): end: begin InputData; InitGraph: Floyd: FindTheBest: OutResults: end. Листинг 4.5. Текст программы к задаче «Ременная передача» program nee4n98b: const MaxN - 100: MaxQ - 10000: Е var G X Q А i W le-6: У.г j.u.> WN Stop array array array array /.N.K.m real: [l..MaxN. [L.MaxN] [l..MaxQ] [l..MaxN] QEnd.QBeg boolean: l..MaxN] of byte: of real: of longint: of real: : longint: procedure InputDataInitGraph: begin assign(input.'input .txf); reset(input):
4.5. Решения задач 271 readln(N.K.m): for i:-l to N do readln(x[i].y[i].r[i]): for i:-l to N do for j:-l to N do G[i.j]:-0: for i:-l to К do begin read(u.v): G[u.v]:-1: G[v.u]:-1: end: for i:-l to N do for j:-i+l to N do if (x[i]-x[j]) and (y[i]-y[j]) then begin G[i.j]:-1: G[j.i]:-1: end: close(input); end; procedure Put(V:longint: W:real): begin inc(QEnd): Q[QEnd]:-V: a[V]:-W: end; procedure Get(var V:longint: var W:real); begin V:-Q[QBeg]: W:-A[V]; inc(QBeg): end; procedure PutAll(V:longint: W:real); begin for i:-l to N do if (G[V.i]<>0) then begin if (x[v]ox[i]) or (y[v]oy[i]) then WN:-W*r[v]/r[i] else WN:-W; if a[1]-l then Put(i.WN) else Stop:- abs(a[i]-WN)>E: end; end: procedure OutResults: продолжение &
272 Глава 4 * Скрытые графы Листинг 4.5 (продолжение) begin assign(output.'output.txt'): rewrite(output); if stop then begin writelnCDO NOT TURN IT 0N!f); halt: end; for i:-l to N do begin a[i]:-trunc(a[i]): if a[1]-l then writeln('IDLE') else writeln(a[i]:0:0); end; close(output); end; begin InputDataInitGraph; QEnd:-0; QBeg:-l; for i:-l tO N do A[i]:-1; Put(m.60000): while (QBeg<-QEnd) and not Stop do begin Get(V.W); PutAll(V.W); end; OutResults; end. ВНИМАНИЕ Среди тестов к этой задаче имеются такие, при которых вычисленные значение количества оборотов достаточно велико; 6 • 1049. Очевидно, для получения полнрго решения требуется модифицировать представленное решение, реализовав «длинное деление и умножение» для вычисления по формуле WN; = W*r[v]/r[i]. Точнее, нужно реализовать длинное умножение и длинное деление на целые числа от 1 до 10, поскольку по условиям задачи радиусы валов — целые числа в таком диапазоне. Предоставля- Предоставляем это в качестве упражения пытливому читателю. Листинг 4.6. Текст программы к задаче «Currency Exchange» program nee4nOle; const MaxN - 100; MaxR - 40: FREE - 0: DONE - 1: var kg : array [l..MaxN] of byte;
4.5. Решения задан 273 G : array [l..MaxN.l..MaxR] of byte: Р : array [l..MaxN.l..MaxR.1..2] of real: color : array [l..MaxN.l..MaxR] of byte; N.M.S.1.f.t : 1nteger: V.RAB.RBA.CAB.CBA.VNew : rea1: procedure ReadAddEdges; beg1n read(f.t.RAB.CAB.RBA.CBA): inc(kG[f]): G[f.kG[f]]:-t: P[f.kG[f].l]:-RAB: P[f.kG[f].2]:<AB: inc(kG[t]): G[t.kG[t]]:-f: P[t.kG[t].l]:-RBA: P[t.kG[t].2]:<BA: color[f.kG[f]]:-FREE: color[t.kG[t]]:-FREE: end; procedure InputDataIn1tGraph: beg1n assign(input.'1nput.txt'): reset(input); readln (N.M.S.V): if V-0 then beg1n ass1gn(output.'output.txt'):rewr1te(output); writeln(*NO'); close(outpyt): halt: end: for 1:-l to M do ReadAddEdges: close(input): end: function Count(F.T:integer: V:real):real: begin RAB:-P[f.t.l]: CAB:-P[f.t.2]: Count:-(V-CAB)*RAB: end: procedureDFS(u:integer; V:real): var j : byte: VCur : real: begin for j:-l to kG[u] do if color[u.j]-FREE then продолжение &
274 Глава 4 • Скрытые графы Листинг 4.6 (продолжение) begin color[u.j]:-DONE; VCur:-Count(u.j.V): if VCur<-0 then exit; if (G[u.j]-S) then begin if VCur>VNew then VNew:-VCur; exit; end; DFS(G[u.j].VCur); color[u.j]:-FREE: end: end: begin InputDataInitGraph; VNew:-V: DFS(S.V); assign(output.'output.txt');rewrite(output): if VNew>V then writelnCYES') else writeln('NO'): close(output); end. Листинг 4.7. Текст программы к задаче «Exchange Rates* program mc99c; const MaxN - 60: MaxR - 60: var kG : array [l..MaxN] of byte: G : array [l..MaxN.l..MaxR] of byte: P : array [l..MaxN.l..MaxR.1..2] of longint: Slist : array [l..MaxN] of string [20]: s.sl.s2 : string: i.k.kl.k2.nl.n2.KV : longint: i Floyded integer: boolean: procedure ProcessName(s:string: var i:longint): begin i:-l: while (i<-KV) and (SList[i]<>s) do inc(i): if i>KV
4.5. Решения задач 275 then begin inc(KV): SList[KV]:-s: end: end: function min(a.b:longint):longint: begin if a<b then min:-a else min:-b; end: procedure Reduce(var nl.n2:longint): begin for j:-2 to min(nl.n2) do while ((nl mod j)-0) and ((n2 mod j)-0) do begin nl:- nl div j: n2:- n2 div j: end: end: procedure AddEdges(kl.k2.nl.n2:longint): begin G[kl.k2]:-1: P[kl.k2.1]:-nl: P[kl.k2.2]:-n2: G[k2.kl]:-1: P[k2.kl.l]:-n2: P[k2.kl.2]:-nl: end: procedure AddGraph: begin delete(s.l.2): k: delete(s.l.k): k: delete(s.l.k+2): k: delete(s.l.k): ProcessName(sl.kl): ProcessName(s2.k2): Reduce(nl.n2): -pos(' \ -pos(' '. -pos(' '. AddEdges(kl.k2.nl.n2): Floyded:-False: end: procedure Floyd: begin for k:-l to KV do for i:-l to KV do for j:-l to KV if (G[i.k]-l) do and (G[k S): S): S): J]- val(copy(s.l.k-l).nl.j) sl:-copy(s.l.k-l): val(copy(s.l.k-l).n2.j) s2:-s: 1) and (i<>j) продолжение
276 Глава 4 * Скрытые графы Листинг 4.7 {продолжение) then begin G[1.j]:-1: nl:-P[1.k.l]*p[kJ.l]: n2:-P[i.k.2]*p[k.j.2]: P[1.j.l]:-nl: P[i.j.2]:-n2: end: Floyded:-true; end: function Exist(kl.k2:longint: var nl.n2:longint):boolean: begin Exist:- G[kl.k2]-1: if (G[kl.k2]-l) then begin nl:-P[kl.k2.1]: n2:-P[kl.k2.2]: Reduce(nl.n2): end: end: procedure Answer: begin delete(s.l.2): k:-pos(' '.S): sl:-copy(s.l.k-l): delete(s.l.k+2): s2:-s; ProcessName(sl.kl): ProcessName(s2.k2): If Not Floyded then Floyd: if Exist(kl.k2.nl.n2) thenwriteln(nl/ '.SList[kl].' - '.n2.1 '.SList[k2]) elsewritelnC? '.SList[kl].' -? '.SList[k2]): end: begin assign(input.'exchange.in'): reset(input): assign(output.'exchange.out'): rewrite(output): KV:-0; for i:-l to MaxN do for j:-l to MaxN do G[i.j]:-0: readln(s): while s<>'.1 do begin if s[l]-'l'then AddGraph else Answer: readln(s):
4.5. Решения задач 277 end: close(input):close(output): end. Листинг 4.8. program ecna01b: const MaxN - 26: Текст программы к MaxN2 - MaxN*MaxN: var s : G : a.b : i.j.N.M Sorted.Cycle : array [l..MaxN2] of array [1..26.1..26] задаче «Sorting It All Out» string[3]: of byte: array [1..26] of byte: integer: boolean: procedure In1tGraph: begin for i:-l to 26 do for j:-l to 26 do G[1.j]:-O: for i:-l to 26 do a[iJ:-0: for i:-l to 26 do b[i]:-0: end: procedure AddEdge(i:integer: var Sorted.Cycle:boolean): var p.k.q : integer: begin j:-ord(s[i.l])-ord('A')+l: k:-ord(s[i.3])-ordCA')+l; if G[j.k]<-0 then inc(a[j]): G[j.k]:-1: for p:-l to N do if G[p.j]-1 then begin if (G[p.k]-0) and (p<>k) then inc(a[p]): 6[p.k]:-l: end: for p:-l to N do if G[k.p]-1 then begin продолжение
278 Глава 4 * Скрытые графы Листинг 4.8 (продолжение) if (G[j.p]-O) and (j<>p) then inc(a[j]): 6[j.p]:-l: end: Cycle:-False; for p:-l to N do Cycle:-Cycle ог (G[p.p]-l): for p:-l to N do b[p]:-0; for p:-l to N do b[a[p]+l]:-l: Sorted:-True; for p:-l to N do Sorted:-Sorted and (b[p]-l): end: function R:string; var b : аггау [1..26] of byte: i.j.k.s.р : byte: Т : string: begin for j:-l to 26 do b[j]:-j: for j:-l to N-1 do begin s:-a[j]: k:-j: for i:-j+l to N do if a[i]>s then begin s:-a[i]: k:-i end: a[k]:-a[j]: a[J]:-s: p:-b[k]: b[k]:-b[j]: b[j]:-p: end: T:-": for 1:-1 to N do T:-T+chr(b[i]+ord('A')-l): R:-T: end: begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'): rewrite(output): readln(N.M); while (N<>0) do begin for i:-l to М do readln(s[i]): InitGraph: for i:-l to М do
4.5. Решения задач 279 begin AddEdge(i.Sorted.Cycle): if Cycle then writeln('Inconsistency found after '.i.' relations.'); if Sorted then writeln('Sorted sequence determined after ',i. 1 relations: '.R.'.'): if Sorted or Cycle then break; end: if not (Sorted or Cycle) then writeln('Sorted sequence cannot be determined.1); readln(N.M); end: close(input): close(output): end. Листинг 4.9. Текст программы к задаче «Проверка веб-страниц» program spb99c: const MaxN - 100; Махб - 2*MaxN: MaxR - 1000: var DocNames : array ReaLDocs : array G : array s.p : string N.i.j.K.m.kl.k2.t [1 Cl [1 ..MaxN*2] ..MaxN*2] ..MaxG.l. integer; of string[32]; of boolean; .MaxG] of byte: procedure UpCaseCompressStr(var s: string): begin for i :- 1 to Length(s) do s[i] :- UpCase(s[i]); p:-s[l]; j:-l; for i:-2 to length(s) do if (p[j]<>' ') or (s[i]<>1 ') then begin p:-p+s[i]: inc(j) end; s:-p; end: procedure AddName(s:string: var m:integer); begin m:-l: продолжение
280 Глава 4 * Скрытые графы Листинг 4.9 (продолжение) while (m<-k) and (s<>DocNames[m]) do inc(m): if m>k then begin inc(k): DocNames[k]:-s; end; end; procedure ReadDatalnitGraph; begin assign(input.'input.txt'): reset(input); readln(N); for i:-l to 2*N do for j:-l to 2*N do G[i.j]:-O: for i:-l to 2*N do RealDocs[i]:-false: k:-0: for t:-l to N do begin readln(s); UpCaseCompressStr(s): AddName(s.kl): RealDocs[kl]:-true: readln(s); {<HTML>} UpCaseCompressStr(s); while (s<>'<END>') do begin j:-posC<A HREF--.s): m:-pos('">'.s): while (j<>0) and (m<>0) and (j<m) do begin p:-copy(s.j+9.m-(j+9)); AddName(p.k2): inc(G[kl.k2]); delete(s.l.m+l): j:-pos('<A HREF-"'.s): m:-pos("'>1 .s); end: readln(s): UpCaseCompressStr(s): end; end: close(input): end: function DeadLinks:integer: begin t:-0:
4.5. Решения задач 281 for j:-l to К do if (not RealDocs[j]) then for i:-l to К do inc(t.G[i.j]): DeadLinks:-t: end: function UnReachDocs:integer: begin for t:-l to к do for i:-l to к do for j:-l to к do if F[i.t]<>0) and (G[t.j]<>0) then G[i.j]:-1: t:-0: for i:-2 to к do if (G[l.i]-0) and RealDocs[i] then inc(t); UnReachDocs:-t; end: begin ReadDataInitGraph: assign(output.'output.txf); rewrite(output): writeln(DeadLinks.' '.UnReachDocs); close(output): end. Листинг 4.10. Текст программы к задаче «Play On Words» program ce99h: var G : аггау [1..26.1..26] of byte; kGin.kGOut : array [1..26] of longint: Letter : array [1..26] of byte: B.E.EvenV.All.i.j.k : byte; t.NT.w.NW : longint: procedure ReadWord(var B.E:byte): var s.c : char: begin read(c): b:-ord(c)-ordCa')+l: while (c<>#13) do begin s:-c: продолжение &
282 Глава 4 • Скрытые графы Листинг 4.10 (продолжение) read(c); end: e:-ord(s)-ord('a')+l: read(c): end; {дочитываем #10} function CorrNumInOut:boolean: var LT, 6T : byte; begin for i:-l to 26 do begin kGIn[i]:-O: kGOut[i]:-0: for j:-l to 26 do begin inc(kGIn [i].G[j.i]): inc(kGOut[i].G[i.j]): end; end: CorrNumInOut:-false: LT:-O: GT:-O; for i:-l to 26 do if (Letter[i]-l) then begin if abs(kGIn[i]-kGOut[i])>l then exit: if kGIn[1]-kGOut[1] - 1 then inc(GT): if kGIn[i]-kGOut[i] —1 then inc(LT): end: CorrNumInOut:- (LT<-1) and (GT<-1): end: function Connected: boolean: begin for k:-l to 26 do for i:-l to 26 do for j:-l to 26 do if (G[i.k]<>O) and (G[k.j]<>O) then G[1.j]:-1: All:-O; for 1:-1 to 26 do if Letter[i]-1 then 1nc<All): Connected:-true:
4.5. Решения задач 283 for i:-l to 26 do begin if (Letter[i]-0) then continue; k:-0: for j:-l to 26 do if (Letter[j]-l) and (i<>j) and (G[i.j]<>O) then inc(k): if k-All-1 then exit; end; Connected:-false: end; begin assign (input.'input.txt'); reset(input); assign (output.'output.txt'); rewrite(output); readln(NT); for t:-l to NT do begin readln(NW); for i:-l to 26 do for j:-l to 26 do G[i.j]:'-0; for i:-l to 26 do Letter[i]:-0; for w:-l to NW do begin ReadWord(B.E); Letter[b]:-1: Letter[e]:-1: if b<>e then inc(G[b.e]); end: if CorrNumInOut and Connected then writeln('Ordering is possible.1) else writelnCThe door cannot be opened.'): end; close(input): close(output): end. Листинг 4.11. Текст программы к задаче «Падение» program ceoi00t4: const MaxN - 1000: M2 - 2*MaxN+2: MaxR - 2: var xl.x2.H.b : array [l..MaxN+l] of integer: pred : array [0..M2] of integer; d : array [0..M2] of longint: продолжение &
284 Глава 4 * Скрытые графы Листинг4.11 (продолжение) kG : array [0..M2] of integer; G : array [0..M2.1..MaxR] of integer: P : array [0..M2.1..MaxR] of longint: N.X.Y.MAX.i.j : integer: procedure InputData: begin assign(input.'fall.in'): reset(input): readln(N.X.Y.MAX): for 1:-1 to N do readln(xl[i].x2[i].H[i]): close(input): end: procedure SortPlatforms: var s.k : integer: begin for i:-l to N do b[1]:-1: for j:-l to N-1 do begin s:-H[j]: k:-j: for i:-j+l to N do if H[i]>s then begin s:-H[i]: k:-i end: H[k]:-H[j]: H[j]:-s: s:-b[k]; b[k] :-b[j]: b[j] :-s: s:-xl[k]: xl[k]:-xl[j]: xl[j]:-s; s:-x2[k]: x2[k]:-x2[j]: x2[j]:-s: end: H[N*1]:-O: xl[N+l]:-20000: x2[N+l]:-20000; end: procedure Buildl(i.X.Y:integer); begin j:-((i+l) div 2)+l: while ((X<xl[j]) or (X>x2[j])) and (Y-H[j]<^1AX) do inc(j): if Y-H[j]>MAX then exit; G[i.kG[i]+l]:-2*j-l: G[i.kG[i]+2]:-2*j : P[i.kG[i]+l]:-(Y-H[j]) + (X-xl[j]); P[i.kG[i]+2]:-(Y-H[j]) + (x2[j]-X); if j-N+1 then
4.5. Решения задач 285 begin P[i.kG[i]+l]:-(Y-H[j]): P[i.kG[i]+2]:-(Y-H[j]): end: inc (kG[i].2): end; procedure Bui1d2(i:integer); begin BuildlB*i-l.xl[i].H[i]): BuildlB*i.x2[i].H[i]): end; procedure InitGraph: begin SortPlatforms; for i:-0 to 2*N+1 do kG[i]:-0; Buildl(O.X.Y): for i:-l to N do Build2(i): end; procedure Dejkstra: const FREE - 0: DONE - 1: var Lab : array [0..M2] of integer: MinD. Bliz : longint: begin {Инициализация данных} for i:-0 to 2*N+1 do begin Lab[i]:-FREE; pred[i]:-0: d[i]:^naxlongint: end: d[0]:-0: Lab[0]:-DONE: pred[0]:--l: {y нее нет предка} for i:-l to kG[0] do d[G[0.1]]:-P[0.1]: {расстояния от 0-й вершины до остальных} for j:-0 to 2*N+1 do begin {Поиск ближайшей из необработанных вершин} MinD:^naxlongint: продолжение &
286 Глава 4 « Скрытые графы Листинг 4.11 (продолжение) for i:-0 to 2*N+1 do if (Lab[i]-FREE) and (d[i]<MinD) then begin MinD:-d[i]: Bliz:-i end: Lab[Bliz]:-Done; { Пересчет кратчайших расстояний через ближайшую вершину} for i:-l to kG[Bliz] do if Lab[6[Bliz.1U-FREE then if d[G[Bliz.i]]>d[bliz]+P[Bliz.i] then begin d[G[Bliz.i]]:-d[bliz]+P[Bliz.i]: pred[G[Bliz.i]]:-Bliz: end: end; end; procedure OutResults: var res : array [l..MaxN] of integer: t.kp. NumP,Dir.Time.Last.Curr : integer: begin assign(output.'fall .ouf); rewrite(output): writeln(d[2*N+l]); t:-2*N+l: kp:-0: while (pred[t]<>0) do begin inc(kp): res[kp]:-pred[t]: t:-pred[t]: end: Last:-X: for i:-kp downto 1 do begin NumP:-(Res[i]+l) div 2: Dir :-l -(Res[i] mod 2): if Dir-0 then Curr:-xl[NumP] else Curr:-x2[NumP]: Time:-D[res[i]]-abs(Curr-Last): writeln(b[NumP].' '.T1me.' '.Dir): Last:-Curr": end:
4.5. Решения задач 287 close(output): end: begin InputData: InitGraph; Dejkstra: OutResults: end. Листинг 4.12. Текст программы к задаче «The Doors» program mc96b: const MaxN - 18: M2 - MaxN*4+2: MaxR - 100: var kG G P d pred x У array [0..M2] of integer: array [0..M2.1..MaxR] of integer: array [0..M2.1..MaxR] of real: array [0..M2] of real: array [0..M2] of integer: array [1..MaxN] of real: array [l..MaxN.1..4] of real: integer: N.i.j.k.m ixl.ix2.iyl.iy2 : integer: xl.yl.x2.y2.A.B.C.CurY.Dij : real: procedure InputData: begin for i:- 1 to N do begin read(x[i]): for j:-l to 4 do read (y[i.j]): end: end: function Dist(xl.yl.x2.y2:real):real: begin Dist:-sqrt(sqr(xl-x2)+sqr(yl-y2)): end: продолжение
288 Глава 4 * Скрытые графы Листинг 4.12 (продолжение) procedure Add(i.j:integer:Di j:real): begin inc(kG[i]): G[i.kG[i]]:-j: P[i.kG[i]]:-Dij: end: function Seen(i.j:integer: var Dij:real):boolean: var k.m : integer; begin Seen:-false; if i-0 then begin ixl:-0; xl:-0: yl:-5.0: end else begin 1xl:- ((i-l) div 4)+l; iyl:-(i mod 4): if iyl-0 then iyl:-4: xl:-x[1xl]: yl:-y[ixl.iyl]: end: if j-4*N+l then begin ix2:-N+l: x2:-10.0: y2:-5.0; end else begin ix2:- ((j-l) div 4)+l: iy2:-(j mod 4): if iy2-0 then iy2:-4; x2:-x[ix2]: y2:-y[ix2.iy2]; end; A:-y2-yl: B:-xl-x2; C:-yl*(x2-xl)-xl*(y2-yl): for k:-ixl+l to ix2-l do begin CurY:-(A*x[k]+C)/B: if (CurY<y[k.l]) or (CurY>y[k.4]) or ((CurY>y[k.2]) and (CurY<y[k.3])) then exit; end: Dij:-Dist(xl.yl.x2.y2): Seen:-true: end: procedure InitGraph: var i.j : integer: begin for i:-0 tO (N-l)*4 do kG[i]:-4; for i:-4*(N-l)+l to 4*N do kG[i]:-l; for i:-l to N-1 do
4.5. Решения задач 289 for j:-l to 4 do for k:-l to 4 do begin G[4*(i-l)+j.k]:-4*i + к: P[4*(i-l)+j.k]:-Dist(x[i].y[i.j].x[i+l].y[i+l.k]); end: for i:-l to 4 do begin G[O.i]:-i: { Начальная вершина } P[0.i]:-Dist@.0.5.0.x[l].y[l.i]): G[4*(N-l)+i.l]:-4*N+l: { Конечная вершина } P[4*(N-l)+i.l]:-Dist(x[N].y[N.i].10.0.5.0): end: for j:-5 to 4*N+1 do if Seen(O.j.Dij) then Add(O.j.Dij): if N>-2 then for j:-l to 4*(N-1) do if Seen(j.4*N+l.Dij) then Add(j.4*N+l.Dij): if N>-3 then for i:-l to N-2 do for j:-i+2 to N do for k:-l to 4 do for m:-l to 4 do if Seen((i-l)*4+k.(j-l)*4+m.Dij) then Add((i-l)*4+k.(j-l)*4+m.Dij): end: procedure Dijkstra: const FREE - 0: DONE - 1: var Lab : array [0..M2] of integer: Bliz : longint: MinD : real: begin {Инициализация данных} for i:-0 to 4*N+1 do begin Lab[i]:-FREE: pred[i]:-0: d[i]:^naxlongint: end: продолжение &
290 Глава 4 * Скрытые графы Листинг 4.12 (продолжение) d[0]:-0: Lab[0]:-DONE: pred[O]:--l: { у нее нет предка} for i:-l to kG[O] do d[G[O.1]]:-P[O.1]: {расстояния от 0-й вершины до остальных} for j:-0 to 4*N+1 do begin { Поиск ближайшей вершины из необработанных} MinD:^naxlongint: for i:-0 to 4*N+1 do if (Lab[i]-FREE) and (d[i]<MinD) then begin MinD:-d[i]: Bliz:-i end: Lab[Bliz]:-Done: { Пересчет кратчайших расстояний через ближайшую вершину} for i:-l to kG[Bliz] do if Lab[G[Bliz.i]]-FREE then 1f d[G[Bliz.i]]>d[bliz]+P[Bliz.i] then begin d[G[Bliz.i]]:^[bliz]+P[Bliz.i]: pred[G[Bliz.i]]:-Bliz: end: end: end: begin assign(input.'doors.in'):reset(input): assign(output.'doors.out'):rewrite(output): read(N): while No-1 do begin 1f N-0 then writelnA0.0:0:2) else begin InputData: InitGraph: Dejkstra: writeln(d[4*N+l]:O:2): end: read(N): end: close(input): close(output): end.
4.5. Решения задач 291 Листинг 4.13. Текст программы к задаче «Борозды» program spb99tl: const MaxN - 100: MaxV - 2*MaxN: MaxR - 100: var x.y. List.kG : array [l..MaxV] of integer: G : array [l..MaxV.l..MaxR] of byte: x0.y0.N.i.j.k.vl.v2. EvenV : integer: Q : array [l..MaxV] of byte: Free : array [l..MaxV] of boolean: BegQ. EndQ : integer: procedure InputData: begin assign(input.'input.txt'): reset(input): readln(N): for i:-l to N do readln(x[2*i-l].y[2*i-l].x[2*i].y[2*i]): close(input): end; procedure AddNode(i.j:integer): begin List[i]:-j; kG[i]:-0: end: procedure AddEdge(i.j:integer): begin inc(kG[i]): G[i.kG[i]]:-j: inc(kG[j]): G[j.kG[j]]:-i: end: function Found (t:integer: var v:integer):boolean: begin v:-l: while (v<-K) and ((x[List[v]]ox[t]) or (y[List[v]]oy[t])) do inc(v): Found :• v<-k: end: procedure InitGraph: продолжение &
292 Глава 4 * Скрытые графы Листинг 4.13 (продолжение) begin k:-2: AddNode(l.l): AddNodeB.2): AddEdgeA.2);< for i:-2 to N do begin if not FoundB*i-l.vl) then begin inc(k): AddNode(k.2*i-l): end; if not FoundB*i.v2) then begin inc(k): AddNode(k.2*i): end: AddEdge(vl.v2) end; end: procedure Count(var EvenV.xO.yO:integer); begin EvenV:-0: xO:-x[l]:yO:-y[l]; for i:-l to k do if Odd(kG[i]) then begin xO:-x[List[i]]: yO:-y[List[i]] end else inc(EvenV): end: procedure Put(i:integer): begin inc(EndQ): Q[EndQ]:-i: Free[i]:-false: end: procedure Get(var i:integer): begin i:-Q[BegQ]: inc(BegQ): end: function Connected:boolean: begin for i:-l to К do Free[i]:-true: BegQ:-l: EndQ:-0: Put(l): while (BegQ<-EndQ) do begin Get(i): for j:-l to kG[i] do if Free[G[i.j]] then Put(G[i.j]): end:
4.5. Решения задач 293 Connected:-false; for i:-l to N do if Free[i] then exit; Connected:-true; end; begin InputData: InitGraph: Count(EvenV.xO.yO); assign(output.'output.txt'): rewrite(output); if Connected and ((EvenV-K) or (EvenV-K-2)) then begin writelnCYES'): writeln(xO.' '.y0) end else writeln('NO'); close(output); end. Листинг 4.14. Текст программы к задаче «N-Credible Mazes» program gnyOOb: const MaxN - 1000: MaxV - 2*MaxN; MaxR - 10: var List.kG : array [l..MaxV] of longint: G : array [l..MaxV.l..MaxR] of byte: N.i.j.k.vl.v2.VFrom.Vto.t : longint: Q : array [l..MaxV] of longint: Free : array [l..MaxV] of boolean; BegQ. EndQ : longint: procedure AddNode(i.j:1ongint); begin List[i]:-j; kG[i]:-0: inc(K); end; procedure AddEdge(i.j:longint): begin inc(kG[i]): G[i.kG[i]]:-j: inc(kG[j]): G[j.kG[j]]:-i; end: продолжение &
294 Глава 4 * Скрытые графы Листинг 4.14 (продолжение) function Found (t:longint: var v:longint):boo1ean: begin v:-l: while (v<-K) and (List[v]<>t) do inc(v): Found :- v<-k; end: procedure Take(var VFrom:longint): var k.s.i.j:longint: begin k:-l: s:-0: Vfrom:-1: for i:-l to N do begin read(j); if j—1 then ex1t: s:-s+k*j: k:-k*10 end: VFrom:-s: end: procedure InputDataInitGraph: begin Take(VFrom): Take(Vto): K:-0: AddNode(l.Vfrom): AddNodeB.Vto): Take(i): while i<>-l do begin Take(j): if not Found(i.vl) then AddNode(vl.i): if not Found(j.v2) then AddNode(v2.j): AddEdge(vl.v2): Take(i) end: end: procedure Put(i:longint): begin inc(EndQ): Q[EndQ]:-1:
4.5. Решения задач 295 Free[i]:-false: end; procedure Get(var i:longint): beg1n 1:-Q[BegQ]: 1nc(BegQ): end: funct1on PathExists:boolean: begin for 1:-1 to K do Free[i]:-true: BegQ:-l: EndQ:-0: Put(l): PathExists:-true: while (BegQ<-EndQ) do begin Get(i): for j:-l to kG[i] do 1fG[1.j]-2 then exit else if (Free[G[i.j]]) then Put(G[i.j]): end: PathExists:-false: end; begin assign(input.'input.txt'):reset(input); assign(output.'output.txt'):rewrite(output): readln(N); t:-0: while(N<>0) do begin inc(t); InputDatalnitGraph: if PathExists then writeln('Maze #'.t.' can be travelled') else writelnCMaze #'.t.' cannot be travelled'): readln(N): end: close(input); close(output): end.
Глава 5. Стратегические игры Общий принцип решения таких задач заключается в следующем. Вводится мно- множество состояний игры. Естественно, помечаются одно или несколько начальных выигрышных состояний. Далее работает цикл типа «Пока добавилось хоть од- одно выигрышное состояние». Просматриваются неопределенные еще состояния и предпринимаются попытки превратить их в выигрышные — разумеется, на ос- основании правил игры. 5.1. Задача «Алиса и Боб» NEERC, Западный четвертьфинал, 1998 Алиса и Боб (D.IN / D.OUT) Дан ориентированный граф G ж (V, ?), вершины которого являются пози- позициями в следующей игре. В игре участвуют два игрока — Алиса и Боб. Они двигают фишку из позиции в позицию по дугам графа. Множество позиций V разделено на два подмножества А и В, A *B - 0, А + В - V. В позициях А ход делает Алиса, а в позициях В — Боб. Если игра находится в позиции v из множества V, владелец этой позиции выбирает выходя- выходящую из Удугу (v, w) из Е и двигает фишку из v в w. Игра начинается в некоторой позиции. Алиса выигрывает, если фишка оказалась в позиции t из V. Если за любое количество ходов фишка не попадает в эту позицию, то выигрывает Боб. Требуется определить, какой игрок выигрывает, в зависимости от старто- стартовой позиции при оптимальной стратегии обеих сторон. Считаем, что V * {1,..., п) A < п < 1000). Ввод: Первая строка: n m t Здесь п — число вершин, т — число дуг, t — заключительная позиция.
5.1. Задача «Алиса и Боб» 297 Со второй строки записаны: ? Ы Ь2 К bn vl w\ v2 w2 ... vm wm\ ? Ы - 1, если i принадлежит Л; Q Ы - 0, если i не принадлежит А\ ? vi wi — список дуг графа. Здесь vi — начальная вершина, wi — конечная вершина соответствующей дуги A ? т ? 10 000). Все параметры — целые числа, разделенные пробелами. Пример ввода: 7 12 7 0 1 0 0 0 1 0 1 2 1 4 1 3 2 4 2 5 3 1 3 6 3 7 4 3 4 6 5 6 6 7 Вывод: 8 текстовом файле О.ОЦТэаписатъ п целых чиселдг1 x2... хпу гдедя - 1, если позиция i выигрышна для Алисы; иначе — xi - 0. В примере результат представляется строкой: 0 1 0 0 1 1 1 Описание решения: Идея решения коротко может быть сформулирована следующим образом: все позиции в принципе разбиваются на два подмножества — победные для Алисы (назовем их выигрышными) и те, в которых выигрывает не Алиса, а Боб (назовем их проигрышными). Из всех выигрышных позиций ход игры при любых ходах Боба и правильных хо- ходах Алисы приводит к позиции t. Из всех проигрышных позиций при любых хо- ходах Алисы и правильных ходах Боба ход игры НЕПРИВОДИТъ позицию t. Оче- Очевидно, что заведомо выигрышными являются те позиции, в которых ход Алисы и из которых есть НЕПОСРЕДСТВЕННАЯдугь в состояние t. Отталкиваясь от тех позиций, которые являются заведомо выигрышными, итеративно строим ВСЕ остальные выигрышные позиции следующим образом: если в текущей позиции ход Алисы и есть хоть одна дуга в любую из позиций, уже объявленных выигрыш- выигрышными, то и текущая позиция объявляется выигрышной. Аналогично если в теку- текущей позиции ход Боба и ВСЕдуги ведут в позиции, возможно, различные, но ВСЕ уже объявленные выигрышными, то и текущая позиция объявляется выигрыш- выигрышной. Процесс пересмотра «невыигрышных» позиций продолжается до тех пор, пока добавилась хоть одна выигрышная позиция. Этот алгоритм более формально может быть записан следующим образом: Помечаем все состояния как проигрышные Помечаем начальные выигрышные позиции (ход А есть дуга в конечную позицию t)
298 Глава 5 * Стратегические игры Пока изменилось на выигрышное состояние хоть одной позиции Цикл no всем невыигрышным позициям если ход А и существует хоть одна дуга в выигрышную позицию ИЛИ ход Б и ВСЕ дуги ведут в выигрышную позицию, то меняем состояние этой позиции на «выигрышная» Рассмотрим одну из возможных реализаций решения данной задачи. По услови- условиям задачи нам дан ориентированный граф из N вершин и для каждой из этих вер- вершин определяется, кто должен ходить из этой вершины, Алиса или Боб (если B[i] * 1, то Алиса, если B[i] - 0, то Боб). Вводим также массив 5, который будет обозначать состояние позиции (если 5[i] - 1, то выигрывает Алиса, иначе — Боб). Главная программа решения данной задачи может выглядеть следующим об- образом: begin InputData; Changed:-true; while Changed do begin Changed:-false: for i:-l to N do if (S[i]-0) and <((B[i]-l) and ExistArc(i)) ог ((B[i]-0) and AllArcs(i))) then begin S[i]:-1; Changed:-true: end: end: OutputData: end. В главной программе вызываются следующие процедуры и функции: ? InputData — для ввода и инициализации исходных данных; ? OutputData — для вывода ответа; ? ExistArc(i)- для выяснения того, существует ли дуга из текущей вершины i в выигрышную позицию; ? AllArcs{i) — для выяснения ВСЕ ли дуги из текущей вершины i ведут в выиг- выигрышные позиции. Рассмотрим их подробнее. procedure InputData; begin assign (input.'d.in'); reset(input): read (N.M.T):
5.1. Задача «Алиса и Боб» 299 for 1:-1 to N do read (B[i]); for i:-l to М do begin read (from.tov); inc(ka[from]); a[from.ka[from]]:-tov: end: close(input); for i:-l to N do S[i]:-0: S[T]:-l: end: Как видно из текста, исходный ориентированный граф представляется набором дуг из вершины, то есть ? ka[i] — содержит информацию о том, сколько дуг выходит из вершины i; ? a[i,j] — (ДЛЯ7 от 1 до ka[i]) содержит номер вершины, в которую ведет из вер- вершины i дуга с номером/ Вершина Гпомечается как выигрышная. Ниже приведена процедура OutputData: procedure OutputData: begin assign (output.'d.ouf); rewrite(output): write (S[1]): for i:-2 to N do writeC '.S[i]): close(output): end: Состояния выводятся в соответствии с форматом вывода в одну строку (без про- пробела за последним состоянием). Далее приведена функция ExistArc: function ExistArc(i:integer):boolean: var j : integer: begin ExistArc:-false: for j:-l to ka[i] do if S[a[i.j]>l then begin ExistArc:-true; exit: end: end: Вначале переменной ExistArc присваивается значение fahe. Затем просматрива- просматриваются все дуги из вершины i. Если хоть одна из них ведет в вершину, которая уже объявлена выигрышной, то переменная ExistArc получает значение true и работа функции ?ш?4гсзавершается с возвращением значения true в качестве результата. Если же ни одна из дуг не ведет в выигрышную вершину, то значение переменной
300 Глава 5 * Стратегические игры ExistArc так и останется/д&е, а функция ExistArc завершит свою работу с возвра- возвращением значения/д&е в качестве результата. Теперь рассмотрим функцию AllArcs: function AllArcs(i:integeD:boolean; var j : integer; begin AllArcs:-true; for j:-l to ka[i] do if S[a[1.j]]-0 then begin AllArcs:-false; exit; end; end. Вначале переменной Alb\rcs присваивается значение true. Затем просматриваются все дуги из вершины i. Если хоть одна из них ведет в проигрышную вершину, то переменная AllArcs получает значение fahe, а функция AllArcs завершает свою работу с возвращением/д&е в качестве результата. Если же ни одна из дуг из вер- вершины i не ведет в проигрышную вершину, то переменная AllArcs сохраняет свое первоначальное значение true, и функция AllArcs завершает свою работу, возвра- возвращая значение true в качестве результата. Полный текст решения задачи «Алиса и Боб» приведен в конце главы. 5.2. Задача «Ладья и конь» Гомельская областная олимпиада по информатике, 2000 Ладья и конь (input.txt / output.txt) Игра ведется на шахматной доске размером NxM (ЛГ— количество гори- горизонталей, М — количество вертикалей). В игреучаствуютладья и конь. Пер- Первой ходит ладья. Ладья выигрывает, если ей удается побить коня. Ограничения: 2 < N9 M < 8, конь ставится в такую начальную позицию, из которой он может сделать по крайней мере один ход (например при доске 3 х 3 конь не может ока- оказаться на ноле 62, так как любой ход из этой позиции выводит за пределы дос- доски). Ввод: Первая строка: N и М. Вторая строка: позиции ладьи и коня (например, Ы c2 означает, что ладья стоит на второй вертикали и первой горизонтали, конь стоит на третьей вертикали и вто- второй горизонтали. Вертикали обозначаются латинскими буквами от а до А, гори- горизонтали — цифрами от 1 до 8.).
5.2. Задача «Ладья и конь» 301 Вывод: Минимальное число ходов, нужное ладье, чтобы сбить коня, если это возможно, иначе должен быть напечатан 0. Пример ввода: 3 3 Ы c2 Пример вывода: 3 Пояснения к примеру: Конь ловится за три хода: 1. ЛЫ-ЬЗ Kc2-al A.... Kc2-a3 2. ЛЬЗ:аЗ). 2. ЛЬЗ-Ь2 Kal-b3 B.... Kal-c2 3. ЛЬ2:с2). 3. ЛЬ2:ЬЗ. Общая идея решения задачи такова: строим все состояния, в которых ладья выиг- выигрывает за один ход. Это состояния, в которых ладья и конь находятся на одной горизонтали или на одной вертикали. Затем по ним строим все состояния, в которых ладья выигрывает в два хода. И так далее. По состояниям, в которых ладья выигрывает за Number ходов, строим со- состояния, в которых ладья выигрывает за Number + 1 ход. Если новые победные состояния построить не удается, то процесс завершается. Правило построения новых победных состояний по старым таково: пусть ладья стоит в позиции (ii,jt)t а конь стоит в позиции (ik,jk) и в такой позиция ладья сбивает коня не более, чем за ЫитЬегходов при наилучшей игре коня. Для построения ВСЕХ позиций, порождаемых данной, и в которых ладья сбивает коня не более чем за Number + 1 ход, строим все возможные поля, откуда сюда мог попасть конь за один ход. И для каждой такой позиции коня удостоверяемся, что ВСЕ ходы коня из этой позиции вели в позицию, в которой победа достигается не более чем за ШтЬегходов. Если такая позиция коня (x, у) найдена, то искомыми позициями (победными за не более чем Number + 1 ход) будут ВСЕ позиции вида: (MovX,MovY,x,y), где MovX и МоаУтакие, что ладья переходит из них в исходную позицию (il,jt) ровно за 1 ход, то есть, находятся с позицией (il,jt) на одной горизонтали или на одной вертикали. Далее описывается одна из возможных реализаций предложенного алгоритма. Тело главной программы, решающей задачу, выглядит следующим образом: begin InputData: Changed:-true: Number:-O; while Changed do
302 Глава 5 * Стратегические игры begin Changed:-false; Inc(NumbeD; for il:-l to М do {По всем состояниям} for jl:-l to N do for ik--l to М do for jk:-l to N do if S[il.jl.ik.jk]-Number then PutAllNewWinStates(il.jl.ik.jk): end; OutputData: end. Рассмотрим реализацию подробнее: procedure InputData; var с : char; begin assign (input.'input.txt'): reset(input): readln (N.M): read(c): li:-Ord(c)-OrdCa')+l: read(lj): read(c): read(c): ki:-Ord(c)-Ord('a')+l; read(kj): close(input): for il:-l to М do {Bce состояния проигрышны} for jl:-l to N do for ik:-l to М do for jk:-l to N do S[il.jl.ik.jk] :-0: for il:-l to М do {Выигрышные состояния за 1 ход} for jl.-l to N do {если ход ладьи} for ik:-l to М do {и конь на одной вертикали} for jk:-l to N do {или на одной горизонтали} if (dl-ik) or (jl-jk)) and {c ладьей.} not ((il-ik) and (jl-jk)) {но не в одной позиции} then S[il.jl.ik.jk] :-l; end: В этой процедуре выполняются следующие действия. 1. Вводим исходные данные, преобразовываем их числовому виду (fi, lj, ki, kj); 2. Строим четырехмерный массив состояний: 5[ 1 ..MaxM, 1 ..MaxN,l..MaxM, 1 .MaxN] (первыедве координаты — позиция ладьи, вторыедве координаты — позиция ко- коня). S[il,jl, ikJk] — минимальное количество ходов для победы ладьи над конем из позиции ладья (ilJF), конь (ik,jk); 0 означает, что победа ладьи недостижима. 3. Все состояния обнуляем. Заносим 1 в состояния, в которых ладья непосред- непосредственно может побить коня: ладья и конь стоят на одной горизонтали или на одной вертикали.
5.2. Задача «Ладья и конь» 303 Changed:-true: Number:-O: while Changed do begin Changed:-false: Inc(Number); Устанавливаем признак «состояния изменялись» Количество ходов до победы ладьи - Number - 0 Пока состояния изменялись Сбрасываем признак «состояния изменялись» Инкрементируем Number - количество ходов до победы ладьи for il:-l to М do {По всем состояниям} for jl:-l to N do for ik:-l to М do for jk:-l to N do if S[il.jl.ik.jk]-Number then PutAllNewWinStates(il.jl.ik.jk): 4. Повсемсостояниям: если текущее состояние — победное за NumberxojxoB, то с помощью процедуры PutAllNewWinStates(il,jl, ik,jk) строим все состояния, победные за Number + 1 ход. Если нашли такое состояние, то устанавливаем признак Changed. end: OutputData: procedure OutputData; begin assign (output.'output.txt'): rewrite(output): writeln(S[li.lj.ki.kj]): close(output): end: После завершения цикла While выводим результат S[li, lj, ki, kj]. То есть, по пост- построению, это минимальное количество ходов, требуемое ладье на позиции (li, lj) поймать коня начинающего на позиции (ki, kj). Если ладья не может поймать коня, то ответ — 0. Рассмотрим более подробно процедуру PutAllNewWinStates(il,jl, ik,jk): procedure PutAllNewWinStates(il.jl.ik.jk:integer): begin for i:-l to 8 do
304 Глава 5 * Стратегические игры begin CurrentX :- ik+steps[i.l] : CurrentY :-jk+steps[i.2] : if (CurrentX>O) and (CurrentX<-M) and (CurrentY>O) and (CurrentY<-N) then PutIfAll(CurrentX.CurrentY): end: end; Для всех возможных позиций (CurrentX, CurrentY), откуда конь мог попасть в по- позицию (ik,jk), вызываем процедуру PutIfAU(CurrentX, CurrentY) для нахождения состояний, которые победны за Number + 1 ход. Рассмотрим подробней процедуру PutIfAU(CurrerUX,CurrentY): procedure PutIfAll(x.y:integer): var All. Was : boolean: begin j:-l: Was:-false: All:-true: while All and (j<-8) do begin CurrentX :- x+steps[j.l] : CurrentY :- y+steps[j.2] : if (CurrentX>O) and (CurrentX<-M) and (CurrentY>O) and (CurrentY<-N) then begin if (S[il.jl.CurrentX.CurrentY]<-Number) and (S[il.jl.CurrentX.CurrentY]<>O) and not ((il-CurrentX)and(jl-CurrentY)) then Was :- true else All :- false; end: inc(j) end: if Was and All then begin for ilt:-l to M do if (S[ilt.jl.x.y]-O) and (ilt<>il) then begin S[ilt.jl.x.y]:-Number+1: Changed:-true: end; for jlt:-l to N do if (S[il.jlt.x.y]-O) and (jlt<>jl) then begin
5.3. Задача «Нечестная игра» 305 S[il .jH.x.y]:=Number+l. Changed -=true. с-гю. end: end: Для всех клеток коня, из которых можио было попасть в клетку (x, у), удостове- удостоверяемся, что был хоть один реальный ход и ВС?ходы вели в состояния, в которых победа одерживалась не более чем за N ходов. Если :>To так, ro все сос гоянпя, которые возникают из текущего одним ходом коня в (л*, у) и одним ходом ладьи (по горизонтали или вертикали), объявляются по- победными за Number + 1 ход. Полный текст решения задачи приводится в конце главы. 5.3. Задача «Нечестная игра» Командный чемпионат школьников Санкт-Петербурга по про/ралшировапию, 2000 Нечестная игра (input.txt / output.txt) 11етя придумал новую игру. На стол кладется кучка изМспичек, затем Петя с Вапей по очереди берут спички из кучки. Первым берет Петя, ему разре- разрешается взять от 1 до К спичек. Затем игрок может взять любое количество спичек, не более чем на 1 превышающее то количество, которое взял игрок перед ним (можно взять меньше или столько же, но обязательно хотя бы одну). Например, если N =8 10, К = 5, то на первом ходу Петя может взять 1, 2,3, Л или 5 спичек; если Петя возьмет 3, то на следующем ходу Ваня может взять 1. 2,3 или 4; и если Ваня возьмет 1, то Петя затем может взять 1 или 2, и т. д. Проигрывает тот, кто возьмет последнюю спичку. Теперь Петя хочет рассчитать, какое количество спичек он должен взять на первом ходу, чтобы выиграть при любой игре Вани. Помогите ему. Ввод: В первой строке входного файла находятся числа N и Ку разделенные пробелом. (l<K<iV<200). Вывод: Выведите в выходной файл все такие X, что, взяв на первом ходуХспичек, Петя выиграег. Если таких не существует, выведите в выходной файл единственное число - 0 . Числа следует разделять пробелами. Пример ввода 53 42 87 Пример вывода 1 0 27
306 Глава 5 * Стратегические игры Это задача на рекуррентное заполнение двумерной таблицы состояний стратеги- стратегической игры. Вводим состояния игры G[iJ]: ? 1, если всего спичек t и первый игрок берету спичек и выигрывает; а 0 — в противном случае (первый берету спичек и проигрывает). Понятно, что G[1, j] e 0 для всеху, то есть, если спичка одна, то всегда проигрыва- проигрывает первый игрок. Для всех i от 2 до N Для всех j от 1 до N (количество взятых спичек иожет расти от игрока к игроку !!!) j-ходы первого игрока G[i.j]:-1 - первый игрок выигрывает, если возьмет j спичек Для всех р от 1 до j+1 (p - ходы второго игрока) если G[i-j.p>l если при каком то ответе второго он возьмет р спичек i-j - осталось спичек, после того как первый взял j спичек G[i-j.p]-l - игрок выигрывает, если его ход. Осталось i-j спичек, он берет Р спичек то G[i.j]-0 первый игрок проиграл, раз второй выиграл хоть в одном варианте. Затем вывод результата - номера j. для которых G[N.J]-1 если такого номера ни одного нет. то вывести 0 m:-0: for j:-l to К do if G[N.j]=l then begin write(j.' '); inc(m): end: if m*0 then writeln(O); Для общности циклов надо определить G[0,j] - 1 для всех/ Решение поставлен- поставленной задачи может выглядеть следующим образом: program aj_OO_f: var G : array[0..202.1..202] of byte; N.K.i.j.p.m : longint; begin assign(input.'input.txt'): reset(input): assign(output.'output.txt'): rewrite(output); read(N.K): for j-l to N do begin G[O.j]:-l: G[l.j]:-O: end: for i:-2 to N do for J:-1 to N do begin G[i.j]-1: for p:-l to j+1 do
5.3. Задача «Нечестная игра» 307 if G[i-j.p]-l then G[i.j]:-O: end: m:-0: for j:-l to K do if G[N.j]-l then begin write(j.' '); inc(m): end: if m-0 then writeln(O): close(input): close(output): end. Интуитивно понятно, что для выяснения результата необязательно заполнять всю таблицу G[i,j]. Поэтому можно использовать рекурсивное заполнение таблицы только для тех элементов, которые потребуются для данных в текущем тесте зна- значений переменных. Положим G[iJ] равным: Q 0, если этот элемент таблицы еще не заполнялся; ? 1, если при параметрах iJ выигрывает 1-й игрок; Q 2, если при параметрах iJ выигрывает 2-й игрок. Процедура заполнения выглядит так: procedure FillG(i.j:longint): var p: longint: function min(a.b:longint):longint: begin if a<b then min:-a else min:-b end begin if G[i.j]<>O then exit: if i-0 then begin G[i.j]:-1: exit: end for p:-l to min(i.j) do begin FillG(i-p.p+l): if G[i-p.p+l]-2 then begin G[i.j]:-1: exit: end: end: G[i.j]:-2: end: а главная программа — так: for i:-l to N do for j:-l to К do G[i.j]:-O: FillG(N.K): if g[N.K]-2 then begin writeln(O): halt: end: for p:-l to К do begin FillG(N-p.p+l): {если уже считали - выходим} {если спичек нет - победа} {для всех возможных ходов} {Проверяем последствия} {если противник проиграл.} {значит мы победили и выходим} {Противник ни разу не проиграл} {Проверить G[N.K]} {Если проиграл -} {вывод 0 и останов} {для всех возможных ходов} {Заполняем матрицу}
308 Глава 5 * Стратегические игры if G[N-p.p+l]-2 then write(p.' '): {Если первый победил - выводим} end: Рекурсивное решение поставленной задачи (работающее значительно быстрее) приведено в конце главы. 5.4. Как играть победно? Во всех трех предыдущих задачах от нас только требовалось разбить множество состояний игры на выигрышные и проигрышные, а затем для заданного состоя- состояния определить, является ли оно выигрышным или проигрышным. А что, если от нас потребуется больше — то есть не только указать, можно выиг- выиграть или нельзя, и если можно, то за сколько ходов. Как указать ПОБЕДНЫЕ ХОДЫ — то есть какими именно ходами нужно побеждать. Оказывается, это воз- возможно сделать, располагая уже построенной нами информацией о разбиении мно- множества возможных состояний игры на помножества выигрышных-проигрышных состояний. Вернемся для примера к задаче «Ладья и конь». Пусть теперь задача усложняется и требуется играть за ладью, то есть непосредственно воспроизводить ее побед- победную стратегию. Это можно делать следующим образом: нам сообщают размеры игрового поля М и N. Мы строим разбиения позиций на победные за 1, 2, 3,... ходов и проигрышные. Далее нам сообщают координаты ладьи и коня. По ним мы определяем, каков ре- результат данной игры при оптимальных стратегиях игры каждого из соперников. Если позиция проигрышная — «сдаемся*. Пусть позиция выигрышная за входов. Тогда мы просматриваем ВСЕ позиции, в которые может сделать ход ладья, и выбираем из них такую, чтобы она стала выигрышной за N - 1 ход при любом ответе коня (очевидно, что это возможно по построению разбиения позиций на подмножества). Затем, получая любой ответ коня, снова выбираем такой ход ладьи, который при- приведет к победе за N - 2 хода при любом ответе коня, и т. д. Рассмотрим задачи, которые в своей формулировке требуют победной игры. 5.5. Задача «Игра loiwari» Международная олимпиада по информатике, 2001 Игра Ioiwari Семейство игр «Mancala» с бусинами и лунками — среди старейших форм развлечений человечества. Эта задача определяет версию игры, специаль- специально разработанную для IOI. Игра ведется двумя игроками на круглой доске с семью лунками по границе. Кроме того, имеется банк для каждого игрока. Игра начинается распределением случайным образом 20 бусин по лункам
5.5. Задача «Игра loiwari» 309 таким образом, чтобы в каждой лунке оказалось не менее 2 и не более 4 бу- бусин. Игроки ходятноочгреди. Чтобы пойти, игроквыбираетнепустуюлунку н забирает все бусины из лунки в руку. Пока бусины в руке игрока еще есть, он просматривает бусины по часовой стрелке, начиная с только что опус- опустевшей лунки, и выполняет следующие операции. Если у игрока в руке больше одной бусины: если текущая лунка уже содер- содержит 5 бусин, го он забирает одну бусину из лунки и кладет в свой банк, иначе — кладет одну бусину из руки в лунку. Если у игрока в руке одна бусина: если текущая лунка содержит от 1 до 4 бусин, то игрок забирает все бусины из этой лунки и одну из своей руки и кладет в свой бнпк, иначе (тоесть если лунка содержит 0 или 5 бусин), игрок кладет бусину из своей руки в банк соперника. Игра заканчивается, когда после выполнения ходов все лунки становятся пустыми. Побеждает тот игрок, в банке которого больше бусин. Начинаю- Начинающий игрок всегда имеет выигрывающую стратегию. Вы должны напиедть программу, которая играет в игру Ioiwari как начинающий игрок и побеж- побеждает. Оппонирующая программа играет оптимально, что дает ей шанс по- побеждать вашу программу. Ввод и вывод: Ваша программа читает входные данные из стандартного ввода и выводит резуль- результаты в стандартный вывод. Ваша программа — игрок 1,оппонирующая — игрок 2. Когда daiua программа на- начинает свою работу, она вначале читает 7 целых чиселр1,р2,рЗ,р4,р5,р6,р7. Это начальные значения бусин в соответствующих лунках. Лунки пронумерованы от 1 до 7 по часовой стрелке. Затем начинается игра при пустых банках. Ваша программадолжна играть следу- следующим образом: ? если сейчас ваша очередь хода, то ваша программа должна выводить в стан- стандартный вывод номер лунки, выбранной на текущем ходу; ? если сейчас ход противника, ваша программа должна считывать из стандарт- стандартного ввода номерлунки, определяющей ход противника. Средства: Вам дается программа (ioiwari2 для Linux, ioiwari2.exe для Windows), которая играет оптимально с одной начальной позиции 4 3 2 4 2 3 2. Она начинает с того, что пишет в стандартный вывод эту начальную позицию, которую ваша програм- программа должна считать. После этого программа играет в игру, считывая ходы первого игрока из стандартного ввода и записывая собственные ходы в стандартный вы- вывод. Вы можете запустить свою программу и Ioiwari2 в различных окнах и переда- передавать ответы от программы к программе вручную. Ioiwari2 записывает диалог в файл loiwari.out. Инструкции по программированию: В примере, приведенном далее, вы читаете последнее число из стандартного вво- ввода в переменную last, а переменная mymove содержит ваш ход.
310 Глава 5 * Стратегические игры Если вы программируете на Паскале, вы должны запрограммировать ввод-вывод следующим образом: Writeln(mymove): Readln(last): Пример: Таблица 5.1. Корректная последовательность из 6 ходов Лунки Начальная ситуация Ход 1 -го игрока Ход 2-го игрока Ход 1-го игрока Ход 2-го игрока Ход 1 -го игрока Ход 2-го игрока 2 3 5 4 5 7 1. 4 4 4 4 0 0 0 2. 3 0 0 0 0 0 0 3. 2 3 0 0 0 0 0 4. 4 5 4 4 0 0 0 5. 2 0 1 0 1 0 0 6. 3 3 4 0 1 0 0 7. 2 2 0 0 1 1 0 Bank1 0 3 3 8 8 10 11 Bank2 0 0 4 4 9 9 9 Оценивание: Если ваша программа выигрывает тест, вы получаете за него 4 очка; играет вни- вничью — получаете 2 очка проигрывает — получает 0 очков. Описание решения: Фактически в данной задаче состояние игры представляют 9 чисел — содержи- содержимое семи лунок и двух банков. Прежде всего; научимся делать один ход: Move(who, k, p, bank); Здесь переменная who определяет номер игрока, который делает ход, k — номер лунки, с которой он начинает, р — массив из семи чисел (количеств бусин в лунке с соответствующим номером), bank — массив из двух чисел (количество бусин в банке у каждого игрока). Procedure Move(who.k:byte:var p:tp: var bank:tb): var nk : byte; begin ruka-P[k]: P[k]:-O: {взяли в руку все бусины их лунки с ноиером K} nk :-k: {nk - номер обрабатываемой лунки} while ruka<>O do begin nk:-Next(nk): if ruka>l {Если бусин в руке > 1} then if p[nk]-5 {a в лунке - 5 бусин.} then begin dec(p[nk]); {забираем бусину из лунки}
5.5. Задача Игра «loiwari» 311 inc(bank[who]): end else begin dec(ruka): inc(p[nk]): end else if ruka-l then if p[nk] in [1..4] then begin inc(bank[who].p[nk]): p[nk]:-0: inc(bank[who]): dec(ruka): end else begin dec(ruka): inc(bank[3-who]): end: end: {WriteState(p.bank):} end: {кладем ее в свой банк} {в лунке - не 5 бусин} {1 бусину из руки} {в лунку} {Если в руке 1 бусина} {Добавляем в свой банк} {Все бусины из лунки} {Добавляем в банк бусину} {Из своей руки} {Из своей руки} {Добавляем в банк соперника} Процедура WriteStateQ>, bank) (вместе с процедурами InputData и TestDebug) ис- используется для отладки процедуры одного хода по тестовому примеру: procedure WriteState(p:tp:Bank:tb): begin for i:-l to 7 do write(p[i].' '): write (bank[l].* '.bank[2]): writeln; end: procedure InputData: begin for i:-l to 7 do read(p[i]): end: Procedure TestDebug: begin MoveA.2.p.bank): MoveB.3.p.bank):
312 Глава 5 * Стратегические игры MoveA.5.p.bank); MoveB.4.p.bank): MoveA.5.p.bank): MoveB.7.p.bank); end: Заметим, что для отладки мы вводим в качестве исходных данныхр[2*] числа 4 3 24232. Последовательность ходов игроков тоже берем из тестового примера: Ход 1-го игрока: 2 Ход 2-го игрока: 3 Ход 1-го игрока: 5 Ход 2-го игрока: 4 Ход 1-го игрока: 5 Ход 2-го игрока: 7 Убедившись, что выводимое содержимое лунок и банков в точности соответствует данным из тестового примера, содержательную часть данной версии игры «Мап- cala* можно забыть навсегда. Более того, все последующие рассуждения справед- справедливы для всего семейства игр «Mancala», получающегося модификацией правил перемещения бусин между лунками, рукой и банками. Тело главной программы может выглядеть следующим образом: begin InputData: {TestDebug: Wri teState:} while SumP(p)<>O do {Пока в лунках есть бусины} begin Calculate(i.p.bank): {Вычисляем i - очередной выигрывающий ход} writeln(i); {Выводимего} Move(l.i.p.bank): {Пересчитываем состояние лунок} if SumP(p)<>O {Если в лунках есть бусины} then begin readln(j); {Вводим J - ответ соперника} MoveB.j.p.bank) {Пересчитываем состояние лунок} end: end: end. Алгоритм работы главной программы прокомментирован в ее тексте. Из этого алгоритма вызываются функция SumP{p) и процедура Calcufate(i,p). Функция SumP(p) очевидна: function SumP(p:tp):byte: var s.i : byte: begin s:-0: for i:-l to 7 do inc(s.p[i]): SumP:-s: end:
5.5. Задача Игра «loKvari» 313 Осталось рассмотреть процедуру Calculate. procedure Calculate(var i1:byte:р:tp:bank:tb): var i: byte: begin 1:-1: While ((p[i]-0) ог (Not Winl(i.p.bank))) and (i<-7) do inc(i): il:-i; end: Процедура Calculate получает на вход состояние лунок в массивер, выбирает оче- очередной оптимальный ход за 1-го игрока и передает его в вызывающую программу через параметр il. Для выяснения оптимального хода она перебирает все возможные ходы от 1 до 7 (если лунка пустая, то такой ход не рассматривается) и вызывает функцию Wini (i, p, bank), которая отвечает на вопрос — выиграет ли первый игрок, если пойдет ходом i (выберет лунку с номером i) при текущем состоянии лунок р. Рассмотрим подробнее текст функции Winl: function Winl(Hole:byte;p:tp:bank:tb): boolean: var i : byte; Res:boolean: begin Move(l.Hole.p.bank); if SumP(p)-0 then Winl:- Bank[l]>Bank[2] else begin Res:-true: for i:-l to 7 do if (p[i]<>0) then Res:-Res and Lost2(i.p.bank); Winl:-Res: end: end: Для выполнения своей работы функция Winl делает указанный ход в лунку Hole, при этом пересчитывается новое состояние лунок в массивр. Далее, если все лун- лунки пустые и в банке у первого игрока бусин больше, то первый игрок победил; иначе он победит только если при ВСЕХ возможных ходах соперника соперник проиграет (только в этом случае переменная Res сохранит значение true). Для выяснения факта поражения соперника при 5С?Хвозможных ответах вызывает- вызывается функция Lost2(i,p), которая получает значение истина, если второй игрок про- проиграет, пойдя ходом i при текущем состоянии лунокр. Рассмотрим теперь подробнее функцию Lost2(i, p, bank): function Lost2(Hole:byte:p:tp:bank:tb):boolean: var i : byte: Res : boolean:
314 Глава 5 * Стратегические игры begin MoveB.Hole.p.bank); if SumP(p)-0 then Lost2:- Bank[l]>Bank[2] else begin Res:-false: for i:-l to 7 do if (p[i]<>0) then Res:-Res ог Winl(i.p.bank): Lost2:-Res: end; end: Вначале делается указанный в параметре ход за второго игрока и пересчитывает- ся новое содержание лунок. Если лунки пустые и в банке у первого игрока боль- больше бусин, то второй игрок проиграл. Иначе второй игрок проиграет, если хотя бы при одном своем ответе первый игрок выиграет. Внимательный читатель заметил, что мы имеем связку из двух рекурсивных функ- функций WinX и Lost2, которые вызываютдругдруга. Но для вызова функции из другой она должна быть прежде определена! В этом случае нам помогает директива компиляции/orward. function Winl(Hole:byte:p:tp:bank:tb):boolean:forward; Полный текст программы ioOldlt2, решающей поставленную задачу, приводится в конце главы. 5.6. Задача «Игра Score» Международная олимпиада по информатике, 2001 Игра Score Score — это настольная игра для двух игроков, которые двигают на доске од- одну и ту же фишку с одной позиции на другую. Доска имеетМпозиций, прону- пронумерованных от 1 до ЛГ, и множество дуг, ведущих от одной позиции к другой. Каждая позиция принадлежит первому или второму игроку, которого мы назовем владельцем этой позиции. Кроме того, каждая позиция имеет поло- положительную цену, причем все цены различны. Позиция 1 — начальная. В на- начале игры оба игрока имеют счет 0. Игра проводится по следующим прави- правилам. Обозначим С позицию, в которой находится фишка в начале хода. В наг чале игры позиция С — это 1. Шаг игры состоит из следующих операций. 1. Если величина С больше, чем текущий счет владельца С, то величина С становится новым счетом владельца С. Иначе счет владельца С остается тем же. Счет другого игрока не изменяется в любом случае.
5.6. Задана «Игра Score» 315 2. Владелец С выбирает одну из дуг из текущей позиции фишки, и оконча- окончание этой дуги задает новую текущую позицию фишки. Заметим, что игрок может сделать несколько последовательных ходов. Игра завершается после того, как фишка возвратится в исходную позицию. Побеждает игрок, имеющий в конце игры больший счет. Дуги всегда организованы так, что выполняются следующие условия: — из любой текущей позиции имеется хотя бы одна дуга; — каждая позиция Р достижима из стартовой позиции, то есть существует последовательность дуг от стартовой позиции к позиции Р; — гарантировано завершение игры через конечное число ходов. Напишите программу, которая играет в эту игру и побеждает. Все пози- позиции, которые предъявляются вашей программе, таковы, что победа воз- возможна вне зависимости от того, делаете ли вы первый ход. Программа противника играет оптимально, и это дает ей шанс выиграть игру у вашей программы. Ввод и вывод: Ваша программа читает входные данные из стандартного ввода и пишет их в стан- стандартный вывод. Ваша программа — Игрок 1, а программа жюри — Игрок 2. Когда ваша программа начинает работать, она должна считать следующую информацию из стандартного ввода. Первая строка содержит одно целое число: количество позиций N. 1 <N< 1000. Каждая из следующих Мстрок содержитМцелых чисел с информацией о дугах. Если есть дуга из позиции i в позицию/ joj-e число в i-й строке из этихМстрок равно 1,иначе — 0. Следующая строка содержит N целых чисел — владельцев позиций. Если пози- позиция i принадлежит игроку 1 (то есть вам), то i-e число равно 1, иначе — 2. Следующая строка содержитМцелых чисел — цены позиций. Если i-e целое чис- число есть/ то цена позиции i равна/ Все эти величины.;' удовлетворяют свойствам: 1 <>j uNn все цены.;' различны. Игра начинается с положения фишки в позиции 1. Ваша программа должна иг- играть, как описано ниже, и закончить игру, когда фишка вернется в позицию 1. ? Если сейчас очередь хода вашей программы, то она должна написать номер следующей позиции Р, 1 <Р<Мвстандартный вывод. ? Если сейчас очередь хода вашего противника, то ваша программа должна про- прочитать номер следующей позиции Р, 1 < Р < N из стандартного ввода. Рассмотрим следующий пример (доска представлена на рис. 5.1). Позиции, помещенные в круглые скобки, принадлежат игроку l,a позиции в квад- квадратных скобках принадлежат игроку 2. Каждая позиция имеет собственную цену, записанную внутри скобок, и номер позиции рядом. Игра, которую предстоит сыграть, представлена далее.
316 Глава 5 * Стратегические игры Рис. 5.1. Пример игрового поля для задачи «Игра Score» Таблица 5.2. Игра и пояснения к ней Stdin Stdout Пояснения 4 0100 001 1 0001 1 000 1 1 22 1 342 1 2 4 N Информация о дугах из позиции 1 Информация о дугах из позиции 2 Информация о дугах из позиции 3 Информация о дугах из позиции 4 Владельцы позиций Цены позиций Игрок 1 ходит Игрок 1 ходит Игрок 2 ходит в начальную позицу По завершению игры игрок 1 имеет счет 3, игрок 2 имеет счет 2. Игрок 1 но- беждает. Инструкции по программированию: В нижеследующем примере target — это целая переменная для позиции. Если вы программируете на Паскале, вы должны так организовать ввод и вывод: Read1n(target); Writeln(target): Средства: Вам дается программа (score2 для Linux, score2.exe для Windows). Эта про- программа читает описание игры из файла score.in в вышеописанном формате. Про- Программа пишет эту информацию в стандартный вывод в том же формате. Этот вывод может быть использован для ввода в вашу программу в тестовых целях. После этого программа играет со случайной стратегией, читая ходы вашей про- программы из стандартного ввода и записывая собственные ходы в стандартный вывод. Оценивание: Для каждого теста, если вы выиграли, вы получаете все баллы, иначе — 0 баллов. Ввод и вывод вашей программы сохраняются. Затем ваша программа исполня- исполняется второй раз, с вводом, перенаправленным из файла, и учетом официального
5.6. Задана «Игра Score» 317 времени на тест. Программа должны производить те же самые результаты, что и при первом запуске. Тело главной программы выглядит следующим образом: begin InputData; {Ввод исходных данных} Calculate: {Подготовка к работе} cv:-l: {Текущая вершина} While true do begin if v[cv]-l {Если ход мой} then begin Next(cv): {Выбрать ход} writeln(cv); {Передать его жюри} end else {Ход соперника} readln(cv); {Ввести ход от соперника} if cv-1 then exit: {Если вернулись в вершину 1 - конец} end: end. Главая программа очевидна и хорошо самодокументирована. Осталось разобрать содержание вызываемых из главной программы процедур InputData, Calculate и Next(cv). Начнем с InputData procedure InputData: begin readln(N): for i:-l to N do for j:-l to N do read (a[i.j]): for i:-l to N do read(v[i]); for i:-l to N do read(c[i]): end: Здесь: О N — количество позиций; а а — граф игры (a[i,j] - 1, если есть дуга из вершины i в вершину^); ? v — владельцы позиций v[i] e 1 или - 2; ? с — цены позиций, от 1 до N, все различны. Теперь рассмотрим Calculate: procedure Calculate: var i : byte: begin for i:-l to N do Sorted[c[i]]:-i:
318 Глава 5 * Стратегические игры for i:-l to N do for j:-l to N do Free[i.j]:-true: end: Строка условия задачи «1 ?j < N, и все цены.;' различны* дает нам замечательно простой способ отсортировать вершины (позиции) по возрастанию цен: for i:-l to N do Sorted[c[i]]:-i: То есть в массиве Sorted на позиции k мы храним номер позиции (вершины), цена которой равна k. Кроме того, мы подготавливаем к последующим расчетам мас- массив пометок использованых дуг графа: Free[i,j] - True, если дуга из i ъ] еще не использовалась. И, наконец, процедура Next(cv) по текущей вершине cv вычисляет следующую вершину cv (в случае хода первого игрока): Procedure Next(var cv:integer); begin for i:«N downto 1 do if (a[cv.Sorted[1]]-l) and (v[Sorted[i]]-l) and Free[cv.Sorted[i]] then begin Free[cv.Sorted[i]]:-False: cv:-Sorted[i]; exit; end; for i:-l to N do if (a[cv.Sorted[i]]-l) and (v[Sorted[i]]-2) and Free[cv.Sorted[i]] then begin Free[cv.Sorted[i]]:-False: cv:-Sorted[i]: exit: end: end: Идея процедуры Next проста. Вначале мы перебираем дуги в принадлежащие нам вершины по УБЫВАНИЮ цены (чтобы лучшую взять первой). Если из текущей вершины нет дуги в нашу вершину, то затем перебираем дуги в принадлежащие сопернику вершины в порядке возрастания цены с тем, чтобы худшую взять первой. Полный текст решения данной задачи приведен в конце главы.
5.7. Задача «Игра-2» 5.7. Задача «Игра-2» Международная олимпиада по информатике, 1996 Игра-2 (input.txt / output.txt) Рассмотрим следующую игру между двумя игроками. На игровой доске записана последовательность положительных целых чисел. Игроки ходят по очереди. Ход заключается в том, что игрок выбирает число, расположен- расположенное на левом или на правом конце последовательности; выбранное число стирается с доски. Игра заканчивается, когда на доске не остается чисел. Первый игрок выигрывает, если сумма выбранных им чисел не меньше, чем сумма чисел, выбранных вторым игроком. Второй игрок играет наилучшим образом. Первый игрок всегда ходит первым. Известно, что если в начале игры на доске записано четное количество чи- чисел, то у первого игрока есть выигрышная стратегия. Требуется написать программу, которая реализует выигрышную стратегию первого игрока. Ответы второго игрока выдает имеющийся в вашем распоряжении модуль Play. В модуле реализованы три процедуры: StartGame, MyMove, YourMove. В самом начале ваша программа должна иницилизировать игру, вызвав процедуру StartGame (без параметров). Когда программа делает очередной ход, она выполняет инструкцию MyMove('L') или MyMove('R*) в зависи- зависимости от того, какое число она выбирает — левое или иравое. Чтобы полу- получить ответ второго игрока, ваша программа должна выполнить инструкцию YourMove(C), где С — переменная символьного типа. Процедура YourMove записывает ответ второго игрока в заданную перемен- переменную ('L', если выбиратся левое число; 7?', если выбиратся правое). Ввод: Первая строка файла исходных данных с именем ШРиТ.ТХТсолержит длину начальной последовательности N (N— четное и 2 <JV< 100). Далее следуют N строк, в каждой из которых содержится одно число. Эти числа задают начальные значения, записанные на доске в порядке слева направо. Числа на доске не превы- превышают 200. Вывод: Когда игра заканчивается, ваша программа должна записать результат игры в файл с именем OUTPUT.TXT. Файл содержит два числа, записанные в одной строке. Первое число задает сумму чисел, выбранных первым игроком, второе число — сумму чисел, выбранных вторым игроком ваша программа должна сыграть игру с модулем Play, a вывод должен соответствовать сыгранной игре. Пример ввода Пример вывода ~6 15 14 4 7 _2 продолжение &
320 Глава 5 * Стратегические игры Пример ввода (продолжение) ~9 5 2 К счастью, стратегия в данной игре чрезвычайно простая и в данном случае не требуется применять подходы, описанные ранее. В этом смысле данную задачу можно рассматривать как задачу-шутку. Поскольку по условиям количество чисел всегда четно, то первый игрок всегда может заставить второго брать только числа, стоящие на четных позициях, или числа, которыестояттолько па нечетных позициях. Остается в начале игры выяс- выяснить, сумма каких чисел не меньше тех, что стоят на четных позициях, или тех, что стоят на нечетных позициях, — и затем заставить второго игрока брать числа, которые дают не большую сумму. Например, для данных 4 7 2 9 5 2 сумма чисел на нечетных позициях 4 + 2 + 5 = 11, а сумма чисел на четных пози- позициях 7 + 9 + 2 = 18. Значит, первому игроку выгодно брать числа, стоящие на ЧЕТ- ЧЕТНЫХ позициях. Как это обеспечить? Берем число на четной позиции 6. Теперь у второго игрока выбор междудвумя числами: 4, которое стоит на первой позиции, и 5, которое стоит на нятой позиции. Оба числа стоят на нечетной позиции. Какое бы из них он не взял — это будет число на нечетной позиции. И первому игроку после хода второго игрока нужно будет снова брать число на четной позиции — то, которое станет доступным после хода второго игрока. В общем случае алго- алгоритм первого игрока выглядит следующим образом: Вычисляем SOdd - сумму всех чисел на нечетных позициях SEven - сумму всех чисел на четных позициях Если SOdd>SEven то первый ход L (слева) иначе первый ход R (справа) И затем (N div 2)-l раз делаем ходы следующим образом Если соперник делает ход L. то и мы отвечаем L. иначе мы отвечаем R Вывод (SEven. Sodd в порядке убывания). Понятно, что набранные игроками суммы ири такой стратегии игры не зависят от порядка выбора чисел. Кроме того, в работе с оригинальным модулем Play экспериментально установлено, что брать числа нужно до тех пор, пока останутся невыбранными два числа. Наверно, предполагается, что в ситуации двух остав- оставшихся чисел выбор обоих игроков очевиден, и его не нужно имитировать. К со- сожалению, в условиях это явно не прописано. Полный текст решения приведен в конце главы.
5.8. Решения задач 321 5.8. Решения задач Листинг 5.1. Текст программы к задаче «Алиса и Боб» program nee4w98d; const MaxN - 1000: МахМ - 10000: МахК - 20: var i.N.M.T. from, tov : integer: Changed : boolean: S.Ka : array[l..MaxN]ofinteger: В : array [l..MaxN] of byte: a : array [l..MaxN.l..MaxK] of integer: • function ExistArc(i:integer):boolean: var j : integer, begin ExistArc:*false: for j:-l to ka[i] do if S[a[i.j]]-1 then begin ExistArc:-true: exit: end: end: function AllArcs(>:integer):boolean: var j : integer: begin . AllArcs:-true: for j:-l to ka[i] do ifS[a[i.j]]-0 then begin AllArcs:-false: exit: end: end: procedure InputData: begin assign (input.'d.in'): reset(input): read (N.M.T): for i:-l to N do read (B[i]): продолжение &
322 Глава 5 * Стратегические игры Листинг 5.1. (продолжение) for i:-l to М do begin read (from.tov); inc(ka[from]): a[from.ka[from]]:-tov: end: close(input): for i:-l to N do S[i]:-0: S[T]:-l: end: procedure OutputData; begin assign (output.'d.out'): rewrite(output): write (S[1]): for i:-2 to N do writeC '.S[1]): close(output); end: begin InputData: Changed:-true; while Changed do begin Changed:-false: for i:-l to N do if (S[i]-0) and (((B[i]-l) and ExistArc(i)) or ((B[i]-0) and A11Arcs(i))) then begin S[i]:-1: Changed:-true: end; end: OutputData; end. Листинг 5.2. Текст программы к задаче «Ладья и конь» program go00dlt2: const MaxN - 8; МахМ - 8: var Changed : boolean; i.j.N.M. CurrentX. CurrentY.ilt.jlt.
5.8. Решения задач 323 li.lj.ki.kj. il.jl.ik.jk. Number : integer; S : array [l..MaxM.l..MaxN.l..MaxM.l..MaxN] of integer: P : array [0..9] of longint: AllP : longint: procedure InputData: var c : char; begin assign (input.'input.txt'); reset(input): readln (N.M); read(c): li:-Ord(c)-Ord('a')+l: read(lj): read(c): read(c): ki:-Ord(c)-Ord(V)+l; read(kj): c1ose(input); for il:-l to M do for jl:-l to N do for ik:-l to M do for jk:-l to N do S[il.jl.ik.jk] :-0; {Bce состояния проигрышны} for il:-l to M do for jl:-l to N do for ik:-l to M do for jk:-l to N do if ((il-ik) or (j1-jk)) and not ((il-ik) and (jl-jk)) then S[il.jl.ik.jk] :-l: {Выигрышные состояния за 1 ход. {если ход ладьи} {и конь на одной вертикали} {или на одной горизонтали} {c ладьей} end: procedure OutputData: begin assign (output.'output.txf); rewrite(output): writeln(S[li.lj.ki.kj]): close(output): end: type Knight - array [1..8.1..2] of integer; const Steps : Knight - (( l.-2).( 1. 2). (-l.-2).(-l. 2). ( 2.-l).( 2. 1). (-2.-l).(-2. 1) ): {все текущие возможные ходы} {Возможные ходы коня} {Массив констант} procedure PutIfAll(x.y:integer); продолжение
324 Глава 5 * Стратегические игры Листинг 5.2 {продолжение) var All. Was : boolean: begin j:-l: Was:-false: All:-true: while All and (j<-8) do begin CurrentX :- x+steps[j.l] : CurrentY :- y+steps[j.2] : if (CurrentX>O) and (CurrentX<-M) and (CurrentY>O) and (CurrentY<-N) then begin if (S[il.jl.CurrentX.CurrentY]<-Number) and (S[il.jl.CurrentX.CurrentY]<>O) and not ((il<urrentX)and(jl-CurrentY)) then Was :- true else All :- false; end; inc(j) end; if Was and All then begin for ilt:-l to M do if (S[ilt.jl.x.y]-O) and (ilt<>il) then begin S[ilt.jl.x.y]:-Number+1; Changed:-true: end; for jlt:-l to N do if (S[il.jlt.x.y]-O) and (jlt<>jl) then begin S[il.jlt.x.y]:-Number+1; Changed:-true: end; end; end: procedure PutAllNewWinStates(il.jl.ik.jk:integeD: begin for i:-l to 8 do begin CurrentX :« ik+steps[i.l] : CurrentY :-jk+steps[i.2] :
5.8. Решения задач 325 if (CurrentX>O) and (CucrentX<-M) and (CurrentY>O) and (CurrentY<-N) then PutIfAl1(CurrentX.CurrentY): end: end: begin InputData; Changed:-true: Number:-O: while Changed do begin Changed:-fa1se; Inc(NumbeD: for il:-l to M do {По всем состояниям} for j1:-l to N do for ik:-l to M do for jk:-l to N do if S[il.jl.ik.jk>Number then PutA11NewWinStates(il.jl.ik.jk): end. OutputData: end. Листинг 5.3. Текст программы к задаче «Нечестная игра» program aj_00Jr; var G : array[0..202.1..202] of byte: N.K.i.j.p.m . longint: procedure FillG<i.j:longint): var p: longint: function min(a.b:longint):longint: begin if a<b then min:-a else min-b end: begin if G[i.j]<>0 then exit: if i-0 then begin G[i.j]:-1: exit: end: for p:-l to min(i.j) do begin FillG(i-p.p+l): if G[i-p.p+l]-2 then begin 6[1.j]:-l: exit: end: end: продолжение тУ
326 Глава 5 * Стратегические игры Листинг 5.3 (продолжение) 6[i.j]:-2: end; begin assign(input.'input.txf): reset(input): assign(output.'output.txt1): rewrite(output); read(N.K): for i:-l to N do for j:-l to К do G[i.j]:-O; FillG(N.K): if g[N.K]-2 then begin writeln@): halt: end: for p:-l to К do begin FillG<N-p.p+l): if G[N-p.p+l]-2 then write(p.' '): end: close(input): close(output): end. Листинг 5.4. Текст программы к задаче «Игра loiwari» program ioOldlt2: type tp - аггау [1. .7] of byte; tb - аггау [1..2] of byte: var Р : tp: N.1.j.k : byte: bank : tb: ruka.nc : byte: funct i on Next(х:byte):byte: begin inc(x): if х-8 then x:-l: Next:-x: end: procedure Wri teState(p:tp:Bank:tb): var i- byte: begin for i:-l to 7 do write(p[i].' '): write (bank[l].' '.bank[2]); wnte1n: end:
5.8. Решения задач 327 Procedure Move(who.k:byte;var p:tp: var bank var nk : byte: begin tb); ruka:-P[k]; P[k]:-0; nk :-k; while ruka<>0 do begin nk:-Next(nk); if ruka>l then if p[nk]-5 then begin dec(p[nk]); inc(bank[who]): end else begin dec(ruka): inc(p[nk]); end else if ruka-l then if p[nk] in [1..4] then begin inc(bank[who].p[nk]); p[nk]:-0: inc(bank[who]): dec(ruka): end else begin dec(ruka): inc(bank[3-who]): end: end: {WriteState(p.bank):} end: {взяли в руку все бусины из лунки с номером К) {nk - номер обрабатываемой лунки) {Если бусин в руке > 1.) {а в лунке - 5 бусин.) {забираем бусину из лунки) {кладем ее в свой банк) {В лунке - не 5 бусин) {1 бусину из руки) {кладем в лунку) {Если в руке 1 бусина) {Добавляем в свой банк) {все бусины из лунки) {Добавляем в банк бусину) {из своей руки) {Из своей руки) {добавляем в банк соперника) Procedure TestDebug: продолжение
328 Глава 5 * Стратегические игры Листинг 5.4 (продолжение) begin MoveA.2.p.bank): MoveB.3.p.bank): MoveA.5.p.bank): MoveB.4.p.bank): MoveA.5.p.bank); MoveB.7.p.bank): end: procedure InputData: var i : byte: begin for i:-l to 7 do read(p[i]): end: function SumP(p:tp):byte; var s.i : byte: begin s:-0: for i:-l to 7 do inc(s.p[i]): SumP:-s: end: function Winl(Hole:byte:p:tp:bank:tb):boolean:forward: function Lost2(Hole:byte:p:tp:bank:tb):boolean: var i : byte: Res : boolean: begin MoveB.Hole.p.bank): if SumP(p)-0 then Lost2:- Bank[l]>Bank[2] else begin Res:-false: for i:-l to 7 do if (p[i]<>0) then Res:-Res ог Winl(i.p.bank): Lost2:-Res: end: end: function Winl(Hole:byte:p:tp:bank:tb): boolean: var i : byte: Res:boolean: begin Move(l.Hole.p.bank): if SumP(p)-0 then Winl:- Bank[l]>Bank[2] else begin
5.8. Решения задач 329 Res:-true: for i:-l to 7 do if (p[i]<>0) then Res:-Res and Lost2(i.p.bank): Winl:-Res; end; end; procedure Calculate(var il:byte:p:tp;bank:tb); var i: byte; begin i:-l: While ((p[i]-0) or (Not Winl(i.p.bank))) and (i<-7) do inc(i); il:-i: end; {TestDebug; WriteState:} {Пока в лунках есть бусины} {Вычисляем i - очередной выигрывающий ход} {Выводим ero} {Пересчитываем состояние лунок} {Если в лунках есть бусины} {Вводим J - ответ соперника} {Пересчитываем состояние лунок} begin InputData; while SumP(p)<>0 do begin Calculate(i.p.bank); writeln(i); Move(l.i.p.bank); if SumP(p)<>0 then begin readln(j): MoveB.j.p.bank) end; end; end. Листинг 5.5. Текст программы к задаче «Игра Score» program ioOld2tl; const MaxN - 1000: var N а Free v с Sorted integer: array [l..MaxN.l..MaxN] of byte: array [l..MaxN.l..MaxN] of boolean: array [l..MaxN] of byte: array [l..MaxN] of integer; array [l..MaxN] of integer: {Кол-во позиций} {Граф} {использованные дуги} {Владельцы позиций} {Цены позиций} продолжение
330 Глава 5 • Стратегические игры Листинг 5.5 {продолжение) cv : integer; i.j : integer: {f : text:} procedure InputData; begin readln(N); for i:-l to N do for j:-l to N do read (a[i.j]); for i:-l to N do read(v[i]); for i:=l to N do read(c[i]): end; Procedure Next(var cv:integer): begin for i:=N downto 1 do if (a[cv.Sorted[i]]-l) and (v[Sorted[i]>l) and Free[cv.Sorted[i]] then begin Free[cv.Sorted[i]]:-False: cv:-Sorted[i]; exit; end; {Текущая вершина} for i:-l to N do if (a[cv.Sorted[i]]-l) and (v[Sorted[i]]-2) and Free[cv.Sorted[i]] then begin Free[cv.Sorted[i]]:-False: cv:-Sorted[i]; exit; end: end; procedure Calculate: var i: byte: begin for i:-l to N do Sorted[c[i]]:-i:
5.8. Решения задач 331 for i:-l to N do for j:-l to N do Freed .j]: end: begin InputData: Calculate: cv:-l: While true do begin if v[cv]-l then begin Next(cv): writeln(cv): end else readln(cv): if cv-1 then exit: end: end. -true: {Просчитать состояния вершин: {Текущая вершина} {Если ход ной} {Выбрать ход} {Передать его жюри} {Ход соперника} {Ввести ход от соперника} {Если вернулись в вершину 1 - выигрыш/проигрыш} конец} Листинг 5.6. Текст программы к задаче «Игра-2» program io96dltl: uses play: var N.SOdd.SEven.Sl.S2.i.j : integer: а : array[1..100] of integer: С : char: procedure InputData: begin assign (input.'input.txf): reset(input): readln(N): for i:-l to N do readln(a[i]): close(input): end: procedure OutputData: begin assign (output.'output.txt'): rewrite(output): writeln(Sl. ' \ S2): close(output): end: procedure Process: продолжение
332 Глава 5 * Стратегические игры Листинг 5.6 (продолжение) begin SOdd:-0: SEven:-0: for i:-l to N do if Odd(i) then inc(SOdd .a[i]) else inc(SEven.a[i]): if SOdd>SEven then begin Sl:-SOdd; S2:- SEven: MyMoveCL') end else begin Sl:-SEven: S2:- SOdd: MyMoveCR*) end: YourMove(C): for i:-l to (N div 2)-2 do begin if (C-'L') then MyMove('L') else MyMove('R'): YourMove(C): end: end: begin InputData: StartGame: Process: OutputData: end.
Глава 6. Диаграмма Юнга Возникновение этого раздела в данной книге связано прежде всего с тем, что на Международной олимпиаде по информатике для школьников 2001 года было пред- предложено сразу две задачи («Склад* и «Twofive»), авторское решение которых опи- опирается надиаграммы Юнга1. 6.1. Введение в диаграмму Юнга Диаграмма Юнга формы (nl, n2,..., nM), гдея1 ? n2 ?... ? пМ ? 0 — это расположение п\ + я2 +...+ яМразличных целых чисел в массиве строк, выровненных по левому краю, где в /-й строке содержится n/элементов; при этом в каждой строке элементы возрастают слева направо, а элементы каждого столбца возрастают сверху вниз. Например, 1 2 5 9 10 15 3 6 7 13 4 8 12 14 11 есть диаграмма Юнга формы F,4,4,1). Такие расположения ввел Альфред Юнг в 1900 году в качестве вспомогательного средства при изучении матричного пред- представления перестановок. Далее для краткости вместо «диаграмма Юнга» будем говорить просто «диаграмма». 6.2. Вставка и удаление элементов диаграммы Рассмотрим алгоритм вставки элемента в диаграмму. Пусть Р — диаграмма из целых положительных чисел, ах — целое положительное число, не содержащееся в Р. Этот алгоритм преобразует Р в другую диаграмму, содержащую х наряду 1 Другое название диаграмм Юнга — «табло Янга». Такой перевод встречается, например, в книге Д. кнута «Искусство программирования на ЭВМ* (M.: Мир, 1978). Название «Диаграммы Юнга» встречается, например, в книге Р. Стенли «Перечислительная комбинаторика» (M.: Мир, 1990).
334 Глава 6 * Диаграмма Юнга с исходными элементами Р. Новая диаграмма будет иметь ту же форму что и ста- старая, с той лишь разницей, что на пересечении строки 5 и столбца Гпоявится но- новый элемент, где 5 и Гформируются в процессе выполнения алгоритма. Неформально алгоритм может быть описан следующим образом: если х больше всех элементов первой строки, то ставим его ЗА последним элементом: s e 1, t = п\ + 1 — и алгоритм завершен. В противном случае в первой строке ищем по- зицию7*такую,чтоРA,7)<*<РA,7 + 1)иставим#напозициюA,7).А#присва- иваем значение элемента P(l,j + 1), и продолжаем аналогичную работу теперь во второй строке. То есть для i-й строки: если она пуста, то ставим элемент в пози- позицию (f, 1) E e f, Т e 1); если элемент х больше всех элементов строки i, то ставим его за последним элементом P(i, ni + l)-#, E - i, Т - ni + 1) (здесь ni — количество элементов в i-той строке). Если найдено такое^', что P(iJ) < х < P(iJ + 1), то обме- обмениваем значения х и P(i,j + 1) и переходим к следующей строке. Например, при добавлении последовательно в пустую диаграмму элементов 3,4, 9, 2, 5,1 получим такую последовательность диаграмм: 3 ; 3 4:34 9:349 2:249 3 5:245 3 9 1:14 5 2 9 3 Можно заметить, что в результате добавления элемента в диацэамму добавляется позиция (S,T), обладающая свойством «крайности». То есть ниже этой позиции и правее ее элементов нет. Важное свойство такого алгоритма вставки элементов в диаграмму — это возмож- возможность его «исполнения в обратном направлении», то есть исполнения алгоритма удаления элемента из диаграммы, которое по заданным 5 и Т (строка и столбец соответственно) удалит элемент с «крайней» позиции диаграммы (S, Т) (в диа- диаграмме нет элементов ниже и правее этой позиции); при этом переменная х полу- получит значение удаленного элемента (всегда из первой строки). Неформально этот алгоритм может быть описан следующим образом: берем эле- элемент из позиции диаграммы с координатами E, Т) и заносим его значение в х. Далее для всех строк с номерами i от S-1 до 1 находим элемент (на позиции.;'), такой что P(iJ) < х < P(iJ + 1). Если такого нет, то используем крайний элемент строки в качестве элемента P(i,j). Обмениваем значениями найденный элемент P(iJ) и ху затем переходим к предыдущей строке (i - 1). Элемент, вытесненный из первой строки, и есть результатдгвыполнения алгоритма наряду с перестроен- перестроенной диаграммой. Например, если из диаграммы 1 4 5 2 9 3 удалить элемент с позиции C,1), то получим диаграмму: 2 4 5 3 9
6.3. Количество возможных диаграмм заданной формы (n1, n2 nM) 335 а удаленным элементом будет x= 1, который находился в исходной диаграмме напозицииA,1). Если из исходной диаграммы удалить элемент B,2), то получим диаграмму: 1 4 9 2 3 а удаленным элементом будет элемент 5. И, наконец, если из исходной диаграммы удалить элемент с позиции A, 3), то по- получим диаграмму: 1 4 2 9 3 а вытесненным элементом будет х - 5. 6.3. Количество возможных диаграмм заданной формы (n1,n2,..., nM) Количество возможных диаграмм заданной формы (я1, n2,..., nM) — это ко- количество различных способов составить из элементов от 1 до п\ + n2+...+nM диаграммы, имеющие форму (я1, я2,.., пМ). То есть имеющие n1 элементов в первой строке я2 элементов во второй строке ... пМ элементов в строке с номером пМ. Это количество равно сумме количеств диаграмм, получающих- получающихся последовательным удалением по одному элементу в каждой из строк, кото- которая содержит хотя бы один элемент. Соответствующее рекуррентное соотно- соотношение имеет вид: если nl>-n2>-...nM>-l. то f(nl.n2 nM) - f(nl-l.n2 nM) + f(nl.n2-l nM) + f(nl.n2 nM-l) если не выполнено соотношение nl>-n2>-...nM>-0 то f(nl.n2 nM) - 0 Кроме того, f(nl.n2....nM.0) - f(nl.n2....nM) Это рекуррентное соотношение вытекает из того факта, что при удалении наи- наибольшего элемента из диаграммы всегда снова получается диаграмма. Далее рассмотрим решение двух задач Международной олимпиады по информа- информатике 2001 года, которая состоялась в Финляндии.
336 Глава 6 - Диаграмма Юнга 6.4. Задача «Склад» Международная олимпиада по информатике, 2001 Склад (depot.in / depot.out) Финская компания имеет большой прямоугольный склад. I Ia складе есть pa- бочпй и менеджер. Стороны склада в иорядкеобхода называютсялевая, верх- верхняя, правая и нижняя. Площадь склада разбита на квадраты с равной илоща- дыо, составляющие строки и столбцы. Строки пронумерованы сверху нелы- мн числами 1, 2 а столбцы пронумерованы слева целыми числами 1, 2,.... Склад имеет контейнеры, которые используюгся для хранения отдельных устропе гв. Контейнеры имеют различные идентификационные номера. Каж- Каждый контейнер занимает ровно один квадрат. Склад такой большой, что количество контейнеров, которые прибываюг, меньше, чем количество строк и количество столбцов. Контейнеры не удаляются со склада, но иногда при- прибывают новые контейнеры. Вход в склад в левом верхнем углу склада. Рабочий разместил контейнеры вокруглевого верхнего угла таким обра- образом, чтобы он мог найти их но пх идентификационным номерам. Он ис- использует следующий метод. Предположим, что идентификационный номер следующего контейнера, который нужно вставить, есть К(конгейнер#для краткости). Рабочий идет вдоль первой строки и ищет первый контейнер с идентификационным номером больше, чем К. Если он не находиттакого контейнера, то он ставит его непосредственно за самым правым контейне- контейнером в строке. Если такой контейнер L найден, то контейнер L заменяется контейнером К, а контейнер L вставляется в следующую строку при помо- помощи того же самого метода. Если рабочий достигает строки, в которой нет ни одного контейнера, он ставит контейнер в позицию самого левого квадрата :m>ii строки. Предположим, что контейнеры 3,4,9, 2, 5, 1 прибыли на склад именно в та- таком порядке. Тогда размещение контейнеров на складе будет таким: 1 15 2 9 3 К рабочему подходит менеджер и между ними происходит следующий диалог Менеджер: «Контейнер 5 прибыл перед контейнером 4?» Рабочий: «Нет, это невозможно». Менеджер: «Так вы можете рассказать мне порядок прибытия контейнеров по их размещению?» Рабочий: «Вообще говоря, пет. Например, контейнеры, находящиеся на складе, могли прибыть в порядке 3, 2, 1, 4, 9, 5, или в порядке 3, 2, 1, 9, 4, 5, пли в одном пз 14 других порядков». Поскольку менеджер не хочет выглядеть намного глупее рабочего, он ухо- уходит. Вы должны помочь менеджеру и написать программу, вычисляющую
6.4. Задача «Склад» 337 по данному размещению контейнеров все возможные порядки, в которых они могли прибывать. Ввод: Имя входного файла depot.in. Первая строка содержит одно целое число — количе- количество строк, в которых содержатся контейнеры. Следующие R строк содержат информацию о строках 1,..., R, начиная с верхней в следующем порядке: вначале каждой из этих строк находится число М — количество контейнеров в этой строке. За ним находятся Мцелых чисел — идентификационные номера контейнеров в стро- строке, начиная слева. Все идентификационные номера контейнеров /удовлетворяют условию 1 < / ? 50. ЧислоМконтейнеров в складе удовлятворяет условию 1 < N й 13. Вывод: Имя выходного файла depot.out. Выходной файл состоит из строк, количество кото- которых равно числу различных вариантов прибытия контейнеров. Каждая из этих строк содержитМцелых чисел — идентификационных номеров контейнеров в порядке, соответствующем порядку прибывания. Все строки должны быть уникальны. Примеры ввода и вывода: depot, in 3 3 1 45 229 1 3 Пример 1 Depot.out 32 1 495 32 1 945 342 1 95 324 195 329 145 392 1 45 342915 349215 324915 32941 5 3924 1 5 342951 349251 324951 329451 392451 Пример 2 depot.in depot.out 2 312 212 132 1 3 Оценивание: Если выходной файл содержит невозможный порядок или не содержит порядков вообще, вы получаете 0 баллов за соответствующий тест. В противном случае ваш счет вычисляется следующим образом: если выходной файл содержит все возможные варианты, вы получаете 4 балла. Если выходной файл содержит
338 Глава 6 * Диаграмма Юнга половину или более возможных вариантов и все они различны, вы получаете 2 балла. Если выходной файл содержит менее половины различных возможных вариантов или некоторые из них встречаются более чем 1 раз, вы получаете 1 балл. Внимательный читатель уже догадался, что алгоритм заполнения склада контей- контейнерами — это алгоритм вставки элементов в диаграмму. И задача заключается в восстановлении всех возможных способов сформировать заданную диаграмму. Поскольку мы уже изучили алгоритм «выталкивания» элементов из диаграммы, то мы знаем, как выяснять, какой элемент мог поступить последним. Применяя последовательно алгоритм ко всем получающимся диаграммам, получаем реше- решение задачи. Ниже оно описано более детально. Из алгоритма заполнения склада контейнерами следует, что при внесении нового контейнера в складе появляется контейнер на ранее незанятой «угловой» позиции, обладающей тем свойством, что справа от нее и ниже ее нет контейнеров. Действительно добавляемый контейнер либо сам становится в такую позицию (ес- (если его номер больше всех остальных), либо вытесняет в следующую строку кон- контейнер, которому передает те же функции; последний делает то же самое, и т. д. Таким образом, рекурсивно перебирая все «угловые» позиции, обладающие свой- свойством «справа от нее и ниже ее нет контейнеров», и накапливая «выталкиваемые обратно» контейнеры, при освобождении таких позиций мы получим все возмож- возможные варианты поступления контейнеров. Тело главной программы выглядит следующим образом: begin InputData: assign(output.'depot.out'): rewrite(output): Process(A); close(output); end. Здесь процедура InputData обеспечивает ввод исходных данных, а рекурсивная процедура Process — формирование (и вывод в файл результата по мере получе- получения) всех возможных порядков поступления контейнеров. Приведем процедуру InputData: procedure Inputdata: begin for i:-0 to 13 do for j:=0 to 13 do a[i.j]:-O: assign(input.'depot.in'); reset(input): readln(R): for i:-l to R do begin read(a[i.0]): for j:-l to a[i.0] do read(a[i.j]); end:
6.4. Задача «Склад» 339 close(input): is:*0: end; Процедура InputData формирует в результате ввода двумерный массив Л, в ко- котором: ? a[i, 0] — количество контейнеров в i-й строке; Q a[i,j] — номер контейнера, стоящего в i-й строке^-м столбце (для; от 1 до a[i, 0]); ? a[iJ] e Одля позиций, незанятых контейнерами; ? R — количество строк в массиве Л, занятых номерами контейнеров. Рассмотрим подробнее процедуру Process{A): procedure Process(a:mas): var i :integer; begin if a[1.0>0 then begin PutAnswer: exit; end: for i:-R downto 1 do if (a[i.0]<>0) and (a[i+l.a[i.0]]-0) then begin k:-a[i.a[i.O]]: ik:-i: Find(a.i-l.k.ik): Push(k); a[ik.a[1k.0]]:-0: dec(a[ik.0]): Process(a): k:-Pop: Add(a.k): end; end: Очевидно, что она рекурсивная и что при каждом вызове к ней в матрице Л стано- становится на 1 элемент меньше. Когда матрица а становится пустой (e[l, 0] e 0), то выводится результат (с помощью процедуры PutAnswer) и осуществляется выход из текущего экземпляра рекурсивной процедуры Process. Из матрицы а последовательно удаляются все «граничные* элементы вида a[i, a[i, 0]] следующим образом: с помощью процедуры Яя^вматрице а находит- находится элемент k, добавление которого привело к заполнению позиции a[i, a[i, 0]], ik — номер строки, в которой находится этот элемент. Найденный элемент k заносится в стек с помощью процедуры Push, a его позиция освобождается (a[ik, a[ik, 0]]: - 0). Также уменьшается на 1 счетчик числа эле- элементов в соответствующей строке (dec(a[ik, 0])). Таким образом, процедураЯгосезз построила «предыдущую копию» матрицыддо добавления в нее элемента k. И теперь вызывает себя вновь — для «уменьшен- «уменьшенной» матрицы а.
340 Глава 6 * Диаграмма Юнга После выхода из процедуры Process k восстанавливается из стека функцией Рор, а затем процедурой АМдрбавляется в матрицу а в соответствии с правилами, опи- описанными в условиях задачи. Так обеспечивается рекурсивный обход всех возмож- возможных состояний матрицы а, приводящих к ее заданному конечному состоянию пос- последовательным добавлением недостающих элементов матрицы а. Теперь рассмотрим подробнее процедуры и функции, использованные в про- процедуре Process, Процедура Push добавляет элемент в стек 5: procedure Push(st:integer); begin inc(is); s[is]:-st: end; Функция Рор достает элемент из стека 5: function Pop:integer: begin Pop:-s[is]: s[is]:-O: dec(is): end: Процедура PutAnswerBbiBojwr содержимое стека в порядке поступления элемен- элементов в матрицу а: procedure PutAnswer; begin write(s[is]): for j:-is-l downto 1 do write С '.s[j]): writeln; end: Процедура Add(a, k) добавляет в матрицу а элемент k в соответствии с правила- правилами, описанными в условиях задачи procedure Add(var a:mas:k:integer): var i.j.t : integer; begin i:-l: while k<>0 do begin j:-l: while ((j<-a[i.O]) and (a[i.j]<k)) do inc(j): if (a[i.0]<>0) and (j<-a[i.O]) then begin t:-a[i.j]: a[i.j]:-k: k:-t: inc(i): end
6.5. Задача «Twofive» 341 else begin inc(a[i.0]): a[i.a[i.O]]:-k: k:-0; end; end: end: И, наконец, рассмотрим процедуру Find: procedure Find(var a:mas:i:integer; var k:integer: var ik:integer); var j.l .t.x : integer; begin for j:-i downto 1 do begin x:-a[j.0]; for 1:-1 to a[j.O] do if (a[j.l]<k) and (a[j.l+l]>k) then x:-l: t:-a[j.x]; a[j.x]:-k: k:-t: end: end: Процедуре Find(a, i, k, ik) передается элемент k, который изменил состояние мат- матрицы, и номер строки f, находящейся непосредственно над строкой с элементом k. Процедура Find(a, i, k, ik) находит в матрице я, начиная со строки f, такой элемент k (возвращается также его строка ik), что при его добавлении матрица а перейдет в состояние, в котором она сейчас находится. Для этого в строках с номером.;' (от i до 1) последовательно выполняется следующая работа: Q находится элемент (a[j, t\ или a[j, a[j, 0]]), добавление которого в строку; и привело к опусканию элемента k в строку; + 1. ? обмениваются значения k и найденного элемента a[j, x]. Полный текст решения приведен в конце главы. 6.5. Задача «Twofive» Международная олимпиада по информатике, 2001 Twofive (twofive.in / twofive.out) Секретные сообщения между Санта Клаусом и его маленькими помощниками всегда кодируются на 25-языке. Алфавит 25-языкатакой же, как и латинский, с одним исключением — буква Z отсутствует. То есть алфавит 25-языка со- содержит 25 латинских букв от А до Y в том же порядке, что и в латинском алфавите. Каждое слово в 25-языке состоит ровно из 25 различных букв.
342 Глава 6 • Диаграмма Юнга Слово может быть записано в таблице 5 х 5 no строкам. Например, слово ADJPTBEKQUCGLRVFINSWHMOXY может быть записано так: А В С F Н D Е G I М J К L N О Р Q R S X т и V W Y Корректное слово в 25-языке содержит буквы в порядке возрастания в каж- каждой строке и в каждом столбце. Таким образом, слово ADJPTBEKQUCGLRVFINSWHMOXY - коррект- корректное, a ADJPTBEGQUCKLRVFINSWHMOXY - некорректное (возраста- (возрастающий порядок нарушен во втором столбце, и в третьем тоже). У Санта Клауса есть свой лексикон. Его лексикон — это список корректных слов из 25-языка в порядке лексикографического возрастания с соответ- соответствующим порядковым номером, начинающимся с 1. Например, в этом лек- лексиконе слово ABCDEFGHIJKLMNOPQRSTUVWXY - это слово с поряд- порядковым номером 1, а слово ABCDEFGHIJKLMNOPQRSUTVWXY - это слово с порядковым номером 2. В слове 2 у букв U и Т изменен порядок по сравнению со словом 1. К несчастью, этот лексикон огромен. Напишите программу, которая опре- определяет порядковый номер произвольного заданного слова из этого лекси- лексикона, а также слово, соответствующее заданному порядкому номеру. В этом лексиконе не более 231 слов. Ввод: Входной файл называется twofive.in и состоит из двух строк. Первая строка содер- содержит один символ: W или N Если первая строка содержит символ W, то вторая строка содержит корректное слово из 25-языка, то есть строку из 25 символов. Если первая строка содержит символ N, то вторая строка содержит порядковый номер корректного слова из 25-языка. Вывод: Выходной файл называется twofive.out и содержит одну строку. Если вторая стро- строка входного файла содержит слово из 25-языка, то строка выходного файла со- содержит его порядковый номер. Если вторая строка входного файла содержит по- порядковый номер, то строка выходного файла содержит слово из 25-языка с этим порядковым номером. Пример ввода "w ABCDEFGHIJKLMNOPQRSUTVWXY Пример вывода
6.5. Задача «Twofive» 343 Пример ввода Пример вывода ~Ы ABCDEFGHIJKLMNOPQRSUTVWXY _2 Общая идея решения такова: если буквы в таблице 5 х 5 заменить на соответствую- соответствующие числа (А - 1, В - 2, С - 3, ... Y - 25), то получится, что правильному слову в 25-языке соответствует некоторая диаграмма (рис. 6.1), 1 2 3 6 8 4 5 7 9 13 10 11 12 14 15 16 17 18 19 24 20 21 23 25 1 2 6 8 4 5 1 9 13 10 11 12 14 15 16 17 18 19 24 20 21 22 23 25 Рис. 6.1. Диаграмма для задачи «Twofive» Для поиска номера заданного слова, равно как и для поиска слова по заданному номеру, нужно уметь находить количество вариантов расстановки оставшихся букв, если некоторое количество букв уже зафиксировано на своих позициях. В то же время количество вариантов расстановки оставшихся букв, если уже рас- расставленные буквы составляют диаграмму, можно найти, используя формулу ко- количества диаграмм заданной формы. Например, если мы поставили только букву Л, то имеем табличку A.... Диаграммы, которые можно составить на оставшихся свободными позициях, име- имеют вид: Общее количество таких диаграмм равно количеству диаграмм вида E, 5, 5, 5, 4). Рассмотрим теперь общий случай. Пусть мы корректно расставили первые Мбукв (от А до буквы, которая в латинском алфавите имеет номер N), и при этом оказа- оказалось, чтоя1 букв находится в 1-й строке, n2 букв во второй строке,... n5 букв в 5-й строке (я1 + n2 + яЗ + пА + я5 - N). ТогдаколичестворазличныхдиаграммвидаE - я5,5 - я4,5 - пЗ, 5 - я2,5 - п\) — это и есть количество различных способов расставить все неиспользованные бук- буквы на позициях, оставшихся свободными. Например, приМв 9 для расстановки
344 Глава 6 * Диаграмма Юнга ABCDE FGHI. имеющей количества элементов в строках E,4,0,0,0), количество вариантов кор- корректной расстановки всех оставшихся букв будет равно количеству всех диаграмм вида E,5,5,1,0), а для расстановки ABCD. EFG.. H.... I.... количество вариантов расстановки всех оставшихся букв будет равно количеству диаграмм вида E,4,4, 2,1). Таким образом, можно реализовать функцию Rest, которая должна вычислять количество вариантов расстановки оставшихся букв по заданной расстановке неко- некоторого подмножества букв. Она пытается использовать заранее расчитанные резуль- результаты, а если это невозможно (расставленные буквы не соответствуютдиаграмме), то перебирает все варианты, добавляя новые буквы на все возможные позиции и рекурсивно вызывая себя для получающихся расстановок букв. При этом если все элементы расставлены по своим позициям (nl + я2 + n3 + я4 + я5 ? МахТ 1) значит, перебором найден еще один вариант, и Rest возвращает значение 1. Имея функцию Rest для поиска количества вариантов расстановки букв на всех оставшихся позициях, для решения задачи достаточно поступать следующим об- образом: для поиска номера слова по строке мы поддерживаем значение специаль- специальной величины соответствующим номеру первого по алфавиту слова, которое мы можем сгенерировать по текущему множеству зафиксированных букв. Увеличи- Увеличивая значение буквы или переходя к букве на следующей позиции, мы каждый раз добавляем к нашей величине количество слов, которое было пропущено, чтобы перейти к новой позиции из старой. Для решения обратной задачи — поиска номера по заданной позиции — мы опять наращиваем накапливаемое значение при добавлении букв на соответствующие позиции. Далее подробно описана реализация данного решения. Тело главной программы выглядит естественно просто: begin InputData: if PT-'W then FindNumber else FindString: OutputData: end.
6.5. Задача «Twofwe» 345 Вызываются четыре процедуры: ? InputData — ввести исходные данные; ? Outputdata — вывести результат; ? FindNumber — найти номер заданной строки; ? FindString — найти строку по заданному номеру. Процедура Outputdata выводит в качестве результата найденную строку (FS) или найденный номер (FN): Procedure OutputData; begin assign(output.'twofive.ouf): rewrite(output): if PT-'W' then writeln (FN) else writeln (FS): close(output): end: Процедура InputData вводит исходные данные (номер или строку) и вызывает процедуру Init для инициализации. procedure InputData; begin assign (input.4wof1ve.in1): reset(input): readln(PT): if PT-'W then readln(S) else readln(N); close(input); Init: end: Рассмотрим теперь процедуру Init. Procedure Init: var il.i2.i3.14.15 : integer: begin for il:-0 to 5 do for i2:-0 to 5 do for i3:-0 to 5 do for i4:-0 to 5 do for i5:-0 to 5 do begin State[il.i2.i3.i4.i5]:-0: if not ((il>-i2) and (i2>-i3) and (i3>-i4) and (i4>-i5) ) then Calculated[il.i2.i3.i4.i5]:-true else Calculated[il.i2.i3.i4.i5]:-false: end:
346 Глава 6 * Диаграмма Юнга State[l.O.O.O.O]:-l: Calculated[l.0.0.0.0]:-True; CalcStateE.5.5.5.5): end: Основная задача процедуры Init — заполнить массивы: Q Calculated[iXy i2, 3, г4, S] (значениями типа boolean) ? State[H, i2, i3, i4, i5] (значениями типа longint) и ? Calculated[il, i2, i3, i4, i5]=true указывает, что элемент массива State c индекса- индексами il, i2, i3, i4, i5 уже вычислен tfake — еще не вычислен). Это используется для избегания повторных вычислений и для предустановки предопределен- предопределенных нулевых значений массива State. ? State[il, i2, f3, i4, i5] — это количество различных способов составить из эле- элементов от 1 до il + i2 + t3 + f4 + i5 диаграммы, имеющие форму (il, i2, i3, i4, i5). То есть такие диаграммы, у которых: 11 элементов в первой строке; 12 элементов во второй строке; 13 элементов в третьей строке; 14 элементов в четвертой строке; 15 элементов в пятой строке. Это количество равно сумме количеств диаграмм, получающихся последователь- последовательным удалением по одному из элементов в каждой строке, которая содержит хотя бы один элемент. Соответствующее рекуррентное соотношение имеет вид: если il>-i2>-i3>-i4>-i5>-l. то f(il.12.13.i4.15) - fAl-l.12.13.14.15) + f(il.12-l.i3.i4.i5) + f(il.12.13-l.14.15) + f(il.i2.i3.i4-l.i5) + f(il.12.13.i4.i5-l) если не выполнено соотношение il ^ У2 ? УЗ ? J4 ^ /5 ^ 0, то f(il. i2. /3. У4. iS) - 0 Кроме того, f(il.i2.13.i4.0) - f(il.12.13.i4) fAl.12.13.0) - fAl.i2.i3) f(il.12.0) - f(il.i2) f(il.0) - f(il) Это рекуррентное соотношение вытекает из того факта, что при удалении наи- наибольшего элемента из диаграммы, всегда снова получается диаграмма. Для вычисления этого рекуррентного соотношения используется рекурсивная функ- функция CalcStateE,5,5, 5,5),котораязаполняетвсютаблицу5&йе[0..5,0..5,0..5,0..5, 0..5] вычисленными значениями: function CalcState(nl.n2.n3.n4.n5:integer):longint: var j.k : byte; s : longint:
6.5. Задача «Twofive» 347 begin if Calculated[nl.n2.n3.n4.n5] then begin Ca1cState:=State[nl.n2.n3.n4.n5]; exit; end: s:-0: if nl>-l then inc (s.CalcState(nl-l.n2.n3.n4.n5)): if n2>-l then inc (s.CalcState(nl.n2-l.n3.n4.n5)): if n3>-l then inc (s.CalcState(nl.n2.n3-l.n4.n5)): if n4>-l then inc (s.CalcState(nl.n2.n3.n4-l.n5)): if n5>-l then inc (s.CalcState(nl.n2.n3.n4,n5-l)); State[nl.n2.n3.n4.n5]:-s; Calculated[nl.n2.n3.n4.n5]:-true; CalcState:-s: end: Может возникнуть вопрос: «A какое отношение имеет этот массив State к реше- решению нашей задачи?» Прежде всего это, фактически, количество различных вари- вариантов составление слов, если первые Л^букв уже расставлены по позициям. Для приведенного примера A.... А общее количество таких диаграмм равно StateE, 5, 5, 5, 4). Рассмотрим теперь общий случай. Пусть, мы корректно расставили первые N букв (от А до буквы, которая в ла- латинском алфавите имеет номер N, и при этом оказалось, что nl букв находится в 1-й строке, я2 букв во второй строке, ... я5 букв в 5-й строке (я1 + я2 + яЗ + + w4 + n5 = N). Тогда State[5 - я5, 5 - я4, 5 - яЗ, 5 - n2, 5 - п\] — количество различных спосо- способов расставить все неиспользованные буквы на позициях, оставшихся свободными. Например, при N - 9 для расстановки ABCDE FGHI. имеющей количества элементов в строках E, 4, 0, 0, 0), количество вариантов кор- корректной расстановки все оставшихся букв будет равно количеству всех диаграмм вида E,5,5,1,0), то есть элементу State[5, 5, 5,1, 0]. А для расстановки ABCD. EFG..
348 Глава 6 * Диаграмма Юнга н.... i.... количество вариантов расстановки всех оставшихся букв будет равно State[5,4,4, 2,1 ]. Таким образом, функция Rest, которая должна вычислять количество способов расставить оставшиеся буквы по заданной расстановке некоторого подмножества букв, пытается использовать заранее расчитанные результаты, а если это невоз- невозможно (функция Right возвращает значение fake), то перебирает все варианты, добавляя новые буквы на все возможные позиции и рекурсивно вызывая себя для получающихся расстановок букв. При этом если все элементы расставлены по своим позициям (я1 + n2 + пЪ + я4 + n5^MaxT- 1), значит, перебором найден еще один вариант, и Rest возвращает значение 1. function Rest(Table:Tab:i:byte:Able:TAb:j:byte: Absent:tabu):longint; var q.k.t : byte: s : longint; begin if Right(Table.nl.n2.n3.n4.n5) then begin Rest:-State[5-n5.5-n4.5-n3.5-n2.5-nl]: exit: end; if nl+n2+n3+n4+n5>-MaxT-l then begin Rest:-1; exit: end: for i.-2 to МахТ do Able[i]:-0: for i:-l to МахТ do begin if Tab1e[i]-MaxS then continue; if (z[i.l]<>0) then Able[z[1.l]]:-l: if (z[i.2]<>0) then Able[z[1.2]]:-1: Able[i]:-0: end: s:-0: for k:-2 to MaxT do if Absent[k] then begin i:-k: break; end; for t:-2 to MaxT do Put(Table.i.Able.t.Absent); Rest:-s: end; ЗАМЕЧАНИЕ Отладка алгоритма в целях удобства проводилась на таблице из букв размером 3 х 3. Соот- Соответственно были введены константы: Rang = 5 для оригинальной задачи, Rang = 3 для отладоч- отладочного образца; MaxS = Rang x Rang; MaxT = MaxS - 1 (последняя буква всегда одна и та же). Функция Rest имеет следующие параметры: О ТаЫе — текущая расстановка букв; ? i — какое значение было поставлено последним;
6.5. Задача «Twofive» 349 ? Able — какие позиции в расстановке ТаЫе сейчас свободны; Q j — на какую позицию было поставлено последнее значение; ? Absent — какие элементы отсутствуют в текущей расстановке; ? Table[k] равно номеру буквы на позиции k или MaxS, если позиция не запол- заполнена; ? Able[k] равно 1, если позиция k свободна, и 0, если позиция k занята; ? Absent[k] равно TRUE, если буквы с номером k нет в текущей расстановке ТаЫе (и FALSE в противном случае). Функция Rest по полученной расстановке строит массивы Able и Absent и вызы- вызывает процедуру Put: procedure Put(Table:Tab;i:byte:Able:TAb:j:byte:Absent:tabu); begin if (Able[j]-l) and (i>Table[P[j.l]]) and (i>Table[P[j.2]]) then begin if (z[j.l]<>O) then Able[z[j.l]]:-1: if (z[j.2]<>0) then Able[z[j.2]]:-1: Able[j]:-O; Table[j]:-i: Absent[Table[j]]:-false: inc(s.Rest(Table.i.Able.j.Absent)): end; end: Процедура Put проверяет, можно ли поставить букву с номером i на позицию/ и если можно, то ставит, корректируя соответствующим образом массивы ТаЫе, Able и Absent, a затем вызывает функцию Restjxnn вновь полученной расстановки, с накоплением суммы всех результатов Rest в переменной 5. Функция RigfU вычисляет, сколько букв в каждой строке расстановки (n\, n2,.., п5), находит букву с максимальным номером в расстановке (Мах) и общее количе- количество букв в расстановке (n). Если Max = я, то расстановка «правильная» в том смысле, что количество способов расставить все оставшиеся буквы можно взять в таблице State на позиции [5 - я5, 5 - п4,5 - пЗ, 5 - n2,5 - я1]. function Right(Table:Tab:var nl.n2.n3.n4.n5:integer):boolean: var i.Max.n.r : integer: t : array[1..5] of integer; begin for i:-l to Rang do t[1]:-0: for i:-l to Rang do for r:-l to Rang do if Table[(i-l)*Rang+r]<>MaxS then inc(t[i]): nl:-t[l]: n2:-t[2]: n3:-t[3]: n4:-t[4]: n5:-t[5];
350 Глава 6 * Диаграмма Юнга Max:-Table[2]: n:-0; for i:-l to МахТ do if Table[i]<>MaxS then begin if Table[i]>Max then Max:-Table[i]: inc(n): end: Right:» (Max-n); end; Теперь, когда мы разобрались с функцией Rest, которая по заданной расстановке букв может вычислять количество способов расставить все оставшиеся буквы, мы можем рассмотреть процедуру FindNumber — нахождение числа по заданной строке: procedure FindNumber: var i.j.k.p.nl.n2.n3.q: byte; begin FN:-1: Init2: j:-2; for 1-2 to MaxS-2 do begin Table[i]:-j: while Table[i]<(Ord(s[1])-OrdCA')+l) do begin inc(FN.Rest(Table.i.Able.j.Absent)): inc(Table[i]); repeat q:-l: whi1e(q<-i-l) and (Table[q]<>Table[i]) do Inc(q): if q<-i-l then inc(Table[i]): until q=i; end: j:-MinAble(i+l): end: end: В переменной ЯУнакапливается ответ. Процедура Init2 инициализирует начальными значениями массивы ТаЫе, АЫе и Absent. Procedure Init2; var i : byte: begin Table[O]:-O: for i:-2 to MaxS do Table[i]:-MaxS;
6.5. Задача «Twofive» 351 Table[l]:-1: for i:-2 to MaxT do Absent[i]:=true; for i:-2 to MaxT do if Table[i]<>MaxS then Absent[Table[i]]:-false: for i:-l to MaxT do Able[i]:-0: Able[2]:-1: Able[Rang+l]:-l: end; Далее выполняется цикл по всем позициям расстановки i от 2 до MaxS - 2. На позицию i ставится буква с номером/ Сначала (для позиции 2)j устанавливается равным 2, а при всех последующих — вычисляется с помощью специальной функ- функции MinAble(i + 1) — найти букву, минимальную из отсутствующих, которую мож- можно поставить на позицию i + 1. Пока поставленная буква меньше, чем буква в исходной строкеGЫ>&[*] < (Ord(s[i]) - - Ord( 'A *) + 1)), накапливаем в переменную FN количество способов, которыми можно расставить оставшиеся буквы, вызывая описанную ранее функцию Rest и изменяем букву в позиции i на следующую возможную (больше предыдущего значения буквы на текущей позиции, но не равное значениям букв на предыду- предыдущих позициях). Функция MinAble имеет следующий вид: function MinAble(i:integer):integer: var j.11.12: integer; mas : array [O..MaxS] of byte; Good.6oodl.Good2 : boolean: begin for j:=l to maxT do mas[j]:-0: for j:-l to MaxT do Mas[Table[j]]:-l: for j:-2 to MaxT do begin Goodl:-(Mas[j]-0): Good2:-(i<-Rang) and (j>Table[i-lJ) or (i>Rang) and (((i mod Rang)-1) and (j>Table[i-Rang]) or ((i mod Rang)<>l) and (j>Table[i-l]) and (j>Table[i-Rang])); Good:-Goodl and Good2: if Good then begin MinAble:-j: exit: end: end: end: Функция MinAble принимает в качестве параметра номер позиции i, на которую в массиве ТаЫе нужно поставить текущую букву. Функция MinAble перебирает все буквы от 2-й (то есть В) до буквы MaxT(ro есть X). Если позиция i свободна и число; больше соседей слева и сверху (если таковые имеются), Toj — то, что нужно.
352 Глава 6 * Диаграмма Юнга Теперь рассмотрим функцию FindString, которая по заданному номеру находит строку: procedure FindString: var i.j.q : byte; add : longint; begin Init2: FN:-0: Table[2]:-2: Able[2]:-0: Able[3]:-1: i:-2: J:-2; Add:-Rest(Table.i.Able.j.Absent); repeat while (FN+Add>-N) and (i<MaxT) do begin inc(i); SetTable(i.2): J:-Table[i]: Add:-Rest(Table.i.Able.j.Absent): end: while (FN+Add<N) do begin inc(FN.Add); SetTable(i.Table[i]+l): J:-Table[1]: Add:-Rest(Table.i.Able.j.Absent); end: until i-MaxT: FS:-'A': for i:-2 to МахТ do FS:-FS+char(Table[i]+ord('A')-l): FS:-FS+Last[Rang]; end: Вычислив в переменной Add количество различных перестановок, начинающих- начинающихся с букв AB, процедура FindString работает до тех пор, пока не построит всю стро- строку до позиции i - МахТ Пока (FN+Add)>N и (i<MaxT) Переходим к следующей позиции; устанавливая корректно символ в этой позиции. перевычисляем Add - количество вариантов построения последовательность букв с таким префиксом Пока (FN+Add<N) увеличивает FN на занесение Add. устанавливаем следующую возможную букву в текущей позиции i. перевычисляем Add Выйдя из цикла, формируем символьную строку по построенному числовому мас- массиву ТаЫе. Для установки возможной буквы в заданную позицию i начиная с заданного зна- значения х используется процедура SetTable(i, x):
6.6. Решения задач 353 procedure SetTable(i.x:integer): var Correct : boolean: q : integer: begin repeat Correct:-(i<-Rang) and (x>Tab1e[i-l]) or (i>Rang) and (((i mod Rang)-1) and (x>Table[i-Rang]) or ((i mod Rang)<>l) and (x>Table[i-l]) and (x>Table[i-Rang])): q:-l: while (q<-i-l) and (Table[q]<>x) do Inc(q): if (q<-i-l) or (not Correct) then inc(x): until (q-i) and Correct: Table[i]:-x; end: Полный текст решения задачи приведен в конце главы. Для перехода к размерности 3 х 3 требуется модификация исходного текста в трех местах, отмеченных соответственно {1}, {2}, {3}. 6.6. Решения задач Листинг 6.1. Текст программы к задаче «Склад» program ioOld2t3: type Mas - аггауСО..13.0..133 of integer; Masl- array[1..13] of integer: var R.i.j.is.k.st.ik : integer; s : Masl; a : Mas: procedure Inputdata: begin for i:-0 to 13 do for j:-0 to 13 do a[i.j]:-0: assign(input.'depot.in'); reset(input): readln(R): for i:-l to R do begin read(a[i.0]): for j:-l to a[i.0] do read(a[i.j]): end: close(input): . продолжение &
354 Глава 6 * Диаграмма Юнга Листинг 6.1 (продолжение) is:-O; end; procedure Push(st:integer): begin inc(is): s[is]:*st: end: function Pop:integer: begin Pop:-s[is]; s[is]:=0: dec(is): end; procedure PutAnswer: begin write(s[is]); for j:-is-l downto 1 do write C '.s[j]): writeln; end: procedure Add(var a:mas:k:integer): var i.j.t : integer: begin 1:-1: while k<>0 do begin j:-l: while ((j<-a[i.0]) and (a[i.j]<k)) do inc(j): if (a[i.0]<>0) and (j<-a[i.O]) then begin t:-a[i.j]: a[i.j]:-k: k:-t: inc(i): end else begin inc(a[i.0]): a[i.a[1.0]]:-k: k:-0: end: end: end: procedure Find(var a:mas:i:integer: var k:integer: var ik:integer):
6.6. Решения задач 355 var j.l.t.x : integer: begin for j:-i downto 1 do begin x:-a[j.0]: for 1:-1 to a[j.0] do if (a[j.l]<k) and (a[j.l+l]>k) then x:-l: t:-a[j.x]: a[j.x]:-k; k:-t: end: end: procedure Process(a:mas): var i :integer: begin if a[1.0]-0 then begin PutAnswer: exit: end; for i:-R downto 1 do if (a[i.0]<>0) and (a[i+l.a[i.0]]-0) then begin k:-a[i.a[i.0]]: 1k:-1: Find(a.i-l.k.ik): Push(k): a[ik.a[ik.0]]:-0: dec(a[ik.O]): Process(a): k:-Pop: Add(a.k): end: end: begin InputData: assign(output.'depot.out'): rewrite(output): Process(A): close(output): end. Листинг 6.2. Текст программы к задаче «Twofive» {*R+} program ioOldlt3: Const {1} { Rang-3: . продолжение &
356 Глава 6 • Диаграмма Юнга Листинг 6.2 (продолжение) Z : array[l..MaxT.1..2] of byte - (B.4).C.5).F.0).E.7). F.8).@.0).(8.0).@.0)): Р : array[l..MaxT.1..2] of byte - (@.0).A.0).B.0).A.0). D.2).E.3).D.0).G.5)): Rang-5; MaxS-Rang*Rang; Max>MaxS-l; Z ; array[l..MaxT.1..2] of byte - ( 2. 6).( 3. 0).( 4. 0).( 5. 0).@.0). ( 7.11).( 8. 0).( 9. 0).A0. 0).@.0). A2.16).A3. 0).A4. 0).A5. 0).@.0). A7.21).A8. 0).A9. 0).B0. O).(O.O). B2. 0).B3. O).B4.O). (O.O) P : array[l..MaxT.1..2] of byte - @.0).A.0).B.0).C.0).D.0). A.0).B.6).C.7).D.8).(9.5). F.O).G.11).(8.12).(9.13).(lO.14). A1.0).A2.16).A3.17).A4.18).A5.19). A6.0).A7.21).A8.22).A9.23) Last : string - 'ADIPY'; Туре Tab - St - Tabu- Var Table Able kk.N.f S.FS PT nl.n2 array array array :N n3.n4 Calculated [0 [0 [0 .n5 ..MaxS] of byte; ..MaxS] of longint; ..MaxS] of boolean; : Tab; : TAb; : longint; : string[25]; : char; : integer; array [0..5.0..5.0..5.0..5.0..5] of boolean
6.6. Решения задач 357 State : array [0..5.0..5.0..5.0..5.0..5] of longint: Absent : tabu: Procedure OutputData: begin assign(output.'twofive.out'): rewrite(output): if PT-'W' then writeln (FN) else writeln (FS): close(output): end: function CalcState(nl.n2.n3.n4.n5:integeD:longint: var j.k : byte: s : longint: begin if Calculated[nl.n2.n3.n4.n5] then begin CalcState:-State[nl.n2.n3.n4.n5]: exit: end: s:-0: if nl>-l then inc (s.CalcState(nl-l.n2.n3.n4.n5)): if n2>-l then inc (s.CalcState(nl.n2-l.n3.n4.n5)): if n3>-l then inc (s.CalcState(nl.n2.n3-l.n4,n5)): if n4>-l then inc (s.CalcState(nl.n2.n3.n4-l.n5)): if n5>-l then inc (s.CalcState(nl.n2.n3.n4.n5-l)): State[nl.n2.n3.n4.n5]:-s: Calculated[nl.n2.n3.n4.n5]:-true: CalcState:-s: end: Procedure Init: var il.i2.i3.i4.i5 : integer: begin for 11:4) to 5 do for i2:-0 to 5 do for i3:-0 to 5 do for i4:-0 to 5 do for i5:-0 to 5 do begin State[1l.12.13.14.15]:-0: if not ((il>-i2) and (i2>-i3) and (i3>-i4) and (i4>-i5) ) then Calculated[il.i2.i3.i4.i5]:-true продолжение &
358 Глава 6 * Диаграмма Юнга Листинг 6.2 (продолжение) else Calculated[1l.i2.i3.i4.i5]:-false: end; State[l.O.O.O.O]:-l: Calculated[1.0.0.0.0]:-True: CalcStateE.5.5.5.5): end; procedure InputData: begin assign (input.'twofive.in'): reset(input): readln(PT); if PT-'W then readln(S) else readln(N): close(input); Init: end: Procedure Init2; var i : byte. begin Table[0]:-0: for i:«2 to MaxS do Table[i]:-MaxS; Table[l]:-1: for i:-2 to МахТ do Absent[i]:-true; for i:-2 to МахТ do if Table[i]<>MaxS then Absent[Table[i]]:-false; for 1:-1 to МахТ do Able[i]:-0: Able[2]:-1: Able[Rang+l]:-l: end: function Rest(Table:Tab:i:byte;Able:TAb:j:byte; Absent:tabu):longint: var q.k.t : byte: s : longint: function Right(Table:Tab:var nl.n2.n3.n4.n5:integer)boolean; var i.Max.n.r : integer: t : array[1..5] of integer: begin for i:-l to Rang do t[i]:-0: for i:-l to Rang do for r:-l to Rang do if Table[(i-l)*Rang+r]<>MaxS then inc(t[i]):
6.6. Решения задач 359 nl:-t[l]: n2:-t[2]: n3:-t[3]: n4:-t[4]: n5:-t[5]; {2} {n4:-0: n5:-0:} Max:-Tab1e[2]; n:-0: for i:-l to МахТ do if Table[i]<>MaxS then begin if Table[i]>Max then Max:-Table[i]: inc(n); end: Right:-(Max-n); end: procedure Put(Table:Tab;i:byte:Able:TAb:j:byte:Absent:tabu): begin if (Able[j]-l) and (i>Table[P[j.l]]) and (i>Table[P[j.2]]) then begin if (z[j.l]<>0) then Able[z[j.l]]:-1: if (z[j.2]<>0) then Able[z[j.2]]:-1: Able[j]:-0: Table[j]:-i: Absent[Table[j]]:-false: inc(s.Rest(Table.i.Able.j.Absent)): end; end: begin if Right(Table.nl.n2.n3.n4.n5) then begin {3} { Rest:-State[3-n3.3-n2.3-nl.0.0]:} Rest:-State[5-n5.5-n4.5-n3.5-n2.5-nl]: exit: end: if nl+n2+n3+n4+n5>41axT-l then begin Rest:-1: exit: end: продолжение &
360 Глава 6 * Диаграмма Юнга Листинг 6.2 (продолжение) for i:-2 to MaxT do Able[i]:-O: for i:-l to MaxT do begin if Table[i]-MaxS then continue: if (z[i.l]<>O) then Able[z[1.l]]:-1: if (z[i.2]<>0) then Able[z[1.2]]:-1: Able[i]:-O: Absent[Table[i]]:-false: end: s:-0: for k:-2 to MaxT do if Absent[k] then begin i:-k: break: end: for t:-2 to MaxT do Put(Table.i.Able.t.Absent): Rest:-s: end: procedure FindNumber: function MinAble(i:integer):integer: var j.il.i2: integer: mas : array [O..MaxS] of byte: Good.Goodl.Good2 : boolean; begin for j:-l to maxT do mas[j]:-O: for j:-l to MaxT do Mas[Table[j]]:-l; for j:-2 to MaxT do begin Goodl:-(Mas[j]-0); Good2:-(i<-Rang) and (j>Table[i-l]) or (i>Rang) and (((i mod Rang)-1) and (j>Table[i-Rang]) or ((i mod Rang)<>l) and (j>Table[i-l]) and (j>Table[i-Rang])): Good:4Soodl and Good2: if Good then begin MinAble:-j:
6.6. Решения задач 361 exit: end: end: end: var i.j.k.p.nl.n2.n3.q : byte: begin FN:-1: Init2: j:-2; for i:-2 to MaxS-2 do begin Table[i]:-j: while Table[i]< (Ord(s[i])-OrdCA')+l) do begin inc(FN.Rest(Table.i.Able.j.Absent)): inc(Table[i]); repeat q:-l: while (q<-i-l) and (Table[q]oTable[i]) do Inc(q): if q<-i-l then inc(Table[i]): until q-i: end: j:^inAble(i+l): end: end: procedure SetTable(i.x:i nteger): var Correct : boolean: q : integer: begin repeat Correct:-(i<-Rang) and (x>Table[i-l]) or (i>Rang) and (((i mod Rang)-1) and (x>Table[i-Rang]) or ((i mod Rang)ol) and (x>Table[i-l]) and (x>Table[i-Rang])): q:-l: while (q<-i-l) and (Table[q]<>x) do Inc(q): if (q<-i-l) or (not Correct) then inc(x): until (q-i) and Correct: . продолжение &
362 Глава 6 * Диаграмма Юнга Листинг 6.2 (продолжение) Table[i]:-x: end: procedure FindString; var i.j.q : byte: add : longint: begin Init2: FN:-0: Table[2]:-2; Able[2]:-0; Able[3]:-1: i:-2: J:-2: Add:-Rest(Table.i.Able.j.Absent): repeat while (FN+Add>-N) and (i<MaxT) do begin inc(i): SetTable(i.2): J:-Table[i]: Add:-Rest(Table.i.Able.j.Absent): end; while (FN+Add<N) do begin inc(FN.Add); SetTable(i.Table[i]+l); J:-Table[i]; Add:-Rest(Table.i.Able.j.Absent): end; until i-MaxT; FS:-*A*; for i:-2 to MaxT do FS:-FS+char(Table[i]+ordCA')-l); FS:-FS+Last[Rang]; end: begin InputData: if PT-'W then FindNumber else FindString; OutputData: end.
Литература 1. Вальвачев А. Я., Крисевич В. С. Программирование на языке ПАСКАЛЬ для персональных ЭВМ ЕС. - Мн.: Высш. шк., 1989. 223 с. 2. Долинский М. С. Алгоритмизация и программирование на Turbo Pascal: от простых до олим- пиадных задач. - СПб: Питер, 2004. 240 с. 3. Емеличев В. A., Мельников О. Я, Сарванов В. Я., Тышкевич Р. Я. Лекции по теории графов. - M.: Наука, 1990.384 с. 4. Зуев Е. А. Программирование на языке TURBO PASCAL 6.0,7.0. - M.: Радио и связь, 1993.384 с. 5. Касьянов В, H., Сабельфельд В. К. Сборник заданий по практикуму на ЭВМ. - M.: Наука, 1986. 272 с. 6. Кирюхин В. M., Лапунов А, В., Окулов С. М. Задачи по информатике: Международные олим- олимпиады 1989-1996. - M.: ABF, 1996. 272 с. 7. КнутпД. Искусство программирования для ЭВМ — Т. 1. Основные алгоритмы. — M.: Мир, 1976. 735 с. 8. Кнут Д. Искусство программирования для ЭВМ — Т. 2. Получисленные алгоритмы. 3-е изд. — M.: Издательский дом «Вильямс», 2000.828 с. 9. КнутД. Искусство программирования для ЭВМ - T3. Сортировка и поиск - M.: Мир, 1978.844 с. 10. Кормен 7*., Лейэерсон 4., Ривест Р. Алгоритмы: построение и анализ. — М.: М ЦН М 0,1999.960 с. 11. Котов В. M., Волков Я. A., Лапо А. Я. Методы алгоритмизации: Учеб. пособие для 9 класса общеобразовательной школы с углубленным изучением информатики. - Мн.: Народная асвета, 1997.160 с. 12. Котов В. M., Волков Я. A., Харитонович А. Я. Методы алгоритмизации: Учеб. пособие для 8 класса общеобразовательной школы с углубленным изучением информатики. — Мн.: На- Народная асвета, 1996.127 с. 13. Котов В. Af., Мельников О. Я. Методы алгоритмизации: Учеб. пособие для 10-11 классов общеобразовательной школы с углубленным изучением информатики. — Мн.: Народная ас- асвета, 2000. 221 с. 14. Кристофидес Н. Теория графов: алгоритмический подход. — M.: Мир, 1988. 250 с. 15. Липский В. Комбинаторика для программистов. — M.:, Мир, 1988.368 с. 16. Новиков Ф. А. Дискретная математика для программистов. — СПб: Питер, 2000.304 с. 17. Овсянников А. П., Овсянникова Т. В., Марченко А. П., Прохоров Р. В. Избранные задачи олим- олимпиад по информатике. — M.: Тровант, 1997.95 с. 18. Окулов С. М. Конспекты занятий по информатике (алгоритмы на графах): Учеб. посо- пособие. — Киров: Вятский госпедуниверситет, 1996. 117 с. 19. Шень А. X. Программирование: теоремы и задачи. — M.: МЦМНО, 1995. 264 с.
Алфавитный указатель А алгоритм, 51 Крускала, 47 Прима, 47 Форда-Фалкерсоиа, 28 Хаффмана, 120 ACM ICPC Region Central European 1999,386 Northern Subregion 2000, 376 Central Subrcgion 2001, 382 B вершина внешняя, 77 внутренняя, 77 вес дуг, 13 Г граф взвешенный, 12 ориентированный, 12 двоичный поиск, 366 дерево битово-индексированное, 82,137 корневое, 77 отношений, 78 сортировки, 81 сумм, 82 дерево, 76, 78 дихотомический поиск, 366 дихотомия, 366 задача «Entropy», 124 «Network», 61 <Nextrec>, 126 «Parliament», 113 «Pre-Post-erous», 151 «Twofive», 341 «Блокада», 198 «Борозды», 246 «Дерево», 110 «Игра, 21 «Кодирование», 120 «Кубики, 18 «Метро», 59 «Новогодняя вечеринка, 16 о максимальном паросочетании в двудольном графе», 14 «Оппозиция», 92 «Падение», 235 «Склад», 336 «Стены», 192 «Тетраэдр», 186 и исток, 15 олимпиада Студенческие ACM: ACM Northwestern Europe Programming Contest (NWERC) 2000, 94 2002, 149
Алфавитный указатель 365 ACM Central Europe Programming Contest (CERC) 1999, 230, 383 ACM Northeastern Europe Programming Contest (NEERC) 1998, 207, 296 2000, 373 2001,61, 113,211,379 ACM Southeastern Europe Programming Contest (SEERC) 2000, 89, 96 ACM Pacific Northwest Programming Contest (PacNW) 1999, 99 ACM North Central North America Programming Contest (NCNA) 1997,85 ACM East Central North America Programming Contest (ECNA) 2000,106 2001, 132, 220 2002, 145, 151 ACM Mid-Central USA Programming Contest (MCUSA) 1996, 241 1999, 215 2000, 117 2001, 147 ACM Greater New York (GNY) 2000, 124, 250 ACM South Central USA Programming Contest (SCUSA) 2002, 254 ACM South America Programming Contest (SAmerica) 2002, 255-257 Международная (IOI) 1996, 319 2000, 192 2001, 137, 308, 314, 336, 341 Американская (USACO) 2002, 16, 55, 126 Белорусская 1994, 364 2001, 21 2002,59, ПО Гомельская 1997,366 1999,47 2000, 300, 369 2002, 120 Российская, командная 1999, 225 2000, 18 2001, 198 Российская 1999, 246 2001, 92 Санкт-Петербургская, командная 2000, 203, 305 Централыюевропсйская 2000,235 п поток в сетях, 12 входящий, 15 исходящий, 15 максимальный, 12 потока сохранение, 13 потомок, 77 предок, 77 пропускная способность, 14 путь дополняющий, 26 увеличивающий поток, 26 Р редактор. См. AppBrowser С сортировка быстрая, 54 сток, 13 т текст программы Игра, 37 Кубики, 35 Новогодняя вечеринка, 33
Михаил СеменовичДолинский Решение сложных и олимпиадных задач по программированию: Учебное пособие Главный редактор Е. Строганова Заведующий редакцией А. Кривцов Руководитель проекта А. Адаменко Литературный редактор К. Кноп Художник Е. Дьяченко Корректоры Н. Лукина, А. Моносов Верстка Л. Родионова Лицензия ИД№05784 от07.09.01. Подписано в печать05.09.05. Формат 70X100/16. Усл. п. л. 29,67. Тираж 3000 экз. Заказ№69. ООО «Питер Принт». 194044, Санкт-Петербург, пр. Б. Сампсониевский, 29a. Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 953005 — литература учебная. Отпечатано с готовых диапозитивов в ФГУП «Печатный двор» им. А. М. Горького Федерального агентства по печати и массовым коммуникациям. 1971 Ю.Санкт-Петербург, Чкаловский пр., 15.