Как получить каждую комбинацию букв, используя yield return и рекурсию?

У меня есть несколько списков таких строк из возможного списка из нескольких десятков:

1: { "A", "B", "C" }
2: { "1", "2", "3" }
3: { "D", "E", "F" }

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

25: { } // empty
 4: { "%", "!", "$", "@" }
16: { "I", "a", "b", "Y" }
 8: { ")", "z", "!", "8" }

Что я хочу сделать, так это получить все возможные комбинации строк, сохраняя при этом «порядок» списков. Другими словами, если мы смотрим на первый список, первой комбинацией будет A1D, затем A1E, затем A1F, затем B1D, затем B1E и так далее и тому подобное. До сих пор я написал этот рекурсивный алгоритм:

public void Tester()
{
    var 2dList = new List { list1, list2, list3 };
    var answer = ReturnString(2dList).ToList();

    answer.ForEach(Console.WriteLine);
}

public IEnumerable<string> ReturnString(List<List<string>> list)
{
    if (!list.Any())
    {
        yield return null;
    }
    else
    {
        // for each letter in the top-most list...
        foreach (var letter in list.First())
        {
            // get the remaining lists minus the first one
            var nextList = list.Where(x => x != list.First()).ToList();

            // get the letter and recurse down to find the next
            yield return letter + ReturnString(nextList);
        }
    }
}

Однако вместо этого я получаю следующее:

AStringGeneration.StringGenerator+<ReturnString>d__11
BStringGeneration.StringGenerator+<ReturnString>d__11
CStringGeneration.StringGenerator+<ReturnString>d__11

StringGeneration — это имя класса, в котором находится ReturnString. Когда я ставлю точку останова на строку yield return letter + ..., кажется, что она проходит через A, B и C, но на самом деле не повторяется. Я не уверен, что здесь происходит. Кто-нибудь может объяснить, что не так с моим алгоритмом?


person Daniel T.    schedule 31.07.2011    source источник
comment
list.Where(x =› x != list.First()).ToList(); возможно, его можно заменить на list.Skip(1)   -  person Grozz    schedule 31.07.2011
comment
@Grozz Спасибо! Я знал, что должен был быть более простой способ сделать это :).   -  person Daniel T.    schedule 31.07.2011


Ответы (3)


Вам нужно перечислить итератор:

foreach(string s in ReturnString(...)) {
    Console.WriteLine(s);
}

Это также относится к каждой итерации:

foreach(string tail in ReturnString(nextList))
    yield return letter + tail;

Кроме того, я подозреваю, что вы можете что-то сделать с SelectMany здесь.

person Marc Gravell    schedule 31.07.2011
comment
Упс, я забыл упомянуть, что вывод, который я получаю, - это именно то, что отображается при использовании вашего кода для перечисления результата из ReturnString (которое должно быть во множественном числе). Также интересно то, что при установке точки останова на строке yield return letter + ... кажется, что выполняется итерация через A, B и C, но на самом деле не происходит рекурсия на следующий уровень. - person Daniel T.; 31.07.2011
comment
Спасибо, вроде получилось! Теперь мне просто нужно выяснить, почему :). - person Daniel T.; 31.07.2011
comment
Меня интересуют любые возможные SelectMany решения этого вопроса. Я пытался понять, как это работает, но в настоящее время это выходит за рамки моего понимания, за исключением самых простых случаев. - person Daniel T.; 31.07.2011

from x in l1
from y in l2
from z in l3
select x + y + + z

Обновление:

Вот схема для произвольной версии. Я заполню детали позже.

private bool m_beforeStart;
private IList<IEnumerable<char>> m_lists;
private Stack<IEnumerator<char>> m_enumerators;

public bool MoveNext() {
    while (CurrentEnumerator != null && !CurrentEnumerator.MoveNext()) {
        RemoveLastChar(m_stringBuilder);
        PopEnumerator();
     }
     if (CurrentEnumerator == null && ! m_beforeStart) {
         return false;
     }
     m_beforeStart = false;
     while (PushEnumerator()) {
          if (!CurrenEnumerator.MoveNext()) {
              ClearEnumerators();
              return false;
          }
          m_stringBuilder.Append(
              m_currentEnumerator.Current
          );
      }
      return true;
}

