【问题标题】:Why is my recursive pathfinding algorithm not behaving correctly?为什么我的递归寻路算法行为不正确?
【发布时间】:2020-10-24 23:01:27
【问题描述】:

对于一个项目,我在程序上生成了一系列坐标为 (0,0) --> (x, y) 的“图块”,其中 (x,y) 是图块数组的最大宽度和高度。然后这个瓦片阵列随机填充正方形,并给出一个入口和多个出口点。我正在尝试创建一个算法,给定一个起点,检查是否可以到达所有出口点。为此,每个图块都包含有关其 NESW 相邻图块的数据,并调用一个递归函数来检查当前图块是否有出口,然后移动到北图块,然后对东、西、南执行相同操作。

这是代码的(伪)示例,除了检查退出之外,它访问所有可用的图块并计算它们(或它的意图):

public class Algo
{
    public int Count(Tile tile)
    {
        tile.Visited = true;
        foreach (direction in NESW)
        {
            if(!tile.direction.IsPopulated & !tile.direction.Visited)
            {
                 return Count(tile.direction) + 1;
            }
        }
        return 0;
     }
}

IsPopulated 是一个布尔值,如果图块填充有正方形,则返回 true;Visted 是布尔值,如果图块已被访问,则返回 true。瓦片空间周围有一个正方形的周边,确保算法保持在瓦片空间的范围内

对于这样的平铺布局:

arrows indicated start, direction, and finishing point of the algorithm (the darker bits are populated tiles)

算法返回 17 而不是预期的 21。

似乎,当算法到达无法向任何方向移动的图块时,函数返回计数,而不是返回调用堆栈并再次尝试。

我尝试过的另一个版本按预期工作,但是它没有成效,并且在方法之外改变了类的成员变量。

public class Algo2
{
    count = 0;
    public void Count(Tile tile)
    {
        tile.Visited = true;
        count += 1;
        foreach (direction in NESW)
        {
            if(!tile.direction.IsPopulated & !tile.direction.Visited)
            {
                 return Count(tile.direction);
            }
        }
     }
}

这将返回值 21 并按预期工作。

为什么有副作用的非结果递归函数可以工作,而没有副作用的有结果函数却不行?

【问题讨论】:

  • 那只是返回1,访问顺序和之前一样——按照图片上的顺序访问17个tile
  • 是的,抱歉,只需将返回 0 更改为返回 1,因为在第一种方法中,您不计算“叶子”
  • 这只是返回一个额外的 1 - 所以 18。我认为问题在于 for 循环中发生的递归,而不是递归是如何停止的。
  • 您能提供一些我们可以尝试测试的数据吗?我仍然认为 return 1 应该与您的第二种方法等效
  • 您想要什么样的数据? return 0 仅用于结束函数。更改该值会以任意数量添加到返回值,而没有附加逻辑来说明这样做的原因

标签: c# algorithm recursion


【解决方案1】:

寻找路径涉及回溯。即,您尝试找到一条路径。如果你达到目标,你就会停下来。如果您到达死胡同或已经访问过的路径,则清除 Visited 标志并返回,即您将控制权交还给嵌套较少的递归级别。这个级别然后尝试其他方向。

类似这样的:

bool FindPath(Tile tile)
{
    tile.Visited = true;
    if (tile.IsTarget)
    {
        return true;
    }
    foreach (direction in NESW)
    {
        var nextTile = tile.direction;
        if(!nextTile.IsPopulated && !nextTile.Visited && FindPath(nextTile))
             return true;
        }        
    }
    // Back tracking
    tile.Visited = false;
    return false;
}

如果对FindPath 的初始调用返回true,则目标路径将用Visited = true 瓦片标记。


另一种选择是查找到目标(或多个目标)的所有可能路径。为此,您将输出当前找到的路径,然后继续。

List<Tile> _path = new List<Tile>();

void FindAllPaths(Tile tile)
{
    tile.Visited = true;
    _path.Add(tile);
    if (tile.IsTarget)
    {
        OutputPath(_path);
        return; // If you want to go past a target to
                // find other targets, remove this `return`.
    }
    foreach (direction in NESW)
    {
        var nextTile = tile.direction;
        if(!nextTile.IsPopulated && !nextTile.Visited)
        {
             FindPath(nextTile);
        }        
    }
    // Back tracking
    tile.Visited = false;
    _path.Remove(tile);
}

除了在OutputPath 方法中使用带有路径图块的列表之外,您还可以遍历所有图块并检查Visited 标志。对于大型迷宫,使用列表可能更有效。


这两种算法并非旨在计算所有未填充的图块。但是您可以计算路径图块给您路径长度。如果您使用列表,这是列表计数,否则您也将回溯计数。即,tile.Visited = true; count++; 开头,tile.Visited = false; count--; 结尾。

【讨论】:

  • 感谢您的回复。我应该澄清几件事。我了解寻路算法的回溯性质,这正是我的问题中的第二个递归函数的工作原理 - 正如您所说,控制权被交还给堆栈上的第一个图块,方向留在 for 循环中。我的问题应该是:为什么具有副作用的递归函数有效,而下面的功能对应物无效?
  • 它们不是等价的。第二个始终递增,而第一个仅在 4 个方向中至少一个方向空闲时才递增。 foreach 循环可能会终止,而不会遇到加 1 的内部返回。
  • 它们中的任何一个增加的唯一方法是在一个方向上一个瓷砖是空闲的
  • 抱歉,如果没有其他可用的图块,第一个图块只会增加 1。但是函数去另一个图块的唯一方法是让另一个图块空闲,否则函数终止
  • 如果没有问题,第一个不计算当前瓦片(始终是费用)。第二个总是在进行任何测试之前增加类字段count。我认为第一个算法的第二个返回应该是return 1; 总是计算当前的瓦片,它总是免费的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-12
  • 1970-01-01
  • 1970-01-01
  • 2019-10-12
相关资源
最近更新 更多