Текст
                    j
The Science
of Programming
DAVID GRIES
Springer-Verlag
New York • Heidelberg Berlin
1981


Д.ГРИС Наука ПРОГРАММИРОВАНИЯ Перевод с английского Н. Н. НЕПЕЙВОДЫ под редакцией А. П. ЕРШОВА МОСКВА «МИР» 1984
ББК 22.18 Г 86 УДК 681.3 Грис Д. Г85 Наука программирования: Пер. с англ. — М.: Мир, 1984.— 416 с, ил. Монография известного американского ученого написана как введение в науку программирования и отражает богатый опыт автора в научной и преподавательской работе. По своему замыслу она примыкает к известной книге Э. Деккстры «Дисциплина программирования» (М.: Мир, 1976). Автор знаком советским читателям по книге «Конструирование компиляторов для цифровых вычислительных машин» (М.: Мир, 1975). Для программистов и разработчиков математического обеспечения ЭВМ. 2405000000-183 ББК 22.18 Г 041 (01)-84 160"84' ч' * 517.8 Редакция литературы по математическим наукам Дэвид Грис НАУКА ПРОГРАММИРОВАНИЯ Научный редактор Бабынина Л. Н. Мл. научн. редактор Полякова Н. С. Художественный редактор Шаповалов В. И. Художник Бычков С. А. Технический редактор Потапенкова Е. С. Корректор Смирнов М. А. ИБ № 3618 Сдано в набор 27.02.84. Подписано к печати 31.08.84. Формат 60X907ie- Бумага типографская № 2 Гарнитура литературная. Печать высокая. Объем 13,00 бум. л. Усл. печ. л. 2 6,00 Усл. кр.-отт. 26,00. Уч.-изд! л. 23,50. Изд. N9 1/2618. Тираж 20 000 экз. Заказ № 1718. Цена 1 р. 90 к. ИЗДАТЕЛЬСТВО «МИР» 129820, Москва, И-110, ГСП, 1-й Рижский пер., 2 Отпечатано с матриц Ордена Октябрьской Революции и ордена Трудового Красного Знамени Первой Образцовой типографии имени А А. Жданова Союзполиграфпрома при Государственном комитете СССР по делам издательств, полиграфии и книжной торговли. 113054, Москва, Валовая, 28 в Ленинградской типографии № 4 ордена Трудового Красного Знамени Ленинградского объединения «Техническая книга» им. Евгении Соколовой Союзполиграфпрома при Государственном комитете СССР по делам издательств, полиграфии и книжной торговли. 191126, Ленинград, Социалистическая ул., 14. © by Springer-Verlag Berlin Heidelberg 1981 All rights reserved. Authorized translation from English language edition published by Springer-Verlag Berlin —Heidelberg — New York (g) Перевод на русский язык, «Мир», 1984
ПРЕДИСЛОВИЕ РЕДАКТОРА ПЕРЕВОДА Несмотря на помещенные ниже два предисловия, хотелось бы еще кое-что сказать об этой незаурядной книге. Дэвид Грис — один из лучших пропагандистов информатики. Во-первых, он прекрасно владеет ее техническим содержанием. Это владение основано как на его собственном существенном вкладе в теорию и методы трансляции, так и на широком и активном интересе к развитию области, который поддерживается личными контактами автора с ведущими специалистами. Во-вторых, он один из немногих специалистов, которые смотрят на информатику в исторической перспективе, улавливая тенденции развития и опираясь на исторические аналогии, обеспеченные хорошим знанием истории науки (и прежде всего истории математики — старшей сестры вычислительной науки). Это ощущение общей линии развития придает учебникам Гриса столь необходимые для хорошего преподавания качества стабильности и авторитета. Благодаря этому ощущению Грис избегает излишних конъюнктурных увлечений, характерных для некоторых авторов, пишущих в угоду коммерции и технике. В-третьих, Грис обладает присущим каждому прирожденному педагогу даром выделять в предмете главные черты, объяснять их просто в их видимой и понятной взаимосвязи, передавая тем самым учащемуся чувство уверенности и способности к прямому действию при решении задачи. Наконец, ему свойственна нетривиальная способность к синтезу американской и европейской образовательной традиции, что, как мне кажется, особенно необходимо для хорошей литературы по информатике, если учесть огромные социальные последствия предстоящего тотального вторжения ЭВМ в нашу жизнь. Хотелось бы заметить, что, по моему мнению, эта способность Гриса была воспитана не только его длительными визитами в Европу, но и особой интеллектуальной обстановкой, созданной профессорами старшего поколения Корнеллского университета, в котором он работает, и до некоторой степени характерной для всех университетов Новой Англии. Результат такого синтеза и есть предлагаемая вниманию читателей книга Д. Гриса. В ее внешнем выражении она является адаптацией известной книги Эдсгера Дейкстры «Дисциплина программирования» (М.: Мир, 1978). Структура книги весьма проста. В первой ее части излагаются элементарные сведения из исчисления высказываний и предикатов. Во второй части на основе пред- и постусловий очень подробно описывается логическая семантика про-
6 Предисловие редактора перевода стого императивного языка программирования (скалярные и векторные величины, присваивания, авосты, охраняемые команды с ветвлениями и итерацией, процедуры). Наконец, третья часть — «сердцевина книги» (по выражению автора) — содержит подробное изложение синтеза нескольких программ (исходя из спецификаций пред- и постусловий задачи), сопровождаемого логическим рассуждением, которое подтверждает правильность каждого шага синтеза. Сказать, однако, только это о книге — значит, не сказать почти ничего. Прежде всего в тексте книги, в подаче материала и в манере изложения находят воплощение те качества автора, о которых говорилось выше. Далее, книга Гриса является наиболее решительным учебником по программированию, который я читал. В первых ее главах показывается, что логика становится прикладной наукой, чья нотация, обогащенная символикой языков программирования, становится такой же неотъемлемой частью человеческой интеллектуальной практики, как и нотация математического анализа. В главной же своей части книга претендует на то, чтобы внушить учащемуся убеждение о невозможности программировать иначе, нежели по дисциплине, подсказываемой логическими правилами синтеза программ. Если считать переломную книгу Э. Дейкстры интеллектуальным откровением, то книга Д. Гриса — это апостольское деяние, направленное на то, чтобы превратить учение одиночки в мировую религию. Я не случайно позволил себе употребить столь далекие от научного языка аналогии, чтобы подчеркнуть, что если фактический облик «просвещенного программирования» еще не вполне прояснился, то мнение о том, что этот облик может сложиться только на основе неустанной и очень напряженной подвижнической работы мыслителей и педагогов, мне представляется неоспоримым. Перевод книги вызвал некоторые специфические трудности. Книга написана «устным» языком, очень азартно, часто в виде прямого обращения к читателю. Кое-где, по нашим стандартам, текст ее просто сыроват. Переводчик и редактор неоднократно подвергались искушению обставить перевод разного рода замечаниями, однако они сознательно решили этого не делать, чтобы не превращать стимулирующее чтение в жвачку, тем более что при внимательном чтении и повседневной работе с этой книгой у активного читателя или преподавателя может появиться немало своих замечаний и предложений по улучшению изложения материала. Естественно, однако, что эти усовершенствования могут появиться лишь как реакция на данный текст. Именно так и достигается прогресс в науке и образовании, когда хорошая книга дает толчок к дальнейшей работе мысли. Академгородок май 1983 г. Л. /7. Ершов
ВСТУПЛЕНИЕ Я надеялся, что такой учебник напишет кто-нибудь вроде про* фессора Дэвида Гриса, а так как соперников у него нет, то я просто надеялся, что напишет его именно он. Тема не допускает автора меньшей величины. В течение последнего десятилетия значение слова «программа» претерпело глубокое изменение: «программы», которые мы писали десять лет назад, и «программы», которые мы можем писать сегодня, выполняются на машине, и это единственное, что их объединяет. За исключением этого поверхностного сходства, они настолько разительно отличны друг от друга, что употребление для них общего термина может привести к путанице. Разница между «старой программой» и «новой программой» столь же глубока, сколь глубока разница между гипотезой и доказанной теоремой или между математическим наблюдением и утверждением, строго выведенным из свода постулатов. Вспомнив, сколько веков потребовалось человечеству, чтобы полностью осознать глубину указанного различия, мы получим представление о грандиозности стоящей перед нами образовательной проблемы: кроме обучения техническим деталям, мы должны преодолеть барьер умственного сопротивления, всегда возникающего при успешной демонстрации применения методов научного мышления к новой области человеческой деятельности. (Мы уже наслышаны о всех таких возражениях, которые настолько тради- ционны, что их можно угадывать наперед: «старые программы» достаточно хороши; «новые программы» не лучше и слишком трудны для построения в реальных ситуациях; правильность программ гораздо менее важна, нежели правильность спецификаций; «реальный мир» живет без доказательств и т. д. и т. п. Обычно эти возражения исходят от тех, кто не владеет приемами, против которых они возражают.) Не так просто объяснить, как эта формальная система позволяет строить «новые программы». Новые формализмы всегда страшат, и требуются значительные и тщательные педагогические ^усилия, чтобы убедить новичка в том, что формализм не только полезен, но и необходим. Отбор и последовательность примеров так же важны, как хороший вкус в применении формализма. Сделать материал доступным может лишь ученый, который объединяет научную причастность к предмету с бесценным даром прирожденного педагога. Нам повезло — профессор Дэвид Грис принял этот вызов. Эдсгер В. Дейкстра
ПРЕДИСЛОВИЕ В Оксфордском словаре английского языка относительно термина «наука» говорится, в частности, следующее: «Однако иногда термин наука понимается расширительно так, что в него включаются области практической деятельности, в которых требуется знание общих принципов и их осознанное применение; в противоположность этому в искусстве требуется лишь знание традиционных правил и достаточно высокое мастерство». Именно в этом контексте и выбрано название настоящей книги. Программирование начиналось как искусство; даже сейчас большинство учится ему, наблюдая, как работают другие (например, преподаватель или более опытный коллега), постигая приемы и мало задумываясь над принципами, которые лежат в их основе. Однако в результате научных исследований последнего десятилетия найдены некоторые полезные теоретические положения и общие принципы, так что наступает время, когда можно начинать учить принципам и их осознанному применению. В этом учебнике я сделал попытку поделиться своим пониманием возникающей на наших глазах науки программирования и восхищением ею. Такой подход к предмету требует некоторой математической культуры и желания постигать новое. Материалом книги может овладеть программист с двухлетним стажем или студент старших курсов, специализирующийся по математическому обеспечению (по крайней мере это тот уровень, на который я рассчитываю). Используемый в книге подход обычно критикуют за то, что он полезен лишь для небольших (одна-две страницы текста), хотя, возможно, и сложных программ. Даже если бы это было действительно так, это не повод, чтобы игнорировать подход. По моему мнению, такой подход дает лучший способ суждения о программах, и я верю, что в ближайшие десять лет он распространится на большие программы. Более того, поскольку каждая большая программа состоит из многих небольших, можно без опаски утверждать, что Никто не сможет научиться хорошо составлять большие программы, пока он не научится хорошо составлять малые. Хотя, конечно, успех не всегда можно гарантировать, мой опыт показывает, что применяемый в книге подход часто приводит за то же время к более коротким, ясным и правильным программам. К тому
Предисловие 9 же он приводит к новому способу мышления, при котором программист уделяет больше внимания определению переменных, хорошему стилю и ясности. Поскольку сейчас большинство программистов сталкивается с трудностями при составлении даже небольших программ, а написанные ими такие программы не очень удобочитаемы, то польза изучения нашего подхода несомненна. В книге не говорится (или почти не говорится) о том, как искать ошибки, как сделать программу надежной, как ее тестировать и т. д. Это объясняется не тем, что эти вопросы неважны или наш подход не может их охватить. Просто для того, чтобы яснее изложить материал, нужно сосредоточиться на одном аспекте, а именно на построении правильной программы. Преподаватель, использующий эту книгу, может при желании пользоваться и другими источниками. Структура книги Часть I служит введением в исчисления высказываний и предикатов. Владение этим материалом важно, поскольку исчисление предикатов должно стать средством практических рассуждений о программах. Любая дисциплина для описания очень сложных явлений обычно обращается к математике, чтобы та помогла ей преодолеть эти сложности. Программирование не является исключением. Само собой разумеется, что я попытался изложить материал с точки зрения программиста. Полнота, корректность и т. п. не упоминаются вообще, поскольку программисту они не необходимы. Он нуждается прежде всего в том, чтобы преобразовывать и упрощать высказывания и предикаты при построении программ. Довольно длинная гл. 3 описывает «систему естественного вывода» для формализации рассуждений. Я сам писал эту главу с намерением изучить такие системы и посмотреть, насколько они пригодны для суждений о программах, поскольку на них основано много программ верификации. По моему заключению, более традиционный подход гл. 2 более полезен, но я оставил гл. 3 для тех, чьи вкусы ближе к системам естественного вывода. Главу 3 можно пропустить целиком, хотя она может быть полезна для курса, более ориентированного на формальную логику и теорию. Те, кто знаком с основными понятиями логики, могут начинать чтение книги прямо с части II и обращаться к части I лишь за обозначениями и соглашениями. Преподаватель, использующий эту книгу, может, конечно, сообщать материал в ином порядке, например касаться кванторов лишь тогда, когда они впервые понадобятся. Часть II содержит определение простого языка программирования через слабейшие предусловия. Важнейшими являются гл. 7 и 8, разд. 9.1 и 9.2, гл. 10 и 11; они необходимы в дальнейшем для понимания построений программ. Часть этого материала (например,
10 Предисловие формальные определения конструкций повторения и доказательство теоремы (11.6) об использовании инвариантов цикла) можно опустить, но я полагаю, что владение этим материалом полезно. Часть III — сердцевина книги. Чтобы активизировать процесс чтения книги, я попытался применять в ней следующий прием. В некоторых местах имеются вопросы, на которые должен ответить сам читатель. За вопросом идет пробел, затем горизонтальная черта и опять пробел. После того как читатель дал свой ответ, он может заглянуть под черту и прочитать мой ответ. Такая активная работа труднее, чем простое чтение, но много полезнее. Глава 21 написана ради удовольствия. Она посвящена обращению программ, изобретенному Э. Дейкстрой и его коллегой В. Фей- еном. Полезно ли это на практике, неизвестно, но сам прием любопытен. В гл. 22 дано несколько простых правил документирования программ; ее можно читать независимо от остальной части книги. Глава 23 содержит краткое описание истории создания науки программирования, а также возникновения рассмотренных в книге программистских задач. Символ □ означает конец теоремы, определения, примера и т. п. На фотонаборном устройстве, использованном мною, кванторы «для каждого» и «существует» были труднодоступны, и я использовал А и Е *>. В тех немногих местах в книге, где «он» и его производные означают человека, обозначаемое лицо может быть любого пола. Благодарности Все знакомые с монографией Э. Дейкстры «Дисциплина программирования» увидят всюду в моей книге следы ее влияния. Дейкстре принадлежат исчисление для построения программ, стиль их построения и многие из примеров. К тому же его замечания на первые варианты книги были для меня бесценными. Столь же важной для меня была и деятельность Тони Хоара. Его статья об аксиоматическом базисе программирования явилась началом новой эры благодаря не только предложенной технике, но и хорошему вкусу и стилю — ее влияние на меня до сих пор нисколько не ослабло. Глубокие и очень подробные замечания Тони по черновику гл. 1 заставили меня перестроить и переписать большую часть этой главы. Я благодарен Фреду Шнейдеру, прочитавшему первые наброски всех глав. Его технические и стилистические замечания использованы почти во всех разделах. Многие сделали мне существенные и конструктивные замечания по различным частям рукописи. За эту помощь я считаю своим 1) В русском тексте используются традиционные символы у и ^.-—Прим. перев.
Предисловие 11 долгом поблагодарить Г. Эндрюса, М. Гордона, Э. Хенера, Г. Левина, Д. Макилроя, В. Мел вилла, Дж. Мисра, X. Перкинса, Дж. Уильямса, М. Вуджера, Д. Райта. Я обязан также всему коллективу моих коллег из Корнеллского университета. Студенты, слушавшие курс CS600, были моими «подопытными кроликами» все последние пять лет, и как преподаватели, так и студенты добродушно терпели мои проповеди о программировании. Корнелл был отличным местом для проведения этого моего эксперимента. Я сам напечатал и отредактировал эту книгу, используя факультетскую ЭВМ PDP 11/60-VAX, работающую под управлением UNIX*\ и экранный редактор, написанный для Terak Файл, содержавший книгу, был объемом 844 592 символа. Окончательный текст изготовлен при помощи программы troff и фотонаборного устройства Comp Edit из лаборатории графики Корнеллского университета. Д. Макилрой ввел меня в таинства troff; А. Демерс, Д. Крафт и М. Хаммонд помогали справиться с системой PDP 11/60-VAX; А. Демерс, В. Джинграс, Ш. Халас потратили много часов, чтобы помочь мне приспособить выдачу программы troff к фотонаборному устройству. Я им весьма обязан. Национальный научный фонд постоянно оказывал мне поддержку в исследованиях, что и привело к появлению этой книги. И наконец, я благодарен моей жене, Элейн, и детям, Полю и Сузен, за их любовь и заботу в последние полтора года. *> UNIX —фирменная марка операционной системы лаборатории фирмы «Белл»,
ЧАСТЬ О Зачем нужно использовать логику и доказывать правильность программ? Рассказ Только что мы закончили писать большую (3000 строк) программу. Помимо всего прочего, в качестве промежуточных результатов она вычисляет частное q и остаток г от деления неотрицательного числа х на положительное целое у. Например, если х~7 и у—2, то получим <7=3 (так как 7^-2=3) и г==1 (так как остаток от деления 7 на 2 равен 1). Ниже приводится фрагмент нашей программы (многоточиями здесь заменены части, которые предшествуют вычислению частного и остатка и которые следуют за ним). Мы выбрали именно такой способ вычислений, потому что программа будет иногда выполняться микрокомпьютером, для которого не предусмотрена операция деления целых чисел, а переносимость должна быть обеспечена во что бы то ни стало! Вычисление частного и остатка выглядит, в сущности, очень просто; поскольку операцией -f- воспользоваться нельзя, мы решаем последовательно вычитать делитель у из копии х (подсчитывая при этом, сколько вычитаний было сделано) до тех пор, пока не будет получен отрицательный результат. г := Х\ q := 0; while r > у do begin r := г—у\ q := q+\ end; Теперь мы готовы приступить к отладке программы. Нам нельзя отказать в сообразительности, и мы сразу понимаем, что делитель в самом начале должен быть больше 0, а в конце вычислений переменные должны удовлетворять условию x=y*q+r. Поэтому для проверки вычислений мы дополняем программу не- сколькими операторами вывода: write ('делимое х~'} х9 'делитель у**', у);
Часть 0. Зачем доказывать правильность программ? 13 г := х\ q := 0; while r > у do begin r := г —у; ? '•= <7+1 end; write ('y*q + r = 'y y*q + r)\ К сожалению, на нас обрушивается огромный поток печатаемой информации, так как данный блок находится в цикле, и наш первый отладочный прогон оказывается неудачным. Попытаемся быть более разборчивыми в том, что печатать. В самом деле, знание этих значений понадобится нам только в том случае, когда будет обнаружена ошибка. Мы решаем опробовать одно новое средство, которым, как мы слышали, был недавно оснащен транслятор. Если логическое выражение заключено в скобках { и }, оно проверяется во всех тех случаях, когда мы попадаем в ту точку программы, где оно записано: если ложь, печатаются соответствующее сообщение и значения переменных программы; если истина, программа продолжает выполняться обычным образом. Такие логические выражения называют утверждениями, поскольку фактически они утверждают выполнение некоторого свойства в тот момент, когда управление передается в ту точку программы, где они выписаны. Системные программисты советуют включать такие утверждения в программу, потому что это способствует ее документированию. Опасения по поводу возможного снижения эффективности выполнения программы напрасны — в трансляторе предусмотрен переключатель, позволяющий выбрасывать эти проверки. Но, немного поразмыслив, мы приходим к выводу, что все-таки лучше всегда проверять утверждения, так как ошибка при рабочем запуске может стоить гораздо больше. Поэтому мы добавляем к программе утверждения: \у>Щ г := х\ q := 0; (1) while r > у do begin r := г—у\ q :== q+\ end; {x = y*q + r\ На этот раз мы получаем для анализа гораздо меньшую по объему выдачу, так что достигнут явный успех! При проверке утверждений обнаруживается ошибка во время отладочного запуска: переменная у равна 0 как раз перед выполнением рассматриваемого фрагмента. Понадобилось лишь 4 часа работы, чтобы найти ошибку в вычислении у и исправить ее. Зато весь следующий день проходит в поисках ошибки, для которой никакой вразумительной аварийной выдачи мы не получили. В конце концов мы обнаруживаем, что вычисление частного и
14 Часть 0. Зачем доказывать правильность программ? остатка дало следующие результаты: х=6у у=3, q=lf г=3. Бесспорно, оба утверждения из (1) истинны для этих значений, но все дело в том, что остаток должен быть меньше делителя, а это не так. Значит, условие цикла должно быть г^у, а не г>у. Если бы утверждение о результате было достаточно сильным, т. е. напиши мы утверждение x=y*q+r and r<y, день не пропал бы впустую! Почему же мы не подумали об этом раньше? Исправляем ошибку и помещаем более сильное утверждение: {7>0} г := х\ q := 0; while r^ у do begin r := г—у; q := q+l end; \x~y*q + r and r < y\ Некоторое время все идет прекрасно, но вдруг неожиданно мы получаем невообразимую выдачу. В результате работы алгоритма получен отрицательный остаток г=—2. Но остаток не должен быть отрицательным! Мы обнаруживаем, что г стало отрицательным, потому что первоначально х был равен —2. Ох, еще одна ошибка при вычислении входных данных нашего алгоритма частного и остатка — не предполагалось, что х может быть отрицательным. Но ведь мы могли обнаружить ошибку раньше и не тратить два дня на ее поиск, и, действительно, мы должны были обнаружить ее раньше; все, что от нас требовалось,— это сделать входное и выходное утверждения фрагмента программы достаточно сильными. Снова исправляем ошибку и усиливаем утверждение: {б<* and 0<y\ г : = х\ q := 0; while r ^ у do begin r := г—у\ q := q+l end; \x = y#q + r and 0^r<y\ Конечно, прекрасно было бы сразу придумывать такие правильные утверждения, а не исправлять их от случая к случаю. Что же нам в этом мешает? Разве отладка так и должна быть серией проб и ошибок? Отчасти проблемы возникали здесь из-за нашей невнимательности при описании того, что должен делать фрагмент программы: нам следовало бы написать входное утверждение (О^л: and 0<Cy) и выходное утверждение (x=y*q+r and Q^r<Cy) перед тем, как писать сам фрагмент программы, поскольку именно эти утверждения и образуют определение частного и остатка.
Часть 0. Зачем доказывать правильность программ? 15 Ну, а что можно сказать об ошибке, допущенной при определении условия цикла? Не могли бы мы предотвратить ее с самого начала? Есть ли возможность доказать, исходя из программы и относящихся к ней утверждений, что всегда, когда управление приходит в точку, где помещено утверждение, это утверждение истинно? Посмотрим, что можно сделать. Непосредственно перед началом цикла одна из частей утверждения о результате (2) x=y*q+r очевидно, истинна, поскольку x=r, a q=0. И судя по присваиваниям, входящим в тело цикла, можно заключить, что если (2) истинно перед выполнением тела цикла, то оно остается истинным и после его выполнения, следовательно, оно истинно именно до и после каждого повторения цикла. Вставим (2) в качестве утверждения в нужных местах программы и сделаем утверждения как можно более сильными: {6 < х and 0 < у} г := х\ q := 0; {0 <> and 0 < у and x = y*q + r\ while r^> у do begin ]0<> and 0<y^r and x = y*q-\-r\ r := r—y\ q := q+\\ {0<> and 0<y and x — y*q + r\ end; {0<><;*/ and x*=*y*q-\-r\ Итак, как же легче всего определять правильное условие цикла, или, если условие задано, как доказать, что оно правильно? Когда цикл заканчивается, это условие ложно. По завершении мы хотели бы иметь г<Су, так что его дополнение г^у должно быть правильным условием цикла. Как все оказывается просто! Очевидно, если бы мы знали, как сделать все утверждения по возможности более сильными, если бы мы научились как следует обдумывать и утверждения, и сами программы, то не совершали бы так много ошибок. Мы бы знали, что наша программа правильна, и нам даже не пришлось бы ее отлаживать! А значит, время, потраченное на прогоны тестов, изучение задач и поиски ошибок можно было провести совсем иначе. Обсуждение Из рассказа следует, что утверждения (или просто логические выражения) в самом деле необходимы в программировании Но не достаточно знать, как составлять логические выражения; надо
16 Часть 0. Зачем доказывать правильность программ? знать, как рассуждать, их используя: упрощать, доказывать, что одно вытекает из другого, доказывать, что в некоторых состояниях данное утверждение не истинно, и т. д. А позднее мы еще увидим, что необходимо использовать утверждения, отличающиеся от обычных логических выражений в языках Паскаль, ПЛ/1 или Фортран, а именно квантифицированные утверждения. Знать, как рассуждать об утверждениях,— это одно, а знать, как рассуждать о программах,— нечто другое. За последнее десятилетие информатика далеко продвинулась в изучении вопросов доказательства правильности программ. Мы достигли такого уровня, когда этот материал может излагаться студентам старших курсов и всем тем, кто имеет некоторые навыки программирования и желание усовершенствовать свои знания. Более важно то, что изучение доказательств правильности программ привело к открытию и разработке методов построения программ. Исходным положением является то, что программу и ее доказательство пытаются строить рука об руку, при этом доказательство прокладывает путь! Если методы применяются аккуратно, они приводят к программам, не содержащим ошибок, требующим меньше времени для построения и отладки и намного более понятным (для тех, кто владеет предметом). Выше я упоминал, что программа может не содержать ошибок, откуда следует, что в некотором смысле отладка может перестать быть необходимой. Это место нуждается в разъяснении. Даже тогда, когда мы станем лучше разбираться в программировании, мы все равно будем делать ошибки, хотя бы синтаксические (описки): мы всего лишь люди. Следовательно, всегда будет требоваться какая-то проверка. Но она не будет называться отладкой, поскольку само слово «отладка» подразумевает наличие ошибок, избавиться от которых полностью очень трудно* сколько бы мы мух ни перебили, они снова появятся *). Упорядоченный метод программирования мог бы приводить к более достоверным программам, нежели это занятие. Мы пропускали бы тесты не для поиска ошибок, а для увеличения нашего доверия к программе, которая заведомо почти наверняка правильна. Нахождение ошибок должно стать скорее исключением, чем правилом. После этих объяснений вернемся к нашей первой теме — изучению логики. *> Надо иметь в виду, что на английском программистском жаргоне ошибка называется словом bug (клоп), а отладка (debugging) в буквальном смысле означает «выведение клопов» — процедура сколь неизбежная, столь и малорезультативная,— Прим. ред.
ЧАСТЬ I Высказывания и предикаты В гл. 1 определяется синтаксис высказываний, т. е. логических выражений, использующих лишь логические, или булевы, переменные, и показывается, как их вычислять. В гл. 2 даны правила работы с высказываниями, которые часто применяются с тем, чтобы найти более простые, но эквивалентные формы высказываний. Эта глава важна для дальнейшего, и потому изучать ее материал нужно внимательно. В гл. 3 вводится система естественного вывода для доказательства теорем в исчислении высказываний. Эта система, как предполагается, в некотором смысле имитирует структуру наших «естественных» рассуждений. Такие системы используются в исследованиях по машинной проверке доказательств правильности программ, и знакомство с ними порой нужно. Но материал гл. 3 не используется в остальном тексте книги и может быть без ущерба опущен. В гл. 4 высказывания обобщаются. Добавляются переменные других типов, кроме логических, и вводятся кванторы. Дается исчисление предикатов, в котором можно выражать утверждения о программных переменных и работать с ними. Вводятся свободные и связанные переменные, изучается понятие подстановки. Этот материал необходим для дальнейшего чтения. В гл. 5 рассматриваются массивы. Представление о массиве как о функции *) из области значений индексов в область значений элементов, а не как о совокупности независимых переменных ведет к довольно изящным обозначениям и правилам работы с массивами. Два первых раздела этой главы прочесть необходимо, третий при первом чтении может быть опущен. И наконец, в гл. 6 кратко обсуждается использование утверждений в программах, о чем подробно говорится в следующих двух частях книги. *> Читатель должен привыкнуть, что автор трактует функции как однозначные отображения из области аргументов в область значений и поэтому постоянно говорит о «функции из одной области в другую».— Прим. ред%
Глава 1 ВЫСКАЗЫВАНИЯ Мы стремимся получить возможность описывать множества состояний программных переменных, а также писать четкие, ясные, недвусмысленные утверждения о программных переменных и манипулировать ими. Сначала рассмотрим лишь переменные (и выражения) типа «логический», или «булев». С операционной точки зрения такие переменные принимают одно из двух значений: Т или F> которые представляют понятия «истина» и «ложь» соответственно. Термин «булев» произошел от фамилии английского математика XIX в. Джорджа Буля, который начал изучение логики алгебраическими методами *}1). Вслед за специалистами по логике мы используем термин высказывание для того вида булевых, или логических, выражений, которые определяются и обсуждаются в этой главе. Высказывания подобны арифметическим выражениям. Имеются операнды, представляющие значения Т и F (а не целые числа), и операции (например, and,'or вместо*, +), а скобки используются, чтобы определить порядок вычислений. Трудность состоит не в том, как определить понятие высказывания или вычислять их, а в том, как выражать утверждения, записанные на естественном языке, в виде высказываний и как рассуждать, используя их. 1.1. Высказывания с полным набором скобок Высказывания строятся по следующим правилам (попутно определяющим и набор операций). Как будет видно, скобки требуется ставить вокруг каждого высказывания, включающего операцию. Это ограничение, которое будет далее ослаблено, позволяет нам на время отвлечься от вопросов старшинства операций. 1. Г и F — высказывания. 2. Идентификаторы являются высказываниями. (Идентификатор — это непустая последовательность букв и цифр, начинающаяся обязательно с буквы.) 3. Если b — высказывание, то (~]Ь) — высказывание. 4. Если b и с—высказывания, то (Ь/\с), ф\/с), (Ь=фс), (Ь=с)— также высказывания. *> Здесь и далее арабскими цифрами со скобкой помечены примечания переводчика, помещенные в конце книги.— Прим. ред.
Гл. 1. Высказывания 19 Эти синтаксические правила легче понять, записав их в виде нотации БНФ (БНФ кратко описана в приложении 1): (1.1.1) <высказывание>) : = Т \F |идентификатор) | (~] высказывание» | ^высказывание) Л высказывание» | (<высказывание>\/высказывание)) | ^высказывание) => <высказывание» | «высказывание) = <высказывание» Пример. Следующие выражения — это высказывания, разделенные запятыми: F, (1Л, ФУхуг), (ПЬ)Л(с=>Ф), ((abcl=id)A(~\d)) □ Пример. Следующие выражения — не высказывания: FF, (bV(c), Ф)Л), а + ) П Как видно из синтаксиса, на значениях типа «логический» определено пять операций: отрицание: (not b) или ("]&), конъюнкция: (b and с) или фЛс), дизъюнкция: ф or с) или (bye), импликация: (b imp с) или ф=$с), равенство, или эквивалентность: ф equals с) или ф~с). Для каждой операции используются два различных обозначения! имя и математический символ. Имя легче использовать на машинке, не имеющей соответствующего математического символа. Оно соответствует названию операции в английском языке. Используется следующая терминология: (Ь/\с) называется конъюнкцией, b и с — конъюнктивными членами или конъюнктами; (Ь\/с) называется дизъюнкцией, ее операнды Ь, с — дизъюнктивными членами или дизъюнктами-, (Ь=>с) называется импликацией, ее операнды — b и с, где b называется посылкой, ас — заключением. 1.2. Вычисление постоянных высказываний Только что мы привели синтаксис высказываний, т. е. определили множество правильно построенных высказываний. Теперь мы придадим им семантику (смысл), показав, как их вычислять. Начнем с того, что определим вычисление постоянных высказываний, содержащих в качестве операндов только константы. Определим это вычисление разбором трех случаев, определяемых структурой высказывания е: е без операций, е с одной операцией, е с более чем одной операцией.
20 Часть I. Высказывания и предикаты (1.2.1) Случай 1. Значение высказывания Т есть Т\ значение высказывания F есть F. (1.2.2) Случай 2. Значения (~|&), (ЬДс), (bVO. (&=><?) и (6 = с), где Ьу с —это константы Г, F в любой комбинации, определяются по следующей таблице, называемой таблицей истинности. В строчках таблицы выписаны комбинация значений b и с к значения операций для каждой из наших пяти операций. Например, в последней строке показано, что значение ("IT")—это F, а значения (ТАТ), (ТУТ), (Г=>Г), (Т = Т)—это Т. (чЬ) (ЬАс) (ЬУс) (Ъ^>с) (Ь=с) (1.2.3) F Г Г F J Т Т F F Т Т Т F T T F F F T F F F Т Т Т Т (1.2.4) Случай 3. Значение постоянного высказывания, содержащего более чем одну операцию, получается последовательными применениями (1.2.2) к подвысказываниям этого высказывания и заменой подвысказываний на их значения до тех пор, пока высказывание не сведется к Т или F. Приведем пример вычисления высказывания! ((TAT)=*F) = (T=*F)-F Замечание. Такое описание операций с помощью таблицы истинности, перечисляющей все возможные комбинации операндов и значения операций, возможно лишь потому, что множество возможных значений конечно. Например, для натуральных чисел таких таблиц построить нельзя. □ Названия операций довольно тесно связаны с их значениями в естественном языке. Например, «не истина» обычно значит «ложь», а «не ложь» — истина. Но отметим, что операция V (или) означает «включающее или», а не «разделительное или». Это означает, что (TV T) есть 7\ в то время как для «разделительного или» это было бы ложью. Операция =>- (следование) не означает никакой причинно-следственной связи. Предложение «Если идет дождь, прогулка отменяется» может быть записано в форме высказывания (дождь => нет прогулки). Из этого предложения мы обычно выводим, что отсутствие дождя означает, что прогулка состоится. Но такого вывода нельзя сделать из высказывания (дождь =>- нет прогулки).
Гл. 1. Высказывания 21 1.3. Вычисление высказываний в данном состоянии Высказывания, подобные ((~\c)\/d)y могут встречаться в программе в разных местах, например в присваивании b := ((~\c)\/d) или в условном операторе if ((~]c)\/d) then... . Когда должно быть выполнено предложение, в котором встречается высказывание, высказывание вычисляется при данном «состоянии» машины и должно привести к Т дли F. Определение такого вычисления требует более аккуратно разобраться с понятием «состояние». Состояние сопоставляет идентификаторам значения. Например, в состоянии, скажем, s идентификатору с могло бы быть сопоставлено значение F, а идентификатору d — значение Т. В образах памяти машины это может быть выражено так: ячейки с именами cud содержат значения F и Т соответственно. В другом состоянии сопоставление может быть другим, например (с> Т) и (d, F). Важным моментом здесь является то, что состояние — это множество пар (идентификатор, значение), в котором все идентификаторы различны, т. е. состояние есть функция. (1.3.1) Определение. Состояние s — это функция из множества идентификаторов в множество значений Т и F. □ Пример. Пусть состояние s определено множеством {(а, Г), (be, F), (yl, 71)}. Тогда s(a) обозначает значение, определяемое применением состояния (т. е. функции) s к идентификатору a: s(a)=T. Подобно этому, s(bc)=F и s(y\)=T. □ (1.3.2) Определение. Высказывание е определено в состоянии s, если каждый идентификатор из е встречается в s. В состоянии s={(b, 71), (с, F)} высказывание {b\Jc) определено, а высказывание (b\)d) — нет. Чтобы определить значение высказывания в данном состоянии, обобщим обозначение s (идентификатор). Для произвольных состояния s и высказывания е s(e) означает значение, вычисляемое по е в состоянии s. Так как идентификатор b также является высказыванием, мы должны позаботиться о том, чтобы s(b) по-прежнему означало значение b в состоянии s. (1.3.3) Определение. Пусть высказывание е определено в состоянии s. Тогда s(e), значение е в состоянии s,— это значение, получаемое заменой всех вхождений идентификаторов b в высказывание е на их значения s(b) и вычислением получившегося постоянного высказывания по правилам разд. 1.2. □ Пример. Вычислим s(((~]b)\/c)) в состоянии s = {(6, Г), (с, F)}: *((ПЬ)\ус)) ee((~17,)V^?) Ф заменяется на Т, с—на F)
22 Часть I. Высказывания и предикаты 1.4. Правила старшинства для операций В предыдущих разделах мы имели дело с ограниченным классом высказываний, с тем чтобы вычисление высказываний можно было бы объяснить, не затрагивая вопроса старшинства операций. Теперь мы ослабим это ограничение. Скобки можно при желании опускать или ставить вокруг любого высказывания. Например, высказывание ((b\/c)=>d) может быть записано как b\/c=>d. В случае, если скобки опущены, порядок вычисления высказываний определяется дополнительными правилами. Эти правила, подобные аналогичным правилам для арифметических выражений, следующие: 1. Последовательность одноименных операций вычисляется слева направо, например b/\c/\d эквивалентно ((b/\c)/\d). 2. Порядок вычислений различных между собой и смежных в записи высказывания операций определяется следующим списком: "] (имеет наивысшее старшинство и связывает сильнее всего), Л> Обычно, чтобы сделать порядок вычисления яснее, лучше свободно использовать скобки, и мы так и будем поступать 2}. Примеры. ~\b~b/\c эквивалентно (~|Ь) = (ЬЛ£), b\f~\c=$>d эквивалентно (b\/(~\c))=$>dt b=$>c=$>d/\e эквивалентно (&=>с)=Ф {d/\e) Q Следующая грамматика в БНФ определяет синтаксис высказываний так, чтобы из их структуры могло быть выведено старшинство операций. (Нетерминальное понятие (идентификатор) оставим неопределенным, оно употребляется в обычном смысле.) 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. П. 12. 13. <высказывание> ^импликация) <дизъюнкция> <конъюнкция> <множитель> : :: = <импликация> | <высказывание> = <импликация> ::= <дизъюнкция> | <импликация> =Ф <дизъюнкция> ::= <конъюнкция> | <дизъюнкция>\/<конъюнкция> ::= <множитель> | <конъюнкция>Л<множитель> : = "] <множитель> | (< высказывание» I Т I F | <идентификатор> Теперь определим s(e) (значение высказывания е в состоянии s) рекурсией по структуре е, определяемой грамматикой. Это значит, что для каждого правила грамматики мы покажем, как вычислять е, порожденное по этому правилу. Например, правило б означает, что для дизъюнкции вида <дизъюнкция> \/<конъюнкция>
Гл. 1. Высказывания 23 значением будет результат применения операции V к значениям s (<дизъюнкция>) и s (<конъюнкция>) для ее операндов <дизъ- юнкция> и <конъюнкция>. Значения пяти операций =, =>, V, Ли"] берутся из таблицы истинности (1.2.3) (они используются в правилах 2, 4, 6, 8, 9). 1. s «высказывание» = s (<импликация>) 2. s «высказывание)) = s «высказывание» = s «импликация» 3. s «импликация» = s «дизъюнкция» = s «импликация)) =Ф s «дизъюкция» = S «КОНЪЮНКЦИЯ» = s «дизъюнкция» Vs «конъюнкция» = s «множитель)) = s «конъюнкция)) /\s «множитель)) = "] s «множитель» = s «высказывание» = Г = F = s «идентификатор» (значение идентификатора в s) 4. s «импликация» 5. s «дизъюнкция)) 6. s «дизъюнкция» 7. s «конъюнкция» s «конъюнкция» s «множитель» s «множитель» s «множитель» s «множитель» s «множитель)) 9 10 11 12 13 Пример вычисления, использующего таблицу истинности Вычислим значения высказывания (Ь=>с) = (~] Ь\/с) при всех возможных значениях операндов, используя таблицу истинности. В таблице, приведенной ниже, в каждой строчке выписаны возможные значения Ъ и с и соответствующие значения "| Ь, ~]b\/cy b=>c и всего высказывания. Эта таблица истинности показывает, как строить таблицу истинности для высказывания, начиная со значений идентификаторов, затем получая значения минимальных под- высказываний и т. д. вплоть до всего высказывания. Мы видим, что значения ~| Ь\/с и Ь=>с совпадают во всех состояниях, и, следовательно, высказывания эквивалентны и могут использоваться одно вместо другого. На самом деле часто Ь=>с определяется как "]b\/cy a b=c как сокращение для (b=>c)A(c=^b) (см. упр. 2i). Т Т т т ъ F F Т Т с F Т F Т пЬ Т Т F F тЬУс т т F Т Ъ^с т т F Т (Ь^с
24 Часть I. Высказывания и предикаты 1.5. Тавтологии Тавтология — это высказывание, истинное в любом состоянии, в котором оно определено. Например, высказывание Т — тавтология, a F нет. Высказывание а\/ ~]а есть тавтология, в чем можно убедиться, вычисляя его для а=Т и a=F: TV"]T = TVF = T FV]F=*F\/T=*T или в форме таблицы истинности ъ т F -,Ь Ъу ->Ь F Т Т Г Чтобы показать, что высказывание является тавтологией, надо показать, что его вычисление дает Т в любом возможном состоянии. К сожалению, каждый дополнительный идентификатор удваивает число комбинаций значений; для высказывания, содержащего i различных идентификаторов, имеется 2' комбинаций! Следовательно, подобное вычисление может оказаться утомительным и долгим. Чтобы проиллюстрировать это, рассмотрим таблицу истинности (1.5.1) для высказывания с тремя идентификаторами b\/c/\d=>- (d^b). Работу можно сократить, опуская некоторые этапы вычисления. Например, взглянув на таблицу истинности (1.2.3), мы видим, что импликация истинна, если ее посылка ложна. Поэтому заключение необходимо вычислять лишь тогда, когда посылка истинна. В примере (1.5.1) есть лишь одно состояние, в котором истинна посылка — то, в котором Ь, с и d истинны,— и, следовательно, нам нужна лишь верхняя строка таблицы истинности bed Т Т Т Т Т F Т F Т Т F F F Т Т F T F F F Т F F F bACAd т F F f F F F F d^>b- Т Т Т Т F Т F Т (bAcAd)^>(d^-b) Т Т Т Т Т Т Т Т Такого рода неформальные соображения помогают уменьшить число состояний, в которых нужно вычислять высказывание. Тем не менее, чем больше идентификаторов в высказывании, тем больше
Гл. 1. Высказываний, 25 состояний надо проверять, и в конце концов вычисление становится непомерно тяжелым. В следующих главах изучаются другие методы доказательства тавтологичности высказываний. Опровержение предложения Иногда мы предполагаем, что высказывание е — тавтология, но не можем доказать это предположение; поэтому мы решаем попытаться его опровергнуть. В чем заключается опровержение такого предположения? Можно было бы попытаться доказать обратное, т. е. что "| е — тавтология, но смысла в этом нет. Если у нас были доводы доверять предположению, непохоже на то, чтобы его отрицание было истинным. Более вероятно, что оно истинно в большинстве состояний, но ложно в одном или двух, а для опровержения нам нужно найти лишь одно такое состояние. Чтобы доказать предположение, необходимо доказать, что оно истинно во всех возможных случаях; чтобы его опровергнуть, достаточно найти единственный случай, когда оно ложно. 1.6. Высказывания как множества состояний Высказывание выражает, или описывает, множество состояний, в котором оно истинно. Обратно, для любого множества состояний, содержащего фиксированный конечный 3) набор идентификаторов, которым сопоставлены Т или F, можно построить высказывание, его представляющее. Так, пустое множество, не содержащее состояний вообще, представимо высказыванием F, поскольку F не истинно ни в одном состоянии. Множество всех состояний представимо высказыванием Т, поскольку Т истинно во всех состояниях. В следующем примере показано, как можно получить высказывание, выражающее заданное множество состояний. Получающееся высказывание содержит лишь операции V, Л» ~] • Пример. Множество из двух состояний {(Ь, Т), (с, Г), (d, Г)} и {(Ь, F), (с, Т), (d, F)} представимо высказыванием (bAcAd)V(lbAcAld) Q Связь между высказываниями и множествами состояний, которые они представляют, так сильна, что мы часто отождествляем эти два понятия. Так, вместо того чтобы писать «множество таких состояний, что Ь\/ ~\с истинно», мы пишем «состояния из bV~"|c», хотя это и не совсем корректное использование естественного языка. В связи с этим мы вводим следующую терминологию. Высказывание Ъ слабее, чем с, если с=$~Ь. Соответственно с сильнее, чем 6.
26 Часть I. Высказывания и предикаты Более сильное высказывание сильнее ограничивает комбинации значений идентификаторов, описываемые им, а более слабое — слабее. В терминах множеств состояний Ь слабее, чем с, если оно «менее ограничительно»: множество состояний, сопоставленное Ь, содержит по меньшей мере те состояния, которые описывает с, а возможно, еще и другие. Слабейшее высказывание — это Т (или любая другая тавтология): оно представляет множество всех состояний; сильнейшее — это F: оно представляет пустое множество. 1.7. Перевод с естественного языка на язык высказываний Сейчас мы переведем несколько предложений в формальные высказывания. Рассмотрим предложение: «Если идет дождь, прогулка отменяется». Пусть идентификатор г обозначает высказывание «идет дождь», а рс — «прогулка отменяется». Тогда это предложение можно записать в виде г=>рс. Пример показывает, что способ преобразования состоит в том, чтобы представить «элементарные части» предложения идентификаторами, а их отношения описать через логические операции. Как выбираются элементарные части, решает переводчик. Сейчас мы приведем еще несколько примеров, в которых используются идентификаторы г, рсу wet и s, определенные следующим образом: идет дождь: г прогулка отменяется: рс вымокнуть: wet остаться дома: s 1. Если идет дождь, но я остался дома, я не вымокну: (r/\s)=$* ~]wet. 2. Я вымокну, если идет дождь: r=>wet. 3. Если идет дождь, а прогулка не отменена или я не остался дома, то я вымокну. Это переводится либо ((гЛ "1 рс)V "| s)=>wet, либо r/\("]pcf\ ~]s)=>wet. Здесь естественный язык двусмыслен; вероятно, второй перевод все же лучше. 4. Будет отменена прогулка или не будет, я остаюсь дома, если идет дождь: (рс\/ ~]pc)\r=>s сводится к r=>s. 5. Либо не идет дождь, либо я остаюсь дома: "|rVs. Упражнения к гл. 1 1. В строчках таблицы записаны высказывание и два состояния si и s2t Вычислите высказывание в обоих состояниях,
Гл. 1. Высказывания 27 высказывание (а) т(тУп) (Ь) лт^п (с) л(т^п) (d) л га л п (е) (га V /7) =>/? (0 mV(n^>p) (g) (т=л)л(р (h) га = (и А (р = (i) га=(и Л^ = G) (га=л)Л(р = 4) = 9» 9) =>*) (к) (га =л Ap)^>q (1) (ro=*>/i)=>(p ^?) (т) (га =>(и ^p^^q состояние $/ га Г Г Г г г г F F F F F F F п F F F F F F F F F Т Т F F Р Т Т т т т т т т т F F F F Л т т т т т т F F F Т Т F F состояние 5. га F F F F Т Т Т Т Т Т Т Т Т п Т Т Т Т т F F F Т Т Т Т _£_ Т Т т т F F Т Т Т F F Т Т ? _3L Г Г Г Г Г F F F F F Т Г 2. Выпишите таблицы истинности, дающие значения следующих высказываний во всех возможных состояниях: (з) by су d (е) ~]Ь => Ь У с (b) Ь Ac Ad (f) -}b = by с (c) b А{СУ d) (g) (~\Ь = с) УЬ (d) by (с Ad) (h) (b V c) A (b =ф с) Л (с => b) (i) (b = c) = (b=>c)A(c->b) 3. Переведите следующие предложения на язык логики высказываний: (a) х < у или х — у. (b) Либо х < у, х—у, либо х > у. (c) Если * > //, а г/ > г, то v — w. (d) Все следующие утверждения истинны: х < //, // < г и u = oj. Се) Самое большее одно из следующих утверждений истинно: х < уу у < г, у=о>. (f) Ни одно из следующих утверждений не истинно: х < //, у < z, v = w. (g) Следующие утверждения не являются истинными одновременно: х < у, у < 2, У = С0. (h) Когда х < у, тогда г/ < г; когда х^у, тогда и = о>. (i) Когда х < у, у < z означает, что у = ш, но если х^у, то /у < z не может выполняться; однако если v = wt то х < у. (j) Если выполнение программы Р начинается при * < г/, то оно завершается при у = 2х. (к) Выполнение программы Р, начавшееся при ас < 0, не завершается. 4. Ниже приведены несколько предложений. Обозначьте идентификаторами элементарные части (например, «живут как кошка с собакой») и переведите предложения на язык высказываний. (a) Будет или не будет дождь я пойду купаться. (b) Если будет дождь, я не пойду купаться. (c) Живут как кошка с собакой. (d) Живут, как кошка или собака. (e) Если дождь будет лить как из ведра, то будь я проклят, купаться не пойду. (f) Если, когда я буду купаться, дождь будет лить как из ведра, то будь я проклят.
Глава 2 РАССУЖДЕНИЯ ПРИ ПОМОЩИ ЭКВИВАЛЕНТНЫХ ПРЕОБРАЗОВАНИЙ Вычисление высказываний редко является самоцелью. Чаще мы хотели бы как-то уметь манипулировать ими, чтобы вывести «эквивалентные», но более простые высказывания (те, которые легче читать и понимать). Два высказывания (или в общем случае выражения) эквивалентны, если они принимают одни и те же значения в любом состоянии. Например, поскольку а+(с—а)=с всегда истинно для целых переменных а, с, два целых выражения а+(с—а) и с эквивалентны, а а+{с—а)—с называется эквивалентностью. В этой главе эквивалентность высказываний определяется через их вычисление, описанное в гл. 1. Приводятся список полезных эквивалентностей, а также два правила для порождения других эквивалентностей. Обсуждается идея «исчисления» и даются правила в виде формального исчисления для «рассуждений» о высказываниях. Эти правила образуют базу для многих манипуляций с высказываниями и очень важны в дальнейшей работе по построению программ. Главу следует тщательно изучить. 2Л. Законы эквивалентности Для высказываний определим эквивалентность через операцию equals и понятие тавтологии следующим образом: (2.1.1) Определение. Высказывания El и Е2 эквивалентны, если и только если Е1=Е2 есть тавтология. В этом случае Е1=Е2 — эквивалентность. □ Итак, эквивалентность — это равенство, являющееся тавтологией. Ниже приводится список эквивалентностей; это — базовые эквивалентности, из которых будут выводиться все остальные, поэтому мы называем их законами эквивалентности. На самом деле это «схемы»: идентификаторы El, E2, ЕЗ являются их параметрами, а частные формы эквивалентностей получаются путем замещения их высказываниями. Например, подставляя x\J у вместо £1, а ; вместо Е2 в первый закон коммутативности (Е1 /\Е2) = (Е2/\Е1), получаем эквивалентность (*Vy)A*=*A(*Vy)
Гл. 2. Рассуждения при помощи преобразований 29 Замечание. Необходимо вставлять скобки там, где это необходимо для сохранения совместимости порядка вычисления с исходным высказыванием при выполнении подстановки. Например, результатом подстановки хуу вместо b в b/\z будет (x\/y)/\z, но не x\Jy/\z> которое эквивалентно x\J(y/\z). □ 1. Законы коммутативности (они позволяют переставлять операнды Л, V и =): (El AE2) = (E2 AE1) (Е1 V Е2) = (Е2 V Е1) (Е1 = Е2) = (Е2 = Е1) 2. Законы ассоциативности (они позволяют нам пренебрегать скобками, когда мы имеем дело с последовательностью Л или последовательностью V)' Е1 /\(Е2/\ЕЗ) = (Е1 /\Е2)/\ЕЗ (так что оба записываются как El/\E2f\E3) Ely'{E2\/ЕЗ) = (Е1уE2)\JЕЗ 3. Законы дистрибутивности (они полезны при раскрытии скобок в высказываниях, подобно тому как 2х(3 + 4) заменяется на 2x3 + 2x4): Ely {E2 /\ЕЗ) = {El\f E2) /\{Е1\/ ЕЗ) El A(E2VЕЗ) = (Е1 /\Е2)У(Е1 /\ЕЗ) 4. Законы де Моргана (по имени английского математика XIX в. Огастеса де Моргана, который вместе с Булем заложил многие из основ математической логики): -](E1AE2)=-]E1\J-]E2 -](Е1уЕ2)= -}Е1А1Е2 5. Закон отрицания: ~](~]Е1) = Е1 6. Закон исключенного третьего: Ely "]E1 — T 7. Закон противоречия: ElА"]Е1 =F 8. Закон импликации: (Е1 =$>Е2)= ~]Е1УЕ2 9. Закон равенства: (Е1 = £2) = (£7 =>Е2)А(Е2=$ ED 10. Законы упрощения V: Е1УЕ1=Е1 Е1УТ = Т EiyF = El Е1У(Е1АЕ2)=Е1 11. Законы упрощения А* Е1АЕ1=Е1 Е1АТ=Е1
30 Часть I. Высказывания и предикаты Elf\F = F Elf\{El\/E2) = El 12. Закон тождества: £7 = £7 Не волнуйтесь по поводу числа законов. Большинство из них вы уже много раз применяли, возможно, неосознанно, а этот список всего лишь поможет вам привыкнуть к ним. Тщательно выучите законы, поскольку их необходимо использовать в каждом шаге при преобразованиях высказываний. Выполняйте упражнения, помещенные в конце этой главы, пока эти законы не будут усвоены в совершенстве. Знание законов по именам облегчает описания их использования. Закон исключенного третьего требует некоторых пояснений. Он говорит, что в любом состоянии либо Ь, либо ~\ b должно быть истинно; середины быть не может. Некоторые сомневаются в этом законе, по крайней мере в его всеобщей применимости. На самом деле в естественном языке есть противоречащий ему пример. Рассмотрим предложение: Это предложение ложно в качестве значения идентификатора Ь. Истинно оно или ложно? Оно не может быть истинным, потому что оно говорит, что оно ложно; не может быть ложным, потому что тогда оно было бы истинным! Это предложение не истинно и не ложно, а потому нарушает закон исключенного третьего. Парадокс возникает из-за того, что предложение ссылается само на себя, так же как и все другие парадоксальные утверждения. (Можно привести и другой подобный парадокс- парикмахер в деревушке стрижет волосы всем жителям, кроме тех, кто стрижет себя сам. Кто стрижет волосы парикмахеру?) В нашей формальной системе нет возможности ввести такое ссылающееся само на себя истолкование, и закон исключенного третьего имеет место. Но это означает, что мы не можем выразить в нашей формальной системе все возможные мысли и доводы.4) И наконец, заслуживают специального упоминания законы равенства и импликации. Они определяют = и =$* через другие операции: а=Ь всегда можно заменить на (a=>b)f\(b=^a)y a ~]a=>b на а\/Ь. Это усиливает то, что мы говорили об этих двух операциях в гл. 1, Как доказать, что логические законы — эквивалентности Мы без доказательства объявили, что законы 1—12 суть эквивалентности. Один из способов доказать это — построить таблицы истинности и убедиться, что законы истинны во всех состояниях. Например, первый закон де Моргана "| (Е1/\Е2)= ~] E1\J ~]E2 имеет следующую таблицу истинности:
Гл. 2. Рассуждения при помощи преобразований 31 El E2 F F F Т Т F Т Т Е1*Е2 п(£/л£2) F Т F Т F Т Т F лЕ\ лЕ2 л El v -, Е2 т т т ТЕ Т FT T F F F -.(£7 л Е2) = -,£7v ЛЕ2 Т Т Т т Ясно, что закон истинен во всех состояниях (в которых он определен), так что он является тавтологией. Упражнение 1 состоит в том, чтобы доказать все законы. 2.2. Правила подстановки и транзитивности Таким образом, мы сейчас обсудили некоторые базисные эквивалентности. Теперь перейдем к способам получения других экви- валентностей без проверки их таблиц истинности. Одно из правил, которое мы все применяем при преобразовании выражений, обычно даже без явного упоминания,— это правило «подстановки равных вместо равных». Приведем пример применения этого правила. Поскольку а~\-(с—а)=^,мыв выражении (а+(с—a))Xd можем подставить с вместо а+(с—а) и заключить, что (а+(с—a))Xd=cXd\ мы просто заменяем а+(с—а) в (а+(с—a))Xd на более простое, но эквивалентное выражение с. (2.2.1) Правило подстановки. Пусть el=--e2— эквивалентность, а Е (р) — высказывание, записанное как функция от одного из своих идентификаторов р. Тогда Е(е1)=Е (е2) и Е(е2)=Е(е1) — также эквивалентности. П Приведем пример использования правила подстановки. Закон импликации говорит, что (Ь ==> с) = ("| b \Jc)—эквивалентность. Рассмотрим высказывание Е(p)~d\Jр. При el=b==>c и е2 = = "\Ь\/с имеем E{el) = d\/(b=$c) E(e2) = dV(lb\/c) так что (d\/(b=>c)) = d\/(~]b\/c)—эквивалентность. Используя правило подстановки, мы часто будем придерживаться следующей формы. Высказывание, про которое мы заключаем, что оно является эквивалентностью, пишется на отдельной строчке. Исходное высказывание стоит слева от знака равенства, а результирующее стоит справа и сопровождается именем примененного закона el=e2: d\/(b~>c)=d\/(~\bye) (импликация) Нам требуется еще одно правило порождения эквивалент- ностей:
32 Часть I. Высказывания и предикаты (2.2.2) Правило транзитивности. Если el=e2 и е2=еЗ— эквивалентности, то el=e3 — также эквивалентность (и, следовательно, el эквивалентно еЗ). □ Пример. Покажем, что (b=>c)=(~]c=>~]b)—эквивалентность (форма записи объясняется ниже): Ь==>с = ~]Ь\/с (импликация) = с V П Ь (коммутативность) = "1 "]c\J ~]b (отрицание) = "| с => "| b (импликация) Эта запись понимается следующим образом. Во-первых, строки 1 и 2 отмечают, что Ь=>с эквивалентно ~| b\Jc на основании правила подстановки и закона импликации. Во-вторых, строки 2 и 3 отмечают, что ( "| Ь\/с) эквивалентно с\/ ~] b на основании правила подстановки и закона коммутативности. Применяя правило транзитивности, мы заключаем также, что первое высказывание, Ь=>су эквивалентно третьему, с V ~| Ъ. Продолжение в том же духе приводит к эквивалентности каждой пары строчек и доводам, почему эквивалентность имеет место. Наконец, мы заключаем, что первое высказывание, Ь=>с, эквивалентно последнему, "]с=^"|Ь. □ Пример. Покажем, что закон противоречия можно доказать, исходя из остальных законов. Фрагмент высказывания, замещаемый на данном шаге, подчеркивается, чтобы помочь распознать подстановки. 1(6 Л ~]b)= ""[fry "1 "]& (закон де Моргана) = ~\b\/b (отрицание) =*= Ь V 1Ь (коммутативность) = Т (исключенное третье) □ Вообще говоря, такая скрупулезность не обязательна. Законы коммутативности и ассоциативность часто используются без пояснений, а применение нескольких шагов может быть записано в одну строку. Например: (&д(6 =><?))=:>с (импликация,- два раза) — ~\ФЛ("]Ь\/с))\/с (закон де Моргана) = 1 bV 1 (1 b\/c)\/c (исключенное третье) Преобразования импликации Пусть требуется доказать, что (2.2.3) Е1/\Е2/\ЕЗ=*Е есть тавтология. Это высказывание преобразуется следующим
Гл. 2. Рассуждения при помощи преобразований образом: Е1ЛЕ2АЕЗ=>Е = -\(Е1/\Е2/\ЕЗ)\/Е (импликация) = IE 1\/ '\Е2\/'\ЕЗ\/Е (закон де Моргана) Получившееся высказывание истинно в любом состоянии, в котором по меньшей мере одно из ~]Е1, ~]Е2, "]ЕЗ, Е истинно. Следовательно, чтобы доказать, что (2.2.3) — тавтология, нам нужно доказать лишь то, что в любом состоянии, где три из приведенных выше высказываний ложны, четвертое истинно. И вдобавок, основываясь на форме высказываний, мы можем сами выбрать, какие из них предположить ложными, чтобы получить более простое доказательство. Подобными доводами можно обосновать, что пять предложений Е1/\Е2/\ЕЗ^Е Е1АЕ2А1Е=>ЕЗ (2.2.4) Е1А~]ЕЛЕЗ=> ~]Е2 -\ЕАЕ2АЕЗ=> ~]Е1 -\El\f~\E2y-\E3\jE эквивалентны, и мы сами можем выбрать, с каким из них работать. Когда дано высказывание, подобное (2.2.3), иногда полезно полностью устранить импликацию в пользу дизъюнкции, как в (2.2.4). Подобным же образом, формулируя задачу, правильным будет с самого начала перевести ее в дизъюнктивную форму 5>. Пример. Доказать, что ^(b~>c)Ainb=>c\/d)=>(-\c=>d) есть тавтология. Устраним главную импликацию и используем закон де Моргана: 1 1MV11 nft=»cVd)V(l*=>d) А теперь просто используем закон отрицания и устраним оставшиеся импликации: Hb\/c)\/(b\/c\Jd)\J(c\/d) Используя законы ассоциативности, коммутативности и упрощение V, приходим к by ~\b\/c\/d которое истинно по закону исключенного третьего, b\/~}b— 7\ и упрощению V- Задача, на первый взгляд довольно трудная, стала простой, когда были устранены импликации. 2 д. грис
34 Часть I. Высказывания и предикаты 2.3. Формальная система аксиом и правил вывода Исчисление, согласно энциклопедическим словарям,— это метод или процесс рассуждений посредством вычислений над символами. В разд. 2.2 введено исчисление, поскольку, выполняя некоторые символьные манипуляции по правилам подстановки и транзитивности, можно рассуждать о высказываниях. По очевидным причинам система, представленная здесь, называется исчислением высказываний (одной из его форм). Мы специально сделали оговорку относительно форм. Небольшим изменением правил получается другое исчисление. Кроме того, мы можем изобрести совершенно другую систему правил и совершенно другое исчисление, которое лучше приспособлено для других целей. Мы хотим подчеркнуть природу этого исчисления как формальной системы для манипулирования высказываниями. Для этого на время отбросим понятия состояния и вычисления и посмотрим, можно ли без их помощи обсуждать эквивалентности, которые будем называть теоремами. Во-первых, пусть высказывания, непосредственно получающиеся из законов 1 —12, являются теоремами. Поскольку го, что они теоремы, принимается с самого начала без доказательства, они также будут называться аксиомами (а законы 1—12 — схемами аксиом). (2.3.1) Аксиомы. Любое вызказывание, получающееся подстановкой высказываний вместо £7, Е2У ЕЗ в один из законов 1 —12, называется теоремой. Q Далее будем считать, что высказывания, получающиеся при применении правил подстановки и транзитивное ги к уже доказанным теоремам, являются теоремами. В связи с этим данные правила часто называются правилами вывода, поскольку их можно использовать для вывода того, что высказывание есть теорема. Правила вывода часто записываются в виде Ei, . .., Еп Я], ..., Еп ——^—- или J' г—- , Е Е. Ео где Et, Е, Е0 обозначают высказывания. Правило вывода имеет следующий смысл. Если высказывания Еъ . . ., Еп— теоремы, то теоремой является и Е (и Е0 во втором случае). Запишем в этой форме правила подстановки и транзитивности. (2.3.2) Правило подстановки: *1=е* Е(е1) = Е(е2), Е(*2) = Е(е1) (2.3.3) Правило транзитивности: бI - е2, е'2 -- еЗ el=e3 "
Гл. 2. Рассуждения при помощи преобразований 35 Тогда теорема формальной системы — это либо аксиома (согласно (2.3.1)), либо высказывание, выведенное по одному из правил вывода (2.3.2) или (2.3.3) из ранее доказанных теорем. Обратите внимание на то, что это совершенно другая система работы с высказываниями, которая была определена без обращения к понятиям состояния и вычисления. Синтаксис высказываний тот же, но делаем мы с ними совершенно другое. Конечно, есть связь между формальной системой и системой вычислений, данной в предыдущей главе. В упр. 9 и 10 требуется доказать следующее соотношение: для любой тавтологии е в смысле гл. 1 е~Т есть теорема и наоборот. Упражнения к гл. 2 1. Проверьте, что законы 1 — 12 являются эквивалентностями, построив для них таблицы истинности. 2. Докажите закон тождества е~е, используя правила подстановки и транзитивности и законы 1 — 11. 3. Докажите, что "\T — F есть эквивалентность, используя правила подстановки и транзитивности и законы 1 — 12. 4. Докажите, что ~\F— T есть эквивалентность, используя правила подстановки и транзитивности и законы 1 — 12. 5. Обе колонки, приведенные ниже, являются последовательностями высказываний, каждое из которых (кроме первого) эквивалентно предыдущему. Эквивалентность можно показать одним применением правила подстановки и одного 0) из законов 1—12 или результатов упр. 3 и 4. Pia30BHTe законы (как это сделано в первых двух случаях). (a) (х л у) V (2 л"» (а) -](~]Ь А (~]Ь =ф г)) V z (b) (х л у) V F (противоречие) (b) (~)b А ( ~|6 => г)) => z (c) х Л и (упрощение V) (с) ~\Ь л ("1 ~]b v г) => z (<0 (xAy)vF (d) -]b AH'}bv ~]'}z)=>z (e) (* Л */) V (F Л z) (e) ~\b A ^\(~}b A~}z)=>z (f) (л- Л у) V (F A z) (I) -]b A ~](b A ~|?) =4> z (g) (x Л у) V ((* Л ~]x) A z) (g) Mb V ("\b A '») => г (h) (x A y) V (x А П* Л z)) (h) "J {(b V ~}b) A (b V ~|z)) —> z (i) x A (y V (> Л г)) (i) П(^ Л (^ V"l2))rr>2 (J) * Л (// V "~|x) Л 0/ V z) (j) "1 (6 V "] г) => г (Ю U(ifV»A(zVy) (k) 1 1 (b V Пг) V 2 <0 *A('|XVH//)A(ZVV) (1) (W1?)V? (m) x л ~|(*Л -|^) /\ (zv у) (m) 6 V (~|г V г) 6. Каждое высказывание ниже мож^т быть упрощено до одного из шести высказываний: F, Т, х, //, л- А //, х v у. Упростите их, используя правила подстановки и транзитивности и'законы 1 — 12. (a) х v (у V л-) v ~\y (g) -]х => (л- Л /у) (b) (XV у) А (х V"]//) (h) T=$(-]x=z>x) (c) *v</ V> (i) *=ф(/,=>(*л */)) (d) (* V «/) Л (л' V "|//) Л Пл V у) А Пх V 1//) (j) "|*=ф П*=Ф С|* А у)) (e) (л: Л #) V (х Л ~\у) V П* л //) V Их А ~\у) (к) ~]у=>у (0 П*Л */) V А' (1) "\у=> -]у 7. Покажите, что любо? высказыг^ ни • L можно преобразовать в эквивалентное ему в дизъюнктивной нормальной с/орме, т. е. имеющее следующий вид; 2*
36 Часть I. Высказывания и предикаты е0 V .. • V еп, где каждое в; имеет вид g0 Л ... Л gm. Каждое g,- есть либо идентификатор id, либо "]id, либо Т или F. Более того, идентификаторы в каждом е{ различны. 8. Покажите, что любое высказывание е можно преобразовать в эквивалентное ему в конъюнктивной нормальной форме, т. е. имеющее следующий вид: е0 Л ... Л еп, где каждое е,- имеет вид g0 v ... V gm. Каждое g/ есть либо идентификатор id, либо ~\id, либо Г или F. Более того, идентификаторы в каждом в( различны. 9. Докажите, что любая теорема, полученная по законам 1 —12 и правилам подстановки и транзитивности, является тавтологией, доказав, что законы 1—12 суть тавтологии (см. упр. 1) и что оба правила могут порождать лишь тавтологии. 10. Докажите, что если е—тавтология, то е = Т можно доказать, применив лишь правила подстановки и транзитивности и законы 1—12. Указание: используйте упр. 8. 11. (Добавлено переводчиком.) Найдите неточность в доказательстве второго примера из разд. 2.2. Использование результата какого упражнения может помочь ее исправить?
Глава 3 СИСТЕМА ЕСТЕСТВЕННОГО ВЫВОДА В этой главе вводится другая формальная система аксиом и правил вывода для проведения доказательства того, что высказывания являются тавтологиями. Она называется системой естественного вывода, поскольку считается, что она повторяет формы рассуждений, которые мы используем в обычном разговорном языке. Материал этой главы не используется в других главах книги и может быть опущен. Система преобразований эквивалентностей, данная в гл. 2, обслуживает в дальнейшем построение правильных программ вполне достаточным образом. Можно было бы пбйти дальше и сказать, что система преобразований эквивалентностей больше подходит для наших нужд, но это, возможно, дело вкуса. Тем не менее изучение этой главы желательно по нескольким причинам. Во-первых, приведенная здесь формальная система минимальна: в ней нет аксиом и минимально число правил вывода. Таким образом, можно, например, начать с бедной системы и построить достаточно теорем, чтобы дальнейшие теоремы было удобно доказывать. Система же преобразований эквивалентностей берет в качестве аксиом все полезные базовые эквивалентности. Во-вторых, системы естественного вывода все больше и больше используются в системах машинной верификации программ, и студенту, изучающему информатику, следует ознакомиться с ними. (Такие системы используются также в популярной игре WFF'N PROOFS (доказательство формул).) Наконец, полезно рассмотреть две совершенно различные системы работы с высказываниями и сравнить их. 3.1. Введение в дедуктивные доказательства Рассмотрим задачу доказательства того, что заключение следует из некоторых посылок. Например, мы можем захотеть доказать, что РЛ(г\/я) следует из/?Л<7> т- е- P/\(r\/q) истинно в любом состоянии, в котором р/\q. Эту задачу можно записать в следующей форме: (3.1.1) посылка: р Л q заключение: p/\(r\lq) На естественном языке мы могли бы обосновать это следующим образом. (3.1.2) Доказательство (3.1.1). Так как p/\q истинно (в состоянии s), то истинно р и истинно q. Одно из свойств V заклю-
38 Часть I. Высказывания и предикаты чается в том, что для любого г r\/q истинно, если q истинно, так что r\/q истинно. Наконец, поскольку и р и r\Jq истинны, свойство Л позволяет нам заключить, что p/\(r\/q) также истинно. Чтобы раскрыть сущность таких доказательств и понять, что же используют такие обоснования, мы намерены сбросить словесную оболочку с доказательства и показать его схему. Несомненно, доказательства на первый взгляд выглядят сложными и излишне подробными. Но после небольшой работы с методом доказательства мы сможем вернуться к неформальным доказательствам на естественном языке, имея гораздо больше возможностей. Мы сможем также дать некоторые указания по построению доказательств (разд. 3.5). Схему доказательства (3.1.2) составляют по порядку: условие теоремы, последовательность высказываний, относительно которых вначале допускается, что они истинны, и последовательность высказываний, которые оказываются истинными согласно предшествующим высказываниям и различным правилам вывода. Такая схема представлена в (3.1.3). В первой строке записана доказываемая теорема: «Из p/\q получить pf\{r\Jq)». Вторая строка содержит посылку (если бы было больше посылок, их записали бы на нескольких последовательных строках), Каждая из последующих строк содержит высказывание, которое можно вывести, основываясь на истинности высказываний, стоящих на предшествующих строках, и на правиле вывода. В последней строке содержится заключение. (3.1.3) Справа от каждого высказывания приведено объяснение, как выведена его «истинность». Например, из строки 4 доказательства следует, что r\Jq истинно согласно свойству V (что r\Jq истинно, если q истинно), а также поскольку q было на предшествующей строке 3. Отметим, что скобки свободно вводятся, чтобы сохранить порядок операций. Мы будем в дальнейшем делать это без формального описания. В этой формальной системе доказываемая теорема имеет вид Из е{1 .., еп получить е В терминах вычислений высказываний такая теорема интерпретируется следующим образом: если си . . ., еп истинны в некотором Из 1 2 3 4 5 Р АЯ р л*? Р Я г V q рл(г получить р л (г ч q) Vq) посылка свойство and, 1 свойство and, 1 свойство or, 3 свойство and, 2, 4
Гл. 3. Система естественного вывода 39 состоянии, го с также истинно в этом состоянии. Если лг=0, чш означает отсутствие посылок, то теорему можно интерпретировать следующим образом: е истинно во всех состояниях, т. е. е — тавтология. В этом случае мы пишем Получить е Наконец, высказывание, стоящее на некоторой строке доказательства, означает, что оно истинно в любом состоянии, в котором истинны высказывания на предыдущих строках 7\ Как уже упоминалось, наша система естественного вывода не имеет аксиом. Использованные выше свойства операций покрываются правилами вывода, которые мы начнем вводить и объяснять со следующего раздела. (Правила вывода впервые были введены в разд. 2.3; просмотрите его, если это необходимо.) Правила естественного вывода приведены на рис. 3.3.1, в конце разд. 3.3. 3.2. Правила вывода Система естественного вывода насчитывает десять правил. Это — довольно большое число, и работать с таким количеством правил можно, лишь если они упорядочены так, что их легко запомнить. В этой системе имеется по два правила вывода для каждой из пяти операций "], Д, V, => и =. Одно из правил позволяет вводить операцию в новое высказывание; другое позволяет ее устранить. Следовательно, имеется пять правил введения и пять правил удаления. Правила введения и удаления д называются Д-I и Д-Е соответственно; подобное верно и для других операций. Правила вывода Д-/, д-£ и \J-I Начнем со следующих трех правил: Д-I, Д-Е, Х/Ь <3-2-" Л1-£.Д—'Л\ £i л л £„ (3.2.2) Л-Е: Е, <3-2-3> VI: Hi,E\En Правило Д-I означает, что если £\ и Е2 встречаются на предше* ствующих строках доказательства (т. е. предполагается, что они истинны, или доказано это), го их конъюнкция может быть записана в данной строке. Если мы утверждаем «идет дождь» и утверждаем, чго «светит солнце», то мы можем заключить, что «идет дождь и светит солнце». Правило называется «введение Д» или «Д-I» (для краткости), так как оно показывает, как может быть введена конъюнкция.
40 Часть 1. Высказывания и предикаты Правило /\-Е показывает, как Л может быть устранено, чтобы получить один из конъюнктивных членов. Если Ег/\Е2 появилось на предшествующей строке доказательства (т. е. предполагается, что оно истинно, или доказано это), то либо £ь либо Е2 можно записать на следующей строке. Опираясь на предположение «идет дождь и светит солнце», мы можем заключить, что «идет дождь», и можем заключить, что «светит солнце». Замечание. Есть места, где часто одновременно идет дождь и светит солнце. Одно из них — Итака, где находится Корнеллский университет. В самом деле, иногда идет дождь, когда над головой, казалось бы, совершенно безоблачное небо. А ясная хорошая погода может в течение нескольких минут смениться яростным бураном, и наоборот. Когда погода ведет себя так странно, а это бывает часто, говорят, что она итакует. □ Правило V-I означает, что если на предшествующей строке написано Еъ то на данной строке можно написать Еху Е2. Если мы утверждаем, что «идет дождь», то мы можем заключить: «идет дождь или светит солнце». Помните, что эти правила выполняются для всех высказываний Ех и Е2. Это фактически «схемы правил», и мы получаем пример правила, заменяя Ех и Е2 конкретными высказываниями. Например, так как р\/q и ~]г — высказывания, то примером Л-1 будет (р vq) Л -]г Перепишем доказательство (3.1.3) в (3.2.4) ниже и отметим точные правила вывода, использованные на каждом шаге. Верхняя строка устанавливает, что требуется доказать. Строка с номером 1 содержит первую (и единственную) посылку (пос 1). Каждая следующая строка обладает таким свойством. Пусть она имеет вид строка № \Е «имя правила», строка № , ..., строка № Тогда можно построить пример названного правила вывода, записывая высказывания со строк «строка №,..., строка №» над чертой, а высказывание Е под чертой, т. е. истинность выводится с помощью одного правила вывода из истинности встретившихся ранее высказываний. Например, из строки 4 доказательства видно, что qlr\Jq — пример правила \1 Л\ (r\Jq) выведено из q. ИЗ рЛд ПОЛУЧИТЬ рЛ(гУ^) (3.2.4) 1 2 3 4 5 pAq Р q r\/q РА(Г Vq) пос 1 л-Е, 1 л-Е, 1 v-I, 3 a-I, 2, 4
Гл. 3. Система естественного вывода 41 Заметьте, что правило Д-Е использовано для того, чтобы разбить высказывание на составляющие-части, тогда как /\-\ и \/-1 использованы, чтобы построить новые высказывания. Это типичное использование правил введения и удаления. Доказательства (3.2.5) и (3.2.6) ниже показывают, что Л — коммутативная операция; если pf\q истинно, то истинно и qf\p, и наоборот. В свете того, что мы раньше изучили о высказываниях, эти свойства очевидны, но прежде, чем их можно будет использовать, это нужно доказать в данной формальной системе. Заметим, чго необходимы оба доказательства; второе из них нельзя вывести как пример первого, заменяя р и q в первом на q и р соответственно. В данной формальной системе доказательство имеет место лишь для частных рассматриваемых высказываний. Оно не является схемой, в то время, как правило вывода ею является. —"~ (3.2.5) Чтобы проиллюстрировать связь между системой вывода и естественным языком, приведем на естественном языке доказательство леммы (3.2.5). Пусть p/\q истинно [строка 1]. Тогда таковы же р и q [строки 2 и 3]. Следовательно, согласно определению /\> Я/\Р истинно [строка 4]. Из 1 2 3 4 р Aq РАЯ P Я ЯАР получить я АР пос 1 л-Е, 1 л-Е, 1 л-1, 3, 2 Из q Ар 1 2 3 4 ЯАР Я Р РАЯ получить р Ая пос 1 л-Е, 1 л-Е, 1 л-1, 3, 2 (3.2.6) Доказательство (3.2.6) можно сократить, опустив строки, содержащие посылки, и используя «пос /» для указания на /-ю посылку, как показано в (3.2.7). Это сокращение будет встречаться часто. Но заметим, что это — всего лишь сокращение, и, даже если оно используется, мы будем продолжать использовать фразу «встречается на предшествующей строке» и для посылок. (3.2.7) Из 1 2 3 q Ар получить р Aq q л-Е, пос 1 р л-Е, пос 1 р Aq A-lj 2, I
42 Часть I. Высказывания и предикаты Правило вывода \J-E Правило вывода для удаления V—'->то /О о о\ \/.р« ^1 V • • • V Еп * ^ i ^-^ ^ > • • • •> *-п —^ Е Правило V-E означает, что если на предшествующей строке встречается дизъюнкция и если на предшествующих строках для каждого дизъюнктивного члена Е( стоят Е^Е, то на данной строке доказательства можно записать Е. Если утверждается: «завтра будет идти дождь или снег» и «если идет дождь, то солнце не светит», а также «если идет снег, то солнце не светит», то можно заключить: «завтра не будет светить солнце». Из (снегудождь), (снег=> нет солнца), (дождь => нет солнца) мы заключаем нет солнца. Приведем простой пример. Из 1 2 3 4 5 pv(q*r),p Р v(<7 Лг) р ^>s (q Ar)^>s s SVp =>j {qbr)^>s получить s vp ПОС 1 noc 2 noc 3 V-E, 1, 2, 3 v-I (правило (3.2.3)), 4 Правило вывода =>-£ (3.2.9) =>-E: E1^Ef' E1 Правило =^-E называется правилом извлечения следствий или modus ponens. Оно позволяет нам записать на очередной строке доказательства заключение импликации, если ее посылка появилась на предшествующей строке. Если утверждается, что х>0 влечет, что у четно, и если мы определили, что О>0, то можно заключить, что у четно. Покажем пример использования этого правила в доказательстве (3.2.10). Чтобы показать связь между формальным доказательством и «человеческим», дадим доказательство на естественном языке. Пусть как рЛЯ, так и р=>г истинны. Из р/\q заключаем, что р (3.2.10) Из 1 2 3 4 5 Р АЯ* р л,? р ^>г Р г г v (q Р =^> =>г г) получить rv{q ^>г) ПОС 1 пос 2 л-Е (правило (3.2.2)), 1 =>-Е, 2. 3 v-I (правило (3.2.3)), 4
Гл. 3. Система естественного вывода 43 истинно. Поскольку р=>г, истинность р влечет истинность г, и г истинно. Но, если г истинно, то к нему можно присоединить при помощи V все, что угодно; следовательно, r\j (q=>r) истинно. Чтобы подчеркнуть использование сокращений при ссылках на посылки, дадим (3.2.10) в сокращенной форме (3.2.11). Из p/\q.p^>r получить rv{q^>r) (3.2. 1 2 3 Р г r\i(q =>r) А-Е, ПОС 1 =>-Е, пос 2, 1 v-1, 2 Правила вывоба~-1 и = h (3.2.12) -l:^ff=^g/ El = E2 (3.2.13) =-Е: —————— Вместе правила =-1 и =-Е определяют эквивалентность через импликацию. Посылки одного правила являются заключениями другого, и наоборот. Это очень похоже на то, как определялась эквивалентность в системе гл. 2. Правило =-1 используется для введения эквивалентности el=e2, опираясь на предшествующее доказательство el=>e2 и e2=>el. Приведем пример использования этих правил: Из р, р -{q Ф>г), г =><? получить г — q 1 2 3 p^iq^r) q ^>r r=q =-E, noc 2 =£>-E, 1, noc 1 ~I, noc 3, 2 Упражнения к разд. 3.2 Каждая из следующих icopeivi может быть показана с помощью в точности одного из базисных правил вывода (и сокращения, вать посылки на отдельных строчках; см. абзац, Назовите это правило вывода. (a) Из а, Ь получить а Л Ь (b) Из а Л Ь A {q V г)} а получить q V г (c) Из *"| а получить ]«Vfl (d) Из c=d, dye получить cf=>c (e) Из /?=фс, b получить Ь V ~| Ь (f) Из ~| а, ~\ Ь, с получить love (g) Из (а =$ b) Л b} а получить а =£> b (h) Из а V b =^с. с=Ф а V b получить a v b~c (i) Из а Л b, q V г получить (а Л t>) Л (q V r) (]) Из р =3> (q =£> г), pt q v г получить q => г (к) Из c=£d, d=$>e, dr=^c получить c = d (1) Из а V bt a V с, а V 6 =4> с пол>чить с позволяющего не выписы- предшествующий (3.2.7)),
44 Часть 1. Высказывания и предикаты (т) Из а => d V с, dvc=>fl получить а = d V с (п) Из а V 6 => с, a v d => с, (а V Ч V И rf) получить с (о) Из а => 6 V с, 6 => bye, а у b получить b \/ с 2. Ниже приведено доказательство того, что р следует из р. Напишите другое доказательство, использующее лишь одну ссылку на посылку. Из р получить р Р пос 1 р пос 1 3. Докажите следующие теоремы, используя правила вывода. (a) Из р Л q, р => г получить г (b) Из p = q, q получить р (c) Из р, q => /-, р =:> г получить р Л г (d) Из 6 Л "1 с получить ~| с (e) Из b получить b V ' | с ([) Из b=z>c Л d, b получить d (g) Из р л <?, р r=> r получить г (h) Из р, q Л (/? => s) получить <? Л s (i) Из p = q получить q = p (j) Из b => (с л d), b получить d 4. Дайте вариант на естественном языке для каждого из ваших доказательств упр. 3. (Естественная версия не обязана в точности воспроизводить формальное доказательство.) 3.3. Выводы и подвыводы Правило вывода =>-/ Теорема вида «Из еи . . ., еп получить е» интерпретируется так: если еъ . . ., еп истинны в некотором состоянии, то истинно и е. Если еъ . . ., еп появились в строчках доказательства, что означает, что они были допущены как истинные или доказаны, то мы должны иметь возможность записать на очередной строке е. Правило =^-1, (3.3.1), разрешает сделать это. Его посылка не обязана появляться на предшествующих строках нашего доказательства; она может появиться где угодно как отдельное доказательство, на которое мы ссылаемся при конкретном использовании правила. Доказательствам необходимо давать различные имена, чтобы исключить двусмысленные ссылки. /Q о 1ч т Из £lf ..., Еп получить Е (6'6Л> =>"1: <£lA... Л *„)=*£ Доказательство (3.3.2) дважды использует =^-1 в применении к леммам, доказанным в предыдущем разделе, чтобы доказать, что
Гл. 3. Система естественного вывода 45 p/\q и q/\p эквивалентны. Попучить {р ьд) = (д лр) (3.3.2) 1 2 3 (pAq)^(qAp) (qAp)^(pAq) (pAq)=(q Ар) ^>-I, (3.2.5) *-I. (3.2.6) =-I, 1, 2 Правило =>--I дает возможность заключить, что /?=Ф</, если есть доказательство q, исходя из данной посылки р. С другой стороны, если p-=>q взята в качестве посылки, то правило =4>-Е позволяет заключить, что q выполняется, если дано р. Мы видим, что выполняется следующее соотношение: Теорема дедукции. «Получить p=$>q» (что означает, что /?=><7— тавтология) является теоремой системы натурального вывода, если и только если «Из р получить q»—.теорема. □ Другой пример использования =->-1 показывает, что р влечет само себя. (3 Получить р =>р ЗД\ . , 1 I p ^>р =>-1, упр. 2 разд. 3.2 Подвыводы Доказательство может содержаться внутри доказательства, подобно тому как процедура может содержаться внутри программы. Это дает возможность посылке =>-1 появиться внутри доказательства (вывода) в качестве строки. Чтобы проиллюстрировать это, перепишем (3.3.2), включив доказательство (3.2.5) как подвывод. Здесь подвывод оказался на строке 1, но он мог быть на любой стро* ке. Если подтеорема встречается, скажем, на строке /, то ее доказательство размещается непосредственно под ней с небольшим отступом, а его строки нумеруются /.1, /.2 и т. д. Подобным же образом можно было бы заменить подвыводом и ссылку на (3.2.6). Получить (р Ag)=(g лр) (3.3.4) Другой пример вывода с подвыводом дан в (3.3.5). Опять-таки будет поучительно сравнить доказательство с его версией на естественном языке. 1 2 3 4 Из pAq получить q Ар 1.1 1.2 1.3 Р Ч qAp (pAq)=>(qAp) (qAp)^(pAq) (pAq ) = (длр) л-Е, пос 1 л-Е, пос 1 л-1, 1.2, 1.1 =>-!, 1 4>-1, (3.2.6) —1, 2, 3
46 Часть 1. Высказывания и предикаты 1 2 3 4 (qVs)^>(pAq) Из р t\q получить 2.1 2.2 <7 q vs S>(qVs) -~(p*q) ЯУ ПОС 1 л-Е, пос 1 v-1, 2.1 =>-I, 2 =-I, 1", 3 Допустим (q\js) => (p/\q). Чтобы доказать эквивалентность, мы должны показать также, что {р/\q) => (q\/s). [Обратите внимание, как здесь используется правило =-1, что а=>Ь и Ь=>а означает а=Ь. Это предложение соответствует строкам 1, 3 и 4 формального доказательства.] Чтобы доказать {p/\q) => (</\Л)> рассуждаем следующим образом. Пусть p/\q истинно. Тогда q истинно. По определению V истинно (/Vs. [Обратите внимание на соответствие со строками 2.1-2.2.1 □ Из (qVs)^>(p Aq) получить (qVs)=(p *q) (3.3.5) Как упоминалось ранее, отношение между выводами и подвы- водами в логике подобно отношению между процедурами, вложенными одна в другую (модулями и подмодулями), в программах. Теорему и ее доказательство можно использовать двумя способами: во-первых, применять саму теорему для доказательства чего-нибудь еще; во-вторых, изучать доказательство теоремы. Процедуру и ее описание тоже можно использовать двумя способами: во-первых, понимать описание как возможность вызова процедуры, во- вторых, изучать тело процедуры, чтобы понять, как она работает. Это подобие, может быть, облегчит понимание идеи подвыводов. Области действия Подвывод может содержать ссылки не только на предыдущие строки его самого, но и на предыдущие сгроки, встречающиеся в объемлющих выводах. Их называют глобальными ссылками на строки. Однако «рекурсия» не допускается: скажем, в строке / не может содержаться ссылка на теорему, чье доказательство не закончено до строки /. Читателю, часто имеющему дело с блочной структурой в языках, подобных ПЛ/1, Алголу-60 и Паскалю, будет нетрудно понять это правило области действия, поскольку, в сущности, здесь применен тот же механизм областей действия (кроме ограничений на рекурсию). Сформулируем правило поточнее. (3.3.6) Правило области действия. Если i — целое число, то строка i доказательства может содержать ссылки на строки 1, . . ., i—1. Строка /./, где i — целое, может содержать ссылки на строки /Л, . . ., /.(/—1) и на любую строку, на которую можно ссылаться из строки / (это исключает ссылки на саму строку /).□
Гл. 3. Система естественного вывода 47 Пример (3.3.7) иллюстрирует использование правила области действия; в строке 2.2 ссылаемся на строку 1, которая находится вне вывода строки 2. Из р ^(<у =>г) получить (р *g)->r (3.3.7) 1 2 3 р =И<7 ^г) пос 1 Из р лд получить г 2.1 2.2 2.3 2.4 (РЛ<7 Р q =>r Я г )^г л-Е, пос ]_ =->-Е, 1, 2.1 л-Е, пос 1 =>-Е. 2.2, 2.3 =>-1, 2 Сейчас мы проиллюстрируем неправильное использование правила области действия. Из р получить р => Лр (Доказательство НЕВЕРНО) р пос 1 Из р получить -»/? 2.1 \ р пос 1 2.2 I p ^> лр =>-1, 2 (неверная ссылка на строку 2) р =^> лр =^>-1, 2 (верная ссылка на строку 2) Ниже мы проиллюстрируем другую обычную ошибку: использование строки, не находящейся в объемлющем выводе. На строке 6.1 сделана попытка сослаться на s в строке 4.1. Поскольку строка 4.1 не лежит в объемлющем выводе, это недопустимо. Подвывод, использующий глобальные ссылки, доказывается в определенном контексте. Взятый вне этого контекста подвывод может быть неверен, потому что он опирается на предположения Нз pv q,p ^>s, s =>г получить г (доказательство НЕВЕРНО) 1 2 3 4 5 6 7 8 р v q пос 1 р '-^s пос 2 s ^>г пос 3 Из р получить г 4.1 4.2 s ^>-Е, 2, пос 1 (верная ссылка на строку 2) г ^>-Е, 3, 4.1 (верная ссылка на строку 3) р ^>г =£>-!, 4 Из q поручить г 6.1 1 г =>-Е, 3, 4.1 (неверная ссылка на 4.1) q ^>r ^>-1, 6 г v-E, 1, 5, 7
48 Часть /. Высказывания и предикаты о контексте. Это еще раз подчеркивает подобие алголовских процедур и подвыводов. Факты, предположенные вне подвывода, могут использоваться внутри его точно так же, как переменные, описанные вне процедуры, могут использоваться в процедуре с помощью того же самого механизма областей действия. Чтобы завершить обсуждение областей действия, приведем вывод с двумя уровнями подвыводов. Легче всего понять его так. Сначала прочитайте строки 1, 2, 3 (не читая вывода леммы на строке 2) и убедитесь, что если доказательство леммы на строке 2 правильно, то и все доказательство правильно. Затем изучите доказательство леммы на строке 2 (только строки 2.1, 2.2 и 2.3). Наконец, изучите доказательство леммы на строке 2.2, которое в доказательстве ссылается на строку двумя уровнями выше. (3.3.8) Из (p^q)^r получить р ^>{q =>r) 1 2 3 (р bq)^>r пос 1 Из р получить q ^г 2.1 2.2 2.3 /?=>( р пос 1 Из q получить г 2.2.1 2.2.2 q ^>r р лд л-1, 2.1, пос 1 г ^>-Е, 1, 2.2.1 ^>-1, 2.2 =£4,2 Доказательство от противного Доказательство от противного, как правило, производится следующим образОхМ. Делается предположение. Из этого предположения получается доказательство противоречия, скажем показывается, что что-то одновременно истинно и ложно. Так как такое противоречие невозможно, а вывод противоречия из допущения верен, предположение обязано быть ложным. Доказательство от противного заложено в правилах вывода 1-1 и ~]-Е8): /о q о\ -1 т Из £ получить El Л "1 El (б.б.У) (-I: yg (3.3.10) 1-Е: Из ] В получить El л ~| El Правило "|-1 означает, что если «из Е получить El Л ~] Eh доказано для некоторого высказывания £7, то "| Е можно написать на очередной строке доказательства. Подобным же образом правило "|-Е позволяет нам заключить, что Е имеет место, если для некоторого высказывания Е1
Гл. 3. Система естественного вывода 49 существует доказательство «из ~| Е получить El Л ~] Eh). Приведем пример (см. (3.3.11)) использования правила ~]-1, а именно что из р следует ~] ~] р. (3.3.11) Из 1 2 3 р получить 1лр р пос 1 Из лр получить р л Лр 2.1 | р а лр л-1, 1, пос 1 л лр т-1, 2 Правило ~]-1 использовано для доказательства того, что ~) ~]р следует из р\ подобным образом правило ~| -Е используется в (3.3.12) для доказательства того, чго р следует из "] "]/?. Из т лр получить р 1 (3.3.12) 2 3 л лр ПОС 1 ИЗ лр ПОЛУЧИТЬ лр Л т т/7 2.1 лр А л лр Л-1, ПОС 1, 1 -,-Е, 2 Теоремы (3.3.11) и (3.3.12) выглядят довольно похожими, тем не менее нужны оба доказательства; нельзя просто получить одно из другого более легким способом, чем они доказаны здесь. Еще важнее то, что нужны оба правила *]-1 и ~] -Е: если одно из них будет опущено из системы вывода, мы не сможем вывести некоторые высказывания, являющиеся тавтологиями в смысле разд. 1.5. Это может показаться странным, поскольку правила так похожи 9). Приведем еще два доказательства. Первое из них указывает, что из р и "]/? можно доказать любое высказывание q> даже эквивалентное лжи. Это так, потому что р и ""]/? не могут быть истинны одновременно, и, следовательно, посылки приводят к абсурду 10). Из р\ лр получить g Т 2 (3.3.13) 3 р пос 1 лр пос 2 Из iff получить р л Лр тгг рА^р л-1, 1, 2 -.-Е,3 Для сравнения приведем доказательство (3.3.14) на естественном языке. Пусть р /\q истинно. Тогда как /?, так и q истинны. Предположим, что р => "| q истинно. Поскольку р истинно, эта импликация позволяет нам заключить, что "| q истинно, но это нелепо, поскольку q истинно. Следовательно, предположение, что
50 Часть I. Высказывания и предикаты (р=> "| q) истинно, неверно, и выполняется "](/?=>"] q). Из рлд получить -л{р^>лд) pAq Из р => -к? П 2.1 2.2 2.3 2.4 р я ^я яА -уя i(p ^^я) пос 1 олучить q л -yq л-Е, 1 л-Е, 1 ^>-Е, пос 1,2.1 л-1, 2.2, 2.3 1-1,2 Резюме Читатель, возможно, заметил различие между системой есте ственного вывода и предыдущими системами вычислений и эквивалентных преобразований: система естественного вывода не позволяет использовать константы Т и F\ Связь между системами можно установить следующим образом. Если «Получить е»—теорема системы естественного вывода, то е—тавтология, а е = Т есть эквивалентность. С другой стороны, если е = Т—тавтология и е не содержит Т и F, то «Получить е»—теорема системы естественного вывода. Если опустить 7 и F, это не доставит хлопот, поскольку по правилу подстановки Т в любом высказывании может быть заменено тавтологией (например, b V ~| b), a F — дополнением тавтологии (например, b Д ~1 6), и при этом получится эквивалентное высказывание. Итак, кратко изложим, что же такое доказательство. Доказательство теоремы «Из еи . . ., еп получить е» или «Получить е» состоит из последовательности строк. Первая строка содержит теорему. Если первой строке не присвоен номер, то все остальные строки пишутся с отступом и нумеруются 1, 2 и т. д. Если номер первой строки /, все остальные пишутся с отступом и нумеруются t'.l, i.2 и т. д. Последняя строка должна содержать высказывание е. Каждая строка i должна быть одного из следующих четырех видов: Форма 1: (/) в; пос / где 1^/^Аг. Строка содержит посылку /. Форма 2: (i) р имя, ссылка 1, . . ., ссылка п Каждое обозначение ссылка i — это либо 1) номер строки (законный в соответствии с правилом области действия (3.3.6)), либо 2) «пос /» (ссылка на посылку / георемы), либо 3) имя ранее доказанной теоремы. Пусть rh обозначает высказывание (или теорему), на которое
Гл. 3. Система естественного вывода 51 ссылается ссылка k. Тогда примером правила вывода будет Р Форма 3: (i) р имя теоремы, ссылка 1, ....ссылка q Здесь имя теоремы — это имя ранее доказанной теоремы; ссылка k означает то же, что и в форме 2. Пусть rk обозначает высказывание, на которое ссылается ссылка k. Тогда «Из г1У ..., г получить р» должно быть названной теоремой. Форма 4: (i) [доказательство другой теоремы] То есть строка содержит полный подвывод, формат которого следует перечисленным правилам. На рис. 3.3.1 представлен список правил вывода. El9..:,En £,л...л£я л-1: а-Е: ■ Е{ л... лЕп Е( £,v...v£-n Из£попучить£7л -,£ лЕ E1--Z-E2, Е2^>Е1 v-I: '• v-E: i-I: =-1: =-Е: Е1У...УЕП9Е1^Е9...9 Еп =>£ Е Из т£ получить El л n El Ё Е1 = Е2 Е1=Е2 ' Е1^>Е2, Е2^>Е1 Из £,,..., £„попучить£ El^>E2, El (£,л...л£„)^>£ ' Е2 Рис. 3.3.1. Совокупность базисных правил вывода Исторические замечания Построенная в этой главе логическая система была предназначена главным образом для того, чтобы охватить наши «естественные» структуры рассуждений. Немецкий математик Герхард Генцен, скончавшийся вскоре после второй мировой войны, развил подобного рода систему математических доказательств в своей статье 1935 г. «Исследования логических законов» [20J, которая включена в [43]. Несколько учебников по логике основано на естественном выводе, например книга Куайна «Логические методы» [41].
52 Часть I. Высказывания и предикаты Приведенная здесь блочная система создана при использовании двух источников: книги Л. Аллена [ 1 ] и книги Р. Констейбла и М. О'Доннела [7]. В первой из них система вывода вводится через серию игр; используется префиксная польская запись, частично для того, чтобы исключить проблемы, связанные со скобками, которые мы обошли при помощи неформальных соглашений. Во второй книге описана программа-верификатор для проверки правильности программ на языке PL/CS (подмножестве PL/C подмножества ПЛ/1), созданная в Корнеллском университете. Ее правила вывода создавались с учетом удобства представления в машине и механической верификации. И в самом деле, верификатор можно использовать для проверки доказательств программ, и в него включены не только исчисление высказываний, но и исчисление предикатов, включая теорию целых чисел и теорию строк. Упражнения к разд. 3.3 1. С помощью леммы (3.2.11) и правила вывода =>-1 дайте доказательство в одну строку, что (р A q А (р => г)) => (г V (q =Ф г)). 2. Докажите; что (р A q) ~> (р V q), используя правило =Ф-1. 3. Докажите, что q => (q A q). докажите, что (q /\q)z=$>q. Используйте первые два результата для доказательства того, что q = (q Aq). Затем перепишите последнее доказательство так, чтобы оно не ссылалось на внешние доказательства. 4. Докажите, что р — р V р. 5. Докажите, что р => ((г v s) => р). 6. Докажите, что q => (г => (q А г)). 7. Докажите, что из р => {г => s) следует г =Ф (р => s). 8. Что неправильно в следующем доказательстве? Получить а^Ь (Доказательство НЕВЕРНО) 1 2 3 а пос 1 Из -лЬ получать Ь л Л6 2.1 2.2 2.3 b •\ b пос 1 чЬ^>Ь^лЬ =>-1,2 ЬЛтЬ =^-Е, 2.2, 2.1 тЕ, 2 9. Докажите, что из "1 р и ("1 p=Z>q) V (р A (/•—>?)) следует rz=$>q. 10. Докажите, что q => (р А г) следует из q => р и q =j> г. 11. Докажите, что из "| q следует q==>p. 12. Докажите, что из "] q следует q =$ ~] р. 13. Докажите, что из 1 q следует (/=>/?Л "1 р. 14. Докажите, что из р V q, '] q следует р. 15. Докажите, что р A (/?=><?) =>?. 16. Докажите, что ((р => q) A (q => г)) —> (р => г). 17. Докажите, что (р =>д) =£> ((р А ~| q) =Ф q).
Гл. 3. Система естественного вывода 53 18. Докажите, что ((р Л "| q) =$>q) => (р =>9)- [Это вместе с упр. 17 позволяет нам доказать (р => q) = ((р л 1 q) =><?).] 19. Докажите, что (р =^>q) =Ф ((р Л ~| а) => "1 р). 20. Докажите, что ((р Л ~] ?) => ~] р) => (р =Ф<?). [Это вместе с упр. 19 позволяет доказать (р=>^) = ((р Л "\ q) => "1 р).] 21. Докажите, что (p=q) =Ф (~| р= 1 #). 22. Докажите, что (*"] р= ~] (7) =Ф (p = q). [Это вместе с упр. 21 позволяет доказать (p = q) = (~\ p= ~| q).] 23. Докажите, что ~] (p = q)=&(~] p = q). 24. Докажите, что (~) р = д) => ""| (р = ^)• [Это вместе с упр. 23 позволяет нам доказать закон неэквивалентности "] (р=^) = (~] /? = <7).] 25. Докажите, что (p = q) =$> (q = р). 26. Докажите «Из р получить р», используя правило противоречия. 27. Для каждого из доказательств упр. 1—7, 9—25 дайте версию на естественном языке (не обязательно в точности следовать формальному доказательству). 3.4. Увеличение гибкости системы естественного вывода Сначала мы введем некоторую гибкость, показав, как теоремы можно рассматривать в качестве схем, т. е. как использовать идентификаторы теоремы в качестве обозначений произвольных высказываний. Затем введем правило подстановки равных вместо равных, включающее в систему естественного вывода способ доказательства эквивалентностей из гл. 2. Наконец, будет доказано несколько теорем, в том числе законы эквивалентности гл. 2. Использование теорем как схем Правила вьшодз, приведенные на рис. 3.3.1, имеют место для любых высказываний £, £*, . . ., Еп. На самом деле они — схемы правил, и мы получаем конкретное правило вывода, заменяя «пустышки» Е, Е1у . . ., Еп конкретными высказываниями. С другой стороны, теоремы вида «Из посылок получить заключение» доказываются лишь для конкретных высказываний. Например, при доказательстве (3.3.2) использовались следующие две теоремы (3.2.5) и (3.2.6): Из р Л Я получить q /\ p Из q Д Р получить р /\ q Хотя похоже, что второе должно следовать непосредственно из первого, в формальной системе необходимо доказывать обе теоремы. Но мы можем доказать кое-что и о самой формальной системе: систематическая подстановка высказываний вместо идентификаторов в теорему и ее доказательство дают другие теорему и доказательство. Так что мы можем рассматривать любую теорему еще как и схему.
54 Часть 1. Высказывания и предикаты Например, из доказательства (3.2.5) «Из p/\q получить q f\p» можно вывести доказательство «Из (a\Jb)f\c получить с/\(а\/Ь)», просто подставляя a\Jb вместо /?, а с вместо q повсюду в доказательстве (3.2.5): Из (аЧЬ)лс получить с л(а vb) 1 2 3 4 (a уЬ)ас а уЬ с с л(я чЬ) пос 1 л-Е, 1 л-Е, 1 Л-1, 3. 2 Сформулируем поточнее эту идею прямой подстановки в теорему и доказательство. (3.4.1) Теорема. Пусть теорема «Из Ег(р), . . ., Еп(р) получить Е (/?)» записана как функция одного из своих идентификаторов /?. Пусть G — высказывание. Тогда «Из Ex(G)y . . ., En(G) получить E(G)y> тоже может быть доказано. Неформальное доказательство. Без ограничения общности предположим, что доказательство теоремы не содержит ссылок на другие теоремы вне себя. (Если это так, то сначала видоизменим доказательство, включив их как подвыводы, как это было сделано при выводе доказательства (3.3.4) из доказательства (3.3.2), и повторяя этот процесс, пока не останется ссылок на внешние теоремы). Тогда доказательство новой теоремы можно получить, просто подставляя G вместо р повсюду в доказательстве исходной теоремы. □ Теоремы, подобные (3.4.1), часто называют метатеоремами, поскольку это не теоремы в системе вывода, как «Из . . . получить...», а доказательства свойств системы вывода. Использование мета- теорем несколько раздвигает рамки нашей формальной системы, но уменьшать формальность этим способом имеет полный смысл. Можно сформулировать метагеорему (3.4.1) в виде производного правила вывода следующим образом: (3.4.2) Из £t у £П(Р) получить Е(Р) (р _ тетяфшатор) v ' Из Е, (G), ..., Еп (G) получить Е (и) vr ^ Vf Используем это производное правило вывода, чтобы переписать теорему (3.3.2), применяя лишь теорему (3.2.5) (но не (3.2.6)). Обратите внимание на то, как строка 2 ссылается на георему (3.2.5) и указывает, какие высказывания подлежат замене. Мы часто опускаем это указание, если оно достаточно очевидно. ПОПучить [р Aq)=(q Ар) 1 2 3 (р Aq)^>(q Ар) (3.2.5) (qAp)-^(pAq) (3.2.5) (с q (pAq)=(qAp) =-I, 1,2
Гл. 3. Система естественного вывода 55 Ранее мы обсуждали соотношение между процедурами в программе и подвыводами в доказательстве. Теперь это соотношение можно распространить на процедуры с параметрами и подвыводы с параметрами. Рассмотрим правило (3.4,2). Доказательство посылки соответствует описанию процедуры с параметром р. Использование заключения в другом доказательстве соответствует вызову процедуры с аргументом G п). Правило подстановки равных вместо равных Правило подстановки, введенное в разд. 2.2, в этом разделе будет использоваться в следующей форме. (3.4.3) Теорема. Пусть высказывание Е рассматривается как функция одного из своих идентификаторов /?, так что мы записываем его как Е(р). Тогда, если el=e2 и Е(е1) появились на предшествующих строках, то мы можем записать Е(е2) на текущей строке 12). □ Например, если дано, что c=>a\Jb истинно, то, чтобы показать, что с=>Ь\'а, мы берем в качестве Е(р) с=>р, в качестве el=e2 берем a\yb=b\/a (закон коммутативности, который будет доказан позже) и применяем теорему. Правило подстановки было правилом вывода в системе эквива- лентностей из гл. 2. Однако оно является метатеоремои системы естественного вывода и должно доказываться. Его доказательство, которое может быть проведено индукцией по структуре высказывания Я, мы оставляем заинтересованному читателю в качестве упр. 10, так что предположим, что оно было проведено. Дадим правило подстановки в виде производного правила вывода: (3.4.4) подст: el = e£2^ {е1) (£(/?)-функция р) Чтобы показать, как используется (3.4.4), приведем схему доказательств, показывающую, что правило подстановки из разд. 2.2 имеет место и здесь. Из el=e2 получить Е(е1) = Е(е2) 1 2 3 4 5 6 el =e2 пос 1 Из Е(е1) получить Е(е2) 2.1 | Е(е2) Е(е1)^>Е(е2) подст, пос 1, 1 =>-1, 2 Из Е(е2) получить Е(е1) 4.1 4.2 e2 = el E(el) Е(е2)=>Е(е!) Е(е/) = Е(е2) =-1, (3.3.3) (р ^>р) подст, 4.1, пос 1 =>-1, 4 =-1, 3, 5
56 Часть I. Высказывания и предикаты Благодаря производному правилу вывода мы получаем удобства сразу двух систем: системы эквивалентностей и естественного вывода. Но мы должны убедиться в том, что действительно выполняются законы разд. 2.1! Сделаем это сейчас. Некоторые базовые теоремы Некоторые георемы, включая законы разд. 2.1, используются часто. Здесь мы намерены сформулировать их и доказать некоторые из них; оставшиеся доказательства предлагаются в качестве упражнений. Первое из доказываемых утверждений очень полезно. Оно устанавливает, что если по меньшей мере одно из двух высказываний истинно, а первое из них ложно, то второе высказывание истинно. Из pv<7, -*р получить q ip пос 2 Из р получить q 2.1 2.2 2.3 Р пос 1 Из ^ получить р л лр 2.2.1 | рл лр лл, 2.1, 1 q -,-Е, 2.2 р ^>q =>-1, 2 Я v-E, пос 1,3,(3.3.3) Вернемся теперь к законам разд. 2.1. Некоторые из их доказательств приводятся здесь; другие оставлены читателю в качестве упражнений. 1. Законы коммутативности. (pAQ)=z(QAP) было доказано в теореме (3.3.4), другие два закона коммутативности остаются для доказательства читателю. 2. Законы ассоциативности. Их нам доказывать не нужно, поскольку правила вывода для Л и V записаны так, что они используют любое количество операндов и не используют скобок 13). 3. Законы дистрибутивности. Здесь дано доказательство первого из них, доказательство второго остается читателю. Доказательство разбивается на три части. В первой части доказывается импликация =^, во второй — ей обратная, так что в третьей можно доказать эквивалентность. Во второй части используется разбор случаев (правило V-E) в применении к закону исключенного третьего bV 1Ь, доказательство которого мы пока откладываем. Использование by ~\b таким путем встречается часто.
Гл. 3. Система естественного вывода Из Ъ yjc^d) получить (Ъ Ус)а(Ь vd) 1 2 3 4 5 i i i i < Из Ъ получить (b vc)^ 1.1 1.2 1.3 6 vc ftvrf (Hr)A(ftVflf) 6 =>(& Vt')A(6 Vrf) Из сЛс/ получить (6 v 3.1 3.2 3.3 3.4 3.5 с d bvc b\id (Ьус)л(Ь yd) (c*d)^>(b vc)A(fo vrf) (6 Vc )Л(& Vrf) (ft о vrf) v-I, noc 1 v-I, noc 1 л-1, 1.1, 1.2 ^>-I, 1 A(£Vrf) Л-Е, noc l A-E, ПОС 1 v-I, 3.1 v-I, 3.2 л-1, 3.3, 3.4 ^>-l, 3 v-E, nocl, 2, 4 Из (6vc) 1 2 3 4 5 6 7 8 A(fcvrf) получить bv(cAd) 6vc 6V</ £>v ,fr Из Ь получить 4.1 | bv(Chd) Ь^>Ьу(с Ad) л-Е, noc 1 л-E, noc 1 (3.4.14) H(cArf) v-I, noc 1 =>-I, 4 Из-ife получить bv(cAd) 6.1 6.2 6.3 6.4 с d chd bv(c Ad) l&=>&V(cAd) 6v(c Ad) (3.4.6), l.nocl (3.4.6), 2, noc 1 л-1, 6.1,6.2 v-I, 6.3 =>-!, 6 v-E, 3, 5, 7 (3.4.9) Получить fcv(cArf)=(Hc)A(fevrf) 1 2 3 iv(cArf)»(6 ve)A(6 vd) (Z>Vc)A(£> Vrf)=£>ftV(cArf) &V(CA</) = (fcVC)A(fc Vrf) =>-I, (3.4.7) =>-I, (3.4.8) =-1, 1, 2 4. Законы де Моргана. Здесь мы докажем лишь первый из них.
58 Часть I. Высказывания и предикаты Из 1 2 (3.4.10) 3 \ -.(ft л л{Ь* Из -, 2.1 2.2 2.3 2.4 2.5 2.6 2.7 «ift v с) получить лЪ v тг с) пос 1 (лЪ v л с) получить (Ь лс)л Л(Ь лс) -i(-i6v-ic) пос 1 ИЗ л 6 ПОЛУЧИТЬ (лЬЧ 1С)А-х(лЬЧ лс) 2.2.1 2.2.2 1*ЬС V-I, ПОС 1 (iftv -,с)л1(п^ v lC) л-1, 2.2.1, 2.1 6 -,-Е, 2.2 ИЗ л С ПОЛУЧИТЬ (TftVnc)AT(T^Vnc) 2.4.1 2.4.2 iftvlC v-i, пос 1 (iftv лс)Ь1(лЬ v тс) л-1, 2.4.1, 2.1 с -,-Е, 2.4 Ьас л-1, 2.3, 2.5 (^Лс)Л т(^Лс) Л-1, 2.6, 1 тс -i-E, 2 Из 1 (3.4.11) 2 3 4 5 ■>6v пс получить т(Ь Из ->Ь получить -}(Ь 1.1 1.2 1.3 л с) лс) т 6 ПОС 1 Из b ас получить b а ЛЬ 1.2.1 1.2.2 i(*Af) • ч{Ь Ac) Ь А-уЪ Из пс получить т(&л 3.1 3.2 3.3 1С* -,(Ь А 1С л-Е, пос 1 л-1, 1.2.1, 1.1 i-I, 1.2 ^-1, 1 с) пос 1 Из b ас получить сАЛс 3.2.1 3.2.2 л(&ЛС) .с) с С А ПС л-Е, пос 1 л-1, 3.2.1,3.1 п-1,3.2 =^•-1,3 v-E, пос 1, 2, 4 (3.4.12) ПОЛУЧИТЬ ч(£ Л£)= ЛЬ V лС 1 2 3 п(6 ЛС)Ф -,& V пс пб v -,<: =>П(Ь л-) «|(6 Лс)= ift V ," =^>-1, (3.4.10) =>-1, (3.4.11) =-1, 1,2
Гл. 3. Система естественного вывода 59 5. Закон отрицания. Это доказательство исключительно простое, поскольку необходимая предварительная работа уже проделана в предыдущих теоремах (3.4.13) Получить т лЪ =Ь 1 2 3 Ь^>туЪ л лЬ ^>Ъ пЬ=Ь ^>-1, (3.3.11) =>-1, (3.3.12) =-1, 1, 2 6. Закон исключенного третьего. Это доказательство проводим, предполагая противное и прямо выводя из него противоречие. Получить Ь v лЪ (3.4.14) 1 2 Из л(Ъ v -xb) получить (Ь v -,6)Л-«(6 v ЛЬ) 1.1 1.2 1.3 1.4 1.5 ftv,i л{ЬЧ -уЪ) НОС 1 ИЗ лЪ ПОЛУЧИТЬ (6 V т6)Л -,(& V тб) 1.2.1 1.2.2 Ь v -,6 v-l, пос 1 (6 v -,6)л-,(6 v-,6) л-1, 1.2.1, 1.1 Ъ -,-Е, 1.2 Ьч лЬ v-i, 1.3 (Ь v -,6)л-,(6 v ,6) л-1, 1.4, пос 1 b 1-Е, 1 7. Закш противоречия. Оставляем читателю. 8. За/соя импликации. Оставляем читателю. 9. Закон равенства. Оставляем читателю. 10. Законы упрощения V и Л- Эти законы используют константы Т и Fy не входящие в нашу формальную систему 14). Упражнения к разд. 3.4 1. Используйте идею теоремы (3.4.1), чтобы вывести из (3.3.7) доказательство того, что (р Л q) ==> (р V q) следует из р => (q => p v q). 2. Используйте идею теоремы (3.4.1), чтобы вывести из (3.3.8) доказательство того, что из (q л г л q) => г следует (q Л г) => (д ==> г). 3. Используйте идею теоремы (3.4.1), чтобы вывести из (3.3.4) доказательство того, что (а Л b Л с) = (с Л а Л Ь). 4. Докажите второй и третий законы коммутативности, (b v с) = (с V Ь) и ф = с) = (с=Ь). 5. Докажите второй закон дистрибутивности, Ъ Л (с V rf) = (£ Л с) V (/? Л d), 6. Докажите второй закон де Моргана, ~| ф V с)= "~| Ь л ~| с. 7. Докажите закон противоречия, ~| (Ь л П £). 8. Докажите закон импликации, Ь V с = (~| 1?г:>с). 9. Докажите закон равенства, (6 = с) = (6 —> с) Л (с=>^). 10. Докажите теорему (3.4.3), используя упр. 13—18 и уточнение формулировки теоремы из примечания 1215).
60 Часть I. Высказывания и предикаты 11. Докажите правило транзитивности: из а = Ь и Ь = с следует а—с. 12. Докажите, что из р V q и ~] q следует р (см. (3.4.6)). 13. Из а = Ь получить '] а=~] Ь. 14. Из a = b, c=d получить а Л с=Ь Л d. 15. Из a = b, c — d получить а V с= Ь V d. 16. Из а = 6, c = d получить (а => с) = (6 =ф d). 17. Из а = 6, c = d получить (а = с) = (b = d). 18. Из а = 6, £ (а) получить Е (Ь). (Это— метатеорема; используя упр. 13—17, покажите, как строить по структуре формулы Е (а) доказательство Е(Ь).) 19. Завершите доказательство второго закона ассоциативности: {а V Ь) V с=* a v (6 V с). 20. Получить (а Л Ь) Л с = а Л (Ь Л с). 21. Получить 6 V (а Л ~| а) = Ь. 22. Получить b V (а => а) = (а =£> а). 23. Получить b Л (а Л ~| а) = (а Л Па). 24. Получить 6 Л (а=> а) — Ь. 25. Докажите (3.4.8) без использования 6 V 1 Ь% 3.5. Построение доказательств в системе естественного вывода Читатель, конечно, бился изо всех сил, чтобы доказать некоторые теоремы в системе естественного вывода, и его, несомненно, интересовало, можно ли строить такие доказательства систематическим образом. В этом разделе будут даны некоторые указания, как это делать. Мы будем постепенно становиться менее педантичными, устанавливая факты без формального доказательства или проводя рассуждения более крупными шагами, если это не затрудняет понимания. Это не только удобно, но и необходимо. Хотя формальные методы обязательны для изучения высказываний, пора начинать пользоваться интуицией, которую они вырабатывают, вместо строгой формалистики, которую они требуют, чтобы высвободиться из-под груды частностей. Чтобы помочь читателю играть более активную роль при построении доказательств, они будут даваться следующим образом. На каждом шаге будет ставиться вопрос, на который нужно ответить, чтобы найти следующий шаг доказательства. Ответ будет отделяться от вопроса пустым местом и горизонтальной чертой, так что читатель может попытаться ответить на вопрос, прежде чем продолжать чтение. Таким путем читатель на самом деле может построить каждый шаг доказательства и проверить его по данному нами.
Гл. 3. Система естественного вывода 61 Несколько общих советов по построению доказательств Предположим, что нужно доказать теорему «Из el, e2 получить еЗ». Доказательство должно иметь вид Из 1 2 3 el, e2 получить еЗ el пос 1 е2 пос 2 еЗ Почему? и мы должно1 всего лишь подтвердить строку 3, т. е. привести доводы, почему на ней можно написать еЗ. При этом мы можем руководствоваться тремя вещами. Во-первых, может оказаться возможным скомбинировать посылки или вывести из них некоторым образом подвысказывания так, чтобы получить, если не само еЗ, то по меньшей мере что-то сходное с ним. Во-вторых, можно исследовать само еЗ. Поскольку для обоснования строки 3 должно быть использовано некоторое правило вывода, вид еЗ может помочь решить, какое именно правило использовать. Это подводит нас к третьей части информации, которую мы можем использовать,— к самим правилам вывода. Есть десять правил вывода, что порождает множество возможностей. К счастью, к любому конкретному высказыванию еЗ применимы лишь немногие из них, поскольку еЗ должно иметь вид заключения правила вывода, использованного для его обоснования. А пользуясь дополнительной информацией о посылках, число действительных возможностей можно сократить еще больше. Например, если еЗ имеет вид е4=>е5, то два правила вывода, которые скорее всего можно использовать, это =-Е и =>-1, а если не кажется возможным вывести из посылок подходящую эквивалентность, то =-Е может быть исключено из рассмотрения. Предположим, что мы пытаемся обосновать строку 3 при помощи правила =>•!, поскольку она имеет вид е4=>е5. Тогда можно продолжить доказательство следующим образом: Из el, e2 получить е4^>е5 el пос 1 е2 пос 2 Из ^ получить е5 3.1 3.2 е4 пос 1 е5 е4^>е5 Почему? =>-1.3 Таким образом мы свели задачу доказательства е4=>е5 из el, e2 к задаче доказательства е5 из е4, а новая задача обещает быть по-
62 Часть I. Высказывания и предикаты проще, поскольку каждое из высказываний е4, е5 содержит меньше логических связок, чем еЗ (они в некотором смысле меньше и проще) 16). Это обсуждение в основном показывает, как подходить к построению доказательства. На каждом шаге исследуйте правила вывода, чтобы определить, основываясь в первую очередь на доказываемом высказывании и во вторую очередь на предыдущих предположениях и уже доказанных теоремах, какие из них скорее всего применимы, и затем пытайтесь применить одно из них, чтобы свести задачу к более простой. По мере того как продолжается доказательство и делается все больше предположений, пытайтесь создать и обосновать (исходя из уже доказанных) новые высказывания, которые могут быть полезны для доказательства нужного результата. Но помните, что хотя посылки, конечно, полезны, построение доказательства — это целенаправленная деятельность, и есть основная цель: высказывание, которое нужно обосновать; наибольшей помощи мы можем ожидать от цели и возможных правил вывода. Успешное построение доказательств требует навыка в обращении с правилами вывода, так что читателю следует потратить некоторое время на их изучение и решение вопроса, когда их можно применять. Здесь мы можем дать краткие советы. Правила =-1 и =-Е вместе определяют операцию =. Они используются лишь для вывода эквивалентности или возврата к импликациям. Если эквивалентность не является частью посылок или цели, их можно не рассматривать. Другие правила введения используются для того, чтобы ввести более длинные высказывания, исходя из более коротких. Следовательно, они полезны, когда нужная цель или ее части могут быть построены из более коротких высказываний, встречающихся на предыдущих строчках. Заметим, что, за исключением =-1, вид заключений всех правил введения различен, так что для обоснования высказывания может быть использовано самое большее одно из этих правил. Правила удаления обычно используются, чтобы «разбить» высказывание так, что одно из его подвысказываний может быть выведено. Все правила удаления (кроме =-Е) имеют в качестве заключения высказывание общего вида. Это значит, что их можно использовать для обоснования любого высказывания. Может ли быть использовано правило удаления, зависит от того, появились ли его посылки на предшествующих строках, так что решение, применять ли эти правила, требует осмотра предыдущих строк. Построение доказательства Задача. Доказать, что если p=>q истинно, то истинно и р/\ ~| </=>"| /?. Первый шаг в построении доказательства — это очертить контуры доказательства и заполнить первую строку теоремой, следующие
Гл. 3. Система естественного вывода 63 строки — посылками, а последнюю строку — целью, г. е. высказыванием, которое должно быть выведено Выполните эгот шаг. Постановка задачи дае1 следующее начало доказательства: Из p^q получить (р л ^q)=> лр Р ^Я (р л ^q)^> -,/? нос 1 Почему? Сейчас благоразумно изучить посылки, чтобы увидеть, какие высказывания можно вывести из них. Сделайте это. Мало что можно вывести из p=>q, кроме дизъюнкции ~] р V q (используя правило подстановки). Мы будем иметь в виду это высказывание. Какие правила вывода можно использовать, чтобы обосновать строку 2? Другими словами, какие правила вывода могут иметь заключением р Л "1 <?=> 1 Р? Возможные правила вывода: =>-!, Л-Е,=»-Е, V-E, ~| -Е, =-Е. Какое из них выглядит самым полезным и почему? Продолжите доказательство в соответствии с тпш правилом. Мало оснований предполагать, что могут быть полезны правила удаления, так как их посылки (кроме =^>-Е) отличаются от высказываний на предшествующих строках, а для применения =>-Е к p=>q не хватает ни посылки /?, ни обоснованности полезности q для построения цели. Остается лишь =>Л. Из p^q получить (рл^)»^ р ^>q пос 1 Из рл^ получить лр 2.1 pA^q пос 1 2.2 I лр Почему? (р л п<7)^> лр =^-1, 2 Что может быть выведено из высказываний, появляющихся на строках, предшествующих 2.2? Используя Л-Е, можно вывести р и ~| q из посылки р Л 1 q. Тогда мы видим, что q может быть выведено из р =z> q и р. (Не странно ли, что может быть выведено как q, так и ~] <7?)
64 Часть 1. Высказывания и предикаты Имея это в виду, перечислите правила вывода, которые можно было бы использовать для обоснования строки 2.217). Возможные правила вывода — это ~|-1, Л-Е, V-E, "1-Е, =>-1. Выберите то из них, которое лучше всего применить, и продолжите доказательство в соответствии с ним. Не видно, как могут быть здесь полезны правила устранения; удаление =Ф в применении к строке 1 дает q, и мы уже знаем, как можно использовать Л-Е, чтобы вывести р w ~| q из р A~]Q- Кажется полезным лишь "|-1: Изр^>3 ПОПуЧИТЬ (р A nff)^» лр 1 2 3 р ^>q пос 1 Изрл^ получить лр 2.1 2.2 2.3 (Р Л п рА^ ПОС 1 Из р получить е л ле (при каком el) 2.2.1 2.2.2 д)^>лр р пос 1 е ^ ле Почему? п-1,2.2 =54,2 Какое высказывание е нужно использовать на строках 2.2 и 2.2.2? Чтобы сделать выбор, посмотрим на высказывания, появившиеся на строках, предшествующих строке 2.2, и те высказывания, про которые мы знаем, как их вывести из появившихся. Продолжите доказательство соответствующим образом. Выше мы рассуждали о том, что могли бы вывести как q, так и ~]q. Так что очевидный выбор — это e=q. Завершаем доказательство следующим образом. Из p^q получить (р л лд)^> лр 1 2 3 Р ФЯ Из /?л^ получить - 2.1 2.2 2.3 2.4 2.5 (РЛ -1 РА^Я Р "•<? Из р получить 2.4.1 2.4.2 Я q*iq -Ф q)^> лр пос 1 «/> пос 1 л-Е, 2.1 л-Е, 2.1 яА-*я =>-Е, 1,2.2 л-1, 2.4.1, 2.3 1-1,2.4 ^-1,2
Гл. 3. Система естественного вывода 65 Построение второго доказательства Задача. Доказать, что из ~\p = q следует *] (p = q). Очертите контуры доказательства и вставьте очевидные детали. Из лр= q получить *>(р =q) *ip=q i(P =q) пос 1 Почему? Какие с ведения можно извлечь из посылок? Правило =-Е можно использовать для того, чтобы вывести две импликации. Здесь это выглядит полезным, поскольку импликации будут нужны при доказательстве цели, и мы выведем обе. Из 1/7 -q получить л(р —q) ip=>q q^ip =-Е, пос 1 =-Е, пос 1 i(p=q) Почему? Следующие правила можно было бы использовать для обоснования строки 3: ~|-1, Л-Е, V-E, =>-Е, "1-Е. Выберите наиболее подходящее и продолжите доказательство соответствующим образом. Правила устранения представляются совсем бесполезными, потому что посылки, которые нужны для их использования, недоступны и не выглядят легко доказуемыми. Единственное правило, которое можно попытаться применить здесь, это ~| -I — выбор маленький! Из ~ip=g получить л(р-д) лр ^>q =-E, пос 1 q => -%р —Е, пос 1 Из р —q получить е л -,<> (при каком el) 3.1 3.2 р=я е л Ле •*(р =я) пос J Почему? 1-1, 3 Какое высказывание е нужно использовать на строках 3 и 3.2 и как можно его доказать? Продолжите доказательство. 3 Д. Грио
66 Часть 1. Высказывания и предикаты Доступны высказывания ~| /?=Ф q и 9=> 1 Р- Вдобавок из строки 3.1 могут быть выведены p=>q и q=>p. Сгруппируем их следующим образом: Р=><7, q=> I р, q=>p 1 P=>q, <?=> 1 p, q=>p Если мы предположим /?, мы можем доказать как /?, так и "| р\ если мы предположим "| /?, мы также можем доказать ри ~] р. Значит, мы должны быть способны доказать противоречие р /\ ~]р. Попытаемся взять е = р и напишем доказательство. Из лр—q получить л(р =q) 1 2 3 4 ■>P^Q q=>->p =-Е, пос 1 =-Е, пос 1 Из p—q получить р л -,р 3.1 3.2 3.3 3.4 3.5 ч(р = p^>q q^>p Р чР рл^р = 4) =-Е, пос 1 =-Ё, пос 1 Почему? > Почему? л-1, 3.3, 3.4 1-1,3 Итак, нам осталось получить высказывания р и "]/?. Это довольно просто сделать, используя предшествующие рассуждения, так что дадим окончательное доказательство: Из -ур—q получить л(р~д) 1 2 3 5 лр =£ q*>. ><? Р Из р -q получить р 3.1 3.2 3.3 3.4 3.5 3.6 3.7 i(p = Р ^<7 <7^Р =-Е, пос 1 —-Е, пос 1 Л п/7 =-Е, пос 1 =-Е, пос 1 Из лр получить р л -,/? 3.3.1 3.3.2 3.3.3 9 Р рЛчр Р Из р получить 3.5.1 3.5.2 3.5.3 Я ip /7 Л -ф "■Р РЛ ip = 9) =^-Е, 1, пос 1 =>-Е,32, 3.3.1 л-1, 3.3.2, пос 1 -,-Е, 3.3 РЛ -,/? =£>-Е, 3.1, пос 1 =>-Е,2, 3.5.1 л-1, пос ], 3.5.2 п-1,3.5 л-1, 3.4, 3.6 1-1.2
Гл. 3. Система естественного вывода 67 На каждом шаге построения доказательства у нас был маленький выбор. Критическое и самое трудное место в доказательстве — выбор правила вывода "~|-1 для обоснования последней строки доказательства, но тщательное изучение правил вывода привело к тому, что оно было единственным правдоподобным кандидатом. Таким образом, прямое изучение доступной информации может очень просто привести к доказательству. Задача об опоздавшем автобусе Задача об опоздавшем автобусе взята из книги [1]. Задача. Даны следующие посылки: 1. Если Билл поедет на автобусе, то Билл потеряет свое место, если автобус опоздает. 2. Билл не сможет вернуться домой, если (а) Билл потеряет свое место и (Ь) Билл будет чувствовать себя подавленным. 3. Если Билл не получит работу, то (а) Билл будет чувствовать себя подавленным и (Ь) Билл не сможет вернуться домой. Какие из следующих предположений верны, т. е. какие могут быть доказаны, исходя из посылок? Дайте доказательства истинных предположений и контрпримеры к остальным. 1. Если Билл поедет на автобусе, то Билл получит работу, если автобус опоздает. 2. Билл получит работу, если (а) Билл потеряет свое место и (Ь) Билл сможет вернуться домой. 3. Если автобус опоздает, то (а) Билл не поедет на автобусе или же Билл не потеряет своего места, если (Ь) Билл не получит работы. 4. Билл не поедет на автобусе, если (а) автобус опаздывает и (Ь) Билл не получит работы. 5. Если Билл не потеряет своего места, то (а) Билл не сможет вернуться домой и (Ь) Билл не получит работы. 6. Билл будет чувствовать себя подавленным, если (а) автобус опоздает или (Ь) Билл потеряет свое место. 7. Если Билл получит работу, то (а) Билл не будет чувствовать себя подавленным или (Ь) Билл не сможет вернуться домой. 8. Если (а) Билл может вернуться домой и Билл поедет на автобусе, то (Ь) Билл не будет чувствовать себя подавленным, если автобус опоздает. Эта задача типична для головоломок. Встретившись с головоломкой, большинство людей приходят в замешательство,— не представляя как же по-настоящему взяться за нее. Однако оказывается, что знание исчисления высказываний делает задачу очень простой. Первый шаг к решению задачи — перевести посылки в форму высказываний. Пусть имеются следующие идентификаторы (вместе с их интерпретацией): 3«
68 Часть I. Высказывания и предикаты tb : Билл поедет на автобусе та: Билл потеряет свое место Ы : автобус опаздывает gh : Билл может вернуться домой fd : Билл чувствует себя подавленным gj : Билл получит работу Посылки приведены ниже. Каждая из них представлена как в виде импликации, так и в виде дизъюнкции, поскольку, как мы знаем, дизъюнктивная форма часто полезна. Посылка 1. tb=s>(bl=>ma) или "| tb V ~] Ы у та Посылка 2. (та Л fd) ==> "] gh или ~| та V 1 fd V ~| gh Посылка 3. "| gj =ф (fd Л 1 gh) или gj V (fd Л 1 g/0 Теперь решим первые несколько задач. Для экономии места посылки 1, 2, 3 не переписываются в каждом доказательстве, а на них просто ссылаемся как на посылки I, 2, 3. Однако выведенные из них высказывания включаются в доказательство, чтобы получить побольше истинных высказываний, из которых выводится результат. Предположение 1. Если Билл поедет на автобусе, то Билл получит работу, если автобус опоздает. Переведите предположение в форму высказывания. В форме высказывания предположение выглядит как tb-=$> (bl=*>gj). Попытаемся доказать «Из tb получить bl^>gj», что обеспечило бы истинность предположения. Очертите контур доказательства и вставьте очевидные детали. Из 1 2 tb получить bl^gj tb noc 1 Ы =$>gj Почему? Какие высказывания могут быть выведены из строки 1 и посылок 1, 2, 3? Продолжите доказательство соответствующим образом. Из посылки 1 и строки 1 может быть выведено высказывание Ы=>та. Из tb получить bl^>gj 1 2 3 tb Ы ^>та bl^-gj пос 1 =5>-Е, посылка 1, 1 Почему?
Гл. 3. Система естественного вывода 69 Какие правила можно было бы использовать, чтобы обосновать строку 3? Высказывание bl=$>gj могло бы быть примером заключения правил =>-1, Л-Е, V-E, "]-Е, =-Е, =Ф-Е. Какое из них выглядит здесь самым полезным. Продолжите доказательство в соответствии g правилом. Высказывания, необходимые ддя использования правил удаления, недоступны, поэтому испытаем =>-1: Из tb получить bl=>gj tb noc 1 bl^ma '=£>-Е, посылка 1, 1 Из Ы получить gj 3.1 Ы пос 1 3.2 | gj Ы ^>gj Почему? =£>-!, 3 Можно ли на строке 3.2 вывести какое-либо высказывание из высказываний на предыдущих строках и посылок 1, 2, 3? Продолжите доказательство соответствующим образом. Из строк 2 и 3.1 может быть выведено высказывание та: Из tb получить Ы ^>gj tb noc 1 Ы ^>та =>-Е, посылка 1, 1 Из Ы получить gj 3.1 3.2 3.3 Ы ^>gj ы та gj пос 1 =^Е, 2, 3.1 Почему? =>-!, 3 Какие правила можно было бы использовать, чтобы обосновать строку 3.3? Высказывание gj могло бы быть заключением правил Л-Е, V-E, ~|-Е, =>-Е. Какие правила кажутся полезными в данном месте?
70 Часть I. Высказывания и предикаты Ни одно из этих правил не кажется полезным. Единственное доступное высказывание, содержащее gj, это посылка 3, и ее дизъюнктивная форма показывает, что gj должно быть обязательно истинно лишь в состояниях, в которых fd Л П gh ложно (согласно теореме (3.4.6)). Но в посылке 2—единственном другом месте, где встречаются fd и gh,— нет ничего, что дало бы нам уверенность в том, что fd Л "1 gh должно быть ложно. Возможно, предположение ложно. К какому контрпримеру, т. е. состоянию, в котором предположение ложно, приводят нас структура доказательства и только что приведенные соображения? Вплоть до строки 3.2 доказательства мы предположили или доказали tb=T, Ы=Т, ша=Т. Чтобы противоречить предположению, нам нужно gj—F. Наконец, только что приведенные соображения означают, что нужно попытаться положить fd/\ ~\gh истинным, так что мы испытаем fd=T и gh=F. И в самом деле, в этом состоянии посылки 1, 2, 3 истинны, а предположение ложно. Предположение 2. Билл получит работу, если (а) Билл потеряет свое место и (Ь) Билл сможет вернуться домой. Переведите это предположение в форму высказывания. Это предположение можно перевести как ma /\ gh=$>gj. Чтобы его доказать, нам нужно доказать «Из та Л gh получить gj». Очертите контур доказательства и вставьте очевидные детали. Из ? 2 та л#Л получить gj та Agh пос 1 gj Почему? Что можно вывести из строки 1 и посылок 1, 2, 3? Продолжите доказательство соответствующим образом. Как строка 1, так и посылка 2 содержат та и gh. Посылку 2 можно рассмотреть в виде ~| (та Agh)V "| fd. Так как та Л gh находится на строке 1, теорема (3.4.6) вместе с законом отрицания позволяют нам заключить, что "] fd истинно, т. е. что fd ложно. Помещая эти доводы в доказательство, получаем
Гл. 3. Система естественного вывода 71 Из maAgh получить gj lYmaAgh noc 1 2 л{та ^gh)\i -yfd подст, пр. де Моргана, посылка 2 3 т л (та Agh) подст, отрицание, 1 4 ,fd (3.4.6), 2,3 5 I gj Почему? Какое правило вывода нужно использовать, чтобы обосновать строку 5? Продолжите доказательство в соответствии с правилом. Применимые правила — Л-Е, V-E, "|-Е, =ф-Е. Это означает, что какое-то из ранее встретившихся высказываний должно быть разбито, чтобы вывести gj. Единственное, что содержит gj, это посылка 3, и в дизъюнктивной форме она выглядит многообещающе. Чтобы показать, что gj истинно, нужно лишь показать, что fd Л "1 gh ложно. Но мы уже знаем, что fd ложно, так что завершаем доказательство следующим образом: Из mabgh получить gj 1 2 3 4 5 6 7 та Agh t{ma a#/j)v i -i(mahgh) Ф ifdVT,gh ■>tfdb-,gh) gj Ф noc 1 подст, пр. де Моргана, посылка 2 подст, отрицание, 1 (3.4.6), 2, 3 v-l, 4 подст, пр, де Моргана, 5 (3.4.6), посылка 3, 6 Предположение 3. Если автобус опоздает, то (а) Билл не поедет на автобусе, или Билл не потеряет своего места, если (Ь) Билл не по* лучит работы. Переведите предположение в форму высказывания. Не двусмысленно ли это предположение? Два возможных перевода — это W=>(1 £/=»(! tbV 1 та)) W=>(-| /6V(1 £/=> 1 та)) Предположим, что имелось в виду первое высказывание. Оно истинно, если мы можем доказать «Из Ы получить ~| gj=& (1 tb V 1 та)». Очертите контур доказательства и вставьте очевидные детали.
72 Часть 1. Высказывания и предикаты Из Ы получить -1&/ ^>(-iffrv чта) 1 6/ пос 1 ■»&/ ^(itbv лта) Почему? Какие высказывания можно вывести из строки 1 и посылок? Никаких высказываний, по крайней мере легко, получить нельзя, так что перейдем к следующему шагу. Какое правило нужно использовать для обоснования строки 2? Продолжите доказательство в соответствии с правилом. Почти очевидно, что должно быть испробовано правило Из Ы получить igj =>(-i/6 v лГпа) 1 2 Ы пос 1 Из igj получить itb v лта 2.1 2.2 -■£/ пос 1 -itby^ma Почему? Какие высказывания можно получить из высказываний, стоящих перед строкой 2.2, и посылок 1, 2, 3? Продолжите доказательство соответствующим образом. Посылка посылки 3 истинна, так что можно заключить, что истинно и заключение. Из 1 2 3 Ы получить ig/ =£ Ы Из п£/ получить 2.1 2.2 2.3 2.4 2.5 •■£/- igj fd^igh fd igh i /£ v лта >(Jfr v -,ma) >(*itbv ima). пос 1 n/Himfl ПОС 1 =£>-E, посылка л-Е, 2.2 л-Е, 2.2 Почему? - 3,2.1 Какое правило вывода нужно использовать для обоснования строки 2.5? Продолжите доказательство в соответствии с правилом.
Гл. 3. Система естественного вывода 73 Высказывание на строке 2.5 может быть заключением правил \М> д-Е, V-E, "1-Е, =^-Е. Первое правило, которое стоит испытать, это V-I- Его использование потребовало бы доказательства, что одно из ]йи "|та истинно. Но, после того как мы проверим посылки, это кажется трудным. Так из посылки 1 мы видим, что как tb, так и та могут быть истинны, в то время как другие посылки также истинны, поскольку их заключения истинны. Может быть есть контрпример? Каков он? В состоянии с tb=T, ma=T, bl=T, gh=F, fd=T, gj=F посылки 1, 2, 3 истинны, а предположение ложно. Упражнения к разд. 3.5 1. Докажите или опровергните предположения 4—8 задачи об опаздывающем автобусе. 2. Для сравнения докажите верные предположения задачи об опаздывающем автобусе, используя лишь эквивалентные преобразования из гл. 2, а затем повторите это же на естественном языке.
Глава 4 ПРЕДИКАТЫ В разд. 1.3 состояние определялось как функция из множества идентификаторов в множество значений {7\ F}. Обобщим это понятие^ тем чтобы дать возможность сопоставлять идентификаторам другие значения, например целые числа, последовательности символов или множества. Понятие высказывания после этого также обобщается в следующих двух смыслах: 1. В высказывании можно заменять идентификатор любым выражением (например, х^у)у принимающим значение Т или F. 2. Вводятся кванторы з (обозначающий «существует»), у («для всех») и N («количество»). Это требует объяснения понятий свободного и связанного идентификаторов и подробного обсуждения области действия идентификаторов в выражениях. Выражения, получающиеся после этих обобщений, называются предикатами, а добавление к формальной системе (подобной системе гл. 2 или 3) правил вывода для обращения с предикатами дает исчисление предикатов. 4.1. Расширение области значений состояния Теперь рассмотрим состояние как функцию из множества идентификаторов в множество значений, которые могут отличаться от Т и F. Идентификатор в любом заданном контексте имеет тип (подобный логическому), определяющий множество значений, которые могут быть ему сопоставлены. Для требующихся дальше стандартных типов используются следующие обозначения: Boolean(\): идентификатору i можно сопоставлять (только) Т или F\ natural number (\): i можно сопоставлять элементы {О, 1,2, ...}; integer'(i): i можно сопоставлять целые числа т. е. элементы {..., —2, —1, 0, 1, 2, ...}; integer set (\): i можно сопоставлять множества целых чисел. По мере необходимости будут вводиться другие типц.
Гл. 4. Предикаты 75 Пусть Р обозначает выражение х<.у, где х и у — типа integer. Будучи вычисленным, Р дает Т или F, поэтому его можно подставлять в высказывание вместо любого идентификатора типа Boolean. Например, заменяя в (b/\c)\Jd идентификатор Ь на Р, получаем ((x<y)Ac)Vd Утверждения, подобные Р, называются атомарными выражениями, а выражения, получающиеся в результате замены идентификаторов на атомарные выражения,— предикатами. Мы не будем останавливаться на деталях синтаксиса атомарных выражений, а будем использовать обычные математические обозначения и полагаться на знания читателей в области математики и программирования. На* пример, приемлемым атомарным выражением является любое пред* ложение языка программирования, вырабатывающее логический ре* зультат. Таким образом, следующие выражения являются преди* катами: ((х<У) А (У < г)) V (х + у < г) (x<yAy<z)\fx + y<z Во втором примере показано, что не всегда нужны скобки, чтобы отделить атомарное выражение от других частей предиката. Старшинство операций в предикате определяется так же, как в обычных математических выражениях. Например, логические операции Д, V, => по старшинству ниже арифметических операций и отношений. Если потребуется, мы будем использовать скобки для того, чтобы явно указать старшинство операций. Вычисление предикатов Вычисление значения предиката в некотором состоянии подобно вычислению значения высказывания. Все идентификаторы заменяются своими значениями в данном состоянии, вычисляются атомарные выражения и заменяются своими значениями (Т или F) и, наконец, вычисляется получившееся постоянное высказывание. Например, предикат x<Zy\Jb в состоянии {(х, 2), (у, 3), (by F)} имеет то же значение, что и 2<.3\JF, которое эквивалентно TV Л а это последнее есть Т. Проиллюстрируем вычисление значений трех предикатов, используя для значения выражение е в состоянии s введенное ранее обозначение s(e)f а состояние записывая как множество пар:
76 Часть I. Высказывания и предикаты s((x<y Ay<z)v(x+y<z)), где 5={(*,l),0,3),(z,5)} = (K3A3<5)v(l+3<5) = (ГА Т) V Т = Т. Н(х<у Ay<z)\f(x+y<z)\ где 5={(дс,3),(у,1),(2,5)} = (3<1 л 1<5) v (3+1 <5) = (F л Т) V Г = Г. *((*<>> л j,<z)v(x+j><z)), где *={(*,5),0>,1),(*,3)} = (5^1 л КЗ) v (5+K3) = (F Л Г) v F = i7. Преобразования атомарных утверждений Точно так же, как были даны правила вывода для преобразований высказываний, следовало бы дать их для преобразований атомарных выражений. Например, мы могли бы формально доказать, что i<k следует из (i<j\/j<k). Но делать этого мы здесь не будем; как говорится, это выходит за рамки нашей книги. Вместо этого полагаемся на то, что у читателя достаточно знаний в области математики и программирования, чтобы преобразовывать атомарные выражения в предикатах. Как уже было сказано, мы будем пользовать арифметикой целых и (изредка) действительных чисел и множествами. Операции, которые будут использоваться в этих выражениях, описаны в приложении 2. Операции cand и сог Любое высказывание имеет значение в любом состоянии, в котором все его идентификаторы имеют значения. Когда мы вводим другие типы значений и выражений, возникает, однако, возможность получить выражения, не имеющие значения (в некоторых состояниях). Например, выражение х/у не определено, если у=0. Конечно же, мы должны быть уверены, что любое выражение в программе имеет значение в любом состоянии, в котором оно будет вычисляться, но иногда полезно позволить не иметь значения некоторым частям выражения. Например, рассмотрим выражение y=0\Jv(x/y)=5 Формально это выражение не определено при у=0, так как х/у не определено при у=0, а V определено лишь тогда, когда его операндами являются либо Т, либо F. Однако кое-кто может воз-
Гл. 4. Предикаты 77 разить, что это выражение должно иметь смысл в любом состоянии, в котором у=0. Поскольку в таких состояниях первый операнд дизъюнкции истинен и поскольку, как определялось, дизъюнкция истинна, если любой из его операндов истинен, выражение должно быть истинным. Более того, такая интерпретация была бы довольно полезна в программировании, так как она позволила бы нам многое записывать яснее и короче. Например, можно было бы писать if у = 0 V (х/у) = 5) then si else s2 вместо if j/sasO then si else if x/y = F> then si else s2 Вместо того чтобы изменить определение Л и V» что потребовало бы от нас изменить полностью нашу формальную логику, введем две новые операции: cand (условное and) и cor (условное or). Операндами этих операций могут быть следующие три значения: Ту F и U (последнее обозначает неопределенность). Новые операции определяются следующими таблицами истинности: Ъ с Т Т Т F т и F Т F F Ъ cand с Ъ cor с Т Т F Т и т F Т F F ь F и и и с и т F и Ъ cand с F и и и b cor с и и и и Это определение ничего не говорит о порядке, в котором должны вычисляться операнды. Разумный способ их вычисления, по крайней мере на современных машинах, дается следующими эквивалентными им условными выражениями: b cand с: if b then c else F b cor c: if b then T else с Операции cand и cor некоммутативны. Например, b cand с не эквивалентно с cand fr. Следовательно, нужно соблюдать осторожность при обращении с выражениями, содержащими эти операции. Следующие законы эквивалентности имеют место для cand и cor (см. упр. 5). Эти законы пронумерованы в соответствии с аналогичными законами гл. 2. 2. Законы ассоциативности: El cand (E2 cand E3) = (E1 cand E2) cand ЕЗ El cor (E2 cor E3) = (E1 cor E2) cor E3
78 Часть I. Высказывания и предикаты 3. Законы дистрибутивности: El cand (Е2 car ЕЗ) = (Е1 cand Е2) cor {El cand E3) El cor (E2 cand £3) = (El cor £2) cand (El cor £3) 4. Законы де Моргана: "] (El cand E2)= ~| £7 cor "] E2) П (£7 cor £2)= ~| El cand ""| £2) 6. Закон исключенного третьего: El cor ~| El = T (если £7 имеет значение). 7. Закон противоречия: El cand "]£/ = £ (если £7 имеет значение). 10. Законы упрощения cor: El cor El = El El cor Т ~Т (если El имеет значение) £Усог£ = £/ £/cor(£/cand£2) = £i И. Законы упрощения cand: ElaindEl=El £7candT = £/ £/cand£ = £ (если El имеет значение) £7 cand (£7 cor £2) = £/ В дополнение к этому можно вывести различные законы, связывающие cor и cand с другими операциями, например El cand (E2\JE3) = (El cand E2)\J(E1 cand £3) Дальнейшую разработку таких законов оставляем читателю ш. Упражнения к разд. 4.1 1. Первые два упражнения заключаются в вычислении значений предикатов и других выражений, включающих целые числа и множества. Дополнительные сведения об используемых операциях даны в приложении 2. Состояние s, в котором должны быть вычислены значения выражений, состоит из двух целых идентификаторов х, у, логического идентификатора Ь, двух идентификаторов для множеств m, n и массива целых чисел с[1:3]. Их значения Л' = 7, */=2, 6 = 7\ т = {1, 2, 3, 4}, п = {2, 4, 6}, с = (2, 4, 6) (a)x~-y = 3 (h) —ceil( — x/y) = x + y (b) (x— 1)-s-y = 3 (i) 7 mod 2 (c) (,*+ 1) ч-у = 3 (j) //oor (*/*/) = * -- у (d) cert (a',/0) = * - 0+ 1 (k) mm (floor (*/2), c«7 (*/2)) < cei/ (x/2) (e) //oo," <(* + 1)/У) = (*+ 1) -s- r/ (1) (afc (- *) = - abs (x)) = b (f) floor (— A'/0) = —3 (m) bvx <y (g) ее// (x/0) = x -7- // 'n) 19 mod 3 2. Вычислите значения следующих выражений в состоянии s, приведенном в упр. i:
Гл. 4. Предикаты 79 (a) m\Jn (g) \т\£т (b) m[\n (h) \n\£n (c) xg/плб (i) ({|m|}U{6, 7})an (d) тспль (j) |m| + |n| = |mUn| (e) 0 Cim (k) min (m) (f) {i\i£mAeven(i)}c:n (1) {f|*gmA'£"} 3. Вычислите следующие предикаты в состоянии s, приведенном в упр. 1. Значение неопределенного выражения обозначьте через U. (a) bvx/(y~2)=0 (f) х = 0 cand */(*/—2) = 0 (b) 6согх/(# — 2) = 0 (g) l<(/<3candc[(/]gm (c) b/\x!(y — 2) = 0 (h) 1 <^/<3corc[A:]gm (d) £ cand */(*/—2) = 0 (i) l<*/<3candc [*/ + 1] gm (e) х = 0лх/(у — 2) = 0 (j) l<x<3corc[*/]£m 4. Пусть высказывания а, 6, с имеют значения F, Г или £/ (т. е. неопределенность). Опишите все состояния, в которых не выполнены законы коммутативности acorb = bcora и a cand 6 = 6 cand a. 5. Докажите законы: ассоциативности, дистрибутивности, де Моргана, исключенного третьего, противоречия, упрощений сог и cand, приведенные непосредственно перед упражнениями. Доказательство проведите путем построения таблиц истинности для каждого из них. 4.2. Кванторы Квантор существования Пусть тип — два выражения с целыми значениями, удовлетворяющие т^п. Рассмотрим предикат (4.2.1) EmVEm+lV. . .VEn_t где каждое Et — предикат. Предикат (4.2.1) истинен в любом состоянии, в котором истинно по меньшей мере одно из Et. Это можно выразить с помощью квантора существования д (читается: «существует»): (4.2.2) (дг. m^i<n: Ef) Множество значений, удовлетворяющих гп^Кп, называется областью значений квантифицированной переменной i. На естественном языке предикат (4.2.2) читается следующим образом. (д/ существует по крайней мере одно (целое) iy такое, что т ^ i < п i лежит между тип—1 (включительно) Et) для которого выполнено Ei Несомненно, читатель уже знаком с некоторыми формами кванти- фикации в математике. Например, сумма и произведение зцаче-
80 Часть /. Высказывания и предикаты ний sm, sm+iy ..., s„_i обозначаются соответственно как п~\ 2 s/ = sm + sm+1+...+sll_1 м- 1 Пз,= SrM*S-„4-1*' • «*SM Эти выражения можно переписать, подобно (4.2.1), в виде (2*':/я<1" </i:s/) (IL:m<* <i:sf.) и эту новую, более линейную форму мы будем использовать далее. С этой точки зрения (4.2.2) является просто сокращением для (4.2.1). Оно может быть рекурсивно определено следующим образом. (4.2.3) Определение д: для п=т: (gr.m<;i <m:£/.)=JF для /i = fe+l > m:(gr. /n<i <&+l :£/) = (gi:m^ i < k\Ei)\/Fk □ Замечание. Базис этого рекурсивного определения, в котором рассматривается пустая область m^Li <Ст, выявляет интересную тонкость. Дизъюнкция нуля предикатов (gi:m <| i < m:££-) имеет значение F; «складывая» 0 предикатов, получаем предикат, который всегда ложен. Например, следующие предикаты эквивалентны значению F: (gi:0<i<0:i = i) (ai:—3<i< —3-.71) Дизъюнкция нуля дизъюнктивных членов есть F. Конъюнкцией нуля конъюнктивных членов оказывается 7\ Подобным образом сумма нуля значений — это 0, а произведение — это 1. Эти четыре факта выражаются следующим образом: (]£i:0<i <():*,.) = 0 (1Ь*:0</<0:^) = 1 (gi:0<i <0:£f.) =F (уг.0<л < Oifi^) =7 (это обозначение объясняется далее) Значение 0 называемся единичным элементом для сложения, поскольку прибавление 0 к любому числу дает то же число. Подобно этому 1, F и Т являются соответственно единичными элементами для *, V и Л- Следующие примеры используют кванторы по двум переменным. Все они эквивалентны и утверждают существование таких i и / между 1 и 99, что / — простое число, а их произведение равно 1079 (верно ли это?). Третий пример использует соглаше-
Гл. 4. Предикаты 81 ние о том, что последовательные кванторы с одинаковыми областями (д*":/я<л < п: (3/:m</ < n:(^k:tn^k < п:.. .))) можно записывать как один (gi, /, &:т<л, /, k< n:...). (1) (з*:0<1'< Ю0:(з/:0</< Ш\ prime (i)f\i* /= 1079)) (2) (at:0<t < 100:prime(1)A(^i-°<i < 100:i*/= 1079)) (3) (gi\ /:0</, /< 100:prtm^(0At*/=1079)) Квантор всеобщности Квантор всеобщности у (читается: «для всех»). Предикат (4.2.4) (Yi:m^i<n:Ei) истинен в некотором состоянии, если и только если Ei истинно в этом состоянии для всех i из области т^/</г. Определим теперь у через д, так что нам потребуется с формальной точки зрения лишь один из этих кванторов рассматривать как новое понятие. Предикат (4.2.4) истинен, если и только если истинны все Eh и таким образом, он эквивалентен £«Л£,я+1Л-..Л£я-1 = 11 (ЕтЛЕт+1Л. • • A£„-i) (закон отрицания) = Kl£/«Vl£„ + iV...Vl£n-i) (закон де Моргана) Это приводит к следующему определению квантора из (4.2.4): (4.2.5) Определение. (yi:m^i<n:Ei)=~\('Z{i'-m^i<n:m]E£). □ Теперь можно доказать, что (4.2.4) истинно, если его область пуста: (у^^< i < tn-.Ei) = ~]('£V\m<^i<m: ~\E£) — ~]F (поскольку область квантора g пуста) = Г Квантор количества Рассмотрим предикаты £0, Е1У ... . Довольно легко формализовать утверждение о том, что k — такое наименьшее число, что выполняется Ek. Нужно лишь указать, что все Е( от Е0 до Ek_x ложны, a Ek истинно: 0^k/\(yi:0<^i<k: !Ei)AEh Более трудно формализовать утверждение о том, что k—второе по величине из чисел, для которых выполняется Ek, поскольку мы должны также описать и первый такой предикат Е/. 0 < / < k/\ (Vr.O < / < /: 1 Е()АЕуА (V*:/ + 1<*<£:1£M£*
82 Часть I. Высказывания и предикаты Очевидно, что описание третьего по величине из значений k> такого, что выполняется &, будет еще более неуклюжим, а описать функцию, которая дает номер истинного Et в ряду истинных, еще труднее. Введем некоторые обозначения. (4.2.6) Определение. (Nr.m</ <я:£\) обозначает число различных значений i из области m^i<n, при которых Ei истинно. N называется квантором счета. □ Это значит, что (gr.m</ <n:£/) = ((Nr.m<t <м:£,-)> 1) (yi:m^i < n:£'/)==((N/:m</ < п:Е;) = п—т) Теперь легко записать утверждение о том, что k—третье по величине из чисел, таких, что выполняется Ek: ((№:0<;<£:£,.)==2)Л£* Замечание об областях До сих пор области кванторов задавались в виде т<^кп, где тип — выражения с целыми значениями. Нижняя граница т включается в область, а верхняя граница п — нет. Далее вид областей будет обобщен, но наше настоящее соглашение полезно, и мы будем использовать его там, где оно подходит. Заметим, что число значений в такой области равно п—т. Заметим также, что кванторы с прилегающими друг к другу областями можно соединять следующим образом: (yiim^i <п: Е() Л (у**: п ^ * < Р: £/) ={yi-m^i < p: Et) (Hi:m < i <n:Ei)+(Ni:n^ i < p\Et) = (Hiim < i < p\Et) Упражнения к разд. 4.2 1. Рассматриваются строки символов языка ПЛ/1 или Паскаль. Пусть | — операция соединения" строк. Например, значение выражения 'abi'l'xl' есть строка символов 'ab:xl\ Каков единичный элемент операции соединения? 2. Определите рекурсивно обозначение (yi:m^i < n:E[). 3. Определите рекурсивно обозначение (fii'.m^i < n:Ej). 4. Запишите предикат, утверждающий, что значение х встречается в массивах Ь[0:п—1] и c[0:m— I] одинаковое число раз. 5. Запишите предикат реппф, с), утверждающий, что массив b[Q:n—1] является перестановкой массива с[0:п—1]. Массив b является перестановкой массива с, если он получается его переупорядочением: каждое значение встречается одинаковое число раз и в массиве Ь, и в массиве с (см. упр. 4). 6. Рассмотрим массив Ь[0:п—1], п > 0. Пусть /, k — два числа, таких, что 0^j^k-\-[^n. При этом b[j:k] означает множество элементов массива b[j], ..., b[k\\ этот список пуст, если /=&+1. Переведите следующие предложения на язык предикатов. Например, первое из них может быть записано предикатом (yi:j <= i < k-\-1 :b [i] — 0). Некоторые из предложений могут быть двусмысленны; в этом случае попытайтесь перевести обе возможности.
Гл. 4. Предикаты 83 (a) Все элементы b[j:k] нулевые. (b) Ни один элемент b[j:k] не нулевой. (c) Некоторые значения b[j:k] нулевые. (Что значит «некоторые»?) (d) Все нули Ь[0:п—\] находятся в b[j:k]. (e) Некоторые нули Ь[0:п—1] находятся в b[]:k]. (f) Те из значений Ь[0:п— 1], которые не находятся в b[j:k], находятся в b[j:k]. (g) Неверно, что все нули Ь[0:п—1] находятся в b[j:k]. (h) Если в Ь[0:п—1] есть нуль, то он есть и в b[j:k]. (i) Если в b[j:k] есть два нуля, то /=1. (j) Либо b[\:j], либо b[j:k] (либо оба) содержат нуль, (к) Значения b[j:k] расположены в возрастающем порядке. (1) Если х находится в b[j:k], то *+1 находится в b[k-{-\:n—\\. (m) b[j:k] содержит по меньшей мере два нуля, (п) Каждое значение b[j:k] встречается и в b[k-{-\:n—1]. (о) / является степенью двойки, если / встречается в b[j:k]. (р) Любой элемент b[0:j] меньше х, а любой элемент b [/+1:п—1] превосходит х. q) Если Ь[1], Ь[2] и b [3] есть соответственно 3, 4 и 5, то / = 3. 4.3. Свободные и связанные идентификаторы Предикат (4.3.1) (yi:/n<f < n:x*i > 0) утверждает, что х, умноженное на любое число от т до п—1 (включительно), превосходит 0. Это утверждение истинно, если как х, так и т превосходят 0 или если х меньше 0, а п не больше 0. Следовательно, (4.3.1) эквивалентно предикату (х > 0Am > 0)V(* < ОД" < 0) Таким образом, истинность (4.3.1) в состоянии s зависит от значений /п, п и х, но не от значения i — на самом деле i может даже не присутствовать в состоянии s. Ясно, что смысл предиката не изменится, если все вхождения i заменить на /: (у/:т^ / <п:х* j > 0) Очевидно, что в (4.3.1) у идентификатора i иная роль, чем у идентификаторов /п, п и х. Поэтому введем терминологию, которая поможет уяснить различие ролей. Идентификаторы /n, n и х свободны в предикате (4.3.1). Идентификатор i связан в (4.3.1) и связан квантором у. Рассмотрим предикат (4.3.2) i>0/\(vi:m^i<n:x*t>0) Этот пример приводит в замешательство, так как самое левое вхождение i свободно (и будет заменено на значение при вычислении предиката в некотором состоянии), а другие вхождения i связаны квантором у. Ясно, что лучше было бы использовать для обозна-
84 Часть I. Высказывания и предикаты чения связного i другой идентификатор (скажем, /) и переписать (4.3.2) в виде i> ОД(у/:т</ <п:х* i >0) Хотя и возможно допускать предикаты, подобные (4.3.2), и большинство логических систем их допускает, рекомендуется использовать идентификатор лишь одним способом. (4.3.3) Ограничение на идентификаторы. В одном и том же выражении один и тот же идентификатор не может быть как связанным, так и свободным и идентификатор не может быть связан двумя различными кванторами. □ Заметим, что предикат (yi:m^i <n:x*i> 0)Д(у*:т< i <n:y*i < 0) не удовлетворяет ограничению. Эквивалентный предикат, удовлетворяющий ему, имеет вид (yiim^i < n:x*i > 0)/\(yk:m <I/?<Az:r/#&<0) Иногда для удобства будут записываться предикаты, не следующие ограничению (4.3.3). В этом случае каждый квантифицированный идентификатор для безопасности необходимо рассматривать как уникальный, не используемый нигде больше. Два различных использования одного идентификатора должны мыслиться как разные идентификаторы. Определим формально понятия связанный и свободный через структуру выражений. (4.3.4) Определение (свободного идентификатора I в данном выражении). 1. i свободен в выражении, состоящем лишь из i. 2. i свободен в выражении (£), если он свободен в Е. 3. i свободен в выражении орЕ, где ор—одноместная операция (например, ~|, —), если он свободен в Е. 4. i свободен в выражении Е1орЕ2, где ор—двуместная операция (например, V, +), если он свободен в Е1 или в Е2 (или в них обоих). 5. i свободен в выражении (у/: т ^ / < п: £), (д/: т ^ / < п: Е) или (Nj:m^j < n:E)t если он свободен в Е, т или в п и отличается от /. □ (4.3.5) Определение (связанного в выражении идентификатора i). 1. I связан в выражении (£), если он связан в Е. 2. i связан в выражении орЕу где ор — одноместная операция, если он связан в Е. 3. i связан в выражении Е1орЕ2у где ор—двуместная операция, если он связан в Е1 или Е2.
Гл. 4. Предикаты 85 4. / связан квантором в выражении {7{i:m^Li <п:Е) (аналогично для у и N). Область действия связанного идентификатора i—это весь предикат (3/:т<л <.п:Е). 5. i связано (но не тем квантором, который показан явно) в выражении (g/:m<; / < я-.Я), если он связан в £, т или в п. То же самое справедливо для у и N. □ Заметим, что i и / свободны в предикате х^у\ когда предикат погружается в выражение (Ny:0^y < 10:* <г/) = 4, х остается свободным, а у становится связанным. Примеры. В приведенных ниже предикатах связанные вхождения идентификаторов обозначены стрелками, ведущими от них к кванторам, которыми они связаны, а все другие вхождения свободны. Неверные предикаты помечены. 2^т <п Л (у i:2^i<m:m+i*0) 2<т <п л (у п:2^п <т:т+п ^0)НЕВЕРНО (почему?) П I 1 П 1 1 (3/: 1 ^i <25:25-Н =0) л (д/: 1 ^/ <25:26-Н =0)НЕВЕРНО я I I п 1 1 (3/:1^/<25:25-г/ =0) л (.3/: 1 <i <25: 26-f-i =0) П I I I (Яi: 1 </<25:25-Н=0 л 26-Н=0) {V mm <m <л+6:(3/:2^1 <т:тН = 0)) (V т:п <т <п+6:(3п:2^п <т\т+п = 0))НЕВЕРНО (V m:n<m<n+6:C3k:2^k<m:m+k=Q)) U Примененный здесь аппарат областей действия подобен блочной структуре Алгола-60 (которая также используется в ПЛ/1 и Паскале). На самом деле впервые она была использована в исчислении предикатов. Предложение (у* : R : Е) вводит новый уровень обозначений, почти как описание процедуры proc p (i); begin. . .end. Внутри такого предложения можно ссылаться на все используемые вне его переменные, кроме i. Эти используемые переменные являются глобальными идентификаторами предложения. Часть yi" является «описанием» нового локального идентификатора i.
86 Часть 1. Высказывания и предикаты Так же как и в Алголе-60, имя локального идентификатора не существенно и может быть изменено (всюду в предложении) без изменения смысла предложения. Но необходимо заботиться о том, чтобы «описывать» связанные идентификаторы именно там, где необходимо, что бы получить тот смысл, который имелся в виду. Упражнения к разд. 4.3 1. Проведите в следующих предикатах стрелки, соединяющие связанные переменные с кванторами, которые их связывают. Отметьте неправильные предикаты. (a) (з£:0<£ < n:PAHk(T))Ak > О (b) (v/:0</ < n:Bj=z>wp(SLj, R)) (c) (3/:0</ < n:(v*:0<i < /+l:/(0 < /(/+1))) (d) (v/:0</ < n:£/Vc/)A(vfc:0<fc < n:Bk => (as:0<s < n:Cs)) (e) (v/:0</ < л:(н*:/+К*< т:(у£:0<£ < n:F (k, t))) 4.4. Подстановка Мы воспользуемся подстановкой в ч.П, чтобы дать изящное и полезное определение присваивания значений переменным. Пусть Е и е — выражения, ах — идентификатор. Символ Ехе обозначает выражение, получающееся одновременной подстановкой е вместо всех свободных вхождений х в Е\ там, где это необходимо, ставятся скобки (чтобы сохранить старшинство операций). Простой пример: (х+у)£=(х+у). Приведем еще несколько примеров подстановки, использующей предикат, который утверждает, что х и все элементы массива Ь[0 : п—1] меньше, чем у. (4.4.1) E = x<yA(vi'0<:i<n:b[q<y) Имеем (4.4.2) Exz = z<yA(Vi'.0<i<n:b[q<y) (4.4.3) Е*х+У = х < x + yA(vi:0<i < n:b[i\x + y) (4.4.4) Elk — E (подстановка имеет место лишь для свободных вхождений i, a i не свободно в Е) (4.4.5) (Ew*z)za+U = (х <w*zA(vi'®<i <n:b[i]< w*z))*a+u = х <w(a + u)/\ (yi:0^ i < n:b[i] <w*(a + u)) В примере (4.4.2) показано замещение свободного идентификатора х идентификатором z, в (4.4.3) — замещение свободного идентификатора выражением. В примере (4.4.4) иллюстрируется, что замещаются лишь свободные вхождения идентификатора. В (4.4.5) показаны две последовательные подстановки и введение скобок вокруг вставляемого выражения. Во второй из подстановок (4.4.5) z замещается на a+и, так что w*z будет изменено на w*(a+u), a
Гл. 4. Предикаты 87 не на w*a+u, которое в силу наших соглашений о старшинстве означало бы (w*a)+u. (Если используются выражения с полным набором скобок или польская префиксная запись, необходимость в этих дополнительных скобках не возникает.) Подстановка уже использовалась нами, но в других обозначениях. Если рассматривать Е из (4.4.1) как функцию идентификатора х, т. е. Е(х)у то El эквивалентно Е(г). В новых обозначениях отмечаются и заменяемый идентификатор, и его заменитель. Поэтому уже не требуются пояснения на естественном языке, чтобы отметить, какой идентификатор заменяется. При подстановке, определенной 1ак, как мы только что сделали, возникают некоторые проблемы, которые мы проиллюстрируем на примерах. Во-первых, £^+i может не иметь смысла, так как она может дать . . .с+Ш]. . ., что синтаксически неправильно. Подстановка должна давать правильно построенное выражение. Во-вторых, допустим, что мы желаем отметить, что идентификатор х и элемент массива Ь меньше, чем у—i, где i — программная переменная. Заметив сходство между этим утверждением и £ из (4.4.1), попытаемся записать это условие, заменяя в Е идентификатор у на.у—i : Е =х<у A(vi'-®<:i<n:b[i]<y) Eyy_i = x<y — iA(yi''Q<i <n:b[i]<y—i) Но это не тот предикат, который нам нужен, гак как i в у—i оказалось связанным квантором у, поскольку теперь оно находится внутри области действия квантора у. Нужно соблюдать осторожность, чтобы не допустить подобных «перехватов» идентификаторов в подставляемом выражении. Для устранения подобных конфликтов можно потребовать сперва (автоматически) заменить связанный идентификатор i предиката Е на неиспользуемый ранее идентификатор (скажем, k)y и мы приходим к Eyy-i = x<y—iA(vb:0^k<n:b[k]<y—i) Теперь определим подстановку более точно. (4.4.6) Определение. £*, где х—идентификатор, а £ и е—выражения, обозначает предикат, получающийся одновременной заменой всех свободных вхождений х в Е на е. Чтобы подстановка считалась правильной, она должна давать синтаксически правильный предикат. Если при подстановке некоторый идентификатор из е может оказаться связанным, то перед подстановкой должно быть произведено подходящее замещение связанных идентификаторов в Е> с тем чтобы устранить возникающий конфликт. Так как следующие две леммы очевидны, они приводятся без
88 Часть I. Высказывания и предикаты доказательств. (4.4.7) Лемма. (E$* = ExttX. Q V (4.4.8) Лемма. Если у не свободно в £, то (Е^ = ЕхцУ. □ Одновременная подстановка Пусть х—список (вектор) различных идентификаторов: X = Х^, л2> • • •» Хп а е—список (той же длины, что и л:) выражений. Одновременная подстановка соответствующих et вместо всех свободных вхождений xL в выражение Е обозначается (4.4.9) Е] или Е%г,е'пп Предостережения, сделанные по поводу простой подстановки в определении (4.4.6), уместны и здесь. Приведем несколько примеров: (x + x + y)2+ybtC = a + b + a + b + c (х + х + у)хх+уу,г = х + у+х + у + г (Vi:0^i<n:b(i)Vc(i+l))nn'+bi,d = (yk:0<^k<n + i:d(k)\/c(k+l)) Во втором примере показывается, что подстановки должны быть одновременными. Если сначала заменить все вхождения х, а затем заменить все вхождения у, результат будет x-\-z-\-x + + г + г, что не одно и то же. В общем случае Eu\v может отличаться от {Effiv. Упражнения к разд. 4.4 1. Рассмотрим предикат £:(y/:0<;i< n:b[i] < b[i-{-\]). Отметьте, какие из следующих подстановок неправильны, и выполните правильные: pi pn pb pti pb pn, b 2. Рассмотрим предикат Е: п > i*A(N/:l^/ < п:п ~- / = 0) > 1. Отметьте, какие из следующих подстановок неправильны, и выполните правильные: pi pn pi pi (pn \i pn, i c/> c/n + f» c/ + l> cfe> \cn+i)t> ^n + i, t 3. Рассмотрим предикат £=(yi:l ^ / < n:(з/:6 [j] = i)). Отметьте, какие из следующих подстановок неправильны, и выполните правильные: 4. Рассмотрим оператор присваивания a". =jc+1. Предположим, мы хотим, чтобы после его выполнения стало истинным R:x > 0. Какое условие или
Гл. 4. Предикаты 89 «предусловие» должно быть истинным перед выполнением оператора, чтобы стало истинным R после выполнения. Можете ли вы дать ответ в терминах подстановки в Ю 5. Рассмотрим оператор присваивания а: =а*Ь, Предположим, мы хотим, чтобы после его выполнения стало истинным R:a*b = c. Какое условие или «предусловие» должно быть истинным перед выполнением оператора, чтобы после выполнения стало истинным R. Можете ли вы дать ответ в терминах подстановки в R? 6. Определите подстановку рекурсивно, через структуру выражений. 4.5. Кванторы по другим областям До сих пор мы рассматривали предикат (gi : т^Кп : Et) как сокращение для Ет\/. . .\/En_v Теперь обобщим понятие квантора, с тем чтобы разрешить квантификацию по другим областям, в том числе и бесконечным. Это приводит к более «сильной» системе: оказывается возможным сформулировать утверждения, которые было невозможно выразить раньше. Однако предикаты с кванторами по бесконечным областям не всегда могут быть вычислены общими методами за конечное число шагов. Следовательно, хотя такие предикаты можно свободно использовать при обсуждении программ, они не могут встречаться в самих программах. Такие предикаты имеют вид (4.5.1) faiiRiE) или (4.5.2) (yi:R:E) где i — идентификатор, a R и Е — предикаты (обычно, но вовсе не обязательно, содержащие i). Первый из них интерпретируется: «существует значение i из области R (т. е. такое, для которого R истинно) при котором Е истинно». Второй из них интерпретируется «для всех значений i из области R выражение Е истинно». Для таких предикатов имеют место такие же понятия свободных и связанных идентификаторов и такие же ограничения на их вхождения в предикаты, какие были определены в разд. 4.3. Пример 1. Пусть Человек (р) выражает предложение: «/? — человек», а Умрет(х) — предложение: «х когда-нибудь умрет». Тогда предложение: «Все люди смертны» или — в не столь возвышенном стиле — предложение: «Каждый человек когда-нибудь умрет» можно выразить в виде (ур: Человек (/?): Умрет (/?)). □ Пример 2. Доказано, что существуют сколь угодно большие простые числа. Эту теорему можно выразить следующим образом: (у/г:0 < n:(^i:n < i:prime(i)))y где prime (i) = (1 < *7\(v/:' < / < *:* mod / Ф °))-
90 Часть t. Высказываний, и предикаты На самом деле Чебышев в 1850 г. доказал, что между каждым числом и числом, превышающим его вдвое, имеется простое число, что и выражает предикат (у/г: 1 < n:(^i:n^i < 2n:prime(i))) □ Пример 3. Следующий предикат утверждает, что максимум целого числа и ему противоположного — это абсолютное значение рассматриваемого числа: (у я: integer (п): max (я, —n)=abs (n)) □ Тип квантифицированного идентификатора При использовании кванторного выражения, подобного (у? i 0^i<Zn : Et), по умолчанию считается, что i—идентификатор типа integer. Однако при работе с более общими областями значений квантифицированного идентификатора это не всегда так. Например, в предикате (см. пример 1 выше) (у/?: Человек (/?): Умрет (/?)) область значений р — множество всех объектов. Следовательно, нужно каким-либо образом выявлять тип квантифицированного идентификатора, с тем чтобы множество его значений определялось однозначно. Формально можно сделать это, включая тип переменной как часть в предикат области значений. Так было сделано в примере 3, где область значений — integer(n). Однако часто текст, окружающий запись предиката, и форма самого предиката определяют тип квантифицированного идентификатора, делая необязательным его явное задание в предикате. Можно полностью опустить область значений, если она может быть определена из контекста. Это — самая обычная попытка устранения излишних подробностей. Например, предикат из примера 3 можно было бы переписать в виде (уя: max (—п, n) = abs(n)) поскольку контекст определяет, что рассматриваются лишь целые числа. Тавтологии и квантификация по умолчанию Пусть доказано, что некоторый предикат, подобный тах(п, —n)=abs(n), где п — типа integer — есть тавтология, т. е. он ио тинен во всех состояниях. Тогда он истинен при всех (целых) значениях /г, так что истинно следующее утверждение: (уя: integer (я): max (—/г, n) = abs(n) или — в сокращенной записи — утверждение: (у/г: тах(—я, n) = abs(n)) Таким образом, мы видим, что
Гл. 4. Предикаты 91 (4.5.3) любая тавтология Е эквивалентна тому же самому предикату £, в котором все его свободные идентификаторы iu. . ., im связаны квантором всеобщности, т. е. (уп>- • •» Un • Е). Этот простой факт будет полезен при установлении в гл. 6 способов описания начальных и конечных значений переменных в программе. Правила вывода для у и g Оставшаяся часть разд. 4.5 требует знания гл. 3 и не нужна для понимания дальнейшего. В ней даны правила введения и удаления для уйди таким образом расширена система естественного вывода из гл. 3. Назначение этого материала — показать по возможности кратко, как можно это сделать. Во-первых, рассмотрим правило введения у. Для этого нужно рассмотреть условия, при которых (yi : R : Е) имеет место в состоянии s. Это утверждение будет истинно в s, если R=^E, истинно в s, и доказательство R=>E не зависит от i, так что R=>E истинно для всех i. Простейший способ ввести это условие — потребовать, чтобы i даже не упоминалось в тех местах доказательства, от которых зависит R=>E. Таким образом, потребуем, чтобы i было новым идентификатором, не встречающимся нигде в доказательствах, от которых зависит R=>E. Итак, приходим к правилу вывода где i—новый идентификатор. Теперь предположим, что (\fi:R:E) истинно в состоянии s. Тогда оно истинно для любого значения i, так что (R=$>E)le выполнено в состоянии s для любого выражения е соответствующего типа. Итак, имеется следующее правило устранения: (4.5.5) Y-E : W:R:E\ для любого выражения е типа соответствующего i. Перейдем теперь к правилам вывода для д. Используя технику, развитую в предыдущих разделах, можно определить g через у: Последнее правило вывода позволяет подставлять одну связанную переменную вместо другой без изменения значения пре-
92 Часть I. Высказывания и предикаты диката: (4.5.8) Замена связанной переменной: , ' .' ,.ч (здесь k не входит свободно в R и Е). Упражнение к разд. 4.5 1. Пусть fool (pt t) означает: «Можно обманывать человека в течение времени Ь Переведите следующие предложения на язык исчисления предикатов: (a) Можно обманывать некоторых людей некоторое время. (b) Можно обманывать всех людей некоторое время. (c) Нельзя обманывать всех людей все время. 2. Запишите следующие предложения на языке предикатов: (a) Квадрат числа неотрицателен. (b) Три числа являются длинами сторон треугольника, если сумма любых двух из них не меньше третьего (для обозначения того, что а, Ь, с — длины сторон треугольника, воспользуйтесь отношением стороны (а, Ъ, с)). (c) Для любого положительного целого числа п существует решение уравнения wn + хп+уп = гп, где w, х, yt z — положительные целые числа. (d) Сумма всех делителей числа /г, кроме самого /г, есть п. (Числа, обладающие таким свойством, называются совершенными. Наименьшим совершенным числом будет 6, так как 1+2+3 = 6.) 4.6. Несколько теорем о подстановке и состояниях В общем случае выражения Е и Е* различаются: вычисление их значений в одном и том же состоянии может привести к разным результатам. Но они взаимосвязаны, и сейчас мы исследуем эту взаимосвязь. Сначала вспомним терминологию. Если е — выражение, a s — состояние, го s (ё) обозначает значение выражения е в состоянии s. Это значение ищется путем подстановки значений идентификагоров е в еостоянии s и вычисления значения получившегося постоянного выражения. Если некоторый идентификатор х не определен в состоянии s, го его значением считается символ U. Теперь нужно получить возможность описывать состояние s', отличающееся от s лишь значением (например) идентификатора х\ в s' это значение есть v. Состояние s' обозначается через (s; x \ v). Например, выполнение присваивания х =2 р состоянии s заканчивается в еостоянии [s; х : 2) В общем случае выполнение присваивания х .—е, начинающееся в состоянии :>, заканчивается в состоянии (s; х i $(e)), гак как переменной х присваивается значение выражения е в состоянии «. Отметим, что имеет место состояние s=(s; х i s(x)) гак как значение х в состоянии з есть s(x). Докажем гри леммы о подстановке Их формальные доказательства существенно зависят от предупреждений,, сделанных при определении подстановки в (4.4.6), и опираются на структуру рас-
Г л, 4. Предикаты 93 сматриваемых выражений. Мы дадим их неформальные доказательства. (4.6.1) Лемма. i(£J)=ss(£;(e)). Это значит, что подстановка выражения е вместо л: в £ с последующим вычислением значения Е% приводит к тому же результату, что и подстановка значения е в состоянии s вместо х и затем вычисление E%ie). Доказательство. Будем рассматривать порядок вычисления значения левой стороны равенства. Повсюду, где х встречается в исходном выражении Е, нужно вычислить е в состоянии s вместо замены х на его значение в состоянии е, так как х заменен на е. Значение е есть s(e). Следовательно, чтобы вычислить s(£*), мы должны вычислять значение Е в состоянии s, заменяя х повсюду, где оно встречается, на значение s (e). Но это и есть способ вычисления значения правой части, и, значит, обе части дают один и тот же результат. Следующая лемма чрезвычайно полезна для понимания определения оператора присваивания, который будет рассматриваться в ч. II. (4.6.2) Лемма- Рассмотрим состояние s. Пусть s' = (s; x:s(e)). Тогда s'(£) = s (£*). Другими словами, вычисление значения Е$ в состоянии s приводит к тому же результату, что и вычисление значения Е в состоянии (s; x:s(e)). Доказательство. sf (£) = (s; x:s(e))(E) (no определению s') = (s; x:s(e))(ExSie)) (при вычислении Е в (s, x:s(e)) вместо х используется значение s(e)) = (s, x:s(x))(Esie)) (* не встречается в £J(ft) поэтому значение Е%«, не зависит от значения х) z=s{E*(e)) (так как s = (s\ x:s(x))) = s(Ef) (по лемме (4.6.1)) Эти леммы легко обобщаются на случай одновременной подстановки, и мы не будем останавливаться на этом. Последняя лемма устанавливает тривиальный, но важный факт и приводится без доказательства. (4.6.3) Лемма. Если х—список различных идентификаторов, Е— выражение, и—список (той же длины, что и х) новых различных идентификаторов, то (Е£\-х==Е. □ Упражнение к разд. 4.6 1. Пусть в состоянии s имеет место л: = 5, у = 6, Ь = Т. Что будет в следующих состояниях: (s; x:6), (s; y:s{x)), (s; y:s(x + y)), (s; b:F)t (s; b;T), ((s; jc:6); y:4), ((s; x\y)\ y:x)}
Глава 5 ОБОЗНАЧЕНИЯ И СОГЛАШЕНИЯ, КАСАЮЩИЕСЯ МАССИВОВ Массив — одно из основных понятий языков программирования. Важно иметь правильную точку зрения на массивы, а также хорошие обозначения для них, чтобы можно было эффективно формулировать утверждения и рассуждать о программах, работающих с массивами. Традиционно массив рассматривался как совокупность независимых переменных с индексами, носящих общее имя. В данной главе представлен другой взгляд на массивы, вводятся подходящие обозначения и приводятся примеры их использования. Этот материал изложен именно здесь, поскольку в нем рассматриваются понятия и обозначения, которые нужны для рассуждений о массивах, а не обозначения, используемые в самом языке программирования. Первые два раздела этой главы будут нужны для определения присваивания элементам массива во второй части книги. 5.1. Одномерный массив как функция Рассмотрим массив, определенный в терминах, подобных терминам языка Паскаль: var a: array [1:3] of integer На языке ПЛ/1 или Фортран это определение можно было бы записать соответственно как DECLARE a (1:3) FIXED или INTEGER а(3) Нижняя граница массива не обязательно должна быть единицей (если исключить старые версии Фортрана); она может быть любым целым числом — отрицательным, нулем или положительным. Нуль часто является более подходящей нижней границей, чем единица, особенно если область значений квантифицированной переменной (скажем, i) записывается в форме m^i<Cn. Например, предположим, что в массиве Ь имеется п элементов и каждый из них не меньше 2. Придавая массиву Ь нижнюю границу 0 и обозначая эти элементы как Ь[0]} МП,. . ., Ь(п—1], мы можем выразить это
Гл. 5. Обозначения и соглашения, касающиеся массивов 95 утверждение следующим образом: (yi:0<^i<n:b[i]^2) На протяжении этого раздела мы будем пользоваться в качестве примера массивом Ь> описанным следующим образом: (5.1.1) varb: array [0:2] of integer Введем некоторые обозначения. Во-первых, обозначение для последовательности (см. приложение 2) используется для описания значений массива. Например, Ь= (4, —2, 7) обозначает, что Ь [0]=4, М1]=— 2, М2]=7. Во-вторых, b. lower обозначает нижнюю границу индексов массива Ь, a b.upper — верхнюю границу его индексов. Например, для Ь, описанного в (5.1.1), b.lower=0y а Ь.иррег=2. Множество индексов массива domain (b) можно определить следующим образом: domain(b) = {i\b.lower ^i^b.upper} Как уже говорилось, массив b> описанный в (5.1.1), традиционно рассматривают как совокупность трех независимых переменных с индексами, b [0], Ь [1] и b [2], каждая из которых имеет тип ш- teger. Можно ссылаться на переменную с индексами как на b(i)y где значение выражения i типа integer находится в domain (b). Можно присваивать значение переменной с индексами (например, b [2]), используя присваивание b [i] :=e, где выражение i имеет в данный момент значение 2. Введение другой точки зрения на массивы может дать определенные преимущества. При этом массив b рассматривается как (частичная) функция: b — простая переменная, в которой содержится функция из множества значений индексов в множество целых чисел. С этой точки зрения bli] —это применение функции: функция, находящаяся сейчас в простой переменной fr, применяется к аргументу i для получения значения целого типа так, как это происходит в abs(i). Замечание. При первом знакомстве функциональный подход к массивам привел меня в недоумение. Работать с ним, казалось, бесполезно и трудно. Только после приобретения опыта в работе с этим подходом я оценил его простоту, элегантность и полезность. Надеюсь, что и у читателя в конце концов сложится то же впечатление. □ Что означает присваивание b[i] :—e при рассмотрении массива как функции? Оно присваивает b новую функцию, которая отличается от старой лишь тем, что при аргументе i ее значение есть е. Например, выполнение Ml] :=8, начинающееся в состоянии И0]=2, М1]=4, М2] = 6
96 Часть I. Высказывания и предикаты заканчивается в состоянии, где массив b отличается от предыдущего лишь одной позицией: Ь [0]=2, М1]=8, М2] = 6 Удобно ввести обозначения для массивов, измененных подобным образом. (5.1.2) Определение. Пусть Ь — массив (функция), i — выражение и е — выражение того же типа, что и элементы массива. Тогда (b; i : е) обозначает массив (функцию), отличающуюся от b лишь тем, что при применении к значению i она дает е : Отметим подобие между обозначением (s; x : v), использовавшимся в разд. 4.6 для модифицированного состояния s, и обозначением ф; i : ё) для модифицированного массива Ь. 8), примененная 8), примененная 8), примененная Пример 1. Пусть b [0:2J = (2, 4, 6). Тогда ф; 0:8)[0] = 8 (т. е. функция (6; 0 к 0, дает 8) ф; 0:8)[1] = 6[1] = 4 (т. е. функция (6; 0 к 1, дает Ь[\]) ф; 0:8) [2]=» 6 [2] = 6 (т. е. функция ф\ 0 к 2, дает b [2]) так что ф; 0:8) = (8, 4, 6). □ Пример 2. Пусть ft [0:2] = (2, 4, 6). Тогда ф; 1:8) = (2, 8, 6) ф; 2:8) = (2, 4, 8) (ф; 0:8); 2:9) = (8, 4, 9) (((&; 0:8); 2:9); 0:7) = (7, 4, 9) Q В примере 2 проиллюстрировано использование вложенных обозначений. Так как (Ь\ 0 : 8) является массивом (функцией) (8, 4, 6), его можно использовать в первом аргументе обозначения (5.1.2). Вложенные скобки становятся довольно обременительными; мы отбрасываем их и вместо этого пользуемся соглашением, что самые правые пары i : e имеют приоритет и являются главными. Итак, последняя строчка примера 2 эквивалентна (й; 0 : 8; 2 : 9; 0 : 7). Пример 3. Пусть 6 [0:2] = (2, 4, 6). Тогда :7)[0] = 7 :7)[1] = (/;; 0:8; 2:9) [1] = (Л; 0:8) [I] = /; [1] = 4 :7)L2J = (6; 0:8; 2:9)[2] = 9 Q ф\ ф\ Ф; 0 0 0 8; :8; 8; 2 2 2 9; :9; 9; 0 0 0
Гл. 5. Обозначения и соглашения, касающиеся массивов 97 Оператор присваивания Ь [i] := e может быть объяснен теперь с функциональной точки зрения на массивы; он просто является сокращением для следующего присваивания простой переменной b: b := (&; i : ё). Итак, имеются два совершенно противоположных взгляда на массивы: массив — это совокупность независимых переменных и массив — это частичная функция. Каждый из них имеет свои преимущества, и, как и в случае корпускулярной и волновой теорий света, мы обращаемся то к одному из них, то к другому, пользуясь тем из них, который удобен для решаемой сейчас задачи. Одним из преимуществ функционального взгляда на массивы является то, что он упрощает язык программирования, поскольку в этом случае имеется лишь один вид переменных — простые переменные. Эти переменные могут содержать функцию, которую можно применять к аргументам, но применение функций уже имеется в большинстве языков программирования. С другой стороны, взгляд на массивы как на совокупность независимых переменных приводит к путанице в трактовке понятия состояния, поскольку теперь состояние должно отображать в множество значений не только идентификаторы, но и единицы текста, подобные МП. Описывая b[i]:= e как сокращение для b:=(b\ i : ё), мы используем функциональную точку зрения для описания результата выполнения оператора, а не для описания того, как должен реализовываться оператор присваивания. Выполнение может по- прежнему производиться на основе совокупности независимых переменных: вычисляются значения i и е, выбирается переменная с индексами, которой присваивается значение, и ей присваивается значение е. Необязательно создавать целый новый массив (&; i:e) и затем присваивать его. Функциональный взгляд на массивы используется и для других целей, кроме описания присваиваний. Например, утверждение perm {(с\ 0 : я), С) говорит, что массив с [О : п—1], в котором нулевому элементу придано значение х, является перестановкой массива С. Утверждать это на формальном языке другим способом было бы неудобно. Упрощение выражений Иногда необходимо упрощать выражения (в том числе и предикаты), содержащие новые обозначения. Часто это можно сделать, как показано ниже, при помощи анализа двух случаев, который основан на определении (5.1.2). Самый трудный шаг — первый, так что кратко объясним его. Прежде всего заметим, что либо t=/, либо [ф\, В первом случае (b\ i : 5) [/]=5 сводится к 5=5, во втором — к &[/]=5. 4 Д. Грив
98 Часть I. Высказывания и предикаты (а) (6; (Ь) (6; (с) (6; 1:3) 1:6(1)) 1:6(4)) (d) (е) (0 (* (^ Ф = (* = / Л 5 = 5) V (ьф / Л Ь [/] — 5) (определение (6; i:5)) = (' = /) V(^/A&[/]=5) ((5 = 5) = 7\ упрощение Л) = (i = / V [ф /) д (i = / V Ъ [/] = 5) (дистрибутивность) = Т Л (* = / V Ь [/] = 5) (исключенное третье) = 1 = / V & [у] = 5 (упрощение Л) Упражнения к разд. 5.1 1. Пусть b [1:4] = (2, 4, 6, 8). Что содержат следующие массивы: 1:6(4); 2:6(3); 3:6(2); 4:6(1)) 4:6(4); 3:6(3); 2:6(2); 1:6(1)) 1:6(1); 1:6(2); 1:6(3); 1:6(4)) 2. Пусть в некотором состоянии i = 2, /=3, 6[0:5] = (—3, —2, —1, 0, 1, 2). Вычислите следующие выражения: (a) (6;/:2)[/] (е) (6; /+/:6)[4] (b) (6; i+I:2)[/] (0 (6; /:2; j:3)L/+/-2] (c) (6; i+2:2)[/] (g)(6; /:2; ,/:3)[/+/-1] (d) (6; /+/:6)[5] (h)(6; /:2; у—1:3)[/] 3. Упростите следующие предикаты, устранив обозначения (Ь; ...): (a) (6;/:5)[/] = (6;/:5)[/] (b) (6; /:6[/])[/] = /• (c) (6;, <:6[/]; j:b\JW] = (b;j:bUl i:b[i])[j] (d) (6; i:b\j]; j:b[i])[i] = (b; j:b[i]; i:b[j])\j] (e) (6; i:b\j]; j:bti])[i] = (b; i:b[i]; j:b[j])\j] (0 Ф\ i:b{iUr\ = (b;j:b\JW] 4. В языке программирования Паскаль есть тип данных record (запись), который позволяет построить новый тип, состоящий из фиксированного числа компонент (полей записи) других типов. Например, описание type /:record п:array[0:10] of char\ ageiinteger end; var p, q:t определяет в языке типа языка Паскаль тип данных / и две переменные: ру q типа /. Каждая переменная содержит два поля. Первое из них называется п и может содержать строку, состоящую из символов в количестве от 0 до 10 (например, имя человека). Второе называется age и может содержать целое число. Следующие присваивания показывают, как можно ссылаться на компоненты р и q и присваивать им значения. После их выполнения как р, так и q содержат «Hehner» в первой компоненте и 32 во второй. Обратите внимание на то, как q.age ссылается на поле age переменной q, образующей запись. р.п:='Hehner'\ p.age := 32; q.n = p. n\ q.age := q.age +1 — 1 С традиционной точки зрения массив состоит из множества отдельных значений одного и того же типа. Запись состоит из множества отдельных
Гл. 5. Обозначения и соглашения, касающиеся массивов 99 значений, которые могут быть разных типов. Чтобы позволить компонентам иметь различные типы, мы несколько пожертвовали удобством: на компоненты записи необходимо ссылаться, используя их имена (а не выражения). Тем не менее массивы и записи подобны. Попытайтесь развить функциональный взгляд на записи, подобный только что представленному функциональному взгляду на массивы. 5.2. Сегменты массивов и картинки массивов Если даны выражения el, е2 типа integer, удовлетворяющие d<]e2+l, то обозначение b(el : е2] используется для массива Ъ, ограниченного областью индексов е\ : е2. Таким образом, для массива, описанного как var 6: array [0:n—1] of integer b[0:n—1] обозначает весь массив, a b[i:j], если 0^i<[/<az, ссылается на сегмент массива, образованный b[i], b[i-\-l], ... ..., b[j]. Если t = / + l, то b[i:j] означает пустой сегмент Ь. Довольно часто приходится утверждать что-то вроде «все элементы массива b меньше х «или» в массиве b содержатся лишь нули». Эти утверждения можно записать следующим образом: (yi:0<^i<n:b[i] <x) (V*:0<* </i:&[i]==0) Поскольку такие утверждения встречаются очень часто, мы сократим их. Они будут записываться соответственно как Ь<Сх и Ь=0, т. е. операции о1тюшений означают поэлементное сравнение, когда они применяются к массивам. Приведем еще несколько примеров, в которых используются массивы b [0 : п—1] и с [О : п—1] и простая переменная х: Сокращение Эквивалентный предикат Ь[\:5]=х Ь[6:10]у±х b[0:k-l]<x<b[k:n-\] b[i:j]<b[j:k] •}(b[6:\0]^x) Будьте очень осторожны с видно, что Ь=у может отличаться от ~| (Ьфу)\ Подобным же образом Ь^у может отличаться от ~| (Ь>у). Введем обозначение х£Ь для утверждения того, что значение х равно (по крайней мере) одному из значений b[i]. Таким образом. (Vi:Ki<5:6[i]=jc) (Vi:0^i<k:b[i]<x)A (Vi:k^i<n:x<b[i]) (Vp,q:i<p^j<q^k:b[p]^b[q]) ,(Vj:6^j^W:b[j]*x) = &j:6<J<10n(b[j]*x)) = Caj:6^j^W;b[j] = x) = и Ф, так как из последнего примера *♦
100 Часть 1. Высказывания и предикаты х£Ь эквивалентно (gi : i£domain(b) : x=b[i]) где domain(b) представляет собой множество значений индексов Ь. Такие сокращения могут облегчить спецификацию программы, а позже и понимание ее спецификаций. Однако при построении программы, удовлетворяющей спецификациям, часто целесообразно расшифровывать сокращения до их полных форм. В некотором смысле сокращения являются одной из форм абстракции. Они позволяют сосредоточиться на их значении, а вопрос о формальном выражении их смысла пока не рассматривать. Это похоже на процедурную абстракцию. Записывая вызов процедуры, мы сосредоточиваемся на том, что она делает, а как она реализована, в этот момент нас не касается. Картинка массивов Перейдем теперь к несколько другой теме, а именно использованию картинок для представления некоторых предикатов, описывающих массивы. Предположим, что мы пишем программу сортировки массива b [0 : п—1] с начальными значениями В [0 : п—1], т. е. вначале Ь=В. Мы хотим описать следующие условия: 1) b [0 : k—1] отсортирован и все его элементы не больше х\ 2) значение, которое имеет b Ik], находится в простой переменной х\ 3) каждое значение из b[k+l:n— 1] не меньше х. Чтобы выразить эти условия на формальном языке, напишем (5.2.1) 0 ^k <n A ordered (b[0:k—\]) perm ((b\ k:x)y B)/\ b[0:k— 1]<*<&[А + 1:п— 1] где ordered(b[0:k— l]) = (vi:0<i <k— l:&[i]<&[i + l]) а обозначение perm (X> Y) означает: «массив Х является перестановкой массива Y». Все это выглядит довольно сложно, не правда ли? А поскольку такие утверждения часто встречаются при работе с массивами, введем так называемые «картинки» для обозначения подобных предикатов таким образом, чтобы их можно было легче понять. Заменим утверждение (5.2.1) следующим: 0 к-1 к к+1 п-\ 0</с <п л Ъ I ordered, ^х \ \ ^х \ Л perm (В, (Ь; к\х)) Второй член прямо описывает имеющее место сейчас деление массива Ь. Имя массива Ь помещено слева от картинки. Свойства каж-
Гл. 5. Обозначения и соглашения, касающиеся массивов 101 дого сегмента массива записаны внутри квадрата, изображающего этот сегмент. Нижние и верхние границы приведены над сегментом, если его размер не менее 0. Если для сегмента дано само значение индекса, то его размер равен 1 (подобно b [k : k\). Границы можно опускать, если картинка однозначна. Картинку, приведенную выше, можно представить по крайней мере еще двумя способами: 0 п-\ Ъ [ordered, ^х >х 0 к-1 к+1 л-1 и Ь ordered, ^x ^х Отметим, что некоторые из сегментов могут быть пустыми. Например, если k=0, то МО : k—1] пуст и картинка сводится к к к+\ л-1 ^х а если А=/1, то сегмент b[k+\ : п] пуст. Один из недостатков этих картинок — то, что из-за них мы часто забываем о вырожденных случаях. Мы невольно думаем, что, поскольку сегмент/? [0 \ k—1] имеется на картинке, он должен что-то содержать. Так что пользуйтесь такими картинками с осторожностью. Существенным свойством подобных картинок является то, что формальное определение присваивания (которое будет дано в ч. II) применимо и к картинкам, если они встречаются в предикатах. Это будет подробно рассматриваться в ч II. Упражнения к разд. 5.2 1. Переделайте упр. 6 к разд. 4.2, используя сокращения, введенные в этом разделе. 2. Представьте следующие предикаты с помощью картинок: (а)0</)<?+1<пл b[0:p—1]<* <b[q+\:n] (b) 0<&— 1</<Л— 1 < п Л \=b[\:k— 1] Л 2 = b[k:f—l] /\3 = b[h+\:n] 3. Замените следующие предикаты на эквивалентные, не использующие картинки: 0 ' к h n (а) 0<А; ^h <п Л Ъ <,х \ = х >х (Ь) 0^1 <и Л b I ordered L
102 Часть I. Высказывания и предикаты 5.3. Обращение с массивами массивов Этот раздел при первом чтении можно опустить. Описание языка Паскаль (5.3.1) var 6:array[0: l] of array[l:3] of integer определяет массив массивов. Это означает, что b [0] (и подобным же образом ИИ) является массивом, состоящим из трех элементов, называемых ЫО] [1], b [0] [2], b [0] [3]. Таким же образом можно получить «массив массивов», и в этом случае могут использоваться три индекса — например, d [i] [/] Ik] и т. д. Массивы массивов пришли на смену двумерным массивам Фортрана и ПЛ/1. Например, (5.3.1) можно считать эквивалентом описания языка ПЛ/1: DECLARE b(0 : 1, 1 • 3) FIXED поскольку оба описания определяют массив, который можно представлять себе как двумерный: *[0][1] 6[0][2] *[0][3] *[0,1] 6 [0,2] £[0,3] или *[!][!] ft [112] ft[l][3] bill] ft[l,2] 6[1.3] Распространим обозначение (b; i : е) на последовательности индексов в том месте, где было /. Воспользуемся следующими соображениями. Если присваивание с [i\ :=2 эквивалентно с:= (с; i: 2), то присваивание b [i] [j]=3 должно быть эквивалентно b : = (b\ [i] [/] : 3), где квадратные скобки помещены вокруг индексов i и /, чтобы обозначения были удобочитаемыми. Нужно уметь ссылаться на последовательности индексных выражений (взятых в квадратные скобки), таких, как U], U'+l][/] и Ш [/] Ik]. Чтобы облегчить это, введем некоторые термины. Термин селектор будет обозначать конечную последовательность индексных выражений, заключенных в квадратные скобки. Нулевой селектор (последовательность, содержащая 0 индексов) обозначается через е. Нулевой селектор обеспечивает некоторые хорошие свойства множества селекторов. Он является единичным элементом операции соединения последовательностей (см. замечание, следующее за определением (4.2.3)), т. е. «для любого идентификатора или селектора s имеем soe=s, где о используется для обозначения операции соединения. Любая ссылка на переменную (с индексами или простую) состоит теперь из идентификатора, соединенного с селек* тором. Ссылка на простую переменную х есть хое.
Гл. 5. Обозначения и соглашения, касающиеся массивов 103 Пример 1. Ьое — это идентификатор Ъ, за которым следует нулевой селектор; он ссылается на весь массив Ь. Ь [0] состоит из идентификатора 6, соединенного с селектором [0]. Для Ь, описанного в (5.3.1), Ъ [0] ссылается на массив (Ь [0] [II, МО] [2], МО] [3]). b [i] [/] состоит из идентификатора Ь> за которым следует селектор [i] [/]. Для Ь, описанного в (5.3.1), это выражение ссылается на некоторое целое число. □ Мы намереваемся определить (b\ s : ё) для любого селектора s. Сделаем это рекурсией по длине s. Первый шаг — определение базиса (Ь\ 8 : ё). Пусть х — простая переменная (которая содержит скаляр или функцию). Поскольку х и хог эквивалентны, присваивания х :== е и хог=е также эквивалентны. Но, согласно введенным ранее обозначениям, последнее из них должно быть эквивалентно х : = (х\ 8 : ё). Отсюда видно, что выражения е и (х\ г : ё) должны давать одно и то же значение, так что имеем е= (х\ е : ё). Используя эту идею, определим понятие (b\ s : ё). (5.3.2) Определение. Пусть b и g — функции или переменные одного и того же типа. Пусть s — некоторый селектор, подходящий для Ь. Обозначение (b; s : ё) для выражения е подходящего типа определяется следующим образом: (6; e:g) «g Пример 2. В этом и последующих примерах пусть с[1:3] =(6, 7, 8), а М0:1][1:3] = ((0, 1, 2), (3, 4, 5)). Тогда (с; в:Ш])-Ь[1],так что (с; е:ф])[2] = &[1][2] = 4. Q Пример 3. (с; 1:3)[1] = (с; [1]ое:3)[1] = (с[1]; e:3)=i3 (с; 1:3) [2] = (с; [1]ов:3) [2] = С[2] = 7 (с; 1:3) [2] = (с; [1]ое:3)[3] = с[3] = 8 Q Пример 4. (Ь; [1][3]:9)[0] = 6[0] = (0, 1, 2) Ф\ [1][3]:9)[1] = (Ь[1]; [3]:9)-(3, 4, 9) П Так же как и раньше, можно опускать все скобки, кроме внешних. Например, следующие два выражения эквивалентны (оба они опре- [
104 Часть /. Высказывания и предикаты деляют массив (функцию), совпадающий с Ъ, за исключением трех мест Ш [/], [/] и Ik] li]): 19) (((&; Ш [/] : е)\ [/]; /): № Ш : g) (&; Ш [/] : в; [/]; /; [Л] Ш : g) Упражнение к разд. 5.3 1. В упр. 4 к разд. 5.1 требовалось развить функциональный взгляд на записи. Можно рассматривать также массивы записей и записи массивов. Например, правильны следующие описания, подобные описаниям языка Паскаль: type /:record x:integer; r/:array[0:10] of integer end; var 6:аггау[0:/г—1] of / Видоизмените обозначение, введенное в этом разделе, таким образом, чтобы можно было допускать ссылки на подзаписи массивов или подмассивы записей и т. д.
Глава 6 ИСПОЛЬЗОВАНИЕ УТВЕРЖДЕНИЙ ДЛЯ ДОКУМЕНТИРОВАНИЯ ПРОГРАММ Эта глава является введением в неформальное использование предикатов в качестве утверждений для документирования программ. Таким образом, она подготавливает почву для более формального изложения этого же материала, которая дана в ч. II и III. 6.1. Спецификации программ Спецификация программы должна точно описывать, что должно быть совершено в результате выполнения программы. Отдельные части спецификации могут затрагивать время выполнения, размер программы и т. п., но сейчас мы сосредоточимся на той ее части, которая описывает лишь то, что должно быть сделано. Один из способов специфицировать программу — выразить ее в форме приказа на естественном языке и на достаточно высоком уровне абстракции. Например, следующее предложение специфицирует программу умножения двух неотрицательных целых переменных: (6.1.1) Записать в z произведение а* Ь, предполагая, что в начале а, 6>0. Если (6.1.1) записано в качестве комментария к сегменту программы, оно называется командой-комментарием. Это комментарий, который является оператором или предписанием выполнить некоторые действия. Неопытный программист может вообразить, что программа, устанавливающая a, b и z в 0, удовлетворяет (6.1.1), поскольку в (6.1.1) не оговорено, что а и b не должны изменяться. Программист же, умудренный опытом, помнит, что программа не должна делать ничего, кроме того, что от нее явно требуется, но и не меньше того, что требуется. Так как в спецификации не упоминается об изменении а и Ь, они не должны изменяться. Команда-комментарий, подобная (6.1.1), должна определять в точности все входные и выходные переменные. Нет ничего более бесполезного, чем частичная спецификация, которую нельзя понять без чтения самой программы. Например, спецификация Перемножьте а и h. \
106 Часть I. Высказывания и предикаты не отражает, куда нужно записать результат умножения, и, следовательно, не может быть понята вне контекста, как это должно бы быть. Естественный язык может быть двусмысленным, так что мы часто обращаемся к технике более формальных спецификаций. Обозначение (6.1.2) {Q} S {R} где Q и R — предикаты, a S — программа (последовательность команд), имеет следующую интерпретацию: (6.1.3) Если выполнение S началось в состоянии, удовлетворяющем Q, то имеется гарантия, что оно завершится через конечное время в состоянии, удовлетворяющем R. □ Предикат Q называется предусловием или входным утверждением S; R — постусловием, обещанием, выходным утверждением или результирующим утверждением S. Фигурные скобки { и } используются для того, чтобы отделить утверждения от самой программы. Обратите внимание, что ничего не сказано о выполнении программы, начинающемся в состоянии, не удовлетворяющем Q. Спецификация затрагивает лишь некоторые начальные состояния. Если программа должна работать со всеми возможными начальными состояниями, например печатая сообщения об ошибках при ошибочных входных данных, то эти случаи должны образовывать часть спецификации и покрываться предикатами Q и R 20). Заметим также, что завершение программы за конечное время гарантировано, конечно же, при условии, что ее выполнение продолжается. И наконец, отметим то, что само (6.1.2) — предикат (т. е. предложение, которое либо истинно, либо ложно), причем обычно мы хотим, чтобы он был истинен. Наша прямая обязанность — при написании программы S, удовлетворяющей (6.1.2), некоторым способом доказать, что Q {S }R действительно имеет место. Во второй части книги будет показано, как такой предикат можно записать в исчислении предикатов, введенном ранее, и как можно доказать формально, что он является тавтологией. В качестве примера использования обозначений (6.1.2) перепишем в таком виде спецификацию (6.1.1). Заметьте, что для того, чтобы дать постусловию имя, в нем использована метка R: (6.1.4) {0<аЛ0<Ь}5{Я:г = а*Ь} К сожалению, в (6.1.4) не отмечается, какие переменные будут изменяться, и в действительности программный сегмент z := 0; а := 0; Ь := 0 удовлетворяет (6.1.4). Чтобы уяснить задачу, как правило, прибегают к здравому смыслу и естественному языку (но см. также разд. 6.2). Часто используется смесь команд-комментариев и фор-
Гл. 6. Использование утверждений 107 мальных обозначений (6.1.2) в следующей стандартной форме: (6.1.5) Даны фиксированные а, £>0, установить (истинность) R г z=a * b. Предусловием программы является то, что дано, перечисляются фиксированные переменные, которые не должны изменяться, а постусловие — то, что надо установить. Приведем еще несколько примеров спецификаций (все переменные имеют целые значения). Пример 1 (суммирование массива). Даны фиксированное п^О и фиксированный массив Ь[0:п—1]. Установить R:s = (%r.0^i<n:b[l\). Q Пример 2 (приближенное значение квадратного корня). Дано фиксированное число я^О, записать в s приближение к квадратному корню я, т. е. установить /?:s2<az<(s+1)2. □ Пример 3 (сортировка). Даны фиксированное м^О и массив Ь[0:п—1]. Отсортировать Ь, т. е. установить /?:(V*:0<*</1— l:6[t]<&[t+l]). □ И вновь в связи с этой спецификацией возникает проблема: результат можно установить, просто сделав все элементы Ь нулями. Эту проблему можно преодолеть, включив комментарий со следующим смыслом: изменять Ъ можно только путем перестановки двух его элементов. Конечно же, при спецификации больших и сложных программ таким простым способом могут возникнуть трудности, и, чтобы справиться с этим, возможно, требуется ввести новые обозначения. Но для большинства случаев будет достаточно простых форм спецификаций, приведенных выше. Даже транслятор может быть специфицирован в этих обозначениях при помощи разумного использования абстракций: {Программа на Паскале (р)} транслятор {программа для IBM 370 (q) Д эквивалентно (р, q)\ где предикаты Программа на Паскале, программа для IBM 370 и эквивалентно должны быть определены где-либо в другом месте 21).
108 Часть I. Высказывания и предикаты 6.2. Представление начальных и конечных значений переменных Программа swapit : = х; х : = у\ у : = t переставляет, или меняет местами, значения целых переменных х и у, используя «локальную» переменную /. Чтобы формализовать, что делает swap, нужен способ описания начальных и конечных значений х и у. Для этого используем идентификаторы X и Y: (6.2.1) {x=XAy=Y}swap {X=Y/\y=X) Теперь мы утверждаем, что (6.2.1) истинно всегда: оно является тавтологией. Напомним, что в разд. 4.5 говорилось о том, что тавтология со свободными переменными эквивалентна тому же предикату, в котором все свободные переменные связаны поставленным перед ним квантором всеобщности, т. е. (6.2.1) эквивалентно (6.2.2) (уХ, Y:\x = X Ay=Y}swap{x = Y /\У = Х\) и на самом деле (уХ, Y, х, y:{x = XAy = Y}swap{x = Y АУ = Х}) На естественном языке (6.2.2) можно прочесть следующим образом: для всех (целых) значений X и У, если первоначально х= =Х и y=Y, после выполнения swap станет у=Х и x=Y. Начальные значения переменных х и у обозначаются X и У, но эти же переменные обозначают и конечные значения у и х. Идентификатор может обозначать либо начальное, либо конечное значение, либо даже значение, от которого зависит начальное или конечное значение. Например, следующий предикат также является спецификацией swap, хотя это и не так легко понять: {x=X + lAy=Y—l} swap {x=Y— \Ау=Х+\} В общем случае прописные буквы используются в идентификаторах, которые представляют начальные и конечные значения переменных программы, а строчные буквы — в идентификаторах, являющихся именами переменных программы. В качестве последнего примера вновь специфицируем программу сортировки массива, используя на этот раз добавочный идентификатор, чтобы облегчить проблемы, упомянутые в примере 3 из разд. 6.1. Предикат perm (с, С) означает: «массив с является перестановкой массива С». См. упр. 5 к разд. 4.2. Пример 1 (сортировка). Даны фиксированное гС^О и массив с [0 : п—1], такой, что с=С. Установить R:perm(c, С) A (v*:0<*< n:c[i]^c[i+ l])
Гл. 6. Использование утверждений 109 Упражнения к разд. 6.2. 1. Запишите спецификации следующих задач. Спецификации выразите в форме, использованной выше в примере 1, а также в форме {Q} S {R}. Задачи могут формулироваться неточно, так что вам, возможно, придется прибегнуть к здравому смыслу и опыту для получения точной спецификации. (a) Придать х максимальное значение массива Ь[0:п — 1]. (b) Придать х абсолютную величину х. (c) Найти 'место максимального значения в массиве Ь[0:п—1]. (d) Найти место первого максимального значения в массиве Ь[0:п—1]. (e) Решить, является ли данное целое число, большее 1, простым. (Число, большее 1, простое, если оно делится только на 1 и на самое себя.) (f) Найти п-е число Фибоначчи /„. Числа Фибоначчи определены следующим рекуррентным соотношением: /0 = 0, /i=l для п > 1, /rt = /n-i + /«-2- Таким образом, последовательность чисел Фибоначчи начинается с (0, I, 1,2,3,5,8). (g) Решить, упорядочен ли массив целых чисел Ь[0:п — 1] (в порядке возрастания). (h) Придать каждому из значений массива Ь[0:п— 1] сумму всех значений массива Ь. (i) Пусть с[0:п — 1] — список всех тех, кто преподает в Корнеллском университете, a w[0:n—1] —список всех тех, кто получает пособие по безработице в Итаке. Оба списка упорядочены по алфавиту. Иззестно, что по крайней мере одно лицо фигурирует в обоих списках. Найти первое такое лицо! (j) Задача та же, что и в (i), но теперь есть три списка: с — список преподавателей Корнеллского университета, w — список получающих пособие и т — список тех, кто зарабатывает, консультируя федеральное правительство, (к) Рассмотрим двумерный массив g[0:n — 1, 0:3]. g [i, 0J, g[it 1], g[i,2] и g[i, 3]—оценки, полученные студентом / в этом семестре. Пусть в массиве пате[0:п—1] содержатся имена студентов. Найти студента с наивысшим средним баллом. Можно использовать действительные переменные, которые могут содержать числа с плавающей запятой. 6.3. Наброски доказательств Мы показали, как записывать предикаты (внутри фигурных скобок) перед программой и после нее для утверждения того, что должно быть истинно перед выполнением и после него. Аналогичным образом предикаты могут появляться между двумя операторами, показывая, что должно быть истинно в этот момент выполнения программы. Например, представим полную формулировку программы swap, которая переставляет (меняет местами) значения переменных х и у, используя локальную переменную t \X = x/\y = Y) t : \t< х : {(■■ У ■ = X = ХД* = = У = ХД* = = t "ХАУ = Y Лу Читатель может неформально проверить следующее: для каждого оператора программы, если истинно его предусловие (т, е. предикат У}
110 Часть /. Высказывания а предикаты в фигурных скобках, предшествующий оператору), выполнение оператора заканчивается в состоянии, где истинно его постусловие (предикат в фигурных скобках, следующий за ним). Предикат, помещенный в программу, называется утверждением. Утверждается, что он истинен в соответствующий момент выполнения программы. Программа вместе с утверждениями между каждой парой соседних операторов называется наброском доказательства, так как она и на самом деле им является. Она является наброском формального доказательства, и можно понять, что программа удовлетворяет своим спецификациям, просто показав для каждой тройки (предусловие, оператор, постусловие), что она удовлетворяет предикату {предусловие} оператор {постусловие}-. Формальный метод доказательства описывается во второй части книги. Помещение в программу утверждений для целей ее документирования часто называется аннотированием программы, а получившаяся программа — аннотированной программой. Приведем ниже набросок доказательства следующего утверждения: {*>0As=l+2+. . .+0 i := i+1; s := s+i {£>0As=l+2+. • .+0 Этот набросок иллюстрирует еще два соглашения. Во-первых, можно давать имя утверждению, чтобы легче было его упоминать. Это имя помещается в начале предиката и отделяется от него двоеточием. Во-вторых, идущие подряд утверждения (например, {Р} {Р1}) означают, что первое из них влечет второе, т. е. Р=>Р1. Строки занумерованы лишь для ссылок при последующем обсуждении доказательства. (1) {Р:£>0 Л s- 1 +2+ ... +»} (2) \p\:i+l>0As==l+2+...+(i+l-l)} (3) i := i+1; (4) {P2u>0As = l+2+...+(i-l)} (5) {P3:i>0/\s + i=l + 2+...+i\ (6) s : = s + i (7) {Rii>0As=l+2+...+i\ В этом наброске доказательства отображены по порядку следующие факты: 1. Р=>Р1 (строки 1, 2) 2. {Pl\i := i+l{P2} (строки 2, 3, 4) 3. Р2=>РЗ (строки 4, 5) 4. {P3}s := s + i{R} (строки 5, 6, 7) Все они вместе и приводят к ожидаемому результату: выполнение i := i+l;s := s + i, начавшееся в состоянии, удовлетворяющем Р, заканчивается в состоянии, удовлетворяющем R.
Гл. 6. Использование утверждений 111 В следующем примере показано использование условного оператора. Заметим, что утверждение, следующее за then, является конъюнкцией предусловия условного оператора и его проверяемого условия, так как именно она и будет истинна в этот момент выполнения программы. Поскольку как часть then, так и часть else условного оператора заканчиваются утверждением x = abs(X), это утверждение и является тем, что мы можем сказать о выполнении условного оператора. {х = Х\ if х < 0 then{x=XA *<0} X '. = X |х = _Хдх>0} {x = abs(X)\ else \х = Х /\х^0\ skip {л: = Х Ax>0}{x = abs(X)} \x = abs(X)\ Дальнейшие подробности, касающиеся аннотирования программ, будут изложены при изучении циклов в ч. П. Однако целесообразно здесь сделать одно замечание: не всегда обязательно давать полный набросок доказательства; нужно вставить достаточно утверждений, чтобы программа стала понимаемой, однако не так много, чтобы она затерялась в них. В общем случае хороший стиль программирования — вводить те утверждения, которые не так уж легко сформулировать читателю, и опускать те, которые программист может восстановить сам.
ЧАСТЬ II Семантика простого языка программирования В этой части книги вводится некоторая система понятий программирования и определяется в терминах понятия «слабейшее предусловие». Главная тема части — операторы, или команды, нашей системы обозначений и то, как их можно трактовать. Синтаксис описаний и выражений — тема второстепенная, и вместо того, чтобы формально определять его, мы рассчитываем на осведомленность читателя в области математики и программирования. В общем для описаний используются обозначения, подобные обозначениям языка Паскаль, которые читатель, вероятно, поймет без особого труда. Понятно, что каждая простая переменная и каждое выражение имеет тип — обычно integer или Boolean, а переменные считаются типа integer, если другое не специфицировано явно или не следует из контекста.
Глава 7 ПРЕОБРАЗОВАТЕЛЬ ПРЕДИКАТОВ wp Наша непосредственная задача — определить команды (операторы) простого языка программирования. Это будет делаться следующим образом. Для любой команды 5 и любого предиката Ry который описывает ожидаемый результат выполнения команды S, определим предикат, обозначаемый wp(S, R), который представляет (7.1) множество всех состояний, для которых выполнение команды S, начавшееся в таком состоянии, обязательно закончится через конечное время в состоянии, удовлетворяющем R. □ Приведем несколько примеров для некоторых команд алголопо- добного языка. Примеры основываются на нашем знании того, как выполняются эти команды. Пример 1. Пусть 5 — команда присваивания i := i+l, a R — это i^l. Тогда wp("i := i + l", f<l) = (/<0) так как если i<!0, то выполнение i := i + l заканчивается при t^l, в то время как для i >0 выполнение оператора не может сделать i^.1. П Пример 2. Пусть 5—это if х^у then г := х else г := y,aR — это г = max (х, у). Выполнение команды 5 всегда присваивает г значение max (х, у), так что wp(Sy R) = T. □ Пример 3. Пусть 5—то же, что и в примере 2, a R—это г = yt Тогда wp(S, R) = (y^ x), так как выполнение 5, начинающееся при у^ху присваивает z значение у, а выполнение 5, начинающееся при у < х, присваивает z значение х, которое не равно у. □ Пример 4. Пусть S—то же, что и в примере 2, a R—это z = = у—1. Тогда wp(S, R) = F (пустое множество состояний), так как выполнение 5 никогда не может сделать z меньше у. □ Пример 5. Пусть S—то же, что и в примере 2, a R—это z = = у+1. Тогда wp(Sy R) = (x = y+l)y так как только тогда выполнение S присвоит z значение у+1. □ Пример 6. Для некоторой команды S предикат wp (5, Т) представляет собой множество всех состояний,, таких,, что выпол-
114 Часть II. Семантика простого языка программирования нение S, начавшееся в одном из этих состояний, обязательно заканчивается. П В разд. 6.1 для обозначения того, что выполнение S, начавшееся в состоянии, удовлетворяющем предикату Q, завершится в состоянии, удовлетворяющем предикату R, использовалось обозначение {Q} S {R}. В таком контексте Q называется предусловием, a R — постусловием S. Подобным же образом wp (S, R) называется слабейшим предусловием для S по отношению к /?, поскольку оно определяет множество всех состояний, таких, что выполнение, начавшееся в любом из них, закончится при истинном/?. (Длятого чтобы определить значение слов «сильнее» и «слабее» в этом контексте, см. разд. 1.6.) Теперь видно, что обозначение {Q} S {R}— просто другая форма предиката (7.2) Q=>wp(Sy R) Обратите внимание на то, что {Q} S {R} — в действительности предложение исчисления предикатов, так как оно эквивалентно Qb^^/?(5, R). Таким образом, в любом состоянии оно либо истинно, либо ложно. Когда мы его записываем, обычно подразумевается, что оно является тавтологией: мы рассчитываем на то, что оно тождественно истинно. Обычно команда S создается для определенной цели, а именно обеспечить истинность какого-то конкретного постусловия R. Из- за этого нас не всегда интересуют общие свойства S; нас интересуют только те свойства, которые имеют отношение к R. Более того, даже для того же R можно интерессшаться не слабейшим предусловием wp(S, R), а (как это обычно и бывает) некоторым более сильным предусловием, например Q, которое представляет подмножество множества, определяемого wp(Sy R). Таким образом, если можно показать, не формулируя явно wp(S> /?), что Q=>wp(S, R), то нас удовлетворяет использование Q в качестве предусловия. Возможность работать с предусловиями, не являющимися слабейшими, полезна, так как выводить само wp(S> R) не всегда практично, что будет показано при рассмотрении циклов. Отметим, что wp — функция с двумя аргументами: командой 5 и предикатом R. Зафиксируем на некоторое время произвольную команду 5. Тогда wp(Sy R) может записываться как функция одного аргумента: wps(R). Функция wps преобразует любой предикат R в другой предикат wps(R). Это объясняет происхождение термина «преобразователь предикатов» для wps. Замечание. Впервые обозначение Q{S}R было использовано в 1969 г. (см. гл. 23) для обозначения частичной корректности программ. Оно имело следующую интерпретацию: если выполнение команды S начинается в состоянии, удовлетворяющем Q, и если оно заканчивается, то конечное состояние будет удовлетворять R. Фигурные
Гл. 7. Преобразователь wp 115 скобки вокруг предикатов (а не вокруг команд) используются для обозначения полной корректности: выполнение обязательно заканчивается. В качестве примера заметим, что Т{while T do skip}T (где skip — пустая команда) является тавтологией, поскольку выполнение этого цикла никогда не заканчивается. Но {Т} while T do skip {T} которое эквивалентно T=5>wp("while T do skip", 71), тождественно ложно. □ Некоторые свойства wp Если мы намереваемся определять систему понятий программирования через концепцию wp, то должны удостовериться, что wp «ведет себя хорошо». Под этим понимается то, что с помощью wp мы сможем определять разумные и реализуемые команды. Более того, было бы неплохо, если бы нереализуемые команды отвергались, выбрасывались из рассмотрения. Поэтому проанализируем интерпретацию (7.1) понятия wp(S, R) и посмотрим, можно ли из нее вывести какие-либо свойства этого понятия. Прежде всего рассмотрим предикат wp(S> F) (для произвольной команды S). Этот предикат описывает множество состояний, таких, что выполнение 5, начавшееся в любом из этих состояний, обязательно закончится в состоянии, удовлетворяющем F. Но никакое состояние не может удовлетворять Z7, так как F представляет пустое множество. Следовательно, не может быть состояния, для которого wp(Sy F), и мы имеем первое свойство. (7.3) Закон исключенного чуда: wp (5, F)=F. Название закона очень подходит к этому свойству, так как, действительно, было бы чудом, если бы выполнение могло завершиться ни в каком состоянии. Второй закон следующий. Для любой команды S и предикатов Q и R выполняется следующее свойство. (7.4) Дистрибутивность конъюнкции: wp(S, Q) Л wp(S9 R) = wp(S, QAR) Посмотрим, почему (7.4) — тавтология. Вначале рассмотрим произвольное состояние s, удовлетворяющее левой части (7.4). Выполнение команды S, начавшееся в s, завершится при истинных Q и R. Следовательно, Q/\R также будет истинно, и s находится в wp(S, Q/\R). Это показывает, что из левой части следует правая. Теперь предположим, что s находится в wp(Sy QAR)- Тогда выполнение 5, начавшееся в s, обязательно завершится в некотором состоянии s', удовлетворяющем QAR- Любое такое s' удовлетворяет
116 Часть II. Семантика простого языка программирования hQ, и R, так чтоs находится в wp(S, Q) и в wp(S, R). Следовательно, из правой части следует левая. Вместе с предыдущей импликацией это влечет, что они эквивалентны. Таким образом, показано, что имеют место (7.3) и (7.4). Наши доводы основывались лишь на неформальной интерпретации (7.1), которую требовалось дать обозначению wp (5, R). Теперь (7.3) и (7.4) можно принять за исходные аксиомы и использовать их так же, как это делается для других аксиом и законов исчисления предикатов. С помощью этих аксиом можно доказать еще два полезных закона; их доказательства оставлены в качестве упражнений. (7.5) Закон монотонности: если Q=>R, то wp (5, Q)=>wp (S, R) (7.6) Дистрибутивность дизъюнкции: wp(S, Q)\/wp(Sy R)=>wp(Sy QVR) Интересно сравнить (7.4) и (7.6). Один из них является эквивалентностью, а другой — импликацией. Почему? Причина заключается в том, что выполнение команд может быть недетерминированным. Выполнение команды недетерминировано, если оно не обязано быть в точности одним и тем же каждый раз, когда оно началось в одном и том же состоянии. Такое выполнение может выдавать разные ответы или просто приходить разными «путями» к одному и тому же ответу. Большинство последовательных языков программирования, подобных Алголу или Фортрану, реализованы детерминистски: выполнение программы, начавшееся в некотором состоянии, всегда одно и то же. Так что идея недетерминизма может быть для вас новой. В качестве примера недетерминистского действия, для которого левая и правая части (7.6) неэквивалентны, рассмотрим бросание монеты, которая теоретически считается столь тонкой, что не может опуститься на ребро. Нет гарантий, что при бросании монеты выпадет герб, так что wp(6pocanuey герб)=Р. Подобным же образом wp(6pocaHue, решетка) =F. Следовательно, wp (бросание, герб) \J wp (бросание, решетка) =F Но монета обязательно упадет либо гербом, либо решеткой вверх, так что wp (бросание у гербу решетка)=Т Если известно, что команда детерминистская, то можно показать (см. упр. 6), что (7.7) wp(S, Q)Vwp(S, R)=-wp(S, QWR) (для детерминистских S) Хорошо запомните, что недетерминированность является свойством реализации команды, а не самой команды. Если команда удовлетворяет (7.7), то ее можно реализовать детерминистски без ограничения общности. Если же команда не удовлетворяет (7.7), то
Гл. 7. Преобразователь wp 117 в случае, когда она реализована детерминистски, реализация, по- видимому, в чем-то ограничивает команду, например требуя такого искусства в бросании монеты, чтобы она падала обязательно гербом вверх. В следующей главе мы приступим к определению системы понятий программирования в терминах wp. При этом необходимо быть особенно аккуратным. Для любой команды S функция wps(R) дает предикат, и на первый взгляд может показаться, что любая функция, чьи область определения и область значений — множество предикатов, может подойти в качестве wps для некоторого S. Но помните, что такие функции должны представлять реализуемые команды. По крайней мере, мы обязаны удостовериться, что такие функции удовлетворяют законам (7.3) и (7.4), поскольку эти свойства были получены, исходя из понятия выполнения команды. Мы не всегда будем делать это в последующих главах (поскольку это уже было сделано раньше), предпочитая оставлять эту задачу в качестве упражнений читателю. Упражнения к гл. 7 1. Определите wp (S, R) для следующих S и R, основываясь на знаниях о том, как выполняется S. Предполагается, что все переменные — типа integer, а все индексы находятся в областях значений индексов массива. S R (а) (Ь) (с) (d) (е) (0 /:=/ + ! i:=/+2;y:=y-2 i:=i + l;j:=j-l z:= z*j; /:=/-l *['']:= 1 «[«[/]]:= i i>0 i+/=0 /*/=0 z*j'=c «['■]=«[/] *[/] = / 2. В каждом из примеров 1—5 в данной главе предикаты были заданы в виде wp(S, R) = Q. Перепишите их в виде {Q}S{#}, чтобы привыкнуть к двум разным системам обозначений. Например, пример 2 можно записать в виде {Т} if х ^у then z: = x else z: =y{z = max (x, у)} 3. Докажите (7.5) и (7.6). Не полагайтесь на понятия выполнения и интерпретации (7.1); выведите их лишь из (7.4) и законов исчисления предикатов. 4. Докажите (wp (5, R)Awp(S, ~}R))=F, используя (7.4). 5. Приведите пример, показывающий, что следующее утверждение не является истинным во всех состояниях: (wp(S, R)\Zwp(S, ~]R)) = T. 6. Покажите, что (7.7) выполнено для всех детерминированных S. (Это нельзя показать, исходя из аксиом (7.3) и (7.4). Это утверждение надо обосновывать, исходя из определений детерминизма и wp, так же, как это было сделано для (7.3) и (7.4).)
lid Часть II. Семантика простого языка программирования 7. Предположим, что доказано Q=$wp(S, R) для некоторых Q, R и S. Полностью проанализируйте предложение (7.8) {(Y*:Q)} S{(Vx:R)} (Истинно ли оно в общем случае? Если это не так, какие ограничения нужно наложить, чтобы оно выполнялось для «подходящих» классов предикатов Р, Q и команд S и т.д.) (Указание: соблюдайте осторожность при рассмотрении случая, когда х входит в 5). Возможно, вы захотите ответить на этот вопрос, основываясь на общем правиле: появление х в S означает, что (7.8) неверно, и квантифицированная переменная может быть изменена перед продолжением рассмотрения. Однако поучительно также ответить на вопрос без применения этого общего правила. См. разд. 4.3. 8. Предположим, что доказано Q=$>wp(S, R) для некоторых Q, R и S. Полностью проанализируйте предложение {(3*:Q)} S{(3*:/?)} (Истинно ли оно в общем случае? Если это не так, какие ограничения необходимо изложить, чтобы оно выполнялось для «подходящих» классов предикатов и команд?) См. указание к упр. 7.
Глава 8 КОМАНДЫ skip, abort И КОМПОЗИЦИЯ КОМАНД Будем определять понятия программирования в терминах wp. Заодно будем отмечать, как должна выполняться каждая команда из нашей системы обозначений, так что читатель может сравнить ее с операторами других, обычных алгоритмических языков. Кроме того, показывая, как можно выполнить команду, мы тем самым демонстрируем, что она действительно полезна. Но рассматривать в качестве определения команды надлежит именно определение в терминах wp. Начнем с команды skip. При выполнении этой команды ничего не делается (надо предполагать, очень короткое время). Команда skip эквивалентна «пустому» оператору Алгола-60 или Паскаля, а также оператору ПЛ/1, состоящему лишь из точки с запятой «;». В нашу систему понятий она включена по двум причинам. Во-первых, часто полезно иметь возможность сказать явно, что ничего делаться не должно. Но, во-вторых, почти столь же важно и то, что преобразователь предикатов этой команды очень прост в математическом смысле: он является тождественным преобразованием. (8.1) Определение, wp (skip, R)=R. П Вторая команда — abort. Эта команда вводится не столько из- за того, что она полезна при программировании, сколько из-за того, что она также обладает простым в математическом смысле определением: это единственная возможная команда, чей преобразователь предикатов — «постоянная» функция (см. упр. 3). (8.2) Определение, wp (abort, R)=F. □ Как выполняется команда abort? Эта команда никогда не будет выполняться, поскольку она может выполняться лишь в состоянии, удовлетворяющем предикату F, но ни одно состояние предикату F не удовлетворяет! Если когда-либо выполнение программы достигает точки, в которой должна выполняться команда abort, то очевидно, что программа (и ее доказательство) ошибочна, и требуется аварийное завершение программы. Последовательное соединение — это один из способов составления больших программных сегментов из меньших. Пусть S1 и S2 — две команды. Тогда SI; S2 — новая команда. Чтобы ее выполнить, нужно сначала выполнить 57, а затем S2. Ее формальное определение следующее: (8.3) Определение. wp("Sl; S2", R) = wp(Sl, wp(S2, R)) Q
120 Часть II. Семантика простого языка программирования В качестве тривиального примера мы имеем wp("skip; skip", R) = wp(skipy wp(skipy R)) = wp(skipy R) (так как wp (skip, R) = R) = /? Рассмотрим теперь последовательность из трех команд: 51; 52; S3. Ее выполнение должно включать выполнение сначала 5/, затем 52 и , наконец, 53, но нужно убедиться в том, что такая последовательность также имеет смысл в терминах wp. Как она должна интерпретироваться: как (5/; 52); S3 или как 5/; (52; 53)? К счастью, операция композиции функций, используемая при определении последовательного соединения, ассоциативна (см. приложение 2). Следовательно, wp("Sl, (52; 53)", R) = wp("(Sly 52); 53", R) Таким образом, не имеет никакого значения, как интерпретировать 57; 52; 53: как 5/, соединенное с 52; 53, или же как S1; 52, соединенное с 53, и можно совершенно спокойно опустить скобки. (Подобным же образом, поскольку сложение ассоциативно, a+b+с полностью определено, так как а+ (b+с) дает тот же результат, что и (а+Ь)+с.) Нужно осознавать роль, которую играет здесь точка с запятой; она используется для соединения двух смежных независимых команд в единую команду примерно так же, как в естественном языке для соединения независимых предложений. (В качестве примера использования точки с запятой в естественном языке см. предыдущее предложение.) Ее можно представить как операцию, соединяющую две команды, подобно операции конкатенации, которая используется в языке Паскаль или ПЛ/1 для соединения двух строк символов. Как только это станет понятным, у вас не будет возникать сомнений по поводу того, где ставить точку с запятой. Такое использование точки с запятой согласуется не только с ее употреблением в естественном языке, но и с тем, как она использовалась в первом языке программирования—-Алголе-60 *\ в систему обозначений которого она была включена. Жаль, что создатели ПЛ/1 и Ады посчитали нужным пойти против обычая и использовали точку с запятой в качестве завершителя оператора, так как это привело к большой путанице. Итак, пока у нас набралось не так уж много понятий языка программирования: все, что мы можем написать, является последовательностью команд skip и abort. В следующей главе определяется оператор присваивания. Прежде чем продолжить чтение, сделайте хотя бы некоторые из упражнений, чтобы твердо овладеть этим (пока простым) материалом. *} Точка с запятой использовалась в качестве соединителя последовательно выполняющихся операторов еще в 5Q-x годах в ряде «доалголовских» языков программирования.— Прим. ред.
Гл. 8. Команды skip, abort и композиция команд 121 Упражнения к гл. 8 1. Докажите, что определение (8.1) удовлетворяет законам (7.3), (7.4) и (7.7). 2. Докажите, что определение (8.2) удовлетворяет законам (7.3), (7.4) и (7.7). 3. Рассмотрите попытку ввести команду сделать-истинным при помощи постоянного преобразователя предикатов: wp (сделать-истинным, R) = T для всех предикатов R Почему сделать-истинным не является правильной командой? 4. Докажите, что определение (8.3) удовлетворяет законам (7.3) и (7.4), если S1 и S2 им удовлетворяют. 5. Докажите, что если S1 и S2 удовлетворяют (7.7), то и (8.3) ему удовлетворяет. Это показывает, что последовательное соединение не вносит недетерминизма. 6. Докажите, что wp("x := e\ abort", R) = F для любого предиката R безотносительно к определению wp("x := e", R).
Глава 9 КОМАНДА ПРИСВАИВАНИЯ 9.1. Присваивание простым переменным На некоторое время ограничимся рассмотрением присваиваний простым переменным типа integer, Boolean и т. п. Присваивания элементам массива будут рассматриваться в разд. 9.3. Команда присваивания выглядит следующим образом: х := е где х — простая переменная, е — выражение и типы х и е совпадают. Такую команду читают «л: получает значение е». Правильно команда присваивания х := е может выполняться лишь в состоянии, в котором может быть вычислено значение е (например, в котором не происходит деления на нуль). Выполнение состоит из вычисления значения е и записи получившегося значения в ячейку памяти, называемую х. В результате (значение) х заменяется на (значение) е, и подобная же, но синтаксическая подстановка составляет сущность следующего определения: (9.1.1) Определение wp ("х :== е", R) = domain (e) cand/?£, где (9.1.2) domain(e) — предикат, описывающий множество всех состояний, в которых может быть вычислено значение е (т. е. где е определено). □ Так как выражения е не определяются формально, не будет формально определяться и предикат domain(e). Тем не менее он должен исключать все состояния, в которых вычисление значения е могло бы не дать результата (например, из-за деления на нуль или выхода индекса за область значений). Этот предикат можно определить рекурсией по структуре выражений (см. упр. 6). Часто мы склонны совсем опускать domain (е)у записывая (9.1.3) wp("x := е\ /?) = /?* поскольку присваивания следует всегда писать лишь в таких контекстах, где входящие в них выражения могут быть правильно вычислены. На первый взгляд определение (9.1.3) может нас озадачить, так как оно, казалось бы, заставляет «мыслить в обратном направлении». Наша интуиция, выработавшаяся при операционном подхо-
Гл. 9. Команда присваивания 123 де, подсказывает, что предусловием должно быть R, а постусловием— Rxe\ Дадим неформальное объяснение определению (9.1.1). Поскольку х после выполнения будет содержать значение е, то R будет истинно после выполнения, если и только если результат подстановки е вместо х в R истинен перед выполнением. Более формальное объяснение остается в качестве упр. 3. Следующие примеры дадут некоторую уверенность в корректности определения. В частности, примеры 7 и 8 должны убедить читателя, что определение согласуется с обычной моделью выполнения присваивания. Пример 1. wp("x := 5", х = 5) = (5=*5) = 7\ Следовательно, выполнение х :== 5 всегда делает х=5. □ Пример 2. wp("x := 5", хф 5) = (5^= 5) = /\ Значит, выполнение никогда не может сделать х Ф 5. □ Пример 3. wp("x := x+V\ x < 0) = (л;+ 1 < 0)= (х < — 1) □ Пример 4. wp("x := x*x"t x* = 10) = ((л;*л;)4= 10) =:(л:8= 10) □ Пример 5. Для любого одноместного предиката р wp ("х := а-т-b", р (х)) = (Ь ф 0 cand p (a ~ b)) В этом примере требуется явное использование понятия do- main(e) из определения (9.1.1). □ Пример 6. Пусть массив Ь описан так, что область значений его индексов составляет 0—100. Тогда wp("x := b[i]n, * = &[Т|) = (0<*<100 cand b[i] = b[i]) = (0</<100) Таким образом, после завершения присваивания х будет содержать значение b[i], если и только если i—правильный индекс для массива Ь. □ Пример 7. Предположим, что с—константа. Тогда wp("x := е", х = с) = (е = с) Это означает, что выполнение х : = е обязательно завершится и даст значение с в х, если и только если значение выражения е перед выполнением присваивания было с. □ Пример 8. Пусть с — константа, а х и у — различные идентификаторы. Тогда wp(ux := е", у = с) = (у = с) О Пример 8 проливает свет на многие вопросы. Поскольку у должен сохранить свое исходное значение с, выполнение присваивания х : = е не может изменить у. Так как изложенное выше должно выполняться для всех переменных у и для всех значений с> вы-
124 Часть II. Семантика простого языка программирования полнение х : = е может изменять только х и никакую другую переменную. Следовательно, не допускается никакого так называемого побочного эффекта. Это ограничение действует всегда: выполнение присваивания может изменять лишь ту переменную, которая стоит в его левой части. Выполнение выражения не может изменять ни одной переменной. Это ограничение запрещает функции с побочным эффектом. Запрещение побочных эффектов исключительно важно, так как оно позволяет рассматривать выражения так же, как это обычно делается в математике. Это означает, что можно использовать все те обычные свойства выражений, которые полезны при математических преобразованиях, такие, как ассоциативность и коммутативность сложения и логические законы гл. 2. Обмен местами значений двух переменных Последовательность присваиваний t: = x\ х: = у; у := t может использоваться для взаимной замены (перестановки) значений переменных х и у. Это показывают следующие преобразования: Wp(«t := х\ х := у\ у := Г, x = X/\y = Y) = wp ("t = wp ("t = wp ("t = wp("t = x\ x := */", wp("y := Г, x = Xf\y = Y)) = x\ x := y", x = X/\t=*Y) = xn, wp{"x := yn, x = XAt = Y)) = xnt y = XAt = Y) = {y = XAx = Y) Подобные преобразования сравнительно трудно читать и писать. Вместо них используется набросок доказательства, как это показано в (9.1.4) слева: (9.1.4) {y = XAx=Y\ t :— х; {y = XA( = Y} х := у; {х-хл/-П у:= t \x = XAy = Y\ \y = XAx=Y) t := x; x := у; У := t; {x = XAy=Y\ Вспомним из разд. 6.4, что в наброске доказательства утверждения стоят между каждыми двумя командами. Утверждение является постусловием первой команды и предусловием второй. Часто набросок доказательств читается от конца к началу, поскольку предусловие определяется постусловием и командой. Можно также сокращать такое доказательство, как это показано в (9.1.4) справа, поскольку определение промежуточных утверждений — простая, почти что механическая работа.
Гл. 9. Команда присваивания 125 Упражнения к разд. 9.1 1. Определите и упростите wp(S, R) для приведенных ниже пар (S, R). Переменная а115 имеет тип Boolean, все остальные — тип integer. S R (а) • (Ь) (с) (d) (е) (0 (g) *:= 2*у+Ъ х'-- х+у 7-7 + 1 all5:=(b[J] = 5) а//5:=а//5л(6[/]=5) х'.= х*у х'= (х—у)* (х+у) лг = 13 х<2*у 0</ л (у i:0<i^j:b[i]=5) all5 =(Yi:0^i^j:b[i]=5) all5 =(Vi:Q^i^j;b[i]=5) x*y =c x+y2^0 2. Докажите, что определение (9.1.3) удовлетворяет законам (7.3), (7.4) и (7.7). Последнее утверждение показывает, что присваивание — детерминистская команда. 3. Вспомните разд. 4.6 (Некоторые теоремы о подстановке). Пусть s — состояние машины перед выполнением х := е, a s' — конечное состояние. Опишите s и s' в терминах того, как выполняется х := е (например, каким должно быть значение х после завершения присваивания?). Затем покажите, что s' (R) истинно для любого предиката /?,если и только если s (Re) истинно. Наконец, докажите, почему из этого последнего факта следует, что определение присваивания совместимо с операционным взглядом на эту команду. 4. Можно записать «правило преобразования вперед» для присваивания, которое выводит из предусловия сильнейшее постусловие sp(Q, ux := е"), такое, что выполнение х := е при истинном Q оставляет истинным sp(Q, ux := е") (в определении, которое дано ниже, v—это начальное значение х): sp(Q, "х := e") = (lv: QxvAx = e$) Покажите, что это определение также согласовано с моделью выполнения присваивания. Один из способов, как сделать это,— показать, что выполнение х := е при истинном Q обязательно завершится при истинном sp(Q, их := е ): {Q}x := e{sp(Q, "x := е")} 5. См. упр. 4. Приведите пример, показывающий, что Q не эквивалентно wp("x := ё\ sp(Q, "х := еп)). 6. Рассмотрите выражения типа integer, определенные при помощи следующего синтаксиса: <выражение> :: = <слагаемое> | <слагаемое>-{-<слагаемое> <слагаемое> :: = <множитель) | <слагаемое> * <множитель> <слагаемое> ~ <множитель> <множитель> :: = <целая константа) | <идентификатор> ^идентификатор массива) «выражение)] Пусть domain (b) — множество значений индекса массива Ь. Определите рекурсией по структуре выражений domain «выражение» для любого выражения. Предполагается, что ошибки, которые могут произойти,— деление на нуль и выход индекса за границы массива.
126 Часть П. Семантика простого языка программирования 9.2. Кратные присваивания простым переменным Кратное присваивание простым переменным имеет вид (y.z. i) х1У х2у ..., хп '.= е1У е2У ..., еп где х(—различные простые переменные, a eL— выражения. В целях упрощения изложения присваивание сокращается дох := еу т.е. любой идентификатор с черточкой наверху представляет целый вектор (соответствующей длины). Команда кратного присваивания выполняется следующим образом. Сначала вычисляются в любом порядке все выражения. Пусть они дали значения vly. . ., vn. Затем хх присваивается vu х2 — v2i. . ., хп — vn именно в этом порядке. (Поскольку xt различны, порядок присваиваний неважен. Однако для целей дальнейших обобщений присваивание требуется осуществлять слева направо.) Кратные присваивания полезны, так как ими легко описываются изменения состояния, затрагивающие более чем одну переменную. Их формальное определение — простое расширение определения присваивания одной переменной. (9.2.2) Определение. wp("x := е"', R) = domain(e) cand Щ. □ Здесь domain(e) описывает множество состояний, в которых могут быть вычислены значения всех выражений из вектора е\ domain(e) = (у i: domain^;)) Пример 1. Присваивание ху у := уу х можно использовать для обмена значений переменных х и у □ Пример 2. Присваивание х, у, г := у, г, х совершает «циклическую перестановку» значений х, у, z. □ Пример 3. wp("zy у := г*х, у—1", y^0/\z*xy = c) = {у — 1 > Од(z *х) *ху~х = с) = {y^\/\z*xy = c) □ Пример 4. wp("s, i := s + b[/], i+l", *>0As= =(S/=°</<'^[/]) = « + 1>0Л& + &[*] = (2/:0</<*+1:6[/]) = i^0As=(V]/:0</<i:b[/]) Отметим, что выполнение присваивания не изменяет соотношение s = (N] /:0</ < i:b[j]). П Пример 5. wp("x, у := х—у, у—х", х-\-у = с) = {х—у + у—х = с) = (0 = с) D
Гл. 9. Команда присваивания 127 Пример 6. wp("x, у := х—у, х + у'\ х + у = с) = (х—У + х+У = с) = (2*х = с) □ Использовать определение команды присваивания сначала трудно, поскольку нам мешает наша старая привычка — рассуждать о присваиваниях в терминах выполнения. Необходимо сознательно заставлять себя использовать это определение. Как ни удивительно, со временем это действительно помогает. Приведем пример, иллюстрирующий сказанное. Пусть имеются массив Ъ и переменные i, m, /?, для которых i^m<i+p. Значения i и i+p—l определяют границы сегмента Ь li : i+p—1] массива 6, а т, как показывает условие, есть индекс в этом сегменте. Желательно уменьшить размер сегмента за счет изменения i на т+1, но в то же самое время р должно быть изменено таким образом, чтобы i+p—l по-прежнему указывало правую границу сегмента, которая не должна измениться, как видно из второго из приведенных ниже предикатов: Л i <щ <i+p Какое значение должно быть присвоено р? Вместо того чтобы определять его каким-то придуманным специально для этой цели приемом, воспользуемся определением wp. Полагая с равным начальному значению с+р, мы хотим найти выражение х9 которое сделало бы истинным следующий предикат: {i+p=c} i9 p := m+1, x {i+p=c} Имеем wp("i, р := m+1, x", i + p = c) = (i + p = c)k$i,x = (m+l+x) = c Так как вначале i+p=c, заменяем с на i+p и получаем m+\+x=i+p Решая это уравнение относительно переменной х, получаем х= p+i—m—1, так что искомое присваивание есть f, p ; = m+l9 p+i— т—1. Определение wp использовалось не только для того, чтобы показать, что присваивание правильно, но и для построения присваи- ч ь\ i m т i i+P ' i+P
128 Часть 11. Семантика простого языка программирования вания. Это — первое указание на то, что wp полезно при построении программ. Замечание. Рассмотрим поиск решения утверждения (9.2.3) {Т} а := а+1; Ь := х {а=Ь} относительно х. Слепое следование определениям приводит нас к wp("a := "а+1; Ь := х"9 а = Ь) = wp("a := а+1", а = х) =а+1=х что, безусловно, неправильно. Нельзя получить тавтологию, подставив в (9.2.3) а+1 вместо х. Проблема разрешается тем, что х должен рассматриваться как функция а и Ь *К Если х записать в виде х(а, Ь), получаем wp(ua := а+ 1; b := х(а, &)", а = Ь) = wp("a : = а+1", а = х(а, Ь)) = а + 1 = х(а + 1, Ь) Таким образом, видно, что х не зависит от Ьу и в качестве х может быть взято выражение а, что и является очевидным ответом. □ Упражнения к разд. 9.2 1. Докажите, что х, у := el, е2 семантически эквивалентно "х := el, у := е2", а также "у := е2\ х :=« еГ\ при условии что х не встречается в е2, а у не встречается в el. 2. Покажите при помощи контрпримера, что если х встречается в el или у в el, то их, у := el, е2", "х := el\ у := е2" и "у := е2\ х := el" в общем случае не эквивалентны. 3. Определите и упростите wp (S, R) для приведенных ниже пар (S, R): S R (а) (Ь) (с) (d) (е) (0 (g) z,x,y'-= 1, с, d i,s\- 1, b[0] a, n ■■- 0, 1 i,s'-= i + \, s+b[i] /:= i+l; j-=j+i j:= j+i; i:= i+l i,j'-=i+l,j+i z*xy-cd \<i<n As=b[0]+...+b[i-l] a2<n A(a+l)2^n 0<i<n As=b[0]+...+b[i-l] i=j i=j i=j 4. В каждом из следующих предикатов х—это неизвестное выражение, которое нужно определить, т.е. должно быть найдено такое выражение для х, включающее другие переменные, чтобы утверждение было тавтологией. Сде- *> Чтобы найти здесь х, найдите wp("c :— х\ а := а+1; Ь \— с", а = Ь). Это легче, чем пытаться записать х как функцию всех переменных, и дает красивый и простой технический прием22).
Г л, 9. Команда присваивания 129 лайте это так же, как это делалось в примере, предшествующем упражнениям. Первые несколько упражнений просты, так что вы легко ознакомитесь с техникой решения. (a) {Т} а,Ь:=а+\,х [Ь =а+\] (b) {Т} а- а+\\ 6:= * {Ь =а + \] (c) {Т} 6:= jc; a:=a+l {b =а+\} (d) U=j] ij'= i + \,x [i=j] (e) {i=j} /:= i+l; y":= дг {/ =j] (0 {/ =у}у:= дг; *: = «+1 {/=y} (g) {z +д*£ =с} z,a:= z+6,x {z + я*6 =,c} (h) {evefi(fl)A z + я*6 =c} a,b:=a / 2,x {z + a*b =c] (i) {*»уел(я) л z +я*/> = cj flf.*= fir / 2; fe:= x {z +a*b ~c] G) {Г}м:=0,х {5=(Zy:0<y^/:6[/-])} (k) {T)i,s:=0,x ls=(lj:0^j<i:bU])} (1) {i>0Aj=(Zj/;0<./</:6[i])}/15:=/+1,x {s=(lj:Q*ij <i:b[j])} 9.3. Присваивание элементу массива Напомним (разд. 5.1), что с функциональной точки зрения на массивы массив b является простой переменной, которая содержит в указателе значения функцию, а обычная «индекция элемента массива» b[i] является просто применением функции, находящейся в данный момент в 6, к аргументу L Напомним также, что через (Ь; i х е) обозначается функция, отличающаяся от b лишь тем, что при аргументе / она принимает значение е. Поэтому можно рассматривать присваивание значения переменной с индексами b [i] i=e как эквивалент присваивания' (9.3.1) Ь:= (6; i i е) так как оба они изменяют b таким образом, чтобы получилась (Ь\ i: ё). Но (9.3.1) — это присваивание простой переменной. Так как присваивание простой переменной уже определено в (9.1.1), то определено и присваивание переменной с индексами! Используя определение (9.1.1), получаем wp("b[i] != е"9 R) = wp("b := (Ь; i\e)\ R) =*domain((b\ i:e)) cand Rb(b;ce) Определение b li] можно переписать, используя предикат inrange (b, i), означающий, что значение i находится в области значений индексов массива b (т. е. является правильным индексом для Ь). (9.3.2) Определение. wp("b[i] := е"9 R) = inrange(b, i) canddomain(e) cand Rbb; he) П 4 5 д. грио
130 Часть II. Семантика простого языка программирования Обычно стремятся опускать inrange и domain, записывая определение просто как (9.3.3) wp("b[i] := ё\ R) = Rbib;i:e) Замечание. Обозначение ф\ i:e) используется при определении присваивания элементов массива и при рассуждениях о программах, но не в программах. Из традиционных соображений команда присваивания по-прежнему записывается в виде "b[i] := e". Q Мы позаботились о том, чтобы определение присваивания элементу массива выглядело довольно просто, и, на самом деле, определение wp("b[i] := e", R) является чисто механической работой. Однако, делая простым определение, мы добавили сложностей в исчисление предикатов: предусловие не всегда легко преобразовать в понятную форму. Следующие примеры показывают, как это можно сделать. В примерах предполагается, что все индексы находятся внутри границ. Пример 1. wp("b\i] := 5й, ВД = 5) = (b[i] = 5)(ь; i: <>) (по определению) = ((Ь; r.5)[5]s=5) (подстановка) = (5 = 5) Следовательно, выполнение b[i] := 5 всегда придает b[i] значение 5. Q Пример 2. wp("b[i] := 5", &[i] = &[/]) =z(b[i]=xb[j])(b;i:b) (по определению) see (ф\ i:5) [t] = ф\ i:5) [/]) (подстановка) = (/^= уд5 =/;[/])V0* = /Л5=5) (разбор случаев -(i^=/A5-ft[/])V0'-/) t^/V/ = /) **(i¥= /V^==/)A(s = ^L/lV^ = /) (дистрибутивность) -=rA(6-4/]Vi-/)-(f-/)V(4/]-6) Часто, если мы руководствуемся только интуитивными соображениями, из-за невнимательности опускается случай i=j. Здесь формальное определение действительно помогает. Произведенный здесь разбор случаев был объяснен в конце разд. 5.1, так что перечитайте его, если разбор случаев вызывает у вас затруднения. □ Пример 3. wpCb[b[t]] := Г, &[*] —О **№"] l=== tfib;b[i)U) (по определению) = (Ь; b[i]ii)[i] = i (подстановка) ** (ВД Ф ^ЛЭДА! = 0 V (b[i] = i Ai — i) (разбор случаев) =*F\J(b[i] = iAT)~b[i]=*i
Г л, 9. Команда присваивания 131 Следовательно, выполнение b lb [i]] ■=/ не влияет на предикат bli]=i. Это упражнение довольно сложно выполнить, используя лишь операционную семантику. □ Пример 4. Предположим, что п> 1. Пусть ordered (b [1 : п\) означает, что элементы массива b расположены в порядке возрастания. Тогда wp("b[n] := х", ordered(b[l:п])) = (ordered(b[ 1: п]))Ьф; п: х) (по определен ию) = (ordered((b\ nix) [1 in]) (подстановка) = (ordered(b[\ :п—l])/\b[n—П ^х (определение предиката ordered) Заменяя ordered(b[l:n]) на его определение, получим более формальный вывод: wp("b[n] := х\ (yt:l<t <n:fr[t]<ft[t + l])) = (yf: 1 ^/ < n:(b\ n:x)[i]^.(b\ n:x)[i-\-l]) = (&; п\х)[п — 1J< (b\ n:x)[n]/\ (y*:l<* <n— l:(b\ n:x)[i]^(b\ n:x)[i+l]) = b\n— 1] < x[\(^i: 1 < i < n— 1 :b[i] < b[i + 1]) Q Упражнения к разд. 9.3 1. Определите и упростите следующие слабейшие предусловия. Массив Ь описан как Ь[0:п—1]; известно, что все индексы находятся в границах (a) ир("6[/]:=|", ft[ft[i]]:=i) (b) wp("b[i]:= 5", (ay:i^7</i:ft[i]<*[/])) (c) wp("ft[/]:= 5", (ЗУ: / «Sy <я: b[i] <b[j])) (d) wp("6[i]:= 5", 6[0:л-1] = Я[0:л-1]) (e) wp("6[i]:= 6[i-I] +6[i]", 6[/] =(Iy: 1 О <i": *[/'])) (0 wp("/:= 6[i]; A[i]:= &[/]; W= <", *[/] = * л ft[/]=^) (g) wp("f.= Ы0; b[i}:= b[j); b[j]:= t'\ k*i.*k Ф} л b[k] = C) ) 2. Выведите определение присваивания r.s := e, где г—запись на языке, подобном Паскалю, a s — имя поля (см. упр. 4 из разд. 5.1) 9.4. Кратное присваивание общего вида Этот раздел можно опустить, так как он не нужен для понимания способа построения программ, описываемого в ч. III. Материал раздела существенно используется при определении вызовов процедур в гл. 12. Мы уже определили присваивание х := е простой переменной х, присваивание х := е вектору различных простых переменных Xj и присваивание b[i] := e элементу массива b[i\ Теперь **
132 Чаешь II. Семантика простого языка программирования мы хотим определить команду кратного присваивания общего вида. Такая команда, например, позволяет обменивать значения двух элементов массива: ь[Ц b[j]i=b[j], b[i] Кроме того, она позволит выполнять присваивания элементам подмассивов. Например, до сих пор не было определено присваивание c[i][/] s= e для массива су описанного как var с -.array [0:10] of array [0:10] of integer Вспомним (из разд. 5.3), что селектор — это последовательность выражений, заключенных в квадратные скобки (т. е. индексов). Нулевой селектор обозначается через 8. Для любого идентификатора х имеем х = хоеу где о обозначает соединение идентификаторов и селекторов. Команда кратного присваивания имеет вид (9.4.1) x^Si, ..., xnosn := еъ ..., еп где каждое xt—идентификатор, каждое st—селектор и каждое выражение et имеет тот же тип, что и j^.os/. Обозначая xtosh ... ..., xnosn через xos> a eit ..., еп через ё, сократим кратное присваивание до (9.4.2) A^s:="i Заметим, что и простое присваивание х := е имеет вид (9.4.1), где ft=l, a sf. = e, так как оно совпадает с хог := е. Присваивание b[i] := е имеет такой же вид, где п=1, хг = Ьу s1 = [i] И £! = £. Кратное присваивание может выполняться способом, согласованным с формальным определением, которое приводится ниже. (9.4.3) Выполнение кратного присваивания. Прежде всего определим переменные, задаваемые Xiosh и вычислим значения выражений eh получив значения vt. Затем присвоим x1osi значение vl9 x2os2—значение v2, ..., xnosn—значение vn. Порядок присваиваний обязательно должен быть слева направо. □ Определим кратное присваивание, дав его преобразователь предикатов. Чтобы понять идею определения этого преобразователя, рассмотрим определение кратного присваивания простым переменным: wp("x := ?\ R) = rI Нам нужно быть уверенными в том, что определение кратного присваивания общего вида включает это определение как частный случай. Поэтому обобщим более простое определение (9.2.2)
Гл. 9. Команда присваивания 133 на случай идентификаторов, соединенных с селекторами: (9.4.4) Определение. wp("~xo~~s := ё", R) = R-S □ Сложность здесь состоит в том, что подстановка определена лишь для идентификаторов, и, таким образом, Rxls пока что не е определено. Обобщим понятие подстановки с целью включить этот новый случай. Обобщение производится путем описания того, как небольшими преобразованиями привести Ri.oS к форме обычной подстановки. Обобщение делается так, что способ выполнения, который дан в (9.4.3), включая и порядок присваиваний слева направо, будет согласовываться с определением (9.4.4). Для обоснования обобщения рассмотрим присваивание (9.4.5) ftoslf ..., bosm:= е1У ..., ет Это присваивание сначала присваивает Ь о si значение eiy затем Ь о s2 значение е2 и т. д. Таким образом, оно должно быть эквивалентно (9.4.6) b:= (b; s^e,; ...; sm:em) Почему? Пусть два селектора, например si и sJy где i < /, совпадают. Тогда после выполнения (9.4.5) в b о s;- будет значение е;- (а не et)y и после этого ссылка b о sy- должна будет выдавать еу. Именно это и происходит при выполнении (9.4.5). Порядок присваиваний слева направо, который имеет место при выполнении (9.4.5), отражается правилом старшинства, согласно которому приоритет имеет самая левая ; . Далее заметим, что для различных идентификаторов b и с и селекторов s и t (не обязательно различных) присваивания bos, со/ := e,g и со/, bs := g,e должны давать один и тот же результат. Это происходит из-за того, что bos и cot ссылаются на разные области памяти машины, и то, что присвоено одному из них, не может воздействовать на то, что присваивается другому. (Напомним, что выражения е и g вычисляются до того, как выполняется какое-либо из присваиваний.) Все это приводит к следующему определению: (9.4.7) Определение. R-, где каждый элемент вектора х—идентификатор, соединенный с селектором, задается следующими тремя правилами: (а) Если х—список различных идентификаторов (таким образом, если все селекторы в х нулевые), 7?! означает обычную подстановку.
134 Часть II. Семантика простого языка программирования (b) Если b и с—различные идентификаторы, то Г)*ТЬ° S, С W. /у qX, Cot, bo St У 7, f, z,~h 7, g, f, л Это правило означает, что смежные ссылочные выражения можно переставлять при условии, что они начинаются с различных идентификаторов. (c) Если ни одно из ссылочных выражений xt не начинается с идентификаторов Ьу то nfcos,, . . .. bosm,~x __ nb, x~ « ет* F (b: sj: ex\...; sm: ет),ё Это правило показывает, как кратные присваивания частям объекта Ь могут рассматриваться в качестве простого присваивания Ь. Пример 1. к»р("*. х := 1, 2", #) = &7?("*oe, *08 := *» ^"» Я) пл; — ^ (х; е: 1, е: 2) = /?£ (см. определение (5.3.2)) Выполнение дс, л: : = 1, 2 эквивалентно выполнению л; := 2, на самом деле нет никакого смысла использовать х, х :== 1,2. Пример 2. ^рСЧаьы- ьщьиг, b[i]=xAb{j] = Y) = (b; i:b\J];j:b[i])[i] = X a (b; i:b\jlj:b[i])[j] = r = ((/=yA6[/] = Jf) v(/^yA6[/]=Jf)) A b[i]=Y = ((i=j A6[/] = Jf) v (i#yAfc[/]=jf)) a 6[i]=y = &[/] = * Л*['] = Г Отметим, что перестановка производится правильно и при * = /, так как этот случай автоматически был охвачен предыдущим выводом. Если эти преобразования показались вам слишком сжатыми, перечитайте разд. 5.1. □ Пример 3. ^РСЧП,ЬУ]:= bUlb[iY\ (Yk:k^i Ak^j:b[k] = B[k])) = (Vk:k*iAk*j:(b; i:b[/]; j:b[i])[k] = B[k]) = (Vk:k& *k*j: b[k] = B[k]) Последняя строчка следует из предыдущих потому, что кфг и кФ\ влечет (Ь; г.Ь[/]; j-b[i])[k] = b[k]. Единственные значения, изменяемые в массиве перестановкой, это b[i] и b[j]. р
Гл. 9. Команда присваивания 135 Упражнения к разд. 9.4 1. Преобразуйте следующие выражения к форме обычной подстановки, пользуясь определением (9.4.7) (т. е. так, чтобы верхние индексы были в каждом из них списком различных идентификаторов): (a) Reblp;j?ulx (b) Я%У}\х;ьт (c) RWfal'lbVl (d) яЩ[\ьшт 2. Определите и упростите следующие слабейшие предусловия (здесь b—массив целых чисел и предполагается, что все индексы находятся в его границах): (a) wp("6[i],*[2]:=3,4", ft[i]=3) (b) И5р("&[1Ш2]:=4,4", b[i]=3) (c) wpC'p, b[p]:= Ь[р],р", р = Ь[р}> л (d) wp(-i,b[i]:=i+l,0"t 0<i *0t.j:0<j<i:b\j]=0)) (e) wp("i, b[i]:= i+\, 0", 0<i л 6[0:i-l]=0) (f) wp("p, b[plblq]:= b[plb[q],p", p =b[q]) (g) wpQ'p, b[p],b[b[p]]:= b[plb[b[p]lp", p =b[b[p]]) (h) wpi'p, b[p],b[b[p]].= b[plb[b[p]lp»,p*b[b[p]]) 3. Докажите следующую импликацию: i = lAb[i\=Kz=>wp(ut, b[i] := b[i], ?>, i=K/\b[I] = l) 4. Дайте определение кратного присваивания общего вида, которое может включать присваивания простым переменным, элементам массивов и полям записей языка Паскаль (см. упр. 4 из разд. 5,3). 5. Докажите, что для расширенного определения подстановки имеет место лемма (4.6.3). Лемма. Пусть каждое х£ из списка х имеет форму идентификатор о селектор. Пусть и—список новых различных идентификаторов. Тогда [ЕХ_\Ч. = Е. □
Глава 10 КОМАНДА ВЫБОРА Системы понятий языков программирования обычно включают условную команду, или оператор if, позволяющую варьировать исполнение своих подкоманд в зависимости от текущего состояния программных переменных. Например, в Алголе-60 и Паскале условный оператор имеет вид if x^O then z := х else z := —х При выполнении этой команды в г записывается абсолютная вели* чина х: если z^O, то выполняется первая альтернатива z := x, в противном случае выполняется вторая альтернатива z : = —х. В наших обозначениях эту команду можно записать следующим образом: * (10.1) if x^0-+z := x Q *<0-+г := —х fi или же, так как она достаточно коротка и проста, в одну строку: if *>0-*г : = xQ*<;0 —г : = —x fi В команде (10.1) содержатся две составляющие вида B->S (разделенные символом []), где В — логическое выражение, a S — команда. Команда Б-kS называется охраняемой командой, так как В служит охраной входа -*, обеспечивая то, что S выполняется лишь при выполнении условия В. Чтобы выполнить команду (10.1), нужно найти истинную охрану и выполнить команду, ей соответствующую. Таким образом, при л:>0 выполняется присваивание z: = х при х<.0— присваивание z : = —х, а при л:=0 выполняется любое (но не оба сразу) из присваиваний. В этом кратком введении был затушеван ряд важных тонкостей. Постараемся сейчас быть более точными при описании синтаксиса и выполнения команды выбора. Общий вид команды выбора: (10.2) if Bi-^St U b2-+su fi
Гл. 10. Команда выбора 137 где п^О и все B^St — охраняемые команды. Каждое из St может быть произвольной командой — skip, abort, последовательным соединением команд, присваиванием, другой командой выбора и т. д. Для сокращения записи общий вид команды (10.2) будем обозначать через IF, а через ВВ будем обозначать дизъюнкцию вгув2 v... \/вп. Выполнить команду IF можно следующим образом. Сначала проверяем Bt. Если некоторая из охран Bt не определена в состоянии, в котором начинается выполнение, то может произойти аварийное завершение программы (как мы говорим, может произойти авост — аварийный останов). Это происходит из-за того, что на порядок вычисления значений охран никаких ограничений не накладывается. Далее должна оказаться истинной по крайней мере одна из охран, в противном случае выполнение завершается авос- том. И наконец, если по крайней мере одна из охран истинна, то выбирается одна из охраняемых команд Bi-+Siy у которой охрана Bt истинна, и выполняется S*. Теперь определение wp (IF, R) почти очевидно. Первый конъюнктивный член указывает на то, что все охраны должны быть определены, второй — на то, что по крайней мере одна охрана истинна. Остальные конъюнктивные члены указывают на то, что выполнение любой команды Siy для которой охрана Bt истинна, заканчивается при истинном R. (10.3а) Определение. wp(\F> /?) = domain(ВВ)ДВВЛ (B1=*wp(S1,R)A...ABf*wp(Sn,R)) Q Обычно предполагается, что охраны — всюду определенные функции, т. е. значение их определено во всех состояниях. Это предположение позволяет упростить определение, опустив первый конъюнктивный член. Таким образом, определение может быть при помощи кванторов преобразовано в форму (10.ЗЬ), приведенную ниже. С этого момента в качестве определения будет использоваться (10.ЗЬ). Однако перед его применением удостоверьтесь, что охраны определены в любом состоянии, в котором будет выполняться команда выбора! (10.ЗЬ) Определение. wp(\F, /?) = (gr. 1 <^<Аг:В/)Д (Vi:l^i^n:B;=>wp(Sh R)) Q Пример t. Покажем, что при любых начальных условиях выполнение (10.1) помещает в z абсолютную величину х, т. е. что
138 Часть II. Семантика простого языка программирования а;/? ((10.1), 2*=*abs(x)) = T. Имеем wp((№.l)9z=abs(x)) = (*>0V*«))A ГВВА (x>0*>wp("z:=x'\z=abs(x)))* < Я, ^иу?^,, Я)Л (x ^0^>wp("z:= -*", z = abs(x))) {B2^>wp(S2,R) = T л (х >0^>x =abs(x)) л (x*£03>-x=abs(x)) = ТЛТАТ = Г D Пример 2. Предполагается, что следующая команда будет телом цикла, подсчитывающего число р положительных значений в массиве b [0:m—1]: (10.4) if b[i]>0-+p, i := /7+1, f+1 П b[i]<0-+i := *+l fi Ожидается, что после выполнения этой команды справедливо условие i^.m> a p равно числу больших нуля значений в b[0:i—1J. Взяв в качестве R утверждение '<mA/? = (N/:0</<*:b[/]>0) вычислим ну((10.4), R) = (b[i]>0 v 6[/]<0) л (b[i]>0 =$> wp("pj:= p + \j + V\ Л)) Л (6[/]<0 =^>и'/>("/:= / + Г, /?)) = Ь[/]^0л (J[i]>0»i4Kwi лр+1=(Му:0<у </ + 1:6[/]>0))Л (6[/]<0=W + l<m *p=(\j:6^j<i + l:b\j]>0)) - 6[/]^0л /<т л /?=(МУ:0^у </:А[/]>0)л р=(1МУ:0<у</:6[/]>0) = 6[/]^0л /<т лр = Ш:0<у </:2>[/]>0) Итак, мы видим, что массив b не должен содержать значение 0, а определение р как числа значений, больших нуля, в массиве МО : i—1] будет правильным после выполнения команды выбора в том случае, если оно было таким до выполнения. Q У читателя может сложиться мнение, что для доказательства того, что было сделано в примере 2, потрачено слишком много усилий. В конце концов, этот результат можно было получить, руководствуясь интуитивными соображениями и, возможно, довольно легко (хотя при этом, вероятно можно проглядеть вопрос о нулевых элементах в массиве Ь). Но сейчас важно научиться проделывать
Гл. 10. Команда выбора 139 подобные формальные преобразования. Это приведет к лучшему пониманию теории и самой команды выбора. Более того, преобразования того же рода, что и выполнявшиеся в примере 2, будут совершенно необходимы при построении некоторых программ, а требуемые для этого навыки могут быть приобретены лишь в практической работе. Даже выполнение нескольких упражнений будет способствовать изменению привычных для вас способов обдумывания программ и тем самым того, что называют интуицией программиста. Позже столкнувшись с задачей, похожей на разобранную выше, вы, возможно, сумеете обойтись без такого формального подхода, но эти формальные навыки будут у вас наготове, когда вы встретитесь с более трудной задачей. Некоторые замечания по поводу команды выбора Команда выбора в нескольких отношениях отличается от обычного оператора if. Обсудим причины этих отличий. Прежде всего в команде выбора допускается любое число альтернатив: не обязательно именно две. Таким образом, она может служить также и в качестве «оператора выбора» (Паскаль) или «оператора SELECT» (ПЛ/1). Нет нужды иметь два различных обозначения: одно для двух альтернатив, а другое для большего их числа. Одно обозначение для каждого понятия (в данном случае для выбора) является хорошо известным и разумным принципом. У нас нет умолчаний: перед каждой из альтернативных команд должна стоять охрана, которая описывает условия, при которых эта команда может выполняться. Например, команда для присваивания х значения абсолютной величины х должна записываться при помощи двух охраняемых команд* ifx>0—► skip^x^O—^x := —х fi Ее аналог в Алголе, if л;<0 then г • = —х, содержит соглашение по умолчанию о том, что при х^О выполнение этого оператора должно быть эквивалентно выполнению skip. Хотя программа и может стать немножко длиннее, в том, что умолчания отсутствуют, есть свои преимущества. Явное появление в тексте всех охран помогает читателю; кроме этого, каждая альтернатива представлена во всех деталях, и возможность упустить из виду какую-либо ситуацию уменьшается. И, что более важно, отсутствие умолчаний помогает при построении программы. Программист после того, как он начал строить возможную команду выбора, вынужден строить условия, при которых ее выполнение будет происходить удовлетворительным образом, и, более того, вынужден продолжать строить альтернативы до тех пор, пока хотя бы одна из них не будет истинной в любом возможном начальном состоянии. Это станет более ясным при чтении ч. III.
140 Часть //. Семантика простого языка программирования Отсутствие умолчаний вносит в пределах разумного возможность недетерминизма. Предположим, что, когда начинается выполнение команды (10.1), х=0. Тогда может выполняться любая из команд (но лишь одна из них), поскольку обе охраны х^О и х^О истинны. Выбор альтернативы целиком зависит от исполнителя. Например, альтернативу можно выбрать случайным образом, или же по нечетным дням выбирать первую альтернативу, а по четным — вторую, или же альтернативу можно выбирать так, чтобы минимизировать время выполнения. Главное здесь то, что, поскольку выполнение любой из альтернатив ведет к правильному результату, программист отнюдь не должен беспокоиться о том, какую именно из альтернатив выполнять. Он волен строить столько альтернатив и столько соответствующих им охран, сколько ему нужно, не обращая внимания на возможные перекрытия. Конечно же, из соображений эффективности программист может усиливать охраны, с тем чтобы исключить недетерминизм. Например, замена второй из охран в (10.1) на х<.0 может помочь в том случае, если вычисление значения операции унарного «—» требует затрат, поскольку при #=0 теперь может выполняться лишь первая команда z := х. И наконец, отсутствие умолчаний делает возможной симметричную запись (см. (10.1)), а это приятно — если даже не необходимо — для всякого математика. Теорема о команде выбора Довольно часто требуется не слабейшее предусловие команды выбора, а лишь проверка того, влечет ли его известное предусловие. Например, если команда выбора входит в программу, уже может быть известно, что ее предусловие — это постусловие предыдущей команды, и нет нужды в том, чтобы вычислять слабейшее предусловие. В таких случаях полезна следующая (10.5) Теорема. Рассмотрим команду IF. Пусть предикат Q удовлетворяет условиям 1) Q=>BB 2) QAB;=S>wp(Sh R) для всех i, l^i^in. Тогда (и только тогда) Q=$>wp(lF, R). □ Доказательство. Во-первых, покажем, как вынести Q из области действия квантора во второй из посылок теоремы. (У* : QABi=*wp(Si9 R)) = (V' :l(QABi)\/wp(Si9 R)) (импликация) в(У* : 1QV "]Bt\/wp(Si9 R)) (закон де Моргана) = "1QV(V'*: ~]BiVwP(Si> R)) (Q не зависит от i) =*Qz=>(yi:Bi=$>wp{Sh R)) (импликация, дважды)
Гл. 10. Команда выбора 141 Следовательно, имеем (Q^BB)A(W-QABi=Swp(Si, R)) (предположения (1), (2)) = (<3=ф ВВ) Л (Q => (V* • Bt => wp(Siy R)) (из приведенного выше) = Q=»(BBA(V"B/=»MS/. R)) = Q=^oyp(IF, R) (определение (10.3b)) Итак, конъюнкция предположений эквивалентна заключению, и теорема доказана. □ Пример 3. Предположим, что производится поиск методом деления пополам значения х, про которое известно, что оно имеется в массиве Ь [0 : п—1]. Мы находимся на таком этапе поиска, когда истинен следующий предикат Q: Q: ordered(b[0:n— 1])Д0<*<&</< nAx£b[i:j] Это значит, что поиск свелся к сегменту b[i:j] массива by a k — индекс в этом сегменте. Нужно доказать, что имеет место (10.6) {Q)\\b[k]^x-+i := k[]b[k]^x-+j := k fi{x^b[i:j]\ Первое предположение теоремы (10.5) Q=>BB выполняется, поскольку дизъюнкция охран (10.6) эквивалентна Т. Второе предположение выполняется, поскольку QAb[k]^x=*x£b[k:j] = wp("i := k\ x£b[i:j] и QAb[k]&*x=$>x€b[i:k] = M'7 := k\ x£b[i:j]) Эти две импликации следуют из того, что в Q указывается, что массив упорядочен и х находится в b [i : /], а также из вторых членов их посылок. Следовательно, по теореме (10.5) истинно (10.6).Q Упражнения к гл. 10 1. Определите wp("ii fi", R) для произвольного предиката R. Известна ли вам уже команда с таким определением? 2. Докажите, что если подкоманды команды IF удовлетворяют свойствам (7.3) и (7.4), сформулированным в гл. 7, то IF им удовлетворяет. 3. Следующая команда S3 используется в программе, ищущей частное и остаток от деления значения х на значение у. Вычислите и упростите wp(S3i q*w-\-r = xAr^0). S3: if w^r—► г, q := r—w, q+\[]w > r—> skip fi 4. Вычислите и упростите wp(S4, a > ОаЬ > 0) для команды S4: if a > b—+a := a—b[]b > a—+b := b — a fi
142 Часть 11. Семантика простого языка программирования 5. Вычислите и упростите wp(S5, х^у) для команды S5: if х > у—»► х, у :— у, х[]х^у—^s&fp fi 6. Массивы /[0:я]и g[0:m] являются списками имел людей, упорядоченными в алфавитном порядке. Известно, что по крайней мере одно имя содержится в обоих списках. Пусть X — первое (в алфавитном порядке) такое имя. Вычислите и упростите слабейшее предусловие для следующей команды выбора по отношению к предикату R, записанному после нее (предполагается, что i и j лежат в границах массива): &'• » fli]<g[i]-+i := 4-1 Р f[i]=g[j)-+skip fi {R: ordered (f [0 :n]) Л ordered (g[0:m])A/[*]<*Ag [/] <*} 7. Команда из следующего наброска доказательства могла бы быть использована некоторым алгоритмом для записи а*6 в переменную г. С помощью теоремы (10.5) докажите, что набросок доказательства правилен, {у > 0лг + у*х = а*Ь} if odd(x)—+z, x := г + у, x—\[]even(x)—+skip fi; уу х := 2*#, x -r-2 {#^=0az+*/** = £* ^} 8. Команда из следующего наброска доказательства могла бы быть использована в некотором алгоритме, который определяет максимальное значение т в массиве Ь[0:п—1]. Докажите с помощью теоремы (10.5), что этот набросок правилен. {0 < i < плт = тах (b[0:i — l])} if b [i] > m—+m : = b[i][\b [i] < m —> s£t> fi {0 < / < nAm = tnax(b[0:i])}
Глава 11 КОМАНДА ПОВТОРЕНИЯ Обычный цикл типа "пока" и команда повторения Цикл типа "пока1' имеет в Паскале вид "while В do S", а в ПЛ/1 довольно вычурную форму "DO WHILE (5); 5 END;" здесь В—булево выражение, a S—команда. Иногда S называют телом цикла. Используя оператор goto, можно выразить выполнение цикла типа "пока" следующим образом: loop:il В then begin 5; goto loop end но часто оно описывается при помощи блок-схемы вход В Т F 'вых S ЭД В наших обозначениях цикл типа "пока" имеет вид do B-+S od где B—+S—охраняемая команда. Эту запись можно обобщить до следующей, которую мы будем называть командой повторения и обозначать DO: do Bi-^St (li.i) ... □ вп-+ sn od Здесь я^О, а каждое из Bi—^Si — охраняемая команда. Обратите внимание на синтаксическое подобие DO и IF. Одна из них—это множество охраняемых команд, заключенных в скобки do и od, а другая—то же множество, заключенное в if и fi. Теперь опишем одним предложением, как может выполняться (11.1). Повторяйте (или итерируйте) следующее действие, пока это возможно: выбирайте охрану Ви которая истинна, и выполняйте соответствующую команду S^
144 Часть II. Семантика простого языка программирования После завершения DO все охраны ложны. Выбор истинной охраны и выполнение соответствующей команды называются выполнением шага цикла. Заметим, что допускается недетерминизм: если истинны две или больше охран, выбирается любая из них (но только одна) и выполняется соответствующая охрана. Такая ситуация может возникать на любом шаге. Отсюда видно, что DO эквивалентна do BB-+if Bi-+St n od где ВВ обозначает дизъюнкцию охран (гл. 10), или же do ВВ—► IF od где IF обозначает команду выбора с теми же охраняемыми командами, что и в (11.1). Это значит, что, если все охраны ложны, т. е. если ВВ ложно, выполнение цикла завершается. В противном случае выполняется соответствующая команда выбора IF, и процесс повторяется. Одна итерация цикла, следовательно, эквивалентна установлению, что ВВ истинно, и выполнению IF; Таким образом, мы могли бы обойтись лишь простым циклом типа «пока». Тем не менее будем продолжать пользоваться более общей формой цикла, поскольку она очень полезна при построении программ, в чем мы убедимся в ч. III. Формальное определение DO Следующий предикат Я0(/?) определяет множество состояний, в которых выполнение DO завершается за 0 шагов из-за того, что все охраны с самого начала ложны, причем после завершения истинно R: H0(R)=~\BBAR Напишем также предикат Hk(R) при &>0, выражающий множество всех состояний, в которых выполнение DO заканчивается за k или меньше шагов при истинном R. Определение будет рекурсивным. Предикат Hk(R) будет определяться через Hk_i(R). Первый случай, когда DO заканчивается за 0 шагов,— это тот случай, когда истинно H0(R). В другом случае происходит по крайней мере один шаг. Таким образом, ВВ вначале должно быть истинным, и шаг DO состоит в выполнении соответствующей команды IF. Это зыполнение IF заканчивается в состоянии, в котором до заверше-
Гл. 11. Команда повторения 145 ния цикла остается k—1 или меньше шагов. Подобные рассмотрения приводят к выражению Hk(R) = H0(R)\/wp(lF, Hk^(R)) для k>0 А теперь напомним, что wp(DO, R) должно выражать множество состояний, в которых выполнение DO завершается за конечное число шагов и при истинном /?, т. е. с самого начала в каждом таком состоянии должно быть такое k, что выполнение цикла потребует не более k шагов. Поэтому определим: (11.2) Определение.23) wp{X)0, R) = (zk:0<^k:Hk(R)) Q Два примера рассуждений о циклах Формальным определением DO не так уж легко пользоваться, а для целей построения программы оно не дает наводящих идей. Поэтому нужно сформулировать теорему, которая позволит работать с произвольным полезным предусловием цикла (по отношению к некоторому постусловию), не являющимся слабейшим предусловием. Сначала проиллюстрируем эту идею на двух примерах. Предполагается, что при выполнении следующего алгоритма в переменную s записывается сумма элементов массива Ь [О : 10] U s := 1, 6[0]; do i < 11 —>iy s : = i + l, s + b[7] od {#:s = (2&:0<&< U:b[k])\ Как доказать, что алгоритм^ работает правильно? Начнем с того, что зададим предикат Р, который выражает логические соотношения между переменными i, s и массивом Ь. На самом деле он служит определением i и s: Покажем, что если Р истинно перед каждым шагом цикла и после него, то оно также истинно в момент завершения цикла. Если же Р истинно во всех этих местах, то, учитывая ложность охран в момент завершения цикла, мы видим, что R также истинно в момент завершения (поскольку Р/\C^\\=>R). Подытожим то, что нужно показать, при помощи аннотирования алгоритма: {Т\ i, s := 1, Ь[0]; {Р\ (11.3) do i< 11-+ii< 11/\P\i, s := i+1, s + ЬШ \P) od {i>llAP\ № Поскольку это очень важно, повторим еще раз: если можно показать, что 1) Р истинно перед выполнением цикла и 2) каждый шаг цикла сохраняет истинность Р, то Р истинно перед выполнением
146 Часть //. Семантика простого языка программирования каждого шага цикла DO и после его выполнения, а также в момент завершения цикла. Следовательно, истинность Р и ложность охран позволяют заключить, что искомый результат R получен. Теперь проверим, что Р истинно после инициализации переменных i, s : = 1, МО] вне зависимости от того, каково начальное состояние. Это можно увидеть непосредственно или же доказать следующим образом: wp("i, s := 1, 6[0]'\ Р) «1<1<ПЛЬ[0] = (2*,-0<*< !:&[*]) -Г Теперь покажем, что шаг цикла завершается при истинном Р, т.е. что выполнение команды i, s := f+1, s+Ш], начинающееся при истинном Р и при Kill, завершается по-прежнему при истинном Р. Опять-таки это можно увидеть непосредственно или же доказать формально: wp(uit s := i + l, s + b[i]n, P) = 1 </+ 1 < 1! As + 6[0 = (Sfe:0<^ < i+l'-&[^]) = 0<i< ПЛ5*(2*:0<Л <i:b[*]) наконец, из РЛ*<И следует последняя строка, т. е. дор. Следовательно, известно, что если выполнение цикла завершается, то в момент завершения будут истинны Р и /^11, и, значит, R. Предикат Р, истинный перед выполнением и после выполнения каждого шага цикла, называется инвариантным отношением или просто инвариантом цикла. (Прилагательное инвариантный означает постоянный, или неизменяющийся. В математике термин «инвариантный» означает не изменяющийся под воздействием совокупности рассматриваемых математических операций. Здесь единственная операция — это выполнение шага цикла при условии истинности Р вначада.) Чтобы показать, что выполнение цикла завершается, введем целочисленную функцию /, определенную на значениях переменных программы. Эта функция является верхней границей числа шагов цикла, которые должны быть выполнены в дальнейшем. При каждом шаге цикла / уменьшается по крайней мере на 1, и до тех пор, пока выполнение цикла еще не завершилось, t ограничена снизу нулем. Следовательно, цикл должен завершиться. Пусть t 11—i. Так как при каждом шаге i увеличивается на 1, то /очевидным образом уменьшается на 1. Кроме того, до тех пор, пока еще есть шаги, которые нужно будет выполнять, т. е. пока /<С11, / больше 0. В данном случае t в точности указывает число шагов, которые осталось выполнить, но в общем оно может задавать лишь верхнюю границу этого числа. Функция t называется изменяющейся функцией
Гл. И. Команда повторения 147 в противоположность инвариантному отношению Р, так как функция изменяется на каждом шаге, а отношение остается неизменно истинным. Однако для того, чтобы подчеркнуть цель введения этой функции, она будет называться ограничивающей функцией. Может показаться, что для такого простого алгоритма, как в предыдущем примере, потребовалось слишком много объяснений. Теперь рассмотрим второй пример, правильность которого не так очевидна. Более того, мы сможем понять эту программу лишь при помощи инварианта. Алгоритм (11.4) предназначен для записи в переменную z значения а # b при 6^0, но без использования операции умножения. {Ь>0\ х, у, z := а, Ь, 0; (11.4) do у>0 Л even (у)-+у, х := #~2, х + х Q odd (у) ~+у, z := у— 1, z + x od \R:z — a*b\ Действие этого цикла можно рассматривать как обработку двоичного представления величины Ьу первоначально записанного в у. Проверка на четность — нечетность производится при помощи анализа последнего бита ячейки у\ вычитание 1 в случае, если число нечетно, т. е. последний бит равен 1, означает установку последнего бита в 0; деление на 2 производится путем сдвига двоичного представления на 1 бит вправо, при котором удаляется последний бит. Но как мы можем убедиться, что алгоритм действительно работает? Введем, как говорится «с потолка», следующий инвариант Р (как искать инварианты, будет одной из тем третьей части книги).» Р:у*^0/\г+х*у = а*Ь Установим, что Р истинен после инициализации переменных: wp("x, у, z := а, Ь, 0", Р) = &>0л0 + а*6 = а*6 а это предусловие, очевидно, следует из предусловия алгоритма (11.4). Далее покажем, что любой шаг цикла, начавшийся при истинном Я, закончится также при истинном Я, так что Р является инвариантом цикла. Для второй из охраняемых команд это можно усмотреть всего лишь из следующих соображений: значение z+ х*у остается тем же самым, если у уменьшается на 1, а л; прибавляется к г: z+x*y=z+x+x*(y—1). Для первой из охраняемых команд заметим, что выполнение у, х := у-г-2, х+х при четном у оставляет неизменным значение z+x*y, поскольку при четном у х*у=(х+х)*(у-±-2). Более формальную проверку инвариантности оставляем читателю в качестве упр. 7. Так как каждый шаг цикла сохраняет истинность Р, то Р должно быть истинно и в момеш завершения цикла. Покажем, что Р
148 Часть 11. Семантика простого языка программирования вместе с ложностью охран следующим образом влечет результат R: РЛ1(У>0Л even(y))Л 1 odd(y) = У>0Лг + х*У = а*ЬА(у^0Л even(y)) Проделанная к настоящему моменту работа сводится воедино следующей аннотированной программой: {Ь>0} х, у, z := а, Ь, 0; \Р\ do у > 0/\ even(y) —>{РАу > 0Aeven(y)\y, х := у 4-2, х + х{Р\ (11.5) Q odd(y) -+{PAodd(y)}y, z := у—I, г + х{Р\ od {PAy<0A~\odd(y)\ {РАУ = 0\ {R:z = a*b\ Чтобы показать, что цикл завершается, используется ограничивающая функция t=y: она больше 0, если еще есть шаги, которые нужно выполнить, и уменьшается по крайней мере на 1 на каждом шаге. Теорема о цикле, его инварианте и ограничивающей функции В только что рассмотренных двух примерах для обоснования того, что цикл выполняется именно так, как требуется, использовался один и тот же способ рассуждений. Этот способ рассуждений оформлен в виде теоремы (11.6). Сейчас формулировка этой теоремы должна быть уже вполне ясна. Из посылки 1 следует, что Р будет истинно в момент завершения DO. Во второй посылке требуется, чтобы функция t была ограничена снизу нулем до тех пор, пока выполнение DO еще не завершилось. В третьей посылке требуется, чтобы t при каждом шаге уменьшалось по крайней мере на 1, так что завершение обязательно произойдет. При неограниченном числе шагов / уменьшилось бы ниже любой заранее установленной границы, что привело бы к противоречию с посылкой 2. И наконец, в момент завершения все охраны ложны, так что ~] ВВ истинно. (11.6) Теорема. Рассмотрим цикл DO. Предположим, что предикат Р удовлетворяет условию 1. Р/\В;=$>ы)р(8{у Р) для всех /, 1</<п. Далее пусть целочисленная функция t удовлетворяет следующим
Гл. 11. Команда повторения 149 условиям, где /1 новый идентификатор: 2. РЛВВ=Ф(/>0). 3. Pj\Bt±bwp{?t\ := /; S/\ /</1) при 1<*<я. Тогда P=>ay/?(DO, РД155). D Доказательство. Оставляем читателю в качестве упр. 2 доказательство того, что из посылки 1 следует Г. PABB=}wp(lF, P)9 а в качестве упр. 3 доказательство того, что из посылки 3 следует 3'. PABBAt<tO + l=$>wp(lF, *</0) для всех Ю. И наконец, оставляем читателю в качестве упр. 4 доказательство того, что при всех k^O (11.7) ЯЛ/<£=>#,(РЛ1ВВ). Предикат (11.7) интерпретируется следующим образом: в любом состоянии, в котором истинно Р и /<4, выполнение цикла завершается не более чем за k шагов при истинном Р и ложном ВВ. Так как /—функция, принимающая конечные значения, в любом состоянии истинно (g&: 0^.k:t ^k). Поэтому /> = РА(Я/г:0<Ы<£) = (3&:0 ^k:PAt ^k) (так как k не свободно в Р) =»(Н^0<*:ЯЛ(РЛ1ВВ))(11.7) = ayp(DO, РД1ВВ) (определение (11.2)) П Обсуждение теоремы У цикла имеется много инвариантов. Например, предикат ##0=0 является инвариантом любого цикла, так как он всегда истинен. Но инварианты, удовлетворяющие условиям теоремы (11.6), способствуют пониманию цикла и поэтому важны. Более того, каждый цикл, за исключением самих тривиальных, нужно аннотировать инвариантом, удовлетворяющим условиям теоремы. Инвариант, как мы увидим в третьей части книги, не только полезен для читателя, но почти необходим и программисту. Будут даны эвристики для построения инварианта и ограничивающей функции до построения самого цикла, при этом будет показано, что такой способ программирования более эффективен, чем обычный. Это станет понятнее, если мы будем рассматривать инвариант как простое определение переменных и всегда помнить о том, что необходимо точно определить смысл переменных перед тем, как их использовать. Но в данный момент, конечно, построение инварианта может казаться почти невозможным, так как даже сама идея ин-
150 Часть //. Семантика простого языка программирования варианта является новой. Отложим процесс построения инварианта до ч. III и сосредоточимся сейчас на понимании циклов, для которых инварианты уже даны. Аннотирование цикла и понимание аннотаций Алгоритмы (11.3) и (11.5) аннотированы для того, чтобы показать, когда и в каких точках истинны инварианты. Часто легче вместо того, чтобы писать инварианты в таком большом количестве точек программы, дать инвариант и ограничивающую функцию в тексте, сопровождающем алгоритм. Если же необходимо включить их в сам алгоритм, имеет смысл использовать сокращения, как показано в (11.8): № Unv P: инвариант} {boundt: ограничивающая функция} (11.8) do Bi-^Si Q... D Bn-+sn od Если имеется цикл, оформленный подобно (11.8), то, согласно теореме (11.6), для того чтобы понять, правилен ли он, достаточно лишь проверить пункты, перечисленные в (11.9). Существование такого списка условий, бесспорно, является преимуществом, так как это позволяет читателю быть уверенным в том, что но ничего не забыл проверить. На самом деле список условий полезен и самому программисту, хотя через некоторое время его использование входит в привычку. (11.9) Список условий для проверки цикла 1. Покажите, что Р истинно перед началом выполнения цикла. 2. Покажите, что {РД^Л^/]^} при всех 1<л^я, т. е. что выполнение каждой охраняемой команды завершается при истинном Р. Отсюда следует, что Р, несомненно, является инвариантом цикла. 3. Покажите, что РД1ВВ =ф#, т. е. что в момент завершения цикла истинен искомый результат. 4. Покажите, что Р/\ ~] ВВ=»(/ > 0), т. е. что t ограничена снизу до тех пор, пока цикл не завершился. 5. Покажите, что \P/\Bi\tl := <;S/{/</l} при всех 1^/^/г, т. е. что каждый шаг цикла приводит к уменьшению ограничивающей функции» □ Часто для документации цикла требуются лишь инвариант и ограничивающая функция, поскольку сам алгоритм тогда проверяется почти тривиально. Такая документация самая лучшая:
Гл. 11. Команда повторения 151 ее вполне достаточно для того, чтобы обеспечить необходимое понимание, и не слишком много для того, чтобы читатель затерялся в избыточных и очевидных частностях. Действуя в том же духе, мы можем иногда опускать части инвариантов, относящиеся к неизменяемым переменным цикла, при этом предполагается, что читатель заметит это. Например, в алгоритме (11.3) явно не отмечалось, что массив & остается неизменным (это можно было бы сделать путем введения в качестве конъюнктивного члена в предусловие, инвариант и постусловие предиката &=Б, где В представляет начальное значение Ь). Подобным же образом в алгоритме (5.4) явно не отмечалось, что а и Ь остаются неизменными. Упражнения к гл. 11 1. Определите wp(do od, /?), при произвольном предикате R. Известна ли вам уже команда с таким определением? 2. Докажите, что Г следует из посылки 1 (см. доказательство теоремы (11.6)). 3. Докажите, что 3' следует из посылки 3 (см. доказательство теоремы (11.6)). 4. Докажите индукцией по к что (11.7) следует из Г, 2, 3' (см. доказательство теоремы (11.6)). 5. Докажите, что свойства (7.3) и (7.4) выполнены для определения тр{Ьо, /?), если они выполнены для каждой из составляющих команд. 6. Hk(R) выражает множество таких состояний, в которых выполнение DO закончится не более чем через k шагов н при истинном R. Определите H'k (R)y выражающее множество состояний, в которых выполнение DO закончится в точности за k шагов. Какое множество состояний определяет предикат fakiO^kiH^R))} Как он отличается от wp(DO, R)? 7. Докажите формально все пункты списка условий (11.9) для алгоритма (11.4). 8. Докажите формально все пункты списка условий (11.9) для следующего алгоритма, который записывает в s сумму элементов массива Ь [1:10]: {Т} h s := 10, 0; {inv P: 0<i<10as = (2£:<+1<*< 10:4*])} {bound t:i) do i Ф 0—► it s := *'—1, s + b od {R:s = (S*:l<*< 10:&[*])} 9. Докажите формально все пункты списка условий (11.9) для следующего алгоритма (этот алгоритм находит место i значения х в массиве Ь[0:п—1], если х £ Ь[0:п — 1], и устанавливает i в п в противном случае): {0<я} i := 0; {inv P:0^i^n Ax(fcb[0:i~\]\ {bound tin—i) do i < n cand x Ф b [i] —► i := i +1 od {#:(0</< n A x = b[i])\/(i = n Л x$b[0:n— 1]) 10. Докажите 4°Рмально все пункты списка условий (11.9) для следующего алгоритма, придающего / наивысшую степень 2, не превосходящую п:
152 Часть //. Семантика простого языка программирования {0<п} i : = 1; {inv Р:0<1<лЛ (ЗР-* = 2р)} (bound t:n — i) do 2 * i: ^ n —► i i — 2 * i od {RiO </<я<2*/л (3P:l" = 2/')} И. Докажите формально все пункты списка условий (11.9) для следующего алгоритма, вычисляющего л-е число Фибоначчи fn при п > 0, определяемое рекуррентным соотношением /0 = 0, /i=l, fn — fn-i + fn-ъ ПРИ я > 1: {л>0} *', a, ft := 1, 1, 0; {inv P:\ <f<nA tf = /i Л £ = //~i} {bound tin — i) do i < л—> i, a, 6 := /+1, я + ^> # od {«:* = /»} 12. Докажите формально все пункты списка условий (11.9) для следующего алгоритма, вычисляющего частное q и остаток г от деления х на у: {^0Л0<1/} <7, г := 0, *; {ту Р:0<гдО< (/Л?* #+>* = *} {bound tir) do r^y—+r, q := r—#, (7+1 od {/?:0<r < г/Л<7**/+г = *} 13. Докажите формально все пункты списка условий (11.9) для следующего алгоритма, ищущего такое целое число /г, что b [k] является максимальным значением в массиве Ь[0:п—1] (заметим, что, если максимальное значение встречается в массиве более одного раза, алгоритм недетерминирован): {0<п} i\ k := 1, 0; {invPiO < i<n Л b[k]^b[0:i~I]} {bound tin—i} do i < n —► if b [i] ^b [k] —► skip U b[i]^b[k]—»k i = fi i := t+1 od {Я:М*]^Ь[0:п —1]}
Глава 12 ВЫЗОВ ПРОЦЕДУРЫ В этой главе дается определение команды вызова процедуры, которая запускает процедуру, описанную с использованием входных и выходных параметров. Доказываются две теоремы, которые приводятся для того, чтобы облегчить доказательство правильности вызовов процедур. И наконец, теоремы обобщаются на процедуры, включающие параметры, вызываемые по ссылке, или параметры- переменные. Эта глава существенно опирается на команду кратного присваивания, определенную в разд. 9.4. Материал данной главы не требуется для понимания ч. III (построение программы); при чтении эта глава может быть опущена. Ее назначение — проиллюстрировать, как вопросы правильности распространяются на более сложные конструкции, но ее материал, однако, не будет использоваться для формальных доказательств. Поскольку была предпринята попытка охватить несколько различных способов передачи параметров, в главе содержится довольно много теорем, рассматривающих вызовы процедур. Конечно при использовании любой конкретной системы понятий программирования, т. е. алгоритмического языка, число способов передачи параметров и соответственно применяемых теорем уменьшается. Чтобы изложить этот подробный материал по возможности про* сто, будем предполагать, что процедуры нерекурсивны. Процедура, или подпрограмма, как она называется в Фортране, является основным строительным блоком в программировании. (Слово подпрограмма (routine) использовалось уже в 1949 г. при программировании на EDSAC, которую принято считать первой построенной машиной с хранением программ в памяти. Эта машина была построена в Кембриджском университете группой, которой руководил Морис Уилкс, и первая программа была выполнена на ней в мае 1949 г.) Процедуры используются главным образом для целей абстракции. Под абстракцией понимается действие, состоящее в выборе для дальнейшего изучения или использования небольшого числа свойств объекта и изъятии из рассмотрения остальных свойств, которые нам в данный момент не нужны. Основное свойство, которое выделяется при написании процедуры, это то, что она делает. Главное свойство из тех, которые опускаются из рассмотрения,— как она это делает. В некотором смысле использование процедуры — в точности то же самое, что и использование любой другой операции (например, +) применяемой системы понятий программирования, а по-
154 Часть //. Семантика простого языка программирования строение процедуры — это расширение языка путем включения новой операции. Например, когда в выражении используется операция +, никогда не задается вопрос о том, как она выполняется; предполагается только, что она выполняется правильно. Подобным же образом, записывая вызов процедуры, мы полагаемся лишь на то, что она делает, а не на то, как она это делает. Другими словами, процедура (плюс ее доказательство) является леммой. Программу можно рассматривать как конструктивное доказательство того, что ее спецификация непротиворечива и вычислима; процедура — это лемма, используемая в таком конструктивном доказательстве. В следующих разделах для описаний процедур и их вызовов используются обозначения, подобные принятым в языке Паскаль, хотя (возможное) выполнение вызова процедуры может быть и не всегда таким же, как в Паскале. Причина в том, что главным фактором, оказывающим влияние на разработку понятия вызова процедуры в данной книге, была потребность получить простую и понятную теорему о его использовании, а в то время, когда создавался Паскаль, такой фактор был вне поля зрения его разработчиков. 12.1. Вызовы с входными и выходными параметрами Описание процедуры Описание процедуры имеет вид ргос <идентификатор> «спецификация параметрах ...; Спецификация параметра)); \Р\ <тело> {Q\ где каждая Спецификация параметра) имеет одну из трех форм: value <список идентификаторов):<тип> value result <список идентификаторов):<тип> result <список идентификаторов):<тип> Как обычно, <список идентификаторов)—это последовательность одного или больше идентификаторов, разделенных запятыми. У параметра процедуры имеется тип (например, Boolean, array of integer), определяющий тип, который может иметь соответствующий аргумент. Границы массива не рассматриваются как часть его типа; массив является просто функцией от одного целого аргумента. Выполнение вызова процедуры влечет за собой выполнение и <тела> процедуры. <Тело> может быть вообще любой командой: последовательностью команд, присваиванием, циклом и г. д. Оно может содержать подходящим образом описанные локальные
Гл. 12. Вызов процедуры 155 переменные. Во время выполнения <тела> процедуры ее параметры рассматриваются как локальные переменные <тела>. Начальные значения параметров и возможность использования их окончательных значений определяются атрибутами value и result, приданными параметрам в заголовке процедуры. Это будет подробнее разъяснено дальше. Предусловие Р и постусловие Q <тела> процедуры необходимы для понимания, а не для выполнения вызова процедуры. Предполагается, что {Р} <тело> {Q} уже доказано и что этот факт можно использовать при написании вызовов процедуры. Согласно общей тенденции при документации программ, целесообразно потребовать записи пред- и постусловия перед телом процедуры, как это показано ниже, поскольку тогда легче найти информацию, нужную для написания и понимания вызовов процедуры. Чтобы суметь написать вызов, достаточно понять лишь первые три строчки описания: Рге:Р\ Post:Q\ proc идентификатор) «спецификация параметра); ...); <тело) На использование идентификаторов в описании процедуры налагаются следующие ограничения. В теле процедуры могут использоваться лишь ее параметры и идентификаторы, описанные в самом теле, т. е. не допускаются «глобальные переменные». Параметры должны быть различными идентификаторами. В предусловие Р могут свободно входить лишь параметры с атрибутом value (и value result), в постусловие Q — лишь параметры с атрибутом result (и value result). Эти ограничения существенны для целей простого определения вызова процедуры, но не ограничивают ни в каком существенном отношении процедуры или их вызовы. Конечно, в Р и Q могут иметься свободные вхождения других идентификаторов, не используемых внутри тела процедуры, для обозначения начальных значений переменных и т. д. По поводу способа устранить это ограничение на идентификаторы см. разд. 12.4. Пример. Даны фиксированное х, фиксированное п > 0 и фиксированный массив Ь[0:п—1], где х£Ь. Следующая процедура определяет местонахождение х в Ъ и устанавливает x = b[i]: {Pre:n = NAx=bAb = BAX£B[0:N—l]\ {Post:0^i<NAB[i] = X\ proc search (value ny x:integer, value b:array of integer, result i:integer); i := 0; {инвариант: 0^/ < Nf\X £B[0u—1]} {ограничение: N — i) do b[/]фх—ti : = i+l od
156 Часть II. Семантика простого языка программирования Отметим, что для обозначения начальных значений параметров, не имеющих атрибута result, были использованы идентификаторы, несмотря на то что эти параметры не изменяются во время выполнения тела процедуры. □ В последующем будем предполагать, что процедура имеет следующий вид: (12.1.1) proc p (value x\ value result ~y\ result г); \p\B\q\ Таким образом, xt являются входными параметрами, yi — входными и выходными, a zi — выходными параметрами процедуры р. Типы параметров опущены, так как в данный момент времени они нам не нужны (это — пример использования абстракции). Вызов процедуры и его выполнение Нам нужно формально определить команду вызова процедуры, которая имеет вид (12.1.2) р(а, Ь, с) Имя процедуры — р\ аи bu ct являются аргументами *> процедуры. at — выражения; bt и ct имеют вид идентификатор о селектор, т. е. на общепринятом жаргоне называются переменными. at — входные аргументы, соответствующие х% из (12.1.1), bt — входные-выходные аргументы и с-г — выходные аргументы. Каждый аргумент должен иметь тот же тип, что и соответствующий параметр. Идентификаторы, доступные в точке вызова процедуры, должны отличаться от параметров процедуры, т. е. от х, у, г. Это ограничение не является существенным и позволяет избежать введения дополнительных обозначений, требующихся для работы в случае конфликта, возникающего при использовании одного и того же идентификатора для двух различных целей. Для иллюстрации дадим пример вызова процедуры search из предшествующего примера: search (50, /, с, position [/]). При выполнении этого вызова в position [/] записывается местонахождение значения t в массиве с [0 : 49]. Вызов р(ау Ьу с) выполняется следующим образом. Все параметры рассматриваются как локальные переменны^ процедуры. Во-первых, определяются значения аргументов а и b и записываются в соответствующие параметры х и у. Во-вторых, определяются переменные, описываемые выходными аргументами Ь и с— *> Для читателей, привыкших к алголовской терминологии, нужно помнить, что у автора «параметры» — это всегда формальные параметры процедуры, а «аргументы» — это всегда фактические параметры процедуры,— Прим. ред.
Гл. 12. Вызов процедуры 157 другими словами, их адреса в памяти. Заметим, что инициализируются все параметры с атрибутом value, а остальные—нет. В-третьих, выполняется тело процедуры. В-четвертых, значения выходных параметров у, г записываются в соответствующие выходные аргументы Ъ, с (по их уже определенным адресам). Запись происходит слева направо. Формальное определение вызова процедуры Из только что приведенного описания выполнения вызова видно, что выполнение вызова р(а, Ь, с) эквивалентно выполнению последовательности команд х, у := а, Ъ\ В\ Ьу ~с := у, 1 (Безразлично, вычисляются ли адреса 6, с перед выполнением тела процедуры или после него, поскольку выполнение В не может их изменить.) Определим (12.1.3) wpfpfc Ъ, cy\J> = _ wp("x,y := а, Ъ\ В\ Ь, с :«= у, z"tR) 12.2. Две теоремы о вызове процедуры Сформулируем теоремы, которые позволяют при написании вызовов процедур использовать процедурную абстракцию. Вначале сформулируем теорему и обоснуем ее правильность, основываясь на понятии выполнения вызова процедуры. (12.2.1) Теорема. Предположим, что процедура р описана так, как в (12.1.1). Тогда имеет место \Р11:РЦЛЫ~»> пфЫя11)\р(а, Ь, с) {R} У а, Ь \ * и, v и, v/ J ' Другими словами, PR=$>wp(p(a, b, с), /?). □ Доказательство. На время предположим, что известны значения и, vy которые будут присвоены параметрам с атрибутом result. Тогда и само по себе выполнение тела процедуры В может рассматриваться как кратное присваивание у, z := и, v. Из (12.1.2) видно, что вызов процедуры может рассматриваться как следующая последовательность команд: (12.2.2) *, у: = а, Ь\Р\; у, ~z := и, ^{Q}; b, с: = у, 1 \R\ Здесь постусловие вызова R помещено в самом конце, а утверждения Р и R — в соответствующих местах, так как мы намереваемся
158 Часть II. Семантика простого языка программирования использовать заключенную в них информацию именно в такой последовательности. Так как (12.2.2) является последовательностью присваиваний, то для каждого предиката легко определить такое слабейшее предусловие, при котором выполнение этой последовательности приведет к выполнению предикатов в том месте, где он стоит в (12.2.2). Заметим, что это предусловие — необходимое и достаточное условие выполнения предиката. Например, R имеет место после завершения вызова, если и только если (12.2.5) имеет место перед выполнением. (12.2.3) Слабейшее предусловие для P:Pf'£. (12.2.4) Слабейшее предусловие для Q: (Qi'lY i — Qtl (так как \ и, v J a, b «, v в Q не содержится х{ и у{). (12.2.5) Слабейшее предусловие для R:((^1)^1)11 = R^l \\ у, г J u,v/ а, Ь и, v (так как в R не содержится xt и у{). Чтобы получить возможность использовать то, что для тела процедуры доказано \Р\ B{Q\, потребуем, чтобы (12.2.3) было истинно перед выполнением вызова, это — первый конъюнктивный член предусловия PR доказываемой теоремы. Следовательно, вне зависимости от того, какие именно значения и и v будут присвоены при выполнении выходным параметрам, Q будет истинно в том месте, где оно находится в (12.2.2). Теперь требуется определить начальные условия, обеспечивающие истинность R в момент завершения вызова независимо от того, какие значения и и v присваиваются выходным параметрам и аргументам. R выполняется после вызова, если истинность Q в (12.2.2) при всех значениях и, v влечет истинность R после вызова, А это условие может быть переписано в терминах начальных значений следующим образом: (уи, у:(12.2.4)=ф(12.2.5)) Полученное утверждение является вторым конъюнктивным членом предусловия теоремы. □ Примеры использования теоремы (12.2.1) Читайте примеры внимательно, так как каждый из них иллюстрирует важную тонкость, возникающую при использовании теоремы. Во всех примерах тело процедуры опускается, так как корректность вызова перепроверять не требуется. Пример 1. Рассмотрим процедуру ргос swap (value result ijU y2\inieger)\ \P;yl = XAy2=Y}B{Q:yl = YAy2 = X\
Гл. 12. Вызов процедуры 159 Требуется доказать, что имеет место (12.2.6) \a = XAb = Y\swap(a9 Ь) \R:a = Y Ab = X\ где a, b—переменные типа integer, а идентификаторы Y и X обозначают соответственно их конечные значения. Применим теорему (12.2.1) для нахождения предусловия PR, удовлетворяющего теореме: PR = (a = XAb-Y)A ((Vul, u2:(yl =У М?= Х)И=» (о «К ЛЬ- Х)%* и2) = (a = XAb = Y)A (Vw/, u2:(ul = 4/\u2= У)=ФИ = ^/\u2=*X)) = (a = XAb = Y)AT Это предусловие следует из предусловия (12.2.6). Следовательно, (12.2.6) правильно. Q Пример 2. Рассмотрим процедуру из примера 1. Пусть нам нужно доказать, что имеет место (12.2.7) {a=AAb=Y} swap(a, b) {a=YAb==A} где а и b — переменные типа integer, а идентификаторы А и Y обозначают соответственно их начальные значения. Трудность здесь в том, что в описании и в вызове использованы разные идентификаторы для обозначения начальных условий X. Эта трудность преодолевается следующим образом. Относительно тела процедуры доказано, т. е. является тавтологией, следующее утверждение: {Р : yl=XAy2=Y) В {Q i yl=Y Ау2=Х) Следовательно, оно эквивалентно № У « {yl=XAy2=Y} В {yl = YAy2=X}) Теперь можно получить частный случай этого квалифицированного предиката, заменив X на Л, a Y на Y соответственно. Имеем {yl=AAy2=Y} В {yl=YAy2=A} Таким образом, и это последнее утверждение о теле процедуры В истинно. А теперь, применяя теорему так же, как в примере 1, получаем искомый результат. Следовательно, выполняется (12.2.7). Этот пример показывает, как можно обращаться с начальными и конечными значениями параметров. Идентификаторы, обозначающие начальные и конечные значения параметров, могут заменяться новыми идентификаторами и даже любыми выражениями, при этом получается другое доказательство аналогичного утверждения о теле процедуры, которое можно затем использовать в теореме (12.2.1).
160 Часть П. Семантика простого Языка программирования Пример 3. Докажем правильность вызова, у которого аргументами являются элементы массива. Рассмотрим процедуру из примера 1. Требуется доказать, что swap (i, b[i\) меняет местами значения i и b[i]y но не изменяет остаток массива Ь. Предполагается, что индекс i лежит в границах массива. Таким образом, требуется доказать (12.2.8) {i = IA(Yj:b[j] = B[j})\ swap(i, b[i]) {R:i = B[I]Ab[I]=lA(vi-I^i-b[j] = B[j])\ Идентификаторы / и В обозначают начальные значения переменных i и b соответственно. В доказательстве тела описания процедуры можно заменить X на / и Y на В[1] и получить {Piyl-lAy2-B[I]\B{Q:yl = B[I]Ay2=I\ Теперь, применяя теорему (12.2.1) к R из (12.2.8), получаем предусловие PR: PR = i = lAb[i] = B[I]A (yuly u2:ul==B[I]Au2==I=S>ul*=B[I]A {b- i:u2)[I] = lA(VJ-l¥=i-(b; i^2)[j]^B[j]) -i~lAb[i]~B[I]AB[I]-B[I]A (b- i:I)[I]=lA(VJ-l¥=j:(b; n/)[/]-A[/]) = i~lAb[i] = B[I]ATAl = lA(vJil¥=j'b[j] = B[j]) а это последнее выражение следует из предусловия (12.2.8) □ Пример 4. Рассмотрим процедуру ргос р (value х\ result zl, z2) {P:x=*X\zlt z2 := x, x{Q:zl = z2 = X\ присваивающую значение входного параметра обоим выходным па* раметрам. Отметим, что в постусловии Q входной параметр не содержится. Нужно выполнить вызов p(b[i], i, b[i+U), при котором bli] присваивается i и M/+U. Таким образом, имеет смысл попытаться доказать (12.2.9) {6[|]-СЛ*-/}/>№, '. b[i + l]){R:i = b[I] = 6[/+1]-С} Сначала заменим свободную переменную X в доказательстве тела процедуры на С: {P:x = C}zl, z2 := х, x{Q:zl = z2 = C\ Теперь, применяя теорему (12.2.1), получаем предусловие 6[*]«СЛ (yvl, v2:vl = v2 = C=$> vl = {b\ i-\-\-.v2)[/]-(&; i+l'.v2)[l+l]^C) = b[i] = CA(b; i+l:C)[l] = {b; /+1:Q[/+1]-C
Гл. 12. Вызов процедуры 161 Так как последняя строка следует из предусловия (12.2.9), то (12.2.9) истинно. □ Теорема, которую легче использовать Было бы хорошо не иметь сложного конъюнктивного члена (V«. о:<£2=>/?52) \ и, v и, и/ в предусловии теоремы (12.2.1). Если каким-либо образом ограничить вид постусловия R, может оказаться возможным устранить этот сложный член. Именно так и получается, например, если допускаются лишь такие R> для которых (12.2.10) RF-i==Qi'lAl и, v и, v где свободные переменные / отличны от Ь и с. Теперь можно следующим образом упростить сложный член: \ и, v и, v J = (V". 0:QE2=>qEa/) = / \ и, v и, v / (так как и, v не свободны в /). Теперь нашей задачей является определение предикатов R, удовлетворяющих (12.2.10). Для этого заменим в (12.2.10) иу v на Ьу си воспользуемся предикатами R, удовлетворяющими условию: R =(rFiI)^1 (по лемме (4.6.3) и упр. 5 из разд. 9.4) = (QFlAlYKl (12.2.4) \ и, v / Ь, с = Q!'-!A / (по лемме (4.6.3) и определению /) Ь, с Следовательно, можно ограничиться предикатами R, удовлетворяющими условию (12.2.11) R^QJJAI Но этого недостаточно. Из (12.2.11) требуется получить, что имеет место (12.2.10), но это не всегда так, поскольку \^Ь, с J u,v не всегда равно и, v 6 Д. Грис
162 Часть И. Семантика простого языка программирования Однако, как известно из леммы (4.6.3) и упр. 2 из разд. 5.3, эти два выражения равны, если (Ь, с) состоит из несовпадающих друг с другом идентификаторов. Итак, получается теорема, накладывающая на предикаты более сильные условия, но более легкая для применения. (12.2.12) Теорема. Пусть процедура р описана так же, как в (12.1.1), а (Ь, с)—список различных идентификаторов. Пусть ни один из свободных идентификаторов предиката / не входит в списки аргументов Ь и с. Тогда {р^ьА1}Р{а,-ЬГс){^1А1} D Предикат / теоремы еще раз привлекает наше внимание к понятию инвариантности: предикаты, не содержащие выходных аргументов, остаются во время вызова процедуры неизменными. Эта теорема проще теоремы (12.2.1) и может применяться в любом случае, когда в качестве выходных аргументов используются лишь идентификаторы. Примеры ее использования оставлены в качестве упражнений. 12.3. Использование параметров-переменных Входной-выходной параметр у вместе с соответствующим аргументом с следующим образом обрабатывается во время выполнения вызова: в у записывается значение с> выполняется тело процедуры и значение у записывается в с. Если у — массив, то реализация такого процесса может потребовать много времени и памяти. Другой метод установления соответствия между аргументом и параметром называется вызовом по ссылке. В этом случае перед выполнением тела процедуры в у записывается адрес с. Во время выполнения каждая ссылка на у понимается как косвенная ссылка на с. Например, присваивание у :—е внутри тела процедуры действует так же, как действовало бы с :=е. Другими словами, у и с рассматриваются как разные имена одной и той же ячейки. Вызов с применением входных-выходных параметров требует столько же места в памяти, каков размер аргументов, а вызов по ссылке требует одного и того же объема памяти во всех случаях. Вызов с входными-выходными параметрами требует на подготовку и завершение времени, по меньшей мере пропорционального размеру аргумента; при вызове по ссылке для этого требуется постоянное время. Но при вызове по ссылке может потребоваться больше времени на каждую ссылку на параметр во время выполнения тела процедуры. Вызов по ссылке предпочтительнее, особенно для аргументов, являющихся массивами.
Гл. 12. Вызов процедуры 163 Параметр, вызываемый по ссылке, обозначается при помощи атрибута var, являющегося сокращением английского слова variable (переменная). Обобщим данное в (12.1.1) описание процедуры и соответствующий ему вызов (12.1.2) следующим образом: (12.3.1) proc р (value x\ value result у\ result г; var r); \P\B\Q\ (12.3.2) Р(а, b, cy d) Как видоизменить теоремы (12.2.1) и (12.2.12), чтобы ввести в рассмотрение вызовы по ссылке? Вызов по ссылке можно рассматривать как эффективную форму реализации вызова входного-выходного параметра: выполнение то же самое, за исключением того, что не нужны начальные присваивания г и заключительные присваивания d. Но доказательство правильности тела процедуры, т. е. \P\B\Q\, совместимо с нашим понятием выполнения вызова для входных-выходных параметров лишь в том случае, если входные-выходные параметры занимают различные куски памяти — присваивание одному из параметров не должно действовать на значение любого другого параметра. Используя вызов по ссылке, нужно удостовериться, что это условие по-прежнему выполняется. Введем обозначение disj (d), означающее, что области в памяти, занимаемые dh не перекрываются. Например, для различных идентификаторов dl, d2 выполняется disj(d\y d2). Имеет место также disj(b[i]tb[i+ l])t a disj(b[i], b[j]) эквивалентно Далее говорят, что два вектора х и у попарно не перекрываются, или pdisj (x, у), если каждое х{ не перекрывается ни с каким yi9 т. е. выполняется disj(xit yj). Тогда теоремы (12.2.1) и (12.2.12) можно видоизменить следующим образом: (12.3.3) Теорема. Пусть выполняются disj (d) и pdisj (d, (fr, с)). Тогда У a, b,d \ * и, v. ы' и, v w J ) р(а, Ъ, с, d) (12.3.4) Теорема. Пусть (Ь, с, d)—список различных идентификаторов. Пусть ref(I) — список свободных идентификаторов предиката /. И наконец, пусть имеет место pdhj(bt с, d; ref /)). Тогда {рЩа,}р&,ь,-с,*){<11Щ п 6»
164 Часть II. Семантика простого языка программирования Если для простоты ограничиться вызовом по ссылке и вызовом по значению (т. е. входными параметрами), то теорему (12.3.4) можно упростить: (12.3.5) Теорема. Пусть процедура р описана как ргос р (value я, var г); {Р\ В {Q} а вызывается при помощи р(ау d), где d—список различных идентификаторов. Пусть ни один свободный идентификатор / не встречается в d. Тогда {pl~:Al}p(a,d){QZAl} П Примеры использования этих теорем оставлены в качестве упражнений. 12.4. Допуск входных параметров в постусловие В описании процедуры (12.1.1) постусловие ее тела Q может не содержать входных параметров х. Для этого имеются веские основания. Входные параметры рассматриваются как локальные переменные тела процедуры. Следовательно, они больше не имеют смысла после завершения выполнения тела процедуры. В общем случае нельзя осмысленно использовать локальные переменные команды в ее постусловии. Но такое ограничение вызывает раздражение, поскольку из-за него почти всегда требуется использовать дополнительные идентификаторы для обозначения начального значения входного параметра. Найдется ли способ допустить вхождение входных параметров в Q, который мог бы устранить эту проблему? Рассмотрим теорему (12.2.1): {pR:P1'Ia (v«. v:QI'1=S> rI' l)\ \ a, b \ u, v u, v J ) p(a, b9 c) Было бы бессмысленно по отношению к нашей модели выполнения вызова, если бы встречался х в QyJ f, поскольку на х нельзя и, v ссылаться до вызова (он находится в списке параметров процедуры). Но что в данном контексте понимается под х? Он на самом деле означает входные аргументы а, так что попытаемся заменить х в Q на а. Но такая замена имеет смысл по отношению к модели выполнения вызова лишь в том случае, если в момент завершения вызова входные параметры по-прежнему имеют началь-
Гл. 12. Вызов процедуры 165 ные значения входных аргументов. А это можно обеспечить, по* требовав, чтобы в Q не встречалось присваиваний входным параметрам и чтобы на входные параметры не влияло выполнение присваиваний другим параметрам. Тогда мы имеем следующий аналог теоремы (12.2.1), в котором на х можно ссылаться в Q. Аналоги теорем (12.2.12), (12.3.3) и (12.3.5) получаются подобным же образом. (12.4.1) Теорема. Пусть процедура р описана так же, как в (12.1.1), но Q может содержать входные параметры х. Пусть в В нет присваиваний входным параметрам, а на входные аргументы не влияет выполнение присваиваний другим параметрам во время выполнения вызова процедуры. Тогда имеет место \PR-.P1-\ Л ( у", ~п:фI!=> &1)\ { а, Ь \ v а, и, v и, v J J р(а, Ьу д) Другими словами, PR=$>wp(p(a> b, с), R). □ Примеры использования этой теоремы оставлены в качестве упражнений. Упражнения к гл. 12 1. Рассмотрим три предиката: {Q(u)}S{R} {(Yu:Q(u)))S{R) {(^u:Q(u))}S{R} где идентификатор и не входит свободно в команду 5 или предикат R. Предположим, что первый из предикатов доказан, т. е. является тавтологией. Эквивалентен ли он второму или третьему предикату? Указание: воспользуйтесь тем, что {Q («)}»£{#} эквивалентно Q (и) =$>wp (S, R). Таким образом, эта формула эквивалентна самой себе, причем идентификатор и связан квантором всеобщности с областью действия, включающей весь предикат. 2. Воспользуйтесь результатом упр. 1 для обоснования того, почему в теореме (12.2.1) не может быть опущен квантор yw, v, w. 3. Найдите контрпример к предположению, что теорема (12.2.12) имеет место, даже если аргументы не являются идентификаторами. Указание: в примере должны быть аргументы, которые не перекрываются, но некоторым образом взаимодействуют. 4. В разд. 12.2 содержится четыре примера использования теоремы (12.2.1). Правильность каких вызовов процедур из этих примеров может быть доказана путем применения теоремы (12.2.12) вместо (12.2.1)? Докажите их правильность, 5. Следующая процедура вставляет х в массив b [0:k—1], если он там не присутствовал, увеличивая таким образом k на 1, и записывает в р местоположение х в массиве Ь [0:k]. Предполагается, что элемент Ь [k] может использоваться процедурой для ее собственных нужд. Специфицирована ли эта процедура полностью? Другими словами, можно ли все, что опущено в спе-
166 Часть II. Семантика простого языка программирования цификациях, доказать, исходя из тела процедуры? {PreiO^k Л х = Х Л Ь = В) {PostiO^p^k л Ь{р] = Х} proc s (value х :'integer; var b: array of integer\ var kt p: integer)', b, b[k] := 0, x; {inv.O^p^k A x (£&[0:p— 1]} {bound: k—p} do x Ф b [p] —► p : = p-\-1 od Правильность каких из приведенных ниже вызовов можно доказать, используя теорему (12.2.1)? Докажите их правильность. (a) {d=0}s(5, с, d,/){c[/] = 5} (b) {0<m}stf, с, m,/) {с [/]=/} (c) {0 < m} s (6 [0], с, m, /) {с [/] = с [0]} (d) {0 < т) s (5, с, т, т) {с [т] = 5} 6. Какие из вызовов, приведенных в упр. 5, могут быть доказаны при помощи теоремы (12.2.12)? Докажите их правильность. 7. Допустим, что параметры k и р в упр. 5 имеют атрибут var, а не value result. Может ли быть доказана правильность вызова (d) при помощи теоремы (12.3.3)? Если может, то докажите это. Можно ли доказать се, пользуясь теоремами (12.3.4)? (12.3.5)? Если можно, то докажите это.
ЧАСТЬ III Построение программ Глава 13 ВВЕДЕНИЕ В части III обсуждается радикальная методология программирования, основанная на понятии слабейшего предусловия и использующая определение программных конструкций в терминах слабейших предусловий. Возможно, эта методология будет непохожа на то, с чем читатель сталкивался раньше. Цель введения — подготовить читателя к нашему подходу, т. е. дать аргументы в его пользу, объяснить ряд моментов и очертить общую картину. Что такое доказательство} Слово радикальная, использованное выше, очень подходящее, так как предлагаемая методология затрагивает существо нынешних проблем и выдвигает основные принципы для успешного их решения. Одна из этих проблем состоит в том, что программисты недостаточно хорошо знают, что такое правильность программ и как доказать эту правильность. Слово доказательство вызывает у многих неприятные ассоциации, и полезно объяснить, что оно значит. Согласно Третьему новому международному словарю Вебстера, «доказательство» — это «убедительное свидетельство, которое заставляет сознание верить в истинность или действительность». Это — доводы, которые убеждают читателя в истинности чего- либо. Из определения доказательства не следует необходимость в формализме или математике. И действительно, программисты пытаются доказать правильность своих программ именно в этом смысле слова «доказательство», ибо они на самом деле пытаются представить свидетельства, которые заставляют их самих поверить в нее. К сожалению, большинство программистов не очень сведущи
168 Часть 111. Построение программ^ в этом, что легко увидеть, если вспомнить, как много времени тратится на отладку. Программисту следует по-настоящему огорчаться от недостатка мастерства в этом вопросе! Это отчасти объясняется тем, что имеющиеся в распоряжении интеллектуальные средства не отвечали сложности задачи. Рассуждения основывались лишь на том, как программы выполняются на вычислительной машине, и доводы относительно правильности базировались на некотором числе тестовых задач, которые пропускались на машине или исполнялись вручную. Интуиции и сообразительности для этого просто недостаточно. Кроме того, не всегда было ясно, что имеют в виду, когда гово-- рят, что программа «правильна», и, в частности, из-за того, что спецификации программы были очень неточны. В ч. II было разъяснено понятие правильности: программа 5 называется правильной — по отношению к данным предусловию Р и постусловию Q — если имеет место {P}S{Q}. У нас есть и формальные средства для доказательства правильности. Таким образом, наш способ построения программ во главу угла ставит понятие формального доказательства, включая слабейшие предусловия и теоремы о конструкциях выбора, повторения и вызова процедуры, которые обсуждались в ч. II. В связи с этим важен следующий принцип: (13.1) Принцип. Программа и ее доказательство должны строиться одновременно с тенденцией опережающей разработки доказательства . Слишком трудно доказывать правильность уже существующей программы, и гораздо лучше использовать идеи доказательства правильности во время программирования для осознания того, что делать дальше. Равновесие между формализмом и здравым смыслом Наш подход к программированию основан на доказательствах правильности программ. Но вы можете быть спокойны: формализму не будет отдано все наше внимание; это не необходимо, и мы этого не хотим. Одного формализма недостаточно, поскольку он ведет к малопонятным деталям; точно так же недостаточно одного здравого смысла и интуиции (главных орудий программиста до настоящего времени), поскольку им сопутствует много ошибок и плохих решений. Что необходимо — это тонкое равновесие между ними. Очевидные факты следует оставить в неявном виде, важные моменты должны быть подчеркнуты, частности должны быть представлены читателю так, чтобы обеспечить ему легкое понимание программы. Должны быть найдены обозначения, уменьшающие необходимость использования формализма. Там, где удобно, вполне годя!ся определения на естественном языке, но если они оказываются рас-
Гл. 13. Введение 169 плывчатыми, то в дальнейших рассуждениях нужно действовать более формально. Это требует сообразительности, вкуса, знаний и опыта. Это нелегко. Конечно, каждый математик старается достичь такого равновесия. В доказательстве могут быть оставлены большие пробелы, если чувствуется, что образованный читатель поймет, как их заполнить. Самым важным и трудным местам уделяется больше внимания. Чтобы доказательство можно было легче понять, оно расчленяется на последовательность лемм. Это равновесие между формалистикой и здравым смыслом тем более важно для программиста. В программировании мы имеем дело с гораздо большим числом деталей, которые должны быть абсолютно правильны, и здесь нельзя полагаться на добрую волю читателя. К тому же многие программы так велики, что за раз их невозможно полностью понять. Отсюда эта постоянная необходимость добиваться равновесия, краткости и даже элегантности. Следовательно, наш подход можно кратко изложить так: (13.2) Принцип. Пользуйтесь теорией для проникновения в суть дела; применяйте здравый смысл и интуицию там, где это возможно, но сразу же возвращайтесь к формальной теории за поддержкой, когда возникают трудности. Однако нельзя достичь равновесия, если нет как здравого смысла, так и легкости в обращении с теорией. Первым программисты пользовались достаточно широко; чтобы восстановить равновесие, необходимо временно опереться на формальную сторону. В связи с этим последующие рассмотрения и упражнения, возможно, будут несколько более формальными, чем это требуется на практике. Доказательство и анализ результатов тестов Выше упоминалось, что часть наших трудностей связана с тем, что во время разработки программы и при ее отладке излишне полагаются на работу с тестовыми примерами. «Построение при помощи тестовых примеров» происходит следующим образом. Программу строят, ориентируясь на несколько примеров того, что программа должна делать. Далее рассматриваются (и, возможно, пропускаются на машине) другие тестовые примеры и программа модифицируется с учетом их результатов. Этот процесс продолжается (с модификациями программы на каждом шаге) до тех пор, пока не будет уверенности, что проверено достаточное количество тестов. Подход же, описываемый в этой части книги, основывается на одновременном построении программы и ее доказательства правильности. Он отличается от обычного операционного подхода. На самом деле опыт, приобретенный при работе с новым подходом, может изменить способ решения читателем и других задач (не только из
170 Часть 111. Построение программ области программирования). Два примера покажут, насколько эффективным может оказаться новый подход. Задача о банке с кофейными зерными. В банке имеется несколько черных и белых кофейных зерен. Следующий процесс нужно повторять, пока это возможно. Случайно выберите из банки два зерна. Если они одного цвета, отбросьте их, но положите в банку другое черное зерно. (Имеется достаточный запас черных зерен, чтобы это делать.) Если они разного цвета, поместите белое зерно обратно в банку и отбросьте черное зерно. Выполнение этого процесса уменьшает количество зерен в банке на единицу. Повторение процесса должно прекратиться, когда в банке останется всего одно зерно, так как тогда нельзя уже выбрать два зерна. Вопрос состоит в следующем: можно ли что-то сказать о цвете оставшегося зерна, если известно, сколько вначале в банке было черных и белых зерен? Прежде чем читать дальше, потратьте на решение этой задачи десять минут (что более чем достаточно). Тестовые примеры здесь не очень помогают! Не помогает и рассмотрение того, что случится, когда вначале в банке имеется 1 черное и 1 белое зерно, а затем когда вначале были 2 черных и 1 белое зерно и т. д. Я видел, как некоторые впустую тратили по полчаса на такой подход. Вместо этого поступим следующим образом. Может быть, есть простое свойство, справедливое для зерен в банке, которое останется истинным, когда зерна удаляются, и которое с учетом того факта, что остается только одно зерно, поможет нам быстро найти ответ? Так как это свойство всегда остается истинным, мы назовем его инвариантом. Хорошо, предположим, что в конце осталось одно черное зерно и не осталось ни одного белого. Какое свойство истинно в момент окончания процесса и при обобщении могло бы стать нашим инвариантом? Единица — нечетное число, так что, быть может, сохраняется нечетность числа черных зерен? Нет, это не так; на самом деле число черных зерен на каждом шаге изменяется с четного на нечетное или с нечетного на четное. Но в момент завершения у нас нуль белых зерен — возможно, четность числа белых зерен остается неизменной. Так оно и есть на самом деле: на каждом возможном шаге либо выбираются два белых зерна, либо число белых зерен остается прежним. Таким образом, последним зерном будет черное, если вначале было четное число белых зерен, в противном случае оно белое. Замыкание кривой. Эта вторая задача решается, в сущности, тем же способом. Рассмотрим точечную решетку произвольного раз*
Гл. 13. Введение 171 мера: Два игрока — А и В — играют в следующую игру. Игроки делают ходы поочередно: первым ходит А. Ход игрока А состоит в том, что он проводит | или — между двумя соседними точками, а ход В — в том, что он проводит пунктирную линию между двумя соседними точками. Например, после трех ходов каждого игрока решетка может выглядеть так, как показано ниже на левом рисунке. Один игрок не имеет права чертить свой ход по ходу, сделанному другим игроком. Игрок А выигрывает, если ему удается получить полностью замкнутую кривую, как показано ниже на правом рисунке. J У игрока В, поскольку он ходит вторым, задача проще: он выигрывает, если может помешать А получить замкнутую кривую. Возникает следующий вопрос: существует ли стратегия, гарантирующая выигрыш либо для Л, либо для В безотносительно к тому, как велика доска? Если такая стратегия есть, то какова она? Потратьте некоторое время на обдумывание задачи, прежде чем читать дальше. Изучение тривиального случая, когда решетка состоит из одной точки, показывает, что А не может всегда выигрывать — для замкнутой кривой нужны четыре точки. Следовательно, будем искать выигрывающую стратегию для В. Играть в игру и просматривать тестовые примеры бесполезно — так ответа не найти. Вместо этого исследуем свойства замкнутых кривых, так как, если на доске можно воспрепятствовать появлению хотя бы одного из этих свойств, А не может выиграть. Соответствующий инвариант состоит в том, чтобы на доске никогда не возникла конфигурация, в которой А мог бы реализовать это свойство. Какими свойствами обладает замкнутая кривая? У нее есть параллельные участки, но В не может предотвратить появление
172 Часть 111. Построение программ их. У нее четное число параллельных участков, но В не может предотвратить и этого. У нее четыре угла [_ , J , f , ™|, но В не может помешать А начертить угол. Однако у замкнутой кривой есть по крайней мере один угол, а именно [__» и В может помешать А начертить такой угол! Если А проводит горизонтальную или вертикальную линию, то В дополняет ее соответствующей вертикальной или горизонтальной линией, как показано на рисунке ниже, если только такая линия уже не была проведена им раньше. Более простой стратегии просто быть не может! У этих двух задач очень проаые решения, но их очень трудно найти, если просто пытаться решать тестовые примеры. Задачи упрощаются, когда начинают искать свойства, остающиеся истинными. А когда они найдены, эти свойства позволяют тривиально увидеть и само решение. Наряду с демонстрацией неадекватности поиска решения при помощи тестовых примеров эти задачи иллюстрируют следующий (13.3) Принцип. Изучите свойства объектов, с которыми должна работать программа. На самом деле мы убедимся на примерах, что, чем больше свойств объектов вы знаете, тем у вас больше возможностей создать эффективный алгоритм. Но оставим очередные примеры использования этого принципа до следующих глав. Программирование в миниатюре За последние десять лет большое число исследований было посвящено «программированию в миниатюре»: частично из-за того, что оно казалось именно той областью, где можно было добиться научного прогресса. Еще важнее, однако, что чувствовалось, что способность строить маленькие программы является необходимым условием для построения больших, хотя, может быть, и не достаточным. Весьма легко убедиться в справедливости этого утверждения на следующем примере. Предположим, что программа состоит из п небольших компонентов (т. е. процедур, модулей), каждый из которых правилен с вероятностью р. Тогда вероятность Р правильности всей программы, конечно, удовлетворяет неравенству P<ipn.
Гл. 13. Введение 173 Так как для любой достаточно большой программы п велико, надеяться на правильность программы можно лишь при значениях /?, очень близких к 1. Например, у программы с 10 компонентами, каждый из которых правилен с 95%-ной вероятностью, шансов быть правильной меньше чем 60%, а программа со 100 такими компонентами верна менее чем на 0,6%! Замечание. Даг Макилрой (лаборатории фирмы «Белл») не согласен с этими доводами. Он утверждает, что правильные программы фактически составляются из неправильных частей. Например, программа управления телефонной сетью более чем наполовину состоит из команд-ревизоров, задачей которых является организация выхода из непредусмотренных состояний. А команды-ревизоры, как известно, скрывают не только ошибки оборудования, но и ошибки программ. Кроме того, неправильная процедура может вызываться из нашей программы ограниченным (как именно, для нас неизвестно) числом способов, так что неправильность никогда не проявится. Процедуры, которые просто разваливаются при некоторых входных данных, великолепно работают в программах, которые изолируют их от таких случаев. Тем не менее для большинства ситуаций, с которыми мы встречаемся, доводы имеют силу. □ В третьей части внимание сосредоточено на построении маленьких сегментов программ, именно здесь появляется много ошибок программирования. Длина приводимых в третьей части программ составляет от 1 до 25 строк, причем длина большинства из них — от 1 до 10 строк. Впрочем, верно, что небольшая длина некоторых из программ объясняется методом их построения. Следование принципам и забота о точности, ясности и элегантности могут проявиться в конечном счете в создании более коротких программ. Наиболее показательным примером этого является «Жулик на пособии» из разд. 16.4. Замечание Описываемые в третьей части методы, безусловно, могут принести пользу почти каждому программисту. В то же время следует сразу же сказать, что существуют и другие способы построения программ. Такая трудная задача, как программирование, требует применения различных средств и методов. Разработка ряда алгоритмов требует использования идей, которые сами по себе не вытекают из приведенных в этой части книги принципов; поэтому с помощью одного нашего метода разрешить все возникающие проблемы невозможно. Некоторые важные идеи, такие, как преобразование программ и «абстрактные типы данных», не обсуждаются вовсе, а другие лишь слегка затрагиваются. И безусловно, опыт и знания могут заменить самые развитые теории.
174 Часть III. Построение программ Ошибки будут встречаться, даже если основной упор делается на доказательствах правильности. Мудрый программист строит программу с уверенностью, что правильная программа может быть построена и будет построена, если приложить достаточно усилий и внимания, а затем тщательно отлаживает ее, зная, что в ней должна быть ошибка. Частое появление ошибок в математических теоремах, доказательствах и при применении теорем хорошо известно и зафиксировано в публикациях, и область доказательства правильности программ не является исключением. Просто мы должны смириться с тем, что человеку свойственно ошибаться, и стремиться сводить число ошибок к минимуму. Тем не менее изучение третьей части позволит овладеть основами строгого мышления, без к©торого нельзя хорошо программировать. Конечно, осознанное применение рассмотренных здесь принципов и стратегий принесет пользу. Организация материала третьей части Чтобы изложение принципов и правил было наиболее доходчивым, большинство разделов построено следующим образом. Для иллюстрации одного или двух новых положений используется небольшой пример. Новые положения обсуждаются. Затем строятся один или два примера с таким расчетом, чтобы заставить читателя воспользоваться новым материалом. Читателю задают вопрос, касающийся построения примера; за вопросом следуют пустое место, горизонтальная черта и сам ответ (как это делалось выше). Читателю предлагают попытаться ответить на вопрос прежде, чем продолжать чтение. И наконец, читателю рекомендуется выполнить некоторые из упражнений в конце раздела или главы. Простое чтение или прослушивание лекций относительно построения программ может дать представление лишь о методе построения. Чтобы научиться им пользоваться, необходим непосредственный опыт работы. В этой связи исключительно важен следующий метапринцип. (13.4) Принцип. Никогда не отвергайте как очевидный никакой фундаментальный принцип, так как только осознанное применение таких принципов может принести успех. Идеи могут быть просты и легки для понимания, но их применение может потребовать усилий. Узнать принцип и следовать ему — совсем разные вещи. Какую пишущую машинку вы выберете? Пишущая машинка появилась в Соединенных Штатах в 1867 г. В 1873 г. было реализовано и уже больше никогда не изменялось распределение символов по клавишам пишущей машинки, назы-
Гл. 13. Введение 175 ваемое клавиатурой QWERTY (по первым шести буквам верхнего ряда клавишей). В то время скорость не играла особой роли — во всяком случае, большинство людей печатало с помощью всего двух пальцев. Более того, машинку часто заедало, и в целях борьбы с этим самые распространенные буквы были произвольным образом распределены по клавиатуре. Сейчас множество отличных машинисток, работающих с высокой скоростью по слепому методу, используют неэффективную клавиатуру QWERTY, поскольку пишущие машинки производятся только с такой клавиатурой. Время от времени разрабатывается и испытывается новое распределение клавиш. Испытания показывают, что хорошая машинистка может изучить новую клавиатуру примерно за месяц, а потом печатать с гораздо большей скоростью, меньшей затратой энергии и меньшим напряжением. И все же новая клавиатура никак не может привиться. Почему? Слишком много отдано старой клавиатуре. Из-за высокой стоимости изменений и из-за инертности клавиатура QWERTY остается вне конкуренции. Посмотрим фактам в лицо: средний программист — это QWERTY-программист. Ему навязали старые обозначения, подобные Фортрану или Коболу. Но, что более важно, он по-прежнему думает «двумя пальцами», используя те же приемы мышления, которые использовались в самом начале развития информатики — в 1940-х и 1950-х годах. Правда, здесь помогло «структурное программирование», но даже оно само по себе недостаточно. Приемы мышления, доступные программистам, стали, попросту говоря, неадекватными. Работа по совместному построению доказательства и программы начинает приносить плоды, и она может привести к более эффективной организации предоставляемой программисту клавиатуры. К счастью, оборудование не нуждается в изменениях. Приемы мышления и принципиальная позиция программиста гораздо важнее для программирования, чем обозначения, в которых выражается окончательный текст программы. Например, можно использовать принципы и правила, изложенные в этой книге, даже если окончательный текст программы должен быть на Фортране: мы программируем для языка программирования, а не на нем. Безусловно, чтобы отучить себя от QWERTY-программирования, потребуется больше месяца учебы и тренировки; старые привычки меняются очень медленно. Тем не менее, я думаю, на это стоит потратить время. Перейдем теперь к объяснению принципов и правил, которые, как мы надеемся, помогут QWERTY-программисту овладеть новой клавиатурой.
Глава 14 ПРОГРАММИРОВАНИЕ КАК ЦЕЛЕНАПРАВЛЕННАЯ ДЕЯТЕЛЬНОСТЬ Простой пример построения программы Рассмотрим следующую задачу. Напишите программу, которая при заданных фиксированных целых числах х и у присваивает z значение, равное наибольшему из них. (Примем соглашение, что переменные, названные фиксированными, не могут быть изменены при выполнении программы; см. разд. 6.3.) Таким образом, нам требуется команда 5, удовлетворяющая (14.1) {Т} S {R :z=max(xy у)} Прежде чем писать программу, необходимо уточнить /?, заменив max его определением (в конце концов, как можно написать программу, не зная, что такое max). Переменная z содержит наибольшее из х и г/, если она удовлетворяет (14.2) R: г^хА?>УЛ (z=x\/z=y) А какую комадду можно было выполнить, чтобы получилось (14.2)? Поскольку (14.2) включает z=x, напрашивается присваивание г :=х. Можно было бы попробовать и z := х+l, но z := x предпочтительнее по крайней мере по двум причинам. Во-первых, оно определяется из самого R: чтобы обеспечить z=x, присвойте z значение х. Во-вторых, оно проще. Чтобы определить условия, при которых выполнение z := х на самом деле дает (14.2), достаточно просто вычислить wp("z := x", R): wp("z := х", R) = x^х /\х^у /\(х = х\/ х = у) = ТЛх^уЛ(Т\/х = у) = х>У Это дает нам требуемое условие, и первая попытка написать программу может быть следующей: if x^y—>z :== х fi Эта программа, действительно, делает то, что нужно, при условии отсутствия авоста. Напомним, что по теореме (10.5) о конструкциях выбора для предотвращения авоста необходимо, чтобы предусловие Q конструкции влекло за собой дизъюнкцию охран, т. е. в любом начальном состоянии из Q как минимум одна из охран должна быть истинна. Следовательно, необходима по крайней мере еще одна охраняемая команда.
Гл. 14. Программирование — целенаправленная деятельность \11 Другая возможность получить R — это выполнить присваивание z :=у. Из изложенного выше должно быть ясно, что требуемая охрана есть у^х. После добавления этой охраняемой команды получается (14.3) if x^y-+z := х D y>x-+z := у fi Теперь хотя бы одна из охран всегда истинна, и мы имеем требуемую программу. Формально то, что (14.3) — требуемая программа, доказывается по теореме (16.5). Чтобы применить теорему, положим Sx\z := х S2:z := у Вг:х^у В2:у^х Р:Т R:z^zx Az^zy Л(г = х\/ * = У) Обсуждение Показанное построение иллюстрирует следующий (14.4) Принцип. Программирование — целенаправленная деятельность. Это значит, что ожидаемый результат или цель R играет при построении программы более важную роль, чем предусловие Q. Конечно, и Q играет свою роль, как будет показано дальше. Но, как правило, более глубоко в природу задачи мы проникаем тогда, когда известно постусловие. Целенаправленность программирования — одна из причин, по которой программные конструкции определялись через слабейшие предусловия (а не через сильнейшие постусловия; см. упр. 4 из разд. 9.1). Чтобы обосновать гипотезу о целенаправленности программирования, рассмотрим следующие соображения. Выше сразу же было отброшено предусловие и была создана программа, удовлетворяющая {?} S {R : z=max(xy у)} Для того чтобы проверить полноту S, рассматривалось требование Q=>wp(Sy R). Поступим по-другому: забудем о постусловии R и попытаемся построить программу S, удовлетворяющую лишь {Т} S {?} Как только S будет построена, проверим Т => wp(S, z=max(xy у)) или T=>wp(S, (14.2)). Как вы думаете, сколько программ S мы напишем, до того как построим правильную? В приведенном построении был использован еще один принцип.
178 Часть III. Построение программ (14.5) Принцип. Прежде чем пытаться решать задачу, убедитесь, что вы понимаете, в чем она состоит. При программировании этот общий принцип превращается в следующий (14.6) Принцип. Прежде чем строить программу, уточните и разъясните себе пред- и постусловия. В только что приведенном примере постусловие было уточнено, а предусловие, которое было просто 71, в разъяснениях не нуждалось. Иногда задача ставится так, что она допускает много толкований. Тогда целесообразно потратить некоторое время, чтобы сделать условия по возможности ясными и недвусмысленными. Более того, способ задания условий может существенно влиять на построение алгоритма, и поэтому полезно стремиться к простоте и элегантности. Для некоторых задач главная трудность вообще состоит в том, чтобы сделать условия простыми и одновременно точными; само построение программы после этого оказывается удивительно легким. Часто условия даются на естественном языке. Иногда же в них используются общепринятые обозначения — подобные max (xy у)у и тогда они оказываются слишком «высокого уровня» для того, чтобы по ним строить программу. Они могут содержать и обозначения, принятые в незнакомой для программиста области приложений. Условия пишут для того, чтобы изложить, что должна делать программа, а абстракция часто используется для упрощения. Но для задания того, как делать, может потребоваться больше частностей. Пример с построением г, равного наибольшему из х и у, ярко иллюстрирует это. Нельзя написать программу, не зная, что означает max, а определение наводит на идею дальнейшего построения. Построение программы (14.3) демонстрирует одну из основных методик построения конструкций выбора, обосновываемую теоремой (10.5) о конструкциях выбора. (14.7) Стратегия построения команд выбора. Чтобы создать охраняемую команду, найдите команду С, выполнение которой даст постусловие R по крайней мере в некоторых случаях; найдите логическое выражение 5, удовлетворяющее B=>wp(C> R); соедините их в выражение В -* С (см. предположение 2 теоремы). Продолжайте создавать охраняемые команды до тех пор, пока предусловие конструкции не будет обеспечивать, что по крайней мере одна из охран истинна (см. предположение 1 теоремы). И эта методика, и подобная ей для конструкций повторения используются часто. Но вернемся к программе (14.3). Радующая глаз симметричность операторов — это одно из следствий того, что мы допускаем не-
Гл. 14. Программирование — целенаправленная деятельность 179 детерминизм. Если нет оснований выбирать между z:=x и г:=у при х=у, то не следует и принуждать к этому выбору. При программировании нужно глубоко продумывать все, а необоснованные требования могут легко сбить с толку. Обычные детерминированные конструкции вынуждают прибегать к выбору, и это служит одной из причин, чтобы предпочесть охраняемые команды. Недетерминированность является важным преимуществом, даже если окончательный текст программы окажется детерминированным, поскольку она позволяет нам разработать хорошую методику программирования. Мы можем построить множество различных охраняемых команд, совершенно независимых между собой. Любая форма детерминизма, например вычисление -охран в порядке их вхождения (как в операторе SELECT языка ПЛ/1), радикально влияет на способ мышления при построении конструкций выбора Второй пример Напишите программу перестановки значений целых переменных х и у таким образом, чтобы имело место х^у. Воспользуйтесь только что обсуждавшимся способом построения. В качестве первого шага, прежде чем продолжить чтение, напишите предусловие Q и постусловие R. Эта задача чуть труднее предыдущей, так как требуется ввести обозначения для начальных и конечных значений переменных. Предусловие Q есть х=Х/\y=Y, где X и Y обозначают соответственно начальные значения переменных х и у. Постусловием R будет (14.8) R : x^yA((x-XAy-Y)W (x=YЛУ=Х)) Замечание. Можно было бы воспользоваться понятием перестановки perm и записать R следующим образом: х^у /\perm ((ху у), (X, Y)). U А теперь ответьте, какие простые команды могут хотя бы в некоторых случаях обеспечить (13.8)? Предусловие Q, а именно х=Х /\y=Y, является частью R, так что имеется возможность того, что skip обеспечит R при некоторых условиях. (Это и есть использование предусловия для получения дополнительной информации.) Другой возможностью может быть перестановка х> у :=у, ху поскольку она также обеспечивает второй конъюктивный член R. Как определить охрану для каждой из этих команд и какие охраны при этом получатся?
180 Часть III. Построение программ Охрана В; охраняемой команды В,—^S,-, входящей в конструкцию выбора, должна удовлетворять Q A Btz=»wp(Sh R) (по теореме о конструкции выбора). Для команды skip мы имеем wp(skipy R) = R. Следовательно, охрана В команды В —► skip должна удовлетворять Qf\B=$R. Поскольку Q влечет второй конъюнктивный член Ry первый конъюнктивный член х^у может быть охраной, и охраняемой командой будет х^у—► skip. Для второй команды имеем wp("x, у := у, х", R) = Опять-таки Q влечет второй конъюнктивный член слабейшего предусловия, так что первый член у^х может стать охраной. Это дает конструкцию выбора if x^.y—+skip □ f/<*—**, у := уу х fi Поскольку дизъюнкция охран x^y\Jy^x всегда истинна, программа правильна (по отношению к данным Q и R). Внимательно проследите, как теорема о конструкциях выбора использовалась для определения охран. Это и не удивительно — теорема всего-навсего формализует принципы, используемые программистами для понимания команд выбора. Ставьте сильную охрану команд выбора Предположим, что в переменной / хранится остаток от деления k на 10 (для fc>0). Значит, / и k должны всегда удовлетворять jzszk mod 10 Таким образом, / может принимать лишь значения 0, 1, ..., 9. Определим команду "увеличить ky сохраняя j = k mod 10", предположив, что функции mod у нас нет. Одной из возможных команд является k, j := £+1, /+1. Однако она работает лишь тогда, когда перед ее выполнением / < 9, и мы ставим охрану /<9—+ky / := k+l,j+l. Однако в исходном состоянии у нас было 0<^/<10, так что нужно рассмотреть еще и случай i = 9. В этом случае, очевидно, подходит команда i, / := HI, 0, и мы получаем следующий сегмент программы: (14.8) if /<9 —*, / : = А+1, /+1 D /=9-**, / := k+l, 0 fi
Гл. 14. Программирование — целенаправленная деятельность 181 Обратите внимание на то, как неформально, но строго была использована стратегия (14.7). Однако возникает вопрос: что предпочесть, программный сегмент (14.8) или (14.9), приведенный ниже? Он отличается от (14.8) лишь второй, более слабой охраной /^9. На первый взгляд (14.9) выглядит привлекательнее, поскольку при его выполнении авост происходит реже. Например, если / первоначально равно 10, оно будет прекрасно установлено в 0. Но как раз именно поэтому (14.9) хуже. Ясно, что /=10 — это ошибка, вызванная сбоем аппаратуры, ошибкой в программе или неосторожным ее изменением; предполагается всегда, что 0^/<С10. А выполнение сегмента (14.9) продолжается как ни в чем не бывало, и ошибка остается необнаруженной. При выполнении же сегмента (14.8) произойдет авост, если /=10, и ошибка будет обнаружена. (14.9) if /<9 —А, / := £+1, /+1 П Все это приводит к следующему принципу. (14.10) Принцип. При прочих равных условиях делайте охраны в командах выбора настолько сильными, насколько возможно, с тем чтобы некоторые ошибки приводили к авосту. Оговорка «при прочих равных условиях» подчеркивает, что принцип нужно применять разумно. Например, я даже не готор выступать сторонником усиления первой охраны: И 0</Л/<9 —ftf / :- 6=1, / + 1 □ / = 9 —*, / := £+1,0 Н И наконец, программа (14.8) может быть перестроена следующим образом: k := k+l; if /<9-* / := /+1 Q / = 9->/ := 0 fi Упражнения к гл. 14 1. Постройте способом, подобным только что применявшемуся, программы для решения следующих задач. Напомним, что подходящие пред- и постусловия нужно строить в самом начале. (a) Установить z равным abs (х). (b) Установить х равным abs (x). (c) Пусть в х содержится число нечетных целых чисел в массиве b [0:&—1], и пусть /е^0. Напишите программу увеличения к на 1 с сохранением этого соотношения. Это означает, что после ее завершения k должно быть больше на 1, чем первоначально, а х должно по-прежнему содержать число нечетных элементов массива b[0:k—1].
182 Часть III. Построение программ (d) Пусть целые переменные а и Ь удовлетворяют 0 < а-\-\ < Ь, и множество {а, а+1, ..., Ь) содержит по меньшей мере три элемента. Допустим, что выполняется также следующее условие: Р:а2^п Л b2> n Возможно ли поделить интервал а:Ь пополам, установив а либо Ь (а-\-Ь)-?-2 таким образом, чтобы Р сохранилось истинным? Ответьте на этот вопрос, попробовав построить программу, которая сделала бы это. 2. (Следующая большая перестановка.) Рассмотрим целое число из п (п > 0) десятичных цифр, содержащихся в массиве d[Q:n— 1], где d [0]— цифра самого старшего разряда. Например, при л = 6 число 123542 можно поместить в d следующим образом: d = (l, 2, 3, 5, 4, 2). Следующая большая перестановка d[0:n—1] — это массив d', представляющий наименьшее из больших целых чисел, составленных из тех же самых цифр. В приведенном примере следующая большая перестановка будет d' = (l, 2, 4, 2, 3, 5). Задача состоит в том, чтобы точно определить следующую большую перестановку. Дает ли ваше определение идею построения искомой программы?
Глава 15 ПОСТРОЕНИЕ ЦИКЛОВ, ИСХОДЯ ИЗ ИНВАРИАНТОВ И ОГРАНИЧЕНИЙ В этой главе обсуждаются два метода построения цикла при данных предусловии Q, постусловии /?, инварианте Р и ограничивающей функции L Первый метод естественно приводит к циклу с одной охраняемой командой: do В ->- S od. Во втором методе учитываются преимущества, предоставляемые гибкостью конструкции повторения. Он в общем случае приводит к циклам с более чем одной охраняемой командой. В данной главе будет интенсивно использоваться список условий (11.9), и целесообразно просмотреть его перед чтением главы. Так же, как и повсюду в этой книге, части построения, иллюстрирующие рассматриваемые принципы, обсуждаются формально и подробно, а другие части — более неформально. 15.1. О первоочередности разработки охраны Суммирование элементов массива Рассмотрим следующую задачу. Напишите программу, которая для данных фиксированного целого числа ri^O и фиксированного массива целых чисел Ь[0 : п—1] записывает в переменную s сумму элементов массива Ь. Предусловие Q — это просто п^0\ постусловие R — это ^:s = (2/:0</<az:&[/]) Ищется цикл со следующими инвариантом и ограничивающей функцией: tin — i Таким образом, введена переменная i. В инварианте говорится, что в любой момент вычисления в s содержится сумма первых i значений Ь. Очевидно, что присваивание /, s :=0, 0 устанавливает Р, так что его нам будет достаточно для выполнения инициализации. (Заметим, что присваивания i, s :=1, b[0] недостаточно, поскольку при п=0 оно не может быть выполнено. Если п—0, то выполнение программы должно присвоить эквивалент «пустого» сложения, т. е. 0.)
184 Часть III. Построение программ Следующий шаг—определить охрану В цикла do В—+S od. В списке условий (П.9) требуется Р Л "]#=£#, так что "]5 выбирается таким, чтобы удовлетворять этому условию. Сравнивая Р и Ry заключаем, что i = n удовлетворяет ему. Искомая охрана цикла В является, следовательно, отрицанием выбранного утверждения, т. е. 1фп. Программа, которую мы ищем, выглядит примерно так: i, s := О, 0; do i^n—*} od А теперь построим команду. Назначение этой команды — приближать цикл к завершению, т. е. уменьшать ограничивающую функцию /; очевидным первым кандидатом является присваивание i : = t+l. Но такое присваивание разрушало бы инвариант, и, чтобы восстановить его, необходимо одновременно прибавить b[i] к s. Таким образом, получаем программу (15.1.1) i, s := 0, 0; do i=£n—+i, s := i+l, s + b[i] od Замечание. Для тех, кто еще не чувствует уверенности в обращении с кратными присваиваниями, дадим формальное доказательство того, что инвариант Р сохраняется. Имеем wp{% s := i+l, s + b[t\n, P) = 0</+l</iAs + &[f] = (S/:0</<t+l:6[/]) а это последнее утверждение следует из Рf\i=£n. □ Обсуждение Прежде всего обсудим равновесие между строгостью рассуждений и интуицией, наблюдаемое в данном построении. Пред- и постусловие, инвариант и ограничивающая функция заданы формально и точно. Построение частей программы дается менее формально, но для этого построения большинство мотивировок и наводящих соображений обеспечивает список условий (11.9), обоснованный на формальной теореме о конструкциях повторения. Чтобы проверить неформальное построение, обратимся к теории (при проверке того, что тело цикла сохраняет инвариант). Этот пример показателен для общего подхода (13.1), упоминавшегося в гл. 13. Важной характеристикой стратегии построения программы является то, что охрана ищется до поиска команды. И первое соображение при поиске охраны В — то, что она должна удовлетворять условию Р/\ ~]В=> R. Итак, строится ]Ви затем находится его отрицание для получения В. Некоторые поначалу возражают против поиска охраны таким способом, поскольку, если следовать традиции, в качестве охраны надо бы использовать i<C.n вместо 1фп. Однако 1фп лучше, поскольку программная или аппаратная ошибка, приводящая к i>n, вызовет зацикливание. Лучше потратить машинное время,
Гл. 15. Построение циклов 185 чем страдать от последствий того, что ошибочный шаг остался необнаруженным, что и могло бы случиться, если бы использовалась охрана Кп. Проделанный анализ приводит к следующему принципу: (15.1.2) Принцип. При прочих равных условиях делайте охраны цикла возможно слабее, чтобы ошибка могла вызвать зацикливание24*. Сравните принцип (15.1.2) с принципом (14.10) об охранах команды выбора. Метод, использованный для построения цикла, очень прост и удобен, так как он основывается на преобразованиях статических математических выражений. В этой связи мне вспоминаются старые времена программирования на Фортране — начало 60-х годов,— когда иногда требовались три отладочных запуска для того, чтобы достичь завершения цикла в нужный момент. В первый раз цикл выполнялся на один раз меньше, чем это было нужно, во второй — на один раз больше и лишь на третий раз — в точности столько раз, сколько нужно. Это был удручающий процесс проб и ошибок. Теперь этого больше не требуется: сразу же постройте ~]Б, удовлетворяющее Р/\ "]В=> R, и найдите его отрицание. Другой важный момент при построении цикла — это упор на завершение его работы. Требование приближаться к завершению побуждало создавать тело цикла, а восстановление инварианта было вторым соображением при его создании. В действительности каждый цикл с одной охраняемой командой имеет следующую интерпретацию в терминах высокого уровня: (15.1.3) {инвариант: Р} {ограничение: t) do В—-► Уменьшить t, сохраняя истинность Р od {РЛ1В\ Этот подход к построению цикла можно кратко изложить следующим образом: (15.1.4) Стратегия построения цикла. Сначала постройте такую охрану В, что Яд"]В=ф/?; затем постройте тело цикла таким образом, чтобы оно уменьшало ограничивающую функцию при сохранении инварианта цикла. Поиск в двумерном массиве Рассмотрим следующую задачу. Напишите алгоритм, который ищет фиксированное значение х в данном фиксированном массиве массивов МО : т—1][0 : п—1], где 0<т и 0<я. Если х встречается в нескольких местах, не имеет значения, какое из мест будет найдено. Для этой задачи применим традиционные обозначения для
186 Часть III. Построение программ двумерного массива, записывая b как [0 : т—1, 0 : п—11. Воспользуемся переменными i и / и потребуем, чтобы в момент завершения программы либо b[iy j]=x, либо, если первое невозможно, i=m. Точнее, выполнение программы должно установить состояние, в котором (15.1.5) /?:(0<1</иЛ0</</1Л*«=&[*, j])V (1**тЛх$Ь) В заданном ниже при помощи диаграммы инварианте Р утверждается, что х не находится в уже проверенных строках b[i, 0 : i—1] и в уже проверенных столбцах b[i, О : /—1] текущей строки г. 0 / 1 и J /i-l здесь нет х (15.1.6) P-.O^i^m *0^j<n л т- Ограничивающая функция t — число значений в непроверенной части массива, т. е. (т—*)*п—/. Определите в качестве первого шага построения алгоритма команду задания начальных значений для создаваемого цикла. Очевидный выбор — это i, j :=0, 0, так как тогда кусок массива, в котором «нет х», пуст. А теперь скажите, какова должна быть охрана В цикла? Выражение ~] В должно удовлетворять условию Р Л 15=>/?. Оно должно быть достаточно сильным для того, чтобы мог быть установлен любой из дизъюнктивных членов R. Чтобы обеспечить первый дизъюнктивный член, выберем i < m cand x = b[i, /]; чтобы обеспечить второй дизъюнктивный член, выберем *' = т. Чтобы быть уверенным в том, что первое выражение определено, нужна именно операция cand, так как b[i, /] может оказаться неопределенным при i^m. Следовательно, в качестве ~~\В выберем "IВ: i = т V (i < т cand х = b [it /]) Используя законы де Моргана, найдем его отрицание, т. е. Вифт /\(i^m cor х ф b [*', /]) Так как охрана В должна вычисляться лишь тогда, когда истинен инвариант Я, что, в частности, означает, что истинно /^т, ее можно упростить до В-Афт Д(/а=т cor хфЬ[1, /'])
Гл. 15. Построение циклов ' 187 и наконец до В'Лфт cand хфЬ\1, /] Итак, последняя строка и является охраной цикла. Следующий шаг — определение тела цикла. Сделайте это до того, как продолжить чтение. Назначение тела цикла — уменьшить ограничивающую функцию /, которая задает число элементов непроверенной части массива: (т—i)*n—/. Из условия Р/\В, при котором выполняется тело цикла, следует, что Km, \<л и хфЬИ, /], так что элемент b[i, /1, находящийся в непроверенной части, может быть перемещен в проверенную часть. Возможной командой для выполнения этого действия является / :=;/+1, но она сохраняет инвариант Р лишь при /<д—1. Итак, мы имеем охраняемую команду j<n— 1-*/ :=/+1 Что делать, если />п—1? В этом случае, поскольку инвариант Р истинен, имеем /=л—1. Следовательно, необходимо определить, что делать в случае, когда j=n—1, т. е. когда bliy /] — последний элемент своей строки. Чтобы переместить Ш, /] в проверенную часть, требуется перейти к началу следующей строки, т. е. выполнить /, / :=Н-1, 0. Итак, тело цикла—это if / < лг — 1 —^ / := /+1 D /-л —1-W, / :- Hi, 0 II Следовательно, программа имеет вид (15.1.7) *, / : = 0, 0; do гфт cand хфЬ\г% /]—+ И/</i—1-*/ := /+1 Q /«^г—1 —^ t, / := /Н- 1, Ofi od При желании тело цикла можно перестроить, получив *\ i := 0, 0; do гфт cand хфЬ[1, /]—■* / ••= Ж; if / <n—+skip □ j = n—-м, / := i+l, 0 fi od Обсуждение J - Отметим, что на самом деле необходима операция cand (а не Л). Обратите внимание, что при построении тела цикла использовался, хотя и неформально, метод построения команды выбора. Сначала i
188 Часть III. Построение программ была выбрана команда / := /+1 и было показано, что она работает так, как нужно, лишь при /</г—1. Формально необходимо было бы доказать (Pf\B/\j<n-\)=zwp(»j :- /+Г\ Р) но данный случай достаточно прост для того, чтобы работать неформально, если быть достаточно аккуратным. Затем была выбрана команда t, / : = t + l, 0 для работы в оставшемся случае, когда j=n. Заметим, что команда выбора имеет охраны ]<jt—1 и \—п—1, а не j<n—1 и у^п—1. В команде выбора, для того чтобы обнаружить ошибки, охраны были сделаны настолько сильными, насколько возможно в соответствии с принципом (14.10). Другое решение этой задачи будет построено в разд. 15.2. Упражнение к разд. 15.1 1. Постройте другую программу для первого примера этого раздела. На этот раз воспользуйтесь следующими инвариантом и ограничивающей функцией: Р:0<*<я Л s = (2/:<</ < n:b[j]) ; t:i 2. Инвариант цикла во втором примере задан при помощи диаграммы (см. (15.1.6)). Замените диаграмму эквивалентным предложением языка исчисления предикатов. 3. Напишите программу, которая при данном фиксированном массиве b [0:n— 1], где п > 0, придает х наименьшее значение из Ь. В случае если наименьшее значение встречается в b более одного раза, программа может быть недетерминированной. Предусловие Q, постусловие R, инвариант цикла Р и ограничивающая функция t следующие: Q: 0 < п R: *<6[0:л—1] л (з/:0</ < п:х = ЬЦ]) Р: 1</<л Л *<&[0:/—1] Л (з/:0</< i:x=b[i]) t: n — i. 4. Напишите программу для задачи из упр. 3, пользуясь следующими инвариантом и ограничивающей функцией: Р: 0<i < п Л x<b[i:n—\] Л (з/-''</ < п:х=Ь [/]) /: / 5. Напишите программу, которая при данном фиксированном п > 0 присваивает i наивысшую степень двойки, не превосходящую п. Предусловие Q, постусловие R, инвариант цикла Р и ограничивающая функция t следующие: <?: 0 < п R:0<i^n<2*iA {^p:i = 2P) Р: 0 < /<л Л (ЗР:*=2/0 /: n — i 6. Переведите программу (15.1.7) на любой выбранный вами язык — ПЛ/1, Паскаль, Фортран и т. д. Помните о необходимости операции cand. Сравните получившуюся программу с (15.1.7),
Гл. 15. Построение циклов 189 15.2. Приближение цикла к завершению Сортировка четверки Рассмотрим следующую задачу. Напишите программу, которая упорядочивает четыре целые переменные: qO, ql, q2y q3, т. е. в момент завершения должно быть истинно q0^ql^.q2^q3. Неявно предполагается, что имеющиеся значения переменных должны переставляться; например, qO, ql> Ф2, q3: = 0i О, 0, 0 не является решением, хотя оно и устанавливает q0^ql^q2^iq3. Чтобы выразить это явно, воспользуемся идентификаторами Qi для обозначения начальных значений qi и запишем формальную спецификацию: Q: q0=Q0Aql=DlAQ2=Q2AQ3=Q3 R: q0^ql^q2^q3 А перестановка ((qO, qU q2, q3), (QO, QI, Q2, Q3)) где второй конъюнктивный член перестановка (...,...) означает, что в четырех переменных qO, ql, q2y q3 содержится перестановка их начальных значений. Напишем цикл. Его инвариант отражает тот факт, что в четырех переменных всегда должна содержаться перестановка их начальных значений: Р: перестановка ((qO, ql, q2, q3), (QO, QI, Q2, Q3)) Ограничивающая функция — это число инверсий в последовательности (qO, ql, q2, q3). Число инверсий в последовательности (<7<ь •••, Qn-i) — это число таких пар (qt, qj), *</, которые идут не по порядку, т. е. q{>qj. Заметим, что при этом определении считаются все пары, а не только пары соседних значений. Например, в (1, 3, 2, 0) число инверсий равно 4. Итак, ограничивающая функция — это t : (Ni, J : 0<*</<4 : qi>qj) В инварианте указывается, что в четырех переменных всегда должна содержаться перестановка их начальных значений. В исходном состоянии это условие, очевидно, истинно, так что инициализация не требуется. В предыдущем разделе на аналогичной стадии построения цикла определялась охрана цикла. Вместо этого здесь мы будем искать несколько охраняемых команд, каждая из которых приближает цикл к завершению. Инвариант означает, что возможными командами являются лишь перестановки значений двух или более переменных. Для упрощения будем рассматривать лишь перестановки двух переменных. Есть шесть возможностей таких перестановок: qO, ql : = ql и qO, ql, q2 :=q2, ql, и т. д. А теперь учтем, что выполнение команды должно приближать цикл к завершению. Рассмотрим одну из возможных команд:
190 Часть III. Построение программ q0, ql :=qly qO. Она уменьшает число инверсий в (q0y ql, q2, q3)y (q0y ql, q2, q3), если и только если q0>ql. Следовательно, охраняемая команда будет иметь вид q0>ql ->- qO, ql :=ql, qO. Оставшиеся пять возможностей аналогичны рассмотренной, и, учитывая их все, получаем программу do qO>ql - qO, ql D ql>q2^ql9qZ Wq2>q3 -q2,q3 D q0>q2-+q09qZ D q0>q3 - q09q3 D ql>q3-+ql,q3 od = qL qo = q2, ql = q3, q2 = q2, qO = q3, qO - q3, qi Остается доказать, что в момент завершения устанавливается результат R — это п. 3 списка условий (11.9): Р/\ ~\ВВ =ф- R. Предположим, что все охраны ложны. Тогда q0^qlt (так как ложна первая охрана), ql^q2 (поскольку ложна вторая), q2^.q3 (поскольку ложна третья); поэтому q0^ql^q2^q3 Из этого неравенства и инварианта Р следует искомый результат. Но заметьте, что для установления искомого результата потребовались только первые три охраны. Поэтому, исключив последние три охраны, получаем программу do qO>ql -+qO, ql\- ql, q0 D q2>q3 ^q29q3:= q3, q2 Q q3>q4 - q3, q4:= q4, q3 od Обсуждение Использованный в данном примере подход можно кратко изложить следующим образом: (15.2.1) Стратегия построения цикла. Стройте охраняемые команды, создавая каждую команду таким образом, чтобы она приближала цикл к завершению, а соответствующую охрану так, чтобы обеспечить сохранение инварианта. Процесс создания охраняемых команд завершается, если их создано достаточно, чтобы доказать P/\~\BB=>R. Создание команд этим способом обеспечивает истинность пп. 2, 4 и 5 списка условий (11.9). В последнем предложении стратегии построения цикла отмечается, что построение цикла закончено, если истинен п. 3 списка. И конечно, возможно, потребуется написать команду инициализации, делающую инвариант истинным перед выполнением цикла (п. 1 списка).
Гл. 15. Построение циклов 191 В этой стратегии построения основной упор делается на пп. 2 и 4 списка условий (11.9), связанных с вопросами приближения цикла к завершению и сохранения инварианта. При подходе, использованном в разд. 15.1, упор делался сначала на доказательстве п. 3, о том, что после завершение истинен результат R. Обсудим, казалось бы, невероятный шаг: исключение из цикла трех охраняемых команд. Когда правильный цикл уже построен, иногда из него можно вывести более короткий и, возможно, более эффективный цикл. Каждая из охраняемых команд уже удовлетворяет пп. 2 и 4 списка условий (11.9). Усиление охран не может разрушить того, что эти пункты выполнены, так что при желании охраны можно менять усиливая их. Единственное, что при этом требуется,— убедиться, что в момент завершения цикла по-прежнему имеет место результат, т. е. по-прежнему истинно Р/\~]ВВ => R. Если без разрушения Р/\~]ВВ => R усилить некоторую охрану до F (ложь), то соответствующая команда никогда не будет выполняться и измененную охраняемую команду можно удалить. Это и имело место в рассмотренном примере. Для доказательства Р/\~\ВВ => R понадобились только первые три охраны, так что последние три можно было усилить до F и затем удалить. К этим вопросам мы возвратимся в гл. 19, где будет рассматриваться эффективность программ. Построенная нами маленькая программа будет недетерминированной при выполнении, поскольку в одно и то же время могут быть истинны две и даже три охраны. Но для любого начального состояния есть в точности одно конечное состояние, так что в терминах результата наша программа детерминирована. Число повторений цикла равно числу инверсий, которое не больше шести. Поиск в двумерном массиве. Вновь рассмотрим задачу, обсуждавшуюся в разд. 15.1,— написать программу для нахождения числа в двумерном массиве. Единственным отличием в постановке задачи будет то, что теперь массив может быть пустым (т. е. может иметь 0 строк или 0 столбцов). Наш фиксированный массив есть МО : т—1, 0 : п—1], где tri^O и п^О; в нем нужно найти значение фиксированного целого числа х. Воспользуемся переменными i и /'. В момент завершения должно быть либо Ш, /]=*, либо, если это невозможно, i=m. Точнее, должно быть установлено R: (15.2.2) Д:(0<1</пЛ0</</1Л*=»5[1\ /]) V (i =*т Л х^Ь) В инварианте Я, заданном ниже при помощи диаграммы, утверждается, что х не находится в уже проверенных строках МО : i—1]
192 Часть 111. Построение программ и в уже проверенных столбцах Mi, 0 : /—1] текущей строки i: О J /i-l о| (15.2.3) Р: 0<i<m Л0<7<п л / wi-1 здесь нет х Ограничивающая функция является суммой числа значений в непроверенной части массива и числа строк в непроверенной части: /= (т—i)*n—j+m—i. Требуется дополнительное значение m—i, поскольку, возможно, j=n=0. В качестве первого шага построения определите команду задания начальных значений для создаваемого цикла. Очевидный выбор — это t, / :=0, 0, так как тогда кусок массива, в котором «нет я», пуст. Обратите внимание на то, что в инвариант входит /<>г, а не j<in. Это необходимо, поскольку число столбцов п может быть нулем. Теперь должны быть построены охраняемые команды цикла. Какова простейшая возможная команда и какова подходящая для нее охрана? Очевидно, можно попробовать применить команду / := /+1> поскольку она уменьшает t. (Другая возможная команда i : = /+ 1 рассматривается далее.) Подходящая охрана должна обеспечить сохранение истинности Р. Формально или неформально можно убедиться в том, что может быть использовано условие гфт Л \Фп cand хфЪ\[, /], так что охраняемой командой будет [фт Л / фп cand хфЬЦ, j]—+j := / + 1 Заметим, что охрана сделана настолько слабой, насколько это возможно. Но решает ли цикл с одной этой охраняемой командой задачу? Почему решает или почему нет? Если не решает, какой другой охраняемой командой можно воспользоваться? Цикл с одной такой охраняемой командой может завершиться при i<Jn/\j=n, а этого даже вместе с инвариантом не достаточно для доказательства R. Конечно, если в первой строке Ъ не содержится х, то цикл закончится после просмотра только первой строки! Нам нужны охраняемые команды, которые производят увеличение /. Команду i := t+1 можно выполнить лишь при i<jn. Более того, она может сохранить истинность Р только тогда, когда в строке / не содержится х, так что рассмотрим ее выполнение лишь при добавочном условии }=п. Но это означает, что заодно с присваи-
Гл. 15. Построение циклов 193 ванием i надо сделать / нулем, чтобы сохранилось условие на текущую строку /. Это приводит к программе (15.2.4) U \ := 0, 0; do гфт f\ \фп cand х Ф Ь [iy j]—+j : = / + 1 -+i> j '- = t + 1, 0 Q 1фтА1 = п od Остается показать, что в момент завершения R истинно, т. е. что Р/\ ~]BB=>R. Предположим, что охраны ложны. Тогда возникают два случая. Во-первых, может быть i=m. Во-вторых, если 1фт, то из ложности второй охраны следует \фп\ поэтому из ложности первой охраны следует x=b U, /]. Таким образом, охраны ложны при условии, если истинно следующее предложение, которое вместе с инвариантом Р влечет результат R: i = m cor (i фт f\ \фп f\ x = b[iy /]) Следовательно, программа правильна. Заметим, что в случае i=m из инварианта следует, что х не находится в строках от 0 до т—1 массива 6, а это означает, что х^Ь. Обсуждение Этот цикл был построен путем последовательного построения простых охраняемых команд, приближающих цикл к завершению до тех пор, пока не оказывалось Р/\ ~]BB=>R. Этот процесс привел нас к циклу совершенно другого вида, чем те, которые обычно строят программисты (частично это происходит из-за того, что большинство из них незнакомы с охраняемыми командами). Им требуется некоторое время, чтобы начать использовать цикл (15.2.4) для поиска в двумерном массиве. Рассмотренная задача часто используется для того, чтобы обосновывать включение в традиционные языки программирования операторов goto или выходов из цикла, поскольку, если не использовать дополнительную переменную, обычно называемую признаком, для решения задачи на обычных языках требуется два вложенных цикла и «выход» из внутреннего. (15.2.5) i, / := 0, 0; while i Фт do begin while \фп do if x = b[i, j] then goto loopexit else / := /+1; i, j := f+1; 0 end; loopexit: 7 Д. Грио
194 Часть III. Построение программ Теперь видно, что понятие охраняемой команды и применяемые здесь методы построения программ приводят к решению, которое оказывается проще и понятнее, конечно при условии, что читатель понимает применяемую методологию. Как может эффективно выполняться программа (15.2.4)? Оптимизирующий компилятор мог бы проанализировать охраны и команды и определить пути исполнения, приведенные на диаграмме (15.2.6). (15.2.6) I, / := 0, 0; do i < т Л / < п cand х Ф Ь [i, j] —> / : = / + 1 Q i<m л/ = я—**', / := i + \f 0 od На диаграмме стрелки с F(T) обозначают пути, выбираемые в том случае, если терм, из которого они исходят, ложен (истинен) *. Но (15.2.6) является, в сущности, блок-схемой для программы (15.2.5)! По крайней мере в этом случае программа «высокого уровня» (15.2.4) может быть промоделирована с использованием конструкций «более низкого уровня» (15.2.5), применяемых в Паскале, Фортране или ПЛ/1. Программа (15.2.4) создана, исходя из разумных принципов, программу же (15.2.5) обычно строят при помощи придуманных для данного случая искусственных приемов, используя построение на основе тестовых примеров; в результате часто возникают сомнения в том, все ли случаи были рассмотрены. Упражнения к разд. 15.2 1. Напишите программу для решения следующей задачи. Дан фиксированный трехмерный массив с[0:т—1, 0:п— 1, 0:р—-1], где т, я, р^О. Дана фиксированная переменная х. Найдите, пользуясь тремя переменными i, j, £, значение с [I, /, к], равное х\ если "]х£с, то присвойте i значение т. 2. Напишите программу, которая по данным фиксированным целым числам X, Y (X > 0, Y > 0) находит их наибольший общий делитель gcd (X, Y). Наибольшей общий делитель чисел X и У, больших 0, это наибольшее целое число, являющееся делителем их обоих. Например, gcd(l,- 1)=1, gcd (2, 5) —1, gcd (10, 25) = 5. При х Ф 0, у Ф 0 имеют место следующие свойства: gcd(x, y)=gcd(x, у—х)^ gcd (x-у, у) gcd(x, y) = gcd(x, x+y) = gcd(x+yt y) gcd (xt x) = x gcd(x, y) = gcd(y, x) gcd(x, 0) = gcd(0, x) = x Первые две строки выполняются потому, что любой делитель х и у является также делителем х-\-у и х—у, так как (х ± y)/d = x/d ± y/cl для любого делителя d чисел х и //. *> О ужас! Стрелки на диаграмме не были проведены! Проведите их там, где нужно. Замените все < на ф. Я теперь знаю, что любое использование диаграмм должно быть объявлено незаконным!
Г л, 15. Построение циклов 195 В искомой программе результатом должно быть утверждение R:x = y = gcd(X, Y) Программа не должна использовать умножение или деление. Она должна быть циклом (с инициализацией) с инвариантом Р:0 < х АО < у A gcd(x, y)=gcd(X, Y) и ограничивающей функцией t:x-\-y. Воспользуйтесь приведенными выше свойствами для построения охраняемых команд цикла. 3. Переделайте программу из упр. 2 для определения наибольшего общего делителя трех чисел X, Y, Z, больших 0. 4. Напишите алгоритм для определения gcd(X, Y) при Х-9 К^О, пользуясь умножением и делением (см. упр. 2). Например, можно вычитать из у кратное х. Постусловие, инвариант и ограничивающая функция следующие: R: x = 0 Agcd(X, Y)=y Р: 0<*лО<*/Л(0, 0)9*(*. у) Agcd(x, y) = gcd(X, Y) t: 2 *x-\-y 5. Эта задача касается той части транслятора или любой другой программы для обработки текстов, которая выделяет следующее слово или последовательность символов до первого пробела. Символы b[j:79] содержат ту часть прочитанной строки Ь [0:79], которая "еще не обработана"* а новую строку можно прочитать в Ь, выполнив команду read(b). Входные строки содержат по 80 символов. Известно, что b [/:79], соединенное g остальными входными строками* дает последовательность WT —'I REST где | обозначает соединение строк, а — означает пробел; W — непустая последовательность символов, не являющихся пробелами, a REST— строка символов. Задача программы — "обработать" входное слово Wy удаляя его со ввода и помещая в массив символов s. Известно, что W достаточно коротко, чтобы поместиться в s. Например, верхняя часть следующей диаграммы описывает образец начальных условий при 10-символьных строках. На диаграмме приведены соответствующие конечные условия: W: 'WORD' REST: 'NEXT-ONE-IS-IT—' \e-IS-IT J 6[/:79]: 'WO' вводятся RD -NEXT-ON Начальные условия b{j:19]: -NEXT-ON' вводятся \E-IS-IT 1 s[l:t]: 'WORD' Конечные услоция конечные условии Ищется цикл с инициализацией. Предусловие Q, постусловие /?, инвариант Р и ограничивающая функция t следующие: Q: 0</ < 80д b[j:79]\ вводный файл = № |'-Ч REST R: 0</ < 80 л s[0:length(W)-l} = W A (Ь Ц:79] | вводный файл) = ' —' | RFST Р: 0</<80 л 0^v^length(W) A (s [0:v- 1] | b [j:79]) = {W |'—' | REST) t: 2* length (W)— 2*t>+/
Глава 16 ПОСТРОЕНИЕ ИНВАРИАНТОВ Предположим, что нужно построить программу S, удовлетворяющую условию {Q} S {R} при данных Q и R, и что уже решено использовать цикл (возможно, вместе с какой-то командой инициализации). Как перед написанием цикла найти для него подходящий инвариант и ограничивающую функцию? В данной главе исследуется именно этот вопрос. В разд. 16.1 показывается, что инвариант можно рассматривать как ослабление постусловия R, и рассматриваются различные способы того, как можно ослабить постусловие. Этот материал вновь подтверждает то, что программирование — целенаправленная деятельность. В разд. 16.2—16.5 подробно обсуждаются способы ослабления результирующего утверждения и техника преобразований демонстрируется на нескольких примерах. 16.1. Теория воздушного шарика В данном разделе дается некоторое представление о сущности инварианта Р цикла {Q} do B-+S od{R}. На рио. 16.1.1(a) показано множество всех состояний, в котором выделено подмножество, Is} [r] (a) (b) Рис. 16.1.1. Надувание воздушного шарика. соответствующее постусловию R. Выделено также множество возможных начальных состояний /5, которых можно достичь при помощи некоторых простых присваиваний. (На самом деле IS и R могли бы и пересекаться, но на рисунке это не показано.) Инвариант Р—это предикат, который истинен перед каждым шагом цикла и после него. Следовательно, как показано на рис. 16.1.1(b), множество состояний, удовлетворяющих Р, должно содержать как множество возможных начальных состояний, представленное IS, так и множество конечных состояний, представленное R.
Гл. 16. Построение инвариантов 197 Представим, что R — состояние воздушного шарика, из которого выпущен воздух, а Р — состояние полностью надутого шарика перед началом выполнения цикла. При каждом выполнении шага цикла из шарика будет выпускаться немного воздуха, до тех пор пока при выполнении последнего шага шарик не вернется в спущенное состояние R. Это показано на рис. 16.1.2, где Р=Р0 — шарик перед первым шагом цикла, Pi — шарик после выполнения первого шага, Р2 — шарик после выполнения второго шага. Г/у] Ql Р2 • » • Р\ щ Рис. 16.1.2. Выпускание воздуха из шарика. Замечание. Более точно наш шарик и состояния его уменьшения можно определить следующим образом. Р — полностью надутый шарик. Рассмотрим ограничивающую функцию /. Пусть U — начальное значение /, коюрое определяется инициализацией, ti — значение / после первого шага, U — значение / после второго шага и т. д. Тогда предикат определяет множество возможных состояний шарика после /-го шага. Таким образом, выполнение инициализации «выпускает» часть состояний из шарика, оставляя лишь те, для которых ЯДО^ <;/</о, первый шаг «выпускает» еще часть, оставляя P/\0^t^ti и т. д. □ Вопрос, конечно, состоит в том, чтобы знать, как-надуть шарик так, чтобы выполнение цикла могло его опустошить полностью, т. е. как найти Р и ограничивающую функцию /? Какая информация нам доступна вначале? Ясно, что только результирующее утверждение R и множество исходных состояний IS. Так как шарик вначале пуст (состояние R) и должен быть надут до такой степени, чтобы включить исходное состояние, а затем уменьшиться опять до /?, похоже на то, что из имеющихся двух предикатов более важен R. Это становится еще более правдоподобным, если мы примем во внимание, что начальные условия могут даже быть не известны, пока не известно Р. Соответственно будут исследоваться способы, как надуть шарик — другими словами, как ослабить отношение,— до тех пор, пока шарик не включит в себя такое множество состояний /S, которое может быть легко построено.
198 Часть ///. Построение программ Ослабление предиката Имеется четыре способа ослабления предиката R. 1. Устранение конъюнктивного члена. Например, предикат А/\ В/\С можно ослабить до А/\С. 2. Замена константы переменной. Например, предикат л:^ ^Ш : 10], где х— простая переменная, может быть ослаблен до л;^Ы1 : i]/\ 1<^10, где I—новая переменная. Так как введена новая переменная, должно быть точно определено множество ее возможных значений ). 3. Расширение области значений переменной. Например, предикат 5<i<10 можно ослабить до 0<j<10. 4. Добавление, дизъюнктивного члена. Например, предикат А можно ослабить до А\/В, где В — некоторый другой предикат. Первые три способа довольно полезны. В каждом из них догадка, как ослабить R, вытекает непосредственно из формы и содержания самого /?, и в общем случае число возможностей, которые стоит испытать, невелико. Поэтому перечисленные методы могут подсказать способ направленного и упорядоченного преобразования предиката, который мы ищем. Четвертый способ ослабления предиката при всей своей общности редко оказывается полезным при программировании. Нет оснований пытаться добавить какой-то один член, а не другой, и, следовательно, добавление дизъюнктивного члена было бы задачей случайного выбора из бесконечного множества возможностей. Этот метод далее анализироваться не будет. 16.2. Устранение конъюнктивного члена В этом разделе рассматривается и иллюстрируется на примерах построение инварианта цикла путем удаления конъюнктивного члена требуемого результирующего утверждения. Приближенное значение квадратного корня числа Требуется написать программу, которая при данном фиксированном целом числе п^О установит следующее результирующее утверждение: (16.2.1) R:0<a2<n<(a+1)2 Извлекая квадратный корень из всех выражений в /?, находим, что /? -жвивалентно O^a^ Vn < а+ 1. Следовательно, а—наибольшее целое число, не превосходящее Vгп. *) Область значений вводимо* новой переменной должна включать область значений заменяемой переменной*
Гл. 16. Построение инвариантов 199 В качестве первого шага перепишем R в виде множества конъюнктивных членов: R: 0 < а2 Л а2 < п Л п < (а + I)2 Устранение третьего конъюнктивного члена R дает возможный инвариант: Р : 0<а2<я Так как я>0, то Р можно установить присваиванием а := 0. В качестве охраны цикла используем отрицание устраненного конъюнктивного члена, так что, когда цикл завершается, поскольку охрана ложна, устраненный член истинен. Это приводит к почти законченной программе а := 0; do(a+ l)2<ft —►Pod Назначение команды цикла—приближать его к завершению. Ясно, что если охрана цикла истинна, то а слишком мало, и приблизиться к завершению можно, увеличив а. Так как а ограничено сверху |/7г, возможной ограничивающей функцией является / = ]/п—а *). Выбрав самый легкий способ увеличить а—добавление единицы, приходим к программе (16.2.2) а := 0; do(a + 1)2<я —а := а+1 od Покажем, что Р—действительно инвариант цикла: = 0<(a+i)2<tt = wp("a : = a+l", P) Обсуждение Здесь была использована стратегия построения цикла (15.1.4) — сначала была создана охрана, а затем тело цикла. Охрана была создана таким простым и полезным приемом, который заслуживает названия стратегии: (16.2.3) Стратегия. Устраняя конъюнктивный член из R для получения инварианта, попытайтесь использовать отрицание устраненного члена в качестве охраны В цикла. Такой способ выбора В автоматически обеспечивает удовлетворение необходимого условия Р Л "15=>/? (п. 3 списка условий (11.9)). Упражнение 1 заключается в построении программы путем удаления второго, а не третьего конъюнктивного члена. Время выполнения программы пропорционально V~n. Более бысграя программа нахождения приближения к квадратному корню целого числа будет построена в разд. 16.3. *) Функция t не является целочисленной функцией,
200 Часть 111. Построение программ Линейный поиск В качестве второго примера устранения конъюнктивного члена рассмотрим следующую задачу. Дан фиксированный массив Ь[0 : т— 1], где т>0. Известно, что фиксированное значение х находится в МО : т—1]. Требуется написать программу, которая определяет первое вхождение х в Ь, т. е. записывает в переменную / значение наименьшего целого числа, для которого b[i]=x. Первый шаг к решению — более формально специфицировать программу. Это сделать легко; мы имеем следующие предусловие Q и постусловие R: Q:0< m ДхО[0:т- I] #:0<f <тЛ*€&[0:*— 1]Л* = Н(1 Более подробно R можно расписать следующим образом: Ц:0^1<тА(У1-0<1<1-хфЬ[1]) Ax = b[i] Теперь в R содержится три конъюнктивных члена. Какой из них нужно удалить, чтобы получить инвариант? Хороший инвариант должен быть легко находимым. Первые два конъюнктивных члена устанавливаются присваиванием i :~ 0, а в установлении третьего и заключается главная трудность. Следовательно, имеет смысл удалить третий конъюнктивный член, получая следующий инвариант: (16.2.4) Я:0 < i < т Л (VJ:0 < / < г.хфЬ [/]) Какой должна быть охрана цикла? Воспользуйтесь отрицанием удаленного конъюнктивного члена. Итак, программой является i := 0; do хфЪ[(\-+1 od Подберите команду для цикла и объясните, как она была найдена. Задача команды цикла — приближать его к завершению. Возможной ограничивающей функцией является t\m — i, которая всегда > 0 (см. инвариант), и очевидный способ ее уменьшить—увеличить i на 1. Очень легко показать, что выполнение i := /+1 при условии х Ф Ь [i] сохраняет истинность инварианта Р. Все это приводит к программе, хорошо известной под названием «линейный поиск»: (16.2.5) {Q} i :» 0; do x^b[t\->i := i+\ od{R}
Гл. 16. Построение инвариантов 201 Обсуждение Эта программа, конечно, правильна, однако попытаемся доказать ее формально, используя список условий (11.9). Во-первых, покажем, что в начале инвариант (16.2.4) истинен? wp(ui : = 0",(16.2.4))«0<0<тЛ*£Ь[0: —1] а это, конечно, следует из Q. Затем докажем, что (16.2.4) — на самом деле инвариант цикла. Для этого требуется показать, что (1Ь.2А)/\хфЬ[1]=*хир(и1 := i+V\ (16.2.4)) или 0</<т Л х^[0:/]=ф0</+1 <т Ax^b[0:i] Истинно ли это? Конечно, нет—посылка недостаточна для доказательства / + 1 < т\ Трудность состоит в том, что мы забыли включить в инвариант то, что х£Ь[0:т— 1]. Формально инвариант должен бы быть (16.2.6) P:0^i<mAx^b[0:i—l]/\x^b[Oim—l] После этого маленького изменения можно доказать формально, что программа правильна (см. упр. 5). Опуская конъюнктивный член x£b[0 ■ т—1], мы воспользовались правом математика опускать очевидные вещи. Заметим, что все три идентификатора, входящие в x£b[0 \ т—1], фиксированы во время линейного поиска: ху b и т не изменяются. Следовательно, факты, касающиеся лишь этих идентификаторов, не изменяются. Хочется надеяться, что читатель алгоритма и окружающего алгоритм текста будет помнить эти факты, чтобы не было нужды повторять их снова и снова. Позднее такие очевидные детали будут опускаться, если это не затрудняет понимания. Сейчас, однако, наша задача — приобрести опыт в работе с формализмом и с тем, как он используется в программировании, а для этого лучше быть настолько точными и аккуратными, насколько возможно. Нужно помнить также, что текст, окружающий программу в книге, подобной данной, редко окружает ту же программу, когда, как это и бывает обычно, она появляется на распечатке. Будьте предельно аккуратны при написании программы, чтобы представить программу при распечатке настолько ясно и полно, насколько sto возможно. Рассмотренная программа иллюстрирует важный, но часто забываемый принцип; (16.2.7) Принцип линейного поиска. Чтобы найти минимальное (не меньшее некоторой нижней границы) значение, обладающее некоторым свойством, проверяйте значения, начиная с нижней границы, в восходящем порядке. Подобным же образом при поиске максимального значения проверяйте их $ нисходящем порядке.
202 Часть 111, Построение программ Упражнения к разд. 16.2 1. Программа для нахождения приближения к квадратному корню была построена путем удаления из результирующего утверждения (16.2.1) конъюнктивного члена п < (а+1)2. Постройте другую программу путем удаления вместо этого члена члена а2 ^ п. Сравните времена выполнения этих двух программ (см. приложение 4). 2. Напишите программу, которая по данному фиксированному целому числу п > 0 находит наибольшее целое число, которое, во-первых, является степенью двойки и, во-вторых, не больше п. (Сначала выпишите формальную спецификацию, а затем выведите из нее инвариант удалением конъюнктивного члена.) 3. Напишите программу, которая по данным двум фиксированным целым числам х и */, удовлетворяющим условиям х > 0 и у > 0, находит частное q и остаток г от деления х на у, т. е. устанавливает 0<;г Л г^у л q *у-\-г = х. В программе нельзя использовать умножение и деление. Постройте инвариант цикла, устранив конъюнктивный член. 4. Напишите программу, которая по данному фиксированному массиву Ь. [0:т—1, 0:/г—1] и по фиксированному значению х в массиве b определяет «первое» вхождение х в Ь. Под «первым» понимается то вхождение х, при котором х нет в предыдущих строках или в предыдущих столбцах той же строки, т. е. программа должна, используя две переменные i и /, установить предикат # = 0<* < т Л0</ < п л x=b[i, j] л x$b[0:i— 1, 0:n— 1] Ax$b[i, 0:/—1] 5. При помощи списка условий (11.9) докажите, что программа (16.2.5) правильна. Воспользуйтесь инвариантом цикла (16.2.6) и ограничивающей функцией t\m—i. 16.3. Замена константы переменной Суммирование элементов массива Второй метод ослабления предиката, а именно замена константы переменной, проиллюстрируем на следующей задаче: написать программу, которая по данному фиксированному целому числу п>0 и фиксированному массиву целых чисел Ь[0 : п—1] находит сумму элементов Ь и записывает ее в переменную s. Результирующее утверждение R можно выразить следующим образом: (16.3.1) /?:s«(2/:0</</i:6[/]) Тот факт, что каждый элемент массива включен в сумму, означает, что нужно построить цикл некоторой формы, так что нужно ослабить R до подходящего инварианта Р. В R содержится постоянная п (т. е. п не может изменяться). Поэтому R можно ослабить, заменив п новой переменной i и получив s = (2/:0</<»:&[/]) В то же самое время нужно, однако, поместить / в соответствующие границы. Обоснованиями выбора границ являются необходимость
Гл. 16. Построение инвариантов 203 первоначального установления инварианта, а также вероятное конечное значение i (равное п). Предикат, приведенный выше, можно установить путем i, s := 0, 0, так что возможная нижняя граница i — это 0. Следовательно, выбирается область значений /, равная 0 ! п, и получается инвариант P:0<i</iAs = (2/:0</<r.6[/]) Для этой задачи была построена программа (15.1.1), использовавшая этот же инвариант и ограничивающую функцию t=n—i: i, s : = 0, 0; do i=£n—+i, s := * + l, s + b[i]od Обсуждение Для получения инварианта можно было бы заменить в R и две другие константы. При замене константы 0 получается инвариант 0<i</iAs = (S/:i</<«:6[/]) Используя этот предикат в качестве инварианта, можно построить цикл, который суммирует элементы Ь в убывающем порядке значений индексов (см. упр. 1 к разд. 15.1). Если записать результирующее утверждение следующим образом: s-(2/:0</</i-l:6[/D то для получения инварианта можно заменить переменной постоянное выражение п—1: -1</<А2-1Л5 = (2/:0</<г.Ь[/]) Обратите внимание на то, какова на этот раз нижняя граница к Поскольку п может быть нулем, массив может быть пуск Поэтому здесь не может быть использовано присваивание i, s := 0, МО], которое, казалось бы, наиболее подходит для инициализации цикла. Инициализацией должно быть if s := —1,0 (см. упр. 1). Из этого примера видно, что может представиться выбор, какую из нескольких констант заменить переменной. В общем случае константа выбирается таким образом, чтобы можно было легко установить получающийся инвариант, чтобы охрана (охраны) цикла была проще и, конечно, чтобы можно было легко написать команду (команды) цикла. Это — процесс проб и ошибок, но по мере накопления опыта он идет все успешней Слишком часто программисты вводят в программу новые переменные, не зная на самом деле, зачем они это делают и нужны ли в действительности эти переменные. В общем случае целесообразно следовать такому принципу: (16.3.2) Принцип. Вводите новую переменную лишь в том случае, если для этого есть серьезные основания.
204 Часть 111. Построение программ Сейчас у нас есть по крайней мере одно серьезное основание для того, чтобы ввести новую переменную: потребность ослабить результирующее утверждение для получения инварианта. Без лишних слов ясно, что каждая вновь введенная переменная должна быть соответствующим способом определена. Той частью этого определения, или описания, о которой часто забывают, является область значений переменной. Подчеркнем то, что эту область необходимо хорошо определять, посредством следующего принципа: (16.3.3) Принцип. Определяйте для каждой новой переменной подходящий диапазон изменения. Приближение к квадратному корню целого числа Рассмотрим в качестве второго примера замены постоянной на переменную следующую задачу Написать программу, устанавливающую при данном фиксированном целом числе п^О истинность: (16.3.4) R : а2^п<(а+1)2 Для этой же задачи в разд. 16.2 была построена программа путем удаления конъюнктивного члена п<(а+1)2. Построенная программа имела время выполнения, пропорциональное |/"/г. Теперь воспользуемся методом замены константы на переменную. Сначала попытаемся заменить выражение а+l новой переменной Ъ. Получается а2^п<Ь2. Ясно, что для того, чтобы этот предикат был истинен, b должно быть больше а. Более того, этот предикат может быть установлен, если выполнить a, b := 0, п-\-\. Следовательно, b заключено между а+1 ид+1, и инвариантом является предикат P:a<b^n+\ Aa2^n<b2 Охрана цикла, получаемая из условия Р Л 1В =>R, это а+\фЬ. Итак, программа имеет вид а, Ь := 0, п+ 1; do а + 1 Ф b —> ? od Поскольку в инварианте указывается, что а+1^£, а цикл завершается при a+l = by задача каждого шага — сближать а и Ь, т. е. уменьшать значение b—а. Выполнение должно продолжаться до тех пор, пока не выполнится b—а=1. Следовательно, b—а—1 есть возможная ограничивающая функция t. Можно было бы уменьшать длину интервала (а, Ь) на 1 на каждом шаге, но, вероятно, имеется более быстрый способ. Интервал можно делить пополам, присваивая а или b значение его середины (а+£)Ч-2. Если делать именно так, то команда цикла могла бы иметь вид (16.3.5) if?—^а := (а + Ь)~2[]?-*6 ;= (a + b)~2fi
Гл. 16. Построение инвариантов 205 Каждая команда должна сохранять инвариант Р. Чтобы найти для первой команды подходящую охрану, вычислим сначала (16.3.6) wp("a : — (а + Ь) + Т\ Р) = (a + b) + 2<b^n+lA((a + b) + 2)2<^nAb2>n Предусловием для (16.3.5) будет инвариант вместе с охраной цикла: РЛа+1фЬ Дополнительным условием, требующимся для того, чтобы из этой формулы следовало (16.3.6), является ((а + b) ч~ 2)2^п. Это условие мы и берем в качестве охраны первой команды. Подобным же образом находим, что охрана второй команды—это ((а + Ь) ч~ 2)2 > п. Вводя новую переменную d для экономии вычислений локальных значений, приходим к программе (16.3.7) а, Ъ := 0, п+1, {инвариант Р: а < Ъ ^ п -f-1 Л#2 ^ я < Ь2\ {ограничение t: Ь—а+1[ do а+\фЬ-+(1 :— (а + ft)-г-2; if d*d^n—+a := d[\d*d> n—* b := d fi od Обсуждение Может показаться, что прием деления интервала пополам появился неизвестно откуда. А это просто один из полезных приемов, которые обязан знать программист, так как их применение часто существенно увеличивает скорость выполнения программы. Время выполнения последней программы пропорционально log я, а программы, построенной в разд. 16.2, \^п. Программа (16.3.7) служит иллюстрацией еще одного основания для введения новой переменной: d ведена для локальной оптимизации программы. Появление d не только уменьшает количество вычислений выражения (а+&)-=-2, оно улучшает восприятие программы. Отметим, что для d не дано никакого описания. Переменная d, в сущности, является константой тела цикла. Ей присваивается значение при входе в тело цикла, и именно это значение используется повсюду в теле цикла. Она не передает значений от одного шага к другому. Более того, d используется лишь на двух последовательных строчках программы, и ее роль очевидна из этих строчек. Описание d было бы повторением прописных истин, и поэтому оно опущено. Подобную же программу можно построить, заменяя переменной второе вхождение а в (16.3.4) (см. упр. 3).
206 Часть Ш, Построение программ Задана о площадках Дан фиксированный, упорядоченный (по ^) массив Ь[0 • п—1], где п>0. Площадка массива — это последовательность равных значений. Написать программу для записи в переменную р значения, установливающего (16.3.8) Ri p является длиной самой длинной площадки в МО ! п— 1]. Удовлетворительную программу можно построить, и не определяя длину самой длинной площадки более подробно. Тем не менее в качестве первого шага к решению запишите (16.3.8) на языке исчисления предикатов. Значение р является длиной самой длинной площадки, если имеется последовательность из р равных значений и нет последовательности из /7+1 равных значений, т. е. в МО \ п—1] содержится площадка длины р/\ в МО i п—1] не содержится площадки длины р+1 Поскольку массив упорядочен, сегмент b[k \ /] является площадкой тогда и только тогда, когда его граничные элементы b[k] и b[j] равны *). Это наблюдение позволяет записать R на языке исчисления предикатов следующим образом: (16.3.9) /?:(з/г:0<6<Аг—p\b[k] = b[k+р— 1])Д (v k: 0 < k < п—р — 1: b [ k ] Ф Ъ [ * + р]) Единственной трудностью при написании (16.3.9) могло оказаться лишь получение верхних границ для k. В последующем будем работать с /?, записанным в виде (16.3.8), но возвращаться к более формализованному определению (16.3.9) в поисках подходящей догадки. Ясно, что для этой программы нужен цикл. Какой инвариант цикла вы выберете, учитывая тему этого раздела? Длина площадки в массиве длины 1, очевидно, равна 1. Следовательно, можно легко установить следующий инвариант, получающийся путем замены в R константы п новой переменной i: (16.3.10) Р: 1 ^i^n/\p — длина самой длинной площадки в 6[0:*-1]. *) То, что Ь [i — p:i] является площадкой, если и только если его крайние точки имеют равные значения, на самом деле серьезный результат. Программа становится проще, но доказательство становится более сложным. Рассмотрите это,
Гл. J6, Построение инвариантов 207 Каковы ограничивающая функция, инициализация и охрана цикла? Ограничивающая функция t=n—i. Инициализация цикла i, р := 1, 1. Охрана цикла 1фп. Какова команда цикла? При каждом шаге i должно увеличиваться, и, казалось бы, целесообразно увеличивать i на 1 (см., однако, упр. 10). Но это может потребовать изменения р для того, чтобы восстановить инвариант. Таким образом, мы рассмотрим две команды: i := i+l и i, p :== i+l, p+l. Определим условия, при которых выполнение первой из них сохраняет Р: wp("i :== /+1", Я) = 1 <* + 1 ОЛ/>—Длина самой длинной площадки в ft[0:i]. Первый конъюнктивный член следует из охраны цикла. Какие дополнительные условия нужны для получения второго конъюнктивного члена? Из Р уже известно, что р — длина самой длинной площадки в ЫО : /—1]. Поэтому р является длиной самой длинной площадки в МО : *'], если и только если b[i—р : i] не является площадкой. Обратившись к определению самой длинной площадки (16.3.9), устанавливаем, что это условие выполнено, если и только если b[i—р]фЫ(\. Из этого рассмотрения непосредственно получаются охраны обеих команд: i := i+l и i, p := i+l, /?+l, и тело цикла будет if b[i]^b[i—p]-+i:= i + l D Ь[(]фЬ[1—р]-+1, р:= i + l, p+l fi Окончательная программа—это (16.3.11) i, p : = 1, 1; {инвариант P: 1 <I I < n/\ p — длина самой длинной площадки в b[0:i — 1]} {ограничение t:n — i\ do i^n—>\\ b[i]=£b[i—/?]—+/ := i+l Q b[i"] = b[t-p]-+i\ P := i+l, P + l fi od
208 Часть III. Построение программ Обсуждение Вводить на слишком ранней стадии программирования дополнительную переменную v, содержащую текущее значение, принимаемое элементами последней из встретившихся самых длинных площадок,— обычная ошибка при построении этой программы. В этом случае нужно было бы проверять b[i]=v, а не b[i]=b[i—р]. Я сам совершил эту ошибку, когда в первый раз строил такую программу. Подобная ошибка приводит лишь к усложнению программы. Нужно следовать принципу (16.3.8) — вводить новую переменную лишь тогда, когда для этого есть серьезные основания. При определении, каково должно быть тело цикла, существенно помогло аккуратное определение (16.3.9) понятия длины самой длинной площадки на языке предикатов. Без него было бы слишком легко проглядеть простую проверку b[i]=b[i—р]. А это еще раз иллюстрирует полезность написания простых и ясных определений. Наша программа находит длину самой длинной площадки в любом, даже неупорядоченном массиве, лишь бы только равные элементы стояли подряд. Упражнения к разд. 16.3 1. Напишите программу для суммирования элементов массива Ь[0:п—1]. Результирующее утверждение — это /?:s = (2J/:0</<n—1:М/]) Инвариант найдите путем замены постоянной п — 1 на переменную. 2. Формально докажите, что тело цикла программы (16.3.7) и в самом деле уменьшает ограничивающую функцию (п. 5 списка условий (11.12)). Обратите внимание на то, что при выполнении тела цикла а+\ < Ь. 3. Постройте программу нахождения приближения к квадратному корню п путем замены второго вхождения а в (16.3.4) на Ь. При замене получается инвариант Не забудьте выбрать для b подходящие границы. Сравните получившуюся программу с (16.3.7) и трудность построения этих программ. 4. (Двоичный поиск.) Напишите программу, которая по данному фиксированному х и фиксированному упорядоченному (по^) массиву Ь[\:п], удовлетворяющему условию Ь[1]^я < Ь[п\, находит, где х можно вставить в массив, т. е. устанавливает для новой переменной i R:l</ < n/\b [i] < x < b [i+ 1] Время выполнения этой программы должно быть пропорционально log п. Написав эту программу, включите ее в программу для решения более общей задачи поиска: определить i, удовлетворяющее (/ = 0л*< b[\\) v (1<* < п Л b[i]^x <b\i ф 1J) у (1 = я Л Ь \п\^х) где значения х не ограничены.
Гл. 16. Построение инвариантов 209 5. Напишите программу, которая по данному фиксированному упорядоченному массиву Ь[0:п-~ 1], где п > 0, находит число площадок в Ь. 6. Напишите программу, которая по данному фиксированному массиву Ь[0:п — — 1], где п > 0, находит в Ь место некоторого максимального значения, т, е, устанавливает #:0<6 < п Л b[k\^b[0:n—\] Если максимальное значение встречается в Ь более одного раза, программа может быть недетерминированной. 7. Напишите программу, которая для данного фиксированного массива Ь[0:п— 1], где я^О, записывает в d число нечетных значений в Ь[0:п—1]. 8. Даны два фиксированных упорядоченных массива: f \0:т—1] и g[0:n—1], где т, п^О. Известно, что никакие два элемента / не равны и что никакие два элемента g не равны. Напишите программу для определения числа значений, встречающихся как в /, так и в g, т. е. устанавливающую k = (Ni, /:0</ < т Л0</ < я:/[/]=£[/]) 9. Напишите программу, которая по данному фиксированному массиву Ь\0:п—1], где я^гО, определяет, состоит ли Ь из одних нулей, т. е. устанавливает, используя новую булеву переменную s, /?:s = (v/:0</ < п:ЬЦ] = 0) 10. Напишите другую программу для нахождения длины самой длинной площадки в Ь[0:п— 1]. Ее алгоритм использует следующую идею: тело цикла на каждом шаге исследует одну площадку. Следовательно, инвариант цикла —это 0<:/^я Л р—длина самой длинной площадки в b[0:i—1] л (/ = ОСОГ *'=А2СОГ Ь [1—1J Ф Ь [I]) Воспользуйтесь тем, что длина самой длинной площадки в пустом массиве равна нулю. В данном упражнении иллюстрируется то, что не все инварианты цикла могут возникнуть путем непосредственного применения стратегий построения инварианта, рассмотренных в данной главе. На самом деле здесь был добавлен конъюнктивный член и, таким образом, чтобы получить другую программу, инвариант был усилен. 16.4. Расширение области значений переменной Иной взгляд на линейный поиск Следующий метод ослабления результирующего утверждения иллюстрируется на примере линейного поиска, который уже рассматривался. Написать программу, которая по данному фиксированному целому числу ti>0 и массиву МО : п—1], про который известно, что в нем содержится значение х, находит первое вхождение х в Ь. Обозначим наименьшее /, удовлетворяющее условию 0^'Л*= =b[i]y через iv. Из условия задачи известно, что iv существует. Тогда результирующее утверждение программы можно следующим образом записать, используя ii R : i=iv Принцип линейного поиска говорит, что поиск значения i9 удовлетворяющего /?, должен производиться в порядке возрастания
210 Часть III. Построение программ значений, начиная с наименьшего. Таким образом, инвариантом цикла будет Р : 0<i<w Тогда цикл можно записать следующим образом: i := 0; do хфЬШ -+ i := i+1 od {i=iv} Обсуждение Метод, использованный для построения инварианта,— это расширение области значений переменной. В R переменная i могла иметь лишь одно значение iv. Эта область значений расширяется до множества {0, 1, . . ., й>}. В данном случае расширение получилось путем ослабления отношения i~iv до i^iv и присоединения нижней границы i. Этот метод подобен последнему из рассмотренных — введению новой переменной и определению ее области значений,— но здесь оказалось так, что нужная переменная уже имеется в R. Рассмотренный пример иллюстрирует следующий важный (16.4.1) Принцип. Вводите имена для значений, которые должны быть определены. Иногда введение такого имени дает возможность действовать не столь формально, но не менее точно. Может оказаться, что довольно легко описать некоторое отношение на естественном языке, но не столь легко перевести его на язык исчисления предикатов; более того, описания на естественном языке может оказаться достаточно для того, чтобы обрести нужную идею. Но не пользуйтесь этим приемом как предлогом для того, чтобы полностью исключить из рассмотрения исчисление предикатов, так как это исчисление дает возможность более эффективно преобразовывать программы, которые мы создаем. Жулик на пособии Перейдем к рассмотрению второго примера, в котором оказывается полезным расширять область значений переменной. Пусть имеются три длинные магнитные ленты, содержащие списки имен людей, упорядоченные по алфавиту. В первом списке содержатся имена сотрудников Йорктаунского исследовательского центра IBM, во втором — студентов Колумбийского университета^, а в третьем — лиц, получающих пособие по безработице в Нью-Йорке. Практически все три списка бесконечны, так что не дается никаких верхних границ. Известно, что по крайней мере одно лицо фигурирует во всех трех списках. Написать программу для отыскания первого такого лица (т. е. чье имя стоит первым по алфавиту).
Гл, 16. Построение инвариантов 211 Чтобы понять сущность задачи, рассмотрим поиск в трех упорядоченных массивах (без верхних границ) / [0 : ?], g [0 ; ?], h [0 ■ ?] первого значения, которое находится во всех трех массивах; известно, что такое наименьшее значение существует. Те, кто не знакомы с методами, излагаемыми здесь, часто для решения подобной задачи пишут программу величиной от 10 до 30 строк текста на Фортране, ПЛ/1 или Алголе-68. Читатель также может захотеть построить такую программу полностью, еще до изучения нашего последующего построения. Каков первый шаг при написании программы? Выполните его. Первый шаг — это запись пред- и постусловий Q и R. Поскольку списки /, g и h фиксированы, будем пользоваться тем, что они упорядочены по алфавиту, но не будем упоминать этого в Q и R. Итак, Q — это просто Т. Используя iv, jv, ко для обозначения наименьших значений, удовлетворяющих условию / [iv]=g [jv]=h [kv], и три простые переменные /, /, k, постусловие R можно записать следующим образом: R: i=iv/\j=jv/\k=kv Обратите внимание, как тонко обошлись мы с задачей полного определения значений iv, jv, ко. Нам известно, что означает наименьшее, и мы надеемся обойтись без формального определения этих значений. Ну, а теперь ответьте, почему нужно использовать цикл? Постройте инвариант и ограничивающую функцию цикла. Программа должна производить поиск среди переменного числа величин в списке, и это предполагает использование команды повторения. Принцип линейного поиска (16.2.7) обусловливает поиск, идущий от начала списков. Расширение области значений трех переменных i, /, k приводит к инварианту Р \ 0<;<шЛ0</<Д>Л0<£<Ь Ограничивающей функцией является t=iv—i+jv—j+kv—k. А теперь, какова инициализация цикла и какие команды вы выберите, чтобы обеспечить приближение цикла к завершению? Инициализация — это i, /, k ■= 0, 0, 0. Простейшие способы уменьшить ограничивающую. функцию — это i := i+l, / := /+1 и k:=k+l. Вообще говоря, потребуется увеличивать все три пере-
212 Часть 111. Построение программ менные, так что можно предложить цикл следующего вида: (16 4.2) /, /, k : = 0, 0, 0; do?-W := i+1 □ ?-*£ := k+l od А теперь постройте подходящую охрану для команды i : = / + !. Имеем wp("i: = i + ln, P) = 0^i+l+ivA0<j<jvA0<k^kv Последние два конъюнктивных члена, а также 0^л+1 следуют из инварианта, так что из охраны должно следовать лишь H-l^ft;. Но /+1^ш не может служить охраной, поскольку в программе, возможно, не будет использоваться iv. Но отношение Н-1^до совместно с Р означает, что человек под именем f[i] не жулик, а это в свою очередь истинно, если / li]<Cg [i]. Таким образом, охраной может служить f [i]<Cg [/]. Другими словами, поскольку имя жулика не может стоять первым по алфавиту g [/], то, если / [i] стоит раньше g [/] по алфавиту, человек под именем / [/] не может быть искомым жуликом. Но точно так же охраной может служить / [t\<Ji Ik], и на время возьмем в качестве охраны дизъюнкцию этих двух предикатов: №<g[i]vf[f]<h[k] Другие охраны записываются подобным же образом, и получается программа (16.4.3) i, /, k : = 0, 0, 0; do f[i]<g[j]Vf[i]<h[k]-+i := <+1 U g[J]<h[k]Vg[i]<f[i]^j := /+1 D h[k]<f[i]Vh[k]<g[l]-»k := ft + 1 od Эта программа завершается, и в момент ее завершения Р истинно. Но мы еще не доказали, что в момент завершения программы имеет место искомый результат. Докажите это. Доказательство п. 3 списка условий (11.9) заключается в доказательстве того, что выполняется (16.4.4) РЛ1ВВ=>/? где Р — инвариант, ВВ — дизъюнкция охран, R—результирующее утверждение.
Гл. 16. Построение инвариантов *'** Предположим, таким образом, что охраны ложны. Рассматривая первые члены каждой охраны и предполагая, что они должны, имеем Следовательно, в момент завершения цикла имеем / [i] = g [/] = ~h[k] и R выполняется. Можно ли как-нибудь просто изменить программу, чтобы повысить ее эффективность? Заметим, что для доказательства (16.4:4) нужны лишь первые члены каждой охраны. Следовательно, вторые дизъюнктивные члены можно опустить, и получается программа (16.4.5) i, /, k :- 0, 0, 0; do f[i\<g[i\-+i := i+l D 8[t]<h[k]^j := / + 1 D h[k]<f[i\-+k := k + l od Обсуждение При построении программы для первой охраны сначала было построено / [7] <g[j], а затем ослаблено до / [i] <g[j]Vf[i] < h[k]. Почему оно ослаблено? Напомним, что в первую очередь нужно получить правильную программу, а во вторую очередь —эффективную. При доказательстве правильности одна из задач—доказать, что при завершении цикла выполняется (16.4.4). Чем сильнее~\ВВ, тем больше шансов доказать (16.4.4). Поскольку ~] ВВ является дополнением к ВВ, то чем слабее ВВ, тем больше шансов доказать (16.4.4). Таким образом, имеем следующий принцип: (16.4.6) Принцип. Чем больше охраняемых команд и чем слабее их охраны, тем легче может оказаться построение правильной программы. Конечно, этот принцип не может применяться шаблонно для построения сотен случаев. По-прежнему нужно сохранять простоту и стремиться сводить число случаев к минимуму. Рассмотрение программы с точки зрения эффективности позволило упростить охраны и получить программу (16.4.5). Этот вопрос будет подробнее рассмотрен в разд. 19.1.
214 Часть III. Построение программ 16.5. Комбинирование пред- и постусловий Иногда использование лишь одного из трех описанных до сих пор методов ослабления утверждений не дает подходящего инварианта цикла. Так может случиться, например, если сами входные переменные должны быть изменены и составляют часть результата выполнения. Таким образом, может потребоваться использовать комбинацию методов. Во многих случаях полезно помнить, что по теории воздушного шарика (разд. 16.1) как пред-, так и постусловие цикла влекут инвариант, и, следовательно, необходимо при построении инварианта рассматривать и то и другое. Можно ли представить как пред-, так и постусловие в одной и той же форме, чтобы инвариант можно было рассматривать как простое обобщение их обоих? Может ли инвариант рассматриваться как в некотором роде объединение пред- и постусловия? В этом разделе такой подход иллюстрируется на примере двух задач. Вставка пробелов Рассмотрим следующую задачу. Написать программу, которая для данного фиксированного п>0, фиксированного р^О и массива Ъ [0 : /1—1] прибавляет ри к каждому элементу Ь [i] массива Ь. Формально мы имеем (Bt представляют начальные значения Ь [i]): предусловие Q: (у':0<* < п:Ь[{] = В{) постусловие R: (у"0<*'<л:Ь[/] = Я, + р»0 Эта задача .возникла при написании программы для вставки пробелов между словами на строке с тем чтобы получить строку, выровненную по правому краю. Здесь Bt — номера столбцов, в которых начинаются последовательные слова в строке, /? — число пробелов, которые вставляются между каждой парой слов. После вставки пробелов первое слово будет начинаться в столбце В0у второе — в столбце Вг+р, третье — в Вг+2*р и т. д. Эта задача наталкивает на мысль об использовании цикла, в котором на каждом шаге изменяется одно Ь [i]. Чтобы получить инвариант, заменим сначала постоянную п переменной /: Р': 0</</1Л(У':0<-' </:й[0 = В/+Р*0 Р' утверждает, что первые / элементов b имеют окончательные значения. Но нужно заодно включить в инвариант и то, что оставшиеся n—j элементов сохраняют свои начальные значения; тогда полный инвариант — это Р: 0<у</1Л(У':0<'<Г'Н0в5/ + Р*')Л (Yi4<i<n:bli]==Bi)
Гл. 16. Построение инвариантов 215 Такое построение приводит к программе (16.5.1) / := 0; do \фп-+и b[i] := /+1» b[j] + p*j od Построение инварианта было двухшаговым процессом: на первом шаге постоянная была заменена переменной, а затем был изменен получившийся предикат, чтобы принять во внимание начальные условия. (По поводу дальнейшей работы с этим примером см. разд. 20.1.) Перестановка отрезков равной длины Следующая задача такова. Написать программу, которая для данного массива Ь [0 : т—1] с неперекрывающимися сегментами Ь [i : i+n—l] и Ь [/: j+n—l] (оба длиной 00) переставляет эти два сегмента. Например, если эти сегменты имеют значения, показанные ниже для Q, то по завершении программы они будут иметь значения, показанные ниже для R. На этих диаграммах X и Y обозначают соответственно начальные значения b [i : 0+n—1] и b [j : j+n—l]: Q: R: b b i i+n-l X[0:n-\] | i i+n-\ Y[0:n-l) | b b j j K[0: X[Q ./+/1-1 "-I] 1 j+n—\ n-\] | В дальнейшем построении будет использоваться менее формальный подход, который полагается на накопленную интуицию, не требующую всех формальных деталей. Будем считать, что должны изменяться лишь упомянутые сегменты и что они не перекрываются, и воспользуемся следующими диаграммами для пред- и постусловий («непереставлено» («переставлено») означает, что соответствующие значения в отмеченном сегменте являются начальными (окончательными)): j J+n-l Q- R: i i+n-l b |н.епереставлено| i i+n-l b ( переставлено^ А Ь непереставлено j+n-l л b переставлено Так как должны быть переставлены каждые элементы этих двух сегментов, напрашивается мысль о цикле, который будет переставлять за шаг по одному элементу каждого сегмента. Первый шаг
216 Часть III. Построение программ при нахождении инварианта ной k: Р:0<к<п л ъ замена постоянной п из R перемен- /+Л-1 переставлено Л Ъ переставлено Но в Р' не отмечено состояние элементов массива с индексами от i+k до i+n— 1 йот j+k до j+n—l. Соответствующая подгонка Р' дает инвариант Р, состоящий из конъюнкции предиката O^k^nAb с i /+Л-1 i+* /+л-1 У j+lc-l j+к j+n-l (16.5.2) |переставл| непереставл л b переставл непереставл Очевидной ограничивающей функцией является п—k9 и программа имеет вид k := О do кфп ->k, Ь [i+k], b [j+k] : = k+l, b [j+k], b iHk] od Для дальнейших целей (разд. 18.1) перепишем ее в виде процедуры (16.5.3). Просмотрите гл. 12 для того, чтобы вспомнить, если это необходимо, соглашения о параметрах и аргументах. (16.5.3) {Перестановка неперекрывающихся сегментов b[i:i + n — -1] *b[jii + n-\]\ ргос swapequalb (var b:array of Integer, value t, /, m integer); begin var k: integer; k := 0; {инвариант: см. выше, ограничение: n — k\ do ft=^/i-+M[i' + A]f &[/'+*]:=*+l,&[/+*]. &[i + £] od end Обсуждение И вновь инвариант был создан заменой константы в У? на переменную и добавлением затем конъюнктивного члена для того, чтобы отразить начальные условия. Чтобы избежать лишнего формализма и беспорядочных деталей, мы воспользовались диаграммами. Некоторые люди легче понимают картинки. Но, пользуясь ими, будьте особенно аккуратны, так как они могут привести к неприятностям. Слишком легко при этом забыть об особых случаях, например о том, что сегмент массива может быть пуст, а это может привести либо
Гл. 16. Построение инвариантов 217 к неправильной, либо к не столь эффективной программе. Чтобы исключить такие случаи, всегда аккуратно определяйте области значений новых переменных и убедитесь в том, что каждая картинка нарисована таким образом, что вам известно, что ее можно легко перевести на язык логики предикатов. Построение инварианта было двухшаговым процессом. Инвариант может быть построен также и следующим образом. Он должен быть следствием как R, так и Q (или же слегка измененной вследствие инициализации его версии), т. е. Q и R должны быть конкретизациями более общего предиката Р. В Q утверждается, что сегменты не переставлены; значит, в инвариант для каждой секции должна быть включена непереставленная подсекция, которая может составлять и весь сегмент. С другой стороны, R устанавливает, что сегменты переставлены; значит, инвариант должен включать для каждого сегмента переставленный подсегмент, который может совпадать и со всем сегментом. В результате приходим к тому, чтобы нарисовать диаграмму (16.5.2), пользуясь переменной k для того, чтобы отметить границу между непереставленным и переставленным сегментами. Упражнения к разд. 16.5 1. Определите формально пред- и постусловие для программы перестановки двух неперекрывающихся сегментов массива равного размера (не пользуясь картинками или диаграммами). 2. (Обращение массива.) Напишите программу, которая обращает сегмент массива b [*:/], т.е. если вначале b\i:j] = (Bi, Bi + li ..., В j), то при завершении программы 6[*:/] = (£у, ...,£/+1, В-). Предполагается, что i и / находятся в границах массива и что i<]-\-\. (Если « = / + 1, то сегмент массива пуст; это разрешено.) 3. Напишите программу, которая для данных фиксированного х, фиксированных тип, таких, что т > п, и сегмента массива b[m:n— 1] переставляет значения Ь и устанавливает значение переменной целого типа р таким образом, чтобы обеспечить R: т <р <л Л Ь т р—\р п— 1 sSx >дг Более формально выражаясь, если вначале было г [т:п~ \]=В [т:п — 1], то программа устанавливает R:m^p^nAb[m:p— 1]<* < Ь[р:п—\]Л перестановка (Ь, В) 4. (Разделение.) Напишите процедуру Partition (b, m, n, р), которая при данных фиксированных т и п, т < я, и массиве Ь[т:п — 1] с начальным значением В [т:п—1] переставляет элементы массива b и устанавливает значение р таким образом, чтобы достичь т р п~1 R: т<р<п г\регт{Ъ, В) Л Ъ \<В[т]\В[т]\ >В[т] Процедура Partition является небольшим видоизменением ответа к упр. 3. 5. (Голландский национальный флаг.) Дан массив Ь[0:п—\] при фиксированном я^О. Каждый из элементов этого массива окрашен в красный, белый или синий цвет. Напишите программу, которая переставляет элементы массива
218 Часть III. Построение программ таким образом, что все красные элементы идут первыми, а все синие- ними, т. е. устанавливающую - послед- красные белые синие Цвет элемента можно проверять при помощи булевых выражений: белый (Ь [ф, красный ф [/]), синий (b [i])t значения которых очевидны. Число проверок должно быть сведено к минимуму. Единственный способ перемещать элементы массива — менять местами два из них. Программа должна сделать не более п перестановок. 6. (Обращение связей.) Используются простая переменная р и два массива р[0:?] и s[0:?], содержащие последовательность значений V0, Уг, ..., Уп-ъ представленную в виде линейного списка: р V 1± S ^ ► V Vi S V 4-1 S -1 Это означает: 1) в v [р] содержится первое значение V0\ 2) при O^i^n—1, если v [k] содержит значение К/, то y[s[&]] содержит значение K/+i- 3) Если в v [k] содержится значение Vn-i> то s [k\ =—1. Никакого упорядочения расположения значений в элементах массива не предполагается. Например, то, что за V0 следует в линейном списке V\t не означает что у[р+1] содержит Vt. Напишите программу, обращающую связи, т. е. стрелки, заданные в массиве s. Массив v не должен изменяться, и после завершения линейный список должен иметь следующий вид: 7. Напишите формальные пред- и постусловия для задачи 6. 8. (Поиск седловой точки.) Известно, что фиксированное целое число х встречается в фиксированном двумерном массиве Ь [0:т—1, 0:п—1J. Далее известно, что каждая строка и столбец массива Ь упорядочены (по<;). Напишите программу для нахождения места х в Ь, т. е. программу, которая, используя переменные i и/, устанавливает x = b[i, /]. Если х встречается вЬ в нескольких местах, то не имеет значения, какое из мест найдено. Попытайтесь минимизировать число сравнений в наихудшем возможном случае. Задачи такого рода возникают при умножении разреженных полиномов, заданных при помощи упорядоченного списка пар (коэффициент, степень). 9. (Преобразование десятичного в двоичное.) Дана целая переменная х = Х, где X > 0. Напишите программу для вычисления целого числа k и массива v [0:&—-1], который задает двоичное представление X, где v[i] есть /-й бит двоичного представления, а самый старший бит v[k—\] не равен нулю. Значение х можно испортить. 10. (Десятичное в В-ичное.) Даны целая переменная х = Х, где X > 0, и целое В > 1. Напишите программу, вычисляющую целое число k и массиву [0:&—1], дающий представление X в системе счисления с основанием В. Здесь v [k— 1] — старшая цифра представления — не является нулем. Значение х можно испортить
Глава 17 ЗАМЕЧАНИЯ ОБ ОГРАНИЧИВАЮЩИХ ФУНКЦИЯХ Ограничивающая функция служит для двух целей. Во-первых, она используется для того, чтобы показать, что цикл завершается. Во-вторых, она дает верхнюю границу того, как много шагов может быть выполнено до завершения цикла, и, таким образом, может быть использована для приближенной оценки времени, требуемого для выполнения программы. Для одной и той же программы могут использоваться различные ограничивающие функции в зависимости от того, желает ли программист лишь доказать завершение или показать, что программа почти оптимальна или быстрее, чем другая. Например, рассмотрим программу (16.3.7), которая дает приближение к квадратному корню из целого числа: а, Ь : = 0, п-\-1; {инвариант: а < 6 ^ я Л я2 ^ п < Ь2} do а+\фЬ-+с1: = (а + Ь)+2-у if d*d^in—+a : = d[]d*d > п—>Ъ : =dfi od{a2<rc<(a+l)2} Для доказательства завершаемости программы использована ограничивающая функция Ь—а+1. А для того, чтобы показать, что эта программа на самом деле намного быстрее программы (16.2.3), которая совершает приблизительно Ь—а шагов: а : = 0; do (а+1)2<я -+ а • = а+1 od используется меньшая ограничивающая функция ceil (log ((b—а)). Сравнение скоростей выполнения разных операторов излагается вкратце в приложении 4. Обычно инвариант создаваемого цикла будет подсказывать ограничивающую функцию. Именно так было в большинстве программ, построенных в предыдущих главах, например при суммировании элементов массива (15.2.1), линейном поиске (16.2.3), в задаче о площадках (16.3.11) и в задаче о жулике на пособии (16.4.2). Однако приведем два приема, которые помогут-при поиске ограничивающих функций
220 Часть 111. Построение программ 0 i 1 0 j п-\ здесь нет л: Использование обозначения для задачи и ее решения Рассмотрим задачу из разд. 16.3 — поиск в непустом двумерном массиве Ь [0 : п—1, 0 • т—1] значения х. Инвариантом этого алгоритма был предикат Р: 0<i<m Л0^;<л л Так как х должен находиться в непроверенной части массива, возможная ограничивающая функция — это число элементов в непроверенной части массива которое равно (т—i) * п—/. Можно формально доказать, что эта функция и на самом деле является ограничивающей функцией цикла. Общая идея здесь следующая: (17.1) Стратегия. Выражайте ограничивающую функцию словами как простое свойство инварианта и задачи, а затем (если это необходимо) формализуйте ее как математическое выражение. Второй пример использования этой стратегии дан в задаче о сортировке четверки из разд. 15.2. Там требовалось переставить четыре переменные q0, ql, q2, q3, чтобы достичь состояния, в котором q0^ql^q2^q3. В качестве ограничивающей функции было выбрано число инверсий в последовательности (q0, ql, q2, q3). (Конечно же, незнание того, что такое инверсия, может привести к некоторым трудностям в начале работы.) Использование лексикографического упорядочения Рассмотрим пары целых чисел (i, /). Говорят, что пара (i, /) меньше, чем другая пара (h, k) (записывается (i, j)<C(h, k)), если либо i<fi, либо i^h/\j<Ck. Например, (—1, 5)<(5, 1)<(5, 2). Такой порядок называется лексикографическим упорядочением пар целых чисел. Он естественно распространяется на операции ^, >, ^. Он распространяется также на тройки, четверки и т. д. Например, (3, 5, 5)<(4, 5, 5) <(4, 6, 0) <(4, 6, 1) А теперь рассмотрим программу (17.2), чье единственное назначение — показать, как доказывается завершаемость цикла с ис-
Гл. 17. Замечания об ограничивающих функциях 221 пользованием лексикографического упорядочения. (17.2) {0</я Л О О} /, /: =ш— 1, п—1; do j^O _->/:в/_1 D *^°Л/«-0-м, /:«.f—1, az—1 od Исполнение обязательно закончится, так как 1) переменная i удовлетворяет условию 0^л<т, а / — условию 0</<л; 2) на каждом шаге пара (/, /) преобразуется в меньшую (лексикографическую) пару. Судя по п. 1, такое может произойти лишь конечное число раз. Но какая ограничивающая функция может быть использована для доказательства завершаемости цикла? По-видимому, она должна включать i и /', поскольку обе переменные уменьшаются. Однако во второй из охраняемых команд уменьшение на 1 сопровождается увеличением / на т—1. Чтобы уменьшение происходило всегда, нужно домножить i на коэффициент т. Следовательно, ограничивающей функцией является t i*m-\-j. На каждом шаге t уменьшается в точности на 1, так что / выражает в точности то число шагов, которые осталось выполнить. Мы изложили общую идею следующей теоремы, которая приводится без формального доказательства, поскольку она естественно вытекает из предшествующих рассмотрений. (17.3) Теорема. Рассмотрим пару (*, /), где i и /—выражения, содержащие переменные, использованные в цикле. Предположим, что на каждом шаге цикла (/, /) (лексикографически) уменьшается. Далее предположим, что i удовлетворяет условию mini^i^maxi, а / — условию minj^Lj^maxj при некоторых постоянных mini, maxi, minjy max]. Тогда выполнение цикла должно завершиться, и подходящей ограничивающей функцией для него является (i—mini) * (l+maxj—minj)+j—minj Подобное же утверждение имеет место и для троек (*', /, k)y четверок (i, /, k, I) и т. д. □ Если можно указать пару (или тройку и т. д.), удовлетворяющую условиям теоремы (17.3), то нет нужды выписывать ограничивающую функцию явно, если это не делает что-либо яснее или она не нужна по другим причинам. Приведем три примера. В разд. 15.2 была дана следующая программа для поиска в (воз-
222 Часть III. Построение программ можно, пустом) двумерном массиве: )0<тЛ0<л} I, /:== 0, 0; do {фт[\\Фп candх ФЬ[i, /]—*/:» /4-1 0^/пЛ/=п —W, /: = i + 1, 0 od {(0</</пЛ0</<я Лл' = 6[г, /])V(' = "*A*0)} Пара (i, /) вначале равна (0, 0) и увеличивается на каждом шаге. Следовательно, пара (т—iy п—/) уменьшается на каждом шаге. Далее, имеем 0<m—i^m и 0<я-—/<я. Следовательно, можно применить теорему (17.3), и цикл завершается. Ограничивающая функция, возникающая в результате применения теоремы, это (т—i) * (п+\)+п—/. В качестве второго примера рассмотрим программу сортировки четверки из разд. 15.2. Эта программа переставляет значения q0y qly q2y q3 таким образом, чтобы достичь состояния, где do q0> ql —* q0, ql : = ql, qO □ ql>q2-+qU q2:=q2, ql Uq2>q3-+q2y q3:=q3, q2 od На каждом шаге четверка (q0, ql, q2y q3) (лексикографически) уменьшается. Снизу она ограничена четверкой, чьи компоненты равны min(qOy qly q2y q3)y а сверху — четверкой с компонентами max(qOy qly q2y q3). Следовательно, цикл завершается. В качестве последнего примера рассмотрим задачу о железнодорожной сортировочной станции. На сортировочной станции находится некоторое число поездов, каждый из которых состоит из одного или большего числа вагонов. В результате применения алгоритма нужно удалить со станции все вагоны, но при условии, что за один раз можно удалять лишь один вагон. Это означает, что поезда нужно разбивать на меньшие поезда, и предлагается следующий алгоритм: do сортировочная станция не пуста—► выбрать поезд поезд U поезд содержит лишь один вагон -—► удалить поезд □ поезд содержит более одного вагона—* разбить поезд на два поезда И od Удаление поезда со станции уменьшает число поездов и общее число вагонов на станции. С другой стороны, разбиение поезда оставляет общее число вагонов тем же, но увеличивает число поездов на 1. Поэтому выберем пару (число вагонов на станции,— (число поездов на станции))
Гл. 17. Замечания об ограничивающих функциях 223 При каждом шаге цикла эта пара (лексикографически) уменьшается. Далее имеем О ^ число вагонов ^ начальное число вагонов — (начальное число вагонов) ^ — (число поездов) ^ О По теореме 17.3 цикл завершается. Упражнение к гл. 17 1. Найти ограничивающую функцию, применив теорему (17.3) к задаче сортировки четверки. Завершение этой программы доказано при помощи четверки (qO, ql, q2, q3).
Глава 18 ИСПОЛЬЗОВАНИЕ ЦИКЛОВ ВМЕСТО РЕКУРСИИ Процедура или функция рекурсивна, если она может быть вновь вызвана во время своего выполнения. Часто рекурсивные процедуры возникают из математических рекурсивных определений. Обычно приводимый пример — функция факториала п\у которая для неотрицательных целых чисел определяется следующим образом: 0!«1 п\=п*(п—1)! при п > О Обратите внимание, как п\ определяется через (п—1)!: это и есть рекурсивное определение. Это определение легко перевести в рекурсивную процедуру для вычисления п\: {Дано п^О, записать в ответ п\) ргос fac (value п: integer; result ответ: integer)] if n = 0 —► ответ : = 1 []я>0—► fac(n—1, ответ); ответ: = п*ответ fi Рекурсия полезна и, без сомнения, является одним из рабочих инструментов программиста. Например, синтаксический разбор сверху вниз с использованием рекурсивных процедур (иногда называемый нисходящей рекурсией) уже более десяти лет является моей излюбленной темой в курсах по построению трансляторов. В то же самое время рекурсивную программу, по крайней мере теоретически, можно записать при помощи циклов (и наоборот), а практически, возможно, имеет смысл поступать именно так. Быть может, пользоваться циклами вынуждает нас доступная нам система программирования, или, возможно, это необходимо ради экономии памяти и времени выполнения, или, быть может, и сам алгоритм легче выразим в форме циклов. Мы опишем на примерах ряд приемов, позволяющих выразить итеративно рекурсивные программы. Один из таких приемов — это с самого начала построения думать в терминах циклов. Это означает, что если программа будет составляться с помощью циклов, то их инварианты должны строиться до написания самих циклов (насколько это вообще возможно). Выбранная тема позволит нам обсудить две важные стратегии построения и рассмотреть соотношения между ними. Именно при-
Гл. 18. Использование циклов вместо рекурсии 225 менение этих стратегий часто порождает рекурсивные процедуры. Первая стратегия — это сведение решения задач к решению более простых, вторая — «разделяй и властвуй». Эти два старых метода по-прежнему могут быть полезны, если применяются осознанно, хотя они и не того уровня подробности и точности, как некоторые из рассмотренных ранее стратегий. В конце разд. 18.3 приводятся некоторые замечания о выборе структур данных при программировании и об использовании преобразований программ. 18.1. Сведение к более простым задачам Иногда мы просто не знаем, как приступить к решению задачи, а ранее разобранные методы, кажется, не помогают. В таких ситуациях может помочь следующая (18.1.1) Стратегия. Попытайтесь свести задачу к более простым. «Более простые» может в разных случаях пониматься по-разному. Задача может быть проще из-за того, что опущены некоторые ог~ раничения (это ее обобщение). Она может быть проще из-за того, что некоторые ограничения добавлены. Как бы ни была изменена задача, если это изменение приводит к решению более простой задачи, то, возможно, удастся, опираясь на эту более простую задачу, решить и исходную. Чтобы проиллюстрировать технику сведения, построим программу для решения задачи перестановки сегментов. Пусть даны фиксированные целые переменные т, п, р, удовлетворяющие условию т<я<р. Дана часть массива Ь [т:р— 1], рассматриваемая как два сегмента: т п р— 1 Q: Ь\В[т:п-1]\ В[п:р-1] где В обозначает начальное значение массива Ь. Программа должна поменять местами два сегмента массива, используя лишь постоянный (не зависящий от m, n и р) объем дополнительной памяти. Таким образом, устанавливается предикат т р—\ ) (18.1.2) R: Ь\ В[п:р-1] \В[т:п-\] С чего начать? Вспомним, что у нас имеется процедура (16.5.3) swapequals, которая меняет местами неперекрывающиеся сегменты одинакового размера. Быть может, и исходную задачу, включающую перестановку сегментов неравной длины, можно свести к этой более простой задаче. 8 д. грио
226 Часть III. Построение программ Итак, предположим, что сегмент Ь[т:п—1] больше, чем Ь[п:р—1]. Представим себе, что Ь[т:п—1] состоит из двух сегментов, первый из которых имеет ту же длину, что и Ь[п:р—1] (см. ниже диаграмму (а)). Тогда можно переставить выравненные по длине сегменты, содержащие хг и у, и получить диаграмму (Ь). Далее исходную задачу можно решить перестановкой двух сегментов, содержащих х2 и хг. Эти два сегмента могут быть неравной длины, но по крайней мере один из них меньше, чем тот, который был в исходной задаче, так что некоторый прогресс достигнут. (а) Ь (Ъ)Ь т х\ т У т-тр—п *2 т+р—п х2 п р—\ У п р—\ Х[ 1 (с)Ь (d)b т У т х2 п т -гр —п Х\ /7-1 *2 п т+р—п /7 — 1 х\ У Теперь предположим, что больше второй сегмент b In : р—l]. Тогда имеет место случай, рассмотренный на диаграмме (с), и можно воспользоваться процедурой swapequals для того, чтобы преобразовать ее в диаграмму (d). А теперь попытаемся воплотить эту идею в программу. Диаграммы (Ь) и (d) показывают, что после выполнения процедуры swapequals n всегда является левой границей самого правого из тех сегментов, которые должны быть переставлены. Но это было так и вначале. Следовательно, можно получить инвариант путем замены констант т и р на переменные, принимая во внимание начальные условия: т" уже переставлен переставить с Ь[п:к-\] переставить с b[h:n-l] /7-1 уже переставлен Заметим, однако, что в алгоритме требуется сравнение длин b [n:k— И и b [h:n— 1]. Кроме того, и для процедуры swapequals требуются длины сегментов. Следовательно, лучше представить в программе длины сегментов, чем концы сегментов. Тогда инвариант Р становится предикатом 0<O'<az—тД0</<р—nf\by рассматриваемым совместно со следующей диаграммой: т n-i П+j /7-1 уже переставлен переставить с b[n:n+j-l] переставить с b[n-i:n-ll уже переставлен Программу, пользуясь ограничивающей функцией i=max(i, /),
Гл. 18. Использование циклов вместо рекурсии 227 записываем следующим образом *); i, j:=n—m, р—п\ {Р\ do i^j—+\\ i> j—+swapequals(b, n—i, n> /); i:=i—/ П / > i—* swapequals (b, n — i, n + j—/, i)\\ : = /— 1 fi od swapequals (p, n—i, n, i) Обсуждение Эту программу можно было бы записать и рекурсивно: {Переставить сегменты Ь[т:п— 1] и Ь [п:р—1J, где т < п < р\ proc swap „sections (var b: array of integer; value m, ny p: integer); if n—m = /? — n—> swapequals (by m, n, p—n) □ n—m > p—n—* swapequals (py m, n, p—n)\ swap „sections (by m + p—n, n, p) [] n—m < p—n—* swapequals (b, m, n, n—m)\ swap „sections (b, m, я, m-\-p—/г) fi В данном конкретном случае мне больше нравится циклический вариант. Изобрести инвариант было нетрудно, и он для меня более понятен, чем рекурсия (а это не всегда так). В циклическом варианте требуются две дополнительные переменные i и /, которые не нужны в рекурсивном. Циклический вариант обладает тем тонким свойством, что при удалении из него всех вызовов swapequals получается программа (18.1.3) для вычисления наибольшего общего делителя исходных размеров массивов: gcd(n—m, p—п). Приятно было видеть, как из полезной, практической задачи программирования возникает хорошо знакомая, элегантная программа. *> Программа правильна, ко ее можно упростить: i, j :=п — т, р—п\ do i>j—> swapequals (b, n — /, /г, /); i: = i — j D />*—> swapequals (bt n — i, n-\-j — i, i); j:^j — i od; swapequals (bf n — i, n, i) 8*
228 Часть III. Построение программ (18.1.3) *> {т<п<р\ iy j :=n—m, p—п\ {inviO <i Л 0 < / Л gcd(n—m, p—n) = gcd(iy j)\ do 1ф\—* if i>j—+i: = i—/ П />* — /: = /—* fi od{i = j = gcd(n—m, p—n)\ Можно было бы построить программу, заменив сначала пир на переменные h и k, а затем определив, как уменьшить размер непереставленной части. Часто оказывается, что программу можно построить многими способами, и в действительности никто не может сказать, какой из них лучше. Можно значительно повысить свое мастерство и писать гораздо лучшие программы, если не бояться переделывать уже решенные задачи с использованием сформулированных принципов, постоянно спрашивая себя, почему же они не были использованы в первый раз. Следующая исповедь — именно об этом. Исповедь. Когда я впервые строил данную программу, я был отягощен своими старыми привычками и не последовал принципу, требующему вводить переменные, только если они необходимы, и искать инвариант путем замены констант на переменные. Я сразу же ввел четыре переменные, которые обозначали соответственно начала двух переставляемых сегментов и их длины. Один из студентов заметил, что одна из переменных не нужна, поскольку начало правого сегмента всегда было п. Это заставило меня остановиться и переделать построение так, как это показано выше, следуя на сей раз принципу (16.3.2) и вводя переменные лишь тогда, когда для этого были серьезные основания. Это и привело к наблюдению, что алгоритм gcd вложен в получившееся решение. Упражнения к разд. 18.1 1. Рассмотрите процедуру reverse (b, t, /), обращающую список значений Ъ [i:j] (см. упр. 2 к разд. 16.5.). Напишите программу перестановки прилегающих друг к другу сегментов, пользуясь той процедурой, которая решает более простую задачу. Из этого примера видно, что более простые задачи, используемые для решения данной задачи, может оказаться нелегко найти. Трудно дать методологию для построения любой программы. Некоторые программы получаются только путем привлечения новых идей, без которых решение вообще не может быть получено. 2. Числа Фибоначчи /„ определены следующим образом: /о = 0 /« = /»-i+/«-s Для п > 1 *> Программа правильна, но ее можно упростить (упрощенный вариант приведен в ответах к разд, 15,2),
Г л. It. Испфльзование циклош вмест§ рекурсии 229 Первые восемь чисел Фибоначчи суть 0, 1, 1, 2, 3, 5, 8, 13. Определение fn при п > 1 в матричных обозначениях может быть з#пиеано следующим образом: Довольно легко написать программу, которая вычисляет fn за время, пропорциональное п. Однако в следующей главе (разд. 19.1) приводится программа для возведения в степень in (n—положительное целое число) за время, пропорциональное log л. В этой программе i может быть и матрицей. Напишите программу для вычисления fn за логарифмическое время, пользуясь решением более простой (?) задачи о возведении в степень. 18.2. Разделяй и властвуй В предыдущем разделе рассматривалось решение задачи путем ее сведения к известной, более простой задаче. В данном разделе рассматривается родственная стратегия, о которой уже вскользь упоминалось. (18.2.1) Стратегия. Разделяй и властвуй В программировании эта стратегия часто используется в следующем смысле. Пытаемся разделить задачу на две или более меньших, подобных исходной задач. Если такое разделение может быть произведено, то тот же самый процесс, с помощью которого решалась большая задача, может быть выполнен для меньших задач. Если такое разделение может быть произведено без слишком больших затрат (при выполнении), то может быть построен эффективный алгоритм. Обычно такая стратегия приводит к разделению объекта пополам и обработке каждой его части тем же способом до тех пор, пока части не станут настолько малыми, что их можно будет обрабатывать непосредственно. Часто такой процесс приводит к логарифмическому множителю в формуле, описывающей скорость выполнения программы. Разница между стратегией (18.1.1) (решение задачи сведением к более простым задачам) и стратегией (18.2.1) (разделяй и властвуй) может быть невелика. Для некоторых задач выбор между ними зависит от того, с постановки какого вопроса начинается построение, а не от чего-либо другого. При стратегии (18.1.1) сначала отыскивается более простая задача и затем ставится вопрос, как можно эффективно ее использовать. Именно так и было при построении программы перестановки сегментов. При стратегии (18.2.1), напротив, вначале ставится вопрос о том, что значило бы разделить задачу на меньшие части, а затем ищутся способы сведения решения исходной задачи к решению ее частей. При стратегии (18.1.1) построение направляется более простой задачей. При стратегии (18.2.1) построение направляется еамой
230 Часть III. Построение программ идеей разделения, хотя она и может привести к использованию более простой задачи. Проиллюстрируем этот подход на примере построения программы Quicksort (быстрая сортировка) — одного из эффективных алгоритмов упорядочения массива. Даны фиксированное целое число гС^О и массив b [0 : п—1]. Массив нужно упорядочить. Если массив достаточно мал, например я<2, то для его упорядочения можно воспользоваться любым простым алгоритмом, например Прямо упорядочить Ь[0:п—1] при предположении я ^2: if пф2 cor &[0]<&[l]-*sfap Р я=2 cand &[0]>&[1]-> &[0], b[l] : = ф], &[0] Однако при п>2 должен быть применен более общий метод. Стратегия «разделяй и властвуй» предлагает нам выполнить сортировку путем упорядочения по отдельности двух (или более) сегментов массива. Пусть массив разделен следующим образом: 0 п-\ 9 ? Какое условие нужно наложить на два его сегмента, чтобы упорядочение их по отдельности привело к упорядоченному массиву? Каждое значение из первого сегмента должно быть не больше каждого значения из второго сегмента: 0 к п-\ (18.2.2) Ъ |<6[fc:/i-l]|^6[0:fc.--l] Это значит, что если значения элементов Ь можно переставить таким образом, чтобы обеспечить выполнение предиката (18.2.2), то для упорядочения массива остается лишь упорядочить части Ъ [0:6—1] и blk:n—ll На самом деле процедура, подобная той, которая устанавливает (18.2.2), уже написана (см. упр. 4 к разд. 16.5), так что будем е^ использовать. Процедура Partition разбивает непустой массив b [т : п—1] на три части, где х в середине получившегося массива — исходное значение b [ml: т п-\ (18.2.3) R: т <р <п Л Ъ <:Х X >х После того как массив разделен на две части, как показано выше, остается упорядочить эти части: b [т : р—1] и b lp+l : n—1].
Гл. 18. Использование циклов вместо рекурсии 231 Если они достаточно малы, их можно упорядочить прямо. Если не так, их можно упорядочить, вновь разделяя их и упорядочивая меньшие подчасти. Пока производится упорядочение одной из под- частей, границы другой необходимо где-нибудь запомнить. Но упорядочение одной части может породить две меньшие части, которые также нужно будет упорядочивать, и их границы также нужно где-то запоминать и т. д. Чтобы хранить последовательность частей, которые нужно упорядочить, воспользуемся переменной для множеств s, которая будет содержать их границы, т. е. является множеством пар целых чисел, и если (*', /) находится в s, то Ь И : /] еще нужно упорядочить. Записываем инвариант: (18.2.4) Р: s является множеством пар (/, /), представляющих непересекающиеся сегменты массива b U, /]. Массив Ь [0: п—1] упорядочен, если и только если все непересекающиеся сегменты, заданные множеством s, упорядочены. Заметьте, как используется естественный язык для того, чтобы не вводить формально идентификатор, обозначающий начальное значение массива Ь. Таким образом, приходим к следующей программе: (18.2.5) s : = {(0, п—\)\ {Инвариант: (18.2.4)} do s^={ }—-► Выбрать {{i, /), s); s:= s—{(£, /)}; if /—1<2—* прямо упорядочить b[i:j] Q / — i^2 Partition (byiyj,p)\ s: = sl){(t,p-\)}U{(p+l,j)\ fi od Операция Выбрать ((/, j), s) записывает в i n j значение некоторой пары (i, /), принадлежащей s, не изменяя само s. Это действие не- детерминировано, поскольку может быть выбран любой элемент s. (См. приложение 2.) Обсуждение Программа (18.2.5) демонстрирует основную идею, лежащую в основе процедуры Quicksort. Доказательство завершения программы оставлено в качестве упр. 1. Время выполнения программы в среднем равно О (п log n) и в худшем случае 0(п2). Память, требующаяся в худшем случае, составляет О (я), что больше того, что нужно на самом деле. В упр. 2 показывается, как уменьшить объем памяти. При построении данной программы нами руководило желание применить принцип «разделяй и властвуй». Более простой задачей, понадобившейся для осуществления этого принципа, оказалась
232 Часть III. Построение программ процедура разбиения массива Partition. Однако, если бы мы вначале заметили, что доступна процедура Partition, и задали себе вопрос, как ее можно использовать, мы бы воспользовались стратегией (18.1.1) — сведением задачи к более простым. Упражнения к разд. 18.2 1. Докажите эавершаемость программы (18.2.5), воспользовавшись методом, описанным в гл. 17 (теорема (17.3)). Будьте внимательны: сегмент разбиения b[i:j] может быть пустым. 2. Какой величины может стать множество s при выполнении программы (18.2.5)? Максимальный размер s может быть существенно уменьшен путем хранения вместо множества s последовательности (см. приложение 2). После разбиения две пары (£, р—1) и (р+1> /) помещаются в эту последовательность в некотором порядке (а не на произвольные места). Пересмотрите алгоритм (18.2.5), чтобы сделать это изменение, и вычислите вновь максимальную длину последовательности. 18.3. Обход двоичных деревьев Определения и обозначения Упорядоченное двоичное дерево — это конечное множество вершин, или значений, образующих наборы, которые либо пусты, либо состоят из одной вершины, называемой корнем дерева, и двух непересекающихся упорядоченных двоичных деревьев, называемых левым и правым поддеревьями соответственно. /\ /\ I J К L Рис. 18.3.1. Пример двоичного дерева. Упорядоченное двоичное дерево представлено на рис. 18.3.1. Корень этого дерева А\ его левое и правое поддеревья состоят из вершин {В, £,/,/} и {С, Fy G, /С, L) соответственно. Корни правого и левого поддеревьев — соответственно С и б. Прилагательное «упорядоченный» использовано для того, чтобы указать, что поддеревья упорядочены: левое поддерево всегда перечисляется первым. Прилагательное «двоичное» указывает на то, что имеется не более двух поддеревьев. С данного момента вместо
Гл. 18. Использование циклов вместо рекурсии 233 термина «упорядоченное двоичное дерево» будет использоваться более короткий термин «дерево». На рис. 18.3.1 у дерева с корнем В левое поддерево пусто. Вершины с двумя пустыми поддеревьями называются листьями. На рис. 18.3.1 листьями являются G, /, «/, /С, L. Если р — дерево, то предикат empty (p) имеет то же значение, что и предложение «дерево р не имеет вершин». Далее, если ~\empty (р)у то для обозначения левого и правого поддеревьев используются соответственно выражения left [p] и right [/?]. И наконец, если дерево р непусто, то root [p] обозначает значение корневой вершины дерева р. Деревья, графы и связанные с ними математические структуры играют важную роль в информатике. Они позволяют легче понять многие идеи, их свойства позволяют понять степень эффективности многих алгоритмов, а сами они являются существенными частями многих алгоритмов. Например, (двоичное упорядоченное) дерево является важным понятием в некоторых алгоритмах сортировки, алгоритмах распределения памяти и в трансляторах. Таким образом, важно понять основные алгоритмы, оперирующие этими структурами. Выше термин дерево был определен самым легким из возможных способов — рекурсивно. Исходя из этого соображения, многие алгоритмы, оперирующие с деревьями, также являются рекурсивными. Здесь мы намереваемся описать некоторые основные алгоритмы, работающие с деревьями, но используя циклы, а не рекурсию. После твердого овладения этим материалом у вас не должно возникать трудностей при построении других алгоритмов, работающих с графами и другими структурами данных,- Реализация дерева Опишем одну из типичных реализаций, или представлений, дерева, которая мотивирована тем, что во многих случаях приходится вставлять и удалять вершины дерева. В представлении используются простая переменная р и три массива: root [0: ?], left [0:?], right Wl В переменной р содержится целое число, удовлетворяющее условию —1^Р- Это число описывает, или представляет, дерев*. Если дерево или поддерево описывается целым числом к, то выполняются следующие условия: 1) empty (k) эквивалентно k=—1; 2) ~\empty(k) эквивалентно k^O. Если выполняется ']empty(k)y то значение корня — rootlk], левое поддерево корня задается. leftlk], а правое — rightlk]. Например, дерево, показанное на рис. 18.3.1, может быть задан© так:
234 Часть III. Построение программ 0 в -1 4 1 А 0 3 2 3 С 5 10 4 £ 6 7 5 F 8 9 б / -1 -1 7 / -1 -1 8 К -1 -1 9 L -1 -1 10 G -1 -1J root (18.3.1) /7 = 1 fe// right Дадим некоторые комментарии к (18.3.1). Во-первых, р не обязательно должно быть равно 0; корень не обязательно должен описываться первыми элементами массивов root[0]y leftlO] и rightlO]. На самом деле в одних и тех же трех массивах могут храниться несколько деревьев. Для «указания на их корни», например, используются переменные /?1, /?2, /?3. Конечно, из этого следует, что и вершины деревьев не должны располагаться в массиве в каком-то заранее определенном порядке. В (18.3.1) элементы трех массивов с индексами 2 вообще не используются для представления дерева р. Более того, в массиве корень левого поддерева А предшествует Ау а корень правого его поддерева стоит после А. Все это означает, что нельзя обрабатывать дерево, обрабатывая элементы root (left и right) по порядку. В оставшейся части раздела мы будем иметь дело с деревом р> пользуясь исходными обозначениями empty{p), root[p]y leftlp] и rightlp]. Однако заметим, что эти обозначения очень близки к тем> которыми мы пользовались бы при работе с деревом, реализованным так, как это было только что показано. Подсчет вершин дерева В качестве первого примера напишем программу Счет вершин, вычисляющую число вершин дерева /?. Пусть для краткости # Р обозначает число вершин дерева р. Таким образом (используя для хранения числа вершин простую переменную с) программа должна установить: (18.3.2) R:#p = c В качестве первого шага нужно, конечно, дать определение # р в надежде, что это определение приведет нас к идее построения программы. Запишите определение 4j= p (здесь может помочь использование рекурсии, поскольку дерево определено рекурсивно). (18.3.3) #р = empty (/?) —* 0 П empty (р) -> 1 + # left [р] + # right [p] Это определение содержит зерно идеи алгоритма: если empty(p), в качестве результата можно воспользоваться 0; иначе вычислить значение 1 + # left [р] + # right [p]. Такое вычисление требует
Гл. 18. Использование циклов вместо рекурсии 235 подсчета числа вершин в left [p] и right [р]. Но 4t= 1ф [р] также задано рекурсивно определением (18.3.3), и, следовательно, можно ожидать, что его вычисление вынудит нас тем же способом подсчитывать вершины поддеревьев уже этого дерева. Таким образом, видна необходимость подсчета числа вершин в различных поддеревьях р, и кажется разумным воспользоваться переменной для множеств (скажем, s) для хранения множества деревьев, чьи вершины еще нужно пересчитать. А это наводит нас на мысль о цикле. Построим инвариант цикла, расширя-я область значений пере- хменной в результирующем утверждении и принимая во внимание тот факт, что s содержит те деревья, которые еще должны быть пересчитаны: (18.3.4) P:#p = c + Cgr:res:#r) где каждый элемент г множества s — некоторое поддерево р, т. е. число вершин в дереве р — это с плюс число вершин в деревьях, заданных множеством s. Инвариант легко установить путем с, s :=0, {р}. Каждое повторение цикла должно приближать нас к его завершению, а это означает, что оно должно некоторым образом обрабатывать поддеревья из s. Воспользовавшись определением (18.3.3), программу легко написать следующим образом: (18.3.5) с, s:=0, {р}; {инвариант: (18.3.4)} {ограничение: 2 * (# р—с) +1 s \} do s^{ } —■* Выбрать (q> s); s:—s—{q}\ if empty (q) —> skip □ "1 empty (q)-+c, s := c+l, s (J {right (q)\ U {left (q)\ od {c = #p} Мы определили ограничивающую функцию, заметив, что пара {# р—су \s\) (лексикографически) уменьшается при каждом шаге цикла (см. гл. 17). Отметим, что порядок, в котором обрабатываются поддеревья из s, роли не играет. Это происходит из-за того, что число вершин каждого поддерева будет прибавлено к су а сложение ассоциативно и коммутативно. В данном случае использование недетерминированной операции Выбрать (q, s), которая записывает в q произвольное значение из s, просто освобождает нас от необходимости делать несущественный выбор. Обход в префиксном порядке Префиксный список — это список вершин дерева р, обозначаемый preorder(p) и определяемый следующим образом. Если дерево
236 Чаешь III. Псетрание пр$грамм пусто, те ен является пустой последовательностью (); иначе «н — последовательность, состоящая из 1) корня, за которым следуют 2) вершины из левого поддерева в префиксном порядке, за которыми следуют 3) вершины из правого поддерева в префиксном порядке. Например, для поддерева е на рие. 18.3.1, корень которого Е, имеем preorder(e)=(Ey /, J). Для всего дерева а на рис. 18.3.1 имеем preorder(a)=(A, В, Е, /, У, С, F, К, L, G) Определение preorder(p) может быть записано следующим образом, где | обозначает соединение последовательностей: ( empty(p)-+( ) (18.3.6) preorder{p) = \ ~]empty(p)-+(root(p))\preorder(teft [p]) { | preorder (right [/?]) Заметим, что preorder(p) определен рекурсивно. Это обозначение и определение префиксного порядка в терминах соединения созданы для того, чтобы дать возможность ясно и просто устанавливать и анализировать различные свойства деревьев и алгоритмы над ними. Данное понятие хорошо демонстрирует, как использование обозначений помогает лучше понимать суть дела. Обход дерева в префиксном порядке состоит в «прогулке» по дереву и «посещении» каждой вершины в последовательности, заданной префиксным порядком, с тем чтобы выполнить над ней некоторую операцию. Сейчас мы рассмотрим построение программы, •существляющей обход в префиксном порядке с записью значений вершин в массив. Точнее, выполнение программы должно будет устанавливать для дерева р предикат (18.3.7) R:c#pA preorder(p) = b[0:c—l] Заметьте, что определения (18.3.3) и (18.3.6) подобны. Они имеют одну и ту же форму, но в первом используется коммутативная операция + , а во втором—некоммутативная операция |. Может быть, удастся построить программу для обхода в префиксном порядке, преобразовав программу счета вершин так, чтобы она обрабатывала деревья из множества s в некотором определенном порядке. Во- нервых, переделаем программу счета вершин в программу (13.3.8), запоминая значения вершин в массиве s, а не просто подсчитывая вершины. Инвариантом программы является множество вершин ps*b[0:c—1]и {множество вершин из s}
Гл. 18. И «пользование циклов вместо рекурсии 237 (18.3.8) с, s:=0, \р}- {ограничение'. 2#(4t=/? —c)-|-Js|} do вф{ }—* Выбрать (q, s); s : =* s—{q}\ if empty(q)—* skip Q 1 empty{q) —» c, 6 [c] : = c + 1, roo* [9]; s:= sU{ng/i^]}U{/^[d} fi od {с=#рЛ£[0'-£—Ч содержит вершины из р) А теперь преобразуем (18.3.8) следующим образом. Воспользуемся вместо множества s последовательностью г. Ключевая идея решения — вставлять деревья в г и убирать их оттуда таким образом, который позволил бы заключить, что Ь содержит начало префиксного списка р. Это легко сделать, рассмотрев определение префиксного порядка. В результате получаем инвариант (18.3.9) Р:0<с<#М preorder(p) = h[0:c—1] j preorder(rQ) |...| preorder(r{ r, _х) Каждый шаг цикла будет обрабатывать теперь дерево г0. Если оно пусто, то оно удаляется из последовательности деревьев, которые еще требуется посетить. Если оно непусто, то его префиксный порядок задан определением (18.3.6), и в соответствии с ним изменяются список префиксного порядка Ь и последовательность деревьев г: (18.3.10) с, г:=0, (р); {ограничение-. 2#(#/?—с) + |г|}- do гф( )-> q, г:-г [0], г[1..]; if empty (q) —> skip с, I г : = left [q] \ right [q D Пempty (q)—>cy b[c]:=*c+l, root [q]\ od {c = #p} fi Обсуждение В (18.3.5) порядок, в котором правые и леЕые поддеревья записывались в s, несуществен, поскольку сложение, которое выполняется над числами вершин поддеревьев, коммутативно. Однако в (18.3.10) порядок, в котором записываются вершины в последовательность г, важен, поскольку операция | некоммутативна. Первое мое построение этой программы, проделанное более пяти лет назад, отличалось от приведенного. Оно было искусственным процессом, который мало направлялся общими идеями, по-
238 Часть III. Построение программ скольку я был еще новичком и мне еще только предстояло изучить и улучшить технику работы. Если не пользоваться понятием последовательности (см. приложение 2), в том числе и понятием соединения, то некоторые пытаются работать при помощи фраз естественного языка. Например, инвариант записывается как: til : i] является последовательностью указателей на поддеревья, которые до сих пор не были посещены, a preorder(p) равно МО : с—1], за которым следуют списки префиксного порядка этих деревьев. Использование естественного языка в некоторых случаях оправданно; например, он существенно использовался в разд. 18.1 при рассмотрении перестановки сегментов. В данном разделе понятия «обход дерева» и «посещение вершин» полезны для общего обсуждения. Но использование их при построении алгоритма может привести к путанице, так как совсем не так просто увидеть из алгоритма, какие вершины мы уже посетили, а какие еще нет. Намного лучше сделать результирующее утверждение и инвариант более формальными (что и было сделано), поскольку это приводит к выразительному, ясному и точному объяснению. Умение легко писать такие итеративные алгоритмы обхода требуется для многих приложений. Изучение приведенных двух примеров и решение нескольких упражнений принесут заметную пользу. Замечание об уточнении данных При построении программ Quicksort из разд. 18.2 и Счет вершин из данного раздела мы использовали те объекты (и соответствующие операции над ними), которые соответствовали задаче: множества пар целых чисел и множества деревьев. Лишь после того, как это было сделано, мы рассматриваем использование других структур данных для того, чтобы сделать программу более эффективной по отношению к используемой памяти или времени выполнения (см., например, упр. 2 к разд. 18.2, где вместо множеств используются последовательности). А как переписать программу в терминах массивов, мы даже не рассматривали, так как это — шаг почти тривиальный по сравнению с предшествующим ему построением. Кроме того, было бы несколько труднее работать с самого начала в терминах массивов, а не в терминах множеств. Эти соображения иллюстрируют важный принцип: (18.3.11) Принцип. Программируйте для языка программирования, а не на нем. В общем случае этот принцип точно так же относится к данным и их представлению, как и к командам. Нужно использовать струк-
Гл. 18. Использование циклов вместо рекурсии 239 туры данных, соответствующие задаче, а после того, как уже создана правильная программа, иметь дело с задачей изменения структур данных для того, чтобы сделать их использование более эффективным, и для того, чтобы представить их на используемом алгоритмическом языке. Эта последняя задача, часто называемая «уточнением данных», не привлекала до сих пор такого внимания, как задача «уточнения программ». В «современных» языках программирования, допускающих «инкапсулированные данные», уточнение данных может обозначать добавление сегмента программы, описывающего, как должны быть представлены объекты и как должны быть реализованы операции над ними. В других языках программирования уточнение данных может означать преобразование программы таким образом, чтобы она оперировала с разрешенными объектами языка. Замечание о преобразовании программ Преобразование программ является сегодня животрепещущей темой. Многие программисты являются сторонниками использования диалоговых систем для преобразования описания задачи в эффективную программу с помощью ряда таких преобразований. В данном разделе мы воспользовались подобным методом для преобразования программы счета вершин в программу, получающую список префиксного порядка данного дерева. Здесь не место для детального рассмотрения систем преобразования программ, но одно замечание должно быть сделано. Производя преобразование, как это и было сделано нами, всегда убедитесь в том, что результат может быть понятен сам по себе, без изучения преобразования. Таким образом, результат должен иметь свое собственное доказательство правильности в терминах инвариантов циклов и т. д. Исключительно трудно понять программу, пытаясь проследить длинную последовательность преобразований: очень быстро вы начнете упускать из виду детали или вам наскучит этот процесс. Упражнения к разд. 18.3 1. Напишите программу, подсчитывающую число листьев в дереве р. 2. Напишите iiporftetoMy, записывающую в массив Ь инфиксный список вершин дерева р. Инфиксный список определяется следующим образом. Если р пусто, то инфиксный список — пустая последовательность ( ), Если р не пусто, то инфиксный список состоит из 1) вершин left[p] в инфиксном порядке, 2) корня, 3) вершин right[p] в инфиксном порядке. 3. Напишите программу для хранения в массиве Ь списка вершин дерева р в суффиксном порядке. Суффиксный порядок определяется следующим образом,
240 Часть III. Построение программ Если р пусто, то список суффиксного порядка—пустой список ( ). Если р не пусто, то список суффиксного порядка — это 1) вершина left[p] в суффиксном порядке, 2) вершины right[p] в суффиксном порядке, 3) корень дерева. 4. По определению корень дерева лежит на глубине 0, корни его поддеревьев —• на глубине 1, корни поддеревьев— на глубине 2 и т. д. Глубина самого дерева— максимальная из глубин его вершин. Глубина пустого дерева равна —1. Например, в дереве (18.3.1) глубина А равна 0, глубина F равна 2, а глубина самого дерева равна 3. Написать программу, вычисляющую глубину дерева.
Глава 19 СООБРАЖЕНИЯ ЭФФЕКТИВНОСТИ Программиста главным образом волнуют две вещи — правильность программы и эффективность решения. До сих пор в книге мы рассматривали в основном вопросы правильности. Это не означает, что вопрос эффективности не является важным вопросом. Сталкиваясь с любой большой проблемой, обычно лучше на некоторое время оставить в стороне некоторые из ее аспектов и сосредоточить внимание на других, и именно это мы делали. Этот важный принцип называется разделением забот. Двух своих главных целей программист может достичь различными способами. Вопрос о правильности решается при помощи теории правильности, подобной той, которая рассматривалась в ч. II. Формальное определение правильности дается не в терминах выполнения программы, а, напротив, в терминах того, как должны доказываться теоремы вида {Q}S{/?}. Это определение является математическим по характеру и существенно опирается на исчисление предикатов. С другой стороны, в настоящее время рассматривать эффективное использование памяти и времени лучше в терминах некоторой модели выполнения программ. При этом необходимо знать, какой объем памяти требуется для целочисленных переменных, массивов и т. д., а также понимать, как операторы языка программирования выполняются на машине. На самом деле мы все время имели в виду обе главные цели. Например, в задаче сортировки четверок (разд. 15.2) были удалены три охраняемые команды, чтобы сделать программу более короткой и, возможно, более эффективной. Были построены также две программы для вычисления приближения к квадратному корню целого числа (разд. 16.2 и 16.3), при этом рассматривались их относительные скорости. Однако, как это и должно было быть, мы заботились в первую очередь о правильности. Эффективная программа бесполезна, если она не делает того, что, как предполагается, должна делать. В данной главе обращается внимание на некоторые общие приемы улучшения программ, повышения их эффективности. 19.1. Ограничение недетерминизма Недетерминизм возникает тогда, когда могут быть истинны одне- временно две или более охраны конструкции выбора или цикла. Он также возникает при использовании команд, подобных
242 Часть III. Построение программ Выбрать (#, s) (см. приложение 2). Иногда можно сделать программу более эффективной путем ограничения или полного устранения недетерминизма. Напомним, что в правильном цикле можно усилить охраны, не нарушая его правильность, если только остается истинным п. 3 списка условий (11.9) (см. разд. 15.2): РЛ~|ВВ=>/? (Р — инвариант, R — результирующее утверждение, ВВ — дизъюнкция охран). Таким образом, можно ограничивать недетерминированность, усиливая охраны. А если охрана усилена до всегда ложного предиката F, то соответствующая охраняемая команда может быть удалена, поскольку она никогда не будет выполняться. Пользуясь следующей простой теоремой, можно устранять недетерминированность без опасения нарушить п. 3 списка условий (11.9). (19.1.1) Теорема. Пусть в цикле имеются (по меньшей мере) две охраняемые команды с охранами В1 и В2. Тогда усиление В2 до 52 Л 1 В1 не изменяет ВВ, и, следовательно, Р /\ "] ВВ=>/?. Доказательство. В ВВ содержится дизъюнктивный член В1 V В2. Усиливая В2, как показано выше, изменяем этот член ВВ (и только его) на В1 V (В2 /\"] В1). Пользуясь законом де Моргана и упрощая этот член, видим, что он эквивалентен исходному члену В1 V В2. Следовательно, ВВ остается неизменным. □ Использование теоремы (19.1.1) устраняет недетерминизм, поскольку после этого преобразования данные две охраны не могут оказаться обе истинными в одном и том же состоянии. Для лучшего усвоения материала приведем несколько примеров усиления охран и использования теоремы (19.1.1). Еще раз о задаче «жулик на пособии» Программа «жулик на пособии» была построена в разд. 16.4 и в упр. 1 этого раздела. Проанализируем ее здесь заново. Даны три списка имен, упорядоченные по алфавиту и помещенные в фиксированные массивы: / [0 :?], g [О :?], h [О :?]. Некоторые имена встречаются во всех трех списках. Задача состоит в том, чтобы найти первое из таких имен. Пусть до, jv и ко — наименьшие целые числа, удовлетворяющие условию / [iv]=g [jv]=h Ikv]. Тогда должно быть установлено следующее условие, использующее переменные i, /, ki R:i = iv Л / = jv Л k = kv Инвариант цикла ищется путем применения принципа линейного поиска (16.2.7) и расширения областей значений переменных в R: Р: 0 < i < iv Л 0 < / < jv Л 0 < k <£y
Гл. 19. Соображения эффективности 243 Очевидная ограничивающая функция—это t:iv—i+}v—/ +kv—k9 и первый вариант программы (16.4.3) —это (19.1.2) /, /, £:=0, О, 0; do f[i]<g[j]Vf[i]<h[k]-+i:=i+l U g[i]<h[k]Vg[j]<№-+!:-j+l U h[k]<f[i]Vh[k]<g[l]-+k: = k+l od Теперь позаботимся об эффективности. Третий пункт списка условия (11.9): Р /\~]BB=$>R (где Я —инвариант, ВВ—дизъюнкция охран, a R — результирующее утверждение) можно доказать, пользуясь лишь первыми дизъюнктивными членами каждой охраны. Следовательно, охраны могут быть усилены путем устранения их вторых дизъюнктивных членов без нарушения п. 3. Это приводит к более короткой и эффективной программе: i9 /, £:=0, 0, 0; do f[i]<g[j U8[i]<h[k i: = i + 1 /:=/ + ! ОВД </[/]-* k:=k + od Заметим, что теперь можно воспользоваться теоремой (19.1.1) для усиления каких-либо двух охран, но лучше этого не делать. Нет оснований предпочесть одну из команд другим, и усиление охран при помощи теоремы лишь усложнит их и сделает программу менее эффективной. В данном случае недетерминированность помогает получить самое простое решение. Еще раз о сортировке четверки В задаче сортировки четверки из разд. 15.2 три охраны могли быть усилены до F — тождественно ложного предиката — и, следовательно, могли быть удалены соответствующие охраняемые команды. Это преобразование устранило часть недетерминированности, но не всю ее. Возведение в степень Рассмотрим написание программы, устанавливающей для двух данных фиксированных целых чисел X п Y, XJ^O, F>0, предикат R:z = Xv (по определению 0°= 1). Эта программа должна состоять из цикла со следующими инвариантом и ограничивающей функцией: tiy
244 Часть III. Построение программ Р легко устанавливается присваиванием х> у, z: = X, Y, 1, и (по крайней мере) две простые команды могут быть использованы для уменьшения ограничивающей функции: у: = у—1 и у: = у-±-2. Нахождение слабейших предусловий этих команд но отношению к инварианту прямо приводит к программе ху у, z: = X, Г, 1 do 0 < у Л even (у)—+ у, х: = у 4-2, х * х И 0<у -+у, z: = у— 1, z*x od {z = Xy} А теперь рассмотрим эффективность этой программы. Деление на 2 в общем случае уменьшает у больше, чем вычитание из него 1; следовательно, деление предпочтительнее. Однако, если у положительно четно, истинны обе охраны; при реализации мы можем выбирать для выполнения любую из них. Воспользовавшись теоремой (19.1.1), заменим охрану 0<# на »<г/Л1(0<//Л even (у)) и упростим ее, получая {0<Х Д0<П х, у, г:= X, Г, 1; do 0 < у Л even (у)—+ у, х: = у -ч- 2, х*х □ 0<у Aodd(y)—+ у, г:= у—1, *** od {г = Х^} Перепишем цикл еще раз в виде do 0 < у -— do even (у) —> у, х: = у -=- 2, л: * л: od; */, г:= у— 1, г*# od В предварительной недетерминированной версии программы цикл может повторяться до Y раз; в окончательной детерминированной ее версии число повторений не превосходит 1+2#ш7 (log Y). (По поводу ceil см. приложение 2.— ред.) 19.2. Вынесение утверждения из цикла Рассмотрим следующий сегмент программы, в котором многоточия ... обозначают произвольные куски текста программы, не присваивающие ничего i: do i < п —► ... ; k : = 5 * г, ...; i:=i + 2; ... od
Гл. 19. Соображения эффективности 245 Эту программу можно преобразовать так, чтобы она использовала более быструю арифметическую операцию сложения вместо умножения. Преобразование производится следующим образом. Сначала введем новую переменную г, которая будет содержать значение 5*/, и преобразуем программу, как показано ниже: do i < п —► ... ; z : = 5 * i\ k:=z; ... ; /:=! + 2; ... od Затем сделаем z=5*i частью инварианта цикла. Это означает, что присваивание z :=5*/ внутри цикла становится ненужным, но тогда, когда i увеличивается, должно соответственно изменяться и г: z := *'#5; {Часть инварианта: г = 5*/} do i < п —►...; k :== z; ...; i, z:=* + 2, г+10;... od Специалисты по трансляции называют такое преобразование уменьшением силы. Оно использовалось уже в конце 50-х и начале 60-х годов как в трансляторах с Фортрана, так и в трансляторах с Алгола, для того чтобы упростить ссылки к двумерным массивам. Например, предположим, что массив Ь [0 i 99,0 : 50] записан в память по строкам. Вычисление адреса элемента Ь [i, j] выполняется следующим образом: адресф [0, 0])+*»51+/ А тогда все вычисления адреса b U', /1 внутри цикла, который на каждом шаге увеличивает /, могут быть для большей эффективности преобразованы так, как это было только что показано. Такая оптимизация эффективна также и потому, что она дает возможность обнаружить и удалить некоторые формы арифметических выражений общего вида. В общем случае такое преобразование называется вынесением утверждения из цикла (и включением его в инвариант цикла). В данном случае было вынесено из цикла и стало частью его инварианта утверждение z=5*i. Таким приемом можно пользоваться повсюду, где значение некоторой переменной, подобно г, может быть вычислено путем поправки ее текущего значения, вместо того чтобы каждый раз вычислять его. В только что приведенном примере вынесение утверждения из цикла смогло уменьшить время его выполнения лишь на постоянный множитель, но существуют и примеры, когда этот прием может уменьшить и порядок величины времени выполнения алгоритма
246 Часть III. Построение программ Схема Горнера Рассмотрим вычисление многочлена а0 + ах * л:1 + ... + + Я/,-!**"""1 при п^\ для фиксированного значения х и данных констант at. Результирующим утверждением является R: г/ = а0*х°+... +ап_1*хп~1 Можно получить инвариант заменой константы п на переменную i и построить следующую программу: lf */, г:= 1, а0, х\ {инвариант-. 1 </<пЛг/ = «0*х°+ • • • +Я/-1**'"1} {ограничение-, п—/} йогфп—► i\ y:=i+l, у + а;*х* od Но заметим, что вычисление х{ на каждом шаге обходится дорого, требуя в общем случае времени, пропорционального log/. Заметив также, что х1' = x*xi~1, мы увидим, что, вводя новую переменную z и сделав z = x{ частью инварианта цикла, можно преобразовать программу следующим образом: iy У у г:= 1, а0, х\ {инвариант-. 1 ^i^n/\z = x1' /\у = а0*х°+ ... +ai_i*x1'-1} {ограничение: п — i\ do 1фп—*1, у у z:=i+\, y-\-a;*z, z*x od Это преобразование также может быть названо уменьшением силы: операция возведения в степень заменена более быстрой операцией умножения. Использование этого преобразования уменьшает порядок времени выполнения программы с 0(nlogn) до 0\п). Замечание. Многочлен можно переписать в виде ((...(а«-1** + яя-а)**+-.0** + а0 а это приводит непосредственно к несколько более простой программе у, i:=an_v /1—1; {инвариант: 0 <[ i < n/\ у = ((...(an_1*x + an_J*x+...)*x + ai\ {ограничение : i) do i=£0 —> i := i—1; у := у*х + а; od Это способ вычисления значений многочлена назван по имени Горнера, который предложил его в связи с другой известной задачей в 1819 г. Но на самом деле он был предложен на 100 лет раньше Ньютоном. Этот пример показывает, что предварительный анализ
Гл. 19. Соображения эффективности 247 спецификаций программы и преобразование их в несколько другук> форму могут оказаться более полезными, чем поиск эффективных программ для исходной спецификации. Упражнение, приписываемое Хэммингу. Рассмотрим последовательность всех чисел, не делящихся ни на какие простые числа, кроме 2, 3 и 5: <7=1, 2, 3, 4, 5, 6, 8, 9, 10,. 12, ... . Будем называть эту последовательность seq. Другой способ описания seq — дать аксиомы, описывающие, какие значения в ней находятся: Аксиома 1.1 находится в seq. Аксиома 2. Если х находится в seq, то в ней находятся и 2*х» 3*л;, 5*л;. Аксиома 3. В seq нет значений, кроме тех, которые заданы аксиомами 1 и 2. Задача состоит в написании программы, записывающей в порядке возрастания первые 1000 значений seq в массив ^0 : 999], т. е. устанавливающей R : q [0 : 999] содержит первые 1000 значений seq в порядке возрастания Требуется цикл некоторого вида. Каков возможный инвариант цикла? Поскольку аксиома 2 гласит, что некоторое значение находится в seq тогда, когда там находится некоторое меньшее значение, возможно, имеет смысл порождать значения последовательности па порядку. Тогда возникает возможность заменить в R константу 1000 на переменную i и получить инвариант Я=1</<1000Л<?[0 : i—1] содержит первые i значений seq При таком инварианте программа, очевидно, имеет следующую структуру: {инвариант: Р\ ограничение: 1000—i) do i Ф 1000 —> Вычислить xnext, i-e значение seq\ i,q[i] := i+ I,xnext od Остается определить, как вычислять xnext, т. е. следующее значение х, которое должно быть порождено. Поскольку значения seq порождаются по порядку, xnext должно быть > q[i—1]_ Далее, так как 1 уже находится в ^[0:/—1], то xnext должна
248 Часть ///. Построение программ удовлетворять аксиоме 2. А это означает, что xnext должно иметь вид 2* ху 3*л: или 5*х для некоторого х, уже находящегося в q[0:i—1]. Следовательно, xnext является минимальным значением *>q\i—1], имеющим вид 2#x, 3#x или 5#x для некоторого х из q[0:i—1]. Таким образом, вводим три переменные х2у хЗ и х5, смысл которых выражается следующим утверждением: Р1: х2 является минимальным значением > q[i—1], имеющим вид 2*л: для некоторого х из q[0:i—1]. хЗ является минимальным значением > q[i—1], имеющим вид 3*л: для некоторого х из q[0:i—1]. х5 является минимальным значением > q[i—1], имеющим вид 5*л: для некоторого х из q[0:i—1]. Значение xnext является минимумом значений х2, хЗ и х5. А тогда мы замечаем, что на самом деле переменная xnext не нужна, и модифицируем структуру программы следующим образом: i, ?[0]:=1, 1; {/>} {инвариант: Р\ ограничение: 1000 — i) do 1Ф 1000—► Вычислить х2у хЗ, х5> удовлетворяющие Р\\ iy q[i]:=i+l, mm(x2t хЗу х5) od Теперь проиллюстрируем вынесение утверждения из цикла. Вычисление л2, хЗ, х5 на каждом шаге для установления Р1 — трудоемкий процесс. Однако, когда i увеличивается, они изменяются довольно медленно (при сохранении инварианта Р), и может оказаться возможным ускорить алгоритм, вынеся Р1 из цикла и сделав его частью инварианта. То, что q [0 : i—1] упорядочен, укрепляет наши надежды. Таким образом, рассмотрим следующую структуру программы: U <?[0]:=1, 1; {Р\ Установить Р1 при /=1; {инвариант: Р/\Р1\ ограничение: 1000 — i) do J^=1000-W, q[i]:*=i+l, m\n(x2f хЗ, х5)\ Восстановить Р1 od Но как восстановить Р1? Рассмотрим х2. Для некоторого / имеем, что x2 = 2*q[j]. Мало того, х2 может лишь возрастать, а не уменьшаться, далее оно будет 2*<7[/ + 1], 2* q[j + 2] и т. д. А это наводит на мысль сохранить индекс /. Подобные же утверждения имеют место и для хЗ и х5. Следовательно, введем три переменные /2, /3, /5 и модифицируем Р1 следующим образом:
Гл. 19. Соображения эффективности 249 PI: x2 = 2*q[j2] является минимальным значением > q[j— 1], имеющим вид 2*л: для некоторого x£q[0:i— 1], хЗ — 3*q[j3] является минимальным значением > q[j — 1], имеющим вид 3*л: для некоторого x£q[0:i— 1]. х5 = 5* q[j5] является минимальным значением > q[j —1], имеющим вид 5 * х для некоторого x£q[0:i—l]. А теперь можно построить окончательный вариант программы i, ДО]:1,1; \Р\ Установить Р1: х2у хЗу х5у j2t j3, /5:= 2, 3, 5, 0, 0, 0; {'инвариант: Р/\Р1\ ограничение: 1000—i) do /=^1000-*i, q[i]:=i+l, mm(x2, x3y x5)\ Восстановить PI: &ox2 < q[i— 1] -+ j2 := j2 +\\x2 := 2 * q[j2] od; dox3^q[i— l]-> j3:=j3 + Iyx3: = 3*q[j3[jod; do x5^q[i—1] —*- j5 := /5+ 1; *5 : = 5*q[j5]o& od Упражнение к разд. 19.2 1. Разложение величины в сумму квадратов. Напишите программу, которая для данного фиксированного целого числа г^О порождает все способы, которыми г может быть записано в виде суммы двух квадратов, т. е, порождает все пары (л:, у), удовлетворяющие условию (19.2.1) л:2 + */2=гл0<*/<л; Предположим еще и следующее, что поможет при написании программы и при подготовке использования стратегии вынесения утверждения из цикла. Значения пар (х, у), удовлетворяющих (19.2.1), будут содержаться в двух массивах: xv, yv. Более того, пары порождаются в порядке возрастания хь а для отметки того, что все пары с первой компонентой меньше х уже по- рождены, используется переменная х. Таким образом, первым приближением. к инварианту главного цикла программы будет PI: O^tA упорядочен (xv[0:i—1])л пары (xv[j]t yv[j]) при 0<;< i являются парами с первой координатой < *, удовлетворяющими (19.2.1).,. 19.3. Изменение представления данных Иногда полезно преобразовать одну программу в другую, которая использует другое представление данных. В качестве простого» примера различных представлений одного и того же значения приведем прямоугольные и полярные координаты точек на плоскости. Другим примером является день года, который может храниться в форме (месяц, день) или же в форме (номер дня в году). Обоснованием изменения представления данных часто является, желание применить одну из следующих двух стратегий в надежде* что они приведут к более простой или более эффективной программе.
250 Часть III. Построение программ Стратегия. Заменяйте дорогостоящие операторы на дешевые. Стратегия. Откладывайте выполнение дорогостоящей операции, чтобы она не выполнялась слишком часто. Другие соображения по поводу изменения представления данных, вероятно, сами придут вам в голову после овладения приемами изменений. Их мы проиллюстрируем на трех примерах. Приближение к квадратному корню В разд. 16.3 была построена следующая программа для нахождения приближенного значения квадратного корня фиксированного целого числа: <19.3.1) а, 6:=0, л+1; {инвариант Р: а < Ь^п+ 1Дя2^ п < Ь2} {ограничение t:b—а+1} do a+l фЪ-^d := (а + Ь) + 2; if d*d^.n-+ а : = d Q d*d> n—+b := d fi od {а2<А2<(а+1)2} Видоизменим эту программу, чтобы проиллюстрировать изменение представления данных. Менее тривиальное преобразование потребуется в упр. 2. Пусть мы хотим заменить в данной программе операцию — на деление. Это легко, если а+b всегда четно, поскольку в данном случае обе операции дают один и тот же результат. Поддерживать четность а+b может оказаться не столь уж легко, но, поскольку разность Ъ—а всегда четна, d можно вычислить, воспользовавшись оператором d :=a+(b—а)12. Следовательно, попытаемся работать с разностью, например, с величин b и а и поддерживать четной именно ее. Это будет легче всего сделать, если с всегда является степенью двойки. Таким образом, имеем Ь-=а+с d=a+c/2 (gp : 1^р : с=2р) (следовательно, с четно) Поскольку b и d определяются через а и с, можно написать программу, пользуясь лишь а и с. Таким образом, попытаемся применить инвариант цикла и ограничивающую функцию Р и t: Р:а2^п< (а + с)2А(ЯР:1<Р:с==2р) t:c+l При инициализации потребуется цикл для установления Р, так как с должно быть степенью двойки. Вся оставшаяся часть программы выводится из программы (19.3.1), в сущности, устранением при-
Гл. 19. Соображения эффективности 25t сваиваний Ь и d и заменой других команд на команды, включающие вместо них с. (19.3.2) а, с:—0,Л; do c2<n-+c:=2*c od; {P} do с^= 1 —>с:=с/2; if (а + с)2<я-+а:=:а + с Q (а + с)2 >n-+ s*ip II od {а2</г<(а+1)2} Сортировка с регулируемой плотностью массива При решении этой задачи попытаемся следовать идее построения без изложения всех формальных частностей, которые будут оставлены в качестве упражнения. Должен храниться список (не обязательно различных) чисел,, который в исходном состоянии пуст. В любой момент времени может выполняться одна из следующих трех операций: 1) Вставить (Vt): вставить в список новое значение V%. 2) Поиск (х, р): найти место значения х и р («место» должна быть дополнительно специфицировано позже). 3) Печать: напечатать список значений в порядке возрастания. Операция 3 должна выполняться за время, пропорциональное числу значений в списке. Более того, общее время, расходуемое на вставки и поиск, должно быть «мало». Требование *fc операции 3 подсказывает, что список значений должен храниться в (возрастающем) порядке. Фактически это нас заставляет думать об алгоритме Сортировка вставками. При использовании массива v [О : п—1], содержащего значения, и простой переменной i список значений V0t . . ., V^iy которые уже расставлены, удовлетворяет условию (19.3.3) Р:0^.i/\упорядочен(и[0:i— I])/\перестановка (v[0:i— 1], {v.,.... VW) При таком представлении печать может быть выполнена за линейное время, а поиск — за время, пропорциональное логарифму текущего размера v (применением двоичного поиска). Но как же обстоит дело со вставкой нового значения х? Вставка потребует нахождения места /, подходящего для х> т. е. значения /, такого, что v [/—1]^*<1> [/], затем сдвига v [/: i—1] на одну позицию в v [/+1, i] и, наконец, помещения xbv [/]. Сдвиг v [/ : i—1} может занимать время, пропорциональное i, что означает, что каждая вставка может занимать время, пропорциональное iy и, следовательно, в худшем случае общее время, расходуемое на вставку п значений, может быть порядка п2. Это дорого, и поэтому нам приходится думать о модификации программы. Сдвиг — дорогостоящая операция; поэтому попытаемся изменить представление данных таким образом, чтобы сделать его менее
252 Часть III. Построение преграмм дорогостоящим. Как это можно сделать так, чтобы иногда можно было вовсе устранить необходимость в сдвиге? Простой способ сделать сдвиг менее дорогой операцией — «разредить» значения таким образом, чтобы пустой элемент массива (или «дыра») стоял между каждой парой значений. Таким образом, определяется массив v [О : 2п—1] двойного размера; (19.3.4) Р: 0<*Д упорядочен^ \2i — 1])Л{^0, ••-, Vt-i} £v[0:2i— l]/\V[0:2i — 1]=(дыра, значение, дыра, значение, ..., дыра, значение) Дыры могут быть представлены вторым массивом дыра [0:2п—1], состоящим из битов. Дыра[]'] имеет значение "/ является дырой ". Разумно положить, что дыра v [/] содержит значение, содержащееся в v [/+1], так что для поиска может по-прежнему использоваться двоичный поиск. Замечание. Если известно, что все значения положительны, то, чтобы отличить значения от дыр, можно использовать знаковый бит v [/]. □ А теперь вставка и сдвиг вообще не занимают времени, поскольку новое значение можно поместить в дыру. Но вставка и сдвиг разрушают такое положение вещей, при котором дыры разделяют каждые два значения, и после вставки необходимо перестроить массив, чтобы восстановить (19.3.4). Но перестройка может оказаться дорогостоящей, так что мы должны найти способ избегать ее, пока это возможно. Отложить на время перестройку массива можно просто путем ослабления инварианта, позволяющего нескольким значениям стоять рядом друг с другом. Однако смежных дыр быть не может: нечетные элементы массива v всегда содержат значения. Введем новую переменную k для того, чтобы отметить, сколько элементов массива использовано, и воспользуемся инвариантом (l9.3.5)P:0^iAynopядoчeн(v[0:k—l])A{V0У...yV^1}ev[G:k—\]A (V/'-O ^/ <kAodd(j):v[j] не является дырой) Л4®:^—П содержит k—1 дыр Л (V/'-O^/ < k:v[j]является дырой =$>v[j] = v[j -{-1]) Теперь отметим, что при вставке первого значения не требуется сдвига, поскольку оно может попасть в дыру. Второе значение также, похоже, попадет в дыру, но может вызвать и сдвиг. Третье вставляемое значение также может попасть в дыру, но вероятность того, что оно вызовет сдвиг, больше, поскольку дыр стало меньше. В некоторый момент будет вставлено столько значений, что сдвиг опять станет слишком дорогостоящей операцией. В. этот момент
Гл. 19. Соображения эффективности 253 разумно перестроить массив таким образом, чтобы опять была одна дыра между каждыми двумя значениями. Подводя итог сказанному, приходим к выводу, что список определяется условием (19.3.5), причем вначале истинно также и (19.3.4), т. е. значения разделены дырами. В исходном состоянии список делается пустым путем присваивания I, £:=0, О {истинны (19.3.4) и (19.3.5)} Вставка значения V{ производится следующим образом: (19.3.6) {(19.3.5)} if сдвиг слишком дорог —* перестроить, восстановив (19.3.4) Q сдвиг не слишком дорог —*■ skip fi; Найти место /, подходящее для V^ Сдвинуть v[j:...] на одну позицию вверх, освободив место для vt\ i> t{/]:=i' + l, Vt Когда сдвиг становится настолько дорогим, что вновь требуется перестройка? Анализ показывает, что перестройку лучше производить либо тогда, когда предыдущий сдвиг потребовал перемещения по крайней мере Vi значений, либо тогда, когда со времени прошлой перестройки было вставлено i/2 значений. При этом общее время, расходуемое на перестройку, грубо говоря, равно общему времени, расходуемому на сдвиги, так что ни одно из них не перекрывает другого. При данных обстоятельствах в наихудшем возможном случае общее время, затрачиваемое на сдвиги и перестройки, пропорционально п\^пу а в среднем случае общее время пропорционально п log п. Построение полного алгоритма остается читателю в качестве упр. 1. Обсуждение При построении этого алгоритма первой идеей было найти способ сделать сдвиг более дешевой операции, для чего мы поместили «дыру» между каждыми двумя значениями. Второй идеей было отложить на время перестройку, поскольку она — слишком дорогостоящая операция. В первом случае мы сделали сдвиг более дешевым, но ввели дорогую операцию перестройки; во втором перестройка откладывалась достаточно часто для того, чтобы общая стоимость сдвига и перестройки была примерно равна. Этот алгоритм является соперником схем, основанных на сбалансированных деревьях, в том случае, когда список значений хранится в памяти.
254 Часть III, Построение программ Эффективная реализация очередей на Лиспе В языке программирования Лисп возможны функции над списками п значений v=(v0, . . ., v^), где я>0. Пять функций, с которыми мы будем иметь дело, следующие: 1) v=( ) дает значение утверждения «список v пуст»; 2) head (v) дает значение v0 (не определена, если v пуст); 3) tail (v) дает значение (vu - . ., vnmml) — списка без первого элемента (не определена, если v пуст); 4) construct (w, v), где w — значение, a v=(v0, . . ., vn) — список, дает список (ш, v0y . . ., vn)\ 5) append (v, w), где w — значение, a v=(v0j . . ., vn) — список, дает список (v0> . . ., vny w). Первые четыре функции исполняются за постоянное время. Однако функция append требует времени, пропорционального длине списка v, к которому w присоединяется. Это все, что нам нужно знать о Лиспе. Рассмотрим реализацию очереди, использующую списки Лиспа и пять только что приведенных функций. Очередь — это список vf над которым могут выполняться три операции: первая — ссылка на первый элемент списка, вторая — удаление первого элемента, а третья — вставка значения w в конец очереди. Таким образом, три операции над очередью v могут быть реализованы следующим образом: 1) ссылка на первый элемент: head(v)\ 2) удаление первого элемента: v : = tail(v)\ 3) вставка значения w в конец очереди: v :=append(vy w). Теперь предположим, что в очередь должны быть вставлены п значений иь . . ., vn_iy а между вставками из очереди могут удаляться значения или проверяться первые элементы очереди. В наихудшем случае время, необходимое для выполнения вставок, имеет порядок п2. Почему? Вставка значения занимает время, пропорциональное длине очереди. Вставка п значений в пустую очередь может занять время, пропорциональное 0+1. . .+п—1=0(п2). Ясно, что вставки, выполняемые посредством лисповской операции append, обходятся дорого, так что надо использовать другое представление данных для того, чтобы сделать их менее дорогими. Какое другое представление могло бы позволить выполнять вставки за постоянное время? Вставки можно было бы легко выполнять, если бы очередь хранилась в обратном порядке. Но это бы сделало дорогой операцию удаления. Таким образом, мы идем на компромисс: представим очередь v=(v0, . . ., u/eJ при помощи двух списков: vh и vt, где
Гл. 19. Соображения эффективности 255 второй из них записан в обратном порядке: (19.3.7) vh=(v0j . . ., vk_t) для некоторых k и ttf=(0/.-ii ^/-2> • • •> ^2), где u/i=( ), только если vt=( ) А теперь вернемся к реализации операций над очередью. Ссылка на первый элемент по-прежнему реализуется как head(vh)\ ограничение, что vh пусто лишь тогда, когда vt пусто, позволяет сделать его реализацию достаточно простой. Далее операция удалить должна удалить из vh первый элемент, но если vh становится пустым, то для сохранения истинности (19.3.7) требуется инвертировать vt и передать его в vh. Таким образом, операция удалить реализуется следующим образом: vh : = tail(vh)\ if vh = ( )Avt^( ) — {инвариант: очередь—это (reverse(vt) \ (vh)} {ограничение: \vt \) do vt Ф ( )—+vh, vt := construct(head(vt), vh), tail(vt) od {(19.3.7)Л^==( )} g vh^( )S/vt**( )-+skip fi И наконец, операцию вставить можно реализовать путем vt: = construct(wy vt). Теперь операция вставить выполняется за постоянное время, а цикл в операцию удалить будет требовать времени, пропорционального длине списка vt. Но общее время, расходуемое на исполнение этого цикла, пропорционально общему числу п значений, вставляемых в очередь. Упражнения к разд. 19.3 1. Напишите процедуру перестройки procedure config(va\ue it k:integer\ var v\ array of integer, var isgap: array of Boolean); которая при данных фиксированных i > О, фиксированном целом k, массиве v и массиве isgap, удовлетворяющих (19.3.5), «разрежает» значения массива для того, чтобы установить (19.3.4) *>. 2. Измените представление переменных в программе (19.3.2) таким образом, чтобы не использовались операции возведения в квадрат. 3. Постройте программу, которая по данным фиксированным целым числам X, У > 0 устанавливает z = XY. При построении воспользуйтесь тем, что г должно быть вычислено серией умножений, так что может иметь смысл вначале установить г равным единичному элементу операции умножения, т. е. 1, попытавшись сначала создать инвариант цикла, а затем изменить представление данных, с тем чтобы все это работало правильно. *> Здесь автор без объяснений вводит обозначения для массивов с неуказанными границами: array of означает, что в этом месте (т. е. в позиции спецификации параметра процедуры в отличие от позиции описания массива) границы массива не указываются, а берутся такими, какие будут у массива- аргумента,— Прим. ред.
Глава 20 ДВА БОЛЬШИХ ПРИМЕРА ПОСТРОЕНИЯ ПРОГРАММ Ниже мы приведем построение двух программ, чтобы показать использование нашей методики на несколько более сложных и больших задачах. Каждая из них преподаст еще несколько уроков, полезных в деле построения программ. Мы по-прежнему будем пытаться наталкивать читателя на решения, как уже делалось в предыдущих главах. В тех местах, которые включают лишь принципы и стратегии, уже проиллюстрированные раньше, построение будет идти в более быстром темпе, чем раньше. В упражнениях содержится серия более длинных и трудных, чем раньше, задач, на которых можно испытать приемы и стратегии, разобранные в данной книге. 20.1. Выровненные строки текста Рассмотрим следующую задачу: написать процедуру, которая выравнивает строки текста, вставляя между словами дополнительные пробелы таким образом, чтобы последнее слово каждой строки заканчивалось в последнем ее столбце. Например, при использовании § для обозначения пробелов, три строки (20.1.1) выравнивание§строк§вставко駧§§§§§§§§§§ дополнительных§пробелов§является §§§§§§§ одной§из§задач§редактирующей§программы после выравнивания могут стать такими: (20.1.2) выравнивани姧§§§§§строꧧ§§§§§вставкой дополнительных§§§§пробело⧧§§§является одной§из§задач§редактирующей§программы. Некоторые ограничения накладываются на то, как должны вставляться пробелы между словами, чтобы уменьшить нежелательное впечатление от выравнивания. Во-первых, число пробелов между различными парами смежных слов на строке должно различаться не более чем на 1. Во-вторых, поскольку большие просветы не могут появляться на одной и той же стороне листа, должен использоваться прием чередования: на четных (нечетных) строках большее число пробелов вставляется справа (слева), если это необходимо. Например, в строке 2 из (20.1.1) при переходе к строке 2 (20.1.2) было вставлено пять пробелов перед последним словом строки; если бы на
Гл. 20. Два больших примера построения программ 257 строке 3 вместо точки стоял пробел, добавочный пробел был бы вставлен после первого слова. Напишем процедуру для вычисления номеров столбцов, в которых будут начинаться слова в выровненной строке, по номерам этих же столбцов в исходной, невыровненной строке. Для строки 1 в приведенном выше примере список номеров столбцов (1, 14, 20), в которых начинаются слова, будет заменен на список столбцов (1, 20, 32), в которых будут начинаться слова в выровненной строке 1 из (20.1.2). Для строки 2 список (1, 16, 26) будет заменен на (1, 19, 32). Предлагается следующий заголовок процедуры: ргос выровнять (value n, z> s\integer\ var b\ array of integer)] В строке г имеется п слов. Они начинаются в столбцах 6[1], ..., Ь[п]. Каждая пара лежащих рядом слов разделена в точности одним пробелом. Параметр s задает общее число дополнительных пробелов, которые нужно вставить между словами для выравнивания строки. В процедуре определяются новые номера столбцов 6[1:п] таким образом, чтобы строка стала выровненной так, как это описано выше. Начало построения Как вы приступите к задаче написания процедуры выравнивания? Первый шаг — написать пред- и постусловия тела процедуры. Начнем с предусловия. Сами слова не являются частью спецификации, поскольку даны лишь номера столбцов. Так что предусловие нельзя написать в терминах слов. Но нам может помочь интерпретация предусловия в терминах слов. Вначале входная строка имеет вид W1 [1] W2 Ш. .Ш Wn Ы где W1 — первое слово, W2 — второе и т. д. и Wn — последнее слово, s — число дополнительных пробелов. Число пробелов, стоящих в данном месте, показано в квадратных скобках. Само предусловие Q должно наложить ограничения на входные данные — например, что не может быть отрицательного числа слов или дополнительных пробелов. Кроме того, поскольку массив Ъ будет изменяться, необходимо обозначить его начальные значения, например, через В. Предусловием является (20.1.3) Q:0<sA0</iA&[l:f!]-B[l:n] Теперь, когда мы уже имеем предусловие, напишите постусловие. 9 д грио
258 Часть III. Построение программ Выровненная строка имеет вид (20.1.4) W1 [р+1]. . .[/7+1] W/ [q+ll . \q+\] Wax где должны быть наложены ограничения на переменные р, q и t. Как /?, так и q должны быть ^0; более того, они должны различаться не более чем на единицу. Общее число вставляемых пробелов должно быть равно s. Предполагается, что W/ — одно из слов W1, . . ., Wn. И наконец, имеется ограничение на то, как расположены дополнительные пробелы в зависимости от номера строки. Эти ограничения можно формализовать следующим образом: (20.1.5) Q/:1</<azA (/ — одно из слов) 0^p/\0^.q/\ (He выбрасывайте пробелы) p*t + q*(n—t) = s/\ (Вставить s пробелов) (odd(z)Aq = P+i\/(even(z)Ap = q+^) (Ограничение на вставление пробелов) Задача нашей процедуры—изменить массив Ьу поэтому специфицируем это изменение. Легко выводится следующее условие: (20.1.6) R:(vi:l^i^t:b[i] = B[i] + p*(i— 1))Д (vi:t<i^n:b[i] = B[i] + p*(t— l) + q*(i — t)) Теперь пред- и постусловиями являются Q (20.1.3) и R (20.1.6) -вместе с условиями Q1 (20.1.5) на переменные ру q, t из R. Какова общая структура алгоритма? Спецификация приводит нас к рассмотрению алгоритма (20.1.7) {Q} Вычисляем /?, #, /, чтобы установить Ql\ \Q1/\Q} Вычисляем новые Ml : n], чтобы установить R {Q1AR} Если бы спецификации были другими, возникла бы, по-видимому, другая структура алгоритма. Вычисление р> q, t Должны быть уточнены две команды из (20.1.7), записанные на естественном языке. Начнем с первой из них. Уточните команду «Вычисляем /7, qy t, чтобы установить Qh. Твердо убедитесь, что уточнение правильно. Если обратиться к Q1 в поисках идей, то в данный момент, казалось бы, невозможно избежать рассмотрения двух случаев; odd (г) и even(z). Поэтохму рассмотрим пока лишь случай odd(z). Тогда q=
Гл. 20. Два больших примера построения программ 259 =/?+1. Мы хотим определить значения р, q и /, удовлетворяющие Q1. Упростим сначала Q/, подставив вместо q его значение: 1</<мЛ0<рЛ0<р+1Лр*(* — l) + (p+\)*(n—t)=*s а это упрощается до (20.1.8) 1</<дд0<рДр*(Аг—l) + n —/ = s Очевидным решением (20.1.8) является p=zs~(n—1), п—1 = = s mod(n—1); р — частное, а м — / есть остаток от деления s на п—1. Но мы сразу же замечаем, что деление на п — 1 невозможно, если л = 1. Строка не может быть выровнена, если в ней одно слово! Таким образом, спецификация противоречива. В данном случае предположим, что пф\. В случае odd(z) и пф\ величины р, q и г можно вычислить следующим образом: (20.1.9) p:=s+(n— 1); t := n — (smod(/2— 1)); ?:=р+1 Остается показать, что Q/\odd(z)=&wp((20A.9)f Q1). Можно вычислить предикат wp((20A.9)9 Q1) и упростить его до (20.1.10) I < n — (s mod(n— 1)) < мДО < s~(n — \)Aodd(z) так что остается доказать, что QЛodd(z)=:>(20.1.10). Здесь мы попадаем в неприятную ситуацию, лишь если я = 0. В данном случае (20.1.10) сводится к 1 < —(s mod— 1) < 0д0 <s-!—1 Aodd(z) которое не может быть истинным. Что значит я = 0? То, что на строке нет слов. Конечно же, строка из 0 слов не может быть выровнена! Предположим, спецификация изменена таким образом, что если строка не содержит ни одного слова или содержит только одно слово, то выравнивание не должно происходить. Случай even(z) рассматривается подобным же образом, и мы приходим к следующему алгоритму для установления Q1 при п>\: Определить р, q, t iieven(z) —+q;= s~(n — l)\ t :=l + (smod(A2 —1)); p := q+ 1 [] odd(z) —> p := s-±-(n—1); t := n — (smod(n— 1)); q:=p+l fi Вычисление новых номеров столбцов Форма результирующего утверждения /?(20.1.6) подсказывает использование двух циклов — одного для вычисления Ь[\ : /], а другого для вычисления blt+l : n]. Постройте эти два цикла, используя следующие указания. Во-первых, может оказаться полез- 9*
260 Часть III. Построение программ ным вычислять b[i] в убывающем, а не в возрастающем порядке значений i. Во-вторых, возможно, окажется разумным воспользоваться переменной е, содержащей количество пробелов, которые должны быть добавлены к bit], что устранит необходимость в умножении на каждом шаге цикла. В-третьих, не нужно вычислять новых значений Ml : /], если /?=0. Здесь приводится одно из решений, которое без всяких добавочных расходов устраняет присваивание b[\ : t] при /7=0: (20.1.П) Вычисляем b[t+l:n]: k, е: = n, s; {инвариант: t ^k^.nAe = P*(t — 1) + <7*Ф — 0Л 6[1:&] содержит исходные значенияД b[k+l:n] содержит окончательные значения} {ограничение: k — /} do kфt-+b[k]:=b[k] + e; k, e:=k—l, e — q od; {k = t/\e=*p*(k—l)\ Вычисляем 6[1:/], предполагая, что вначале инвариант выполнен: {инвариант: 1 <!&<;/Д£ = /?*(&—1)Л 6[1:А] содержит начальные значенияД b[k + l:n] содержит окончательные значения} {ограничение: k—1} do ефО-+b[k]:= b[k] + e; k, e:=k—1, e—p od или же проще: ky e := n, s; do кф1 —* b[k] := b[k] + e\ k, e := k—1, e—q od; do ефО —> b[k] := b[k] + e\ k, e:=k — \, e—p od Каждый из циклов построен путем написания сначала инварианта, затем команды цикла, а затем определения подходящей охраны. Охрана ефО для второго цикла найдена из того наблюдения, что инвариант устанавливает e=p*(k—1) и равенство е=0 влечет либо /7=0, либо &=1. В свою очередь каждое из этих равенств означает, что все значения bli] уже окончательны. Обсуждение Построение этой программы поднимает некоторые интересные вопросы. Прежде всего, рассмотрим построение постусловия (20.1.6). Обычной ошибкой при написании этой спецификации является описание выровненной строки при помощи двух случаев: W1I/7+1]. . .[/7+1] Wdp+2]. . .[/7+2]Waz, если odd(z) WH/7+2]. . .lp+2\W&p+ll . .[p+UW/г, если even (г)
Гл. 20. Два больших примера построения программ 261 Хотя это и может привести к правильной программе, программа будет несколько менее эффективной, чем та, которая построена. В общем случае нужно пытаться следовать такому принципу: (20.1.12) Принцип. Сводите число различных случаев к минимуму. В только что рассмотренном примере даже двух случаев слишком много! Конечно же, когда-либо два различных случая должны быть приняты во внимание, но разумно отложить это настолько, насколько возможно. Стремление удерживать количество рассматриваемых случаев в разумных пределах является одним из оснований для того, чтобы не пользоваться таблицами решений. Вторая интересная тонкость — построение алгоритма для вычисления р, q и t. Задача появилась как нечто о строках символов, но превратилась в математическую задачу, требующую нахождения решений некоторых уравнений. Если вы не заметили, что случаи п=0 и лг = 1 не могут быть охвачены, вы недостаточно внимательны. Нахождение таких допущенных при построении ошибок является случайным и бессистемным процессом при отладке, но на самом деле в нем более нет необходимости. Каждый из введенных операторов (подобно -г- или mod) может исполняться лишь в некоторых условиях. Далее дисциплина программирования требует, чтобы всегда, когда построена команда S для установления {Q}5{/?}, доказывалось Q=>wp(S, R). Не имеет значения, делается это формально или же неформально, лишь бы только проявлять достаточную осторожность, чтобы сохранить уверенность в правильности. Последнее замечание касается построения цикла, вычисляющего b[\ : t]. Было бы легко написать его, скажем, на языке ПЛ/1 в форме е=0'у DO k=2 TO t\ е=е+р\ b(k)=b(k)+e; END; Но это помешало бы заметить то, что цикл можно написать и без потери возможности остановиться немедленно при р=0. Те, кто знакомы с использованием инвариантов цикла, построят инвариант и цикл, данные в (20.1.11), столь же легко, как и цикл на ПЛ/1. 20.2. Максимальная восходящая последовательность Рассмотрим последовательность значений (v0y . . ., vn_x). Если устранить из списка i (не обязательно идущих подряд) значений, получается подпоследовательность длины п—i. Такая подпоследовательность называется восходящей последовательностью, если ее значения не убывают. Например, в списке (1, 3, 4, 6, 2, 4) имеется подпоследовательность (1, 3, 2), не являющаяся восходящей, и другая подпоследовательность (1, 3, 6), являющаяся восходящей. Мы хотим написать программу, которая по данной последовательности Ь [0 ; п— 1], где /г>0, вычисляет длину ее максимальной,
262 Часть 111. Построение программ т. е. самой длинной, восходящей последовательности. В качестве сокращения воспользуемся обозначением lup(s), означающим lup(s)=Jimma самой длинной максимальной восходящей последовательности из s Таким образом, программа имеет пред- и постусловие Q i п>0 R ik=lup(b[0\ /i—l]) где переменная k используется для ответа. Заметим, что изменение любого значения из последовательности может изменить и ее максимальную восходящую подпоследовательность, а это означает, что, возможно, должно быть опрошено каждое значение s для определения lup(s). А это наводит на мысль о цикле. Начнем с написания возможного инварианта и наброска цикла. Цикл будет опрашивать значения из МО : п—1] в некотором порядке. Так как 1ир(Ы0 : 0]) — это 1, возможный инвариант цикла может быть получен путем замены в R константы п на переменную: P:l^i^nAk = lup(b[0:i—l]) Сам цикл будет иметь вид I, k : = 1,1; do 1фп —► увеличить i, сохраняя Р od Увеличение i расширяет последовательность МО ; i—1], для которой k является длиной максимальной восходящей подпоследовательности, и, следовательно, может потребовать возрастания k. Должно ли k возрастать, зависит от того, является ли Ь [i] таким же большим, как и значение, которым завершается максимальная восходящая последовательность в МО : i—1] (может быть более чем одна такая подпоследовательность). Имеет смысл сохранить в других переменных информацию, позволяющую сделать такую проверку эффективной. Какова минимальная информация, требуемая для того, чтобы убедиться, что k должно возрасти? Должно быть известно наименьшее значение (скажем, /п), завершающее восходящую последовательность длины k в Ь [0 : i—1], так как тогда Ъ [0 : i] имеет восходящую последовательность длины k+l, если и только если Ь [iX^m. Следовательно, пересматриваем инвариант Р для того, чтобы ввести в рассмотрение т: Р: 0^i^nAk = lup(b[0:i — l])A т является наименьшим значением в b[0:i—1J, завершающим восходящую последовательность длины k
Гл. 20. Два больших примера построения программ 263 В случае b [i\^m значение k может быть увеличено и b [i\ присваивается т, так что программа теперь выглядит так: i, k, т:= 1, 1, Ь[0]\ {Р} do i Ф п —* if b[i] ^ m-+ k, т:= k + l, b[t] D b[t] < m — ? fi i:=i+l od Теперь возникает вопрос: что же делать, если ЬЦ]<,пг? Переменная не должна увеличиваться, но что можно сказать о т? При каком условии т должно быть изменено? Если в b [0 : i—1] содержится восходящая последовательность длины k—1, которая заканчивается значением ^Ш], то МЛ заканчивает восходящую последовательность длины k в МО : Л. Если, кроме того, b[i]<Lm, то bli] должно быть изменено. Чтобы проверить это условие, рассмотрим сохранение минимального значения ml, которое завершает восходящую последовательность длины k—1 в МО : f—1]. Это значит, что нужны два значения: минимальное значение т, которым заканчивается восходящая последовательность длины k, и минимальное значение ml, которым заканчивается восходящая последовательность длины k—1. Следуя линии рассуждения, попробуйте обобщить это требование. Поддержание нужного т требует введения ml\ поддержание нужного ml потребует введения т2, которое содержит минимальное значение, заканчивающее восходящую последовательность длины k—2, и т. д. Следовательно, нужен массив значений. Изменим инвариант еще раз: (20.2.1) Р: 0<^i<^nAk = lup(b[0:i—l])A (у/: 1 < j^k:m[j] является наименьшим значением, заканчивающим восходящую последовательность длины / в b[0:i—1]) Программа изменится следующим образом: i, k, m[l]:= 1, 1, 6[0]; {Р\ do i Ф п —* if b[i] > m[k] —+ k:= k+l; m[k] : = b[i] D b[i] < m[k] — ? fi; od f:=i"+l Прежде чем продолжить рассуждения, имеет смысл исследовать массив т\ обладает ли он какими-либо полезными свойствами?
264 Часть III. Построение программ Массив т упорядочен, поскольку минимальное значение, заканчивающее восходящую последовательность длины /, должно быть не больше, чем минимальное значение, которым заканчивается восходящая последовательность длины /+1. Теперь перед нами стоит задача — определить, какие значения т [1 : к] должны быть изменены в случае Ь \i]<jm Ik]. Решите эту задачу. Самый легкий случай это b [i\<Lm [1]. Поскольку т [1] является наименьшим значением, которым заканчивается восходящая последовательность длины 1 в b [0 : i—1], то, если b \i]<m [l], 6 [i] является наименьшим значением в b [0 : Л и должен стать новым mil]. Ни одно другое значение изменять не нужно, так как все восходящие последовательности заканчиваются значением, большим, чем Ь [Л. И наконец, рассмотрим случай т [\]^Ь [i]<m [k]. Какие значения т должны быть изменены? Ясно, что могут изменяться лишь те, которые больше b [Л, так как они представляют минимальные значения. Так что предположим, что мы нашли такое /, что т [/—1]^ <й [Л<ш [/1. Тогда т [1 \ j— 1] не должно изменяться. Далее, так как восходящую последовательность длины /—1 заканчивает m[j—1], восходящую последовательность длины / заканчивает bli]. Следовательно, т [/] должно быть заменено на bli]. И наконец, /n[/+l; k\ не должно изменяться (почему?). Для нахождения может быть применен двоичный поиск (упр. 4 к разд. 16.3). Окончательная программа — это (20.2.2). Время выполнения программы (20.2.2) пропорционально в худшем случае п log n и в лучшем случае п. Она требует для массива т памяти, пропорциональной в худшем случае п. В ней используется прием, называемый динамическим программированием, хотя она и была построена без явной ссылки на него. (20.2.2) I, k, m[l]:= 1,1, 6[0]; {Р\ {инвариант: (20.2.1); ограничение: п — i\ do 1фп-+ ii b[t\^ m[k] -> k := k + l\ m[k]:=b\i] D b[i\<m[l]-+m[l]:=b[i] Q m[l]^b[i]<m[k]-+ Установить m[j — l]^.b[i] < m[j]: A, /:= 1, k\ {инвариант: 1 </i < j^k/\m\h\?C b[j]<tn[j]} {ограничение: j—h — 1} do Нф\—\ -+e:= (h + i)+2 if/n[e]^fr[/] —* h := e Q m[e\>b[i\-+i\=e fi
Гл. 20. Два больших примера построения программ 265 od m[j]:=b[i\ fi i:=l + l od Упражнения к гл. 20 1. (Бесповторные 5-битовые последовательности.) Рассмотрим последовательность из 36 битов. В каждой такой последовательности имеется 32 5-битовые последовательности стоящих подряд битов. Например, в последовательности 1101011... содержатся 5-битовые последовательности ПОЮ, 10101, 01011,... Напишите программу, которая печатает все 36-битовые последовательности со следующими свойствами: 1) первые 5 битов —00000; 2) никакие две 5-битовые подпоследовательности не совпадают. 2. (Ближайшая большая перестановка.) Пусть в массиве Ь[0:п—1] содержится последовательность (не обязательно различных) цифр, например /г = 6 и 6[0:5] (2, 4, 3, 6, 2, 1). Рассмотрим эту последовательность как целое число243621. Для любой такой последовательности (кроме таких, чьи цифры расположены в порядке убывания) существует перестановка цифр, которая приводит к ближайшему большему числу (составленному из тех же цифр). Например, таково (2, 4, 6, 1, 2, 3), которое представляет 246123. Напишите программу, которая по данному массиву Ь[0:п—1], имеющему следующую большую перестановку, изменяет Ь таким образом, чтобы он стал этой перестановкой. 3. (Неравные смежные подпоследовательности.) Рассмотрим последовательности единиц, двоек и троек. Последовательность называется хорошей, если в ней любые две лежащие рядом непустые подпоследовательности не совпадают. Например, хорошими являются следующие последовательности: 2 32 32123 1232123 Следующие последовательности плохие (т. е. нехорошие)'. 33 32121323 123123213 Известно, что существуют хорошие последовательности любой длины. Рассмотрим «лексикографическое упорядочение» последовательностей, при котором последовательность s/.<. последовательности s2, если si < s2 как дробные части десятичных дробей. Например, 123.<. 1231, так как 0.123<0.1231, 12.<.13. Отметим, что если мы допускаем присутствие нулей в последовательности, то s/|0. = .s/. Например, ПО. = .11, поскольку 0.110 = 0.11. Напишите программу, которая по данному фиксированному числу /г^0 записывает в массив Ь[0:п— 1] наименьшую хорошую последовательность длины /г. 4. (Генератор строк.) Дан некоторый текст, записанный по одному символу в элементы массива Ь[0:п—1]. Возможные символы — буквы А,..., Я, пробел и символ перевода строки. Текст рассматривается как последовательность слов, разделенных пробелами и переводами строк. Требуется программа, разбивающая текст на строки и записывающая в двумерный массив строки[0: чистрок—\г 0: длстроки — 1]. В этом массиве строки[0, 0: длстроки—\] —
266 Часть 111. Построение программ первая строка, строки[\, 0: длстроки—1] —вторая и т. д. Строки должны удовлетворять следующим свойствам: 1) ни одно слово не переходит со строки на строку; 2) в каждой строке содержится не более чем длстроки символов; 3) в строке содержится как можно больше слов, каждые два слова разделены одним пробелом. Строки заполняются до конца пробелами вплоть до символа длстроки— 1; 4) символы перевода строки означают конец строки, но не переходят в массив строки. б. (Кодирование перестановки.) Пусть N — целое число, N > 0, и пусть X [0: N—1] — массив, содержащий перестановку чисел 0, 1, ..., N—1: перест(Х [0: N-1], {0, 1, ..., Л/-1}) Для X можно определить другой массив X' [0: N—1] следующим образом. При каждом i элемент X' [i] является числом значений в X [0: i— 1], меньших X [i]. Например, приведем один из возможных массивов X и соответствующий ему массив X' при Af = 6: Х = (2, 0, 3, 1, 5, 4) Х' = (0, 0, 2, 1, 4, 4) Формально массив X1 удовлетворяет предикату (V*:0<« < A/:X'[i] = (N/:0</< i:X[j] < X [i])) Напишите программу, которая изменяет данный массив х, содержащий перестановку X, таким образом, чтобы он содержал соответствующее X'. В программе могут использоваться простые переменные, но не другие массивы, помимо х. 6. (Декодирование перестановки.) См. упр. 5. Напишите программу, которая по данному массиву х — Х', где X' — код перестановки X, восстанавливает X и записывает его в х. Нельзя пользоваться никакими другими массивами. 7. (Не жулики.) В массиве f[0:F—1] содержатся имена людей, работающих в Корнеллском университете, а в массиве g[0:G — 1] — имена людей, получающих пособие по безработице в Итаке. Оба массива упорядочены по алфавиту. Таким образом, ни в одном массиве не содержится дубликатов, и оба массива монотонно возрастают: fW<f[\]<f[2]<...<f[F-l) £[0]<£[1] <g[2] <...<g[G-\] Сосчитайте число людей, которые предположительно не мошенники, а именно тех, имена которых встречаются по крайней мере в одном массиве, а не в обоих. 8. См. упражнение 7. Предположим, что массив может содержать дубликаты, но массивы по-прежнему упорядочены. Напишите программу, которая подсчитывает число различных имен, которые не находятся в обоих списках, т. е. не считайте дубликаты. 9. (Период десятичного разложения.) При п > 1 десятичное разложение \/п является периодическим, т. е. оно состоит из начальной последовательности цифр di, ..., d/, за которой следует последовательность d{+li ..., d(+jt повторяющаяся вновь и вновь. Например, 1/4 = 0.250000..., так что повторяется последовательность 0 (i = 2 и /=1), в то время как 1/7 = = 0.142857142857142857..., так что повторяется последовательность 142857 (/ = 0 и / = 6, хотя можно было бы принять в качестве / и любое другое положительное число). Напишите программу для нахождения длины повторяющегося периода /. Пользуйтесь лишь простыми переменными, а не массивами, 10. (Задача Фейена.) Дан массив g[0:N—l], N^z2, удовлетворяющий уело-
Гл. 20. Два больших примера построения программ 267 вию 0^g[0]«^ .. <g[A/— 1J. Определим ui = g[0] + g[l] hk = hkm.t + 8[k\ при 1 < fc<N —1 Напишите программу, создающую массив X[0:2*N — 1], содержащий в возрастающем порядке значения £[0], .... gltf-1], ^ .... A.v-i Скорость выполнения программы должна быть линейной относительно N. 11. (Возведение в степень.) Напишите программу, которая по данным двум целым числам х^О и у > 0 вычисляет значение г = хУ. Дано также двоичное разложение у: bkmmi ,.. ЬгЬ0\ в программе можно ссылаться на i'-й бит разложения, пользуясь обозначением Ь[. Далее дано значение k. Программа должна начинаться при г=1 и пользоваться каждым битом двоичного представления один раз в порядке bk„1} 6^«2, ••# »
Глава 21 ОБРАЩЕНИЕ ПРОГРАММ Не правда ли, было бы интересно выполнить программу задом наперед, а именно вывести из программы Р другую программу Р"1, которая вычисляет обращение Р? Это означает, что выполнение сначала Р, а затем Р"1 дает то же, что и невыполнение никакой программы! Итак, если у нас есть результат выполнения программы Р, но потеряны входные данные, можно выполнить Р"1 и определить их Эта глава посвящена этому забавному обращению программ. Обращение некоторых простых программ Некоторые простые команды инвертировать легко. Обращением х := х+1 (записываемым как (х :=x+l)~i) служит х := х—1. Но некоторые команды необратимы. Например, вычисление обращения х : = 1 требует знания того, каким было значение до присваивания. Такая команда может тем не менее быть обратимой по отношению к некоторому предусловию. Например, обращением {х=3} x: = l служит {х=1} х :=3 Таким образом, выполнение первого фрагмента начинается при х=3 и заканчивается при #=1, а второго — наоборот. (Хорошенько запомните, как получается обращение: чтением в обратном направлении и преобразованием утверждения в команду, а команды — в утверждение. Такое преобразование само является некоторым видом обращения.) В этом примере показано, что можно вычислять обращения программ вместе с их пред- и постусловиями. Команда х := х*х необратима, поскольку два различных начальных значения х=2 и х=—2 дают один и тот же результат х=4. Для обратимости программа должна давать при разных входных данных разные результаты. Перестановка двух переменных Что является обращением х, у : = у, х? Читая символы, составляющие эту команду, в обратном порядке, мы получаем х, у := у> х. Ясно, что команда х, у := у, х является своим собственным обращением! Идея переписывания программы в обратном порядке для того,
Гл. 21. Обращение программ 269 чтобы получить ее обращение, уже запала нам в голову, так что попытаемся развить ее и дальше. Вычислим обращение (21.1) х := х+у\ у :== х—у\ х : = х—у Выполнение в обратном порядке с целью ликвидировать эффект выполнения последовательности команд означает следующее: сначала ликвидировать последнюю команду х: = х—у (т. е. выполнить ее обращение), затем ликвидировать вторую команду у :=х—у, а затем ликвидировать действие первой х := х+у. Запишем это следующим образом: индекс —1 означает обращение: (21.2) (х := х—у)'1; (у := х—у)'1; (х := х+у)'1 Обращением х := х—у служит х : = х+у, и наоборот. Вычислим обращение у := х—у. Этот оператор эквивалентен у := —(у~х), который эквивалентен у :=у—х\ у : =—у. Обращением этой последовательности служит у :== —у; у := у+х> который эквивалентен У .' = —У+х> последний в свою очередь эквивалентен у :== х—у. Итак, у :=х—у является своим собственным обращением, и (21.2) эквивалентно х := х+у\ у := х—у; х : = х—у Но тогда и (21.1) является своим собственным обращением! Доказательство того, что (21.1) переставляет значения х и у, оставлено в качестве упр. 1. Обращение команд общего вида После знакомства с самой идеей обращения рассмотрим некоторые обращения, которые понадобятся позже. При выполнении обращений не забывайте об общем методе чтения в обратном направлении. Обращение skip. Обращением skip будет piks, так что введем piks в качестве синонима для skip. Обращение SI; S2\ . . .; Sn. Согласно тому, что мы сделали перед этим, обращение последовательности команд — это последовательность обращений отдельных команд, выстроенная в обратном порядке: (SI; S2; . . .; S/iJ-^S/r1; . . .; S2"1; SI"1 Обращение х := cl\ S{x=c2}, где cl и с2 — константы. Это некоторый род «блока». Новой переменной л: задается начальное значение cl — выполняется 5, и после его завершения х имеет окончательное значение с2. При обращении х присваивается с2, затем выполняется обращение S, которое завершается при x=cl: (x:=cl\ S{x=c2})"1=x:-= c2\ S'^x^cl)
270 Часть 11L Построение программ Обратите внимание на то, как при выполнении обращения утверждение стало присваиванием, а присваивание — утверждением. Обращение команды выбора. Рассмотрим команду (21.3) {В\\/В2) if B\-+S\{R\) D B2-+S2{R2) fi {R\VR2) Выполнение ее должно начаться в состоянии, когда по меньшей мере одна из охран истинна, поэтому перед командой помещена дизъюнкция охран. При завершении исполнения истинно либо Rly либо R2 в зависимости от того, какая из команд выполнялась, так что постусловием является R1\JR2. Чтобы выполнить обращение (21.3), мы должны знать, выполнить ли обращение R1 или же R2y поскольку при выполнении (21.3) выполняется лишь одно из них. Для определения этого необходимо знать, истинно R1 или R2, а это означает, что они оба не могут быть истинны в одно и то же время. Следовательно, потребуем RlJ\R2=F. Для симметрии потребуем также B1/\B2=F. Теперь построим обращение (21.3). Начнем с конца (21.3) и будем читать по направлению к началу. Последняя строка (21.3) дает первую строку обращения: {R2\JR1}\\. Это имеет смысл; поскольку (21.3) заканчивается в состоянии, удовлетворяющем RlyR2y его обращение должно начинаться в состоянии, удовлетворяющем R2\/R1. Чтение в обратном направлении четвертой строки дает первую охраняемую команду: R2-+S2-4B2) Это должно пониматься следующим образом. Выполнение (21.3), начинающееся при истинном В2У выполняет S2 и устанавливает R2. А выполнение его обращения, начинающееся при истинном R2y аннулирует то, что сделало S2y устанавливая таким образом В2. Обратите внимание на то, как при обращении охраняемой команды с постусловием меняются местами охрана и постусловие. Продолжая читать в обратном направлении, получаем следующее обращение (21.3) (предполагая R1/\R2=F): (21.4) {R2\/R1\ if R2-+S2-1{B1\ и R1-+S1-4B1} H{B2\JB1)
Гл. 21. Обращение программ 271 Обращение команды повторения. Рассмотрим команду (21.5) do B1-+S1 od {-]B1} Цикл (21.5) снабжен очень бедной информацией: отмечено лишь то, что должно быть ложно в момент завершения. Кажется, что инвариант цикла не нужен для его обращения. Из предшествующего опыта обращения команды выбора мы знаем, что охраняемая команда, которую нужно обратить, требует постусловия. Далее можно ожидать, что "]В/ станет предусловием цикла (поскольку мы читаем в обратном направлении), и, следовательно, у цикла должно быть предусловие, которое станет постусловием. Два вхождения В1 в (21.5) подводят нас к тому, чтобы ввести другой предикат С1 следующим образом: (21.6) {-]С1} do Bl-^Sl-^Bl} od {-]C1\ А теперь легко произвести обращение: просто читаем задом наперед, обращая ограничители do и od и охраняемую команду так же, как это делалось в случае команды выбора. Обращением (21.6) является (21.7) {-]В1} do Cl-^Sl-^Bl} od {1С/} Обращение перестановки равных В разд. 16.5 была построена программа для перестановки двух неперекрывающихся сегментов равного размера n:b[i : i-\-n—1] и b[j : j+n—1], где ri^Q. Инвариантом цикла программ является O^k^n вместе с i i+Jfc-l i+k I+/I-1 j j+k-l j+k j+n~\ b переставл | непереставл | л Ъ I переставл J непереставл Ограничивающей функцией является п—&, а программой fc:=0; do кфп-+Ь U+k}y b [j+k] := b [j+k], b [i+k]\ k := k+l od Эта программа выглядит как «блок», в котором инициализируется новая переменная k. Для того чтобы воспользоваться ранее описанной техникой обращения блока, она должна иметь и постусловие, описывающее значение k. Это постусловие k=n есть дополнение к охране цикла. Для обращения цикла нам нужно иметь его предусловие и постусловие для его тела. Ими могут быть k=0 и кфО
272 Часть III. Построение программ соответственно. Таким образом, переписываем программу в виде (21.8) Л:=0; loop: {k=0\ do кфп~+ b[i + k], b[j + k]:=b[j + k], b[i + k}: od {k=n\ \k = n) Метка loop отмечает пять строк, нужных для инверсии цикла: цикл и его пред- и постусловия. Воспользовавшись правилом обращения блока, находим обращение программы в виде k i= n; loop~1{k=0} Воспользовавшись правилом для обращения цикла, находим loop'1 в виде pool: {k-n) do кфЪ^ (b[i + k], b\l + k]:=b[i + k], b[i + k];k:=k+l)-* {кфЩ od {* = 0} Далее тело цикла — обращение кратного присваивания в исходном цикле — есть k := k—U b [i+k], b [j+k] := b [j+k], b [i+k] Соединяя все это вместе, получаем обращение программы (21.8): k := п\ pool: {k = n\ do k ф 0 -> k:=k—\; b[i + k], b[j+k]:=b[i + k]9 Ь[1+к]{1гфп\ od {6 = 0} \k = 0\ Отметим, что исходная перестановка начиналась с первых элементов сегментов, а ее обращение начинается с перестановки последних элементов и продолжается тем же образом в обратном порядке. Заметим также, что (21.8) является своим собственным обращением, так что (21.8) имеет по крайней мере два обращения2^.
Гл. 21. Обращение программ 273 Обращение кодирования перестановки В упр. 5 к гл. 20 требовалось написать программу для решения следующей задачи. Пусть Л^—целое число, УУ>0, а Х[0 : N—1] — массив, в котором содержится перестановка целых чисел 0, 1, . . ., N—1. Формально (21.9) перестановка (Х[0 : Л^—11, {0, 1, . . ., N-]}) Определим для X другой массив Х'[0 : N—1] следующим образом. Элементами X'[i] являются числа значений X [0 : i—1], меньших X [i]. Например, приведем при N=6 один из возможных массивов X и соответствующий массив X': (21.10) Х = (2, 0, 3, 1, 5, 4) Х'=(0, 0, 2, 1, 4, 4) X' называется кодом X. Формально массив X' удовлетворяет условию (21.11) yi:0^i<N:X'[i]=(Nj:0^j<i:X[j]<X[q)) Написать программу, которая изменяет данный массив ху содержащий перестановку X множества {0, . . ., N—1}, таким образом, чтобы он содержал X'. Программа может пользоваться лишь простыми переменными, но не массивами. Теперь мы построим программу и обратим ее. Это построение доказывает конструктивно, что для каждой перестановки X существует в точности один код X' и наоборот. Программа должна преобразовать массив х, содержащий исходное значение X, в его код X'. Следовательно, возможной спецификацией является ttyi-.O^i < N:x[i]= X[i\)\ {(Vi'-0<i< N:x[i\ = X'[i])\ Должен быть изменен каждый элемент массива х, так что, вероятно, требуется цикл. Каков возможный инвариант цикла? Попытаемся написать цикл, который изменяет на каждом шаге одно значение х с начального на окончательное. Обычной стратегией в таком случае является замена константы в результирующем утверждении на переменную. Здесь можно заменить N или 0, что соответственно приводит к вычислению элементов массива в восходящем или в нисходящем порядке индексов. Что нам нужно сделать? В примере (21.10) значения X [N—11 и X' [N—1] одни и те же. Если последние значения X и X' всегда одни и те же, то может иметь больший смысл работать в нисходящем порядке индексов. Так что попытаемся доказать, что они всегда одни и те же.
274 Часть 111. Построение программ X[N—1] является последним значением X. Поскольку значения в X—это 0, . . ., N—1, имеется в точности X [N—1] значений, меньших X [N— 1] в X [О : N—2]. Но X' [N— 1] определено как число значений в X [О : N—2], меньших, чем X [N— 1]. Следовательно, X [N— 1] и X' [N— 1] совпадают. Заменяя в постусловии константу 0 переменной &, получаем первое приближение к инварианту: О <6 < NAiyi'k <* < N:*[*•] = X' [*]) Но инвариант должен отражать также и то, что нижняя часть х по-прежнему сохраняет начальное значение, так что перепишем его в виде 0<^k^NA(yi-k^i<N:x[q = X'[i])A (yi:0^i<k:x[i] = X[i]) Очевидная ограничивающая функция — это &, инвариант цикла может быть установлен присваиванием k : = N. При использовании такого инварианта по-прежнему возникают большие проблемы. Построение инварианта началось с замечания о том, что X [N—l]=X' [N—1], так что конечное значение х [N—1] то же, что и начальное. Чтобы обобщить эту ситуацию, было бы хорошо, если бы на каждом шаге х [k—1] содержало свое окончательное значение, но в инварианте это до сих пор не отражено. Обобщение действовало бы, если бы на каждом шаге х [О : k—1] содержал перестановку чисел {0, . . ., k—1} и код этой перестановки был равен X' [О : k—1]. Но это не так: в инварианте не говорится даже то, что х [0 : k—1] является перестановкой чисел {0, . . ., k—l). Может быть, удастся модифицировать х на каждом шаге цикла таким образом, чтобы это выполнялось? Перепишем инвариант: Р:0 ^ k ^ N A(Vi:k ^ i < N:x[i] = X' \i]A перестановка (x[0:k—1], {0, ..., k — 1})Л^Г^:^—П'— = Х/[0:А—1] Следовательно, программа будет иметь вид do k=£0-+k:=k — 1; Восстановить Р od Теперь возникает вопрос, как восстановить Р? Заметим, что после выполнения k :=k—1 в х[0 : k—1] содержится множество {0, . . ., k) без значения х [k]. Если мы вычтем 1 из каждого значения в х [0 : k—1], большего х Ш, то х [0 : k—1] будет содержать перестановку {0, . . ., k—1}. Например, если мы начинаем с * = (2, 5, 4, 1, 0, 3) и й=§
Гл. 21. Обращение программ 275 то первый шаг уменьшения k даст х=(2, 5, 4, 1, 0, 3) и k=5 и изменение х даст х=(2, 4, 3, 1, 0, 3) и £=5 Легко видеть, что перестановка (2, 4, 3, 1, 0) совместима с исходной в том смысле, что ее код тот же, что и первые пять чисел кода исходного массива (2, 5, 4, 1, 0, 3). В общем случае код х [0 i k—1]' для х [0 : k—11 будет тот же, что и X' [0 i k—l]9 поскольку в хЮ \k—1] значения идут в том же относительном порядке, что и в X [0:6—1]. Эти рассмотрения прямо приводят к программе (21.12). Инвариант внутреннего цикла достаточно прост для того, чтобы оставить его получение читателю. (21.12) ki=N\ do k=£0-+ £:=* —1; Вычитаем 1 из каждого члена х Г0:/? — 1] большего x[k]: /:=0 do }фк-»{х[\]фх[к]) if x[l]>x[k]-+ x[j]:=x[j]~ 1 D x[j]<x[k]-+ skip fi; od od Теперь мы хотим обратить программу (21.12). Первый шаг — это вставка таких утверждений, чтобы данные ранее способы обращения могли быть применены. Мы опустили пред- и постусловия команды выбора, так как они являются просто соответственно дизъюнкцией охран и постусловий команд. loopa: \k = N\ do k^=0-+ k:=k—l; /:=0; loopb: {/ = 0} do \Фк—+ if x[j]>x[k]rx[j]:=x[j]-\{x[j]^x[k]} D x [/] <x[k]-+ skip {x [/] < x [k]\ fl; /:=/ + l \i¥=o\
276 Часть III. Построение программ od </ = *} od {k = 0\ \k = 0\ Теперь обратим шаг за шагом всю программу, применяя правила обращения, приведенные раньше. Во-первых, обратим блок k := N\ loopa {k=0}y получая k := 0; loopa"1 {k = N}. Далее, loopa'1—это арсю/: {£ = 0} do к Ф N-^(k := k— 1; j := 0; loopb {j = £})-i {£ ф 0} od {£ = #} i '/ i / Продолжая тем же образом, получаем следующее обращение (21.11): k:=0; apool: {& = 0} do k^N-^ j := k\ bpool: {j=*k\ do (ф0-> if x[k] > *[/] — piks \x[k] > x[j]\ fi od; {/ = 0} {/ = 0} od или же без утверждений: k:=0; do k^N-*
Гл. 21. Обращение программ 277 do }ф0-+ if x[k] > x[j] —► piks D *[*]<*[/]- +x[j]:=x[j]'+l fi od; od Упражнения к гл. 21 1. Докажите, что wp((2\.l), х = Хлу = У) = х = У лу=Х. 2. Является ли х := */2 теоретически обратимым? А практически? Обратим ли х:= х-т-2? 3. Обратите программу Обращение массива (упр. 2 к разд. 16.5). 4. Обратите программу Обращение связей (упр. 6 к разд. 16.5). Какая трудность возникает в связи с этим? 5. Обратите программу из упр. 1 к разд. J8.1. 6. Постройте и обратите несколько ваших собственных интересных программ,
Глава 22 ЗАМЕЧАНИЯ О ДОКУМЕНТАЦИИ Почти все программы этой книги были написаны на языке охраняемых команд с добавлением кратных присваиваний, вызовов процедур и описаний процедур. Чтобы выполнить программы на машине, их обычно требуется перевести на Паскаль, ПЛ/1, Фортран или другой реализованный на ЭВМ язык программирования. Тем не менее по-прежнему имеет смысл пользоваться охраняемыми командами, поскольку метод построения программ тесно связан с ними. Вспомним принцип (18.3.11): программируйте для языка программирования, а не на нем. В данной главе рассматриваются проблемы, возникающие при написании программ как в терминах охраняемых команд, так и на других языках программирования. Даются общие правила документирования и оформления, описываются проблемы, возникающие в связи с определениями и описаниями переменных, приводятся примеры того, как можно перевести охраняемые команды на другие языки. 22.1. Размещение программы при печати Во времена становления программирования программы писались на Фортране и языке ассемблера совсем без отступов и выравнивания, и из-за этого их было трудно понимать. Некоторое облегчение приносило использование блок-схемы, поскольку они давали двумерное представление программы, более ясно отражающее ее структуру. Сохранение двух различных форм программы — самого текста программы и блок-схемы — всегда было чревато ошибками, поскольку трудно было поддержать их совместимость. Более того, многие программисты никогда не любили вычерчивать блок-схемы и часто они создавали их лишь после того, как программа была закончена, и лишь потому, что блок-схемы требовались в качестве документации. Таким образом, польза, которую могли бы принести блок-схемы, отсутствовала именно тогда, когда она была наиболее нужна,— при построении программы. Выравнивание команд в программе, следующее некоторым простым правилам, обеспечивает такое ее двумерное представление, которое достаточно просто показывает ее структуру. Отступы и выравнивание — это то, что программист может делать в процессе
Гл. 22. Замечания о документации 279 программирования как само собой разумеющееся, по привычке. Следовательно, требуется только один документ — программа с отступами. Все проблемы, связанные с согласованием двух форм представления, исчезают. Хорошее выравнивание команд устраняет необходимость в блок-схемах. Приведем некоторые простые правила для отступов и выравнивания, которые могут использоваться в самых распространенных языках программирования. В этих правилах могут быть небольшие отличия для разных языков, но в главных чертах они остаются одними и теми же. Последовательное соединение Многие соглашения о программировании вынуждают программиста размещать каждую команду на отдельной строке. Это приводит к непомерному разбуханию программы, которая уже не умещается на одной странице. Следовательно, становится трудно следить за отступами. Правило, рекомендуемое нами, следующее: (22.1.1) Правило. Последовательные команды могут располагаться на одной строке, если они логически составляют единое целое. Приведем пример. В программе (20.2.2) для установления инварианта цикла Р использовалась следующая команда: i9 ky m [1] : = 1, 1, Ь [0]. На языке ПЛ/1, в котором нет команд кратного присваивания, ее можно записать в одну строку следующим образом: j=l; k=l; m(l)=b(0)\ /*/V Эти три присваивания вместе выполняют единую функцию установки Р. Нет оснований заставлять программиста записывать их таким образом: *= 1; /я(1)= 6(0); (Заметьте, что присваивания в ПЛ/1 записываются без пробела слева от знака - и с одним пробелом справа от него. Так как в Фортране одни и те же символы используются и для присваивания, и для равенства, программисту надлежит сделать так, чтобы они выглядели различными.) Не пользуйтесь правилом (22.1.1) в качестве разрешения на то, чтобы отвести на программу как можно меньше места; применяйте его осторожно и разумно. Правило, касающееся выравнивания последовательности команд, очевидно. (22.1.2) Правило. Команды последовательности, начинающиеся на разных строках, должны начинаться в одном и том же столбце.
280 Часть III'. Построение программ Таким образом, не следует писать £ = 1; т(1) = Ь(0); Выравнивание продолжений Правило, относящееся к продолжениям, следующее: (22.1.3) Правило. Начинайте продолжение команды на три-четыре колонки правее той, где начинается сама команда (если это разумно, то можно отступать и на большее число колонок). Например, пишем: do a+l фЬ-+с1:= (а + Ь)+2; if d*d^n —+ a:= d [] d*d> n—±b:=d fi od или же на ПЛ/1: DO WHILE (а+1~1=Ь); d = FLOOR((a + &)/2); IF d*d^n THEN a = d; ELSE b = d\ END; Заметим, что тело цикла пишется с отступом. Далее тело является последовательностью двух команд, которые в соответствии с правилом (22.1.2) начинаются в одном и том же столбце. Подкоманды условного оператора языка ПЛ/1 также пишутся с отступом по отношению к его началу. Условный оператор языка ПЛ/1 можно было бы записать и так: IF d*d<tt THEN a = d; ELSE b = d] или даже в одну строчку, поскольку он короток и прост] IF d*d<n THEN a=d\ ELSE b=d\ He имеет значения, каким соглашениям следовать относительно точного метода размещения THEN и ELSE, до тех пор пока эти соглашения 1) согласуются между собой и 2) следуют правилу (22.1.3), так что легко проследить структуру программы. Согласованность и последовательность применяемых соглашений важны, чтобы читатель мог знать, чего ему ожидать в данном месте программы.
Гл. 22. Замечания о документации 281 Отступы могут и должны использоваться и в Фортране. Приведенный выше цикл может быть записан на Фортране-77 следующим образом *): С DO WHILE (а+\*Ь)\ • < • 05 IF(flf+l .NE. b) GOTO 25 10 rf=FLOOR((fl+6)a) JF(d*d-n) 12,12,14 12 a=d GOTO 20 14 b=d 20 GOTO 05 25 CONTINUE Утверждения Лучшему пониманию программы помогает, как уже упоминалось в гл. 6, помещение в нее утверждений. Следует включать достаточное количество утверждений, чтобы программист смог понять программу, но не слишком много, чтобы он погряз в груде деталей. Конечно, самое важное утверждение — инвариант цикла. На самом деле, если программа снабжена предусловиями, постусловиями, инвариантами и ограничивающими функциями каждого цикла, оставшиеся пред- и постусловия могут быть в принципе порождены и автоматически. Конечно, в языках, которые не допускают утверждения в качестве своих конструкций, утверждения должны фигурировать в качестве комментариев **). Выравнивание утверждений осуществляется согласно двум правилам: (22.1.4) Правило. Пред- и постусловие команды должны начинаться в той же колонке, что и команда. (22.1.5) Правило. Перед циклом должны стоять его инвариант и ограничивающая функция. Они должны начинаться с той же колонки, что и начало самого цикла. Этими правилами мы пользовались здесь все время, так что они теперь уже должны казаться естественными. Два примера использования правила (22.1.5) можно увидеть в программе (20.2.2). *) Мои старые привычки времен Фортрана II (с 1960 г.) привели к тому, что я написал \F(d*d — п) 12, 12, 14. Замените этот условный оператор на более современный. **) В Аде допускались утверждения в качестве конструкций до того, как она «достигла совершеннолетия». Ставшая зрелой Ада их уже не допускает.
282 Часть 111. Построение программ Выравнивание ограничителей Существует три соглашения о выравнивании ограничителя, завершающего команду (например, od, fi или END; языка ПЛ/1). Согласно первому из них, ограничитель помещают на отдельной строке с той же колонки, что и начало команды. Именно это соглашение использовалось повсюду в нашей книге. Согласно второму соглашению, ограничитель помещается там же, где и подкоманды данной команды, как в цикле языка ПЛ/1: DO WHILE (выражение)-, END; Это соглашение обладает тем преимуществом, что команду, следующую за данной, определить легко: просто ищется команда, начинающаяся в той же колонке, что и DO WHILE. При третьем соглашении ограничитель полностью помещается на последней строке команды, например DO WHILE (выражение) END; или же do охрана —* ... '.!'. od Это соглашение показывает, что правила отступов и выравнивания делают ненужными завершающие ограничители. Это означает, что, если для установления структуры программы транслятор пользуется выравниваниями, завершающие ограничители больше не являются необходимыми. Ограничители по-прежнему пишутся, поскольку они обеспечивают полезную избыточность, которая может быть проверена транслятором, но от взора они скрыты. Не важно, каким из трех соглашений пользоваться, важно быть последовательным, чтобы читателя не ожидали сюрпризы. (22.1.6) Правило. Пользуйтесь последовательно тем соглашением о выравнивании завершающих ограничителей, которое вами принято. Команды-комментарии Некоторые из представленных в книге программ (подобно программе (20.2.2)) использовали в качестве меток или комментариев предложения естественного языка (с последующим двоеточием в случае меток). На самом деле текст на естественном языке был коман-
Гл. 22. Замечания о документации 283 дой, требующей что-то сделать, а программа, выполнявшая эту команду, была помещена с отступом непосредственно под предложением. Например, Установить z равным максимуму хну: if х^у —* z := х D y>x-+z:=y fi В языке Паскаль предложение естественного языка было бы комментарием, и, поскольку комментарии ограничены (* и *), оно выглядело бы следующим образом: (* Установить z равным максимуму х и г/*) if х^ у then z := х else z := у При чтении программы, содержащей такие команды-комментарии, они рассматриваются так же, как и любые другие команды программы. Программный текст, расположенный с отступом непосредственно под ними, рассматривается как уточнение такой команды-комментария: это — сегмент программы, показывающий, как выполнять команду-комментарий. Читая программу, содержащую команду-комментарий, к ее уточнению нужно обращаться лишь для того, чтобы понять, как это уточнение работает; в других случаях нужно читать лишь саму команду-комментарий, в которой объясняется, что должно быть сделано. Командами-комментариями можно пользоваться для того, чтобы разбить программу на куски и уменьшить объем текста, который должен просмотреть по порядку читатель программы, чтобы что-то найти. Так же как двоичный поиск позволяет найти значение в упорядоченном массиве за логарифмическое время, разумное использование команд-комментариев позволяет просмотреть программу для того, чтобы что-то найти за более короткое время. Использование команд-комментариев во время написания программы может оказать неоценимую помощь программисту, так как оно заставляет его быть точным и аккуратным при структурировании программы. Для того чтобы команды-комментарии принесли еще большую пользу и для того, чтобы они согласовывались с представленной в данной книге методикой, их следует писать до их уточнений. Команды-комментарии должны быть точными. Они должны точно выражать в терминах входных и выходных переменных то7 что делает их уточнение. Например, команда-комментарий Сложить элементы массива Ъ
284 Часть III. Построение программ недостаточно точна, так как она заставляет прочитать свое уточнение, чтобы определить, куда же помещена сумма элементов массива. Намного лучше команда-комментарий Записать в х сумму элементов массива Ъ [0 : п—1] или же При данном фиксированном п^О и фиксированном массиве Ь установить x=(2j : 0^Lj<Cn : b [/]) Как видно из последнего примера, команда-комментарий может иметь ту форму, которая использовалась для спецификации (сегментов) программ на протяжении всей данной книги. Приведем выравнивания для команд-комментариев. (22.1.7) Правило. Команда-комментарий помещается там же, где помещалась бы на ее месте любая другая команда. Следующее за ней ее уточнение сдвигается на три-четыре столбца вправо. Некоторые пользуются соглашением, по которому и команда-комментарий, и ее уточнение начинаются в одном и том же столбце, например (*Установить z равным максимуму х и #*) if х^ у then г := х else z := у\ £:=20 Основание для того, чтобы не пользоваться таким соглашением, ясно из примера: нельзя сказать, где заканчивается уточнение команды. Намного лучше пользоваться правилом (22.1.7): (* Установить z равным максимуму х и #*) if х^у then z := х else z := у fe:=20 Может помочь и разумное использование пробелов (пропущенных строк), но никакое простое правило по пропуску строк после уточнений не может охватить все возможные случаи, если уточнения не пишутся с отступом. Так что следуйте правилу (22.1.7). Еще одно замечание касается выравнивания комментариев. Не вводите их таким образом, чтобы структура программы оказалась скрытой. Например, если последовательность команд начинается с колонки 10, никакой комментарий, находящийся между ними, не должен начинаться с колонки, расположенной левее колонки 10. Делайте сегменты программы маленькими Одним из способов сохранять программу в состоянии, поддающемся разумному управлению,— делать программные сегменты
Гл. 22. Замечания о документации 285 разумной длины, так как число деталей, которые можно понять за один прием, ограничено. Обычное правило говорит, что длина процедурной части (не включающей спецификации и описаний) сегмента программы не должна превосходить одной страницы. Это не очень большое ограничение, если соответствующим образом используются процедуры и макрокоманды, обеспечивая высокий уровень абстракции и структурности. На самом деле часто даже бывает трудно сделать программный сегмент столь длинным. Ограничение одной страницей помогает также сохранять подходящее размещение программы при печати. Заголовки процедур Как упоминалось в гл. 12, назначение процедуры — обеспечить более высокий уровень абстракции: тому, кто ею пользуется, нужно знать лишь то, что делает процедура и как ее вызывать; а как процедура работает, знать не требуется. Чтобы подчеркнуть это, описание процедуры должно размещаться следующим образом: (22.1.8) Правило. Заголовок процедуры, включающий список параметров, их спецификации и описание того, что делает процедура, должен размещаться там же, где размещалась бы любая другая команда в данном контексте. Тело процедуры сдвигается на три- четыре столбца вправо по отношению к заголовку. Может иметь смысл пропустить по строчке до и после описания процедуры, чтобы отделить его от окружающего текста. В качестве примера приведем описание процедуры на языке типа Паскаль: (*Предусловие: n = N f\x = X/\B = Bf\X £B[0:N— 1]*) (^Постусловие: О < i < n/\B[t\ = X*) ргос поиск (value n> х: integer; value b: array of integer; result i: integer); тело процедуры Может быть, стоит давать пред- и постусловия менее формально (но не менее точно), как это показано ниже. Часто такую запись понять легче, чем чисто предикатную. (*Даны фиксированные п, х и Ь[0: N—1], удовлетворяющие х£Ь*) (*3аписать в i значение, устанавливающее х = b [i]*) ргос поиск (value ny х: integer; value b: array of integer; result i: integer); тело процедуры
286 Часть III. Построение программ В качестве дополнения опишем проблемы, возникающие в связи с программированием на ПЛ/1, На языке ПЛ/1 спецификации параметров и описания переменны выглядят одинаково и могут быть перемешаны. Например, можно написать /*Даны фиксированные пу х и b(0:N—1), удовлетворяющие x£b*i /*3аписать в i такое значение, что л; = &(/)*/ поиск: PROC (n, x> b, i)\ DCL (л, х, &(*), k9 i) FIXED; тело процедуры END; При написании вызова процедуры поиск требуется знать типы ее параметров, и при чтении этих типов мы наталкиваемся на описание локальной переменной k. Чтобы устранить эту проблему и предоставить читателю программы лишь то, что необходимо ему для написания вызова процедуры, нужно отделить спецификации параметров от описаний локальных переменных: /*Даны фиксированные я, х и Ь(0:п—1), удовлетворяющие x£b*/ /*3аписать в i такое значение, что л; = &(*")*/ поиск: PROC(az, x, b, i); DCL (/i, *, &(»), г) FIXED; DCL k FIXED; тело процедуры END; 22.2. Определения и описания переменных Определение переменных Приведем одну из простых и наиболее важных стратегий работы с переменными. (22.2.1) Стратегия. Перед использованием переменных определите их, а затем убедитесь, что их использование следует их определению. Эта стратегия кроется за многим из того, что было изложено в данной книге. Определение некоторого множества переменных — это просто утверждение о их логической взаимосвязи, которое должно быть истинным в некоторых ключевых точках программы. Подобным же образом инвариант цикла является лишь определением множества переменных, которое выполняется перед каждым шагом цикла и после него. Теория воздушного шарика из разд. 16.1 дает просто некоторые эвристики для построения (в некоторых случаях) определений переменных, исходя из спецификации программы. Правило (22.2.1) выглядит совершенно очевидным, но тем не
Гл. 22. Замечания о документации 28? менее именно ему трудно научиться и следовать на практике. Не раз находил я ошибки в программах (когда их авторы не могли сделать этого или даже считали, что программа правильна), задавая решающий вопрос: А что означают эти переменные? Проводя вместе с автором программы примерно десять минут за выяснением того, что же должны означать переменные, я указывал затем места в программе, разрушающие значения этих переменных. Ключевое место в данном вопросе — точно определить переменные перед их использованием и строго следовать введенным определениям. Определения переменных важны точно так же при чтении программы, как и при ее построении. Читателю должны быть предложены вначале определения вместе с текстом, помогающим их понять. А когда они поняты, сама программа часто становится очевидной. Кроме того, просто неэтично предлагать программу без точных определений переменных. Размещение определений переменных Подходящим местом для определений (большинства) переменных является так же, как и для их описаний, начало программы. Это приносит некоторые преимущества: 1. Такое решение заставляет нас группировать переменные в соответствии с логическими связями между ними (вместо того чтобы группировать их по типам или в произвольном порядке). Описания каждой группы логически связанных переменных должны размещаться единой группой вместе с их определением и, возможно, отделяться пустыми строчками от других групп. 2. Определения, если они написаны достаточно рано и точно, дают в руки программисту добавочное орудие проверки. Всякий раз, когда он напишет текст в программе, который изменит одну из переменных некоторой группы, он сможет обратиться к ее определению и посмотреть, каким образом должны быть изменены другие переменные, чтобы сохранилось их определение. Заметим, что и методы программирования, рассмотренные в третьей части книги, ориентированы на определение переменных до их использования. Спецификации программы, инвариант цикла и т. д. предшествуют соответствующему тексту программы. 3. Читателю известно, куда ему следует посмотреть, чтобы понять, как используется переменная: ее описание сопровождается ее определением, а также определением логически связанных с ней переменных. 4. В комментариях внутри программы, например в командах- комментариях, можно ссылаться на определения переменных и таким образом укорачивать программу. Например, вместо того чтобы написать команду-комментарий
288 Часть 111. Построение программ Прибавить 1 к п, затем присвоить bin] . . ., с . . ., a d[n] максимальное значение . . ., а затем, если е такова, что . . . добавить ... к / . . . можно написать просто Прибавить 1 к п и восстановить его определение. Тогда читателю остается лишь прочитать определение и посмотреть, какое утверждение должно быть установлено вновь. Примеры описаний и определений Приведем простой пример на языке Паскаль, чтобы проиллюстрировать соответствующее размещение описаний и определений. Читателю нужно уметь применять такое размещение и в более сложных и длинных последовательностях описаний. Переменные используются программой, хранящей список сотрудников, их номера телефонов и отделы, в которых они работают. Внутри программы будет иногда необходимо строить и обрабатывать список людей, работающих в некотором конкретном отделе, и их номеров телефонов. Оба списка будут храниться в алфавитном порядке. Будут использоваться следующие описания типов данных type String24 = packed array[1..24] of char\ String8 =packed array [1..8] of char; Emprec =record (*запись о сотруднике*) name: String2A\ (*ФИО сотрудника*) phone, integer', (*номер телефона (7 цифр)*) division: String^; (*отдел*) end; Phonerec = record name: String24; (*ФИО сотрудника*) phone: integer; (*номер телефона (7 цифр)*) end Заметим, что задана и форма записи имени сотрудника. А теперь приведем пример того, как не нужно делать описания 'основания для критики будут приведены ниже): var staff: array [0.. 10000] of Emprec; phones: array [0.. 1000] of Phonerec; staffsize, divsize, i, /: integer; div: char; q: Phonerec; Эти описания страдают несколькими недостатками. Во-первых, переменные не сгруппированы в соответствии с логическими взаимосвязями. Из имени переменной staff size можно вывести, что она связана логически с массивом staff, но это не обязательно так.
Гл. 22. Замечания о документации 289 Кроме того, невозможно понять цель или мотивы, по которым введена переменная divsize. Далее определения переменных, которые важны для всего текста, перехмешаны с определениями локальных переменных, используемых лишь в немногих лежащих рядом местах текста (например, i и /'). Кроме того, отсутствуют определения переменных. Например, как мы можем знать, где именно находятся сотрудники в массиве staff? Помещены ли они в начале этого массива, в его конце или же в середине? Не отмечено также и то, что списки упорядочены. Дадим лучший вариант этих описаний: var staff: array [0..1000] (*staff[0:staffsize—l] — *) of Emprec; (*записи о сотрудниках*) staffsize: integer; (*в порядке алфавита*) phones: array [0..1000] (*phones[0-.divsize—1] — *) of Phonerec; (*сотрудники отдела*) divsize: integer (*whichdiv, *) whichdiv: StringS; (*в порядке алфавита*) /, /: integer; q: Phonerec; Теперь переменные сгруппированы в соответствии с их логическими взаимосвязями и даны определения, которые описывают эти взаимосвязи. На самом деле эти определения являются инвариантами (но не инвариантами цикла), которые выполняются (почти) во всех точках программы. Переменные i, /, qt как предполагается, будут использованы лишь в ограниченном количестве мест, и, следовательно, они не нуждаются в определении. Посмотрите внимательно на форму определений. Сами переменные начинаются в одном и том же столбце, что облегчает нахождение нужной переменной. Далее комментарии, описывающие каждую группу переменных, располагаются справа от них и опять-таки начинаются в одном и том же столбце. Имеет смысл потратить несколько минут на такое выравнивание описаний, поскольку это поможет и программисту, и тому, кто будет читать программу. Еще одно замечание: нет ничего хуже, чем комментарии типа и является индексом в массиве Ь». При определении переменных воздерживайтесь от употребления слов-паразитов типа «указатель», «счетчик» или «индекс», так как они служат лишь указателями на вашу лень и нечеткость ваших мыслей. 22.3. Написание программ на других языках До тех пор пока кратные присваивания и охраняемые команды не станут частью реализованных систем программирования, необходимо будет переводить программы на языки Паскаль, Фортран, Ю Д. Гриз
290 Часть III. Построение программ ПЛ/1 или другие, чтобы иметь возможность выполнить их на машине. Кратные присваивания, команды выбора и повторения должны быть смоделированы при помощи команд того языка, на который переводится программа. Иногда перевод легок. Например, команду повторения с одной охраняемой командой можно записать при помощи цикла while языка Паскаль или ПЛ/1. Команду выбора можно записать детерминированным образом, пользуясь операторами типа case или SELECT. Однако команда повторения с более чем одной охраняемой командой не имеет простого аналога в перечисленных выше языках и должна моделироваться. Например, рассмотрим программу (16.4.5) для нахождения «жулика на пособии». Она ищет первое значение, такое, что / [iv]= =g [jv]=k [hv] (которое, как известно, существует) в трех упорядоченных массивах / [0 i ?], g [О :?] и h [О :?]; i, /, *:=0, 0, 0; [инвариант: 0 < i ^ iv/\0 <gl / < /иДО ^ k < kv) {ограничение*, i—iv + j—\v-\-k—kv) do f[i]<g[j]-+i:=i+l D h[k]<f[t]-+k:=k+l od {i = iv/\j = jvf\k = kv) На ПЛ/1 эту программу можно записать следующим образом: (=0; / = 0; 6=0; /*Моделирование цикла с тремя охраняемыми командами:*/ I*инвариант: 0 < i < шДО < / < /^Л° < k < kv*/ /*ограничение: i—iv+j — jv + k—kv*/ LOOP: IF f(i)<g(i) THEN DO; / = / + 1; GOTO LOOP\ END; IF g(i)<h(j) THEN DO; / = *+i; GOTO LOOP] END; IF f(i)<g(j) THEN DO; k = k+\\ GOTO LOOP; END; /*; = шЛ/ = jv/\k = kv*/ Здесь используются следующие соглашения. Моделирование цикла из охраняемых команд состоит из комментария, указывающего на факт моделирования, метки LOOP, на которую осуществляется переход для следующего шага, и условного оператора для каждой охраняемой команды цикла. Заметьте, что на каждом шаге цикла будет выполняться точно одна из охраняемых команд. То же самое можно сделать и на Фортране, хотя этот язык еще больше препятствует краткости выражений, чем ПЛ/1, что показано ниже. В случае Фортрана для того, чтобы отмечать конец каждого из моделируемых циклов с охраняемыми командами, используется оператор CONTINUE. Он также отмечает место, куда нужно переходить после окончания цикла. Не помечайте следующий за
Гл. 22. Замечания о документации 291 циклом оператор, чтобы передавать управление на него вместо CONTINUE, поскольку тогда моделируемый цикл больше не будет независимым от остальной части программы. Должна иметься возможность выбрать любую команду, выполняющую некоторую работу, и перенести ее в другую программу без изменений, но это невозможно, если в команде содержится передача управления наружу. С Моделирование цикла из трех охраняемых команд — С метки 20—26 С инвариант: 0 <л^ iv/\0^. /^ jvf\0 ^.k^kv С ограничение: iv—i + kv—k + jv—/ 20 IF(/(/) GE. g(j)) GOTO 22 GOTO 20 22 IF (g(j) .GE. h(k)) GOTO 24 /-Ж GOTO 20 24 IF (h(k) .GE. /(0) GOTO 26 GOTO 20 26 CONTINUE С {i —* iv/\j *= jv/\k = kv) Эти примеры показывают, как можно подходящим образом смоделировать охраняемые команды на других языках. Постарайтесь пользоваться для каждой команды повторения одними и теми же соглашениями о моделировании. За исключением тех случаев, когда крайне важна эффективность программы, не пытайтесь воспользоваться вашим знанием программы для того, чтобы сделать это моделирование проще или эффективнее. Для вашей же собственной пользы и для пользы тех, кто будет читать вашу программу, пользуйтесь для всех подобных друг другу конструкций одними и теми же соглашениями. Сейчас мы приведем программу, написанную на четырех разных языках: языке, использовавшемся в данной книге, Паскале, ПЛ/1 и Фортране. Каждая версия программы полностью документирована, исходя из предположения, что никакой другой текст не будет сопровождать программу. Программа на языке данной книги {Имеется п слов, п^0, на строке номер z, которые начинаются в столбцах Ь [1], . . ., Ь [п]. Каждые два смежных слова разделены в точности одним пробелом. Величина s, s^0,— это общее число пробелов, которые нужно вставить для того, чтобы выровнять строку. Определить новые номера столбцов b [1 : п]9 представляющие выровненную строку. В результирующем утверждении /?, ю*
292 Часть III. Построение программ приведенном ниже, говорится, что число пробелов, вставляемых между разными словами, различается не более чем на 1, и добавочные пробелы вставляются в зависимости от номера строки слева или справа. Выровненная строка при п>\ должна иметь следующий формат, где W7 есть i-e слово* Wl[p+ 1 пробелов].. .[p+\]Wt[q+l].. .[<?+ IJWaz где р, q, t удовлетворяют условию Q1:1 <* <яЛ°<М0<?ЛР* ('—!) + ?* (я—*)в$Л ((odd(z)/\Q~P + l)\/(even(2)AP-*q+l)) Результирующее утверждение R (В—начальное значение массива Ь)\ R:(0^n^l/\b = B)\/((vi:l < i < t:b[i]~B[i\ + p« (i — 1))Д (Vi:t^i^nib[i] = B[i] + p*(t—l) + q*(i — t)))\ ргос выровнять (value ny z, s: integer] var b: array of integer); var p, q9 t, ey k: integer; if n ^ 1 —■#- skip Определить /?, q и t: if even(z) —+ q: = s-f- (n— 1); /i=l-Hs mod (a? — 1)); Qodtf(e) —+pi= s^-(n — 1); t := n—(s mod (/z— 1)); fl; Вычислить новые номера столбцов £[1:я]: ky e := n, s; {инвариант: t^k^ n/\e = p*(t — 1)-Ky* (/?—1)/\ b[l:k]=zB[\ :k]/\b[k+ l:n\ имеют окончательные значения} do k=fit —*b[k] i = b[k] -\-e\ к, e:=k — 1, e—q od; {инвариант: l^k^t/\e = p*(t — 1)Д b[l :A] = B[1 :k]/\b[k+ \:n] имеют окончательные значения) do ефО—>b[k]|:= b[k]+e\ k, e:=k—l, e—p od fi Программа на Паскале (* Имеется п слов, п^О, на строке номер г, которые начинаются в столбцах 6(1), . . ., Ь(п). Каждые два смежных слова разделены в точности одним пробелом. Величина s, s^O,— это общее число пробелов, которые нужно вставить для того, чтобы выровнять строку. Определить новые номера столбцов b{\ i /г), представляющие выровненную строку. В результирующем утверждении R, приведенном ниже, говорится, что число пробелов, вставляемых
Гл. 22. Замечания о документации 293 между разными словами, различается не больше чем на 1, и добавочные пробелы вставляются в зависимости от номера строки слева или справа. Выровненная строка при п>1 должна иметь следующий формат, где Wi есть i-e слово: Wl[/?+ 1 пробелов]. ..[/?+ \]Wt[q+ 1].. .[q + l]W/i где р, q, t удовлетворяют условию Ql:l <г<яЛ0<М°<<7Лр*(^ —-l) + q*(n—/) = sA {{odd{z)r\q^PJr^\/even{z)f\p^q^\) Результирующее утверждение R (В—начальное значение массива Ь)\ R:(0^n^\Ab = B)V((yiil^i^t:b(i) = B(i) + p*(i—l))/\ (V*li <i<n:b(i) = B(i) + p*(t — \) + q*(i—t))) *) procedure justify(n, z, s: integer; var b: array of integer); var p, q, t, e> k: integer begin if n > 1 then begin (*Определить p, q, t: *) if г mod 2 = 0 then begin q:=s div (ai—1); /: = l + (s mod (n—1)); /?:= q+ 1 end else begin p:=s div (/2—1); 1: = n — (s mod (ft—1)); q:=p+l end; (*Вычислить новые номера столбцов &(1:я):*) & := я; е : = s; (^инвариант: t^.k^nf\e — p*(t — 1) + */* (k —1)/\ b(l:k)~B(l:k)/\b(k+\:n) имеют окончательные значения*) while k< >t do begin b(k)i= b(k) + e; k:=k—l\ e := e—q end; (*инвариант: \^k^.t/\e~p*(t — 1)Д b[\ik] = B(\:k)Ab(k+l:n) имеют окончательные значения *) while e< >0 do begin b{k) i = b(k) + e\ k\~k — \; e:= e—p end end end Программа на ПЛ/1 /* Имеется п слов, ft^O, на строке номер г, которые начинаются в столбцах 6(1), . , ., Ь(п). Каждые два смежных слова разделены в точности одним пробелом. Величина s, s^O,— это общее число пробелов, которые нужно вставить для того, чтобы выровнять строку. Определить новые номера столбцов Ь{\ • п), представляющие выровненную строку. В результирующем утверждении R, приве-
294 Часть 111. Построение программ денном ниже, говорится, что число пробелов, вставляемых между разными словами, различается не больше чем на 1, и добавочные пробелы вставляются в зависимости от номера строки слева или справа. Выровненная строка при п>\ должна иметь следующий формат, где Wt есть i-e слово: W1 [р+ 1 пробелов]. . .[р+ 1] W/ [q+ 1].. .[q+l]Wn где р, q, t удовлетворяют условию Ql:l </</2Л0<рЛ0<^Лр*(<—1)+^*(л —0 = «Л ((odd(z)Aq = P+l)V(even(z)Ap = q+l)) Результирующее утверждение R (В—начальное значение массива Ь): R:(0^n^lAb = B)\/((yi:\<^i^t:b(i) = B(i) + p*(i---l))A (Vt-t <:i <п:Ь (i) = В (i) + р* (t — l) + q* (i—t))) */ justify: PROC(n, z, s, b)\ DECLARE (я, г, s, &(»)) FIXED; DECLARER, p, t, e, k) FIXED; IF n> 1 THEN DO; /*Определить pt q, t:*/ IF MOD (г, 2) = THEN DO; q=s/(n—l)\ / = l+MOD(s, (n — 1)); p = <7+l; END; ELSE DO; p = s/(n—l); / = /г—MOD(s, (я — 1)); ?==p+l; END; /* Вычислить новые номера столбцов b(l:n)*/ k = n\ e = s\ /* инвариант: t^Lk^nAe — pxit—l) + <7* (k—t)A b (1: k) = В (1: k) A b (k + 1: n) имеют окончательные значения */ DO WHILE (* 1=0; b(k) = b(k\ + e\ k = k — \\ e = e—q\ END; I* инвариант: 1 ^ k^ f f\e = p* (t — 1)Д b[l:k] = B(l:k)Ab(k+l :n) имеют окончательные значения */ DO WHILE (e~\ = 0); b(k) = b(k) + e\ k = k—\\ e = e—p\ END; END; END justify; Программа на Фортране Обратите внимание, что в примере на Фортране, приведенном ниже, все циклы с охраняемыми командами реализованы при помощи операторов IF, передающих управление на помеченный оператор CONTINUE. Эти операторы CONTINUE включены лишь для обе-
Гл. 22. Замечания о. документации 295 спечения того, чтобы каждый цикл был отдельным сегментом, независимым от предшествующих и следующих за ним операторов. С Имеется п слов, п ^0;, на строке номер z, которые начинают- С ся в столбцах 6(1), * . ., Ь(п). Каждые два смежных слова С разделены в точности одним пробелом. Величина s ^0 - об- С щее число пробелов, которые нужно вставить для того, чтобы С выровнять строку. Определить новые номера столбцов в (1 :п ), С представляющие выровненную строку. В результирующем С утверждении R, приведенном ниже, говорится, что число про- С белов, вставляемых между разными словами, отличается не С больше, чем на 1. Добавочные пробелы вставляются, в зави- С симости от номера строки, слева или справа. Выровненная С строка при п > 1 должна иметь следующий формат, где Wi - С i- e слово: С С W1 [(р+\) пробелов]...[>+1] W/ [q+l] ... [q + l] Wn С где р, q, t удовлетворяют условию С С QU Кг ^п АО^р Л0<<7 *p*(t-l) + q*(n-t)=s л С (odd(z)*q=p+l V even(z)Ap=q + \) С С Результирующее утверждение R (В ~- начальное значение Ъ): С С R:(0<n<\ ЛЪ=В) v ((Ah K/<r: b(i) = B(i)+p*(i-\)) ь С (Л U t<i<n: b(i) = B(i)+p*(t-l) + q*(f-t))) С SUBROUTINE justify (л, z, s, b) INTEGER w, z,s,b(ri) С INTEGER q,p,t9e,k IF (n .LE. 1) GOTO 100/ С Определить р, q и /: e= zfl IF(z .NE. 2*^) GOTO 20 ?= J/(/i-l) t= l+s —q*(n—l) ' p=q+l GOTO 30 20 P=s/(n-l) t= n ~s +p*(n~l) q=p+l 30 CONTINUE
296 Часть III. Построение программ С Вычислить новые номера столбцов Ь[1:п]: к=п е~ s С Цикл из охраняемых команд С инвариант: t <£<я Ае ~p*(t—l)+q*(k—t) л С Ь(\\к) — В(1\к) л Ь(к+1.п) имеет окончательные С значения 40 IF (it ,EQ. О GOTO 50 к=к-1 e~e—q GOTO 40 50 CONTINUE С Цикл из охраняемых команд С инвариант: 1 ^к </ а е =/?*(^—I) a С Ь(\:к)-В(\:к) л &(&+1:л)имеет окончательные С значения 60 IF (e .EQ.0)GOTO7Q Ъ(к)^Ъ(к)+е к=к-1 е= е—р GOTO 60 70 CONTINUE J00 CONTINUE END ;
Глава 23 ИСТОРИЧЕСКИЕ ЗАМЕЧАНИЯ В этой главе содержатся краткая история исследований в области программирования и краткий обзор задач программирования, представленнпх в нашей книге. История отражает мою точку зрения, описаны лишь те события, которые повлияли на мои исследования методов программирования. Например, охвачена лишь та часть методологии программирования, которая относится скорее к последовательным, чем к параллельным программам. Более того, громадное количество исследований по теории правильности программ не упоминаются вовсе просто потому, что они не повлияли на мои собственные идеи, а также на мнения о методах программирования, изложенных в книге. 23.1. Краткая история методологии программирования Период до 1960 г. Моими первыми языками программирования были Фортран и FAP — язык ассемблера для машины IBM-7090, и мне они очень нравились. Я мог на них кодировать свои программы наилучшим образом, и мои блок-схемы были всегда ясными и изящными. Впервые мне пришлось столкнуться с Алголом-60 [39] в 1962 г., когда я принимал участие в проекте, целью которого было написание транслятора ALCOR-ILLINOIS 7090. Как и многих других, первая встреча с этим языком привела меня в замешательство. Опгсание синтаксиса при помощи бэкусовой нормальной формы (БНФ, см. приложение 1) казалось чуждым и трудным. Динамические массивы, которые размещались в памяти при входе в блок и убирались при выходе из него, выглядели чем-то расточительным. Использование «:=» в качестве символа присваивания казалось излишним. Необходимость описывать все переменные выглядела нелепой идеей. Раздражали меня и многие другие вещи. Теперь я рад, что принимал участие в проекте, поскольку, по* знакомившись с Алголом-60, я начал видеть и его привлекательные стороны. БНФ оказалась полезным орудием. Я начал ощущать вкус и хороший стиль Алгола-60 и самого Сообщения об этом языке. И теперь я согласен со словами Т. Хоара: «Алгол-60, несомненно, явился большим шагом вперед, и р этом его существенное преиму*
298 Часть III. Построение программ ществонад большинством последующих языков». В качестве языка программирования Алгол-60 уже перестал быть полезен, поскольку он во многих отношениях несовершенен (точно так же, как и Фортран). Но необходимость простоты, хорошего вкуса, точности и математического единства как самого языка, так и его описания, выявленная в процессе создания Алгола-60, до сих пор оказывает глубокое влияние на программирование. Шестидесятые годы Шестидесятые годы были десятилетием синтаксиса и трансляции. Это подтверждается огромным количеством статей по бесконтекстным языкам, синтаксическому разбору, трансляторам, компиляторам компиляторов и т. д. Лингвисты также увлеклись синтаксическим разбором, и люди получали ученую степень за написание трансляторов. Алгол-60, возможно, из-за сильного влияния рабочей группы (РГ) 2.1 ИФИП (по Алголу), которая собиралась большей частью в Европе 1—2 раза в год, был в центре многих исследований. (ИФИП — это Международная федерация по обработке информации.) Кроме всего прочего, с 60-х годов РГ 2.1 выпускает «Алгол- бюллетень» — неформальное, довольно широко распространенное издание. Этот бюллетень держит исследователей в курсе работ, проводившихся в области Алгола и алголоподобных языков. В то время мало кто был глубоко заинтересован в понимании сути самого программирования (хотя можно найти несколько ранних работ по этому вопросу), и по крайней мере в начале 60-х годов казалось, что все удовлетворены программированием в том виде, в котором оно было. Если и делались попытки построения формальных определений языков программирования, они предпринимались в основном для того, чтобы понять сами языки и трансляторы, а не для того, чтобы понять сущность программирования. В этих построениях играли большую роль понятия теории автоматов и формальных языков, в чем можно убедиться, ознакомившись г трудами одной важной конференции [42], состоявшейся под эгидой ИФИП. Несколько отдельных статей и дискуссий являлись первыми признаками того, как много еще предстояло сделать в области программирования. Одно из первых упоминаний идеи доказательства правильности программ содержится в вдохновившей многих исследователей статье Дж. Маккарги [35], опубликованной в 1961 г. и представленной в 1962 г. на конгрессе ИФИП. (В то время Дж. Маккарти работал в Массачусетском технологическом институте, сейчас—Станфордском университете.) Маккарти заявил, что «вместо того, чтобы испытывать программы на тестовых примерах до тех пор, пока они не окажутся отлаженными, нужно доказывать, что они имеют желаемые свойства». На том же самом конгрессе
Гл. 23. Исторические замечания 299 Э. В. Дейкстра (Эйндховенский технологический университет, Нидерланды; стал позднее также сотрудником фирмы «Бэрроуз») сделал доклад «Некоторые соображения о более развитом программировании» [11]. На конгрессе ИФИП 1965 г. С. Гилл (Англия) отметил, что «другая практическая проблема, которая начинает сейчас принимать угрожающие размеры и для которой трудно будет найти удовлетворительное решение,— это проверка правильности больших программ». Но в основном проблемой правильности занимались исследователи более теоретического склада и лишь в терминах формального доказательства эквивалентности двух различных программ, что нельзя назвать полезным с точки зрения практики. К концу 60-х годов постепенно стало ясно, что в области математического обеспечения в действительности имеются огромные проблемы. Размеры и сложность проектов в 60-х годах непомерно разрастались, что было несоразмерно со средствами программирования и возможностями программистов. Это приводило к ненадежному математическому обеспечению, ведущему к нарушению плановых сроков разработок и возрастанию стоимости проектов. В 1968 г. для обсуждения критической ситуации в Гармише (ФРГ) состоялась конференция по математическому обеспечению [6]. Я получил приглашение принять в ней участие и помочь в ее организации от одного из главных организаторов конференции Ф. Л. Бауэра, под руководством которого в 1966 г. я защитил докторскую диссертацию. Мне предоставилась возможность услышать дискуссии ведущих специалистов из научных кругов и из промышленности, обсуждавших друг с другом проблемы программирования с двух довольно различных точек зрения. Они открыто говорили не только о своих успехах, но и о своих ошибках в области математического обеспечения, чтобы добраться до сути возникших проблем. Впервые стороны пришли к единому мнению о том, что действительно имеется кризис математического обеспечения и что сущность программирования слабо понята Сознавая серьезность сложившейся ситуации, в 1969 г. ИФИП утвердила создание Рабочей группы 2.3 (по вопросам методологии программирования) под председательством М. Вуджера (Национальная физическая лаборатория, Англия). Некоторые из членов этой группы, включая Э. В. Дейкстру, Б. Рэнделла (Университет Ньюкасла на Тайне), Д. Росса (компания «Софтек»), Г. Зигмюллера (Технический университет, Мюнхен), В. М. Турского (Варшавский университет) и Н. Вирта (Высшая техническая школа, Цюрих), вышли ранее из состава Рабочей группы 2.1, когда ею был принят Алгол-68 в качестве «следующего языка семейства Алгол». Осознавая глубину проблем, возникших в программировании, они пришли к выводу, что Алгол-68 был шагом в неверном направлении и что необходимо создать меньший, более простой язык программирования и его описание.
300 Часть III. Построение программ Таким образом, примерно в 1969 г. программирование стало признанной респектабельной — и действительно очень важной — областью научных исследований. Появление в 1968 г. статьи Дей- кстры о вреде go to [12] растревожила осиное гнездо. А его книга «Структурное программирование» [14] (в которой термин «структурное программирование» ни разу не встретился в тексте, но был введен в заглавие) вместе со статьей Вирта [44] о пошаговом уточнении программ определили исследования на много лет вперед. Ранние работы о правильности программ Хотя вопрос доказательства правильности программ упоминал- ся много раз, немногие работали в этой области вплоть до конца 60-х годов. Однако три важные статьи, написанные в 60-х годах, оказали глубокое влияние на эту область. Первая статья — о доказательстве правильности программ — принадлежала П. Науру (Копенгагенский университет) и была написана в 1966 г. [40]. В этой статье Наур подчеркнул важность доказательств правильности программ и дал неформальные способы спецификации программ. Источником, определившим дальнейшую работу в этой области, явился доклад Р. Флойда на конференции Американского математического общества в 1967 г. [19]. В докладе Флойда рассматривалось приписывание оператором блок-схемы утверждений, которые должны были быть истинны во время выполнения соответствующей программы в любой момент, когда управление передавалось соответствующему оператору. Для цикла Флойд помещал утверждение Р в произвольном (но, конечно, фиксированном) операторе цикла, называвшемся точкой сечения. При этом он отметил, что если выполнение цикла началось в точке сечения с истинным Р и опять достигло этой точки, то Р должно быть истинным в этой точке. Таким образом родилась идея инварианта цикла. Флойд высказал также предположение, что спецификация методов доказательств может обеспечить адекватное определение языка программирования. Тони Хоар, работавший тогда в Белфастском университете (теперь он в Оксфорде), выбрал предположение Флойда в качестве основной идеи своей статьи [27] и определил простой язык программирования через логическую систему аксиом и правил вывода для доказательства частичной правильности программ, являющуюся расширением исчисления предикатов. Например: оператор присваивания определялся схемой аксиом Р?{х:=е}Р а цикл типа while—правилом вывода PaB{S}P Р {while В doS}PA~]B
Гл. 23. Исторические вамечания 30! Это правило вывода означает, что если доказано P/\B{S\ P, то можно заключить, что выполняется также и Р\while В do S\P/\ ^В. В статье Хоара делается попытка решить саму задачу программирования. Язык программирования ограничивается (вместо блок- схем) конструкциями, «поддающимися управлению». Автор стремится продемонстрировать необходимость таких ограничений. В работе показывается, что определение языка не в терминах исполнения программы, а в терминах доказательства ее правильности упрощает процесс построения программы. Сам стиль статьи вместе со всесторонней оценкой возможных преимуществ принятия аксиоматического подхода к определению языка как для программирования, так и для самого определения формального языка сделали эту статью уникальной. Должен сознаться, что в 1969 г. работа Хоара не произвела на меня особого впечатления. Я не угадал ее последствий, возможно, потому что не был готов глубоко задумываться над проблемами программирования, так как в то время был слишком занят построением транслятора. Такая занятость какой-то конкретной работой — одна из причин, по которой преподавание программирования так далеко отстало от исследований в данной области. Специалисты в большинстве своем слишком заняты выполнением своих собственных исследований, чтобы тратить время на размышления о программировании и изучение его. Жаль, поскольку хорошее преподавание программирования — важная часть тех задач, которые стоят перед учеными и преподавателями в области информатики. Двумя годами позже я стал больше интересоваться работой Хоара и начал сознавать ее последствия. На самом деле она произвела на меня столь большое впечатление, что в 1972 г. я посвятил часть своего второго курса программирования в Корнеллском университете инвариантам циклов и включил эту тему в учебник [9]. Исследования в области аксиоматических определений языков в 70-х годах На базе работы Хоара [27] создалась целая научная школа, занимающаяся аксиоматическими определениями языков программирования. Сегодня имеются буквально сотни работ по аксиоматизации различных конструкций — от операторов присваивания до различных форм циклов и от вызовов процедур до сопрограмм. Был аксиоматизирован даже оператор go to и на самом деле очень просто. Эти исследования были чреваты опасностью потери понимания и разочарованиями. Одна из причин этого состоит в том, что исследователи, работавшие в области информатики, в целом недостаточно были знакомы с формальной логикой. Некоторые из работ появились лишь потому, что их авторы не поняли более ранних работ; в других содержались ошибки, вызванные незнанием логики.
302 Часть 111. Построение программ Трудно было построить хорошую аксиоматическую систему, пока единственной работой, где такая система упоминалась, оставалась статья Хоара 1969 г. [27]. Но тем не менее и я, и другие работали именно в таких условиях. Из-за нашего невежества мы потратили очень много времени на то, чтобы бить по воде вместо того, чтобы плавать. Задним числом я могу сказать, что лучше всего для меня было бы пройти хороший курс логики 10 лет назад. Я убедил в этом многих студентов, но сам такого курса никогда не слушал. Перечислим некоторые достижения 70-х годов в области аксиоматизации языков программирования. Некоторые из этих результатов были независимо получены и опубликованы также другими авторами; перечисляются лишь статьи, которые повлияли на меня. Работы обычно делались на год или два раньше публикации. В 1971 г. были построены правила вывода для ограниченных вызовов процедур [28]. Несколько последующих работ, основывавшихся на результатах [28], содержали ошибки, которых не имелось в самой работе [28]. В 1972 г. в статье [29] о доказательстве правильности представления данных было сделано многое для того, чтобы ускорить исследования по «абстрактным типам данных». Алгебраические спецификации типов данных [35] появились позже; они основываются на работе [33], опубликованной в 1974 г. В 1973 г. были написаны правила доказательства для большинства конструкций языка Паскаль [39]. В этой работе содержалось первое правило вывода для присваиваний элементам массива, в котором массив рассматривается как функция (как в гл. 9). На самом деле этой работе во многом мешал сам язык: легче построить язык, имея в виду аксиоматизацию, чем аксиоматизировать уже созданный язык. В 1975 г. была построена автоматическая система верификации для (подмножества) Паскаля, основанная на аксиомах и правилах вывода [32]. В 1975 г. была использована модель исполнения программы, для доказательства «относительной полноты» множества аксиом и правил вывода для фрагмента Алгола [81. Таким образом, было показано, что если программу нельзя доказать внутри аксиоматической системы, то это происходит не из-за аксиоматического определения языка, а из-за других вещей, например из-за того, что любая аксиоматизация арифметики неполна. В 1979 г. был определен язык программирования Евклид, в проект которого с самого начала была заложена идея аксиоматизации [34]. В 1980 г. был определен и использован для описания аксиом для вызовов процедур общий оператор кратного присваивания [23]. В этой статье были разъяснены некоторые вопросы, касающиеся начальных и окончательных значений переменных.
Гл. 23. Исторические замечания 303 Исследования по построению программы вместе с доказательством В начале 70-х годов часто можно было услышать: «Недостаток методики, которую предлагает Хоар, в том, что она заставляет искать инвариант каждого цикла!» Многие, включая меня, отвечали на это: «Преимущество методики, которую предлагает Хоар, в том, что она заставляет искать инвариант каждого цикла!» В некотором отношении первое мнение отвечало положению дел, существовавшему в то время. Когда теория была представлена впервые, доказывать правильность существующей программы казалось необычайно трудно, и вскоре пришли к выводу, что единственный способ доказать правильность программы — строить ее вместе с доказательством, и при этом доказательство должно опережать построение программы. Тогда мы действительно не знали, как это делать. Например, было известно, что инвариант цикла должен предшествовать построению самого цикла, но хороших методов его нахождения мы не знали и, конечно же, не могли научить других искать его. Спорящие некоторое время продолжали обмениваться аргументами «за» и «против», но сторонники инвариантов постепенно укрепляли свои позиции. Предмет спора затуманивался неопределенностью понятия доказательства. Некоторые считали, что единственный способ формально доказать правильность программы — воспользоваться программой верификации или поиска доказательств теорем. Другие утверждали, что доказательства, найденные машиной, были и будут бесполезными из-за множества возникающих деталей. Третьи доказывали, что никто не сможет читать такие доказательства. В статье [10], которую стоит прочесть, подытожены аргументы против доказательства правильности программ. В данной книге используется промежуточная точка зрения: нужно строить программу вместе с доказательством, но доказательство должно быть частично формальным, частично основанным на здравом смысле. В 70-х годах существовало много возможностей для обсуждения технических работ в области программирования. Две из них, кроме обычных конференций или рабочих встреч, заслуживают отдельного упоминания. Во-первых, это рабочая группа ИФИП 2.3 (по методологии программирования). В ней позже и в РГ 2.1, 2.2 и 2.4 довольно интенсивно ставились и обсуждались проблемы, связанные с программированием. С момента своего создания РГ 2.3 собиралась дважды или трижды в год на пять дней. Группа не выпускала даже формальных сборников работ, а результаты встреч публиковались ее членами в обычном порядке. Группа выпустила сборник ранее опубликованных статей ее членов [22], который хорошо иллюстрирует влияние, оказывавшееся РГ 2.3 на программирование в течение 70-х годов. Этот сборник рекомендуется тем, кто интересуется методикой программирования.
304 Часть 111. Построение программ Во-вторых, заслуживают упоминания двухнедельные курсы, которые в 70-х годах организовывались Мюнхенским техническим университетом. На этих курсах преподавали ведущие специалисты по программированию, а обучались способные студенты старших курсов, молодые ученые, специалисты, пришедшие в программирование из других областей. Эти курсы были организованы не столько для того, чтобы обучить чему-то конкретному, сколько в качестве очень хорошо организованного форума для обсуждения проводившихся в данный момент исследований. Многие из курсов, посвященные собственно программированию (трансляции, операционным системам и т. п.), финансировались научным комитетом НАТО. Эти школы были необычны тем, что от 50 до 100 исследователей проводили две недели вместе для обсуждения только одной темы. Лекции, прочитанные на многих таких школах, были опубликованы: см., например, [2—4]. Вернемся к построению программ. В 1975 г. Дейкстра опубликовал статью [15], явившуюся предшественником его книги [16]. В этой книге введены для простого языка программирования слабейшие предусловия и на многих примерах показано, как их можно использовать в качестве «исчисления для вывода программ». Впервые стало теперь понятно, как можно строить инвариант цикла до построения самого цикла. Стало ясно, что акцент на теорию и формализм, смягченный здравым смыслом, на самом деле может привести к построению программ более надежным способом. Начали проясняться принципы и понятия, на которых может базироваться наука программирования. Книга, которую вы читаете, является моей попыткой изложить эти понятия и принципы, и показать, как практически можно заниматься программированием как наукой. 23.2. Задачи, использованные в книге В нижеследующем перечне приводится история возникновения задач (разумеется, в меру моей осведомленности о ней), расположенных в порядке их появления в тексте. Задача о банке с кофейными зернами (гл. 13). Эта задача упоминалась Э. В. Дейкстрой в письме 1979 г. Он узнал задачу от своего коллеги К. Схолтена. Задача была решена за пять минут. Замыкание кривой (гл. 13). Ее предложил мне Дж. Уильяме в 1973 г. (тогда он был сотрудником Корнеллского университета, теперь — фирмы IBM, г. Сан-Хосе). Я не смог ее решить, и Уильяме показал мне ответ. Задача о максимуме (гл. 14). См. [16, с. 52—53]. Следующая большая перестановка (упр. 2 к гл. 14 и гл. 20). Задача известна уже давно. Построения взяты из [16, с. 107—110]. Цииса в двумерном майте (разд. 15.1 и 15.2). Решение мое.
Гл. 23. Исторические замечания 305 Сортировка четверки (разд. 15.2). См [16, с. 61]. Наибольший общий делитель (упр. 2 к разд. 15.2). Конечно, эта задача восходит еще в Евклиду. Ее версии, представленные в книге, взяты в основном из [16]. Приближение к квадратному корню (разд. 16.2, 16.3 и 19.3). См. [16, с. 61—65]. Линейный поиск и Принцип линейного поиска (разд. 16.2). Построение взято из [16, с. 105—106]. Задача о площадках (разд. 16.3). Я пользовался этой задачей для иллюстрации инвариантов цикла в 1974 г. на конференции в Мюнхене, ФРГ. Из-за отсутствия опыта в моей программе использовалось слишком много переменных (см. разбор решения в конце разд. 16.3) М Гриффите (Университет Нанси) написал рекурсивное определение площадки в массиве и преобразовал его в циклическую программу. Получилась программа, подобная (16.3.11). Идеализированное построение, приведенное в разд. 16.3, появилось позже. Двоичный поиск (упр. 4 к разд. 16.3) Построение, приведенное в ответе к упражнению, принадлежит Дейкстре (1978 г.) Жулик на пособии (разд. 16.4) принадлежит коллеге Дейкстры В. Фейену. Эта задача использовалась в качестве упражнения на Международной летней школе в Марктобердорфе, ФРГ, в 1976 г. [2]. Ответственность за ее нынешнее оформление лежит на мне. Перестановка сегментов равной длины (разд. 16.5). Фольклор. Обращение массива (упр. 2 к разд. 16.5). Фольклор. Разбиение (упр. 4 к разд. 16.5). Использовалась в алгоритме упорядочения массива, построенном Т. Хоаром (Оксфорд) в 1962 г. [26]. Решение мое. Голландский национальный флаг (упр. 5 к разд. 16.5). См. [16, с. 111—116]. Обращение связей (упр. 6 к разд. 16.5). Уже в течение многих лет это — излюбленное упражнение и контрольный вопрос в специальном курсе программирования для студентов Корнеллского университета. Поиск седловой точки (упр. 8 к разд. 16.5). Один аспирант из Университета в Беркли поставил мне эту задачу во время дискуссии по программированию весной 1980 г. Г. Левин (тогда аспирант Корнеллского университета, теперь сотрудник Аризонского университета) решил ее в точности гак, как дано я ответе к упражнению. Десятичное в двоичное (упр. 9 к разд. 16.5). Фольклор. Десятичное в основание В (упр. 10 к разд. 16.5). Простое обобщение предыдущей задачи. ^Перестановка сегментов (разд. 18.1). Задача была поставлена мне и X. Миллсу (фирма IBM, Мэриленд) на конгрессе ИФИП 1980 г. в Японии Э. Нельсоном, который не мог ее решить. К одному из двух первых ее решений мы пришли еще во время путешествия [24]. История третьего решения — через обращение сегментов массива (см. ответ к упр. 1 к разд. 18.1) — канула в Лету. Оно было показа-
306 Часть 111. Построение программ но мне А. Демерсом (Корнеллский университет) и используется в текстовом редакторе UNIX и в дисплейном Terak, при помощи которых я напечатал и отредактировал большую часть этой книги. Быстрая сортировка (разд. 18.2) принадлежит Хоару [26]. Подсчет числа вершин дерева (разд. 18.3). В 1972 г. этой задачей пользовались для того, чтобы показать, как трудно искать инварианты циклов (см. [5])! А сейчас она выглядит почти тривиальной. Обход по префиксному, инфиксному и суффиксному порядку (разд. 18.3). Термины придуманы Д. Кнутом (Станфордский университет), построения программ — мои. Упражнение, приписываемое Хэммингу (разд. 19.2). Эту программу вывел Дейкстра [16, с. 129—134]; он приписывает ее Р Хэммингу (лаборатории фирмы «Белл»). Нахождение сумм квадратов (упр. 1 к разд. 19.2). См. [16, с. 140-142]. Возведение в степень (разд. 19.1 и упр. 15 к гл. 20). Построение, проделанное в разд. 19.1, взято из [16, с. 65—67]. Программа упр. 11, обрабатывающая в другом порядке двоичное представление, была показана мне Дж Уильямсом. Однажды я слышал подряд два выступления специалистов, обсуждавших возведение в степень. Каждый из них считал, что эта единственная программа возведения в степень, не подозревая, что существует и другая. Сортировка с регулируемой плотностью массива (разд. 19.3). Р. Мелвилл вывел этот алгоритм в своей диссертации [36]; алгоритм был опубликован в [37]. Эффективная реализация очередей на языке Лисп (разд. 19.3). Р. Мелвилл вывел этот алгоритм в своей диссертации [36]. Выровненные строки текста (разд. 20.1). Вывод этой программы был опубликован в [21]. Максимальная восходящая последовательность (разд. 20.2). Эту задачу Дейкстра дал в качестве упражнения за день до того, как он ее вывел на курсах 1978 г. по построению программ в Марктобер- дорфе [4]. Из числа присутствующих лишь четыре или пять человек, имевшие опыт применения рассмотренного здесь метода программирования, не испытывали никаких затруднений при ее решении. Аналогичное решение дал Дж. Мисра (Техасский университет, г. Остин) несколько раньше в своей статье по построению программ [38]; обобщение этого решения использовано в программе DIFF [31] из редактора UNIX. Бесповторные 5-битовые подпоследовательности (упр. 1 к гл. 20). Из [131. Кодирование перестановки (упр. 5 к гл. 20). Эта задача была решена в связи с обращением программ (см. гл. 21) Дейкстрой и его коллегой В. Фейеном [17]. Понятие обращения программ и большинство обращений, приведенных в гл. 21, также принадлежат им. Декодирование перестановки (упр. 6 гл. 20). См. предыдущую задачу.
Приложение 1 ФОРМА БЭКУСА —НАУРА БНФ — нормальная форма Бэкуса, или форма Бэкуса — На- ура,— является языком для описания (части) синтаксиса выражений других языков. Она была предложена примерно в 1959 г. Дж Бэ- кусом, одним из тринадцати членов комитета по Алголу-60, для описания синтаксиса этого языка. Сотрудник фирмы IBM Дж. Бэ- кус является также одним из основных создателей языка Фортран. С этой формой также связывается имя П. Наура (Копенгагенский университет) благодаря предложенным им изменениям и интенсивному использованию БНФ в сообщении об Алголе-60, редактором которого он был. К идее такой формы независимо пришел ранее (в 1956 г.) Н. Хомский, лингвист по специальности. БНФ и ее модификации стали стандартным средством описания синтаксиса языков программирования, и во многих случаях по описанию синтаксиса при помощи БНФ автоматически порождаются некоторые части трансляторов. Для того чтобы ввести БНФ, воспользуемся описанием цифр, констант целого типа и упрощенных арифметических выражений при помощи БНФ. То, что 1 является цифрой, выражается при помощи БНФ следующим образом: (А1.1) (цифра).ч = 1 Чтоб1ы отметить, что понятие (цифра) не может встречаться в предложениях описываемого языка, а используется лишь для того, чтобы помочь описать эти предложения, это понятие заключено в угловые скобки. Такое понятие является «синтаксическим», подобно понятиям «глагол» или «группа существительного» при описании естественного языка. Обычно такое понятие называется нетерминалом или нетерминальным символом. Напротив, символ 1 может встречаться в предложениях описываемого языка и называется терминалом или терминальным символом. (А1.1) называется продукцией или правилом (замены) Левая (слева от ■:=) часть правила является нетерминальным символом, правая (справа от ::=) часть — непустой, конечной цепочкой нетерминальных и терминальных символов. Символ •: = читается «может состоять из», так что (А1.1) можно прочитать следующим образом: Понятие (цифра) может состоять из символа 1. Для того чтобы показать, что (цифра) может быть 0 или 1, можно
308 Приложение 1 воспользоваться двумя правилами. Щ (цифра )::=0 (понятие (ци ра) может состоять из 0) щ (цифра):: = 1 (понятие (цифра) может состоять из 1) Щ Сократить эти два правила, выражающие различные формы одного щ и того же нетерминального понятия, можно при помощи символа I щ (читается как «или»): ■ (цифра):: =011 ((цифра) может состоять из 0 или из 1) щ Таким же сокращением можно воспользоваться и для того, чтобы ж специфицировать все цифры: ж (цифра)::=0 |1|2|3|4|5|6|7|8|9 1 Константа целого типа — это (конечная) последовательность ж не менее чем из одной цифрь. Константы целого типа могут быть щ следующим образом рекурсивно определены (нетерминальный сим- щ вол (константа) представляет класс всех таких констант): Ж (А1.2) <константа> ::= <цифра> 1 <константа> :: = <константа> <цифра> ж <цифра> ::=0|1|2|3|4|5|6|7|8|9 1 Первое из правил (А1.2) читается следующим образом: (константа) Я может состоять из (цифры). Второе правило читается: (константа) щ может состоять из другой (константы), за которой следует (цифра). ж Правила, перечисленные в (А1.2), образуют грамматику для щ языка (констант). Предложениями этого языка являются те после- ■ довательности терминальных символов, которые могут быть вы- щ ведены из нетерминального символа (константа). Вывод происходит щ следующим образом. Начинаем с последовательности, состоящей щ лишь из нетерминального символа (константа). Последовательно щ заменяем один из нетерминальных символов текущей последователь- ж ности на соответствующую правую часть правила до тех пор, пока щ не получится последовательность, состоящая лишь из терминаль- щ ных символов. Щ Предложение 325 выводится следующим образом (=ф* обозначает щ замену): щ (А1.3) <константа>=> (используется второе правило) Я <константа> <цифра> Щ =><константа> 5 (<цифра> заменяется на 5) щ =>< константах цифра) 5 (второе правило) щ => <константа> 2 5 «цифра) заменяется на 2) ж =><цифра> 2 5 (первое правило) I =>3 2 5 (<цифра> заменяется на 3) ■ Вывод одной последовательности из другой может быть определен I следующей схемой. Предположим, что U::=u есть правило нашей 1 грамматики, где U — нетерминальный символ, а и — последова- ж
Форма Бэкуса — Наура 309 тельность символов. Тогда определим для любых (возможно, пустых) последовательностей х и у: xUy=^xuy Символом => обозначается одна замена—непосредственный вывод. Последовательность из неотрицательного числа непосредственных выводов обозначается символом =>*. Таким образом, <константа> 1=># <константа> 1 поскольку <константа> 1 выводится из самой себя за нуль шагов вывода. Кроме того, <константа> 1 =>* <константа><цифра> 1 по второму правилу грамматики (А 1.2). И наконец, <константа> 1 => * 3 2 5 1 поскольку в (А 1.3) показано, что <константа>=>* 3 2 5. Грамматика (упрощенных) арифметических выражений Рассмотрим запись грамматики для арифметических выражений, использующих операции сложения, вычитания, умножения, скобки, целые константы в качестве операндов. Это довольно легко сделать: <выражение> ::= <выражение> + <выражение> <выражение> ::= <выражение> — <выражение> <выражение> ::= <выражение> * <выражение> < выражение) ::= (<выражение>) <выражение> ::= <константа> где <константа> описана так же,, как и в (А1.2). Приведем вывод в данной грамматике выражения (1+3)*4: (А 1.4) < выражение) => <выражение> * <выражение> =Ф «выражение» * <выражение> => «выражение) + выражение» * <выражение> =>«константа>+<выражение»*<выражение> => «константа>+<константа» * <выражение> => «константа) + <константа>) * <константа) => (<цифра> + <константа>) * <константа> => «цифра) + <цифра» * <константа> => «цифра) + <цифра>) * <цифра> =>(1 + <цифра))*<цифра> =>(1 +3)*<цифра> =Ф(1+3)*4 Следовательно, <выражение> => * (1 + 3) * 4.
310 Приложение 1 Дерево разбора и неоднозначность Последовательность выводов можно описать при помощи дерева разбора. Например, для вывода (А1.4) деревом разбора является < выражение > <выражение> * <выражение> ( <выражение> ) <константа> <выражение> + <выражение> <цифра> I I I <константа> <константа> 4 . I <цифра> I 3 <цифра> I 1 Шаг вывода U ::= и представляется в дереве разбора вершиной U, из которой выходят линии, указывающие на символы последовательности и. Таким образом, в дереве разбора для каждого шага из последовательности выводов имеется нетерминальный символ, а символы, на которые он заменяется, расположены непосредственно под ним. Например, первый шаг вывода — это (выражение)^ (выражение)*(выражение), так что на верху приведенного выше дерева стоит вершина (выражение), из которой выхоДят три линии, указывающие на (выражение), * и (выражение). Кроме того, имеется шаг вывода по правилу (цифра) :: — 1 , так что есть и соответствующая ветвь дерева, ведущая из вершины (цифра) в вершину 1. Главное различие между выводом и деревом разбора — то, что дерево разбора не фиксирует порядка, в котором были сделаны некоторые шаги вывода. Например, нельзя определить по приведенному выше дереву, какое из двух правил (цифра) ::= 1 или (цифра) ::= 3 было применено раньше. Каждому выводу соответствует дерево разбора, по одному и тому же дереву может соответствовать несколько выводов. Такие выводы считаются эквивалентными. Рассмотрим теперь множество выводов, выраженное соотношением (выражение)^ * (выражение) + (выражение) * (выражение) На самом деле имеются два различных дерева разбора (выражение) + (выражение) * (выражение)}
Форма Бэкуса — Наура 311 х выражение> <выражение> <аыражение> + <выражение> <выражение> * <выражение> < выражение > * < выражение> <выражение> + <выражение> Грамматика, в которой имеется более чем одно дерево разбора для некоторого предложения, называется неоднозначной. Она названа так потому, что наличие двух деревьев разбора позволяет проанализировать одно и то же предложение двумя различными способами, и, следовательно, у него может оказаться два разных смысла. В данном случае наличие неоднозначности указывает на то, что в грамматике не отмечается, что должно выполняться раньше: + или #. Левое из приведенных выше деревьев указывает на то, что первым должно выполняться *, поскольку то (выражение), из которого выведена *, является в некотором смысле операндом операции сложения + . С другой стороны, правое синтаксическое дерево указывает на то, что первым должен выполняться + . Можно написать однозначную грамматику, в которой говорится, что умножение имеет приоритет над сложением (кроме того случая, когда для преодоления приоритета использованы скобки). Для того чтобы сделать это, требуется ввести новые нетерминальные символы (слагаемое) и (множитель). <выражение> ::= <слагаемое> | <выражение> + <слагаемое> | <выражение> — <слагаемое> <слагаемое> ::= <множитель> | <слагаемое>*<множитель> <множитель> ::= <константа> | «выражение)) <константа> ::== <цифра> <константа> ::= <константа> <цифра> <цифра> ::=0|1|2|3|4|5|6|7|8|9 В этой грамматике у каждого предложения имеется одно дерево разбора, так что неоднозначности не возникает. Например, у предложения 1+3*4 одно дерево разбора: V < выражение > 1 1 <слагаемое> 1 1 <множитель> 1 1 :константа> 1 1 <цифра> 1 1 1 < выражение > + <сл < слагаемое > 1 1 < множитель> 1 1 < константа> 1 1 < цифра> 1 1 3 , агаемое> " 1 "-"-—— 1 * <множитель> 1 1 <константа> 1 1 < цифра> 1 1 4
312 Приложение 1 В этом дереве разбора указывается, что первым должно быть произведено умножение, и в общем случае в данной грамматике * имеет приоритет над +, кроме тех случаев, когда для преодоления приоритета использованы скобки. Расширения БНФ Для того чтобы облегчить чтение и понимание БНФ, используются несколько ее расширений. Одно из наиболее важных — использование фигурных скобок для обозначения повторений: {х} означает нуль или больше вхождений подряд цепочки символов х. При помощи этого расширения можно описать (константу) одним правилом: (константа) ::= (цифра) {(цифра)} Грамматику арифметических выражений можно переписать следующим образом: <выражение> ::= <слагаемое> {+ <слагаемое> | — «(слагаемое)} <слагаемое> :: = <множитель> {* (множитель)} <множитель> ::= <константа> | «выражение» <константа> ::= <цифра> {<цифра>} <цифра> ::=0|1|2|3|4|5|6|7|8|9 Ссылки Синтаксис формальных языков интенсивно изучался теоретически. Отличным изданием по данной теме является книга Дж. Хоп- крофта и Дж. Ульмана «Introduction to Automata Theory, Languages and Computation» (Addison-Wesley, 1979). Практическое использование теории при построении компиляторов рассматривается в книге Д. Гриса «Конструирование компиляторов для цифровых вычислительных машин» (М.: Мир, 1975) и в книге А. Ахо и Дж. Ульмана «Principles of Compiler Design» (Addison-Wesley, 1977).
Приложение 2 МНОЖЕСТВА, ПОСЛЕДОВАТЕЛЬНОСТИ, ЦЕЛЫЕ И ДЕЙСТВИТЕЛЬНЫЕ ЧИСЛА В этом приложении кратко определяююя важнейшие гипы данных, использованные в книге. Множества будут описываться более подробно, чем другие типы, с тем чтобы читатель мог изучить важный материал, на который, возможно, он не обратил ранее достаточного внимания. Множества и операции над ними Множество — это совокупность различных объектов, обычно называемых его элементами. Поскольку слово совокупность столь же неопределенно, как и множество, дадим несколько примеров, чтобы конкретизировать эту идею: Множество {3, 5} состоит из целых чисел 3 и 5. Множество {5, 3} состоит из целых чисел 3 и 5. Множество {3, 3, 5} состоит из целых чисел 3 и 5. Множество {3} состоит из числа 3. Множество { } называется пустым множеством, оно не содержит элементов. Иногда оно обозначается значком 0. Эти примеры иллюстрируют один из способов описания множества: записать список его элементов в фигурных скобках { и }, разделяя элементы запятыми. В первых двух примерах показано, что порядок элементов в этом списке не имеет значения. В третьем примере показывается, что элемент, перечисленный в списке более одного раза, находится в множестве в единственном экземпляре: элементы множества должны быть различны. В последнем примере показано, что в множестве может содержаться нуль элементов, и в этом случае оно называется пустым множеством. Невозможно перечислить все элементы бесконечного (т. е. содержащего бесконечное число элементов) множества. В этом случае часто употребляют многоточие, чтобы показать, что читатель должен воспользоваться воображением и продолжить список элементов, которые выписаны явно. Например, {О, 1, 2, ...} — множество натуральных чисел; {..., —2,—1, 0, 1, 2, ...} — множество всех целых чисел; {1, 2, 4, 8, 16, 32, ...} —множество степеней двойки. Определение может быть и более явным, использующим другое
3U Приложение 2 обозначение {i | существует натуральное число /, такое, что 2j = i) При таком обозначении между { и | стоит идентификатор i, а между | и } — предикат, т. е. предложение, принимающее значения истина и ложь. Множество, им описанное, состоит из всех элементов iy удовлетворяющих предикату. В данном случае, это множество состоит из всех степеней двойки. Следующая формула описывает множество четных чисел: {k | even (k)} Это обозначение может быть несколько обобщено: Если предполагать, что i и / принимают целые значения, то данной формулой описывается множество пар {..., (-1, -2), (0,-1), (1, 0), (2, 1), (3, 2),...} Мощность, или размер множества,— это число его элементов. Для обозначения мощности множества а часто используются обозначения \а\ и card(a). Таким образом, |{ }|=0, |{1, 5}|=2, card({3, 3, 3})=1. Для построения новых множеств используются следующие три операции: объединение U, пересечение П и разность —: а[}Ь—множество, состоящее из элементов, лежащих по крайней мере в одном из множеств а или Ь\ а(]Ь—множество, состоящее из элементов, принадлежащих как а, так и Ь\ а—Ь—множество, состоящее из элементов а, не являющихся элементами Ь. Например, если а={Л, В, С}, Ь = {В, С, D\, то а[) Ь = \А, В, С, D}, аПЬ = {В, С\ и а—Ь = {А\. Следующие три операции над множествами, помимо проверок на равенство и неравенство (т. е. а = Ь и а Ф {2, 3, 5}), дакг булево значение Т или F (истина или ложь). х£а—"л: является элементом множества а"; х (£ а—эквивалентно ~| (х € а)\ aczb—"множество а является подмножеством множества Ь", т. е. каждый элемент а принадлежит Ь. Таким образом, 1 g{2, 3, 5, 7} ложно, 1 g{2, 3, 5, 7} истинно, 1, 3, 5}с{1, 3, 5} истинно и {1, 3, 5[с{1, 3, 5, 0} истинно, а 3, 5, 1, 2}с{1, 3, 5, 0} ложно. Иногда полезно говорить о минимальном или максимальном значении из множества (если на его элементах определено упорядо-
Множества, последовательности, числа 315 чение). Минимальное значение из множества а обозначается min(a), а максимальное— max (a). И наконец, опишем команду, которая полезна при программировании с множествами. Пусть а — непустое множество и х — переменная, которая может содержать значение того типа, который имеют элементы множества. При выполнении команды Выбрать (а, х) в х записывается один из элементов множества а. Множество а остается неизменным. Эта команда недетерминирована (см. гл. 7), поскольку до ее выполнения неизвестно, какой именно из элементов а будет записан в х. Предполагается, что эту команду можно использовать лишь для конечных множеств. Ее использование для бесконечных множеств порождает проблемы, рассмотрение которых выходит за рамки данной книги (см. [16] по поводу ограниченного и неограниченного недетерминизма). Более того, множества, с которыми мы работаем в наших программах, конечны. Через слабейшие предусловия команда Выбрать (а, х) определяется следующим образом (см. гл. 7): хюр(Выбрать(а, х)у Я) = аФ{ }/\(yi*i£a:Rf) Последовательности Последовательность — это список элементов (разделенных запятыми), ограниченный круглыми скобками. Например, последовательность (1, 3, 5, 3) состоит из элементов 1, 3, 5, 3 в данном порядке, ( ) обозначает пустую последовательность. В противоположность множествам в последовательности порядок элементов существен. Длина последовательности s (записывается \s\) — число элементов в ней. Соединение последовательностей с последовательностями и (или) значениями обозначается через |. Таким образом, (1, 3, 5)1(2,8)=(1, 3, 5, 2, 8) (1, 3, 5)|8|2=(1, 3, 5, 8, 2) (1, 3, 5)|( ) = (1, 3, 5) В программировании используются следующие обозначения для элементов последовательностей. Пусть переменная s — это последовательность с п элементами. Тогда s=(s[0],s[l],s[2], ...,sk— 1]) т. е. s [0] обозначает первый элемент, s [1] — второй и т. д. Далее через s [&..], O^k^n, обозначается последовательность s[k..]=(slk], s[k+ll ..., sin—I]) т.е. slk.A обозначает новую последовательность, которая такая же, как и s, но с удаленными первыми kэлементами. Например,
316 Приложение 2 если s непусто, то присваиванием s := s [1..] из s удаляется первый элемент s [0]. Выполнение присваивания при s=( ) вызывает авост, поскольку в этом случае s[l..] не определено. Стек может быть реализован при помощи последовательности s, если ограничиться операциями s[0] —верхний элемент стека; s:=( ) —опустошить стек; х, s: = s[0J, s[l..] —взять элемент в х\ s: = s | v —добавить к стеку значение v. Очередь можно реализовать при помощи последовательности s, если ограничиться операциями s[0] —ссылка на первый элемент очереди; s:=( ) —опустошить очередь; х, s := s[0], s[l..] —удалить первый элемент, записав его в х\ s:—s | у —поставить в конец очереди значение v. Использование последовательностей вместо обычных операций «взять» и «добавить» для стеков и взятия и постановки в очереди для очередей может привести к более понятным программам. Понятие присваивания уже было рассмотрено (см. гл. 9), и его легко использовать в данном контексте. Операции над целыми и действительными числами Обычно используются следующие множества? Множество целых чисел; {..., —2, —1, 0, 1, 2, ...} Множество натуральных чисел! {0, 1, 2, ...} Мы пользуемся также и множеством действительных чисел, хотя такие числа на любой машине заменяются приближенными с плавающей точкой. Тем не менее предполагается, что арифметические операции выполняются над действительными числами, и таким образом устраняются проблемы, связанные с плавающей точкой. Следующие операции допускают в качестве операндов и целые, и действительные числа; сложение, вычитание, умножение деление х/у\ дает действительный результат операции отношений: х < у: «х меньше у» х^.у: <ах не больше у» х=*у: <lx равно у» х^у: «а: не меньше у» х > у: «л: больше уъ хфух «л: отличается от уъ +, —• * / <, <, =, >, >, Ф
Множества, последовательности, числа 317 abs(x) или |л:| абсолютная величина х:if х < 0 then—х else х floor(x) наибольшее целое число, не превосходящее х наименьшее целое число, не меньшее х минимум х, у, .... Минимум пустого множества—это оо (бесконечность) максимум ху у, .... Максимум пустого множества—это —оо логарифм х по основанию 2: y = log(x), если и только если х = 2у В следующих операциях допускаются в качестве аргументов лишь целые числа: -^ х-г-у—наибольшее целое число, не превосходящее х/у х mod у остаток от деления х на у (при х^О, */>0) even(x) «х—четное», или х mod 2 = 0 odd(x) <a—нечетное», или х mod 2 = 1 ceil(x) min(x, у, тах(х, у, log(x) ...) ...)
Приложение 3 ОТНОШЕНИЯ И ФУНКЦИИ Отношения Пусть А к В — два множества. Декартово произведение А и В, записываемое АхВ,— это множество упорядоченных пар (а, Ь), где а из Л, а Ь из В: Лх5={(а, Ь)\а£А/\Ь£В} Декартово произведение названо по имени творца аналитической геометрии, математика и философа XVII в. Рене Декарта. Число элементов в АхВ равно |Л|*|б|; отсюда и название «декартово произведение». Двуместное отношение между множествами А и В — подмножество Ах В. Поскольку мы будем иметь дело в основном с двуместными отношениями, прилагательное двуместный отбрасывается, и они называются просто отношения. По поводу других отношений будет сказано несколько слов в конце данного приложения. Пусть Р — множество людей. Одним из отношений на РХР является отношение родитель: родитель = {(ау b)\b является родителем а} Пусть N—множество целых чисел. Одним из отношений на NxN является отношение следует_за: следует_за = {(1> i+l) \ i£N\ Следующее отношение сопоставляет каждому человеку год, в который он расстается с жизнью: умер_в={(р, i) | человек р умер в году i} Отношение равенства на Ах А, обозначаемое /, — это / = {(а, <*)\а£А\ При работе с двуместными отношениями имя отношения часто используется как двуместная операция, и для обозначения того, что пара элементов принадлежит отношению, пользуются инфиксными обозначениями. Например, имеем с родитель d> если и только если (d, c)£\(ay b) \b является родителем а) i следует^за /", если и только если i-\-\ = j q умерев /', если и только если (q, /)€{(/?, 01 человек р умер в j-м году}
Отношения и функции 319 Из рассмотрения приведенных отношений можно сделать несколько выводов. В некоторых отношениях члену а могут соответствовать различные пары (а, Ь). Такое отношение называется одно- многозначным. Отношение родитель одно-многозначно, так как у большинства людей более чем один родитель. В некотором отношении члену Ь могут соответствовать различные пары (а, Ь) одного и того же Ь. Такие отношения называются много-однозначными. Многие люди могут умереть в одном и том же году, так что для каждого целого числа i может быть много пар (/?, i) в отношении умерев. Но для каждого члена р имеется не больше одной пары (/?, /)> принадлежащей отношению умерев. Отношение умерев является примером много-однозначного отношения. В отношении следует^за нет двух различных пар с одним и тем же первым или вторым членом. Оно является примером одно- однозначного, или взаимно однозначного, отношения. Отношение на ЛхВ может не содержать ни одной пары (а, Ь) для некоторого а£А. Такое отношение называется частичным. И наоборот, отношение на АхВ является всюду определенным, если для каждого а£А существует пара (ау Ь), принадлежащая отношению. Отношение умерев является частичным, поскольку еще не все люди умерли. Отношение следует_за является всюду определенным (на NxN). Если отношение R на АхВ содержит пару (а, Ь) для каждого b£B, то говорят, что R отображает на В. Отношение родитель отображает на множество всех людей, поскольку у каждого ребенка есть родитель (в предположении, что первого человека не было). Пусть R и 5 — два отношения. Тогда их композицией RoS является отношение, определенное посредством aRoSc, если и только если (gfc: aRb/\bSc) Например, отношение родитель о родитель является отношением дед „или „бабка. Отношение у мер последует _за сопоставляет каждому человеку год, следующий за тем, когда он умер. Композиция ассоциативна. Это означает следующее. Пусть R, S и Т—три отношения. Тогда (RoS)oT*=Ro(SoT). Это легко выводится из определений отношения и композиции. Поскольку композиция ассоциативна, скобки обычно опускаются и пишется просто RoSoT. Композиция отношения с самим собой обозначается показателем степени 2. родитель2 эквивалентно родитель о родитель следующий ^за2 эквивалентно следующий^за о следующий ..за
320 Приложение 3 Подобным же образом определяется R* для любого отношения и натурального числа i: R° = I отношение равенства Я'^о/?'-1 при *>0 Например, родитель0 = I родитель1 = родитель родитель2 = дед_или_бабка родитель3 = прадед _или_прабабка i следует_зак /', если и только если i-\-k~j Рассматривая отношения как множества и пользуясь степенными обозначениями, можно определить транзитивное замыкание R+ и (транзитивное и рефлексивное) замыкание R* отношения R: #+ = /?i(j/?2(J#3U... я»»/?0 и я1 и я* и... Другими словами, пара (а, Ь) принадлежит R+ тогда и только тогда, когда она для некоторого / > 0 принадлежит R1'. Приведем несколько примеров: родитель^—это отношение предок i следуетттза+ /, если и только если для некоторого k > 0, i + k = /, т. е. /< / i следует^за* /, если и только если i^lj И наконец, определим обратное отношение R"1 к R Ь R'1 а, если и только если a R b т. е. (Ьу а) принадлежит обратному отношению 7?""1 тогда и только тогда, когда (а, Ь) принадлежит R, Обращение отношения родитель— это дитя, обращение < есть >, обращение ^ есть ^, обращение отношения равенства / есть само отношение /. Функции Пусть А и В — множества. Функция / из А в В (обозначается /: А -»- В) — это отношение, которое сопоставляет каждому элементу а из Л не более одной пары (а, Ь)> т. е. не многозначно. Отношение родитель не является функцией, поскольку у ребенка может быть больше одного родителя, и для некоторых людей р может быть более чем одна пара (/?, q), принадлежащая множеству родитель. Отношения следует_за и умер_в — функции. Каждую пару из функции f записывают (ау f(a)). Второй ее член f(a) называется значением функции / при аргументе а. Например, для функции следует_за имеем
Отношения и функции 321 следует_за (i)=i+l для всех натуральных чисел /, поскольку следует^за — множество пар {. .., (-2,-1), (-1,0), (0, 1), (1,2),...} Отметим три способа, которыми может использоваться функция /. Во-первых, / обозначает множество пар, таких, что для каждого значения а есть не более одной пары (а, Ь) из /. Во-вторых, a f Ь выполняется, если (а, b)£f. И в-третьих, /(а) — значение, сопоставленное а, т. е. (а, /(а)) — элемент функции (отношения) /. Достоинство определения функции как частного случая отношения состоит в том, что терминология и теория, разработанная для отношений, переносятся и на функции. Таким образом, мы уже знаем, что такое взаимно однозначная функция. Мы знаем, что композиция функций ассоциативна. Знаем для любой функции, что означают /°, /\ /2, /+, /*. Знаем, что такое обращение /_1 функции / и что f"1— функция тогда и только тогда, когда / не является многооднозначной. Функции из выражений в выражения Рассмотрим выражение, например х*у. Можно рассматривать х*у как функцию (одного или более) его идентификаторов, например у: f(y)=x*y В данном случае / рассматривается как функция из выражений в выражения. Например, /(2) =х*2 f(x+2)=x*(x+2) f(x*2) =**л:*2 Таким образом, применение такой функции к аргументу означает текстуальную подстановку выражения, служащего аргументом, вместо идентификатора у повсюду в выражении, служащем определением функции. При подстановке нужно для сохранения приоритета операций вставлять скобки вокруг аргумента (как во втором примере), но мы часто опускаем их, когда они не имеют значения. Такая подстановка рассматривалась подробно в разд. 4.4. п-местные отношения и функции До сих пор мы имели дело лишь с двуместными отношениями. Предположим, что имеются множества Л0, . . ., Апу гО>0. Тогда можно определить отношение на А0хА1Х. . ,хАп как множество упорядоченных кортежей (До, аи . . ., ап_и ап) где каждое аь является элементом множества At. 11 Д. Грис
322 Приложение 3 Пусть g — такое отношение. Далее пусть для каждого кортежа (а0, . . ., a„_s) имеется не более одного кортежа (а0, ..., anmmV an) из g. Тогда g является n-местной функцией — функцией п аргументов, где значение ап — это значение функции, примененной к первым п элементам кортежа: (до, я* . . ., a„-i, g(flo, . . ., a„-i)) g-(a0, . . ., an-i)=an Терминология, использовавшаяся для двуместных отношений и функций одного аргумента, легко распространяется на я-местные отношения и функции.
Приложение 4 АСИМПТОТИЧЕСКИЕ СВОЙСТВА ВРЕМЕНИ ВЫПОЛНЕНИЯ ПРОГРАММ Полезна такая мера времени выполнения алгоритмов, которая применима для любой их реализации и для любой вычислительной машины, в особенности для больших входных значений (т. е. для больших массивов данных). В данном приложении описывается метод измерения времен выполнения программ. Цель приложения — не столько подробно изложить вопрос, сколько очертить основные идеи для тех, кто еще не знаком с ними. Сначала сопоставим некоторое число единиц времени выполнения каждой команде программы. Команда skip и присваивание за- считываются за 1 единицу каждая, поскольку их выполнение требует примерно одного и того же времени всякий раз, когда оно происходит. Команде выбора приписывается максимальное число единиц, сопоставленное ее альтернативам: (в некоторых случаях это может быть сделано не так грубо, как рекомендовано сейчас). Команде повторения приписывается сумма, числа единиц всех ее шагов или же число шагов, умноженное на максимальное число единиц времени, сопоставленное каждому шагу. Важность ограничивающих функций для циклов при установлении времени их выполнения можно пояснить следующим образом. Если в программе нет циклов, то время ее выполнения ограничено независимо от того, каковы ее входные данные. Лишь если в ней имеются циклы, время ее выполнения может существенно зависеть of входных значений, и тогда число повторений цикла, для которого ограничивающая функция задает верхнюю границу, используется для того, чтобы дать оценку используемых единиц времени. Рассмотрим следующие три цикла, гС^О: i := n\ do / > 1 —W := i—1 od i\= n\ / : = 0; do i > 1 —W : = i— 1; / : = 0 od i := n\ do i > 1 —► i := i -f- 2 od Первый цикл требует для выполнения п единиц времени, второй — 2п. Время, требуемое третьей программой, определить труднее. Пусть п — степень двойки, тогда для некоторого k имеет место i=2k. Деление i на 2 эквивалентно вычитанию 1 из k: 2А-=-2=2*~1. Следовательно, k на каждом шаге уменьшается на 1. В момент завершения t=l=2°, так что k=0. Следовательно, подходящей ограничивающей функцией является t=k, и на самом деле цикл выпол- 11*
324 Приложение 4 няется в точности k раз. Итак, в третьей программе для каждого гО>0 делается в точности ceil (log (n)) шагов цикла. Всегда, когда алгоритм делит переменную пополам на каждом шаге цикла, можно быть уверенным, что в оценку времени выполнения входит логарифмический множитель. Это видно из программ быстрой сортировки (разд. 18.2) и двоичного поиска (упр. 4 к разд. 16.3). Логарифмический множитель важен, поскольку log n по мере того, как п растет, становится намного меньше п. Времена выполнения первой и второй из приведенных выше программ могут считаться, грубо говоря, равными, а время выполнения третьей — намного меньше. Это иллюстрируется следующей таблицей, в которой приведены значения п> 2п и log n для различных п. Таким образом, при я=32768 первая программа требует 32768 единиц времени, вторая — вдвое больше, а третья — лишь 15. п: 2/г: log n: 1 2 64 2 4 128 0 1 6 128 256 7 32768 65536 15 Нам нужна мера, которая позволяла бы говорить, что третья программа намного быстрее других, а оставшиеся две, в сущности, эквивалентны. Для этого определим порядок времени выполнения. (А4.1) Определение. Пусть f (п) и g(n)—две функции. Функция f(n) не больше по порядку, чем g(n) (записывается 0(g(n)))f если существует константа с > 0, такая, что для всех (кроме, может быть, конечного числа) положительных значений / (п) ^ ^ic*g{n). Далее f(п) пропорционально g(n), если f(n) есть 0(g(n))> a g(n) есть О (/(я)). В этом случае говорят, что f (п) и g(n)—одного и того же порядка. □ Пример 1. Пусть / (п) = п + 5у g(n) = n. Выберем с = 5. Тогда при 1 < п, / (п) = п + 5 < Ъп = 5 *§• (п). Следовательно, / (п) есть О (g(n)). При с = 1 видим, что g(n) есть 0(f(n)). Следовательно, п-\-5 пропорционально п. □ Пример 2. Пусть f(n) = n и g(n) = 2n. При с=1 видим, что /(п) есть 0(g(n)). При с = 2 видим, что g(n) есть 0(f(n)). Следовательно, п пропорционально 2п. Для любых констант /С1^=0 и К2 /С1*Аг+/С2 пропорционально п. □ Так как первая и вторая из приведенных выше программ имеют время выполнения п и 2п единиц соответственно, то их времена выполнения одного и того же порядка. Далее можно доказать, что log лг есть 0(п)у но не наоборот. Следовательно, порядок времени выполнения третьей программы меньше, чем у первых двух.
Время выполнения программ 325 Ниже приведена таблица типичных порядков времен выполнения от самых маленьких до самых больших, которые часто встречаются в программировании. Даются и обычные термины, применяемые для обозначения этих времен. Времена заданы через единственный входной параметр п. Помимо символьных обозначений, приведены (округленные) значения порядков для п=Ю0 и лг= 1000, так что разницу между ними легко увидеть Порядок 1 \оя,п Vn п n\ogn п \Гп /г2 я3 2я гс=100 1 7 10 100 700 1000 10000 1000000 1.26*103° п=1000 1 10 32 1000 10000 31623 1000000 109 10зоо Используемый термин Алгоритм, требующий постоянного времени Логарифмический алгоритм Линейный алгоритм Квадратичный алгоритм Кубический алгоритм Экспоненциальный алгоритм Для алгоритмов с несколькими входными значениями вычисление порядка времени выполнения труднее, но приемы остаются теми же самыми. Сравнивая два алгоритма, нужно сначала сравнить порядки их времен выполнения, а затем, если они окажутся одинаковыми, переходить к тонкостям, таким, как множитель при требуемом числе единиц времени, число совершаемых сравнений массивов и т. п. Алгоритм может выполняться за разное время в зависимости от конфигурации входных данных. Например, один массив Ь [1: п] может быть упорядочен за п шагов, а другой Ь[\ : п] — за п2 одним и тем же алгоритмом. В таком случае имеются два метода сравнения алгоритмов: по среднему (или ожидаемому) времени выполнения и по времени в наихудшем возможном случае. Первый из них довольно сложен, второй обычно намного проще. Например, программа линейного поиска (16.2.5) требует в наихудшем случае п единиц времени, а в среднем случае п/2 единиц, если предполагается, что вероятность обнаружения значения в любом месте одна и та же.
ОТВЕТЫ К УПРАЖНЕНИЯМ Ответы к гл. ' 1. Покажем вычисление выражений для состояния si: (a) 1(mVn) = 1(rvf) = ,Г = 1' (b) -,mv„ = ,rvF = FVF = F (c) i(m*n) = i(TAF) = iF = T (d) лшЛи = ,TAF = FAF = F (e) (m4n)^>p = (TVF)^>T = T^>T = T (f) т^(пФр) = T4(F^>T) = Tvr = T (g) (m =и)л(р =?) = (F=F)A(7 = F) = JAF = F (h) m=(/jA(p=4)) = F=(FA(J = F)) = F=(FAF) = F.= F = T (i) w =(иЛр =q) = F=(F*T = F) = F=(F=F) = F = T - F (j) (m =п)л(р»?) = (F = r)A(F»r) = FAT = f (k) (m=n*p)^>q = (F = TAF)^J = (F = F)^T = T^>T = T (1) (m^>H)=S>(p=>4) = (F=>F)=HF=#>F) = T^-T = T (m)(m =*>(/? $>p))^>q = (F^>(F^>F))^F - (F^>T)^F = T^F = F Для состояния s2 дадим (лишь) ответы: (a) F, (b) Т, (с) 7\ (d) неопределено, (е) F, (f) Г, (g) F, (h) F, (i) T, (j) Г, (k) T, (1) Г, (m) Г. b с d т т т Т Т F Т F Т Т F F F Т Т F T F F F Т F F F Ъ Ус т т т т т т F F bvcvd Т т т т т т т F . Ъ Ас Т Т F F F F F F Ь Ac A J Т F F F F F F F
Ответы к упражнениям 327 2. l\d bed T Т T Т Т F Т F Т Т F F F Т Т F T F F /•• Т F F F cVd Т Т Т F Т т г F Ь'*(сЧ Т Т Т F F F F F d) с *d Т F F F Т F F F bV(cbd) Т Т Т Т Т F F F 2. e.f.g Ь с Т Т Т F F Т F F 1 b F F Т Т b vc Т т т F i6^ b vc т т т F ,6 = b vc F F T F лЬ = c F T T F (ib=c) vb T T T F 2. h / b с T'T T F F T F F ЬЧс T T T F b =>c T F T T c^b T T F r (6VC)A (b Фс) T F T F (Ис)Л (Ь =^>С)Л (C=5>6) T F F F b с T T T F\ b =c T F F T\ F F F\ T b ^>c T F T T c^>b T T F j T 1 (b Фс)л (c^b) T F | (b^c) = {{b =>c) л- (c^b)), T T F T T [ T
328 Ответы к упражнениям 3. Пусть булевы идентификаторы имеют следующий смысл: хменьшеу \х < у\ хравноу:х = у; хболыиеу.х > у\ хнеменьшеу: х^у\ уменьшег\у<^г\ уравног:у*=г\ убольшег\у^>г\ vpaenow:v=w; прихменьшеу: выполнение Р начинается при х < у; прихменьшеО:выполнение Р начинается при х<0; заве ршаетсяприу равно м2вх\ выполнение Р завершается при у = 2х\ некончается: выполнение Р не заканчивается. Приведем по одному из возможных высказываний; имеются и другие: (a) хменьшеу \Jхравноу (b) хменыиеу V хравноу V хболыиеу (c) (хболыиеу Л уболыиег) =Ф> vpaenow (d) хменьшеу /\у меньшег /\vpaenow (e) "] (хменыиеу /\уменыиег) Л 1 (хменыиеуЛ vpaenow)Л "I (у меньшег/\vpaenow) (f) "] (хменьшеу V у меньшег V vpaenow) (g) ~] (хменьшеу Д у меньшег /\ vpaenow) (h) (хменьшеу =$> у меньшег) /\ (хнеменьшеу => vpaenow) (i) (хменьшеу =>(#меньшег => vpaenow)) Л (хнеменьшеу =£> "| у меньшег) Л (vpaenow => хменьшеу) ()) прихменьшеу => заве ршаетсяприуравном2вх (к) прихменьшеО =ф некончается 4. Пусть Д означает «идет дождь», К—«я пойду купаться», П— «будь я проклят», Ж—«живут как кошка с собакой», ЖК «живут, как кошка», ЖС—«живут, как собака». (а) ДУ1Д=>К, (Ь) Д=>1К, (с) Ж, (d) ЖКУЖС, (е) Д-ХПД1К), (f) (ДДК)=>П. Ответы к гл. 2. 1. Приведем таблицы истинности для первого из законов коммутативности и для закона отрицания: Ъ с Т Т Т F F Т F F b^c c^b (b лс)=(с л/?) т т т F F Т F F Т F F Т ib iib iib— b FT T T F T Таблица для первого из законов дистрибутивности (закон справедлив, так как два последних столбца одинаковы и, следовательно, выражения, которым они соответствуют, эквивалентны):
Ответы к упражнениям 329 bed Т Т Т Т Т F Т F Т Т F F F Т Т F T F F F Т F F F с Ad т. F F F Т F F F ЪЧс т т т т т т F F bvd т т т т т F Т F Ъ V(c Л Г Т Т т т F F F d) (Ь Vc)A(fc VJ) т Т Т Т Т F F F Таблица истинности для первого из законов де Моргана (поскольку последние два столбца совпадают, соответствующие им выражения эквивалентны и закон справедлив): b с Т Т Т F .F Т F F b лс -yb л с Т F F F F Т F T F F Г Т •у(ЬЛс) -хЬ^лС F F Т Т Т Т Т Т Таблица истинности для законов исключенного третьего, противоречия и импликации (поскольку два последних столбца совпадают, соответствующие им выражения эквивалентны и, следовательно, справедлив закон импликации)! Ь с Т Т Т F F Т F F лЬ F F Т Т ЬЧтЬ т т т т Ъ Aib F F F F Ъ^-с Т F Т Т чЬЧс Т F Т Т Таблица истинности для закона эквивалентности (поскольку последние два столбца совпадают, соответствующие им выражения эквивалентны и закон справедлив): Ъ с Т Т Т F F Т F F h*>c c^>b Т Т F Т Т F Т Т c=b {b^>c) *{с^>Ь) т т F F F F Т Т Таблица истинности для законов упрощения V (кроме последнего)*
530 Ответы к упражнениям ъ Т F ЬЧЬ bvb =Ь т т F Т Ъ УТ ЬМТ =Т т т т т Ь VF b VF =b т т F Т Таблица истинности для законов упрощения Д (кроме последнего): ь т F bAb bAb =b т т F Т Ьат ЬаТ =Ь т т F Т bAF bAF =F F T F T 2. e = e\/e (упрощение V) = e (упрощение V) 3. ~] T = "| (Г V 1 Т) (исключенное третье) 8=8 ~1^Л 1 ~]Т (закон де Моргана) = ~]Т/\Т (отрицание) = Т Д "| Т (коммутативность) —F (противоречие) 4. "| F = ~] (ТД "| Т) (противоречие) = "jrv ~| ~]Т (закон де Моргана) = "]Т\/Т (отрицание) = Т V "1 Т (коммутативность) = Т (исключенное третье) 5. Столбец 1: (Ь) противоречие, (с) упрощение V, (d) упрощение \Д (е) упрощение Д, (f) равенство, (g) противоречие, (h) ассоциативность, (i) дистрибутивность, (j) дистрибутивность, (к) коммутативность (дважды), (1) отрицание, (т) закон де Моргана. Столбец 2: (Ь) импликация, (с) импликация, (d) отрицание, (е) закон де Моргана, (f) равенство, (g) закон де Моргана, (h) дистрибутивность, (i) исключенное третье, (j) упрощение Д, (к) импликация, (1) отрицание, (т) ассоциативность. 6. (a) xy{y\Zx)\f]y = xV(xVy)V~]y = (x\Jx)\/{y\J-}y) = х\/Т = т (b) (xVy)A(xV]y) = x\J(yAly) = x\/F = T (c) x\/y\J ~]x = x\/ ~\x\/y = T\Jy = Г (коммутативность) (ассоциативность) (упрощение V, исключенное третье) (упрощение V) (дистрибутивность) (противоречие) (упрощение V) (коммутативность) (исключенное третье) (упрощение V)
Ответы к упражнениям 331 (d) (xVy)A(xV\y)AnxVy)AnxWly) = (xV(yA 1 У))А(~]х\/(уА 1 У)) (дистрибутивность, два- = (xVF)AnxVF) = F (е) (хАУ)\/(ха1у)УПхАу)\/ПхА1у) ~{xA(yV ly))\ZC]xA(yV !#)) (дистрибутивность) жды) (противоречие) (упрощение V) (противоречие) =*(хАТ)\/ПхАТ) = *V ~\x = Г Ф (lxAy)Vx ^(xW(lxAy) ^(х\У]х)А(хУу) = TA(x\Jy) ~х\/у (g) ~]х=з>(хАу) =**V(xAy) (h) T=$>(-\x=$>x)=*-\T\/{x\/x) =*F\/(x\/x) (i) x=s>(y=s>(xAy)) ~1х\/{-\у\/(хАу)) =» 1*V~1W(*A</) = l(xAy)V(xAy) (j) ~и=з>(-|х=И"1*Л</)) ~*V(*V(1*M)) = *V(1*A</) = (*V"1*)A(*V</) ^rA(^Vy) (k) 1f/=># = J/V*/ = </ (1) 1</=» ~\У = У\/~\у (исключенное третье) (упрощение Л) (исключенное третье) (коммутативность) (дистрибутивность) (исключенное третье) (коммутативность, упрощение Л) (импликация, отрицание) (упрощение V) (импликация, отрицание) (упр. 3) (упрощение V, дважды) (импликация, дважды) (ассоциативность) (закон де Моргана) (исключенное третье) (импликация, отрицание) (ассоциативность, упрощение V) (дистрибутивность) (исключенное третье) (упрощение Л) (импликация, отрицание) (упрощение V) (импликация, отрицание) (исключенное третье) 7. Высказывание е преобразуется к нормальной форме с использованием законов эквивалентности. Главными являются следующие шесть шагов преобразования!
332 Ответы к упражнениям 1. Закон равенства используется для исключения всех вхождений = . 2. Закон импликации используется для исключения всех вхождений =>. 3. При помощи закона де Моргана и закона отрицания ~| «проносится внутрь» таким образом, чтобы ~] применялось лишь к идентификаторам и константам. Например, "] (а\/ (F/\ 1с)) преобразуем следующим образом: -](aV(FA 1с)) = la A I (F А 1с) = laAilFV 1 1^)== laA(lFVc) 4. При помощи ~\F = T и ~]T = F устраняются все вхождения IF и ~|7 (см. упр. 3 и 4). 5. Теперь высказывание имеет вид e0\J ... \/еп для некоторого п ^ 0, где каждое е, имеет вид g0 Л ... Л gm- До тех пор пока все gf во всех е{ не примут форму идентификатор или 1 идентификатору Т, F, повторяются следующие действия: Рассмотрим некоторое eh в котором есть gjy не имеющее нужной формы. При помощи закона коммутативности передвинем gj вправо, после чего оно станет gm. А теперь gm должно иметь вид (AeV..-Vft*). а все е{—вид g0A... Agm-iA(h0V... Vft*). Пользуясь дистрибутивностью, заменяем это выражение на (£Л.. • Agm-iAhQ)V ..'. V(£0A. • • Аёт^УК) При этом на главном уровне добавляется k высказываний eh но уровень вложенности операций уменьшается по крайней мере в одном месте. Следовательно, после нескольких повторений этот процесс должен завершиться. 6. Теперь высказывание имеет вид е0\/.. .\/еп, я^О, где каждое е{ имеет вид (g0A • • • Agm), a g{ — идентификатору ~| идентификатору Т или F. Пользуясь законами коммутативности, противоречия, исключенного третьего и упрощения V, приводим высказывание к окончательному виду. Если некоторое из et сводится к Г, то и все высказывание сводим к Т\ если некоторое из et сводится к Fy устраняем его, пользуясь законом упрощения V (если только все высказывание не оказалось F). 8. Процедура сведения та же, что и в упр. 7, за исключением следующего. На шаге 5 рассматривается высказывание, имеющее вид е0А • • • Асп при я> О, где каждое изе, имеет вид g0V ... VgOT. Для упрощения g( используется другой из законов дистрибутивности. На шаге 6 используются законы упрощения Д. а не законы упрощения V; если некоторое е( сводится к F, то и все высказывание сводится к F\ если некоторое е{ сводится к Г, то для его устранения используется закон упрощения Л- 9. В упр. 1 было доказано, что законы эквивалентности являются тавтологиями. Покажем теперь, что индукцией по виду выска-
Ответы к упражнениям 333 зывания Е(р), где р—идентификатор, что использование правила подстановки также порождает тавтологии. Случай 1, Е (р) — Т, или F, или идентификатор, отличный от р. В данном случае как Е (el), так и Е (е2) совпадают с самим Е. По закону равенства Е = Е, так что порождается тавтология. Случай 2. Е(р) есть р. В данном случае Е(е1) есть el, а Е(е2) есть е2. По предположению el=e2—тавтология. Случай 3, Е (р) имеет вид ~]Е\ (/?). По предположению индукции £1 (el) = El (е2). Следовательно, £1 (el) и Е\ (е2) в любом состоянии имеют одно и то же значение. Тогда следующая таблица истинности устанавливает нужный результат: Е1(е1) Е1(е2) Т Т F F чЕ1(е1) лЕ1{е2) F F Т Т л Е1(е1) = -, Е1(е2) Т т Случай 4. Е(р) имеет вид Е1(р)/\Е2(р). По предположению индукции Е1(е1)=Е1 (е2) и Е2(е1)=Е2(е2). Следовательно, El (el) и Е1(е2), Е2(е1) и Е2(е2) в любом состоянии имеют одно и то же значение. Тогда следующая таблица истинности устанавливает справедливость ожидаемого результата: £1(е1) Е1(е2) Т Т Т Т F F F F Е2(е1) Е2(е2) Т Т F F Т Т F F Ж1(е1)ЛЕ2(е1) Т F F F Е1(е2)КЕ2(е2) Т F F F Оставшиеся случаи, когда Е(р) имеет вид Е1 (р)\/Е2(р), Е1(р)=> Е2(р) и Е1(р)=Е2(р), подобны уже разобранным и здесь не рассматриваются. А теперь покажем, что использование правила транзитивности порождает тавтологии. Так как el=e2 и е2=еЗ — тавтологии, то известно, что как el и е2 в любом состоянии имеют одни и те же значения, так и е2 и еЗ в любом состоянии имеют одни и те же значения. Следующая таблица истинности устанавливает справедливость ожидаемого результата: el e2 еЗ Т Т Т F F F е!=еЗ Т Т 10. Сведем д_к высказыванию el, находящемуся в конъюнктивной нормальной форме (см. упр. 8). Из упражнения следует, что e=el
334 Ответы к упражнениям является тавтологией, а поскольку предполагается, что е — тавтология, то и el должно быть тавтологией. Следовательно, el истинно во всех состояниях. Высказывание el есть 71, или Fy или имеет вид £оЛ- • -Л^п, где каждое из et имеет вид g0\A • -VgVn, каждое из gi — вид идентификатор или "] идентификатору причем идентификаторы, входящие в разные gt, различны. Поскольку в последнем случае высказывание не является тавтологией, оно не может иметь вид е0/\. • -Л^тг- Оно не может быть и F, так как F — не тавтология. Следовательно, оно должно быть Г, и е=Т доказано. Ответы к разд. 3.2 1. (а) Л-1, (Ь) Л-Е, (с) V-l, (d) =-E, (e) V-I, (f) V-I, (g) Л-Е, (h) = -I, (i) Л-I, (j) =>-E, (k) «-I, (1) =>-E, (m) «-I, (n) V-E, (o) V-E. Из 1 2 3 p получить p p ПОС 1 p hp Л-1, 1, I p Л-Е, 2 Из 1 2 3 4 PhQ, РАЯ p =*>/• P r P~- $>r получить r ПОС 1 пос 2 л-Е, 1 ^>-E, 2, 3 ИЛИ Из p Kq»P ^r ПОЛУЧИТЬ р P P л-Е, пос 1 =>-E, пос 2,1 3. (b) ИЗ р =q, q ПОЛУЧИТЬ р 1 2 3 4 p=q Я q^p P пос 1 пос 2 =-E, 1 *>-E, 3, 2 или Из р —q, q получить p q ">p =-E, noc 1 P ^-E, I, noc 2
Ответы к упражнениям 3. (с) Из р, д ^г,р ^>г получить р ^г 1 г =>-Е, пос 3, пос 1 р Лг * A-I, ПОС 1,1 3. (d) Из Ь Атс получить к? 1 [ 1С А-Е, ПОС 1 3. (е) Из Ъ получить bv тс 1 I 6vlC v-i, пос 1 3, (0 Из 6 ^>с AJ, 6 получить с/ с л rf =£>-Е, пос 1, пос 2 rf А-Е, i 3. (g) Из pbg^p^>r получить г р л-Е, пос 1 г ^-Е, пос 2,1 3. (h) ИЗ р, f? л(р =>$) ПОЛУЧИТЬ ff Алг__ 1 2 3 4 <7 Р ^^ л ^7 А<; л-Е, пос 2 А-Е, пос 2 =>-Е, 2, пос 1 М, 1,3 3. (i) Из р -д получить д -р 1 2 3 Р ^Я д ^>р д=р =-Е, пос 1 —Е, пос 1 =-1, 2, 1 3. (j) Из Ь =>(с л^), 6 получить с/ с л с/ =>-Е, пос 1, пос 2 d А-Е, 1
336 Ответы к упражнениям 4. Доказательство 3(a). Поскольку p/\q истинно, р должно быть истинно. А тогда из р=>г заключаем, что должно быть истинно г. Доказательство 3(b). Так как p=q истинно, то истинность q означает, что и р должно быть истинно. Доказательство 3(c). Поскольку р истинно, из p=w заключаем, что г также истинно. Следовательно, истинно р/\г. Доказательство 3(d). Поскольку Ь/\ ~]с истинно, должно быть истинно и ~\с. Доказательство 3(e). Поскольку b истинно, дизъюнкция Ь с любыми высказываниями также истинна, и Ь\1 ~\с истинно. Доказательство 3(f)! Поскольку b=>c/\d> из истинности b следует истинность c/\d. Но тогда должно быть истинно d. Доказательство 3(g). Из p/\q мы заключаем, что р истинно. Так как истинно и р^=>гу то истинно л Доказательство 3(h). Поскольку q/\(p^s) истинно, должны быть истинны как q, так и p=>s. Это последнее высказывание вместе с р позволяет заключить, что и s истинно. Но тогда истинно qAs- Доказательство 3(i). Эквивалентность является коммутативной операцией, так что p=q влечет q=p. Доказательство 3(j). Из b и b=>(c/\d) мы заключаем, что истинно с Ad. Но тогда истинно d. Ответы к разд. 3.3 1. Получить (р л<? J\(p^>r))^>(rV(g^>r)) 1 | (/)Лдл(^г))Ф(гУ(^г)) =>-1, (3.2.11) 2. Получить (рЛд)^>(рУд) р 1 ИЗ р Лд ПОЛУЧИТЬ р V д р Л-Е, пос 1 p\!q V-I, 1.1 1.1 1.2 (pAq)^(pVq) *>-I,l 3. (а) Получить а ^>д *д 1 Из q получить д ^д 1.1 I g^q а-!, пос 1, пос 1 д '^>g^q ^-I, i 3. (Ь) Получить д t\q ^>q 1 Из д Лд получить д 1.1 \ д л-Е. пос 1
Ответы к упражнениям 3. (с) Получить д =(д *д) ' 1 I q=q =-I,-3.(a),3.(b) 3. (d) Получить д=(длд) 1 2 3 4 5 Из q ПОЛУЧИТЬ q Afl 1.1 | q A<jr л-l, пос 1, пос 1 q^q Aq =>-!, 1 Из q *q получить q 3.1 1 q Л-E, пос 1 qAq^q =>-I, 3 q=(qAq) =-1,2,4 4. Получить /? =/?.vp 1 2 3 4 5 Из p получить р vp 1.1 | pvp v-I, пос 1 p^>p4p =^>-I, 1 •• Из р vp получить р 3.1 | p v-E, пос 1, (3.3.3), (3.3.3) p Vp =S>p =>-I, 3 Р =P Vp —I, 2, 4 5. Получить р »((rV5-):»p) P пос 1 Из р получить (rVs)^p 2.1 2.2 Из /-Vi- получить р 2.1.1 | p >v5)=>p ((/-v,-)=>p) ==>-E, (3.3.3), 1 =>-I, 2.1 ^>-I, 2 б. Получить q=>(r^>(q лг)) 1 Из д получить г^(длг) 1.2 1.3 q пос 1 Из г получить д Лг 1.2.1 | qAr r^>(q Лг) q-^(r^>(q*r)) Л-I, 1.1, ПОС 1 =5>-1, 1.2
338 Ответы к упражнениям 7, ИЗ./? =И/ =^5) получить r-->(p^>s) 1 2 3 р =>(Г =5>5) ПОС Г Из г получить /?=£>$ 2.1 2.2 2.3 г=>(/ г пос 1 Из /; получить s 2.2.1 2.2.2 г ^>s =>-Е, I, пос 1 .у =>-Е, 2.2.1, 2.1 =>-!, 2.2 8. Ссылка в строке 2.2 на строку 2 незаконна. 9. Из лрЛ-ур ^>д)У(р а (г ^><?)) получить г ^>д 1 2 3 4 5 6 т/7 ПОС 1 Из -\p^>q ПОЛУЧИТЬ r^>q 2.1 2.2 2.3 •\р ^>q пос 1 Из г получить <? 2.2.1 | g =^Е, 2.1, 1 r^>q =М, 2.2 (^P^g)^(r^q) ^-1,2 ИЗ p/\(r^q) ПОЛУЧИТЬ r^q 4.1 | г ^>q Л-Е, пос 1 (Рл(^?))»(г^>?) =£4,4 г ^д v-E, пос 2,3,5 .Из 1 2 3 4 Я^Р* q ^>r q^p q^>r ПОЛУЧИТЬ <? =>(/> А г) ПОС 1 пос 2 Из q получить р л г 3.1 3.2 3.3 с/ =>(/; Р г р Лг АГ) =^-Е, 1, пос 1 =>-Е, 2, пос 1 л-1, 3.1, 3.2 ^-1,3
Ответы к упражнениям 11. Из лд получить д^р 1 2 3 •xq пос 1 Из q получить р 2.1 2.2 2.3 q^p q пос I Из ip ПОЛУЧИТЬ q^-\q 2.2.1 \ q^iq Л-1, 2,1, 1 р п-Е, 2.2 =>-!, 2 12. Из -iff получить д^-лр ■>q Из пос \ д получить лр 2.1 2.2 2.3 ч Из пос 1 получить д л -,д 2.2.1 ■»Р qA-iq tf»-.p л-1, 2.1, 1 1-1,2.2 =>-!. 2 ' 13. Доказательство точно такое же, как и для упр. 11, за исключением того, что р всюду заменяется на р/\ ~\р. 14. Из 1 2 3 4 5 Pv4 , 1 q получить р рЧ q пос 1 19 пос 2 Из q получить р 3.1 3.2 3.3 q^> Р q ПОС 1 Из ->р получить q A-,<jr 3.2.1 \qb-iq Л-1,11,2 р т-Е, 3.2 р ^-1, 3 VE, 1,(3.3.3), 4' 15. Получить р л(р^>д):>д 1 2 Из р л(/> =5><7) 1.1 1.2 1.3 (рЛ| Р P^-tf Я [p^>q))^q получить с/ Л-Е, ПОС 1 Л-Е, ПОС 1 ^>-Е, 1.2, 1.1 ^-1,1
340 16. Получить ((р ^д)л(д =>r))^Q? =^r) Ответы к упражнениям 1 ) ИЗ (р =2>q)b(q ^r) ПОЛУЧИТЬ р =>г 1.1 1.2 1.3 1.4 ({Р- Р^Ч q ^>r Из р получить г 1.3.1 1.3.2 Я г р^>Г >Ч)ЧЯ^Г)) =$>(£=§> л-Е,пос 1 А-Е, ПОС 1 =>-Е, 1.1,пос 1 ==>-Е, 1.2, 1.3.1 =5>-1, 1.3 /•) *Ч 1 17. Получить (pJ>q)^((pA.4q)=>q) 1, * 2. Из 1.1 1.2 1.3 (Р^ p^-q ПОЛУЧИТЬ (р Л ^q)^>q P^q пос 1 Из р л п<7 получить с7 1.2.1 1.2.2 /? • л-Е, пос 1 q ^>-E, 1.1, J.2.1 (/> *iq)^q =>-!. 1.2 ~>q) >{{p Л i<7)^) -'-К 1 18. ПОЛУЧИТЬ ((рЛтд)Ф^)Ф(рФ^) 1 ИЗ (p^^q)^q ПОЛУЧИТЬ /J=5><7 1.1 1.2 1.3 ((Р (р л-,</).^>д пос 1 Из р получить q 1.2.1 1.2.2 1.2.3 р ^q /7 ПОС 1 Из -к/ получить ^л1(| 1.2.2.1 1.2.2.2 1.2.2.3 Ч >q)^(p рл^ л-1, 1.2.1, пос 1 q =5>-Е, 1.1, 1.2.2.1 - ^ л п(5! л-1, 1.2.2.2, пос 1 -,-Е, 1.2.2 =£>-!, 1.2 =5>?) =S>-1, 1
Ответы к. упражнениям 19. (а) Из р =>д получить (р а^) "> -*р 1 2 3 Р ^д пос 1 , Из /;А^ получить лр 2.1 2.2 2.3 (рЛ-, -i# А-Е, ПОС 1 Из р ПОЛУЧИТЬ q A лц 2,2.1 2.2.2 <7," =£>-Е, 1, пос 1 9 ап<? A-j; 2.2.1, 2.1 -,/> i-I,2.2 <?)=>■!/> =>-1,2 341 19. (Ь) Получить (р^д)^>({р ^лд)^>лр) 1 \ (p^>q)^((pA-iq)^ip) ^-1, 19(a) 20.(а)Из И^)ф1Р получить jp =^^ 1 2 3 1(рЛ ИЗ , 2.1 2.2 2.3 р ^ч ■><7)=>-i/> ПОС 1 Р получить q Р nocl ИЗ 1? ПОЛУЧИТЬ рЛ.,/7 2.2.1 2.2.2 2.2.3 Ч 1 р л-i gr M, 2.1.1ЮС 1 ■ф =#>-Е, 1,2.2.1 р^лр М, 2.1.2.2.2 1-Е, 2.2 =^-1,2 20. (Ь) Получить ((рА-1?)=>т/7):>(рФ<7) 1 1((РЛ-|^)=^п/?)^>(р=>9) *>•!, 20(a) 21. (а) Из р = </ получить лр-лд 1 2 3 4 5 g =>/> —Е, пос 1 Из лр получить -уд 2.1 2.2 2.3 т/7 ПОС 1 ИЗ q ПОЛУЧИТЬ р А пр 2.2.1 2.2.2 р =£>-Е, 1, пос 1 /?Л1/7 л-1, 2.2.1, 2.1 п? -,-1,2.2 ~*р ^ ~*д ^-^ 2 т q ^> 1 р (подобно строкам 2-3) Т/7 = = ->? =-1, 3, 4
342 Ответы к упражнениям 21, (Ь)Получить (р -д)^>{лр - лд) 1 | (p=q)=Z>(,p'=iq) ^-l, 21(a) 22. Доказательство опущено, так как оно подобно доказательству упр. 21. 23.(а) Из ^(р-д) получить лр—д 1 2 3 4 5 лф—q) ПОС 1 Из q получить чр 2.1 2.2 2.3 q ПОС 1 ИЗ р ПОЛУЧИТЬ (р= q)Л -,(p=q) 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 р пос 1 Из р получить q 2.2.2.1 [q 2.1 p^q ^>-I, 2.2.2 Из q получить р 2.2.4.1 |р 2.2.1 ? =5>р =>-1, 2.2.4 p=q =-1,2.2.3,2.2.5 Ср=<?)Л-,(р=5) М, 2.2.6, I ip -,-1,2.2 q^-лр _, ^>-1,2 ip^q - (подобно строкам 2' чр = -ч --1,3,4 -3)1 23. (Ъ) Получить i(p =^)»dp=<7^ 1 I i(p=9)=>(ip=g) -I, 23(a) 24. (а) ИЗ i/) -q получить ->(р = q) 1 2 3 ip ~'q — E, ПОС 1 q > -\p —E, noc 1 ИЗ р —q ПОЛУЧИТЬ р A ip 3.1 3.2 3.3 3.4 /? ^-g =-E, noc 1 q -^p =-E, noc 2 Из -i/; получить p^-^p 3.3.1 3.3.2 3.3.3 p q =>-E, 1, noc 1 ^ p ^-E, 3.2, 3.3.1 > /jA^ M, 3.3.2,noc I > n-E, 3.3
Ответы к упражнениям 3.5 3.6 3.7 Из р 3.5.1 3.5.2 3.5.3 ПОЛУЧИТЬ р А лр г/ ^>-Е, 3.1, пос 1 -,/> =>-Е, 2, 3.5.1 р л тр л-1, пос 1, 3.5.2 ч/> 1-1,3.5 рЛ,р л-1, 3.4, 3.6 л[р=Ч) -»"1»3 24, (Ь) Получить (-ур=д)^-у{р=д) 1 I (лр=ч)^л{р=я) ^1, 24(a) 25. Получить (р-д)^(д =р) 1 2 Из /> = </ подучить 1.1 1.2 1.3 (/> = Р^Ч Ч^Р Ч=Р Ч)^(Ч=Р) Ч =Р ~Е, пос 1 =-Е, пос 1 =-1, (.1, 1.2 26. Из /? получить р р пос 1 Из т/у получить р А~*р ТГ\ РА^Р л-1, 1,пос 1 -,-Е, 2 27. Следующие доказательства «на естественном языке» не нужно считать чем-то заслуживающим особого внимания. Они лишь по- казывают, как можно обосновать высказывания, пользуясь естественным языком. Более разумно строить таблицы истинности или пользоваться эквивалентными преобразованиями из гл. 2 — они заслуживают большего доверия. Многие из приведенных доказательств опираются на то, что высказывание Ь=>с истинно в любом состоянии, в котором истинно его заключение с, и, следовательно, для того чтобы доказать, что Ь=>с является тавтологией, достаточно исследовать лишь те состояния, в которых ложно с. Доказательство 1. Высказывание (pAQA(P=^r))=^(r\/(q^^r)) истинно, поскольку уже было доказано в (3.2.11), что r\J (q=>r) следует из рЛЯЛ(Р=>г). Доказательство 2. Если p/\q истинно, то р истинно. Следовательно, дизъюнкция р с чем угодно тоже истинна, так что p\/q истинно. Доказательство 3. Если q истинно, то таково же и q/\q. Доказательство 4. Всегда, когда р истинно (ложно), таково же и РАР- Значит, они должны быть эквивалентны.
344 Ответы к упражнениям Доказательство 5. Предположим, что р истинно. Тогда истинное е=>р независимо от того, каково е. Следовательно, (r\/s)=>p истинно. Значит, истинно и p=>((r\/s)=>p). Доказательство 6. Если q ложно, то истинно q=>e при любом е. Следовательно, остается показать истинность r=^(q/\r)y предполагая, что q истинно. Если г ложно, то истинно r=^q/\r. Предположим, что г истинно. Тогда таково же и q/\ry и, следовательно, r^q/\r. Доказательство 7. Если ложно г или р, то истинно r=>(p=>s), так что рассмотрим лишь.случай, когда г=р=Т. Из предположения тогда можно заключить, что истинно и s, так что истинно r=>(p=>s). Следовательно, заключение доказано, исходя из предположения. Доказательство 9. Покажем, что заключение следует из каждого дизъюнктивного члена второго предположения (и первого предположения). Тогда заключение будет истинно при условии, что истинна дизъюнкция. Высказывание q следует из предположения "]/? и из первого дизъюнктивного члена. Тогда r=>q истинно, поскольку q истинно. И наконец, заметим, что если истинен второй дизъюнктивный член p/\(r=>q), то должен быть истинен и его второй конъюнктивный член, г. е. r=>q. Доказательство 10. Предположения означают, что риг истинны, когда истинно q. Следовательно, р/\г также истинно. Доказательство 11. Если ~]q истинно, то q ложно. Поскольку F=>p истинно, таково же и q=>p. Доказательство 12 Подобно доказательству 11. Доказательство 13. Подобно доказательству 11. Доказательство 14. Предположим, что p\Jq и "1^7 истинны в некотором состоянии. Тогда в нем должно быть истинно либо /?, либо q. Так как q ложно, то р должно быть истинно. Доказательство 15. Если q истинно, то таково же и заключение, поскольку r=>q истинно для любого высказывания л Если q ложно, то таково же и pf\(p=>q) (чтобы убедиться в этом, достаточно рассмотреть для р обе возможности! Т и F). А поскольку F=>F истинно, истинно и все заключение. Доказательство 16. р=>г истинно всюду, кроме тех состояний, в которых р истинно, а г ложно. Следовательно, всюду, кроме таких состояний, истинно и заключение (поскольку х=>Т истинно для всех л:). Чтобы получить теорему, нужно заключить, что (p=>q)/\ (q=^r) ложно, если р истинно, а г ложно, т. е. что ложно (T=>q)/\ (q=>F). Первый член T=>q истинен, лишь если истинно q, но если оно истинно, то ложно q=>F, и, следовательно, (T=>q)/\(q=>F) ложно. Доказательство 17. Если g истинно, то таково же и (р/\ ~]q)=>q. Следовательно, таково же и высказывание. Предположим, что q ложно Если, кроме того, р истинно, то p=s-q ложно, так что ложна посылка высказывания, и все высказывание истинно. С другой
Ответы к упражнениям 345 стороны, если р ложно, то таково же и р/\ ~]qy так что истинно pA~]q=>q> и все высказывание истинно. Доказательство 18. Если q истинно, то таково же значение рассматриваемого высказывания p=>q и таково же и само высказывание. Так что предположим, что q ложно, а р истинно. Тогда посылка высказывания сводится к T/\T=F, которое ложно. Поскольку посылка ложна, все высказывание должно быть истинно. Доказательство 19. Если р ложно, то "]р истинно, и заключение высказывания рД ~]q= ~|p истинно. Следовательно, истинно все высказывание. Так что предположим, что р истинно. Если, кроме того, истинно и #, то ложно р/\ ~\q> так что посылка высказывания PAlq=>~]P ложна, и все высказывание истинно. С другой стороны, если q ложно, то ложна посылка рассматриваемого высказывания p=>q, и все высказывание истинно. Доказательство 20. Заключение высказывания истинно, так что все высказывание истинно во всех случаях, кроме тех, когда р=Т% a q=F. Поэтому рассмотрим именно эти случаи. В них посылка PA~]q=>lP сводится к T/\T=>F> которое ложно. Поскольку ложна посылка, все высказывание истинно. Доказательство 21. Если истинно p=qi то р и q имеют одни и те же значения, так что и "]/? с ~\q имеют одни и те же значения, и ~]р= "]q также истинно. С другой стороны, если p=q ложно, то р и q имеют разные значения, так что ~]р с ~~|д также имеют разные значения, и ~]p=~]q тоже ложно. Доказательство 22. Подобно доказательству 21. Доказательство 23. Если "] (p=q) истинно, то р и q имеют различные значения, и ~]p=q также истинно. С другой стороны, если ~] {p=^q} ложно, то p=q истинно, р и q имеют одни и те же значения, и "| р= q также ложно. Доказательство 24. Подобно доказательству 23. Доказательство 25. Высказывание, которое надо доказать, истинна в любом состоянии, в котором q=p. Рассмотрим состояние, в котором q=p ложно. Следовательно, все высказывание сводится к. F=>Fy которое истинно. Ответы к разд. 3.4 1. Из р ^(д ^{р vд)) получить Q лд)^>(р Уд) 1 2 3 p^(g^(pvg)) Из р Ад получить р 2Л~~ 2.2 2.3 2.4 (р*д) р g^ipvg) д рУд >(рЧд) пос 1 м . л-Е, пос 1 =>-Е, 1, 2.1 л-Е, пос 1 =>-Е, 2.2, 2.3 *Ч2
346 Ответы к упражнениям 2, Из (q^rAq)^>r ПОЛУЧИТЬ (q *r)^>(q^>r) I 2 3 (q Лг Лдг)ф-г ПОС 1 ИЗ q л г ПОЛУЧИТЬ q =^r 2.1 2.2 2.3 <7 лг <7 л г пос 1 Из q получить г 2.2.1 2.2.2 q лг Aq Л-I, 2.1, пос 1 г ^-Е, 1,2.2.1, q =>r =>-l, 2.2 ^>(<7=?>Г ) *Ч, 2 3. Получить ((а лЬ)лс)-(с л(д а6)) 1 2 3 4 Из ((а лб)Лс) получить (с л(а лб)) 1.1 1.2 1.3 а л 6 л-Е, пос 1 с Л-Е, пос 1 сЛ(аЛЬ) Л-1, 1.2, 1.1 ((йЛб)Лс)=>(сЛ(йлб)) =>-1, 1 (сЛ(аЛЙ))^>((аЛй)лс) (3.2.6) ((а/ ^)лс) = (сл(влб)) =-1,2,3 4, (а) Получить (fcvQ = (cvfc) 1 2 3 4 Из 6 v с получить 1.1 1.2 1.3 1.4 1.5 , сЧЬ Из /> получить с v^ 1.1.1 | c*b 6 =>('<• vz>) v-l, пос 1 =>-!,1.1 Из с получить с^Ь 1.3.1 | cV/> с^>(с vb) сЧЬ (6Vc)=>(CV6) (с v^)=>(6 vc) (6V< :) = (cvb) •v-l, пос 1 ^■-1, 1.3 v-E, пос 1, 1.2, 1.4 ^-1, 1 (подобно строкам 1-2) =-1, 2, 3 4. (Ь) Получить (h=c) = (c = b) 1 Иэ,л*"г получить с —Ь 1.1 1".2 1.3 (Ь = b ^>с с)-^(с=Ь) (c=b)^(b=c) (6= с) = (<=£) =-Е, пос 1 =-Е, пос 1 =-1, 1.2, 1.1 =>-1, 1 (подобно строкам 1-2} =-1, 2, 3
Ответы к упражнениям 34? 5.(а) Из b^(cvd) получить (b*c)v(b лd) 1 2 3 4 5 6 6 А-Е, ПОС 1 с Vd А-Е, пос 1 Из с получить (Ь лс)У(Ь лd) 3.1 3.2 6 Af Л-1, 1, ПОС 1 (b*c)v(b*d) v-i, 3.1 С=>(6 Af)V(iArf) =^-1, 3 ' d=>(b *c)v(b *d) (подобно строкам 3-4) (6 АС )V(iArf) v-E,2t4.5 5.(Ъ) Из (5 лс)У(й л J) попучить 3 л (с V) 1 2 3 4 Из Ъ л с получить 1.1 1.2 1.3 1.4 ft ^ cVd bA(cVd) (fcAc)=#>£A(cV (ЬЛ(1)>>ЬЦс ^ [£Л(< :Vif) <0 'rf) bh(cVd) л-Е, пос 1 Л-Е, noc 1 v-l, 1.2 Л-I, l.l, 1.3 54,1 (подобно строкам 1~2) V-E, noc 1,2,3 5.(c) Получить b b(cVd)=(b Ac)v(b *d) l 2 M Ь A(fV(/)»(J AC)V(ftA(/) ■ib*c)4(b*d)^>b*(cVd) ft A(C *</)=(& Af)V(ftArf) =>-I, 5(a) =>-I, 5(b) =-1, 1, 2 б.(а) Из i(iVf) получить чЬл^с -i(£ Vc) ПОС 1 ИЗ 6 ПОЛУЧИТЬ (6 Ус) Л i(6 Ус) 2.1 6 Ус 2.2 I (/? Vc)An(6Vc) -.6 тС v-l, пос 1 A-I, 2.1, 1 п-1,2 (подобно строкам 2-3} л-1, 3, 4
348 Ответы к упражнениям 6.(Ь) Из лЬ ^лс получить n(/>vc>) 1 2 3 4 ib л-Е, пос 1 п с л-Е, пос 1 Из Ъ vc получить сл,с 3.1 3.2 1(6 V с (3.4.6). пос 1,1 сл,с л-1, 3.1,2 с) 1-1,3 (.(с) ПОЛУЧИТЬ i(AVc)=njAif i(2>vc)=i6,Aic =^-1, 6(a) >-I, 6(b) =-I. 1.2 7. ПОЛУЧИТЬ -,(b At 5) 1 ИЗ fr Л -, fr ПОЛУЧИТЬ fr Л т fr 1 1.1 I bA*,b л(ЬА-,Ь) пос 1 -1,1 8.(a) Из ЪЧс получить -,b^c Ъ v с пос 1 Из ift получить с 2.1 | с (3.4.6), 1,пос1 8.(Ь) Из ib =^c получить frVg 1 2 3 4 5 6 7 -»Z> ^>С fe v-,6 лос 1 (3.4.14) Из 6 получить 6 Ус 3.1 | Z>vc fo ^>ЪЧс v-I, пос 1 ^-1,3 Из ift получить 6 Ус 5.1 5.2 с bvc л/? =>£VC b Ус - =>-Е, 1,пос 1 v-I, 5.1 =£4,5 V-E, 2, 4, 6
Ответы к упражнениям Я.(с) Получить fcVg=(ife=>g) 1 2 3 (6vc)»(,6 ФС) (ib=>c)=>6vc &vc=(1b=$>c) ^-1, 8(a) Ф-I, 8(b) =-I, 1, 2 349 9. Получить (6 = c) = (6 ^>с)л(с =£>£) 1 2 3 4 5 Из fc =г получить (b =>с)л(с =t>6) 1.1 1.2 1.3 Ь =>с =-Е, пос 1 с =>6 =-Е, пос 1 (Ь^>с)л(с=>Ь) Л-1Е, 1.1, 1.2 (£>=$>c)=>(6=>c)A(c=>fc) Ф-I, 1 Из ( 3.1 3.2 3.3 Ъ =>с)л(с=?>6) получить 6=с 6 =>С Л-Е, ПОС 1 с ^>Ь л-Е, пос 1 Ь=с =-1,3.1,3.2 (Ь^>с)Цс^Ь)>(Ь~с) =*>-!. 4 |(6 = с) = (6=>с)л(с=>6) =-Е, 2,4 10. Теорема доказывается индукцией по структуре выражения Е{р). Случай 1. Е(р) — сам идентификатор р. В данном случае Е(е1) это просто el, а Е(е2) — просто е2у и мы имеем доказательство Из el—e2, el получить е2 1 2 el^>e2 е2 —Е, пос 1 ^-Е, 3, пос 2 Случай 2. Е(р)—идентификатор, отличный от /?, скажем v. В данном случае и Е(е1)у и £(е2) есть vy и теорема выполняется тривиально. Случай 3. Е(р) имеет вид ~]G(p) для некоторого выражения G. По предположению индукции можно допустить, что существует доказательство Из e2 = el, G (е2) получить G (el) и доказать нужный нам результат следующим образом:
350 Ответы к упражнениям Из eI-e29iG(el) ПОЛУЧИТЬ чв(е2) 1 2 3 4 el = e2 пос 1 -. G(el) пос 2 Из G(e2) получить G(el)A-,G(el) 3.1 3.2 3.3 3.4 3.5 •xG(e e2=el =^-Е, упр. 25 из 3.3, 1 (e2 = el)^G(e2)^>G(el) =>-1, предп. индукции k (rf = el)AG(e2) Л-I, 3.1, пос 1 G(e7) ^-Е, 3.2, 3.3 G(el)*iC(el) Л-I, 3.4,2 2) n-I,3 Случай 4. Е (р) имеет вид G(p)/\H(p) при некоторых G и Я. В данном случае по предположению индукции существуют доказательства Из el=e2y G (el) получить G (е2) Из el~e2, H (el) получить Н (е2) Тогда можно дать следующее доказательство: Из el=e2, G(el)*H(el) попучить G(e2)^H(e2) 1 2 3 4 5 G(el) G(e2) H(el) H(e2) G(e2)AH(e2) л-Е, пос 2 Предп. индукции, пос 1,1 л-Е, пос 2 Предп. индукции, пос 1,3 л-1, 2, 4 Оставшиеся случаи, когда Е(р) имеет вид G(p)=>H(p), G(p)\/H(p)r G(p)=H(p)y предлагается рассмотреть самостоятельно (упр. 15—17). 11, Из а —Ъ,Ъ—с попучить а 1 2 3 4 5 6 а^>Ь =-Е, пос 1 b ^>c =-Е, пос 2 Из а получить с 3.1 3.2 с =>а а —с b с =>-Е, 1, пос 1 ==>-Е, 2,3.1 ^-1,3 (подобно строкам Д-4) =-1, 4, 5 12. Из р чд, ->д попучить р (Р vqr) 9vp ^(д'-'Р) =-Е, упр. 4 =^-Е, 1, пос 1 (3.4.6), 2, пос 2
Ответы к упражнениям 351 Ответы к разд. 3.5 1. Высказывание 4 можно формально записать, как bl/\ ~] gy'=> ~| lb. Оно не всегда справедливо, как можно видеть из рассмотрения состояния, в котором tb=T, та=Т, Ы=Т, gh=F, fd=T, gj=F. Высказывание 5 можно записать, как ~] ma=>* ^gh/\ ~]gj. Оно также не тождественно истинно, как можно видеть из состояния tb=F, ma=Fy Ы=Т, gh=T, fd=T, gj=T. Высказывание 6 может быть записано в виде bl\/ma=>fd. Оно несправедливо, как можно видеть из состояния tb=F, tna=T, bl=Tf gh=Tt fd=F, gj=T. Высказывание 7 может быть записано, как gj=>~\td\/ ~\gh. Оно несправедливо, как можно видеть из состояния tb=F, ma=F9 bl=F, gh=T, fd=T, gj=T. Высказывание 8 можно записать как gh/\tb=>(bl=>~\fd). Оно доказывается следующим образом: Из gh,tb получить bl^ifd 1 2 3 4 5 б ■ntb •\blVma Tigh imav ifd подст, отрицание, пос 2 (3.4.6), посылка 1,1 подст, отрицание, пос 1 (3.4.6), посылка 2,3 Из Ы попучить ifd 5.1 5.2 5.3 5.4 Ы*> life/ ma ллтпа •>fd ->fd подст, отрицание, пос 1 (3.4.6), 2, 5.1 подст, отрицание, 5.2 (3.4.6), 4, 5.3 =5>-1.5 2. Для доказательства верных предположений задачи об опаздывающем автобусе при помощи системы эквивалентных преобразований из гл. 2 запишем сначала дизъюнктивные нормальные формы посылок. Посылка 1: "] tb V 1 bl V пга Посылка 2: "]maV ~| fd V ~\gh Посылка 3: gj V (fd Л 1 gh) Предположение 2, которое можно записать в виде (tna/\gh)=>gjf доказывается следующим образом. Сначала воспользуемся законами отрицания и де Моргана для перевода предположения в дизъюнктивную нормальную формуз (E.l)-]maV-]ghVgl Мы докажем, что (Е.1) истинно, показав, что по крайней мере один из его дизъюнктивных членов истинен. Предположим, что два первых члена ложны: ma=T, gh=T. В этом случае посылка 2 сводится
352 Ответы к упражнениям к ~\fdt и мы заключаем, что fd=F. Но тогда посылка 3 сводится к gj. Поскольку посылка 3 истинна, gj истинно, так что и (ЕЛ) истинно. Предположение 8 можно записать, как ghf\tb=$>(bl=> ~]fd). Пользуясь законами импликации и де Моргана, переводим его в дизъюнктивную нормальную форму: (Е.2) IgfcVltfVlWVl/d Предположим, что ложны последние три дизъюнктивных члена: tb = T,bl = T, fd = Т. Из посылки 1 следует, что та должно быть истинно; следовательно, посылка 2превращается в ~]Т\/ ~]T\J ~\ghf которое сводится к "]g/i, что означает, что ~]gh истинно, и таково же и все (Е.2). Следовательно, (Е.2) всегда истинно. Ответы к разд. 4.1 1. (а) 7, (Ь) 7\ (с) F9 (d) 7\ (е) 7\ (f) F (floor(-x/y)-vro -4), (g) F, (h) Г, (i) 1, (j) 7\ (k) 7\ (1) F, (m) 7\ (n) 1. 2. (a) {1, 2, 3, 4, 6}, (b) {2, 4}, (c) F, (d) F, (e) T (пустое множество является подмножеством любого множества), (f) T{{i\i£mAeven(i)}-{2, Ц), (g) F, (h) T, (i) F, (j) F, (k) 1, (1) {2, 4}. 3. (a) U, (b) T, (c) U, (d) t/, (e) U, (f) F, (g) T, (h) 7\ (/) /\ (j) T. 4. acorfr в отличие от bcora в тех состояниях, где {а = £/, Ь = Т) или (а = 7\ b = U). Во всех других состояниях они совпадают. #candfr отлично от 6candа в состояниях, где (a = Uy b = F) или 5. Не станем явно строить таблицы истинности, а вместо этого разберем различные случаи. Ассоциативность. Чтобы показать, что a cor (b core) и (a cor b) cor с эквивалентны, исследуем различные значения а. Предположим, что а = 7\ Тогда вычисление, использующее таблицу истинности для cor, показывает, что оба выражения дают Т. Предположим, что a — F. Тогда вычисление показывает, что оба выражения дают 6 cor с, так что они одинаковы. Предположим, что a = U. Тогда вычисление показывает, что оба выражения дают U. Доказательство другого закона ассоциативности аналогично. Дистрибутивность. Рассмотрим возможные значения а, чтобы показать, что a cor (b cand б) = (acor b) cand (a cor с). Если a = 7\ обе стороны равенства дают b cand с, так что их значения совпадают. Если a = Uy то обе стороны равенства неопределенны, так что они дают одно и то же значение U. Доказательство другого закона дистрибутивности аналогично.
Ответы к упражнениям 353 Закон де Моргана. Следующая таблица истинности показывает, что выполняется второй закон де Моргана (первый доказывается подобным же образом): b с Т Т Т F т и F Т F F F U и т U F и и лЪ F F F Т Т Т и и и тС F Т и F Т и F Т и Ь сог с Т Т Т Т F и и и и ль сог с) F F F F Т и и и и т b cand л с F F F F Т и и и и Исключенное третье. Таблица истинности не включает значения U для £7, поскольку закон имеет место, лишь если Е1 определено: £1 Т F i£I F Т Е1 сог п £ 1 Т Т Т Т Т Противоречие. Таблица истинности не включает значения U для Е1У поскольку закон выполняется, лишь если El определено. £1 Т F •хЕХ F т Е1 cand т £ 1 F F F F F Упрощение сог: £1 Т Т Т F F F и £2 Т F и Т F и 9 £1 сог £1 Т F и £1 сог Т Т Т и £1 сог F Т F и £1 cand £2 Т F и F F F и £1 сог (£1 cand £2) Т Т т F F F и 12 Д. Грис
354 Ответы к упражнениям Упрощение cand: Е\ £2 Т Тг Т F т и F Т F F F U и ? £1 cand Е\ Т F и Е\ cand Т Т F и Е\ cand F F F и Е\ cor _Е1_ Т Т Т Т F и и Е\ cand (El cor E2) Т Т т F F F и Ответы к разд. 4.2 1. Единичным элементом конкатенации является пустая строка е—строка, содержащая нуль символов, поскольку для всех строк* имеет место х\г = х. 2. (yi:m^i < m:Е{) = Т, при £>0 (yi:m<i <m + k + l:E;) = (yi:m^i<m + k:E;) AEm+k 3. (Nr.m</<m:£,-) = 0, при £>0 (Nt:m<*<m + *+l:£,) = (Ni:m^i < m + k:E;) + (ii Em+k then 1 eiseO) 4. (Nr.O^t <«:* = &[/]) = (№:0<t <m:x = c[t']) 5. (y/e:0<£ <n:(Ni:0<i < л :6 [*] = &[*]) = (N/:0</<n:6[*]-cpJ)) 6. (a) (v*:/<i<* + l:b[t]«°) (Ь)П(н»"-/<»<* + 1:&И —0) ил" (V':/<«*<^ + b6[t]^0) (c) «Некоторые» означает «по меньшей мере один». (ЗГ./<i<*+1 :b[i] = 0) или, что лучше, (Nt:/' < i<k+l :&[t']=0)>0 (d) (0</<n cand &[i] = 0) =?>/<i'<£ или (vt:0<t'<«:6[t] = 0=* / < i < *) (e) (Ni:/<f<* + l:6[t]-0)>l (f) (vt:0<i<«:(Np:/<p<* + l:6[/] = 6[p])=0 =>(НР:/<Р<Н1#]-1'И) (g) l(V':0<i <«:b[t] = ° =>/<*<*) или (3/:0< i< j:b[i] Ф0)\у (^i:k+ 1 < / <п:Ь[(\ф0) (h) (3t :0 < i < л :fc [i] = 0) => (31: / < t < k + 1 :b [i] = 0) (i) (№:/ < i < k + 1 :b[f'] = 0) > 2=» / = 1 (j) (Hr. 1 < i < / +1 :b [/] = 0) V (H» :/<'<*+ 1:&[i] = 0) (k) (v«:/<f<A^[i]<6[/ + l]) (1) (З'--/<*'<* + 1:х = &}1'|)=Ф(зг.£+1 <t< л:л-+1 =';[»]) (m) (№: / < 1 < к + 1 :b \i] = 0) > 2 (n) (Vi:/ < i < k + 1 :(3/>:* + 1 <p < п:ОД =»&[/>]))
Ответы к упражнениям 355 (о) (Зг: / < i < k + 1: / =b [г]) =» (3i:0 < i:j = 2') (Что здесь неверно?) (р) (v«:0< t < /+ 1:6[f] < х) A (Vi-j + 1 < i <n:b[i) > *) (q) ((6[l] = 3V(6[2] = 4A6[3]=-5))=>/ = 3 Ответы к разд. 4.3 л г 1. (а) (ЗА-: О^А <п : Р л ЯН Г)) л А: >0 (неверно) (b) (V j: 0s£/ <п : в) =>ир (Slj , R)) (c) (3y:0^/<n:(Vi:0<i</ + l:/(i)</(/ + 1))) ^J I I ! л г (d)(Vj:O^J<n:B}Vc})A <П 1 1 Л I I (\ k:0^k<n:Bl =>(3*:0O <п : С])) (e) (Vj:0sSy <n:&\:j+\<\ <m: I t-1 L- iVHO^iFft,,))) Ответы к разд. 4.4 1. E)—E (i не свободно в Е) £2+,=(V':0<t<n + l:&[i]<&[i + l])=«(v»:0 < i < л :& [i] < 6 [i + 1]) Л 6[n] < b [n + 1] ~EAb[n]<b[n + l] Ebc = (Vi:0 < i < n:c[t] < с [i+ 1]) ЯЯ+, требует замены связанного идентификатора: = (yk:0z^ k < п + i:b[k] < b[k + I]) Еь+i незаконно (6+1[«] синтаксически недопустимо) ЕпЛ =(v*:0< i < m:k[i] < k[i+ 1]) 2. E'j неправильно (оно допускает две интерпретации /'): Е)=п > / Л (Nj: 1 < / < п:п + j = 0) > 1 E"m+i = m + i>i A (N/:l</<m + f:(m + /)^/ = 0)> I EJi+i = E (j не свободна в Е) El = n>kA(Nj:l^j<n:n + j = 0)>l (Enn+iy< = n+t>tA(Nj:l^j<n + t:(n + l)+j = 0)>l Е'кЛ ~« +» > t A (Nj: 1 < / < л+ *:(л +i) -r / = 0) > 1 3. E) = E (поскольку I не свободно в Е) £g = <v<:l<<<MH/^[/] = Q) £3.< = (V* •• 1 < * < л«: (Я/:Ь [j] = A;)) П*
356 Ответы к упражнениям 4. Предусловие — это х-\- 1 > 0; его можно записать в виде /?*+i (ср. с оператором присваивания х := х+1). 5. Предусловием является (а*&)*6 = с, которое можно записать в виде R%b (ср. с оператором присваивания а := а*Ь). 6. (а) Е1е = Е для выражений Т, F и id, где id—идентификатор, не совпадающий с i. (b) Ele = e для выражения, состоящего из идентификатора i. (c) (£){=(£& (d) (-]£)<=-!(£<) (e) (£1 Л^2)|=:(£1*Л52й (так же и для =>, = , V) (f) (у*" im^i^niEii = (у* :m^.i^n:E) Идентификатор / не есть i: (yj-m^l \ ^n:E)le = = (yj*tnle^ij ^nle:Ei) (подобным же образом для з и Н). Ответы к разд. 4.5 1. (а) з коммутативно, так же как и у. Это выражение можно записать, как {^t\(^p:fool(p, /))), или как (з/?:(Н^:/00' (Р> 0))> или как (зр, t:fool(py /)). (b) (yp-fatifoolip, /))). Это означает, что для любого человека найдется время, когда его можно обмануть, но для разных людей времена, когда их можно обмануть, различны. На самом деле предикат следовало бы читать так: для каждого человека есть время, когда его можно обмануть. Заметим, что предикат (^t\(^p:fool(pt t))) совершенно другой: он утверждает, что есть одно и то же время t (один и тот же момент), когда можно обмануть всех людей. В общем случае у и Н не переставляются друг с другом. (c) 1(VP> t:fool(p, t)) эквивалентно (зр, /:~]/оо/(/?, t)) 2. (a) (yi:integer(i):i2^0) (b) (уя> b, ciinteger {a, b, с) стороны (а, Ь, с) = =*a + b^c /\a + o>b Ab + c^za) (c) (\т:0 < nifaw, *» *Л z:0<w, х, 0, z:wn + xn + yn = zn)) (d) (2j**:1 ^* < п Л nmodi'eO'.tje/i Ответы к разд. 4.6 1. (а) х —6, 0 = 6, 6 = Г (е) х = 5, // = 6, 6 = Г (b) jc-5, i/^5, 6=Г (f) х = 6, {/ = 4, Ь = Г (c) х-5, 0 = 11, b = T (g) х = 6, 0 = 6, 6=7 (d) a: = 5, 0 = 6, b = F Ответы к разд. 5.1 1. (а) (3, 4, 6, 8), (Ь) (2, 4, 6, 8), (с) (8, 4, 6, 8), (d) (8, 6, 4, 2), (e) (2, 4, 6, 8), ([) (8, 4, 6, 8).
Ответы к упражнениям 357 2. (а) О, (Ь) 2, (с) О, (d) 6, (е) 1, (f) 3, (g) l, (h) 3. 3. (a) (i = /A5-5)V(*#/A5-6[/])-(i = /)V&[/] = 5. (b) b[i] = i. (c) (i= i A (b{j\ = b[i])) V (1Ф j Л (b\j] = b[i])) = (b[j] = b[i]). (d) (i = iAb [i] = b [/]) V ЦФ i A b [/J= b [if) = (6 [i] - b [/]). (e) (i = / Л & [/]=& [/]) V (/ ¥= / Л b[}]=b [/]) - (' = /) V (» ^ /) = T. 0) (« = / Л &[/]=&[/]) VO:#/Л& [/]=&[/])=(*=/V*#/)=7\ 4. Пусть л обозначает имя поля записи о, тип которого t, и е— любое выражение типа /. Пусть s—другое имя поля. Пусть = означает синтаксическое равенство. Тогда значение записи (и; г:е) определяется следующим образом: (v; r'.e).s J' — s)- t).S Ответы к разд. 5.2 1. (К упражнениям, в которых нет сокращений, ответы не даны.) (a) b\J:k] = 0. (b) 6[/:*]*0. (c) Ое*[/:*]. , (d) Of Л[0:у —1] л Of 6[Л-+1:п—1]. (e) 06Л[/:А-]. (О (Vv:veA[0:n-l]:vfi[/:ifc]=^v€6[/:A]). (g) 0'=6[0:y-l]v0e6[* + l:n-l]. (h) 0еЬ[0:я-1]=*»0б6[/:Л]. 0) 0*b[\:j]voeb\j:kl (1) .ve6[/:*]=>.v + iefc[jfc + l:n-l]. (n) (Vv:vfi/)[/:)t]»v66[Hl:n-l]). (o) j<=b\j:k]*>(3i:0Ki:j=2). (P) 6[0:/]<jf<6[/ + l:n-l]. 0 p q n— 1 2.(а)0^р^</ + 1<лЛ & | sg.y (Ь) 1а-к/<и<и аь «=.v 6 >x 1 A: / h 1 2 n 3 ... 3. (a)O^A:*£/;</! Л 6[0:*-1]<х a .v =b[k]ux=b[h]Ax<b[h + l:n]. (b) 0</<яА or</mtf(6[0:/-1]).
358 Ответы к упражнениям Ответы к разд. 5.3 1. Пусть Ь обозначает произвольный массив, a v — произвольную запись (и то и другое рассматриваются как функции). Пусть е и g — выражения подходящих типов. Пусть г и t — имена полей в записи v. Пусть s — селектор, т. е. последовательность выражения в квадратных скобках (индексов) и имен полей, и 8 — пустой селектор. Пусть = обозначает синтаксическое равенство. Тогда функции (b\ s : е) и (v\ v : g) определяются следующим образом: (Ь\ г:е) ~е г., , / i¥=j—+b[j] (v; ros:g)t=\r^t_'{vt. s.g) Ответы к разд. 6.2 1. (а) Первая спецификация: {п > 0} S {x= max ({у \ у g Ь [0:п — \]})\ Вторая спецификация: для данных фиксированного п > 0 и фиксированного массива Ь[0:п—1] установить R:x=*max({y\y£b\) Для построения программы может оказаться полезной замена max на его смысл. Тогда результирующим утверждением R будет #:(gr.0<i</i:*==6[i]) Л (у':0< * < n:b[i\^x) (b) Первая спецификация: \х = Х\ S \x = abs(X)\ Вторая спецификация: установить при данной целой переменной х с начальным значением X предикат R:x = abs(X). (c) Первая спецификация: \п>0\ S \0^i<nA(yj'.0<j<n:b[i]^b[j])\ Вторая спецификация: при данном фиксированном п > 0 и фиксированном массиве Ь[0:п—1] установить i таким, чтобы выполнялось /?:0<i</iA(v/s0</<^:6W>&[/]) (d) Первая спецификация: \п>0\ S {0^i<nAb[i]^b[0:n-l]Ab[i]>b[Q:i-l]} Вторая спецификация: при фиксированном п > 0 и фиксированном массиве Ь[0:п—1] установить i таким, чтобы /?:0<t <п ДЬ[0:п—lj<&[i] A6[i]> Ь[0:8—1] (e) Определим простое (п): простое (п) »л > 1 Д (у/(: 1 ^i; < пхптой i Ф0)
Ответы к упражнениям 359 Первая спецификация: {я > 1} 5 \ans = простое (п)\ Вторая спецификация: при данном фиксированном п > 1 установить ans таким образом, чтобы обеспечить R:ans~ простое (п). (f) Первая спецификация: \п^0\ S {s =*/„}. Вторая спецификация: при данном фиксированном п^О установить значение s равным я-му числу Фибоначчи, т. е. обеспечить истинность R:s=fn. (g) Первая спецификация: {Т\ S \s = (Yi-A<i<n:b[i— 1]<Ь[(|)} Вторая спецификация: при данном целом числе п и фиксированном массиве Ь[0:п— 1] записать в s признак того, упорядочен ли Ьу т. е. установить /?:s = (vr.l<i</i:6[t — l]<6[i]) (h) Первая спецификация: {п^О Ab[0:n— 1]= 5[0:я — I]} 5 {b = (%i:0^i <n:B[i])} Вторая спецификация: при данных фиксированном целом п и массиве Ь[0:п—1] с начальным значением В придать каждому элементу b значение, равное сумме всех элементов В, т. е. установить #:&[0:л—1] = (2*:0<*<л:Я[*]) (i) Пусть U и // обозначают наименьшие индексы в массивах с n w, такие, что c[li] = w[lj]. Следующее утверждение служит их определением: О < U < п Л 0 < // < т Л c[li] = w [//] Л (N/, /:0<f</i\ 0</<//:c[/]«c[/])«l Первая спецификация: {Т} S {/? = # /\q = lj\. Вторая спецификация: для данных фиксированных массивов с[0:я— i] и ш[0:/п—I] записать в целые переменные р и q наименьшие целые числа, такие, что с[/?] = оу[<7], установив таким образом R:p=*li Л <? = // (j) Определим li, lj, Ik для массивов с, о/, f подобно тому, как это было сделано в упр. 1 (i). Первая спецификация: \Т\ S {/? = /* Л 4 = 4 Л r = lk\. Вторая спецификация: остается читателю. (к) Определим уровень (i) = (2]/:0 ^ )'< 4: оценки [/, /]). Первая спецификация: \п > 0} 5 {0</^дД (V/: 0 ^ / < п: уровень (0^ уровень (/))}.
360 Ответы к упрао/снениям Ответы к гл. 7 1. (а) /+1>0, или *>0; (Ь) / + 2+/ — 2 = 0, или * + / = 0; (с) (/+ ),*(/—1) = 0; (d) г*/*/'""1 =с или г*/'=б; (е) /=i V ^[/]=I; (f) a[i\ = i. 2. Пример 1: {i<0} f :=--= i + 1{i<1} 2: \T\ if x^r/ then г := x else z := у {г = тах (*, у)} 3: ]г/^л:} if л:^/у (hen г := х else г := //{г = г/} 4: {F} if х^г/ then г := х else г := y\z — y—1} 5: \х = у+\\ \\х ^ у then z i= x else г : = у {2-У+Ч 3. Предположим, что Q=>/?. Тогда Q /\ R = Q. Следовательно, wp(S, Q) = ^Р (5, Q A R) (поскольку Q Л R = R) = wp(Sy Q)Awp(Sy R) (из (7.4)) =>^(S, /?) Эти рассуждения доказывают (7.5). Для доказательства (7.6) заметим, что Q => Q V R- Из этого вместе с (7.5) следует до/? (5, Q)=> и>Р (S, QVR). Подобным же образом wp(S, R)=$>wp(S, Q \J R). Из всего этого немедленно следует (7.6). 4. wp(S, R)Awp(S, ~]R) = wp(S, RA1R) (из (7.4)) = wp (S, F) (противоречие) = F (из (7.3)) 5. Пусть S—действие, заключающееся в подбрасывании монеты. Обозначим предикат R: «монета выпала гербом вверх» через герб, тогда ~]R — это решетка. Как wp(S, R), так и wp(S, "]R) есть/7, так что их дизъюнкция есть F. Но wp(S, /? V ~]R) есть Т. В качестве второго примера пусть S — цикл «while t=^=0 do i := i— 1», и пусть /? — это Г. Тогда ayp(S, /?) представляет множество всех состояний, в которых цикл закончится: j^O. Но wp{Sy "| R)~wp(Sy 1 Г) = ш/?(5, F) = F. Следовательно, wp(S, R) \/wp(S, 1R)=z(i^0), что не совпадает с 7. 6. Мы видим из (7.6), что левая часть (7.7) влечет его правую часть. Следовательно, остается показать, что правая часть (7.7) влечет его левую часть. Чтобы это доказать, необходимо показать, что любое состояние s из wp(S> Q\/R) обязательно находится либо в wp (S, Q), либо в wp(S, R). Рассмотрим состояние s, находящееся в wp(S, Q\/R). Поскольку 5 детерминировано, выполнение 5, начинающееся в состоянии s, обязательно завершится в некотором единственно возможном состоянии s', находящемся в Q\/R. Это
Ответы к упражнениям 361 единственное состояние s' должно лежать либо в Q (и тогда s лежит в wp(Sy Q)), либо в R (и тогда s лежит в wp(S> /?)). 7. Это упражнение предназначено для того, чтобы помочь читателю осмыслить то, как кванторы взаимодействуют с wp, и показать необходимость правила, согласно которому в предикате каждый идентификатор может быть использован единственным способом. Предположим, что в любом состоянии истинно Q=S>wp(S, R). Это предположение эквивалентно (Е7.1) (yx:Q=>wp(S, R)) Нам нужно проанализировать предикат (7.8): \(yx:Q)\ S\yx:R)\, который эквивалентен (Е7.2) (Vx: Q)=*wp(S, (yxiR)) Сначала проанализируем этот предикат с учетом правила, что в предикате ни один идентификатор не может быть использован разными способами. Следовательно, (Е7.2) переписываем в виде (Е7.3) (yx:Q)=$wp(S, (уг:/?Э) предполагая, что х не встречается в S, а г — новый идентификатор. Обоснуем при помощи анализа действий, что (Е7.3) истинно. Предположим, что в некотором состоянии s истинна посылка (Е7.3), а выполнение S, начавшееся в s, завершается в состоянии s'. Поскольку 5 не содержит идентификатора х, имеем s(x)=s'(x). Поскольку в состоянии s истинна посылка (Е7.3), заключаем, исходя из (Е7.1), что (ух \ wp(S, R)) также истинна в состоянии s. Следовательно, независимо от значения х в s' истинно s'(R). Но s(x)=s'(x). Таким образом, независимо от того, каково значение х в s', s'(R) истинно. Следовательно, таково же и s'((yx : /?)), и s'((v2 : R%)). Таким образом, в состоянии s истинно заключение (Е7.3), и выполняется (Е7.3). Теперь приведем контрпример, показывающий, что если в команде 5 происходит присваивание переменной х и х встречается в /?, то (Е7.2) не обязательно имеет место. Возьмем команду S ■ х : = 1. Возьмем R : х=\ и Q \ Т. Тогда (Е7.1) это (Vx:T=>wp("x =1", х=1)) которое истинно. Но в данном случае (Е7.2) ложно: его посылка (ух:Т) истинна, его заключение wp^x : = 1", (ух:х= 1)) ложно, поскольку предикат (ул::х=1) есть F. Отсюда заключаем, что если х встречается как в S, так и в У?, то (Е7.2) в общем случае не следует из (Е7.1). 8. В этой задаче опять иллюстрируются сложности, возникающие, если идентификатор может использоваться в предикате более чем одним способом, если только не рассматривать такие разные ис-
362 Ответы к упражнениям пользования как совершенно различные идентификаторы независимо от их одного и того же имени. Предположим, что такое использование допустимо. Тогда, если х свободно входит в 5 (даже если оно и не входит в R), наш предикат может быть не всегда истинен. Например, {х=\} у := х {у=1} истинно, но {(^х : х=1)} У :== х {(Н* : ^==1)}» эквивалентный {Т} у :== х {у=1}, ложен. Однако если хне встречается в S, то наш предикат тождественно истинен. Покажем это следующим образом. Еслих несвободен в 5, то значение х не может влиять на выполнение 5. Следовательно, выполнение S, начинающееся в состоянии s, завершается в состоянии s', если и только если выполнение 5, начинающееся в (s; х : v), завершается в (s'; x : v) при произвольном v. Теперь предположим, что посылка предиката (дх : Q)=>wp (S, (gx : R)) истинна в состоянии s. Покажем, что тогда должно быть также истинно и заключение. Существует такое v, что Q истинно в состоянии (s; x : v), т. е. посылка истинна в состоянии s. Данное нам предположение Q^wp(Sy R) позволяет заключить, что выполнение S, начавшееся в состоянии (s; x : v), завершается при истинном R в некотором состоянии (s'; x : v). Следовательно, выполнение 5, начинающееся в s, завершается в состоянии s'. Так как истинно (s'; х ■ v) (R), то в состоянии s' истинно заключение (gx : R). Ответы к гл. 8 1. По определению wp(skip, F) = F, так что выполняется (7.3). Чтобы доказать (7.4), заметим, что (wp(skip, Q) Л wp (skipy R)) = = (Q/\R) nwp(skip, Q Л R) = (QAR)' Подобным же образом имеет место и закон (7.7). 2. Поскольку wp (abort, R) = F для любого предиката Ry имеем wp (abort, F) = F, и выполняется (7.3). Чтобы доказать (7.4) и (7.7), достаточно просто заметить, что wp(abort, Q), wp (abort, R), wp (abort, Q Л R) и wp (abort, Q V R) все имеют значение F. 3. По определению wp(cdeAamb-ucmuHHbiM, F) = T, что нарушает закон исключительного чуда (7.3). 4. wp("Sl; 52", F) = wp(Sl, wp(S2, F)) (по определению) = wp(Sl, F) (52 удовлетворяет (7.3)) = F (SI удовлетворяет (7.3)) Следовательно, композиция тоже удовлетворяет (7.3). Чтобы показать, что она удовлетворяет (7.4), напишем = wp(uSU S2», Q)Awp("Sl; 52", R) = wp(S\, wp(S2, Q))Awp(Sl, wp(S2, R)) (по определению) ~wp{Sl, wp(S2, Q)Awp(S2, R))
Ответы к упражнениям 363 (51 удовлетворяет (7.4)) = wp(Sl, wp(S2y QAR)) (52 удовлетворяет (7.4)) = wp(uSl; 52", QAR) (no определению) 5. wp("S\; 52", Q)Vwp("Sl; 52", R) = wp(Sly wp(S2y Q))\Jwp{Sl, wp(S2y R)) (по определению) = wp(Sl, wp(S2y Q)\/wp(S2y R)) (51 удовлетворяет (7.7)) = wp(Sl, wp(S2y Q V R)) (S2 удовлетворяет (7.7)) = a>p("Sl; 52", Q V R) (по определению) 6. wp("x :=e; abort", R) = wp("x =e"9 wp(aborty R)) (по определению) = wp("x :== e"y F) (определение abort) = F (закон исключенного чуда) Ответы к разд. 9.1 1. (а) (2*# + 3)= 13 или у = 5 (b) х-\-у <2*у или х<у (c) 0</+lA(v^-°<^/ + 1=6[f] = 5) (d) (b[/] = 5)==(Vr.0<i</:b[/] = 5) или (V*':0<t</:&[t] = 5) (e) (aM5Ab[/] = 5) = (vt:0<t</:6[t] = 5) или a//5 = (VnO<f </:6[f| = 5) V&[/]^=5 (f) х*у2 = с (g) (х—У)*(х + у) + У*Ф®> или *2=И=0, или хфО. 2. wp("x := p", F) = FeK~F. Следовательно, удовлетворяется (7.3). Для доказательства (7.4) рассмотрим a/p(u* := e\ Q) A wp("x := e", /?) = Qe Л Re (по определению) = (Q Л Я)? (упр. (4.4.6)) = wp("x := е", Q A R) (по определению) Доказательство того, что выполняется (7.7), аналогично. 3. При выполнении х := е в состоянии s вычисляем значение е, получая s(e), и записываем это значение в качестве нового значения х. Это—обычная модель выполнения присваивания. Следовательно, в конечном состоянии s' имеем s' = (s\ x: s(e)). Мы хотим показать, что s' (R) = s (Re). Но это есть просто лемма (4.6.2). Значит, если R должно быть истинным (ложным) после присваивания (т. е. в состоянии s'), то перед присваиванием (т. е. в состоянии s) должно быть истинно Re. А это в точности то, что и говорится в определении (9.1).
364 Ответы к упраоюнениям 4. Записывая как Q, так и е в виде функций от х> имеем wp("x := *(*)", s/7 (<?(*), "* := *(*)")) = шр("* := е(х)\ (3v:Q(v)A*=e(v))) (Е4.1) =(^:QWA^W = ^W) Последняя строка следует из предыдущих, поскольку ни Q (v), ни e(v) не содержат ссылок на х. А теперь предположим, что Q истинно в некотором состоянии s. Пусть i» = s(a:), т.е. значение х в состоянии s. Для этого и в состоянии s истинно (Q {v) Л e(*) = e(f)), так что (Е4.1) также истинно в состоянии s. Следовательно, Q=> (E4.1), а это и есть то, что нужно было доказать. 5. sp{x=\, "х:=2") = (^:у = 2л^ = и)=(х + 2). Но wp ("х := 2й, x = 2) = (2 = 2) = r. 6. Определение дается через задание области определения исходных множителей трех видов и операций -t-, *, -f-. Область определения будем обозначать через d. Наше определение d само не является вычислимым во всех состояниях, т. е. вычисление d(E) само может в некоторых состояниях вызвать ошибку. Однако можно упорядочить вычисление области определения таким образом, чтобы ошибки не случалось. Идентификатор В обозначает наибольшее из возможных значений целых чисел, а —В — наименьшее. d ((целая константау) = —В ^ (целая константа} ^.В d ((идентификатору) = —В ^ (идентификатору <; В d ((идентификатор массива [(выражениеу]>)~ ((идентификатор массивау.(нижняя граница^(выражениеу^. (идентификатор массивау. верхняя граница cand — б^ (идентификатор массивау [(выражение}]) ^ В d ((слагаемоеу * (множительу) = d ((слагаемоеу) Л d ((множительу) cand — В ^ (слагаемоеу * (множительу ^ В d ((слагаемоеу-2г(множительУ) =d((cлaгaeмoey)/\&((множительУ)/\ (множительу Ф О cand — В ^ (слагаемоеу -f- (множительу ^ В d ((выражениеу + (слагаемоеу) = d ((выражение}) /\ d ((выражениеу) cand — В ^ (слагаемоеу + (выражениеу ^ В Ответы к разд. 9.2 1. Докажем лишь то, что х :— el; у := е2 эквивалентно х> у := el, е2. Запишем произвольное постусловие R в виде функции от х и y:R(x, у)
Ответы к упражнениям 365 wp(ux := el\ у := е2", R (х, у)) = wp("x := еГ, шр("у := еГ, Я (*, у)) = wp("x := еГ\ R{xy у)*л) = wp(ux := еГ, R(x, е2)) = /?(*, e2fel = R (el, e2) (x не свободно в e2) = wp("x, у := e/, e2", R(x, у)) (по определению) 2. ^("x := y; r/ := x\ x = 2 Л у = 2) = (y = 2) wp('% у := у, *", х=2л^ = 2) = (х = 2Л{/ = 2) шр("=х; х := у", х = 2л^/ = 2) = (х = 2) Следовательно, выполнение, начинающееся при у = 2, х= 1, дает разные результаты для первых двух команд, а выполнение при л: = 2, #=1 дает разные результаты для последних двух команд. 3. (а) 1 *cd = cd или Т (b) 1<1<* ЛЬ[0] = (2/:0</<0:6[/]) или 1 < п (c) 02< 1 д(0 + 1)2^ 1 или Г (d) 0<i + l <n A s + b[i] = b[0]+ ... +6[i + l — 1] или 0</</7 —1 As = fe[0]+...+6[i—1] (e) {-|-1 = / + /+1 или / = 0 (f) i -j- 1 = i-f- i или / = 1 (g) *' + 1 = / + * или / = 1 4. В данном случае необходимо вспомнить, что х является функцией идентификаторов, входящих в выражение. Следовательно, если х встречается в выражении, в котором делается подстановка, то при подстановке может измениться также и х. В тех местах, где это происходит, х записывается как функция встречающихся в операторе переменных. См., в частности, упр. (Ь). (a) wp("a4 Ь := я-И, х", Ь = а + 1) = (х = а + 2). Следовательно, берем х — а-\-2. {b)wp("a := a + U Ъ := х(а)\ Ь = я + 1) = ^/?("а := я+1", х(а) = а-{-1) = х(а+\) = а + 2 Это удовлетворяется, если принять х(а) = а+\. Итак, возьмем х = а+ 1. (с) ау?("Ь := х\ а := а+1", 6 = я + 1) = (х = а + 2). Итак, берем х = я + 2. (d)wp("i, j := /+1, а:", / = /) = (/ + i = *). Отсюда видно, что должно быть выполнено / = /=> t-f-1 = #, и берем # = / + 1. (е) шрП := t + 1; / := *(0". ' = /) = < + 1=х(*+1) Это уравнение имеет решение x(i) = i, и выбираем x = i.
366 Ответы к упражнениям (f) wp(«j :== Х] i :e /+1», ; = /) == i -f- 1 = x Следовательно, берем х = / + 1 • (g)wp("z, a := z + £> *", г + а#Ь = £) = г + Ь + **6 = с В предусловии отмечается, что c = z-\-a*b. Подставляя это выражение вместо с, получаем z + b-\-x*b = z-\-a*b Решая получившееся уравнение относительно ху получаем х—а—1. (h) wp("ay b := а/2, х'\ z + a*b = c) = z-{-(a/2)* x = c В предусловии говорится, что а четно и с = z-\-a*b. Подставляя это выражение вместо с, получаем z + (а/2) * x = z + a*b Решая уравнение относительно х (при условии что а четно), получаем л: = 2#6. (i) wp("a : = a/2; b := л;(а)", г + я*^ = £) = wp("a := а/2", г + а*л:(а) = с) = г + я/2*х(а/2) = с В предусловии говорится, что а четно, a c = z-|-a#6. Подставляя это выражение вместо с, получаем z + a/2*x(a/2) = z + a*b а решая уравнение относительно х, находим х(а/2) = 2*Ь, что не зависит от а. Следовательно, берем х = 2*Ь. (j) wp{% s := 0, *", s = (2/:0</<*:&[/])) = x = (S/:0</<0:6[/]) = *=&[0J Так что берем х = Ь[0]. (k) a;p(%s: = 0,*", s = (2/:0</<r.b[/])) = ^ = (2/-°</<°-Н/]) Так что полагаем х = 0. (1) wp(«i, s := i + l, x"fs = (S/:0</<t:6[/])) = x-(S/:0</<f+l:6[/]) (E4.1) =^-(S/:0</<i:ft[/])+ft [i] Так как в предусловии говорится, что i > 0 и s=(2/ :0^/<*:Ь[/])» заменяем в (Е4.1) выражение с квантором 2 на s и получаем л- = s -f b [i].
Ответы к упражнениям 367 Ответы к разд. 9.3 1. Дается и далее упрощается слабейшее предусловие, получаемое для данного выражения подстановкой. (a) ((&; i:i)[(b; i: i) [i]] = i) = ((&; i:i) [i] = i) = (i = i) = T (b) (3/'"</<n:(&; *:5) [*]<(&; »:5)[/J) = (3/:i</<«:5<(6; r.5)[/]) = (H/:'</<«:5<(^; r.5)[/])V5<(fe; r.5)[i] = (3/':K/<«:5<(b; t:5)[/])v5<5 = T (c) (3/:i</<n:(&; t:5)[«] < (b; t:5)[/]) -=(H/:'</<n:5<(b; i:5) = (H/:«</<«:5<(&; t:5) = (H/-.*</<«:5<(6; i:5)" 7])V5<(6; /:5)[i] t/])V5<5 = (g/:«</<«:5 <&[/]) (d) (b, r.5)[0:n—l] = fl[0:« —1] -5 = B[i]A(v/:0<«<n. /#^ft[/J=»fl[/D (e) ((ft; i; 6[i-l]+6[/])[i] = (2/: 1 <l<t:(b; i:b[i-\] + b[i])[j])) = (b[i-l] + b[i] = (2ji\<j<i:b[j])) = (бГ0 = (2/':1</<'-1:&Ш)) (f) ((&; *:&[,]; /:b [i]) fi] = * Л (Ь; »:6[/]; /:*[«'])[/]=»У)) = (6[|] = xAb[i] = </) (g) (кф1ЛЬФ1 ЛФ; i:b[j\, j:b[q)[k] = C) = (k=£i АкФ) /\ЬЩ=С) 2. Определим функцию (г; s: е), где г—запись, s—идентификатор (имя) поля в г, е—выражение, аналогично тому, как было определено (b; i: e) для массива (функции) Ъ: I s=t—+e Здесь s=t обозначает синтаксическое равенство идентификаторов. Тогда определим wp(ur.s = e", R)=wp("r := (r; s:e)w, R) = domain (e) cand R[ns.e) Ответы к разд. 9.4 1. (a) R{b.xi:e. i:fh g (C) R{b.tCi:e;j:g),(c:i:f) (Ь) ^(&;*:r, /:g), / (d) ^(b'i:e; /:g), (c; i:f; /:A) 2. Для каждого выражения дается, а затем упрощается слабейшее предусловие, определяемое подстановкой. (a) (&; f:3; 2:4) [/] = 3 = (/ = 2 Л 4 =3) V (1ф 2 Д 3 = 3) = 1ф2 (b) (&; i:4; 2:4) [f| = 3 = (i = 2 Д 4 = 3) V (*V 2 Д 4^3) = F
368 Ответы к упражнениям (c) {Ь[р] = (Ь; р:р)[Ь[р]]) = (Р = Ь[р] ЛЬ[р] = р) V (рфЬ[р] Л b[p] = b[b[p]]) = P = b[p]V (рфЬ[р]/\Ь[р] = Ь[Ь[р]]) = (p = b[p] У рФ Ь\р]) Л (Р-Ь[р] V b[p] = b[b[p]]) = р = Ь[р]\/Ь[р] = Ь[Ь[р]] (d) 0<i+lA(VJ-0<j<i+U Ф; *:0)[/] = 0) = 0<( л (V/-°</ < ^•• (Ь; i:0[j]=.0)A(b\ i:0)[/]=0 = 0</Л(у/:0</<':*Ш = °) (e) 0<i + l АФ; f:0)[0:i + l-l] = 0 = 0<i + lA&[0:f-l] = 0 (f) Ь[р] = (Ь; p:b[q\, q:p)[q]-b[p] = p (g) b[p] = (b; p:b[b[p]]; b[p]:p)[(b; p:b[b[p]]; b[p]; p)[b[p]]] = b[p] = (b; p:b[b[p]];b[p]:p)[p] = (р = Ь[р]ЛЬ[р]-р)Ч(рфЬ[р]ЛЬ[р]~Ь[Ь[р$> = p = b[p]Vb[p] = b[b[p]l (см. (с)) (h) Ь[р]ф(Ь; p:b[b[p]}; b[p]:p)[(b; p:b[b[p]]; b [p]: p) [b [p]]] = Ь[р]Ф(Ь;р:Ь[Ь[р]\:Ь[р]:р)[р] = (P = b[p] А Ь[р]Фр) V (рфЬ[р] ЛЬ[р]фЬ[Ь[р]]) = РфЬ[р]АЬ[р]фЬ[Ь[р]] 3. Слабейшее предусловие: b[i] = K А Ф\ i-i)[I]. Таким образом, мы должны показать, что / = / Ab[i] = K=*b[i] = KA(b\ *:*)[/] = / Если выполнена посылка, то заключение эквивалентно b U'] = KAl=I> а это в свою очередь b[i]=K, которое истинно, поскольку истинна посылка. 4. Сначала распространим понятие селектора на последовательности индексов (в квадратных скобках) и имен полей (которым предшествуют точки). Например, селектором является U'H/I.sl&l. А теперь определим обозначение (b\ s : е), где b — запись либо массив, s — селектор, е — выражение: (Ь; г:ё) =е (bW°s--e)[l]-\lmmj_{b. s:e) | i {r = t)~+b.t (b\ ros:e).t =\ IU ч В данном определении предполагается, что тип выражения е является подходящим, что массив b применяется лишь к подходящему индексу, что функция b применяется лишь к подходящему имени поля и т. д. Обозначение "r=t" означает синтаксическую эквивалентность идентификаторов ги/. Рассмотрим ххо sXy ...,xBosB :== ei9 ..., еп. Это присваивание сокращенно записывается следующим образом: х о s :== е ^
Ответы к упражнениям 369 Определим где подстановка Rj-QS расширена аналогично тому, как она была расширена в разд. 9.4. 5. Эта лемма уже была доказана для случая, когда х состоит из различных идентификаторов, и нужно рассмотреть лишь случай, когда x = 6oSi, ...,bosn. Чтобы доказать лемму в данном случае, потребуется воспользоваться очевидным утверждением, что (Е5.1) (Ь\ s:bos)=b Вспомнив, что xi=^b:si1 имеем \ ujx \Z~{b\ 4t\ut\ ...; s„:un)Jx = Eb(b; Н:ь с Hi...; sn:b0 ,я> (подставляя xt вместо щ) = Ebb = E (п применений (Е5.1)) Ответы к гл. 10 1. В If fi охраняемых команд нет, так что по определению в шр (if fi, R) имеется конъюнктивный член (gr. 1 ^ i < 0:6,.). Но дизъюнкция нуля выражений — это F. Следовательно, wp (if fi, R) = F и if fi эквивалентно abort. 2. wp (IF, F) = BB A(vi'-l<i<n:Bi=zwp(Si9 F)) = BB Л (v*:1 ^ l ^ n: &i "==> F) (закон исключенного чуда) = (g/:l </<п:В/) Д (V*:I <*'<Я'. ~\Bi) = (Zi:l<^i^:n:Bi)Al(3i-l<i<n-Bi) = F Таким образом, удовлетворяется (7.3). Для доказательства (7.4) напишем: wp (IF, Q)Awp(lFy R) = BBA(W^<i<n:B£^wp(Si,Q))/\(yi:l^i^:B^wp(Si,R)) = BB Д (V<: \<i<n:Bi=>wp(Sl, Q) A wp(Si9 /?)) = BB A(vi:l<i<n:Bi=*wp(Sl, Q A R)) = wp(lFy Q AR) 3. Положив R = q*w + r =x A r >0, имеем wp(S3, R) = (w^r \/ w> r) A (w<^r=>wp("ry q :== r—wy q+l'\ R))/\ A(w> r=$wp(skip, R)) = (w^r=?>((q+ l)*w + r — w = xAr — w^0))A(w<r:=&R) = (w^r=s>q*w-\-r=x)A(w<r=z>r^O)
370 Ответы к упражнениям А это следует из R. 4. wp(S4y a>0hb>0) = (а>Ь\/Ь>а)А(а>Ь=ьа—Ь>ОАЬ>0)Л (Ь>а=$а>0/\Ь—а>0) = а>Ь>0\/Ь>а>0 5. wp(S5> х^у) = (х > У Ух < у)/\(х > у => у < *)Д(* < У => х < у) = Т 6. wp(Sb, R) = (f[i]<g[J]\/f[i] = g[J]Vf[i]>g[j]A (/W <«[/]=>/?{+1)Л(/М = Й/]=»/?)Л(/[0 > ff[/]=* *'/■" 7. Пусть Q и /?—данные пред- и постусловие. Пусть R'=Wp(«y9 х :=2*у,х + 2", R)=y^QAz + 2*y*(x + 2) = a*b Нам нужно показать, что справедливы три утверждения: (1) Q => odd(x) V even(x) (2) QAodd(x)=s>wp("2, x :=-z + y, x—V\ R') (3) QAeven(x)=>wp(skip, R') Поскольку заключение (1) выполняется в любом состоянии, то (1) выполняется. Для (2) вычислим упоминающееся слабейшее предусловие и покажем, что Q Aodd{x) => у > ОДг + у + 2 * у * ((*— 1) ~ 2) = а * Ь Поскольку х нечетно и х—1 четно, это условие сводится к Q Aodd(x) => у > 0Az + У + У * (х— 1) = а* 6 или к QAodd(x)=s>y^OAz + y*x = a*b которое истинно. (3) доказывается легко. 8. Положив, чго Q и R — пред- и постусловие, мы должны доказать, что (1) b[i]>m\/b[i]^m (2) Q Ab[i] >m=> wp("m := b[i]n, R) (3) QAb[i\^fnz=^>wp(skipy R) Предикат (1), очевидно, истинен. Чтобы доказать (2), заменим wp на его определение и получим Q Ab[i] > m => 0 < i < мДВД = max(b[0: i]) которое истинно. Чтобы доказать (3), заменим wp определением и получим Q Дб[/]^т=>/?, которое истинно. Ответы к гл. 11 1. Определением do od является: wp(do od, R) = (^k:0^k:Hk(R)). Из упр. 1 к гл. 10 известно, что if H эквивалентно abort. Воспользовавшись определением Hk(R), видим, что при k > 0 Hk(R) = F4
Ответы к упражнениям 371 Следовательно, wp(dood, R) = H0(R). Но H0(R)= "]ВВд/?, что эквивалентно ~]F/\R (поскольку ВВ последовательность из нуля дизъюнктивных членов), а это есть R. Следовательно, wp(do od, R) = R и do od эквивалентно skip. Это имеет смысл, когда выполняется do od, истинной охраны не может найтись (охран нет), и, таким образом, оно немедленно завершается. 2. При доказательстве теоремы (10.5) было, в частности, доказано, что (Vi:PABi=*wp(Slt PV^PAivi'.B^ywpiSi, P)) Следовательно, если дано предположение 1, то мы имеем рдвв =РАВВАТ ^PAB^Mw-PABi^wpiSi, P)) (так как предположение 1 истинно) ^PABBAPMvr.Bg^wpiSt, P)) = BBA(V':5|=>o'P(S/, P)) = wp(\F, P) 3. Приемом, подобным тому, который был использован в упр. 2, можно показать, что предположение 3 теоремы (11.6) влечет (Е3.1) PABB=>wp("T :=t\ IF", t < T) Таким образом, нужно показать лишь то, что из (Е3.1) следует п. 3' теоремы (11.6). Заметим, что Р, IF и / не содержат tl или t0. Поскольку IF не зависит от Г и t0, известно, 4Towp(\F, tl^.t0+l) = = ВВ Atl ^ t0-{-1. Тогда имеем следующее: (Е3.1) = PABB=$>wp(\F, t<tl)" (по определению :=) =ФРЛВВД*<*0+ 1 =>wP(\F, t^tl — l)"At<tO+ l (добавляем t^tO+l в обе части =>) = Р ДВВ At < W + 1 => wp(lF, t^tl— 1)/;Л(^<^+1)Г - Р ABB At <W+ I => (wp(lF, t^tl — l)/\tl <>М+1)" (дистрибутивность подстановки) = PA&BAt<tO+ l=>(wp(\Fy t^tl—l)Awp(lF, tl^tO+l))'1 (IF не содержит ни //, ни tO) = P/\BB/\t<tO+l =>wp(lF, /<*/—1л^<*0+1)" (дистрибутивность конъюнкции) = PABBAt<tO+\=±>wp(lF, /<«?)" = PABBAt^tO+\==>wp("tl :=/; IF", t<^W) = P ABB At <: tO+\=>wp("\F" /,<«?) Поскольку вывод справедлив независимо от значения Ю, он > справедлив для всех t0> и истинно 3'.
372 Ответы к упражнениям 4. Покажем сначала, что при /? = 0 выполняется (П.7), доказав, что оно эквивалентно предположению 2: ЯДВВ => / > 0 (предположение 2) = l^V I BBV* > 0 (импликация, закон де Моргана) = 1PV1(^<0)V1BB = /эЛ^0=>"1 ВВ (закон де Моргана, импликация) =/>л/<о=>ял~1Вв =ЯЛ'<0=>#0(РЛ1ВВ) (определение Н0) А теперь предположим, что (11.7) истинно для k = K> и докажем, что оно истинно для k = K + l. Имеем: PA&BAt<K + l =*wp(IF, PAt^K) (no 3') =>ay/?(IF, Я^(РЛ"|ВВ)) (предположение индукции) РЛ1ВВА/</С + 1->Рл1ВВ=//0(РЛ1ВВ) Эти два предположения влекут PA/<K + l=4>tf0(PAlBB)Va>p(IF, ^Л 1 ВВ) = НК+Х{Р А1ВВ) что показывает, что (П.7) имеет место для k = K + l. По индукции (Н.7) выполнено для всех k. 5. Сначала докажем индукцией по k, что Hk(F) = F. При k = 0 имеем H0(F)= ~|ВВЛ^ (по определению), а это есть/7. А теперь предположим, что HK(F) — F. Имеем =HQ{F)\Jwp(\¥, HK(F)) (по предположению индукции) =F\Jwp(lF, F) = F (IF удовлетворяет (7.3)) А теперь wp(DO, F)^(^k:0^k:Hk(F)), но поскольку для всех k Hk(F) — F, то wp(DOy F) = F, и DO удовлетворяет (7.3). Далее покажем по индукции, что для всех k имеет место (El) Hk(Q)AHk(R) = Hk(QAR) При & = 0 имеем /Ш)ЛВД) = -|ВВЛ<ЗЛ "]ВВд/? (по определению) =H0(QAR) Предположим, что (Е1) выполнено для & = /(. Тогда
Ответы к упражнениям 373 = -]BBAwp(lF, HK(Q))AwP(lFy ffK(R)) = ^BBAwp(lF, ffK(Q)AHK(R)) (no 7.4)) = "|BBA^p(IF, Hk(Q/\R)) (по предположению индукции) =HK+i(QAR) (по определению) Мы доказали, что (Е1) выполняется для всех k. А теперь покажем, что для 5 —DO левая часть (7.4) влечет правую часть (7.4). Пусть левая часть (7.4) истинна в состоянии s. Тогда также ис] инна (zk:0^k:Hk(Q))A№-.0<k:Hk(R)) Пусть kl и k2—значения, для которых в состоянии s истинны соответственно Hk(Q) и Hk(R). Тогда в состоянии s истинны Hmax(kl% k2)(Q) и Haax(kl, k2){R). А это означает, что Hmax(kly k2)(Qf\R) истинно в состоянии s. Следовательно, (^k:Hk(Q/\R)) истинно в состоянии s, и истинно wp(DO, Q/\R). Чтобы завершить доказательство, покажем, что правая часть (7.4) влечет его левую часть. Это делается подобным же образом. 6. H'0(R)= 1ВВДЯ. При k>0 H'k{R) = wp(\F, //£_,(/?)). Множе- ство состояний, в которых DO завершается при истинном R в точности за k шагов, представляется предикатом H'k(R). Для сравнения Hk(DO, R) представляет множество состояний, в которых DO завершается при истинном R и за k или меньше шагов. 7. (1) wp("x, у, z := а, 6, 0й, Р) =6>0л0 + я#/; = д* 6, что следует из Ь^О. (2) wp(SlyP) = y -f- 2^0/\z+(x + x)*(y -т-2) = а*й, что следует из РАУ > 0/\even(y). wp(S2,P) = y—1 >0л* + дг + **(//—1)=*а*&, что следует из PAodd(y). (3) Это было показано непосредственно перед алгоритмом (11.5). (4) РДВВ =>у ^ оД((у > 0Aeven(y))Vodd(y)) = {у> 0Aeven(y))\/(y > OAodd(y)) =># > 0, что есть t > 0. (5) wp("tl := t\ S,'\ t <tl) = wp{"tl :=</; y, x :=y + 2, x + x\ y<tl) = wp("tl :=y\ y + 2<tl) = у -f- < 2, что следует из у > 0Д£шг(#) wp(«tl := t\ St", ( < tl) = wp{"tl :=y; у, г := r/-l, г + х", у < «) = ^('7/ :=*/", y-l<//) = /y—1 < у, что тождественно истинно. 8. (1) wp(% s := 10, 0", P) =o<io< юло = (2ЫО+1</<<Ю:&[&]) = г
374 Ответы к упражнениям (2) wp(Su P) = 0<i—l<10As + 6[i]== (2*:*—! + !<*< 10:6[А]) = 0<i<llAs = (S*:t + l<*<10: ОД, что следует из РЛ»>0. (3) PATBB = t = 0As = (2*:» + l<*<10: ОД, что влечет Д. (4) РЛВВ=>1>0, что и есть t > 0. (5) wp("tl:=t; St", t<tl) = wp("tl :=t;i, s : = / —1, s+ &[/]", t </l)=OT?("fl := i", i—1 < /l) = i — 1 < i, что всегда истинно. 9. (1) wp("i := 0", Р) = 0^0<пЛ^О[0:0-1], что следует из 0<п. (2) wpiS,, P) = wp("i := i'+l", 0<1<яЛх#6[0и'—1]) = 0<i + 1 < яД*0[O:i+ 1 — 1]) что следует из Р/\i <пс&п&хфЬЩ. (3)РЛ1ВВ = 0 s^i t^n Ax $b[0:i—\]A(i>n cor x = b[i]) =^(0^i<ncnndx = b[i])\/(i = nAx^b[0:n — l]) что и есть R. (4) РдВВ=>1 <n = n—i >0, что и есть * > 0 (5) wp("tl := t\ S", t< tl) = wp("tl := n—i; i := i+1", n—t < tl) = wp("tl := «—i", n—(i + l)<tl) — n—(t'-f-l)<n—i, что всегда истинно. 10. (l)wp("i: = 1", P) = 0< 1<пЛ(НР:1=2'')=7\ что следует из 0 < п (возьмем р = 0). (2) wp(Sl9 Р) = wp("i := 2* Г, 0< *<nA(H/?:' = 2^)) = 0 < 2 * t < /1Л(НР"-2 * 1 = 2я) что следует из ЯД2*^^Аг. (3) РЛ1ВВ = 0<1<ЛЛ(НР-^2Я)Л2»1>Л, что эквивалентно R. (4) ЯЛВВ=5>0<1 <nA2*t<n=5>/i—1>0, что и есть /> 0. (5) wp(utl := f; V. f < fi) = wp("tl := /г — г, j := 2* Г, я—i<tl) = wp("tl :=n — inf n—2*i<tl) = n—2*f < n—/ = —i<0, что следует из Я. 11. (1) wp("i9 af b: = 1, 1, 0", 1 < i < n/\a = ft/\b = -/,-1)-1<1<яЛ1=/1Л0 = /1.1)-0<п
Ответы к упражнениям 376 (2) wp(Su Я) = 1 <i"+ 1 <:n/\a + b=*fi+lAa = fi+i-i = 0<t</iAfl = //A6«/i + i—// = 0^i <in/\a = fi/\b = fi-li что следует из P/\i<n.. (3) ЯЛ IBB = 1 <li<lnAa = fiAb = fi-1Ai>n = /=мЛа=!/г/Л*в/|-1» что влечет /?. (4) ЯЛВВ=>1 </<яЛ'<" => я—/ > 0, что и есть f > 0. (5) а;р("/7 := *; ЯД / </У) = wp("tl := /г—i; f, a, 6 : = t -f- 1, а + 6, а", /г—i<tl) = wp("tl := n—i'\ n—(i+l)<tl) = п—(/+l)</z—i, что истинно. 12. (1) ш/?("</, г:=0, хи, 0^rA0<yAq*y + r = x) = 0^^л0 <#Л0 *# + * = *> что следует из *>0Л*/>0„ (2) ^(Slf Я) = wp("r, q:^r—y, q+V\ 0^rA®<yAq*y + r = x) = 0 ^ r-yАО < у A(q + I) * У + (г—у) = х к = 0^r—#Л0 <УАЯ*У + r — x, что следует из РА?>у* (3) ЯЛ 1ВВ = 0^гЛ?*^/ + ^ = ^Л/'<У» что эквивалентно /?. (4) ЯлВВ=>0<#<г=>г> 0, что и есть t > 0. (5) ау/?('7/ := f; 5Д / <//) = wp("tl := г; г, д:= r—yy g+V\ r <tl) = wp(utl := г", r—y<tl) = r—У<г, что следует из ЯЛВВ. 13. (1) a;p("t, А:= 1, 0", 0 < i<^nAb[k]^b[0:i— 1]) = 0< 1<лЛб[0]^6[0:1 —1]) = 0<я, что и является предусловием алгоритма. (2) wp(Su Я) = ^/?("if...fi; i := i+V\ Я) = ay/?("if.. .fi, 0 < i+l^nAb[k]^b[0:i]) = (b[i]^b[k]Vb[i]^b[k])A (b[i] <b[k] => 0< i^nAb[k] > 6[0:f — 1])Л {b[i]^b[k]=$wp{"k :== Г, 0 < /+ 1 </2Д &[£]>fo[0:*])) = (&[i] < 6[A] => 0 < * < nAb[k] > ft[0: i — 1 J)Л (Щ>&Й=ф0</+1<яЛйЙ^6[0:ф что и следует из PAi<n- (3) ЯЛ IBB — 0<t</iA6[*]>6[0:t— l]Ai>n = i = nAb[k]^b[0:n—l], что влечет /?. (4) ЯлВВ=>0</</2 =>я — * > 0, что и есть / > 0.
376 Ответы к упражнениям (5) wp(«tl := /; S/\ t < (1) = wp("tl := л —t; if...Я; i := i + Г, n — i<tl) ~wp("tl := n — V\ n—(i-\-l)<tl) (в if.. .fi нет присваиваний i) = д — (*+!)< ft — i, что истинно. Ответы к гл. 12 1. Делаем следующие эквивалентные преобразования: \Q(u)\ S {R\ = (Vu:{Q(u)\S{R\ = {yu:Q(u)=$>wp(Sy R)) = (Vu: 1 Q(u)Vwp(S, R)) (импликация) = (Vw: ~\Q(u)Vwp(S, R) (ни S, ни Я не содержат и) ~~](RuiQ{u)Vwp(S9 R) = (7[u:Qu))=2>wp(Sy R) (импликация) (El) ={(zu:Q(u))\S{R\ Следовательно, эквивалентны первый и третий предикаты, а не первый и второй. 2. Квантор у необходим в предусловии теоремы (12.2.1). Предикат предусловия, на который навешен квантор у, может пониматься следующим образом. Вызов процедуры может быть выполнен в состоянии s, чтобы получить нужный нам результат R, если все возможные присваивания значений и> v выходным параметрам и соответствующим аргументам устанавливают истинность предиката R. Только что доказанная (упр. 1) эквивалентность указывает на то, что без квантора у неявно присутствовал бы квантор д. Предикат, на который навешен этот неявный квантор существования, может пониматься следующим образом: вызов процедуры может быть выполнен в состоянии s, чтобы получить нужный нам результат R, если существует по крайней мере одно возможное присваивание значений и, v устанавливающее истинность R. Но, так как нет гарантии, что на самом деле параметрам и соответствующим аргументам будет присвоено именно это конкретное возможное множество значений и, и, данное высказывание в общем случае ложно. Например, рассмотрим процедуру ргос ^(result г); \P:T\z:=3\Q:z¥=2\ и ее вызов р(с) с постусловием с=1. Если в предусловии теоремы (12.2.1) опустить квантор, то оно становится эквивалентным условию с квантором з (см. упр. 1), так что имеем {PRxT/\{Zv:v=£2=zv=l)\p{c){R:c=\\
Ответы к упражнениям 377 Поскольку выполнено (^v:v^= 2=$>v= 1) (берем у=1), имеем \PR:T}p(c)\R:c=l\ что несовместимо с операционной моделью выполнения данного вызова процедуры. 3. В качестве контрпримера воспользуемся процедурой ргос /?(value result xl, x2) \T\x2 := xl \x2 = xl\ и вызовом процедуры p(b [Л, i), В предположении, что теорема (12.2.12) имеет место, даже если аргументы не являются различными идентификаторами, возьмем 1=Т и докажем {Т} р(Ь [Л, i) {i=b [i]}. Но этот вызов эквивалентен i := b [Л, которое не всегда устанавливает i=b [Л. 4. Поскольку в вызове процедуры из примера 1 аргументами являются различные идентификаторы, может быть использована теорема (12.2.12). Применение теоремы при 1 = Т дает {a = X/\b = Y\swap(a, b){a = Y Ab = X) что и требовалось доказать. В примере 2 заменим идентификатор X в условиях Р и Q на А (см. пример 2, где приведен разбор такого действия). Получаем \P:yl = AAy2-Y\B{Q:yl = YAy2 = A\ Тогда, воспользовавшись теоремой (12.2.12) при 1 = Т, получаем \a*=AAb = Y\swap(a, b){a = Y/\b=A\ Для вызовов из примеров 3 и 4 теоремой (12.2.12) пользоваться нельзя, так как эти вызовы содержат аргументы, не являющиеся идентификаторами. 5. (а) Для /?*ss (c[j] = 5) заменяем X на 5, а В на с в пред- и постусловиях Р и Q и получаем Р/?:0<</л5==5Лс = сЛ (y^b, vk, vp-.O^vp <vk/\vb\vp]~b=$>vb\vp] = 5) (b) Для специфицированной так, как указано, процедуры s нельзя принимать никаких предположений о значении / в момент завершения вызова. Добавляя к Р и Q конъюнктивные члены k=K и заменяя в них В на с, а X на /, можно получить PR:0<^mAf = fAc = cA (yvby vk, up:0^vp^:vkAvb[vp] = f=$>vb[vp] = f) что и является предусловием вызова.
378 Ответы к упражнениям (c) Это доказать нельзя, поскольку нет оснований предполагать, что перед выполнением вызова процедуры Ь [0]=d0]. (d) Обратите внимание на то, что т появляется в качестве аргумента в двух местах вызова. Следовательно, при подстановке в R нужно соблюдать аккуратность. Производя в Р и Q замену X на 5. а В на су получаем PR:Q^.m/\5 = 5/\c = c/\('4vb, vky vp:0<^vp^vk/\ vb[vp] = 5=s>vb[(m\ e:vk\ s:vp)] = 5) = 0<тЛ (yvb, vk, vp:0^vp^vk/\vb[vp] = 5 => vb[vp] = 5) = 0<m А это следует из предусловия 0<Cm. Заметим, что если изменить порядок, в котором идут параметры k и р, то в момент окончания будет значение k> а не р. 6. Нельзя доказать правильность вызова (с), поскольку он неправилен. Нельзя доказать правильность вызова (d) при помощи теоремы (12.2.12), поскольку два его аргумента одинаковы. (a) Заменим в пред- и постусловии тела процедуры X на 5, а В на с. Применяя теорему (12.2.12) при /=7\ получаем s(5, с, 0, /) {0</<dM/] = 5} из чего и следует нужный нам результат. (b) Прямое применение теоремы (12.2.12) дает {0<тЛ/ = ХЛс-В} s(f, с, ту /) {0<j<fAd[i] = X\ Заменяя в пред- и постусловии этого истинного предложения X на /, а В на с и упрощая его, получаем s(/, с, ту j) 7. Правильность вызова не может быть доказана при помощи теорем (12.3.3) — (12.3.5), поскольку аргументы, соответствующие k и /?, не занимают разные участки памяти (они одинаковы). Ответы к гл. 14 la (a) Q:T R:{z = x\fz = — x)/\z^Q if x^O-^z :=x[]x^0-»z := — x fi.
Ответы к упражнениям 379 (b) Q:x = X R:(X^OAx = X)\/(X^OAx = — X) Ux^O—>skip [] x^O-^ x := —x\\ (c) Q:k = KAx = (Ni:0^i<k:odd(b[i])) R:k = K+\Ax = (W:0<^i<:k:odd(b[i])) if odd(t[k])-+k, x : = 6+1, *+l Q even(b[k]) -> A := jfe+1 fi или if odd(£[&]) -> * : = * -f 1 [] o;m (£[£]) —* sfo'p fi; As := k+l где odd(y) = (r/ mod 2) = 1 (d) Q:PAa = AAb = B R:((a = (A + B) + 2Ab = B)\/ (a = AAb = (A + B) + 2))AP \i((a + b) + 2)2<^n-+a :- (a + b)~2 \]((a + b) + 2)2>n-+b := (a+ 6) -=- 2 fi или d: - (fl+ 6)-1-2; \\d2^n-^a\= d[]d2>n-+b = d\\ 2. Предположим, что следующая большая перестановка существует (это не так, например, для d = 543221). В последующих рассмотрениях может оказаться полезным иметь в виду конкретный пример: d =(1,2,3,5,4,2) d ' = (1,2,4,2,3,5) Имеется наименьшее число i, 0 ^ i < ny такое, что d[i] < d[i'+l] (и, следовательно, d[0: i—l] = d'[0: i—1]). При этом i определено корректно, поскольку d[i+l: п—1] является невозрастаю- щей последовательностью чисел, а d[i]<d[7+l]. Чтобы д! было следующей большей перестановкой членов, в d' [i] должно содержаться наименьшее из значений dfi-fl: п — 1J, больших d [/]. Пусть самым правым элементом d [i + 1: п—1], который имеет такое значение, является d[j]. Рассмотрим массив d" = (d; i:d[j\\ j:d[i]). Здесь d" — это массив d, в котором переставлены значения с индексами / и /. В приведенном выше примере d" = (l, 2, 4, 5, 3, 2). Очевидно, что d" является большей перестановкой, чем с/, но, возможно, не следующей большей. Более того, d'[0: i]**d'[0: /]. Можно доказать, что d"[i + 1 :п— 1] является невозрастаю- щей последовательностью чисел. Следовательно, обращение этого сегмента превращает его в неубывающую последовательность и,
380 Ответы к упражнениям следовательно, в наименьшую из возможных. А это и приводит к искомой следующей большей перестановке элементов d. Подытоживая сказанное выше, положим / и / такими, что 0 < i < n—\f\d [i] <ф'+ ПЛф + l:/i — 1] невозрастающая Ai<j<nAd[j]>d[i]Ad[j + l. я —l]<d|7] Следующая большая перестановка d'—это d''вобращение^; i:b[j]\ j:b[t]), i+K n — 1) где обращениефу /, g) обозначает массив Ьу в котором сегмент b[f:g] обращен. Тогда алгоритм таков: вычислить г, вычислить /; переставить b[i] и b[j]\ обратить b[i+l:n—1]. Формализация идеи следующей большей перестановки непосредственно привела к алгоритму для ее вычисления! Ответы к разд. 15.1 1. t, s =tt, 0; doj'^O—-W, s: = /—1, s + b[i—l]od 2. x^b[0: i —1, 0: n—\}/\x^b[i, 0: /-1] или (VP, ?:0 </?</, 0^q<n:x=£b[p, q])/\ (YQ:0<Q< f'.x¥=b[i, q]) 3. t, x. = l, ft[0]; do i=£n—*tix^b[i]—+it x == / + 1, b[f] Qjc<b[i]-W: = < + 1 fi od или *\ л: -1, 6[0]; do j =^= л —> if x ^b[i]—+ x • = b [i] Qjc </?[/] —*skip fi; ir-z + l od Обе программы недетерминированы. Целесообразно преобразовать алгоритм таким образом, каким это было сделано выше в тексте книги, чтобы получить другую его версию. 4. i, х:— п— 1, Ь[п—1]; do f^O—Hf *>&[/— l]-+i, *== i —1, fe[f—1] Q*<&[* —1]-W: — i— 1 fi ' od
Ответы к упражнениям 381 или j, х : = п — 1, Ь[п — 1]; do tr^O—*t := i—1; if xl^b[i]-+ x •« 6|7] □ * < 6 [/] —► skip fi od Отметим, что преобразование во второй версии несколько упрощает алгоритм из-за того, что выражение i—1 встречается в меньшем числе мест. 5. i = I, do 2 * / ^ п —* I: = 2 * i od 6. Перепишем на другой язык не саму программу (15.1.7), а ее перестроенную версию, стоящую непосредственно перед обсуждением. Запишем ее на ПЛ/1, но те же самые проблемы возникли, если бы мы переписали ее на Паскале или Фортране. Главной проблемой является то, что операции cand нет ни в одном из этих языков, и, следовательно, нельзя написать цикл типа while с охраной из программы (15.1.7). Одним из возможных решений является преобразование алгоритма таким образом, чтобы охраной цикла был лишь один из дизъюнктивных членов, а это требует искусственного выхода из цикла: 1 = 0; /=0; DO WHILE (1фт)\ IF x = b[j] THEN GOTO выходизцикла\ /-/-hi; IF / = n THEN DO; i-i+1; /=0; END; END; выходизцикла:\ Другим решением является моделирование цикла с охраняемыми командами, как это показано ниже. Этот алгоритм должен быть хорошо документирован. '=0; /=0; /* Моделируем do 1фт cand хфЬ\ьу /]—►*/ /* /: = /+1; */ /* if / < п -* skip*/ /* D l = n-+U /: = '+!, 0 •/ /* Н */ /* od 7 цикл: IF i — tn THEN GOTO выходизцикла; IF x = b[i, j] THEN GOTO выходизцикла; / = / + 1;
382 Ответы к упражнениям IF ]Фп THEN GOTO конецусловищ f = t-f-l; / = 0; GOTO конецусловий; конецусловий: GOTO цикл; выход изцикла\\ Ответы к разд. 15.2 1. Эта задача является простым обобщением разобранного в тексте примера на случай трех измерений вместо двух. Такое обобщение требует введения еще одной охраняемой команды. Инвариант цикла приведен ниже, а его завершаемость слишком очевидна для того, чтобы давать формальную ограничивающую функцию. Р: 0<1'</пЛ0</</1Л0<А<М х не находится в c[0:i—1, 0 : лг — 1, О : р—1]Д х не находится в c[i, 0:/—1, 0 : р—1]/\ х не находится в c[iy /, 0:k — 1] i, U k: = 0y 0, 0; do 1фтГ\\Фп/\кФр cand хфс [i, /, k] □ 1фтГ\\фп[\к = р--^\, A:=/ + l, 0 □ [фтГ\\=*п -W, /, k:=i+l, 0, 0 od 2. Инициализация цикла: х, у := X, Y. Очевидные кандидатуры команд, которые надо испытывать на основании приведенных свойств, это х: = х+у, х: = х—у, х := у—хи т. п. Поскольку 7>0, первая из команд никогда не уменьшает ограничивающую функцию t: х+у, и ее использовать не нужно. Вторая из команд уменьшает t, но сохраняет инвариант лишь при х>у. Таким образом, имеем охраняемую команду х>у-+х := х—у. Из соображений симметрии целесообразно использовать и у<х->у : = у—х, и окончательная программа имеет вид х, у: = X, У; do x>y—+х:=х—у [}у> х-+у :=у — х od {0<x = yAgcd(x, y) = gcd(Xt Y)\ \x = gcd(X, Y)\ 3. x, y, z := X, Yy Z; {инвариант P: 0 < x, y, z/\gcd(x, y, z)=gcd(X, Y\ Z)\ {ограничение t\ x + y-\-z) do x > y—+x : = x — y D г>*--*?:=*—* od
Ответы к упражнениям 383 4. Из данных нам свойств gcd мы можем заключить при #>0, что для любого k gcd(xy y) = gcd(x> у — k*x) = gcd(y—k*x, x). Выберем k таким, чтобы оно уменьшало у—Ых настолько, насколько это возможно, но сохраняло его ^0, т. е. у—k*x должно быть остатком от деления у на х. Тогда, придавая паре (л:, у) значение {у—k*x, х), уменьшаем данную нам ограничивающую функцию. (Проверьте это, исследовав два случая: х > у и *<#.) ху у:=Х, Y\ do х Ф 0 —> х, у : = у— (у ~ х) * ху х od \РАх = 0\ \y = gcd(Xy Y)\ 5. Эту программу труднее специфицировать и объяснить, чем написать! v:=0 do /V=80 cand Ь[(\Ф"-+ь% s[v], j:=v+\, b[j], / + 1 □ / = 80 -+read(b); /:=0 od Ответы к разд. 16.2 1. a : = n\ {инвариант P: 0 ^a2/\n < (a+ l)2f {ограничение t: a—floor(sqrt n)\ do a2 > n —>a := a— 1 od Цикл повторяется п — V~n раз, что по порядку то же самое,, что и п раз. Цикл программы (16.2.2) повторяется лишь раз, так что программа (16.2.2) намного быстрее. 2. Устраняем из R, приведенного после алгоритма, конъюнктивный член п < 2 * i {Q: 0<п\ I := 1; {инвариант: 0</^пД(зР: 2p — i)\ {ограничение: п — i (на самом деле подошло бы и log (/z — i))\ do 2 # i ^ n —* i : = 2 * i od {/?: 0<t</i<2*tA(3p: 2/? = 0} 3. Устраняем из R, приведенного после алгоритма, конъюнктив- ный член г <у. {Q: 0^хА0<у\ Я, г:=0, х\ {инвариант: 0^r/\q*y-{-r = x} {ограничение: г) do r^y—*/■, q:=r — y, q+l od {/?: 0<г<*/Л <?*# + >- = *}
384 Ответы к упражнениям 4. Устраняем из R конъюнктивный член x = b[i, /]. *. / := 0, 0; {инвариант: O^i <^т/\0^ j <i n/\x^b[0: i — 1, 0:п—1]Д x$b[i, 0:,— l]A*€b\ {ограничение: (т — i)*n — /} do x=£b[i, j]Aj¥=n — 1 -*/ := /+1 [\x¥=b[it /]Л/ = /г—1 —/, /:=i + l, О od 5.(1) wp(ui:=0n9 (16.2.6)) = 0<0<тЛ^ОГ0:- ЧЛ*€Ь[0: m—l], что следует из предусловия Q. (2) wp("is-i+l"f (16.2.6)) = 0</ + 1 <mAx$b[0:i]x € 6 [0: т—I]. Само Р влечет все, кроме i+\ <tn/\x¥=b[i]. Из охраны В следует хфЬЩ. А это вместе с Р влечет x£b[i+ I: т — 1], так что этот сегмент массива непуст и i+\ </л. (3) Тривиально. (4) Из Р/\ВВ следует, что х£ b[i+ 1: т — 1]. Итак, /+ 1 <m—l и а/2 — i > 0. (5) wp{"t\ := m—i; *:=/ + !", m — i</l)=sm— t — 1 < m — i, что истинно. Ответы к разд. 16.3 1. i9 s : = — 1, 0; {инвариант: — 1^/^я—lAs==(2/: 0^/^i: &[/])} {ограничение: п — 1—i\ do Ьфп — 1 —> / : = / + 1; $ := s+-b[i] od 2. шр("Л := b—a+l\ тело цикла", Ь—а+1<*1) = (d*d^n\/d*d>n)A(d*d^nz>b — d + l <b—a+\)A (d*d> n =^d—a+ 1 < b—# + 1) где d = (a -+-b) -r- 2. Первый конъюнктивный член истинен. Далее покажем, что второй конъюнктивный член следует из а+\ < 6, так как из него следует заключение члена. Заключение второго члена—это b—d+\ <b—a+ 1 = — d<—a = — (а + ft) -f- 2 < — а = а < (а + Ь) -г- 2 А это истинно, если а+1<Ь. (Оно истинно также и тогда, когда odd {a + b) и а < £>.) Третий член доказывается подобным же образом. 3. a, b := 0, /г; {инвариант: 0^а^Ь^.пАа2^п < (Ь+ О2} {ограничение: b—а}
Ответы к упражнениям 385 do афЪ—*&\= ceil ((а + Ь)/2)); ifd#dO—^а:= d []d*d>n-+b: = d—l fi od He так уж легко написать команду этого цикла таким образом, чтобы ограничивающая функция и на самом деле уменьшалась. Если Ь—а = 1, то придание а или Ь значения (а-\-Ь)~2 может не уменьшить ограничивающую функцию. Правильным значением d является ceil ((a + b)/2). Вывод алгоритма при помощи замены константы выражением а+\ вместо а кажется проще. 4. (а) I, /: = 1, л; {инвариант: 1 <; J < i ^n/\b[i]^x < b[j]\ {ограничение: ceil(\og(j—i))\ do i + l^j-*e:=(i + j) + 2; \fb[e]^lx—+i:=e Ub[e]>x-+j:=e fi od Очевидным вариантом решения второй части задачи является помещение программы для решения первой ее части ь качестве одной из альтернатив команды выбора: if x<b[l] -W:=Q □ b [1] <; x < b [n]~-► программа (а) Q b ln\ < x —+i:=n fi Однако есть и более простой способ. Предположим существование b [0], содержащего значение —оо, и b [n], содержащего значение -f-oo. До тех пор, пока программа не ссылается на эти значения, это можно предполагать. Тогда может быть использована после небольшого изменения инициализации программа для решений первой части задачи. Она работает даже в том случае, когда массив пуст, придавая / значения 1. |^У ** U /: = 0, л+1; {инвариант: 0<л < / 0 + 1Л&[*1 О < b[j]\ {ограничение: ceil(\og(j — i))\ do (+1ф]-+е:=*(1 + 1) + 2; jl <е<я} if b[e]^.x—-> i : = e Ub[e] > x—*j:=e fi od 13 д. грис
386 Ответы к упражнениям 5. /, <7:= 1, 1; {инвариант: 1 <| i^n/\q = число площадок в b[0:i—1]} {ограничение: n — i) b[i—l] —>i:=i+l я i\¥=b[i—l]-+i, q :=*+!, q+l do i-ф n—-* if b\ fi od {<7 = число площадок в b[0:n—1]} i9 A:=l, 0; {инвариант: O^k < *<АгЛН^]^ 6[0:i—1]} {ограничение: n—i'} <6[iJ--*i:=i+l do [фп—+\\ b[k Ub[k fi od {0<A<nA6[*]>6[0:i—1]} 7. i, d:=0, 0; {ограничение: п—i\ do 1фп—+\\even(b[i]) —> i:= i-\-1 DoW№"])-^f, d:=i + l, d+1 fi od {d = (Nj:0^j<i:odd(b[j]))\ 8. Сначала построим инвариант {P: 0</7<тЛО<^7<АгЛ *«(№, /:0<1<рЛ0</<^:/М=г[/])} Однако такой инвариант трудно сохранять, поскольку увеличение i внутри тела цикла требует определения, какое из значений g[0 : п—1] равно новому / U], а это требует нового цикла. Пользуясь тем, что массивы упорядочены, и предполагая существование виртуальных значений f\m] = oo и gln] = ooy усиливаем инвариант добавлением конъюнктивного члена glO : /—1]</ [i] (и / [0 : i—1]< g[j])y так что по всем законам единственным значением в g [0 : /"], которое может быть равно f[i], является само g [/]. А это приводит к следующему алгоритму: р, ?, А:=0, 0, 0; {инвариант: О^р^тДО ^q^nf\ *«(№, /: 0<i</V\0</<</: /М**[/])Л «г[0:/-1]</Ил/[0:1-1]<«[/]} {ограничение: т—/? + az—#} do рфт[\цфп—» и/М<г[?]-^=Р + 1
Ответы к упражнениям 387 Uf[p]>gm->Q--=Q+l fi od {/? : U(N/, /:0 < i < mA0< / < /i:/[*] = g[/])} 9. *, sl = 0, Г; {инвариант: 0< i < «As = (fc[0:i —1] = 0)} {ограничение: п — 1} do ъ/\(фп—> i, s:=/+l, £>[j] = 0 od {/?:A = (6[0:n-f] = 0)} В следующей версии используется чуть более слабый инвариант: I, s:=0, Т\ {инвариант: O^i <n/\b[0:i — f| = 0} {ограничение, n — i) do /^=/1 cand£[7] = 0—*/:= *+1 od; s:= (n = i) {R:s = (b[0:n — i] = Q)\ 10. i, /?:=0, 0; {инвариант: см. упр. 10; ограничение: я— i\ do [фп—* Увеличить i, сохраняя истинность инварианта: {инвариант: все &[/:/—1J равны; ограничение: п—/} do \Фп candft[/1 = Ь[i] —-► /: == / + 1 od; р:= max(/?, / — i); i' = i od Ответы к разд. 16.5 1. Обозначив начальное значение массива Ь через В, имеем: Q: 0<i\ /</n—/1Л0<лЛ(/ + л</\//+л<0Л &[0:/и-1]=:В[0:/л—1] Я: й[/:/ + д-1] = б[/:/ + п-1]лЬ[/:/ + Аг-1] = B[ri>/i —1]Л(у*П ('<*<*+я)Л "!(/<*< /+л): 6[ft] = 5[Л]) 2. Пред и постусловия можно задать следующим образом, где «обращен» и «необращен» имеют очевидный смысл: / / i J R:b\ Q: b обращен необращен Инвариант может быть построен заменой в R констант i и / на переменные и учетом предусловия или же рассмотрением инварианта 13*
388 Ответы к упражнениям как предиката, который должен следовать как из Q, так и из R и, следовательно, в котором должны быть и обращенный, и необращенный сегменты. Инвариант — это Р: i ^р ^q+l^j + l л Ь\ обр, необо. обр. Ограничивающей функцией является q—р—1, и алгоритм имеет вид р, q:=i, /; do p<q-+b[p], b[q]:=b[q]yb[p]; р, q:=p+ly q—\ od Заметим, что выполнение цикла сразу завершается при g=p\ сегмент, в котором лишь одно значение, не нуждается в обращении. 3. Вначале значения массива неизвестны. В момент завершения они должны быть распределены между двумя сегментами. Следовательно, в инварианте должны иметься «неизвестный сегмент» и два сегмента, данных в R. Для инварианта есть по крайней мере две возможности: т q n — \ PI: m ^p ^q ^п л b Р2:т ^p^q + l^n л Ь 1 ^* т 1 ^* I 1 >х Р Я ? 1 Г ? 1 п-\ >х | Инвариант Р2 приводит к более быстрому алгоритму, в котором производится не более одной перестановки за шаг цикла: р, q:=m, n—\\ do p^q—+ if b[/?]<;х Ub[q Ub[P fi >x >x^b[q]. >p:=p + \ !ip\,qb[q]:=b[q}, b[p]; p, q:=p+\, q—\ od 4. Единственным различием данной задачи и упр. 3 является то, что значение, используемое для разбиения массива на два сегмента, уже находится в массиве и что это значение должно быть помещено в Ыр]. Инвариант р цикла, приведенного в нижеследующей процедуре, содержит (помимо того что b [m]=B[m], a b является
Ответы к упражнениям перестановкой В) условия m<Cj^p+\^nf\x=B [m] и т т+\ q p п—\ 389 X ^х ? >Х | ргос разбиение (var b: array of integer; value m, n: integer; result A: integer); var *, #, p: integer; begin xy q> p:=b[m], m-f 1, n—1; {инвариант: Р; ограничение: р—q+Ц do q^p—+ ii b[q]^x —+q:=q+l Ub[p]>x —p:=p_l \]b[q]>x>b[p]-+b[q]9 b[p]:=b[p]y b[q]; q, p:=q+l, p—\ fi od; \p = q-lAb[m+l:p]^b[m]A b[p+l:n — l]>b [m\/\b [m] = Б [m] = x\ b[m]t Ь[р]:=Ь\р], b[m] end 5. При использовании методов, описанных в данном разделе, можно получить по меньшей мере два возможных инварианта: Pl.^i^j^k^n л ъ. P2:Q^i^j^k+l^n л/3 0 1 КР ! 0 1 кр 7 белый ./ синий к п-\ 7 1 / ./ к 77—1 белый 7 синий Второй из инвариантов приводит к программе, делающей не более одной перестановки за шаг: 7, /, k := 0, 0, /2—1; do / <; k —+ if красныйф [/]) -> b [7], b [/]: = b [/], b [i]; j, 7:=/ + l, 7+1; U6eAbiu(b[j])-+j:=j + l []cuHUU(b[j])-*b[Jl b[k]:=b[k], b[j]; k:^k-\ fi od
390 Ответы к упражнениям 6. В предусловии говорится, что в линейном списке — прямой порядок элементов, а в постусловии — что этот список обращен. Это подсказывает нам алгоритм, обращающий на каждом шаге одну связь. Часть списка уже обращена, а часть находится в исходном порядке. Таким образом, инвариантом является р г V и, V Ут s -1 S J ...У Vi V Уп \ v S Л D где новая переменная / используется для указания на находящуюся в исходном порядке часть списка. Вначале обращенная часть списка пуста, а необращенная составляет весь список. Все это приводит к алгоритму р, t: = — l, р\ do 1ф— 1 — р, t, s[t] := t, s[t], p od 7. Пусть s°[p] — это /?, a s1' [p] = s[sl'""l[/?]] при i>0. Сначала определим предикат линейный список(р, v> s, V)> где v и s — массивы, а V—последовательность (Vly ..., Vn): линейныйсписок(р, v, s, K) = (yt: 0<i</i: V/+,=i>[s'[/>]]) Л (V*\ /- 0<t</</i: ^'[p]^s^pl)As"[/?l = 0 Тогда пред- и постусловиями являются Q: линейныйсписок(р, v, s, (У1э ..., l/J) R: линейныйсписок(р, v, s, (Уя, ..., l/t)) 8. Пред- и постусловиями являются Q: xgb[0:m—1, 0:лг— 1] Я: 0<1</яЛ0</</1Л* = &|>\ /] Q и R подобны. В обоих утверждается, что х находится в прямоугольном сегменте массива Ъ — в R\ этот прямоугольный сегмент таков, что у него одна строка и один столбец. Таким образом, по- видимому, окажется возможным воспользоваться инвариантом, говорящим, что х находится в прямоугольном сегменте массива Ь: Р: 0<1'<р</лЛ0<?</ <n/\x£b[i:p, q:j] Инициализация: i, /?, qy /:=0, m— 1, 0, п— 1. Чтобы приблизить цикл к завершению, необходимо сделать прямоугольник меньше, и для этого имеется четыре простых способа: i \— i+l и т. п. Та-
Ответы к упражнениям т ким образом, попытаемся применить цикл вида I, /?, q, /:=0, /п—1, 0, п— 1; do?-*i:=i-|-l П?-р:=/7-1 D?->^?:=^+l D?-/:=/-l od Что может служить в качестве охран? Рассмотрим команду i:~ /+1. При ее выполнении инвариант может сохраниться, если известно, что х не находится в строке i массива Ь. Так как массив упорядочен, это условие можно проверить путем Ь [i, /]<*, ибо если Ь U, /1 меньше х, то таковы же и все значения в строке L Подобным же образом определяем остальные охраны: *\ Ру Q> /:=0, т—1, 0, л—1: do Ъ [*', /] < а: —* i: = t + 1 od P> q]>x-+p:=p—l P, q]<x-+q:=q+l *\ /] > *->/:=/—1 Для доказательства того, что результирующее утверждение истинно в момент завершения цикла, нужны лишь первая и последняя охраны. Таким образом, средние охраняемые команды можно удалить и получить программу i, /:=0, *—1; do b[i, /] <*—м':= i + l Ub[i9 j]>x-+j^=j — 1 od{x = b[t, /]} В этой программе требуется самое большее п-\-т сравнений. Это число нельзя существенно улучшить по следующим соображениям. Предположим, что массив квадратный: т=п. Его элементы, лежащие на побочной диагонали: b [О : т—1], b [1 : т—2],. . ., Ыт—1 : 0], образуют неупорядоченный список. Дадим дополнительную информацию о том, что х лежит на побочной диагонали. Тогда в худшем случае требуется как минимум т сравнений. 9. Если вы не написали результирующего утверждения, которое определяет двоичное представление X, сделайте это и вновь попытайтесь выполнить упражнение перед тем, как продолжить чтение. Пред- и постусловиями Q и R являются: Q:X = x>0 R:X = v[k— 1]* 2*-l+ ... +П[0]*2°Л0 < v[k—l] где каждое v[i] удовлетворяет условию 0^i>[i]<2.
392 Ответы к упражнениям Инвариант можно найти комбинированием пред- и постусловия: P:0^xA0^kAX = x*2k + v[k— 1]*2Л"1 + ... + и[0]*2°Л для всех v[f\ выполнено 0^и[7]<2} Используя тот факт, что х = (х-±- 2)#2-f *mod 2, можно записать программу следующим образом: k:=0 {инвариант: Р\ ограничение: \+ceil([ogx)} do хфО —+х, v[k]y k:= х-±-2, х mod 2, k + l od 10. Программа подобна программе из упр. 9: Q: Х = х R: X = v\k—l *Ьк~х+ ... +v[0]*b°/\ = и[/г—1 0<v[k— 1] где v[i] удовлетворяют условию 0^у[/]<Б k:=0 {инвариант: x^0/\®^k/\X = x*bk -f v[k — 1]*6*~J + ... ...+*[01*Ь«л для всех v[i] выполнено 0<^i;|7] < £} {ограничение: 1 + ceil (log x)\ do х =^0—►х, v[k], k:—x + b, xmodby k + l od Ответы к гл. 17 1. Пусть m=min(qOyqlyq2yq3)t a М = max(q0,qlyq2,q3). Ограничивающей функцией является (^0—т)*(1+Л« — m)» + fo/_ т)*(1+М — m)2 + (q2 — m)»(l + +Af — т) + (<7<3—т) Ответы к разд. 18.1 1. reverse(b, т,п — 1); reverse(by п, р— 1); reverse(by тур—1) 2. Пусть А—это матрица Легко доказать, что -(IS) (LH-4J) Таким образом, алгоритм состоит в вычислении Ап~1 и взятии в качестве результата значения в строке 1, столбце 1. Программа воз-
Ответы к упражнениям 393 ведения в степень из разд. 19.1 показывает, как вычислять Л""1 за время, пропорциональное log n, если А является целым числом. Но тот же самый алгоритм работает и для массива А и арифметических действий над матрицами рместо арифметических действий над целыми числами. Ответы к разд. 18.2 1. Алгоритм (18.2.5) был разработан в надежде на то, что в общем случае на каждом шаге цикла будет уменьшаться общее число элементов (назовем его, например, S) в частях, которые еще нужно упорядочить. Таким образом, S является первым приближением к ограничивающей функции. Однако часть, описанная множеством S, может быть и пустой, а выбор пустой части и удаление ее из s не уменьшает S. Рассмотрим пару (5, |s|), где \s\ — число элементов s. Выполнение тела первой альтернативы команды выбора приводит к уменьшению (в лексикографическом смысле) этой пары, поскольку оно уменьшает \s\. Выполнение второй альтернативы приводит также к ее уменьшению — хотя \s\ и увеличивается на 1, 5 уменьшается по крайней мере на 1. Следовательно, ограничивающей функцией является 2*S+ls|. 2. Предположим, что до разбиения Ь [1] является наименьшим элементом массива. Тогда разбиение приводит к р X /1-1 >х | R: т —р ^п л Ъ т. е. часть b [т : р—11 пуста и часть Ь \p-\-\ : т—1] содержите— 1 элемент. Помещение в s обеих частей увеличивает число частей |s|, которые нужно упорядочить, на 1. Если так происходит на каждом шаге, то \s\ может вырасти до п—2. Даже если пустые части не записываются в s, s может в худшем случае вырасти примерно до я/3 элементов. Предположим, что последовательность частей, которые нужно упорядочить, хранится в виде стека, а не множества и что большая из двух образующихся частей, которые нужно упорядочить, помещается в стек первой. Тогда можно показать, что длина последовательности ограничена сверху log п. Например, если п — это 32178, то в последовательности содержится не более 15 частей! Следовательно, модифицируем программу так, как это показано ниже (по поводу обозначений, связанных с последовательностями, см. приложение 2). Команда присваивания (iy /) := s [0] в данной программе записывает в i первый элемент пары s[0], а в / — ее второй элемент:
394 Ответы к упражнениям s:= ((/и,/г—1)); \s является последовательностью пар (/?, q), представляющих непересекающиеся сегменты b[p:q] массива Ь. Далее Ь[т:п — 1] упорядочен тогда и только тогда, когда все непересекающиеся сегменты из s упорядочены} do 8ф( ) — (/,/), s:=s[0], s[l..]; if j — *<3—►непосредственно упорядочить b[i, j] D /—*^3—* разбиение (by i, /, /?); if p-i^j-p-+s:=((l,p-l))\((p+l9!))\s □ p_t > j-p-+s : = ((p + 1, /)) | ((*, p-1)) | s fi fi od вершин, у копе ременная для Ответы к разд. 18.3 Ь Пусть #L(p) обозначает число листьев (т. е торых нет последующих) в дереве р. Имеем (s— множества деревьев): с, s:=0, {/?}; {инвариант: ^L(p) = c + (Nr: r— лист дерева из s)} {ограничение: уменьшается следующая тройка; (#L(/?)—с, число вершин в деревьях из s, \s\\ do s Ф \ \ —► Выбрать (k, s); s := s—{k)\ if empty (k) —* sfo'p Q "] emp/y (£) —> if emp/y (left [k]) cand em/?/y (n'gft/ [k]) —+c:=c+\ □ "I (empty (left [k]) cand em/?/*/ (rtgft/ [k])) _*s :== s U {/ф [A]} U {rigft/ [A]} fi fi od {#L(p)=c\ 2. Определим inorder (p) =< empty(p) "}etnpty(p)- >0 inorder(left [/?]) | inorder(right [/?]) Постусловие программы и инвариант цикла следующие (s- менная, содержащая множество поддеревьев р): R:c = #p/\inorder(p) = b[0:c— 1] Я: 0 <; с/\ inorder (p) = b[0 :c— 1J | inorder(q) \ -nepe- (root[s\ (root[, [0]])\ inorder(right[s[0]])\. r's|~l]]) | inorder(right[s[\s\— Щ
Ответы к упражнениям 395 с, q, s:=0, p, 0; {инвариант: Р, приведенный выше} do empty(q) —> qy s := /*/*[?], (?) | s Q m]empty(q)As¥= 0 — ?, s : = sfO], s[l. .]; iiemptyiq) —* skip U~\empty(q)-^ c% b[c], q:=c+\, root[q], right[q] if od\R\ 3. Определим | empty(p)-+0 postord (/?) = < ~| empty(p) —- postord(left[p]) \ ' —♦ postord(right[p]) | (гло*[p]) Циклическая формулировка обхода в постфиксном порядке несколько сложнее, чем соответствующие формулировки обходов в префиксном и инфиксном порядке. Необходимо помнить, посещено ли уже правое поддерево некоторого дерева из s. Чтобы запомнить это, последовательность будет состоять из нар (q, lq), где q — дерево, а булева переменная lq имеет, в сущности, смысл «правое поддерево уже посещалось». Постусловием программы является R :с = #p/\postord(p) =.-• b[0xc— 1] Перед тем как сформулировать инвариант, сформулируем обход в постфиксном порядке для пар (qt lq): I ~\empty(q)Alq-+root[q] post(q) — < "| empty(q) ~] /<7 —* postord(right[q]) \ root[q] I etnptylq) —*( ) Инвариантом является (где s—переменная для последовательностей): Р:0 <сА® < q/\postord(p) = fc[0:c— 1] | postord(q) I pos/(s[0])|...|pos/ (s[|si — 1 ]) Тогда программа имеет вид с, ?, s:=0, p, ( ), do ~\empty(q) —► <?, s:= /e/%], (</, ^)|s Uempty(Q)As¥=( ) — (<7,/<7), s:=s[0], s[l..]; if-]emp/*/(<7)Afy—* <7, с, 6[ф= 0, c+1, roo%] [\etnpty(q) —+skip Ulempty(q)All<l-+q, si=right[q], (q,T)\s fi od{#}
396 Ответы к упражнениям 4. Пусть переменная для множества s содержит множество пар (d, tree), где d— глубина, на которой лежит корень поддерева tree в главном дереве р. Пусть D(s) — максимальное значение (d+ глубина дерева tree) для пар (d, tree) из s. Тогда результирующим утверждением программы является R :с=глубина дерева р а сама программа имеет вид с, s: = — 1, {(—1, р)\ {инвариант: глубина дерева р есть тах(с> D(s))\ do s=£{ \ —+ Выбрать ((d, t)> s); s:=s—{(d, t)}\ if empty(t)—>c:= max(cy d) D lempty(t) -s:=su {(d + 1, /*//[/])} U \(d+ 1, rigft/[/])} fi od Ответы к разд. 19.2 1. PI легко установить при помощи присваивания i, х := О, 0. Если (xv\i]y yv [Л) является решением (19.2.1), то r=xv [Л2 +yv [Л2<2*лх> [Л2 Следовательно, все решения (xv [Л, yv [i]) нашей задачи удовлетворяют условию г^2*лх'1Л2. Пользуясь принципом линейного поиска, выпишем исходный цикл для определения наименьшего х, удовлетворяющего г^2*л;2, и первым приближением к программе будет i, х: = 0, 0; do г > 2* х2 —>х: = х+ 1 od; {инвариант: Р1 /\г ^%*х2\ do х2 <| г —► увеличить х, сохраняя истинность инварианта od Чтобы увеличить х, сохранив истинность инварианта, необходимо определить, существует ли подходящее у для нашего х, и, если это так, вставить пару (х, у) в наши массивы. Для этого прежде всего требуется найти значение уу такое, что (El) x2 + i/^rAx2 + {y+l)2>r Выбирая в качестве инварианта внутреннего цикла второй конъюнктивный член Р2: л-2 + (/у + 1)2>г перепишем программу следующим образом: /, х:= 0, 0; do г > 2*х2 — + х:=- х+ 1 od; {инвариант: Р1/\г ^.2*х2\
Ответы к упражнениям 397 ^о х2^г--+ увеличить х, сохраняя истинность инварианта: определить у, удовлетворяющее (Е1): у:=х\ {инвариант: Р2\ do х2 + у2>г-+у:=у— 1 od; if *2-f-*/2 = r — ф], */[i>], |, л: := л;, у, t+l,x+1 Ux* + y*<r-+x:=x+l fi od А теперь отметим, что при выполнении тела главного цикла Я2 не нарушается, и, следовательно, Р2 может быть вынесено из цикла. Тогда соответствующая перестройка приводит к программе i, х:=0, 0; do г > 2* л:2 —* *:= х + 1 od; #:=*; {инвариант: Р\/\Р2) do л:2 ^/-—> увеличить *, сохраняя истинность инварианта: определить уу удовлетворяющее (El): do х2 + у2> г -+у:=у— 1 od; if *2 + у2 = г -» ф]. ylvl *> х:== х> У* *+J'. *+1 \]х2 + у2<г-+х:=х+\ Н od Эта программа является существенным улучшением (с точки зрения эффективности) предыдущей. Однако легче поднимается следующий алгоритм, принадлежащий Р. Бакхаузу. Рассмотрим массив b [0 : г, 0 : г], элементами которого являются b [x, у]=х2+у2. Каждая его строка и столбец упорядочены. Более того, значения в каждой строке (столбце) различны. Следовательно, можно воспользоваться одним из вариантов программы поиска седловой точки (упр. 8 к разд. 16.5). Далее на самом деле нет необходимости хранить этот массив, поскольку его значения можно легко вычислить. Имеем R: Каждая пара (xv[i]> yv[i]) при 0^/<д7 удовлетворяет условию 0 < yv[i] < xv[i]/\xv[if + L/v[i]* = г и никаких других таких пар нет. Инвариантом цикла является '{yi:0^i<k:0<^yv[i]z ((x,y)i(xv[0:i—l], yv[0:i—l]Ax*+y2 = r=z>m^x, y</i) P:'(yi:0^i<k:0^yv[i]^xv[i]/\xv[i]2 + yi[i\2^r)A "):i—11 Тогда алгоритм имеет вид
Ответы к упражнениям i, т, я|= О, О, 1; do гс2</- —* /г:= /? + 1 od do /и < n —► if m2 + ^2 <r —► m:= m+ 1 []m2 + n2 = r -+xv[i\ yv[i]:=n, m\ m, ny f:=//: + l, л—1, *+l []m2 + ai2 > r —+ n ■= az—1 fi od Ответы к разд. 19.3 1. Для того чтобы уловить суть задачи, опишем пред- и постусловие процедуры следующими диаграммами: Q:v R: v 0 А:дыр, / значений i+k 2*1-1 ? 0 2*/-1 / дыр, / значений (Повсюду) подразумевается, что нечетные элементы у (К) всегда содержат значения (кроме сегментов v, помеченных ? ), а четные элементы могут содержать дыры либо значения, что дыра содержит то же значение, которое следует за ней, и что значения з v сохраняют тот же порядок. Кажется целесообразным использовать цикл, распределяющий значения, начиная с самого правого (т. е. перемещающим сначала v[i+k—11 в vl2*i—И). Его инвариант получается заменой в постусловии константы 0 на переменную /, которая всегда принимает четное значение, и модификацией результирующего утверждения, принимающей во внимание то, что значения и [0 i/'— II остаются исходными. О ./ 2*/-1 \h-j/2 Дыр, //2значений ? /-у/2 дыр, /—у/2 значений! На каждом шаге значение, предшествующее u[h], перемещается туда, где онс должно оказаться в конце программы, и перед ним помещается дыра. Программа имеет вид {Даны *, v, isgap и k, удовлетворяющие (19.3.5), распределить значения таким образом, чтобы каждому из них предшествовала одна дыра и было выполнено (19.3.4)}
Ответы к упражнениям 399 ргос config(value i% k: integer; var v: array of integer] var isgar: array of Boolean)] begin var /i, /: integer] К j •= i + k, 2*r, {инвариант: приведен выше; ограничение: j— h) do h=£j-^h, j:=h—\y /—2; do j'sga/? [/i]—>h:=h—1 od; \v[h] является значением} v[j], v[j+l]:=v[h]y v[h]] isgap [/], isgap [/+ 1] := 7\ F od end 2. Программа (19.3.2) для нахождения приближения к квадратному корню неотрицательного целого числа п имеет вид {/7>0} а, с :=0, 1; do с2^.п—*с:=2*с od; {инвариант: а2^п < (а + с)2Л(н/7:1 ^Р".с = 2^} do с=й= 1 —*с '-= с/2] tf (а + с)2^1 п —+ а := а-\-с U(a + c)2>n-+skip П od {а2^п< (а+1)2} Попытаемся проиллюстрировать, как найти такое изменение представления данных, чтобы устранить операцию возведения в квадрат. Переменные а и с должны быть представлены через другие переменные и устранены, и тогда возведение в квадрат перестанет быть необходимым. В качестве первого шага к этому заметим, что операцию возведения в квадрат с2 можно выполнять и другим способом. Можно воспользоваться новой переменной р, которая будет всегда удовлетворять соотношению р=с2. А теперь ответьте, какие операции, включающие с, можно легко заменить операциями над Команда с := 1 может быть заменена на р := 1, выражение с2 на р, с := 2*с на р := 4*/?, с^=1 на рф\ и с := с/2 на р : = р/4. Операторы, которые еще нужно перестроить,— это а := О (если необходимо), а := а+су (а+с)2^.п и (а+с)2>я. Рассмотрим два последних выражения, включающих возведение в квадрат. Произведя преобразование (а+с)2=а2+2а*с+с2, выделяем еще одно с2, которое заменяется на р, и первый из перестраиваемых операторов запишется в виде (Е1) а2 + 2*а*с + р — я<0
400 Ответы к упражнениям Должно быть перестроено и выражение (Е1) при помощи новых переменных и таким образом, чтобы могла быть перестроена и команда а := а+с. Каковы возможные новые переменные и их смысл? Есть несколько возможностей, например q=a2, q=a*cy q=a2—п и т. д. Многообещающим выглядит определение (Е2) q=a*c поскольку оно позволяет нам заменить почти все операторы, включающие а. Таким образом, перед главным циклом q должно быть нулем, так как а в данном месте равно 0. Далее, для того чтобы сохранить (Е2) при с := с/2, можно вставить оператор q := q/2. И наконец, при выполнении команды а := а+с (Е2) сохраняется присваиванием нового значения.Каково это значение? Для определения значения х, которое нужно присвоить q, вычислим wp("a, q := а + с, x"f q = a*c) = x=(a + c)*c Искомое присваивание, следовательно, имеет вид q := (а + с) *с, что эквивалентно q := а*с + с2, что эквивалентно q-=q + P При таком представлении данных (Е1) становится следующим оператором: (ЕЗ) a2 + 2*q + p—n<0 А теперь попытаемся ввести третью переменную г> содержащую значение п—а2, которое всегда будет ^0. При этом (ЕЗ) превращается в 2*q + p—r<0 И конечно же, определение г также может быть легко сохранено. Подытоживая все это, получаем, что использованы три переменные /?, q, г, удовлетворяющие условиям р = с2, q = a*c> г = п — а2, и программа перестраивается следующим образом: /?, q, г := 1, 0, п\ йор^п—+ р :== 4*р od; do рф\ -*/?:= /?/4; q:=q/2\ if 2*q + p^ir —► q, r := q + p\ r — 2 *q—p []2*q + p > r —+skip fi od В момент ее завершения имеем р = 1, с=1, q~a*c=a> так что искомый результат получается в q. Была устранена не только операция
Ответы к упражнениям 401 возведения в квадрат, но и все умножения и деления теперь производятся лишь на 2 и 4. Следовательно, на двоичной машине они могут быть реализованы при помощи сдвигов. Таким образом, данная программа нахождения приближения к квадратному корню может быть выполнена с использованием лишь сложения, вычитания и сдвигов. 3. Целесообразно предполагать, что z=Xy будет установлено при помощи некоторого числа умножений, поскольку на самом деле Ху есть X , умноженное Y раз само на себя. Это приводит к возможности инициализации z единичным элементом умножения, т. е. 1, и использования инварианта г*/? :=ХУ, где р — новая переменная. Однако остается проблема инициализации /?, и она столь же тяжела, как и исходная задача. Возможно, р удастся представить через другие переменные. «Очевидный» выбор этих переменных — воспользоваться двумя новыми переменными х 4и у, имеющими определение р=хУ, поскольку такое определение может быть легко установлено путем х, у := Ху YI А это прямо приводит к программе из разд. 19.1 х, у, z:= X, У, 1; {инвариант: Р:0 < у/\г*х? = Х¥\ {ограничение: I +ceil(logy)\ do у > 0/\even (у) —> г/, х:= у -f- 2, х*х []y>0Aodd(y) -+yy z:=y—l, z*x od Ответы к гл. 20 v 1. Назовем последовательность (нулей и единиц), начинающуюся с 0000 (четырех нулей) и удовлетворяющую свойству 2, хорошей. Последовательность из k битов будем называть й-последователь- ностью. Следующим образом определим упорядочение на множестве хороших последовательностей. Последовательность меньше, чем последовательность si (записывается sl.<C s2)f если при рассмотрении этих последовательностей как двоичных чисел с запятой слева от первого члена si меньше, чем s2. Например, 101.<.1011, поскольку 0.10К0.1011. Подобным же образом, 101. = .101000, поскольку 0.101=0.101000. Добавление к последовательности нуля дает равную ей последовательность; добавление единицы дает большую последовательность. Все хорошие последовательности, которые необходимо распечатать, удовлетворяют условию 0.<.s.<.00001 и должны начинаться с 00000. Программа, приведенная ниже, последовательно порождает в порядке возрастания все хорошие последовательности, удовлетво-
402 Ответы к упражнениям ряющие условию O.^.s.^.00001, печатая все порождаемые 36-битовые последовательности. Последовательность, рассматриваемая в данный момент, будет обозначаться s. Переменной s не будет; s — просто имя для рассматриваемой в данный момент последовательности, s всегда содержит не менее 5 битов. Более того, чтобы устранить проблемы, возникающие для равных последовательностей, мы всегда будем обеспечивать, чтобы s было самой длинной хорошей последовательностью среди равных. PI: xopoiua(s)A ~]xopouia(s|0)Д5<|s|A0.<.s.<.00001 Л все хорошие последовательности . <.s напечатаны Последовательность s, состоящую из п битов, можно было бы представлять как массив битов. Но лучше представлять s как массив целых чисел с [4 : п—1], где с [i] — десятичное представление 5-битовой подпоследовательности s, заканчивающейся в бите i. Таким образом, в качестве части инварианта главного цикла будет сохраняться утверждение Р2:5<п = |sК36д(у':4 <i<n:c[i] = = s[i — 4]»24 + s[t — 3]»23 + s[i — 2]»2a + s[i— l]#2+s[i]) Далее, для того чтобы сохранять след тех 5-битовых последовательностей, которые содержат s, воспользуемся булевым массивом in [0:31]: P3:(vr.0<t <32:w[t] = (t'644:^—'])) А вот и алгоритм: л, 44L in[0]:=b,0,T] ш[1:31]:=Р; {s= (0,0,0,0,0)} {инвариант: Plf\P2/\P3/\') xopouia{s|0)} do с[4] Ф 1 —► if п = 36 —► печатать последовательность s □ п Ф 36 —-► skip Н; Заменить s на следующую большую хорошую последовательность: do in[(c[n—l]*2+ 1) mod32] {(т. е. ~\хороша (s\l))\ —> Устранить из s завершающие единицы: doodd(c[n—l])-^n := n—l\in[c[n]]:==F od; Устранить из s завершающие нули: [п:=п — 1; in[c[n]]:=F od; Добавить 1 к s: с[п\: = (с[п—I]*2 +\) mod 32; in[c[n]]: = T\ п:= п+ 1 od
Ответы к упражнениям 403 2. В ответе к упр. 2 к гл. 14 определена следующая большая перестановка элементов Ь. Пусть b [i\ — самая левая цифра из тех, которые нужно изменить; 0^а<Аг—1Д* — наибольшее целое число, удовлетворяющее ьихьи+и Следовательно, b li+l i n—11 — невозрастающая последовательность. Пусть b [/] — то значение, которое придается в следую- щей большей перестановке: / — наибольшее целое число, удовлетворяющее условию b U'l< <b [/I. Искомая программа дана ниже. Заметим, что первые два цикла поиска / и/начинаются с правого конца Ь. Если ищется наибольшее число, удовлетворяющее некоторому свойству, принцип линейного поиска говорит о том, что надо начинать поиск с самого верхнего числа и двигаться вниз. Инварианты первых двух циклов не нужны, поскольку эти циклы являются простыми применениями принципа линейного поиска. Определяем i (при помощи линейного поиска): г. = п—2; do b[i] > b[i + 1J -^ i: = /— 1 od; Определяем / (при помощи линейного поиска): j:=n— l; do b[q^b[j]-+j:= ,—1 od; b\i\ b[j]:=b[j], b[i]; {b[j + 1 in— 1] — неубывающая последовательность, искомая следующая большая перестановка—b[6:i]\reverse(b[i + 1 :п— lj)} Обращаем b\i+l:n— 1]: Л, k := /+ I, п—1; {инвариант: b[m:k] еще предстоит обратить} do h<k-^b[h]t b[k]i=b[k], b[h\\ A, k:=h+l, k—l od 3. Пусть п-последовательность" обозначает последовательность длины п. Пусть "хороша^)" означает "s является хорошей последовательностью". Результирующим утверждением является R\ хороша (Ь [0 .' п— И) Л не существует меньшей хорошей ^-последовательности где «меньшая» понимается в смысле упорядочения, определенного в условии упражнения. Программа последовательно порождает большие хорошие последовательности (в порядке, определенном в условии), начиная с пустой, до тех пор, пока не будет порождена хорошая ^-последовательность. Инвариантом главного цикла программы является
404 Ответы к упражнениям Р : 0<&<я/\хорошаф [0 : k— \X)/\ не существует хороших ^-последовательностей, меньших Ь [0 : k—1] При попытке породить следующую большую хорошую последовательность необходимо проверять, является ли последовательность Ь [0 : k] хорошей, зная, что b [0 : k—1] является хорошей. А это требует проверки лишь того, что прилежащие друг к другу последовательности, содержащие Ь Ik], хороши. Таковы следующие пары последовательностей: ф [k—\ : k— 1], b Ik : &]), (blk—3 : k—2], blk—l: k])y (b[k—5 : k—3], b [k—2 : k\) и т. д. В куске программы, помеченном "установить Хороша в хорошаф [0 : &])", проверяются эти смежные пары в перечисленном порядке. Для выполнения проверки пар на равенство используется операция высокого уровня. Если читатель желает, он может ее конкретизировать далее. k:=0 {инвариант: Р} do k фп—> Заменить b[0:k— 1] на следующую большую хорошую последовательность: b[k\ Хороша := 0, F\ {хорошаф[0:1г — 1])Л 1 хорошаф\0:к])АЬ\0:к — I] . = .b[0:k]\ {инвариант: хорошаф[0: k — 1 ]) Л (Хороша = хоро- ша(Ь[0:£]))Л не существует хороших п- последовательностей, меньших Ь[0:&]} do "~| Хороша —> Устранить конечные тройки из b[0:k]: do b[k] = 3-+k:=k—\ od; b[k]:=b[k] + \'y Установить Хороша = хорошаф[0:к]), предполагая хорошаф[0-Л— 1J): Хороша, m, d := 7, £+ 1, £ + 1; {инвариант: в b[m:/?j нет равных лежащих рядом подпоследовательностей Л even(k—m+\)/\d=(m+k+ l)-^2} do Хороша/\\ <^т—+т, d := ш—2, d-f 1; Хороша := 6[m:d—l]^=fe[d:/] od od; od 4. Результирующее утверждение можно записать довольно неформально ./?:Ь[0:/?-— 1] правильно записано в строки[0:чи- строк— 1, 0:длстроки— 1]. Инвариант главного цикла программы приведен ниже (дополнительная целочисленная переменная &е; служит для экономии вычислений). 0 ^ i ОЛО^ k ^dAcmpOKU/\b[0:i~- 1J правильно записано в строки[0:Н— 1,*] и строки[1г, Q:k]/\b[w:k— 1] чаетич-
Ответы к упражнениям 405 но построенное слово (т. е. w = k< если нет остающегося куска слова) I", /г, k, ш:=0,0,0,0; do 1фп—► if &[/] = "— Вставить пробел в строки\\г\, если нужно: if 0 < k <Сдлстроки cand строки[ку k—1] = " —+cmpoKu[h, k]y k: = ", k+l D"1(0 <k<C длстроки) cor строки[ку k—Цф" —► skip fi w:= k []b[i] = 'NL' —> cmpoKu[h, k: длстроки—1] : = "; А, /г, ш:=А+1, О, 0 U'A' < &[i] <'Z' — Bcmaeumb(b[i]) *: =t+1 od где Вставить(р) определяется следующим образом: {Добавить символ &[*'] не являющийся пробелом, к строке, сохраняя истинность инварианта:} Вставить(р): Перенести слово строки[ку w:k—1] на следующую строку, если b[i] не помещается на текущую: if k Ф длстроки Vw = k --* skip \]k = длстроки Л w < k —* строки[Н +1,0: k—w— 1] : = строки [/г, w:k— 1J; строки [/г, k: длстроки— 1J := "; ft, k, w:= h + 1, k—w,0 H; строки [h, k], k, w:=p, k+l, min(w, k—1) 5. Смотри главу 21. 6. Смотри главу 21. 7. Результирующим утверждением является R:c=(W:0^i<F:f[i]ig[0:G-l]) + (Nj:0^j<G:g[j)$f[0:F-l]) Мы желали бы написать программу, проходящую по обоим нашим массивам снизу вверх некоторым согласованным способом и по ходу дела производящую подсчет. Таким образом, имеет смысл построить инвариант путем замены констант F и G на переменные следующим образом: 0</z</rA0</e<GA с = (W:0 < i < h:f[i] (g[0:G — I]) + (N/:0</<*:g[/]$/[0:F-l])
406 Ответы к упражнениям А теперь рассмотрим исполнение h := h+l. При каких условиях оно сохраняет истинность инварианта? Охрана этой команды должна влечь / [h]^g [0 : G—1], но хотелось бы, чтобы она была простой. В том виде, в котором условие находится сейчас, нахождение такой охраны выглядит достаточно нелегким делом. Может быть, усиление инварианта позволит нам найти простой алгоритм? Еще одно соображение, которое мы не попытались использовать,— то, что движение по массивам происходит согласованным образом. Нынешний инвариант этого вообще не влечет. Предположим, что к инварианту добавлены условия / [h— \\<g[k] и* g [k— 1]</ [h] — они могли бы обеспечить желаемую согласованность поисков. Итак, воспользуемся инвариантом P:0^h^FAO<;k<GAf[h-l]<g[k]Ag[k-l}<f[h] c^(Ni:O^i<h:f[i]^g) + (Ni:O^l<k:g[j]0 Тогда из дополнительного условия f[h\ < g[k] получается g\k—1] < j[h] < g[k\y так что f[h\ не может встречаться в g", и увеличение h сохранит инвариант. Подобным же образом охраной для k:=k+\ будет g[k] < f[h]. Все это дает искомую программу, приведенную ниже. Предполагается существование виртуальных значений /[—l] = g[—1]= = — оо и f[F] = g[G] = +°°; это позволяет обойти трудности, связанные с граничными условиями инварианта. A, k, c:=0, 0, 0; {инвариант:Р\ ограничение:F—p + G—q) do f=£FAg=^G-^ if/[A]<g[£] —А, с:=А+1, c+l №] = «[*] — *, A:=A+.l. k+l Uf[h]>g[k]-+k,c:=k+l. c+l fi od; Добавить к с число непросмотренных элементов f и gi с ? = c+F—h+G—k 8. Отличие между данной задачей и предыдущей то, что повторения могут присутствовать в массивах, но не должны считаться. В новом результирующем утверждении R подсчитываются лишь такие индексы i и /, для которых / [i]=g[j] и /U], glj] отличны от их предшественников. Опять-таки предполагаем существование виртуальных значений /[—11, g\—11, ДЛ, glG]. c = (№0^i < Fif[i-l]^f[i]Af[{\$g) + (Нг.0<>1 <GifAJ-l^g[j]/\&l]$f) Тогда инвариантом является Р:0 < ■'; < FA0 < k < 6 Д/[А-1] Ф /[А]Д в[*-1]^в[*]Л/[А-1]<ЙА]Л8[*-1]</[А]
Ответы к упражнениям 407 (Nj:0^j<k:g[j-l]^g[j]Ag[lW) К fc, c:=0, 0, 0; {инвариант: Р\ ограничение: F — p + G—q\ do f^FAg^G-^ Uf[b]<g[k]-+ h, c:=A+l, c+l Uf[h] = g[k]-^hy k:=h+l, k+l D/I fl; .'*]>*[*] — *. c:=A+l, c+l Восстановить /[A— 1J ф f[h]Ag[k— 1 ] Ф g[k] do /i^O cand кф¥ cand /[Л — 1 ] —/[Л] —► Л:=Л + 1 od; do кфЪ cand ^^=G cand g[k—l] = g[k] -* Л:=А+1 od od 9. В этом упражнении еще раз иллюстрируется необходимость предварительного анализа задачи и формулировки аккуратных, точных определений: главные трудности могут встретиться при анализе задачи, а не при составлении самой программы. Сначала должна быть определена последовательность цифр di в десятичном разложении .d,d2. . . дроби \1п. Некоторые идеи по поводу ее определения можно извлечь, вспомнив то, как мы сами делим 1 на п—«ручное» деление. Довольно просто вычисляется, что 10//1 = 10 -f- n + (10 mod n)/n Следовательно, dx = 10 -f- /г. Положив rt равным 10 mod д. видим r1 = .d2d3d4... Повторение этого процесса приводит к d2= 10 я^ -г- п /■я=10*г2-т-/г; r2/n + .d3d4d5... и т. д. Если принять г0=1, можно записать следующие равенства: при i> 0 г. = (10 * г,-. г) mod я, d/ = (10 * r^j) -т- /г А теперь отметим, что каждое dt зависит лишь от п и rimmii a /'/_! удовлетворяют условию 0^/*/_1<я. Следовательно, исходная неповторяющаяся часть десятичного разложения может состоять не более чем из п различных цифр. А это значит, что dn+l~(l0* rn)modn лежит в повторяющейся части. Если rn+J первый остаток после гп, равный гп, то длина повторяющейся части — /, Все это приводит к программе Вычислить т — гп: i, гп := 0, 1 {инвариант 0 ^ / <; /г Д /-п = /-,-} do i < п—►/, rn:=i+l, (10* гп) mod n od;
408 Ответы к упражнениям Определить следующий остаток гп+у, равный т /\ ri-=l, (10*/71) mod я; do тфг\ —* /, г/:=/+1, (10* г/) modrcod {/ является длиной периода \/п\ Эта программа исполняется за время, пропорциональное n+j. Программу, имеющую время исполнения 0(/+log n), можно построить, воспользовавшись дополнительными свойствами десятичного разложения. Это опять подчеркивает то, что, чем больше мы знаем об объектах, с которыми работаем, тем лучше (более эффективной и т. д.) может получиться программа. Имеет место 1/д = (d+r)/(M-\) 10/* где d—начальная последовательность из i цифр, а г—периодическая часть из / цифр. Например, \/6 = — + -^~>== .16666... так что d=l, /=1, г = 6, /=1. Преобразованием этих соотношений получаем Ю'* (10>— \)/п =d*(10' — 1, + л Поскольку правая часть данного равенства является целым числом, им является и правая часть. Если устранить из п все делители 2 и 5, то результат (назовем его т) должен являться делителем 10^'—I, состоящим из / девяток. Следовательно, для нахождения / нужно лишь выполнить «ручное» деление 9.. .9/т, до тех пор пока остаток не станет равен 0. А это приводит к следующей программе: т := п\ Устранить из т множители 2 и 5: do m mod 2 = 0—>т := т/2 od; do m mod 5 = 0—>т : = т/5 od; /f k : = 1, 9 mod m\ {инвариант 0</Д/^ длина периода \/п f\k=.k(\QJ'—1) modm} do кф0-+ j, k := / + 1, (k* Ю + 9) mod m od {инвариант /\ k = 0\ 10. Пусть 5(/?, q) обозначает последовательность значений (£[0]. •••! g[p]' hi> •••» ^)» расположенных в возрастающем порядке. Результирующим утверждением является: R:x[0:2*N — 2] содержит S(N—l, N—l) Ясно, что требуется никл, и имеет смысл написать его таким образом, чтобы элементы помещались вх в порядке их величин. Воспользуемся тем, что g[k] < hk. В сущности, инвариант строится заменой на переменные двух констант N из R.
Ответы к упражнениям 409 P:0<i<# Л0</<ЛГЛ Л 1<£<2*/V — I х[0:£—1] содержит S(i— 1, j—\)/\hj — hj Программа имеет вид *[0], *. /'. * — 8[Ч 1. 1. 1; Л/ := *[0] + £[1]; {инвариант: Р; ограничение: N — 1 + A1 — /} do i=£N—+ if £[']<Л/—'. 4Н A := t'+l, £[t], A+l D g\}] > Л/—•/, A/, *[*], « := / + l,/i/ + g[/+ 1], Л;,/.. + 1 fi od {инвариант: P/\i = N\ ограничение: N—/} do /##-*/, hj, x[k], k : = /+1, hj + g[i—\], hj, k+1 Эту программу можно записать также и в виде *[0], i, /, k := g[0], 1, 1, 1; А/ ■■= x[0] + g[4, {инвариант: Р\ ограничение: N — i + N— /} do i^N cand g[i] < hj ->/, x[k], k : = i+l, g[i], k+1 D (i = N /\}ФЫ)\' {1фп cand g[i]>hj) -+j, N, x[k], k := /+1, hj + g\j+\\ hj, k+l od 11. Значение ху удовлетворяет условию ^ = (П f:0<f<*:Hfr, = 0 then 1 else x2<) Следовательно, результирующим утверждением /? является г«(П*:0<*<*» if &/«0 then l else x2i) Инвариант цикла обработки Ъ{ в убывающем порядке значений индексов получается путем замены константы 0 постусловия R на переменную / Р:0 < / < k Л 2=(H/:/<i<ft:if ft,«0 then 1 else *•') Тогда программа записывается следующим образом: /, г: = А, 1: do /=^=0->/, г :== /—1, г*г; if £.= 1 —► £ := г*л: [] bj^O—^skip fi od
ПРИМЕЧАНИЯ ПЕРЕВОДЧИКА 1} Здесь история излагается несколько упрощенно. Логику начинали иссле* довать алгебраическими методами еще до Буля (Лейбниц, де Морган и другие). Буль первым систематически исследовал класс алгебр, названных впоследствии •его именем (булевы алгебры), и дал им интерпретацию в виде подмножеств универсума (множества всех рассматриваемых объектов). 2) В частности, в связи с тем ito в разных учебниках и языках программирования имеются прямо противоположные соглашения о старшинстве л и v , мы в переводе восстановили все скобки, использовавшие соглашение о том, что л сильнее v. 3> Оговорки о конечности множества идентификаторов у автора не было. Однако без нее все дальнейшее рассмотрение неправильно. Достаточно рассмотреть хотя бы множество из бесконечного числа состояний, в каждом из которых одному из бесконечного множества идентификаторов сопоставляется Т. Его нельзя описать высказыванием в рассматриваемом здесь смысле. 4) Здесь автор не вполне точен. Строго говоря, парадоксы, рассмотренные автором, не зависят от закона исключенного третьего. Наличие промежуточных или неопределенных логических значений не противоречит закону исключенного третьего; например, можно построить четырехзначную логику, сохраняющую все свойства булевой логики. Последняя фраза автора более существенна. Использование методов математической логики XX в. стало потребностью многих отраслей науки. Однако стиль рассуждений и весь мир логики сильно отличаются от мира обычной математики. В частности, логик четко осознает относительность обычаев, согласно которым некоторые математические утверждения считаются истинными, а некоторые тексты — доказательствами. Возможны совершенно другие системы таких обычаев, столь же стройные, как традиционная булева логика и классическая математика. Столкнувшись с оговорками об неуниверсальности логических законов, человек со стороны комментирует их иногда так: дескать, это объективные трудности, но меня-то они не касаются. К сожалению, касаются, и очень. Само доказательство знаменитой теоремы Гёделя о неполноте основано на построении формулы, выразимой в системе, используемой Грисом в следующих главах (с кванторами и числами), говорящей при некотором истолковании сама о себе: «Я не доказуема». Итак, утверждений, не являющихся ни истинными, ни ложными, не может избежать ни одна достаточно богатая теория. Именно в том, что закон исключенного третьего постулирует, что мы знаем все, а на самом деле мы знаем очень мало, и заключаются главные возражения против этого закона. 5) С этим правилом трудно согласиться в качестве универсального рецепта. Избавляться от импликации нужно лишь при проверке истинности некоторого высказывания, а это далеко не единственное, что приходится делать с высказываниями. 6) В одном месте автор применил сразу два закона вместо одного; найдите это место. 7) Это — упрощение, которое будет отмечено в следующем разделе. На самом деле главной особенностью естественного вывода, отличающей его от большинства других формальных систем, является блочная структура доказательства. 8) Хотя правила ~]-\ и ~~|-Е выглядят довольно похоже и крайне симметрично, на самом деле их добавление к системе остальных правил вывода нарушает ее внутреннюю симметрию; эти правила не являются взаимно обратными по отношению друг к другу, и правило удаления не есть следствие правила введения. Автор допускает здесь неточность, смешивая под одним именем два способа доказательства, в которых используется вывод противоречия из допущения. Приведение к абсурду (правило ~]-1 (3.3.9)) является определением отрицания, а рассуждение от противного (правило ~~|-Е (3.3. Ш)) есть главное препятствие при использовании обычной логики высказываний при построении программ. Оно дает возможность строить «чистые» доказательства существования, когда не строится -никакого объекта, существование которого утверждается,
Примечания переводчика 411 у> Автор не задает себе вопроса, а так ли уж страшно не получить некоторые тавтологии, например ру ~]/?, зато иметь логику, лучше отражающую специфику задачи программирования? 10) То, что из ложного допущения можно вывести ложное следствие, совсем не парадоксально. Это можно сделать, используя хотя бы доказанную нами теорему /?=>/?. Парадокс состоит в том, что из ложного высказывания можно вывести именно любое, даже логически с ним никак не связанное (пример Гильберта: «Если 2-2=5, то Луна сделана из зеленого сыра»). 11) Эта аналогия не совсем точна. Дело в том, что вызов процедуры требует задания параметра определенного типа, а здесь в качестве Е может стоять любое высказывание, даже более сложное, чем Е. 12> Формулировка теоремы неверна. Мы можем выписать не Е(е2), а последовательность строк, каждая из которых является следствием из предыдущих по правилам вывода, и последней из этих строк будет Е(е2). Однако мы можем доказать метатеорему, обосновывающую возможность введения правила подстановки, следующим образом: каковы бы ни были высказывания el, e2 и высказывание Е(р), записанное как функция идентификатора р, теорему Из el~e2, E (el) получить Е(е2) можно доказать в системе естественного вывода. i3) Это объяснение неточно. Соглашение об опускании скобок в последователь- • ностях Л " V было неформальным соглашением, введенным в гл. 2 на основе законов ассоциативности. В нашей системе мы не можем пока что утверждать, что а/\Ьлс=(алЬ)Лг. Соответствующие упражнения добавлены в конец раздела. Здесь мы дадим часть доказательства закона (ауЬ)ус-ауфус). Из (a у b) у с получить a у ф у с) I Из a у b получить а у ф у с) Из а получить a у (b У с) 1.1.1 | а У (Ь У с) а=2>а V (Ь V с) Из Ь получить а V (Ь V с) V-I, пос 1 =>1, 1.1 1.3.1 \ b V с 1.3.2 | а V (b у с) Ь=$а V (b V с) а V (Ь V с) V-I, пос 1 V-I, 1.3.1 =М, 1.3 V-E, пос 1, 1.2, 1.4 avbz=^>av(bvc) Из с получить a v (b V с) =Ф-1, 3.1 \ b V с 3.2 I a v (Ь ус) с=$а V (Ь V с) а У фу с) V-I, пос 1 V-I, 3.1 =£-1 3 V-E, пос 1, 2, 4 14> Неточность. Т и F, как говорил выше сам автор, представимы в формальной системе, например, через (а=>я) и (аЛ ~~\а) соответственно. Доказательство этих законов также необходимо, и оно добавлено в качестве упражнений. 15) Формулировка этого упражнения уточнена, а упр. 13—25 добавлены переводчиком в связи с предыдущими замечаниями. 16) А главное, что и общее число их логических связок меньше, чем в е.3. 17) В данный момент доказательство можно закончить за один шаг, обратив внимание еще раз на то странное обстоятельство, которое отметил автор, и вспомнив один из результатов предыдущих разделов, использующих эту странность. Сделайте это.
412 Примечания переводчика Этот результат — теорема (3.3.13): "Из р, ~]р получить д". Используя ее в качестве схемы и подставляя q вместо р, а ~]р вместо q, получим требуемое. 18) К полезным соотношениям можно добавить также и следующие законы: а) Т cor E1=T б) F cand E1=F в) если значения всех идентификаторов, входящих в операнды cand и сог, определены (т. е. равны Т или F), то cand и сог можно без изменения значения выражения заменить на Л и v. 19> Эти выражения можно упростить, так как одна из замен перекрывается другой. Найдите перекрывающиеся замены, упростите выражения и исправьте их таким образом, чтобы получилось именно то, что имел в виду автор (три существенные замены). 20) Насчет обработки особых случаев не все так просто. Во-первых, Q в этом случае вырождается в тавтологию, если следовать предложенной автором методике, во-вторых, наивно думать, что мы можем предусмотреть все возможные случаи, и, в-третьих, задача пополнения алгоритмической функции по всюду определенной теоретически неразрешима и, следовательно, практически очень трудна. Здесь просто не может быть единого рецепта на все случаи жизни, ч это было бы лучше констатировать явно. 21) Это утверждение автора трудно рекомендовать для буквального восприятия. Предикаты, описывающие транслятор, исключительно трудно (из принципиальных, а не из технических соображений) выразить в языке исчисления предикатов. Этот язык просто не приспособлен для формулировки таких задач. Задача описания транслятора — лишь одна из достаточно широкого класса задач, описание которых с помощью предикатов рассматриваемого вида прагматически нецелесообразно, или вообще теоретически невозможно. 22) Рассмотрим подробнее разобранный автором пример. Отметим, что его новый прием дан в виде маленькой хитрости без обоснования. Разберемся с тем, что же происходит. Первое решение было бы правильно, если бы до того, как мы применили найденное выражение для *, оно бы не изменилось. Чтобы спасти найденное первым способом решение до того момента, когда оно пригодится, и введена в примечании автора вспомогательная переменная с. Однако с ее помощью получается решение, которое хуже приведенного в основном тексте. На самом деле здесь помогает прием, часто используемый в математике: если какая- то задача плохо решается, нельзя ли ее обобщить, с тем чтобы получилась легче решаемая задача? Оказывается, что рассматриваемую задачу обобщить можно. Пусть дана последовательность присваиваний хх := ег, . . ., хп := еп. В какое место этой последовательности целесообразно добавить присваивание х := 1, с тем чтобы выполнялось постусловие R (I обозначает некоторое выражение)? Эту задачу целесообразно решать в два этапа. Вначале следует воспользоваться приемом из примечания автора (для его примера мы получим с := а+1; а := а+1; Ъ := с) и затем продвигать присваивание с := Н, вперед до тех пор, пока не получится самое простое выражение для | (в нашем примере а после продвижения перейдет в а+1 и получится решение автора из основного текста). 23> Это определение содержит важную тонкость: предикат wp(DO, R) не является предикатом языка, определенного в ч. I, и выразим ли он на языке исчисления предикатов — это вопрос, который не имеет общего решения. 24> Этот принцип действительно нельзя считать вполне универсальным. Рекомендуемое автором правило больше подходит для циклов, в которых условия окончания указаны не явно, а понимаются как невыполнение одной из охран. Для языков, в которых условия выхода из цикла задаются в виде явно независимо существующего условия, стратегия программирования выглядит иначе. 25) Заметим, что то обращение, которое получено сейчас по формальным правилам обращения, в некотором смысле более сильное. Оно не использует того факта, что сегменты не перекрываются, и восстанавливает исходное состояние массива, казалось бы безнадежно изуродованного ошибочным применением программы перестановки равных для перекрывающихся сегментов. Вторичное применение самой программы перестановки лишь изуродует массив еще сильнее.
ЛИТЕРАТУРА 1. Allen L. E. WFF'N PROOF: The Game of Modern Logic. Autotelic Instructional Material Publishers, New Haven, 1972. 2. Bauer F. L., Samelson K. (eds). Language Hierarchies and Interfaces. Lecture Notes in Computer Science, v. 46, Springer, 1976. 3. Bauer F. L., Вгоу М. (eds). Software Engineering: an advanced course. Lecture Notes in Computer Science, v. 30, Springer, 1976. 4. Bauer F. L., Broy M. (eds). Program Construction. Lecture Notes in Computer Science, v. 69, Springer, 1979. 5. Burstall R. Proving programs as hand simulation with a little induction. Information Processing 74, North-Holland Publ. Co., Amsterdam, 1974, p. 308—312. 6. Buxton J. N., Naur P., Randell B. Software Engineering. Petrocelli, 1975. (Сообщение о двух конференциях НАТО, состоявшихся в Гармише, октябрь 1968 г., и в Риме, октябрь 1969 г.) 7. Constable R. L., O'Donnell M. A Programming Logic. Cambridge, 1978. 8. Cook S. A. Axiomatic and interpretative semantics for an Algol fragment. University of Toronto, CS TR 79, 1975. 9. Conway R., Gries D. An Introduction to Programming. Wintrop Publ., Cambridge, 1973. 10. DeMillo R. A., Lipton R. J., Perlis A. J. Social processes and proofs of theorems and programs. Communications of the ACM, v. 22 (May 1979), p. 271—280. 11. Dijkstra E. W. Some meditations on advanced programming. Proc. IFIP Congr. 1962, p. 535—538. 12. Diikstra E. W. Go to statement considered harmful. Communications ACM, v.'ll (March 196S), p. 147—148. 13. Dijkstra E. W. A short introduction to the art of programming. EWD316, Eindhoven, August 1971. 14. Dijkstra E. W. Notes on Structured Programming. In DahlO. J., HoareC. A. R., Dijkstra E. W. Structured Programming. New York, 1972. (Еото русский перевод: Э. В. Дейкстра в кн. Дал У., Дейкстра Э., Хоир ¥. Структурное программирование, М.: Мир, 1975. с. 7—97.) 15. Dijkstra E. W. Guarded commands, nondeterminacy and the tomal derivation of programs. Communications ACM, v. 18 (August 1975), p. 453—457. 16. Dijkstra E. W. A Discipline of Programming. Prentice Hall, Englewood Cliffs, 1976. (Есть русский перевод: Дейкстра Э. Дисциплина программирования. М.: Мир, 1978) 17. Dijkstra E. W. Program inversion. EWD671, Eindhoven, 1978. 18. Feijen W.H.J. A set of programming exercises. WF25, Eindhoven, July 1979. 19. Floyd R. Assigning meaning to programs. In Mathematical Aspects of Computer Science, Providence, 1967, p. 19—32. 20. Gentzen G. Untersuchungen iiber das logische Schlissen. Math. Zeitschrifft, v. 39 (1935), p. 176—210, 405—431. (Есть русский перевод: в кн. «Математическая теория логического вывода». М.: Наука, 1967, с. 9—76.) 21. Gries D. An illustration of current ideas on the derivation of correctness proofs and correct programs. IEEE Transactions on Software Engeneering, v. 2 (December 1976), p. 238—^44. 22. Gries D. (ed.). Programming methodology, a collection of articles by members of WG2.3. Springer, New York, 1978. 23. Gries D., Levin G. Assignments and procedure call proof rules. Theory of Programming Languages and their Semantics, v. 2 (October 1980), p. 564—579.
414 Литература 24. Gries D., Mills H. Swapping sections. TR 81-542, Cornell University, Ithaca January 1981. 25. Guttag J. V., Horning J. J. The algebraic specification of data types. Acta Informatica, v. 10, (1978), p. 27—52. 26. Hoare C.A.R. Quicksort. Computer Journal, v. 5 (1962), p. 10—15. 27. Hoare C.A.R. An axiomatic basis of computer programming. Communications ЛСМ, v. 12 (October 1969), p. 576—580, 583. 28. Hoare C.A.R. Procedures and parameters: an axiomatic approach. In Symposium on Semantics of Programming Languages, New York, 1971, p. 102—116. 29. Hoare C.A.R. Proof of correctness of data representation. Acta Informatica, v. 1 (1972), p. 271—281. (Есть русский перевод: в сб. «Данные в языках программирования», М.: Мир, 1982, с. 54—67.) 30. Hoare C.A.R., Wirth N. An axiomatic definition of the programming language Pascal. Acta Informatica, v. 2 (1973), p. 335—355. 31. Hunt J. W., Mcllroy M.D. An algorithm for differential file comparison. CS TR 41, Bell Laboratories, Murray Hill, New Jersey, 1976. 32. Igarashi S., London R. L., Luckham D. C. Authomatic program verification: a logic basis and its implementation. Acta Informatica, v. 4 (1975), p. 145—182. 33 Liskov В., Zilles S. Programming with abstract data types. Proc. ACM SIG- PLAN Conf. on Very High Level Languages, in SIGPLAN Notices, v. 9 (April 1974), p. 50—60. 34. London R. L., Guttag J. V., Horning J. J., Mitchell B. W., Popek G. J. Proof rules for the programming language Euclid. Acta Informatica, v. 10 (1979), p. 1—79. 35. McCarthy J. A basis for a mathematical theory of computation. Proc. Western Joint Сотр. Conf., Los Angeles, May 1961, p. 225—238. 36. Melville R. Asymptotic Complexity of Iterative Computations. Ph. D. Thesis, CS Department, Cornell University, Ithaca, January 1981. 37. Melville R., Gries D. Controlled density sorting. Information Processing Letters, v. 10 (July 1980), p. 169—172. 38. Misra J. A. A technique of algorithm construction on sequences. IEEE Transactions on Software Engineering, v. 4 (January 1978), p. 65—69. 39. Naur P. et al. Report on the algorithmic language ALGOL-60. Communications ACM, v. 3 (May 1960), p. 299-314. 40. Naur P. Proofs of algorithms by general snapshots. BIT, v. 6 (1969), p. 310—316. 41. Quine W.V.O. Methods of Logic. New York, 1961. 42. Steel T. B. (ed). Formal language description languages for computer programming. Proc. IFIP Working Conference on Formal Language Description Languages. Vienna, 1964, Amsterdam, 1971. 43. Szabo M. E. The collected works of Gerhard Gentzen. Amsterdam, 1969. 44. Wirth N. Program development by stepwise refinement. Communications ACM, v. 14 (April 1971), p. 221—227,
ОГЛАВЛЕНИЕ Предисловие редактора перевода 5 Вступление 7 Предисловие 8 ЧАСТЬ О ЗАЧЕМ НУЖНО ИСПОЛЬЗОВАТЬ ЛОГИКУ И ДОКАЗЫВАТЬ ПРАВИЛЬНОСТЬ ПРОГРАММ? 12 ЧАСТЬ I. ВЫСКАЗЫВАНИЯ И ПРЕДИКАТЫ 17 Глава 1. Высказывания 18 1.1. Высказывания с полным набором скобок 18 1.2. Вычисление постоянных высказывание 19 1.3. Вычисление высказываний в данном состоянии 21 1.4. Правила старшинства для операций 22 1.5. Тавтологии 24 1.6. Высказывания как множества состояний 25 1.7. Перевод с естественного языка на язык высказываний 26 Глава 2. Рассуждения при помощи эквивалентных преобразований ... 28 2.1. Законы эквивалентности 28 2.2. Правила подстановки и транзитивности 31 2.3. Формальная система аксиом и правил вывода 34 Глава 3. Система естественного вывода 37 3.1. Введение в дедуктивные доказательства 37 3.2. Правила вывода 39 3.3. Выводы и подвыводы 44 3.4. Увеличение гибкости системы естественного вывода 53 3.5. Построение доказательств в системе естественного вывода .... 60 Глава 4. Предикаты 74 4.1. Расширение области значений состояния 74 4.2. Кванторы 79 4.3. Свободные и связанные идентификаторы 83 4.4. Подстановка 4 86 4.5. Кванторы по другим областям 89 4.6. Несколько теорем о подстановке и состояниях 92 Глава 5. Обозначения и соглашения, касающиеся массивов 94 5.1. Одномерный массив как функция 94 5.2. Сегменты массивов и картинки массивов 99 5.3. Обращение с массивами массивов . . . . , 102 Глава 6. Использование утверждений для документирования программ 105 6.1. Спецификации программ 105 6.2. Представление начальных и конечных значений переменных . . . 108 6.3. Наброски доказательств 109 ЧАСТЬ II. СЕМАНТИКА ПРОСТОГО ЯЗЫКА ПРОГРАММИРОВАНИЯ 112 Глава 7. Преобразователь предикатов wp 113 Глава 8. Команды skip, abort и композиция команд 119 Глава 9. Команда присваивания 122 9.1. Присваивание простым переменным 122 9.2, Кратные присваивания простым переменным 126
416 Оглавление 9.3. Присваивание элементу массива 129 9.4. Краткое присваивание общего вида 131 Глава 10. Команда выбора 136 Глава П. Команда повторения 143 Глава 12. Вызов процедуры 153 12.1. Вызовы с входными и выходными параметрами 154 12.2. Две теоремы о вызове процедуры 157 12.3. Использование параметров — переменных 162 12.4. Допуск входных параметров в постусловие 164 ЧАСТЬ III. ПОСТРОЕНИЕ ПРОГРАММ 167 Глава 13. Введение 167 Глава 14. Программирование как целенаправленная деятельность 176 Глава 15. Построение циклов, исходя из инвариантов и ограничений . . . 183 15.1. О первоочередности разработки охраны 183 15.2. Приближение цикла к завершению 189 Глава 16. Построение инвариантов 196 16.1. Теория воздушного шарика 196 16.2. Устранение конъюнктивного члена 198 16.3. Замена константы переменной 202 16.4. Расширение области значений переменной 209 16.5. Комбинирование пред- и постусловий 214 Глава 17. Замечания об ограничивающих функциях 219 Глава 18. Использование циклов вместо рекурсии 224 18.1. Сведение к более простым задачам 225 18.2. Разделяй и властвуй 229 18.3. Обход двоичных деревьев 232 Глава 19. Соображения эффективности 241 19.1. Ограничение недетерминизма 241 19.2. Вынесение утверждения из цикла 244 19.3. Изменение представления данных 249 Глава 20. Два больших примера построения программ 256 20.1. Выровненные строки текста 256 20.2. Максимальная восходящая последовательность 261 Глава 21. Обращение программ 268 Глава 22. Замечания о документации 278 22.1. Размещение программы при печати 278 22.2. Определения и описания переменных 286 22.3. Написание программ на других языках 289 Глава 23. Исторические замечания 297 23.1. Краткая история методологии программирования ....... 297 23.^. Задачи, использованные в книге 304 Приложение 1. Форма Бэкуса — Наура 307 Приложение 2. Множества, последовательности, целые и действительные числа 313 Приложение 3. Отношения и функции 318 Приложение 4. Асимптотические свойства времени выполнения программ 323 Ответы к упражнениям 326 Примечания переводчика 410 Литература 413