Алгоритм получения списка всех слов, являющихся анаграммами всех подстрок (scrabble)?

Например, если входная строка - helloworld, я хочу, чтобы вывод был похож на:

do
he
we
low
hell
hold
roll
well
word
hello
lower
world
...

вплоть до самого длинного слова, являющегося анаграммой подстроки слова helloworld. Как в Скрэббл, например. Входная строка может быть любой длины, но редко превышает 16 символов.

Я провел поиск и нашел такие структуры, как trie, но я все еще не уверен, как это сделать на самом деле.


person PowerApp101    schedule 19.05.2009    source источник
comment
почему do является допустимой строкой в ​​приведенном выше случае? Это не анаграмма какой-то подстроки ryt?   -  person nitish712    schedule 21.05.2013
comment
@ nitish712 do действителен, потому что во входной строке есть буквы «d» и «o».   -  person PowerApp101    schedule 24.01.2014


Ответы (8)


Структура, используемая для хранения вашего словаря действительных записей, будет иметь огромное влияние на эффективность. Организуйте его как дерево, корнем которого является единственная буква «слово» с нулевой буквой, пустая строка. Каждый дочерний элемент корня представляет собой одну первую букву возможного слова, дочерние элементы тех, которые являются второй буквой возможного слова и т. д., с каждым узлом, помеченным относительно того, образует ли он на самом деле слово или нет.

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

Так, например, если ваша входная строка «helloworld», вы собираетесь сначала вызвать свою функцию рекурсивного тестера с «», передав оставшиеся доступные буквы «helloworld» в качестве второго параметра. Функция видит, что "" не является словом, но дочерний элемент "h" существует. Поэтому он называет себя с «h» и «elloworld». Функция видит, что "h" не является словом, но дочернее "e" существует. Так он называет себя на «он» и «ллоуорлд». Функция видит, что помечено «е», значит, «он» — это слово, обратите внимание. Кроме того, дочерний элемент «l» существует, поэтому следующий вызов — «hel» с «loworld». Затем он найдет «ад», затем «привет», затем ему придется отступить и, возможно, затем найти «пустой», прежде чем снова вернуться полностью к пустой строке, а затем начать со слов «e».

person Community    schedule 19.05.2009
comment
Мне нравится это объяснение. Я могу (почти) понять это. Я думаю, нужно сделать анализ ручкой и бумагой. - person PowerApp101; 19.05.2009
comment
Похоже, это довольно близко к DAWG Нормана Рэмси (другой пост); должен был знать, что для этого есть формальное определение. - person ; 19.05.2009
comment
Нет... Я подумал об этом и решил, что не понимаю функции тестера. Я понимаю, как построить дерево. Как он переходит от приветствия к пустоте? Рекурсия никогда не была моей сильной стороной! - person PowerApp101; 21.05.2009
comment
Он не будет напрямую переходить от приветствия к пустоте. Вызов, получивший параметры h и elloworld, впоследствии будет обращаться ко всем дочерним узлам, которые будут состоять из всех двухбуквенных комбинаций, с которых начинаются слова (he & lloworld; ho & ellworld; но не hl & ничего, потому что этот дочерний элемент не будет существовать). ). Вызов через него и т. д. дойдет до приветствия, и в конечном итоге эта рекурсия исчерпает себя. Затем из h & elloworld вы снова вызовете ho & ellworld, и этот вызов в конечном итоге рекурсивно достигнет пустого - person ; 21.05.2009
comment
Как это можно сделать с помощью базы данных SQL? Размер словаря, который я хочу использовать, не может быть обработан языком, который я использую (PHP). - person Joe Phillips; 26.08.2009

Я не мог устоять перед собственной реализацией. Он создает словарь, сортируя все буквы в алфавитном порядке и сопоставляя их со словами, которые можно из них составить. Это операция запуска за O(n), которая устраняет необходимость поиска всех перестановок. Вы можете реализовать словарь в виде попытки на другом языке, чтобы добиться более быстрого ускорения.

Команда «getAnagrams» также является операцией O(n), которая ищет каждое слово в словаре, чтобы увидеть, является ли оно подмножеством поиска. Выполнение getAnagrams("радиотелеграфически")" (слово из 20 букв) заняло на моем ноутбуке примерно 1 секунду и вернуло 1496 анаграмм.

