Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
20-34.docx
Скачиваний:
3
Добавлен:
01.03.2025
Размер:
70.61 Кб
Скачать

Вопрос 31

9. ПЕРВЫЙ ПРИМЕР ПОЭТАПНОГО СОСТАВЛЕНИЯ ПРОГРАММЫ

В разделе "О понимании программ" я делал упор на необходимость такой систематизации следования, при которой структура вычислений могла бы отражаться в структуре программы; в этом плане мы можем говорить о единой структурной организации программы и вычислений. В данной секции я попытаюсь придать несколько больше содержания все еще довольно туманному понятию .структурной организации. Мы попытаемся употребить нашу способность к абстракции для того, чтобы сократить область применения рассуждений с перечислением, и будем последовательно применять способы разложения, рассмотренные в разделе "О понимании программ".

Вместо того чтобы предоставить читателям (как уже готовую конструкцию) то, что я склонен называть хорошо организованными .программами, мне хотелось бы описать чрезвычайно подробно процесс составления такой программы. Я поступаю так потому, что программы не существуют изначально; напротив, их нужно создавать, и меня особенно интересуют именно те программы, которые, .как мне кажется, достаточно хорошо соответствуют нашим возможностям построения и понимания.

Пусть перед нами стоит задача научить машину печатать таблицу первой тысячи простых чисел, причем число 2 считается первым простым числом.

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

Замечание 2. Я не утверждаю, что моя окончательная программа будет "наилучшей возможной" с точки зрения любого критерия, .который вздумает выбрать кто-либо из читателей. По крайней мере два читателя предыдущего варианта этой работы, встретив вычисление остатков посредством операции деления, бурно прореагировали: "Но ведь всякий знает, что самый эффективный способ порождения простых чисел состоит в использовании решета Эратосфена"; после этого они оказались не в состоянии читать что-либо далее.

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

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

Простейшей формой нашей программы является описание 0:

begin "напечатать первую тысячу простых чисел" end

и если "напечатать первую тысячу простых чисел" отсылает нас к инструкции из принятого набора, то описание 0 решает нашу задачу. Чтобы продолжить рассуждения, предполагаем, что такая инструкция не входит в принятый набор. Поэтому нам следует придумать вычисление, состоящее из "более простых" действий и приводящее к получению желаемых результатов. Прежде всего предлагается отделить получение простых чисел от их печати, и мы вводим описание 1":

begin variable "таблица р";

"заполнить таблицу р первой тысячью простых чисел";

"напечатать таблицу р"

end

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

Опять-таки, если "заполнить таблицу р первой тысячью простыx чисел" и "напечатать таблицу р" встречаются в принятом наборе инструкций (а "таблица р" относится к классу допустимых ресурсов), то наша задача решена. И снова, чтобы продолжить рассуждения, мы предполагаем, что это не так. Это означает, что в следующей конкретизации мы должны выразить результаты этих двух действий с помощью двух дальнейших подвычислений. Кроме того, мы должны решить, как представлять информацию, которая должна содержаться в промежуточном значении все еще довольно неопределенного объекта "таблица р".

Прежде чем следовать далее, я хотел бы подчеркнуть, как мало мы приняли решений, когда написали описание 1, и в сколь незначительной степени была принята во внимание исходная постановка нашей задачи. Мы предположили, что доступность ресурса "таблица р" (в той или иной форме) позволит нам вычислить первую тысячу простых чисел прежде, чем начнется печать, и на основании этого предположения пользовались возможностью вычисления простых чисел независимо от их печати. Что касается исходной постановки задачи, то мы игнорируем тот факт, что в действительности существует гораздо больше, чем по крайней мере тысяча различных простых чисел. (Мы должны были предположить существование не менее, чем тысячи простых чисел, чтобы постановка задачи имела смысл.) На этом этапе остается все еще совершенно несущественным реальное содержание понятия "простое число". Кроме того, мы пока не связывали себя по крайней мере в том, что касается специфических форматных требований относительно выполнения выдачи на печать. Очевидное преимущество нашего подхода состоит в том, что логическое развитие этих двух довольно независимых аспектов исходной постановки задачи оказывается отнесенным к соответствующим конкретизациям двух последовательных действий. Поэтому есть основания утверждать, что попытка применить золотой принцип "разделяй и властвуй" оказалась до некоторой степени успешной.