public string Current {
    get {
        return m_stringBuilder.ToString();
    }
}

private IEnumerator<char> CurrentEnumerator {
    get {
        return m_enumerators.Count != 0 ? m_enumerators.Peek() : null;
    }
}

private void PopEnumerator() {
    if (m_enumerators.Count != 0) {
        m_enumerators.Pop();
    }
}

private bool PushEnumerator() {
    if (m_enumerators.Count == m_lists.Count) {
        return false;
    }
    m_enumerators.Push(m_lists[m_enumerators.Count].GetEnumerator());
}
person Scott Wisniewski    schedule 31.07.2011
comment
Это предполагает, что вы знаете глубину, хотя - person Marc Gravell; 31.07.2011
comment
Извините, я должен был упомянуть, что списки, из которых пользователь может выбирать, будут динамическими. - person Daniel T.; 31.07.2011
comment
Урожайность не лучший вариант для этого случая. Он плохо справляется с рекурсией. Однако вы можете сделать это с помощью ручной реализации IEnumerator. - person Scott Wisniewski; 31.07.2011
comment
Вы можете использовать yield, но вам понадобится стек для ведения собственных записей активации. - person Scott Wisniewski; 31.07.2011
comment
Кстати, у него есть список строк, а не символов. - person Grozz; 31.07.2011

public static IEnumerable<string> ReturnString(IEnumerable<IEnumerable<string>> matrix)
{
    if (matrix.Count() == 1)
        return matrix.First();

    return from letter in matrix.First()    // foreach letter in first list
           let tail = matrix.Skip(1)        // get tail lists
           let tailStrings = ReturnString(tail)   // recursively build lists of endings for each tail
           from ending in tailStrings       // foreach string in these tail endings
           select letter + ending;          // append letter from the first list to ending
}

вызовите как ReturnString(lst.Where(l => l.Any()), чтобы пропустить пустые последовательности.

person Grozz    schedule 31.07.2011
comment
Ваш код отлично работает и выдает тот же результат, что и мой обновленный код. Однако мне трудно понять, как это на самом деле работает, и если меня попросят переписать его, я не смогу этого сделать. - person Daniel T.; 31.07.2011
comment
Вместо return matrix.First() он мог заменить matrix.Count() == 1 на matrix.Count() == 0 и return new List<string>(). - person Daniel T.; 31.07.2011
comment
исправил его для обработки пустых списков, это немного сложнее, чем это - person Grozz; 31.07.2011
comment
Кстати, подача matrix.SkipWhile(lst =› !lst.Any()) на первый вход была бы более понятной и эффективной, чем в функции - person Grozz; 31.07.2011
comment
К вашему сведению: конкатенация строк добавляет дополнительный член O (N ^ 2) к сложности времени выполнения. Вместо этого вы можете рассмотреть возможность использования связанного списка, а затем иметь функцию-оболочку, которая принимает IEnumerable‹LinkedList‹char›› и сворачивает его в IEnumerable‹string›. - person Scott Wisniewski; 31.07.2011
comment
Да. Проблема в том, что для объединения строк требуется создание новой строки. Итак, если вы это сделаете: string s = ; for (int i = 0; i ‹ n; ++i) { s = s + , + i.ToString(); } Вы будете строить строки sum(i=1, n) или n*(n+1)/2. Если вы хотите эффективно конкатенировать строки, вам следует использовать построитель строк. - person Scott Wisniewski; 31.07.2011
comment
Я очень сомневаюсь, что в данном конкретном случае new StringBuilder(letter).Append(ending).ToString() даст лучший результат, чем letter + ending, но это легко проверить. - person Grozz; 31.07.2011
comment
Кстати, проблема требует эффективного решения динамического программирования, но я в этом не силен. - person Grozz; 31.07.2011
comment
Я не имею в виду замену каждой строки + на построитель строк. Я имею в виду создание связанного списка (совместно используемого, такого как InmutableList в библиотеке дополнений bcl), а затем выполнить окончательный concat (используя построитель строк) в обертке над окончательным IEnumerable. Проблема с вашим решением (как есть) заключается в том, что вы выполняете конкат строк на каждом уровне рекурсии. - person Scott Wisniewski; 31.07.2011
comment
@Grozz позвольте нам продолжить это обсуждение в чате - person Scott Wisniewski; 01.08.2011