Leetcode題解 Python & C#:五月挑戰DAY15 Maximum Sum Circular Subarray

給一個環形陣列 A,要找出總和最大的子陣列。
這題蠻有意思的,讓我沒有順利解出來,有想到好幾種可能,看了別人的解法,發現是用最大最小的方式。
但我想到這方法的當下,沒有去進一步思考。
官方有 Solution ,內有四種解法,但我還是照步調去走從未知去導出解法的思維分析。
如果會做陣列中子陣列最大總和,那環形的也只是稍做變化。
要如何後面接回前面?這是第一個要解決的問題。

對於一個陣列來說,用Python是可以再添加一個一樣的,達到 2n 的長度,在 n – 1 位置之後,又會回到開頭的值。
但由於限制每個元素只能被取一次,這作法可能會讓一元素被選中二次(如全部為正的),並不是一種簡單可行的方法。
又或者弄 linked list ,然後作 DP 以各元素的選取保存每一位置的最大值總和。這能避免重複,但寫來會有點麻煩。
照著 Hint ,要用「 Kadane’s algorithm 」, 然而這不是算環形的陣列,需要改良才能用上。如果只跑一遍陣列,那也不會碰到同元素被選二次的狀況。
如果最大值落於陣列間,最後的位置不是 n – 1,那 Kadane’s algorithm 就可以找出正解。
但最後位置有碰到 n – 1,那麼就會接回開頭去找的需求。
再思考,如果目前正計算最大的子陣列,沒到計算結束前是不知道目前的是否處在最大子陣列中。而且 Kadane’s algorithm 的方式也沒有保存位置,判斷最後位置並不是好的方法。
但以結果回推,最大子陣列以外的元素加總就是最小子陣列。
因此取得最小子陣列的總和,用整個數列的總和去減它,也可以得到最大子陣列的總和。
如果最小子陣列在陣列之間,那最大子陣列會是環形接起的。
因此所求在「陣列(沒有環)子陣列最大總和」或「總合減陣列子陣列最小總和」中,兩者取大。
但這樣子有一個狀況,如果是遞減數列,最小總和會是全體元素集合,而總和減去它會是 0 ,也比最大總和還大。
因此,如果 總和 == 子陣列最小總和 ,那只能回傳前者。

Python

class Solution:
def maxSubarraySumCircular(self, A: List[int]) -> int:
min_now = max_now = A[0]
nmin = nmax = nsum = A[0]
for num in A[1:]:
min_now = min(num, min_now + num)
nmin = min(nmin, min_now)
max_now = max(num, max_now + num)
nmax = max(nmax, max_now)
nsum += num
if nsum == nmin: return nmax
return max(nsum - nmin, nmax)

C#

public class Solution {
public int MaxSubarraySumCircular(int[] A) {
int min_now = A[0]; int max_now = A[0];
int nmin = A[0]; int nmax = A[0]; int nsum = A[0];
for(int i = 1; i < A.Length; i++)
{
int num = A[i];
min_now = (min_now + num < num) ? min_now + num : num;
nmin = nmin < min_now ? nmin : min_now;
max_now = (max_now + num > num) ? max_now + num : num;
nmax = nmax > max_now ? nmax : max_now;
nsum += num;
}
if(nsum == nmin){return nmax;}
return (nmax > nsum - nmin) ? nmax : nsum - nmin;
}
}

Leetcode題解 Python & C#:五月挑戰DAY14 Implement Trie (Prefix Tree)

要實作 trie ,有 insert, search 和 startsWith 的功能。

trie 很實用,但在 algorithm 中,不太好用上這樣的方法解題。昨日在學 cisco ,就使用 trie 來加速指今輸入。

由於 Leetcode 有教學,這裡就不多說如何解題。(如果懂 trie 是怎樣的型態就會知道要怎麼寫)

Python

class Trie:

def __init__(self):
"""
Initialize your data structure here.
"""
self.trie = {}


def insert(self, word: str) -> None:
"""
Inserts a word into the trie.
"""
now = self.trie
for char in word:
if char not in now: now[char] = {}
now = now[char]
now['exist'] = True


