Learn DSA one pattern at a time. For each pattern: read the "use this when" line, try the questions in order (Easy, then Medium, then Hard), then open the solution to compare. Tick what you've solved. Progress is saved in this browser. Want the topic-wise list instead? Open the classic DSA sheet.
The base of everything. 14 patterns, mostly about scanning smarter than a double loop.
Use this when the question says find the max, count, or check all elements, and one pass with a running answer is enough.
Walk the prices once. Keep the lowest price seen so far in `lowest`. At each day, the best profit if you sell today is `price - lowest`. The answer is the largest of those values, so no second loop over earlier days is needed.
Complexity: O(n) time, O(1) extra space
class Solution:
def maxProfit(self, prices: List[int]) -> int:
lowest = float("inf")
best = 0
for price in prices: # one pass, keep a running answer
lowest = min(lowest, price) # cheapest day so far
best = max(best, price - lowest) # profit if we sell today
return bestOne pass keeps two running numbers: `tank` for the current attempt and `total` for the whole circle. When `tank` drops below zero, no station from the current start up to here can work as a start. So the next start must be `i + 1`, and `tank` resets. If `total` is never negative, the last start found is the answer.
Complexity: O(n) time, O(1) extra space
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
total = tank = start = 0
for i in range(len(gas)): # single pass with running totals
gain = gas[i] - cost[i]
total += gain # balance of the whole circle
tank += gain # balance of the current attempt
if tank < 0: # this start failed: restart after i
start = i + 1
tank = 0
return start if total >= 0 else -1Every child needs at least one candy, so start with a list of ones. A left to right pass fixes the rule against the left neighbour: a higher rating gets one more than the left child. A right to left pass fixes the right neighbour and keeps the larger value with `max`. Each pass is a plain traversal, and the sum of the list is the answer.
Complexity: O(n) time, O(n) extra space
class Solution:
def candy(self, ratings: List[int]) -> int:
n = len(ratings)
candies = [1] * n
for i in range(1, n): # pass 1: compare with the left neighbour
if ratings[i] > ratings[i - 1]:
candies[i] = candies[i - 1] + 1
for i in range(n - 2, -1, -1): # pass 2: compare with the right neighbour
if ratings[i] > ratings[i + 1]:
candies[i] = max(candies[i], candies[i + 1] + 1)
return sum(candies)Use this when the array is sorted (or the answer depends on the two ends) and you need a pair, a best pair, or a boundary walk.
Put `l` at the start and `r` at the end. If the sum is too small, only moving `l` right can raise it. If the sum is too big, only moving `r` left can lower it. Every skipped pair is provably useless, so one pass finds the answer.
Complexity: O(n) time, O(1) extra space
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
l, r = 0, len(numbers) - 1
while l < r:
s = numbers[l] + numbers[r]
if s == target:
return [l + 1, r + 1] # the problem is 1-indexed
if s < target: # too small: move l right
l += 1
else: # too big: move r left
r -= 1
return []The array is not sorted, but the two ends give the widest container, so start there. The area is limited by the shorter wall. Moving the taller wall inward can only shrink the width and cannot raise the height limit. So always move the pointer at the shorter wall.
Complexity: O(n) time, O(1) extra space
class Solution:
def maxArea(self, height: List[int]) -> int:
l, r = 0, len(height) - 1
best = 0
while l < r:
best = max(best, min(height[l], height[r]) * (r - l))
if height[l] < height[r]: # the shorter wall limits the area, so move it
l += 1
else:
r -= 1
return bestWater above a bar depends on the lower of the tallest bars on its left and right. Keep `left_max` and `right_max` while the pointers walk inward. Always move the side with the lower bar, because that side's max is the one that limits its water. Add `max - height` for the bar you move past.
Complexity: O(n) time, O(1) extra space
class Solution:
def trap(self, height: List[int]) -> int:
l, r = 0, len(height) - 1
left_max = right_max = water = 0
while l < r:
if height[l] < height[r]: # the lower side decides the water level
left_max = max(left_max, height[l])
water += left_max - height[l]
l += 1
else:
right_max = max(right_max, height[r])
water += right_max - height[r]
r -= 1
return waterUse this when the question asks about every sub-array of exactly size k: max sum, average, or a count.
Compute the sum of the first k numbers once. Then slide: add the new element `nums[i]` and subtract the one that left, `nums[i - k]`. Never re-add k numbers. Keep the best sum and divide by k at the end.
Complexity: O(n) time, O(1) extra space
class Solution:
def findMaxAverage(self, nums: List[int], k: int) -> float:
window = sum(nums[:k])
best = window
for i in range(k, len(nums)):
window += nums[i] - nums[i - k] # add the new element, drop the old one
best = max(best, window)
return best / kTaking k cards from the two ends means the cards you leave form one block in the middle of size n - k. To maximise what you take, minimise what you leave. That block is a fixed-size window, so slide it and track the smallest sum. The answer is the total minus that smallest sum.
Complexity: O(n) time, O(1) extra space
class Solution:
def maxScore(self, cardPoints: List[int], k: int) -> int:
n = len(cardPoints)
size = n - k # cards left behind form a fixed-size window
window = sum(cardPoints[:size])
smallest = window
for i in range(size, n):
window += cardPoints[i] - cardPoints[i - size] # slide by one
smallest = min(smallest, window)
return sum(cardPoints) - smallestA fixed window of size k moves right, and we need its maximum each time. Keep a deque of indices whose values decrease from front to back. Before adding `nums[i]`, pop smaller values from the back, because they can never be a maximum again. The front is the maximum, and it is removed once its index leaves the window.
Complexity: O(n) time, O(k) extra space
from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
dq = deque() # indices; values decrease front to back
out = []
for i, x in enumerate(nums):
while dq and nums[dq[-1]] <= x: # smaller values can never be the max again
dq.pop()
dq.append(i)
if dq[0] <= i - k: # front index slid out of the window
dq.popleft()
if i >= k - 1: # first full window reached
out.append(nums[dq[0]])
return outUse this when the question asks for the longest or shortest contiguous sub-array (or the number of them) that satisfies a condition.
Grow the window by moving `r` and adding `nums[r]`. While the sum is at least the target, the window is valid, so record its length and shrink from the left. All numbers are positive, so shrinking always lowers the sum and the method is safe. Each index enters and leaves the window once.
Complexity: O(n) time, O(1) extra space
from math import inf
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
l = total = 0
best = inf
for r in range(len(nums)):
total += nums[r] # grow right
while total >= target: # window valid: try to shrink
best = min(best, r - l + 1)
total -= nums[l]
l += 1
return 0 if best == inf else bestWe want the longest window that holds at most k zeros. Move `r` right and count each zero added. While the window has more than k zeros, move `l` right, and lower the count when a zero leaves. After that the window is valid, so its length is a candidate for the answer.
Complexity: O(n) time, O(1) extra space
class Solution:
def longestOnes(self, nums: List[int], k: int) -> int:
l = zeros = best = 0
for r, x in enumerate(nums):
if x == 0:
zeros += 1 # grow right
while zeros > k: # window is bad: shrink left
if nums[l] == 0:
zeros -= 1
l += 1
best = max(best, r - l + 1) # window is valid here
return best"Exactly k distinct" is not monotonic, but "at most k distinct" is, so a variable window works for it. Count the sub-arrays with at most k distinct values: for each `r`, every start from `l` to `r` is valid, which adds `r - l + 1`. The answer is `at_most(k) - at_most(k - 1)`. The `count` dict tracks the values inside the window.
Complexity: O(n) time, O(k) extra space
from collections import defaultdict
class Solution:
def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:
def at_most(limit):
count = defaultdict(int)
l = total = 0
for r, x in enumerate(nums):
count[x] += 1 # grow right
while len(count) > limit: # too many distinct values: shrink left
count[nums[l]] -= 1
if count[nums[l]] == 0:
del count[nums[l]]
l += 1
total += r - l + 1 # all windows ending at r that start in [l, r]
return total
return at_most(k) - at_most(k - 1)Use this when you must answer many range-sum questions, or count sub-arrays by their sum (negatives allowed).
Build `prefix` once, where `prefix[i]` is the sum of the first i numbers and `prefix[0]` is 0. The sum of `nums[left..right]` is `prefix[right + 1] - prefix[left]`. The build costs O(n), and every query after that is O(1).
Complexity: O(n) build, O(1) per query, O(n) space
class NumArray:
def __init__(self, nums: List[int]):
self.prefix = [0]
for x in nums:
self.prefix.append(self.prefix[-1] + x) # prefix[i] = sum of the first i numbers
def sumRange(self, left: int, right: int) -> int:
return self.prefix[right + 1] - self.prefix[left] # whole prefix minus the part before leftThe sum of a sub-array ending here equals k when an earlier prefix equals `prefix - k`. Keep a dict `seen` that counts how often each prefix sum has occurred. At each element, add `seen[prefix - k]` to the answer, then record the current prefix. This works with negatives, where a sliding window fails.
Complexity: O(n) time, O(n) extra space
from collections import defaultdict
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
seen = defaultdict(int)
seen[0] = 1 # the empty prefix
prefix = count = 0
for x in nums:
prefix += x # running prefix sum
count += seen[prefix - k] # earlier prefixes that leave a sub-array summing to k
seen[prefix] += 1
return countNegatives rule out a plain sliding window, so build the prefix sums instead. A sub-array from i to j-1 has sum `prefix[j] - prefix[i]`. Keep a deque of start indices whose prefix values increase. When `prefix[j]` is at least k above the front, pop the front and record the length. A larger older prefix can never be a better start than a smaller newer one, so pop those from the back.
Complexity: O(n) time, O(n) extra space
from collections import deque
from math import inf
class Solution:
def shortestSubarray(self, nums: List[int], k: int) -> int:
prefix = [0]
for x in nums:
prefix.append(prefix[-1] + x) # prefix sums handle negatives
dq = deque() # start indices, prefix values increasing
best = inf
for j, p in enumerate(prefix):
while dq and p - prefix[dq[0]] >= k: # sub-array sum is big enough
best = min(best, j - dq.popleft())
while dq and prefix[dq[-1]] >= p: # a smaller, newer start beats these
dq.pop()
dq.append(j)
return -1 if best == inf else bestUse this when you apply many range updates (add v to a[l..r]) and read the final array only once.
Each booking adds `seats` to every flight from `first` to `last`. In `diff`, add `seats` at the start index and subtract it right after the end index. Each booking costs O(1). One running total over `diff` rebuilds the final seat count for every flight.
Complexity: O(n + m) time for m bookings, O(n) space
class Solution:
def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
diff = [0] * (n + 1)
for first, last, seats in bookings:
diff[first - 1] += seats # start adding here (0-indexed)
diff[last] -= seats # stop adding after the last flight
out, running = [], 0
for i in range(n):
running += diff[i] # running total of diff rebuilds the array
out.append(running)
return outEach trip adds passengers at the pickup point and removes them at the drop-off point. Record that in `diff`: plus at `start`, minus at `end`. A running total over the positions is the number of passengers in the car at each point. If it ever passes the capacity, the answer is False.
Complexity: O(n + L) time for n trips and L = 1001 positions, O(L) space
class Solution:
def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
diff = [0] * 1002
for people, start, end in trips:
diff[start] += people # passengers get in
diff[end] -= people # and get out at end
load = 0
for change in diff:
load += change # running total = passengers in the car
if load > capacity:
return False
return TrueScan left to right and flip greedily: if the bit at i is 0 after earlier flips, a flip must start here. Do not touch the k bits. Mark the flip in a difference array: `active` counts flips covering index i, and `diff[i + k]` removes this flip when it ends. A bit is 0 after flips when `nums[i] + active` is even. If a flip would run past the end, the answer is -1.
Complexity: O(n) time, O(n) extra space
class Solution:
def minKBitFlips(self, nums: List[int], k: int) -> int:
n = len(nums)
diff = [0] * (n + 1) # diff[i] = change in the number of active flips at i
flips = active = 0
for i in range(n):
active += diff[i] # flips covering index i
if (nums[i] + active) % 2 == 0: # the bit is 0 now: a flip must start here
if i + k > n:
return -1
flips += 1
active += 1 # range add on [i, i + k - 1] ...
diff[i + k] -= 1 # ... that ends before i + k
return flipsUse this when the question asks for the best contiguous sub-array (max sum, max product, circular) and the array can hold negatives.
At each number there are two choices: extend the run so far, or start fresh at this number. `cur = max(x, cur + x)` picks the better one. If the running sum is negative, it can only hurt, so it is dropped. `best` keeps the largest `cur` seen, and an all-negative array still works.
Complexity: O(n) time, O(1) extra space
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
cur = best = nums[0]
for x in nums[1:]:
cur = max(x, cur + x) # extend the run or restart here
best = max(best, cur)
return bestSame extend-or-restart choice, but a negative number can turn the smallest product into the largest. So keep both `cur_max` and `cur_min` ending at the current index. For each `x`, the new max and min come from `x`, `cur_max * x` and `cur_min * x`. `best` keeps the largest `cur_max`.
Complexity: O(n) time, O(1) extra space
class Solution:
def maxProduct(self, nums: List[int]) -> int:
cur_max = cur_min = best = nums[0]
for x in nums[1:]:
candidates = (x, cur_max * x, cur_min * x) # restart, or extend with max or min
cur_max, cur_min = max(candidates), min(candidates)
best = max(best, cur_max)
return bestThe best sub-array is either a normal one, or it wraps around the ends. A wrapping one equals the total minus the smallest middle sub-array. Run Kadane twice in one pass: once for the max run and once for the min run. The answer is `max(best_max, total - best_min)`, except when all numbers are negative, where the answer is `best_max`.
Complexity: O(n) time, O(1) extra space
class Solution:
def maxSubarraySumCircular(self, nums: List[int]) -> int:
cur_max = cur_min = best_max = best_min = nums[0]
for x in nums[1:]:
cur_max = max(x, cur_max + x) # Kadane for the largest run
best_max = max(best_max, cur_max)
cur_min = min(x, cur_min + x) # Kadane for the smallest run
best_min = min(best_min, cur_min)
if best_max < 0: # all negative: total - best_min would be an empty pick
return best_max
return max(best_max, sum(nums) - best_min)Use this when the question asks have I seen this, how many times, or which pair, and a dict or set gives O(1) lookups.
For each number, the partner we need is `target - x`. Look it up in the dict `seen`, which maps value to index. If it is there, we are done. If not, store the current number and move on. One pass, because the partner always appears earlier or later and is found from the second of the two.
Complexity: O(n) time, O(n) extra space
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
seen = {} # value -> index
for i, x in enumerate(nums):
if target - x in seen: # O(1) lookup of the partner
return [seen[target - x], i]
seen[x] = i
return []Put all numbers in a set for O(1) membership tests. Only start counting at a number `x` where `x - 1` is not in the set, since that is the start of a run. From there, step up while `x + length` is in the set. Each number is visited by at most one run, so the total work is linear and no sort is needed.
Complexity: O(n) time, O(n) extra space
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
values = set(nums) # O(1) membership
best = 0
for x in values:
if x - 1 not in values: # x is the start of a run
length = 1
while x + length in values:
length += 1
best = max(best, length)
return bestFix a point i and count, in a frequency map, how many other points share each direction from it. Two points are on one line through i exactly when their reduced direction `(dx, dy)` is the same. Reduce with `gcd` and fix the sign so equal slopes get equal keys, which avoids float errors. The biggest count plus point i itself is the best for i.
Complexity: O(n^2) time, O(n) extra space
from collections import defaultdict
import math
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
n = len(points)
if n <= 2:
return n
best = 0
for i in range(n):
slopes = defaultdict(int) # direction -> how many points
for j in range(i + 1, n):
dx = points[j][0] - points[i][0]
dy = points[j][1] - points[i][1]
g = math.gcd(dx, dy)
dx, dy = dx // g, dy // g # reduced direction, no floats
if dx < 0 or (dx == 0 and dy < 0): # one sign convention per line
dx, dy = -dx, -dy
slopes[(dx, dy)] += 1
best = max(best, max(slopes.values(), default=0) + 1)
return bestUse this when the order of the input does not matter for the answer, so sorting first puts equal values together and unlocks two pointers or binary search.
Sort both arrays, so equal values sit next to each other. Walk one pointer in each array. Move the pointer at the smaller value. When the values match, keep that value once and move both pointers, which matches duplicates one for one.
Complexity: O(n log n + m log m) time, O(1) extra space besides the output
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
nums1.sort() # sort first: equal values sit together
nums2.sort()
i = j = 0
out = []
while i < len(nums1) and j < len(nums2):
if nums1[i] < nums2[j]: # move the pointer at the smaller value
i += 1
elif nums1[i] > nums2[j]:
j += 1
else: # match: use one copy from each side
out.append(nums1[i])
i += 1
j += 1
return outSort the array. Fix the first number `nums[i]`, then run two pointers on the rest to find pairs that sum to its negative. Because the array is sorted, equal values are adjacent, so skipping repeats removes duplicate triplets. Sorting costs O(n log n), and the loops cost O(n^2).
Complexity: O(n^2) time, O(1) extra space besides the output
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort() # sorting unlocks two pointers and the dedupe skips
out = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]: # skip repeated first values
continue
l, r = i + 1, len(nums) - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s < 0:
l += 1
elif s > 0:
r -= 1
else:
out.append([nums[i], nums[l], nums[r]])
l += 1
while l < r and nums[l] == nums[l - 1]: # skip repeated second values
l += 1
return outSort by width ascending, and for equal widths by height descending, so two envelopes of the same width can never both be in one increasing run. The question is now the longest strictly increasing sequence of heights. Keep `tails` and use binary search (`bisect_left`) to place each height, which is the search half of the pattern.
Complexity: O(n log n) time, O(n) extra space
from bisect import bisect_left
class Solution:
def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
envelopes.sort(key=lambda e: (e[0], -e[1])) # width up; same width: height down
tails = [] # tails[i] = smallest end of a run of length i + 1
for _, h in envelopes:
i = bisect_left(tails, h) # binary search for where h fits
if i == len(tails):
tails.append(h)
else:
tails[i] = h
return len(tails)Use this when the array is sorted (or sorted then rotated) and you want a position or a value in O(log n).
This is exactly the lower bound: the first index whose value is at least the target. Keep the search range `[lo, hi)` and look at `mid`. If `nums[mid] >= target`, the answer is at `mid` or before, so `hi = mid`. Otherwise it is after `mid`, so `lo = mid + 1`. The range halves each step.
Complexity: O(log n) time, O(1) extra space
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
lo, hi = 0, len(nums) # the answer can be len(nums)
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] >= target: # first index with nums[i] >= target
hi = mid
else:
lo = mid + 1
return loAfter a rotation, at least one half around `mid` is still sorted. Check which half is sorted by comparing `nums[lo]` with `nums[mid]`. If the target lies inside the sorted half, search that half. Otherwise search the other half. That still discards half the array each step.
Complexity: O(log n) time, O(1) extra space
class Solution:
def search(self, nums: List[int], target: int) -> int:
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]: # left half is sorted
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else: # right half is sorted
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1The median splits the merged data into a left half and a right half. Binary search how many elements `i` the left half takes from the shorter array, so `j = half - i` come from the other. The split is correct when each side's last left value is at most the other side's first right value. If not, move `i` by halving, which gives O(log(min(m, n))).
Complexity: O(log(min(m, n))) time, O(1) extra space
from math import inf
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1 # binary search on the shorter array
m, n = len(nums1), len(nums2)
half = (m + n + 1) // 2
lo, hi = 0, m
while lo <= hi:
i = (lo + hi) // 2 # elements taken from nums1 for the left half
j = half - i # elements taken from nums2
left1 = nums1[i - 1] if i > 0 else -inf
right1 = nums1[i] if i < m else inf
left2 = nums2[j - 1] if j > 0 else -inf
right2 = nums2[j] if j < n else inf
if left1 > right2: # took too many from nums1
hi = i - 1
elif left2 > right1: # took too few from nums1
lo = i + 1
else:
if (m + n) % 2:
return float(max(left1, left2))
return (max(left1, left2) + min(right1, right2)) / 2
return 0.0Use this when the question says minimise the maximum or find the smallest value that works, and a yes/no check flips from no to yes exactly once.
We are not searching an array but the possible answers 0..x. The check `mid * mid <= x` is true for small values and false for large ones, so it flips once. Keep the largest value for which the check is true. Use the upper middle so `lo = mid` always makes progress.
Complexity: O(log x) time, O(1) extra space
class Solution:
def mySqrt(self, x: int) -> int:
lo, hi = 0, x
while lo < hi:
mid = (lo + hi + 1) // 2 # upper middle so the loop always shrinks
if mid * mid <= x: # ok(mid): mid is not too big
lo = mid
else:
hi = mid - 1
return loThe answer is the eating speed. Write `ok(speed)`: total hours at that speed is the sum of `ceil(pile / speed)`, and it must be at most h. A faster speed never needs more hours, so `ok` is monotonic. Binary search speeds from 1 to the largest pile for the smallest speed where `ok` is true.
Complexity: O(n log max(piles)) time, O(1) extra space
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
def ok(speed): # monotonic: faster never needs more hours
return sum((p + speed - 1) // speed for p in piles) <= h
lo, hi = 1, max(piles) # the answer is in [1, max(piles)]
while lo < hi:
mid = (lo + hi) // 2
if ok(mid): # works: try slower
hi = mid
else:
lo = mid + 1
return loMinimise the largest sub-array sum is a "minimise the maximum" question, so search the answer. `ok(limit)` greedily cuts a new part whenever adding the next number would pass `limit`, and returns whether at most k parts are enough. A bigger limit never needs more parts, so the check is monotonic. The search range runs from `max(nums)` to `sum(nums)`.
Complexity: O(n log(sum(nums))) time, O(1) extra space
class Solution:
def splitArray(self, nums: List[int], k: int) -> int:
def ok(limit): # can we cut into at most k parts, each sum <= limit?
parts, cur = 1, 0
for x in nums:
if cur + x > limit:
parts += 1
cur = 0
cur += x
return parts <= k
lo, hi = max(nums), sum(nums) # the answer lies between these
while lo < hi:
mid = (lo + hi) // 2
if ok(mid): # works: try a smaller limit
hi = mid
else:
lo = mid + 1
return loUse this when the question is about overlapping ranges: merge them, insert one, or keep a set of disjoint ranges.
Sort the intervals by start. After sorting, a new interval can only overlap the last merged one. If `start <= out[-1][1]`, extend the end with `max`. Otherwise push a new interval. One pass after the sort is enough.
Complexity: O(n log n) time, O(n) extra space
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort() # sort by start
out = [intervals[0]]
for start, end in intervals[1:]:
if start <= out[-1][1]: # overlaps the last merged interval
out[-1][1] = max(out[-1][1], end) # max keeps [1, 10] + [2, 3] as [1, 10]
else:
out.append([start, end])
return outThe list is already sorted, so no sort is needed. Copy every interval that ends before the new one starts. Then merge every interval that overlaps into the new one by taking the min start and max end. Finally copy the rest, which all start after the new interval ends.
Complexity: O(n) time, O(n) extra space
class Solution:
def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
out = []
i, n = 0, len(intervals)
while i < n and intervals[i][1] < newInterval[0]: # entirely before the new interval
out.append(intervals[i])
i += 1
while i < n and intervals[i][0] <= newInterval[1]: # overlaps: merge into the new interval
newInterval = [min(newInterval[0], intervals[i][0]), max(newInterval[1], intervals[i][1])]
i += 1
out.append(newInterval)
out.extend(intervals[i:]) # entirely after
return outKeep the merged, disjoint intervals sorted in two lists: `starts` and `ends`. Each new number is the interval `[v, v]`. Binary search (`bisect_right`) finds the neighbours, and the number either is already covered, touches the left interval, touches the right one, touches both (merge them), or stands alone.
Complexity: addNum O(log n) search plus O(n) list shift, getIntervals O(n)
from bisect import bisect_right
class SummaryRanges:
def __init__(self):
self.starts = [] # sorted starts of the disjoint intervals
self.ends = [] # ends[i] belongs to starts[i]
def addNum(self, value: int) -> None:
i = bisect_right(self.starts, value) # first interval that starts after value
if i > 0 and self.ends[i - 1] >= value: # already covered
return
touch_left = i > 0 and self.ends[i - 1] == value - 1
touch_right = i < len(self.starts) and self.starts[i] == value + 1
if touch_left and touch_right: # value bridges two intervals: merge them
self.ends[i - 1] = self.ends[i]
del self.starts[i]
del self.ends[i]
elif touch_left:
self.ends[i - 1] = value
elif touch_right:
self.starts[i] = value
else: # stands alone: new interval [value, value]
self.starts.insert(i, value)
self.ends.insert(i, value)
def getIntervals(self) -> List[List[int]]:
return [[s, e] for s, e in zip(self.starts, self.ends)]Use this when the numbers lie in the range 0..n or 1..n and you must find a missing or duplicate number in O(1) space.
The values are 0..n with one missing. Put each value `j` at index `j` by swapping, and skip the value n because it has no slot. After that, the first index `i` where `nums[i] != i` is the missing number. If every slot is right, the answer is n.
Complexity: O(n) time, O(1) extra space
class Solution:
def missingNumber(self, nums: List[int]) -> int:
i, n = 0, len(nums)
while i < n:
j = nums[i] # the slot where nums[i] belongs
if j < n and nums[j] != nums[i]: # not home yet: swap it into place
nums[i], nums[j] = nums[j], nums[i]
else:
i += 1
for i in range(n):
if nums[i] != i: # first slot holding the wrong value
return i
return nValues are 1..n, so value `v` belongs at index `v - 1`. Swap each value toward its own slot; if that slot already holds the same value, move on. When all swaps are done, any index `i` that holds a value other than `i + 1` holds a duplicate of a value that is already at home.
Complexity: O(n) time, O(1) extra space besides the output
class Solution:
def findDuplicates(self, nums: List[int]) -> List[int]:
i = 0
while i < len(nums):
j = nums[i] - 1 # the slot where nums[i] belongs
if nums[j] != nums[i]: # not home yet: swap it into place
nums[i], nums[j] = nums[j], nums[i]
else:
i += 1
return [x for i, x in enumerate(nums) if x != i + 1] # a wrong value here is a duplicateThe smallest missing positive must be in 1..n+1, so only values in 1..n matter. For each index, keep swapping `nums[i]` to slot `nums[i] - 1` while it is in range and that slot does not already hold it. Negatives, zeros and big values stay where they are. Then the first index with `nums[i] != i + 1` gives the answer `i + 1`.
Complexity: O(n) time, O(1) extra space
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
for i in range(n):
while 1 <= nums[i] <= n and nums[nums[i] - 1] != nums[i]: # value belongs in range and not home
j = nums[i] - 1
nums[i], nums[j] = nums[j], nums[i] # send it to its own slot
for i in range(n):
if nums[i] != i + 1:
return i + 1
return n + 1Use this when the input is a grid and you walk neighbours with a direction array and bounds check, shrink four boundaries, or transpose and reverse.
For every cell, visit its neighbours with direction vectors `(dr, dc)` and a bounds check, so border cells have fewer neighbours. The cell itself is the offset `(0, 0)`. Add the values and count the cells that were inside the grid, then use integer division for the average.
Complexity: O(rows * cols) time, O(1) extra space besides the output
class Solution:
def imageSmoother(self, img: List[List[int]]) -> List[List[int]]:
rows, cols = len(img), len(img[0])
DIRS = [(dr, dc) for dr in (-1, 0, 1) for dc in (-1, 0, 1)] # the cell and its 8 neighbours
out = [[0] * cols for _ in range(rows)]
for r in range(rows):
for c in range(cols):
total = count = 0
for dr, dc in DIRS:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols: # bounds check
total += img[nr][nc]
count += 1
out[r][c] = total // count
return outKeep four boundaries: `top`, `bottom`, `left`, `right`. Walk the top row, then the right column, then the bottom row, then the left column, and shrink the matching boundary after each side. Check `top <= bottom` and `left <= right` before the last two sides so a single row or column is not read twice. Stop when the boundaries cross.
Complexity: O(rows * cols) time, O(1) extra space besides the output
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
out = []
while top <= bottom and left <= right:
for c in range(left, right + 1): # top row, left to right
out.append(matrix[top][c])
top += 1 # shrink after each side
for r in range(top, bottom + 1): # right column, top to bottom
out.append(matrix[r][right])
right -= 1
if top <= bottom: # a single row left: do not walk it twice
for c in range(right, left - 1, -1): # bottom row, right to left
out.append(matrix[bottom][c])
bottom -= 1
if left <= right: # a single column left: do not walk it twice
for r in range(bottom, top - 1, -1): # left column, bottom to top
out.append(matrix[r][left])
left += 1
return outRotating 90 degrees clockwise is two moves. First transpose: swap `matrix[r][c]` with `matrix[c][r]` for every pair above the diagonal. Then reverse each row. Both steps work in place, so no second matrix is needed.
Complexity: O(n^2) time, O(1) extra space
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
for r in range(n):
for c in range(r + 1, n): # transpose: swap across the diagonal
matrix[r][c], matrix[c][r] = matrix[c][r], matrix[r][c]
for row in matrix:
row.reverse() # then reverse each rowA string is an array of characters. Most array patterns carry over, plus a few string-only ones.
Use this when the answer depends on how many times each character appears, not on where it appears.
Count every letter once with Counter. Then walk the string a second time and return the first index whose count is 1. The counting pass turns "is this letter unique?" into a dictionary lookup, so the whole thing is two passes.
Complexity: O(n) time, O(1) extra space (at most 26 letters)
from collections import Counter
class Solution:
def firstUniqChar(self, s: str) -> int:
count = Counter(s) # pass 1: how many times each letter appears
for i, ch in enumerate(s): # pass 2: first letter whose count is exactly 1
if count[ch] == 1:
return i
return -1Build the frequency table with Counter. most_common() lists the letters from the highest count to the lowest. Repeat each letter by its count and join the pieces. The order of the original string does not matter here, only the counts do.
Complexity: O(n + k log k) time, O(n) space (k = number of distinct characters)
from collections import Counter
class Solution:
def frequencySort(self, s: str) -> str:
count = Counter(s) # character frequency
return "".join(ch * n for ch, n in count.most_common()) # highest count firstCount the letters. If one letter appears more than (n + 1) // 2 times, no arrangement works, so return "". Otherwise write the letters from most frequent to least frequent into positions 0, 2, 4, ... and then 1, 3, 5, ... The most frequent letter takes the even slots first, which keeps equal letters apart.
Complexity: O(n + k log k) time, O(n) space (k = number of distinct characters)
from collections import Counter
class Solution:
def reorganizeString(self, s: str) -> str:
count = Counter(s)
if max(count.values()) > (len(s) + 1) // 2: # too frequent to ever be separated
return ""
res = [""] * len(s)
idx = 0
for ch, n in count.most_common(): # most frequent letter first
for _ in range(n):
res[idx] = ch
idx += 2 # fill the even slots, then the odd slots
if idx >= len(s):
idx = 1
return "".join(res)Use this when you can walk a string from both ends, or with a read pointer and a write pointer, and every step settles one position.
Put one pointer at each end. Swap the two characters, then move both pointers toward the middle until they meet. Each character is touched once and no extra array is needed, because the swap happens in place.
Complexity: O(n) time, O(1) extra space
class Solution:
def reverseString(self, s: List[str]) -> None:
l, r = 0, len(s) - 1
while l < r: # pointers walk toward each other
s[l], s[r] = s[r], s[l]
l += 1
r -= 1Building both final strings costs O(n) extra space. Instead, put one pointer at the end of each string and move it left past every character that a "#" erases. The helper keeps a skip counter: "#" adds one, and a letter with skip above zero is erased. Then compare the two surviving characters and step both pointers left.
Complexity: O(n + m) time, O(1) extra space
class Solution:
def backspaceCompare(self, s: str, t: str) -> bool:
def next_valid(string: str, i: int) -> int:
skip = 0
while i >= 0: # walk left past erased characters
if string[i] == "#":
skip += 1
elif skip:
skip -= 1
else:
break
i -= 1
return i
i, j = len(s) - 1, len(t) - 1 # one pointer per string, starting at the ends
while i >= 0 or j >= 0:
i, j = next_valid(s, i), next_valid(t, j)
if i >= 0 and j >= 0:
if s[i] != t[j]:
return False
elif i >= 0 or j >= 0: # one string ran out of letters first
return False
i -= 1
j -= 1
return TrueUse a read pointer to scan each group of equal characters and a write pointer to build the answer in the same array. The write pointer never gets ahead of the read pointer, so nothing unread is overwritten. After each group, write the character, and if the group is longer than 1, write the digits of its length one by one. Return the write pointer as the new length.
Complexity: O(n) time, O(1) extra space
class Solution:
def compress(self, chars: List[str]) -> int:
write = read = 0
while read < len(chars):
ch, start = chars[read], read
while read < len(chars) and chars[read] == ch: # read pointer scans the whole group
read += 1
chars[write] = ch # write pointer records the result
write += 1
if read - start > 1:
for digit in str(read - start):
chars[write] = digit
write += 1
return writeUse this when the question looks at every substring of one fixed length, so you can slide by one step: add the new character and drop the old one.
Every window of length k needs as many recolors as it has white blocks. Count the whites in the first window. Then slide right: add one if the new block is white and subtract one if the block that left was white. The answer is the smallest white count seen.
Complexity: O(n) time, O(1) extra space
class Solution:
def minimumRecolors(self, blocks: str, k: int) -> int:
whites = blocks[:k].count("W") # whites in the first window
best = whites
for i in range(k, len(blocks)):
if blocks[i] == "W": # new block enters on the right
whites += 1
if blocks[i - k] == "W": # old block leaves on the left
whites -= 1
best = min(best, whites)
return bestAn anagram of p is any substring of s with length len(p) and the same letter counts. Keep a Counter for the current window. For each new character, add it on the right, and once the window is longer than len(p), remove the character on the left. When the window Counter equals the Counter of p, record the start index.
Complexity: O(n) time (comparing two Counters costs at most 26), O(1) extra space
from collections import Counter
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
need, win, out = Counter(p), Counter(), []
k = len(p) # fixed window length
for i, ch in enumerate(s):
win[ch] += 1 # character enters on the right
if i >= k:
old = s[i - k] # character leaves on the left
win[old] -= 1
if win[old] == 0:
del win[old]
if win == need:
out.append(i - k + 1)
return outAll words have the same length wl, so a valid substring is exactly k words long (k = number of words). Split s into wl-letter chunks for each of the wl possible starting offsets. On each chunk list, slide a window of exactly k chunks: add the new chunk, drop the oldest, and compare the window Counter with the Counter of words. A match at chunk j means the substring starts at offset + (j - k + 1) * wl.
Complexity: O(n * (wl + d)) time (d = distinct words), O(n / wl + k) extra space
from collections import Counter
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
k, wl = len(words), len(words[0])
need, out = Counter(words), []
for offset in range(wl): # words may start at any of wl alignments
win = Counter()
chunks = [s[i:i + wl] for i in range(offset, len(s) - wl + 1, wl)]
for j, w in enumerate(chunks):
win[w] += 1 # new word enters on the right
if j >= k: # window is fixed at k words
old = chunks[j - k]
win[old] -= 1 # oldest word leaves on the left
if win[old] == 0:
del win[old]
if win == need:
out.append(offset + (j - k + 1) * wl)
return outUse this when you need the longest or shortest substring that stays valid, so you grow the right end and shrink the left end while the window is broken.
Walk the right end r over the string. Store the last index of every character in a dictionary. If the current character was last seen inside the window, move the left end l to one past that index. The window s[l..r] never holds a repeat, so its length is a candidate for the answer.
Complexity: O(n) time, O(min(n, alphabet)) extra space
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
last, l, best = {}, 0, 0
for r, ch in enumerate(s): # r grows the window
if ch in last and last[ch] >= l: # repeat inside the window
l = last[ch] + 1 # jump past the previous copy
last[ch] = r
best = max(best, r - l + 1)
return bestA window can be turned into one repeated letter if its length minus the count of its most common letter is at most k. Grow the window on the right and update the count map. While the window needs more than k replacements, remove the leftmost character and move l right. The longest window that stays valid is the answer.
Complexity: O(26 * n) time, O(1) extra space
from collections import defaultdict
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
count, l, best = defaultdict(int), 0, 0
for r, ch in enumerate(s):
count[ch] += 1
while (r - l + 1) - max(count.values()) > k: # too many letters to replace: shrink
count[s[l]] -= 1
l += 1
best = max(best, r - l + 1)
return bestneed counts the letters of t, and missing counts how many letters of t the window still lacks. Grow r and decrease the counts; a negative count means the window has a spare copy. When missing reaches 0 the window is valid, so move l right while the letter at l is spare, and record the length. Then drop s[l] to break the window and keep searching.
Complexity: O(n + m) time, O(1) extra space (alphabet size)
from collections import Counter
class Solution:
def minWindow(self, s: str, t: str) -> str:
need = Counter(t)
missing = len(t) # letters of t not yet covered by the window
l, best_len, best_start = 0, len(s) + 1, 0
for r, ch in enumerate(s):
if need[ch] > 0:
missing -= 1
need[ch] -= 1 # negative means a spare copy inside the window
if missing == 0: # window is valid: shrink from the left
while need[s[l]] < 0:
need[s[l]] += 1
l += 1
if r - l + 1 < best_len:
best_len, best_start = r - l + 1, l
need[s[l]] += 1 # drop s[l] to break the window
missing += 1
l += 1
return s[best_start:best_start + best_len] if best_len <= len(s) else ""Use this when you keep asking "have I seen this character, substring or word before, and where?" and a dictionary or set can answer in O(1).
Two strings are isomorphic if the letters pair up one to one. Keep one dictionary from s to t and one from t to s. For each position, if either dictionary already holds a different partner, return False. Otherwise store the pair in both.
Complexity: O(n) time, O(1) extra space (bounded by the alphabet)
class Solution:
def isIsomorphic(self, s: str, t: str) -> bool:
s_to_t, t_to_s = {}, {}
for a, b in zip(s, t):
if s_to_t.get(a, b) != b or t_to_s.get(b, a) != a: # a clash in either direction
return False
s_to_t[a] = b
t_to_s[b] = a
return TrueSlide over every substring of length 10. A set called seen remembers each one. If a substring is already in seen, it has now appeared twice, so put it in the answer set. Using a second set keeps each repeated sequence in the output only once.
Complexity: O(10 * n) time, O(10 * n) space
class Solution:
def findRepeatedDnaSequences(self, s: str) -> List[str]:
seen, repeated = set(), set()
for i in range(len(s) - 9):
chunk = s[i:i + 10]
if chunk in seen: # second time we meet this 10-letter substring
repeated.add(chunk)
seen.add(chunk)
return list(repeated)Store every word with its index in a dictionary. For each word w, try every cut into left and right. If left is a palindrome, the reverse of right placed in front of w makes a palindrome, so look up the reverse of right in the dictionary. If right is a palindrome, the reverse of left placed after w works the same way. The dictionary lookup replaces checking every other word.
Complexity: O(n * L^2) time, O(n * L) space (L = longest word)
class Solution:
def palindromePairs(self, words: List[str]) -> List[List[int]]:
index = {w: i for i, w in enumerate(words)} # word -> position, for O(1) lookup
def is_pal(x: str) -> bool:
return x == x[::-1]
out = []
for i, w in enumerate(words):
for cut in range(len(w) + 1):
left, right = w[:cut], w[cut:]
if is_pal(left): # words[j] + w, with words[j] = reverse(right)
j = index.get(right[::-1])
if j is not None and j != i:
out.append([j, i])
if cut < len(w) and is_pal(right): # w + words[j], with words[j] = reverse(left)
j = index.get(left[::-1])
if j is not None and j != i:
out.append([i, j])
return outUse this when two strings must match letter for letter in any order, so equal letter counts (or an equal sorted form) is the test.
Two strings are anagrams when every letter has the same count in both. Use a 26-slot array: add one for each letter of s and subtract one for each letter of t. If the lengths match and every slot ends at 0, the counts are equal.
Complexity: O(n) time, O(1) extra space
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
count = [0] * 26 # one slot per lowercase letter
for a, b in zip(s, t):
count[ord(a) - ord("a")] += 1
count[ord(b) - ord("a")] -= 1
return not any(count) # all zeros means the same lettersAnagrams share the same letter counts, so the count tuple works as a dictionary key. For each word, build its 26-slot count, turn it into a tuple, and append the word to the list stored under that key. Each list in the dictionary is then one group, and no sorting of letters is needed.
Complexity: O(total characters) time, O(total characters) space
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
groups = defaultdict(list)
for word in strs:
count = [0] * 26
for ch in word:
count[ord(ch) - ord("a")] += 1
groups[tuple(count)].append(word) # same counts means same key
return list(groups.values())Count the letters of s, then subtract the letters of t. A positive number means s has that many copies that t lacks. Each missing copy costs one replacement in t, so the answer is the sum of the positive differences. Letters where t has extra copies are matched by those same replacements, so they are not counted twice.
Complexity: O(n) time, O(1) extra space
from collections import Counter
class Solution:
def minSteps(self, s: str, t: str) -> int:
diff = Counter(s)
diff.subtract(t) # count(s) - count(t) per letter
return sum(v for v in diff.values() if v > 0)Use this when the question is about a string that reads the same both ways: compare from the ends inward, or grow outward from every centre.
Put one pointer at each end. Move each pointer past characters that are not letters or digits. Compare the two characters in lower case; any mismatch means False. Move both pointers inward until they cross.
Complexity: O(n) time, O(1) extra space
class Solution:
def isPalindrome(self, s: str) -> bool:
l, r = 0, len(s) - 1
while l < r:
while l < r and not s[l].isalnum(): # skip non-alphanumerics
l += 1
while l < r and not s[r].isalnum():
r -= 1
if s[l].lower() != s[r].lower(): # compare from the ends inward
return False
l += 1
r -= 1
return TrueEvery palindrome has a centre: a single character (odd length) or the gap between two characters (even length). For each index c, try both centres and expand outward while the two ends match. When the loop stops, s[l+1:r] is the palindrome for that centre; keep the longest. There are 2n - 1 centres and each expansion costs at most O(n).
Complexity: O(n^2) time, O(1) extra space
class Solution:
def longestPalindrome(self, s: str) -> str:
best = ""
for c in range(len(s)):
for l, r in ((c, c), (c, c + 1)): # odd centre, even centre
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1 # grow outward
r += 1
if r - l - 1 > len(best):
best = s[l + 1:r]
return bestLet cuts[i] be the fewest cuts for the first i characters, starting from the worst case of i - 1. Expand around every centre. Each time s[l..r] is a palindrome, the first r + 1 characters can be made from the first l characters plus one more piece, so cuts[r+1] = min(cuts[r+1], cuts[l] + 1). Setting cuts[0] = -1 makes a palindrome that starts at index 0 cost zero cuts.
Complexity: O(n^2) time, O(n) space
class Solution:
def minCut(self, s: str) -> int:
n = len(s)
cuts = list(range(-1, n)) # cuts[i] = fewest cuts for s[:i]; worst case i - 1
for c in range(n):
for l, r in ((c, c), (c, c + 1)): # odd centre, even centre
while l >= 0 and r < n and s[l] == s[r]:
cuts[r + 1] = min(cuts[r + 1], cuts[l] + 1) # s[l..r] is one palindrome piece
l -= 1
r += 1
return cuts[n]Use this when you must count or measure substrings and can reason about where each one starts or ends, instead of listing all O(n^2) of them.
A valid substring is some 0s followed by the same number of 1s, or the other way round. So it sits on the border between two neighbouring runs of equal characters. Two touching runs of lengths a and b give min(a, b) valid substrings. Track the previous and current run lengths and add min(prev, cur) every time a run ends.
Complexity: O(n) time, O(1) extra space
class Solution:
def countBinarySubstrings(self, s: str) -> int:
prev, cur, total = 0, 1, 0 # lengths of the previous run and the current run
for i in range(1, len(s)):
if s[i] == s[i - 1]:
cur += 1 # the same run continues
else:
total += min(prev, cur) # two touching runs give min(prev, cur) substrings
prev, cur = cur, 1
return total + min(prev, cur) # close the last pair of runsFix the end index r and ask how many start indices make a valid substring. The substring must reach back to the latest a, the latest b and the latest c, so every start up to the oldest of those three positions works. That count is min(last positions) + 1. Add it for every r, and nothing is added until all three letters have appeared.
Complexity: O(n) time, O(1) extra space
class Solution:
def numberOfSubstrings(self, s: str) -> int:
last = {"a": -1, "b": -1, "c": -1} # latest index of each letter
total = 0
for r, ch in enumerate(s):
last[ch] = r
# valid starts for the substring ending at r: 0 .. min(last)
total += min(last.values()) + 1
return totalInstead of looking at each substring, ask how many substrings each character occurrence is the only copy of its letter in. For an occurrence at index k with previous copy at p and next copy at q, the substring may start in (p, k] and end in [k, q). That gives (k - p) * (q - k). Store the positions of every letter with sentinels -1 and len(s), then add this product for each occurrence.
Complexity: O(n) time, O(n) space
from collections import defaultdict
class Solution:
def uniqueLetterString(self, s: str) -> int:
positions = defaultdict(lambda: [-1]) # indices of each letter, sentinel -1 in front
for i, ch in enumerate(s):
positions[ch].append(i)
total = 0
for idx in positions.values():
idx.append(len(s)) # sentinel after the last copy
for k in range(1, len(idx) - 1):
# substrings where this copy is the only one of its letter
total += (idx[k] - idx[k - 1]) * (idx[k + 1] - idx[k])
return totalUse this when you must find a pattern inside a text, or compare a string with its own prefixes, and the text pointer should never move backwards.
This is textbook KMP. First build the LPS table of the needle: lps[i] is the longest proper prefix of needle[:i+1] that is also a suffix. Then scan the haystack once, keeping k = how many needle characters match now. On a mismatch, fall back with k = lps[k-1] instead of moving the haystack pointer back. When k equals the needle length, the match started at i - k + 1.
Complexity: O(n + m) time, O(m) extra space
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
lps, k = [0] * len(needle), 0 # KMP failure table of the needle
for i in range(1, len(needle)):
while k and needle[i] != needle[k]:
k = lps[k - 1]
if needle[i] == needle[k]:
k += 1
lps[i] = k
k = 0 # needle characters matched so far
for i, ch in enumerate(haystack): # i never moves backwards
while k and ch != needle[k]:
k = lps[k - 1] # fall back inside the needle
if ch == needle[k]:
k += 1
if k == len(needle):
return i - k + 1
return -1Build the LPS table of s itself. lps[-1] is the length of the longest proper prefix that is also a suffix. The smallest repeating block then has length n - lps[-1]. The string is made of repeated blocks only if lps[-1] is above 0 and that block length divides n.
Complexity: O(n) time, O(n) extra space
class Solution:
def repeatedSubstringPattern(self, s: str) -> bool:
n = len(s)
lps, k = [0] * n, 0 # prefix function of s itself
for i in range(1, n):
while k and s[i] != s[k]:
k = lps[k - 1]
if s[i] == s[k]:
k += 1
lps[i] = k
period = n - lps[-1] # length of the smallest repeating block
return lps[-1] > 0 and n % period == 0We may only add characters in front, so we need the longest prefix of s that is already a palindrome. Build s + "#" + reverse(s) and compute its LPS table. The last LPS value is exactly the length of the longest prefix of s that equals a suffix of reverse(s), which means a palindromic prefix. Reverse the rest of s and put it in front.
Complexity: O(n) time, O(n) extra space
class Solution:
def shortestPalindrome(self, s: str) -> str:
combined = s + "#" + s[::-1]
lps, k = [0] * len(combined), 0 # KMP failure table
for i in range(1, len(combined)):
while k and combined[i] != combined[k]:
k = lps[k - 1]
if combined[i] == combined[k]:
k += 1
lps[i] = k
longest = lps[-1] # length of the longest palindromic prefix of s
return s[longest:][::-1] + sThis is Rabin-Karp inside a binary search on the length. If a duplicate of length L exists, one of length L - 1 exists too, so binary search the length. For one length, roll a polynomial hash across the string in O(1) per step and store hashes in a dictionary. When a hash repeats, compare the actual substrings to rule out a collision.
Complexity: O(n log n) expected time, O(n) space
class Solution:
def longestDupSubstring(self, s: str) -> str:
n, base, mod = len(s), 131, (1 << 61) - 1
nums = [ord(c) - 96 for c in s]
def find(length: int) -> int: # start of a duplicate of this length, or -1
power = pow(base, length, mod)
h = 0
for i in range(length):
h = (h * base + nums[i]) % mod
seen = {h: [0]}
for i in range(1, n - length + 1):
h = (h * base + nums[i + length - 1] - nums[i - 1] * power) % mod # rolling hash
for j in seen.get(h, []):
if s[j:j + length] == s[i:i + length]: # verify on a hash match
return i
seen.setdefault(h, []).append(i)
return -1
lo, hi, best = 1, n - 1, ""
while lo <= hi: # binary search on the length
mid = (lo + hi) // 2
i = find(mid)
if i != -1:
best, lo = s[i:i + mid], mid + 1
else:
hi = mid - 1
return bestUse this when the most recent unfinished piece must be closed or resolved first, as with brackets, nested encodings and expressions.
Push every opening bracket on a stack. When a closing bracket arrives, the top of the stack must be its matching opener, so pop and compare. If the stack is empty at a closer or the pair does not match, return False. At the end the stack must be empty, otherwise some opener was never closed.
Complexity: O(n) time, O(n) extra space
class Solution:
def isValid(self, s: str) -> bool:
pair, stack = {")": "(", "]": "[", "}": "{"}, []
for ch in s:
if ch in pair: # closer: top of stack must be its opener
if not stack or stack.pop() != pair[ch]:
return False
else:
stack.append(ch) # opener: wait for its closer
return not stackKeep the text built so far in cur and the repeat count being read in num. On "[", push (cur, num) on the stack and start fresh. On "]", pop the saved text and count, and set cur to saved text plus cur repeated count times. Letters are appended to cur and digits build num, so nested brackets resolve from the inside out.
Complexity: O(output length) time, O(n) extra space
class Solution:
def decodeString(self, s: str) -> str:
stack, cur, num = [], "", 0
for ch in s:
if ch.isdigit():
num = num * 10 + int(ch) # counts can have several digits
elif ch == "[":
stack.append((cur, num)) # save the outer state
cur, num = "", 0
elif ch == "]":
prev, k = stack.pop() # restore it and expand
cur = prev + cur * k
else:
cur += ch
return curKeep a stack of Counters, one per open bracket. Read an atom name and its optional count, and add it to the Counter on top. On "(", push a new Counter. On ")", read the number after it, pop the Counter, multiply every count in it, and merge it into the Counter below. At the end, sort the atom names and write each count only when it is above 1.
Complexity: O(n^2) time in the worst case, O(n) space
from collections import Counter
class Solution:
def countOfAtoms(self, formula: str) -> str:
stack = [Counter()] # one Counter per open bracket
i, n = 0, len(formula)
while i < n:
ch = formula[i]
if ch == "(":
stack.append(Counter()) # open: start a new level
i += 1
elif ch == ")":
i += 1
j = i
while j < n and formula[j].isdigit():
j += 1
mult = int(formula[i:j] or 1)
i = j
top = stack.pop() # close: multiply and merge into the parent
for atom, c in top.items():
stack[-1][atom] += c * mult
else:
j = i + 1
while j < n and formula[j].islower():
j += 1
atom = formula[i:j]
i = j
while j < n and formula[j].isdigit():
j += 1
stack[-1][atom] += int(formula[i:j] or 1)
i = j
total = stack[0]
return "".join(a + (str(total[a]) if total[a] > 1 else "") for a in sorted(total))No random access, so every trick is about pointers: moving them at different speeds, reversing them, or anchoring them with a dummy node.
Use this when one pointer must move twice as fast as another so the slow one lands in the middle, or the two meet inside a loop.
Repeatedly replacing a number by the sum of its squared digits makes a chain, and a chain behaves like a linked list. It either reaches 1 or falls into a loop. Slow takes one step per round and fast takes two. If fast reaches 1 the number is happy. If slow and fast meet before that, there is a loop that does not contain 1.
Complexity: O(log n) time, O(1) extra space
class Solution:
def isHappy(self, n: int) -> bool:
def next_number(x: int) -> int:
total = 0
while x:
x, digit = divmod(x, 10)
total += digit * digit
return total
slow, fast = n, next_number(n)
while fast != 1 and slow != fast: # meeting without hitting 1 means a loop
slow = next_number(slow) # slow: 1 step
fast = next_number(next_number(fast)) # fast: 2 steps
return fast == 1A list cannot be read backwards, so fast and slow pointers first find the middle: when fast reaches the end, slow is at the middle. The second half is then reversed in place with the save, flip, advance steps. Now the first half and the reversed second half are walked together and compared value by value. For an odd length the middle node is compared with itself, which is harmless.
Complexity: O(n) time, O(1) extra space
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
slow = fast = head
while fast and fast.next: # slow ends at the middle
slow, fast = slow.next, fast.next.next
prev = None
while slow: # reverse the second half
nxt = slow.next
slow.next = prev
prev, slow = slow, nxt
left, right = head, prev
while right: # compare outside-in
if left.val != right.val:
return False
left, right = left.next, right.next
return TrueThe target order is first, last, second, second-last, and so on. Fast and slow pointers find the middle and the list is cut there. The second half is reversed so that its nodes come out last-first. Then the two halves are woven together one node at a time, saving both next pointers before relinking.
Complexity: O(n) time, O(1) extra space
class Solution:
def reorderList(self, head: Optional[ListNode]) -> None:
if not head:
return
slow = fast = head
while fast and fast.next: # 1. find the middle
slow, fast = slow.next, fast.next.next
prev, cur = None, slow.next
slow.next = None # cut into two halves
while cur: # 2. reverse the second half
cur.next, prev, cur = prev, cur, cur.next
first, second = head, prev
while second: # 3. weave the halves together
next_first, next_second = first.next, second.next
first.next = second
second.next = next_first
first, second = next_first, next_secondUse this when the direction of next pointers must be flipped, in the whole list, in a window, or in one half.
Walk the list with prev and cur. For each node, save cur.next first, then point cur.next back at prev, then move both names forward. When cur falls off the end, prev is the new head. Saving nxt before the flip is what keeps the rest of the list from being lost.
Complexity: O(n) time, O(1) extra space
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
prev, cur = None, head
while cur:
nxt = cur.next # 1. save
cur.next = prev # 2. flip
prev, cur = cur, nxt # 3. advance
return prevWalk to the node right before position left, called before, using a dummy so left = 1 works. Run the same save, flip, advance loop, but only for right - left + 1 nodes. After the loop, prev is the new start of the window and cur is the first node after it. Two assignments reconnect the window: the old first node points to cur, and before points to prev.
Complexity: O(n) time, O(1) extra space
class Solution:
def reverseBetween(self, head: Optional[ListNode], left: int, right: int) -> Optional[ListNode]:
dummy = ListNode(0, head)
before = dummy
for _ in range(left - 1): # stop right before the window
before = before.next
prev, cur = None, before.next
for _ in range(right - left + 1): # reverse only the window
nxt = cur.next
cur.next = prev
prev, cur = cur, nxt
before.next.next = cur # old window start -> node after window
before.next = prev # before -> new window start
return dummy.nextThe most significant digit comes first, but addition must start from the least significant digit. Reversing both lists puts the digits in the order addition needs. The loop adds digit by digit with a carry. Each new digit is pushed on the front of the result, which reverses the answer back into the right order. This version reverses the input lists, which LeetCode allows.
Complexity: O(n + m) time, O(1) extra space
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
def reverse(head):
prev, cur = None, head
while cur:
nxt = cur.next # save
cur.next = prev # flip
prev, cur = cur, nxt # advance
return prev
a, b = reverse(l1), reverse(l2) # least significant digit first
carry = 0
result = None
while a or b or carry:
total = carry + (a.val if a else 0) + (b.val if b else 0)
carry, digit = divmod(total, 10)
result = ListNode(digit, result) # push on the front: reverses the answer back
a = a.next if a else None
b = b.next if b else None
return resultUse this when the head may be removed, replaced or built from scratch, so the first node should be handled like every other node.
If the head itself has the target value, there is no previous node to rewire. A dummy node placed before the head gives every real node a previous node. The loop looks at cur.next: if it matches, it is unlinked and cur stays put, otherwise cur moves on. Return dummy.next, because the head may have changed.
Complexity: O(n) time, O(1) extra space
class Solution:
def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]:
dummy = ListNode(0, head) # the head can now be removed like any node
cur = dummy
while cur.next:
if cur.next.val == val:
cur.next = cur.next.next # unlink, stay on cur
else:
cur = cur.next
return dummy.next # not dummyBuild two separate chains, one for nodes smaller than x and one for the rest, each with its own dummy node. Walk the original list once and append every node to the right chain, keeping the original order. Finally set big.next to None so the old tail does not leave a loop, and link the small chain to the start of the big chain. Return small_dummy.next.
Complexity: O(n) time, O(1) extra space
class Solution:
def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
small_dummy, big_dummy = ListNode(), ListNode() # one dummy per chain
small, big = small_dummy, big_dummy
while head:
if head.val < x:
small.next = head
small = small.next
else:
big.next = head
big = big.next
head = head.next
big.next = None # cut the old tail so no loop appears
small.next = big_dummy.next # small chain, then big chain
return small_dummy.nextEvery value that appears more than once is removed completely, so even the first node may disappear. A dummy node gives the head a previous node. prev is the last node known to stay. When cur starts a run of equal values, skip the whole run and set prev.next to the node after it. Otherwise cur is unique, so prev moves onto it.
Complexity: O(n) time, O(1) extra space
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(0, head) # the first node may be a duplicate too
prev = dummy # last node we are sure to keep
cur = head
while cur:
if cur.next and cur.next.val == cur.val:
dup = cur.val
while cur and cur.val == dup: # skip the whole run
cur = cur.next
prev.next = cur
else:
prev, cur = cur, cur.next
return dummy.nextUse this when two pointers must walk the same list at once, as neighbours, as odd and even positions, or as the two ends of a section to cut out.
The list is sorted, so equal values sit next to each other. cur and cur.next are the two pointers, always neighbours. If they hold the same value, unlink cur.next and compare again. If they differ, move cur forward. The head is never removed, so no dummy node is needed.
Complexity: O(n) time, O(1) extra space
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
cur = head
while cur and cur.next: # compare cur with its neighbour
if cur.next.val == cur.val:
cur.next = cur.next.next # drop the duplicate
else:
cur = cur.next
return headTwo pointers, odd and even, each build their own chain from the same list. Each step, odd jumps over the even node and even jumps over the odd node. Because they skip each other, the nodes at odd positions and at even positions end up in two chains without any new nodes. The last line attaches the saved head of the even chain to the tail of the odd chain.
Complexity: O(n) time, O(1) extra space
class Solution:
def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head:
return head
odd, even = head, head.next
even_head = even # remember where the even chain starts
while even and even.next:
odd.next = even.next # odd jumps over even
odd = odd.next
even.next = odd.next # even jumps over odd
even = even.next
odd.next = even_head # odd chain, then even chain
return headTwo pointers mark the edges of the part to cut out: before is the node right ahead of index a, and after is the node right past index b. Both are found by walking from list1, so the cut part is skipped without touching it. A third pointer finds the tail of list2. Then before points to list2 and the tail of list2 points to after.
Complexity: O(n + m) time, O(1) extra space
class Solution:
def mergeInBetween(self, list1: ListNode, a: int, b: int, list2: ListNode) -> ListNode:
before = list1
for _ in range(a - 1): # pointer 1: node right before index a
before = before.next
after = before
for _ in range(b - a + 2): # pointer 2: node right after index b
after = after.next
tail = list2
while tail.next: # last node of list2
tail = tail.next
before.next = list2 # splice list2 in
tail.next = after
return list1Use this when two sorted lists must become one sorted list, or a list can be split, sorted in halves and merged back.
Put a dummy node before the result and keep a tail pointer at its end. Compare the two current heads, attach the smaller one to tail, and advance that list. When one list runs out, attach the rest of the other in one step. Return dummy.next.
Complexity: O(n + m) time, O(1) extra space
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
dummy = tail = ListNode() # dummy removes the "which head first" case
while list1 and list2:
if list1.val <= list2.val:
tail.next, list1 = list1, list1.next
else:
tail.next, list2 = list2, list2.next
tail = tail.next
tail.next = list1 or list2 # attach the leftover list
return dummy.nextThis is merge sort, which suits linked lists because merging needs no extra array. Fast and slow pointers find the end of the first half, and the list is cut there. Each half is sorted by a recursive call. The two sorted halves are then combined with the same dummy-and-tail merge used for two sorted lists.
Complexity: O(n log n) time, O(log n) space for the recursion
class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
slow, fast = head, head.next # slow stops at the end of the first half
while fast and fast.next:
slow, fast = slow.next, fast.next.next
second = slow.next
slow.next = None # cut into two lists
return self.merge(self.sortList(head), self.sortList(second))
def merge(self, a: Optional[ListNode], b: Optional[ListNode]) -> Optional[ListNode]:
dummy = tail = ListNode()
while a and b:
if a.val <= b.val:
tail.next, a = a, a.next
else:
tail.next, b = b, b.next
tail = tail.next
tail.next = a or b
return dummy.nextMerging k lists one after another would re-walk the early lists many times. Instead, merge them in pairs: lists 0 and 1, lists 2 and 3, and so on, using the two-list merge. Each round halves the number of lists, so there are about log k rounds and every node is touched once per round. A min-heap of the k heads is the other common way to do it.
Complexity: O(N log k) time for N nodes in total, O(1) extra space
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
if not lists:
return None
while len(lists) > 1: # each round halves the number of lists
merged = []
for i in range(0, len(lists), 2):
a = lists[i]
b = lists[i + 1] if i + 1 < len(lists) else None
merged.append(self.merge(a, b))
lists = merged
return lists[0]
def merge(self, a: Optional[ListNode], b: Optional[ListNode]) -> Optional[ListNode]:
dummy = tail = ListNode()
while a and b:
if a.val <= b.val:
tail.next, a = a, a.next
else:
tail.next, b = b, b.next
tail = tail.next
tail.next = a or b
return dummy.nextUse this when you must know whether following next pointers, or any rule like i -> nums[i], ever loops back on itself.
Slow moves 1 step and fast moves 2. Without a cycle, fast reaches the end and the loop stops. With a cycle, both end up circling inside it and fast gains one step per round, so it must land on slow. The check slow is fast detects that meeting.
Complexity: O(n) time, O(1) extra space
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow = fast = head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
if slow is fast: # fast lapped slow inside a cycle
return True
return FalseRead the array as a linked list where index i points to nums[i]. Two indices hold the duplicate value, so two nodes point to the same next node, and that makes a cycle whose entrance is the duplicate. Phase one: slow and fast pointers walk until they meet inside the cycle. Phase two: reset slow to the start and move both one step at a time. They meet at the cycle entrance, which is the duplicate. Starting from index 0 is safe because no value is 0, so nothing points back to it.
Complexity: O(n) time, O(1) extra space
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
slow = fast = 0
while True: # phase 1: meet inside the cycle
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
slow = 0 # phase 2: restart one pointer from the head
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow # the cycle entrance is the duplicateEach index points to the index (i + nums[i]) mod n, so the array is a graph where every node has one next node. Run slow and fast pointers from each unvisited start. A step is only allowed when the next value has the same sign, and a step that returns to the same index is a loop of length 1, which does not count. If slow meets fast, a valid loop exists. Otherwise, zero out the dead path so no start is explored twice.
Complexity: O(n) time, O(1) extra space
class Solution:
def circularArrayLoop(self, nums: List[int]) -> bool:
n = len(nums)
def step(i: int, forward: bool) -> int:
j = (i + nums[i]) % n
if j == i or nums[j] == 0 or (nums[j] > 0) != forward:
return -1 # self-loop, dead cell or direction change
return j
for start in range(n):
if nums[start] == 0:
continue
forward = nums[start] > 0
slow = fast = start
while True:
slow = step(slow, forward) # 1 step
fast = step(fast, forward) # 2 steps
if fast != -1:
fast = step(fast, forward)
if slow == -1 or fast == -1:
break
if slow == fast:
return True
i = start # mark this dead path so it is not retried
while nums[i] != 0 and (nums[i] > 0) == forward:
j = (i + nums[i]) % n
nums[i] = 0
i = j
return FalseUse this when you need the middle node in one pass, without knowing the length of the list first.
Slow moves 1 step while fast moves 2. When fast has nothing left to jump over, it has covered the whole list while slow has covered half of it. So slow is at the middle. With an even length the loop ends one step later, which gives the second middle as the question asks.
Complexity: O(n) time, O(1) extra space
class Solution:
def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = fast = head
while fast and fast.next:
slow, fast = slow.next, fast.next.next # slow: 1 step, fast: 2 steps
return slow # 2nd middle for even lengthTo delete a node, the node before it must be known. Slow starts one node behind the head, on a dummy, while fast starts at the head. They move at the usual 1 and 2 steps, so when fast ends, slow sits exactly before the middle. Then slow.next = slow.next.next removes it. The dummy also covers a one-node list, whose only node is the middle.
Complexity: O(n) time, O(1) extra space
class Solution:
def deleteMiddle(self, head: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode(0, head) # lets a one-node list lose its only node
slow, fast = dummy, head # slow trails one node behind the middle
while fast and fast.next:
slow, fast = slow.next, fast.next.next
slow.next = slow.next.next # unlink the middle
return dummy.nextNode i and node n - 1 - i are twins, so the pairs run from the outside in. Fast and slow pointers find the middle, which is where the second half starts. Reversing the second half lets both halves be read from the front, so twins line up. Walk them together and keep the largest sum.
Complexity: O(n) time, O(1) extra space
class Solution:
def pairSum(self, head: Optional[ListNode]) -> int:
slow = fast = head
while fast and fast.next: # slow lands on the start of the second half
slow, fast = slow.next, fast.next.next
prev = None
while slow: # reverse the second half
nxt = slow.next
slow.next = prev
prev, slow = slow, nxt
best = 0
first, second = head, prev
while second: # twins are now read in step
best = max(best, first.val + second.val)
first, second = first.next, second.next
return bestUse this when you need the n-th node from the end in a single pass, by keeping two pointers a fixed gap apart.
Start both pointers on a dummy node and move fast n steps ahead, which sets the gap. Then move both together until fast is on the last node. Now slow is the node right before the one to delete, so slow.next = slow.next.next removes it. The dummy makes removing the head work with no special case.
Complexity: O(n) time, O(1) extra space
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy = ListNode(0, head)
slow = fast = dummy
for _ in range(n): # open a gap of n nodes
fast = fast.next
while fast.next: # slide the gap to the end
slow, fast = slow.next, fast.next
slow.next = slow.next.next # slow is right before the target
return dummy.nextTwo nodes are needed: the k-th from the start and the k-th from the end. The first is found by walking k - 1 steps. For the second, keep fast on that first node and start slow at the head, so the gap is fixed at k - 1. Move both until fast reaches the last node, and slow is then the k-th node from the end. Swapping the two values is enough.
Complexity: O(n) time, O(1) extra space
class Solution:
def swapNodes(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
first = head
for _ in range(k - 1): # k-th node from the start
first = first.next
slow, fast = head, first # fast is already k - 1 steps ahead
while fast.next:
slow, fast = slow.next, fast.next
first.val, slow.val = slow.val, first.val # slow is the k-th node from the end
return headRotating right by k means the last k nodes move to the front. Count the length first and reduce k with k %= length, since a full turn changes nothing. Then use the gap idea: move fast k steps ahead, and slide both until fast is on the last node. slow is now the new tail, slow.next is the new head. Cut after slow and join the old tail to the old head.
Complexity: O(n) time, O(1) extra space
class Solution:
def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
if not head or not head.next:
return head
length, cur = 1, head
while cur.next: # count the nodes
cur = cur.next
length += 1
k %= length # a full turn changes nothing
if k == 0:
return head
slow = fast = head
for _ in range(k): # gap of k nodes
fast = fast.next
while fast.next: # slow stops on the new tail
slow, fast = slow.next, fast.next
new_head = slow.next
slow.next = None # cut after the new tail
fast.next = head # old tail -> old head
return new_headUse this when two lists may join into one shared tail and you must find the first shared node without extra memory.
Pointer p walks list A and then list B. Pointer q walks list B and then list A. Both walk the same total distance, lenA + lenB, so the different lengths cancel out. They arrive at the shared node on the same step, or they both reach None together if the lists never join. The test p is not q ends the loop in either case.
Complexity: O(n + m) time, O(1) extra space
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
p, q = headA, headB
while p is not q:
p = p.next if p else headB # after A, continue on B
q = q.next if q else headA # after B, continue on A
return p # shared node, or NoneFirst find any node m inside the cycle with fast and slow pointers. Cut the list right after m. Now there are two lists that both end at m: one starting at head, one starting at the old m.next. They share a tail that begins exactly at the cycle entrance. Run the two-list intersection walk on them, then restore the link, and the shared node is the answer.
Complexity: O(n) time, O(1) extra space
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = fast = head
while fast and fast.next: # step 1: meet inside the cycle
slow, fast = slow.next, fast.next.next
if slow is fast:
break
else:
return None # no cycle
loop_start = slow.next # second list starts after the meeting node
slow.next = None # cut: two lists, both ending at the meeting node
p, q = head, loop_start
while p is not q: # step 2: the intersection walk
p = p.next if p else loop_start
q = q.next if q else head
slow.next = loop_start # put the cycle back
return p # first shared node = cycle entranceUse this when a list must be reversed block by block, with a fixed or growing block size, and a short last block needs its own rule.
A pair is a group of size 2. If fewer than two nodes remain, return head unchanged. Otherwise solve the rest of the list first with a recursive call and attach it to the first node of the pair. Then flip the pair so the second node points to the first, and return the second node as the new head of this piece.
Complexity: O(n) time, O(n) space for the recursion
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next: # fewer than 2 nodes: leave as is
return head
first, second = head, head.next
first.next = self.swapPairs(second.next) # the rest is already solved
second.next = first # flip the pair
return secondThe groups have sizes 1, 2, 3, and so on, and the last group can be shorter than its size. For each group, first count how many nodes it really has. Reverse it only if that count is even. The reversal is the same flip loop as for reverse-in-k, where prev starts as the node after the group so the reversed part is already linked to the rest. before always points to the last node of the finished part.
Complexity: O(n) time, O(1) extra space
class Solution:
def reverseEvenLengthGroups(self, head: Optional[ListNode]) -> Optional[ListNode]:
before, size = head, 2 # first group (size 1) never changes
while before.next:
node, count = before, 0
while count < size and node.next: # real length of this group
node, count = node.next, count + 1
if count % 2 == 0:
tail = before.next # first node becomes the group's tail
prev, cur = node.next, before.next # prev starts as the rest of the list
for _ in range(count): # reverse `count` nodes
cur.next, prev, cur = prev, cur, cur.next
before.next = prev
before = tail
else:
before = node
size += 1
return headFor each block, first check that k nodes really exist ahead of before, and return the list as it is if not, so a short last block stays unchanged. Then reverse exactly k nodes with the save, flip, advance loop, where prev starts as the node after the block so the reversed block is already linked to the rest. Link before to the new block head, and move before to the block's old first node, which is now its tail.
Complexity: O(n) time, O(1) extra space
class Solution:
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
dummy = ListNode(0, head)
before = dummy # last node of the finished part
while True:
node = before
for _ in range(k): # is there a full group of k?
node = node.next
if not node:
return dummy.next # short tail stays as is
tail = before.next # first node becomes the group's tail
prev, cur = node.next, before.next # prev starts as the rest of the list
for _ in range(k): # flip k nodes
cur.next, prev, cur = prev, cur, cur.next
before.next = prev
before = tailLast in, first out. Use it when the most recent unfinished item must be handled first.
Use this when the most recent unfinished item must be handled first, such as undoing a step, evaluating postfix or processing nested groups.
Each valid score is one item on the stack. A number is pushed, `C` pops the last score (undo), `D` pushes double of `scores[-1]` and `+` pushes `scores[-1] + scores[-2]`. The answer is the sum of what is left on the stack at the end.
Complexity: O(n) time, O(n) extra space
class Solution:
def calPoints(self, operations: List[str]) -> int:
scores = [] # stack of valid round scores
for op in operations:
if op == "+":
scores.append(scores[-1] + scores[-2]) # peek the top two
elif op == "D":
scores.append(2 * scores[-1]) # peek the top
elif op == "C":
scores.pop() # undo the last score
else:
scores.append(int(op)) # push a new score
return sum(scores)Push every number onto the stack. An operator works on the two most recent numbers, so pop the right operand first, then the left, and push the result back. The single item left at the end is the answer. Division must truncate toward zero, which is why the code uses `int(a / b)`.
Complexity: O(n) time, O(n) extra space
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
st = [] # stack of operands
for t in tokens:
if t in ("+", "-", "*", "/"):
b = st.pop() # right operand is on top
a = st.pop() # left operand is below it
if t == "+":
st.append(a + b)
elif t == "-":
st.append(a - b)
elif t == "*":
st.append(a * b)
else:
st.append(int(a / b)) # truncate toward zero
else:
st.append(int(t)) # push a number
return st[0]Scan the string once, building the current number and remembering the sign of the next term. On `(` push the result so far and its sign onto the stack and start a fresh sub-total. On `)` close the sub-total, pop the saved pair and fold it in with `prev_result + prev_sign * result`. The stack remembers each unfinished outer expression, so nesting can be any depth.
Complexity: O(n) time, O(n) extra space
class Solution:
def calculate(self, s: str) -> int:
st = [] # saved (result, sign) for each open "("
result, num, sign = 0, 0, 1
for ch in s:
if ch.isdigit():
num = num * 10 + int(ch)
elif ch in "+-":
result += sign * num # finish the previous term
num = 0
sign = 1 if ch == "+" else -1
elif ch == "(":
st.append((result, sign)) # push the unfinished outer expression
result, sign = 0, 1
elif ch == ")":
result += sign * num
num = 0
prev_result, prev_sign = st.pop() # resume the outer expression
result = prev_result + prev_sign * result
return result + sign * numUse this when brackets must close in the right order: push each opener, and on a closer pop the latest opener and compare.
Push every opener. When a closer arrives, the latest unclosed opener must be its partner, so pop and compare with the `pairs` map. If the stack is empty or the pair is wrong, the string is invalid. At the end the stack must be empty.
Complexity: O(n) time, O(n) extra space
class Solution:
def isValid(self, s: str) -> bool:
pairs = {")": "(", "]": "[", "}": "{"}
st = []
for ch in s:
if ch in pairs: # a closer
if not st or st.pop() != pairs[ch]: # latest opener must match
return False
else:
st.append(ch) # an opener
return not st # nothing left unclosedUse the stack to hold the indices of `(` that have not found a partner yet. A `)` with an empty stack has no opener, so blank it out. After the scan, every index still on the stack is an unmatched `(`, so blank those too. What remains is valid with the fewest removals.
Complexity: O(n) time, O(n) extra space
class Solution:
def minRemoveToMakeValid(self, s: str) -> str:
chars = list(s)
st = [] # indices of "(" still waiting for a ")"
for i, ch in enumerate(chars):
if ch == "(":
st.append(i)
elif ch == ")":
if st:
st.pop() # matched with the latest opener
else:
chars[i] = "" # no opener for this closer: drop it
for i in st:
chars[i] = "" # leftover openers: drop them
return "".join(chars)Keep indices on the stack, with `-1` at the bottom as the position just before the current valid stretch. Push the index of every `(`. On `)` pop; if the stack is now empty, this `)` is unmatched, so push its index as the new base. Otherwise the valid stretch ends at `i` and starts after `st[-1]`, so its length is `i - st[-1]`.
Complexity: O(n) time, O(n) extra space
class Solution:
def longestValidParentheses(self, s: str) -> int:
st = [-1] # index just before the current valid stretch
best = 0
for i, ch in enumerate(s):
if ch == "(":
st.append(i)
else:
st.pop() # match with the latest opener (or the base)
if not st:
st.append(i) # unmatched ")": new base
else:
best = max(best, i - st[-1])
return bestUse this when each element's answer depends on the nearest bigger or smaller element on one side, so you keep the stack sorted and pop whatever the new value beats.
Sort the cars by position and walk from the one closest to the target. Each car has an arrival time if nothing blocks it. If its time is longer than the time on top of the stack, it can never catch the fleet ahead, so it starts a new fleet and is pushed. Otherwise it joins the fleet ahead. The stack stays strictly increasing in time and its size is the number of fleets.
Complexity: O(n log n) time, O(n) extra space
class Solution:
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
cars = sorted(zip(position, speed), reverse=True) # closest to the target first
st = [] # arrival time of each fleet
for pos, spd in cars:
t = (target - pos) / spd
if not st or t > st[-1]: # slower than the fleet ahead: new fleet
st.append(t) # stack stays increasing
# otherwise it catches up and merges into the fleet ahead
return len(st)Scan from the right and keep a decreasing stack. When the current value `x` is bigger than the stack top, the popped values are smaller numbers to its right, so `x` can be the "3" and the largest popped value is the best "2", stored in `third`. Any later value that is smaller than `third` is the "1", so the answer is True. Each number is pushed and popped once.
Complexity: O(n) time, O(n) extra space
from math import inf
class Solution:
def find132pattern(self, nums: List[int]) -> bool:
third = -inf # best "2" so far: it has a bigger "3" on its left
st = [] # decreasing stack, scanning from the right
for x in reversed(nums):
if x < third: # x is the "1"
return True
while st and st[-1] < x: # x beats these: x is their "3"
third = st.pop() # last pop is the largest candidate for "2"
st.append(x)
return FalseKeep a stack of bar indices with decreasing heights. When a taller bar `h` arrives, each shorter bar popped is a valley floor. The new stack top is its left wall and `h` is its right wall. That layer holds width times (lower wall minus floor) of water, and the layers add up to the total.
Complexity: O(n) time, O(n) extra space
class Solution:
def trap(self, height: List[int]) -> int:
st = [] # indices of bars, heights decreasing
water = 0
for i, h in enumerate(height):
while st and height[st[-1]] < h: # h is taller: the top is a valley floor
floor = st.pop()
if not st:
break # no left wall, nothing is trapped
left = st[-1]
width = i - left - 1
water += width * (min(height[left], h) - height[floor])
st.append(i)
return waterUse this when you need, for each element, the first larger value to its right, or how many steps away it is.
Run the decreasing-stack loop over `nums2`. Whenever a new value `x` is larger than the top, `x` is the next greater element of the popped value, so record it in a dict. Values never popped have no next greater element. Then look up each value of `nums1` in the dict.
Complexity: O(n + m) time, O(n) extra space
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
nxt = {} # value -> its next greater value in nums2
st = [] # decreasing stack of values
for x in nums2:
while st and st[-1] < x: # x is the answer for everything it beats
nxt[st.pop()] = x
st.append(x)
return [nxt.get(x, -1) for x in nums1]Keep a stack of day indices whose temperatures are decreasing, so they are the days still waiting for a warmer day. When day `i` is warmer than the top, pop that day `j` and its answer is `i - j`. Push `i` and carry on. Days left on the stack never get a warmer day and keep 0.
Complexity: O(n) time, O(n) extra space
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
ans = [0] * len(temperatures)
st = [] # indices of days waiting, temperatures decreasing
for i, t in enumerate(temperatures):
while st and temperatures[st[-1]] < t: # today is warmer than the top
j = st.pop()
ans[j] = i - j # day j found its warmer day
st.append(i)
return ansThe array is circular, so walk it twice (`2 * n` steps) and read values with `i % n`. Use the same decreasing stack of indices and answer each popped index with the new value. Only push indices during the first lap, so every index is pushed once. Indices still on the stack after two laps have no greater element and keep -1.
Complexity: O(n) time, O(n) extra space
class Solution:
def nextGreaterElements(self, nums: List[int]) -> List[int]:
n = len(nums)
ans = [-1] * n
st = [] # indices, values decreasing
for i in range(2 * n): # two laps simulate the circle
x = nums[i % n]
while st and nums[st[-1]] < x: # x is the next greater of the top
ans[st.pop()] = x
if i < n:
st.append(i) # push only in the first lap
return ansUse this when you need the first smaller value to the right, or when a bigger item must be dropped as soon as a smaller one shows up.
Keep an increasing stack of indices, flipping the rule from next greater to next smaller. When the current price `p` is less than or equal to the top price, `p` is the discount for that item, so pop it and subtract. Items never popped get no discount and keep their full price.
Complexity: O(n) time, O(n) extra space
class Solution:
def finalPrices(self, prices: List[int]) -> List[int]:
ans = prices[:] # full price unless a discount is found
st = [] # indices, prices increasing
for i, p in enumerate(prices):
while st and prices[st[-1]] >= p: # p is the next smaller-or-equal price
j = st.pop()
ans[j] = prices[j] - p # apply the discount
st.append(i)
return ansTo make the number small, a digit that is bigger than the one after it should go. Keep an increasing stack of digits. When the new digit is smaller than the top, pop the top (it just found its next smaller digit) and use up one removal. If removals are left at the end, cut them from the tail, then strip leading zeros.
Complexity: O(n) time, O(n) extra space
class Solution:
def removeKdigits(self, num: str, k: int) -> str:
st = [] # digits, non-decreasing
for d in num:
while k and st and st[-1] > d: # d is smaller: drop the bigger digit before it
st.pop()
k -= 1
st.append(d)
if k:
st = st[:-k] # stack is non-decreasing, so cut the largest tail
return "".join(st).lstrip("0") or "0"Build the smallest result with an increasing stack of letters, using the next-smaller rule: a bigger letter on top is dropped when a smaller letter arrives. The extra idea is that a letter may only be dropped if it appears again later, which `last[...] > i` checks. The `seen` set keeps each letter in the stack at most once.
Complexity: O(n) time, O(1) extra space (26 letters)
class Solution:
def removeDuplicateLetters(self, s: str) -> str:
last = {c: i for i, c in enumerate(s)} # last position of each letter
st, seen = [], set()
for i, c in enumerate(s):
if c in seen:
continue # already placed in the best spot
while st and st[-1] > c and last[st[-1]] > i: # bigger on top and it comes again later
seen.discard(st.pop())
st.append(c)
seen.add(c)
return "".join(st)Use this when each element needs the nearest smaller or bigger value on its left, which is what sits on top of the stack right after popping.
Count how many subarrays have `arr[mid]` as their minimum. Keep an increasing stack. When `mid` is popped by index `i`, the top after popping is the previous smaller element, so `left` is read from `st[-1]`, and `i` is the next smaller. `mid` is the minimum of `(mid - left) * (i - mid)` subarrays. A sentinel at the end pops everything.
Complexity: O(n) time, O(n) extra space
class Solution:
def sumSubarrayMins(self, arr: List[int]) -> int:
st, total = [], 0 # st: indices, values increasing
for i in range(len(arr) + 1):
cur = arr[i] if i < len(arr) else -1 # sentinel pops everything
while st and arr[st[-1]] > cur: # cur is the next smaller of the top
mid = st.pop()
left = st[-1] if st else -1 # previous smaller: top after popping
total += arr[mid] * (mid - left) * (i - mid)
st.append(i)
return total % (10**9 + 7)Treat each element as the minimum of a subarray and stretch it as far as it stays the minimum. When `mid` is popped by index `i`, the previous smaller element is `st[-1]` and the next smaller is `i`. The subarray runs from `st[-1] + 1` to `i - 1`, and a prefix-sum array gives its sum in O(1). Take the best `nums[mid] * sum`, and apply the modulo only at the end.
Complexity: O(n) time, O(n) extra space
class Solution:
def maxSumMinProduct(self, nums: List[int]) -> int:
prefix = [0]
for x in nums:
prefix.append(prefix[-1] + x) # prefix[i] = sum of nums[:i]
st, best = [], 0 # st: indices, values increasing
for i in range(len(nums) + 1):
cur = nums[i] if i < len(nums) else 0 # sentinel 0 is below every value
while st and nums[st[-1]] >= cur: # cur is the next smaller (or equal)
mid = st.pop()
left = st[-1] + 1 if st else 0 # just after the previous smaller
best = max(best, nums[mid] * (prefix[i] - prefix[left]))
st.append(i)
return best % (10**9 + 7)A subarray's range is its max minus its min, so the answer is the sum of all maxes minus the sum of all mins. The sum of mins comes from the previous-smaller and next-smaller stack loop. The max of a subarray is the min of the negated array, so the same helper gives the sum of maxes. Two passes of the same loop solve the problem in O(n).
Complexity: O(n) time, O(n) extra space
from math import inf
class Solution:
def subArrayRanges(self, nums: List[int]) -> int:
def sum_of_mins(a):
st, total = [], 0 # st: indices, values increasing
for i in range(len(a) + 1):
cur = a[i] if i < len(a) else -inf # sentinel pops everything
while st and a[st[-1]] > cur:
mid = st.pop()
left = st[-1] if st else -1 # previous smaller: top after popping
total += a[mid] * (mid - left) * (i - mid)
st.append(i)
return total
# max of a subarray is the min of the negated array
return -sum_of_mins([-x for x in nums]) - sum_of_mins(nums)Use this when you need how many consecutive earlier items are not bigger than the current one, which is how far you can stretch to the left.
Store `(price, span)` pairs on a decreasing stack. A new price swallows every earlier day whose price is less than or equal to it, so pop them and add their spans to today's span of 1. Push the new pair. Each price is pushed and popped at most once, so the cost per call is amortised O(1).
Complexity: O(1) amortised per call, O(n) extra space
class StockSpanner:
def __init__(self):
self.st = [] # (price, span), prices strictly decreasing
def next(self, price: int) -> int:
span = 1 # today counts
while self.st and self.st[-1][0] <= price:
span += self.st.pop()[1] # absorb the span of each smaller-or-equal day
self.st.append((price, span))
return spanThe stock span of `nums[i]` counts the subarrays ending at `i` where `nums[i]` is the maximum: `i - p`, with `p` the previous greater index. Those count only if `nums[i]` lies in `[left, right]`. Every subarray that starts at or before `p` has the same maximum as the ones ending at `p`, so add `ways[p]`. The stack finds `p` for each index.
Complexity: O(n) time, O(n) extra space
class Solution:
def numSubarrayBoundedMax(self, nums: List[int], left: int, right: int) -> int:
st = [] # indices, values strictly decreasing
ways = [0] * len(nums) # ways[i]: good subarrays ending at i
for i, x in enumerate(nums):
while st and nums[st[-1]] <= x: # pop days that are not greater than x
st.pop()
p = st[-1] if st else -1 # previous greater element
span = i - p # stock span of x
ways[i] = span if left <= x <= right else 0
if p >= 0:
ways[i] += ways[p] # longer subarrays keep the maximum of p
st.append(i)
return sum(ways)Turn each row into a histogram of consecutive ones above it. For column `j`, count the all-ones rectangles whose bottom-right corner is at `(row, j)`. The stack gives `p`, the previous column with a smaller height, which is the stock span idea. Rectangles within `(p, j]` number `heights[j] * (j - p)`, and those reaching past `p` are already counted in `count[p]`.
Complexity: O(m * n) time, O(n) extra space
class Solution:
def numSubmat(self, mat: List[List[int]]) -> int:
n = len(mat[0])
heights = [0] * n
answer = 0
for row in mat:
for j in range(n):
heights[j] = heights[j] + 1 if row[j] else 0 # histogram for this row
st = [] # column indices, heights increasing
count = [0] * n # rectangles with bottom-right corner (row, j)
for j in range(n):
while st and heights[st[-1]] >= heights[j]:
st.pop()
if st:
p = st[-1] # previous column with a smaller height
count[j] = count[p] + heights[j] * (j - p)
else:
count[j] = heights[j] * (j + 1) # stretches to the left edge
st.append(j)
answer += count[j]
return answerUse this when each bar is the shortest in some rectangle and you need how far that rectangle can stretch to the left and right.
Keep an increasing stack of bar indices. When a shorter bar at `i` arrives, the popped bar has found its right limit `i`, and its left limit is the new top `st[-1]` (or -1). The widest rectangle with the popped bar as the shortest is `height * (i - left - 1)`. Appending a 0 as a sentinel forces every remaining bar to be popped.
Complexity: O(n) time, O(n) extra space
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
st, best = [], 0 # st: indices of bars, heights increasing
for i, h in enumerate(heights + [0]): # sentinel 0 flushes the stack
while st and heights[st[-1]] > h: # bar i is the next smaller of the top
height = heights[st.pop()]
left = st[-1] if st else -1 # previous smaller bar
best = max(best, height * (i - left - 1))
st.append(i)
return bestBuild a histogram row by row: a column grows by 1 on a "1" and resets to 0 on a "0". After each row, the biggest all-ones rectangle that ends on that row is the largest rectangle in that histogram. Reuse the same stack routine for every row and keep the best.
Complexity: O(rows * cols) time, O(cols) extra space
class Solution:
def maximalRectangle(self, matrix: List[List[str]]) -> int:
if not matrix:
return 0
heights = [0] * len(matrix[0])
best = 0
for row in matrix:
for j, cell in enumerate(row):
heights[j] = heights[j] + 1 if cell == "1" else 0 # histogram for this row
best = max(best, self.largest_in_histogram(heights))
return best
def largest_in_histogram(self, heights: List[int]) -> int:
st, best = [], 0 # st: indices of bars, heights increasing
for i, h in enumerate(heights + [0]): # sentinel 0 flushes the stack
while st and heights[st[-1]] > h:
height = heights[st.pop()]
left = st[-1] if st else -1
best = max(best, height * (i - left - 1))
st.append(i)
return bestThe score is `min * length` of a subarray that must contain index `k`, which is a rectangle in the histogram `nums`. Run the largest-rectangle loop, but only count a rectangle when it covers `k`, that is `left < k < i`. The popped bar is the minimum of that rectangle, and its width is `i - left - 1`.
Complexity: O(n) time, O(n) extra space
class Solution:
def maximumScore(self, nums: List[int], k: int) -> int:
st, best = [], 0 # st: indices of bars, heights increasing
for i, h in enumerate(nums + [0]): # sentinel 0 flushes the stack
while st and nums[st[-1]] > h:
mid = st.pop() # mid is the minimum of the rectangle
left = st[-1] if st else -1 # previous smaller bar
if left < k < i: # the rectangle must contain index k
best = max(best, nums[mid] * (i - left - 1))
st.append(i)
return bestUse this when a stack must also answer a question about everything beneath its top, such as the minimum or the most frequent value, in O(1).
Store a pair `(value, min_so_far)` in every entry. The new minimum is the smaller of the pushed value and the minimum of the entry below it. `pop` just removes the top pair, which restores the earlier minimum automatically. `getMin` reads the second item of the top pair.
Complexity: O(1) time per operation, O(n) extra space
class MinStack:
def __init__(self):
self.st = [] # (value, min_so_far)
def push(self, val: int) -> None:
m = min(val, self.st[-1][1]) if self.st else val # min of everything beneath, plus val
self.st.append((val, m))
def pop(self) -> None:
self.st.pop()
def top(self) -> int:
return self.st[-1][0]
def getMin(self) -> int:
return self.st[-1][1] # the top remembers the minimumSame idea as Min Stack: keep extra information next to each entry. `inc[i]` is a pending add that applies to `st[0..i]`. `increment` only changes one cell, so it is O(1). On `pop`, add the pending value to the popped item and pass it down to the entry below, so it is still applied to the rest.
Complexity: O(1) time per operation, O(maxSize) extra space
class CustomStack:
def __init__(self, maxSize: int):
self.max_size = maxSize
self.st = []
self.inc = [] # inc[i]: pending add for st[0..i]
def push(self, x: int) -> None:
if len(self.st) < self.max_size:
self.st.append(x)
self.inc.append(0)
def pop(self) -> int:
if not self.st:
return -1
add = self.inc.pop()
if self.inc:
self.inc[-1] += add # hand the pending add to the entry below
return self.st.pop() + add
def increment(self, k: int, val: int) -> None:
i = min(k, len(self.st)) - 1
if i >= 0:
self.inc[i] += val # remember it in one cell: O(1)The stack must answer "most frequent value, ties by most recent". Keep one stack per frequency: when a value reaches count `f`, push it onto `groups[f]`. The top of `groups[max_freq]` is the answer, and the value stays in the lower groups for its smaller counts. Each entry stores what is needed to answer in O(1), like the Min Stack idea.
Complexity: O(1) time per operation, O(n) extra space
from collections import defaultdict
class FreqStack:
def __init__(self):
self.freq = defaultdict(int) # value -> current count
self.groups = defaultdict(list) # count -> stack of values that reached it
self.max_freq = 0
def push(self, val: int) -> None:
self.freq[val] += 1
f = self.freq[val]
self.max_freq = max(self.max_freq, f)
self.groups[f].append(val) # val joins the stack for its new count
def pop(self) -> int:
val = self.groups[self.max_freq].pop() # most recent among the most frequent
self.freq[val] -= 1
if not self.groups[self.max_freq]:
self.max_freq -= 1 # that frequency level is now empty
return valFirst in, first out (queue), or add/remove at both ends (deque).
Use this when items must be handled in the order they arrived, or when old items expire from the front while new ones join at the back.
Every ping joins the back of a deque, and the deque always holds the calls from the last 3000 ms.
Calls are added in increasing time order, so the oldest call is always at the front.
The while loop pops expired calls from the front with popleft, which is O(1). The answer is the size of the deque.
Complexity: O(1) amortized per ping, O(W) space where W is the number of calls in the window
from collections import deque
class RecentCounter:
def __init__(self):
self.q = deque()
def ping(self, t: int) -> int:
self.q.append(t) # newest call joins the back
while self.q[0] < t - 3000: # oldest calls leave from the front
self.q.popleft()
return len(self.q)A queue gives out the oldest item first, but a stack must give out the newest item first.
After each push, rotate the queue: move every older item from the front to the back, so the new item ends up at the front.
Then pop is just popleft and top is the front item. The rotation loop in push is the key line.
Complexity: push O(n), pop O(1), top O(1), empty O(1); O(n) space
from collections import deque
class MyStack:
def __init__(self):
self.q = deque()
def push(self, x: int) -> None:
self.q.append(x)
for _ in range(len(self.q) - 1): # send every older item behind x
self.q.append(self.q.popleft())
def pop(self) -> int:
return self.q.popleft() # the front is the newest item
def top(self) -> int:
return self.q[0]
def empty(self) -> bool:
return not self.qKeep two queues holding the positions of the Radiant and Dire senators, in voting order.
Each round, take the front senator of each queue. The one with the smaller position votes first and bans the other.
The winner goes back to the end of its queue with position + n, which means "I vote again next round". The senator who was banned is not re-added.
The loop ends when one queue is empty.
Complexity: O(n) time, O(n) space
from collections import deque
class Solution:
def predictPartyVictory(self, senate: str) -> str:
n = len(senate)
radiant, dire = deque(), deque()
for i, party in enumerate(senate):
(radiant if party == "R" else dire).append(i)
while radiant and dire:
r, d = radiant.popleft(), dire.popleft()
if r < d:
radiant.append(r + n) # winner re-joins the back for the next round
else:
dire.append(d + n)
return "Radiant" if radiant else "Dire"Use this when you need a fixed-size buffer that reuses freed slots by wrapping indices with modulo, or when positions on a ring wrap around.
Store the items in a fixed array and track only head (index of the front item) and size.
The slot for a new item is (head + size) % cap, so after the end of the array it wraps back to index 0 and reuses freed slots.
Tracking size tells full (size == cap) from empty (size == 0), which two indices alone cannot do.
Complexity: O(1) time per operation, O(k) space
class MyCircularQueue:
def __init__(self, k: int):
self.data = [0] * k
self.cap = k
self.head = 0 # index of the front item
self.size = 0 # track size to tell full from empty
def enQueue(self, value: int) -> bool:
if self.isFull():
return False
self.data[(self.head + self.size) % self.cap] = value # wrap with modulo
self.size += 1
return True
def deQueue(self) -> bool:
if self.isEmpty():
return False
self.head = (self.head + 1) % self.cap # freed slot is reused later
self.size -= 1
return True
def Front(self) -> int:
return -1 if self.isEmpty() else self.data[self.head]
def Rear(self) -> int:
return -1 if self.isEmpty() else self.data[(self.head + self.size - 1) % self.cap]
def isEmpty(self) -> bool:
return self.size == 0
def isFull(self) -> bool:
return self.size == self.capSame fixed array with head and size, but now both ends can grow.
Insert at the back writes to (head + size) % cap. Insert at the front first moves head one step left with (head - 1) % cap, which wraps from 0 to the last index.
Deleting from the back only shrinks size. Deleting from the front moves head right.
Complexity: O(1) time per operation, O(k) space
class MyCircularDeque:
def __init__(self, k: int):
self.data = [0] * k
self.cap = k
self.head = 0
self.size = 0
def insertFront(self, value: int) -> bool:
if self.isFull():
return False
self.head = (self.head - 1) % self.cap # step left, wrapping 0 -> cap - 1
self.data[self.head] = value
self.size += 1
return True
def insertLast(self, value: int) -> bool:
if self.isFull():
return False
self.data[(self.head + self.size) % self.cap] = value
self.size += 1
return True
def deleteFront(self) -> bool:
if self.isEmpty():
return False
self.head = (self.head + 1) % self.cap
self.size -= 1
return True
def deleteLast(self) -> bool:
if self.isEmpty():
return False
self.size -= 1
return True
def getFront(self) -> int:
return -1 if self.isEmpty() else self.data[self.head]
def getRear(self) -> int:
return -1 if self.isEmpty() else self.data[(self.head + self.size - 1) % self.cap]
def isEmpty(self) -> bool:
return self.size == 0
def isFull(self) -> bool:
return self.size == self.capThe friends sit on a ring, so counting k steps from the current spot must wrap past the end of the list.
The line idx = (idx + k - 1) % len(friends) is the same modulo wrap as in the circular queue.
That index is the friend who leaves. Counting restarts at the friend now sitting at the same index, so idx is kept as it is.
Repeat until one friend remains.
Complexity: O(n^2) time (each pop shifts the list), O(n) space
class Solution:
def findTheWinner(self, n: int, k: int) -> int:
friends = list(range(1, n + 1))
idx = 0
while len(friends) > 1:
idx = (idx + k - 1) % len(friends) # walk k steps around the ring
friends.pop(idx) # next count starts at this same index
return friends[0]Use this when you need the maximum of every window of size k as it slides across an array, or the best earlier value inside a window that moves with the current position.
Keep a deque of indices whose values are decreasing, so the front index is always the window maximum.
For each new x, pop smaller values off the back, because they can never be a maximum again while x is in the window.
Pop the front if its index has left the window (dq[0] <= i - k). Once i >= k - 1, record nums[dq[0]].
Each index enters and leaves the deque once, so the whole pass is O(n) instead of O(nk).
Complexity: O(n) time, O(k) extra space
from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
dq, out = deque(), [] # dq holds indices, values decreasing
for i, x in enumerate(nums):
while dq and nums[dq[-1]] <= x: # smaller values behind x are useless
dq.pop()
dq.append(i)
if dq[0] <= i - k: # front index left the window
dq.popleft()
if i >= k - 1:
out.append(nums[dq[0]]) # front is the window maximum
return outLet dp[i] be the best sum of a valid subsequence that ends at index i. Then dp[i] = nums[i] + max(0, best dp among the previous k indices).
That "best dp in the last k indices" is a sliding window maximum, so keep a deque of indices with decreasing dp values.
Pop the front when it is more than k behind i, read the maximum from dp[dq[0]], then push i after popping smaller dp values.
The answer is the largest dp[i].
Complexity: O(n) time, O(n) space
from math import inf
from collections import deque
class Solution:
def constrainedSubsetSum(self, nums: List[int], k: int) -> int:
dp = [0] * len(nums)
dq = deque() # indices, dp values decreasing
best = -inf
for i, x in enumerate(nums):
if dq and dq[0] < i - k: # front is more than k behind
dq.popleft()
dp[i] = x + (dp[dq[0]] if dq and dp[dq[0]] > 0 else 0)
while dq and dp[dq[-1]] <= dp[i]: # smaller dp values are never the max again
dq.pop()
dq.append(i)
best = max(best, dp[i])
return bestFor i < j the value is yi + yj + xj - xi, which splits into (yi - xi) + (yj + xj). For each j we want the largest (yi - xi) among earlier points with xj - xi <= k.
That is a sliding window maximum, where the window is defined by the x distance instead of by index count.
The deque keeps (yi - xi, xi) with decreasing first values. Pop from the front while xj - xi > k, then the front gives the best partner for j.
Complexity: O(n) time, O(n) space
from math import inf
from collections import deque
class Solution:
def findMaxValueOfEquation(self, points: List[List[int]], k: int) -> int:
dq = deque() # (y - x, x), y - x decreasing
best = -inf
for x, y in points:
while dq and x - dq[0][1] > k: # too far to the left
dq.popleft()
if dq:
best = max(best, dq[0][0] + y + x) # best (yi - xi) + (yj + xj)
while dq and dq[-1][0] <= y - x: # smaller y - x is never the max again
dq.pop()
dq.append((y - x, x))
return bestUse this when you must keep reading the minimum or maximum of a range whose left edge moves, such as a variable window or a range of earlier prefix sums.
A window is valid when its max minus its min is at most limit. Keep two deques of indices: one with decreasing values (front = window max) and one with increasing values (front = window min).
Move right one step at a time. While the window is invalid, move left forward and pop any front index that fell off the left edge.
The longest valid window seen is the answer.
Complexity: O(n) time, O(n) space
from collections import deque
class Solution:
def longestSubarray(self, nums: List[int], limit: int) -> int:
maxq, minq = deque(), deque() # indices; maxq values decreasing, minq increasing
left = best = 0
for right, x in enumerate(nums):
while maxq and nums[maxq[-1]] <= x:
maxq.pop()
maxq.append(right)
while minq and nums[minq[-1]] >= x:
minq.pop()
minq.append(right)
while nums[maxq[0]] - nums[minq[0]] > limit: # window invalid: shrink from the left
left += 1
if maxq[0] < left:
maxq.popleft()
if minq[0] < left:
minq.popleft()
best = max(best, right - left + 1)
return bestA circular subarray is an ordinary subarray of nums + nums that is at most n long. Build prefix sums over the doubled array.
For each end j, the best sum is prefix[j] minus the smallest prefix[i] with j - n <= i < j. That is a window minimum over prefix values.
Keep a deque of indices with increasing prefix values. Pop the front when it is more than n behind j, and read the minimum from the front.
Complexity: O(n) time, O(n) space
from math import inf
from collections import deque
class Solution:
def maxSubarraySumCircular(self, nums: List[int]) -> int:
n = len(nums)
prefix = [0]
for x in nums + nums: # prefix sums of the doubled array
prefix.append(prefix[-1] + x)
dq = deque([0]) # indices, prefix values increasing
best = -inf
for j in range(1, 2 * n + 1):
if dq[0] < j - n: # subarray would be longer than n
dq.popleft()
best = max(best, prefix[j] - prefix[dq[0]]) # front = smallest prefix in range
while dq and prefix[dq[-1]] >= prefix[j]:
dq.pop()
dq.append(j)
return bestWith prefix sums, a subarray (i, j] has sum prefix[j] - prefix[i]. We want the closest i for which this is at least k. Negative numbers rule out a plain two-pointer window.
Keep a deque of indices with increasing prefix values. While the front gives sum >= k, record j - i and pop it: any later j would only be longer.
Before pushing j, pop back indices whose prefix is >= prefix[j]. They start longer and smaller-sum subarrays than j, so they are never better.
Complexity: O(n) time, O(n) space
from math import inf
from collections import deque
class Solution:
def shortestSubarray(self, nums: List[int], k: int) -> int:
prefix = [0]
for x in nums:
prefix.append(prefix[-1] + x)
dq = deque() # indices, prefix values increasing
best = inf
for j, p in enumerate(prefix):
while dq and p - prefix[dq[0]] >= k: # valid: the front cannot do better later
best = min(best, j - dq.popleft())
while dq and prefix[dq[-1]] >= p: # keep the deque increasing
dq.pop()
dq.append(j)
return best if best != inf else -1Use this when you need the fewest steps in an unweighted graph, grid or word-transformation puzzle, or must visit nodes level by level.
Build an adjacency list, then run BFS from the source with a queue and a seen set.
Mark a node as seen when you push it, not when you pop it, so no node enters the queue twice.
If the destination is ever popped, a path exists. If the queue empties first, it does not.
Complexity: O(V + E) time, O(V + E) space
from collections import deque
class Solution:
def validPath(self, n: int, edges: List[List[int]], source: int, destination: int) -> bool:
adj = [[] for _ in range(n)]
for a, b in edges:
adj[a].append(b)
adj[b].append(a)
seen, q = {source}, deque([source])
while q:
u = q.popleft()
if u == destination:
return True
for v in adj[u]:
if v not in seen:
seen.add(v) # mark seen on push
q.append(v)
return FalsePut every rotten orange in the queue first (multi-source BFS), and count the fresh ones.
Each pass of the for _ in range(len(q)) loop handles one level of the queue, which is one minute of rotting spread.
A fresh neighbour is turned rotten and pushed in the same step, so it is never queued twice.
If fresh oranges are left when the queue empties, return -1.
Complexity: O(rows * cols) time and space
from collections import deque
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
rows, cols = len(grid), len(grid[0])
q, fresh = deque(), 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
q.append((r, c)) # all rotten oranges start together
elif grid[r][c] == 1:
fresh += 1
minutes = 0
while q and fresh:
for _ in range(len(q)): # one level = one minute
r, c = q.popleft()
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 2 # mark seen on push
fresh -= 1
q.append((nr, nc))
minutes += 1
return minutes if fresh == 0 else -1Treat each word as a node, with an edge to every word that differs by one letter. The shortest ladder is the shortest path in an unweighted graph, so BFS finds it.
Neighbours are not stored: for each position try all 26 letters and keep the result if it is in the word set and not yet seen.
Process the queue level by level, with steps counting words in the ladder. The first time endWord is popped, steps is the answer.
Complexity: O(N * L^2 * 26) time, O(N * L) space (N words of length L)
from collections import deque
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
words = set(wordList)
if endWord not in words:
return 0
q, seen = deque([beginWord]), {beginWord}
steps = 1
while q:
for _ in range(len(q)): # one level = one more word in the ladder
word = q.popleft()
if word == endWord:
return steps
for i in range(len(word)):
for ch in "abcdefghijklmnopqrstuvwxyz":
nxt = word[:i] + ch + word[i + 1:]
if nxt in words and nxt not in seen:
seen.add(nxt) # mark seen on push
q.append(nxt)
steps += 1
return 0Trade memory for speed: O(1) average lookup turns many O(n²) searches into O(n).
Use this when the question asks "how many times", the majority element or the top K most frequent items.
Count every value once with Counter. The majority element appears more than n/2 times, so it has the highest count.
The key line is `max(counts, key=counts.get)`, which picks the value with the biggest count.
Complexity: O(n) time, O(n) extra space
class Solution:
def majorityElement(self, nums: List[int]) -> int:
counts = Counter(nums) # frequency map built in one pass
return max(counts, key=counts.get) # the value with the highest countBuild the frequency map first, then group values by how often they occur. `buckets[freq]` holds all values with that count.
A count can never exceed n, so the buckets are an array of size n + 1. Reading it from the highest index down gives the most frequent values first, without sorting.
Complexity: O(n) time, O(n) extra space
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
counts = Counter(nums) # value -> frequency
buckets = [[] for _ in range(len(nums) + 1)] # index = frequency
for value, freq in counts.items():
buckets[freq].append(value)
result = []
for freq in range(len(buckets) - 1, 0, -1): # highest frequency first
for value in buckets[freq]:
result.append(value)
if len(result) == k:
return result
return resultKeep two maps. `freq[val]` counts how many copies of val are in the stack. `stacks[f]` is a stack of the values that reached frequency f, in push order.
On push, bump the count and add the value to the stack for its new frequency. On pop, take from the stack at the highest frequency, so ties go to the most recent push.
The key lines are `self.stacks[f].append(val)` and `self.stacks[self.max_freq].pop()`.
Complexity: O(1) time per operation, O(n) extra space
from collections import defaultdict
class FreqStack:
def __init__(self):
self.freq = Counter() # value -> how many copies are in the stack
self.stacks = defaultdict(list) # frequency f -> values in the order they reached f
self.max_freq = 0
def push(self, val: int) -> None:
self.freq[val] += 1
f = self.freq[val]
self.max_freq = max(self.max_freq, f)
self.stacks[f].append(val) # val now also lives at level f
def pop(self) -> int:
val = self.stacks[self.max_freq].pop() # most frequent, most recent
self.freq[val] -= 1
if not self.stacks[self.max_freq]:
self.max_freq -= 1 # that level is empty, drop one level
return valUse this when the question asks "is there any repeat?" or "what is the first repeat?".
Walk the array and keep a set of values already seen. If the current value is already in the set, a repeat exists, so return True.
The key line is `if x in seen`. A set lookup is O(1), so one pass is enough.
Complexity: O(n) time, O(n) extra space
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
seen = set()
for x in nums:
if x in seen: # seen before: found a duplicate
return True
seen.add(x)
return FalseA set is not enough here because the two equal values must also be close. Use a dict from value to the last index where it appeared.
When x shows up again, compare `i - last_index[x]` with k. Always overwrite the stored index with the latest one, because the latest copy is the closest to any later copy.
Complexity: O(n) time, O(n) extra space
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
last_index = {} # value -> latest index
for i, x in enumerate(nums):
if x in last_index and i - last_index[x] <= k: # repeat that is close enough
return True
last_index[x] = i # keep only the newest position
return FalseNow "equal" becomes "within valueDiff", so a plain set fails. Split the number line into buckets of width valueDiff + 1. Two numbers in the same bucket are always close enough.
A close pair can also sit in neighbouring buckets, so check bucket b - 1 and b + 1 by value. The dict holds only the last indexDiff numbers, so old buckets are deleted as the window slides.
Complexity: O(n) time, O(indexDiff) extra space
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], indexDiff: int, valueDiff: int) -> bool:
width = valueDiff + 1 # two numbers in one bucket differ by at most valueDiff
buckets = {} # bucket id -> the one value in the window
for i, x in enumerate(nums):
b = x // width # floor division keeps negatives in the right bucket
if b in buckets:
return True
if b - 1 in buckets and x - buckets[b - 1] <= valueDiff:
return True
if b + 1 in buckets and buckets[b + 1] - x <= valueDiff:
return True
buckets[b] = x
if i >= indexDiff: # slide the window: forget nums[i - indexDiff]
del buckets[nums[i - indexDiff] // width]
return FalseUse this when you need two items that combine to a target, and the input is unsorted.
For each x, ask whether target - x has already been seen. Store value to index in a dict as you scan.
Insert x only AFTER the check, so an element can never pair with itself. This turns the O(n^2) pair search into one pass.
Complexity: O(n) time, O(n) extra space
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
idx = {} # value -> index
for i, x in enumerate(nums):
if target - x in idx: # complement already seen
return [idx[target - x], i]
idx[x] = i # insert AFTER checking: no self-pairing
return []This is two sum with a difference. For each distinct x, the partner is x + k, so look it up in a Counter instead of a set.
Count each pair once by only looking upward (x + k, never x - k). When k is 0 the partner is x itself, so the value must appear at least twice.
Complexity: O(n) time, O(n) extra space
class Solution:
def findPairs(self, nums: List[int], k: int) -> int:
counts = Counter(nums)
pairs = 0
for x in counts: # each distinct value once
if k == 0:
if counts[x] > 1: # needs a second copy of itself
pairs += 1
elif x + k in counts: # look up the complement x + k
pairs += 1
return pairsFour arrays are too many to search at once, so split them into two halves. Count every a + b from the first two arrays in a Counter.
Then for every c + d from the last two arrays, add the count of -(c + d). Each lookup is the two-sum complement idea, applied to pair sums.
Complexity: O(n^2) time, O(n^2) extra space
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
pair_sums = Counter(a + b for a in nums1 for b in nums2) # every a + b and how often it occurs
count = 0
for c in nums3:
for d in nums4:
count += pair_sums[-(c + d)] # complement lookup: a + b = -(c + d)
return countUse this when you must count sub-arrays whose sum meets a condition and the numbers may be negative.
A sub-array sum equals k when two prefix sums differ by k: P[j] - P[i] = k. So for each new prefix, count how many earlier prefixes equal total - k.
The dict counts every prefix seen so far. Seed it with {0: 1} for the empty prefix so sub-arrays starting at index 0 are counted.
Negatives are fine because only equal prefix values matter, not growth of the sum.
Complexity: O(n) time, O(n) extra space
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
count, total, seen = 0, 0, {0: 1} # {0: 1} = the empty prefix
for x in nums:
total += x
count += seen.get(total - k, 0) # earlier prefixes that end a sub-array summing to k
seen[total] = seen.get(total, 0) + 1
return countTwo prefixes give a sub-array divisible by k when they have the same remainder mod k. So store prefix remainders instead of raw prefix sums.
For each new remainder, add how many earlier prefixes had the same one. Python's % never returns a negative number, so negative values need no special case.
Complexity: O(n) time, O(k) extra space
class Solution:
def subarraysDivByK(self, nums: List[int], k: int) -> int:
count, prefix, seen = 0, 0, {0: 1} # empty prefix has remainder 0
for x in nums:
prefix = (prefix + x) % k # store the prefix mod k, not the sum
count += seen.get(prefix, 0) # same remainder earlier = divisible gap
seen[prefix] = seen.get(prefix, 0) + 1
return countFix a left column and a right column. Adding up each row between them turns the matrix slice into a 1D array, one number per row.
Counting sub-arrays of that array that sum to target is exactly subarray-sum-equals-k, with the same {0: 1} map. Row prefix sums make each row total O(1).
Complexity: O(cols^2 * rows) time, O(rows * cols) extra space
from itertools import accumulate
class Solution:
def numSubmatrixSumTarget(self, matrix: List[List[int]], target: int) -> int:
rows, cols = len(matrix), len(matrix[0])
row_prefix = [[0] + list(accumulate(row)) for row in matrix] # sum of row[l..r] = p[r+1] - p[l]
count = 0
for left in range(cols):
for right in range(left, cols): # fix the two columns
total, seen = 0, {0: 1} # now run subarray-sum-equals-k down the rows
for r in range(rows):
total += row_prefix[r][right + 1] - row_prefix[r][left]
count += seen.get(total - target, 0)
seen[total] = seen.get(total, 0) + 1
return countUse this when you need the longest, shortest or any sub-array with a sum property, so the map stores an index per prefix value.
Map each 0 to -1 and each 1 to +1. A sub-array with equal 0s and 1s is then a sub-array with sum 0, so two equal running balances bound it.
Store only the FIRST index of each balance, because the earliest index gives the longest span. Seed {0: -1} for the empty prefix.
Complexity: O(n) time, O(n) extra space
class Solution:
def findMaxLength(self, nums: List[int]) -> int:
first_index = {0: -1} # balance 0 before the array starts
balance, best = 0, 0
for i, x in enumerate(nums):
balance += 1 if x == 1 else -1 # map 0 to -1
if balance in first_index:
best = max(best, i - first_index[balance]) # equal balance: equal 0s and 1s between
else:
first_index[balance] = i # first index only: gives the longest span
return bestA sub-array sum is a multiple of k when two prefix sums have the same remainder mod k. Store the first index of each remainder, with {0: -1} for the empty prefix.
When a remainder repeats, the gap `i - first_index[remainder]` must be at least 2 to meet the length rule. Do not overwrite the stored index when the gap is too short, so later matches still see the earliest one.
Complexity: O(n) time, O(min(n, k)) extra space
class Solution:
def checkSubarraySum(self, nums: List[int], k: int) -> bool:
first_index = {0: -1} # remainder 0 before the array
remainder = 0
for i, x in enumerate(nums):
remainder = (remainder + x) % k # prefix sum mod k
if remainder in first_index:
if i - first_index[remainder] >= 2: # same remainder, length at least 2
return True
else:
first_index[remainder] = i # keep the earliest index only
return FalseLet need = total % p. We must remove a sub-array whose sum leaves remainder need, and we want the shortest one. If need is 0, remove nothing.
At index i, the start of that sub-array is an earlier prefix with remainder (prefix - need) % p. Store the LAST index of each remainder, because the latest start gives the shortest sub-array.
Removing the whole array is not allowed, so a result equal to n becomes -1.
Complexity: O(n) time, O(min(n, p)) extra space
class Solution:
def minSubarray(self, nums: List[int], p: int) -> int:
need = sum(nums) % p # remainder the removed part must have
if need == 0:
return 0
last_index = {0: -1} # prefix remainder -> latest index
prefix, best = 0, len(nums)
for i, x in enumerate(nums):
prefix = (prefix + x) % p
want = (prefix - need) % p # earlier prefix that leaves a gap of remainder `need`
if want in last_index:
best = min(best, i - last_index[want])
last_index[prefix] = i # latest index: shortest gap later
return best if best < len(nums) else -1Use this when you need runs of consecutive values in an unsorted array, in O(n) without sorting.
Put all numbers in a set. Only start counting at a number with no predecessor, meaning `x - 1` is not in the set.
From that start, walk up with `y + 1 in s` and measure the run. Each number is walked over only by the one run that starts it, so total work is O(n).
Complexity: O(n) time, O(n) extra space
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
s, best = set(nums), 0
for x in s:
if x - 1 not in s: # x starts a run
y = x
while y + 1 in s: # walk the run upward
y += 1
best = max(best, y - x + 1)
return bestThe run steps are squares instead of +1: x, x*x, x^4 and so on. Put the numbers in a set, and only start at a number that is not the square of another number in the set.
From a start, keep squaring while the result is in the set. Values are at most 10^5, so a run is at most a few steps long and total work stays O(n).
A streak needs length 2 or more, otherwise return -1.
Complexity: O(n) time, O(n) extra space
import math
class Solution:
def longestSquareStreak(self, nums: List[int]) -> int:
values = set(nums)
best = 0
for x in values:
root = math.isqrt(x)
if root * root == x and root in values: # x has a predecessor: not a run start
continue
length, y = 1, x
while y * y in values: # walk the run: x, x^2, x^4, ...
y *= y
length += 1
best = max(best, length)
return best if best >= 2 else -1Each group is a run of groupSize consecutive cards. Count the cards, then go through the values in increasing order.
The smallest value left has no usable x - 1, so it must start a run, and it must start `counts[x]` runs. Take that many of each of x ... x + groupSize - 1, and fail if any of them has too few.
Complexity: O(n log n) time, O(n) extra space
class Solution:
def isNStraightHand(self, hand: List[int], groupSize: int) -> bool:
if len(hand) % groupSize:
return False
counts = Counter(hand)
for x in sorted(counts): # smallest value first
if counts[x] == 0:
continue
need = counts[x] # x must start this many runs
for y in range(x, x + groupSize):
if counts[y] < need: # a card in the run is missing
return False
counts[y] -= need
return TrueUse this when you must bucket items that are "the same" under some rule, by building a canonical key for each item.
Two words are anagrams when they have the same letter counts, so the count map is the canonical key of a word.
Build the key for both words and compare. A different length rules the pair out early.
Complexity: O(n) time, O(1) extra space (at most 26 letters)
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
return Counter(s) == Counter(t) # same letter counts = same keyGive every word a canonical key: a tuple of 26 letter counts. Anagrams have the same counts, so they get the same key.
Use a defaultdict(list) keyed by that tuple and append each word to its bucket. The tuple is hashable, so it works as a dict key, and building it costs O(length) instead of the O(length log length) of sorting.
Complexity: O(n * L) time, O(n * L) extra space, where L is the longest word
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
groups = defaultdict(list)
for word in strs:
counts = [0] * 26
for ch in word:
counts[ord(ch) - ord('a')] += 1
groups[tuple(counts)].append(word) # same counts = same key = same group
return list(groups.values())A move swaps two letters at positions with the same parity. So even-position letters can be reordered freely, and so can odd-position letters, but they never mix.
The canonical key is the pair (sorted even letters, sorted odd letters). Strings in the same group share the key, so the answer is the number of distinct keys in a set.
Complexity: O(n * L log L) time, O(n * L) extra space
class Solution:
def numSpecialEquivGroups(self, words: List[str]) -> int:
keys = set()
for w in words:
even = ''.join(sorted(w[0::2])) # even positions can be reordered freely
odd = ''.join(sorted(w[1::2])) # odd positions can be reordered freely
keys.add((even, odd)) # canonical key of the group
return len(keys)Use this when the answer depends on counts, such as "first non-repeating", "sort by frequency" or "isomorphic", so you count first and read the counts in a second pass.
Pass one counts every character with Counter. Pass two walks the string in order and returns the first index whose count is 1.
Order comes from the second pass over the string, not from the dict. The key line is `if counts[ch] == 1`.
Complexity: O(n) time, O(1) extra space (at most 26 letters)
class Solution:
def firstUniqChar(self, s: str) -> int:
counts = Counter(s) # pass 1: count everything
for i, ch in enumerate(s): # pass 2: read the counts in string order
if counts[ch] == 1:
return i
return -1Each character in s must always map to the same character in t, and no two characters may share a target. Keep one dict per direction.
Scan both strings together. If either dict already holds a different partner for the current pair, the strings are not isomorphic.
Complexity: O(n) time, O(1) extra space (bounded by the alphabet)
class Solution:
def isIsomorphic(self, s: str, t: str) -> bool:
s_to_t, t_to_s = {}, {} # both directions must stay consistent
for a, b in zip(s, t):
if s_to_t.get(a, b) != b or t_to_s.get(b, a) != a: # a different partner was recorded
return False
s_to_t[a] = b
t_to_s[b] = a
return TrueFirst count each character. The second step reads those counts: order the (character, count) pairs from the highest count down.
Repeat each character `n` times and join. Counter.most_common() already returns the pairs sorted by count, and there are at most 62 distinct characters, so the sort is cheap.
Complexity: O(n) time, O(n) extra space
class Solution:
def frequencySort(self, s: str) -> str:
counts = Counter(s) # pass 1: count
return ''.join(ch * n for ch, n in counts.most_common()) # pass 2: highest count firstA function that solves a problem by calling itself on a smaller one. Backtracking is recursion that tries, then undoes.
Use this when a problem is defined in terms of a smaller copy of itself and you can name the smallest case that needs no more work.
fib(k) is defined by fib(k-1) and fib(k-2), so each call hands two smaller problems to itself.
The base case `k < 2` stops the calls: fib(0) = 0 and fib(1) = 1.
Plain double recursion is exponential because the same k is solved again and again. The `memo` dictionary caches each answer, so every k is solved once.
Complexity: O(n) time, O(n) extra space
class Solution:
def fib(self, n: int) -> int:
memo = {}
def go(k):
if k < 2: return k # base case: fib(0) = 0, fib(1) = 1
if k in memo: return memo[k] # already solved: reuse it
memo[k] = go(k - 1) + go(k - 2) # two smaller calls, trust them
return memo[k]
return go(n)x^n is (x^(n/2)) squared, times one more x when n is odd. That is one smaller call on half the power, so the depth is only about log n.
The base case is `n == 0`, which returns 1. A negative power is flipped once with `1 / myPow(x, -n)`.
Computing `half` once and reusing it is the key line: calling myPow twice would undo the speed-up.
Complexity: O(log n) time, O(log n) stack space
class Solution:
def myPow(self, x: float, n: int) -> float:
if n < 0:
return 1 / self.myPow(x, -n) # negative power: flip once
if n == 0:
return 1.0 # base case
half = self.myPow(x, n // 2) # one smaller call on half the power
if n % 2:
return half * half * x # odd power needs one extra x
return half * halfAsk one question: does `s[i:]` match `p[j:]`? Each answer comes from a smaller question on shorter suffixes.
The base case is an empty pattern, which only matches an empty string. When the pattern has `x*`, there are two smaller calls: skip the whole `x*`, or use `x` once and stay on the same `x*`.
Both branches revisit the same (i, j) pairs, so `lru_cache` turns the exponential recursion into a table of at most len(s) * len(p) answers.
Complexity: O(m * n) time, O(m * n) space for m = len(s), n = len(p)
from functools import lru_cache
class Solution:
def isMatch(self, s: str, p: str) -> bool:
@lru_cache(None)
def go(i, j): # does s[i:] match p[j:]?
if j == len(p):
return i == len(s) # base case: pattern is used up
first = i < len(s) and p[j] in (s[i], '.')
if j + 1 < len(p) and p[j + 1] == '*':
# skip "x*" entirely, or use x once and stay on the same "x*"
return go(i, j + 2) or (first and go(i + 1, j))
return first and go(i + 1, j + 1) # plain char: both advance
return go(0, 0)Use this when every element gives a yes-or-no choice (take it or leave it) and you need to count, collect or test the outcomes.
Each of the 10 LEDs is one element: pick it (turn it on) or not pick it (leave it off). `left` counts how many LEDs still have to be turned on.
When `left` reaches 0, the picked LEDs form a time, so the leaf condition is "valid time" and it is saved.
A time with hours above 11 or minutes above 59 can only get worse, so the first line of `go` prunes it.
Complexity: O(2^L) time for L = 10 LEDs (a constant), O(L) stack space
class Solution:
def readBinaryWatch(self, turnedOn: int) -> List[str]:
leds = [8, 4, 2, 1, 32, 16, 8, 4, 2, 1] # first 4 are hours, last 6 are minutes
out = []
def go(i, left, hours, minutes):
if hours > 11 or minutes > 59: return # prune: not a real time
if left == 0:
out.append(f"{hours}:{minutes:02d}") # leaf: all needed LEDs are on
return
if i == len(leds): return # ran out of LEDs
if i < 4:
go(i + 1, left - 1, hours + leds[i], minutes) # pick an hour LED
else:
go(i + 1, left - 1, hours, minutes + leds[i]) # pick a minute LED
go(i + 1, left, hours, minutes) # not pick
go(0, turnedOn, 0, 0)
return outGiving every number a sign is the same as picking a group that gets "+". If the numbers sum to `total`, that group must add up to `need = (total + target) / 2`.
So each number is picked (subtract it from `remain`) or not picked, and the leaf condition is `remain == 0`, which counts as one way.
Two different paths often reach the same (i, remain), so the `memo` dictionary stops the 2^n blow-up.
Complexity: O(n * need) time, O(n * need) space
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
if abs(target) > total or (total + target) % 2:
return 0 # no split can reach target
need = (total + target) // 2 # the "+" group must add up to need
memo = {}
def go(i, remain):
if i == len(nums):
return 1 if remain == 0 else 0 # leaf: count it if the sum is hit
if (i, remain) in memo: return memo[(i, remain)]
ways = go(i + 1, remain) # not pick nums[i]
if nums[i] <= remain:
ways += go(i + 1, remain - nums[i]) # pick nums[i]
memo[(i, remain)] = ways
return ways
return go(0, need)Each character is one choice. A bracket can be picked (kept) or not picked (removed). Letters are always kept.
First count how many `(` and `)` must go (`left_rem`, `right_rem`). The "not pick" branch only exists while a removal is still owed, and the "pick" branch drops a `)` that has no open `(`. These two checks prune most of the 2^n tree.
At the leaf, accept the string when no removals are owed and no `(` is left open. A `set` removes duplicate strings.
Complexity: O(2^n) time in the worst case, O(n) stack space
class Solution:
def removeInvalidParentheses(self, s: str) -> List[str]:
left_rem = right_rem = 0 # how many ( and ) must be removed
for ch in s:
if ch == '(':
left_rem += 1
elif ch == ')':
if left_rem: left_rem -= 1
else: right_rem += 1
out, path = set(), []
def go(i, left_rem, right_rem, open_count):
if i == len(s):
if left_rem == 0 and right_rem == 0 and open_count == 0:
out.add(''.join(path)) # leaf: balanced, nothing owed
return
ch = s[i]
if ch == '(' and left_rem > 0: # not pick: remove this (
go(i + 1, left_rem - 1, right_rem, open_count)
if ch == ')' and right_rem > 0: # not pick: remove this )
go(i + 1, left_rem, right_rem - 1, open_count)
if ch == ')' and open_count == 0:
return # prune: a ) with no ( can never be kept
path.append(ch) # pick: keep it
go(i + 1, left_rem, right_rem, open_count + (ch == '(') - (ch == ')'))
path.pop() # undo
go(0, left_rem, right_rem, 0)
return list(out)Use this when you need every possible group of elements and the order inside a group does not matter.
Walk the array once and make the pick / not pick choice for each number. After the last index, the path is one finished subset.
Instead of storing the subset, carry its running XOR down the calls: `xor_so_far ^ nums[i]` when picked, `xor_so_far` when not.
At the leaf return that XOR, and add the answers of the two branches. There are 2^n leaves, one per subset.
Complexity: O(2^n) time, O(n) stack space
class Solution:
def subsetXORSum(self, nums: List[int]) -> int:
def go(i, xor_so_far):
if i == len(nums):
return xor_so_far # one finished subset: its XOR
pick = go(i + 1, xor_so_far ^ nums[i]) # pick nums[i]
not_pick = go(i + 1, xor_so_far) # not pick
return pick + not_pick
return go(0, 0)This is the template itself. At index i, append `nums[i]` and recurse (pick), pop it, and recurse again (not pick).
When `i == len(nums)` the path holds one subset, so save a copy `path[:]`. Saving `path` itself would leave every result empty.
Every element has two choices, so there are 2^n subsets.
Complexity: O(n * 2^n) time, O(n) extra space besides the output
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
out = []
def go(i, path):
if i == len(nums):
out.append(path[:]) # save a copy, not the list itself
return
path.append(nums[i]); go(i + 1, path) # pick
path.pop(); go(i + 1, path) # not pick
go(0, [])
return outSort first so equal numbers sit next to each other. Pick / not pick would now build the same subset twice, for example {2} from either 2.
Fix it in the "not pick" branch: if you leave out `nums[i]`, move `j` past every copy of that value. This is the same as skipping `a[i] == a[i-1]` at the same level.
Picking still goes to `i + 1`, so each value is used 0, 1, 2, ... times and every subset appears once.
Complexity: O(n * 2^n) time, O(n) extra space besides the output
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
nums.sort() # equal values become neighbours
out = []
def go(i, path):
if i == len(nums):
out.append(path[:])
return
path.append(nums[i]); go(i + 1, path) # pick
path.pop()
j = i + 1
while j < len(nums) and nums[j] == nums[i]:
j += 1 # not pick: skip every copy of this value
go(j, path)
go(0, [])
return outUse this when you need pieces of a sequence that keep their order but may skip elements, and a condition at the end decides which ones count.
Build the subsequence of `t` that equals `s`. At each step the next char of `t` is either skipped (not pick) or used to match `s[i]` (pick).
`t.find(s[i], j)` skips every unusable char of `t` in one move, so the recursion only goes one level deeper per matched char.
The base case is `i == len(s)`: every char of `s` was matched. If `find` returns -1, `t` ran out first.
Complexity: O(len(t)) time, O(len(s)) stack space
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
def go(i, j):
if i == len(s):
return True # base case: all of s matched
j = t.find(s[i], j) # not pick: skip chars that are not s[i]
if j == -1:
return False # t ran out first
return go(i + 1, j + 1) # pick it, then match the rest
return go(0, 0)Every subsequence is a pick / not pick walk, and the leaf rule is "length at least 2". Picking `nums[i]` is only allowed when it is not smaller than the last picked value.
Duplicates are the twist. Skip `nums[i]` only when it differs from the last picked value. If it is equal, picking it already covers that subsequence, so skipping would repeat it.
No `set` of results is needed: the two rules together make each subsequence appear exactly once.
Complexity: O(n * 2^n) time, O(n) extra space besides the output
class Solution:
def findSubsequences(self, nums: List[int]) -> List[List[int]]:
out, path = [], []
def go(i):
if i == len(nums):
if len(path) >= 2:
out.append(path[:]) # leaf condition: at least 2 elements
return
if not path or nums[i] >= path[-1]: # pick only if the order stays non-decreasing
path.append(nums[i]); go(i + 1)
path.pop()
if not path or nums[i] != path[-1]: # not pick, unless picking an equal value covers it
go(i + 1)
go(0)
return outCount the ways to build `t[j:]` from `s[i:]`. For each char of `s` there is a not pick branch (`go(i + 1, j)`), and a pick branch only when `s[i] == t[j]`.
The base cases: `j == len(t)` means t is fully built, which is one way; `i == len(s)` first means zero ways.
Many (i, j) pairs repeat, so `lru_cache` keeps it polynomial. The depth can pass 1000, so the recursion limit is raised.
Complexity: O(m * n) time, O(m * n) space for m = len(s), n = len(t)
import sys
from functools import lru_cache
class Solution:
def numDistinct(self, s: str, t: str) -> int:
sys.setrecursionlimit(10000) # depth can reach len(s) + len(t)
@lru_cache(None)
def go(i, j): # ways to build t[j:] from s[i:]
if j == len(t): return 1 # t fully built: one way
if i == len(s): return 0 # s used up first: no way
ways = go(i + 1, j) # not pick s[i]
if s[i] == t[j]:
ways += go(i + 1, j + 1) # pick s[i] to match t[j]
return ways
return go(0, 0)Use this when order matters and every element is used exactly once, so each position can take any unused element.
Fill the positions one by one. At each position loop over all indices and skip the ones with `used[i]` set.
Choose: mark the element used and append it. Explore: recurse. Un-choose: pop it and clear the mark.
When the path has the same length as `nums`, save a copy `cur[:]`. There are n! results.
Complexity: O(n * n!) time, O(n) extra space besides the output
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
out, used, cur = [], [False] * len(nums), []
def go():
if len(cur) == len(nums):
out.append(cur[:]) # copy, not cur itself
return
for i in range(len(nums)):
if used[i]: continue # each element once per permutation
used[i] = True; cur.append(nums[i]) # choose
go() # explore
cur.pop(); used[i] = False # un-choose
go()
return outSame loop as plain permutations, but equal numbers would build the same permutation more than once.
Sort first. Then skip `nums[i]` when it equals `nums[i-1]` and `nums[i-1]` is not used: that copy has to wait for its twin, so equal values are always placed left to right.
Everything else (mark, recurse, unmark, copy the path) is unchanged.
Complexity: O(n * n!) time, O(n) extra space besides the output
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
nums.sort() # equal values become neighbours
out, used, cur = [], [False] * len(nums), []
def go():
if len(cur) == len(nums):
out.append(cur[:])
return
for i in range(len(nums)):
if used[i]: continue
if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
continue # skip a duplicate whose twin is unused
used[i] = True; cur.append(nums[i])
go()
cur.pop(); used[i] = False # undo
go()
return outThis is permutations-ii with one more rule. Place the elements one by one, and skip a duplicate whose twin is unused.
Prune early: a candidate is only allowed when `prev + nums[i]` is a perfect square, so dead branches stop after one step.
Nothing needs to be stored. The leaf (`placed == n`) returns 1, and each call adds up the counts of its children.
Complexity: O(n * n!) time worst case, much less with pruning; O(n) stack space
import math
class Solution:
def numSquarefulPerms(self, nums: List[int]) -> int:
nums.sort() # equal values become neighbours
n = len(nums)
used = [False] * n
def is_square(x):
r = math.isqrt(x)
return r * r == x
def go(prev, placed):
if placed == n:
return 1 # leaf: one valid arrangement
count = 0
for i in range(n):
if used[i]: continue
if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
continue # skip duplicate
if placed and not is_square(prev + nums[i]):
continue # prune: neighbour sum must be a square
used[i] = True # choose
count += go(nums[i], placed + 1) # explore
used[i] = False # un-choose
return count
return go(0, 0)Use this when order does not matter, so you only look forward from a start index and never build both [1,2] and [2,1].
Choose k numbers from 1..n. Each call loops `i` from `start` to n, appends i, and recurses with `i + 1`, so only larger numbers can follow.
That forward-only start index is what stops [1,2] and [2,1] both appearing.
When the path has k numbers, save a copy. The `break` prunes loops where too few numbers are left to reach k.
Complexity: O(k * C(n, k)) time, O(k) extra space besides the output
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
out = []
def go(start, path):
if len(path) == k:
out.append(path[:]) # k chosen: save a copy
return
for i in range(start, n + 1):
if n - i + 1 < k - len(path):
break # prune: not enough numbers left
path.append(i) # choose
go(i + 1, path) # explore, only look forward
path.pop() # un-choose
go(1, [])
return outThis is the template from the notes. The loop starts at `start`, so only the current or later candidates are used and no order repeats.
The same number may be used again, so the recursive call passes `i`, not `i + 1`.
Save the path when `remain == 0`. Skip any candidate bigger than `remain`: that is the early prune.
Complexity: O(n^(target / min)) time in the worst case, O(target / min) stack space
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
out = []
def go(start, path, remain):
if remain == 0:
out.append(path[:]) # found a combination
return
for i in range(start, len(candidates)):
if candidates[i] > remain: continue # prune
path.append(candidates[i]) # choose
go(i, path, remain - candidates[i]) # i, not i + 1: reuse allowed
path.pop() # un-choose
go(0, [], target)
return outEach candidate may be used once, so the recursive call moves on with `i + 1`. Sort first so duplicates sit together.
Inside one loop, skip `candidates[i]` when `i > start` and it equals the previous one. Taking the same value twice at the same level would repeat a combination.
Because the list is sorted, `break` replaces `continue`: once one candidate is too big, so are all later ones.
Complexity: O(2^n) time, O(n) stack space
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
candidates.sort() # duplicates become neighbours
out = []
def go(start, path, remain):
if remain == 0:
out.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remain: break # sorted, so the rest are too big
if i > start and candidates[i] == candidates[i - 1]:
continue # same value at the same level
path.append(candidates[i])
go(i + 1, path, remain - candidates[i]) # i + 1: each number once
path.pop()
go(0, [], target)
return outUse this when you build an answer step by step and can undo a step (and cut the branch) as soon as the partial answer cannot work.
Build the string one character at a time with the three steps: choose (append), explore (recurse), un-choose (pop).
Two conditions prune the tree. Add `(` only while fewer than n are used, and add `)` only while it would not close more than has been opened.
Because bad branches are never entered, every string that reaches length 2n is already valid.
Complexity: O(4^n / sqrt(n)) time (Catalan number of results), O(n) stack space
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
out, path = [], []
def go(open_used, close_used):
if len(path) == 2 * n:
out.append(''.join(path)) # complete and valid by construction
return
if open_used < n: # prune: at most n opening brackets
path.append('(') # choose
go(open_used + 1, close_used) # explore
path.pop() # un-choose
if close_used < open_used: # prune: never close more than opened
path.append(')')
go(open_used, close_used + 1)
path.pop()
go(0, 0)
return outCut the string from left to right. At each call, loop over every end position and take the piece `s[start:end]`.
Prune: only recurse when the piece is a palindrome. A bad first piece can never lead to a valid partition.
Choose (append the piece), explore (continue from `end`), un-choose (pop). When `start` reaches the end, save a copy of the path.
Complexity: O(n * 2^n) time, O(n) stack space
class Solution:
def partition(self, s: str) -> List[List[str]]:
out, path = [], []
def go(start):
if start == len(s):
out.append(path[:]) # whole string is cut into palindromes
return
for end in range(start + 1, len(s) + 1):
piece = s[start:end]
if piece != piece[::-1]: continue # prune: piece is not a palindrome
path.append(piece) # choose
go(end) # explore
path.pop() # un-choose
go(0)
return outPlace one queen per row. For each column, check whether it is already attacked: the column set, the `row - c` diagonal set, or the `row + c` diagonal set.
Choose (add the column and both diagonals), explore the next row, then un-choose by removing them all again.
Attacked squares are skipped at once, so most of the n^n placements are never tried. A full board is built only at the leaf.
Complexity: O(n!) time, O(n) extra space besides the output
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
out, queens = [], [] # queens[r] = column of the queen in row r
cols, diag1, diag2 = set(), set(), set()
def go(row):
if row == n:
out.append(['.' * c + 'Q' + '.' * (n - c - 1) for c in queens])
return
for c in range(n):
if c in cols or row - c in diag1 or row + c in diag2:
continue # prune: this square is attacked
cols.add(c); diag1.add(row - c); diag2.add(row + c) # choose
queens.append(c)
go(row + 1) # explore
queens.pop() # un-choose
cols.remove(c); diag1.remove(row - c); diag2.remove(row + c)
go(0)
return outUse this when the answer for a node comes from the answers of its left and right subtrees, or from information passed down to them.
Bottom-up: the base case is `None`, which has depth 0. Otherwise ask the left child and the right child, then combine.
The depth of a node is 1 plus the larger depth of its two subtrees. This is the `height` example from the notes.
Complexity: O(n) time, O(h) stack space for tree height h
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0 # base case: empty tree
left = self.maxDepth(root.left) # ask the left subtree
right = self.maxDepth(root.right) # ask the right subtree
return 1 + max(left, right) # combineTop-down: pass information down through a parameter. Each call subtracts the node's value and hands the smaller `remain` to its children.
The base cases are `None` (no path, False) and a leaf (the path is complete, so check `remain == 0`).
Only a leaf can end a path, so `1 -> 2` with target 1 is False even though the root alone equals 1. Either child may succeed, so combine with `or`.
Complexity: O(n) time, O(h) stack space for tree height h
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False # base case: no node, no path
remain = targetSum - root.val # pass the smaller target down
if not root.left and not root.right:
return remain == 0 # a leaf ends the path
return self.hasPathSum(root.left, remain) or self.hasPathSum(root.right, remain)Bottom-up with a twist. `gain(node)` returns the best sum of a path that starts at that node and goes down one side only, because a parent can only continue along one branch.
At each node a path may bend: `node.val + left + right` is a candidate for the overall answer, stored in `best`. A negative child gain is replaced by 0, which means "do not take that branch".
The value returned upward is `node.val + max(left, right)`, while the bent path is only recorded in `best`.
Complexity: O(n) time, O(h) stack space for tree height h
from math import inf
class Solution:
def maxPathSum(self, root: Optional[TreeNode]) -> int:
best = -inf
def gain(node):
nonlocal best
if not node:
return 0 # base case: empty tree adds nothing
left = max(gain(node.left), 0) # drop a branch that only hurts
right = max(gain(node.right), 0)
best = max(best, node.val + left + right) # path that bends at this node
return node.val + max(left, right) # going up, take one side only
gain(root)
return bestEach node has at most two children. Nearly every problem is a traversal with a small twist.
Use this when the answer for a node depends on its two subtrees (or on what you met on the way down), so you recurse left and right and combine the results.
Inverting a tree means every node swaps its two children. DFS handles that one node at a time: recurse into both children first, then swap the two results. The line `root.left, root.right = right, left` is the whole trick. Every node is touched once.
Complexity: O(n) time, O(h) extra space (recursion stack, h = tree height)
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root: # base case: empty subtree
return None
left = self.invertTree(root.left) # DFS into both children
right = self.invertTree(root.right)
root.left, root.right = right, left # swap the two inverted subtrees
return rootA tree is symmetric when its left and right subtrees are mirror images. Run DFS on two nodes at the same time: `a` walks the left side and `b` walks the right side. The outer children must match each other, and the inner children must match each other. Any mismatch in value or shape returns False immediately.
Complexity: O(n) time, O(h) extra space
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
def mirror(a, b): # DFS on a pair of nodes
if not a and not b:
return True
if not a or not b:
return False
return (a.val == b.val and
mirror(a.left, b.right) and # outer pair
mirror(a.right, b.left)) # inner pair
return not root or mirror(root.left, root.right)Some answers depend on the path from the root, not only on the subtree. Pass the largest value seen so far down as a parameter: `best`. A node is good when its value is at least `best`. Update `best` before recursing into the children, so each branch carries its own maximum.
Complexity: O(n) time, O(h) extra space
from math import inf
class Solution:
def goodNodes(self, root: TreeNode) -> int:
def dfs(node, best): # best = max value on the path so far
if not node:
return 0
good = 1 if node.val >= best else 0
best = max(best, node.val) # carry the new maximum down
return good + dfs(node.left, best) + dfs(node.right, best)
return dfs(root, -inf)Use this when the question talks about levels, rows or the nearest-to-the-root node, so you process the tree one level at a time with a queue.
Put the root in a queue and read `len(q)` before the inner loop. That freezes the size of the current level. Add up exactly that many nodes, pushing their children for the next round. Divide the level total by `size` and append it.
Complexity: O(n) time, O(w) extra space (w = widest level)
from collections import deque
class Solution:
def averageOfLevels(self, root: Optional[TreeNode]) -> List[float]:
if not root:
return []
out, q = [], deque([root])
while q:
size = len(q) # freeze: exactly this level
total = 0
for _ in range(size):
n = q.popleft()
total += n.val
if n.left: q.append(n.left)
if n.right: q.append(n.right)
out.append(total / size)
return outThis is the template itself. Take `len(q)`, pop that many nodes into a `level` list, and push their children. After the inner loop, the list holds one full level, so append it to the answer. An empty tree returns an empty list.
Complexity: O(n) time, O(w) extra space (w = widest level)
from collections import deque
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root:
return []
out, q = [], deque([root])
while q:
level = []
for _ in range(len(q)): # exactly this level
n = q.popleft()
level.append(n.val)
if n.left: q.append(n.left)
if n.right: q.append(n.right)
out.append(level)
return outThe traversal is the same level-by-level BFS. The one extra idea is a flag `left_to_right` that flips after every level. When the flag is False, store the level reversed: `level[::-1]`. The queue itself always runs left to right, so the children are pushed the normal way.
Complexity: O(n) time, O(w) extra space
from collections import deque
class Solution:
def zigzagLevelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root:
return []
out, q, left_to_right = [], deque([root]), True
while q:
level = []
for _ in range(len(q)): # exactly this level
n = q.popleft()
level.append(n.val)
if n.left: q.append(n.left)
if n.right: q.append(n.right)
out.append(level if left_to_right else level[::-1])
left_to_right = not left_to_right # flip direction for the next level
return outUse this when you must handle a node before its children, for example when copying a tree, rebuilding one from a traversal, or writing it out.
Preorder means node, left, right. In the DFS template, put the `out.append` before the two recursive calls. Nothing else changes between the three orders.
Complexity: O(n) time, O(h) extra space
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
out = []
def dfs(n):
if not n:
return
out.append(n.val) # PRE: visit the node first
dfs(n.left)
dfs(n.right)
dfs(root)
return outIn a preorder list the next unused value is always the root of the subtree being built. Find that value in the inorder list (a hash map gives its index) to learn where the left side ends. Build the left subtree first, then the right, because preorder lists the left subtree before the right one. The iterator `pre` hands out roots in exactly that order.
Complexity: O(n) time, O(n) extra space
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
index_of = {v: i for i, v in enumerate(inorder)}
pre = iter(preorder) # preorder yields roots one by one
def build(lo, hi): # builds from inorder[lo..hi]
if lo > hi:
return None
root = TreeNode(next(pre)) # PRE: next value is this subtree's root
mid = index_of[root.val]
root.left = build(lo, mid - 1) # left first, matching preorder
root.right = build(mid + 1, hi)
return root
return build(0, len(inorder) - 1)Serialize in preorder: write the node, then its left subtree, then its right subtree. Write `#` for every missing child, so the reader knows exactly where each subtree ends. Deserialize reads the tokens in the same preorder using an iterator, so each call to `build` consumes precisely the tokens of one subtree.
Complexity: O(n) time, O(n) extra space
class Codec:
def serialize(self, root: Optional[TreeNode]) -> str:
out = []
def dfs(n):
if not n:
out.append('#') # marker for an empty child
return
out.append(str(n.val)) # PRE: write the node before its children
dfs(n.left)
dfs(n.right)
dfs(root)
return ','.join(out)
def deserialize(self, data: str) -> Optional[TreeNode]:
tokens = iter(data.split(','))
def build():
t = next(tokens) # read in the same preorder
if t == '#':
return None
node = TreeNode(int(t))
node.left = build()
node.right = build()
return node
return build()Use this when the tree is a BST and you need its values in sorted order, the k-th value, or the place where the order breaks.
Inorder means left, node, right. In the DFS template, put the `out.append` between the two recursive calls. On a BST this visits values in sorted order.
Complexity: O(n) time, O(h) extra space
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
out = []
def dfs(n):
if not n:
return
dfs(n.left)
out.append(n.val) # IN: visit between the children
dfs(n.right)
dfs(root)
return outInorder on a BST gives sorted values, so the k-th smallest is the k-th node visited. Use the same left, node, right order with an explicit stack, so you can stop as soon as the count reaches zero. The line `k -= 1` sits at the visit step. You never walk the part of the tree after the answer.
Complexity: O(h + k) time, O(h) extra space
class Solution:
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
stack, node = [], root
while stack or node:
while node: # go left as far as possible
stack.append(node)
node = node.left
node = stack.pop() # IN: visit after the left side is done
k -= 1
if k == 0:
return node.val # stop early at the k-th visit
node = node.rightA valid BST read inorder is sorted. Two swapped values create one or two places where a value is smaller than the one before it. Walk inorder and keep `prev`. The first time `prev.val > node.val`, remember `prev` as `first`. On every such break, set `second = node`, so the last break wins. Swap the two values at the end.
Complexity: O(n) time, O(h) extra space
class Solution:
def recoverTree(self, root: Optional[TreeNode]) -> None:
first = second = prev = None
stack, node = [], root
while stack or node:
while node:
stack.append(node)
node = node.left
node = stack.pop() # IN: nodes arrive in inorder
if prev and prev.val > node.val: # sorted order broke here
if not first:
first = prev # the larger value, too early
second = node # the smaller value, too late
prev = node
node = node.right
first.val, second.val = second.val, first.valUse this when a node can only be decided after both children are done, such as deleting, pruning, or combining answers from below.
Postorder means left, right, node. In the DFS template, put the `out.append` after both recursive calls. Every node is recorded only after its whole subtree.
Complexity: O(n) time, O(h) extra space
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
out = []
def dfs(n):
if not n:
return
dfs(n.left)
dfs(n.right)
out.append(n.val) # POST: visit after both children
dfs(root)
return outA subtree should be removed when it contains no 1. You cannot know that for a node until its children have been pruned. So recurse into both children first and reassign them. Then, after the recursive calls (the postorder spot), a node that has no children left and a value of 0 returns None.
Complexity: O(n) time, O(h) extra space
class Solution:
def pruneTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return None
root.left = self.pruneTree(root.left) # children first
root.right = self.pruneTree(root.right)
if not root.left and not root.right and root.val == 0:
return None # POST: now a useless leaf, cut it off
return rootGreedy works from the bottom, so it needs postorder. Each node reports a state to its parent: 0 = not covered, 1 = has a camera, 2 = covered without one. If any child is not covered, this node must hold a camera. If a child has a camera, this node is covered. Otherwise this node is not covered yet and the parent will deal with it. A root that comes back as 0 needs one more camera.
Complexity: O(n) time, O(h) extra space
class Solution:
def minCameraCover(self, root: Optional[TreeNode]) -> int:
NOT_COVERED, HAS_CAMERA, COVERED = 0, 1, 2
cameras = 0
def dfs(n):
nonlocal cameras
if not n:
return COVERED # an empty child needs nothing
l, r = dfs(n.left), dfs(n.right) # POST: children report first
if l == NOT_COVERED or r == NOT_COVERED:
cameras += 1 # a child is exposed: camera here
return HAS_CAMERA
if l == HAS_CAMERA or r == HAS_CAMERA:
return COVERED
return NOT_COVERED # leave it to the parent
if dfs(root) == NOT_COVERED:
cameras += 1
return camerasUse this when the answer is a number of levels, or when a decision depends on how deep a subtree goes.
The height of a node is one more than the taller of its two children, and an empty tree has height 0. That sentence is the code. Each call returns the height of its subtree to its parent, so the root ends up with the answer.
Complexity: O(n) time, O(h) extra space
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0 # empty tree has height 0
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))The tempting `1 + min(left, right)` is wrong when a node has only one child, because the empty side would count as depth 0. A path must end at a leaf. So when one child is missing, follow the other one. The line `return 1 + l + r` works because the missing side contributes 0.
Complexity: O(n) time, O(h) extra space
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
l, r = self.minDepth(root.left), self.minDepth(root.right)
if not root.left or not root.right: # only one child: must follow it
return 1 + l + r # one of l, r is 0
return 1 + min(l, r)In a complete tree, at least one side of every node is a perfect tree. Measure the height down the leftmost edge and down the rightmost edge. If they are equal, the whole tree is perfect and has `2 ** h - 1` nodes, with no further recursion. Otherwise recurse into both children. One of them is perfect and returns at once, so only one path goes deep.
Complexity: O(log^2 n) time, O(log n) extra space
class Solution:
def countNodes(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
left_h, node = 0, root
while node: # height along the leftmost edge
left_h += 1
node = node.left
right_h, node = 0, root
while node: # height along the rightmost edge
right_h += 1
node = node.right
if left_h == right_h: # perfect tree: count by formula
return 2 ** left_h - 1
return 1 + self.countNodes(root.left) + self.countNodes(root.right)Use this when the best answer is a path that can bend at any node (left arm plus right arm), so each node returns one arm upward and updates a global best.
This is the template. The recursive function returns a height, but at every node it also updates `best` with `l + r`, the longest path that bends at that node. Returning only the taller arm upward keeps the heights correct. One postorder pass gives O(n) instead of recomputing heights at every node.
Complexity: O(n) time, O(h) extra space
class Solution:
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
best = 0
def h(n):
nonlocal best
if not n:
return 0
l, r = h(n.left), h(n.right)
best = max(best, l + r) # path THROUGH this node
return 1 + max(l, r) # height to return up
h(root)
return bestSame shape as the diameter, with one extra rule: an arm only extends when the child has the same value as the node. Each call returns the longest same-value arm going down from `n`. If a child's value differs, that arm is reset to 0. The path bending at `n` has length `l + r`, which updates `best`.
Complexity: O(n) time, O(h) extra space
class Solution:
def longestUnivaluePath(self, root: Optional[TreeNode]) -> int:
best = 0
def arm(n): # longest same-value arm down from n
nonlocal best
if not n:
return 0
l, r = arm(n.left), arm(n.right)
l = l + 1 if n.left and n.left.val == n.val else 0
r = r + 1 if n.right and n.right.val == n.val else 0
best = max(best, l + r) # path through this node
return max(l, r) # one arm goes up
arm(root)
return bestThis is the diameter with sums instead of edge counts, and with negative values to handle. Each call returns the best one-armed sum going down from the node, and `max(..., 0)` drops an arm that would lower the total. The path that bends at the node is `n.val + l + r`, which updates `best`. Only `n.val + max(l, r)` can go up to the parent.
Complexity: O(n) time, O(h) extra space
from math import inf
class Solution:
def maxPathSum(self, root: Optional[TreeNode]) -> int:
best = -inf
def gain(n):
nonlocal best
if not n:
return 0
l = max(gain(n.left), 0) # ignore an arm that hurts
r = max(gain(n.right), 0)
best = max(best, n.val + l + r) # path through this node
return n.val + max(l, r) # one arm goes up
gain(root)
return bestUse this when you must check or build a tree whose two subtrees never differ in height by more than one.
Checking balance node by node would recompute heights again and again. Instead, compute the height in one postorder pass and return -1 as a sentinel for "unbalanced somewhere below". A node is unbalanced if a child already returned -1 or the two heights differ by more than 1. The sentinel passes up to the root.
Complexity: O(n) time, O(h) extra space
class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def h(n): # -1 means "unbalanced"
if not n:
return 0
l, r = h(n.left), h(n.right)
if l < 0 or r < 0 or abs(l - r) > 1:
return -1 # pass the sentinel upward
return 1 + max(l, r)
return h(root) >= 0To build a tree whose two sides stay equal in size, make the middle element the root. The left half becomes the left subtree and the right half becomes the right subtree, recursively. Sizes of the two sides differ by at most one at every node, so the heights do too. Because the array is sorted, the result is also a valid BST.
Complexity: O(n) time, O(log n) extra space (recursion stack)
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
def build(lo, hi): # builds from nums[lo:hi]
if lo >= hi:
return None
mid = (lo + hi) // 2 # middle as root keeps both sides balanced
root = TreeNode(nums[mid])
root.left = build(lo, mid)
root.right = build(mid + 1, hi)
return root
return build(0, len(nums))Two steps. First, an inorder walk of the BST collects its values in sorted order. Second, rebuild a tree from that sorted list by always taking the middle as the root, which keeps both sides equal in size. The result holds the same values and is height-balanced.
Complexity: O(n) time, O(n) extra space
class Solution:
def balanceBST(self, root: TreeNode) -> TreeNode:
values = []
def inorder(n): # BST inorder = sorted values
if not n:
return
inorder(n.left)
values.append(n.val)
inorder(n.right)
inorder(root)
def build(lo, hi): # builds from values[lo:hi]
if lo >= hi:
return None
mid = (lo + hi) // 2 # middle as root keeps it balanced
node = TreeNode(values[mid])
node.left = build(lo, mid)
node.right = build(mid + 1, hi)
return node
return build(0, len(values))Use this when the question is about root-to-leaf (or downward) paths and a running total, so you pass the remaining target down as you go.
Carry the remaining target as a parameter: at each node subtract `n.val` before going to the children. At a leaf, the path works when the leaf value equals what is left. A node with only one child is not a leaf, so an empty side must return False, not True.
Complexity: O(n) time, O(h) extra space
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
if not root.left and not root.right: # leaf: path ends here
return root.val == targetSum
remaining = targetSum - root.val # subtract as you go down
return (self.hasPathSum(root.left, remaining) or
self.hasPathSum(root.right, remaining))Now every valid path must be listed, so use backtracking on top of the top-down walk. Keep one shared `path` list: append the node on the way down and `pop()` it on the way back. At a leaf with nothing left to subtract, copy the path with `path[:]`. Without the copy, later changes would alter the saved answer.
Complexity: O(n^2) time in the worst case (copying paths), O(h) extra space besides the output
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
out, path = [], []
def dfs(n, remaining):
if not n:
return
path.append(n.val) # choose this node
remaining -= n.val # subtract as you go down
if not n.left and not n.right and remaining == 0:
out.append(path[:]) # copy, the list keeps changing
dfs(n.left, remaining)
dfs(n.right, remaining)
path.pop() # undo the choice
dfs(root, targetSum)
return outPaths may start and end anywhere, as long as they go downward. Keep the running sum from the root, `prefix`. A path ending at this node with sum `targetSum` exists once for each earlier prefix equal to `prefix - targetSum`, so look that up in the counter. Add the current prefix before recursing and remove it on the way back, so only ancestors are counted.
Complexity: O(n) time, O(h) extra space
from collections import Counter
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
seen = Counter({0: 1}) # prefix sums on the current root path
def dfs(n, prefix):
if not n:
return 0
prefix += n.val
count = seen[prefix - targetSum] # paths ending at this node
seen[prefix] += 1
count += dfs(n.left, prefix) + dfs(n.right, prefix)
seen[prefix] -= 1 # leaving this node
return count
return dfs(root, 0)Use this when the answer hangs on the lowest node where two things (given nodes, or pairs of leaves) sit on different sides, so you search both sides and see where they split.
Search both subtrees for `p` and `q`. A call returns the node itself if it is `p` or `q`, otherwise whatever the sides found. If both sides return something, `p` and `q` are on different sides, so this node is the split and the answer. If only one side found something, pass it up unchanged.
Complexity: O(n) time, O(h) extra space
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root or root is p or root is q: # found a target (or fell off)
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right: # p and q are on different sides: split here
return root
return left or right # otherwise pass up what was foundThe shortest route between two nodes always climbs to their lowest common ancestor and then walks down. Find that ancestor with the same idiom: return the node if it matches, ask both children, and the node that gets answers from both sides is the split. Then find the downward path from it to each value. The path to `startValue` becomes one `U` per step, and the path to `destValue` is kept as `L` and `R`.
Complexity: O(n) time, O(h) extra space
class Solution:
def getDirections(self, root: Optional[TreeNode], startValue: int, destValue: int) -> str:
def lca(node):
if not node or node.val == startValue or node.val == destValue:
return node # found a target (or fell off)
left, right = lca(node.left), lca(node.right)
if left and right: # the two values are on different sides: split here
return node
return left or right # otherwise pass up what was found
def find_path(node, target, moves): # fills moves with L/R steps from node down to target
if not node:
return False
if node.val == target:
return True
for child, step in ((node.left, 'L'), (node.right, 'R')):
moves.append(step)
if find_path(child, target, moves):
return True
moves.pop() # undo the step when the target is not below
return False
top = lca(root)
to_start, to_dest = [], []
find_path(top, startValue, to_start)
find_path(top, destValue, to_dest)
return 'U' * len(to_start) + ''.join(to_dest) # climb to the split, then walk downEvery pair of leaves has exactly one lowest common ancestor, the node where the two leaves are on different sides. So each call returns an array `counts`, where `counts[d]` is the number of leaves exactly `d` steps below that node. At a node, every leaf on the left pairs with every leaf on the right, and the path between them is one step to each side plus their depths. Add `left[i] * right[j]` to the answer when that length is at most `distance`, then pass the merged counts up shifted by one.
Complexity: O(n * distance^2) time, O(h * distance) extra space
class Solution:
def countPairs(self, root: TreeNode, distance: int) -> int:
pairs = 0
def dfs(node): # counts[d] = leaves exactly d steps below node
nonlocal pairs
counts = [0] * (distance + 1)
if not node:
return counts
if not node.left and not node.right:
counts[0] = 1
return counts
left, right = dfs(node.left), dfs(node.right)
for i in range(distance + 1):
for j in range(distance + 1):
if i + j + 2 <= distance: # path = (i + 1) + (j + 1) steps through this node
pairs += left[i] * right[j] # this node is the split for these pairs
for d in range(distance): # deeper leaves can never be part of a good pair
counts[d + 1] = left[d] + right[d]
return counts
dfs(root)
return pairsUse this when the question is about the outer edge of the tree (its leaves, its left side or its right side), as in a boundary walk.
A boundary walk has three parts: the left edge, the leaves, and the right edge. This question is the leaves part. Visit left before right, and record a node only when it has no children. That gives the bottom edge in left-to-right order, so two trees match when their leaf lists are equal.
Complexity: O(n) time, O(n) extra space
class Solution:
def leafSimilar(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> bool:
def leaves(n, out):
if not n:
return out
if not n.left and not n.right: # a leaf belongs to the bottom edge
out.append(n.val)
leaves(n.left, out) # left before right keeps left-to-right order
leaves(n.right, out)
return out
return leaves(root1, []) == leaves(root2, [])This is the left-edge part of a boundary walk: a leaf that hangs off the left side of its parent. Pass a flag `is_left` down with every call. A node with no children counts only when its flag is True. The root has no parent, so it starts with False.
Complexity: O(n) time, O(h) extra space
class Solution:
def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
def dfs(n, is_left): # is_left: n hangs off its parent's left
if not n:
return 0
if not n.left and not n.right: # a leaf: count it only on the left edge
return n.val if is_left else 0
return dfs(n.left, True) + dfs(n.right, False)
return dfs(root, False)This is the right-edge part of a boundary walk: the nodes you would see from the right. Walk down depth by depth, trying the right child before the left child. The first node reached at a new depth is the rightmost one there, so record it when `depth == len(edge)`. If the right side runs out, the walk falls back to the left child.
Complexity: O(n) time, O(h) extra space
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
edge = []
def walk(n, depth):
if not n:
return
if depth == len(edge): # first node seen at this depth = rightmost
edge.append(n.val)
walk(n.right, depth + 1) # right first, so the edge wins
walk(n.left, depth + 1)
walk(root, 0)
return edgeUse this when you must group or report nodes by column, so you tag each node with a column number while walking the tree.
This introduces the key move of the pattern: each queue entry carries a tag next to the node. Here the tag is the parent. Process the tree level by level and record the parent of `x` and of `y` if they appear on this level. Cousins are on the same level with different parents. If only one of them shows up on a level, their depths differ and the answer is False.
Complexity: O(n) time, O(w) extra space (w = widest level)
from collections import deque
class Solution:
def isCousins(self, root: Optional[TreeNode], x: int, y: int) -> bool:
q = deque([(root, None)]) # each node carries a tag: its parent
while q:
found = {} # value -> parent, for this level only
for _ in range(len(q)):
node, parent = q.popleft()
if node.val == x or node.val == y:
found[node.val] = parent
if node.left: q.append((node.left, node))
if node.right: q.append((node.right, node))
if len(found) == 2: # both on this level
return found[x] is not found[y]
if found: # only one here: different depths
return False
return FalseEvery node has a fixed cell in the grid, so tag each queue entry with `(node, row, col)`. The root sits in the middle column of row 0. A child is one row lower and moves left or right by an offset that halves each row. Walk the tree breadth first and write each value into `grid[row][col]`.
Complexity: O(h * 2^h) time and space (the size of the grid)
from collections import deque
class Solution:
def printTree(self, root: Optional[TreeNode]) -> List[List[str]]:
def height(n): # number of levels
return 1 + max(height(n.left), height(n.right)) if n else 0
rows = height(root)
cols = 2 ** rows - 1
grid = [[""] * cols for _ in range(rows)]
q = deque([(root, 0, (cols - 1) // 2)]) # tag: (node, row, col)
while q:
node, row, col = q.popleft()
grid[row][col] = str(node.val)
step = (cols + 1) >> (row + 2) # column offset halves each row
if node.left: q.append((node.left, row + 1, col - step))
if node.right: q.append((node.right, row + 1, col + step))
return gridWalk the tree breadth first with `(node, row, col)`. The root is column 0, a left child is `col - 1`, a right child is `col + 1`. Collect `(row, value)` pairs in a dict keyed by column. Sorting each column's pairs puts shallower nodes first and breaks ties at the same row by value. Output the columns from left to right.
Complexity: O(n log n) time, O(n) extra space
from collections import deque, defaultdict
class Solution:
def verticalTraversal(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root:
return []
cols = defaultdict(list)
q = deque([(root, 0, 0)]) # tag: (node, row, col)
while q:
node, row, col = q.popleft()
cols[col].append((row, node.val)) # keep the row for tie-breaking
if node.left: q.append((node.left, row + 1, col - 1))
if node.right: q.append((node.right, row + 1, col + 1))
return [[v for _, v in sorted(cols[c])] for c in sorted(cols)]A binary tree where left < node < right holds for every node. That ordering gives O(h) search, and sorted inorder.
Use this when you look for a value, or a range of values, in a BST and can throw away one whole subtree at every comparison.
This is the textbook BST search. Compare val with the current node: smaller goes left, larger goes right.
The loop `while node and node.val != val` stops on a match or when it falls off the tree.
Only one side is ever visited, so the cost is the height of the tree.
Complexity: O(h) time, O(1) extra space
class Solution:
def searchBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
node = root
while node and node.val != val: # stop on a match or on a missing child
node = node.left if val < node.val else node.right # discard the side that cannot hold val
return nodeSearching for a whole range is the same search, but sometimes both sides stay open. The BST order tells us which sides to skip.
If node.val is not above low, everything on its left is too small, so the left side is skipped.
If node.val is not below high, everything on its right is too big, so the right side is skipped.
Complexity: O(n) time in the worst case (O(h + k) for k nodes in range), O(h) extra space
class Solution:
def rangeSumBST(self, root: Optional[TreeNode], low: int, high: int) -> int:
total = 0
stack = [root]
while stack:
node = stack.pop()
if not node:
continue
if low <= node.val <= high:
total += node.val
if node.val > low: # the left side can still hold values >= low
stack.append(node.left)
if node.val < high: # the right side can still hold values <= high
stack.append(node.right)
return totalFor each query we need the largest value that is at most q and the smallest value that is at least q. That is a BST search that remembers the last left turn and the last right turn.
Walking down costs O(h) per query, and h can be n on a skewed tree. So the inorder walk flattens the tree once into sorted values.
Then `bisect_left` makes the same smaller-or-larger decision in O(log n) per query.
Complexity: O(n + q log n) time, O(n) extra space
from bisect import bisect_left
class Solution:
def closestNodes(self, root: Optional[TreeNode], queries: List[int]) -> List[List[int]]:
values = []
stack, node = [], root
while stack or node: # iterative inorder: values come out sorted
while node:
stack.append(node)
node = node.left
node = stack.pop()
values.append(node.val)
node = node.right
answer = []
for q in queries:
i = bisect_left(values, q) # first index with values[i] >= q
if i < len(values) and values[i] == q:
answer.append([q, q])
continue
floor = values[i - 1] if i > 0 else -1
ceil = values[i] if i < len(values) else -1
answer.append([floor, ceil])
return answerUse this when you must add values to a BST, or rebuild a BST from an insertion order: follow the search path until a child is missing and put the new node there.
Insert is a search that ends at an empty spot. The recursive call `insert(root.left, val)` returns the (possibly new) root of that side.
When `root` is None, the search has ended and `TreeNode(val)` is created there. Assigning the result back to root.left or root.right links it in.
Complexity: O(h) time, O(h) recursion stack
class Solution:
def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if not root: # the search ended: the new node goes here
return TreeNode(val)
if val < root.val:
root.left = self.insertIntoBST(root.left, val) # smaller goes left
else:
root.right = self.insertIntoBST(root.right, val) # larger goes right
return rootA preorder list shows every parent before its children. So inserting the values one by one, in the given order, rebuilds the exact tree.
The helper `insert` is the same search-then-place routine as the previous question. With n at most 100, the O(n * h) cost is fine.
Complexity: O(n * h) time, O(h) extra space
class Solution:
def bstFromPreorder(self, preorder: List[int]) -> Optional[TreeNode]:
root = None
for value in preorder: # parents come first, so plain insertion rebuilds the tree
root = self.insert(root, value)
return root
def insert(self, node: Optional[TreeNode], value: int) -> TreeNode:
if not node: # the search ended: place the new node
return TreeNode(value)
if value < node.val:
node.left = self.insert(node.left, value)
else:
node.right = self.insert(node.right, value)
return nodeInserting nums in order builds one BST. The first value is the root, smaller values go left, larger go right.
Any other order gives the same tree if the root stays first and each side keeps its own internal order. The two sides can mix freely.
Choosing which positions the left values take gives comb(len - 1, len(left)) ways. Multiply that over every subtree, then subtract 1 for the original order.
Complexity: O(n^2) time, O(n) extra space
from math import comb
class Solution:
def numOfWays(self, nums: List[int]) -> int:
MOD = 10 ** 9 + 7
ways = 1
stack = [nums] # explicit stack: a skewed tree would be too deep to recurse
while stack:
seq = stack.pop()
if len(seq) <= 2: # 0, 1 or 2 values: only one valid order
continue
root = seq[0] # the first value is the root, as in repeated insert
smaller = [x for x in seq[1:] if x < root]
larger = [x for x in seq[1:] if x > root]
ways = ways * comb(len(seq) - 1, len(smaller)) % MOD # mix the two sides
stack.append(smaller)
stack.append(larger)
return (ways - 1) % MOD # remove the original order itselfUse this when you must remove a node from a BST and keep it valid: each recursive call returns the new root of its subtree, and the parent links to it.
Search for the key with the usual left or right choice. On a match there are three cases.
With no left child or no right child, return the other child and it takes the node's place. With two children, copy the inorder successor (smallest value of the right subtree) into the node.
Then delete that successor from the right subtree with the same function.
Complexity: O(h) time, O(h) recursion stack
class Solution:
def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
if not root:
return None
if key < root.val:
root.left = self.deleteNode(root.left, key)
elif key > root.val:
root.right = self.deleteNode(root.right, key)
else:
if not root.left: # 0 or 1 child: the other child takes its place
return root.right
if not root.right:
return root.left
successor = root.right
while successor.left: # inorder successor: smallest in the right subtree
successor = successor.left
root.val = successor.val
root.right = self.deleteNode(root.right, successor.val)
return rootTrimming is delete in bulk, and the BST order decides what to drop. If root.val is below low, the root and its whole left side are out, so return the trimmed right side.
If root.val is above high, return the trimmed left side. Otherwise keep the root and trim both sides, assigning the results back.
Complexity: O(n) time, O(h) recursion stack
class Solution:
def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
if not root:
return None
if root.val < low: # root and everything on its left are too small
return self.trimBST(root.right, low, high)
if root.val > high: # root and everything on its right are too big
return self.trimBST(root.left, low, high)
root.left = self.trimBST(root.left, low, high) # root stays: trim both sides
root.right = self.trimBST(root.right, low, high)
return rootUse this when you must check or repair BST order: every node has to fit a (low, high) range inherited from all its ancestors, not only from its parent.
Comparing a node only with its children misses a deep node that breaks an ancestor's bound. So pass a (low, high) range down.
Going left, the node's value becomes the new high. Going right, it becomes the new low. A node outside its range fails the whole tree.
Complexity: O(n) time, O(h) recursion stack
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
def valid(node, low, high):
if not node:
return True
if not (low < node.val < high): # must sit inside the inherited range
return False
return (valid(node.left, low, node.val) # left side: values below node
and valid(node.right, node.val, high)) # right side: values above node
return valid(root, float("-inf"), float("inf"))Inorder of a BST must be increasing, so two swapped values show up as one or two dips in the inorder sequence.
Walk inorder with a `prev` node. The first dip gives `first = prev`, and the last dip gives `second = node`. Swapping their values repairs the tree.
Complexity: O(n) time, O(h) extra space
class Solution:
def recoverTree(self, root: Optional[TreeNode]) -> None:
first = second = prev = None
stack, node = [], root
while stack or node: # inorder walk: values should only go up
while node:
stack.append(node)
node = node.left
node = stack.pop()
if prev and prev.val > node.val: # a dip: these two are out of order
if not first:
first = prev
second = node
prev = node
node = node.right
first.val, second.val = second.val, first.val # swap the two wrong valuesHere the tree is not a BST, so we test every subtree. A bottom-up check keeps the (low, high) range as the min and max of each subtree, instead of passing it down.
Each call returns (is_bst, min, max, sum). A node is a BST root when both sides are BSTs and left_max < node.val < right_min.
The best sum seen at any valid root is the answer, and 0 stands for the empty tree.
Complexity: O(n) time, O(h) recursion stack
from math import inf
class Solution:
def maxSumBST(self, root: Optional[TreeNode]) -> int:
best = 0
def dfs(node):
nonlocal best
if not node:
return True, inf, -inf, 0 # empty tree: fits any range
left_ok, left_min, left_max, left_sum = dfs(node.left)
right_ok, right_min, right_max, right_sum = dfs(node.right)
if left_ok and right_ok and left_max < node.val < right_min: # the range check
total = left_sum + right_sum + node.val
best = max(best, total)
return True, min(left_min, node.val), max(right_max, node.val), total
return False, -inf, inf, 0
dfs(root)
return bestUse this when a BST question asks for the kth smallest or largest value, or anything about values in sorted order: inorder visits values in increasing order, and reverse inorder in decreasing order.
Inorder visits a BST in increasing order, so the closest two values are always neighbours in that order.
Walk inorder and keep the previous value in `prev`. Each step compares the current value with prev, so no sorting or extra list is needed.
Complexity: O(n) time, O(h) extra space
class Solution:
def getMinimumDifference(self, root: Optional[TreeNode]) -> int:
best = float("inf")
prev = None
stack, node = [], root
while stack or node: # inorder walk: values arrive sorted
while node:
stack.append(node)
node = node.left
node = stack.pop()
if prev is not None:
best = min(best, node.val - prev) # only neighbours can be closest
prev = node.val
node = node.right
return bestInorder visits values from smallest to largest, so the kth node visited is the kth smallest.
The stack walk goes as far left as possible, then pops a node and counts it. Counting down `k` to 0 lets us stop early without visiting the rest of the tree.
Complexity: O(h + k) time, O(h) extra space
class Solution:
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
stack, node = [], root
while stack or node:
while node: # go as far left as possible: smallest first
stack.append(node)
node = node.left
node = stack.pop()
k -= 1
if k == 0: # this is the kth node in sorted order
return node.val
node = node.rightEach node must become its value plus every larger value. That is a running sum taken from the largest value downward.
Reverse inorder (right, node, left) visits values from largest to smallest, the kth largest direction. Keep `running`, add each node's value to it, and write it back.
Complexity: O(n) time, O(h) extra space
class Solution:
def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
running = 0
stack, node = [], root
while stack or node: # reverse inorder: right, node, left
while node:
stack.append(node)
node = node.right # largest values first
node = stack.pop()
running += node.val # sum of this value and all larger ones
node.val = running
node = node.left
return rootUse this when you need the lowest common ancestor in a BST: it is the first node where p and q stop going the same way.
Walk down from the root. If both p and q are smaller than the node, they are both in the left subtree, so go left. If both are larger, go right.
The first node where they split, or where the node equals one of them, is the LCA. No recursion and no extra memory are needed.
Complexity: O(h) time, O(1) extra space
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
node = root
while node:
if p.val < node.val and q.val < node.val: # both on the left
node = node.left
elif p.val > node.val and q.val > node.val: # both on the right
node = node.right
else: # they split here
return nodeWithout BST order we cannot choose a side by value, so we search both sides. The idea is still to find where p and q split.
A call returns p or q when it finds one in its subtree, and None otherwise. If left and right both return something, p and q are on different sides, so this node is the LCA.
Complexity: O(n) time, O(h) recursion stack
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root or root is p or root is q: # found a target, or ran out of tree
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right: # one on each side: the split point
return root
return left or rightThere are no p and q here, but the idea of the split point is the same. Each call returns (depth, lca) for its subtree.
If one side is deeper, the deepest leaves are all on that side, so pass its answer up. If both sides are equally deep, the deepest leaves are on both sides, so the current node is the LCA.
Complexity: O(n) time, O(h) recursion stack
class Solution:
def lcaDeepestLeaves(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
def dfs(node): # returns (depth, lca of the deepest leaves)
if not node:
return 0, None
left_depth, left_lca = dfs(node.left)
right_depth, right_lca = dfs(node.right)
if left_depth > right_depth: # deepest leaves are all on the left
return left_depth + 1, left_lca
if right_depth > left_depth: # deepest leaves are all on the right
return right_depth + 1, right_lca
return left_depth + 1, node # equal depth: the paths split here
return dfs(root)[1]Use this when you must build a height-balanced BST from sorted values: make the middle element the root and build each half the same way.
Taking the middle element as the root puts half the values on each side, so the two subtrees have almost equal height. The sorted order already matches the BST order.
`build(lo, hi)` works on indices instead of slices, so no copies are made.
Complexity: O(n) time, O(log n) recursion stack
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
def build(lo, hi): # builds a tree from nums[lo:hi]
if lo >= hi:
return None
mid = (lo + hi) // 2 # the middle becomes the root: balanced
return TreeNode(nums[mid], build(lo, mid), build(mid + 1, hi))
return build(0, len(nums))A linked list has no random access, so the middle is costly to find again and again. Copy the values into an array once, then it is the same question as the array version.
`build(lo, hi)` takes the middle index as the root and recurses on each half.
Complexity: O(n) time, O(n) extra space
class Solution:
def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
values = []
while head: # copy the list into an array for random access
values.append(head.val)
head = head.next
def build(lo, hi):
if lo >= hi:
return None
mid = (lo + hi) // 2 # the middle is the root: balanced
return TreeNode(values[mid], build(lo, mid), build(mid + 1, hi))
return build(0, len(values))Inorder of a BST is already sorted, so flatten the skewed tree into a sorted list first. That gives the "sorted array" input.
Then rebuild with the middle as root. The values are the same, only the shape changes to height-balanced.
Complexity: O(n) time, O(n) extra space
class Solution:
def balanceBST(self, root: TreeNode) -> TreeNode:
values = []
stack, node = [], root
while stack or node: # iterative inorder: sorted values
while node:
stack.append(node)
node = node.left
node = stack.pop()
values.append(node.val)
node = node.right
def build(lo, hi):
if lo >= hi:
return None
mid = (lo + hi) // 2 # the middle is the root: balanced
return TreeNode(values[mid], build(lo, mid), build(mid + 1, hi))
return build(0, len(values))Gives the smallest (or largest) item in O(1) and removes it in O(log n). Use it when you keep asking for "the best so far".
Use this when you keep asking for the smallest item so far while new items are pushed in between the asks.
Each round needs the smallest number, and its value changes after the round. That is a push and pop on a min heap. Store (value, index) so that equal values are popped in index order, as the problem asks. Pop the smallest, multiply it, and push it back with `heappush`. Then write the heap contents back into their original positions.
Complexity: O((n + k) log n) time, O(n) extra space
import heapq
class Solution:
def getFinalState(self, nums: List[int], k: int, multiplier: int) -> List[int]:
heap = [(x, i) for i, x in enumerate(nums)] # (value, index): ties pop the smaller index first
heapq.heapify(heap) # build the min heap in O(n)
for _ in range(k):
x, i = heapq.heappop(heap) # smallest value so far
heapq.heappush(heap, (x * multiplier, i)) # it goes back with its new value
result = [0] * len(nums)
for x, i in heap:
result[i] = x # heap order is not array order, so use the index
return resultThis is a design question where `reserve` always needs the smallest free seat. Keep all free seat numbers in a min heap. `reserve` is `heappop`, and `unreserve` is `heappush` of the seat that came back. The heap is the whole data structure, so each call costs O(log n).
Complexity: O(n) to build, O(log n) per reserve or unreserve, O(n) space
import heapq
class SeatManager:
def __init__(self, n: int):
self.free = list(range(1, n + 1)) # every seat starts free
heapq.heapify(self.free) # min heap of free seat numbers
def reserve(self) -> int:
return heapq.heappop(self.free) # the smallest free seat
def unreserve(self, seatNumber: int) -> None:
heapq.heappush(self.free, seatNumber) # the seat becomes free againWater inside the grid is held in by the lowest wall around it. Put the whole border in a min heap and always pop the lowest border cell. Look at its unseen neighbours: a neighbour lower than the popped cell holds `h - height` water. The neighbour then joins the border with height `max(h, height)`. Popping the lowest wall first is what guarantees the water level is right.
Complexity: O(m n log(m n)) time, O(m n) space
import heapq
class Solution:
def trapRainWater(self, heightMap: List[List[int]]) -> int:
rows, cols = len(heightMap), len(heightMap[0])
heap = [] # min heap of border cells: (height, row, col)
seen = [[False] * cols for _ in range(rows)]
for r in range(rows):
for c in range(cols):
if r in (0, rows - 1) or c in (0, cols - 1):
heapq.heappush(heap, (heightMap[r][c], r, c))
seen[r][c] = True
water = 0
while heap:
h, r, c = heapq.heappop(heap) # the lowest wall on the current border
for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
if 0 <= nr < rows and 0 <= nc < cols and not seen[nr][nc]:
seen[nr][nc] = True
water += max(0, h - heightMap[nr][nc]) # the wall h holds this much water
heapq.heappush(heap, (max(h, heightMap[nr][nc]), nr, nc))
return waterUse this when you keep asking for the largest item so far, so you store negated values in Python's min heap.
Every round smashes the two heaviest stones, so you need the largest value again and again. Store `-stone` so `heappop` returns the heaviest. Pop twice, negate both, and push the difference back if there is one. The loop ends with at most one stone left.
Complexity: O(n log n) time, O(n) space
import heapq
class Solution:
def lastStoneWeight(self, stones: List[int]) -> int:
heap = [-s for s in stones] # max heap: store negatives
heapq.heapify(heap)
while len(heap) > 1:
first = -heapq.heappop(heap) # heaviest stone
second = -heapq.heappop(heap) # second heaviest
if first != second:
heapq.heappush(heap, -(first - second)) # the leftover goes back in
return -heap[0] if heap else 0To avoid equal neighbours, always place the letter with the most copies left. Keep (negated count, letter) in a max heap. After placing a letter, hold it out for one turn so it cannot be placed twice in a row, then push it back with one fewer copy. If the heap is empty while a letter is still held, no valid order exists and the answer is an empty string.
Complexity: O(n log a) time where a is the alphabet size, O(n) space for the answer
import heapq
from collections import Counter
class Solution:
def reorganizeString(self, s: str) -> str:
heap = [(-count, ch) for ch, count in Counter(s).items()] # max heap by count
heapq.heapify(heap)
result = []
held = None # the letter we just used, waiting one turn
while heap or held:
if not heap:
return "" # only the held letter is left, it would touch itself
count, ch = heapq.heappop(heap) # letter with the most copies left
result.append(ch)
if held:
heapq.heappush(heap, held) # the previous letter may be used again
held = None
if count + 1 < 0: # counts are negative: still has copies left
held = (count + 1, ch)
return "".join(result)Drive as far as the fuel allows, and remember the fuel of every station you pass in a max heap. You do not need to decide at the station. When you cannot go further, pop the biggest tank you passed and act as if you had refuelled there. Each pop costs one stop, and the greedy choice of the biggest tank gives the fewest stops. If the heap is empty and you are still short, return -1.
Complexity: O(n log n) time, O(n) space
import heapq
class Solution:
def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -> int:
reach = startFuel # farthest position we can get to so far
passed = [] # max heap (negated) of fuel at stations we could stop at
stops = i = 0
while reach < target:
while i < len(stations) and stations[i][0] <= reach:
heapq.heappush(passed, -stations[i][1]) # in hindsight we may refuel here
i += 1
if not passed:
return -1 # stuck: no station left to use
reach += -heapq.heappop(passed) # take the biggest tank we passed
stops += 1
return stopsUse this when the question asks for the kth largest or kth smallest value and you do not need the full sorted order.
Keep a min heap that holds only the k largest numbers seen so far. Its top is the smallest of those winners, which is exactly the kth largest. After each `heappush`, if the heap has more than k items, pop the smallest. Each `add` then costs O(log k), and no sorting is ever done.
Complexity: O(log k) per add, O(k) space
import heapq
class KthLargest:
def __init__(self, k: int, nums: List[int]):
self.k = k
self.heap = [] # min heap of the k largest numbers so far
for x in nums:
self.add(x)
def add(self, val: int) -> int:
heapq.heappush(self.heap, val)
if len(self.heap) > self.k:
heapq.heappop(self.heap) # kick out the smallest
return self.heap[0] # smallest of the k winners = kth largestThis is the same idea as the stream version, applied to a list. Push each number into a min heap and pop whenever the size passes k. What remains is the k largest numbers, and `heap[0]` is the kth largest. This is O(n log k), better than sorting when k is small. For the kth smallest you would keep a max heap of size k instead.
Complexity: O(n log k) time, O(k) space
import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
heap = [] # min heap holding the k largest numbers
for x in nums:
heapq.heappush(heap, x)
if len(heap) > k:
heapq.heappop(heap) # drop the smallest
return heap[0] # the kth largestEach query asks for the kth smallest trimmed number, with ties broken by the smaller index. For the kth smallest, flip the idea: keep a max heap of size k by pushing negated keys. The key is `(trimmed value, index)`, so the tuple itself settles ties. After pushing, pop when the size passes k. The top is then the largest of the k smallest, which is the kth smallest.
Complexity: O(q n log k) time, O(k) extra space
import heapq
class Solution:
def smallestTrimmedNumbers(self, nums: List[str], queries: List[List[int]]) -> List[int]:
answer = []
for k, trim in queries:
heap = [] # max heap (negated) of the k smallest keys
for i, num in enumerate(nums):
value = int(num[-trim:]) # keep the rightmost `trim` digits
heapq.heappush(heap, (-value, -i)) # (value, index) compared as a pair
if len(heap) > k:
heapq.heappop(heap) # drop the largest key
answer.append(-heap[0][1]) # top = kth smallest, read its index
return answerUse this when you need the K best items (most frequent, closest, highest scoring), not only the Kth one.
Count each value first, then pick the K counts that are largest. Push (count, value) into a min heap and pop when the size passes k, so the least frequent candidate is kicked out. What is left in the heap is the top K. This is O(n log k) instead of sorting all the distinct values.
Complexity: O(n log k) time, O(n) space
import heapq
from collections import Counter
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
heap = [] # min heap of (count, value), at most k items
for value, count in Counter(nums).items():
heapq.heappush(heap, (count, value))
if len(heap) > k:
heapq.heappop(heap) # kick out the least frequent
return [value for count, value in heap]The K closest points are the K smallest distances, so keep a max heap of size k. Push the negated squared distance, and pop when the size passes k. The popped point is the farthest of the current K, so it cannot be a winner. Squared distance gives the same order as the real distance, so no square root is needed.
Complexity: O(n log k) time, O(k) space
import heapq
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
heap = [] # max heap (negated distance) of the k closest points
for x, y in points:
dist = x * x + y * y # squared distance, same order as the real one
heapq.heappush(heap, (-dist, x, y))
if len(heap) > k:
heapq.heappop(heap) # drop the farthest point
return [[x, y] for _, x, y in heap]Sort engineers by efficiency from high to low. When you reach an engineer, that engineer's efficiency is the smallest in the team, so only speed is left to choose. Keep the K largest speeds in a min heap with a running sum, and pop the slowest when the size passes k. The performance for this step is `speed_sum * eff`, and the answer is the best step.
Complexity: O(n log n) time, O(n) space
import heapq
class Solution:
def maxPerformance(self, n: int, speed: List[int], efficiency: List[int], k: int) -> int:
engineers = sorted(zip(efficiency, speed), reverse=True) # best efficiency first
heap = [] # min heap of the k biggest speeds so far
speed_sum = best = 0
for eff, spd in engineers:
heapq.heappush(heap, spd)
speed_sum += spd
if len(heap) > k:
speed_sum -= heapq.heappop(heap) # drop the slowest engineer
best = max(best, speed_sum * eff) # eff is the lowest efficiency in this team
return best % (10 ** 9 + 7)Use this when you have K sorted lists or rows and need their merged order, or the smallest values across all of them.
Every row is a sorted list, so this is a K-way merge of the rows. Put the first element of each row in a min heap as (value, row, col). Pop the smallest, then push the next element of the same row. After k - 1 pops, the top of the heap is the kth smallest. The heap never holds more than one entry per row.
Complexity: O(k log min(n, k)) time, O(min(n, k)) space
import heapq
class Solution:
def kthSmallest(self, matrix: List[List[int]], k: int) -> int:
n = len(matrix)
heap = [(matrix[r][0], r, 0) for r in range(min(n, k))] # head of each row: (value, row, col)
heapq.heapify(heap)
for _ in range(k - 1):
value, r, c = heapq.heappop(heap) # smallest head
if c + 1 < n:
heapq.heappush(heap, (matrix[r][c + 1], r, c + 1)) # refill from the same row
return heap[0][0]Put the head node of each list in a min heap. Pop the smallest node, attach it to the answer, and push that node's next node. The list index `i` sits between the value and the node, so two equal values never make Python compare ListNode objects. The heap holds at most k nodes, so each of the N nodes costs O(log k).
Complexity: O(N log k) time for N nodes in total, O(k) extra space
import heapq
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
heap = [(node.val, i, node) for i, node in enumerate(lists) if node] # head of each list
heapq.heapify(heap) # list id i breaks ties, nodes are never compared
dummy = tail = ListNode()
while heap:
_, i, node = heapq.heappop(heap) # smallest head among all lists
tail.next = node
tail = node
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next)) # refill from the same list
return dummy.nextA range that covers all K lists must contain one number from each list. Keep one candidate per list in a min heap, and track the largest candidate in `current_max`. The heap top is the smallest candidate, so `[low, current_max]` is the best range for this moment. To shrink the range, advance the list that holds the smallest candidate. Stop when that list runs out.
Complexity: O(N log k) time for N numbers in total, O(k) space
import heapq
class Solution:
def smallestRange(self, nums: List[List[int]]) -> List[int]:
heap = [(row[0], i, 0) for i, row in enumerate(nums)] # one candidate per list
heapq.heapify(heap)
current_max = max(row[0] for row in nums) # largest candidate right now
best = [heap[0][0], current_max]
while True:
low, i, j = heapq.heappop(heap) # smallest candidate
if current_max - low < best[1] - best[0]:
best = [low, current_max] # a tighter range that covers all lists
if j + 1 == len(nums[i]):
return best # this list is used up, no more full ranges
nxt = nums[i][j + 1]
current_max = max(current_max, nxt)
heapq.heappush(heap, (nxt, i, j + 1)) # refill from the same listUse this when two groups need their own best-first order at once, such as items still waiting and items ready now.
Two heaps run side by side. `free` holds the empty chair numbers, smallest first. `busy` holds (leave time, chair) for seated friends, earliest leaver first. Handle friends in arrival order. Before seating one, move every chair whose friend has left from `busy` to `free`. Then `heappop(free)` is the smallest empty chair.
Complexity: O(n log n) time, O(n) space
import heapq
class Solution:
def smallestChair(self, times: List[List[int]], targetFriend: int) -> int:
order = sorted(range(len(times)), key=lambda i: times[i][0]) # friends by arrival time
free = list(range(len(times))) # heap 1: empty chairs, smallest number first
heapq.heapify(free)
busy = [] # heap 2: (leave time, chair) of seated friends
for i in order:
arrive, leave = times[i]
while busy and busy[0][0] <= arrive: # these friends have left, their chairs are empty
heapq.heappush(free, heapq.heappop(busy)[1])
chair = heapq.heappop(free) # smallest empty chair
if i == targetFriend:
return chair
heapq.heappush(busy, (leave, chair))Only the first `candidates` and the last `candidates` workers can be hired, so give each end its own min heap, `front` and `back`. Each round, compare the two tops and hire the cheaper one. A tie goes to `front`, which holds the smaller index. After a hire, slide that end inward by pushing the next unseen worker into the same heap.
Complexity: O((c + k) log c) time for c = candidates, O(c) space
import heapq
class Solution:
def totalCost(self, costs: List[int], k: int, candidates: int) -> int:
n = len(costs)
front = costs[:candidates] # heap 1: first candidates workers
back = costs[max(candidates, n - candidates):] # heap 2: last candidates workers
heapq.heapify(front)
heapq.heapify(back)
lo, hi = candidates, n - candidates - 1 # next unseen worker from each end
total = 0
for _ in range(k):
if not back or (front and front[0] <= back[0]): # ties go to the smaller index
total += heapq.heappop(front)
if lo <= hi:
heapq.heappush(front, costs[lo]) # slide the front window inward
lo += 1
else:
total += heapq.heappop(back)
if lo <= hi:
heapq.heappush(back, costs[hi]) # slide the back window inward
hi -= 1
return totalUse two heaps for two groups of projects. `locked` is a min heap by capital for projects you cannot afford yet. `ready` is a max heap by profit for projects you can afford now. Each round, move every project whose capital is at most `w` from `locked` to `ready`. Then take the most profitable ready project and add its profit to `w`, which may unlock more.
Complexity: O((n + k) log n) time, O(n) space
import heapq
class Solution:
def findMaximizedCapital(self, k: int, w: int, profits: List[int], capital: List[int]) -> int:
locked = list(zip(capital, profits)) # heap 1: not affordable yet, cheapest first
heapq.heapify(locked)
ready = [] # heap 2: affordable, biggest profit first (negated)
for _ in range(k):
while locked and locked[0][0] <= w: # move newly affordable projects across
cap, profit = heapq.heappop(locked)
heapq.heappush(ready, -profit)
if not ready:
break # nothing affordable, stop early
w += -heapq.heappop(ready) # take the most profitable project
return wUse this when numbers keep arriving (or a window slides) and you must report the median after every change.
Split the numbers into a lower half in a max heap `lo` (negated) and an upper half in a min heap `hi`. In `addNum`, push into `lo`, move its biggest to `hi`, and move one back if `hi` got bigger. This keeps `lo` the same size as `hi` or one larger, and every number in `lo` is at most every number in `hi`. The median is then at the tops, so `findMedian` is O(1).
Complexity: O(log n) per addNum, O(1) per findMedian, O(n) space
import heapq
class MedianFinder:
def __init__(self):
self.lo = [] # max heap (negated): the lower half
self.hi = [] # min heap: the upper half
def addNum(self, num: int) -> None:
heapq.heappush(self.lo, -num)
heapq.heappush(self.hi, -heapq.heappop(self.lo)) # move the biggest of lo up to hi
if len(self.hi) > len(self.lo): # keep lo the same size or one bigger
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def findMedian(self) -> float:
if len(self.lo) > len(self.hi):
return float(-self.lo[0]) # odd count: the top of lo
return (-self.lo[0] + self.hi[0]) / 2 # even count: average of the two topsUse the same two heaps, `small` (a negated max heap) and `large`, but now numbers also leave the window. A heap cannot remove from the middle, so record the leaver in `buried` and only pop it when it reaches a heap top (`prune`). Track the live size of each heap separately, because the real sizes include buried values. After each add and remove, `rebalance` so `small` is the same size as `large` or one larger. The median is then read from the tops.
Complexity: O(n log k) time, O(n) space
import heapq
from collections import defaultdict
class Solution:
def medianSlidingWindow(self, nums: List[int], k: int) -> List[float]:
small, large = [], [] # small: max heap (negated) lower half, large: min heap upper half
buried = defaultdict(int) # numbers that left the window but are still inside a heap
size = [0, 0] # live sizes of small and large
def prune(heap, sign): # pop buried numbers sitting on top of a heap
while heap and buried[sign * heap[0]] > 0:
buried[sign * heap[0]] -= 1
heapq.heappop(heap)
result = []
for i, x in enumerate(nums):
if not small or x <= -small[0]: # add x to the half it belongs to
heapq.heappush(small, -x); size[0] += 1
else:
heapq.heappush(large, x); size[1] += 1
if i >= k: # the oldest number leaves the window
out = nums[i - k]
buried[out] += 1 # lazy delete: remove it later
size[0 if out <= -small[0] else 1] -= 1
prune(small, -1); prune(large, 1)
if size[0] > size[1] + 1: # rebalance: move one top across
heapq.heappush(large, -heapq.heappop(small)); size[0] -= 1; size[1] += 1
prune(small, -1)
elif size[0] < size[1]:
heapq.heappush(small, -heapq.heappop(large)); size[1] -= 1; size[0] += 1
prune(large, 1)
if i >= k - 1:
result.append(float(-small[0]) if k % 2 else (-small[0] + large[0]) / 2)
return resultNodes and edges. Most problems hide a graph: grids, dependencies, word ladders, networks.
Use this when you need the fewest steps in an unweighted graph or grid, or when nodes must be handled level by level.
The maze is a grid graph where each open cell joins its four open neighbours, so the fewest steps to an exit is a BFS from the entrance.
The queue holds (row, col, steps). The line maze[nr][nc] = '+' turns a cell into a wall when it is pushed, which is the seen mark.
The first open border cell found (other than the entrance, which is already marked) is reached by the fewest steps, so return steps + 1 at once.
If the queue empties without reaching a border cell, there is no exit.
Complexity: O(rows * cols) time and space
from collections import deque
class Solution:
def nearestExit(self, maze: List[List[str]], entrance: List[int]) -> int:
rows, cols = len(maze), len(maze[0])
q = deque([(entrance[0], entrance[1], 0)])
maze[entrance[0]][entrance[1]] = '+' # the entrance is seen, so it never counts as an exit
while q:
r, c, steps = q.popleft()
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and maze[nr][nc] == '.':
if nr in (0, rows - 1) or nc in (0, cols - 1):
return steps + 1 # BFS: the first exit found is the nearest
maze[nr][nc] = '+' # mark seen when pushed
q.append((nr, nc, steps + 1))
return -1Searching from every 1 for its nearest 0 would repeat work. Reverse it: start from all the 0 cells together (multi-source BFS) and let the distance spread outwards.
Every 0 is queued with distance 0 and every 1 starts as -1, meaning not seen yet.
The first time a cell is reached, dist[nr][nc] == -1 turns false and its value is dist[r][c] + 1, which is already the shortest distance.
Complexity: O(rows * cols) time and space
from collections import deque
class Solution:
def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
rows, cols = len(mat), len(mat[0])
dist = [[-1] * cols for _ in range(rows)] # -1 means not seen yet
q = deque()
for r in range(rows):
for c in range(cols):
if mat[r][c] == 0:
dist[r][c] = 0
q.append((r, c)) # every 0 is a BFS source
while q:
r, c = q.popleft()
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and dist[nr][nc] == -1:
dist[nr][nc] = dist[r][c] + 1 # first arrival is the shortest distance
q.append((nr, nc))
return distThe nodes to search over are the buses, not the stops, because the answer counts buses taken. Riding one bus reaches every stop on its route for the cost of 1.
First map each stop to the routes that pass through it. Then run BFS where the queue holds (stop, buses taken so far).
From a stop, board every route not yet used, and push each new stop on that route with buses + 1.
The seen_routes set matters: a route is scanned once, so the total work stays linear in the number of stops listed.
Complexity: O(total stops across all routes) time and space
from collections import deque, defaultdict
class Solution:
def numBusesToDestination(self, routes: List[List[int]], source: int, target: int) -> int:
if source == target:
return 0
stop_routes = defaultdict(list) # stop -> indexes of routes through it
for i, route in enumerate(routes):
for stop in route:
stop_routes[stop].append(i)
q = deque([(source, 0)])
seen_stops = {source}
seen_routes = set()
while q:
stop, buses = q.popleft()
for i in stop_routes[stop]:
if i in seen_routes:
continue
seen_routes.add(i) # each route is boarded at most once
for nxt in routes[i]:
if nxt == target:
return buses + 1 # BFS level = number of buses taken
if nxt not in seen_stops:
seen_stops.add(nxt)
q.append((nxt, buses + 1))
return -1Use this when you must explore everything reachable, list or count paths, or try choices and backtrack.
Start at (sr, sc) and recurse into the four neighbours that still have the old colour.
Painting a cell with the new colour is also the seen mark, so no cell is visited twice.
The early return when old == color matters: if nothing would change, the recursion would never stop.
Complexity: O(rows * cols) time, O(rows * cols) recursion stack in the worst case
class Solution:
def floodFill(self, image: List[List[int]], sr: int, sc: int, color: int) -> List[List[int]]:
old = image[sr][sc]
if old == color: # nothing to paint, and no seen mark would ever be set
return image
rows, cols = len(image), len(image[0])
def dfs(r, c):
if not (0 <= r < rows and 0 <= c < cols) or image[r][c] != old:
return
image[r][c] = color # painting doubles as the seen mark
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
dfs(r + dr, c + dc) # explore each neighbour fully before the next
dfs(sr, sc)
return imageListing paths is DFS with backtracking: append the next node to path, recurse, then pop it.
When the DFS reaches the last node, copy the current path into the answer (the copy matters because path keeps changing).
The graph is a DAG, so there are no cycles and no seen set is needed. A seen set would hide valid paths that share a node.
Complexity: O(2^n * n) time in the worst case, O(n) extra space for the path
class Solution:
def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
target = len(graph) - 1
paths, path = [], [0]
def dfs(u):
if u == target:
paths.append(path[:]) # copy: path is reused by later branches
return
for v in graph[u]:
path.append(v) # choose
dfs(v) # explore
path.pop() # un-choose (backtrack)
dfs(0)
return pathsThe tickets are directed edges and the route must use every one exactly once, so this is a DFS that walks edges, not nodes.
From JFK, always take the smallest destination next. Add an airport to route only after all of its tickets are used (post-order).
Reading route backwards gives the itinerary, because the airport where we get stuck is the last stop.
Sorting in reverse order lets list.pop() hand out the smallest name in O(1).
Complexity: O(E log E) time, O(E) space
from collections import defaultdict
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
adj = defaultdict(list)
for src, dst in sorted(tickets, reverse=True): # reverse sort: pop() gives the smallest name
adj[src].append(dst)
route = []
def dfs(airport):
while adj[airport]:
dfs(adj[airport].pop()) # use each ticket once
route.append(airport) # post-order: add after all tickets are used
dfs("JFK")
return route[::-1]Use this when the question asks how many separate groups, islands or provinces there are, or how big each group is.
Cities are nodes and isConnected is an adjacency matrix.
Loop over every city. Each time one is unseen, a new province starts, so run dfs to mark its whole group and add 1.
The number of times dfs is launched from the outer loop is the number of components.
Complexity: O(n^2) time, O(n) space
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
n = len(isConnected)
seen = set()
def dfs(u):
seen.add(u)
for v in range(n):
if isConnected[u][v] and v not in seen:
dfs(v)
provinces = 0
for city in range(n): # loop over all nodes
if city not in seen: # an unseen node starts a new component
dfs(city) # flood-fill its whole group
provinces += 1
return provincesSame idea on a grid: each cell is a node and its neighbours are the 4 direction vectors.
Scan every cell. A land cell that has not been visited starts a new island, so run dfs and add 1.
The dfs sinks each land cell it visits (writes "0"), which acts as the seen mark and stops the same island being counted twice.
Complexity: O(rows * cols) time and space
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
rows, cols = len(grid), len(grid[0])
def dfs(r, c):
if not (0 <= r < rows and 0 <= c < cols) or grid[r][c] != "1":
return
grid[r][c] = "0" # sink the land so it is never counted again
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
dfs(r + dr, c + dc)
islands = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == "1": # unseen land: a new component
dfs(r, c)
islands += 1
return islandsFirst find the components: run dfs on each island, label its cells with a new id (2, 3, ...) and store its area in size.
Then try every water cell. Flipping it joins the distinct islands around it, so the new area is 1 plus their sizes.
The set touching removes duplicates, so one island that borders the cell on two sides is counted once.
If there is no water, the answer is the biggest island, which is the starting value of best.
Complexity: O(n^2) time and space
class Solution:
def largestIsland(self, grid: List[List[int]]) -> int:
n = len(grid)
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
size = {} # island id -> area
def dfs(r, c, island_id):
grid[r][c] = island_id # label the cell with its component id
area = 1
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 1:
area += dfs(nr, nc, island_id)
return area
next_id = 2 # ids start at 2 so they differ from 0 and 1
for r in range(n):
for c in range(n):
if grid[r][c] == 1:
size[next_id] = dfs(r, c, next_id)
next_id += 1
best = max(size.values(), default=0)
for r in range(n):
for c in range(n):
if grid[r][c] == 0:
touching = {grid[r + dr][c + dc] for dr, dc in dirs
if 0 <= r + dr < n and 0 <= c + dc < n and grid[r + dr][c + dc] > 1}
best = max(best, 1 + sum(size[i] for i in touching)) # flip this water cell
return bestUse this when you must know whether a graph has a cycle: prerequisites that can never finish, a loop in a grid, or a walk that returns to itself.
A prerequisite cycle is a directed cycle, so a node seen before is not enough. Use DFS with three states: unvisited, in the current path, done.
Reaching a node that is in the current path means a cycle, so the courses cannot all be finished.
Setting a node to DONE after its neighbours are explored means it is never explored again.
Complexity: O(V + E) time, O(V + E) space
from collections import defaultdict
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
adj = defaultdict(list)
for course, pre in prerequisites:
adj[pre].append(course)
UNVISITED, IN_PATH, DONE = 0, 1, 2
state = [UNVISITED] * numCourses
def has_cycle(u):
state[u] = IN_PATH # u is on the current DFS path
for v in adj[u]:
if state[v] == IN_PATH: # back to a node on our own path: cycle
return True
if state[v] == UNVISITED and has_cycle(v):
return True
state[u] = DONE # fully explored, safe to skip next time
return False
return not any(state[c] == UNVISITED and has_cycle(c) for c in range(numCourses))This graph is undirected, so the rule is: a cycle exists when the walk meets a seen cell that is not the cell it came from.
Each stack entry carries its parent (pr, pc). Skipping the parent is the key line.
Any other same-letter neighbour that is already seen closes a loop. In a grid, such a loop always has at least 4 cells, so no extra length check is needed.
The loop uses an explicit stack, so a large grid cannot overflow the recursion limit.
Complexity: O(rows * cols) time and space
class Solution:
def containsCycle(self, grid: List[List[str]]) -> bool:
rows, cols = len(grid), len(grid[0])
seen = [[False] * cols for _ in range(rows)]
for sr in range(rows):
for sc in range(cols):
if seen[sr][sc]:
continue
seen[sr][sc] = True
stack = [(sr, sc, -1, -1)] # (row, col, parent row, parent col)
while stack:
r, c, pr, pc = stack.pop()
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if not (0 <= nr < rows and 0 <= nc < cols) or grid[nr][nc] != grid[r][c]:
continue
if (nr, nc) == (pr, pc): # the cell we came from is not a cycle
continue
if seen[nr][nc]: # seen and not our parent: cycle
return True
seen[nr][nc] = True
stack.append((nr, nc, r, c))
return FalseEvery node has at most one outgoing edge, so each walk is a single path that either ends or runs into a node already seen.
Give each node a global time stamp when it is first reached. Remember the stamp where the current walk began (begin).
If the walk lands on a node with dist[u] >= begin, that node is on the current path (the "in-path" state), so the cycle length is clock - dist[u].
If it lands on a node from an earlier walk, that node is finished (the "done" state) and gives no new cycle. Each node is stamped once.
Complexity: O(n) time, O(n) space
class Solution:
def longestCycle(self, edges: List[int]) -> int:
n = len(edges)
dist = [0] * n # time stamp of first visit, 0 = unvisited
clock = 1
best = -1
for start in range(n):
if dist[start]:
continue
begin = clock # stamps from this walk are >= begin
u = start
while u != -1 and not dist[u]:
dist[u] = clock
clock += 1
u = edges[u]
if u != -1 and dist[u] >= begin: # landed on the current path: a new cycle
best = max(best, clock - dist[u])
return bestUse this when nodes must be split into two sides so that no edge joins two nodes on the same side (conflicts, dislikes, odd cycles).
Two-colour the graph with BFS. A start node gets colour 0, and each unseen neighbour gets the opposite colour with color[u] ^ 1.
If an edge joins two nodes with the same colour, there is an odd cycle, so the answer is False.
The outer loop over s starts a new BFS in every component, because the graph may be disconnected.
Complexity: O(V + E) time, O(V) space
from collections import deque
class Solution:
def isBipartite(self, graph: List[List[int]]) -> bool:
n = len(graph)
color = [-1] * n
for s in range(n): # graph may be disconnected
if color[s] != -1:
continue
color[s] = 0
q = deque([s])
while q:
u = q.popleft()
for v in graph[u]:
if color[v] == -1:
color[v] = color[u] ^ 1 # neighbours get the opposite colour
q.append(v)
elif color[v] == color[u]: # same colour on both ends: odd cycle
return False
return TrueEach dislike pair is an edge. Splitting people into two groups where nobody shares a group with someone they dislike is the same as 2-colouring this graph.
Build the adjacency list (people are numbered from 1, so it has n + 1 slots), then colour each component with BFS.
Meeting a neighbour with the same colour means an odd cycle of dislikes, so the answer is False.
Complexity: O(n + E) time, O(n + E) space
from collections import deque, defaultdict
class Solution:
def possibleBipartition(self, n: int, dislikes: List[List[int]]) -> bool:
adj = defaultdict(list)
for a, b in dislikes:
adj[a].append(b)
adj[b].append(a)
color = [-1] * (n + 1)
for s in range(1, n + 1):
if color[s] != -1:
continue
color[s] = 0
q = deque([s])
while q:
u = q.popleft()
for v in adj[u]:
if color[v] == -1:
color[v] = color[u] ^ 1 # put the disliked person in the other group
q.append(v)
elif color[v] == color[u]: # both would be in the same group: impossible
return False
return TrueEvery edge must join nodes in neighbouring groups, so groups behave like BFS levels and each component must be bipartite.
BFS from a node: an edge between two nodes on the same level means an odd cycle, so return -1.
For one component, the number of BFS levels from a start node is the number of groups it can form. Try every node as the start and keep the best.
The component is identified by its smallest node label. Add the best value of each component.
Complexity: O(n * (n + m)) time, O(n + m) space
from collections import deque
class Solution:
def magnificentSets(self, n: int, edges: List[List[int]]) -> int:
adj = [[] for _ in range(n + 1)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
def bfs(src):
level = {src: 0}
q = deque([src])
while q:
u = q.popleft()
for v in adj[u]:
if v not in level:
level[v] = level[u] + 1
q.append(v)
elif level[v] == level[u]: # same level on both ends: odd cycle
return -1, 0
return max(level.values()) + 1, min(level) # groups, component id
best = {} # component id -> most groups
for start in range(1, n + 1):
groups, comp = bfs(start)
if groups == -1:
return -1
best[comp] = max(best.get(comp, 0), groups)
return sum(best.values())Use this when an edge means "A must come before B" and you need a valid order, a cycle check, or the longest chain through the dependencies.
Kahn's algorithm. Count each course's prerequisites (in-degree) and start the queue with the courses that have none.
Pop a course, append it to order, and lower the in-degree of every course it unlocks. A course whose in-degree reaches 0 joins the queue.
If order ends up shorter than numCourses, the rest are stuck in a cycle, so return an empty list.
Complexity: O(V + E) time, O(V + E) space
from collections import deque, defaultdict
class Solution:
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
adj, indeg = defaultdict(list), [0] * numCourses
for course, pre in prerequisites:
adj[pre].append(course) # edge: pre -> course
indeg[course] += 1
q = deque(i for i in range(numCourses) if indeg[i] == 0)
order = []
while q:
u = q.popleft()
order.append(u)
for v in adj[u]:
indeg[v] -= 1 # one prerequisite of v is now done
if indeg[v] == 0:
q.append(v)
return order if len(order) == numCourses else [] # short = cycle existsMake each ingredient a node with an edge to every recipe that needs it. A recipe's in-degree is the number of ingredients it needs.
The queue starts with the supplies, which need nothing. When a recipe's in-degree drops to 0 it can be made, so record it and push it, because it may be an ingredient of another recipe.
Recipes in a cycle, or missing an ingredient, never reach in-degree 0 and are left out.
Complexity: O(V + E) time, O(V + E) space
from collections import deque, defaultdict
class Solution:
def findAllRecipes(self, recipes: List[str], ingredients: List[List[str]], supplies: List[str]) -> List[str]:
adj = defaultdict(list) # ingredient -> recipes that need it
indeg = {}
for recipe, needs in zip(recipes, ingredients):
indeg[recipe] = len(needs)
for item in needs:
adj[item].append(recipe)
q = deque(supplies) # supplies are the nodes with in-degree 0
made = []
while q:
item = q.popleft()
for recipe in adj[item]:
indeg[recipe] -= 1
if indeg[recipe] == 0: # every ingredient is available
made.append(recipe)
q.append(recipe)
return madeA course can start only after all its prerequisites finish, so it starts at the latest finish time among them.
Process the courses in Kahn's order and keep start[v] = the largest finish time seen so far for v. When u is popped its finish time is start[u] + time[u].
The answer is the largest finish time. This is the longest path in a DAG, computed while taking the topological order.
Complexity: O(n + E) time, O(n + E) space
from collections import deque, defaultdict
class Solution:
def minimumTime(self, n: int, relations: List[List[int]], time: List[int]) -> int:
adj, indeg = defaultdict(list), [0] * n
for prev, nxt in relations:
adj[prev - 1].append(nxt - 1)
indeg[nxt - 1] += 1
start = [0] * n # earliest month each course can begin
q = deque(i for i in range(n) if indeg[i] == 0)
answer = 0
while q:
u = q.popleft()
end = start[u] + time[u]
answer = max(answer, end)
for v in adj[u]:
start[v] = max(start[v], end) # v waits for its slowest prerequisite
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return answerUse this when every move costs 1 (BFS) or only 0 or 1 (0-1 BFS with a deque), and the state may need more than the node itself.
Every move costs 1, so BFS finds the shortest path. The only change from a normal grid BFS is 8 neighbours, which two loops over (-1, 0, 1) produce.
The queue entry carries the path length so far, and the first time the bottom-right cell is popped, that length is the answer.
Writing 1 into a cell when it is queued marks it as seen. A blocked start or end cell returns -1 at once.
Complexity: O(n^2) time, O(n^2) space
from collections import deque
class Solution:
def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
n = len(grid)
if grid[0][0] == 1 or grid[n - 1][n - 1] == 1:
return -1
grid[0][0] = 1 # mark seen
q = deque([(0, 0, 1)]) # (row, col, path length so far)
while q:
r, c, dist = q.popleft()
if r == n - 1 and c == n - 1:
return dist # BFS: first arrival is the shortest
for dr in (-1, 0, 1):
for dc in (-1, 0, 1): # 8 directions
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 0:
grid[nr][nc] = 1
q.append((nr, nc, dist + 1))
return -1The best way into a node depends on the colour of the last edge used, so a BFS state is (node, colour of last edge), not the node alone.
From a state, only edges of the other colour may be taken: nxt = color ^ 1. Start at node 0 with both colours so either colour can go first.
The first time a node is popped in any state, its step count is the shortest alternating path. The seen set is over states.
Complexity: O(n + E) time, O(n + E) space
from collections import deque, defaultdict
class Solution:
def shortestAlternatingPaths(self, n: int, redEdges: List[List[int]], blueEdges: List[List[int]]) -> List[int]:
adj = {0: defaultdict(list), 1: defaultdict(list)} # 0 = red, 1 = blue
for u, v in redEdges:
adj[0][u].append(v)
for u, v in blueEdges:
adj[1][u].append(v)
answer = [-1] * n
q = deque([(0, 0, 0), (0, 1, 0)]) # (node, colour of last edge, steps)
seen = {(0, 0), (0, 1)} # states, not only nodes
while q:
u, color, steps = q.popleft()
if answer[u] == -1:
answer[u] = steps # first arrival is the shortest
nxt = color ^ 1 # next edge must have the other colour
for v in adj[nxt][u]:
if (v, nxt) not in seen:
seen.add((v, nxt))
q.append((v, nxt, steps + 1))
return answerStepping into an empty cell costs 0 and into an obstacle costs 1, so edge weights are only 0 or 1. That is the case for 0-1 BFS.
Use a deque instead of a heap: a cost-0 move goes to the front (appendleft), a cost-1 move goes to the back. The deque stays ordered by distance, like Dijkstra's heap but in O(1) per push.
dist[r][c] is the fewest obstacles removed to reach that cell, and a cell is re-queued only when its distance improves.
Complexity: O(rows * cols) time and space
from collections import deque
class Solution:
def minimumObstacles(self, grid: List[List[int]]) -> int:
rows, cols = len(grid), len(grid[0])
dist = [[float("inf")] * cols for _ in range(rows)]
dist[0][0] = 0
dq = deque([(0, 0)])
while dq:
r, c = dq.popleft()
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
cost = dist[r][c] + grid[nr][nc] # entering an obstacle costs 1
if cost < dist[nr][nc]:
dist[nr][nc] = cost
if grid[nr][nc] == 0:
dq.appendleft((nr, nc)) # weight 0: front of the deque
else:
dq.append((nr, nc)) # weight 1: back of the deque
return dist[rows - 1][cols - 1]Use this when edges have non-negative weights (or a cost that never goes down) and you need the cheapest route from one source.
Directed weighted graph, one source k, all weights non-negative: the textbook Dijkstra case.
Keep a min heap of (distance, node). Pop the closest one; if d > dist[u] it is a stale entry, so skip it.
Otherwise relax every outgoing edge and push any improved distance. The signal reaches everyone at the largest distance, or -1 if some node stays at infinity.
Complexity: O((V + E) log V) time, O(V + E) space
import heapq
from math import inf
from collections import defaultdict
class Solution:
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
adj = defaultdict(list)
for u, v, w in times:
adj[u].append((v, w))
dist = [inf] * (n + 1)
dist[k] = 0
heap = [(0, k)]
while heap:
d, u = heapq.heappop(heap) # closest unsettled node
if d > dist[u]:
continue # stale entry
for v, w in adj[u]:
if d + w < dist[v]: # relax the edge
dist[v] = d + w
heapq.heappush(heap, (dist[v], v))
slowest = max(dist[1:])
return slowest if slowest < inf else -1Run Dijkstra from city 0, and also keep ways[v], the number of shortest paths that reach v.
If d + t is strictly shorter than dist[v], the old paths are worse, so replace: ways[v] = ways[u]. If it is equal, another shortest path exists, so add: ways[v] += ways[u].
Every road takes at least 1 minute, so ways[u] is complete by the time u is popped. Take the answer modulo 10^9 + 7.
Complexity: O((V + E) log V) time, O(V + E) space
import heapq
from math import inf
from collections import defaultdict
class Solution:
def countPaths(self, n: int, roads: List[List[int]]) -> int:
MOD = 10**9 + 7
adj = defaultdict(list)
for u, v, t in roads:
adj[u].append((v, t))
adj[v].append((u, t))
dist = [inf] * n
dist[0] = 0
ways = [0] * n
ways[0] = 1
heap = [(0, 0)]
while heap:
d, u = heapq.heappop(heap)
if d > dist[u]:
continue # stale entry
for v, t in adj[u]:
if d + t < dist[v]: # strictly shorter: forget the old count
dist[v] = d + t
ways[v] = ways[u]
heapq.heappush(heap, (dist[v], v))
elif d + t == dist[v]: # tie: one more shortest path
ways[v] = (ways[v] + ways[u]) % MOD
return ways[n - 1] % MODA cell can only be entered when the clock has reached its value, and you can wait only by stepping back and forth, which costs 2 minutes at a time. So the arrival time into a neighbour keeps the parity of t + 1.
The earliest arrival is max(t + 1, value + (value - t - 1) % 2). Costs never decrease, so Dijkstra with this arrival formula still works.
If both cells next to the start are locked past minute 1, you cannot even bounce, so the answer is -1. Otherwise every cell is reachable.
Complexity: O(rows * cols * log(rows * cols)) time, O(rows * cols) space
import heapq
from math import inf
class Solution:
def minimumTime(self, grid: List[List[int]]) -> int:
if grid[0][1] > 1 and grid[1][0] > 1: # cannot make the first move or wait at the start
return -1
rows, cols = len(grid), len(grid[0])
dist = [[inf] * cols for _ in range(rows)]
dist[0][0] = 0
heap = [(0, 0, 0)] # (time, row, col)
while heap:
t, r, c = heapq.heappop(heap)
if t > dist[r][c]:
continue # stale entry
if r == rows - 1 and c == cols - 1:
return t
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols:
# wait by bouncing back and forth: arrival keeps the parity of t + 1
arrive = max(t + 1, grid[nr][nc] + (grid[nr][nc] - t - 1) % 2)
if arrive < dist[nr][nc]:
dist[nr][nc] = arrive
heapq.heappush(heap, (arrive, nr, nc))
return -1Use this when you need single-source shortest paths with a cap on the number of edges, with negative weights, or when relaxing every edge in rounds is the simplest model.
Bellman-Ford with a fixed number of rounds: after round i, dist holds the cheapest price using at most i flights.
At most k stops means at most k + 1 flights, so run exactly k + 1 rounds and relax every edge in each.
Each round reads from dist but writes to a copy (nxt), so one round cannot chain two flights together. That copy is the key line.
Complexity: O(k * E) time, O(n) space
from math import inf
class Solution:
def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
dist = [inf] * n
dist[src] = 0
for _ in range(k + 1): # k stops = k + 1 flights
nxt = dist[:] # read old values, write new ones
for u, v, price in flights:
if dist[u] + price < nxt[v]: # relax the edge
nxt[v] = dist[u] + price
dist = nxt
return dist[dst] if dist[dst] < inf else -1Same relax-every-edge loop, but it maximises a product of probabilities instead of minimising a sum. best[v] is the highest probability found so far to reach v.
The edges are undirected, so each one is relaxed in both directions.
A best path has at most n - 1 edges and probabilities never exceed 1, so n - 1 rounds are enough. Stop early when a full pass changes nothing. On the largest inputs Dijkstra with a max heap is faster.
Complexity: O(n * E) time worst case (usually far less with the early exit), O(n) space
class Solution:
def maxProbability(self, n: int, edges: List[List[int]], succProb: List[float], start_node: int, end_node: int) -> float:
best = [0.0] * n
best[start_node] = 1.0
for _ in range(n - 1):
changed = False
for (u, v), p in zip(edges, succProb):
if best[u] * p > best[v]: # relax u -> v
best[v] = best[u] * p
changed = True
if best[v] * p > best[u]: # relax v -> u (undirected)
best[u] = best[v] * p
changed = True
if not changed: # nothing improved: done early
break
return best[end_node]Bellman-Ford again, but the rounds are minutes instead of edge counts: dp[t][v] is the cheapest cost to be standing in city v at exactly minute t.
For each minute t, relax every road both ways: arriving at v at minute t from u at minute t - w costs dp[t - w][u] plus the fee of v.
The answer is the cheapest dp[t][n - 1] over all t up to maxTime, or -1 if none is finite.
Complexity: O(maxTime * E) time, O(maxTime * n) space
from math import inf
class Solution:
def minCost(self, maxTime: int, edges: List[List[int]], passingFees: List[int]) -> int:
n = len(passingFees)
dp = [[inf] * n for _ in range(maxTime + 1)] # dp[t][v]: cheapest way to be in v at minute t
dp[0][0] = passingFees[0]
for t in range(1, maxTime + 1):
for u, v, w in edges: # relax every road in this round
if w <= t:
dp[t][v] = min(dp[t][v], dp[t - w][u] + passingFees[v])
dp[t][u] = min(dp[t][u], dp[t - w][v] + passingFees[u])
best = min(dp[t][n - 1] for t in range(maxTime + 1))
return best if best < inf else -1Use this when the graph is small (a few hundred nodes at most) and you need the shortest distance, or only reachability, between every pair.
The question needs the distance from every city to every other city, and n is at most 100, so run Floyd-Warshall to fill dist[i][j].
The loop over k, the intermediate city, must be the outermost one. Then count, for each city, how many others are within the threshold.
Pick the city with the fewest; on a tie take the larger index, which is why the comparison is <=.
Complexity: O(n^3) time, O(n^2) space
from math import inf
class Solution:
def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
dist = [[inf] * n for _ in range(n)]
for i in range(n):
dist[i][i] = 0
for u, v, w in edges:
dist[u][v] = dist[v][u] = w
for k in range(n): # k (the intermediate node) is the OUTER loop
for i in range(n):
for j in range(n):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
best_city, fewest = -1, inf
for i in range(n):
reach = sum(1 for j in range(n) if j != i and dist[i][j] <= distanceThreshold)
if reach <= fewest: # ties go to the larger index
best_city, fewest = i, reach
return best_cityFloyd-Warshall can also compute reachability. Let reach[i][j] be True when course i is a prerequisite of course j, directly or through a chain.
Use the same three loops with k outermost, but replace min/+ with or/and: reach[i][j] becomes True if reach[i][k] and reach[k][j].
After that, every query is one table lookup.
Complexity: O(n^3 + q) time, O(n^2) space
class Solution:
def checkIfPrerequisite(self, numCourses: int, prerequisites: List[List[int]], queries: List[List[int]]) -> List[bool]:
n = numCourses
reach = [[False] * n for _ in range(n)]
for a, b in prerequisites:
reach[a][b] = True # direct prerequisite
for k in range(n): # intermediate course is the OUTER loop
for i in range(n):
for j in range(n):
if reach[i][k] and reach[k][j]: # i -> k -> j
reach[i][j] = True
return [reach[a][b] for a, b in queries]Keep the full distance table and build it once with Floyd-Warshall. A query is then a lookup.
When an edge (u, v, w) is added, any new shortest path uses it once, so it has the form i -> u -> v -> j. One O(n^2) sweep over all pairs with dist[i][u] + w + dist[v][j] updates the table, no need to rerun all three loops.
A distance of infinity is reported as -1.
Complexity: constructor O(n^3), addEdge O(n^2), shortestPath O(1); O(n^2) space
from math import inf
class Graph:
def __init__(self, n: int, edges: List[List[int]]):
self.n = n
self.dist = [[inf] * n for _ in range(n)]
for i in range(n):
self.dist[i][i] = 0
for u, v, w in edges:
self.dist[u][v] = min(self.dist[u][v], w)
for k in range(n): # Floyd-Warshall, k outermost
for i in range(n):
for j in range(n):
if self.dist[i][k] + self.dist[k][j] < self.dist[i][j]:
self.dist[i][j] = self.dist[i][k] + self.dist[k][j]
def addEdge(self, edge: List[int]) -> None:
u, v, w = edge
d = self.dist
for i in range(self.n):
for j in range(self.n):
# a new shortest path uses the new edge once: i -> u -> v -> j
d[i][j] = min(d[i][j], d[i][u] + w + d[v][j])
def shortestPath(self, node1: int, node2: int) -> int:
d = self.dist[node1][node2]
return -1 if d == inf else dUse this when you must connect all nodes with the least total cost (V - 1 edges, no cycle), or decide which edges every cheapest tree needs.
Every pair of points is an edge weighted by Manhattan distance, so the graph is dense and the answer is the weight of its MST. Prim fits a dense graph.
Grow the tree from point 0. key[v] is the cheapest edge from the tree to v. Each step adds the outside point with the smallest key and adds that key to the total, then updates the keys of the rest.
A plain scan finds the smallest key, so no heap is needed: O(n) per step and O(n^2) overall, instead of sorting about n^2 / 2 edges.
Complexity: O(n^2) time, O(n) space
from math import inf
class Solution:
def minCostConnectPoints(self, points: List[List[int]]) -> int:
n = len(points)
key = [inf] * n # cheapest edge from the tree to each point
key[0] = 0
in_tree = [False] * n
total = 0
for _ in range(n):
u = -1
for v in range(n): # cheapest point still outside the tree
if not in_tree[v] and (u == -1 or key[v] < key[u]):
u = v
in_tree[u] = True
total += key[u] # this edge joins u to the tree
for v in range(n):
if not in_tree[v]:
d = abs(points[u][0] - points[v][0]) + abs(points[u][1] - points[v][1])
key[v] = min(key[v], d) # u may now be the closest tree point to v
return totalAlice and Bob each need a spanning tree, so keep one DSU per person. Each person's tree needs exactly n - 1 useful unions, and every other edge can be removed.
An edge usable by both (type 3) serves two people at once, so take those first, the way Kruskal takes the cheapest edge first. Then add the single-person edges.
An edge is kept only if union returns True for someone. If either person ends with fewer than n - 1 unions, the graph is not fully traversable and the answer is -1. Otherwise removable = total edges - kept edges.
Complexity: O(E * alpha(n)) time, O(n) space
class DSU:
def __init__(self, n):
self.p = list(range(n))
self.sz = [1] * n
def find(self, x):
while self.p[x] != x:
self.p[x] = self.p[self.p[x]] # path compression
x = self.p[x]
return x
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False # already connected: this edge is useless
if self.sz[a] < self.sz[b]:
a, b = b, a
self.p[b] = a
self.sz[a] += self.sz[b]
return True
class Solution:
def maxNumEdgesToRemove(self, n: int, edges: List[List[int]]) -> int:
alice, bob = DSU(n + 1), DSU(n + 1)
alice_joins = bob_joins = kept = 0
for t, u, v in edges: # shared edges first
if t == 3:
a, b = alice.union(u, v), bob.union(u, v)
alice_joins += a
bob_joins += b
kept += a or b
for t, u, v in edges: # then edges for one person only
if t == 1 and alice.union(u, v):
alice_joins += 1
kept += 1
elif t == 2 and bob.union(u, v):
bob_joins += 1
kept += 1
if alice_joins != n - 1 or bob_joins != n - 1: # someone cannot reach everyone
return -1
return len(edges) - keptWrite a helper mst(skip, force) that runs Kruskal over the edges sorted by weight, optionally skipping one edge or forcing one in first. It returns the tree weight, or infinity if the tree cannot be built.
An edge is critical if skipping it makes the MST heavier or impossible: every MST needs it. If it is not critical but forcing it in still gives the base weight, it is pseudo-critical: some MST uses it, not all.
With at most 200 edges, running Kruskal about twice per edge is fast enough.
Complexity: O(E^2 * alpha(n)) time, O(n + E) space
from math import inf
class DSU:
def __init__(self, n):
self.p = list(range(n))
self.sz = [1] * n
def find(self, x):
while self.p[x] != x:
self.p[x] = self.p[self.p[x]] # path compression
x = self.p[x]
return x
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False
if self.sz[a] < self.sz[b]:
a, b = b, a
self.p[b] = a
self.sz[a] += self.sz[b]
return True
class Solution:
def findCriticalAndPseudoCriticalEdges(self, n: int, edges: List[List[int]]) -> List[List[int]]:
order = sorted(range(len(edges)), key=lambda i: edges[i][2]) # edge indexes, lightest first
def mst(skip=-1, force=-1):
dsu = DSU(n)
total = joined = 0
if force != -1: # take this edge before anything else
u, v, w = edges[force]
dsu.union(u, v)
total, joined = w, 1
for i in order:
if i == skip:
continue
u, v, w = edges[i]
if dsu.union(u, v): # Kruskal: keep edges that join two groups
total += w
joined += 1
return total if joined == n - 1 else inf
base = mst()
critical, pseudo = [], []
for i in range(len(edges)):
if mst(skip=i) > base: # without it the MST is worse: critical
critical.append(i)
elif mst(force=i) == base: # usable in some MST: pseudo-critical
pseudo.append(i)
return [critical, pseudo]Use this when edges can be sorted by weight and you take each one that joins two different groups, or when queries ask what is connected using only edges up to some weight.
A route's effort is its largest height step, so we want to join the two corners using the smallest possible largest edge. That is exactly what Kruskal builds.
Make an edge between each pair of neighbouring cells, weighted by the height difference, and sort them. Union them in that order.
The first edge after which find(0) == find(target) is the answer: every lighter edge is already used, so no better route exists.
Complexity: O(E log E) time with E about 2 * rows * cols, O(E) space
class DSU:
def __init__(self, n):
self.p = list(range(n))
self.sz = [1] * n
def find(self, x):
while self.p[x] != x:
self.p[x] = self.p[self.p[x]] # path compression
x = self.p[x]
return x
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False
if self.sz[a] < self.sz[b]:
a, b = b, a
self.p[b] = a
self.sz[a] += self.sz[b]
return True
class Solution:
def minimumEffortPath(self, heights: List[List[int]]) -> int:
rows, cols = len(heights), len(heights[0])
edges = [] # (height difference, cell a, cell b)
for r in range(rows):
for c in range(cols):
if r + 1 < rows:
edges.append((abs(heights[r][c] - heights[r + 1][c]), r * cols + c, (r + 1) * cols + c))
if c + 1 < cols:
edges.append((abs(heights[r][c] - heights[r][c + 1]), r * cols + c, r * cols + c + 1))
edges.sort() # Kruskal: lightest edge first
dsu = DSU(rows * cols)
target = rows * cols - 1
for effort, a, b in edges:
dsu.union(a, b)
if dsu.find(0) == dsu.find(target): # the corners became connected
return effort # this edge is the biggest step on the best route
return 0 # a single cell needs no effortSort the edges by weight and answer the queries offline in order of increasing limit. One DSU is reused for all of them.
For each query, move the pointer e forward and union every edge whose weight is strictly below the limit. This is Kruskal paused at each limit.
Then find(p) == find(q) says whether p and q are connected using only edges that light. Answers are stored back at the query's original index.
Complexity: O(E log E + Q log Q) time, O(n + Q) space
class DSU:
def __init__(self, n):
self.p = list(range(n))
self.sz = [1] * n
def find(self, x):
while self.p[x] != x:
self.p[x] = self.p[self.p[x]] # path compression
x = self.p[x]
return x
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False
if self.sz[a] < self.sz[b]:
a, b = b, a
self.p[b] = a
self.sz[a] += self.sz[b]
return True
class Solution:
def distanceLimitedPathsExist(self, n: int, edgeList: List[List[int]], queries: List[List[int]]) -> List[bool]:
edgeList.sort(key=lambda e: e[2]) # Kruskal order: lightest first
order = sorted(range(len(queries)), key=lambda i: queries[i][2]) # smallest limit first
dsu = DSU(n)
answer = [False] * len(queries)
e = 0
for qi in order:
p, q, limit = queries[qi]
while e < len(edgeList) and edgeList[e][2] < limit: # add every edge below the limit
dsu.union(edgeList[e][0], edgeList[e][1])
e += 1
answer[qi] = dsu.find(p) == dsu.find(q)
return answerA good path starts and ends on equal values and never passes a larger one. Give each edge the weight max(vals[u], vals[v]) and process edges in that order, as Kruskal does.
Each component remembers its peak value (top) and how many nodes hold it (count). When two components merge and have the same peak, every pair of peak nodes, one from each side, is a new good path: count[a] * count[b].
If the peaks differ, the lower side can never end a good path with the higher side, so only the larger peak's count is kept. Every single node is also a good path, so paths starts at n.
Complexity: O(E log E) time, O(n) space
class DSU:
def __init__(self, n):
self.p = list(range(n))
self.sz = [1] * n
def find(self, x):
while self.p[x] != x:
self.p[x] = self.p[self.p[x]] # path compression
x = self.p[x]
return x
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False
if self.sz[a] < self.sz[b]:
a, b = b, a
self.p[b] = a
self.sz[a] += self.sz[b]
return True
class Solution:
def numberOfGoodPaths(self, vals: List[int], edges: List[List[int]]) -> int:
n = len(vals)
dsu = DSU(n)
top = vals[:] # top[root]: biggest value in the component
count = [1] * n # count[root]: how many nodes hold that value
edges.sort(key=lambda e: max(vals[e[0]], vals[e[1]])) # Kruskal: by edge weight
paths = n # every single node is a good path
for u, v in edges:
a, b = dsu.find(u), dsu.find(v)
if a == b:
continue
peak = max(top[a], top[b])
if top[a] == top[b]: # equal peaks: pair them up
paths += count[a] * count[b]
ways = (count[a] if top[a] == peak else 0) + (count[b] if top[b] == peak else 0)
dsu.union(a, b)
root = dsu.find(a)
top[root], count[root] = peak, ways
return pathsUse this when you can grow one region from a start node and must always take the cheapest (or best) node on its border, which a heap hands out.
First run a multi-source BFS from every thief to get dist, the distance of each cell to the nearest thief.
Then grow a region from (0, 0) Prim-style: the heap holds the border cells, and each step takes the safest one (max heap, so values are negated).
The path is only as safe as its least safe cell, so keep the running minimum of the popped values. When the bottom-right cell is popped, that minimum is the answer: while a safer border cell exists it is taken first.
Complexity: O(n^2 log n) time, O(n^2) space
import heapq
from collections import deque
class Solution:
def maximumSafenessFactor(self, grid: List[List[int]]) -> int:
n = len(grid)
dirs = ((1, 0), (-1, 0), (0, 1), (0, -1))
dist = [[-1] * n for _ in range(n)] # distance to the nearest thief
q = deque()
for r in range(n):
for c in range(n):
if grid[r][c] == 1:
dist[r][c] = 0
q.append((r, c))
while q: # multi-source BFS from all thieves
r, c = q.popleft()
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and dist[nr][nc] == -1:
dist[nr][nc] = dist[r][c] + 1
q.append((nr, nc))
heap = [(-dist[0][0], 0, 0)] # border of the grown region, safest first
seen = [[False] * n for _ in range(n)]
seen[0][0] = True
safest = dist[0][0]
while heap:
neg, r, c = heapq.heappop(heap) # Prim: best cell on the border
safest = min(safest, -neg) # a path is only as safe as its worst cell
if r == n - 1 and c == n - 1:
return safest
for dr, dc in dirs:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and not seen[nr][nc]:
seen[nr][nc] = True
heapq.heappush(heap, (-dist[nr][nc], nr, nc))
return 0The water level must be at least as high as every cell on the path, so the answer is the smallest possible highest cell on a route to the corner.
Grow a region from (0, 0) with a min heap of border cells keyed by elevation. Always pop the lowest border cell, as Prim picks the cheapest edge leaving the tree.
Keep time = max(time, elevation) over the popped cells. When the bottom-right cell is popped, time is the answer, because every lower border cell was already used.
Complexity: O(n^2 log n) time, O(n^2) space
import heapq
class Solution:
def swimInWater(self, grid: List[List[int]]) -> int:
n = len(grid)
heap = [(grid[0][0], 0, 0)] # border cells keyed by elevation
seen = {(0, 0)}
time = 0
while heap:
h, r, c = heapq.heappop(heap) # Prim: lowest cell on the border
time = max(time, h) # water must reach this cell's height
if r == n - 1 and c == n - 1:
return time
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and (nr, nc) not in seen:
seen.add((nr, nc))
heapq.heappush(heap, (grid[nr][nc], nr, nc))
return timeUse this when groups keep merging and you must ask "are these two in the same group?", especially with edges that arrive one at a time or in sorted order.
The input is a tree plus one extra edge. Add the edges one by one with union.
union returns False when both ends already share a leader, which means this edge would close a cycle. The first such edge is the answer.
The first failing union is the edge that completes the cycle, which is the one that appears last in the input among the cycle's edges, as the problem asks.
Complexity: O(n * alpha(n)) time, O(n) space
class DSU:
def __init__(self, n):
self.p = list(range(n))
self.sz = [1] * n
def find(self, x):
while self.p[x] != x:
self.p[x] = self.p[self.p[x]] # path compression
x = self.p[x]
return x
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False # already connected: this edge closes a cycle
if self.sz[a] < self.sz[b]:
a, b = b, a # union by size: attach the small tree under the big one
self.p[b] = a
self.sz[a] += self.sz[b]
return True
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
dsu = DSU(len(edges) + 1) # nodes are numbered 1..n
for u, v in edges:
if not dsu.union(u, v): # same group already: redundant edge
return [u, v]
return []Make each account a node. Two accounts that share an email belong to the same person, so union them.
owner remembers the first account that held each email. When an email is seen again, union the current account with that owner.
At the end, group the emails by the leader of their account, sort each group, and put the name in front.
Complexity: O(N * L * alpha(N) + E log E) time for N accounts, E emails; O(E) space
from collections import defaultdict
class DSU:
def __init__(self, n):
self.p = list(range(n))
self.sz = [1] * n
def find(self, x):
while self.p[x] != x:
self.p[x] = self.p[self.p[x]] # path compression
x = self.p[x]
return x
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False
if self.sz[a] < self.sz[b]:
a, b = b, a
self.p[b] = a
self.sz[a] += self.sz[b]
return True
class Solution:
def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
dsu = DSU(len(accounts))
owner = {} # email -> first account that had it
for i, acc in enumerate(accounts):
for email in acc[1:]:
if email in owner:
dsu.union(i, owner[email]) # shared email: same person
else:
owner[email] = i
groups = defaultdict(list)
for email, i in owner.items():
groups[dsu.find(i)].append(email) # group emails by the leader account
return [[accounts[root][0]] + sorted(emails) for root, emails in groups.items()]Sort the meetings by time and handle all meetings of the same minute together. Within a minute, union the two people of every meeting, so the secret spreads along any chain of meetings.
After that minute, a person who is not in person 0's group never actually got the secret, so undo their unions by resetting them to their own group (the reset step).
At the end, everyone whose leader is the same as person 0's leader knows the secret.
Complexity: O(M log M + M * alpha(n)) time, O(n) space
class DSU:
def __init__(self, n):
self.p = list(range(n))
self.sz = [1] * n
def find(self, x):
while self.p[x] != x:
self.p[x] = self.p[self.p[x]] # path compression
x = self.p[x]
return x
def union(self, a, b):
a, b = self.find(a), self.find(b)
if a == b:
return False
if self.sz[a] < self.sz[b]:
a, b = b, a
self.p[b] = a
self.sz[a] += self.sz[b]
return True
def reset(self, x):
self.p[x] = x # leave the group, become a singleton again
self.sz[x] = 1
class Solution:
def findAllPeople(self, n: int, meetings: List[List[int]], firstPerson: int) -> List[int]:
dsu = DSU(n)
dsu.union(0, firstPerson)
meetings.sort(key=lambda m: m[2]) # earliest meeting first
i = 0
while i < len(meetings):
j = i
while j < len(meetings) and meetings[j][2] == meetings[i][2]:
j += 1 # meetings[i:j] share the same minute
for x, y, _ in meetings[i:j]:
dsu.union(x, y)
for x, y, _ in meetings[i:j]:
if dsu.find(x) != dsu.find(0): # no secret reached them: undo their merge
dsu.reset(x)
dsu.reset(y)
i = j
return [p for p in range(n) if dsu.find(p) == dsu.find(0)]Make the locally best choice at each step and never look back. It only works when a local best leads to a global best.
Use this when you must pick as many non-conflicting intervals (activities with a start and an end) as possible.
Each pair is an activity that runs from left to right. Sort by the right end so the pair that finishes earliest comes first. Take a pair only when its left end is strictly greater than the right end of the last pair taken (strict, because the chain rule is b < c). The key line is `if start > last_end`. Finishing early leaves the most room for the pairs that come later.
Complexity: O(n log n) time, O(1) extra space
class Solution:
def findLongestChain(self, pairs: List[List[int]]) -> int:
pairs.sort(key=lambda p: p[1]) # earliest FINISH first
count, last_end = 0, float("-inf")
for start, end in pairs:
if start > last_end: # fits after the last pick
count += 1
last_end = end
return countA balloon is an interval and an arrow is a single point. Sort by the right end and shoot the first arrow at the right end of the first balloon, because that point hits the most balloons that overlap it. A balloon whose start is at or before the last shot is already burst. Only a balloon with `start > last_shot` needs a new arrow, so the arrows used equal the activities picked by earliest finish.
Complexity: O(n log n) time, O(1) extra space
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
points.sort(key=lambda p: p[1]) # earliest end first
arrows, last_shot = 0, float("-inf")
for start, end in points:
if start > last_shot: # not burst by the last arrow
arrows += 1
last_shot = end # shoot at this balloon's end
return arrowsFirst turn each letter into one interval. It starts at the first copy of the letter and must grow to cover the last copy of every letter inside it. If the scan meets a letter that first appears before `start`, this start is not self-contained and is dropped. After that it is plain activity selection: sort by end (shorter first when ends tie) and take each interval that starts after the last taken end. Earliest finish gives the most substrings, and the nested shorter ones give the smallest total length.
Complexity: O(26 * n) time, O(n) extra space for the answer
class Solution:
def maxNumOfSubstrings(self, s: str) -> List[str]:
first, last = {}, {}
for i, ch in enumerate(s):
first.setdefault(ch, i)
last[ch] = i
candidates = [] # (end, -start) of every valid interval
for ch in first:
start, end = first[ch], last[ch]
i, valid = start, True
while i <= end: # grow end to hold every copy of letters inside
if first[s[i]] < start: # needs a letter from before start
valid = False
break
end = max(end, last[s[i]])
i += 1
if valid:
candidates.append((end, -start))
candidates.sort() # earliest end first, shorter first on ties
result, last_end = [], -1
for end, neg_start in candidates:
if -neg_start > last_end: # activity selection: starts after the last pick
result.append(s[-neg_start:end + 1])
last_end = end
return resultUse this when you fill a limited capacity with items that have a value per unit of cost, so taking the best ratio first is safe.
Every box weighs 1, so the value-to-weight ratio of a box type is its units per box. Sort the box types by that ratio, best first. For each type take `min(boxes, truckSize)`, which is the "whole items, then a fraction of the last one" step. Stop when the truck is full.
Complexity: O(n log n) time, O(1) extra space
class Solution:
def maximumUnits(self, boxTypes: List[List[int]], truckSize: int) -> int:
boxTypes.sort(key=lambda b: b[1], reverse=True) # best units per box first
total = 0
for boxes, units in boxTypes:
take = min(boxes, truckSize) # all of this type, or what still fits
total += take * units
truckSize -= take
if truckSize == 0:
break
return totalEvery bar is worth 1, so its value per coin is 1 / cost and the best ratio is the cheapest bar. Sort the costs ascending and buy while the coins last. The bars cannot be split, but this is safe because all values are equal: a cheaper bar never blocks a better choice. The `break` fires at the first bar that does not fit, since every later bar costs at least as much.
Complexity: O(n log n) time, O(1) extra space
class Solution:
def maxIceCream(self, costs: List[int], coins: int) -> int:
costs.sort() # best ratio (cheapest) first
bars = 0
for cost in costs:
if cost > coins: # this and every later bar is too expensive
break
coins -= cost
bars += 1
return barsEveryone in the group is paid at the same wage-per-quality rate, and that rate must be the highest ratio in the group. So sort workers by ratio, cheapest first, and treat the current worker's ratio as the group rate. A max-heap keeps the k smallest qualities seen so far; when it grows past k, pop the largest quality. The cost for the group is `ratio * quality_sum`, and the smallest of these over all workers is the answer.
Complexity: O(n log n) time, O(n) extra space
from heapq import heappush, heappop
class Solution:
def mincostToHireWorkers(self, quality: List[int], wage: List[int], k: int) -> float:
workers = sorted(zip(quality, wage), key=lambda w: w[1] / w[0]) # lowest ratio first
heap = [] # max-heap of qualities (stored negated)
quality_sum = 0
best = float("inf")
for q, w in workers:
ratio = w / q # highest ratio in the group so far
heappush(heap, -q)
quality_sum += q
if len(heap) > k:
quality_sum += heappop(heap) # drop the largest quality (popped value is negative)
if len(heap) == k:
best = min(best, ratio * quality_sum)
return bestUse this when you must keep the most tasks that fit on one timeline, or count how many to remove so the rest do not clash.
Keeping the most intervals is activity selection: sort by end time and keep every interval whose start is at or after the last kept end (touching is allowed). Whatever you skip has to be removed. The answer is the dual, `len(intervals) - kept`. Sorting by end is the safe key because it leaves the most room for later intervals.
Complexity: O(n log n) time, O(1) extra space
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key=lambda x: x[1]) # sort by end time
kept, last_end = 0, float("-inf")
for start, end in intervals:
if start >= last_end: # starts after the last kept one ends
kept += 1
last_end = end
return len(intervals) - kept # everything else must goYou can attend one event per day, on any day inside its range. Walk the days in order, and push the end day of every event that has started onto a min heap. Drop the ends that are already in the past, then attend the event that ends soonest with `heappop`. Earliest deadline first is the interval scheduling rule applied day by day. When nothing is open, jump straight to the next start day.
Complexity: O(n log n) time, O(n) extra space
from heapq import heappush, heappop
class Solution:
def maxEvents(self, events: List[List[int]]) -> int:
events.sort() # by start day
open_ends = [] # min-heap of end days of started events
i, day, attended = 0, 0, 0
while i < len(events) or open_ends:
if not open_ends:
day = events[i][0] # idle: jump to the next start
while i < len(events) and events[i][0] <= day:
heappush(open_ends, events[i][1])
i += 1
while open_ends and open_ends[0] < day:
heappop(open_ends) # expired, can no longer be attended
if open_ends:
heappop(open_ends) # attend the one that ends soonest
attended += 1
day += 1
return attendedEach course is a task with a length and a deadline. Sort by deadline and add the courses one at a time, tracking the total time used. If the total passes the current deadline, remove the longest course taken so far with a max-heap. Removing the longest frees the most time and keeps the count as high as before. The number of courses left in the heap is the answer.
Complexity: O(n log n) time, O(n) extra space
from heapq import heappush, heappop
class Solution:
def scheduleCourse(self, courses: List[List[int]]) -> int:
courses.sort(key=lambda c: c[1]) # earliest deadline first
taken = [] # max-heap of durations (stored negated)
time = 0
for duration, last_day in courses:
heappush(taken, -duration)
time += duration
if time > last_day: # too late: drop the longest course taken
time += heappop(taken) # popped value is negative
return len(taken)Use this when intervals may overlap and you need to combine them, measure the covered length, or drop the ones hidden inside another.
Each attack poisons the half-open interval [t, t + duration). The attack times are already sorted by start, so no sort step is needed. Compare each new interval with the end of the last one, `last_end`. Only the part after `last_end` is new poison time, so add `end - max(start, last_end)`. That is the overlap check of Merge Intervals, used to measure the union instead of building it.
Complexity: O(n) time, O(1) extra space
class Solution:
def findPoisonedDuration(self, timeSeries: List[int], duration: int) -> int:
total = 0
last_end = 0 # end of the last interval seen
for t in timeSeries: # already sorted by start
start, end = t, t + duration
total += end - max(start, last_end) # count only the part not already covered
last_end = end
return totalSort by start, and on equal starts put the longer interval first. Now an interval can only be covered by one that came earlier. Keep `max_end`, the end of the last interval kept. If the current end is not past `max_end`, the interval is inside an earlier one and is skipped. Otherwise it is kept and becomes the new last interval.
Complexity: O(n log n) time, O(1) extra space
class Solution:
def removeCoveredIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key=lambda x: (x[0], -x[1])) # start ascending, longer first on ties
kept = 0
max_end = 0 # end of the last interval kept
for start, end in intervals:
if end > max_end: # sticks out past every earlier interval
kept += 1
max_end = end
return keptKeep the tracked ranges as sorted, disjoint, half-open intervals in two parallel lists, `starts` and `ends`. `addRange` uses `bisect` to find every interval that overlaps or touches the new one, then merges them all into a single interval. That is the merge step of Merge Intervals. `removeRange` finds the overlapped intervals and keeps only their left and right leftovers. `queryRange` checks the one interval that starts at or before `left`.
Complexity: O(log n) search plus O(n) list splice per call
from bisect import bisect_left, bisect_right
class RangeModule:
def __init__(self):
self.starts = [] # sorted, disjoint half-open intervals [start, end)
self.ends = []
def addRange(self, left: int, right: int) -> None:
i = bisect_left(self.ends, left) # first interval ending at or after left
j = bisect_right(self.starts, right) # intervals i..j-1 overlap or touch [left, right)
if i < j: # merge them with the new range
left = min(left, self.starts[i])
right = max(right, self.ends[j - 1])
self.starts[i:j] = [left]
self.ends[i:j] = [right]
def queryRange(self, left: int, right: int) -> bool:
i = bisect_right(self.starts, left) - 1 # last interval starting at or before left
return i >= 0 and self.ends[i] >= right
def removeRange(self, left: int, right: int) -> None:
i = bisect_right(self.ends, left) # first interval ending after left
j = bisect_left(self.starts, right) # intervals i..j-1 overlap [left, right)
new_starts, new_ends = [], []
if i < j:
if self.starts[i] < left: # keep the piece left of the removed range
new_starts.append(self.starts[i])
new_ends.append(left)
if self.ends[j - 1] > right: # keep the piece right of the removed range
new_starts.append(right)
new_ends.append(self.ends[j - 1])
self.starts[i:j] = new_starts
self.ends[i:j] = new_endsUse this when you need the most things happening at once, such as rooms, platforms, or seats required.
The minimum number of groups equals the maximum number of intervals that overlap at one moment. Sort the starts and the ends separately and sweep the starts. Before counting a new start, free every interval whose end is strictly before it (`ends[j] < t`, because touching ends still count as overlapping here). The largest running count is the answer.
Complexity: O(n log n) time, O(n) extra space
class Solution:
def minGroups(self, intervals: List[List[int]]) -> int:
starts = sorted(iv[0] for iv in intervals) # arrivals
ends = sorted(iv[1] for iv in intervals) # departures
need = best = 0
j = 0
for t in starts:
while ends[j] < t: # that interval is over: its group is free
need -= 1
j += 1
need += 1 # this interval needs a group
best = max(best, need)
return bestTurn each trip into two events: people get on at the start and get off at the end. Sort all events by position. At the same stop, the negative change (getting off) sorts before the positive one, which handles a stop where one trip ends and another begins. Add each change to a running count and fail as soon as it goes over the capacity.
Complexity: O(n log n) time, O(n) extra space
class Solution:
def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
events = []
for people, start, end in trips:
events.append((start, people)) # people get on
events.append((end, -people)) # people get off
events.sort() # same stop: get-off (negative) comes first
onboard = 0
for _, change in events:
onboard += change # sweep: how many are on the car now
if onboard > capacity:
return False
return TrueThe answer after each booking is the maximum number of bookings that overlap at one time, which is the minimum-platforms quantity. Store +1 at each start and -1 at each end in a dictionary. Sweep the keys in sorted order with a running sum and keep the maximum. Bookings are half-open, so an end and a start at the same time cancel out in one key.
Complexity: O(n^2 log n) time over n calls, O(n) extra space
from collections import defaultdict
class MyCalendarThree:
def __init__(self):
self.delta = defaultdict(int) # +1 where a booking starts, -1 where it ends
def book(self, startTime: int, endTime: int) -> int:
self.delta[startTime] += 1
self.delta[endTime] -= 1
overlap = best = 0
for t in sorted(self.delta): # sweep the time points in order
overlap += self.delta[t]
best = max(best, overlap)
return bestUse this when each position tells you how far you can reach and you need to know if, or in how few steps, you can get to the end.
Keep one number, `far`, the furthest index reachable so far. Walk left to right. If the current index is beyond `far`, nothing could land here, so the end is unreachable. Otherwise update `far = max(far, i + x)`. You never need to remember which jump got you here.
Complexity: O(n) time, O(1) extra space
class Solution:
def canJump(self, nums: List[int]) -> bool:
far = 0 # furthest index reachable so far
for i, x in enumerate(nums):
if i > far: # cannot even reach index i
return False
far = max(far, i + x)
return TrueThink of the jumps as levels. `cur_end` is the edge of what the current number of jumps can reach, and `far` is the best reach with one more jump. Reaching `cur_end` forces a jump, so count it and move the edge to `far`. Choosing the landing spot that reaches furthest is the greedy step, and the input is guaranteed reachable.
Complexity: O(n) time, O(1) extra space
class Solution:
def jump(self, nums: List[int]) -> int:
jumps = 0
cur_end = 0 # edge reachable with `jumps` jumps
far = 0 # furthest reach with one more jump
for i in range(len(nums) - 1):
far = max(far, i + nums[i])
if i == cur_end: # forced to jump: take the best landing spot
jumps += 1
cur_end = far
return jumpsTurn each tap into a jump. For every left end, store the furthest right end any tap covering that left end can reach. The garden is the line 0..n, and covering it with the fewest taps is Jump Game II with `far` and `cur_end`. If `far` cannot move past the current position, there is a gap and the answer is -1.
Complexity: O(n) time, O(n) extra space
class Solution:
def minTaps(self, n: int, ranges: List[int]) -> int:
farthest = [0] * (n + 1) # farthest[l] = furthest right end of a tap starting at l
for i, r in enumerate(ranges):
left, right = max(0, i - r), min(n, i + r)
farthest[left] = max(farthest[left], right)
taps = cur_end = far = 0
for i in range(n):
far = max(far, farthest[i]) # best reach using taps that start at or before i
if i == cur_end: # the current taps end here: open one more
if far <= i: # nothing extends the covered area: gap
return -1
taps += 1
cur_end = far
return tapsUse this when a running total (tank, balance, energy) must never drop below the limit, and a failure tells you where to restart.
Track the running total and the lowest value it ever reaches, like watching the lowest point of a fuel tank. The starting value only has to lift that lowest point up to 1. So the answer is `1 - lowest`, and `lowest` starts at 0 so the answer is never below 1. It is one pass with no search.
Complexity: O(n) time, O(1) extra space
class Solution:
def minStartValue(self, nums: List[int]) -> int:
total = lowest = 0 # running total and its lowest point
for x in nums:
total += x
lowest = min(lowest, total)
return 1 - lowest # start high enough that every total stays >= 1First check `sum(gas) >= sum(cost)`; if it fails no start works. Then drive once around with a running `tank`. When the tank goes negative at station i, no station between the old start and i can be the start either, so move `start` to i + 1 and reset the tank. The first check guarantees that the last candidate left standing is valid.
Complexity: O(n) time, O(1) extra space
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
if sum(gas) < sum(cost): # not enough fuel in total
return -1
start = tank = 0
for i in range(len(gas)):
tank += gas[i] - cost[i]
if tank < 0: # failed: no start in [start, i] works
start = i + 1
tank = 0
return startKeep `tank` as the furthest position you can reach with the fuel gathered so far. Walk the stations in order, with the target added as a last stop. If the next stop is out of reach, you should have refueled earlier at the passed station with the most fuel, so pop it from a max-heap and count a stop. If the heap runs empty before you can reach the stop, the trip is impossible.
Complexity: O(n log n) time, O(n) extra space
from heapq import heappush, heappop
class Solution:
def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -> int:
heap = [] # max-heap of fuel at stations already passed
tank = startFuel # furthest position reachable so far
stops = 0
for position, fuel in stations + [[target, 0]]:
while tank < position: # out of reach: refuel at the best passed station
if not heap:
return -1
tank -= heappop(heap) # popped value is negative
stops += 1
heappush(heap, -fuel)
return stopsUse this when jobs have a limit (deadline, difficulty, cooldown, or capital) and a profit, and you take the best-paying job that is allowed.
A worker may do any job up to their ability, so the limit is the difficulty and the goal is the highest profit inside it. Sort the jobs by difficulty and the workers by ability. For each worker, unlock every job they can do and keep the best profit seen so far in `best`. That best profit is added to the total, and the job pointer only moves forward.
Complexity: O(n log n + m log m) time, O(n) extra space
class Solution:
def maxProfitAssignment(self, difficulty: List[int], profit: List[int], worker: List[int]) -> int:
jobs = sorted(zip(difficulty, profit)) # easiest job first
best = i = total = 0
for ability in sorted(worker):
while i < len(jobs) and jobs[i][0] <= ability:
best = max(best, jobs[i][1]) # best profit among jobs allowed so far
i += 1
total += best
return totalJobs of one type must be at least n slots apart, so the busiest type decides the layout. Place it in `max_freq` rounds. The first `max_freq - 1` rounds each have n + 1 slots, and the last round only needs one slot for each type tied for busiest. Other tasks fill the idle slots, and if there are more tasks than slots the answer is the number of tasks.
Complexity: O(n) time, O(1) extra space (26 letters)
from collections import Counter
class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
counts = Counter(tasks)
max_freq = max(counts.values())
num_busiest = sum(1 for c in counts.values() if c == max_freq)
slots = (max_freq - 1) * (n + 1) + num_busiest # rounds of n+1 slots, busiest tasks first
return max(slots, len(tasks)) # no idle time needed if tasks fill every slotChoose up to k projects to maximise capital, where a project needs enough capital to start. Sort projects by the capital they need. Each round, push the profit of every project you can now afford onto a max-heap, then take the biggest profit with `heappop`. This is "highest profit first among the jobs that are allowed", and taking a profit can unlock new projects.
Complexity: O(n log n + k log n) time, O(n) extra space
from heapq import heappush, heappop
class Solution:
def findMaximizedCapital(self, k: int, w: int, profits: List[int], capital: List[int]) -> int:
projects = sorted(zip(capital, profits)) # cheapest requirement first
affordable = [] # max-heap of profits (stored negated)
i = 0
for _ in range(k):
while i < len(projects) and projects[i][0] <= w:
heappush(affordable, -projects[i][1]) # unlock everything we can pay for
i += 1
if not affordable:
break # nothing can be started
w -= heappop(affordable) # take the highest profit
return wRecursion + remembering. If a problem has overlapping subproblems and optimal substructure, solve each subproblem once.
Use this when the answer for position i depends only on a few earlier positions, so one row of results is enough.
State: dp[i] is the number of 1 bits in the number i. Shifting i right by one drops its last bit, so i >> 1 is a smaller number whose answer is already filled in. The transition is dp[i] = dp[i >> 1] + (i & 1). The base case is dp[0] = 0.
Complexity: O(n) time, O(n) space
class Solution:
def countBits(self, n: int) -> List[int]:
dp = [0] * (n + 1) # dp[i] = number of 1 bits in i
for i in range(1, n + 1):
dp[i] = dp[i >> 1] + (i & 1) # i without its last bit, plus that last bit
return dpState: dp[d] is the cheapest way to cover every travel day up to day d. On a day you do not travel, nothing is owed, so dp[d] = dp[d - 1]. On a travel day, take the best of three choices: a 1-day pass after dp[d - 1], a 7-day pass after dp[d - 7], or a 30-day pass after dp[d - 30]. The array is indexed by calendar day, so each cell only looks back at fixed offsets.
Complexity: O(last day) time, O(last day) space
class Solution:
def mincostTickets(self, days: List[int], costs: List[int]) -> int:
travel = set(days)
last = days[-1]
dp = [0] * (last + 1) # dp[d] = min cost to cover all travel days up to day d
for d in range(1, last + 1):
if d not in travel:
dp[d] = dp[d - 1] # no travel today, nothing to pay
continue
dp[d] = min(dp[d - 1] + costs[0], # 1-day pass
dp[max(0, d - 7)] + costs[1], # 7-day pass
dp[max(0, d - 30)] + costs[2]) # 30-day pass
return dp[last]State: dp[i] is the length of the longest valid substring that ends exactly at index i. It is 0 unless s[i] is ')'. If s[i-1] is '(', we close a fresh pair: dp[i] = dp[i-2] + 2. If s[i-1] is ')', the block ending at i-1 has length dp[i-1], so the character right before that block is at j = i - dp[i-1] - 1. If s[j] is '(', it matches s[i], and dp[i] = dp[i-1] + 2 + dp[j-1] glues the blocks on both sides.
Complexity: O(n) time, O(n) space
class Solution:
def longestValidParentheses(self, s: str) -> int:
n = len(s)
dp = [0] * n # dp[i] = longest valid substring ending exactly at i
for i in range(1, n):
if s[i] != ')':
continue
if s[i - 1] == '(': # the "()" case
dp[i] = (dp[i - 2] if i >= 2 else 0) + 2
else: # the "))" case: find the '(' that matches s[i]
j = i - dp[i - 1] - 1
if j >= 0 and s[j] == '(':
dp[i] = dp[i - 1] + 2 + (dp[j - 1] if j >= 1 else 0)
return max(dp, default=0)Use this when you count the ways, or the cheapest way, to reach step n and every move comes from one or two steps back.
State: dp[i] is the number of ways to stand on step i. You arrive from step i-1 (one step) or step i-2 (two steps), so dp[i] = dp[i-1] + dp[i-2]. Only the last two values are needed, so two variables replace the array. The base cases are one way to be on step 0 and one way to be on step 1.
Complexity: O(n) time, O(1) extra space
class Solution:
def climbStairs(self, n: int) -> int:
prev2, prev1 = 1, 1 # ways to reach step 0 and step 1
for _ in range(2, n + 1):
prev2, prev1 = prev1, prev1 + prev2 # dp[i] = dp[i-1] + dp[i-2]
return prev1The same two-step shape, but now each cell stores a minimum instead of a count. dp[i] is the cheapest cost to stand on step i, where the top is index len(cost). You come from step i-1 and pay cost[i-1], or from step i-2 and pay cost[i-2]. Start values are 0 because you may begin on step 0 or step 1 for free.
Complexity: O(n) time, O(1) extra space
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
prev2 = prev1 = 0 # cost to stand on step 0 and step 1 (free start)
for i in range(2, len(cost) + 1):
# arrive from one step back (pay cost[i-1]) or two steps back (pay cost[i-2])
prev2, prev1 = prev1, min(prev1 + cost[i - 1], prev2 + cost[i - 2])
return prev1Think of the string as stairs: dp[i] is the number of ways to decode the first i characters. The last character alone is one move (valid if it is not '0'), and the last two characters together are a two-step move (valid if they read 10 to 26). So dp[i] adds dp[i-1] and dp[i-2] under those two conditions. Two variables hold the last two values.
Complexity: O(n) time, O(1) extra space
class Solution:
def numDecodings(self, s: str) -> int:
prev2 = 1 # dp[0]: one way to decode the empty prefix
prev1 = 0 if s[0] == '0' else 1 # dp[1]
for i in range(2, len(s) + 1):
ways = 0
if s[i - 1] != '0': # last character alone is a letter 1-9
ways += prev1
if 10 <= int(s[i - 2:i]) <= 26: # last two characters together are a letter 10-26
ways += prev2
prev2, prev1 = prev1, ways
return prev1Use this when you pick values from a line to maximise a total and no two neighbours may both be picked.
State: dp[i] is the best total using the first i houses. For house i you either skip it (keep dp[i-1]) or rob it (add its value to dp[i-2]). So dp[i] = max(dp[i-1], dp[i-2] + a[i]), which is the key line. Only two previous values are needed.
Complexity: O(n) time, O(1) extra space
class Solution:
def rob(self, nums: List[int]) -> int:
prev2 = prev1 = 0
for x in nums:
# skip this house (prev1) or rob it and add the best from two houses back
prev2, prev1 = prev1, max(prev1, prev2 + x)
return prev1The houses form a circle, so the first and last house are neighbours and cannot both be robbed. Split into two straight streets: one without the first house and one without the last. Run the plain house robber recurrence on each and take the larger answer. A single house is a special case.
Complexity: O(n) time, O(n) space for the two slices
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
def rob_line(a): # plain house robber on a straight street
prev2 = prev1 = 0
for x in a:
prev2, prev1 = prev1, max(prev1, prev2 + x)
return prev1
# the circle breaks into two lines: skip the first house, or skip the last
return max(rob_line(nums[1:]), rob_line(nums[:-1]))Taking a value x earns x times its count and forbids x-1 and x+1. Put the total earnings of each value v into earn[v]. Now the values sit on a line, and picking v forbids its neighbours v-1 and v+1, which is exactly house robber. Run the same two-variable recurrence over earn.
Complexity: O(n + max(nums)) time, O(max(nums)) space
class Solution:
def deleteAndEarn(self, nums: List[int]) -> int:
earn = [0] * (max(nums) + 1)
for x in nums:
earn[x] += x # earn[v] = total points from taking every copy of v
prev2 = prev1 = 0
for e in earn: # house robber over values: v-1 and v cannot both be taken
prev2, prev1 = prev1, max(prev1, prev2 + e)
return prev1Use this when each term is a fixed sum of the previous two or three terms, so only that many variables need to be kept.
State: fib(k) is the k-th Fibonacci number. The transition is fib(k) = fib(k-1) + fib(k-2) with base cases 0 and 1. This version is top-down: the plain recursion repeats the same calls, and @lru_cache remembers each answer so every k is computed once.
Complexity: O(n) time, O(n) space
from functools import lru_cache
class Solution:
def fib(self, n: int) -> int:
@lru_cache(None) # TOP-DOWN: remember each fib(k) after the first call
def go(k):
if k < 2:
return k # base cases: fib(0) = 0, fib(1) = 1
return go(k - 1) + go(k - 2)
return go(n)The same idea with three terms: T(i) = T(i-1) + T(i-2) + T(i-3). The base cases are T0 = 0, T1 = 1, T2 = 1. Keep the last three values in three variables and slide them forward, so no array is needed.
Complexity: O(n) time, O(1) extra space
class Solution:
def tribonacci(self, n: int) -> int:
if n < 3:
return 1 if n else 0
a, b, c = 0, 1, 1 # T(i-3), T(i-2), T(i-1)
for _ in range(3, n + 1):
a, b, c = b, c, a + b + c # new term is the sum of the previous three
return cState: dp[n] is the number of ways to tile a 2 x n board. Counting by how the right end is closed gives dp[n] = dp[n-1] + dp[n-2] + 2 * (dp[n-3] + ... + dp[0]). Subtracting the same formula for n-1 collapses it to dp[n] = 2 * dp[n-1] + dp[n-3], a Fibonacci-style recurrence. Take the result modulo 10^9 + 7.
Complexity: O(n) time, O(n) space
class Solution:
def numTilings(self, n: int) -> int:
MOD = 10 ** 9 + 7
dp = [1, 1, 2] # ways to tile widths 0, 1 and 2
for i in range(3, n + 1):
dp.append((2 * dp[i - 1] + dp[i - 3]) % MOD) # only earlier cells matter
return dp[n]Use this when every item can be taken at most once and you must stay inside one or more capacities while maximising or minimising a total.
Each string is an item that costs some zeros and some ones, and every item is worth 1. The capacity has two dimensions, so dp[a][b] is the most strings you can pick with at most a zeros and b ones. For each string, loop both capacities backwards so the same string is never counted twice. The key line is dp[a][b] = max(dp[a][b], dp[a - zeros][b - ones] + 1).
Complexity: O(len(strs) * m * n) time, O(m * n) space
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
dp = [[0] * (n + 1) for _ in range(m + 1)] # dp[a][b] = most strings using <= a zeros and <= b ones
for s in strs:
zeros = s.count('0')
ones = len(s) - zeros
for a in range(m, zeros - 1, -1): # BACKWARDS in both capacities: use each string once
for b in range(n, ones - 1, -1):
dp[a][b] = max(dp[a][b], dp[a - zeros][b - ones] + 1)
return dp[m][n]Hiring the paid painter for wall i paints that wall and gives the free painter time[i] units, so it clears time[i] + 1 walls in total for cost[i]. So each wall is an item with weight time[i] + 1 and price cost[i], and you need total weight at least n for the least price. dp[j] is the cheapest cost to get j walls painted, and an index below 0 is treated as 0. Loop j backwards so each paid painter is hired once.
Complexity: O(n^2) time, O(n) space
class Solution:
def paintWalls(self, cost: List[int], time: List[int]) -> int:
n = len(cost)
INF = float('inf')
dp = [0] + [INF] * n # dp[j] = min cost to get j walls painted
for c, t in zip(cost, time): # item: weight t + 1, price c
for j in range(n, 0, -1): # BACKWARDS: each paid painter used at most once
dp[j] = min(dp[j], dp[max(0, j - t - 1)] + c)
return dp[n]Each crime is an item used at most once, with members as its weight. The twist is that profit is also tracked, and any profit above minProfit counts the same, so profit is capped at minProfit. dp[g][p] is the number of schemes using exactly g members with capped profit p. For each crime, loop g downwards so the crime is used once, and add dp[g - members][p] into dp[g][min(minProfit, p + gain)].
Complexity: O(len(group) * n * minProfit) time, O(n * minProfit) space
class Solution:
def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
MOD = 10 ** 9 + 7
# dp[g][p] = schemes using exactly g members with profit capped at minProfit
dp = [[0] * (minProfit + 1) for _ in range(n + 1)]
dp[0][0] = 1 # the empty scheme
for members, gain in zip(group, profit):
for g in range(n, members - 1, -1): # BACKWARDS: each crime used once
for p in range(minProfit + 1):
q = min(minProfit, p + gain) # profit above minProfit is all the same
dp[g][q] = (dp[g][q] + dp[g - members][p]) % MOD
return sum(dp[g][minProfit] for g in range(n + 1)) % MODUse this when items can be reused any number of times and you want the fewest pieces, the best value, or the number of ways to fill an exact total.
The items are the perfect squares 1, 4, 9, ... and each can be used again and again. dp[a] is the fewest squares that sum to a. For each a, try every square that fits: dp[a] = min(dp[a], dp[a - square] + 1). Because dp[a - square] may already use the same square, reuse is allowed.
Complexity: O(n * sqrt(n)) time, O(n) space
import math
class Solution:
def numSquares(self, n: int) -> int:
squares = [i * i for i in range(1, math.isqrt(n) + 1)]
INF = float('inf')
dp = [0] + [INF] * n # dp[a] = fewest perfect squares that sum to a
for a in range(1, n + 1):
for sq in squares: # unbounded: any square may be used again
if sq > a:
break
dp[a] = min(dp[a], dp[a - sq] + 1)
return dp[n]This is rod cutting. dp[i] is the best product when i is split into at least two positive parts. Cut off a first piece of size j, then either stop (product j * (i - j)) or keep splitting the rest (product j * dp[i - j]). Since dp[i - j] may start with the same size j again, pieces are reusable, which is the unbounded shape.
Complexity: O(n^2) time, O(n) space
class Solution:
def integerBreak(self, n: int) -> int:
dp = [0] * (n + 1) # dp[i] = best product when i is split into 2 or more parts
for i in range(2, n + 1):
for j in range(1, i): # first piece has size j; it may repeat inside dp[i - j]
dp[i] = max(dp[i], j * (i - j), j * dp[i - j])
return dp[n]Each move appends a block of zero '0's or one '1's, and blocks can be used any number of times. dp[i] is the number of ways to build a string of length exactly i, so dp[i] = dp[i - zero] + dp[i - one] when those lengths are not negative. The base case is dp[0] = 1. The answer adds dp[i] for every length from low to high, modulo 10^9 + 7.
Complexity: O(high) time, O(high) space
class Solution:
def countGoodStrings(self, low: int, high: int, zero: int, one: int) -> int:
MOD = 10 ** 9 + 7
dp = [1] + [0] * high # dp[i] = ways to build a string of length exactly i
for i in range(1, high + 1):
if i >= zero:
dp[i] += dp[i - zero] # last block was zero '0's
if i >= one:
dp[i] += dp[i - one] # last block was one '1's
dp[i] %= MOD
return sum(dp[low:high + 1]) % MODUse this when the question is whether some chosen subset of the numbers adds up to an exact target.
Two equal halves need an even total, and then one half must sum to total / 2. So the question becomes: is there a subset with sum target? dp[s] is true when some subset sums to s. For each number, loop s downwards so the number is used once: dp[s] = dp[s] or dp[s - x].
Complexity: O(n * total) time, O(total) space
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total = sum(nums)
if total % 2:
return False # an odd total cannot split into two equal halves
target = total // 2
dp = [True] + [False] * target # dp[s] = some subset sums to s
for x in nums:
for s in range(target, x - 1, -1): # BACKWARDS: each number used once
dp[s] = dp[s] or dp[s - x]
return dp[target]Subset sum, but instead of true or false each cell remembers a count. dp[s] is the most numbers that add up to exactly s, and minus infinity means s cannot be made. For each number loop s downwards so it is used once: dp[s] = max(dp[s], dp[s - x] + 1). If the target is unreachable, return -1.
Complexity: O(n * target) time, O(target) space
class Solution:
def lengthOfLongestSubsequence(self, nums: List[int], target: int) -> int:
NEG = float('-inf')
dp = [0] + [NEG] * target # dp[s] = most numbers that add to s (NEG: impossible)
for x in nums:
for s in range(target, x - 1, -1): # BACKWARDS: each number used once
dp[s] = max(dp[s], dp[s - x] + 1)
return dp[target] if dp[target] > 0 else -1If a group of k numbers has the same average as the whole array, its sum must be k * total / n. So ask, for each size k up to n / 2, whether some subset of exactly k numbers has that sum. dp[k] is a bitmask where bit s is set if k numbers can sum to s. For each number, go through k backwards and set dp[k] |= dp[k - 1] << x. The other group then automatically has the same average.
Complexity: O(n^2) big-integer shifts of up to sum(nums) bits, O(n * sum) bits of space
class Solution:
def splitArraySameAverage(self, nums: List[int]) -> bool:
n, total = len(nums), sum(nums)
half = n // 2
dp = [0] * (half + 1) # dp[k]: bit s is set if some k numbers sum to s
dp[0] = 1
for x in nums:
for k in range(half, 0, -1): # BACKWARDS over k: each number used once
dp[k] |= dp[k - 1] << x
for k in range(1, half + 1):
# k numbers must sum to k * average, which has to be a whole number
if total * k % n == 0 and (dp[k] >> (total * k // n)) & 1:
return True
return FalseUse this when you split the numbers into two groups and care about their difference or about +/- sign choices, which turns into a subset-sum target.
Smashing stones ends with one stone whose weight is the difference of two groups: some stones count as plus and the rest as minus. So split the stones into two groups with the smallest possible difference. Find the heaviest group that does not exceed total / 2 using 0/1 subset sum; the answer is total - 2 * that weight.
Complexity: O(n * total) time, O(total) space
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
total = sum(stones)
half = total // 2
dp = [True] + [False] * half # dp[s] = some group of stones weighs exactly s
for x in stones:
for s in range(half, x - 1, -1): # BACKWARDS: each stone goes in one group once
dp[s] = dp[s] or dp[s - x]
best = max(s for s in range(half + 1) if dp[s]) # heaviest group not above half
return total - 2 * best # the other group minus this oneGiving each number a + or - splits the array into a plus group P and a minus group N. Then P - N = target and P + N = total, so P = (total + target) / 2. The problem becomes: count the subsets whose sum is P. dp[s] is the number of subsets with sum s, filled with s going downwards so each number is used once.
Complexity: O(n * total) time, O(total) space
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
if abs(target) > total or (total + target) % 2:
return 0
need = (total + target) // 2 # P - N = target and P + N = total, so P = (total + target) / 2
dp = [1] + [0] * need # dp[s] = number of subsets with sum s
for x in nums:
for s in range(need, x - 1, -1): # BACKWARDS: each number used once
dp[s] += dp[s - x]
return dp[need]Two supports of equal height means splitting some rods into two groups with equal sum. Track states by the difference d between the two supports, and store the height of the shorter one. For each rod you may skip it, weld it to the taller support (d grows by r), or weld it to the shorter one (the new difference is |d - r|, and the shorter side gains min(d, r)). The answer is the stored height at difference 0.
Complexity: O(n * S) time, O(S) space, where S is the sum of the rods
class Solution:
def tallestBillboard(self, rods: List[int]) -> int:
best = {0: 0} # best[d] = tallest "shorter support" when the supports differ by d
for r in rods:
nxt = dict(best) # option 1: skip this rod
for d, shorter in best.items():
nxt[d + r] = max(nxt.get(d + r, 0), shorter) # weld to the taller support
nd = abs(d - r)
nxt[nd] = max(nxt.get(nd, 0), shorter + min(d, r)) # weld to the shorter support
best = nxt
return best[0]Use this when you build an amount from reusable coins and need the fewest coins, the number of combinations, or the number of orderings.
State: dp[a] is the fewest coins that make amount a, with infinity meaning impossible. Try every coin as the last coin: dp[a] = min(dp[a], dp[a - c] + 1). The base case is dp[0] = 0. If dp[amount] is still infinity, return -1.
Complexity: O(amount * len(coins)) time, O(amount) space
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
INF = float("inf")
dp = [0] + [INF] * amount # dp[a] = fewest coins that make amount a
for a in range(1, amount + 1):
for c in coins:
if c <= a:
dp[a] = min(dp[a], dp[a - c] + 1) # use coin c as the last coin
return dp[amount] if dp[amount] != INF else -1Now count the combinations, where 1+2 and 2+1 are the same. dp[a] is the number of combinations that make amount a, with dp[0] = 1. Put the coins in the OUTER loop and the amount in the inner loop, going forwards so a coin can repeat. Each coin is added to the table once, in a fixed order, which stops different orderings from being counted.
Complexity: O(amount * len(coins)) time, O(amount) space
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp = [1] + [0] * amount # dp[a] = number of combinations that make amount a
for c in coins: # coins in the OUTER loop: 1+2 and 2+1 count once
for a in range(c, amount + 1): # forwards, so a coin may be reused
dp[a] += dp[a - c]
return dp[amount]This counts orderings, so 1+2 and 2+1 are different. dp[t] is the number of ordered sequences that sum to t, with dp[0] = 1. Put the amount in the OUTER loop and the numbers in the inner loop. For every t, every number may be the last one, so each order is counted separately: dp[t] += dp[t - x].
Complexity: O(target * len(nums)) time, O(target) space
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp = [1] + [0] * target # dp[t] = number of ordered sequences that sum to t
for t in range(1, target + 1): # amount in the OUTER loop: 1+2 and 2+1 count separately
for x in nums:
if x <= t:
dp[t] += dp[t - x] # x is the last number of the sequence
return dp[target]Use this when you move through a grid (usually right and down) and each cell's answer comes from its top and left neighbours.
State: dp[r][c] is the number of ways to reach cell (r, c). You can only arrive from above or from the left, so dp[r][c] = dp[r-1][c] + dp[r][c-1]. The first row and first column have exactly one way each, so the table starts filled with 1s.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1] * n for _ in range(m)] # first row and first column: exactly one way
for r in range(1, m):
for c in range(1, n):
dp[r][c] = dp[r - 1][c] + dp[r][c - 1] # arrive from above or from the left
return dp[-1][-1]Same two-neighbour idea, but each cell now stores a minimum cost. The cheapest path to a cell is its own value plus the smaller of the cheapest path to the cell above and to the cell on the left. The grid is overwritten in place, and cells outside the grid count as infinity so the first row and column work too.
Complexity: O(m * n) time, O(1) extra space
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
R, C = len(grid), len(grid[0])
for r in range(R):
for c in range(C):
if r == c == 0:
continue
up = grid[r - 1][c] if r else float("inf")
left = grid[r][c - 1] if c else float("inf")
grid[r][c] += min(up, left) # best way in, plus this cell's cost
return grid[-1][-1]Filling from the top-left fails, because the health needed now depends on what happens later. So fill backwards from the princess. dp[r][c] is the minimum health needed when entering cell (r, c). It equals the smaller of the two next cells' needs minus this cell's value, and never less than 1. The extra border row and column of infinity keep the edges simple.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def calculateMinimumHP(self, dungeon: List[List[int]]) -> int:
R, C = len(dungeon), len(dungeon[0])
INF = float("inf")
# dp[r][c] = minimum health needed on entering (r, c)
dp = [[INF] * (C + 1) for _ in range(R + 1)]
dp[R][C - 1] = dp[R - 1][C] = 1 # after the princess's room we need at least 1 hp
for r in range(R - 1, -1, -1): # fill BACKWARDS from the princess
for c in range(C - 1, -1, -1):
need = min(dp[r + 1][c], dp[r][c + 1]) - dungeon[r][c]
dp[r][c] = max(1, need) # health must never drop below 1
return dp[0][0]Use this when two strings or arrays are compared and the answer is built from the best match between their prefixes.
State: dp[i][j] is the LCS length of the first i characters of A and the first j characters of B. If the last characters match, extend the diagonal: dp[i-1][j-1] + 1. If they differ, drop one character from either string and take the better result: max(dp[i-1][j], dp[i][j-1]). The extra row and column of zeros are the base case.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)] # dp[i][j] = LCS of text1[:i] and text2[:j]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1 # match: extend the diagonal
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) # mismatch: drop one character
return dp[m][n]This is the substring version: the shared part must be contiguous. dp[i][j] is the length of the common run that ends at nums1[i-1] and nums2[j-1]. On a match extend the diagonal, dp[i-1][j-1] + 1. On a mismatch the run is broken, so the cell stays 0 instead of taking a max like LCS. Keep the largest cell seen.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
m, n = len(nums1), len(nums2)
dp = [[0] * (n + 1) for _ in range(m + 1)] # dp[i][j] = common run ending at nums1[i-1], nums2[j-1]
best = 0
for i in range(1, m + 1):
for j in range(1, n + 1):
if nums1[i - 1] == nums2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1 # match: extend the diagonal
best = max(best, dp[i][j])
# mismatch: the run is broken, the cell stays 0 (unlike LCS)
return bestThe shortest string that contains both inputs as subsequences writes each shared character once, so its length is len(A) + len(B) - LCS. Build the LCS table first. Then walk back from dp[m][n]: on a match write that character once and move diagonally; otherwise write the character from the side with the larger table value and move that way. Leftover characters are copied at the end.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def shortestCommonSupersequence(self, str1: str, str2: str) -> str:
m, n = len(str1), len(str2)
dp = [[0] * (n + 1) for _ in range(m + 1)] # the LCS table of the two strings
for i in range(1, m + 1):
for j in range(1, n + 1):
if str1[i - 1] == str2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
i, j, out = m, n, []
while i > 0 and j > 0: # walk back through the table, building the answer reversed
if str1[i - 1] == str2[j - 1]:
out.append(str1[i - 1]) # shared character is written once
i -= 1
j -= 1
elif dp[i - 1][j] >= dp[i][j - 1]:
out.append(str1[i - 1])
i -= 1
else:
out.append(str2[j - 1])
j -= 1
out.extend(reversed(str1[:i])) # one of these two leftovers is empty
out.extend(reversed(str2[:j]))
return ''.join(reversed(out))Use this when you need the longest strictly increasing subsequence, or a problem that reduces to sorting by one key and then increasing on another.
Keep an array tails where tails[k] is the smallest possible last value of an increasing subsequence of length k+1. For each number x, binary search for the first tail that is not smaller than x. If there is none, x extends the longest subsequence, so append it. Otherwise x replaces that tail, which keeps the tails as small as possible. The length of tails is the answer, but tails itself is not the real subsequence.
Complexity: O(n log n) time, O(n) space
from bisect import bisect_left
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
tails = [] # tails[k] = smallest tail of an increasing subsequence of length k+1
for x in nums:
i = bisect_left(tails, x) # first tail >= x (bisect_right would allow equal values)
if i == len(tails):
tails.append(x) # x extends the longest subsequence
else:
tails[i] = x # x gives a smaller tail for length i+1
return len(tails)Counting needs more than the tails array, so use the O(n^2) form. length[i] is the LIS length ending at i, and count[i] is how many such subsequences end at i. For each earlier j with a smaller value: a longer chain replaces length[i] and copies count[j]; an equal-length chain adds count[j]. At the end, add the counts of every index whose length equals the maximum.
Complexity: O(n^2) time, O(n) space
class Solution:
def findNumberOfLIS(self, nums: List[int]) -> int:
n = len(nums)
length = [1] * n # length[i] = LIS length ending at i
count = [1] * n # count[i] = number of LIS of that length ending at i
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
if length[j] + 1 > length[i]:
length[i] = length[j] + 1
count[i] = count[j] # a longer chain: restart the count
elif length[j] + 1 == length[i]:
count[i] += count[j] # another way to reach the same length
best = max(length)
return sum(c for l, c in zip(length, count) if l == best)An envelope fits inside another only if both width and height are strictly smaller. Sort by width ascending, so only heights still need checking, and now the task is the LIS of the heights. For equal widths sort heights descending, so two envelopes of the same width can never both be chosen. Then run the tails and binary search routine on the heights.
Complexity: O(n log n) time, O(n) space
from bisect import bisect_left
class Solution:
def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
envelopes.sort(key=lambda e: (e[0], -e[1])) # same width: taller first, so they cannot chain
tails = [] # LIS tails over the heights
for _, h in envelopes:
i = bisect_left(tails, h)
if i == len(tails):
tails.append(h)
else:
tails[i] = h
return len(tails)Use this when you turn one string into another with deletes, inserts or replaces and want the lowest cost.
This is edit distance with only deletions allowed. dp[i][j] is the fewest deletions to make the first i characters of word1 equal the first j characters of word2. If the last characters match, the cost is dp[i-1][j-1]. Otherwise delete the last character of one word: 1 + min(dp[i-1][j], dp[i][j-1]). The first row and column are the cost of deleting everything.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)] # dp[i][j] = deletions to make word1[:i] equal word2[:j]
for i in range(m + 1):
dp[i][0] = i # delete all i characters
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] # match: free
else:
dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1]) # delete from word1 or from word2
return dp[m][n]dp[i][j] is the fewest operations to turn the first i characters of word1 into the first j characters of word2. A match costs nothing, so dp[i][j] = dp[i-1][j-1]. On a mismatch take the best of three moves plus 1: delete (dp[i-1][j]), insert (dp[i][j-1]) or replace (dp[i-1][j-1]). Row 0 and column 0 are the cost of inserting or deleting everything.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i # delete all i characters
for j in range(n + 1):
dp[0][j] = j # insert all j characters
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = 1 + min(dp[i - 1][j], # delete
dp[i][j - 1], # insert
dp[i - 1][j - 1]) # replace
return dp[m][n]Same deletion table, but each deletion costs the ASCII value of the character, not 1. dp[i][j] is the cheapest deletion cost to make s1[:i] equal s2[:j]. A match costs dp[i-1][j-1]. Otherwise pay for deleting s1[i-1] or s2[j-1], whichever leaves the cheaper rest. The first row and column are the sums of the ASCII values of the prefixes.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def minimumDeleteSum(self, s1: str, s2: str) -> int:
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)] # dp[i][j] = cheapest deletions to make s1[:i] equal s2[:j]
for i in range(1, m + 1):
dp[i][0] = dp[i - 1][0] + ord(s1[i - 1]) # delete all of s1[:i]
for j in range(1, n + 1):
dp[0][j] = dp[0][j - 1] + ord(s2[j - 1]) # delete all of s2[:j]
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(dp[i - 1][j] + ord(s1[i - 1]), # delete from s1
dp[i][j - 1] + ord(s2[j - 1])) # delete from s2
return dp[m][n]Use this when you work with palindromes inside a string and the answer for s[i..j] depends on the shorter interval s[i+1..j-1].
State: dp[i][j] is true when s[i..j] is a palindrome. It holds if s[i] == s[j] and the inside s[i+1..j-1] is a palindrome (or is shorter than 2 characters). Fill the table by increasing interval length, so the shorter inside is always ready. Remember the start and length of the longest true interval.
Complexity: O(n^2) time, O(n^2) space
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[False] * n for _ in range(n)] # dp[i][j] = s[i..j] is a palindrome
start, best = 0, 1
for length in range(1, n + 1): # interval DP: shorter intervals first
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j] and (length <= 2 or dp[i + 1][j - 1]):
dp[i][j] = True # equal ends around a palindrome
if length > best:
start, best = i, length
return s[start:start + best]Characters may now be skipped. dp[i][j] is the longest palindromic subsequence inside s[i..j]. If the two ends match, wrap them around the inside answer: dp[i+1][j-1] + 2. If not, drop one end and take the better: max(dp[i+1][j], dp[i][j-1]). Fill by increasing interval length, starting from the single characters (answer 1).
Complexity: O(n^2) time, O(n^2) space
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
dp = [[0] * n for _ in range(n)] # dp[i][j] = longest palindromic subsequence in s[i..j]
for i in range(n):
dp[i][i] = 1 # one character is a palindrome
for length in range(2, n + 1): # interval DP: shorter pieces are ready first
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2 # ends match: wrap the inside
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) # drop one end
return dp[0][n - 1]Two tables work together. First fill pal[i][j] by interval length, as before. Then cuts[j] is the fewest cuts for the prefix s[0..j]: if the whole prefix is a palindrome it is 0, otherwise try every last piece s[i..j] that is a palindrome and take cuts[i-1] + 1. The palindrome table makes each piece check a single lookup.
Complexity: O(n^2) time, O(n^2) space
class Solution:
def minCut(self, s: str) -> int:
n = len(s)
pal = [[False] * n for _ in range(n)] # pal[i][j] = s[i..j] is a palindrome (interval DP)
for length in range(1, n + 1):
for i in range(n - length + 1):
j = i + length - 1
pal[i][j] = s[i] == s[j] and (length <= 2 or pal[i + 1][j - 1])
cuts = [0] * n # cuts[j] = fewest cuts for the prefix s[0..j]
for j in range(n):
if pal[0][j]:
continue # the whole prefix is a palindrome: no cut
cuts[j] = min(cuts[i - 1] + 1 for i in range(1, j + 1) if pal[i][j]) # last piece is s[i..j]
return cuts[-1]Use this when a tree answer depends on a choice at each node, so every node returns a small tuple of states to its parent.
A postorder DFS returns the height of each subtree, so the parent can build on its children's answers. At every node, the longest path that bends there is left height + right height, so update the best answer with that. Then return 1 + max(left, right) to the parent. Each node is visited once.
Complexity: O(n) time, O(h) space for the recursion
class Solution:
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
best = 0
def depth(node): # postorder: returns the height of this subtree
nonlocal best
if not node:
return 0
left, right = depth(node.left), depth(node.right)
best = max(best, left + right) # longest path that bends at this node
return 1 + max(left, right)
depth(root)
return bestEach node returns two numbers: the best total if its house is robbed (take) and the best total if it is skipped (skip). If this node is robbed, both children must be skipped, so take = val + left skip + right skip. If it is skipped, each child is free to choose its better option, so skip = max(left) + max(right). The answer is the larger of the two values at the root.
Complexity: O(n) time, O(h) space
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
def go(node): # postorder: returns (take, skip)
if not node:
return (0, 0)
lt, ls = go(node.left)
rt, rs = go(node.right)
take = node.val + ls + rs # rob this house: the children must be skipped
skip = max(lt, ls) + max(rt, rs) # skip it: each child chooses freely
return (take, skip)
return max(go(root))Each node returns three costs for its subtree, where the whole subtree below the node is always covered. a is the cost if this node has a camera, b is the cost if it has no camera but a child covers it, and c is the cost if it has no camera and is not covered yet, so the parent must cover it. A missing child is (infinity, 0, 0). The combinations are: a = 1 + best of each child, b = at least one child has a camera, c = both children are in state b. The answer is min(a, b) at the root.
Complexity: O(n) time, O(h) space
class Solution:
def minCameraCover(self, root: Optional[TreeNode]) -> int:
INF = float('inf')
def go(node): # postorder: returns (camera here, covered by a child, not covered)
if not node:
return (INF, 0, 0)
la, lb, lc = go(node.left)
ra, rb, rc = go(node.right)
a = 1 + min(la, lb, lc) + min(ra, rb, rc) # camera here: children can be in any state
b = min(la + ra, la + rb, lb + ra) # no camera here: a child must have one
c = lb + rb # not covered: children covered without a camera
return (a, b, c)
a, b, _ = go(root)
return min(a, b)Use this when you match or split a string against a dictionary or a pattern and dp[i] describes its first i characters.
State: dp[i] is true when the first i characters can be split into dictionary words. The last word is s[j:i] for some j, so dp[i] is true if dp[j] is true and s[j:i] is in the set. Only lengths up to the longest word need checking. The base case is dp[0] = True for the empty prefix.
Complexity: O(n * L) substring checks, where L is the longest word length; O(n) space
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
words = set(wordDict)
max_len = max(map(len, wordDict))
dp = [True] + [False] * len(s) # dp[i] = the first i characters can be split into words
for i in range(1, len(s) + 1):
for j in range(max(0, i - max_len), i): # the last word is s[j:i]
if dp[j] and s[j:i] in words:
dp[i] = True
break
return dp[len(s)]State: dp[i][j] is true when s3[:i+j] is an interleaving of s1[:i] and s2[:j]. The last character of s3[:i+j] must come from s1 or from s2. It comes from s1 if dp[i-1][j] is true and s1[i-1] equals that character; similarly for s2 with dp[i][j-1]. If the lengths do not add up, the answer is false at once.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
m, n = len(s1), len(s2)
if m + n != len(s3):
return False
dp = [[False] * (n + 1) for _ in range(m + 1)] # dp[i][j] = s3[:i+j] interleaves s1[:i] and s2[:j]
dp[0][0] = True
for i in range(m + 1):
for j in range(n + 1):
if i > 0 and dp[i - 1][j] and s1[i - 1] == s3[i + j - 1]:
dp[i][j] = True # the last character of s3 came from s1
if j > 0 and dp[i][j - 1] and s2[j - 1] == s3[i + j - 1]:
dp[i][j] = True # the last character of s3 came from s2
return dp[m][n]State: dp[i][j] is true when s[:i] matches p[:j]. A normal character or '.' matches one character, so dp[i][j] = dp[i-1][j-1] if they agree. For 'x*', either use zero copies, dp[i][j-2], or one more copy when x matches s[i-1], dp[i-1][j]. Row 0 handles patterns like a*b* that match the empty string.
Complexity: O(m * n) time, O(m * n) space
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)] # dp[i][j] = s[:i] matches p[:j]
dp[0][0] = True
for j in range(2, n + 1): # patterns like a*b*c* can match the empty string
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 2]
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j - 1] == '*':
dp[i][j] = dp[i][j - 2] # '*' means zero copies of the previous char
if p[j - 2] == '.' or p[j - 2] == s[i - 1]:
dp[i][j] = dp[i][j] or dp[i - 1][j] # or one more copy of it
elif p[j - 1] == '.' or p[j - 1] == s[i - 1]:
dp[i][j] = dp[i - 1][j - 1] # single character matches
return dp[m][n]A tree where each edge is a character, so words sharing a prefix share a path. Prefix questions cost O(length of word), not O(number of words).
Use this when many words must be stored so that words with the same start share one path, and you want to keep extra facts (an end flag, a count) on the nodes while you add them.
Insert every string into one trie, so the start shared by all strings is one path from the root.
Walk down while the node has exactly one child and no word ends at it. Each step adds one letter to the answer.
The key line is `n = n.kids.setdefault(ch, Trie())` in insert: it reuses a path that exists or grows a new branch.
The walk stops when the path forks (the strings differ) or a word ends (the shortest string is used up).
Complexity: O(total characters) time, O(total characters) space
class Trie:
def __init__(self):
self.kids, self.end = {}, False
def insert(self, w):
n = self
for ch in w:
n = n.kids.setdefault(ch, Trie()) # reuse the shared path or grow a branch
n.end = True
class Solution:
def longestCommonPrefix(self, strs: List[str]) -> str:
root = Trie()
for w in strs:
root.insert(w)
prefix, n = [], root
while len(n.kids) == 1 and not n.end: # one way forward and no word stops here
ch, n = next(iter(n.kids.items()))
prefix.append(ch)
return "".join(prefix)Each node holds a `kids` dict and an `end` flag. insert walks the word one letter at a time and creates any missing child with setdefault.
The last node gets `end = True`, which marks "a word stops here".
search and startsWith both use the `_walk` helper that follows the letters and returns the final node, or None if a letter is missing.
search also needs `end` to be True, so "app" is not found after inserting only "apple". startsWith only needs the path to exist.
Complexity: O(L) time per operation, O(total characters) space
class Trie:
def __init__(self):
self.kids, self.end = {}, False
def insert(self, word: str) -> None:
n = self
for ch in word:
n = n.kids.setdefault(ch, Trie()) # one node per letter
n.end = True # a word stops at this node
def _walk(self, s):
n = self
for ch in s:
if ch not in n.kids:
return None
n = n.kids[ch]
return n
def search(self, word: str) -> bool:
n = self._walk(word)
return bool(n and n.end) # path must exist AND a word must end there
def startsWith(self, prefix: str) -> bool:
return self._walk(prefix) is not None # path alone is enoughGive each node a `count` instead of an end flag, and add 1 to it every time an insert passes through the node.
After all words are inserted, the count at a node is the number of words that start with the prefix spelled by that path.
The score of one word is the sum of the counts along its own path, one count for each of its prefixes.
The key line is `n.count += 1` inside the insert loop.
Complexity: O(total characters) time, O(total characters) space
class Trie:
def __init__(self):
self.kids, self.count = {}, 0
class Solution:
def sumPrefixScores(self, words: List[str]) -> List[int]:
root = Trie()
for w in words:
n = root
for ch in w:
n = n.kids.setdefault(ch, Trie())
n.count += 1 # one more word passes through this prefix
scores = []
for w in words:
n, total = root, 0
for ch in w:
n = n.kids[ch]
total += n.count # words that start with this prefix
scores.append(total)
return scoresUse this when you must know whether a whole word is stored, or which stored words end while you read a string, which means checking the end flag at each step.
Insert every word and keep a number at the node where it stops, so a word that appears twice counts twice.
Then walk `s` once from the root. Each time the walk lands on a node whose `end` is above 0, those words are prefixes of `s`.
Add `n.end` to the total at that point, and stop as soon as the next letter has no child.
Complexity: O(total characters of words + |s|) time, O(total characters of words) space
class Trie:
def __init__(self):
self.kids, self.end = {}, 0 # end = how many words stop at this node
class Solution:
def countPrefixes(self, words: List[str], s: str) -> int:
root = Trie()
for w in words:
n = root
for ch in w:
n = n.kids.setdefault(ch, Trie())
n.end += 1 # duplicate words each count
n, total = root, 0
for ch in s: # one walk along s
if ch not in n.kids:
break
n = n.kids[ch]
total += n.end # words ending here are prefixes of s
return totalInsert every root into a trie and set `end` on the last node of each root.
For each word of the sentence, walk down it and return the letters read so far the moment a node has `end` set. The first end flag on the way is the shortest root.
If a letter has no child, no root starts this word, so the word is kept as it is.
Complexity: O(characters in dictionary + characters in sentence) time, O(characters in dictionary) space
class Trie:
def __init__(self):
self.kids, self.end = {}, False
class Solution:
def replaceWords(self, dictionary: List[str], sentence: str) -> str:
root = Trie()
for w in dictionary:
n = root
for ch in w:
n = n.kids.setdefault(ch, Trie())
n.end = True
def shortest_root(word):
n = root
for i, ch in enumerate(word):
if ch not in n.kids:
return word # no root starts this word
n = n.kids[ch]
if n.end:
return word[:i + 1] # first end flag = shortest root
return word
return " ".join(shortest_root(w) for w in sentence.split())A word matches when it is a suffix of the letters seen so far, so insert each word reversed.
On every query, add the letter to the stream and walk the trie from the newest letter backwards.
If the walk reaches a node with `end` set, some word is a suffix of the stream, so return True.
The walk is never longer than the longest word, because it follows trie edges and the trie is that deep.
Complexity: O(L) time per query where L is the longest word, O(total characters) space
class Trie:
def __init__(self):
self.kids, self.end = {}, False
class StreamChecker:
def __init__(self, words: List[str]):
self.root = Trie()
for w in words:
n = self.root
for ch in reversed(w): # insert each word backwards
n = n.kids.setdefault(ch, Trie())
n.end = True
self.stream = []
def query(self, letter: str) -> bool:
self.stream.append(letter)
n = self.root
for ch in reversed(self.stream): # newest letter first
n = n.kids.get(ch)
if n is None:
return False
if n.end: # a stored word ends the stream
return True
return FalseUse this when you need the count, the sum or the list of stored words that begin with a prefix: walk to the prefix node, then read or explore what sits below it.
Add 1 to a `count` on every node an insert passes through, so each node knows how many words go through it.
To answer a prefix question, walk to the prefix node and read its count. If a letter is missing on the way, the answer is 0.
Every word below that node starts with the prefix, so one read replaces checking each word.
Complexity: O(total characters) time, O(total characters) space
class Trie:
def __init__(self):
self.kids, self.count = {}, 0
class Solution:
def prefixCount(self, words: List[str], pref: str) -> int:
root = Trie()
for w in words:
n = root
for ch in w:
n = n.kids.setdefault(ch, Trie())
n.count += 1 # words passing through this node
n = root
for ch in pref: # walk to the prefix node
if ch not in n.kids:
return 0
n = n.kids[ch]
return n.countKeep a `total` on each node: the sum of the values of all keys whose path goes through that node.
insert adds `delta` along the key's path, where delta is the new value minus the old value of that key. This way inserting an existing key replaces its value instead of adding to it.
sum walks to the prefix node and returns its total, or 0 if the path breaks.
Complexity: O(L) time per operation, O(total characters) space
class Trie:
def __init__(self):
self.kids, self.total = {}, 0 # total = sum of values of keys below this node
class MapSum:
def __init__(self):
self.root = Trie()
self.values = {}
def insert(self, key: str, val: int) -> None:
delta = val - self.values.get(key, 0) # re-inserting a key replaces its old value
self.values[key] = val
n = self.root
for ch in key:
n = n.kids.setdefault(ch, Trie())
n.total += delta
def sum(self, prefix: str) -> int:
n = self.root
for ch in prefix: # walk to the prefix node
if ch not in n.kids:
return 0
n = n.kids[ch]
return n.totalInsert every product and store the product string on the node where it ends.
For each typed letter, move one step down the trie to the prefix node. Once the path breaks, every longer prefix has no suggestions.
Then run a DFS below the prefix node with children in alphabetical order and stop after 3 words. A node's own word comes before its children, which is dictionary order.
The DFS uses a stack, because a product can be 3000 letters long and deep recursion would fail.
Complexity: O(total characters) to build, then each prefix visits only nodes until 3 words are found
class Trie:
def __init__(self):
self.kids, self.word = {}, None # word is stored where a product ends
class Solution:
def suggestedProducts(self, products: List[str], searchWord: str) -> List[List[str]]:
root = Trie()
for p in products:
n = root
for ch in p:
n = n.kids.setdefault(ch, Trie())
n.word = p
def first_three(node): # DFS below the prefix node, alphabetical
found, stack = [], [node]
while stack and len(found) < 3:
cur = stack.pop()
if cur.word:
found.append(cur.word)
for ch in sorted(cur.kids, reverse=True): # push big letters first so small ones pop first
stack.append(cur.kids[ch])
return found
answer, n = [], root
for ch in searchWord:
n = n.kids.get(ch) if n is not None else None # once the path breaks it stays broken
answer.append(first_three(n) if n is not None else [])
return answerUse this when you only need to know whether some stored word begins with a prefix, so you walk the path and ignore the end flag.
Insert each word of the sentence and save, on every node, the position of the first word that reached it (`n.first`).
To ask "does some word start with searchWord", only the path matters: walk searchWord and return -1 if a letter has no child.
The position saved on the last node is the leftmost word that starts with searchWord.
Complexity: O(total characters) time, O(total characters) space
class Trie:
def __init__(self):
self.kids, self.first = {}, 0 # first = position of the first word through this node
class Solution:
def isPrefixOfWord(self, sentence: str, searchWord: str) -> int:
root = Trie()
for pos, w in enumerate(sentence.split(), start=1):
n = root
for ch in w:
n = n.kids.setdefault(ch, Trie())
if n.first == 0:
n.first = pos # remember the leftmost word to reach this prefix
n = root
for ch in searchWord: # starts-with: only the path matters
if ch not in n.kids:
return -1
n = n.kids[ch]
return n.firstTurn each number of arr1 into a string of digits and insert it, so every prefix of every number becomes a path in the trie.
For each number of arr2, walk its digits and stop when the next digit has no child. That is a starts-with check that failed.
The number of steps taken is the longest prefix it shares with some number of arr1. Keep the maximum over arr2.
Complexity: O((n + m) * D) time, O(n * D) space, where D is the number of digits
class Trie:
def __init__(self):
self.kids = {}
class Solution:
def longestCommonPrefix(self, arr1: List[int], arr2: List[int]) -> int:
root = Trie()
for x in arr1:
n = root
for ch in str(x):
n = n.kids.setdefault(ch, Trie()) # every prefix of x becomes a path
best = 0
for y in arr2:
n, depth = root, 0
for ch in str(y):
if ch not in n.kids: # starts-with fails here
break
n = n.kids[ch]
depth += 1
best = max(best, depth)
return bestA query asks for a prefix and a suffix at once. For every word and every suffix of it, insert the key `suffix + "{" + word`, so one path spells both parts.
A query walks `suff + "{" + pref`, which is a starts-with check on that key.
Each node saves the largest word index that passed through it. Words go in order, so a later index overwrites an earlier one.
The query returns the index saved on the last node, or -1 if the walk breaks.
Complexity: O(n * L^2) to build, O(L) per query, O(n * L^2) space
class WordFilter:
def __init__(self, words: List[str]):
self.root = {} # a node is a dict; "idx" holds the largest index through it
for idx, w in enumerate(words):
for i in range(len(w) + 1): # every suffix, including the empty one
key = w[i:] + "{" + w # suffix, separator, whole word
n = self.root
for ch in key:
n = n.setdefault(ch, {})
n["idx"] = idx # a later word overwrites an earlier one
def f(self, pref: str, suff: str) -> int:
n = self.root
for ch in suff + "{" + pref: # starts-with on the combined key
if ch not in n:
return -1
n = n[ch]
return n["idx"]Use this when a query holds wildcards or allows a wrong letter, so at those positions you branch over every child with DFS.
addWord is a normal insert. search calls a recursive helper `_dfs(word, i)` on the node reached so far.
A normal letter follows its single child, or fails if there is none. A "." tries every child and succeeds if any branch does, which is the DFS from the notes.
When all letters are used, return the node's end flag, so "b" is not found when only "bad" was added.
Complexity: add O(L); search O(L) with no dots, O(26^d * L) with d dots
class WordDictionary:
def __init__(self):
self.kids, self.end = {}, False
def addWord(self, word: str) -> None:
n = self
for ch in word:
n = n.kids.setdefault(ch, WordDictionary())
n.end = True
def search(self, word: str) -> bool:
return self._dfs(word, 0)
def _dfs(self, w, i):
if i == len(w):
return self.end # all letters used: must be a word end
ch = w[i]
if ch == ".": # wildcard: try every child
return any(c._dfs(w, i + 1) for c in self.kids.values())
return ch in self.kids and self.kids[ch]._dfs(w, i + 1)Treat the one allowed change as a wildcard that may be used once. buildDict is a normal insert.
`_dfs(w, i, changed)` goes down the trie. A child whose letter equals `w[i]` continues with the same flag.
A child with a different letter is taken only if no change was used yet, and then `changed` becomes True.
At the end of the word the node must be a word end and `changed` must be True, so an exact match does not count.
Complexity: O(26 * L^2) time per search worst case, O(total characters) space
class MagicDictionary:
def __init__(self):
self.kids, self.end = {}, False
def buildDict(self, dictionary: List[str]) -> None:
for w in dictionary:
n = self
for ch in w:
n = n.kids.setdefault(ch, MagicDictionary())
n.end = True
def search(self, searchWord: str) -> bool:
return self._dfs(searchWord, 0, False)
def _dfs(self, w, i, changed):
if i == len(w):
return self.end and changed # a word end, with exactly one change made
for ch, child in self.kids.items():
if ch == w[i]:
if child._dfs(w, i + 1, changed): # keep the letter
return True
elif not changed:
if child._dfs(w, i + 1, True): # spend the one change here
return True
return FalsePut all words into a trie. For each word, `can_build(start)` walks the trie along `w[start:]`.
Each time the walk passes a node with `end` set, it may cut the word there and try to build the rest. That is one branch per cut point, like the dot branching in a wildcard search.
A memo on `start` stops the branches from repeating work.
The check `whole` stops a word from counting as one piece of itself, so at least two words are needed.
Complexity: O(sum of L^2) time over all words, O(total characters) space
class Trie:
def __init__(self):
self.kids, self.end = {}, False
class Solution:
def findAllConcatenatedWordsInADict(self, words: List[str]) -> List[str]:
root = Trie()
for w in words:
n = root
for ch in w:
n = n.kids.setdefault(ch, Trie())
n.end = True
def can_build(w, start, memo):
if start == len(w):
return True
if start in memo:
return memo[start]
n, ok = root, False
for j in range(start, len(w)):
n = n.kids.get(w[j]) # walk the trie along the word
if n is None:
break
whole = start == 0 and j == len(w) - 1 # the word itself is not a piece
if n.end and not whole and can_build(w, j + 1, memo): # cut here, build the rest
ok = True
break
memo[start] = ok
return ok
return [w for w in words if can_build(w, 0, {})]Use this when you must find words inside a grid, so you walk the grid and the trie together and stop at once when the trie has no such branch.
Walk the grid and the word together. The index `i` says how many letters are matched, which plays the role of the depth in a trie.
From a cell equal to `word[i]`, try the four neighbours with `i + 1`. Mark the cell as used first and restore it on the way back.
One word is a trie with a single path, so no trie is needed here. The next question has many words and uses the same DFS with a real trie.
Complexity: O(R * C * 3^L) time, O(L) space for the recursion
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
rows, cols = len(board), len(board[0])
def dfs(r, c, i): # i = letters of word matched so far
if i == len(word):
return True
if not (0 <= r < rows and 0 <= c < cols) or board[r][c] != word[i]:
return False # no branch for this letter
saved, board[r][c] = board[r][c], "#" # mark the cell as used
found = (dfs(r + 1, c, i + 1) or dfs(r - 1, c, i + 1)
or dfs(r, c + 1, i + 1) or dfs(r, c - 1, i + 1))
board[r][c] = saved # undo the mark
return found
return any(dfs(r, c, 0) for r in range(rows) for c in range(cols))Build a trie of all the words and store each word on the node where it ends.
Run one grid DFS and step to a neighbour only if its letter is a child of the current trie node. Dead prefixes are never explored.
When a node holds a word, add it to the answer and clear it so it is reported once.
After exploring a node, delete it from its parent if it has no children left, which prunes later searches.
Complexity: O(R * C * 3^L) worst case, far less in practice because of the pruning
class Trie:
def __init__(self):
self.kids, self.word = {}, None # word is stored where a word ends
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
root = Trie()
for w in words:
n = root
for ch in w:
n = n.kids.setdefault(ch, Trie())
n.word = w
rows, cols, found = len(board), len(board[0]), []
def dfs(r, c, parent):
ch = board[r][c]
node = parent.kids[ch]
if node.word:
found.append(node.word)
node.word = None # report each word once
board[r][c] = "#" # mark the cell as used
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] in node.kids: # follow trie edges only
dfs(nr, nc, node)
board[r][c] = ch # undo the mark
if not node.kids:
del parent.kids[ch] # prune the dead branch
for r in range(rows):
for c in range(cols):
if board[r][c] in root.kids:
dfs(r, c, root)
return foundUse this when you must find the pair (or the best partner for a number) with the largest XOR, which a binary trie answers by taking the opposite bit at each level.
Two numbers x <= y are a strong pair when y <= 2 * x. Sort the array and slide a window: add each y to a binary trie, then remove numbers from the left while `2 * x < y`.
Each trie node keeps a count of the numbers passing through it, so `add(x, -1)` can remove a number again.
To find the best partner of y, walk its bits from the top and take the opposite-bit child whenever it exists with count above 0. That sets this bit of the XOR to 1.
The window always holds y itself, so the result is at least 0.
Complexity: O(n log n + n * B) time, O(n * B) space, where B is the bit length of the largest number
class Solution:
def maximumStrongPairXor(self, nums: List[int]) -> int:
nums.sort()
bits = max(nums).bit_length()
root = [None, None, 0] # node = [child for bit 0, child for bit 1, count]
def add(x, delta): # delta = +1 to insert, -1 to remove
n = root
for b in range(bits - 1, -1, -1):
bit = (x >> b) & 1
if n[bit] is None:
n[bit] = [None, None, 0]
n = n[bit]
n[2] += delta
def best_partner(x):
n, cur = root, 0
for b in range(bits - 1, -1, -1):
bit = (x >> b) & 1
want = n[bit ^ 1] # opposite bit, if a number still has it
if want is not None and want[2] > 0:
cur |= 1 << b
n = want
else:
n = n[bit]
return cur
best, left = 0, 0
for y in nums:
add(y, 1)
while nums[left] * 2 < y: # x is too small to pair with y
add(nums[left], -1)
left += 1
best = max(best, best_partner(y))
return bestInsert every number into a binary trie as a path of bits, from the top bit down to bit 0.
For each number, walk the trie from the top. `want = bit ^ 1` is the opposite bit. If that child exists, take it and set that bit of the result, because a higher bit is worth more than all lower bits together.
If it does not exist, follow the same bit and leave the result bit at 0. The best result over all numbers is the answer.
The trie is stored in two flat int arrays, `child[0]` and `child[1]`, where a node is just an index. Nested dicts would work too, but they use hundreds of MB on the largest inputs.
Complexity: O(n * B) time, O(n * B) space in compact int arrays, where B is the bit length of the largest number (at most 31)
from array import array
class Solution:
def findMaximumXOR(self, nums: List[int]) -> int:
bits = max(nums).bit_length()
child = (array('i', [0]), array('i', [0])) # child[bit][node] = id of that child, 0 = none
for x in nums: # insert as bit paths, top bit first
node = 0
for b in range(bits - 1, -1, -1):
bit = (x >> b) & 1
if not child[bit][node]:
child[0].append(0)
child[1].append(0)
child[bit][node] = len(child[0]) - 1
node = child[bit][node]
best = 0
for x in nums:
node, cur = 0, 0
for b in range(bits - 1, -1, -1):
bit = (x >> b) & 1
want = bit ^ 1 # opposite bit makes this XOR bit a 1
if child[want][node]:
cur |= 1 << b
node = child[want][node]
else:
node = child[bit][node]
best = max(best, cur)
return bestSort the queries by their limit, smallest first, and sort nums. Walk a pointer through nums and insert every number that is at most the current limit, so the trie holds only allowed numbers. This is the offline trick: no query ever sees a number it is not allowed to use.
Then answer the query exactly like Maximum XOR of Two Numbers: from the top bit down, prefer the child with the opposite bit.
If no number is allowed yet (the pointer is still at 0), the answer is -1.
The trie lives in two flat int arrays instead of nested dicts, which keeps memory small on 100,000 numbers.
Complexity: O((n + q) * 30 + n log n + q log q) time, O(n * 30) space in compact int arrays
from array import array
class Solution:
def maximizeXor(self, nums: List[int], queries: List[List[int]]) -> List[int]:
nums.sort()
order = sorted(range(len(queries)), key=lambda i: queries[i][1]) # smallest limit first
child = (array('i', [0]), array('i', [0])) # child[bit][node] = id of that child, 0 = none
answer, p = [-1] * len(queries), 0
for qi in order:
x, limit = queries[qi]
while p < len(nums) and nums[p] <= limit: # trie holds only numbers <= limit
node = 0
for b in range(29, -1, -1):
bit = (nums[p] >> b) & 1
if not child[bit][node]:
child[0].append(0)
child[1].append(0)
child[bit][node] = len(child[0]) - 1
node = child[bit][node]
p += 1
if p == 0:
continue # nothing allowed yet: answer stays -1
node, cur = 0, 0
for b in range(29, -1, -1):
bit = (x >> b) & 1
want = bit ^ 1 # opposite bit makes this XOR bit a 1
if child[want][node]:
cur |= 1 << b
node = child[want][node]
else:
node = child[bit][node]
answer[qi] = cur
return answerWork directly on the binary digits. Fast, O(1) space, and the source of many neat tricks.
Use this when the question compares or combines the binary digits of numbers: AND keeps shared 1s, OR keeps any 1, XOR keeps the bits that differ.
In an alternating number such as 10101, every bit differs from the bit above it. So n ^ (n >> 1) turns it into all 1s (11111). Any two equal neighbours leave a 0 below the top bit instead. A run of 1s plus one is a power of two, so x & (x + 1) == 0 tests for all 1s.
Complexity: O(1) time, O(1) extra space
class Solution:
def hasAlternatingBits(self, n: int) -> bool:
x = n ^ (n >> 1) # XOR with itself shifted: alternating bits become all 1s
return x & (x + 1) == 0 # AND with x + 1 is 0 only when x is 1...1A bit in the AND of a range is 0 as soon as it changes anywhere in the range. The bits that change are the ones below the point where left and right first differ. Shift both numbers right until they are equal; what is left is the shared prefix. Shifting it back with left << shift puts the zeros back in the low bits.
Complexity: O(log right) time, O(1) extra space
class Solution:
def rangeBitwiseAnd(self, left: int, right: int) -> int:
shift = 0
while left < right: # differing low bits would AND to 0, so drop them
left >>= 1
right >>= 1
shift += 1
return left << shift # shared prefix, low bits restored as zerosXOR adds two numbers bit by bit while ignoring carries. AND shows where both bits are 1, and shifting that left by one puts each carry in its column. Repeat until no carry is left. Python ints have no fixed width, so & mask keeps 32 bits, and the last line turns a pattern with the sign bit set back into a negative number.
Complexity: O(1) time (at most 32 rounds), O(1) extra space
class Solution:
def getSum(self, a: int, b: int) -> int:
mask = 0xFFFFFFFF # Python never overflows, so cut to 32 bits ourselves
while b:
carry = (a & b) << 1 # AND finds columns where both bits are 1; carry moves left
a = (a ^ b) & mask # XOR is the sum without carries
b = carry & mask
return a if a <= 0x7FFFFFFF else ~(a ^ mask) # sign bit set: rebuild the negative numberUse this when the next step depends on whether a number is odd or even: x & 1 tests it and x >> 1 halves it.
The last bit tells odd from even: num & 1. An odd number loses 1, which clears that last bit. An even number is halved with num >>= 1. Each pass is one step, so the loop runs at most about twice the number of bits.
Complexity: O(log num) time, O(1) extra space
class Solution:
def numberOfSteps(self, num: int) -> int:
steps = 0
while num:
if num & 1: # odd: last bit is 1, subtract 1
num -= 1
else: # even: shifting right halves it
num >>= 1
steps += 1
return stepsWrite the exponent in binary. Every 1 bit means one power of x (x, x squared, x to the 4th, ...) goes into the answer, and n & 1 tests the lowest bit. Squaring x and halving n with n >>= 1 moves to the next bit. This takes about log n multiplications instead of n. A negative exponent becomes 1 / x with a positive exponent.
Complexity: O(log n) time, O(1) extra space
class Solution:
def myPow(self, x: float, n: int) -> float:
if n < 0:
x, n = 1 / x, -n
result = 1.0
while n:
if n & 1: # odd exponent: this bit contributes the current x
result *= x
x *= x # next bit stands for x squared
n >>= 1 # halve the exponent
return resultAn even number is always halved. For an odd number, look at its last two bits with n & 3. If they are 01, subtract 1 so a 0 is left behind. If they are 11, add 1: the carry clears the whole run of trailing 1s at once. The number 3 is the exception, because 3 -> 2 -> 1 is shorter than 3 -> 4 -> 2 -> 1.
Complexity: O(log n) time, O(1) extra space
class Solution:
def integerReplacement(self, n: int) -> int:
steps = 0
while n > 1:
if n & 1 == 0: # even: halve
n >>= 1
elif n == 3 or n & 3 == 1: # odd ending in 01: subtract 1
n -= 1
else: # odd ending in 11: add 1 to carry through the 1s
n += 1
steps += 1
return stepsUse this when you must read, set, clear or flip single bits: build a mask with 1 << i, then use & to read, | to set, & ~ to clear and ^ to flip.
A mask of 1s exactly as wide as the number is (1 << num.bit_length()) - 1. XOR with that mask flips every bit of num and leaves the bits above it alone. For 5 (101) the mask is 111, and 101 ^ 111 = 010, which is 2.
Complexity: O(1) time, O(1) extra space
class Solution:
def findComplement(self, num: int) -> int:
mask = (1 << num.bit_length()) - 1 # 1s over every bit of num, e.g. 101 -> 111
return num ^ mask # XOR toggles each of those bitsRead bit i of n with (n >> i) & 1. Set the mirrored position 31 - i in the result with |=. After 32 rounds the bit order is reversed. Bits that are 0 need no work, because result starts at 0.
Complexity: O(1) time (32 rounds), O(1) extra space
class Solution:
def reverseBits(self, n: int) -> int:
result = 0
for i in range(32):
bit = (n >> i) & 1 # read bit i
result |= bit << (31 - i) # set the mirrored bit in the result
return resultHandle each bit position on its own. Read bit i of a, b and c with (x >> i) & 1. If c has a 1, at least one of a and b must have a 1, which costs one flip when both are 0. If c has a 0, both a and b must be 0, so every 1 among them costs one flip. Add up the flips for all 31 positions.
Complexity: O(1) time (31 positions), O(1) extra space
class Solution:
def minFlips(self, a: int, b: int, c: int) -> int:
flips = 0
for i in range(31):
bit_a = (a >> i) & 1 # read bit i of each number
bit_b = (b >> i) & 1
bit_c = (c >> i) & 1
if bit_c == 1:
if bit_a == 0 and bit_b == 0:
flips += 1 # set one of them
else:
flips += bit_a + bit_b # clear every 1
return flipsThe two moves can be undone, so going from n to 0 costs the same as going from 0 to n. The shortest route from 0 to the single top bit 100...0 (value 2^k) takes 2^(k+1) - 1 moves and passes through every number with that many bits, including n. The part of the route after n is the same problem for n with its top bit cleared, which is n ^ top. So the answer is 2 * top - 1 minus the answer for n ^ top.
Complexity: O(log n) time, O(log n) stack space
class Solution:
def minimumOneBitOperations(self, n: int) -> int:
if n == 0:
return 0
top = 1 << (n.bit_length() - 1) # highest set bit
# 0 -> top takes 2*top - 1 moves and passes through n; the rest is the cost of n without its top bit
return 2 * top - 1 - self.minimumOneBitOperations(n ^ top) # n ^ top clears the top bitUse this when you must count the 1 bits of a number, or how many bits differ between numbers: n & (n - 1) removes the lowest set bit.
n & (n - 1) clears the lowest set bit of n. Repeat until n is 0, and the loop runs once per 1 bit. The number of loops is the answer. This is Brian Kernighan's method, and it skips the 0 bits entirely.
Complexity: O(k) time where k is the number of set bits, O(1) extra space
class Solution:
def hammingWeight(self, n: int) -> int:
count = 0
while n:
n &= n - 1 # drop the lowest set bit
count += 1 # one loop per set bit
return countDropping the last bit of i gives i >> 1, a smaller number whose count is already stored. The count for i is that count plus the dropped bit (i & 1). Fill the array from small to large, so each answer is one lookup and one addition.
Complexity: O(n) time, O(n) space for the answer
class Solution:
def countBits(self, n: int) -> List[int]:
dp = [0] * (n + 1)
for i in range(1, n + 1):
dp[i] = dp[i >> 1] + (i & 1) # bits of i = bits of i // 2, plus its last bit
return dpHamming distance counts the bits that differ, so count them per bit position instead of per pair. At bit i, if ones numbers have a 1 and n - ones have a 0, then ones * (n - ones) pairs differ at that position. Summing over the 30 bit positions avoids checking every pair.
Complexity: O(30 * n) time, O(1) extra space
class Solution:
def totalHammingDistance(self, nums: List[int]) -> int:
n, total = len(nums), 0
for i in range(30): # values are below 2^30
ones = sum((x >> i) & 1 for x in nums) # how many numbers have bit i set
total += ones * (n - ones) # each (1, 0) pair differs at bit i
return totalUse this when you must test for a power of 2 (or 4), or hit sums that are powers of 2: a power of 2 has exactly one set bit, so n & (n - 1) == 0.
A power of two has exactly one set bit. n & (n - 1) removes that bit and leaves 0. The check n > 0 rules out 0 and negative numbers, which would otherwise pass or confuse the test.
Complexity: O(1) time, O(1) extra space
class Solution:
def isPowerOfTwo(self, n: int) -> bool:
return n > 0 and n & (n - 1) == 0 # one set bit: clearing it leaves 0A power of four is a power of two whose single bit sits at an even position (0, 2, 4, ...). First check for one set bit with n & (n - 1) == 0. Then AND with 0x55555555, which has a 1 at every even position, and require a non-zero result.
Complexity: O(1) time, O(1) extra space
class Solution:
def isPowerOfFour(self, n: int) -> bool:
# one set bit (power of 2) that also lies on an even position (0x5555... mask)
return n > 0 and n & (n - 1) == 0 and n & 0x55555555 != 0A sum of two items is at most 2^21, so only 22 powers of two matter: 1 << k for k from 0 to 21. For each item x and each power, look up how many earlier items equal (1 << k) - x in a Counter. Counting only earlier items counts each pair once. The result is taken modulo 10^9 + 7 as the problem asks.
Complexity: O(22 * n) time, O(n) space
class Solution:
def countPairs(self, deliciousness: List[int]) -> int:
seen = Counter()
pairs = 0
for x in deliciousness:
for k in range(22): # powers of two up to 2^21
pairs += seen[(1 << k) - x] # earlier items that complete x to a power of 2
seen[x] += 1
return pairs % (10 ** 9 + 7)Use this when pairs or repeats should cancel out (a single number, a missing number, parity): XOR everything, because x ^ x = 0 and x ^ 0 = x.
XOR does not depend on order, and x ^ x = 0, so every pair cancels itself. After result ^= x runs over the whole array, only the number that appears once is left. No set or hash map is needed.
Complexity: O(n) time, O(1) extra space
class Solution:
def singleNumber(self, nums: List[int]) -> int:
result = 0
for x in nums:
result ^= x # equal pairs cancel to 0, and x ^ 0 = x
return resultStart result at n = len(nums), then XOR in every index i and every value x. Each number from 0 to n shows up once as an index (n as the start value) and once as a value, except the missing one, which shows up only as an index. All matching pairs cancel and the missing number is left.
Complexity: O(n) time, O(1) extra space
class Solution:
def missingNumber(self, nums: List[int]) -> int:
result = len(nums) # n has no index of its own
for i, x in enumerate(nums):
result ^= i ^ x # index and value cancel unless one is missing
return resultXOR of everything gives a ^ b, where a and b are the two numbers that appear once. They differ at every set bit of a ^ b, so xor_all & -xor_all picks one such bit. Splitting the numbers by that bit puts a and b in different groups, and each pair stays together in one group. XOR-ing one group gives a, and xor_all ^ a gives b.
Complexity: O(n) time, O(1) extra space
class Solution:
def singleNumber(self, nums: List[int]) -> List[int]:
xor_all = 0
for x in nums:
xor_all ^= x # pairs cancel, leaving a ^ b
low_bit = xor_all & -xor_all # lowest set bit: a and b differ here
a = 0
for x in nums:
if x & low_bit: # one group: numbers that have this bit
a ^= x # pairs cancel inside the group, leaving a
return [a, xor_all ^ a]A vowel count is even exactly when its parity bit is 0. Keep a 5-bit mask and flip a vowel's bit with mask ^= bit[ch] each time it appears. Two prefixes with the same mask have an even number of every vowel between them. Store the first index of each mask; the best answer is the largest i - first_seen[mask].
Complexity: O(n) time, O(1) extra space (at most 32 masks)
class Solution:
def findTheLongestSubstring(self, s: str) -> int:
bit = {'a': 1, 'e': 2, 'i': 4, 'o': 8, 'u': 16}
first_seen = {0: -1} # mask -> earliest index; the empty prefix has mask 0
mask, best = 0, 0
for i, ch in enumerate(s):
if ch in bit:
mask ^= bit[ch] # flip the parity of this vowel
if mask in first_seen:
best = max(best, i - first_seen[mask]) # same mask: every vowel between is even
else:
first_seen[mask] = i
return bestUse this when a small set (letters, skills, flags) can be packed into one integer with one bit per member, so union, overlap and subset tests become a single | or &.
Give each letter its own bit and set it with seen |= 1 << (ord(ch) - ord('a')). Repeated letters do no harm, because setting a bit twice changes nothing. The sentence is a pangram when all 26 low bits are set, which is the value (1 << 26) - 1.
Complexity: O(n) time, O(1) extra space
class Solution:
def checkIfPangram(self, sentence: str) -> bool:
seen = 0
for ch in sentence:
seen |= 1 << (ord(ch) - ord('a')) # set this letter's bit
return seen == (1 << 26) - 1 # all 26 bits setTurn each word into a 26-bit mask of its letters. Two words share no letter exactly when the AND of their masks is 0, which replaces a letter-by-letter comparison with one operation. Try every pair and keep the best product of lengths.
Complexity: O(L + n^2) time where L is the total number of letters, O(n) space
class Solution:
def maxProduct(self, words: List[str]) -> int:
masks = []
for word in words:
mask = 0
for ch in word:
mask |= 1 << (ord(ch) - ord('a')) # the word's letters as one mask
masks.append(mask)
best = 0
for i in range(len(words)):
for j in range(i + 1, len(words)):
if masks[i] & masks[j] == 0: # no common letter
best = max(best, len(words[i]) * len(words[j]))
return bestGive each required skill a bit, and turn each person into a mask of their skills. The dictionary best maps a skill mask to the smallest team found for it. For each person, OR their mask into every existing entry and keep the shorter team for the merged mask. The answer is the entry whose mask has every skill bit set.
Complexity: O(people * 2^skills) time, O(2^skills) space
class Solution:
def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
index = {skill: i for i, skill in enumerate(req_skills)}
full = (1 << len(req_skills)) - 1
best = {0: []} # skill mask -> smallest team with exactly these skills
for i, skills in enumerate(people):
person = 0
for skill in skills:
person |= 1 << index[skill] # the person's skills as a mask
for mask, team in list(best.items()):
merged = mask | person # OR joins the two skill sets
if merged not in best or len(best[merged]) > len(team) + 1:
best[merged] = team + [i]
return best[full]Use this when n is small (about 20 or less) and you must visit every subset: count a mask from 0 to 2^n - 1, where bit i says whether item i is in.
Each mask from 0 to 2^n - 1 is one subset: bit i set means nums[i] is in it. For every mask, XOR the chosen items together and add the result to the total. There are 2^n masks and n bits to test in each, so no recursion is needed.
Complexity: O(n * 2^n) time, O(1) extra space
class Solution:
def subsetXORSum(self, nums: List[int]) -> int:
n, total = len(nums), 0
for mask in range(1 << n): # every mask is one subset
xor = 0
for i in range(n):
if (mask >> i) & 1: # bit i set: nums[i] is in this subset
xor ^= nums[i]
total += xor
return totalThe same loop builds the subsets themselves. For each mask from 0 to 2^n - 1, collect nums[i] for every bit i that is set. Mask 0 gives the empty subset and the all-ones mask gives the full set. All 2^n subsets come out with no recursion.
Complexity: O(n * 2^n) time, O(n * 2^n) space for the output
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
n, result = len(nums), []
for mask in range(1 << n): # 0 .. 2^n - 1
result.append([nums[i] for i in range(n) if (mask >> i) & 1]) # bit i set: take nums[i]
return resultA mask is a set of jobs. work[mask] is the total time of those jobs, and best[mask] is the smallest possible largest workload using the workers added so far. A new worker takes some submask of mask; submasks are visited with sub = (sub - 1) & mask. The cost of that split is the larger of the new worker's load and the best result for the remaining jobs. Visiting every submask of every mask costs 3^n per worker.
Complexity: O(k * 3^n) time, O(2^n) space
class Solution:
def minimumTimeRequired(self, jobs: List[int], k: int) -> int:
n = len(jobs)
work = [0] * (1 << n) # work[mask] = total time of the jobs in mask
for mask in range(1, 1 << n):
low = (mask & -mask).bit_length() - 1 # index of the lowest set bit
work[mask] = work[mask & (mask - 1)] + jobs[low]
best = work[:] # one worker does every job in mask
for _ in range(min(k, n) - 1): # add one more worker per round
nxt = best[:]
for mask in range(1, 1 << n):
sub = mask
while sub: # every non-empty submask of mask
cost = max(best[mask ^ sub], work[sub]) # new worker takes sub
if cost < nxt[mask]:
nxt[mask] = cost
sub = (sub - 1) & mask # next smaller submask
best = nxt
return best[(1 << n) - 1]