Алгоритм генерации анаграмм - PullRequest
49 голосов
/ 11 сентября 2008

Какова была бы лучшая стратегия для генерации анаграмм.

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.
  • Одиннадцать плюс два - анаграмма Двенадцать плюс один
  • Десятичная точка является анаграммой Я на месте
  • Астрономы - анаграмма Лунных звезд

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

Я наткнулся на эту страницу, Решение анаграмм в Ruby .

Но каковы ваши идеи?

Ответы [ 14 ]

43 голосов
/ 18 декабря 2009

Большинство из этих ответов ужасно неэффективны и / или дают только одно слово решения (без пробелов). Мое решение будет обрабатывать любое количество слов и очень эффективно.

То, что вы хотите, это структура данных 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. Возможно, вы можете найти более короткий список слов, который содержит только более общие английские слова.

Кроме того, сокращение "I'm" (из одного из ваших примеров) не будет отображаться в результатах, если вы не добавите "im" в словарь и не установите MIN_WORD_SIZE в 2.

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

19 голосов
/ 11 сентября 2008

Для каждого слова в словаре сортируйте буквы по алфавиту. Таким образом, «foobar» становится «abfoor».

Затем, когда входит входная анаграмма, также сортируйте ее буквы, затем ищите. Это так же быстро, как поиск по хэш-таблице!

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

(см. Комментарии для дополнительной оптимизации и подробностей)

8 голосов
/ 11 сентября 2008

См. Это задание от факультета CSE Вашингтонского университета.

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

4 голосов
/ 11 сентября 2008

Предварительная обработка:

Создайте дерево с каждым листом как известным словом, введенным в алфавитном порядке.

Во время поиска:

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

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

Дополните эту процедуру напоминанием для более быстрого поиска по обычным остальным мультимножествам.

Это довольно быстро - каждый обратный ход гарантированно дает фактическое подслово, и каждый обход занимает линейное время по длине подслова (и, согласно стандартам кодирования, подслов обычно довольно малы) Однако, если вы действительно хотите что-то еще быстрее, вы можете включить все n-граммы в ваш предварительный процесс, где n-грамм - это любая строка из n слов в строке. Конечно, если W = #words, то вы перейдете с размера индекса O (W) на O (W ^ n). Может быть, n = 2 реалистично, в зависимости от размера вашего словаря.

3 голосов
/ 19 декабря 2016

И здесь - мое новое решение.

Книга Джона Бентли «Программирование жемчуга» содержит проблему поиска анаграмм слов. Утверждение:

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

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

Основная теорема об арифметике гласит, что

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

Таким образом, идея состоит в том, чтобы использовать массив первых 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;
        }
    }
}
3 голосов
/ 03 января 2013

Итак вот рабочее решение на Java, предложенное Джейсоном Коэном, и оно работает несколько лучше, чем использующее trie. Ниже приведены некоторые из основных пунктов:

  • Загружать словарь только теми словами, которые являются подмножествами данного набора слов
  • Словарь будет хэшем отсортированных слов в качестве ключа и набором фактических слов в качестве значений (согласно предложению Джейсона)
  • Перебирайте каждое слово из словарного ключа и выполняйте рекурсивный прямой поиск, чтобы увидеть, найдена ли какая-либо действительная анаграмма для этого ключа
  • Только ищите вперед, потому что анаграммы для всех слов, которые уже были пройдены, должны были быть уже найдены
  • Объединить все слова, связанные с ключами, например, если «enlist» - это слово, для которого должны быть найдены анаграммы, и одним из набора ключей для слияния являются [ins] и [elt], а фактические слова для ключей [ins] - [sin] и [ins], а для ключа [elt] есть [let], тогда последний набор слов слияния будет [sin, let] и [ins, let], который будет частью нашего окончательного списка анаграмм
  • Также отметим, что эта логика будет перечислять только уникальный набор слов, то есть «одиннадцать плюс два» и «два плюс одиннадцать» будут одинаковыми, и только один из них будет указан в выходных данных

Ниже приведен основной рекурсивный код, который находит набор ключей анаграммы:

// 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;
}

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

3 голосов
/ 28 декабря 2008

Одной из основополагающих работ по программным анаграммам был Майкл Мортон (Mr. Machine Tool), использующий инструмент под названием Ars Magna. Вот легкая статья на основе его работы.

2 голосов
/ 15 сентября 2008

Книга Programming Pearls , написанная Джоном Бентли, довольно хорошо описывает подобные вещи. Обязательно прочитайте.

1 голос
/ 11 октября 2011

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

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

Я предполагаю, что слово может содержать не более 100 символов (или более, но есть ограничение).

Таким образом, любое слово, которое мы принимаем за последовательность индексов, например, слово "привет", может быть представлено как "1234". Теперь анаграммы "1234" - "1243", "1242" .. и т. Д.

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

Чтобы проверить, действительны ли анаграммы, просто внесите их в словарь и подтвердите.

Единственное, что нужно обработать, это дубликаты. Это можно легко сделать. Например, когда нам нужно сравнить с предыдущими, которые были найдены в словаре.

Решение подчеркивает производительность.

1 голос
/ 28 декабря 2008

Некоторое время назад я написал сообщение в блоге о том, как быстро найти анаграммы из двух слов. Это работает очень быстро: поиск всех 44 двухсловных анаграмм для слова с текстовым файлом более 300 000 слов (4 мегабайта) занимает всего 0,6 секунды в программе на Ruby.

Алгоритм поиска анаграмм в два слова (в Ruby)

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...