def search(self, word: str) -> bool:
"""
Returns if the word is in the trie.
"""
now = self.trie
for char in word:
if char not in now: return False
now = now[char]
return 'exist' in now


def startsWith(self, prefix: str) -> bool:
"""
Returns if there is any word in the trie that starts with the given prefix.
"""
now = self.trie
for char in prefix:
if char not in now: return False
now = now[char]
return True

C#

class TrieNode
{
public bool End;
public Dictionary Next;
public TrieNode() {
Next = new Dictionary();
End = false;
}
}

public class Trie {
private TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root = new TrieNode();
}

/** Inserts a word into the trie. */
public void Insert(string word) {
TrieNode now = root;
foreach(char c in word)
{
if(!(now.Next.ContainsKey(c)))
{
now.Next[c] = new TrieNode();
}
now = now.Next[c];
}
now.End = true;
}

/** Returns if the word is in the trie. */
public bool Search(string word) {
TrieNode now = root;
foreach(char c in word)
{
if(now.Next.ContainsKey(c))
{
now = now.Next[c];
}
else
{
return false;
}
}
return now.End;
}

/** Returns if there is any word in the trie that starts with the given prefix. */
public bool StartsWith(string prefix) {
TrieNode now = root;
foreach(char c in prefix)
{
if(now.Next.ContainsKey(c))
{
now = now.Next[c];
}
else
{
return false;
}
}
return true;
}
}

Leetcode題解 Python & C#:五月挑戰DAY13 Remove K Digits

給一個非負整數以 string 型態表示,移除 k 個數字,讓數字變成最小。
如果把它作DP,會超時,因為 num 的長度最多到10002。
不用 DP ,我們要來想想要如何從頭開始找起,並作篩選。
如果要讓結果是最小,那可想像會在一個範圍中,保留較小的,取走較大的。如此運作,直到挑出 k 個。
仔細觀察,被挑選過後的樣子。
“1432219”, 3 -> “1219”
“4321234”, 2 -> “21234”
“10200”, 1 -> “200”
如果要排成最小,那麼會讓數列盡可能朝著「非升序排列」,也就是若左方大於右方,左方就會被挑走。
若左右相等或小於,則住右方移動。

“1432219”為例:
“0” # 預設
“01” # 左 < 右
“014” # 左 < 右
“014” “3” -> “01” “3” 挑走左方-> “013” (K = 2)
“013” “2” -> “01” “2” -> “012” (K = 1)
“0122” # 左 == 右
“0122” “1” -> “012” “1” (K = 0,將剩下的加入) -> “01219”
選完之後,先檢查 k 值,因為是 「非升序排列」,最後面的會是比較大的,若 k 有剩,從最後再拿走 k 個。
再去除前導零(leading zero),用 lstrip(“0”)。
最後檢查答案是否為 “” ,是則回傳 “0” ,否則 result。

Python

class Solution:
def removeKdigits(self, num: str, k: int) -> str:
result = "0"
for i, c in enumerate(num):
while c < result[-1] and k:
result = result[:-1]
k -= 1
if k == 0:
result += num[i:]
break
result += c

if k: result = result[:-k]
result = result.lstrip("0")
if result: return result
return "0"

C#

public class Solution {
public string RemoveKdigits(string num, int k) {
string result = "0";
int n = result.Length;
for(int i = 0; i < num.Length; i++)
{
n = result.Length;
while(num[i] 0)
{
result = result.Substring(0, n - 1);
k -= 1;
n -= 1;
}
if(k == 0)
{
n = num.Length;
result = result + num.Substring(i, n - i);
break;
}
result += num[i];
}
n = result.Length;
if(k > 0)
{
result = result.Substring(0, n - k);
}
result = result.TrimStart('0');
if(result.Length == 0)
{
return "0";
}
return result;
}
}

Leetcode題解 Python & C#:五月挑戰DAY12 Single Element in a Sorted Array

