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"

Leetcode題解 Python:Flatten a Multilevel Doubly Linked List

給一串 Doubly Linked List ,其中某節可能有 child ,要求你把這串 Doubly Linked List 平坦化,而 child 會插入在 該節 與 下一節 之間。

A – B – C – D – E        => A – B – F – G – C – D – E
      F  – G

寫一個遞迴函數,如果遇到 child ,你需要它的頭尾,針對這兩點修改就好。
(尾)G.next = B.next,  G.next.prev = G 、B.next = F(頭) , B.next.prev = B

於是遞迴函數的回傳值,就設定是 head 與 tail。

頭是搜尋的起點,有頭才能夠搜尋,而尾在尚未搜尋前是未知的,於是遞迴函數的回入值是頭節。

這樣在一路到底搜尋時,若是碰到 child,則把 child node 傳給遞迴函數,取得該子串的頭尾,然後插入當前的位置與下一位置的中間。

空間複雜度為 O(1)

class Solution:
def flatten(self, head: 'Node') -> 'Node':
def nextNode(node):
head = tail = node
while tail:
if tail.child:
ch, ct = nextNode(tail.child)
if tail.next:
ct.next = tail.next
ct.next.prev = ct
tail.next = ch
tail.next.prev = tail
tail.child = None
tail = ct
else:
if tail.next:
tail = tail.next
else:
break
return head, tail
return nextNode(head)[0]

另外一種則是先遍歷,記錄順序,最後再重組。

class Solution:    
def flatten(self, head: 'Node') -> 'Node':
nodes = []
def nextNode(node):
if node:
nodes.append(node)
nextNode(node.child)
nextNode(node.next)
nextNode(head)
p = None
for each in nodes:
each.child = None
each.prev = p
if p: p.next = each
p = each
return head

Leetcode題解 Python:Palindrome Linked List

判別 Linked List 的值是否為回文。

凡是回文問題,就一定要知道長度,才能知道哪裡是分水嶺。

如果想用紀錄數值,最後再判別是否回文,由於該值可能為雙位數或是負數,如果想用 str 紀錄數值,就必須做特殊處理。

使用 list 紀錄,等過了分水嶺之後,就是該值與紀錄逐一對比,不符合就不是回文,找完就是回文。

class Solution:
def isPalindrome(self, head: ListNode) -> bool:
if not head: return True
len_head = 1
SP = FP = head
while FP.next:
len_head += 1
FP = FP.next
#
stack = []
for _ in range(len_head//2):
stack.append(SP.val)
SP = SP.next
if len_head%2==1: SP = SP.next
while stack:
if SP.val != stack.pop():
return False
SP = SP.next

return True


簡化回文的判斷,使用 stack == stack[::-1],這樣只需要比較一次,不用記錄長度,空間複雜度仍是 O(n)。

class Solution:
def isPalindrome(self, head: ListNode) -> bool:
stack = []
while head:
stack.append(head.val)
head = head.next
return stack == stack[::-1]

如果利用循環的概念,快指針走 長度-1 、 慢指針走 1 ,兩兩相相比,不相等就不是,直到快指針追到慢指針結束。

但是這樣會超時。

如果空間複雜度局限於 O(1),就要使用 reverse 的概念,不論是把分水嶺前面的或後面的 reverse 都可以。反序完再兩兩比較。

class Solution:
def isPalindrome(self, head: ListNode) -> bool:
if not head: return True
len_head = 1
SP = FP = head
while FP.next:
len_head += 1
FP = FP.next
#
for _ in range(len_head//2):
FP = SP
SP = SP.next
FP.next = head
head = FP
if len_head%2==1: SP = SP.next
while SP:
if SP.val != head.val:
return False
SP = SP.next
head = head.next

return True

最近我的leetcode總是跑得比別人慢,即使是一樣的代碼,就是遜一節,不知道為什麼。