Какая стратегия создания анаграмм была бы лучшей.
An anagram is a type of word play, the result of rearranging the letters of a word or phrase to produce a new word or phrase, using all the original letters exactly once; ex.
- Eleven plus two is anagram of Twelve plus one
- A decimal point is anagram of I'm a dot in place
- Astronomers is anagram of Moon starers
Поначалу это выглядит просто, просто перемешайте буквы и сгенерируйте все возможные комбинации. Но каков был бы эффективный подход для генерации только слов в словаре.
Я наткнулся на эту страницу, Решение анаграмм в Ruby.
Но каковы ваши идеи?
Конечно, создание ХОРОШИХ анаграмм - сложная проблема, но создание плохих анаграмм проще :)





На мой взгляд, наиболее разумным решением было бы случайным образом выбрать букву из входной строки и отфильтровать словарь на основе слов, которые начинаются с нее. Затем выберите другую, отфильтруйте по второй букве и т. д. Кроме того, отфильтруйте слова, которые не могут быть объединены с оставшимся текстом. Затем, когда вы дойдете до конца слова, вставьте пробел и начните его заново с оставшихся букв. Вы также можете ограничить слова на основе типа слова (например, у вас не будет двух глаголов рядом друг с другом, у вас не будет двух статей рядом друг с другом и т. д.).
Для каждого слова в словаре отсортируйте буквы по алфавиту. Итак, «foobar» становится «abfoor».
Затем, когда появится входная анаграмма, отсортируйте и ее буквы, а затем найдите ее. Это так же быстро, как поиск по хеш-таблице!
Для нескольких слов вы можете комбинировать отсортированные буквы, сортируя их по мере необходимости. Тем не менее много быстрее, чем создание всех комбинаций.
(см. комментарии для дополнительных оптимизаций и подробностей)
Похоже, что это (вместе с ответом Джимми) будет работать только для анаграммы с одним словом - как это можно применить к анаграммам фраз?
Как я уже сказал в посте, для нескольких слов вы можете исследовать все пары, тройки и т. д., В каждом случае комбинируя буквы и сортировку (и использовать mergesort, чтобы op работал быстрее!) И проверять на соответствие этой комбинации. Вы могли бы быть еще умнее, например битовые поля вообще используемых символов и ...
... очевидно, общее количество символов, поэтому, когда я говорю «протестируйте все тройки», есть огромные категории, которые вы можете сократить. Например, сначала сохраняйте слова по длине, а затем по хешу. Таким образом, в ваших парах / тройках вы уже можете пропускать комбинации с неправильным количеством символов.
сортировка символов в первую очередь не помогает. Это может дать вам одну или две комбинации, но вам нужно проверить все комбинации, а затем отклонить их. Один из способов - сгенерировать все возможные тройки, а затем сравнить их с первыми тремя буквами всех слов из словаря.
сортировка действительно помогает - это самый простой способ (помимо, скажем, использования .NET HashSet <T> или Python set ()) сопоставить упорядоченный список букв с неупорядоченным списком.
хорошо, честно, это ускоряет работу, поскольку анаграммы "foobar" и "barfoo" разрешаются в один и тот же набор результатов, но если вы собираетесь получить все анаграммы только из одного предложения, то сортировка вам не поможет так как нужно учитывать все доступные символы.
это тоже вопрос обратного просмотра. как только у вас есть большой список букв («астрономы»), вы найдете список отсортированных подстрок (например, «mno» + «aat» + «sors» или «mnoo» + «aerrsst»), чтобы вы могли его посмотреть вверх в таблице поиска, которую вы генерируете
@Jason, побитовые операции не будут работать, потому что буква может появляться в строке более одного раза. Если вы используете ИЛИ, эти повторяющиеся буквы не будут учитываться, а если вы используете сложение, возникнут коллизии.
Неэффективно для нескольких слов. Смотрите мой ответ.
Большой вопрос здесь: как выглядят ключи словаря? Я бы сделал их отсортированными строками. И значения будут всеми возможными анаграммами ключей.
Как я это вижу:
вы хотите создать таблицу, которая отображает неупорядоченные наборы букв в списки слов, то есть просматривает словарь, чтобы вы закончили, скажем,
lettermap[set(a,e,d,f)] = { "deaf", "fade" }
затем из вашего начального слова вы найдете набор букв:
astronomers => (a,e,m,n,o,o,r,r,s,s,t)
затем прокрутите все разделы этого набора (это может быть самая техническая часть, просто генерируя все возможные разделы) и найдите слова для этого набора букв.
edit: хммм, это в значительной степени то, что опубликовал Джейсон Коэн.
edit: кроме того, в комментариях к вопросу упоминается создание "хороших" анаграмм, как в примерах :). после того, как вы составите свой список всех возможных анаграмм, прогоните их через WordNet и найдите те, которые семантически близки к исходной фразе :)
После этого вам нужно будет провести какой-то рекурсивный исчерпывающий поиск. Псевдокод очень примерно:
function FindWords(solutionList, wordsSoFar, sortedQuery)
// base case
if sortedQuery is empty
solutionList.Add(wordsSoFar)
return
// recursive case
// InitialStrings("abc") is {"a","ab","abc"}
foreach initialStr in InitalStrings(sortedQuery)
// Remaining letters after initialStr
sortedQueryRec := sortedQuery.Substring(initialStr.Length)
words := words matching initialStr in the dictionary
// Note that sometimes words list will be empty
foreach word in words
// Append should return a new list, not change wordSoFar
wordsSoFarRec := Append(wordSoFar, word)
FindWords(solutionList, wordSoFarRec, sortedQueryRec)
В конце концов, вам нужно пройти по списку решений и распечатать слова в каждом подсписке с пробелами между ними. Возможно, вам придется распечатать все заказы для этих случаев (например, «Я Сэм» и «Сэм Я» - оба решения).
Конечно, я не тестировал это, и это подход грубой силы.
См. Этот назначение из отдела CSE Вашингтонского университета.
По сути, у вас есть структура данных, в которой есть только количество каждой буквы в слове (массив работает для ascii, обновите карту до карты, если вам нужна поддержка юникода). Вы можете вычесть два из этих наборов букв; если счетчик отрицательный, вы знаете, что одно слово не может быть анаграммой другого.
Работа со счетчиками упрощает задачу комбинирования. у вас есть карта для поисковой фразы и сопоставьте ее с комбинациями карт слов с той же суммой подсчетов. Это элегантное решение.
Предварительная обработка:
Создайте дерево с каждым листом как известное слово, набираемое в алфавитном порядке.
Во время поиска:
Считайте входную строку мультимножеством. Найдите первое подслово, просматривая дерево индекса, как при поиске в глубину. В каждой ветке вы можете спросить, есть ли буква x в оставшейся части моего ввода? Если у вас хорошее представление мультимножества, это должен быть запрос с постоянным временем (в основном).
Как только у вас есть первое подслово, вы можете сохранить оставшееся мультимножество и рассматривать его как новый ввод, чтобы найти остальную часть этой анаграммы (если таковая существует).
Дополните эту процедуру мемоизацией для более быстрого поиска общих мультимножеств остатков.
Это довольно быстро - каждый обход trie гарантированно дает фактическое подслово, и каждый обход занимает линейное время по длине подслова (а подслова обычно чертовски малы по стандартам кодирования). Однако, если вам В самом деле нужно что-то еще быстрее, вы можете включить все n-граммы в свой предварительный процесс, где n-грамма - это любая строка из n слов в строке. Конечно, если W = #words, тогда вы перейдете от размера индекса O (W) к O (W ^ n). Возможно, n = 2 реалистично, в зависимости от размера вашего словаря.
Пару месяцев назад я использовал следующий способ вычисления анаграмм:
Вычислите «код» для каждого слова в вашем словаре: создайте справочную таблицу от букв алфавита до простых чисел, например начиная с ['a', 2] и заканчивая ['z', 101]. В качестве шага предварительной обработки вычислите код для каждого слова в вашем словаре, найдя простое число для каждой буквы, из которой оно состоит, в поисковой таблице и умножьте их вместе. Для последующего поиска создайте карту кодов слов.
Вычислите код вашего входного слова, как описано выше.
Вычислить codeInDictionary% inputCode для каждого кода на мульти-карте. Если результат равен 0, вы нашли анаграмму и можете найти соответствующее слово. Это также работает для анаграмм из 2 или более слов.
Надеюсь, это было полезно.
Зачем такой сложный словарь ... простые числа, предварительная обработка, мульти-карта? Просто сделайте ключи словаря отсортированными строками.
См .: scribd.com/document/284697348/…
@IgorGanapolsky Потому что само по себе это может дать вам только анаграммы из одного слова. Пример «Одиннадцать плюс два» не был бы возможен в качестве вывода.
Книга Жемчуг программирования Джона Бентли довольно хорошо освещает этот вопрос. Обязательно к прочтению.
Не знаю, почему вы отказались от модификации, но во второй колонке Programming Pearls рассматривается реализация программы, которая находит все наборы анаграмм по словарю. Определенно стоит посмотреть. Скомпилируйте и запустите код следующим образом: ./sign <somedictionary | сортировать | ./давить
Некоторое время назад я написал в блоге сообщение о том, как быстро найти анаграммы из двух слов. Он работает очень быстро: поиск всех 44 анаграмм из двух слов для слова с текстовым файлом, содержащим более 300 000 слов (4 мегабайта), занимает всего 0,6 секунды в программе Ruby.
Алгоритм поиска анаграмм из двух слов (на Ruby)
Можно сделать приложение быстрее, если разрешить предварительную обработку списка слов в большой хэш-сопоставление из слов, отсортированных по буквам, в список слов, использующих эти буквы. Эти предварительно обработанные данные можно сериализовать и использовать с этого момента.
Я удалил свой предыдущий комментарий, потому что он был неправильным. В любом случае: ("az" .sum + "by" .sum) - "mmnn" .sum => 0. Эта функция контрольной суммы не подходит для решения анаграмм
Не идеально, но очень быстро. Вам нужно сделать окончательную проверку с любой контрольной суммой, потому что возможность коллизий никуда не делась.
Одна из основополагающих работ по программным анаграммам была написана Майклом Мортоном (Mr. Machine Tool) с использованием инструмента под названием Ars Magna. Вот легкая статья, основанный на его работе.
Большинство этих ответов ужасно неэффективны и / или дадут только решения из одного слова (без пробелов). Мое решение обрабатывает любое количество слов и очень эффективно.
Вам нужна структура данных trie. Вот реализация Python полный. Вам просто нужен список слов, сохраненный в файле с именем words.txt. Вы можете попробовать список слов словаря Scrabble здесь:
http://www.isc.ro/lists/twl06.zip
MIN_WORD_SIZE = 4 # min size of a word in the output
class Node(object):
def __init__(self, letter='', final=False, depth=0):
self.letter = letter
self.final = final
self.depth = depth
self.children = {}
def add(self, letters):
node = self
for index, letter in enumerate(letters):
if letter not in node.children:
node.children[letter] = Node(letter, index==len(letters)-1, index+1)
node = node.children[letter]
def anagram(self, letters):
tiles = {}
for letter in letters:
tiles[letter] = tiles.get(letter, 0) + 1
min_length = len(letters)
return self._anagram(tiles, [], self, min_length)
def _anagram(self, tiles, path, root, min_length):
if self.final and self.depth >= MIN_WORD_SIZE:
word = ''.join(path)
length = len(word.replace(' ', ''))
if length >= min_length:
yield word
path.append(' ')
for word in root._anagram(tiles, path, root, min_length):
yield word
path.pop()
for letter, node in self.children.iteritems():
count = tiles.get(letter, 0)
if count == 0:
continue
tiles[letter] = count - 1
path.append(letter)
for word in node._anagram(tiles, path, root, min_length):
yield word
path.pop()
tiles[letter] = count
def load_dictionary(path):
result = Node()
for line in open(path, 'r'):
word = line.strip().lower()
result.add(word)
return result
def main():
print 'Loading word list.'
words = load_dictionary('words.txt')
while True:
letters = raw_input('Enter letters: ')
letters = letters.lower()
letters = letters.replace(' ', '')
if not letters:
break
count = 0
for word in words.anagram(letters):
print word
count += 1
print '%d results.' % count
if __name__ == '__main__':
main()
Когда вы запускаете программу, слова загружаются в дерево памяти. После этого просто введите буквы, по которым вы хотите выполнить поиск, и он распечатает результаты. Он покажет только результаты, в которых используются все введенные буквы, не короче.
Он фильтрует короткие слова из вывода, иначе количество результатов будет огромным. Не стесняйтесь изменять настройку MIN_WORD_SIZE. Имейте в виду, что простое использование «астрономов» в качестве входных данных дает 233 549 результатов, если MIN_WORD_SIZE равен 1. Возможно, вы найдете более короткий список слов, который содержит только более распространенные английские слова.
Кроме того, сокращение «Я» (из одного из ваших примеров) не будет отображаться в результатах, если вы не добавите «im» в словарь и не установите MIN_WORD_SIZE на 2.
Уловка для получения нескольких слов заключается в том, чтобы вернуться к корневому узлу в дереве всякий раз, когда вы встречаете полное слово в поиске. Затем вы продолжаете перемещаться по дереву, пока не будут использованы все буквы.
Только 16818 анаграмм для астрономов с длиной слова 1 из моей программы анаграмм, так как она не выдает перестановок. Время работы около 2 с для получения результатов на моем скромном компьютере AMD Sempron. Я сохраняю результаты в файл, это полезнее, чем поток слов в текстовой консоли. Я не использую древовидные структуры, а использую простой текст с рекурсией, совпадающей с ключами из словаря, хешированными с ключами отсортированных букв.
Я разместил свой предыдущий код в DaniWeb как daniweb.com/software-development/python/code/393153/….
Отчет об ошибке: если в списке слов есть две записи: «foobar» и «foob» (в указанном порядке), то фрагмент кода не найдет анаграмму для «boof». Только если вы измените порядок слов в списке, он правильно вернет "foob". Я думаю, это можно исправить, добавив еще одно предложение if в самый первый цикл for, но я оставлю это тому, кто знает Python.
Не могли бы вы описать свой алгоритм в двух предложениях? Что меня особенно интересует, так это то, что происходит после того, как вы решите, что определенное словарное слово может быть составлено с использованием некоторых букв ввода. Я понимаю, что затем мы проверяем, можно ли использовать оставшиеся символы для составления другого слова. Как мы узнаем, что исчерпали все возможности?
@MadPhysicist структура trie позволяет вам воспользоваться тем, что в английском языке много слов одинаковы, но с разными окончаниями. Итак, если введенные вами буквы для анаграммы содержат «q», «u», но не «i», то всего за 3 хода мы можем исключить «быстро», «быстро», «быстрее», «быстрее» и т. д. это структура, которая на практике группирует слова в подмножества друг друга. Я подозреваю, что есть другая структура данных, которая также позволяет вам исключить все слова с буквой «i» и не заботится о порядке букв, но не знает, как сохранить размер легко управляемым.
@MadPhysicist Первое, что он делает, это находит все слова, которые могут быть внутри полного предложения анаграммы. Он делает это путем тестирования всех дочерних элементов из верхнего узла, а затем использует рекурсию, чтобы по очереди следить за всеми внуками тех дочерних элементов, которые прошли, и т. д. Это исчерпывает все возможности для первого слова. Для каждого из этих возможных первых слов он затем повторяет весь процесс, чтобы получить другое слово с оставшимися буквами, и снова округлить для каждого из них, пока все буквы не будут израсходованы как надо. Как вы понимаете, даже это не может работать с длинной входной строкой.
Если я возьму словарь как хеш-карту, так как каждое слово уникально, а ключ - это двоичное (или шестнадцатеричное) представление слова. Тогда, если у меня есть слово, я могу легко найти его значение со сложностью O (1).
Теперь, если нам нужно сгенерировать все действительные анаграммы, нам нужно проверить, есть ли сгенерированная анаграмма в словаре, если она присутствует в словаре, действительна ли она, иначе нам нужно игнорировать это.
Я предполагаю, что слово может содержать не более 100 символов (или больше, но есть предел).
Таким образом, любое слово, которое мы воспринимаем как последовательность индексов, например слово «привет», может быть представлено как «1234». Теперь анаграммы «1234» - «1243», «1242» и т. д.
Единственное, что нам нужно сделать, это сохранить все такие комбинации индексов для определенного количества символов. Это одноразовая задача. А затем из комбинаций можно сгенерировать слова, выбрав символы из индекса, и мы получим анаграммы.
Чтобы проверить, действительны ли анаграммы, просто выполните индексирование в словаре и подтвердите.
Единственное, что нужно обработать, - это дубликаты, это легко сделать. Как когда нам нужно сравнить с предыдущими, которые искали в словаре.
Решение делает упор на производительность.
Итак, вот - рабочее решение на Java, которое предложил Джейсон Коэн, и оно работает несколько лучше, чем то, которое использует trie. Ниже приведены некоторые из основных моментов:
Ниже приведен основной рекурсивный код, который находит набор ключей анаграммы:
// recursive function to find all the anagrams for charInventory characters
// starting with the word at dictionaryIndex in dictionary keyList
private Set<Set<String>> findAnagrams(int dictionaryIndex, char[] charInventory, List<String> keyList) {
// terminating condition if no words are found
if (dictionaryIndex >= keyList.size() || charInventory.length < minWordSize) {
return null;
}
String searchWord = keyList.get(dictionaryIndex);
char[] searchWordChars = searchWord.toCharArray();
// this is where you find the anagrams for whole word
if (AnagramSolverHelper.isEquivalent(searchWordChars, charInventory)) {
Set<Set<String>> anagramsSet = new HashSet<Set<String>>();
Set<String> anagramSet = new HashSet<String>();
anagramSet.add(searchWord);
anagramsSet.add(anagramSet);
return anagramsSet;
}
// this is where you find the anagrams with multiple words
if (AnagramSolverHelper.isSubset(searchWordChars, charInventory)) {
// update charInventory by removing the characters of the search
// word as it is subset of characters for the anagram search word
char[] newCharInventory = AnagramSolverHelper.setDifference(charInventory, searchWordChars);
if (newCharInventory.length >= minWordSize) {
Set<Set<String>> anagramsSet = new HashSet<Set<String>>();
for (int index = dictionaryIndex + 1; index < keyList.size(); index++) {
Set<Set<String>> searchWordAnagramsKeysSet = findAnagrams(index, newCharInventory, keyList);
if (searchWordAnagramsKeysSet != null) {
Set<Set<String>> mergedSets = mergeWordToSets(searchWord, searchWordAnagramsKeysSet);
anagramsSet.addAll(mergedSets);
}
}
return anagramsSet.isEmpty() ? null : anagramsSet;
}
}
// no anagrams found for current word
return null;
}
Вы можете форк репо от здесь и поиграться с ним. Есть много оптимизаций, которые я мог пропустить. Но код работает и находит все анаграммы.
И здесь - мое новое решение.
В книге Джона Бентли «Жемчужины программирования» содержится проблема поиска анаграмм слов. Заявление:
Given a dictionary of english words, find all sets of anagrams. For instance, “pots”, “stop” and “tops” are all anagrams of one another because each can be formed by permuting the letters of the others.
Я немного подумал, и мне пришло в голову, что решением было бы получить подпись слова, которое вы ищете, и сравнить его со всеми словами в словаре. Все анаграммы слова должны иметь одинаковую подпись. Но как этого добиться? Моя идея состояла в том, чтобы использовать фундаментальную теорему арифметики:
Основная теорема арифметики утверждает, что
every positive integer (except the number 1) can be represented in exactly one way apart from rearrangement as a product of one or more primes
Итак, идея состоит в том, чтобы использовать массив из первых 26 простых чисел. Затем для каждой буквы в слове мы получаем соответствующее простое число A = 2, B = 3, C = 5, D = 7… и затем мы вычисляем произведение нашего входного слова. Затем мы делаем это для каждого слова в словаре, и если слово соответствует нашему входному слову, мы добавляем его в результирующий список.
Спектакль более-менее приемлемый. Для словаря из 479828 слов на получение всех анаграмм уходит 160 мс. Это примерно 0,0003 мс / слово или 0,3 микросекунды / слово. Сложность алгоритма составляет O (mn) или ~ O (m), где m - размер словаря, а n - длина входного слова.
Вот код:
package com.vvirlan;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Scanner;
public class Words {
private int[] PRIMES = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73,
79, 83, 89, 97, 101, 103, 107, 109, 113 };
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
String word = "hello";
System.out.println("Please type a word:");
if (s.hasNext()) {
word = s.next();
}
Words w = new Words();
w.start(word);
}
private void start(String word) {
measureTime();
char[] letters = word.toUpperCase().toCharArray();
long searchProduct = calculateProduct(letters);
System.out.println(searchProduct);
try {
findByProduct(searchProduct);
} catch (Exception e) {
e.printStackTrace();
}
measureTime();
System.out.println(matchingWords);
System.out.println("Total time: " + time);
}
private List<String> matchingWords = new ArrayList<>();
private void findByProduct(long searchProduct) throws IOException {
File f = new File("/usr/share/dict/words");
FileReader fr = new FileReader(f);
BufferedReader br = new BufferedReader(fr);
String line = null;
while ((line = br.readLine()) != null) {
char[] letters = line.toUpperCase().toCharArray();
long p = calculateProduct(letters);
if (p == -1) {
continue;
}
if (p == searchProduct) {
matchingWords.add(line);
}
}
br.close();
}
private long calculateProduct(char[] letters) {
long result = 1L;
for (char c : letters) {
if (c < 65) {
return -1;
}
int pos = c - 65;
result *= PRIMES[pos];
}
return result;
}
private long time = 0L;
private void measureTime() {
long t = new Date().getTime();
if (time == 0L) {
time = t;
} else {
time = t - time;
}
}
}