給一個經排序後的數列,找出其中只出現一次的元素。(其他重複二次)
被限制在 時間複雜度 O(log n) 和 空間複雜度 O(1),解決。
說到 O(log n) time ,經典的方式是二分法。
由於其他都出現二次,可想 pos % 2 == 0 的位置會等於其右方的,除非有出現單次的元素在左方。
用二分搜尋法,計算的 m 在比較的時候要用 % 2 修正位置,去讓奇數位比其右方的位罝。
這裡的 r 跟往不同減了 1 ,是因應「超界」做處理,如果出現單次的在最右方,那麼一般的二分搜尋法,會在比較時比 nums[n] 。如果左方都出現兩次,那最後 r 值不變,指向最後一個為單一的元素。

Python

class Solution:
def singleNonDuplicate(self, nums: List[int]) -> int:
l, r = 0, len(nums)-1
while l < r:
m = (r-l)//2 + l
if nums[m-m%2] == nums[m+1-m%2]:
l = m+1
else:
r = m
return nums[r]

C#

public class Solution {
public int SingleNonDuplicate(int[] nums) {
int l = 0; int r = nums.Length-1;
while(l < r)
{
int m = (r-l)/2 + l;
if(nums[m - m%2] == nums[m - m%2 + 1])
{
l = m + 1;
}
else
{
r = m;
}
}
return nums[r];
}
}

Leetcode題解 Python:Trapping Rain Water

給一個代表(牆)高度的非負整數數列,而下雨之後,水會積在低窪處,回傳水的體積。
水會積多少,取決於左右方的最高高度取較低者減當前高度,若小於零代表沒有積水。
這樣子很好寫出正確寫法,但是超時的問題得考慮進去。
如果每次都重找最大值,時間複雜度會是 O(n**2),得想辦法拉到 O(n) 解決。
最大值一定要重找嗎?如果從左方開始,左方的最大值會一路遞增,而右方會一路遞減。過了數列的最大值,消漲就會互換。
也就是說,若能使用 TwoPointer 去從左右方往中間靠近,邊找邊算所求,就能把時間複雜度用在 O(n)。
當然可以做到,官方也有範例可供參考。

Python(基本)

class Solution:
def trap(self, height: List[int]) -> int:
result = 0
n = len(height)
for i in range(1,n-1):
lmax = max(height[:i])
rmax = max(height[i:])
result += max(min(lmax, rmax) - height[i], 0)
return result

Python(進階)

class Solution:
def trap(self, height: List[int]) -> int:
result = 0
li, lmax = 0, 0
ri, rmax = len(height)-1, 0
while li <= ri:
if lmax > rmax:
if height[ri] > rmax: rmax = height[ri]
else: result += rmax - height[ri]
ri -= 1
else:
if height[li] > lmax: lmax = height[li]
else: result += lmax - height[li]
li += 1
return result

Leetcode題解 Python & C#:五月挑戰DAY11 Flood Fill

給一個二維陣列,要把[sr][sc]位置上的顏色(值)換成 newColor,像是油漆桶工具,回傳取代過後的二維陣列。

這題是「遞迴函數」的基本題。

遇到在這種二維陣列的題型,選一個點上下左右找,為了避免同一位置重複找,用hash類紀錄找過的位置作檢查。

如果用 for迴圈 要思考整體是由左上至右下找,會不會因此有漏網之魚,如果有,還是得回歸 遞迴函數。

Python

class Solution:
def floodFill(self, image: List[List[int]], sr: int, sc: int, newColor: int) -> List[List[int]]:
rows, cols = len(image), len(image[0])
oldColor = image[sr][sc]

@lru_cache(None)
def search(y, x):
if y < 0 or x < 0 or y == rows or x == cols:
return
if image[y][x] == oldColor:
image[y][x] = newColor
search(y-1, x)
search(y+1, x)
search(y, x-1)
search(y, x+1)

if oldColor != newColor: search(sr, sc)
return image

C#

