Swift中非常慢的扫雷递归算法

Posted

技术标签:

【中文标题】Swift中非常慢的扫雷递归算法【英文标题】:Very slow minesweeper recursive algorithm in Swift 【发布时间】:2016-11-19 00:11:33 【问题描述】:

我正在使用 Swift 3 和 Xcode。

我正在创建一个基本上是扫雷游戏的 ios 游戏,但没有正方形,只有六边形,所以每个六边形的周围最多可以有 6 个地雷。

我创建了一个递归算法,这样当玩家触摸一个六边形时,如果它不是炸弹,它就会调用一个名为“reveal”的递归函数: - 如果周围有一个或多个地雷并且被触摸的六边形仍然隐藏(隐藏的意思是我们不知道它是否是地雷),显示六边形并设置周围地雷的标签数量,并停止功能 - 如果周围没有地雷,对于附近每个隐藏的六边形,调用显示函数。

这就是我的代码的样子:

class Hexagon: SKShapeNode

    var mine: Bool
    var hide: Bool
    var proximityMines: Int

    init(mine: Bool = false, proximityMines: Int = 0, hide: Bool = true)
    
        self.mine = mine // if it's a mine
        self.proximityMines = proximityMines // number of surrounding mines (that I calculated using a function after I generated the level)
        self.hide = hide // if the hexagon is still hidden

        super.init()
    
    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    


func reveal(hexagon: Hexagon)

    if hexagon.proximityMines == 0 && hexagon.hide == true // if there are no mines in the surrounding
    
        hexagon.hide = false // we update the value of this hexagon
        setNumberLabel(hexagon: hexagon) // we set the .proximityMines number as a label (here 0)

        for proxyHexagon in proximityHexagons(hexagon: hexagon) // for each surrounding hexagon ...
        
            if proxyHexagon.hide == true // ... that is still hidden
            
                reveal(hexagon: proxyHexagon) // we call this function again
            
        
    
    else if hexagon.proximityMines != 0 && hexagon.hide == true // else if there are mines in the surrounding
    
        hexagon.hide = false // update
        setNumberLabel(hexagon: hexagon) // set label
    

proximityHexagons(hexagon: Hexagon) 函数返回一个数组,其中包含给定六边形的所有周围六边形。

所以我真的一次又一次地检查了我的算法,我真的认为这是一个很好的算法。

但事实是,当我创建一个 0 或我的数量非常少的关卡并单击一个六边形时,递归函数需要大约 2 秒来更新所有空的六边形。 我的地图包含或多或少的 260 个六边形,我调试了 reveal() 的调用次数,大约是相同的数量。

那么为什么要花这么多时间呢?我不认为 iPhone 6 不能处理这么多的操作!我在我的 iPhone 上试过,不是模拟器。 你有什么想法吗?

【问题讨论】:

你在每个周围的六边形上递归地调用它到每个周围的六边形。我不是专家,但我认为这是 O(N * N!) ?也许O(N ^ N)?而且您每次都在检查每个六边形。您需要保留一组被覆盖的六边形,每次发现一个六边形时您都需要移除这些六边形,并且只检查它们。 如果对于地图上的每个六边形只调用一次reveal(),那么您的算法似乎没问题。也许在同步呼叫或类似的情况下设置数字标签阻塞会导致一些与 UI 相关的减速?还要检查您的proximityHexagons() 函数需要多长时间。一个想法可能是编写一个 hiddenProximityHexagons() 函数,该函数仅返回 hide == true 的六边形。另外,检查您是从函数返回对象引用还是新创建的对象 @twiz_,不,我调用函数的时间与六边形的数量完全相同,所以问题不存在。 @samgak,你是对的,问题是proximityHexagons函数需要很多时间,我在回答中解释了,谢谢! 【参考方案1】:

好的,我一直在考虑这个问题,因为这听起来很有趣。我没有查找任何扫雷解决方案,所以我可能会在左侧领域出路,但这是我将如何解决您的问题。

首先,您必须给每个矿井一个索引,并且您需要知道该索引的模式,以便您可以做一些数学运算来获得每个矿井的周围索引。如果行具有相同的编号,并且跨行编号是连续的,则周围的索引是:

[index - 1, index + 1, 
index - rowCount, index - rowCount - 1, 
index + rowCount, index + rowCount + 1]

然后我会创建一个类,其中包含您在构建拼图时拥有的地图上的所有安全点。我称它为SafetyManager。

class SafetyManager 

var safeSpots: Set<Int> = all your safe spots

func indices(surrounding index: Int) -> Set<Int> 
    return [index - 1, index + 1, 
            index - rowCount, index - rowCount - 1, 
            index + rowCount, index + rowCount + 1]


func safePlaces(around hexagon: Int) -> Set<Int> 

    let allIndices = indices(surrounding: hexagon)
    let safe = allIndices.intersection(safeSpots)

    safeSpots.subtract(safe)

    return safe


它有两个重要的功能,一个计算周围的索引,第二个过滤安全点。我正在使用集合,因此我们可以快速确定安全点和周围点之间的交点。

接下来我们需要一个在移动时实例化的类,以便我们可以进行递归。让我们称之为 CheckManager。

class CheckManager 

var checked : [Int]
var unchecked : Set<Int>

