Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Теоретическая информатика. Доказательство правильности. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
Глава 3. ДОКАЗАТЕЛЬСТВО ПРАВИЛЬНОСТИ
ПРОГРАММ, НАПИСАННЫХ НА ОБЫЧНЫХ
ЯЗЫКАХ ПРОГРАММИРОВАНИЯ
3.1. ОСНОВНЫЕ ПРИЕМЫ ДОКАЗАТЕЛЬСТВА ПРАВИЛЬНОСТИ ПРОГРАММ
Метод индуктивных утверждений, о котором шла речь в предыду­щей главе, можно непосредственно использовать для доказательства частичной правильности программ, написанных на каком-нибудь тра­диционном языке программирования, например на Фортране, Паскале или Си++. Конечность таких программ можно доказывать так же, как мы это делали для блок-схем. При использовании метода индуктивных утверждений необходимо, по крайней мере, с одной из точек програм­мы в каждом из замкнутых путей (циклов) связать соответствующее утверждение. Конечно, если мы используем язык программирования, то порядок выполнения (пути) неявно определяется структурой управ­ления, тогда как на блок-схемах этот порядок явно указывается стрел­ками. Следовательно, применяя метод индуктивных
утверждений, нужно четко представлять порядок выполнения программы, чтобы быть уверенным, что не пропущен ни один из замкнутых путей в про­грамме. Именно этот вопрос мы и рассмотрим здесь, так как сам метод утверждений мы уже описали выше, а его использование в данном случае аналогично использованию для блок-схем.
Проиллюстрируем метод на
простых программах, написанных на
Фортране.
Пример 3.1
READ (5, 1) J1, J2 (Операторы READ… FORMAT производят ввод 1 FORMAT (2I10) информации по соответствующему формату)
С ЗНАЧЕНИЯ, СЧИТЫВАЕМЫЕ В J1 И J2 – С ЦЕЛЫЕ, УДОВЛЕТВОРЯЮЩИЕ УСЛОВИЯМ С 0.LE.J1 И 1.LE.J2. (логические операции: LE – “” ;
IQ=0 IR=J1 2 IF(IR .LT.J2) GO TO 4
LT – “< “ EQ – “=” GE – “”
С J1 .EQ. IQ * I2 + IR AND 0 .LE. IR GT – “>” IQ=IQ+1 NE – “” IR=IR–J2
31
3 GO TO 2 (C – признак комментария) 4 WRITE (6,5) IQ, IR (Операторы WRITE … FORMAT
производят вывод
5 FORMAT (2I10) информации по соответствующему формату)
С JL .EQ. IQ*J2 + IR AND 0.LE. IR AND С IR .LT. J2
STOP END
Приведенная программа на Фортране – это просто некоторый вари­ант программы, блок-схема которой показана на рис. 3. Напомним, что программа вычисляет целое частное IQ и
остаток IR от деления це­лого J1 на целое J2. Мы уже включили в качестве комментариев в программу индуктивные утверждения, необходимые для доказатель­ства частичной правильности. Например, комментарий, помещенный сразу после оператора с меткой 2, нужно рассматривать как коммен­тарий, связанный с точкой, расположенной перед выполняемой в этом операторе проверкой. Таким образом
, предполагается, что в момент
прихода в точку, находящуюся непосредственно перед проверкой в операторе с меткой 2, справедливы утверждения J1 = IQ J2 + IR и 0 IR. Отметим, что в доказательствах мы используем „ = " как символ равенства, а не присваивания.
После этого доказательство частичной правильности данной про-
граммы почти идентично доказательству частичной правильности
для блок-схемы на рис. 3. При этом необходимо рассмотреть следующие пути.
1. Путь от оператора READ до оператора с меткой 2. Предпо­ложим, что только что выполнился оператор READ и справедливо утверждение, записанное сразу после него. Теперь последовательно выполняются операторы до оператора с меткой 2. Нам нужно показать, что справедливо утверждение, записанное после этого
оператора. Если
мы дошли до этой точки, то имеем IQ = 0, IR = J1, 0 J1 и 1 J2. Таким образом, J1=IQ J2 + IR = 0 J2 + J1 = J1 и 0 J1 = IR.
Следовательно, наше утверждение справедливо.
2. Путь от оператора с меткой 2 до оператора с меткой 3 и опять к оператору с
меткой 2 (основной цикл программы). Предполо-
жим, что мы выполняем оператор 2 (мы перестаем говорить «оператор с меткой 2» и начинаем употреблять «оператор 2». Здесь уместно от-
32
метить, что если убрать из языка программирования метки, то иденти­фицировать оператор станет весьма затруднительно) и справедливо записанное после него утверждение, затем выполняем цикл и возвра­щаемся к метке 2. Необходимо показать, что указанное утверждение снова будет справедливо. Пусть IQ и IR до выполнения цикла при­нимают значения IQ
и IRN. Тогда J1 = IQN J2 + IRN и 0  IRN. При
N
возврате к метке 2 после прохождения цикла значения IQ и IR будут
= IQN + 1 и IR
IQ
N+1
= IRN – J2, а значения J1 и J2 останутся без
N+1
изменений. Таким образом, после возврата к метке 2 имеем
J2 + IR
IQ
N+1
= IQ
J2 + J2 + IRN – J2 = IQN J2 + IRN = J1.
N
= (IQN + 1) J2 + (IRN – J2) =
N+1
Кроме того, известно, что если мы проходили по циклу, то провер-
ка IR следует, что при возврате к метке 2 имеем 0 IR
.LT. J2 дала отрицательный результат, т. е. J2 IRN. Отсюда
N
– J2 = IR
N
N+1
.
3. Путь от оператора 2 к оператору 4. Предположим, что мы вы-
полнили оператор 2, справедливо соответствующее утверждение, и переходим к оператору 4. Нужно показать, что справедливо утвержде­ние, записанное ниже этого оператора. Оператор 2 передаст управле­ние на оператор 4 только при положительном результате проверки IR .LT. J2. При переходе от оператора 2 к оператору 4 ни
одно из зна-
чении переменных не изменяется и, следовательно, при достижении метки 4 мы имеем J1 = IQ J2 + IR, 0 IR и, кроме того, IR < J2. Таким образом, мы доказали частичную правильность.
Доказательство конечности программы на Фортране идентично до­казательству конечности для блок-схем. Для доказательства конечно­сти программы необходимо только
показать, что при выполнении про­граммы мы в конце концов выйдем из единственного в программе цикла. Следовательно, надо показать, что проверка IR .LT. J2, управ- ляющая циклом, когда-нибудь даст положительный результат. Так как значение IR при каждом прохождении цикла уменьшается на величи­ну J2, a J2 остается без изменений и, кроме
того, 1 J2, то можно
заключить, что IR уменьшается каждый раз, по крайней мере, на 1 и когда-нибудь станет меньше J2. В этот момент условие IR < J2 ста­нет справедливым и мы выйдем из цикла, т. е. программа закончится.
33
Пример 3.2
T
С1 Х – ВЕЩЕСТВЕННЫЙ МАССИВ, ЕГО РАЗМЕР С1 N .GE. 2
XSMLST = X(1)
DO 10 I = 2, N C2 XSMLST РАВНО МИНИМАЛЬНОМУ ИЗ Х(1), ..., Х(I – 1) С2 КРОМЕ ТОГО, 2 .LE. I И I .LE. N
IF (XSMLST .LE. X(I)) GO TO 10
XSMLST = X(I) C3 XSMLST РАВНО МИНИМАЛЬНОМУ Х(1), ..., X(I – 1) C3 КРОМЕ ТОГО, 3 .LE. I И I .LE. N + 1
10 CONTINUE C4 XSMLST РАВНО МИНИМАЛЬНОМУ ИЗ Х(1),..., X(N)
Предполагается, что приведенный фрагмент программы должен
присвоить XSMLST значение наименьшего элемента из массива Х(1),
... , X(N). В Фортране цикл строится по следующей основной схеме:
DO метка I = M, N С ПРИМЕЧАНИЕ 1 Тело цикла
I M
Утверждение 1
тело цикла
С ПРИМЕЧАНИЕ 2 метка CONTINUE
I  I + 1
I  N
Утверждение 2
F
Рис. 10
Эта схема работает аналогично приведенной на рис. 10 блок-схеме.
34
Примечание 1 (комментарий), связанное с таким циклом и стоящее непосредственно за оператором DO, нужно рассматривать как связан­ное с некоторой точкой утверждение 1 на блок-схеме, а примечание 2, стоящее перед концом цикла, нужно рассматривать как утверждение 2 для некоторой точки программы, соответствующей точке на блок­схеме.
Для доказательства частичной правильности программы нужно рассмотреть следующие пути
.
1. Путь от начала фрагмента до точки 2 (точки, с которой
связано утверждение 2). Если мы попадаем в эту точку, то XSMLST = = Х(1) и I = 2 и очевидно, что значение XSMLST равно наименьше-
му из Х(1), ..., Х(I – 1) = Х(2 – 1) = Х(1) и 2  I = 2 N (так как N   2). (Обратите внимание, что без
условия N 2 наше утверждение
доказать нельзя и, следовательно, правильная работа программы не гарантируется, если N = 1.)
2. Путь от точки 2 до точки 3 (точки, с которой связано утвер-
ждение 3). Предположим, что мы достигли точки 2, утверждение 2
справедливо, I имеет значение I вращаясь в точку 2. В теле цикла XSMLST сравнивается с Х(I и, если XSMLST X(I
), XSMLST остается без изменений. В против-
N
ном случае значение заменяется на Х(I что значение XSMLST равно наименьшему из Х(1), ... , Х(I
и мы проходим по телу цикла, воз-
N
). Так как до этого известно,
N
– 1), то
N
)
N
можно легко убедиться, что теперь значение XSMLST будет равно наименьшему из Х(1), ... , Х(I точки 3, значение I увеличится на 1, и, таким образом, I
). Однако, прежде чем мы дойдем до
N
= IN + 1;
N+1
следовательно, значение XSMLST снова будет равно наименьшему из Х(1), ... , Х(I
2 I
N, и, следовательно, в точке 3: 2 3 I
N
) = Х(I
N
– 1). Кроме того, из примечания 2 известно, что
N+1
= IN + 1 N + 1.
N+1
3. Путь из точки 3 к точке 2. Мы пройдем по этому пути, если
только проверка I N даст положительный ответ. При этом условии справедливость примечания 3 приводит к справедливости примечания
2 в момент достижения этой точки.
4. Путь из точки 3 в точку 4. Мы пройдем по этому пути, если
только проверка 1
N даст отрицательный ответ, т. е. при I > N. Но
так как справедливо утверждение в примечании 3, следовательно, I = N + 1, и при достижении точки 4 значение XSMLST будет равно наименьшему из Х(1), ... , Х(I – 1) = X[(N + 1) – 1] = X(N), что и тре­бовалось доказать, т. е. доказана частичная правильность.
35
Конечность этого фрагмента программы очевидна: здесь есть толь­ко один цикл, и он является фортрановским, а конечность фортранов­ского цикла, следует считать свойством, присущим самому языку.
Эти два примера показывают, что метод индуктивных утверждений можно распространить непосредственно и на доказательства правиль­ности программ, написанных на Фортране. Затруднение заключается лишь в том
, что управление в программах на Фортране не такое явное, как в блок-схемах. Это приводит к тому, что в программах легко не заметить некоторые пути или неправильно их интерпретировать (например, можно предположить, что в операторе цикла проверка вы­полняется перед выполнением тела цикла). Кроме того, нужно припи­сывание индуктивных утверждений
к некоторым точкам в программе на Фортране проводить очень внимательно и все время помнить, с ка­кой неявной точкой связано соответствующее утверждение. Например, один-единственный оператор Фортрана, такой, как оператор DO, фак­тически состоит из нескольких действий: установки счетчика, увели­чения счетчика и проверки. При доказательстве правильности мы должны четко осознавать,
с какой из этих точек связано индуктивное утверждение. Использование переходов GO TO в программах на Форт­ране тоже порождает проблемы. Если в программе много переходов и при их использовании не придерживались некоторой дисциплины, то программу часто трудно понимать и трудно доказывать ее правиль­ность. Это объясняется тем, что сложно проследить за всеми возмож ными путями выполнения программы и указать нужные для этих путей индуктивные утверждения. Однако существует и иная точка зрения. Если у нас есть только переходы, то программа на Фортране становит­ся полностью аналогичной блок-схеме и доказательство правильности проходит так же легко, как и для блок-схем, ибо мы «видим» все
точки
с утверждениями. Но переходы действительно усугубляют трудности
«неявных» точек, например в операторе цикла.
-
3.2. ЗАДАНИЯ ДЛЯ САМОСТОЯТЕЛЬНОЙ РАБОТЫ
На одном из языков программирования напишите программу по предлагаемым ниже вариантам, расставьте индуктивные утверждения и докажите ее правильность. Блок-схемы программ приведены в [4, рис. 2.3–2.5].
1. Вычисление произведения M на N при условии, что операцию.
умножения использовать нельзя, M и N имеют целые значения и M 0.
36
2. Вычисление и печать значения M N при условии, что M и N
имеют целые значения и M 1, N 1.
3. Вычисление и печать значения M! = 1  2  3    (M – 1) M при
условии, что в качестве M вводится положительное целое.
4. Поиск минимального элемента одномерного массива.
3.3. КОНТРОЛЬНЫЕ ВОПРОСЫ К ГЛАВЕ 3
Тема «Доказательство правильности программ»
1. Можно ли применять для доказательства правильности программ
математическую индукцию?
2. Можно ли применять метод индуктивных утверждений для дока-
зательства правильности программ?
3. Как в программе записываются индуктивные утверждения?
4. Назовите три основных отличия доказательства правильности
программ от доказательства правильности блок-схем.
Глава 4. ДОКАЗАТЕЛЬСТВО ПРАВИЛЬНОСТИ
РЕКУРСИВНЫХ ПРОГРАММ
Во многих языках программирования разрешается писать рекур­сивные программы, т. е. программы, в которых при вычислениях до­пускается обращение (рекурсивное) к самой себе аналогично тому, как обычная нерекурсивная программа обращается к некоторой подпро­грамме. Рекурсивные программы особенно удобны в тех случаях, ко­гда речь идет о данных, структура которых определяется (например, списки или деревья). В языке Лисп, созданном специально для «нечисленных» вычислений на основе списков, понятие рекурсии является фундаментальным. В этой главе описаны методы, которые полезны при доказательстве правильности рекурсивных программ. Ос­новной метод в таких доказательствах называется методом структур­ной индукции. Доказательство методом структурной индукции пред­ставляет собой но индукция осуществляется по структуре данных, с которыми работа­ет программа.
доказательство с помощью математической индукции,
рекурсивно
37
Если студент незнаком с понятием рекурсии, он все равно сможет понимать излагаемый здесь материал. Мы не будем пользоваться ка­ким-нибудь стандартным рекурсивным языком программирования, например, Алголом или Лиспом, а «изобретем» упрощенный язык, ко­торого нам будет достаточно, чтобы проиллюстрировать понятие ре­курсии.
4.1. УПРОЩЕННЫЙ ЯЗЫК ПРОГРАММИРОВАНИЯ ДЛЯ ИЛЛЮСТРАЦИИ ПОНЯТИЯ РЕКУРСИИ
Упрощенный язык программирования нужен нам, чтобы ознако­мить студентов с понятием рекурсии. Если студент знаком с рекурсией в каком-либо из существующих языков программирования, ему будет очень просто сопоставить наш упрощенный язык с соответствующими возможностями в этих реальных языках. Однако, если вы даже и не­знакомы с понятием рекурсии из существующих
языков, это не поме-
шает вам понять содержание настоящего раздела.
Программа в нашем упрощенном языке программирования состоит из определения функции, заданного в виде
F(Х1, ... , ХN) IF проверка 1 THEN выражение 1
ELSE IF проверка 2 THEN выражение 2
…
ELSE IF проверка m THEN выражение m
ELSE OTHERWISE выражение m + 1
Такая программа вычисляет значение F(Х1, ... , ХN). Сначала вы­полняется проверка 1. Если
проверка 1 дает ответ «истина», то значе­ние функции есть значение выражения 1. Если проверка 1 дает ответ «ложь», то выполняется проверка 2. Если эта проверка дает ответ «ис­тина», то значение функции есть значение выражения 2. Процесс идет таким образом до тех пор, пока не будет обнаружена первая из прове-
i-я), дающая значение «истина». Значение функции в этом случае
рок ( есть значение выражения i. Если ни одна из проверок не окажется положительной, то значение функции есть значение выражения m + 1. Рекурсия вводится в этот язык программирования следующим обра­зом: функция F, определяемая в нашей программе, может входить как
38
часть в любое из выражений или проверок в программе. Такое появле­ние F называется рекурсивным обращением к этой функции. Для каж­дой такой рекурсивной программы нужно еще указать, для каких зна­чений аргументов X1, ... , ХN применима программа, т. е. нужно задать область определения функции. Выполнение программы заключается в применении ее к некоторым конкретным
данным из ее области опре-
деления.
Пример 4.1. Рассмотрим рекурсивную программу, определенную
для любого положительного целого числа X:
F(Х) IF Х = 1 THEN 1 ELSE OTHERWISE X•F(Х–1)
Чтобы понять, как работает такая программа, выполним ее для не-
которого конкретного значения аргумента X. Например,
F(4) = 4 • F(3) [Так как условие 4 = 1 ложно, то F(4) = 4•F(3).
Теперь нужно вычислить F(3), т. е. рекурсивно обратиться к F.]
= 4 • 3 • F(2) [Так как при вычислении F(3) условие 3 = 1 ложно,
то F(3) = 3•F(2).]
= 4 • 3 • 2 • F(1) [Так как условие 2 = 1 ложно, то F(2) = 2•F(1).] = 4 • 3 • 2 • 1 [Так как условие 1 = 1 истинно, то F(1) = 1.]
= 24 = 4 !
Далее мы докажем, что F(Х) = Х! для любого положительного це-
лого числа X.
Пример 4.2. Рассмотрим следующую рекурсивную функцию
(функцию Аккермана), применимую для любой пары неотрицательных целых чисел X1, Х2:
А(Х1, Х2) IF X1 = 0 THEN Х2 + 1
ELSE IF Х2=0 THEN А(Х1–1, 1) ELSE OTHERWISE А(Х1–1, А(Х1, Х2–1))
Проследим, как выполняется такая программа для некоторой пары
X1 и Х2.
39
А(1, 2) = А(0, А(1, 1)) [Так как условия X1 = 0 и Х2 = 0 ложны, необходимо вычислять А(1, 1) – рекурсивное обращение.] = А(0, А(0, А(1, 0))) [Так как условия X1 = 0 и Х2 = 0 ложны,
нужно вычислять А(1, 0).]
= А(0, А(0, А(0, 1))) [Так как условие X1 = 0 ложно,
а Х2 = 0 истинно.]
= А(0, А(0, 2)) [Так как X1 = 0 истинно,
следовательно, А(0, 1) = 1 + 1 = 2.]
= А(0, 3) [Так как X1 = 0
истинно, следовательно,
А(0, 2) = 2 + 1 = 3.]
= 4 [Так как X1 = 0 истинно, следовательно, А(0, 3) = 3 + 1 = 4.]
Из этого примера следует, что при выполнении вычислений для ре­курсивной программы мы можем сталкиваться с неоднозначностями, которые требуют уточнений. Например, в приведенных вычислениях при обращении А(0, А(1, 1)) мы предполагаем, что А(1, 1) должно быть вычислено прежде, чем мы продолжим вычисления, относящиеся к внешнему обращению к А. Если отдать предпочтение вычислениям, связанным с внешним обращением, то вычисления должны были бы идти в следующем порядке:
А(1, 2) = А(0, А(1, 1)) = А(1, 1) + 1 = А(0, А(1, 0)) + 1 =
= (А(1, 0) + 1) + 1 = (А(0, 1) + 1) + 1 = (2 + 1) + 1 = 4.
Значение А(1, 2) остается тем же, хотя последовательность вычис­лений отлична от предыдущей. Можно доказать, что если к одним и
же аргументам некоторой рекурсивной функции применять две
тем различные последовательности ее вычисления, то результат будет од­ним и тем же при условии конечности этих последовательностей. Од­нако вполне возможно, что одна последовательность не будет заканчи­ваться, хотя другая последовательность закончится. Рассмотрим сле­дующий пример.
40
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]