public class Solution {
int rows;
int cols;
public int[][] FloodFill(int[][] image, int sr, int sc, int newColor) {
var oldColor = image[sr][sc];
rows = image.Length;
cols = image[0].Length;
if(oldColor != newColor)
{
Fill(image, sr, sc, newColor, oldColor);
}
return image;
}

public void Fill(int[][] image, int y, int x, int newColor, int oldColor)
{
if(y >= 0 && y = 0 && x < cols && image[y][x] == oldColor)
{
image[y][x] = newColor;
Fill(image, y-1, x, newColor, oldColor);
Fill(image, y+1, x, newColor, oldColor);
Fill(image, y, x-1, newColor, oldColor);
Fill(image, y, x+1, newColor, oldColor);
}
}
}

Leetcode題解 Python:Combination Sum II

給一個數列 candidates,要從中選元素加總等於target,每一個元素只能選一次,回傳有幾種不同的組合。
由於要不重複的組合,所以用 hash 類可避免重複,又或者以計數每個各(不重複)元素的使用次數去推組合。
前不久說這類在「一數列中」的題目會有一些特點去知道該用什麼方法去解。
這一題則是「實際組合」,因此得保存用了什麼數字去嘗試,變成由上到下。(沒有用hash比較)
元素不必相鄰,而且都是正整數,但元素可能會出現重複,練習抓這些規則可以幫助順利寫出解法。
要撈出組合,有好幾種方法,我一開始使用 backtrace,不過這不好,backtrace是相對沒效率的方法,光是移出移回,每層就多耗了二步在搜索上。
將 backtrace 攤平後,用 stack 一層層找。
先 candidates.sort(),因為由小至大,加上避免重複,所以每個可能的組合只能往最後一個選擇位置的右方去挑選。
如 [1,2,3,4], target=3 中,若目前選到 1 ,則接下來只可以往右方,從 2 開始做選取。(若能往左,只會多出很多重複)
不過這沒辦法做到完全不重複,要是在 [1,1,1,1], t=3,[1,1,1]會出現兩次。不過這方法可以明顯加速找組合。
按照這樣的規則,再加上要比較的是總合,所以stack初始要放入的會是,當前總合(int)與每個選取位置(List)。

Python

class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
result = set()
candidates.sort()
n = len(candidates)
stack = [[candidates[i], [i]] for i in range(n)]
while stack:
total, index = stack.pop()
if total == target:
result.add(tuple(candidates[i] for i in index))
continue
for i in range(index[-1]+1, n):
new_total = total + candidates[i]
if new_total > target: break
stack.append([new_total, index+[i]])
return result

Leetcode題解 Python:Next Permutation

給一個組合,要回傳(按各組合由小到大)下一個組合,如果是最大的組合,則回傳最小的組合。

這一題是一種數學題,如果數學想通了就很好解,沒想通,那就只好琢磨到理解為止。

這裡要講如何理解。以 [4,2,3,1] 排組為例。

如果要找到下一個,就只要從右方移一個(從尾開始),讓該組合的大小略增即可。

那要跟誰換呢?從右方開始,如果該位置的值比左方的大,那就是目標位置。
如果左方皆 >= 右方,那代表該組合是最大的。如 [4,3,2,1] [5,1,1]
([4,【2,3】,1])※選左右哪方當目標位置都可以。這裡用左右,必免混淆。

接著從目標位置右及右方開始選一個大於目標位置左的最小值,二者互相交換。可以想成進位。
([4,【2(左),3(右)】,1] -> [4,2,【3,1】] -> [4,2,【3】,1] -> [4,3,2,1])

換完之後,目標位置右方,應該要變成小到大的排序﹐才會是下一個組合。
([4,【3】,2,1] -> [4,3,1,2])

在換之前,目標位置右及右方已經是非升序排列(左>=右),若把它看成一個排列組合,那此時的右方排列會是該排列組合中最大的。

最大的組合,只要反序,就會回到最小的組合。

若碰到 [4,3,2,1],目標位置會在 4 的前方,沒辦法交換,所以直接把數列反序即可。

而交換之後,右方的非升序排列的性質依舊不變,因此只要把右方反序,就會變成右方最小的組合。

反序之後的組合就是題目所求。

流程

Python

