Еще одна головоломка перестановочного слова... С Linq?

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

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

Например, учитывая буквы: P я G G Y Мне нужен массив всех возможных комбинаций этих букв, поэтому я могу проверить список слов (scrabble?) И в конечном итоге получить список всех возможных слов, которые вы можете сделать, используя эти буквы (от трех букв до общего числа, в этом случай 5 букв).

Ответ 1

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

Сначала найдите несколько списков слов (вы говорите, что собираетесь проверить список слов).

Вот хороший источник списков слов:

http://www.poslarchive.com/math/scrabble/lists/index.html

Далее, для каждого списка слов (например, для трех буквенных слов, 4 буквенных слов и т.д.), создайте словарь, ключ которого является буквами слова в алфавитном порядке и значением которого является слово. Например, учитывая следующий список слов:

ACT
CAT
ART
RAT
BAT
TAB

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

ABT - BAT, TAB
ACT - ACT, CAT
ART - ART, RAT, TAR

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

Далее, чтобы найти слова кандидата для заданного набора из N букв, сгенерируйте все возможные комбинации длины K для интересующих вас длин. Для scrabble это будет все комбинации (порядок не важен, поэтому CAT == ACT, поэтому все перестановки не требуются) из 2 (7 выберите 2), 3 (7 выберите 3), 4 (7 выберите 4), 5 (7 выберите 5), 6 (7 выберите 6), 7 букв (7 выберите 7). Это можно улучшить, сначала упорядочив N букв в алфавитном порядке, а затем найдя комбинации длины K.

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

Итак, для CAKE, закажите буквы:

ACEK

Получите 2, 3 и 4 комбинации букв:

AC
AE
AK
CE
CK
EK
ACE
CEK
ACEK

Теперь используйте эти ключи в словаре. Вы найдете ACE и CAKE кандидатами.

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

Например, данный:

TEA

Существует 6 подстановок (длины 3), но только 1 комбинация (длины 3). Таким образом, требуется только один поиск, используя ключ AET.

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

Я написал программу, которая многое делает, когда я впервые изучал С# и .NET. Я попытаюсь опубликовать некоторые фрагменты (улучшенные на основе того, что я узнал с тех пор).

Это расширение строки вернет новую строку, которая представляет символы строки ввода, собранные в алфавитном порядке:

public static string ToWordKey(this string s)
{
  return new string(s.ToCharArray().OrderBy(x => x).ToArray());
}

Основываясь на этом ответе на @Adam Hughes, вот метод расширения, который вернет все комбинации (n выбирает k, а не все перестановки) для всех длин (1 к string.Length) входной строки:

public static IEnumerable<string> Combinations(this String characters)
{
  //Return all combinations of 1, 2, 3, etc length
  for (int i = 1; i <= characters.Length; i++)
  {
    foreach (string s in CombinationsImpl(characters, i))
    {
      yield return s;
    }
  }
}

//Return all combinations (n choose k, not permutations) for a given length
private static IEnumerable<string> CombinationsImpl(String characters, int length)
{
  for (int i = 0; i < characters.Length; i++)
  {
    if (length == 1)
    {
      yield return characters.Substring(i,1);
    }
    else
    {
      foreach (string next in CombinationsImpl(characters.Substring(i + 1, characters.Length - (i + 1)), length - 1))
        yield return characters[i] + next;
    }
  }
}

Используя метод "InAlphabeticOrder", вы можете создать список ваших входных слов (scrabble dictionary), индексированных по их "ключу" (аналогично словарю, но многие слова могут иметь один и тот же ключ).

public class WordEntry
{
  public string Key { set; get; }
  public string Word { set; get; }

  public WordEntry(string w)
  {
    Word = w;
    Key = Word.ToWordKey();
  }
}

var wordList = ReadWordsFromFileIntoWordEntryClasses();

Учитывая список WordEntry, вы можете запросить список, используя linq, чтобы найти все слова, которые могут быть сделаны из заданного набора букв:

string lettersKey = letters.ToWordKey();

var words = from we in wordList where we.Key.Equals(lettersKey) select we.Word;

