iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0

讓我們來針對昨天留下的問題,來修改一下程式碼.把DeadEnd考慮進去,並且不要走回頭路.

fun BFSWithDeadEnds(deadEnds:Array<String>,target:String):Int{
    //記錄要略過的DeadEnd
    val deadMap = mutableSetOf<String>()
    for(element in deadEnds){
        deadMap.add(element)
    }
    //記錄走過的節點
    val visited = mutableSetOf<String>()

    val queue: Queue<String> = LinkedList()
    var step = 0
    queue.offer("0000")
    visited.add("0000")
    while(queue.isNotEmpty()){
        val size = queue.size
        println("queue size: $size")
        //將目前放在Queue中的所有節點往外擴散 向外尋找
        for(i in 0 until size){
            val currentCode = queue.poll()

            if(currentCode!=null) {
                if( deadMap.contains(currentCode)){//判斷是否在DeadEnd中
                    continue
                }
                if(currentCode.equals(target)){ //判斷是否到終點
                    return step
                }

                for(j in 0 ..3){
                    val up = plusOne(currentCode,j)
                    if(!visited.contains(up)){
                        queue.offer(up)
                        visited.add(up)
                    }
                    val down = minusOne(currentCode,j)

                    if(!visited.contains(down)){
                        queue.offer(down)
                        visited.add(down)
                    }
                }
            }
        }
        step++//在這裡增加一步
    }
    return -1
}

我們多增加了DeadEnd的Map來記錄哪些不能經過,又增加了一個visted來記錄哪些我們已經走過了,如果在DeadEnd中有的我們就跳過,在visted中有的我們下次就不會再走重複的路,而這兩種都是String來記錄的,所以可以把兩個Map合併起來,這部分就交給各位了.

BFS的基礎到這邊就結束了,但是我們可以聊聊一點進階的.雙向BFS

雙向BFS

基礎的BFS從起點開始,一路往外擴張路徑,一直到找到終點為止,從頭到尾終點都沒有移動或改變過.

讓我們換個角度,如果在我們從起點往外擴大的時候,終點也遵循相同模式往外擴大,當兩邊擴張的的節點碰在了一起,就是得到正確解答的時候.前半部的路徑,就是由起點到達目前相遇的節點的路徑,而後半段的路徑,就是由相遇節點到終點的路徑.雖然時間複雜度沒有改變,但是在實際上,所搜尋的路徑只有全部搜尋的一半.更加有效率.

不過,雙向BFS也是有前提的,就在”從終點往外擴大”,如果一開始不知道終點是哪一個的話,自然也不能從終點開始擴大,例如我們前幾天的例子,最矮的二元樹,由於不知道哪個葉節點才是最終解答,因此自然不能使用雙向BFS.

不過我們的密碼鎖問題,題目知道我們要尋找的終點,就能使用雙向BFS

fun BFSWith2Way(deadEnds: Array<String>, target: String): Int {
    //記錄要略過的DeadEnd
    val deadMap = mutableSetOf<String>()
    for (element in deadEnds) {
        deadMap.add(element)
    }
    //記錄走過的節點
    val visited = mutableSetOf<String>()

    var setFromStart = mutableSetOf<String>() //利用Set取代Queue 可以快速判斷是否存在某個元素
    var setFromEnd = mutableSetOf<String>()
    var step = 0
    setFromStart.add("0000") //定下起點
    setFromEnd.add(target) //定下終點
    visited.add("0000")
    while (setFromStart.isNotEmpty() && setFromEnd.isNotEmpty()) {
        //往外擴散的過程中,不能夠直接修改set內容 所以改用temp來儲存擴散結果
        val temp = mutableSetOf<String>()
        for(currentNode in setFromStart){
            if(deadMap.contains(currentNode)){
                continue
            }

            if(setFromEnd.contains(currentNode)){
                return step
            }
            visited.add(currentNode)

            for (j in 0..3) {
                val up = plusOne(currentNode, j)
                if (!visited.contains(up)) {
                    temp.add(up)
                }
                val down = minusOne(currentNode, j)
                if (!visited.contains(down)) {
                    temp.add(down)
                }
            }
        }
        step++//在這裡增加一步
        //這邊交換setFromStart setFromEnd 下一輪會從setFromEnd開始擴散
        //利用temp保存現在的setFromStart
        setFromStart = setFromEnd
        setFromEnd = temp
    }
    return -1
}

其實實際上還是那套BFS,只是不使用Queue而使用Set,就能更加快速判斷兩個集合是否有交集,有了交集就代表找到答案了.

另外一個小技巧就是在while最後交換兩個Set,這樣兩邊就能夠輪流擴散…

說到這個,其實還能小小優化一下,可以在一開始時做一個判斷兩個set的大小,然後擴散小的那個,這樣可以稍稍壓縮使用的空間,佔用的空間成長也會慢上一些,效率會高上一點點

不過不管是單向還是雙向,或是有沒有進行最佳化,從Big O的角度來看,所佔用的空間複雜度都是一樣.所以還是先熟悉BFS的基本就可以啦.


上一篇
Day 12 : BFS與密碼鎖問題
下一篇
Day 14 : 快慢雙指標的使用技巧
系列文
從零開始的LeetCode生活,使用Kotlin學習刷題30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言