Однако, подводя итог нашим рассмотрениям, мы должны задать себе вопрос, до какой же степени эти два подвычисления могут продумываться далее независимо одно от другого. Точнее говоря, вопрос состоит в следующем: "Достигли ли мы уже такого уровня, на котором проекты двух алгоритмов (которые должны вызывать наши два вычисления) могли бы продумываться двумя программистами, работающими независимо один от другого?"

Когда уже нельзя считать, что наши два действия вызываются инструкциями из принятого набора, то нельзя и рассматривать переменную "таблица р" как доступный в принципе элементарный ресурс. И аналогично тому, как мы разлагаем действия на поддействия, нам следует выбрать способ организации переменной "таблица р", т. е. выбрать структуру данных для представления информации, которая должна передаваться от одного действия к другому. На каком-то этапе придется решать эту проблему, и возникают вопросы: "когда?" и "как?"

В принципе намечаются два подхода. Первый подход состоит в том, чтобы попытаться отсрочить решение о разложении "таблицы р" на (более нейтральные и менее проблемно-ориентированные) компоненты. Если мы откладываем решение вопроса о разложении "таблицы р", то нам остается теперь конкретизировать то или иное действие или оба действия. Мы можем сделать это, подбирая подходящий набор операций над все еще таинственным объектом "таблица р", затем мы объединяем эти операции и, руководствуясь их требованиями, разрабатываем наиболее удобную структуру "таблицы р".

С другой стороны, можно пытаться принимать решения относительно структуры "таблицы р". Если решено, как будет представляться таблица первой тысячи простых чисел, то дальнейшая конкретизация каждого из двух действий может выполняться независимо от другого действия.

Оба подхода одинаково сложны; степень удобства алгоритма для выполнения, скажем, первого подвычисления будет сильно зависеть от легкости и изящества возможной реализации предполагаемых операций над "таблицей р", и если одна или несколько операций оказываются слишком неудобными, то вся постройка разбивается вдребезги. С другой стороны, если мы поспешно примем необдуманное решение относительно структуры "таблицы р", то вполне; возможно, что тогда окажется затруднительной реализация подвычислений. Невозможно обойти эту проблему: в изящной программе, структура "таблицы р" и относящиеся к ней вычисления должны быть хорошо согласованы. Я полагаю, что поведение умелого программиста сводится в первую очередь к попыткам найти самое лёгкое решение, т. е. такое решение, которое требовало бы минимальных затрат усилий (методом проб и ошибок, последовательной подгонки и т.д.) при максимально обоснованной надежде на то, что не придется сожалеть об этом решении.

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

С одной стороны, мы можем попытаться использовать тот факт, что "таблица первой тысячи простых чисел" является не просто таблицей из тысячи чисел, какой была бы, скажем, таблица месячных заработков тысячи работников фабрики, но что все эти числа различны между собой.

Воспользовавшись этим, мы можем представить нашу информацию с помощью линейного логического массива, последовательные элементы которого соответствуют последовательным числам натурального ряда и указывают, является ли соответствующее число ряда простым. Теория чисел дает нам оценку порядка тысячного простого числа, а тем самым и граничную достаточной длины массива. Организовав нашу информацию таким способом, мы подготовили бы аппарат для того, чтобы с легкостью отвечать на вопрос: "Является ли значение п (меньшее чем максимум) простым?" С другой стороны, можно предпочесть массив типа integer, в котором будут перечислены последовательные простые числа. (При этом будет использована та же оценка, полученная методами теории чисел, коль скоро максимальное значение элемента массива должно быть задано заранее). Во втором случае мы создаем аппарат, удобный для ответа на вопрос: "Какое значение принимает k-е простое число при k1000?"

