在解决这类问题时,我倾向于使用不可变类,因为一切都更容易推理。以下实现使用了 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;
}
}
}
}
我们完成了。这是最优雅或最快的算法吗?可能不是,但我发现它是最容易理解的,因此也是可以维护的。希望对你有所帮助。
更新基于下面的 cmets,我运行了一些测试用例,其中之一是:
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