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]

Python KMP演算法:字串搜尋

在電腦科學中,Knuth-Morris-Pratt字串尋找演算法(簡稱為KMP演算法)可在一個主文字字串S內尋找一個字W的出現位置。此演算法通過運用對這個詞在不匹配時本身就包含足夠的資訊來確定下一個匹配將在哪裡開始的發現,從而避免重新檢查先前匹配的字元。(維基百科)

來講一下一般的字串搜尋,假設一個情境:
Pos:     01234
Target: aaaac
Pattern:aac

通常會逐一比對,從0開始,Target[0]對上Pattern[0],接著[1],[2],匹配失敗。繼續往下,再Target[1]比Pattern[0],繼續往下比…

這樣會有些字符會重複搜尋過,如果第二個 a 有對上,第三個配不上,因為第二個 a 對上第一個 a,所以下次搜尋應該從第一個 a 對上的後面開始。

如此一來,便能夠減少搜尋次數,提高搜尋的效率。

KMP便是一種這樣構想的演算法。

推薦教學:bilibili

這幾天碰到一個困難的題目,我開始推理,思考搜尋字串重複的可能性解法,像是0~999之間,不出現 55 跟不出現 54的次數是不相等的。

這裡不講艱深的理論,只講要如何簡單地理解並使用。

範例:Github 入門版 、 入門版2、入門簡化 、函式簡化

如果毫無基礎,也可以直接從範例入門版看起並嘗試理解,不一定要看以下解說。

沿用情境,Target = “aaaac”,  Pattern = “aac”。

首先要用 pattern 要建立一個前綴表 PrefixTable。目的是決定該位置匹配失敗後,該移到pattern的哪個位置繼續匹配。

因此以 aac 來說,當pattern索引到 2 時,如果在 aa「c」的位置匹配失敗,那麼 prefixTable[2] 要導向 1 ,使得接下來的搜尋去比對 a「a」c  即 pattern[1]。就不用因為一次失敗而回到pattern[0]開始。

要如何找出決定這個 PrefixTable (dict)的值?

如果今天pattern只有一個字母,當前Target位置沒有匹配到,Target位置換下一個繼續匹配。

因此pattern在第一個字母[0]時沒有匹配上,代表Target位置要換下一個位置,因此PrefixTable[0]是特別的。

「a」ac,在 [0] 位置上會是讓 Target位置換下一位的值,設為 -1 能跟其他索引有所區別。不能設為 0,因為 [0] 代表「a」ac 自身,這樣會造成無限循環。

接下來講超越長度1的部分,直接跳到 aa「c」,這裡先跳過 a「a」c不講,因為我們對 aa「c」的已經有想法。

如果 aa|a|ac aa|c|匹配失敗,會希望用下一個是 aa|a|ac a|a|c。

這裡有個很重要的問題,是甚麼樣的計算,決定了要回到 a|a|c 的位置?

aa:如果第一個字母為 a,第二個字母如果也是 a,前後相同,長度為1。

abab:如果前兩個字母為 ab,後兩個字母為 ab,前後相同,長度為2。

後方新加入的部分要是跟前面部分的後方相同,長度就會 +1,換句話說也等於後綴與前綴最大相同的數目。

如果前後綴有相同的部分,就能夠不用回到[0]重新開始。PrefixTable的值跟pattern前後綴最大相同的數目是相關的。

回到 aa「c」上面,如果搜索失敗,回到用 pattern[1] 匹配。而 1 也是 aa 的前後綴最大相同的數目。

決定 aa「c」回去位置的,是 c 前面的 aa 。 決定 a「a」c回去位置的,是 a 前面的 a 。

如此可以把 aac 分切成: “”、”a”、”aa”,分別對應「a」ac、a「a」c、aa「c」。

到這裡,「a」ac: -1、aa「c」: 1,而 a「a」c 對應值為 0,因為 a 沒有辦法找前後綴相同。

PrefixTable的值已經知道如何計算了,而鍵可以用 0、1、2或是 a、aa、aac都可以。

接下來要如何使用 PrefixTable 進行 KMP 搜尋呢?

Target跟Pattern的搜尋位置,各需要一個索引(index)控制,這裡稱作 TI、PI。

TI、PI預設為0,當 Target[0] 跟 Pattern[0] 相同時,兩者往下 TI +1、 PI + 1。

當 Target[0] 跟 Pattern[0] 不相同時,查 PrefixTable[0],得 -1,因此 TI +1 往下找。

接著來到 Target[2] 跟 Pattern[2] 不相同,查 PrefixTable[2],得 1,PI = 1
接下來 Target[2] 跟 Pattern[1] 相同,兩者往下 TI +1、 PI + 1。

這樣搜尋直到找到底,或是出現全匹配的狀況。

如果是全匹配且需要繼續搜索,此時 PI 會跟 Pattern 的長度相等,超出範圍,因此 PI -= 1,讓 Pattern 回到 PrefixTable[PI-1]處搜尋。

範例有Python的代碼,可以參考看看,然後自己從頭實踐看看。

Leetcode題解 Python:四月挑戰DAY9 Backspace String Compare

給一個字符串,如果遇到「#」,就等於按下Backspace鍵去修改字符串。

如果直接直譯規則書寫,寫起來就是那麼樸實無華且枯燥,這樣也能通過。

class Solution:
def backspaceCompare(self, S: str, T: str) -> bool:
newS = ""
for char in S:
if char == "#":
if newS: newS = newS[:-1]
else:
newS += char
newT = ""
for char in T:
if char == "#":
if newT:
newT = newT[:-1]
else:
newT += char
return newS == newT

不過呢,題目希望我們能在 O(N)時間 並 O(1)空間 完成。


由於倒退符是往前刪除,因此由後往前對比就可以遇到#字再往前挪動。

