Болезни Военный билет Призыв

Построение дка по регулярному выражению. Преобразование регулярных выражений в недетерминированный конечный автомат. Дополнение регулярного языка

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

  • Команда grep операционной системы Unix или аналогичные команды для поиска цепочек, которые можно встретить в Web-броузерах или системах форматирования текста. В таких системах РВ используются для описания шаблонов, которые пользователь ищет в файле. Различные поисковые системы преобразуют РВ либо в детерминированный конечный автомат (ДКА), либо недетерминированный конечный автомат (НКА) и применяют этот автомат к файлу, в котором производится поиск.
  • Генераторы лексических анализаторов. Лексические анализаторы являются компонентом компилятора, они разбивают исходную программу на логические единицы (лексемы), которые могут состоять из одного или нескольких символов и имеют определенный смысл. Генератор лексических анализаторов получает формальные описания лексем, являющиеся по существу РВ, и создает ДКА, который распознает, какая из лексем появляется на его входе.
  • РВ в языках программирования.

В данной статье мы сначала ознакомимся с конечными автоматами и их видами (ДКА и НКА), и далее рассмотрим пример построения минимального ДКА по регулярному выражению.

Конечные автоматы

Конечный автомат (КА) - это преобразователь, который позволяет сопоставить входу соответствующий выход, причем выход этот может зависеть не только от текущего входа, но и от того, что происходило раньше, от предыстории работы конечного автомата. Даже поведение человека, а не только искусственных систем можно описать с помощью КА. Например, ваша реакция на то что ваш сосед слушает громко музыку по ночам, будет одной после первого такого случая и совершенно другой после нескольких таких случаев. Таких предысторий может быть бесконечное число, возникает вопрос: какой памятью должен обладать КА, чтобы вести себя по разному для каждой предыстроии? Понятно, что хранить бесконечное число предысторий невозможно. Поэтому автомат как бы разбивает все возможные предыстории на классы эквивалентности. Две истории являются эквивалентными, если они одинаково влияют на поведение автомата в дальнейшем. Класс эквивалентности к которому автомат отнес свою текущую предысторию, называют еще внутренним состоянием автомата.

Рассмотрим пример работы примитивного КА:

Данный КА состоит из:

  • ленты, представленной входной цепочкой.
  • считывающее устройство.
  • блок управления, который содержит список правил переходов.

Считывающее устройство может двигаться в одном направлении, как правило слева на право, тем самым считывая символы входной цепочки. За каждое такое движение оно может считать один символ. Далее, считанный символ передается блоку управлений. Блок управления изменяет состояние автомата на основе правил переходов. Если список правил переходов не содержит правила для считанного символа, то автомат «умирает».

Теперь рассмотрим, какими способами можно задать КА. Они могут задаваться в виде графов или в виде управляющих таблиц. В виде графа КА задается следующим способом:

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

В виде управляющей таблицы, так:

Пример КА в виде графа и в виде управляющей таблицы будет представлен ниже.

ДКА и НКА

Основное отличие ДКА и НКА состоит в том, что ДКА в процессе работы может находится только в одном состоянии, а НКА в нескольких состояниях одновременно. В качестве примера работы НКА можно привести идею американского физика Хью Эверетта от том, что любое событие разбивает мир на несколько миров, в каждом из которых это событие закончилось по-своему. Например, в одном мире Гитлер выиграл Вторую мировую войну, в другом – Ньютон вместо физики занялся бизнесом и открытие законов классической механики пришлось отложить лет на 50. Чтобы сделать какие-то выводы из работы автомата, следует изучить все «миры». После того как вся входная цепочка будет считана, мы полагаем, что НКА допускает данную цепочку, если он завершил работу в допускающем состоянии хотя бы в одном из множества «миров». Соответственно, автомат отвергает цепочку, если он завершил работу в недопускающем состоянии в каждом «мире». ДКА же принимает цепочку, это очевидно, если после считывания всей входной цепочки он оказывается в допускающем состоянии.

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

Построение минимального ДКА по регулярному выражению

Для начала приведем список операций РВ используемый в данной статье, в порядке их приоритетности:

  • итерация (замыкание Клини), с помощью символа "*"
  • конкатенация задается с помощью пробела или пустой строки (например: ab)
  • объединение, с помощью символа "|"

Рассмотрим пример, дано регулярное выражение:

Xy* (x | y*) | ab (x | y*) | (x | a*) (x | y*)

Нужно построить минимальный ДКА по регулярному выражению и продемонстрировать распознавание корректной и некорректной цепочки.

Для начала упростим данное РВ, используя правосторонний дистрибутивный закон конкатенации относительно объединения получаем следующее РВ:

(xy* | ab | (x | a*)) (x | y*)

Теперь строим автомат по данному РВ:

По правилу преобразования конкатенации (не будем приводить правила преобразования РВ в КА, так как они довольно очевидные), получаем следующий автомат:

По правилу преобразования объединения:

По правилу преобразования конкатенации:

И в конце применяем правило преобразования замыкания и получаем εНКА. Здесь нужно оговорится, что εНКА - это НКА, который содержит ε-переходы. В свою очередь ε-переход - это переход, при котором автомат не учитывает входную цепочку или другими словами переход по пустому символу.

Избавляемся от ε-переходов («звездочкой» обозначены конечные состояния):

В данном НКА состояния s3 и s5 эквивалентны, так как δ(s3, x) = δ(s5, x) = s1 и δ(s3, y) = δ(s5, y) = s5, s7. Переименовываем состояния s6 -> s5 и s7 -> s6:

Строим ДКА по НКА:

В данном ДКА состояния p1 и p5 эквивалентны, так как
δ(p1, x) = δ(p5, x) = p4 и δ(p1, y) = δ(p5, y) = p5. Переименовываем состояния p6 -> p5 и p7 -> p6:

Данный автомат является минимальным ДКА.

Пускай δ - функция переходов, то расширенную функцию переходов, построенную по δ, обозначим δ’, и ω - входная цепочка.

Допустим на вход подается цепочка ω = aaax, мы ожидаем, что автомат окажется в одном из допускающих состояний.

δ’(p0, ε) = p0
δ’(p0, a) = δ(δ’(p0, ε), a) = δ(p0, a) = p3
δ’(p0, aa) = δ(δ’(p0, a), a) = δ(p3, a) = p5
δ’(p0, aaa) = δ(δ’(p0, aa), a) = δ(p5, a) = p5
δ’(p0, aaax) = δ(δ’(p0, aaa), x) = δ(p5, x) = p4

P4 - допустимое конечное состояние, таким образом цепочка aaax является корректной для данного автомата.

Теперь допустим, что ω = xyyb:

δ’(p0, ε) = p0
δ’(p0, x) = δ(δ’(p0, ε), x) = δ(p0, x) = p1
δ’(p0, xy) = δ(δ’(p0, x), y) = δ(p1, y) = p1
δ’(p0, xyy) = δ(δ’(p0, xy), y) = δ(p1, y) = p1
δ’(p0, xyyb) = δ(δ’(p0, xyy), b) = δ(p1, b) = ∅

Здесь мы видим, что если подать на вход автомату символ b, когда он находится в состоянии p1, то данный автомат умрет, следовательно цепочка xyyb - некорректна.

P. S. В данной статье был рассмотрен алгоритм построение ДКА по РВ, но существуют более удобные алгоритмы, в частности для программирования, но это уже тема для другой статьи…

Описывать лексику языка удобнее в форме регулярных выражений, а распознавать язык с помощью КА. Поэтому важно уметь преобразовывать определения языка в форме регулярных выражений в определение в форме КА. Такое преобразование предложил Kennet Thompson.

Конечный автомат это пятерка (S, S, d, S 0 , F)

S - конечное множество состояний.

S - конечное множество допустимых входных сигналов.

d - функция переходов. Она отражает множество Sх(SÈ{e}) в множество состояний недетерминированного конечного автомата. Для детерминированного автомата функция переходов отражает множество SхS во множество состояний автомата. Другими словами, в зависимости от состояния и входного символа, d определяет новое состояние автомата.

S 0 - начальное состояние конечного автомата, S 0 Î S.

F - множество конечных состояний автомата, F Î S.

