Книга
Книга
Введение
В настоящее время «олимпиадное программирование» переживает настоящий бум среди студентов и школьников. Тому есть множество причин. Перечислим наиболее важные из них.
Программирование — чрезвычайно увлекательная сфера деятельности, в то же время обеспечивающая достойный уровень оплаты труда во всем мире, в том числе и в странах СНГ. Последнее связано со стремительной компьютеризацией всех сфер жизнедеятельности человека и открытием крупнейшими фирмами (Intel, IBM, Motorola и т. д.) центров исследований и коммерческих разработок в области программного обеспечения на территории России, Беларуси и других стран СНГ.
При изучении программирования и других компьютерных наук молодой человек получает «конвертируемые» знания, позволяющие ему работать не только на родине, но и практически в любой стране мира.
Олимпиады по программированию для студентов и школьников имеют более чем двадцатилетнюю историю. При этом высокие результаты предыдущих поколений студентов и школьников стимулируют занятие олимпиадным про- грамл<ированием все новых и новых молодых людей.
Приведем только несколько цифр. С 1997 года Россия была представлена на — Международной олимпиаде школьников по информатике (International Olympiad in Informatics, IOI) тридцатью двумя школьниками (восемь олимпиад, по 4 школьника в команде на каждой олимпиаде). Все эти школьники завоевали медали, причем подавляющее большинство — золотые или серебряные. Аналогичный результат имеет только одна страна в мире — Китай.
С 1996 года вузы России и других стран СНГ начали принимать участие в командном студенческом первенстве мира по программированию, проводимом под патронажем ЛСМ (Association for Computing Machinery) — одной из крупнейших и старейших ассоциаций компьютерных профессионалов. Результаты и здесь выглядят впечатляюще. В течение последних шести лет команды Санкт-Петербургских вузов (СПбГУ и СПбГУТМО) трижды становились чемпионами мира по программированию. При этом в финале 2005 года второе, третье н девятое места
(а в финале 2004 года — первое, четвертое и восьмое) были заняты командами российских вузов. Такими стабильно высокими результатами своих студентов не может похвастаться ни одна страна в мире.
Понятно, что эти успехи достигнуты благодаря педагогическим кадрам Санкт- Петербурга, Москвы, Саратова, Нижнего Новгорода, Ижевска, Петрозаводска, Перми и других городов России. В них знания передаются «изустно» от ведущих преподавателей к олимпиадникам, а также от одного поколения олимпиадников к другому.
В то же время во множестве городов России и других стран СНГ наблюдается острейшая нехватка как педагогических кадров, так и литературы по подготовке к олимпиадам и решению сложных олимпиадных задач по программированию для студентов и школьников, — и это на фоне огромного интереса к олимпиадам со стороны школьников, студентов и преподавателей. Достаточно сказать, что в 2004 году за право попасть в Северо-Восточный полуфинал студенческого первенства мира по программированию (NEERC) в четырнадцати (!!!) четвертьфиналах соревновалось более 630 команд более чем из 210 вузов. Сопоставимы по числу участников в олимпиадах и соревнования для школьников по информатике.
Данная книга написана с целью восполнить указанный пробел в отечественном книгоиздании. Она ориентирована на следующие категории читателей:
участников командных чемпионатов мира по программированию для студентов, проводимых иод эгидой АСМ;
тренеров таких студенческих команд;
преподавателей вузов, ведущих учебные дисциплины «Теория алгоритмов», «Методы алгоритмизации», «Структуры данных», ^Олимпиадные задачи» и т. д.;
участников региональных и Всероссийских олимпиад но информатике для школьников, а также русскоязычных участников национальных олимпиад в странах СНГ и Балтии;
тренеров региональных команд по информатике;
учителей школ, ведущих занятия по информатике на углубленном и профильном уровнях;
школьников, изучающих информатику на углубленном и профильном уровнях.
Автору представляется полезным знакомство читателя с материалом его предыдущей книги «Алгоритмизация и программирование на 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.
ЗАМЕЧАНИЕ Условия задач по правилам студенческих олимпиад выдаются участникам на английском языке. Мы перевели все условия на русский, однако всем тем, кто собирается принимать участие в студенческих соревнованиях по программированию, настоятельно рекомендуем потренироваться в решении англоязычных задач как на сайте http://dl.gsu.unibel.by, так и на других тестирующих системах, например, на http://acm.timus.ru или на http://acm.uva.es.
Другое используемое название — <табло Янга».
1111От издательства
11
11
От издательства
Седьмая глава книги, содержащая описание шести задач, решаемых методом дихотомии, помещена (в качестве дополнения) на сайт издательства http://www.piter.com в виде pdf-файла. Алфавитный указатель содержит термины из этой главы с ссылкой на нумерацию страниц этого файла.
Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Литер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
Подробную информацию о наших книгах вы найдете на веб-сайте издательства http://www.piter.com.
Глава 1 - Максимальныйпоток
Данная глава посвящена теме «Максимальный поток» и имеет следующую структуру. Вначале на простейших примерах вводится понятие «максимальный поток». Затем приводится формальная постановка задачи, примеры олимпиадных задач и методики их сведения к задаче о «максимальном потоке». Далее объясняется алгоритм Форда-Фалкерсона для построения максимального потока и описывается его реализация, позволяющая решить все приведенные задачи.
1. Примеры задач на максимальный поток
Ориентированный взвешенный граф служит математической моделью широкого класса задач на нахождение потоков в сетях. Для примера рассмотрим граф с четырьмя вершинами, пронумерованными от 1 до 4:
Рис. 1.1. Граф с четырьмя вершинами
Веса C[iJ\ дуг графа:
1
2
3
4
1
0
0
10
10
2
0
0
10
0
3
0
0
0
0
4
0
0
0
0
Пусть веса дуг (от вершин 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).
В общем случае пропускные способности разных дуг, безусловно, могут различаться. Более того, вместо потоков жидкого вещества (нефти) можно рассматривать движение тока по проводам и многое другое.
Для корректности применения описанного ниже метода нахождения максималь* ного потока в сетях необходимо, чтобы в физической постановке задачи соблюдались следующие условия.
Ограничение пропускной способности:/[I/, V] <c[U, V], где f[U,V\ — поток от вершины U к вершине V, а с[ С/, V] — пропускная способность дуги от вершины U к вершине V.
Граф должен иметь ровно одну вершину-исток (в нее нет входящих дуг, а есть только исходящие из нее), и ровно одну вершину-cmozc (нет исходящих из нее дуг, а есть только входящие в нее).
Сохранение потока: для любой вершины графа, кроме истока и стока, сумма потоков по всем входящим в вершину дугам равна сумме потоков по исходящим из нее дугам
Для нашей задачи о нефтедобывающих вышках и нефтеперерабатывающих заводах не выполнено условие 2 — у нас два истока (вершины 1 и 2) и два стока (вершины 3 и 4). Для того чтобы обеспечить выполнение условия 2, в таких случаях искусственно добавляют исток и сток. Например, для нашей задачи исток — вершина 0 — общий (подземный) резервуар нефти, а сток — вершина 5 — общее «виртуальное» хранилище потребленной нефти. Тогда граф имеет вид, показанный на рис. 1.2:
Рис. 1.2. Граф с добавленными вершинами (истоком и стоком)
Важно еще правильно определить веса введенных дуг, то есть «пропускные способности» труб от вершины 0 к вершинам 1 и 2 и от вершин 3 и 4 к вершине 5. В данной задаче корректно ввести понятие ^неограниченных^ весов. На практике эта переменная получает максимально возможное значение (maxlongint, например, в случае целочисленных весов c[ij]).
Классической задачей, сводящейся к задаче о потоках в сетях, является также задача о максимальном паросочетании в двудольном графе. Пусть имеется граф (рис. 1.3):
Рис. 1.3. Пример графа для задачи о максимальном паросочетании
Пусть вершины 1 и2 соответствуют женихам, вершины Зи4 — невестам, a c[ij] = 1 в том случае, если i и j готовы стать супругами. Требуется узнать максимально возможное количество супружеских пар. Правильный ответ в данных условиях — 2. Сочетаться браком могут жених 1 с невестой 4 и жених 2 с невестой 3.
Формальная постановка задачи
В примерах, приведенных выше, граф задавался матрицей весов своих дуг. Для формальной записи алгоритма удобно, кроме матрицы весов c[i,刃,оперировать также списками дуг графа из каждой вершины, которые представлены в следующем виде:
ka[i] — количество дуг из вершины i;
a[ij] — номер вершины, в которую ведет /я дуга из вершины £
Например, для графа из 6 вершин массивы ka[i] и a[ij] таковы:
/012345
ка[/]221110
Рис. 1.4. Граф с шестью вершинами
Таблица 1.1. Значения а[/, /]
/
/= 1
/ = 2
0
1
2
1
3
4
2
3
3
5
4
5
5
здесь i — номер строки (вершины),/ — номер столбца (дуги).
Пусть задан граф с N + 2 вершинами (вершина 0 — исток, вершина N+ 1 — сток и «промежуточные» вершины от 1 до N).
Результат решения задачи о максимальном потоке — это построение такого массива f[ij] (f[ij] — поток по дуге из вершины i в вершинуj), что сумма f[OJ] (для всех j от 1 до N + 1) (исходящий поток) максимальна.
Понятно, что то же максимальное значение имеет и другая величина — входящий поток — сумма/[i,1] (для всех i от 0 до N).
Прежде чем перейти к изложению алгоритма решения задач о максимальном потоке, рассмотрим несколько конкретных задач с реальных олимпиад и сведем их к задаче о максимальном потоке.
Необходимо также отметить, что при решении задач основная трудность часто заключается именно в формализации задачи и организации ввода исходных данных таким образом, чтобы можно было применять стандартный алгоритм нахождения максимального потока.
Для решения всех трех задач, условия которых приведены далее («Новогодние вечеринки», «Кубики», «Игра»), тело программы выглядит следующим образом:
begin
InputData:
MaxFlow:
OutputData: end.
При этом если процедуры InputData и OutputData отражают специфику задачи, то процедура MaxFlow абсолютно идентична для всех трех решений.
Задача «Новогодняя вечеринка»
USA Computing Olympiad, 2002
Новогодняя вечеринка (party.in / party.out))
Группа из N (3 < N< 200) коров устраивает новогоднюю вечеринку. Каждая корова может приготовить несколько различных видов пищи, измеряемой в единицах, называемых «блюдо». Всего имеется D (5 < D < 100) различных видов пищи. Каждый вид пищи обозначается числом в диапазоне от 1 до D.
Координатор коровьей вечеринки хочет максимизировать общее количество блюд, которые будут принесены на вечеринку, но имеет установленный лимит на количество блюд каждого типа. Каждая корова может принести К(1<К<5) блюд, но они должны отличаться друг от друга. К примеру, одна корова не может принести 3 пирожка с говядиной, но может принести пирожок, хлеб и вкусную люцерну в апельсиновом соусе. Каково максимальное количество пищи, которую коровы могут принести на вечеринку? Ввод:
Строка 1: Три целых числа: N, К, D
Строка 2: D неотрицательных чисел — предел суммарного количества для каждого из различных блюд, которые могут быть принесены на вечеринку.
Строки 3・・N+ 2: Каждая строка содержит начальное целое Z(1 <Z< D), обозначающее количество типов различных блюд, которое может приготовить одна корова; остаток строки содержит Z целых чисел — идентификаторов типов пищи, которую может приготовить корова, соответствующая текущей строке (в строке 3 — корова 1, в строке 4 — корова 2, и т. д.).
Пример ввода [файл party.in]:
4 3 5
2 2 2 2 3
4 12 3 4
4 2 3 4 5
3 12 4
3 12 3
Пояснения к примеру:
Данные
Пояснения
435 22223
4 1234 42345
3 124
3 123
4 коровы, каждая до 3 блюд, всего 5 различных типов пищи max — 2 блюда типов 1..4 и 3 блюда типа 5
1-я корова может приготовить4различных блюда(1,2,3, 4)
2-я корова может приготовить4различных блюда(2,3,4, 5)
3-я корова может приготовить3различных блюда(1,2,4)
4-я корова может приготовить3различных блюда(1,2,3)
Вывод'.
Одна строка содержит одно целое число — максимальное количество блюд, которое может быть принесено на вечеринку.
Пример вывода [файл party.out]:
9
Пояснения:
Корова 1 叩мнесет блюда 3 и 4;
Корова 2 принесет блюда 3. 4 и 5:
Корова 3 принесет блюда 1 и 2;
Корова 4 прннесет блюда 1 и 2.
Рассмотрим граф, состоящий из N + Q вершин. Вершины с номерами от 1 до N соответствуют коровам, вершины с номерами от N+1 до N+Q соответствуют различным видам пищи (блюдам).
Прежде всего мы должны добавить в граф вершины исток (вершину с номером 0) и сток (вершину с номером Finish = N + Q + 1). Понятно, что от истока должны быть дуги ко всем вершинам 1..N (коровам), а от вершин N+1..N+Q (блюда) должны быть дуги к стоку. Каковы же должны быть веса введенных дуг от истока и к стоку? Для дуг «от истока»: с[0, i] = К (для всех i от 1 до N), поскольку каждая корова может принести К блюд. Соответствующий фрагмент программы выглядит следующим образом:
{Добавляем вершину-исток}
ka[O]:=N:{Из истока - N дуг к каждой корове}
for 1:-1 to N do
begin
a[0.i]:-1:{Номер вершины, соединенной с истоком}
c[0.i]:-K:{ее вес - максимальное кол-во блюд для коровы}
end:
Для дуг «к стоку»: c[i, Finish] = Lim[i-N] (для всех i от N + 1 до N + D), поскольку имеется лимит на количество блюд каждого типа. Соответствующий фрагмент программы выглядит следующим образом:
for i:T to D do read (Lim[i]):for i:-N+l to N+D dobeginka[i] :-l:a[i.1]:-Finish :c[i,Finish]:-Lim[i-N]:end;for i:T to D do read (Lim[i]):for i:-N+l to N+D dobeginka[i] :-l:a[i.1]:-Finish :c[i,Finish]:-Lim[i-N]:end;{Пределы блюд каждого типа}
for i:T to D do read (Lim[i]):
for i:-N+l to N+D do
begin
ka[i] :-l:
a[i.1]:-Finish :
c[i,Finish]:-Lim[i-N]:
end;
for i:T to D do read (Lim[i]):
for i:-N+l to N+D do
begin
ka[i] :-l:
a[i.1]:-Finish :
c[i,Finish]:-Lim[i-N]:
end;
{Добавляем вершину-сток}
{одна дуга от каждого блюда к стоку}
{ее вес - предел числа блюд данного типа}
Мы строим дугу от коровы £ к блюду j в том и только в том случае, если корова i может приготовить блюдо/ Вес каждой такой дуги должен получить значение 1,
поскольку блюда, которые принесет корова, должны отличаться друг от друга. Соответствующий фрагмент программы выглядит следующим образом:
{Добавляем вершины от коров к блюдам}
for i:-1 to N do
{Для каждой коровы}
begin
read(Z); ka[i]:-Z;
{Количество блюд, которые она готовит}
for j:-l to Z do
{Для каждого такого блюда}
begin
read(FoodlD);
{Читаем его номер}
a[i. j]:-N+FoodID:
{Добавляем дугу от коровы к блюду}
c[i.a[i.j]]:-l:
{Вес такой дуги - 1}
end
end;
Мы имеем граф, заданный списками дуг {ka[i]f a[ij]} и матрицей весов c[i,j]t на котором требуется решить задачу о построении максимального потока Д/,У]. После чего ответ на вопрос, поставленный в задаче («Каково максимальное количество пищи, которую коровы могут принести на вечеринку?»), можно получить, например, следующим образом:
{Поток, входящий в вершину-сток, с номером Finish}
Мах:-0:
for i:-N+l to N+D do Max:-Max+f[i.Finish];
writeln(Max):
ЗАМЕЧАНИЕ Поскольку по построению связь с вершиной-стоком (номер Finish) имеют только вершины, соответствующие блюдам (с номерами от N+1 до N+D), то суммируем только их. Потоки из вершин 0..N в вершину-сток будут равны нулю, поскольку в графе для данной задачи вообще нет дуг, соединяющих эти вершины со стоком.
Полный текст решения задачи приведен в конце главы.
Задача «Кубики»
Всероссийская командная олимпиада школьников по программированию, 2000
Кубики (input.txt / output.txt /10 с)
Родители подарили Пете набор детских кубиков. Поскольку Петя скоро пойдет в школу, они купили ему кубики с буквами. На каждой из шести граней каждого кубика написана буква.
Теперь Петя хочет похвастаться перед старшей сестрой, что научился читать. Для этого он хочет сложить из кубиков ее имя. Но это оказалось довольно сложно сделать — ведь разные буквы могут находиться на одном и том же кубике, и тогда Петя не сможет использовать обе буквы в слове. Правда, одна и та же буква может встречаться на разных кубиках. Помогите Пете!
Дан набор кубиков и имя сестры. Выясните, можно ли выложить ее имя с помощью этих кубиков, и если да, то в каком порядке следует выложить кубики.
Ввод:
В первой строке входного файла находится число N(1 <N< 100) — количество кубиков в наборе у Пети. Во второй строке записано имя Петиной сестры — слово, состоящее только из больших латинских букв, не длиннее 100 символов. Следующие N строк содержат по 6 букв (только большие латинские буквы), которые написаны на соответствующем кубике.
Вывод:
В первой строке выходного файла выведите KES, если выложить имя Петиной сестры данными кубиками можно, и NO — в противном случае.
Если ответ YES, то во второй строке выведите М различных чисел из диапазона 1..N, где М — количество букв в имени Петиной сестры, i-e число должно быть номером кубика, который следует положить на i-e место при составлении имени Петиной сестры. Кубики нумеруются с 1,в том порядке, в котором они заданы во входном файле. Если решений несколько, выведите любое. Разделяйте числа пробелами.
Пример вводаПример вывода
~NO
ANN
ANNNNN
BCDEFG
HIJKLM
YES 2 1354YES 2 1354NOPQRS
YES 2 1354
YES 2 1354
HELEN ABCDEF GHIJKL MNOPQL STUVWN EIUOZK
Составим для данной задачи граф следующим образом: в качестве базовых вершин будем рассматривать заданные кубики, а также вершины, соответствующие буквам, которые могут быть написаны на заданных кубиках (26 заглавных букв латинского алфавита). Добавим также вершину-исток (ее соединим со всеми вершинами, соответствующими кубиками) и вершину-сток (с ней соединим все вершины, соответствующие буквам).
Фрагменты программы, которые обеспечат необходимый ввод исходных данных и построение графа, приведены ниже:
{Количество кубиков} {Имя Петиной сестры} {Надписи на кубиках} {Кубики + алфавит + сток}{Количество кубиков} {Имя Петиной сестры} {Надписи на кубиках} {Кубики + алфавит + сток}readIn (N):
{Количество кубиков} {Имя Петиной сестры} {Надписи на кубиках} {Кубики + алфавит + сток}
{Количество кубиков} {Имя Петиной сестры} {Надписи на кубиках} {Кубики + алфавит + сток}
readln (Name):
for i:-l to N do readln(Qubics[i]): Finish :- N+26+1:
Поскольку с каждого кубика может быть использована только одна буква, то веса дуг от истока к кубикам равны 1.
ka[O]:-N;for i:-1 to N dobegina[0.i]:-i:c[0.i]:T:end:ka[O]:-N;for i:-1 to N dobegina[0.i]:-i:c[0.i]:T:end:{Добавляем вершину-исток}
ka[O]:-N;
for i:-1 to N do
begin
a[0.i]:-i:
c[0.i]:T:
end:
ka[O]:-N;
for i:-1 to N do
begin
a[0.i]:-i:
c[0.i]:T:
end:
{Из истока - N дуг к каждому кубику}
{Номер вершины. соединенной с истоком} {вес соответствующей дуги}
Фрагмент графа от вершин, соответствующих заглавным буквам латинского алфавита, к стоку определяется именем Петиной сестры (переменная Name), которое по условиям задачи требуется составить из исходных кубиков:
{Добавляем вершину-сток}
for i:»1 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]){ее вес - количество таких букв в имени}
end;
ЗАМЕЧАНИЕ
кулике строится дуга от соответствующегокулике строится дуга от соответствующегоПоскольку имя Петиной сестры может включать повторяющиеся буквы, то при формировании веса соответствующей дуги используется инкрементирование ——lnc(c[N+NomLet,Finish]).
кулике строится дуга от соответствующего
кулике строится дуга от соответствующего
Наконец для каждой буквы на каждом кубика к соответствующей букве.
Заметим, что на одном кубике буквы могут повторяться, и мы можем для таких дуг либо увеличивать их вес, либо добавлять новую дугу (с весом 1 — именно такой вариант и реализован в приведенном фрагменте программы). Алгоритм построения максимального потока, который будет изложен далее, работает и в графах, содержащих более одной дуги между одной и той же парой вершин.
{Добавляем дуги от кубиков к буквам} for i:-1 to N do{Для каждого кубика}
begin for j:-l to 6 do{Для каждой буквы на кубике}
begin
NomLet:-Ord(Qubics[i,j])-Ord('A')+1; {Вычисляем номер буквы} a[i,j]:-N+NomLet:{Добавляем дугу от кубика к букве}
c[i.a[i.j]]:-l:{Вес такой дуги - 1}
NomKub[Nomlet]:-l; inc(ka[i]) end:
end;
Чтобы получить ответ на вопрос, поставленный в задаче (<Выясните, можно ли выложить ее имя с помощью этих кубиков и если да, то в каком порядке следует выложить кубики»), после построения максимального потока на заданном графе следует высчитать максимальный исходящий поток Мах:
Мах:-0:
for i:-N+l to N+26 do Max:-Max+f[i.Finish];•
Если значение Max не равно длине имени Name, то ответ отрицательный (NO), иначе — положительный (УЕ5). Для того чтобы вывести номера использованных кубиков в порядке, обеспечивающем построение имени, нужно для каждой буквы имени (NomLet) найти такую вершину А в графе (и вывести ее номер), что f[k, Nomlet] н 1. Фрагмент программы, выводящий ответ задачи, приводится ниже:
If MaxoLength(Name)
then writeln('NO') else
begin
writelnCYES'):
for i:-l to Length(Name) do
begin
NomLet:-0rd(Name[i])-0rd('A')+1; {Вычисляем номер буквы} for k:-l to n do
if f[k,N+NomLet]-l
then
begin writeCk,''):
f[k.N+NomLet]:-0:
break;
end;
end:
Полный текст решения задачи приведен в конце главы.
Задача «Игра»
Белорусская олимпиада по информатике, 2001
Игра (game.in / game.out)
Известная на весь Могилев компания выпустила игру, для которой необходима конструкция, состоящая из маленьких платформ и труб. Платформы разделяются на стартовые (их N1 штук), финишные (№ штук) и промежуточные (^2 штук). Все стартовые платформы находятся на одинаковой высоте. Финишные платформы также находятся на одинаковой высоте. Все высоты промежуточных платформ различны. Они меньше высоты стартовых, но больше высоты финишных.
Каждой платформе соответствует уникальный номер от 1 до NI+N2+N3. Нумерация следующая: сначала перечислены все стартовые платформы, затем промежуточные и, наконец, финишные. Все промежуточные платформы
пронумерованы по убыванию высоты. То есть если высота промежуточной платформы Л больше высоты платформы В, то номер Л меньше номера В. На каждой из стартовых платформ находится шарик. Шарик может скатиться с платформы А на платформу В, если они соединены трубой и высота А больше высоты В. На каждой из финишных платформ может оказаться не более одного шарика. Если шарик находится на некоторой платформе, то игрок может выбрать направление дальнейшего пути шарика, то есть выбрать платформу, на которую шарик скатится. Также для каждой промежуточной платформы задано число С, равное максимальному количеству шариков, которые могут прокатиться по ней за время игры. Цель игры заключается в том, чтобы на финишных платформах оказалось как можно больше шариков.
Вам нужно узнать, какое максимальное количество шариков может оказаться на финишных платформах в результате игры.
Ввод:
Во входном файле Game.in находится информация о конструкции в следующей форме:
Nl N2 N3
CN1+1
CN1+N2
К1 А[1.1] : А[1.К1]
К2 А[2.1] : А[2.К2]
KN1+N2 A[N1+N2.1] : A[N1+N2.KN1+N2]
где числа NltN2,N3 — соответственно количество стартовых, промежуточных и финишных платформ. Cj — максимальное количество шариков, которые могут прокатиться по промежуточной платформе с номером j (Nl +1 4 j 4 Nl +N2) за все время игры. Ki — количество труб, выходящих из платформы с номером i (1 4 i 4 Nl +N2). Элементы массива А, перечисленные в строке, являются номерами платформ, на которые может скатиться шарик с соответствующей платформы.
Ограничения'.
Все числа на вводе целые.
О < Nl. N2. N3 < 51
1 < N1+N2+N3 < 201
0 J Cj J 50
Не существует труб между стартовыми платформами.
Не существует труб между финишными платформами.
Вывод:
В первой строке выходного файла Game.out должно находиться единственное число, равное максимальному количеству шариков, которые могут оказаться на финишных платформах в результате игры.
Пример вводаПример вывода
3432
3 2 1 2 1 4 1 4 1 4 256 1 7 1 7 389 10
Стартовые, промежуточные и финишные платформы естественно представлять вершинами графа, а трубы между ними — дугами. Однако данная задача слегка отличается по постановке от классической, поскольку здесь задан вес вершины: «...для каждой промежуточной платформы задано число С, равное максимальному количеству шариков, которые могут прокатиться по ней за время игры». Для перехода к классической постановке достаточно заменить такие вершины на дуги. При этом введенным дугам присваивается вес замененных вершин. Понятно, что должны появиться дополнительные вершины. Например, пусть в вершину 3 с весом X входит две дуги (1тЗи2тЗ)и выходит две дуги (3 т 4 и 3 т 5), как показано на рис. 1.5:
14
\ /зх
/ \
25
Рис. 1.5. Исходное состояние вершины 3
Мы добавляем вершину 6 и дугу 3 т 6 с весом X (рис. 1.6):
Рис. 1.6. Преобразованный фрагмент графа
Покажем теперь, как такой подход может быть реализован в данной задаче. Поскольку промежуточные платформы превращаются в дуги, то всего вершин ста-
количество промежуточных платформ. {Количества платформ}количество промежуточных платформ. {Количества платформ}новится на N2 больше, где№2 —
количество промежуточных платформ. {Количества платформ}
количество промежуточных платформ. {Количества платформ}
readln (N1.N2.N3):
All :- N1+N2+N3;
{Платформы + сток}{Платформы + сток}Finish :- N1+2*N2+N3+1:
{Платформы + сток}
{Платформы + сток}
Добавляем исток и связываем его с вершинами, соответствующими стартовым платформам. Пропускная способность дуг равна 1, поскольку по условию задачи на каждой из стартовых платформ находится шарик.
ka[0]:-Nl:for i:-l to N1 dobegina[0.i]:-i:c[0.i]:-l:end:ka[0]:-Nl:for i:-l to N1 dobegina[0.i]:-i:c[0.i]:-l:end:{Добавляем вершину-исток}
ka[0]:-Nl:
for i:-l to N1 do
begin
a[0.i]:-i:
c[0.i]:-l:
end:
ka[0]:-Nl:
for i:-l to N1 do
begin
a[0.i]:-i:
c[0.i]:-l:
end:
{Из истока - N дуг - к каждой стартовой платформе}
{Номер вершины, соединенной с истоком}
{на каждой из стартовых платформ - 1 шарик}
Для каждой промежуточной платформы с номером / добавляем две вершины с номерами Nl + i и а〃 + i (где all = N1 + № + №) соответственно. Добавляем также дугу между этим вершинами с пропускной способностью вершины.
for i:-1 to N2 do beginreadIn (cn): a[Nl+i.l]:-all+i; c[Nl+i,all+i]:-cn: ka[Nl+i]:-l:end;for i:-1 to N2 do beginreadIn (cn): a[Nl+i.l]:-all+i; c[Nl+i,all+i]:-cn: ka[Nl+i]:-l:end;{Добавляем дуги "внутри" пронежуточимх платформ} {Для каждой промежуточной платформы}
for i:-1 to N2 do begin
readIn (cn): a[Nl+i.l]:-all+i; c[Nl+i,all+i]:-cn: ka[Nl+i]:-l:
end;
for i:-1 to N2 do begin
readIn (cn): a[Nl+i.l]:-all+i; c[Nl+i,all+i]:-cn: ka[Nl+i]:-l:
end;
{Читаем сколько шариков она выдержит}
{Добавляем дугу "внутри" этой платформы} {Пропускная способность добавленной дуги} {Такая дуга всегда одна}
Все входящие дуги от стартовых платформ будем строить в вершины N1 +i. По условиям задачи их пропускная способность не ограничена.
{Добавляем дуги от стартовых платформ к "началу" промежуточных}
for i:=1 to N1 do
{Количество труб от платформы} {Количество дуг из вершины 1} {Для каждой трубы}{Количество труб от платформы} {Количество дуг из вершины 1} {Для каждой трубы}{Дуга из i в a[i,j]}{Ее пропускная способность не ограничена}{Дуга из i в a[i,j]}{Ее пропускная способность не ограничена}begin
{Количество труб от платформы} {Количество дуг из вершины 1} {Для каждой трубы}
{Количество труб от платформы} {Количество дуг из вершины 1} {Для каждой трубы}
{Дуга из i в a[i,j]}
{Ее пропускная способность не ограничена}
{Дуга из i в a[i,j]}
{Ее пропускная способность не ограничена}
read(k);
ka[i].-k:
for j:-l to k do
begin
read(a[i.j]):
c[i.a[i.j]]:-MaxP; end;
end:
Все дуги от промежуточных платформ будем строить от вершин all ^ i — их пропускная способность также не ограничена.
{Добавляем дуги от "конца" промежуточных платформ к финишным платформам}
for i:-1 to N2 do
beginread(k);ka[all+i]:-k:for j:-l to k dobeginread (np);a[all+i.j]:-np:c[all+i,a[all+i.j]]:=MaxP: end:end:beginread(k);ka[all+i]:-k:for j:-l to k dobeginread (np);a[all+i.j]:-np:c[all+i,a[all+i.j]]:=MaxP: end:end:{Количество труб от платформы} {Количество дуг из вершины all+i} {Для каждой трубы}{Количество труб от платформы} {Количество дуг из вершины all+i} {Для каждой трубы}{Номер "целевой" платформы}{Дуга из Nl+2*i в a[i.j]}{Ее пропускная способность не ограничена}{Номер "целевой" платформы}{Дуга из Nl+2*i в a[i.j]}{Ее пропускная способность не ограничена}
begin
read(k);
ka[all+i]:-k:
for j:-l to k do
begin
read (np);
a[all+i.j]:-np:
c[all+i,a[all+i.j]]:=MaxP: end:
end:
begin
read(k);
ka[all+i]:-k:
for j:-l to k do
begin
read (np);
a[all+i.j]:-np:
c[all+i,a[all+i.j]]:=MaxP: end:
end:
{Количество труб от платформы} {Количество дуг из вершины all+i} {Для каждой трубы}
{Количество труб от платформы} {Количество дуг из вершины all+i} {Для каждой трубы}
{Номер "целевой" платформы}
{Дуга из Nl+2*i в a[i.j]}
{Ее пропускная способность не ограничена}
{Номер "целевой" платформы}
{Дуга из Nl+2*i в a[i.j]}
{Ее пропускная способность не ограничена}
Добавляем вершину-сток с дугами к ней от всех финишных платформ. Веса этих дуг равны 1, поскольку на каждой из финишных платформ может оказаться нс более одного шарика.
N:=N1+N2;for i:-1 to N3 dobeginka[N+i]:-l:a[N+i,1]:-Finish :N:=N1+N2;for i:-1 to N3 dobeginka[N+i]:-l:a[N+i,1]:-Finish :{Добавляем вершину-сток}
N:=N1+N2;
for i:-1 to N3 do
begin
ka[N+i]:-l:
a[N+i,1]:-Finish :
N:=N1+N2;
for i:-1 to N3 do
begin
ka[N+i]:-l:
a[N+i,1]:-Finish :
{Одна дуга на каждую финишную платформу}{Одна дуга на каждую финишную платформу}{Стартовый номер финишных платформ} {Для каждой финишной платформы}
{Одна дуга на каждую финишную платформу}
{Одна дуга на каждую финишную платформу}
c[N+i.Finish]:-1: {На каждой из финишных платформ - не более 1 шарика} end:
Для вывода ответа на поставленный в задаче вопрос «Какое максимальное количество шариков может оказаться на финишных платформах в результате игры?» после построения максимального потока достаточно просуммировать количество шариков, выкатившихся из истока на стартовые платформы:
Мах:-0:
for i:-1 to N1 do Max:-Max+f[O.i];
writeln(Max):
Полный текст решения задачи приведен в конце главы.
Пример максимального потока на графе
Вернемся к задаче о паросочетании:
Добавляем вершины — исток и сток:
Рис. 1.8. Граф с добавленными вершинами (истоком и стоком)
Сначала проиллюстрируем неформально на данном примере алгоритм Форда- Фалкерсона, изложенный в следующем пункте данной главы.
Все начинается с нулевого потока f[itj] = 0 для всех /,/ Далее строится остаточная пропускная способность cf[i,j] (из вершины i в вершину j) для всех дуг: cf[4]-c[ij]^f[ij].
Напомним, что в этой задаче c[ij] * 1, если есть дуга из вершины i в вершину j. Затем строится произвольный дополняющий путь из истока (0) в сток (5), увеличивающий поток. Путь называется увеличивающим поток, если он состоит только из дуг с неотрицательной остаточной пропускной способностью cf[ij].
Пусть, например, это путь 0-1-3-5.
Далее выбирается минимальное значение из весов всех дуг увеличивающего пути.
В нашем случае минимальное значение ищется из величин:
с/[0, 1] = неограниченно
cf[\^]-\
с/[3, 5] = неограниченно
Минимальное значение = 1.
Далее для каждой дуги из увеличивающего пути увеличиваем поток по этой дуге на найденное минимальное значение и получаем (рис. 1.9):
Рис. 1.9. Граф с отмеченным увеличивающим путем
Затем — это ключевой момент алгоритма — для каждой дуги (“, и) из увеличивающего пути СТРОИМ ОБРАТНЫЕ ДУГИ с ОТРИЦАТЕЛЬНЫМ потоком:/[d, и] * "71〃,^]» в частности,/[3,1] = -/[1,3] = -1 (рис. 1.10):
Рис. 1.10. Граф с обратной дугой с отрицательным потоком
При вычислении остаточной пропускной способности дуги с отрицательным потоком появляется НОВАЯ дуга с ПОЛОЖИТЕЛЬНОЙ остаточной пропускной способностью:(/[3,1]* с[3,1] -/[3,1] * 0 - (-1) * 1. Теперь при попытке построения нового увеличивающего пути нам удается найти путь 0->2->3->1->4->5. Для всех дуг этого пути cf[ij] > 0.
Минимальное значение увеличивающего пути из всех cf[i,j] опять равно 1. Снова прибавляем это минимальное значение ко всем дугам увеличивающего пути, получаем с[3,1] * -1 + 1 *0 (рис 1.11):
Рис. 1.11. Граф с новым увеличивающим путем
И снова для каждой дуги (“,и) из увеличивающего пути строим обратные дороги с отрицательным потоком: Дг, и] = -Д«, и], в частности Д 1,3] = -/[3,1] = 0 и окон-
Рис. 1.12. Граф с новыми обратными дугами с отрицательным потоком
Теперь найти увеличивающий путь невозможно. Алгоритм закончил свою работу. Осталось проинтерпретировать результаты. Мы построили следующий поток: □ из вершины 0 (истока) — в вершину 1 и вершину 2: ДО, 1] = 1,Д0, 2] Т;
из вершины1в вершину 4:Д1,4] e1;
из вершины2в вершину 3:Д2,3] e1;
из вершины3в вершину 5:ДЗ, 5] e1;
из вершины4в вершину 5: Д4, 5] e1.
Максимальный поток равен максимальному исходящему потоку ДО, 1] + Д0,2] = 2 и равен максимальному входящему потоку ДЗ, 5] + Д4, 5] = 2.
Алгоритм Форда-Фалкерсона
Неформально этот алгоритм может быть записан следующим образом:
Формируем нулевой поток
Пока существует путь из истока в сток, увеличивающий поток
Выбираем Cmin - минимальную из остаточных пропускных способностей найденного пути
Для каждой дуги (u.v) пути (из вершины и в вершину v) f[u,v] :- f[u.v] + Cmin 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]; v:- path[i]:
{U->V - дуга с номером 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:
Как можно заметить, в процедуре MaxFlow вызываются процедура Init для инициализации нулевого потока и функция ExistPath, которая возвращает значение true, если увеличивающий путь существует, и значение false в противном случае. Очевидно, что если функция ExistPath возвращает значение false, то процедура MaxFlow завершает свою работу (увеличивающих путей больше нет). Если же ExistPath возвращает true (найден увеличивающий путь), то через свои параметры она возвращает также Cmin (минимальное значение из остаточных пропускных способностей дуг увеличивающего пути) и KR (количество дуг в увеличивающем пути). Кроме того, в глобальном для процедуры MaxFlow массиве path возвращаются номера вершин увеличивающего пути, перечисленные в порядке от стока к истоку.
Если путь найден, то в соответствии с алгоритмом Форда-Фалкерсона для всех дуг увеличивающего пути пересчитывается поток f[ut d], строятся обратные дуги с отрицательным потоком f[v, и] и пересчитываются остаточные пропускные способности cf[ut 矶 и cf[v, и]. Рассмотрим подробней содержание процедур Init и ExistPath.
Procedure Init:
begin
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]:
{построение списков входящих дуг}
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:
{по всем исходящим дугам}
for i:-0 to Finish do
{По всем вершинам}
for j:-l to ka[i] do begin
{по всем исходящим дугам}
inc(kia[a[i,j]]):
{инкремент к-ва дуг. входящих в a[i.j]}
ia[a[i.j].kia[a[i.j]]]:-i:
{запоминаем вершину 1. как входящую в 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]:
Далее для более удобной работы с «обратными» дугами увеличивающего пути строится «инвертированный» граф таким образом, что номера вершин в «инвертированном» графе остаются прежними, а направления дуг меняются на противоположные. Тем самым мы для каждой вершины получаем список входящих в нее дуг.
kia[i] — количество дуг, входящих в вершину i
ia[i,j] — номер вершины, из которой ведет /я по счету дуга, входящая в вершину i
{построение списков входящих дуг}
{Инициализация}
for 1:-0 to Finish do kia[i].-O;
for i =0 to Finish do{По всем вершинам}
for j--l to Finish do ia[i.j]:=-l; {по всем исходящим дугам}
Заметим, что величины ia[it j] инициализируются значением -1, поскольку нулем инициализировать нельзя — в графе есть дуги, исходящие из вершины 0.
for i:-0 to Finish do for j:=l to ka[i] do begininc(kia[a[i.j]]):ia[a[i.j].kia[a[i.j]]].-i; end:for i:-0 to Finish do for j:=l to ka[i] do begininc(kia[a[i.j]]):ia[a[i.j].kia[a[i.j]]].-i; end:{По всем вершинам}
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:
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:
{по всем исходящим дугам}
{инкремент к-ва дуг. входящих в a[i.j]}
{запоминаем вершину 1. как входжцую в [i.j]}
Наконец рассмотрим функцию ExistPath.
{ Поиск в ширину }{ Breadth-First Search }{ Поиск в ширину }{ Breadth-First Search }Function ExistPathCvar KR.Cmin:integer):boolean;
{ Поиск в ширину }
{ Breadth-First Search }
{ Поиск в ширину }
{ Breadth-First Search }
Const
WHITE - 1:
GRAY = 2:
var
color,back:array [0..MaxV]ofinteger:
Q•array [0..MaxV]ofinteger;
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:{Все вершины свободны}
for i:-0 to Finish do Q[i]:--1:{Все вершины свободны}
color[0]:=GRAY:{Начальная вершина - обработана}
back[0]:--l;
BegQ:-l:EndQ:-0:BegQ:-l:EndQ:-0:{Начало очереди}
BegQ:-l:
EndQ:-0:
BegQ:-l:
EndQ:-0:
{Очередь пуста}
{взять следующую дугу}{Проход по отрицательным ребрам} {номер дуги из вершины i}{пока дуги не кончились}{если вершина a[i.j] свободна}{поставить ее в очередь} вершину a[i.j] - из вершины i} {пометить вершину a[i.j]} {как использованную}{взять следующую дугу}{Восстановление пути из очереди}{Начинаем с конца}{Пока не начало}{Увеличиваем к-во дуг в пути} {Заносим номер предыдущей вершины}{взять следующую дугу}{Проход по отрицательным ребрам} {номер дуги из вершины i}{пока дуги не кончились}{если вершина a[i.j] свободна}{поставить ее в очередь} вершину a[i.j] - из вершины i} {пометить вершину a[i.j]} {как использованную}{взять следующую дугу}{Восстановление пути из очереди}{Начинаем с конца}{Пока не начало}{Увеличиваем к-во дуг в пути} {Заносим номер предыдущей вершины}
{взять следующую дугу}
{Проход по отрицательным ребрам} {номер дуги из вершины i}
{пока дуги не кончились}
{если вершина a[i.j] свободна}
{поставить ее в очередь} вершину a[i.j] - из вершины i} {пометить вершину a[i.j]} {как использованную}
{взять следующую дугу}
{Восстановление пути из очереди}
{Начинаем с конца}
{Пока не начало}
{Увеличиваем к-во дуг в пути} {Заносим номер предыдущей вершины}
{взять следующую дугу}
{Проход по отрицательным ребрам} {номер дуги из вершины i}
{пока дуги не кончились}
{если вершина a[i.j] свободна}
{поставить ее в очередь} вершину a[i.j] - из вершины i} {пометить вершину a[i.j]} {как использованную}
{взять следующую дугу}
{Восстановление пути из очереди}
{Начинаем с конца}
{Пока не начало}
{Увеличиваем к-во дуг в пути} {Заносим номер предыдущей вершины}
KR:-0;{Количество дуг в пути - 0}
Put(O):{Поместить в очередь начальную вершину}
Find:-false: while (BegQ<eEndQ) and not Find do{Пока очередь не пуста и путь не найден}
begin
Get(i);{Взять вершину i из очереди}
j:-l;{номер дуги из вершины 1}
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] - из вершины 1}
color[a[i.j]]:-GRAY;{пометить вершину a[i.j]}
{как использованную}
Find :- a[i.j]-Finish;
end: inc(j): end:
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:{в
color[ia[i.j]]:-GRAY:
Find :- ia[i.j卜Finish: end: inc(j): end: end:
KR:-0;
i:-Finish:
Cmin:- maxi nt: 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:
Для построения увеличивающего пути используется механизм очереди от истока
Put(O):{Поместить в очередь начальную вершину}
while (BegQ<-EndQ) and not Find do {Пока очередь не пуста и путь не найден} begin
Get(1);{Взять вершину i из очереди}
Вначале идет попытка увеличить путь за счет прямых дуг
j:-l:{номер дуги из вершины 1}
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] - из вершины 1}
color[a[i,j]]:-GRAY:{пометить вершину a[i.j]}
{как использованную}
Find :- a[i.j]-Finish: end: inc(j):{взять следующую дугу}
end:
Здесь:
массив Color[i] хранит пометки для всех вершин (включена/нет в увеличивающийся путь);
массив back[k] хранит номер вершины, из которой пришли в вершину А в увеличивающем пути;
переменная Find получает значение true, если увеличивающий путь достиг стока (a[itj] = Finish).
Далее делаем попытку увеличить путь за счет обратных дуг
{Проход по отрицательным ребрам}
j:-l;{номер дуги из вершины 1}
while (ia[i.j]>-0) and not Find do{пока дуги не кончились}
beginif (color[ia[i.j]]=WHITE) and (cf[i.ia[i.j]]>0)then beginPut(ia[i.j]): back[ia[i,j]]:-i: color[ia[i.j]]:-GRAY:Find := ia[i.j]-Finish; end:inc(j):end:beginif (color[ia[i.j]]=WHITE) and (cf[i.ia[i.j]]>0)then beginPut(ia[i.j]): back[ia[i,j]]:-i: color[ia[i.j]]:-GRAY:Find := ia[i.j]-Finish; end:inc(j):end:{если вершина a[i.j] свободна}{поставить ее в очередь}{в вершину a[i,j] - из вершины 1} {пометить вершину a[i.j]}{как использованную}{взять следую!цую дугу}{если вершина a[i.j] свободна}{поставить ее в очередь}{в вершину a[i,j] - из вершины 1} {пометить вершину a[i.j]}{как использованную}{взять следую!цую дугу}
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: color[ia[i.j]]:-GRAY:
Find := ia[i.j]-Finish; end:
inc(j):
end:
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: color[ia[i.j]]:-GRAY:
Find := ia[i.j]-Finish; end:
inc(j):
end:
{если вершина a[i.j] свободна}
{поставить ее в очередь}
{в вершину a[i,j] - из вершины 1} {пометить вершину a[i.j]}
{как использованную}
{взять следую!цую дугу}
{если вершина a[i.j] свободна}
{поставить ее в очередь}
{в вершину a[i,j] - из вершины 1} {пометить вершину a[i.j]}
{как использованную}
{взять следую!цую дугу}
После выхода из цикла построения увеличивающего пути остается только восстановить увеличивающий путь в массив path, используя массив back. Попутно подсчитываются количество дуг в увеличивающем пути (KR) и минимальный добавляемый поток (Cmin):
KR:-0;i:-Finish;Cmin:- maxi nt:while (i<>0) dobeginInc(KR):path[KR]:-i:i:-back[i]:if Cmin>cf[i,path[KR]]then Cmin:-cf[i,path[KR]]:end:KR:-0;i:-Finish;Cmin:- maxi nt:while (i<>0) dobeginInc(KR):path[KR]:-i:i:-back[i]:if Cmin>cf[i,path[KR]]then Cmin:-cf[i,path[KR]]:end:{Восстановление пути из очереди}{Начинаем с конца}{Пока не начало}{Увеличиваем к-во дуг в пути} {Заносим номер предыдущей вершины} {Меняем текущую вершину}{Минимальный добавляемый поток}{Восстановление пути из очереди}{Начинаем с конца}{Пока не начало}{Увеличиваем к-во дуг в пути} {Заносим номер предыдущей вершины} {Меняем текущую вершину}{Минимальный добавляемый поток}
KR:-0;
i:-Finish;
Cmin:- maxi nt:
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:
KR:-0;
i:-Finish;
Cmin:- maxi nt:
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:
Для получения цельного представления о решении задачи методом Форда-Фал- керсона в конце главы приведено полное решение задачи «Новогодняя вечеринкам (листинг 1.4).
Решения задач
Листинг 1.1. Текст программы к задаче «Новогодняя вечеринка»
const
MaxN - 200:
MaxD - 100:
MaxV - MaxN+MaxD+1:
Листинг 1.1 (продолжение)
var N
3..MaxN:
{Количество коров}
К
1..5;
{Количество блюд на одну корову}
D
5..MaxD;
{Количество видов пищи}
Lim
array [1..MaxD]
of integer;{Предел количества блюд на вид пищи}
Z
integer;
{Количество типов блюд, которое корова может
FoodID :
:integer.
приготовить}
{Идентификатор блюда. которое корова может
Max
:integer:
приготовить}
{Максимальное количество блюд, которое может
a
:array [0..MaxV,
быть принесено на вечеринку}
1..MaxV] of integer; {Граф исток-коровы-блюда-сток}
Ka
array [0..MaxV]
of integer;{Количество ребер из вершины}
c.f.cf :
array [0..MaxV.
0..MaxV] of byte:{Веса вершин в графе А}
i.j. Finish : integer: ia: array [0..MaxV.
1..MaxV] of integer; {Список входящих вершин}
Kia
array [0. MaxV]
of integer:{Количество входящих ребер}
Procedure InputData;
begin
assignC input.'party.in*); reset(input);
read(N.K.D);{Прочитали N-коров. D-блюд. К・предел блюд}
Finish :- N+D+l;
for i:-0 to N+D+l do{Инициализируем пустой граф}
begin{исток-коровы-блюда-сток}
for j:=l to MaxD do a[i.j]:-0: ka[i]:-0:
end.
{Добавляем вершину-исток}
ka[0]:-N;{Из истока - N дуг - к каждой корове}
for i.=l to N do
begin
a[0.i]:-i:{Номер вершины, соединенной с истоком}
c[0.i]:nK:{ее вес - максимальное кол-во блюд для коровы}
end;
for i:-1 to D do read (Lim[i]):{Пределы блюд каждого типа}
{Добавляем вершину-сток}
for i--N+1 to N+D do begin ka[i] T: a[i.l]:=N+D+l :{одна дуга от каждого блюда к стоку}
c[i,Finish]:»Lim[i-N]; {ее вес - предел числа блюд данного типа} end:
for 1:-1 to N dofor 1:-1 to N do{Добавляем вершины от коров к блюдам} {Для каждой коровы}
for 1:-1 to N do
for 1:-1 to N do
begin
read(Z); ka[i]:-Z:{Количество блюд, которые она готовит}
for j:-1 to Z do{Для каждого такого блюда)
begin
read(FoodlD):{Читаем его номер}
a[i.j]:eN+FoodID:{Добавляем дугу от коровы к блюду}
c[i.a[i,j]]:-l:{Вес такой дуги - 1}
end end; close(input): end;
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
MaxN - 100:
MaxV -
MaxN+26+1:
{Макс, k-bo вершин}
MaxC -
MaxV div 20;
{Макс к-во дуг из вершины}
var N
:3..MaxN:
{Количество кубиков}
a
:array [0..MaxV, 1..MaxN] of byte;
{Граф исток-кубики-буквы-сток}
Ka
:array [0..MaxV] of integer;
{Количество ребер из вершины}
c.f.cf
:array [0..MaxV. 0..MaxV] of byte:
{Веса вершин в графе А}
i, j. Finish : integer;
ia : array [0..MaxV. 1..MaxC] of shortint; {Список входящих вершин}
Kia : array [0..MaxV] of integer:{Количество входящих ребер}
Max: longint:{Макс, поток}
Name: string[100]:
Qubics : array [1..100] of string [6]:
Lim : array [1..26] of integer;{Количество букв в имени}
Листинг 1.2 (продолжение)
NomKub : array [1..26] of integer;
{Номер кубика с такой буквой}
NomLet : byte:
s.k : byte;
Procedure InputData:
begin
assign( input.*input.txt'): reset(input);
readIn (N):{Количество кубиков}
readIn (Name);{Имя Петиной сестры}
for i:-l to N do readln(Qubics[i]);{Надписи на кубиках}
Finish :- N+26+1;{Кубики + алфавит + сток}
for i:-0 to Finish do{Инициализируем пустой граф}
begin{исток-кубики-буквы-сток}
for j:-0 to Finish do a[i.j]:-0: ka[i]:-0:
end:
ka[0]:-N:
{Добавляем вершину-исток}
{Из истока - N дуг - к каждому кубику}
for i:-1 to N dobegin
a[0.i]:-i:
c[0.i]:-l: end:
{Номер вершины, соединенной с истоком} {вес соответствующей дуги}
{Добавляем вершину-сток}
for i:-1 to Length(Name) do
begin
NomLet:- Ord(Name[i]) - Ord('A') + 1;
ka[N+NanLet]:-l;
a[N+NomLet.U:-Finish :{Одна дуга на каждую букву из имени}
Inc(c[N+NomLet.Finish]){ее вес - количество таких букв в имени}
end:
for i:-1 to N do
{Добавляем дуги от кубиков к буквам} {Для каждого кубика}
begin
for j:-l to 6 do begin
{Для каждой буквы на кубике}
NomLet:-Ord(Qubics[i.j])-Ord('A')+1. {Вычисляем номер буквы}
a[i.j]:-N+NomLet: c[i.a[i.j]]:-l: NomKub[Nomlet]-l: inc(ka[i])
{Добавляем дугу от кубика к букве} {Вес такой дуги - 1}
end:
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 writelnC'NO') else
begin writeln('YES');
for i:-l to Length(Name) do begin
NomLet:-0rd(Name[i])-0rd('A')+1. {Вычисляем номер буквы} for k:-l to n do if f[k.N+NomLet]-l then
begin writeCk,'');
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:MaxC - 2*MaxN2: vara:array[0..MaxV.Ka:array[0..MaxV]c.f.cf :array[0..MaxV,i.j. Finish : integer: ia:array[0..MaxV.Kia:array[0..MaxV]Max:longint:constMaxP - 127:MaxNl - {50} 20:MaxN2 - {100} 20:MaxN3 - {50} 20:MaxV - MaxNl+2*MaxN2+MaxN3:MaxC - 2*MaxN2: vara:array[0..MaxV.Ka:array[0..MaxV]c.f.cf :array[0..MaxV,i.j. Finish : integer: ia:array[0..MaxV.Kia:array[0..MaxV]Max:longint:constMaxP - 127:{Макс, кол-во стартовых платформ}
MaxNl - {50} 20:
MaxN2 - {100} 20:
MaxN3 - {50} 20:
MaxV - MaxNl+2*MaxN2+MaxN3:
MaxC - 2*MaxN2: var
a:array[0..MaxV.
Ka:array[0..MaxV]
c.f.cf :array[0..MaxV,
i.j. Finish : integer: ia:array[0..MaxV.
Kia:array[0..MaxV]
Max:longint:
const
MaxP - 127:
MaxNl - {50} 20:
MaxN2 - {100} 20:
MaxN3 - {50} 20:
MaxV - MaxNl+2*MaxN2+MaxN3:
MaxC - 2*MaxN2: var
a:array[0..MaxV.
Ka:array[0..MaxV]
c.f.cf :array[0..MaxV,
i.j. Finish : integer: ia:array[0..MaxV.
Kia:array[0..MaxV]
Max:longint:
const
MaxP - 127:
{Макс, кол-во промежуточных платформ}
{Макс, кол-во финишных платформ} {Макс, к-во вершин} {Макс, к-во дуг из вершины}
.MaxV] of byte: {Граф исток-кубики-буквы-сток} of integer;{Количество ребер из вершины}
0..MaxV] of shortint:{Веса вершин в графе А}
.MaxV] of shortint: {Список входящих вершин} of integer:{Количество входящих ребер}
{Макс, поток}
Листинг 1.3 (продолжение)
var
Nl : 0..MaxNl:{Кол-во стартовых платформ}
N2 : O..MaxN2:{Кол-во промежуточных платформ}
N3 : O..MaxN3;{Кол-во финишных платформ}
сп. пр. N. k. tn. all : byte:
Procedure InputData.
begin assignC input.'game.in'): reset(input); readIn (N1.N2.N3):{Количество платформ}
Finish :- N1+2*N2+N3+1;{Платформы + сток}
{Промежуточные платформы превращаются в дуги} All :- N1+N2+N3: for i:-0 to Finish do{Инициализируем пустой граф}
begin{исток-задачи-ресурсы-сток}
for j:-l to МахС do a[i.j]:-0: ka[i]:-0; end:{Добавляем вершину-исток}
ka[0]:-Nl:
for i:-l to Nl do
begin
a[0.i]:-i:
c[0,i].-l: end:
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:
for i:-l to Nl do begin
read(k);
ka[i]:-k;
for j:-l to k do begin
read(a[i.j]);
c[i.a[i.j]]:卄
{Из истока - N дуг - к каждой стартовой платформе}
{Номер вершины, соединенной с истоком} {на каждой из стартовых вершин - 1 шарик}
{Добавляем дуги "внутри" промежуточных платформ}
{Для каждой промежуточной платформы}
{Читаем сколько шариков она выдержит} {Добавляем дугу "внутри" этой платформы} {Пропускная способность добавленной дуги}
{Такая дуга всегда одна}
{Добавляем дуги от стартовых платформ к "началу" промежуточных}
{Количество труб от платформы}
{Количество дуг из вершины 1}
{Для каждой трубы}
{Дуга из i в a[i.j]}
1ахР;{Ее пропускная способность не ограничена}
end;
{Добавляем дуги от "конца" промежуточных платформ к финишным платформам}
{Количество труб от платформы}{Количество дуг из вершины all+i} {Для каждой трубы}{Номер "целевой" платформы}{Дуга из Nl+2*i в a[i.j]}{Ее пропускная способность не ограничена}{Добавляем вершину-сток}{Стартовый номер финишных платформ}{Для каждой финишной платформы}{Количество труб от платформы}{Количество дуг из вершины all+i} {Для каждой трубы}{Номер "целевой" платформы}{Дуга из Nl+2*i в a[i.j]}{Ее пропускная способность не ограничена}{Добавляем вершину-сток}{Стартовый номер финишных платформ}{Для каждой финишной платформы}for i:-1 to N2 do begin read(k): ka[all+i]:-k; for j:-l to k do begin
{Количество труб от платформы}
{Количество дуг из вершины all+i} {Для каждой трубы}
{Номер "целевой" платформы}
{Дуга из Nl+2*i в a[i.j]}
{Ее пропускная способность не ограничена}
{Добавляем вершину-сток}
{Стартовый номер финишных платформ}
{Для каждой финишной платформы}
{Количество труб от платформы}
{Количество дуг из вершины all+i} {Для каждой трубы}
{Номер "целевой" платформы}
{Дуга из Nl+2*i в a[i.j]}
{Ее пропускная способность не ограничена}
{Добавляем вершину-сток}
{Стартовый номер финишных платформ}
{Для каждой финишной платформы}
read (np); a[all+i.j]:-np: c[all+i.a[all+i,j]]:-MaxP; end:
end:
N:=N1+N2:
for i:-l to N3 do begin
{Одна дуга на каждую финишную платформу}{Нр каждой из финишных платформ - не более 1 шарика}{Одна дуга на каждую финишную платформу}{Нр каждой из финишных платформ - не более 1 шарика}ka[N+i]:-l;
{Одна дуга на каждую финишную платформу}
{Нр каждой из финишных платформ - не более 1 шарика}
{Одна дуга на каждую финишную платформу}
{Нр каждой из финишных платформ - не более 1 шарика}
a[N+i,1]:-Finish : c[N+i.Finish]:-!: end:
close(input): end:
Procedure OutputData:
begin
assign(output.'game.out'): rewriteCoutput):
Max:-0;{Количество шариков из истока на стартовые
платформы}
for i:-1 to N1 do Max:-Max+f[O,i]:
writeln(Max);
close(output);
end;
Листинг 1.4. Текст программы к задаче «Новогодняя вечеринка» (метод Форда-Фалкерсона)
{R+.S+}
program us02win2; const
MaxN - 200:
MaxD - 100;
var NКvar NК:3..MaxN: :1..5;:3..MaxN: :1..5;MaxV - MaxN+MaxD+1;
var N
К
var N
К
:3..MaxN: :1..5;
:3..MaxN: :1..5;
{Количество коров}
{Количество блюд на одну корову}
Листинг 1.4 (продолжение)
D:5..MaxD:{Количество видов пищи}
Lim:array [1..MaxD]of integer;{Предел количества блюдна вид пищи}
Z:integer:{Количество типов блюд,которое короваможет
приготовить}
FoodID : integer:{Идентификатор блюда. которое корова может пригото
вить}
Мах : integer;{Максимальное количество блюд.которое может быть
принесено на вечеринку}
а :array[0..MaxV.1..MaxV] ofinteger: {Граф исток-коровы-блюда-сток}
Ка :array[0..MaxV]of integer;{Количество ребер из вершины}
c.f.cf :array[0..MaxV,0..MaxV] ofbyte: {Веса вершин в графе А}
i.j. Finish : integer:
ia: array [0..MaxV. 1..MaxV] of integer: {Список входящих вершин}
Kia: array [0..MaxV] of integer;{Количество входящих ребер}
N-коров. D-блюд. К-препел блюд}{Инициализируем пустой граф} {исток-коровы-блюда-сток}N-коров. D-блюд. К-препел блюд}{Инициализируем пустой граф} {исток-коровы-блюда-сток}
N-коров. D-блюд. К-препел блюд}
{Инициализируем пустой граф} {исток-коровы-блюда-сток}
N-коров. D-блюд. К-препел блюд}
{Инициализируем пустой граф} {исток-коровы-блюда-сток}
Procedure Init:
begin
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];
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:
for 1:-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:
Procedure InputData;
begin
assign( input.'party.in'); reset(input);
read (N.K.D):{Прочитали
Finish :- N+D+l:
for i:-0 to Finish do
begin
for j:-0 to Finish do a[i.j]:-0;
ka[i]:-0:
end;
{Нулевой поток f(u,v)}
{Построение списков входящих дуг}
{По всем вершинам}
{по всем исходящим дугам}
{По всем вершинам}
{по всем исходящим дугам}
{инкремент к-ва дуг. входящих в a[i.j]}
{запоминаем вершину 1. как входящую в a[i.j]}
{Добавляем вершину-исток)
ka[0]:-N:{Из истока - N дуг - к каждой корове}
for i:-1 to N do
begin
a[0,i]:-i;{Номер вершины, соединенной с истоком}
с[0.1]:-К;{ее вес - максимальное кол-во блюд для коровы}
end;
for i:-1 to D do read (Lim[i]);{Пределы блюд каждого типа}
{Добавляем вершину-сток}
for 1:-N+l to N+D do begin ka[i] :-l;
a[i,1]:-Finish :{одна дуга от каждого блюда к стоку}
c[i,Finish]:-Lim[i-N]; {ее вес - предел числа блюд данного типа}
{Добавляем вершины от коров к блюдам}{Для каждой коровы}{Количество блюд, которые она готовит} {Для каждого такого блюда}{Читаем его номер}{Добавляем дугу от коровы к блюду} {Вес такой дуги - 1}{Добавляем вершины от коров к блюдам}{Для каждой коровы}{Количество блюд, которые она готовит} {Для каждого такого блюда}{Читаем его номер}{Добавляем дугу от коровы к блюду} {Вес такой дуги - 1}end:
{Добавляем вершины от коров к блюдам}
{Для каждой коровы}
{Количество блюд, которые она готовит} {Для каждого такого блюда}
{Читаем его номер}
{Добавляем дугу от коровы к блюду} {Вес такой дуги - 1}
{Добавляем вершины от коров к блюдам}
{Для каждой коровы}
{Количество блюд, которые она готовит} {Для каждого такого блюда}
{Читаем его номер}
{Добавляем дугу от коровы к блюду} {Вес такой дуги - 1}
for 1:-1 to N do
begin
read(Z): ka[i]:-Z:
for j:-l to Z do
begin
read(FoodlD):
a[i. j]:-N+FoodID;
c[i.a[i.j]]:-l: end
end:
close(input):
end:
Procedure OutputData;
begin
a s s i gn (out put.' pa rty. out'); rewrite (out put): 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 [0..MaxV] of integer;
{Поиск в ширину}{Поиск в ширину}Function ExistPathCvar KR.Cmin:integer):boolean;
{Поиск в ширину}
{Поиск в ширину}
{с помощью очереди}{с помощью очереди}Листинг 1.4 (продолжение)
{с помощью очереди}
{с помощью очереди}
const
WHITE - 1:
GRAY - 2: var
color, back:array [0..MaxV]ofinteger;
Q:array [0..MaxV]ofinteger:
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:
{Начальная вершина - обработана}{Начальная вершина - обработана}{Начало очереди} {Очередь пуста} {Количество дуг в пути - 0} {Поместить в очередь начальную вершину}{Начало очереди} {Очередь пуста} {Количество дуг в пути - 0} {Поместить в очередь начальную вершину}and not Find doand not Find do{Пока очередь не пуста и путь не найден}{Пока очередь не пуста и путь не найден}
{Начальная вершина - обработана}
{Начальная вершина - обработана}
{Начало очереди} {Очередь пуста} {Количество дуг в пути - 0} {Поместить в очередь начальную вершину}
{Начало очереди} {Очередь пуста} {Количество дуг в пути - 0} {Поместить в очередь начальную вершину}
and not Find do
and not Find do
{Пока очередь не пуста и путь не найден}
{Пока очередь не пуста и путь не найден}
begin
for i--0 to Finish for i:-0 to Finish color[0]:-GRAY; back[0]:-l;
BegQ:el:
EndQ:-0;
KR:-0:
Put(0);
Find:=false: while (BegQ<-EndQ)
do color[i]:-WHITE;
do Q[i]:--1:
{Все вершины свободны} {Все вершины свободны}
begin
Get(i);
j:T;
while (a[i.j]>0) and not Find do begin
if (color[a[i.j]]-WHITE) and (cf[i.a[i.j]]>0)
then begin
Put(a[i.j]): back[a[i,j]]:-i: color[a[i,j]]:-GRAY:
{Взять вершину i из очереди} {номер дуги из вершины 1}
{пока дуги не кончились}
{если вершина a[i.j] свободна}
{поставить ее в очередь}
{в вершину a[i.j] - из вершины 1} {пометить вершину a[i.j]} {как использованную}
Find :- a[i.j]-Finish: end: inc(j): end: j:-l: while (ia[i,j]>-0) and not Find do begin if (color[ia[i.j]]-WHITE) (cf[i,ia[i.j]]>0) then beginPut(ia[i.j]);back[ia[i.j]]:-i:Find :- a[i.j]-Finish: end: inc(j): end: j:-l: while (ia[i,j]>-0) and not Find do begin if (color[ia[i.j]]-WHITE) (cf[i,ia[i.j]]>0) then beginPut(ia[i.j]);back[ia[i.j]]:-i:color[ia[i,j]]:-GRAY;Find :- ia[i.j卜Finish:color[ia[i,j]]:-GRAY;Find :- ia[i.j卜Finish:
Find :- a[i.j]-Finish: end: inc(j): end: j:-l: while (ia[i,j]>-0) 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:
Find :- a[i.j]-Finish: end: inc(j): end: j:-l: while (ia[i,j]>-0) 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[i,j]]:-GRAY;
Find :- ia[i.j卜Finish:
color[ia[i,j]]:-GRAY;
Find :- ia[i.j卜Finish:
end;
inc(j): end;
end;
KR:-0: i:-Finish: Cmin:- maxi nt: while (ioO) do begin
Inc(KR):
path[KR]:-i:
1:-back[i]:
if Cm1n>cf[i,path[KR]]
then Cmin:-cf[i,path[KR]]; end;
path[kR+l]:-0;
ExistPath:-Find;
{взять следующую дугу}
{Проход по отрицательным ребрам} {номер дуги из вершины 1}
{пока дуги не кончились}
{если вершина a[i,j] свободна}
{поставить ее в очередь}
{в вершину a[i.j] - из вершины 1} {пометить вершину a[i.j]} {как использованную}
{взять следующую дугу}
{Восстановление пути из очереди}
{Начинаем с конца}
{Пока не начало}
{Увеличиваем к-во дуг в пути} {Заносим номер предыдущей вершины} {Меняем текущую вершину}
{Минимальный добавляемый поток}
{Нулевой поток f(u.v)} {CMin - минимальный из cf(u.v)-c(u.v)-f(u.v))} {Для каждой дуги увеличивающего пути}{Нулевой поток f(u.v)} {CMin - минимальный из cf(u.v)-c(u.v)-f(u.v))} {Для каждой дуги увеличивающего пути}
{Нулевой поток f(u.v)} {CMin - минимальный из cf(u.v)-c(u.v)-f(u.v))} {Для каждой дуги увеличивающего пути}
{Нулевой поток f(u.v)} {CMin - минимальный из cf(u.v)-c(u.v)-f(u.v))} {Для каждой дуги увеличивающего пути}
end;
begin
Init:
While ExistPath(KR.Cmin) do for 1:-KR downto 1 do begin
{U->V - дуга с номером i в пути}
u:- path[i+l];
Листинг 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.
Замечания по реализации
Возможно, некоторым читателям покажется чрезвычайно неэкономным использование оперативной памяти в приведенных выше решениях. Ничуть не пытаясь оспаривать это, автор тем не менее отмечает следующее.
Современные компиляторы и операционные системы, в которых они работают, снимают привычные для DOS-приложений (например, Turbo Pascal) ограничения на статический размер сегмента данных (64 Кбайт). Более того, они поощряют использование массивов данных больших размеров оптимизацией работы с оперативной и дисковой памятью на уровне приложений (страничная организация памяти, динамическая подгрузка страниц, оптимальные алгоритмы вытеснения страниц и т. д.).
В международных олимпиадах по информатике для школьников (IOI) и студенческих командных олимпиадах по программированию для студентов (АСМ ICPC) приветствуется использование 32-битных компиляторов (Free Pascal, Delphi, GNU С и др.).
Основной целью книги является максимально простое н понятное изложение идей, сводящих олимпиадные задачи к задачам на построение максимального потока и применение алгоритма Форда-Фалкерсона. Потому и были выбраны наиболее простые, а возможно, и избыточные структуры данных.
Как уже упоминалось, задача «Новогодние вечеринки» могла решаться с использованием 32-битного компилятора FreePascal. Задача «Кубики» решается и с использованием компилятора Turbo Pascal в операционной системе DOS при правильном подборе величины МахС. Наконец, модификация авторского решения задачи «Игра», чтобы оно могло быть выполнено в DOS с помощью Turbo Pascal (как это и предлагалось на белорусской республиканской олимпиаде в 2001 году) является неплохим домашним заданием для вдумчивых читателей.
Автор полагает, что методы оптимального использования оперативной памяти и использование сложно организованных структур хранения данных заслуживают специального раздела, посвященного рассмотрению этих и только этих вопросов.
Глава 2. Минимальное остовное дерево
Данная глава посвящена теме «Минимальное остовное дерево» и имеет следующую структуру. Вначале приводится формальная постановка задачи построения минимального остовного дерева заданного графа. Затем на примере решения достаточно простой задачи «Веревочный телеграф» объясняются два основных алгоритма (Прима и Крускала), применяемых для построения минимального остовного дерева. Дальше приводятся полные решения трех олимпиадных задач: <Secret Milk Pipes», <Метро» и «Network».
Формальная постановка задачи
Взвешенный граф служит математической моделью широкого класса задач на нахождение минимального остовного дерева. Для примера рассмотрим граф с четырьмя вершинами, которые пронумерованы от 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
Примерами минимального остовного дерева для данного графа могут быть его подграфы, показанные на рис. 2.2, 〃一z:
Рис. 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, а-г (изменились веса некоторых входящих в остовные ц)афы ребер):
Рис. 2.3. Примеры остовных деревьев с весами
Теперь суммы весов, приведенных выше четырех деревьев, будут 3, 7, 7, 7 соответственно, и минимальное остовное дерево будет единственным (рис. 2.4): Заметим также, что в остовном дереве для графа из N вершин всегда содержится N-1 соединяющих их ребер.
Формально минимальное остовное дерево определяется следующим образом: пусть имеется связный неориентированный граф G=(匕Е ), где V— множество
его вершин, а £— множество его ребер. И пусть для каждого ребра графа (“, и) (соединяющего вершины и и v) задан его неотрицательный вес Q(〃,己).Задача нахождения минимального остовного дерева заключается в нахождении такого подмножества Г ребер исходного гра巾а, которое связывает все вершины графа G и — для которого суммарный вес всех ребер минимален.
1 13
2 4
Рис. 2.4. Минимальное остовное дерево
Далее приводятся формулировки реальных олимпиадных задач, которые сводятся к нахождению минимального остовного дерева, описываются два алгоритма построения минимального остовного дерева — алгоритм Прима и алгоритм Крускала и показывается их применение для решения приведенных задач.
Алгоритм Прима
Гомельская городская олимпиада, 1999
Веревочный телеграф
Тимур и его друзья, приехав летом на свои старые дачи, решили устроить на время своего отдыха игру. Они организовали команду, чтобы тайно помогать жителям дачного городка в их повседневных делах.
Дачный поселок довольно большой, и дома, в которых живут друзья Тимура, расположены далеко друг от друга. Как быстро передавать друг другу сообщения? Как собирать ребят на совет?
Тимур решил проложить веревочный телеграф, который связал бы все домики, в которых живут ребята из его команды. Всего домиков N. По карте ребята вычислили координаты каждого домика (Xi, Yi) в целых числах и выписали их на бумаге. За единицу измерения координат они взяли один метр. Однако возник вопрос: какие домики нужно соединять веревочным телеграфом, чтобы связь была между всеми домиками (возможно, через другие домики), а общая длина всех веревок была как можно меньше?
Требуется написать программу, которая по координатам домиков определяла бы, какова минимальная общая длина всех веревок, соединяющих все домики между собой (возможно, через другие домики).
48
Глава 2 • Минимальное остовное дерево
Порядок ввода
Порядок вывода
N
X1 Y1
Х2 Y2
Х.хх
Xn Yn
где Хлх — минимальная общая длина веревки с точностью до двух знаков в дробной части.
Пример ввода
Пример вывода
5
100 200
200 200
300 400
400 200
400 100
623.61
Ограничения'.
0<N<100
-32 000 4 Xi, Н« 32 000
Рассмотрим домики тимуровцев как вершины графа, веревки между ними — как ребра графа, а длины веревок — как веса ребер. Теперь перед нами задача о минимальном остовном дереве. Тело программы, решающей эту задачу, может выглядеть следующим образом:
begin
InputData:
MinTree:
OutputData: end.
{Ввод исходных данных}
{Построение минимального остовного дерева} {Вывод результата}
В данном случае исходный граф — полный, то есть существует ребро между любыми его двумя вершинами, поскольку по условиям задачи веревки можно протянуть между любыми двумя домиками.
В данной задаче удобно представлять граф в виде матрицы весов ребер: D[ifj]— расстояние между вершинами i и / Ввод данных, обеспечивающий вычисление D[i,j] по исходным данным задачи, представлен ниже и не требует комментариев.
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:-1 to N do
for j:-l to N do
D[i.j]:-sqrt(sqr(x[i]-x[j])+sqr(y[i]-y[j])).
end:
Результат работы алгоритма построения остовного дерева методом Прима будет представлен в виде массива Pred [1..N]: Pred [z] = j, то есть предком вершины i в остовном графе будет вершина/ или, другими словами, минимальное остовное дерево будет содержать ребро (i, j). У вершины 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 снабжена также приоритетом — специальной характеристикой Кеу[〃,которая равна минимальному весу ребер, соединяющих вершину i с вершинами из А.
Более формально алгоритм Прима может быть записан следующим образом:
Ставим в очередь Q все вершины исходного графа
Устанавливаем для всех вершин Кеу[1] - бесконечность (MaxD)
Заносим в дерево А вершину 1:
кеу[1] -0 - расстояние от нее до вершин дерева А
Pred[l]-0 - у корневой вершины нет предка
Пока очередь Q не пуста
Выбираем из очереди вершину U такую, для которой расстояние
от нее до вершин из множества А минимально
Для каждой вершины V. соседней с U
(то есть в графе имеется ребро (U.V)).
если V находится в очереди и D[U,V]<key[V]
то Pred[V]-U key[V]-D[U.V]
Ниже приведена реализация алгоритма Прима:
procedure Mi пТгее.
Const
MaxD -1е16: {"бесконечность"}
Free =0:
Used -1:
var
Lbl:array[1..MaxN]ofbyte:
Key:array[1..MaxN]ofsingle;
u.v:byte:
Min:single.
begin
for i -1 to N do
begin
key[i]:*MaxD;
Lbl[i]:-Free: { Все вершины в очереди}
end:
Кеу[1]:-0:
Pred[l]:-0:
for i:=1 to N Do
{ Начнем построение с остова с 1-й вершины}
{ У 1-й вершины нет предка}
{ Для всех вершин }
begin
Min.=MaxD;
for j:-l to N Do
{ Находим вершину. }
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;
Далее приводится полное решение задачи «Веревочный телеграф» алгоритмом Прима.
Алгоритм Крускала
Для решения приведенных далее задач «Secret Pipes» , «Метро» и «Network» удобнее применять метод Крускала, и поскольку придется его модифицировать, то рассмотрим вначале его применение в «чистом виде» для решения задачи «Веревочный телеграф».
В тело главной программы внесено существенное изменение:
begin
InputData:
QuickSortR(l.N*(N-l) div 2):
MinTree_Kruskal:
OutputData: end.
Алгоритм Крускала работает на множестве У ПОРЯ ДО ЧЕННЫХ ПО ВОЗРАСТАНИЮ весов ребер R[k]. Поэтому перед началом его работы НЕОБХОДИМО произвести переупорядочивание ребер по возрастанию весов. Эту работу и выполняет рекурсивная процедура QuickSortR. которой при начальном вызове передаются номера начального (номер 1) и конечного (номер ^(^ -1)/2 ) ребер. В данном случае рекомендуется применять алгоритм быстрой сортировки, который ввиду своей важности рассматривается отдельно в следующем пункте.
Ввод данных тоже приходится немного изменить.
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-l 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:
Здесь граф представлен как набор из N(N -1)/2 ребер, где:
R[A] — вес ребра (длина веревки) с номером А;
B[k] — начальная вершина ребра;
E[k] — конечная вершина ребра.
Ответ будет накапливаться в переменной 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:-1 to N do Makeset(i):
Вначале строятся N(no числу вершин) непересекающихся множеств, каждое из которых хранит номера вершин, в него входящих (вначале каждое такое множество хранит только одну вершину — с номером вершины, соответствующим номеру множества).
Далее осуществляется цикл по всем ребрам (B[i], E[i]) с весом R[i] (в порядке возрастания весов /?[/]). Если ребро (B[i], E[i]) соединяет два разных множества (FindSet(B[i]) х FindSet(E\iJ), то это ребро включается в минимальный остовный граф, его вес добавляется к ответу: Answer, = Answer + R[i]t а сами множества объединяются (как связывающиеся данным ребром): Union(B[i]fE[i]);
Рассмотрим подробнее процедуры, которые реализуют работу с множеством непересекающихся подмножеств вершин в процессе построения минимального остовного дерева. Все они работают с двумя массивами: Pred[i..N] и Rank[i..N]. Массив Pred[i] содержит номер вершины — предка. Массив Rank[i] содержит ранг вершины. Понятие ранга вводится для ускорения обработки.
Как уже говорилось, процедура MakeSet осуществляет начальное построение системы независимых множеств вершин:
procedure MakeSet(x:longint);
begin
Pred[x]:-x:{вначале вершина ссылается сама на себя}
гапк[х]:-0;{ранг ее равен 0}
end;
Рекурсивная функция FindSet(x) отвечает на вопрос: «Какому множеству из построенной системы подмножеств принадлежит вершина х?» Каждое из подмножеств фактически представляет собой поддерево, и функция FindSet возвращает номер вершины, которая является корнем этого поддерева.
function FindSet(x:1ongint):longint:
begin
if xopredfx]
then Pred[x]:-FindSet(pred[x]): FindSet:-Pred[x]: end:
Процедура Union(x, у) вызывается в том случае, если вершины хну лежат в разных поддеревьях, и служит для объединения этих поддеревьев. При этом она находит корни соответствующих поддеревьев, вызывая рекурсивную функцию FindSet два раза (Findset(x) и FindSet^y)), а затем вызывает процедуру Link для объединения деревьев с заданными корнями:
Procedure Union(x.y:longint);
begin
Link (FindSet(x).FindSet(y)): end:
И, наконец, процедура Link(xf у) осуществляет объединение деревьев с корнями хиу и перестроение рангов вершин, входящих в эти деревья.
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;
Напомним, что ранги вершин обеспечивают сокращение количества операций (и соответственно времени), затрачиваемых на обработку системы непересекаю- щихся поддеревьев.
Полное решение задачи «Веревочный телеграф» методом Крускала приводится далее.
Быстрая сортировка
Быстрая сортировка участка массива Л от р до /т происходит следующим образом:
элементы массива R переставляются так, что бы любой из элементов Л[р],...» R[q] был не больше любого из элементов R[g+1],..., R[rr]・ Эта операция называется разделением (partition). Здесь q — некоторое число в интервале p^q<rr\
процедура сортировки рекурсивно вызывается для массивов R[p・.g] и R[q+1, гг].
procedure QuicksortRCp.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 Parti11on(p,гг•1ongint):1ongint;
var
i .J.z : longint:
.t • single: begin
x:- R[p]:
i.ep-1: j:-rr+l; while i<j do begin
repeat j:*j-1 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;
В качестве «граничного элемента» принимается X = R[p]. Далее заводим два указателя: i — при просмотре элементов с начала сортируемого участка, и j — при просмотре элементов с конца сортируемого участка. Пока г < j, выполняется следующее: находим R[j] — первый с конца элемент меньше X, и R[i] — первый с начала элемент, больший X. Если i </ то меняем местами элементы R[i] и /?[;]: t: м /?[/]; R[il: = Л[Л; RL/]: = ±;
Поскольку в данной задаче нужно синхронно с перемещением весов ребер R[k] перемещать также вершины их начала и конца, выполняются также соответствующие операции:
z:-B[i]: B[i]:-B[j]: B[j]:-z:
z:-E[i]; E[i]:-E[j]; E[j]:«z;'
По завершении цикла «пока i < j» мы и получаем границу «разделения» Partition: = j. Понятно, что вызывать алгоритм быстрой сортировки нужно, передавая ему вначале номера начального и конечного элементов массива, например, для задачи «Веревочный телеграф»: QuicksortR(i, N*(N-1) div 2).
Задача «Secret Pipes»
US ACO Open 2002, Green Division
Secret Pipes
Фермер Джон хочет как можно дешевле организовать свою систему распределения воды, но он не хочет, чтобы его конкурент фермер Плуто мог предсказать маршруты, которые он выбирает. ФД знает, что такая задача обычно требует самого дешевого способа прокладки труб поэтому он решил использовать второй по стоимости способ.
Дан список всех двунаправленных труб, которые могут соединять множество из W(3 К W4 2 000) станций с водой (каждая из которых может быть встроена в колодец). Ваша задача — найти второй из самых дешевых способов соединить насосные станции, используя не более чем Р(Р< 20 000) труб с заданной стоимостью каждой трубы. Не должно быть трубы, соединяющей станцию саму с собой. Не должно быть двух труб, соединяющих дважды одну и ту же пару станций.
Гарантируется, что есть только один самый дешевый способ распределить воду, и что существует, как минимум, два способа распределить воду. Все стоимости —положительные числа, помещающиеся в 16-битное целое. Водная станция идентифицируется своим номером — целым числом в диапазоне 1..1V.
Ввод:
строка 1- два разделенных пробелом целых числа, WnP;
строки 2..Р + 1 — каждая строка описывает одну трубу и содержит 3 числа, разделенных пробелом, — номера станций начала и конца трубы, а также стоимость этой трубы.
Пример ввода: 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 P do readln(B[i].E[i].R[i]); close(input): end:
Теперь решение задачи можно обеспечить следующим образом.
Найдем минимальное остовное дерево.
«Удаляя» в цикле по спереди одно из найденных ребер минимального остовного дерева и достраивая оставшийся нецельным остов до минимального остовного дерева, вычисляем стоимости получающихся деревьев и запоминаем меньшую.
Для нахождения минимального остовного дерева используем алгоритм Крускала: procedure MinTree_Kruskal_l: begin
Answer:-0: j:-0: for i:-l to N do MakesetCi); for i:-l to P do if (F i nd set(В[i])<>F i ndSet(E[i])) then begin
inc(j): Key[j]:-i;
Union(B[i].E[i]): end: end;
Легко заметить, что в процедуру поиска минимального остовного дерева вместо строки увеличения ответа Xnswer * Answer + R[i] введена строка шс(/); Key\j]:= которая сохраняет в массиве Key номера всех ребер, включенных в минимальное остовное дерево.
Тогда тело главной программы может выглядеть следующим образом:
begin
InputData:
QuickSortR(l.P):
MinTree_Kruskal_l:
for i:-l to P do NotMasked[i]:-true:
CurAnswer:-maxint;
for v:-l to N-l do
begin
NotMasked[key[v]]:-false:
MinTree_Kruskal_2;
if Answer<CurAnswer then CurAnswer:-Answer;
NotMasked[key[v]]:-true;
end;
assign (output.'secret.out*); rewriteCoutput):
writeln(CurAnswer):
close(output); end.
Здесь первые три строки
InputData:
QuickSortR(l.P):
M1nTree_Kruskal_l:
обеспечивают построение минимального остовного дерева, где Key[i] (для i от 1 до N- 1) — номера ребер остовного дерева. Затем отмечаем все ребра как свободные :
for 1:-1 to P do NotMasked[i]:-true:
и инициализируем поиск минимального ответа:
CurAnswer:-maxint:
Далее в цикле по всем ребрам, включенным в минимальное остовное дерево,
for v:-l to N-l do
помечаем текущее ребро минимального остовного дерева как занятое,
NotMasked[key[v]]:-false;
вызываем алгоритм построения минимального остовного дерева, который учитывает, можно ли использовать при построении текущего дерева данное ребро.
MinTree_Kruskal_2;
Среди получемых ответов Answer т\ш минимальный.
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 MakesetCi);
for i:-l to P do
if (FlndSet(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])
то есть если вершины находятся в разных поддеревьях и текущее ребро не маскировано. Однако такое решение на тестах максимальной размерности (2000 вершин, 20 000 ребер) не проходит по времени исполнения. Задумаемся: а можно ли сократить объем работы, выполняемой в процедурах Min Tree_Kruskal_ 1 и MinTree^Krus- А〃/_2? Да, можно — из следующих соображений.
В обеих процедурах мы осуществляем перебор по всем ребрам fori^l toPdo ..., в то время как достаточно перебирать ДО НАХОЖДЕНИЯ (N - 1)-го ребра минимального остовного дерева: while (Jv>N - 1) do, где j — номер последнего ребра, найденного для минимального остовного дерева
В процедуре Min Tree ^Kruskal_2 можно сделать несколько изменений с целью ее оптимизации:
начинать с остовного дерева из N — 2 ребер. Для этого достаточно в главной программе без перебора включить в минимальное остовное дерево ^- 2 найденных в процедуре Min Tree_Kruskal_ 1 ребер (поочередно исключая из рассмотрения одно из найденных ребер);
не вызывать процедуру Union;
просмотр ребер из множества заданных начинать с ребра, следующего за исключенным из просмотра ребром минимального отстовного дерева;
оператор if можно разбить на два, чтобы для помеченного ребра не вызывалась рекурсивная процедура FindSet.
Полный текст программы, решающей задачу «Secret Pipes», приводится в конце главы.
2.6. Задача «Метро»
Белорусская республиканская олимпиада, 2002.
Метро
В некотором городе есть метро, состоящее из N(1 <N< 1000) станций и М (0 4 М 4 500 000) линий, соединяющих их. Каждая линия обеспечивает проезд между какими-то двумя станциями в обе стороны. Между любой парой станций проведено не более одной линии. Сеть метро построена таким образом, чтобы с каждой станции можно было проехать на каждую (возможно, через промежуточные станции). Назовем это свойство связностью метро,
В связи с изобретением принципиально нового вида транспорта метро стало убыточным, и его работу решили прекратить. На заседании мэрии города было постановлено закрывать каждый год по одной станции, но так, чтобы связность метро каждый раз сохранялась. При закрытии какой-либо станции линии, ведущие от этой станции к другим, естественно, тоже перестают функционировать.
Задание'.
По введенной информации о сети метро разработать какой-либо порядок закрытия станций, при котором метро всегда будет оставаться связным. Например, пусть метро выглядит так, как показано на рисунке. Тогда станции можно закрывать, например, в порядке 1,2,4,3,5. А порядок 3,1,2,4,5 —не подходит, так как после закрытия 3-й станции метро распадется на четыре не связных части.
Ввод:
Первая строка входного файла будет содержать числа Nh М. В следующих Л/строках находится информация о линиях. Каждая из этих строк содержит через пробел числа Ai и Bi (Ai, Bi) — две станции, которые соединяет i-я линия.
Вывод'.
Выходной файл должен состоять из N строк. Каждая строка должна содержать одно число — номер станции. Вывести станции нужно в порядке их закрытия.
Пример вводаПримервывода
541
3 12
3 24
343
3 55
Естественно представить станции метро вершинами графа, а линии, их соединяющие, —ребрами графа. По условиям задачи длины линий не имеют значения,
поэтому мы можем задать значения весов всех ребер равными 1. Ввод исходных данных осуществляется следующим образом:
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[i]:-1:
end:
close(input): end:
Теперь нам достаточно найти остовное дерево (минимальность не требуется, сортировку вызывать нет необходимости), а затем обойти исходный граф поиском в глубину, используя только ребра, включенные в остовное дерево. Тело главной проц)аммы будет выглядеть следующим образом:
begin
InputData;
MinTree_Kruskal:
assign (output,'output.txt'): rewriteCoutput);
for i:-l to N do Color[i]:-FREE;
DFS(l):
close(output);
end.
Здесь Min Tree ^Kruskal — процедура построения остовного дерева, сохраняющая найденный подграф с помощью двух массивов:
kG [U] — количество ребер, связанных с вершиной U;
G[uJ] н и — из вершины U ребро с номером j ведет в вершину V.
Ниже приведен ее текст:
procedure MinTree_Kruskal;
begin
for i:-l to N do kG[i]:-O;
for i :-l to N do Makesetd):
for i:-l to P do
if (F1ndset(B[i])<>FindSet(E[i])) then
begin
inc(kG[B[i]]); G[B[i].kG[B[i]]]:-E[i];
1nc(kG[E[i]]); G[E[i].kG[E[i]]]:-B[i]:
Union(B[i].E[i]): end:
end;
Массив 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;
Неформально поиск в глубину с помощью массива Color можно пояснить так: при заходе в очередную вершину мы выясняем, есть ли из нее ребро к неокрашенным (непосещенным) вершинам. Если есть — идем туда. Если же у текущей вершины больше нет «потомков», значит, это «листовая» вершина, и ее можно выводить в результат. Полный текст решения задачи «Метро» приводится в конце главы.
Задача «Network»
NEERC, Северный четвертьфинал, 2001
Network
Андрей работает системным адм ин истратором и планирует создание новой сети в своей компании. Всего будет N хабов, они будут соединены друг с другом с помощью кабелей.
Поскольку каждый сотрудник компании должен иметь доступ ко всей сети, каждый хаб должен быть достижим от любого другого хаба — возможно, через несколько промежуточных хабов. Поскольку имеются кабели различных типов и короткие кабели дешевле, требуется сделать такой план сети (соединения хабов), чтобы максимальная длина одного кабеля была как можно меньшей. Есть еще одна проблема — не каждую пару хабов можно непосредственно соединять по причине проблем совместимости и геометрических ограничений здания. Андрей снабдит вас всей необходимой информацией о возможных соединениях хабов.
Вы должны помочь Андрею найти способ соединения хабов, который удовлетворит всем указанным выше условиям.
Ввод:
Первая строка входного файла содержит два целых числа: N—количество хабов в сети (2 <N< 1000) и М — количество возможных соединений хабов (1 ^М< 15 000). Все хабы пронумерованы от 1 до N. Следующие М строк содержат информацию о возможных соединениях — номера двух хабов, которые могут быть соединены, и длина кабеля, который требуется, чтобы соединить их. Эта длина — натуральное число, не превышающее 106. Существует не более одного способа соединить
каждую пару хабов. Хаб не может быть присоединен сам к себе. Всегда существует хотя бы один способ соединить все хабы.
Вывод:
Сначала выведите максимальную длину одного кабеля в вашем плане соединений (это величина, которую вы должны минимизировать). Затем выведите свой план: сначала выведите Р — количество кабелей, которые вы использовали, затем выведите Р пар целых чисел — номера хабов, непосредственно соединенных в вашем плане кабелями.
Пример:
Пример ввода
Пример вывода
46
1 2 1
1 3 1
1 42
23 1
34 1
24 1
1
4
1 2
1 3
23
34
Приведем краткую формализацию постановки задачи. Для заданного взвешенного графа найти такое остовное дерево, чтобы максимальный из весов ребер этого ос- товного дерева был минимальным, и вывести этот вес, количество ребер в остовном дереве и сами ребра (номера вершин, соединенных соответствующим ребром).
По построению минимального остовного дерева алгоритмом Крускала получается, что оно всегда является и решением поставленной задачи. Таким образом, решение задачи сводится к нахождению минимального остовного дерева алгоритмом Крускала, а затем к нахождению максимального по весу из входящих в него ребер.
beginInputData:QulckSortR(l.M).MinTree_Kruskal: Max:-RA[Key[l]]: for i:=1 to j dobeginInputData:QulckSortR(l.M).MinTree_Kruskal: Max:-RA[Key[l]]: for i:=1 to j doРассмотрим подробнее некоторые детали реализации. Поскольку количества вершин (до 1000) и ребер (до 15 000) таковы, что требуемые алгоритму Крускала структуры данных не поместятся в статической памяти Турбо-Паскаля, массивы начал, концов и весов ребер (В, Е и R, соответственно) размещаются в динамической памяти. Тело главной программы выглядит следующим образом:
begin
InputData:
QulckSortR(l.M).
MinTree_Kruskal: Max:-RA[Key[l]]: for i:=1 to j do
begin
InputData:
QulckSortR(l.M).
MinTree_Kruskal: Max:-RA[Key[l]]: for i:=1 to j do
{Ввод исходных данных}
{Сортировка ребер по возрастанию весов}
{Построение минимального остовного дерева}
{Нахождение максимального ребра в ост. дереве}
if RA[Key[i]]>Max then Max:-RA[Key[i]]:
OutputData:OutputData:{Вывод результата в требуемом формате}
OutputData:
OutputData:
епс
Процедуры QuickSortR. MinTree_Kruskalподвергнуты минимальным изменениям, вызванным переходом со статических массивов В, Е, R на динамические массивы: ВЛ, Ел, Лл. Полный текст решения задачи приведен в следующем разделе.
Решения задач
Листинг 2.1. Текст программы к задаче «Веревочный телеграф» (алгоритм Прима)
program gg99dlt2:
Const
MaxN - 100;
var
N.i.j : L.MaxN:
X.Y : array [1..MaxN] of longint;
D : array [1..MaxN.l..MaxN] of single:
Pred : array [1..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[i.j]:-sqrt(sqr(x[i]-x[j])+sqr(y[i]-y[j])): end:
procedure OutputData:
var
Answer : single:
begin
Answer:-0:
for i:-2 to N do
An swe г:-An swe r+D[i.P red[i]]:
assign (output.'output.txt'); rewriteCoutput):
writeln(Answer:0:2):
close(output): end;
procedure MinTree;
Const
MaxD -lel6:
Free -0:
Used -1;
var
Lbl: array [1..MaxN] of byte:
Key: array [1..MaxN] of single:
Листинг 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
{ Начнем построение с остова с 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 doLbl[u]:-Used:for v:-l to N do{ Помечаем ее как использованную}
Lbl[u]:-Used:
for v:-l to N do
Lbl[u]:-Used:
for v:-l to N do
{ Для всех оставшихся вершин}
if (Lbl[v]-Free) and (D[u.v]<Key[v]) { Пересчитываем } then
Key[v]:-D[u.v]:Pred[v]:-u: end:Key[v]:-D[u.v]:Pred[v]:-u: end:begin
Key[v]:-D[u.v]:
Pred[v]:-u: end:
Key[v]:-D[u.v]:
Pred[v]:-u: end:
{ расстояние до остова } { Переустанавливаем предка}
end: end:
begin
InputData;
MinTree:
OutputData: end.
Листинг 2.2. Текст программы к задаче «Веревочный телеграф» (алгоритм Крускала)
program gg99dlt2;
Const
MaxN - 100:
var
N.i.j:longint.
X.Y:array [1..MaxN]oflongint:
Pred.Rank:array [1..MaxN]oflongint:
k.t.m.p.rr:longint:
R: array [0. 4950] of single:
B.E : array [1..4950] of O..MaxN:
Answer.Min : single;
procedure InputData:
begin
assign (input.'input.txf): reset(input): readln(N).
for i:-l to N do readln(x[i].y[i]). close(input):
к :-0;
for i:-l to N-l 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.O2):
close(output).
end:
procedure MakeSet(x:longint):
begin
Pred[x]:-x.
rank[x] =0:
end:
function FindSet(x:1ongint):1ongint:
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
Листинг 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])<>FinclSet(E[i]) then
begin
Answer:-Answer+R[i]: Union(B[i].E[i]); end: end:
function Partition(p.rr:longint):longint: var
i.j.z : longint;
x.t : single:
begin
x:- R[p]:
i :-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:
procedure QuicksortRCp,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.3. Текст программы к задаче «Secret Pipes»
{$E+.N+.R+・O+} program us02ope2:
Const
MaxN - 2000:
MaxP - 20000;
Free -0;
Used -1:
var
N
1..MaxN:
P
1..MaxP:
i.J
integer;
Pred.Rank
array [1..MaxN] of integer;
В. E
array [1..MaxP] of integer:
NotMasked
array [1..MaxP] of boolean;
Key
array [1..MaxN-1] of integer;
Answe г.Mi nAnsweг.Си гAnsweг : longint;
u.v: integer;
R: arraytl..MaxP] of integer;
procedure InputData;
begin
assign (input.'secret.in'); reset(input);
Листинг 2.3 (продолжение)
readln(N.P):
for i:-l to P do readln(B[i].E[i].R[i]): close(input):
end;
function Partition(p.rr:longint)rlongint: var
i .j.z : longint;
x.y : integer:
begin
x:- R[p]; i:-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 1<j
then begin
y:-R[i]; R[i]:-R[j]; R[j]:=y; 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:
procedure QuicksortR(p.rr:longint);
var
q : longmt:
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;
гапк[х]:-О: 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
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 JoN-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;
Листинг 2.3 (продолжение)
var к : longint;
FindSetConst:array [1..MaxN] of integer; begin While (J<>N-1) do begin if (NotMasked[i]) then if (Findset(B[i])<>FindSet(E[i])) then begin
An swe г:-An swe r+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:-maxlongint: MinAnswer:-Answer: for v:-l to N-l do begin
NotMasked[key[v]]:-false; Answer:-MinAnswer-R[key[v]]: for i:-l to N do Makeset(i): for u:-l to N-l 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-l do NotMasked[key[u]]:-true: end;
assign (output.'secret.out*): rewrite(output): writeln(CurAnswer): close(output):
end
Листинг 2.4. Текст программы к задаче «Метро*
program us02ope2:
Const
MaxN - 1000:
МахР - 500000:
MaxShortI nt - 32767;
Free -0;
Used -1:
var
N P
Lj
Pred.Rank.Color
В. E R kG
G
1..MaxN;
1..MaxP; integer: array [1..MaxN]of integer;
array [1..MaxP]of integer:
array[l..MaxP] of integer; array[l..MaxN] of integer; arraytl..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[i]:-1:
end:
close(input);
end;
procedure MakeSet(x:longint);
begin
Pred[x]:-x;
rank[x]:-0:
end:
function FindSet(x:1 ongint) Jongint;
begin
if x<>pred[x]
then Pred[x]:-FindSet(pred[x]); FindSet:-Pred[x]:
end;
Procedure Link(x.y:longint);
Листинг 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 i -1 to P do
if (Findset(B[i])<>FindSet(E[i]))
then
begin
1nc(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):
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:
begin
InputData:
MinTree Kruskal:
assign (output.'output.txt'): rewriteCoutput);
for i:-l to N do Color[i].=FREE:
DFS(l): close(output): end.
Листинг 2.5. Текст программы к задаче «Network»
program n401dlt3;
Const
MaxN -1000:
MaxP - 15000:
Type
TMas - array [1..MaxP] of longint;
PMas - TMas: var
N:L.MaxN:
M:L.MaxP:
.j:integer;
Key:array [1..MaxN-1] of integer:
Pred.Rank : array [1..MaxN] of integer:
E. R:PMas;
Max:longint:
procedure InputData;
begin
assign (input.'input.txt'): reset(input).
readln(N.M);
B:=New(PMas): E:-New(PMas): R:-New(PMas):
for i:-l to M do readln(Bx[i].EA[i].Rx[i]): close(input); end.
function Partition(p.rr:1ongint):1ongint: var
i. j.z : longint:
x.y : longint:
begin
x:- RA[p]:
i:-p-l;
j:-rr+l:
while i<j do
begin
repeat j:»j-l until RA[j]<-x: repeat i:-i+l until RA[i]>-x;
Листинг 2.5 (продолжение)
if i<j then begin y:-RA[i]: RTU:=RTj]; RA[j]:-y; z:-BA[U: BA[i]:-BA[j]; BA[j]:-z; z:-EA[i]: ETU:=ETj]: ETj]:=z: end;
end:
Partition:-j; end:
procedure QuicksortRCp,rr:longint):
var
q : 1ongint:
begin
if p<rr then begin q:- Partition(p.rr); QuicksortRCp.q): QuicksortR(q+l.rr);
end; end;
procedure MakeSet(x:longint);
begin
Pred[x]:-x:
rank[x]:-0: end:
function FindSet(x:1ongint):1ongint: begin
if x<>pred[x]
then Pred[x]:-Fi ndSet(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.y:longint);
begin
Link (FindSet(x).FindSet(y));
end:
procedure MinTree_Kruskal;
begin
j:-0;
for 1:-l to N do Makeset(i):
:-l:
While J<>N-1 do
begin
if (Findset(B*[i])<>FindSet(ETi])) then
begin
inc(j); Key[j]:-i;
Union(BA[1],Ex[i]):
end:
inc(i):
end;
end:
Procedure OutputData:
begin
assign (output.'output.txt'): rewrite(output):
writeln(Max):
wrlteln(J):
for 1:-1 to J do writein (Вж[Кеу[1]].' ',Еж[Кеу[1]]): close(output):
end:
begin
InputData:
QulckSortR(l.M):
MinTree_Kruskal:
Max:-RA[Key[l]J:
for 1:-l to j do
if RA[Key[i]]>Max then Max:-R*[Key[i]J;
OutputData:
end.
Глава 3. Решение задач на деревьях и с помощью деревьев
В ранее выпущенной книге автора ([2]) уже рассматривались некоторые методы решения задач на графах, в том числе метод Флойда, метод Дейкстры, поиск в глубину, поиск в ширину. В первых двух главах этой книги рассмотрены построение минимального остовного дерева и построение максимального потока.
Материал 3Toii главы посвящен методам и средствам решения задач на деревьях и с помощью деревьев. Дерево, являясь разновидностью графа, имеет множество эквивалентных определений. Возьмем в качестве базового такое определение: «Деревом называется связныii гра(|)без циклов».
ПРИМЕЧАНИЕ Ва*мо отметить также, что пустой граф (то есть граф, не содержащий ни вершин, ни ребер) также считается деревом. Это дополнение введено для удобства обобщения многих алгоритмов на деревьях.
В качестве аналогии хочется привести пример определения факториала: факториал натурального числа Л/ определяется как N! = 1・2・3・...・N , а 0! доопределен искусственно и равен 1.
Рассмотрим основные свойства деревьев, вытекающие из приведенного определения, иллюстрируя их на примере, показанном на рис. 3.1.
Рис. 3.1. Пример дерева с пятью вершинами
Связность дерева обеспечивает наличие пути из любой вершины в любую.
Отсутствие циклов в дереве гарантирует ЕДИНСТВЕННОСТЬ такого пути.
口 Количество ребер (£) pi вершин (V) в дереве связаны соотношением £» V- 1 (в примере 5 вершин и 4 ребра).
Важным классом деревьев является дерево с предопределенным корнем и установленным отношением «предок-потомок» {ancestor, descendant). Такие деревья называются корневыми деревьями (rooted tree). Вершины, имеющие потомков, называются внутренними. Вершины, не имеющие потомков, называются внешними, или листьями. Например, корневое дерево может иметь вид, показанный на рис. 3.2, или вид, показанный на рис. 3.3:
Рис. 3.2. Пример корневого дерева, вариант 1
или
221133
2
2
1
1
3
3
4
Рис. 3.3. Пример корневого дерева, вариант 2
Здесь вершины 1 и 3 — внутренние, вершины 2 и 4 — листья.
Заметим, что определением корневых деревьев ЗАПРЕЩЕНО «множественное наследование». То есть, например, граф на рис. 3.4 не является корневым деревом (если считать, что направленные дуги идут сверху вниз — от вершин 1 и 3 к вершине 2, от вершины 2 — к вершинам 4 и 5).
Рис. 3.4. Пример графа, не являющегося корневым деревом
У корневых деревьев свойство единственности пути может быть переформулировано следующим образом: для любой вершины дерева существует единственный путь к ней из корневой вершины. В дальнейшем в этой главе мы будем употреблять название дерево, имея ввиду именно корневое дерево. Заметим, что корневые деревья активно используются в реальной жизни и программистской практике.
Очевидно, что каждый читатель, хотя бы на интуитивном уровне, многократно сталкивался с подобным отображением реалий. Прежде всего вспоминается «генеалогическое древо» («Авраам родил Исаака; Исаак родил Иакова; Иаков родил Иуду и братьев его...»). Заметим, что, в отличие от реальности, родителем в таком дереве выбирается только отец.
Другое часто употребляемое применение корневых деревьев — это оглавление книги. Книга состоит из глав, главы состоят из пунктов, пункты состоят из подпунктов. При желании или необходимости детализацию можно и продолжить, например, так: подпункты состоят из абзацев, абзацы состоят из предложений, предложения состоят из слов, слова состоят из букв.
Обобщая такой подход, можно заметить, что дерево — удобное средство представления сложной системы, с помощью которого последовательно детализируются иерархические составляющие (компоненты) такой системы.
Практические примеры деревьев
В этом разделе приведены примеры отображения деревьями сложных систем. Эти примеры важны для понимания условий и решений задач, разбираемых в следующих пунктах.
Деревья отношений
Организация (партия), построенная по принципу единоначалия: во главе организации стоит один человек, ему непосредственно подчиняются несколько руководителей. Каждому из них подчиняются руководители более низкого уровня, и т. д. Руководителю самого низкого уровня подчиняются несколько рядовых членов организации.
Ученые, имеющие совместные публикации с Эрдёшем, получают «номер Эрдёша», равный 1. Ученые, имеющие совместные публикации с учеными, имеющие номер Эрдёша 1, получают номер Эрдёша 2, и так далее. Ученые, не имеющие публикаций ни с кем из тех, кто имеет установленный номер Эрдёша, получают признак infinity.
Деревья попиксельного представления плоских цветных образов
Деревья с четырьмя или менее дочерними вершинами применяются для свертки информации о графическом образе, представленном квадратом из Nx N пикселов (где N — степень двойки). Цвет каждого пиксела кодируется собственным числом (в например, от 0 до 255) примерно так: если весь квадрат имеет один тот же цвет, то для кодирования информации используется единственное число (дерево
с одной вершиной). Иначе квадрат разбивается на 4 равных квадратных части. Если какая-то из частей имеет единый цвет для всех пикселов в ней, то она кодируется этим числом. Для всех частей, не имеющих единого цвета для составляющих ее пикселов, процедура деления квадрата на четыре меньших квадрата продолжается. Например, для такого образа (4x4 пиксела)
1212121212121001001212121212121001001361361361361212121213613613613612121212
12
12
12
12
12
12
100
100
12
12
12
12
12
12
100
100
136
136
136
136
12
12
12
12
136
136
136
136
12
12
12
12
получается дерево, показанное на рис. 3.5:
Рис. 3.5. Дерево представления графического образа рисункаРис. 3.5. Дерево представления графического образа рисунка
Рис. 3.5. Дерево представления графического образа рисунка
Рис. 3.5. Дерево представления графического образа рисунка
Деревья представления сложных композиций трехмерных объектов
BSP (Binary Space Partition) Trees — деревья двоичного разбиения пространства — используются для хранения информации о расположении объектов на трехмерной сцене относительно обозревателя. Это полезно в процессе прорисовки объектов с тем, чтобы сначала нарисовать объекты, наиболее удаленные от наблюдателя, затем более близкие и т. д.
Алгоритм построения BSP-дерева может быть описан следующим образом. Между наблюдателем и сценой проводится секущая плоскость (не пересекающая ни одного из объектов). Тогда часть объектов окажется между наблюдателем и секущей плоскостью, а часть — между секущей плоскостью и сценой. Процесс проведения таких секущих плоскостей может быть продолжен. После проведения каждой из них мы имеем дерево объектов, в котором иерархия внутренних вершин обозначает последовательность проведения секущих плоскостей, сами внутренние вершины хранят информацию о секущих плоскостях, а листовые вершины включают совокупности отображаемых объектов в одной (ограниченной проведенными секущими плоскостями) области пространства.
Деревья кодирования символов
Хаффман предложил сжатие последовательности символов за счет кодирования символов переменным количеством битов: те символы, которые встречаются в этой
последовательности символов чаще, кодируются меньшим количеством битов, а символы, встречающиеся в этой последовательности реже, кодируются большим количеством битов.
Для кодирования символов строится дерево, листьями которого являются пары «символ + частота встречаемости», а иерархия в дереве выстраивается так, чтобы:
каждая внутренняя вершина имела РОВНО двух потомков;
口 в первую очередь объединялись потомки, имеющие наименьшие частоты встречаемости;
при объединении двух потомков их родитель (предок) получал частоту встречаемости, равную сумме частот встречаемости потомков.
Например, пусть в символьной строке из 100 символов встретились только символы 〃,Ь, с, d, е,/со следующими частотами:
Символ
Частота
А
45 раз
В
13
С
12
D
16
Е
9
F
5
Тогда соответствующее дерево кодирования будет иметь вид, показанный на рис. 3.6.
Заметим, что каждая дуга дерева нагружена цифрой 0 (дуга к левому потомку) или 1 (дуга к правому потомку). Таблица кодировки получается при анализе пути по дугам дерева от корня к листу соответствующего символа:
Символ
Код
А В С D Е F
0
101 .
100
111
1101
1100
Оценим теперь степень сжатия. Исходный текст, как следует из таблицы встречаемости, состоял из 100 символов (〃 一 45, 6 — 13, с — 12, d — 16, е — 9,/ — 5). Если закодировать символы способом, указанным выше (на а — 1 бит, на Ь, с, d — по 3 бита, на е и / — по 4 бита), то для представления исходной строки из 100 символов потребуется 224 бита: 4 5 xl + 13x3+12x3+16x3 + 9x4 + 5x4* 224. При обычном (равномерном) кодировании требовалось бы минимум три бита на каждый символ, то есть всего нужно 300 битов (100 х 3). Коэффициент сжатия 300/224 = 1,34.
юо/ Xа:45 (b,c,d,e,f:55)(b,c:25)(e,d,f:30)юо/ Xа:45 (b,c,d,e,f:55)(b,c:25)(e,d,f:30)/Xс:12Ь:13/Xс:12Ь:13(f©14)d:16(f©14)d:16/ X/ X£5е:9£5е:9
юо/ Xа:45 (b,c,d,e,f:55)
(b,c:25)(e,d,f:30)
юо/ Xа:45 (b,c,d,e,f:55)
(b,c:25)(e,d,f:30)
/Xс:12Ь:13
/Xс:12Ь:13
(f©14)d:16
(f©14)d:16
/ X
/ X
£5е:9
£5е:9
Рис. 3.6. Дерево кодирования символов исходной строки
Деревья сортировки
Для примера рассмотрим дерево:
Рис. 3.7. Пример дерева сортировкиРис. 3.7. Пример дерева сортировки
Рис. 3.7. Пример дерева сортировки
Рис. 3.7. Пример дерева сортировки
Такое дерево является деревом сортировки, поскольку для любой вершины этого дерева выполняется следующее свойство: в левом поддереве данной вершины находятся все числа, меньшие числа в самой вершине (и только они). А в правом поддереве данной вершины находятся все числа, большие числа самой вершины (и только они). Заметим также, что каждая вершина такого дерева имеет не более двух потомков.
Для поиска наименьшего элемента в этом дереве потребуется анализ всего трех вершин (10, 5, 1). Для поиска наибольшего элемента — анализ четырех вершин (10, 20, 25, 27). В то же время для поиска минимального (максимального) числа в одномерном массиве из этих же девяти чисел обычным способом потребовался бы анализ всех девяти элементов.
Аналогичным образом сокращается трудоемкость ответа на вопрос «имеется ли в данном дереве заданное число?». В дерево сортировки относительно несложно добавляется новый элемент с сохранением основного свойства дерева. И, наконец, обходом «левое_поддерево т корень т правое_поддерево» выводится последовательность всех чисел в порядке возрастания.
Деревья сумм
Один пример деревьев сумм представлен выше при описании дерева составленного для оптимального кодирования символов по методу Хаффмана, Другой пример, «битово-индексированное дерево» {Bit-Indexed Tree), ввиду своей сложности объясняется далее — непосредственно при описании решения задачи «Мобильные телефоны».
3-1-7- Перечисление деревьев
Особый интерес представляет тема «Перечисление деревьев». Алгоритмы подобного вида обычно отвечают на вопросы: «сколько существует деревьев заданного типа?», «каково следующее дерево заданного типа?». Ответы на такие вопросы интересны потому, что можно сопоставить деревья реальным объектам, и тогда мы получим ответы на вопросы «сколько сущестует реальных объектов?» и <какой следующий по порядку реальный объект?». Например, в одной из задач рассматриваются деревья, в которых каждая из вершин имеет РОВНО ДВУХ потомков или не имеет НИ ОДНОГО. Для нумерации используется следующая система:.
Каждой вершине назначается число — количество листьев в поддереве, которое имеет эту вершину в качестве корневой.
Дерево кодируется последовательностью чисел, ассоциированных с корневой вершиной и всеми левыми потомками.
Рис. 3.8. Пример бинарного дерева
Так, дерево, изображенное на рис. 3.8, кодируется последовательностью чисел 7, 4,1,2,1,1,1. Символом «*» отмечены числа, вошедшие в кодовую числовую последовательность. Деревья упорядочены по лексико-мафическому возрастанию представляющих их числовых последовательностей. Требуется по кодовой последовательности текущего дерева узнать кодовую последовательность следующего дерева.
В другой задаче рассматриваются все деревья, у каждой из вершин которых может быть НИ ОДНОГО, ОДИН или ДВА потомка. Они упорядочены в порядке возрастания вершин в дереве, а в случае равенства вершин в двух деревьях — в порядке возрастания количества вершин в левом поддереве, в случае равенства у двух
деревьев и вершин в левом поддереве — в порядке возрастания количества вершин в правом поддереве. По заданному номеру дерева требуется вывести в специальном виде само дерево.
Представление деревьев в памяти компьютера
К-ичные деревья (то есть деревья, про которые заведомо известно, что они имеют не более К вершин) удобно хранить в одномерном массиве, используя индексы этого массива для выстраивания иерархии дерева. Каждой вершине дерева соответствует ОДИН элемент массива.
Индекс корневой вершины равен 1.
Индексы вершин — потомков корневой — равны 2,3,...»К + 1.
Индексы вершин — потомков вершины 1 — равны К + 2, К + 3,…,2К + 1.
Индексы вершин — потомков вершины 2 — равны 2К + 2, 2К + 3,...»ЗК + 1 и т. д. В общем случае, если вершина расположена в этом массиве на позиции :, то индексы ее вершин-потомков таковы: (i - 1)*К + 2,(/ - 1)*К + 3, ...,(i - 1)*К + 1 + К. Легко также найти и предка любой вершины. Пусть вершина расположена в массиве на позиции i (или, говоря другими словами, вершина имеет индекс: в массиве). Тогда индекс (расположение в массиве) вершины, которая является предком вершины i, есть l + (i div К), то есть, увеличенное на 1 частное от целочисленного деления i на К. Например, пусть К 工 4. То есть в дереве каждая вершина может иметь 0, 1, 2, 3 или 4 наследника. Нам нужно узнать позицию предка вершины, находящейся в массиве на позиции 23. Вычисляем: 1 + (23 div 4) = 6. Значит, предком вершины, которая находится в массиве на позиции 23, является вершина на позиции 6. Действительно, выпишем для проверки последовательно потомков всех вершин до 6-й включительно:
1
2
3
4
5
2
6
7
8
9
3
10
И
12
13
4
14
15
16
17
5
18
19
20
21
6
22
23
24
25
Для того чтобы отметить отстутствие потомков у какой-то из вершин, соответствующему элементу массива присваивается специальное значение (0, -1, пустая строка и т. д.).
Основное достоинство такого хранения деревьев — упрощение алгоритмов обработки деревьев. Основной недостаток — неэффективное использование памяти для несбалансированных деревьев, то есть, деревьев, для которых количества потомков от предка к предку сильно различаются. Если по ограничениям задачи массив может быть размещен в оперативной памяти, то такое представление дерева вполне приемлемо.
Бывают задачи, в которых такое размещение невозможно. Тогда для хранения деревьев могут быть использованы другие варианты. Например, те, которые использовались ранее для хранения графов (в частности списки дуг из вершин). Приведем для примера один вариант хранения двоичного дерева (дерева, в котором у каждой вершины-предка имеется не более двух потомков), использующий три массива: A[i] служит для хранения содержимого вершин (количество элементов в нем должно быть не меньше максимального числа вершин дерева для данной задачи). Еще нужны два специальных массива {Left и Right) такой же размерности для хранения ссылок на левого и правого наследника: Left[i] указывает индекс в массиве Л элемента, который является левым потомком элемента i, Right[i]— указывает индекс в массиве А элемента, который является правым потомком элемента :.Один из элементов массива А объявляется корнем дерева (Root).
Порядок обхода деревьев
Во многих задачах требуется обойти все вершины дерева. В литературе наиболее распространены три порядка обхода деревьев — нисходящий (pre-order), фланговый (in-order) и восходящий (post-order), в зависимости от того, в какой момент мы посещаем вершину-предка относительно посещения ее потомков:
прямой, или нисходящий (pre-order traversal), — такой порядок обхода, при котором вначале посещается (анализируется) вершина, потом ее левое поддерево, затем ее правое поддерево;
фланговый (in-order traversal) — это порядок обхода, при котором вначале посещается (анализируется) левое поддерево, потом сама вершина, а затем ее правое поддерево;
восходящий (post-order traversal) — это такой порядок обхода, при котором вначале посещается (анализируется) левое поддерево, потом — правое поддерево, и только в последнюю очередь сама вершина.
Организация материала и технология работы с ним
Далее приводятся условия и описания решений 15 задач. Задачи разбиты на группы по темам и, по мнению автора, представлены в порядке возрастания сложности. Последний раздел включает задачи для самостоятельного решения.
Задачи на основные свойства деревьев
В данном разделе приведены условия и решения задач, не требующих никаких специальных знаний и навыков, кроме понимания определения деревьев и способов их хранения.
Задача «Is it a tree?»
NCNA, 1997
Is It a Tree? (B.IN / B.OUT / 10 c)
Дерево — это широко известная структура данных, которая или пуста, или является множеством из одной или нескольких вершин, соединенных ориентированными ребрами (дугами), удовлетворяющим следующим свойствам: 1) существует ровно одна вершина, называемая корневой, в которую не приходят дуги; 2) в каждую из вершин, кроме корневой, приходит ровно одна дуга; 3) существует ровно одна последовательность дуг, приводящая из корня в каждую вершину.
В данной задаче вам будет дано несколько описаний наборов вершин, соединенных дугами. Для каждого из них вы должны определить, удовлетворяет ли данный набор определению дерева или нет.
Ввод:
Ввод состоит из последовательности описаний (тестов) за которой следует пара отрицательных чисел. Каждый тест состоит из последовательности описаний дуг, за которой следует пара нулей. Описание каждой дуги состоит из пары целых чисел. Первое число определяет вершину, из которой дуга выходит, второе число определяет вершину, в которую дуга входит. Номера вершин всегда больше нуля. Вывод'.
Для каждого теста выведите одну из двух фраз: «Case A is a tree.» или «Case A is not a tree.», где А соответствует номеру теста. Тесты нумеруются последовательно, начиная с номера 1.
Пример ввода-.
6 8
5 3
5 2
6 4
5 6
0 0
8 1
7 3
6 2
8 9
7 5
7 4
7 8
7 6
0 0
3 8
6 8
6 4
5 3
5 6
5 2
0 0
-1
-1
Пример вывода:
Case 1 is a tree.
Case 2 is a tree.
Case 3 is not a tree.
Итак, ориентированный граф задается наборами ребер. Требуется выяснить, является ли этот граф деревом. При этом пустой граф (не имеющий вообще ни вершин, ни дуг) считается деревом по определению. Известно, что для того, чтобы
непустой граф был деревом, необходимо и достаточно, чтобы выполнялись два свойства:
количество дуг должно быть ровно на 1 меньше количества вершин;
граф должен быть связным.
С учетом того, что по условиям задачи во входном наборе данных может быть несколько тестовых наборов, разделяемых парой 0 0, главная программа может выглядеть так: begin
assigndnput. 'b.in'); reset (input):
assignCoutput.'b.out'): rewriteCoutput):
i:-0; x:-l: while x<>0 do begin inc(i): InputData: if (Nodes<>0) and ((Edges<>Nodes-l) or (not Connected)) then writeln ('Case ' ,i.' is not a tree.') else writeln ('Case ' ,i.' is a tree.') end; end.
Здесь в процедуре InputData вводятся данные об очередном графе. Там же одновременно вычисляются величины: Nodes — количество вершин в графе, и Edges — количество дуг в графе.
Если граф не пустой (Nodes<>0) и
(несоответствие количеств вершин и ребер (Edges<>Nodes-l) или граф несвязный (это определяется функцией Connected)) ТО данный граф не является деревом ИНАЧЕ данный граф является деревом
Рассмотрим подробнее процедуру ввода 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:=1 to MaxN do V[i]:-0;
read(x.y). Nodes:=0; Edges:-0:
if X--1 then begin close(input); close(output):
halt(O):
end:
while (х<>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 = Shoftlnt), и константы MaxNn GREAT, изменяя которые, можно увеличивать размерность обрабатываемых данной программой графов. Основной цикл ввода очередного теста выглядит так:
while (х<>0) do
begin
g[x.y]:-l: v[x]:-l; v[y]:-l;
Max(MaxNode.x.y);
inc(Edges);
read(x.y)
end:
Здесь:
口 (x, у) — очередная дуга в графе из вершины % в вершину у;
g[x, у] = 1, если есть дуга из % в у, GREAT— в противном случае;
v[x] = 1, если вершина х присутствует в графе, 0 — в противном случае;
Edges — количество обработанных дуг;
MaxNode — встреченный максимальный номер вершины;
Мах — процедура, выбирающая максимальное значение из трех величин Мах- №d% х, у.
Условиями задачи не запрещаются такие входные данные, в которых для обозначения вершин используется не последовательная их нумерация. Поэтому после ввода данных необходимо подсчитать Nodes — реальное число вершин в графе — как разность между максимальным встреченным номером MaxNode и количеством пропусков (Holes)
Holes:-0:
for i:-l to MaxNode do
if v[i]-0 then inc(Holes):
Nodes:-MaxNode-Holes:
Далее необходимо обрабатывать конец ввода исходных данных: (-1-1) if Xе-1 then begin
close(input): close(output); halt(O):
end.
И, наконец, необходимо в начале процедуры инициализировать граф G отсутствием всех дуг, а список вершин V— отсутствием всех вершин:
for i:-1 to MaxN do
for j:-l to MaxN do G[i,j]:-GREAT:
for i:=1 to MaxN do V[i]:-0:
Теперь рассмотрим функцию Connected, которая определяет, является ли исходный граф связным.
function Connected : boolean; var
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 dp
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:
Здесь вначале в граф добавляются «пропущенные вершины» и их дуги ко всем вершинам графа:
for i:-1 to MaxNode do
if v[i]»0
then
for j:=l to MaxNode do
begin
g[i .J]:T: 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]:=niin(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.
Если имеется хотя бы одна такая пара (f, j) для которой g[i, j] = GREAT, то есть вершина j не достижима из вершины i, то граф не связный. Полный текст решения задачи приведен в конце главы.
Задача «Strategic game»
Southeastern European Regional Contest, 2000
Strategic game (input.txt / output.txt / 10 с)
Боб любит играть в компьютерные игры, особенно стратегические. Но иногда он не находит решение достаточно быстро — и тогда сильно огорчается. Сейчас перед ним стоит следующая задача. Он должен защитить средневековый город, дороги которого образуют дерево. Бобу надо расставить минимальное количество стражников в вершины так, чтобы они могли обозревать все дуги. Можете ли вы ему помочь?
Ваша программа должна найти минимальное количество стражников, которое нужно расставить в вершины данного дерева.
Входной файл содержит несколько наборов тестов в текстовом формате. Каждый тест представляет дерево следующим образом: 1) количество вершин; 2) описание каждой вершины в следующем формате: номер_вершины:(ко- личество^дорог) номер ^вершины 1 номер^ершины2 ... номер вершины _ко~ личество_дорог или номер_вершины:(0). Номера вершин — целые числа от 0
до w - 1 для п вершин (0 < л < 1500). Каждая дуга появляется во входном тесте ровно один раз.
Например, для дерева, показанного на рис. 3.9, ответ — один стражник (в вершине 1). Вывод для каждого теста должен содержать одно целое число (каждое в отдельной строке) — минимальное количество стражников для этого теста.
0I1/\23
Рис. 3.9. Пример дерева для задачи «Strategic game»
Пример вводаПример вводаПример вывода
Пример ввода
Пример ввода
\/ lj_r \/ VIZ \/ 1 2 0 0 3 ((/\ /\ /\ 4 ^ h ^ ^ 5 ^\/ lj_r \/ VIZ \/ 1 2 0 0 3 ((/\ /\ /\ 4 ^ h ^ ^ 5 ^223322⑴⑼⑼⑼ h ^ ^ ^⑴⑼⑼⑼ h ^ ^ ^1 о1 о4422
\/ lj_r \/ VIZ \/ 1 2 0 0 3 ((/\ /\ /\ 4 ^ h ^ ^ 5 ^
\/ lj_r \/ VIZ \/ 1 2 0 0 3 ((/\ /\ /\ 4 ^ h ^ ^ 5 ^
2
2
3
3
2
2
⑴⑼⑼⑼ h ^ ^ ^
⑴⑼⑼⑼ h ^ ^ ^
1 о
1 о
4
4
2
2
Алгоритм решения таков:
Подсчитываем степени вершин.
Пока максимальная степень не равна О
Удаляем вершину с максимальной степенью и ее ребра
(попутно уменьшая степени вершин, ребра к которым удалены)
Инкрементируем количество удаленных вершин
Напомним, что степенью вершины называется количество связанных с ней ребер. Заметим, что в случае нескольких вершин с одинаковой максимальной степенью нужно выбирать вершину, максимально далекую от корня дерева. На рис. 3.10 и 3.11 показаны примеры, иллюстрирующие это положение.
С учетом возможности нескольких тестов во входном файле тело главной программы может быть записано на Паскале так:
begin
assigndnput.'input.txt'): reset(input): assign(output.'output.txt'): rewriteCoutput); while not EOF(input) do
begin
InputData;
Del eted:-0:
while MaxPower(Node)<>0 do
begin
inc(Deleted):
Annigilate(Node):
end:
writeln(Deleted):
end;
close(input): close(output);
end.
Рис. 3.10. Поиск максимума от корня к листьям (количество удаленных вершин — 6)
Рис. 3.11. Поиск максимума от листьев к корню (количество удаленных вершин — 6)
Здесь:
InputData — процедура, обеспечивающая ввод дерева;
MaxPower — функция, находящая максимальную степень MaxPower и соответствующую вершину Node;
Armigilate — процедура, удаляющая из дерева вершину Node со всеми ее ребрами;
Deleted — количество удаленных вершин.
Рассмотрим подробнее процедуру Annigilate:
procedure Annigilate(Node:integer):
begin
j:-l:
for 1:-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:
Исходное дерево представлено в данной задаче следующим образом:
Powei\i] — степень вершины i
G[itj] — номер вершины, в которую есть ребро из вершины i (для всех j от 1 до Power[i])
Процедура Annigilate сначала уменьшает на 1 степени всех вершин, связанных ребрами с текущей, а затем обращает в 0 степень самой текущей вершины.
Процедура InputData выглядит несколько громоздко. Это связано с неудобным для ввода форматом исходных данных, когда приходится «выковыривать» нужные данные из текстовой строки. Кроме того, в качестве разделителей данных в оригинальных тестовых примерах использовались как пробелы, так и символы табуляции. Полный текст решения приведен в конце главы.
Задача «Оппозиция»
XIII Всероссийская олимпиада школьников по информатике, 2001
Оппозиция (org.in / org.out /5 с)
В некоторой стране полиция выявила разветвленную сеть оппозиционной партии. Партия сильно законспирирована и состоит из рядовых членов и руководителей различных уровней. Во главе партии стоит один главный руководитель —лидер партии. До начала арестов приказ лидера может быть доведен до любого члена партии. Все члены партии пронумерованы от 1 до ^ Каждый член партии знает только своего непосредственного руководителя (ровно одного) и своих непосредственных подчиненных (руководитель не знает подчиненных своего подчиненного и наоборот). Естественно, что с началом арестов членов партии она распадется на мелкие, не связанные друг с другом группы. Например, с арестом члена партии № 2 партия разваливается на 4 группы. Полицмейстер уверяет, что группа, состоящая из менее чем К членов партии, идеологически вырождается и не представляет угрозы для государства.
Стремясь не уронить престиж страны в глазах мирового общественного мнения, полицмейстер поставил задачу произвести минимальное количество арестов членов партии так, чтобы от нее остались только Идеологически вырождающиеся маленькие группы.
Требуется написать программу, которая бы по входным данным, описывающим структуру подпольной партии, выводила количество арестов и номера членов партии, которых нужно арестовать.
Ввод:
Входной файл с именем org.in содержит три строки. В первой записано число К(1 V К V 10 000), во второй строке — число N (1 <N< 10 000), определяющее коли-чество членов партии. Третья строка содержит набор из N 一 1 числа. В этой строке для каждого члена партии, кроме лидера, задается номер его непосредственного руководителя. Номер руководителя всегда меньше, чем номер подчиненного. При этом первое число задает номер руководителя второго члена партии, второе — третьего и так далее. Числа в строке разделяются одним пробелом.
Вывод'.
Выходной файл с именем org.out состоит из двух строк. В первую строку необходимо поместить количество арестов, а во вторую — номера членов партии, подлежащих аресту. Эти номера разделяются одним пробелом. При наличии нескольких решений выведите одно из них.
9 10 1113 12 14
Рис. 3.12. Пример дерева для входного файла
Пример ввода
Пример вывода
3
14
1122323666747
4
6278
Математическая постановка задачи выглядит следующем образом: в заданном дереве удалить минимальное количество вершин (вместе с инциндентными ребрами) так, чтобы в получившемся графе не оказалось связных компонент с числом вершин К или более. Алгоритм решения (похожий на алгоритм решения предыдущей задачи) таков:
Подсчитываем степени вершин.
Пока максимальная степень >- К-1
Удаляем вершину с максимальной степенью и ее ребра
(попутно уменьшая степени вершин, ребра к которым удалены)
Инкрементируем количество удаленных вершин
Запоминаем номера удаленных вершин
Также по сравнению с предыдущей задачей существенно упрощен ввод дерева, а в выводе надо вывести не только количество удаленных вершин, но и их номера. Полный текст решения задачи приведен в конце главы.
Задача «Erdos Numbers»
NWERC, 2000
Erdos Numbers (input.txt / output.txt / 10 с)
Венгерский математик Пал Эрдёш (1913-1996) был не только одним из самых сильных математиков XX века, но и одним из самых известных. Он часто публиковал свои работы в соавторстве с другими учеными, и каждый математик считал за честь стать соавтором Эрдёша.'
Однако мало кому выпадала честь стать соавтором работ Эрдёша, поэтому многие были рады, если им удавалось опубликовать совместную работу с кем-то, кто был соавтором Эрдёша.
Так появилось понятие «номер Эрдёша». Автор, который имел совместные с Эрдёшем публикации, получал номер Эрдёша 1. Автор, который не имел совместных публикаций с Эрдёшем, но имел совместную публикацию с кем- то, кто имел номер Эрдёша 1, получал номер Эрдёша 2, и т. д. Сегодня почти каждый хочет знать, каков его номер Эрдёша.
Ваша задача — написать программу, которая вычислит номера Эрдёша для заданного множества ученых.
Ввод:
Первая строка ввода содержит количество сценариев. Ввод для каждого сценария состоит из списка описаний статей и списка имен. Он начинается со строки
Р N
(Р и N — натуральные числа). Далее следует Р строк, содержащих описания статей. Каждая статья описывается в одной строке следующим образом:
Smith. M.N.. Martin. G.. Erdos. P.: Newtonian forms of prime factors matrices
Вы можете предполагать, что авторами одной статьи будут не более 20 авторов, а полное описание статьи всегда короче, чем 2 000 символов.
За списком статей следуют Nстрок списка имен авторов, по одному в строке, для которых вы должны вычислить номера Эрдёша.
Вывод:
Для каждого сценария вы должны вывести строку, содержащую текст «Scenario i» (где i — номер сценария) и имена авторов вместе с их номерами Эрдёша для всех авторов из заданного списка имен. Авторы должны выводиться в том же порядке, в котором они появились в списке имен во вводе. Номера Эрдёша должны вычисляться по заданному списку статей этого сценария. Автор, для которого невозможно вычислить номер Эрдёша (то есть не имевший отношения к работам Эрдёша через заданный список статей), получает номер Эрдёша «infinity».
Пример ввода:
2
1 1
Hahn. Р.М. Му hat is the best
Hahn. P.M.
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.: Selfstabilizing data structures Smith. M.N. Hsueh. Z. Chen. X.
Пример вывода:
Scenario 1 Hahn. P.M. infinity Scenario 2 Smith. M.N. 1 Hsueh. Z. infinity Chen. X. 2
Задача решается поиском в ширину от СПЕЦВЕРШИНЫ. Заметим, что основная сложность в этой задаче — организация ввода исходных данных. С учетом того, что по условиям задачи исходные данные могут содержать указанное число тестов, тело главной программы может выглядеть так: begin
Authors[l]:-'Erdos. Р.'; assign(input.'1nput.txt'): reset(input); 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.
To есть циклом по числу тестов:
вводим все статьи (процедура GetPaper), по ходу ввода статей пополняем гра力 отношений соавторства;
для всех вершин графа строим номера Эрдёша (уровни вершин относительно вершины Эрдёша), это делается в процедуре BuildErdosNumbers поиском в ширину от вершины Эрдёша (1);
вводим авторов и выводим ответы для каждого из них.
В программе использованы следующие структуры данных:
口 G — граф отношений соавторства;
Power — степени вершин;
Authors — список фамилий авторов;
С4 — номера авторов, написавших текущую статью;
Erdos — номера Эрдеша для всех авторов.
Полный текст решения задачи приведен в конце главы.
Задача «Closest Common Ancestor»
Southeastern European Regional Contest, 2000
Closest Common Ancestors (input.txt / output.txt / 10 с)
Напишите программу, которая возьмет на входе корневое дерево и список пар вершин. Для каждой пары (“, и) программа должна определить ближайшего общего предка вершин 〃 и и в этом дереве.
Ближайший общий предок вершин unv — это такая вершина wt которая является предком для обоих вершин 〃 и и и имеет наибольшую глубину в дереве. Вершина может считаться своим предком. Например, для дерева, показанного на рис. 3.13, вершина 2 является ближайшим общим предком вершин 2 и 5. Набор данных, который нужно прочитать из текстового файла, начинается с описания дерева, заданного в виде:
количество_вершин
вершина:(количество_потомков) потомок1 потомок? ... потомокК
5/1\142
\
3
Рис. 3.13. Пример дерева для задачи «Closest Common Ancestor»
Вершины представляются целыми числами от 1 до N. За описанием дерева следует описание пар вершин в виде
количество_пар
(и V) .. (х у)
Входной файл содержит множество тестов (не менее одного). Заметим, что разделители (знаки табуляции, пробелы и переводы строк) могут встречаться в любом месте входного файла. Для каждого ближайшего общего предка в отдельной строке (в порядке возрастания ближайших общих предков) программа должна вывести его и количество пар, для которых он был ближайшим общим предком в виде:
предок:количество_раз
Пример для графа, показанного на рис. 3.13:
Пример ввода
Пример вывода
5
2:1
5:(3) 1 4 2
5:5
1:(0)
4:(0)
2:(1) 3
з:(о)
6
(1 5) (1 4) (4 2) (2 3) (1 3) (4 3)
Математическая постановка задачи такова: в заданном дереве найти ближайшего общего предка для каждой пары вершин, указанных на входе.
Решение заключается в построении всех предков для каждой из вершин и нахождении ближайшего общего предка (по определению дерева, предок всегда есть, в частности одна из двух вершин пары может быть общим предком). Кроме того, на входе задается множество запросов о предках и требуется интегральный ответ: сколько раз каждая вершина была найдена в качестве ближайшего общего предка. Выводить предлагается только вершины, которые хоть раз побывали ближайшим общим предком. С учетом вышеизложенного и множественности тестов во входном файле тело главной программы может выглядеть следующим образом: 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.
Здесь:
InputTree — процедура ввода дерева. Заметим, что при вводе строится инверсный граф (то есть для каждой вершины указывается ее единственная предшественница);
InputProcessVerticePairs — процедура, которая вводит очередную пару вершин, находит для этой пары ближайшего общего предка, вызывая процедуру FindCCA,
инкрементирует в массиве Л количество раз, когда соответствующая вершина была ближайшим общим предком;
OutputResults — процедура форматированного вывода номеров вершин и количеств раз, когда она была ближайшим общим предком.
Заметим, что ввод исходных данных выполняется аналогично тому, как это делалось в задаче из пункта 2 данной главы. Рассмотрим подробнее процедуру FindCCA
procedure FindCCACx.y:integer);
var
i : integer:
begin
for i:-l 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]-1) and (Ay[g[y.l]]<>l) do
begin y:-g[y.1]: Ay[y]--1; end:
while (Ax[i]<>l) or (Ay[i]<>l) do inc(i);
inc(A[i]);
end;
Здесь:
Ax — массив предков вершины x;
Ax[i] = 1, если вершина i является предком вершины х;
口 Ау — массив предков вершины у;
Ay[i] = 1, если вершина i является предком вершины у;
Л — массив для хранения найденных ближайших общих предков (вершина i была ближайшим общим предком A[i] раз).
В Ах[х] н Ах[у] вписываются единицы по мере последовательного продвижения вверх по дереву. A[i] инкрементируется, если Ar[i] и Ay[i] одновременно равны 1. Полный текст решения задачи приведен в конце главы.
Задачи на представление образов
В данном разделе приведены условия и решения двух задач на представление образов. Первая задача, «Unscrambling Images», посвящена свертке попиксельных изображений, а вторая, «BSP Trees», связана с представлением деревьями сложных трехмерных сцен.
Задача «Unscrambling Images»
Pacific Northwest Programming Contest, 1999
Unscrambling Images (D.txt / вывод на экран /5 с)
Деревья квадрантов широко используются для компактного кодирования цифровых образов.
Пусть мы имеем изображение Nx N пикселов (где N — степень числа 2, 1 <N< 16). Тогда оно кодируется следующим образом: Начинаем с дерева квадрантов, имеющего ровно 1 узел — корень, и ассоциируем эту вершину со всем образом целиком — регион N х N пикселов. Затем рекурсивно выполняем следующие действия: 1) если цвет каждого пиксела в регионе, ассоциированном с текущей вершиной, имеет значение р, то вершина становится листовой и с ней ассоциируется значение р; 2) иначе к этой вершине добавляются четыре вершины в качестве потомков. Ее регион разбивается на четыре одинаковые квадратные части (квадранты), и каждый квадрант становится регионом соответствующей вершины-потомка. Алгоритм рекурсивно применяется к каждому потомку.
Когда процесс завершается, мы получаем дерево квадрантов, в котором каждая внутренняя вершина имеет четыре потомка. С каждым листом ассоциирована величина, представляющая цвет всех пикселов соответствующей части изображения.
В качестве исходного изображения (размером 8 х 8) может быть, например, такое:
12
12
136
136
136
136
0
0
12
12
136
136
136
136
0
0
12
12
12
12
0
0
0
0
100
100
12
12
0
0
0
0
100
100
100
100
48
48
48
48
100
100
100
100
48
48
48
48
100
100
100
100
100
100
48
48
100
100
100
100
100
100
48
48
Предположим, что потомки (слева направо) представляют соответственно квадранты: верхний левый, верхний правый, нижний левый, нижний правый. Чтобы было легче идентифицировать вершину в дереве квадрантов, мы назначим каждой вершине номер в соответствии со следующими правилами: 1) корневая вершина получает номер 0;
если номер вершины А, то ее потомки (слева направо), получают номера 4A + 1, 4А + 2, 4A + 3, 4А + 4.
Образы, закодированные деревьями квадрантов, могут быть зашифрованы с помощью паролей так: после того как определены подрегионы, потомки переупорядочиваются. Переупорядочение может различаться для каждой вершины, но оно полностью определяется паролем и номером вершины.
К несчастью, некоторые люди используют один и тот же пароль для кодирования разных образов.
В этом случае, увидев результат кодирования специально подобранного тестового образа, можно легко раскодировать любой образ, закодированный тем же паролем. В этом тестовом образе каждый пиксел имеет различное значение цвета — от 0 до №- 1, размещенные слева направо, сверху вниз в порядке возрастания значения цвета.
Вам дан результат кодирования такого тестового образа.
Напишите программу, которая, используя его, сможет раскодировать любой образ, закодированный тем же паролем.
Ввод:
Во вводе задается множество тестов. Первая строка содержит их количество. Каждый тест начинается со строки, содержащей число N、за которой следует описание дерева квадрантов — результат кодирования тестового образа, а затем еще одно дерево квадрантов — оно задает образ, который требуется раскодировать.
Описание каждого дерева квадрантов начинается с положительного целого числа М, обозначающего количество листьев в дереве. Следующие т строк имеют вид к с
Это означает, что вершина А является листовой, а ее цвет — с.
Вершины не встречаются на вводе, если они являются внутренними или их нет в дереве вообще. Вы можете предполагать, что все значения цветов находятся в диапазоне от 0 до 255 включительно и что каждое четверичное дерево — корректный результат кодирования по алгоритму, описанному выше.
Вывод'.
Для каждого теста выведите его номер, пустую строку и цвета пикселов в декодированном образе по одной строке пикселов в строке текста. Выводите цвета в четырех колонках с выравниванием по правой стороне. Добавьте пустую строку между тестами.
Пример ввода
Продолжение ввода
Пример вывода
3
Case 1
2
4
4
16
253 40
1 3
58
123 23
22
69
30
7 13
Case 2
4 1
8 12
4
90
10 10 20 20
1 23
104
10 10 20 20
2 123
11 1
41 42 30 30
3 253
12 5
43 44 30 30
4 40
132
Пример ввода
Продолжение вводаПример вывода
4
1 6 58 69
7 13
8 12 90 104 11 1
12 5 13 2
143 157
16 6 17 10
18 11 19 15 20 14
7
2 10
3 20
4 30
5 41
6 42
7 44
8 43
14 3Case 3
157
16 6255 255 30 30
17 10255 255 28 28
18 1140 40 140 140
19 1541 42 140 140
20 14
10
2 255
4 140
40
40
42
41
13 30
14 30
15 28
16 28
Сокращенно постановка задачи заключается в следующем: по заданному зашифрованному изображению и ключу к расшифровке построить расшифрованное изображение. В табл. 3.1. указаны подрегионы, построенные по приведенной выше таблице данных.
Таблица 3.1. Подрегионы пикселов
12
12
136
136
136
136
0
0
12
12
136
136
136
136
0
0
12
12
12
12
0
0
0
0
100
100
12
12
0
0
0
0
100
100
100
100
48
48
48
48
100
100
100
100
48
48
48
48
100
100
100
100
100
100
48
48
100
100
100
100
100
100
48
48
Дерево, представляющее такой образ, будет иметь вид, показанный на рис. 3.14.
12 12 100 100
Рис. 3.14. Дерево образа 16 х 16 пикселов
Это дерево квадрантов, каждая вершина которого имеет не более четырех потомков. Для хранения полного такого дерева в памяти достаточно иметь одномерный массив, имеющий 340 элементов. Элементы 1-4 хранят дерево для 九=2 (рис. 3.15).
1234
Рис. 3.15. Дерево для п = 2
Элементы 5-20 хранят дерево для л = 4 (рис. 3.16).
5 6 7 89 10 11 1213 14 15 16 17 18 19 20
Рис. 3.16. Дерево для п = 4
Элементы 21 -84 храпят дерево для лв8,и элементы 85-340 хранят дерево для п* 16. При этом соответствие двумерным изображениям выглядит следующим образом:
п = 2
п = 4
и = 8
п = 16
1 2
569 10
21 22 25 26 ...
85 86 89 90 ...
34
78 11 12
23 24 27 28 ...
87 88 91 92 ...
13 14 17 18
29 30 ...
93 94 ...
15 16 19 20
31 32 ...
95 96 ...
Для обхода дерева квадрантов используется алгоритм вида: Безусловный цикл Если можно. то идем вниз. (j=4*i+l) иначе - если можно.
то идем вправо, (j-1+l)
иначе -
если можно.
то идем вверх, (j-i div 4) иначе, конец
Здесь i — индекс в массиве текущей вершины, у — индекс в массиве следующей вершины. В данной задаче дело осложняется еще тем, что образ был зашифрован, а для его расшифровки дается специальный ключ. Для иллюстрации рассмотрим второй тест из примера ввода. Зашифрованный образ задается так:
7
10
20
30
41
б 42
44
43
Соответствующее дерево образа имеет вид, показанный на рис. 3.17.
Рис. 3.17. Дерево зашифрованного образа
Зашифрованный образ имел вид:
41 42 10 10
44 43 10 10
20 20 30 30
20 20 30 30
Теперь ключ к расшифровке задан:
4
16
8
9
13
12
0
4
1
5
2
14 3
15 7
16 6
17 10
18 11
19 15
20 14
Для расшировки рассмотрим два массива.
А
В
8904
4142 10 10
13 12 1 5
44 43 10 10
231011
20 20 30 30
7 61514
20 20 30 30
По условиям задачи массив расшифрованного изображения строится по ним следующим образом: находим самый маленький элемент в массиве Л(0) и переносим элемент (10) из такой же позиции массива В в начальную позицию массива С и так далее по возрастанию значений в массиве Л. Построенный таким образом массив С будет иметь вид:
J 1010 20 20] 1010 20 20 [ 41 42 30 30 [ I 43 44 30 30 1
При программировании этого фрагмента выгодно в момент считывания ключа переупорядочивать его так, чтобы сразу получать позицию, с которой нужно брать элемент из В для занесения в очередную позицию массива С.
Рассмотрим детальнее реализацию решения. С учетом обработки нескольких тестов в одном входном файле тело главной программы может выглядеть так: begin.
assigndnput. 'D.txt'): reset(input); readln(NT);{Количество тестов}
for t:-l to NT do begin InputData;{Ввод и формирование образа}
OutRI;{Раскодирование и вывод}
end: close(input); end. Здесь:
InputData — процедура ввода зашифрованного образа и ключа к расшифровке. Одновременно с вводом частей образа сразу формируется представление в памяти зашифрованного образа;
OutRI — процедура вывода расшифрованного образа (в ней же одним оператором осуществляется расшифровка).
Рассмотрим подробнее некоторые фрагменты процедуры InputData. Запись в массив 77 порядка расшифровки при считывании эталонного кода осуществляется так:
for i:-1 to М do begin
{Кодирование эталонного образа}{Кодирование эталонного образа}readln(x.y):
{Кодирование эталонного образа}
{Кодирование эталонного образа}
TI[y+G[N2]+l]:-x: end:
«Разворачивание» зашифрованного изображения в массив RI в памяти делается так:
for i:-1 to S do
{Читаем зашифрованный образ}{Выставляем введенный цвет листу} {Закрашиваем все поддерево}{Читаем зашифрованный образ}{Выставляем введенный цвет листу} {Закрашиваем все поддерево}begin
{Читаем зашифрованный образ}
{Выставляем введенный цвет листу} {Закрашиваем все поддерево}
{Читаем зашифрованный образ}
{Выставляем введенный цвет листу} {Закрашиваем все поддерево}
readln(SiP.SIC);
if SiP>G[N2]
then RI[SiP]:-S1C
else Fill SubTree: end:
При этом для заполнения целого поддерева одним цветом вызывается процедура FillSubTree:
{Обход четверичного дерева}{Спускаемся к листьям}{Закрашиваем 4 листа}{Есть куда наверх ?}{Подымаемся вверх}{Выходим}{Обход четверичного дерева}{Спускаемся к листьям}{Закрашиваем 4 листа}{Есть куда наверх ?}{Подымаемся вверх}{Выходим}procedure Fill SubTree; var
{Обход четверичного дерева}
{Спускаемся к листьям}
{Закрашиваем 4 листа}
{Есть куда наверх ?}
{Подымаемся вверх}
{Выходим}
{Обход четверичного дерева}
{Спускаемся к листьям}
{Закрашиваем 4 листа}
{Есть куда наверх ?}
{Подымаемся вверх}
{Выходим}
.j : integer:
begin
i :-S1P;
while 4*1+1 <- G[N] do i:-4*i+l;
while true do
begin
for j:-0 to 3 do RI[i+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;
Эта процедура осуществляет обход «четверичного» дерева изображения и при достижении листьев этого дерева заносит номер соответствующего цвета в массив образа в памяти. Полный текст решения задачи приведен в конце главы.
3.3.2. Задача «BSP Trees»
East Central North America Programming Contest, 2000
BSP Trees (input.txt / output.txt / 10 с)
При прорисовке сцен с множеством объектов на экране очень важен порядок, в котором объекты прорисовываются. В общем случае чем дальше объект от наблюдателя, тем раньше он должен быть нарисован, чтобы ближние объекты рисовались поверх дальних. Если два объекта не перекрываются, то порядок прорисовки не существен. Деревья двоичного разбиения пространства (BSP-деревья) — одна из структур данных, которые используются для упрощения представления порядка следования объектов. Это делается следующим образом. Предположим, что экран лежит в плоскости XY и в его центре находится основание оси Z, а сама ось Z уходит от наблюдателя, смотрящего на экран. Пусть для простоты наблюдатель находится на оси Z в точке «минус бесконечность». Предположим, что все объекты лежат «за экраном», то есть для всех них z > 0. BSP-дерево строится проведением серии плоскостей параллельно плоскости ХУ. Первая плоскость делит пространство на два региона — регион, содержащий наблюдателя, и регион, не содержащий наблюдателя. Мы разделяем все объекты в пространстве на два подмножества — те объекты, которые попали в один регион с наблюдателем, и те объекты, которые не попали в один регион с наблюдателем. Понятно, что все объекты, попавшие в один регион с наблюдателем должны прорисовываться после всех объектов, которые не попали в один регион с наблюдателем. В данный момент BSP-дерево может рассматриваться как дерево, имеющее корень и двух потомков. Каждый потомок содержит объекты соответствующего множества. Теперь мы можем добавить вторую плоскость, которая разделит пространство опять. Мы разделяем каждое из двух разбиений на два, получая тем самым четыре разбиения, и в результате BSP-дерево будет иметь три уровня с разбиениями в листьях. Заметим, что часть этих разбиений могут содержать несколько объектов, а некоторые могут не содержать ни одного. Этот процесс продолжается до тех пор, пока каждое разбиение не станет содержать не более одного объекта или пока не будет проведено предопределенное заранее количество секущих плоскостей.
Если BSP-дерево построено корректно, то простой обход дерева даст вам порядок, в котором нужно прорисовывать объекты.
Ввод:
Первая строка содержит положительное число N« 20 — количество объектов на сцене. Все объекты, полагаются лежащими в одной плоскости. Следующие Nстрок содержат описание этих объектов в виде
m xl zl х2 z2 ... xmzm,
где т — количество вершин объекта, а оставшиеся величины — вершины пересечения объекта с плоскостью ZX. У каждого объекта от 3 до 6 вершин, и они имеют идентификаторы А, В, С,... в порядке ввода. Далее следует строка с положитель-
ным числом Р ^ 10, обозначающим количество плоскостей, использованных для создания BSP-дерева. Последние Р строк входа содержат описание каждой плоскости в виде
х1 zl х2 z2,
представляющее две точки линии пересечения этой плоскости с плоскостью ZX. Вы можете считать, что нет линий, пересекающих объекты (их ребра и вершины), и что нет плоскостей, параллельных оси Z. Все координаты — целые числа.
Вывод:
Вывод состоит из одной строки, содержащей идентификаторы объектов в порядке, в котором они должны прорисовываться в соответствии с построенным BSP- деревом. В случае если разбиение содержит один или более объектов, выводите их идентификаторы в алфавитном порядке.
Пример ввода
(продолжение ввода)Пример вывода
10
3 65 5 66 5 65 6
3 65 123 66 123 65 124
3 122 176 123 176 122 177
3 56 23 57 23 56 24
3 11 49 12 49 11 50
3 167 111 168 111 167 112
3 57 123 58 123 57 124
3 130 6 131 6 130 7
3 100 85 101 85 100 86
3 11 28 12 28 11 29
10BCGEJFIHDA
159 165 -131 -177 -153-192-197 158 -77 -86 -98 30 -177 59 146 63 192-117 92 43 121 -67 -62-134
41 -81 130 196 95 -Т85 -89 154 -163 -179 93 175 11341 -92 -28
Вначале приведем краткое изложение постановки задачи. BSP-деревья применяются для представления пространственных сцен. Чем дальше изображаемый объект от экрана, тем раньше он должен быть нарисован. BSP-дерево строится с помощью серии плоскостей, разбивающих пространство на подпространства. Первая плоскость делит пространство на два региона — ближний к обозревателю, и дальний от него. Соответственно все подлежащие изображению объекты делятся на два региона. Все объекты в ближнем регионе должны быть нарисованы ПОСЛЕ всех объектов дальнего региона. Затем проводим следующую плоскость, она делит каждый из имеющихся регионов на подрегионы. Продолжаем процесс для всех имеющихся плоскостей. Если регион содержит не более одного объекта, то он далее не делится. Таким образом получается BSP-дерево пространственной сцены.
В данной задаче предлагается упрощенный вариант — построить BSP-дерево для плоских объектов. Задано некоторое количество объектов (многоугольников) на плоскости и некоторое количество прямых, разделяющих эти объекты на подмножества. Гарантируется, что ни один объект не имеет общих точек ни с одной прямой. Кроме того, все прямые не параллельны оси ординат. Объекты обозначаются латинскими буквами А, В, С,... (всего объектов не более 20, а прямых — не более 10). Нужно вывести последовательность порядка отрисовки объектов.
Идея решения: по условиям задачи прямые не пересекают фигуры и, следовательно, для определения положения всей фигуры относительно прямой достаточно анализировать положение любой (например, первой) ее точки. Разбиваем множество всех первых точек фигур на подмножества по разные стороны от очередной прямой. В левое подмножество включаем точки, лежащие выше прямой. После завершения процесса для всех прямых выводим получившиеся листы дерева в порядке обхода слева направо.
Рассмотрим подробнее реализацию этого алгоритма. Тело главной программы выглядит так:
begin
assigndnput. '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*M]):
WriteTree(l):
close(input); close(output);
end.
Здесь процедура InputData обеспечивает ввод первых точек каждой фигуры (остальные ипюрируются) ” секущих прямых. Для каждой прямой по условиям даются ее 2 точки. В процедуре ввода сразу строятся коэффициенты уравнения прямой (A[i]tB[i]t C[i])・
При заданных ограничениях на количества объектов и прямых двоичное дерево можно хранить в виде массива Т, в котором сыновья вершины с индексом j имеют индексы 2/ и 2; + 1. Вершина T[j] хранит строку букв, обозначающих объекты, находящиеся в данном регионе. В Д1] помещаются буквы всех объектов.
Далее, циклом по количеству прямых (:) и по номеру региона j делим регионы Г[刃,(в которых еще более одного объекта) на подрегионы T[2*j] и T[2*j + 1]. Разбиение на подрегионы осуществляет процедура Split.
Рекурсивная процедура WriteTree(\) обеспечивает обход дерева по листам слева направо и вывод ответов. Рассмотрим подробнее реализацию процедуры 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:
B[i]:-xl-x2;
C[i]:eyl*(x2-xl)-xl*(y2-yl);
end:
Pof2[l]:-1;
for i:-2 to NL+2 do pof2[i]:-2*Pof2[i-1];
for i:-l to Pof2[NL+2] do Т[1]:-";
end;
Здесь:
NP — количество фигур (или точнее, точек — мы берем только координаты первой точки каждой фигуры x[i], y[i]);
口 NL— количество секущих прямых xl, yl, х2, у2 — две точки, через которые проходит текущая прямая. Л[:], B[i], C[i] — коэффициенты канонического уравнения этой прямой;
Pof2 — массив степеней двойки;
T[i] — массив, который будет содержать дерево разбиения.
Теперь обратимся к процедуре Split.
procedure Splitd integer; var S.S1 ,S2:string20):
begin
for k:-l to length(S) do
begin
m:=ord(S[k])・ord('A')+l:
if y[m]>(-(x[m]*a[i]+c[i])/b[i])
then Sl:-Sl+S[k]
else S2:-S2+S[k];
end:
end:
m — номер точки, определяемый по букве фигуры, которую представляет эта точка. Если точка (х[т],у[т]) лежит выше прямой {А^х+ВЩу+С^О), то она заносится в левое поддерево, иначе — в правое.
И, наконец, рекурсивная процедура левостороннего обхода листьев дерева:
procedure WriteTreeCj:integer);
begin
if (j>2048) then exit:
if (j<-1024) and (T[2*j]<>'') then WriteTree(2*j);
if (j<-1024) and (T[2*j+1]<>'') then WriteTree(2*j+l):
if (j>1024) and (T[j]<>") or
(T[2*j]-") and (T[2*j+1]-")
then write(T[j]);
end:
Нам нужно выводить содержимое только тех элементов массива, у которых нет «потомков», а содержимое — есть.
Полный текст решения задачи приведен в конце главы.
Задачи на двоичные деревья сортировки
В этом разделе представлены задачи «Дерево», «Parliament», ^Falling Leaves», при решении которых дерево используется как сортирующая структура.
Задача «Дерево»
XV Белорусская олимпиада по информатике, 2002
Дерево (Input.Txt / Output.Txt /1с)
Пете Булочкину крупно повезло: он наконец устроился на работу в фирму Macrohard. Он хочет показать себя с самой лучшей стороны, поэтому к первому своему заданию отнесся весьма ответственно.
Задание состоит в том, чтобы написать поисковую систему. Пете заранее известен набор чисел Л1, Д2,..., Ak (все Ai — различные целые числа) — назовем их ключами. Система должна обрабатывать запросы типа: «Содержится ли среди ключей число s?» Известно, что число s может быть любым целым числом от 1 до п (л < 109).
Руководство фирмы сказало Пете, что ему нужно использовать как можно меньше памяти. Поразмыслив, Петя решил, что оптимальным решением поставленной задачи будет использование двоичных деревьев поиска, описание которых приведено далее.
Описание.
Двоичное дерево может быть пустым или состоять из вершины, к которой присоединены два двоичных дерева, то есть левое и правое поддеревья (в этом случае вершину, к которой присоединяются деревья, называют корнем). Если в каждую вершину поместить по ключу, причем так, чтобы в разных вершинах были различные ключи, то получим двоичное дерево для заданного набора ключей. Будем говорить, что дерево является двоичным деревом поиска, если левое и правое поддеревья являются двоичными деревьями поиска, а также любой ключ из левого поддерева, выходящего из корня, меньше ключа, записанного в корне, а любой ключ из правого поддерева —больше. На рис. 3.18 показаны различные двоичные деревья для набора ключей 1, 3, 7, 11, из них деревья 6 и 6 — двоичные деревья поиска. Корни деревьев изображены сверху. Корни деревьев апб— вершина с ключом 1, корень дерева в — вершина с ключом 7.
Для того чтобы проверить, содержится ли в заданном двоичном дереве поиска ключ s, используется следующий алгоритм: 1) положить текущую вершину равной корню дерева; 2) проверить, совпадает ли s с ключом, записанным в текущей вершине. Если да, то ключ s найден. Иначе перейти к шагу 3; 3) если s меньше ключа, записанного в текущей вершине, то положить текущую вер: шину равной корню левого поддерева, иначе — равной корню правого поддерева (если соответствующие поддеревья отсутствуют, то алгоритм заканчивает работу, выдавая, что ключ s в дереве отсутствует). Перейти к шагу 1.
Стоимостью поиска ключа s назовем количество выполненных шагов вышеописанного алгоритма. Например, для дерева на рис. 3.18, в стоимости поиска различных ключей указаны в табл. 3.2.
7 абв
Рис. 3.18. Примеры двоичных деревьев поиска
Таблица 3.2. Стоимости поиска ключей
Ключ 1234567891011
Стоимость 23333312222 поиска
Стоимостью заданного двоичного дерева для диапазона поиска от 1 до л назовем сумму стоимостей поиска каждого из ключей от 1 до 〃 в этом дереве. Например, стоимость дерева на рис. 3.18, в для диапазона поиска от 1 до И равна 26.
Задание'.
Петя хочет построить для своей поисковой системы двоичное дерево поиска минимальной стоимости. По введенным числам л, Л, /11,..., Ak определить минимальную стоимость С двоичного дерева поиска для набора ключей Л1, А2,..., Ak и диапазона поиска от 1 до п.
Ввод:
Входные данные состоят из А + 2 строк: 1-я строка содержит число 九,2-я строка — число к、строки с 3-й по (А + 2)-ю — числа Л1,...»Ak по одному в строке.
Вывод\
Выходной файл должен содержать единственное число С.
Пр-мер ььо|Д&Пр*м❷р ььш<ад&
-1022
4
9
3
7
4
Оптимальное дерево для заданного примера показано на рис. 3.19.
Рис. 3.19. Оптимальное дерево для заданного примера
Описание решения'.
По условиям задачи нам нужно найти минимальную стоимость двоичного дерева поиска для набора ключей Л[1],...»А[К] и диапазона поиска от 1 до ^ Будем искать эту минимальную стоимость как минимальную из всех стоимостей, который могут получаться для ВСЕХ вариантов двоичных деревьев поиска для заданного множества ключей.
Прежде всего упорядочим ключи A[i] по возрастанию. Положим также Л[0] = О, А伏+ 1]г+ 1.
Введем обозначение F(i, j) — минимальная стоимость двоичного дерева поиска для набора ключей Л/,...»Л/ и диапазона поиска от Л[/~ 1 ] + 1 до Л[/ + 1]-1. Тогда по условиям задачи нам фактически требуется найти F(l, А).
Будем вычислять минимальную стоимость 式iJ) рекурсивно через минимальную из возможных стоимостей при выборе вершины s (s изменяется в диапазоне от: до j) в качестве корня и минимального левого поддерева (из элементов от i до s-1) и минимального правого поддерева (из элементов от s+1 до j). Пусть
[О, если / > 丿
F("丿)—力[丿 +1]—/р — 1] — 1 + min(F (i, s — 1) + 尸(s +1, у)), если j 4 丿
Для ускорения рекурсивных вычислений заведем также массив ЯИ, в котором будем хранить вычисленные значения 网i J) для устранения потерь времени на повторные вычисления 网 i J). Значение-1 в массиве FM[i,j] будем трактовать как невычис- ленное ее значение функции F(itj). Тело главной программы может выглядеть так: begin
assi gn(i nput,'1nput.txt'): reset(input);
assign(output.'output.txt'): rewrite(output):
read(N.K);
for i:-l to k do read(a[i]):1
SortA; A[0]:-0: A[K+1]:-N+1;
for i:-0 to K+l do
for j:-0 to K+l do FM[i,j]:--l:
writeln(Fd.k)):
close(input): close(output);
end.
Рекурсивная функция F(itj) может быть реализована так:
function F(1,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:-maxlongint:
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;
Полный текст решения задачи приведен в конце главы.
Задача «Parliament»
NEERC, центральный четветьфинал, 2001
Parliament (lnput.txt / output.txt /3 с)
В государстве ММММ выбран новый парламент. При регистрации каждый член парламента получил уникальный идентификатор (положительное целое число). Идентификаторы раздавались в случайном порядке, возможны и разрывы в последовательности чисел. Кресла в парламенте организованы в виде «дерева». Когда члены парламента входят в аудиторию, они занимают места в следующем порядке. Первый вошедший занимает место председателя. Каждый следующий делегат возглавляет левое дерево, если его идентификатор меньше идентификатора председателя, или справа в противном случае. После чего он садится в пустое кресло и объявляет себя председателем крыла.
Если кресло председателя крыла уже занято, то алгоритм распространяется далее — делегат отправляется налево или направо в зависимости от результатов сравнения своего идентификатора с идентификатором председателя крыла.
Рисунок 3.20 показывает пример рассаживания членов парламента, если они входили в аудиторию в следующем порядке: 10,5,1,7,20,25,22,21,27. Во время первой сессии парламент принял решение не изменять порядок рассаживания. Был также принят порядок выступлений.
Если номер сессии нечетный, то парламентарии высказывались в следующем порядке: левое крыло, правое крыло, председатель. Если в крыле более
одного парламентария, то их порядок выступления был такой же: левое крыло, правое крыло, председатель.
Если номер сессии четный, то порядок выступлений менялся: правое крыло, левое крыло, председатель.
Рис. 3.20. Пример дерева для задачи «Parliament»
Для приведенного примера порядок выступлений для нечетных сессий — 1, 7, 5, 21, 22, 27, 25, 20, а для четных сессий — 27, 21, 22, 25, 20, 7,1, 5,10.
Ваша задача — определить порядок выступления для четной сессии, если известен порядок выступления для нечетной сессии.
Ввод:
Первая строка входного файла содержит количество парламентариев N. Следующие строки содержат N целых чисел — идентификаторы парламентариев в порядке их выступления на нечетных сессиях.
Вывод:
Выходной файл должен содержать идентификаторы парламентариев в порядке их выступления на четных сессиях.
Ограничения:
Общее количество членов парламента не превысит 3 000. Идентификаторы не превысят 60 000.
Пример ввода: 9
1 7 5 21 22 27 25 20 10 Пример вывода'. 27 21 22 25 20 7 1
5 10
Итак, дан «левосторонний» обход двоичного отсортированного дерева. Нужно построить его «правосторонний обход». Под «левосторонним» обходом здесь понимается такой обход, при котором вершины посещаются в порядке «левое поддерево - правое поддерево - корень». Под «правосторонним», соответственно, понимается обход в порядке «правое поддерево 一 левое поддерево - корень».
Решение задачи заключается в выполнении двух шагов.
Построение двоичного отсортированного дерева по заданному «левостороннему» обходу. При этом элементы массива добавляются в отсортированное дерево в обратном порядке (начиная с последнего).
«Правосторонний» обход построенного дерева.
Тело главной программы может выглядеть следующим образом:
begin assign(input.'input.txt'): reset(input): assign(output.'output.txt*): rewriteCoutput): 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.
Простейший способ представления двоичного дерева — хранить его в массиве (D), используя индексы массива в качестве ссылок к сыновьям.
Корень дерева имеет индекс 1. Для вершины i левый потомок имеет индекс 2i, а правый потомок — индекс 2i + 1.
Процедура AddToTree(a[i\) добавляет в двоичное отсортированное дерево элемент a[i]. Процедура WriteTree^ 1) выводит элементы дерева, корнем которого является вершина номер 1.
Решение задачи приведено в конце главы. Оно имеет, к сожалению, недостаток, связанный с ограничениями, установленными авторами данной задачи. Три тысячи чисел (по условию их может быть столько) могут не поместиться в памяти, если использовать такое представление дерева.
Поэтому далее приводится аналогичное по содержанию решение, которое использует другое представление дерева. А именно: в дополнение к массиву введенных чисел а[1 ..Зоб。] для хранения дерева используются еще два массива Le/k[1..3000] и Right[i..3000], Корнем дерева (Root) объявляется элемент N. Left[i] указывает индекс в массиве а элемента, который является левым потомком элемента i. Right[i] указывает индекс в массиве а элемента, который является правым потомком элемента i. При прохождении текущего узла необходимо также знать номер элемента, который является его предком. Для этого используется переменная pred.
Тело главной программы нового решения выглядит так:
begin
assigndnput.'input.txt'): reset(input);
assign(output.'output.txt'): rewriteCoutput):
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-1 downto 1 do AddToTreed);
WriteTreeCRoot.Root.2):
close(input); close(output): end.
Вначале инициализируется пустое дерево (Left[i] * 0, Right[i] = 0). Затем определяется корень дерева Root. Процедуры AddToTree (добавить элемент в дерево) и Write Tree (вывести дерево) несколько видоизменены, рассмотрим их подробнее.
procedure AddtoTreed:word):
begin
Тек:-Root:
while ((a[i]>a[Tek]) and (Right[Tek]<>0)) 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]-0)
then Right[Тек]:-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]<>0 then WriteTree(Right[j].j.2):
if Left [j]<>0 then WriteTreeCLeft [j].j.D:
writeln(a[j]):
if Dir-2
then Right[pred]:-0
else Left[pred]:-O: end;
Процедура WriteTree имеет три входных параметра:
j — номер элемента в текущей вершине дерева;
pred — номер элемента, который ссылался на текущий;
Dir — направление (1 — влево, 2 — вправо), по которому шли от предыдущего элемента к текущему.
В соответствии с «правосторонним» обходом вначале обходится правое дерево (WriteTree(Right[j]Jt 2)), затем левое дерево ( WriteTree(Left [j] J,l))・ Затем выводится текущий узел и обнуляется ссылка, по которой мы в него пришли.
Полный текст решения задачи приведен в конце главы.
Задача «Falling Leaves»
Mid-Central USA Programming Contest, 2000
Falling Leaves (leaves.In / leaves.out /5 с)
АЛ
Рис. 3.21. Пример дерева для задачи «Falling Leaves»
Рисунок 3.21 показывает графическое представление двоичного дерева из букв. Двоичное дерево букв — либо пустое, либо имеет букву в качестве данных и ссылки на левое и правое поддеревья. Левое и правое поддеревья также являются двоичными деревьями букв. В графическом представлении двоичных деревьев букв пустые деревья опускаются, каждая вершина идентифицируется своей буквой, отрезком линии вниз-влево к левому поддереву (если оно непустое), и отрезком линии вниз-вправо к правому поддереву (если оно непустое). Листом в двоичном дереве называется вершина, оба поддерева которой пусты. Например, в изображенном выше дереве листами являются вершины с данными В, D, Н, Р, У. Прямой порядок обхода деревьев удовлетворяет следующим свойствам: 1) если дерево пусто, то и порядок его обхода пуст; 2) если дерево непустое, тогда прямой обход выполняется в следующем порядке: корень дерева, прямой обход левого поддерева, прямой обход правого поддерева. Например, порядок прямого обхода дерева на рис. 3.21 — KGCBDHQMPY.
Дерево, представленное на рис. 3.21, является также и деревом двоичного поиска букв. Двоичное дерево поиска букв — это двоичное дерево букв, в котором каждая вершина удовлетворяет следующим свойствам: 1) буква в корневой вершине встречается в алфавите после всех букв в вершинах левого поддерева; 2) буква в корневой вершине встречается в алфавите перед всеми буквами в вершинах правого поддерева.
Задание:
Рассмотрим следующую последовательность операций на двоичном дереве поиска из букв: удалим все листья и выпишем все удаленные данные; повторим эту процедуру, пока дерево не станет пустым. Если начинать с дерева, представленного на рисунке, мы получим последовательность деревьев, показанную на рис. 3.22, и такие удаляемые данные (листья):
BDHPY
СМ
GQ
Рис. 3.22. Иллюстрация «опадания листьев»
Ваша задача — обработав такую последовательность строк листьев, удаленных из буквенного двоичного дерева поиска, вывести порядок прямого обхода этого дерева.
Ввод:
Входной файл содержит один или более тестов. Каждый тест — последовательность из одной или более строк с заглавными латинскими буквами. Они содержат листья, удаленные из буквенного двоичного дерева поиска по описанным стадиям.
Буквы на строке представлены в алфавитном порядке.
Тесты разделяются строкой, содержащей только символ '*'.
За последним тестом следует строка, содержащая только символ '$'.
Вывод:
Для каждого теста выведите одну строку — прямой обход соответствующего дерева.
Пример ввода
Пример вывода
BDHPY
СМ GQ
К
KGCBDHQMPY ВАС
АС В
$
Итак, требуется построить обход дерева в порядке <корень т левое_поддерево т правое_поддерево». Как и в предыдущей задаче, решение включает в себя два шага.
Восстановление дерева.
Вывод дерева в нужном порядке.
С учетом особенностей формата ввода и того, что элементами деревьев являются символы (а не числа, как в предыдущей задаче), тело главной программы выглядит так:
begin
assign(input.'1 eaves.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;
Root:-N:
for i :-N-l downto 1 do AddToTreed):
WriteTree(Root); writeln;
readln(s):
end:
close(input): close(output): end.
Как и в предыдущей задаче, AddToTree — процедура добавляющая очередной элемент к дереву, WriteTree — процедура обхода дерева и вывода его элементов в нужном порядке.
Рассмотрим детальнее реализацию процедуры WriteTree, которая претерпела некоторые изменения по сравнению с предыдущей задачей.
procedure WriteTreeCj:integer);
begin
write(a[j]);
if Left [j]<>0 then WriteTreeCLeft [j]):
if Right[j]<>0 then WriteTree(Right[j]):
end:
Вначале выводится корневой элемент, потом выполняется обход левого дерева (WriteTree(Left[/])) и затем обход правого дерева (WriteTree(Right[j])).
Полный текст решения задачи приведен в конце главы.
Кодирование последовательностей символов методом Хаффмана
В данном разделе приводятся две задачи: «Кодирование» и «Entropy», решение кооторых основывается на кодировании символов по методу Хаффмана.
Задача «Кодирование»
Гомельская городская олимпиада школьников по информатике, 2002
Кодирование (input.txt / output.txt /5 с)
Необходимо произвести оптимальное кодирование исходного текста, представляющего собой набор символов из множества М ^ {^:^,^,^,^.^,^ *, '0'..'9', 'A'..'Z', 'a'..'z', 'А'..'Я', 'а'..'я'} в двоичное представление. Это значит, что каждый символ входного предложения должен быть закодирован уникальным набором нулей и единиц. Символы, не принадлежащие множеству М, кодировать и помещать в выходной файл не надо. Выходные данные, представляющие собой закодированный текст, также должны однозначно декодироваться. В данном случае оптимальным кодом называется код, содержащий наименьшее суммарное число единиц и нулей.
Ограничения'.
Гарантируется, что размер входного файла не будет превышать 170 Кбайт.
Алгоритм построения оптимального кода для некоторой последовательности символов состоит в том, чтобы кодировать символы, встречающиеся наиболее часто, последовательностью минимальной длины, и наоборот.
Вычисляем для каждого символа количество раз, которое он встретился в последовательности, и записываем это число в список.
В данном списке находим два самых маленьких числа. Суммируем их, добавляем сумму в список и вычеркиваем их из списка.
Пока в списке больше двух чисел, продолжаем выполнение пункта 2.
Ставим в соответствие первому числу из списка 0, а второму — 1.
Начинаем последовательно выполнять возврат к исходному состоянию списка (то есть как бы разъединяя суммы и добавляя элементы в список). При этом, если мы разъединяем элемент списка, которому сопоставлено двоичное число X, то необходимо первому из создаваемых элементов сопоставить число ХО, а второму XI.
Продолжаем выполнение пункта 5 до тех пор, пока не исчерпаем все суммы, которые необходимо разъединить.
В итоге мы получим двоичные коды для чисел из последовательности.
Пример:
Последовательность: ABCAD. Символы:
А
2\
/-—-
0
код для А
В
1\
2\
//
10
код для В
С
1\
1\
2-
Сопоставим 0
/ю
110
код для С
D
1-
2-
3-
Сопоставим 1
-И
111
код для D
Оптимальный код для последовательности:
0101100111
Ввод:
Входной файл содержит N (N не дано) строк с текстом. Вывод:
Выходной файл состоит из двух частей.
Коды символов, встречающихся во входном тексте, выписанные в произвольном порядке.
[символ][пробел][код]
[символ][пробел][код]
Одна строка, содержащая закодированный текст. Пример ввода\ А В А.
Пример вывода: 001 .ООО А 1 В 01 1010011000
Фактически в условии задачи описано ее решение. Рассмотрим практическую реализацию. Тело главной программы выглядит так: begin
InputData;
SumUp:
BuildBits;
OutputData: end.
Здесь:
InputData — процедура ввода исходных данных;
SumUp — процедура построения сумм;
BuildBits — процедура кодирования входных символов двоичными строками; □ OutputData — процедура кодирования и вывода исходного текста.
Рассмотрим их детальнее.
procedure InputData;
var
с : char;
s : array [0..255] of longint:
begin
assigndnput. '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[i]; sk[j]:-chr(i) end;
kj:=j:
close(input):
end:
По условию задачи символы с кодами #10 и #13, разделяющие строки, кодировать не нужно, поэтому их частоты не считаются. В результате ввода:
kj — количество различных сиволов во входном файле;
sA[ j] — массив различных символов;
k[j] — частоты этих символов.
procedure SumUp:
var
si. s2. ml. m2 : longint;
begin
if kj-1
then begin
assignCoutput.'output.txt'): rewrite(output):
writeln(sk[l],' ' .0): writeln(O);
close(output):
halt(O) end;
for i:-l to kj do Free[i]:-true: kkl:-kj; kk2:-kj;
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 запоминаются номера элементов массива А, просуммированных в соответствующий элемент массива А, начиная с номера 佇 + 1 и до номера АА2.
procedure BuildBits:
begin
bits[kk2]:・’l'; Free[kk2]:-false;
b1ts[a[kk2]]:-bits[kk2]+,0,:
bits[b[kk2]]:-bits[kk2]+'r: i:-M1nK: bits[m]:='O': Free[m]:-true: bits[a[m]]:・bits[m]+'O': bits[b[m]]:=bits[m]+'l'; for 1:- kk2-l downto 1 do if not Freed] then begin
bits[a[i]]:-bits[i]+,O,; bits[b[i]]:=bits ⑴+T: end; end:
Процедура BuildBits присваивает элементу с номером kk2 префикс 1. Затем находит в массиве k второй по величине элемент (после k[kk2]) и присваивает ему префикс 0.
Далее для всех оставшихся элементов массива А строит их префиксы, используя номера элементов (а[:] и Ьр]), составивших очередной элемент k[i].
procedure OutputData;
var
str : string:
begin
assign(output.'output.txt'); rewrite(output):
for i:- 1 to kj do writeln(sk[i],' '.bitsCi]);
for i:-l to kj do res[ord(sk[i])]:-bits[i]:
assign(i nput.'i nput.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, в котором хранятся полученные коды символов. Полный текст решения задачи приведен в конце главы.
Задача «Entropy»
Greater New York Regional Contest, 2000
Entropy (input.txt / output.txt / 10 с)
Энтропийное кодирование — это метод кодирования данных, который обеспечивает компрессию данных за счет удаления избыточной информации. Например, английский текст, закодированный с помощью таблицы ASCII,— это пример сообщения с высокой энтропией. В тоже время сжатые сообщения, например zip-архивы, имеют очень маленькую энтропию, и потому попытки их энтропийного кодирования не принесут пользы.
Английский текст, закодированный с помощью ASCII, имеет высокую степень энтропии, потому что для кодирования всех символов используется одно и тоже количество бит — восемь. В то же время известный факт состоит в том, что буквы E,L、N, R,SnT встречаются со значительно более высокой частотой, чем другие буквы английского алфавита. Если мы найдем способ закодировать только эти буквы четырьмя битами, то закодированный текст станет существенно меньше и при этом будет содержать всю исходную информацию и иметь меньшую энтропию. Однако как нам различить при декодировании, четырьмя или восемью битами закодирован очередной символ? Эта проблема решается с помощью префиксного кодирования.
В такой схеме кодирования любое количество битов может быть использовано для конкретного символа. Однако для того чтобы иметь возможность восстановить информацию, запрещено, чтобы последовательность битов, кодирующая некоторый символ, была префиксом битовой последовательности, используемой для кодирования любого другого символа. Это позволя-
ет читать входную последовательность бит за битом, и как только встречено обозначение символа — его декодировать.
Рассмотрим текст AAAAABCD. Кодирование, использующее ASCII, требует 64 бит. Если же мы символ А закодируем битовой последовательностью 00, символ В — последовательностью 01, символ С - последовательностью 10,аО — последовательностью 11, то для кодирования потребуется всего 16 бит. Результирующий поток битов будет такой: 00000000000ПОП. Но это все еще кодирование с фиксированной длиной, мы просто использовали для каждого символа два бита вместо восьми.
Символ А встречается чаще, чем другие, можем ли мы его закодировать с помощью меньшего количества битов?
Да, можем, тогда мы закодируем символы такими последовательностями битов:
Л-0
8-10
С- 110
D- 111
Используя такое кодирование, мы получим только 13 бит в закодированном сообщении: 0000010110111. Коэффициент сжатия в этом случае равен 4.9 к 1. Это означает, что каждый бит в последнем закодированном сообщении содержит столько же информации, сколько и 4.9 бит в первом закодированном сообщении (с помощью ASCII).
Попробуйте читать сообщение 0000010110111 слева направо — и убедитесь, что «префиксное» кодирование обеспечивает простое декодирование текста, даже несмотря на то, что символы кодируются различным количеством битов. В качестве другого примера рассмотрим текст THE CAT IN THE HAT.
В этом тексте символы Г и пробел встречаются чаще других. Поэтому их нужно кодировать меньшим количеством битов. А символы С, / и N встречаются только по одному разу, потому будут кодироваться самыми длинными кодами. Например, так: пробел — 00
Л - 100
С- 1110
£-1111
Н- 110
/- 1010
N— 1011
Т-01
При таком кодировании исходного предложения потребуется только 51 бит против 144, которые необходимы, чтобы закодировать исходное сообщение с помощью 8-битного ASCII-кодирования. Коэффициент сжатия равен 2.8 к 1.
Ввод:
Входной файл будет содержать список текстовых сообщений, по одному в строке. Сообщения будут состоять только из больших английских букв, цифр и символов подчеркивания (вместо пробелов). Конец файла обозначается строкой END. Эту строку не нужно обрабатывать.
Вывод:
Для каждого входного сообщения выведите количество бит в восьми-битовом ASCII-кодировании, количество бит при оптимальном префиксном кодировании и коэффициент сжатия с точность до одного знака после десятичной точки.
Пример ввода:
AAAAABCD
THE_CAT_IN_THE_HAT
END
Пример вывода:
64 13 4.9
144 51 2.8
В данной задаче, как и в предыдущей, предлагается провести кодирование текста алгоритмом Хаффмана. Отличиями являются представление входных и выходных данных. Каждую входную строку нужно кодировать по отдельности. Строка «END» обозначает конец ввода, ее кодировать не нужно.
На выходе нужно указать три числа:
длину сообщения в битах при стандартном восьми-битном кодировании;
длину сообщения в битах при выполненном оптимальном кодировании;
коэффициент сжатия.
Полный текст решения приведен в конце главы.
3.6. Перечисление деревьев
В данном разделе приведены задачи «Nextree» и «Trees Made to Order», связанные с тематикой «Перечисление деревьев».
Задача «Nextree»
USA Computing Olimoiad, 2000
Nextree (nextree.in / nextree.out)
На ферме Джона растет большое количество деревьев. После обучения в компьютерном классе корова Бетси заметила, что все эти деревья являются двоичными деревьями, у которых каждая вершина, не являющаяся листом, имеет ровно две вершины-наследницы. Бетси назначила каждой вершине число — количество листьев в поддереве, которое имеет эту вершину в качестве корневой.
Затем Бетси расположила числа, ассоциированные с вершинами, в соответствующем порядке, однако она указала только числа, ассоциированные с корневой вершиной и всеми левыми наследницами всех вершин. Для уточнения рассмотрим дерево, показанное на рис. 3.23.
Рис. 3.23. Пример дерева для задачи «Nextree»
Звездочками помечены вершины, указанные Бетси. Набор чисел, представляющих такое дерево — 7412111.
После представления каждого дерева таким образом Бетси заметила, что все деревья имеют одинаковое количество листьев; все деревья имеют различное числовое представление; все возможные двоичные деревья имеются на ферме.
Поэтому, будучи творческой коровой, она решила отсортировать коды этих деревьев.
Решите эту же задачу и вы. Вам задано числовое представление (код) дерева, найдите код, который следует непосредственно за ним, в списке Бетси.
Ввод:
Строка 1:丄一длина кода (1 < L < 1000);
Строка 2:丄 разделенных пробелами целых чисел, которые представляют код дерева в списке Бетси.
Пример ввода:
5
5 3 2 1 1
Вывод:
Одна строка с разделенными пробелом целыми числами кода, который следует за данным кодом в лексико-графическом порядке. Если входной код является последним в списке Бетси, выведите 0. Помните: деревья, указанные на вводе и представленные в выводе, должны иметь одно и то же количество листьев.
Пример вывода:
5 4 111
Описание решения'.
Прежде всего заметим, что при таком способе нумерации деревьев (слева направо) самые ранние изменения произойдут в самом правом «нетупиковом» поддереве. «Нетупиковым» мы здесь называем поддерево, которое может быть изменено так, что его корневая вершина не изменится, а некоторые из поддеревьев изменятся. Рассмотрим это положение на примере.
Входной строке 5 3 2 1 1 соответствует дерево на рис. 3.24.
/ \
*32
/\ /\
*21*11
/\
11
Рис. 3.24. Дерево с кодом 5 3 2 1 1
Символом '*' помечены вершины, вошедшие в запись кода дерева. Здесь правое поддерево не может быть изменено, и потому изменяем левое поддерево (рис. 3.25).
Рис. 3.25. Дерево с кодом 5 4 111
Было: 5 3 2 1 1
Ответ: 5 4 111
Рассмотрим еще один пример (рис. 3.26).
/\ /\ 24 *12
/\ /\ /\
*11 *22*11
/\/\
*11*11
Рис. 3.26. Дерево с кодом 9 6 2 1 2 1 1 1
Код представленнного дерева — 96212111 1. Самое правое его «нетупиковое» поддерево показано на рис. 3.27:
3
/\
12
/\
1 1
Рис. 3.27. Самое правое «нетупиковое» поддерево
Оно имеет код 1 1 ( в записи кода всего дерева). Это поддерево может быть изменено на поддерево, изображенное на рис. 3.28.
3
/\
*2 2
/\
1 1
Рис. 3.28. Преобразованное «нетупиковое» поддерево
/\*6 3/\ /\/\*6 3/\ /\Его код — 2 1. В результате все дерево примет такой вид, как на рис. 3.29.
/\*6 3/\ /\
/\*6 3/\ /\
2/\1 12/\1 124・
2/\1 1
2/\1 1
/I /\I 1 *22
/\ /\*1 1 *1 1
Рис. 3.29. Дерево с кодом 9 6 2 1 2 1 1
Его код 96212112 1. Теперь самое правое «нетупиковое» поддерево (рис. 3.30) имеет код 2 11.
/\ /\*1 1 *1 1/\ /\*1 1 *1 1
/\ /\
*1 1 *1 1
/\ /\
*1 1 *1 1
Рис. 3.30. Новое самое правое «нетупиковое» поддерево
Следующее по счету дерево (рис. 3.31) будет иметь код 3 11.
/\
*31
/\
*1 2/\*11
Рис. 3.31. Следующее по счету поддерево
Целиком следующее дерево будет иметь вид, показанный на рис. 3.32, и код 9 6 2 131111.
Рис. 3.32. Дерево с кодом 9 6 2 1 3 1 1 1 1
Заметим, что после реорганизации самого правого «нетупикового» дерева, все более правые «тупиковые» деревья преобразуются к «стартовой позиции» (рис. 3.33).
Рис. 3.33. «Стартовая позиция» нетупикового поддерева
Таким образом, очевидно, что для нахождения следующего кода дерева нужно найти самое правое «нетупиковое» поддерево, перестроить его (в сторону минимального увеличения кода), преобразовать все более правые «тупиковые деревья» к «стартовой» позиции и вывести код получившегося дерева.
При реализации достаточно работать с фрагментом массива исходного кода, представляющего найденное тупиковое дерево.
Алгоритм изменения этого фрагмента кода таков: с правой стороны фрагмента массива ищем первый элемент, который меньше предыдущего па 2 или более. Увеличиваем его на 1, а все позиции справа от него заполняем единицами.
Рассмотрим конкретную реализацию программы, решающей поставленную задачу. Тело главной программы выглядит следующим образом: begin assign(input・'nextree.in'): reset(input): assign(output.'nextгее.out*):rewrite(output): read(L): for i:-l to L do read(a[i]): Done:-false: CheckTree(l.L): if not Done then begin writeln(O): exit end: for i:-l to L-l do write(a[i].' '): 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[i];
if (not Done) and (a[p+l]>2)
then CheckTree(p+1.p+a[p+l]). end 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[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:
Здесь:
(a[p] > a[p + 1] + 2) — основное условие наличия правого дерева;
口夕—номер позиции в массиве, на которую нужно внести отсутствующую в коде вершину правого дерева;
a[q] = 〃[р] - а[р + 1] — количество вершин в правом дереве;
6 и с — используются для сохранения/восстановления затираемых элементов в массиве;
CheckTree(p + 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 с)
Мы можем перенумеровать двоичные деревья по следующей схеме: пустое дерево получает номер 0; дерево с одной вершиной получает номер 1; все двоичные деревья с т вершинами получают номера меньше, чем все двоичные деревья с 加+.1 вершиной; любое двоичное дерево с т вершинами и левым и правым поддеревьями L и Л получает такой номер 〃,что все деревья с т вершинами и номером, большим 九,обладают одним из следующих свойств: 1) левое поддерево имеет номер больший, чем 丄;2) левое дерево имеет тот же номер, что и L, а правое дерево имеет номер, больший, чем R. Первые 10 деревьев, а также дерево с номером 20 изображены на рис. 3.34. Ваша задача — вывести двоичное дерево по его номеру.
Ввод:
Ввод состоит из множества тестов. Каждый тест — одно целое число N (1 <N< < 500 000 000). Значение N я 0 означает конец ввода. Вам не нужно выводить пустое дерево.
Вывод'.
Для каждого теста выведите одну строку, содержащую дерево, соответствующее введенному номеру. Для вывода дерева используйте следующую схему:
дерево без потомков обозначается как X;
О 1XО 1X2 3 4 5 6 7 8
О 1
X
О 1
X
X X X X X X X
\ / \ \/\ / /
XX X XX XX X
\| \/XX X X
Рис. 3.34. Первые 10 двоичных деревьев и двоичное дерево с номером 20
дерево с левыми и правыми поддеревьями 丄 и R нужно выводить как (L')X(R'), где 2/ н R' — представления деревьев 丄 и R;
если L пусто, выводите просто X(R'); □ если R пусто, выводите просто (£*)Х.
Пример ввода: 1 20 31117532 О
Пример вывода: х ((Х)Х(Х))Х
(Х(Х(((Х(Х))Х(Х))Х(Х))))Х(((Х((Х)Х((Х)Х)))Х)Х)
Итак, среди деревьев с одинаковым количеством вершин меньший номер получает то дерево, у которого меньший номер имеет левое поддерево, а при одинаковых левых деревьях — то, у которого меньше номер правого поддерева.
На входе задается номер дерева, нам нужно вывести его представление в виде соответствующего скобочного выражения. Xуказывает на корень поддерева; (..)Х указывает на наличие левого поддерева; Х(...) — указывает на наличие правого поддерева. Например, для дерева с номером 20 (рис. 3.35) скобочная запись имеет вид: ((Х)Х(Х))Х
Рис. 3.35. Двоичное дерево с номером 20
Описание решения'.
Прежде всего мы можем подсчитать, сколько существует двоичных деревьев с заданным количеством вершин К.
Известно, что Ь[0] = Л[1] = 1 по определению. (Пустое дерево — одно, и дерево с одной вершиной тоже одно).
Для любого К>2
b[K] - b[0]*b[K-l] + b[l]*b[K-2]+...+ Ь[К-1]*Ь[к].
Действительно, дерево с К вершинами имеет левое и правое поддерево соответственно с 0 и К-1 вершинами, 1 и К-2,..., К-1 и 0.
Далее мы можем получить абсолютный номер последнего дерева с К вершинами:
a[k]:-b[O]+b[l]+...+b[k]
Теперь, получив на входе номер дерева N, мы можем с помощью a[i] узнать, сколько вершин А в этом дереве, исходя из выполнения соотношения a[K-l] < N<a[K]t а также узнать, какой номер этого дерева среди деревьев, имеющих ровно К вершин: N - 〃[К-1]. Далее можно узнать, сколько вершин в левом (丄)и правом (R) поддеревьях текущего дерева. £ выбирается как максимальное такое, что
a[k-l]+b[O]*b[K-l]+ b[l]*b[K-2]+...+ b[L]*b[k-l-L]<N
Тогда R = К 一 1 一 丄(одна вершина — в корне дерева). И, наконец, нужно вычислить какие абсолютные номера имеют левое (NL) и правое поддеревья (NR). Для этого сначала вычислим относительные номера левого и правого поддеревьев среди деревьев с соответствующим количеством вершин. Если в левом или правом поддереве 0 (1) вершин, то это поддерево имеет номер 0 (1). Противоположное дерево имеет номер
N-a[k-l]+b[0]*b[K-l]+b[l]*b[K-2]+...+b[L]*b[k-l-L]
Если же в левом и правом поддеревьях более одной вершины, то номера левого и правого поддеревьев находятся в результате исполнения следующего алгоритма:
NT - a[k-l] . NL - О Пока (NT + b[R])< N
NT - NT + b[R]
NL - NL + 1
NR - N - NT
To есть пока добавление к текущему номеру дерева (NT) количества всех деревьев с Л вершинами(В[Л]) не превышает номер дерева N, увеличиваем на 1 номер левого поддерева (NL). Когда добавить количество всех правых деревьев нельзя, поскольку будет превышен заданный номер дерева N, вычисляем номер правого поддерева (NR = N - NT).
В завершение к полученным относительным номерам деревьев (NL, NR) прибавим количества всех деревьев с £ - 1 и Л - 1 вершинами соответственно:
NL = NL + a[L-l]
NR = NR + a[R-l]
Далее работу по разложению дерева на поддеревья нужно рекурсивно продолжать с обоими поддеревьями до тех пор, пока не получим дерево с одной или нулем вершин. По выходу из рекурсии формируем строку ответа по правилам:
Если число вершин - 0 то S = 'пустая строка'
Если число вершин - 1 то S = X
Если SL (строка левого поддерева) не пуста то S - (SDX иначе S * X
Если SR (строка правого поддерева) не пуста то S = S + (SR)
Рассмотрим подробнее реализацию. Тело главной программы имеет вид: begin assignCinput.'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 — суммирует элементы В, строя массив a[j] (абсолютный номер последнего дерева с j вершинами);
BuildS — рекурсивная процедура, которая по заданному номеру Устроит символьное представление дерева 5.
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-l then begin S:-'X'. exit: end: FindCN.NL.NR): BuildS(NL.SL): BuildS(NR.SR):
if SL<>"
then S:='('+SL+')'+'X'
else S:・'X':
if SR<>" then S:=S+'('+SR+')':
end:
Процедура BuildS вызывает процедуру Find, которой передается абсолютный номер текущего дерева N, а она возвращает абсолютные номера левого и правого поддеревьев этого дерева: NL и NR.
procedure Find(N:longint; var NL.NR.longint):
begin
k:-Place(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:7:
if (L-0) or (L-l) then NL:-L;
if (R-0) or (R-l) 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:
Процедура Find реализует описанный выше алгоритм. Вначале вызывается функция Place, которая находит место Nмежду a[i] и, соответственно, количество вершин в текущем дереве. Затем операторы
NT:»a[k-l]; i:-0:
While (NT+b[i]*b[k-l-i]<N)
do begin inc(NT.b[ij*b[k-l-i]): inc(i): end;
L:-i;
R:-K-l-L:
находят количества вершин в левом (丄)и правом (R) поддеревьях. А операторы
NL:-0:
while (NT+b[R]<N) do
begin inc(NT.b[R]). inc(NL) end;
NR:-N-NT;
находят относительные номера левого и правого поддеревьев (NL, NR) среди деревьев с соответствующим количеством вершин для Общего случая (丄 <> 0,丄 <> 1, Л <> О, Л <> 1). В конце главы приведен полный текст решения задачи.
3.7. Битово-индексированные деревья
В данном разделе приводится условие задачи «Мобильные телефоны», а затем несколько попыток решить эту задачу. В завершение приводится авторское решение задачи, опирающееся на структуру данных «битово-индексированные деревья», и необходимая для понимания решения информация о самих битово- индексированных деревьях.
3-7.1. Задача «Мобильные телефоны»
Международная олимпиада по информатике, 2001
Мобильные телефоны
Предположим, что базовые станции для мобильных телефонов 4-го поколения, расположенные в районе Тампере, функционируют следующим образом: район поделен на квадраты. Квадраты образуют матрицу 5x5, в которой строки и столбцы нумеруются от 0 до 5 - 1. Каждый квадрат содержит базовую станцию. Количество активных мобильных телефонов внутри квадрата может изменяться, потому что телефоны перемещаются из квадрата в квадрат или потому, что телефоны включаются и выключаются. Время от времени каждая базовая станция сообщает изменения в своем количестве активных телефонов на главную базовую станцию, единственную для всех строк и столбцов матрицы.
Напишите программу, которая получает эти отчеты и отвечает на запросы о текущем общем количестве активных мобильных телефонов в любой области прямоугольной формы.
Ввод и вывод:
Ввод необходимо читать из стандартного ввода как целые числа, и ответы на запросы нужно выводить в стандартный вывод как целые числа. Каждый ввод задается на отдельной строке и содержит одну инструкцию и некоторое количество параметров (все — целые числа), в соответствии с табл. 3.3.
Таблица 3.3. Инструкции ввода
ИнструкцииПараметрыЗначения
ОSИнициализировать матрицу размером S х S так,
чтобы она содержала все нули. Эта инструкция дается только один раз и будет первой инструкцией
Таблица 3.3 (продолжение)
Инструкции
Параметры
Значения
1
X YA
Прибавить к количеству активных телефонов в квадрате (X, У). А может быть как положительным, так и отрицательным
2
LB RT
Выдать текущее количество активных телефонов в квадратах (X, У) из прямоугольной области L<X<R,B<Y<T
3
Завершает работу программы. Эта инструкция дается только один раз и будет последней инструкцией
Все значения всегда будут в указанных диапазонах, поэтому нет необходимости их проверять. В частности, если А отрицательное, можно полагать, что оно не установит количество мобильных телефонов в квадрате в отрицательное значение. Индексация начинается от О.То есть для таблицы размером 4x4 мы имеем 0 ^ X < 3, 0< У<3. Ваша программа не должна отвечать на инструкции, кроме инструкции 2. Если же инструкция — 2, то программа должна вывести в стандартный вывод ответ на запрос, как одно целое число в строке.
Инструкции по программированию'.
В примерах, приведенных далее, целое last — это последнее целое, которое вы прочитали из входной строки, a answer — целая переменная, которая содержит ваш ответ. Если ваша программа написана на Паскале, вы должны использовать следующий способ чтения из стандартного ввода и записи в стандартный вывод:
Read(last): . . Readln.
Writeln(answer):
Пример ввода
Пример вывода
Пояснение
04
Инициализировать таблицу 4x4
1123
Добавить 3 в позицию (1, 2)
20022
Запрос на сумму прямоугольника 0$Х$2, 0$Y$2
3
Ответ на запрос
1112
Добавить 2 в позицию (1, 1)
112-1
Добавить -1 в позицию (1,2)
2 1123
Запрос на сумму прямоугольника 1 $Хм2, 1 $Y$3
4
Ответ на запрос
3
Завершить работу программы
Ограничения'.
Размер таблицы 5x5
1 х 1<5х5<1024 х 1024
Значение ячейки V в любое время
04
Величина изменения А
-2 JA«2 J
Количество U инструкций на вводе
3WU4 60 002
Максимальное количество телефонов во всей таблице
М = 23°
Из 20 тестовых примеров — 16 таких, в которых таблица не превышает 512 х 512.
Описание решения'.
Первое решение — самое простое — реализовать непосредственно хранение данных в двумерном массиве и суммировать количества двойным циклом:
program ioOldltl:
const
MaxS - 100: var
SS:array [-1..maxS-1,-1..MaxS-1] of longint:
Cm.X.Y.A.B.T.L.R.i.j.S : integer;
Ans:longint:
procedure Init:
begin
readln(S):
for y:--l to S-l do
for x:--l to S-l 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 inc(Ans.SS[i.j]);
writeln(Ans):
end:
begin
assign(i nput.'i nput.txt'); reset(input):
assign(output.'output.txt*): rewrite(output);
Cm:T:
while (cm<>3) do
begin
read(Cm):
case Cm of
О : Init:
: Add:
: Result:
:: end; end:
end.
Понятно, что сложность получения ответов на запросы имеет порядок 5 х 5. На оригинальных тестах такое решение выдает правильные ответы только на первых пяти тестах, но не успевает по времени на шестом (размер массива 250 х 250).
Другой вариант решения — хранить для каждой клетки суммарное количество мобильных телефонов в текущей строке от первой колонки до текущей, а для выдачи ответов суммировать соответствующие разности. Такой алгоритм имеет сложность порядка 5. То есть при изменении количества телефонов в клетке (X, У) мы делаем одни и те же изменения во всех клетках от клетки (X, У) до клетки (5 - 1, У). В результате процедура Add будет выглядеть следующим образом:
procedure Add;
begin
readln(X.Y.A):
for j-X to S-l do inc(SS[Y,j],A): end:
При получении ответа накапливаем разности, соответствующие прямоугольной области запроса. Для чего процедура Result записывается следующим образом:
procedure Result;
begin
readln(L.B.R.T):
Ans -0;
for i:-B to T do inc(Ans.(SS[i,R]-SS[i,L-1])): writeln(Ans):
end;
Такое решение, имеющее сложность 5, выдает правильные ответы на первых 8 тестах, не успевая по времени начиная с теста 9 (размер массива 441 х 441).
Третье решение предлагает хранить дерево сумм в одномерном массиве SS (рис. 3.36):
55[0] — хранит сумму чисел во всем квадрате;
55(1] — хранит сумму чисел в первом квадранте;
55[2] — хранит сумму чисел во втором квадранте;
S5[3] — хранит сумму чисел в третьем квадранте;
55[4] — хранит сумму чисел в четвертом квадранте;
S5[5] — S5[8] — хранят суммы подквадрантов 1-го квадранта;
55(9] — 55[12] — хранят суммы подквадрантов 2-го квадранта;
Рис. 3.36. Порядок квадрантов для хранения сумм
55[13]~ 55(16] — хранят суммы подквадрантов 3-го квадранта;
55(17] — S5[20] — хранят суммы подквадрантов 3-го квадранта.
И так далее.
При изменении количества телефонов в клетке (X, У) мы проводим соответствующие изменения во всех квадрантах, содержащих данный квадрант. Очевидно, что их не более чем log 5. При получении запроса на подсчет числа мобильников на заданной прямоугольной территории, мы вычисляем ответ на вопрос как СУММУ количеств телефонов во всех непересекающихся квадрантах, участвующих в покрытии заданной территории. В конце главы приводится текст соответствующего решения. Это решение, к сожалению, не прошло все тесты. Полное решение автор книги придумать не смог, потому пришлось разбираться с авторским решением, которое вместе с другими материалами 101-2001 выложено по адресу ftp://ftp.cs.uta.fi/ pub/reports/pdf/A-2001 -7.pdf.
Структура данных BIT
Автор задачи предлагает использование такой структуры хранения данных, как Б-деревья (Binary Indexed Tree, далее мы будем использовать аббревиатуру BIT). Рассмотрим вначале упрощенный вариант задачи в случае размерности l.To есть, если требуется искать различные суммы подряд идущих элементов одномерного массива.
Вместо хранения самих элементов предлагается хранить предвычисленные суммы следующим образом.
Пусть m[i] — элементы массива, q[i] — предвычисленные суммы.
Таблица 3.4 показывает, сумму каких элементов массива m[i] будет хранить элемент q[i] (в примере размерность обоих массивов — от 1 до 16).
Факт вхождения элемента m[i] в сумму q[j] отображается символом X в элементе таблицы на пересечении строки i и столбца j.
Например, в таблице отображено, что элемент т[1 ] входит в суммы ^[1],^[2], ^[4], q[8] и ^[16], и что ^[12] — это сумма элементов ти[9],加[10], m[ll] и т[12].
В общем случае элемент q\i\ содержит сумму элементов m[i] из интервала [i-2 Y+ +1, i], где А — количество «хвостовых» нулей в двоичном представлении числа i. Например, вычислим интервал для вышеупомянутого ^[12].
Таблица 3.4. Элементы массива
q
1
1
1
1
1
1
1
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
m
1
X
X
X
X
X
2
X
X
X
X
3
X
X
X
X
4
X
X
X
5
X
X
X
X
6
X
X
X
7
X
X
X
8
X
X
9
X
X
X
X
10
X
X
X
11
X
X
X
12
X
X
13
X
X
X
14
X
X
15
X
X
16
X
Двоичное представление 12 — 1100. Количество «хвостовых» нулей равно 2. Следовательно, А = 2. Тогда левая граница интервала: 12 - 22 + 1 = 9.
Рассмотрим, как поддерживать массив q в корректном состоянии при модификации (увеличении/умепьшении на величину Л) элемента m[i]).
Нужно соответствующим образом изменить элементы массива q、которые имеют некоторые индексы j в интервале от :до 16. При этом индекс следующего элемента初 находится через индекс предыдущего элементаjp следующим образом: jn:=加 + 2八厶, где k — количество «хвостовых» нуле!! в двоичном разложении числа jp.
Пусть мы изменили элемент а[1]. Начинаем с» = 1, А = 0 (количество «хвостовых» нулей в 1). j〃 =jp + 2Ao = 1+1= 2. Пусть теперь /р = 2, тогда А = 1, a j九=4. Продолжая процесс, мы получим ряд элементов массива q、которые нужно модифицировать при изменении 加⑴:^[1J, g[2], ^[4], ^[8], ^[16]. Построенный таким образом массив q позволяет легко находить суммы элементов массива т с номерами от 1 до Р. Обозначим такую сумму 网Р). Тогда для нахождения суммы элементов массива т с номерами от£ до R достаточно найти 貝R)-尸(丄一1).
Уточним, как с помощью массива S находить суммы типа F(T). Для этого достаточно просуммировать элементы массива q, начиная от Г и уменьшая индекс на величину 2* до тех пор, пока i > 0. Здесь, как и везде ранее, А — количество «хвостовых» нулей в двоичном представлении текущего индекса.
Например, пусть мы хотим получить сумму элементов от 1 до 12. Значит, мы должны сложить элементы с индексами 12 и 8. Как это получается: первый индекс (12) имеет двоичное представление 1100. А = 2. 2* = 4. 12-4 = 8. Двоичное представление 8 — 1000. А = 3, 2厶=8. 8 — 8 = 0. Процесс закончен.
Теперь применим данный подход к исходной задаче. То есть в элементе двумерного массива SS(R, Г) мы будем хранить частичные суммы значений элементов исходного массива таким образом, чтобы функция Sum(R, 7) могла легко подсчитать значение суммы элементов исходного массива в прямоугольниках вида [1..Л,1..7]. Тогда сумма элементов в прямоугольнике [L.R, В..7] находится как: Ans:-Sum(R.T)-Sum(L-l.T)-Sum(R.B-l)+Sum(L-l.B-l)
Элемент двумерного массива S5[X, У] хранит сумму элементов прямоугольного подмассива, в котором X и У соответствуют описанным правилам изменения индексов для одномерного массива.
Рассмотрим конкретную реализацию этого подхода. Главыая программа не претерпела никаких изменений по сравнению со всеми предыдущими версиями.
begin
assign(input.'input.txt'): reset(input): assign(output.'output.txt'): rewriteCoutput);
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 i:=1 to MaxS do
for j:*l to MaxS do SS[i.j]:-0;
S2:-NearestP2(S): for i:-l to S2 do begin k:-l; j:=i; while (j and 1)-0 do begin j:-j shr 1; k:-k shl 1; end: ad[i]:-k; end:
end:
Очевидно, обнуляется двумерный массив 55. Кроме того, величина 52 получает значение «ближайшая большая или равная по отношению к 5 степень числа 2».
И далее для i от 1 до S2 заранее заполняем массив ad[i] константами перехода от предыдущего индекса к следующему (ad[i] — количество «хвостовых» нулей в двоичном представлении числа i).
Функция NearestP2 вычисляет 52, используя оператор выбора:
function NearestP2(s:integer):integer:
var Temp : integer; begin
case S of
1
Temp:-1:
2..4
Temp:=4:
5..8
Temp:»8;
9..16
Temp:-16;
17..32
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, У]:
procedure Add;
begin
readln(X.Y.A):
while i<- 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:
Здесь выполняется именно то, о чем говорилось выше, — инкрементируется некоторое количество элементов массива S5[i, j]. При этом индексы i и j выбираются исходя из описанных выше правил.
Процедура вывода ответа Result реализует описанный подход:
procedure Result:
begin
readln(L.B.R.T):
1nc(L): inc(B): inc(R): 1nc(T): Inc(N):
Ans:-Sum(R.T)-Sum(L-l.T)-Sum(R.B-l)+Sum(L-l.B-l):
writeln(Ans):
end:
Вызывая функцию S〃m:
funct1on Sum(R.T:integer):1ong1nt:
var
Ans: longlnt;
begin
1:-R: Ans:-0:
while 1>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 вычисляет сумму элементов массива S5[i,力,индексы которых подчиняются описанным выше закономерностям. Полный текст решения приводится в конце главы.
Задачи для самостоятельного решения
Ничто так не проверяет приобретенные навыки, как выполнение заданий для самостоятельного решения. Здесь приводятся четыре таких задачи: <Knockout Tournament», <Split Windows», «Haffman Trees», <Pre-Post-erous!». Свои решения, как обычно, можно проверить на сайте http://dl.gsu.unibel.by в разделе «Тре・ нировочный курс АСМ>.
Задача «Knockout Tournament»
North America Programming Contest, 2002
Knockout Tournament (tour.in / tour.out /5 с)
В турнире с выбыванием участвуют 2" игроков. Каждый проигравший выбывает из турнира. Победители играют попарно, новые победители проходят в следующий круг и так до тех пор, пока не останется только один победитель. Если мы перенумеруем игроков от 1 до 2я, и в первом раунде играют игроки 2k - 1 против 2А, где Л - 1,2,..., 2я'1, то весь турнир можно представить
8/ \58/\ /\8/ \58/\ /\полным двоичным деревом. Победитель каждой пары указывается во внутренних вершинах. На рис. 3.37 приведен пример турнира при 〃 = 3.
8
/ \
58
/\ /\
8
/ \
58
/\ /\
1z\13
/\ /\123 4
Рис. 3.37. Пример дерева для задачи «Knockout Tournament»
После турнира некоторые репортеры спорили относительно рейтинга игро・ ков по результатам этого турнира. Предполагается, что если игрок А выиграл у игрока В, который в свою очередь выиграл у игрока С, то это означает, что игрок А сильнее игрока С (победа транзитивна). Нет сомнений, кто самый лучший игрок. Однако для всех остальных игроков все не так однозначно. Например, игрок 2, проигравший победителю, может быть вторым по силе игроком (рейтинг 2), а может быть и самым плохим игроком турнира (рейтинг 8). Игрок 5 может быть 3-м по силе игроком (поскольку точно нам известно, что он слабее 1-го и 8-го), но не может иметь рейтинг меньше 7 (поскольку он обыграл одного игрока в первом раунде).
Вам необходимо определить максимальный и минимальный возможные рейтинги игроков по заданным результатам турнира.
Ввод:
В файле содержится несколько тестов. Каждый тест состоит из трех строк. Первая строка содержит положительное число п(п< 8), указывающее, что в турнире приняли участие 2" игроков с номерами от 1 до 2" (разбитые вначале на пары, как описано ранее), л = 0 означает конец ввода. Следующая строка содержит результаты каждого раунда турнира, выписанные слева направо, начиная с первого раунда. Например, представленный турнир задается так:
1 3 5 8 1 8 1
Последняя строка содержит положительное целое число т, за которым следуют числа А] ,...,£ — номера игроков, для которых нужно определить диапазон рейтинга. Вывод'.
Для каждого k. выведите одну строку в виде:
Player ki can be ranked as high as h or as low as 1.
в которую нужно подставить подходящие числа. Строки должны появляться в том же самом порядке, в котором k. задаются на входе. Результаты для разных тестов должны разделяться пустой строкой.
Пример ввода:
3
1358181
2 2 5
4
2 3 6 7 9 11 14 15 3 6 9 15 6 9 6
4 1 15 7 6
О
Пример вывода:
Player 2 can be ranked as high as 2 or as low as 8.
Player 5 can be ranked as high as 3 or as low as 7.
Player 1 can be ranked as high as 4 or as low as 16.
Player 15 can be ranked as high as 3 or as low as 13.
Player 7 can be ranked as high as 2 or as low as 15.
Player 6 can be ranked as high as 1 or as low as 1.
Задача «Split Windows»
USA Programming Contest, 2001
Split Windows (split.in / split.out)
Компания Dotty Software разрабатывает программы, обеспечивающие ввод на недорогой текстовый терминал. Одно из приложений для этой системы имеет вид главного окна, которое может разделяться на подокна.
Ваша задача: по заданному описанию расположения окон нарисовать минимальную решетку окон, соответствующую заданному описанию.
В данной задаче мы сосредоточимся на границах окон. Поэтому содержание всех символов внутри окон — пробелы.
Каждое окно, которое не имеет подокон, получает метку — одну прописную букву английского алфавита. Все метки различны. Границы окон должны отрисовываться символами, выбираемыми по следующим правилам: Метка ставится в левый верхний угол каждого неразделенного окна. Символ '' ставится во все углы окон, где нет метки. Символы '—' используются для обозначения верхних и нижних границ окон, не являющихся углами. Символ Т используется для обозначения вертикальных границ окон, не являющихся углами.
Например, экран на рис. 3.38 можно получить таким образом: вначале было окно с меткой М, затем оно было разделено вертикально, на левую и правую половины, и правая половина получила метку R. Затем левая половина была разделена на верхнюю и нижнюю части и нижняя часть получила метку С.
М —R—*
Для каждой последовательности разделений существует двоичное дерево символов, которое описывает эту последовательность.
Ниже приводятся общие правила для построения разбиений и соответствующего двоичного дерева.
Окно может быть неразделенным прямоугольником. Такое окно имеет заглавную английскую букву в качестве метки. Дерево для такого окна содержит только эту метку.
Окно может быть разделено на верхнее и нижнее подокно или на левое и правое подокно. Соответствующие деревья в качестве корня граничный символ разделения: *-* или Т. Корень имеет левое и правое поддеревья соответствующие верхнему/нижнему или левому/правому подокнам. Такие деревья могут быть представлены с помощью прямого (нисходящего) обхода. Каждое неразделенное окно должно иметь как минимум один символ внутри. Следовательно, с каждым деревом разделений может быть ассоциирован минимальный размер внешнего окна. Очевидно, что даже при минимальном размере внешнего окна, не все неразделенные окна содержат только один символ.
Ввод:
Первая строка ввода содержит одно целое число - общее количество заданных обходов, описывающих вложенные оконные структуры. За этой строкой следуют строки, каждая из которых описывает один нисходящий обход и содержит символы Т,и от 1 до 26 заглавных английских букв.
Вывод:
Для каждого нисходящего обхода выведите номер обхода, а за ним минимальное окно, которое может представлять заданный обход. Гарантируется, что общее количество строк и столбцов вывода не будет превышать 53.
Пример ввода,
3
I-MCR
|-|-ABC-D|E-FG-P-|Q|RST
-|-|ABC|D-E|FG|P|-Q-RST
Рис. 3.39. Пример отображения окон для задачи «Split Windows»
Пример вывода'.
1 — изображение, показанное на рис. 3.38;
2 — изображение, показанное на рис. 3.39;
3 — изображёние, показанное на рис. 3.40.
A - b —D — e
Рис. 3.40. Пример отображения окон для задачи «Split Windows»
Задача «Huffman Trees»
North Western European, Programming Contest 2002
Huffman Trees (trees.in / trees.out /25 c)
Относительно простой метод сжатия данных работает созданием так называемого дерева Хаффмана для файла и использованием его для компресси- ,и и декомпрессии данных, которые содержит это файл. В большинстве случаев используется двоичное дерево Хаффмана (то есть каждая вершина дерева либо является листом, либо имеет ровно двух потомков). В общем случае дерево Хаффмана может быть с любым количеством поддеревьев. Дерево Хаффмана для файла, содержащего Z различных символов, имеет Z листьев. Путь от корня к листу, который представляет этот символ, определяет кодирование этого символа. Каждый шаг к листу определяет кодирующий символ (который может быть 0,1,..., N - 1 в случае N-арного дерева Хаффмана).
Размещая наиболее часто используемые символы ближе к корню, а менее часто используемые символы дальше от него, мы достигаем желаемого уровня сжатия.
Строго говоря, такое дерево будет деревом Хаффмана только если в результате такого кодирования мы используем минимальное количество Марных символов для кодирования всего файла.
В данной задаче мы рассмотрим только деревья, каждая вершина которых или внутренняя, или лист, кодирующий символ. Рисунок 3.41 показывает пример троичного дерева Хаффмана. Символы аие кодируются с использованием только одного троичного символа. Менее часто встречающиеся символы
s и р кодируются двумя троичными символами. Наконец, наиболее редко встречающиеся символы х, q、у кодируются тремя троичными символами каждый.
s | р00 | 02
*
X q У010 011 012
Рис. 3.41. Пример троичного дерева
Конечно, если мы хотим декодировать поток Парных символов, нам необходимо знать дерево, которое использовалось для кодирования исходного текста. Это может быть сделано несколькими способами. В данной задаче потоку закодированных символов предшествует заголовок — поток кодирования Z символов, встреченных в файле, в алфавитном порядке.
Задание'.
Вам дано количество исходных символов Z, значение N, обозначающее «-арность» дерева Хаффмана, и заголовок. Постройте отображение исходных символов в закодированные символы.
Ввод:
Ввод начинается с целого числа Т, обозначающего количество тестов. Далее для каждого из тестов следуют три строки.
Количество Zразличных символов в файле (2 <Z< 20).
Число N, обозначающее «арность» дерева Хаффмана.
Строка, представя я ющая заголовок полученного сообщения; каждый символ будет цифрой в диапазоне от 0 до (N-1). Эта строка будет содержать не более 200 символов.
Теоретически возможны заголовки с неоднозначной интерпретацией (например, заголовок 010011101100 при Z = 5 и N я 2). Однако вам гарантируется, что все представленные в тесте заголовки имеют однозначное решение.
Вывод:
Для каждого из Г тестов выведите Z строк, дающие представление кодирования каждого из Z символов в порядке возрастания. Используйте формат исходный т кодирование,
где «исходный» - число в интервале от 0 до Z-1, а кодирование — строка цифр, кодирующих этот символ, каждая цифра неотрицательна и меньше N.
Пример ввода: 3 3 2 10011 4 2 ооошою 19 10
01234678950515253545556575859
Пример вывода: 0 т 10 1 т 0 2 Tli 0 т 00 1 т 011 2 т 1 3 т 010 0 т 0 1 т 1 2 T 2 3 т 3 4 -> 4 5 т 6 6 т 7 7 т 8 8 T 9 9 -> 50 10 т 51 11 т 52 12 т 53 13 т 54 14 т 55 15 т 56 16 т 57 17 т 58 18 т 59
Задача «Pre-Post-erous!»
East Central North America Programming Contest, 2002 Pre-Post-erous! (post.in / post.out /5 с)
Известны стандартные варианты обхода двоичных деревьев (нисходящий, фланговый, восходящий). Часто встает задача найти нисходящий обход двоичного дерева, если заданы его фланговый и восходящий обходы.
Или вы можете найти восходящий обход двоичного дерева, если известны его фланговый и нисходящий обходы.
Однако в общем случае невозможно определить фланговый порядок обхода двоичного дерева, если даны его нисходящий и восходящий обходы. Для примера рассмотрим четыре двоичных дерева, показанные на рис. 3.42.
Рис. 3.42. Примеры двоичных деревьев для задачи «Pre-Post-erous»
Все эти деревья имеют одинаковые нисходящий и восходящий обходы. Однако, очевидно, они различны и имеют различные фланговые обходы. Такой феномен присущ не только бинарным, но и Марным деревьям.
Ввод:
Ввод содержит множество тестов. Каждый тест состоит из одной строки вида:
m si s2
указывающей, что мы рассматриваем m-арные деревья, si — нисходящий обход дерева, s2 — восходящий обход этого же дерева. Все обходы задаются маленькими латинскими буквами. Для всех тестов 1 < m < 20, длины si и s2 — от 1 до 26. Если длина si равна А (длина s2, очевидно, тоже равна А), то первые А букв алфавита используются в строке si (и s2). Строка, содержащая число 0, означает конец файла.
Вывод:
Для каждого теста Вы должны вывести строку, содержащую количество возможных деревьев, которые имеют такие же нисходящий и восходящий обходы. Гарантируется, что все выводимые значения не превысят 32-битного знакового числа. Также гарантируется существование хотя бы одного дерева с заданными нисходящим и восходящим обходами.
Пример ввода:
Пример вывода:
2 abc cba
2 abc Ьса
10 abc Ьса
13 abejkcfghid jkebfghicda
4
1
45
207352860
Решения задач
Листинг 3.1. Текст программы к задаче «Is it a tree?»
program nc97b:
Type
Tint - Shortlnt:
const
MaxN-100;
GREAT-120:
var
G : array [1..MaxN.l..MaxN] of Tint:
V : array [1..MaxN] of Tint:
N.x,y.i.j,Nodes.Edges.MaxNode : Tint:
procedure MaxCvar Nodes.x.у:Tint);
begin
if x>Nodes then Nodes:-x;
if y>Nodes then Nodes:-y;
end:
procedure InputData: var
.j. Holes : Tint;
begin
MaxNode:-0;
for i:-l to MaxN do
for j:-l to MaxN do G[i,j]:-GREAT;
for 1:-l to MaxN do V[i]:-0:
read(x.y); Nodes:-0: Edges:-0; if x--l then begin
close(input); close(output);
halt(O);
end;
while (xoO) 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
Листинг 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.i]:-l: end:
for k:-l to MaxNode do
for i:-l to MaxNode do
for j -1 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]<iREAT
then begin
Connected:-false; exit:
end:
end:
begin
assigndnput.'b.in'); reset(input):
assign(output.'b.out'); rewrite(output);
i:-0; x:-l:
while x<>0 do
begin
inc(i);
InputData:
if (Nodes<>0) and ((Edges<>Nodes-l) or (not Connected)) then writein ('Case ',i.' is not a tree.') else writein ('Case '.i.' is a tree ') end: end.
Листинг 3.2. Текст программы к задаче «Strategic game» program seeOOa: const
MaxN-1500:
MaxE-20:
var
G : array [0..MaxN-1.1 .MaxE] of integer;
Power : array [0..MaxN-1] 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-l do Power[i]:-0: for i:-0 to N-l do begin readln(s):
k:=pos(':'.s): т:-ро5(')'.s): vaKcopy(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[l]e, ') or (s[l]=Tab) do delete (s.1.1); k:-pos(Tab.s);
if k-0 then k:-pos(' '.s): vaKcopy(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 MaxPowerCvar Node:integer):1nteger;
Листинг 3.2 (продолжение)
begin m:-Power[N-l]: Node:=NT: for i:»N-2 downto 0 do if Power[i]>(n
then begin m:-Power[i]: Node:-i; end;
MaxPower:-m; end;
procedure Annigi1ate(Node:integer); begin
j:T: 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:
Del eted:-0:
while MaxPower(Node)<>0 do begin
inc(Deleted);
Annigilate(Node); end; writein(Del eted): end:
closednput): close(output); end.
Листинг 3.3. Текст программы к задаче «Оппозиция» {$г+} program ru01t2; const
MaxN-10000;
MaxE-1000;
var
G: array [1..MaxN.l..MaxE] of integer:
Power: array [1..MaxN] of integer;
DeletedNodes : array [1..MaxN] of integer; Deleted. Node. N, i. j. m. К : integer:
procedure InputData: var
i.b.e : integer: begin
assigndnput, '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*); rewriteCoutput): writeln(Deleted);
if Deleted<>0 then begin for i:-l to Del eted-1 do write(DeletedNodes[i],''): writeln(DeletedNodes[Deleted]); end:
close(output); end;
function MaxPowerCvar Node.M:integer)integer: begin
m:-Power[N]; Node:-N;
for i:»N-1 downto 1 do if Power[i]>m
then begin m:-Power[i]; Node:-i: end; MaxPower:-m;
end:
Листинг 3.3 (продолжение)
procedure Annigi1ateCNode:intедег):
begin
j:-l;
for i:-l to Poweг[Node] do begin
while Power[G[Node.j]]-0 do inc(j):
dec(Power[G[Node.j]]):
inc(j) end:
Power[Node]:-0;
DeletedNodes[Del eted]:-Node; end;
begin
InputData;
Del eted:*0:
while (MaxPoweг(Node.m)>-K-1) and (m<>0) do
begin
inc(Deleted):
Annigi1ateCNode):
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
[1..MaxN] of string[MaxA];
Power
:array
[1..MaxN] of integer:
CA
:array
[1..MaxN] of integer;
Erdos
• array
[1..MaxN] of integer;
i.T.P.N.NT.a.c.ii,jj : integer:
s
:string;
procedure AddAuthorlfNewCs:string); var
i : integer:
begin
while (i<-a) and (Authors[i]<>s) do inc(i):
if 1>a
then begin inc(a); Authors[a]:=s: end:
inc(c);
CA[c]:-i:
end:
procedure GetPaper:
var
. j.k.q.b.e : integer;
begin
readln(s):
i :-l; q:=0; b:T: c:=0;
while (s[i]<>':') do
begin
if s[i] - '.' then inc(q):
if q-2 then
begin
AddAuthorlfNewCcopy(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<ePower[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
.j.BegQ,EndQ : longmt:
Листинг 3.4 (продолжение)
Q: array [1..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 GetAuthorPri ntResult: 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):
assign(output.'output.txt'): rewrite(output); readln(NT): for t:-l to NT do
begin
writelnC'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.
Листинг 3.5. Текст программы к задаче «Closest Common Ancestor»
program seeOOa:
const
MaxN-802;
MaxE-20;
var
G
:array [1..MaxN.l..MaxE] of integer:
Power
:array [1..MaxN] of integer;
Ax.Ду.A {
:array [1..MaxN] of integer;
В
:array [1..MaxN.l..MaxE] of integer;
kB
}
:array [1..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): mi-posO' .s): vaKcopy(s.l.k-l).b.j): val(copy(s.k+2.m-k-2).p.j): delete(s.l.m+l): s:-s+* ':
Листинг 3.5 (продолжение)
for о:-1 to р do begin
while (s[l]-' ') or (s[l]-Tab) do delete (s.1.1): k:-pos(Tab.s):
if k-0 then k:-pos(* '.s): vaKcopy(s.l.k-l).e.j): inc(Power[e]): g[e.Power[e]]:-b: delete(s.l.k):
end; end;
end;
procedure FindCCACx.у integer): var
i : integer; begin
for i:-l to N do begin
Ax[i]:-0: Ay[i]:-0: end:
Ax[x]:-1: Ay[y]:-1:
while (Power[x]-1) and (Ax[g[x.l]]<>l) do
begin x:-g[x.l]; Ax[x]:-1; end:
while (Power[y]-1) and (Ay[g[y.l]]<>l) do begin y:-g[y.l]: Ay[y]:T: end:
i :-l;
while (Ax[i]ol) or (Ay[i]<>l) 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:=pos(')'.s): val(copy(s,2,k-2).x.j);
val(copy(s.k+l.m-k-l).y,j):
FindCCA(x.y):
end:
end:
procedure OutputResults:
begin
for i:-l to N do
if a[i]<>0 then writeln (i.':',A[i]):
end:
begin
assigndnput. '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.
Листинг 3.6. Текст программы к задаче «Unscrambling Images*
program pnw99d:
var
TI. SI. RI. RR:
array [1..512] of integer;
t.NT.N.M.k.i.S.j.S1P.SiC.N2 :
integer:
G:
array[0..16] of integer:
procedure FillSubTree:
{Обход дерева квадратное}
var
i. j : integer:
begin
i:-S1P;
while 4*1+1 <- 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:
Листинг 3.6 (продолжение) procedure InputData; var x.y : 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]:-0; RI[i]:-0 end: N2:-N div 2: for i:-l to M do begin readln(x.y); TI[y+G[N2]+l]:-x;{Кодирование эталонного образа}
end:
readln(S):{количество листьев в дереве зашифрованного
образа}
if N-1 then begin readln(SiP.SiC); writeln ('Case ' ,t): writein: writein (SiC:4); writeln; exit:
end:
{Читаем зашифрованный образ}{Выставляем введенный цвет листу} {Закрашиваем все поддерево}{Читаем зашифрованный образ}{Выставляем введенный цвет листу} {Закрашиваем все поддерево}for 1:-l to S do begin
{Читаем зашифрованный образ}
{Выставляем введенный цвет листу} {Закрашиваем все поддерево}
{Читаем зашифрованный образ}
{Выставляем введенный цвет листу} {Закрашиваем все поддерево}
readln(SiP.SiC):
if SiP>G[N2]
then RI[S1P]:-SiC
else FillSubTree: end:
end:
procedure OutRI: var j.c.ci,cj : integer; begin if n-1 then exit; writeln ('Case '.t): writeln: for i:-G[N2]+l to G[N] do RR[i]:-RI[TI[i]]: {Расшифровываем образ} for i:-l to n do{Выводим его}
begin
for j:-l to n do write (RR[G[N2]+(i-l)*n+j]:4); writeln; end:
writeln; end:
begin
assigndnput.'D.txf); reset(input);
readln(NT):
{Количество тестов}
for t:-l to NT do
begin
InputData:
{Формирование образа}
OutRI:
{Раскодирование и вывод}
end:
closed nput):
end.
Листинг 3.7. Текст программы к задаче «BSP Trees*
program ecnaOOg: const
alt : string - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
NS - 20:
NT - 2049:
type
string20 - string[NS]:
var
Т
:array [1..NT] of string20;
Pof2
:array [1..12] of longint:
А.В.С
:array [1..10] of real:
х.у
:array [1..20] of real:
i .j.NP.NL.xLyl.x2.y2.m.k : longint;
procedure InputData;
begin
read(NP):
for i--1 to NP do readln(m.x[i].y[i]):
read (ND;
for i:-l to NL do
begin
readln(xl.yl.x2,y2);
A[i]:-y2-yl:
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;
procedure WriteTreeCj:integer):
Листинг 3.7 (продолжение)
begin
if (j>2048) then exit:
if (j<-1024) and (T[2*j]o") then WriteTree(2*j):
if (j<-1024) and (T[2*j+l]o'') then WriteTree(2*j+l):
if (j>1024) and (T[j]o") or (T[2*j]-") and (T[2*j+1]-") then write(T[j]); end;
procedure SplitCi integer; var S.Sl.S2:string20): begin
for k:-l to length(S) do begin
m:»ord(S[k])-ord('A')+1:
if y[m]>(-(x[m]*a[i]+c[i])/b[i]) then Sl:-Sl+S[k] else S2:-S2+S[k];
end: end:
begin
assigndnput. 'input.txt'); reset(input);
assignCoutput.'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.
Листинг 3.8. Текст программы к задаче «Дерево*
{$R+.M 65000.0.8192}
program byO2d2t3: const
MaxK - 300;
var
a: array [0..301] of longint:
i .j.K.N.s.g : longint:
fm: array [0..MaxK+1.0..MaxK+1] of longint;
procedure SortА: var
.j.s.t : longint: begin
for j:-l to K-l do begin
s:-a[j]: t:-j:
for i:-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 : 1ongint: begin
if fm[i.j]<0 then begin if i>j then Fm[i.j]:-0 else begin fs:-maxlongint: 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): assignCoutput.'output.txt'); rewriteCoutput): read(N.K):
for i:-l to к do read(a[i]);
SortA: A[0]:-0; A[K+1]:-N+l;
Листинг 3.8 (продолжение)
for i:-0 to К+1 do
for j:-0 to K+l do FM[i,j]:--l:
writeln(Fd.k)):
closednput); close(output); end.
Листинг 3.9. Текст программы к задаче «Parliament*
{$R+.M 65000.0.8192}
program nee4c01d: const
MaxN-3000;
MaxD-25000:
var
a:array[1..MaxN]ofword:
d:array[1..MaxD]ofword;
i.j.K.N.s.g : integer;
procedure AddtoTree(x:word):
begin
j:-l:
while d[j]<>0 do
if x>d[j] then j:-2*j+l else j:-2*j;
d[j]:-x;
end;
procedure WriteTreeCj:word);
begin
if d[2*j+l]<>0 then WriteTree(2*j+l):
if d[2*j ]<>0 then WriteTree(2*j ):
if (d[2*j]-0) and (d[2*j+l]-0) then begin writeln(d[j]): d[j]:-0 end
end:
begin
assignCinput.'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.
Листинг 3.10. Текст программы к задаче «Parliament»
{$R+.M 65000,0.8192}
program nee4c01d:
const
MaxN - 3000:
var
a: array [1..MaxN] of word;
Right, Left: array [1..MaxN] of word:
i.j.K.N.s.g. Tek. Root : integer:
procedure AddtoTreed :word):
begin
Tek:-Root:
while ((a[i]>a[Tek]) and (Right[Tek]<>0)) 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]-0)
then Right[Tek]:-i
else Left [Tek]:-i;
end;
procedure WriteTreeCj.pred.Dir:word);
begin
if Right[j]<>0 then WriteTree(Right[j].j.2):
if Left [j]o0 then WriteTreeCLeft [j],j.l);
writeln(a[j]);
if Dir-2
then Right[pred]:-0
else Left[pred]:-0:
end:
begin
assign(i nput.'input.txt'); reset(input):
assign(output.'output.txt'); rewriteCoutput):
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-1 downto 1 do AddToTreed):
WriteTree(Root.Root.2);
close(input); close(output): end.
Листинг 3.11. Текст программы к задаче «Falling Leaves»
{$R+.M 65000.0.8192}
program mcOOd;
const
MaxN - 3000;
MaxD - 25000:
var
a. s:string:
d:array [1..MaxD]ofchar:
.j.N.Tek.Root :integer:
Right.Left:array [1..MaxN]ofword;
procedure AddtoTreed :word):
begin
Tek:-Root:
while ((a[i]>a[TekJ) and (Right[Tek]<>0)) 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 WriteTreeCj:integer):
begin
write(a[j]);
if Left [j]<>0 then WriteTreeCLeft [j]):
if Right[j]<>0 then WriteTree(Right[j]):
end:
begin
assign(input.*1 eaves.in'); reset(input):
assign(output.'leaves.out'); rewriteCoutput):
readln(s):
while not EOF(input) do
begin
a:-'';
while (so'*') and (so'$')do
begin a:-a+s: readln(s) end:
N:-length(a):
for 1:-1 to N do
begin Left[i]:-0; Right[i]:-0: end;
Root:-N;
for 1:-N-l downto 1 do AddToTree(l):
WriteTree(Root): writeln;
readln(s):
end;
close(input): close(output): end.
Листинг 3.12. Текст программы к задаче «Кодирование» {$R+} program gg02t3: const
MaxK - 1000:
var
k.a.b
:array [1..MaxK] of longint:
bits
:array [0..MaxK] of string[20]:
sk
:array [1..MaxK] of char;
Free
:array [1..MaxK] of boolean:
res
:array [0..255] of string[20];
kj.i,j,m.kkl.kk2 : longint:
procedure InputData:
var
c : char:
s : array [0..255] of longint:
begin
assigndnput. 'input.txt'): reset (input):
for 1:-0 to 255 do s[1]:-0:
while not EOF(input) do
begin
read(c);
if (c<>#10) and (c<>#13)
then inc(s[ord(c)]):
end:
j:-0:
for 1:-0 to 255 do
if s[i]<>0
then begin inc(j): k[j]:-s[i]: sk[j]:-chr(i) end: kj:-J:
Листинг 3.12 (продолжение) close(input): end:
function M1nK:longint: var min : longint: begin i :-l; while (not Freed]) 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:-min; Free[m]:-false; end:
procedure SumUp: var
s2. ml. m2 : longint: begin if kj-1 then begin assign(output.'output txt'): rewrite(output); writeln(sk[l],* '.0): writeln(O): close(output): halt(O) end:
for i:-l to kj do Free[i]:-true: kkl:-kj; kk2:-kj; 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;
procedure BuildBits; begin bits[kk2]:='l': Free[kk2]:-false;
bits[a[kk2]]:-bits[kk2]+,0,: bits[b[kk2]]:・bits[kk2]+'l';
i:-MinK;
bits[m]:='O': Free[m]:-true: bits[a[m]]:=bits[m]+'O': bits[b[m]]:=bits[m]+'1';
for i:- kk2-l downto 1 do
if not Free[i]
then
begin
bits[a[i]]:=bits⑴+'0'; bits[b ⑴]「bits ⑴+T: end;
end;
procedure OutputData;
var
str : string:
begin
assi gn(output.'output.txt'): rewrite(output):
for i:- 1 to kj do writeln(sk[i],' '.bitsEi]): for i:-l to kj do res[ord(sk[i])]:-bits[i]: assignCinput.'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 : array [1..MaxK] of longint:
Листинг 3.13 (продолжение)
bits:array[0..МахК]ofstring[20];
sk:array[1..MaxK]ofchar;
Free:array[1..MaxK]ofboolean:
res:array[0..255]ofstring[20];
kj.i.j.m,kkl,kk2.n : longint:
str : string;
procedure InputData:
var c : 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:-min: Free[m]:-false;
end:
procedure SumUp:
var
s2. ml. m2 : longint:
begin
If kj-1
then begin
writeln(8*length(str).' '.length(str).' '.8.0:0:1): exit:
end;
for i:-l to kj do
begin Free[i]:-true: a[i]:-0: b[i]:-0; end:
kkl:-kj: kk2:-kj;
while kkl>2 do
begin
sl:-MinK; ml:-m;
s2:-M1nK: m2:-m;
inc(kk2):
k[kk2]:-sl+s2; a[kk2]:-ml: b[kk2]:-m2;
Free[kk2]:-true:
dec(kkl);
end:
end:
procedure BuildBits:
begin
bits[kk2]:-'l'; Free[kk2]:-false:
bits[a[kk2]]:・bits[kk2]+'O';
bits[b[kk2]]:-bits[kk2]+'r:
i:-MinK;
bits[m]:・'O': Free[m]:-true;
bits[d[m]]:・bits[m]+'O':
bi ts[b[m]]:"bi ts[m]+'Г:
for i:- kk2-l downto 1 do
if not Freed]
then
begin
bits[a[i]]:・bits[H+'O':
bitsCbCiJli-bltsCU+'r:
end:
end:
procedure OutputData:
var
b8. bh : longint:
begin
for i:-l to kj do res[ord(sk[i])]:-bits[i]:
Листинг 3.13 (продолжение)
b8:-8*length(str);
bh:-O:
for i:-l to length(stг) do begin
incCbh.length(res[ord(str[i])]));
end:
writeln(b8.' '.bh.' '.b8/bh:0:D end;
begin
assigndnput.'input.txt'): reset(input): a ss i gn(out put.'output.t xt'); rewrite(output): while not EOF(input) do begin
InputData:
SumUp:
if kjol then BuildBits:
if kj<>l then OutputData: end;
close(input): close(output): end.
Листинг 3.14. Текст программы к задаче «Nextree»
{$m 65000,0.0}
program us02dl: var
a.b.c : arraytl..1000] of longint;
L.i.j.k.p.q : integer:
Done : boolean:
procedure CheckTree(p.г:i nteger): begin
if Done then exit:
if Ca[p]>a[p+l]+2) and (г-Р>»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[i]:
if (not Done) and (a[p+l]>2)
then CheckTree(p+1.p+a[p+1]);
end
else
begin
if (not Done) and (a[p+l]>2) then CheckTree(p+1.p+a[p+1]);
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:
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:-faIse:
CheckTree(l.L):
if not Done
then begin writeln(O): exit end:
for i:-l to L-l do write(a[i].' '); writeln(a[L]);
closednput): 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[0]:-l: b[l]:-l;
for i:-2 to MaxB do
begin
sb:=0:
Листинг 3.15 (продолжение)
for j-0 to i-1 do inc(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:-Place(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) or (L-l) then NL:=L: if (R-0) or (R-l) 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]): mc(NR.a[R-l]): end:
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-l then begin S:='X': exit: end;
FindCN.NL.NR);
BuildS(NL.SL):
BuildS(NR.SR): if SLo"
then S:='('+SL+')'+'X'
else SAX':
if SR<>" then S:=S+'('+SR+')': end:
begin
assigndnput. '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.
Листинг 3.16. Текст программы к задаче «Мобильные телефоны»
{$М 65500.0.0} program ioOldltl; const
MaxS - 100:
MaxK - 1048576: var
SS: array [0..MaxK] of longint:
Cm.X.Y.A.B.T.L.R.i.j.S : integer:
Ans: longint:
procedure Init:
Листинг 3.16 (продолжение)
begin
readln(S):
for у:-0 to Махк do SS[y]:-O; end:
procedure Sum(L.B.R.T.X.Y,A,i:integer):
var XN. YN : integer; begin
if(x<L)or (x>R) or
(y<B)or (y>T) thenexit:
inc(ss[i],A);
if(x-L)and (x-R) and
(y-B)and (y-T) thenexit:
if
(x<-XN)
and
(y<-YN)
then
if
(x<-XN)
and
(y> YN)
then
if
(x> XN)
and
(y> YN)
then
if
(x> XN)
and
(y<-YN)
then
end:
i:-4*i: XN:-(R+L) div 2: YN:-(B+T) div 2:
Sum(L.B.XN.YN.X.Y.A.i+l):
Sum(L.YN+l.XN.T.X.Y.A.i+2):
Sum(XN+l.YN+l.R.T.X.Y.A.i+3):
Sum(XN+l.B.R.YN.X.Y.A.i+4);
procedure Add:
begin
readln(X.Y.A):
Sum(l.l.S.S.X+l.Y+l.A.O):
end:
funct i on mi n(a.b:i nt еде г):i ntеде г;
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) or
(T<B) or (B>T) then exit;
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
assigndnput.'input.txt'): reset(input):
assign(output.'output.txt'): rewrite(output):
Cm:T:
while (cm<>3) do
begin
read(Cm);
case Cm of
0 : Init:
: Add;
: Result;
::
end;
end: end.
Листинг 3.17. Текст программы к задаче «Мобильные телефоны»
program ioOldltl;
const
MaxS - 1024;
var
Листинг 3.17 (продолжение)
SS: array [1..MaxS.l..MaxS] of 1ongint;
Cm.X.Y.A.B.T.L.R.i.j.k.S :integer:
Ans:1 ongint:
s2:integer;
ad:array [1..MaxS] of integer:
function NearestP2(s:integer):integer:
var Temp : integer:
begin
case S of
Temp:-1;
.4: Temp:-4;
.8Temp:-8:
.16Temp:-16:
.32: Temp:-32:
64Temp:-64:
.128 : Temp:-128:
.256 . Temp--256;
.512 : Temp:-512:
.1024 : Temp:-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]-0:
S2:=NearestP2(S):
for i:-l to S2 do
begin k:-l: j:-i: while (j and 1)-0 do begin j:-j shr 1: k:-k shl 1; end; ad[i]:-k; end; end:
procedure Add:
begin
readln(X.Y.A):
1:-Х+1:
while i<= 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:
function Sum(R.T:integer):1ongint: var
Ans: 1ongint;
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;
deed .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);
Листинг 3.17 (продолжение)
Сш:-1:
while (cm<>3) do
begin
read(Cm);
case Cm of
0 : Init:
: Add;
: Result;
:: end; end: end.
Глава 4. Скрытые графы
Материал этой главы посвящен способам расширения области применения приведенных методов решения задач на графах. Существует множество классов задач, в которых для непосвященного совсем не очевидно, что эта задача может быть сведена к задаче на графе. Прежде всего, попытаемся дать некоторые общие рекомендации. Начнем с известного в математике понятия «отношение инцидентности». В геометрическом смысле две области (фигуры) на плоскости называются инцидентными (смежными) если они имеют общую границу. Для перехода к графам вершинам графа сопоставляют сами области, а ребрам графа — состояние инцидентности. То есть ребра проводятся между вершинами, соответствующими смежным областям. Такими областями могут быть треугольники (задача «Тетраэдр»), многоугольники (задача «Стены»), наборы смежных квадратных клеток (задача «Блокада»), Более того, смежность может быть нестандартной, например клетки шахматной доски могут быть смежными в том смысле, что из одной клетки можно попасть в другую ровно за один ход шахматного коня (задача «Мудрый правитель»), или смежными будут считаться валы, связанные ременной передачей (задача «Ременная передача»). Все эти задачи разобраны в первом разделе.
Во второй раздел включены задачи, в которых между негеометрическими объектами существуют какие-то отношения. Например, коээфициенты соотношения валют (задачи «Currency Exchange» и «Exchange Rates»), отношение порядка между величинами (задача «Sorting It All Out»), отношение ссылки из одного файла на другой (задача.<Проверка веб-страниц»), отношения вершины-буквы, ребра- слова, начинающиеся и заканчивающиеся на соответствующие буквы (задача «Play On Words»).
В третий раздел включены задачи, в которых исходными данными являются множества отрезков. Тогда в качестве вершин принимаются точки (как правило, концы отрезков), а в качестве ребер — отрезки между точками, принятыми в качестве вершин графа (задачи «Падение», «The Doors», «Борозды», «N-Credible Mazes»). Последний раздел включает задачи для самостоятельного решения. Все рассматриваемые задачи взяты с официальных соревнований по информатике и программированию.
Инцидентность областей
В данном разделе собраны задачи «Тетраэдр», «Стены», «Блокада», «Мудрый правитель» и «Ременная передача», в которых граф возникает вследствие применения абстракции «инцидентность (соседство) областей».
Задача «Тетраэдр»
Республиканская олимпиада по информатике, Минск, 1999
Тетраэдр (input.txt / output.txt)
Дано треугольное поле в виде равностороннего треугольника. Оно разбито на маленькие одинаковые равносторонние треугольники со сторонами, в М раз меньшими, чем сторона большого треугольника.
1 \2/31 \2/34 \5/ 6 \7/ 84 \5/ 6 \7/ 8Рис. 4.1. Общий вид треугольного поля для задачи «Тетраэдр»Рис. 4.1. Общий вид треугольного поля для задачи «Тетраэдр»
1 \2/3
1 \2/3
4 \5/ 6 \7/ 8
4 \5/ 6 \7/ 8
Рис. 4.1. Общий вид треугольного поля для задачи «Тетраэдр»
Рис. 4.1. Общий вид треугольного поля для задачи «Тетраэдр»
Маленькие треугольники пронумерованы подряд с верхнего ряда вниз по рядам, начиная с 0. Числами показаны номера треугольников, /-му треугольнику приписана пометка Рг.
Имеется также тетраэдр (правильная треугольная пирамида) с ребром, равным длине стороны маленького треугольника. Тетраэдр установлен на 5-м треугольнике. Все грани тетраэдра пронумерованы следующим образом:
) основание тетраэдра;
) правая грань тетраэдра, если смотреть сверху тетраэдра в направлении стороны АВ перпендикулярно ей;
) левая грань тетраэдра, если смотреть сверху тетраэдра в направлении сто роны АВ перпендикулярно ей;
) оставшаяся грань.
Например, при 5=2 жирной линией выделено нижнее ребро третьей грани, а при S = 3 жирной линией выделено нижнее ребро второй грани. J-я грань тетраэдра имеет пометку Rj.
Имеется возможность перекатывать тетраэдр через ребро, но при каждом перекатывании взимается штраф, равный квадрату разности между пометками совмещаемой грани тетраэдра и треугольника. Требуется перекатить тетраэдр с треугольника 5 на D с наименьшим суммарным штрафом (5 н Q).
Входные данные находятся в текстовом файле INPUT.TXT. Первая строка содержит целые числа S, Q и М (М < 90). Каждая из следующих М2 строк содержит пометку соответствующего треугольника. В последней строке записаны пометки граней тетраэдра. Пометки (как граней, так и треугольников) —целые неотрицательные числа, не превосходящие 300. Числа в одной строке разделены пробелами.
В выходной файл OUTPUT.TXT должно быть записано одно число — минимально возможный штраф.
Пример вводаПример вывода
оТз9446
4 3 8 100 7 3 2 49 9 7 50 100 8
Описание решения'.
Перейдем к графу следующим образом: вершина — маленький треугольник. Ребро —наличие возможности перекатить тетраэдр через ребро из одного треугольника в другой. Тогда, например, поле, изображенное на рис. 4.2, превратится в граф на рис. 4.3.
4 \5/б\7/ 84 \5/б\7/ 81\2/31\2/3Рис. 4.2. Пример треугольного поля для задачи «Тетраэдр»Рис. 4.2. Пример треугольного поля для задачи «Тетраэдр»
4 \5/б\7/ 8
4 \5/б\7/ 8
1\2/3
1\2/3
Рис. 4.2. Пример треугольного поля для задачи «Тетраэдр»
Рис. 4.2. Пример треугольного поля для задачи «Тетраэдр»
О
1 - 2 — 3
4 - 5 — 6 — 7—8
Рис. 4.3. Начальное положение развертки тетраэдра
На этом графе требуется найти путь минимальной стоимости из одной вершины в другую. Поскольку веса ребер в этом графе зависят от того, какой именно гранью тетраэдр придет на соответствующий треугольник, то воспользуемся поиском в ширину. Но прежде проясним процесс перекатывания тетраэдра. В соответствии с условиями задачи начальное положение развертки тетраэдра показано на рис. 4.4.
Рис. 4.4. Перекатывание тетраэдраРис. 4.4. Перекатывание тетраэдра
Рис. 4.4. Перекатывание тетраэдра
Рис. 4.4. Перекатывание тетраэдра
Обозначим его 1〃 (в основании грань номер 1, повернутая вверх). Очевидно, что всего существует 8 возможных различных состояний тетраэдра: 1〃,2〃,3〃,4% Id, 2d, 3d, 4d. На рис. 4.5 и 4.6 приведены соответствующие развертки:
Рис. 4.5. Развертки перекатывания тетраэдра вверх
Рис. 4.6. Развертки перекатывания тетраэдра вниз
Составим теперь таблицу, отображающую, в какое из состояний переходит тетраэдр при перекатывании его вниз, вверх, вправо, влево из текущего состояния.
Таблица 4.1 ・ Состояния тетраэдра
ВнизВверхВправоВлево
1и 1d 2и 2d Зи 3d 4и 4d
4dx 3d 2d х 4и 2и Зи 3d х 4d 1d х Зи 1и 4и 2dx 1d 4d х 2и 4и 1и 1d х 2d 3d х 1и Зи 2и
Для удобства использования этой информации введем следующее кодирование:
X
1и
и
2и
2d
3〃
3d
4〃
4d
0
1
2
3
4
5
6
7
8
Вниз
Вверх
Вправо
Влево
1
2
3
4
Получаем двумерный массив Г (8 строк, 4 столбца), который описывает все возможные перекатывания тетраэдра.
Т
1
2
3
4
1
8
0
6
4
2
0
7
3
5
3
6
0
8
2
4
0
5
1
7
5
4
0
2
8
6
0
3
7
1
7,
2
0
4
6
8
0
1
5
3
Основная идея решения заключается в следующем:
Заносим в очередь стартовую позицию S.
Пока очередь не пуста,
берем из очереди очередную позицию
ставим в очередь позиции, в которые тетраздр может попасть за одно перекатывание.
При установке в очередь очередной элемент включает номер вершины на графе, тип прихода (l〃..4d), текущий штраф после перехода в эту вершину. Элемент не нужно ставить в очередь, если текущий штраф больше ранее запомненного для этой вершины графа.
Перейдем к рассмотрению реализации. Тело главной программы выглядит следующим образом:
begin
InputData:
InitGraph;
QEnd:-O: QBegin:-l;
Put(S.TS.O):
while (QBegin<-QEnd) do begin
Get(V.TV.CV):
PutAll(V.TV.CV):
end;
OutResult:
end.
Здесь:
InputData — процедура, обеспечивающая ввод исходных данных;
InitGraph — процедура, обеспечивающая создание графа по исходным данным;
Put(VfTV,CV) — постановка в очередь одной вершины графа (позиции на треугольном поле);
V— номер вершины (позиции);
TV — тип вершины;
CV— текущее значение штрафа (при прохождении от начальной вершины 5 до текущей);
Get(VfTVfCV) — взятие из очереди очередной вершины графа (позиции на треугольном поле);
PutAll — постановка в очередь всех вершин, смежных с текущей (CVприбавляется к штрафу за текущий переход);
OutResult — вывод результата — минимального штрафа при путешествии от позиции 5 до позиции D.
Рассмотрим подробнее реализацию процедуры InitGraph
procedure InitGraph:
begin
Pw[O]:-l: G[0.1]:-2;
for i:-l to M-l 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:=1 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 1:-0 to m*m-l do cp[i]:-maxint; end;
В данной задаче граф представляется двумя массивами:
Pw[i] — количество дуг из вершины i;
G[ij] — вершина, в которую идет дуга номерj из вершины:. Из условий задачи очевидно, что из каждой вершины идет не более трех ребер к другим вершинам.
Кроме того, для каждой вершины i хранится cp[i] — текущее значение штрафа до этой вершины от исходной вершины 5.
В процессе инициализации устанавливается начальное положение тетраэдра ( 75)— одно из двух возможных: 1и или 1J.
И, наконец, рассмотрим подробнее процедуру PutAlk
procedure PutAlKV.TV.CV:1 ongint):
var
NV. NTV. Dir. Base. NCV : 1ongint; begin
for i:-l to Pw[V] do
begin
NV:-G[V.i];
{Right}{Left}{Down} {UP}{Right}{Left}{Down} {UP}if NV-V+1
{Right}
{Left}
{Down} {UP}
{Right}
{Left}
{Down} {UP}
then Dir:-3
else
if NV-V-1
then Di г:-4 else
if NV>V
then Dir:-1
else Dir:-2:
NTV:-T[TV.Dir];
Base:- (NTV+1) div 2:
NCV:-CV+SQR(p[NV]-R[Base]):
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), смежных с текущей, выясняется направление движения (Dif)t тип прибытия (N7V), грань прибытия (Base), штраф попадания в вершину NV по текущему маршруту с учетом накопленного штрафа (CV) и штрафа за последний перекат (SQR(p[NV]-R[Base})). Очередная вершина ставится в очередь, только если новый штраф для этой вершины меньше прежнего (NCV < cp[NV\). Полный текст решения задачи приведен в конце главы.
Задача «Стены»
Международная олимпиада по информатике, 2000
Стены (walls.in / walls.out)
В некоторой стране стены построены таким образом, что каждая стена соединяет ровно два города, и стены не пересекают друг друга. Таким образом, страна разделена на отдельные части. Чтобы добраться из одной области в другую, необходимо либо пройти через город, либо пересечь стену. Два любых города Л иВ могут соединяться не более чем одной стеной (один конец стены в городе А, другой — в городе В). Более того, всегда существует путь из города А в город В, проходящий вдоль каких-либо стен и через города. Существует клуб, члены которого живут в городах. В каждом городе может жить либо один член клуба, либо вообще ни одного. Иногда у членов клуба возникает желание встретиться внутри одной из областей, но не в городе. Чтобы попасть в нужную область, каждый член клуба должен пересечь какое-то множество стен, возможно, равное нулю. Члены клуба путешествуют на велосипедах. Они не хотят въезжать ни в один город из-за интенсивного движения на городских улицах. Они также хотят пересечь минимально возможное количество стен, так как пересекать стену с велосипедом довольно трудно. Исходя из этого, нужно найти такую оптимальную область, чтобы суммарное количество стен, которые требуется пересечь членам клуба, называемое суммой пересечений, было минимальным из всех возможных.
Города пронумерованы целыми числами от 1 до N, где N— количество городов. На рис. 4.7 пронумерованные вершины обозначают города, а линии, соединяющие вершины, обозначают стены. Предположим, членами клуба являются три человека, которые живут в городах с номерами 3,6 и 9. Тогда оптимальная область и соответствующие маршруты движения членов клуба показаны на рис. 4.8. Сумма пересечений равна 2, так как членам клуба
требуется пересечь две стены: человеку из города 9 требуется пересечь стену между городами 2 и 4, человеку из города 6 требуется пересечь стену между городами 4 и 7, а человек из города 3 не пересекает стен вообще.
Рис. 4.7. Карта городов и стен для задачи «Стены»
Рис. 4.8. Оптимальная область и маршруты движения
Требуется написать программу, которая по заданной информации о городах, областях и местах проживания членов клуба находит оптимальную область и минимальную сумму пересечений.
Ввод:
Первая строка содержит одно целое число: количество областей М, удовлетворяющее неравенствам 2^М< 200. Вторая строка содержит одно целое число: количество городов N, причем 3 < N< 250. Третья строка содержит одно целое число: количество членов клуба £, причем 1<£<30hL<N.
Четвертая строка содержит 丄 различных целых чисел в возрастающем порядке: номера городов, где живут члены клуба.
Оставшаяся часть файла содержит 2М строк, по паре строк для каждой из Af областей. Первые две строки описывают первую область, вторые две строки — вторую область, и т. д. В каждой паре первая строка содержит количество городов на гра・ нице области; вторая содержит номера этих / городов в порядке обхода границы области по часовой стрелке. Единственным исключением является последняя область — это находящаяся снаружи (внешняя) область, окружающая все города и другие области, и для нее порядок следования городов на границе задается против часовой стрелки.
Порядок описания областей во входном файле задает целые номера этим областям. Область, описанная первой во входном файле, имеет номер Дописанная второй —
номер 2, и т. д. Обратите внимание, что входной файл содержит описание всех областей, образованных городами и стенами, включая находящуюся снаружи (внешнюю) область.
Вывод:
Первая строка этого файла содержит одно целое число — минимальную сумму пересечений. Вторая строка содержит одно целое число — номер оптимальной области. Если существует несколько различных решений, вам необходимо найти лишь одно из них.
Пример ввода и вывода'.
Walls.in
(продолжение ввода)
Walls, out
10
2
10
3
3
486
369
3
3
687
1 23
3
3
458
1 37
4
4
78 109
2473
3
3
5 108
467
7
3
79 10542 1
Описание решения'.
Основная идея заключается в том, чтобы преобразовать данную задачу к задаче на графе следующим образом: пусть вершины — это области, образованные стенами. Наличие ребра означает смежность областей. Для перехода в смежную область требуется перелезть ровно одну стену. Например, для данных из примера ввода получим граф из 10 вершин (10 областей), связанных ребрами (смежные области) следующим образом:
Область<
:Смежные области
1
23 10
2
1 3 10
3
1 24 10
4
3 56 10
5
467
6
458
7
59 10
8
69 10
9
7 8 10
10
1234789
Нужно найти такую вершину на графе, чтобы сумма расстояний к ней из городов, где живут члены клуба, была минимальной. Каждый город может быть граничным для нескольких областей. Поэтому для каждого города нужно выбирать наиболее выгодную область, среди его граничных областей.
Рассмотрим реализацию решения. Тело главной программы выглядит следующим образом:
begin
InputData;
InitGraph:
Floyd;
FindBest:
OutResult:
end.
Здесь:
InputData — процедура ввода исходных данных;
InitGraph — процедура инициализации графа;
Floyd — процедура поиска алгоритмом Флойда кратчайшего расстояния между всеми парами вершин;
FindBest — процедура поиска оптимального места встречи;
OutResult — процедура вывода результатов.
Рассмотрим подробнее реализацию этих процедур. Начнем с процедуры InputData:
procedure InputData:
begin
assigndnput, 'walls.in'); reset(input):
read(M.N.L):
for i:-l to L do read(c[i]);
for i:-l to M do
begin
read(k[i]);
for j:-l to k[i] do read(t[i.j]):
end:
close(input);
end:
Здесь:
c[i] — города, в которых живут члены клуба;
k[i] — количество городов, ограничивающих область:;
t[i] — список городов, ограничивающих область i (r[i] состоит из t[ij] для j от 1 ДО 砲])•
Теперь рассмотрим процедуру IninGraph:
procedure InitGraph;
var
wl,w2 : array [1..MaxM] of byte:
s.p : byte:
begin
for i:-l to M do
for j:-l to M do
begin
G[i.j]:-GREAT;
if i-j then continue:
for x:-l to M 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 M do inc(s.wl[x]*w2[x]);
if s>-2
then begin G[i.j]:-l: G[j.i]:-1; end: end:
end:
Здесь
-.,—{GREAT(максимальновозможная константа), если области несмежные;
」 [1, если области смежные.
Для установления факта смежности областей используется следующий алгоритм. Если списки городов, ограничивающих области (E[i] и ф]), включают 2 одинаковых города, значит, эти области являются смежными, поскольку они имеют общую стену, соединяющую эти два города.
Для реализации установки факта наличия двух общих чисел используются два массива «4 и @2. wi содержит 1 на позициях городов, ограничивающих первую область, и 0 на всех остальных позициях. Аналогично w2 содержит 1 на позициях городов, ограничивающих вторую область, и 0 на всех остальных позициях. Если единицы встречаются в обоих городах на одних и тех же позициях более двух раз, то области смежные.
Процедура Floyd находит кратчайшие расстояния G[i, j] на построенном графе между всеми парами вершин (i, j).
procedure Floyd:
begin
for x:-l to M do
for i:-l to M do
for j:-l to M do
beginif i-j then G[i.j]:-0:if (G[i.x]<>GREAT) and (G[x.j]<>GREAT)beginif 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]-bG[x.j]):then G[i.j]:- min(G[i.j].G[i.x]-bG[x.j]):end:end:end;To есть в результате выполнения алгоритма G[if j] будет содержать количество стен, которые нужно перелезть, чтобы попасть из области : в область j. Заметим, что в соответствии с семантикой задачи G[i, i] полагается равным нулю для всех Теперь обратим свое внимание к процедуре FindBest:procedure FindBest:end;To есть в результате выполнения алгоритма G[if j] будет содержать количество стен, которые нужно перелезть, чтобы попасть из области : в область j. Заметим, что в соответствии с семантикой задачи G[i, i] полагается равным нулю для всех Теперь обратим свое внимание к процедуре FindBest:procedure FindBest:varCurS. BM : longint: tc : byte:varCurS. BM : longint: tc : byte:beginbegin{Строим массив город - список областей}{Строим массив город - список областей}for i:-1 to М do kR[i]:-0: for i:-l to M dofor i:-1 to М do kR[i]:-0: for i:-l to M do{i - номер области}{i - номер области}for j:-l to k[i] dobeginx :■ t[i.j];for j:-l to k[i] dobeginx :■ t[i.j];{x - номер города}{x - номер города}inc(kR[x]);inc(kR[x]);R[x.kR[x]]:-i: end:R[x.kR[x]]:-i: end:MinS:-maxlongint: NumM:-0; for i:-l to M doMinS:-maxlongint: NumM:-0; for i:-l to M do{Цикл по областям}{Цикл по областям}beginCurS:-0;for j:-l to L dobeginCurS:-0;for j:-l to L do{Цикл по членам клуба}{Цикл по членам клуба}begintc:-c[j]:BM:-GREAT:for x:-l to kR[tc] dobegintc:-c[j]:BM:-GREAT:for x:-l to kR[tc] do{Город с членом клуба} {Лучший для члена}{Город с членом клуба} {Лучший для члена}
begin
if i-j then G[i.j]:-0:
if (G[i.x]<>GREAT) and (G[x.j]<>GREAT)
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]-bG[x.j]):
then G[i.j]:- min(G[i.j].G[i.x]-bG[x.j]):
end:
end:
end;
To есть в результате выполнения алгоритма G[if j] будет содержать количество стен, которые нужно перелезть, чтобы попасть из области : в область j. Заметим, что в соответствии с семантикой задачи G[i, i] полагается равным нулю для всех Теперь обратим свое внимание к процедуре FindBest:
procedure FindBest:
end;
To есть в результате выполнения алгоритма G[if j] будет содержать количество стен, которые нужно перелезть, чтобы попасть из области : в область j. Заметим, что в соответствии с семантикой задачи G[i, i] полагается равным нулю для всех Теперь обратим свое внимание к процедуре FindBest:
procedure FindBest:
var
CurS. BM : longint: tc : byte:
var
CurS. BM : longint: tc : byte:
begin
begin
{Строим массив город - список областей}
{Строим массив город - список областей}
for i:-1 to М do kR[i]:-0: for i:-l to M do
for i:-1 to М do kR[i]:-0: for i:-l to M do
{i - номер области}
{i - номер области}
for j:-l to k[i] dobegin
x :■ t[i.j];
for j:-l to k[i] dobegin
x :■ t[i.j];
{x - номер города}
{x - номер города}
inc(kR[x]);
inc(kR[x]);
R[x.kR[x]]:-i: end:
R[x.kR[x]]:-i: end:
MinS:-maxlongint: NumM:-0; for i:-l to M do
MinS:-maxlongint: NumM:-0; for i:-l to M do
{Цикл по областям}
{Цикл по областям}
begin
CurS:-0;
for j:-l to L do
begin
CurS:-0;
for j:-l to L do
{Цикл по членам клуба}
{Цикл по членам клуба}
begin
tc:-c[j]:
BM:-GREAT:
for x:-l to kR[tc] 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;
Вначале в ней строится массив /?[/] — для города i список смежных областей, то есть тех областей, для попадания в которые из этого города не требуется перелезать стены.
Затем циклом по всем областям находим минимальную стоимость проведения сбора членов клуба именно в ней и среди этих значений находим минимальное (М加5) и номер соответствующей области (NumM). Заметим, что для каждого члена клуба в качестве области, из которой он отправится в путь, нужно последовательно выбирать все области, смежные с городом, запоминая ту, которая имеет кратчайший путь к текущему месту сбора (i).
Наконец, процедура OutResult выводит найденные величины MinS и NumM:
procedure OutResult:
begin
assignCoutput.'walls.out*): rewrite(output):
writeln(MinS):
writeln(NumM):
close(output):
end:
Полный текст решения задачи приведен в конце главы.
Задача «Блокада»
Всероссийская командная олимпиада школьников по программированию, 2001
Блокада (input.txt / output.txt /2 с)
Государство Флатландия представляет собой прямоугольник размером М х N, состоящий из единичных квадратиков. Флатландия разделена на К провинций (2<К< 100). Каждая провинция представляет собой связное множество квадратиков, то есть из каждой точки провинции можно дойти до любой другой ее точки, при этом разрешается переходить с квадратика на квадратик, только если они имеют общую сторону (общей вершины недостаточно). Во Флатландии нет точки, которая граничила бы более чем с тремя провинциями (то есть четыре квадратика, имеющие общую вершину, не могут принадлежать четырем разным провинциям).
Каждая провинция имеет свой символ. Столица Флатландии находится в провинции, имеющей символ А (заглавная латинская буква А). Провинция называется пограничной, если она содержит граничные квадратики. Провинция, в которой находится столица Флатландии, не является пограничной.
Король Ректилании, соседнего с Флатландией королевства, решил завоевать Флатландию. Для этого он хочет захватить столицу Флатландии. Однако он знает, что сил его армии недостаточно, чтобы сделать это сразу. Поэтому сначала он хочет окружить центральную провинцию, ослабить силы противника долгой блокадой, а потом захватить столицу.
Чтобы окружить провинцию, требуется захватить все провинции, с которыми она граничит. Две провинции граничат, если существует два квадрата, имеющие общую сторону, один из которых принадлежит первой из них, а другой — второй. Чтобы захватить провинцию, надо чтобы выполнялось одно из двух условий: либо она пограничная, либо граничит с какой-либо уже захваченной провинцией.
Чтобы сберечь силы своей армии, король Ректилании хочет установить блокаду центральной провинции, захватив как можно меньше провинций. Помогите ему выяснить, сколько провинций потребуется захватить. Захватывать саму центральную провинцию нельзя, поскольку для этого сил армии Ректилании пока недостаточно.
Ввод:
Первая строка содержит М и N (3 « М, NК 200). Следующие М строк содержат N символов каждая и задают карту Флатландии. Символ, находящийся в (: + 1)-й строке входного файла на/м месте, представляет собой символ провинции, которой принадлежит квадратик (ij). Все символы имеют ASCII-код, больший 32.
Вывод.
Выведите в выходной файл единственное число — количество провинций, которые требуется захватить. Если установить блокаду невозможно, выведите «-1».
Пример вводаПример вывода
Тб4
BBBBBZ
BCCCBZ
BCAbbZ
BDDDbZ
33333Z
Описание решения'.
Переход к графам осуществляем следующим образом: вершина — область (обозначается символом с возможным кодом от 32 до 255). Ребро отображает смежность двух областей. При вводе исходных данных анализируются две смежные строки: соседние (горизонтально) символы во второй строке и соседние (вертикально) символы первой и второй строк. По этим данным пополняются ребра графа.
Области, смежные с областью А, которую нужно блокировать, сразу включаем в список областей, которые нужно захватить. Их количество обозначаем переменной А. Затем алгоритмом Дейкстры находим кратчайшие расстояния от внешней области, обозначаемой пробелом (код пробела — 32), до всех заданных областей.
Если какая-то из областей, смежных с областью А, недостижима (при вычислении расстояния в d[i] так и осталось GREAT), то выводим ответ «-1» и прекращаем работу программы.
Циклом по количеству смежных с А областей:
все новые вершины, вошедшие в оптимальный путь, добавляем в список всех различных вершин, необходимых для захвата области А;
обнуляем веса всех ребер, связанных с вершинами, вошедшими в оптимальный путь;
алгоритмом Дейкстры находим новые кратчайшие расстояния до всех вершин.
После завершения этого цикла выводим количество вершин в созданном списке. Рассмотрим подробнее реализацию. Тело главной программы выглядит следующим образом: begin
InputDatalnitGraph:
Dejkstra: for i:-33 to 255 do if (GEi.ordCA')]-!) and (i<>ord('A')) and (d[i]-GREAT) then BadStop:
A:-K;
for p.-l to A do
begin ZeroG: Dejkstra: end:
assignCoutput.'output.txt');rewnteCoutput): writeln(K): close(output);
end.
Здесь:
InputDatalnitGraph — процедура, которая обеспечивает ввод и формирование графа;
G[32..255,32..255] — матрица смежности построенного графа;
Dejkstra — процедура, реализующая поиск кратчайших расстояний алгоритмом Дейкстры;
BadStop — процедура вывода -1 в случае недостижимости хоть одной из областей, смежных с областью А от внешней области;
ZeroG — обнуление весов ребер в графе, связанных с вершинами, вошедшими в оптимальный путь;
口 К — количество областей, смежных с областью А (после первого выполнения алгоритма Дейкстры), и количество областей, требуемых для захвата (после последнего выполнения алгоритма Дейкстры).
Рассмотрим подробнее процедуру ввода исходных данных и формирования графа:
procedure InputDatalnitGraph: begin
assign(input.'input.txt*): reset(input); readln(M.N); si:-"; 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 M 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:
si =s2:
end;
s2:=''; for i:-l to N+2 do s2:-s2+'
for j:-2 to N+l 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[ord('A').i卜1) and (i<>ord('A')) then
begin
{Через А идти нельзя){Смежные с А сразу включаем}{Через А идти нельзя){Смежные с А сразу включаем}G[ord('AD.i『GREAT:
{Через А идти нельзя)
{Смежные с А сразу включаем}
{Через А идти нельзя)
{Смежные с А сразу включаем}
iic、K).
diff [К] г:
end. close(input); end:
Слева и справа к каждой входной строке добавляется по символу пробел (''), обозначающему внешнюю область. Кроме того, в качестве первой и последней строк также используем строки, состоящие из N + 2 пробелов.
□ G[i,j] =□ G[i,j] =1, если во вводе есть соседние символы с кодами : и j,
□ G[i,j] =
□ G[i,j] =
GREAT, в противном случае;
diff — массив, содержащий все различные символы областей, соседствующих с областью «А»;
口 К — количество элементов в этом массиве.
Теперь рассмотрим реализацию алгоритма Дейкстры.
procedure Dejkstra:
const
FREEDONEFREEDONE0:1:0:1:
FREE
DONE
FREE
DONE
0:
1:
0:
1:
LabLab:array [32..255] of byte:
Lab
Lab
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]:-D0NE;
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]+G[Bliz.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]<>0 do begin for i:-33 to 255 do if G[i.j]-1
then begin G[i.j]:-0: G[j.i]:-O;end:
G[pred[j].j]:-O;
G[j.pred[j]]:-O:
AddNew(j): j:=pred[j]: end:
end;
Эта процедура обнуляет все ребра вершин графа, вошедших в текущий оптимальный путь от внешней области к области «А».
Заметим, что на первый взгляд по семантике задачи нужно искать кратчайшие пути до вершин, смежных с «А». Однако поскольку алгоритм Дейкстры ищет кратчайшие пути одновременно до всех вершин, то он справляется с нужной нам задачей.
Здесь:
pred — массив, хранящий номера вершин-предков, для тех вершин, который вошли в оптимальный путь;
AddNew(j) — процедура, добавляющая в массив diff вершину, если ее там еще не было.
procedure AddNew(j:byte); var i : byte; begin
while (i<-K) and (diff[i]<>j) do Inc(i):
if i>K
then begin inc(K): diff[K]:=j: end: end;
Полный текст решения задачи приведен в конце главы.
Задача «Мудрый правитель»
Командный чемпионат школьников Санкт -Петербурга по программированию, 2000
Мудрый правитель (INPUT.TXT / OUTPUT.TXT / 10 с)
Известно, что о создателе шахмат ходит множество легенд. Недавно в одной древней библиотеке была обнаружена еще одна. В ней утверждается, что когда создатель игры рассказал правителю о своем изобретении и попросил скромную награду, правитель сказал ему следующее: «Возьми шахматную доску и поставь на нее коня. На ту клетку, на которую ты поставишь коня, я положу 2" золотых монет. На те клетки, на которые ты сможешь дойти конем за 1 ход, я положу 2"一 золотых монет. И вообще, если с клетки, на которую ты поставишь коня, ты сможешь дойти до некоторой клетки самое меньшее за Р V N ходов, я положу на нее 2"_/, золотых монет. Но если ты проявишь чрезмерную жадность и не сможешь унести все монеты, которые я выложу на доску, то я прикажу отрубить тебе голову!»
Ученым известно, что создатель шахмат был очень умным человеком. Он знал, что сможет унести не более М монет. Поэтому он поставил коня на такую клетку, чтобы получить как можно больше монет и остаться живым. Если бы такой клетки не было, то он бы тихо покинул город. Помогите ученым узнать, сколько монет заработал создатель шахмат своим изобретением и на какую клетку он поставил коня.
ПРИМЕЧАНИЕ Напоминаем, что шахматная доска имеет форму квадрата, поделенного на клетки; столбцы называются латинскими буквами от а до h, а строки — цифрами от 1 до 8, клетка имеет название в виде пары буква-цифра, в зависимости от того, на пересечении какого столбца и какой строки она находится. Конь ходит буквой «Г» — на 2 клетки в горизонтальном или вертикальном направлении и затем на одну клетку в перпендикулярном направлении. Разумеется, конь не может выходить за пределы доски.
Ввод: На первой строке находятся числа (О <N< 25) и (1 <М< 109). Вывод:
На первой строке выходного файла выведите число 5 — количество монет, которое получил создатель шахмат (если ему не удалось заработать ни одной монеты, то 0). Если 5 > 0, на второй строке выведите в любом порядке, но без повторений, все возможные клетки, в которые он мог поставить коня. Разделяйте имена клеток пробелами.
Пример вводаПримервывода
155
а2 а7Ы Ь8 g1 g8 h2 h7 22117
a1 a8 hih8
3 9291
b3 b6 c2 c7 f2 f7 g3 g6 240
Описание решения'.
Для перехода к задаче на графе вводим следующие аналогии: вершина — клетка доски, ребро — наличие возможности хода шахматного коня из одной клетки в другую. Ребрам присваиваем веса: 0 — для ребра к самой себе; 1 — для ребра к клетке, в которую можно добраться ровно за 1 ход; -1 — в остальных случаях.
Затем алгоритмом Флойда ищем кратчайшие расстояния от каждой клетки до каждой. Далее для каждой клетки строим и запоминаем сумму, которую можно получить по условиям задачи, если начать с этой клетки, параллельно выбираем максимальную из этих сумм, не превышающую М. Выводим найденную сумму. Для каждой клетки, имеющей такую же сумму, выводим координаты этой клетки в шахматно访 нотации. Рассмотрим подробнее реализацию. Тело главной программы выглядит следующим образом:
begin
InputData;
InitGraph;
Floyd;
FindTheBest:
OutResults: end.
Здесь используются процедуры, обеспечивающие отдельные этапы решения:
InputData — ввод исходных данных;
InitGraph — формирование графа;
Floyd — вычисление кратчайшего пути от каждой вершины до каждой алгоритмом Флойда;
FindTheBest — нахождение оптимальной суммы;
OutResults — вывод результатов.
Процедура ввода данных очевидна:
procedure InputData: begin
assigndnput. '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-1) then a[i.j][k.l] :- 0;
end:
end:
Заметим, что матрица смежности графа шахматной доски хранится в виде четырехмерного массива (по два измерения на вершину). Элегантно выглядит при таком подходе и условие существования хода шахматного коня из клетки (i, j ) в клетку (А, / ): 〃加((i - Л)*(/ - /)) в 2.
Отметим также тот факт, что синтаксис Паскаля допускает различные способы записи обращения к элементу четырехмерного массива с индексами /,/ А, /, в том числе a[itj, А, /], a[i][j][k][l] и a[itj][kt /]. В решении выбран последний вариант, как наиболее адекватный реалиям задачи. Теперь рассмотрим процедуру, реализующую алгоритм Флойда:
procedure Floyd:
begin
for p :- 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.U - -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:
Кратчайшее расстояние от клетки (i, j) до клетки (А, /) заменяется путем через клетку 3 q), если:
уже существует путь из клетки (i, j) в клетку (р, q);
(a[i.j][p.q] <> -1)
уже существует путь из клетки (р, q) в клетку (А, /);
(a[p,q][k.l] <> -1)
это расстояние еще не вычислялось (a[i, j][kf /] = -1) или новое расстояние (через клетку (р, 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 p 0: for k :- 1 to 8 do for 1 :- 1 to 8 do if a[i.j][k,l] <- N then p :* p + (1 shl (N - a[i.j][k.l])): if (p > best) and (p <- m) then best :- p:
s[i.j]:-p:
end;
end:
В этой процедуре используются величины a[i, j][kf I] — кратчайшие расстояния от каждой клетки (i, j) до каждой клетки (А, /). При фиксированной клетке (i, j) просматриваются все клетки доски и суммируются соответствующие количества монет, учитывающие количества ходов до этой клетки.
Если до клетки доходим не более чем за N ходов (〃[i, j ][А, / ] < N), то к сумме прибавляем 2A(N - a[itj ][А, / ]). Напомним, что SHL — это операция сдвига влево. А сдвиг влево двоичного целого числа на одну позицию как раз и обеспечивает его умножение на 2. Сдвиг влево двоичного целого числа на X позиций обеспечивает его умножение на 2Л. Соответственно, 1 shl X обеспечивает константу 2х.
Теперь рассмотрим процедуру Out Results:
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;
После вывода оптимального количества монет (best) выводятся все клетки, начиная с которых можно получить такую же сумму (S[iJ] = best).
Полный текст решения приведен в конце главы.
Задача «Ременная передача»
NEERC, Северный четвертьфинал, 1998
Ременная передача (input.txt / output.txt / 10 с)
На плоскости расположены п < 100 валов (Xi,匕一координаты центров, Ri — радиусы). Для каждого i такого, что 1 4 i V 〃, Xi, Yi — целые числа в диапазоне [-10 000, 10 000], Ri — целые числа в диапазоне [1, 10]), связанные К ременными передачами. Валы, имеющие общий центр вращения (одинаковые Xi и Н — координаты центра), жестко связаны, то есть при всех условиях имеют одинаковую угловую скорость (количество оборотов в единицу времени). При этом задаваемая система геометрически правильна, то - есть валы не пересекаются между собой и валы, имеющие общий центр вращения, не могут быть соединены ремнем.
Будем считать вал с номером 1 <m^N вращающимся по часовой стрелке с угловой скоростью 1000 оборотов в минуту. Требуется для каждого вала найти количество полных оборотов, которое он совершит за 1 час.
Ввод:
Первая строка входного файла содержит три числа N, К,加,разделенные пробелами. Затем следуют N строк, каждая из которых содержит тройку чисел Xi,匕,Ri. Следующая группа состоит из К строк вида Uj VJ, где Uj .Vj — номера валов, связанных ремнем j(i<j<K).
Вывод.
Выходной файл должен содержать Nстрок, где i-я строка содержит число полных оборотов i-ro вала, которое он совершит за 1 час или строку «IDLE», если вал не вращается. В случае если заданная система противоречива, то есть на один вал передается вращение с разными угловыми скоростями от нескольких источников, выходной файл должен содержать строку «DO NOT TURN IT О№».
Пример ввода
Пример вывода
52 1
00 1
10 10 3
10 10 1
20 20 3
30 30 10
1 2
34
60000
20000
20000
6666 IDLE
Описание решения :
Для перехода к задаче о графе сопоставим вершинам валы, а ребрам — ременные передачи или жесткое сцепление между ними. Тогда с помощью очереди от заданного исходного вала можно рассчитать количество полных оборотов в час всех валов. Тело главной программы выглядит следующим образом:
begin
InputDatalnitGraph;
QEnd:-O; QBeg -l:
for i =1 to N do A[i]:=-l;
Put(m.60000):
while (QBeg<=QEnd) and not Stop do
begin
Get(V.W):
PutAll(V.W):
end;
OutResults;
end.
Здесь:
InputDatalnitGraph — процедура ввода исходных данных и построения графа;
OutResults — процедура вывода результатов в заданном формате. А остальные строки реализуют очередь начиная от вершины т для вычисления количества оборотов за час всех валов;
QBeg — переменная начала очереди;
QEnd — переменная конца очереди;
Stop — логическая переменная, получает значение «истина», если в результате вычислений получилось противоречние (новое количество оборотов не совпадает с ранее вычисленным);
Get(V,W) — процедура, берущая из очереди вал номер Vс количеством оборотов W;
Put{ У,Ю — процедура, помещающая в очередь вал номер Vс количеством оборотов 死
PutAll{ V,W) — процедура, помещающая в очередь все валы, связанные с валом номер Vc количеством оборотов W
Рассмотрим процедуру ввода данных и построения графа.
procedure InputDatalnitGr^ph:
begin
assign(1nput.'i nput.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]:-0:
for i:-l to К do
begin
read(u.v):
G[u.v]:ж1: G[v.u]:-l;
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]:-l: G[j.i]:-1; end:
close(input):
end;
Здесь:
x[i], y[i], г[Л — координаты центра и радиус вала номер i;
八 г. л [1, если валы i и / связаны,
G 5 = |
[О-в противном случае;
К— количество ременных передач;
〃,и — очередная ременная передача связывает валы 〃 и и.
Теперь рассмотрим процедуры работы с очередью.
procedure Put(V:longint: W real):
begin
inc(QEnd):
Q[QEnd]:-V:
a[V].=W: end;
procedure Get(var Vilongint: var W:real);
begin
V-Q[QBeg]: W:=A[V]:
inc(QBeg): end.
Q— хранит номера вершин, поставленных в очередь;
{
-1, если количество оборотов вала i еще не вычислялось, вычисленное кол-во оборотов в противном случае.
Процедура пополнения очереди PutAlh
procedure PutAll(V:longint: W:real):
begin
for i:-l to N do
if (G[V.i]<>0)
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(то есть при G[V, i] <> 0), если валы на ременной передаче, то вычисляем количество оборотов зависимого вала MW (^[i]) из соотношения W[i]*R[i] = W[v]*R[V]. Если же валы на жестком сцеплении, то количества оборотов одинаковы ( WM = W).
Далее, если количество оборотов этого вала еще не вычислялось, то добавляем в очередь текущий вал i; иначе (a[i] уже вычислялось ранее), если новое (UW) и старое (〃[订)значения не совпадают, (abs(a[i] 一 WN) >Е, здесь Е— принятая
константа различения вещественных чисел, например 0.000001), то переменная stop получает значение true, что прекращает дальнейшую обработку очереди.
И, наконец, процедура вывода результатов:
procedure OutResults:
begin
assignCoutput.'output.txt'): rewrite(output);
if stop
then begin writelnC'DO NOT TURN IT ON•'): halt: end:
for i:-l to N do
begin
a[i]:-trunc(a[i]);
if a[i]-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»).
Задача «Currency Exchange»
NEERC, Северный четвертьфинал, 2001
Currency Exchange (input.txt / output.txt /2 с)
В нашем городе работает несколько пунктов обмена валют. Каждый пункт специализируется на обмене двух конкретных валют и выполняет обменные операции только над ними. Может быть несколько пунктов, специализирующихся на обменах одной и той же пары валют. Каждый пункт имеет свой собственный обменный курс. Обменный курс валюты А к валюте В есть количество валюты В, которое выдается за единицу валюты А. Кроме того, каждый пункт обмена назначает «комиссионные» — сумму, которую вы должны заплатить за операцию обмена. Комиссия всегда собирается в исходной валюте.
Например, если вы хотите обменять 100 долларов США на российские рубли в обменном пункте, в котором курс 29.75, а комиссия 0.39, то вы получите (100 — 0.39) * 29.75 = 2963.3975 российских рублей
Вы точно знаете, что всего есть N различных валют, по которым выполняются обмены в нашем городе — и они обозначаются номерами от 1 до N. Тогда каждый обменный пункт может быть описан 6 числами: целые Л и В — номера валют, с которыми работает данный обменный пункт; и вещественные Л4В, CAB, RBA и СВА — курсы обмена и комиссионные для обмена валюту Л на В и В на Л соответственно.
Николай имеет некоторое количество денег в валюте S и хочет узнать, может ли он некоторой последовательностью операций обмена увеличить свой капитал. Конечно, он хочет иметь в конце операций деньги в той же исходной валюте 5. Помогите ему ответить на этот трудный вопрос. Николай всегда должен иметь неотрицательную сумму денег во время выполнения операций обмена.
Ввод:
Первая строка входного файла содержит четыре числа: N— количество валют, М — количество обменных пунктов, 5 — первоначальный номер валюты у Николая и V— первоначальное количество валюты у Николая.
Каждая из последующих А/строк содержит 6 чисел — описание соответствующего пункта обмена в указанном выше порядке; числа разделены одним или более пробелами. 1 <S<N< 100,1 <М< 100, V— действительное число, 0 < V< 1000.
Во всех обменных пунктах «курс» и «комиссия» — вещественные числа, заданные с двумя знаками после десятичной точки, 0.01 < курс < 100, 0 < комиссия 4 100. Назовем некоторую последовательность обменных операций простой, если в цепочке операций обмена каждый обменный пункт использовался не более одного раза. Вы можете полагать, что коэффициент отношения сумм в конце и начале простой последовательности обменных операций будет меньше, чем 10 000.
Вывод:
Если Николай может увеличить свой капитал, выведите «YES», иначе — выведите «NO».
Пример ввода
Пример выАода
32 1 10.0
1 2 1.00 1.00 1.00 1.00
23 1.10 1.00 1.10 1.00 3 2 1 20.0
1 2 1.00 1.00 1.00 1.00
23 1.10 1.00 1.10 1.00
NO
YES
Коротко постановка задачи может быть переформулирована следующим образом: имеется N видов валют и М пунктов, в которых можно обменивать валюту. Каж-
дый пункт работает с валютами РОВНО двух видов. То есть если он работает с валютами Л и В, то в нем можно поменять валюту А на валюту В и наоборот. Каждый валютный пункт задается 6 числами:
4 В RAB CAB RBA СВА
Первые два числа (номера видов валют) — целые, остальные четыре числа — вещественные. Обмен VA единиц валюты Л на VB единиц валюты В производится по формуле
VB:-(VA - CAB)*RAB.
Аналогично обмен VB единиц валюты В на УЛ единиц валюты А производится по формуле
VA:-(VB - CBA)*RBA
Требуется узнать, можно ли увеличить сумму V, заданную в валюте 5, с помощью имеющейся системы обменных пунктов. При этом обмен каждого типа можно производить не более одного раза. В условиях оговорено также, что недопустимы обмены, при которых получается отрицательная сумма.
Описание решения'.
Перейдем к графу следующим образом. Пусть вершина — вид валюты, а дуга — наличие валютной операции от одной валюты к другой. Фактически каждый обменный пункт задает две дуги между соответствующими вершинами. Начав с вершины, соответствующей валюте 5, необходимо найти цикл (завершающийся в этой же вершине), при котором сумма V увеличится в результате конвертаций, соответствующих циклу на графе. Если такой цикл есть — ответ «YES», иначе — «NO». Цикл можно строить поиском в глубину. Рассмотрим подробнее реализацию. Тело главной программы выглядит следующим образом: begin
InputDatalnitGraph;
VNew:-V:
DFS(S.V);
assign(output. 'output .txt' ):rewriteCoutput):
if VNew>V
then writeln('YES')
else writelnC'NO'):
close(output): end.
Здесь:
InputDatalnitGraph — процедура ввода исходных данных и построения соответствующего графа;
V— исходная сумма;
VNew — новая сумма, если ее удалось увеличить при конвертировании;
DFS — рекурсивная процедура, реализующая «поиск в глубину».
Рассмотрим подробнее процедуру InputDatalnitGraph
procedure InputDatalnitGraph:
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 M 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].1]:-RAB: P[f,kG[f].2]:-CAB:
1nc(kG[t]): G[t.kG[t]]:-f: P[t.kG[t].1]:-RBA: P[t,kG[t].2]:-CBA;
color[f.kG[f]]:-FREE; color[t.kG[t]]:-FREE:
end;
Здесь:
/ — номер вида валюты A (from — откуда);
t — номер вида валюты В (to — куда);
kG[i] — количество дуг из вершины :;
G[i,j] — номер вершины, в которую ведет дуга j из вершины i;
P[i,j] — вес дуги из вершины i в вершину G\ij};
P[i,j, 1] — коэффициент обмена (ЛД);
P[i, j, 2] — комиссия (tyZ);
color[ifj] — пометка об использовании дуги j из вершины i при построении цикла во время поиска в глубину;
FREE — константа, обзначающая, что дуга не использовалась.
Рассмотрим теперь процедуру DFS, реализующую поиск в глубину:
procedure DFS(u:integer; V:real):
var
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 (“, j, V) — функция, осуществляющая обмен суммы V с валюты типа U на валюту, в которую ведет дуга j;
VCur — текущая сумма денег, получившаяся в результате последнего обмена.
Если цикл замкнулся (G[u,j] = 5), то выходим из процедуры 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:
Полный текст решения задачи приведен в конце главы.
Задача «Exchange Rates»
USA Programming, 1999
Exchange Rates (exchange.in / exchange.out /5 с)
Использование денег для оплаты за товары и услуги обычно упрощает жизнь, но иногда люди предпочитают обмениваться непосредственно товарами. В таком случае торговцы устанавливают коэффициенты обменов между товарами. Коэффициент обмена между двумя товарами Л иВ выражается двумя положительными целыми числами тип. В этом случае говорят, что т предметов товара А равноценны п предметам товара В. Например, 2 печи могут быть равноценны 3 холодильникам.
Ваша задача - написать программу, которая по заданному списку обменных коэффициентов вычислит обменный коэффициент между двумя заданными товарами.
Входной файл содержит одну или более команд, за которой следует строка, начинающаяся с символа «точка», которая сигнализирует о конце входного файла. Каждая команда находится на отдельной строке и является или утверждением, или вопросом. Утверждение начинается с восклицательного знака и имеет вид «/ т itema = п itemb》, где Пета и itemb — различные имена товаров, а m и л - натуральные числа, меньшие 100.
Эта команда говорит, что т предметов товара itema равноценны п предметам товара itemb.
Запрос начинается со знака вопроса и имеет вид « ? itema = itemb» — требуется узнать обменный курс между itema и itemb, где itema и itemb — различные товары, каждый из которых уже появлялся в предыдущих утверждениях (хотя и не обязательно в одном и том же).
Для каждого запроса выведите обменный курс между itema и itemb, основываясь на всех утверждениях, обработанных до текущего запроса. Числа, задающие обменный курс, должны быть целыми и взаимно простыми. Если в этой точке невозможно определить обменный курс между данными товарами, используйте знак вопроса вместо целых чисел.
Форматируйте вывод, как указано в примерах вывода.
Примечания:
Названия товаров будут иметь длину не более 20 символов и содержать только маленькие латинские буквы. Для всех названий будет использоваться единственное число. Всего будет не более 60 товаров.
Для каждой пары товаров будет использовано не более одного утверждения. Не будет противоречивых утверждений. Например, противоречивы утверждения «2 pig n 1 cow^, «2 cow 二 1 horses, «2 horse = 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
ЗАМЕЧАНИЕ По условию на вводе соотношения могут задаваться не взаимно простыми числами, тем не менее ответы нужно приводить к взаимно простым коэффициентам отношения товаров.
Описание решения'.
Для перехода к графу сопоставим товарам вершины, а соотношениям — дуги. Каждая строка типа «I» добавляет две дуги в граф и, если есть новые товары, то одну или две вершины. По каждой строке типа «?» алгоритмом Флойда устанавливаем соотношения между всеми товарами и отвечаем на поставленный вопрос.
Рассмотрим реализацию подробнее. Тело главной программы выглядит следующим образом:
begin
assigndnput. 'exchange.in'): reset(input):
assignCoutput.'exchange.out'): rewriteCoutput):
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]e,•'then AddGraph else Answer;
readln(s):
end:
close(input);close(output): end.
Здесь:
KV — количество вершин (различных наименований товаров);
G[i,j ] — указывает, установлено ли соотношение между товаром : и j;
AddGraph — процедура добавления дуг в граф G;
Answer — процедура формирования ответа на вопрос;
口 s — вводимая строка.
Рассмотрим процедуру AddGraph',
procedure AddGraph;
begin
delete(s.l.2): k:=pos('
'.S): vaKcopy(s.l.k-l).nl.j):
delete(s.l.k): k:=pos('
'.S); sl:-copy(s.l.k-l):
delete(s.l.k+2): k:-pos('
'.S); val(copy(s,l.k-l).n2,j);
delete(s.l.k):
ProcessName(sl.kl);
ProcessName(s2.k2):
Reduce(nl.n2):
s2:-s:
AddEdges(kl.k2.nl.n2):
Floyded:-False: end:
Здесь:
si, s2 — названия товаров во введенной строке типа «!»;
九 1, 〃2 — количества товаров в соотношении;
ProcessName{St К) — процедура, которая по названию товара 5 выясняет его номер К;
Reduce(nlt п2) — процедура сокращения ni и п2 до состояния взаимной простоты;
AddEdges(klf А2, 〃1, 〃2) — добавить в граф дуги между вершинами ki и k2 с весами ni и 九2;
Floyded — переменная, которая сбрасывается в false при добавлении в граф очередного ребра и устанавливается в true после обработки графа алгоритмом Флойда. Ее наличие позволяет отвечать на вопросы без лишних запусков алгоритма Флойда (пока граф не менялся от последнего запуска).
Процедура ProcessName:
procedure ProcessNameCs:string: var i:longint): begin
i
while (i<=KV) and (SList[i]<>s) do inc(i): if i>KV
then begin inc(KV); SList[KV]:-s; end: end;
просматривает список SList уже имеющихся наименований товаров. Если 5 в нем содержится, то i получает соответствующий номер, иначе количество наименований KV увеличивается на 1, a i автоматически имеет номер нового товара после исполнения оператора while.
Процедура Reduce:
procedure Reduce(var nl.n2:16ngint);
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:
последовательно делит на все общие множители числа п\ и 九2, в результате чего числа п\ и п2 становятся взаимно простыми.
Процедура AddEdges добавляет в граф две дуги с взаимнообратными соотношениями товаров. G[A1, А2] = 1, если между товарами А1 и k2 введено соотношение. Это соотношение есть Р[Л1, А2,1]/Р[А1, А2, 2].
procedure AddEdges(к1.к2.nl.n2:1ongint);
begin
G[kl,k2]:-1; P[kl.k2.1]:-nl; P[klЛ2.2]:-n2:
G[k2.kl]:-1: P[k2.kl.l]:-n2: P[k2.kl.2]:-nl:
end:
Теперь рассмотрим, как обрабатываются строки типа «?».
Процедура Answer выделяет в si и s2 наименования товаров, которые нужно сопоставить. Затем с помощью процедуры ProcessName находятся соответствующие номера товаров А1 и А2. Далее, если процедура Floyd еще не вызывалась после дополнения ц)афа, то она вызывается, строя все соотношения между товарами. Функция Exist отвечает на вопрос, установлено ли соотношения между товарами А1 и А2. Если соотношение установлено, то в 九1 и 〃2 находятся соответствующие коэффициенты.
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)
then writelnCnl.' '.SList[kl].' - ',n2.' ',SList[k2])
else writelnC? ' .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]-1) and (G[k.j]-1) and (1<>j) then begin
G[i.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:
P[i.j.2]:-n2;
end;
Floyded:-true; end:
Функция Exist получает значение false, если сотношение не установлено, и true — в противном случае. Кроме того, если соотношение между товарами А1 и k2 установлено, оно упрощается с помощью процедуры Reduce.
function Exist(kl.k2:longint; var nl.n2:longint):boolean:
begin
Exist:- G[kl,k2]-1:
if (G[kl.k2]-1) then begin nl:-P[kl.k2.1]: n2:-P[kl.k2.2]: Reduce(nl,n2):
end:
end:
Полный текст решения задачи приведен в конце главы.
Задача «Sorting It All Out»
East Central North America Programming Contest, 2001
Sorting It All Out (input.txt / output.txt / 10 с)
Отсортированная в порядке возрастания последовательность различных величин может быть сформирована, если над этими величинами установлено отношение порядка (<). Например, отсортированная по возрастанию последовательность Л, В, С,О означает, что А< В.В < СиС < D. Вам дается множество отношений видаЛ < В, и требуется установить, можно ли сформировать возрастающую последовательность.
Ввод:
Ввод состоит из множества тестов. Каждый тест начинается со строки, которая содержит два положительных целых числа пит. Первое число указывает количество объектов, которые нужно отсортировать (2^п< 26). Объекты, подлежащие сортировке, обозначаются первыми п большими латинскими буквами. Второе число т обозначает количество отношений вида Л < В, которые даются в текущем тесте. Далее следуют М строк, задающих одно такое соотношение тремя символами: большая буква, символ «V» и другая большая буква. Значение л * m * О обозначает конец ввода.
Вывод:
Для каждого теста выведите одну строку. Эта строка может быть одного из следующих видов:
Sorted sequence determined after xxx relations: yyy...y.
Sorted sequence cannot be determined.
Inconsistency found after xxx relations.
Здесь ххх — количество отношений, обработанных к моменту построения отсортированной последовательности или к моменту обнаружения противоречия (что встретится раньше), а ууу...у — последовательность, отсортированная в порядке возрастания.
Пример ввода:
4 6 А<В А<С В<С C<D B<D А<В 3 2 А<В В<А 26 1 A<Z О О Пример вывода. Sorted sequence determined after 4 relations: ABCD. Inconsistency found after 2 relations.
Sorted sequence cannot be determined.
Коротко говоря, в задаче требуется установить один из трех возможных фактов:
Между величинами построено отношение порядка («меньше»).
Обнаружено противоречие в отношении порядка.
Отношение порядка недоопределено.
Причем третий факт можно обнаружить только после обработки всех отношений, а любой из первых двух может возникнуть до конца обработки всех отношений, поэтому требуется выводить еще и номер отношения, при обработке которого обнаружен соответствующий факт. Кроме того, в случае построения отношения порядка (например, А < В < С < D), нужно его вывести (опустив знак «меньше»: ABCD). В примере ввода приведены варинты исходных данных для всех трех возможных случаев.
Описание решения:
Переход к графу осуществим следующим образом: вершина — это величина, дуга — отношение «меньше» между двумя величинами. Тогда факт 2 означает, что в графе существует цикл (то есть нашлись две величины такие, что X v У и Y<X). Факт 3 означает, что граф не связный (то есть существуют величины, между которыми не установлено отношение порядка). Факт 1 означает, что граф связный (между всеми величинами установлено отношение порядка). Нам также требуется вывести порядок величин (от самой меньшей к самой большей), установленный отношением «меньше».
Рассмотрим подробнее реализацию. Тело главной программы таково:
begin
assigndnput. 'input.txt'): reset(input):
assign(output.'output.txt'): rewrite(output):
readln(N.M).
while (N<>0) do
begin
for i:-l to M do readln(s[i]);
InitGraph;
for i:-l to M do
begin
Add Edged .Sorted. Cycle):
if Cycle
then writelnC'Inconsistency found after '.i.' relations.'):
if Sorted
then writelnC'Sorted sequence determined after ',i.
'relations: '.R.'.'):
if Sorted or Cycle then break:
end;
if not (Sorted or Cycle)
then writelnC'Sorted sequence cannot be determined.'):
readln(N.M);
end:
close(input); close(output):
end.
Здесь:
InitGraph — процедура инициализации графа;
AddEdge — процедура добавления очередной введенной дуги к графу. Она же устанавливает, обнаружен ли в графе цикл (Cyclestrue) и обнаружен ли полный порядок {Sorted^true). Если обнаружена истинность одного из условий Cycle или Sorted, то выводится соответствующее сообщение. В случае Sorted^true прямо из оператора writein вызывается функция R.
口 R — функция, которая возвращает строку символов в порядке, установленном введенным отношением.
Если все строки отношения обработаны, a Sorted и Cycle не были ни разу установлены в состояние true, то выводится сообщение, соответствующее факту 3 (отношение порядка не достроено). Заменить внутри цикла оператор break на оператор halt нельзя, поскольку нам нужно обрабатывать множество тестов в одном входном файле. Рассмотрим процедуру InitGraph:
procedure InitGraph;
begin
for i:-l to 26 do
for j:-l to 26 do G[i.j]:-0: for i:-l to 26 do a[i]:-0: end:
_r. ., [1, если есть дуга из вершины i в вершину j, [0 в противном случае;
□ a[i] — количество вершин, с которыми связана вершина i.
Теперь рассмотрим процедуру AddEdge
procedure AddEdged:integer: var Sorted.Cycle:boolean): var
p.k.q : integer:
begin
j:-ord(s[i.l])-ord('A*)+1;
k:-ord(s[i.3])-ord('A*)+1;
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]-1):
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])・ord('A')+l:
обеспечивают переход от букв к соответствующим числам (вместо буквы А — число 1, вместо буквы В — число 2 ит. д.).
if G[j.k]<-0 then inc(a[j]);
G[j.k]:-1:
Если от вершины j к вершине А еще не было пути, то увеличиваем количество вершин а [У], достижимых от вершины j. И отмечаем достижимость вершины А от вершины у(G[j, А]: = 1;)
for р:-1 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;
Для всех вершин p, от которых была достижима вершина j, теперь устанавливаем достижимость и вершины А. Если ранее вершина А была недостижима отр, то увеличиваем количество (〃[「]) вершин, достижимых от вершины р.
for р:-1 to N do
if G[k.p]-1 then begin if (G[j.p]-O) and (j<>p) then inc(a[j]):
G[j.p]:-1: end;
Для всех вершин p, которые были достижимы от вершины А, теперь устанавливаем достижимость и от вершины/ Если ранее вершинар была недостижима от/ то увеличиваем количество (〃[/]) вершин, достижимых от вершины/
Cycle:-False;
for р:-1 to N do Cycle:-Cycle or (G[p.p]-1):
Если для какого-то p существует путь из вершины р в нее же (G[p, р] ж 1), то обнаружен цикл {Cycle получает значение true)
for р:*1 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]工 1. Таким образом, если все b[i] равны 1, то Sorted устанавливается в true, иначе — в false.
Теперь рассмотрим процедуру R:
function R:string;
var
b: array [1..26] of byte:
i.j.k.s.p : byte:
Т: string:
begin for j:-l to 26 do b[j]:-j: for j:-l to N-l do begin s:-a[j]: k:-j: for i:-j+l to N do if a[i]>s then begin s:-a[i]: k:T 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:-T+chr(b[i]+ord('A')-1):
R:-T: end;
Здесь вначале массив а сортируется по убыванию. Параллельно сортируется массив b — номеров элементов в массиве а. Затем в строку Г объединяются символы в порядке, соответствующем построенному соотношению.
Полный текст решения приведен в конце главы.
4.2.4. Задача «Проверка веб-страниц»
Всероссийская олимпиада школьников по программированию, 1999 Проверка веб-страимц (Input, txt / Output.txt /10 с)
Многим из тех, кому приходилось работать в Интернете, случалось сталкиваться с неправильными ссылками, то есть ссылками на несуществующие документы. Ваша задача — реализовать упрощенную проверку страницы на корректность ссылок.
Ввод:
Входной файл содержит название одного или нескольких документов и их содержимое. Содержимое документов имеет следующий вид:
<HTML>
Текст <END>
В тесте могут присутствовать ссылки на другие документы на данном сервере. Они имеют следующий вид: <Л НКЕА^имя файла»〉.
Первая строка входного файла содержит число N — количество файлов (N< 100). Далее, с новой строки, идет название 1-го документа, за которым следует содержимое 1 -го документа, потом начиная с новой строки название 2-го документа, потом содержимое 2-го документа, и т. д. Размер входного файла не превышает 100 Кбайт.
ПРИМЕЧАНИЯ Ваша программа должна игнорировать различие между строчными и прописными буквами. Любое ненулевое количество пробелов считается эквивалентным одному пробелу. Гаран* тируется, что в теле документа не будут встречаться последовательности символов <html> и <end>. Имена файлов имеют длину не более 32 символов и содержат только латинские буквы, цифры и точки. Длина каждой строки входного файла не превышает 100 символов. В каждом документе не более десяти ссылок.
Вывод:
Поместите в выходной файл два числа, общее количество ссылок на несуществующие документы и количество документов, до которых нельзя добраться, начав с первого документа и переходя по ссылкам.
Пример ввода: 4 index.html <html> Index 1>А HREF=T> 2<А href「2”>yTB <end> 1 <html> This picture shows an example <A HREF-l.”〉 <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
Описание решения.
В качестве вершин графа возьмем имена заданных в исходном файле документов, а также имена файлов, на которые есть ссылки из заданных документов. В качестве дуг на этом графе примем ссылку из файла на файл.
Тогда общее количество ссылок на несуществующие документы — это сумма всех ссылок в графе на вершины, соответствующие файлам, имена которых не заданы в исходном файле как имена документов. Количество документов, на которые нельзя попасть по ссылкам, начиная с первого, можно вычислить следующим образом: вначале методом Флойда построим матрицу достижимости, а затем посчитаем количество вершин, недостижимых от первой.
Рассмотрим подробнее реализацию. Тело главной программы выглядит так: begin
ReadDatalnitGraph:
assign(output.'output.txt*): rewrite(output):
writeln(DeadLinks.' '.UnReachDocs):
close(output);
end.
Здесь:
ReadDatalnitGraph — процедура, которая вводит исходные данные и строит по ним граф;
DeadLinks — функция, вычисляющая общее количество ссылок на несуществующие документы;
UnReachDocs — функция, вычисляющая количество документов, до которых нельзя добраться по ссылкам, начиная с первого документа и переходя по ссылкам.
Рассмотрим теперь процедуру ReadDatalnitGraph',
procedure ReadDatalnitGraph:
begin
assigndnput.' 1 nput.txt'): reset(input):
readln(N):
for 1:-l to 2*N do
for j:-l to 2*N do G[i.j]:-0:
for 1:-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:-pos('<A HREF-M,.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-M,.s): т:-ро5(,и>'.s):
end:
readln(s);
UpCaseCompressStr(s):
end:
end:
close(input):
end:
Здесь:
□□.,_ j 1, если файл i ссылается на файл j,
□
□
」(0 в противном случае.
Заметим, что мы вводим последовательную нумерацию всех имен файлов — в порядке их появления в качестве имени существующего документа или в качестве ссылки;
true, если номер вершины i соответствует 口 RealDocsfi] = < существующему документу,
false в противном случае;
N— количество документов;
К — общее количество имен файлов (вершин в графе — документов и ссылок);
UpCaseCompressStr(s) — процедура приведения всех символов входной строки s к большим буквам, а также замены в ней всех последовательностей двух и более пробелов на один пробел;
AddName(st k) — процедура, которая добавляет в список файлов DocNames новый файл с именем s или находит номер существующего в этом списке имени. В обоих случаях А — номер имени файла s в списке всех файлов.
Строки
readln(s);
UpCaseCompressStr(s):
AddName(s.kl): RealDocs[kl]:-true;
обеспечивают обработку имен документов (существующих файлов). При этом А1 — номер текущего обрабатываемого документа.
Строки
while (so'<END>') do
begin
j:-pos('<A HREF「’.s):
m:・pos('">'.§):
while (j<>0) and (20) 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):
т:-ро5(,и>',s):
end:
readln(s):
UpCa seCompres sSt г(s);
end:
обеспечивают обработку тела документа: в каждой строке ищутся определения ссылок (<А НЯЕР=*файл»>), файлу-ссылке устанавливается номер Л2 (если он новый, то одновременно пополняется список имен).
Строка
inc(G[kl,k2]):
обеспечивает увеличение количества ссылок из файла с номером ki на файл с номером 山2. Оператор while обеспечивает обработку нескольких ссылок в одной строке.
Рассмотрим теперь процедуру UpCaseCompressStr
procedure UpCaseCompressStr(var s: string):
begin
for i :- 1 to Length(s) do
s[i] :- UpCase(s[1]):
p:-s[l]; j:-l:
for i:-2 to length(s) do
if (p[j]o' ') or (s。]。’’)
then begin p:-p+s[1]: 1nc(j) end:
s:-p:
end;
С помощью встроенной в Паскаль функции UpCase все символы переводятся в заглавные. Далее в новую строку р переносим символ очередной из строки 5, если последний символ в строке р — не пробел (или текущий символ в строке s — не пробел). Таким образом, результат выполнения — строка, в которой выброшены все лишние пробелы, а все маленькие латинские буквы заменены на большие.
Процедура AddName обеспечивает поиск строки s в массиве DocNames и возвращение в переменной m соответствующего номера вне зависимости от того, существовала ли строка или это строка новая и ее пришлось добавить в массив DocNames.
procedure AddName(s:string: var m:integer);
begin
m:-l:
while (nx-k) and (s<>DocNames[m]) do 1nc(m):
if m>k
then begin inc(k); DocNames[k]:-s: end:
end:
Для обнаружения «мертвых ссылок» и вычисления их количества использовалась функция DeadLinks.
function DeadLinksinteger; begin
t :-0;
for j:-l to К do
if (not RealDocsEj])
then for i:-l to К do inc(t.G[i,j]);
DeadLinks:-t;
end:
Для всех номеров j нереальных документов (not RealDocs[j]) суммируем все ссылки на них (G[i,j])・
Наконец, для вычисления количества «недостижимых» файлов использовалась функция UnReachDocs:
function UnReachDocs:1nteger:
begin
for t:-l to k do
for i:-l to k do
for j:-l to k do
if (G[i.t]<>0) and (G[t.j]<>0) then G[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[l, i]工 0).
Полный текст решения задачи приведен в конце главы.
Задача «Play On Words»
Central Europe Programming Contest, 1999
Play on Words (input.txt / output.txt)
Некоторые из кодовых дверей содержат очень интересные словесные головоломки. Команда археологов должна решить одну из них, поскольку нет другого способа открыть двери.
На каждой двери имеется большое количество магнитных плат. На каждой плате написано ровно одно слово. Платы могут быть переупорядочены в последовательность таким образом, что каждое слово начинается на ту букву, на которую предыдущее слово заканчивается. Например, за словом «ас加》 может следовать слово «motorola». Ваша задача — написать программу, которая прочитает список слов и определит, можно ли их переупорядочить таким образом, чтобы они составили последовательность (в соответствии с описанным выше правилом), и тем самым открыть дверь.
Ввод:
Ввод состоит из Г тестовых случаев. Их количество задается в первой строке входного файла. Каждый тест начинается со строки, содержащей одно целое число N которое задает количество плат(1 4 N & 100 000). Каждая из последующих N строк содержит ровно одно слово. Каждое слово состоит из строчных латинских букв. Длина слова — от 2 до 1000 букв. Одно и то же слово может появиться в списке несколько раз. Вывода
Ваша программа должна определить, возможно ли переупорядочить заданные слова таким образом, чтобы первая буква каждого слова совпадала с последней буквой предыдущего слова. При этом каждое слов из входного файла должно быть использовано, и только один раз. Слово, которое на вводе встретилось несколько раз, должно быть использовано столько же раз.
Если существует такой порядок слов, ваша программа должна вывести фразу «Ordering is possible». Иначе выведите фразу «The door cannot be opened».
Пример ввода
Пример вывода
3
2 аст ibm
3 аст malform mouse
2 ok ok
The door cannot be opened Ordering is possible The door cannot be opened
Описание решения.
Перейдем к графу следующим образом: буква — вершина, слово — дуга из начальной буквы слова к конечной букве. Тогда ответ на вопрос задачи будет утвердительным, если выполняются три нижеследующих условия.
Граф связен: существует вершина, от которой доступна любая из остальных вершин.
Не существует вершин, у которых количество входящих дуг отличается от количества исходящих на 2 или больше.
LT< 1 и GT< 1, где LT— количество вершин, у которых количество входящих дуг на 1 меньше количества исходящих дуг, GT — количество вершин, у которых количество входящих дуг на 1 больше количества исходящих дуг.
Рассмотрим реализацию подробнее. Тело главной программы выглядит так:
begin
assign (input,'input.txt'); reset(1nput):
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 CorrNumlnOut and Connected
then writeln('Ordering is possible.')
else writeln('The door cannot be opened.'):
end:
close(input): close(output);
end.
Здесь:
ОС — номер теста во входном наборе данных;
NT— количество тестов;
., fl, если есть дуга из вершины i в вершину j,
Gg =,
в противном случае;
Letter[i] =1, если буква, соответствующая вершине i была во входных данных первой или последней хотя бы в одном из слов;
ReadWord(bt е) — процедура, которая умеет считывать слова длиной до 1000 символов и возвращает b и е — номера, соответствующие первой и последней букве прочитанного слова;
CorrNumlnOut — функция, которая получает значение true, если степени количества входных и выходных дуг для всех вершин графа удовлетворяют вышеописанным правилам (2 и 3);
Connected — функция, которая получает значение true, если граф связный (условие 1).
Рассмотрим процедуру ReadWord
procedure ReadWord(var В.E:byte): var s.c : char: begin read(c); b:-ord(c)-ord('a*)+1: while (co#13) do begin s:-c: read(c);
end;
e:=ord(s)・ord('a')+l:
read(c):{дочитываем #10}
end:
Строки в файле разделяются парами символов с кодами #13 и #10. Поэтому процедура читает первый символ, строит по нему номер Ь. Затем сохраняет прочитанный символ В5и читает следующий до тех пор, пока не доберется до ограничителя строки — символа #13. Затем по символу s, содержащему последнюю букву введенного слова, строится величина е. После чего делается еще один оператор read, чтобы дочитать второй символ — разделитель строк (#10).
Теперь рассмотрим функцию CorrNumlnOut
function CorrNumlnOut:boolean; var
LT. GT : byte; begin for 1:-l to 26 do begin kGIn[1]:-0; kG0ut[1]:-0: for j:-l to 26 do begin 1nc(kGIn [i].G[j.i]): 1nc(kG0ut[i].G[i.j]): end: end:
CorrNumlnOut:-fa1se:
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:
CorrNumlnOut:- (LT<-1) and (GT<-1); end;
Здесь kGIn[i] и kGOut[i] — количества входящих и исходящих дуг вершины i. Если они отличаются более чем на 1,то выход и CorrNumlnOut = false. Для всех реально введенных букв (Letter[i] = 1) считаем величины GTи LTt если они обе не превышают 1, то CorrNumlnOut = true, иначе — false. Наконец, рассмотрим функцию 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]<>0) then G[i.j]:-l;
Al 1:-0: for i:-l to 26 do 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]-1) and (1<>j) and (G[i.j]<>0) then inc(k): if k-Al 1-1 then exit: end;
Connected:-faIse: end:
Вначале с помощью алгоритма Флойда строим матрицу достижимости. Затем считаем величину АН — количество реально введенных букв (в качестве начальных или конечных всех введенных слов). Если существует вершина, из которой достижимы все остальные (А * All - 1), то Connected получает значение true, иначе —значение false. Полный текст решения задачи приведен в конце главы.
4.3. Задачи на множествах отрезков
В данном разделе собраны задачи, в исходных формулировках которых фигурируют множества отрезков, но которые позволяют переформулирование в терминах графов.
Задача «Падение»
Центральноевропейская олимпиада, 2000
Падение (FALL.IN / FALLOUT) Рассмотрим игру, основанную на устройстве, изображенном на рис. 4.9.
Рис. 4.9. Схема устройства для задачи «Падение»
Устройство состоит из множества горизонтальных платформ различной длины, размещенных на различных высотах. Самая нижняя платформа — уровень земли (она расположена на высоте 0 и имеет бесконечную длину).
С заданной позиции отпускают мяч в свободное падение в момент времени 0. Мяч падает с постоянной скоростью 1 м/с. Когда мяч достигает платформы, он начинает катиться по направлению к одному из концов платформы, по выбору игрока, с той же скоростью — 1 м/с. Когда он достигает конца платформы, он продолжает вертикальное свободное падение. Мячу не позволено свободное падение более чем на МАХ метров в одно падение (между двумя платформами). Напишите программу, которая найдет минимальный по времени путь мяча к земле.
Ввод:
Первая строка: NX YМАХ — четыре целых числа, разделенных пробелом: количество платформ (не учитывая землю), начальные координаты мяча, и установленное максимальное расстояние «свободного падения» между двумя платформами. Платформы нумеруются от 1 до N.
Следующие Nстрок: Xli X2i Hi — 3 целых числа разделенных пробелами; :-я платформа расположена на высоте Hi между горизонтальными координатами xii и Х2: включительно (Xli < X2i, i 工 1..巾.
ЗАМЕЧАНИЯ Игнорируйте диаметр мяча и тотцину платформы. Если мяч падает точно на край платформы, он рассматривается как упавший на эту платформу. Никакие две платформы не имеют общих точек. Для заданных тестовых данных решение всегда существует. Все величины измерены в метрах.
Выбо@:
Первая строка: целое число — время, через которое мяч коснется земли в соответствии с вашим решением.
Оставшиеся строки до конца файла: PTD, три целых числа, разделенных пробелами. Мяч коснется платформы Рв момент времени Г и покатится в направлении 0 (0 — влево, 1 — вправо). .
касание земли не должно появиться в этих строках.
порядок вывода должен быть таким, что величины Г приводятся в порядке возрастания.
Если существует несколько решений, выведите только одно.
Ограничения*.
14NW1000
20 000 4 XUf X2i 4 20 000 (: = 1..N)
Q<Hi<Y<20 ООО
Цример ввода
Цримар «м<м|в
3 8 17 20
0 10 8
0 10 13
4 14 33 16 1
23
24 1 1 11 1
Описание решения:
Переход к графу осуществляем следующим образом: вершины — это начальное расположение шарика, все начала и концы горизонтальных отрезков (платформ), а также две специальных конечных веришны, описывающие платформу <земля». Ребра — время, потребное шарику, чтобы добраться от %дной вершины до другой. С учетом того, что скорость и горизонтального и вертикального движения 1 м/с, а все расстояния измерены в метрах, это время просто равно сумме вертикальных и горизонтальных расстояний между вершинами.
Результат задачи может быть получен с помощью алгоритма Дейкстры, который ищет кратчайшие расстояния от заданной вершины (начальной точки расположения шарика) до всех остальных. В качестве ответа нас интересует кратчайшее расстояние (время) от начальной вершины до вершины <земля>.
Рассмотрим подробнее реализацию этого решения. Тело главной программы выглядит следующим образом:
begin
InputData:
InitGraph;
Dejkstra:
OutResults;
end.
Здесь:
InputData — процедура ввода исходных данных;
InitGraph — процедура построения графа;
Dykstra — процедура, исполняющая алгоритм Дейкстры для поиска кратчайших расстояний на графе от начальной до всех остальных. Параллельно, для каждой вершины оптимального пути запоминается предыдущая вершина.
OutResults — процедура вывода результатов в соответствии с форматом вывода.
Рассмотрим подробнее процедуру InputData
procedure InputData;
begin
assigndnput.'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— количество платформ;
Х,У — начальные координаты шарика;
%l[i], x2[i], Н[г] — описание платформы номер /(начало и конец по Хи высота);
МАХ — максимальное вертикальное расстояние, которое шарику разрешается пролететь между платформами.
Теперь перейдем к процедуре InitGraph
procedure InitGraph:
begin
SortPlatforms:
for 1:-0 to 2*N+1 do kG[i]:-0;
Buildl(O.X.Y):
for i:-l to N do Build2(i): end;
Она вызывает процедуру сортировки платформ SortPlaforms. Затем последовательно дополняет граф с помощью процедур Buildi и Build!, Процедура SortPlatforms приведена далее:
procedure SortPlatforms;
var
s.k : Integer:
begin
for i:-l to N do b[i]:-i:
for j:-l to N-l 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;
Можно заметить, что ключом сортировки по убыванию является высота платформ H[i]. Одновременно синхронно переносятся начала (х1[:]) и концы (х2[:]) платформ, а также первоначальные номера платформ (&[:]). Массив b[i] будет использован при выводе в ответ исходных номеров платформ. Кроме того, в конец массива добавляется плафторма «земля» на позицию N + 1 осортированного массива.
Процедура Build\(it X, У) заносит в граф расстояния от вершины (X, У) до ближайшей платформы:
procedure BuildKi .X.Y:integer):
begin
j:-((i+l) div 2)+1:
while ((X<xl[j]) or (X>x2[j])) and (Y-H[j]<-MAX) 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[iAG[i]+2]:-(Y-H[j]) + (x2[j]-X):
if j-N+1 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:-((i+l) div 2)+1
Платформа считается найденной, если координата вершины по Xнаходится между координатами платформы,
((X<xl[j]) or (X>x2[j]))
и свободный полет шарика еще не превысил допустимый предел (У- H[j] < МАХ).
Граф хранится в виде двух массивов:
kG[i] — количество ребер из вершины i;
G[iJ] — номер вершины, в которую ведет ребро номер j из вершины i.
Если вершина не «Земля» (j*N 十 1), то суммируются вертикальный и горизонтальный пути, иначе — только вертикальный.
Процедура Build2 вызывает процедуру Buildi для обоих концов очередной платформы:
procedure Build2(iinteger); begin
Buildl(2*i-l.xl[i].H[i]):
Buildl(2*1.x2[1].H[1]):
end:
Рассмотрим теперь процедуру Dykstra
procedure Dejkstra: const
FREE - 0;
DONE - 1:
var
Lab : array [0..М2] of integer:
MinD. Bliz : longint:
{Инициализация данных}{Инициализация данных}{у нее нет предка}{расстояния от 0-й до остальных}{у нее нет предка}{расстояния от 0-й до остальных}begin
{Инициализация данных}
{Инициализация данных}
{у нее нет предка}
{расстояния от 0-й до остальных}
{у нее нет предка}
{расстояния от 0-й до остальных}
for i:-0 to 2*N+1 do begin
Lab[i]:=FREE;
pred[i]:-0:
d[i]:-maxlongint:
end:
d[0]:-0:
Lab[0]:-D0NE:
pred[0]:--l:
for i:-l to kG[0] do d[G[0.i]]:-P[0.i];
for j:-0 to 2*N+1 do
beginMinD:-maxlongint:for i:-0 to 2*N+1 doif (Lab[i]-FREE) and then beginMinD:-d[i]; end:beginMinD:-maxlongint:for i:-0 to 2*N+1 doif (Lab[i]-FREE) and then beginMinD:-d[i]; end:{Поиск ближайшей из необработанных}{Поиск ближайшей из необработанных}(d[i]<MinD)(d[i]<MinD)Bliz:-iBliz:-i
begin
MinD:-maxlongint:
for i:-0 to 2*N+1 do
if (Lab[i]-FREE) and then begin
MinD:-d[i]; end:
begin
MinD:-maxlongint:
for i:-0 to 2*N+1 do
if (Lab[i]-FREE) and then begin
MinD:-d[i]; end:
{Поиск ближайшей из необработанных}
{Поиск ближайшей из необработанных}
(d[i]<MinD)
(d[i]<MinD)
Bliz:-i
Bliz:-i
Lab[Bliz]:-Done;
{ Пересчет кратчайших расстояний через ближайшую}
for 1:-1 to kG[Bliz] do
if Lab[G[Bliz.i]]-FREE
then if d[G[Bl1z.i]]>d[bliz]+P[Bliz.i]
then begin
d[G[Bliz.i]]:-d[bl1z]+P[Bliz.1]:
pred[G[Bliz.i]]:-Bliz:
end:
end;
end;
Эта процедура, как обычно, инициализирует начальные данные, а затем в цикле находит ближайшую к текущей вершину (Bliz) и пересчитывает через нее расстояния до всех оставшихся вершин. Попутно в pred[i\ хранится номер вершины, являющейся предыдущей для вершины: на оптимальном маршруте из начальной вершины в конечную.
Теперь рассмотрим подробнее процедуру вывода результатов.
procedure OutResults:
var
res : array [1..MaxN] of integer:
t.kp. NumP.Dlr,Time.Last.Curr : integer;
begin
assign(output.'fall.out'): rewrite(output):
writeln(d[2*N+l]):
t:-2*N+l; kp:-O:
while (pred[t]oO) 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,' '.Dlr):
Last:-Curr:
end:
close(output):
end:
Вначале выводится минимальное время полета шарика «2 •" + 1 ])• Далее по массиву pred восстанавливается оптимальный маршрут Res. И затем циклом по всем
элементам оптимального маршрута выводятся результаты в соответствии с форматом вывода.
Полный текст решения приведен в конце главы.
Задача «The Doors»
Mid-Central USA Programming Contest, 1996
The Doors (doors.ln / doors.out / 30 c)
Вы должны найти длину кратчайшего пути в комнате, в которой расположены препятствия. Комната всегда имеет стороны % = 0, % = 10, у = 0, у = 10. Начальная и конечная точки пути всегда (0,5) и (10,5). Может быть от 0 до 18 вертикальных стен внутри комнаты, каждая с двумя дверями.
Ввод/вывод:
Первая строка содержит количество внутренних стен. Затем идет строка, описывающая каждую стену пятью вещественными числами. Первое число — х-коор- дината стены (0 <х< 10). Оставшиеся четыре числа — этоу-координаты концов дверей в этой стене. Х-координаты стен задаются в порядке возрастания. Для каждой стены у-координаты дверей также задаются в порядке возрастания. Входной файл содержит не менее одного такого набора данных. Конец входного файла обозначается числом -1 на месте количества стен.
Выходной файл должен содержать одну строку для каждого теста. Эта строка должна содержать длину минимального пути, округленную до двух знаков после десятичной точки.
Пример ввода,
1.
5 4 6 7 В
2
4 2 7 8 9
7 3 4.5 6 7
-1
Пример вывода:
10.00
10.06
Фактически в задаче требуется найти кратчайший путь из точки (0,5) в точку (10,5), учитывая наличие задаваемых препятствий в виде вертикальных стен. В каждой стене есть две двери, через которые можно преодолевать стену.
Перейдем к графу следующим образом. Пусть начальная точка, конечная точка и границы дверей — это вершины графа. Ребро связывает две вершины, если одна точка находится в зоне прямой видимости другой. В качестве веса ребер примем расстояния по прямой между вершинами. Найти кратчайшее расстояние
на заданном графе от начальной вершины до конечной можно с помощью алгоритма Дейкстры.
Рассмотрим подробнее реализацию решения.
С учетом формата ввода (с возможностью наличия нескольких тестов в одном файле) тело главной программы выглядит следующим образом:
begin
assigndnput.*doors.in'):reset(input):
assignCoutput.'doors.out'):rewriteCoutput):
read(N):
while N<>-1 do
begin
if N-0
then writelnClO.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 — процедура построения графа;
Dykstra — процедура, реализующая поиск кратчайшего пути от вершины 0 до всех остальных вершин алгоритмом Дейкстры. Все найденные кратчайшие расстояния находятся в массиве D[i.A*N + 1].
Q[4 *N + 1] — кратчайшее расстояние от вершины 0 до вершины 4N + 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 координаты на каждую вертикальную стену.
Рассмотрим подробнее процедуру построения графа InitGraph:
procedure InitGr叩h:
var l.j : integer:
begin
for 1:-0 to (N-l)*4 do kG[i]:-4;
for 1:-4*(N-1)+1 to 4*N do kG[i]:-l:
for i:-l to N-l 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:
for i:-l to 4 do
begin
G[0.i]:-1;{Начальная вершина}
P[0.i]:-Dist(0.0.5.0.x[l].y[l.i]):
G[4*(N-l)+i.1]:-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(0,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 1:-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] — количество ребер из вершины:;
G[i,j] — номер вершины, которую с вершиной: соединяет ребро j;
P[i,j] — вес ребра от вершины i к вершине G[i,j].
Построение графа проводится в несколько этапов. Прежде всего формируем количества «обязательных ребер»: for i:-0 to (N-l)*4 do kG[i]:-4: for i:-4*(N-l)*l to 4*N do kG[i]:-l;
то есть каждая вершина, кроме вершин из последней стены, имеет по 4 ребра к вершинам следующей стены. Вершины последней стены связаны только с одной вершиной (последней). Затем формируем элементы массивов G и R
for 1:-1 to N-l 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, параметрами которой являются координаты точек (xl,yl) и (х2, у2) между которыми нужно найти расстояние:
function Dist(xl.yl.x2,)y2:real) :real; begin
Dist:»sqrt(sqr(xl-x2)+sqr(yl-y2)); end:
Затем добавляем в граф ребра от вершины 0 к ближайшим (ничем не заслоненным) вершинам 1-4. И одновременно добавляем в граф ребра от вершин из последней стены (4*(N 一 1) + i) до последней вершины (4 *N + 1).
for i:-1 to 4 do begin
G[0.i]:-i:{Начальная вершина}
P[0,i]:-Dist(0.0.5.0,x[l].y[l,i]): G[4*(N-1)+1,1]:-4*N+1;{Конечная вершина}
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(0,j.Dij);
Здесь используется функция Seen(i,j, Dij). которая отвечает на вопрос видна ли напрямую вершина j из вершины i, и, если ответ утвердительный, то в Dy возвращается кратчайшее расстояние от вершины i до вершины/
Также здесь используется процедура Add(i,j, Dy), которая добавляет в граф ребро от вершины i к вершине j с весом Dy,
Конкретную реализацию функции Seen и процедуры Add мы рассмотрим чуть позже.
Следующий шаг в процессе формирования графа — внесение в него ребер к последней вершине (4 *N + 1) от всех вершин графа, которые могут иметь преграды
на своем пути (если таковые имеются: Nn 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 к каждой вершине стены j, если они находятся в зоне прямой видимости. При этом i и j рассматриваются такими, чтобы между ними была по меньшей мере одна стена:
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-Hn.Dij)
then Add((i-l)*4+k.(j-l)*4+m.Dij):
Здесь, как и прежде, используются функция Seen и процедура ADD, Рассмотрим реализацию функции Seen:
function Seend ,jinteger: var Dij:real):boolean:
var
k.m : integer:
begin
Seen:-faIse:
if i-0
then begin ixl:-0: xl:-0: yl:-5.0; end
else begin
ixl:- ((i-1) div 4)+1; iyl:-(i 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-1) div 4)+1; 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 1x2-1 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:
Прежде всего по номерам вершин i, j находятся номера стен (irl и £г2) и относительные номера вершин в стене от 1 до4 (iyl, iy2) Затем находятся соответствующие реальные координаты вершин i(xl, у 1) и j(%2, у2). Отдельно обрабатываются номера вершин, не укладывающиеся в схему общей нумерации: начальная (0) и конечная (4*N+ 1).
Далее через найденные точки (xl, yl) и (х2, у2) проводится прямая Л*х + + В*г/+С = 0.
Затем для всех стен между стенами с этими вершинами проверяем, не закрывает ли какая-то из них «линию прямой видимости». Это делается следующим образом. Для горизонтальной координаты очередной стены (%[А]) выясняем на какой высоте проходит наша прямая
CurY:-(A*x[k]+C)/B
Если текущая высота не на уровне ни одной из дверей, то есть ниже нижней двери (CurY < y[kf 1]), или выше верхней двери (CurY > y[k, 4]), или между верхом нижней и низом верхней дверей ((CurY >y[kf 2]) и (CurY <y\kt 3])), то выходим из функции со значением Seen = false.
Иначе (если вершины i и j находятся в зоне прямой видимости) — вычисляем расстояние между ними с помощью функции Dist и выходим из функции со значением Seen = true.
Процедура Add добавляет очередное ребро в граф:
procedure Add(i .jinteger,Dij:real);
begin
inc(kG[i]): G[i.kG[i]]:-j: P[i.kG[i]]:-Dij:
end:•
Процедура Dejkstra, реализующая поиск всех кратчайших растояний на графе от вершины 0 до всех остальных реализована точно так, как при решении предыдущей задачи.
Полный текст решения задачи приведен в конце главы.
Задача «Борозды»
Всероссийская олимпиада по программированию, 1999
Борозды (lnput.txt / Output.txt / 10 с)
На паркетном полу физико-математической школы № 932 какой-то хулиган стамеской проделал несколько борозд, идущих параллельно стенам.
Администрация школы решила закрасить все борозды синей краской. Приготовив все необходимое для покраски, главный маляр задумался: а можно ли закрасить все борозды, не отрывая валика от пола и, более того, не закрашивая одну и ту же борозду дважды. Ваша задача будет состоять в том, чтобы определить, возможно ли такое окрашивание, и если это так, то вывести координаты какой-нибудь точки, с которой можно начинать покраску.
Ввод:
В первой строке входного файла находится число N(1 <N< 100) — количество борозд, проделанных хулиганом. За ним следуют 4N целых чисел — координаты концов каждой из борозд (-1000 ^х1,у1,х2,у2 < 1000).
Вывод:
В первую строку выходного файла выведите «NO», если такого окрашивания не существует, и «YES», если оно существует. В последнем случае во вторую строку выведите координаты точки, с которой можно начинать красить.
Пример вводаПример вывода
YES
363 13186
9 1 9 16
18 1 18 16
9 1 18 1
3696
9 6 18 6
3 13 18 13
9 16 18 16
Описание решения\
Задача к графам сводится следующим образом: вершины графа — точки (концы отрезков), ребра — наличие отрезка между соответствующими точками. Граф можно будет полностью обойти указанным в задаче образом (пройдя по каждому ребру РОВНО 1 раз) только при выполнении следующих двух условий/
Граф связный.
Степени всех его вершин четны (и тогда обход можно начинать с любой из его вершин) или степени ровно двух вершин нечетны (тогда обход можно начинать с любой из вершин с нечетной степенью).
Рассмотрим реализацию решения. Тело главной программы выглядит так: begin
InputData:
InitGraph;
Count(EvenV.xO.yO):
assignCoutput,'output.txt'); rewriteCoutput):
if Connected and ((EvenV-K) or (EvenV-K-2))
then begin writeln('YES'): writeln(xO.' *,yO) end
else writeln('NO'): close(output);
end.
Здесь:
InputData — процедура ввода исходных данных об отрезках;
InitGraph — процедура формирования графа;
Count — процедура, возвращающая количество вершин с четной степенью (Even) и координаты точки (хО, уО), с которой нужно начинать обход;
Connected — функция, возвращающая значение Me, если граф связный, и false — в противном случае;
口 К — количество всех вершин в графе (количество всех различных точек из тех, которые введены в качестве концов отрезков).
Рассмотрим детальнее процедуру InputData:
procedure InputData; begin
assigndnput. 'input.txf): reset (input): readln(N):
for i:-l to N do readln(x[2*i-1].y[2*i-1],x[2*i],y[2*i]); close(input):
end:
Все введенные точки удобнее хранить в виде пары массивов Хи У. Тогда процедура формирования графа выглядит следующим образом:
procedure InitGraph:•
begin
k:-2: AddNode(l.l); AddNode(2,2): AddEdge(1.2):
for i:-2 to N do begin
if not Found(2*i-l.vl)
then begin inc(k): AddNode(k.2*i-1): end:
if not Found(2*i,v2)
then begin inc(k): AddNode(k.2*i); end: AddEdge(vl,v2)
end:
end:
Здесь:
List — массив номеров различных точек из тех, что поданы на вход в качестве концов отрезков;
AddNode(itj) — процедура добавления к графу вершины с номером i (используя при этом входную точку с номером j).
Концы первого отрезка сразу заносятся в граф вместе с соответствующим ребром. Затем в цикле по всем остальным отрезкам делается следующее:
если первый конец отрезка отсутствует в графе, то он туда добавляется, d1 — номер найденной или добавленной вершины;
если второй конец отрезка отсутствует в графе, то он туда добавляется, и2 — номер найденной или добавленной вершины.
С помощью процедуры AddEdge в граф добавляется ребро между вершинами ^1 и必
Ниже представлена процедура AddNode:
procedure AddNoded,jinteger):
begin
List[i]:-j:
kG[i]:-O:
end:
Процедура AddNode запоминает в список List номер добавленной вершины и устанавливает степень добавленной вершины (kG[i]) равной нулю.
Ниже представлена процедура AddEdge:
procedure AddEdgeCi,j:integer):
begin
inc(kG[i]): G[i.kG[i]]:-j:
inc(kG[j]); G[j.kG[j]]:-i:
end:
Процедура AddEdge(ij) увеличивает степени вершин i иj и модифицирует граф G, внося ссылки из вершины i на вершину j и наоборот.
Ниже представлена функция Found. Она анализирует, есть ли в списке List вершина с координатами, равными координатам очередной точки (%[£], у[С])・ Если есть, то г получает значение, равное номеру этой точки в списке List, иначе и получает значение на 1 больше, чем количество точек в списке List.
function Found (t:integer: var v:integer):boolean;
begin
v:-l:
while (v<-K) and
((x[List[v]]<>x[t]) or
(y[List[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 1:-l to k do
if Odd(kG[i])
then begin xO:-x[List[i]]: yO:-y[List[i]] end
else inc(EvenV);
end.
Она возвращает EvenV — количество вершин с четной степенью, а также (хО, уО). Если все вершины имеют четную степень, то (хО, уО) — это координаты первой точки (в таком случае любая вершина графа является правильной начальной точкой).
Если же есть вершины с нечетной степенью, то (хО, уО) — координаты одной из них (в данном случае — последней).
Осталось рассмотреть функцию 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:-fa Ise;
for i:-l to N do
if Free[i] then exit:
Connected:-true:
end;
Это делается с помощью поиска в ширину от первой вершины. Если все вершины достигнуты (Free[i] ж false для всех вершин), то граф связный, иначе — нет.
Полный текст решения задачи приведен в конце главы.
4.3.4. Задача «N-Credible Mazes»
Greater New York Programming Contest, 2000
N-Credible Mazes (input.txt / output.txt / 10 с)
Точку в «-мерном пространстве назовем л-тересной, если все ее координаты —неотрицательные числа. Например (1, 2, 3) — 3-тересная точка (л-тересная в трехмерном пространстве. Две л-тересных точки назовем соседними, если их координаты отличаются ровно на 1 и только в одном измерении. Например (1, 2,3) — соседняя с (0, 2,3), (2, 2,3) и (1,2,4), но не соседняя с (2, 3, 3) или с (3, 2, 3).
Определим л-тересное пространство как коллекцию путей между соседними л-тересными точками. Наконец, л-тересным лабиринтом назовем л-те-
ресное пространство с указанными в нем двумя л-тересными точками — начальной и конечной.
Ввод:
Входной файл состоит из описаний одного или более лабиринтов. Первая строка описания всегда задает п — размерность пространства. В данной задаче п всегда меньше 10. Вторая строка содержит 2л неотрицательных чисел. Первые 勿 из них описывают начальную позицию начиная с меньшей размерности, а следующие п описывают конечную позицию. Далее идет неотрицательное количество строк, указывающих путь между соседними л-тересными точками в этом пространстве. Этот список завершается строкой, содержащей только число -1.
В одном файле может быть задано несколько таких описаний. Конец файла сигнализируется строкой, задающей 0 в качестве размерности пространства.
Вывод:
Для каждого лабиринта выводите его номер во вводе, например, для первого лабиринта выводите «Maze 1», для второго «Maze 2» и т. д. Если для этого лабиринта возможно добраться от начальной точки к конечной, выведите в той же строке «сап be travelled», иначе выведите «cannot be travelled».
Пример ввода:
2
0 0 2 2 0 0 0 1 0 10 2 0 2 12 12 2 2 -1
3
11112 3 112 113 113 12 3 111110 110 10 0 1 0 0 0 0 0 -1 0
Пример вывода'.
Maze #1 can be travelled
Maze #2 cannot be travelled
Описание решениям
К графу переходим очевидным образом: точки — вершины, отрезки — ребра. Требуется узнать, существует ли маршрут в графе от одной вершины до другой. Это можно сделать, например, поиском в ширину.
Расмотрим подробнее реализацию. Тело главной программы с учетом формата ввода (с несколькими тестами в одном входном наборе данных) выглядит следующим образом: begin
assigndnput.'input.txt'):reset(1nput): assignCoutput.'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.
Здесь:
InputDatalnitGraph — процедура, которая формирует граф непосредственно в процессе ввода исходных данных;
PathExists — функция, которая возвращает true, если путь существует, и false — в противном случае.
Рассмотрим подробнее процедуру InputDatalnitGraph', procedure InputDatalnitGraph: begin Take(VFrom): Take(Vto): K:-0: AddNode(l.Vfrom): AddNode(2.Vto): Take(i): while i<>-l do begin
Take(j): if not Foundd,vl) then AddNode(vl.i): if not Found(j,v2) then AddNode(v2.j): AddEdge(vl,v2);
Take(i) end:
end:
Здесь:
Take①—процедура ввода очередной точки N-мерного пространства и преобразования ее к соответствующему числу (по условиям задачи координаты — числа от 0 до 9);
AddNode(i,j) — добавить в граф вершину i, которая имеет соответствующее ей число 力
AddEdge⑺,и2) — добавить в граф ребро между вершинами d1 и d2;
Found(i, d) — поискать вершину с кодовым числом i в списке вершин графа 丄汲;
V— номер найденной вершины, если она существует, или номер на 1 больше имеющегося количества вершин в противном случае. Вершина <откуда идти» вносится в граф под номером 1, вершина «куда идти» вносится в граф под номером 2.
Рассмотрим процедуру Take:
procedure Take(var VFrom:longint); var
k.s.i.j : 1ongint: begin
k:-l; s:-0; Vfrom:--1: for i:-l to N do begin read(j): if j--l then exit; s:-s+k*j;
k:-k*10 end:
VFrom:-s; end:
В результат Vfrom попадает сумма взвешенных координат точки. Первая координата умножается на 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
begin
Get(i):
for j:-l to kG[i] do
if G[i.j]-2 then exit else if (Free[G[i.j]]) then Put(G[i.j]): end;
PathExists:=false:
end;
Здесь выполняется поиск в ширину от вершины 1 к вершине 2. Если вершина 2 достигнута, поиск прекращается. Если поиск завершен (очередь пуста), а вершина 2 так и не достигнута, значит, она недостижима.
Полный текст решения приводится в конце главы.
Задачи для самостоятельного решения
Задача «Door Мап»
South Central USA Programming Contest, 2002
Door Man (клавиатура / экран /5 с)
Вы — дворецкий в огромном доме. В нем так много комнат, что они просто перенумерованы (0, 1, 2, 3,...). Ваш хозяин постоянно оставляет двери открытыми.
Ваша задача — определить, существует ли такой путь, что: 1) дверь закрывается сразу после того, как вы в нее прошли; 2) закрытая дверь никогда не открывается; 3) завершается путь в той же комнате, где начинался (с номером 0), причем все двери уже закрыты.
В этой задаче вам дается список комнат и открытых дверей между ними. Нет необходимости определять маршрут, нужно только выяснить, существует ли он.
Ввод:
Ввод стоит из серии (до 100) тестов. Каждый тест содержит 3 компонента: ^стартовая строка ^START М N», где М обозначает номер комнаты, из которой необходимо начать обход, N — количество комнат в доме (1 ^N< 20); 2) список комнат с открытыми дверями (из N строк). В каждой строке для текущей комнаты приводятся номера открытых дверей в комнаты с большим номером. Например, если комната 3 имеет открытые двери в комнаты 1, 5, 7. то строка для комнаты 3 будет содержать числа 5 и 7. Первая строка представляет комнату 0, вторая строка представляет комнату 1, ит. д., последняя строка представляет комнату с номером (N — 1). Некоторые строки могут быть пустыми (в частности, последняя строка будет пустой всегда, поскольку имеет самый большой номер). В каждой строке номера дверей приводятся в порядке возрастания. Комнаты могут соединяться множеством дверей; 3) конечная строка - «END».
За последним набором данных идет строка «ENDOFINPUT». В одном наборе данных может быть не более 100 дверей.
Вывод:
Для каждого входного набора данных выведите ровно одну строку. Если существует требуемый путь — выведите «YESX», где 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 6 7 8 9 END
ENDOFINPUT
Пример вывода:
YES 1 NO YES 10
Задача «This Sentence is false»
South America Programming Contest, 2002
This Sentence is False (sentence.in / sentence.out / 25 c)
В руки секретной службы короля Xeon 2.4 попал документ с утверждениями вида «Предложение X истинно/ложно».
Вам приказано проверить, является ли этот набор предложений противоречивым. Если нет, то какое максимальное количество предложений в нем может быть истинным.
Ввод:
Входной файл содержит несколько документов. Каждый документ начинается со строки, содержащей целое число N, которое обозначает количество предложений
в документе (1 4 N 4 1000). Каждая из следующих Nстрок содержит одно утверждение. Утверждения нумеруются последовательно начиная с 1 в том порядке, в котором они появляются во вводе. Утверждения имеют вид ^Sentence X is true» или ^Sentence X is falser, где I sXwN. Значение 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
Задача «Will Indiana Jones Get There?»
South America Programming Contest, 2002
WШ Indiana Jones Get There? (get.in / get.out / 25 c)
Индиана Джонс находится в городе, разрушенном войной. Крыши всех домов снесены, стоят только части стен. В земле столько мин, что единственный безопасный путь — по уцелевшим стенам. Для перемещения между стенами Индиана Джонс пользуется доской от одной стены к другой. Его цель — спасти друга, попавшего в ловушку.
Начальные позиции Индианы Джонса и друга — в некоторых секциях стен. Стены расположены в направлениях Юг—Север или Запад—Восток.
Вам дана карта того, что осталось от города. Вы должны найти минимальную длину доски, требуемой Индиане Джонсу, чтобы добраться до друга.
Ввод:
Ваша программа должна обрабатывать несколько тестов.
Каждый тест начинается с целого числа N, указывающего количество стен, оставшихся в городе (2 4 NS 1000). Каждая из следующих Nстрок описывает стену.
Первая стена — та, на которой находится Индиана Джонс вначале. Вторая стена —та, куда он должен добраться. Каждая стена описывается тремя целыми числами X, У и L. Числа X и У определяют самую южную точку стены (для стен, идущих в направлении Юг—Север) или самую западную точку стены (для стен в направлении Запад—Восток).丄 определяет длину и направление стены: При L >0 стена идет с запада на восток, а при L < 0 —в направлении Юг-Север с длиной |L|. Концу ввода соответствует 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 О 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
Задача «I Hate SPAM, But Some People Love It»
South America Programming Contest, 2002
I Hate SPAM, But Some People Love It (spam.in/spam.out/25 c)
В наши дни, к сожалению, спамерские письма получают все большее распространение. В некоторых из них авторы просят вас переслать письмо всем
вашим друзьям. Некоторые такие письма желают вам удачи, другие обещают богатство, третьи просто напоминают вам, как важно сообщить вашим друзьям, что вы дорожите их дружбой. Вот пример спама:
От: Алиса
Кому: Боб, Мэри, Юлия, Джон
Привет, это письмо счастья. Я желаю тебе стать миллионером, но все зависит от тебя самого. Если ты
пошлешь это письмо 10 или более друзьям, ты станешь миллионером,
пошлешь это письмо 5 или более друзьям, ты станешь богатым,
пошлешь это письмо меньше чем 5 друзьям, ты станешь бедным, Алиса
Получив спам, люди реагируют одним из двух способов: либо удаляют такое письмо не читая (если ненавидят спам), либо пересылают такое письмо всем, кого знают (если любят спам).
В данной задаче мы предположим, что все люди любят спам, но никто не пересылает одно и то же письмо дважды. Каждое спам-письмо имеет уникальные свойства, зависящие от количества друзей, которым вы отправите его. Например, письмо может сообщать, что вы будете бедным, если пошлете его 5 друзьям, богатым — если 10, и самым богатым, если 20, и т. д.
В данной задаче мы будем рассматривать только спам-письма, подобные описанным. Точнее, спам-письмо будет определять две пороговых величины Т1 и 12 и три атрибута Л1.Л2 и АЗ.
Человеку присваивается атрибут (один из указанных трех) в зависимости от числа отправленных им писем для соответствующего спам-письма. Если он отправит Г писем, причем Г < Т1, он получает атрибут Л1, если Г1 < Г <72, он получает атрибут А2, иначе получает атрибут АЗ.
Вам даются имена группы людей и для каждого из них множество друзей, чьи электронные адреса он знает. Вам также дается множество различных спам-писем, и для каждого письма пороговые числа Т7, 72 и атрибуты Л/, А2, АЗ, а также персона, первая пославшая это письмо.
Вы должны написать программу, которая определит для каждого человека в группе, какие атрибуты он получит в зависимости от пересланных им спам-писем.
Вы можете полагать, что первый, кто отсылает спам-письмо, пошлет как минимум одно письмо, и что человек не шлет писем сам себе.
Ввод:
Ваша программа должна обрабатывать несколько тестовых случаев. Первая строка теста содержит целое число N, определяющее количество человек в группе (2 <N< 20). Во вводе люди обозначаются номерами от 1 до N. Каждая из следующих Nстрок содержат список друзей каждого человека. Список друзей персоны i описывает друзей, адреса которых он знает, и состоит из списка целых чисел Fi (i <Fi<Nt Fi^i); последнее в списке число равно 0.
После списка друзей идет описание спам-писем (не более 100). Каждое описание занимает отдельную строку и содержит целое число Р, указывающее номер человека —источника письма (2 4 Pw W); два целых числа П и 12 и три атрибутаЛ1, А2, АЗ (каждый атрибут — слово, стоящее не более чем из 20 символов).
Список описаний спам-писем заканчивается строкой, содержащей только число 0. Следующие N строк содержат имена — по одному слову в строке (длиной не более 20 символов). Имя в строке i — это имя человека с номером i.
Конец списка обозначается строкой с N = 0. Вывод:
Для каждого теста ваша программа должна вывести список имен с полученными атрибутами. Выводить нужно в порядке ввода в формате имя : атрибуты (в порядке разосланных ими писем)
Пример ввода: 5 2 3 0 1 3 5 4 0 5 О О 4 1 О 12 4 poor rich millionaire 5 3 10 sad normal happy 0 Bob Paul Mary Alice Julia 6 2 0 1 3 0 12 4 0 1 2 3 5 0 1 2 3 4 0 13 4 0 1 2 4 red green blue 12 4 dumb normal smart 6 3 5 ugly bad good 0 Peter Paul Victoria John
Julia Anne О О
Пример вывода.
Bob: rich sad Paul: millionaire normal Mary: 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.1. Текст программы к задаче «Тетраэдр» program by99d2tl; type
Table - array [1..8.1..4] of byte: const
MaxM - 10: {90}
T :Table -
((8.0
6.4).
(0.7
3.5).
(6.0
8.2).
(0.5
1.7).
(4.0
2.8).
(0.3
7.1).
(2.0
4.6),
(0.1
5.3));
MaxQ - MaxM*MaxM*MaxM;
var p.cp : arrayvar p.cp : arrayPwg QRPwg QR:array :array :array :array:array :array :array :array[0..MaxM*MaxM-l] of integer;[O..MaxM*MaxM-l] of byte:[0..MaxM*MaxM-l.l..3] of integer;[1..MaxQ.l..3] of integer;[1..4] of longint;[0..MaxM*MaxM-l] of integer;[O..MaxM*MaxM-l] of byte:[0..MaxM*MaxM-l.l..3] of integer;[1..MaxQ.l..3] of integer;[1..4] of longint;
var p.cp : array
var p.cp : array
Pw
g Q
R
Pw
g Q
R
:array :array :array :array
:array :array :array :array
[0..MaxM*MaxM-l] of integer;
[O..MaxM*MaxM-l] of byte:
[0..MaxM*MaxM-l.l..3] of integer;
[1..MaxQ.l..3] of integer;
[1..4] of longint;
[0..MaxM*MaxM-l] of integer;
[O..MaxM*MaxM-l] of byte:
[0..MaxM*MaxM-l.l..3] of integer;
[1..MaxQ.l..3] of integer;
[1..4] of longint;
i.S.D.M,j.a.TS.QEnd.QBegin.V.TV.CV.Last : longint:
procedure InputData:
begin
assigndnput. '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]);
closed nput):
end;
procedure OutResult:
begin
assign(output. 'output .txt'); rewrite (output):
writeln(Cp[D]):
close(output):
end;
procedure InitGraph;
begin
Pw[0]:-l: G[0,l]:-2:
for 1:-l to M-l 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:.
продолжение 夕
Листинг 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 PutAll(V.TV.CV:longint): var
NV. NTV. Dir. Base. NCV : Iongint: 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]); if NCV<cp[NV] then Put(NV.NTV.NCV): end:
end:
begin
InputData:
InitGraph;
QEnd:-0: QBegin:»!:
Put(S.TS.O): while (QBegin<-QEnd) do
begin
Get(V.TV.CV);
PutAll(V.TV.CV):
end:
OutResult: end.
Листинг 4.2. Текст программы к задаче «Стены»
program ioO2d2t3: const
MaxM -200:{Количествообластей}
MaxN -250:{Количествогородов }
MaxL -30:{Количествочленов клуба }
МахТ -MaxN;{Количествогородов на границеобласти}
MaxR -10;{Количествосмежных областейдлягорода}
GREAT- 255:
с : array [1..MaxL] of byte:k : array [1..MaxM] of byte:с : array [1..MaxL] of byte:k : array [1..MaxM] of byte:{ Города с членами клуба }
с : array [1..MaxL] of byte:
k : array [1..MaxM] of byte:
с : array [1..MaxL] of byte:
k : array [1..MaxM] of byte:
{ Количество городов на границе области}
t:array[1..MaxM.l..MaxT]ofbyte:{Города на границе области}
G:array[1..MaxM.l..MaxM]ofbyte;{Матрица инцидентности областей}
R:array[1..MaxN.l..MaxR]ofbyte;{Области, смежные с городом}
kR : array [1..MaxN] of byte:kR : array [1..MaxN] of byte:К-во областей, смежных с городом}К-во областей, смежных с городом}
kR : array [1..MaxN] of byte:
kR : array [1..MaxN] of byte:
К-во областей, смежных с городом}
К-во областей, смежных с городом}
M.N.L.i.j.x.NumM : byte:
MinS : longint;
procedure InputData:
begin
assigndnput. 'walls.in'); reset(input): read(M.N.L):
for i:-l to L do read(c[i]):
for i:-l to M 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 [1..MaxM] of byte:
Листинг 4.2 (продолжение)
s.p : byte;
begin
for 1:-l to M do
for j:-l to M do
begin
G[i.j]:-GREAT:
if i=j then continue;
for x:-l to M 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 M do 1nc(s.wl[x]*w2[x]):
if s>-2
then begin G[i.j]:-l; G[j,i]:-1: 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 M do
for 1:-l to M do
for j:-l to M do
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[1.j].G[i.x]+G[x.j]);
end;
end;
procedure FindBest;
var
CurS. BM : longint:
tc : byte:
begin
{ Строим массив город - список областей}
for i:-1 to M do kR[i]:-0:
for i:-l to M do{i - номер области}
for j:-l to k[i] do
begin
х :■ t[i.j]; inc(kR[x]);х :■ t[i.j]; inc(kR[x]);{х - номер города}{х - номер города}R[x.kR[x]]:-i: end:R[x.kR[x]]:-i: end:MinS:-maxiongint; NumM:-0; for i:T to M doMinS:-maxiongint; NumM:-0; for i:T to M do{ Цикл по областям}{ Цикл по областям}beginCurS:-0:for j:-l to L dobeginCurS:-0:for j:-l to L do{ Цикл по членам клуба}{ Цикл по членам клуба}begintc:-c[j]:BM:-GREAT:for x:-l to kR[tc] dobegintc:-c[j]:BM:-GREAT:for x:-l to kR[tc] do{ Город с членом клуба}{ Лучший для члена клуба}{ Город с членом клуба}{ Лучший для члена клуба}BM:-min(BM.G[r[tc.x].i]); inc(CurS.BM);BM:-min(BM.G[r[tc.x].i]); inc(CurS.BM);end;if CurS<MinSend;if CurS<MinSthen begin MinS:-CurS: NumM:-i end:then begin MinS:-CurS: NumM:-i end:end:end:end;procedure OutResult;begin assign(output.'walls.out*); rewrite(output): writeln(MinS): writeln(NumM); close(output);end;procedure OutResult;begin assign(output.'walls.out*); rewrite(output): writeln(MinS): writeln(NumM); close(output);end;end;beginInputData;InitGraph:Floyd:FindBest:OutResult;end.Листинг 4.3. Текст программы к задаче «Блокада»program spbOlb: constGREAT - 255:beginInputData;InitGraph:Floyd:FindBest:OutResult;end.Листинг 4.3. Текст программы к задаче «Блокада»program spbOlb: constGREAT - 255:var g dvar g d:array [32..255.32..255] of byte::array [32..255] of byte::array [32..255.32..255] of byte::array [32..255] of byte:
х :■ t[i.j]; inc(kR[x]);
х :■ t[i.j]; inc(kR[x]);
{х - номер города}
{х - номер города}
R[x.kR[x]]:-i: end:
R[x.kR[x]]:-i: end:
MinS:-maxiongint; NumM:-0; for i:T to M do
MinS:-maxiongint; NumM:-0; for i:T to M do
{ Цикл по областям}
{ Цикл по областям}
begin
CurS:-0:
for j:-l to L do
begin
CurS:-0:
for j:-l to L do
{ Цикл по членам клуба}
{ Цикл по членам клуба}
begin
tc:-c[j]:
BM:-GREAT:
for x:-l to kR[tc] 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);
BM:-min(BM.G[r[tc.x].i]); inc(CurS.BM);
end;
if CurS<MinS
end;
if CurS<MinS
then begin MinS:-CurS: NumM:-i end:
then begin MinS:-CurS: NumM:-i end:
end:
end:
end;
procedure OutResult;
begin assign(output.'walls.out*); rewrite(output): writeln(MinS): writeln(NumM); close(output);
end;
procedure OutResult;
begin assign(output.'walls.out*); rewrite(output): writeln(MinS): writeln(NumM); close(output);
end;
end;
begin
InputData;
InitGraph:
Floyd:
FindBest:
OutResult;
end.
Листинг 4.3. Текст программы к задаче «Блокада»
program spbOlb: const
GREAT - 255:
begin
InputData;
InitGraph:
Floyd:
FindBest:
OutResult;
end.
Листинг 4.3. Текст программы к задаче «Блокада»
program spbOlb: const
GREAT - 255:
var g d
var g d
:array [32..255.32..255] of byte:
:array [32..255] of byte:
:array [32..255.32..255] of byte:
:array [32..255] of byte:
Листинг 4.3 (продолжение)
pred:array[32..255]ofbyte:
diff:array[1..255]ofbyte;
M.N.i.j.K.A.p :byte:
sl.s2:string:
procedure InputDatalnitGraph: begin assigndnput, 'input.txt*): reset(input): readln(M.N); si:-''; 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:-1 to M 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+l 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[ord('A').i]V) and (i<>ord('A')) then begin
G[ord('A').i]:=GREAT:{Через А идти нельзя}
inc(K);{Смежные с А сразу включаем}
diff[К]:=i; end: close(input): end:
procedure Dijkstra: const
FREE - 0:
DONE - 1:
var
Lab : array
Bliz. MinD : begin
[32..255] of byte: byte;
{Инициализация данных}
for i:-33 to
255 do
begin
Lab[i]:-FREE;
d[i]:-G[32.i]: pred[i]:-32: end:
{расстояния от 0-й вершины до остальных}
d[32]:-0: Lab[32]:-DONE: pred[32]:-0;
{у нее нет предка}
for j:-33 to 255 dobegin
{Поиск ближайшей из необработанных}
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]+G[Bliz.i]: pred[i]:-B11z:
end:
end:
end;
procedure AddNew(j:byte);
var i : byte:
begin
1 :-l:
while (1<-K) and (diff[i]<>j) do Inc(i): if 1>K
then begin inc(K): diff[K]:-j; end: end:
procedure ZeroG:
Листинг 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]:-0; G[j.i]:-0;end:
G[pred[j].j]:-0:
G[j.pred[j]]:-0;
AddNew(j); j:-pred[j]; end: end;
procedure BadStop:
begin
assignCoutput.'output.txt*):rewrite(output);
writeln(-l):
close(output);
halt(O):
end:
begin
InputDatalnitGraph;
Dejkstra:
for i:-33 to 255 do
if (G[i .ordCA')]-!) and (i<>ord('A')) and (d[i]-GREAT)
then BadStop:
A:-K;
for p--l to A do
begin ZeroG: Dejkstra; end;
assignCoutput.'output.txt'):rewriteCoutput);
writeln(K):
close(output):
end.
Листинг 4.4. Текст программы к задаче «Мудрый правитель»
program spbOOc;
var
z. i. j. k. 1. p. q. n. m. best : longint:
a : array [1..8. 1..8, 1..8. 1..8] of longint;
s : array [1..8. 1..8] of longint;
procedure InputData:
begin
assigndnput. '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[iJ][k.l] :- 1;
If (i-k) and (j-1) then a[i.j][k.l] :- 0; end:
end;
procedure Floyd: begin
for p :- 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 jJCp.q] <> -1) and (a[p.q][k.l] <> -1) and ((a[i.j][k,U - -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
p :- 0;
for к :- 1 to 8 do
Листинг 4.4 (продолжение)
for 1 :- 1 to 8 do
if a[i,j][k.l] <- n
then p :- p + (1 shl (n - a[i.j][k.l])): if (p > best) and (p <- m) then best :- p: s[i,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;
Е - le-6:
var
G : array [1..MaxN.l..MaxN] of byte:
x.y.r:array[1..MaxN]ofreal:
Q:array[1..MaxQ]oflongint;
A:array[1..MaxN]ofreal;
i.j.u.v.N.K.m.QEnd.QBeg : longint:
W.WN:real:
Stop:boolean:
procedure InputDatalnitGraph;
begin
assigndnput,'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]:-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]:-l: 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[i]-l
then Put(i,WN)
else Stop:- abs(a[i]-WN)>E: end;
end:
procedure OutResults;
Листинг 4.5 (продолжение)
begin
assign(output,'output.txt'): rewrite(output);
if stop
then begin writelnC'DO NOT TURN IT ON!'); halt; end:
for i:-1 to N do
begin
a[i]:-trunc(a[i]);
if a[i]--l
then writelnCIDLE')
else writeln(a[i]:0:0):
end;
cl ose(output);
end:
begin
InputDatalnitGraph:
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 ■ 104®.
Очевидно, для получения полного решения требуется модифицировать представленное решение, реализовав «длинное деление и умножение» для вычисления по формуле WN: = W*r[v]/r[i]. Точнее, нужно реализовать длинное умножение и длинное деление на целые числа от 1 до 10, поскольку по условиям задачи радиусы валов — целые числа в таком диапазоне. Предоставля* ем это в качестве упражения пытливому читателю.
Листинг 4.6. Текст программы к задаче «Currency Exchange»
program nee4n01e;
const
MaxN - 100:
MaxR - 40:
FREE - 0;
DONE - 1:
var
kg: array [1..MaxN] of byte;
G: array [1..MaxN.l..MaxR] of byte;
P: array [1..MaxN.l..MaxR.l..2] of real:
color : array [1..MaxN.l..MaxR] of byte;
N.M.S.I.f.t : integer;
V.RAB.RBA.CAB.CBA.VNew : real;
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;
procedure InputDatalnltGraph:
begin
assignCinput.'Input.txt'): reset(input);
readln (N.M.S.V):
if V-0
then begin assign(output,'output.txt'):rewriteCoutput): wrlteln('NO'): close(outppt): halt: end:
for 1:-l to M do ReadAddEdges:
close(lnput);
end:
function CountCF.T:Integer: V:real):real:
begin
RAB:-P[f.t.l]: CAB:-P[f.t.2J:
Count:-(V-CAB)*RAB;
end:
procedure DFS(u:Integer; V:real): var
j : byte: VCur : real:
begin
for j:-l to kG[u] do
If color[u.j]-FREE
then
Листинг 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
InputDatalnitGraph;
VNew:-V:
DFS(S.V);
assignCoutput.'output.txt'):rewrite(output);
if VNew>V
then writeln('YES') else writeln('NO'): close(output); end.
Листинг 4.7. Текст программы к задаче «Exchange Rates»
program mc99c: const
MaxN » 60:
MaxR » 60;
van
kG:
:array [1..MaxN] of byte:
G:
:array [1..MaxN.l..MaxR] of byte:
P:
:array [1..MaxN.l..MaxR.l..2] of longint
Slist :
:array [1..MaxN] of string [20]:
s.sl.s2
:string:
i.k.kl.k2.nl.n2.KV : longint;
j : integer;
Floyded : boolean;
procedure ProcessName(s:stri ng: var i:longint); begin
i :-1:
while (i<-KV) and (SList[i]<>s) do inc(i);
if 1>KV
then begin inc(KV); SList[KV]:-s: end; end:
function min(a,b:longint)Jongint: 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,l]:-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:-pos(''.S):vaKcopy(s.l.k-l).nl.j);
delete(s.l.k):k:-pos(**.S):sl:-copy(s,l,k-l):
delete(s.l.k+2):k:-pos(*'.S):val(copy(s.l.k-l).n2.j);
delete(s.l.k);s2:-s:
ProcessName(sl.kl);
ProcessName(s2.k2):
Reduce(nl.n2);
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 do if (G[i,k]-1) and (G[k.j]-1) and (1<>j)
Листинг 4.7 (продолжение)
then begin
G[i.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;
P[i.j.2]:-n2: end:
Floyded:-true: end;
function Exist(kl.k2:longint: var nl.n2:1ongint):boolean; begin
Exist:- G[kl.k2]-1;
if (G[kl.k2]-1)
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(' '.$): 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)
then writeln(nl.' '.SList[kl],' - '.n2・’ '.SList[k2]) else writelnC? ' ,SList[kl].' - ? ' .SL:st[k2]);
end:
begin
assigndnput.'exchange.in'): reset(input);
assignCoutput.'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]-'•'then AddGraph else Answer:
readln(s);
end:
closed nput ):close(output): end.
Листинг 4.8. Текст программы к задаче «Sorting It All Out»
program ecnaOlb:
const
MaxN - 26:
MaxN2 - MaxN*MaxN:
var
s: array [1..MaxN2] of string[3]:
G:array [1..26.1..26]of byte:
a.b:array [1..26]ofbyte:
i.j.N.M:integer;
Sorted.Cycle :boolean:
procedure InitGraph;
begin
for i:-l to 26 do
for j:-l to 26 do G[i.j]:-0:
for i:-l to 26 do a[i]:-0:
for i:-l to 26 do b[i]:-0: end:
procedure AddEdged integer: var Sorted .Cycle: bool ean); var
p.k.q : integer:
begin
j:-ord(s[i .ID-ordC'A')+1:
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 1nc(a[p]):
G[p.k]:-1;
end:
for p:-l to N do if G[k.p]-1 then begin
Листинг 4.8 (продолжение)
if (G[j.p]-O) 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]-1):
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 Ristring; var
b: array [1..26] of byte:
.j.k.s.p : byte:
T: string:
begin
for j:=l to 26 do b[j]:-j:
for j:-l to N-l 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:・T+chr(b[订+ord('A')-l): R:-T:
end;
begin
assigndnput, 'input.txt'): reset(input): assignCoutput.'output.txt'); rewriteCoutput): readln(N.M); while (N<>0) do
begin
for i:-l to M do readln(s[i]);
InitGraph;
for i:-l to M do
begin
AddEdge(i.Sorted.Cycle);
if Cycle
then writeln('Inconsistency found after ' .1,' relations.'): if Sorted
then writeln('Sorted sequence determined after '.1.
'relations: '.R.'.'):
if Sorted or Cycle then break;
end:
if not (Sorted or Cycle)
then writeln('Sorted sequence cannot be determined.'): readln(N.M): end:
close(input): close(output): end.
Листинг 4.9. Текст программы к задаче «Проверка веб-страниц» program spb99c: const
MaxN - 100;
MaxG - 2*MaxN:
MaxR ■ 1000;
var
DocNames:array[1..MaxN*2]ofstring[32];
ReaLDocs:array[l..MaxN*2]ofboolean;
G:array[1..MaxG.l..MaxG] of byte:
s.p:string;
N.i.j.K.m.kl.k2.t : integer;
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⑴<>'')
then begin p:-p+s[i]: inc(j) end:
s:-p: end:
procedure AddNameCs:string: var m:integer):
begin
m:-l :
Листинг 4.9 (продолжение)
while (пк-k) and (s<>DocNames[m]) do inc(m): if m>k
then begin inc(k): DocNames[k]:-s: end; end;
procedure ReadDatalnitGraph:
begin
assigndnput. 'lnput.txt'): reset (input): readln(N):
for i:-l to 2*N do
for j:-l to 2*N do G[i.j]:-0;
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.k1): RealDocs[kl]:-true; readln(s); {<HTML>} UpCaseCompressStr(s);
while (s<>'<END>,) do begin
j:-pos('<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('”>'.s):
end;
readln(s):
UpCa seCompres sSt г(s):
end;
end:
close(input): end:
function DeadLinksinteger: begin
t :-0:
for j:-l to К do
if (not RealDocsCj])
then for i:-l to К do inc(t,G[i.j]): DeadLinks:-t:
end:
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]<>0) and (G[t.j]<>0) then G[i,j]:-1;
t:-0:
for i:-2 to k do
if (G[l.i]-0) and RealDocs[i] then inc(t);
UnReachDocs:-t:
end:
begin
ReadDatalnitGraph: assign(output.'output.txt'): rewrite(output): writeln(DeadLinks.' '.UnReachDocs); close(output):
end.
Листинг 4.10. Текст программы к задаче «Play On Words»
program ce99h; var
G: array [1..26.1..26] of byte:
kGin.kGOut:array[1..26]oflongint:
Letter:array[1..26]ofbyte:
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)-ord('a*)+1: while (c<>#13) do
begin
Листинг 4.10 (продолжение)
read(c):
end:
{дочитываем #10}{дочитываем #10}e:»ord(s)-ord('а*)+1:
{дочитываем #10}
{дочитываем #10}
read(c): end;
function CorrNumlnOut: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
IncCkGIn [i].G[j.i]):
1nc(kG0ut[i].G[i,j]):
end:
end:
CorrNumlnOut:-false;
LT:-O: GT:-O:
for i:-l to 26 do
if (Letter[i]-1)
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;
CorrNumlnOut:e (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]<>0) and (G[k.j]<>0) then G[i.j]:-l:
All:-0;
for i:-l to 26 do
if Letter[i]-1 then inc(All);
Connected:-true:
for i:-1 to 26 do begin
if (Letter[i]-0) then continue;
k:-0;
for j:-l to 26 do
if (Letter[j]-1) and (1<>j) and (G[i.j]<>0) then inc(k):
if к-Al 1-1 then exit: end:
Connected:-faIse: end;
begin
assign (input.'input.txt'); reset(1nput);
assign (output.'output.txt'): rewrite(output): readln(NT); for t:-l to NT do
begin readln(NW): for 1:-l to 26 do
for j:-l to 26 do G[i .j]:'-0;
for i:T 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 CorrNumlnOut and Connected then writeln('Ordering is possible.') else writelnC'The door cannot be opened.'): end;
close(input): close(output): end.
Листинг 4.11. Текст программы к задаче «Падение» program ceoi00t4: const
MaxN - 1000;
М2 - 2*MaxN+2;
MaxR - 2: var
xl.x2,H.b : array [1..MaxN+1] of integer:
pred: array [0..М2] of integer;
d: array [0..М2] of 1ongint:
Листинг 4.11 (продолжение)
kG:array[0..М2] of integer:
G:array[0. .M2.1..MaxR] ofinteger:
P:array[0..M2.1..MaxR] oflongint:
N.X.Y.MAX.i.j : integer:
procedure InputData:
begin
assigndnput, 'fall .in'): reset(1 nput);
readln(N.X.Y.MAX):
for i:-l to N do readln(xl[1].x2[i].H[i]): close(lnput):
end:
procedure SortPlatforms:
var
s.k : integer:
begin
for 1:-l to N do b[1]:-1:
for j:-l to N-l 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+l]:-0; xl[N+l]:-20000: x2[N+l]:-20000; end:
procedure Buildl(i.X.Y:integer);
begin
j:-((i+l) div 2)+1:
while ((X<xl[j]) or (X>x2[j])) and (Y-H[j]<-MAX) do inc(j): if Y-H[j]>MAX then exit:
G[i.kG[i]+l]:-2*j-l:
G[1.kG[i]+2]:-2*j :
P[i.kG[i]+l]:-(Y-H[j]) + (X-xl[jJ);
P[i.kG[i]+2]:-(Y-H[j]) + (x2[j]-X);
if j-N+1 then
begin
P[i.kG[i]+l]:-(Y-H[j]):
P[i.kG[i]+2]:-(Y-H[j]):
end:
inc (kG[i],2): end;
procedure Build2(i integer):
begin
Buildl(2*i-l.xl[i].H[i]);
Buildl(2*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..М2] of integer:
MinD. Bliz : 1ongint:
begin
{Инициализация данных}
for i:-0 to 2*N+1 do
begin
Lab[i]:-FREE;
pred[i]:-0:
d[i]:-maxiongint;
end:
d[0]:-0:
Lab[0]:-D0NE:
pred[0]:--l:{у нее нет предка}
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:-maxiongint;
Листинг 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:-1 to kGEBliz] do if Lab[G[Bliz.il]-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 [1..MaxN] of integer:
t,kp. NumP.Dir.Time.Last.Curr : integer; begin
assignCoutput.'fall.out'); 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].' '.Time.' '.Dir): Last:-Curr;
end:
close(output):
end:
begin
InputData:
InitGraph;
Dejkstra:
OutResults:
end.
Листинг 4.12. Текст программы к задаче *The Doors*
program mc96b: const
MaxN - 18:
М2 - MaxN*4+2:
MaxR - 100:
var
kG
:array
[0..М2] of integer:
G
:array
[0..M2.1..MaxR] of integer;
P
:array
[0..М2.1..MaxR] of real:
d
:array
[0..М2] of real:
pred
:array
[0..М2] of integer:
X
:array
[1..MaxN] of real;
У
:array
[1..MaxN.l..4] of real:
N.i.j.k.m : integer;
ixl.1x2,iyl.iy2 : integer;
xl.yl.x2.y2.A.B.C.CurY.Dij : real:
procedure InputData;
begin
for 1:- 1 to N do
begin
read(x[i]):
for j:-l to 4 do read (y[i,j]): end:
end:
function Di st(xl,y1.x2,y2:real):real:
begin
Dist:»sqrt(sqr(xl-x2)+sqr(yl-y2)):
end:
Листинг 4.12 (продолжение}
procedure Add(i,j:integer:Dij:real):
begin
1nc(kG[i]): G[i.kG[i]]:-j: P[i.kG[i]]:-Dij: end:
function Seend.j:integer: var Dij:real):boolean: var
k.m: integer;
begin
Seen:-faIse:
If i-0
then begin ixl:-0: xl:-0: yl:-5.0: end
else begin
ixl:- ((i-1) div 4)+1; iyl:-(i mod 4):
if iyl-0 then 1yl:・4:
xl:-x[ixl]: yl:-y[1xl.iyl]: end:
if j-4*N+l
then begin ix2:-N+l: x2:-10.0: y2:・5.0: end else begin
1x2:- ((j-1) div 4)+1: 1y2:-(j mod 4): if iy2-0 then iy2:・4;
x2:-x[ix2J: y2:-y[ix2.1y2J:
end:
A:-y2-yl:
B:-xl-x2;
C:-yl*(x2-xl)-xl*(y2-yl):
for k:-ixl+l to 1x2-1 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:
Di j:=Dist(xLyLx2.y2):
Seen:-true:
end:
procedure InitGr^ph:
var i.j : integer:
begin
for 1:-0 to (N-l)*4 do kG[i]:-4:
for 1:-4*(N-1)+1 to 4*N do kG[i]:-l;
for 1:-l to N-l 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: for 1:-l to 4 do begin
G[0.i]:-i;{ Начальная вершина }
P[0.i]:-Dist(0.0.5.0.x[l].y[l.i]);
G[4*(N-l)+i.1]:-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(0.j.Dij) then Add(0.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..М2] of integer;
Bliz : longint;
MinD : real:
begin
{Инициализация данных}
for i:-0 to 4*N+1 do begin
Lab[i]:-FREE:
pred[i]:-0:
d[i]:-maxlongint:
end;
Листинг 4.12 (продолжение)
d[0]:-0:
Lab[0]:-D0NE:
pred[0]:--l:{ у нее нет предка}
for i:-1 to kG[O] do d[G[0.1]]:-P[0.1]: {расстояния от 0-й вершины до остальных}
for j:-0 to 4*N+1 do begin
{ Поиск ближайшей вершины из необработанных} MinD:-maxiongint: for 1:-0 to 4*N+1 do
if (Lab[i]-FREE) and (d[1]<M1nD) then begin
MinD:-d[1]: B11z:-i end;
Lab[B11z]:-0one;
{ Пересчет кратчайших расстояний через ближайшую вершину}
for 1:-1 to kG[B11z] do if Lab[G[Bliz.i]]-FREE then 1f d[G[Bliz,i]]>d[bliz]+P[Bliz.1] then begin
d[G[Bliz.1]]:-d[bliz]+P[B11z,1]:
pred[G[B11z.1]]:-B11z:
end:
end:
end:
begin
assigndnput. 'doors.in'):reset(1 nput):
assign (output .'doors.out'): rewrite( output): read(N); while No-1 do
begin
If N-0 then writelnClO.0:0:2) else begin
InputData:
InltGraph:
Dejkstra:
writeln(d[4*N+l]:0:2):
end:
read(N):
end:
closednput): close(output): end.
Листинг 4.13. Текст программы к задаче «Борозды» program spb99tl; const
MaxN - 100:
MaxV - 2*MaxN:
MaxR - 100:
[1..MaxV] of integer:[1..MaxV.1..MaxR] of byte: EvenV : Integer: [l..MaxV] of byte: [1..MaxV] of boolean:[1..MaxV] of integer:[1..MaxV.1..MaxR] of byte: EvenV : Integer: [l..MaxV] of byte: [1..MaxV] of boolean:var x.y. List.kG :array
[1..MaxV] of integer:
[1..MaxV.1..MaxR] of byte: EvenV : Integer: [l..MaxV] of byte: [1..MaxV] of boolean:
[1..MaxV] of integer:
[1..MaxV.1..MaxR] of byte: EvenV : Integer: [l..MaxV] of byte: [1..MaxV] of boolean:
G:array
xO.yO.N.i.j.k.vl.v2.
Q:array
Free:array
BegQ. EndQ : integer;
procedure InputData:
begin
asslgn(input.'input.txt'): reset(input):
readln(N):
for i:-l to N do readln(x[2*i-1],y[2*1-1].x[2*1],y[2*1]): close" nput):
end;
procedure AddNode(i .jinteger);
begin
List[i]:-j;
kG[i]:-0:
end:
procedure AddEdgeCl.j:integer):
begin
1nc(kG[i]); G[1.kG[1]]:-j:
1nc(kG[j]): G[j.kG[j]]:-1: end;
function Found (t:integer: var v:Integer):boolean: begin
V:"l:
while (v<-K) and ((x[L1st[v]]ox[t]) or (y[L1st[v]]oy[t])) do 1nc(v):
Found :, v<-k: end:
procedure InitGraph:
Листинг 4.13 (продолжение)
begin
k:・2: AddNode(l.l): AddNode(2.2): AddEdge(l,2); ' for 1:-2 to N do
begin
if not Found(2*i-l.vl)
then begin inc(k): AddNode(k,2*i-l): end:
if not Found(2*1.v2)
then begin inc(k); AddNode(k.2*i): end:
AddEdge(vl.v2) end; end:
procedure Count(var EvenV.xO.у0: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 1 integer):
begin
i:-Q[BegQ];
inc(BegQ):
end:
function Connected:boolean:
begin
for 1:-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:
Connected:-faIse;
for 1:-l to N do
if Free[i] then exit;
Connected:-true:
end;
begin
InputData:
InitGraph:
Count(EvenV.xO.yO):
assignCoutput.'output.txt'): rewriteCoutput);
if Connected and ((EvenV-K) or (EvenV-K-2)) then begin
writeln('YES'):
writeln(xO.' '.yO) 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 [1..MaxV] of longint:
G: array [1..MaxV,1..MaxR] of byte;
N.I,j.k,vl,v2.VFrom.Vto,t : longint;
Q: array [1..MaxV] of longint:
Free: array [1..MaxV] of boolean;
BegQ. EndQ : longint;
procedure AddNoded.j:longint):
begin
List[i]:-j;
kG[i]:-0:
inc(K);
end:
procedure AddEdged. j:longint);
begin
inc(kG[i]): G[i.kG[i]]:-j;
inc(kG[j]): G[j.kG[j]]:-i;
end:
Листинг 4.14 (продолжение)
function Found (t:longint: var v:long1nt):boolean: begin
v:-l:
while (v<-K) and (L1st[v]<>t) do 1nc(v): Found :- v<-k;
end:
procedure TakeCvar VFromJonglnt): var
k.s.l,j:longint: begin
k:-l: s:-0: Vfrom:--1;
for 1:-l to N do begin read(j); if j—1 then exit: s:-s+k*j: k:-k*10
end:
VFrom:-s: end:
procedure InputDatalnitGraph: begin
Take(VFrom); Take(Vto);
K:-0: AddNode(l.Vfrom); AddNode(2.Vto):
Take ⑴: while i<>-l do begin
Take(j): if not Foundd,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:
Free[1]:-false: end:
procedure Get(var i Jongint):
begin
1:-Q[BegQJ:
1nc(BegQ): end:
function PathExIsts:boolean: begin
for 1 :-l to К do Freed]:-true:
BegQ:-l: EndQ:-0: Put(l): PathExIsts:-true: while (BegQ<-EndQ) do begin
Get(l): for j:-l to kG[i] do if G[1.j]-2 then exit else if (Free[G[i.j]J) then Put(G[i,j]): end:
PathExists:-false: end;
begin
assigndnput.'1nput.txt'):reset(1nput);
assign(output.'output.txt'):rewrite(output): readln(N); t:-0: while(N<>0) do
begin
1nc(t);
InputDatalnitGraph: if PathExIsts then wrltelnCMaze #* ,t.' can be travelled*) else wrltelnCMaze #*,t.* cannot be travelled'): readln(N); end:
closed nput); close (output): end.
Глава 5. Стратегические игры
Общий принцип решения таких задач заключается в следующем. Вводится множество состояний игры. Естественно, помечаются одно или несколько начальных выигрышных состояний. Далее работает цикл типа «Пока добавилось хоть одно выигрышное состояние». Просматриваются неопределенные еще состояния и предпринимаются попытки превратить их в выигрышные — разумеется, на основании правил игры.
Задача «Алиса и Боб»
NEERC, Западный четвертьфинал, 1998
Алиса и Боб (D.IN / D.OUT)
Дан ориентированный граф G 工(匕 Е), вершины которого являются позициями в следующей игре. В игре участвуют два игрока — Алиса и Боб. Они двигают фишку из позиции в позицию по дугам графа. Множество позиций V разделено на два подмножества А п В, А*В О, А + В ж V. В позициях А ход делает Алиса, а в позициях В — Боб. Если игра находится в позиции d из множества 匕 владелец этой позиции выбирает выходящую из V дугу (и, w) из Е и двигает фишку из и в 物 Игра начинается в некоторой позиции. Алиса выигрывает, если фишка оказалась в позиции С из V. Если за любое количество ходов фишка не попадает в эту позицию, то выигрывает Боб.
Требуется определить, какой игрок выигрывает, в зависимости от стартовой позиции при оптимальной стратегии обеих сторон.
Считаем, что У* {/,..., л} (1 <л < 1000).
Ввод:
Первая строка:
n m t
Здесь п — число вершин, т — число дуг, t — заключительная позиция.
Со второй строки записаны:
□ bl b2 K bn d1 о4 d2 w2 ... vm wm;
bi * 1, если i принадлежит A;
Ы ж 0, если i не принадлежит Л;
viwi — список дуг графа.
Здесь 加一начальная вершина, wi — конечная вершина соответствующей дуги (1 ^т^ 10 000). Все параметры — целые числа, разделенные пробелами.
Пример вводам
7 12 7
0 1 0 0 0 1 0
12 14 13
2 4 2 5
3 1 3 6 3 7
4 3 4 6
5 6 б 7
Вывод:
В текстовом файле D.OUTзаписать п целых чисел х1 х2... лп, rnexi = 1, если позиция i выигрышна для Алисы; иначе — xiж 0. В примере результат представляется строкой: 0 10 0 111
Описание решения:
Идея решения коротко может быть сформулирована следующим образом: все позиции в принципе разбиваются на два подмножества — победные для Алисы (назовем их выигрышными) и те, в которых выигрывает не Алиса, а Боб (назовем их проигрышными).
Из всех выигрышных позиций ход игры при любых ходах Боба и правильных ходах Алисы приводит к позиции С. Из всех проигрышных позиций при любых ходах Алисы и правильных ходах Боба ход игры НЕ ПРИВОДИТ в позицию С. Очевидно, что заведомо выигрышными являются те позиции, в которых ход Алисы и из которых есть НЕПОСРЕДСТВЕННАЯ дуга в состояние С. Отталкиваясь от тех позиций, которые являются заведомо выигрышными, итеративно строим ВСЕ остальные выигрышные позиции следующим образом: если в текущей позиции ход Алисы и есть хоть одна дуга в любую из позиций, уже объявленных выигрышными, то и текущая позиция объявляется выигрышной. Аналогично если в текущей позиции ход Боба и ВСЕ дуги ведут в позиции, возможно, различные, но ВСЕ уже объявленные выигрышными, то и текущая позиция объявляется выигрышной. Процесс пересмотра «невыигрышных» позиций продолжается до тех пор, пока добавилась хоть одна выигрышная позиция.
Этот алгоритм более формально может быть записан следующим образом:
Помечаем все состояния как проигрышные
Помечаем начальные выигрышные позиции
(ход А есть дуга в конечную позицию t)
Пока изменилось на выигрышное состояние хоть одной позиции Цикл по всем невыигрышным позициям
если ход А и существует хоть одна дуга в выигрышную позицию ИЛИ ход Б и ВСЕ дуги ведут в выигрышную позицию, то меняем состояние этой позиции на «выигрышная»
Рассмотрим одну из возможных реализаций решения данной задачи. По условиям задачи нам дан ориентированный граф из 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]-1) and ExistArc(i)) or ((B[i]-0) and AllArcs(i))) then begin S[i]:-l; Changed:-true; end: end: OutputData: end.
В главной программе вызываются следующие процедуры и функции:
InputData — для ввода и инициализации исходных данных;
OutputData — для вывода ответа;
ExistArc(i)- для выяснения того, существует ли дуга из текущей вершины : в выигрышную позицию;
AllArcs(i) — для выяснения ВСЕ ли дуги из текущей вершины i ведут в выигрышные позиции.
Рассмотрим их подробнее.
procedure InputData: begin
assign (input.'d.in'): reset(input): read (N.M.T);
for i:-1 to N do read (B[iJ);
for 1:-l to M 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] — содержит информацию о том, сколько дуг выходит из вершины f;
a[itj] — (для j от 1 до ka[i]) содержит номер вершины, в которую ведет из вершины i дуга с номером /
Вершина Г помечается как выигрышная.
Ниже приведена процедура OutputData:
procedure OutputData:
begin
assign (output.'d.out'): rewrite(output);
write (S[l]):
for i :-2 to N do writeC '.S[i]);
close(output):
end;
Состояния выводятся в соответствии с форматом вывода в одну строку (без пробела за последним состоянием). Далее приведена функция ExistArc:
function ExistArcd :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:
Вначале переменной ExistArc присваивается значение false. Затем просматриваются все дуги из вершины i. Если хоть одна из них ведет в вершину, которая уже объявлена выигрышной, то переменная ExistArc получает значение true и работа функции ExistArc завершается с возвращением значения true в качестве результата. Если же ни одна из дуг не ведет в выигрышную вершину, то значение переменной
ExistArc так и останется false, а функция ExistArc завершит свою работу с возвращением значения false в качестве результата.
Теперь рассмотрим функцию AllArcs:
function AllArcsd integer):boolean;
var j : integer;
begin
AllArcs:-true:
for j:-l to ka[i] do
if S[a[i.j]]-0 then begin All Arcs:-false; exit;
end:
end.
Вначале переменной AllArcs присваивается значение true. Затем просматриваются все дуги из вершины i. Если хоть одна из них ведет в проигрышную вершину, то переменная AllArcs получает значение false, а функция AUArcs завершает свою работу с возвращением false в качестве результата. Если же ни одна из дуг из вершины :не ведет в проигрышную вершину, то переменная AllArcs сохраняет свое первоначальное значение true, и функция AllArcs завершает свою работу, возвращая значение true в качестве результата. Полный текст решения задачи «Алиса и Боб» приведен в конце главы.
Задача «Ладья и конь»
Гомельская областная олимпиада по информатике, 2000
Ладья и конь (input.txt / output.txt)
Игра ведется на шахматной доске размером N х М (N — количество горизонталей, М — количество вертикалей). В игре участвуют ладья и конь. Первой ходит ладья. Ладья выигрывает, если ей удается побить коня.
Ограничения:
< N, М <8, конь ставится в такую начальную позицию, из которой он может сделать по крайней мере один ход (например при доске 3x3 конь не может оказаться на иоле 62, так как любой ход из этой позиции выводит за пределы доски).
Ввод:
Первая строка: NhM.
Вторая строка: позиции ладьи и коня (например, Ы с2 означает, что ладья стоит на второй вертикали и первой горизонтали, конь стоит на третьей вертикали и второй горизонтали. Вертикали обозначаются латинскими буквами от а до А, горизонтали —цифрами от 1 до 8.).
Вывода
Минимальное число ходов, нужное ладье, чтобы сбить коня, если это возможно, иначе должен быть напечатан 0.
Пример ввода:
3
Ы с2
Пример вывода,
3
Пояснения к примеру'.
Конь ловится за три хода:
ЛЫ-ЬЗ Kc2-al (1.... Кс2-аЗ 2.ЛЬЗ:аЗ).
ЛЬЗ-Ь2 Ка1-Ь3 (2.... Ка1-с2 3. ЛЬ2:с2).
ЛЬ2:ЬЗ.
Общая идея решения задачи такова: строим все состояния, в которых ладья выигрывает за один ход. Это состояния, в которых ладья и конь находятся на одной горизонтали или на одной вертикали.
Затем по ним строим все состояния, в которых ладья выигрывает в два хода. И так далее. По состояниям, в которых ладья выигрывает за Number ходов, строим состояния, в которых ладья выигрывает за Number + 1 ход. Если новые победные состояния построить не удается, то процесс завершается.
Правило построения новых победных состояний по старым таково: пусть ладья стоит в позиции (,, //), а конь стоит в позиции (ikt jk) и в такой позиция ладья сбивает коня не более, чем за Number холов при наилучшей игре коня.
Для построения ВСЕХ позиций, порождаемых данной, и в которых ладья сбивает коня не более чем за Number + 1 ход, строим все возможные поля, откуда сюда мог попасть конь за один ход. И для каждой такой позиции коня удостоверяемся, что ВСЕ ходы коня из этой позиции вели в позицию, в которой победа достигается не более чем за Number ходов.
Если такая позиция коня (х, у) найдена, то искомыми позициями (победными за не более чем Number + 1 ход) будут ВСЕ позиции вида:
(MovX, MovYt х, у),
где MovX и MovY такие, что ладья переходит из них в исходную позицию (UJI) ровно за 1 ход, то есть, находятся с позицией (〃, jl) на одной горизонтали или на одной вертикали.
Далее описывается одна из возможных реализаций предложенного алгоритма. Тело главной программы, решающей задачу, выглядит следующим образом: begin
InputData:
Changed:-true: Number:-0;
while Changed do
begin
Changed:-faIse:
Inc(Number):
for il:-l to M do{По всем состояниям}
for jl:-l to N do
for ik-1 to M do
for jk:-l to N do
if S[il.jl.ik.jk]-Number
then PutAllNewWinStatesdl ,jl ,ik. jk);
end:
OutputData: end.
Рассмотрим реализацию подробнее:
procedure InputData: var c : char; begin
assign (input.'input.txt'): reset(input):
readln (N.M):
read(c); li:-0rd(c)-0rd('a')+1: readdj); read(c):
read(c): ki:»0rd(c)-0rd('a*)+1; read(kj):
close(input):
for il:-l to M dofor jl:-l to N dofor ik:-l to M dofor jk:-l to N doSEil.jl.ik.jk] :-0:for il:-l to M dofor jl:-l to N dofor ik:-l to M dofor jk:-l to N doif ((il-1k) or (jl-jk)) and not ((il-ik) and (jl-jk))then SEil.jl.ik.jk] :-l;for il:-l to M dofor jl:-l to N dofor ik:-l to M dofor jk:-l to N doSEil.jl.ik.jk] :-0:for il:-l to M dofor jl:-l to N dofor ik:-l to M dofor jk:-l to N doif ((il-1k) or (jl-jk)) and not ((il-ik) and (jl-jk))then SEil.jl.ik.jk] :-l;{Все состояния проигрышны}{Все состояния проигрышны}{Выигрышные состояния за 1 ход} {если ход ладьи}{и конь на одной вертикали}{или на одной горизонтали}{с ладьей,}{но не в одной позиции}{Выигрышные состояния за 1 ход} {если ход ладьи}{и конь на одной вертикали}{или на одной горизонтали}{с ладьей,}{но не в одной позиции}
for il:-l to M do
for jl:-l to N do
for ik:-l to M do
for jk:-l to N do
SEil.jl.ik.jk] :-0:
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-1k) or (jl-jk)) and not ((il-ik) and (jl-jk))
then SEil.jl.ik.jk] :-l;
for il:-l to M do
for jl:-l to N do
for ik:-l to M do
for jk:-l to N do
SEil.jl.ik.jk] :-0:
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-1k) or (jl-jk)) and not ((il-ik) and (jl-jk))
then SEil.jl.ik.jk] :-l;
{Все состояния проигрышны}
{Все состояния проигрышны}
{Выигрышные состояния за 1 ход} {если ход ладьи}
{и конь на одной вертикали}
{или на одной горизонтали}
{с ладьей,}
{но не в одной позиции}
{Выигрышные состояния за 1 ход} {если ход ладьи}
{и конь на одной вертикали}
{или на одной горизонтали}
{с ладьей,}
{но не в одной позиции}
end:
В этой процедуре выполняются следующие действия.
Вводим исходные данные, преобразовываем их числовому виду (й,夕 ki,田);
Строим четырехмерный массив состояний:5[1..МахМ,1..МахЫ,1..МахМ,1..МахЫ\ (первые две координаты — позиция ладьи, вторые две координаты — позиция коня). S[iltjlf ikjk] — минимальное количество ходов для победы ладьи над конем из позиции ладья (i/J/), конь (iA,*); 0 означает, что победа ладьи недостижима.
Все состояния обнуляем. Заносим 1 в состояния, в которых ладья непосредственно может побить коня: ладья и конь стоят на одной горизонтали или на одной вертикали.
Changed:-true: Number:-0; while Changed do begin
Changed:-false; Inc(Number):
Устанавливаем признак «состояния изменялись»
Количество ходов до победы ладьи - Number - О
Пока состояния изменялись
Сбрасываем признак «состояния изменялись»
Инкрементируем Number - количество ходов до победы ладьи
for 11:-1 to М do{По всем состояниям}
for jl:-1 to N do for ik:-l to M do for jk:-l to N do if S[il.jl.ik.jk]-Number then PutAl1NewWinStates(11,jl.ik.jk);
По всем состояниям:
если текущее состояние — победное за Number ходов, то с помощью процедуры Put AllNew WinStates(il, % ikfjk) строим все состояния, победные за Number + 1 ход. Если нашли такое состояние, то устанавливаем признак Changed.
end:
OutputData:
procedure OutputData;
begin
assign (output.'output.txt'): rewrite(output):
writeln(S[li.1j ,ki ,kj]): close(output):
end:
После завершения цикла While выводим результат S[li, lj, ki, kj]. To есть, по построению, это минимальное количество ходов, требуемое ладье на позиции (А,夕) поймать коня начинающего на позиции (Ai, kj). Если ладья не может поймать коня, то ответ — 0.
Рассмотрим более подробно процедуру PutAUNewWinStates(il, jl, ikjk):
procedure PutAl 1 NewWinStatesdl.jl .ik.jk:integer): begin
for i:-l to 8 do
begin
CurrentX :- ik+steps[i.1]:
CurrentY :- jk+steps[i.2]: if (CurrentX>0) and (CurrentX<-M) and (CurrentY>0) and (CurrentY<-N) then PutlfAll(CurrentX.CurrentY); end: end;
Для всех возможных позиций {CurrentX, CurrentY), откуда конь мог попасть в позицию (iktjk)t вызываем процедуру PutlfAll^CurrentX, CurrentY) для нахождения состояний, которые победны за Number + 1 ход.
Рассмотрим подробней процедуру PutlfAll^ CurrentX,CurrentY)^
procedure PutlfAlKx.у:integer): var All. Was : boolean: begin
j:T: Was:-false: Al 1 :»true; while All and (j<-8) do begin
CurrentX :- x+steps[j.l]: CurrentY :- y+steps[j,2]: if (CurrentX>0) and (CurrentX<-M) and (CurrentY>0) and (CurrentY<-N) then begin if (S[il,jl.CurrentX.CurrentY]<-Number) and (S[il.jl.CurrentX.CurrentY]<>0) and not ((11-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 (Stilt.jl.x.y]-O) and (ilt<>il) then begin S[ilt.jl.x.y]:-Number+l: Changed:-true: end: for jlt:-l to N do if (S[il.jlt.x.y]-O) and (jlt<>jl) then begin
S[】l. j;t .x.y]:=Number+L
Changed -true.
6ПС1.
end:
end:
Для всех клеток коня, из которых можно было попасть в клетку (х, “), удостоверяемся, что был хоть один реальный ход и ВСЕ ходы вели в состояния, в которых победа одерживалась не более чем за N ходов.
Если это так, го все сос гояния, которые возникают из текущего одним ходом коня в (л; у) и одним ходом ладьи (по горизоннти или вертикали), объявляются победными за Number 十 1 ход.
Полный текст решения задачи приводится в конце главы.
Задача «Нечестная игра»
Командный чемпионат школьников Санкт-Петербурга 〃。программированию, 2000
Нечестная игра (input.txt / output.txt)
Петя придумает новую игру. На стол кладется кучка из Nспичек, затем Петя с Ваней по очереди берут спички из кучки. Первым берет Петя, ему разрешается взять от 1 до К спичек. Затем игрок может взять любое количество спичек, не более чем на 1 превышающее то количество, которое взял игрок перед ним (можно взять меньше или столько же, но обязательно хотя бы одну). Например, если ^^ 10,К= 5, то на первом ходу Петя может взять 1, 2,3,4 или 5 спичек; если Петя возьмет 3, то на следующем ходу Ваня может взять 1.2,3 или 4; и если Ваня возьмет 1, то Петя затем может взять 1 или 2, и т. д. Проигрывает тот, кто возьмет последнюю спичку.
Теперь Петя хочет рассчитать, какое количество спичек он должен взять на первом ходу, чтобы выиграть при любой игре Вани. Помогите ему.
Ввод:
В первой строке входного фаИла находятся числа Nh К, разделенные пробелом. (1 <X<N<200).
Вывод:
Выведите в выходной 巾айл все такие X, что, взяв на первом ходу X спичек, Петя выиграег. Если таких не существует, выведите в выходной файл единственное число - 0 . Числа следует разделять пробелами.
Пример ввода
Пример вывода
53
1
42
0
8 7
27
Это задача на рекуррентное заполнение двумерной таблицы состояний стратегической игры. Вводим состояния игры G[ifj]:
1, если всего спичек i и первый игрок берет j спичек и выигрывает;
0 — в противном случае (первый берет j спичек и проигрывает).
Понятно, что G[l, j] e 0 для всех/ то есть, если спичка одна, то всегда проигрывает первый игрок.
Для всех i от 2 до N
Для всех j от 1 до N (количество взятых спичек может расти
от игрока к игроку I!!)
j-ходы первого игрока
G[i.j]:-l - первый игрок выигрывает, если возьмет j спичек
Для всех p от 1 до j+1(p - ходы второго игрока)
если
G[i-j.p]-l
если при каком то ответе второго он возьмет р спичек
i-j - осталось спичек, после того как первый взял j спичек G[i-j.pJ"l - игрок выигрывает, если Осталось i-j спичек, он
его ход.
берет Р спичек
то
Затем вывод если такого
G[i.j]-0 первый игрок проиграл, раз второй хоть в одном варианте.
результата - номера j. для которых G[N.J]-1 номера ни одного нет. то вывести 0
выиграл
т:»0:
for j:-l to К do
if G[N.j]=l then begin writeCj.' '); inc(m): end:
if m*0 then writeln(O):
Для общности циклов надо определить G[Ofj]二 1 для всех/ Решение поставленной задачи может выглядеть следующим образом:
program aj_OO_f:
var
G: array[O..202.1..202] of byte:
N.K.i.j.p.m : longint;
begin
assigndnput.' input.txt'); reset(input):
assign(output.'output.txt'): rewriteCoutput):
read(N.K):
for j *1 to N do begin G[0.j]:-l: G[l.j]:-0: end:
for i:-2 to N do
for J:-l to N do
begin
G[i.j]-1:
for p:-l to j+1 do
if G[i-j.p]"l then G[i.j]:-0: end;
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): close(input); close(output):
end.
Интуитивно понятно, что для выяснения результата необязательно заполнять всю таблицу G[i,/]. Поэтому можно использовать рекурсивное заполнение таблицы только для тех элементов, которые потребуются для данных в текущем тесте значений переменных. Положим G[i,j] равным:
0, если этот элемент таблицы еще не заполнялся;
1, если при параметрах itj выигрывает 1-й игрок;
2, если при параметрах i, j выигрывает 2-й игрок.
Процедура заполнения выглядит так:
procedure FillG(i.j:long1nt):
var р: 1ongint:
function min(a.b:longint):longint: begin
if a<b then min:-a else min:-b end
{если уже считали - выходим} end: {если спичек нет - победа}{для всех возможных ходов}{если уже считали - выходим} end: {если спичек нет - победа}{для всех возможных ходов}{Проверяем последствия}{если противник проиграл.}{Проверяем последствия}{если противник проиграл.}begin
{если уже считали - выходим} end: {если спичек нет - победа}
{для всех возможных ходов}
{если уже считали - выходим} end: {если спичек нет - победа}
{для всех возможных ходов}
{Проверяем последствия}
{если противник проиграл.}
{Проверяем последствия}
{если противник проиграл.}
if G[i.j]<>0 then exit:
if i-0 then begin G[i.j]:-l; exit;
for p:-l to mind. j) do
begin
FillG(i-p.p+l):
if G[i-p.p+l]»2
then begin G[i.j]:el: exit: end: {значит мы победили и выходим}
end;
{Противник ни разу не проиграл}{Противник ни разу не проиграл}{Проверить G[N,K]} 任спи проиграл -} {вывод 0 и останов} {для всех возможных ходов}{Проверить G[N,K]} 任спи проиграл -} {вывод 0 и останов} {для всех возможных ходов}{Заполняем матрицу}{Заполняем матрицу}G[i.j]:-2; end:
{Противник ни разу не проиграл}
{Противник ни разу не проиграл}
{Проверить G[N,K]} 任спи проиграл -} {вывод 0 и останов} {для всех возможных ходов}
{Проверить G[N,K]} 任спи проиграл -} {вывод 0 и останов} {для всех возможных ходов}
{Заполняем матрицу}
{Заполняем матрицу}
а главная программа — так:
for i:-1 to N do
for j:-l to К do G[i.j]:-0:
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):
if G[N-p.p+l]-2 then write(p.' '): {Если первый победил - выводим}
end:
Рекурсивное решение поставленной задачи (работающее значительно быстрее) приведено в конце главы.
Как играть победно?
Во всех трех предыдущих задачах от нас только требовалось разбить множество состояний игры на выигрышные и проигрышные, а затем для заданного состояния определить, является ли оно выигрышным или проигрышным.
А что, если от нас потребуется больше — то есть не только указать, можно выиграть или нельзя, и если можно, то за сколько ходов. Как указать ПОБЕДНЫЕ ХОДЫ — то есть какими именно ходами нужно побеждать. Оказывается, это возможно сделать, располагая уже построенной нами информацией о разбиении множества возможных состояний игры на помножества выигрышных-проигрышных состояний.
Вернемся для примера к задаче <Ладья и конь». Пусть теперь задача усложняется и требуется играть за ладью, то есть непосредственно воспроизводить ее победную стратегию. Это можно делать следующим образом: нам сообщают размеры игрового поля М и N. Мы строим разбиения позиций на победные за 1, 2, 3,... ходов и проигрышные.
Далее нам сообщают координаты ладьи и коня. По ннм мы определяем, каков результат данной игры при оптимальных стратегиях игры каждого из соперников. Если позиция проигрышная — «сдаемся».
Пусть позиция выигрышная за N ходов. Тогда мы просматриваем ВСЕ позиции, в которые может сделать ход ладья, и выбираем из них такую, чтобы она стала выигрышной за N — 1 ход при любом ответе коня (очевидно, что это возможно по построению разбиения позиций на подмножества).
Затем, получая любой ответ коня, снова выбираем такой ход ладьи, который приведет к победе за N — 2 хода при любом ответе коня, и т. д.
Рассмотрим задачи, которые в своей формулировке требуют победной игры.
Задача «Игра loiwari»
Международная олимпиада по информатике, 2001
Игра loiwari
Семейство игр <Мапса1а» с бусинами и лунками — среди старейших форм развлечений человечества. Эта задача определяет версию игры, специально разработанную для IOI. Игра ведется двумя игроками на круглой доске с семью лунками по границе. Кроме того, имеется банк для каждого игрока. Игра начинается распределением случайным образом 20 бусин по лункам
таким образом, чтобы в каждой лунке оказалось не менее 2 н не более 4 бусин. Игроки ходят по оч(*реди. Чтобы пойти, игрок выбирает непустую лунку л забирает все бусины из лунки в руку. Пока бусины в руке игрока еще есть, он просматривает бусины по часовой стрелке, начиная с только что опус- TcBuieii лунки, и выполняег следующие операции.
Если у игрока в руке больше одной бусины: если текущая лунка уже содержит 5 бусин, то он забирает одну бусину из лунки и кладет в свой банк, иначе — к:1адс1 одну бусину из руки в лунку.
Если у игрока в руке одна бусина: если текущая лунка содержит от 1 до 4 бусин, то игрок забирает все бусины из этой лупки и одну из своей руки и кладет в свой банк, иначе (то есть если лунка содержит 0 или 5 бусин), игрок кладет бусину из своей руки в банк соперника.
Игра заканчивается, когда после выполнения ходов все лунки становятся пустыми. Побеждает тот игрок, в банке которого больше бусин. Начинающий игрок всегда имеет выигрывающую стратегию. Вы должны написдть программу, которая играет в игру loiwari как начинающий игрок и побеждает. Оппонирующая npoipaMNfa играет оптимально, что дает ей шанс побеждать вашу программу.
Ввод и вывод'.
Ваша программа читает входные данные из стандартного ввода и выводит результаты в стандартный вывод.
Ваша программа — игрок 1, оппонирующая — игрок 2. Когда Иаша программа начинает свою работу, она вначале читает 7 целых чисел pl, р2, рЗ, р4, р5, рб, р7. Это начальные значения бусин в соответствующих лунках. Лунки пронумерованы от 1 до 7 по часовой стрелке.
Затем иачипается игра при и^-стых банках. Ваша программа должна играть следующим образом:
口 если сейчас ваша очередь хода, то ваша программа должна выводить в стандарта i>ui вывод номер лунки, выбранной на текущем ходу;
口 если сейчас ход противника, ваша программа должна считывать из стандартного ввода помер лунки, определяющей ход противника.
Средства'.
Вам дастся программа (ioiwari2 для Linux, ioiwari2.exe для Windows), которая играет oiiTHMiLibiio с одноЛ начальной позиции 4 3 2 4 2 3 2. Она начинает с того, что пишет в стандартный вывод эту начальную позицию, которую ваша проц)ам- ма должна считать. После эгого программа нграет в игру, считывая ходы первого игрока из стандартного ввода и записывая собственные ходы в стандартный вывод. Вы можете запустить свою программу и Ioiwari2 в различных окнах и передавать ответы от npoi-раммы к nporpaAizc вручную. Ioiwari2 записывает диалог в файл loiwari.out.
Инструкции 〃о программированию'.
В примере, приведенном далее, вы читаете последнее число из стандартного ввода в переменную las* а переменная mymove содержит ваш ход.
Если вы программируете на Паскале, вы должны запрЬп)аммировать ввод-вывод следующим образом:
Writeln(mynrave):
Readln(last):
Пример:
Таблица 5.1. Корректная последовательность из 6 ходов
Лунки
1.
2.
3.
4.
5.
6.
7.
Bankl
Bank2
Начальная ситуация
4
3
2
4
2
3
2
0
0
Ход 1 -го игрока
2
4
0
3
5
0
3
2
3
• 0
Ход 2-го игрока
3
4
0
0
4
1
4
0
3
4
Ход 1-го игрока
5
4
0
0
4
0
0
0
8
4
Ход 2-го игрока
4
0
0
0
0
1
1
1
8
9
Ход 1-го игрока
5
0
0
0
0
0
0
1
10
9
Ход 2-го игрока
7
0
0
0
0
0
0
0
11
9
Оценивание:
Если ваша программа выигрывает тест, вы получаете за него 4 очка; играет вничью —получаете 2 очка проиц)ывает — получает 0 очков.
Описание решения:
Фактически в данной задаче состояние игры представляют 9 чисел — содержимое семи луиок и двух банков.
Прежде всего; научимся делать один ход: Move(who, А, р, bank);
Здесь переменная who определяет номер игрока, который делает ход, k — номер лунки, с которой он начинает, р — массив из семи чисел (количеств бусин в лунке с соответствующим номером), bank — массив из двух чисел (количество бусин в банке у каждого игрока).
var nk : byte;beginruka-P[k]: P[k]:-0:nk :-k:while ruka<>0 dobeginnk:-Next(nk):if ruka>lthenif p[nk]-5thenbegindec(p[nk]):var nk : byte;beginruka-P[k]: P[k]:-0:nk :-k:while ruka<>0 dobeginnk:-Next(nk):if ruka>lthenif p[nk]-5thenbegindec(p[nk]):Procedure Move(who.k:byte:van p:tp: var bank:tb);
var nk : byte;
begin
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]):
var nk : byte;
begin
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]):
{взяли в руку все бусины их лунки с номером К} {nk - номер обрабатываемой лунки}
{Если бусин в руке > 1}
{а в лунке - 5 бусин.}
{забираем бусину из лунки}
inc(bank[who]); end
else
begin
dec(ruka):
{кладем ее в свой банк}{в лунке - не 5 бусин}{1 бусину из руки}{в лунку}{кладем ее в свой банк}{в лунке - не 5 бусин}{1 бусину из руки}{в лунку}elseif ruka-1thenif p[nk] in [1..4] thenbegininc(bank[who],p[nk]): p[nk]:-0:inc(bank[who]): dec(ruka):endelsebegindec(ruka):inc(bank[3-who]): end:elseif ruka-1thenif p[nk] in [1..4] thenbegininc(bank[who],p[nk]): p[nk]:-0:inc(bank[who]): dec(ruka):endelsebegindec(ruka):inc(bank[3-who]): end:{Если в руке 1 бусина}{Если в руке 1 бусина}{Добавляем в свой банк} {Все бусины из лунки} {Добавляем в банк бусину} {Из своей руки}{Добавляем в свой банк} {Все бусины из лунки} {Добавляем в банк бусину} {Из своей руки}{Из своей руки}{Добавляем в банк соперника}{Из своей руки}{Добавляем в банк соперника}inc(p[nk]): end
{кладем ее в свой банк}
{в лунке - не 5 бусин}
{1 бусину из руки}
{в лунку}
{кладем ее в свой банк}
{в лунке - не 5 бусин}
{1 бусину из руки}
{в лунку}
else
if ruka-1
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:
else
if ruka-1
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:
{Если в руке 1 бусина}
{Если в руке 1 бусина}
{Добавляем в свой банк} {Все бусины из лунки} {Добавляем в банк бусину} {Из своей руки}
{Добавляем в свой банк} {Все бусины из лунки} {Добавляем в банк бусину} {Из своей руки}
{Из своей руки}
{Добавляем в банк соперника}
{Из своей руки}
{Добавляем в банк соперника}
end:
{WriteState(p.bank);} end:
Процедура WriteState(pf bank) (вместе с процедурами InputData и TestDebug) используется для отладки процедуры одного хода по тестовому примеру:
procedure WriteState(p:tp:Bank:tb):
begin
for i:-l to 7 do write(p[i],''):
write (bank[l].' '.bank[2]):
writein;
end;
procedure InputData;
begin
for i:-l to 7 do read(p[i]):
end;
Procedure TestDebug:
begin
Moved.2.p,bank): Move(2,3.p.bank):
Moved,5.р.bank); Move(2.4.p.bank);
Moved,5.р.bank): Move(2.7.p.bank); end:
Заметим, что для отладки мы вводим в качестве исходных данных p[i] числа 4 3 24 232.
Последовательность ходов игроков тоже берем из тестового примера:
Ход 1-го игрока: 2
Ход 2-го игрока: 3
Ход 1-го игрока: 5
Ход 2-го игрока: 4
Ход 1-го игрока: 5
Ход 2-го игрока: 7
Убедившись, что выводимое содержимое лунок и банков в точности соответствует данным из тестового примера, содержательную часть данной версии игры «Мап・ cala> можно забыть навсегда. Более того, все последующие рассуждения справедливы для всего семейства игр «Mancala», получающегося модификацией правил перемещения бусин между лунками, рукой и банками.
Тело главной программы может выглядеть следующим образом:
beginInputData;while SumP(p)<>0 dobeginCalculated .p.bank): writelnd):Moved.i .p.bank);if SumP(p)<>0thenbegin readln(j); Move(2.j.p.bank)end:end: end.beginInputData;while SumP(p)<>0 dobeginCalculated .p.bank): writelnd):Moved.i .p.bank);if SumP(p)<>0thenbegin readln(j); Move(2.j.p.bank)end:end: end.{TestDebug; WriteState;}
begin
InputData;
while SumP(p)<>0 do
begin
Calculated .p.bank): writelnd):
Moved.i .p.bank);
if SumP(p)<>0
then
begin readln(j); Move(2.j.p.bank)
end:
end: end.
begin
InputData;
while SumP(p)<>0 do
begin
Calculated .p.bank): writelnd):
Moved.i .p.bank);
if SumP(p)<>0
then
begin readln(j); Move(2.j.p.bank)
end:
end: end.
{Пока в лунках есть бусины}
{Вычисляем i - очередной выигрывающий ход}
{Выводим его}
{Пересчитываем состояние лунок}
{Если в лунках есть бусины}
{Вводим J - ответ соперника}
{Пересчитываем состояние лунок}
Алгоритм работы главной программы прокомментирован в ее тексте. Из этого алгоритма вызываются функция SumP(p) и процедура Calculate^, р).
Функция 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;
Осталось рассмотреть процедуру Calculate.
procedure CalculateCvar il:byte:p:tp:bank:tb);
var
i: byte: begin
While ((p[i]-0) or (Not WinKi .p.bank))) and (i<-7)
do inc(i):
il:-i:
end:
Процедура Calculate получает на вход состояние лунок в массиве р, выбирает очередной оптимальный ход за 1-го игрока и передает его в вызывающую программу через параметр il.
Для выяснения оптимального хода она перебирает все возможные ходы от 1 до 7 (если лунка пустая, то такой ход не рассматривается) и вызывает функцию Winl (:,р, bank), которая отвечает на вопрос — выиграет ли первый игрок, если пойдет ходом: (выберет лунку с номером i) при текущем состоянии лунок р.
Рассмотрим подробнее текст функции Win\\
function Winl(Hole:byte:p:tp;bank:tb): boolean:
var i : byte: Res:boolean:
begin
Moved, 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). Для выяснения факта поражения соперника при ВСЕХ возможных ответах вызывается функция Lost2(i,p)3 которая получает значение истина, если второй игрок проиграет, пойдя ходом i при текущем состоянии лунок р.
Рассмотрим теперь подробнее функцию Lost2(i, р, bank):
function Lost2(Hole:byte:p:tp:bank:tb):boolean:
var i : byte: Res : boolean;
begin
Move(2.Hole.р.bank);
if SumP(p)-0
then Lost2:- Bank[l]>Bank[2] else
begin
Res:-faIse:
for i:-l to 7 do
if (p[i]<>0) then Res:-Res or WinKi.p.bank):
Lost2:-Res:
end;
end:
Вначале делается указанный в параметре ход за второго игрока и пересчитывается новое содержание лунок. Если лунки пустые и в банке у первого игрока больше бусин, то второй игрок проиграл. Иначе второй игрок проиграет, если хотя бы при одном своем ответе первый игрок выиграет.
Внимательный читатель заметил, что мы имеем связку из двух рекурсивных функций Wini и Lost2t которые вызывают друг друга.
Но для вызова функции из другой она должна быть прежде определена! В этом случае нам помогает директива компиляции forward.
function Winl(Hole:byte:p:tp:bank:tb):boolean:forward:
Полный текст программы io01dlt2> решающей поставленную задачу, приводится в конце главы.
Задача «Игра Score»
Международная олимпиада по информатике, 2001
Игра Score
Score — это настольная игра для двух игроков, которые двигают на доске одну и ту же фишку с одной позиции на другую. Доска имеет N позиций, пронумерованных от 1 до N, и множество дуг, ведущих от одной позиции к другой. Каждая позиция принадлежит первому или второму игроку, которого мы назовем владельцем этой позиции. Кроме того, каждая позиция имеет положительную цену, причем все цены различны. Позиция 1 — начальная. В начале игры оба игрока имеют счет 0. Игра проводится по следующим правилам. Обозначим С позицию, в которой находится фишка в начале хода. В начале игры позиция С — это 1. Шаг игры состоит из следующих операций.
Если величина С больше, чем текущий счет владельца С, то величина С становится новым счетом владельца С. Иначе счет владельца С остается тем же. Счет другого игрока не изменяется в любом случае.
Владелец С выбирает одну из дуг из текущей позиции фишки, и окончание этой дуги задает новую текущую позицию фишки. Заметим, что игрок может сделать несколько последовательных ходов.
Игра завершается после того, как фишка возвратится в исходную позицию. Побеждает игрок, имеющий в конце игры больший счет.
Дуги всегда организованы так, что выполняются следующие условия:
из любой текущей позиции имеется хотя бы одна дуга;
каждая позиция Р достижима из стартовой позиции, то есть существует последовательность дуг от стартовой позиции к позиции Р;
гарантировано завершение игры через конечное число ходов.
Напишите программу, которая играет в эту игру и побеждает. Все позиции, которые предъявляются вашей программе, таковы, что победа возможна вне зависимости от того, делаете ли вы первый ход. Программа противника играет оптимально, и это дает ей шанс выиграть игру у вашей программы.
Ввод и вывода
Ваша программа читает входные данные из стандартного ввода и пишет их в стандартный вывод. Ваша программа — Игрок 1, а программа жюри — Игрок 2. Когда ваша программа начинает работать, она должна считать следующую информацию из стандартного ввода.
Первая строка содержит одно целое число: количество позиций М 1 <N< 1000. Каждая из следующих N строк содержит N целых чисел с информацией о дугах. Если есть дуга из позиции i в позицию/ то j・e число в i-й строке из этих N строк равно 1, иначе — 0.
Следующая строка содержит N целых чисел — владельцев позиций. Если позиция i принадлежит игроку 1 (то есть вам), то i-e число равно 1, иначе — 2.
Следующая строка содержит Nцелых чисел — цены позиций. Если i-е целое число есть j, то цена позиции i равна j. Все эти величины j удовлетворяют свойствам: 1 ^j^Nn все цены jразличны.
Игра начинается с положения фишки в позиции 1. Ваша программа должна играть, как описано ниже, и закончить игру, когда фишка вернется в позицию 1.
Если сейчас очередь хода вашей программы, то она должна написать номер следующей позиции Р, 1 <P<Nb стандартный вывод.
Если сейчас очередь хода вашего противника, то ваша программа должна прочитать номер следующей позиции Р, 1 W Р 4 N из стандартного ввода.
Рассмотрим следующий пример (доска представлена на рис. 5.1).
Позиции, помещенные в круглые скобки, принадлежат игроку 1, а позиции в квадратных скобках принадлежат игроку 2. Каждая позиция имеет собственную цену, записанную внутри скобок, и номер позиции рядом. Игра, которую предстоит сыграть, представлена далее.
14
(1)4——И
Рис. 5.1. Пример игрового поля для задачи «Игра Score»
Таблица 5.2. Игра и пояснения к ней
Stdin
StdoutПояснения
4
0 100
00 1 1
000 1
1000
1122
1342
N
Информация о дугах из позиции 1 Информация о дугах из позиции 2 Информация о дугах из позиции 3 Информация о дугах из позиции 4 Владельцы позиций
Цены позиций
2Игрок 1 ходит
4Игрок 1 ходит
Игрок 2 ходит в начальную позиции^ — п“ю окончена
По завершению игры игрок 1 имеет счет 3, игрок* 2 имеет счет 2. Игрок 1 побеждает.
Инструкции по программирова нию:
В нижеследующем примере target — это целая переменная для позиции. Если вы программируете на Паскале, вы должны так организовать ввод и вывод:
Readln(target);
Writeln(target):
Средства'.
Вам дается программа (score2 для Linux, score2.exe для Windows). Эта программа читает описание игры из файла score.in в вышеописанном формате. Программа пишет эту информацию в стандартный вывод в том же формате. Этот вывод может быть использован для ввода в вашу программу в тестовых целях. После этого программа играет со случайно!! стратегией, читая ходы вашей программы из стандартного ввода и записывая собственные ходы в стандартный вывод.
Оценивание:
Для каждого теста, если вы выиграли, вы получаете все баллы, иначе — 0 баллов. Ввод и вывод вашей программы сохраняются. Затем ваша программа исполняется второй раз, с вводом, перенаправленным из файла, и учетом официального
времени на тест. Программа должны производить те же самые результаты, что и при первом запуске.
Тело главной программы выглядит следующим образом:
beginInputData;Calculate:cv:-l:While true dobeginif v[cv]-lthen beginNext(cv): writeln(cv): end else readln(cv): if cv-1 then exit: end:end.beginInputData;Calculate:cv:-l:While true dobeginif v[cv]-lthen beginNext(cv): writeln(cv): end else readln(cv): if cv-1 then exit: end:end.{Ввод исходных данных} {Подготовка к работе} {Текущая вершина}
begin
InputData;
Calculate:
cv:-l:
While true do
beginif v[cv]-lthen begin
Next(cv): writeln(cv): end else readln(cv): if cv-1 then exit: end:
end.
begin
InputData;
Calculate:
cv:-l:
While true do
beginif v[cv]-lthen begin
Next(cv): writeln(cv): end else readln(cv): if cv-1 then exit: end:
end.
{Если ход мой}
{Выбрать ход} {Передать его жюри}
{Ход соперника}
{Ввести ход от соперника}
{Если вернулись в вершину 1 - конец}
Главая программа очевидна и хорошо самодокументирована. Осталось разобрать содержание вызываемых из главной программы процедур InputData. Calculate и Next(cv). Начнем с InputData
procedure InputData: begin
readln(N);
for 1:-l to N do
for j:-l to N do read (a[i,j]);
for i:-l to N do read(v[i]);
for 1:-l to N do read(c[i]): end;
Здесь:
ON— количество позиций;
а — граф игры (a[i,j]工 1, если есть дуга из вершины i в вершину j);
и — владельцы позиций v[i] * 1 или * 2;
с — цены позиций, от 1 до N, все различны.
Теперь рассмотрим Calculate.
procedure Calculate:
var
i : byte;
begin.
for i:-l to N do Sorted[c[i]]:-i:
for i:-1 to N do for j:-l to N do Free[i.j]:-true: end:
Строка условия задачи «1 4 j 4 N, и все цены j различны» дает нам замечательно простой способ отсортировать вершины (позиции) по возрастанию цен:
for i:-1 to N do Sorted[c[i]]:-i:
To есть в массиве Sorted на позиции k мы храним номер позиции (вершины), цена которой равна А. Кроме того, мы подготавливаем к последующим расчетам массив пометок использованых дуг графа: Free[i, j] = True, если дуга из i в j еще не использовалась.
И, наконец, процедура Nextg) по текущей вершине cv вычисляет следующую вершину си (в случае хода первого игрока):
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
FreeEcv.Sorted[i]]:-False: cv:-Sorted[i]; exit; end;
for i:«1 to N do if (a[cv.Sorted[i]]-l) and (v[Sorted[i]]-2) and FreeEcv.Sorted[i]] then begin
Free[cv.Sorted[i]]:-False; cv:-Sorted[i]: exit: end: end:
Идея процедуры Next проста. Вначале мы перебираем дуги в принадлежащие нам вершины по УБЫВАНИЮ цены (чтобы лучшую взять первой).
Если из текущей вершины нет дуги в нашу вершину, то затем перебираем дуги в принадлежащие сопернику вершины в порядке возрастания цены с тем, чтобы худшую взять первой.
Полный текст решения данной задачи приведен в конце главы.
Задача «Игра-2»
Международная олимпиада по информатике, 1996
Игра-2 (input.txt / output.txt)
Рассмотрим следующую игру между двумя игроками. На игровой доске записана последовательность положительных целых чисел. Игроки ходят по очереди. Ход заключается в том, что игрок выбирает число, расположенное на левом или на правом конце последовательности; выбранное число стирается с доски. Игра заканчивается, когда на доске не остается чисел. Первый игрок выигрывает, если сумма выбранных им чисел не меньше, чем сумма чисел, выбранных вторым игроком. Второй игрок играет наилучишм образом. Первый игрок всегда ходит первым.
Известно, что если в начале игры на доске записано четное количество чисел, то у первого игрока есть выигрышная стратегия. Требуется написать программу, которая реализует выигрышную стратегию первого игрока.
Ответы второго игрока выдает имеющийся в вашем распоряжении модуль Play. В модуле реализованы три процедуры: StartGame, Му Mod% YourMove. В самом начале ваша программа должна иницилизировать игру, вызвав процедуру StartGame (без параметров). Когда программа делает очередной ход, она выполняет инструкцию A^Moue(Z) или MyMoue('R) в зависимости от того, какое число она выбирает — левое или правое. Чтобы получить ответ второго игрока, ваша проц)амма должна выполнить инструкцию YourMove(C)t где С — переменная символьного типа.
Процедура YourMove записывает ответ второго игрока в заданную переменную (Z ; если выбиратся левое число; 'R\ если выбиратся правое).
ВвоЭ:
Первая строка файла исходных данных с именем INPUT.TXT содержит длину начальной последовательности N (N— четное и 2 <N< 100). Далее следуют N строк, в каждой из которых содержится одно число. Эти числа задают начальные значения, записанные на доске в порядке слева направо. Числа на доске не превышают 200.
Вывод:
Когда игра заканчивается, ваша программа должна записать результат игры в файл с именем OUTPUT.TXT. Файл содержит два числа, записанные в одной строке. Первое число задает сумму чисел, выбранных первым игроком, второе число — сумму чисел, выбранных вторым игроком ваша программа должна сыграть игру с модулем Play, а вывод должен соответствовать сыгранной игре.
15 14
4
7
2
Пример ввода (продолжение)
5
2
К счастью, стратегия в данной игре чрезвычайно простая и в данном случае не требуется применять подходы, описанные ранее. В этом смысле данную задачу можно рассматривать как задачу-шутку.
Поскольку по условиям количество чисел всегда четно, то nepBbiii игрок всегда может заставить второго брать только числа, стоящие на четных позициях, или числа, которые стоят только на нечетных позициях. Остается в начале игры выяснить, сумма каких чисел не меньше тех, что стоят на четных позициях, или тех, что стоят на нечетных позициях, — и затем заставить второго игрока брать числа, которые дают не большую сумму.
Например, для данных
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)-1 раз делаем ходы следующим образом Если соперник делает ход L.
то и мы отвечаем L.
иначе мы отвечаем R
Вывод (SEven, Sodd в порядке убывания).
Понятно, что набранные игроками суммы при такой стратегии игры нс зависят от порядка выбора чисел. Кроме того, в работе с оригинальным модулем Play экспериментально установлено, что брать числа нужно до тех пор, пока останутся невыбранными два числа. Наверно, предполагается, что в ситуации двух оставшихся чисел выбор обоих игроков очевиден, и его не нужно имитировать. К сожалению, в условиях это явно не прописано. Полный текст решения приведен в конце главы.
Решения задач
Листинг 5.1. Текст программы к задаче «Алиса и Боб»
program nee4w98d;
const
MaxN - 1000:
МахМ - 10000:
MaxK - 20:
var
i.N.M.T. from, tov
integer:
Changed
boolean;
S.Ka
array [1..MaxN] of integer:
В
array [1..MaxN] of byte:
a
array [1..MaxN.1..MaxK] of integer:
function ExistArcd :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 Al 1Arcs(>:1nteger):boolean;
var j : integer:
begin .
AllArcs:-true:
for j:-l to ka[i] do
if S[a[i.j]]-0
then begin
All Arcs:-faIse; exit:
end:
end:
procedure InputData:
begin
assign (input.'d.in'): reset(1nput):
read (N.M.T):•
for 1:-l to N do read (B[i]):
Листинг 5.1. (продолжение)
for i:-1 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]:-1:
end:
procedure OutputData;
begin
assign (output.'d.ouf): rewrite(output);
write (S[l]):
for i :-2 to N do writeC '.S[i]);
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]-1) and ExistArc(i)) or ((B[i]-0) and AllArcs(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,jit.
li.lj.ki.kj. il.jl,1k.jk. Number : integer:
S : array [1..MaxM.l..MaxN.l..MaxM.l..MaxN] of integer: P : array [0..9] of 1ongint;
Al IP : 1ongint:
procedure InputData;
var c : char;
begin
assign (input.'input.txt'); reset(input);
readIn (N.M); read(c): 11:-0rd(c)-0rd('a')+1; read(c): ki:-0rd(c)-0rd('a')+1: close(input);
for 11:-l to M do
for jl:-l to N do
for ik:-l to M do for jk:-l to N do SEil.jl.ik.jk] :-0;
readdj): read(c);
read(kj);
{Все состояния проигрышны}
{Выигрышные состояния за 1 ход.} {если ход ладьи}{и конь на одной вертикали}{или на одной горизонтали}{с ладьей}{Выигрышные состояния за 1 ход.} {если ход ладьи}{и конь на одной вертикали}{или на одной горизонтали}{с ладьей}for il:-l to M do
{Выигрышные состояния за 1 ход.} {если ход ладьи}
{и конь на одной вертикали}
{или на одной горизонтали}
{с ладьей}
{Выигрышные состояния за 1 ход.} {если ход ладьи}
{и конь на одной вертикали}
{или на одной горизонтали}
{с ладьей}
for jl:-l to N do for ik:-l to M do for jk:-l to N do if ((il-ik) or (jl-jk)) and not ((il-ik) and (jl-jk))
then S[il.jl.ik.jk] :-l: end:
procedure OutputData:
begin
assign (output.'output.txt*); rewriteCoutput):
writeln(S[li.1j.ki,kj]):
close(output):
{все текущие возможные ходы}{Возможные ходы коня}{Массив констант}{все текущие возможные ходы}{Возможные ходы коня}{Массив констант}end:
{все текущие возможные ходы}
{Возможные ходы коня}
{Массив констант}
{все текущие возможные ходы}
{Возможные ходы коня}
{Массив констант}
type
Knight - array [1..8.1..2] of integer; const
Steps : Knight - (( 1.-2).( 1. 2). (-l.-2).(-l. 2). (2.-1),( 2. 1). (-2.-1).(-2. 1)):
procedure PutlfAll (х,у integer):
Листинг 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>0) and (CurrentX<-M) and (CurrentY>0) and (CurrentY<-N) then begin
if (S[il.jl.CurrentX,CurrentY]<-Number) and (S[il.jl.CurrentX.CurrentYjoO) and not ((11-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 (iltoil) then begin
S[ilt.jl,x.y]:-Number+l;
Changed:-true: end; for jlt:-l to N do if (S[il.jlt.x.y]-O) and (jltojl) then begin
S[il.jlt.x.y]:-Number+l; Changed:-true: end;
end: end:
procedure PutAl 1NewWinStates(i1Jl,1k.jk:integer): begin
for 1:-l to 8 do begin
CurrentX :- ik+steps[i.1]:
CurrentY :- jk+steps[i.2]:
if (CurrentX>0) and (CucrentX<-M) and (CurrentY>0) and (CurrentY<-N) then PutlfAll(CurrentX.CurrentY): end: end:
begin
InputData;
Changed:-true: Number:*0:
while Changed do
begin
Changed:-false;
Inc(Number):
for 11:-l to M do{По всем состояниям}
for jl:-l to N do
for ik:-l to M do for jk:-l to N do if S[il.jl.ik.jkj-Number
then PutAllNewWinStatesCil.jl.ik.jk);
end.
OutputData: end.
Листинг 5.3. Текст программы к задаче «Нечестная игра*
program dj_00_fr: var
G: array[0..202.1..202] of byte:
N.K.Lj.p.m . 1 ongint:
procedure FillGd. j:longint):
var p: 1ongint;
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]:-l: exit: end: for p:-l to mind. j) do
begin
FillGCi-p.p+l):
if G[i-p.p+l]»2 then begin G[1.j]:-l: exit: end;
end:a
продолжение 夕
Листинг 5.3 (продолжение)
G[i.j]:-2:
end:
begin
assign(input.'input.txt'); reset(input);
assignCoutput.'output.txt'): rewriteCoutput);
read(N.K):
for i:-l to N do
for j:-l to К do G[i.j]:-0;
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):
if G[N-p.p+l]»2 then writeCp.''):
end:
close(input): close(output): end.
Листинг 5.4. Текст программы к задаче «Игра loiwari»
program io01dlt2:
type'
tp - array [1. .7] of byte;
tb - array [1..2] of byte:
var
P: tp:
N.i .j.k : byte:
bank : tb:
ruka.nc : byte:
function Next(x:byte):byte:
begin
inc(x);
if x-8 then x:-l;
Next:-x; end;
procedure Wri teStateCp:tp:Bank:tb):
var i• byte:
begin
for i:-l to 7 do write(p[i].''):
write (bank[l],* ',bank[2]);
wnteln:
end;
Procedure Move(who.k:byte;var p:tp: var bank:tb);
var nk : byte:
begin
ruka:-P[k]; P[k]:-0:
{взяли в руку все бусины из лунки
с номером К}
nk :-k;
{nk - номер обрабатываемой лунки}
while ruka<>0 do
begin
nk:-Next(nk);
if ruka>l
{Если бусин в руке > 1.}
then
if p[nk]-5
{а в лунке - 5 бусин.}
then
begin
dec(p[nk]);
{забираем бусину из лунки}
inc(bank[who]);
{кладем ее в свой банк}
end
else
{В лунке - не 5 бусин}
begin
dec(ruka):
{1 бусину из руки}
inc(p[nk]);
{кладем в лунку}
end
else
if ruka-l
{Если в руке 1 бусина}
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:
Procedure TestDebug;
Листинг 5.4 (продолжение)
begin
Moved.2,р.bank): Move(2.3.p.bank):
Moved.5.р.bank): Move(2.4.p.bank):
Moved.5,p.bank); Move(2.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 WinKHole:byte:p:tp:bank:tb):boolean:forward:
function Lost2(Hole:byte:p:tp:bank:tb):boolean:
var i : byte: Res : boolean;
begin
Move(2,Hole,p.bank);
if SumP(p)-0
then Lost2:- Bank[l]>Bank[2]
else
begin
Res:-faIse;
for i:-l to 7 do
if (p[i]<>0) then Res:-Res or WinKi .p.bank): Lost2:-Res:
end;
end:
function Winl(Hole:byte:p:tp;bank:tb): boolean:
var i : byte: Res:boolean:
begin
Moved.Hole.p.bank);
if SumP(p)-0
then Winl:- Bank[l]>Bank[2]
else
begin
beginInputData;while SumP(p)<>0 do beginCalculated .p.bank); writelnd):Moved,i.p.bank);if SumP(p)<>0 thenbegin readln(j): Move(2,j,p.bank)end:end:end.beginInputData;while SumP(p)<>0 do beginCalculated .p.bank); writelnd):Moved,i.p.bank);if SumP(p)<>0 thenbegin readln(j): Move(2,j,p.bank)end:end:end.
begin
InputData;
while SumP(p)<>0 do begin
Calculated .p.bank); writelnd):
Moved,i.p.bank);
if SumP(p)<>0 then
begin readln(j): Move(2,j,p.bank)
end:
end:
end.
begin
InputData;
while SumP(p)<>0 do begin
Calculated .p.bank); writelnd):
Moved,i.p.bank);
if SumP(p)<>0 then
begin readln(j): Move(2,j,p.bank)
end:
end:
end.
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 CalculateCvar il:byte:p:tp;bank:tb);
var
i: byte;
begin
i :-l:
While ((p[i]-0) or (Not WinKi .p.bank))) and (i<-7) do inc(i):
il:-i:
end:
{TestDebug; WriteState:}
{Пока в лунках есть бусины}
{Вычисляем i - очередной выигрывающий ход}
{Выводим его}
{Пересчитываем состояние лунок}
{Если в лунках есть бусины}
{Вводим J - ответ соперника}
{Пересчитываем состояние лунок}
Листинг 5.5. Текст программы к задаче «Игра Score*
program io01d2tl: const
MaxN -
1000:
var
N
:integer;
{Кол-во позиций}
a
:array [1..MaxN.l..MaxN] of byte:
{Граф}
Free
:array [1..MaxN.l..MaxN] of boolean:
{использованные дуги}
V
:array [1..MaxN] of byte;
{Владельцы позиций}
c
:array [1..MaxN] of integer;
{Цены позиций}
Sorted
:array [1..MaxN] of integer:
Листинг 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 F гее [ c v. So rted [ i ] ] : -Fa 1 se: cv:-Sorted[i]: exit;
end;
end;
procedure Calculate: var
i: byte: begin
for i:-l to N do Sorted[c[i]]:-i:
for i:-1 to N do
for j:-l to N do Free[i,j]:-true; 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:
{Если вернулись в вершину 1 - конец}
end:
end.
Листинг 5.6. Текст программы к задаче *Игра-2»
program io96dltl:
uses play:
var
N.S0dd.SEven.Sl.S2.i.j : integer;
a:аггау[1..100] of integer;
C:char:
procedure InputData:
begin
assign (input.'input.txt'): reset(input); readln(N);
for 1:-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:
Листинг 5.6 (продолжение)
begin
SOdd:=O: SEven:=O:
for i:-l to N do
if Odd(i)
then incCSOdd .a[i])
else inc(SEven.a[1]):
if SOdd>SEven
then begin Sl:-SOdd; S2:- SEven: MyMove('L') end
else begin SI:-SEven: S2:- SOdd: MyMove('R') 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, п2,..., яЛ/),где 〃1 N 〃2 N... N пМ 2 0 — это расположение 〃1 + 〃2 +・..+ пЛ/различных целых чисел в массиве строк, выровненных по левому краю, где в 7-й строке содержится п! элементов; при этом в каждой строке элементы возрастают слева направо, а элементы каждого столбца возрастают сверху вниз. Например, 1 2 5 9 10 15
3 6 7 13 4 8 12 14 11
есть диаграмма Юнга формы (6,4,4,1). Такие расположения ввел Альфред Юнг в 1900 году в качестве вспомогательного средства при изучении матричного представления перестановок. Далее для краткости вместо «диаграмма Юнга» будем говорить просто «диаграмма».
Вставка и удаление элементов диаграммы
Рассмотрим алгоритм вставки элемента в диаграмму. Пусть Р — диаграмма из целых положительных чисел, ах — целое положительное число, не содержащееся в Р. Этот алгоритм преобразует Р в другую диаграмму, содержащую х наряду
с исходными элементами Р. Новая диаграмма будет иметь ту же форму что и старая, с той лишь разницей, что на пересечении строки 5 и столбца Г появится новый элемент, где 5и Г формируются в процессе выполнения алгоритма.
Неформально алгоритм может быть описан следующим образом: если х больше всех элементов первой строки, то ставим его ЗА последним элементом: s = 1, f = wl + 1 — и алгоритм завершен. В противном случае в первой строке ищем позицию j такую, что P(i,j)<x<P(ltj + 1)и ставим х на позицию (1J). А х присваиваем значение элемента P(l, j + 1), и продолжаем аналогичную работу теперь во второй строке. То есть для i-й строки: если она пуста, то ставим элемент в позицию (i, 1) (S = i, Т = 1); если элемент х больше всех элементов строки i, то ставим его за последним элементом P(i, ni + l)r, (S = i, Т =加 + 1) (здесь т — количество элементов в i-той строке). Если найдено такое / что P(ij) <х< P(i,j + 1), то обмениваем значения х и P(i, j + 1) и переходим к следующей строке.
Например, при добавлении последовательно в пустую диаграмму элементов 3, 4, 9, 2, 5,1 получим такую последовательность диаграмм:
3 : 3
4:34
9:349
2:249
5:245
1:14 5
3
3 9
2 9
3
Можно заметить, что в результате добавления элемента в диаграмму добавляется позиция (5,Г), обладающая свойством «крайности». То есть ниже этой позиции и правее ее элементов нет.
Важное свойство такого алгоритма вставки элементов в диаграмму — это возможность его «исполнения в обратном направления», то есть исполнения алгоритма удаления элемента из диаграммы, которое по заданным 5 и Г (строка и столбец соответственно) удалит элемент с «крайней» позиции диаграммы (5, Г) (в диа- П)амме нет элементов ниже и правее этой позиции); при этом переменная х получит значение удаленного элемента (всегда из первой строки).
Неформально этот алгоритм может быть описан следующим образом: берем элемент из позиции диаграммы с координатами (5, Т) и заносим его значение в х. Далее для всех строк с номерами i от S-1 до 7 находим элемент (на позиции j), такой что P(i, j) <х< P(i, j + 1). Если такого нет, то используем крайний элемент строки в качестве элемента P(i, j). Обмениваем значениями найденный элемент P(f, j) и х, затем переходим к предыдущей строке (i 一 1). Элемент, вытесненный из первой строки, и есть результат х выполнения алгоритма наряду с перестроенной диаграммой.
Например, если из диаграммы
4 5
9
3
удалить элемент с позиции (3,1), то получим диаграмму:
4 5
9
6・3・ Количество возможных диаграмм заданной формы (rd, п2пМ) 335
а удаленным элементом будет х* 1, который находился в исходной диаграмме на позиции (1, 1).
Если из исходной диаграммы удалить элемент (2,2), то получим диаграмму:
1 4 9
2
3
а удаленным элементом будет элемент 5.
И, наконец, если из исходной диаграммы удалить элемент с позиции (1, 3), то получим диаграмму:
4
9 3 а вытесненным элементом будет % = 5.
Количество возможных диаграмм заданной формы (п1,п2,пМ)
Количество возможных диаграмм заданной формы (〃1,九2,..., пМ) — это количество различных способов составить из элементов от 1 до 〃1 + 九2+...+〃М диаграммы, имеющие форму (九 1,九2,.” пМ). То есть имеющие п1 элементов в первой строке п2 элементов во второй строке ... пМ элементов в строке с номером пМ. Это количество равно сумме количеств диаграмм, получающихся последовательным удалением по одному элементу в каждой из строк, которая содержит хотя бы один элемент. Соответствующее рекуррентное соотношение имеет вид:
если п1>-п2>»...пМ>-1, то f(nl.n2пМ) - f(nl-l,n2пМ) + f(nl.n2-lпМ) + f(nLn2пМ-1) если не выполнено соотношение п1>-п2>»...пМ>-0 то f(nl.n2пМ) - О
Кроме того,
f(nl.n2,...пМ.О) - f(nl,n2....пМ)
Это рекуррентное соотношение вытекает из того факта, что при удалении наибольшего элемента из диаграммы всегда снова получается диаграмма.
Далее рассмотрим решение двух задач Международной олимпиады по информатике 2001 года, которая состоялась в Финляндии.
Задача «Склад»
Международная олимпиада 〃о информатике, 200 7
Склад (depot.in / depot.out)
Финская компания имеет больиклт прямоугольны»! cicniu. I la складе есть ра- бочп1111 менеджер. Стороны склада в порядке обхода иазывакпся левая, верхняя, правая 11 нижняя. Площадь склада разбнта на квадраты с равной площа- дыо. составляющие строки и столбцы. Строки пронумерованы сверху целыми числами 1, 2...., а столбцы иронул!срованы слева целыми числам» 1, 2,.... Склад имеет контейнеры, которые используются для хранение】 отдельных yc rpoiicrB. Контейнеры имеют ра;ишчные идентификационные номера. Каж- дьп1 контейнер занимает ровно один квадрат. Склад такоН большой, что ко;1ичсство контейнеров, которые прибывают, меньше, чем количество строк п количество столбцов. Контейнеры не удаляются со склада, но иногда прибывают новые контейнеры. Вход н склад в левом верхнем углу склада.
Рабочи11 разместил контейнеры вокруг левого верхнего угла таким образом, чтобы он мог iiaiiTii их по их идентификационным номерам. Он ис- лол ьзуст с л еду к)i ци й метод. Предположим, что идентификационный номер следующего контейнера, который нужно вставить, есть К (кон геймер К для кргггкости). Рабочий идет вдоль первой строки н ищет первый контейнер с 11денти([)11кац11О11пым номером больше, чем К. Если он не паходит такого KOHTeiiiiepa, то он ставит его непосредственно за самым правым контейнером в строке. Если такой контейнер 丄 наеден, то icoirreiniep 丄 заменяется KoHTeiniepoM К, а контейнер 丄 вставляется в следуюи!ую строку при помощи того же самого мегода. Если рабочи!! достигает строки, в которой нет ни одного контейнера, он ставит контейнер в позицию самого левого квадрата ;л oii строки.
Предположим, что контейнеры 3,4,9, 2, 5, 1 прибыли на ск;1ад именно в таком порядке. Тогда размещение контейнеров на складе будет таким:
1 4 5
2 9
3
К рабочему подходит менеджер и между ними происходи! следующий диалог Менеджер: «Контейнер 5 прибыл перед контейнером 4?»
Pn6o4iiii: «Нет, это невозможно».
Менеджер: «Так вы можете рассказать мне порядок прибытия контейнеров по их размещению?»
PnGsuiii: «Вообще гоиоря, нет. Например, контейнеры, находящиеся на ск「【п工с, Morjiif прибыть в порядке 3, 2, 1,4, 9, 5, или в порядке 3, 2, 1, 9, 4, 5, или в одном из 14 других порядков».
Поскольку менеджер нс хочет выглядеть намного глупее рабочего, он уходит. Вы должны помочь менеджеру н написать программу, вычисляющую
по данному размещению контейнеров все возможные порядки, в которых они могли прибывать.
Ввод:
Имя входного файла depot.in. Первая строка содержит одно целое число — количество строк, в которых содержатся контейнеры. Следующие R строк содержат информацию о строках 1,..., R, начиная с верхней в следующем порядке: вначале каждой из этих строк находится число М — количество контейнеров в этой строке. За ним находятся М целых чисел — идентификационные номера контейнеров в строке, начиная слева. Все идентификационные номера контейнеров / удовлетворяют условию 1 4 / 4 50. Число Nконтейнеров в складе удовлятворяет условию 1 <N^ 13. Вывод:
Имя выходного файла depot.out. Выходной файл состоит из строк, количество которых равно числу различных вариантов прибытия контейнеров. Каждая из этих строк содержит N целых чисел — идентификационных номеров контейнеров в порядке, соответствующем порядку прибывания. Все строки должны быть уникальны. Примеры ввода и вывода:
depot.in
Пример fПример 2
Depot.outdepot.indepot.out
3
3 145
229
1 3
3 2 1 49523 1 2
32 1 9452 1 21 32
342 1 951 3
324195
329145
392145
342915
349215
324915
329415
392415
342951
349251
324951
329451
392451
Оценивание'.
Если выходной файл содержит невозможный порядок или не содержит порядков вообще, вы получаете 0 баллов за соответствующий тест. В противном случае ваш счет вычисляется следующим образом: если выходной файл содержит все возможные варианты, вы получаете 4 балла. Если выходной файл содержит
половину или более возможных вариантов и все они различны, вы получаете 2 балла. Если выходной файл содержит менее половины различных возможных вариантов или некоторые из них встречаются более чем 1 раз, вы получаете 1 балл. Внимательный читатель уже догадался, что алгоритм заполнения склада контейнерами —это алгоритм вставки элементов в диаграмму. И задача заключается в восстановлении всех возможных способов сформировать заданную диаграмму. Поскольку мы уже изучили алгоритм «выталкивания» элементов из диаграммы, то мы знаем, как выяснять, какой элемент мог поступить последним. Применяя последовательно алгоритм ко всем получающимся диаграммам, получаем решение задачи. Ниже оно описано более детально.
Из алгоритма заполнения склада контейнерами следует, что при внесении нового контейнера в складе появляется контейнер на ранее незанятой «угловой» позиции, обладающей тем свойством, что справа от нее и ниже ее нет контейнеров.
Действительно добавляемый контейнер либо сам становится в такую позицию (если его номер больше всех остальных), либо вытесняет в следующую строку контейнер, которому передает те же функции; последний делает то же самое, и т. д. Таким образом, рекурсивно перебирая все «угловые» позиции, обладающие свойством «справа от нее и ниже ее нет контейнеров», и накапливая «выталкиваемые обратно» контейнеры, при освобождении таких позиций мы получим все возможные варианты поступления контейнеров.
Тело главной программы выглядит следующим образом:
begin
InputData:
assignCoutput.'depot.out*): rewriteCoutput):
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]:-0:
assigndnput. 'depot.in'); reset(input):
readln(R);
for i:-l to R do
begin
read(a[i.O]):
for j:-l to a[i.0] do read(a[i,j]);
end:
close(input):
is:*0:
end;
Процедура InputData формирует в результате ввода двумерный массив А, в котором:
a[i, 0] — количество контейнеров в i-й строке;
a[i,j] — номер контейнера, стоящего в i-й строке,/м столбце (для j от 1 до a[i, 0]);
a[i,j] = 0 для позиций, незанятых контейнерами;
Л — количество строк в массиве А, занятых номерами контейнеров.
Рассмотрим подробнее процедуру 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.0]]: ik:-i;
Find(a.i-l.k.ik):
Push(k); a[ik.a[ik.O]]:-O; dec(a[ik,O]):
Process(a):
k:-Pop:
Add(a.k):
end;
end:
Очевидно, что она рекурсивная и что при каждом вызове к ней в матрице Л становится на 1 элемент меньше. Когда матрица а становится пустой (а[1, 0] = 0), то выводится результат (с помощью процедуры PutAnswer) и осуществляется выход из текущего экземпляра рекурсивной процедуры Process.
Из матрицы а последовательно удаляются все «граничные» элементы вида a[i, a[i, 0]] следующим образом: с помощью процедуры Find в матрице а находится элемент А, добавление которого привело к заполнению позиции a[if a[it 0]],汲一 номер строки, в которой находится этот элемент.
Найденный элемент k заносится в стек с помощью процедуры Push, а его позиция освобождается (a[ik, a[ik, 0]]: = 0). Также уменьшается на 1 счетчик числа элементов в соответствующей строке (dec(a[ik, 0])).
Таким образом, процедура Process построила «предыдущую копию> матрицы а до добавления в нее элемента 山.И теперь вызывает себя вновь — для «уменьшенной» матрицы а
После выхода из процедуры Process k восстанавливается из стека функцией Pop, а затем процедурой Add добавляется в матрицу а в соответствии с правилами, описанными в условиях задачи. Так обеспечивается рекурсивный обход всех возможных состояний матрицы а, приводящих к ее заданному конечному состоянию последовательным добавлением недостающих элементов матрицы а.
Теперь рассмотрим подробнее процедуры и функции, использованные в процедуре Process,
Процедура Push добавляет элемент в стек 5:
procedure Push(st:integer);
begin
inc(is); s[is]:-st: end:
Функция Pop достает элемент из стека 5:
function Pop:integer:
begin
Pop:-s[is]: s[is]:-O: dec(is); end:
Процедура PutAnswer выводит содержимое стека в порядке поступления элементов в матрицу а:
procedure PutAnswer;
begin
write(s[is]);
for j:»is-l downto 1 do write (' '.s[j]);
writeln;
end;
Процедура Add(a,山)добавляет в матрицу а элемент k в соответствии с правилами, описанными в условиях задачи
procedure Add(var a:mas;k:integer):
var i.j.t : integer:
begin
while k<>0 do begin
while ((j<-a[i.0]) and (a[i.j]<k))
do inc(j):
if (a[1,0]<>0) and (j<-a[i.O])
then begin
t:-a[i.j]: a[1.j]:-k:
k:-t: inc(i):
end
else begin
inc(a[i.0]);
a[i.a[i,0]]:-k: k:-0;
end;
end:
end:
И, наконец, рассмотрим процедуру Find.
procedure FindCvar 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.O];
for 1:-l to a[j,0] do
if (a[j,l]<k) and (d[j.l+l]>k) then x:-l:
t:-a[j.x]: a[j.x]:-k: k:-t: end;
end;
Процедуре Find(a, i, k,加)передается элемент 瓦 который изменил состояние матрицы, и номер строки i, находящейся непосредственно над строкой с элементом А. Процедура Find(at i, kt 汲)находит в матрице а, начиная со строки i, такой элемент k (возвращается также его строка 血),что при его добавлении матрица а перейдет в состояние, в котором она сейчас находится. Для этого в строках с номером j (от i до 1) последовательно выполняется следующая работа:
находится элемент (a[j, /] или a[j, a[j, 0]]), добавление которого в строку j и привело к опусканию элемента А в строку j + 1.
обмениваются значения А и найденного элемента 〃[ j, х].
Полный текст решения приведен в конце главы.
6.5. Задача «Twofive»
Международная олимпиада по информатике, 2001
Twofive (twofive.in / twofive.out)
Секретные сообщения между Санта Клаусом и его маленькими помощниками всегда кодируются на 25-языке. Алфавит 25-языка такой же, как и латинский, с одним исключением — буква Z отсутствует. То есть алфавит 25-языка содержит 25 латинских букв от А до Y в том же порядке, что и в латинском алфавите. Каждое слово в 25-языке состоит ровно из 25 различных букв.
Слово может быть записано в таблице 5 х 5 по строкам. Например, слово ADJPTBEKQUCGLRVFINSWHMOXY может быть записано так:
А
D
J
Р
т
В
Е
К
Q
и
С
G
L
R
V
F
I
N
S
W
Н
М
О
X
Y
Корректное слово в 25-языке содержит буквы в порядке возрастания в каждой строке и в каждом столбце.
Таким образом, слово ADJPTBEKQUCGLRVFINSWHMOXY — корректное, a ADJPTBEGQUCKLRVFINSWHMOXY — некорректное (возрастающий порядок нарушен во втором столбце, и в третьем тоже).
У Санта Клауса есть свой лексикон. Его лексикон — это список корректных слов из 25-языка в порядке лексикографического возрастания с соответствующим порядковым номером, начинающимся с 1. Например, в этом лексиконе слово ABCDEFGHIJKLMNOPQRSTUVWXY — это слово с порядковым номером 1, а слово ABCDEFGHIJKLMNOPQRSUTVWXY — это слово с порядковым номером 2. В слове 2 у букв U иТ изменен порядок по сравнению со словом 1.
К несчастью, этот лексикон огромен. Напишите программу, которая определяет порядковый номер произвольного заданного слова из этого лексикона, а также слово, соответствующее заданному порядкому номеру. В этом лексиконе не более 231 слов.
Ввод:
Входной файл называется twofive.in и состоит из двух строк. Первая строка содержит один символ: W или N.
Если первая строка содержит символ % то вторая строка содержит корректное слово из 25-языка, то есть строку из 25 символов. Если первая строка содержит символ N, то вторая строка содержит порядковый номер корректного слова из 25-языка.
Вывод'.
Выходной файл называется twofive.out и содержит одну строку. Если вторая строка входного файла содержит слово из 25-языка, то строка выходного файла содержит его порядковый номер. Если вторая строка входного файла содержит порядковый номер, то строка выходного файла содержит слово из 25-языка с этим порядковым номером.
Пример ввода
Пример вывода
W
ABCDEFGHIJKLMNOPQRSUTVWXY
2
Пример вводаПример вывода
NABCDEFGHIJKLMNOPQRSUTVWXY
2
Общая идея решения такова: если буквы в таблице 5x5 заменить на соответствующие числа (Л - 1, В - 2, С - 3,... У - 25), то получится, что правильному слову в 25-языке соответствует некоторая диаграмма (рис. 6.1),
1
4
10
16
20
1
4
10
16
20
2
5
11
17
21
2
5
11
17
21
3
7
12
18
22 —
► 3
7
12
18
22
6
9
14
19
23
6
9
14
19
23
8
13
15
24
25
8
13
15
24
25
Рис. в.1. Диаграмма для задачи «Twofive»
Для поиска номера заданного слова, равно как и для поиска слова по заданному номеру, нужно уметь находить количество вариантов расстановки оставшихся букв, если некоторое количество букв уже зафиксировано на своих позициях.
В то же время количество вариантов расстановки оставшихся букв, если уже расставленные буквы составляют диаграмму, можно найти, используя формулу количества диаграмм заданной формы.
Например, если мы поставили только букву А, то имеем табличку
А....
Диаграммы, которые можно составить на оставшихся свободными позициях, имеют вид:
Общее количество таких диаграмм равно количеству диаграмм вида (5, 5, 5, 5, 4). Рассмотрим теперь общий случай. Пусть мы корректно расставили первые Убукв (от А до буквы, которая в латинском алфавите имеет номер N), и при этом оказалось, что «1 букв находится в 1-й строке, 〃2 букв во второй строке,... п5 букв в 5-й строке (〃1 + 〃2 + 九3 + 九4 + 九5 n N).
Тогда количество различных диаграмм вида (5 - п5,5 - «4,5 - пЗ, 5 - «2,5 - л1)— это и есть количество различных способов расставить все неиспользованные буквы на позициях, оставшихся свободными. Например, при N= 9 для расстановки
ABCDE FGHI.
имеющей количества элементов в строках (5,4,0,0,0), количество вариантов корректной расстановки всех оставшихся букв будет равно количеству всех диаграмм вида (5,5,5,1,0), а для расстановки
ABCD.
EFG..
Н....
количество вариантов расстановки всех оставшихся букв будет равно количеству диаграмм вида (5, 4,4, 2,1).
Таким образом, можно реализовать функцию Rest, которая должна вычислять количество вариантов расстановки оставшихся букв по заданной расстановке некоторого подмножества букв. Она пытается использовать заранее расчитанные результаты, а если это невозможно (расставленные буквы не соответствуют диаграмме), то перебирает все варианты, добавляя новые буквы на все возможные позиции и рекурсивно вызывая себя для получающихся расстановок букв. При этом если все элементы расставлены по своим позициям (〃1 + 九2 + пЗ + 〃4 + 〃5 А МахТ 1) значит, перебором найден еще один вариант, и Rest возвращает значение 1.
Имея функцию Rest для поиска количества вариантов расстановки букв на всех оставшихся позициях, для решения задачи достаточно поступать следующим образом: для поиска номера слова по строке мы поддерживаем значение специальной величины соответствующим номеру первого по алфавиту слова, которое мы можем сгенерировать по текущему множеству зафиксированных букв. Увеличивая значение буквы или переходя к букве на следующей позиции, мы каждый раз добавляем к нашей величине количество слов, которое было пропущено, чтобы перейти к новой позиции из старой.
Для решения обратной задачи — поиска номера по заданной позиции — мы опять наращиваем накапливаемое значение при добавлении букв на соответствующие позиции.
Далее подробно описана реализация данного решения. Тело главной программы выглядит естественно просто:
begin
InputData:
if PT-'W
then FindNumber
else FindString:
OutputData:
end.
Вызываются четыре процедуры:
InputData — ввести исходные данные;
Outputdata — вывести результат;
FindNumber — найти номер заданной строки;
FindString — найти строку по заданному номеру.
Процедура Outputdata выводит в качестве результата найденную строку (FS) или найденный номер (/W):
Procedure OutputData; begin
assign(output.'twofi ve.out'): rewrite(output): if PT-'W , then writein (FN) else writein (FS): close(output): end:
Процедура InputData вводит исходные данные (номер или строку) и вызывает процедуру Init для инициализации.
procedure InputData; begin
assign (input,'twoflve.in'): reset(input); readln(PT): if PT-'W then readln(S) else readln(N); close(input);
Init;
end:
Рассмотрим теперь процедуру Init
Procedure Init:
var 11.12.13.14.15 : integer;
begin
for
il:-0
to
5
do
for
i2:=0
to
5
do
for
13:-O
to
5
do
for
14:-0
to
5
do
for
15:-0
to
5
do
begin
State[il.i2.i3.14.i5]:-0:
if not ((il>-12) and (12>-i3) and (13>-14) and (i4>35))
then Calculated[il.12,i3.i4.i5J:-true
else Calculated[il.i2,i3,i4,15]:-false;
end;
State[1.0,0.0.0]:-l: Calculated[l.0.0.0.0]:-True; CalcState(5,5.5,5.5): end:
Основная задача процедуры Init — заполнить массивы: □ Calculated[ii, 2 й, /4, i5] (значениями типа boolean) □ State\i\t i2, й, i4, :5] (значениями типа longint) и
Calculated[iit i2, i3, i4, i5]xtrue указывает, что элемент массива State с индексами il, i2, i3, i4, :5 уже вычислен (false — еще не вычислен). Это используется для избегания повторных вычислений и для предустановки предопределенных нулевых значений массива State.
State[ii, :2, :3, i4, /5] — это количество различных способов составить из элементов от 1 до il + i2 + /3 + i4 + i5 диаграммы, имеющие форму (й, /2, /3, /4, /5). То есть такие диаграммы, у которых:
:1 элементов в первой строке;
:2 элементов во второй строке;
/3 элементов в третьей строке;
/4 элементов в четвертой строке;
/5 элементов в пятой строке.
Это количество равно сумме количеств диаграмм, получающихся последовательным удалением по одному из элементов в каждой строке, которая содержит хотя бы один элемент.
Соответствующее рекуррентное соотношение имеет вид: если il>-i 2>-i 3>-i 4>-i 5>-1.
то f(il.i2.i3.i4.15) - f(il-l.i2.i3.14.i5) + f(il.12-1,13.14,15) + f(il.i2.13-l.i4.i5) + f(il,i2.i3.i4-1.15) + f(il.i2.i3.14,i5-l) если не выполнено соотношение 71 n i2 n ?3 N M n /5 N 0, to f(il. 12. ?3, ?4, i5) - 0
Кроме того, f(il.12.13.14.0) - f(il.12.13.14) f(il.12.i3.0) - f(il,i2.13) f(il.i2.0) - f(iL⑵ f(il.O) - f(il)
Это рекуррентное соотношение вытекает из того факта, что при удалении наибольшего элемента из диаграммы, всегда снова получается диаграмма.
Для вычисления этого рекуррентного соотношения используется рекурсивная функция CalcState(5t 5,5, 5,5), которая заполняет всю таблицу State[0..5t 0..5,0..5,0..5, 0..5] вычисленными значениями:
function CalcStateCnl.n2.n3.n4.n5:intедег):1ongint: var j.k : byte; s : 1ongint:
begin
if Calculated[nl.n2.n3.n4.n5]
then begin Cal estate:-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;
Может возникнуть вопрос: «А какое отношение имеет этот массив State к решению нашей задачи?» Прежде всего это, фактически, количество различных вариантов составление слов, если первые N6yKB уже расставлены по позициям.
Для приведенного примера
А....
А общее количество таких диаграмм равно State(5t 5, 5, 5, 4).
Рассмотрим теперь общий случай.
Пусть, мы корректно расставили первые N букв (от А до буквы, которая в латинском алфавите имеет номер N, и при этом оказалось, что п! букв находится в 1-й строке, п2 букв во второй строке, ... п5 букв в 5-й строке (九 1 + 〃2 + 〃3 + + п4 + п5 = N)・
Тогда State[5 一〃5, 5 一 〃4, 5 - n3f 5 - n2f 5 - л1] — количество различных способов расставить все неиспользованные буквы на позициях, оставшихся свободными. Например, при N = 9 для расстановки
ABCDE
FGHI.
имеющей количества элементов в строках (5, 4, 0,0, 0), количество вариантов корректной расстановки все оставшихся букв будет равно количеству всех диаграмм вида (5,5,5,1,0), то есть элементу State[5,5, 5,1, 0].
А для расстановки
ABCD.
EFG..
количество вариантов расстановки всех оставшихся букв будет равно State[5,4,4, 2,1]. Таким образом, функция Rest, которая должна вычислять количество способов расставить оставшиеся буквы по заданной расстановке некоторого подмножества букв, пытается использовать заранее расчитанные результаты, а если это невозможно (функция Rigfit возвращает значение false), то перебирает все варианты, добавляя новые буквы на все возможные позиции и рекурсивно вызывая себя для получающихся расстановок букв. При этом если все элементы расставлены по своим позициям (九 1 + 〃2 + 九9 + 九4 + 九5 N МахТ - 1), значит, перебором найден еще один вариант, и Rest возвращает значение 1.
function Rest(Table:Tab:i:byte:Able:TAb:j:byte: Absent:tabu):longlnt: 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:-l: exit: end;
for i.-2 to MaxT do Able[i]:-0:
for i:-l to MaxT do
begin
if Table[i]-MaxS then continue:
if (z[i.l]<>0) then Able[z[i.l]]:-1:
汗(z[i.2]<>0) then Able[z[i.2]]:-l:
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 PutCTable.i.Able.t.Absent);
Rest:-s:
end;
ЗАМЕЧАНИЕ
Отладка алгоритма в целях удобства проводилась на таблице из букв размером 3x3. Соответственно были введены константы: Rang = 5 для оригинальной задачи, Rang = 3 для отладочного образца; MaxS = Rang х Rang; MaxT = MaxS - 1 (последняя буква всегда одна и та же).
Функция Rest имеет следующие параметры:
Table — текущая расстановка букв;
i — какое значение было поставлено последним;
Able — какие позиции в расстановке Table сейчас свободны;
j — на какую позицию было поставлено последнее значение;
Absent — какие элементы отсутствуют в текущей расстановке;
Table[k] равно номеру буквы на позиции k или MaxS, если позиция не заполнена;
Able[k] равно 1, если позиция k свободна, и 0, если позиция А занята;
Absent[k] равно TRUE, если буквы с номером k нет в текущей расстановке Table (и FALSE в противном случае).
Функция Rest по полученной расстановке строит массивы Able и Absent и вызывает процедуру Put:
procedure Put(Table:Tab;1:byte:Able:TAb;j:byte:Absent:tabu): begin if (Able[j]-1) and
(1>Table[P[j.l]]) and (1>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]:-1:
Absent[Table[j]]:-false:
inc(s.Rest(Table,1.Able,j.Absent)): end:
end:
Процедура Put проверяет, можно ли поставить букву с номером : на позицию / и если можно, то ставит, корректируя соответствующим образом массивы Table, Able и Absent, а затем вызывает функцию Rest для вновь полученной расстановки, с накоплением суммы всех результатов Rest в переменной 5.
Функция Rig/rt вычисляет, сколько букв в каждой строке расстановки (〃1,九2,.., л5), находит букву с максимальным номером в расстановке (Мах) и общее количество букв в расстановке (九).Если Мах 工九,то расстановка «правильная» в том смысле, что количество способов расставить все оставшиеся буквы можно взять в таблице State на позиции [5 - п5> 5 - 〃4,5 - «3, 5 - «2,5 - л1].
function Right(Table:Tab;var nl.п2,п3.п4.п5:1nteger):boolean; var i.Max.n.r : integer: t: аггау[1..5] of integer;
begin
for i:T to Rang do t[i]:-0:
for 1:-l to Rang do
for r:-l to Rang do
if Tabie[(i-1)*Rang+r]<>MaxS then inc(t[i]):
nl:-t[l]; n2:-t[2]; n3:-t[3]; n4:-t[4]: n5:-t[5];
Мах:-ТаЫе[2]; п:-0;
for i:-1 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:T;
Init2; j:-2;
for i -2 to MaxS-2 do
begin
Table[i]:-j:
while Table[i]<(Ord(s[i])-Ord('A*)+1) do begin
incCFN.Rest(Table.i.Able,j.Absent));
inc(Table[i]);
repeat
q:-l:
while-(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;
В переменной FN накапливается ответ.
Процедура Init2 инициализирует начальными значениями массивы Table, Able и Absent,
Procedure Init2; var i : byte: begin
Table[O]:=O:
for i:-2 to MaxS do Table[i]:-MaxS;
Table[l]:-1:
for i:-2 to MaxT do Absent[i]:-true:
for i:=2 to MaxT do
if Table[i]<>MaxS then Absent[Tabie[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.
Пока поставленная буква меньше, чем буква в исходной строке (Table[i\ < (Ord(s[i])- 一 Ord('A) + 1)), накапливаем в переменную FNколичество способов, которыми можно расставить оставшиеся буквы, вызывая описанную ранее функцию Rest и изменяем букву в позиции i на следующую возможную (больше предыдущего значения буквы на текущей позиции, но не равное значениям букв на предыдущих позициях).
Функция MinAble имеет следующий вид:
function MinAbledinteger):integer:
var j.il.i2: integer;
mas: array [0..MaxS] of byte:
Good.Goodl.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-1]) or
(i>Rang) and
(((i mod Rang)-1) and (j>Table[i-Rang]) or
((i mod Rang)<>l) and (j>Table[i-1]) and (j>Table[i-Rang]));
Good:-Goodl and Good2;
if Good then begin MinAble:-j: exit; end: end:
end:
Функция MinAble принимает в качестве параметра номер позиции i, на которую в массиве Table нужно поставить текущую букву. Функция MinAble перебирает все буквы от 2-й (то есть В) до буквы МахТ(то есть X).
Если позиция i свободна и число j больше соседей слева и сверху (если таковые имеются), то j — то, что нужно.
Теперь рассмотрим функцию 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(Tabie.i.Able.j.Absent): repeat while (FN+Add>・N) and (1<MaxT) do begin
inc(i); SetTabled .2): J:-Table[i];
Add:-Rest(Tabie.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;
FSl'A':
for i:-2 to MaxT do FS:-FS+char(Table[i]+ord('A*)-1): FS:=FS+Last[Rang]:
end;
Вычислив в переменной Add количество различных перестановок, начинающихся с букв АВ, процедура FindString работает до тех пор, пока не построит всю строку до позиции i - МахТ
Пока (FN+Add)>N и (КМахТ)
Переходим к следующей позиции; устанавливая корректно символ в этой позиции, перевычисляем Add - количество вариантов построения последовательность букв с таким префиксом Пока (FN+Add<N)
увеличивает FN на занесение Add.
устанавливаем следующую возможную букву в текущей позиции i. перевычисляем Add
Выйдя из цикла, формируем символьную строку по построенному числовому массиву Table.
Для установки возможной буквы в заданную позицию i начиная с заданного значения % используется процедура SetTable(if х):
procedure SetTabled.х:integer); var Correct : boolean; q : integer: begin repeat
Correct:-(i<-Rang) and (x>Table[i-U)or (i>Rang) and (((1 mod Rang)-1) and (x>Table[1-Rang]) or ((i mod Rang)<>l) and (x>Table[i-l]) and (x>Table[1-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:
Полный текст решения задачи приведен в конце главы.
Для перехода к размерности 3x3 требуется модификация исходного текста в трех местах, отмеченных соответственно {1}, {2}, {3}.
6.6. Решения задач
Листинг 6.1. Текст программы к задаче “Склад»
program ioOld2t3: type
Mas ■ аггауСО..13.0..13] of Integer;
Masi- array[l..13] of Integer: var
R.i, j.ls.k.st.ik : Integer:
s: Masi;
a: Mas;
procedure Inputdata: begin for i:-0 to 13 do for j:-0 to 13 do a[i.j]:-0:
ass1gn(1nput,'depot.in'); reset(1nput); readln(R); for 1:-l to R do begin
read(a[i.0]):
for j:-l to a[i.0] do read(a[1,j]); end:
closed nput):人
продолжение 夕
Листинг 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 (' '.s[j]): writeln;
end:
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.0]) and (a[i.j]<k)) do inc(j);
if (a[i.0]<>0) and (j<-a[i.0]) then begin
t:-a[i.j]; a[i.j]:-k: k:-t; inc(i);
end else begin
inc(a[i.0]);
a[i.a[i,0]]:-k:
k:-0: end;
end:
end;
procedure Find(var a:mas:i:integer:
var к integer; var ik: integer);
var j.l.t.x : integer:
begin
for j:-i downto 1 do
begin
x:-a[j.O]:
for 1:-l 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:
procedure Process(a:mas);
var i :integer:
begin
if a[1.0]-0
then begin PutAnswer: exit: end;
for 1:-R downto 1 do
if (a[i,0]o0) and (a[i+l,a[i,0]]-0) then
begin
k:-a[i.a[1,0]]: ik:-i: Find(a.i-l.k.ik);
Push(k);
a[ik,a[ik,0]]:-0: dec(a[ik,0]):
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 1o01dlt3;
Const
{1}
Rang-3:
Листинг 6.2 (продолжение)
Z : аггау[1..МахТ.1..2] of byte ((2.4).(3.5).(6.0).(5,7).
0.0).(8.0),(0,0)):
Р : аггау[1..MaxT.l..2] of byte ((0.0).(1.0).(2.0).(1.0).
(4.2).(5.3).(4,0),(7.5)):
}
Rang-5;
MaxS-Rang*Rang:
MaxT-MaxS-1:
Z : array[l..MaxT.l..2] of byte -
(2. 6),( 3.(7.11),( 8. (12.16),(13. (17.21),(18. (22. 0).(23.):(2. 6),( 3.(7.11),( 8. (12.16),(13. (17.21),(18. (22. 0).(23.):0).( 4. 0),( 5. 0).(0.0).
(2. 6),( 3.
(7.11),( 8. (12.16),(13. (17.21),(18. (22. 0).(23.
):
(2. 6),( 3.
(7.11),( 8. (12.16),(13. (17.21),(18. (22. 0).(23.
):
0).( 9. 0),(10, 0),(0.0).
0).(14. 0).(15. 0).(0.0).
0).(19. 0).(20. 0).(0.0).
0),(24.0), (0.0)
P : аггау[1..MaxT.l..2] of byte
(0.0).(1.0).(2.0).(3.0).(4.0).
(1.0).(2.6).(3.7).(4.8).(9.5), (6.0),(7.11).(8.12).(9.13).(10.14).
(11.0).(12.16).(13.17).(14.18).(15.19).
(16,0).(17,21).(18.22).(19.23)
):
Last : string - 'ADIPY';
Type
Tab - array [0..MaxS] of byte;
St - array [0..MaxS] of longint;
Tabu- array [0..MaxS] of boolean;
Var
Table:Tab:
Able:TAb;
kk.N.FN:longint;
S.FS:string[25];
PT:char;
п1.п2.п3.п4.п5 :integer;
Calculated : array [0..5.0..5.0..5.0..5.0..5] of boolean:
State : array [0..5.0..5.0..5.0..5.0..5] of longint:
Absent : tabu:
Procedure OutputData:
begin
assignCoutput.'twofive.ouf): rewrite(output):
if PT-'W
then writein (FN)
else writeln (FS);
close(output):
end;
function Cal estate(п1,п2,п3.п4.п5:1nteger)Jongint: var j.k : byte;
s : longint:
begin
if Ca1culated[nl.n2.n3.n4.n5]
then begin Cal estate:-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>T
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;
Cal estate:-s: end:
Procedure Init:
var 11.12.13.14.15 : integer; begin
for il:-0 to 5 do
for i2:-0 to 5 do
for i3:-0 to 5 do
for 14:-0 to 5 do
for 15:-0 to 5 do
begin
State[il,i2,i3,i4.i5]:-0:
if not ((il>-i2) and (12>-13) and (i3>-i4) and (14>-15))
then Calculated[il.12.i3.i4.i5]:-true
Листинг 6.2 (продолжение)
else Calculated[il.i2.i3.i4.i5]:-false: end;
Stated.О.О.О.О]:-!; Calculated[1.0.0.0.0]:-True;
CalcState(5.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 Tableti]:-MaxS:
Table[l]:-1;
for i:-2 to MaxT do Absent[i]:-true:
for i:-2 to MaxT do
if Table[i]<>MaxS then Absent[Tabie[i]]:-fa1se:
for i:-l to MaxT do Able[i]:-0:
Able[2]:-l; 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 : аггау[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]):
nl:-t[l]; n2:-t[2]: n3:-t[3]: n4:-t[4]: n5:-t[5]:
{2}{n4:-0; n5:-0:}
Max:-Table[2]; n:-0: for i:-l to MaxT do if Tabled]<>MaxS then begin if Table[i]>Max then Max:-Table[i]; inc(n); end;
Right:-(Max-n): end:
procedure Put(Table:Tab:i:byte:Abie:TAb;j:byte:Absent:tabu): begin
if (Able[j]-1) and (i>Table[P[j.l]J) 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(Tabiе.п1.п2,п3.п4.п5) 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>-MaxT-1 then begin Rest:-1;
exit: end;
Листинг 6.2 (продолжение)
for i:-2 to МахТ do АЫе[1]:-0:
for i:-1 to MaxT do
begin
if Table[i]-MaxS then continue:
if (z[i,l]oO) then Able[z[i .1]]:-1:
if (z[i ,2]o0) then Able[z[i .2]]:-l:
Able[i]:-0:
Absent[Tabie[i]]:-false:
end:
s:-0:
for k:-2 to MaxT do
if Absentee]
then begin i:・k:
break;
end;
for t:-2 to MaxT do PuKTable.i .Able.t.Absent);
Rest:-s:
end;
procedure FindNumber:
function MinAbled:Integer):integer;
var j.il.i2: integer:
mas: array [0..MaxS] of byte:
Good.Goodl,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)ol) and
(j>Table[i-l]) and (j>Table[i-Rang]));
Good:-Goodl and Good2;
if Good
then begin
MinAble:-j:
exit: end: end:
end;
var i,j.k.p.nl.n2.n3,q : byte:
begin
FN:-1:
Init2: j:-2;
for 1:-2 to MaxS-2 do
begin
Tabled ]:-j:
while Tabled]< (0rd(s[i])-0rd('А')+1) do begin
inc(FN.Rest(Table.i.Able.j.Absent)): inc(Table[i]);
repeat
q:-l:
while (q<-i-l) and (Table[q]oTable[iJ) do Inc(q):
if q<-i-l then inc(Table[i]):
until q-i:
end:
j:«^inAble(i+l):
end;
end;
procedure SetTabled,x:integer):
var Correct : boolean:
q : integer:
begin
repeat
Correct:-(i<-Rang) and (x>Table[i-U) or
(i>Rang) and
(((i mod Rang)-1) and (x>Table[i-Rang]) or
((1 mod Rang)ol) and
(x>Table[i-l]) and (x>Table[i-Rang])); q:T;
while (q<-i-l) and (Table[q]ox)
do Inc(q):
if (q<-i-l) or (not Correct) then inc(x);
until (q-i) and Correct:
Листинг 6.2 (продолжение)
Tableti]:-х; end:
procedure FindString;
var i.j.q : byte;
add : 1ongint;
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 (1<MaxT) do
begin
inc(i); SetTableCi.2); J:-Table[i]:
Add:-Rest(Tabie.i.Able.j.Absent):
end:
while (FN+Add<N) do
begin
inc(FN.Add);
SetTableCi,Table[i]+l): J:-Table[i]:
Add:-Rest(Tabie.i.Able.j.Absent):
end;
until 1-MaxT:
FS—A';
for i:-2 to MaxT do FS:-FS+char(Table[i]+ord('A')-1):
FS:=FS+Last[Rang]: end:
begin
InputData;
if PT-'W
then FindNumber
else FindString:
OutputData:
end.
Литература
Вальвачев А. Я., Крисевич В. С Программирование на языке ПАСКАЛЬ для персональных ЭВМ ЕС. - Мн.: Высш. шк.. 1989. 223 с.
Долинский М. С. Алгоритмизация и программирование на Turbo Pascal: от простых до олимпиадных задач. — СПб: Питер, 2004. 240 с.
Емеличев В. А, Мельников О. И., Сарванов В 〃.,Тышкевич Р. И. Лекции по теории графов.- М.: Наука, 1990.384 с.
Зуев Е. Л. Программирование на языке TURBO PASCAL 6.0,7.0. - М.: Радио и связь, 1993.384 с.
Касьянов В. Я., Сабельфельд В. К. Сборник заданий по практикуму на ЭВМ. — М.: Наука, 1986. 272 с.
Кирюхин В. М, Лапунов А. В., Окулов С. М. Задачи по информатике: Международные олимпиады 1989-1996. — М.: ABF, 1996.272 с.
КнутД. Искусство программирования для ЭВМ — Т. 1. Основные алгоритмы. — М.: Мир, 1976. 735 с.
Кнут Д. Искусство профаммирования для ЭВМ — Т. 2. Получисленные алгоритмы. 3-е изд.— М.: Издательский дом «Вильямс», 2000.828 с.
КнутД. Искусство протраммирования для ЭВМ - Т.З. Сортировка и поиск - М.: Мир, 1978.844 с. 10. Кормен Г, Лейзерсон ¥., Ривест Р. Алгоритмы: построение и анализ. — М.: МЦНМО, 1999.960 с. И. Котов В. М., Волков И. Л., Лапо А И. Методы алгоритмизации: Учеб, пособие для 9 класса общеобразовательной школы с углубленным изучением информатики. - Мн.: Народная асвета, 1997.160 с.
Котов В. М, Волков И. А, Харитонович А И. Методы алгоритмизации: Учеб, пособие для 8 класса общеобразовательной школы с углубленным изучением информатики. — Мн.: Народная асвета, 1996.127 с.
Котов В. М., Мельников О. И. Методы алгоритмизации: Учеб, пособие для 10-11 классов общеобразовательной школы с углубленным изучением информатики. — Мн.: Народная асвета, 2000. 221 с.
Кристофидес Я. Теория графов: алгоритмический подход. — М.: Мир, 1988. 250 с.
Липский В. Комбинаторика для программистов. — М.:, Мир, 1988. 368 с.
Новиков Ф. А Дискретная математика для программистов. — СПб: Питер, 2000.304 с.
Овсянников А. 77., Овсянникова Г В., Марченко А. 77., Прохоров Р. В, Избранные задачи олимпиад по информатике. — М.: Тровант, 1997.95 с.
Окулов С. М. Конспекты занятий по информатике (алгоритмы на графах): Учеб, пособие. —Киров: Вятский госпедуниверситет, 1996. 117 с.
Шень А X. Программирование: теоремы и задачи. — М.: МЦМНО, 1995. 264 с.
Алфавитный
указатель
А
алгоритм, 51
Крускала, 47
Прима, 47
Форда- Фалкерсона, 28
Хаффмана. 120
ACM ICPC Region Central European 1999,386
Northern Subregion 2000, 376
Central Subregion 2001, 382
3
задача
<Entropy>, 124 <Network>, 61
<Nextrec>, 126 <Rirliamcnt>, ИЗ <Pre-Post-crous>, 151
<Twofive>, 341 «Блокада», 198 «Борозды», 246 «Дерево», 110 <Игра, 21
<Кодированис>, 120
В
вершина внешняя. 77 внутренняя, 77
вес дуг, 13
«Кубики, 18
<Метро>, 59
«Новогодняя вечеринка, 16 о максимальном паросочстании в двудольном графе*, 14
Г
граф
взвешенный, 12 ориентированный, 12
д
двоичный поиск, 366
дерево
битово-индексированное, 82, 137
корневое, 77
отношений, 78
сортировки, 81
сумм, 82
дерево. 76, 78
дихотомический поиск, 366
дихотомия, 366
«Оппозиция», 92
<Падепие>, 235
<Склад>, 336
«Стены», 192
<Тстраэдр>, 186
И исток, 15
О
олимпиада
Студенческие АСМ:
ACM Northwestern Europe Programming
Contest (NWERC)
2000, 94
2002, 149
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 Wk (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, 110
Гомельская
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, Санкт-Петербург, пр. Б. Сампсониевский, 29а.
Налоговая льгота — общероссийский классификатор продукции ОК 005-93, том 2; 953005 — литература учебная.
Отпечатано с готовых диапозитивов в ФГУП «Печатный двор» им. А. М. ГорькогоФедерального агентства по печати и массовым коммуникациям.
197110, Санкт-Петербург. Чкаловский пр., 15.
М. С ДолинскийРешение сложных и олимпиадных задач по программированиюУЧЕТНОЕ /同貢ПЁКЕ"Книга написана опытным преподавателем, ученики которого с 1997 года регулярно становятся победителями белорусских национальных и международных олимпиад по информатике и программированию. Она является продолжением первой книги автора «Алгоритмизация и программирование на Turbo Pascal: от простых до олимпиадных задач». Цель данного учебного пособия — поднять уровень знаний читателя на более высокий:уровень участников и победителей международных олимпиад. Ориентирована на школьников и студентов, углубленно изучающих программирование, преподавателей, будет полезна для дополнительного и профильного образования, в том числе и при подготовке к олимпиадам.М. С ДолинскийРешение сложных и олимпиадных задач по программированиюУЧЕТНОЕ /同貢ПЁКЕ"Книга написана опытным преподавателем, ученики которого с 1997 года регулярно становятся победителями белорусских национальных и международных олимпиад по информатике и программированию. Она является продолжением первой книги автора «Алгоритмизация и программирование на Turbo Pascal: от простых до олимпиадных задач». Цель данного учебного пособия — поднять уровень знаний читателя на более высокий:уровень участников и победителей международных олимпиад. Ориентирована на школьников и студентов, углубленно изучающих программирование, преподавателей, будет полезна для дополнительного и профильного образования, в том числе и при подготовке к олимпиадам.
М. С Долинский
Решение сложных и олимпиадных задач по программированию
УЧЕТНОЕ /同貢ПЁКЕ"
Книга написана опытным преподавателем, ученики которого с 1997 года регулярно становятся победителями белорусских национальных и международных олимпиад по информатике и программированию. Она является продолжением первой книги автора «Алгоритмизация и программирование на Turbo Pascal: от простых до олимпиадных задач». Цель данного учебного пособия — поднять уровень знаний читателя на более высокий:уровень участников и победителей международных олимпиад. Ориентирована на школьников и студентов, углубленно изучающих программирование, преподавателей, будет полезна для дополнительного и профильного образования, в том числе и при подготовке к олимпиадам.
М. С Долинский
Решение сложных и олимпиадных задач по программированию
УЧЕТНОЕ /同貢ПЁКЕ"
Книга написана опытным преподавателем, ученики которого с 1997 года регулярно становятся победителями белорусских национальных и международных олимпиад по информатике и программированию. Она является продолжением первой книги автора «Алгоритмизация и программирование на Turbo Pascal: от простых до олимпиадных задач». Цель данного учебного пособия — поднять уровень знаний читателя на более высокий:уровень участников и победителей международных олимпиад. Ориентирована на школьников и студентов, углубленно изучающих программирование, преподавателей, будет полезна для дополнительного и профильного образования, в том числе и при подготовке к олимпиадам.
Основные особенности книги:
базируется на реальных задачах национальных и международных олимпиад для студентов и школьников за последние годы, содержит уникальную подборку условий задач на английском языке
с авторскими переводами;
«блочность» изложения материала: можно пропускать детали тем, кто уже все понял, и углубляться в изложение тем, кому он показался сложным;
обратная связь с обучаемыми: практические задания размещены на сайте http://dl.gsu.unibel.by; используя Интернет или электронную почту (dlrobot@gsu.unibel.by), можно послать на проверку собственное решение любой из приведенных в книге задач. Система автоматическом проверки, как правило, обеспечивает проверку решений в течение нескольких минут.
С^ППТЕР
1
9
“piter сот
rkov.piter.com
I информация о книгах и веб-магазин
М. С. Долинский
^ППТЕР
Решение сложных
и олимпиадных задач
по программированию
УЧЕБНОЕ /ПОСОБИЕ
• для школьников, студентов и их преподавателей
• теоретическиеосновы
и практическая реализация
• технологииразработки
• задания
для самостоятельной работы
2
4
М. С. Долинский
Решение сложныхи олимпиадных задачпо программированию
УЧЕБНОЕ ГЛ О СО Б ИЕ
衫птвр®
Москва • Санкт-Петербург • Нижний Новгород • ВоронежРостов-на-Дону • Екатеринбург • Самара • НовосибирскКиев • Харьков • Минск
2006
ББК 32.973-018я7
УДК 004.42(075)
Д64
Долинский М. С.
Д64 Решение сложных и олимпиадных задач по программированию:
Учебное пособие. — СПб.: Питер, 2006. — 366 с.: ил.
ISBN 5-469-00794-4
В книге рассматриваются решения оригинальных задач международных и национальных олимпиад по информатике и программированию для школьников и студентов. Задачи сгруппированы по темам: максимальный поток, минимальное остовное дерево, деревья, скрытые графы, стратегические игры, табло Янга. В начале каждой главы лаконично, но доступно излагается необходимый теоретический материал по теме, затем для каждой задачи приводятся условие, идея решения и описание конкретной реализации на языке программнровання Паскаль. Для школьников, студентов и их преподавателей.
ББК 32.973-018я7
УДК 004.42(075)
Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав.
Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги.
ISBN 5-469-00794-4ISBN 5-469-00794-4© ЗАО Издательский дом «Питер». 2СЮ6
ISBN 5-469-00794-4
ISBN 5-469-00794-4
Краткое содержание
Введение 8
Глава 1. Максимальный поток12
Глава 2. Минимальное остовное дерево45
Глава 3. Решение задач на деревьях и с помощью деревьев76
Глава 4. Скрытые графы 185
Глава 5. Стратегические игры296
Глава 6. Диаграмма Юнга 333
Литература363
Алфавитный указатель364
Содержание
Введение 8
От издательства 11
Глава 1. Максимальныйпоток12
Примеры задач на максимальный поток 12
Формальная постановка задачи14
Задача «Новогодняя вечеринка»16
Задача «Кубики»18
Задача «Игра» 21
Пример максимального потока на графе25
Алгоритм Форда-Фалкерсона 28
Решения задач33
Замечания по реализации 44
Глава 2. Минимальное остовное дерево45
Формальная постановка задачи45
Алгоритм Прима47
Алгоритм Крускала51
Быстрая сортировка54
Задача «Secret Pipes»55
Задача «Метро» 59
Задача «Network» 61
Решения задач63
Глава 3. Решение задач на деревьях и с помощью деревьев76
Практические примеры деревьев78
Деревья отношений 78
Деревья попиксельного представления плоских цветных образов78
Деревья представления сложных композиций трехмерных объектов79
Деревья кодирования символов79
Деревья сортировки81
Деревья сумм82
Перечисление деревьев82
Представление деревьев в памяти компьютера83
Порядок обхода деревьев 84
Организация материала и технология работы с ним 84
Задачи на основные свойства деревьев84
Задача «Is it a tree?» 85
Задача «Strategic game»89
Задача «Оппозиция»92
Задача «Erdos Numbers»94
Задача «Closest Common Ancestor» 96
Задачи на представление образов98
Задача «Unscrambling Images» 99
Задача «BSP Trees» 106
Задачи на двоичные деревья сортировки110
Задача «Дерево» 110
Задача «Parliament» 113
Задача «Falling Leaves»117
Кодирование последовательностей символов методом Хаффмана 120
Задача «Кодирование» 120
Задача «Entropy»124
Перечисление деревьев126
Задача «Nextree» 126
Задача «Trees Made to Order»132
Битово*индексированные деревья137
Задача «Мобильные телефоны» 137
Структура данных BIT141
Задачи для самостоятельного решения145
Задача «Knockout Tournament»145
Задача «Split Windows»147
Задача «Huffman Trees» 149
Задача «Pre-Post-erous!» 151
Решения задач153
Глава 4. Скрытые графы 185
Инцидентность областей 186
Задача «Тетраэдр» 186
Задача «Стены» 192
Задача «Блокада»198
Задача «Мудрый правитель»203
Задача «Ременная передача» 207
Отношения других видов 211
Задача «Currency Exchange»211
Задача «Exchange Rates» 215
Задача «Sorting It All Out» 220
Задача «Проверка веб-страниц»225
Задача «Play On Words» 230
Задачи на множествах отрезков234
Задача «Падение»235
Задача «The Doors»241
Задача «Борозды» 246
Задача «N-Credible Mazes»250
Задачи для самостоятельного решения254
Задача «Door Мап»254
Задача «This Sentence is false» 255
Задача «Will Indiana Jones Get There?» 256
Задача «I hate SPAM, but some people love it»257
Решения задач260
Глава 5. Стратегические игры296
Задача «Алиса и Боб»296
Задача «Ладья и конь» 300
Задача «Нечестная игра» 305
Как играть победно? 308
Задача «Игра loiwari» 308
Задача «Игра Score»314
Задача «Игра-2»319
Решения задач321
Глава 6. Диаграмма Юнга 333
Введение в диаграмму Юнга 333
Вставка и удаление элементов диаграммы333
Количество возможных диаграмм заданной формы (п1, п2, .... пМ) 335
Задача «Склад» 336
Задача «Twofive»341
Решения задач353
Литература363
Алфавитный указатель364