Московский государственный индустриальный университет
Опубликован: 27.09.2006 | Доступ: свободный | Студентов: 3331 / 379 | Оценка: 4.17 / 3.79 | Длительность: 24:17:00
Специальности: Программист
Лекция 7:

Базисные схемы обработки информации

< Лекция 6 || Лекция 7: 12345 || Лекция 8 >
Аннотация: Группы задач на написание программ. Рекурсия и итерация. Инвариант и ограничивающая функция цикла. Схема вычисления инвариантной функции. Функции на пространстве последовательностей.
Ключевые слова: доказательство, класс, проектирование сверху вниз, программирование, объектно-ориентированное проектирование, деление, программная реализация, рекурсия, программа, итерация, рекурсивно определенная функция, операции, умножение, рекурсивный вызов, значение, математическая индукция, предикат, равенство, вызов функции, коэффициенты, вычисление, ПО, функция, цикл, спецификация задачи на итерацию, Произведение, цикла, условие продолжения цикла, слабейшее предусловие, интерпретация, математическая модель итерации, метод проектирования цикла при помощи инварианта, выражение, инвариант, инвариант цикла, тавтология, определение, ограничивающая функция цикла, тело цикла, подмножество, множества, T-инвариантная функция, , common divisor, пустой оператор, индуктивная функция, алфавит, произвольное, символ алфавита, цепочка над алфавитом, длина цепочки, отображение, схема Горнера, правильная программа, обобщение, целое число, остаток

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

В качестве дополнительного источника информации к данной главе можно порекомендовать книги [4] и [9].

Все задачи на написание программ можно разделить на следующие три группы.

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

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

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

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

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

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

Базисные схемы обработки информации

Рис. 7.1. Базисные схемы обработки информации

Рекурсия и итерация

Как определения этих двух базисных схем обработки информации, так и их взаимосвязь уже были рассмотрены нами ранее. Напомним самое главное.

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

Итерация — способ организации обработки данных, при котором определенные действия повторяются многократно, не приводя при этом к рекурсивным вызовам программ.

Любой алгоритм, реализованный в рекурсивной форме, может быть переписан в итерационном виде, и наоборот.

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

1) всегда ли и почему программа заканчивает работу?

2) почему после окончания работы программы будет получен требуемый результат?

Рассмотрим следующую задачу.

Задача 7.1. Напишите рекурсивную программу, перемножающую два целых числа, одно из которых неотрицательно, без использования операции умножения. Точные пред- и постусловия требуемой программы, временная сложность которой не должна превосходить \Theta(\log b), таковы: Q=(a\in \mathbb{Z}_M \land b\in \mathbb{Z}_M \land b \geqslant 0), R=(z=ab). Числа a и b в программе изменять нельзя.

Решение Фиксировав a, рассмотрим функцию mul_a(b)\colon \mathbb{Z}_M
\rightarrow \mathbb{Z}_M, задаваемую следующим рекурсивным определением:

mul_a(0)=0,

mul_a(b) = 2\cdot mul_a(b/2), когда b положительно и четно,

mul_a(b) = mul_a(b-1) + a, когда b положительно и нечетно.

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

Текст программы

public class MulR {
    static int mul(int x, int y) {
        // Xterm.print("  Вызов mul(" + x + "," + y + ")\n");
        return (y == 0) ? 0 :
            ((y&1) == 0) ? mul(x,y >>> 1) << 1 : mul(x,y-1) + x;
    }	
    public static void main(String[] args) throws Exception {    
        int a = Xterm.inputInt("a -> ");
        int b = Xterm.inputInt("b -> ");
        Xterm.println("a * b = " + mul(a,b));
    }
}

Эта программа заканчивает работу, так как каждый следующий рекурсивный вызов уменьшает значение аргумента y функции mul_a не менее, чем на единицу (нечетное число уменьшается ровно на один, а четное — делится пополам, что при y=2 приводит к уменьшению на один, а при больших значениях y — к большему уменьшению). В результате конечного числа таких операций y гарантированно станет нулем, что приведет к завершению цепочки рекурсивных вызовов.

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

Доказательство правильности получаемого результата проведем с помощью метода математической индукции, показав, что при фиксированном a и 0\leqslant b\leqslant n является тавтологией предикат P(b) = ( данная программа печатает a\cdot
b ).

Справедливость базы индукции P(0) следует непосредственно из текста программы, а индуктивный переход P(n) \Rightarrow P(n+1) осуществляется следующим образом. Если предположить, что программа правильна при b=n, то mul_a(n) = a\cdot
n. Рассмотрим далее два возможных случая.

Пусть сначала число y=n+1 — четно. Тогда результатом работы программы будет

