Построение кроссвордов с помощью языка Wolfram Language (Mathematica)

http://www.mathematica-journal.com/2009/11/constructing-crossword-arrays-faster/
  • Перевод

Перевод поста Майкла Тротта (Michael Trott), «Constructing Crossword Arrays Faster».
Скачать перевод в виде документа Mathematica, который содержит весь код использованный в статье, можно здесь.


В главе 6 моей книги Mathematica GuideBook for Programming, в качестве примера работы со списками я обсудил то, как построить массив, представляющий собой кроссворд. Хотя этот пример был хорош для демонстрации продвинутой работы со списками, тем не менее, использование списков не является оптимальным путем построения массива кроссворда. Сложность добавления нового слова в массив с уже размещенными n-1 словами составляла для этого алгоритма ConstructingCrosswordArrays_1.png, таким образом общая сложность составления массива кроссворда из n слов становилась равной ConstructingCrosswordArrays_2.png.

На протяжении последних нескольких лет, некоторые пользователи Mathematica спрашивали меня о том, можно ли построить более быстрый алгоритм. Ответ — да, можно. Если мы будем применять методы хеширования, то мы сможем быстро и за одно и тоже время проверять, можно ли использовать некоторый элемент массива и, следовательно, мы сможем снизить общую сложность алгоритма с ConstructingCrosswordArrays_3.png до ConstructingCrosswordArrays_4.png, что для кроссвордов из тысяч слов даст большую разницу во времени, затрачиваемом на вычисления. Этот алгоритм реализован в данной статье. Когда мы размещаем отдельные буквы слова в некоторой прямоугольной таблице необходимо рассматривать множество различных ситуаций. В результате в статье содержится большее, чем обычно, количество процедурного кода. Хотя некоторые определения функций несколько длинные, благодаря комментариям между шагами вычислений и ветками решений код должен быть довольно простым для чтения и понимания.

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

Вот формулировка задачи, которую мы будем решать:

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

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

ConstructingCrosswordArrays_5.png

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

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

Функция

positionAWord[characterList,k,{i0,j0},hv]

располагает список символов characterList таким образом, что k-й элемент списка будет располагаться в квадрате {i0,j0} и слово characterList будет расположено в соответствии со значением параметра hv (горизонтально или вертикально, что будет отмечаться "" или "", соответственно). Мы будем использовать конструкцию вида C[data] для того, чтобы задавать текущий статус квадрата, при этом возможны следующие ситуации:

  • C[] — квадрат должен оставаться пустым;
  • C[«char»] — квадрат содержит символ char и является частью горизонтального или вертикального слова.
  • C[«char»,""] — квадрат содержит символ char в вертикальном слове и может быть использован также в горизонтальном слове.
  • C[«char»,""] — квадрат содержит символ char в горизонтальном слове и может быть использован также в вертикальном слове.
  • C[_,""] — квадрат не содержит пока что никаких символов и может быть использован в горизонтальном слове.
  • C[_,""] — квадрат не содержит пока что никаких символов и может быть использован в вертикальном слове.

Вся эта информация будет ассоциирована с down values функции status. При этом status[{i,j}] будет вычислять текущее состояние квадрата с центром в точке {i,j}. На выходе она выдаст объект вида C[…] или останется в невычисленном виде, если квадрат еще не был использован каким либо способом или же не является соседом для некоторого из уже расположенных в таблице слов.

Мы свяжем значение этой функции с текущим содержимым квадратом посредством выражения status[square]=value. Это позволит нам зафиксировать время поиска статуса каждого из нужных квадратов с помощью вычисления выражения status[square].

Функция positionAWord устанавливает значения квадратов в которых размещены символы слова, а также добавляет информацию о потенциальном использовании соседних квадратов в будущем. Например, квадраты расположенные непосредственно над или под горизонтально расположенным словом могут быть в будущем использованы только для вертикальных слов.

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

In[1]:=

ConstructingCrosswordArrays_6.png

В качестве примера, мы расположим слово Mathematica, начиная с квадрата {1,1} горизонтально.

In[2]:=

ConstructingCrosswordArrays_7.png

Ниже приведен список квадратов, которые были помечены.

In[3]:=

ConstructingCrosswordArrays_8.png

Out[3]=

ConstructingCrosswordArrays_9.png

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

In[4]:=

ConstructingCrosswordArrays_10.png

Приведенный ниже график показывает текущий статус квадратов, соседствующих с уже расположенными по квадратам символами.

In[5]:=

ConstructingCrosswordArrays_11.png

Out[5]=

ConstructingCrosswordArrays_12.png

Функция

positioningAWordIsPossibleQ[characterList,k,{i0,j0},hv]