Мы советовали бы программисту предпочесть второе представление. Оно более удобно для операции печати, когда следует печатать именно простые числа, а не числа натурального ряда с указанием того, простые они или нет. Оно обеспечивает преимущества и на этапе вычислений, даже если учесть, что для поиска простых множителей заданного числа может оказаться полезной проверка, является ли простым то или иное число.

Следующим этапом конкретизации нашей программы будет формулировка соглашения относительно представления все еще таинственного объекта "таблица p" и переопределение наших двух операций в соответствии с этим соглашением.

Соглашение состоит в том, что информация, которая должна содержаться в "таблице p", будет представляться значениями элементов массива "integer array p[1:1000]"; при 1k1000 значение p[k] будет равно k-му простому числу, причем простые числа упорядочиваются в порядке возрастания их значений. (Если возникнет вопрос о максимальном значении этих целых чисел, то мы будем предполагать, что согласно теории чисел оно достаточно велико.)

Если мы захотим теперь описать эту конкретизацию, то столкнемся с новой трудностью. Наше описание 1 имело форму единой программы благодаря тому, что представляло собой конкретизацию единого действия "напечатать первую тысячу простых чисел", сформулированного в описании 0. (Если говорить более строго, описание 1 могло бы иметь форму тела процедуры.) Однако дело обстоит не так на нашем следующем уровне, когда требуется конкретизировать (в каком-то смысле одновременно) три именования, а именно "таблицу p" и два действия, и мы должны ввести некоторую идентифицирующую терминологию для указания того, что конкретизируется.

Чтобы продолжить обсуждение, сделаем весьма спорное утверждение. Скажем так: описание 0 - это правильный текст программы, выраженный с помощью одного именованного действия "напечатать первую тысячу простых чисел"; пусть оно идентифицируется кодом 0a.

Описание 1 называется "1", потому что представляет собой следующую конкретизацию описания 0; оно содержит конкретизацию действия 0a (единственного термина, которым выражается описание 0), а само выражается с помощью трех именованных понятий, которым мы присваиваем следующие коды: "таблица p" 1a

"заполнить таблицу p первой тысячей простых чисел" 1b

"напечатать таблицу p" 1c

Эти коды начинаются с цифры 1, потому что с их помощью выражается описание 1; символы "a", "b" и "c" приписываются, чтобы отличать коды один от другого.

Теперь нам нужно описать соглашение, выбранное для представления информации, которая должна содержаться в "таблице p"; но это соглашение относится ко всем трем элементам 1a, 1b и 1c. Поэтому мы назовем его описанием 2; в него следует включить описатели трех отдельных элементов. (Мы используем знак равенства в качестве разделителя.)

Описание 2:

1a="integer array p[1:1000]"

1b="для k от 1 до 1000 приравнивать p[k] k-му простому числу"

1c="p[k] для k от 1 до 1000"

Описание 2 выражается с помощью трех именованных понятий, которым мы присваиваем (очевидным образом) коды 2a, 2b и 2c. (В кодовом обозначении описание 2 выглядит очень банально: оно только констатирует, что для 1a, 1b и 1c мы выбрали конкретизации 2a, 2b и 2c соответственно.)

Замечание. При представлении информации, которая должна содержаться в "таблице p", мы предпочли не использовать ни тот факт, что каждое печатаемое значение встречается только один раз, ни то, что они встречаются в порядке возрастания величин. Другими словами, это означает, что действие, которое должно выполняться под именем 2c, рассматривается как частный случай печати любого набора из тысячи целых чисел (это могла бы быть таблица месячных заработков тысячи пронумерованных работников!). Точный результат действия печати в этом примере однозначно определяется как первая тысяча простых чисел; однако мы представляем его себе как частый случай более широкого класса возможностей. При дальнейшей конкретизации действия 2c мы занимаемся всем этим классом, а частный случай в рамках этого класса описывается значениями элементов массива p. Когда люди говорят об "описании взаимосвязей", у меня часто возникает впечатление, что они отвлекаются от подразумеваемого обобщения понятия класса "возможных" действий.