指派一個 counter ,遇到 # 就 +1 ,非# 則 -1,使用 while 要跑到 counter 為 0 或是 跑到出界 為止。

這樣就無需使用到 O(N) 空間,用 O(1) 空間。同時也只需要跑一遍 N,花上 O(N) 時間。

class Solution:
def backspaceCompare(self, S: str, T: str) -> bool:
# 回車函式
def getPrev(text, pos):
backspaceCounter = 1
while backspaceCounter and pos >= 0:
pos -= 1
if text[pos] == "#":
backspaceCounter += 1
else:
backspaceCounter -= 1
if pos >= 0: return pos
return -1

SP, TP = getPrev(S, len(S)), getPrev(T, len(T))

while SP > -1 and TP > -1:
if S[SP] != T[TP]: break
SP, TP = getPrev(S, SP), getPrev(T, TP)

if TP == -1 and SP == -1: return True
return False

Leetcode題解 Python:四月挑戰DAY8 Middle of the Linked List

要回傳 Linked List 中間的節點,如果長度是偶數,則中間偏右那一個。

有兩種解法:

第一種是跑完一遍得到長度,計算距離,再從頭跑到中間即可。前進距離是 length //2 。

第二種是用快慢指針,每回合快針走兩步,慢針走一步。當到了快針不能再走的回合,慢針會在中間的位置。

class Solution:
def middleNode(self, head: ListNode) -> ListNode:
if not head: return head
FP, SP = head, head
while FP and FP.next:
FP = FP.next.next
SP = SP.next
return SP

Leetcode題解 Python:四月挑戰DAY7 Counting Elements

給一個數列,如果元素其值 +1 也在數列內的個數。如果有重複,則分開計算。

不多說甚麼,順一下解題邏輯。

如果要得知元素其值+1是否也存在於數列內,就得找完所有數列,並記錄元素是否有出現。

如果有重複出現,像是 1 1 2 2, 1 出現兩次且 2 在數列內,所以答案要加兩次。

於是記錄元素出現的時候,就要順便把其出現次數記錄下來。

最後跑完 memo 的鍵,鍵就是出現過的元素,若元素+1也在 memo 裡,答案就加上該元素的出現次數。

class Solution:
def countElements(self, arr: List[int]) -> int:
memo = dict()
for num in arr:
if num in memo: memo[num] += 1
else: memo[num] = 1
result = 0
for key in memo:
if key+1 in memo:
result += memo[key]
return result

Leetcode題解 Python:Stone Game III

Alice 跟 Bob 進行石頭遊戲,把石頭排成一排,一人一次拿 1 ~ 3 個,每個石頭都各有其分數,最後雙方誰持有的石頭分數總和高者勝。

這是 contest 的題目,在時限內沒有解出這一題,可惜。

雙方都會不會放水,採取最佳策略進行。這樣的情況下,如果是 Alice 贏就回傳 Alice,Bob 贏則 Bob,平手則 Tie。

這是一個博奕問題:有雙方,互相採取行動,兩者互相對抗。

當看到雙方都採取一樣的策略之時,雙方的判斷都是一樣的,因此用一個邏輯函數,就要能指出該下哪一步。

以拿走三顆石頭的時候來看,不論是此時是輪到 A 或者是 B ,兩者當前的最佳解都是一樣的。先後順序不影響決定最佳解,局面進行到哪裡才是。

因此,用一個 memo 記錄所有情形,其鍵會是拿走的數量或是剩餘的數量。這memo紀錄的分數對雙方都是一樣的。


接下來講一點博奕心理。

在選擇行動時,不一定會選擇當下分數最高的,如果最後要贏對手,採取行動原則是為了要讓自己領先對手加大差距,距離變化才是關鍵。

像是 -1 -2 -3 ,如果選擇分數最高的行動只拿 -1 ,對手會只拿 -2 ,最後你不得已只能拿 -3 而輸掉。

考量到對手的行動,你拿 -1 對手拿 -2 整體反倒被 對手拉近 1 的距離,若拿 -1 -2 對手會只能拿 -3,整體差距並沒有縮小,反到是更有利的選擇。

不過這沒提到後手再後手。你拿 -1 要思考對手的下一步, 而對手會思考對手的下一步再下一步,行動連鎖直到結束。連鎖到底,這種特性是遞迴。

整理一下觀察到的線索,推導出邏輯函數會是一個考慮拉大對手差距的遞迴函數。

那這個遞迴函數要回傳甚麼?

回到 -1 -2 -3 的例子,當你拿 -1,對手會採取最佳解,也就是剩下 -2 -3 的最佳拿法的分數。拿 -1 -2,也需要對手的最佳解。然後在三種選擇中,拿 -1 -2 是最佳解。

於是你可以找出當前狀況最佳解,回傳分數,遞迴往前傳,就能把最佳解一路往前推導。

最後來到起點,雙方一動也沒動的時候,此時由 Alice 先手,因此沒動過的最佳解,就是 Alice 的最佳下法的分數,如果為正的,意味著到最後 Alice 會贏那些分數。0則平手,負則輸掉。

class Solution:
def stoneGameIII(self, stoneValue):
stones = len(stoneValue)
pos = 0
memo = {} # 使用座標當標籤

def searchMaxRelativeScore(pos):
if pos in memo: return memo[pos]
# 計算出當前剩餘個數
score = 0
scores = []
remains = min(stones - pos, 3)
for i in range(remains):
scores.append(sum(stoneValue[pos:pos+i+1]) - searchMaxRelativeScore(pos+i+1))
if scores: score = max(scores)
memo[pos] = score
return score

searchMaxRelativeScore(0)

if memo[0] > 0: return "Alice"
elif memo[0] == 0: return "Tie"
else: return "Bob"