mul_a(n+1) = 2\cdot mul_a((n+1)/2) = 2\cdot a
\cdot
((n+1)/2) = a\cdot (n+1)
. Первое равенство здесь следует из текста программы, а второе — из предположения индукции. Если число y=n+1 является нечетным, то mul_a(n+1) =
mul_a(n) + a =
a\cdot n + a = a\cdot (n+1). Обоснование этих равенств проводится аналогично.

Таким образом, нами проверена истинность индуктивного перехода P(n) \Rightarrow P(n+1), что и завершает доказательство.

Для того чтобы увидеть цепочку рекурсивных вызовов функции mul_a достаточно убрать символы комментария из первой строки этого метода. Это позволяет понять, почему программа имеет сложность порядка \Theta(\log b). Докажем аккуратно, что глубина рекурсии (количество рекурсивных вызовов) для нашей программы не превосходит 2\cdot [\log b] + 1 (здесь [x] — целая часть от x ).

Каждый рекурсивный вызов функции mul_a уменьшает значение аргумента y либо на единицу, либо вдвое. Два последовательных вызова этой функции всегда приводят к уменьшению величины y более, чем в два раза, поэтому y обратится в нуль после не более, чем после 2\cdot [\log
b] +1 рекурсивных вызовов. Обратите внимание, что мы не учитываем в наших рассуждениях самый первый, нерекурсивный вызов функции mul_a.

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

Рассмотрим еще одну задачу.

Задача 7.2. Напишите программу, печатающую значение многочлена степени n\geqslant0 в заданной точке x_0. Коэффициенты многочлена хранятся в массиве a в порядке убывания степеней и являются целыми числами, также как и значение x_0. Величины n, x_0 и элементы массива a изменять в программе нельзя.

Решение Заметим, что

P_n(x)=a_0x^n+a_1x^{n-1}+\ldots+a_{n-1}x+a_n =
x\cdot P_{n-1}(x) + a_n
, где P_{n-1}(x)=a_0x^{n-1}+a_1x^{n-2}+\ldots+a_{n-1}. Воспользовавшись этим, определим функцию f\colon \mathbb{Z}_M \rightarrow
\mathbb{Z}_M следующим образом:

f(0) = a_0,

f(n) = x_0\cdot f(n-1) + a_n, при n>0.

Теперь можно написать программу, реализующую вычисление этой функции.

Текст программы

public class PolVal {
    static int a[], x0;
    static int p(int k) {
        return (k == 0) ? a[0] : x0 * p(k-1) + a[k];
    }	
    public static void main(String[] args) throws Exception {    
        int n = Xterm.inputInt("n -> ");
        a = new int[n+1];  
        for (int i=0; i<=n; i++)
            a[i] = Xterm.inputInt("a[" + i + "] -> ");
        x0 = Xterm.inputInt("x0 -> ");
        Xterm.println("P(" + x0 + ") = " + p(n));
    }
}

Завершение программы за конечное число шагов следует из уменьшения на единицу при каждом рекурсивном вызове аргумента функции p, а ее правильность получается из доказательства по индукции следующего утверждения R(n) = ( программа печатает значение P(x_0) многочлена P_n(x)=a_0x^n+a_1x^{n-1}+\ldots+a_{n-1}x+a_n степени n в точке x_0 ).

База индукции проверяется непосредственно, ибо при нулевом значении аргумента функция p(k) возвращает a[0], что совпадает с P_0(x_0). Индуктивный переход удается осуществить благодаря формуле, приведенной в начале решения данной задачи, которая позволяет выразить значение P_n(x_0) через P_{n-1}(x_0).

Рекурсивное решение задачи зачастую является менее эффективным, чем эквивалентное ему по другим параметрам итерационное решение. Обычным способом программной реализации итерации является цикл, и поэтому мы будем считать, что спецификация задачи на итерацию имеет вид \{Q\}\ "S0;while(e)S;" \ \{R\}.

Математическая модель итерации

Рис. 7.2. Математическая модель итерации

Рассмотрим X — множество значений всех программных переменных (прямое произведение множеств значений отдельных переменных), его подмножества X_Q \subset X и X_R \subset X, определяемые предикатами Q и R, и преобразование T\colon X \rightarrow X, соответствующее однократному выполнению тела цикла S. Тогда построение условия продолжения цикла e и его тела S сводится к нахождению предиката e и преобразования T таких, что \forall x \in X_Q\ \exists k \geqslant 0\
T^k(x) \in X_R.

Именно этот факт является основным результатом, вытекающим из определения слабейшего предусловия wp для оператора цикла (см. "Спецификация программ и преобразователь предикатов" ). Геометрическая интерпретация математической модели итерации, сводящейся к многократному применению преобразования T, приведена на рис. 7.2.

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

< Лекция 6 || Лекция 7: 12345 || Лекция 8 >
Анастасия Халудорова
Анастасия Халудорова
екатерина яковлева
екатерина яковлева