Как найти все возможные слова, используя соседние буквы в матрице
у меня есть следующая тестовая матрица:
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