Если 2b и 2c встречаются в принятом наборе инструкций (а следовательно, 2a является потенциально доступным ресурсом), то вся наша проблема решается. Чтобы продолжить рассуждения, мы опять предполагаем, что это не так. При этом мы сталкиваемся с задачей представления подвычислений для 2b и 2c. Однако теперь, поскольку уже введен уровень 2, соответствующие конкретизации для 2b и 2c могут быть построены независимо.

Конкретизация для 2b: "для k от 1 до 1000 приравнивать p[k] k-му простому числу".

Мы ищем описание 2b1, т. е. первую конкретизацию для 2b. Дополнительная нумерация после 2b (вместо того, чтобы называть наше следующее описание "3 с чем-то") вводится для того, чтобы отметить взаимную независимость конкретизации для 2b и 2с соответственно.

В описании 2b1мы должны дать алгоритм присвоения значений элементам массива p. Это означает, что мы должны, например, описать, в каком порядке будут производиться эти присваивания. В нашей первой конкретизации мы хотим описать именно это и ничего более. Очевидный, но нелепый вариант описания начинается следующим образом ("номер варианта" заключается в скобки):

2b1(1):

begin р [1]:=2; р[2]:=3; р[3]:=5; р[4]:=7; р[5]:=11; ... end

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

Если задано первое простое число (=2), а тысячное предполагается неизвестным программисту, то самым естественным порядком заполнения массива р представляется порядок возрастания значений индексов. Выражая эту мысль, мы приходим (например) к записи

2b1(2):

begin integer k, j; k:=0; j:= 1;

while k<1000 do begin "увеличить j до значения

следующего простого числа"; k:=k+1; p[k]:=j end

end

Если идентифицировать переменную k как номер очередного найденного простого числа и убедиться в том, что наше первое простое число (=2) действительно представляет собой наименьшее простое число, большее чем 1 (оно равно начальному значению j), то правильность описания 2b1(2) легко доказывается математической индукцией (в предположении, что существует достаточное количество простых чисел).

Описание 2b1(2) оказывается законченной программой, если в предусмотренный набор входит операция "увеличить j до значения следующего простого числа" - назовем ее 2b1(2)а, - однако мы предположим, что это не так. В таком случае нам нужно выразить в следующей конкретизации способ увеличения j (и опять-таки ничего более). Мы приходим к описанию уровня 2b2(2)

2b1(2)а=

begin boolean jпростое;

repeat j:=j+1;

"присвоить'переменной jпростое значение предиката:

j является простым числом"

until jпростое

end

Замечание. Здесь мы используем форму repeat- until ("повторить - пока не"), чтобы указать, что значение j всегда должно увеличиваться по крайней мере один раз.

И в этом случае правильность описания вряд ли может быть поставлена под сомнение. Впрочем, если предположить, что программист знает, что, за исключением числа 2, все последующие простые числа являются нечетными, то можно ожидать, что его не удовлетворит приведенный выше вариант, поскольку это описание оказывается совершенно неэффективным. Расплатой за "отсутствие дара предвидения" является пересмотр варианта 2b1(2).

Простое число 2 будет обрабатываться отдельно, после чего будут циклически перебираться в поиске простых чисел только нечетные значения. Вместо записи 2b1(2) мы получаем описание

2b1(3):

begin integer k, j; р[1]:=2; k:= 1; j:= 1;

while k<<1000 do

begin "увеличить нечетное j до значения следующего

простого числа"

k:=k+1; p[k]:=j

end

end

причем аналогичная конкретизация заключенной в кавычки операции - назовем ее 2b1(3) а - приводит к описанию уровня 2b2(3):

2b1(3)а=

begin boolean jпростое;

repeat j:=j+2;

"присвоить при нечетном j переменной jпростое значение

предиката: j является простым числом";

until jпростое

end

