Leetcode題解 Python:四月挑戰DAY15 Product of Array Except Self

給一串數列,回傳一串除了對應[i]以外的元素乘積數列。

如果要在空間複雜度 O(1) 完成,那麼只能透過修改傳入的數列。不過也有說回傳值不算在內。

最險惡的地方在於 0 的出現,否則直接用全部元素乘積除每個就好。除 0 會發生錯誤,所以要避開有 0 的情況。

列舉所有情況有三種︰
1. 0 出現兩次以上︰全部為 0 。
2. 0 出現一次︰只有 [i] == 0 為總乘積,其餘為 0。
3. 沒有 0 出現︰總乘積除[i]。

遍歷一次,取得非0元素的所有乘積與 0 出現次數,然後針對三種情形決定回傳值。

class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
product = 1
count = 0
for num in nums:
if num: product *= num
else: count += 1
if count >= 2: return [0 for _ in nums]
elif count == 1: return [0 if num else product for num in nums]
return [product // num for num in nums]

Leetcode題解 Python:四月挑戰DAY14 Perform String Shifts

給一個字串 s,已經一串指令 shift,指令為 [direction, amount] 代表向 direction 轉 amount 次,direction {0(左), 1(右)}。

由於只需要最終結果,而且左右又可以抵銷,因此不需要傻傻跟著每個指令轉,只需要統計最後要轉哪個方向幾次就好。

如果向同個方向轉 s長度 次,就等於轉一圈,因此統計次數可以取 len(s) 餘。

若向右轉 t 次,在位置[len(s)-t]之後的字母會跑到前方,在前方的字母會移到後方,可以寫成 s[len(s)-t:] + s[:len(s)-t]

也可以簡化成 s[-t:] + s[:-t] 。

class Solution:
def stringShift(self, s: str, shift: List[List[int]]) -> str:
right = 0
for each in shift:
right += each[1] if each[0] else -each[1]
right %= len(s)
return s[-right:] + s[:-right]

Leetcode題解 Python:Regular Expression Matching

這裡的表達式的特殊符號只有 “.”, “*” 所以蠻簡化的。

想到正則表達式,如果能夠 import re,那絕對相當地快速。用 re 的函式就不需要去寫判斷了。

今天拋棄 re,這題限制居多,輸入字串不能局部配對,一定要全部吻合。難度會比真的re運作還簡單不少。

在我心目中,不能只是解題,還要符合 re 的運作模式,才是我要的解法。

但還是先解題優先吧。

首先有 si 跟 pi,代表兩個字串的索引,兩者都從 0 找起,如果 si 到底(等於s長度),而且也 pi 到底,代表 s 跟 p 全部匹配。

說到特殊符號,”.”相對容易處理,這裡就不提了,不過”*”就困難一些,”*”代表前面字符可以出現 0或1次或1次以上,因此產生了許多可能性。

由於”*”僅影響前面的字符,所以當我們在匹配 s[si] 與 p[pi] 時,要考慮 p[pi+1]是否為”*”,同時也要小心是否 pi+1 超出邊界,以免p[pi+1]報錯。

當後面是”*”時,匹配有三種變化,分別對應 0 或 1次 或 1次以上 的情形
第一種出現0次,si保持不到,而pi直接+2跳過”*”,繼續遞迴下去。
第二種出現1次,若有匹配到,si+1 pi+1,往深遞迴。
第三種出現多次,若有匹配到,si+1 pi不變,往深遞迴。

由於”*”使得可能性開枝散葉變多,而造成重複計算,而且每組 si pi都只會有一種結果,因此可以使用dp去減少計算時間。(這裡使用lru_cache)

這讓我更了解dp的使用時機:會重複計算,而且相同參數只有會一種結果。

接下來要考慮例外,如果 (s,p) = (“”,””)、(“”,”.*”)、(“a”,””)、(“”,”b”),這些情況下能順利解決?

這裡先看到 si == len(s),搜尋到了底,這時要提防 p 是否還有長度可以搜尋,如果剩下”.*”要如何解決。

當 si == len(s),代表只剩 “” 可以拿來比較,如果剩餘的p可以匹配””字串,那代表兩者相同。

而p能匹配空字串的,只有p[pi]後面是”*”。

空字串只會匹配後面有”*”的第一種情形,於是pi+2。如果剛好是”.*”也能匹配到,但匹配到的結果會使 si +1 而往下會使得 si > len(s),超出範圍就不用匹配。

這樣子寫,也不需要在開頭做例外處理。

class Solution:
def isMatch(self, s: str, p: str) -> bool:
from functools import lru_cache
n, n2 = len(s), len(p)
self.res = False

@lru_cache(None)
def nextMatch(si, pi):
if si > n or pi >= n2:
if si == n and pi == n2: self.res = True
return False
elif si == n:
c = ""
else:
c = s[si]
#
ch = p[pi]
# 如果後面是 "*" 字,分別採取不同搜尋
if pi+1 < n2 and p[pi+1] == "*":
if c == ch or ch == ".":
nextMatch(si+1, pi) # 原地不動
nextMatch(si+1, pi+2) # 或向前走
nextMatch(si, pi+2) # 當作沒有配對到
else:
# 後面沒有 *
if c == ch or ch == ".":
nextMatch(si+1, pi+1)

nextMatch(0, 0)
return self.res

Leetcode題解 Python:Pizza With 3n Slices

一個pizza切成3n片,每片面積大小不同,每片面積當作一串數列傳入。當你選一片之後,上下兩片會被朋友拿走,請問能拿到的最大面積的為何?

這是一道難題,能解出來的不多。

對於這個 pizza 3n 片這般的難題,除了學會解法之外,更應該去探索解題思路。

暴力解法是很容易,有 3n 片,會有 3n 種取法,3n 種取法後面會衍生出 3n-3 種的取法,如此到底,時間複雜度會是 O(N**N),所以超過10片就會跑到超時。

不僅運算時間久,衍生出的各種可能也需要保存,空間複雜度也是很大,即便用上 backtrace 降低記憶體使用量,時間複雜度的問題依舊沒有解決。

得用「空間換取時間」,那麼空間要保存甚麼?對,這就是動態規劃的問題。

有沒有甚麼辦法,可以保存各種pizza的拿取情形?參考

若把pizza攤成一排,000000 代表尚未拿取任何一片,100000 代表拿取了第一塊,下一塊不能拿取第一塊鄰近,因此只能選[2]與之後的位置。※注意※Pizza是圓的!

如此類推,只要兩個1不相鄰即可,於是會有 101000、100100、100010、……的拿法,不過這樣還是太多了。

但是繼續往深想,如果拿了第一片,而剩下的組合是固定的,只會出現一種最大取法,不需要分開討論。

於是當前要保存的空間可以推演成 dp[pos][times] ,pos是當前位置,times目前是拿取的次數。

接下來講遞迴函數,經過前面的發想,會是深度優先的dfs,從底搜到頭。

dfs(pos, tiems),而 dfs(0, 0) 是起點,也代表從0開始取n片的最大值,所以回傳值為當前最大值。

如果還能夠取,就比對 slices[pos] + dfs(pos+2, tiems+1), dfs(pos+1, times),看誰大取 max()。

如果不能取,當前最大值即 dfs(pos+1, times) 亦作 dfs(pos+1, n) 。

到達最深處,一旦 pos == 3n 或是 times == n,代表無法再取,回傳 0,這要寫在前方。

基本架構完成,問題點來了,前面用※注意※提示Pizza是圓的,萬一同時取到第1片跟最後1片怎麼辦?

當遞回到深處的時候,是無法知道前面是否有取到第一片,那麼最後1片可取或不可取便是疑問。

這有兩個策略,1是分別考慮,從第一片取到第3n-1片與第2片取到第3n片,這裡就不會出現第一片跟最後一片都取到的狀況。2是讓最後一片絕對不取到,甚麼情況下絕對不會取到最後一片,如果最後一片是最小的那塊。

為求簡潔好懂,所以採取2。

class Solution:
def maxSizeSlices(self, slices):
memo = dict()
sl = len(slices)
n = sl//3

minSliceIdx = slices.index(min(slices))
slices = slices[minSliceIdx+1:] + slices[:minSliceIdx+1] # 把最小片的移到最後一塊

def maxSize(pos, times):
if pos >= sl or times == n: return 0
if pos in memo and times in memo[pos]: return memo[pos][times]
else:
if pos not in memo: memo[pos] = {}
if times not in memo[pos]: memo[pos][times] = 0

if times < n:
score = max(slices[pos] + maxSize(pos+2, times+1), maxSize(pos+1, times))
else:
score = maxSize(pos+1, times)
memo[pos][times] = score
return score

return maxSize(0, 0)

做了幾次題目之後,對於DP要建立多維字典實在感到厭倦,如果Python要快速建立多維字典,用 collections.defaultdict 會省下不少時間。

class Solution:
def maxSizeSlices(self, slices):

sl = len(slices)
from collections import defaultdict
memo = defaultdict(dict)
minSliceIdx = slices.index(min(slices))
slices = slices[minSliceIdx+1:] + slices[:minSliceIdx+1] # 把最小片的移到最後一塊

def maxSize(pos, remain):
if pos+remain*2-1 >= sl or remain == 0:
return 0
elif remain in memo[pos]:
return memo[pos][remain]
else:
memo[pos][remain] = max(slices[pos] + maxSize(pos+2, remain-1), maxSize(pos+1, remain))
return memo[pos][remain]
return maxSize(0, sl//3)

Leetcode題解 Python:四月挑戰DAY13 Contiguous Array

這題有難度,礙於我現在的時間管理策略,若沒能在時間內想到解法,就去找別人的理念講解,然後自己突破。參考

給一串數列,只有 0, 1,回傳 0 與 1 數量相等的子串列的最大長度。

0 與 1 數量相等,第一直覺就是要想到如何計數。如果不帶技巧,暴力解法需要 O(n**3) 時間解決。

如果要減少時間,那麼「用空間換取時間」的想法就可以派上用場,那麼空間要記錄些甚麼呢?

如果一個子串內的 0, 1 數量相等,總和會是長度的一半。也許派得上用場。(但沒有用)

先假設個 000111 ,若走到[2],0會是 3 個,走到[4],遇到 1 才會出現第一個合格長度 2。再遇到 1 變成長度 4,再遇到 1 變成長度 6。

由於 0、1 一定要都出現才會有長度,出現整排1跟0都不會出現合法長度,因此跟 0 與 1 數量差相關,數量差可以為 0,為 1,為 -1,這裡先繼續探索「數量差」是否能用。

0、1數量差為0,第一次出現在位置[-1],也就是起始值。下次 數量差0 出現在位置[5],因此 5 – (-1) = 6。數量差看起來是有用的。

若第一次紀錄後出現同樣的數量差,其中一定出現成對的 0、1。長度多少,因為只需要取最大長度,因此只要保留第一次出現的位置即可。

於是空間拿去紀錄各 0、1數量差 的出現位置。

回頭看 000111 走到 [3] 的時候,此時 0與1數量差 為2,上次出現 2 的位置為[1],長度為 3 – 1 = 2。

class Solution:
def findMaxLength(self, nums: List[int]) -> int:
memo = {0:-1} # 在 -1 位置,數量差一定為0
counter0 = 0
maxLength = 0
for i in range(len(nums)):
if nums[i] == 0: counter0 += 1
else: counter0 -= 1
if counter0 not in memo:
memo[counter0] = i
else:
maxLength = max(maxLength, i - memo[counter0])

return maxLength

Leetcode題解 Python:Check if There is a Valid Path in a Grid

給一個二維串列 grid,每格代表一種道路,其值可以對應下表,要回傳是否能從左上角走到右下角。

1 which means a street connecting the left cell and the right cell.
2 which means a street connecting the upper cell and the lower cell.
3 which means a street connecting the left cell and the lower cell.
4 which means a street connecting the right cell and the lower cell.
5 which means a street connecting the left cell and the upper cell.
6 which means a street connecting the right cell and the upper cell.

對於這種地圖或是面積的圖像問題,我總是感到棘手,也許有一套通用思路,不過可能需要很多經驗值累積才能解得順手。

從左上角開始,但是沒有規定左上角一定是從外面延伸進來,如果是 grid[0][0]  == 5,一開始就死局。如果等於 4,那一開始就會有兩條路線。

要怎麼解決起始有兩條路線的問題,就會是一個難題。又或者拋開心中虛擬的小車車,直接用遞迴 + memo 去從(0, 0)拔起所有合格座標,再看看右下角是否存在。

街道有六種形式,而且具有方向問題,困難點在此,要怎麼妥善解決轉向,這就值得思考。


每一種街道只能接受兩種方向,例如 street 1,如果從右邊來,通過後方向朝左邊;如果從左邊來,通過後方向朝右邊。如果從其他方向,那就代表無法接到一起。

街道對應的方向就只能老實打出,無法再快,這是題目的定義。

如果在當前位置向右走,對下一個位置是從左來,對於制定方向前進的人,要留意方向上的切換。

思考一下之後,使用遞迴拔出所有與(0,0)連接的街道會比較泛用。

class Solution:
def hasValidPath(self, grid):
"""
1 which means a street connecting the left cell and the right cell.
2 which means a street connecting the upper cell and the lower cell.
3 which means a street connecting the left cell and the lower cell.
4 which means a street connecting the right cell and the lower cell.
5 which means a street connecting the left cell and the upper cell.
6 which means a street connecting the right cell and the upper cell.
"""
# 0 up 1 left 2 down 3 right
streets = {1:(1,3), 2:(0,2), 3:(1,2), 4:(3,2), 5:(1,0), 6:(3,0)}

path = set()
rows, cols = len(grid), len(grid[0])

def searchMap(y,x):
if y < 0 or x < 0: return
if y == rows or x == cols: return
path.add((y,x))
ds = streets[grid[y][x]]

for d in ds:
x2,y2=x,y
if d == 0:
y2 = y-1
elif d == 1:
x2 = x-1
elif d == 2:
y2 = y+1
else:
x2 = x+1
d = (d+2)%4 #換方向(下一個位置的相對方向)
if (y2, x2) not in path:
if y2 < 0 or x2 < 0 or y2 == rows or x2 == cols:
continue
elif d in streets[grid[y2][x2]]:
searchMap(y2,x2)

searchMap(0,0)
return (rows-1, cols-1 ) in path

Leetcode題解 Python:Number of Ways to Paint N × 3 Grid

給一個 n,代表 (n , 3) 的二維串列,每個位置可以填入R、G、Y三色,而每個顏色不能相鄰(上下左右)相同,要求出一共有幾種填法。

這是我第二次做到要取餘的題目,代表方法數真的很大。

如果 n = 7,就會超時。

我左右一看,感覺到這只是個數學問題,可以代公式求解,不用真的計算。結果真的找到公式,代上去只用 一百多毫秒 就能通過測試。

在我心中,用公式解,就像拋棄電腦強大計算能力不用,背棄了用程式語言邏輯去解決問題的宗旨,跟作弊是沒兩樣的。

於是我還是老實地寫出一個合格的解法。

先看題目,第一層有12種組合。每一層的所有組合,都在這12種之內。

第二層有多少種組合,取決於第一層,也就是最後一層相鄰的那一層。

每種組合後面能接上的組合是固定的,只要找出來,就不必去費心再去搜尋下一層有甚麼可能。

所有A組合能接的下一層組合,都加上A組合的數目。n層的總和就是n層所有組合的加總。

不論是三種顏色還是四種顏色,寬三格還是四格,都可以套用這樣的邏輯去處理。

class Solution:
def numOfWays(self, n: int) -> int:
# 建立第一層
memo = dict()
memo["now"] = {}
memo["next"] = {}
colors = set(["r","g","y"])
# 找到所有顏色組合
def findColorSet(stats):
if len(stats) == 3:
memo["now"][stats] = 1
return
for c in colors:
if not stats or c != stats[-1]:
findColorSet(stats+c)
findColorSet("")

#
c2c = {}
for c1 in memo["now"]:
c2c[c1] = []
for c2 in memo["now"]:
isLegal = True
for ch1, ch2 in zip(c1,c2):
if ch1 == ch2:
isLegal = False
break
if isLegal:
c2c[c1].append(c2)
#
if n > 1:
keys = memo["now"].keys()
for row in range(2,n+1):
memo["next"] = {}.fromkeys(keys,0)
for key in keys:
v = memo["now"][key]
for key2 in c2c[key]:
memo["next"][key2] += v
memo["now"].update(memo["next"])

return sum(memo["now"].values()) % (10**9+7)

這樣子符合要求我自身要求,用程式邏輯找到答案。

下面附贈公式解,也是我第一個想出來的解法。

class Solution2:
def numOfWays(self, n: int) -> int:
self.ans = 0
table = dict()
table2 = dict()
table2[1] = 12
table[1] = 0
table2[2] = table2[1]*9//2+table[1]
table[2] = 3

if n >= 3:
for i in range(3, n+1):
table2[i] = table2[i-1]*9//2+table[i-1]
table[i] = table2[i-2]+table[i-1]

return table2[n] % (10**9+7)

Leetcode題解 Python:四月挑戰DAY12 Last Stone Weight

給一串數列,取出最大的兩個相減,如果結果非零,而把結果加回數列,重複進行,直到剩一個元素或是沒有元素,回傳該元素的值,若無而回傳 0。

一說到要取出最大的兩個,就會想到用 sort() 排列。 ※注意 sort() 是從小而大排列,想要反敘可以這樣寫 sort(reverse = True)。

每一次取出前兩個對減,加回去再重新排列,重複步驟直到不能再進行,這是最直覺的方式。

排列之後會照大小排列,如果取出兩元素相減後還 >= 剩下的第一個元素,代表兩者是當前最大的兩元素,就可以繼續相減。

條件不符合加回去再重新排列,這樣可以減少不必要的排列次數。

class Solution:
def lastStoneWeight(self, stones: List[int]) -> int:
n = len(stones)
while n > 1:
stones.sort(reverse = True)
v = stones.pop(0)
while n > 1 and v >= stones[0]:
v -= stones.pop(0)
n -= 1
if v == 0: n-= 1
else: stones.append(v)
if stones: return stones[-1]
else: return 0

Leetcode題解 Python:四月挑戰DAY11 Diameter of Binary Tree

給一串二元樹,問直徑,也就是節對節之間的最長距離。

如果以 root 來看,通過 root 的最長距離,就是 root.left 的最大深度加上 root.right 的最大深度。

於是方向很清晰,我們要用一個函數去取得該node的最大深度。這樣就能進而找出各個距離,再找出最長距離。


要寫遞迴函數,而且會從最深的地方開始,而最深的地方會往上層層回傳最大深度。

因此回傳的值是「深度」,如果沒有node,就回傳 0。

如果有node,要往上回傳最大深度,就需要知道 node.left 與 node.right 的深度,最大深度是從二者選著大的,然後加上自身 +1。

當我們得到 node.left 與 node.right 的深度時,兩者相加後,就能得到通過該 node 的最長距離。

這樣就能用 max() 去更新最大值,等待遞迴函數跑完,此時的最大值就是該二元樹的最長距離。

class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
self.maxDM = 0
def getMaxDepth(node):
if node:
left_depth = getMaxDepth(node.left)
right_depth = getMaxDepth(node.right)
DM = left_depth + right_depth
self.maxDM = max(self.maxDM, DM)
return 1 + max(left_depth, right_depth)
else:
return 0
getMaxDepth(root)
return self.maxDM

Leetcode題解 Python:Find All Good Strings

這題相當困難,從通過的人數就知道。為什麼困難?因為這需要使用 KMP + 數位DP。

如果不清楚 KMP 跟 數位DP 的人,請先去看我之前的入門級介紹。本篇主要參考

給兩個字串 s1, s2,同樣是 n 大小,給一字串 evil,回傳在 s1, s2 區間內不含 evil 的字串數。

這題目的敘述,便是典型的數位DP。

因此我們先將數位DP的模型寫出來。

       from functools import lru_cache

@lru_cache(None) #紀錄傳過的參數與結果,若下次回入一樣的參數,便可以直接回傳結果。
def dfs(pos, stats, bound):
if stats == np: return 0
if pos == n: return 1

l = ord(s1[pos]) if bound & 1 else ord("a")
r = ord(s2[pos]) if bound & 2 else ord("z")

result = 0
for u in range(l, r+1):
char = chr(u)
if bound == 3 and char == s1[pos] and char == s2[pos]:
nextBound = 3
elif bound & 2 and char == s2[pos]:
nextBound = 2
elif bound & 1 and char == s1[pos]:
nextBound = 1
else:
nextBound = 0

nextStats = search(stats, char) #此時尚未安排
result += dfs(pos+1, nextStats, nextBound)

return result % (10**9+7) # 題目要求取餘

這裡沒有數字,只有a-z,就算沒有數字,也能用ord()把字母轉成unicode,把 a-z 當成二十六進位制,因此可以取出左右範圍。

接著要講 search() ,能不能使用暴力匹配法呢?

如果這樣使用,逐一匹配,過程中失敗後從 Target 的下一位開始從頭匹配。然而 數位DP 在過程中有部分匹配時,不論Target或Pattern都已經往下一位,萬一發生匹配失敗,Target是無法回到匹配開頭的下一位開始。

既然不能使Target回溯,暴力匹配法也會遇到阻礙,那有甚麼搜尋法是可以讓Target的索引一直遞增下去?使用KMP搜尋。

使用KMP,Target的索引會逐漸遞增到結尾,Target匹配過的部分就不需要再匹配,這能與數位DP結合上。

直接把KMP的模型套入,也決定了search()。

        np = len(evil)
# 建立 prefixTable
prefixTable = [0] * np
for i in range(np):
if i == 0:
prefixTable[i] = -1
else:
pi = prefixTable[i-1]
while pi >= -1:
if evil[i-1] == evil[pi]:
prefixTable[i] = pi + 1
break
else:
if pi == -1:
prefixTable[i] = 0
break
pi = prefixTable[pi]

def search(stats, char):
while stats > -1 and char != evil[stats]:
stats = prefixTable[stats]
return stats +1 if char == evil[stats] else 0

將兩部分整合之後,可以得到一個完整代碼。

class Solution:
def findGoodStrings(self, n: int, s1: str, s2: str, evil: str) -> int:
from functools import lru_cache

np = len(evil)
# 建立 prefixTable
prefixTable = [0] * np
for i in range(np):
if i == 0:
prefixTable[i] = -1
else:
pi = prefixTable[i-1]
while pi >= -1:
if evil[i-1] == evil[pi]:
prefixTable[i] = pi + 1
break
else:
if pi == -1:
prefixTable[i] = 0
break
pi = prefixTable[pi]

def search(stats, char):
while stats > -1 and char != evil[stats]:
stats = prefixTable[stats]
return stats +1 if char == evil[stats] else 0

@lru_cache(None)
def dfs(pos, stats, bound):
if stats == np: return 0
if pos == n: return 1

l = ord(s1[pos]) if bound & 1 else ord("a")
r = ord(s2[pos]) if bound & 2 else ord("z")

result = 0
for u in range(l, r+1):
char = chr(u)
if bound == 3 and char == s1[pos] and char == s2[pos]:
nextBound = 3
elif bound & 2 and char == s2[pos]:
nextBound = 2
elif bound & 1 and char == s1[pos]:
nextBound = 1
else:
nextBound = 0

nextStats = search(stats, char)
result += dfs(pos+1, nextStats, nextBound)

return result % (10**9+7)

return dfs(0, 0, 3)

Python 數位DP (Digit dynamic programming) 在區間內找符合條件的組合數

數位是指數字位數,個人偏好講位數。DP是指動態規劃,一次參與進兩個,對於初學者肯定是相當難理解的。(我也花幾天去釐清思路到能用

動態規劃簡單的說法:就是用「空間換時間」,把問題拆成子問題作解決,子問題都常會被重複計算,像是費氏數列 fib(x) = fib(x-1) + fib(x-2),用空間記住參數對應的答案,就能減少計算時間。

而數位DP的特徵就是會給予「界限 Bound」,例如在 0 ~ 99 區間找不含某種條件的數字總數等,像是不包含 5 的數字總數。

一旦看到這樣的敘述,就能夠準備開打數位DP的模板了。

先不講如何寫,這裡先講觀念。

dp = dict() #字典

dp[pos][stats][bound],由三個部分組成,pos位數、stats統計、bound邊界。(※pos跟bound是固定的,stats可以超出一項。

像是”99″有兩位數,所以 pos = {0, 1} 分別對應 |9|9 跟 9|9| 的位數。

bound是邊界,狀態有四種。
0:無上下界
1:下界
2:上界
3:上下界

l, r為該位數的左右邊界,如果受下界影響, l 會是下界相同位數的數字;如果受上界影響,r 會是上界相同位數的數字。

如果區間是 09 到 21 間,首先搜尋函數會傳入 pos = 0, stats(暫不提), bound = 3。

在 [0] 位置,bound = 3 代表 l, r 受上下界 |0|9 與 |2|1 的影響,因此可選範圍在 {0, 1, 2},可寫成 range(l, r+1)。

此時如果選 0,數字與下界符合,往深搜尋要傳入 (pos+1, nextStats, nextBound=1),仍受下界限制。

此時如果選 1,數字不與上下界相同,往深搜尋要傳入 (pos+1, nextStats, nextBound=0),代表下一位可選範圍是最自由的 0 – 9 。

選 2 的話,下一位會受上界影響。

關鍵點來了,stats 這個會受到題目限制而有所影響。

目前的題目是:在 09 到 21 區間內找 不含 “5” 個數。

要知道有沒有 “5” 的出現,我們可以用 “5” 去匹配,如果有匹配到,索引+1,然後直到索引到達自身長度就代表全匹配,即有 “5” 出現。

因為要找不含 “5” 的組合數,所以這裡的 pattern 便是 “5”。而 stats 起始條件是 0,代表從 pattern 位置 [0] 開始匹配。

因此 dp[0][0][3] 也代表題目所求且符合所有條件的個數,符合上下界條件跟搜索條件,而搜索條件會寫在搜尋函數。

如果當前組合位置符合 patter[i],便 i+1 往下,下一位傳入的參數 (pos+1, i+1, bound),於是下一位數所有枚舉會去匹配 patter[i+1] 。

要是pattern索引達到自身長度,代表當前組合含 “5”,沒有必要繼續往下搜尋,也不需要再枚舉, retrun 0。

符合題目要求的條件是pos到達自身長度,到達邊界時,仍沒有全匹配出現,此時 return 1,代表此組合符合題目要求。

"""
digit dynamic programming 入門
「在 [0-9] 區間內,找出 不含"5" 個數」
"""

# 在 [0-9] 區間內,找出 不含"5" 個數
s2 = "9"
s1 = "0"
p = "5"

# 建構 dp 表
dp = dict()
for pos in range(len(s2)):
dp[pos] = {}
for stats in range(len(p)):
dp[pos][stats] = {}
for bound in range(4):
dp[pos][stats][bound] = -1 # -1 代表尚未搜尋過

# 建構 搜尋函數 dfs深度優先搜尋(白話:從底找上來)
def dfs(pos, stats, bound):
# 回傳符合條件或不符合條件的值
# 如果模板索引為1,代表找到 "5"
if stats == len(p): return 0
# 找到底,且過程中沒有找到 "5"
if pos == len(s2): return 1
# 有被找過,則直接回傳值
if not dp[pos][stats][bound] == -1: return dp[pos][stats][bound]

"""
當前枚舉範圍設定
bound = 0: 無上下界
bound = 1: 只下界
bound = 2: 只上界
bound = 3: 上下界
"""
if bound == 1 or bound == 3:
l = int(s1[pos]) # ※要小心,s1是str,但l得是int
else:
l = 0
if bound == 2 or bound == 3:
r = int(s2[pos])
else:
r = 9

# 開始枚舉
result = 0
for num in range(l, r+1):
"""
後面的邊界會受前面的影響。
想想 (12, 16) 、 (09, 21) 後位取界的問題
在 (12, 16) 中,最高位都是 1,因此後位需要受上下界影響
在 (09, 21) 中,枚舉時最高位取 2,可以想成在枚舉(20, 21),後位受上界影響。
在 (09, 21) 中,枚舉時最高位取 1,可以想成在枚舉(10, 19),後位不受上下界影響。
在 (09, 21) 中,枚舉時最高位取 0,可以想成在枚舉(09, 09),後位受下界影響。
"""
if bound == 3 and num == l and num == r:
nextBound = 3
elif (bound == 2 or bound == 3) and num == r:
nextBound = 2
elif (bound == 1 or bound == 3) and num == l:
nextBound = 1
else:
nextBound = 0
# 匹配:暴力匹配法(只適用pattern長度1,超過1請用KMP)。匹配結果影響放在函式前方
nextStats = stats + 1 if str(num) == p[stats] else 0

result += dfs(pos+1, nextStats, nextBound)

dp[pos][stats][bound] = result
return result

print(dfs(0, 0, 3)) # => 9

這裡的搜尋使用暴力匹配法,但只限於長度1,為什麼長度超過1不能使用?

在暴力匹配法在過程中匹配失敗,就會回到Target索引+1繼續從頭匹配。但是數位DP的索引是一路往深,若碰到11112、1112,會在 111|1|2、111|2| 位置上匹配失敗。若採用暴力匹配法,下一步應該是去匹配 1|1|112、|1|112,此時數位DP已經在pos[3],很難回到pos[1]再重新匹配。(會花很多時間。)

所以要是使用KMP,就能夠不回溯Target的索引搜尋到底,相對也會順暢許多。

Leetcode題解 Python:四月挑戰DAY10 Min Stack

設計一個 stack,而且能在 O(1) 時間內回傳最小值。

stack是後進先出,如果眼前有書疊,要拿也是會從最上方拿起,最上方的書也是最後放的。

要實現stack,用list儲存資料就可以。

stack新增資料,用list.append()。 stack.pop => list.pop()。用list實現stack基本功能是沒有問題的。

這個 Min Stack 特別之處,在 O(1) 時間內回傳最小值,如果用 min(),而時間複雜度會是 O(n)。

因此在 Min Stack 的類別下,要有一個保存最小值的屬性,這樣才能第一時間回傳最小值。

用 int 保存可以嗎?

如果最後面加入的是最小值,然後被pop()掉,此時如果要調出此時的最小值,要怎麼處理?

如果連續加入兩個最小值,然後被pop()掉一個,又要怎麼處理最小值呢?

要是pop()掉的值是最小值,此時再用min()重新定位最小值,如果剛剛pop()掉的是僅剩的那一個元素呢?

如果考慮到stack的後進先出,這裡有個更妙的處理方式。就是使用一個minStack(list)。

加入的資料要是小於minStack[-1]的值,該值就順便加入到minStack。由於後進先出的特性,pop()掉的值一定會先遇到minStack[-1],才會到[-2]、[-3]…

返還最小值,也只要回傳minStack[-1]就可以了。

class MinStack:
def __init__(self):
"""
initialize your data structure here.
"""
self.stack=[]
self.mini_stack=[]

def push(self, x: int) -> None:
self.stack.append(x)
if not self.mini_stack or x<=self.mini_stack[-1]:
self.mini_stack.append(x)

def pop(self) -> None:
if self.stack.pop() == self.mini_stack[-1]:
self.mini_stack.pop()

def top(self) -> int:
return self.stack[-1]

def getMin(self) -> int:
return self.mini_stack[-1]