Вы можете найти все слова, которые могут быть сделаны из любой комбинации (любой длины) заданного набора букв:

string lettersKey = letters.ToWordKey();

var words = from we in wordList
            from key in lettersKey.Combinations()
            where we.Key.Equals(key)
            select we.Word;

[EDIT]

Вот еще пример кода:

Учитывая список из 2, 3 и 4 буквенных слов отсюда: http://www.poslarchive.com/math/scrabble/lists/common-234.html

Вот некоторый код, который будет читать эти слова (я вырезал их и вставлял их в txt файл) и создавал список объектов WordEntry:

private IEnumerable<WordEntry> GetWords()
{
  using (FileStream fs = new FileStream(@".\Words234.txt", FileMode.Open))
  using (StreamReader sr = new StreamReader(fs))
  {
    var words = sr.ReadToEnd().Split(new char[] { ' ', '\n' }, StringSplitOptions.RemoveEmptyEntries);
    var wordLookup = from w in words select new WordEntry(w, w.ToWordKey());
    return wordLookup;
  }
}

Я переименовал метод расширения InAlphateticalOrder в ToWordKey.

Ничего особенного здесь, просто прочитайте файл, разделите его на слова и создайте новый WordEntry для каждого слова. Возможно, здесь было бы более эффективно читать одну строку за раз. Список также будет довольно длинным, если вы рассмотрите 5, 6 и 7 буквенных слов. Это может быть проблемой, и это может быть не так. Для игрушки или игры это, вероятно, не имеет большого значения. Если вы хотите получить фантазию, вы можете подумать о создании небольшой базы данных со словами и ключами.

Учитывая набор букв, найдите все возможные слова той же длины, что и ключ:

  string key = "cat".ToWordKey();
  var candidates = from we in wordEntries 
                   where we.Key.Equals(key,StringComparison.OrdinalIgnoreCase) 
                   select we.Word;

Учитывая набор букв, найдите все возможные слова от длины 2 до длины (буквы)

string letters = "seat";

IEnumerable<string> allWords = Enumerable.Empty<string>();

//Get each combination so that the combination is in alphabetical order
foreach (string s in letters.ToWordKey().Combinations())
{
  //For this combination, find all entries with the same key
  var words = from we in wordEntries 
              where we.Key.Equals(s.ToWordKey(),StringComparison.OrdinalIgnoreCase) 
              select we.Word;
  allWords = allWords.Concat(words.ToList());
}

Этот код, вероятно, может быть лучше, но он выполняет свою работу. Одна вещь, которую он не делает, - это дублировать буквы. Если у вас есть "яйцо", две комбинации букв будут "например", "например" и "gg". Это можно легко зафиксировать, добавив вызов в Distinct в цикл foreach:

//Get each combination so that the combination is in alphabetical order
//Don't be fooled by words with duplicate letters...
foreach (string s in letters.ToWordKey().Combinations().Distinct())
{
  //For this combination, find all entries with the same key
  var words = from we in wordEntries 
              where we.Key.Equals(s.ToWordKey(),StringComparison.OrdinalIgnoreCase)
              select we.Word;
  //I forced the evaluation here because without ToList I was only capturing the LAST 
  //(longest) combinations of letters.
  allWords = allWords.Concat(words.ToList());
}

Это самый эффективный способ сделать это? Может быть, может и нет. Кто-то должен сделать работу, почему бы не LINQ?

Я думаю, что при таком подходе вам, вероятно, не нужен словарь списков (Dictionary<string,List<string>>).

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

Это должно помочь вам.

[Больше уточнений]

В терминах вашего первоначального вопроса вы принимаете в качестве входных данных "piggy", и вы хотите найти все возможные слова, которые могут быть сделаны из этих букв. Используя метод расширения комбинаций на "piggy", вы получите список, подобный этому:

p
i
g
g
y
pi
pg
pg
py
ig
ig
iy
gg
gy
gy
pig
pig
piy

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

