Прослушал лекции Мартина Одерски на курсере по Scala.
Курс читался два раза, и следующего набора пока нет, поэтому вместо решения предложенных заданий и для теста языка, я решил попробовать портировать Алгоритм разминирования бомб Джеймса Бонда, написанный мной на Python’e 5 лет назад.
За основу взял код Мартина из итоговой лекции, которой использовался для решения задачи The Water Pouring Problem (как получить X литров воды, имея заданное число стаканов разного литража без маркировки), так как она решается в общем случае тем же алгоритмом - методом поиска в ширину.
Именно эту задачу было интересно решить по нескольким причинам:
В лекции алгоритм оставлен “сырым” (в него добавлен для оптимизации список уже пройденных вершин и кеширование последнего состояния вместо его вычисления, и то, только для того, чтобы он не тормозил на совсем простых данных).
В моей задаче исходные данные из игры, из которых одна из загадок методом полного перебора решается очень долго, поэтому алгоритм нужно оптимизировать.
Так что была и простая подзадача - переделать алгоритм для решения задачи с бомбами, а не со стаканами; и более сложная - ускорить его работу.
После окончания можно сравнить результаты с решением оригинальной задачи. В частности, по количеству строк кода, а то Python нравится как раз лаконичностью и удобным Repl, простые задачки можно решать, не выходя из него.
Так что:
Оригинальный код, перезаточенный для решения задачи разминирования бомбы, решил 3 задачи из 5, на остальных стал выдавать “java.lang.OutOfMemoryError: GC overhead limit exceeded“ из-за разрастающегося размера массива входных данных.
Решение проблемы - не исследовать все возможные пути, а в первую очередь подробно рассматривать те, которые приближают к ответу.
Для того, чтобы реализовать это, к классу описания пути нужно примешать trait Ordered
:
class Path(path: List[Move], val endState: State) extends Ordered[Path]{ |
функции сравнения - количество символов, которые стоят не своих местах (diffFromEnd).
Также надо сменить контейнер, хранящий Path, с Set на Vector или любой другой, поддерживающий хранение данных в отсортированном виде, и в определённый момент (например, после раскрытия всех вершин из начала потока), отсортировать вершины по порядку наибольшей близости к желаемому состоянию endState.
Ещё один шаг оптимизации - можно допустить, что из состояния, более близкого к решению (похожего на решение), до самого решения нужно будет сделать меньшее количество шагов, чем из состояния, менее похожего на решение.
Тогда можно отбросить более далёкие от решения варианты и перестать их обрабатывать(при этом есть риск выбросить и само оптимальное решение, всё зависит от качества оценивающей функции). Я решил обрубать по 50000 вариантов - решение при этом всё равно нашлось за 9 ходов, как и в Python-версии:val sortedMore = (more take 50000).sorted
MoveLeft(2) //двигаем третью строку влево |
Что полностью аналогично решению на Python для той же злополучной 4-й ракеты.
В итоге, получилось по строкам кода:
Решение на Python - 120 строк
Решение на Scala - 65 строк
(выбросил для подсчёта функции вывода результата на python, на scala результат по умолчанию оказался читаем - список из “Имя кейскласса + параметр” - это как раз то, в каком виде хотелось увидеть ответ).
Кажется, можно попробовать поэкспериментировать со Scala Worksheets для решения простых задач вместо Idle with Python.