你需要停下来,后退一大步。
第一步应该是提出方法的签名。问题陈述是什么?
找到所有可能的路径
未提及:从特定坐标开始。
所以该方法需要返回一个路径集:
static Set<Path> AllPaths(Coordinate start) { /* a miracle happens */ }
好的,现在我们到了某个地方;现在很清楚我们需要什么。我们需要一组东西,我们需要一条路径,我们需要坐标。
什么是坐标?一对整数:
struct Coordinate
{
public int X { get; }
public int Y { get; }
public Coordinate(int x, int y) : this()
{
this.X = x;
this.Y = y;
}
}
完成。所以弹出堆栈;什么是路径?路径可以是空的,也可以是第一步后跟路径:
sealed class Path
{
private Path() { }
private Path(Coordinate first, Path rest)
{
this.first = first;
this.rest = rest;
}
public static readonly Path Empty = new Path();
private Coordinate first;
private Path rest;
public bool IsEmpty => this == Empty;
public Coordinate First
{
get
{
if (this.IsEmpty) throw new Exception("empty!");
return first;
}
}
public Path Rest
{
get
{
if (this.IsEmpty) throw new Exception("empty!");
return rest;
}
}
public Path Append(Coordinate c) => new Path(c, this);
public IEnumerable<Coordinate> Coordinates()
{
var current = this;
while(!current.IsEmpty)
{
yield return current;
current = current.Rest;
}
}
}
完成。
现在你实现Set<T>。您将需要进行“所有项目”和“将此集合与另一个组合以产生第三个”的操作。确保集合是不可变的。当您向其中添加新项目时,您不想更改集合;你想要一个不同的集合。加1时不会将3变为4的方法相同; 3和4是不同的数字。
现在您拥有实际解决问题所需的所有工具;现在你可以实现了
static Set<Path> AllPaths(Coordinate start)
{
/* a miracle happens */
}
那么这是如何工作的呢?请记住,所有递归函数都具有相同的形式:
- 解决小问题
- 如果我们的情况不是很简单,请将问题简化为更小的情况,递归求解,然后组合解决方案。
那么微不足道的情况是什么?
static Set<Path> AllPaths(Coordinate start)
{
/* Trivial case: if the start coordinate is at the end already
then the set consists of one empty path. */
实现它。
什么是递归案例?
/* Recursive case: if we're not at the end then either we can go
right, go down, or both. Solve the problem recursively for
right and / or down, union the results together, and add the
current coordinate to the top of each path, and return the
resulting set. */
实现它。
这里的教训是:
- 列出问题中的所有名词:集合、路径、坐标等。
- 创建一个代表每一个的类型。保持简单,并确保准确实现每种类型所需的操作。
- 现在您已经为每个名词实现了一个抽象,您可以开始设计使用这些抽象的算法,并确信它们会起作用。
- 记住递归的基本规则:如果可以,请解决基本情况;如果不是,请解决较小的递归案例并组合解决方案。