Мы метались между двумя уровнями описания всего лишь для того, чтобы привести к удобному для нас виду взаимосвязь между внешней структурой и тем примитивным действием, которое должно соответствовать этой структуре. Ясно, что это метание, этот способ проб и ошибок не вызывает положительных эмоций, но, не обладая даром предвидения, я не нахожу другого выхода, когда приходится принимать последовательные решения; наши попытки могут рассматриваться как сравнительно дешевые исследовательские эксперименты в поиске наиболее удобного способа организации такой взаимосвязи. Замечание. И 2b1(2), и 2b1(3) могут быть в общих чертах описаны так:

begin "присвоить таблице p и j начальные значения";

while "таблица p не заполнена" do

begin "увеличить j до значения следующего простого числа,

которое должно быть добавлено"; "добавить j к таблице p"

end

end

но мы не станем этим пользоваться, так как эти два варианта отличаются организацией следования (см. разд. "О сравнении программ"), и мы считаем их несравнимыми. Выбрав описание 2b1(3), мы принимаем решение, что наш пробный вариант 2b1(2) - как и 2b1(1) - уже не является применимым и потому отбрасывается.

Замена варианта 2b1(2) на 2b1(3) оправдывается повышением эффективности на уровнях более детальной конкретизации. Это повышение эффективности достигается на уровне 2b2, поскольку теперь значение j может увеличиваться каждый раз на 2. Оно проявится также в связи с недоопределенным еще примитивным действием на уровне 2b2(3), где алгоритм "присвоить при нечетном j переменной jпростое значение предиката: j является простым числом" должен только служить для анализа нечетных значений.

И снова в описании 2b2(3) мы конкретизируем 2b1(3) в виде алгоритма, который полностью решает нашу проблему, если в принятый набор входит инструкция: "присвоить при нечетном j переменной jпростое значение предиката: j является простым числом", назовем ее "2b2(3)a". Мы опять предполагаем, что это не так, другими словами, мы должны произвести явно вычислительную проверку, имеется ли у заданного нечетного значения j какой-нибудь множитель. Только на этом этапе алгебра по-настоящему попадает в поле нашего зрения. Здесь мы используем свое знание того, что нужно только проверять простые множители; более того, мы будем пользоваться тем фактом, что все подлежащие проверке простые числа уже могут быть найдены в заполненной части массива p.

Мы используем следующие факты:

(1) Если значение j нечетное, то наименьшим возможным множителем, подлежащим проверке, является p[2], т.е. минимальное простое число, большее чем 2.

(2) Наибольшим простым числом, подлежащим проверке, является p[ord-1], где p[ord] - минимальное простое число, квадрат которого превосходит j.