# Using the 38617 word dictionary at 
# http://www.cs.umd.edu/class/fall2008/cmsc433/p5/Usr.Dict.Words.txt
# Usage: getAnagrams("helloworld")

def containsLetters(subword, word):
    wordlen = len(word)
    subwordlen = len(subword)

    if subwordlen > wordlen:
        return False

    word = list(word)
    for c in subword:
        try:
            index = word.index(c)
        except ValueError:
            return False
        word.pop(index)
    return True

def getAnagrams(word):
    output = []
    for key in mydict.iterkeys():
        if containsLetters(key, word):
            output.extend(mydict[key])

    output.sort(key=len)
    return output

f = open("dict.txt")
wordlist = f.readlines()
f.close()

mydict = {}
for word in wordlist:
    word = word.rstrip()
    temp = list(word)
    temp.sort()
    letters = ''.join(temp)

    if letters in mydict:
        mydict[letters].append(word)
    else:
        mydict[letters] = [word]

Пример запуска:

>>> getAnagrams("helloworld")
>>> ['do', 'he', 'we', 're', 'oh', 'or', 'row', 'hew', 'her', 'hoe', 'woo', 'red', 'dew', 'led', 'doe', 'ode', 'low', 'owl', 'rod', 'old', 'how', 'who', 'rho', 'ore', 'roe', 'owe', 'woe', 'hero', 'wood', 'door', 'odor', 'hold', 'well', 'owed', 'dell', 'dole', 'lewd', 'weld', 'doer', 'redo', 'rode', 'howl', 'hole', 'hell', 'drew', 'word', 'roll', 'wore', 'wool','herd', 'held', 'lore', 'role', 'lord', 'doll', 'hood', 'whore', 'rowed', 'wooed', 'whorl', 'world', 'older', 'dowel', 'horde', 'droll', 'drool', 'dwell', 'holed', 'lower', 'hello', 'wooer', 'rodeo', 'whole', 'hollow', 'howler', 'rolled', 'howled', 'holder', 'hollowed']
person Unknown    schedule 19.05.2009
comment
Это, безусловно, делает работу! Но, возможно, метод Джона Пири более эффективен? Я поиграю с обоими. - person PowerApp101; 19.05.2009
comment
@ 20th Century Boy: Джон Пири описывает своего рода три, который я уже предложил в качестве возможной замены хеш-таблице. Однако звучит неполноценно. Он описывает прогулку по трие с помощью H-e, что дает слово he, но пренебрегает перестановкой eh (я думаю, это слово). - person Unknown; 19.05.2009
comment
@Unknown: я думаю, что eh будет обнаружен после всех слов h, то есть когда функция вернется к вершине дерева и перейдет на ветвь, начинающуюся с e. - person PowerApp101; 19.05.2009

Структура данных, которую вы хотите, называется направленным ациклическим графом слов (dawg) и описывается Эндрю Аппель и Гай Якобсен в своей статье «Самая быстрая в мире программа Scrabble», которую, к сожалению, они решили не размещать в свободном доступе в Интернете. Членство в ACM или университетская библиотека сделают это за вас.

Я реализовал эту структуру данных как минимум на двух языках — она проста, легка в реализации и очень, очень быстра.

person Norman Ramsey    schedule 19.05.2009
comment
Я нашел это на первой странице поиска Google :-) - person PowerApp101; 19.05.2009
comment
Возможно, есть более быстрый... см. ericsink.com/downloads/faster- scrabble-gordon.pdf - person Steve Powell; 26.05.2009
comment
спасибо @Norman и Steve за то, что указали на эти замечательные ресурсы :) - person Dzung Nguyen; 24.02.2015
comment
GADDAG — это более быстрый алгоритм для создания ходов в Scrabble, но не обязательно для простого поиска анаграмм/субанаграмм. Они должны быть сопоставимы, за исключением того, что DAWG может быть быстрее, поскольку он достаточно мал, чтобы поместиться в кэш-память. - person Cesar; 05.02.2016

Простой подход состоит в том, чтобы сгенерировать все «подстроки» и для каждой из них проверить, является ли она элементом множества допустимых слов. Например, в Python 2.6:

import itertools
import urllib

def words():
  f = urllib.urlopen(
    'http://www.cs.umd.edu/class/fall2008/cmsc433/p5/Usr.Dict.Words.txt')
  allwords = set(w[:-1] for w in f)
  f.close()
  return allwords

