Решатель филвордов
Решатель филвордов

На работе, в обеденный перерыв коллега показал игрушку на Яндекс играх – Филворды. Как то не заладилась игра у меня – вроде простые слова, но дело шло медленно. А у товарища уровень был выше 400. Первая  мысль при таком фэйле – конечно, показать глупой машине, что есть кто-то умнее ее! То есть другая машина…

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

Алгоритм поиска слов

Очень интересной и простой показалась задача создания алгоритма перебора всех цепочек. В основе классический алгоритм поиска пути. Первоначальное решение:

  • Переводим нашу таблицу букв в более понятный список.

  • Из каждой ячейки таблицы строить цепочки - [[0, 0], [0, 1]], [[0, 0], [1, 0]], [[0, 0],  [0, 1], [0, 2]], [[0, 0], [1, 0], [2, 0]] и.т.д  стартуя со всех ячеек.

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

  • Переводим обратно цифры в буквы (как во втором пункте, но наоборот). Разбиваем цепочки на составные части  “ро, роз, роза, розас”.

  • Для отсечения лишних наборов проверяем и отсекаем слова, в которых повторяются более 2 гласных или более 3 согласных, так же ставим ограничение на длину слова (тут здравая логика).

  • Проверяем полученные наборы в словаре.

Подойдя к моменту тестирования, столкнулся с тем (что и ожидалось), что при увеличении исследуемой таблицы резко возрастает время обработки (на моем «старичке» очень). Чтобы не углублять в описание O-большого, по-простому: таблица 4*4 считается порядка 20 секунд, 5*5 десятка минут, чем дальше в лес, тем еще страшнее. Решение – разделяй и властвуй. Посчитал оптимально «шагать» по исходной таблице филвордов ячейкой 4*4. Есть минус такого подхода – вероятность выпадения слов содержащих 5 букв и более, у которых 4 буквы расположены в ряд. Плюсы – стабильная скорость обработки, возможность обрабатывать не только квадратные таблицы «6*6», а и «5*25» и.т.п.

Далее постарался расписать подробно по коду (весь код сложил в один файл, чтобы было удобнее пробовать в google colab).

Настройка распознавания картинок

Чтобы каждый раз не вводить наборы букв, написал скрипт распознавания скриншота с игры и перевода полученного результата - букв, в «удобоваримый» список python. С помощь его мы получаем список, который можно поместить в скрипт (указанный выше в гугл колаб) в start_data = np.array(полученный список), для последующей препарации.

Скрипт с описанием

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

Скрин нужно сделать с равными отступами снизу и сверху, так как далее картинка «разрезается» на равные части. Пользуюсь Яндекс.диском, скрин из папки диска перекидываю сразу в рабочую папку гугла. Картинка должна выглядеть приблизительно так:

P.S.: мои первые шаги в народ, просьба сильно не бить. Учусь - ставлю задачу → читаю книгу.

Комментарии (5)


  1. OlegFX
    23.09.2021 10:17

    Писал когда-то подобное для 4*4 на Go — только ищет всё, потому что делает полный перебор рекурсивно. Правда, мультипоточность и хитрый старт поиска. Только что проверил время поиска на 4-ядерном макбуке с использованием словаря на 99431 слов:

    4*4 — Execution time: 1.323269ms


    1. zxgame Автор
      20.10.2021 12:12

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

      Не могли бы вы поделиться "хитрым стартом поиска", заинтриговали) т.к. Execution time: 1.323269ms прям взбодрили меня.


      1. OlegFX
        15.11.2021 10:57

        Для начала - подготовка словаря. Я создавал слайс:

        dict [][]string // [аб]аббатский

        То есть кодируем первые 2 буквы каждого слова в число - это индекс основного слайса. А по этому индексу лежит отсортированный массив слов в нижнем регистре, начинающихся с этих двух букв.

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

        Так как массив отсортирован - можно искать только пока префикс текущего слова в списке "меньше" найденной комбинации букв (я этого не делал, но это ещё ускорило бы поиск).


        1. OlegFX
          15.11.2021 11:21
          +1

          Если бы я ставил себе цело перейти на микросекундные скорости, пожертвовав памятью - я бы, наверное, сделал что-то типа такого:

          var ultrafastDict [][]WordsStruct // [аббат]аббатский
          
          type WordsStruct struct {
          	isWord bool
          	isPrefix bool
          	subWords []string
          }

          Первые 5 букв слова образуют индекс в слайсе ultrafastDict. В алфавите 33 буквы, так что слайс для всех возможных комбинаций букв имел бы размер 33 ^ 5 = 39,135,393 элементов.

          Если мы ищем слово до 5 букв включительно (а это подавляющее количество поисков для данного алгоритма) - нам достаточно достать элемент под индексом, образующимся из данной комбинации букв, и проверить в нём поля isWord (данная комбинация букв является словом) и isPrefix (данная комбинация букв является префиксом для других слов). Если же мы ищем слово из комбинации больше 5 букв - ориентируемся уже на массив subWords - как в алгоритме, который я описывал выше.


          1. OlegFX
            15.11.2021 18:11

            Там, конечно, ошибка в объявлении ultrafastDict. Правильная версия:

            var ultrafastDict []WordsStruct // [аббат]аббатский

            Либо через указатели, чтобы сэкономить память (но не GC):

            var ultrafastDict []*WordsStruct // [аббат]аббатский