递归 + 900 个元素 + 邻居检查 = 导致堆栈溢出

Posted

技术标签:

【中文标题】递归 + 900 个元素 + 邻居检查 = 导致堆栈溢出【英文标题】:recursive + 900 elements + neighbor check = causes *** 【发布时间】:2011-03-22 04:26:32 【问题描述】:

我有一个城市模拟游戏,并试图找到一种方法来检查我们电力系统的流量。 基础: 城市的地图基于瓷砖(30 x 30 瓷砖 = 900 瓷砖)。 现在我从一家发电厂开始,进行递归邻居检查(上、左、右、下),以检查是否有东西可以传输电力。如果有什么东西,我也会开始检查这些瓷砖是否有邻居。 为了防止双重检查和/或无限递归调用,我用处理过的图块填充 ArrayList 并检查是否已经处理了新图块并将其添加到 ArrayList...

递归启动:

public void updatePowerEnvironment(int id, ArrayList<Integer> elements) 
    Log.w("GT", "update env for id: " + id);
    int newId = id - GameMap.mMapSize;
    if (newId >= 0 && GameMap.mMapCells.get(newId).mPowerEnabled
            && !elements.contains(newId)) 
        elements.add(newId);
        updatePowerEnvironment(newId, elements);
    
    newId = id + GameMap.mMapSize;
    if (newId < GameMap.mMapCells.size() && GameMap.mMapCells.get(newId).mPowerEnabled
            && !elements.contains(newId)) 
        elements.add(newId);
        updatePowerEnvironment(newId, elements);
    
    newId = id - 1;
    if (newId >= 0 && GameMap.mMapCells.get(newId).mPowerEnabled
            && !elements.contains(newId)) 
        elements.add(newId);
        updatePowerEnvironment(newId, elements);
    
    newId = id + 1;
    if (newId < GameMap.mMapCells.size()
            && GameMap.mMapCells.get(newId).mPowerEnabled
            && !elements.contains(newId)) 
        elements.add(newId);
        updatePowerEnvironment(newId, elements);
    

如果我可以信任日志输出,则不会尝试处理两次磁贴。这意味着,我在递归调用中没有错误。这也意味着,堆栈太小了。

有人知道如何避免堆栈限制吗?

[根据 Erics 的回答更新和我的代码]

public void updatePowerEnvironment(int id, ArrayList<Integer> elements) 
    Stack<Integer> toProcess = new Stack<Integer>();
    toProcess.push(id);
    int mapSize = GameMap.mMapCells.size();
    while (!toProcess.empty()) 
        id = toProcess.pop();
        Log.e("GT", "id to process: " + id);
        if (elements.contains(id)) 
            continue;
        
        int[] neighborIds = computeNeighbors(id);
        for (int neighbor : neighborIds) 
            if (neighbor < 0 || neighbor >= mapSize) 
                continue;
            
            if (!GameMap.mMapCells.get(neighbor).mPowerEnabled) 
                continue;
            
            toProcess.push(neighbor);
        
        elements.add(id);
    


private int[] computeNeighbors(int id) 
    return new int[] id + GameMap.mMapSize, id - GameMap.mMapSize, id + 1, id - 1;

【问题讨论】:

我在这里没有看到基本情况... @NullUserException:基本情况是“如果在任何方向上都没有未处理的通电图块,则什么也不做”。你看不到它,因为实现“什么都不做”不需要代码。 FWIW,您可以在创建线程时显式设置线程堆栈的大小(请参阅线程构造函数)。正如其他人所指出的那样,这不是正确的解决方案,但我想为了完整起见我会提到它。 @Fadden,这是真的,但在文档中还提到给定的大小可能会被完全忽略。所以它不仅不正确,它根本没有解决方案,但 Eric 给了我正确的解决方案。 【参考方案1】:

如果我正确理解您的问题,您正在尝试计算两个图块之间“由”关系的传递闭包。当然可以非递归地计算传递闭包。

