Проблемы остановки и тождественности

Материал из synset
Перейти к: навигация, поиск
Немного программирования << Оглавление >> Перечислимые и разрешимые множества

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

Так как алгоритмы (функции) оперируют только с целыми числами, необходимо разобраться как они будут рассматривать тексты программ. Ранее мы договорились, что программа — это некоторая конечная строка символов из конечного алфавита. Поэтому, все возможные программы можно отсортировать сначала по длине, а при одинаковой длине — по алфавиту (лексографический порядок). Каждая получит порядковый номер. Выше это и предполагалось, так как имя каждой функции имело вид , где — некоторое натуральное число.

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

Такое представление удобно тем, что легко провести обратную операцию и, разложив номер программы на простые множители, получить её текст в виде последовательности . Естественно, подобные действия являются алгоритмическими, и существует функция , дающая -й "символ" программы под номером . Буква дана программе в честь Гёделя, который подобным образом предложил нумеровать формулы.

Вернёмся к Проблеме Остановки и рассмотрим сначала простое рассуждение, отличное от того, которое сделал сам Тьюринг. Будем большими буквами обозначать имена программ (алгоритмов), подразумевая, что за этим именем скрывается строка символов, которая его задаёт. Маленькими буквами обозначим целочисленный номер этого алгоритма. Так, — это номер алгоритма (строки) .

Пусть существует функция , которая по номерам -той программы и её -тых входных данных возвращает 1, если эта программа завершается, выдав результат, и ноль в противном случае:

Невозможно разобрать выражение (неизвестная функция «\begin{array}»): {\displaystyle T(p, i):\; \left\{ \begin{array}{l} 1,\;\;\;если\;P(i)\;возвращает\;значение\\ 0,\;\;\;если\;P(i)\;не\;возвращает\\ \end{array} \right.}

Докажем теорему:

Функция не существует.

Пусть это не так, и существует. Определим ещё одну функцию:

Невозможно разобрать выражение (неизвестная функция «\begin{array}»): {\displaystyle Q(x):\; \left\{ \begin{array}{ll} 1,\;\;\;&если\;T(x,x)=0\\ \infty,\;\;&если\;T(x,x)=1.\\ \end{array} \right.\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;}

Символ означает, что если , то мы зациклим программу , не дав ей остановиться. Например, в этом случае будет запускаться строка "while(1=1)". Вычислим теперь значение , передав ей её собственный номер:

Невозможно разобрать выражение (неизвестная функция «\begin{array}»): {\displaystyle \left\{ \begin{array}{ll} 1, &если\;T(q,q)=0\;\;:если\;Q\;не\;завершается,\;то\;Q\;завершается = 1\\ \infty, &если\;T(q,q)=1\;\;:если\;Q\;завершается,\;то\;Q\;не\;завершается \,(\infty)\\ \end{array} \right.}

Мы пришли к противоречию, значит теорема верна .

В доказательстве обсуждается два объекта: "неконструктивная" функция (текста которой мы не предъявляем) и "конструктивная" . Поэтому противоречие должно свидетельствовать в пользу теоремы.

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

Не существует функции способной проанализировать свой текст и выяснить, остановится она или нет.

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

Исходные рассуждения Тьюринга выглядели несколько по-другому. Рассмотрим все программы, вычисляющие функции от одного аргумента. Их бесконечно много, но они счётны: , т.е. пронумерованы (нумерацию ведём с нуля). Пусть существует функция определённая выше. Тогда рассмотрим функцию:

Невозможно разобрать выражение (неизвестная функция «\begin{array}»): {\displaystyle F(x):\; \left\{ \begin{array}{ll} F_x(x)+1,\;\;\;&если\;T(x,x)=1,\;т.е.\;F_x\;при\;x\;определена\\ 0, \;\;\;&если\;T(x,x)=0,\;т.е.\;F_x\;при\;x\;не\;определена.\\ \end{array} \right.}

Она отлична от всех функций . Действительно, если значение функции определено, то и не равно . Если же значение не определено (она не останавливается), то определено и равно 0. Поэтому отлична от при . Аналогично отлична от при , и т.д. Но это противоречит тому, что все функции были пронумерованы. Поэтому объект использовавшийся при построении невозможен, и теорема доказана .

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

Так как алгоритма решающего проблему остановки и применимого к самому себе не существует, то естественно не существует и универсальных алгоритмов, применимых ко всем программам. А это, собственно, и составляло теорему Тьюринга.

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

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

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

Каждой функции можно поставить в соответствие множество имён (или номеров) других функций, которые она использует при своей работе ("множество зависимостей"). Для этого необходимо использовать алгоритм "поиска в ширину":

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

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

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

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

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

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

Существует ли универсальный алгоритм, который выясняет тождественность двух функций?

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

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

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

Функции, не использующие операции и вызывающие только примитивно рекурсивные функции, всегда останавливаются.

Примером примитивной рекурсии является реализация функции на стр. \pageref{prg_mult_rec}. В общем случае она определяется так:

Logic8.png

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

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

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

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

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

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

Прежде чем идти дальше, полезно поразмышлять над следующей проблемой. В математике нет ограничений на быстродействие компьютера. Т.е. каждая элементарная операция алгоритма может быть выполнена за сколь угодно короткое время . Если функция вычислима, то её алгоритм, совершив конечное число действий , останавливается и выдаёт результат. А это значит, что каким бы большим не было , время вычисления функции можно сделать сколь угодно малым, в пределе (ограничений нет!), в том числе и нулевым. Но тогда не должно быть и Проблемы Остановки. Любая определённая при данном функция сразу выдаёт результат, а неопределённая не выдаёт. Между алгоритмом с большим, но конечным числом шагов и алгоритмом с бесконечным числом шагов существует принципиальная разница. Это два разных класса алгоритмов. Но различить их надо без компьютеров...


Немного программирования << Оглавление >> Перечислимые и разрешимые множества

Истинность и доказуемость - о конструктивной математике, Канторе и Гёделе