проверяет, возможно ли список символов characterList расположить таким образом, чтобы k-й элемент списка располагался в квадрате {i0,j0}, а слово characterList было расположено в соответствии со значением параметра hv (горизонтально "" или вертикально ""). Для того, чтобы сделать это нам потребуется проверить, чтобы каждый из необходимых квадратов был либо доступен, либо же имел необходимый символ уже на месте. Также, соседние квадраты должны быть либо пусты, либо должны иметь буквы из уже расположенного перпендикулярно слова. Вспомогательная функция whileTrueLoops является итеративной функцией, которая заканчивает свою работу, как только с помощью техники Throw-Catch найден первый несовместимый квадрат. В дальнейшем мы всегда будем предполагать, что новое слово размещается только с помощью функции positionAWord и только тогда, когда функция positioningAWordIsPossibleQ выдает значение True.

In[6]:=

ConstructingCrosswordArrays_13.gif

In[8]:=

ConstructingCrosswordArrays_14.png

Если мы уже расположили слово Mathematica горизонтально, мы можем расположить еще одну копию слова Mathematica вертикально.

In[9]:=

ConstructingCrosswordArrays_15.png

Out[9]=

ConstructingCrosswordArrays_16.png

Но мы не можем расположить горизонтально слово Mathematica в той же позиции.

In[10]:=

ConstructingCrosswordArrays_17.png

Out[10]=

ConstructingCrosswordArrays_18.png

В целом, можно было бы поместить другую копию вдоль уже расположенной, начиная с буквы “t” в позиции {3, 3}.

In[11]:=

ConstructingCrosswordArrays_19.png

Out[11]=

ConstructingCrosswordArrays_20.png

Конечно, мы могли бы поместить другую копию слова Mathematica посередине, скажем, сделать ее вертикальной, проходящей через вторую букву “m” горизонтальной копии слова Mathematica.

In[12]:=

ConstructingCrosswordArrays_21.png

Out[12]=

ConstructingCrosswordArrays_22.png

In[13]:=

ConstructingCrosswordArrays_23.png

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

In[14]:=

ConstructingCrosswordArrays_24.png

Out[14]=

ConstructingCrosswordArrays_25.png

Функция

findAPossiblePositionAndPositionAWord[characterList]

ищет возможное расположение символов слова characterList и затем размещает их. Функция возвращает либо координаты добавляемого символа, либо $Failed, в случае, если никакого способа добавить слово нет. Мы также снабдили эту функцию несколькими довольно очевидными опциями, которые, среди прочего, позволяют располагать слова либо настолько плотно, насколько это возможно, либо наоборот — максимально свободно. Если значение опции AttachmentPosition сделано равным Inner, то функция стремится добавлять каждое новое слово внутри уже размещенных, значение Outer — наоборот, если же используется значение RandomSample, то позиция нового слова выбирается случайным образом.

In[15]:=

ConstructingCrosswordArrays_26.png

In[16]:=

ConstructingCrosswordArrays_27.png

In[17]:=

ConstructingCrosswordArrays_28.png

Наконец, мы создади функцию makeCWPGrid[status], которая будет конструировать, собственно, таблицу из букв на основе down values функции status.

In[18]:=

ConstructingCrosswordArrays_29.png

График ниже показывает состояние всех уже добавленных квадратов после того, как слово Mathematica было добавлено 6 раз к первоначальному. Серые квадраты соответствуют уже окончательно размещенным словам; красные — должны оставаться пустыми; зеленые — через них могут быть добавлены новые слова. Стрелками показано в каком направлении можно добавить новые слова.

In[19]:=

ConstructingCrosswordArrays_30.gif

Out[22]=

ConstructingCrosswordArrays_31.png

In[23]:=

ConstructingCrosswordArrays_32.gif

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

In[25]:=

ConstructingCrosswordArrays_33.png

Out[25]=

ConstructingCrosswordArrays_34.png

Применяя функцию Manipulate, мы можем с легкостью создать интерактивную версию алгоритма, которая позволит нам проследить за его шагами наглядно и просто. Приведенная ниже интерактивная версия алгоритма позволяет проследить за 12 его первыми шагами для различных значений опций.

In[26]:=

ConstructingCrosswordArrays_35.png

Out[26]=

ConstructingCrosswordArrays_36.png

Теперь давайте рассмотрим сложность реализованного метода. Ввиду того, что поиск текущего состояния квадрата является почти постоянной величиной (не зависящей от количества уже заданных квадратов), мы ожидаем линейную сложность O(n) в зависимости от количества слов. В качестве теста, мы найдем размещение 500 слов Mathematica. Для визуализации процесса вычислений мы будем использовать функцию Monitor.

