Как найти все возможные слова, используя соседние буквы в матрице

у меня есть следующая тестовая матрица:

a l i
g t m
j e a

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

например:

минимум: 3 буквы

максимум: 6 букв

основываясь на тестовой матрице, я должен иметь следующее результаты:

  • Али
  • alm
  • alg
  • alt
  • ati
  • atm
  • atg
  • ...
  • atmea

etc.

Я создал тестовый код (C#), который имеет пользовательский класс, представляющий буквы.

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

здесь его код:

public class Letter
{
    public int X { get; set; }
    public int Y { get; set; }

    public char Character { get; set; }

    public List<Letter> Neighbors { get; set; }

    public Letter PreviousLetter { get; set; }

    public int Generation { get; set; }

    public Letter(char character)
    {
        Neighbors = new List<Letter>();
        Character = character;
    }

    public void SetGeneration(int generation)
    {
        foreach (var item in Neighbors)
        {
            item.Generation = generation;
        }
    }
}

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

к сожалению, следующий код создает первые 4 слова, потом останавливается. Неудивительно,что рекурсия остановлена на заданном уровне генерации.

основная проблема заключается в том, что рекурсия возвращает только один уровень, но было бы лучше вернуться к начальной точке.

 private static void GenerateWords(Letter input, int maxLength, StringBuilder sb)
    {
        if (input.Generation >= maxLength)
        {               
            if (sb.Length == maxLength)
            {
                allWords.Add(sb.ToString());
                sb.Remove(sb.Length - 1, 1);
            }                
            return;
        }
        sb.Append(input.Character);
        if (input.Neighbors.Count > 0)
        {
            foreach (var child in input.Neighbors)
            {
                if (input.PreviousLetter == child)
                    continue;
                child.PreviousLetter = input;
                child.Generation = input.Generation + 1;
                GenerateWords(child, maxLength, sb);
            }
        }
    }

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

2 ответов


отсюда, вы можете рассматривать это как проблему обхода графов. Вы начинаете с каждой заданной буквы, находя каждый путь длины min_size to аргумент max_size, С 3 и 6 в качестве этих значений в вашем примере. Я предлагаю рекурсивную процедуру, которая строит слова как пути через сетку. Это будет выглядеть примерно так: замените типы и псевдокод вашими предпочтениями.

<array_of_string> build_word(size, current_node) {
    if (size == 1)  return current_node.letter as an array_of_string;
    result = <empty array_of_string>
    for each next_node in current_node.neighbours {
        solution_list = build_word(size-1, next_node);
        for each word in solution_list {
             // add current_node.letter to front of that word.
             // add this new word to the result array
        }
    }
    return the result array_of_string
}

это двигает вас к решению?


при решении такого рода проблем, я предпочитаю использовать неизменяемые классы, потому что все намного проще. Следующая реализация использует ad hoc ImmutableStack потому что его довольно просто реализовать один. В производственном коде я, вероятно, хотел бы посмотреть System.Collections.Immutable для повышения производительности (visited будет ImmutableHashSet<> чтобы указать на очевидный случай).

так зачем мне нужен неизменяемый стек? Отслеживать течение путь персонажа и посещенные "локации" внутри Матрицы. Поскольку выбранный инструмент для задания неизменен, отправка его рекурсивных вызовов не является проблемой, мы знаем, что он не может измениться, поэтому мне не нужно беспокоиться о моих инвариантах на каждом уровне рекурсии.

Итак, давайте реализуем неизменяемый стек.

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

public class ImmutableStack<T>: IEnumerable<T>
{
    private readonly T head;
    private readonly ImmutableStack<T> tail;

    public static readonly ImmutableStack<T> Empty = new ImmutableStack<T>(default(T), null);
    public int Count => this == Empty ? 0 : tail.Count + 1;

    private ImmutableStack(T head, ImmutableStack<T> tail)
    {
        this.head = head;
        this.tail = tail;
    }

    public T Peek()
    {
        if (this == Empty)
            throw new InvalidOperationException("Can not peek an empty stack.");

        return head;
    }

    public ImmutableStack<T> Pop()
    {
        if (this == Empty)
            throw new InvalidOperationException("Can not pop an empty stack.");

        return tail;
    }

    public ImmutableStack<T> Push(T value) => new ImmutableStack<T>(value, this);

    public IEnumerator<T> GetEnumerator()
    {
        var current = this;

        while (current != Empty)
        {
            yield return current.head;
            current = current.tail;
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

struct Coordinates: IEquatable<Coordinates>
{
    public int Row { get; }
    public int Column { get; }

    public Coordinates(int row, int column)
    {
        Row = row;
        Column = column;
    }

    public bool Equals(Coordinates other) => Column == other.Column && Row == other.Row;
    public override bool Equals(object obj)
    {
        if (obj is Coordinates)
        {
            return Equals((Coordinates)obj);
        }

        return false;
    }

    public override int GetHashCode() => unchecked(27947 ^ Row ^ Column);

    public IEnumerable<Coordinates> GetNeighbors(int rows, int columns)
    {
        var increasedRow = Row + 1;
        var decreasedRow = Row - 1;
        var increasedColumn = Column + 1;
        var decreasedColumn = Column - 1;
        var canIncreaseRow = increasedRow < rows;
        var canIncreaseColumn = increasedColumn < columns;
        var canDecreaseRow = decreasedRow > -1;
        var canDecreaseColumn = decreasedColumn > -1;

        if (canDecreaseRow)
        {
            if (canDecreaseColumn)
            {
                yield return new Coordinates(decreasedRow, decreasedColumn);
            }

            yield return new Coordinates(decreasedRow, Column);

            if (canIncreaseColumn)
            {
                yield return new Coordinates(decreasedRow, increasedColumn);
            }
        }

        if (canIncreaseRow)
        {
            if (canDecreaseColumn)
            {
                yield return new Coordinates(increasedRow, decreasedColumn);
            }

            yield return new Coordinates(increasedRow, Column);

            if (canIncreaseColumn)
            {
                yield return new Coordinates(increasedRow, increasedColumn);
            }
        }

        if (canDecreaseColumn)
        {
            yield return new Coordinates(Row, decreasedColumn);
        }

        if (canIncreaseColumn)
        {
            yield return new Coordinates(Row, increasedColumn);
        }
    }
}

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

public static IEnumerable<string> GetWords(char[,] matrix,
                                           Coordinates startingPoint,
                                           int minimumLength,
                                           int maximumLength)

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

static IEnumerable<string> getWords(char[,] matrix,
                                    ImmutableStack<char> path,
                                    ImmutableStack<Coordinates> visited,
                                    Coordinates coordinates,
                                    int minimumLength,
                                    int maximumLength)

теперь остальное просто сантехника и подключение проводов:

public static IEnumerable<string> GetWords(char[,] matrix,
                                           Coordinates startingPoint,
                                           int minimumLength,
                                           int maximumLength)
    => getWords(matrix,
                ImmutableStack<char>.Empty,
                ImmutableStack<Coordinates>.Empty,
                startingPoint,
                minimumLength,
                maximumLength);


static IEnumerable<string> getWords(char[,] matrix,
                                    ImmutableStack<char> path,
                                    ImmutableStack<Coordinates> visited,
                                    Coordinates coordinates,
                                    int minimumLength,
                                    int maximumLength)
{
    var newPath = path.Push(matrix[coordinates.Row, coordinates.Column]);
    var newVisited = visited.Push(coordinates);

    if (newPath.Count > maximumLength)
    {
        yield break;
    }
    else if (newPath.Count >= minimumLength)
    {
        yield return new string(newPath.Reverse().ToArray());
    }

    foreach (Coordinates neighbor in coordinates.GetNeighbors(matrix.GetLength(0), matrix.GetLength(1)))
    {
        if (!visited.Contains(neighbor))
        {
            foreach (var word in getWords(matrix,
                                          newPath,
                                          newVisited,
                                          neighbor,
                                          minimumLength,
                                          maximumLength))
            {
                yield return word;
            }
        }
    }
}

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

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

var matrix = new[,] { {'a', 'l'},
                      {'g', 't'} };
var words = GetWords(matrix, new Coordinates(0,0), 2, 4);
Console.WriteLine(string.Join(Environment.NewLine, words.Select((w,i) => $"{i:00}: {w}")));

и результат ожидаемый:

00: ag
01: agl
02: aglt
03: agt
04: agtl
05: at
06: atl
07: atlg
08: atg
09: atgl
10: al
11: alg
12: algt
13: alt
14: altg