class Solution:
def nextPermutation(self, nums) -> None:
n = len(nums)
si = n - 1
while si > 0 and nums[si] <= nums[si-1]:
si -= 1
while si:
for j in range(n-1, si-1, -1):
if nums[j] > nums[si-1]:
nums[si-1], nums[j] = nums[j], nums[si-1]
nums[si:] = nums[si:][::-1]
return
si -= 1
nums[si:] = nums[si:][::-1]

Leetcode題解 Python & C#:五月挑戰DAY10 Find the Town Judge

有 N 個人(編號 1,…,N),若所有人信任第x個,而且第x個不信任任何人,若只有一個符合這樣條件的人,回傳其編號。
把條件一一拿來解讀:
他必須被所有人信任,所以那人得到的信任會是 N-1 個。
他不會信任任何人,所以在 trust[i][0] 的人都不是候選人。
於是在遍歷 trust 的同時,要去統計每個編號的被信任數(t[1]),並刪除候選人(t[0])。
剩下的候選人是不相信任何人的人,再剔除其被信任數不為 N-1 的。
最後符合條件1,2,若只剩一個即符合條件3,回傳其編號。若無則-1。

Python
class Solution:
def findJudge(self, N: int, trust: List[List[int]]) -> int:
from collections import defaultdict
beTrust = defaultdict(int)
candidates = set(range(1,N+1))
for t in trust:
beTrust[t[1]] += 1
if t[0] in candidates: candidates.discard(t[0])
for c in list(candidates):
if beTrust[c] != N-1:
candidates.discard(c)
if len(candidates) == 1:
return candidates.pop()
return -1

C#

public class Solution {
public int FindJudge(int N, int[][] trust) {
var candidates = new HashSet();
var beTrusted = new Dictionary();
for(int i=1; i <= N; i++)
{
candidates.Add(i);
beTrusted[i]=0;
}
foreach(var t in trust){
beTrusted[t[1]] += 1;
if(candidates.Contains(t[0]))
{
candidates.Remove(t[0]);
}
}
int result = -1;
foreach(var c in candidates)
{
if(beTrusted[c] == N-1)
{
if(result==-1)
{
result = c;
}
else
{
return -1;
}
}
}
return result;
}
}

Leetcode題解 Python:Number of Ways of Cutting a Pizza

給一塊矩陣 pizza,要用k-1刀切出 k 塊,每塊上面都要有至少一個 ‘A'(蘋果),每刀只能切一直線,而且切完會拿走右方或是上方的那一塊。要回傳有幾種切法。
這算hard中偏簡單的問題,結果我在這題犯小錯卡很久,但正確的主體寫法一下子就寫好了。
因為這題給的拿取規則,使我們隨著每一刀而往右下移動,不用考慮前方的結果。(前不影響後)
因此,使用 dfs(深度優先),以[y][x]做為dp的依據是沒問題的。(數字很大要取餘也是dp題的特色)
有刀數限制,再組合剛才的dp,組合成 dfs(y, x, pieces)的遞迴函數,而終止條件是 pieces == k。(可能有重複參數出現,可以使用 memo 紀錄)

首先拿到一大塊之後,可以直切,也可以橫切,選一個間隔作為下刀點。此時可分左右或上下。
如果被取走的那塊有蘋果,就可以往下一刀進行。dfs(y1, x1, pieces+1)
所以,總要判斷給定的區間內pizza是否有 1 個 “A”,寫成一個函數。(可能有重複參數出現,可以使用 memo 紀錄)
dfs 來到當 pieces == k ,就以目前的範圍找是否有蘋果,有則回傳 1 ,沒有則 0。

Python

class Solution:
def ways(self, pizza: List[str], k: int) -> int:
from functools import lru_cache
rows, cols = len(pizza), len(pizza[0])
@lru_cache(None)
def hasApple(y0,y1,x0,x1):
for y in range(y0,y1):
for x in range(x0,x1):
if pizza[y][x] == "A": return True
return False
@lru_cache(None)
def dfs(y, x, pieces):
if pieces == k:
if hasApple(y,rows, x,cols):
return 1
else: return 0