Итак, мы можем получить список всех комбинаций букв из заданного набора букв. Мой алгоритм зависит от списка объектов WordEntry, которые можно найти на основе свойства Key. Свойство Key - это просто буквы слова, перестроенного в алфавитном порядке. Итак, если вы читаете файл слова, содержащий такие слова:

ACT
CAT
DOG
GOD
FAST
PIGGY

Список объектов WordEntry будет выглядеть так:

Word  Key

ACT   ACT
CAT   ACT
DOG   DGO
GOD   DGO
FAST  AFST
PIGGY GGIPY

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

Например, (предположим, что несколько слов выше образуют ваш весь словарь), если у вас были буквы "o" g "d" на вашем лотке для царапин, вы могли бы сформировать слова DOG и GOD, потому что оба имеют ключ DGO.

Учитывая набор букв, если мы хотим найти все возможные слова, которые могут быть сделаны из этих букв, мы должны иметь возможность генерировать все возможные комбинации букв. Мы можем протестировать каждую из них против "словаря" (цитаты, потому что это не ДЕЙСТВИТЕЛЬНО словарь в смысле .NET, это список (или последовательность) объектов WordEntry). Чтобы убедиться, что клавиши (из последовательности букв, которые мы "нарисовали" в scrabble), совместимы с полем Key в объекте WordEntry, мы должны сначала заказать буквы.

Скажем, у нас есть PIGGY на нашем подносе. Чтобы использовать предложенный алгоритм, мы хотим получить все возможные значения "ключа" от PIGGY. В нашем списке объектов WordEntry мы создали поле Key, упорядочив буквы Word в алфавитном порядке. Мы должны сделать то же самое с буквами на нашем лотке.

Итак, PIGGY становится GGIPY. (Это то, что делает ToWordKey). Теперь, учитывая буквы из нашего лотка в алфавитном порядке, мы можем использовать комбинации для создания всех возможных комбинаций (НЕ-перестановки). Каждая комбинация, которую мы можем найти в нашем списке на основе Key. Если комбинация из GGIPY соответствует значению ключа, то соответствующее слово (класса WordEntry) может быть построено из наших букв.

Лучший пример, чем PIGGY

SEAT

Первое использование ToWordKey:

AETS

Теперь сделайте все комбинации всех длин:

A
E
T
S
AE
AT
AS
ET
ES
TS
AET
ATS
ETS
AETS

Когда мы смотрим в нашем списке объектов WordEntry (сделанных из чтения в списке из 2, 3, 4 буквенных слов), мы, вероятно, обнаружим, что найдены следующие комбинации:

AT
AS
AET
ATS
ETS
AETS

Эти значения клавиш соответствуют следующим словам:

Key   Word
AT    AT
AS    AS
AET   ATE
AET   EAT
AET   TEA
AST   SAT
EST   SET
AEST  EATS
AEST  SEAT
AEST  TEAS

В последнем примере кода выше будут буквы ('s' 'e' 'a' 't'), конвертировать в формат ключа (ToWordKey) сгенерировать комбинации (комбинации), сохранить только уникальные возможные значения ключа (Distict - не проблема здесь, поскольку нет повторяющихся букв), а затем запросить список всех объектов WordEntry для тех объектов WordEntry, Ключ которых совпадает с одной из комбинаций.

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

Попробуйте использовать мой код в шагах.

Сначала используйте метод расширения комбинаций:

var combinations = "piggy".Combinations();

Распечатайте результат (p я g g y... pi pg pg... pig pig piy... pigg pigy iggy... и т.д.)

Далее, получите все комбинации после применения метода расширения ToWordKey:

//
// "piggy".ToWordKey() yields "iggpy"
//
var combinations = "piggy".ToWordKey().Combinations();

Распечатайте результат (i g g p y ig ig ip iy igg igp igy... и т.д.)

Устранить дубликаты с помощью метода Distinct():

var combinations = "piggy".ToWordKey().Combinations().Distinct();

Распечатайте результат (i g p y ig ip iy igg igp igy... и т.д.)

Используйте другие наборы букв, например, "съедайте" и "место".

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

