When solving such problems, I tend to use immutable classes, because everything is much easier to reason about. The next implementation uses ad hoc ImmutableStack
, because it is quite simple to implement. In production code, I would probably want to look into System.Collections.Immutable
for better performance ( visited
would be ImmutableHashSet<>
to indicate an obvious case).
So why do I need an immutable stack? To track the current character path and visit "locations" inside the matrix. Since the selected tool for the task is immutable, sending it in recursive calls is not a problem, we know that this cannot change, so I do not need to worry about my invariants at each level of recursion.
So, it allows you to implement an immutable stack.
We also implement the Coordinates
helper class, which encapsulates our "locations" inside the matrix, gives us the semantics of equality of values ββand a convenient way to get real neighbors of any given location. This will certainly come in handy.
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); } } }
Well, now we need a method that traverses the matrix, visiting each position after returning words that have a given minimum number of characters and do not exceed a specified maximum.
public static IEnumerable<string> GetWords(char[,] matrix, Coordinates startingPoint, int minimumLength, int maximumLength)
It looks right. Now that we are recursing, we need to track which characters we visited. It is easy to use our immutable stack, so our recursive method will look like this:
static IEnumerable<string> getWords(char[,] matrix, ImmutableStack<char> path, ImmutableStack<Coordinates> visited, Coordinates coordinates, int minimumLength, int maximumLength)
Now the rest is just laying and connecting the wires:
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; } } } }
And you're done. Is this the most elegant or fastest algorithm? Probably not, but I think this is the most understandable and, therefore, supported. Hope this helps you.
UPDATE Based on the comments below, I ran several test cases, one of which:
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}")));
And the result is expected:
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