Работа конечного автомата представляет собой последовательность шагов. Шаг определяется состоянием автомата и входным символом. Сам шаг состоит в изменении состояния автомата и считывании следующего символа входной последовательности.

Существуют следующие правила преобразования регулярных выражений в конечный автомат.

1 Регулярное выражение “e” преобразуется в автомат из двух состояний и e -перехода между ними (рисунок 1).

Рисунок 1. – Автомат для e-перехода

2 Регулярное выражение из одного символа “а” преобразуется в конечный автомат из двух состояний и перехода между ними по входному сигналу а (рисунок 2).

Рисунок 2. – Автомат для перехода по символу а

3 Пусть есть регулярное выражение rs и уже построены конечные автоматы для выражения r и выражения s. Тогда два автомата соединяются последовательно. На рисунке 3 представлены исходные автоматы для языков r и s. На рисунке автомат для распознавания конкатенации этих языков.

Автомат для r Автомат для s

Рисунок 3. – Исходные автоматы


Рисунок 4. – Автомат для конкатенации языков

4 Пусть есть регулярное выражение r | s и уже построены конечные автоматы для выражения r и выражения s (рисунок 3). Тогда в результирующем автомате должна быть альтернатива выполнения одного из двух автоматов. То есть автомат для выражения r | s при автоматах для r и s с рисунка 3 имеет вид, представленный на рисунке 5.

Рисунок 5. – Автомат для объединения языков

5 Пусть есть регулярное выражение r* при построенном конечном автомате r. В этом случае вводятся два новых состояния для возможности обхода автомата выражения r, а также вводится e-переход между конечным и начальным состояниями для возможности многократного повторения автомата r. Если для регулярного выражения r построен автомат аналогичный рисунку 3, то регулярному выражению r* соответствует конечный автомат, представленный на рисунке 6.

Необходимо по недетерминированному конечному автомату M = (Q , T , D , q 0 , F ) построить детерминированный конечный автомат M = (Q" , T , D" , q" 0 , F" ). Начальным состоянием для строящегося автомата является ε-замыкание начального состояния автомата исходного. ε-замыкание - множество состояний, которые достижимы из данного путём переходов по ε. Далее, пока есть состояния, для которых не построены переходы (переходы делаются по символам, переходы по которым есть в исходном автомате), для каждого символа вычисляется ε-замыкание множества состояний, которые достижимы из рассматриваемого состояния путём перехода по рассматриваемому символу. Если состояние, которое соответствует найденному множеству, уже есть, то добавляется переход туда. Если нет, то добавляется новое полученное состояние.

Пример

Инициализация

Помечаются состояния, соответствующие ε-замыканию начального. Эти состояния будут соответствовать состоянию A будущего ДКА.


Первая итерация

Из ε-замыкания есть переходы в состояния НКА 3 и 10 (по a и c , соответственно). Для состояния 3 ε-замыканием является множество состояний {3, 4, 6}, для состояния 10 - {10}. Обозначим соответствующие данным множествам новые состояния ДКА как B и C .

Состояние ДКА Множество состояний НКА
a b c
A {1, 2, 9} B - C
B {3, 4, 6} - - -
C {10} - - -


Вторая итерация

Из множества состояний НКА {3, 4, 6}, соответствующего состоянию ДКА B есть два перехода - в состояние 5 (по b ) и 7 (по c ). Их ε-замыкания пересекаются, но сами множества различны, поэтому им ставятся в соответствие два новых состояния ДКА - D и E . Из состояний НКА, соответствующих состоянию ДКА C , никаких переходов нет.

Состояние ДКА Множество состояний НКА Символы, по которым осуществляется переход
a b c
A {1, 2, 9} B - C
B {3, 4, 6} - D E
C {10} - - -
D {2, 5, 8, 9} - - -
E {2, 7, 8, 9} - - -


Третья итерация

Из множеств состояний НКА, соответствующих состояниям ДКА D и E переходы делаются в множества состояний, соответствующие уже имеющимся состояниям (из множества {2, 5, 8, 9}, соответствующего состоянию D , по a переход в состояние 3, принадлежащее множеству {3, 4, 6}, соответствующему состоянию ДКА B , по c - переход в состояние 10, соответствующее состоянию C ; аналогично для множества, соответствующего состоянию ДКА E ). Процесс построения таблицы состояний и переходов ДКА завершён.

Состояние ДКА Множество состояний НКА Символы, по которым осуществляется переход
a b c
A {1, 2, 9} B - C
B {3, 4, 6} - D E
C {10} - - -
D {2, 5, 8, 9} B - C
E {2, 7, 8, 9} B - C


Результат:

Построение праволинейной грамматики по конечному автомату

Каждому состоянию ставим в соответствие нетерминал. Если есть переход из состояния X в состояние Y по а , добавляем правило X aY . Для конечных состояний добавляем правила X → ε. Для ε-переходов - X Y .

Пример 1 (детерминированный конечный автомат)

  • A → a B | c C
  • B → b D | c E
  • C → ε
  • D → a B | c C
  • E → a B | c C

Пример 2 (недетерминированный конечный автомат)

  • 1 → 2 | 9
  • 2 → a 3
  • 3 → 4 | 6
  • 4 → b 5
  • 5 → 8
  • 6 → c 7
  • 7 → 8
  • 8 → 2 | 9
  • 9 → c 10
  • 10 → ε

Построение ДКА по РВ

Пусть есть регулярное выражение r . По данному регулярному выражению необходимо построить детерминированный конечный автомат D такой, что L (D ) = L (r ).

Модификация регулярного выражения

Добавим к нему символ, означающий конец РВ - «#». В результате получим регулярное выражение (r )#.

Построение дерева

Представим регулярное выражение в виде дерева, листья которого - терминальные символы, а внутренние вершины - операции конкатенации «.», объединения «∪» и итерации «*». Каждому листу дерева (кроме ε-листьев) припишем уникальный номер и ссылаться на него будем, с одной стороны, как на позицию в дереве и, с другой стороны, как на позицию символа, соответствующего листу.

Вычисление функций nullable, firstpos, lastpos

Теперь, обходя дерево T снизу вверх слева-направо, вычислим три функции: nullable , firstpos , и lastpos . Функции nullable , firstpos и lastpos определены на узлах дерева. Значением всех функций, кроме nullable , является множество позиций. Функция firstpos (n ) для каждого узла n синтаксического дерева регулярного выражения дает множество позиций, которые соответствуют первым символам в подцепочках, генерируемых подвыражением с вершиной в n . Аналогично, lastpos (n ) дает множество позиций, которым соответствуют последние символы в подцепочках, генерируемых подвыражениями с вершиной n . Для узлов n , поддеревья которых (т. е. дерево, у которого узел n является корнем) могут породить пустое слово, определим nullable (n ) = true , а для остальных узлов false . Таблица для вычисления nullable , firstpos , lastpos :

узел n nullable (n ) firstpos (n ) lastpos (n )
ε true
i ≠ ε false {i } {i }
u ∪ v nullable (u ) or nullable (v ) firstpos (u ) ∪ firstpos (v ) lastpos (u ) ∪ lastpos (v )
u . v nullable (u ) and nullable (v ) if nullable (u ) then firstpos (u ) ∪ firstpos (v ) else firstpos (u ) if nullable (v ) then lastpos (u ) ∪ lastpos (v ) else lastpos (v )
v* true firstpos (v ) lastpos (v )

Построение followpos