ans = 0
# h cut
for i in range(y, rows-1):
if hasApple(y, i+1, x, cols):
ans += dfs(i+1, x, pieces+1)
# v cut
for i in range(x, cols-1):
if hasApple(y, rows, x, i+1):
ans += dfs(y, i+1, pieces+1)
return ans % (10**9+7)
return dfs(0, 0, 1)

Leetcode題解 Python:Count Triplets That Can Form Two Arrays of Equal XOR

給一列整數數列,問兩相鄰的子數列XOR總值相等的個數。
今天就差這一個沒有解出來。QQ
這種在「一數列」中取「組合」做「比較」的題目,都有一個解題方向,就是用dp的觀念去保存比較值。
比較值,比較值,比較值,因為很重要,所以說三次。而且比較也要朝著在hash類內的做處理。

這一題的比較值,很明顯地就是XOR總值。
在計算XOR總值時,會選定一個區間,而這區間可能會重複計算,因此可用hashtable,dict() 保存。
因為題目所求,兩了數列一定要相鄰,如果前面是[i,j-1],後面一定從 j 開始才有符合題目所求的可能性。
而結尾落在 j-1 位置的可能有好幾個,因此DP的會到二維,DP[pos][xor],這樣才能充分保存比較值。
DP[pos][xor]是在以pos-1位置結尾的XOR之出現次數,若同值出現2次以上的話,計數也該加計相同次數。
流程
用雙迴圈去找出所有XOR總值,並順便找出在前方結尾即DP[pos]是否出現xor,若有則加DP[pos][xor],
然後把目前的XOR總值加到DP去。

Python

class Solution:
def countTriplets(self, arr: List[int]) -> int:
from functools import lru_cache
from collections import defaultdict
@lru_cache(None)
def xor(si, ei):
if si == ei: return 0
ans = arr[si] ^ xor(si+1, ei)
return ans

result = 0
memo = defaultdict(dict)
n = len(arr)
for i in range(n):
for j in range(i,n):
val = xor(i, j+1)
if val in memo[i]:
result += memo[i][val]
memo[j+1][val] = memo[j+1].get(val,0) + 1

return result

Leetcode題解 Python:Substring with Concatenation of All Words

給一字串 s 跟 words 字串串列,問哪些位置可以找到全部在words中的詞只出現一次。
思路很簡單,想辦法在一個 n 的時間讓全部都搜過。(用搜尋法倒很隨意)
有一個條件可以利用,那就是每個 word 都是一樣的長度,因此不必逐字元去判斷,可以拿詞去匹配。
於是產生原型的逐詞匹配,但這還不夠!速度還太慢。
每當遇到這種匹配題型,盡可能不要反覆同一位置重複匹配。之前有提到一個方式 KMP,若全照用,這裡可能會在 prefix table 產生太多組合 。
因為 words 的詞沒有指定先後順序,所以要是當前位置沒有匹配到,就可把最前面匹配的放回待匹配的隊伍,繼續匹配,若都沒有可匹配,那就往下移動。

Python(逐詞匹配)

class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
result = []
if not s or not words: return result
counter = Counter(words)
nowCounter = counter.copy()
i = 0
m = len(words[0])
n = len(s)
n2 = len(words)
while i <= n-m*n2:
nowCounter = counter.copy()
for j in range(n2):
word = s[i+j*m:i+m+j*m]
if word in nowCounter:
if nowCounter[word] > 0:
nowCounter[word] -= 1
if nowCounter[word] == 0:
del nowCounter[word]
else:
break
if not nowCounter: result.append(i)
i += 1
return result

Python(逐詞匹配2)

class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
result = []
if not s or not words: return result
wl = len(words[0])
wn = len(words)
counter = Counter(words)
select = list()
n = len(s)
def search(pos, wi):
if pos+wl > n:
while select: counter[select.pop(0)] += 1
return
word = s[pos: pos+wl]
while select and (word not in counter or counter[word] == 0):
counter[select.pop(0)] += 1
wi -= 1
if word in counter and counter[word] > 0:
counter[word] -= 1
select.append(word)
if wi+1 == wn: result.append(pos - wi*wl)
search(pos+wl, wi+1)
else:
search(pos+wl, wi)
for i in range(wl):
search(i, 0)
return result