Теперь представьте, что комбинации, которые мы только что сделали, являются ключевыми значениями, которые мы будем использовать для просмотра в нашем списке объектов WordEntry, сравнивая каждую комбинацию с ключом WordEntry.

Используйте функцию GetWords выше и ссылку на слова 2, 3, 4 буквы, чтобы создать список объектов WordEntry. Еще лучше сделайте очень урезанный список слов всего несколькими словами и распечатайте его (или посмотрите на него в отладчике). Посмотрите, как это выглядит. Посмотрите на каждое слово и каждый ключ. Теперь представьте, хотите ли вы найти ВСЕ слова, которые вы могли бы сделать с помощью "AET". Легче представить, используя все буквы, поэтому начните там. Есть 6 перестановок, но только 1 комбинация! Правильно, вам нужно только сделать один поиск в списке слов, чтобы найти все 3 буквенных слова, которые могут быть сделаны с этими буквами! Если бы у вас было 4 буквы, было бы 24 перестановки, но опять же, только 1 комбинация.

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

Надеюсь, что это поможет.

Ответ 2

Этот метод работает. Он использует как Linq, так и процедурный код.

IEnumerable<string> GetWords(string letters, int minLength, int maxLength)
{
    if (maxLength > letters.Length)
        maxLength = letters.Length;

    // Associate an id with each letter to handle duplicate letters
    var uniqueLetters = letters.Select((c, i) => new { Letter = c, Index = i });

    // Init with 1 zero-length word
    var words = new [] { uniqueLetters.Take(0) };

    for (int i = 1; i <= maxLength; i++)
    {
        // Append one unused letter to each "word" already generated
        words = (from w in words
                 from lt in uniqueLetters
                 where !w.Contains(lt)
                 select w.Concat(new[] { lt })).ToArray();

        if (i >= minLength)
        {
            foreach (var word in words)
            {
                // Rebuild the actual string from the sequence of unique letters
                yield return String.Join(
                    string.Empty,
                    word.Select(lt => lt.Letter));
            }
        }
    }
}

Ответ 3

Проблема поиска всех перестановок слова - это объем работы, который будет потрачен на вычисление абсолютной тарабарщины. генерируя все перестановки, является O (n!) и sooo, большая часть из которых будет абсолютно потрачена впустую. Вот почему рекомендую wageoghe answer.

Вот рекурсивная функция linq, которая возвращает все перестановки:

    public static IEnumerable<string> AllPermutations(this IEnumerable<char> s) {
        return s.SelectMany(x => {
            var index = Array.IndexOf(s.ToArray(),x);
            return s.Where((y,i) => i != index).AllPermutations()
                    .Select(y => new string(new [] {x}.Concat(y).ToArray()))
                    .Union(new [] {new string(new [] {x})});
        }).Distinct();
    }

Вы можете найти нужные слова:

"piggy".AllPermutations().Where(x => x.Length > 2)

Однако:

ПРЕДУПРЕЖДЕНИЕ: Я не люблю этот очень неэффективный ответ

Теперь наибольшее преимущество linq (для меня) - это то, насколько это читаемо. Сказав это, однако, , я не думаю, что намерение вышеуказанного кода ясное (и я его написал!). Таким образом, самое большое преимущество linq (для меня) отсутствует выше, и оно не так эффективно, как решение без linq. Я обычно прощаю отсутствие эффективности исполнения из-за эффективности, которую он добавляет для времени кодирования, удобочитаемости и простоты обслуживания, но я просто не думаю, что решение linq лучше всего подходит... квадратная привязка, круглая сортировка отверстий если хотите.

Кроме того, есть вопрос о сложности, о которой я говорил выше. Конечно, он может найти 153 буквы или более перестановок "piggy" за 0,2 секунды, но дайте ему слово "бухгалтерию", и вы будете ждать твердой 1 минуты 39 секунд, чтобы она найти все 435 574 три буквы или больше перестановок. Так почему я опубликовал такую ​​ужасную функцию? Сделать то, что wageoghe имеет правильный подход. Генерация всех перестановок просто недостаточно эффективна для решения этой проблемы.