Алгоритм и тактика поиска слов в игре Балда
Однажды на Хабре нашел статью об алгоритме поиска слов в игре балда: habrahabr.ru/post/207734 Я сам являюсь автором решателя «Робот Балда 2», который за многие годы приобрел популярность у многих онлайн игроков в игре Балда. И я хотел бы то же поделиться своим опытом и рассказать об одном уникальном алгоритме в игре балда, который еще ни кем не применялся.
Про ту статью в целом. По такому же алгоритму и у меня ищутся слова, через префиксные деревья. Но вместо двух деревьев, у меня одно, которое содержит символ «разделитель», после которого оставшаяся часть слова идет инвертированной.
Есть также возможность включить более сложное префиксное дерево («турбо-режим»), с символом «пустышка». В этом случае терминальный узел содержит все буквы, которые можно поставить в пустышке. Например, мы прошли путь К*Т, и встретили терминальный узел. Он будет содержать две буквы, «О» и «И». В ячейке «О» будет ссылка на слово КОТ, а в «И» на слово КИТ. В итоге, пустышка позволяет избавиться от перебора 32-х букв в каждой итерации на пустых клетках. Но увеличивает размер префиксного словаря примерно в 5 раз. В чистом виде это дает ускорение в 4 раза (если не изменяет память), но у меня помимо поиска слов время тратится еще и на анализ, поэтому общее ускорение всего в 1.5 раза.
Лучше всего символ пустышки сделать первым символом(корневым узлом) в префиксном дереве, и поиск слов начинать всегда с пустышки(даже если у вас дерево без символа пустышки). В этом случае скорость поиска будет гораздо быстрей, и дерево не вырастет в 5 раз. Однако, у меня специально сделано что бы поиск начинался с уже поставленных букв. Хотя это крайне не эффективно, т.к. увеличивает кол-во рекурсивных проходов, и нужно будет отсеивать дубли, которые неизбежно появятся на каждой клетке, через которые проходит слово. Но такой подход(поиск с не пустых клеток) задуман специально, чтоб можно было применить одну хитрую оптимизацию, о которой я расскажу в конце. С помощью нее проблема дублей отпадет, скорость компенсируется(и даже подрастет), и появится новое свойство — стабильность времени поиска.
Многие могут спросить, зачем еще быстрей. Если даже самые простые и медленные алгоритмы ищут слова за миллисекунды. Эти вопросы могут возникать только у тех программистов, кто не пробовал играть против «чемпионов». Их программы умеют думать на несколько ходов вперед. Значит, программы должны не просто искать слова, но и применять тактику. А самая универсальная для всех логических игр тактика — минимакс. Что это такое: хорошо описано здесь или здесь. А применение минимакса означает, что нужно делать очень много виртуальных ходов. Ну например, если каждому игроку в среднем достается 100 слов в каждом ходу, то что бы просмотреть все варианты развития игры на 4 хода вперед потребуется 100^(4-1)=1.000.000 поисков. Если ваша программа умеет искать все слова за 1мс, то она проверит игру на 4 хода вперед за 16 минут! А на раздумья над ходом дается как правило 2 минуты. Теперь вы понимаете, зачем нужен очень быстрый поиск. Моя программа умеет за несколько секунд анализировать игру на 8-10 ходов вперед.
Самое большое ускорение дает отсеивание слов. Если сказать проще — ветками дерева перебора становятся только самые длинные слова(Это примерно. В реальности, у меня отбор слов чуточку сложней). Как показала практика, крайне редко бывает, что в начале и середине партии более короткие слова в будущем отыгрывают свою разницу в очках с более длинным словом, да еще и приносят больше очков. У соперника слишком много вариантов «отыграться», и вероятность поймать его в ловушку слишком низкая. А значит, нет смысла терять время на короткие слова. А если уже конец партии, и остается не много ходов, программа расширяет список анализируемых слов. У меня уже за 4-6 ходов до конца игры участвуют в переборе абсолютно все слова. И к концу партии как раз и уместно смотреть короткие слова. Не редко бывает, что под конец партии имея слова из 4-5 букв, выгодней походить из 2-3-х.
Второе существенное ускорение дает альфа бета отсечения.
До этого я упоминал стандартные алгоритмы, которые вы всегда сможете найти и ознакомиться. А теперь напишу свое «изобретение». Оно касается оптимизации поиска слов в процессе построения дерева виртуальных ходов. Вот смотрите. Как обычно мы строим дерево перебора в балде:
1. Находим все слова.
2. Перебираем все слова, проставляя поочередно каждое слово на игровое поле.
3. Переходим рекурсивно на пункт 1. или возвращаемся с рекурсии, если слишком глубоко залезли.
А теперь напишу, как у меня:
1. Находим все слова.
2. Перебираем все слова, проставляя поочередно каждое слово на игровое поле.
3. Находим все слова, но уже по другому принципу! Копируем в результат поиска все слова, которые уже были найдены в последнем ходу. Исключаем из этого списка те слова, которые теперь невозможно составить, а именно, вычеркиваем все слова, чья вставляемая буква находилась в той же самой клетке, куда была поставлена буква последнего сыгранного слова (ведь эта клетка теперь занята, а значит больше не составишь в ней слова). Дальше добавляем в список новые слова. Для этого ищем не все слова на игровом поле, а только те, которые проходят через занятую клетку последним словом. Ведь что изменилось с предыдущего хода — на игровом поле появилась новая буква. А значит, если и появились новые слова, то они все должны проходить через новую букву. Если не проходят — то эти слова уже ранее найдены, и есть у нас в списке.
Что в итоге: Вместо того, что бы пускать рекурсивный поиск слов от каждой клетки игрового поля, мы ее пускаем всего в одну клетку! и не важно, какого размера игровое поле. Пусть даже игровое поле содержит миллион клеток. Мы будем «доискивать» новые слова всегда только в одной клетке!
4. Переходим рекурсивно на пункт 2. или возвращаемся с рекурсии, если слишком глубоко залезли.
У меня на самом деле список слов не копируется каждый раз, и слова не удаляются в чистом виде. А осуществляются все это за счет переключения указателя на страницы в многомерном массиве, чья размерность определяется как «адрес клетки», «глубина хода», и собственно ID-шники слов, которые были найдены в конкретной клетке, на конкретной глубине. Таким образом, когда мы откатываемся в дереве на уровень назад, нам не нужно восстанавливать список найденных слов в этом узле, мы просто уменьшаем указатель на «страницу». А когда наоборот, углубляемся — указатель увеличивается, и в новую страницу заносятся новые слова. Таким образом. Где нибудь на 10 ходу чтоб узнать все слова в этом ходу, нам нужно сложить все слова из 10-ти «доисканных страниц». Все они в сумме и представляют собой список слов, как если бы искали обычным способом. Но если честно, я уже не помню всех технических подробностей структуры многомерного массива.
Как ни странно, но мой алгоритм «доискивания» слов не дал ошеломляющего ускорения. Но зато он дал стабильность времени поиска. Обычный метод поиска уже к середине игры существенно замедляется, а мой не замечает этой «нагрузки». Ведь как ранее говорилось, сколько бы букв не было на игровом поле — на каждый виртуальный ход будут искаться слова только в 1-ой клетке, а не во всех. Особенно ценно это свойство на больших игровых полях.
В заключение хочу сказать, что анализ минимаксом сильно зависит от точности словаря. Поэтому важен не размер словаря, а его соответствие словарю игрового портала.