Функция followpos вычисляется через nullable , firstpos и lastpos . Функция followpos определена на множестве позиций. Значением followpos является множество позиций. Если i - позиция, то followpos (i ) есть множество позиций j таких, что существует некоторая строка...cd ..., входящая в язык, описываемый РВ, такая, что i соответствует этому вхождению c , а j - вхождению d . Функция followpos может быть вычислена также за один обход дерева по следующим двум правилам

  1. Пусть n - внутренний узел с операцией «.» (конкатенация); a , b - его потомки. Тогда для каждой позиции i , входящей в lastpos (a followpos (i ) множество firstpos (b ).
  2. Пусть n - внутренний узел с операцией «*» (итерация), a - его потомок. Тогда для каждой позиции i , входящей в lastpos (a ), добавляем к множеству значений followpos (i ) множество firstpos (а ).

Пример

Вычислить значение функции followpos для регулярного выражения (a (b |c ))*c .

Позиция Значение followpos
1: (a (b |c ))*c {2, 3}
2: (a (b |c ))*c {1, 4}
3: (a (b |c ))*c {1, 4}
4: (a (b |c ))*c {5}

Построение ДКА

ДКА представляет собой множество состояний и множество переходов между ними. Состояние ДКА представляет собой множество позиций. Построение ДКА заключается в постепенном добавлении к нему необходимых состояний и построении переходов для них. Изначально имеется одно состояние, firstpos (root ) (root - корень дерева), у которого не построены переходы. Переход осуществляется по символам из регулярного выражения. Каждому символу соответствует множество позиций {p i }. Объединение всех followpos (x) есть состояние в которое необходимо перейти, где x - позиция, присутствующая как среди позиций состояния и так и среди позиций символа из РВ, по которому осуществляется переход. Если такого состояния нет, то его необходимо добавить. Процесс нужно повторять, пока не будут построены все переходы для всех состояний. Конечными объявляются все состояния, содержащие позицию добавленного в конец РВ символа #.

ДКА, полученный из РВ (a (b |c ))*c

Пример

Построить ДКА по регулярному выражению (a (b |c ))*c .

Состояние ДКА Символ
a {1} b {2} c {3, 4}
A {1, 4} B {2, 3} - C {5}
B {2, 3} - A {1, 4} A {1, 4}
C {5} - - -

Построение ДКА с минимальным количеством состояний

ДКА с минимальным числом состояний строится для всюду определённого ДКА, т.е. такого, что . Для любого ДКА существует эквивалентный ему всюду определённый ДКА.

Построение всюду определённого ДКА

Введём новое состояние и определим новое множество состояний .

Определим новую функцию перехода так:

Построение разбиения (формально)

Построим начальное разбиение П множества состояний на две группы: заключительные состояния F и остальные S\F , т. е. П = {F , Q\F }.

Применить к каждой группе G П следующую процедуру. Разбить G на подгруппы так, чтобы состояния s и t из G оказались в одной группе тогда и только тогда, когда для каждого входного символа a состояния s и t имеют переходы по a в состояния из одной и той же группы в П .

Полученные подгруппы добавляем в новое разбиение П new .

Принимаем П = П new и повторяем построение разбиения до стабилизации, т. е. пока разбиение не перестанет меняться.

Построение разбиения (алгоритм)

Для построения разбиения существует следующий алгоритм. Строим таблицу переходов для исходного автомата, строим начальное разбиение.

Каждой группе из разбиения назначаем идентификатор (для начального разбиения, например, 0 и 1).

Каждому состоянию (каждой строке таблицы) приписываем строку вида “a.bcd…xyz”, где - идентификатор группы, к которой принадледжит состояние из [первого столбца (откуда переходим), второго столбца (куда переходим по первому символу), …, последнего столбца (куда переходим по последнему символу)].

Строим новые группы по совпадению строк, т. е. так, чтобы в одну группу попали состояния с одинаковыми приписанными строками.

Затем, новая итерация. Назначаем получившимся группам новые идентификаторы, например {0, 1, …, n}. И повторяем построение разбиения до стабилизации.

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

Построение приведённого автомата

Каждая из получившихся групп становится состоянием нового ДКА. Если группа содержит начальное (заключительное) состояние исходного автомата, эта группа становится начальным (соответственно заключительным) состоянием нового ДКА. Переходы строятся очевидным образом: переход в состояние из группы считается переходом в группу.

Приводим автомат. Удаляем сначала непорождающие (бесплодные, «мёртвые»), затем недостижимые состояния (определения даны для символов, но очевидным образом переносятся на состояния автомата).

Вообще говоря, удаление мертвых состояний превращает ДКА в НКА, поскольку в ДКА все переходы должны быть определены. Однако в Dragon Book такое отклонение от формального определения считается всё же допустимым .

Пример

Для построим ДКА с минимальным числом состояния для ДКА следующего вида:

  • Начальное разбиение: {C} (конечное состояние ), {A, B, D, E} (все остальные состояния ).
  • {C} (без изменений ), {A, D, E}, {B}, (так как из A, D, E по a, c переходим в B и C соответственно ).
  • Больше никаких разбиений сделать не можем.

Пусть группе {C} соответствует состояние С, группе {A, D, E} - состояние A, а группе {B} - состояние B. Тогда получаем ДКА с минимальным числом состояний:

Пример (алгоритм построения разбиения)

Таблица переходов для всюду определённого (добавлено состояние Z) ДКА, соответствующего РВ (ab|ε)a*|abb|b*a. Из заданий экзамена 2012 года.

a b I 0 I 1
→A* B C 0.01 0.12
B* D E 0.00 1.01
C F C 1.01
D* D Z 0.01 0.04
E* D Z 0.00 1.03
F* Z Z 0.11
Z Z Z 1.11

Итерации:

  • I 0: ABCDEF(0), CZ(1).
  • I 1: AD(0), BE(1), C(2), F(3), Z(4).
  • I 2: A, B, C, D, E, F, Z.

Результат: в автомате и так было минимальное число состояний:-)

ДКА, допускающий дополнение языка

Алгоритм построения ДКА, принимающего дополнение языка L (L̅) состоит из двух шагов:

  • Построение полного ДКА
  • Построение из него искомого автомата

На самом деле, нет такого понятия, как полный ДКА. Под полным ДКА некоторые преподаватели понимают автомат, в таблице переходов которого нет пустых клеток. Однако в соответствии с определением ДКА - δ: Q × Σ → Q - пустых клеток быть не может в любом случае. Автомат "с пустыми клетками", впрочем, удовлетворяет определению НКА. В ходе решения нередко получается именно такой НКА, которому до ДКА не хватает лишь переходов.

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

Теперь для того, чтобы построить искомый автомат, требуется лишь поменять роли принимающих и непринимающих состояний. Иными словами, F" = Q \ F.

Построение LL(k) анализатора

Преобразование грамматики

Не всякая грамматика является LL(k)-анализируемой. Контекстно-свободная грамматика принадлежит классу LL(1), если в ней нет конфликтов типа FIRST-FIRST (левая рекурсия - частный случай такого конфликта) и FIRST-FOLLOW.

Иногда удаётся преобразовать не LL(1)-грамматики так, чтобы они стали LL(1). Некоторые (точнее, те, которые рассматривались в курсе) преобразования приведены ниже.

Удаление левой рекурсии

Пусть у нас имеется правило вида (здесь и далее в этом разделе, заглавные буквы - нетерминальные символы , строчные - цепочки любых символов ):

  • A → Aa | Ab | … | Ak | m | n | … | z

Оно не поддается однозначному анализу, поэтому его следует преобразовать.

Легко показать, что это правило эквивалентно следующей паре правил:

  • A → m B | n B | … | z B
  • B → a B | b B | … | k B | ε

Левая факторизация

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

Пример
  • A → a c | a df | a dg | b

Преобразуется в

  • A → a B | b
  • B → c | d f | d g

Что в свою очередь превратится в

  • A → a B | b
  • B → c | d С
  • С → f | g

Пример преобразования грамматики

G = {{S, A, B}, {a, b, c}, P, S}

  • S → SAbB | a
  • A → ab | aa | ε
  • B → c | ε

Удаление левой рекурсии для S :

  • S → aS 1
  • S 1 → AbBS 1 | ε

Левая факторизация для A :

  • A → aA 1 | ε
  • A 1 → b | a

Итоговая грамматика:

  • S → aS 1
  • S 1 → AbBS 1 | ε
  • A → aA 1 | ε
  • A 1 → b | a
  • B → c | ε

Построение FIRST и FOLLOW

FIRST(α), где α ∈ (N ∪ T)* - множество терминалов, с которых может начинаться α. Если α ⇒ ε, то ε ∈ FIRST(α). Соответственно, значение функции FOLLOW(A ) для нетерминала A - множество терминалов, которые могут появиться непосредственно после A в какой-либо сентенциальной форме. Если A может являться самым правым символом в некоторой сентенциальной форме, то заключительный маркер $ также принадлежит FOLLOW(A )

Вычисление FIRST

Для терминалов
  • Для любого терминала x , x T , FIRST(x ) = {x }
