
Понятие алгоритма
Обобщив вышесказанное, сформулируем следующее понятиеалгоритма.
Алгоритм—понятное и точное предписание исполнителю на выполнение конечной последовательности действий, приводящей от исходных данных к искомому результату.
Приведенное определение не является определением в математическом смысле слова, т.е. это не формальное определение (формальное определение алгоритма см. в статье “Теория алгоритмов”).
Отметим, что для каждого исполнителянабор допустимых действий (СКИ) всегда ограничен — не может существовать исполнителя, для которого любое действие является допустимым. Перефразированное рассуждение И.Канта обосновывает сформулированное утверждение следующим образом: “Если бы такой исполнитель существовал, то среди его допустимых действий было бы создание такого камня, который он не может поднять. Но это противоречит допустимости действия «Поднять любой камень»”.
Интересно, что существуют задачи, которые человек, вообще говоря, умеет решать, не зная при этом алгоритм ее решения. Например, перед человеком лежат фотографии кошек и собак. Задача состоит в том, чтобы определить, кошка или собака изображена на конкретной фотографии. Человек решает эту задачу, но написать алгоритм решения этой задачи пока чрезвычайно сложно.
С другой стороны, существуют задачи, для которых вообще невозможно построить процедуру решения. Причем данный факт можно строго доказать. Об этом вы можете прочитать в статье “Алгоритмически неразрешимые проблемы” 2.
2. Алгоритмически неразрешимые проблемы
Математики в течение веков пользовались интуитивным понятием алгоритма(см. “Алгоритм”). В рамках подобного определения были сформулированы и успешно применялись на практике алгоритмы для решения таких задач, как выполнение арифметических действий “столбиком”, нахождение корней квадратных уравнений, решение систем линейных уравнений и т.д. Постепенно они переходили к постановке и решению все более сложных задач. Так, Г.Лейбниц в XVII веке пытался построить общий алгоритм решения любых математических задач. В XX веке эта идея приобрела более конкретную форму: построить алгоритм проверки правильности любой теоремы при любой системе аксиом. Построить такие алгоритмы не удавалось, и математики выдвинули предположение: а вдруг для того или иного класса задач в принципе невозможно построить алгоритм решения? На основе этого предположения возникло понятиеалгоритмически неразрешимой задачи— задачи, для которой невозможно построить процедуру решения.
Проблема останова
Одной из первых проблем, для которых была строго доказана алгоритмическая неразрешимость, была так называемая проблема останова. Сформулируем ее для программ, написанных на процедурных языках программирования (см. “Языки программирования”).
Зададимся следующим вопросом. Нельзя ли определить программным способом, с помощью самого компьютера, зациклится ли данная программа на определенных входных данных? Может быть, можно написать некоторую универсальную программу (обозначим ее через U), которая принимала бы на входтекстзаданной программы и входные данные к ней, анализировала его и выдавала бы ответ, зациклится эта программа на этих входных данных или нет1. Возможность написания программыUкажется правдоподобной: ведь, например, программа-компилятор умеет анализировать текст заданной программы на наличие в нем возможных синтаксических ошибок и т.п. ПрограммаUмогла бы стать надстройкой над компилятором, которая вылавливала бы ошибку особого рода — ошибку “бесконечного цикла”.
Уточним формулировку задачи. Каждая
программа Пв каждом конкретном
случае работает с входными данными.
(Строго говоря, некоторые программы,
например программа вычисления числас
точностью до 100 000 знаков, могут и ничего
не получать на вход — в этом случае
будем считать, что входные данные для
такой программы образуют “пустой набор”
— файл из нуля байт.) Можно считать, что
эти входные данные берутся всегда из
какого-то файлаД. Действительно,
все входные данные — символы, вводимые
с клавиатуры, файлы и даже движения
мышки (здесь подразумевается, что нажатие
определенных кнопок в интерфейсе
программы — это тоже ввод данных в нее)
— можно закодировать в одном общем
файле данных. Может случиться, что
некоторая программа, получая на вход
одни данные, зацикливается, а получая
другие — нет.
Работу программы Uможно спроектировать следующим образом. ПрограммаUдолжна получать на вход, во-первых, текст программыП(текстовый файл), а во-вторых, некоторый файл с даннымиД (текстовый файл). Затем она должна проанализировать эти два файла и выдать точный ответ: зациклится ли программа П, еслиПполучила на вход файлД. Можно всегда считать, что программаUможет воспринимать на входлюбыефайлы: например, если файлПне является синтаксически правильной программой на выбранном языке программирования (скажем, Паскале), то программаUэто легко определяет, но все равно считает, что в этом случаеПявляется “программой”: например, такой, которая “ничего не делает” и, следовательно, не зацикливается. Соответственно, если файлДимеет “неправильный формат” (например, на вход программеПтребуется число, а в файлеДимеется что-то другое), то программаПвсегда останавливается на этих “неправильных данных”. Итак, вот более точное описание программыU:
а) программа Uчитает два произвольных файла:ПиД;
б) если файл Псодержит синтаксически правильную программу (для определенности, на Паскале), а файлДпредставляет собой корректные данные для программыП, то программаUпроверяет, зациклится ли Пна данныхД. Если она зациклится, то на экран компьютера будет выдано сообщение “зациклится”, иначе — “не зациклится”;
в) если файлы ПиДне удовлетворяют условиям б), то на экран выдается сообщение “не зациклится” (и в этой ситуации для простоты мы все равно называем Ппрограммой, аД— данными, иПв этом случае не зацикливается наД“по определению”).
Предположим, что нам удалось написать такую программу U. Можно считать, что она тоже написана на Паскале. Теперь мы собираемся написать новую программу, которую обозначим через U1. Но прежде мы введем специальное понятие —стандартный номер файла. Любой файл можно представить как слово, быть может, очень длинное. Каждая “буква” этого слова берется из некоторого алфавита. Например, можно считать, что алфавит для таких слов состоит из 256 символов, а каждая буква в слове — это один байт в файле; в качестве “букв” здесь выступают все ASCII-символы — “настоящие буквы”, знаки препинания, пробел, специальные символы и т.д. Таким образом, все файлы могут быть расположены в некоторую упорядоченную бесконечную последовательность:
Ф0, Ф1, Ф2, Ф3, ... (*)
(Сначала идет пустой файл Ф0, в котором нет ни одного байта. Затем перечисляются в алфавитном порядке все файлы, состоящие из одной “буквы”, затем — состоящие из двух “букв”, и т.д.) В этой последовательностикаждыйфайл получает некоторый номер. Этот номер мы и назовемстандартным номером файла. Ясно, что можно написать программу, которая по заданному файлу вычислит стандартный номер этого файла. (Мы считаем, что у нас есть “идеальный” компьютер. Он не имеет ограничений памяти и поэтому может работать с числами, состоящими из сколь угодно большого количества цифр.) Можно также написать программу, которая по заданному числу восстанавливает файл, стандартный номер которого равенn.
Итак, любойфайл попадает в последовательность (*) и имеет в ней свой уникальный номер, который мы называем стандартным номером этого файла. Значит, и у любого файла с программой (П), и у любого файла данных (Д) есть свои стандартные номера.
Теперь можно написать программу U1, которая будет делать следующее:
1) получать на вход натуральное число i;
2) восстанавливать файл Фiиз последовательности (*), т.е. восстанавливать файл со стандартным номеромi;
3) запускать программу U, подавая ей на вход в качестве файлаПфайл Фi, а в качестве файлаД— тот же самый файл Фi.
Коротко работу программы U1можно описать так: по заданному числуiона определяет, зациклится ли программа, реализованная в файле со стандартным номеромi при работе с данными, записанными в файле, который имеет стандартный номерi(эта программа решаетпроблему самоприменимости).
Ясно, что программу U1всегда можно написать так, чтобы она в самом конце работы некоторой переменнойzприсваивала значение 0 или 1, и в соответствии с этим значением выводила одно из двух сообщений:
if z = 0 then writeln('нe зациклится')
elsewritein('зациклится')
Теперь подвергнем программу U1маленькой переделке. Фрагмент, приведенный выше, заменим на такой (**):
if z = 0 then
repeat
until 1 = 2
else writeln('зациклится')
Эту программу (назовем ее U2) сохраним в другом файле. Что делает U2? Она тоже, как и U1, получает на вход числоi, но отличается от U1вот чем: если выяснилось, что исследуемая программа со стандартным номеромiне зацикливается на файле данных со стандартным номеромi, то сама U2зацикливается, а в противном случае U2выдает сообщение “зациклится” и после этого завершает работу.
Программа U2сама тоже записана в файле. Этот файл обязательно находится в последовательности (*) и имеет некоторый стандартный номер. Пустьk— этот номер. Здесь начинается самое интересное. Что произойдет, если на вход программе U2в качестве числаiмы подадим числоk? В этом случае, конечно, выполнение программы U2может либо зациклиться, либо остановиться. Предположим, что оно остановится. Тогда, как только компьютер дойдет до выполнения фрагмента (**), переменнаяzдолжна получить нулевое значение. После этого, в соответствии с (**), произойдет зацикливание. Предположив, что выполнение остановится, мы выяснили, что выполнение зациклится! Это невозможно. Теперь предположим, что выполнение зациклится. Но тогда программа U2, как говорилось выше, должна выдать сообщение “зациклится” и после этого остановиться. Это тоже невозможно.
Таким образом, программа должна зациклиться, если она остановится, и должна остановиться, если она зациклится. В чем тут дело? Конечно же в том, что мы предполагали возможность написания универсальной программы U, которая определяла бы любую программу на возможность зацикливания. Итак, такой программыUне существует в принципе, или, как говорят математики,проблема останова алгоритмически неразрешима.
Неразрешимость проблемы останова впервые была доказана Аланом Тьюрингом в его работе, опубликованной в 1936 г. Конечно, тогда не было никаких компьютеров и тем более языков программирования, да и сам Тьюринг в той работе даже не пользовался термином “программа”. Но его изложение, по сути, мало чем отличалось от нашего. Мы использовали язык Pascal и говорили о привычных нам компьютерах, однако ясно, что эти подробности (о файлах, о конкретном языке программирования) были совсем не важны: существо наших рассуждений было чисто логическим.