Язык описания операций над целочисленными массивами задан следующей грамматикой:
<digit> ::= “0” | “1" | “2” | “3" | “4” | “5" | “6” | “7" | “8” | “9"
<number> ::= <digit> | <digit> <number>
<operation> ::= “+” | “-” | “*” | “>” | “<” | “=” | “&” | “|”
<constant-expression> ::= “-” <number> | <number>
<binary-expression> ::= “(” <expression> <operation> <expression> “)”
<expression> ::= “element” | <constant-expression> | <binary-expression>
<map-call> ::= “map{” <expression> “}”
<filter-call> ::= “filter{” <expression> “}”
<call> ::= <map-call> | <filter-call>
<call-chain> ::= <call> | <call> “%>%” <call-chain>
Арифметические операции имеют стандартную семантику. Операция “&” это логическое “и”, операция “|” --- логическое “или“. Бинарные выражения с операторам “&”, “|” , “=”, “>”, “<” имеют булевый тип, а с операторами “+”, “-”, “*” --- арифметический. Операнды арифметических операций должны иметь целочисленный тип, а операнды логических --- булевый. Вызов функции map заменяет каждый элемент массива на результат вычисления переданного арифметического выражения, в котором вместо element подставляется значение текущего элемента. Вызов функции filter оставляет в массиве только элементы, для которых переданное выражение истинно.
Последовательность вызовов применяется к массиву по очереди, слева направо.
Необходимо написать преобразователь выражений описываемых правилом <call-chain>
в выражения вида <filter-call> “%>%” <map-call>
, эквивалентные исходному.
Решение должно принимать на стандартный поток ввода одну строку ---
выражение описываемое правилом и выводить строку
с преобразованным выражением. В случае наличия синтаксической ошибки,
решение должно вывести SYNTAX ERROR, а если тип выражения не совпадает c
ожидаемым TYPE ERROR.
Дополнительно, к решению предъявляются следующие требования:
- решение должно быть выполнено на языке Java или Kotlin;
- решение должно быть оформлено в виде публичного git репозитория;
- код должен быть хорошо структурирован;
- решение должно включать в себя тесты;
Бонус:
- решение производит упрощение выражений;
Есть два пути для запуска решения:
-
- Установить Apache Maven (или открыть проект в IntelliJ IDEA как Maven проект)
- Выполнить команду
mvn -q compile exec:java 2>/dev/null
(STDERR можно и не перенаправлять, но при определённых условиях Maven может выдаватьWARNING: An illegal reflective access operation has occurred
). - Также можно выполнить встроенные тесты при помощи команды
mvn test
.
- Запустить скомпилированный файл
Solution
при помощи командыjava -jar Solution
.
Решение стоит разбить на две основных части: ru.covariance.jbintern.parser и simplifier.
Парсинг входного выражения осуществляется методом рекурсивного спуска.
Проверка на соответствие типов выражений осуществляется здесь же, во время их разбора. Для
отслеживания ошибок во время парсинга существует класс ParserException
, имеющий двух
наследников — TypeMismatchException
и SyntaxException
, отвечающих за
соответствующий тип ошибок разбора выражения.
Некоторая часть упрощения выражений вынесена на этап парсинга — вместо набора арифметических операций все арифметические выражения хранятся в виде полиномов, так как набор арифметических операций включает в себя только сложение, вычитание и умножение, что позволяет представить любое выражение, заданное такими операциями в виде полинома от element.
Второй этап упрощения представляет из себя свертку всей цепи вызовов в
filter{...}%>%map{...}
. Это делается за один линейный проход по цепи, который
"собирает" все изменения element из мапперов и подставляет в последующие
фильтры, формируя набор условий, которые должны выполнятся для начальных элементов.
После этого прохода создаётся конъюнкция всех этих условий, которая и является
единственным начальным фильтром, а начальным маппером становится композиция всех мапперов
в цепи.
Третьим этапом упрощения является метод toMiniString()
, который
переопределяет вывод полиномов, упрощая их представление благодаря тому,
что лишние нули в сложении и единицы в умножении не выводятся.
Оба этапа решения могут быть до некоторой степени улучшены.
Единственное узкое место парсера — подсчёт значений полиномов, входящих в арифметические выражения. И если сложение и вычитание в нём реализованы за линейное время, что оптимально, то умножение работает за O(n2). Эту ассимптотику можно существенно улучшить при помощи использования FFT, которое позволяет перемножать многочлены за O(n·log(n)).
К сожалению, на данный момент в simplifier-е реализованы только упрощения арифметических выражений, а булевы остаются неизменными. Это может быть исправлено при помощи:
- Разворота выражений в СДНФ или СКНФ с последующим удалением ненужных термов.
- K-maps для упрощения булевых выражений, с возможным последующим применением алгоритма Куайна-МакКласки для упрощения поиска покрытий.
Для свёртки тоже существует ряд возможных оптимизаций:
- Временная ассимптотика композиции многочленов зависит от временной ассимптотики умножения, которая, как описано выше, может быть существенно улучшена.
- Чтобы не упрощать огромное булево выражение в фильтре после свёртки, можно упрощать их по отдельности, а только затем собирать в конъюнкцию (не реализовано, так как булевы выражения не упрощаются).