Для нетерминалов
  • Если X - нетерминал, то положим FIRST(X ) = {∅}
  • Если в грамматике есть правило X → ε, то добавим ε к FIRST(X )
  • Для каждого нетерминала X и для каждого правила вывода X Y 1 …Y k добавим в FIRST(X ) множества FIRST всех символов в правой части правила до первого, из которого не выводится ε, включая его
Для цепочек
  • Для цепочки символов X 1 …X k FIRST есть объединение FIRST входящих в цепочку символов до первого, у которого ε ∉ FIRST, включая его.
Пример
  • S → aS 1
  • S 1 → AbBS 1 | ε
  • A → aA 1 | ε
  • A 1 → b | a
  • B → c | ε

FIRST нетерминалов в порядке разрешения зависимостей:

  • FIRST(S) = {a}
  • FIRST(A) = {a, ε}
  • FIRST(A 1) = {b, a}
  • FIRST(B) = {c, ε}
  • FIRST(S 1) = {a, b, ε}

FIRST для правил вывода:

  • FIRST(aS 1) = {a}
  • FIRST(AbBS 1) = {a, b}
  • FIRST(ε) = {ε}
  • FIRST(aA 1) = {a}
  • FIRST(a) = {a}
  • FIRST(b) = {b}
  • FIRST(c) = {c}

Вычисление FOLLOW

Вычисление функции FOLLOW для символа X:

  • Пусть FOLLOW(X) = {∅}
  • Если X - аксиома грамматики, то добавить в FOLLOW маркер $
  • Для всех правил вида A → αXβ добавить FIRST(β)\{ε} к FOLLOW(X) (за X могут следовать те символы, с которых начинается β)
  • Для всех правил вида A → αX и A → αXβ, ε ∈ FIRST(β) добавить FOLLOW(A) к FOLLOW(X) (то есть, за X могут следовать все символы, которые могут следовать за A, в случае, если в правиле вывода символ X может оказаться крайним правым)
  • Повторять предыдущие два пункта, пока возможно добавление символов в множество
Пример
  • S → aS 1
  • S 1 → AbBS 1 | ε
  • A → aA 1 | ε
  • A 1 → b | a
  • B → c | ε

Результат:

  • FOLLOW(S) = {$}
  • FOLLOW(S 1) = {$} (S 1 - крайний правый символ в правиле S → aS 1)
  • FOLLOW(A) = {b} (после A в правиле S 1 → AbBS 1 следует b)
  • FOLLOW(A 1) = {b} (A 1 - крайний правый символ в правиле A → aA 1 , следовательно, добавляем FOLLOW(A) к FOLLOW(A 1))
  • FOLLOW(B) = {a, b, $} (добавляем FIRST(S 1)\{ε} (следует из правила S 1 → AbBS 1), FOLLOW(S 1) (так как есть S 1 → ε))

Составление таблицы

В таблице M для пары нетерминал-терминал (в ячейке M [A , a ]) указывается правило, по которому необходимо выполнять свёртку входного слова. Заполняется таблица следующим образом: для каждого правила вывода заданной грамматики A → α (где под α понимается цепочка в правой части правила) выполняются следующие действия:

  1. Для каждого терминала a ∈ FIRST(α) добавить правило A α к M [A , a ]
  2. Если ε ∈ FIRST(α), то для каждого b ∈ FOLLOW(A ) добавить A α к M [A , b ]
  3. ε ∈ FIRST(α) и $ ∈ FOLLOW(A ), добавить A α к M [A , $]
  4. Все пустые ячейки - ошибка во входном слове

Пример

Построить таблицу для грамматики

  • S → aS 1
  • S 1 → AbBS 1 | ε
  • A → aA 1 | ε
  • A 1 → b | a
  • B → c | ε

Результат:

a b c $
S S → aS 1 (Первое правило, вывод S → aS 1 , a ∈ FIRST(aS 1)) Error (Четвёртое правило) Error (Четвёртое правило) Error (Четвёртое правило)
S 1 S 1 → AbBS 1 (Первое правило, вывод S 1 → AbBS 1 , a ∈ FIRST(AbBS 1)) S 1 → AbBS 1 (Первое правило, вывод S 1 → AbBS 1 , b ∈ FIRST(AbBS 1)) Error (Четвёртое правило) S 1 → ε (Третье правило, вывод S 1 → ε, ε ∈ FIRST(ε), $ ∈ FOLLOW(S 1))
A A → aA 1 (Первое правило, вывод A → aA 1 , a ∈ FIRST(aA 1)) A → ε (Второе правило, вывод A 1 → ε, b ∈ FOLLOW(A 1)) Error (Четвёртое правило) Error (Четвёртое правило)
A 1 A 1 → a (Первое правило, вывод A 1 → a, a ∈ FIRST(a)) A 1 → b (Первое правило, вывод A 1 → b, b ∈ FIRST(b)) Error (Четвёртое правило) Error (Четвёртое правило)
B B → ε B → ε (Второе правило, вывод B → ε, a ∈ FOLLOW(B)) B → c (Первое правило, вывод B → c, c ∈ FIRST(c)) B → ε (Третье правило, вывод B → ε, $ ∈ FOLLOW(B))

Разбор строки

Процесс разбора строки довольно прост. Его суть в следующем: на каждом шаге считывается верхний символ v c входной цепочки.

  • Если v - терминальный символ
    • Если v совпадает с с , то они оба уничтожаются, происходит сдвиг
    • Если v не совпадает с с , то сигнализируется ошибка разбора
  • Если v - нетерминальный символ, c возвращается в начало строки, вместо v в стек возвращается правая часть правила, которое берется из ячейки таблицы M [v , c ]