init(firstHex: Hexagon, surroundingSafeSpots: Set<Int>) 
    checked = [firstHex.index]
    unchecked = surroundingSafeSpots



func nextUnchecked() -> Int? 
    guard !unchecked.isEmpty else  return nil 

    let next = unchecked.removeFirst()
    checked += [next]
    return next


func pleaseTake(these indices: Set<Int>) 
    unchecked.formUnion(indices)


你用你的第一个六边形或十六进制索引初始化它,以及安全管理器会给你的周围安全点,如果你没有从 SafetyManager 获得安全点,则无需实例化。 它保留了一组检查点和未检查点。两个重要的功能,第二个是你用来给它从安全管理器新获得的安全点添加到未经检查的列表中。另一个返回一个可选的 Int?下一个检查周围环境的安全地点。

然后进行递归,类似这样..

func check(spot: Hexagon) 

let safe = safetyMan.safePlaces(around: spot.index)
guard safe.count > 0 else  .. 

let checkMan = CheckManager(firstHex: spot, surroundingSafeSpots: safe)

while let i = checkMan.nextUnchecked() 
    let safeSpots = safetyMan.safePlaces(around: i)
    checkMan.pleaseTake(these: safeSpots)
 // goes until unchecked is empty

for spot in checkMan.checked 
   // get the hex and reveal 



您可以保留 [Int: Hexagon] 的字典,以快速获取给定索引的十六进制。我没有对此进行测试,所以我不确定它是否运作良好,或者根本没有或有一些不正确的语法。使用多线程也可能快得多。有趣的问题。祝你好运。

【讨论】:

【参考方案2】:

好的,我设法解决了我的问题。

问题在于 proximityHexagons 函数花费了很多时间。事实上,我每次调用这个函数,他都要进行6次复杂的计算,并将周围的六边形添加到一个数组中,所以很耗时。

这就是它的样子:

func proximityHexagons(hexagon: Hexagon) -> Array<Hexagon>

    var array = [Hexagon]()

    var nodeArray = [[Hexagon]]()
    nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x, y: hexagon.position.y + hexagon.height)).filter($0 is Hexagon) as! [Hexagon])
    nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x + hexagon.width * 3/4, y: hexagon.position.y + hexagon.height / 2)).filter($0 is Hexagon) as! [Hexagon])
    nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x + hexagon.width * 3/4, y: hexagon.position.y - hexagon.height / 2)).filter($0 is Hexagon) as! [Hexagon])
    nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x, y: hexagon.position.y - hexagon.height)).filter($0 is Hexagon) as! [Hexagon])
    nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x - hexagon.width * 3/4, y: hexagon.position.y - hexagon.height / 2)).filter($0 is Hexagon) as! [Hexagon])
    nodeArray.append(nodes(at: CGPoint(x: hexagon.position.x - hexagon.width * 3/4, y: hexagon.position.y + hexagon.height / 2)).filter($0 is Hexagon) as! [Hexagon])

// first, for each 6 directions, I'm adding in an array every nodes that are Hexagon, and then adding all of theses arrays in another bigger one

    for node in nodeArray // for each hexagon array in the big array
    
        if node.count != 0 // if there is an hexagon
        
             array.append(node.first!) // we set the hexagon in the final array
        
    

    return array // we return the array containing all surrounding hexagons

我更喜欢使用nodes(at: Point) 函数检查周围的六边形,因为我的关卡并不总是常规地图,它们的位置可能很奇怪,并且 twiz_ 的func indices(surrounding index: Int) 函数无法工作。 所以我保留了我的函数,但我在关卡开始时调用它一次,并将每个六边形的所有周围六边形存储在我的 hexagon 类中的一个新变量中:

class Hexagon: SKShapeNode

    var mine: Bool
    var hide: Bool
    var proximityMines: Int
    var proxyHexagons: [Hexagon] // here

    init(mine: Bool = false, proximityMines: Int = 0, hide: Bool = true, proxyHexagons: [Hexagon] =
        [Hexagon]())
    
        self.mine = mine
        self.proximityMines = proximityMines
        self.hide = hide
        self.proxyHexagons = proxyHexagons

        super.init()
    
    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

然后,在reveal函数中,我没有调用proximityHexagons函数,而是使用六边形的.proxyHexagons数组,如下所示:

func reveal(hexagon: Hexagon)
        
    if hexagon.proximityMines == 0 && hexagon.hide == true
    
        hexagon.hide = false
        setNumberLabel(hexagon: hexagon)
        for proxyHexagon in hexagon.proxyHexagons // here
        
            if proxyHexagon.hide == true
            
                reveal(hexagon: proxyHexagon)
            
        
    
    else if hexagon.proximityMines != 0 && hexagon.hide == true
    
        hexagon.hide = false
        setNumberLabel(hexagon: hexagon)
    

现在我的功能更快了,我设法在 0.001 秒内显示所有 260 个六边形,而不是旧的 2.81 秒。

【讨论】:

以上是关于Swift中非常慢的扫雷递归算法的主要内容,如果未能解决你的问题,请参考以下文章

大数据库中非常慢的 MongoDB 计数

MySQL中非常慢的子查询

Windows 10 中非常慢的 git clone

Swift递归回溯算法不起作用

Python算法——递归思想

面试常见算法