def substrings(s):
  for i in range(2, len(s)+1):
    for p in itertools.permutations(s, i):
      yield ''.join(p)

def main():
  w = words()
  print '%d words' % len(w)
  ss = set(substrings('weep'))
  print '%d substrings' % len(ss)
  good = ss & w
  print '%d good ones' % len(good)
  sgood = sorted(good, key=lambda w:(len(w), w))
  for aword in sgood:
    print aword

main()

будет излучать:

38617 words
31 substrings
5 good ones
we
ewe
pew
wee
weep

Конечно, как указывалось в других ответах, целенаправленная организация ваших данных может значительно ускорить время выполнения - хотя лучшая организация данных для быстрого поиска анаграмм может быть другой... но это во многом будет зависеть от характера вашего словаря. разрешенных слов (несколько десятков тысяч, как здесь, или миллионы?). Следует учитывать хэш-карты и «подписи» (основанные на сортировке букв в каждом слове), а также попытки и т. д.

person Alex Martelli    schedule 19.05.2009
comment
Это работает для небольшой тестовой строки, но 16-символьная дает 20922789888000 возможных подстрок. Для длинных тестовых строк вы хотите, чтобы они были связаны допустимыми записями, а не возможными комбинациями букв. - person ; 19.05.2009

Вам нужна реализация набора полномочий.

Также посмотрите блог Эрика Липпартса, он писал о это самое некоторое время назад

РЕДАКТИРОВАТЬ:

Вот реализация, которую я написал для получения набора мощности из заданной строки...

private IEnumerable<string> GetPowerSet(string letters)
{
  char[] letterArray = letters.ToCharArray();
  for (int i = 0; i < Math.Pow(2.0, letterArray.Length); i++)
  {
    StringBuilder sb = new StringBuilder();
    for (int j = 0; j < letterArray.Length; j++)
    {
      int pos = Convert.ToInt32(Math.Pow(2.0, j));
      if ((pos & i) == pos)
      {
        sb.Append(letterArray[j]);
      }
    }
    yield return new string(sb.ToString().ToCharArray().OrderBy(c => c).ToArray());
  }
}

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

Dictionary<string,IEnumerable<string>>

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

wordlist = (from s in fileText.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
                let k = new string(s.ToCharArray().OrderBy(c => c).ToArray())
                group s by k).ToDictionary(o => o.Key, sl => sl.Select(a => a));
person Tim Jarvis    schedule 19.05.2009

Например, Тим Дж, блог Эрика Липперта, где в первую очередь нужно прийти мне на ум. Я хотел добавить, что он написал продолжение о том, как улучшить производительность своей первой попытки.

person Lucas    schedule 19.05.2009

Я верю коду Ruby в ответах на этот вопрос также решит вашу проблему.

person las3rjock    schedule 19.05.2009

В последнее время я много играл в Wordfeud на своем телефоне, и мне было любопытно, могу ли я придумать какой-нибудь код, чтобы получить список возможных слов. Следующий код берет ваши доступные исходные буквы (* для подстановочных знаков) и массив с основным списком допустимых слов (TWL, SOWPODS и т. д.) и генерирует список совпадений. Он делает это, пытаясь построить каждое слово в основном списке из ваших исходных букв.

Я нашел эту тему после написания своего кода, и он определенно не так эффективен, как метод Джона Пири или алгоритм DAWG, но все же довольно быстр.

public IList<string> Matches(string sourceLetters, string [] wordList)
{
    sourceLetters = sourceLetters.ToUpper();

    IList<string> matches = new List<string>();

    foreach (string word in wordList)
    {
        if (WordCanBeBuiltFromSourceLetters(word, sourceLetters))
            matches.Add(word);
    }

    return matches;
}


public bool WordCanBeBuiltFromSourceLetters(string targetWord, string sourceLetters)
{
    string builtWord = "";

    foreach (char letter in targetWord)
    {
        int pos = sourceLetters.IndexOf(letter);
        if (pos >= 0)
        {
            builtWord += letter;
            sourceLetters = sourceLetters.Remove(pos, 1);
            continue;
        }


        // check for wildcard
        pos = sourceLetters.IndexOf("*");
        if (pos >= 0)
        {
            builtWord += letter;
            sourceLetters = sourceLetters.Remove(pos, 1);
        }


    }

    return string.Equals(builtWord, targetWord);

}
person Pete Nelson    schedule 26.01.2011