(Здесь я пользуюсь также и тем фактом что минимальное простое число, квадрат которого превосходит j, уже может быть найдено в таблице p. Смиренно процитирую замечание Кнута по поводу прежней версии этой программы, где я использовал данный факт без обоснования: "Здесь вы упустили существенное обстоятельство! В вашей программе применяется тонкий результат теории чисел, согласно которому всегда

где pn - простое число с номером n". Признаю свою ошибку.)

Если множество подлежащих проверке простых чисел не пусто, то может найтись множитель; коль скоро такой множитель найден, то можно прекратить исследование данного значения j. Мы должны принять решение, в каком порядке испытывать простые числа из нашего множества; будем перебирать их в порядке возрастания значений, так как чем меньше простое число, тем больше шансов, что оно окажется множителем числа j.

Если известно значение параметра ord, то мы можем дать для действия "присвоить при нечетном j переменной jпростое значение предиката: j является простым числом" следующее описание на уровне 2b3(3):

2b3(3)a =

begin integer n; n:=2; jпростое:=true;

while n<ordjпростое do

begin "присвоить переменной jпростое значение: p[n] не

является множителем числа j"; n:=n+1

end

end

Этот вариант описания подразумевает, что известно значение выражения ord как функции от j. Мы могли бы начать эту конкретизацию следующим образом:

begin integer n, ord;

ord:=1; while p[ord]2j do ord:=ord+1;

... ...

т.е. вычислять значение ord заново всякий раз, когда оно понадобится. Здесь представляется уместным сэкономить время счета ценой дополнительной затраты места в машинной памяти: вместо того чтобы вычислять эту функцию заново каждый раз, когда бы она не потребовалась, мы вводим дополнительную переменную ord для ее текущего значения; этой переменной присваивается значение, когда производится установка значения j, а при изменении j она перевычисляется.

Это вынуждает нас проделать дополнительную работу по программированию. Можно было бы ввести наряду с j целую переменную ord и просмотреть имеющийся программный материал, вставляя операции над ord всюду, где производятся действия над j. Я не хочу поступать так потому, что на том уровне, где вводится и обретает смысл переменная j, функция ord еще не может существовать. Поэтому нам следует пытаться ввести ord только на подходящем для этого уровне, и мы будем действовать очень аккуратно.

Для действия 2b: "для k от 1 до 1000 приравнивать p[k] k-му простому числу" мы вводим (по аналогии с уровнем 2b1(3))

уровень 2b1(4):

begin integer k, j; р[1]:=2; k:= 1;

"установить j=1"

while k<1000 do

begin "увеличить нечетное j до значения следующего

нечетного простого числа";

k:=k+1; p[k]:=j

end

end

Описание этого уровня выражено с помощью действий 2b1(4)а "увеличить нечетное j до значения следующего нечетного простого числа",

2b1(4)b "установить j=1".

На следующем уровне мы введем только подвычисление для 2b1(4)а; другое подвычисление переносится на более поздний уровень

уровень 2b2(4):

2b1(4)а=

begin boolean jпростое;

repeat "увеличить j на два"; "присвоить при нечетном j переменной jпростое значение:

j является простым числом"

until jпростое

end

2bl(4)b=2b2(4)b.

Описание этого уровня выражено с помощью действий 2b2(4)b что также означает "установить j=1";

2b2(4)с "увеличить j на два";

2b2(4)d "присвоить при нечетном j переменной jпростое значение: j является простым числом".

Только на следующем уровне возникает надобность говорить о вычислении ord. Поэтому записываем теперь

уровень 2bЗ(4): integer ord;

2b2(4)b=

begin j:==1; "установить начальное значение ord" end;

2b2(4)с =

begin j:=j+2; "перевычислить ord" end;

2b2(4)d=

begin integer n; я:=2; jпростое:= true;

while n<ordjпростое do

begin "присвоить переменной jпростое значение:

р[п] не является множителем числа j";

n:= n+1

end

end

Описание этого уровня выражено с помощью действий 2b3(4)a "установить начальное значение ord";

2bЗ(4)b "перевычислить ord";

2bЗ(4)с "присвоить переменной jпростое значение: р[п] не является множителем числа j"

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

(1) если ord - неубывающая функция от j и значения j только возрастают, то перевычисление ord означает условное увеличение;

(2) вопрос, является ли p[n] множителем числа j, сводится к вопросу, равен ли остаток нулю. Это приводит к появлению уровня 2b4(4):

2bЗ(4)а=2b4(4)а

2bЗ(4)b=

begin while "значение ord слишком мало" do "увеличить ord на 1"

end;

2b3(4)c =

begin integer r,

"приравнять r к остатку от деления j на р[п]"; <

i>jпростое:=(r0)

end

Описание этого уровня выражено с помощью действий 2b4(4)а по-прежнему "установить начальное значение ord";

2b4(4)b "значение ord слишком мало";

2b4(4)c "увеличить ord на 1";

2b4(4)d "приравнять r к остатку от деления j на р[п]"

Если имеется встроенная операция деления, то можно считать, что реализация действия "приравнять r к остатку от деления j на p[n]" не представляет труда. Интересующимся читателям предоставляется самостоятельно рассмотреть случай, когда конкретизация 2b4(4)d может быть произведена непосредственно. Чтобы сделать алгоритм нетривиальным, предположим, что отсутствует возможность удобного вычисления остатка от деления. В этом случае алгоритм

r:=j; while r>0 do r:=r-p[n]

позволяет получить (неположительный) остаток, однако такой алгоритм неприемлем с точки зрения времени вычислений. Снова возникает задача введения некоторой дополнительной табличной информации (аналогично способу введения функции ord).

Мы хотим знать, является ли заданное значение j кратным р[п] при n<ord. Чтобы облегчить себе этот анализ, мы вводим второй массив, в элементах которого будем запоминать кратные последовательных простых чисел, достаточно близкие к значению j. Чтобы задать размер массива, нам желательно знать верхнюю границу для значения ord; разумеется, допустима оценка 1000, однако из теории чисел следует, что уже число 30 является надежной верхней оценкой. Поэтому мы вводим

integer array крат[1:30]

и устанавливаем условие, что при n<ord значениями крат[n] будут кратные р[п], причем каждый раз будет удовлетворяться отношение

крат[n]<j+р[n]

которое сохраняет силу, если значение j возрастает. Всякий раз, когда нужно проверить, является ли р[п] множителем числа j, мы последовательно увеличиваем крат[n] на р[п], пока выполняется отношение

крат[n]<j

После этого наращивания значение крат[n]=j оказывается необходимым и достаточным признаком того, что число j - это кратное числа р[п].

Нижняя экстремальная оценка для ord выражается иначе: проверка условия "значение ord слишком мало" может быть представлена так:

p[ordl]2j

Однако эта проверка должна выполняться многократно при одном и том же значении ord. Естественно предположить, что можно ускорить процесс, введя переменную (с именем "квадрат"), значение которой равно p[ord]2.

Итак, мы подходим к завершающему уровню 2b5(4):

integer квадрат; integer array крат[1:30];

2b4(4)а=

begin ord:= l; квадрат:=4 end;

2b4(4)b=

(квадрат j);

2b4(4)c=

begin крат[ord]:= квадрат; ord:=ord+l; квадрат:=p[ord]2 end;

2b4(4)d=

begin while крат[n]<j do крат[n]:=крат[n]+p[n]; r:=j - крат[n]

Тем самым наши вычисления свелись к применению решета Эратосфена.

Замечание. В конкретизации для 2b4(4)d по мере сравнений значений крат[п] с текущим значением j величина крат[п] увеличивается, пока не превысит значение j. Можно было бы наращивать ее с шагом 2*p[n], так как мы рассматриваем только нечетные значения j, а поэтому интересуемся только нечетными кратными для p[n]. (Величина крат[1] сохраняет первоначально установленное значение.

Читателям предоставляется самостоятельно описать конкретизацию для 2с: "печатать p[k] для k от 1 до 1000". Я полагаю, что таблицу следует напечатать на пяти страницах так, чтобы каждая страница содержала четыре столбца по 50 последовательных простых чисел.

Итак, я выполнил поставленную перед собой в начале этого раздела задачу "описать чрезвычайно подробно процесс составления такой (хорошо организованной) программы". Хочу закончить этот раздел некоторыми замечаниями.

Весьма примечательно, что наше рассмотрение очень простой программы оказалось очень продолжительным, поистине слишком продолжительным, даже если принять во внимание, что в сущности мы сделали два дела: составили программу и подробно обсудили соображения, приводящие к ее получению. Меня не очень огорчает пространность рассуждений (ведь писатели заполняют целые романы описаниями человеческих характеров); что меня беспокоит, так это изобилие текстов на различных уровнях. Поэтому особое значение приобретает разработка приемов сокращенной записи.

Однако можно сделать и обнадеживающий вывод. Осознав полностью тот факт, что бедный программист не в состоянии все решить сразу, мы успешно организовали последовательное построение этой программы, принимая на каждом этапе только по одному решению. В нашем примере существенная часть программирования оказалась уже выполненной, в то время как по ряду основных вопросов еще не были приняты решения; независимо от того, какими оказываются заключительные решения, кодировка нижних уровней сохраняется. Это весьма ценно с точки зрения требования об управляемости программ.