Skip to content

Commit 216df1b

Browse files
committed
Add strings hard questions
1 parent bb93f3b commit 216df1b

File tree

7 files changed

+561
-0
lines changed

7 files changed

+561
-0
lines changed

2024/meta/prep.py

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,28 @@ def myPow(self, x: float, n: int) -> float:
180180
res = self.myPow(x, n//2)
181181
return res ** 2
182182

183+
########## 384. Shuffle an Array ##########
184+
import random
185+
class Solution:
186+
def __init__(self, nums: List[int]):
187+
self.original = nums
188+
self.cur = nums.copy()
189+
190+
def reset(self) -> List[int]:
191+
self.cur = self.original.copy()
192+
return self.cur
193+
194+
def shuffle(self) -> List[int]:
195+
"""
196+
Time O(n) | O(1) Extra Space
197+
"""
198+
n = len(self.cur)
199+
for i in range(n-1, 0, -1):
200+
random_idx = random.randint(0, i)
201+
self.cur[i], self.cur[random_idx] = self.cur[random_idx], self.cur[i]
202+
return self.cur
183203

204+
########## 295. Find Median from Data Stream ##########
184205
import heapq
185206
class MedianFinder:
186207
"""
@@ -856,6 +877,90 @@ def dfs(course, mapping, visiting):
856877
return False
857878
return True
858879

880+
############ 346. Moving Average from Data Stream ############
881+
from collections import deque
882+
class MovingAverage:
883+
884+
def __init__(self, size: int):
885+
self.window = deque()
886+
self.size = size
887+
self.sum = 0
888+
889+
def next(self, val: int) -> float:
890+
self.window.append(val)
891+
self.sum += val
892+
if len(self.window) > self.size:
893+
self.sum -= self.window.popleft()
894+
895+
return self.sum / len(self.window)
896+
897+
############ 227. Basic Calculator II ############
898+
OPERANDS = set('+-*/')
899+
class Solution:
900+
def calculate(self, s: str) -> int:
901+
"""
902+
We use a stack to keep track of numbers and intermediate results.
903+
We iterate through the string, building up numbers digit by digit.
904+
When we encounter an operator or reach the end of the string, we perform the operation based on the previous sign:
905+
906+
For '+', we push the number onto the stack.
907+
For '-', we push the negative of the number.
908+
For '*', we pop the last number, multiply it by the current number, and push the result.
909+
For '/', we pop the last number, divide it by the current number (using integer division), and push the result.
910+
911+
912+
After processing all characters, we sum up all numbers in the stack to get the final result.
913+
"""
914+
stack = [] # only stores intermediary value result, in the end res == sum(stack)
915+
num = 0
916+
lastsign = '+'
917+
918+
for i, char in enumerate(s):
919+
if char.isdigit():
920+
num = num * 10 + int(char)
921+
922+
if char in OPERANDS or i == len(s)-1:
923+
# when a new sign is encountered, we flush the current number
924+
# and immediately compute last expression consisting of stack[-1] OPERAND curnum
925+
# and reset curnum to zero, and update last sign
926+
if lastsign == '+':
927+
stack.append(num)
928+
elif lastsign == '-':
929+
stack.append(-num)
930+
elif lastsign == '*':
931+
stack.append(stack.pop() * num)
932+
elif lastsign == '/':
933+
stack.append(int(stack.pop() / num)) # caveat: don't use //, because // rounds UP when result is negative
934+
935+
lastsign = char
936+
num = 0
937+
938+
return sum(stack)
939+
940+
941+
############ 16. 3Sum Closest ############
942+
class Solution:
943+
def threeSumClosest(self, nums: List[int], target: int) -> int:
944+
res = float('inf')
945+
diff = float('inf')
946+
nums.sort()
947+
n = len(nums)
948+
for i in range(n-2):
949+
j, k = i+1, n-1
950+
while j < k:
951+
s = nums[i] + nums[j] + nums[k]
952+
d = s-target
953+
if abs(d) < diff:
954+
res = s
955+
diff = abs(d)
956+
if d > 0:
957+
k -= 1
958+
elif d < 0:
959+
j += 1
960+
else:
961+
return target
962+
return res
963+
859964
############# 116. Populating Next Right Pointers in Each Node II #############
860965

861966
class Solution:
@@ -892,6 +997,73 @@ def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
892997
node.next = queue[0]
893998
return root
894999

1000+
############# 727. Minimum Window Subsequence ############
1001+
class Solution:
1002+
def minWindow(self, s1: str, s2: str) -> str:
1003+
"""
1004+
dp[i][j] stores the LARGEST starting index in s1 of the substring
1005+
where s2 has length i and the window ends at j-1 (s2[:i] is totally included in first j characters s1[:j])
1006+
1007+
So dp[i][j] would be:
1008+
if s2[i - 1] == s1[j - 1], this means we could borrow the start index from dp[i - 1][j - 1] to make the current substring valid;
1009+
else, we only need to borrow the start index from dp[i][j - 1] which could either exist or not.
1010+
1011+
Finally, go through the last row to find the substring with min length and appears first.
1012+
"""
1013+
n, m = len(s1), len(s2)
1014+
1015+
# Initialize the DP array, m+1 rows and n+1 columns
1016+
dp = [[-1] * (n + 1) for _ in range(m + 1)] # dp[i][j] stores the LARGEST starting index in s1 of the substring
1017+
1018+
# when s2 is empty string, the min window in s1 that includes it is still empty string
1019+
# by definition, dp[i][j] stores largest index of min window that ends at j-1 (or first j chars s[:j])
1020+
# so the min window that includes empty string AND ends at j-1 starts at (s1[j:j] == "")
1021+
for j in range(n + 1):
1022+
dp[0][j] = j # s1[j:j] == "" includes s2[:0] == ""
1023+
1024+
# Fill the dp array
1025+
for i in range(1, m + 1):
1026+
for j in range(1, n + 1):
1027+
# s1[dp[i-1][j-1]:j-1] includes a valid subsequence of s2[:i-1] && s1[j] == s2[i]
1028+
# => s1[dp[i][j]:j] includes a valid subsequence of s2[:i]
1029+
if s2[i - 1] == s1[j - 1]: # current matching s1[j] == s2[i] belongs to same window as s1[j-1] == s2[i-1]
1030+
dp[i][j] = dp[i - 1][j - 1]
1031+
else: # if no match, need stricter condition: find s2[:i] in first j-1 characters of s1. The start index is same if found
1032+
dp[i][j] = dp[i][j - 1]
1033+
1034+
1035+
# Now find the minimum length window
1036+
start, length = 0, n + 1
1037+
for j in range(1, n + 1):
1038+
if dp[m][j] != -1:
1039+
if j - dp[m][j] < length:
1040+
start = dp[m][j]
1041+
length = j - dp[m][j]
1042+
1043+
return "" if length == n + 1 else s1[start : start + length]
1044+
1045+
############# 543. Diameter of Binary Tree ############
1046+
class Solution:
1047+
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
1048+
res = 0
1049+
1050+
def dfs(node):
1051+
"""
1052+
DFS returns the depth of the tree from current root
1053+
Each recursive call updates res with two-way path passing root node
1054+
Leaf node: 0, None node: -1, so adding one to none equals zero (don't contribute to path length)
1055+
"""
1056+
nonlocal res
1057+
if not node:
1058+
return -1
1059+
1060+
left, right = dfs(node.left), dfs(node.right)
1061+
res = max(res, 2+left+right) # maxlen duo-way path passing current node
1062+
1063+
return 1 + max(left, right) # maxlen single-way path from deepest leave up to current node
1064+
1065+
dfs(root)
1066+
return res
8951067

8961068
############# 658. Find K Closest Elements ############
8971069
class Solution:
@@ -923,6 +1095,43 @@ def findClosestElements(self, arr: List[int], k: int, x: int) -> List[int]:
9231095
left -= 1
9241096
return arr[left:right]
9251097

1098+
############# 88. Merge Sorted Array ############
1099+
class Solution:
1100+
def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
1101+
"""
1102+
Do not return anything, modify nums1 in-place instead.
1103+
"""
1104+
i, j = m-1, n-1
1105+
k = len(nums1)-1
1106+
1107+
while i >= 0 and j >= 0:
1108+
if nums1[i] > nums2[j]:
1109+
nums1[k] = nums1[i]
1110+
i -= 1
1111+
else:
1112+
nums1[k] = nums2[j]
1113+
j -= 1
1114+
1115+
k -= 1
1116+
1117+
while j >= 0:
1118+
nums1[k] = nums2[j]
1119+
k -= 1
1120+
j -= 1
1121+
1122+
############ 398. Random Pick Index ############
1123+
from collections import defaultdict
1124+
import random
1125+
class Solution:
1126+
1127+
def __init__(self, nums: List[int]):
1128+
self.d = defaultdict(list)
1129+
for i, num in enumerate(nums):
1130+
self.d[num].append(i)
1131+
1132+
def pick(self, target: int) -> int:
1133+
return random.choice(self.d[target])
1134+
9261135
############ 865. Smallest Subtree with all the Deepest Nodes ############
9271136
############# 1123. Lowest Common Ancestor of Deepest Leaves ############
9281137
class Solution:
@@ -966,3 +1175,108 @@ def threeSumClosest(self, nums: List[int], target: int) -> int:
9661175
else:
9671176
return target
9681177
return res
1178+
1179+
1180+
############# 79. Word Search ############
1181+
DIR = {(0, 1), (0, -1), (-1, 0), (1, 0)}
1182+
VISITED_SENTINEL = '#' # any character that will not appear in word charset
1183+
class Solution:
1184+
def exist(self, board: List[List[str]], word: str) -> bool:
1185+
m, n = len(board), len(board[0])
1186+
def backtrack(x, y, wordi):
1187+
if wordi == len(word):
1188+
return True
1189+
if not (0 <= x < m and 0 <= y < n):
1190+
return False # cannot match if out of bound
1191+
if board[x][y] != word[wordi]:
1192+
return False
1193+
1194+
original = board[x][y]
1195+
board[x][y] = VISITED_SENTINEL
1196+
1197+
for dx, dy in DIR:
1198+
nx, ny = x+dx, y+dy
1199+
if backtrack(nx, ny, wordi+1):
1200+
board[x][y] = original
1201+
return True
1202+
1203+
board[x][y] = original
1204+
return False
1205+
1206+
for i in range(m):
1207+
for j in range(n):
1208+
if backtrack(i, j, 0):
1209+
return True
1210+
return False
1211+
1212+
1213+
############# 791. Custom Sort String ############
1214+
from collections import Counter
1215+
class Solution:
1216+
def customSortString(self, order: str, s: str) -> str:
1217+
freq = Counter(s)
1218+
res = ""
1219+
for char in order:
1220+
res += char * freq[char]
1221+
del freq[char]
1222+
1223+
for char, f in freq.items():
1224+
res += char * f
1225+
return res
1226+
1227+
############# 30. Substring with Concatenation of All Words ############
1228+
# Solution with illustration https://leetcode.com/problems/substring-with-concatenation-of-all-words/solutions/1753357/clear-solution-easy-to-understand-with-diagrams-o-n-x-w-approach/
1229+
class Solution:
1230+
def findSubstring(self, s: str, words: List[str]) -> List[int]:
1231+
"""
1232+
Let wordlen = len(words[0])
1233+
Time O(len(s) * wordlen)
1234+
Use two hashmaps + two pointers
1235+
- `need` one hashmap to count all frequencies of each word in words
1236+
- `window` one to count current each substring's frequency in current window s[left:left+wordlen*len(words)]
1237+
- `matched_substrs` count how many words in window has been matched
1238+
1239+
We consider all such windows starting from 0, 1, 2, 3, ... wordlen-1, each time moving left/right pointer by wordlen.
1240+
This problem effectively is a combination of wordlen sliding window problems.
1241+
i ---> i+w ---> i+2w ----> i+3w ----> i+4w
1242+
(i+1) ---> (i+1)+w ---> (i+1)+2w ----> (i+1)+3w ----> (i+1)+4w
1243+
(i+2) ---> (i+2)+w ---> (i+2)+2w ----> (i+2)+3w ----> (i+2)+4w
1244+
(i+3) ---> (i+3)+w ---> (i+3)+2w ----> (i+3)+3w ----> (i+3)+4w
1245+
1246+
If there's a mismatch at right pointer, we move left pointer to right of right pointer because
1247+
any window that includes the mismatch is invalid window. Also we need to reset counter to zero
1248+
1249+
If advancing right pointer causes excess of substring in current window, we shrink the window size by wordlen and
1250+
update substring count in `window` hashmap accordingly
1251+
"""
1252+
need = Counter(words) # substr => frequency in `words`
1253+
res = []
1254+
wordlen = len(words[0])
1255+
1256+
for k in range(wordlen):
1257+
window = Counter() # window substr => frequency in s[left:right], right == i + wordlen*len(words)
1258+
left = k
1259+
matched_substrs = 0 # sum of all frequencies in `window`. matched_substrs == sum(window.values())
1260+
for right in range(left, len(s), wordlen):
1261+
if right + wordlen > len(s): # out of bounds, cannot form window
1262+
break
1263+
nextsubstr = s[right:right+wordlen]
1264+
if nextsubstr in need: # matched
1265+
window[nextsubstr] += 1
1266+
matched_substrs += 1
1267+
while window[nextsubstr] > need[nextsubstr]: # matched, but excess, need move left pointer till no excess
1268+
oldsubstr = s[left:left+wordlen]
1269+
window[oldsubstr] -= 1
1270+
matched_substrs -= 1
1271+
left += wordlen
1272+
if matched_substrs == len(words): # yay! we found a permutation of words
1273+
res.append(left)
1274+
window[s[left:left+wordlen]] -= 1
1275+
matched_substrs -= 1
1276+
left += wordlen
1277+
else: # mismatch, reset counter, and move left
1278+
window.clear()
1279+
matched_substrs = 0
1280+
left = right + wordlen
1281+
1282+
return res
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from collections import Counter
2+
class Solution:
3+
def minWindow(self, s1: str, s2: str) -> str:
4+
"""
5+
dp[i][j] stores the LARGEST starting index in s1 of the substring
6+
where s2 has length i and the window ends at j-1 (s2[:i] is totally included in first j characters s1[:j])
7+
8+
So dp[i][j] would be:
9+
if s2[i - 1] == s1[j - 1], this means we could borrow the start index from dp[i - 1][j - 1] to make the current substring valid;
10+
else, we only need to borrow the start index from dp[i][j - 1] which could either exist or not.
11+
12+
Finally, go through the last row to find the substring with min length and appears first.
13+
"""
14+
n, m = len(s1), len(s2)
15+
16+
# Initialize the DP array, m+1 rows and n+1 columns
17+
dp = [[-1] * (n + 1) for _ in range(m + 1)] # dp[i][j] stores the LARGEST starting index in s1 of the substring
18+
19+
# when s2 is empty string, the min window in s1 that includes it is still empty string
20+
# by definition, dp[i][j] stores largest index of min window that ends at j-1 (or first j chars s[:j])
21+
# so the min window that includes empty string AND ends at j-1 starts at (s1[j:j] == "")
22+
for j in range(n + 1):
23+
dp[0][j] = j # s1[j:j] == "" includes s2[:0] == ""
24+
25+
# Fill the dp array
26+
for i in range(1, m + 1):
27+
for j in range(1, n + 1):
28+
# s1[dp[i-1][j-1]:j-1] includes a valid subsequence of s2[:i-1] && s1[j] == s2[i]
29+
# => s1[dp[i][j]:j] includes a valid subsequence of s2[:i]
30+
if s2[i - 1] == s1[j - 1]: # current matching s1[j] == s2[i] belongs to same window as s1[j-1] == s2[i-1]
31+
dp[i][j] = dp[i - 1][j - 1]
32+
else: # if no match, need stricter condition: find s2[:i] in first j-1 characters of s1. The start index is same if found
33+
dp[i][j] = dp[i][j - 1]
34+
35+
36+
# Now find the minimum length window
37+
start, length = 0, n + 1
38+
for j in range(1, n + 1):
39+
if dp[m][j] != -1:
40+
if j - dp[m][j] < length:
41+
start = dp[m][j]
42+
length = j - dp[m][j]
43+
44+
return "" if length == n + 1 else s1[start : start + length]

0 commit comments

Comments
 (0)