In[27]:=

ConstructingCrosswordArrays_37.gif

Теперь изобразим измеренные значения затраченного времени. График справа показывает среднее время на добавление заданного количества слов. Он растет линейно с числом слов, которые необходимо добавить.

In[30]:=

ConstructingCrosswordArrays_38.png

Out[30]=

ConstructingCrosswordArrays_39.png

Таким образом, суммарное время формирования массива кроссворда из n слов имеет сложность порядка ConstructingCrosswordArrays_40.png. График показанный ниже наглядно демонстрирует это (черный — затраченное время, красный — найденная модель вида ConstructingCrosswordArrays_41.png).

In[31]:=

ConstructingCrosswordArrays_42.png

Out[32]=

ConstructingCrosswordArrays_43.png

Ниже представлен полученный массив кроссворда. Мы используем небольшой размер шрифта для того, чтобы отобразить его здесь полностью.

In[33]:=

ConstructingCrosswordArrays_44.png

Out[33]=

ConstructingCrosswordArrays_45.png

Благодаря намного большей скорости работы этого подхода, основанного на хешировании для большого числа слов (скажем, тысяч), мы теперь можем очень быстро составить кроссворд, скажем, из 250 слов. При этом в качестве списка этих слов возьмем 250 выбранных случайно встроенных функций языка Wolfram Language (системы Mathematica).

In[34]:=

ConstructingCrosswordArrays_46.gif

Out[38]=

ConstructingCrosswordArrays_47.png

In[39]:=

ConstructingCrosswordArrays_48.png

Out[39]=

ConstructingCrosswordArrays_49.png

График, показанный ниже отображает порядок в котором слова были расположены на плоскости (в таблице). Красные, высокие столбцы соответствуют словам, которые были добавлены в начале, синие, низкие “башенки” — добавленным позже.

In[40]:=

ConstructingCrosswordArrays_50.png

Out[40]=

ConstructingCrosswordArrays_51.png

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

In[41]:=

ConstructingCrosswordArrays_52.gif

Out[42]=

ConstructingCrosswordArrays_53.png

In[43]:=

ConstructingCrosswordArrays_54.gif

In[47]:=

ConstructingCrosswordArrays_55.png

Out[47]=

ConstructingCrosswordArrays_56.png

Теперь создадим функцию потенциала f для функции positionAWord. Эта функция будет использоваться для определения того, какому именно уже расположенному слову принадлежит тот или иной символ.

In[48]:=

ConstructingCrosswordArrays_57.gif

Создадим теперь некоторую вариацию функции makeCWPGrid, которая будет использовать информацию связанную с функцией f для раскрашивания слов.

In[53]:=

ConstructingCrosswordArrays_58.png

Результат представлен в таблице ниже. Цвет букв стоящих на пересечении слов смешивается.

In[54]:=

ConstructingCrosswordArrays_59.png

Out[54]=

ConstructingCrosswordArrays_60.png

Мы закончим статью, создав единую функцию CrossWordConstructionCached, которая служит для создания кроссворда на основе списка слов, который подается ей на вход.

In[55]:=

ConstructingCrosswordArrays_61.png

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

In[56]:=

ConstructingCrosswordArrays_62.png

Out[56]=

ConstructingCrosswordArrays_63.png

Или же можно создать кроссворд из фамилий всех нобелевских лауреатов (граждан России и СССР):

In[57]:=

ConstructingCrosswordArrays_64.gif

Out[58]=

ConstructingCrosswordArrays_65.png

Ресурсы для изучения Wolfram Language (Mathematica) на русском языке: habrahabr.ru/post/244451
  • +24
  • 9,1k
  • 7
Wolfram Research 45,77
Wolfram Language, Mathematica, Wolfram Alpha и др.
Поделиться публикацией
Комментарии 7
  • +2
    На КДПВ — плохой кроссворд, негодный.
    • 0
      Было бы интересно увидеть эффективный алгоритм для составления такого кроссворда:

      image
      • 0
        Довольно интересно, но очевидно не из любых слов такого рода конструкции собираются, а значит нужно еще и слова подбирать отдельно.
        • 0
          Именно. Мне интересно можно ли какой-то хэш придумать для ускорения подбора слов — что-то вроде фильтра Блума.
          • 0
            Да, это было бы круто. А для слогового письма, интересно, есть ли что-то ткаоке.
          • +1
            Пытаюсь как раз, в качестве хобби, написать такой алгоритм, после вот этой статьи: geektimes.ru/post/243643/.

            По факту напишу статью…
          • 0
            А кроссворд из терминов языков программирования — это интесная идея!

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое