Podczas rozwiązywania tego typu problemów używam niezmiennych klas, ponieważ wszystko jest o wiele łatwiejsze do zrozumienia. Następująca implementacja korzysta z ad hocImmutableStack
, ponieważ jest dość prosta w implementacji. W kodzie produkcyjnym prawdopodobnie chciałbym zaglądnąć do System.Collections.Immutable
, aby poprawić wydajność (visited
byłby to ImmutableHashSet<>
, aby wskazać oczywisty przypadek).
Dlaczego więc potrzebuję niezmiennego stosu? Aby śledzić bieżącą ścieżkę znaków i odwiedzane "lokalizacje" wewnątrz macierzy. Ponieważ wybrane narzędzie do pracy jest niezmienne, wysłanie go w dół wywołań rekursywnych nie ma sensu, wiemy, że nie może się zmienić, więc nie muszę się martwić o moje niezmienniki na każdym poziomie rekurencji.
Pozwala więc zaimplementować niezmienny stos.
Zaimplementujemy również klasę pomocniczą Coordinates
, która hermetyzuje nasze "lokalizacje" wewnątrz macierzy, da nam wartość semantyki równości i wygodny sposób na uzyskanie ważnych sąsiadów z dowolnej lokalizacji. Będzie oczywiście przydatny.
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);
}
}
}
Ok, teraz musimy metodę, która przemierza matrycę odwiedzając każdą pozycję po powracającego słowa, które mają określoną minimalną liczbę znaków i nie przekraczają określonego maksimum.
public static IEnumerable<string> GetWords(char[,] matrix,
Coordinates startingPoint,
int minimumLength,
int maximumLength)
To wygląda dobrze. Teraz, kiedy recursing musimy śledzić jakie znaki odwiedziliśmy, to łatwe przy użyciu naszą niezmienną stos, więc nasz rekurencyjna metoda będzie wyglądać następująco:
static IEnumerable<string> getWords(char[,] matrix,
ImmutableStack<char> path,
ImmutableStack<Coordinates> visited,
Coordinates coordinates,
int minimumLength,
int maximumLength)
teraz reszta jest po prostu hydraulicznego i podłączania przewodów:
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;
}
}
}
}
I skończymy. Czy jest to najbardziej elegancki lub najszybszy algorytm? Prawdopodobnie nie, ale uważam, że jest to najbardziej zrozumiałe i dlatego możliwe do utrzymania. Mam nadzieję, że ci to pomoże.
UPDATE oparciu komentarzach poniżej, Zabrakło mi kilka testów, z których jedna jest:
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}")));
a wynik jest oczekiwany:
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
Cóż, robi więcej niż posuwanie się naprzód :) Właściwie przedstawiłeś działające rozwiązanie ... Dodałem kilka sprawdzeń (nie używać tej samej litery, nie sprawdzam sąsiada rodzica itp.), ale teraz działa. Myślę, że powinienem wrócić do szkoły, aby nauczyć się teorii algorytmów jeszcze raz ... Dzięki za przypomnienie mnie :) – Nestor