Python演算法之動態規劃(Dynamic Programming)解析:二維矩陣中的醉漢(魔改版leetcode出界的路徑數)

語言: CN / TW / HK

原文轉載自「劉悅的技術部落格」v3u.cn/a_id_168

現在很多網際網路企業學聰明瞭,知道應聘者有目的性的刷Leetcode原題,用來應付演算法題面試,所以開始對這些題進行“魔改”,比如北京某電商平臺的這道題:

有一個正方形的島,使用二維方形矩陣表示,島上有一個醉漢,每一步可以往上下左右四個方向之一移動一格,如果超出矩陣範圍他就死了,假設每一步的方向都是隨機的(因為他是醉的),請計算n步以後他還活著的概率。

例如:輸入矩陣大小2*2,起點(0,0),隨機走出一步 n = 1  
  
輸出0.5  也就是有一半的機率還活著  
  
例如:輸入矩陣大小3*3,起點(1,1),隨機走出一步 n = 1  
  
輸出1  也就是百分之百還活著
複製程式碼

乍一看有點懵,但是提取關鍵字:二維矩陣、上下左右四個方向、矩陣範圍、n步,有沒有感到很熟悉?刷過Leetcode的同學一定已經聯想到了Leetcode原題第576題:出界的路徑數,難度等級為中等。

給定一個 m × n 的網格和一個球。球的起始座標為(i,j),你可以將球移到相鄰的單元格內,或者往上、下、左、右四個方向上移動使球穿過網格邊界。但是,你最多可以移動N次。找出可以將球移出邊界的路徑數量。答案可能非常大,返回 結果 mod 109+ 7 的值。

和魔改版的題聯絡起來,所謂醉漢“死了”,其實就是移出邊界,而每走一步都會有四種可能,所以所謂的“存活率”也就是當我們算出移出邊界的路徑數量之後,再除以方向的基數4,就可以算出“存活率”,相反也可以推算“死亡率”,歸根結底,魔改版題的題眼還是算出移出邊界的路徑數,並不是最後問的“存活率”問題,這題只是用了一個並不是很講究的障眼法,很有可能是該電商平臺老闆讓手下的某個研發出道演算法題招人用,而該研發已經被需求搞的暈頭轉向,無奈之下隨便從leetcode複製了一道出來,隨便改了改。

至於解法,下意識想到並且非常好理解的解法就是利用BFS(Breadth First Search 廣度優先),因為醉漢最多隻能移動N次,我們只要bfs依次遍歷如果發現出界,就代表死亡,進行累加1,當bfs的深度大於N的時候break結束。理論上是沒有任何問題。

import collections  
def how_likely_alive(m,n,N,i,j):  
    mod = 10**9 + 7  
    Q = collections.deque([(i,j,0)])  
    res = 0  
    while Q:  
        x,y,step = Q.popleft()  
        if step > N: break  
        if 0<=x<m and 0<=y<n:  
            Q.append((x+1,y,step+1))  
            Q.append((x-1,y,step+1))  
            Q.append((x,y+1,step+1))  
            Q.append((x,y-1,step+1))  
        else:  
            res += 1  
    num = res % mod  
    if num == 0:  
        return 1  
    else:  
        return num / 4  
  
  
print(how_likely_alive(2,2,1,0,0))
複製程式碼

一般情況下,如果該崗位的技術要求並不高,使用bfs基本就算過關了,但是如果面試官想來一次壓力面試(所謂壓力面試就是想探探你的底),看看你的極限在哪裡,就會要求你用效率更高的演算法來解題。(這裡需要簡單分辨一下壓力面試還是故意刁難,壓力面試如果不會的話,禮貌詢問就能拿到答案,而如果連面試官都不知道面試的答案,那肯定就是故意刁難了,也就沒有面下去的必要了)。

我們再回到題目中想一想,魔改版題目並沒有定義醉後隨機走的步數N的範圍,假設N的取值範圍達到了50,我們對任意一個座標點bfs有四個方向進行遍歷,同時考慮往回走的可能性,那麼複雜度達到了N的四倍,這個效率顯然不會令人滿意,所以當N相對小的情況下,比如只走1步,bfs是最優解,而範圍過大就需要考慮dp了。

dp(Dynamic Programming)演算法即是業界大名鼎鼎的動態規劃演算法了,其核心思路是把一個複雜的大問題拆成若干個子問題,通過解決子問題來逐步解決大問題,是不是和分治法有點像?關於分治演算法可以參考這篇文章:當我們談論演算法我們在談論什麼:由疫情核酸檢測想到的分治演算法(Divide-and-Conquer),但是和分治法有區別的地方是,使用動態規劃思想有個前提:當且僅當每個子問題都是離散的(即每個子問題都不依賴於其他子問題時),才能使用動態規劃。

再次回到題目,假設這個醉漢在第 N 步到達 (mi, nj) 位置有 dp[N][mi][nj] 種路徑,可以假設一下當前狀態如何從上一步移動中得來。其實就是上下左右四個方向移動過來的,而移動步數則是 N-1。

def how_likely_alive(m, n, N, i, j):  
  
    tmp=[[[0 for i in range(n)] for j in range(m)] for k in range(N+1)]  
    for k in range(1,N+1):  
        for p in range(m):  
            for q in range(n):  
                if 0==p:  
                    up=1  
                else:  
                    up=tmp[k-1][p-1][q]  
                if m-1==p:  
                    down=1  
                else:  
                    down=tmp[k-1][p+1][q]  
                if 0==q:  
                    left=1  
                else:  
                    left=tmp[k-1][p][q-1]  
                if n-1==q:  
                    right=1  
                else:  
                    right=tmp[k-1][p][q+1]  
                tmp[k][p][q]=(up+down+left+right)%1000000007  
  
    num = tmp[N][i][j]  
    if num == 0:  
        return 1  
    else:  
        return num / 4  
    return num

print(how_likely_alive(2,2,1,0,0)) 
複製程式碼

結語:Leetcode演算法題浩如煙海,想要每一道題都瞭如指掌,個人感覺難度不小,但是從這道二維矩陣中的醉漢來看,企業就算想要“魔改”,也是萬變不離其宗,多多少少都有跡可循,所以我們在刷題的過程中,應該本著寧缺毋濫的原則,真實的掌握演算法核心思想,才能夠做到舉一反三、百戰不殆。

原文轉載自「劉悅的技術部落格」 v3u.cn/a_id_168

「其他文章」