这是一个计算 C# 中关系的传递闭包的非递归算法。您应该能够使其适应您选择的语言。

http://blogs.msdn.com/b/ericlippert/archive/2010/02/08/making-the-code-read-like-the-spec.aspx

请注意,我在这里所做的基本上是通过在堆上分配我自己的堆栈来避免堆栈限制。那东西可以长到你喜欢的大小。 (如果堆内存用完了,问题就更大了!)

另请注意,明智的做法是选择一种使“是成员?”的数据结构。谓极便宜。一个大小为 n 的数组列表通常是 O(n) 来回答“这个元素是这个集合的成员吗?”这个问题。这意味着您的算法总体上是 O(n^2) 。您可以使用具有 O(1) 包含测试的集合或哈希表之类的集合吗?

此外,在纯粹的“代码质量”级别上,这种方法可能需要一些工作。有这么多重复代码的事实是一个危险信号。我会倾向于像这个草图那样写这个方法:

Set<int> PoweredTiles(int powersource)

    Set<int> result = an empy set;
    Stack<int> stack = an empty stack;
    stack.Push(powersource);
    while (stack is not empty)
    
        int current = stack.Pop();
        if (result.Contains(current)) continue;
        result.Add(current);
        int[] neighbours =  compute the neighbours 
        foreach(int neighbour in neighbours)
        
            if (neighbour is not in range of grid) continue;
            if (neighbour is not a power carrier) continue;
            stack.Push(neighbour);
        
    
    return result;

简明扼要,不递归,无重复代码,O(n)。

【讨论】:

我不得不承认我对复杂性理论不是很熟悉,但是您的代码可以解决问题!我用我生成的代码更新了问题。 @WarrenFaith:很高兴你喜欢它。关于复杂性的一点是,当您问“这个包含 n 个元素的数组列表是否包含 x 时?”它回答这个问题的方式是“第一项是 x 吗?不。第二项是 x 吗?不……是第 n 项 x 吗?不。好吧,它不在列表中”。如果该列表中有 400 个项目,那么您每次循环都进行 400 次比较。数组列表经过优化,以慢搜索为代价在末尾快速插入。您可能会考虑使用一些针对快速搜索而优化的“集合”数据类型,因为您所做的大部分工作都是搜索 hm 比真的。感谢您的建议!它总是被遗忘的明显) @WarrenFaith:作为一名程序员,您可能应该花一些时间了解复杂性和大 O 表示法。 你首先应该问“不熟悉”是什么意思:)【参考方案2】:

您只需要将递归实现转换为迭代实现(理论告诉我们总是可能的)。

例如,您可以:

维护一个待检查单元的队列 当此队列不为空时,处理前面的元素 要处理一个单元格,对单元格本身执行任何您必须执行的操作,然后对它的四个相邻单元执行任何操作 如果它们尚未在队列中,则将它们添加到队列中 重复直到队列为空

【讨论】:

一个小问题是,如果您使用的是 immutable 数据结构,那么使用 stack 几乎总是比使用 更有效排队。结果是一样的,只是处理的顺序不同。 @Eric 您指的是 集合中的对象 是不可变的,还是指 FP 风格的不可变 容器,例如您在博客中提到的最近? 我指的是容器。不可变堆栈比不可变队列便宜。【参考方案3】:

如果您在执行递归之前清除了标志(我假设您只是在图块是否有电源上设置标志),那么高效的递归算法应该可以工作。像这样的:

void updateCell(position)

  for each direction (north, south, east, west) do the following:
  -- is there a cell there? (test for edges), if not, exit now;
  -- can it be powered? 
     false: exit now;
     true: set powered=true, call updateCell(this position);


void updatePowerGrid(start)

  clearPowerFlags();
  set powered=true for start;
  updateCell(start);

在您使用非常大的网格尺寸之前,这应该足够好。

【讨论】:

【参考方案4】:

你可以让它迭代。有两个列表,一个跟踪您去过的地方,另一个跟踪您当前检查的位置。

使用您的代码的伪代码:

While(ToBeChecked is not empty) 
    //Note In python i'd be using a copy of the list so I could edit it without 
    //concequence during the iteration. ie for a in b[:]
    for each element in ToBeChecked
         updatePowerEnvironment(...);
         //Remove element you are checking        
         removeElementFromToBeChecked(...);


public void updatePowerEnvironment(int id, ArrayList<Integer> elements) 
    Log.w("GT", "update env for id: " + id);
    int newId = id - GameMap.mMapSize;
    if (newId >= 0 && GameMap.mMapCells.get(newId).mPowerEnabled
            && !elements.contains(newId)) 
        elements.add(newId);
        //call addElementToBeChecked instead and I beleive the above already 
        //makes sure it has not already been checked
        addElementToBeChecked(newId, elements);
    
    newId = id + GameMap.mMapSize;
    if (newId < GameMap.mMapCells.size() && GameMap.mMapCells.get(newId).mPowerEnabled
            && !elements.contains(newId)) 
        elements.add(newId);
        addElementToBeChecked(newId, elements);
    
    newId = id - 1;
    if (newId >= 0 && GameMap.mMapCells.get(newId).mPowerEnabled
            && !elements.contains(newId)) 
        elements.add(newId);
        addElementToBeChecked(newId, elements);
    
    newId = id + 1;
    if (newId < GameMap.mMapCells.size()
            && GameMap.mMapCells.get(newId).mPowerEnabled
            && !elements.contains(newId)) 
        elements.add(newId);
        addElementToBeChecked(newId, elements);
    


addElementToBeChecked(...) 
    ToBeChecked.add();
    //Some other stuff if needed

removeElemenToBeChecked(...) 
    ToBeChecked.remove();
    //Some other stuff if needed

【讨论】:

【参考方案5】:

我会尝试的第一件事就是将搜索顺序从北-西南-东更改为东北-西南。像这样:

public void updatePowerEnvironment(int id, ArrayList<Integer> elements)  
    if (!GameMap.ValidCellId(id))
        return;
    if (!GameMap.mMapCells.get(id).mPowerEnabled)
        return;
    if (elements.Contains(id))
        return;
    elements.Add(id);
    updatePowerEnvironment(id - GameMap.mMapSize, elements);
    updatePowerEnvironment(id + 1, elements);
    updatePowerEnvironment(id + GameMap.mMapSize, elements);
    updatePowerEnvironment(id - 1, elements);

这可能会减少递归深度,具体取决于所涉及的地图。

【讨论】:

最小可能的递归深度是图形形状的函数,而不是递归完成的顺序。可能的最小值等于到距离电源最远的图块的最短路径的长度。在 30x30 的网格上,我们可以轻松构建一个子集,它是一个 450 个节点长的单路径,因此我们需要能够处理至少 450 个递归,对于 nxn 图,我们需要能够处理 n^2 / 2 个递归.递归解决方案根本无法扩展。我们需要在这里找到一个非递归的解决方案。 @Eric Lippert - 是的,你当然是对的。当我写这个答案时,我想象它们的处理顺序可能会对由一个大的连续网格区域组成的图形产生影响,但我没有考虑到它对深度优先搜索没有影响的事实。螺旋和扫掠一样糟糕。我还认为,旨在创建最长路径的病态配置不是问题,但是,当然,在每个人都会尝试的游戏中!

以上是关于递归 + 900 个元素 + 邻居检查 = 导致堆栈溢出的主要内容,如果未能解决你的问题,请参考以下文章

如何为邻居访问优化 OpenCL 代码?

使用递归进行 3D 数组操作——导致 ***(不是无限的!)

树,树的遍历和堆排序

具有递归数据结构的 Boost 序列化最终导致堆栈溢出

在 django 中,我想在已经排序的查询集中获取特定元素及其几个邻居

递归和栈溢出。