Опубликован: 08.04.2009 | Уровень: для всех | Доступ: платный
Лекция 15:

Контекстно-свободные грамматики

15.2. Метод рекурсивного спуска

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

Идея метода рекурсивного спуска такова. Для каждого нетерминала K мы строим процедуру ReadK, которая - в применении к любому входному слову x - делает две вещи:

  • находит наибольшее начало z слова x, которое может быть началом выводимого из K слова;
  • сообщает, является ли найденное слово z выводимым из K.

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

\begin{center}\ttfamily
     Next: Symbol
\end{center}
дающая первый непрочитанный символ. Ее значениями могут быть терминальные символы, а также специальный символ EOI (End Of Input - конец входа), означающий, что все слово уже прочитано. Вызов этой функции, разумеется, не сдвигает границы между прочитанной и непрочитанной частью - для этого есть процедура Move, которая сдвигает границу на один символ. (Она применима, если Next<>EOI.) Пусть, наконец, имеется булевская переменная b.

Теперь мы можем сформулировать наши требования к процедуре ReadK. Они состоят в следующем:

  • ReadK прочитывает из оставшейся части слова максимальное начало A, являющееся началом некоторого слова, выводимого из K ;
  • значение b становится истинным или ложным в зависимости от того, является ли A выводимым из K или лишь невыводимым началом выводимого (из K ) слова.

Для удобства введем такую терминологию: выводимое из K слово будем называть K -словом, а любое начало любого выводимого из K слова - K -началом. Два сформулированных требования вместе будем выражать словами " ReadK корректна для K ".

Начнем с примера. Пусть правило

\begin{center}\ttfamily
K \to L M
\end{center}
является единственным правилом грамматики, содержащим K в левой части, пусть L, M - нетерминалы и ReadL, ReadM - корректные (для них) процедуры.

Рассмотрим такую процедуру:

procedure ReadK;
begin
| ReadL;
| if b then begin
| | ReadM;
| end;
end;

15.2.1. Привести пример, когда эта процедура будет некорректной для K.

Ответ. Пусть из L выводится любое слово вида 00..00, а из M выводится лишь слово 01. Тогда из K выводится слово 00001, но процедура ReadK этого не заметит.

Укажем достаточные условия корректности процедуры ReadK. Для этого нам понадобятся некоторые обозначения. Пусть фиксированы КС-грамматика и некоторый нетерминал N этой грамматики. Рассмотрим N -слово A, которое имеет собственное начало B, также являющееся N -словом (если такие есть). Для любой пары таких слов A и B рассмотрим терминальный символ, идущий в A непосредственно за B. Множество всех таких терминалов обозначим Посл(N). (Если никакое N -слово не является собственным началом другого N -слова, то множество Посл(N) пусто.)

15.2.2. Указать (а) Посл(E) для примера 1 (см. "пункт 15.1." ); (б) Посл(E) и Посл(T) для примера 2 (см. "пункт 15.1." ); (в) Посл(\langle{слаг}\rangle) и Посл(\langle{множ}\rangle) для примера 3 (см. "пункт 15.1." );

Ответ. (а) Посл(E) = {[, (}. (б) Посл(E) = {[, (} ; Посл(T) пусто (никакое T -слово не является началом другого). (в) Посл(\langle{слаг}\rangle) = \{\hbox{\texttt{*}}\} ; Посл(\langle{множ}\rangle) пусто.

Кроме того, для каждого нетерминала N обозначим через Нач(N ) множество всех терминалов, являющихся первыми буквами непустых N -слов. Это обозначение - вместе с предыдущим - позволит дать достаточное условие корректности процедуры ReadK в описанной выше ситуации.

15.2.3. Доказать, что если Посл(L) не пересекается с Нач(M) и множество всех M -слов непусто, то ReadK корректна.

Решение. Рассмотрим два случая.

(1) Пусть после ReadL значение переменной b ложно. В этом случае ReadL читает со входа максимальное L -начало A, не являющееся L -словом. Оно является K -началом (здесь важно, что множество M -слов непусто.). Будет ли оно максимальным K -началом среди начал входа? Если нет, то A является началом слова BC, где B есть L -слово, C есть M -начало и BC - более длинное начало входа, чем A. Если B длиннее A, то A - не максимальное начало входа, являющееся L -началом, что противоречит корректности ReadL. Если B = A, то A было бы L -словом, а это не так. Значит, B короче A, C непусто и первый символ слова C следует в A за последним символом слова B, т.е. Посл(L) пересекается с Нач(M). Противоречие. Итак, A максимально. Из сказанного следует также, что A не является K -словом. Корректность процедуры ReadK в этом случае проверена.

(2) Пусть после ReadL значение переменной b истинно. Тогда прочитанное процедурой ReadK начало входа имеет вид AB, где A есть L -слово, а B есть M -начало. Тем самым AB есть K -начало. Проверим его максимальность. Пусть C есть большее K -начало. Тогда либо C есть L -начало (что невозможно, так как A было максимальным L -началом), либо C =
A'B', где A' - L -слово, B' - M -начало. Если A' короче A, то B' непусто и начинается с символа, принадлежащего и Нач(M), и Посл(L), что невозможно. Если A' длиннее A, то A - не максимальное L -начало.

Итак, A' = A. Но в этом случае B' есть продолжение B, что противоречит корректности ReadM. Итак, AB - максимальное K -начало. Остается проверить правильность выдаваемого процедурой ReadK значения переменной b. Если оно истинно, то это очевидно. Если оно ложно, то B не есть M -слово, и надо проверить, что AB - не K -слово. В самом деле, если бы выполнялось AB = A'B', где A' - L -слово, B' - M -слово, то A' не может быть длиннее A ( ReadL читает максимальное слово), A' не может быть равно A (тогда B' равно B и не является M -словом) и A' не может быть короче A (тогда первый символ B' принадлежит и Нач(M), и Посл(L) ). Задача решена.

Перейдем теперь к другому частному случаю. Пусть в КС-грамматике есть правила

\begin{align*}
 \hbox{\texttt{K}}&\to\hbox{\texttt{L}}\\
 \hbox{\texttt{K}}&\to\hbox{\texttt{M}}\\
 \hbox{\texttt{K}}&\to\hbox{\texttt{N}}
\end{align*}
и других правил с левой частью K нет.

15.2.4. Считая, что ReadL, ReadM и ReadN корректны (для L, M и N ) и что множества Нач(L), Нач(M) и Нач(N) не пересекаются, написать процедуру, корректную для K.

Решение. Схема процедуры такова:

procedure ReadK;
begin
| if (Next принадлежит Нач(L)) then begin
| | ReadL;
| end else if (Next принадлежит Нач(M)) then begin
| | ReadM;
| end else if (Next принадлежит Нач(N)) then begin
| | ReadN;
| end else begin
| | b := true или false  в зависимости от того,
| |      выводимо ли пустое слово из K или нет
| end;
end;

Докажем, что ReadK корректно реализует K. Если Next не принадлежит ни одному из множеств Нач(L), Нач(M), Нач(N),то пустое слово является наибольшим началом входа, являющимся K -началом. Если Next принадлежит одному (и, следовательно, только одному) из этих множеств, то максимальное начало входа, являющееся K -началом, непусто и читается соответствующей процедурой.

Татьяна Новикова
Татьяна Новикова
Россия, Пошатово
Artem Bardakov
Artem Bardakov
Россия