Вы находитесь на странице: 1из 276

А DISCl.

PLINE OF PROGRAMMING

EDSGER W. DIJКSTRA

Burroughs Research Fellow,


Prof essor Extraordinarius,
Technological University, Eindhoven

PRENТICE-HALL, INC.
ENGLEWOOD CLIFFS, N. J.
1976
мтгмгапческое
ОБЕСПЕЧЕНИЕ
эвм

З. .D.ейкстра
ВИСЦИПЛИНА
ПРОГРАММИРОВАНИЯ

Перевод с английского
И. Х. Зусман,
В. В. Мартынюка и
Л. В: Ухова

Под редакцией
Э. 3. Любимского

ИЗДАТЕЛЬСТВО
«МИР»
Москва 1978
УДК 681.142.2

Книга написана одним из крупнейших зарубежных специа-


листов в области программирования, известным советскому чи-
тателю по переводам его книг на русский язык (например, «Струк-
турное программирование», «Мир», 1972). Она посвящена фунда-
ментальным вопросам конструирования корректных и изящных
программ для ЭВМ. В ней предлагается методика формального
вывода программы из математической- постановки задачи. При
этом прослеживается развитие алгоритмов вплоть до создания
программ. Материал излагается в форме остроумных и поучи-
тельных задач по программированию.
Книга представляет значительный интерес для широкого кру-
га программистов.

Редакция. литературы по математическим наукам

Original · English language edition puЬlished Ьу Ргепйсе-


Hall Inc., Englewood Cliffs· New Jer.sey USA Copyright
@1976 Ьу Prentice-Hall Inc. Al1 Rights Reserved
20204-033 @Перевод на русский язык, «Мир», 1978
33-78
д 041(01)-78
ЛРЕДНСЛОВИЕ РЕДАКТОРА ПЕРЕВОДА

Программирование богато и многообразно. Ведь кажется


'Нет такой. сферы человеческой деятельности, где нельзя было
-бы с пользой применитъ вычислительную машину для оценки,
информационно-справочного обслуживания, планирования,
моделирования и т. п. И это многообразие задач переходит в
многообразие программ, которые должны разрабатывать
программисты, Они пытаются справиться с этим многообрази-
-ем, «заключив» его в проблемно-ориентированные языки про-
граммирования. Языки вбирают в себя специфические черты
конкретных сфер программирования - характерные структу-
'РЫ данных, принципы организации типичных процессов, соот-
ветствующую терминологию - и таким 'образом делают сам
процесс программирования более универсальным. Одновре-
менно они освобождают программистов от необходимости де-
талиаировать программы до уровня слишком мелких машин-
ных команд и даже от необходимости знать особенности кон-
кретных вычислительных машин. Более того, операционные
системы призваны превратить вычислительные машины из
предмета постоянного беспокойства в «существа», которые
сами заботятся о программисте и готовы оказывать всяче-
ские услуги ему и его программе.
И тем не менее, после того, как любая более или менее
сложная задача сформулирована (пусть даже в адекватных
и удобных терминах) и машина выбрана (пусть даже в самом
деле готовая к всевозможным услугам), каждый прогр.аммист
снова и снова остается один на один со своей собственной за-
дачей: ему нужно составить программу! Выбрать, как именно
следует расположить и связать данные в памяти, понять, ка-
кая именно последовательность операторов, - способных сде-
лать все что угодно и оттого одновременно и податливых и
опасных - выполнит поставленную задачу. И как организовать
Эти операторы в цикл, который будет с каждым шагом при-
ближать машину к намеченной цели. Выбрать, понять, изо-
брести, проверить, усомниться и повторить все сначала.
6 Предисловие редактора перевода

Показательно, что хотя в таких «внешних» разделах про-


граммирования, как языки и трансляторы, операционные си-
стемы и базы данных, широко развивается и используется тео-
рия (от матсм агической лингвнстикп 11 логики до статистики
и теории массового обслуживания), во внутренних, собствен-
ных разделах программирования госполствуют умение и ин-
туиция или в лучшем случае «полезные советы». Есть, прав-
да, некоторые исследования, касающиеся уже готовых
программ, но нет никакой теории самого процесса программи-
рования.
Предл агаем ая книга представляет собой один из первых
шагов в этом направлении. В ней с каждым оператором,
с их комбинациями и, в том числе, с циклами связываются
преобразования 'предикатов, являющиеся формальным выра-
жением их свойств, и определение свойств программы
в целом превращается в задачу логического вывода. Особен-
но важно, что автор объясняет и демонстрирует на примерах,
как этим формализмом можно пользоваться для практическо-
го программирования. При этом он, разумеется, не дает отве-
та на вопрос о том, как написать любую программу, - ответа
на этот вопрос вообще не существует. Он предлагает нам ин-
струмент, позволяющий проверять наши гипотезы в процессе
проектирования программ, а иногда и подсказывающий те или
иные варианты решений.
А это совсем не мало. Представьте себе человека, живу-
щего в эпоху зарождения математического анализа, которо-
му нужно находить неопределенные интегралы от весьма гро-
моздких подынтегральных выражений и который знает толь-
ко определение первообразной функ~ии и не знает никаких
приемов интегрирования. Каким для него было бы подспорь-
ем, если бы ему сообщили о правиле интегрирования по час-
тям или о таком приеме, как замена переменных! Его работа
не перестанет быть творческой, но насколько расширятся его
возможности и как много перейдет из области находок в об-
пасть техники. То, что было сложным, станет простым, то,
что было непосильным, станет только сложным. Сист ем атиче-
скос использование, обобщение и расширение этих приемов
позволит постепенно перейти к формализации, алгоритмиза-
ции, а затем и к автоматиза'цин отдельных этапов решения
или полного решения специальных классов задач.
Автор книги, Э. Дейкстра, не нуждается в. представле-
нии, - его работы хорошо известны советским программистам.
Вместе с Ч. Хоаром они недавно совершили поездку по круп-
нейшим городам нашей страны, во время которой выступили
с лекциями перед многочисленными аудиториями.
П редис.юеие редакл ора переводи 7

О структуре 11 стиле книги лосгаточно полно c1\a::!;i110 в


п релис.чонии л втор а. Т;1м ж с оп п рслу п рсжл.п-:', 111() 111[1,111>
его книгу трудно. Причина этого заключается в с.тож ност п
самих программ, послуживших для нее материалом Я хотел
бы добавить, что они сложны для нас только сеЙЧ3С, ьог л а
теория программирования делает свои первые шаги. Прилет
время, и такие программы сможет сост а вля гь (11.111 выноли гь)
прямо на уроке ка ж дый школьник I I чтобы это время при-
блпзптъ, надо осваивагь, виелрят ь 11 p<J {ВШ?ать теорию .\ э гот
процесс легко не проходит.
Перевод нрсдисловня и глав 1-7 выпо.тнен В. В· .\\ар-
тынюком, глав 8-21 - И. Х. Зусман, . глав 22-27-
Л. В. Уховым.

Э. З. Любимскии
.ПРЕДИСЛОВИЕ

Й:сtорики таких древних интеллектуальных дисцяп.лин~


как поэзия, музыка, живопись и наука, высоко оценивают
роль выдающихся практиков, чьи достижения обогатили опыт
и расшир)IЛИпредставления поклонников этих дисциплин,
пробудили и укрепили таланты последователей. То новое, что
ими внесено, основывается на сочетании виртуозного практи-
ческого мастерства и проницательного осмысливания фунда-
ментальных принципов. Во многих случаях влияние этих лю-
дей усиливалось благодаря их высокой культур~, богатству
и выразительности их речи.
В этой книге в присущем ее автору утонченном стиле пред-
ставлена принципиально новая точка зрения на существо
программирования. Исходя из этой точки зрения, автор раз-
работал совокупность новых методов программирования и
средств обозначения, которые демонстрируются и проверяют-
ся на многочисленных изящных и содержательных примерах.
Этот труд, несомненно, будет признан одним из выдающихся
дости)Кений в разработке новой интеллектуальной дисципли-
ны - программирования для вычислительных машин.

Ч. А. Р. Хоар
ОТ АВТОРА

У.Же давно мне хотелось написать книгу такого рода. Я


знал, что программы могут очаровывать глубиной своего ло-
гического изящества, но мне постоянно приходилось убеж-
даться, что большинство из них появляются в виде, рассчитан-
ном на механическое исполнение, но совершенно непригодном
для человеческого восприятия- где уж .там говорить об изя-
ществе. Меня не удовлетворяло и то, что алгоритмы часто пуб-
ликуются в форме готовых изделий, почти без упоминания
тех рассмотрений, которые проводились в процессе разработ-
ки и служили обоснованием для окончательного вида завер-
шенной программы. Сначала я задумал изложить некоторое
количество изящных алгоритмов таким способом, чтобы чи-
татель смог прочувствовать их красоту; для этого я собирался
каждый раз описывать истинный или воображаемый процесс
построения, который приводил бы к получению искомой про-
граммы. Я не изменил своему первоначальному намерению в
том СМЬ{СЛе, что основой этой монографии остается длинная
последовательность глав, в каждой из которых ставится и
решается новая задача. Тем не менее 'окончательный вариант
книги существенно отличается от ее первоначального замысла,
поскольку возложенная мною на себя обязанность представ-
лять решения в естественной и убедительной манере повлекла
за собой гораздо большее, чем я- ожидал, и я навсегда сохраню
чувство благодарности еудьбе за то, что взялся за эту работу.
Когда начинаешь писать подобную книгу, сразу возникает
проблема: каким языком программирования пользоваться?
И это не только вопрос представления! Наиболее важным, но
в то же время' и наиболее незаметным свойством любого инст-
румента является его влияние на формирование привычек
людей, которые имеют обыкновение им пользоваться. Когда
этот инструмент - язык программирования, его влияние, не-
зависимо от нашего желания, сказывается на нашем способе
мышления. Проанализировав в свете этого влияния все извест-
ные мне языки программирования, я пришел .к выводу, что
ни они сами, ни их подмножества не подходят Для моих це-
лей. С другой стороны, я считал себя настолько не подготов-
10 От автора

ленным к созданию нового языка программирования, что дал


зарок не заниматься этим в ближайшее пятилетие, и я твер-
до знаю, что этот срок еще не вышел. (Прежде, помимо все-
го прочего, мне нужно было написать эту монографию.) Я по-
пытался выбраться из этого тупика, создав лишь подходящий
для моих целей мини-язык и включив в него только те элемен-
ты, которые представляются совершенно необходимыми и до-
статочно обоснованными.
Эти мои колебания и самоограничение, если их неправиль-
но понять, могут разочаровать многих потенциальных чита-
телей. Наверняка разочаруются все те, кто отождествляет
трудность программирования с трудностью изощренного ис-
пользования громоздких и причудливых сооружений, извест-
ных под названием «языки программирования высокого уров-
ня» или - еще хуже! - «системы программирования». Если они
сочтут себя обманутыми из-за того, что я вовсе не касаюсь всех
этих погремушек и свистулек, могу ответить им только одно:
«А вполне ли вы уверены, что все эти погремушки и свистуль-
ки, все эти потрясающие возможности ваших, так сказать,
«мощных» языков программирования имеют отношение к про-
цессу решения, а не к самим задачам?» Мне остается лишь на-
деяться, что, несмотря на употребление мною мини-языка, они
все же прочтут предлагаемый текст. · Тогда они, возможно,
признают, что помимо погремушек и свистулек имеется очень
богатое содержание и возникнет вопрос, стоило ли большин-
ство из них вообще придумывать. А всем читателям, интере-
сующимся преимущественно разработкой языков программи-
рования, я могу только принести извинения в связи с тем, что
еще не могу высказаться на эту тему более определенно. Тем
временем эта монография, возможно, наведет их на некото-
рые размышления и поможет им избежать ошибок, которые
они могли бы совершить, если бы не прочли ее.

Процесс работы над книгой, который явился для меня не-


прерывным источником· удивления и· вдохновения, привел к
появлению текста, основательно отличающегося от первона-
чального замысла. Сначала у меня было· вполне понятное на-
мерение представить построение программ с помощью аппа-
рата, ·чуть более формального чем тот, который я имел обык-
новение использовать в своих вводных лекциях, где семантика
обычно вводилась интуитивно, а доказательства правильности
представляли собой смесь строгих рассуждений, жестикуля.-
ции и красноречия. Разрабатывая необходимые основы для
более формального подхода, я обнаружил два неожиданных
обстоятельства.
(!т автора ll

Первая неожиданность состояла в том, что ~ак называемые


«преобразователи предикатов», выбранные мною в качестве
средства изъяснения, позволили прямо определять связь меж-
ду начальным и конечным состояниями без каких-либо ссылок
на промежуточные состояния, которые могут возникать во время-
выполнения программы. Я обрадовался тому, что это дает
возможность провести четкое разграничение двух основных
проблематик программирования: проблематики математиче-
ской корректности (речь идет о проверке, определяет ли про-
грамма правильное соотношение между начальным и конеч-
ным состояниями - и преобразователи предикатов обеспечи-
вают нам формальное средство для такого исследования без
рассмотрения вычислительного процесса) и инженерной про-
блематики эффективности (благодаря разграничению стано-
вится очевидным, что последняя проблематика определена
только в связи с реализацией). Пожалуй, самое полезное
открытие состоит в том, что один и тот же текст программы
всегда допускает две (в известном смысле дополняющие друг
друга) интерпретации. Интерпретация в виде кода преобра-
зователей предикатов, которая представляется более подхо-
дящей для нас, противостоит интерпретации в виде кода для
выполнения - ее я предпочитаю оставлять машинам!
Второй неожиданностью оказалось то, что самые естест-
венные и систематизированные «коды преобразователей пре-
дикатов», какие я мог себе представить, потребовали бы не-
детерминированной реализации, если рассматривать их как
«коды для выполнения». Вначале я содрогался от мысли, что
придется ввести недетерминированность уже в однопрограм-
мном режиме (слишком хорошо мне были известны сложности,
возникающие из-за этого в мультипрограммировании); однако
потом я понял, что интерпретация текста как кода преобразо-
вателя предикатов имеет право на независимое существова-
ние. (Оглядываясь назад, мы можем отметить, что многие
проблемы мультипрограммирования, ставившие нас прежде
в тупик, являются всего лишь следствием априорной тенден-
ции придавать детерминированности слишком большое зна-
чение.) В конце концов я пришел к тому, что стал считать не-
детерминированность естественной ситуацией, при этом де-
терминированность свелась к довольно банальному частному
случаю.
Установив эти основы, я приступил, как и намеревался, к
решению длинной последовательности задач. Это занятие ока-
залось неожиданно увлекательным. Я убедился в том, что
формальный аппарат позволяет мне ухватывать существо дела
.гораздо четче, чем-раньше. Я получил удовольствие, обнару-
жив, что явная постановка вопроса о завершимости может
12 От автора

иметь большое эвристическое значение; тут я даже начал со-


жалеть об излишне распространенной склонности к частичной
корректности. Но самое приятное состояло в том, что боль-
шинство решенных мною ранее задач теперь увенчалось более
изящными решениями. Я воспринял это как весьма ободря-
ющее свидетельство того, что разработанная методика и в са-
мом деле улучшила мои программистские возможности.

Как следует изучать эту монографию? Лучшее, что я могу


посоветовать: прерывайте чтение, как только усвоите поста-
новку задачи, и пытайтесь сначала решить ее самостоятельно,
затем продолжайте чтение. Попытка самостоятельного реше-
ния задачи представляется единственным способом прочув-
ствовать, насколько она трудна; кроме того, вы можете срав-
нивать мое решение с вашим и получить удовлетворение, если
ваше окажется лучше, Предупреждаю заранее: не огорчай-
тесь, когда увидите, что этот текст читается отнюдь не легко.
Те, кто изучали его в рукописи, часто испытывали затруднения
(но вполне вознаграждались за это). Впрочем, каждый раз
при анализе их затруднений мы совместно убеждались в том,
что «виновным» оказывался вовсе не текст ( т. е. способ из-
ложения}, а сам излагаемый материал. Мораль этого может
быть только. такова: нетривиальный алгоритм и вправду не-
тривиален, а его окончательная запись на языке программи-
рования слишком лаконична по сравнению с рассуждениями,
обосновывающими его разработку; эта краткость окончатель-
ного текста не должна нас дезориентировать. Один из моих
сотрудников внес предложение (а я довожу его до вашего
сведения, поскольку оно может оказаться полезным), чтобы
небольшие группы студентов изучали книгу вместе. (Здесь я
должен добавить в скобках замечание по поводу «трудности»
этого текста. Посвятив немало лет своей научной жизни тому,
чтобы прояснить задачи программиста и сделать их более
подвластными нашему интеллекту, я обнаружил с удивлением
·(и раздражением), что мое стремление внести ясность приво-
дит к систематическим обвинениям в том, что я «внес в про-
граммирование трудности». Но эти трудности всегда в нем
были; и только сделав их видимыми, мы сможем надеяться,
что научимся разрабатывать программы с высокой степенью
.надежности, а не просто «лепить команды», т. е. выдавать
-тексты, основанные на неубедительных предположениях, не-
состоятельность которых может выявиться после первого
же противоречащего примера. Незачем и говорить, что ни
-одна программа из этой монографии не проверялась на ма-
.шине.)
От автора 13

Я должен объяснить читателю, почему я пользуюсь мини-


языком, столь ограниченным, что в нем нет даже процедур и
рекурсий. Поскольку каждое следующее расширение языка
добавляло бы к этой книге еще несколько глав, тем самым со-
ответственно увеличивая ее стоимость, отсутствие большинст-
ва возможных расширений (таких, как, например, мультипро-
граммирование) не нуждается в дополнительных оправданиях.-
Однако процедуры всегда занимали такое важное место, а ре-
курсия для вычислительной науки в такой степени считалась
признаком академической респектабельности, что некоторое
разъяснение представляется необходимым.
Прежде всего эта книга предназначена не для начина-
ющих, и я рассчитываю, что мои читатели уже знакомы с ука-
занными понятиями. Во-вторых, книга не является вводным
текстом по какому-то конкретному языку программирования,
так'что отсутствие в ней этих конструкций и примеров их
употребления не следует объяснять моей неспособностью или
нежеланием ими пользоваться или же воспринимать как на-
мек на то, что вообще лучше воздерживаться от их примене-
ния. Просто они не потребовались мне для разъяснения моей
главной мысли о том, насколько существенно тщательное раз-
граничение проблематик для разработки всесторонне высоко-
качественных программ; скромные средства мини-языка
предоставляют нам более чем достаточный простор для нетри-
виальных и в то же время вполне приемлемых разработок.
Можно обойтись этим объяснением, но оно все же не яв-
ляется исчерпывающим. Я все равно считал себя обязанным
ввести повторение как самостоятельную конструкцию, по-
скольку мне представлялось, что это следовало сделать уже
давно. Когда языки программирования зарождались, «дина-
мическая» природа оператора присваивания казалась не очень
приспособленной к «статической» природе традиционной ма-
тематики. Из-за отсутствия соответствующей теории мате-
матики ощущали некоторые затруднения, связанные с этим
оператором, а поскольку именно конструкция повторения соз-
дает необходимость в присваиваниях переменным, математики
ощущали затруднения и в связи с повторением. Когда были
разработаны языки без присваивания и без повторений - та-
кие, как чистый ЛИСП, - многие почувствовали значительное
облегчение. Они снова ощутили под ногами знакомую почву
и увидели проблеск надежды превратить программирование
в занятие с твердой и солидной математической основой. (До
сего времени среди склонных к теоретизированию специали-
стов по машинной математике все еще широко распростране-
но мнение, что рекурсивные программы «более естественны»,
чем программы с повторениями.)
14 Or автора

Другого выхода из положения путем надежного и дейст-


венного математического обоснования пары Понятий «повто-
рение~ и «присваивание переменной» нам предстояло ждать
еще десять лет. А выход, как показано в этой монографии,
заключался в том, что семантику конструкции повторения
можно описать с помощью рекуррентных отношений между
предикатами, тогда как для описания семантики общей ре-
курсии требуются рекуррентные отношения между преобра--
аователями предикатов. Отсюда совершенно очевидно, почему
я считаю общую рекурсию на порядок более сложной конст-
рукцией, чем простое повторение; и поэтому мне больно смот-
-реть, как семантику конструкции повторения
«While В do S»
определяют как семантику обращения
«whUedo (В, S) »
к рекурсивной процедуре ( описанной в синтаксисе языка
АЛГОЛ 60):
procedure whiledo (исловие, оператор);
begin lf условие then begin оператор;
whiledo (условие, операзор] end
end
Несмотря на формальную правильность, это мне неприят-
но, потому что я не люблю, когда, из пушки стреляют по во-
робьям, вне зависимости от того, насколько эффективно пушка
может справляться с такой работой. Для поколения теорети-
ков машинной. математики, которые подключались к этой те-
матике в течение шестидесятых годов, приведенное выше ре-
курсивное определение часто является не только «естествен-
ным», но даже «самым правильным». Однако ввиду того, что
без понятия повторения мы не можем даже описать поведе-
ние машины Тьюринга, представляется необходимым произ-
вести некоторое восстановление равновесия.
По поводу отсутствия библиографии я не предлагаю ни
объяснений, ни извинений.
Благодарности. Следующие лица оказали непосредствен-
ное влияние на разработку этой книги, либо приняв участие в
обсуждении ее предполагаемого содержания, либо высказав
замечания относительно готовой рукописи или ее частей:
К. Брон, Р. Берсталл, У. Фейен, Ч. Хоар, Д. Кнут, М. Рем,
Дж. Рейнольдс, Д. Росс, К. Шолтен, Г. Зигмюллер, Н. Вирт и
От автора 15

М. Вуджер. Я считаю честью для себя возможность пу.блично


выразить им мою признательность за сотрудничество. Кроме
того, я весьма обязан корпорации Burroughs, создавшей мне
благоприятные условия и предоставившей необходимые
средства, и благода рен моей жене за неизменную поддержку
и одобрение.
Э. В. Дейксхра
Нейен
Нидерланды
О АБСТРАКЦИЯ ИСПОЛНЕНИЯ

Абстракция исполнения лежит в основе всего понятия «ал-


горитма» настолько глубоко, что обычно ее считают само со-
бой разумеющейся и оставляют без внимания. Ее назначение
в том, чтобы сопоставлять между собой различные вычисле-
ния. Иначе говоря, она предоставляет нам способ осмыслива-
ния конкретного вычисления как элемента большого класса
различных вычислений; мы ·можем отвлечься от взаимных от-
личий элементов такого класса и, руководствуясь определени-
ем класса в целом, высказывать утверждения, применимые к
каждому его элементу, а следовательно, справедливые 'и для
конкретного вычисления, которое мы хотим рассматривать.
Чтобы разъяснить, что подразумевается под «вычислени-
ем», я опишу сейчас невычислительную конструкцию «получе-
ния» (преднамеренно не употребляю термина «вычисление»),
например, -наибольшего общего делителя чисел 111 и 259. Она
состоит из двух картонных карточек, расположенных одна по-
верх другой. На верхней карточке написан текст «НОД
( 111,259) = ». Чтобы получить от конструкции ответ, мы под-
нимаем верхнюю карточку и кладем ее слева от нижней, на
которой теперь ·можно прочесть текст «37». '
Простота карточной конструкции является большим досто-
инством, но она омрачается двумя недостатками - мелким и
крупным. Мелкий недостаток состоит в том, что хотя эту кон-
струкцию можно в самом деле использовать для получения
наибольшего общего делителя чисел .} 11 и 259, но помимо
этого она мало на что пригодна. Однако крупный недостаток
в том, что, как бы тщательно мы ни проверяли устройство
конструкции, наша вера в то, что она вырабатывает правиль-
ный ответ, может основываться только на нашем доверии к ее
создателю: он мог ошибиться либо при проектировании своей
машины, либо при изготовлении нашего конкретного экземп-.
ляра,
Чтобы преодолеть меньшее затруднение, мы могли бы рас-
смотреть изображение на огромном листе картона большого
18 Глава О

прямоугольного массива из сетевых точек с целыми координа-


тами х и у, удовлетворяющими отношениям О~х~500 и
О~у~ 500. Для каждой такой точки (х, у) с положительны-
ми координатами ( т. е. за исключением точек на осях) мы мо-
жем выписать в соответствующей позиции значение НОД (х, у);
предлагается двумерная таблица из 250 ООО элементов. С точ-
ки зрения полезности это значительное усовершенствование:
вместо конструкции, способной выдавать наибольший общий·
делитель единичной пары чисел, мы имеем теперь «конструк-
цию», способную выдавать наибольший общий делитель для
.любой пары из 250 ООО различных пар чисел. Это много, но
особенно радоваться нечему, так как указанный ранее второй
недостаток (почему мы должны верить, что конструкция вы-
дает правильный ответ?). помножился на те же самые 250 ООО,
и теперь от нас требуется уже совсем безграничное доверие к ее
изготовителю.
. Поэтому перейдем к рассмотрению другой конструкции.
На таком же листе картона с сетевыми. точками написаны
только числа, пробегающие значения от 1 до 500 вдоль обеих
осей. К.роме того, начерчены следующие прямые линии:
1) вертикальные линии (с уравнением х=константа);
2) горизонтальные линии (с уравнением· у=констgнта) ~
3) диагонали (с уравнением х+у=константа); ·
4). «линия ответа» с уравнением х= у.
Чтобы работать на этой машине, мы должны следовать.
следующим инструкциям (еигратъ по следующим правилам»).
Когда хотим найти наибольший общий делитель двух чисел
Х и У, мы помещаем фишку - также поставляемую изготови-
телем - в сетевую точку с координатами х=Х и у= У. Коль.
скоро фишка не находится на «линии ответа», рассматриваем
наименьший равнобедренный прямоугольный треугольник, у
которого вершина прямого угла· совпадает с фишкой, а один
из концов гипотенузы (либо ниже фишки, либо слева от нее)
находится на одной из осей. (Поскольку фишка не лежит на
линии ответа, такой прямоугольный треугольник будет иметь.
"на осях только одну вершину.) Затем фишка перемещается в ,
сетевую точку, совпадающую с другим концом гипотенузы.
Такое перемещение повторяется до тех пор, пока фишка не
достигнет линии ответа. После этого х-координата (или у-ко-
ордината) окончательного положения фишки является иско-·
мым ответом.
Как нам убедиться в том, что эта машина будет выдавать
правильный результат? Если (х, у)..- любая из 249 500 точек
не на линии ответа и (х', у') - точка, в которую передвинется
·фишка эа один шаг игры, то либо х' =х и у'= у-х, либо
Абстракция исполнения 19

х'=х-=-у и y'={J. Нетрудно доказать, что НОД(х, у) =НОД


(х', у'). Важный момент здесь состоит в том, что одно и то же
рассуждение применяется одинаково верно к Любому из
249 500 возможных шагов! Кроме того, - и опять-таки без тру-
да - мы можем доказать для любой точки (х, у), где х= у
(т. е. (х, у) является одной из 500 точек на линии ответа), что
НОД (х, у) =х. И снова важный момент в том, что одинаковое
рассуждение применимо к любой из 500 точек на линии отве-
та. В-третьих, - и вновь это не составит труда - нам нужно
показать, что при любом исходном положении (Х, У) конеч-
ное число шагов в самом: деле перенесет фишку на линию от-
вета, и опять важно отметить, что одно и то же рассуждение
одинаково применимо к любому из 250 ООО исходных поло-
жений (Х, У). Три простых рассуждения, пространность кото-
рых не зависит от, числа сетевых точек: эта миниатюра пока-
зывает, насколько велики возможности математики. Если
обозначить через (х, у) произвольное положение фишки на
протяжении игры, начатой в положении (Х, У), то первая на-
ша теорема ·позволяет утверждать, что во время этой игры
отношение
НОД (х, у) = НОД (Х, У)
будет всегда справедливо, или - выражаясь на соответству-
ющем жаргоне - «оно сохраняет инвариантность» .. Далее, вто-
рая теорема гласит, что мы можем интерпретировать х-коор-
динату окончательного положения фишки как требуемый от-
вет, а третья теорема гласит, что такое окончательное
положение существует (т. е. будет достигнуто за конечное чис-
ло шагов). И этим завершается анализ того, что мы могли бы
назвать «нашей абстрактной машиной».
Теперь нам остается убедиться в том, что лист, поступив-
ший от изготовителя, является на са мом деле правильной мо-
делью абстрактной машины. Для этого нужно проверить ну-
мерацию вдоль обеих осей, а также проверить, правильно ли
проведены все прямые линии. Это несколько затруднительно,
так как предстоит исследовать объекты, число которых про-
порционально N, где N (в нашем примере 500) - длина сто-
роны квадрата, но все же предпочтительнее, чем №, число Б(')З-
можных вариантов вычисления.
Другая машина могла бы работать не с огромным листом
картона, а с двумя девятибитовыми регистрами, в каждом из
которых можно запомнить двоичное число от О до 500. При
этом мы могли бы использовать один регистр для запоминания
значения х-координаты, а другой - для запоминания значения
у-координаты, соответствующей «текущему положению фиш-
ки». Перемещение тогда соответствует уменьшению содержи-
20 Глава О

мого одного регистра на содержимое другого. Мы могли бы


-ееализовать арифметику самостоятельно, но, разумеется, луч-
ше, если машина сможет делать это за нас. Если мы захотим
полагаться на полученный ответ, то нам нужно уметь убеж-
даться в том, что машина правильно выполняет операции
сравнения и вычитания. В уменьшенном масштабе повторяет-
ся та же история: мы выводим единожды и на все случаи, т. е.
для любой пары п-разрядных двоичных чисел, уравнения для
устройства двоичного вычитания, а затем удостоверяемся в
.том, что наша физическая ~ашина правильно моделирует это
'абстрактное устройство.
Если это устройство параллельного вычитания, то число
проверок ........ пропорциональное числу элементов и их взаимо-
свяэейь- пропорционально значению n=log2N. В последова-
тельной машине сделан еще один шаг .на пути упрощения обо-
рудования за счет расхода времени.
. .
Теперь я попытаюсь, хоть бы для своего собственного про-
свещения, уловить основной смысл приведенного примера.
Вместо того чтобы рассматривать одиночную проблему
вычисления НОД ( 111, 259), мы обобщили ее и подошли к ней
как к частному случаю более широкого класса проблем вы-
числения НОД · (Х, У). Стоит отметить, что мы могли бы об-
общить проблему вычисления НОД (111, 259) по-разному:
можно было бы рассматривать эту задачу как частный случай
иного, более широкого класса задач, например вычисления
HOД(lll,259), HOK(lll,259), 111Х259, 111+259, 111/259"
111-259, 111 259, дня недели для 1111-го дня 259-го года нашей
эры и т. д. В результате мог бы появиться «процессор для
111 и 259», и для того, чтобы он выдал упомянутый выше от-
вет, нам следовало бы дать на его вход команду «НОД"
пожалуйста». Вместо этого мы предложили «НОД-вычисли-
тель», которому для получения этого ответа потребуется за-
дать на вход пару чисел «111, 259», и это совсем другая ма-
шина!
Другими словами, когда требуется выработать один или
несколько результатов, обычная практика состоит в том, что-
бы обобщить проблему и рассматривать эти результаты как
частные случаи некоего более широкого класса. Однако мало
радости, если ограничиться· утверждением, что любой предмет
является частным случаем чего-то более общего. Если мы хо-
тим следовать этому подходу, то на нас возлагаются две обя-
аанности:
1 . .Ni.ы должны иметь полную ясность относительно способа
обобщения, т. е. должны тщательно выбрать и явно опреде-
Абстракция исполнения 21

лить более широкий класс, поскольку наши рассуждения дол-


жны применяться ко всему этому классу.
2. Мы должны выбрать (еизобрестн», если вам угодно) та-
кое обобщение, которое окажется полезным для наших целей.

В нашем примере я, разумеется, .отдаю предпочтение


«НОД-вычислителю», а не «процессору для 111 и 259», и срав-
нение этих двух конструкций дает нам намек на то, какие ха-
. рактеристики делают обобщение «полезным для наших целей».
Машина, которая по команде может вырабатывать в качестве
ответа значения всех видов забавных функций от 111 и 259,
становится все более неудобной для проверки по мере того,
как растет набор функций. В этом явный контраст с нашим
«НОД-вычислителем».
НОД-вычислитель был бы столь же плох, если бы он пред-
ставлял собой таблицу из 250 ООО записей, содержащих «за-
готовленные» ответы. Его характерное отличие в том, что он
может быть задан в форме компактного набора «правил иг-
ры», который, если играть в соответствии с этими правилами,
обеспечит выдачу нужного ответа.
Огромный выигрыш состоит в том, что единое рассуждение
применительно к этим правилам позволяет нам доказывать
существенные утверждения о результатах любого варианта
игры. Это достигается ценой того, что при каждом пз 250 ООО
конкретных применений этих правил мы получаем ответ «не
сразу»: каждый раз игра должна быть сыграна в соответствии
с правилами! .
Тот факт, что мы в состоянии дать столь компактную фор-
мулировку правил игры, когда единое рассуждение позволяет
нам выводить заключения о любых вариантах игры, непосред-
ственно связан с систематизированным расположением
250 ООО узловых точек. Мы оказались бы беспомощными, если
бы лист картона содержал беспорядочный случайный разброс
точек, исключающий какую-либо систематизацию. В нашем
же случае мы могли бы разделить свою фишку на две поло-
винки и двигать. одну половинку вниз, пока она не ляжет на
горизонтальную ось, а другую половинку влево, пока она не
ляжет на вертикальную ось. Вместо того чтобы вос~роизво-
дить с одной фишкой 250 ООО возможных положений, мы мог-
ли бы добиться того же с двумя фишками, для каждой из ко-
торых нужно только 500 возможных положений, т. е. всего
1000 положений в общей сложности. Мы бы достигли того же
уровня в 250 ООО позиций, используя то обстоятельство, что лю-
бое из 500 положений одной половинки фишки может комби-
. нироваться с любым из 500 положений другой половинки: чис-
ло положений неразделенной фишки равно произведению
22 Глава О

числа положений одной половинки на число положений дру-


гой. На принятом жаргоне мы говорим, что «общее простран-
ство ~остоянйй рассматривается как декартово ~роизведение
пространств состояний переменных х и у».
Возможность замены одной фишки с двумерной свободой
выбора положения на две половинки с одномерной свободой
используется в предложенной выше двухрегистровой машине.
С точки зрения технической реализации это представляется
весьма заманчивым: требуется только построить регистры, спо-
собные различать 500 разных случаев (еэначений»), а за счет
простого объединения этих двух регистров общее число разных
случаев возводится в квадрат! Это перемножительное правило
позволяет нам различать громадное число возможных общих
состояний с помощью ограниченного числа компонентов, у
каждого из которых только ограниченное число возможных
состояний. По мере добавления таких компонентов размер
пространства состояний возрастает экспоненциально, но нам
следует иметь в виду, что это допустимо только при условии,
что обоснование нашего нововведения остается весьма компакт-
ным; если такое обоснование тоже возрастает экспоненциаль-
но, то вообще нет никакого смысла создавать такую машину.
Замечание. Убедительную иллюстрацию к сказанному вы-
ше М{)ЖНО найти в изобретении, возраст которого уже превы-
сил десять веков: в десятичной системе счисления! Она обла-
дает тем поистине пленительным свойством, что число необхо-
димых цифр возрастает всего лишь пропорционально
логарифму максимального из чисел, которые должны. быть
представлены. Двоичная система счисления - это то, что полу-
чается, когда вы забываете, что на каждой руке имеется по
пяти пальцев. (Конец замечания.)
Выше мы занимались одним аспектом множественности, а
именно большим числом позиций фишки (=возможных со-
стояний). Имеется еще одна аналогичная множественность, а
именно большое число различных игр (=вычислений)' кото-
рые могут состояться в соответствии с нашими правилами иг- '
ры: по одной игре для каждого начального положения, если
говорить точнее. Наши правила игры являются очень общими
в том смысле, ч·то они применимы к любому начальному поло-
жению. Но мы настаиваем на компактности обоснования пра-
вил игры, а это означает, что и сами правила должны быть
компактными. В нашем примере это достигалось следующим
приемом: вместо перечисления есделай это, сделай то» мы
задали правила игры в виде правил для выполнения «шага» в
сочетании с критерием того, должен ли «шаг» быть выполнен
Абстракция исполнения 23

в следующий раз. (На самом деле шаг должен повторяться,


пока не будет достигнуто состояние, в котором он становится
неопределенным.) Иначе говоря, даже всю игру от начала до
конца можно произвести с помощью повторяемых применений
одного и того же «подправила».
Это очень продуктивный прием. Один алгоритм заключает
в себе проект определенного класса вычислений, которые мо-
гут выполняться под его управлением; благодаря условному
повторению «Шага», вычисления из такого .класса могут иметь
весьма различные протяженности. Этим объясняется, как ко-
роткий алгоритм может занимать машину в течение длитель-
ного времени. С другой стороны, в этом можно усмотреть на-
чальный намек на то, зачем нам могут понадобиться особенно
быстрые машины.
Меня завораживает мысль, что эта глава могла писаться
еще в 'Те времена, когда Евк.лид мог смотреть на нее из-за мо-
его плеча.
1 РОЛЬ ЯЗЫКОВ ПРОГРАММИРОВАНИЯ

В главе «Абстракция исполнения» я дал неформаль-


ные описания различных «машин», предназначенных для вы-
числения наибольшего общего делителя двух положительных
(и не слишком больших) чисел. Одно описание было выраже-
но с помощью фишки, передвигаемой по листу картона, дру-
гое - с помощью двух половинок фишки, каждая из которых
двигалась вдоль своей оси, а последнее - с помощью двух ре-
гистров, каждый из которых мог содержать целое число (не
превышающее некоторой границы). Физически эти три «ма-
шины» весьма различны, однако математически они очень
сходны: основная часть доказательства их способности вы-
числять наибольший общий делитель совпадает для всех трех
машин. Это объясняется тем, что они представляют собой
всего лишь различные воплощения одного и того же набора
«правил игры», и это именно тот набор правил, который со-
ставляет сущность реального изобретения, известного как «ал-
горитм Евклида».
В предыдущей главе алгоритм Евклида описывался сло-
весно, не совсем формальным образом. Однако мы отметили,
что число соответствующих ему возможных вычислений столь
велико, что нужно доказать его корректность. Коль скоро
алгоритм дан только не формально, он не является вполне
подходящим объектом для формального рассмотрения. Для
дальнейшего нам потребуется описание этого алгоритма в не-
которой удобной формальной записи.
Такая формальная запись может обладать многочислен-
ными преимуществами. Любой способ записи подразумевает,
что всякий описываемый с его помощью предмет задается как
конкретный представитель (часто бесконечного) класса объ-
ектов, которые могут быть описаны этим способом. Наш спо-
соб описания должен, разумеется, обеспечивать изящное и
точное описание алгоритма Евклида, но когда мы этого достиг-
нем, алгоритм Евклида окажется в действительности заданным
как представитель огромного класса всех видов алгоритмов.
Роль языков п.рограммирования 25

И в описаниях некоторых из этих других алгоритмов мы смо-


жем найти более интересные применения нашего способа за-
писи. В случае алгоритма Евклида есть основания утверждать,
что он настолько прост, что можно обойтись его неформаль-
ным описанием. Сила формальной записи должна проявиться
в таких достижениях, которых без нее мы никогда не смогли
бы добиться!
:Второе преимущество формального способа записи состо-
ит в том, что он дает нам возможность изучать алгоритмы как
математические объекты; при этом формальное описание ал-
горитма служит основой, позволяющей нам интеллектуально
охватить этот алгоритм. Благодаря этому мы сумеем доказы-
вать теоремы о классах алгоритмов, например пользуясь тем,
что их описания обладают некоторым общим структурным
свойством.
Наконец, такой способ записи позволит нам описывать ал-
горитмы настолько точно, что если будет задан описанный так
алгоритм- и заданы значения аргументов (вход), то не будет
никаких сомнений относительно того, какими должны быть со-
ответствующие ответы (выход). При этом можно считать, что
вычисление выполняется автоматом, который, получив (фор-
мально описанный) алгоритм и аргументы, порождает ответы
без дальнейшего вмешательства человека- Такие автоматы,
способные обеспечивать взаимное воздействие алгоритма и
аргумента, и в самом деле были построены. Они называются
«автоматическими вычислителями». Алгоритмы, преднаэначен-
ные для автоматического выполнения такими вычислителями,
называются «программами», и с конца пятидесятых годов
формальные способы, используемые для записи программ,
называются «языками программирования». (Введение терми-
на «ЯЗЫК>> применительно к способам записи программ вызы-
вает смешанные чувства. С одной стороны, оно оказалось весь-
ма полезным, поскольку существующая теория лингвистики
обеспечила естественную основу и устоявшуюся терминологию
(«грамматика», «синтаксис», «семантика» и т. д.) для рас-
смотрени.й применительно к этой новой тематике.· С другой
стороны, необходимо отметить, что аналогия с (ныне имену-
емыми так) «естественными языкамн» часто оказывалась дез-
ориентирующей, так как для естественных языков, не формаль-
ных по существу, источником как их слабости, так и их силы
является присущая им неопределенность и неточность).
В историческом плане этот последний аспект, т. е. возмож-
ность использования языков программирования в качестве
средства.общения с существующими автоматическими вычис-
лителями, в течение долгого времени рассматривался как их
наиболее важное свойство. Эффективность, с которой сущест-
26 Глава 1

вующие автоматические вычислители могли бы выполнять


программы, написанные на конкретном языке, становилась
главным критерием качества этого языка. Как огорчительное
следствие мы нередко обнаруживаем, что аномалии существу-
ющих вычислительных машин старательно воспроизводятся в
языках программирования, причем это происходит в ущерб
интеллектуальной управляемости программ, выражаемых на
таком языке (как будто программирование и без этих анома-
лий не было уже достаточно трудным!). В рамках нашего
подхода мы постараемся восстановить равновесие и поэтому
будем относиться к возможности фактического выполнения
наших алгоритмов вычислительной машиной только как к
счастливой случайности, которая не должна занимать цент-
рального места в наших рассмотрениях. (В одном недавно
опубликованном учебном тексте по программированию на
PL/l можно найти настойчивый совет избегать обращений к
процедурам, насколько это возможно, «потому что они делают
программу столь неэффективной». В свете того, что процедуры
в языке PL/l являются одним из основных средств описания
структуры, этот совет выглядит ужасно, настолько ужасно, что
вряд ли можно называть упомянутый текст «учебным». Если
вы убеждены в полезности понятия процедуры и соприкаса-
етесь с приложениями, где дополнительные затраты на аппа-
рат процедур обходятся слишком дорого, то порицайте эти не-
подходящие приложения, а не возводите их на уровень стан-
дартов. Равновесие и в самом деле надлежит восстановить')
Я рассматриваю язык программирования преимуществен-
но как средство для описания (потенциально весьма сложных)
абстрактных конструкций. Как показано в главе «Абстракция
исполнения», первейшим достоинством алгоритма является по-
тенциальная компактность рассуждений, на которых может
основываться наше проникновение в его сущность. Как толь-
ко эта компактность потеряна, алгоритм в значительной мере
теряет «право на существование», и поэтому мы будем стре-
миться к сохранению такой компактности. Соответственно и
наш выбор языка программирования будет подчинен той· же
цели.
2 СОСТОЯНИЯ И ИХ ХАРАКТЕРИСТИКА

В течение многих столетий человек оперирует натуральны-


ми числами. Мне представляется, что в доисторические вре-
мена, когда перед нашими предками впервые забрезжило по-
нятие числа, они изобретали индивидуальные имена для
каждого числа, на которое у них обнаруживалась потребность
сослаться. Им приходилось иметь им·ена для чисел точно так
же, как мы имеем имена «один, два, три, четыре и так далее».
Это поистине «имена» в том смысле, что при исследовании
последовательности «один, два, три» никакое правило не по-
может нам умозаключить, что следующим -именем будет «че-
тыре». Вы, конечно же, должны это знать. (В том возрасте,
когда я вполне удовлетворительно умел считать - по-голланд-
ски, - мне пришлось учиться счету по-английски и во время
экзамена самое изощренное знание слов «семь» и «девять» не
смогло помочь мне установить, как писать слово «восемь», не
говоря уж о том, как произносить его.)
Очевидно, что такое несистематизированное разнообразие
позволяет нам выделять индивидуально только весьма ограни-
ченное количество различных чисел. Чтобы избежать подоб-
ного ограничения, каждый язык ~ цивилизованном мире вво-
дит (более или менее) систематизированное именование на-
туральных чисел, и обучение счету в значительной
степени сводится к обнаружению системы, лежа-
щей в основе этого именования. Когда ребенок учится считать
ло тысячи, ему не приходится эаучиватъ наизусть эту тысячу
имен (в их точном порsiдкеl); он руководствуется определен-
ными правилами: наступает момент, когда ребенок узнает, как
переходить от любого числа к следующему, например от «че-
тыреста двадцать девять» к «четыреста тридцать».
Легкость обращения с числами сильно зависит от выбран-
ной нами для них систематизации. Г~раздо труднее устано-
вить, что
дюжина дюжин= гросс
одиннадцать плюс двенадцать=двадцать три
XLVII+IV=LI
.28 Глава 2

чем установить. что


12Х 12= 144
11+12=23
47+4=51
так как последние три ответа можно получить, применив про-
стой набор правил, которыми может руководствоваться любой
восьмилетний ребенок.
При механических манипуляциях над числами преимуще-
ства десятичной системы счисления проявляются гораздо бо-
.лее отчетливо. Уже в течение столетий мы располагаем меха-
ническими сумматорами, показывающими ответ в окошке, по-
зади которого имеется несколько колес с десятью различными
положениями, причем каждое колесо в любом своем положе-
нии показывает одну десятичную цифру. (Теперь уже не яв-
ляется проблемой показать «00000019», прибавить 4 и затем
показать «00000023»; было бы проблемой - по крайней мере
если пользоваться чисто механическими средствами - пока-
зать вместо этого «девятнадцать» и «двадцать три».)
Существенное свойство такого колеса состоит в том, что оно
обладает десятью различными устойчивыми положениями.
Терминологически это выражается разными способами. На-
пример, колесо называется «переменной с десятью значения-
ми», и если мы хотим большей точности, то даже перечисли-
ем эти значения: от О до 9. Здесь каждое «положение» колеса
отождествляется со «значением» переменной. Колесо называ-
ется «переменной», потому что, хотя его положения и устой-
чивы, колесо может повернуться в другое положение:_ «значе-
ние» может измениться. (Я вынужден отметить, что этот тер-
мин неудачен по крайней мере в двух отношениях. Во-первых,
такое колесо, которое (почти) всегда находится в одном из
своих десяти положений и,· следовательно, (почти) всегда
представляет некое значение, является понятием, весьма от-
личным от того, что математики называют «переменной», по-
скольку, как правило, математическая переменная вообще не
представляет какого-либо конкретного значения. Если мы го-
ворим, что для любого целого числа п утверждение п2;;:=0
истинно, то здесь п является переменной совсем другого рода.
Во-вторых, в нашем контексте мы используем термин «пере-
менная» для того, что существует во времени и чье знанение,
если его не трогать, остается постоянным! Термин «изменя-
емая константа» подошел бы лучше, но мы не станем вводить
его, а будем придерживаться прочно установившейся тради-
ции.)
Другой способ терминологически отразить сущность тако-
го колеса, которое (почти) всегда находится в одном из деся-
Состояния а их характеристика 29

ти различных положений или «состояний», состоит в том, что-


бы поставить этому колесу в соответствие «пространство со-
стояний из десяти точек». Здесь каждое состояние (положение)
связывается с «точкой», а собрание этих точек называется -
в соответствии с математической традицией - пространством
или, если мы хотим большей точности, «пространством состо-
яний». Вместо того чтобы говорить, что переменная обладает
~почти) всегда одним из ,своих возможных значений, мы можем
теперь выразить то же, сказав, что система, состоящая из
единственной переменной, находится (почти) всегда в одной
из точек своего пространства состояний. Пространство состоя-
ний описывает степень свободы системы; больше ей некуда
переходить.
Отвлечемся теперь от отдельно взятого колеса и сосредото-
чим наше внимание на регистре с восемью такими колесами
'В строке. Поскольку каждое из этих колес находится в одном
из десяти различных состояний, этот регистр, рассмотренный
как целое, находится в одном из 100 ООО ООО возможных -раз-
.личных состояний, каждое из которых естественно идентифи-
цируется числом (а точнее, строкой из восьми цифр), показы-
ваемым в окошке. ·
Если заданы состояния всех колес, то состояние регистра
'В целом однозначно определено; и обратно, по любому состоя-
'НИЮ регистра в целом однозначно определяется состояние каж-
дого отдельного колеса. В этом случае мы го1юрим (в преды-
дущей главе нами уже использован этот термин), что получа-
ем (или строим) пространство состояний регистра в целом,
формируя «декартово произведение» пространств состояний
восьми- отдельных колес. Общее число точек в этом простран-
стве состояний является произведением чисел точек в тех
пространствах состояний, из которых оно построено (именно
поэтому оно называется декартовым произведением).
В зависимости от .того, что нас интересует в этом предме-
те, мы либо рассматриваем такой регистр как единую пере-
менную с 108 различными возможными значениями, либо как '
составную переменную,' составленную из восьми различных
переменных, называемых. «колесами», с десятью значениями.
Если мы интересуемся только показываемыми значениями,
то будем, по-видимому, рассматривать регистр как неделимое
понятие, тогда как инженер-эксплуатационник, которому нуж-
но заменить колесо со сломанным зубцом, будет, конечно, рас-
сматривать регистр как составной объект.
Мы наблюдали другой пример построения пространства
-состояний как декартова произведения более мелких про-
странств состояний, когда обсуждали алгоритм Евклида и об-
наружили, что положение фишки где-то на листе можно пол-
30 Глава 2

ностью идентифицировать с помощью двух полуфишек, каж-


дая из которых находится где-то ·на своей оси, т. е. с помощью
комбинации (а точнее, упорядоченной пары) двух переменных
«Х» и «у». (Идея идентификации положения точки на плоско-
сти значениями ее координат х и у идет от Декарта, который
разработал аналитическую геометрию и в честь которого на-
звано декартово произведение.) Фишка на листе была введена
для демонстрации того факта, что развивающийся вычисли-
тельный процесс - такой, как выполнение алгоритма Евкли-
да, - можно рассматривать как систему, движущуюся по
своему пространству состояний. В соответствии с этой метафо-
рой начальное состояние именуется также «исходной точкой».
В этой книге мы будем преимущественно - а может быть, .
даже исключительно - заниматься системами, пространства
состояний которых будут в конечном итоге рассматриваться
как некие декартовы произведения. Разумеется, не следует
усматривать в этом мое мнение, будто бы пространства состоя-
ний, построенные с помощью декартовых произведений, явля-
ются общим и окончательным решением всех наших проблем;
я отлично знаю, что это не так. Из дальнейшего станет очевид-
ным, почему они заслуживают столь большого нашего вни-
мания и в то же время почему это понятие играет столь гла-
венствующую роль во многих языках программирования.
Прежде чем продолжить рассуждения, отмечу одну проб-
лему, с которой нам придется столкнуться. Когда мы образу-
ем пространство состояний, строя дека ртов о произведение, то
вовсе не обязательно, что нам окажутся полезными все его
точки. Типичным примером этому является характеристика
. дней данного года с помощью пары (месяц, день), где «ме-
сяц» - переменная с 12 значениями (от «ЯНВ» до «дек»), а
«день» - переменная с 31 значениями (от 1 до 31). В этом слу-
чае мы образуем пространство состояний из 372 точек, тогда
как никакой год не содержит более чем 366 дней. Что нам де-
лать, скажем, о- парой (июн, 31)? Либо мы запрещаем ее, вво-
дя понятие «невозможных дат» и тем самым создавая в неко-
тором смысле возможность внутренних противоречий в систе-
ме, либо мы разрешаем ее как другой вариант имени для од-
. ного из «истинных» дней, например, приравнивая ее к (июл, 1).
Феномен «неиспользуемых· точек, пространства состояний» воз-
никает всякий раз, когда число различных возможных значе-
ний, которые мы желаем распознавать, оказывается простым
числом.
Систематизация, которая автоматически вводится, когда
мы строим пространство состояний как декартово произведе-
ние, позволяет нам идентифицировать отдельно взятую точку.
Например, я могу утверждать, что мой день рождения - это
Состояния и их хараклерисгика 31

(май, 11). Однако благодаря Декарту нам известен теперь и


другой способ фиксации этого факта: мой день рождения при-
ходится на ту дату (месяц, день), которая соответствует ре-
шению уравнения!'
(месяц=май) and (день=ll)
Приведенное выше уравнение имеет только одно решение
и поэтому представляет собой несколько усложненный способ
указания этого единственного дня в году. Однако преимуще-
ство использования уравнения состоит в том, что оно позво-
ляет нам охарактеризовать множество всех его решений, и
такое множество может содержать значительно больше чем
.одну точку. Тривиальным примером может служить определе-
ние рождества:
(месяц-- дек) and ((день=25) or (день=26))
Более примечательным примером является определение мно-
жества дат, в которые выплачивается мое ежемесячное жа-
леванье:
(день=23)
и разумеется, это гораздо более компактное описание, чем
перечисление вида « (янв, 23), (фев, 23), (мар, 23) » и т. д.
Из сказанного выше явствует, что степень легкости, с ко-
торой мы используем такие уравнения для характеристики
множеств состояний, зависит от того, насколько множества,
которые мы хотим охарактеризовать, «соответствуют» струк-
туре Пространства состояний, т. е. «соответствуют» введенной
системе координат. Например, в рассмотренной выше системе
координат было бы несколько неудобно охарактеризовать мно-
жество дней.. которые приходятся на тот же день недели, что
и (янв, 1). Во многих случаях программисту приходится при
решении задач вводить пространства состояний с подходяши-
ми для его целен системами координат; причем дополнитель-
ные требования часто вынуждают его вводить также простран-
ства состояний, в которых число точек во много раз больше,
чем число различных возможных значений, которые он дол-
жен распознавать.
Другой пример использования уравнения для характери-
стики множества состояний встречался в нашем описании кар-
тонной машины для вычисления НОД (Х, У), где
Х=У
1> Здесь и далее в математических формулах используются для обозна-
чения логических операций символы and (конъюнкция), ог (дизъюнкция)
и поп (отрицание). - Прим. перев.
32 Глава 2

характеризует все точки того, что мы назвали «линией отве-


та»; это множество конечных состояний, т. е. вычисление
прекращается тогда и только тогда, когда достигнуто какое-
нибудь состояние, удовлетворяющее уравнению х=у.
Помимо координат пространства состояний, т. е. перемен-
ных, в терминах значений которых выражается ход вычисли-
тельного процесса, мы встречали в наших уравнениях констан-
ты (такие, как «май» или «23»). Кроме того, можно пользо-
ваться так называемыми «свободными переменным:и», которые
можно представить себе как «неспецифицированные констан-
ты». Мы применяем их специально для обозначения связей
различных состояний, которые могут встречаться на последо-
вательных шагах одного -и того же вычислительного процесса.
Например, во время некоего конкретного выполнения алгорит-
ма Евклида с начальной точкой (Х, У) все состояния (х, у J
будут· удовлетворять формуле

НОД(х, у) =НОД(Х, У) and О<х~Х and О<у~У


Здесь Х и У не являются такими переменными, как х и у.
Они представляют собой «их начальные. значения». Это кон-
станты с точки зрения конкретного вычисления', но они не
специфицированы в том смысле, что мы мог ли бы запустить
алгоритм Евклида с любой точкой сетки в качестве на чаль-
ного положения нашей фишки.
В заключение некоторые терминологические замечания.
Я буду называть такие уравнения «условиями» или «предика-
тами». (Я мог бы, а возможно, и должен бы различать эти
понятия, зарезервировав термин «предикат» для формального
выражения, обозначающего «условие». Тогда мы имели бы
возможность сказать, например, что два разных предиката
«Х= у» и «у=х» обозначают одно и то же условие. .Однако,
зная себя, я не надеюсь преуспеть в такой манере изложения.)
Я буду считать синонимичными такие выражения, как «состоя-
ние, для которого предикат истинен», . «состояние, которое
удовлетворяет условию», «состояние, в котором условие удов-
летворяется», «состояние, в котором условие справедливо».
и т. д. Если система заведомо достигнет состояния, удовлет-
воряющего условию Р, то мы будем говорить, что система
заведомо «обеспечит истинность Р».
Предполагается, что всякий предикат определен в каждой
точке рассматриваемого пространства состояний: в каждой
точке значением предиката является либо «истина», либо
«ложь», и предикат служит для характеристики множества
всех точек, для которых (или в которых) этот предикат
истинен.
Состояния и их характеристика 33

Мы будем называть два предиката Р и Q равными (фор-


мально «Р = Q»), если они обозначают одно и то же условие,
т. е.' характеризуют одно и то же множество состояний.
Два предиката будут играть особую роль, и мы резерви-
руем для них имена «Т» и «F».
Т - это предикат, который Истинен во всех точках рассмат-
риваемого пространства состояний; ему соответствует полное
множество состояний.
F - предикат, который ложен во всех точках пространства
состояний; он соответствует пустому множеству.

2-6
3 XAPAKTEPИCTtf KA СЕМАНТИКИ

Нас интересуют преимущественно такие системы, которые,


приступив к работе в некоем «начальном состоянии», завер-
шат ее в каком-то «конечном состоянии», которое, как правило,
зависит от выбора начального состояния. Этот подход несколь-
ко отличается от концепции автомата с конечным числом со-
стояний, который, с одной стороны, воспринимает поток вход-
ных символов, а с другой стороны, порождает. поток выходных
символов. Чтобы перевести это в нашу схему, мы должны
предположить, что значение входа (т. е. аргумент) отража-
ется в выборе начальногQ состояния и что значение выхода
(т, е. ответ) отражается в выборе конечного состояния. Наш
подход избавляет нас от -всевозможных непринципиальных
сложностей.
Первая часть' этой главы посвящена почти исключительно
так называемым «детерминированным машинам», тогда как
во второй части (которую можно пропустить при первом чте-
нии) речь идет о так называемых «недетерминированных ма-
шинах». Различие между этими двумя понятнямн состоит
в том, что для детерминированной машины событие, которое
произойдет после запуска конструкции, полностью определя-
ется ее начальным состоянием. Если она запускается дважды
в· одинаковых начальных состояниях, то произойдут одинако-
вые события: поведение детерминированной машины полностью
воспроизводимо. Для недетерминированной машины, напро-
тив, запуск в заданном начальном состоянии приведет к ка-
кому-то одному событию из класса возможных событий; на-
чальное состояние только фиксирует этот класс в целом.
Теперь я предполагаю, что проектирование такой систе-
мы является 'целенаправленной деятельностью, т. е. что мы
желаем достичь чего-то с помощью этой системы. Например,
если мы хотим создать машину, способную вычислять наиболь-
ший общий делитель, то можем потребовать, чтобы конечное
состояние удовлетворяло условию
Х=НОД(Х, У) (1)
'
Характеристика семанлики З5

-В рассмотренной ранее машине мы будем иметь также


у= над (Х, У), потому что игра заканчивается при х= у, но
-вто отнюдь не часть наших требований, когда мы решаем при-
-нять. конечное значение х в качестве ответа.
Мы называем условие ( 1) (желаемым) «постусловием» -
(<ПОСТ», потому что оно налагается 'на то состояние, в котором
система должна оказаться после· своей .работы. Заметим, что
постусловие может удовл.етворяться многими возможными
состояниями. В таком случае мы естественно считаем каждое
из них. одинаково удовлетворительным, так что нет оснований
· требовать, чтобы конечное состояние было однозначной функ-
цией от начального состояния. (Читатель увидит из дальней-
шего, что именно здесь проявляется потенциальная полезность
недетерминированного устройства.)
Для того чтобы использовать эту машину, когда мы хотим
получить от нее ответ (например, хотим «чтобы. она достигла
конечного состояния, удовлетворяющего постусловию ( 1) для
ваданного' набора значений Х и У»), 'Цам желательно энатъ
множество соответствующих начальных · состояний, а более
точно.: множество таких начальных состояний, при которых
запуск' обязательно приведет к 'событню правильного завер-
шения, причем система останется в конечном состоянии, удов-
летворяющем постусловию. Если мы можем привести систему
без вычислительных усилий в одно из таких состояний, то мы
уже знаем, как использовать эту систему для получения· же-
лаемого ответа. Приведем пример для евклидовой игры на
картоне: мы можем гарантировать конечное состояние, удов-
летворяющее постусловию ( 1) для· любого· начального состоя-
ния, удовлетворяющего условию
над(х, у) =НОД(Х, У) and О<х~500 and 0<у~500 (2)
(Верхние границы добавлены, чтобы учесть ограниченный
размер листа картона. Если мы начинаем с парой (Х, У), 'га-
кой, что над (Х, У)= 713, то не существует пары (х, у), улов-
летворяющей условию (2), т. е. для таких значений Х и У
условие. (2) сводится к предикату Р; а это означает, что рас-
сматриваемая машина не может быть использована для вычис-
ления над (Х, ·У) применительно к этой паре значений Х и У)..
При многих комбинациях (Х, У) многие состояния удовлет-
воряют условию (2). В случае 0<Х:::;;;500 и 0<У~500 три·
виальным выбором является х = Х и у= У. Этот выбор может
быть произведен без какого-либо вычисления. функции над
и даже без учета того факта, что это симметричная функция
от своих аргументов.
Условие, характеризующее множество всех начальных со-
стояний, при которых запуск обязательно приведет к событию
2*
36 Глава 3

правильного завершения, причем система останется в конеч-


ном состоянии, удовлетворяющем заданному постусловию,
называется «слабейшим предусловием, соответствующим это-
му постусловию». (Мы называем его «слабейшим», посколь-
ку, чем слабее условие, тем больше состояний удовлетворяют
ему, а мы стремимся здесь охарактеризовать все возможные
начальные состояния, которые неизбежно приведут к желае-
мому конечному состs>янию.:)
Если ~истема,4 машина, хонструкция) обозначается через
S, а желаемое постусловие- через R, то соответствующее сла-
бейшее предусловие мы обозначим I>
wp(S, R)
Если начальное состояние удовлетворяет wp ( S, R), то кон-
-струкция обязательно обеспечит в конце концов истинность R.
Поскольку wp (S, -R) - это слабейшее предусловие, мы знаем
также, что если начальное состояние не удовлетворяет
р (S, .R), то такой гарантии дать нельзя, т. е. ход событий
'\\1

может привести к завершению работы в конечном состоянии,


11е удовлетворяющем R, а может даже и вообще помешать
достижению какого бы -то ни было конечного состояния (как
мы увидим далее, либо потому, что система оказывается во-
влеченной в выполнение незавершимого задания, либо потому,
что система попадает в тупик).
Мы считаем, что нам достаточно хорошо известно, как ра-
ботает конструкция S, если можем вывести для любого пост-
условия R соответствующее слабейшее предусловие wp (S, R),
поскольку тем самым мы уловили, чтб эта конструкция спо-
собна сделать для нас; это называется «ее семантикой».
Здесь уместны дв~ ~~~еч~1ц1я. Во-первых, множество воз-
можных постусловий, вообще говоря, столь огромно, что эта
информация в табличной форме (т. е. в виде таблицы, где
каждому постусловию R соответствует запись, в которой со-
держится соответствующее предусловие wp (S, R)), оказалась
бы совершенно необозримой, а следовательно, бесполезной.
Поэтому определение семантики той или иной конструкции
всегда дается другим способом - в виде правила, описываю-
щего, как для любого заданного постусловия R можно вывес-
ти соответствующее слабейшее предусловие wp (S, R). Для
фиксированной конструкции S такое правило, которое по за-
данному предикату R, означающему постусловие, вырабаты-
вает предикат wp (S, R), означающий соответствующее сла-
бейшее предусловие, называется «преобразователем предика-

1> По начальным буквам- английских слов weakest pre-condition. -


Прим. перев.'
Характеристика семантики 37

тов». Когда мы просим, чтобы нам сообщили описание


семантики конструкции S, то в сущности речь идет о соответ-
ствующем этой конструкции преобразователе предикатов.
Во-вторых,- и я чувствую искушение добавить «благода-
рение судьбе» - часто нас не интересует полная семантика
конструкции. Это объясняется тем, что мы стремимся исполь-
зовать конструкцию S только для конкретной надобности, а
точнее, для тего, чтобы обеспечить истинность весьма кон-
кретного постусловия R, ради которого производилась разра-
ботка конструкции. И даже применительно к этому конкретно-
му постусловию ·R нас часто не интересует точный вид
wp (S, R); зачастую мы удовлетворяемся более сильным усло-
вием Р, т. е. таким условием, для которого можно показать,
что утверждение _
Р =Ф wp (S, R) для всех состояний (3)
справедливо. (Предикат «Р =9 Q» (читается «из Р следует Q»)
дожен только в тех точках пространства состоянии," где усло-
\

вие Р справедливо, а Q не справедливо; во всех остальных


точках он истинен. Требуя, чтобы утверждение «Р ~ wp (S, R) »
было справедливо для всех состояний, мы тем самым требу-
ем, чтобы всякий раз когда Р- истина, условие wp (S, R)
тоже было истиной; тогда Р - достаточное предусловие.
В теоретико-множественной терминологии это означает, что
.множество состояний, характеризуемое условием Р, является
подмножеством того множества состояний, которое характе-
ризуется условием wp (S, R) .) Если для заданных Р, S и R
отношение (3) выполняется, это часто можно доказать, не
прибегая к точной формулировке,- или, если вам так боль-
ше нравится, «вычислению» или «выводу» - предиката
\.\·р (S, R). И это отрадное обстоятельство, поскольку, за ис-
ключением тривиальных случаев, следует ожидать, что явная
формулировка условия wp (S, R) превысит по крайней мере
резерв нашей писчей бумаги, нашего терпения или нашей (ана-
литической) изобретательности (или какую-то комбинацию
этих резервов).
Смысл ·wp (S, R), т. е. «слабейшего предусловия для на-
чального состояния, при котором запуск обязательно приведет
1< событию правильного завершения, причем система S оста-
нется в конечном состоянии, удовлетворяющем постусловию
R», позволяет нам установить, что преобразователь предика-
тов, рассматриваемый как функция от постусловия R, облада-
ет рядом определенных свойств.
Свойство 1. Для любой конструкции S мы имеем
wp(S, F) =F (4).
З8 Глава 3

Предположим, что оказалось не так; при Этом нашлось бы


хоть одно состояние, удовлетворяющее условию wp (S, F).
Возьмем такое состояние в качестве начального состояния
для конструкции S. Тогда, согласно нашему определению, за-
пуск привел бы к событию правильного завершения, причем:
система осталась бы в конечном состоянии, удовлетворяющем·
предикату F. Но здесь возникает противоречие, так как по оп-·
ределению предиката F не существует состояний, удовлетворя-
ющих F; тем самым доказано отношение ( 4). Свойство 1 из-
вестно также под названием «закон исключенного чуда» ..

. Свойство 2. Для любой конструкции S и любых постусло-


вий Q и R, таких, что
Q ~R для всех состояний (5)
справедливо также отношение
1
wp (S, Q) =Фwр (S, R) для всех состояний (6)
В самом деле, при любом начальном состоянии, удовлетво-
РЯJ?Щем wp (S, Q), согласно определению, по· окончании рабв-:
ты .системы будет обеспечена истинность Q. С учетом отноше-
ния (5) тем самым будет обеспечена и истинность R. Следо-
рательно, данное начальное состояние будет одновременно
удовлетворять и условию wp (S, R), как записано в (6). Свой . .
ство 2 - это свойство монотонности.
Свойство 3. Для любой конструкции S и для любых пост-
'условий Q и R справедливо , '
(wp (S, Q) and wp (S, R)) =wp (S, Q and R) (7)
В любой ~очке пространства состояний из левой части от-
ношения (7) логически следует его правая часть, потому что
для любого начального состояния, удовлетворяющего ч.
wp (S, Q), и wp (S, ,R), мы знаем в совокупности, что устано-
вится конечное состояние, удовлетворяющее и Q, и R. Далее,
поскольку, в силу определения,
( Q and R) =Ф Q для всех состояний
свойство 2 позволяет нам заключить, что
wp (S, Q and R) =Фwр (S, Q) для всех состояний
и аналогично
wp (S, Q and R) =Ф wp (S, R) для всех состояний
Однако из А =Ф В и А =ФС, согласно исчислению предика-
тов, можно вывести, что А =Ф (В and С). Поэтому правая часть
отношения (7) логически следует из его левой части в любой
Характеристика семанзики 39

-точке пространства состояний. Поскольку обе части всюду


следуют друг из друга, они должны быть равны, а тем самым
доказано свойство 3.
Свойство 4. Для любой конструкции S и любых постусло-
~иil Q и R справедливо
(wp (S, Q) or wp (S, R)) =Ф wp (S, Q or R) для всех состояний
(8)
Поскольку, в силу определения,
Q·=Ф (Q or R) для всех состояний
свойство 2 позволяет нам прийти к заключению, что
wp (S, Q) =Ф wp (S, Q or R) для всех состояний
и аналогично·
:wp (S, R) =Ф wp (S, Q or R) для всех состояний
Но, согласно исчислению высказываний, из А =Ф С II В =Ф С
можно заключить, что (А or В) ==} С, и этим доказана спра-
ведливость соотношения (8). Вообще говоря, следование
в обратном направлении \Ie имеет места. Так про женщину,
.которая ждет ребенка, неверно утверждение, что она родит
-обязательно сына и неверно утверждение, что она родит обя-
зательно дочь; однако с абсолютной уверенностью можно ут-
-верждатъ, что она родит сына или дочь. Впрочем, для детер-
минированных конструкций справедливо следующее, более
сильное свойство.
Свойство 4'. Для любой детерминированной конструкции S
1 любых постусловий Q и R справедливо
(wp (S, Q) or wp (S, R)) =wp (S, Q or R)
_ Нам нужно показать, что следование выполняется 11 в ле-
вую сторону. Рассмотрим какое-то начальное состояние, улов-
.летворяющее wp (S, Q ог R); этому начальному состоянию со-
ответствует единственное конечное состояние, удовлетворяю-
щее либо Q, либо Р, либо обоим условиям; следовательно . лан-
.ное начальное состояние должно удовлетворять либо \\'Р (S, Q),
либо wp (S, R), либо обоим условиям соответственно, т. е.
оно должно удовлетворятъ (wp (S, Q) or \Vp (S, R)). И. этим
доказано свойство 4'.
В этой книге я буду- и это может оказаться одной нз ее
отличительных особенностей - рассматривать недетерминиро-'
ванностъ как правило, а детерминированность как исключе-
ние: детерминированная машина будет рассматриваться как
:частный случай недетерминированной, как конструкция, для
40 Глава 3

которой справедливо свойство 4', а не только более слабое


свойство 4. Такое решение отражает коренное изменение в
моем мировоззрении. В 1958 г. я был одним из первых, кто
разрабатывал базовое программное обеспечение для машины
с прерываниями ввода-вывода, и невоспроизводимость пове-
дения такой во всех отношениях недетерминированной машины
явилась горестным обстоятельством. Когда впервые была пред-
ложена идея прерываний ввода-вывода, меня настолько пуга-
ла сама мысль о необходимости разрабатывать надежное
программное обеспечение для такого неукротимого зверя,
что я оттягивал принятие решения о допущении таких преры-
ваний по крайней мере в течение трех месяцев. И даже после
того, как я сдался (мое сопротивление сломили лестью), чув-
ствовал я себя весьма неуютно: ведь ошибка в программе спо-
собва вызвать несуразное поведение системы, столь сходное
с невоспроизводимым машинным сбоем. Кроме того,- и это
в то время, когда для детерминированных машин мы все еще
полагались на «отладку»,- с самого начала было совершен-
но очевидно, что тестирование программ- оказывалось теперь
совсем непригодным средством для достижения должного
уровня надежности.
В течение многих лет впоследствии si относился к невоспро-
-изводнмости поведения недетерминированной машины как
к добавочному осложнению, которого следует избегать любы-
ми способами. Прерывания были для меня не чем иным, как
злыми кознями инженеров по аппаратуре против бедных
разработчиков программного обеспечения. Из этого моего
страха родилась дисциплина «гармонически взаимодействую-
щих последовательных процессов». Несмотря ·на ее успех, мои
опасения сохранялись, поскольку наши решения - хоть бы и
с доказанной правильностью - представлялись частными ре-
шениями проблемы «укрощения» (именно так мы это воспри-
'нималнг) конкретных видов недетерминированности. Основа-
нием для моего страха было отсутствие общей методологии.
С тех пор два обстоятельства изменили картину. Первое
возникло с пониманием того, что даже в случае полностью
детерминированных машин полезность тестирования проврамм
оказывается сомнительной. Как я уже много раз говорил и во
многих местах писал: тестирование программы может вполне
эффективно служить для демонстрации наличия в ней оши-
бок, но, к сожалению, непригодно для доказательства их
отсутствия. Другим прояснившимся тем временем обстоятель-
ством явилось обнаружение необходимости того, чтобы вся-
кая дисциплина проектирования должным образом учитывала
тот факт, что само проектирование конструкции, предназначен-
ной для какой-то цели, тоже должно быть целенаправленной
Характеристика семантики 41

деятельностью. В нашем конкретном случае это означает ес-


тественность предположения, что отправной точкой для про-
ектных рассмотрений будет служить заданное постусловие.
В каком-то смысле мы будем «работать вспять». Действуя
так, мы обнаружим, что весьма существенным оказывается
следование из свойства 4, тогда как от равенства из свойства
4' мы получим очень мало пользы.
Как только мы разработали математический аппарат, по-
зволяющий проектировать недегерминированные конструкции,
достигающие цели, недетерминированная машина перестает
нас пугать. Напротив! Мы научимся даже ценить ее как важ-
ную ступень на пути разработки проекта в конечном итоге
полностью детерминированной конструкции.
(Последующая часть этой главы может быть пропущена
при первом чтении.) Ранее мы договорились, что представля-
ем себе функционирование конструкции S достаточно хорошо,
если -только знаем, как соответствующий ей преобразователь
предикатов wp (S, R) действует над любым постусловием R.
Если нам известно также, что данная конструкция детерми-
нирована, то знание этого преобразователя предикатов пол-
ностью определяет возможное поведение конструкции. Для
детерминированной ~онструкции S и некоторого постусловия
R всякое начальное состояние попадает в.одно из трех непе-
ресекающихся множеств в соответствии со следующими вза-
имно исключающими возможностями:
(а) Запуск конструкции S приведет к конечному состоя-
нию, удовлетворяющему R.
(б) Запуск конструкции S приведет к конечному состоянию,
удовлетворяющему поп R.
(в) Запуск конструкции S не приведет к какому бы то ни
было конечному состоянию, т. е. работа не сможет эавершить-
ся правильным образом.
Первое множество характеризуется выражением wp (S, R),
второе множество- выражением wp (S, поп R), их объедине-
ние - выражением ·
(wp(S, R) or wp(S, поп R)) =wp(S, R or поп R) =wp(S, Т)
' -

а следовательно, третье множество характеризуется выраже-


нием поп wp (S, Т).
Труднее дать полную семантическую характеристику неде-
терминированной системы. По отношению к любому заданно-
му постусловию R мы снова имеем три возможных типа собы-
тий, как перечислено выше в пунктах (а), (б) и (в). Однако
в случае недетерминированной системы. одно и то же на чаль-
42 Глава З

ное состояние не обязательно приводит к единственному со-


бытию, которое по определению относилось бы к одному из
трех взаимно исключающих типов; для любого начального
состояния возможные события могут теперь относиться к од-
ному, двум или даже ко всем трем типам.
Чтобы описывать такие ситуации, мы можем использовать
понятие «свободного предусловня>. Ранее мы рассматривали
такие предусловия, при которых гарантировалась достижи-
мость· «правильного результата», т. е. конечного состояния,
удовлетворяющего iR. Свободное предусловие является более
слабым: оно гарантирует только, что система не выдаст не"
правильного результата, т. е. не достигнет такого конечного
состояния, которое. не удовлетворяло бы R, однако не исключа-
ется возможность незавершения работы системы. Аналогично
и для свободных предусловий мы можем ввести понятие «Сла-
бейшего свободного предусловия»; обозначим его через
v:lp (S, R). При этом пространство начальных состояний под"
·разделяется в принципе на семь взаимно непересекающихся
областей, ни одна из которых не обязана быть пустой. (Их
семь, потому что из трех объектов можно сделать семь непус-
тых выборок.} Все они легко характеризуются тремя предиката-
ми: wlp {S, R), wlp {S, поп R) и wp (S,' Т) ..
(а) wp (S, R) = (wlp (S, R) апd wp (S, Т))
Запуск обеспечит истинность R.
(б) wp (S, поп R) = (wlp (S, поп R) апd wp (S, Т))
Запуск обеспечит истинность поп R.
(в) wlp (S, F) = (wlp (S, R) апd wlp (S, поп R))
Запуск не сможет привести к правильно завершаемой работе.
(аб) wp {S, Т) апd поп wlp (S, R) апd поп wlp (S, поп R)
Запуск приведет к завершаемой работе, но по начальному со-
стоянию нельзя определить, будет ли конечное состояние удов-
летворять условию R или нет.
(ав) wlp (S, R) and поп wp {S, Т)
Если ·запуск приводит к какому-то конечному состоянию, такое
конечное состояние будет удовлетворять R, но по начальному
состоянию нельзя определить, завершится ли работа системы.
(бв) wlp (S, поп R) апd поп wp (S, Т) .
Если запуск при.водит к какому-то конечному состоянию, такое .
конечное состояние не будет удовлетворять R, но по начально-
Характеристика семанзики 43

му состоянию нельзя определить, завершится ли работа си-


стемы.
(абв) поп (wlp (S, R) or \\ lp (S, поп R) or \\'Р (S, Т))
1

'По начальному состоянию нельзя определить, приведет ли


запуск к завершаемой работе, а в случае аавершаемости нель-
зя определить, будет ли удовлетворяться условие R.
Последние четыре возможности существуют только для
недетерминнрованных машин.
Из определения wlp (S, R) следует, что
wlp(S, Т) =Т
Ясно также, что
(wlp (S, F) and \Vp (S, Т)) = F
Если бы было не так, то существовало бы начальное со-
стояние, для которого можно было бы гарантировать и завер-
шимость, и незавершимость работы.
На рис. 3.1 дано гра-
фическое представление
пространства состояний,
причем внутренние части
прямоугольников удовлет-
воряют wlp (S, R), wlp (S,
поп R) и wp (S, Т) соот- аЪс wp ,s. п
ветственно.
Этот анализ приведен
для полноты, а также по- ,
тому, что на практике по-
нятие СВОбОДНОГО предус- wlp (S. non R/

ловия оказывается весьма


полезным. Если, напри- Рис. З 1
мер, кто-то реализует
язык программирования, то он не станет доказывать, что при
этой реализации любая правильная программа выполняется
правильно. Он был бы удовлетворен и счастлив, если бы твер-
до знал, что никакая правильная программа не будет выпол-
няться неправильно без соответству~щего предупреждения.-
разумеется, при условии, что класс программ. которые на са-
мом деле будут выполняться правильно, достаточно велик
для того, чтобы эта реализация представляла какой-то ин-
терес.
Однако пока мы не будем рассматривать понятие свобод-
ного предусловия, а сосредоточим свое внимание на способе
44 Глава 3

характеристики тех начальных состояний, которые гарантиру-


ют получение правильного результата. Когда такой способ бу-
дет разработан, мы поемотрим, как можно его видоизменить
таким образом, чтобы это позволило нам рассуждать о свобод-
ных предусловиях в той мере, в которой они нас интересуют.
4 СЕМАНТИЧЕСКАЯ ХАРАКТЕРИСТИКА
ЯЗЫКА ПРОГРАММИРОВАНИЯ

В предыдущей главе мы выдвинули тезис, что знаем семан-


тику конструкции S достаточно хорошо, если знаем ее «преоб-
разователь предикатов», т. е. правило, указывающее нам, как
вывести по любому постусловию R соответствующее слабейшее
предусловие (которое мы обозначили через wp (S, R)), харак-
теризующее те начальные состояния, при которых запуск при-
ведет к событию правильного завершения, причем система
останется в конечном состоянии, удовлетворяющем постусло-
вию R. Вопрос в том, как выводить wp (S, R) для задан-
ных S и я.
Оставим пока вопрос об одиночной конкретной конструк-
ции S. Программа, написанная на хорошо определенном язы-
ке программирования, может рассматриваться как некая кон-
струкция, такая конструкция, которую мы знаем достаточно
хорошо, если знаем соответствующий преобразователь преди-
катов. Однако язык программирования полезен ТО{IЬКО при
том условии, что его можно применять для записи многих
различных программ, и для всех этих программ нам жела-
тельно знать соответствующие им преобразователи предикатов.
Каждая такая программа задается своим текстом на хоро-
шо определенном языке программирования, и поэтому ее текст
должен служить для нас отправной точкой. Но теперь перед
нами неожиданно открываются два совершенно различных
назначения такого текста программы. С .одной стороны, текст
программы предназначен для машинной интерпретации, если
мы хотим, чтобы она могла выполняться автоматически, если
мы хотим, чтобы по ней для нас был произведен какой-либо
конкретный расчет. С другой стороны, желательно, чтобы
текст программы говорил нам о .том, как строить соответству-
ющий преобразователь предикатов, как производить преобра-
зование предикатов, чтобы выводить предусловие wp (S, R)
для любого данного постусловия R., которое нас почему-либо
заинтересовало. Это замечание позволяет понять, что подра-
зумевается под «kорошо определенным языко~ программиро- ·
46 Глава 4

вания». с нашей точки зрения. Когда семантика конкретной


конструкции (или программы)_ -аадаегся ее преобразователем
предикатов, мы рассматриваем семантическую характеристику
языка программирования как набор правил, которые позволя-
ют любой программе, написанной· на этом языке, поставить
в соответствие преобразователь, предикатов. С такой точки
зрения мы мажем рассматривать программу как «код» для
соответствующего преобразователя предикатов.
При желании можно подойти к проблеме проектирования
языка 'программирования с такой позиции. Можно руководст-
воваться (довольно формально) тем, что правила построе-
ния преобразователей предикатов должны быть такими, что-
бы, применяя их, нельзя· было построить ничего другого, кро-,
ме как преобразователя предикатов, обладающего свойствами
1-4 из' предыдущей главы. В самом деле, если правила не да-
ют такой гарантии, то это означает, что вы деформируете пре-.
дикаты таким образом, что они уже не могут интерпретиро-
ваться как постусловия и соответствующие слабейшие пред-
условия.
Сразу напрашиваются два примера весьма простых преоб-.
разователей предикатов, которые обладают требуемым» свой-.
сгвами.
Начнем с тождественного преобразования, т1. е. с конструк-.
ции S, такой, что для .дюбого пос:rусловия R мы имеем·
wp (S, R) =R. Эту конструкцию знают и' любят все програм-
мисты: она известна им под названием. «пустой оператор»,
и в своих программах они 'часто используют ее-, оставляя про-
пуск в том месте текста, где синтаксически требуется какой--
то оператор. Это не слишком похвальный прием (компилятор
.! '• ' •I

должен считать, что он -евиянтз этот оператор, на том основа-


нии, что он ничего не
видит,' и Поэтому мы дадим этой кон-
струкции наииенование; скажем.. «прописпиь». Итак, семан-.
т.ика _ 'оператора «nponyi:;тurь» определяется следующим об-
разом:
wp (прописпиь; R)-=·~ Для любого постусловия R
(Как и все, я буду пользоваться термином еоператорь.лю-
тому что он прочно. вошел ·В· жаргон. Когда . люди сообразили.
что «Команда» моглабы окаааться- более подходящим. терми-
ном, было уже слишком поздно 1).) -

L) По традиции мы переводим английский. термин «statement» Тутв.ерж-


пение, предложение) термином еоператорь, введенны.м в ~:~рограммир9~анне.
А_. А. Ляпуновым, итаким образам, русский читатель .окаэывается 8~ более
выгодном пбложении,' чем· английский. «-Прим.'ред. ',j -
Семанзическая характеристика языка программирования 47

Замечание. Тем, кто считает расточительством символов


~ведение явного имени «пропустить» для пустого оператора,
когда «hусто» столь красноречиво выражает его семантику,
следует осознать, что десятичная система счисления оказалась
возможной только благодаря введению символа «0» для поня-
тия «ничто». (Конец эамечаниял
Прежде чем продолжить наши рассуждения, мне хотелось
бы не упустить возможность отметить, что тем временем мы
уже определили некий язык программирования. Остается до-
бавить только одно: это однооператорный язык, в котором
можно описать только одну конструкцию, причем единствен-
ное; что способна сделать для нас данная· конструкция, это
-еоставитъ. все, как есть» (или «ничего не делать», но такое
негативное употребление языка представляет опасность, см.
следующий абзац).
Другой простой преобразователь предикатов приводит к
постоянному слабейшему предусловию, которое вовсе не зави-
сит от постусловия R. Мы имеем два предиката-константы, Т
и F. Конструкция S, для которой wp (S, R) = Т при всех R, не
может существовать, потому что она нарушила бы закон ис-
ключенного чуда. Однако конструкция S, для которой
wp (S, R) =F при всех R, обладает преобразователем преди-
катов, удовлетворяющим всем необходимым свойствам. Это-
му оператору мы тоже присвоим имя, назовем его «отказать».
Итак, семантика ёператора «озкаэазь» задается следующим
образом:
wp (отказать, R) = F для любого постусловия R
Этот оператор не может даже «ничего не делать» в смысле
«оставить все, как есть»; он вообще ни на что не способен.
Если мы полагаем R= Т, т. е. не накладываем на конечное
состояние никаких дополнительных требований, кроме самого
факта его существования, даже тогда не найдется соответст-
вующего начального состояния. Если запустить конструкцию
по имени «отказать», она не сможет достичь конечного состоя-
ния: сама попытка ее запуска является гарантией неудачи.
(Нас не должно занимать теперь (а также и впоследствии)
то, что позднее мы введем структуру операторов, в которой со-
держатся как частные случаи семантические эквиваленты для
«пропустить» и «отказа ты.)
Теперь мы располагаем неким (все еще весьма зачаточным)
лвухоператорным языком программирования, в котором можем
описать две конструкции; одна из нихничего не делает, а вто-
рая всегда терпит неудачу. Со времени опубликования знаме-
-нитого «Сообщения об алгоритмическом языке АЛГОЛ 60»
48 Глава 4

в 1960 г. никакой уважающий себя ученый, занимающийся


программированием, не позволит себе обойтись на этом этапе
без формального определения синтаксиса столь далеко про-
двинутого языка программирования в системе обозначений,
называемой «НФБ» (сокращение от «Нормальная форма
Бэкуса»), а именно:
<операгор з-г: =пропустить1 отказать
(Читается так: «Элемент синтаксической категории, именуе-
мой «оператор» (именно это обозначают забавные скобки
«<»и«>»),. определяется как (это обозначает знак«::=»)
«пропустить» или (это обоэна чает вертикальная черта « 1 »)
«отказать». Колоссально! Но не беспокойтесь; более впечат-
ляющие применения НФБ в качестве способа записи последу-
ют в надлежащее время.) ~
Один класс безусловно более интересных преобразовате-
лей предикатов основывается на подстановке, т. е. на замене
всех вхождений некоей переменной в формальном выражении
дяя постусловия R на (одно и то же) «что-нибудь другое».
Если в предикате R. все вхождения переменной х заменяются
некоторым выражением (Е), то мы обозначаем результат
этого преобразования через Rш-+х· Теперь мы можем рассмот-
реть для заданных х и Е такую конструкцию, чтобы для любых
постусловий R получалось wp (S~ R) =RE-.x; здесь х- «коорди-
натная переменная» нашего пространства состояний, а Е -
выражение соответствующего типа.
Замечание. Такое Преобразование путем подстановки удов-
летворяет свойствам 1-4 из предыдущей главы. Мы не ста-
нем пытаться демонстрировать это и предоставим самому чи-
тателю решать в зависимости от своего вкуса, будет ли он от-
носиться к этому как к тривиальному или же как к глубокому
математическому результату. (Конец замечаниял
Таким способом вводится целый класс преобразователей
предикатов, целый класс конструкций. Они обозначаются опе-
ратором, который называется «оператор присваивания»; такой ,
оператор должен определять три вещи:
1) наименование переменной, подлежащей замене;
2) тот факт, что правило, соответствующее преобразованию
предикатов, есть подстановка;
3) выражение, которым должно заменяться всякое вхожде-
ние этой переменной в постусловии.
Если переменная х должна быть заме.иена выражением
(Е), то обычная запись такого оператора выглядит следующим
Семантическая характеристика; языка программирования 49

образом:
Х:=Е
(где так называемый знак присваивания «: =» следует читать
как «сгановится») ..
Сказанное можно суммировать, определив
wp ( «Х : = Е», R) = RE~x для любого постусловия R.
При желании это можно рассматривать как семантическое
определение оператора присваивания для любой координатной
переменной х и любого выражения Е соответствующего типа.
· Наслаждаясь, по нашему обыкновению, употреблением
НФБ, мы можем расширить наш формальный синтаксис, что-
бы он читался так:
<оператор> :: =пропустить1отказать1
<оператор присваивания>
<щrератор присваивания> : : =<переменная> . -
<выражение>
причем последнюю строчку следует читать так: «Элемент син-
таксической категории, именуемой «оператор присваивания»,
определяется как элемент синтаксической категории, именуе-
мой «переменная», за которым следуют сначала знак присваи-
вания «: = », а затем элемент синтаксической категории, име-
нуемой «выражение».
Перед тем как продолжить рассуждение, представляется
разумным убедиться в том, что этим формальным описанием
семантики оператора присваивания в самом деле охватывает-
ся наше интуитивное понимание оператора присваивания -
сели оно у нас есть! Рассмотрим пространство состояний с дву-
мя целыми координатными переменными «а» и «Ь>>. Тогда
wp(«a:=7», а=7) = {7=7}
и, поскольку логическое выражение в правой части является
истиной для всех значений а и Ь, т. е. для всех точек простран-
ства состояний, мы можем упрощенно записать это так:
wp(«a:=7», а=7) =Т
Иначе говоря, всякое начальное состояние гарантирует, что
присваивание <<а:= 7» обеспечит истинность «а= 7». Аналогично
wp(<<a:=7», а=6)= {7=6}
и, поскольку это логическое выражение является ложью для
всех значений а и Ь, мы получаем
wp(«a:=7», а=б) =F
~50 Глава 4

.Это означает, что не существует никакого начального состоя·


.ния, для которого мы могли.бы .гарантировать, что присваива-
ьие «а:= 7» обеспечит истинность «а= 6». (Это находится в со-
-ответствии с нашим предыдущим результатом, что все началь-
-ные состояния обеспечат конечную истинность «а=]», а
-следовагельно, конечную ложность «a-=f=.7».) Далее,
wp(«a:=7», Ь=ЬО) = {Ь=ЬО}
·т. е. если мы хотим гарантировать, что после присваивания
-«а:= 7» переменная Ь имеет некоторое значение ЬО, то нужно,
чтобы Ь имела это значение еще в начальном состоянии. Дру-
тимн словами,· все переменные, за исключением «а>>, не изме-
-няются, они сохраняют те значения, которые имели раньше;
-присваивание «а:= 7» перемещает в пространстве состояний
точку, соответствующую текущему-состоянию системы, парал-
лельно оси а так, что обеспечивается конечное выполнение
.равенства «а= 7».
Вместо того чтобы брать в качестве выражения Е констан-
ту, мы могли бы взять какую-то функцию от начального со-
-стояния. Это иллюстрируе~ся следующими примерами:
wp («а: =2:icb+1», а= 13) = {2* Ь+ 1=13} = {Ь=б}
wp («а:=а+ 1», а> 10) ={а+ 1>10} = {а>9}
wp'(«a:=a-b», а>Ь) = {а-Ь>Ь} = {a>2:ic Ь}
Возникает небольшое осложнение, если мы разрешаем вы-
ражению Е быть частично определенной функцией начально-
го состояния, т. е. такой функцией, попытка вычисления кото-
рой для начального состояния, не входящего в область опре-
деления, не приведет к правильно завершаемой работе. Если
мы хотим предусмотреть и эту ситуацию, то нам нужно уточ-
нить наше определение семантики оператора присваивания и
записать
wp ( «Х: = Е», R) = {D (Е) cand RE-+x}
Здесь предикат D (Е) означает «в области определения Е»;
логическое выражение «В 1 cand В2» (так называемая «услов->
ная конъюнкция») имеет то же значение, что и «В 1 and В2»,
€сли оба операнда определены, но помимо этого по определе-
нию оно имеет значение «ложь», если В 1 является «ложью»;
последнее справедливо вне зависимости от того, определен ли
операнд В2. Обычно условие D (Е) не подчеркивается явно ли-
бо потому, что оно= Т, либо потому, что мы исходим из пред-
положения, что оператор присваивания никогда не будет за-
пущен в начальных состояниях, не принадлежащих области
определения выражения Е.
Семантическая характеристика языка программирования 5}

Естественным обобщением оператора присваивания явля-


ется излюбленное некоторыми программистами так называе-
мое «одновременное присваивание». В этом случае возможна
одновременная- замена для нескольких различных переменных.
Оператор одновременного присваивания обозначается списком·
различных переменных), подлежащих замене (эти переменные
разделяются запятыми), слева от знака присваивания и столь
же протяженным списком выражений (также разделяемых
запятыми) справа от знака присваивания. Итак, разрешается
пйсать
xl, х2: =El, Е2
xl, х2, хЗ:=Еl, Е2, ~3
Заметим, .что i-я переменная из списка левой части должна
быть заменена на i-e выражение из списка правой части, так
что, например, пр~ заданных х 1, х2, Е 1 и Е2 оператор
х1, х2:=Е1, Е2
семантически эквивалентен оператору
х2, х1: =Е2, EI
Одновременное присваивание позволяет нам предписать, что-
бы две переменные х и у обменялись своими 'аначениями с по-
мощью оператора
х, у:=у, х
В иной записи эта операция выглядела бы более громоздкой.
Легкость реализации одновременного присваивания и воз-
можность с его помощью избегать некоторой избыточности
записи являются причинами популярности таких операторов.
Заметим, что если списки становятся слишком длинными, то
получаемая программа становится весьма неудобочитаемой.
Истинный любитель НФБ расширит свой синтаксис, обеспе-
чив две различные формы для оператора присваивания, сле-
дующим образом:
<оператор присваивания э-ть- <лтеременная >: =
· <выражение> 1 <пере-
менная>, <оператор
присваивания>, <выра-
жение>
Это так называемое «рекурсивное определение», поскольку
одна из альтернативных форм для синтаксической едшшцы,
имену~мой «оператор присваивания» (а именно вторая фор-
ма), содержит в качестве одного из своих компонентов снова
52 Глава 4

эту же синтаксическую единицу, именуемую «оператор при-


сваивания», т. е. ту самую синтаксическую единицу, которую
мы определяем! На первый взгляд такое циклическое опреде-
ление выглядит ужасающе, но после более внимательного изу-
чения мы можем убедиться в том, что, по крайней мере с син-
таксической точки зрения, в этом нет ничего неправильного.
Например, поскольку, согласно первой альтернативе,
х2:=Е1
является примером оператора присваивания, то формула
х1, х2: =Е1, Е2
допускает разбор вида
х1, <оператор прнсваиванияо-, Е2
а следовательно, согласно второй альтернативе, также явля-
ется оператором присваивания. Однако с семантической точки
зрения это отвратительно, потому что получается, что Е2
ассоциируется с х1 вместо того, чтобы ассоциироваться с х2.
В сравнении с двухоператорным языком, содержащим толь-
ко «пропустить» и «Отказать», наш язык с оператором при-
сваивания выглядит значительно более богатым: уже нет ка-
кой бы то ни было верхней границы для числа различных при-
меров синтаксической единицы «оператор прнсваивания». Но
он все же явно недостаточен для наших целей; нам нужна
воаможность строить более изощренные программы, более
сложные конструкции. Для построения потенциально сложных
конструкций мы пользуемся схемой, которую можно рекурсив-
но описать так:
<конструкция>::= <простая конструкция> 1
<правильная композиция записей
вида: <конструкция>>
Для того чтобы эта схема могла принести хоть какую-ни-
будь пользу,· необходимо выполнение двух условий: в нашем
распоряжении должны иметься «простые конструкции», что-
бы было с чего начать, и, кроме того, мы должны знать, как
строить «правильные композиции». Введенные раньше опера-
торы можно взять в качестве простых конструкций; оставшая-
ся часть этой rлавы посвящена именно процессу правильной
композиции некоей новой конструкции из заданных конструк-
ций. Новая конструкция в свою очередь может выступать в
роли части еще более сложного составного объекта.
После того как объект был образован композицией час-
тей, мы можем рассматривать его двумя способами. Во-пер·
Семантическая характеристика языка программирования 53

вых, мы можем считать его «нераздельным целым», восприни-


мая его свойства в большей или меньшей степени как маги-
ческие (или принимая их на веру или как постулаты). При
таком подходе существенны только свойства конструкции; не
имеет никакого значения, каким образом она образована. из
своих частей. С такой точки зрения любые две конструкции,
обладающие одинаковыми свойствами, эквивалентны. Или же
мы рассматриваем конструкцию. как «составной объект», об-
разованный так, что мы можем понять, почему она обладает
объявленными свойствами. При этом мы воспринимаем части
как «малые» нераздельные целые, для которых принимаются
в расчет только их общие свойства. Второй подход вносит яс-
ность в то, что мы понимаем под «композицией». Композиция
должна определять, как свойства целого следуют из свойств
частей.
После этих общих замечаний вернемся к нашим копкрет-
ным конструкциям, свойства которых, как мы считаем, выра-
жаются их преобразователями предикатов. Точнее говоря,
если заданы две конструкции Sl и S2, чьи преобразователи
предикатов известны, можем ли мы представить себе правило
вывода нового преобразователя предикатов из двух заданных?
Если да, то мы можем считать результирующий преобразова-
тель предикатов описанием свойств составного объекта, по-
строенного специальным образом из частей S 1 и S2.
Одним из простейших способов получения новой функции
из двух заданных является так называемая «функциональная
композиция», т. е. предоставление значения одной функции
·в качестве аргумента для другой. Составной объект, соответ-
ствующий такому преобразователю предикатов, принято обо-
значать через «Sl; S2», и мы определяем

wp(«Sl; S2», R) =wp(SI, wp(S2, R))

что при желании можно рассматривать как семантическое оп-


ределение точки с запятой.

Замечание. Из того факта, что преобразователи предика-


тов для Sl и S2 обладают свойствами 1-4 из предыдущей гла-
вы, можно заключить, что и определенный выше преобразова-
тель предикатов Для «Sl; S2» также обладает этими четырьмя
свойствами. Например, поскольку для S 1 и S2 справедлив
за кон исключенного чуда:

wp (Sl, F) =F и wp (S2, F) =F
54 Глава 4

мы заключаем, подставляя F вместо R в верхнее определе-


ние, что
wp ( «S 1; S2», F) = wp ( S 1, wp ( S2, F))
=wp (Sl, F)
=F
Читателю предоставляется в качестве упражнения прове-
рить, что остальные три свойства тоже сохраняются. (Конец
эамечаниял
Перед тем как продол~·ить наши рассуждения, убедимся
в том, что этим формальвым описанием семантики точки с за-
пятой охватывается наше интуитивное представление о. ~-:ей
(если оно у нас есть'}, т. е. что составная конструкция «Sl;
S2» может быть реализована по правилу «сначала запустить.
конструкцию Sl и по окончании ее работы запустить S2». В са-
мом деле,_ в нашем. определении wp(«Sl; S2», R) мы пред-
ставляем R - постусловие для составной конструкции - как
постусловие к преобразователю предикатов для S2, и этим
отражается то.' что общая работа конструкции «S 1; S2» может
закончиться с окончанием работы S2. Соответствующее сла-
'бейшее предусловие для S2, т. е. wp (S2, R), представляется
как постусловие к преобразователю предикатов для Sl; тем
самым мы явно отождествляем начальное состояние для SZ
с конечным состоянием для S 1. Однако именно так и бывает"
если работа· S 1 непосредственно предшествует во времени за-
пуску S2.
Чтобы удостовериться в этом, рассмотрим пример. Пусть
«S 1 ; S2» представляет собой
«а:=а+Ь; Ь:=а*Ь»
и пусть нашим постусловием является некоторый , предикат
R (а, Ь); в таком случае
wp (S2, я (а, Ь)) =wp («Ь: =а* Ь», R (а, Ь))
=R(a, а•Ь)
и
wp(«Sl; S2», R(a, Ь)) =wp(Sl, wp(S2, R(a, b)))-
=wp (Sl, R (а, а•Ь))
=wр(«а:=а+Ь>>, R(a, а *Ь))
=IR(a+b, (а+Ь)·•Ь)
т. е. мы можем гарантировать выполнение отношения R меж-
ду конечными значениями а и Ь при условии, что первоначаль-
'-Ю то же отношение выполняется между а+ Ь и (а+ Ь) * Ь
соответственно.
Семантическая характеристика языка программирования 55

И наконец, поскольку функциональная композиция обла-


дает свойством ассоциативности, то не имеет значения, будем
ли мы разбирать «S 1; S2; SЗ» как «[S 1; S2]; SЗ» или же как
reSI; {S2; SЗ}». Иначе говоря, мы имеем полное право тракто-
вать точку с запятой как символ сочленения: поэтому не воз-
никает никакой неопределенности, когда мы выписы.ваем опе-
.раторный список вида «Sl; S2; SЗ; ... ; Sn», и мы будем без
.стеснения поступ~ть так, когда для этого представятся подхо-
дящие случаи.
Упражнение
Проверьте, что конструкции
«xl:=El; х2:=Е2» и «х2:=Е2; xl:=El»
семантически эквивалентны, если переменная xl не встреча-
·ется в выражении Е2, а переменная х2 не встречается в выра-
жении EI. На самом деле, в таком случае обе эти конструкции
семантически эквивалентны одновременному присваиванию
.<<.Х1, х2: =Е1 ,· Е2». (Эта эквивалентность является одним из ар-
гументов в пользу одновременного присваивания; ее примене-
ние позволяет нам избежать избыточности последовательной
записи.. более того, при одновременном присваивании стано-
вится очевидным, что два выражения El и Е2 могут вычислять-
ся одновременно, и это последнее обстоятельство представляет
интерес при некоторых методиках реализации.· Помимо того,
имеется, быть.может, более интересная возможность, что кон-
струкция «Xl, х2:=Е1, Е2» не окажется семантически эквива-
лентной· ни конструкции «xl:=El; х2:=Е2», ни конструкции
«Х2: = Е2; xl: =El ».) (Конец упражнения.)
До введения точки с запятой мы могли писать только одно-
операторные программы; с помощью точки с запятой мы обрели
способность писать программы в виде сочленения из п
.(п>О) операторов: «S 1; S2; SЗ; ... ; Sn». Если исключить про-
межуточную незавершенность, выполнение каждой программы
всегда означает временную последовательность выполнений п
операторов, сначала Sl, потом S2 и так далее до Sn включи-
тельно. Однако из нашего примера игры на листе карт.она,
реализующей алгоритм Евклида, мы знаем, что должны уметь
описыватъ более широкий класс «правил игры»: всякая игра
будет состоять из последовательности перемещений, где каж-
дое перемещение имеет вид либо «Х: :d:.x-y», либо «у:= у-х»,
но способ чередования этих перемещений во времени и даже
их общее число будут изменяться от игры к игре; он зависит от
начального положения фишки, он аависит от начального со-
стояния системы. Если точка с запятой является единственным
56 Глава 4

нашим средством для составления нового целого из заданных


частей, то мы не в состоянии этого выразить, и поэтому нам
необходимо искать нечто новое. .
Пока точка с запятой остается единственным имеющимся
ь нашем распоряжении средством соединения, единственным
обстоятельством, при котором происходит запуск одной из со-
ставляющих конструкций Si (i> 1),. является правильное за-
вершение работы (лексикографически) предшествующей кон-'
струкции. Чтобы добиться нужной нам гибкости, необходимо
обеспечить возможность запуска той или Иной (под) конструк-
ции в зависимости от текущего состояния системы. Для этого
мы введем - в два этапа - понятие «охраняемой команды»,
синтаксис которой задается следующим образом:
<охраняющий заголовок>::= <Логическое выражение э-э-
<оператор>
<охраняемая команда э з е- <охраняющий заголовок>{;
<оператор>}
где фигурные скобки « {» и «} » следует читать так: «сопровож-
дается любым числом (быть может, нулем) экземпляров со-
держимого скобою>.
(Другой вариант синтаксиса охраняемой команды мот бы
выглядеть так:
<список операторов>::= <оператор> {;<оператор>}
<охраняемая команда>::= <логическое выражение.з=е-
<сп11сок операторов>
Однако по причинам, которые не должны нас теперь интересо-
вать, я предпочитаю синтаксис, в котором вводится понятие
охраняющего заголовка).
В этом сочетании логическое выражение, предшествующее
стрелке, называется «предохранителем». Идея состоит в том,
что следующий за стрелкой список операторов будет выпол-
няться лишь при том условии, что обеспечена начальная
истинность значения соответствующего предохранителя. Предо-
хранитель позволяет нам избежать выполнения списка опера ·
торов при тех первоначальных обстоятельствах, когда это вы-.
полнение оказалось бы нежелательным или (если употребля-
ются частично определенные ·операции) невозможным.
Истинность значения предохранителя является необходи-
мым предварительным условием для выполнения охраняемой
команды как Целого; это условие, разумеется, не является
достаточным, поскольку тем или иным способом - с двумя
такими способами мы встретимся - до него еще должна дойти
потенциальная «очередь». Именно поэтому охраняемая ко-
манда не рассматривается как оператор: оператор безогово·
Семантическая характеристика языка программирования 57

рочно выполняется, когда наступает его очередь, а охраняе-


мая команда может использоваться в качестве строительного
блока .для оператора. Если говорить точнее, мы предложим
два различных способа составления оператора из набора охра·
няемых команд.
После некоторого осмысливания рассмотрение набора ох-
раняемых команд представляется вполне естественным. Пред-
положим, что нам требуется построить конструкцию, такую,
чтобы при условии, что начальное состояние удовлетворяет Q,
конечное состояние удовлетворяло R. Предположим далее, что
нам не удается найти единый список операторов, который вы-
полнял бы эту работу в любых случаях. (Если бы такой спи-
сок операторов существовал, то мы именно его бы и исполь-
зовали и не возникло бы потребности в охраняемых коман-
дах.) Однако нам может удасться найти несколько списков
операторов, каждый из которых будет выполнять нужную ра-
боту для некоего подмножества возможных начальных состоя-
ний. К каждому из этих списков операторов мы можем при-
соединить в качестве предохранителя логическое выражение,
характеризующее то подмножество, для которого этот список
подходит, и если у нас имеется. вполне достаточный .набор
предохранителей, такой, что из истинности Q всегда логически
следует истинность значения хотя бы одного предохранителя,
то для каждого начального состояния, удовлетворяющего Q,
мы располагаем конструкцией, которая переведет систему в
состояние, удовлетворяющее условию R, причем этой конструк-
цией служит одна из охраняемых команд, чей предохранитель
имел начальное значение «истина».
Чтобы выразить это, определим сначала
<набор охраняемых команд>::= <охраняемая команда">
{О <охраняемая командаэ-}

где символ «О» выступает в роли разделителя вариантов, в


остальном не упорядоченных. Один из способов формирова-
ния оператора из набора охраняемых команд состоит в том,
чтобы заключить такой набор в пару скобок «if ... fi», при
этом наш синтаксис для синтаксической категории, именуемой
«оператор», пополняется следующей формой: ·
<оператор>::= if<набор охраняемых команд> fi

Таким образом, мы получаем возможность объединить .нес-


колько охраняемых команд в новую конструкцию. Мы можем
представить себе работу, которая произойдет' после запуска
этой конструкции, следующим образом. Выбирается одна из
'58 Глава 4

охраняемых команд, чей 'предохранитель является истиной, 0


запускается ее список операторов.
Прежде чем мы перейдем к изложению формального опи-
сания семантики нашей новой конструкции, уместно сделать
три замечания.

1. Предполагается, что все предохранители определены;


если это не так, т. е. вычисление какого-то предохранителя
может привести к работе без правильного завершения, 1'0 до-
пускается, что и вся конструкция не сможет правильно завер-
шить свою работу.
т; 2. Вообще говоря, наша конструкция приведет к нслетер-
минированности при тех начальных состояниях, для которых
истинны значения более чем одного предохранителя, посколь-
ку остается неопределенным, какой из соответствующих спис-
ков операторов будет тогда выбираться для запуска. Никакой
недетерминированности не возникает, если все предохранители
попарно исключают друг друга.
3. Если начальное состояние таково, что ни один пз предо-
хранителей не является истиной, то мы встречаемся с началь-
ным состоянием, для которого не подходит ни один пз зари-
антов, а следовательно, и вся конструкция в целом. Запуск
при таком нача~ьном состоянии приведет к отказу.
Замечание. Если мы допускаем также и пустой набор охра-
няемых команд, то оператор «if fi» семантически эквивалентен
нашему Прежнему оператору еогказагь». (Конец замечания.у
(В следующем формальном определении слабейшего
предусловия для конструкции if-fi мы ограничимся случаем,
когда все предохранители являются полностью определенными
функциями. Если это не так, то нужно переписать выражение
с использованием символа сапё при дополнительном требова-
нии, чтобы начальное состояние принадлежало области опре-
деления всех предохранителей.)
Обозначим через «IF» оператор 1>
if B1~S~1 о Bz-+SL2 о ... о Bn~SLn fi
Тогда для любого постусловия R
\VP (IF, R) = (Е j: 1 ~j~n: BJ) and
(А j: I~j~n: BJ =9 wp(SLJ, R))
_ Эту формулу следует читать так: wp (IF, R) является исти-
ной для каждой такой точки в пространстве состояний, где
1>
SL - аббревиатура для Statement List - список операторов.-:-
Прим. перев.
Семантическая характеристика языка програлt.чиро~ания 59

существует хотя бы одно значение j из отрезка 1 ~j~n,


.такое, .что В, является истиной, и где, кроме того, для всех j
нз отрезка 1 ~j~n, таких, что Bj- истина, значение
\Vp (SLj, R) также является истиной. Используя символ « ... »,
какмы уже делали при описании самого оператора IF, мы :\Ю-
жем предложить иную форму определения:
\\'Р (IF, R) = (В1 ог В2 or ... or Вп) and'
(В1 =9 wp (SL1, R)) and
(В2 =9 wp (SL2, R)) and ... and
(Вп =9 wp (SLп, R))
Понимание этих формул не представляет особого труда.
Требованием, чтобы значение хотя бы одного предохранителя
являлось истиной, отражается факт отказа в ,елуча·е, когда
значения для всех предохранителей ложны. Кроме того, мы
требуем для каждого начального состояния, удовлетворяющего.
\Vp (IF, R),. чтобы выполнялось в, ~ \\'Р (SLj, R) для всех j.
Для тех значений j, при которых Bj- ложь, это следование
истинно независимо от значения wp (SLJ, R), т. е. для таких
значений j, разумеется, безразлично, что будет делать SLJ.
При нашей реализации это учитывается в том, что для запуска
не берется SLJ с ложным начальным значением предохрани-
теля Bj. При тех значениях j, для которых Bj- истина, данное
'следование может быть истиной только в .том случае, когда
\\'Р ( S~.f, R) также является истиной. Поскольку наше формаль-
ное определение требует истинности следования при всех
эначениях j, то реализация на самом деле имеет свободу вы-
бора, когда более чем один предохранитель является истиной.
- Конструкция if-fi представляет собой только один нз двух
возможных способов построения оператора из набора охраня-
емых команд. В конструкции if-fi состояние" при котором все
предохранители имеют ложные значения, ведет к отказу.
U нашей второй форме мы разрешаем, чтобы состояние, при
котором нет ни одного предохранителя с истинным значени-
ем, приводило к правильному завершенню; поскольку в этой
ситуации не запускается никакой список операторов, вполне
естественно, что при этом возникает семантическая эквивалент-
ность с пустым оператором. Однако это разрешение на пра-
вильное завершение, когда нет ни одного предохранителя с
истинным значением, дополняется тем, что работе не разре-
шается завершаться, пока хотя бы один предохранитель явля-
ется истиной: Итак, после запуска проверяются все предохра-
нители. Если нет ни одного истинного значения предохраните-
.ля, то работа завершается; если же имеются прелохранителн
с. истинными эначениями, то один из соответствующих списков
60 Глава 4

операторов запускается, а после его завершения реализация


снова начинает общую проверку всех предохранителей. Эта
вторая конструкция обозначается путем заключения списка
охраняемых команд в пару скобок «do ... od».
Формальное определение слабейшего предусловия' для кон-
струкции do-od является более сложным, чем для конструкции
if-fi; дело в том, что первое выражается через второе. Мы.
сначала приведем это формальное определение, а затем объяс-
ним его. Обозначим через «DO» оператор
do B1~SL10Bт+SL20 ... 0 Bn~SLn od
и через «IF» оператор, получаемый путем заключения того же
набора охраняемых команд в пару скобок «if ... fi». Если ус-
ловия Hk (R) задаются в виде
Ho(R) =R and поп (Е j: 1 ~;;;j~n: В1)
11 при fl>O
Hk(R) =wp (IF, Hk-1 (R)) or Ho(R)
то
wp(DO,R) = (Ek:k~O:Hk(R))
Здесь интуитивно мы понимаем Hk(R) следующим обра- •
зом: это слабейшее , предусловие, такое, что конструкция
do-od завершит свою работу после не более чем k выборок
охраняемых команд, причем система останется в конечном со-
стоянии, удовлетворяющем постусловию R.
При k=O требуется, чтобы конструкция do-od завершила
работу, не производя выборки какой-либо охраняемой коман-
ды, т. е. не допускается. существования какого-либо предохра-
нителя с истинным значением, что и выражено вторым логи-
ческим сомножителем. При этом начальная истинность R оче-
видным образом является необходимым условием для конечной
истинности R, что и выражено первым логическим сомно-
жителем.
При k>O нам нужно различать два случая: либо ни один
из предохранителей не имеет истинного значения, но тогда
должно выполняться условие R, и это приводит нас ко второ-
му ЛОГИЧеСКОМУ слагаемому; либо ХОТЯ бы ОДИН предохрани-
тель' является истиной, и тогда события развиваются так, как
при однократном запуске оператора «IF» (в начальном состоя-
нии, которое не может привести к немедленному отказу вслед-
ствие отсутствия истинных значений предохранителей). Одна-
ко после такого выполнения, при котором производилась вы-
борка одной охраняемой команды, нам необходима гарантия
попадания в состояние, такое, чтобы потребовалось не более
Семантическая характеристика языка программирования 61

чем (k~ 1) дальнейших выборок для достижения завершения


работы в конечном состоянии, удовлетворяющем R. В соот-
ветствии с нашим определением, таким постусловием для
этого оператора «IF» служит Н k-1 (R).
Согласно последней строке, определяющей wp (DO, R),
должно существовать такое значение k, чтобы потребовалось
не более чем k выборок для обеспечения завершения работы
в конечном состоянии, удовлетворяющем постусловию Р.

Замечание. Если мы допускаем также и пустой набор ()Х-


раняемых - команд, то в силу сказанного оператор «do od»
семантически эквивалентен нашему прежнему оператору «про-
пустить». (Конец замечания.]
5 ДВЕ ТЕОРЕМЫ

В этой главе мы выводим две теоремы об операторах, ко-


·торые -строятся иэ наборов- охраняемых команд. Первая тео-
_рема касается конструкции выбора if-fi, а вторая - конструк-
1щи повторения do-od. В этой главе мы будем рассматривать
конструкции, выводимые из набора охраняемых команд
B1-+SL1 о Br+SL2 о ... о Bn-+SLn
Будем обозначать через «IF» и «DO» операторы, получае-
-мые заключением этого набора охраняемых команд в пары
-скобок «if ... fi» и «do ... od» соответственно. Мы будем также
использовать сокращение
BB=(Ej: I~j~n:BJ)
Теорема. (Основная теорема для конструкции выбора)
Используя введенные только 'что обозначения, .мы можем
<формулировать основную теорему для конструкции-выбора:
'Пусть конструкция выбора IF и пара предикатов Q и R
таковы, что
о-ь ен (1)
и
(А j: 1 ~j~n: (Q and Вз) =Ф wp (SLз, R)) (2) .
-одновременно справедливы для всех состояний. Тогда
Q =Фwр (IF, R) (3)
-справедливо также для всех состояний.
Поскольку, в силу определения,
wp (IF, R) = ВВ and (А j: 1 ~j~n: BJ~wp (SLз, R))
и, согласно (1), из Q логически следует первый член правой
части, то отношение (3) доказывается, если на основании (2)
мы можем сделать вывод, что
Q =Ф (А j: 1 ~j~n·: Вз=Ф wp (SLJ, R)) (4)
-справедливо для всех состояний. Для любого состояния, при
Две теоремы 63·

котором Q является ложью, отношение ( 4) истинно в силу оп-


ределения логического .следования. Для любого состояния •
.при котором Q является истиной, и для любого значения j мы
~ожем различать два случая: либо В, является .ложью, но·
.тогда Bj=Фwp (SLj, R) является истиной в силу определения
следования, либо В, является истиной, но тогда, согласно (2) ,.
wp (SLj, R) является истиной, а следовательно, B.i=Фwp (SLj, R)
тоже является истиной. В результате мы доказали отношение·
(4), а следовательно, и (3).
Замечание. В частном случае бинарного выбора (п = 2) и·
при В2=поп В1 мы имеем ВВ= Т, и слабейшее предусловие
цреобразуется так:
(В1 =Ф wp (SL1, ~)) апd (поп В1 =Ф \Vp (SL2, R)) =
(поп В1 or wp (SL1, R)) апd (В1 or wp (SL2, R)) =
(В1 апd wp (SL1, R)) or (поп В1 апd wp (S,L2, R)) (5)

Последнее преобразование возможно потому, что нз четы--


рех перекрестных произведений член В1 апd поп В1 = F может·
быть отброшен, а член wp (SL1, R) апd wp (SL2, 'R) тоже мо-
жет быть отброшен по следующей причине: в любом состоя-
нии, где он истинен, ·обязательно является истиной какой-то·
один из двух членов формулы (5), и поэтому его самого мож-
но исключить из дизъюнкции. Формула (5) имеет прямое от-
ношение к предложенному Хоаром способу описания семан-
тики конструкции if-theп-else из языка АЛГОЛ· 60. Поскольку
Здесь ВВ =Т логически следует из чего угодно, мы можем вы-
вести (3) при более слабом предположении:
((QaпdB1)=Фwp(SL1,R)) апd ((Qaпd поп В1) =Фwp(SL2, R))
(Конец эамечаниял
Теорема для конструкции выбора представляет особую·
важность в случае, когда пара предикатов Q и R может быть
записана в виде
R=P
Q=P and ВВ
В. этом случае предпосылка ( 1) выполняется ав гоматически,
а поскольку (ВВ апd Bj) = Bj, предпосылка (2) · сводится
к виду
(А j: 1 ~j~n: (Р апd Bj) =Фwр (SLj, Р)) (6)
из чего мы можем вывести, согласно (3),
(Р апd ВВ) =Ф wp (IF, Р) для всех состояний (7)
64 Глава 5

Это отношение послужит предпосылкой для нашей следу-


ющей теоремы.
Теорема. (Основная теорема для конструкции повторения)
Пусть набор охраняемых команд с построенной для него
конструкцией выбора IF и предикат Р .таковы, что
(Р апd ВВ) =Ф wp (IF, Р) (7)
справедливо для всех состояний. Тогда для соответствующей
конструкции повторения ро можно вывести, что
(Р апd wp (DO, Т)) ==> wp (DO, Р апd поп ВВ) (8)
для всех состояний.
Эту теорему, которая известна также под названием «Ос-
ыовная теорема инвариантности для циклов», на интуитивном
уровне не трудно понять. Предпосылка (7) говорит нам, что
если предикат Р первоначально истинен и одна из охраняемых
команд выбирается для выполнения, то после ее выполнения
Р сохранит свою нсгивность. Иначе говоря, предохранители
гарантируют, что выполнение соответствующих списков опе-
раторов не нарушит истинности Р, если начальное значение Р
было истинным. Следовательно, вне зависимости от того, как
часто производится выборка охраняемой команды из имеюще-
гося набора, предикат Р будет справедлив при любой новой
проверке предохранителей. После завершения всей конструк-
ции повторения, когда ни один из предохранителей не явля-
ется истиной, мы тем самым закончим работу в конечном со-
стоянии, удовлетворяющем Р апd поп ВВ. Вопрос в том, за-
вершится ли работа правильно. Да, если условие wp (DO, Т)
справедливо 11 -вначале: поскольку любое состояние удовлет-
воряет Т, то wp (РО, Т) по определению является слабейшим
предусловием для начального состояния, такого, что запуск.
оператора DO приведет к правильно завершаемой работе.
Формальное доказательство основной теоремы для конст-
рукции повторения. основывается на фо~мальном описании
семантики этой конструкции (см. предыдущую главу), из кd-
торого мы выводим
Но(Т) =поп ВВ (9)
при k>O: ~ Hk(T) =wp(IF, Hk-i(T)) or поп ВВ ( 10)
Н0 (Р апd поп ВВ) =Р апd поп ВВ ( 11)
при k>O: Hk (Р апd поп ВВ) =wp (IF, Hk-l (Р апd
поп ВВ)) or Р апd поп ВВ ( 12)
Дае теоремы 65

Начнем с того, что докажем посредством математической


индукции, что предпосылка (7) гарантирует справедливость
при k~O: (Р апd Hk(T)) =ФНk(Р and поп ВВ) (13)
для всех состояний.
В силу отношений (9) и ( 11) отношение ( 13) справедливо.
при k=O. Мы покажем, что отношение ( 13) может быть дока-
зано при k =К (К>О) на основании предположения, ~то ( 13}
справедливо при k=K-1.
Р апd Нк(Т)=Р апd wp(IF, Нк-1(Т)) or Р апd поп ВВ.
=Р апd ВВ апd wp (IF, Нк-~ (Т)) or Р апd
поп вв
=Ф wp (IF, Р) апd wp (IF, Н к-~ (Т)) or Р апd
поп вв
=wp (IF, Р). апd Нк-~ (Т)) or Р. апd поп ВВ
=Ф wp (IF, Н к-1 (Р апd поп ВВ)) or Р апd
поп вв
=Нк (Р апd поп ВВ)
Равенство в первой строке следует из ( 1 О), равенство во
второй строке следует из того, что всегда wp (IF, R) =Ф ВВ,
логическое следование в третьей строке вытекает из (7),
равенство в четвертой строке основывается на свойстве 3
преобразователей предикатов, следование в пятой стро-
ке вытекает из свойства 2 преобразователей предикатов и из
индуктивного предположения (13) для k=K-1, и последняя
строка следует из (12). Итак, мы доказали (13) для k=K,
а следовательно, для всех значений k~O.
Наконец, для любой точки в пространстве состояний мы
имеем, в силу ( 13),
Р апd wp'(DO, Т) = (Е k: k~O: Р and Hk(T))
=Ф (Е k: k~O: Н k (Р and поп ВВ))
=wp (DO, Р and поп ВВ)
и тем самым доказана основная теорема (8) для-
конструкции повторения. Ценность основной, теоремы для
конструкции . повторения основывается на том, что ни в
предпосылке, ни в ее следствии не упоминается фактическое
число выборок охраняемой команды. Поэтому она применима
даже в тех случаях, когда это число не определяется началь-
ным состоянием.

3-6
6 О ПРОЕКТИРОВАНИИ
ПРАВИЛЬНО ЗАВЕРШАЕМЫХ КОНСТ.РУКЦИА

Основная теорема для конструкция повторения примени-


тельно к условию Р, сохраняемому инвариантно истинным, ут-
верждает, что
(Р. апd wp (DO, Т)) =9 гоо, Р and поп _ВВ)
' '

Здесь член wp (DO, Т) представляет собой слабейшее


предусловие, такое; что конструкция повторения завершится.
Если задана произвольная конструкция DO, то в общем слу-
чае очень трудно (а может быть, невозможно} определить
wp (DO, Т). Поэтому я предлагаю проектировать наши кон-
струкции повторения, постоянно помня о требовании заверши-
мости, т. е. предлагаю искать подходящее доказательство за·
вершимости и строить программу таким способом, чтобы она
удовлетворяла предположениям, на которых основывается это
доказательство.
. Предположим опять, что Р - отношение, которое сохраня-
ется инвариантно истинным, т. е.
(Р апd ВВ) =9 wp (IF, Р) для всех состояний ( 1)
Пусть t - конечная целочисленная функция от текущего со-
стояния, такая, что
(Р апd ВВ) =9 (t>O) для всех состояний (2)
и, кроме того, для любого. значения tO и для всех состояний
(Р and ВВ and t~tO+·I) =Ф wp (IF, t~tO). (3)
Тогда мы докажем, что
Р =9 wp (DO, Т) для всех состояний
Сопоставив этот факт с основной теоремой для повторения,
мы можем заключить, что имеем для всех состояний ·
Р =9 wp (DO, Р апd поп ВВ) (5)
О проектировании правильно завершаемых конструкций f;J

Мы покажем это, доказав сначала методом математиче-


ской индукции, что
(Р апd t~k) =Ф Hk(T) для всех состояний (бj
справедливо при всех k~O. Начнем с обоснования истинности
(6) при k=O. Поскольку Н0(Т) =поп ВВ, нам требуется по-
казать, что
(Р апd t~O) =Ф поп ВВ для всех состояний (7)
Однако (7) .:___это просто другая форма записи выражения (2):
оба они равны выражению
поп Р or поп ВВ or (t>O)
и поэтому (6) справедливо при k=O.
Предположим теперь, что (6) справедливо при k=K; тогда
(Р and ВВ and t,~K+ 1) =Ф wp (IF, Р апd t~K)
=Фwp(IF, Нк(Т));
(Р and поп ВВ апd t~K + 1) =Ф поп ВВ
=Но(Т)
И эти два логических следования можно объединить (из
·А==9 С и В==;. D мы можем заключить, что справедливо
(А or В) =Ф (С or D)):
. (Р and t~K+1) =Ф wp{IF, Нк(Т)) or Н0(Т)=Нк+1(Т)
и тем самым истинность (6) доказана для всех k~O. Посколь-
ку t - ограниченная функция, мы имеем
(Ek:k~O:t~k)
и
Р=Ф (Е k: k;;:iO: Р and t~k)
=Ф (Е k:k~O:Hk(T))
=wp(DO, Т)

и тем самым докааано ( 4).


Интуитивно теорема совершенно ясна. С одной стороны, Р
останется истиной, а следовательно, t~ О тоже останется исти-
ной; с другой стороны, из отношения (3) следует, что каждая
выборка охраняемой команды приведет к эффективному
уменьшению t по крайней мере на 1. Неограниченное количе-
ство выборок охраняемых команд уменьшило б'ы Значение t
ниже любого предела, что привело бы к противоречию.
Применимость этой теоремы основывается на выполнении
условий (2) и (3). Отношение (2) является достаточно про-
стым, отношение (3) выглядит более запутанным. Наша ос-
з•
б8 Глава 6

новная теорема для конструкции повторения при


Q= (Р апё ВВ and t~to+ 1)
R= (t~tO)
{присутствие свободной переменной tO в обоих предикатах
является причиной того, что мы говорили о «паре предика-
тов») позволяет нам заключить, что условие (3) справедливо,
если
(A.j :·l~j~n: (Р and Bi ·a!Jd t.~t0+1) ==?wp(SLj, t~-tO))
Иначе говоря, нам нужно доказать для всякой охраняемой
команды, что выборка· Приведет к эффективному уменьше-
нию t. Помня о том, что t является функцией от текущего со-
стояния, мы можем рассмотреть
wp (SLJ, t~tO) (8)
Это предикат; включающий, помимо координатных перемен-
ных пространства состояний, также и свободную переменную
iO. До сих пор· мы рассматривали такой предикат как преди-
кат, характеризующий некое подмножество состояний. Однако
.для любого заданного состояния мы можем также рассматри-
вать предикат как условие, налагаемое на tO. Пусть tO= tmin
представляет собой минимальное решение уравнения (8) отно-
сительно tO; тогда мы можем интерпретировать значение tmin
как наименьшую верхнюю границу для конечного значения t.
Если вспомнить, что, подобно функции t, tmin также является
функцией от текущего состояния, то можно интерпретировать
предикат
tmln~t-1
как слабейшее предусловие, при котором гарантируется, что
выполнение SLJ уменьшит значение t по крайней мере на 1.
Обозначим это предусловие, где - мы повторяем - второй
аргумент является целочисленной функцией от текущего со-
стояния, через
wdec ( зс; t)
При этом инвариантность Р и эффективное уменьшение t га-
рантируются, если мы имеем при всех значениях j
(Р and BJ) =Ф (wp (SLJ, Р) and wdec(SL;, t)) (9):
Обычно практический способ отыскания подходящего пре-
дохранителя В; состоит в следующем. Уравнение (9) отно-
сится к типу
(Р and Q) =9 R
О проекзировании правильно завершаемых конструкций 69

где (практически вычислимое!) значение Q.. . нужно найти для


заданных значений Р и R. Мы замечаем, что
1. Q = R является решением.
2. Если Q = ( Q 1 апd Q2) является решением и Р =9> Q2,
то Q 1 тоже является решением.
3. Если Q = (Q 1 or Q2) является решением- и Р =;> поп Q2
(или, что сводится к тому же самому, (Р апd Q2) =F), то Ql
тоже является решением.
4. Если Q является решением и QI =;> Q, то QI тоже явля-
ется решением.
Замечание 1. Если, действуя таким образом, мы приходим
к кандидатуре Q для Bj, такой, что Р=Фпоп Q, то эта канди-
датура может быть далее упрощена (в соответствии с преды-
дущим наблюдением 3; поскольку при любом Q мы имеем Q =
=(ложь or Q)) к виду Q =ложь; это означает, что рассматри-
ваемая охраняемая команда введена неудачно; ее можно иск-
лючить из набора, потому что она никогда не будет выбирать-
ся. (Конец замечания 1.)
Замечаниг 2. Часто на практике расщепляют уравнение (9)
на два уравнения:
(Р апd Bj) =Фwр (SLj, Р) (9а)
и
(Р апd Bj) ~wdec(SLj, t) (96)
и рассматривают их по отдельности. Тем самым разделяются
две вадачи: (9а) относится к тому, что остается инвариант-
ным, тогда как (96) относится к тому, что обеспечивает про-
движение вперед. Если, имея дело с уравнением (9а), мы при-
ходим к решению Bj, такому, что Р =ФВJ, то тогда очевидно,
что это условие не будет удовлетворять уравнению (96), по-
скольку при таком В j инвариантность· Р привела бы к недетер-
минированности. (Конец замечания 2.)
Таким образом, мы можем построить конструкцию DO,
такую, что
Р=9> wp (DO, Р апd поп ВВ)
Наши условия Bi должны быть достаточно сильными, чтобы
удовлетворялись следования (9); в результате этого новое га-
рантируемое постусловие Р апd поп ВВ может оказаться
слишком слабым и не обеспечить нам желаемого постусло-
вия R. В таком случае мы все-таки не решили нашу проблему
и нам следует рассмотреть другие возможности.
7 ПЕРЕСМОТРЕННЫИ АЛГОРИТМ ЕВКЛИДА

Рискуя наскучить моим читателям, я посвящу еще одну


главу алгоритму Евклида. Полагаю, что к этому времени не-
которые из читателей уже закодируют его в виде
х, у:=Х, У;
do x:;i6=y-+if х>у~х:=х-у
О у>х-+у:=у-х
fi
od;
печатать(х)
'
где предохранитель конструкции повторения гарантирует, что
конструкция выбора не. приведет к отказу. Другие читатели
обнаружат, что этот алгоритм можно закодировать более
просто· следующим образом:
Х, у:=Х, У;
do х> у-+х: ::== х-у
Оу>х-+у:=у-х
od;
печатать(х)
Попробуем теперь забыть игру на листе картона и попы-
таемся изобрести заново алгоритм Евклида для отыскания
наибольшего общего делителя. двух положительных чисел Х
и У. Когда мы сталкиваемся с такого рода проблемой, в прин-
ципе всегда возможны два подхода.
Первый состоит в том, чтобы пытаться следовать опреде-
лению требуемого ответа настолько близко, насколько это
возможно. По-видимому, -мы могли бы сформировать таблицу
делителей числа Х; эта таблица содержала бы только конеч-
ное число элементов, среди которых имелись бы 1 в качестве
наименьшего и Х в качестве наибольшего элемента. (Если
Х = 1, то наименьший и наибольший элементы совпадут.) За-
Пересмотренный алгоритм Евклида 71

тем мы могли бы сформировать также аналогичную таблицу


делителей У. Из этих двух таблиц мы могли бы сформировать
таблицу чисел, присутствующих в них обеих. Она представля-
ет собой таблицу общих делителей чисел Х и· У и обязательно
является непустой, так как содержит элемент 1. Следова-
тельно, из этой третьей таблицы мы можем выбрать (посколь-
ку она тоже конечная') максимальный элемент, и он был бы
наибольшим общим делителем.
Иногда близкое следование определению, подобное обри-
сованному выше, является лучшим из того.э что мы можем
сделать. Существует, однако, и другой подход, который стоит
испробовать, если мы знаем (или можем узнать) свойства
функции, подлежащей вычислению. Может оказаться, что мы
знаем так много свойств, что они в совокупности определяют
эту функцию; тогда мы можем попытаться построить ответ,
используя эти свойства.
В случае наибольшего общего делителя мы замечаем, на-
пример, что, поскольку делители числа э-- х те же самые, что
и для самого числа х, НОД (х, у) определен также для отри-
цательных аргументов и не меняется, если мы изменяем знак,
аргументов. Он определен и тогда, когда один из аргументов
равен нулю; такой аргумент обладает бесконечной таблицей
делителей (и поэтому нам не следует пытаться строить эту
таблицу'}, но поскольку второй аргумент ( =i=O) обладает ко-
нечной таблицей делителей, таблица общих делителей явля-
ется все же непустой и конечной. Итак, мы приходим к за-
каюченню. ЧТО НQД (Х, у) определен ДЛЯ ВСЯКОЙ пары (х, у),
такой, что (х, у)-::/= (О, О). Далее, в силу симметр.ии понятия
«общий» наибольший общий делитель является симметричной
функцией своих двух аргументов. Еще одно небольшое умст-
венное усилие позволит нам убедиться в том, что .наибодьший
общий делитель двух аргументов не изменяется, если мы за-
меняем один из этих аргументов их суммой или разностью.
Объединив все эти результаты, мы можем записать:
для (х, у) =i= (О, О)
(а) НОД(х, у) =НОД(у, х).
(б) НОД (х, у) =НОД(-х, у).
(в) НОД(х, у) =Н.ОД(х+у, у) =НОД(х-у, у) и т. д.
(г) НОД (х, у) =abs (х), если х= у.
Предположим для простоты рассуждений, что этими че-
тырьмя свойствами исчерпываются наши познания о функции
НОД. Достаточно ли ик? Вы видите, что первые три отноше-
ния выражают наибольший общий делитель чисел х и у че-
рез' над для _другой пары, ~ последнее свойство выражает
72 Глава 7

его непосредственно· через х. И в Этом уже просматриваются


контуры алгоритма, который для начала обеспечивает истин-
ность
Р= (НОД(Х, У) =НОД(х, у))
(это легко достигается путем присваивания «х, у: =Х, У»),
после чего мы «утрамбовываем» пару значений (х, у) таким
образом, чтобы в соответствии с (а), {б) или (в) отношение
Р оставалось инвариантным. Если. мы можем организовать
этот процесс утрамбовки так, чтобы достигнуть состояния,
удовлетворяющего х=у, то, согласно (г), мы находим иско-
мый ответ, взяв абсолютное значение х.
Поскольку наша конечная цель состоит в том, чтобы обес-
печить при инвариантности Р истинность х=у, мы могли бы
испытать в качестве монотонно убывающей функции функцию
t=abs(x-y).
Чтобы упростить наш анализ (всегда похвальная целы},
мы отмечаем, что если начальные значения х и у неотрица-
тельные, то ничего нельзя выиграть введением отрицательного
значения: если присваивание' х: =Е обеспечило бы х<О·, то
присваивание х: =-Е никогда не привело- бы к получению
большего конечного значения t (потому что у~О). Поэтому
мы усиливаем наше отношение Р, которое должно сохранять-
ся инвариантным:
Р= (Pl and Р2)
при
Р1 = (НОД (Х, У)= над (х, у))
и
Р2= (х~О and у~О)
Это означает, что мы отказываемся от всякого использования
операций х: = -х и у:= -у, т. е. преобразований, допусти-
мых согласно свойству {б). Нам остаются операции
из (а): х, у:=у, х
из (в): Х:=х+у у:=у+х
х:=х-у у:=у-х
х:=у-х у:=х-у
Будем заниматься ими по очереди и начнем с рассмотре-
ния х, у :=у, х:.
wp («х, у: =У, Х», abs(x-y) ~tO) = (abs(y-x) ~tO)
поэтому
tmin(x, у) =abs(y-x)
Пересмотренный алгоритм Евклида 73

следовательно
wdec ( «Х, у:= у, Х», abs (х-у)) = (abs (у-х) ~
~abs(x-y)-1) =F
И здесь - для тех, кто не поверил бы без формального вы-
вода,- мы доказали (или, если угодно, обнаружили) с по-
мощью нашего исчисления, что преобразующая операция
х, у:= у, х не представляет интереса, так как она не может
привести к желаемому эффективному уменьшению выбранной
нами функции t.
Следующей испытыва~тся операция х: =х+ у, и мы нахо-
дим, снова применяя исчисление из предыдущих глав:
wp ( «Х:::::;: х+ у», abs (х-у) ~ to-) = (abs (х) ~ tO)
tmin (х, у) = abs (х) = х
(мы ограничиваемся состояниями, удовлетворяющими Р)
wdec(«X:=x+y», abs(x-y)) = (tmin(x, у) ~t(x, у)-1)
= (x~abs(x-y)-1)
= (х+ 1 ~abs (Х-:-У))
= (х+ 1 ~х-у or х+ 1 ~у-х)
Поскольку из Р следует отрицание первого члена и к тому
же Р =Ф wp ( «Х: =х+ у», Р), то уравнение нашего предохра-
нителя
(Р and Bj) =Ф (wp (SLj, Р) and wdec (SLj, t))
удовлетворяется последним членом, и мы нашли нашу первую,
а также (из соображений симметрии) нашу вторую охраняе-
мую команду:

и
У+ 1 ~x-y-ry:=y+x
Аналогично мы находим (формальные манипуляции предо-
ставляются в качестве упражнения прилежному чит.ателю)
1 ~у and 3 *' у~2 * x-I-rx: =х-у
и
1 ~х and 3 * х~2 * y-I-ry: =у-х
и
х+ 1 ~y-x-rx: = у-х
и
.У+ 1 ~x-y-ry: =Х-у
74 Глава 7'

Разобравшись в том, чего мы достигли, мы вынуждены


прийти к досадному заключению, что способом, намеченным
в конце предыдущей главы, нам не удалось решить свою за-
дачу: из Р апd поп ВВ не следует, что х= у. (Например, при
(х, у)= (5,7) значения всех предохранителей оказываются
ложными.) Мораль сказанного, разумеется, в том, что наши
шесть шагов не всегда обеспечивают такой путь из начального
состояния в конечное состояние, при котором abs (х-у) моно-
тонно уменьшается. Поэтому нам ·нужно испытать другие
возможности.
Для начала заметим, что не повредит нескольkо усилить
условие Р2:
Р2= (х>О апd у>О)
так как начальные значения х и у удовлетворяют такому ус-
ловию, и, кроме того, нет никакого смысла в генерации рав-
ного нулю значения, поскольку это значение может возник-
нуть .только при вычитании в состоянии, где х =н. т.. е: когда
уже достигнуто конечное состояние. Но это только малая мо-
днфикация: основная модификация должна быть связана с
введением новой функции .t, и я предлагаю взять такую функ-
цию t, которая только ограничена снизу в силу инвариантно-
сти Р. Очевидным примером является
t=x+y
Мы выясняем, что для одновременного присваивания
wdec(«x, у:=у; х», х+у) =F
и поэтому одновременное присваивание отвергается.
Мы находим для присваивания х: = х+ у
wdec(«x:=x+y», х+у) = (у<О)
Истинность этого выражения исключается истинностью инва-
риантного отношения Р, а следовательно, такое присваивание
(наряду с присваиванием у:= у+х) также отвергается.
Однако- для следующего присваивания х: = х-у мы нахо-;_
дим
wdec(«x:=x-y», х+у) = (у>О)
т. е. условие, которое Логически следует из условия Р (уси-
ленного мною ради этого).
Окрыленные надеждой, мы изучаем
wp(«X:=x-y», Р) = (НОД(Х, У)=
=НОД(х-у, у) апd х~у>О апd у>О)
Пересмотренный алгоритм Евклида 75

Крайние члены можно отбросить, потому что они следуют из


Р, и у нас остается средний член; итак,-мы находим
х>у-+х:=х-у
и
у>~-+у:=у-х
И на этом мы могли бы прекратить исследование, так как,
когда значения обоих предохранителей становятся ложными,
выполняется желаемое нами отношение х =у. Если мы продол-
жим, то найдем третий и четвертый варианты:
х>у-х and у>х-+х:=у-х
и
у>х-у and х>у-+у:=х-у
но не ясно, что можно выиграть их включением.
Упражнения
1. Исследуйте при том же Р выбор t=max(x, у).
2. Исследуйте при том же Р выбор t=x+2 *у.
3. Докажите, что при Х>О и У>О следующая программа,
оперирующая над четырьмя переменными,
х, у, и, v:=X, У, У, Х;
do х>у-+х, v:=x-y, v+и
О у>х-+у, и:=у-х, и+v
od;
печатать ((х+у)/2); печатать ((u+v)/2)
печатает наибольший общий делитель чисел Х и У, а следом
за ним их наименьшее общее кратное. (Конец упражнений.)
Наконец, если наш маленький алгоритм запускается при
паре (Х, У), которая не удовлетворяет предположению Х>О
and У>О, то произойдут неприятности: если (Х, У)= (О, О), то
получится неправильный нулевой результат, а если один из
аргументов отрицательный, то запуск приведет к бесконечной
работе. Этого можно избежать, написав
if Х>О and У>О-+
х, у:=Х, У;
do х>у-+х:=х-уО у>х-+у_:=у-х od;
печатать(х)
fi
76 Глава 7

Включив только один вариант в конструкцию выбора; мы


явно выразили условия, при которых ожидается работа этой
маленькой программы. В таком виде это хорошо защищенный
и довольно самостоятельный фрагмент, обладающий тем при-
ятным свойством, что попытка запуска вне его области опре-·
деления приведет к немедленному отказу.
8 ФОРМАЛЬНОЕ РАССМОТРЕНИЕ
НЕСКОЛЬКИХ НЕБОЛЬШИХ ПРИМЕРОВ

В этой rлаве я проведу формальное построение несколь-


ких небольших программ для решения простых задач. Не сле-
дует понимать эту главу как предложение строить программы
только так и не иначе: такое предложение было бы довольно
смехотворным. Я полагаю, что многим из моих читателей ·зна-
комо большинство примеров, а если нет, они, вероятно, не за-
думываясь смогут написать любую из этих программ.
Следовательно, построение программ проводится здесь сов-
сем по другим причинам. Во-первых, это ближе познакомит
нас с формализмом, .развитым в предыдущих главах. Во-вто-
рых, убедит нас в том, что по крайней мере в принципе, фор-
. мализм способен сделать ясным и совершенно строгим то, что
часто объясняется при помощи энергичной жестикуляции.
В-третьих, многие · из нас настолько хорошо знают эти про-
граммы, что уже забыли, каким образом давным-давно мы
убедились в их правильности: в этом отношении настоящая
глава напоминает начальные уроки планиметрии, которые по
традиции посвящаются доказательству очевидного. В-четвер-
тых, мы можем случайно обнаружить, к своему удивлению, что
маленькая знакомая задачка не такая уже знакомая. Нако-
нец, процесс построения программ может пролить свет на осу-
ществимость, трудности и возможности автоматического со-
ставления программ или использования машины в процессе
программирования. Это может оказаться важным, даже если
автоматическое составление программ не вызывает у нас ни
малейшего интереса, потому что может помочь нам лучше
оценить ту роль, которую могут или должны играть наши изо-
бретательские способности.
В моих примерах я буду формулировать требования зада-
чи в форме «для фиксированных х, у, ... », что является сокра-
щенной записью формы «для любых значений хО, уО, ... пост-
условие вида х=хО and у=уО and ... должно вызывать пред-
условие, из которого следует, что х = хО and у= уО and ... >. Эта
связь между постусловием и предусловием будет гарантиро-
вана тем, что мы будем относиться к таким величинам как к
78 Глаsа 8

«временным константам»; они не будут встречаться в левых


частях операторов присваивания.
Первый пример
Для фиксированных х и у обеспечить истинность отноше-
ния R(т): .
(m=x or m=y) and т~х and т~у
Для любых значений х и у отношение m::::dx может стать
истинным только в результате присваивания т: = х; следова-
тельно, истинность (m=x or m=y) обеспечивается только
выполнением либо т: = х, либо т: =н- В виде блок-схемы:

Рис. 8.1
Все· дело в том, что на. входе нужно сделать правильный
выбор.зкоторый обеспечит истинность R (m) после завершения
вычислений, Для этого мы «проталкиваем постусловие через
альтерна тивы»:

Рис. 8.2
Формальное рассмотрение нескольких небольших примеров 79

и мы получили предохранители! Так как


R (х) = ( (х=х or х= у) and х~х and х~у) = (х>-у)
и
R(y) = ((у=х or н=н) and у~.х апё у~у) = (у'>х)
мы приходим к нашему решению:
if x~y-rm:=x О y~x-rm:=y fi
Поскольку (х~у or у~ х) = Т, отказа никогда не произ'ойдет
(попутно мы получили доказательство существования: для лю-
бых значений х и у существует т, удовлетворяющее R (т)).
Поскольку (х;;::;у and у~х) =l=F, наша программа не обяза-
тельно детерминирована. Если первоначально х=у, то ока-
зывается неопределенным, какое из двух присваиваний будет
выбрано для исполнения; такая недетерминированность со-
вершенно корректна, так как мы показали, что выбор не имеет
эначеиия.
Замечание. Если бы среди доступных примитивов имелась
функция «тах», мы могли бы написать программу
т := тах(х, у), поскольку R (тах(х, у))= Т. (Конец замеча-
ния.)
Полученная программа не производит очень сильного впе-
чатления; с другой стороны, мы видим, что в процессе вывода
программы из постусловия на долю нашей изобретательности
почти ничего не осталось.
Второй пример
Для фиксированного значения п(п>О) задана функция
f (i)для O~i<n. Обеспечить истинность R:
O~k<n and (А i: O~i<n: f (k) ;;з::f (i))
Поскольку наша программа должна работать при любом по-
ложительном значении п, -трудно себе представить, как мож-
но обеспечить истинность R, не прибегая к помощи цикла;
поэтому мы ищем отношение Р, которое легко можно сделать
истинным в начале программы и такое, что в конце (Р and поп
ВВ) =ФR. Следовательно, отыскивая Р, мы ищем отношение,
более слабое, чем R; иначе говоря, мы хотим получить обоб-
щение нашего конечного состояния, Стандартный способ обоб-
щёния отношения - это замена константы переменной (воз-
можно, в ограниченном интервале}, и здесь мой опыт подска-
зывает мне, что нужно заменить константу п новой
переменной, например j, и взять в качестве Р
O~k<j~n and (А i: O~i<j: f (k) ;;з::f (i))
80 Глава 8

где· условие j~n добавлено для того, чтобы область опреде-


ления функции f была конечной. Теперь, имея такое обобще-
ние, мы легко получаем
( Р and j = п) =Ф R
Чтобы подтвердить возможность использования таким об-
разом выбранного Р, мы должны располагать легким средст-
вом для обеспечения истинности Р в начале программы. По-
скольку
(k=Oandj=l) ==ФР
мы отваживаемся предложить следующую, структуру для на-
шей программы (комментарии даются в скобках):
k, j: =О, 1 {Р стало истинным};
do j=l=n-+некий шаг к j=n при инвариантности Р od
{R стало истинным}
Снова мой опыт подсказывает мне выбрать в качестве мо-
нотонно убывающей функции t текущего состояния функцию
t= (n-j), которая и в самом деле такова, что Р ~ (t~O).
Чтобы гарантировать монотонное убывание t, я предлагаю
увеличивать j на 1, после чего мы можем написать
wp(«j:=j+ 1», Р) =
O~k<j+ 1 ~п and (А i: O~i<i+ 1: f (k) ~f (i)) =
O~k<j+l~n and (Ai:O~i<j:f(k)~f(i)) and f(lг)~f(j)
Первые два члена следуют из Р and j =1= п (потому что
(j~n and j=l=n) =9 (j + 1 ~п), и именно поэтому мы решили
увеличить j только на 1). Следовательно,
(Р and f=l=n and f(k)~f{j))=Ф wp(«j:=j+l», Р)

и мы можем использовать последнее условие в качестве пре-


. дохраяителя. Программа
k, j:=O, 1;
do j=l=n-+if f(k)~f(j)-+j:=j+I fi od

действительно даст правильный ответ, если она завершится


нормально. Однако нормальное завершение не гарантировано,
поскольку конструкция выбора может привести к отказу - и
это действительно так и будет, если k=O не удовлетворяет R.
Формальное рассмотрение нескольких небольших примеров 81

.Еслн неверно, что f (k) ;;:;:f(j), мы можем сделать это отноше-


ние верным при помощи присваивания k : = j и продолжить
наши исследования:
wp(«k, j:=j, j+I», Р)=
O~j<j+ 1 ~п and (А i: O~i<j+ 1: f (j) ;;:;:f (i)) =
O~j<j + 1 ~п and (А i: O~)<j: f (j) ;;:;:f (i))
С огромным облегчением мы замечаем, что
.(Р and j::/=n and f(k) ~f(j))-=Фwp(«k, j:=j:j+l», Р)

и следующая программа будет работать, не подвергаясь опас-


ности отказа:
k, j:=O, 1;
do j=l=-n->- if f (k) ;;:;:f (j) ~ j: = j + 1
'Of(k)~f(j) ~k,1j:=j, j+I fi od

Следует сделать несколько примечаний. Первое: посколь-


ку предохранители конструкции выбора не обязательно иск-
лючают друг друга, программа таит в себе внутреннюю неде-
терминированность того же типа, что и в первом примере. Эта
недетерминированностъ может проявиться и внешне. Функ-
ция f могла оказаться такой, что конечное значение k неодно-
значно; в этом случае наша программа может выработать лю-
бое допустимое значение!
Второе примечание: то, что мы построили правильную про-
грамму, не означает, что мы справились с задачей. Программи-
рование в равной степени является как математической, так и
инженерной дисциплиной; мы должны заботиться о правиль-
ности в той же мере, как скажем, и об эффективности. Исходя
из предположения, что на вычисление значения функции f
для данного аргумента затрачивается относительно много
времени, хороший инженер должен обратить внимание- на то,
что в программе, по всей вероятности, будет многократно вы-
числяться f (k) для одного и того же эначения k. В этом .случае
рекомендуется пойти на сделку и пожертвовать некоторым
объемом памяти, чтобы сэкономить часть времени, затрачи-
ваемого на вычисление. Но при этом наши усилия, направ-
ленные на повышение эффективности программы и уменьше-
ние затрат времени, ни в коей мере не должны служить изви-
нением за внесение беспорядка в программу. (Это очевидная
вещь, но я хочу подчеркнуть эту мысль. потому что очень час-
82 Глава 8

то запутанность программы оправдывается ссылками на эф-


фективность. Однако при ближайшем рассмотрении такое оп-
равдание всегда оказывается необоснованным; должно быть"
потому, что запутанность программы всегда неоправданна.)
Чтобы аккуратно реализоватъ обмен времени вычислений на
память, можно воспользоваться методом введения одной или
нескольких дополнительных переменных, чье значение можно
использовать, так как сохраняется инвариантным некоторое
отношение. В 'данном примере было замечено, что возможны
частые повторные вычисления f (k) для одного и того же k"
а это наводит на, мысль о введении дополнительной перемен-
ной, -скажем- тах; и расширении инвариантного отношения за
счет дополнательного члена

max=f (k)

Это отношение должно стать истинным, когда k получит на-


чальное значение, и должно сохраняться 'инвариантным (при
помощи явного присваивания значения переменной тах) пос-
ле изменения k. Мы приходим к следующей программе:

k, j, тах: =0, 1, f (О);


do j=Fn-+ if max~f (j)-+ j: = j+ 1
О max~f(j)-+k, j, max:=j, j+l, f(j) fi od

Вероятно, эта программа гораздо более эффективна, чем


·наШа предыдущая версия. В: таком случае хороший инженер
на этом не остановится, потому что теперь он обнаружит, что
мог бы распорядиться повторными вычислениями' f (j) для од-
ного и того же j. Для этого можно ввести дополнительную пе-
ременную, скажем н; и обеспечить инвариантность отношения

Однако мы не может сделать это на том же глобальном


уровне, как для нашего предыдущего члена: значение j=n не
исключено, а при этом значение f (j) не обязательно определе-
но. Следовательно, истинность отношения h=f (j) должна аа-:
ново обеспечиваться после каждой проверки, показывающей,
что j=Fn; по завершении внешней охраняемой команды (так
сказать, «к_ак раз перед od») мы имеем h=f(j-1), но нас это
Формальное рассмотрение нескольких небольших примеров 83

не должно беспокоить, и пусть все остается так, как есть.


k, j, тах:=О, 1, f(O); ,
do j=l=n"-+h: =f (j);
if тах;;:з;h-+ j:=j+ 1
О max~h_,,..k, j, тах:= j, j+ 1, h, fi od

Последнее примечание касается не столько нашего реше-


ния задачи, сколько наших соображений о подходе к реше-
нию. Мы говорили о математическом подходе, мы говорили об
инженерном подходе и провели разделение между этими под-
ходами, сосредоточивая внимание .то на одном, то на другом
аспекте. Хотя разделение подходов абсолютно необходимо,
когда речь идет о более сложных задачах, я должен подчерк-
нуть, что, сосредоточивая внимание на одном из аспектов, не
следует полностью игнорировать другие аспекты. Разрабаты-
вая математическую часть проекта, мы должны помнить, что
даже математически правильная программа может погибнуть,
-если она плоха с инженерной точки зрения. Аналогично, осу-
ществляя «сделку» между памятью и временем, мы должны
делать это аккуратно .и систематически, чтобы по неряшли-
вости не внести ошибок в программу; кроме того, хотя пред-
варительно был проведен математический анализ, мы долж-
ны к тому же достаточно хорошо разбираться в задаче, чтобы
судить о том, приведут ли предполагаемые изменения к зна-
чительному улучшению программы.

Замечание. До того как я начал пользоваться этими фор-


мальными построениями, я всегда испольвовал «j<n» в каче-
стве предохранителя в этой конструкции повторения, теперь я
должен отучиться от этой привычки, так как в случае, подоб-
ном данному, конечно, предпочтительнее предохранитель
«j=l=n». Для этого имеются две причины. Предохранитель
«j=l=n» позволяет нам сделать вывод, что по завершении про-
граммы j = п, не ссылаясь при этом на инвариантное отноше-
ние Р; тем самым упрощается дискуссия о том, что дает нам
вся конструкция в целом по сравнению ·.с предохранителем
«j<n». Гораздо важнее, однако, что предохранитель «j=l=n»
ставит завершение программы в зависимость от (части) ин-
вариантного отношения, а именно j~n, и поэтому ему следует
отдать предпочтение, так как его использование приведет к
усилению конструкции. Если в результате выполнения j: =
j + 1 значение j так возрастет, что станет справедливым отно-
шение j>n, предохранитель «j<n» не поднимет тревогу, тогда
84 Рлава В

как предохранитель «j=Fn» по крайней мере не допустит нор-


мального завершения. Такой аргумент представляется вполне
весомым, даже если не принимать во внимание сбой машины.
Пусть в последовательности х0, х1, х2, ... значение х0 задается,
а значение х, для i>O определяется как Xi=f(xi-1), где f-
некоторая вычислимая функция. Будем внимательно и точно
следить за тем, чтобы сохранялось инвариантным отношение
Х =Xi. Предположим, что в программе имеется монотонно
возрастающая переменная п, такая, что для некоторых значе-
ний п нас интересуют Xn. При условии что п~ i, мы всегда
можем сделать истинным отношение Х =Xn при помощи
do i=Fn-+i, X:=i+1, f(X) od
Если же отношение п~ i не обязательно имеет место (мо-
жет быть, последующие изменения в программе привели к то-
му, что в процессе вычислений п будет не только возрастать),
приведенная выше конструкция (к счастьюг) не придет к за-
вершению, в то время как конструкция
do i<n-+ i, X:=i+ 1, f(X) od
не обеспечит истинности отношения Х =Xn. Мораль этой исто-
рии такова, что при прочих равных условиях мы должны вы-
бирать наши предохранители как можно более слабыми.
(Конец замечания.)
Третий пример
Для фиксированных а(а~О) и d(d>O) требуется обеспе-
чить истинность R:
dJ (a_.r)
O~r<d
(Здесь вертикальная черта
.« 1and
» заменяет собой слова
«являет-
ся делителем».) Иначе говоря, нам требуется вычислить наи-
меньший неотрицательный остаток r, полученный в результате
деления а на d. Чтобы эта задача действительно была задачей,
мы должны ограничить использование арифметических опе-
раций только операциями сложения и вычитания. Поскольку
условие dl (a-r) выполняется при к=а и при этом, так как
а~О, верно, что O~r, предлагается выбрать в качестве инва-
риантного отношения Р:
0~11' and dl (a-r)
В качестве функции t, убывание которой должно обеспе-
чить завершение работы программы, мы выбираем само r. По- ·
скольку изменение r должно быть таким, чтобы неизменно вы-
полнялось условие d\ (а-г), r можно Изменять только на чис-
Формальное рассмотрение нескольких небольших примеров 85·

ло, кратное d, например на само d. Таким образом, нам, как


оказалось, предлагается вычислить
wp(«r:=r-d», Р) and wd€c («r:=r-d», r) =
O~r-d and dl (a-;+d) and d>O
Поскольку член d>O можно было бы добавить к инвари-
антному отношению Р, обоснования требует только первый
член; мы находим соответствующий предохранитель «r~d»
и пробуем составить такую программу: ·
if а~О and d>O-+
r:=a;
do r~d-+r:= r-d 1 od
fi
По завершении программы стало истинным отношение
Р and поп r~d. из чего следует истинность Р, и, таким обра-
зом, задача решена.
Предположим теперь, что нам, кроме того, потребовалось
бы присвоить q такое значение, чтобы по окончании програм-
мы было бы также верно, что
a=d *q+r
иначе говоря, требуется найти не только остаток, но и частное.
Попробуем добавить этот член к нашему инвариантному от-
ношению. Поскольку
(a=d * q+r) =Ф (a=d * (q+ 1) + (;-d))
мы приходим к программе
if а~О and d>O-+
q, r:=O, а;
'do r~d-+q, r:=q+ 1, r-d od
fi
На выполнение приведенных выше программ будет, конечна,
затрачиваться очень много времени, если частное велико. Mo-
жем ли мы сократить это время? Очевидно, этого можно добить-
ся, если уменьшать r на величину, кратную и большую d. Вво-
дя для этой цели новую переменную, скажем dd, мы получаем
условие, которое должно неизменно выполняться на протяже-
нии всей работы программы:
d\dd and dd~d
86 Глава 8

Мы можем ускорить . нашу первую программу, заменив


«п : = r-d»уменьшением, возможно повторным, r на dd, пре-
доставляя в ,то же время возможность dd, первоначально рав-
ному d, довольно быстро возрасгать, например каждый раз
удваивая dd. Тогда мы приходим к следующей программе:
ff а >О and d >О -
r:=a;
do r>d-
dd :=d;
do r>dd-r:=r-dd;
#
dd:=dd+dd od
od
. fi
Ясно, что ?Тношение O~r and .dl (a-r) сохраняется инвари-
антным, и поэтому программа обеспечит истинность R, если
она завершится нормально, но так ли это? Конечно, так, по-
скольку .внутренний 1:1-~.кл, который завершается при dd> О,
возбуждается только при начальных со~т~я!_iи~~.' удовл,етвqр~-
ющих условию r~dd, и поэтому уменьшение r ~ = r-dd вы-
полняется по крайней мере один раз для каждого повторения
внешного цикла.
Но такое рассуждение (хотя и достаточно убедительное!)
является весьма неформальным, а поскольку в этой главе мы
говорим о формальном построении программ, мы можем по-
пробовать сформулировать и доказать теорему, которой инту-
итивно пользовались.
Пусть IF, DO и ВВ понимаются в обычном смысле, и
Р- это отношение, сохраняющееся инвариантным, т. е.
(Р and ВВ) =9 wp (IF, Р) 'для всех· состояний (1)
и пусть t - целочисленная функция, такая, что для любого
значения tO и для всех состояний
(Р and ВВ and t ~ tO+ 1) =9 wp (IF, {-<. tO) (2)
или, что то же,
(Р and ВВ) ~ wdec (IF, t) для всех состояний (3)
Тогда для любого значения tO и для всех состояний
(Р and ВВ and wp (DO, Т) and t ~ tO+ 1) ~ wp (DO, t<tO) (4)
или, что то же,
(Р and ВВ and wp пэо, Т)) =9 wdec (00, t) (5)
Формальное рассмотрение нескольких небольших примеров 87

В словесной формулировке: если сохраняемое инвариантным


отношение Р гарантирует, что каждая выбранная для испол-
нения охраняемая команда вызывает действительное умень-
шение t, то конструкция повторения вызовет действительное
уменьшение t, если она нормально завершится после по край-
ней мере одного выполнения какой-либо охраняемой команды.
Теорема настолько очевидна, что было бы досадно, если бы ее
доказательсгво оказалось трудным, но, к счастью, это не так.
Мы покажем, что из ( 1) и (2) следует, что для любого зна-
чения tO и для всех состояний
(Р and ВВ and Hfl (Т) and t-< to+ 1) =Ф Hk (t ~ tO) (6)
для всех k~O. Это справедливо для k=O, поскольку (ВВ
and Н0(Т)) =F, и, исходя из предположения, что (6)
справедливо для k=K, мы должны показать, что (6) справед-
ливо и для k=K + 1.
(Р and ВВ and Н к+~ (Т) and t < ю+
1)
=Ф wp (IF, Р) and wp (IF, Н к (Т)) and wp (IF, t < tO)
=wp(IF, .Р and Нк(Т) and t<tO)
=Ф wp ((IF, (Р ~nd ВВ and Н к (Т) and t < to+ 1) or (t < tO
and поп ВВ))
=Ф wp (IF, н к (t ~ tO) or Н0 (t ~ tO))
= wp (IF, Нк (t-< tO))
=9 wp (IF, Н к (t ~ tO)) or Н0 (t-< tO)
=Н K+l (t ~ tO)
Первая импликация следует из ( 1), определения Н к+1 ( Т)
и (2); равенство в третьей строке очевидно; импликация в чет-
вертой строке выводится при помощи присоединения
(ВВ or поп ВВ) и последующего ослабления обоих членов;
импликация в пятой строке следует из (6) при k =К и опреде-
ления Н0 (t::;;;;, tO); остальное понятно. Таким образом, мы до-
казали справедливость (6) для всех k~O, а отсюда немедлен-
но вытекают (4) и (5).

Упражнение
Измените нашу вторую программу таким образом, чтобы
она вычисляла также и частное, и дайте формальное доказа-
тельство правильности вашей программы. (Конец упражне-
ния.)
88 Глава 8

Предположим теперь, что имеется маленькое число, ска-


жем 3, на которое нам позволено умножать и делить, и что эти
операции выполняются достаточно быстро, так что имеет
смысл воспользоваться ими. Будем обозначать произведение
«m * 3» (или «3 * m») и частное «m/3»; последнее выражение
будет использоваться только при условии, что первоначально
справедливо 3 I т. (Мы ведь работаем с целыми числами, не
так ли?)
И опять 'мы пытаемся обеспечить истинность желаемого от-
ношения R при помощи конструкции повторения, для которой
инвариантное отношение Р выводится путем замены констан-
ты переменной. Заменяя константу d переменной dd, чьи зна-
чения будут представляться только в виде d * (степень 3), мы
приходим к инвариантному отношению Р:
О< г <dd and dd 1 (а-г) and (Ei: i >О: dd=d •3l)

Мы обеспечим истинность этого отношения и затем, сохра-


няя его инвариантным, постараемся достичь состояния, при ко-
тором d=dd.
Чтобы обеспечить истинность Р, нам понадобится еще одна
,конструкция повторения: сначала' мы обеспечим истинность
отношения
O<r and dd) (a-r) and (Ei:i>O:dd=d•31)
а затем будем увеличивать dd, пока его значение не станет
достаточно большим и таким, что r<dd. Получим следующую
программу:
if а> О and d >О -
r,dd:=a,d;
do r~dd-dd:=dd*3 od;
do dd=Fd-dd:= dd/3;
do r>dd-r:=r-dd od
od
fi

Упражнение

Измените приведенную выше программу таким образом,


чтобы она вычисляла: также и частное, и дайте формальное
доказательство правильности вашей программы. Это доказа-
-тельство должно наглядно показывать, что всякий раз, когда
вычисляется dd/3, имеет место Зldd. (Конец упражнения.)
Формальное рассмотрение нескольких небольших примеров 89

Для приведенной выше программы характерна довольно


распространенная особенность. На внешнем уровне две кон-
струкции повторения расположены подряд; когда две или
больше конструкций повторения на одном и том же уровне
следуют друг за другом, охраняемые команды более поздних
конструкций оказываются, как правило, более сложными, чем
команды предыдущих конструкций. (Это явление известно как
«закон Дейкстры», который, однако, не всегда выполняется.)
Причина такой тенденции ясна: каждая конструкция повто-
рения добавляет свое «апd поп ВВ» к отношению, которое она
сохраняет инвариантным, и это дополнительное отношение
следующая конструкция также должна сохранять инвариант-
ным. Если бы не внутренний цикл, второй цикл был бы в точ-
ности противоположен первому; и функция дополнительного
оператора
do г >
dd _,,r: = r-dd od
именно в том и состоит, чтобы восстанавливать возможно на-
рушенное отношение r<dd, достигаемое при выполнении пер-
вого цикла.
Четвертый пример
Для фиксированных Q 1, Q2, QЗ и Q4 требуется обеспечить
, истинность R, причем R задается как R1 and R2, где
R 1: последовательность значений ( q 1, q2, q3, q4) есть не-
которая перестановка значений ( Q 1, Q2, QЗ, Q4)
R2: q1 ~q2~q3~q4
Если мы возьмем Rl в качестве сохраняемого инвариант-
ным отношения Р, получим такое возможное решение:
ql, q2, q3, q4: = Ql, Q2, QЗ, .Q4;
do ql > q2-q1, q2: = q2, ql
о q2>q3-+q2, q3: = q3, q2
о q3 >q4-+q3, q4: = q4, q3
od
Очевидно, что после первого присваивания Р становится
истинным, и ни одна из охраняемых команд не нарушает его
истинности. По завершении программы мы имеем поп ВВ, а
это есть отношение R2. В том, что программа действительно
придет к завершению, каждый из нас убеждается по-разному
в зависимости от своей профессии: математик мог бы заме-
тить, что число перестановок убывает, исследователь операций
90 Глава 8

будет интерпретировать это как максимизацию ql +2 * q2+


+3 * q3+4 * q4, а я- как физик- сразу «вижу», что центр тя-
жести смещается в одном направлении (вправо, если быть
точным). Программа примечательна в том смысле, что, какие
бы предохранители мы ни выбрали, никогда не возникнет
опасности нарушения истинности Р: предохранители, исполь-
зуемые в нашем примере, являются чистым следствием необ-
ходимости завершения программы.
Замечание. Заметьте, что мы могли бы добавить также и
другие варианты, такие, как
q1 >q3~q1,q3 := qЗ, ql
причем их нельзя использовать для замены одного из трех,
перечисленных в программе. (Конец эамечаниял
Это хороший пример того, -какого рода ясности мы можем
достичь при нашей недетерминированности; излишне говорить,
однако, что я не рекомендую сортировать большое количест-
во значений аналогичным способом.
Пятый пример
Нам требуется составить программу аппроксимации квад-
ратного корня; более точно: для фиксированного п (п~О)
программа должна обеспечить· истинность
R: <
а2 п and(a+ 1)2>n
Чтобы ослабить э.то отношение, можно, например, отбро-
сить один из логических сомножителей, скажем последний, и
~осредоточиться на
Р: а2<п.
Очевидно, что это отношение верно при а=О, поэтому вы-
бор начального значения не Должен нас· беспокоить. Мы ви-
дим, что если второй член не является истинным, то это вы-
зывается слишком маленьким значением а, поэтому мы могли
бы обратиться к оператору «а:= а+ 1 ». Формально мы полу-
чаем
wp (<<а:= а+ 1>>, Р)=((а+ 1)2< п)
Используя это .У~ловие в качестве ( едннственногог) предо-
хранителя, мы получаем (Р and поп ВВ) ==R и приходим к
следующей программе:
if п~О-
а:= О {Р стало истинным};
Формальное рассмогрение .нескольких небольших примеров 9J

<
do (а+ 1)2 п -а:= а+ 1 {Р осталось истинным} od
{R стало истинным]
fi { R стало истинным]
При составлении. программы мы исходили из предположе-
ния, что она завершится, и это действительно так, поскольку
корень из неотрицательного числа есть монотонно возрастаю-
щая функция: в качестве t мы можем взять функцию п-а2•
Эта программа довольно очевидна, к тому же она и не
очень эффективна: при больших значениях п она будет рабо-
тать довольно долго. Другой способ обобщения R - это вве-
дение другой переменной (скажем, Ь - и снова в ограничен-
ном интервале изменения), которая заменит часть Р, на...
пример,
Р: >
а2 ~ п and Ь2 п and О -<,. а Ь <
Благодаря такому выбору Р обладает тем приятным свойст-
вом, что
(Р and (а+ 1 = Ь)) ==Ф R
Таким образом, MI;>I. приходим к программе, представлен-
ной в такой форме (здесь и далее конструкция if п~О-+ ... fi
опускается) :
а,Ь ~ = О,п+ 1 {Р стало истинным};
do a+I =1=-Ь-уменьшить Ь-а при инвариантности Pod
{ .!:( стало истинным J
Пусть d будет -величиной, на которую уменьшается раз-
ность Ь-а при каждом выполнении охраняемой команды.
Разность может уменьшаться либо за счет уменьшения Ь,
либо за счет увеличения а, либо за счет и того и другого. Не
теряя общности, мы можем ограничиться рассмотрением таких
шагов, когда изменяется либо а, либо Ь, но не а и Ь одновре-
менно: если а слишком мало и Ь слишком велико и на одном
шаге только уменьшается Ь, тогда на следующем шаге можно
увеличить а. Эти соображения приводят нас к программе в
следующей форме:
а,Ь: =О, п+ 1 {Р стало истинным};
do a-f-l=тf= b-
d : = ... { d имеет соответствующее значение и Р по-преж-
нему выполняется};
if ... -а: =a+d {Р осталось истинным]
92 Глава 8

О ...
--+ Ь : = Ь - d { Р осталось истинным}

fl { Р стало истинным}
я
od { стало истинным}
Теперь
wp(<<a == a+d>>, P)=((a+d)2~ п and ь2>п)
Поскольку истинность второго члена следует из истинности Р,
мы можем использовать первый член в качестве нашего пер-
, вого предохранителя; аналогично выводится второй предохра-
нитель, и мы получаем очередную форму нашей программы:
аЬ: = 0,п+ 1;
do a+l=Fb-+d:= ... ;
Н (a+d)2~ n-+a == a+d
O(b-d)2> n-+ ь := b-d
fl { Р осталось истинным}
od { я стало истинным}
Нам остается еще соответствующим образом выбрать d.
Поскольку мы взяли Ь-а (на самом деле, Ь-а-1) в качестве
нашей функции t, эффективное убывание должно быть таким,
чтобы а удовлетворяло условию d>O. Кроме того, последую-
щая конструкция выбора не должна приводить к отказу, т. е.
по крайней мере один из предохранителей должен быть истин-
ным. Это значит, что из отрицания одного, (a+d)2>n, должен
.следовать другой, (b-d)2>n; это гарантируется, если
a+d~b-d
или
2*d~b-a
Наряду с нижней· границей мы установили также и верхню№
границу для d. Мы могли бы взять d= 1, но чем больше d, тем
быстрее работает программа, поэтому мы предлагаем
а,Ь:= О, п+ 1;
do а+ 1 =1= Ь-+d : = ( Ь - а) div 2;
if (a+d)2~ n-+a == a+d
О (b-d)2>n-+ Ь := b-d
fi
od
Формальное рассмотрение нескольких небольших примеров 93

где п div 2 есть п/2, если 21 п, и (п-1) /2, если 21 (n-1).


Использование действия oiv побуждает нас разобраться
в том, что произойдет, если мы навяжем себе ограничение на
Ь-а, полагая, что Ь-а четно при каждом вычислении d. Вво-
дя с=Ь-а и исключая Ь, мы получаем инвариантное отно-
шение
Р: а2-<.п and (а+с)2>п and(Ei:i>-O:c=2i)
и программу (в которой с играет роль d)
а, с:= О, 1; do с2-<. п-+с := 2 *с od;
do с .=t= 1-+ с: =с/2;
if (а +с)2-<_. п~а: = а+с
О (а+с)2>п~ пропустить
fi
od
Замечание. Эта программа очень похожа на последнюю
программу для третьего примера, где вычислялся остаток в
предположении, что мы имеем право умножать и делить на 3.
В приведенной выше программе можно было бы заменить кон-
струкцию выбора на
do (а+с)2-<_. п-+а: =а+ с od
Если, условие, которому должен удовлетворять остаток,
O~r<d, записать как r<d and (r+d) ~d, сходство станет
еще более отчетливым. (Конец эамечаниял
Понимая, что так можно окончательно «замучить» этот ма-
ленький пример, я бы хотел тем не менее предложить послед-
ний вариант преобразования программы. Мы написали про-
грамму,' исходя из предположения, что операция возведения
числа в квадрат входит в состав имеющихся операций; но
предположим, что это не так и что единственные операции
типа умножения, имеющиеся в нашем распоряжении,- это
умножение и деление на (малые) степени 2. Тогда наша 'по-
, следняя программа оказывается не такой уж хорошей, т. е.
она нехороша, если мы предполагаем, что значения перемен-
ных, с которыми непосредственно оперирует машина, отож-
дествлены со значениями переменных а и с, как это было бы
-при «абстрактных» вычислениях. Другими словами, мы мо-
жем рассматривать а и с как абстрактные переменные, чьи
значения представляются (в соответствии с соглашением, пре-
дусматривающим более сложные условия, чем просто тожде-
94 Глава В

ственность) аначениями других переменных, с которыми


13действительности оперирует машина при выполнении про-
граммы. Мы можем позволить машине работать не непосред-
ственно с а и с, а с р, q, r, такими, что
р=а*с
q=c2
r=n_....a2
Мы видим, что это - преобразование координат, и каждой
траектории в нашем (а, с)-пространстве соответствует траек-
тория в нашем (р, q, r)-пространстве. Обратное не всегда вер-
но, так как значения р, q и r не .независнмы: в терминах
р, q и r мы получаем избыточность и, следовательно, потен-
циальную возможность, пожертвовав некоторым объемом
памяти, не только выиграть время вычислений, но
даже избавиться от необходимости возводить в квад-
рат! (Совершенно ясно, что преследовалась именно эта цель,
когда строилось преобразование, переводящее точку в (а, с)-
, пространстве в точку в (р, q, ;)-пространстве . ) Теперь мы
можем попытаться перевести все булевы выражения и пере-
мещения в (а, с)-пространстве в соответствующие булевы вы-
ражения и перемещения в (р, q, r)-просrранстве. Если бы нам
удалось сделать это в терминах допустимых операций, наша
аадача.была бы успешно решена. Предлагаемое преобразова-
ние и в самом деле отвечает нашим требованиям, и результа-
том его является следующая программа (переменная h была
введена для очень локальной оптимизации) :
р, q, г : =О, 1, п; do q < п-+ q : = q * 4 od;
do q .=1= 1-+q :=q/4; h :=p+q; р ==р/2 {h=2 * p+q};
if r~.h-+ р, г ==p+q, r-h
О 'r < h-+ пропустить
fi
od. {р имеет значение, требуемое для а}
Этот пятый пример был включен благодаря исторпи его
создания (правда, несколько приукрашенной}. Когда млад-
шей из двух наших собак было всего несколько месяцев, я
однажды вечером пошел погулять с ними обеими. Назавтра
мне предстояло читать лекции студентам, которые всего лишь
несколько недель назад начали знакомиться с программиро-
ванием, и мне нужна была простая задача, чтобы на ее при-
мере я мог «массировать» решения. В течение часовой прогул-
Формальное рассмотрение нескольких небольших примеров 95

ки я продумал первую, третью и четвертую, программы, прав-


да, правильно ввести h в последней программе я смог только
при помощи· карандаша и бумаги, когда вернулся домой. Ко
второй программе, той, которая оперирует с а и Ь и которая
была здесь представлена как переходная ступень к нашему
третьему решению, я пришел лишь несколько недель спустя --
и она была тогда в гораздо менее элегантной форме, чем пред-
ставлена здесь. Вторая причина для ее включения в число
представленных программ - это соотношение между третьей
и четвертой программами: по -отношению к четвертой програм-
ме третья представляет собой наш первый пример так назы-
ваемой «абстракции представления».
Швстой пример
Для фиксированных Х (Х> 1) и У (У~ О) программа долж-
на обеспечить истинность отношения
R.: Z=XY
при очевидном предположении, что операция возведения в сте-
пень не входит в набор доступных операций. Эта задача может
быть решена при помощи «абстрактной переменной», скажем
h; при решении задачи мы будем пользоваться циклом, для
которого инвариантным является отношение
Р: h * z=-XY
и наша (в равной степени «абстрактная») программа могла
бы выглядеть так:
н, ~: =ХУ, 1 {Р стало истинным};
do h =/= 1 -э-сжиматъ h при инвариантности Р od
{ R стало истинным}
Последнеезаключение справедливо, поскольку (Р and
h = 1) ==ФR. Приведенная выше программа придет к заверше-
нию, если после конечного числа применений операции «сжи-
мания» h станет равным 1. Проблема, конечно, в том, что мы
ве можем представить значение h значением конкретной пере-
менной, с которым непосредственно оперирует машина; если
бы мы могли так сделать, мы могли бы сразу присвоить z
значение ХУ, не затрудняя себя введением h. Фокус в том, что
для представления текущего значения h мы можем ввести
две-. на этом уровне конкретные - переменные, скажем х и
у, и наше первое присваивание предлагает в качестве согла-
шения об этом представлении
h=Xll
96 Глава 8

Тогда условие «h=F 1» переходит в условие «y=FO», и наша сле-


дующая задача состоит в том, чтобы подыскать выполнимую
операцию «сжимания». Поскольку при этой операции произ-
ведение h * z должно оставаться инвариантным, мы должны
делить h на ту же величину, на которую умножается z. Исхо-
дя из представления h, наиболее естественным кандидатом на
эту величину можно считать текущее значение х. Без дальней-
ших затруднений мы приходим к такому виду нашей абстракт-
ной программы:
·х, у, z :=Х, У, 1 {Р стало истинным};
do у =Р О-+ у, z : =у - 1, z *х { Р осталось истинным} od
{R стало истинным}
Глядя на эту программу, мы понимаем, что число выполне-
ний цикла равно первоначальному значению У, и можем за-
дать себе вопрос, нельзя ли ускорить программу. Ясно, что за-
дачей охраняемой команды является сведение у к нулю; не из-
меняя значения h, мы можем проверить, нельзя ли изменить
представление этого значения в надежде уменьшить значение
у. Попытаемся воспользоваться тем фактом, что конкретное
представление значения h, заданное как хУ, вовсе не является
однозначным. Если у четно, мы можем разделить у на 2 и
возвести х в квадрат, при этом значение h совсем не изменит-
ся. Непосредственно перед операцией сжимания мы вставляем
преобразование, приводящее к наиболее привлекательному
представлению h, и получаем следующую программу:
х, у, z :=Х, У, 1;
do y.=FO-+do 2Jy-+X, у:=Х*Х, у/2 od;
·у' z : =у - 1, z * х
od {R стало истинным}
Существует только одна величина, которую можно беско-
нечно делить пополам и она не станет нечетной, эта величи-
на --нуль; иначе говоря, внешний предохранитель гарантирует
нам, что внутренний цикл придет к завершению.
Я включил этот пример по разным причинам. Меня пора-
зило открытие, что если просто вставить в программу что-то,
что на абстрактном уровне действует как пустой оператор,
можно так изменить алгоритм, что число операций, которое
раньше было пропорционально У, станет пропорциональным
только log (У). Это открытие было прямым следствием того,
что я заставил себя думать в терминах отдельной абстрактной
Формальное рассмотрение нескольких небольших примеров 97

переменной. Программа возведения в степень, которую я знал,


была следующей:
х, у, z:=X, У, 1;
do у =1= О-+ if поп 2 ! у-+ у, z : =н - 1, z * х
О 21 у-+ пропустить fi;
Х, у ==Х*Х, у/2
od
. Эта последняя программа очень хорошо известна, многие из
нас пришли к ней независимо друг от друга. Поскольку послед-
нее возведение х в квадрат, когда у стал равным нулю, уже
излишне, на эту программу часто ссылались как на пример,
подтверждающий необходимость иметь то, что мы назвали бы
«промежуточными выходами». Принимая во внимание нашу
вторую программу, я прихожу к заключению, что этот довод
слаб. ·
Седьмой пример
Для фиксированного значения п (n;?;O) задана функция
f (i)для O~i<n. Присвоить булевой переменной «всешесзьь
такое значение, чтобы в конечном результате стало справед-
ливым
R: всешесmь=(А i: О< i.< п : f (i)=6)
(Этот пример обнаруживает некоторое сходство со вторым
примером данной главы. Заметим, однако, что в этом приме-
ре допускается равенство п нулю. В таком случае интервал
возможных значений для квантора «А» пуст и должно быть
верным; что всешесть =истина.) По аналогии со вторым приме-
ром введем инвариантное отношение
Р: (всеШесть=(А i: О< i <
j: f (i)=6)) and О< j < п
поскольку это отношение легко сделать истинным при j =О
и к тому же (Р and j=n) =ФR. Единственное, что. мы должны
сделать, это понять, как увеличивать j при инвариантности Р.
Поэтому берем
wp (<<j ==i+ 1>>, Р)=
(всешесть=(А i: О< i < j + 1: f (i) =6)) and О< j +1< п
Справедливость последнего члена следует из справедливости
Р and j'=l==n; и в этом нет никакой проблемы, так как мы уже
решили, что условие j=l=n, взятое в качестве предохранителя,
является достаточно слабым, чтобы делать выводы о значе-
4-6
98 Глава 8

нии R по завершении программы .• Слабейшим предусловием"


таким, что присваивание
всешесть :=всешесть .and/ (Л=6
сделает истинным другой член, является
(всешесть and f (j)=6)=(A i: О~ i < j + 1: f (i)=6)
условие, которое следует из Р. Таким образом, мы приходим
к программе: ·
всешесть, j :=истина, О;
do j =1=· п-+ всешесть : = всешесть and f (j) = 6;
j:=}+l
od
(Мы не воспользовались одновременным присваиванием в ох-
раняемой команде просто так, без особых на то причин.)
К моменту чтения этой программы - или даже раньше -
у нас должно было возникнуть чувство беспокойства по пово-
ду того, что, как только будет найдено значение функции =1==6~
не будет смысла продолжать работу программы. И в самом
деле, хотя (Р and j=n) =Ф R, мы могли бы воспользоваться
более слабым утверждением
(Р and .(j=n or поп всешесть)) =9 R
приводящим к более сильному предохранителю «j=#=n and
всешесзь»и к программе -
всешесть, j: =истина, О;
do._j +п апd всешесть=ь-всешесть ; j :=/ (Л=6, j+ 1 od
(Отметим упрощение присваивания значения всешесзь, упро-
щение, которое объясняется использованием более сильного
предохранителя.)

Упражнение
Для той же задачи дать доказательство правильности для
if n=О-+всешесть л=систина
0 n>O-+ j :=0;
do j -F п - 1 and f (j) = 6 -+ j : = j
;
+1 od;
всешесть ==f U)=6
fi
Формальное рассмотрение. нескольких небольших примеров 99

а также ·для более «хитрой» программы (которая избавилась


от необходимости обращаться к функции f более чем из одного
места в программе)
j:=O;
do j ,=/= п cand f (j)=6~): =J +1 od;
всешесть ==j=n

(Операция условной конъюнкции. «сапё» была здесь примене-


на для того, чтобы учесть тот факт, что значение f (п) не обя-
.аательно должно быть определено.) Эта последняя програм-
ма некоторым очень нравится. (Конец упражнения.}

Восьмой пример
Прежде чем я смогу сформулировать нашу следующую за-
дачу, я должен дать некоторые определения и сформулировать
теорему. Пусть р=« (Ро, Р1, ... , Рп-1) обозначает перестановку
п (n>l) различных значений Pi (О~i<п), т. е. (i=l=j) =Ф-
(Pi==l=pj). Пусть q= (qo, Q1, ... , Qn-1) обозначает другую пе-
рестановку той же совокупности п значений. По определению
«перестановка р предшествует перестановке q в алфавитном
по~ядке» в том и только том случае, если для минимального
значения k, такого, что P11.=l=qh, мы имеем p11.<q11..
1

Так называемый «алфавитный индекс-,» перестановки п


различных значений есть порядковый номер, сообщаемый пере-
становке, когда мы перенумеровываем пl возможных переста-
новок, расположенных в алфавитном порядке, от О до п!-1.
Например, для п= 3 и совокупности значений 2, 4, 7 мы имеем

индекса (2, 4, 7)=0


индекс, (2, 7, 4)= 1
индекса (4, 2, 7)=2
индекс, (4, 7, 2)=3
индекса (7, 2, 4)=4
индекс, (7, 4, 2)=5

Пусть (Ро § Р1 § ... § Ptt-1) обозначает перестановку п раз-


личных значений в порядке их монотонного возрастания, т. е.
индекс; ((ро§ Pi§ ... § Рп-1)) =0. (Например, (4 § 7 § 2) =
= (2, 4, 7)' но и (7 § 2 § 4) = (2, 4, 7) .) .
4•
100 Глава 8

Пользуясь введенными выше обозначениями, мы можем


сформулировать следующую теорему для n> 1:
индекс, (р0, Р1, ... , Рп-1) =
индекс, (Ро, (Р1 § Р2 § .. · § Рп-1)) + индексь.., (Р1, Р2,. · . ,Рп-1)
(например, индексз(4, 7, 2) =индекс3(4, 2, 7) +индексэ б', 2) =
=2+1=3).
Словесная формулировка: индекс; перестановки п различ-
ных значений есть индекс первой по алфавиту перестановки
с тем же самым левым значением, увеличенный на нндексь-т
перестановки оставшихся правых п-1 значений. Следствие: из

Pn-k < Pn-k+l <·· ·.·< Рп-1


вытекает, что индекс; (ро, Р1,- ... , Pn-i) кратен k!, и наоборот.
Теперь, после всех этих приготовлений, мы можем изло-
жить нашу задачу. Имеется ряд из n позиций (п> 1), перену-
мерованных слева направо с О до п-1; в каждой позиции
находится карточка, на которой написано некоторое число,
причем все эти числа различны.
Если в любой момент ci(O~i<n) обозначает число на
карточке, находящейся в позиции i, то в начальный момент
мы имеем

(т, е. карточки разложены в порядке возрастания написанных


на них чисел). Для заданного значения r (O~r<n!) мы дол-
жны так переупорядочить карточки, чтобы выполнялось ус-
ловие
R:
Чтобы иметь возможнссть задавать действия над карточка-
ми, нам придется ввести оператор
переставить (i, j) при о < i, j <п
который будет менять местами карточки в позициях и j,
если i=I= j (и ничего не будет делать, если i= j).
Чтобы выполнить заданное преобразование, мы должны
найти класс состояний (причем все они должны удовлетворять
соответствующему условию Р 1), такой, что и начальное, и ко-
нечное состояния являются специфическими представителями
этого класса. Введем новую переменную, скажем s, и предста-
Формальное рассмотрение нескольких небольших примеров 101

пим в качестве очевидного кандидата для Pl условие


индексп(с0, с1, ••• , Cп-i)=s
поскольку его легко удовлетворить в самом начале (а именно
при ПОМОЩИ «S: =0») и поскольку (Pl and з= r) =ФR.
Теперь мы можем подумать об ограничении интервала зна-
чений s, и, учитывая начальное значение s, можно было бы
попробовать
Р1: индекс; (с0, с1, ... , cn_1)=s and О-< s-< r
что л:ри1вел.о бы ~к программе в такой форме:
s: =0 {Pl стало истинным};
do s =f=. г~{Р1 апё s<r}
увеличить s на соответствующую
величину при инвариантности Pl
·{ Pl остается истинным}
od { R стало истинным}
Наша следующая забота - выбор «соответствующей вели-
чины». Поскольку увеличение s должно сопровождаться пере-
упорядочиванием карточек, призванным сохранить инвариант-
ность PI, было бы благоразумным проверить, нельзя ли найти
такие условия, при которых отдельная операция переставить
"соответствовала бы известному приращению s. Пусть для k,
такого, что 1 ~ k <п, имеет место
rn-k< Cn-k+l <· · · < Cn-1
Это предположение эквивалентно предположению, что k! 1 s
(т. е. s делится нацело на k!). Пусть i=n-k-1, т. е. сi-это
значение на карточке, находящейся непосредственно слева от
этой последовательности. Далее, пусть ci<Cn-1, и пусть Cj
будет минимальным значением для j в интервале n-т-k~j<n,
таким, что ci<cj (т. е. Cj есть наименьшее значение справа от
ci, превосходящее с.). В этом случае операция переставить
(i, j) оставляет правые k· значений в том же самом порядке,
и наша теорема о перестановках и их индексах говорит о том,
что k! есть соответствующее приращение з. Она также говорит.
нам, что когда, кроме k! 1 s, мы имеем
s<r<s+k!
величины от с0 до Cn-h-l достигли своих конечных значений.
Поэтому я предлагаю усилить наше первоначальное инва-
риантное отношение Р 1 дополнительным отношением Р2
102 Глава 8.

(фиксируя функцию новой переменной k),


Р2:
которое означает, что значения на правых k карточках еще
монотонно возрастают, н в то же время левые n-k карточек
уже находятся в своих окончательных позициях. Мы выбрали
'«крупные шаги», которыми мы· будем двигаться к нашей це-
ли. Чтобы найти «соответствующую величину» для крупного
шага, машина сначала определяет самое маленькое значение
k, для которого условие r<s+k! уже не выполняется (это
означает, что ci при i=n;-k-1 еще слишком мало, но слева
от него уже все в порядке), и затем увеличивает s на мини-
мальную величину, кратную kl, которая требуется для того,
чтобы условие r<s+kt опять выполнялось: для этого исполь-
зуются «мелкие шаги», равные k!, и одновременно увеличива-
ется Ct за счет карточек, находящихся справа. В следующей
программе мы вводим дополнительную переменную kfac,
удовлетворяющую условию
РЗ: kfac=kl
и для второго внутреннего цикла вводим переменные i и j,
такие, что i=n-k-1 и либо j=n, либо i<j<n and Cj>ci
and ~j-1 ~ci.
s :=0 {Pl стало истинным};
kfac, k :=1,1 {РЗ тоже стало истинным};
do k =l=n-+kfac, k: =kfac • (k+ 1), k+ 1 od
{Р2 тоже стало истинным};
do s =1= r-+ {s<r, т. е. по крайней мере одна и, следователь-
но, по крайней мере две карточки еще не заняли
своих окончательных позиций}
do r <s+kfac-+kfac, k ==kfac/k, k-1 od
{Pl и РЗ остались истинными, но в Р2 последний
член заменяется ~а
~+kfac ~ r<s+(k+ 1) *kfac};
i, j ==n-k-1, n-k;
do s+kfac ~ r-+ {n-k~ j< п}
s:=s+kfac;inepecmq.вumь~(i, j); j:= j+l
od {Р2 опять восстановлено: Р1 and _Р2 and РЗ}
od {R стало истинным}
Формальное рассмотрение нескольких небольших примеров 103

Упражнение
Убедитесь в том, что и следующая, довольно похожая на
предыдущую, программа справится с нашим заданием:
s :=0; kfac, k: = 1, 1;
do k =1= п-+ kf ас, k : = kf ас * ( k + 1), k + 1 od;
do k ::/= 1-+
k f ас, k : = k f ас/ k, k - 1;
i,j:=n-k-1,n-k;
do s+kfac ~r-+
s :=s+kfac; переставить (i, j); j ==j+ 1
od
od
(Совет: в качестве монотонно убывающей функции t~O для
внешнего цикла возьмите функцию i=r-s+k-1.) (Конец yn-
. ражнениял
9 КОГДА НЕДЕТЕРМИНИРОВАННОСТЬ ОГРАНИЧЕНА

Это опять очень формальная глава. В главе «Характеристи-


ка семантики» мы говорили о четырех свойствах, которыми
должно обладать усвовие ~р (S, R), рассматриваемое как
функция R для любой конструкции S, чтобы его можно было
интерпретировать как слабейшее предусловие, обеспечиваю-
щее истинность R. (Для недетерминированных конструкций
четвертое свойство было прямым следствием второго.)
В -следующей главе «Семантическая характеристика языка
программирования» мы дали средства для построения новых
преобразователей предикатов, указав при этом, что такое по-
строение должно приводить только к преобразователям преди-
катов, обладающим вышеупомянутыми свойствами (т. е. чтобы
имело смысл продолжать всю эту продедуру). Для каж-
дого основного оператора (-«пропустить», «отказать» и опера-
торов присваивания) нужно получать подтверждение, что он
обладает упомянутыми свойствами; для каждого способа по-
строения новых операторов из составляющих операторов (точ-
ка с запятой, конструкции выбора и конструкции повторения)
нужно показывать, что полученные составные операторы так-
же обладают этими свойствами, при этом можно исходить из
предположения, что составляющие операторы этими свойст-
вами обладают. Мы проверили правильность этого предполо-
жения вплоть до закона исключенного чуда для точки с запя-
той, предоставив проверку остальных свойств читателям в каче-
стве упражнения. Оставим теперь эти свойства; в данной главе
мы·докажем, что наши конструкции обладают более rлубоким
свойством, причем на этот раз явно проверим правильность
нашего утверждения и для конструкций выбора, и для кон-
струкций повторения. (Способом выполнения этих проверок
можно воспользоваться как образцом для проведения пропу-
щенных доказательств.) -Свойство, о котором пойдет речь, из-
вестно как «СВОЙСТВО непрерывности».
Когда недегерминированносзь ограничена 105

Свойство 5. Для любой конструкции S и любой бесконеч-


ной последовательности предикатов Со, С1, С2, · ... , таких, что
для r >о: С, =Ф cr+l для всех состояний (1)
мы имеем для всех состояний
wp(S,(Er:r>O:Cr))=(Es:s>O:wp(S, Cs)). (2)
Для операторов «пропустить», «отказать» и для операторов
присваивания истинность (2) непосредственно вытекает из их
определений, и даже можно обойтись без предположения ( 1).
Для точки с запятой мы получаем
wp( <<Sl; S2>>, (Е r: r >О: Сг))=
{по определению семантики точки с запятой)
wp (Sl, wp (S2, (Е r: r >О: Cr)))=
(поскольку предполагается, что S2 обладает свойством 5)
wp (Sl,. (Er': r' >О: wp (S2, Cr,)))=
(поскольку предполагается, что S2 обладает свойством 2, так
что wp(S2, Сr,)=Ф wp(S2, ·Сг +1), и предполагается,
1 что SI
обладает свойством 5)
(Es: s>O:wp(Sl, wp(S2, Cs)))=
(по определению семантики точки с запятой)
(Е s: s >а·: wp (<<Sl; S2>>, Cs)) ч.т.д.
Для конструкции выбора мы разбиваем доказательство
истинности (2) на два шага. Сначала легко показать, что ле-
вая часть (2) следует из его правой части. Для этого рассмот-
рим произвольную точку Х в пространстве состояний, такую,
что правая часть (2) выполняется, т. е. существует неотрица-
тельная величина, скажем з', такая, что в точке Х выполня-
ется условие wp (S, Cs' ). Но так как Cs' =Ф (Е r: r~O: Ст) и
любая конструкция S обладает свойством 2, мы приходим к
выводу, что в точке Х выполняется также
wp(S, (Er:r>O:Cr))
Поскольку Х было произвольным состоянием, для которо- ·
го справедлива правая часть (2), значит, справедлива и левая
часть (2). В ходе этого рассуждения не использовалось пред-
положение ( 1), но оно понадобится нам для доказательства
того, что правая часть (2) следует из его левой части.
wp (IF, (Е r: r >О: Сг))=
106 Глава 9

(по (?Пределениюсемантики конструкции выбора)


ВВ and (Aj:1.:r;;;,J.:r;;;.n:B19>wp(SL1,(Er:r>--O:C,)))=
(поскольку предполагается, что SLJ обладает свойством 5)
ВВ and (Aj:~.:r;;;,j.:r;;;,n:B1=Ф(Es:s>--O:wp(SL1, С5))) (3)
1

Рассмотрим произвольное состояние Х, для которого (3)


истинно, и пусть j' - это такое значение j, при котором
В (Х) =истина; тогда в точке Х мы имеем
(4)

Исходя из ( 1) и того факта, что SL1' обладает свойством 2, мы


приходим к выводу, что
wp (SL1,, Cs) =Ф wp (SL1,, Cs+1)

и, учитывая (4), мы получаем, что в точке Х справедливо


также
(Е s': s' ~О: (А s: s ~ s': wp (SL1 С5))) (5) 1,

Пусть s' =s' (j') - минимальное значение, удовлетворяющее


(5). Определим зтах как максимальное значение s' (j') по
всем значениям j" (таких значений самое большее п, и, следа-·
вательно, максимум существует!), для которых В1' (Х) = исзи-
на. Тогда из (З) и (5) следует, что в точке Х
ВВ and (А j : 1.::;;;, j.::;;;, п : В19> wp (SL1, Csmax))=
(по определению семантики конструкции выбора)
wp (IF, Csmax)
Но отсюда следует, ~то для состояния Х справедливо и
(Es:s>--O:wp(IF, С5)'
Но поскольку.Х было произвольным состоянием, удовлетворя-
ющим (3), то для S= IF доказано, что правая часть (2) сле-
дует из его левой части и тем самым что конструкция выбора
также обладает свойством 5. Заметим, что существенную роль
в доказательстве сыграли предположение ( 1) и тот факт, что
набор· охраняемых команд является конечным набором охра-
няемых команд.
Для доказательства того, что конструкция повторения об-
ладает свойством 5, воспользуемся методом математической
индукции.
Когда недетер.ми.нированность ограничена 107

Основная предпосылка: Но обладает свойством 5. ·


H0(Er:r>O:C,)=
(Ег: r> О: С,) апd поп ВВ=
(Е s : s > О : Сs and поп В В)=
(Е s: s :>О: Но (t;s)} ч. т. д
Переход по индукции: из предположения, что Hk и Но об-
ладают свойством 5, следует, что им обладает и Нн1.
Н н1 (Ег : г >О: С,)=
(из определения Нн1)
wp(IF, Hk(Er:r>O:C,)) or H0(Er:r>O:C,)=
(из предположения, что Hk и Но обладают свойством 5)
wp (IF, (Е r': r' >О: Н 1i (С,,))) ог (Е з : ~>О: Н0 (Cs))=
(поскольку конструкция выбора обладает свойством 5 и Н 11.
обладает свойством 2)
(Es:s>O:wp(IF, Hk(C5))) ог (Es:s>O:H0(Cs))=
(E~:s>O:wp(IF, Hk(C5)) ог H0(Cs))=
(из определения Нн1)
(Е s: s >О: Нн1 (С5)) ч. т. Д.
Таким образом, пользуясь методом индукции, мы показали, Что
все Н н обладают свойством 5, а отсюда
wp (DO, (Е г : r >'О: С,))=
(по определению семантики конструкции повторения)
(Е k: k >О: Hk (Е r: r >О: С,))=
(поскольку все Hk обладают свойством 5)
(Е k : k > О : (Е s : s > О : Н k ~С5))) =
(поскольку этим выражается существование (k, s)-пары)
(Е s: s >О: (Е k: k >.О: Hk (Cs)))=
(по определению семантики конструкции повторения)
(Es:s>O:wp(DG, С5)) Ч. Т. Д.
Свойство 5 приобретает особое значение благодаря семан-
тике конструкции повторения
wp (DO, R)=(E k: k >О: Hk (R))
108 Г лава 9

в соответствии с которой предусловие могло бы быть посту-


словием для другого оператора. Поскольку
для k >О : Н k (R) =Ф Н н1 (R) для всех состояний
(это легко доказывается по индукции), условия, нужные для
существования свойства 5, удовлетворяются. Мы можем, на-
пример, доказать, что во всех начальных состояниях, для ко-
торых ВВ выполняется, конструкция
do В1 ---+SL10 B2---+SL2 0 ... 0 Вп---+SLп od
эквивалентна конструкции
if в;-sL10B2--SL20 0Bп-SLпfi;
do .В1 --SL1 0 B2-SL2 0 0 Вп--SLпо~
(В начальных состояниях, для. которых ВВ не выполняется,
первая программа будет действовать как «пропустить», вто-
рая - как «отказать».) Иначе говоря, мы должны доказать, что
(ВВ and wp(DO, R))=(BB and~wp(IF, wp(DO, R)))
ВВ and wp (IF, wp (DO, R))= .
(из-за семантики конструкции повторения)
ВВ and wp (IF, (Е k: k >О: Hk (R)))=
~поско~ьку IF обладает свойством 5)
ВЕ_ and (Es:s>O:wp(IF, Hs(R)))=
(так как (ВВ and Ho(R)) =F)
ВВ and (Es:s>O:wp(IF, H;(R)) or H0(R))=
(в силу рекуррентного отношения для Hk(R))
· , ВВ and (Es:s>O:Hsн(R))=
(поскольку (ВВ and H0(R)) =F)
ВВ and (Ek:k>O:Hk(R))=
(иэ-за семантики 'конструкции повторения)
ВВ and wp (DO, R) ч. т. Д.
В конце мне бы хотелось привлечь ваше внимание к совсем
другому следствию, вытекающему из того факта, что все наши
конструкции обладают свойством 5. Мы могли бы попробо-
вать составить программу S: «установить х равным любому
подожнтельному целому», обладающую свойствами:
(а) wp (S, х>О) = Т
(б) (As: s~O: wp(S, O<x<x<s) =F)
Когда недегерминированносзь ограничена 109

Здесь свойство (а) выражает требование, чтобы програм-


ма S пришла к завершению с х, равным некоторому положи-
тельному числу, а свойство (б) говорит о том, что S есть кон-
струкция неограниченной недетерминированности, т. е. что
а priori не 'существует верхней границы для окончательного
значениях. Для такой программы S мы могли бы, однако, по-
.лучитъ теперь
T=wp(S, х>О)
=wp (S, (Е r: r >О: О <х <г))
=(Es: s>O: wp(S,O<x<s))
= (Е s : s >О : F)
=F
Мы пришли к противоречию; это означает, что для конст-
;рукции S «установить х равным любому положительному це-
.лому» не существует .никакой программы! ·
Следовательно, любые попытки написать программу «уста-
новить х равным любому положительному целому» обречены
на неудачу. Например, мы могли бы рассмотреть такую про-
грамму:
продолжение ==истина; x:=l;
do продо лжение-и х ==х+ 1
О продолжение -- продолжение: =ложь
od
Эта программа· будет продолжать увеличение х до тех пор,
floкa действует первая альтернатива; как только произойдет
переход ко второй альтернативе, программа немедленно пре-
кратится. По завершении· программы х действительно может
-быть «любым положительным целым» в том смысле, что мы не
можем представить себе такое положительное значение Х,
для которого по завершении программы было бы невозможным
-отношение х=Х. Но завершение программы вовсе не гаран-
тировано! Мы можем вызвать принудительное завершение:
используя некоторую большую положительную константу N,
мы можем написать:
продолжение: =истина; х := 1;
do продолжение and x<N .-х :=х+ 1
О продолжение - продолжение: =ложь
od .
1110 тогда программа больше не обладает свойством (б).
То, что программы «установить х равным любому положи-
110 Глава 9

тельному целому» не существует, успокаивает нас по двум


причинам. Во-первых, если бы такая программа могла сущест-
вовать, наше определение семантики конструкции повторения
следовало бы, мягко выражаясь, подвергнуть сомнению. Если
взять
S.: do х>О-+х==х-1 .
О х <О-+ «установитъ х равным любому положительному
целому»
od
наш формализм для конструкции повторения приведет к тому"
что wp (S, Т) = (х~О), в то время как большинство моих чи-
тателей, я надеюсь, придет к заключению, что в предположе-
нии существования программы «установитъ х равным любому
положительному целому» для х<О завершение программы бы-
ло бы- гарантировано. Но тогда трактовка wp (S, Т) как слабей-
шего предусловия, гарантирующего завершение, перестает
быть справедливой. Однако, когда мы подставляем в качестве
программы нашу' первую, так сказать, ее реализацию и полу-
чаем
S: do х>О-+х:=х-1
О х <О-;-+ продолжение :»« истина: х: = 1;
do продолжение -+X:=x+I
О продолжение - продолжение: =ложь
od
od
то равенство wp (S, Т) = (х~О) абсолютно верно, как ин-
туитивно, так и формально.
Вторая причина имеет совсем другую природу. Конструк-
ция неограниченной недетерминированности, тем не менее за-
ведомо приходящая к завершению, могла бы за конечное вре-
мя производить выбор из бесконечно большого числа возмож-
ностей: если бы такую конструкцию можно было описать на .
нашем языке программирования, то сам по себе этот факт
представлял бы непреодолимый барьер на пути к возможности
реализации этого языка программирования.
Благодарность. Я хотел бы выразить свою огромную при-
· знательность Джону К. Рейнольдсу за то, что он обратил мое
внимание на ту роль, которую играет свойство 5, и на то, что-
несуществование конструкции «установить х равным любому
положительному целому» очень важно для интуитивного оп-
равдания семантики конструкции повторения. Естественно, что
он не несет никакой ответственности за все вышеизложенное.
· (Конец благодарности.) ·
10 РАЗМЫШЛЕНИЯ НА ТЕМУ:
«ОБЛАСТЬ ДЕЙСТВИЯ ПЕРЕМЕННЫХ».

Прежде чем приступить к выяснению того, что могло бы


или должно было бы означать понятие «область действия пе-
ременных», имеет смысл поставить сначала предварительный
вопрос, а именно: «Почему мы вообще ввели понятие перемен-
ных?». Этот вопрос не так уж бессодержателен, как могло бы
показаться тому, кто имел дело только с программированием.
Например, когда разрабатывается последовательное соедине-
ние логических элементов машины, то довольно часто сначала
проектируется автомат с конечным числом состояний, причем
различные возможные состояния проектируемого автомата
просто перенумеровываются «0, 1, 2, ... » в том порядке, в ко-
тором конструктор обнаруживает желательность их включения.
В ходе проектирования конструктор строит так называемую
«таблицу переходов», которая определяет для каждого состоя-
ния переход к следующему состоянию в зависимости от вход-
ного символа, поступающего для обработки. Только когда фор-
.мироtзание таблицы переходов закончено, конструктоР. вспоми-
нает, что в его распоряжении есть только.двоичные переменные
(он называет их триггерами) и что если число состояний ока-
валось между 33 и 64 (включая границы), то ему потребуется
по крайней мере шесть таких переменных. Эти шесть перемен-
ных охватывают пространство состояний из 64 точек, и конст-
руктор может по своему усмотрению выбирать способ пред-
ставления различных состояний своего автомата в виде точек
в 64-точечном пространстве состояний. То, что этот выбор ска-
зывается на проектируемой конструкции, становится ясно сра-
зу же, как только мы поймем, что переходы автомата с конеч-
ным числом состояний из одного состояния в другое должны
представляться как перевычисление , комбинаций булевых
переменных в зависимости от тех или иных булевых значений.
Конструкторам логических схем этот выбор известен как
«задача назначения состояний», и благодаря техническим огра-
ничениям или стремлению к· оптимизации задача может ока-
заться запутанной, столь.эапутанной, что возникает искушение
112 Глава 10

усомниться в пригодности методологии проектирования, вызы-


вающей такие проблемы. Однако критическое обсуждение ме-
тодологии проектирования логических схем лежит За преде-
лами моей компетентности и, следовательно, выпадает из круга
вопросов, рассматриваемых в данной книге; мы упомянули об
этом только для того, чтобы ·показать, что существуют такие
традиции проектирования, для которых введение «переменных»
в самом начале разработки проекта не является, по-видимому,
естественным.
По двум причинам программист живет в другом мире, чем
конструктор логических схем, в особенности конструктор ста-
рой школы. Было время, когда технические соображения ока-
зывали очень сильное влияние на стремление конструкторов
минимизировать число используемых триггеров, и если при
создании механизма с конечным числом состояний, не превы-
шающим 64, использовалось больше шести триггеров, это
рассматривалось если не как преступление, то уж по меньшей
мере как полный провал. Поскольку триггеры дороги, конст-
рукторы логических схем занимаются проектированием
(под) механизмов, у которых число возможных состояний куда
более скромно, чем число внутренних состояний у конструк-
ций, предлагаемых здесь вниманию программистов. Програм-
мист живет в мире, где биты обходятся дешевле, и отсюда вы-
текают два следствия: во-первых, его конструкции могут иметь
во много раз больше внутренних состояний, во-вторых, «неис-
пользованная» часть пространства состояний у нег.о может
быть большей, чем у конструктора логических схем. Вторая
причина, по которой мир программиста отличается от мира
конструктора схем, заключается в том, что большее число из-
менений (групп) битов приводит к большему времени вычис-
лений: самое дешевое средство; которое может в этом случае
прописать программист,- оставлять большие области памяти
незатронутыми!
Сравнивая мир программиста с миром конструктора схем,
который первоначально просто «именует» состояния своей ков-
струкции, сообщая им порядковые номера по мере ввода новых
состояний, мы сразу же понимаем, почему программисту хо-
чется с самого начала ввести переменные, которые он рассмат-
ривает как декартовы координаты в пространстве состояний
стольких измерений, сколько он чувствует необходимым ввес-
ти: число различных вводимых состояний так невероятно
велико, что несистематизированная терминология сделала бы
проектирование совершенно не поддающимся контролю. Мож-
но ли (и если можно, то как?) сохранить «управляемость»
проектирования, вводя переменные посредством «номенклату-
ры»,- это, конечно, решающий вопрос.
Размышления на з емз]: «Область действия переменных» 113

Основным средством изменения состояния служит опера-


тор присваивания, устанавливающий (вплоть до следующего
присваивания) новое значение одной переменной. В простран-
стве состояний ему соответствует движение, параллельное
одной из осей координат. Его можно «принарядить», введя
одновременное присваивание; из небольшого числа операторов
присваивания мы можем таким образом сформировать про-
граммную компоненту, но число переменных, на которые будет
воздействовать эта компонента, оказывается всегда весьма
скромным. Чистый результат выполнения такой программной
компоненты можно описать в терминах движения в под-
пространстве, охватываемом несколькими переменными, на ко-
торые воздействует эта компонента (если угодно, в терминах
проекции на это подпространство). Тогда она становится абсо-
лютно независимой от всех остальных переменных в системе:
мы можем «понимать» ее, не принимая во внимание никакие
другие переменные, даже не сознавая их существования. Воз-
можность такого разделения, известного также как «фактори-
зация», неудивительна, если учесть, что наше полное простран-
ство состояний было построено прежде всего как декартово
«пронэвеленве». Важно заметить, что это разделение, так не-
обходимое нам, чтобы мы могли охватить (мысленно) всю
программу в целом, становится тем более нужным, чем больше
общее число переменных, используемых в программе. Вопрос
заключается в том, должно ли такое «разделение» (и если дол-
жно, то как?) отразиться более явно на текстах наших про-
грамм.
Наши первые «автокодировщики» (позже некоторые из них
были. нецравильно названы «системами автоматического про-
граммирования» или - даже хуже - «языками программиро-
вания высокого уровня»), конечно, не обладали средствами
для реализации таких возможностей. Они были задуманы в то
время, когда, по общему мнению, программы были предназ-
начены для того, чтобы командовать машинами, тогда как в
наше время все больше и больше людей склоняются к мнению,
что это машины предназначены для выполнения программ.
В те далекие дни было вполне естественным, что разнообраз-
ные свойства машины находили непосредственное отражение
в записи программ. Так, например, поскольку машины имели
так называемые «команды передачи управления», в програм-
мах использовались «операторы перехода». Аналогично, по-
скольку машины располагали памятью постоянного размера,
считалось, что вычисления происходят в пространстве состоя-
ний с постоянным числом измерений, т. е. что в вычислениях
участвуют значения постоянного набора переменных. Анало-
гично, поскольку для памяти с произвольной выборкой каж-
114 Глава 10

дая ячейка памяти равнодоступна, программисту было позво-


.лено ссылаться на любую переменную программы в любом.
месте текста программы.
Все это было хорошо в те далекие дни, когда из-за ограни-
ченных размеров памяти тексты программ были короткими и
число различных переменных, используемых в программе,
было невелико. Однако с ростом размеров и сложности про-
грамм стали возникать сомнения в достоинствах такой одно-
родности. Конечно, с точки зрения гибкости и широкой при-
менимости память с произвольной выборкой является блестя-
щим изобретением, но наступает момент, когда мы должны
ясно понять, что любая гибкость, любая общность наших
средств требует дисциплины для их испольэовання. Этот мо-
мент настал. Давайте прежде всего примемся за «свободный
доступ».
В первой версии ФОРТРАНа. было два типа переменных,
целые переменные и переменные с плавающей запятой, тип
переменной определялся - в соответствии с фиксированным
соглашением - по первой букве ее имени, и любое появление
амени переменной в каком-либо месте текста программы пред-
полагало постоянное существование переменной с этим име-
нем на протяжении всего времени выполнения программы. На
практике это оказалось очень опасным: если в программе, опе-
рирующей с переменной, названной «TEST», встречалась един-
ственная описка, где вместо имени «TEST»· было написано
«TETS», никакого сигнала об ошибке не выдавалось, просто
заводилась другая переменная с именем «TETS». В АJJГОЛ~
'60 была принята идея так называемых «описаний», и в том,
что касается ловли таких глупых описок, она оказалась крайне
ценной формой избыточности. Основная идея явного описания
переменных заключается в том, что операторы могут ссылать-
ея только на те переменные, о· существовании которых было
явно объявлено; в- этом случае ошибочное обращение к пере-
менной «TETS» обнаруживается автоматически, так как о су-
ществовании такой переменной не было объявлено. У описа-
ний АЛГОЛа 60 было и второе назначение: кроме того, что они
· представляли собой «словарь» имен переменных, допустимых
для использования в операторах, они ставили в соответствие
каждому введенному имени тип переменной, отменяя тем
самым первоначальное соглашение, принятое в ФОРТРАНе,
что тип переменной определяется по первой букве ее имени.
Поскольку люди все больше и больше стремилис~ использо-
вать мнемонику при выборе имен переменных, такая свобода
наименований была высоко оценена.
Еще большим отклонением от ФОРТРАНа была принятая
в АЛГОЛе 60 так называемая «концепция блока», которая
Размышления на те,иу: «Область действия переменных» 115

использовалась для установления г~аниц так называемой


«текстуальной области действия» описаний. «Блок» в АЛГОЛе
60 простирается от открывающей скобки «begin» до соответ-
ствующей закрывающей скобки «end», и эта пара скобок
отмечает границ~ текстуального уровня перечня наименова-
ний. За открывающей скобкой «begin» следует описание одного
или более новых имен (когда имен больше одного, они все
должны быть· различными) с указанием того, что означает
каждое имя, и эти имена в их новом значении считаются
«локальными» для данного блока: употребление этих имен
в их новом значении должно быть сосредоточено в операторах,
находящихся между предшествующим «begin» и соответству-
ющим «end». Если вся наша программа - единственный блок,
к каждой переменной можно обратиться из любого места
текста, и единственная выгода от использования описаний-
это защита против описок (о чем говорилось в предыдущем
абзаце). Блок, однако,- это одна из возможных форм опера-
торов, следовательно, могут ~стречаться вложенные блоки, и
здесь описания дают возможность осуществить некоторую
защиту переменных, поскольку переменные, локализованные
во внутреннем блоке, недоступны для внешнего блока.
Правила области действия в АЛГОЛе 60 защищают ло-
кальные переменные внутреннего блока от внешнего воздейст-
вия; в другом направлении, однако, они не обеспечивают ни-
какой защиты. Для внутреннего блока в принципе доступно
все, что находится. снаружи, т. е. доступно все, что находится
в текстуально объемлющем блоке. Я сказал «в принципе».
потому что существует одно исключение,- исключение, кото-
рое в то время рассматривалось как крупное изобретение, а
теперь, 15 лет спустя, при ближайшем рассмотрении оказыва-
ется больше похожим на логическую заплату. Я имею в виду
так называемый «повторно описанный идентификатор». Если
в заголовке внутреннего блока описано имя - в АЛГОЛ~
60 имена называются <<идентификаторами»,- которое ( слу-
чайно) уже было употреблено в объемлющем блоке, тог да
внешнее значение этого идентификатора остается текстуально
скрытым для внутреннего блока, чье локальное описание то-
го же идентификатора перекрывает его внешнее описание.
Идея, лежащая За этим «приоритетом самых внутренних опи-
саний», заключается в том, что составителю внутреннего бло-
ка нужно знать только те глобальные имена в окружающем
контексте, на которые он действительно ссылается, а все дру-.
гие глобальные имена не должны ограничивать его свободу
выбора своих собственных имен с их локальным значением.
Тот факт, Что благодаря этому соглашению вписывание внут-
ренних блоков в «разрастающееся» окружение проходило
'116 Глава 10

гладко (например, программы пользователей целиком встав-


.•лялись в уже имеющийся внешний блок, содержащий обраще-
пне к стандартным библиотечным процедурам), в течение
длительного времени рассматривался как вполне достаточное
объяснение причин использования соглашения о приоритете
внутренних описаний. Оправданию этого соглашения способ-
-ствовало и то, что незадолго до его принятия было обнаруже-
но, как однопроходный ассемблер может использовать стек
для обработки текстуально вложенных различных значений
одного и того же имени,
Однако, с точки зрения пользователя, это соглашение не
"Так уж привлекательно, ибо делает переменные, описанные во
внешнем блоке, крайне уязвимыми. Если пользователь с ужа-
-сом обнаруживает, что значение одной из этих переменных
испорчено, причем непонятно, когда и каким образом, он дол-
жен в принципе тщательно просмотреть все внутренние блоки,
'Включая и те, которые и вовсе не должны обращаться к этой
·переменной, чтобы обнаружить то место, где происходит это
-ошибочное обращение. Если предположить, что программисту
пе. обязательно приходится обращаться из любого места к лю-
бой переменной, то лучше было бы предложить . ему белее
явные средства ограничения текстуальной области действия
имен, чем более или менее случайное повторное описание.
В качестве первого шага в этом направлении, сохраняюще-
го идею текстуально вложенных контекстов, предлагается сле-
дующее: для каждого блока мы постулируем для его уровня
(т. е. для его текста, за исключением входящих в него внут-
ренних блоков) текстуальный контекст, т. е. постоянную но-
менклатуру, в которой все имена имеют уникальное значение.
Имена, встречающиеся в текстуальном контексте блока, могут
быть либо «глобальными», т. е. доставшимися ему вместе с их
значениями по наследству от непосредственного окружения,
либо «локальными», т. е. имеющими смысл только для текста
данного блока. Предлагается помещать вслед за открывающей
скобкой блока перечень имен (с соответствующими раздели-
телями), составляющих вместе текстуальный контекст, напри-
мер, сначада перечислить.все глобальные имена (если таковые
имеются), затем все локальные (если они есть); очевидно, что
все имена должны быть различными.

Признание. Нужно сказать, что я решился на это предло-


жение после длительных колебаний и поисков альтернативных
предложений, которые позволили бы программисту вводить
в блок одно или больше локальных имен, не вынуждая его
при этом перечислять все используемые в блоке глобальные
имена, т. е. таких предложений, которые дали бы ему возмож-
Размышления на тему: «Область действия переменных» 117

ность указывать (с той же компактностью) изменения в но-


менклатуре блока АЛ~ОЛа 60 (без «повторного описания
идентификаторов»}, т. е. в чистом виде расширять номенкла-
туру, Это предоставило бы программисту возможность указы-
вать и на сужение номенклатуры, т. е. на ограниченное ис-
пользование имен из непосредственного окружения, но не вы-
пуждало бы его делать это в обязательном порядке. И
некоторое время такая позиция избавления программиста от
~отеческого попечения» казалась мне правильной для разра-
ботчика языка; я чувствовал также, что такая возможность
сделает мои правила области действия более привлекатель-
ными, поскольку я опасался, что многие программисты будут
возражатъ против необходимости перечислять все используе-
мые глобальные переменные всякий раз, когда у них возникнет
потребность ввести локальную переменную. Так продолжа-
...лось до тех пор, пока я страшно не рассердился сам на себя:
столько проектов языков загублено из опасения, что они не
найдут одобрения, а ведь я знаю только одного программис-
та, который собирается писать программы на этом языке, и
этот программист - я сам! А я в достаточной степени пурита-
нин и могу заставить себя явно указывать унаследованные
глобальные переменные. Более того, не только мои внутрен-
ние блоки будут ссылаться только на явно унаследованные
глобальные переменные, но и среди унаследованных перемен-
ных не будут упоминаться глобальные переменные, к-которым
ео внутренних блоках нет обращения. Перечень унаследован-
ных переменных даст полное описание возможных нарушений,
вызываемых блоком в пространстве состояний, определенном
для окружения блока, не больше и не меньше! Когда я обна-
ружил, что мое желание «доставить удовольствие своей пуб-
"пике» - желание, я считаю, вполне благородное - повлияло
не только на способ представления, но и на существо дела,
я испугался, разозлился на себя и устыдился. (Конец при-
энаниял
Кроме имени переменные обладают уникальным свойством
иметь некоторое значение, которое может изменяться. Сразу
же возникает вопрос: каким будет значение локальной пере-
менной при входе в блок? Были предложены различные отве-
ты. АЛГОЛ 60 постулирует, что при входе в блок значения
его локальных переменных не определены.. т. е. для любой
попытки узнать их значение до того, как им будет присвоено
какое-то значение, 'реаультат не определен. Очень распростра-
ненной ошибкой является использование в цикле локальных
переменных, которым перед входом в цикл не было присвоено
начальное значение, 'поэтому контроль за использованием
118 Глава 10

неопределенных значений локальных переменных, осуществля-


емый во время выполнения программы, хотя и обходится доро-
го, во многих случаях оказывается отнюдь не роскошью.
Такой контроль является, конечно, прямой реализацией ответа
чистого математика на вопрос о том, что делать с переменной"
чье значение не определено, а именно дополнить интервал зна-
чений специальным значением, называемым «НЕОПРЕДЕ-
ЛЕННОЕ» и при входе в блок придавать это уникальное, спе-
циальное значение каждой локальной переменной. Тогда лю-
бая попытка использовать переменную с этим уникальным"
специальным значением приведет к прекращению работы про-
граммы и выдаче сообщения об ошибке,
Однако при ближайшем рассмотрении оказывается, что
это простое предложение приводит к логическим проблемам;
например, становится невозможным копировать значения лю-
бого набора переменных. Чтобы совладать с такой ситуацией,
можно, например, проверять, определено значение переменной
или нет. Но возможность манипулировать со специальным
значением, например со значением «НИЧТО» для указателя"
никуда не указывающего, легко приводит к неразберихе и про-
тиворечию: так. и про двух холостяков можно сказать, что мы
имеем дело с двоемужием, поскольку можно считать, что у
них одна и та же жена - «никто».
Другим выходом из положения, упраздняющим перемен-
ные с неопределенными значениями, было неявное придание
переменным некоторого значения при входе в блок, причем не
очень специального, а очень распространенного, почти
«нейтрального» значения (скажем, «нуль» для всех целых и
«истина» для всех булевых переменных). Но это, конечно, все-
го лишь игра в прятки с самим собой; теперь обнаружить
очень распространенную программистскую ошибку стало не-
возможно, поскольку разнообразные лишенные смысла про-
граммы были искусственно превращены в программы дозво-
ленные. (Чтобы смягчить это предложение, было принято
соглашение о том, что «нейтральное» начальное значение
будет присваиваться переменной только «по умолчанию», т. е.
• v 1
если нет других указании, однако ясно, что такое соглашение
об умолчании-с- это всего лишь латание дыр).
Другой попыткой решить задачу обнаружения использова-
ния переменных с еще не определенными значениями было
проведение (автоматического) анализа текста, позволявшего
по крайней мере предупредить программиста, что в каких-то
местах переменные будут - или могу1: - использоваться до
того, как им будет присвоено некоторое значение. Можно
считать, что в каком-то смысле мое предложение было инспи-
Размышления на тему: «Область действия переменных» 119

рировано этим подходом. Я предлагаю такую жесткую дисци-


плину, что
1' анализ текста становится тривиальным;
2) ни в одном месте программы, где происходит обращение
к переменной, не может существовать неопределенности, каса-
ющейся того, было ли присвоено начальное значение этой пе-
ременной.
Одним из способов достижения этого была бы обязатель-
ная инициализация всех локальных переменных при входе в
блок; боюсь, что в сочетании с желанием не использовать
в качестве начальных значений «бессмысленные» значения,-
желанием, которое подразумевает, что локальные переменные
должны вводитьс~ только на той стадии, когда доступны их
осмысленные начальные значения,- это приведет к слишком
уж глубокой вложенности текстуальных областей действия.
К тому же нам пришлось бы «распределять» вход в блок по
'различным охраняемым командам конструкции выбора всякий
раз, когда инициализация должна была бы выполняться одной
из охраняемых команд из некоторого набора. Оба эти сообра-
жения вынудили меня искать альтернативу, которая требо-
вала бы меньшего количества (и более однозначных) границ
блока. Следующее предложение, как мне кажется, отвечает
этим требованиям. ,
Прежде всего мы настаиваем.гчтобы при входе в блок пере-
числялись все имена (как унаследованные, так и личные).
Кроме оператора присваивания, который уничтожает текущее
значение переменной, присваивая ей новое значение, у нас
будут инициализирующие операторы, которые распознаются
синтаксически и которые сообщают личной переменной ее
первое после входа в блок значение. (Если угодно, мы можем
считать, что выполнение инициализирующего оператора совпа-
дает по времени с «порождением» Переменной; тогда более·
раннее упоминание ее имени во входе в блок можно рассмат-
ривать как «резервирование идентификатора для пе~е-
менной».)
Иначе говоря, текстуальная область действия личной (т. е.
локальной) переменной блока - это область от открывающего
«begin» блока до соответствующего закрывающего «end», за
исключением текстов тех внутренних блоков, которые не на-
следуют эту переменную. Мы предлагаем раэделить текстуаль-
ную· область действия переменной на «пассивную областъ
действия», где обращение к переменной не допускается и ее
нельзя рассматривать как координату в локальном простран-
стве состояний, и «активную область действия», где разрешено
обращение к переменной. Пассивная и. активная области дей-
120 Глава 10

ствия переменной всегда будут отделены друг от друга Иници-


ализирующим оператором для этой переменной, и эти опера-
торы должны быть размещены таким образом, чтобы незави-
симо от значений предохранителей
1) после входа в блок и перед соответствующим выходом.
из блока выполнялся в точности один инициализирующий опе-
ра тор;
2) между входом в блок и выполнением инициализирующе-
го оператора не мог 'выполняться ни один оператор из актив-
ной области действия переменной.
Выполнение вышеизложенных требований гарантирует
следующая дисциплина: прежде всего мы рассматриваем блок
как синтаксическую структуру, где за перечнем его личных
имен следует список операторов, отделенных друг от друга
точкой с запятой. Такой список операторов должен обладать
следующими свойствами:
(А) Он должен содержать единственный оператор, инициали-
зирующий данную личную переменную.
(Б) Операторы, предшествующие инициализирующему опера-
тору (если таковые имеются), находятся в пассивной об-
ласти действия этой переменной, и если они представляют
собой внутренние блоки, то они не имеют права наследо-
вать эту переменную.
(В) Операторы, следующие за инициализирующим оператором
(если таковые имеются), находятся в активной области
действия переменной, и если они являются внутренними
блоками, могут наследовать эту переменную.
(Г) Для самого инициализирующего оператора имеются три
возможности:
( 1) Это простой инициализирующий оператор.
(2) Это внутренний блок. В этом случае он наследует пе-
ременную, о которой идет. речь, и список операторов"
следующий за перечнем имен внутренней номенклату-
ры, также обладает свойствами (А), (Б), (В) и (Г).
(3) Это конструкция выбора. В этом случае все списки
операторов, следующие за ее предохранителями, обла-
дают свойствами (А), (Б), (В) и (Г).
Для приверженцев Н Ф Б может быть полезным следующий
синтаксис (где инициализация относится к одной личной пе-
ременной):
·<блок>::=Ьеgiп <номенклатура>; <сп.исок инициализи-
рующих операторов> end
<список инициализирующих операторовэ-т.= {<пассивный
оператор>;} <инициализирующий оператор>{; <актив-
ный оператор>}
Размышления на тему: «Область действия переменных» 121

<инициализирующий оператор э-л= <простой инициализи ..


рующий оператор> 1
begin <номенклатура>; <список инициализирующих
операторов> end 1
if <предохранитель>-+ <список инициализирующих опе-
раторов> { О <предохранитель>-+ <список инициали-
зирующих операторов>} fi
Замечание. При составлении программ на АЛГОЛе 60 не-
обходимость полностью выписывать скобки «begin» и «end»
вызывает неудобство; упраздняя составные операторы
АЛГОЛа 60, мы рассчитываем уменьшить потребность в этих
скобках. (Конец замечаниял
Соответствующая конструкция повторения не вклювена в
число допустимых форм инициализирующего оператора, по-
скольку ее включение привело бы к нарушению нашего пер-
вого требования: независимо от последовательности действий
инициализация должна происходить в точности один раз. Та-
кое ограничение не встречается в языках программирования
типа АЛГОЛ 60, где просто (динамически) первое присваива-
ние рассматривается как «инициализация». За большую сво-
боду в языке типа АЛГОЛ нам приходится расплачиваться
тем, что для программ, написанных на таком языке, мы не
можем в каждом месте программы, где стоит точка с запятой,
решать статически (т. е. для всех вычислений), значения ка-
ких переменных определены, что видно из следующих при-
меров па языке АЛ ГОЛ 60.
Если В - глобальная булева переменная,
begin integer х, у; if В then х: = 1 else у: =2; ...
и если N - глобальная целая переменная.,
begin integer i, х; for i: = 1 step 1 until N do х: = 1; ...
В первом примере значение х определено только при усло-
вии, что В - истина; во втором примере значение х определено
только при N~ 1. Очевидно, что ни одна из этих двух конст-
рукций не может быть переведена на наш язык, и мы должны
убедить себя, что сможем жить при этих ограничениях, кото-
рые мы сами же себе и навязали, или даже что без таких начал
блоков, как в двух приведенных примерах, не только можно
обойтись, но в каком-то смысле они просто бесцельны. Ин-
туитивным: обоснованием того, что внутри конструкции повто-
рения нам никогда не понадобится выполнять дивамически
первое присваивание значения той переменной, чье -значение
при завершении конструкции несущественно, может служить
следующее рассуждение: такая переменная должна входить
122 Глава 10

либо в инвариантное отношение, либо в предохранители, либо


и в отношения, и в предохранители, и поэтому лучше было бы
придать ей определенное значение и в начальном состоянии
конструкции повторения. (В следующей главе мы увидим, что
такая позиция заставит нас пересмотреть понятие «перемен-
ной».)
Поскольку мы ограничили себя рассмотрением такого язы-
ка, что при подходе к каждой точке с запятой статически из-
вестно, какие переменные составляют текущее пространство
состояний, нам нет нужды сомневаться - если мы пишем на-
ши программы на языке типа АЛГОJJа - в том, является
ли данное присваивание значения личной переменной динами-
чески первым с момента входа в блок, и поэтому требование,
чтобы синтаксическая форма <простой инициализирующий
оператор> отличалась от синтаксической формы обычного опе-
ратора присваивания, не является дополнительным ограниче-
нием. По зрелом размышлении представляется, что было бы
даже нечестно использовать одинаковые обозначения, посколь-
ку эти операторы весьма различны: в то время как инициали-
зирующий оператор является в каком-то смысле созидающим
оператором (он порождает новую переменную с начальным
значением), обычный оператор присваивания является опера-
тором разрушающим в том смысле, что он уничтожает ин-
формацию, а именно предыдущее значение переменной.

Замечание. Нас не должна очень удивлять специальная


роль конструкции повторения, так как, если бы мы им~ли дело
с языком, в котором такая конструкция не предусмотрена,
нам никогда не понадобилось бы настоящее присваивание; мы
могли бы обойтись только инициализацией: без повторения мы
могли бы писать программы в терминах констант, которым
один раз придается начальное· значение. Только с введением
повторения у нас возникает необходимость иметь' в полном
смысле слова переменные, т. е. нам нужно, чтобы на различ-
ных шагах процесса повторения при ссылке на одно и то же
имя мы получали разные значения. Именно понятие повторе-
ния влечет за собой понятие переменной, чье значение можно
изменять при помощи оператора присваивания. Именно в про-
цессе повторения нам действительно нижно присваивать зна-
чения переменным, существующим вне данного блока, и имен-
но для повторения мы не можем разрешить инициализацию
таких переменных. (Конец замечания.)

Нам еще осталось решить несколько вопросов. Первый -


где и как указывать тип личных переменных. Представляются
возможными два места: либо - как в АЛГОЛе 60 - в начале
Размышления на тему: «Область действия переменных» 123

блока, для которого переме.нная является личной и ее имя


встречается где-то в перечне имен, либо в простом ини-
циализирующем операторе (или операторах).
Существуют, однако, две причины, по котqрым я чувствую
необходимость отойти от соглашения, принятого в АЛГОЛе 60,
и ограничить функцию перечисления имен в начале блока
только определением текстуальной области действия имен, не
отягощая этот перечень информацией о типе переменных. Мы
дадим обоснование этому отдельно для унаследованных и "лич-
ных имен.
Если мы потребуем, чтобы каждое унаследованное имя в
номенклатуре сопровождалось явным указанием типа, во что
нам это обойдется и что мы выиграем? Нам придется распла-
чиваться не только удлинением текстов, но и тем, что характе-
ристика внешней среды, а именно тип глобальной переменной,
распространится по всему тексту. Во всех номенклатурах, со-
держащих унаследованное имя и рассеянных -по всему тексту,
мы будем находить копию информации о типе переменной. Все
это выглядит особенно непривлекательно в случае необходи-
мости сменить тип такой глобальной переменной на другой,
для которого определен тот же набор функций и операций.
Кроме того, мы решили, что явное указание унаследованных
переменных выполняет функцию защиты от внешнего мира,
служит сжатым выражением того, как внутренний блок может
взаимодействовать со своим окружением, которое, как пред-
полагается, нам известно. Неоднократное повторение типа не
скажет нам в сжатой форме чего-нибудь нового или интерес-
ного о внутреннем блоке .. Единственный выигрыш, полученный
от включения в блок информации о типе глобальной перемен-
ной, по-видимому, состоит в том, что при изучении такого бло-
ка изолироаанно от всей программы в нем будет легче разо-
браться. Но я не совсем уверен, что нужная нам информация
об окружении может быть сведена только к типу глобальных
переменных: могут оказаться существенными и предполагае-
мые отношения между начальными значениями. Если мы хо-
тим понять, что делает какой-либо внутренний блок, нам нуж-
но более полное описание интерфейса, и до 'тех пор, пока мы
не собираемся автоматизировать доказательства правильно-
сти программы, более подходящим средством кажутся пред-
ставленные в удобной форме комментарии. (Читателя, кото-
рый сейчас восклицает: «А как же быть с независимой комли-
ляцией?» - мы просим понять, что это эамечание относится
только к реализации очень специфического характера, и пред-
лагаем ему вообразить себя, как это делаю я, находящимся в
такой обстановке, когда этот вопрос абсолютно лишен
смысла.)
1~4 Глава 10

Вернемся теперь к личным переменным. Если тип перемен-


ной указывается в перечне имен, каждая активизация блока
вызовет инициализацию переменной этого типа. Мы видели,
однако, что если инициализирующий оператор является кон-
струкцией выбора, то при каждой активизации блока будет-
выполняться один из множества простых инициализирующих
операторов. Я допускаю, что таки~ образом переменная мо-
жет получать начальные значения различных типов, и НИКТ()
не будет возражать -против этого, если только над такими ти-
пами определены одни и те же функции и операции, Полезна
ли такая свобода выбора типа личной переменной или нет -
это довольно трудно обсуждать на данном этапе. Во всяком
случае, было бы неразумно принять сейчас такое соглашение
об обозначениях, которое лишало бы нас возможности решить
этот вопрос в будущем. (Как заметит внимательный читатель"
возможность того, что тип личной переменной может быть.
различным при разных активизациях блока, для которого эта
переменная является личной, служит еще одной причиной для
возражений против копирования информации о типах при
описании наследуемых переменных, т. е. во внутренних бло-
ках.)
Следующий вопрос, который мы должны решить, - ЭТ()
вопрос о том, нужно ли при входе в блок давать указания о
природе воздействия блока на унаследованные переменные.
Я напоминаю, что одной из причин включения в начало блока
перечня переменных из окружающего контекста, к которым
блок обращается, было стремление сократить объем текста,
_ который приходится изучать, когда одна из таких переменных
оказывается непостижимым образом испорченной. Если та-
кая переменная при каждой активизации внутреннего блока·
действует как «глобальная константа», нам хотелось бы прямо
"в начале блока видеть, что он не может испортить значение
этой переменной - более точно, что он может только выби-
рать, но не изменять это значение.
Давайте на минуту сосредоточим внимание на внутреннем
блоке (с контекстом «ВНУТРИ»), полностью находящемся в
активной области действия переменной, которую он наследует
от непосредственно объемлющего его блока (с контекстом
«ВНЕ.,>).
Если на уровне контекста ВНЕ переменная может изме-
няться, то, как мы видели, должно быть два возможных спосо-
ба наследования переменной: либо она может также изме-
няться и в контексте ВНУТРИ, т. е. внутренний блок может
содержать операторы присваивания, которые придают этой
переменной новые значения, либо внутренний блок полу~ает
эту переменную как глобальную константу. Этот факт, однако"
Размышления на тему: «Область действия переменных» · 12&

имеет последствия для обоих контекстов. Для контекста ВНЕ


он означает, что, когда нам интересно узнать, что может воз-
действовать на значение переменной, мы можем пропустить.
текст внутреннего блока. Если, однако, мы хотим читать текст
внутреннего блока (более или менее в отрыве от его окруже-
ния) и при этом понимать, как s:>н будет выполняться, тогда
тот факт, что такая глобальная константа есть константа, а не
переменная, приобретает решающее значение. Уже в первом
примере главы «Формальное рассмотрение нескольких неболь-
ших примеров» мы начинали с такой постановки задачи: «Для·
фиксированных х и у обеспечить истинность отношения R (т)·
(m=x or m=y) and т ""> х and т "">у>>
То что х и у считаются фиксированными, весьма сущест-
венно для данной задачи.
Если: переменная является константой уже на уровне кон-
текста ВНЕ, тогда она может передаваться внутреннему бло-
ку только одним способом: тоже как константа. Для случая"
когда внутренний блок целиком находится в активной области
действия наследуемой переменной, все обошлось благопо-
лучно.
Случай, когда внутренний блок целиком находится в пас-
сивной области действия переменной, также не qредставляет·
никакой трудности: блок не может наследовать эту перемен-
ную из своего окружения.
Нашего внимания заслуживает третий случай, когда блок
начинается в пассивной области действия переменной, а кон--
чается в ее активной области действия; это означает не более
и не менее как то, что блок обязан инициализировать пере-
менную. Ему передали в наследство то, что мы могли бы на-
звать «нетронутой переменной», чтобы он выполнил инициали-
зацию. Как для контекста ВНУТРИ, так и для контекста ВНЕ
нас может интересовать вопрос о возможности изменения. пе-
ременной в ее активной области. Когда наследуется нетрону-
тая переменная, эти два вопроса полностью независимы; то
обстоятельство, что на текстуальном уровне контекста ВНЕ
.значение, полученное переменной после инициализации, не бу-
дет меняться, не исключает возможность, что сама инициали-
зация (во внутреннем блоке) ~редс1'авляет собой многошаго-
вую операцию, а это так и есть, когда мы рассматриваем ини-
циализацию не как единый, неделимый акт, но - интересуясь
деталями - как последовательный процесс формирования на-
чального значения.
После всех этих рассуждений пришла пора быть как можно
более точным. Напомним, что мы ввели название «тексту-
альный контекст» блока для постоянной номенклатуры (в ко-
126 Глава 10

торой все имена имеют единственное значение), принадлежа-


щей «уровню» блока, т. е. его тексту, за исключением его внут-
ренних блоков. Рассмотрим теперь два блока: внутренний
блок (с контекстом, называемым «ВНУТРИ») и объемлющий
его внешний блок (с контекстом, называемым «ВНЕ»). По
отношению к контексту ВНУТРИ контекст ВНЕ будет высту-
пать как «окружающий контекст».
Имена, используемые в контексте, могут быть двух типов:
.либо они являются личной собственностью блока, т. е. не име-
ют отношения к тому, что расположено снаружи блока, либо
они «получены по наследству» от окружающего контекста.
В этом случае мы должны различать две возможности: имя
переменной может передаваться контексту ВНУТРИ от кон-
текста ВНЕ с обязательством или без обязательства инициа-
лизировать эту переменную при актнвизации блока. Мы бу-
дем различать все эти три вида переменных при помощи сле-
дующих обозначений:
pri ( «private» - личная переменная, т. е. переменная,
не имеющая отношения к окружающему контекс-
ту);
vir . ( «virgin» - нетронутая переменная, т. е. перемен-
ная, полученная от окружающего контекста с обя-
зательством инициализировать эту переменную);
glo ( «global» - глобальная переменная, т. е. перемен-
ная, полученная от окружающего контекста без
обязательства и без разрешения ее инициализа-
ции).
Переменная может принадлежать более чем одному тек-
стуальному контексту: прежде всего она принадлежит тексту-
альному контексту того блока, для которого она является лич-
ной, и, кроме того, она принадлежит текстуальным контекс-
там всех внутренних блоков, которые наследуют ее от своих
окружающих контекстов. Область действия· переменной рас-
пространяется на уровни всех блоков, чьим текстуальным кон-
текстам принадлежит эта переменная. Область действия пе-
ременной всегда подразделяется на две части: на пассивную
область действия и активную область действия, и возможно-
сти размещения в тексте инициализирующих операторов для
переменных были ограничены таким образом, чтобы гаран-
тировать для каждой переменной своевременное выполнение
последовательности действий:
вход в блок, для которого переменная является личной;
нуль или более выполнений операторов из ее пассивной
области действия;
Размышления на тему: «Область действия переменных» 127

одна инициализация переменной;


нуль или более выполнений операторов из ее активной
области действия;
соответствующий выход из блока.
Когда речь идет о правах и обязанностях блока по отноше-
нию к некоторой переменной, не имеет значения, была она пе-
речислена под заголовком «pri» или под заголовком «vir»; в
обоих случаях блок обязан инициализировать эту перемен-
ную. Единственное различие между этими случаями состоит
в том, что переменная, перечисленная под заголовком «pri»,
· . не имеет отношения к контексту ВНЕ, в то время как пере-
менная, перечисленная под заголовком «vir», должна встре-
чаться в контексте ВНЕ, и внутренний блок должен начи-
наться' в ·пассивной области дейст~ия этой переменной и кон-
чаться в ее активной области действия. Переменная, перечис-
ленная под заголовком «glo», должна встречаться в контексте
ВНЕ, и внутренний блок должен целиком находиться в актив-
ной области действия этой переменной.
Кроме внешних характеристик используемых в блоке пе-
ременных, которые задаются при помощи «pri», «vir» или
«glo», имеются и внутренние характеристики, а именно ука-
зания о том, может ли на уровне блока изменяться значение
переменной в ее активной области действия. Если значение
переменной может изменяться, это указывается при помощи
«var» ( «VariaЬle» - переменная); если нет - для указания ис-
пользуется «СОП>> ( «сопыапг» - константа). Каждая из трех
внешних характеристик может комбинироваться с каждой из
двух внутренних характеристик, образуя шесть возможностей:
«privar», «pricon», «Virvar», «vircon», «glovar» и «glocon». Для
шести заголовков в контексте ВНУТРИ мы приводим допу-
стимые заголовки в контексте ВНЕ:

ВНУТРИ ВНЕ
privar, pricon не применяется
glovar privar, virvar (только если внутренний блок
целиком лежит в активной области действия)
или glovar (без ограничений)
glocon privar, pricon, virvar, vircon (только если внут-
ренний блок целиком лежит в активной обла-
сти действия) или glovar, glocon (без ограни-
чений)
virvar, vircon privar, pricon, virvar, vircon (только если внут-
ренний ·блок начинается в пассивной области
действия)
128 Глава 10

Замечание 1. Из изложенного выше следует, что характе-


ристика «con» в контексте ВНЕ исключает возможность изме-
нения значения этой переменной во внутренних блоках пос-
.ле ее инициализации. (Конец ~амечания 1.)
Замечание 2. Характеристика «con» в контексте ВНЕ не
исключает инициализацию переменной во внутреннем блоке,
на уровне которого переменная может обладать' характери-
стикой «var»: в контексте ВНЕ мы можем иметь переменную
«pricon taЬle», инициализация которой передается внутрен-
нему блоку, в чьем контексте будет создана переменная «Vir-
var tаЫе» и будет сформировано ее значение. После того как
один раз выполнится такой инициалиэирующий . внутренний
-блок, значение переменной «tаЫе» останется постоянным на
протяжении выполнения· внешнего блока (и других его внут-
ренних блоков, если таковые имеются). (Конец замечания 2.)
Решение оставшихся вопросов, хотя и достаточно важных
(они определяют, какими должны быть наши тексты, насколь-
ко легко писать их и читать), в меньшей степени чревато по-
-следствиями; эти вопросы касаются только синтаксиса. Мы
должны представить синтаксические определения для поня-
тий <номенклатура> и <простой нннциалиэирующий опера-
тор>. Я предлагаю дать определение номенклатуры, анало-
гичное определению понятия <заголовок блока> в АЛГОЛе
'60, - определению, к которому У· меня никогда не возникало
никаких претензий.
<номенклатура>::= <элемент номенклатуры> {;<элемент
номенклатуры>}
<элемент номенклатуры> :: = <з'аголовок номенклатуры>:
<переменная> {,<переменная>}
<~аrоловок номенкяатуры >;: =privar 1 . pricon 1 virvar 1
vircon 1 glovar 1 glocon
Допуская возможность последующего расширения, я пред-
.лагаю вывести определение инициализирующего оператора
иэ определения оператора присваивания, добавив после пере-
менной в левой части специальный символ «vir» - признак
·того, что мы имеем дело с .нетронутой переменной, - за кото-
рым следует наименование ее типа:
<простой инициализирующий оператор> :: = <переменная>
vir <тип> := <выражение>
Что касается типа, то до сих пор мы ограничивались толь-
:1(0 целыми и булевыми переменными: --
<тип>: : =цел 1 бул
Размышления на тему: «Область действия переменных» 129

Замечание 1. Расширение до одновременной инициализа-


ции нескольких переменных и одновременных инициализации
и присваивания предоставляется честолюбивому читателю.
(Конец замечания 1.)
Замечание 2. Выражение (выражения) в правой части дол-
жно рассматриваться как еще находящееся в пассивной обла-
сти действия инициализируемых переменных. (Конец замеча-
ния 2.)
Приведем в качестве примера внутренний блок, который
инициализирует глобальную целую переменную х, придавая
ей значение НОД (Х, У), где Х и У по отношению к внутрен-
. нему блоку являются положительными константами. Блок ис-
пользует личную переменную, называемую у.
begin glocon Х, У; virvar х; privar у;
х vir цел, у vir цел : = Х, У;
do х>у-•х: =х-у
ОУ>х--у:=у-х
od
end
Хочу Заметить, что мое решение включать указатели «var»
или «con» в номенклатуру было довольно произвольным. С од-
ной стороны, кому-то это указание может показаться излиш-
ним. Ну что же, если он захочет отменить это соглашение, он
волен это сделать; он может просто сократить заголовки до
«pri», «Vir» и «glo», опустив все var и con в моих программах.
Если мои пр.ограммы были правильными в соответствии с мои-
ми соглашениями, его версии программ также будут правиль-
ными в соответствии с его соглашениями. С другой стороны,
у кого-то может появиться. желание дать более точную ин-
формацию, чем это позволяют предлагаемые мной средства.
Он мог бы захотеть, например, указать,
1) что значение переменной может только возрастать;
2) что от начального значения глобальной переменной бу-
дет зависеть только ее собственное конечное значение, но не
значение других переменных;
3) что из всего набора способов изменения значения гло-
бальной переменной будет использоваться только его подмно-
жество.
Указание типа ( 1) представляется слишком специальным,
поскольку оно применимо только к тем переменным, чьи зна-
чения от природы упорядочены. Указание типа (2) тоже ка-
5-6
130 Глава 10

жется слишком специфическим. Как быть, например, с парой


глобальных перем~нных, чьи начальные значения вЛ'ияют
только на собственные конечные значения и на конечные зна-
чения друг друга? А вот указание типа (3) могло бы иметь
смысл. В эт9м случае наше указание «con» можно трактовать
как указание о том, что подмножество допустимых модифи-
каций пусто.
11 ВЕКТОРНЫЕ ПЕРЕМЕННЫЕ

Меня приучили рассматривать массив в смысле АЛГОЛа 60


как конечное множество одиночных, последовательно перену-
мерованных переменных, чьи «идентификаторы» могут «вы-
числяться». Однако такая точка зрения меня больше не удов-
летворяет, и объясняется это двумя причинами.
Первая причина - мое неприятие переменных с неопреде-
ленными значениями. В предыдущей главе мы разрешили эту
проблему, введя для каждой переменной пассивную и актив-
ную области действия, разделенные синтаксически распозна-
ваемой инициализацией этой· переменной. Но когда мы рас-
сматриваем массив как совокупность переменных (с индек-
сами), это решение уже не годится.
Вторая причина носит комбинаторный характер и являет-
ся более основательной. В АЛГОЛе 60 составной оператор,
который заставляет переменные х и у обменяться своими зна-
чениями, нуждается в дополнительной переменной, скажем h,
h := х; х:= у;·у := h

что выглядит громоздко и даже безобразно по сравнению с


одновременным присваиванием
х, н+=», х
Мы потребовали, чтобы п.ри одновременном присваивании
все переменные в левой части были различными: в самом де-
ле, глупо было бы вкладывать в «Х, х: = 1, 2» какой-то иной
смысл, чем «ошибка». Тем не менее я не сразу решился при-
нять одновременное присваивание из-за проблем:, возникаю-
щих в случаях, подобных такому:
A[i], A[J]:=x, у
Следует ли считать такое присваивание допустимым, ког-
да i=f= j, но не когда i = j? Или, может быть, оно допустимо при
5*
132 Глава 11

i=j, если при этом х=у, так же, как, например, и в случае
А [i], A[j] := A[j], А [i]
Если мы пойдем по такому пути, то, очевидно, будем гро-
моздить одну логическую заплату на другую. Однако я те-
перь пришел к заключению, что виновником всего этого яв-
ляется не одновременное присваивание, а понятие переменной
с индексом. В аксиоматическом определении оператора при-
сваивания через «подстановку переменной» нельзя позволить
себе - как, я полагаю, и во всех областях логики - никакой
неопределенности по поводу того, являются ли две переменные
одной и той же переменной или нет. \
Мораль всего этого такова, что мы должны рассматривать
весь массив в целом как одну переменную, так называемую
«векторную переменную», в противоположность рассматри-
ваемым до сих пор «скалярным переменным». В дальнейшем
я ограничусь векторными переменными, являющимися анало-
гом одномерных массивов.
Мы можем рассматривать переменную (ее значение) це-
лого типа как целочисленную функцию беэ аргументов (т. е.
как функцию, область определения которой состоит из един-
ственной безымянной точки), которая сохраняет свое значе-
ние, если не задано явно его изменение (обычно при помощи
присваивания). Вероятно, покажется необычным рассматри-
вать функцию без аргументов, но мы встали на такую точку
зрения ради проведения аналогии. Потому что подобным же
образом мы можем рассматривать переменную (ее значение)
типа «целый массив» как целочисленную функцию с одним
аргументом, определенную на множестве целых чисел, - функ-
цию, которая также сохраняет свое значение, пока не будет
явно задано его изменение. ·
Но значение переменной типа «целый массив» не может
быть 'любой целочисленной функцией, определенной на множе-
стве целых чисел, так как я ограничусь рассмотрением таких
типов, что для любых двух переменных этого типа мы можем
составить алгоритм, определяющий, имеют ли эти переменные
одно и то же значение. Если х и у являются скалярными пе-
ременными целого типа, тогда этот алгоритм сводится к бу-
левому выражению х=у, т. е. обе функции вычисляются в
единственной (безымянной) точке их области определения,
после чего сравниваются полученные Ь.елочисленные значе-
ния. Аналогично, если ах и ау - две переменные типа «целый
массив», их значения равны тогда и только тогда, когда они
как функции имеют одну и ту же область определения и в
каждой: точке этой области значения этих функций совпадают.
Чтобы можно было выполнить все эти сравнения, мы должны
Векторные переменные 133

ограничиться рассмотрением конечных областей определения.


Более того, эти области, кроме того, что они конечны, должны
быть тем или иным способом доступны для алгоритма срав-
нения значений векторных переменных ах и ау.
Из практических соображений я буду рассматривать толь-
ко области определения, состоящие из последовательных це-
лых чисел (когда область не пуста). Но и при таком ограни-
чении 'воаникает , по крайней мере две возможности.
В АЛГОЛе 60 область определения фиксируется при помощи
задания в описании нижней и верхней границ для значения
индекса, например boolean array А [1 : 1 О], В· {1-: 5]». Так как
тип определяет класс возможных значений для переменной.
этого типа, мы должны сделать вывод, что в приведенном
примере массивы А и В являются массивами разных типов:
А может иметь 1024 различных значения, а В- только 32.
В АЛГОЛе 60 мы имеем столько различных типов булевых
массивов, сколько имеется -граничных пар (а так как гранич-
ная пара может содержать выражения, то тип определяется,
вообще говоря, только при входе в блок). Кроме того, должны
существовать и другие средства, позволяющие 'получить не-
обходимые знания об области определения: не располагая до-
полнительной информацией, невозможно написать на языке
АЛГОЛ 60 внутренний блок, определяющий, равны ли два
глобальных булевых массива А и В!
В качестве альтернативы предлагается ввести только один
тип «целый массив» и только один тип «булев массив» и рас-
сматривать «область определения» как часть (аспект) любого
значения такого типа; тогда мы должны уметь извлекать этот
аспект из любого такого значения. Пусть ах - векторная пе-
ременная; в ее активной области действия· я предлагаю извле-
кать из ее значения границы области определения при помощи
двух целочисленных функций, обозначаемых соответственно
через «ах.ниг» и «ах.вег», полагая при этом, что функция
«ах (i) » определена ДЛЯ всех целых i, ТаКИХ, ЧТО
ах.ниг в; i <ах.вег
Кроме этих двух функций, я предлагаю ввести третью (за-
висимую) функцию «ах.обл», равную числу точек в области
определения переменной. Три функции связаны соотношением
ах.обл=ах.вег-ах.ниг+ 1 ~О
(Заметим, что даже пустая область определения, обл=О, за-
нимает свое место на числовой оси; ниг и вег по-прежнему оп-
ределены и удовлетворяют соотношению вег=Нtfг - 1.)
Мы применили здесь новое обозначение, использовав точку
~ «ах.ниг», «ах.вег» и «ах.обл». Имена, следующие за точкой,
134 Глава 11

называются именами, подчиненными типу той переменной, чье


имя предшествует точке. За точкой, следующей за именем пе-
ременной, могут следовать только имена, подчиненные типу
этой переменной, и их значение будет определяться по отно-
шению к этому типу.
Примечание 1. В Других контекстах, т. е. не ·вслед за точ-
кой.. те же имена могут использоваться в совершенно другом
значении. Мы могли бы ввести векторную переменную с име-
нем «обл» и обращаться в ее активной области действия к
функциям «обл.ниг», «обл.вег» и даже «Обл.обл»! Однако
подобные выкрутасы не рекомендуются, поэтому я старался
найти такие подчиненные имена, которые, хотя и имеют.неко-
торый мнемонический смысл, вряд ли могут быть придуманы
самим программистом. (Конец примечания 1.)
Примечание 2. .Еще одна причина, заставившая нас отка-
заться от обычного обозначения функции, например «обл (ах)>-
и т. д., и воспользоваться точкой, заключается в том, что если
мы при этом не введем различные наборы имен для этих функ-
ций применительно к булевым массивам и целым массивам
(что выглядело бы довольно неуклюже), мы будем вынужде-
ны ввести функции от .аргумента, который может быть вели-
чиной более чем оцного типа, чего я хотел бы по мере возмож-
ности избегать. (Конец примечания 2. ).
Примечание 3. Выражение «ax(i)» используется для обо-
значения значения функции в точке i. Необходимость в опре-
делении аргумента i возникает только тогда, когда нам тре-
буется значение «ax(.i) », при этом должно выполняться соот-
ношение
ах .ниг в; i < а х .вег
Исходя из обозначения функций с использованием точки.
мы могли бы рассматривать «ax(i) » как сокращение от
«ах.знач (i) », где «знач» - подчиненное имя, служащее ука-
занием, что нужно вычислить значение функции в точке, оп-
ределяемой значением последующего аргумента i. Для каж-
дого типа такое сокращение можно вводить только один раз]
Заметим, что тип «целый» тоже мог бы иметь подчиненное имя.
«знач», что позволило бы нам писать немного более явно
х : = у. знач, .
вместо обычной и несколько неаккуратной формы х: =ч- (Ко-
нец примечания 3.)
Векторные переменные 135

Введем для удобства две дополиитедьные функции; для


векторной переменной ах они определены при ах.обл>О. Это
функции
ах.низ, равная по определению ах(ах.ниz)
и
ах .верх, равная по определению ах(ах.вег)
Они обозначают значения функции соответственно в самой
нижней и самой верхней точках области определения. Они не
Представляют собой ничего действительно нового и определе-
ны в терминах уже известных понятий; при определении се-
мантики операций над значениями массивов не обязательно
явно упоминать об их влиянии на значения этих функций.
Как сказано выше, скалярную переменную можно рас-
сматривать как функцию (без аргумента), значение которой
можно изменить при помощи присваивания ей нового зна че-
ния: такое присванваниеполностъю уничтожает информацию,
представлявшую собой «ее старое значение». Нам нужны
также операции, позволяющие изменять значение векторной
переменной (без таких операций мы всегда имели бы вектор-
ную константу!), но присваивание переменной нового значе-
ния, т. е. значения, совершенно не связанного с ее старым
значением, уже не будет играть такую центральную роль.
И не- потому, что присваивание значения векторной перемен-
ной вызывает какие-то логические трудности - совсем наобо-
рот, мне хочется сказать - просто такая операция была бы
неэкономичной. Если область определения велика, объем ин-
формации, представляющей собой «значение векторной пере-
менной», может быть весьма значительным, и тогда ни копи-
рование, ни уничтожение информации такого объема нельзя
считать «хорошими» операциями.'
С другой стороны, для многих задач' программирования
'суть ·проблемы состоит в том, что значение массива формиру-
ется постепенно, т. е. за ряд шагов, каждый из которых можно
считать «хорошей» операцией, «хорошей» в том смысле, что
новое значение массива можно рассматривать как «приятную»
производную от его старого значения. Будут ли такие опера-
ции «хорошими» или «приятными», существенно зависит от
двух моментов: во-первых, отношение между старым и новым
значениями должно поддаваться математическому выраже-
нию, иначе эти операции становятся слишком громоздкими
для использования; во-вторых, их реализация не должна быть
слишком дорогой для того оборудования, которое мы собира-
-емся использовать для выполнения нашей программы. Вопрос
·О том, в какой мере мы хотим учитывать накладываемые обо-
рудованием ограничения, является вопросом политики, а не
136 Глава 11

научным вопросом, и поэтому я не чувствую себя обязанным


давать развернутое обоснование своим предложениям. Из со-
ображений удобства я позволю себе быть более либеральным,
чем многие программисты, в особенности те, кому приходится
ежедневно работать на машине, которая создавалась десять
или больше лет тому назад; с другой стороны, мне кажется,
что, я сознаю возможные технические последствия своих пред-
ложений в достаточной степени, так что они остаются если не
реалистическими, то по крайней мере' не совсем нереальными.
Наша первая операция над значением векторной перемен-
ной, скажем ах, не изменяет ни размера области определения,
ни набора значений функции, ни их порядка; она только сдви-
гает область определения на некоторое число позиций, ска-
жем k, вверх по числовой оси. (Если k<O, происходит сдвиг на
k позиций в другом направлении; если k =О, мы имеем тожде-
ственное преобразование, семантически эквивалентное опера-
.ции «пропустить».) Обозначим нашу операцию как
ах: сдвинуть (k)
Мы ввели здесь двоеточие «:». Нижняя точка, как обычно,
указывает на то, что следующее имя является подчиненным
для типа упомянутой слева переменной; верхняя точка служит
просто украшением (на Что нас вдохновила операция присва-
ивания «: = »), указывающим, что значение упомянутой слева
переменной подлежит переопределению.
Сразу же возникает вопрос, можем ли мы дать аксиомати-
ческое определение преобразователя предикатов wp («ах : сдви-
нуть (Е) », R). Ясно, что это должен быть преобразователь
предикатов, аналогичный преобразователю из аксиомы о при-
сваивании значения скалярной переменной, но более слож-
ный; то же справедливо и для всех других операций над зна-
чением массива, поскольку значение скалярной переменной
полностью определяется одним (элементарным) значением,
в то время как значение векторной переменной включает в се-
бя и саму область определения, и значение функции для всех
точек этой области. Так как значение векторной переменной
. ах полностью опред~ляется
значением ах.ниг
значением ах.обл и
<
значением ах (i) для ах. ниг i .<·ах. ниг+ах. обл
мы можем, по крайней мере в принципе, ограничиться рас-
смотрением постусловий R, ссылающихся на значение массива
только в терминах «ах.ниг», «ах.обл» и «ах(арг)», где арг-
любое целочисленное выражение. Для такого постусловия R
Векторные переменные 137

соответствующее слабейшее предусловие


wp (<~ах: сдвинуть (Е)>>, R)
выводится из R при помощи одновременной замены
1) всех вхождений ах.ниг.на (а~.ниг+ (Е)) и
2) всех вхождений - (под) выражений вида ах (арг) на
ах( (арг) - (Е)).
Замечание. Если Е само зависит от значения ах, самое бе-
зопасное - вычислить сначала для данного R, использовав
в нем совсем другое Имя, скажем К, wp·( «ах: сдвинуть(К)»,
R), а затем подставить вместо К фактическое значение R.
Мы уже сталкивались с таким же осложнением, когда
применяли аксиому о присваивании к операторам вида
х := x+f(x). (Конец замечания.)
Приведем несколько примеров. Пусть R- это отношение
ах.ниг = 1 О, тогда
wp (<<ах : сдвинуть (ах. нигл», R) =(ах. ниг +ах. ниг = 10)
=(ах.ниz=5)
Пусть R- это (Ai: О~i<ах.обл: ах(ах.ниг+i) =i); тогда
wpJ <<ах: сдвинуть (7)>>, R)= (А i: O~i <ах. обл: ах (ах. ниг+
+1+i-7)=i)=R · .
Можно записать это слабейшее предусловие по-другому:
wp (<<ах: сдвинуть (Е)>>, R)=Rax'-ax 1

(т. е. копия R, в которой каждое вхождение ах заменено на


ах'), 'где
ах'. ниг =ах. ниг Е +
ах' .обл=ах.обл
ах'(арz)=ах(арг-Е) для любого значения арг
Из трех этих определений следует, что
ах' .веz=ах.вег+Е
ах' .ниэьеах гниз
ах' .верх=ах.верх
Замечание. Из этих равенств следует, что, если правая
часть не определена, левая часть также не определена. (Конец
замечаниял
При определении дальнейших операций мы будем исполь-
зовать этот последний прием: он позволяет описать в более
138 Глава 11

ясной форме, как конечное значение ах' зависит от начального


значения ах.
Следующие операции расширяют область определения на
одну точку либо с верхнего, либо с нижнего ее· конца. Значе-
ние функции в новой точке задается как параметр, тип кото-
рого должен быть так называемым «основным типом» масси-
ва, т. е. булевым для булева массива и т. д. Операции задают-
ся в виде
ах: вверх (х) или ах: вниз (х)
Семантическое определение вверх задается соотношением
wp (<<ах: вверх (х)>>, R)=Rax'-+ax
где
ах' .ниг=ах.ниг
ах' .вег=ах.вег+ 1
ах' .обл=ах.обл+ 1
ах'(арг)=х для арг=ах.вег+ 1
=ах(арг) для арг :;6ах.вег+ 1
Семантическое определение вниз задается в виде
wp (<<ах: вниз (х)>>, R)=Rax'-.ax
где
ах'. ниг =ах. ниг - 1
ах' .вег =ах. вег
ах'. обл=ах. обл+ 1
ах' (apz)=x для арг=ах.ниг-1
=ах (арг) для арг =1= ах. ниг - 1
Замечание. Наше предыдущее примечание о том, что и
пустая область определения имеет свое место на числовой оси,
должно служить гарантией, что операции расширения вверх
и вниз определены и для векторной переменной, для которой
обл=О. (Конец замечания.)
Следующие две операции удаляют точку из области опре-
деления либо с верхнего, либо с нижнего ее конца. Они опре-
делены только в том случае, когда перед их выполнением
обл>О; когда они применяются к массиву, для которого
обл=О, это вызывает отказ. Эти операции уничтожают ин-
формацию в том смысле, что одно из значений функции утра-
чивается.
Ьекхорные переменные 139

Семантическое определение сверху задается соотношением


wp (<<ах: сверхи»; R)=(ax. обл> О and Rax'-ax)
где
ах' .ниг=ахrниг
ах'. вег =ах. вег- 1
ах' .обл=ах.обл-1
ах' (арг) н~ определено для арг=ах.вег
=ах(арг) для а рг =1= ах. вег
Семантическое определение снизу Задается соотношением
wp(<<ax: снизу», R)=(ах.обЛ >О and Rax'_,.ax)
где
ах'<ниг =ах.ниг+ 1
ах' .вег =ах.веz
ах'.обл =ах.обл-1
ах' (арг) не определено для арг=ах.ниг
=ах(арг) для арг э- ах.ниг
Введем для удобства еще две операции, семантика кото-
рых может быть выражена в терминах уже введенных функ-
ций и операций. Это операции
х, ах: векон, семантически эквивалентная <<Х: =ах. верх;
ах .сверху»
и
х, ах: нихон, семантически эквивалентная <<Х: = ах. низ;
ах: снизу»
Обозначение этих операций напоминает обозначение опе-
рации одновременного присваивания; имя, следующее за «:»,
должно быть подчиненным именем для типа переменной, имя
которой непосредственно предшествует«:». Очевидно, что дру-
гая. переменная х должна иметь основной тип векторной пе-
ременной ах.
Все описанные выше операции изменяют область определе-
ния функции, либо только смещая ее вдоль числовой оси, ли-
бо изменяя при этом и ее размер.,Будут введены еще две опе-
рации, которые сохраняют область определения и только воз-
действуют на одно или двазначения функции.
Очень важной является операция, которая не порождает
новых значений функции, а только переупорядочивает имею-
140 Глава 11

щиеся значения. Операция задается в виде


ах: переставить (i, j)
Если i и j не лежат оба в области определения функции,
операция вызовет отказ. Семантика операции задается соот-
ношением
wp (<<ах: переставить (i, Г)», R)=(ах.~ниг .<,. i .<,. ах.вег and
ах. ниг .<,. j .<,. ах. вег and
Rax -+ax) 1

где
ах' .ниг =ах.ниг
ах' .вег =ах.вег
ах' .обл =ах.обл
ах' (арг)=ах(j) для арг=i
·ax(i)- для арг=»]
=ах (ар?-) для арг =F i and арг =F j
Замечание. Не требуется, чтобы перед выполнением опера-
ции i=I= j: если i= j, значение векторной переменной останется
прежним. (Конец замечаниял
Наша последняя операция позволяет изменить одно зна-
чение функции; она задается в виде
ах: из.н(i, х)
Операция приводит к отказу, если i не лежит в области опре-
деления; второй параметр х должен иметь основной тип век-
торной переменной. Семантика операции задается соотноше-
нием
wp (<<ах: азм (i, .е)», R)= (ах. ниг .<,. i .<,.ах. вег and
Rax'-+ax)

где
ах'. ниг =ах. ниг
ах'. вег =ах. вег
ах' .обл =ах.обл
ах' (арг)=х для арг=i
=ах(арг) для арг=/; i
Операция, обозначенная выше как «ах : изм(i, х) », семан-
тически эквивалентна операции, известной тем, кто програм-
мирует на ФОРТРАНе и АЛГОЛе 60, как «присваивание зна-
Векторные переменные · 141

чения переменной с индексом». (Они соответственно написали


бы в этом случае «АХ(/) =Х» и «ax{i]: = Х».) Я ввел эту опе-
рацию в форме «ах: изм (i, ~) », чтобы подчеркнуть, что такая
операция оказывает воздействие на массив ах в целом: если
две функции с одной и той же областью определения отлича-
ются хотя бы в одной точке этой области, то такие функции
различны. Однако «официальное» - или, если вам так больше
нравится, «пуританское» - обозначение «ах: иэм (i, х) », даже
на мой взгляд, слишком громоздко и слишком непривычно,
'поэтому я предлагаю (и у меня есть свои слабости!) исполь-
зовать вместо него обозначение
ах: (i)=x
которое и короче, и похоже на столь знакомый нам оператор
присваивания, и своим началом «ах: » говорит о том, что эта
операция направлена на векторную переменную ах. (Решение
использовать форму «ах: {i) =Х» не очень отличается от реше-
ния использовать форму «ах ( i) » вместо более помпезной фор-
мы «ах.энач (i) ».)
Ни одна из предыдущих. операций не может быть исполь-
зована для инициализации. Эти операции -могут только изме-
нять значение массива при условии, что он уже обладает
каким-то значением; они могут встречаться только в активной
области действия векторной переменной. Мы еще не ввели
присваивание
ах:= Ьх
- конструкцию, которая могла бы выполнить инициализацию.
Я, однако, никак не могу на это решиться, потому что во всей
своей общности «присваивание значения» подразумевает обыч-
но «копирование значения», и если область определения функ-
ции Ьх велика, то при существующей технологии такую опера-
цию присваивания нельзя считать «хорошей». Не могу ска-
зать, что я абсолютно несклонен к введению «неприятных»
операций, но если уж я это делаю, то мне бы не хотелось. что-
бы на бумаге они выглядели как невинные операции. Язык
программирования, в котором операция «Х : = у» считается
«хорошей». а операция «ах : =Ьх» расценивается как «непри-
ятная», ввел бы нас в заблуждение; по крайней мере он ввел
бы в заблуждение меня. Выход из этой дилеммы состоит в до-
пущении, что в правой части такого оператора присваивания
значения векторной переменной можно только перечислять
константы, например в форме
(<целое>{, <значение основного типа э-])
142 Глава 11

так что, например, в результате присваивания


Ьх : = (5, истина, истина, ложь, истина)
мы получим
Ьх.ниг=5 Ьх (5) =истина
ох.вег =8 ох (6)=истина
Ьх.обл=4 Ьх (1)=ложь
Ьх (8) =истина
Вследствие такого ограничения использование при при-
сваивании или инициализации значения с областью определе-
ния большого размера не может пройти незамеченным.
Я предполагаю, что в большинстве случаев при инициализа-
ции будут использоваться значения, для которых обл=О.
Теперь несколько заключительных замечаний.
Начнем с вопроса об .экономичности операций. Я исхожу
из того, что выполнение всех упомянутых в этой главе опера-
ций стоит примерно одинаково. Нужно высказать некоторые
предположения о природе этих затрат, ибо без этого задача
программирования теряет смысл. Например, вместо
ах: (5)=7
мы могли бы написать внутренний блок
begin glovar ах; privar ох;
if ах. ниг ~ 5 and 5 '" ах.вег ~
Ьх vir цел array : =(0);
do ах-вег -=1= 5~Ьх: вверх'ах.ве пху; ах: сверху od;
ах: сверху; ах: вверх(7);
do Ьх.обл-=t=О~ах~вверх(Ьх.верх); ох л сверху od
fi
end
но я бы забраковал этот внутренний блок, так как это худшая
замена, и не только из-за длинного текста, но и из-за неэффек-
тивности. Я даже не стану рассматривать «ах : (5) =7» как со-
кращенную запись этого внутреннего блока.
Я предполагаю, в частности, что стоимость выполнения
всех операций, за исключением, может быть, присваивания пе-
речисленных значений, не зависит от значений аргументов.
входящих в операцию: стоимость выполнения ах: сдвинуть (k)
Векторные переменные 143

не будет зависеть от значения k, стоимость выполнения ах: пе-


реставить (i, j) не будет зависеть от значений i и j и т. д. При
современной технологии такие предположения не так уж не-
реальны.
Принимая во внимание соображения эффективности, мне
следует дать обоснование моей готовности вводить дополни-
тельные избыточные имена; мы могли бы ограничиться функ-
циями ах.ниг и ах.обл, потому что всякий раз, когда нам пона-
добилась бы функция ах.вег, мы мог ли бы вместо нее писать
ах .ниz+ах .обл-1
но тогда эффективное использование ах.вег обошлось бы
«вдвое дороже» эффективного использования ах.ниг, и то, что
мы осознаем это, легко может .повлиять на наш способ мыш-
ления (и, что гораздо хуже, действительно, влияет).
Я говорил об одинаковых затратах. При этом я имею в
виду, что они одинаковы по сравнению с :Затра1'ами на опера-
ции, которые мы считаем примитивными. Если бы операции
над массивами обходились дороже, чем другие операции, мы
.могли бы, например, заменить
ах: переставить'], j)
на
f .i =l=j ~ах: пе р ест авитьй, j) Oi=j -в-пропустить fi
и сразу перед нами возникла бы необходимость знать точное
соотношение затрат и очень хорошо оценить 'вероятность слу-
чая «i= j», чтобы мы могли решить, действительно ли мы
получим повышение эффективности, если заменим ах: пере-
ставить(i, j) конструкцией выбора. Я знаю математиков, ко-
торые упиваются такими задачами оптимизации, полагая
иногда, что эти задачи занимают центральное место при со-
ставлении программ для вычислительных машин. Я с радо-
стью предоставляю им решение этих задач, раз уж они полу-
чают от этого такое удовольствие; операции, которые мы пред-
почитаем считать примитивными, не должны приводить нас
к таким конфликтам. Я полагаю, у нас есть более важные
проблемы, заслуживающие нашего внимания.
Последнее замечание о реализации. При инициализации
векторной переменной ах в принципе можно указывать неко-
торые ограничения: ограничение снизу для ах.ниг, или огра-
ничение сверху для ах.вег, или оба этих ограничения, или,
может быть, только ограничение сверху для ах.обл. Если та-
кие «подсказки компилятору» включаются в программу, то
становится возможным использовать все богатство традицион-
144 Глава 11

ных приемов управления памятью. Я предпочитаю, однако, не


считать такие «подсказки компилятору» частью программы.
Они могут только удешевить реализацию (и то для какого-то
оборудования!); они разрешают (но не заставляют!) при кон-
кретных реализациях выдавать· отказ, когда происходит на-
рушение указанных ограничений.
12 ТЕОРЕМА О ЛИНЕЙНОМ ПРОСМОТРЕ

Обозначим через В булеву функцию, определенную на мно-


жестве целых чисел, и рассмотрим следующее начало блока:
begin privar i;
i vir цел: =0;
do В (i) -:,..i : = i +1 od;

Для этой конструкции повторения мы можем сформули-


ровать инвариантное отношение
р (i): (Aj:O~j<i:B(j))
Доказательство. Так как Р(О) истинно, P(i) становится
истинным при инициализации. Далее,
(P(i) and B(i))=P(i+ 1)
=wp (<<i := i+ 1>>, P(i)) Ч.Т.Д.
Без дополнительных знаний о· булевой функции В мы не
сможем доказать, что конструкция повторения завершится.
Если, однако,
(Е j : j>О : поп В (}))
то, предположив, что конструкция не завершится, мы придем
к обычному противоречию: достаточно существования по край-
ней мере одного значения j~O, такого, что
non В(})
истинно, чтобы гаран.тировать завершение конструкции. Мы
знаем, однако, что по завершении
Р (i) апd -поп В (i) =
(Aj: О~ j <i: В (j)) and поп B(i)
146 ·глава 12

т. е. i является минимальным значением ~О·, при котором


поп В (i) истинно. Иначе говоря, когда мы ищем минимальное
значение (не меньшее, чем некоторая нижняя граница), удов-
летворяющее некоторому критерию, наша программа просмат-
ривает значения (начиная с нижней границы) в порядке их
возрастания. Такой просмотр превращает первое встретившее-
ся значение, удовлетворяющее данному критерию, в наимень-
шее из существующих значений, удовлетворяющих данному
критерию. Аналогично, когда мы ищем максимальное значе-
ние, мы ведем просмотр в порядке убывания значений.
Очень часто эти два оператора имеют форму
х:=хнуль;
do В (х)-+х := F (х) od
Такая программа осуществляет поиск среди последова-
тельности значений, определенной как
х0=хнуль
для i>O:
X1=F (Х1-1)
такого значения х, с минимальным значением i(~O), при ко-
тором поп В ('):i) истинно. (Более формальные доказательства
мы оставляем в качестве упражнения трудолюбивому чита-
телю, имеющему склонность к таким доказательствам.)
На результаты, полученные в этой главе, ссылаются как
на «теорему о линейном просмотре». В следующей главе мы
будем их использовать в наших рассуждениях, направленных
на решение реальных задач. Оказывается, что, несмотря на
свою простоту, теорема о линейном просмотре имеет большое
эвристическое значение.
13 ЗАДАЧА О СЛЕДУЮЩЕИ ПЕРЕСТАНОВКЕ

Нам требуется написать внутренний блок, выполняющий


действие над глобальной целой векторной переменной, именуе-
мой с, при
с. ниг = 1 и с. вег = п
для некоторого постоянного значения п (п> 1). Кроме того,
известно, что упорядоченная последовательность значений
c(l), с(2), ..., с(п) является некоторой перестановкой значений
от 1 до п, но не последней в алфавитном порядке: п, п'- 1, ... , 1.
Внутренний блок должен преобразовать последовательность
c(l), с(2), ... , с(п) в последовательность, являющуюся ее не-
посредственным ·следующим по алфавиту преемником. (По
поводу понятия «алфавитный порядок» обратитесь к послед-
нему примеру в главе «Формальное рассмотрение нескольких
небольших примеров».) Например, при n=9 . последователь-
ность
1 4 6 2 9 5.8 7 3
должна быть преобразована в последовательность
1 4 6 2 9 7 3 5 8
· Как видно из приведенного примера, ряд значений функ-
ции на нижнем конце может остаться неизмененным:. Выпол-
няемое преобразование сводится к перестановке значений на
верхнем конце, и первое, что мы должны сделать,- это найти
место раздела, т. е. определить значение i, такое, что
с ( k) сохраняется для 1 k i < <
с f k) изменяется для k=i
Значение i определяется как максимальное значение
i(<n}, такое, что
148 Глава 13

(Это значение не может быть большим, потому что тогда мы


были бы вынуждены переставлять значения в, первоначально
монотонно убывающей последовательности, а такая операция
не может привести к получению последовательности, стоящей
выше в алфавитном порядке; оно также не должно быть мень-
шим, так как в этом случае мы никогда бы не получили непо-
средственного следующего по алфавиту преемника.)
Замечание. Тот факт, что исходная последовательность не
является последней в алфавитном порядке, гарантирует нам
существование i(O<i<n), такого, чго c(i)<c(i+l). (Конец
замечания.)
После того как i найдено, мы должны отыскать в «хвосте»,
т. е. среди значений с (j) при i + 1 ~j ~ п, новое значение с ( i).
Так как мы ищем непосредственного преемника, мы должны
найти такое значение j в интервале i + 1 ~j ~ п, что с (j) есть
наименьшее значение, удовлетворяющее условию
с (j) >с (i)
После того как j найдено, мы можем позаботиться о том,
чтобы с (i) получило свое новое значение, для чего используем
операцию «С: переставить (i, j) ». Дополнительным преимуще-
ством этой операции является то, что после ее выполнения
вся последовательность продолжает оставаться перестановкой
чисел от 1 до п; заключает всю цепочку действий переупоря-
дочивание значений в хвосте последовательности, чтобы они
монотонно возрастали. Общая структура нашей программы
представляется теперь в таком виде:
определение i;
определение };
с: переставить (i, j);
сортировка хвоста
(В нашем Примере i=6, j=8 и окончательный результат будет
достигаться через промежуточную последовательность 1 4 6
z 9 7 8 5 3.)
При определении i мы ищем максимальное значение i; тео-
рема о линейном просмотре говорит нам, что мы должны про-
сматривать потенциально возможные значения i в порядке
убывания.
При определении j мы ищем минимальное значение с (j);
теорема о линейном просмотре говорит нам, что мы должны
исследовать значения с (j) в порядке их возрастания. Посколь-
ку хвост представляет собой монотонно убывающую функцию
Задача о следующей перестановке 149

(из-за способа определения i), из этого следует, что мы долж-


ны будем вести просмотр значений c(j) в порядке убывания j.
Операция «с: переставить (i, j) » не нарушает монотонно-
сти значений функции в хвосте (докажите это!), и поэтому
«сортировка хвоста» сводится к расположению значений в
обратном порядке. (При выполнении сортировки наша про-
грамма пользуется переменными i и j, которые уже сослужи-
ли свою службу. Заметим, что способ сортировки одинаково
применим и для четного, и для нечетного числа значений в
хвосте.)
begin giovar r; privar i, };
+
i vir цел:= с. вег - 1; do с (i) >-с (i 1); i: = i -1 .оё;
} vir цел:= с. вег: do с(})<: с (i)- j/. =} - 1 od;
г : переставить (i, j);
+
i : = i 1; j: = с. вег;
< +
do i j - с: переставить (i, j); i, j: ==i 1, j -1 od
end
Примечание 1. Мы нигде не использовали тот факт, что все
значения с ( 1), с (2), ..., с (п) различны. Следовательно, можно
было бы предположить, что наша программа будет правильно
преобразовывать исходную последовательность в следую1;1-1-ую
по алфавиту последовательность и в том случае, когда неко-
торые значения входят в последовательность более чем один
раз. И это действительно так, поскольку при определении i и-
j мы формировали наши предохранители путем «механическо-
го» отрицания требуемых условий с (i) <с (i+ 1) и с (j) >с (i)
соответственно. Однажды в университете я продемонстриро-
вал эту программ.у аудитории, которая наотрез отказалась
принять мои предохранители с включенным в них равенством.
Слушатели настаивали на том, чтобы я писал
do c(i)>c(i+l)-. ...
и
do с (i) < с ( i) -- ...
если известно, что все значения в последовательности различ-
ны. Они были непоколебимы в своем убеждении, что проверка
также и равенства обойдется значительно дороже . .Я уступил,
но при этом мне было интересно, каким же оборудованием
должен располагать университет, чтобы у студентов возникли
такие превратные представления. (Конец примечания 1.)
Примечание 2. Программисты, не знакомые с теоремой о
линейном просм~тре, часто совершают ошибку, представляя
150 Глава 13

«определение j» в следующем виде:


j vir цел: =i+ +
1; do с (j +
1) >с (i) - j: = j 1 od
Они утверждают, что эта программа будет присваивать j зна-
чение j + 1 только после того, как будет установлено, что это
новое значение допустимо с точки зрения с (j) >с (i). Проана-
лизируйте, почему их версия ·«определения j» может работать
неправильно. (Конец примечания 2.)
Примечание 3. Однажды мне пришлось выполнять непри-
ятную обязанность и экзаменовать студента, который, как мне
было известно, обладал весьма ограниченными изобретатель-
ными способностями. Так как он был знаком с описанной
выше программой, я попросил его написать программу, преоб-
разующую исходную последовательность, про которую изве-
стно, что она не. является первой в алфавитном порядке, в по-
следовательность, непосредственно предшествующую исходной
в алфавитном порядке. Я надеюсь, что время, которое вы
потратите на это упражнение, будет значительно меньше часа,
который потребовался моему студенту. (Конец примечания 3.)
Примечание 4. Эта программа мне особенно близка, пото-
му что я помню, как я бился над ней в мои студенческие годы,
в «каменном веке» программирования, когда программы пи-
сались в коде машины (даже без индекс-регистров: в добрых
старых традициях фон Неймана программы должны были
изменять свои собственные команды, хранящиеся в памяти
машины!). И я помню Tal)JRe, что после тщетной борьбы, про-
должавшейся более двух часов, я сдался 1 И это в то время,
когда я уже был опытным программистом! Несколько лет
назад; когда мне понадобился пример для моей лекции, я
вдруг вспомнил' эту старую задачу и решил ее без всяких за-
труднений (и мог даже объяснить ее на следующее утро до-
вольно неопытной аудитории за двадцать минут). То обстоя-
тельство, что теперь можно за · двадцать минут объяснить
неопытной аудитории решение задачи, которое двадцать лет
назад не мог найти опытный программист, свидетельствует о
поразительных успехах искусства программирования (до та-
кой степени, что теперь даже трудно поверить, что тогда я не
мог решить эту задачу!). (Конец примечания 4.)
Примечание 5. Эквивалентом нашего критерия для i ( «мак-
симальное значение i (<п), такое, что c(i)<c(i+l)») явля-
ется критерий «максимальное значение i ( <n), такое, что
(Е j: i<j~n: c(i) <c(j) )».Однако последний критерий более
труден для применения, и тот, кто начнет с него, придет к
первому критерию (тем или иным путем). (Конец примеча-
ния 5.)
14 ЗАДАЧА О ГОЛЛАНДСКОМ НАЦИОНАЛЬНОМ
'ФЛАГЕ

Имеется ряд сосудов, перенумерованных от 1 до N. Зада-


ны два условия Р 1 и Р2.
PI: в каждом сосуде лежит один камень.
Р2: каждый камень либо красный, либо белый, либо си-
ний.
Перед сосудами расположена мини-машина, которую нуж-
но запрограммировать таким образом, чтобы она переложила
(если нужно) камни в порядке цветов голландского нацио-
нального флага, т. е. в сосудах в порядке возрастания их но-
меров должны располагаться сначала красные камни, затем
белые и, наконец, синие. Чтобы иметь возможность выполнить
эту программу, мини-машина должна быть снабжена одной.
командой вывода, которая может воздействовать на располо-
жение камней, а именно
«сосуд: поменять (i, j)>> для 1-<: с«; N и 1-<: Г«: N:
для i = }: камни остаются на прежних местах,
для i =1= j: две управляемые мини-машиной «руки» выни-
мают камни из сосудов с номерами i и j со-
ответственно и затем меняют их местами. (Эта
операция сохраняет отношения Pl и Р2 инва-
риантнымн.)
и одной командой ввода, которая может проверять цвет кам-
ня, а именно
«сосуд (i)>> для 1-<: i-<: N:
когда программа задает вычисление этой функции
типа «цвет», подвижный «глаз» направляется на
сосуд с номером i и передает мини-машине в ка-
честве значения функции цвет (т. е. красный, бе-
лый или синий) камня, лежащего в сосу де, содер-
жимое которого только что проверял «глаз».
Константа N -'это глобальная константа из того контекс-
та, в который наша программа должна входить как внутрен-
152 Глава 14

ний блок. Наша программа, однако, должна отвечать трем


специальным требованиям:
1. Она должна уметь справляться со всеми возможными
формами «вырождения», т. е. отсутствия какого-то цвета: со-
суды могут быть наполнены камнями только двух цветов, или
только одного цвета, или вообще никакого цвета (если N =О).
2. Мини-машина обладает очень малой памятью по срав-
нению с теми значениями N, с которыми она должна уметь
справляться, поэтому мы не разрешаем вводить какие бы то
ни было массивы, а только' фиксированное число переменных
целого типа и/или переменных типа «цвет». (Под переменными
целого типа мы здесь будем понимать переменные, которые не
могут принимать много больше, чем N различных значений.)
З_Программа может направлять «глаз» на каждый камень
не более одного раза (предполагается, что на операцию ввода
затрачивается столько времени, что смотреть дважды на один
и тот же камень означает вызвать недопустимую трату вре-
мени).
Кроме того, среди программ одинаковой степени сложности
предпочтительней та, при выполнении которой происходит (в
среднем) меньше перекладываний камней.
Хотя все наши камни могут быть только трех различных
цветов, тот факт, что наш «глаз» может проверять только один
камень за один «взглядэ, в сочетании с требованием (3) при-
водит к тому, что в процессе переупорядочивания камней мы
должны раэличать камни четырех различннх категорий, а
именно: «оказавшийся красным» (ОК), «оказавшийся белым»
(ОБ), «оказавшийся синим» (ОС) и «еще не проверенный»
(Х) ..Требование (2) исключает возможность того, .что камни,
относящиеся к этим различным категориям, могут лежать
вперемежку: мы не можем хранить в памяти мини-машины
информацию о том, «что есть что». Единственный выход-
разделить все сосуды на фиксированное число (может быть,
пустых) зон, состоящих из последовательно перенумерован-
ных сосудов, причем каждая зона отводится для камней опре-
деленной категории. Поскольку таких зон может быть мини-
мум четыре, попробуем для начала ввести ровно четыре зоны.
Но в каком порядке? Я обнаружил, что многие программисты
склонны, не особенно задумываясь, принять порядок «0К»,
«ОБ», «ОС», '<Х», хотя такое решение явно опрометчиво. Как
только кому-нибудь покажется привлекательным расположить
зону «0К» в нижнем конце, соображения симметрии сделают
таким же привлекательным расположение зоны «ОС» в верх-
нем конце. Придерживаясь нашего прежнего решения ввести
ровно четыре зоны, мы, таким образом, приходим к ааключе-
Задача о голландском национальном флаге 153

нию, что зоны «ОБ» и «Х» должны находиться в середине в


том или ином порядке (убедитесь, что в этом случае порядок
несуществен!), например,
<<0К>>, <<Х>>, «ОБ>>, <<ОС>>
Раз мы выбрали описанную выше «общую ситуацию», на-
ша задача в сущности решена, так как мы имеем общую си-
туацию, для которой как начальное состояние (все сосуды в
зоне «Х»), так и конечное состояние (зона «Х» пуста) являют-
ся особыми случаями! Мы можем принять эти условия, и тог-
да конструкция повторения должна уменьшать размер зоны
«Х», сохраняя при этом общую ситуацию. В нашей мини-ма-
шине нам понадобятся три переменные 'целого типа, например
«К», «6» и «С», по которым можно будет следить за границами
зон:
<
1 i <·к: i-й сосуд находится в зоне <<0К>> (число сосудов
к-1 >-О)
к< < i б: i-й сосуд находится в зоне <<Х>> (число сосудов
6-к+ 1 >-О)
б < i <с: i-й сосуд находится в зоне <<06>> (число сосу дов
с-б~ОJ
с< i-< N: i-й' сосуд находится в зоне <<ОС» (число сосу дов
N-c>O)
Обеспечить истинность отношения Р, которое должно со-
храняться инвариантным, значит придать трем этим перемен-
ным такие начальные 'значения, чтобы «все сосуды были в
зоне «Х>> », и наша программа могла бы иметь следующую
структуру:
begin glovar сосуд; glocon N; privar к, б, с;
к vir цел, б vir цел, с vlr цел: = 1, N, N;
{ Р стало истинным}
>-
do б к-+ «уменьшить число сосудов в
зоне <<Х>> при инвариантности _Р>>
od
end
Сразу же возникает вопрос: на сколько охраняемый опера-
тор должен уменьшать число сосудов в зоне «Х»? Есть три
довода - и, как заме-гит читатель, довольно общего характе-
154 Глава 14

ра - в защиту того, чтобы сначала проверить возможность


«уменьшить число сосудов в зоне «Х» на 1 ». Эти доводы та-
ковы:
1. Уменьшения на 1 достаточно.
2. Так как мы выбрали в качестве. предохранителя условие
«б~к», мы можем гарантировать, что в зоне «Х» находится
по крайней мере один сосуд. Чтобы гарантирова~ь наличие
двух сосудов, нам понадобился бы предохранитель «б>ю>.
3. Проверка одного камня даст нам три различных резуль-
тата, проверяя два камня, мы получим уже девять различных
случаев; такое _размножение числа подлежащих рассмотрению
случаев следует расценивать в принципе как высокую плату
за тот выигрыш, который мы можем получить, уменьшая зону
«Х» больше чем на 1.
Следующий вопрос: на какой из еще не проверенных кам-
ней мы будем смотреть? Вопрос не такой уж праздный, по-
скольку за это время в расположении зон «ОК», «Х», «ОБ»,
«ОС» возникла асимметрия. Ни один опытный программист
не станет проверять произвольный камень, а обратится к кам-
ням в нижнем или верхнем концах. При том, что все три цвета
равновероятны, проверяя камень в к-м сосуде, мы получим
число перекладываний, равное (О+ 1 + 2)/3 = 1, проверяя же
камень в 6-м сосуде; мы получим только ( 1 + О + 1) /3 = 2/3,
что и определяет наш выбор. Таким образом, мы приходим к
следующей программе:
begin glovar сосуд; glocon н.prlvar 1', б, с;
1' vir цел, б vlr цел, с vlr цел: = 1, N, N;
do б"';;::-1'-+-
begin glovar сосуд, 1', б, с; pricon цв·;
цв vir цвет:= сосуg(б);
if ЦВ=1'расный-ч сосуд : поменять (к, 6); к:= к+ 1
О цв=белый 7 б := б-1
О цвьесиний -.сосуд .поменять (б, г]; б, с:= б-1,
с-1
н
end
od
end
Замечание. Программа разумна в том смысле, что она при-
ведет к отказу, если ей придется работать с неправильными
данными, например если один из камней окажется зеленым.
(Конец замечания.)
Задача о голландском национальном флаге 155

В том случае, когда все камни красные и не требуется


никаких перекладываний, наша программа потребует выпол-
нения N перекладываний, и, как сознательные программисты,
мы должны провести исследования, чтобы понять, насколько
'бодее сложным окажется более утонченное решение: может
быть, отклонив его раньше, мы проявили трусость. (В качест-
ве общего подхода я бы рекомендовал не браться за более
утонченное решение, пока не испробовано более простое; та-
кой подход позволяет нам получить не тоЛ.Ь)Ш работающую
программу, но и довольно недорогое указание того, с чем
должна конкурировать такая утонченная программа.) Я всег-
да считал приведенное выше решение абсолютно удовлетвори-
тельным и до сих пор никогда не рассматривал более слож-
ного решения. Возьмемся же за него теперь!
Можно расширить проверку одного камня до «проверки
одного или двух» или до «проверки стольких камней, сколько
нам будет удобно расположить». Для случая «все камни крас-
ные» последний вариант оказывается в чем-то предпочтитель-
нее. Прежде чем проверять непроверенный камень в верхнем
конце, мы могли бы попытаться передвинуть границу, задан-
ную к, насколько возможно ближе к верхнему концу, не
перекладывая при этом камни, потому что было бы обидно
заменять один красный камень, находящийся в совершенно
правильной позиции, другим красным камнем. Внешняя конст-
рукция повторения могла бы тогда иметь следующее начало:
do б-:;:,.к-
begin glovar сосуд, к, б, с; privar цвк;
цок vir цвет : =сосуд (к):
do цвк=красный and к «; 6-к: =к+ 1; цвк :=сосуд (к) od;
. Внутренняя конструкция повторения прекращает работу
либо если проверены все камни (к=б), либо если мы наткну-
лись на некрасный камень. Случай к=6, где цвк может иметь
одно из трех различных значений, сводится к конструкции вы-
бора предыдущей программы, правда, благодаря тому, что
оператор «Сосуд: поменять (к, 6) » можно опустить, поскольку
к и 6 равны. Случай к-с.б означает, что цвкье красный; теперь
мы просто должны проверить другой камень, иначе наше ре-
шение сведется к тому решению, при котором всегда проверя-
ется камень в нижнем конце зоны «Х», а мы знаем, что такое
решение дает в среднем больше перекладываний, чем наш
первый вариант. Поскольку к-сб, другой непроверенный ка-
мень действительно есть, и первым кандидатом на проверку
является, очевидно, камень в б"м сосуде.
И снова при цвк+белый кажется обидным менять местами
камень в х-м сосуде и белый камень в 6-м сосуде, поэтому для
156 Глава 14

случая к-сб предпочтительнее ввести новый внутренний блок


begin glovar сосуд, к, б, с; glocon цвк; privar цвб;
цвб vir цвет:= сосуд (б);
do цвб=белый and б>к+ 1-+б:= б-1; цвб:= сосуд (б)
od;
Теперь мы имеем два возможных значения для цвк - бе-
лый или синий, три возможных значения для цвб - красный,
белый или синий и набор непроверенных камней, может быть,
пустой. На какое-то мгновение я испугался, что мне придется
разбирать 12 различных случаев! Но после длительного обду-
мывания (и после одного фальстарта) я понял, что сначала
надо поместить подлежащий сейчас проверке камень в б'-й
сосуд и проследить за тем, чтобы во всех трех случаях камень,
первоначально находившийся в к-м сосуде, остался в (новом)
б-м сосуде. Тогда можно объединить три возможности и со-
ставить отдельную часть программы, выполняющую единооб-
разные действия над вторым камнем, цвет которого определя-
ется переменной цвк.
ifцвб=красный-сосуд:поwенять(к, б); к: =к+l
Оцвб=белый -б==б-1
Оцвб=синий - сосуд: поменять (б, с); б, с: =б-1,
с - 1; сосуд: поменять (к, б)
fi;
jf цвк=« белый - б: = б- 1
О цвк=синий- сосуд: поменять (б, с); б, с :=б-1,
с-1
fi
Замечание 1. Мы получили приятный результат, состоящий
в том, что соединени:е двух конструкций выбора обеспечивае·г
работу ·программы для 2ХЗ комбинаций цветов! (Конец за-
мечания 1.)
Замечание 2. Убедитесь, что случай цвб=белый был разоб-
ран правильно, поскольку в этом случае первоначально
6 =к+ 1. (Конец замечания 2.)
Примечание. Из педагогических соображений я немного
жалею, что случай «два проверенных камня лежат не в тех
сосудах» не оказался более сложным; может быть, я должен
был подавить искушение выполнить даже эту кропотливую
работу как можно лучше. (Конец примечания.)
Я предоставляю читателю довести до конца нашу вторую
программу, если ему еще не расхотелось (я надеюсь, что рас-
Задача о голландском национальном флаге 157

хотелось!); думаю, что мораль уже ясна. Я довел анализ воз-


можных случаев до этой стадии, чтобы стало ясно, что такого
анализа, известного также под названием «комбинаторный
взрыв», нужно всегда избегать, как чумы. Он удлиняет текст
программы и может легко привести к снижению эффективно-
сти! Такой анализ всегда ухудшает надежность программы,
потому что взваливает на плечи бедного программиста такую
гяжестъ, которой тот обычно не выдерживает (не в последнюю
очередь из-за того, что работа становится очень нудной). Чи-
татель может рассматривать вышесказанное как намек руко-
водству на то, что не стоит оценивать труд программистов по
числу строк текста, которые они производят за месяц; я бы
скорее ~аставил их оплачивать из своего кармана использован-
ные ими перфокарты.
Я часто разбирал эту задачу со студентами, располагая зо-
ны в порядке «0К», «Х», «ОБ», «ОС» или в порядке «ОК»,
«ОБ», «Х», «ОС». Когда я спрашивал, какой камень нужно
проверять, они всегда предлагали «самый левый». У меня воз-
никла мысль, что такое предпочтение объясняется нашей при-
вычкой читать слева направо. Позже я столкнулся со студен-
тами, которые предложили начинать проверку с самого право-
го камня: один был специалистом по машинной математике
-из Израиля, другой-э- сирийцем по происхождению. Есть
что-то пугающее в том, какими неисповедимыми путями наши
привычки влияют на наш способ мышления!
На этом я заканчиваю рассмотрение задачи о голландском
национальном флаге,- задачи, которой я обязан Фейену.
15 ОБНОВЛЕНИЕ ПОСЛЕДОВАТЕЛЬНОГО ФАflЛА

Когда появились охраняемые команды и о них заговорили,


один студент, занимавшийся главным образом решением ком-
мерческих задач на машине, выразил сомнение, имеет ли наш
подход какое-то значение вне (как он выразился.) области на-
учных или технических прикладных задач. В подтверждение
своих сомнений он ссылался на задачу, которую считал типич-
ной коммерческой прикладной задачей, а именно на обновле-
ние последовательного файла. (Более точная формулировка
задачи будет приведена нйже.) Он показал нам блок-схему
программы, предназначавшейся для решения этой задачи,но
стрелки, которые вели от одного квадратика к другому, были
так беспорядочно расположены, что мы оба решили не рас-
сматривать это решение (если оно было решением, что мы так
и не смогли выяснить!). С некоторой гордостью он показал
нам таблицу решений - его таблицу, - которая, как он
считал, могла решить задачу, но _размер этой таблицы
ужаснул нас. Однако, поскольку нам была брошена перчатка,
нам не оставалось ничего другого, как поднять ее. Наши пер-
вые два варианта, хотя и более аккуратные чем то, что мы ви-
дели, все же не удовлетворили Фейена, чье решение я приведу
в этой главе. Оказалось, что наши первые попытки были отно-
сительно безуспешными, потому что задача была представле-
на таким образом, что мы поверили, будто этот крепкий оре-
шек будет трудно расколоть. Но это совсем не так. Я включил
задачу об обновлении файла в настоящую книгу по трем при-
чинам. Во-первых, это дает мне возможность опубликовать
изящное решение, полученное Фейеном для задачи общего ти-
па, увешенной гроэдьями весьма неаккуратных решений. Во-
вторых, потому что решение было найдено благодаря тем же
методам, которые привели к элегантному решению задачи о
голландском национальном флаге. Наконец, эта задача поз-
Обновление последовательного файла 159

воляет нам рассеять некоторые сомнения, вызванные тем, что


коммерческие программы являются чем-то особенным. (Если
уж говорить о чем-то особом, то, скорее, о характере коммер-
ческих программистов ... )
Имеется файл, т. е. упорядоченная последовательность за-
писей, или, более точно, значений типа «запись». Если х-
переменная (ее значение) типа запись, тогда определены бу-
лева функция х.норм и целая функция х.ключ, такие, что для
некоторой константы инф ·
х. норм =Ф (х лслюн < инф)
(поп .х. норм'; =Ф (х. ключ, = инф)
Данный файл записей называется «схарфайл», и записи в
нем располагаются в порядке монотонного возрастания зна-
чений их «ключей»; только для последней записи старого фай-
ла «х.норм» есть ложь.
Имеется, кроме того, еще один файл, называемый «измен-
файл» и представляющий собой упорядоченную последова-
тельность изменений, или более точно, значений типа «измене-
ние». Если у- переменная (ее значение) типа изменение,
определены булева функция у.норм и целая функция у.ключ,
такие, что для той же константы инф
у. норм =Ф (у. ключ, < инф)
(поп у. норм) =Ф (у. ключ, = инф)
Изменения в файле располагаются в порядке монотонного
неубывания значений их «ключей», только последнее измене-
ние не является нормальным, и для него у.ключ= инф, Если
у.норм есть исхина, определены еще три булевы функции, а
именно «у.обн», «и.искл» и «и.вкл», причем эти функции тако-
вы, что всегда в точности одна из них есть истина.
Далее, для переменной х типа запись и переменной у типа
изменение, такой, что у.норм есть истина, определены три опе-
рации, изменяющие х:

х: обновить (у) определена только в случае истинности


хнорм апd у.обн апd (.х.ключ,=у,ключ,);
по завершении операции сохраняется истин-
ность .х. норм and (.х. ключ,= у. клюн;
х : исключить (у) определена только в случае истинности
х. норм апd у. искл and (.х. КЛЮЧ,=
=у. ключ,); по завершении операции
х.нор.Н=ЛОЖЬ
160 Глава 15

х: включить (у) определена только в случае истинности


поп х .но рм and у. вкл; по завершении
операции становится истинным х. но рм and
(х. ключ= у. ключ)
Для переменной х типа запись имеется дополнительная опе-
рация «х : анорм», в результате которой х.нормыложь.
Программа должна сгенерировать файл записей, называ-
емый «новфайл», окончательное значение которого зависит от
начального значения исходных файлов «схарфайл» и «измен-
файл» следующим образом. Мы можем объединить записи
старого файла и изменения файла изменений, располагая их
в порядке неубывания ключей таким образом, что если данное
значение ключа встречается и в старом файле (один раз!), и
в файле изменений (может быть, больше одного раза), то в
объединенной последовательности запись с этим ключом пред-
шествует изменению (изменениям) с тем же ключом. В процес-
се объединения не должен нарушаться внутренний порядок сле-
дования изменений с одним и тем же ключом. Объединенная
последовательность подвергается описанному ниже преобра-
зованию до тех пор, пока в ней не останется ни одного измене-
ния; оставшаяся последовательность, состоящая только из за-
писей, и есть окончательное значение нового файла. Преобра-
зование выполняется следующим образом:
Пусть у- первое изменение в объединенной последователь-
ности и х - непосредственно предшествующая ему запись ( ес-
ли она есть), тогда
если функция у.обн истинна и имеется предшествующая
у запись х, такая, что х.ключ=«у.ключ, то запись модифициру-
ется при помощи операции х: обновить(у) и у удаляется из
последовательности;
если функция у.искА истинна и имеется предшествующая
у запись х, такая, что х.ключ=у.ключ, то и Х1 и у удаляются
из последовательности;
если функция у.вкл истинна и нетпредшествующей у запи-
си х, такой, что х.ключ-« у.ключ, то у заменяется записью, оп-
ределенной операцией включения записи;
если поп у.норм есть истина, изменение у удаляется из'
последовательности;
если первое изменение не удовлетворяет ни одному из пе-
речисленных четырех критериев, оно удаляется из последова-
тельности по операции «сообщение об ошибке», которую мы не
будем здесь описывать более подробно.
Представим модель программы в виде внутреннего блока,
оперирующего с тремя глобальными массивами:
Обновление последовательного файла 161

запись array схарфайл, к которому ьэожно обращаться


только по операции ~никон» и
который в конце концов стано-
вится пустым (т. е. старфайл.
обл=О)
изменение array изменфайл, к которому можно обращаться
только ПО операции «Никон» и
который в конце концов стано-
вится пустым (т. е. изменфайл.
обл=О)
запись array новфайл, к которому можно обращаться
только по операции «вверх» и
который первоначально пуст
(т. е. новфайл, обл=О)
Мы видим, что задача усложняется, если объединенная по-
следовательность содержит последовательность изменений с
одинаковыми ключами и характеризующие изменения функ-
ции со значением «истина» следуют друг за другом, например,
в таком порядке:
вкл, вкл, обн, искл, искл, обн, вкл, обн, искл
В этом случае второе «вкл», второе «искл» и второе «обн»
приведут к сообщению об ошибке и вся последовательность
ничего не дает для получения окончательного значения нового
файла; за такой последовательностью может следовать и мо-
жет не следовать нормальная запись.
Поскольку длины файлов неизвестны. наша ,программа бу-
дет состоять из вступительной части, одной или нескольких
конструкций повторения и, может быть, части (не повторя-
емой), которая завершает новый файл записью, не являющей-
ся нормальной. В общем случае сгенерировать новую запись
для нового файла можно только при условии, что мы уже
епросмотрели» запись со следующим по порядку ключом из
старого файла и изменение со следующим по порядку ключом
из файла изменений. ·(Подумайте о возможности извлечения
следующей записи только из файла изменений.) В связи с
этим мы вводим переменную х типа «эапись» и переменную у
типа «из~енение», значением которых будут соответственно
запись и изменение с этими следующими по порядку ключа-
ми; после создания записи для нового файла «запись х и сле-
дующие за' ней оставшиеся записи старого файла» означают
еще необработанные записи, а «изменение у и следующие за
ним оставшиеся изменения файла изменений» означают еще
необработанные изменения. Мы пользуемся этим, когда к но-
вому файлу добавляется новая нормальная запись; послед-
6-6
162 Глава 15

нюю, не являющуюся нормальной запись добавит к файлу за-


ключительная часть программы. Поскольку неизвестно, сколь-
ко будет сгенерировано новых записей, пока не исчерпается
один из и,сходных файлов, предлагается такая общая струк-
тура нашей программы:
begin glovar старфайл, изменфайл, новфайл; privar х, у;
х vir запись, старфайл : никон;
у vr изменение, изменфайл: никон;
do х.норм or у. норм-
«обработать по крайней мере одну запись или
одно изменение, такие, чтобы их обработка обес-
печивала продвижение в создании нового файла»
od;
«добавить к новому файлу запись, не являю-
щуюся нормальной»
end
При составлении конструкций повторения решающее зна-
чение имеет синхронизация прогресса в вычислениях с про-
движением по циклу, т. е. определение того, какой объем ра-
боты будет выполнен каждым списком охраняемых операто-
ров, выбираемым для исполнения. Если мы в этом примере
будем определять ко~ец цикла по концу старого .файла или
файла изменений, могут возникнуть неприятности, так как
один из файлов может уже быть исчерпанным; аналогичные
неприятности возникают и в том случае, когда мы считаем
концом цикла завершение генерации записей для нового файла,
потому что, хотя ни один из исходных файлов не исчерпан, мы
могли уже сгенерировать последнюю нормальную запись для
нового файла. Эта проблема должна решаться тем же спосо-
бом, как и для задачи о голландском национальном флаге,
когда нужно было определить, сколько камней подлежит про-
верке: охраняемый оператор будет выполнять в точности такой
объем работы - не больше и не меньше, - сколько нужно и
можно сделать, когда предохранитель имеет значение «исти-
на».
Если наш предохранитель х.норм or у.норм есть истина, мы
можем сделать единственное заключение, а именно что необ->
работанные части исходных файлов содержат запись и (или)
изменения по крайней мере для одного значения 'ключа, тако-
го, что ключ-синф: следовательно, список охраняемых опера-
торов будет осуществлять обработку всех записей и (или) из-
менений с этим значением ключа!
Первый оператор - конструкция выбора - определяет это
значение ключа и присваивает его локальной константе «те-
ключ» (сокращение от «текущий ключ»). Кроме того, он ини- ·
Обновление последовательного файла 163

циализирует переменную «ХХ» типа запись; значение именно


этой переменной будет выбираться в качестве новой записи,
если потребуется сформировать такую запись. Если текущий
ключ извлекается из старого файла, обрабатывается единст-
венная запись с этим ключом, в противном случае старый
файл останется нетронутым.
Второй оператор - конструкция повторения - обрабаты-
вает все изменения с этим значением ключа; поскольку обра-
ботка изменений целиком выполняется этой конструкцией пов-
торения, в ней учтены различия между' разнообразными вида-
ми изменений. ·
Третий операrор - конструкция выбора - может добавлять
новую запись к новому файлу.
begin glovar ста рфайл, иеменфайл, новфайл; privar х, у;
х vir запись, старфайл: никон:
у vir изменение, иэменфайл : никон;
do х.норм or y.нopм-
begin glovar старфайл; из менфай.а, новфайл; х, у;
pricon теключ: privar хх;
if х . ключ «; у. к иоч.- теключ vir цел:= х. ключ;
хх vir вапись : =х; х, старфайл: ниюон» .
О х, ключ> у. ключ - теклюн vir цел : = у. ключ;
хх vir запись: ано рм
<
fi {теключ инф and теключ <
х. ключ and
теключ ~у. ключ};
do у.ключ=теключ-{у.ключ..<инф, т. е. у.норя}
if у.обн and хх гнорм=ь хх : обновить (у)
О у.искл and хх.норм=ч хх: исключить (у)
О у . вкл and поп хх . норм - хх : включить (у)
О у. вкл=хх. норм - сообщение об ошибке
fi; у, иеменфай.л : никон
od { теклюн < х. ключ and теклюн <у. ключ};
if хх.норм-« новфайл: вверх (хх) О поп хх .норм-«
пропустить
fi
end
od; новфайл: вверх (х) {новый файл замыкается не являю-
щейся нормальной записью}
end
Примечание 1. Избранный нами способ представления этой
задачи предусматривал в качестве одного из возможных зна-
чений переменной х типа запись такое значение, при котором
х.нормы ложь; при постановке задачи это значение использо-
валось в качестве маркера конца старого файла. Благодаря
6*
164 Глава 15

такому соглашению оказалось возможным ограничить обра-


щение к старому файлу операцией «Никон»; если бы, однако,
нам была доступна также функция «сгарфайл. обл», такое ис-
пользование в качестве маркера конца файла записи, не явля-
ющейся нормальной, стало бы ненужным и вся задача могла
бы формулироваться в терминах переменных типа «нормаль-
ная запись». Тогда, чтобы составить такую программу, нам
понадобились бы языковые средства, позволяющие ввести тип
«запись» в том смысле, в каком он используется в нашем ре-
шении. Языки программирования, лишенные таких средств, вы-
нуждают своих пользователей удовлетворять эту потребность,
вводя, например, пару переменных, скажем переменную хх
типа «нормальная запись» и булеву переменную «ххнорм», и
затем в явном виде употреблять в программе эти переменные;
тогда ххнорм указывает, является ли значение хх осмыслен-
ным. Но такой «условный смысл» несовместим с нашей идеей
явного присвоения переменным осмысленных начальных зна-
чений. В случае х.ключ~у.ключ как хх, так и ххнорм могут
получить осмысленные начальные значения, в случае же
х.ключ'з-и.ключ, действительно имеет смысл только
ххнорм vir бул ==ложь
Но тогда наши соглашения заставляют нас присвоить пере-
менной хх в качестве начального значения некоторое пустое
значение. Предположив, что переменная типа запись может
иметь и значение, не являющееся нормальным, мы обошли эту
проблему, отсрочив тем самым дискуссию о языковых средст-
вах, необходимых для введения новых типов. (Конец примеча-
ния 1.)
16 ЕЩЕ РАЗ О ЗАДАЧАХ СЛИЯНИЯ

В двух предыдущих главах мы вели рассуждения· в стиле,


несколько отличном от наших более ранних формальных рас-
суждений, когда мы получали предохранители, как только бы-
ли выбраны инвариантное отношение и изменяющаяся функ-
ция (для конструкции повторения) или постусловие (для кон-
струкции выбора) и список охраняемых операторов. Однако,
решая задачу о голландском национальном флаге и задачу об-
новления файла, мы начинали с' рассуждения о конструкции
повторения с предохранителем и далее обсуждали, каким мо-
жет быть охраняемый оператор, т. е. придерживались более
классического 'подхода. Что касается программы обновления
файла, могу себе представить, что некоторые мои читатели,
изучив более ранние главы, стали настолько подозрительными,
что они удивляются, как это я осмеливаюсь верить в правиль-
ность программы, приведенной в последней главе; против всех
правил я даже не упомянул об инвариантном отношении! На-
стоящая глава призвана восполнить эти пробелы, и это одна
из причин ее включения в книгу; одновременно она пре-
доставит нам возможность решать более широкий класс за-
дач в более абстрактной форме.
Пусть х, у и z есть некоторые наборы (не теряя общности,
мы можем ограничиться наборами целых чисел - их вполне
достаточно'). Напомним, что из определения понятия «набор»
следует, что все его элементы отличны друг от друга; в этом
отношении «набор целых чисел» отличается от «совокупности
чисел, которая может содержать одинаковые числа». Два на-
бора равны (х= у) тогда и только тогда, когда каждый эле-
мент одного набора входит в другой набор, и наоборот.
Обозначим объединение двух наборов символом «+», т. е.,
если z=x+y, это значит, что набор z содержит некоторый
элемент тогда и только тогда, когда этот элемент входит либо
в набор х, либо в набор у, либо в оба набора.
· Обозначим пересечение двух наборов символом « * », т. е.,
если z=x *у, это значит, что набор z содержит некоторый эле-
166 Глава 16

мент тогда и только тогда, когда этот элемент входит и в на·


бор х, и в набор у.
Обозначим пустой набор символом «f25», т. е. z= f25 тогда в
только тогда, когда z вообще не содержит элементов.
Рассмотрим .теперь задачу вычисления для фиксированных
наборов Х и У значения Z, такого, что
Z=X+Y
. (В ходе обсуждения Х и У, а следовательно, и Z рассматрива ..
ются как константы: Z есть искомое окончательное значение
некоторой переменной, которая будет введена позже.) Наша
программа должна выполнить эту задачу, производя поэле-
ментно действия над наборами - проверку, изменение и т. д.
Прежде чем приступить к более детальному продумыванию
алгоритма, мы должны осознать, что в какой-то момент вы-
числительного процесса некоторые элементы Z уже будут най-
дены, а некоторые нет, т. е. существует разделение набора Z
на два поднабора zl и z2:
Z=zl+. z2
Здесь символ «+» используется для сокращенной записи
Z=zl+z2 and zl *Z2 . 0
(Мы можем рассматривать zl как набор элементов, чья при-
надлежность к Z уже установлена, и z2 - как набор остальных
элементов Z.)
Аналогично в некоторый момент 'вычислительного процес-
са наборы Х и У могут быть представлены в виде
X=xl + х2 и Y=yl + у2
{Здесь мы можем рассматривать наборы xl и yl как те эле-
менты Х и У соответственно, которые больше не нужно при-
нимать во внимание, поскольку они уже были успешно обра-
ботаны.)
Такая интерпретация смысла разделения Z, Х и У будет
интересовать нас позже, а сейчас мы сначала докажем, со-
вершенно независимо 01 того, что может произойти во время
выполнения нашей программы, теорему о таких разделениях"
Теорема
Если .Z=X+Y . (1)
X=xl +
Y=yI +u2
х2 (2}
(3)
z 1 =х1+у1 (4)
z2=x2+y2 , (5}
то Z=zl +z2#(xl * у2=0 and yl *Х2=50) (6)
Еще раз о задачах слияния 167

Доказательство. Чтобы показать, что правая часть (6)


следует из его левой части, воспользуемся тем, что
Z=zl + z2=Фz1*z2=0
Подставляя (4) и (5), получаем
(х1+у1) * (х2+у2)=
(xl *Х2)+(х1 *У2)+(у1 *Х2)+(у1 *У2)=0
отку да следует правая часть (6). Чтобы показать, что левая
часть (6) следует из его правой части, мы должны показать,
что из нее следует
zl * z2= 0 and_ Z=z1+z2
zl * z2= (х1+ yl) ~ (х2+ у2)
=(х1 * x2)+(xl * у2)+(у1 *Х2)+(у1 * у2)
·0+0+0+0=0
zl+z2=(xl +у1)+(х2+у2)
=xI+x2+y1+y2
=X+Y=Z (Конец доказательства.)
Если имеют место отношения с ( 1) по (5), то правая часть
(6) и, следовательно, равенство
Z=zl + z2
вытекают также из того, что
zl * х2= 0 and zl * у2= 0 (1)
и из отношений с ( 1) по· (5) и (7) следует, что, если было
выбрано разделение Z, два других разделения однозначно
определены.
Вооруженные этими знаниями, мы возвращаемся к исход-
ной задаче, где наша программа должна обеспечить истин-
ность
я. Z=Z
Используя тот же подход, что и в предыдущих задачах,
введем две переменные х и у и попробуем взять в качестве
инвариантного отношения
Р: z+,(x+y)=Z
которое обладает тем приятным свойством, что его истинность
тривиально обеспечивается при помощи
РО: z= 0 and х=Х and у=У
168 Глава 16

и в то же время при (х+у) = 0 из него следует истинность R.


Наша теорема предлагает нам отождествить х с х2, у с у2
и z с z1 (такая асимметрия вызвана тем, что Х и У - извест-
ные наборы, а Z предстоит вычислить). После такого отожде-
ствления отношения с (2) по (5) определяют все наборы в
терминах х и у. ЕсЛи мы теперь будем синхронно сокращать
х и у таким образом, чтобы неизменно сохранялась истинность
правой части (6) или
Z•X=0 and Z*Y=0 (7')
то, как мы знаем,
Z=X+Y=z1 -t-z2
+
Так как zl z2 есть разделение постоянного набора Z, то
добавление элемента е к z ( =zl) означает удаление этого
элемента е из х+ у ( = z2). Чтобы такой элемент существовал)
объединение не должно быть пустым, т. е. (х+у) =1=0 или, что
то же, «X=i= 0 or Y=i= 0»;· элемент е может принадлежать х, но
не принадлежать у, или он принадлежит у, но не принадле-
жит х, или принадлежит, и х, и у и должен уда-
ляться соответственно либо из х, либо из у, либо и из х, и из у.
Структура программы представляется в следующем виде:
Х, у, z:= Х, У, 0;
do х :;t= 0 or у =1= 0 - «перевести элемент из (х+ у) в г» od
Предположим теперь, что элементы набора х представля-
ются значениями ax(i) при ах.ниг си-с ах.вег и элементы на-
бора у представляются значениями ay(i) при ау.ниг~i<ау.вег,
где ах и ау - это монотонно возрастающие целые функции, и
ах.верхь-ач.еерх-еинф. Использование дополнительной пере-
менной инф дает нам то преимущество, что, даже если х или у
пусты, все равно определены ах.низ и ау.низ. Преимущество
от монотонного возрастания значений ах и ау состоит в том~
что для непустого набора х значение ах.низ равно минималь-
ному элементу х, и то же для у. В результате, если не оба на-
бора пусты, существует один элемент из их объединения, для
которого очень легко определить, принадлежит ли он х, у ИЛИ'
обоим наборам, а именно
min (ах. низ, ау. низ)
Если, например, ах.низ<ау.низ, этот элемент равен ах.низ
и входит в х, но не может входить в у, так как все элементы
набора у больше, чем этот элемент; его удаление из набора х
осуществляется при помощи операции ах: снизу, при этом ос-
Еще раз о задачах слияния 169

-тавшиеся элементы набора х по-прежнему представляются


монотонно возрастающей функцией.
Если представление эл~ментов набора z должно отвечать
тому же соглашению, мы выбираем для их представления зна-
чения az(i) при аz.ниг~J~аz.вег; тогда последнее значение
инф можно добавить только в конце вычислений. Полученная
_в результате функция аг будет монотонно возрастающей, если
ее значение изменяется только при помощи операции
az: вверх (К) и при этом либо аг.об л=б, либо аz.обл>О and
аz.верх<К. Наша программа будет удовлетворять этим огра-
ничениям.
Если ах и ау- это правильно инициализированные вектор-
ные' переменные (т. е. ах.верхь-аи.вер хьеинф'; и az- вектор-
ная переменная с аz.обл,,,,;О, то мы можем получить желаемое
преобразование при помощи следующих двух операторов:
dоах.низ =/= инф ог ау.низ=1=инф-{тiп(ах.низ, ау.низ)<
инф}
if ах .низ-с ау .низ -az: вверх (ах. низ); ах: снизу
О ах.низ>ау.н.из-аz: вверх;(ау.низ); ау: снизу
О ах .ниэ =ау. низ-+ аг : вверх (ах. низ); ах: снизу;
ау: снизу
fi {аz.верх<тiп(ах.низ, ау.низ)}
сd{аz.обл=О соr(аz.обл>О and аz.верх<инф)};
az: вверх (инф)

Упражнения
1. Изменить эту программу таким образом, чтобы она обес-
печила также истинность и=И, где И=Х 1' У.
2. Составить аналогичную программу, которая обеспечит
истинность z=Z, где Z= W+X+Y.
3. Составить аналогичную программу, которая обеспечит
истинность z=Z, где Z.= W+ (Х* У).
4. Составить программу, обеспечивающую истинность
гь- Х +У, но без предположения о наличии= (и без введения!)
значения «инф», отмечающего верхние концы областей опре-
деления; пустые наборы можно обнаруживать по ах.обл=О и
ау.обл= О соответственно. (Конец упражнений.)
Ценой еще большей формализации мы могли бы еще ус-
пешнее сыграть в нашу формальную игру.
Пусть для любого предиката P(z) семантика «Z:=x+ у,,>
задается слабейшим предусловием
wp(<<z==x +у>), P(z))=(X*Y=0 and Р(х+у))
170 Глава 16

где первый член означает, что пересечение х и у должно быть


пустым, чтобы было определено х + у.
Пусть для любого предиката Р (z) семантика «Z: =Х ~у»
задается слабейшим предусловием
wp(<<z:=x~y>>, P(z))=(x*y=y cand Р(х~у))
где первый член означает, что у должен целиком содержать-
ся в х, чтобы было определено х=::=.у, и тогда х~у представ-
ляет собой единственное решение уравнения (х~у) .f-y=x.
Исключая. х1, х2, у1, у2, zl и z2, мы получим следующие
отношения между х, у и z:
PJ: х *Х=х
Р2: У*У=у
РЗ: z=(X~x)+(Y~y)
Р4: Х*(У~у)=0
Р5: У*(Х~х)=0

и мы можем задаться вопросом, при каких начальных услови-


ях выполнение операции
S: z, х : = z +{
е}, х ~ {е}
сохранит эти отношения инвариантными для некоторого эле-
мента е. Начнем с выяснения условий, при которых определе-
но это· одновременное присваивание, т. е.
wp(S, T)=(z* {е}=0 and Х* {е}={е})
Так как
(Р1 and Х* {е}={е})~(Х~х) * {е}=0
(Р2 and Р4 and Х* {е}={е})~(У~у) * {е}=0
Q=9wp(S, Т) при
Q=(P1 and Р2 and РЗ and Р4 and Х* {е}={е})
Теперь нетрудно установить следование
Q~wp(S, Р1 and Р2 and РЗ and Р4)

Однако
wp(S, P5)=(wp(S, Т) and У*(Х~(х~{е}))=0)
=(wp(S, Т) and н» ((Х ~х) +{е})= 0)
=(wp (S, Т) and Р5 and.y * {е} = 0)
Еще раз о задачах слияния 171

и, следовательно,предохранитель для S, обеспечивающий ин"


вариантность отношений с Pl по Р5, - это
х * { е} = { е} and у * { е} = 0
т, е. е должен быть элементом х, но не у, и т. д.
· Вышесказанное завершает мою ревизию задач слияния. В
двух последних главах я проводил рассуждения с различной
степенью формальности, и какую из этих степеней предпочтет
мой читатель, зависит настолько же от его потребностей, на"
сколько и от его настроения. Но мне. казалось поучительным
проделать все это хоть один раз! (По всей вероятности, за"
иетно, что при написании этой главы возникло гораздо боль"
ше трудностей, чем предполагалось заранее. Это по крайней
мере пятая версия, что само по себе служит достаточным ос"
вованием для ее включения в настоящую монографию.)
17 УПРАЖНЕНИЕ, ПРИПИСЫВАЕМОЕ Р. У. ХЭММИНГУ

Эта задача известна мне в такой формулировке: «Генери-


ровать в порядке возрастания последовательность 1, 2, 3, 4, 5,
6, 8, 9, 10, 12, ... , состоящую 11з всех чисел, не делящихся ни
на какие простые числа, кроме 2, 3 и 5». Другое определение
того, какие значения являются элементами этой последова-
тельности, задается при помощи трех аксиом:
Аксиома 1. Значение 1 является элементом последователь-
ности.
Аксиома 2. Если х является элементом последовательности,
то 2 * х, 3 tc х и 5 * х также являются .элементами
этой последовательности.
Аксиома 3. Последовательность не содержит других значе-
ний, кроме тех, которые принадлежат ей соглас-
но аксиомам 1 и 2.
(Предоставляем специалистам по теории чисел установить эк-
вивалентность двух этих определений.)
Мы включили это упражнение, потому что его структура
довольно типична для большого класса задач. Поскольку нас
интересуют только заканчивающиеся программы, будем со-
ставлять программу, генерирующую, скажем, лишь первые
1 ООО значений последовательности.
Пусть
РО(п, q) означает: значение «q» представляет упорядоченный
набор первых «п» значений последователь-
ности.
Тогда, согласно аксиоме 1, 1 входит в последовательность,
и так как 2 * х, 3 * х и 5 * х - это функции, чье значение боль-
ше х при х>О, то, согласно аксиоме 2, 1 есть минимальное из
всех значений, чья принадлежность к последовательности
может быть установлена на основании первых двух аксиом.
Согласно аксиоме 3, 1 есть минимальное значение, входящее
Упражнение, приписываемое Р. У. Хэммингу 173

в последовательность, и поэтому истинность РО(п, q) легко


обеспечивается для n= 1: «а» тогда содержит только значе-
ние 1. Очевидная структура программы такова:
«обеспечить истинность РО (п, q) для n= 1>>;
do п =1= 1000-
«увеличить п. на 1 при инвариантности РО (п, q)>>
od
В предположении, что мы можем дополнить последова-
тельность значением «хслед», если это значение известно, ос"
новная проблема, возникающая при реализации операции
«увеличить 'п. на 1 при инвариантности РО ( п, q) », состоит в
том, как определить значение «хслед», Поскольку значение 1
уже принадлежит q, хслед;» 1 и принадлежность хслед после-
довательности должна обосновываться аксиомой 2. Назовем
максимальное эначение, входящее в q, «а.верх», тогда хслед-
это минимальное значение, большее q.верх, которое можно
представить в виде 2 * х, 3 * х и 5 * х, где х - элемент после-
довательности. Но так как 2 * х, 3 * х и 5 * х - это функции,
чье значение больше х при х>О, то значение х должно улов-
летворять условию »<хслед; далее, х не может удовлетворять
условию х'з-ц.еерх, так как мы тогда имели бы
q.верх<х<хслед
а Это противоречит тому, что хслед есть минимальное значе-
ние, большее q.верх, т. е. х должен уже входить в q. (Именно
ради проведенного анализа мы инициализировали РО (п, q)
для п=1; так же просто можно было сделать это для n=O, но
тогда значен,ие а.верх не было бы определено.)
Если мы хотим получить простую реализацию описанного
выше анализа, мы можем ввести набор qq, состоящий из всех
значений хх з-о.верх, таких, что хх можно представить как
хх=2 * х, где х-элемент q,
или как
хх=З * х, где х-элемент q,
или как
хх=5 * х, где х-элемент q.
Набор qq не пуст, и хслед есть в этом случае минимальное
входящее в qq значение. Но при ближайшем рассмотрении та-
кой путь оказывается не очень привлекательным, так как кор-
ректировка qq могла бы привести (в терминах предыдущей
главы) к операции
qq: = (qq ~ {хслед})+ (2 * хслед, 3 * хслед, 5 * хслед)
174 Глава 17

тце е-}.» означает «образовать объединение двух наборов». Так


как мы должны определять минимальное значение, входя-
щее в qq, было бы хорошо, если бы элементы q были
упорядочены; но тогда образование объединения при такой
корректировке qq привело бы к значительной перетасовке
элементов, чего нам хотелось бы избежать.
Однако, поразмыслив немного, мы приходим к выводу, что
нам не нужно следить за всем набором qq, а можно выбрать
-в качестве хслед минимальное значение, входящее в гораздо
меньший набор
qqq={x2} + {хЗ} + {х5}
где
х2 есть минимальное значение>q.верх, такое, что
х2=2 * х их входит в q,
хЗ есть минимальное значение > q.верх, такое, что
Х3 =3 * Х И Х ВХОДИТ В q, И
~5 есть минимальное значение >q.верх, такое, что
х5=5 * х и~ входит в q.
Эти отношения' между а, х2, х3 и х5 обозначаются. как
Р1 (q, х2, х3, х5).
Поэтому очередной вариант нашей программы выглядит
так:
«обеспечить истинность РО ( п, q) для п = 1 »;
do n=/= 1 ООО -+
«обеспечить истинность Pl (q, х2, х3, х5). для текущего
значения q»;
«увеличить п на 1 при инвариантности РО (п, q),.
т. е. добавить min (х2, х3, х5) к q»
od

Такая программа была бы во всех отношениях правильной,


но теперь пришлось бы выполнять· операцию «обеспечить ис-
тинность Pl (q, х2, х3, х5) для текущего значения q», опера-
цию весьма неприятную, даже если предположить, ~то мы име-
ем такой доступ к элементам упорядоченного набора q, какой
пожелаем. Справиться с этим можно· стандартным образом:
вместо того чтобы каждый раз, когда нам нужны х2, х3 и х5,
заново вычислять их как функцию q, мы можем, понимая, что
значение q изменяется «медленно», пытаться «корректировать»
значения, являющиеся функцией q, только тогда, когда q из-
меняется. Этот прием настолько стандартен, что стоит дать
ему специальное название; назовем его «вынесением отн_оше-
ния "(иэ конструкции повторения)». Применение, этого приема
Упражнение, приписываемое Р. У. Хэммингу 175

отражено в следующей струю:Уре программы:


«обеспечить истинность РО ( п, q) для п = 1 »;
«обеспечить истинность PI (q, х2, хЗ, х5) для текущего значе ..
ния q»;
do п*1000-+
«увеличить п на 1 при инвариантности РО(п, q), т. е.
добавить к q тлп (х2, хЗ, х5) »;
«восстановить истинность Р1 (q, х2, х3, х5) для нового
значения q»
od
Теперь мы должны следить за тем, чтобы Pl (q, х2, х3, х5)
было истинным после расширения q, т. е. после добавления
q.верх; в результате корректировка х2, хЗ и х5 есть либо пус-
тая операция, либо их увеличение, а именно замена соответст-
вующим кратным следующего значения х из q. Представляя
упорядоченный набор q в виде массива aq, т. е. в виде монотон-
но возрастающих значений aq(I), aq(2), ... , aq(n), мы вводим
три индекса i2, iЗ и i5 и расширяем Pl, добавляя к нему
... and х2=2 * aq (i2) and х3=3 * aq (i3) and х5=5 * aq (i5)
Тогда наш внутренний блок, придающий глобальной век-
торной переменной aq в качестве начального значения нужное
нам конечное значение, будет выглядеть следующим образом:
begin virvar aq; privar i2, i3, i5, х2, х3, х5;
- aq vir цел array := (1, 1); '{истинность РО обеспечена}
i2 vir цел, i3 vir цел, i5 vir цел:= 1, 1, 1:
х2 vir цел, х3 vir цел, х5 vir цел.:= 2, 3, 5; {истин-
ность Р1 обеспечена}
do aq. обл =f=: 1 ООО -
<
if х3 :>- х2 x5-aq: вверх (х2)
<
О х2 :>- х3 х5- aq: ввеох (х3)
U х2 :>- х5 <хЗ-аq: вверх (х5)
fi { аа . обл увеличилась на 1 при инвариантности
РО};
<
do х2 аq.верх- i2: =i2+ 1; х2: =2 * aq(i2)od;
do х3 < aq.вepx-+i3 :'=iЗ+ 1; хЗ:=3 * aq(iЗ)od;
do x5.<aq.вepx-i5:=i5+1; x5:=5*a{J(i5)od
{вновь обеспечена истинность Р 1}
od
end
В этой версии ясно выражено, что после того, как заново
обеспечена истинность Р1, мы имеем х2>аq.верх and
х3>аq.верх and хб'з-аа.верх. Заметим, что мы могли бы с тем
же успехом использовать « ... = аа.верх» вместо « ... ~ аа.верх».
176 Глава 17

Замечание 1. В трех последних внутренних конструкциях


повторения каждый список охраняемых операторов будет вы­
бираться для выполнения самое большее один раз. Поэтому
.мы могли бы представить их в таком виде:
if x2=aq. верх-i2: = i2+ 1; х2 :=2 * aq (i2)
>
О х2 aq. верх=« пропустить
fi; и т. д.
Когда я начинаю думать о такой возможности, я прихожу
к выводу, что явно предпочитаю конструкции повторения, так
как не вижу ничего настолько особенного в- том, что повторе-
ние может прекратиться после нуля шагов или одного шага,
чтобы ради этого стоило использоватъ специальные синтакси-
ческие средства. Всякие колебания по поводу того, можно ли
считать «нуль или один раз» частным случаем «самое большое
k раз», вызываются, по-видимому, нашим языковым наследи-
ем, так как все западные языки делают различие между фор-
мами единственного и множественного числа. (Если бы мы
были классическими греками (т. е. мыслили бы также и кате-
гориями двойственного числа), мы, вероятно, чувствовали бы
себя обязанными ввести специальное дополнительное синтак-
сическое средство для выражения завершения конструкции
повторения после самое большее двух шагов!) В этом смысле
было бы, вероятно, «честнее» закончить главу «Обновление
последовательного файла» конструкцией
do хх.яорм=« новфайл : вверх(хх); хх: анорм od
вместо
i f хх. но рм - новфайл ~ вверх (хх) О поп хх. норм --+

пропустить fi
так как при этом удовлетворяется условие завершения конст-
рукции, выраженное через хх.норм. (Конец замечания 1).
Замечание 2. Три последние внутренние конструкции пов-
торения можно было бы объединить в одну:
do х2-< aq .верх-+ i2:=i2+1; х2 * aq (i2)
О хЗ-<: aq. верх--+ i3 : = i3 + 1; х3 * aq (i3)
О х5 <: aq. верх-+ i5: =i5+ 1; х5 * aq (i5)
od
Однако я предпочитаю не делать этого и не объединять ох-
раняемые команды в один набор, когда выполнение одного
списка охраняемых команд не может повлиять на истинность
других предохранителей из этого набора. Тот факт, что при
этом три конструкции повторения, разделенные точкой с запя-
Упражнение, приписываемое Р. У. Хзмминги l 77

той, предстают в порядке, выбранном произвольно, меня не


беопокоит: это обычная форма избыточной спецификации, с
которой мы всегда сталкиваемся в последовательнr1х програм-
мах, описывающих последовательно действия, которые могли
бы выполняться параллельно. (Конец замечания 2.)
Упражнение, разобранное в этой главе, является частным
случаем более общей задачи, а именно задачи о генерации
первых N значений последовательности. определенной следу-
ющими аксиомами:
Аксиома 1. Значение 1 является элементом последователь-
ности.
Аксиома 2. Если х есть элемент последовательности, элемен-
тами последовательности являются также f (х),
g (х) и h (х), где f, g и h - монотонно возрастаю"
щие функции, такие, что f (х) >х, g (х) >х и
h (х) >х.
Аксиома 3. Последовательность не содержит друг~х значе-
ний кроме тех, которые принадлежат ей согласно
аксиомам 1 и 2.
Заметим, что если бы о "функциях f, g и h не было ничего
известно, задача не могла бы быть решена!
Упражнения
1. Решить эту задачу, если аксиома 2 формулируется по-
др,.Угому:
Аксиома 2. Если х есть элемент последовательности, то
элементами последовательности являются
также f (х) и g(x), такие, что f(x) >х и
g(x) >х.
2. Решить задачу, если· аксиома 2 формулируется так:
Аксиома 2. Если х и у принадлежат последовательности,
то ей принадлежит и f (х, у), где f обладает
свойствами:
1. f (х, у) >х
2. (у1>у2) =Фf(х, yl)>f(x, у2)
(Конщ упражнений.)
Пытливый читатель, который успешно справился с этими
упражнениями, может придумывать дальнейшие варианты сам.
18 ЗАДАЧА ПОИСКА ПО ОБРАЗЦУ

Задача, которая решается в этой главе, широко известна,


за ее решение принимались независимо многие программисты.
Однако мы надеемся, что наш подход доставит некоторое удо-
вольствие даже тем читателям, которые считают, что они до-
статочно хорошо знакомы с этой задачей и ее различными
решениями.
Даны две последовательности значений
р(О), p(l), ... , p(N-1) при N':;? 1
и
х(О), x(l), .•. , х(М-1) при М':;?О
(считается обычно, что М во много раз больше N).
Требуется определить, сколько раз «образец», заданный
первой последовательностью, входит во вторую последователь-
ность.
Используя форму
(Ni : О< i < т: В (i))
для обозначения «числа различных значений i в интервале
о:::;; i<m, для которых выполняется В (i) », мы получаем более
точное выражение конечного ·отношения R, истинность кото-
рого должна быть обеспечена нашей программой.
R: cчem=(N i: О< i < М -JY: совпадение (i))
г де функция совпадение ( i) определяется следующим образом:
для О~ i < М -N: совпадение (i)=(Aj: О< j < N: p(J)=
х (i+ j))
для i <О or i >М- N: .совпадение (i) =ложь
(То что вне интервала O:::;;;i:::;;;M-N мытгридаем функции
ложное значение, .делая ее тем самым полностью определен-
ной, является вопросом удобства.)
Задача поиска по образцу 179

Если мы возьмем в качестве инвариантного отношения


Р1: счет=(N i: О< i < r: совпадение (i)) and r >-О
то его истинность тривиально ·обеспечивается оператором
«счет, r: =0, 0» и, кроме того, получаем, что
(Pl and r>M-N)==ФR
(Здесь мы опираемся на то, что выше было названо «вопро-
сом удобствах.)Получаем некоторый набросок программь~:
счет, r :=О, О;
do r < М -N-+ «увеличить r при инвариантности Р':» od
iI читателю предлагается разработать для себя более деталь-
ный вариант, где r всегда увеличивается на 1; в самом плохом
случае время, потраченное на выполнение этой программы,
.будет пропорционально М * N. -
Однако в зависимости от образца иногда становится воз-
можным увеличивать r на число, гораздо большее чем 1; если,
например, образцом является последовательность ( 1, 2, 3, 4,
5) и функция совпадение (r) 'истинна, то операция «счет,
r:=сч~т+'l, r+5» сохранит инвариантность Pl! Рассматривая
инвариантное отношение
Р2: - (Aj: О<}< k: р (J)=x (r+ })) and О< k < N
(которое, как можно предположить, сыграет роль в конструк-
ции повторения, вычисляющей совпадение (r)), мы можем
посмотреть, что мы выиграем, если вынесем это условие из
конструкции повторения, Для чего рассмотрим такой вариант
программы:
счет, r, k ==О, О, О;
do r < М -N-+ «увеличить r при инвариантности Pl and Р2>>
od
{при k=O Р2 удовлетворяется из-за пустоты множества).
Исходя из истинности отношения Р2 и формулы для сов-
падение (r), самым естественным будет начать повторяемый
оператор с попытки определить значение функции совпадение
(г): так как истинность совпадение (r) следует из Р2 and
k=N, мы считаем, что нужно увеличивать k до тех пор, пока
это необходимо и возможно:
do k =1=.N cand p(k)=x(r+k)-+k :=k+ 1 od (1)
180 Глава 18

По завершении этой конструкции, а завершение гарантирова-


но, мы имеем
Р2 and (k=N сог p(k) =i=x(r+k))
откуда мы можем заключить, что совпадение(r) = (k=N).
Таким образом, известно, будет увеличение r на 1 сопро-
вождаться оператором «счез: =счет+ 1 » или нет. Нам бы хо-
телось знать, насколько можно увеличивать r, не увеличивая
счет и не принимая во внимание последующие х-значения.
(Эти значения принимаются во внимание в операторе ( 1);
учитывать их является его специальным назначением! Мы же
хотим здесь использовать только свойства постоянного об-
разца.)
Если k=O, мы делаем вывод (поскольку N>O), что совпа-
дение(r) =ложь; отношение Pl позволяет увеличить " на 1
(Pl сохраняется инвариантным, поскольку счет не изменяет-
ся), но Р2 не допускает увеличения .J"' больше чем на 1 и k
остается равным нулю (при Этом Р2 истинно, так как соот-
ветствующее множество пусто).
Для любого k будем рассуждать следующим образом.
Определим для O<i~k~N .булеву функцию
dif (i, k)=(Ej: О< j < k-i: р (j) =;zf: p(i+ j))
Отсюда следует, что dif (k, k) =•ложь. Если; однако;
dif(l, k) =истина; мы заключаем-поскольку O~i+j<k-
что благодаря истинности Р2 ,
(Ej:O<}<k-i: p(j) =i=x(r+i+Л)
т. е. dif ( i, k) =Ф поп совпадение (r+ i). Следовательно, пере-
менная «счет» не нуждается в последующей корректировке
(кроме одного раза, что вызвано значением функции совпаде-
ние (r)), когда r увеличивается на d (k), где d (k) есть мини-
мальное решение для i (O<i~k) уравнения dif (i, k) =ложь,
или
(Aj :O<j<k-i :p(j)=p(i+Л) (2)
Из того, что d (k) есть решение (2), следует
(Aj :O<j<k-d(k):p(j)=p(d(k)+Л)
что с учетом Р2 равносильно
(Aj: 0 < j < k-d
-,
(k) : р (j)=X (r+d (k)+ j))
и в результате (помимо корректировки «счет», подразумевае-
мой значением совпадение(r)) при помощи «Г, k:=r+d(k), ,
k-d(k)» обеспечивается сохранение инвариантности как Pl,
так и Р2.
Задача поиска по· образцу 18)

Поскольку минимальное решение (2) зависит только от k


и р, мы получаем:
begin glocon р, N, х, М; virvaг счет; privar r, k; pricond);
«ннициализировать а»;
счет vir цел, r vir цел, k ·vir цел_:= О, О, О;
do r-.<M-N-+
do k=I= N cand р (k)=x(r+k)-+k: =k+ 1 od;
if k=N-+ счет:= счет+ 1; r, k := r+d (k),
k-d(k)
OO<k<N-+r, k := r+d(k), k-d(k)
0k=0-+r:=r+l
fi
od
end
Единственное, что нам осталось сделать,- это инициализи-
ровать векторную переменную d, т. е. найти для каждого-
k ( 1 ~k~N) минимальное решение уравнения (2) относитель-
но i. Согласно теореме о линейном просмотре, мы должны
проверять i-значения в порядке их возрастания. Полезно, од-
нако, ясно представлять себе, что это минимальное значение-
для i должно быть определено для всей последовательности
k-значений. Пусть k1 >k2 и пусть d (k1) - минимальное ре-
шение уравнения (2) относительно i при k=·k1. Из
(Aj :O<j<k1-d(kl) :p(j)=p(d(kl)+Л) and kl >k2
следует
(Aj: О< <
j k2-d (k1): р (j)=p (d (kl)+ j))
т. е. для k=k2, d (k1) также является решением (2) относи-
тельно i, но не обязательно наименьшим! Отсюда мы заклю-
чаем, что d (k) есть монотонно неубывающая функция k. Сле-
довательно, мы должны исследовать значения i в порядке
возрастания и проверять каждый раз, выполняется ли условие
i= d (k) для одного или для нескольких значений k. Более·
точно, пусть j (i) для данного i будет максимальным значени-
ем ~N-i, таким, что
(Aj :О< <j +
j (i) : р (j) = р (i Л)
тогда d (k) = i для. всех k, таких, что k-i~j (i) (или-
k~j (i) + i), и для которых не существует решения d(k)<i-
182 Глава 18

Так как мы будем исследовать все значения i в порядке их


возрастания и значение, являющееся минимальным решением,
-будет представлять очередное значение монотонно неубыва-
ющей функции d, k удовлетворяет условию
d .вег <
k-< j (i)+i
н мы получаем следующую программу
«инициалиёироватъ а»:
begin glocon р, N; vlrvar d; privar i;
d vir цел аггау; i vir цел : · {l)Д;
do а.вег =1= N :-+
begln glocon р, N; glovar d, i; prlvar j;
j vir цел : =О; i : = i 1; +
do j < +
N - i cand р (j) = р (i j)-+j : = j +1 od;
do d .вег < j +i-+.d: вверх (i) od
end
od
-end

-Упражнения

1. Дать формальное доказательство правильности приве-


.денной выше инициализацин .
.2. Используя «r, k: =r+d (k), r-d (k) » для O<k, наш ал-
горитм корректирует r и k, не изменяя .r+k. Исследуйте не-
значительный выигрыш, который можно получить, для
O<k<N, если известно, что все х могут иметь только два раз-
личных значения. (Конец упражнений.)
Примечание. Я считаю, что время выполнения нашего по-
следнего алгоритма будет расти пропорционально М +N.
Если задаться целью найти, если возможно, алгоритм с такой
производительностью, то его действительная разработка не
потребует никаких чрезмерных усилий; решающим моментом
представляется отказ удовлетвориться (без дальнейших ис-
'следований) очевидным алгоритмом с временем выполнения,
пропорциональныи М * N, разработку которого я предоставил
читателям в качестве упражнения. Однако небольшое изме-
нение формулировки задачи позволит нам разглядеть здесь
.использование общего принципа, который можно было бы
Задача поиска по образцу 183-

назвать «поиском малого супернабора». Предположим, что


нам нужно было не подсчитывать число вхождений одной по-
следовательности в другую, а сгенерировать последователь-
носгь г-значений, для которых функция совпадение (r) истинна,
Когда программа должна генерировать элементы набора·
А, возникают .в первом приближении только две ситуации .
Либо мы располагаем простой и определенной «функцией сле-
дующего», при помощи которой можно получить следующий
элемент А,- и тогда генерация всего набора выполняется три-
виально посредством повторного применения этой функции -
либо мы не располагаем подобной функцией. В этом случае
обычно прибегают к генерации элементов некоторого набора
В, такого, что
1. Каждый элемент А является также элементом В.
2. Существует генератор последовательных элементов В.
3. Существует способ проверки, принадлежит ли элемент В
также и набору А.
Алгоритм генерирует и проверяет по очереди все эле-
менты В.
Чтобы, пользуясь этим методом, получить удовлетвори-
тельную проиэводительностъ, необходимо выполнение трех
условий:
1. Генерация элементов набора В должна быть разумно
эффективной.
2. Проверка принадлежности элемента набора В набору А
также должна быть разумно эффективной (особенно в том
случае, когда элемент В не принадлежит А, так как В обычно
на порядок больше А).
3. Набор В не должен быть излишне большим.
Тот, ктоимеет большой опыт решения подобных задач, бу-
дет, исходя из вышесказанного, искать набор В, меньший, чем
это представляется очевидным. В данном примере таким оче-
видным набором является набор всех г-значений, удовлетворя-
ющих условию O~r~M-N. Заметим, что в предыдущей гла-.
ве замена набора «qq» гораздо меньшим набором «qqq» была
еще одним применением принципа поиска малого супернабора.
И наряду с «вынесением отношения из конструкции повторе-·
ния» это иллюстрирует второе стратегическое сходство между
решениями, представленными в настоящей и предыдущей гла-
вах. (Конец примечаниял
19 ПРЕДСТАВЛЕНИЕ ЧИСЛА
В ВИДЕ СУММЫ ДВУХ КВАДРАТОВ

Предположим, что нам требуется составить программу, ко-


торая для любого заданного r~O будет получать все сущест-
венно различные формы представления r в виде суммы двух
квадратов, или, более точно, будет генерировать все пары
{х,у),такие, что
( 1)
Ответом должны быть две векторные переменные хо и yv,
такие, что для i от хи.ниг ( = уv.ниг) до хо.вег ( = уv.вег)
пары (xv (i), yv (i)) будут представлять все решения ( 1). Что-
бы быть уверенным, что наш последовательный алгоритм
получает все решения ( 1), воспользуемся стандартным спо-
собом упорядочивания решений по какому-либо признаку.
Я предлагаю располагать их в порядке возрас.тания значения
х (поскольку никакие два различных решения не имеют одно-
го и того же значения х, такое упорядочивание однозначно).
Мы предлагаем сохранять инвариантным следующее отно-
шение:
Р 1: хо ( i) будет монотонно возрастающей функцией с той же
областью определения, что и монотонно убывающая функ-
ция yv (i), причем эти функции таковы, что пары (xv (i),
yv (i)) представляют собой все решения ( 1) при хо ( i) <х
Начальная истинность Pl легко обеспечивается, если ини-
циализировать хи и yv так, что их область определения пуста,
н выбрать х не слишком большим. Если пара (xv (i),
yv Gi)) является решением ( 1), мы всегда будем иметь
2• xv(i)2~xv(i)2+yv(i)2=r, и, следовательно, так как
хо (i) <х, наименьшее значение х~~, такое, что 2 * x2~r
не слишком велико. Наименьшее значение х можно устано-
вить при помощи теоремы о линейном просмотре. Однако,
поскольку для каждого хо (i) будет 'выполняться условие
..xv (i)2~r, мы знаем, что из истинности Pl and x2>r следует,
Представление числа в виде суммы двух квадратов 18S.

что все решения получены. Поэтому наш первый вариант


может быть таким:
b~gin glocon r; virvar хо, yv; privar х;
<
х vir цел : = О; do 2 * х2 r-+ х : = х 1 od; +
хи vir цел аггау, yv vir цел аггау : = (1), (1);
<
do х2 r-+ «увеличить х при инвариантности Pl » od
end
Из этой программы видно, что инвариантным отношением
является в действительности более сильное отношение
Р 1 ': Р l and 2 * х2 r >
Наивно было бы надеяться, что для каждого значения х
можно определить такое значение у, что х2+у2=,;, так как
такое значение вовсе не обязательно ·существует. Что мы дей-
ствительно можем сделать - это обеспечить истинность отно-
шения
+ <
х2 у2 r and х2 (у+ 1)2 + г >
Отсюда мы можем заключить не только то, что если
x2+y2=r, значит решение (1) было найдено, но также и то,
что если х2+ y2<r, значит для данного значения х не сущест-
вует значения .у, которое могло бы быть парой для х. Прини-
мая отношение
Р2:
в качестве инвариантного отношения для внутренней конструк-
ции повторения, получаем программу
«уве личить х при инвариантности Pl '»:
begiп glocon г; glovar хи, ьv, х; prlvar у;
у vir цел : = х; {благодаря истинности Р 1' стало истин-
ным Р2}
+ >
do х2 у2 r-+ у : = у - 1 od; { х2 у2 + <
г and Р2}
if х2+у2=r-+хv:вверх(х); уv:вверх (у); x==x+l
О x2+y2<r-+x:=x+1
fi
end
Замечая, однако, что последняя конструкция выбора не на-
рушит справедливости Р2, мы можем значительно повысить
'186 Глава 19

эффективность этой программы, если вынесем отношение Р2


внешней конструкции повторения:
·ИЗ

hegin .glocon г; vfrvar хи, yv; privar х, у;


х vir цел, у vir цел:= О, О;
do д:2+у2<г-+х, у:= х+1, у+1 od;
хи vir цел array, yv vir цел array := (1), (1);
do х2~ r-+do х2+ у2>г-+у := у-1 od;
if х2+ !i=r ~xv: вверх (х); yv: вверх(у);
x:=x+l
О x2+y2<r-+x:=x+I
fi
od
end
Последнее усовершенствование есть результат применения-
поиска малого супернабора для у; оно было достигнуто бла-
годаря вынесению отношения из конструкции повторения, а
именно отношения Р2.
Замечание. В качестве упражнения предлагается внести
такие очевидные усовершенствования, как проверка справед-
ливости r mod 4=3 и использование рекуррентного отношения·
(х+·1)2=х2+ (2*х+1). (Конец эамечаниял
Примечание. Приведенная выше программа, которой мы
обязаны Фейену, явно превосходит программу, составленную
нами несколько лет назад, когда, например, чтобы показать,
что ни одного решения не пропущено, всегда требовалось при-
бегать к рисунку. (Конец примечаниял
20 ЗАДАЧА О НАИМЕНЬШЕМ ПРОСТОМ
МНОЖИТЕЛЕ БОЛЬШОГО ЧИСЛА

В этой главе мы займемся задачей определения наимень-


шего простого множителя большого числа Ы;» 1 (под «боль-
шим» я понимаю здесь число порядка, скажем, 1016), исходя
из предположения, что программа предназначается Для малой
машины, которая выполняет операции сложения и сравнения
гораздо быстрее, чем проиэвольные операции умножения и
деления. (В наше время такое предположение вполне реально
для большинства так называемых «мвии-машинэ: описывае-
мый алгоритм был разработан несколько лет назад для устрой-
ства, которое, несмотря на его физический размер, сегодня
можно было бы назвать «микромашиной».)
Из непосредственного применения теоремы о линейном про-
смотре следует, что мы должны исследовать простые числа
в качестве возможных множителей в порядке их возрастания.
Поскольку составное число имеет по крайней мере один про-
стой множитель, не превосходящий корень квадратный из это-
го числа, наше исследование должно вестись не дальше этого
корня; если множитель еще не будет найден, это означает, что
число N простое. На этих рассуждениях основан следующий
алгоритм:
begin glocon. N; virvar р; privar f;
f vir~цел : __: 2;
do Nmodf =I:= О and f2<N-+
«увеличить f до следующего простого числа»
od;
if Nmod/ =1=0-+ р vir цел :=N
О Nmod/=0-+p vir цел:= f
fi
end
187
188 Глава 20

В этом алгоритме, однако, есть еще над чем подумать, по-


тому что пока неясно, как мы собираемся увеличивать f до
следующего простого числа.
Мы предположили, что составляем программу для малой
машины, а это исключает возможность хранения таблицы
последовательных простых чисел до 108• (Если хранить такую
таблицу, не прибегая ни к каким ухищрениям, потребовалось
бы 5 * 101 битов, а это совсем не то, что называется -даже
сегодня! - «малой памятью».) Вместо того чтобы определять
следующее простое число по хранящейся в памяти таблице,
его можно было бы вычислять; но если идти обычным путем,
пришлось бы, вообще говоря, проверять последовательность
1+1, 1+2, 1+.з, ...
пока не встретится первое простое число. Но проверка, явля-
ется ли число простым, сводится обычно к вопросу о том, рав-
но ли оно своему наименьшему простому множителю!
Существует возможность очень просто разрешить эту дн-
.лемму. Для Н;» 1 наименьший простой множитель числа N
является также наименьшим натуральным числом ~2, на ко-
торое делится N. Это свойство позволяет нам найти наимень-
ший простой.множитель числа N, не обращаясь больше к кон-
цепции «простого числа»:
begin glocon N; virvar р; privar f;
f vir цел:= 2;
doNmod/::;=O and (/+1)2<N-/:=/+1 od;
if Nmod/ .=1=0--р vir цел :=N
О Nmod/=0-p vir цел:=/
fi
end
Основная неприятность, связанная с этим алгоритмом,
сбъясняется тем, что только операции сложения и сравнения
мы считали быстрыми, но допустили, что вычисления N mod f
и (f+ 1)2 могут быть такими медленными, что их по возмож-
ности следует избегать во внут.реннем цикле.
Единственный выход из положения, как кажется, состоит
в том, чтобы найти способ применить прием «вынесения отно-
шения из конструкции повторения», т. е. попытаться хранить
такую информацию, чтобы после вычисления r= N mod f
можно было на ее основе вычислить следующее значение r
(для f + 1). Что же мы можем хранить?
Для начала можно воспользоваться тем, что r=N mod f
есть решение уравнения
N=f •q+r and O<r<f
Задача о наименьшем простом множителе большого числа 189

и мы мог ли бы хранить q. Тогда мы знаем, что


N =(! + 1) *q+(r-q)
и, вообще говоря, можем рассчитывать на «выигрыш» в том
смысле, что (r-q) будет ближе к нулю, чем исходное N, и,
следовательно, от него «легче» получить остаток по модулю
(f + 1). Но для меньших значений f (и г) и, следовательно,
для больших значений q мы не можем рассчитывать на боль-
шой выигрыш.
· Что касается нового значения r,a именно (r-q) mod (f+l),
~о нас, оказывается, совсем не интересует само значение
q! Нам точно также подошло бы любое меньшее значение.
равное а по модулю (f + 1), а уменьшение r на это значение
причинило бы нам меньшее беспокойство. Иначе говоря, мы
предпочли бы уменьшать r не на q, а, скажем, на q mod (f+ 1).
Тогда почему бы нам не хранить это значение? Повторяя наши
рассуждения, приходим к следующим уравнениям:
N=f* %+r0 O<.r1<f+i
q0=(f +
1) *q1 1 +г для О <.i-<:. п.
q1=(f +2) *q2+r2

qn-1=(/ +п) *qп+rп


qп=О
Искяючая все q, получаем
N=ro+
f *Г1+
+
f * (/ 1) * r2+
j * (/ + 1) * (/ +2) * r3+
+ + +
f * (! 1) * (! 2) * ... * (! п - 1) * r п ( 1)
откуда ясно видно, как N полностью определяется через f и
конечную последовательность r.
Заменяя f на f + 1 и компенсируя приращение, появляю-
щееся в каждой строке, уменьшением предшествующего ей
члена, мы получа ем другое представление N:
N =(r0-r1 )+
(! +
1) * ( r 1 ~ 2 * r 2) +
(/ +
1) * (/ +2) * (r2-3 * r3)+
(! +
1) * (! +2)* (! +з) * (r;-4 * r )+
4

(! + 1) * ( f + 2) * (! + 3) * ... * (! + п) * r,,
190 Глава 20

Этому преобразованию соответствует программа


/:=/+1; i:=O;
do i<п-r1:=r1-(i+1)*rн1; i:=i+l od

но, однако, при этом могут нарушиться неравенства


O-<r1~<f+i
так как r может стать отрицательным. Но это дело поправи-
мое, поскольку из (1) видно, что приращение r0:=r0+f можно
компенсировать уменьшением r1:=r1-l. В общем случае
ri:=ri+ (f+i) компенсируется при помощи rн1:=rн1-1.
В результате полное преобразование правильно описывается
программой
/:=/+1; i:=O;
do i<п~
Г1 := Г1-(i+ 1) * Гн1;
do r1 <О-:-+
Г1: = r1+ (/ +i); rн1: = Гrн -1
od;
i == i_+ 1
od;
do rп=О--+п := п-1 od
В предположении, что умножение малых целых чисел не
представляет серьезной проблемы - оно может выполняться
путем повторного, сложения,- вычисление последовательных
значений N mod f может быть реализовано при помощи набора
допустимых операций. Кроме того, проверка (f2<N в нашей
первой версии и (f + 1) 2~N в следующей версии), имеет ли
смысл продолжать поиск или мы уже достигли значения кор-
ня квадратного из N, может быть заменена проверкой п;» 1"
так как (п::::;;;l)=Ф (N<(f+l)2).
Полагая ar(k) =rk для О~k~аr.вег, мы приходим к сле-
дующей программе:
begin glocon N; virvar р; privar f, ar;
Ьegin glocon N; virvar ar; privar х, у;
ar vir цел аггау: = (О); х vir цел, у vir цел:=
N, 2;
do х =1= О--+ ar : вверх (х' mod у); х, у:= х div у"
y+l od
Задача о наименьшем простом множителе большого числа 191

end {выполнена инициализация аг};


f vir цел:= 2 {отношение (1) стало истинным};
do аг (О) =1= О and ar. вег >
1 --+
begin glovar f, ar; privar i;
+
f: = f 1; i vir цел:= О;
do i =1= аг. вег -
begin glocon /; glovar аг, i; pricon j;
j vir цел:= i+ 1; ах : (i)==ar(i)-
j * аг (j);
do аг (i) <
0-аг: (i) =ar(i)+ f +i;
ar : (}) = аг (j) - 1
od;
i:= j
end
od
end·
do ~г.верх=О-аг: сверху od
od;
if аг (0)=0--+ р vir цел:= f
О аг (О),=!= О- р vir цел:= N
fi
end ·
Примечание 1. Некоторые могут подумать, что можно
вдвое ускорить программу, если рассматривать отдельно чет-
ные и нечетные значения N. Тогда для нечетных N мы могли
бы ограничиться последовательностью f-значений 3, 5, 7, 9,
11, 13, 15, ...; операция
Г1 := Г1-(i+ 1) *Г1+1
превращается тогда в операцию
Гz := Г1-2* (i+ 1) * Г1+1
и это насилие над ri приведет к тому, что число прохождений
по следующему за операцией циклу, который вновь приводит
ri к нужному интервалу, возрастет в среднем в два раза. Так
что мы вряд ли получим улучшение программы, но зато ис-
портим формулу. (Конец примечания 1.)
Примечание 2. Хорошо известный способ обнаружения тех
мест в программе, где можно приложить энергию, направлен-
ную на оптимизацию программы, состоит в том, что при со-
ставлении программы в ней предусматриваются такие дейст-.
вия, которые позволят по завершении программы получить
распечатку полной гистограммы, указывающей, сколько раз
выполнялся каждый оператор; тем самым определяется, где
192 Глава 20

нужно оптимизировать программу. Если применить этот спо-


соб к нашей программе, мы увидим, что значительную часть
времени поглощает внутренний цикл, приводящий ar(i)
к нужному интервалу, особенно когда относительно много
времени уходит на операции над векторными переменными и
па получение значений функций от этих переменных. Для
данного примера такая информация не только тривиальна, но
и вредна в том смысле, что этот внутренний цикл и не нужно
оптимизировать! (Оптимизируя его, сокращая количество
«индексов», что легко можно сделать, если ввести две скаляр-
ные переменные ari и arj, равные соответственно ar (i) и ас (j)?
мы получим более длинный и более беспорядочный текст.)
Дело в том, что вычисления поглощают много времени ТОJ]ЬКО
тогда, когда N - простое число или когда его наименьший
простой множитель близок к его квадратному корню. В этом
случае на протяжении основной части времени вычислений мы
будем иметь аr.вег=2 (а именно начиная с того момента, ког-
да f минует значение корня кубического из N). Мы можем
заменить предохранитель внешнего цикла на
ar (О) =1= О aod ar. вег >2
и воспользоваться на заключительном этапе тремя скалярны-
ми переменными rO, rl и r2. В составление этой заключитель- ·
ной части программы можно вложить всю свою изобретатель-
ность. (Конец примечания 2.)
Примечание 3. Если вычисления дают нам p=N, этот ре-
зультат очень трудно проверить. Даже если программу вы-
полняет машина, которую нельзя назвать сверхнадежной, наш
алгоритм позволяет значительно повысить степенв уверенно-
сти в полученном результате: после завершения вычислений
должно иметь место отношение N =f * r1 +;о! Если какая-то из
арифметических операций была выполнена неверно, то очень
маловероятно, что, несмотря на это, последнее равенство бу-
дет иметь место. С точки зрения надежности этот алгоритм,
где новое значение r всегда вычисляется как функция старого
значения, имеет существенное преимущество перед алгорит-
мом, где очередное простое число, проходящее-проверку в ка-
честве возможного множителя, выбирается из таблицы: табли-
ца может иепортиться, и, кроме того, каждая проверка на де-
лимость представляет собой совершенно отдельные вычисле-
ния. Стоит отметить, что такой огромный выигрыш в надеж-
ности стал возможным благодаря «вынесению отношения из.
конструкции повторения» и ничему больше. Алгоритм, опи-
санный в этой главе, использовался не только для получения
высоконадежного разложения на множители (типа p=N), но
Задача о наименьшем просюм множителе большого числа 193

также и для проверки надежности арифметического устрой-


ства машины. (Конец примечания 3.)
Утешение. Тем из моих читателей, кому эта глава показа-
лась трудной, приятно будет узнать, что даже моим ближай-
, шим сотрудникам понадобилось больше. часа, чтобы «перева-
рить» ее: программы могут быть очень компактными. (Конец
утешения.)

7-6
21 ЗАДАЧА О САМЫХ УДА.[IЕННЫХ СЕЛЕНИЯХ

Рассмотрим п селений (п> 1), перенумерованных с О до ·


tl-1; для O~i<n и O~j<n задана вычислимая функция
f (i, .f}, удовлетворяющая для некоторой заданной положи-
тельной константы М условию
для i -:::pj:O<f(i, Л<М
для i=j: / (i, j)=M
Для. i-ro селения степень его удаленности «Ы (i) » задается как
idi(i)=минимум f (l, Л=минимум / (i, Л
. J+l }
«.
{Здесь f j) можно интерпретировать как расстояние от i до
j; правило] (i, /) =М было добавлено с целью упрощення.)
Требуется определить набор максимально удаленных селе-
ний, т. е. набор значений k, таких, что
(А h: О~ li < <
п : id (h) id (k))
Предполагается, что программа представит этот набор вначе-
ний в виде последовательности
miv (miv. н,иz.), .•. , пи» (miv. веz.)
Заметим, что, вообще говоря, возможны все значения
l~miv.oбл~n.
Очень простая и бесхитростная программа вычисляет по-
следовательно п степеней удаленности и сохраняет максималь-
~ое из полученных значений. В соответствии с границами
f (i, j) мы ·можем взять в качестве минимума для пустого на-
бора значение М и в качестве максимума для пустого па-
бора-О.
begin glocon п, М; vlrvar пи»; __ prlvar тах, l;
пи» vir цел array: = (О); тах vir цел, i vlr цел : = 0 О; 1

do i =1=п-
Задача о самых удаленных селениях 195

begln glocon п, М; glovar miv, тах, i; privar min, );


тй: vlr цел, j vlr цел:= М, О;
do j =f=. n-+
do f (i, Л < пип-» пип := f (i, Л od;
j:= j+ l
od {min=ia (i)}~
lf max>min-+nponyrmumь
О тах=; тйп=» miv: вверх (i)
О тах < тiп-+ miv : = (О, i); тах := min
f i;
i :=i+ 1
end
od
end
Приведенная программа очень безыскуена: в .самом внутрен-
нем цикле значение тлп при каждом прохождении по циклу
монотонно не возрастает, и следующая эа ним конструкция
выбора будет одинаково реагировать на любое значение пип,
удовлетворяющее условию пип-с тах. Из двух этих замечаний
мы делаем вывод, что продолжать внутренний цикл имеет
смысл только до тех пор, пока min;;::. тах. Следовательно, мы
можем заменить строчку «do j=l=n-+» на
<<do j =F п. and min >- тах-+>>
и комментарий, следующий за соответствующим od, на
{id(i)--<min<max or id(i)=min>-max}
Назовем эту модификацию «оптимизация 1 ».
Совсем по-другому можно оптимизировать программу,
сели известно, что
f (i, j)= f (i, i)
и тогда, учитывая, что на вычисление f затрачивается много
времени, не будем вычислять f (i, j) для тех значений аргу-
ментов, для которых уже вычислено значение f (j, i). Мы мо-
жем так усовершенствовать первоначальную программу, что
для каждой неупорядоченной пары аргументов соответствую-
щее !-значение будет вычисляться только один раз. М1а1 достиг-
7-...
196 , ГЛ(!,/Щ 2J

нем этого; если вместо начального значения, равного нулю,


будем каждый раз придавать j значение i+ 1, т. е. будем. так
сказать, просматривать только верхний треугольник симмет-
ричной матрицы расстояний. В этом случае программа обе-
спечит правильное вычисление min только при условии, что
начальным значением min будет не М, а
минимум ! (i, h)
O<h<l

Чтобы реализовать такую инициализацию, введем· массив,


скажем Ь, такой, что для k, удовлетворяющего условию
i~k<n,
для i=O: Ь (k)~M
для i>О:Ь(k)=минимум f(k, h)
O<:h<l

(В словесной фермулировке: Ь (k) есть минимальное из свя-


занных с селением k расстояний, вычисленных к настоящему
моменту.)
Программа, полученная 8 результате оптимизации 2, так-
же довольно бесхитростна.
qegin glocon п, М; virvar miv; privar .тах, i, Ь; ·
. miv vk Цел array: = (О); тах vir цел, i vlr цел : = О, О;
..:Ь vlr цел array :=(О); do Ь.обл =l=n -- Ь: вверх (М) od.;
do i '=1= п-« -
begi·n glocon п; glovar miv, тах, i, Ь; privar пип; };
min vir цел, Ь : никон; j vir цел : = i 1; +
do j# п--·
begin glocon i; glovar min, [, Ь; privar f /;
ff vir цел:=/(i, });
do // <тin-min:=ff od;
do f f <
Ь (j) -- Ь : (j) = f f od;
j:·=j+I
end
od{min=id (i)};
>
if тах тiп- пропустить
О тахьетйс-«miv: вверх (i)
О max<min--miv :=(О, i); тах := min
fi;
i == i+ 1
end
od
end
Задача о самых удаленных селениях 197

При попытке объединить обе эти оптимизации возникает


н.екоторая проблема. При оптимизации 1 сканирование строки
матрицы расстояний прекращается, когда min становится до-
статочно малым; при оптимизации 2, однако, сканирование
строки есть 'Также сканирование столбца, и это сделано, чтобы
хранить последние полученные значения Ь (k). Давайте при-
меним оптимизацию 1 и заменим строчку «do j =l=n-+» на
«по j =i=- п and min > тах ->>
Теперь самый внутренний цикл может завершиться при
j<n; значения Ь (k) для j~k<n, изменение которых еще мо-
жет представлять интерес,- это значения, удовлетворяющие
условию Ь (k) ~тах, остальные уже достаточно малы. Мы по-
лучим желаемый результат, если вставим в программу сле-
дующую конструкцию:
do I=/.= п-
if Ь (j) <
тах _,, j : = j +1
>-
О Ь (j) тах _,,
begin glocon i; glovar J, Ь; privar / /;
// yir цел:=/ (i, });
do / / <
Ь (J) -- Ь : (Л = / / od;
j:=j+I
end
fi
od
Лучше всего вставить эту конструкцию непосредственно перед
«i: = i + 1 », но после корректировки тах; чем больше тах,
тем больше вероятность, что корректировка Ь (k) больше не
потребуется.
Мьi объединили две оптимизации, природа которых весьма
различна. Оптимизация 2 состоит в том, что мы избегаем по-
вторного выполнения работы, про которую известно, что она
уже' сделана, и эффективность этой оптимизации известна
заранее. Оптимизация 1, однако,- это прием, эффективность
которого зависит от неизвестных значений f; это один из мно-
гих возможных однотипных приемов.
Мы ищем те строки матрицы расстояний, чей минимальный
элемент S превосходит минимальные элементы остальных
строк, и идея оптимизации 1 состоит в том, что для этой цели
нам не нужно вычислять действительный минимум для осталь-
ных строк, если мы можем найти Для каждой строки верхнюю
границу В, ее минимума, такую, что Bi.<S. На некоторой про-
межуточной стадии вычислений для некоторой строки (строк)
минимум S известен, поскольку все ее (их) элементы уже вы-
числены; для других строк известна только верхняя граница
7*-6
198 Глава 21

Вь Теперь совершенно ясно, что нам нужно выработать опре-


деленную стратегию. Должны ли мы сначала вычислить наи-
меньшее число дополнительных элементов матрицы, которые
еще потребуются для определения нового минимума, в надеж-
де, что он окажется большим; чем тот минимум, которым мы
располагаем, и следовательно.: превысит некоторые В? Или
мы должны сначала вычислить неизвестные элементы в стро-
ках с большим В в надежде добиться дешевой ценой снижения
этой верхней границы? Или нужно как-то объединить эти под-
ходы?
В моей первоначальной версии, объединявшей два· подхода"
«обновление оставшихся Ь (k) » откладывалось на некоторый
более поздний момент в надежде, что тем временем тах ста-
нет больше, но сомнительно, чтобы эта программа была более
эффективной, чем та, которая приведена в настоящей. главе.
Конечно, она была более сложной, ей требовался еще один
массив для хранения последовательности номеров селений. Я
пришел к публикуемой версии только тогда, когда уже писал.
эту главу. . 1
Глядя в прошлое, я понимаю, что зря потратил свою изо-
бретательность на мою первоначальную программу: если она
и была «более эффективной», то это могло быть только «в.
среднем». Но что значит в среднем? Определить это можно.
только при условии, что мы постулируем (совершенно произ-
вольно!) распределение вероятности для матрицы расстояний
f(i, j). Но в мои намерения- совсем не входила подгонка алго-
ритма под специальный подкласс матриц расстояний!
Мораль такова: составляя универсальную программу, мы
должны удерживаться от искушения применить хитроумную
стратегию, дающую повышение производительности в случаях,
которые могут никогда не встретиться, если такая стратегия
значительно усложняет программу: простота программы-:- это
гораздо менее расплывчатая цель. (Сложность в том, что мы
часто так гордимся своей хитроумно.й стратегией, что отказ
о'Т нее причиняет нам боль.)

Примечание. Наша окончательная программа сочетает в


себе две идеи, и мы пришли к ней, рассматривая сначала, таk
сказать, в качестве «переходных ступеней» две программы,
каждая из которых основывалась только 1:1а одной из идей. Во
многих случаях я нахожу такие переходные ступени весьма
полезными. (Конец примечания.)
22 ЗАДАЧА О КРАТЧАЙШЕМ
QОКРЫВАЮЩЕМ ДЕРЕВЕ

Две точки можно соединить одной простой связью; три


точки можно соединить вместе двумя простыми связями, в
общем случае N точек могут быть соединены в одно целое
N-1 простой связью. Такое множество вэаимосвязей называ-
ется «деревом», или, если мы хотим подчеркнуть, что- оно со-
единяет все N точек друг с другом, «покрывающим деревом»,
а сами связи называются «его ветвями». Кейли первым дока-
зал, что количество различных деревьев для заданных N
;rочек равно NN-2 •. (Проверьте истинность данного утвержде-
ния при N = 4, не советую проверять для N = 5.)
Далее будем предполагать, что задана длина каждой из
возможных N * (N-1)/2 ветвей. Определив длину дерева как
сумму длин его ветвей, мы можем задать вопрос: как найти
самое короткое дерево для заданных N точек? (Пока будем
считать, что заданные длины таковы, что кратчайшее дерево
единственно.)
Замечание. Рассматриваемые точки не обязательно долж-
ны принадлежать евклидовой плоскости, а заданным длинам
разрешается не иметь никакого отношения к евклидову рас-
стоянию. (Конец эамечания.)
Одно из очевидных и бесхитростных решений заключается
в том, чтобы последовательно получить все деревья для N
точек, определить их длину и отобрать самое короткое. Но,
согласно теореме Кейли, с увеличением N этот метод быстро
становится очень невыгодным, а затем и совершенно неприем:
лемым. Нам бы хотелось найти более эффективный алгоритм.
Сталкиваясь с проблемами, подобными этой (а нам с такой
проблемой уже пришлось иметь дело при решении задачи о
голландском национальном флаге), часто оказывается пло-
дотворным, учитывая то, что мы пытаемся построить последо-
вательный алгоритм, внимательно рассмотреть промежуточ-
ные состояния, которые могут возникнуть в процессе выч.исле-
ния. Так как конечный результат является набором из N-1
ветвей, принадлежность которых самому короткому дереву
200 Глава 22

будет - мы надеемся -устанавливать:Ся поочередно, нам сле-


дует разобраться в том, что можно сказать, когда уже извест-
но некоторое количество ветвей искомого дерева.
Сразу же необходимо решить, будем ли мы иметь дело с
общим случаем, когда известно произвольное подмножество
этих ветвей, или ограничимся только некоторыми типами
подмножеств, надеясь, что они окажутся как раз теми под-
множествами, которые встретятся в наших вычислениях, и
надеясь также, что это ограничение упростит наш анализ.
Такой выбор представляется по крайней мере тогда, когда мы
имеем в виду «естественный» тип подмножеств. В нашем при-
мере напрашивается естественный тип подмножества, опреде-
ляемый так, что выбранные ветви распределены между точ-
ками не случайным образом, а
тоже образуют дерево. Так как
этот специальный случай и на самом деле может оказаться
проще общего случая, мы зададим наш вопрос в другой форме.
Можно ли извлечь какую-нибудь пользу из того, что нам
известно некоторое поддерево искомого кратчайшего дерева?
В дальнейшем 'будем предполагать, что ветви уже опреде-
ленного поддерева и точки, связываемые ими, выкрашены в
красный цвет, а все оставшиеся точки выкрашены в синий
цвет. Можем ли мы найти еще одну ветвь, которая также
должна- принадлежать кратчайшему дереву? Так как резуль-
тирующее дерево соединяет все точки друг с другом, оно долж-
но содержать по крайней мере одну ветвь. соединяющую крас-
ную точку с синей. Назовем ветвь между красной и синей
точкой «фиолетовой ветвью». Напрашивается очевидное пред-
положение о том, что кратчайшая фиолетовая ветвь Принад-
лежит и кратчайшему дереву.
Правильность этого предположения нетрудно доказать.
Рассмотрим некоторое дерево Т для N точек, которое содер-
жит все эти красные ветви, но не содержит самую короткую
фиолетовую ветвь. Добавим к нему самую короткую фиоле-
товую ветвь. При этом образуется цикл, содержащий по край-
ней мере одну красную и одну синюю точку, т. е. цикл, кото-
рый, следовательно, содержит по крайней мере еще одну
(более длинную) фиолетовую ветвь. Удалим из цикла такую
более длинную фиолетовую ветвь. Получающийся в резуль-
тате граф также является деревом. В дереве Т мы заменили
некоторую фиолетовую ветвь более короткой, и поэтому де-
рево Т не могло быть кратчайшим. (Дерево для N точек есть
граф, обладающий следующими тремя свойствами:

1. Оно соединяет друг с другом. N точек.


2. У него N-1 ветвей.
3. У него нет циклов.
Задача о кратчайшем покрывающем дереве 201

Из любых этих двух свойств следует, что граф является дере-


. вом и, следовательно, обладает и третьим свойством.)
Но теперь мы можем представить общий контур нашего
алгоритма. при условии, что нам удастся найти начальное крае-
ное дерево. Как только мы это сделаем, мы можем выбрать
самую короткую фиолетовую ветвь, покрасить ее и синюю
точку на ее конце в красный цвет и т. д. до тех пор, пока
красное дерево не станет таким большим, что не останется
ни одной синей точки. Чтобы начать этот процесс, достаточно
покрасить _произвольную точку в красный цвет:
покрасить произвольную точку в красный цвет, а оставшие-
ся точки в синий;
do число красных точек =!= N-+
выбрать самую короткую фиолетовую ветвь;
покрасить ее и синюю точку на ее конце в красный цвет
od
Теперь нашей главной задачей становится «выбор самой
короткой фиолетовой ветви», так как число фиолетовых вет-
вей может быть очень большим, а именно k * (N-k), где k -
число красных точек. Если «выбор самой короткой фиолето-
вой ветви» выполнять как отдельную операцию, потребуется
в среднем число сравнений, пропорциональное №, и объем
работы, выполняемой всем алгоритмом, будет пропорционален
№. Впрочем, замечая, что операция «выбрать самую короткую
фиолетовую ветвь» одна не встречается, а лишь как компонен-
та конструкции повторения, зададим себе вопрос, нельзя ли
применять метод «вынесения отношения из .конструкции по-
вторения», т. е. нельзя ли организовать программу так, чтобы
последующие выполнения оператора «выбрать самую корот-
кую фиолетовую ветвь» могли воспользоваться результатом
предыдущего. Имеются веские соображения в пользу того, что
это возможно, так как каждое множество фиолетовых ветвей
тесно связано с последующим: множество фиолетовых ветвей
определяется тем, как были разделены точки на красные и
синие, и изменение этого разбиения каждый раз связано с
тем, что одна синяя точка делается красной.
Желание сильно сократить время поиска при выборе самой
короткой ветви из некоторого множества означает сокращение
объема этого множества; другими словами, мы ищем подмно-
жество фиолетовых ветвей - назовем их «ультрафиолетовы-
ми»,- которое будет содержать самую короткую ветвь и ко-
торое можно использовать для передачи полезной информации
от одного выбора к следующему. Мы представляем себе про-
грамму такой структуры:
202 -Глава 22

покрасить произвольную точку в красный цвет, а остальные


в синий;
определить множество ультрафиолетовых ветвей;
do количество красных точек =l=N-+
выбрать самую короткую ультрафиолетовую ветвь;
покрасить ее и синюю точку на ее конце в красный цвет;
перевычислить множество ультрафиолетовых ветвей
od
где «ультрафиолетовый цвет» выбирается так, что
1. Гарантируется, что самая короткая фиолетовая ветвь
находится среди ультрафиолетовых.
2. Множество ультрафиолетовых ветвей в среднем гораздо
меньше множества фиолетовых.
3. Операция «перевычислить множество ультрафиолетовых
ветвей» относительно недорогая.
(Мы требуем выполнения первого свойства, поскольку тог-
да наш новый алгоритм будет также правилен; мы требуем
выполнения второго и третьего свойств, так как хотим, чтобы
новый алгоритм был эффективнее прежнего.)
Можем ли мы найти такое определение понятия «ультра-
фиолетовый»? За неимением других сведений, я могу только
предложить попробовать. Учитывая, что множество фиолето-
вых ветвей, идущих из k красных точек в N-k синих точек,
содержит k * (N-k) элементов и что должен соблюдаться наш
первый критерий, немедленно напрашиваются такие два оче-
видных возможных подмножества:
1. Для каждой красной точки кратчайшую фиолетовую
ветвь, оканчивающуюся в ней, сделайте ультрафиолетовой:
следовательно, множество ультрафиолетовых ветвей имеет k
членов.
2. Для каждой синей точки кратчайшую фиолетовую ветвь,
оканчивающуюся в ней, сделайте ультрафиолетовой; следова-
тельно, множество ультрафиолетовых ветвей имеет N-;k чле-
нов.
Нам нужно, чтобы ультрафиолетовое подмножество было
небольшим, но мы не можем сделать выбор исходя из размера:
в первом случае он будет меняться в пределах от 1 до N-1,
во втором - от N-1 до 1. Поэтому если мы и можем на что-то
решиться, то с учетом цены операции «перевычислить множе-
ство ультрафиолетовых ветвей».
Впрочем, не испытывая различные способы перевычисле-
Задача о кратчайшем покрывающем дереве 203

ния, можно сразу же высказать одно соображение в пользу


второго варианта. В первом варианте различные ультрафио-
летовые ветви могут идти к одной и той же синей точке, и
тогда мы знаем а priori, что самое большее одна из них будет
покрашена в красный цвет; во втором варианте каждая синяя
точка связана с красным деревом одним способом, т. е. крас-
ные и ультрафиолетовые ветви все время образуют покрываю-
щее дерево для N точек. Поэтому давайте внимагельно рас-
смотрим следствия второго определения понятия «ультрафио-
летовый».
, Рассмотрим состояние, когда у нас есть красное поддерево
Д и когда из множества соответствующих ультрафиолетовых
ветвей (в соответствии со вторым определением - я больше
не буду повторять это) самая короткая ветвь вместе со своей
первоначально синей точкой Р покрашены в красный цвет.
Число ультрафиолетовых ветвей, как ·и следовало ожидать,
уменьшилось на 1. Но правильны ли оставшиеся ветви? Для
каждой синей точки они представляют самую короткую связь
с первоначальным красным деревом Р, и следовало бы пред-
ставлять самую короткую связь с новым красным деревом
R+P. Этот вопрос решается одним простым сравнением для
каждой синей точки В: если ветвь ВР короче ультрафиолето-
вой ветви, соединяющей В с R, последняя должна быть заме-
нена на ВР, в противном случае рост красного дерева не ведет
к новому кратчайшему пути между ним и точкой В. В резуль-
тате мы видим, что и выбор кратчайшей ультрафиолетовой
ветви, и перевычисление множества ультрафиолетовых ветвей
являются операциями, стоимость которых пропорциональна N,
стоимость всего алгоритма растет как №,, и введение понятия
«ультрафиолетовый» в самом деле позволило нам получить
экономию, на которую мы надеялись.

Упражнение

Убедитесь, что нерассмотренная альтернатива для понятия


«ультрафиолетовый» менее плодотворна. (Конец упражнения.)
Так как операция «перевычислить множество ультрафио-
летовых ветвей» предполагает для каждой ультрафиолетовой
ветви проверку, нужно ли эту ветвь заменить или сохранить,
заманчиво объединить этот процесс и выбор самой короткой
ветви нового множества ультрафиолетовых ветвей в одном
цикле. Вместо того чтобы передавать следующему повторению
модифицированное множество ультрафиолетовых ветвей, бу-,
дем передавать немодифицированное множество вместе с
204 Глава 22

«пкц», номером точки, которая была последней выкрашена в


красный цвет. Задача инициализации может быть легко ре-
шена, если известна верхняя граница длин всех ветвей инф;
будем считать, что эта константа нам известна, ультрафиоле-
товым ветвям немодифицированного множества зададим на-
чальную длину, равную инф (и соединим синие точки с вооб-
ражаемой точкой с номером О).
Мы предполагаем, что заданные точки пронумерованы чис-
лами от 1 до N, а длина ветви между точками р и q задается
симметрической функцией «расст(р, q) ». Ответ нужно полу"
чить в виде дерева, состоящего из N-1 ветвей, причем ветви
. идентифицируются номерами своих конечных точек; то есть
ответ- это (неупорядоченное) множество (неупорядоченных)
пар чисел. Будем представлять их двумя массивами «из» и
«8» соответственно таким образом, что для 1 ~h~N-1
«uз(h)» и «8(h)» есть две конечные точки (их порядковые
номера) h-й ветви. В нашем окончательном ответе все ветви
будут упорядочены (по h); единственный порядок, · который
имеет смысл,- порядок их окрашивания в красный цвет. Это
наводит на мысль во .время вычисления использовать массивы
«из» и «8» для хранения и ультрафиолетовых ветвей с помо ..
шью следующих условий:
для 1 ~ h < k: h-я ветвь красная;
для k ~ h < N : п-« ветвь ультрафиолетовая.
Чтобы не перевычислять длины ультрафиолетовых ветвей,
введем локальный массив уфд («ультрафиолетовая длина»),
т. е.
для k ~ h < N: уфд (h) =длина h-й ветви.
Далее, поскольку существует однозначное соответствие
между ультрафиолетовыми ветвями и ~иними точками, то длJI
того, чтобы определить, какие точки этих ветвей синие, можно
договориться, что h-я ультрафиолетовая ветвь соединяет крас-
ную точку «ие(Е)» с синей точкой «в(h)».
В приводимой программе точка N - это произвольная точ-
ка, покрашенная вначале в красный цвет.
Замечание. По отношению к массиву «ИЗ» и скалярной пе-
ременной «куф» можно сделать упрек, что нам не удалось
избежать бессмысленных инициализаций; однако они относят-
ся к виртуальной точке О, находящейся на расстоянии «инф»
от всех других точек, и к такой же виртуальной нулевой ульт-
рафиолетовой ветви. (Конец эамечания.)
Задача о крахчайшем покрывающем дереве 205

begin glocon N, инф; virvar из, в; privar уфд, k, пкц;


из vir цел array:= (1); в vir цел array:=(l);
уфд vir цел аггау : = (1);
do из.обл=/= N -1-
из: вверх (О); в: вверх (из.обл); уфд: вверх (инф)
od;
k vir цел, пкц vir цел :=1, N;
do k =1= N-
begin glocon N, инф; gJovar из, в, уфд, k, пки; privar
куф, тлп, h;
куф vir цел, ппп vir цел, h vir· цел:= О, инф, k;
do h=/=N-
be_gin glocon N, в, пкц; glovar из, уфд, куф, min, h;
privar д лн,
длн vi·r цел.:= расст (пкц, в(h));
if длн «; уфд (h) - уф{): (h) =длн; из: (h) =nкц
>
О длн уфд (h)-длн := уфд (h)
fi {длн=уфд(h)};
do длн <тiп-тiп := длн; куф :=h od {min=
уфд (куф)};
h :=h+ 1
end
od {ветвь с номером куф есть кратчайшая ультра-
фиолетовая ветвь};
из: переставить(k, куф); в: переставить (k, куф);
уфд: переставить (k, куф);
пкц : =в (k); k: = k+ 1; уфд: снизу
end
od
end
Несмотря на простоту конечной программы - собственно
говоря, всего лишь цикл в цикле,- описываемый ею алгоритм
нельзя считать тривиальным. По существу это весьма эффек-
тивный алгоритм как в отношении использования памяти, так
и в отношении количества сравнений длин ветвей. Поэтому
стоит еще раз перечислить основные моменты, приведшие к
его обнаружению.
Первый решающий шаг был сделан тогда, когда мы попы-
тались ограничиться такими промежуточными состояниями, в
которых красные ветви, т. е. ветви конечного результата, .сами
образуют дерево. Преимущественный выигрыш состоит в том.
что тогда количество красных точек превышает количество
красных ветвей ровно на единицу и мы имеем право сделать
206 Глава 22

вывод, что ветвь из одной красной точки в .другую не может


никогда стать красной: произошло бы запретное замыкание
цикла. (А теперь становятся видны контуры еще одного алго-
ритма: рассортировать все ветви по возрастанию их длин и
обрабатывать их в этом порядке; причем обработка заклю-
чается в том, что, если обрабатываемая ветвь вместе с крас-
ными ветвями образует' цикл, ее надо отбросить, иначе ее
надо сделать красной. Очевидно, что этот алгоритм получает
элементы конечного результата в порядке увеличения длин
красных ветвей. Такой алгоритм менее привлекателен, по-
скольку, во-первых, нам нужно рассортировать все ветви, и,
во-вторых, проверка, не замыкает ли новая ветвь цикл, также
не слишком привлекательна. Эта проблема является темой
следующей главы.) Отсюда можно сделать вывод, что попыт-
ка достижения конечной цели посредством «простых» проме-
жуточных состояний обычно окупается!
Вторым решающим шагом явилась формулировка нашего
предположения, что самая короткая фиолетовая ветвь может
быть сделана красной. И здесь это предположение появилось
·не на пустом месте; если мы хотим «вырастить» красное дере-
во, нам нужно обращаться именно к фиолетовой ветви, а тогда
то, что вероятным кандидатом будет ветвь самая короткая,
вряд ли может показаться удивительным.
Наше решение не довольствоваться №-алгоритмом - ре-
шение, которое привело к понятию «ультрафиолетовый»,-
может показаться самым неожиданным. Нам могут возразить:
«Предположим, что мне не пришло в голову исследовать, не
могу ли я найти алгоритм получше?.» Что же, это решение
пришло тогда, когда у нас уже был алгоритм, и математиче-
ский анализ первоначальной задачи по существу завершен.
Мы занялись всего лишь оптимизацией, для которой никакого
более глубокого знания, например теории графов, не требо-
валось! Кроме того, эта оптимизация следовала хорошо из-
вестному образцу: вынесение отношения из цикла. Мораль
такова: когда. алгоритм сделан, не считайте, что работа над
ним уже закончена, а проверьте, нельзя ли его еще утрамбо-
вать. Трудно представить, чтобы понятие «ультрафиолетовый»
могло ускользнуть от внимания тех, у кого вошло в привычку
проводить такую работу.
Замечание. Из анализа алгоритма следует, что самое ко-
роткое покрывающее дерево единственно, если длины любых
двух различных ветвей неодинаковы. Проверьте, что если су-
ществует более одного кратчайшего покрывающего дерева, то
наш алгоритм может построить любое из них. (Конец эамеча-
ния.}
Задача о кразчайшем покрывающем дереве 207

Существует и совершенно другой алгоритм, который поме-


щает ветви в произвольном порядке, но каждый раз происхо-
дит проверка, не образуется ли при этом цикл, и тогда из него
удаляется самая длинная ветвь (или одна из самых длин-
ных).
23 АЛГОРИТМ РЕМА ВЫДЕЛЕНИЯ КЛАССОВ
ЭКВИВАЛЕНТНОСТИ

В общем случае точки графа (не обязательно являющегося


деревом) называются «вершинами», а связи между ними-
«ребрами»., а не ветвями. Граф называется «связанным» тог-
да и только тогда, когда он либо содержит только одну вер-
шину, либо между любыми двумя различными вершинами
существует по крайней мере один путь, проходящий по его
ребрам. Так как многие из возможных ребер графа с N верши-
нами могут отсутствовать, граф не всегда связан. Но каждый
граф, как связанный, так и несвязанный, можно всегда одно-
значно разделить на связанные подграфы, т. е: все вершины
графа можно раздели1:ь на такие подмножества, что любая
пара вершин из одного и того же подмножества связана, а
для любых двух вершин, взятых из двух различных подмно-
жеств, связывающего их пути не существует. (Для математи-
ков: «связанность» есть рефлективное, симметричное и тран-
зитивное отношение, которое, следовательно, порождает клас-
сы эквивалентности.)
Мы будем рассматривать N вершин, занумерованных от ()
до N-1, предполагая, что N достаточно велико (скажем,
10 ООО), и последовательность графов Go, 01, G2, ... , которые
получаются при связывании этих вершин ребрами множеств
Еь, Е1, Е2, ... , где Ео пусто, а Ен1 =Ei+ {ei}; ео, е1, е21 ... - есть
некоторая заданная последовательность ребер. Ребра ео,е1~
е21 ... должны обрабатываться в данном порядке, и после обра-
ботки п ребер (т, е. когда последнее из обработанных ребер,
если такое было, есть ребро en-1) для любой пары вершин
мы должны иметь возможность определить, является ли она
связанной в графе О« или нет. Основная задача, которую
нужно решить в этой главе, заключается в следующем: «Как
хранить в памяти всю существенную для нас информацию,
которую мы получаем, когда знаем последовательность ребер
ео, е1, ... , en-1 ?»
Конечно, мы могли бы хранить саму последовательность
ребер «е0, е1, ••• , en-1», но такое решение не очень хорошо с
Алгоритм Рема выделения классов эквивалентности 209

практической точки зрения, так как, во-первых, одновременно


,мы запоминаем и .много не относящейся к делу информации;
например, если мы уже обработали ребра {7, 8} и { 12, 7}, то
новое ребро {12, 8} не' говорит нам ничего нового! И, во-вто-
рых, ответ на вопрос: «Связаны ли вершины р и q?» - наш
главный вопрос!- спрятан слишком глубоко.
Как пример другой крайности мы могли бы хранить пол-
.ностью всю матрицу связности, т. е. N2 битов (или, если ис-
пользовать то, что эта матрица симметрична, N * (N + 1) /2 би-
тов), так что ответом на вопрос, связаны ли вершины р и q,
является булевское значение, хранимое как элемент матрицы
connp,q. (Нас не должно смущать то, что мы говорим о мат-
рице, хотя введенные нами векторные переменные более по-
хожи на векторы; мы могли бы ввести, скажем, переменную
«асоп» и воспользоваться следующим равенством:
connp,q=acon(N*p+q) для.О-<.р<N и O-<.q<N)
Я назвал это другой крайностью, потому что здесь на са-
мом деле запоминаются готовые ответы. на все возможные
вопросы вида: «Связаны ли вершины р и q»?; но запоминает-
ся чересчур много, так как большинство из тех ответов, вооб-
ще говоря, зависят друг от друга. Решение хранить информа-
цию в такой избыточной форме - достаточно серьезное дело,
так как, чем больше избыточной информации мы храним, тем
большие преобразования информации могут потребоваться
при обработке нового ребра.
Более компактный способ представления того, какие вер-
шины связаны друг с другом, состоит в указании (например,
перечислении) тех подмножеств, которые соответствуют теку-
щему разделению. Тогда вопрос о том, являются ли связанны-
ми вершины р и q, сводится к вопросу об их принадлежности
к одному и тому же подмножеству, т. е. к вычислению значения
логического выражения
подмножествосрл = подмножество(q)
Так как имеется самое большее N различных подмножеств, по-
требуется не больше чем N * log2 N битов (а не N2 битов пол-
ной матрицы связности).
Для ответа на вопрос о том, связаны ли вершины р и q,
было бы очень полезно ввести массив для хранения значений
функции «подмножество», но ... ее перевычисление может ока-
заться утомительной процедурой! В самом деле, предположим,
например, что подмножество(р) = Р, подмножесгво ( q) = Q и
P::i= Q, и предположим, далее, что очередное ребро, которое
нам нужно обработать, есть {р, q}. В конечном счете оба эти
подмножества должны быть объединены в одно, а это, если
210 Глава 23

сами подмножества уже достаточно велики, потребует значи-


тельных модификаций. Кроме того, если мы работаем только
с векторной· переменной «подмножество», то как нам найти,
например, все значения k, такие, что подмножество(k) = Q
(предполагая, что Р станет именем нового подмножества, по-
лученного в результате выполненного объединения), не пере-
бирая всю область определения функции «подмножество»?
Способ разрешения этой дилеммы заключается в том, что"
бы хранить не функцию «подмножество», а некоторую другую
функцию, скажем «[», обладающую следующими свойствами:
1. Для заданного значения р знание функции J позволяет,
по крайней мере в среднем, легко вычислить значение подмно-
жество(р).
2. При обработке нового ребра {р" q} обеспечена, по край-
ней мере в среднем, легкость перевычисления функции f.
Можно ли придумать такую функцию?
Учитывая, что основная информация, которая обрабатыва-
ется при введении нового ребра, состоит в том, что впредь обе
вершины 'будут принадлежать одному и тому же подмножест-
ву, напрашивается такое-определение функции f:
«вершина пr.k принадлежит тому же подмножеству, что и
вершина пr.f(k) »
т. е. как аргумент f, так и значение функции f интерпретируют-
ся как номера вершин. ·
Так как в начале работы Е0 пусто, то каждая вершина яв-
ляется единственным членом подмножества, которому она при-
надлежит, и, следовательно, наша функция должна быть ини-
циализирована следующим образом: ·
j (kJ=k для O.<_.k<N
и самым естественным наименованием каждого из N различ-
ных подмножеств будет.,.таким образом, номер той единствен-
ной вершины, которую это подмножество содержит. Очевидное
обобщение на случай подмножеств, состоящих из большего
числа вершин, заключается в отождествлении каждого под-,
множества с номером одной из содержащихся в нем вершин -
с которой именно, нас пока не интересует; принимая это усло-
вие, мы можем рассмотреть функцию f, такую, что '
если f (k) =k, то вершина пr.k принадлежит подмножеству
nr.k, и
если f(k) =:/=k, то вершина nr.k принадлежит тому же под-
множеству, что и вершина пr.f(k).
Последовательно применяя вычисление функции f, будем
Алгоритм Рема выделения. классов эквивалентности ~211

переходить от одной вершины подмножества к некоторой дру-


гой вершине того же самого подмножества. Если мы будем
следить за тем, чтобы этот процесс не стал циклическим, преж-
де чем мы найдем «идентифицирующую вершину подмноже-
ства», т. е. ту вершину, номер которой унаследован самим под-
множеством, то знание функции f действительно' позволит нам
вычислить подмножество(р) для любого значения р.
Более точно, в формальных обозначениях:
/О (р) =.; р
и для i>O
Ji (р) = / (ji-1 (р))
для каждой вершины р, принадлежащей подмножеству рв, су-
ществует значение j, т.акое, что
<
для i j: Ji (р)-=/= ps
для i "> } : р (р) = ps
Отсюда следует, что для О~ j 1 <j2 ~J: f JI (р) =1= f J2 (р) и кон-
струкция
«рз vir цел:= р; do ps =1= / (ps)-+ ps: = / (ps) od>>
завершит свою работу·при р зь- подмножесгво цц .
Тот факт, что функция f дает - возможно, после повторных
применений - для всех точек подмножества nr.qs одну и ту же
идентифицирующую вершину nr.qs, позволяет объединить это
подмножество с подмножеством nr.ps и идентифицировать
результат точкой nr.ps с помощью всего лишь очень неболь-
ших изменений в функции f; вместо равенства f (qs) =qs дол-
жно выполняться равенство f (qs) =ps. Обработка ребра
{р, q}, следовательно, может быт.ь сделана следующим опера-
тором:
begin glocon р, q; glovar /; privar ps, qs;
ps vir цел:= р; do ps =1= / (ps)-+ рз : = / (ps) od;
{рs=подмножество (р)}
qs vir цел:= q; do qs =1= / (qs)-* qs: = / (qs) od; {qs=
подмножество (q)}
j: tqs)=pS
end
Приведенный выше способ обработки нового ребра, хотя и
правилен, но не очень привлекателен, поскольку в худшем слу-
.чае может стать слишком неэффективным: эти циклы может
потребоваться повторять очень много раз. Кажется полезным
212 Глава 23

произвести «чистку дерева»; как для вершины р, так и для


вершины q мы теперь знаем текущие идентифицирующие вер-
шины и было бы жалко вновь и вновь прослеживать связан-
ные с ними пути. Чтобы исправить эту ситуацию, предлагаем .
следующий внутренний блок (мы также включили в' него по-
пытку уменьшить число вычислений функций f):

begin glocon р, q; glovar f; pricon ps;


begin glocon р, f; virvar ps; privar psO;
psO vir цел, ps vir цел:= р, f (р);
do psO-::/= ps-+ psO, ps := ps, f (ps) od
end {рs=подмножество (р)};
begin glocon р, ps; glovar /; privar psO, ps1;
psO vi~ цел, ps1 vir цел : = р, f (р);
do psO -=f=ps1-+ f :(psO)=ps; ps0,ps1 :, psl, f(ps1) od
end t «подчистка» вершин, которые встречаются. после
nr.p};
begin glocon q, ps; glovar /; privar qsO, qsl;
qsO vir цел, qs1 vir цел:= q, f (q);
do qsO =/= qs1-+
. f: (qsO)=ps;
.
qsO, qs1 := qsl,/(qsl) ~d;
/: (qsO)=ps
end [еподчвстка» вершин, которые встречаются после
nr. q, И объединение всего подмножества с пг . ps}
end

Упражнения
1. Дайте более формальное доказательство правильности
приведенного выше алгоритма.
2. Мы совершенно произвольно решили, что подмножество
(р) не изменяется и что подмножество(q) переопределяется.
Можете ли вы с выгодой использовать эту свободу?
(Конец упражнений.)

Рассмотренный алгоритм был включен не из-за его красоты.


На самом деле, я бы не удивился, если бы большинство моих
Алгорит.м Рема выделения классов эквиваленгносги 213

читателей остались им неудовлетворены. Например, очень не-


приятно, что совсем непросто произвести оценку того, что мы
выиграли, заменив его первую версию второй. Этот алгоритм
был включен по двум другим причинам.
Во-первых" это дало· мне прекрасную возможность обсу-
дить различные способы представления информации и проил-
люстрировать некоторые соображения экономичности. Во-вто-
рых, интересно отметить, что мы допускаем в качестве проме-
жуточного состояния неоднозначное представление текущего
разбиения (однозначного) и позволяем откладывать ликвида-
цию неопределенности и производить ее тогда, когда это будет
удобно. Я могу припомнить не один случай, когда использова-
ние неоднозначного представления приводило к находкам, ко-
торые вряд ли можно было обнаружить другим способом.
Когда Рем прочитал этот текст, он задумался - мое реше-
.ние показалось ему далеко неудовлетворительным - и вскоре
-показал мне другое решение. В некоторых отношениях алго-
ритм Рема настолько красив, что я не избежал соблазна и
включил его также. (Следующие рассуждения являются ре-
зультатом совместного обсуждения, в котором принимал уча-
стие 'и Фейен.)
В первом решении плохо то, 'что, начиная с р, путь к вер-
шине дерева проходится дважды: один раз для определения ps
1и второй раз для чистки дерева. Кроме этого, разрушается
симметрия между р и q.
Два просмотра пути из р были необходимы, так как мы хо-
тели произвести полную чистку дерева. Впрочем, не зная но-
мера идентифицирующей вершины, мы могли бы произвести
по крайней мере частичную чистку за один просмотр, если бы
знали направление в сторону «более чистого дерева». Поэто-
му предлагается использовать отношение порядка между но-
мерами вершин и для каждого подмножества выбрать в ка-
честве идентифицирующего номера, скажем, минимальное
значение; в таком случае, чем меньше f-значение, тем чище
дерево. Так как в начале f (k) =k для всех '/i,, а наша задача.
превратилась в задачу уменьшения f-значений, то нам следует
ограничиться представлениями разбиений, удовлетворяющим
неравенству
f(k)<k для O<k<N
У этого ограничения есть еще одно преимущество, заклю-
чающееся в том, что становится очевидным, что единственно
возможные циклы - это стационарные точки, для которых
f(k) =k (т. е. идентифицирующие вершины).
Для того чтобы иметь возможность выражаться более точ-
но; введем функцию «разб» и двуместную операцию « $ :..
214 Глава 23

разб (f) обозначает разбиение, представляемое функцией f.


раеб (f) $ (р, q) обозначает разбиение, получающееся из раз-
биения разб(f) в результате объединения в
одно подмножество. подмножества, содержа-
щего р, и подмножества, содержащего ·q. Тог-
да и только тогда, когда в разбиении разб (f)
вершины р и q уже содержатся в одном и том
же подмножестве, справедливо
JW-Зб (!) = разб (!) $ (р, q)
Обозначая начальное значение f через /нач. обработку реб-
ра (р, q) можно описать как обеспечение истинности отноше-
ния
R: разб (!)= разб (/нач)$ (р, q)
Для этого (как обычно) вводятся две локальные перемен-
ные, скажем рО и qO (или, если вам больше нравится, локаль-
ное ребро), удовлетворяющие отношению
Р: разб (!) $ (рО, qO)= разб (/ JШ14) $ (р, q)
что тривиально обеспечивается при такай инициализации
рО vir цел, qO vlr цел : = р, q
Далее, после обеспечения отношения Р, алгоритм должен
пересчитывать f, рО и qO при инвариантности Р, пока мы не
сможем заключить, что выполняется условие
Q: раэб (/) = разб (/) $ (рО, qO)
так как ( Q and Р) ::::ф я.
· Уравнение R в общем случае относительно f имеет много
решений, нам бы хотелось получить «самое чистое»; поэтому
предлагается искать такой процесс, при котором на каждом
шаге уменьшается по крайней мере 'одно {-значение. В этом
случае гарантируется, что процесс завершится (в качестве
монотонно убывающей функции MO?I<HO взять сумму Nf-значе-
ний), и мы должны попытаться найти достаточное количество
шагов, таких, что. условие ВВ, дизъюнкция предохранителей,
настолько слабогчто (non ВВ) Q.
Можно изменить значение функции f в точке рО, например,
так:
/ : (рО) =нечто
но, чтобы обеспечить эффективное убывание вариантной функ-
ции, это «нечто» должно быть меньше первоначального значе-
ния f (рО), т. е. меньше чем pl, если мы введем
Pl: p1=f (p0) andql=/ (qO)
Алгорит.м Рема выделения классов экьььалензност 215

·(Введение второго члена объясняется соображениями симмет-


. рии.)
Так как разб (f) $ (рО, qO) должно оставаться постоянным,
очевидными кандидатами на это «нечто» являются qO и ql;
но, учитывая, что q1 ~qO, выбор ql в общем случае более эф-
фективен, и нам нужно рассмотреть
ql <Pl-+ f: (pO)=ql
где предохранитель полностью обеспечен требованием эффек-
тивного уменьшения вариантной функции. Следующий вопрос
такой: можем ли мы после изменения ·f воссоединить ( рО, qO)
так, чтобы восстановилось, возможно нарушенное, условие Р.
Так как связь (из рО) с pl удалена, надежным воссоединением
будет возобновление (возможно) нарушенной связи с pl. Пос-
ле изменения f оператором f: (рО) =q1 мы знаем, что
разб (!) $ (pl, х) =разб (/нач)$ (р, q)
Для х, равных рО, qO или ql. Условие Р легко восстанавлива-
ется (так как в качестве х можно выбрать qb) с помощью опе-
ратора «р0:=р1», который с учетом Pl кодируется как
рО, pl: = pl, / (pl)
Таким образом, мы приходим к следующей программе, где
вторая охраняемая команда введена по соображениям сим-
метрии:
begin glocon р, q; glovar /; privar рО, pl, qO, q1;
р0 vir цел, qO vfr цел :=р, q \Р обеспечено\;
pl vfr цел, ql vir цел:= f (рО), f (qO) { Pl обеспечено}
·do ql<pl-+f:(p0)=q1; р0, р1 :=pl, /(pl)
0 р1 <q1 7 / : ( qO )' = р 1; qO, q 1 : = q 1, / (q 1)
od
end
Конструкция повторения построена таким образом, что ее
окончание не вызывает сомнений: при завершении мы можем
заключить, что р1 =q1, откуда при учете Р1 вытекает f (рО) =
=f (qO) и откуда в свою очередь следует QI
Замечание. При еконтролируемойэ разработке этой про-
граммы - и даже при последующем доказательстве правиль-
ности - введение обозначений «разб», «$ » или других подоб-
ных обозначений, по-видимому, очень существенно. Тем читате-
216 ГАава 23

лям, которые сомневаются в нравильности этого утверждения,


предл.агается попытаться самим провести это доказатель-
ство в, терминах N значений f (k), а не в терминах удачно под-
меченного свойства функции f как целого, как это сделали мы.
(Конец замечаниял
Совет. Те читатели, которые не вполне оценили все очаро-
вание алгоритма Рема, пусть очень внимательно прочтут эту
главу .еще раз. (Конец совета.)
24 ЗАДАЧА О ВЫПУКЛОИ ОБОЛОЧКЕ
В ТРЕХМЕРНОМ ПРОСТРАНСТВЕ

Чтобы отвести от себя обвинение в том, что я привожу


только такие примеры, которые допускают простые и нагляд-
ные решения и в известном смысле не требуют сложной про-
граммы, сейчас мы займемся задачей, которая, как я уверен,
окажется значительно более трудной. (К сведению моих чи-
тателей: когда я начинал работать над этой главой, я еще не
видел ни одной программы, решающей задачу, которой мы со-
бираемся заниматься.)
Пусть на прямой линии задано несколько различных точек,
скажем с помощью их координат, и требуется выбрать такие
точки Р, что все остальные точки лежат по одну сторону от Р.
Эта задача проста: просмотрев один раз· все координаты, мы
определим их максимальное и минимальное значения.
Когда несколько различных точек с помощью их коорди- ,
нат х, у заданы на плоскости, нас могут попросить выбрать те
точки Р, через которые можно провести прямые линии, такие,
что, все остальные точки лежат по одну сторону от каждой из
этих· прямых. Такие. точки Р являются вершинами того, что
называют «выпуклой оболочкой заданных точек». Сама вы-
пуклая оболочка - это циклически упорядоченная последова-
тельность точек, обладающая тем свойством, что все осталь-
ные точки лежат по одну сторону от прямых, проходящих
через пары ее последовательных точек. Выпуклая оболочка -
это кратчайшая замкнутая линия, такая, -про каждую точку
которой известно, что она лежит либо на этой, . линии, либо
внутри нее.
В данной главе мы будем заниматься аналогичной задачей
в трехмерном пространстве. Своими координатами х, у и z за-
даются N различных точек (N большое), таких, что никакие
четыре различные точки не лежат в одной плоскости. Требу-
ется выбрать все точки Р, через которые можно «провести»
плоскость, такую, что все остальные точки лежат по одну сто-
рону от этой плоскости. Эти точки являются вершинами вы-
пуклой оболочки Для заданных N .точек, т. е. минимальной
замкнутой поверхности, обладающей тем свойством, что каж-
8-6
218 Глава 24

дая точка лежит либо на ней, либо внутри нее. (Ограничение,


чтобы в каждой плоскости находилось не более трех точек,
введено для упрощения задачи; в результате все грани выпук-
лой оболочки будут треугольниками.)
На время мы оставим открытым вопрос о том, следует ли
получать выпуклую оболочку как объединение треугольников,
являющихся ее гранями, или как граф из ее вершин и ребер,
где ребро - это линия, проведенная через две различные вер-
шины, так что через эту линию можно провести плоскость,
причем все оставшиеся точки окажутся по одну сторону от
нее.
Причина, по которой мы откладываем принятие этого ре-
шения, - это та же самая причина, по которой задача .о трех-
мерной выпуклой оболочке является такой неудобной. В дву-
мерном случае выпуклая оболочка одномерна, и ее «обработ-
. ка» (просмотр, формирование и т. д.) вполне естественно
выполняется последовательным алгоритмом над линейной па-
мятью. Однако в трехмерном случае ни представление «дву-
мерного» ответа с помощью линейной памяти, ни «установка
очередности» при работе с ним далеко не самоочевидны.
Все известные мне алгоритмы для решения соответству-
ющих двумерных задач можно считать частными случаями
такой абстрактной программы:
построить выпуклую оболочку для двух или трех точек;
do существует точке вне· текущей оболочки-э- выбрать . неко-
торую точку вне текущей оболочки;
модифицировать текущую оболочку так, чтобы она вклю-
чала и выбранную точку
od
Известные алгоритмы отличаются один от другого в 'не-
скольких отношениях. В одном из подклассов этих алгорит-
мов в операторе «выбрать некоторую точку вне текущей обо-
лочки» выбираются только такие точки, которые являются
вершинами конечного ответа .. Эти алгоритмы разделяются на
два подподкласса: в одном подподклассе конечному ответу
будет принадлежать не только новая вершина, но также и од-
но из новых ребер; во втором подподклассе новая вершина для,
ответа выбирается так, чтобы по возможности уменьшить чис-
ло точек, все еще находящихся вне -оболочки. Эффективность
такой стратегии в худшем и .лучшем случаях зависит, однако,
QT положения заданных точек, а их «средняя эффективность»
определяется только по отношению к совокупности множеств
точек, из которых наше входное множество является, случай-
ной выборкой. (Вообще говоря, алгоритм с резко различа-
ющейся произ~одителыiостью в лучшем и худшем случаях не
Задача о выпуклой оболочке в зрехмерном пространстве 2ig

очень привлекателен, беда состопт в том, что часто эти грани-


цы можно сблизить, лишь сделав эффекъивностъ одинаково
низкой во всех случаях.)
Второй аспект, по которому различаются известные алго-
ритмы для' двумерного случая, - это вопрос о том, какие точ-
ки лежат внутри текущей оболочки (и поэтому могут быть
исключены из рассмотрения}. Чтобы определить, лежит ли
произвольная точка внутри текущей оболочки, мы можем про-
смотреть все ребра (например, последовательно): если для
всех ребер верно, что· точка находится во внутренней полу-
плоскости, то 1 очка находится внутри выпуклой оболочки.
Вместо того чтобы просматривать все вершины, можно ис-
пользовать то свойство, что точка лежит внутри выпуклой
оболочки; только когда она лежит внутри какого-либо тре-
угольника, вершины которого являются вершинами оболочки,

мы можем попытаться найти такую тройку вершин в соот-
~ветствии с некоей· стратегией «наибыстрейшего включения».
'Некоторые из этих стратегий могут быть в среднем значитель-
но убыстрены с помощью хранения дополнительной информа-
ции и экономии вычислительного времени за счет памятп ма-
шины.
Из всего этого следует ожидать, что для трехмерного слу-
чая набор алгоритмов, заслуживающих определения «разум·
ный», будет достаточно разнообразен. Было бы тщетно пытать-
ся исследовать этот класс с достаточной полнотой, и уверяю
вас, что я буду более чем счастлив, если мне удастся найти
один, возможно, два «разумных» алгоритма, которые не по-
кажутся излишне запутанными.
Лично я считаю такое глобальное исследование потенци-
альных трудностей, как приведенное выше, очень полезным, так
как из него следует, что, решая задачу, я должен учитывать
свои скромные возможности. В данном случае из него следу-
ет, что я должен учитывать свои очень скромные возможности
и, по-видимому, не должен увлекаться сложными рассуждени-
ями, пока мы не обнаружим - если обнаружим! - что, в конце
концов, наша задача не так плоха, как показалось с первого
взгляда. Самое радикальное упрощение, которое я :могу при-
думать, - это ограничиться топологией и воздержаться от
всех стратегических соображений, основанных на математи-
ческих ожиданиях числа точек внутри данных объемов.
По-видимому, самым разумным будет рассмотреть различ-
ные способы решения двумерной задачи и разобраться в спо-
собах их обобщения на случай трех измерений.
Одно из простых решений двумерной задачи связано с про-
извольно упорядоченными точками; начиная с выпуклой обо-
лочки для трех точек, последовательно получаются выпуклые
8*
220 Глава 24

оболочки для п первых точек. Всегда, когда в рассмотрение


включается следующая точка, необходимо решить две задачи:
1. Нужно узнать, где лежит новая точка: внутри или вне
выпуклой оболочки.
2. Если она лежит вне текущей оболочки, то последнюю
нужно модифицировать.
Один из способов состоит в нахождении множества k смеж-
ных ( 1) ребер, таких, что новая точка лежит вне их. Эти k ре-
бер (и k- 1 точек) должны быть удалены при модификации;
они будут заменены одной новой точкой и двумя ребрами. Ес-
ли в процессе поиска нам не удалось найти такое множество,
то новая точка лежит внутри.
Исходя из предположения, что текущие вершины цикличе-
ски упорядочены, т. е. для каждой вершины можно найти
предшествующую и последующую вершины, наименее «хит-
рая» программа будет исследовать ребра по порядку. В трех-
мерной задаче эквивалентом ребер являются грани-треуголь-
ники, эквивалентом множества смежных ребер - множество
связанных треугольников, которое топологически эквивалент-
но кругу. Будем называть его «шапкой».
Выпуклая. оболочка для двумерной задачи должна быть
разделена на две «секции»: множества ребер, которые отвер-
гаются и которые сохраняются соответственно; секции разде-
ляются двумя точками. В трехмерной задаче выпуклая обо-
лочка должна быть разделена на две шапки, разделенные
циклом из· точек и ребер.
Мы можем начать с сопоставления выбранной новой точки
с произвольной гранью и считать ее начальной гранью одной
из двух шапок; к шапке может добавляться каждый раз не
более чем по одной грани, и процесс прекратится либо потому,
что шапка содержит все грани (а вторая шапка пуста; новая
точка лежит внутри выпуклой оболочки), либо потому, что она
полностью окружена гранями, принадлежащими второй
шапке,
Ключевая задача, состоит, по-видимому, в том, чтобы орга-
низовать изменяющийся набор граней таким образом, что ес-
ли нам .лана шапка, то мы всегда можем найти грань, присо-
единив которую, мы вновь получим шапку. Мы можем это сде-
лать, если будем хранить (в циклическом порядке) точки (и
ребра}, принадлежащие границе шапки. У грани, которую
можно добавить к шапке, должны быть общее с границей шап-
ки ребро и, следовательно, две общие с ней точки: если в гра-
нице шапки окажется и третья точка этой грани, то в старой
границе она должна быть соседней с одной из двух первых.
Задача о выпуклой оболочке в трехмерном пространстве 221

Мне бы не хотелось, чтобы поиск грани, расширяющей шап-


ку, включал в себя просмотр всех оставшихся граней, т. е.,
если мне задается ребро границы, я бы предпочел иметь воз-
.можность быстрого доступа к необходимым данным, определя-
ющим другую грань, которой. принадлежит это ребро. Из на-
шего желания свести данные в таблицу - привычка работать
с массивами - вытекает, что нам нужно также вести учет и
группы перенумерованных ребер.
Тот факт, что ребро определяет две точки так же, как и
две грани, и что грань равно хорошо определяется своими тре-
мя точками и тремя ребрами, наводит нас на мысль постарать-
ся выбрать в качестве основных управляющих параметров
ребра. По-видимому, наиболее симметричным наш аппарат
будет тогда, когда мы занумеруем каждое неориентированное
ребро дважды, т. е. будем считать его суперпозицией двух за-
нумерованных, ориентированных ребер.
Пусть i - номер ориентированного ребра выпуклой обо-
лочки: тогда мы можем определить
инв(i) =номер ориентированного ребра, которое соединя-
ет те же самые точки, что и ориентированное реб-
ро nr.i, но в обратном направлении.
Далее, если мы связываем с каждой гранью три ориенти-
рованных ребра ее границы в порядке обхода по часовой
стрелке (правосторонней границы), то каждое ориентирован-
~ое ребро является ребром в точности одной грани и вся топо-
..логия полностью определяется с помощью еще одной функции
след(i) =номер ориентированного ребра, являющегося
.следующим по часовой стрелке ребром той гра-
ни, к правосторонней границе которой принад-
лежит ребро nr.i; слово «следуюшийь означает,
что ребро «след (i) » начинается там, где ребро
«i»кончается.
В результате . мы имеем, что «i», «след (i) » и «след
~след (i)) » - это номера ребер, образующие в данном порядке
границу грани. Так как все грани - треугольники, справедливо
след (след (след(i)))=i
Функции «ИНВ» и «след» определяют полное топологиче-
ское описание выпуклой оболочки в терминах имен ориентиро-
ванных ребер. Если мы хотим перейти к номерам точек, мы
можем ввести третью функцию
конец ( i) = номер точки, в которой оканчивается ориенти-
рованное ребро nr.i.
2'22 Глава 24

Тогда верно равенство конец(инв(i)) =конец(след (след


(i))), так как оба выражения обозначают номер точки, в ко-
торой 'начинается ориентированное ребро nr.i; левое выраже-
ние предпочтительнее не столько из-за того, что оно проще"
сколько потому, что оно не зависит от предположения, что все'
грани - треугольники.
Производить поиск множества ребер, которые оканчивают-
ся в точке, скажем м.k, очень неудобно, и его следует избе-
гать. Вместо того чтобы запоминать значение ~<k», нам нужно
запоминать «е]:», такое, что конец (ek) =k. Тогда, например,..
оператор
ek :=инв (след (ek))
присвоит ek номер следующего ребра, оканчивающегося в k;
выполняя повторно данное преобразование, мы сможем «про-
крутить» ek по всем ребрам, которые заканчиваются в точке
n,;,r.k (и таким образом мы получим доступ ко всем граням, у
которых точка nr.k является вершиной).
Замечание. Так как инв (инв (i)) = i и я думаю, что мы до-
статочно свободны в выборе номеров для ребер, мы можем вы-
бирать номера =#:О и ввести условие инв ( i) = -i; в та ком слу-
чае нам вообще не нужно хранить функцию инв. (Конец за-
мечаниял
Теперь мы попытаемся разделить текущую оболочку на две
шапки по отношению к новой вершине. Я стою на той точке
зрения, что обнаружение, по какую сторону грани лежит новая
вершина, является трудоемкой операцией, и мне бы хотелось.
сопоставлять новую точку с каждой гранью самое большее
один раз (а с некоторыми гранями, может быть, и не сопостав-
лять вообще).
Пользуясь языком метафор, мы можем считать новую точ-
ку лампой, освещающей все грани, на которые свет от нее па-
дает снаружи. Назовем их «светлыми» гранями, а все осталь-
ные «темными» гр~нями. Светлые грани должны быть удале-
ны; только тогда, когда точка находится внутри текущей
оболочки, все грани остаются темными.
После того как окрашена первая грань, мне, естественно"
хочется покрасить новые грани, являющиеся соседними к по-
крашенной. Теперь передо. мной возникают два основных воп-
роса:
1. Считаем ли мы, что, пока мы обнаруживаем грани толь-
ко одного цвета, каждая следующая рассматриваемая грань.
должна образовывать шапку с уже покрашенными гранями, или .
допускаем наличие «дыр»? Дело в том, что проверка, повлечет
Задача о выпуклой оболочке в трехмерном пространстве 223

.ли добавление новой грани появление дыры, - операция, не


<>чень. привлекательная.
2. После того как оказались использованными оба цвета,
мы знаем, что новая точка лежит вне текущей оболочки, и нам
также известно ребро, разделяющее две различно покрашен-
вые грани. Нужно ли нам изменить стратегию и отныне пы-
таться прослеживать границу между темными и светлыми гра-
нями, вместо того чтобы продолжать красить все грани пер-
воначальным цветом?
Мне потребовалось много времени, чтобы найти подходя-
щий ответ на эти вопросы. Так как мы встали на ту точку зре-
ния, что будем сопоставлять каждую грань с новой точкой са-
мое большее один раз, у нас в общем случае встречается три
вида граней: «темные», «светлые» и «еще неизвестно какие».
И нам необходимо запомнить определенное количество инфор-
мации об этом, чтобы не повторять сопоставления одних и тех
же граней с нашей новой точкой. Но у граней нет имен! Про
грань мы знаем только то, что «она расположена вдоль реб-
ра i'1!, т. е. у нее на границе при обходе по часовой стр.елке име-
ется ребро i, а для каждой грани существует три таких реб-
ра. Поэтому кажется более выгодным в качестве основного
элемента рассматривать не грани, а ребра, тем более что на
первом этапе мы ищем ребро между светлой и темной гранью.
Для того чтобы было удобнее сопоставлять. новую точку с
гранями, введем две константы: «правильный» и «неправиль-
ный»; «правильный» - это цвет (соответственно темный или
светлый) первой (произвольно выбранной) грани, которая со-
поставлялась с новой точкой, «неправильный» (соответствен-
но светлый или темный) - это противоположный ей пвет.
Пусть К - множество ребер i, такое, что
1) грань вдоль ребра i правильная (и это уже установле-
но);
2) грань вдоль ребра - i еще не проверялась.
Таким образом, начальное состояние К содержит три реб-
ра правосторонней границы первой сопоставленной грани. Ког-
да множество К не пусто, мы выбираем из него произвольное
ребро, скажем ребро х, и грань вдоль ребра -х сопоставляем
(впервые, так как ребро х удовлетворяет критерию (2)) с но-
вой точкой. Если новая грань неправильная, то найдено пер-
вое ребро между двумя различно окрашенными гранями и мы
переходим ко второму этапу, обсуждать который будем позд-
·нее. Если новая грань тоже правильная, мы выбираем ребра
у=-х, у=след(-х) и у=след(след(-х)), т. е. ребра право-
.сторовней границы новой правильной трани, и для каждого из
224 Глава 24

этих трех значений выполняем следующне действия:


if ребро - у в /(-+ удалить ребро - у из К
О ребро - у не в К-+ добавить ребро у в !(
fi
В первом случае ребро -у больше не удовлетворяет критерию
(2), во втором случае ребро у (которое не могло принадле-
жать К, так как не удовлетворяло критерию ( 1)) теперь удов-
летворяет критериям ( 1) и (2) (поскольку, если бы грань
вдоль -у была проверена раньше, ребро -у все еще было бы
в К).
Тем временем мы уже выработали некоторое мнение, по
крайней мере предварительное, по поводу вопросов 1 и 2: мы
не настаиваем на том, чтобы грани, правильность которых ус-
тановлена, всегда" образовывали шапку, и мы действительно
намереваемся менять стратегию при обнаружении первого реб-
ра, принадлежащего границе между шапками. (Эти решения
были приняты, когда я бросил писать и отправился на вечер-
нюю прогулку по нашему поселку. Сначала я заметил себе,
что прослеживание границы между двумя шапками, после то-
го как найдено одно из ее ребер, сводит двумерный . поиск к
одномерному и, следовательно, надо постараться использовать.
эту возможность. Затем я осознал, что коль скоро я принял
эту стратегию, то, по-видимому, нет особого смысла добивать-
ся на первом этапе, чтобы грани, правильность которых уста-
новлена, всегда образовывали шапку. Далее некоторое время
я пытался представить себе, как выполнить первый этап в тер-
минах граней, являющихся правильными или непроверенны-
ми, пока не вспомнил то, что временно забыл, а именно что я
уже решил описывать структуру в терминах ребер. С уверен-
ностью, что теперь все должно получиться, я вернулся за стол
и написал то, что вы сейчас прочитали.)
Теперь нам нужно заняться вторым этапом. Пусть х -
ребро правосторонней границы правильной шапки .Как найти"
например, следующее ребро этой границы? Мы видели, как
можно перебирать грани с общей точкой; неизвестно одно -
достаточно ли знание множества К для соблюдения принципа,
что никакая грань не должна сопоставляться с новой точкой·
больше одного раза.
Ну что ж, по-видимому, на это рассчитывать не приходит-
ся, так как когда мы находим какую-либо неправильную грань,
то, с одной стороны, мы не можем гарантировать, что мы уже
точно знаем, что надо делать с ней, т. е. со всеми соседними с
ней гранями, а с другой стороны, нам бы не хотелось сопостав-
Задача о выпуклой оболочке в трехмерном пространстве 225

лягъ эту неправильную грань с новой точкой более одного ра-


за. Простейший выход, вероятно, состоит в том, чтобы ввести
аналогичный аппарат для классификации ребер по отношению
к неправильным граням, а именно пусть Н - множество ре-
бер i, такое, что
1) установлено, что грань вдоль ребра i неправильная:
2) грань вдоль ребра - i еще не про.верялась.
Достаточно ли нам этоrо? Замечаем, что пересечение К и Н
пусто, так как никакое ребро i не может быть элементом и К,
иН.
Можно ли перевычислить оба множества К и Н, когда но-
вая грань правильная и когда новая грань неправильная?
Пусть х - ребро из множества К, и снова рассмотрим три реб-
ра у, где у=-~, у=след(-х) и у=след(след(-х)). Пусть
в дальнейшем В - множество обнаруженных ребер правосто-
ронней границы правильной шапки.
Если новая грань правильная, то каждое из ее трех ребер у
обрабатывается так:
if ребро - у в К-+ удалить ребро - у из К
О ребро-у в Н-+ удалить ребро - у из Н и
добавить ребро у к В
О ребро - у не в (Н +К) -э-добавитъ ребро у к К
fi
Если новая грань неправильная, каждое из ее трех ребер у
должно быть обработано так:
Jf ребро - у в К-+ удалить ребро - у из К и
добавить ребро - у к В
О ребро - у в Н-+ уда лить ребро - у из Н
О ребро-у не в (Н +К)-+ добавить ребро у к Н
fi
Глядя на эти формулы, я делаю одно из удивительных от-
крытий, которыми так полна жизнь! Когда Н все еще пусто, а
новая грань правильная, перевычисление сводится - и это не
очень удивительно - к перевычислению из первого этапа. Но
мы же нигде не пользовались тем, что х- элемент К: если бы
х был элементом Н, этот алгоритм работал бы так же хорошо.
Мы требуем только, чтобы х был элементом объединения
Н+К, так как хотим избежать повторного сопоставления и
226 Глава 24

тем самым обеспечить завершаемость работы! С точки зрения


логики нам достаточно в качестве начального состояния К
выбрать ребра правосторонней границы первой грани, а в ка-
честве начального состояния Н и В - пустые множества. Тог-
да, до тех пор пока это возможно, мы будем выбирать произ-
вольное ребро х из объединения Н+К и подвергать последо-
вательные ребра у грани, расположенной вдоль ребра -х,
описанной выше обработке. Этот процесс заканчивается, ког-
да множество н+к становится пустым; новая точка находится
внутри текущей оболочки тогда и только тогда, когда по окон-
чании работы В по-прежнему пусто!
Прежде чем продолжить рассуждения, мне кажется умест-
ным сделать несколько ретроспективных замечаний. Я ввел
ограничение, согласно которому каждая грань сопоставляется
с новой точкой только один раз, из соображений эффективно-
сти, сделав логически необоснованное предположение, что для
выполнения этой операции может потребоваться много време-
ни. Мне не следовало бы заботиться об эффективности еще на
первом этапе; мне следовало бы сосредоточить свое внимание
скорее на логических требованиях, гарантирующих окончание
работы. Если бы я это сделал, мне не пришлось бы так долго
возиться с «шапками» - этой навязчивой идеей - и я не стал
бы считать оба этапа логически различными! (Я должен был
бы более подозрительно отнестись к тому, что слишком долго
выбираю ответы на вопросы ( 1) и (2) ! ) К чему все сводится?
Мы замечаем, что если ребра в непустом множестве В образу-
ют цикл, то можно прекратить работу, даже если н+к все
еще не пусто, и зная это, можно попытаться использовать наше
право выбора произвольного х из Н+К, так чт.обы найти по
возможности быстрее все ребра этого цикла. Поскольку мы
пытаемся сделать все как можно проще, естественно отложить.
добросовестное прослеживание границы между двумя шапка-
ми как оптимизацию, которую мы, по всей видимости, можем
без тру да ввести на более поздней стадии.
Введение понятий «правильный» и «неправильный», по-ви-
димому, также было ошибкой, если учесть то, как мы выбира-
ем ребро х из объединения н+к. (Это было не· только ошиб-
кой, это было глупой ошибкой. Я долго колебался, перед тем
как пойти на это, у меня были необычайные трудности с вы-
бором подходящих имен для них и с формулировкой их опре-
деления, но я не внял этим предостережениям, убеждая себя,
что я нашел нечто «хитрое».) Предадим забвению эти понятия
и будем работать с нашими первоначальными терминами
есветлый» и «темный». На этом пути у нас должна появиться
воэможность сделать три упрощения:
Задача о выпцклой оболочке в хрехмерном пространстве 227

1. Нам незачем по-особому обрабатывать первую прове-


ряемую грань; ее три ребра у могут обрабатываться так же,
как и ребра любой другой грани.
2. Обе 'приведенные выше различные конструкции обработ-
ки трех ребер новой проверяемой грани (для правильной гра-
ни и для неправильной грани соответственно) можно отобра-
зить на одну конструкцию выбора.
3. Нам не нужно менять направление ребер из множества
В, если оно окажется правосторонней границей темной
шапки.
Некоторые другие частности мы обсудим позднее, когда.
исследуем операцию модификации выпуклой оболочки для
включения новой точки.
После обнаружения непустой границы нам нужно удалить
внутренние ребра светлой шапки, если таковые имеются, и
добавить ребра, соединяющие новую точку с точками границы
этой шапки. Так как количество удаляемых ребер в общем
случае никак не связано с количеством добавляемых ребер,
мы предлагаем решать эти две задачи раздельно. В еще неде-
талиэированный процесс поиска границы, при котором прове-
ряются все грани, нетрудно было бы ввести одновременное
удаление всех внутренних ребер светлой шапки. Но мне хо-
телось бы сохранить за собой право на оптимизацию поиска
границы, и поэтому нам надо выбрать метод нахождения
множества внутренних ребер светлой Шапки, когда задана ее
непустая правосторонняя граница В, независимо от того, как
эта граница была найдена.
В виду того что нахождение множества внутренних ребер
светлой шапки является частным случаем (не таким уж специ-
альным) более общей проблемы (ее более специальные слу-
чаи - это задача Хэмм инга и обход двоичного дерева), мы
займемся сначала этой общей проблемой (которая по сущест-
ву является задачей о нахождении транзитивного замыкания).
Задано (конечное) множество элементов. Для каждого эле-
мента х определяются несколько (или ни одного) других эле-
ментов, называемых «преемниками х». Детя любого множест-
ва В множество S (В) задается следующими ·аксиомами:
Аксиома 1. Каждый элемент В принадлежит S (В).
Аксиома 2. Если х принадлежит S (В), то преемники х,
если таковые имеются, также принадлежат
S(B).
Аксиома 3. Множество S (В) содержит только те эле мен-
ты, которые принадлежат ему согласно аксио-
мам 1 и 2.
228 Глава 24

Для заданного В требуется обеспечить истинность


R: V=S(B)
Идея алгоритма очень проста: нннцналианровать V: =В и
до тех пор, пока в V имеется элемент х, преемники которого
не принадлежат V, дополнять V этими элементами; когда все
такие элементы будут исчерпаны, остановиться. Алгоритм
можно улучшить, если учесть следующее: после того как ус-
тановлено, что для данного элемента у все его преемники уже
находятся в множестве V, такое положение будет сохраняться
и в дальнейшем. Это верно, потому что единственная модифи-
кация V состоит в расширении V новыми элементами. Следо-
вательно, оптимизация должна состоять в том, чтобы следить
за подмножеством из таких элементов у, поскольку их преем-
ников рассматривать больше не нужно. Таким образом, мно-
жество V разбивается на две части, которые мы назовем С
и v-..-
С. Здесь V ~ссодержит все элементы- для которых бы-
ло установлено, что их преемники уже находятся в множестве
V, а С содержит все оставшиеся элементы, у которых могут
найтись преемники, не содержащиеся в V. Алгоритм обраба-
тывает каждый раз один элемент из С. Пусть с - элемент
из С; он удаляется из С (и, следовательно, добавляется к
V ~С), а все его преемники, не принадлежащие V, заносятся
в V и С. Алгоритм прекращает работу, когда С пусто. Окон-
чание работы гарантируется конечностью множества S (В),.
так как, хотя число элементов С может и увеличиться, число
элементов V ~с
увеличивается на единицу на каждом шаге
работы, и это число ограничено.сверху числом элементов
множества S (В). (Количество шагов работы алгоритма не
зависит от порядка выбора элемента с из множества С!)
Данное неформальное описание предполагаемого вычисли-