Процесс заканчивается, когда и строка и стек дошли до концевого маркера (#).

Пример

разберем строку «aabbaabcb»:

стек строка действие
S # a abbaabcb$ S → aS 1
a S 1 # a abbaabcb$ сдвиг
S 1 # a bbaabcb$ S 1 → AbBS 1
A bBS 1 # a bbaabcb$ A → aA 1
a A 1 bBS 1 # a bbaabcb$ сдвиг
A 1 bBS 1 # b baabcb$ A 1 → b
b bBS 1 # b baabcb$ сдвиг
b BS 1 # b aabcb$ сдвиг
B S 1 # a abcb$ B → ε
S 1 # a abcb$ S 1 → AbBS 1
A bBS 1 # a abcb$ A → aA 1
A bBS 1 # a abcb$ A → aA 1
a A 1 bBS 1 # a abcb$ сдвиг
A 1 bBS 1 # a bcb$ A 1 → a
a bBS 1 # a bcb$ сдвиг
b BS 1 # b cb$ сдвиг
B S 1 # c b$ B → c
c S 1 # c b$ сдвиг
S 1 # b $ S 1 → AbBS 1
A bBS 1 # b $ A → ε
b BS 1 # b $ сдвиг
B S 1 # $ B → ε
S 1 # $ S 1 → ε
# $ готово

Построение LR(k) анализатора

Вычисление k в LR(k)

Не существует алгоритма, который позволял бы в общем случае для произвольной грамматики вычислить k. Обычно, стоит попробовать построить LR(1)-анализатор. Если у него на каждое множество приходится не более одной операции (Shift, Reduce или Accept), то грамматика LR(0). Если же при построении LR(1)-анализатора возникает конфликт и коллизия, то данная грамматика не является LR(1) и стоит попробовать построить LR(2). Если не удаётся построить и её, то LR(3) и так далее.

Пополнение грамматики

Добавим новое правило S" → S, и сделаем S" аксиомой грамматики. Это дополнительное правило требуется для определения момента завершения работы анализатора и допуска входной цепочки. Допуск имеет место тогда и только тогда, когда возможно осуществить свёртку по правилу S → S".

Построение канонической системы множеств допустимых LR(1)-ситуаций

В начале имеется множество I 0 с конфигурацией анализатора S" → .S, $. Далее к этой конфигурации применяется операция закрытия до тех пор, пока в результате её применения не перестанут добавляться новые конфигурации. Далее, строятся переходы в новые множества путём сдвига точки на один символ вправо (переходы делаются по тому символу, который стоял после точки до перехода и перед ней после перехода), и в эти множества добавляются те конфигурации, которые были получены из имеющихся данным образом. Для них также применяется операция закрытия, и весь процесс повторяется, пока не перестанут появляться новые множества.

Пример

Построить каноническую систему множеств допустимых LR(1)-ситуаций для указанной грамматики:

  • S" → S
  • S → ABA
  • A → Aa | ε
  • B → cBc | d

Решение:

  • Строим замыкание для конфигурации S" → .S, $:
    • S → .ABA, $
  • Для полученных конфигураций (S → .ABA, $) также строим замыкание:
    • A → .Aa, c
    • A → .Aa, d
    • A → ., c
    • A → ., d
  • Для полученных конфигураций (A → .Aa, c; A → .Aa, d) также строим замыкание:
    • A → .Aa, a
    • A → ., a
  • Больше конфигураций в состоянии I 0 построить нельзя - замыкание построено
  • Из I 0 можно сделать переходы по S и A и получить множество конфигураций I 1 и I 2 , состоящее из следующих элементов:
    • I 1 = {S" → S., $}
    • I 2 = {S → A.BA, $; A → A.a, c; A → A.a, d; A → A.a, a}
  • I 1 не требует замыкания
  • Построим замыкание I 2:
    • B → .cBc, a
    • B → .cBc, $
    • B → .d, a
    • B → .d, $
  • Аналогично строятся все остальные множества.

Построение таблицы анализатора

Заключительным этапом построения LR(1)-анализатора является построение таблиц Action и Goto . Таблица Action строится для символов входной строки, то есть для терминалов и маркера конца строки $, таблица Goto строится для символов грамматики, то есть для терминалов и нетерминалов.

Построение таблицы Goto

Таблица Goto показывает, в какое состояние надо перейти при встрече очередного символа грамматики. Поэтому, если в канонической системе множеств есть переход из I i в I j по символу A, то в Goto(I i , A) мы ставим состояние I j . После заполнения таблицы полагаем, что во всех пустых ячейках Goto(I i , A) = Error

Построение таблицы Actions

  • Если есть переход по терминалу a из состояния I i в состояние I j , то Action(I i , a) = Shift(I j)
  • Если A ≠ S" и есть конфигруация A → α., a, то Action(I i , a) = Reduce(A → α)
  • Для состояния I i , в котором есть конфигурация S" → S., $, Action(I i , $) = Accept
  • Для всех пустых ячеек Action(I i , a) = Error

Пример

Построить таблицы Action и Goto для грамматики

  • S" → S
  • S → ABA
  • A → Aa | ε
  • B → cBc | d

Решение:

Action Goto
a c d $ S S" A B a c d
I 0 Reduce(A → ε) Reduce(A → ε) Reduce(A → ε) I 1 I 2
I 1 Accept
I 2 Shift(I 6) Shift(I 4) Shift(I 5) I 3 I 6 I 4 I 5
I 3 Reduce(A → ε) Reduce(A → ε) I 13
I 4 Shift(I 8) Shift(I 9) I 7 I 8 I 9
I 5 Reduce(B → d) Reduce(B → d)
I 6 Reduce(A → Aa) Reduce(A → Aa) Reduce(A → Aa)
I 7 Shift(I 10) I 10
I 8 Shift(I 8) Shift(I 9) I 11 I 8 I 9
I 9 Reduce(B → d)
I 10 Reduce(B → cBc) Reduce(B → cBc)
I 11 Shift(I 12) I 12
I 12 Reduce(B → cBc)
I 13 Shift(I 14) Reduce(S → ABA) I 14
I 14 Reduce(A → Aa) Reduce(A → Aa)

Разбор цепочки

На каждом шаге считывается верхний символ v из стека анализатора и берется крайний символ c входной цепочки.

Если в таблице дейстивий на пересечении v и c находится:

  • Shift(I k ), то в стек кладется с и затем I k . При этом c удаляется из строки.
  • Reduce(A u ), то из верхушки стека вычищаются все терминальные и нетерминальные символы, составляющие цепочку u , после чего смотрится состояние I m , оставшееся на верхушке. По таблице переходов на пересечении I m и A находится следующее состояние I s . После чего в стек кладется A, а затем I s . Строка остается без измененеий.
  • Accept, то разбор закончен
  • пустота - ошибка

Пример

Построим разбор строки aaaccdcc:

Стек Строка Действие
I 0 a aaccdcc$ Reduce(A → ε), goto I 2
I 0 A I 2 a aaccdcc$ Shift(I 6)
I 0 A I 2 a I 6 a accdcc$ Reduce(A → Aa), goto I 2
I 0 A I 2 a accdcc$ Shift(I 6)
I 0 A I 2 a I 6 a ccdcc$ Reduce(A → Aa), goto I 2
I 0 A I 2 a ccdcc$ Shift(I 6)
I 0 A I 2 a I 6 c cdcc$ Reduce(A → Aa), goto I 2
I 0 A I 2 c cdcc$ Shift(I 4)
I 0 A I 2 c I 4 c dcc$ Shift(I 8)
I 0 A I 2 c I 4 c I 8 d cc$ Shift(I 9)
I 0 A I 2 c I 4 c I 8 d I 9 c c$ Reduce(B → d), goto I 11
I 0 A I 2 c I 4 c I 8 B I 11 c c$ Shift(I 12)
I 0 A I 2 c I 4 c I 8 B I 11 c I 12 c $ Reduce(B → cBc), goto I 7
I 0 A I 2 c I 4 B I 7 c $ Shift(I 10)
I 0 A I 2 c I 4 B I 7 c I 10 $ Reduce(B → cBc), goto I 3
I 0 A I 2 B I 3 $ Reduce(A → ε), goto I 13
I 0 A I 2 B I 3 A I 13 $ Reduce(S → ABA), goto I 1
I 0 S I 1 $ Accept

Трансляция арифметических выражений (алгоритм Сети-Ульмана)

Примечание. Код генерируется doggy style Motorola-like, то есть

Op Arg1, Arg2

обозначает

Arg2 = Arg1 Op Arg2

Построение дерева

Дерево строится как обычно для арифметического выражения: В корне операция с наименьшим приоритетом, далее следуют операции с приоритетом чуть выше и так далее. Скобки имеют наивысший приоритет. Если имеется несколько операций с одинаковым приоритетом - a op b op c, то дерево строится как для выражения (a op b) op с.

Пример

Построить дерево для выражения a + b / (d + a − b × c / d − e) + c × d

Решение: Запишем выражение в виде

((a) + ((b) / ((((d) + (a)) − ((b) × (c)) / (d)) − (e)))) + ((c) × (d))

Тогда в корне каждого поддерева будет операция, а выражения в скобках слева и справа от неё - её поддеревьями. Например, для подвыражения ((b) × (c)) / (d) в корне соответствующего поддерева будет операция «/», а её поддеревьями будут являться подвыражения ((b) × (c)) и (d).

Разметка дерева (вычисление количества регистров)

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

    1. Для вершины с меткой 0 код не генерируется

    2. Если вершина - лист X с меткой 1 и регистром Ri , то ему сопоставляется код

    MOVE X, Ri

    3. Если вершина внутренняя c регистром Ri и её левый потомок - лист X с меткой 0, то ей соответствует код

    <Код правого поддерева> Op X, Ri

    4. Если поддеревья вершины с регистром Ri - не листья и метка правой вершины больше или равна метке левой (у которой регистр Rj, j = i + 1), то вершине соответствует код

    <Код правого поддерева> <Код левого поддерева> Op Rj, Ri

    5. Если поддеревья вершины с регистром Ri - не листья и метка правой вершины (у которой регистр Rj, j = i + 1) меньше метки левой, то вершине соответствует код

    Распределение регистров показано на графике справа. Сгенерированный код:

    MOVE d, R0 ;R0 = d MOVE c, R1 ;R1 = c MUL b, R1 ;R1 = (b × c) DIV R1, R0 ;R0 = (b × c) / d MOVE a, R1 ;R1 = a ADD d, R1 ;R1 = a + d SUB R1, R0 ;R0 = (a + d) − ((b × c) / d) MOVE e, R1 ;R1 = e SUB R0, R1 ;R1 = ((a + d) − ((b × c) / d)) − e MOVE R1, R0;R0 = ((a + d) − ((b × c) / d)) − e DIV b, R0 ;R0 = b / (((a + d) − ((b × c) / d)) − e) ADD a, R0 ;R0 = a + (b / (((a + d) − ((b × c) / d)) − e)) MOVE d, R1 ;R1 = d MUL c, R1 ;R1 = c × d ADD R0, R1 ;R1 = (a + (b / (((a + d) − ((b × c) / d)) − e))) + (c × d) MOVE R1, R0;R0 = (a + (b / (((a + d) − ((b × c) / d)) − e))) + (c × d)

    Трансляция логических выражений

    В данном разделе показан способ генерации кода для ленивого вычисления логических выражений. В результате работы алгоритма получается кусок кода, который с помощью операций TST, BNE, BEQ вычисляет логическое выражение путём перехода на одну из меток: TRUELAB или FALSELAB.

    Построение дерева

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

    Приоритет операций: наивысший приоритет у операции NOT, далее идёт AND, а затем OR. Если в выражении используются другие логические операции то они должны быть выражены через эти три определённым образом (обычно, других операций нет и преобразования выражения не требуется). Ассоциативность у операций одного приоритета - слева направо, то есть A and B and C рассматривается как (A and B) and C

    Пример

    Построить дерево для логического выражения not A or B and C and (B or not C).

    Решение: см. схему справа.

    Для каждой вершины дерева вычисляются 4 атрибута:

    • Номер узла
    • Метка, куда необходимо перейти, если выражение в узле - ложь (false label, fl)
    • Метка, куда необходимо перейти, если выражение в узле - истина (true label, tl)
    • Метка-знак (sign) (подробнее см. далее)

    Нумерация вершин выполняется в произвольном порядке, единственным условием является уникальность номеров узлов.

    Разметка дерева производится следующим образом:

    • В fl указывается номер вершины, в который делается переход или falselab, если данная вершина false
    • В tl указывается номер вершины, в который делается переход или truelab, если данная вершина true

    Sign указывает то, в каком случае можно прекратить вычисления текущего поддерева.

    Для корня дерева fl=falselab, tl=truelab, sign=false.

    Таким образом:

    Пример

    Разметить дерево, построенное для логического выражения not A or B and C and (B or not C).

    Генерация кода

    Машинные команды, используемы в сгенерированном коде:

    • TST - проверка аргумента на истинность и установка флага, если аргумент ложен
    • BNE - переход по метке в случае, если флаг не установлен, то есть проверяенное при помощи TST условие истинно
    • BEQ - переход по метке в случае, если флаг установлен, то есть проверяенное при помощи TST условие ложно

    Построение кода производится следующим образом:

    • дерево обходится от корня, для AND и OR сначала обходится левое поддерево затем правое
    • для каждой пройденной вершины печатается ее номер (метка)
    • для листа A(номер, tl, fl, sign) печатается TST A
      • если sign == true, печатается BNE tl
      • если sign == false, печатается BEQ fl

    Пример

    Для рассмотренного выше выражения сгенерируется следующий код:

    1:2:4: TST A BEQ TRUELAB 3:5:7: TST B BEQ FALSELAB 8: TST C BEQ FALSELAB 6:9: TST B BNE TRUELAB 10:11: TST C BNE FALSELAB TRUELAB: FALSELAB:

    Метод сопоставления образцов

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

    Постановка задачи

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

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

    Примеры образцов

    Построение промежуточного представления

    Сначала строим дерево разбора для всего выражения.

    Построение покрытия

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

    Генерация кода

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

    Регистров у нас в этих задачах бесконечное количестов, так что каждый раз можно использовать новый.

    Построение РВ по ДКА

    Построение НКА по праволинейной грамматике

    Приведение грамматики

    Для того чтобы преобразовать произвольную КС-грамматику к приведенному виду, необходимо выполнить следующие действия:

    • удалить все бесплодные символы;
    • удалить все недостижимые символы;

    Удаление бесполезных символов

    Вход: КС-грамматика G = (T, N, P, S).

    Выход: КС-грамматика G’ = (T, N’, P’, S), не содержащая бесплодных символов, для которой L(G) = L(G’).

    Метод:

    Рекурсивно строим множества N 0 , N 1 , ...

    1. N 0 = ∅, i = 1.
    2. N i = {A | (A → α) ∈ P и α ∈ (N i - 1 ∪ T)*} ∪ N i-1 .
    3. Если N i ≠ N i - 1 , то i = i + 1 и переходим к шагу 2, иначе N’ = N i ; P’ состоит из правил множества P, содержащих только символы из N’ ∪ T; G’ = (T, N’, P’, S).

    Определение: символ x ∈ (T ∪ N) называется недостижимым в грамматике G = (T, N, P, S), если он не появляется ни в одной сентенциальной форме этой грамматики.

    Пример

    Удалить бесполезные символы у грамматики G({A, B, C, D, E, F, S}, {a, b, c, d, e, f, g}, P, S)

    • S → AcDe | CaDbCe | SaCa | aCb | dFg
    • A → SeAd | cSA
    • B → CaBd | aDBc | BSCf | bfg
    • C → Ebd | Seb | aAc | cfF
    • D → fCE | ac | dEdAS | ε
    • E → ESacD | aec | eFf

    Решение

    • N 0 = ∅
    • N 1 = {B (B → bfg) , D (D → ac) , E (E → aec) }
    • N 2 = {B, D, E, C (C → Ebd) }
    • N 3 = {B, D, E, C, S (S → aCb) }
    • N 4 = {B, D, E, C, S} = N 3

    G"({B, C, D, E, S}, {a, b, c, d, e, f, g}, P", S)

    • S → CaDbCe | SaCa | aCb
    • B → CaBd | aDBc | BSCf | bfg
    • C → Ebd | Seb
    • D → fCE | ac | ε
    • E → ESacD | aec

    Удаление недостижимых символов

    Вход: КС-грамматика G = (T, N, P, S)

    Выход: КС-грамматика G’ = (T’, N’, P’, S), не содержащая недостижимых символов, для которой L(G) = L(G’).

    Метод:

    1. V 0 = {S}; i = 1.
    2. V i = {x | x ∈ (T ∪ N), (A → αxβ) ∈ P и A ∈ V i - 1 } ∪ V i-1 .
    3. Если V i ≠ V i - 1 , то i = i + 1 и переходим к шагу 2, иначе N’ = V i ∩ N; T’ = V i ∩ T; P’ состоит из правил множества P, содержащих только символы из V i ; G’ = (T’, N’, P’, S).

    Определение: КС-грамматика G называется приведенной, если в ней нет недостижимых и бесплодных символов.

    Пример

    Удалить недостижимые символы у грамматики G"({B, C, D, E, S}, {a, b, c, d, e, f, g}, P", S)

    • S → CaDbCe | SaCa | aCb
    • B → CaBd | aDBc | BSCf | bfg
    • C → Ebd | Seb
    • D → fCE | ac | ε
    • E → ESacD | aec

    Решение

    • V 0 = {S}
    • V 1 = {S, C (S → CaDbCe) , D (S → CaDbCe) , a (S → CaDbCe) , b (S → CaDbCe) , e (S → CaDbCe) }
    • V 2 = {S, C, D, a, b, e, E (C → Ebd) , d (C → Ebd) , f (D → fCE) }
    • V 3 = {S, C, D, E, a, b, d, e, f, c (E → ESacD) }
    • V 4 = {S, C, D, E, a, b, d, e, f, c} = V 3

    G""({C, D, E, S}, {a, b, c, d, e, f}, P"", S)

    • S → CaDbCe | SaCa | aCb
    • C → Ebd | Seb
    • D → fCE | ac | ε
    • E → ESacD | aec

    Основные определения Регулярные выражения в алфавите Σ и регулярные множества, которые они обозначают, определяются рекурсивно следующим образом: 1) – регулярное выражение, обозначающее регулярное множество; 2) e – регулярное выражение, обозначающее регулярное множество {e}; 3) если a Σ, то a – регулярное выражение, обозначающее регулярное множество {a}; 4) если p и q – регулярные выражения, обозначающие регулярные множества P и Q, то а) (p+q) – регулярное выражение, обозначающее P Q; б) pq – регулярное выражение, обозначающее PQ; в) p* – регулярное выражение, обозначающее P*; 5) ничто другое не является регулярным выражением.

    Основные определения Расстановка приоритетов: * (итерация) – наивысший приоритет; конкатенация; + (объединение). Таким образом, 0 + 10* = (0 + (1 (0*))). Примеры: 1. 01 означает {01}; 2. 0* – {0*}; 3. (0+1)* – {0, 1}*; 4. (0+1)* 011 – означает множество всех цепочек, составленных из 0 и 1 и оканчивающихся цепочкой 011; 5. (a+b) (a+b+0+1)* означает множество всех цепочек {0, 1, a, b}*, начинающихся с a или b.

    Основные определения Леммы: 1) α + β = β + α 2) * = e 3) α + (β + γ) = (α + β) + γ 4) α(βγ) = (αβ)γ 5) α(β + γ) = αβ + αγ 6) (α + β)γ = αγ + βγ 7) αe = eα = α 8) α = 9) α+α* = α* 10) (α*)* = α* 11) α+α = α 12) α+ = α

    Связь РВ и РМ РМ – языки, порождаемые РВ. Например: x = a+b, y = c+d, x X = {a, b}, y Y = {c, d}, x + y X Y = {a, b, c, d}. Конкатенация: xy XY = {ac, ad, bc, bd}. к(и+о)т {к}{и, о}{т} = {кит, кот} или по леммам № 5 и № 6 к(и+о)т = кит + кот {кит, кот}. Итерация: x = a, x* X* = {e, a, aaa, …}, т. е. x* = e + xxx + …

    Связь РВ и РМ Итерация конкатенации и объединения: (xy)* = e + xyxyxy + … (x + y)* = e + (x + y)(x + y) + … = = e + xx + xy + yx + yy + xxx + … Пример: 0 + 1(0+1)* {0} ({1} {0, 1}*) = {0, 1, 10, 11, 100, 101, 110, 111…}. Объединение коммутативно: x + y = y + x Конкатенация – нет: xy ≠ yx

    Связь РВ и РМ Примеры на приоритет: x + yz {x, yz}, (x + y)z {xz, yz}, x + y* {e, x, y, yyy, yyyy, …}, (x + y)* {e, x, y, xx, xy, yx, yy, xxx, …}, (xy)* {e, xyxy, …}, xy* {x, xyy, xyyy, …}. Новые леммы: a* + e = a*; (a + e)* = a*; a*a* = a*; e* = e; и т. д.

    Регулярные системы уравнений Уравнение с регулярными коэффициентами X = a. X + b имеет решение (наименьшую неподвижную точку) a*b: aa*b + b = (aa* + e)b = a*b Система уравнений с регулярными коэффициентами: X 1 = α 10 + α 11 X 1 + α 12 X 2 + … + α 1 n. Xn X 2 = α 20 + α 21 X 1 + α 22 X 2 + … + α 2 n. Xn …………………………. . Xn = αn 0 + αn 1 X 1 + αn 2 X 2 + … + αnn. Xn Неизвестные – Δ = {X 1, X 2, …, Xn}.

    Регулярные системы уравнений Алгоритм решения (метод Гаусса): Шаг 1. Положить i = 1. Шаг 2. Если i = n, перейти к шагу 4. Иначе записать уравнения для Xi в виде Xi = αXi + β (β = β 0 + βi+1 Xi+1 + … + βn. Xn). Затем в правых частях для уравнений Xi+1, …, Xn заменим Xi регулярным выражением α*β. Шаг 3. Увеличить i на 1 и вернуться к шагу 2. Шаг 4. Записать уравнение для Xn в виде Xn = αXn + β. Перейти к шагу 5 (при этом i = n). Шаг 5. Уравнение для Xi имеет вид Xi = αXi + β. Записать на выходе Xi = α*β, в уравнениях для Xi– 1, …, X 1 подставляя α*β вместо Xi. Шаг 6. Если i = 1, остановиться, в противном случае уменьшить i на 1 и вернуться к шагу 5.

    Преобразование ДКА в РВ Для ДКА M = (Q, Σ, δ, q 0, F) составим систему с регулярными коэффициентами где Δ = Q: 1. полагаем αij: = ; 2. если δ(Xi, a) = Xj, a Σ, то αij: = αij + a; 3. если Xi F или δ(Xi,) = HALT, то αi 0: = e. После решения искомое РВ будет X 1 = q 0.

    Преобразование ДКА в РВ Пример: для числа с фиксированной точкой получим систему q 0 = + q 0 + sq 1 + pq 2 + dq 3 + q 4 q 1 = + q 0 + q 1 + pq 2 + dq 3 + q 4 q 2 = + q 0 + q 1 + q 2 + q 3 + dq 4 q 3 = e + q 0 + q 1 + q 2 + dq 3 + pq 4 = e + q 0 + q 1 + q 2 + q 3 + dq 4 Здесь: s – знак числа, s = "+" + "–"; p – десятичная точка, p = ". "; d – цифры, d = "0" + "1" + … + "9".

    Преобразование ДКА в РВ Решение: q 0 = *(sq 1 + pq 2 + dq 3 + q 4 +) = sq 1 + pq 2 + dq 3 q 1 = + q 0 + q 1 + pq 2 + dq 3 + q 4 = pq 2 + dq 3, q 2 = + q 0 + q 1 + q 2 + q 3 + dq 4 = dq 4, q 3 = e + q 0 + q 1 + q 2 + dq 3 + pq 4 = dq 3 + pq 4 + e, q 4 = e + q 0 + q 1 + q 2 + q 3 + dq 4 = dq 4 + e. Из третьего уравнения: q 3 = dq 3 + pq 4 + e = d*(pq 4 + e). Из четвертого уравнения: q 4 = dq 4 + e = d*.

    Преобразование ДКА в РВ Обратный ход: q 3 = d*(pq 4 + e) = d*(pd* + e), q 2 = dq 4 = dd*, q 1 = pq 2 + dq 3 = pdd* + dd*(pd* + e), q 0 = sq 1 + pq 2 + dq 3 = s(pdd* + dd*(pd* + e)) + pdd* + dd*(pd* + e). Таким образом, данному ДКА соответствует РВ s(pdd* + dd*(pd* + e)) + pdd* + dd*(pd* + e). Упростим: s(pdd* + dd*(pd* + e)) + pdd* + dd*(pd* + e) = = spdd* + sdd*(pd* + e) + pdd* + dd*(pd* + e) = (s + e)(pdd* + dd*(pd* + e)) Для более короткой записи можно использовать положительную итерацию aa* = a*a = a+: (s + e)(pdd* + dd*(pd* + e)) = (s + e)(pd+ + d+pd* + d+)

    Преобразование ДКА в РВ Сопоставление графа функции переходов ДКА основным операциям с регулярными выражениями: q 0 a b a q 1 q 2 q 1 q 0 a+b a b ab q 2 a*

    Преобразование ДКА в РВ Более сложные комбинации операций: q 0 a q 1 b b b q 0 a q 2 q 1 (a + e)b c b q 0 q 2 ab(cab)* q 0 (a + b)* q 0 a q 1 aa* = a+ q 0 a q 1 a b a a a (ab)+ q 2 b q 1 c e + (a + b)c*

    Преобразование ДКА в РВ Для РВ (s + e)(pd+ + d+(pd* + e)): q 0 p q 2 d s p q 1 d d q 3 d p q 4 d q 5 d

    Программирование РВ Регулярные выражения: Встроены во многие языки программирования (PHP, Java. Script, …); Реализованы в виде подключаемых компонентов (например, класс Regex для платформы. NET). Отличия в формах записи: x? = x + e x{1, 3} = x + xxx и т. д.

    Программирование РВ Конструкции класса Regex (System. Text. Regular. Expressions): Символ Интерпретация Escape-последовательности b При использовании его в квадратных скобках соответствует символу «←» (u 0008) t, r, n, a, f, v Табуляция (u 0009), возврат каретки (u 000 D), новая строка (u 000 A) и т. д. c. X Управляющий символ (например, c. C – это Ctrl+C, u 0003) e Escape (u 001 B) ooo Символ ASCII в восьмеричной системе xhh Символ ASCII в шестнадцатеричной системе uhhhh Символ Unicode Следующий символ не является специальным символом РВ. Этим символом нужно экранировать все специальные символы Пример (в примере приведен шаблон и строка поиска, в строке найденные совпадения подчеркнуты): @"rnw+" – "rn. Здесь имеютсяnдве строки".

    Программирование РВ Подмножества символов. Любой символ, кроме конца строки (n) Любой символ из множества [^xxx] Любой символ, кроме символов из множества Любой символ из диапазона ] Вычитание одного множества или диапазона из другого p{name} Любой символ, заданный категорией Unicode с именем name P{name} Любой символ, кроме заданных категорией Unicode с именем name w Множество символов, используемых при задании идентификаторов W Множество символов, не используемых при задании идентификаторов s Пробелы S Все, кроме пробелов d Цифры D Не цифры Примеры: @". +" – "rn. Здесь имеютсяnдве строки"; @"+" – "0 xabcfx"; @"[^fx]+" – "0 xabcfx"; @"+" – "0 xabcfx"; @"[^a-f]+" – "0 xabcfx"; @"]+" – "0 xabcfx"; @"p{Lu}" – "City Lights"; // Lu – прописные буквы @"P{Lu}" – "City"; @"p{Is. Cyrillic}" – "ха. OS"; // Is. Cyrillic – русские буквы

    Программирование РВ Привязка ^, A В начале строки $, Z В конце строки или до символа «n» в конце строки z В конце строки G В том месте, где заканчивается предыдущее соответствие b Граница слова B Любая позиция не на границе слова Примеры: @"G(d)" – "(1)(3)(5)(9) "; // три соответствия (1), (2) и (3) @"bnS*ionb" – "nation donation"; @"Bendw*b" – "end sends endure lender".

    Программирование РВ Операции (кванторы) *, *? Итерация +, +? Положительная итерация? , ? ? Ноль или одно соответствие {n}, {n}? Точно n соответствий {n, }, {n, }? По меньшей мере, n соответствий {n, m}, {n, m}? От n до m соответствий Примеры (первые кванторы – жадные, ищут как можно большее число элементов, вторые – ленивые, ищут как можно меньшее число элементов): @"d{3, }" – "888 -5555"; @"^d{3}" – "913 -913"; @"-d{3}$" – "913 -913"; @"5+? 5" – "888 -5555"; // три совпадения – 55, 55 и 55 @"5+5" – "888 -5555".

    Программирование РВ Группирование () Группа, автоматически получающая номер (? :) Не сохранять группу (?) или (? "имя") При обнаружении соответствия создается именованная группа (?) или Удаление ранее определенной группы и (? "имя– имя") сохранение в новой группе подстроки между ранее определенной группой и новой группой (? imnsx:) Включает или выключает в группе любую из пяти (? –imnsx:) возможных опций: i – нечувствительность к регистру; s – одна строка (тогда «. » – это любой символ); m – многострочный режим («^» , «$» – начало и конец каждой строки); n – не захватывать неименованные группы; x – исключить не преобразованные в escapeпоследовательность пробелы из шаблона и включить комментарии после знака номера (#) (? =) Положительное утверждение просмотра вперед нулевой длины

    Программирование РВ (? !) Отрицательное утверждение просмотра вперед нулевой длины (?) Невозвращаемая (жадная) часть выражения Примеры: @"(an)+" – "bananas annals"; @"an+" – "bananas annals"; // сравните, три совпадения – an, an и ann @"(? i: an)+" – "ba. NAnas annals"; @"+(? =d)" – "abc xyz 12 555 w"; @"(?

    Src="https://сайт/presentation/-112203859_437213351/image-24.jpg" alt="Программирование РВ Ссылки число Ссылка на группу k Ссылка на именованную группу Примеры: @"> Программирование РВ Ссылки число Ссылка на группу k Ссылка на именованную группу Примеры: @"(w)1" – "deep"; @"(? w)k " – "deep". Конструкции изменения | Альтернатива (соответствует операции объединения) (? (выражение)да|нет) Сопоставляется с частью «да» , если выражение соответствует; в противном случае сопоставляется с необязательной частью «нет» (? (имя)да|нет), Сопоставляется с частью «да» , если названное имя (? (число)да|нет) захвата имеет соответствие; в противном случае сопоставляется с необязательной частью «нет» Пример: @"th(e|is|at)" – "this is the day";

    Программирование РВ Подстановки $число Замещается часть строки, соответствующая группе с указанным номером ${имя} Замещается часть строки, соответствующая группе с указанным именем $$ Подставляется $ $& Замещение копией полного соответствия $` Замещение текста входной строки до соответствия $" Замещение текста входной строки после соответствия $+ Замещение последней захваченной группы $_ Замещение всей строки Комментарии (? #) Встроенный комментарий # Комментарий до конца строки

    Программирование РВ Результаты работы Regex: Regex Matches() Match. Collection Match Groups() Group. Collection Group Captures() Capture. Collection Captures()

    Программирование РВ Пример на языке C++ CLI (Visual C++/CLR/Консольное приложение CLR): int main() { Regex ^r = gcnew Regex(L"((\d)+)+"); Match ^m = r->Match(L"123 456"); int match. Count = 0; while (m->Success) { Console: : Write. Line(L"Соответствие {0}", ++match. Count); for (int i = 1; i Groups->Count; i++) { Group ^g = m->Groups[i]; Console: : Write. Line(L" Группа {0} = "{1}"", i, g->Value); for (int j = 0; j Captures->Count; j++) { Capture ^c = g->Captures[j]; Console: : Write. Line(L" Захват {0} = "{1}", позиция = {2}, длина = {3}", j, c, c->Index, c->Length); } } m = m->Next. Match(); } return 0; } System: : Text: : Regular. Expressions

    Включение действий и поиск ошибок Ограничение количества значащих цифр в числе: (s + e)(pd+ + d+(pd* + e)) s = +|p = . d = d s + e = s? = (+|-)? pd* + e = (pd*)? = (. d*)? @"(+|-)? (. d+|d+(. d*)?)" или @"^(+|-)? (. d+|d+(. d*)?)$" Regex r = new Regex(@"^(+|-)? (. (? "digit"d)+|(? "digit"d)+(. (? "digit"d)*)?)$"); Match m = r. Match("+1. 23456789"); if (m. Success) { Group g = m. Groups["digit"]; if (g. Captures. Count

    Включение действий и поиск ошибок Определение позиции ошибки: Regex r = new Regex(@"(+|-)? (. (? "digit"d)+|(? "digit"d)+(. (? "digit"d)*)?)"); string str = "+1. 2345!678"; Match m = r. Match(str); if (m. Success) { Group g = m. Groups["digit"]; if (g. Captures. Count 0) Console. Write. Line("Ошибка в позиции 1: неожиданный символ "{0}"", str); else if (m. Length

    Включение действий и поиск ошибок Определение позиции ошибки: 1. первая позиция входной цепочки (1), если первое соответствие не начинается с позиции Index = 0; 2. позиция, следующая за последним соответствием (match. Length + 1), если она не совпадает с последней позицией входной цепочки; 3. позиция первого разрыва между соответствиями (match[i]. Index + match[i]. Length + 1), если символ, следующий за предыдущим соответствием, не является первым символом следующего соответствия.

    Index) break; index = m[i]. Index + m[i]. Length; } Console. Write. Line("Ошибка в позиции {0} "{1}"", index + 1, str); } «abc. xyz. pqr» – правильно; «+abc. xyz. pqr» – ошибка в позиции 1 («+»); «abc. xyz. pqr!» – ошибка в позиции 12 («!»); «abc. xyz!. pqr» – ошибка в позиции 8 («!»).

    Включение действий и поиск ошибок Но! «abc. xyz. +pqr» – ошибка в позиции 8 («. »). Новый вариант шаблона: @"w+(. w+)*(. (? !$))? " Проверка: «abc. xyz. +pqr» – ошибка в позиции 9 («+»); «abc. xyz. pqr. » – ошибка в позиции 12 («. »).

    Сбалансированные определения: «(? "x")» добавляет в коллекцию с именем «x» один элемент; «(? "-x")» убирает из коллекции «x» один элемент; «(? (x)(? !))» проверяет, что в коллекции «x» не осталось элементов. Язык L, описывающий вложенные операторы языка Pascal «begin end; »: @"^s*((? "begins+)+(? "-begin"ends*; s*)+)*(? (begin)(? !))$".