Coding DSA Interview At Big Tech — with Full Solutions

Phạm Ngọc Lâm — ex-Senior Software Engineer @ TikTok / Grab · co-founder EngineerPro

Lê Quang Hoà — ex-Tech Lead @ TikTok · co-founder EngineerPro

2026

Project goal: to compile the most representative DSA coding interview questions at Big Tech companies, with Python 3 solutions, complexity analysis, common interview pitfalls, and related practice problems so that readers can train systematically by pattern, not by memorising individual problems.

Scope: 288 problems (267 fully solved + 21 cross-reference recaps across chapters) · 44 patterns · Python 3 solutions matching the LeetCode style.

📚 This book is based on the curriculum of the DSA Coding Interview course currently taught at EngineerPro.

However, a book cannot replace the deeper training in algorithmic thinking and the way you absorb algorithms that you get by attending the course directly at EngineerPro.

If you are interested in the course, please message our fanpage for a consultation:

This project is sponsored by EngineerPro.

EngineerPro is a community and training platform dedicated to helping Vietnamese software engineers break into Big Tech and international companies — from coding interviews and system design to career growth. This book is one of our contributions to the Vietnamese learning ecosystem for engineers.

How to find us:

About the authors

Phạm Ngọc Lâm

Phạm Ngọc Lâm

ex-Senior Software Engineer @ TikTok · Grab

Co-founder EngineerPro

in LinkedIn 🌐 Portfolio

Lê Quang Hoà

Lê Quang Hoà

ex-Tech Lead @ TikTok

Co-founder EngineerPro

in LinkedIn

Both authors have been through hundreds of technical interviews, on both sides of the table — as candidates and as interviewers — at TikTok, Grab, and other leading tech companies. This book distils real lessons from both sides of that experience.

🙏 Acknowledgments

This book is inspired by the DSA topics that many outstanding mentors at EngineerPro have taught over the years across our courses and mentor sessions. The authors would like to sincerely thank the teaching team who has accompanied the community:

  • Lam Đỗ — ex-Software Engineer @ Meta
  • Lê Chương — Senior Software Engineer @ Google
  • Trần Khánh Hiệp — Software Engineer @ Spotify
  • Tùng Trần — Senior SWE @ Pendle · ex-Senior SWE @ Shopee
  • Quang Hoàng — Senior Software Engineer @ Google
  • Kyle Nguyễn — Senior Engineer @ Citadel
  • Tùng Lâm — Senior Software Engineer @ Shopee
  • Hiếu — Senior Software Engineer @ Acronis · ex-SWE @ Shopee
  • … and many other mentors across the EngineerPro community.

Introduction

Video: a sample coding interview session

Before diving into the main content, please watch a sample coding interview session produced by EngineerPro. The video lets you visualise the real flow of an interview round: how the interviewer asks questions, how the candidate clarifies, analyses, codes, and discusses follow-ups.

Sample coding interview — EngineerPro Watch on YouTube

▶ Watch the full playlist — other sample interviews on the EngineerPro YouTube channel.

Viewing tip: the first time you watch, focus on how the candidate communicates, not on understanding every algorithm. Come back for a second viewing after finishing Chapters 1–5 and you will recognise many familiar patterns.


0.1 Foreword

Welcome to Coding DSA Interview At Big Tech — with Full Solutions.

This book is written with one simple belief: a Big Tech coding interview is not a trivia contest — it is a learnable skill that can be trained systematically.

Most existing interview materials fall into one of two extremes: - Too academic (textbook DSA), with little real-world interview context. - Too “tips and tricks” (300 LeetCode lists), without a systematic thinking framework.

This book aims to sit between the two: learn PATTERNS, not problems. Each chapter focuses on one pattern with 5–18 representative problems, including whiteboard presentation guidance and common pitfalls from real interviews. Once you internalise the pattern, new problems become variations.

Who this book is for: - CS students preparing for internships / new-grad roles at Big Tech. - Working engineers looking to switch into FAANG-tier companies. - Self-learners on LeetCode who feel stuck without a systematic approach.

We wish you success in every interview round!

How to use this book

There are three reading paths depending on your time and experience:

Sequential reading (recommended for beginners): chapters 1 → 44 in order. For each chapter you must code the problems yourself before peeking at the solutions. After finishing Level 1 (Chapters 1–20) you will have a solid foundation for entry-level Big Tech interviews.

Pattern-based reading (if you already have a foundation): skim the table of contents and dive into chapters where you feel weak. Each chapter stands alone and includes clear cross-references to related material.

Cramming before an onsite: see the Learning Roadmap below.

Learning roadmap

1-week roadmap (last-minute onsite prep): - Day 1–2: Frontmatter (0.1–0.5) + Appendix D (50 must-do problems). - Day 3: Review Chapters 1, 2, 5, 6 (Array, String, Binary Search, Hash) — all Easy/Medium tier. - Day 4: Chapters 7, 9, 10, 11 (Linked List, Graph, BFS, DFS). - Day 5: Chapters 17, 18, 27 (D&C, Monotonic, Sliding Window). - Day 6: Chapters 28, 29 (Backtracking, DP) — the two longest, most important chapters. - Day 7: Mock interviews + read Appendix E (Behavioral).

2-week roadmap (already know DSA, refresher): - Week 1: Level 1 (Chapters 1–20) — 3 chapters/day. - Week 2: Level 2 (Chapters 21–32) — 2 chapters/day + mock interviews at the weekend.

6-week roadmap (beginners): - Weeks 1–2: Frontmatter + Level 1 (Ch 1–10) — 1 chapter/day, code every problem yourself. - Weeks 3–4: Level 1 (Ch 11–20) — 1 chapter/day. - Week 5: Level 2 (Ch 21–32) — 2 chapters/day, read patterns carefully. - Week 6: Level 3 (Ch 33–44) — 2 chapters/day, no need to memorise — just know they exist.

Skip if short on time (priority chapters): skip Ch 20 (Prime), Ch 31 (Game Theory), Ch 33–36 (MST / Hash / KMP / Z) — niche patterns, rare in entry/mid-level interviews.

Self-assessment: - Solve Easy in < 30 min each → Level 1 is enough. - Medium in 30–60 min → read up to Level 2. - Hard takes > 60 min or you get stuck often → read all three levels.

⚠️ Do not read the code before planning yourself. The book intentionally places the solution after the “Approach” section — you must struggle with the problem before peeking at the answer; that is how the brain absorbs patterns most effectively.


0.2 The coding interview process (UMPIRE)

UMPIRE = a 6-step framework that keeps you from “freezing” when you receive a new problem:

U — Understand (5 min)

M — Match (2 min)

P — Plan (5 min)

I — Implement (15 min)

R — Review (3 min)

E — Evaluate (2 min)

Mindset tip: the interviewer wants to see your thought process, not a perfect solution on the first try. Think out loud.


0.3 Big-O in 30 minutes

Quick definition

f(n) = O(g(n)) ↔︎ there exists c > 0, n₀ such that f(n) <= c·g(n) for every n >= n₀.

In interviews: drop constants, drop lower-order terms. 3n² + 100n + 5 = O(n²).

“Cheatsheet” table

Notation Name Example
O(1) Constant Hash lookup, push/pop on a stack
O(log n) Logarithmic Binary search
O(n) Linear Single-array scan
O(n log n) Linearithmic Sort, segment-tree build
O(n²) Quadratic Brute-force double loop
O(2^n) Exponential Subset brute force
O(n!) Factorial Permutation brute force

Calculation rules

  1. Sequential (a(); b();): O(a + b), take the max.
  2. Nested loops: O(n × n) = O(n²).
  3. Recursion:
  4. Master theorem for T(n) = aT(n/b) + f(n):

Amortised analysis

Some operations are occasionally slow but on average fast: - Python list.append(): O(1) amortised (despite occasional dynamic resizing). - Hash table with open addressing: O(1) amortised.

Common pitfalls

Practical bounds in interviews

n Acceptable
n ≤ 10 O(n!) brute force
n ≤ 20 O(2^n) bitmask
n ≤ 5000 O(n²) is fine
n ≤ 10^5 O(n log n) or O(n)
n ≤ 10^7 strict O(n)
n ≤ 10^9 O(log n) (search on the answer)

0.4 Python 3 cheat-sheet for interviews

Data structures

# List
lst = [1, 2, 3]
lst.append(x); lst.pop()      # O(1) both
lst.insert(0, x); lst.pop(0)  # O(n) — avoid!
sorted_lst = sorted(lst)      # O(n log n), returns a new list
lst.sort()                    # in-place
lst[::-1]                     # reverse, O(n)
lst[a:b]                      # slicing, O(b-a) copy

# Dict
d = {}; d[k] = v               # O(1) amortised
from collections import defaultdict, Counter
dd = defaultdict(list)
cnt = Counter("anagram")       # {'a':3, 'n':1, 'g':1, 'r':1, 'm':1}
cnt.most_common(2)             # [('a',3), ('n',1)]

# Set
s = {1, 2, 3}
s.add(x); s.discard(x)         # O(1)
s & t; s | t; s - t            # intersection / union / difference

# Deque (double-ended queue) — used for BFS, sliding window
from collections import deque
dq = deque()
dq.append(x); dq.appendleft(x)
dq.pop(); dq.popleft()         # all O(1)

# Heap (min-heap)
import heapq
h = []
heapq.heappush(h, x)
heapq.heappop(h)              # O(log n)
heapq.heapify(lst)            # O(n)
heapq.nsmallest(k, lst)       # O(n log k)

Important idioms

# Enumerate
for i, x in enumerate(lst):
    pass

# Zip
for a, b in zip(lst1, lst2):
    pass

# Comprehension
[x*2 for x in lst if x > 0]
{x: i for i, x in enumerate(lst)}

# Bisect
from bisect import bisect_left, bisect_right
idx = bisect_left(sorted_lst, x)

# Functools
from functools import cache, reduce
@cache
def f(n):
    ...
reduce(lambda a, b: a + b, lst, 0)

# Itertools
from itertools import combinations, permutations, product
list(combinations([1,2,3], 2))    # [(1,2),(1,3),(2,3)]

Pitfalls (CRITICAL — read carefully!)

Arithmetic & division: - dict[k] raises KeyError if k is missing → use dict.get(k, default) or defaultdict. - int / int = float; use // for integer division. - -7 // 2 = -4 (floor), not -3 (truncate toward 0). Truncate via int(-7/2). - -7 % 2 = 1 in Python (always ≥ 0), different from C/C++/Java. Be careful when taking modulo of negative numbers in prefix sums.

Heap & comparison: - Python heapq is only a min-heap. For max-heap, push -x. - Heap compares tuples element-by-element: (priority, idx, payload) — use idx as a tiebreaker when priorities are equal (the payload may not be hashable or comparable, e.g. ListNode). - heappush-ing a tuple whose payload is not comparable (such as ListNode) raises TypeError when priorities collide.

Recursion & cache: - Python’s default recursion limit is around 1000. Use sys.setrecursionlimit(10**6) for large graphs/trees. - Python has no tail-call optimisation → deep recursion can blow the stack. Switch to iterative when n > 10^5. - @functools.cache only works for hashable arguments. list / dict / set cannot be cached; wrap with tuple(...) / frozenset(...). - @cache on a self.method shares a global cache across instances. Use @cached_property if you want per-instance caching.

Mutable defaults & references: - Default mutable argument: def f(x=[]) is a serious bug (every call shares the same list). Use def f(x=None): x = x or []. - result.append(path) appends a reference, not a copy. Use result.append(path.copy()) or result.append(path[:]).

Sort & stability: - Python’s sorted() is Timsort, stable, O(n log n). Take advantage of stability to sort multi-key by sorting multiple times in reverse order. - Custom sort: Python 3 has no cmp= argument. Use key=... or functools.cmp_to_key(...).

Integers & overflow: - Python int is unbounded → no need to worry about overflow like in Java/C++. But explicitly truncate (32-bit) when the problem requires it (LC 7, LC 8, LC 50).


0.5 How to present code on a whiteboard / CoderPad

Golden rules

  1. Think out loud. Silence makes it hard for the interviewer to follow your thought process — they need to hear your reasoning to help when you get stuck, and to evaluate how you approach problems rather than only the final answer.
  2. Start from concrete examples. Draw the input on paper and run the algorithm by hand, step by step.
  3. Code top-down. Write the main def solve(...) first, call helper functions, then implement the helpers afterwards.
  4. Use clear names:
  5. Do not chase brevity. Clear code matters more than a “clever” one-liner.

When stuck

After finishing

Demeanor during the interview

Sample phrases per stage

Stage Sample phrase
Clarify “Let me confirm the problem: input is …, output is …, any additional constraints?”
Brute force “To start, here is the direct approach: enumerate every pair — O(n²)…”
Optimise “I think we can use a hash map to reduce each lookup to O(1)…”
Stuck “I am torn between a hash map and a sorted array. Do you have any hint?”
Done “The solution runs in O(n) time and O(n) space. Let me try a few edge cases…”

Chapter 1 — Array

Array is the most fundamental data structure and also the pattern that shows up most often in Big Tech coding interviews. Most techniques in later chapters (two pointers, sliding window, prefix sum, monotonic stack, …) originate from array manipulation. The goal of this chapter is to master in-place and two-pass operations and to start cultivating the habit of “thinking in terms of indices, not values”.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

from typing import List

def two_pass_pattern(nums: List[int]) -> List[int]:
    """Two-pass template: pass 1 collects information, pass 2 uses it."""
    n = len(nums)
    aux = [0] * n
    # pass 1: compute prefix / suffix / count
    for i in range(n):
        aux[i] = ...   # problem-specific
    # pass 2: use aux to produce the result
    out = [0] * n
    for i in range(n):
        out[i] = ...   # problem-specific
    return out


def two_pointers_in_place(nums: List[int]) -> int:
    """Two-pointer in-place: slow = write position, fast = read position."""
    slow = 0
    for fast in range(len(nums)):
        if condition(nums[fast]):
            nums[slow] = nums[fast]
            slow += 1
    return slow  # length of the "valid" compacted prefix

End-of-chapter practice


1.1 Two Sum (LC 1)

Problem

You are given an integer array nums and an integer target. Return the indices of the two elements in nums whose sum equals target.

You may assume each input has exactly one solution, and you cannot use the same element twice.

Example

Input:  nums = [2, 7, 11, 15], target = 9
Output: [0, 1]
Explanation: nums[0] + nums[1] == 9.

Constraints

Clarifying questions

Approach

Brute force — O(n²). Enumerate every pair (i, j) with i < j and check nums[i] + nums[j] == target. Easy to write but TLEs for large n.

Optimal — one-pass hash map — O(n). When we are at index i, we need to know whether some j < i satisfies nums[j] == target - nums[i]. Use a dict storing {value: index} of elements we have already seen.

Presentation tip: Always start with brute force, state its complexity explicitly, then say: “I think we can replace the linear O(n) lookup with an O(1) hash-map lookup, which brings the total cost down from O(n²) to O(n)…” — interviewers love this flow of reasoning.

Code (Python 3)

from typing import List

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        seen: dict[int, int] = {}
        for i, x in enumerate(nums):
            complement = target - x
            if complement in seen:
                return [seen[complement], i]
            seen[x] = i
        return []  # per the problem statement this line never executes

Complexity

Comments


1.2 Best Time to Buy and Sell Stock (LC 121)

Problem

Given an array prices where prices[i] is the stock price on day i, you are allowed to buy once then sell once afterwards (you cannot sell before you buy). Return the maximum profit, or 0 if no transaction is profitable.

Example

Input:  prices = [7, 1, 5, 3, 6, 4]
Output: 5
Explanation: buy on day 2 (price 1), sell on day 5 (price 6); profit = 6 - 1 = 5.

Input:  prices = [7, 6, 4, 3, 1]
Output: 0
Explanation: prices only decrease; no profitable transaction exists.

Constraints

Clarifying questions

Approach

Brute force — O(n²). Try every (i, j) with i < j and take max(prices[j] - prices[i]). TLE at n = 10^5.

Optimal — one pass, O(n). If we decide “we will sell today on day i, the best profit is prices[i] - min(prices[0..i-1]). So we just maintain min_so_far as we sweep and update best at every step.

Illustration for prices = [7, 1, 5, 3, 6, 4]:

day        :   0    1    2    3    4    5
prices     :   7    1    5    3    6    4
                    │              │
                    │ buy here     │ sell here
                    ▼              ▼
min_so_far :   7    1    1    1    1    1
profit_now :   0    0    4    2    5    3   (= prices[i] - min_so_far)
best       :   0    0    4    4    5    5   ← answer = 5
                              ▲
                              unchanged since 2 < 4

Mindset: this is a DP in one variable. The state min_so_far is the O(1)-space compression of dp[i] = min(prices[0..i]) — a technique you will see repeatedly in the DP chapters.

Code (Python 3)

from typing import List
import math

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        min_so_far = math.inf
        best = 0
        for p in prices:
            min_so_far = min(min_so_far, p)
            best = max(best, p - min_so_far)
        return best

Complexity

Comments


1.3 Product of Array Except Self (LC 238)

Problem

Given an array nums of n integers, return an array answer of the same length such that answer[i] is the product of all elements of nums except nums[i].

Special constraints: - Division is not allowed. - The algorithm must run in O(n) time.

Example

Input:  nums = [1, 2, 3, 4]
Output: [24, 12, 8, 6]
Explanation:
  answer[0] = 2*3*4 = 24
  answer[1] = 1*3*4 = 12
  answer[2] = 1*2*4 = 8
  answer[3] = 1*2*3 = 6

Input:  nums = [-1, 1, 0, -3, 3]
Output: [0, 0, 9, 0, 0]

Constraints

Clarifying questions

Approach

Brute force — O(n²). For each i, recompute the product of the rest of the array. The problem already bans this.

Division — O(n) but not allowed. Compute total = product(nums) then answer[i] = total / nums[i]. Forbidden because nums[i] = 0 breaks it; and many languages lack exact integer division.

Optimal — Prefix × Suffix products — O(n) time, O(1) extra space (output excluded).

Observation: answer[i] = (∏ nums[0..i-1]) * (∏ nums[i+1..n-1]). Define: - left[i] = product of nums[0..i-1] (left product, left[0] = 1). - right[i] = product of nums[i+1..n-1] (right product, right[n-1] = 1). Then answer[i] = left[i] * right[i].

To reach O(1) extra space, reuse answer: - Pass 1 (left → right): fill answer[i] = left[i]. - Pass 2 (right → left): multiply answer[i] *= right, updating the rolling variable right as we go.

Illustration for nums = [1, 2, 3, 4]:

              i=0     i=1     i=2     i=3
nums      :  [  1  ,   2  ,   3  ,   4  ]

                 ┌─────────────┐
                 │  prefix →   │   (product of elements LEFT of i)
                 ▼             ▼
left[i]   :  [  1  ,   1  ,   2  ,   6  ]
              (empty)(1)   (1·2) (1·2·3)

                         ┌─────────────┐
                         │   ← suffix  │   (product of elements RIGHT of i)
                         ▼             ▼
right[i]  :  [ 24  ,  12  ,   4  ,   1  ]
            (2·3·4)(3·4)  (4)   (empty)

                         ↓  pointwise multiplication  ↓

answer[i] :  [ 24  ,  12  ,   8  ,   6  ]
              1·24   1·12   2·4    6·1

In real code we do not keep both left and right arrays — we use answer for the prefix pass, then a single rolling right variable swept right-to-left to multiply into answer in place.

Code (Python 3)

from typing import List

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        answer = [1] * n

        # Pass 1: answer[i] = product of elements left of i.
        left = 1
        for i in range(n):
            answer[i] = left
            left *= nums[i]

        # Pass 2: multiply by the right-side product, using a rolling variable.
        right = 1
        for i in range(n - 1, -1, -1):
            answer[i] *= right
            right *= nums[i]

        return answer

Complexity

Comments


1.4 Move Zeroes (LC 283)

Problem

Given an array nums, move all zeros to the end of the array while preserving the relative order of the non-zero elements. Do it in place; you may not create an auxiliary array.

Example

Input:  nums = [0, 1, 0, 3, 12]
Output: [1, 3, 12, 0, 0]

Input:  nums = [0]
Output: [0]

Constraints

Clarifying questions

Approach

Brute force — O(n) time, O(n) extra space. Build an auxiliary array of non-zero values and pad with zeros to length n. The problem forbids auxiliary arrays — rejected.

Optimal — Two pointers — O(n) time, O(1) extra space. Use two pointers: - slow = the next index to write a non-zero value. - fast = the index currently being read.

Pass 1: for each fast, if nums[fast] != 0nums[slow] = nums[fast], then increment slow. Pass 2: from slow to the end of the array, write 0.

Illustration for nums = [0, 1, 0, 3, 12]:

                    Pass 1: pack non-zero values to the front
                    ──────────────────────────────────────────

Initial  :  [ 0 , 1 , 0 , 3 , 12]      slow=0  fast=0
              S
              F

fast=0:  nums[0]=0, skip
         [ 0 , 1 , 0 , 3 , 12]         slow=0  fast=1
           S
               F

fast=1:  nums[1]=1 != 0 → write nums[0]=1, slow++
         [ 1 , 1 , 0 , 3 , 12]         slow=1  fast=2
               S
                   F

fast=2:  nums[2]=0, skip                slow=1  fast=3
fast=3:  nums[3]=3 != 0 → write nums[1]=3, slow++
         [ 1 , 3 , 0 , 3 , 12]         slow=2  fast=4
                   S
                       F

fast=4:  nums[4]=12 != 0 → write nums[2]=12, slow++
         [ 1 , 3 ,12 , 3 , 12]         slow=3  fast=done

                    Pass 2: from slow to end, write 0
                    ──────────────────────────────────────────

         [ 1 , 3 ,12 , 0 , 0 ]    ← answer
                       ▲   ▲
                   write 0  write 0

This approach minimises the number of writes to exactly n (each cell is written at most once). A swap-as-you-go variant exists with shorter code but twice as many writes.

Code (Python 3)

from typing import List

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        slow = 0
        # Pass 1: pack non-zero values to the front.
        for fast in range(len(nums)):
            if nums[fast] != 0:
                nums[slow] = nums[fast]
                slow += 1
        # Pass 2: fill the tail with zeros.
        for i in range(slow, len(nums)):
            nums[i] = 0

Complexity

Comments

slow = 0
for fast in range(len(nums)):
    if nums[fast] != 0:
        nums[slow], nums[fast] = nums[fast], nums[slow]
        slow += 1

1.5 Container With Most Water (LC 11)

Problem

Given an array height where height[i] is the height of the i-th pole, choose two poles i < j such that the water held between them is maximal.

Water volume = min(height[i], height[j]) * (j - i).

Example

Input:  height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
Output: 49
Explanation: choose poles 1 (height 8) and 8 (height 7) → 7 * (8-1) = 49.

Input:  height = [1, 1]
Output: 1

Constraints

Clarifying questions

Approach

Brute force — O(n²). Try every (i, j) and take the max. TLE for n = 10^5.

Optimal — Two pointers, O(n). Set l = 0, r = n - 1. The current area is min(height[l], height[r]) * (r - l).

Core question: which pointer do we move? — Move the pointer at the shorter side.

Why? The area is bounded by the shorter pole. Moving the taller pointer inward shrinks the width while the shorter pole is still the bottleneck → the area can only stay the same or decrease. Moving the shorter pointer at least gives us a chance (no guarantee) to find a taller pole, which can lift the bottleneck.

“Elimination” proof: Fixing the shorter pole (say the left one) and moving the right pointer inward, every pair (l, r' < r) has area ≤ height[l] * (r - l). None of those pairs can beat the current area unless they were already going to lose to the current best — we can “eliminate” all of them at once and only need to move l.

Illustration for height = [1, 8, 6, 2, 5, 4, 8, 3, 7]:

         ▓                                ▓
   8     ▓       ▓                        |          ← height 8
   7     ▓       ▓                        ▓
   6     ▓   ▓   ▓               ▓        ▓
   5     ▓   ▓   ▓       ▓       ▓        ▓
   4     ▓   ▓   ▓       ▓   ▓   ▓        ▓
   3     ▓   ▓   ▓       ▓   ▓   ▓    ▓   ▓
   2     ▓   ▓   ▓   ▓   ▓   ▓   ▓    ▓   ▓
   1 ▓   ▓   ▓   ▓   ▓   ▓   ▓   ▓    ▓   ▓
       └─┴───┴───┴───┴───┴───┴───┴────┴───┴─
index: 0   1   2   3   4   5   6    7   8
       L                                  R

Trace (★ = best so far):

  step │  L   R │ min(h[L], h[R]) │ width │  area │ move
  ─────┼────────┼─────────────────┼───────┼───────┼──────────────
   1   │  0   8 │       1         │   8   │    8  │ h[L]<h[R] → L++
   2   │  1   8 │       7         │   7   │  49 ★ │ h[L]>=h[R] → R--
   3   │  1   7 │       3         │   6   │   18  │ → R--
   4   │  1   6 │       8         │   5   │   40  │ → R--
   5   │  1   5 │       4         │   4   │   16  │ → R--
   6   │  1   4 │       5         │   3   │   15  │ → R--
   7   │  1   3 │       2         │   2   │    4  │ → R--
   8   │  1   2 │       6         │   1   │    6  │ → R--
   ─── │  1   1 │       stop      │       │       │

Answer: 49 (pair of pole index 1 and 8, heights 8 and 7).

Code (Python 3)

from typing import List

class Solution:
    def maxArea(self, height: List[int]) -> int:
        l, r = 0, len(height) - 1
        best = 0
        while l < r:
            h = min(height[l], height[r])
            best = max(best, h * (r - l))
            # Always move the pointer on the shorter side.
            if height[l] < height[r]:
                l += 1
            else:
                r -= 1
        return best

Complexity

Comments


1.6 Rotate Array (LC 189)

Problem

Given an array nums and a non-negative integer k, rotate the array to the right by k steps.

Example

Input:  nums = [1, 2, 3, 4, 5, 6, 7], k = 3
Output: [5, 6, 7, 1, 2, 3, 4]
Explanation: 1-step right → [7,1,2,3,4,5,6]; 2-step → [6,7,1,2,3,4,5]; 3-step → [5,6,7,1,2,3,4].

Input:  nums = [-1, -100, 3, 99], k = 2
Output: [3, 99, -1, -100]

Constraints

Clarifying questions

Approach

Brute force — rotate by one step, repeated k times — O(n·k). TLE.

Auxiliary array — O(n) time, O(n) space. new[(i + k) % n] = nums[i], then copy new back to nums. Simple but violates the O(1)-space follow-up.

Optimal — Three reverses, O(n) time, O(1) extra space. Look at n = 7, k = 3: - Reverse the whole array: [7, 6, 5, 4, 3, 2, 1]. - Reverse [0..k-1]: [5, 6, 7, 4, 3, 2, 1]. - Reverse [k..n-1]: [5, 6, 7, 1, 2, 3, 4]. ✓

Intuition: the full reverse brings the elements that “should end up at the beginning” to the front, but each block is locally backwards. The two inner reverses fix the order inside each block.

Illustration for n = 7, k = 3:

Input         :  [ 1   2   3   4 │ 5   6   7 ]
                                  ▲
                  the last k=3 elements must "jump" to the front

──────────────────────────────────────────────────

Step 1: reverse the whole array [0..6]
                  ◀═══════════════════════════▶
                 [ 7   6   5   4   3   2   1 ]
                   ↑               ↑
                  (5,6,7 reversed) (1,2,3,4 reversed)

Step 2: reverse [0..k-1] = [0..2]   (fix the first 3 elements)
                  ◀═══════▶
                 [ 5   6   7 │ 4   3   2   1 ]
                                ↑
                  first 3 fixed; last 4 still backwards

Step 3: reverse [k..n-1] = [3..6]   (fix the last 4 elements)
                              ◀═══════════════▶
                 [ 5   6   7 │ 1   2   3   4 ]   ← answer ✓

Code (Python 3)

from typing import List

class Solution:
    def rotate(self, nums: List[int], k: int) -> None:
        n = len(nums)
        k %= n  # always normalise first

        def reverse(left: int, right: int) -> None:
            while left < right:
                nums[left], nums[right] = nums[right], nums[left]
                left += 1
                right -= 1

        reverse(0, n - 1)        # reverse everything
        reverse(0, k - 1)        # reverse the first k elements
        reverse(k, n - 1)        # reverse the rest

Complexity

Comments

Chapter summary & decisions

Array decision checklist (before you start coding)

Question If YES If NO
May we mutate the input? In-place (Move Zeroes, Rotate) Allocate a result array
Must we preserve the original order? Same-direction two-pointer Free to swap anywhere
Return index or value? Be careful when sorting: keep (value, index)
Are there zeros / negatives? Product-Except-Self cannot use division Plain prefix × suffix is fine
Need O(1) memory? 3-reverse trick, in-place markers Hashes / aux arrays are fine

Gateway to other patterns

Rotate Array recap — three approaches compared

Approach Time Space When to choose
Extra array O(n) O(n) Easiest to write, fewest bugs; when RAM is plentiful
3-reverse O(n) O(1) Default for interviews — elegant and short
Cyclic replacement (GCD) O(n) O(1) When the interviewer asks for a “no-reverse” follow-up

Chapter 2 — String

A string is really an array of characters — every technique from Chapter 1 (two pointers, in-place, prefix) applies. Strings, however, have two specific quirks: (i) you must handle the character set (ASCII or full Unicode?) and (ii) in Python, strings are immutable — you cannot mutate them in place; every “character swap” must go through a list followed by ''.join.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

from collections import Counter
from typing import List

def two_pointers_in_string(s: str) -> bool:
    """Two-pointer template: check a symmetric / pairwise condition."""
    l, r = 0, len(s) - 1
    while l < r:
        if not check(s[l], s[r]):
            return False
        l += 1
        r -= 1
    return True


def count_chars(s: str) -> dict[str, int]:
    """Character frequency table — used by almost every string problem."""
    return Counter(s)

End-of-chapter practice


2.1 Valid Anagram (LC 242)

Problem

Given two strings s and t, return True if t is an anagram of s (same characters with the same counts, possibly in different order), otherwise False.

Example

Input:  s = "anagram", t = "nagaram"
Output: True

Input:  s = "rat", t = "car"
Output: False

Constraints

Clarifying questions

Approach

Brute force — sort, O(n log n). sorted(s) == sorted(t). One-liner, but O(n log n) time and O(n) space (because sorted returns a list).

Optimal — single-pass Counter, O(n). Count the characters of s, then sweep t decrementing. If any count goes negative — not an anagram. Final state must be all zeros.

Even faster — fixed 26-element table, O(1) extra space (alphabet dependent). With only 26 letters we can use int[26] (or a length-26 list) instead of a dict. Memory is genuinely O(1) (independent of n).

Code (Python 3)

from collections import Counter

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        return Counter(s) == Counter(t)


class SolutionFast:
    """Fixed 26-letter table — true O(1) memory."""
    def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        count = [0] * 26
        for ch in s:
            count[ord(ch) - ord('a')] += 1
        for ch in t:
            count[ord(ch) - ord('a')] -= 1
            if count[ord(ch) - ord('a')] < 0:
                return False
        return True

Complexity

Comments


2.2 Valid Palindrome (LC 125)

Problem

Given a string s, consider it a palindrome if, after converting all letters to lowercase and removing every non-alphanumeric character, it reads the same forwards and backwards. Return True / False.

Example

Input:  s = "A man, a plan, a canal: Panama"
Output: True
Explanation: filtered → "amanaplanacanalpanama" — reads identically both ways.

Input:  s = "race a car"
Output: False
Explanation: filtered → "raceacar" — not a palindrome.

Input:  s = " "
Output: True
Explanation: an empty filtered string is considered a palindrome.

Constraints

Clarifying questions

Approach

Brute force — filter then compare reversed — O(n) time, O(n) space. filtered = ''.join(ch.lower() for ch in s if ch.isalnum()), then filtered == filtered[::-1]. Simple but uses O(n) extra memory.

Optimal — Two pointers in place — O(n) time, O(1) space. Use two pointers l (left) and r (right). On each side, skip non-alphanumeric characters, then compare s[l].lower() == s[r].lower(). Mismatch → return False.

Code (Python 3)

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():
                l += 1
            while l < r and not s[r].isalnum():
                r -= 1
            if s[l].lower() != s[r].lower():
                return False
            l += 1
            r -= 1
        return True

Complexity

Comments


2.3 Longest Common Prefix (LC 14)

Problem

Given an array of strings strs, return the longest common prefix. If none exists, return "".

Example

Input:  strs = ["flower", "flow", "flight"]
Output: "fl"

Input:  strs = ["dog", "racecar", "car"]
Output: ""
Explanation: no character is shared at position 0.

Constraints

Clarifying questions

Approach

Approach 1 — Vertical scan, O(S) where S is the total length. Iterate column-by-column i = 0, 1, 2, .... At each i, check whether strs[0][i] equals strs[j][i] for every j. If a string runs out or a mismatch occurs → return strs[0][:i].

Approach 2 — Horizontal scan. Take prefix = strs[0], then for each subsequent string, shrink prefix until it is a prefix of that string.

Approach 3 — Sort + compare endpoints, O(n log n · L). Sort lexicographically. The longest common prefix equals the common prefix of strs[0] and strs[-1]. Cute but not time-optimal.

We recommend vertical scan because it is easiest to present on a whiteboard and supports early-exit on the first mismatch.

Code (Python 3)

from typing import List

class Solution:
    def longestCommonPrefix(self, strs: List[str]) -> str:
        if not strs:
            return ""
        for i, ch in enumerate(strs[0]):
            for s in strs[1:]:
                if i >= len(s) or s[i] != ch:
                    return strs[0][:i]
        return strs[0]  # all of strs[0] is a common prefix

Complexity

Comments


2.4 String to Integer / atoi (LC 8)

Problem

Implement atoi (ASCII to Integer), converting a string into a signed 32-bit integer. Rules:

  1. Skip leading whitespace.
  2. Read an optional + or - sign.
  3. Read consecutive digit characters until a non-digit is reached.
  4. Apply the sign to the result.
  5. Clamp to the range of int 32-bit: [-2^31, 2^31 - 1].
  6. Return 0 if no digits were read (e.g. the string is entirely letters).

Example

Input:  s = "42"
Output: 42

Input:  s = "   -42"
Output: -42                (skip leading spaces, read '-', then "42")

Input:  s = "4193 with words"
Output: 4193               (stop at the whitespace after "4193")

Input:  s = "words and 987"
Output: 0                  (we hit 'w' immediately — no digits read)

Input:  s = "-91283472332"
Output: -2147483648        (= INT_MIN, clamped because the number is too small)

Input:  s = "+-12"
Output: 0                  (already consumed '+', then '-' is non-digit → fail)

Constraints

Clarifying questions

Approach

Approach 1 — Sequential procedure with an index walker. Four clear steps: skip space → read sign → read digits → clamp. Each step maintains its own state variables.

Approach 2 — Finite State Machine (FSM). A state machine yields concise code and is easy to extend when the problem adds requirements (decimals, scientific notation, …). Highly worth learning since it is the canonical pattern for any parser problem (Chapter 32).

FSM diagram:

       blank        sign        digit       other
   ┌───────────────────────────────────────────────┐
S  │  start  →  start    signed   in_number   end │
T  │  signed →  end      end      in_number   end │
A  │ in_num  →  end      end      in_number   end │
T  │  end    →  end      end      end         end │
E  └───────────────────────────────────────────────┘

States:
  start     : still skipping leading spaces
  signed    : sign already read, awaiting digits
  in_number : currently consuming digits
  end       : finished; remaining characters are ignored

Code (Python 3)

INT_MAX = 2**31 - 1   # 2147483647
INT_MIN = -2**31      # -2147483648

class Solution:
    """Approach 1 — sequential procedure."""

    def myAtoi(self, s: str) -> int:
        i, n = 0, len(s)

        # 1. Skip leading whitespace.
        while i < n and s[i] == ' ':
            i += 1

        # 2. Optional sign.
        sign = 1
        if i < n and s[i] in '+-':
            sign = -1 if s[i] == '-' else 1
            i += 1

        # 3. Read digits.
        result = 0
        while i < n and s[i].isdigit():
            result = result * 10 + (ord(s[i]) - ord('0'))
            # Optimisation: clamp early to avoid running too long.
            if result > 2**31:
                break
            i += 1

        # 4. Apply sign and clamp.
        result *= sign
        return max(INT_MIN, min(INT_MAX, result))


class SolutionFSM:
    """Approach 2 — Finite State Machine. Easy to extend later."""

    table = {
        'start':     {'blank': 'start',  'sign': 'signed',   'digit': 'in_num', 'other': 'end'},
        'signed':    {'blank': 'end',    'sign': 'end',      'digit': 'in_num', 'other': 'end'},
        'in_num':    {'blank': 'end',    'sign': 'end',      'digit': 'in_num', 'other': 'end'},
        'end':       {'blank': 'end',    'sign': 'end',      'digit': 'end',    'other': 'end'},
    }

    @staticmethod
    def _kind(ch: str) -> str:
        if ch == ' ':           return 'blank'
        if ch in '+-':          return 'sign'
        if ch.isdigit():        return 'digit'
        return 'other'

    def myAtoi(self, s: str) -> int:
        state = 'start'
        sign = 1
        result = 0
        for ch in s:
            state = self.table[state][self._kind(ch)]
            if state == 'in_num':
                result = result * 10 + int(ch)
                result = min(result, INT_MAX + 1)  # early-clamp
            elif state == 'signed':
                sign = -1 if ch == '-' else 1
            elif state == 'end':
                break
        return max(INT_MIN, min(INT_MAX, sign * result))

Complexity

Comments


2.5 Group Anagrams (LC 49)

Problem

Given an array of strings strs, group strings that are anagrams of one another. Return the list of groups (the order of groups and the order within each group does not matter).

Example

Input:  strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
Output: [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]

Constraints

Clarifying questions

Approach

Core idea: two strings are anagrams ↔︎ they share the same “signature”. Use a dict {signature: [strings]} to bucket them.

Approach 1 — signature = sorted(s), O(n · k log k). key = ''.join(sorted(s)). Anagrams share the same sorted form.

Approach 2 — signature = tuple of 26 counts, O(n · k). key = tuple(Counter(s)[ch] for ch in 'abcdefghijklmnopqrstuvwxyz'). Avoids the O(k log k) sort, at the cost of a length-26 tuple overhead.

Illustration for ["eat", "tea", "tan", "ate", "nat", "bat"]:

str    sorted_key   bucket
─────  ───────────  ─────────────────────
"eat"   "aet"  ──┐
"tea"   "aet"  ──┤───►  bucket "aet" = ["eat", "tea", "ate"]
"ate"   "aet"  ──┘
"tan"   "ant"  ──┐
"nat"   "ant"  ──┤───►  bucket "ant" = ["tan", "nat"]
"bat"   "abt"  ──────►  bucket "abt" = ["bat"]

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        groups: dict[str, list[str]] = defaultdict(list)
        for s in strs:
            key = ''.join(sorted(s))
            groups[key].append(s)
        return list(groups.values())


class SolutionCount:
    """Signature = tuple of 26 counts — no sort needed."""

    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        groups: dict[tuple, list[str]] = defaultdict(list)
        for s in strs:
            count = [0] * 26
            for ch in s:
                count[ord(ch) - ord('a')] += 1
            groups[tuple(count)].append(s)
        return list(groups.values())

Complexity

Approach Time Space
sorted-key O(n · k log k) O(n·k)
count-key O(n · k) O(n·k)

where n = number of strings, k = string length.

Comments


2.6 Reverse Words in a String (LC 151)

Problem

Given a string s containing multiple words separated by at least one space, reverse the order of words and return the result such that:

Example

Input:  s = "the sky is blue"
Output: "blue is sky the"

Input:  s = "  hello world  "
Output: "hello world"   (compress leading/trailing + inner)

Input:  s = "a good   example"
Output: "example good a"   (collapse multiple inner spaces into one)

Constraints

Clarifying questions

Approach

Approach 1 — Pythonic split-reverse-join, O(n). return ' '.join(reversed(s.split())). s.split() with no argument automatically collapses runs of whitespace and drops leading / trailing spaces — exactly what we need.

Approach 2 — Three Reverses (in place on a character array). Apply the same idea as Rotate Array (problem 1.6): 1. Reverse the entire string. 2. Reverse each “word” inside the reversed string. 3. Clean up spaces (keep only one space between words; remove leading / trailing).

Illustration for s = "the sky is blue":

Input               :  "the sky is blue"

Step 1: reverse the whole string
                       "eulb si yks eht"

Step 2: reverse each word inside the reversed string
                       "blue is sky the"   ← answer ✓

Comparison with Rotate Array: Rotate Array reverses at the granularity of individual elements; Reverse Words reverses at the granularity of words (substrings between spaces). Same idea, different abstraction level.

Code (Python 3)

class Solution:
    """Approach 1 — Pythonic, shortest."""

    def reverseWords(self, s: str) -> str:
        return ' '.join(reversed(s.split()))


class SolutionInPlace:
    """Approach 2 — three reverses, in place on a character list."""

    def reverseWords(self, s: str) -> str:
        chars = list(s.strip())  # Python strings are immutable → convert to list

        # 1. Reverse the entire array.
        self._reverse(chars, 0, len(chars) - 1)

        # 2. Reverse each word.
        start = 0
        for i in range(len(chars) + 1):
            if i == len(chars) or chars[i] == ' ':
                self._reverse(chars, start, i - 1)
                start = i + 1

        # 3. Collapse multiple spaces between words.
        return self._collapse_spaces(chars)

    @staticmethod
    def _reverse(arr: list, l: int, r: int) -> None:
        while l < r:
            arr[l], arr[r] = arr[r], arr[l]
            l += 1
            r -= 1

    @staticmethod
    def _collapse_spaces(chars: list) -> str:
        out, prev_space = [], False
        for ch in chars:
            if ch == ' ':
                if not prev_space and out:
                    out.append(' ')
                prev_space = True
            else:
                out.append(ch)
                prev_space = False
        if out and out[-1] == ' ':
            out.pop()
        return ''.join(out)

Complexity

Comments

Chapter summary & decisions

Character assumptions — clarify them before you write code

  1. Alphabet: lowercase a–z (26)? ASCII 128? Unicode? — The [26] counting array only works when the alphabet is exactly 26 letters.
  2. Case sensitivity? — is "Aa" a palindrome? LC 125 lowercases first; LC 5 does not.
  3. Non-alphanumeric characters? — Filter with isalnum(), or does the problem guarantee a clean input?
  4. Leading / trailing whitespace? — Call strip() before parsing numbers.

String pattern map

Pattern When to use Chapter
Counting (Counter, [26]) Anagrams, frequency 02, 06
Two pointers (in / out) Palindrome, reverse 02, 26
Sliding window Substring under a dynamic constraint 27
Parsing with stack / FSM atoi, calculator, Decode 08, 32
Pattern matching strStr, anagrams in text 35, 36, 34
String hashing Rabin-Karp, distinct substrings 34

Group Anagrams — how to choose the key?

Bridge to Chapter 32 (String Parser)

LC 8 (atoi) is a small FSM with 4 states: start, sign, digits, overflow. When problems get more complex (Valid Number, Calculator) → see Chapter 32.


Chapter 3 — Recursion

Recursion is the natural language for problems with a self-similar structure — solving a large problem by combining the solutions of a few smaller subproblems. This chapter teaches you to feel the three components of a recursive solution: (i) the base case (stopping condition), (ii) the recursive case (calling smaller subproblems), (iii) the combination of subproblem results. Once these three click, you will see DP, Backtracking, Trees, and Graph DFS as dialects of the same language.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Three questions to answer before coding any recursion: 1. What variables make up the state of the function? (Enough to fully define a subproblem.) 2. What is the base case? (When do we return immediately?) 3. What does the recursive step look like — how do we split the problem and combine the results?

Template code

from functools import cache

# 1) "Pure" recursion (can be slow because subproblems repeat).
def recurse(state):
    if base_condition(state):
        return base_value
    result = combine(recurse(subproblem_1(state)),
                     recurse(subproblem_2(state)))
    return result


# 2) Top-down DP: cache for O(#states) total work.
@cache
def f(*state):
    if base_condition(*state):
        return base_value
    return combine(f(*sub1(*state)), f(*sub2(*state)))


# 3) Backtracking: enumerate + undo.
def backtrack(path, choices):
    if is_solution(path):
        results.append(path.copy())
        return
    for c in choices:
        if not valid(c, path):
            continue
        path.append(c)
        backtrack(path, next_choices(choices, c))
        path.pop()              # undo — the signature of backtracking

End-of-chapter practice


3.1 Fibonacci Number (LC 509)

Problem

Compute the n-th Fibonacci number, defined by F(0) = 0, F(1) = 1, and F(n) = F(n-1) + F(n-2) for n >= 2.

Example

Input:  n = 2  → 1
Input:  n = 3  → 2
Input:  n = 10 → 55

Constraints

Clarifying questions

Approach

Approach 1 — Pure recursion, O(2^n). Each F(n) triggers two recursive calls. The call tree has ~2^n nodes → very slow.

Approach 2 — Memoisation (top-down DP), O(n) time, O(n) space. Cache results → each F(k) is computed exactly once.

Approach 3 — Iterative (bottom-up), O(n) time, O(1) space. Keep two rolling variables prev, curr.

Approach 4 — Matrix exponentiation, O(log n). When n reaches 10^18.

Illustration — call tree for F(5):

                F(5)
              /      \
          F(4)        F(3)
         /    \      /    \
       F(3)  F(2) F(2)   F(1)
       /  \   / \  / \
     F(2) F(1)...   ...   ← F(3), F(2), F(1) are RECOMPUTED many times

→ With memoisation, only n+1 unique calls are made (n=5 → 6 calls).
→ Without memoisation, total calls ~ Fibonacci(n+1) ~ φ^n (exponential).

Code (Python 3)

from functools import cache

class Solution:
    """Approach 3 — iterative O(1) space, the production answer."""
    def fib(self, n: int) -> int:
        if n < 2:
            return n
        prev, curr = 0, 1
        for _ in range(2, n + 1):
            prev, curr = curr, prev + curr
        return curr


class SolutionMemo:
    """Approach 2 — top-down DP."""
    @cache
    def fib(self, n: int) -> int:
        if n < 2:
            return n
        return self.fib(n - 1) + self.fib(n - 2)


class SolutionMatrix:
    """Approach 4 — matrix exponentiation, O(log n)."""
    MOD = 10**9 + 7

    def fib(self, n: int) -> int:
        if n < 2:
            return n
        # [[F(n+1), F(n)], [F(n), F(n-1)]] = [[1,1],[1,0]] ^ n
        result, base = [[1, 0], [0, 1]], [[1, 1], [1, 0]]
        while n > 0:
            if n & 1:
                result = self._mul(result, base)
            base = self._mul(base, base)
            n >>= 1
        return result[0][1]  # F(n)

    def _mul(self, a, b):
        return [[(a[0][0]*b[0][0] + a[0][1]*b[1][0]) % self.MOD,
                 (a[0][0]*b[0][1] + a[0][1]*b[1][1]) % self.MOD],
                [(a[1][0]*b[0][0] + a[1][1]*b[1][0]) % self.MOD,
                 (a[1][0]*b[0][1] + a[1][1]*b[1][1]) % self.MOD]]

Complexity

Comments


3.2 Power(x, n) (LC 50)

Problem

Implement a function that computes x^n where x is a real number and n is an integer (possibly negative).

Example

Input:  x = 2.00000, n = 10     → 1024.00000
Input:  x = 2.10000, n = 3      → 9.26100
Input:  x = 2.00000, n = -2     → 0.25

Constraints

Clarifying questions

Approach

Approach 1 — Multiply by hand, O(n). Loop n times. TLEs at n = 2^31.

Approach 2 — Fast Power (recursive / iterative), O(log n).

Recursive observation: - If n == 0: return 1. - If n is even: x^n = (x^(n/2))^2. - If n is odd: x^n = x · x^(n-1).

Handling negative n: recurse with -n then invert.

Illustration — call tree for x^10:

                  x^10
                   │ even → (x^5)^2
                   ▼
                  x^5
                   │ odd  → x · x^4
                   ▼
                  x^4
                   │ even → (x^2)^2
                   ▼
                  x^2
                   │ even → (x^1)^2
                   ▼
                  x^1
                   │ odd  → x · x^0
                   ▼
                  x^0 = 1

Total multiplications: ~ 2 log₂(10) ≈ 8  (vs. 10 for brute force)

Code (Python 3)

class Solution:
    """Recursive fast power."""

    def myPow(self, x: float, n: int) -> float:
        if n == 0:
            return 1.0
        if n < 0:
            return 1.0 / self.myPow(x, -n)
        half = self.myPow(x, n // 2)
        return half * half if n % 2 == 0 else half * half * x


class SolutionIter:
    """Iterative — avoids recursion depth. Reads bits from low to high."""

    def myPow(self, x: float, n: int) -> float:
        if n < 0:
            x, n = 1.0 / x, -n
        result = 1.0
        base = x
        while n > 0:
            if n & 1:
                result *= base
            base *= base
            n >>= 1
        return result

Complexity

Comments


3.3 Reverse Linked List (recursive) (LC 206)

Problem

Given the head of a singly linked list, reverse the list and return the new head. (There are two classic approaches: iterative and recursive. This chapter focuses on the recursive version; the iterative version returns in Chapter 7.)

Example

Input:  head = 1 → 2 → 3 → 4 → 5 → None  (singly linked list)
Output: 5 → 4 → 3 → 2 → 1 → None

Input:  head = None     (empty list)
Output: None

Constraints

Clarifying questions

Approach

Recursive idea: - Base case: if head is None or head.next is None → return head. - Recursive step: call reverseList(head.next) to reverse the tail; we receive the new last node (which is the original tail → the new head of the reversed list). - Combination: at this point head.next still points to the original node right after head (recursion only operated on the tail). Set head.next.next = head and head.next = None to stitch head to the end of the reversed list.

Illustration with 1 → 2 → 3 → None:

Call reverseList(1):
  reverseList(2):
    reverseList(3):
      base case → return 3              #  3 → None
    # here head=2, head.next=3
    # tail is reversed: 3 → None
    # stitch 2 after 3:
    head.next.next = head                #  3 → 2
    head.next = None                     #  2 → None
    # chain so far: 3 → 2 → None, new head = 3
    return 3
  # here head=1, head.next=2
  # tail is reversed: 3 → 2 → None
  # stitch 1 after 2:
  head.next.next = head                  #  2 → 1
  head.next = None                       #  1 → None
  # chain: 3 → 2 → 1 → None
  return 3

Code (Python 3)

class ListNode:
    def __init__(self, val: int = 0, next: 'ListNode | None' = None):
        self.val = val
        self.next = next


class Solution:
    def reverseList(self, head: ListNode | None) -> ListNode | None:
        if head is None or head.next is None:
            return head
        new_head = self.reverseList(head.next)
        head.next.next = head
        head.next = None
        return new_head

Complexity

Comments

prev = None
while head:
    nxt = head.next
    head.next = prev
    prev = head
    head = nxt
return prev

3.4 Generate Parentheses (LC 22)

Problem

Given an integer n, generate all well-formed parenthesis strings of length 2n.

Example

Input:  n = 3
Output: ["((()))", "(()())", "(())()", "()(())", "()()()"]

Input:  n = 1
Output: ["()"]

Constraints

Clarifying questions

Approach

Idea: generate the characters ( and ) one at a time. Each step has 2 choices, constrained by validity: - Number of ( placed must not exceed n. - Number of ) placed must not exceed the number of ( placed (otherwise an unmatched close-paren appears).

Recurse with two counters: open_count, close_count. When len(path) == 2n → push to results.

Illustration — decision tree for n = 2:

                       ""
                     /    \
                ( /        \ )  ✗ (close > open)
                   "("
                  /   \
              ( /      \ )
              "(("    "()"
               │        │
            ) ▼     ( / \ )  ✗
              "(()"   "()("
               │       │
            ) ▼     ) ▼
              "(())" ★  "()()" ★

Answer: ["(())", "()()"]
(✗ = branch pruned because invalid)

Code (Python 3)

from typing import List

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        result: list[str] = []

        def backtrack(path: list[str], open_cnt: int, close_cnt: int) -> None:
            if len(path) == 2 * n:
                result.append(''.join(path))
                return
            if open_cnt < n:
                path.append('(')
                backtrack(path, open_cnt + 1, close_cnt)
                path.pop()
            if close_cnt < open_cnt:
                path.append(')')
                backtrack(path, open_cnt, close_cnt + 1)
                path.pop()

        backtrack([], 0, 0)
        return result

Complexity

Comments


3.5 Permutations (LC 46)

Problem

Given an array nums of distinct integers, return all possible permutations of them.

Example

Input:  nums = [1, 2, 3]
Output: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

Constraints

Clarifying questions

Approach

Idea: at each step, pick an unused number and push it into path. When path reaches n elements → record one complete permutation.

Two ways to track “used”: - A used: bool[n] array. - A set of used indices.

Illustration — decision tree for [1, 2, 3]:

                          [ ]
              ┌────────────┼────────────┐
            [1]           [2]          [3]
           /   \         /   \         /  \
        [1,2] [1,3]   [2,1] [2,3]   [3,1] [3,2]
          │     │       │     │       │     │
       [1,2,3][1,3,2] [2,1,3][2,3,1][3,1,2][3,2,1]

Total: 3 · 2 · 1 = 6 permutations.

Code (Python 3)

from typing import List

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        result: list[list[int]] = []
        n = len(nums)
        used = [False] * n
        path: list[int] = []

        def backtrack() -> None:
            if len(path) == n:
                result.append(path.copy())
                return
            for i in range(n):
                if used[i]:
                    continue
                used[i] = True
                path.append(nums[i])
                backtrack()
                path.pop()
                used[i] = False

        backtrack()
        return result


class SolutionSwap:
    """Approach 2 — swap in place, no used array required."""

    def permute(self, nums: List[int]) -> List[List[int]]:
        result: list[list[int]] = []

        def backtrack(start: int) -> None:
            if start == len(nums):
                result.append(nums.copy())
                return
            for i in range(start, len(nums)):
                nums[start], nums[i] = nums[i], nums[start]
                backtrack(start + 1)
                nums[start], nums[i] = nums[i], nums[start]  # undo

        backtrack(0)
        return result

Complexity

Comments


3.6 Subsets (LC 78)

Problem

Given an array nums of distinct integers, return all possible subsets of nums (including the empty set and the full set), totalling 2^n subsets.

Example

Input:  nums = [1, 2, 3]
Output: [[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]]

Input:  nums = [0]
Output: [[], [0]]

Constraints

Clarifying questions

Approach

There are three classic approaches, all worth knowing.

Approach 1 — Backtracking “pick / skip”. At each index i, branch into two: include nums[i] in path or skip it.

Approach 2 — Backtracking “start-index”. Every node in the recursion tree calls result.append(path.copy()) (every prefix is a valid subset), then loops for i in range(start, n).

Approach 3 — Bitmask iteration. Each number from 0 to 2^n - 1 represents one subset: bit i set ↔︎ nums[i] is in the subset. (See Chapter 21.)

Illustration — “pick / skip” tree for [1, 2, 3]:

                            [ ]
                skip 1 /          \ pick 1
                  [ ]             [1]
            skip 2 / \ pick 2  skip 2 / \ pick 2
                [ ] [2]            [1] [1,2]
          skip3/\ ... ...          ...  ...
           [ ] [3]

Each LEAF = one subset. The tree has 2^3 = 8 leaves.

Code (Python 3)

from typing import List

class Solution:
    """Approach 2 — every prefix is a subset."""

    def subsets(self, nums: List[int]) -> List[List[int]]:
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(start: int) -> None:
            result.append(path.copy())          # every state is a subset
            for i in range(start, len(nums)):
                path.append(nums[i])
                backtrack(i + 1)
                path.pop()

        backtrack(0)
        return result


class SolutionBitmask:
    """Approach 3 — iterate through 2^n bitmasks."""

    def subsets(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        result = []
        for mask in range(1 << n):
            subset = [nums[i] for i in range(n) if mask & (1 << i)]
            result.append(subset)
        return result


class SolutionPick:
    """Approach 1 — pick / skip backtracking."""

    def subsets(self, nums: List[int]) -> List[List[int]]:
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(i: int) -> None:
            if i == len(nums):
                result.append(path.copy())
                return
            # branch: skip nums[i]
            backtrack(i + 1)
            # branch: pick nums[i]
            path.append(nums[i])
            backtrack(i + 1)
            path.pop()

        backtrack(0)
        return result

Complexity

Comments

Chapter summary & decisions

Recursion vs DFS vs Backtracking vs Top-down DP

Property Recursion DFS Backtracking Top-down DP
Goal Self-call to solve a subproblem Traverse a graph/tree Enumerate every solution Optimal value / count
Need undo? Optional Rare Required (choose/unchoose) No
Need memo? Sometimes Rarely Rarely (state depends on path) Required
Examples Factorial, Fibonacci Number of Islands Permutations, N-Queens LCS, Coin Change

Backtracking mantra

def backtrack(path, choices):
    if is_goal(path):
        record(path); return
    for c in choices:
        if not feasible(path, c): continue
        path.append(c)          # choose
        backtrack(path, ...)    # explore
        path.pop()              # unchoose

Python recursion caveats


Chapter 4 — Sorting

Sort is itself a solved problem. This chapter does not teach you to implement quicksort — Python already ships an excellent sorted() (Timsort, worst-case O(n log n), stable). What you really need to learn is when sorting is the enabler that solves the problem, and the secret lies in custom comparators and the techniques that scan a sorted array with two pointers / sweep line.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Three golden questions: 1. Sort by which key? start, end, length, frequency, ratio? 2. How do we sweep after sorting? one-pass / two pointers / sweep line / heap? 3. Do we need stability? Python’s sorted is stable by default — a valuable asset.

Template code

from functools import cmp_to_key
from typing import List

# 1) Sort by a simple key
nums.sort(key=lambda x: x[0])

# 2) Sort by multiple keys (tie-breaker)
nums.sort(key=lambda x: (x[0], -x[1]))    # x[0] ascending, x[1] descending

# 3) Sort with a custom comparator
def cmp(a, b) -> int:
    if a + b > b + a:   return -1   # a comes first
    if a + b < b + a:   return  1   # b comes first
    return 0
arr.sort(key=cmp_to_key(cmp))

# 4) Sweep line over an event array
events = [(start, +1), (end, -1)]
events.sort()

End-of-chapter practice


4.1 Sort Colors / Dutch National Flag (LC 75)

Problem

Given an array nums containing only the values 0, 1, and 2 (representing 3 colours), rearrange it so equal colours are adjacent in the order 0 → 1 → 2. You must do it in place, without using the language’s sort function.

Example

Input:  nums = [2, 0, 2, 1, 1, 0]
Output: [0, 0, 1, 1, 2, 2]

Input:  nums = [2, 0, 1]
Output: [0, 1, 2]

Constraints

Clarifying questions

Approach

Approach 1 — Two-pass counting sort, O(n). Count the number of 0s, 1s, 2s, then overwrite. Simple but two passes.

Approach 2 — Dutch National Flag (Edsger Dijkstra), one pass, O(n).

Maintain 3 pointers: - lo = the right boundary of the 0s region (everything in [0..lo-1] is 0). - hi = the left boundary of the 2s region (everything in [hi+1..n-1] is 2). - mid = the scanning pointer between the two regions.

Invariant: [0..lo-1] = 0, [lo..mid-1] = 1, [mid..hi] unprocessed, [hi+1..n-1] = 2.

At each step: - nums[mid] == 0 → swap with nums[lo], lo++, mid++. - nums[mid] == 1 → already in the correct region, mid++. - nums[mid] == 2 → swap with nums[hi], hi-- (do not increment mid because the value that just came from hi has not been processed).

Illustration for nums = [2, 0, 2, 1, 1, 0]:

                  lo  mid          hi
Init       :  [ 2,  0,  2,  1,  1,  0 ]
                ↑   ↑                ↑
              lo=0 mid=0           hi=5

mid=0: nums[0]=2 → swap(0,5), hi--
              [ 0,  0,  2,  1,  1,  2 ]
                ↑   ↑           ↑
              lo=0 mid=0      hi=4

mid=0: nums[0]=0 → swap(lo,mid)=swap(0,0), lo++, mid++
              [ 0,  0,  2,  1,  1,  2 ]
                    ↑   ↑       ↑
                  lo=1 mid=1  hi=4

mid=1: nums[1]=0 → swap(1,1), lo++, mid++
              [ 0,  0,  2,  1,  1,  2 ]
                        ↑   ↑   ↑
                       lo=2 mid=2 hi=4

mid=2: nums[2]=2 → swap(2,4), hi--
              [ 0,  0,  1,  1,  2,  2 ]
                        ↑   ↑   ↑
                       lo=2 mid=2 hi=3

mid=2: nums[2]=1 → mid++
              [ 0,  0,  1,  1,  2,  2 ]
                        ↑       ↑
                       lo=2 mid=3 hi=3

mid=3: nums[3]=1 → mid++
              [ 0,  0,  1,  1,  2,  2 ]
                        ↑           ↑
                       lo=2  mid=4 hi=3   ← mid > hi → stop

Result : [0, 0, 1, 1, 2, 2]  ✓

Code (Python 3)

from typing import List

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        lo, mid, hi = 0, 0, len(nums) - 1
        while mid <= hi:
            if nums[mid] == 0:
                nums[lo], nums[mid] = nums[mid], nums[lo]
                lo += 1
                mid += 1
            elif nums[mid] == 1:
                mid += 1
            else:  # nums[mid] == 2
                nums[mid], nums[hi] = nums[hi], nums[mid]
                hi -= 1
                # DO NOT increment mid — we have not seen what just came from hi

Complexity

Comments


4.2 Merge Intervals (LC 56)

Problem

Given an array of intervals intervals[i] = [start_i, end_i], merge all overlapping intervals into non-overlapping ones and return the result.

Example

Input:  intervals = [[1,3], [2,6], [8,10], [15,18]]
Output: [[1,6], [8,10], [15,18]]
Explanation: [1,3] and [2,6] overlap → merged into [1,6].

Input:  intervals = [[1,4], [4,5]]
Output: [[1,5]]
Explanation: [1,4] and [4,5] touch at point 4 → counted as overlapping.

Constraints

Clarifying questions

Approach

Brute force. Repeatedly find an overlapping pair and merge. O(n²) or worse.

Optimal — Sort + one pass — O(n log n).

Sort by start ascending. Sweep through, keeping last = the most recently appended interval. For each new cur: - If cur.start <= last.end → overlap; extend last.end = max(last.end, cur.end). - Otherwise → push cur as a new interval.

Illustration for [[1,3], [2,6], [8,10], [15,18]]:

Number line:
   1   3   5   7   9  11  13  15  17  19
   |   |   |   |   |   |   |   |   |   |
   ├───┤                                       [1,3]
       ├──────────┤                            [2,6]
                       ├───┤                   [8,10]
                                       ├───┤   [15,18]

After sorting by start: [[1,3], [2,6], [8,10], [15,18]]

Sweep:
  Push [1,3]                              result = [[1,3]]
  cur=[2,6], 2 <= 3 → extend [1, max(3,6)] = [1,6]
                                           result = [[1,6]]
  cur=[8,10], 8 > 6 → push                result = [[1,6], [8,10]]
  cur=[15,18], 15 > 10 → push             result = [[1,6], [8,10], [15,18]]

Result: [[1,6], [8,10], [15,18]]

Code (Python 3)

from typing import List

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort(key=lambda x: x[0])
        result: list[list[int]] = []
        for cur in intervals:
            if result and cur[0] <= result[-1][1]:
                result[-1][1] = max(result[-1][1], cur[1])
            else:
                result.append(cur[:])   # copy so we don't alias the input
        return result

Complexity

Comments


4.3 Largest Number (LC 179)

Problem

Given an array of non-negative integers nums, arrange them (in some order) to form a single concatenated string that represents the largest possible number. Return the result as a string (because the number can be huge).

Example

Input:  nums = [10, 2]
Output: "210"

Input:  nums = [3, 30, 34, 5, 9]
Output: "9534330"

Input:  nums = [0, 0]
Output: "0"   (not "00")

Constraints

Clarifying questions

Approach

Brute force — try every permutation, O(n! · n). TLE for any non-trivial n.

Optimal — Sort with a custom comparator — O(n log n · L) where L is the max string length.

Insight: to decide whether a should precede b, compare the two possible concatenations: str(a) + str(b) vs str(b) + str(a) — the one that is lexicographically larger wins.

Why is it valid? “Concatenation-bigger” is a transitive relation — provable rigorously via the lexicographic ordering of concatenations, which guarantees a consistent sort order exists.

Example: a = 3, b = 30"330" > "303"3 comes before 30.

Code (Python 3)

from functools import cmp_to_key
from typing import List

class Solution:
    def largestNumber(self, nums: List[int]) -> str:
        strs = [str(x) for x in nums]

        def cmp(a: str, b: str) -> int:
            if a + b > b + a:   return -1   # a first
            if a + b < b + a:   return  1   # b first
            return 0

        strs.sort(key=cmp_to_key(cmp))
        result = ''.join(strs)
        # edge case: [0, 0, 0] → avoid returning "000"
        return '0' if result[0] == '0' else result

Complexity

Comments


4.4 Meeting Rooms II (LC 253)

Problem

Given an array of intervals intervals[i] = [start_i, end_i] representing meetings, find the minimum number of rooms required to host them all.

(That is, at any point in time, what is the maximum number of simultaneous meetings?)

Example

Input:  intervals = [[0,30], [5,10], [15,20]]
Output: 2
Explanation: at t=5, [0,30] and [5,10] overlap → 2 rooms needed.

Input:  intervals = [[7,10], [2,4]]
Output: 1
Explanation: 2 meetings don't overlap, 1 room suffices.

Constraints

Clarifying questions

Approach

There are three solid approaches, all worth knowing:

Approach 1 — Heap (priority queue) — O(n log n).

Sort by start. Scan each meeting and maintain a min-heap of end_times for currently open meetings. When a new meeting arrives at start: - If the heap top has end <= start → the old meeting is over → pop it (reuse the room). - Push the new end onto the heap.

The heap size at any moment = number of rooms currently in use → the max of that size over time is the answer.

Approach 2 — Sweep line / chronological — O(n log n).

Build two arrays: starts (sorted) and ends (sorted). Use two pointers: if starts[i] < ends[j] → a new meeting begins before the oldest ends → need a room (rooms++, i++); otherwise → an old meeting ends, advance j.

Approach 3 — Event-driven, O(n log n).

Each meeting emits two events: (start, +1) and (end, -1). Sort all events (prefer -1 before +1 at the same time → close before open). Sweep, tracking cur and peak.

Illustration for [[0,30], [5,10], [15,20]] — heap-based:

Sort by start: [[0,30], [5,10], [15,20]]

Step 1: meeting [0,30]    heap = [30]     → rooms = 1
Step 2: meeting [5,10]    top=30 > 5 → keep; push 10  heap = [10, 30]  → rooms = 2 ★
Step 3: meeting [15,20]   top=10 <= 15 → pop 10; push 20  heap = [20, 30] → rooms = 2

Number line:
   0   5  10  15  20  25  30
   |   |   |   |   |   |   |
   ├───────────────────────┤    [0, 30] uses room A
       ├───┤                    [5, 10] uses room B
                ├───┤            [15, 20] reuses room B

Code (Python 3)

import heapq
from typing import List

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return 0
        intervals.sort(key=lambda x: x[0])
        heap: list[int] = []   # min-heap of end times
        for start, end in intervals:
            if heap and heap[0] <= start:
                heapq.heappop(heap)
            heapq.heappush(heap, end)
        return len(heap)


class SolutionEvents:
    """Event-driven — clean for follow-ups."""

    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        events = []
        for s, e in intervals:
            events.append((s, +1))
            events.append((e, -1))
        # On a tie: -1 before +1 (close room before opening a new one)
        events.sort(key=lambda x: (x[0], x[1]))

        cur = peak = 0
        for _, delta in events:
            cur += delta
            peak = max(peak, cur)
        return peak

Complexity

Comments


4.5 Custom Sort String (LC 791)

Problem

Given two strings order (containing distinct characters) and s, rearrange s so that the order of characters appearing in order is respected. Characters of s not present in order may be placed anywhere in the result.

Example

Input:  order = "cba", s = "abcd"
Output: "cbad"
Explanation: in order, c < b < a. The character 'd' does not appear in order
            and can be placed anywhere.

Input:  order = "bcafg", s = "abcd"
Output: "bcad"

Constraints

Clarifying questions

Approach

Approach 1 — Sort by an index table comparator, O(|s| log |s|).

Build a dict priority = {ch: i for i, ch in enumerate(order)}. Characters not in order get a very large priority (e.g. 26). Sort s by this priority.

Approach 2 — Counter + emit in order, O(|s|).

Compute Counter(s), then iterate over each character in order and “emit” it the right number of times. Finally append the remaining characters (those not in order).

Approach 2 needs no sort, is faster, and feels very natural — interviewers typically expect this version.

Code (Python 3)

from collections import Counter

class Solution:
    """Approach 2 — Counter + emit in order."""

    def customSortString(self, order: str, s: str) -> str:
        cnt = Counter(s)
        parts: list[str] = []
        # 1. Characters that belong to `order`, in the exact order.
        for ch in order:
            if ch in cnt:
                parts.append(ch * cnt.pop(ch))
        # 2. Remaining characters (not in `order`) — order doesn't matter.
        for ch, c in cnt.items():
            parts.append(ch * c)
        return ''.join(parts)


class SolutionSort:
    """Approach 1 — comparator by index table."""

    def customSortString(self, order: str, s: str) -> str:
        priority = {ch: i for i, ch in enumerate(order)}
        return ''.join(sorted(s, key=lambda ch: priority.get(ch, 26)))

Complexity

Approach Time Space
Counter O(|s| + |order|) O(1)
Sort key O(|s| log |s|) O(|s|)

Comments


4.6 Wiggle Sort (LC 280)

Problem

Given nums, rearrange it so that:

nums[0] <= nums[1] >= nums[2] <= nums[3] >= nums[4] <= ...

Odd indices are always >= their left and right neighbours.

Example

Input:  nums = [3, 5, 2, 1, 6, 4]
Output: [3, 5, 1, 6, 2, 4]   (one of many valid answers)

Input:  nums = [6, 6, 5, 6, 3, 8]
Output: [6, 6, 5, 6, 3, 8]   (already satisfies)

Constraints

Clarifying questions

Approach

Approach 1 — Sort then swap pairs, O(n log n). Sort ascending, then swap each pair (i, i+1) for odd i. Correct but suboptimal.

Approach 2 — Greedy one pass, O(n).

Observation: at each index i, - If i is odd (1, 3, 5, …): nums[i] >= nums[i-1]. - If i is even (2, 4, 6, …): nums[i] <= nums[i-1].

Sweep from i = 1. On violation, swap nums[i] and nums[i-1]. Why does the swap not break the previous relation? We only modify the element at index i-1 (making it smaller or larger), and the relation between nums[i-2] and nums[i-1] from the previous step already enforces a “safe boundary”.

Illustration for nums = [3, 5, 2, 1, 6, 4]:

Index :   0   1   2   3   4   5
Input :  [3,  5,  2,  1,  6,  4]
            odd even odd even odd
            ≥   ≤    ≥   ≤    ≥

i=1 (odd) :  want nums[1] >= nums[0]   5 >= 3 ✓
i=2 (even):  want nums[2] <= nums[1]   2 <= 5 ✓
i=3 (odd) :  want nums[3] >= nums[2]   1 >= 2 ✗ → swap
           [3, 5, 1, 2, 6, 4]
i=4 (even):  want nums[4] <= nums[3]   6 <= 2 ✗ → swap
           [3, 5, 1, 6, 2, 4]
i=5 (odd) :  want nums[5] >= nums[4]   4 >= 2 ✓

Result : [3, 5, 1, 6, 2, 4]  ✓

Code (Python 3)

from typing import List

class Solution:
    def wiggleSort(self, nums: List[int]) -> None:
        for i in range(1, len(nums)):
            should_be_greater = (i % 2 == 1)
            if (should_be_greater and nums[i] < nums[i - 1]) or \
               (not should_be_greater and nums[i] > nums[i - 1]):
                nums[i], nums[i - 1] = nums[i - 1], nums[i]

Complexity

Comments

Chapter summary & decisions

What sorting buys / costs you

Buys: - Brings order to monotonic, unlocking two-pointer, binary search, sweep. - Groups “similar” elements together (anagram, intervals).

Costs: - Loses the original index → if the output requires indices, store (value, idx) first. - Mutates the input — clarify with the interviewer before sorting. - O(n log n), not free.

Largest Number (LC 179) — comparator pitfalls

Meeting Rooms II — heap vs sweep

Heap Sweep line
Mental model Which room frees up earliest → reuse Count overlap at each timestamp
Code heapq + sort by start Sort events (time, ±1)
Output Max rooms Max rooms
Extending Easy to also return the schedule (which room when) Hard to return the schedule

Wiggle Sort: LC 280 vs LC 324


Chapter 5 — Binary Search

Binary Search looks simple — “halve a sorted array” — yet in interviews it is the number-one bug magnet. Knuth famously wrote: “although the idea is simple, getting it right is harder than it looks”. This chapter teaches you one single template that applies to every variant: find equal, find boundary, search on rotated, search on the answer, … — we return to it in Chapter 25 (Advanced Binary Search).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Standard thought template: 1. What is the search space? (index, value, the answer itself). 2. What does check(mid) return as True/False? Is it monotonic? 3. Which boundary is the answer? First True or last False?

Template code

def lower_bound(nums: list[int], target: int) -> int:
    """Return the first index where nums[i] >= target. Returns len(nums) if absent."""
    lo, hi = 0, len(nums)        # [lo, hi)  — half-open
    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] < target:
            lo = mid + 1
        else:
            hi = mid
    return lo


def upper_bound(nums: list[int], target: int) -> int:
    """Return the first index where nums[i] > target."""
    lo, hi = 0, len(nums)
    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] <= target:
            lo = mid + 1
        else:
            hi = mid
    return lo


def binary_search_answer(check, lo: int, hi: int) -> int:
    """Find the smallest value in [lo, hi] for which check(x)=True (check is F→T monotonic)."""
    while lo < hi:
        mid = (lo + hi) // 2
        if check(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

Final tip: I always use the half-open [lo, hi) interval and the loop condition lo < hi. This convention matches Python’s bisect and has fewer off-by-one bugs than lo <= hi.

End-of-chapter practice


5.1 Binary Search (LC 704)

Problem

Given a sorted (ascending) array nums and a number target, return the index of target in nums, or -1 if absent. Must run in O(log n).

Example

Input:  nums = [-1, 0, 3, 5, 9, 12], target = 9
Output: 4

Input:  nums = [-1, 0, 3, 5, 9, 12], target = 2
Output: -1

Constraints

Clarifying questions

Approach

Brute force — O(n). Linear scan — fails the O(log n) requirement.

Optimal — half-open Binary Search, O(log n).

Maintain [lo, hi). Each iteration: - mid = (lo + hi) // 2. - If nums[mid] == target → return mid. - If nums[mid] < target → answer (if any) is in [mid+1, hi)lo = mid + 1. - If nums[mid] > target → answer is in [lo, mid)hi = mid.

Illustration for nums = [-1, 0, 3, 5, 9, 12], target = 9:

index :     0    1    2    3    4    5
nums  :  [ -1,   0,   3,   5,   9,  12 ]

Iter 1: lo=0, hi=6  → mid=3, nums[3]=5 < 9 → lo = mid+1 = 4
Iter 2: lo=4, hi=6  → mid=5, nums[5]=12 > 9 → hi = mid = 5
Iter 3: lo=4, hi=5  → mid=4, nums[4]=9 == 9 → return 4  ✓

Code (Python 3)

from typing import List

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        lo, hi = 0, len(nums)    # [lo, hi)
        while lo < hi:
            mid = (lo + hi) // 2
            if nums[mid] == target:
                return mid
            if nums[mid] < target:
                lo = mid + 1
            else:
                hi = mid
        return -1

Complexity

Comments


5.2 Search Insert Position (LC 35)

Problem

Given a sorted (no duplicates) array nums and target, return: - The index of target if present. - The position where target would be inserted to keep nums sorted.

Must run in O(log n).

Example

Input:  nums = [1, 3, 5, 6], target = 5   → 2
Input:  nums = [1, 3, 5, 6], target = 2   → 1
Input:  nums = [1, 3, 5, 6], target = 7   → 4   (append at end)
Input:  nums = [1, 3, 5, 6], target = 0   → 0   (prepend)

Constraints

Clarifying questions

Approach

This is exactly lower_bound. The first index i such that nums[i] >= target — semantically: “this is where target would sit on insertion”.

Code (Python 3)

from typing import List

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        lo, hi = 0, len(nums)
        while lo < hi:
            mid = (lo + hi) // 2
            if nums[mid] < target:
                lo = mid + 1
            else:
                hi = mid
        return lo

Complexity

Comments


5.3 First Bad Version (LC 278)

Problem

You have n versions labelled 1 to n. There is a “bad” version, and every version after it is also bad. You can call an API isBadVersion(int) returning True/False. Find the first bad version with the fewest API calls.

Example

n = 5, bad version = 4
call isBadVersion(3) → False
call isBadVersion(5) → True
call isBadVersion(4) → True
→ return 4

Constraints

Clarifying questions

Approach

This is search on a monotonic predicate. Perfect for the binary_search_answer template:

Illustration for n = 7, bad = 4:

version :  1     2     3     4     5     6     7
check  :   F     F     F     T     T     T     T
                              ↑
                       want this position

Iter 1: lo=1, hi=7  → mid=4, check(4)=T → hi=4
Iter 2: lo=1, hi=4  → mid=2, check(2)=F → lo=3
Iter 3: lo=3, hi=4  → mid=3, check(3)=F → lo=4
lo == hi → return lo = 4  ✓

Code (Python 3)

def isBadVersion(v: int) -> bool: ...     # provided API

class Solution:
    def firstBadVersion(self, n: int) -> int:
        lo, hi = 1, n      # closed interval [lo, hi]
        while lo < hi:
            mid = lo + (hi - lo) // 2      # overflow-safe in other languages
            if isBadVersion(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Complexity

Comments


5.4 Find First and Last Position of Element (LC 34)

Problem

Given a sorted (ascending, with possible duplicates) array nums and target, return [first, last] — the first and last positions of target in nums. Return [-1, -1] if absent. Must run in O(log n).

Example

Input:  nums = [5, 7, 7, 8, 8, 10], target = 8
Output: [3, 4]

Input:  nums = [5, 7, 7, 8, 8, 10], target = 6
Output: [-1, -1]

Input:  nums = [], target = 0
Output: [-1, -1]

Constraints

Clarifying questions

Approach

Use two binary searches: - first = lower_bound(target) — first index >= target. - last = upper_bound(target) - 1 — last index <= target.

Then sanity-check first is valid and nums[first] == target.

Illustration for nums = [5, 7, 7, 8, 8, 10], target = 8:

index :     0    1    2    3    4    5
nums  :  [  5,   7,   7,   8,   8,  10 ]
                          ↑    ↑
                       first  last

lower_bound(8) = 3   (first index >= 8)
upper_bound(8) = 5   (first index > 8)
last           = 4   (= upper - 1)

Return [3, 4]

Code (Python 3)

from typing import List

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        def lower_bound(t: int) -> int:
            lo, hi = 0, len(nums)
            while lo < hi:
                mid = (lo + hi) // 2
                if nums[mid] < t:
                    lo = mid + 1
                else:
                    hi = mid
            return lo

        first = lower_bound(target)
        if first == len(nums) or nums[first] != target:
            return [-1, -1]
        last = lower_bound(target + 1) - 1
        return [first, last]

Complexity

Comments


5.5 Search in Rotated Sorted Array (LC 33)

Problem

Given nums, originally sorted ascending with distinct values, then rotated at an unknown pivot (rotated right k times, k unknown). Given target, return its index, or -1. Must run in O(log n).

Example

Input:  nums = [4, 5, 6, 7, 0, 1, 2], target = 0
Output: 4

Input:  nums = [4, 5, 6, 7, 0, 1, 2], target = 3
Output: -1

Input:  nums = [1], target = 0
Output: -1

Constraints

Clarifying questions

Approach

Key observation: splitting a rotated array at mid, at least one half ([lo, mid] or [mid, hi]) is truly sorted (not rotated).

Each step: 1. Compute mid. 2. If nums[mid] == target → return mid. 3. Identify the sorted half: - If nums[lo] <= nums[mid] → left half is sorted. - Otherwise → right half is sorted. 4. Check whether target lies in the sorted half (compare with <, >): - Yes → search the sorted half. - No → search the other half.

Illustration for nums = [4, 5, 6, 7, 0, 1, 2], target = 0:

index :   0    1    2    3    4    5    6
nums  :  [4,   5,   6,   7,   0,   1,   2]
          lo                            hi

Iter 1: lo=0, hi=6, mid=3, nums[mid]=7
  nums[lo]=4 <= nums[mid]=7 → left [0..3] = [4,5,6,7] sorted.
  Is target=0 in [4..7]? No (0 < 4) → go right.
  → lo = mid + 1 = 4

Iter 2: lo=4, hi=6, mid=5, nums[mid]=1
  nums[lo]=0 <= nums[mid]=1 → left [4..5] = [0,1] sorted.
  Is target=0 in [0..1]? Yes → go left.
  → hi = mid - 1 = 4

Iter 3: lo=4, hi=4, mid=4, nums[mid]=0 == target → return 4  ✓

Code (Python 3)

from typing import List

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        lo, hi = 0, len(nums) - 1        # closed interval
        while lo <= hi:
            mid = (lo + hi) // 2
            if nums[mid] == target:
                return mid

            # Is the left half [lo..mid] sorted?
            if nums[lo] <= nums[mid]:
                if nums[lo] <= target < nums[mid]:
                    hi = mid - 1     # target in left half
                else:
                    lo = mid + 1
            # Otherwise the right half [mid..hi] is sorted
            else:
                if nums[mid] < target <= nums[hi]:
                    lo = mid + 1     # target in right half
                else:
                    hi = mid - 1

        return -1

Complexity

Comments


5.6 Sqrt(x) (LC 69)

Problem

Given a non-negative integer x, return the floor of its square root — the largest integer r such that r * r <= x.

Built-in sqrt is not allowed.

Example

Input:  x = 4   → 2
Input:  x = 8   → 2   (because 2² = 4 ≤ 8 < 9 = 3²)
Input:  x = 0   → 0
Input:  x = 1   → 1

Constraints

Clarifying questions

Approach

Approach 1 — Binary search on the answer — O(log x).

Find the largest integer r such that r² <= x. Equivalent to “last True” in the monotonic sequence [T, T, ..., T, F, F, ...] (T = r² <= x).

Search range: [0, x] (or [0, x//2 + 1] to save).

Approach 2 — Newton’s Method — O(log x) with a smaller constant.

Iterate r = (r + x/r) / 2 until r² <= x < (r+1)². Convergence is quadratic — this is how sqrt is implemented in many standard libraries.

Illustration — Binary Search for x = 8:

   r :    0    1    2    3    4    5    6    7    8
  r² :    0    1    4    9   16   25   36   49   64
                    ↑
                last r with r² <= 8

Iter 1: lo=0, hi=8   mid=4, 16 > 8 → hi = 3
Iter 2: lo=0, hi=3   mid=1,  1 <= 8 → answer=1, lo=2
Iter 3: lo=2, hi=3   mid=2,  4 <= 8 → answer=2, lo=3
Iter 4: lo=3, hi=3   mid=3,  9 > 8 → hi=2  → lo > hi, stop

Return 2  ✓

Code (Python 3)

class Solution:
    """Approach 1 — binary search."""

    def mySqrt(self, x: int) -> int:
        if x < 2:
            return x
        lo, hi = 1, x // 2 + 1
        answer = 0
        while lo <= hi:
            mid = (lo + hi) // 2
            if mid * mid <= x:
                answer = mid
                lo = mid + 1
            else:
                hi = mid - 1
        return answer


class SolutionNewton:
    """Approach 2 — Newton's method, smaller constant."""

    def mySqrt(self, x: int) -> int:
        if x < 2:
            return x
        r = x
        while r * r > x:
            r = (r + x // r) // 2
        return r

Complexity

Comments

Chapter summary & decisions

Template invariants (pick one and stick with it)

Closed interval [lo, hi]:

lo, hi = 0, n - 1
while lo <= hi:
    mid = (lo + hi) // 2
    if check(mid): return mid
    elif too_small(mid): lo = mid + 1
    else: hi = mid - 1
return -1

Half-open [lo, hi) — first-true (lower_bound):

lo, hi = 0, n          # hi is NOT inclusive
while lo < hi:
    mid = (lo + hi) // 2
    if pred(mid): hi = mid
    else: lo = mid + 1
return lo                # first index where pred holds

Pre-submission checklist

  1. lo, hi are initialised correctly (especially for search on answer: lo = min, hi = max or max + 1).
  2. The loop condition matches the interval kind (<= vs <).
  3. The updates mid+1 / mid-1 / mid are correct — no infinite loop.
  4. What does the “not found” case return? -1, n, lo?
  5. (lo + hi) // 2 overflow is safe in Python; in Java/C++ use lo + (hi - lo) // 2.

Sqrt overflow note (other languages)

Python ints never overflow. Java/C++: mid * mid may overflow int32. Use (long) mid * mid or compare mid <= x / mid to avoid the multiplication.


Chapter 6 — Hash Table

Hash Table is the “magic weapon” of coding interviews: it turns many O(n²) problems into O(n). Philosophy: trade memory for time — accept O(n) extra memory in exchange for O(1) lookups. This chapter teaches you to recognise when you should and when you shouldn’t use hashing, along with 6 classic problems that frequently appear in Big Tech interviews.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

When not to use hash: - You need sorted order → use SortedSet / TreeMap (Python: sortedcontainers). - You need O(1) worst-case (not amortised) → hash is vulnerable to adversarial collisions. - Keys are complex (list, dict) → must convert to tuple / frozenset.

Template code

from collections import Counter, defaultdict
from typing import List

# 1) Counter: frequency table
cnt = Counter(nums)               # {value: count}
top3 = cnt.most_common(3)         # 3 most-common elements

# 2) defaultdict(list): group by key
groups: dict[str, list[int]] = defaultdict(list)
for i, v in enumerate(arr):
    groups[v].append(i)

# 3) Prefix sum + dict: find subarrays
prefix_index = {0: -1}            # prefix_sum -> earliest index
cur = 0
for i, x in enumerate(arr):
    cur += x
    if cur - target in prefix_index:
        # found a subarray with sum = target
        ...
    if cur not in prefix_index:
        prefix_index[cur] = i

End-of-chapter practice


6.1 Contains Duplicate (LC 217)

Problem

Given an array nums, return True if any value appears at least twice, otherwise False.

Example

Input:  nums = [1, 2, 3, 1]   → True
Input:  nums = [1, 2, 3, 4]   → False
Input:  nums = []             → False

Constraints

Clarifying questions

Approach

Brute force — O(n²). Compare every pair.

Sort — O(n log n), O(1) extra space. Sort then check adjacent elements.

Hash set — O(n) time, O(n) space — the most common answer.

Pythonic one-liner: return len(set(nums)) != len(nums).

Code (Python 3)

from typing import List

class Solution:
    def containsDuplicate(self, nums: List[int]) -> bool:
        seen: set[int] = set()
        for x in nums:
            if x in seen:
                return True
            seen.add(x)
        return False

Complexity

Comments


6.2 Longest Consecutive Sequence (LC 128)

Problem

Given an unsorted array nums, return the length of the longest sequence of consecutive integers (they need not be adjacent in the array). Must run in O(n).

Example

Input:  nums = [100, 4, 200, 1, 3, 2]
Output: 4
Explanation: the consecutive run [1, 2, 3, 4] has length 4.

Input:  nums = [0, 3, 7, 2, 5, 8, 4, 6, 0, 1]
Output: 9
Explanation: the run [0, 1, 2, 3, 4, 5, 6, 7, 8].

Input:  nums = []
Output: 0

Constraints

Clarifying questions

Approach

Brute force — O(n³). For each element, count x, x+1, x+2, ... in the array.

Sort — O(n log n). Sort, count consecutive runs. Simple but fails the O(n) target.

Optimal — Hash Set + “only start from a chain’s beginning” — O(n).

Key insight: a number x is a chain starter ↔︎ x - 1 is not in the array. Only for such x do we count the chain x, x+1, x+2, ... via hash lookup. Each element is “walked forward” at most once across all starters → total O(n).

Illustration for nums = [100, 4, 200, 1, 3, 2]:

Set: {100, 4, 200, 1, 3, 2}

Iterate over each x in the set:
  x=100: 99 not in set → starter
         Count: 100 ✓, 101 ✗  → length 1
  x=4:   3 IS in set → SKIP (will be counted starting from 1)
  x=200: 199 not in set → starter
         Count: 200 ✓, 201 ✗  → length 1
  x=1:   0 not in set → starter
         Count: 1 ✓, 2 ✓, 3 ✓, 4 ✓, 5 ✗  → length 4  ★
  x=3:   2 IS in set → SKIP
  x=2:   1 IS in set → SKIP

Total: max length = 4.

Key: each element of the chain 1-2-3-4 is "walked forward" exactly once
      (when x=1). Total work ~ O(n).

Code (Python 3)

from typing import List

class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        num_set = set(nums)
        best = 0
        for x in num_set:
            # Only start from a chain head (x-1 not in set).
            if x - 1 not in num_set:
                cur = x
                length = 1
                while cur + 1 in num_set:
                    cur += 1
                    length += 1
                best = max(best, length)
        return best

Complexity

Comments


6.3 Top K Frequent Elements (LC 347)

Problem

Given an array nums and integer k, return the k most frequent elements (output order does not matter).

Example

Input:  nums = [1, 1, 1, 2, 2, 3], k = 2
Output: [1, 2]

Input:  nums = [1], k = 1
Output: [1]

Constraints

Clarifying questions

Approach

Approach 1 — Counter + sort — O(n log n). Simple but doesn’t meet the hint.

Approach 2 — Min-heap of size k — O(n log k). Maintain a heap of size k; pop the least-frequent when it overflows.

Approach 3 — Bucket sort by frequency — O(n)the slickest.

Maximum frequency is n → create n + 1 buckets where bucket i holds elements whose frequency is i. Sweep buckets from high to low, gather k elements.

Illustration for nums = [1,1,1,2,2,3], k=2:

Step 1: Counter → {1:3, 2:2, 3:1}

Step 2: Bucket sort by frequency (n=6, 7 buckets 0..6):
  bucket[0] = []
  bucket[1] = [3]      # element 3 appears 1 time
  bucket[2] = [2]      # element 2 appears 2 times
  bucket[3] = [1]      # element 1 appears 3 times
  bucket[4..6] = []

Step 3: Walk buckets from index 6 down:
  i=6: empty
  i=5: empty
  i=4: empty
  i=3: [1] → result = [1]
  i=2: [2] → result = [1, 2]   reached k=2, stop

Output: [1, 2]

Code (Python 3)

import heapq
from collections import Counter
from typing import List

class Solution:
    """Approach 3 — bucket sort, O(n)."""

    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        cnt = Counter(nums)
        n = len(nums)
        buckets: list[list[int]] = [[] for _ in range(n + 1)]
        for x, freq in cnt.items():
            buckets[freq].append(x)
        result: list[int] = []
        for freq in range(n, 0, -1):
            for x in buckets[freq]:
                result.append(x)
                if len(result) == k:
                    return result
        return result


class SolutionHeap:
    """Approach 2 — min-heap of size k, O(n log k)."""

    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        cnt = Counter(nums)
        # heapq.nlargest uses a heap-based partial sort
        return heapq.nlargest(k, cnt.keys(), key=cnt.get)

Complexity

Approach Time Space
Counter + sort O(n log n) O(n)
Min-heap O(n log k) O(n)
Bucket sort O(n) O(n)

Comments


6.4 Subarray Sum Equals K (LC 560)

Problem

Given an integer array nums and integer k, return the number of contiguous subarrays whose sum equals k.

Example

Input:  nums = [1, 1, 1], k = 2
Output: 2
Explanation: two subarrays [1,1] (positions 0..1 and 1..2).

Input:  nums = [1, 2, 3], k = 3
Output: 2
Explanation: [1,2] and [3].

Constraints

Clarifying questions

Approach

Brute force — O(n²). For each i, accumulate a prefix sum and check == k. Acceptable but not optimal.

Optimal — Prefix sum + Hash map — O(n).

Let P[i] = sum of nums[0..i-1] (P[0] = 0). The sum of nums[j..i-1] is P[i] - P[j]. A subarray of sum k ↔︎ P[i] - P[j] = k ↔︎ P[j] = P[i] - k.

→ Walk and count j < i with P[j] == cur - k. Maintain a dict {prefix_sum: count}.

Illustration for nums = [3, 4, 7, 2, -3, 1, 4, 2], k = 7:

i    :   0    1    2    3    4    5    6    7
nums :   3    4    7    2   -3    1    4    2
P[i+1]:  3    7   14   16   13   14   18   20

Hand-trace:
P[]   = [0, 3, 7, 14, 16, 13, 14, 18, 20]
counts = {0:1}                        cur=0
i=0  cur=3   (3-7=-4 not in counts)  add 3 → {0:1, 3:1}
i=1  cur=7   (7-7= 0 in counts: +1)  add 7 → {0:1, 3:1, 7:1}   answer=1
i=2  cur=14  (14-7=7 in counts: +1)  add 14 → ...              answer=2
i=3  cur=16  (16-7=9 not in counts)                            answer=2
i=4  cur=13  (13-7=6 not in counts)                            answer=2
i=5  cur=14  (14-7=7 in counts: +1)  cur seen → counts[14]+=1
                                                              answer=3
i=6  cur=18  (18-7=11 not in counts)                          answer=3
i=7  cur=20  (20-7=13 in counts: +1)                          answer=4

Answer: 4 subarrays with sum = 7.

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        counts: dict[int, int] = defaultdict(int)
        counts[0] = 1            # prefix sum 0 has occurred once (empty prefix)
        cur = 0
        result = 0
        for x in nums:
            cur += x
            result += counts[cur - k]    # how many j satisfy P[j] = cur - k
            counts[cur] += 1
        return result

Complexity

Comments


6.5 Isomorphic Strings (LC 205)

Problem

Two strings s and t are isomorphic if there exists a bijection between their characters such that replacing each character in s according to the mapping yields t.

Example

Input:  s = "egg", t = "add"   → True
Explanation: e→a, g→d (bijection).

Input:  s = "foo", t = "bar"   → False
Explanation: o would map to both a and r (not a function).

Input:  s = "paper", t = "title"   → True

Input:  s = "badc", t = "baba"   → False
Explanation: d→a and c→a, two different chars map to one → not a bijection.

Constraints

Clarifying questions

Approach

Approach 1 — Two dicts (mapping in both directions).

Walk the two strings together: - If s[i] is already in s2t → check s2t[s[i]] == t[i]. - Otherwise → confirm t[i] is not already in t2s (avoids multiple s mapping to the same t). - Store the pair (s[i], t[i]) in both dicts.

Approach 2 — Replace by “first-occurrence index”.

A string can be “normalised” by replacing each character with the index of its first occurrence. Two strings are isomorphic ↔︎ their normalised sequences match.

Example: "egg"[0, 1, 1], "add"[0, 1, 1] → equal → True.

Approach 1 is more intuitive; Approach 2 is algorithmically slicker. Both O(n).

Code (Python 3)

class Solution:
    """Approach 1 — two dicts, verify bijection."""

    def isIsomorphic(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        s2t: dict[str, str] = {}
        t2s: dict[str, str] = {}
        for a, b in zip(s, t):
            if a in s2t:
                if s2t[a] != b:
                    return False
            else:
                if b in t2s:        # b already mapped from a different char
                    return False
                s2t[a] = b
                t2s[b] = a
        return True


class SolutionNormalize:
    """Approach 2 — normalise by first-occurrence index."""

    def isIsomorphic(self, s: str, t: str) -> bool:
        return self._normalize(s) == self._normalize(t)

    @staticmethod
    def _normalize(s: str) -> list[int]:
        idx: dict[str, int] = {}
        out: list[int] = []
        for ch in s:
            if ch not in idx:
                idx[ch] = len(idx)
            out.append(idx[ch])
        return out

Complexity

Comments


6.6 LRU Cache (LC 146)

Problem

Design a Least Recently Used (LRU) Cache with both operations in O(1):

Example

Input (LC-style operation arrays):
  ops  = ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
  args = [[2],        [1,1], [2,2], [1],   [3,3], [2],   [4,4], [1],   [3],   [4]]

Output: [null, null, null, 1, null, -1, null, -1, 3, 4]

Step-by-step trace (capacity = 2; right = MRU, left = LRU):

  LRUCache(2)         → null;   cache = {}                    (LRU ← → MRU)
  put(1, 1)           → null;   cache = {1=1}
  put(2, 2)           → null;   cache = {1=1, 2=2}
  get(1)              → 1;      cache = {2=2, 1=1}   (1 just used → MRU)
  put(3, 3)           → null;   cache = {1=1, 3=3}   (evict 2: LRU)
  get(2)              → -1;     (key 2 gone)
  put(4, 4)           → null;   cache = {3=3, 4=4}   (evict 1)
  get(1)              → -1
  get(3)              → 3;      cache = {4=4, 3=3}
  get(4)              → 4;      cache = {3=3, 4=4}

Constraints

Clarifying questions

Approach

Core requirement: O(1) for both get and put ↔︎ we need both: - Hash map: key → reference to a node (gives O(1) lookup). - Doubly Linked List (DLL): access order (MRU at one end, LRU at the other). Lets us delete an arbitrary node in O(1) given its reference.

Each get(k): - If k is in the map → take the node, move it to the front of the DLL (= MRU), return value. - Otherwise return -1.

Each put(k, v): - If k is already present → update value, move to front. - Otherwise → insert a new node at the front. On capacity overflow → remove the tail node (LRU) and erase from the map.

Pythonic shortcut — use OrderedDict (already supports both operations):

OrderedDict is implemented underneath as a hash map combined with a doubly linked list. It exposes two methods that are gold for LRU: move_to_end(key) and popitem(last=False) (pop from the front).

Illustration — DLL state through the operations:

capacity = 2
                    Head (MRU)              Tail (LRU)
                          │                       │
                          ▼                       ▼
put(1,1):   DLL:    1                              cache = {1: node1}
put(2,2):   DLL:    2 ─── 1                       cache = {1: ., 2: .}
get(1)=1:   DLL:    1 ─── 2     (1 → MRU)
put(3,3):   DLL:    3 ─── 1     (evict 2 as LRU)
get(2)=-1
put(4,4):   DLL:    4 ─── 3     (evict 1)
get(1)=-1
get(3)=3:   DLL:    3 ─── 4
get(4)=4:   DLL:    4 ─── 3

Code (Python 3)

from collections import OrderedDict

class LRUCache:
    """Pythonic — OrderedDict already provides hash + DLL."""

    def __init__(self, capacity: int):
        self.cap = capacity
        self.cache: OrderedDict[int, int] = OrderedDict()

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)        # push to MRU end
        return self.cache[key]

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.cap:
            self.cache.popitem(last=False)  # pop LRU at the front


# ─────────────────────────────────────────────────────────────
# Hand-rolled version (interview-friendly): dict + doubly linked list.
# Use this when the interviewer says "Implement LRU without built-ins."

class _Node:
    __slots__ = ("key", "val", "prev", "next")

    def __init__(self, key: int = 0, val: int = 0):
        self.key, self.val = key, val
        self.prev: "_Node | None" = None
        self.next: "_Node | None" = None


class LRUCacheManual:
    def __init__(self, capacity: int):
        self.cap = capacity
        self.cache: dict[int, _Node] = {}
        # Two sentinels (head/tail) keep code short — no None checks at edges.
        self.head, self.tail = _Node(), _Node()
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove(self, node: _Node) -> None:
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_to_front(self, node: _Node) -> None:
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        node = self.cache[key]
        self._remove(node)
        self._add_to_front(node)
        return node.val

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            node = self.cache[key]
            node.val = value
            self._remove(node)
            self._add_to_front(node)
            return
        if len(self.cache) == self.cap:
            lru = self.tail.prev          # LRU = just before tail sentinel
            self._remove(lru)
            del self.cache[lru.key]
        new_node = _Node(key, value)
        self.cache[key] = new_node
        self._add_to_front(new_node)

Complexity

Comments

Chapter summary & decisions

When hash is NOT enough

Requirement Hash enough? Replacement
O(1) lookup, no order needed
Need ordered traversal OrderedDict / sorted list
Range query [l, r] Fenwick / Segment Tree (Chapter 22)
Top-k frequent Partial Heap (Chapter 15)
Nearest neighbour Sorted set / BST
Subarray sum with negatives ✅ Prefix sum + hash
Subarray sum non-negative only Use sliding window (27)

LRU two ways

Longest Consecutive — why O(n)?


Chapter 7 — Linked List

Linked List is “simple in theory, painful in code”. Each node points to the next — that’s it — but writing bug-free linked-list code requires memorising 5 small tricks: dummy head, two pointers, in-place reverse, split-by-pivot, and cross-pointer relinking. By the end of this chapter, LL will stop being intimidating.

This chapter has 12 problems — twice as many as basic chapters — because the LL pattern has many important variants, ranging from Easy (Reverse, Merge, Cycle) to Hard (Reverse k-Group, Sort, Reorder).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

5 tricks to memorise:

  1. Dummy head: create a fake dummy.next = head, use prev = dummy. Avoids a barrage of if head is None checks.
  2. Two pointers (slow / fast): find the middle (1× / 2× speed), detect cycles (Floyd), find offset-from-end.
  3. Reverse in place: 3 pointers prev / curr / nxt.
  4. Split → process → merge: pattern used by merge sort, palindrome check, reorder.
  5. Pointer relinking: when assigning a.next = b, always detach a from its old spot first (update both incoming and outgoing pointers).

Input format (applies to ALL problems in this chapter)

Every problem in Chapter 7 (and 3.3, 15.5) uses LeetCode’s standard ListNode:

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

Template code

class ListNode:
    def __init__(self, val: int = 0, next: "ListNode | None" = None):
        self.val = val
        self.next = next


def use_dummy(head: ListNode | None) -> ListNode | None:
    """Dummy-head pattern — problems that insert/delete near the head."""
    dummy = ListNode(0, head)
    prev = dummy
    while prev.next:
        # ... operate on prev.next ...
        prev = prev.next
    return dummy.next      # head may have changed


def find_middle(head: ListNode | None) -> ListNode | None:
    """Slow/fast pointers — find middle (LC 876)."""
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow


def reverse(head: ListNode | None) -> ListNode | None:
    """Reverse iterative — 3 pointers."""
    prev, curr = None, head
    while curr:
        curr.next, prev, curr = prev, curr, curr.next
    return prev

End-of-chapter practice


7.1 Reverse Linked List (LC 206) — iterative

Problem

Given head of a singly linked list, reverse it and return the new head. (The recursive version lives in Chapter 3.3 — this section focuses on the iterative version with O(1) space.)

Example

Input:  head = 1 → 2 → 3 → 4 → 5 → None  (singly linked list)
Output: 5 → 4 → 3 → 2 → 1 → None

Constraints

Clarifying questions

Approach

Three pointers: - prev = the node right before curr in the result. - curr = the node we are processing. - nxt = backup of curr.next before we overwrite.

Each iteration: flip curr.next to point at prev, then advance both prev and curr.

Illustration with 1 → 2 → 3 → None:

Start  :   prev = None
           curr → 1 → 2 → 3 → None

Iter 1:    nxt = 2
           curr.next = prev   →   None ← 1   2 → 3 → None
           prev = curr = 1, curr = 2

Iter 2:    nxt = 3
           curr.next = prev   →   None ← 1 ← 2   3 → None
           prev = 2, curr = 3

Iter 3:    nxt = None
           curr.next = prev   →   None ← 1 ← 2 ← 3
           prev = 3, curr = None  → stop

Return prev = 3, list: 3 → 2 → 1 → None

Code (Python 3)

class Solution:
    def reverseList(self, head: ListNode | None) -> ListNode | None:
        prev, curr = None, head
        while curr:
            nxt = curr.next
            curr.next = prev
            prev = curr
            curr = nxt
        return prev

Pythonic in-loop one-liner: curr.next, prev, curr = prev, curr, curr.next. Tuple unpacking evaluates the RHS first, so no temporary nxt is needed.

Complexity

Comments


7.2 Merge Two Sorted Lists (LC 21)

Problem

Given two heads of two ascending sorted linked lists, return the head of the merged list (also ascending).

Example

Input:  l1 = 1 → 2 → 4,  l2 = 1 → 3 → 4
Output: 1 → 1 → 2 → 3 → 4 → 4

Constraints

Clarifying questions

Approach

Iterative — use a dummy head. Create dummy with tail = dummy. Each step attach tail.next to the smaller node of the two lists, advance tail. Finally splice the remaining tail.

Recursive (very concise but O(n) stack):

if not l1: return l2
if not l2: return l1
if l1.val <= l2.val:
    l1.next = self.mergeTwoLists(l1.next, l2)
    return l1
else:
    l2.next = self.mergeTwoLists(l1, l2.next)
    return l2

Code (Python 3)

class Solution:
    def mergeTwoLists(self, l1: ListNode | None, l2: ListNode | None) -> ListNode | None:
        dummy = ListNode()
        tail = dummy
        while l1 and l2:
            if l1.val <= l2.val:
                tail.next, l1 = l1, l1.next
            else:
                tail.next, l2 = l2, l2.next
            tail = tail.next
        tail.next = l1 if l1 else l2     # splice the remaining tail
        return dummy.next

Complexity

Comments


7.3 Linked List Cycle (LC 141)

Problem

Given the head of a linked list, return True if there is a cycle, otherwise False. Requirement: O(1) extra space.

Example

Input:  head = [3, 2, 0, -4], cycle begins at index 1
        3 → 2 → 0 → -4
            ↑________|
Output: True

Constraints

Clarifying questions

Approach

Brute force — Hash set, O(n) space. Store every seen node.

Optimal — Floyd’s Tortoise and Hare, O(1) space.

Two pointers slow (1×) and fast (2×). If a cycle exists, fast “catches up” to slow inside the loop (their distance shrinks by 1 each step). If not, fast runs off the end.

Illustration — slow/fast on a cycle:

Linked list:  3 → 2 → 0 → -4 → ⟲ (back to 2)

Step 0:  slow=3, fast=3
Step 1:  slow=2, fast=0
Step 2:  slow=0, fast=2     (fast looped back)
Step 3:  slow=-4, fast=-4   ★ they meet → return True

On a cycle of length L, slow steps 1, fast steps 2 → distance shrinks
by 1 per step → meet within L steps.

Code (Python 3)

class Solution:
    def hasCycle(self, head: ListNode | None) -> bool:
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if slow is fast:
                return True
        return False

Complexity

Comments


7.4 Middle of the Linked List (LC 876)

Problem

Given the head, return the middle node. If there are two middles (even length), return the second.

Example

Input:  head = 1 → 2 → 3 → 4 → 5     (singly linked list)
Output: node with value 3 (middle; belongs to the right half on even length)
Input:  head = 1 → 2 → 3 → 4 → 5 → 6   (singly linked list, even length)
Output: node with value 4 (the second of the two middles)

Constraints

Clarifying questions

Approach

Brute force — 2 passes, O(n). Count length, then walk to the middle.

Optimal — Slow/fast one pass, O(n). When fast hits the end, slow is at the middle.

Code (Python 3)

class Solution:
    def middleNode(self, head: ListNode | None) -> ListNode | None:
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        return slow

Complexity

Comments


7.5 Remove Nth Node From End of List (LC 19)

Problem

Given the head and integer n, remove the n-th node counting from the end (1-indexed) and return the possibly-changed head.

Example

Input:  1 → 2 → 3 → 4 → 5, n = 2     → 1 → 2 → 3 → 5  (remove node 4)
Input:  1, n = 1                     → None
Input:  1 → 2, n = 1                 → 1

Constraints

Clarifying questions

Approach

Brute force — 2 passes. Count length L, then remove the (L - n)-th from the start.

Optimal — single pass, two pointers separated by n. - Create a dummy to handle removal of the head. - fast advances n steps first. - Then slow and fast advance together until fast.next is None. At that point slow.next is exactly the node to remove.

Illustration with 1 → 2 → 3 → 4 → 5, n = 2:

Start:    dummy → 1 → 2 → 3 → 4 → 5 → None
          slow
          fast

After fast advances n=2 steps:
          dummy → 1 → 2 → 3 → 4 → 5 → None
          slow        fast

Advance together until fast.next == None:
          dummy → 1 → 2 → 3 → 4 → 5 → None
                          slow      fast

slow.next = 4 → the node to remove. slow.next = slow.next.next.
          dummy → 1 → 2 → 3 → 5 → None  ✓

Code (Python 3)

class Solution:
    def removeNthFromEnd(self, head: ListNode | None, n: int) -> ListNode | None:
        dummy = ListNode(0, head)
        slow = fast = dummy
        for _ in range(n):
            fast = fast.next
        while fast.next:
            slow = slow.next
            fast = fast.next
        slow.next = slow.next.next
        return dummy.next

Complexity

Comments


7.6 Palindrome Linked List (LC 234)

Problem

Given the head of a singly linked list, return True if the values form a palindrome. Required: O(n) time, O(1) space.

Example

Input:  head = 1 → 2 → 2 → 1     → Output: True   (palindrome)
Input:  head = 1 → 2             → Output: False  (1 ≠ 2)

Constraints

Clarifying questions

Approach

Brute force — copy values to an array + two pointers, O(n) space. Simple but violates the O(1) space goal.

Optimal — Split + Reverse half + Compare, O(1) space. 1. Find the middle (slow/fast). 2. Reverse the second half (in place). 3. Compare node-by-node between the first half and the reversed second half. 4. (Optional) Restore the second half (usually unnecessary in interviews).

Illustration with 1 → 2 → 3 → 2 → 1:

Step 1: find middle (slow lands at node 3)
              1 → 2 → 3 → 2 → 1
                      ↑ slow

Step 2: reverse the second half (starting from slow.next = 2):
              1 → 2 → 3   1 → 2
              (first half) (reversed second half)

Step 3: compare node-by-node:
              1 vs 1 ✓
              2 vs 2 ✓
        → True

Code (Python 3)

class Solution:
    def isPalindrome(self, head: ListNode | None) -> bool:
        # 1. Find middle.
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        # 2. Reverse second half (starting at slow).
        prev, curr = None, slow
        while curr:
            curr.next, prev, curr = prev, curr, curr.next

        # 3. Compare.
        left, right = head, prev
        while right:    # reversed second half may be one node shorter
            if left.val != right.val:
                return False
            left = left.next
            right = right.next
        return True

Complexity

Comments


7.7 Add Two Numbers (LC 2)

Problem

Given two linked lists representing two non-negative integers with digits stored in reverse (ones digit at the head), return the linked list = sum of the two numbers (also reversed).

Example

Input:  l1 = 2 → 4 → 3   (represents 342)
        l2 = 5 → 6 → 4   (represents 465)
Output: 7 → 0 → 8         (represents 807 = 342 + 465)

Input:  l1 = 9 → 9 → 9 → 9 → 9 → 9 → 9
        l2 = 9 → 9 → 9 → 9
Output: 8 → 9 → 9 → 9 → 0 → 0 → 0 → 1

Constraints

Clarifying questions

Approach

Simulate addition by hand: walk both lists together, keep a carry. Each step: - total = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry - digit = total % 10, carry = total // 10. - Push digit to the result; advance l1, l2.

Stop only when both are exhausted and carry == 0.

Code (Python 3)

class Solution:
    def addTwoNumbers(self, l1: ListNode | None, l2: ListNode | None) -> ListNode | None:
        dummy = ListNode()
        tail = dummy
        carry = 0
        while l1 or l2 or carry:
            total = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry
            carry, digit = divmod(total, 10)
            tail.next = ListNode(digit)
            tail = tail.next
            if l1: l1 = l1.next
            if l2: l2 = l2.next
        return dummy.next

Complexity

Comments


7.8 Copy List with Random Pointer (LC 138)

Problem

Given a linked list where each node has both next and random — pointing to any node in the list (or None) — produce a deep copy (every new node is a distinct instance, with random pointers pointing into the copies).

Example

Input (LC-style):
  head = [[7, null], [13, 0], [11, 4], [10, 2], [1, 0]]
  (each entry [val, random_index]; random_index is the 0-based index of
   the node that `random` points to, or null if `random = None`)

  Corresponding linked list:
        node 0      node 1      node 2      node 3      node 4
        val=7   →   val=13  →   val=11  →   val=10  →   val=1  →  None
        random:                                                    (next pointers)
          [0] → None
          [1] → node 0  (val 7)
          [2] → node 4  (val 1)
          [3] → node 2  (val 11)
          [4] → node 0  (val 7)

Output: deep copy of the above — same vals and same random structure,
        but EVERY node is a NEW instance (no sharing with the input).

Output as an LC array:
  [[7, null], [13, 0], [11, 4], [10, 2], [1, 0]]

Constraints

Clarifying questions

Approach

Approach 1 — Hash map “old → new”, O(n) time, O(n) space.

Pass 1: create the new nodes, store old_to_new[old] = new. Pass 2: for each old, set new.next = old_to_new[old.next] and new.random = old_to_new[old.random].

Approach 2 — Interweave, O(n) time, O(1) extra space.

A classic trick: 1. Pass 1: insert each copied node right after its original: A → A' → B → B' → C → C'. 2. Pass 2: for each original node A, set A'.random = A.random.next (since A.random.next is the copy of A.random). 3. Pass 3: split the two lists apart.

Illustration — Approach 2 with 3 nodes A, B, C:

Pass 1 (insert copy):
  A → A' → B → B' → C → C'

Pass 2 (set random):
  Suppose A.random = C
  → A'.random = A.random.next = C.next = C'   (copy of C)  ✓

Pass 3 (split):
  Original:  A → B → C
  Copy:      A' → B' → C'

Code (Python 3)

class Node:
    def __init__(self, val: int = 0, next=None, random=None):
        self.val = val
        self.next = next
        self.random = random


class Solution:
    """Approach 1 — hash map. Easiest to debug."""

    def copyRandomList(self, head: "Node | None") -> "Node | None":
        if not head:
            return None
        old_to_new: dict[Node, Node] = {}
        # Pass 1: create copy nodes.
        cur = head
        while cur:
            old_to_new[cur] = Node(cur.val)
            cur = cur.next
        # Pass 2: link next/random.
        cur = head
        while cur:
            old_to_new[cur].next = old_to_new.get(cur.next)
            old_to_new[cur].random = old_to_new.get(cur.random)
            cur = cur.next
        return old_to_new[head]


class SolutionInterleave:
    """Approach 2 — interweave, O(1) extra space."""

    def copyRandomList(self, head: "Node | None") -> "Node | None":
        if not head:
            return None
        # 1. Insert a copy right after each original node.
        cur = head
        while cur:
            cur.next = Node(cur.val, cur.next)
            cur = cur.next.next
        # 2. Set random pointers for the copies.
        cur = head
        while cur:
            if cur.random:
                cur.next.random = cur.random.next
            cur = cur.next.next
        # 3. Split into two lists.
        new_head = head.next
        cur, copy = head, new_head
        while cur:
            cur.next = copy.next
            cur = cur.next
            copy.next = cur.next if cur else None
            copy = copy.next
        return new_head

Complexity

Comments


7.9 Reverse Nodes in k-Group (LC 25)

Problem

Given the head and integer k, reverse each consecutive group of k nodes in the list. If the remaining nodes are fewer than k, leave them as is. Required: O(1) extra space.

Example

Input:  1 → 2 → 3 → 4 → 5, k = 2
Output: 2 → 1 → 4 → 3 → 5    (last group has only 1 node → leave it)

Input:  1 → 2 → 3 → 4 → 5, k = 3
Output: 3 → 2 → 1 → 4 → 5

Constraints

Clarifying questions

Approach

Procedure: 1. Walk k steps to locate the group tail. If fewer than k → break. 2. Reverse the group in [head_of_group, tail_of_group]. 3. Splice the head of the reversed group onto prev_group_tail, and the tail of the reversed group on to next_group_head. 4. Update prev_group_tail for the next group.

Illustration with 1 → 2 → 3 → 4 → 5, k = 2:

Start:      dummy → 1 → 2 → 3 → 4 → 5 → None
            prev

Group 1: [1, 2]. Reverse → [2, 1].
            dummy → 2 → 1 → 3 → 4 → 5 → None
                        ↑
                       prev (= 1, the tail of the just-reversed group)

Group 2: [3, 4]. Reverse → [4, 3].
            dummy → 2 → 1 → 4 → 3 → 5 → None
                                ↑
                               prev (= 3)

Group 3: [5]. Only 1 node → fewer than k=2 → leave.
            dummy → 2 → 1 → 4 → 3 → 5 → None   ✓

Code (Python 3)

class Solution:
    def reverseKGroup(self, head: ListNode | None, k: int) -> ListNode | None:
        dummy = ListNode(0, head)
        prev_group_tail = dummy

        while True:
            # 1. Find the group tail — walk k steps.
            kth = prev_group_tail
            for _ in range(k):
                kth = kth.next
                if not kth:
                    return dummy.next      # fewer than k left → leave as is

            group_next = kth.next
            # 2. Reverse from prev_group_tail.next to kth.
            prev, curr = group_next, prev_group_tail.next
            while curr is not group_next:
                curr.next, prev, curr = prev, curr, curr.next
            # 3. Splice the reversed group in.
            old_head = prev_group_tail.next
            prev_group_tail.next = kth
            prev_group_tail = old_head     # this is now the reversed group's tail

Complexity

Comments


7.10 Sort List (LC 148)

Problem

Sort a linked list ascending in O(n log n) time and O(1) extra space (per the follow-up — counts call stack/aux only, not nodes).

Example

Input:  head = 4 → 2 → 1 → 3   (singly linked list)
Output: 1 → 2 → 3 → 4

Constraints

Clarifying questions

Approach

O(n log n) ⇒ quicksort, mergesort, heapsort. Quicksort is awkward on LL, heapsort needs O(n) extra. Merge sort is the most natural:

  1. Split the list into two halves (slow/fast for the middle, cut slow.next).
  2. Recurse on both halves.
  3. Merge the two sorted lists (problem 7.2).

Top-down (recursive) is clean but consumes O(log n) stack. Bottom-up (iterative) achieves true O(1) space — but more complex. In an interview, top-down is good enough.

Illustration with 4 → 2 → 1 → 3:

                   sort([4, 2, 1, 3])
                  /                  \
        sort([4, 2])             sort([1, 3])
         /        \                /        \
    [4]            [2]         [1]            [3]
       \          /                \          /
       merge → [2, 4]              merge → [1, 3]
                       \              /
                       merge → [1, 2, 3, 4]   ✓

Code (Python 3)

class Solution:
    def sortList(self, head: ListNode | None) -> ListNode | None:
        if not head or not head.next:
            return head

        # 1. Split: find middle, cut in half.
        slow, fast = head, head.next
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        mid = slow.next
        slow.next = None

        # 2. Recurse.
        left = self.sortList(head)
        right = self.sortList(mid)

        # 3. Merge.
        return self._merge(left, right)

    @staticmethod
    def _merge(l1: ListNode | None, l2: ListNode | None) -> ListNode | None:
        dummy = ListNode()
        tail = dummy
        while l1 and l2:
            if l1.val <= l2.val:
                tail.next, l1 = l1, l1.next
            else:
                tail.next, l2 = l2, l2.next
            tail = tail.next
        tail.next = l1 or l2
        return dummy.next

Complexity

Comments


7.11 LRU Cache (LC 146) — recap

This problem is fully solved in Chapter 6 — Hash Table (section 6.6). Here we summarise the Doubly Linked List pattern, which is the LL highlight.

Problem

Design an LRU Cache with get(key) and put(key, value) both O(1). On capacity overflow → evict the least-recently-used key.

Constraints

Clarifying questions

Approach: Doubly Linked List + Hash Map

LRU requires O(1) for both: - Lookup by key → hash map. - Move an arbitrary node to the front → doubly linked list (only DLL supports O(1) unlink given a reference).

Illustration of DLL state on LRU eviction

capacity = 2
Head sentinel — MRU end                LRU end — Tail sentinel
       │                                                  │
       ▼                                                  ▼
     ┌───┐    ┌──────┐    ┌──────┐    ┌──────┐    ┌───┐
     │ H │ ↔ │ K=4  │ ↔ │ K=3  │ ↔ │ K=1  │ ↔ │ T │
     └───┘    └──────┘    └──────┘    └──────┘    └───┘
                                          ▲
                          on capacity overflow, evict this node
                          (Tail.prev = LRU)

Hash map:  {1: node1, 3: node3, 4: node4}

Code (Python 3) — DLL pattern

class Node:
    __slots__ = ("key", "val", "prev", "next")
    def __init__(self, key=0, val=0):
        self.key, self.val = key, val
        self.prev = self.next = None


# Two sentinels (head, tail) save many None checks.
head, tail = Node(), Node()
head.next, tail.prev = tail, head

def _remove(node):              # O(1) — only requires a reference
    node.prev.next = node.next
    node.next.prev = node.prev

def _add_to_front(node):
    node.prev = head
    node.next = head.next
    head.next.prev = node
    head.next = node

Complexity

Comments


7.12 Reorder List (LC 143)

Problem

Given a linked list L: L0 → L1 → ... → Ln-1, rearrange it to:

L0 → Ln-1 → L1 → Ln-2 → L2 → Ln-3 → ...

In place; no new nodes may be created.

Example

Input:  head = 1 → 2 → 3 → 4
Output: 1 → 4 → 2 → 3 (mutate in place)

Input:  head = 1 → 2 → 3 → 4 → 5
Output: 1 → 5 → 2 → 4 → 3   (mutate in place)

Constraints

Clarifying questions

Approach

Three standard steps — the “split → reverse → merge” pattern:

  1. Find the middle (slow/fast).
  2. Reverse the second half (in place).
  3. Merge interleaved the first half and the reversed second half.

Illustration with 1 → 2 → 3 → 4 → 5:

Step 1: find middle (slow at 3)
            1 → 2 → 3 → 4 → 5

Step 2: split and reverse the second half:
            1 → 2 → 3      5 → 4
            (first half)   (reversed second half)

Step 3: merge interleaved:
            Take 1 from left  → 1
            Take 5 from right → 1, 5
            Take 2 from left  → 1, 5, 2
            Take 4 from right → 1, 5, 2, 4
            Take 3 from left  → 1, 5, 2, 4, 3

Output: 1 → 5 → 2 → 4 → 3  ✓

Code (Python 3)

class Solution:
    def reorderList(self, head: ListNode | None) -> None:
        if not head or not head.next:
            return

        # 1. Find middle.
        slow = fast = head
        while fast.next and fast.next.next:
            slow = slow.next
            fast = fast.next.next

        # 2. Reverse second half (starting from slow.next).
        second = slow.next
        slow.next = None      # cut in half
        prev, curr = None, second
        while curr:
            curr.next, prev, curr = prev, curr, curr.next
        second = prev          # head of the reversed second half

        # 3. Merge interleaved.
        first = head
        while second:
            tmp1, tmp2 = first.next, second.next
            first.next = second
            second.next = tmp1
            first, second = tmp1, tmp2

Complexity

Comments

Chapter summary & decisions

Pointer-safety checklist

  1. Before overwriting a.next = b, did you save the old a.next?
  2. Is there a dummy/sentinel at the head? (Needed if head may change.)
  3. while cur and cur.next: be careful with the compound condition for the last two nodes.
  4. After reverse or split, did you set the old tail’s .next = None? (Avoid cycles.)
  5. Edge cases: empty list (head = None), single node, k > len.

Dummy/sentinel pattern (the core)

dummy = ListNode(0, head)
prev = dummy
# ... operate on prev.next ...
return dummy.next

Used by: Remove Nth From End, Merge Two Sorted, Partition, Odd Even, Reverse K-Group.

LRU (LC 146) bridge


Chapter 8 — Queue + Stack

Stack (LIFO) and Queue (FIFO) are mirror images of each other. Stack solves every problem with a nested structure (parentheses, recursion, nested expressions); Queue handles level-order traversal (BFS, sliding window). This chapter focuses on stack — pure queue problems return in Chapter 10 (BFS). The chapter ends with a teaser on monotonic stack — a powerful pattern explored in depth in Chapter 18.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Stack fits perfectly when: - The structure is nested / balanced (parentheses, HTML tags, nested expressions). - You need to “undo” — the previous step must finish only after the current step (iterative DFS). - Problems like “next greater element”, “largest rectangle”, “daily temperatures” → monotonic stack (Chapter 18).

Queue fits perfectly when: - You need level-order / BFS traversal (Chapter 10). - “Sliding window max” → monotonic deque (Chapter 18). - Producer-consumer, task scheduler.

Template code

from collections import deque
from typing import List

# 1) Stack with a list (built-in in Python, O(1) amortised).
stack: List[int] = []
stack.append(x)        # push
top = stack[-1]        # peek
val = stack.pop()      # pop

# 2) Queue with collections.deque — O(1) push/pop at both ends.
queue: deque[int] = deque()
queue.append(x)        # enqueue (push at the back)
val = queue.popleft()  # dequeue (pop from the front)

# 3) Stack of (index, value) — monotonic pattern.
stack: list[tuple[int, int]] = []        # (index, value)
for i, v in enumerate(arr):
    while stack and stack[-1][1] < v:
        idx, _ = stack.pop()
        # ... process idx ...
    stack.append((i, v))

End-of-chapter practice


8.1 Valid Parentheses (LC 20)

Problem

Given a string s containing only ()[]{}, return True if it is valid: - Every opening bracket has a matching closing one. - Closing brackets respect LIFO order.

Example

Input:  s = "()"            → Output: True
Input:  s = "()[]{}"        → Output: True
Input:  s = "(]"            → Output: False
Input:  s = "([)]"          → Output: False    (wrong nesting)
Input:  s = "{[]}"          → Output: True

Constraints

Clarifying questions

Approach

The classic stack pattern. Scan each character: - Opening bracket → push. - Closing bracket → check the stack top for the matching opener. If it does not match, or the stack is empty → return False. Otherwise pop.

At the end the stack must be empty (all brackets matched).

Illustration for s = "{[()]}":

ch   action          stack after action
─────────────────────────────────────
{    push            ['{']
[    push            ['{', '[']
(    push            ['{', '[', '(']
)    pop, match (    ['{', '[']
]    pop, match [    ['{']
}    pop, match {    []

Stack empty → True ✓

Code (Python 3)

class Solution:
    def isValid(self, s: str) -> bool:
        pairs = {')': '(', ']': '[', '}': '{'}
        stack: list[str] = []
        for ch in s:
            if ch in pairs:                # closing bracket
                if not stack or stack.pop() != pairs[ch]:
                    return False
            else:                          # opening bracket
                stack.append(ch)
        return not stack

Complexity

Comments


8.2 Min Stack (LC 155)

Problem

Design a stack supporting all four operations in O(1): - push(x) - pop() - top() — peek - getMin() — return the current minimum in the stack

Example

Input (LC-style operation arrays):
  ops  = ["MinStack","push","push","push","getMin","pop","top","getMin"]
  args = [[],        [-2],   [0],  [-3],  [],     [],   [],   []]

Output: [null, null, null, null, -3, null, 0, -2]

Step-by-step:
  MinStack()  → init
  push(-2);  push(0);  push(-3)
  getMin()   → -3
  pop()      → drop -3
  top()      → 0
  getMin()   → -2

Constraints

Clarifying questions

Approach

Problem: getMin() in O(1) ⇒ the min has to be stored somewhere. But when we pop the min, we must know the new min — that’s the crux.

Approach 1 — Auxiliary stack holding min_so_far.

The auxiliary stack mins mirrors the main stack; its top is the min of all elements at and below in the main stack. On push, push min(x, mins[-1]). On pop, pop both stacks.

Approach 2 — Single stack of (value, current_min) tuples.

Same idea as Approach 1, just consolidated into tuples. Same overhead.

Illustration — Approach 1 for push(-2), push(0), push(-3), pop():

push(-2):  stack=[-2]      mins=[-2]
push(0):   stack=[-2, 0]   mins=[-2, -2]    (min(0, -2)=-2)
push(-3):  stack=[-2,0,-3] mins=[-2,-2,-3]   (min(-3, -2)=-3)

getMin() → mins[-1] = -3   ✓
pop():    stack=[-2, 0]    mins=[-2, -2]
getMin() → mins[-1] = -2   ✓

Code (Python 3)

class MinStack:
    def __init__(self):
        self.stack: list[int] = []
        self.mins: list[int] = []

    def push(self, val: int) -> None:
        self.stack.append(val)
        cur_min = val if not self.mins else min(val, self.mins[-1])
        self.mins.append(cur_min)

    def pop(self) -> None:
        self.stack.pop()
        self.mins.pop()

    def top(self) -> int:
        return self.stack[-1]

    def getMin(self) -> int:
        return self.mins[-1]

Complexity

Comments


8.3 Implement Queue using Stacks (LC 232)

Problem

Design a Queue (FIFO) using only two stacks. Support push, pop, peek, empty.

Example

Input (LC-style operation arrays):
  ops  = ["MyQueue","push","push","peek","pop","empty"]
  args = [[],       [1],   [2],   [],    [],   []]

Output: [null, null, null, 1, 1, false]

Step-by-step:
  MyQueue()  → init
  push(1);  push(2)
  peek()    → 1   (FIFO: first in first out)
  pop()     → 1
  empty()   → false

Constraints

Clarifying questions

Approach

Idea — two stacks: in (input) and out (output). - push(x): push onto in. - pop / peek: if out is empty → “pour” the entire in into out (reversing order due to LIFO). Then pop/peek from out.

Amortised analysis: each element is moved between the two stacks at most once. Worst-case single op is O(n), but amortised is O(1).

Illustration for push(1), push(2), push(3), pop(), push(4), pop():

push(1): in=[1]              out=[]
push(2): in=[1, 2]           out=[]
push(3): in=[1, 2, 3]        out=[]

pop(): out empty → pour in into out
       in=[]                  out=[3, 2, 1]    (1 on top)
       out.pop() → 1
       in=[]                  out=[3, 2]

push(4): in=[4]              out=[3, 2]

pop(): out non-empty → out.pop() → 2
       in=[4]                out=[3]

→ FIFO: 1 came out first (matching push order)

Code (Python 3)

class MyQueue:
    def __init__(self):
        self.in_st: list[int] = []
        self.out_st: list[int] = []

    def push(self, x: int) -> None:
        self.in_st.append(x)

    def pop(self) -> int:
        self._shift()
        return self.out_st.pop()

    def peek(self) -> int:
        self._shift()
        return self.out_st[-1]

    def empty(self) -> bool:
        return not self.in_st and not self.out_st

    def _shift(self) -> None:
        """When out is empty, pour all of in into out."""
        if not self.out_st:
            while self.in_st:
                self.out_st.append(self.in_st.pop())

Complexity

Comments


8.4 Evaluate Reverse Polish Notation (LC 150)

Problem

Given an array tokens representing a Reverse Polish Notation (postfix) expression. Each token is an integer or one of the four operators + - * /. Return the result (division truncated toward 0).

Example

Input:  tokens = ["2","1","+","3","*"]
Output: 9
Explanation: (2 + 1) * 3 = 9.

Input:  tokens = ["4","13","5","/","+"]
Output: 6
Explanation: 4 + (13 / 5) = 4 + 2 = 6.

Input:  tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
Output: 22

Constraints

Clarifying questions

Approach

RPN ↔︎ stack — classic. Walk through: - Number → push. - Operator → pop two values (b first, then a), compute a op b, push the result.

At the end the stack contains one element = the answer.

Illustration for ["2","1","+","3","*"]:

token   action                      stack
─────────────────────────────────────────────
"2"     push 2                      [2]
"1"     push 1                      [2, 1]
"+"     pop b=1, a=2; push 3        [3]
"3"     push 3                      [3, 3]
"*"     pop b=3, a=3; push 9        [9]

Result: 9

Code (Python 3)

from typing import List
import operator

class Solution:
    OPS = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': lambda a, b: int(a / b),   # truncate toward 0
    }

    def evalRPN(self, tokens: List[str]) -> int:
        stack: list[int] = []
        for tk in tokens:
            if tk in self.OPS:
                b = stack.pop()
                a = stack.pop()
                stack.append(self.OPS[tk](a, b))
            else:
                stack.append(int(tk))
        return stack[0]

Complexity

Comments


8.5 Daily Temperatures (LC 739) — monotonic stack teaser

Problem

Given an array temperatures, for each day i find how many days you have to wait before a day with a strictly higher temperature. If none exists, return 0.

Example

Input:  temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
Output: [1, 1, 4, 2, 1, 1, 0, 0]
Explanation:
  day 0: 73 → day 1 (74) is higher → 1
  day 2: 75 → wait until day 6 (76) → 6-2=4
  day 6: 76 has nothing higher → 0

Constraints

Clarifying questions

Approach

Brute force — O(n²). For each i scan forward for a greater value. TLEs at n = 10^5.

Optimal — Monotonic decreasing stack — O(n).

Idea: maintain a stack of indices whose temperatures decrease from bottom to top. When day i arrives with a temperature greater than the stack top → that is the answer for the top day. Pop and write result[top] = i - top.

Illustration for [73, 74, 75, 71, 69, 72, 76, 73]:

i  temp   action                                stack (idx)    result
─────────────────────────────────────────────────────────────────────────
0  73     push 0                                [0]            [_, _, _, _, _, _, _, _]
1  74     74 > 73 → pop 0, result[0]=1-0=1     [1]            [1, _, _, _, _, _, _, _]
          push 1
2  75     75 > 74 → pop 1, result[1]=2-1=1     [2]            [1, 1, _, _, _, _, _, _]
          push 2
3  71     71 < 75 → push 3                     [2, 3]
4  69     69 < 71 → push 4                     [2, 3, 4]
5  72     72 > 69 → pop 4, result[4]=5-4=1     [2, 3, 5]
          72 > 71 → pop 3, result[3]=5-3=2
          push 5
6  76     76 > 72 → pop 5, result[5]=6-5=1     [6]
          76 > 75 → pop 2, result[2]=6-2=4
          push 6
7  73     73 < 76 → push 7                     [6, 7]

Leftover in stack: [6, 7] → result stays at 0.
Answer: [1, 1, 4, 2, 1, 1, 0, 0]  ✓

Stack invariant: indices on the stack have strictly decreasing temperatures from bottom to top. Each index is pushed once and popped at most once → O(n) total.

Code (Python 3)

from typing import List

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        n = len(temperatures)
        result = [0] * n
        stack: list[int] = []   # indices with decreasing temperatures
        for i, t in enumerate(temperatures):
            while stack and temperatures[stack[-1]] < t:
                j = stack.pop()
                result[j] = i - j
            stack.append(i)
        return result

Complexity

Comments


8.6 Decode String (LC 394)

Problem

Given a string s encoded as k[encoded_string] — meaning encoded_string is repeated k times — decode the string.

Example

Input:  s = "3[a]2[bc]"
Output: "aaabcbc"

Input:  s = "3[a2[c]]"
Output: "accaccacc"   (nested)

Input:  s = "2[abc]3[cd]ef"
Output: "abcabccdcdcdef"

Constraints

Clarifying questions

Approach

A nested structure ⇒ use a stack. Whenever we encounter [, “save” the current k and the in-progress string onto the stack, then reset. When we hit ], pop (prev_str, k) and assemble prev_str + k * cur_str.

Approach 2 — Recursive. Each k[...] is a recursive call. Cleaner code but uses recursion stack (can exceed limits with deep nesting).

Illustration — Stack approach for "3[a2[c]]":

ch    action                                     stack         cur
─────────────────────────────────────────────────────────────────
'3'   k = 3                                      []           ""
'['   push (k, cur); reset k, cur                [(3, "")]    ""
'a'   cur += 'a'                                 [(3, "")]    "a"
'2'   k = 2                                      [(3, "")]    "a"
'['   push (k=2, cur="a"); reset k, cur          [(3, ""), (2, "a")]  ""
'c'   cur += 'c'                                 [(3, ""), (2, "a")]  "c"
']'   (prev_k=2, prev_str="a") = pop;
      cur = prev_str + prev_k * cur = "a" + "cc" [(3, "")]    "acc"
']'   (prev_k=3, prev_str="") = pop;
      cur = "" + 3 * "acc" = "accaccacc"         []           "accaccacc"

Answer: "accaccacc"  ✓

Code (Python 3)

class Solution:
    def decodeString(self, s: str) -> str:
        stack: list[tuple[str, int]] = []   # (prev_str, prev_k)
        cur, k = "", 0
        for ch in s:
            if ch.isdigit():
                k = k * 10 + int(ch)
            elif ch == '[':
                stack.append((cur, k))
                cur, k = "", 0
            elif ch == ']':
                prev_str, prev_k = stack.pop()
                cur = prev_str + cur * prev_k
            else:
                cur += ch
        return cur


class SolutionRecursive:
    """Approach 2 — recursive. Clean code but uses recursion stack."""

    def decodeString(self, s: str) -> str:
        self.i = 0
        return self._decode(s)

    def _decode(self, s: str) -> str:
        result = ""
        k = 0
        while self.i < len(s) and s[self.i] != ']':
            ch = s[self.i]
            if ch.isdigit():
                k = k * 10 + int(ch)
                self.i += 1
            elif ch == '[':
                self.i += 1
                inner = self._decode(s)
                result += k * inner
                k = 0
                self.i += 1     # skip ']'
            else:
                result += ch
                self.i += 1
        return result

Complexity

Comments

Chapter summary & decisions

Stack mental models

Purpose What the stack holds Example problems
Match symmetric pairs Opening brackets / tokens awaiting close LC 20, 1249
Save previous tokens not yet finished Numbers / strings to “expand” later LC 394 Decode
Undo / context Parent operations LC 224 Calculator
Monotonic Index/value increasing/decreasing Chapter 18
Iterative DFS Frame call Tree iterative inorder

Decode String (LC 394) — trace 3[a2[c]]

Step Char num cur numStack strStack
0 3 3 "" [] []
1 [ 0 "" [3] [""]
2 a 0 "a" [3] [""]
3 2 2 "a" [3] [""]
4 [ 0 "" [3,2] ["", "a"]
5 c 0 "c" [3,2] ["", "a"]
6 ] 0 "acc" (a + c×2) [3] [""]
7 ] 0 "accaccacc" (×3) [] []

RPN (LC 150) — operand order

Pop b first, then a, compute a op b. Reversing the order is the classic bug for - and /.

Min Stack comparison


Chapter 9 — Graph

Graph is the most abstract yet most ubiquitous data structure in real life: friendships, road networks, dependencies, … This chapter introduces graph representations and 6 general graph problems. The two traversal techniques BFS and DFS are explored in depth in Chapters 10 and 11; here we use both at a basic level to build familiarity.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Three questions to answer first: 1. Directed or undirected? Directed graphs require extra care for cycles. 2. Weighted? If yes → consider Dijkstra (Chapter 30); otherwise BFS/DFS suffices. 3. Special properties? DAG → topological sort, bipartite, planar, …

Graph representations

from collections import defaultdict
from typing import List

# 1) Adjacency List — what most problems use
graph: dict[int, list[int]] = defaultdict(list)
for u, v in edges:
    graph[u].append(v)
    graph[v].append(u)    # drop this line if the graph is directed

# 2) Edge List — the "raw" input format
edges: list[tuple[int, int]] = [(0, 1), (1, 2), ...]

# 3) Adjacency Matrix — only when V is small (≤ 1000) and density is high
adj = [[0] * n for _ in range(n)]
for u, v in edges:
    adj[u][v] = 1

Which one to use?

Representation Lookup (u, v) Walk neighbours of u Memory
Adj list O(deg(u)) O(deg(u)) O(V + E)
Edge list O(E) O(E) O(E)
Adj matrix O(1) O(V) O(V²)

Default to adjacency list. Switch to a matrix only when you need O(1) edge checks and V is small.

Template code

from collections import defaultdict, deque

# Recursive DFS
def dfs(node: int, visited: set[int], graph: dict) -> None:
    if node in visited:
        return
    visited.add(node)
    for neighbor in graph[node]:
        dfs(neighbor, visited, graph)

# Iterative DFS with a stack
def dfs_iter(start: int, graph: dict) -> set[int]:
    visited = set()
    stack = [start]
    while stack:
        node = stack.pop()
        if node in visited:
            continue
        visited.add(node)
        for nb in graph[node]:
            if nb not in visited:
                stack.append(nb)
    return visited

# BFS with a queue
def bfs(start: int, graph: dict) -> set[int]:
    visited = {start}
    queue = deque([start])
    while queue:
        node = queue.popleft()
        for nb in graph[node]:
            if nb not in visited:
                visited.add(nb)
                queue.append(nb)
    return visited

End-of-chapter practice


9.1 Find if Path Exists in Graph (LC 1971)

Problem

Given n nodes labelled 0..n-1 and an array of undirected edges edges[i] = [u, v], plus source and destination, return True if there is a path between them.

Example

Input:  n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
Output: True

Input:  n = 6, edges = [[0,1],[0,2],[3,5],[5,4],[4,3]], source = 0, destination = 5
Output: False

Constraints

Clarifying questions

Approach

Three classic approaches, all O(V + E): 1. BFS — level traversal, return as soon as we hit destination. 2. DFS — recursive or stack-based. 3. Union Find (Chapter 24) — merge endpoints of every edge under a single root; check find(source) == find(destination). Great when the problem asks many “is there a path between u, v?” queries.

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    """BFS — cleanest for a single query."""

    def validPath(self, n: int, edges: List[List[int]],
                  source: int, destination: int) -> bool:
        if source == destination:
            return True
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        visited = {source}
        queue = deque([source])
        while queue:
            node = queue.popleft()
            for nb in graph[node]:
                if nb == destination:
                    return True
                if nb not in visited:
                    visited.add(nb)
                    queue.append(nb)
        return False

Complexity

Comments


9.2 Clone Graph (LC 133)

Problem

Given a node in an undirected, connected graph. Each Node has val: int and neighbors: list[Node]. Return a deep copy of the graph: every new node is a distinct instance with neighbors pointing at the corresponding new nodes.

Example

Input:  adjList = [[2,4], [1,3], [2,4], [1,3]]
        (adjList[i-1] = neighbour list of node i; values are 1-indexed
         per LC. The actual API parameter is Node = adjList[0] = node 1)

        Corresponding graph:
            1 ── 2
            │    │
            4 ── 3

Output: [[2,4], [1,3], [2,4], [1,3]]
        (same adjacency structure, but EVERY Node in the output is a NEW
         instance; no Node is shared with the input)

Constraints

Clarifying questions

Approach

Same pattern as LC 138 (Copy List with Random Pointer, problem 7.8): use a hash map original_to_copy to avoid duplicates and to handle cycles.

BFS or DFS works — the crucial part is check visited via the dict.

Illustration for graph 1—2—3—4—1:

Start: visit 1.
  cloned = {1: Node(1)}
  queue = [1]

Pop 1:  neighbours = [2, 4]
  Create Node(2), Node(4); add to cloned.
  cloned[1].neighbors = [cloned[2], cloned[4]]
  queue = [2, 4]

Pop 2:  neighbours = [1, 3]
  cloned[1] exists; create Node(3).
  cloned[2].neighbors = [cloned[1], cloned[3]]
  queue = [4, 3]

Pop 4:  neighbours = [1, 3]
  Both exist in cloned.
  cloned[4].neighbors = [cloned[1], cloned[3]]

Pop 3:  neighbours = [2, 4]
  Both exist.
  cloned[3].neighbors = [cloned[2], cloned[4]]

→ Return cloned[1] as the head of the copy.

Code (Python 3)

from collections import deque

class Node:
    def __init__(self, val: int = 0, neighbors: "list[Node] | None" = None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []


class Solution:
    """BFS with a hash map."""

    def cloneGraph(self, node: "Node | None") -> "Node | None":
        if not node:
            return None

        cloned: dict["Node", "Node"] = {node: Node(node.val)}
        queue: deque["Node"] = deque([node])

        while queue:
            cur = queue.popleft()
            for nb in cur.neighbors:
                if nb not in cloned:
                    cloned[nb] = Node(nb.val)
                    queue.append(nb)
                cloned[cur].neighbors.append(cloned[nb])

        return cloned[node]


class SolutionDFS:
    """Recursive DFS — shorter code."""

    def cloneGraph(self, node: "Node | None") -> "Node | None":
        cloned: dict["Node", "Node"] = {}

        def dfs(cur: "Node") -> "Node":
            if cur in cloned:
                return cloned[cur]
            copy = Node(cur.val)
            cloned[cur] = copy           # MUST set BEFORE recursing on neighbours
            copy.neighbors = [dfs(nb) for nb in cur.neighbors]
            return copy

        return dfs(node) if node else None

Complexity

Comments


9.3 Number of Connected Components (LC 323)

Problem

Given n nodes labelled 0..n-1 and an array of undirected edges, count the number of connected components.

Example

Input:  n = 5, edges = [[0,1],[1,2],[3,4]]
Output: 2
Explanation: 2 components {0,1,2} and {3,4}.

Input:  n = 5, edges = [[0,1],[1,2],[2,3],[3,4]]
Output: 1

Constraints

Clarifying questions

Approach

Approach 1 — DFS/BFS from each unvisited node — O(V + E). For each unvisited node, increment a counter and DFS/BFS-mark the whole component.

Approach 2 — Union Find — O((V + E) · α(V)). Union endpoints of every edge under the same root. Count distinct roots.

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        visited = [False] * n
        count = 0

        def dfs(node: int) -> None:
            visited[node] = True
            for nb in graph[node]:
                if not visited[nb]:
                    dfs(nb)

        for i in range(n):
            if not visited[i]:
                count += 1
                dfs(i)
        return count


class SolutionUF:
    """Union Find — nicer when many queries appear."""

    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        parent = list(range(n))

        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]   # path compression
                x = parent[x]
            return x

        def union(x: int, y: int) -> bool:
            px, py = find(x), find(y)
            if px == py:
                return False
            parent[px] = py
            return True

        components = n
        for u, v in edges:
            if union(u, v):
                components -= 1
        return components

Complexity

Comments


9.4 Course Schedule (LC 207)

Problem

There are numCourses courses labelled 0..numCourses-1. Each prerequisites[i] = [a, b] means to take course a you must first finish course b. Return True if you can finish all courses, otherwise False.

Example

Input:  numCourses = 2, prerequisites = [[1, 0]]
Output: True

Input:  numCourses = 2, prerequisites = [[1, 0], [0, 1]]
Output: False    (cycle: 1 requires 0, 0 requires 1)

Constraints

Clarifying questions

Approach

Reformulation: build a directed graph b → a (b must finish before a). The question becomes: does the graph have a cycle? No cycle → all courses can be finished.

Approach 1 — DFS with three colours (white / gray / black).

If DFS encounters gray → back-edge → cycle.

Approach 2 — Topological sort BFS (Kahn’s algorithm).

Count indegree of each node. Enqueue nodes with indegree == 0. Pop and decrement the indegree of neighbours. If at the end we processed numCourses nodes → DAG; otherwise a cycle exists.

Topological sort is the heart of Chapter 13. We introduce it early because Course Schedule is the classic application.

Illustration — 3-colour DFS with the cycle 0 → 1 → 0:

Start: all white.
DFS(0):
  mark 0 = gray.
  visit 1 (white):
    DFS(1):
      mark 1 = gray.
      visit 0 (gray!) → back-edge detected → return False (cycle)

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    """3-colour DFS."""

    WHITE, GRAY, BLACK = 0, 1, 2

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        graph = defaultdict(list)
        for a, b in prerequisites:
            graph[b].append(a)            # b → a

        color = [self.WHITE] * numCourses

        def has_cycle(node: int) -> bool:
            if color[node] == self.GRAY:
                return True               # back-edge
            if color[node] == self.BLACK:
                return False              # already cleared
            color[node] = self.GRAY
            for nb in graph[node]:
                if has_cycle(nb):
                    return True
            color[node] = self.BLACK
            return False

        for i in range(numCourses):
            if has_cycle(i):
                return False
        return True


class SolutionKahn:
    """BFS topological sort (Kahn's algorithm)."""

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        graph = defaultdict(list)
        indeg = [0] * numCourses
        for a, b in prerequisites:
            graph[b].append(a)
            indeg[a] += 1

        queue = deque(i for i, d in enumerate(indeg) if d == 0)
        processed = 0
        while queue:
            node = queue.popleft()
            processed += 1
            for nb in graph[node]:
                indeg[nb] -= 1
                if indeg[nb] == 0:
                    queue.append(nb)

        return processed == numCourses

Complexity

Comments


9.5 Is Graph Bipartite? (LC 785)

Problem

Given an undirected graph as adjacency list graph[i] = [neighbours of i], return True if the graph is bipartite — the vertices can be split into two sets such that every edge connects vertices from different sets.

Example

Input:  graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
Output: False
Explanation: 0—1—2—0 is a triangle → cannot 2-colour.

Input:  graph = [[1,3],[0,2],[1,3],[0,2]]
Output: True
Explanation: set A = {0, 2}, set B = {1, 3}.

Constraints

Clarifying questions

Approach

Equivalent: bipartite ↔︎ can 2-colour such that no two adjacent vertices share a colour.

BFS/DFS with 2-colouring: start each component, colour the first vertex 0. As BFS/DFS visits neighbours, flip the colour. If a neighbour already has the same colour → return False.

Illustration — triangle (not bipartite):

       0
      / \
     1───2

BFS from 0:
  color[0] = 0.
  Visit 1: color[1] = 1 (opposite of 0).
  Visit 2: color[2] = 1 (opposite of 0).
  From 1, visit 2: color[2] is already 1 == color[1] = 1 → CONFLICT → False

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def isBipartite(self, graph: List[List[int]]) -> bool:
        n = len(graph)
        color = [-1] * n        # -1 = uncoloured

        for start in range(n):
            if color[start] != -1:
                continue
            # BFS the component containing `start`.
            color[start] = 0
            queue = deque([start])
            while queue:
                node = queue.popleft()
                for nb in graph[node]:
                    if color[nb] == -1:
                        color[nb] = 1 - color[node]
                        queue.append(nb)
                    elif color[nb] == color[node]:
                        return False
        return True

Complexity

Comments


9.6 Evaluate Division (LC 399)

Problem

Given equations equations[i] = [Ai, Bi] and values values[i], meaning Ai / Bi = values[i]. Given queries queries[j] = [Cj, Dj], answer each with Cj / Dj, or -1 if it cannot be determined.

Example

Input:  equations = [["a","b"], ["b","c"]]
        values    = [2.0, 3.0]
        queries   = [["a","c"], ["b","a"], ["a","e"], ["a","a"], ["x","x"]]
Output: [6.0, 0.5, -1.0, 1.0, -1.0]
Explanation:
  a/c = a/b * b/c = 2 * 3 = 6
  b/a = 1/(a/b) = 0.5
  a/e is undefined (e is not in any equation)
  a/a = 1
  x/x: x not in any equation → -1

Constraints

Clarifying questions

Approach

Insight: each equation A / B = k ↔︎ two edges in a directed weighted graph: - A → B with weight k. - B → A with weight 1/k.

Then C / D = product of weights along any path from C to D. No path → return -1.

DFS on the weighted graph suffices. A tighter solution: Weighted Union Find (see Chapter 24).

Illustration for equations a/b=2, b/c=3:

Weighted graph:
            ── 2 ──>          ── 3 ──>
       a               b               c
            <─ 0.5 ──         <─ 1/3 ─

Query a/c: DFS from a:
  a → b (× 2), then b → c (× 3) → product 2 * 3 = 6.  ✓

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def calcEquation(
        self, equations: List[List[str]],
        values: List[float], queries: List[List[str]]
    ) -> List[float]:
        graph: dict[str, dict[str, float]] = defaultdict(dict)
        for (a, b), v in zip(equations, values):
            graph[a][b] = v
            graph[b][a] = 1.0 / v

        def dfs(src: str, dst: str, visited: set[str]) -> float:
            if src not in graph or dst not in graph:
                return -1.0
            if src == dst:
                return 1.0
            visited.add(src)
            for nb, weight in graph[src].items():
                if nb in visited:
                    continue
                sub = dfs(nb, dst, visited)
                if sub != -1.0:
                    return weight * sub
            return -1.0

        return [dfs(c, d, set()) for c, d in queries]

Complexity

Comments

Chapter summary & decisions

Graph problem diagnosis

Symptom in the problem Best pattern Chapter
“Path from A to B?” BFS / DFS / Union Find 10, 11, 24
“Number of clusters/islands” DFS / Union Find 12, 24
“Ordering with constraints” Topological sort 13
“Shortest path positive weights Dijkstra (Chapter 30)
“Shortest path 0/1 edges 0-1 BFS / regular BFS
“All-pairs shortest” Floyd-Warshall O(V³)
“Minimum spanning network” MST (Chapter 33)
“Bottleneck min/max on path” Kruskal + DSU / BS + BFS (37)
“Bipartite?” BFS/DFS 2-colour

Clone Graph (LC 133) — mapping diagram

old:  1 — 2
       \  /
        3 — 4

old_to_new = {1:1', 2:2', 3:3', 4:4'}   (dict from original to copy)
clone(node):
    if node in old_to_new: return old_to_new[node]
    new = Node(node.val)
    old_to_new[node] = new       # SET BEFORE recursing → avoid cycles
    for nei in node.neighbors:
        new.neighbors.append(clone(nei))
    return new

Evaluate Division (LC 399) — weighted DFS

Course Schedule in this chapter = teaser

The full Topological Sort treatment lives in Chapter 13. This chapter only introduces DFS cycle detection.


Chapter 10 — Breadth-First Search (BFS)

BFS traverses a graph level by level. The key property: if every edge has the same weight (= 1), BFS from source yields the shortest path to every other vertex. That’s why BFS shows up repeatedly in “shortest path on an unweighted graph”, “minimum steps”, “minimum transformations”, … problems.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

BFS vs DFS: - BFS: shortest path, level traversal. - DFS: deep exploration (first path to a target), connectivity, count components.

Template code

from collections import deque

# 1) Standard BFS — shortest path from start to target
def bfs_shortest(start, target, neighbors_fn) -> int:
    if start == target:
        return 0
    visited = {start}
    queue = deque([(start, 0)])         # (node, distance)
    while queue:
        node, dist = queue.popleft()
        for nb in neighbors_fn(node):
            if nb == target:
                return dist + 1
            if nb not in visited:
                visited.add(nb)
                queue.append((nb, dist + 1))
    return -1


# 2) Level BFS — no need to store distance in the queue
def bfs_by_level(start, neighbors_fn):
    visited = {start}
    queue = deque([start])
    level = 0
    while queue:
        size = len(queue)
        for _ in range(size):
            node = queue.popleft()
            # ... process node at this level ...
            for nb in neighbors_fn(node):
                if nb not in visited:
                    visited.add(nb)
                    queue.append(nb)
        level += 1


# 3) Multi-source BFS — push many sources into the queue simultaneously
def multi_source(sources: list, neighbors_fn):
    queue = deque(sources)
    visited = set(sources)
    while queue:
        ...

End-of-chapter practice


10.1 Binary Tree Level Order Traversal (LC 102)

Problem

Given the root of a binary tree, return its level-order traversal as a list of lists (each inner list contains the nodes at one level, top to bottom, left to right).

Example

Input:  root = [3, 9, 20, null, null, 15, 7]   (LC level-order serialisation)

        Actual tree:
              3
             / \
            9   20
               /  \
              15   7

Output: [[3], [9, 20], [15, 7]]

Constraints

Clarifying questions

Approach

Level-order BFS. Each outer loop iteration = one level. At the start of each iteration, record size = len(queue), then pop exactly size nodes — those form the entire current level.

Illustration:

Init:  queue = [3]
Level 0: size=1
  Pop 3. result.append([3]). Push 9, 20.
  queue = [9, 20]

Level 1: size=2
  Pop 9 → null children.
  Pop 20 → push 15, 7.
  result.append([9, 20]).
  queue = [15, 7]

Level 2: size=2
  Pop 15, 7 → null children.
  result.append([15, 7]).
  queue = []

→ [[3], [9, 20], [15, 7]]

Code (Python 3)

from collections import deque
from typing import List, Optional

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right


class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if not root:
            return []
        result: List[List[int]] = []
        queue = deque([root])
        while queue:
            size = len(queue)
            level_vals: list[int] = []
            for _ in range(size):
                node = queue.popleft()
                level_vals.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            result.append(level_vals)
        return result

Complexity

Comments


10.2 Rotting Oranges (LC 994) — Multi-source BFS

Problem

Given a grid grid with values: - 0 = empty cell - 1 = fresh orange - 2 = rotten orange

Every minute, each rotten orange turns its 4 axis-adjacent (up/down/left/right) fresh neighbours into rotten ones. Return the minimum number of minutes until no fresh orange remains, or -1 if impossible.

Example

Input:  grid = [[2,1,1],
                [1,1,0],
                [0,1,1]]
        (0 = empty, 1 = fresh orange, 2 = rotten)
Output: 4   (minutes for every orange to rot)

Input:  grid = [[2,1,1],
                [0,1,1],
                [1,0,1]]
Output: -1   (the orange at (2,0) is isolated and never rots)

Constraints

Clarifying questions

Approach

Insight: every rotten orange is a source. All sources spread simultaneously every minute → multi-source BFS.

Procedure: 1. Enqueue all initial rotten oranges (level 0). 2. Level-order BFS — each level increments minutes by 1. 3. Count the initial fresh oranges. Every newly rotted one decrements the count. 4. End: if any fresh remains → -1, otherwise → minutes.

Illustration with a 3x3 grid:

Init:                t=0:              t=1:              t=4:
[2, 1, 1]            [2, 1, 1]         [2, 2, 1]         [2, 2, 2]
[1, 1, 0]            [1, 1, 0]         [2, 1, 0]         [2, 2, 0]
[0, 1, 1]            [0, 1, 1]         [0, 1, 1]         [0, 2, 2]
                                       (4 fresh)         (done)

BFS dynamics:
The queue holds same-level cells (i, j) → each outer iteration pops the
entire queue then enqueues neighbours. Outer iterations = minutes.

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def orangesRotting(self, grid: List[List[int]]) -> int:
        rows, cols = len(grid), len(grid[0])
        queue: deque[tuple[int, int]] = deque()
        fresh = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == 2:
                    queue.append((r, c))
                elif grid[r][c] == 1:
                    fresh += 1

        if fresh == 0:
            return 0

        minutes = 0
        dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        while queue and fresh > 0:
            minutes += 1
            for _ in range(len(queue)):
                r, c = queue.popleft()
                for dr, dc in dirs:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                        grid[nr][nc] = 2
                        fresh -= 1
                        queue.append((nr, nc))

        return minutes if fresh == 0 else -1

Complexity

Comments


10.3 Word Ladder (LC 127)

Problem

Given beginWord, endWord, and a wordList (all same length). Each transformation step replaces exactly one character of the current word so that the new word is in wordList. Return the minimum number of steps to transform beginWordendWord (counting both ends). Return 0 if impossible.

Example

Input:  beginWord = "hit", endWord = "cog"
        wordList = ["hot","dot","dog","lot","log","cog"]
Output: 5
Explanation: hit → hot → dot → dog → cog  (length 5)

Input:  beginWord = "hit", endWord = "cog"
        wordList = ["hot","dot","dog","lot","log"]
Output: 0    (cog not in wordList)

Constraints

Clarifying questions

Approach

Modelling: each word is a vertex; an edge exists between two words that differ by exactly one character. The problem becomes shortest path in an undirected graph → BFS.

Neighbour generation optimisation: instead of comparing the current word with every word in wordList (O(N·L) per node — too slow), we generate neighbours by replacing each position with a..z (O(26·L) per node).

Illustration for "hit" → "cog":

Level 1: hit
Level 2: hot                (change i→o)
Level 3: dot, lot           (change h→d/l)
Level 4: dog, log           (change t→g)
Level 5: cog                ★ answer (5 steps)

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        word_set = set(wordList)
        if endWord not in word_set:
            return 0

        queue = deque([(beginWord, 1)])
        visited = {beginWord}
        while queue:
            word, steps = queue.popleft()
            if word == endWord:
                return steps
            for i in range(len(word)):
                for ch in 'abcdefghijklmnopqrstuvwxyz':
                    if ch == word[i]:
                        continue
                    next_word = word[:i] + ch + word[i + 1:]
                    if next_word in word_set and next_word not in visited:
                        visited.add(next_word)
                        queue.append((next_word, steps + 1))
        return 0

Complexity

Comments


10.4 Open the Lock (LC 752)

Problem

A 4-digit lock starts at "0000". Each step rotates one digit up or down by 1 (wrap 0..9). Given a list of deadends (forbidden states) and a target, return the minimum number of steps to reach target, or -1.

Example

Input:  deadends = ["0201","0101","0102","1212","2002"], target = "0202"
Output: 6
Explanation: 0000 → 1000 → 1100 → 1200 → 1201 → 1202 → 0202
            (avoiding every deadend)

Constraints

Clarifying questions

Approach

Implicit state-space graph. Each state is a 4-digit string → 10^4 = 10000 states. From each state there are 8 transitions (4 digits × 2 directions).

BFS: start from "0000", BFS to target. Skip states in deadends.

Illustration of part of BFS:

Level 0: 0000
Level 1: 1000, 9000, 0100, 0900, 0010, 0090, 0001, 0009  (8 neighbours)
Level 2: ... (each node 8 neighbours, except visited / deadend)
...
Level 6: 0202  ★

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        dead = set(deadends)
        if "0000" in dead:
            return -1
        if target == "0000":
            return 0

        def neighbors(state: str):
            for i in range(4):
                d = int(state[i])
                for delta in (-1, 1):
                    new_d = (d + delta) % 10
                    yield state[:i] + str(new_d) + state[i + 1:]

        visited = {"0000"}
        queue = deque([("0000", 0)])
        while queue:
            state, steps = queue.popleft()
            for nb in neighbors(state):
                if nb in dead or nb in visited:
                    continue
                if nb == target:
                    return steps + 1
                visited.add(nb)
                queue.append((nb, steps + 1))
        return -1

Complexity

Comments


10.5 Shortest Path in Binary Matrix (LC 1091)

Problem

Given an n × n matrix of 0 (passable) and 1 (obstacle), find the shortest path from (0,0) to (n-1,n-1) moving in 8 directions (4 axes + 4 diagonals). Path length = number of cells passed (including start and end). Return -1 if no path exists.

Example

Input:  grid = [[0,0,0],
                [1,1,0],
                [1,1,0]]
Output: 4
Explanation: (0,0) → (0,1) → (1,2) → (2,2)

Constraints

Clarifying questions

Approach

BFS from (0,0) with 8 directions. Each edge has weight 1 (one step = one cell).

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
        n = len(grid)
        if grid[0][0] != 0 or grid[n - 1][n - 1] != 0:
            return -1
        if n == 1:
            return 1

        dirs = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
        queue = deque([(0, 0, 1)])     # (r, c, steps)
        grid[0][0] = 1                  # mark visited
        while queue:
            r, c, steps = queue.popleft()
            for dr, dc in dirs:
                nr, nc = r + dr, c + dc
                if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 0:
                    if (nr, nc) == (n - 1, n - 1):
                        return steps + 1
                    grid[nr][nc] = 1
                    queue.append((nr, nc, steps + 1))
        return -1

Complexity

Comments


10.6 Snakes and Ladders (LC 909)

Problem

Given an n × n board where cells are numbered in zigzag (Snakes & Ladders style). board[i][j] = -1 means an ordinary cell; if >= 1, that’s a snake/ladder that teleports you to the indicated cell.

Each step you roll a six-sided die and move 1..6 cells; if you land on a snake/ladder, follow it immediately. Find the minimum number of rolls to reach cell n*n. Return -1 if impossible.

Example

Input:  board =
        [[-1,-1,-1,-1,-1,-1],
         [-1,-1,-1,-1,-1,-1],
         [-1,-1,-1,-1,-1,-1],
         [-1,35,-1,-1,13,-1],
         [-1,-1,-1,-1,-1,-1],
         [-1,15,-1,-1,-1,-1]]
Output: 4

Constraints

Clarifying questions

Approach

State space: each cell is labelled 1..n². From cell s we can reach s+1, s+2, ..., s+6 (then teleport if a snake/ladder lives there). Each edge = one roll → BFS yields the minimum number of rolls.

Zigzag → coordinate trick: - Row (counting from the bottom): (label - 1) // n. - Column depends on row parity: even rows from the bottom run left→right, odd rows run right→left.

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def snakesAndLadders(self, board: List[List[int]]) -> int:
        n = len(board)
        def label_to_pos(label: int) -> tuple[int, int]:
            quot, rem = divmod(label - 1, n)
            row = n - 1 - quot
            col = rem if quot % 2 == 0 else n - 1 - rem
            return row, col

        target = n * n
        visited = {1}
        queue = deque([(1, 0)])     # (square, throws)
        while queue:
            square, throws = queue.popleft()
            for d in range(1, 7):
                nxt = square + d
                if nxt > target:
                    break
                r, c = label_to_pos(nxt)
                if board[r][c] != -1:
                    nxt = board[r][c]
                if nxt == target:
                    return throws + 1
                if nxt not in visited:
                    visited.add(nxt)
                    queue.append((nxt, throws + 1))
        return -1

Complexity

Comments

Chapter summary & decisions

BFS state design (broader than you might think)

State Representative problem
node Shortest path on an unweighted graph
(r, c) Grid (Number of Islands, 01 Matrix)
word Word Ladder
(r, c, k_remaining) Shortest Path with K Obstacles
board_serialized Sliding Puzzle, Open Lock
bitmask_visited Shortest Path Visiting All Nodes
(node, parity) Bipartite, even/odd step counting

Word Ladder — neighbour generation

Snakes & Ladders — 1D ↔︎ 2D

Serpentine n×n board: index i (1..n²) → coordinates:

row_from_bottom = (i - 1) // n     # 0 = bottom row
col_in_row      = (i - 1) % n
r = n - 1 - row_from_bottom
c = col_in_row if row_from_bottom % 2 == 0 else n - 1 - col_in_row

Classic bug: forgetting to flip on odd rows, or 0/1 indexing.

Distance: level BFS vs storing in the queue


Chapter 11 — Depth-First Search (DFS)

DFS goes as deep as possible before backtracking. This is the natural pattern for any tree/graph problem with a recursive structure: depth, path sum, validate, LCA, … This chapter focuses on DFS on trees — easy to memorise and very common in interviews. DFS on grids is covered in Chapter 12 (Island Matrix).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Three DFS templates on trees: 1. Top-down: pass state down (e.g. path_so_far, current_sum). 2. Bottom-up: leaves return values upward, the node aggregates from children. 3. Hybrid: pass down and collect back up.

Input format (applies to ALL tree problems in this chapter)

Every tree problem in chapters 10/11/22 uses LeetCode’s standard TreeNode:

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

Template code

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right


# 1) Bottom-up: return a value from children
def dfs_bottom_up(node) -> int:
    if not node:
        return 0
    left = dfs_bottom_up(node.left)
    right = dfs_bottom_up(node.right)
    return combine(node.val, left, right)


# 2) Top-down: pass state down
def dfs_top_down(node, state) -> None:
    if not node:
        return
    new_state = update(state, node.val)
    if is_leaf(node):
        # ... record result ...
        return
    dfs_top_down(node.left, new_state)
    dfs_top_down(node.right, new_state)


# 3) Iterative DFS via stack
def dfs_iter(root):
    stack = [root]
    while stack:
        node = stack.pop()
        if not node:
            continue
        # ... visit node ...
        stack.append(node.right)
        stack.append(node.left)     # left ends up on top first

End-of-chapter practice


11.1 Maximum Depth of Binary Tree (LC 104)

Problem

Given the root of a binary tree, return its maximum depth (number of nodes on the longest root-to-leaf path).

Example

Input:  root = [3, 9, 20, null, null, 15, 7]   (LC level-order serialisation)

        Actual tree:
              3
             / \
            9   20
               /  \
              15   7

Output: 3

Constraints

Clarifying questions

Approach

Bottom-up one-liner:

depth(node) = 1 + max(depth(left), depth(right)), base case empty → 0.

Code (Python 3)

class Solution:
    def maxDepth(self, root) -> int:
        if not root:
            return 0
        return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))

Complexity

Comments


11.2 Path Sum II (LC 113)

Problem

Given root and targetSum, return all root-to-leaf paths whose values sum to targetSum.

Example

Input:  root = [5, 4, 8, 11, null, 13, 4, 7, 2, null, null, 5, 1]
        targetSum = 22

        Actual tree:
                  5
                 / \
                4   8
               /   / \
              11  13  4
             /  \    / \
            7    2  5   1

Output: [[5,4,11,2], [5,8,4,5]]

Constraints

Clarifying questions

Approach

Top-down DFS + backtracking: - Descend each node, decrementing target and appending the node to path. - At a leaf: if target == leaf.val → copy path into the result. - On return (after children) → path.pop() to restore state.

Illustration — the path 5 → 4 → 11 → 2 for target = 22:

DFS(5, target=22, path=[]):
  path=[5], remaining=17
  DFS(4, 17):
    path=[5,4], remaining=13
    DFS(11, 13):
      path=[5,4,11], remaining=2
      DFS(7, 2):  leaf, 7 != 2 → skip
                  pop → path=[5,4,11]
      DFS(2, 2):  leaf, 2 == 2 → ADD [5,4,11,2] to result
                  pop → path=[5,4,11]
      pop → path=[5,4]
    pop → path=[5]
  ...

Code (Python 3)

from typing import List, Optional

class Solution:
    def pathSum(self, root: Optional["TreeNode"], targetSum: int) -> List[List[int]]:
        result: list[list[int]] = []
        path: list[int] = []

        def dfs(node, remaining: int) -> None:
            if not node:
                return
            path.append(node.val)
            remaining -= node.val
            if not node.left and not node.right and remaining == 0:
                result.append(path.copy())
            else:
                dfs(node.left, remaining)
                dfs(node.right, remaining)
            path.pop()                  # backtrack

        dfs(root, targetSum)
        return result

Complexity

Comments


11.3 All Paths From Source to Target (LC 797)

Problem

Given a DAG graph (adjacency list, node i has neighbours graph[i]), return all paths from node 0 to node n - 1.

Example

Input:  graph = [[1,2],[3],[3],[]]
        # graph:  0 → 1 → 3
        #         0 → 2 → 3
Output: [[0,1,3], [0,2,3]]

Constraints

Clarifying questions

Approach

DAG ⇒ no cycle ⇒ no visited required. DFS from 0; whenever we reach n-1, record path.

Code (Python 3)

from typing import List

class Solution:
    def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
        n = len(graph)
        result: list[list[int]] = []
        path: list[int] = [0]

        def dfs(node: int) -> None:
            if node == n - 1:
                result.append(path.copy())
                return
            for nb in graph[node]:
                path.append(nb)
                dfs(nb)
                path.pop()

        dfs(0)
        return result

Complexity

Comments


11.4 Validate Binary Search Tree (LC 98)

Problem

Given root, determine whether the tree is a valid BST under: - Every left descendant: value strictly less than the current node. - Every right descendant: value strictly greater than the current node. - Both subtrees are also BSTs.

Example

Input:  root = [2, 1, 3]
        Actual tree:
              2
             / \
            1   3
Output: true

Input:  root = [5, 1, 4, null, null, 3, 6]
        Actual tree:
              5
             / \
            1   4
               / \
              3   6
Output: false
        (node 3 is in the right subtree of 5, but 3 < 5 → violates BST)

Constraints

Clarifying questions

Approach

Common wrong approach — checking only node.left.val < node.val < node.right.val. Wrong because BST requires the entire left subtree < node, not just the direct child.

Counterexample:

    5
   / \
  1   4
     / \
    3   6

At node 5: 4 < 5 (OK), 1 < 5 (OK). At node 4: 3 < 4 < 6 (OK). Local checks pass, but 3 < 5 is in the right subtree → invalid.

Correct approach — DFS with (low, high) bounds:

dfs(node, low, high): node must satisfy low < node.val < high. Going down: - Left: new bound (low, node.val). - Right: new bound (node.val, high).

Approach 2 — Inorder traversal must be strictly ascending.

BST inorder = sorted sequence. Traverse inorder and check each value exceeds the previous.

Code (Python 3)

import math
from typing import Optional

class Solution:
    """Approach 1 — DFS with (low, high) bounds."""

    def isValidBST(self, root: Optional["TreeNode"]) -> bool:

        def dfs(node, low: float, high: float) -> bool:
            if not node:
                return True
            if not (low < node.val < high):
                return False
            return dfs(node.left, low, node.val) and \
                   dfs(node.right, node.val, high)

        return dfs(root, -math.inf, math.inf)


class SolutionInorder:
    """Approach 2 — inorder traversal."""

    def isValidBST(self, root) -> bool:
        self.prev = -math.inf

        def inorder(node) -> bool:
            if not node:
                return True
            if not inorder(node.left):
                return False
            if node.val <= self.prev:
                return False
            self.prev = node.val
            return inorder(node.right)

        return inorder(root)

Complexity

Comments


11.5 House Robber III (LC 337)

Problem

Given root of a binary tree (LC level-order serialised, e.g. [3,2,3,null,3,null,1]). Each node represents a house with value node.val. The thief cannot rob two adjacent houses (parent ↔︎ child). Return the maximum money he can steal.

Example

Input:  root = [3, 2, 3, null, 3, null, 1]
        Actual tree:
              3
             / \
            2   3
             \   \
              3   1
Output: 7
        (rob 3 + 3 + 1 = 7)

Input:  root = [3, 4, 5, 1, 3, null, 1]
        Actual tree:
              3
             / \
            4   5
           / \   \
          1   3   1
Output: 9
        (rob 4 + 5 = 9)

Constraints

Clarifying questions

Approach

Tree DP — each node returns two values: - rob_this = max money if we rob this node (children skipped). - skip_this = max money if we don’t rob this node (children can do whatever they want).

Recurrences: - rob_this = node.val + left.skip + right.skip - skip_this = max(left.rob, left.skip) + max(right.rob, right.skip)

Answer = max(root.rob, root.skip).

Illustration:

       3
      / \
     2   3
      \   \
       3   1

Post-order DFS:
  node 3 (left-leaf of 2): rob=3, skip=0
  node 1 (right-leaf of right 3): rob=1, skip=0

  node 2: rob = 2 + 0 (no left) + 0 (skip the 3) = 2
          skip = 0 + max(3, 0) = 3
  node 3 (right child of root): rob = 3 + max(0, 0)(no left) + 0 = 3
                                skip = 0 + max(1, 0) = 1

  root 3: rob = 3 + 3 (skip 2) + 1 (skip right-3) = 7
          skip = max(2,3) + max(3,1) = 3 + 3 = 6

Answer: max(7, 6) = 7

Code (Python 3)

from typing import Optional, Tuple

class Solution:
    def rob(self, root: Optional["TreeNode"]) -> int:

        def dfs(node) -> Tuple[int, int]:
            """Return (rob_this, skip_this)."""
            if not node:
                return 0, 0
            l_rob, l_skip = dfs(node.left)
            r_rob, r_skip = dfs(node.right)
            rob_this = node.val + l_skip + r_skip
            skip_this = max(l_rob, l_skip) + max(r_rob, r_skip)
            return rob_this, skip_this

        return max(dfs(root))

Complexity

Comments


11.6 Lowest Common Ancestor of a Binary Tree (LC 236)

Problem

Given root of a binary tree (not a BST) and two nodes p, q, find their lowest common ancestor (LCA) — the lowest node that has both p and q in its subtree.

Example

Input:  root = [3, 5, 1, 6, 2, 0, 8, null, null, 7, 4]
        Actual tree:
              3
             / \
            5   1
           / \ / \
          6  2 0  8
            / \
           7   4

Input:  p = 5, q = 1   → Output: 3
Input:  p = 5, q = 4   → Output: 5   (a node is its own ancestor)

Constraints

Clarifying questions

Approach

Subtle insight: at each node: - If node == p or node == q → return node straight away. - Recurse on left and right. - If both recursions return non-None → node is the LCA. - If only one is non-None → return that one (both p and q lie on the same side).

Code (Python 3)

from typing import Optional

class Solution:
    def lowestCommonAncestor(self, root, p, q):
        if not root or root is p or root is q:
            return root

        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)

        if left and right:
            return root          # p and q on different sides → root is LCA
        return left if left else right

Complexity

Comments

Chapter summary & decisions

Traversal order cheat sheet

Traversal When to use
Pre-order (root → L → R) Serialise, clone, copy
In-order (L → root → R) BST sorted output, kth smallest
Post-order (L → R → root) Aggregate from children (tree DP, diameter)
Level-order (BFS) By layer, by distance

DFS return-value design (heart of tree DP)

def dfs(node):
    if not node: return base
    L = dfs(node.left)
    R = dfs(node.right)
    # combine L, R with node.val → answer for this subtree
    # UPDATE global answer if needed
    return result_to_pass_up

Validate BST — global bounds

House Robber III — (rob, skip) trace

Tree:

    3
   / \
  2   3
   \   \
    3   1

Post-order returns (rob_this, skip_this): - Leaf 3 (left of 2): (3, 0). - Leaf 1 (right of right-3): (1, 0). - Node 2: rob = 2 + 0 = 2, skip = max(3,0) = 3(2, 3). - Node 3 (right of root): rob = 3 + 0 = 3, skip = max(1,0) = 1(3, 1). - Root 3: rob = 3 + 3 + 1 = 7, skip = max(2,3) + max(3,1) = 3 + 3 = 6max(7,6) = 7.

LCA variants — choose wisely

Tree type How
General binary tree Bottom-up recursion, return node if it contains p or q (LC 236)
BST Compare values with root, walk one side (LC 235) — O(log n)
With parent pointer Hash ancestors of p, then walk up from q

Chapter 12 — Island Matrix Traversal

A 2D grid is really an implicit graph: each cell is a node, and the 4 adjacent cells (up/down/left/right) are edges. Every “island” / “region painting” / “flood fill” problem is just DFS/BFS on that graph. This chapter teaches 4 tricks specific to grids: (1) flood fill, (2) multi-source BFS from the border, (3) reverse thinking (mark what we don’t need), (4) mutating the input to mark visited.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

from collections import deque
from typing import List

DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

# 1) Flood fill DFS (recursive)
def flood_fill(grid: List[List[int]], r: int, c: int, marker: int) -> int:
    rows, cols = len(grid), len(grid[0])
    if not (0 <= r < rows and 0 <= c < cols) or grid[r][c] != 1:
        return 0
    grid[r][c] = marker             # mark visited
    size = 1
    for dr, dc in DIRS:
        size += flood_fill(grid, r + dr, c + dc, marker)
    return size


# 2) Multi-source BFS from all boundary or all "special" cells
def multi_source_bfs(grid, sources):
    queue = deque(sources)
    visited = set(sources)
    while queue:
        r, c = queue.popleft()
        for dr, dc in DIRS:
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in visited:
                visited.add((nr, nc))
                queue.append((nr, nc))
    return visited

End-of-chapter practice


12.1 Number of Islands (LC 200)

Problem

Given a grid m × n with '1' = land and '0' = water. An island is a maximally 4-connected region of land. Count the number of islands.

Example

Input:
[["1","1","1","1","0"],
 ["1","1","0","1","0"],
 ["1","1","0","0","0"],
 ["0","0","0","0","0"]]
Output: 1   (all "1"s connect into one island)

Input:
[["1","1","0","0","0"],
 ["1","1","0","0","0"],
 ["0","0","1","0","0"],
 ["0","0","0","1","1"]]
Output: 3   (top-left, middle, bottom-right)

Constraints

Clarifying questions

Approach

Classic flood fill pattern. Walk every cell: - If it’s '1' and not yet visited → increment counter, DFS/BFS to mark the whole island as visited (change '1''0' to avoid a separate set).

Illustration for the 4×5 grid above:

Step 1, cell (0,0) = '1' → DFS:
[1 1 0 0 0]      [* * 0 0 0]
[1 1 0 0 0]  →   [* * 0 0 0]
[0 0 1 0 0]      [0 0 1 0 0]
[0 0 0 1 1]      [0 0 0 1 1]
                 (island 1 marked)

Step 2, hit (2,2) = '1' → DFS (marks itself only):
[* * 0 0 0]
[* * 0 0 0]
[0 0 * 0 0]
[0 0 0 1 1]

Step 3, hit (3,3) = '1' → DFS:
[* * 0 0 0]
[* * 0 0 0]
[0 0 * 0 0]
[0 0 0 * *]

Count: 3 islands

Code (Python 3)

from typing import List

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        if not grid or not grid[0]:
            return 0
        rows, cols = len(grid), len(grid[0])
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        def dfs(r: int, c: int) -> None:
            if not (0 <= r < rows and 0 <= c < cols) or grid[r][c] != '1':
                return
            grid[r][c] = '0'              # mark visited
            for dr, dc in DIRS:
                dfs(r + dr, c + dc)

        count = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == '1':
                    count += 1
                    dfs(r, c)
        return count

Complexity

Comments


12.2 Max Area of Island (LC 695)

Problem

Same 0/1 grid. Return the largest area of any island (cell count), or 0 if there are no islands.

Example

Input:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
 [0,0,0,0,0,0,0,1,1,1,0,0,0],
 [0,1,1,0,1,0,0,0,0,0,0,0,0],
 [0,1,0,0,1,1,0,0,1,0,1,0,0],
 [0,1,0,0,1,1,0,0,1,1,1,0,0],
 [0,0,0,0,0,0,0,0,0,0,1,0,0],
 [0,0,0,0,0,0,0,1,1,1,0,0,0],
 [0,0,0,0,0,0,0,1,1,0,0,0,0]]
Output: 6

Constraints

Clarifying questions

Approach

A variant of 12.1, but DFS returns the size instead of void. Take max across all DFS starts.

Code (Python 3)

from typing import List

class Solution:
    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        if not grid:
            return 0
        rows, cols = len(grid), len(grid[0])
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        def dfs(r: int, c: int) -> int:
            if not (0 <= r < rows and 0 <= c < cols) or grid[r][c] != 1:
                return 0
            grid[r][c] = 0
            return 1 + sum(dfs(r + dr, c + dc) for dr, dc in DIRS)

        best = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == 1:
                    best = max(best, dfs(r, c))
        return best

Complexity

Comments


12.3 Surrounded Regions (LC 130)

Problem

Given a grid of 'X' and 'O'. Flip every 'O' region that is fully surrounded by 'X' (regions that don’t touch the grid boundary) into 'X'. 'O' regions touching the boundary are preserved.

Example

Input:  board = [['X','X','X','X'],
                 ['X','O','O','X'],
                 ['X','X','O','X'],
                 ['X','O','X','X']]
Output: board = [['X','X','X','X'],
                 ['X','X','X','X'],
                 ['X','X','X','X'],
                 ['X','O','X','X']]

(mutate in place; the 'O' at (3,1) touches the bottom border → keep;
 the inner 'O' cluster is surrounded → flip to 'X')

Constraints

Clarifying questions

Approach

Reverse thinking — an extremely useful pattern: - Instead of finding the surrounded 'O' regions, find the 'O' regions that touch the border (much easier). - Mark them (e.g. temporarily change to '#'). - Final pass: - '#''O' (keep). - Remaining 'O''X' (flip).

Illustration:

Initial grid:               After DFS from boundary 'O's (marker '#'):
X X X X                     X X X X
X O O X                     X O O X     (the O at (1,1),(1,2) does NOT touch
X X O X         →           X X O X      the border → not marked)
X O X X                     X # X X     (O at (3,1) touches border → marked)

Final sweep:
'#' → 'O'; 'O' → 'X':
X X X X
X X X X
X X X X
X O X X

Code (Python 3)

from typing import List

class Solution:
    def solve(self, board: List[List[str]]) -> None:
        if not board:
            return
        rows, cols = len(board), len(board[0])

        def dfs(r: int, c: int) -> None:
            if not (0 <= r < rows and 0 <= c < cols) or board[r][c] != 'O':
                return
            board[r][c] = '#'
            dfs(r + 1, c); dfs(r - 1, c); dfs(r, c + 1); dfs(r, c - 1)

        # 1. DFS from every boundary 'O' — mark them as '#'.
        for r in range(rows):
            dfs(r, 0)
            dfs(r, cols - 1)
        for c in range(cols):
            dfs(0, c)
            dfs(rows - 1, c)

        # 2. Flip: '#' → 'O' (keep); 'O' → 'X' (flip).
        for r in range(rows):
            for c in range(cols):
                if board[r][c] == 'O':
                    board[r][c] = 'X'
                elif board[r][c] == '#':
                    board[r][c] = 'O'

Complexity

Comments


12.4 Pacific Atlantic Water Flow (LC 417)

Problem

Given a matrix heights[i][j] representing island heights. The left and top edges border the Pacific; the right and bottom edges border the Atlantic. Water flows from (r, c) to a neighbouring cell of height ≤ (r, c).

Return all (r, c) from which water can flow to both oceans.

Example

Input:  heights = [[1,2,2,3,5],
                   [3,2,3,4,4],
                   [2,4,5,3,1],
                   [6,7,1,4,5],
                   [5,1,1,2,4]]
Output: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]

Constraints

Clarifying questions

Approach

Forward thinking — TLE. For each cell, BFS to see which oceans it can reach. Worst-case O((mn)²).

Reverse thinking — O(m · n).

Instead of “which cells flow to the ocean?”, ask “where can the ocean climb up to?”. The ocean climbs only to cells of height the current one.

Illustration — Pacific reach (P) and Atlantic reach (A) on a 5x5 matrix:

P P P P P/A                   A A A A A
P . . . A             P/A . . . A
P . . . A             P . . . A
P/A . . . A             P . . . A
P/A A A A A             P P P P P

P ∩ A = cells in both sets → answer.

Code (Python 3)

from typing import List

class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        if not heights:
            return []
        rows, cols = len(heights), len(heights[0])
        pacific: set[tuple[int, int]] = set()
        atlantic: set[tuple[int, int]] = set()

        def dfs(r: int, c: int, visited: set, prev_h: int) -> None:
            if (r, c) in visited:
                return
            if not (0 <= r < rows and 0 <= c < cols):
                return
            if heights[r][c] < prev_h:    # ocean cannot climb to a lower cell
                return
            visited.add((r, c))
            for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
                dfs(r + dr, c + dc, visited, heights[r][c])

        # Pacific: left + top edges.
        for r in range(rows):
            dfs(r, 0, pacific, heights[r][0])
        for c in range(cols):
            dfs(0, c, pacific, heights[0][c])
        # Atlantic: right + bottom edges.
        for r in range(rows):
            dfs(r, cols - 1, atlantic, heights[r][cols - 1])
        for c in range(cols):
            dfs(rows - 1, c, atlantic, heights[rows - 1][c])

        return [[r, c] for r, c in pacific & atlantic]

Complexity

Comments


12.5 Walls and Gates (LC 286)

Problem

Given rooms: an m × n integer matrix with three meaningful values: - -1 = wall (blocks the way). - 0 = gate. - INF (= 2³¹ - 1) = empty room.

Fill each empty room with the shortest distance (4-direction steps) to the nearest gate. If a room can’t reach any gate, leave it as INF.

Example

Input:  rooms = [[INF, -1,  0, INF],
                 [INF,INF,INF, -1],
                 [INF, -1,INF, -1],
                 [  0, -1,INF,INF]]
        (-1 = wall, 0 = gate, INF = empty room)

Output: rooms = [[3, -1, 0, 1],
                 [2,  2, 1,-1],
                 [1, -1, 2,-1],
                 [0, -1, 3, 4]]
        (mutate in place; each cell = 4-step distance to the nearest gate)

Constraints

Clarifying questions

Approach

Multi-source BFS — enqueue all gates simultaneously. They expand outward; each cell receives a distance equal to the steps from the nearest gate.

Why is multi-source better than BFS from each gate? Multi-source O(mn), per-gate BFS O(gates · mn)gates can be O(mn).

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def wallsAndGates(self, rooms: List[List[int]]) -> None:
        if not rooms:
            return
        rows, cols = len(rooms), len(rooms[0])
        queue: deque[tuple[int, int]] = deque()
        for r in range(rows):
            for c in range(cols):
                if rooms[r][c] == 0:
                    queue.append((r, c))

        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        while queue:
            r, c = queue.popleft()
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                # Only write to cells still at INF — BFS first-write = min.
                if 0 <= nr < rows and 0 <= nc < cols and rooms[nr][nc] == 2147483647:
                    rooms[nr][nc] = rooms[r][c] + 1
                    queue.append((nr, nc))

Complexity

Comments


12.6 01 Matrix (LC 542)

Problem

Given a matrix mat containing only 0 and 1. Return a matrix of the same size where each cell = the distance (4-direction steps) to the nearest 0.

Example

Input:  mat = [[0,0,0],
               [0,1,0],
               [1,1,1]]
Output:       [[0,0,0],
               [0,1,0],
               [1,2,1]]
        (each cell = Manhattan distance to its nearest 0)

Constraints

Clarifying questions

Approach

Same as Walls and Gates: multi-source BFS from every 0. The initial distance of each 0 is 0; each 1 is unknown.

Alternative — 2-pass DP, O(m · n): - Pass 1 (top-left → bottom-right): dp[r][c] = min(dp[r-1][c], dp[r][c-1]) + 1. - Pass 2 (bottom-right → top-left): dp[r][c] = min(dp[r][c], dp[r+1][c]+1, dp[r][c+1]+1).

Both are O(m · n). BFS is more intuitive; DP is space-efficient.

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
        rows, cols = len(mat), len(mat[0])
        INF = float('inf')
        dist = [[INF] * cols for _ in range(rows)]

        queue: deque[tuple[int, int]] = deque()
        for r in range(rows):
            for c in range(cols):
                if mat[r][c] == 0:
                    dist[r][c] = 0
                    queue.append((r, c))

        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        while queue:
            r, c = queue.popleft()
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and dist[nr][nc] > dist[r][c] + 1:
                    dist[nr][nc] = dist[r][c] + 1
                    queue.append((nr, nc))
        return dist


class SolutionDP:
    """Two-pass DP — space-efficient (can be in-place where allowed)."""

    def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
        rows, cols = len(mat), len(mat[0])
        INF = rows + cols + 1
        dist = [[0 if mat[r][c] == 0 else INF
                 for c in range(cols)] for r in range(rows)]
        # Pass 1: top-left → bottom-right.
        for r in range(rows):
            for c in range(cols):
                if dist[r][c] == 0:
                    continue
                top = dist[r-1][c] if r > 0 else INF
                left = dist[r][c-1] if c > 0 else INF
                dist[r][c] = min(top, left) + 1
        # Pass 2: bottom-right → top-left.
        for r in range(rows - 1, -1, -1):
            for c in range(cols - 1, -1, -1):
                if dist[r][c] == 0:
                    continue
                bot = dist[r+1][c] + 1 if r < rows - 1 else INF
                right = dist[r][c+1] + 1 if c < cols - 1 else INF
                dist[r][c] = min(dist[r][c], bot, right)
        return dist

Complexity

Comments

Chapter summary & decisions

Matrix traversal checklist

  1. Direction array: dirs = [(-1,0),(1,0),(0,-1),(0,1)] (4-conn) or 8-conn.
  2. Bounds: 0 <= nr < R and 0 <= nc < C.
  3. Visited: in-place mark (change '1' → '0' or #) or set?
  4. Mutation OK? Ask the interviewer; otherwise use a 2D visited.
  5. Stack overflow for recursive DFS: a 1000×1000 grid can exceed the limit. Use an iterative stack or BFS.

Boundary-first technique

For Surrounded Regions (LC 130) and Pacific Atlantic (LC 417): - “A cell that does not satisfy the property” = “a cell that is connected to the boundary”. - Seed BFS/DFS from the boundary, mark reachable cells; remaining cells are the surrounded ones.

Multi-source BFS

For 01 Matrix (LC 542) and Walls and Gates (LC 286): - Enqueue every source up front (cell 0 for 542, gate 0 for 286). - BFS by level → distances spread outward. Each cell is visited onceO(R·C).

In-place mark vs visited set

Criterion In-place Set/2D bool
Extra memory O(1) O(R·C)
Mutates input? Yes No
Concurrency / restore Hard Easy
Pick when Mutation allowed & O(1) extra needed Grid is immutable or needs re-run

Chapter 13 — Topological Sort

Topological Sort orders the vertices of a DAG (Directed Acyclic Graph) so that for every edge u → v, u appears before v in the order. This is the mandatory pattern for any “complete-in-dependency-order” problem: build systems, task schedulers, course prerequisites, …

Chapter objectives

After this chapter, you will be able to:

Edge-direction convention

One of the most common topo-sort bugs is drawing the edges in the wrong direction. For this reason, the entire book follows one convention:

Real description           Edge in graph         Indegree
─────────────────────────────────────────────────────────
"a must come before b"     a → b                 indeg[b] += 1
"b depends on a"           a → b                 indeg[b] += 1
"a is prerequisite of b"   a → b                 indeg[b] += 1
─────────────────────────────────────────────────────────
LC 207/210 input:          prerequisites[i] = [course, prereq]
                           i.e. [b, a] meaning "to do b, must do a"
                           → edge a → b (prereq → course)
─────────────────────────────────────────────────────────
LC 269 Alien Dict:         words[i] < words[i+1] in lex order
                           → first differing character: c1 < c2
                           → edge c1 → c2

Kahn’s invariant: popping a node with indeg == 0 ↔︎ “nobody must finish before it”.

Every topo-sort problem in this book uses the convention u → v means u finishes before v. When the problem uses different wording, always draw 2–3 edges on paper first to make sure your code’s edge direction matches the problem statement.

When to use this pattern?

Two classic algorithms:

  1. Kahn’s algorithm (BFS) — count indegree, enqueue nodes with indeg=0.
  2. DFS with post-order — DFS recursively, push the node when its subtree is done; reverse the stack.

Both are O(V + E). We default to Kahn because it’s easy to extend to “min levels”.

Template code

from collections import defaultdict, deque
from typing import List

def topo_sort_kahn(n: int, edges: List[tuple]) -> List[int]:
    graph = defaultdict(list)
    indeg = [0] * n
    for u, v in edges:
        graph[u].append(v)
        indeg[v] += 1

    queue = deque(i for i in range(n) if indeg[i] == 0)
    order: list[int] = []
    while queue:
        u = queue.popleft()
        order.append(u)
        for v in graph[u]:
            indeg[v] -= 1
            if indeg[v] == 0:
                queue.append(v)

    return order if len(order) == n else []     # empty = cycle exists

End-of-chapter practice


13.1 Course Schedule II (LC 210)

Problem

Given numCourses courses and prerequisites[i] = [a, b] (taking a requires finishing b first), return a valid order of courses, or [] if there is a cycle.

Example

Input:  numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
Output: [0, 1, 2, 3]   (or [0, 2, 1, 3])
Explanation:
  Edges: 0 → 1, 0 → 2, 1 → 3, 2 → 3.

Constraints

Clarifying questions

Approach

Kahn’s algorithm — straight from the template. As we pop, append to order. At the end: if len(order) == numCourses → return order; otherwise a cycle exists.

Illustration for [[1,0],[2,0],[3,1],[3,2]]:

Graph:        0
             / \
            1   2
             \ /
              3

Initial indeg: [0, 1, 1, 2]
queue = [0]                   (indeg 0)

Pop 0 → order=[0]
  decrement indeg[1], indeg[2] → [_, 0, 0, 2]
  queue = [1, 2]

Pop 1 → order=[0, 1]
  decrement indeg[3] → [_, _, _, 1]

Pop 2 → order=[0, 1, 2]
  decrement indeg[3] → [_, _, _, 0]
  queue = [3]

Pop 3 → order=[0, 1, 2, 3]

len(order)=4=numCourses → return [0, 1, 2, 3]  ✓

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        graph = defaultdict(list)
        indeg = [0] * numCourses
        for a, b in prerequisites:
            graph[b].append(a)
            indeg[a] += 1

        queue = deque(i for i, d in enumerate(indeg) if d == 0)
        order: list[int] = []
        while queue:
            node = queue.popleft()
            order.append(node)
            for nb in graph[node]:
                indeg[nb] -= 1
                if indeg[nb] == 0:
                    queue.append(nb)

        return order if len(order) == numCourses else []

Complexity

Comments


13.2 Alien Dictionary (LC 269)

Problem

An alien language uses Latin letters in a different order. Given a list of words sorted in that order, find a valid letter ordering (a string of letters). Return "" if contradictory.

Example

Input:  words = ["wrt","wrf","er","ett","rftt"]
        (the list of words sorted by the alien alphabet)
Output: "wertf"  (one valid alphabet order; many answers may exist)
Explanation:
  wrt < wrf  → t < f
  wrf < er   → w < e
  er  < ett  → r < t
  ett < rftt → e < r
  → topo: w < e < r < t < f

Input:  words = ["z","x","z"]
Output: ""             (cyclic contradiction: z<x from pair 1 but x<z from pair 2)

Constraints

Clarifying questions

Approach

Two steps:

  1. Extract order relations from adjacent pairs (words[i], words[i+1]):
  2. Topological sort on those relations.

Illustration for ["wrt","wrf","er","ett","rftt"]:

Pair 1: wrt vs wrf
  first diff at index 2 → t < f
  → edge t → f

Pair 2: wrf vs er
  first diff at index 0 → w < e
  → edge w → e

Pair 3: er vs ett
  first diff at index 1 → r < t
  → edge r → t

Pair 4: ett vs rftt
  first diff at index 0 → e < r
  → edge e → r

Graph: w → e → r → t → f
Topo: w, e, r, t, f → "wertf"

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def alienOrder(self, words: List[str]) -> str:
        # Initialise indeg for every character that appears.
        indeg = {ch: 0 for w in words for ch in w}
        graph = defaultdict(set)

        # Extract relations from adjacent pairs.
        for i in range(len(words) - 1):
            w1, w2 = words[i], words[i + 1]
            # Invalid prefix edge case.
            if len(w1) > len(w2) and w1.startswith(w2):
                return ""
            for c1, c2 in zip(w1, w2):
                if c1 != c2:
                    if c2 not in graph[c1]:
                        graph[c1].add(c2)
                        indeg[c2] += 1
                    break

        # Kahn's.
        queue = deque(ch for ch, d in indeg.items() if d == 0)
        order: list[str] = []
        while queue:
            ch = queue.popleft()
            order.append(ch)
            for nb in graph[ch]:
                indeg[nb] -= 1
                if indeg[nb] == 0:
                    queue.append(nb)

        return ''.join(order) if len(order) == len(indeg) else ""

Complexity

Comments


13.3 Minimum Height Trees (LC 310)

Problem

Input: n (number of nodes) and edges: List[List[int]] — a list of n-1 undirected edges [u, v] describing a tree. Nodes labelled 0..n-1.

Find every root that yields the minimum tree height. Return the list of such roots (1 or 2 possible).

Example

Input:  n=6, edges = [[0,3],[1,3],[2,3],[4,3],[5,4]]
Tree:    0   1   2
          \  |  /
           \ | /
             3
             |
             4
             |
             5
Output: [3, 4]

Constraints

Clarifying questions

Approach

Insight: the centroid of a tree (1 or 2 nodes) minimises tree height. To find it: BFS from the leaves, “peeling” inwards.

Procedure: 1. Build the graph + count degree. 2. Enqueue every leaf (degree == 1). 3. Loop: pop one layer of leaves, decrement neighbour degrees, push new leaves (degree == 1). 4. When ≤ 2 nodes remain → those are the centroid(s).

Illustration:

Initial leaves: [0, 1, 2, 5]
Peel → remaining: [3, 4]
                3 (degree=1 after peel), 4 (degree=1 after peel)
→ ≤ 2 nodes → centroids = [3, 4]

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
        if n == 1:
            return [0]
        graph = defaultdict(set)
        for u, v in edges:
            graph[u].add(v)
            graph[v].add(u)

        leaves = deque(i for i in range(n) if len(graph[i]) == 1)
        remaining = n
        while remaining > 2:
            size = len(leaves)
            remaining -= size
            for _ in range(size):
                leaf = leaves.popleft()
                nb = next(iter(graph[leaf]))    # a leaf has exactly one neighbour
                graph[nb].remove(leaf)
                if len(graph[nb]) == 1:
                    leaves.append(nb)
        return list(leaves)

Complexity

Comments


13.4 Sort Items by Groups Respecting Dependencies (LC 1203)

Problem

Given n items, each belonging to a group (group[i] = -1 means not yet assigned — should get its own group). beforeItems[i] lists items that must come before item i. Arrange items so that: - beforeItems are respected. - Items of the same group are contiguous.

Return [] if impossible.

Example

Input:  n=8, m=2, group=[-1,-1,1,0,0,1,0,-1], beforeItems=[[],[6],[5],[6],[3,6],[],[],[]]
Output: [6,3,4,1,5,2,0,7]

Constraints

Clarifying questions

Approach

Two topological sorts: 1. Sort the groups among themselves (an item-level edge i → j across groups becomes a group-level edge). 2. Within each group, sort its items. 3. Concatenate the result: use the group order; inside each group write its items by item order.

Pre-processing: any item with group[i] == -1 → assign its own private group so “no group” doesn’t influence the topology.

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def sortItems(
        self, n: int, m: int,
        group: List[int], beforeItems: List[List[int]]
    ) -> List[int]:
        # Assign a private group to items with -1.
        for i in range(n):
            if group[i] == -1:
                group[i] = m
                m += 1

        item_graph = defaultdict(list)
        item_indeg = [0] * n
        group_graph = defaultdict(set)
        group_indeg = defaultdict(int)

        for cur, befores in enumerate(beforeItems):
            for prev in befores:
                item_graph[prev].append(cur)
                item_indeg[cur] += 1
                if group[prev] != group[cur]:
                    if group[cur] not in group_graph[group[prev]]:
                        group_graph[group[prev]].add(group[cur])
                        group_indeg[group[cur]] += 1

        def topo(nodes, graph, indeg) -> List[int]:
            queue = deque(x for x in nodes if indeg[x] == 0)
            out: list = []
            while queue:
                x = queue.popleft()
                out.append(x)
                for nb in graph[x]:
                    indeg[nb] -= 1
                    if indeg[nb] == 0:
                        queue.append(nb)
            return out if len(out) == len(nodes) else []

        item_order = topo(range(n), item_graph, item_indeg)
        if not item_order:
            return []
        group_order = topo(range(m), group_graph, group_indeg)
        if not group_order:
            return []

        # Bucket by group respecting group_order; items keep item_order.
        bucket: dict[int, list[int]] = defaultdict(list)
        for item in item_order:
            bucket[group[item]].append(item)
        result: list[int] = []
        for g in group_order:
            result.extend(bucket[g])
        return result

Complexity

Comments


13.5 Sequence Reconstruction (LC 444)

Problem

Given nums (a permutation of 1..n) and sequences (a list of sub-sequences), check whether nums is the unique topological order implied by sequences.

Example

Input:  nums = [1, 2, 3], sequences = [[1,2],[1,3]]
Output: False
Explanation: from [1,2] and [1,3] → either [1,2,3] or [1,3,2] → not unique.

Input:  nums = [1, 2, 3], sequences = [[1,2],[1,3],[2,3]]
Output: True

Constraints

Clarifying questions

Approach

Run Kahn’s. For uniqueness, every level must contain exactly one node with indeg == 0 — ≥ 2 ⇒ multiple topo orders ⇒ False. Additionally the pop order must match nums.

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def sequenceReconstruction(self, nums: List[int], sequences: List[List[int]]) -> bool:
        n = len(nums)
        graph = defaultdict(set)
        indeg = [0] * (n + 1)
        for seq in sequences:
            for i in range(len(seq) - 1):
                u, v = seq[i], seq[i + 1]
                if v not in graph[u]:
                    graph[u].add(v)
                    indeg[v] += 1

        queue = deque(i for i in range(1, n + 1) if indeg[i] == 0)
        idx = 0
        while queue:
            if len(queue) > 1:
                return False              # >1 choice → not unique
            x = queue.popleft()
            if nums[idx] != x:
                return False              # diverges from nums
            idx += 1
            for nb in graph[x]:
                indeg[nb] -= 1
                if indeg[nb] == 0:
                    queue.append(nb)
        return idx == n

Complexity

Comments


13.6 Parallel Courses (LC 1136)

Problem

Given n courses and relations [a, b] (take a before b). Each semester you may take any number of courses provided all their prerequisites are done. Return the minimum number of semesters to take all courses, or -1 if a cycle exists.

Example

Input:  n=3, relations=[[1,3],[2,3]]
Output: 2
Explanation: Semester 1 take [1,2], semester 2 take [3]

Constraints

Clarifying questions

Approach

Kahn’s BFS but count by level (semester). Each outer iteration of BFS processes the entire current queue = the courses takeable in the same semester.

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def minimumSemesters(self, n: int, relations: List[List[int]]) -> int:
        graph = defaultdict(list)
        indeg = [0] * (n + 1)
        for u, v in relations:
            graph[u].append(v)
            indeg[v] += 1

        queue = deque(i for i in range(1, n + 1) if indeg[i] == 0)
        taken = 0
        semesters = 0
        while queue:
            semesters += 1
            for _ in range(len(queue)):
                u = queue.popleft()
                taken += 1
                for v in graph[u]:
                    indeg[v] -= 1
                    if indeg[v] == 0:
                        queue.append(v)
        return semesters if taken == n else -1

Complexity

Comments

Chapter summary & decisions

Topo + DP framing (bridge to Chapter 43)

Sequence Reconstruction (LC 444) — why the queue must always have ≤ 1 element?

Alien Dictionary (LC 269) — invalid prefix case

If w_i is a prefix of w_{i-1} (e.g. ["abc", "ab"]), the dictionary is invalid → return "". Check this BEFORE building edges (don’t forget to break correctly).

Sort Items by Groups (LC 1203) — 2-layer DAG

items 5,6 ∈ groupA   items 7,8 ∈ groupB   items 9 ∈ -1 (private)

Item DAG:  5 → 6,  7 → 8,  6 → 7   (intra + cross-group)
Group DAG: A → B   (because 6 → 7 with 6 ∈ A, 7 ∈ B)

→ Topo on group order → within each group topo on item order.

Chapter 14 — Interval

Intervals ([start, end]) cover many important calendar/scheduling/booking problems. Chapter 4 (Sorting) already touched Merge Intervals and Meeting Rooms II; this chapter dives deep into 8 operation patterns on intervals (merge, insert, intersection, overlap, free time) — unavoidable in interviews for calendar (Google Calendar) and booking (Airbnb, Booking) companies.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Four canonical relations between intervals A = [a₁, a₂], B = [b₁, b₂]:

1. Disjoint:        A.end < B.start  →  A entirely before B
2. Touching:        A.end == B.start →  adjacent; may merge depending on problem
3. Partial overlap: A.start < B.start ≤ A.end < B.end
4. Containment:     A.start ≤ B.start ≤ B.end ≤ A.end

Template code

from typing import List

# 1) Merge two overlapping intervals
def merge_two(a, b):
    return [min(a[0], b[0]), max(a[1], b[1])]


# 2) Overlap check (touching counts as overlap)
def overlaps(a, b) -> bool:
    return a[0] <= b[1] and b[0] <= a[1]


# 3) Sweep line: same pattern for every "max overlap" question
events: List[tuple[int, int]] = []
for s, e in intervals:
    events.append((s, +1))       # open
    events.append((e, -1))       # close
events.sort()
cur = peak = 0
for _, delta in events:
    cur += delta
    peak = max(peak, cur)

End-of-chapter practice


14.1 Merge Intervals (LC 56) — recap

Fully solved in Chapter 4.2 under the Sorting lens. Here we summarise it under the “interval” lens and extend the follow-ups.

Problem

Merge overlapping intervals. [1,3] and [2,6][1,6].

Approach

Sort by start. Walk once and keep last = the most recent interval in the output. If cur.start <= last.endlast.end = max(last.end, cur.end); otherwise push cur.

Code (Python 3)

class Solution:
    def merge(self, intervals):
        intervals.sort(key=lambda x: x[0])
        result = []
        for cur in intervals:
            if result and cur[0] <= result[-1][1]:
                result[-1][1] = max(result[-1][1], cur[1])
            else:
                result.append(cur[:])
        return result

Complexity

Extra comments for the interval lens


14.2 Insert Interval (LC 57)

Problem

Given intervals sorted by start and pairwise non-overlapping, insert newInterval and merge as needed.

Example

Input:  intervals = [[1,3],[6,9]], newInterval = [2,5]
Output: [[1,5],[6,9]]

Input:  intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
Output: [[1,2],[3,10],[12,16]]

Constraints

Clarifying questions

Approach

Approach 1 — O(n) single sweep in 3 phases.

  1. Before newInterval: push every interval with end < newInterval.start.
  2. Overlapping: for every interval with start <= newInterval.end, extend newInterval (start = min, end = max). At the end push newInterval.
  3. After newInterval: push the remaining intervals.

Approach 2 — Concat + merge (reuse problem 14.1). Simple but O(n log n) from the unnecessary sort.

Illustration for intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], new = [4,8]:

Number line:
1   3   5   6  7  8  10        12      16
├─┤ ├───┤ ├─┤  ├──┤            ├──────┤
        ├──────────┤  new = [4, 8]

Phase 1 (end < 4):  [1, 2]
                     result = [[1,2]]

Phase 2 (start <= 8):
  [3, 5]: extend newInterval = [min(4,3), max(8,5)] = [3, 8]
  [6, 7]: extend = [3, 8]
  [8, 10]: extend = [3, 10]
  Push [3, 10]
                     result = [[1,2], [3,10]]

Phase 3: remaining [12, 16]
                     result = [[1,2], [3,10], [12,16]]  ✓

Code (Python 3)

from typing import List

class Solution:
    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
        result: list[list[int]] = []
        i, n = 0, len(intervals)
        # 1) Before newInterval.
        while i < n and intervals[i][1] < newInterval[0]:
            result.append(intervals[i])
            i += 1
        # 2) Overlap — extend newInterval.
        while i < n and intervals[i][0] <= newInterval[1]:
            newInterval[0] = min(newInterval[0], intervals[i][0])
            newInterval[1] = max(newInterval[1], intervals[i][1])
            i += 1
        result.append(newInterval)
        # 3) After newInterval.
        while i < n:
            result.append(intervals[i])
            i += 1
        return result

Complexity

Comments


14.3 Non-overlapping Intervals (LC 435)

Problem

Given an array of intervals, return the minimum number of intervals to remove so the rest are pairwise non-overlapping.

Example

Input:  intervals = [[1,2],[2,3],[3,4],[1,3]]
Output: 1
Explanation: remove [1,3] → remainder [1,2],[2,3],[3,4] is non-overlapping.

Input:  intervals = [[1,2],[1,2],[1,2]]
Output: 2

Input:  intervals = [[1,2],[2,3]]
Output: 0              (already non-overlapping, nothing to remove)

(each [start, end] represents the half-open interval [start, end))

Constraints

Clarifying questions

Approach

Greedy — sort by end ascending. Keeping the interval with the smallest end leaves the most “room” for subsequent ones.

Pseudocode: - Sort by end. - Keep last_end = -∞. For each interval [s, e]: - If s >= last_end → keep (no overlap), last_end = e. - Otherwise → count removal.

Why sort by end, not start? Greedy works because: “always pick the interval with the earliest end” maximises the remaining “free time” — provable by induction.

Illustration for [[1,2],[2,3],[3,4],[1,3]]:

Sort by end:  [[1,2], [2,3], [1,3], [3,4]]
                       end=2   end=3   end=3   end=4

Walk:
  [1,2]: start=1 >= -inf → keep; last_end=2          kept: 1
  [2,3]: start=2 >= 2    → keep; last_end=3          kept: 2
  [1,3]: start=1 < 3     → remove                     removed: 1
  [3,4]: start=3 >= 3    → keep; last_end=4          kept: 3

Kept 3, removed 1 → answer = 1.

Code (Python 3)

from typing import List

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return 0
        intervals.sort(key=lambda x: x[1])
        kept = 1
        last_end = intervals[0][1]
        for s, e in intervals[1:]:
            if s >= last_end:
                kept += 1
                last_end = e
        return len(intervals) - kept

Complexity

Comments


14.4 Meeting Rooms II (LC 253) — recap

Fully solved in Chapter 4.4. Here we summarise and link.

Problem

Find the minimum number of rooms needed for all meetings.

Approach

Three approaches (heap, sweep-line events, chronological two-pointer) — all O(n log n). Sweep line is the canonical interval language.

Code (Python 3)

from typing import List

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        events = [(s, +1) for s, _ in intervals] + [(e, -1) for _, e in intervals]
        events.sort(key=lambda x: (x[0], x[1]))
        cur = peak = 0
        for _, d in events:
            cur += d
            peak = max(peak, cur)
        return peak

Complexity

Additional remarks


14.5 Minimum Number of Arrows to Burst Balloons (LC 452)

Problem

Given an array of balloons [x_start, x_end] (each balloon spans an interval on the x-axis). A vertical arrow shot at x = X bursts every balloon with x_start <= X <= x_end. Find the minimum number of arrows needed to burst all balloons.

Example

Input:  points = [[10,16],[2,8],[1,6],[7,12]]
        (each entry [xstart, xend] is one balloon spanning the closed [xstart, xend])
Output: 2   (at least 2 arrows: shoot x=6 bursts [1,6] and [2,8]; shoot x=11 bursts [7,12] and [10,16])
Explanation:
  1 arrow at x = 6 bursts [1,6] and [2,8].
  1 arrow at x = 11 bursts [7,12] and [10,16].

Constraints

Clarifying questions

Approach

Equivalent to problem 14.3 (Non-overlapping Intervals): each arrow corresponds to a group of balloons that share a common intersection. Count groups = number of arrows.

Greedy sort by end just like 14.3: - Sort balloons by end. - Keep last_end = -∞. For each balloon [s, e]: - If s > last_end → need a new arrow; last_end = e. - Otherwise → this balloon is burst by the current arrow.

Code (Python 3)

from typing import List

class Solution:
    def findMinArrowShots(self, points: List[List[int]]) -> int:
        if not points:
            return 0
        points.sort(key=lambda x: x[1])
        arrows = 1
        last_end = points[0][1]
        for s, e in points[1:]:
            if s > last_end:        # disjoint → new arrow needed
                arrows += 1
                last_end = e
        return arrows

Complexity

Comments


14.6 Employee Free Time (LC 759)

Problem

Given schedule[i] = a list of busy intervals for employee i. Return every interval that is free for all employees, sorted ascending. (Skip the time before the first busy or after the last finishes.)

Example

Input:  schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]]
Output: [[3, 4]]
Explanation:
  Union of busy: [1,3] (from [1,2] + [1,3]), [4,10] (from [5,6] + [4,10]).
  Free in between: [3, 4].

Constraints

Clarifying questions

Approach

Step 1: combine every busy interval into one big list independent of employees. Step 2: sort by start, merge (like problem 14.1). Step 3: gaps between merged intervals = free time.

Code (Python 3)

from typing import List

class Interval:
    def __init__(self, start: int = 0, end: int = 0):
        self.start, self.end = start, end


class Solution:
    def employeeFreeTime(self, schedule: "List[List[Interval]]") -> "List[Interval]":
        all_busy: list[tuple[int, int]] = []
        for emp_sched in schedule:
            for iv in emp_sched:
                all_busy.append((iv.start, iv.end))
        all_busy.sort()

        merged: list[list[int]] = []
        for s, e in all_busy:
            if merged and s <= merged[-1][1]:
                merged[-1][1] = max(merged[-1][1], e)
            else:
                merged.append([s, e])

        free = []
        for i in range(1, len(merged)):
            if merged[i - 1][1] < merged[i][0]:
                free.append(Interval(merged[i - 1][1], merged[i][0]))
        return free

Complexity

Comments

Chapter summary & decisions

Interval-convention checklist

  1. Closed [s, e] or half-open [s, e)?
  2. Sort by start (Merge, Insert) or sort by end (Greedy, Min Arrows)?
  3. Sweep-line tie-break: for events at the same time t:

Employee Free Time — visual

e1: |==1==|       |==3==|
e2:    |==2==|       |==4==|
sort all → merge ⇒ busy: [1∪2] [3∪4]
free = complement between busy blocks

Recap lens (why Merge & Meeting reappear)


Chapter 15 — Heap

Heap (Priority Queue) quickly answers “what is the largest/smallest element right now?”. The two core operations push and pop are both O(log n). Heap concisely solves: top-k, streaming median, merge k lists, scheduling. Most common pattern: a size-k heap to maintain the k best elements.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Python heapq is only a min-heap. For max-heap → push -x.

Template code

import heapq

# 1) Basic min-heap
heap: list[int] = []
heapq.heappush(heap, x)
top = heap[0]              # peek (no pop)
val = heapq.heappop(heap)

# 2) Max-heap via negation
heapq.heappush(heap, -x)
val = -heapq.heappop(heap)

# 3) K smallest / largest from a list
smallest_k = heapq.nsmallest(k, arr)        # O(n log k)
largest_k = heapq.nlargest(k, arr)

# 4) Heapify O(n) — faster than n pushes
heapq.heapify(arr)

# 5) Heap with arbitrary key — push tuple (priority, payload)
heapq.heappush(heap, (priority, payload))

End-of-chapter practice


15.1 Kth Largest Element in an Array (LC 215)

Problem

Given an array nums and integer k, return the k-th largest element (1-indexed, descending order). Duplicates allowed.

Example

Input:  nums = [3, 2, 1, 5, 6, 4], k = 2     → 5
Input:  nums = [3, 2, 3, 1, 2, 4, 5, 5, 6], k = 4 → 4

Constraints

Clarifying questions

Approach

Approach 1 — Sort, O(n log n). sorted(nums)[-k]. Simplest.

Approach 2 — Size-k min-heap, O(n log k).

Walk through the array, push to the heap. When the heap exceeds k → pop. At the end heap[0] is the Kth largest.

Approach 3 — Quickselect, O(n) average.

A quicksort variant: partition around a pivot, recurse only into the half that contains the Kth.

Code (Python 3)

import heapq
from typing import List

class Solution:
    """Approach 2 — size-k heap."""

    def findKthLargest(self, nums: List[int], k: int) -> int:
        heap: list[int] = []
        for x in nums:
            heapq.heappush(heap, x)
            if len(heap) > k:
                heapq.heappop(heap)
        return heap[0]


class SolutionQS:
    """Approach 3 — quickselect, O(n) average."""

    def findKthLargest(self, nums: List[int], k: int) -> int:
        # Kth largest = (n-k)-th smallest (0-indexed).
        target = len(nums) - k

        def partition(lo: int, hi: int) -> int:
            pivot = nums[hi]
            store = lo
            for i in range(lo, hi):
                if nums[i] < pivot:
                    nums[store], nums[i] = nums[i], nums[store]
                    store += 1
            nums[store], nums[hi] = nums[hi], nums[store]
            return store

        lo, hi = 0, len(nums) - 1
        while True:
            p = partition(lo, hi)
            if p == target:
                return nums[p]
            elif p < target:
                lo = p + 1
            else:
                hi = p - 1

Complexity

Approach Time Space
Sort O(n log n) O(1)
Size-k heap O(n log k) O(k)
Quickselect O(n) avg, O(n²) worst O(1)

Comments


15.2 Top K Frequent Words (LC 692)

Problem

Given words and integer k, return the k most frequent words. On tie, the lexicographically smaller word wins.

Example

Input:  words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2
Output: ["i", "love"]
Explanation: "i" and "love" tie at frequency 2. "i" < "love" lexicographically.

Input:  k = 4
Output: ["the", "is", "sunny", "day"]   (tie → lex order)

Constraints

Clarifying questions

Approach

Crux: the comparator tie-break — higher frequency wins; on tie, lexicographically smaller wins.

Approach 1 — Sort, O(n log n). sorted(cnt.keys(), key=lambda w: (-cnt[w], w))[:k].

Approach 2 — Size-k min-heap, O(n log k).

The heap holds (freq, word) tuples. Because it’s a min-heap, we want: - Lower frequency at the top (pop first) → push freq directly. - On tie → lexicographically larger word at the top (pop first).

A trick: per tuple use (-freq, word), then sort… no, a custom wrapper is cleaner. Approach 1 is much simpler.

Code (Python 3)

import heapq
from collections import Counter
from typing import List

class Solution:
    """Approach 1 — sort. Cleanest for this problem."""

    def topKFrequent(self, words: List[str], k: int) -> List[str]:
        cnt = Counter(words)
        return sorted(cnt.keys(), key=lambda w: (-cnt[w], w))[:k]


class SolutionHeap:
    """Approach 2 — size-k min-heap."""

    def topKFrequent(self, words: List[str], k: int) -> List[str]:
        cnt = Counter(words)
        # In a min-heap, we want the "loser" at the top to pop first.
        # "Loser" = lower freq OR (same freq + larger word).
        # Using (freq, -word_chars) is awkward because word is a string.
        # Solution: use a wrapper class.

        class Wrap:
            __slots__ = ("freq", "word")
            def __init__(self, f, w): self.freq, self.word = f, w
            def __lt__(self, other):
                if self.freq != other.freq:
                    return self.freq < other.freq      # min-heap → smaller freq on top
                return self.word > other.word          # tie → larger word on top
            def __eq__(self, other):
                return (self.freq, self.word) == (other.freq, other.word)

        heap: list[Wrap] = []
        for w, f in cnt.items():
            heapq.heappush(heap, Wrap(f, w))
            if len(heap) > k:
                heapq.heappop(heap)
        # Pop everything → reverse for correct order.
        return [heapq.heappop(heap).word for _ in range(k)][::-1]

Complexity

Comments


15.3 Find Median from Data Stream (LC 295)

Problem

Design a class: - addNum(num): add num to the data stream. - findMedian(): return the current median.

Median of n numbers: the middle element (sorted), or the average of the two middle elements if n is even.

Example

Input (LC-style operation arrays):
  ops  = ["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
  args = [[],             [1],      [2],      [],           [3],      []]

Output: [null, null, null, 1.5, null, 2.0]

Explanation:
  MedianFinder()    → init, null
  addNum(1)         → null;  stream = [1]
  addNum(2)         → null;  stream = [1, 2]
  findMedian()      → 1.5    (mean of the 2 middle)
  addNum(3)         → null;  stream = [1, 2, 3]
  findMedian()      → 2.0    (middle of 3 sorted numbers)

Constraints

Clarifying questions

Approach

The two-heap trick: - low = max-heap holding the lower half (below the median). - high = min-heap holding the upper half (above the median).

Invariants: - Every element of low ≤ every element of high. - len(low) == len(high) or len(low) == len(high) + 1.

addNum: 1. Push into low (max-heap → push -num). 2. Move the top of low into high (rebalance). 3. If len(high) > len(low) → move high’s top back to low.

findMedian: - len(low) > len(high)low[0] (negated). - Equal sizes → average of the two tops.

A cleaner formulation:

addNum(num):
  if not low or num <= -low[0]:
      heappush(low, -num)
  else:
      heappush(high, num)
  # rebalance: |len(low) - len(high)| <= 1, low >= high in size
  if len(low) > len(high) + 1:
      heappush(high, -heappop(low))
  elif len(high) > len(low):
      heappush(low, -heappop(high))

Code (Python 3)

import heapq

class MedianFinder:
    def __init__(self):
        self.low: list[int] = []   # max-heap (negated)
        self.high: list[int] = []  # min-heap

    def addNum(self, num: int) -> None:
        if not self.low or num <= -self.low[0]:
            heapq.heappush(self.low, -num)
        else:
            heapq.heappush(self.high, num)
        # Rebalance.
        if len(self.low) > len(self.high) + 1:
            heapq.heappush(self.high, -heapq.heappop(self.low))
        elif len(self.high) > len(self.low):
            heapq.heappush(self.low, -heapq.heappop(self.high))

    def findMedian(self) -> float:
        if len(self.low) > len(self.high):
            return float(-self.low[0])
        return (-self.low[0] + self.high[0]) / 2

Complexity

Comments


15.4 K Closest Points to Origin (LC 973)

Problem

Given an array of points (each [x, y]), return the k points closest to the origin (Euclidean distance).

Example

Input:  points = [[1,3], [-2,2]], k = 1
Output: [[-2, 2]]
Explanation: dist²(1,3) = 10, dist²(-2,2) = 8.

Constraints

Clarifying questions

Approach

Size-k heap — O(n log k). A max-heap holds the k closest points; when it exceeds k, pop the farthest.

Note: use dist² instead of sqrt(dist) — avoids floats, preserves the ordering.

Code (Python 3)

import heapq
from typing import List

class Solution:
    def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
        heap: list[tuple[int, list[int]]] = []   # (-dist², point) — max-heap
        for p in points:
            d2 = p[0] ** 2 + p[1] ** 2
            heapq.heappush(heap, (-d2, p))
            if len(heap) > k:
                heapq.heappop(heap)
        return [p for _, p in heap]

Complexity

Comments


15.5 Merge k Sorted Lists (LC 23)

Problem

Input: lists: List[Optional[ListNode]] — an array of k heads of k ascending-sorted singly linked lists. Merge them all into one sorted linked list and return the new head.

Example

Input:  lists = [1→4→5, 1→3→4, 2→6]
Output: 1→1→2→3→4→4→5→6

Constraints

Clarifying questions

Approach

Approach 1 — Min-heap k-way merge, O(N log k).

The heap holds (val, idx, node) for the head of each list. Pop the smallest, push the next element from the same list. Repeat until empty.

idx is the tie-breaker — Python can’t compare ListNodes when values tie.

Approach 2 — Pairwise merge (Divide & Conquer), O(N log k).

Tournament-style merge sort: pair lists and merge, repeat. Same complexity.

Code (Python 3)

import heapq
from typing import List, Optional

class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next


class Solution:
    """Approach 1 — min-heap k-way merge."""

    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        heap: list[tuple[int, int, ListNode]] = []
        for i, head in enumerate(lists):
            if head:
                heapq.heappush(heap, (head.val, i, head))

        dummy = ListNode()
        tail = dummy
        while heap:
            val, i, node = heapq.heappop(heap)
            tail.next = node
            tail = node
            if node.next:
                heapq.heappush(heap, (node.next.val, i, node.next))
        return dummy.next

Complexity

Comments


15.6 Task Scheduler (LC 621)

Problem

Given tasks (uppercase letters A-Z) and integer n. Each unit of time runs one task or stays idle. Two identical tasks must be at least n units apart. Return the minimum number of time units required to finish everything.

Example

Input:  tasks = ["A","A","A","B","B","B"], n = 2
Output: 8
Schedule:   A → B → idle → A → B → idle → A → B

Constraints

Clarifying questions

Approach

Approach 1 — Greedy + max-heap, O(N log 26).

Each tick: - Take the highest-frequency task (heap top). Decrement and put into a “cooldown queue” (end_time, freq). - When cooldown_queue.front().end_time <= now → pop and push back into the heap.

Approach 2 — Direct formula, O(N).

Let f_max = max frequency, count_max = number of tasks with that frequency. Answer = max(n_total, (f_max - 1) * (n + 1) + count_max).

Intuition: the most-frequent task creates f_max - 1 slots of length n+1, plus count_max more at the “last row”.

Code (Python 3)

from collections import Counter
from typing import List

class Solution:
    """Approach 2 — O(N) formula."""

    def leastInterval(self, tasks: List[str], n: int) -> int:
        cnt = Counter(tasks)
        f_max = max(cnt.values())
        count_max = sum(1 for v in cnt.values() if v == f_max)
        slots = (f_max - 1) * (n + 1) + count_max
        return max(slots, len(tasks))


import heapq
from collections import deque

class SolutionHeap:
    """Approach 1 — heap + cooldown queue."""

    def leastInterval(self, tasks: List[str], n: int) -> int:
        heap = [-c for c in Counter(tasks).values()]   # max-heap (negated)
        heapq.heapify(heap)
        cooldown: deque[tuple[int, int]] = deque()     # (available_time, neg_count)
        time = 0
        while heap or cooldown:
            time += 1
            if heap:
                neg = heapq.heappop(heap) + 1          # decrement freq
                if neg < 0:
                    cooldown.append((time + n, neg))
            if cooldown and cooldown[0][0] == time:
                _, neg = cooldown.popleft()
                heapq.heappush(heap, neg)
        return time

Complexity

Comments

Chapter summary & decisions

Heap vs Sort vs Quickselect

Requirement Heap O(n log k) Sort O(n log n) Quickselect O(n) avg
Top-k with n large, k small ✅ Best OK ✅ When order doesn’t matter
Need k sorted elements Needs an extra sort
Streaming (data arrives in pieces)
Worst-case guarantee ❌ (worst O(n²))
Interview preference Default, easy to explain Small n “Linear time” demo

Median Finder invariant

max_heap (lo)      min_heap (hi)
  …,3,5,7,8  ←     ←  9,10,12,…
top = 8                top = 9

Task Scheduler two approaches

Top K Frequent Words tie-break

Sort by (-freq, word): descending frequency, ascending lexicographic.


Chapter 16 — Greedy

Greedy = at every step pick the locally best choice in the hope that the cumulative result is globally optimal. The trick: it is very hard to prove a greedy works. In an interview, you must both guess the right “greedy rule” and concisely justify why it is optimal. This chapter teaches 6 classic problems so you internalise the reasoning patterns.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Greedy-proof toolbox: 1. Exchange argument: assume another optimal solution differs from greedy. Swap one of its choices into the greedy choice without making it worse. Repeat → it equals greedy. 2. Matroid structure: rare in interviews but elegant in theory.

When greedy fails → DP saves you: if exchange does not preserve optimality, you must consider the global picture → DP.

Template code

def greedy_template(items):
    items.sort(key=...)         # 90% of greedy problems sort first
    result = 0
    for x in items:
        if local_condition(x):
            result += x
            # ... update state ...
    return result

End-of-chapter practice


16.1 Jump Game (LC 55)

Problem

Given nums, nums[i] = max steps you can jump from position i. Start at i = 0. Return True if you can reach i = n - 1.

Example

Input:  nums = [2, 3, 1, 1, 4]   → True
Explanation: 0 → 1 → 4 or 0 → 2 → 3 → 4

Input:  nums = [3, 2, 1, 0, 4]   → False
Explanation: stuck at index 3 (nums[3]=0).

Constraints

Clarifying questions

Approach

Brute force — DFS from 0, O(2^n). TLE.

Optimal — Greedy one pass, O(n).

Maintain farthest = the farthest index reachable so far. At each i: - If i > farthest → can’t reach i → return False. - Update farthest = max(farthest, i + nums[i]). - If farthest >= n - 1 → return True.

Illustration for [2, 3, 1, 1, 4]:

index :   0   1   2   3   4
nums  :   2   3   1   1   4

i=0: farthest = max(0, 0+2) = 2
i=1: 1 <= 2 OK; farthest = max(2, 1+3) = 4 → >= n-1=4 → True ✓

Code (Python 3)

from typing import List

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        farthest = 0
        for i, x in enumerate(nums):
            if i > farthest:
                return False
            farthest = max(farthest, i + x)
            if farthest >= len(nums) - 1:
                return True
        return True

Complexity

Comments


16.2 Jump Game II (LC 45)

Problem

Same setup as 16.1, but assume you always reach n - 1. Return the minimum number of jumps.

Example

Input:  nums = [2, 3, 1, 1, 4]
Output: 2
Explanation: 0 → 1 → 4.

Input:  nums = [2, 3, 0, 1, 4]
Output: 2

Constraints

Clarifying questions

Approach

Level-by-level BFS. Each “level” of BFS = positions reachable after k jumps.

One-pass Greedy implementation: - current_end = boundary of the current level. - farthest = farthest reachable from anywhere in the current level. - When i == current_end (level ends), bump jumps += 1 and set current_end = farthest.

Illustration for [2, 3, 1, 1, 4]:

i=0:  farthest = max(0, 0+2) = 2
      i == current_end (=0) → jumps=1, current_end=2
i=1:  farthest = max(2, 1+3) = 4
i=2:  farthest = max(4, 2+1) = 4
      i == current_end (=2) → jumps=2, current_end=4
i=3:  ... (we can stop once current_end >= n-1)

Answer: 2  ✓

Code (Python 3)

from typing import List

class Solution:
    def jump(self, nums: List[int]) -> int:
        jumps = 0
        current_end = 0
        farthest = 0
        for i in range(len(nums) - 1):
            farthest = max(farthest, i + nums[i])
            if i == current_end:
                jumps += 1
                current_end = farthest
                if current_end >= len(nums) - 1:
                    break
        return jumps

Complexity

Comments


16.3 Gas Station (LC 134)

Problem

A circular route has n gas stations; station i has gas[i] gas. Going from station i to i+1 costs cost[i]. Find the starting station so you can complete the loop, or -1 if impossible.

Example

Input:  gas = [1,2,3,4,5], cost = [3,4,5,1,2]
Output: 3
Explanation: start at station 3.

Input:  gas = [2,3,4], cost = [3,4,3]
Output: -1

Constraints

Clarifying questions

Approach

Brute force — try every start, O(n²). Can TLE.

Optimal Greedy — O(n).

Necessary & sufficient condition: sum(gas) >= sum(cost). Else -1.

When the condition holds, exactly one valid answer exists (with distinct sums). Find it via: - Walk through, maintaining tank = current balance. - If tank < 0 at station i → every start ∈ [last_start..i] fails. Set start = i + 1, reset tank = 0.

Illustration for gas = [1,2,3,4,5], cost = [3,4,5,1,2]:

i :    0     1     2     3     4
gas:   1     2     3     4     5
cost:  3     4     5     1     2
diff: -2    -2    -2    +3    +3

Cumulative tank:
  i=0: tank = -2 → negative, reset start=1, tank=0
  i=1: tank = -2 → reset start=2, tank=0
  i=2: tank = -2 → reset start=3, tank=0
  i=3: tank = +3 OK
  i=4: tank = +6 OK

sum(diff) = -2-2-2+3+3 = 0 >= 0 → a solution exists.
Answer: start=3 ✓

Code (Python 3)

from typing import List

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        if sum(gas) < sum(cost):
            return -1
        start = 0
        tank = 0
        for i in range(len(gas)):
            tank += gas[i] - cost[i]
            if tank < 0:
                start = i + 1
                tank = 0
        return start

Complexity

Comments


16.4 Assign Cookies (LC 455)

Problem

Given g[] (children’s greed factors) and s[] (cookie sizes). Child i is happy if assigned cookie j with s[j] >= g[i]. Each child gets at most one cookie; each cookie goes to at most one child. Maximise the number of happy children.

Example

Input:  g = [1, 2, 3], s = [1, 1]   → 1
Input:  g = [1, 2], s = [1, 2, 3]   → 2

Constraints

Clarifying questions

Approach

Classic Greedy. Sort both arrays ascending. Two pointers i (children), j (cookies). Walk cookies from small to large: if s[j] >= g[i] → child i gets cookie → i++; j++ always.

Justification: give the smallest cookie that satisfies the least-greedy child — no large cookies “wasted”. Exchange argument: any other optimal solution can be re-arranged into this greedy without losing satisfied children.

Code (Python 3)

from typing import List

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        i = j = 0
        while i < len(g) and j < len(s):
            if s[j] >= g[i]:
                i += 1
            j += 1
        return i

Complexity

Comments


16.5 Partition Labels (LC 763)

Problem

Given a string s, partition s into as many parts as possible so each character appears in only one part. Return the lengths of the parts.

Example

Input:  s = "ababcbacadefegdehijhklij"
Output: [9, 7, 8]
Explanation:
  "ababcbaca" - contains a, b, c.
  "defegde"   - contains d, e, f, g.
  "hijhklij"  - contains h, i, j, k, l.

Constraints

Clarifying questions

Approach

Greedy with “last occurrence”:

  1. Find last[ch] = last index of each character.
  2. Walk s, keep end = max(end, last[s[i]]).

Illustration for "ababcbacadefegdehijhklij":

last[a]=8, last[b]=5, last[c]=7, last[d]=14, last[e]=15, ...

i=0 (a): end = max(0, 8) = 8
i=1 (b): end = max(8, 5) = 8
...
i=8 (a): end = 8, i == end → part 1: length 9 (0..8)
i=9 (d): end = 14
...
i=15 (e): end = 15, i == end → part 2: length 7 (9..15)
...

Code (Python 3)

from typing import List

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        last = {ch: i for i, ch in enumerate(s)}
        result: list[int] = []
        start = end = 0
        for i, ch in enumerate(s):
            end = max(end, last[ch])
            if i == end:
                result.append(i - start + 1)
                start = i + 1
        return result

Complexity

Comments


16.6 Candy (LC 135)

Problem

Given an array ratings, distribute candies to n children so that: 1. Each child gets at least 1 candy. 2. A child with a higher rating than a neighbour gets more candies.

Return the minimum number of candies.

Example

Input:  ratings = [1, 0, 2]   → 5    (candies: [2, 1, 2])
Input:  ratings = [1, 2, 2]   → 4    (candies: [1, 2, 1])

Constraints

Clarifying questions

Approach

Two passes through the array: - Pass 1 (left → right): if ratings[i] > ratings[i-1]candies[i] = candies[i-1] + 1. - Pass 2 (right → left): if ratings[i] > ratings[i+1]candies[i] = max(candies[i], candies[i+1] + 1).

Sum of candies = answer.

Illustration for ratings = [1, 0, 2]:

ratings:    1   0   2

Pass 1 (L→R), init candies = [1, 1, 1]:
  i=1: r[1]=0 <= r[0]=1 → keep 1
  i=2: r[2]=2 > r[1]=0  → candies[2] = candies[1]+1 = 2
  → candies = [1, 1, 2]

Pass 2 (R→L):
  i=1: r[1]=0 <= r[2]=2 → keep
  i=0: r[0]=1 > r[1]=0  → candies[0] = max(1, candies[1]+1) = max(1, 2) = 2
  → candies = [2, 1, 2]

Sum: 2 + 1 + 2 = 5  ✓

Code (Python 3)

from typing import List

class Solution:
    def candy(self, ratings: List[int]) -> int:
        n = len(ratings)
        candies = [1] * n
        # Pass 1.
        for i in range(1, n):
            if ratings[i] > ratings[i - 1]:
                candies[i] = candies[i - 1] + 1
        # Pass 2.
        for i in range(n - 2, -1, -1):
            if ratings[i] > ratings[i + 1]:
                candies[i] = max(candies[i], candies[i + 1] + 1)
        return sum(candies)

Complexity

Comments

Chapter summary & decisions

Three ways to “justify greedy” in an interview

  1. Exchange argument: assume another optimal solution. Swap one of its choices for the greedy choice and show the cost doesn’t increase. Iterate → optimum equals greedy.
  2. Stay-ahead: at every step k, the greedy solution is at least as “advanced” as every other solution. By induction → globally optimal.
  3. Cut/Matroid property (for MST, Greedy Choice): every optimum contains the lightest edge of some cut.

Jump Game (LC 55) — farthest-reach invariant

i:        0  1  2  3  4
nums:    [2, 3, 1, 1, 4]
reach:    2  4  4  4  ≥4 ✅

Gas Station (LC 134) — why discard a failed segment?

If starting from s the tank goes negative at i, then every k in [s, i] also fails when starting from k (because we had surplus from s to k, yet still couldn’t cross from s to i+1). ⇒ Try i+1 next.

Candy (LC 135) — 2-pass invariant


Chapter 17 — Divide and Conquer

Divide and Conquer (D&C): split a problem into independent subproblems, solve recursively, then combine. Unlike DP (where subproblems can overlap), D&C subproblems do not overlap. Famous algorithms (merge sort, quicksort, fast power, closest pair) are all D&C.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Master-theorem cheat: - a = b, f(n) = O(n)T(n) = O(n log n) (merge sort). - a = 1, b = 2, f(n) = O(1)T(n) = O(log n) (binary search). - a = 2, b = 2, f(n) = O(n)T(n) = O(n log n).

Template code

def divide_conquer(arr, lo: int, hi: int):
    if lo >= hi:                    # base case
        return base_value(arr[lo])
    mid = (lo + hi) // 2
    left = divide_conquer(arr, lo, mid)
    right = divide_conquer(arr, mid + 1, hi)
    return combine(left, right)     # "conquer" step — typically O(n)

End-of-chapter practice


17.1 Maximum Subarray — D&C version (LC 53)

Problem

Given nums, find the contiguous subarray with maximum sum. Return the sum.

Example

Input:  nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6 (subarray [4,-1,2,1])

Constraints

Clarifying questions

Approach

Three approaches: 1. Kadane’s (1D DP, O(n)) — standard for LC 53. 2. Prefix sum + min, O(n). 3. Divide & Conquer, O(n log n)the focus of this chapter.

D&C insight: split [lo, hi] into [lo, mid], [mid+1, hi]. The maximum subarray is either: - Entirely in the left half (recursion). - Entirely in the right half (recursion). - Crossing the middle — starting in the left, ending in the right.

Crossing sum computed in O(n): - left_max = max of nums[mid] + nums[mid-1] + ... extending leftward. - right_max = same extending rightward. - crossing = left_max + right_max.

Take the max of the three.

Code (Python 3)

from typing import List

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:

        def dc(lo: int, hi: int) -> int:
            if lo == hi:
                return nums[lo]
            mid = (lo + hi) // 2

            left = dc(lo, mid)
            right = dc(mid + 1, hi)

            # Crossing — must include both nums[mid] and nums[mid+1].
            left_max = -float('inf')
            cur = 0
            for i in range(mid, lo - 1, -1):
                cur += nums[i]
                left_max = max(left_max, cur)
            right_max = -float('inf')
            cur = 0
            for i in range(mid + 1, hi + 1):
                cur += nums[i]
                right_max = max(right_max, cur)
            crossing = left_max + right_max

            return max(left, right, crossing)

        return dc(0, len(nums) - 1)

Complexity

Comments


17.2 Merge Sort & Count Inversions (classic)

Problem

Count the number of inversions in nums: pairs (i, j) with i < j and nums[i] > nums[j].

Example

Input:  nums = [2, 4, 1, 3, 5]
Output: 3
(pairs i < j with nums[i] > nums[j]; namely (2,1), (4,1), (4,3))

Constraints

Clarifying questions

Approach

Brute force — O(n²). Count every pair.

D&C via Merge Sort — O(n log n).

While merging two sorted halves: - When taking from the right half (nums_right[j] < nums_left[i]), every remaining element in the left half creates an inversion with nums_right[j] → add (len_left - i) to the counter.

Illustration for [2, 4, 1, 3, 5]:

Split:   [2, 4, 1]  |  [3, 5]
Recurse  [2, 4]  [1]    [3]  [5]
left:    [2] [4]
         merge [2,4]: 0 inversion
       merge [2,4] with [1]:
         take 1 from right → 2 elements [2,4] are larger → +2 inversion
         result: [1, 2, 4]; total = 2

right:   merge [3] [5]: 0 inversion → [3, 5]

merge [1, 2, 4] with [3, 5]:
  1 (left), 2 (left), 4 (left), 3 (right) — when 3 is taken, 4 still in left → +1
  4, 5 → 5 from right, nothing left → 0 inv

Total: 2 + 1 = 3  ✓

Code (Python 3)

from typing import List

class Solution:
    def countInversions(self, nums: List[int]) -> int:

        def merge_sort(arr: List[int]) -> tuple[List[int], int]:
            if len(arr) <= 1:
                return arr, 0
            mid = len(arr) // 2
            left, inv_l = merge_sort(arr[:mid])
            right, inv_r = merge_sort(arr[mid:])
            merged, inv_split = merge(left, right)
            return merged, inv_l + inv_r + inv_split

        def merge(L: List[int], R: List[int]) -> tuple[List[int], int]:
            i = j = inv = 0
            out: List[int] = []
            while i < len(L) and j < len(R):
                if L[i] <= R[j]:
                    out.append(L[i]); i += 1
                else:
                    out.append(R[j]); j += 1
                    inv += len(L) - i        # every L[i..] > R[j]
            out.extend(L[i:])
            out.extend(R[j:])
            return out, inv

        _, total = merge_sort(nums)
        return total

Complexity

Comments


17.3 Quickselect — Kth Largest (LC 215) — recap

Fully solved in Chapter 15.1. Here we summarise under the D&C lens.

Problem

Given nums, find the k-th largest element (1-indexed). The D&C pattern solves it in O(n) average.

Example

Input:  nums = [3, 2, 1, 5, 6, 4], k = 2
Output: 5

Input:  nums = [3, 2, 3, 1, 2, 4, 5, 5, 6], k = 4
Output: 4

Approach

Quickselect is a special D&C — recursing only one half (the one containing the Kth) instead of both. Therefore: - T(n) = T(n/2) + O(n) = O(n) average (not O(n log n) like Quicksort). - Worst-case O(n²) with bad pivots — avoid via random pivot.

Index direction: the partition below sorts ascending, so p is the p-th from smallest (0-based). The Kth largest lives at position len(nums) - k (0-based) after sorting. So the LC-public function takes k 1-indexed and converts it to target = len(nums) - k before calling quickselect.

Code (Python 3)

import random
from typing import List


class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        # Kth largest 1-indexed ⇔ index (n - k) 0-based after ascending sort.
        target = len(nums) - k
        return self._quickselect(nums, 0, len(nums) - 1, target)

    def _quickselect(self, arr: List[int], lo: int, hi: int, target: int) -> int:
        while lo <= hi:
            p = self._partition(arr, lo, hi)
            if p == target:
                return arr[p]
            elif p < target:
                lo = p + 1
            else:
                hi = p - 1
        return -1  # unreachable per problem

    def _partition(self, arr: List[int], lo: int, hi: int) -> int:
        # Random pivot to avoid worst-case O(n^2) on already-sorted input.
        rand = random.randint(lo, hi)
        arr[rand], arr[hi] = arr[hi], arr[rand]
        pivot = arr[hi]
        store = lo
        for i in range(lo, hi):
            if arr[i] < pivot:
                arr[store], arr[i] = arr[i], arr[store]
                store += 1
        arr[store], arr[hi] = arr[hi], arr[store]
        return store

Complexity

Comments


17.4 Pow(x, n) (LC 50) — recap

Fully solved in Chapter 3.2. Here we summarise under the D&C lens.

Problem

Compute x^n for real x and integer n (possibly negative). D&C gives O(log n).

Example

Input:  x = 2.00000, n = 10
Output: 1024.00000

Input:  x = 2.10000, n = 3
Output: 9.26100

Input:  x = 2.00000, n = -2
Output: 0.25000   (= 1 / 2^2)

Approach

x^n = (x^(n/2))^2 (even) or x · (x^((n-1)/2))^2 (odd). Each step halves nT(n) = T(n/2) + O(1) = O(log n).

Code (Python 3)

class Solution:
    def myPow(self, x: float, n: int) -> float:
        if n == 0: return 1.0
        if n < 0: return 1.0 / self.myPow(x, -n)
        half = self.myPow(x, n // 2)
        return half * half if n % 2 == 0 else half * half * x

Complexity


17.5 Different Ways to Add Parentheses (LC 241)

Problem

Given an expression s with numbers and operators +, -, *, return all possible values that can result from parenthesising it differently.

Example

Input:  s = "2*3-4*5"
Output: [-34, -14, -10, -10, 10]
Explanation:
  (2*(3-(4*5))) = -34
  ((2*3)-(4*5)) = -14
  ((2*(3-4))*5) = -10
  (2*((3-4)*5)) = -10
  (((2*3)-4)*5) = 10

Constraints

Clarifying questions

Approach

At each operator op, split s into left and right. Recursively compute all values of each side. Combine: for every (l, r) pair, append l op r to the result.

Memoise by substring to avoid recomputation.

Illustration for "2*3-4":

"2*3-4":
  At '*' (pos 1):
    Left  = "2" → [2]
    Right = "3-4" → recurse:
      At '-' (pos 1):
        Left = "3" → [3]
        Right = "4" → [4]
        → [3 - 4] = [-1]
      → "3-4" yields [-1]
    Combine: 2 * -1 = -2

  At '-' (pos 3):
    Left  = "2*3" → recurse → [6]
    Right = "4" → [4]
    → [6 - 4] = [2]

  → "2*3-4" yields [-2, 2]

Code (Python 3)

from functools import cache
from typing import List

class Solution:
    def diffWaysToCompute(self, s: str) -> List[int]:

        @cache
        def compute(expr: str) -> tuple[int, ...]:
            if expr.isdigit():
                return (int(expr),)
            result: list[int] = []
            for i, ch in enumerate(expr):
                if ch in '+-*':
                    for l in compute(expr[:i]):
                        for r in compute(expr[i + 1:]):
                            if ch == '+': result.append(l + r)
                            elif ch == '-': result.append(l - r)
                            else: result.append(l * r)
            return tuple(result)

        return list(compute(s))

Complexity

Comments


17.6 Closest Pair of Points (classic)

Problem

Given n points on the plane, find the pair with the smallest Euclidean distance. Must run in O(n log n).

Example

Input:  points = [(0,0), (1,1), (4,5), (2,2), (8,8)]
Output: ((0,0), (1,1))  dist = sqrt(2)

Constraints

Clarifying questions

Approach

Brute force — O(n²). Compare every pair.

D&C — O(n log n).

  1. Sort points by x.
  2. Split at x_mid.
  3. Recurse → d_left, d_right.
  4. d = min(d_left, d_right).
  5. Strip check: consider points with |x - x_mid| < d, sort by y, each point only compares with ≤ 7 next points in the strip (geometric proof).
  6. Return the min.

This is a CS-textbook classic — mostly absent from standard LeetCode, but asked at Big Tech interviews as a follow-up to brute O(n²).

Code (Python 3)

import math
from typing import List, Tuple

Point = Tuple[float, float]

class Solution:
    def closestPair(self, points: List[Point]) -> float:
        # Sort by x first.
        pts_x = sorted(points)

        def dist(p: Point, q: Point) -> float:
            return math.hypot(p[0] - q[0], p[1] - q[1])

        def dc(pts: List[Point]) -> float:
            n = len(pts)
            if n <= 3:
                return min(dist(pts[i], pts[j])
                           for i in range(n) for j in range(i + 1, n))
            mid = n // 2
            mid_x = pts[mid][0]
            d_left = dc(pts[:mid])
            d_right = dc(pts[mid:])
            d = min(d_left, d_right)

            # Strip.
            strip = sorted([p for p in pts if abs(p[0] - mid_x) < d],
                           key=lambda p: p[1])
            for i in range(len(strip)):
                # Proof: only need to check ≤ 7 neighbours along y.
                for j in range(i + 1, min(i + 8, len(strip))):
                    d = min(d, dist(strip[i], strip[j]))
            return d

        return dc(pts_x)

Complexity

Comments

Chapter summary & decisions

D&C recurrence framework

solve(P):
    if |P| ≤ threshold: brute()
    split P → P1, P2 (roughly equal)
    A1 = solve(P1)
    A2 = solve(P2)
    return combine(A1, A2, cross_information)

Recurrence tree (merge sort / inversion count)

n  ━━━ split ━━━ n/2, n/2 ━━━ split ━━━ n/4, n/4, n/4, n/4 ━━━ ...
                                                              depth = log n
combine cost = O(n) per layer × log n layers ⇒ O(n log n)

Quickselect vs Sort vs Heap (LC 215 — Kth Largest)

Quickselect Heap size k Sort
Average O(n) O(n log k) O(n log n)
Worst O(n²) (random pivot ↘) O(n log k) O(n log n)
In place ✗ extra heap
Code medium short shortest

Closest Pair (reference LC) — setup


Chapter 18 — Monotonic Queue + Stack

Monotonic stack/deque = a stack/deque whose elements maintain monotonicity (ascending or descending) as we go through. This is the strongest “weapon” for “next greater/smaller”, “range max/min in sliding window”, “largest rectangle”-style problems. Problem 8.5 (Daily Temperatures) teased it — this chapter goes deep.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

When ascending vs descending? - Ascending stack (top is the max in the stack): handy for “previous smaller”. - Descending stack (top is the min): for “previous greater” / “next greater”.

Invariant of monotonic stack/deque

The difference between a “regular stack” and a “monotonic stack” is the invariant — a rule that always holds. Understanding the invariant = understanding the entire pattern.

Monotonic decreasing stack (decreasing from bottom → top)

Invariant at all times:
   bottom → top
   [v0  ,  v1  ,  v2  ,  ...  ,  vk]
   v0 ≥ v1 ≥ v2 ≥ ... ≥ vk         (decreasing)

When pushing a new value x:
   - While stack[-1] < x: pop (stack[-1] no longer a candidate for "next greater")
   - Push x

Interpretation:
   • The stack holds candidates "waiting for their next greater".
   • When we see x larger than the top → the top has found its answer (= x) → pop.
   • Each index is pushed once, popped at most once → O(n) total.

Trace example for arr = [73, 74, 75, 71, 69, 72, 76]:

Step  i   v   Action                                Stack (index, value)
─────────────────────────────────────────────────────────────────────────
init                                                []
1     0   73  push                                  [(0, 73)]
2     1   74  74 > 73 → pop (0,73), result[0]=74    [(1, 74)]
3     2   75  75 > 74 → pop (1,74), result[1]=75    [(2, 75)]
4     3   71  push                                  [(2, 75), (3, 71)]
5     4   69  push                                  [(2, 75), (3, 71), (4, 69)]
6     5   72  72 > 69 → pop (4,69), result[4]=72    [(2, 75), (3, 71), (5, 72)]
                72 > 71 → pop (3,71), result[3]=72
7     6   76  76 > 72 → pop (5,72), result[5]=76    [(6, 76)]
                76 > 75 → pop (2,75), result[2]=76

Result: [74, 75, 76, 72, 72, 76, -1]
        Each index pushed once and popped ≤ once → O(n).

Monotonic deque (sliding window max)

Invariant at all times (deque decreasing from head → tail):

   head                              tail
   [i0  ,  i1  ,  i2  ,  ...  ,  ik]
   arr[i0] ≥ arr[i1] ≥ ... ≥ arr[ik]

   • Head is always the MAX of the current window.
   • Tail is the most recent element.

When the window slides (add index r, possibly drop index l):
   1. Pop from tail while arr[tail] ≤ arr[r]   (useless — r is newer and larger)
   2. Push r at the tail
   3. Pop head while head ≤ r - k             (outside the window)
   4. Max = arr[head]

Trace example for arr = [1, 3, -1, -3, 5, 3, 6, 7], k = 3:

i  v   deque (index)  Pop tail/head?            Max when i≥k-1=2
──────────────────────────────────────────────────────────────────
0  1   [0]            push 0
1  3   [1]            pop 0 (arr[0]=1 ≤ 3); push 1
2 -1   [1, 2]         push 2                     arr[1]=3
3 -3   [1, 2, 3]      push 3 (head 1 still > 3-k=0,
                       so within window)         arr[1]=3
4  5   [4]            pop 3,2,1 (all ≤ 5)        arr[4]=5
5  3   [4, 5]         push 5                     arr[4]=5
6  6   [6]            pop 5 (3 ≤ 6); pop 4 (5 ≤ 6); push 6  arr[6]=6
7  7   [7]            pop 6                      arr[7]=7

Output: [3, 3, 5, 5, 6, 7]  ✓

This invariant makes debugging fast: if the deque is no longer monotonic, or the head is outside the window, you have a bug for sure.

Template code

# 1) Monotonic decreasing stack — Next Greater Element
def next_greater(arr: list[int]) -> list[int]:
    n = len(arr)
    result = [-1] * n
    stack: list[int] = []           # index, values decreasing
    for i, v in enumerate(arr):
        while stack and arr[stack[-1]] < v:
            result[stack.pop()] = v
        stack.append(i)
    return result


# 2) Monotonic deque — Sliding Window Max
from collections import deque

def sliding_max(arr: list[int], k: int) -> list[int]:
    dq: deque[int] = deque()         # index, values decreasing head→tail
    out = []
    for i, v in enumerate(arr):
        while dq and arr[dq[-1]] <= v:
            dq.pop()
        dq.append(i)
        if dq[0] <= i - k:
            dq.popleft()
        if i >= k - 1:
            out.append(arr[dq[0]])
    return out

End-of-chapter practice


18.1 Daily Temperatures (LC 739) — recap

Fully solved in Chapter 8.5. Pattern: monotonic decreasing stack, each element pushed/popped exactly once → O(n).

Code (Python 3)

class Solution:
    def dailyTemperatures(self, t):
        result = [0] * len(t)
        stack = []
        for i, v in enumerate(t):
            while stack and t[stack[-1]] < v:
                j = stack.pop()
                result[j] = i - j
            stack.append(i)
        return result

Complexity


18.2 Next Greater Element II (LC 503)

Problem

Given a circular array nums, for each element find the first greater number going clockwise (the array wraps around). If none → -1.

Example

Input:  nums = [1, 2, 1]   → [2, -1, 2]
Explanation: 1 (idx 0) → 2; 2 → none; 1 (idx 2) → 2 (wraps).

Constraints

Clarifying questions

Approach

Circular trick: iterate 2n times, use i % n to access values. The monotonic stack stores real indices (0..n-1) and only pushes in the first round (the second round only propagates).

Code (Python 3)

from typing import List

class Solution:
    def nextGreaterElements(self, nums: List[int]) -> List[int]:
        n = len(nums)
        result = [-1] * n
        stack: list[int] = []
        for i in range(2 * n):
            cur = nums[i % n]
            while stack and nums[stack[-1]] < cur:
                result[stack.pop()] = cur
            if i < n:
                stack.append(i)
        return result

Complexity

Comments


18.3 Largest Rectangle in Histogram (LC 84)

Problem

Given heights[] (one bar of width 1 per index), find the largest rectangular area in the histogram.

Example

Input:  heights = [2, 1, 5, 6, 2, 3]
Output: 10
Explanation: choose bars [5, 6] → 2 * 5 = 10.

Constraints

Clarifying questions

Approach

Brute force — O(n²). For each bar i, expand sides until lower bars.

Monotonic increasing stack — O(n).

The stack holds indices, heights ascending from bottom to top. When we see h[i] smaller than h[stack top], the top bar ends on the right at i. Pop the top, compute the area: - height = h[top]. - width = i - stack[-1] - 1 (if the stack is empty after pop, width = i).

Illustration for heights = [2, 1, 5, 6, 2, 3]:

heights:    2   1   5   6   2   3
                       █
              █        █
              █        █
              █        █   █
              █        █   █   █
          █   █    █   █   █   █
       _________________________________
              0   1   2   3   4   5

Stack tracing (sentinel -1):
i=0  h=2: push 0     stack=[0]
i=1  h=1: h[0]=2 > 1 → pop 0. height=2, width=1-(-1)-1=1, area=2
         push 1     stack=[1]
i=2  h=5: push 2     stack=[1, 2]
i=3  h=6: push 3     stack=[1, 2, 3]
i=4  h=2: h[3]=6 > 2 → pop 3. height=6, width=4-2-1=1, area=6
         h[2]=5 > 2 → pop 2. height=5, width=4-1-1=2, area=10  ★
         push 4     stack=[1, 4]
i=5  h=3: push 5     stack=[1, 4, 5]

End of array. Right sentinel (i=6, h=0):
  pop 5. height=3, width=6-4-1=1, area=3
  pop 4. height=2, width=6-1-1=4, area=8
  pop 1. height=1, width=6-(-1)-1=6, area=6

Max = 10  ✓

Code (Python 3)

from typing import List

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        stack: list[int] = [-1]      # sentinel
        max_area = 0
        for i, h in enumerate(heights):
            while stack[-1] != -1 and heights[stack[-1]] >= h:
                height = heights[stack.pop()]
                width = i - stack[-1] - 1
                max_area = max(max_area, height * width)
            stack.append(i)
        # Drain the stack with right sentinel = n.
        n = len(heights)
        while stack[-1] != -1:
            height = heights[stack.pop()]
            width = n - stack[-1] - 1
            max_area = max(max_area, height * width)
        return max_area

Complexity

Comments


18.4 Sliding Window Maximum (LC 239)

Problem

Given nums and k, a window of k slides from left to right. Return the maximum at every window position.

Example

Input:  nums = [1,3,-1,-3,5,3,6,7], k = 3
Output: [3, 3, 5, 5, 6, 7]

Constraints

Clarifying questions

Approach

Brute force — O(n · k). TLE.

Monotonic Deque — O(n).

Deque holds indices, values decreasing from head → tail. When new i arrives: 1. Pop from tail if value <= nums[i] (they’re useless — i is newer and larger). 2. Push i to the tail. 3. Pop from the head if the index is outside the window (dq[0] <= i - k). 4. When i >= k - 1, nums[dq[0]] is the max.

Illustration for nums = [1,3,-1,-3,5,3,6,7], k = 3:

i  v  deque (index)  state                       max when i>=k-1=2
─────────────────────────────────────────────────────────────────
0  1  [0]            v=1
1  3  [1]            v=3 > 1, pop 0
2 -1  [1, 2]                                     nums[1]=3
3 -3  [1, 2, 3]      check head: 1 > 3-k=0 OK   nums[1]=3
4  5  [4]            5 > -3,-1,3 → pop all       nums[4]=5
5  3  [4, 5]                                     nums[4]=5
6  6  [6]            6 > 3,5 → pop              nums[6]=6
7  7  [7]            7 > 6 → pop                nums[7]=7

Output: [3, 3, 5, 5, 6, 7]  ✓

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        dq: deque[int] = deque()
        result: list[int] = []
        for i, v in enumerate(nums):
            while dq and nums[dq[-1]] <= v:
                dq.pop()
            dq.append(i)
            if dq[0] <= i - k:
                dq.popleft()
            if i >= k - 1:
                result.append(nums[dq[0]])
        return result

Complexity

Comments


18.5 Sum of Subarray Minimums (LC 907)

Problem

Given arr, return the sum of min(subarray) over every contiguous subarray. Modulo 10^9 + 7.

Example

Input:  arr = [3, 1, 2, 4]
Output: 17
Subarrays:  [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]
Mins:        3 + 1 + 2 + 4 + 1 + 1 + 2 + 1 + 1 + 1 = 17

Constraints

Clarifying questions

Approach

Reframe insight:

Instead of iterating over subarrays, ask: of how many subarrays is arr[i] the min?

arr[i] is the min of [l, r] ↔︎ l ∈ (prev_less[i], i] and r ∈ [i, next_less[i]).

So the number of subarrays with min = arr[i] = (i - prev_less[i]) * (next_less[i] - i).

Answer = Σ arr[i] * (i - prev_less[i]) * (next_less[i] - i).

Compute prev_less, next_less via monotonic stack in O(n).

Careful with duplicates: use strict < on one side, <= on the other so each subarray min is counted exactly once.

Code (Python 3)

from typing import List

class Solution:
    MOD = 10**9 + 7

    def sumSubarrayMins(self, arr: List[int]) -> int:
        n = len(arr)
        # prev_less[i]: nearest j < i with arr[j] < arr[i]; -1 if none.
        prev_less = [-1] * n
        stack: list[int] = []
        for i in range(n):
            while stack and arr[stack[-1]] >= arr[i]:
                stack.pop()
            prev_less[i] = stack[-1] if stack else -1
            stack.append(i)
        # next_less[i]: nearest j > i with arr[j] <= arr[i]; n if none.
        next_less = [n] * n
        stack.clear()
        for i in range(n - 1, -1, -1):
            while stack and arr[stack[-1]] > arr[i]:
                stack.pop()
            next_less[i] = stack[-1] if stack else n
            stack.append(i)

        result = 0
        for i in range(n):
            left = i - prev_less[i]
            right = next_less[i] - i
            result = (result + arr[i] * left * right) % self.MOD
        return result

Complexity

Comments


18.6 Remove K Digits (LC 402)

Problem

Given a digit string num and integer k, remove exactly k digits so that the resulting string is the smallest possible (preserving the order of remaining digits). Strip leading zeros; if empty return "0".

Example

Input:  num = "1432219", k = 3   → "1219"
Input:  num = "10200", k = 1     → "200"
Input:  num = "10", k = 2        → "0"

Constraints

Clarifying questions

Approach

Greedy + Monotonic increasing stack.

Walk num. For each digit d: - While the stack is non-empty, its top > d, and k > 0: pop the top (removing it makes the number smaller). - Push d.

At the end: if k > 0 remains, pop from the back of the stack (the last digit is the largest among those left, because the stack is ascending).

Finally: strip leading zeros and return.

Illustration for num = "1432219", k = 3:

i=0 '1': stack=[1]
i=1 '4': 4 > 1, push. stack=[1,4]
i=2 '3': 4 > 3, k=3 → pop. stack=[1]. now stack[-1]=1 < 3, push. stack=[1,3] k=2
i=3 '2': 3 > 2, k=2 → pop. stack=[1]. 1 < 2, push. stack=[1,2] k=1
i=4 '2': 2 not > 2, push. stack=[1,2,2]
i=5 '1': 2 > 1, k=1 → pop. stack=[1,2]. 2 > 1, k=0 → stop. push. stack=[1,2,1]
i=6 '9': k=0, no pop. push. stack=[1,2,1,9]

len=4 OK. Result "1219". ✓

Code (Python 3)

class Solution:
    def removeKdigits(self, num: str, k: int) -> str:
        stack: list[str] = []
        for d in num:
            while k > 0 and stack and stack[-1] > d:
                stack.pop()
                k -= 1
            stack.append(d)
        # Still k? Pop from the back (largest digit if stack is ascending).
        while k > 0:
            stack.pop()
            k -= 1
        # Strip leading zeros.
        result = ''.join(stack).lstrip('0')
        return result if result else '0'

Complexity

Comments

Chapter summary & decisions

Largest Rectangle (LC 84) — sentinel trace

Input: heights = [2, 1, 5, 6, 2, 3], append sentinel 0 at the end.

i  h[i]  stack (idx)   pop & compute       maxA
0   2    [0]                                0
1   1    pop 0: h=2, w=1-0  ⇒ 2             2
         [1]
2   5    [1,2]                              2
3   6    [1,2,3]                            2
4   2    pop 3: h=6, w=4-2-1=1 ⇒ 6          6
         pop 2: h=5, w=4-1-1=2 ⇒ 10        10
         [1,4]
5   3    [1,4,5]                           10
6   0    pop 5: h=3, w=6-4-1=1 ⇒ 3
         pop 4: h=2, w=6-1-1=4 ⇒ 8
         pop 1: h=1, w=6     ⇒ 6           10

Invariant: the stack stores indices whose heights are strictly ascending; a smaller height “closes the rectangle” of every taller bar to its left.

Sum of Subarray Minimums (LC 907) — contribution proof

Each a[i] contributes a[i] × left × right times as a minimum, with: - left[i] = number of elements to the left where a[i] is still the min (including i itself). - right[i] = the same to the right. - Tie-break: use < on the left, on the right (or vice versa) so each subarray min is counted exactly once.

Remove K Digits (LC 402) — greedy proof


Chapter 19 — Prefix Sum

Prefix Sum = P[i] = arr[0] + arr[1] + ... + arr[i-1]. Lets us compute the sum of [l, r] in O(1): P[r+1] - P[l]. This is the gateway to Chapter 6.4 (Subarray Sum K) and many problems involving sums / remainders / counting subarrays by condition. It is also the foundation for Fenwick Tree (Chapter 22).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

# 1) 1D prefix sum
P = [0] * (n + 1)
for i in range(n):
    P[i + 1] = P[i] + arr[i]

# Sum of arr[l..r] (inclusive):
sum_lr = P[r + 1] - P[l]


# 2) Prefix sum + hash counting subarray = K (Subarray Sum K)
from collections import defaultdict
def count_subarray_sum_k(arr, k):
    counts = defaultdict(int)
    counts[0] = 1
    cur = result = 0
    for x in arr:
        cur += x
        result += counts[cur - k]
        counts[cur] += 1
    return result


# 3) 2D prefix sum
P = [[0] * (cols + 1) for _ in range(rows + 1)]
for r in range(rows):
    for c in range(cols):
        P[r+1][c+1] = mat[r][c] + P[r][c+1] + P[r+1][c] - P[r][c]
# Sum of rect (r1, c1) → (r2, c2):
# P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1]

End-of-chapter practice


19.1 Range Sum Query - Immutable (LC 303)

Problem

Design a class: - NumArray(nums): initialise with an integer array. - sumRange(left, right): return the sum of nums[left..right] inclusive.

The array is immutable after construction. sumRange must be O(1).

Example

Input (LC-style operation arrays):
  ops  = ["NumArray",            "sumRange", "sumRange", "sumRange"]
  args = [[[-2, 0, 3, -5, 2, -1]], [0, 2],     [2, 5],     [0, 5]]

Output: [null, 1, -1, -3]

Explanation:
  NumArray([-2, 0, 3, -5, 2, -1])  → null  (internal prefix sum precomputed)
  sumRange(0, 2)                   → 1      (-2 + 0 + 3)
  sumRange(2, 5)                   → -1     (3 + (-5) + 2 + (-1))
  sumRange(0, 5)                   → -3     (sum of the whole array)

Constraints

Clarifying questions

Approach

Precompute the prefix sum in __init__. Each query is O(1).

Code (Python 3)

from typing import List

class NumArray:
    def __init__(self, nums: List[int]):
        n = len(nums)
        self.P = [0] * (n + 1)
        for i in range(n):
            self.P[i + 1] = self.P[i] + nums[i]

    def sumRange(self, left: int, right: int) -> int:
        return self.P[right + 1] - self.P[left]

Complexity

Comments


19.2 Subarray Sum Equals K (LC 560) — recap

Fully solved in Chapter 6.4. Pattern: prefix sum + hash counting cur - k.

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        counts = defaultdict(int)
        counts[0] = 1
        cur = result = 0
        for x in nums:
            cur += x
            result += counts[cur - k]
            counts[cur] += 1
        return result

Cross-references

Complexity


19.3 Continuous Subarray Sum (LC 523)

Problem

Given nums and k, return True if there is a subarray of length ≥ 2 with sum that is a multiple of k (including 0×k = 0).

Example

Input:  nums = [23, 2, 4, 6, 7], k = 6   → True
Explanation: [2, 4] sums to 6 = 1*6.

Input:  nums = [23, 2, 6, 4, 7], k = 6   → True
Explanation: [23, 2, 6, 4, 7] sums to 42 = 7*6.

Input:  nums = [1, 2, 3], k = 5          → False

Constraints

Clarifying questions

Approach

Prefix sum mod K + hash.

A subarray nums[l..r] is divisible by k ↔︎ P[r+1] % k == P[l] % k. Find two positions with the same modulo and distance ≥ 2.

Dict {remainder: earliest_index}. For each i, compute cur % k: - If we’ve seen this remainder before at index j and i - j >= 2 → True. - Otherwise → record i.

Code (Python 3)

from typing import List

class Solution:
    def checkSubarraySum(self, nums: List[int], k: int) -> bool:
        # remainder -> earliest index at which this prefix-sum remainder appeared.
        # Initialise {0: -1} so a subarray starting at index 0 counts.
        first_idx: dict[int, int] = {0: -1}
        cur = 0
        for i, x in enumerate(nums):
            cur += x
            rem = cur % k
            if rem in first_idx:
                if i - first_idx[rem] >= 2:
                    return True
            else:
                first_idx[rem] = i
        return False

Complexity

Comments


19.4 Range Sum Query 2D - Immutable (LC 304)

Problem

Design a class NumMatrix(matrix) with a method sumRegion(row1, col1, row2, col2) returning the sum of the rectangle with corners (row1, col1) and (row2, col2) (both inclusive) in O(1).

Example

Input (LC-style operation arrays):
  ops  = ["NumMatrix", "sumRegion", "sumRegion", "sumRegion"]
  args = [[[[3,0,1,4,2],
            [5,6,3,2,1],
            [1,2,0,1,5],
            [4,1,0,1,7],
            [1,0,3,0,5]]],
          [2, 1, 4, 3],
          [1, 1, 2, 2],
          [1, 2, 2, 4]]

Output: [null, 8, 11, 12]

Explanation:
  NumMatrix(matrix)          → init, returns null
  sumRegion(2, 1, 4, 3)      → sum of [2..4] × [1..3] = 8
  sumRegion(1, 1, 2, 2)      → sum of [1..2] × [1..2] = 11
  sumRegion(1, 2, 2, 4)      → sum of [1..2] × [2..4] = 12

Constraints

Clarifying questions

Approach

2D prefix sum: P[r+1][c+1] = the sum of the rectangle [0,0] → [r,c].

Inclusion-exclusion for the sum of [r1,c1] → [r2,c2]:

P[r2+1][c2+1] − P[r1][c2+1] − P[r2+1][c1] + P[r1][c1]

Illustration:

P[r2+1][c2+1] = sum of the whole rectangle (0,0)..(r2,c2)
  − P[r1][c2+1] = subtract the top strip (0,0)..(r1-1, c2)
  − P[r2+1][c1] = subtract the left strip (0,0)..(r2, c1-1)
  + P[r1][c1]  = add back the overlap subtracted twice

Code (Python 3)

from typing import List

class NumMatrix:
    def __init__(self, matrix: List[List[int]]):
        if not matrix or not matrix[0]:
            return
        rows, cols = len(matrix), len(matrix[0])
        self.P = [[0] * (cols + 1) for _ in range(rows + 1)]
        for r in range(rows):
            for c in range(cols):
                self.P[r + 1][c + 1] = (
                    matrix[r][c]
                    + self.P[r][c + 1]
                    + self.P[r + 1][c]
                    - self.P[r][c]
                )

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        return (
            self.P[row2 + 1][col2 + 1]
            - self.P[row1][col2 + 1]
            - self.P[row2 + 1][col1]
            + self.P[row1][col1]
        )

Complexity

Comments


19.5 Product of Array Except Self (LC 238) — recap

Fully solved in Chapter 1.3. Linking to prefix sum.

Code (Python 3)

from typing import List


class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        result = [1] * n
        left = 1
        for i in range(n):
            result[i] = left
            left *= nums[i]
        right = 1
        for i in range(n - 1, -1, -1):
            result[i] *= right
            right *= nums[i]
        return result

Complexity


19.6 Find Pivot Index (LC 724)

Problem

Given nums, find the pivot index — the index i such that the sum of nums[0..i-1] equals the sum of nums[i+1..n-1]. Return the leftmost index, or -1.

Example

Input:  nums = [1, 7, 3, 6, 5, 6]
Output: 3
Explanation: left sum 1+7+3=11; right sum 5+6=11.

Input:  nums = [1, 2, 3]   → -1
Input:  nums = [2, 1, -1]  → 0    (left = empty = 0; right = 1 + -1 = 0)

Constraints

Clarifying questions

Approach

left[i] + nums[i] + right[i] = total. Pivot ↔︎ left[i] == right[i] ↔︎ left[i] = (total - nums[i]) / 2.

One pass: keep left_sum. At i: pivot ↔︎ left_sum == total - left_sum - nums[i].

Code (Python 3)

from typing import List

class Solution:
    def pivotIndex(self, nums: List[int]) -> int:
        total = sum(nums)
        left_sum = 0
        for i, x in enumerate(nums):
            if left_sum == total - left_sum - x:
                return i
            left_sum += x
        return -1

Complexity

Comments

Chapter summary & decisions

P[0] = 0 — why

arr:  [ a0, a1, a2, a3 ]
P:    [  0, a0, a0+a1, a0+a1+a2, a0+a1+a2+a3 ]
       P[0] P[1] P[2]   P[3]      P[4]

Sum of arr[l..r] (inclusive, 0-indexed) = P[r+1] - P[l]. Without P[0] = 0, the formula needs a special case for l == 0.

Continuous Subarray Sum (LC 523) — distance condition

Modulo with negative numbers

Python’s % always returns [0, k): (-3) % 5 == 2. Safe for prefix modulo. Java/C++: (-3) % 5 == -3 → need ((x % k) + k) % k.

Product Except Self recap

Full code lives in Chapter 1.3. This recap emphasises: it’s prefix product + suffix product, not P[r+1] - P[l]. No separate P[0] = 1 is needed — the code uses left = 1, right = 1 initially.


Chapter 20 — Prime Number

The last chapter of Level 1, with a “basic number-theory” flavour. Prime numbers are not a huge interview pattern at Big Tech (Google asks occasionally), but it’s worth learning so the Sieve of Eratosthenes sits in your toolbox — a classical algorithm that remains extremely useful.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

def is_prime(n: int) -> bool:
    """Trial division O(sqrt(n))."""
    if n < 2:
        return False
    if n < 4:
        return True
    if n % 2 == 0:
        return False
    i = 3
    while i * i <= n:
        if n % i == 0:
            return False
        i += 2
    return True


def sieve(n: int) -> list[bool]:
    """Sieve of Eratosthenes — O(n log log n)."""
    is_p = [True] * (n + 1)
    is_p[0] = is_p[1] = False
    for i in range(2, int(n ** 0.5) + 1):
        if is_p[i]:
            for j in range(i * i, n + 1, i):
                is_p[j] = False
    return is_p


def prime_factors(n: int) -> list[int]:
    """Factor — O(sqrt(n))."""
    factors: list[int] = []
    d = 2
    while d * d <= n:
        while n % d == 0:
            factors.append(d)
            n //= d
        d += 1
    if n > 1:
        factors.append(n)
    return factors

End-of-chapter practice


20.1 Count Primes (LC 204)

Problem

Given n, return the count of primes strictly less than n.

Example

Input:  n = 10   → 4
Explanation: primes < 10 = {2, 3, 5, 7}.

Input:  n = 0    → 0
Input:  n = 1    → 0

Constraints

Clarifying questions

Approach

Brute force — trial division per number, O(n sqrt n). TLE at n = 5·10^6.

Sieve of Eratosthenes — O(n log log n).

A boolean is_prime[] array. For each i from 2, if is_prime[i] holds → mark every multiple (i*i, i*i+i, i*i+2i, ...) as composite.

Illustration for n = 30:

i=2: mark 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28
i=3: mark 9, 12, 15, 18, 21, 24, 27
i=4: composite → skip
i=5: mark 25
i=6..29: either composite or i² > 30 → skip

Remaining: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 → 10 primes.

Code (Python 3)

class Solution:
    def countPrimes(self, n: int) -> int:
        if n < 2:
            return 0
        is_p = [True] * n
        is_p[0] = is_p[1] = False
        for i in range(2, int(n ** 0.5) + 1):
            if is_p[i]:
                # Start at i*i — smaller multiples were already marked by smaller primes.
                for j in range(i * i, n, i):
                    is_p[j] = False
        return sum(is_p)

Complexity

Comments


20.2 Ugly Number II (LC 264)

Problem

An ugly number is a positive integer of the form 2^a · 3^b · 5^c. Return the n-th ugly number. (1 is also ugly.)

Example

Input:  n = 10
Output: 12
Explanation: the first 10 ugly numbers = 1, 2, 3, 4, 5, 6, 8, 9, 10, 12.

Constraints

Clarifying questions

Approach

Brute force counting and checking is too slow for large n.

Three-pointer DP — O(n).

ugly[k] = the k-th ugly. Each new ugly = min(ugly[i2]*2, ugly[i3]*3, ugly[i5]*5). After selecting, advance the corresponding pointer.

Illustration:

ugly = [1]                       i2=i3=i5=0
n2=1*2=2, n3=1*3=3, n5=1*5=5     min=2 → ugly=[1,2], i2=1
n2=2*2=4, n3=3, n5=5             min=3 → ugly=[1,2,3], i3=1
n2=4, n3=2*3=6, n5=5             min=4 → ugly=[1,2,3,4], i2=2
n2=3*2=6, n3=6, n5=5             min=5 → i5=1
n2=6, n3=6, n5=2*5=10            min=6 → i2=3, i3=2
...

Code (Python 3)

class Solution:
    def nthUglyNumber(self, n: int) -> int:
        ugly = [1]
        i2 = i3 = i5 = 0
        while len(ugly) < n:
            next_ugly = min(ugly[i2] * 2, ugly[i3] * 3, ugly[i5] * 5)
            ugly.append(next_ugly)
            if next_ugly == ugly[i2] * 2:
                i2 += 1
            if next_ugly == ugly[i3] * 3:
                i3 += 1
            if next_ugly == ugly[i5] * 5:
                i5 += 1
        return ugly[-1]

Complexity

Comments


20.3 Prime Arrangements (LC 1175)

Problem

Given n, count the permutations of 1..n such that every prime sits at a prime position (1-indexed). Modulo 10^9 + 7.

Example

Input:  n = 5
Output: 12
Explanation: 5 positions, 3 are prime (2, 3, 5). Primes in 1..5 are 3.
            3! * 2! = 6 * 2 = 12.

Constraints

Clarifying questions

Approach

Counting: let p = number of primes ≤ n. There are p! ways to place primes in the p prime positions, and (n - p)! ways for non-primes.

Answer = p! * (n - p)! mod (10^9 + 7).

Code (Python 3)

from math import factorial

class Solution:
    MOD = 10**9 + 7

    def numPrimeArrangements(self, n: int) -> int:
        # Count primes ≤ n.
        if n < 2:
            return 1
        is_p = [True] * (n + 1)
        is_p[0] = is_p[1] = False
        for i in range(2, int(n ** 0.5) + 1):
            if is_p[i]:
                for j in range(i * i, n + 1, i):
                    is_p[j] = False
        p = sum(is_p)
        return (factorial(p) * factorial(n - p)) % self.MOD

Complexity

Comments


20.4 Closest Prime Numbers in Range (LC 2523)

Problem

Given left, right, return the pair of primes (p, q) with left <= p < q <= right whose q - p is minimum. Ties: pick the pair with smaller p. None → [-1, -1].

Example

Input:  left=10, right=19
Output: [11, 13]

Constraints

Clarifying questions

Approach

  1. Sieve up to right, collect primes within [left, right].
  2. Scan adjacent pairs in the list for the smallest gap.

Code (Python 3)

from typing import List

class Solution:
    def closestPrimes(self, left: int, right: int) -> List[int]:
        is_p = [True] * (right + 1)
        is_p[0] = is_p[1] = False
        for i in range(2, int(right ** 0.5) + 1):
            if is_p[i]:
                for j in range(i * i, right + 1, i):
                    is_p[j] = False
        primes = [i for i in range(left, right + 1) if is_p[i]]
        if len(primes) < 2:
            return [-1, -1]
        best_gap = float('inf')
        result = [-1, -1]
        for i in range(len(primes) - 1):
            gap = primes[i + 1] - primes[i]
            if gap < best_gap:
                best_gap = gap
                result = [primes[i], primes[i + 1]]
        return result

Complexity

Comments


20.5 Largest Component Size by Common Factor (LC 952)

Problem

Given nums, connect two elements in the same component if they share at least one prime factor > 1. Return the largest component size.

Example

Input:  nums = [4, 6, 15, 35]   → 4
Explanation: 4 and 6 share 2; 6 and 15 share 3; 15 and 35 share 5 → one component.

Constraints

Clarifying questions

Approach

Insight: instead of unioning elements directly (which would need O(n²)), we union each element with its own prime factors. Two elements sharing any prime → same component.

Pseudocode: - For each x in nums, factor x. Union x with every prime factor. - Count component sizes.

Code (Python 3)

from collections import Counter
from typing import List

class DSU:
    def __init__(self): self.par = {}
    def find(self, x):
        if x not in self.par: self.par[x] = x
        while self.par[x] != x:
            self.par[x] = self.par[self.par[x]]
            x = self.par[x]
        return x
    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra != rb: self.par[ra] = rb


class Solution:
    def largestComponentSize(self, nums: List[int]) -> int:
        dsu = DSU()
        for x in nums:
            d = 2
            v = x
            while d * d <= v:
                if v % d == 0:
                    dsu.union(x, d)
                    while v % d == 0:
                        v //= d
                d += 1
            if v > 1:
                dsu.union(x, v)
        # Count: only over elements of nums (not prime nodes).
        counter = Counter(dsu.find(x) for x in nums)
        return max(counter.values())

Complexity

Comments


20.6 Distinct Prime Factors of Product in Array (LC 2521)

Problem

Given nums, return the number of distinct prime factors of the product of the elements.

Example

Input:  nums = [2, 4, 3, 7, 10, 6]
Output: 4
Explanation: product = 2·4·3·7·10·6 = 10080 = 2^5 · 3^2 · 5 · 7. 4 distinct primes.

Constraints

Clarifying questions

Approach

There’s no need to compute the actual product (it may overflow). For each number, gather its prime factors into a set.

Code (Python 3)

from typing import List

class Solution:
    def distinctPrimeFactors(self, nums: List[int]) -> int:
        primes: set[int] = set()
        for x in nums:
            d = 2
            while d * d <= x:
                while x % d == 0:
                    primes.add(d)
                    x //= d
                d += 1
            if x > 1:
                primes.add(x)
        return len(primes)

Complexity

Comments

Chapter summary & decisions

Prime toolbox — pick by constraint

n (bound) Method Time
Test a single n ≤ 10¹² Trial division up to √n O(√n)
Count primes ≤ n, n ≤ 10⁷ Sieve of Eratosthenes O(n log log n)
Factor many numbers ≤ n, n ≤ 10⁶ SPF (Smallest Prime Factor) sieve precompute O(n log log n), each factor O(log n)
Factor a single n ≤ 10¹⁸ Pollard ρ + Miller-Rabin sub-exponential

Largest Component by Common Factor (LC 952)

Prime Arrangements (LC 1175) — modular counting


Chapter 21 — Bit Manipulation + Mask

Bit manipulation unlocks O(1) solutions for many “seems-O(n)” problems. XOR, AND, OR + bit-shift tricks are a programmer’s second language. This chapter teaches 6 classic problems plus bit tricks you should memorise. Bitmask is expanded later in Chapter 40 (Bitmask DP) and 41 (Bitmask + Trie).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Bit-trick cheat-sheet

# Set bit i:                     x |= (1 << i)
# Clear bit i:                   x &= ~(1 << i)
# Toggle bit i:                  x ^= (1 << i)
# Test bit i:                    (x >> i) & 1
# Lowest set bit (rightmost 1):  x & -x      (e.g. x=12=0b1100 → 4=0b100)
# Pop lowest set bit:            x &= x - 1
# Count set bits:                bin(x).count('1')   # or x.bit_count() Py3.10+

# Iterate over subsets of mask:
sub = mask
while sub:
    # ... use sub ...
    sub = (sub - 1) & mask

# Check power of 2:              x > 0 and (x & (x - 1)) == 0
# Invert 32-bit:                 xor with 0xFFFFFFFF

End-of-chapter practice


21.1 Single Number (LC 136)

Problem

Given nums where every element appears twice except for one which appears once, find that element. O(n) time, O(1) space.

Example

Input:  nums = [2, 2, 1]            → Output: 1
Input:  nums = [4, 1, 2, 1, 2]      → Output: 4
(every element appears exactly twice except a unique element appearing once)

Constraints

Clarifying questions

Approach

XOR trick: a XOR a = 0; a XOR 0 = a. XOR is commutative and associative. XOR the whole array → pairs cancel, leaving the lonely element.

Code (Python 3)

from typing import List
from functools import reduce
from operator import xor

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        return reduce(xor, nums)

Complexity

Comments


21.2 Number of 1 Bits (LC 191)

Problem

Given integer n, return the number of 1 bits in its binary representation (Hamming weight).

Example

Input:  n = 11 (0b1011)   → 3
Input:  n = 128 (0b10000000) → 1

Constraints

Clarifying questions

Approach

Approach 1 — Bit loop, O(32). Approach 2 — n &= n - 1 trick, O(set bits). Each n & (n-1) clears the lowest 1 bit.

Illustration — n = 12 = 0b1100:

n = 1100
n-1=1011
n & (n-1) = 1000  → count = 1

n = 1000
n-1=0111
n & (n-1) = 0000  → count = 2

n = 0, stop. Total 2 bits.  ✓

Code (Python 3)

class Solution:
    def hammingWeight(self, n: int) -> int:
        count = 0
        while n:
            n &= n - 1
            count += 1
        return count

Complexity

Comments


21.3 Counting Bits (LC 338)

Problem

Given n, return an array of length n + 1 where result[i] = the set-bit count of i.

Example

Input:  n = 5
Output: [0, 1, 1, 2, 1, 2]

Constraints

Clarifying questions

Approach

Brute force — call hammingWeight(i) for each i, O(n log n).

DP — O(n): result[i] = result[i >> 1] + (i & 1).

Intuition: the set bits of i = the set bits of i >> 1 (drop the last bit) + the last bit.

Alternatively: result[i] = result[i & (i-1)] + 1.

Code (Python 3)

from typing import List

class Solution:
    def countBits(self, n: int) -> List[int]:
        result = [0] * (n + 1)
        for i in range(1, n + 1):
            result[i] = result[i >> 1] + (i & 1)
        return result

Complexity

Comments


21.4 Sum of Two Integers (LC 371)

Problem

Compute a + b without using + or -.

Example

Input:  a = 1, b = 2   → 3
Input:  a = 2, b = 3   → 5

Constraints

Clarifying questions

Approach

Insight: - a XOR b = the sum of bits without carry. - (a AND b) << 1 = carries. - Loop until carry = 0.

Code (Python 3)

class Solution:
    MASK = 0xFFFFFFFF

    def getSum(self, a: int, b: int) -> int:
        while b:
            carry = (a & b) << 1
            a = (a ^ b) & self.MASK
            b = carry & self.MASK
        # Handle negative numbers in Python (unbounded int).
        return a if a < 0x80000000 else ~(a ^ self.MASK)

Complexity

Comments


21.5 Bitwise AND of Numbers Range (LC 201)

Problem

Given left, right, return the AND of every integer in [left, right].

Example

Input:  left = 5 (0b101), right = 7 (0b111)
Output: 4 (0b100)
Explanation: 5 & 6 & 7 = 100 & 110 & 111 = 100 = 4.

Constraints

Clarifying questions

Approach

Insight: the AND over a range = common binary prefix of left and right, padded with zeros at the lower bits.

Because somewhere in a long range every lower bit hits 0 → ANDed away to 0.

Method: shift both right until left == right (find the common prefix), then shift back left.

Code (Python 3)

class Solution:
    def rangeBitwiseAnd(self, left: int, right: int) -> int:
        shifts = 0
        while left < right:
            left >>= 1
            right >>= 1
            shifts += 1
        return left << shifts

Complexity

Comments


21.6 Maximum XOR of Two Numbers in an Array (LC 421)

Problem

Given nums, return the largest XOR of two distinct elements.

Example

Input:  nums = [3, 10, 5, 25, 2, 8]
Output: 28
Explanation: 5 XOR 25 = 28.

Constraints

Clarifying questions

Approach

Brute force — O(n²). Every pair.

“Greedy bit” trick — O(n · 32).

Build the result one bit at a time from the top. At bit b: - Suppose the result from the high bit down to b+1 is result. - Try turning on bit b: candidate = result | (1 << b). - Check: does a pair (a, b) exist in nums with (a ^ b) & mask == candidate? (mask = bits ≥ b) - How to check: build prefixes = {x & mask for x in nums}. For each prefix p, test if p ^ candidate in prefixes.

If yes → keep candidate; otherwise → keep result as is.

Trie approach (Chapter 41) also works — that’s the extension.

Code (Python 3)

from typing import List

class Solution:
    def findMaximumXOR(self, nums: List[int]) -> int:
        result = 0
        mask = 0
        for b in range(31, -1, -1):
            mask |= (1 << b)
            prefixes = {x & mask for x in nums}
            candidate = result | (1 << b)
            for p in prefixes:
                if p ^ candidate in prefixes:
                    result = candidate
                    break
        return result

Complexity

Comments

Chapter summary & decisions

Python signed-int caveat (LC 371 Sum of Two Integers)

Python int is infinite-bit(-1) << 1 doesn’t overflow. To simulate C++ 32-bit:

MASK = 0xFFFFFFFF
INT_MIN_NEG = 0x80000000
while b:
    a, b = (a ^ b) & MASK, ((a & b) << 1) & MASK
return a if a < INT_MIN_NEG else ~(a ^ MASK)

Counting Bits — two recurrences

Range AND (LC 201) — common prefix

LC 421 (Max XOR of Two Numbers) is the gateway to Binary Trie in Chapter 41 (Bitmask + Trie). Each number = a 32-bit path through the trie; greedily pick the opposite bit to maximise XOR.


Chapter 22 — Advanced Tree (BST, Segment Tree, Fenwick Tree)

The advanced-tree chapter covers three key structures: BST (Binary Search Tree), Segment Tree, and Fenwick Tree (Binary Indexed Tree). All three answer “range query with update” in O(log n) per op. In Big Tech interviews, BST is asked frequently; Segment/Fenwick show up in Hard on-site rounds.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Quick selection: - Need sum / prefix only → Fenwick (simpler). - Need min / max / GCD / complex merge → Segment Tree. - Range updates with lazy → Segment Tree with lazy propagation.

Template code

# 1) BST validate / search — see Chapter 11.4

# 2) Fenwick Tree (1-indexed)
class Fenwick:
    def __init__(self, n: int):
        self.n = n
        self.tree = [0] * (n + 1)

    def update(self, i: int, delta: int) -> None:    # 1-indexed
        while i <= self.n:
            self.tree[i] += delta
            i += i & -i

    def query(self, i: int) -> int:                  # prefix sum [1..i]
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i
        return s

    def range_query(self, l: int, r: int) -> int:
        return self.query(r) - self.query(l - 1)


# 3) Segment Tree — sum, iterative version
class SegTree:
    def __init__(self, n: int):
        self.n = n
        self.tree = [0] * (2 * n)

    def update(self, i: int, val: int) -> None:
        i += self.n
        self.tree[i] = val
        while i > 1:
            i //= 2
            self.tree[i] = self.tree[2*i] + self.tree[2*i+1]

    def query(self, l: int, r: int) -> int:          # [l, r)
        res = 0
        l += self.n; r += self.n
        while l < r:
            if l & 1: res += self.tree[l]; l += 1
            if r & 1: r -= 1; res += self.tree[r]
            l //= 2; r //= 2
        return res

End-of-chapter practice


22.1 Validate BST (LC 98) — recap

Fully solved in Chapter 11.4. Pattern: DFS with bounds (low, high), or inorder check for strictly increasing.

Code (Python 3) (recap)

import math

class Solution:
    def isValidBST(self, root) -> bool:
        def dfs(node, low: float, high: float) -> bool:
            if not node:
                return True
            if not (low < node.val < high):
                return False
            return dfs(node.left, low, node.val) and \
                   dfs(node.right, node.val, high)
        return dfs(root, -math.inf, math.inf)

Complexity


22.2 Recover Binary Search Tree (LC 99)

Problem

Input: root of a BST (LC level-order serialised). Exactly two nodes of the BST have swapped values. Restore the BST (mutating values) without changing the tree structure. O(1) extra space (follow-up).

Example

Input:  root = [1, 3, null, null, 2]
        (LC level-order serialisation)

        Initial tree (NOT a valid BST):
              1
             / \
            3   *
             \
              2

Output: root after recovery = [3, 1, null, null, 2]
        Recovered tree (valid BST):
              3
             / \
            1   *
             \
              2

Explanation: the function mutates in place; the two nodes that were
            swapped hold values 1 and 3 → swap them back.

Constraints

Clarifying questions

Approach

Inorder traversal of a correct BST yields a strictly ascending sequence. With 2 swapped nodes, the inorder will show two violations (a > b with a before b).

After finding first, second → swap their values.

Code (Python 3)

class Solution:
    def recoverTree(self, root) -> None:
        first = second = prev = None

        def inorder(node) -> None:
            nonlocal first, second, prev
            if not node:
                return
            inorder(node.left)
            if prev and prev.val > node.val:
                if not first:
                    first = prev
                second = node
            prev = node
            inorder(node.right)

        inorder(root)
        first.val, second.val = second.val, first.val

Complexity

Comments


22.3 Serialize and Deserialize Binary Tree (LC 297)

Problem

Design two functions: serialize(root) -> str and deserialize(str) -> root.

Example

Input:  root = [1, 2, 3, null, null, 4, 5]
        (LC level-order serialisation)

        Actual tree:
              1
             / \
            2   3
               / \
              4   5

Output (serialize): "1,2,#,#,3,4,#,#,5,#,#"
        (preorder DFS; `#` marks a None node)

Round-trip requirement: deserialize(serialize(root)) recreates an
equivalent tree (same structure + same values) as the original root.

Constraints

Clarifying questions

Approach

Preorder DFS with # for None.

Code (Python 3)

class Codec:
    def serialize(self, root) -> str:
        parts: list[str] = []

        def go(node):
            if not node:
                parts.append('#')
                return
            parts.append(str(node.val))
            go(node.left)
            go(node.right)

        go(root)
        return ','.join(parts)

    def deserialize(self, data: str):
        tokens = iter(data.split(','))

        def build():
            tk = next(tokens)
            if tk == '#':
                return None
            node = TreeNode(int(tk))
            node.left = build()
            node.right = build()
            return node

        return build()

Complexity

Comments


22.4 Binary Tree Maximum Path Sum (LC 124)

Problem

Given root of a binary tree. A path is any node-sequence connected by edges (not necessarily through the root). Find the path with the maximum sum.

Example

Input:  root = [1, 2, 3]
        Actual tree:
              1
             / \
            2   3
Output: 6
        (path 2 → 1 → 3, sum = 6)

Input:  root = [-10, 9, 20, null, null, 15, 7]
        Actual tree:
              -10
              /  \
             9    20
                 /  \
                15   7
Output: 42
        (path 15 → 20 → 7, sum = 42, skip root -10 because it would lower the sum)

Constraints

Clarifying questions

Approach

Bottom-up DFS: each node returns its best “downward path to a leaf” = node.val + max(left_path, right_path, 0). Also update best with the “path passing through this node” = node.val + left_path + right_path.

max(..., 0) lets us drop any subtree with a negative sum.

Code (Python 3)

class Solution:
    def maxPathSum(self, root) -> int:
        self.best = -float('inf')

        def dfs(node) -> int:
            if not node:
                return 0
            left = max(dfs(node.left), 0)
            right = max(dfs(node.right), 0)
            self.best = max(self.best, node.val + left + right)
            return node.val + max(left, right)

        dfs(root)
        return self.best

Complexity

Comments


22.5 Count of Smaller Numbers After Self (LC 315)

Problem

Given nums, return counts[i] = the number of elements smaller than nums[i] that appear after it.

Example

Input:  nums = [5, 2, 6, 1]
Output: [2, 1, 1, 0]
Explanation:
  5 has {2, 1} smaller after → 2.
  2 has {1} → 1.
  6 has {1} → 1.
  1 has 0.

Constraints

Clarifying questions

Approach

Brute force — O(n²). TLE.

Approach 1 — Merge sort + inversion count (Chapter 17.2).

When merging two sorted halves, every time we take from the left L[i], everything already taken from the right is smaller than L[i] → add to counts[i].

Approach 2 — Fenwick Tree, O(n log n).

Walk from right to left. For each nums[i]: - counts[i] = fenwick.query(rank(nums[i]) - 1) — count of smaller values seen so far. - fenwick.update(rank(nums[i]), +1).

rank = position of nums[i] in sorted unique values → coordinate compression.

Code (Python 3) (Fenwick)

from typing import List
from bisect import bisect_left

class Fenwick:
    def __init__(self, n): self.n = n; self.tree = [0] * (n + 1)
    def update(self, i, d=1):
        while i <= self.n:
            self.tree[i] += d
            i += i & -i
    def query(self, i):
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i
        return s


class Solution:
    def countSmaller(self, nums: List[int]) -> List[int]:
        sorted_vals = sorted(set(nums))
        rank = {v: i + 1 for i, v in enumerate(sorted_vals)}  # 1-indexed
        f = Fenwick(len(sorted_vals))
        counts = [0] * len(nums)
        for i in range(len(nums) - 1, -1, -1):
            r = rank[nums[i]]
            counts[i] = f.query(r - 1)
            f.update(r, 1)
        return counts

Complexity

Comments


22.6 Range Sum Query - Mutable (LC 307)

Problem

Design a class: - NumArray(nums): initialise. - update(index, val): set nums[index] = val. - sumRange(left, right): sum of nums[left..right].

Both update and sumRange must be O(log n).

Example

Input (LC-style operation arrays):
  ops  = ["NumArray", "sumRange", "update", "sumRange"]
  args = [[[1, 3, 5]], [0, 2],    [1, 2],   [0, 2]]

Output: [null, 9, null, 8]

Explanation:
  NumArray([1, 3, 5])  → init, returns null
  sumRange(0, 2)       → 1 + 3 + 5 = 9
  update(1, 2)         → set nums[1] = 2; array is now [1, 2, 5]
  sumRange(0, 2)       → 1 + 2 + 5 = 8

Constraints

Clarifying questions

Approach

Fenwick Tree is the ideal pick — the shortest code for this problem. A Segment Tree also works but is longer.

Code (Python 3) (Fenwick)

from typing import List

class NumArray:
    def __init__(self, nums: List[int]):
        self.n = len(nums)
        self.nums = nums[:]
        self.tree = [0] * (self.n + 1)
        for i, x in enumerate(nums):
            self._add(i + 1, x)

    def _add(self, i: int, delta: int) -> None:
        while i <= self.n:
            self.tree[i] += delta
            i += i & -i

    def _prefix(self, i: int) -> int:
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i
        return s

    def update(self, index: int, val: int) -> None:
        delta = val - self.nums[index]
        self.nums[index] = val
        self._add(index + 1, delta)

    def sumRange(self, left: int, right: int) -> int:
        return self._prefix(right + 1) - self._prefix(left)

Complexity

Comments

Chapter summary & decisions

Tree family map

Structure Supports When to use
Binary Tree (general) Traversal, paths LC 124, 543
BST (sorted invariant) Search, kth, range count LC 230, 938
Fenwick / BIT Prefix sum / count, point update LC 307, 315
Segment Tree General range query/update LC 732, 218
Trie Prefix strings Chapters 23, 41

Fenwick vs Segment Tree

Fenwick Segment Tree
Basic op Prefix sum, point update Range sum, range min/max/gcd…
Code ~10 lines ~40-60 lines
Memory n 4n
Lazy propagation Hard Easy
Pick when Only need prefix/sum Need complex range query + update

Count Smaller After Self (LC 315) — coordinate compression

Serialize / Deserialize (LC 297) — choose a traversal


Chapter 23 — Trie

Trie (prefix tree) is a tree where each node represents one character; every root-to-node path = one prefix. Tries elegantly solve “find a word with prefix X”, “search with wildcards”, “longest common prefix across many queries”. The Bitmask + Trie pattern shows up in Chapter 41.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

class TrieNode:
    __slots__ = ("children", "is_end")
    def __init__(self):
        self.children: dict[str, "TrieNode"] = {}
        self.is_end = False


class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_end = True

    def search(self, word: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end

    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None

    def _find(self, s: str) -> "TrieNode | None":
        node = self.root
        for ch in s:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

End-of-chapter practice


23.1 Implement Trie (LC 208)

Problem

Implement a Trie class with three methods: insert, search, startsWith. Each op must run in O(length(word)).

Example

Input (LC-style operation arrays):
  ops  = ["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
  args = [[],     ["apple"], ["apple"], ["app"], ["app"],      ["app"],  ["app"]]

Output: [null, null, true, false, true, null, true]

Explanation:
  Trie()                  → init, returns null
  insert("apple")         → null
  search("apple")         → true
  search("app")           → false  ("app" not yet inserted on its own)
  startsWith("app")       → true   (still a prefix of "apple")
  insert("app")           → null
  search("app")           → true

Constraints

Clarifying questions

Approach

Trie = a tree where each node is one character; root → node is a prefix. Insert: walk character by character, create new nodes as needed, mark is_end at the final node. Search/StartsWith: walk by character, return False on a missing node.

Code (Python 3)

class TrieNode:
    __slots__ = ("children", "is_end")
    def __init__(self):
        self.children: dict[str, "TrieNode"] = {}
        self.is_end = False


class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word: str) -> None:
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_end = True

    def search(self, word: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end

    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None

    def _find(self, s: str) -> "TrieNode | None":
        node = self.root
        for ch in s:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

Complexity

Comments


23.2 Add and Search Word (LC 211)

Problem

Implement a class with addWord(word) and search(word). search allows . to match any single character.

Example

Input (LC-style operation arrays):
  ops  = ["WordDictionary", "addWord", "addWord", "addWord", "search", "search", "search", "search"]
  args = [[],               ["bad"],   ["dad"],   ["mad"],   ["pad"],  ["bad"],  [".ad"],  ["b.."]]

Output: [null, null, null, null, false, true, true, true]

Explanation:
  WordDictionary()      → null
  addWord("bad")        → null
  addWord("dad")        → null
  addWord("mad")        → null
  search("pad")         → false   ("pad" not added)
  search("bad")         → true
  search(".ad")         → true    (`.` matches any single char → "bad", "dad", "mad")
  search("b..")         → true    (matches "bad")

Constraints

Clarifying questions

Approach

addWord is identical to a standard Trie. search needs recursive DFS when it hits .: try all children.

Code (Python 3)

class WordDictionary:
    def __init__(self):
        self.root: dict = {}

    def addWord(self, word: str) -> None:
        node = self.root
        for ch in word:
            node = node.setdefault(ch, {})
        node['$'] = True       # end marker

    def search(self, word: str) -> bool:
        def dfs(node: dict, i: int) -> bool:
            if i == len(word):
                return '$' in node
            ch = word[i]
            if ch == '.':
                return any(dfs(child, i + 1)
                           for k, child in node.items() if k != '$')
            if ch not in node:
                return False
            return dfs(node[ch], i + 1)

        return dfs(self.root, 0)

Complexity

Comments


23.3 Word Search II (LC 212)

Problem

Given a board and a list of words, return every word that appears on the board (via 4-direction paths, no cell visited twice).

Example

Input:  board = [["o","a","a","n"],
                 ["e","t","a","e"],
                 ["i","h","k","r"],
                 ["i","f","l","v"]]
        words = ["oath", "pea", "eat", "rain"]

Output: ["oath", "eat"]   (output order may vary)

Constraints

Clarifying questions

Approach

Brute force: for each word, DFS from each cell → O(W · m · n · 4^L). TLE.

Trie + DFS — O(m · n · 4^L) (independent of word count).

Build a Trie from words. DFS from each cell while walking the Trie in parallel. On hitting a node with is_end → record the word. After recording a word, delete it from the trie (set is_end = None) to avoid duplicates.

Code (Python 3)

from typing import List

class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        # 1. Build Trie.
        root: dict = {}
        for w in words:
            node = root
            for ch in w:
                node = node.setdefault(ch, {})
            node['$'] = w

        rows, cols = len(board), len(board[0])
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        result: list[str] = []

        def dfs(r: int, c: int, node: dict) -> None:
            ch = board[r][c]
            if ch not in node:
                return
            nxt = node[ch]
            if '$' in nxt:
                result.append(nxt['$'])
                del nxt['$']           # avoid duplicates
            board[r][c] = '#'
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != '#':
                    dfs(nr, nc, nxt)
            board[r][c] = ch
            if not nxt:
                del node[ch]            # prune

        for r in range(rows):
            for c in range(cols):
                dfs(r, c, root)
        return result

Complexity

Comments


23.4 Longest Word in Dictionary (LC 720)

Problem

Given words, find the longest word such that every prefix of it also appears in the array. Tie → take the lexicographically smallest.

Example

Input:  words = ["w","wo","wor","worl","world"]
Output: "world"   (every nested prefix is in words; lex smallest on tie)

Input:  words = ["a","banana","app","appl","ap","apply","apple"]
Output: "apple"
(both "apple" and "apply" have all nested prefixes in words;
 "apple" < "apply" lexicographically → pick "apple")

Constraints

Clarifying questions

Approach

Approach 1 — Sort + set, O(N · L log N). Sort words. Maintain a valid set. Walk each word: if every prefix (only need to check length-1 prefix) is in valid, add it.

Approach 2 — Trie + BFS/DFS. Build a Trie. DFS preorder, preferring is_end. Only descend if is_end is true.

Approach 1 is shorter for this problem.

Code (Python 3)

from typing import List

class Solution:
    def longestWord(self, words: List[str]) -> str:
        words.sort()                          # lex
        valid: set[str] = {""}
        best = ""
        for w in words:
            if w[:-1] in valid:
                valid.add(w)
                if len(w) > len(best):
                    best = w
        return best

Complexity

Comments


23.5 Replace Words (LC 648)

Problem

Given a dictionary of “roots” and a sentence. For each word in the sentence, replace it with the shortest root in the dictionary that is a prefix of it. If no root → leave it alone.

Example

Input:  dictionary = ["cat","bat","rat"]
        sentence = "the cattle was rattled by the battery"
Output: "the cat was rat by the bat"

Constraints

Clarifying questions

Approach

Trie. Insert every root. For each word in the sentence, walk the Trie: on the first is_end → replace the word with the prefix up to that point.

Code (Python 3)

from typing import List

class Solution:
    def replaceWords(self, dictionary: List[str], sentence: str) -> str:
        root: dict = {}
        for w in dictionary:
            node = root
            for ch in w:
                node = node.setdefault(ch, {})
            node['$'] = True

        def replace(word: str) -> str:
            node = root
            prefix = []
            for ch in word:
                if '$' in node:
                    return ''.join(prefix)
                if ch not in node:
                    return word
                prefix.append(ch)
                node = node[ch]
            return ''.join(prefix) if '$' in node else word

        return ' '.join(replace(w) for w in sentence.split())

Complexity

Comments


23.6 Stream of Characters (LC 1032)

Problem

Design StreamChecker whose constructor takes a list of words. The method query(letter) returns True if some suffix of the current stream matches a word in the list.

Example

Input (LC-style operation arrays):
  ops  = ["StreamChecker", "query", "query", "query", "query", "query", "query"]
  args = [[["cd","f","kl"]], ["a"],  ["b"],   ["c"],   ["d"],   ["e"],   ["f"]]

Output: [null, false, false, false, true, false, true]

Explanation:
  StreamChecker(["cd","f","kl"])
  query('a')  → false  (stream = "a")
  query('b')  → false  (stream = "ab")
  query('c')  → false  (stream = "abc")
  query('d')  → true   (suffix "cd" of stream "abcd" matches)
  query('e')  → false
  query('f')  → true   (suffix "f" matches)

Constraints

Clarifying questions

Approach

Insight: suffixes are hard to look up in a prefix Trie. Reverse trick — build the Trie on reversed words. Reverse the stream too (newest first): now a prefix of the reversed stream matches the Trie.

Implementation: keep a buffer of queried characters. Each query, walk the Trie reading the buffer back to front (= reversed word).

Code (Python 3)

from typing import List

class StreamChecker:
    def __init__(self, words: List[str]):
        self.root: dict = {}
        for w in words:
            node = self.root
            for ch in reversed(w):
                node = node.setdefault(ch, {})
            node['$'] = True
        self.buf: list[str] = []

    def query(self, letter: str) -> bool:
        self.buf.append(letter)
        node = self.root
        for ch in reversed(self.buf):
            if '$' in node:
                return True
            if ch not in node:
                return False
            node = node[ch]
        return '$' in node

Complexity

Comments

Chapter summary & decisions

Trie design trade-off

Criterion Dict children {ch: TrieNode} Array [26]
Memory Small when sparse Large but uniform
Access O(1) hash O(1) index
Unicode support ❌ (26 letters only)
Code Pythonic, concise Faster in C++/Java

Word Search II (LC 212) — pruning

Stream of Characters (LC 1032) — reverse trie

Replace Words (LC 648) — shortest root

While descending the trie, stop at the first terminal node — that is the shortest root to replace with. Going further only finds longer roots (not what we want).


Chapter 24 — Union Find (Disjoint Set Union)

Union Find (DSU) is the data structure for two operations on disjoint sets: (i) find(x) — the root of the set containing x; (ii) union(x, y) — merge two sets. With path compression + union by rank/size, each op is nearly O(1) (precisely O(α(n)) with α the inverse Ackermann function).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

class DSU:
    def __init__(self, n: int):
        self.parent = list(range(n))
        self.rank = [0] * n
        self.size = [1] * n
        self.components = n

    def find(self, x: int) -> int:
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]   # path compression
            x = self.parent[x]
        return x

    def union(self, x: int, y: int) -> bool:
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return False              # already same set
        # Union by rank — attach the shorter tree below the taller one.
        if self.rank[rx] < self.rank[ry]:
            rx, ry = ry, rx
        self.parent[ry] = rx
        self.size[rx] += self.size[ry]
        if self.rank[rx] == self.rank[ry]:
            self.rank[rx] += 1
        self.components -= 1
        return True

    def connected(self, x: int, y: int) -> bool:
        return self.find(x) == self.find(y)

End-of-chapter practice


24.1 Number of Provinces (LC 547)

Problem

There are n cities and a matrix isConnected[i][j] = 1 if i and j are directly connected. A province is a maximal connected set of cities. Count the provinces.

Example

Input:  isConnected=[[1,1,0],[1,1,0],[0,0,1]]
Output: 2

Constraints

Clarifying questions

Approach

DSU: union every connected pair, count components. Or DFS/BFS — simpler.

Code (Python 3)

from typing import List

class Solution:
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        n = len(isConnected)
        dsu = DSU(n)
        for i in range(n):
            for j in range(i + 1, n):
                if isConnected[i][j]:
                    dsu.union(i, j)
        return dsu.components

Complexity

Comments


24.2 Redundant Connection (LC 684)

Problem

Input: edges: List[List[int]] — edges [u, v] of an undirected graph. The graph was a tree (n nodes, n-1 edges) before one extra edge was added that created a cycle. Return the last edge in the input that we can remove so the rest forms a tree.

Example

Input:  edges=[[1,2],[1,3],[2,3]]
Output: [2,3]

Constraints

Clarifying questions

Approach

DSU union edge by edge. The first edge where find(u) == find(v) is the one forming the cycle → return it.

Code (Python 3)

from typing import List

class Solution:
    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        n = len(edges)
        dsu = DSU(n + 1)
        for u, v in edges:
            if not dsu.union(u, v):
                return [u, v]
        return []

Complexity

Comments


24.3 Accounts Merge (LC 721)

Problem

Given accounts where each entry is [name, email1, email2, ...]. Two accounts belong to the same person ↔︎ they share at least one email. Merge accounts of the same person; output emails lex-sorted.

Example

Input:  accounts=[["John","a@x","b@x"],["John","b@x","c@x"],["Mary","d@x"]]
Output: [["John","a@x","b@x","c@x"],["Mary","d@x"]]

Constraints

Clarifying questions

Approach

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
        dsu_parent: dict[str, str] = {}
        email_to_name: dict[str, str] = {}

        def find(x: str) -> str:
            if dsu_parent.setdefault(x, x) != x:
                dsu_parent[x] = find(dsu_parent[x])
            return dsu_parent[x]

        def union(x: str, y: str) -> None:
            dsu_parent[find(x)] = find(y)

        for acc in accounts:
            name, emails = acc[0], acc[1:]
            for e in emails:
                email_to_name[e] = name
                union(emails[0], e)

        groups: dict[str, list[str]] = defaultdict(list)
        for e in email_to_name:
            groups[find(e)].append(e)

        return [[email_to_name[g[0]]] + sorted(g) for g in groups.values()]

Complexity

Comments


24.4 Number of Islands II (LC 305)

Problem

Given an m × n grid initially all water. Given positions[i] = [r, c] — turn each cell into land in order. After each addition, return the current island count.

Example

Input:  m = 3, n = 3
        positions = [[0,0], [0,1], [1,2], [2,1]]

Output: [1, 1, 2, 3]

Step-by-step trace:

  After [0,0]:  [1, 0, 0]     → 1 island
                [0, 0, 0]
                [0, 0, 0]

  After [0,1]:  [1, 1, 0]     → 1 island  (connects to (0,0))
                [0, 0, 0]
                [0, 0, 0]

  After [1,2]:  [1, 1, 0]     → 2 islands
                [0, 0, 1]
                [0, 0, 0]

  After [2,1]:  [1, 1, 0]     → 3 islands
                [0, 0, 1]
                [0, 1, 0]

Constraints

Clarifying questions

Approach

Online DSU. For each position: - Mark as land, components += 1. - For each of the 4 neighbours already land, union → if union succeeds, components -= 1. - Push components into the result.

Code (Python 3)

from typing import List

class Solution:
    def numIslands2(self, m: int, n: int, positions: List[List[int]]) -> List[int]:
        parent = [-1] * (m * n)         # -1 = water
        components = 0
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        result: list[int] = []

        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        def union(x: int, y: int) -> bool:
            rx, ry = find(x), find(y)
            if rx == ry: return False
            parent[rx] = ry
            return True

        for r, c in positions:
            idx = r * n + c
            if parent[idx] != -1:
                result.append(components)
                continue
            parent[idx] = idx
            components += 1
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < m and 0 <= nc < n:
                    nidx = nr * n + nc
                    if parent[nidx] != -1 and union(idx, nidx):
                        components -= 1
            result.append(components)
        return result

Complexity

Comments


24.5 Satisfiability of Equality Equations (LC 990)

Problem

Given equations of the form "a==b" or "a!=b". Return True if values can be assigned to variables to satisfy every equation.

Example

Input:  ["a==b","b!=a"]
Output: False

Constraints

Clarifying questions

Approach

Two passes: 1. Union all == equations. 2. Check != equations: if two variables share a root → contradiction.

Code (Python 3)

from typing import List

class Solution:
    def equationsPossible(self, equations: List[str]) -> bool:
        parent = list(range(26))

        def find(x):
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        for eq in equations:
            if eq[1] == '=':
                parent[find(ord(eq[0]) - 97)] = find(ord(eq[3]) - 97)
        for eq in equations:
            if eq[1] == '!':
                if find(ord(eq[0]) - 97) == find(ord(eq[3]) - 97):
                    return False
        return True

Complexity

Comments


24.6 Swim in Rising Water (LC 778)

Problem

A matrix grid[i][j] = “elevation” at cell (i, j). At time t, every cell with elevation ≤ t is submerged enough to swim into. Starting at (0, 0), reach (n-1, n-1). Find the smallest t.

Example

Input:  grid=[[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]]
Output: 16

Constraints

Clarifying questions

Approach

Approach 1 — Offline DSU. Sort cells by elevation ascending. “Activate” cells one at a time and union with active 4-neighbours. When (0,0) and (n-1,n-1) lie in the same component → return the elevation of the cell just added.

Approach 2 — Binary search on the answer + DFS (Chapter 37). Approach 3 — Dijkstra (Chapter 30).

Approach 1 is the most “DSU-flavoured” pattern.

Code (Python 3)

from typing import List

class Solution:
    def swimInWater(self, grid: List[List[int]]) -> int:
        n = len(grid)
        parent = list(range(n * n))
        def find(x):
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        # Enumerate cells by increasing elevation.
        cells = sorted((grid[r][c], r, c) for r in range(n) for c in range(n))
        active = [[False] * n for _ in range(n)]
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        for t, r, c in cells:
            active[r][c] = True
            idx = r * n + c
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < n and 0 <= nc < n and active[nr][nc]:
                    parent[find(idx)] = find(nr * n + nc)
            if find(0) == find(n * n - 1):
                return t
        return -1

Complexity

Comments

Chapter summary & decisions

DSU = “dynamic connectivity, no traversal”

DSU invariant — parent forest

nodes:  0  1  2  3  4  5
parent: 0  0  0  3  3  5   (after union(1,0), union(2,1), union(4,3))

Forest:
   0           3        5
  / \          |
 1   2         4

Path compression — before/after

Before find(4):           After find(4) with compression:
   0                            0
   |                          / | \
   1                         1  2  4
   |                         |
   2                         3  ← 4 points directly to 0 via compression
   |
   3
   |
   4

Number of Islands II (LC 305) — online add

Swim in Rising Water (LC 778) — threshold connectivity

A bridge to Chapter 37 (BS + Graph) and Chapter 33 (MST): - Sort cells by ascending elevation. - Union adjacent cells that have been “flooded” by the current elevation. - When (0, 0) and (n−1, n−1) fall into the same component, the current elevation is the answer.


Chapter 25 — Advanced Binary Search (Search on Answer)

Chapter 5 taught binary search on sorted arrays. This chapter teaches a stronger technique: binary search on the answer (search on answer / parametric search). When the answer space has a monotonic predicate, we binary-search on the value of the answer instead of on an index.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Procedure: 1. Identify the answer (capacity, speed, distance, …). 2. Bound [lo, hi] for the answer. 3. Write check(mid) -> bool monotonic. 4. Binary-search for “first True” or “last True”.

Template code

def search_on_answer(lo: int, hi: int, check) -> int:
    """Find the smallest value in [lo, hi] with check(x)=True."""
    while lo < hi:
        mid = (lo + hi) // 2
        if check(mid):
            hi = mid                # try to shrink to the True side
        else:
            lo = mid + 1
    return lo

End-of-chapter practice


25.1 Koko Eating Bananas (LC 875)

Problem

Koko has n piles of bananas, pile i has piles[i] bananas. Each hour Koko eats one pile at speed k bananas/hour (if the pile is smaller, the hour still counts). Find the smallest k such that all bananas are eaten in h hours.

Example

Input:  piles = [3,6,7,11], h = 8   → 4
Input:  piles = [30,11,23,4,20], h = 5 → 30
Input:  piles = [30,11,23,4,20], h = 6 → 23

Constraints

Clarifying questions

Approach

The answer k[1, max(piles)]. The predicate check(k) = “eat everything in ≤ h hours?”.

time(k) = Σ ceil(piles[i] / k). The predicate is monotonic: bigger k → smaller time. → Find the smallest k with time(k) ≤ h.

Code (Python 3)

from math import ceil
from typing import List

class Solution:
    def minEatingSpeed(self, piles: List[int], h: int) -> int:
        def can_finish(k: int) -> bool:
            return sum((p + k - 1) // k for p in piles) <= h

        lo, hi = 1, max(piles)
        while lo < hi:
            mid = (lo + hi) // 2
            if can_finish(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Complexity

Comments


25.2 Capacity To Ship Packages Within D Days (LC 1011)

Problem

Given weights[] (in order) and days. Find the smallest capacity of a ship that can transport everything within days days (one trip per day; the ship must carry packages contiguously in order).

Example

Input:  weights = [1,2,3,4,5,6,7,8,9,10], days = 5
Output: 15

Constraints

Clarifying questions

Approach

Answer cap[max(weights), sum(weights)]. (cap < max → can’t load the heaviest package; cap = sum → one trip is enough.)

Predicate: “with this cap, can we ship within ≤ days days?” → greedy day counter.

Code (Python 3)

from typing import List

class Solution:
    def shipWithinDays(self, weights: List[int], days: int) -> int:
        def can_ship(cap: int) -> bool:
            d = 1
            cur = 0
            for w in weights:
                if cur + w > cap:
                    d += 1
                    cur = 0
                cur += w
            return d <= days

        lo, hi = max(weights), sum(weights)
        while lo < hi:
            mid = (lo + hi) // 2
            if can_ship(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Complexity

Comments


25.3 Split Array Largest Sum (LC 410)

Problem

Given nums (non-negative) and k, split into k non-empty contiguous subarrays. Find a split that minimises the maximum subarray sum.

Example

Input:  nums = [7,2,5,10,8], k = 2
Output: 18    (split [7,2,5] and [10,8])

Constraints

Clarifying questions

Approach

Identical to 25.2! Answer = max sum. check(cap) = “can we split into ≤ k subarrays with each sum ≤ cap?”.

Code (Python 3)

from typing import List

class Solution:
    def splitArray(self, nums: List[int], k: int) -> int:
        def can_split(cap: int) -> bool:
            groups = 1
            cur = 0
            for x in nums:
                if cur + x > cap:
                    groups += 1
                    cur = 0
                cur += x
            return groups <= k

        lo, hi = max(nums), sum(nums)
        while lo < hi:
            mid = (lo + hi) // 2
            if can_split(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Complexity

Comments


25.4 Find K-th Smallest Pair Distance (LC 719)

Problem

Given nums, compute all pairwise distances |nums[i] - nums[j]| with i < j. Return the k-th smallest distance.

Example

Input:  nums=[1,3,1], k=1
Output: 0

Constraints

Clarifying questions

Approach

Brute force: generate all C(n, 2) distances, sort, take k-th. O(n² log n²). TLE with n = 10^4.

Search on answer + Sliding-window count: - Sort nums. Answer ∈ [0, max - min]. - check(d) = “are there ≥ k pairs with distance ≤ d?” - Count pairs via sliding window: for each right, advance left until nums[right] - nums[left] <= d. The pair count contributed = right - left.

Code (Python 3)

from typing import List

class Solution:
    def smallestDistancePair(self, nums: List[int], k: int) -> int:
        nums.sort()

        def count_le(d: int) -> int:
            cnt = 0
            left = 0
            for right in range(len(nums)):
                while nums[right] - nums[left] > d:
                    left += 1
                cnt += right - left
            return cnt

        lo, hi = 0, nums[-1] - nums[0]
        while lo < hi:
            mid = (lo + hi) // 2
            if count_le(mid) >= k:
                hi = mid
            else:
                lo = mid + 1
        return lo

Complexity

Comments


25.5 Median of Two Sorted Arrays (LC 4)

Problem

Given two sorted arrays nums1, nums2, find the median of their union. O(log(m+n)).

Example

Input:  nums1=[1,3], nums2=[2]
Output: 2.0
Explanation: median of [1,2,3] = 2

Constraints

Clarifying questions

Approach

Binary search on the partition. Find i (split nums1 at index i) and j = (m + n + 1) // 2 - i (split nums2) such that: - nums1[i-1] <= nums2[j] and nums2[j-1] <= nums1[i].

When found, median = max(left) (if total length odd) or (max(left) + min(right)) / 2 (even).

Always binary-search on the shorter array to keep it O(log min(m, n)).

Code (Python 3)

from typing import List

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        # Ensure nums1 is shorter.
        if len(nums1) > len(nums2):
            nums1, nums2 = nums2, nums1
        m, n = len(nums1), len(nums2)
        half = (m + n + 1) // 2
        INF = float('inf')

        lo, hi = 0, m
        while lo <= hi:
            i = (lo + hi) // 2
            j = half - i
            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 and left2 <= right1:
                if (m + n) % 2:
                    return max(left1, left2)
                return (max(left1, left2) + min(right1, right2)) / 2
            elif left1 > right2:
                hi = i - 1
            else:
                lo = i + 1
        return 0.0

Complexity

Comments


25.6 Aggressive Cows (classic)

Problem

Given n stalls at positions pos[i] (sorted) and c cows. Place each cow in a stall so the minimum distance between any two cows is maximised. Return that distance.

Example

Input:  pos = [1, 2, 4, 8, 9], c = 3
Output: 3
Explanation: place at 1, 4, 8 → min distance = 3.

Constraints

Clarifying questions

Approach

Answer d[0, max - min]. Predicate: “can we place c cows with all pairwise distances ≥ d?” → greedy: place the first cow at the leftmost position, then each subsequent at the first position ≥ previous + d.

Code (Python 3)

from typing import List

class Solution:
    def aggressiveCows(self, pos: List[int], c: int) -> int:
        pos.sort()

        def can_place(d: int) -> bool:
            placed = 1
            last = pos[0]
            for p in pos[1:]:
                if p - last >= d:
                    placed += 1
                    last = p
                    if placed >= c:
                        return True
            return False

        lo, hi = 0, pos[-1] - pos[0]
        while lo < hi:
            mid = (lo + hi + 1) // 2     # "last True" search → ceil mid
            if can_place(mid):
                lo = mid
            else:
                hi = mid - 1
        return lo

Complexity

Comments

Chapter summary & decisions

Search on Answer — universal table

Problem Answer to find lo, hi Predicate can(x) Goal
Koko (LC 875) speed k 1, max(piles) total hours ≤ H min x with can(x)
Ship (LC 1011) capacity max(w), sum(w) ship in ≤ D days min
Split Array (LC 410) largest subarray sum max(arr), sum(arr) split into ≤ m parts min
Aggressive Cows distance d 1, max - min place ≥ C cows max x with can(x)
Magnetic Force (LC 1552) force 1, max - min place ≥ m max

General template (first-true / last-true):

# first-true monotonic: F F F T T T → return first T
while lo < hi:
    mid = (lo + hi) // 2
    if can(mid): hi = mid
    else: lo = mid + 1
return lo

Predicate monotonicity — proof

Verify can(x) is monotonic in x: - Koko: bigger k → eat faster → smaller total hourscan is monotonically increasing. - Aggressive Cows: bigger d → harder to place → fewer cows fit ⇒ can is monotonically decreasing.

Median of Two Sorted Arrays — NOT search on answer

This is binary partition: find i, j such that A[..i] ∪ B[..j] forms the smaller half. A different pattern — don’t force it into the general template.

Aggressive Cows — feasibility

positions sorted: [1, 2, 4, 8, 9]    cows c = 3, d = ?
d = 3 ⇒ pick 1, 4, 8 ✅ (3 cows)
d = 4 ⇒ pick 1, 8     (only 2 cows) ❌
⇒ max d allowing ≥ 3 cows = 3

Chapter 26 — Two Pointers (Fast & Slow Pointer)

Two pointers is the basic weapon already encountered in Array (1.4, 1.5), String (2.2), Linked List (7.3-7.6). This chapter systematises it into 3 templates: (i) two-end (converging), (ii) slow & fast (1×/2×), (iii) same-direction (sliding window). Each template solves a family of problems.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

The three most useful templates:

Template Example problems Characteristic
Two-end 3Sum, Container Most Water, Trapping sort first, converge from sides
Slow & Fast Cycle, Middle of LL, Move Zeroes different speeds
Same direction Sliding window, Remove Dup window grows / shrinks

Template code

# 1) Two-end (sorted)
l, r = 0, len(arr) - 1
while l < r:
    if check(arr[l], arr[r]):
        # ... use the pair ...
        l += 1
        r -= 1
    elif arr[l] + arr[r] < target:
        l += 1
    else:
        r -= 1


# 2) Slow & Fast (in-place)
slow = 0
for fast in range(len(arr)):
    if condition(arr[fast]):
        arr[slow] = arr[fast]
        slow += 1

End-of-chapter practice


26.1 3Sum (LC 15)

Problem

Given nums, find all triplets [a, b, c] with a + b + c == 0 and no duplicates.

Example

Input:  nums = [-1, 0, 1, 2, -1, -4]
Output: [[-1, -1, 2], [-1, 0, 1]]

Constraints

Clarifying questions

Approach

Brute force O(n³). Every triplet.

Sort + two pointers O(n²).

Sort. For each i, use two pointers l, r to find a pair summing to -nums[i] in nums[i+1..n-1].

Handle duplicates: skip duplicates at i, l, r.

Illustration for nums = [-1, 0, 1, 2, -1, -4] sorted → [-4, -1, -1, 0, 1, 2]:

i=0 (-4): target=4. l=1 (-1), r=5 (2). -1+2=1 < 4. l++. ... nothing.
i=1 (-1): target=1. l=2 (-1), r=5 (2). -1+2=1 ✓ → add [-1,-1,2].
                    l++, r--. l=3 (0), r=4 (1). 0+1=1 ✓ → add [-1,0,1].
i=2 (-1): skip (duplicate of nums[1]).
i=3 (0): target=0. l=4 (1), r=5 (2). 1+2=3 > 0. r--. → l=r → stop.

Code (Python 3)

from typing import List

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        result: list[list[int]] = []
        n = len(nums)
        for i in range(n - 2):
            if nums[i] > 0:
                break
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            l, r = i + 1, n - 1
            target = -nums[i]
            while l < r:
                s = nums[l] + nums[r]
                if s == target:
                    result.append([nums[i], nums[l], nums[r]])
                    l += 1; r -= 1
                    while l < r and nums[l] == nums[l - 1]: l += 1
                    while l < r and nums[r] == nums[r + 1]: r -= 1
                elif s < target:
                    l += 1
                else:
                    r -= 1
        return result

Complexity

Comments


26.2 Trapping Rain Water (LC 42)

Problem

Given an array height[]. Each bar has width 1. Compute the water trapped between bars.

Example

Input:  height = [0,1,0,2,1,0,1,3,2,1,2,1]
        (each element is the height of one width-1 bar)
Output: 6   (total water trapped between bars)

Constraints

Clarifying questions

Approach

Approach 1 — Prefix max + Suffix max, O(n) time, O(n) space.

Cell i holds: min(max_left[i], max_right[i]) - height[i].

Approach 2 — Two pointers, O(n) time, O(1) space.

l = 0, r = n - 1, keep lmax, rmax. Move the pointer on the lower side — that side determines the water at the cell being considered.

Code (Python 3)

from typing import List

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

Complexity

Comments


26.3 Container With Most Water (LC 11) — recap

Fully solved in Chapter 1.5. This is the core two-pointer pattern.

Code (Python 3) (recap)

from typing import List

class Solution:
    def maxArea(self, height: List[int]) -> int:
        l, r = 0, len(height) - 1
        best = 0
        while l < r:
            h = min(height[l], height[r])
            best = max(best, h * (r - l))
            if height[l] < height[r]:
                l += 1
            else:
                r -= 1
        return best

Complexity


26.4 Sort Colors (LC 75) — recap

Fully solved in Chapter 4.1. Three pointers (Dutch flag).

Code (Python 3) (recap)

from typing import List

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        lo, mid, hi = 0, 0, len(nums) - 1
        while mid <= hi:
            if nums[mid] == 0:
                nums[lo], nums[mid] = nums[mid], nums[lo]
                lo += 1; mid += 1
            elif nums[mid] == 1:
                mid += 1
            else:
                nums[mid], nums[hi] = nums[hi], nums[mid]
                hi -= 1

Complexity


26.5 Remove Duplicates from Sorted Array II (LC 80)

Problem

Given a sorted array nums, remove duplicates so each element appears at most twice; return the new length. In place.

Example

Input:  nums = [1,1,1,2,2,3]
Output: 5, nums = [1,1,2,2,3,_]

Constraints

Clarifying questions

Approach

Slow & fast two pointers. Sorted → just check nums[fast] != nums[slow - 2].

Code (Python 3)

from typing import List

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        slow = 0
        for x in nums:
            if slow < 2 or x != nums[slow - 2]:
                nums[slow] = x
                slow += 1
        return slow

Complexity

Comments


26.6 4Sum (LC 18)

Problem

Given nums and target, find all quadruplets [a, b, c, d] summing to target, no duplicates.

Example

Input:  nums=[1,0,-1,0,-2,2], target=0
Output: [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

Constraints

Clarifying questions

Approach

Generalisation of 3Sum. Sort + 2 outer loops (i, j) + two pointers for (l, r). O(n³).

Skip duplicates at all four indices i, j, l, r.

Code (Python 3)

from typing import List

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()
        n = len(nums)
        result: list[list[int]] = []
        for i in range(n - 3):
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            for j in range(i + 1, n - 2):
                if j > i + 1 and nums[j] == nums[j - 1]:
                    continue
                l, r = j + 1, n - 1
                tgt = target - nums[i] - nums[j]
                while l < r:
                    s = nums[l] + nums[r]
                    if s == tgt:
                        result.append([nums[i], nums[j], nums[l], nums[r]])
                        l += 1; r -= 1
                        while l < r and nums[l] == nums[l - 1]: l += 1
                        while l < r and nums[r] == nums[r + 1]: r -= 1
                    elif s < tgt:
                        l += 1
                    else:
                        r -= 1
        return result

Complexity

Comments

Chapter summary & decisions

Two-pointer family — which shape?

Shape When to use Representative problems
Two-end (l=0, r=n-1, converge inward) Sorted, palindrome, container LC 11, 15, 167, 125
Same-direction (l, r both forward) Subarray with invariant (sliding window is a special case) LC 26, 283; Chapter 27
Slow-fast (slow step 1, fast step 2) Cycle detect, middle node LC 141, 142, 876

3Sum / 4Sum — duplicate-skip checklist

nums.sort()
for i in range(n):
    if i > 0 and nums[i] == nums[i-1]: continue          # skip anchor dup
    l, r = i+1, n-1
    while l < r:
        s = nums[i] + nums[l] + nums[r]
        if s == 0:
            ans.append([nums[i], nums[l], nums[r]])
            l += 1; r -= 1
            while l < r and nums[l] == nums[l-1]: l += 1  # skip inner left dup
            while l < r and nums[r] == nums[r+1]: r -= 1  # skip inner right dup
        elif s < 0: l += 1
        else: r -= 1

Four skip points — forgetting any one produces duplicates.

Trapping Rain Water (LC 42) — two approaches

Prefix/suffix max array Two pointers
Time O(n) O(n)
Space O(n) O(1)
Code length Medium Shorter
Insight Visual: water at i = min(L[i], R[i]) - h[i] Must argue “the lower bar decides”

Recommendation: present prefix/suffix first (easier to explain), then optimise to two-pointer if asked about space.

Container / Sort Colors recap lens


Chapter 27 — Sliding Window

Sliding Window = two pointers moving in the same direction. The window [l, r] extends r, shrinks l when the condition is violated. This pattern crisply solves many “longest / shortest / count substring/subarray with condition” problems in O(n).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Two main templates:

Template Pseudo
Fixed-size window maintain r - l + 1 == k always
Variable-size window always extend r, shrink l until valid()

Template code

from collections import defaultdict, Counter

# 1) Variable-size: longest with condition
l = 0
state = ...
best = 0
for r in range(len(s)):
    add(s[r], state)
    while not valid(state):
        remove(s[l], state)
        l += 1
    best = max(best, r - l + 1)


# 2) Count subarray "exactly K" = "at most K" - "at most K-1"
def at_most(k):
    l, total = 0, 0
    state = ...
    for r in range(len(arr)):
        add(arr[r], state)
        while violates(state):
            remove(arr[l], state)
            l += 1
        total += r - l + 1
    return total

result = at_most(k) - at_most(k - 1)

End-of-chapter practice


27.1 Longest Substring Without Repeating Characters (LC 3)

Problem

Given a string s, find the length of the longest substring without repeating characters.

Example

Input:  s = "abcabcbb"      → Output: 3   (best substring: "abc")
Input:  s = "bbbbb"         → Output: 1   (substring "b")
Input:  s = "pwwkew"        → Output: 3   (substring "wke")

Constraints

Clarifying questions

Approach

Sliding window + set / dict.

Extend r. When s[r] is already in the window, shrink l until it isn’t.

Better — dict of last-seen indices: l = max(l, last_seen[s[r]] + 1).

Code (Python 3)

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        last: dict[str, int] = {}
        l = 0
        best = 0
        for r, ch in enumerate(s):
            if ch in last and last[ch] >= l:
                l = last[ch] + 1
            last[ch] = r
            best = max(best, r - l + 1)
        return best

Complexity

Comments


27.2 Minimum Window Substring (LC 76)

Problem

Given s, t, find the smallest substring of s containing every character of t (including frequency).

Example

Input:  s = "ADOBECODEBANC", t = "ABC"   → "BANC"

Constraints

Clarifying questions

Approach

Sliding window + 2 Counters.

Optimisation: track formed (number of satisfied keys) instead of comparing whole dicts.

Code (Python 3)

from collections import Counter

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if not t or len(t) > len(s):
            return ""
        need = Counter(t)
        have: dict[str, int] = {}
        required = len(need)
        formed = 0
        l = 0
        best = (float('inf'), 0, 0)
        for r, ch in enumerate(s):
            have[ch] = have.get(ch, 0) + 1
            if ch in need and have[ch] == need[ch]:
                formed += 1
            while formed == required:
                if r - l + 1 < best[0]:
                    best = (r - l + 1, l, r)
                have[s[l]] -= 1
                if s[l] in need and have[s[l]] < need[s[l]]:
                    formed -= 1
                l += 1
        return "" if best[0] == float('inf') else s[best[1]:best[2] + 1]

Complexity

Comments


27.3 Longest Repeating Character Replacement (LC 424)

Problem

Given s and k, you may replace at most k characters with any others. Find the length of the longest substring of identical characters achievable.

Example

Input:  s = "ABAB", k = 2   → 4
Input:  s = "AABABBA", k = 1 → 4 ("AABA" or "ABBA")

Constraints

Clarifying questions

Approach

Insight: the window is valid ↔︎ window_len - max_freq <= k (characters to flip ≤ k).

Extend r, update max_freq. On violation → shrink l by 1.

Trick: max_freq does not need to “decrease” when shrinking l — the answer doesn’t grow when max_freq stays at its previous peak (the window can’t be larger than that peak’s run).

Code (Python 3)

from collections import defaultdict

class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        count: dict[str, int] = defaultdict(int)
        l = 0
        max_freq = 0
        best = 0
        for r, ch in enumerate(s):
            count[ch] += 1
            max_freq = max(max_freq, count[ch])
            while (r - l + 1) - max_freq > k:
                count[s[l]] -= 1
                l += 1
            best = max(best, r - l + 1)
        return best

Complexity

Comments


27.4 Permutation in String (LC 567)

Problem

Given s1, s2, check whether s2 contains any permutation of s1 (= some substring of s2 has Counter == Counter(s1)).

Example

Input:  s1="ab", s2="eidbaooo"
Output: True
Explanation: s2 contains "ba" — a permutation of "ab"

Constraints

Clarifying questions

Approach

Fixed-size sliding window of length len(s1). Compare two 26-element count arrays each step.

Code (Python 3)

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        if len(s1) > len(s2):
            return False
        need = [0] * 26
        have = [0] * 26
        for ch in s1:
            need[ord(ch) - 97] += 1
        for i, ch in enumerate(s2):
            have[ord(ch) - 97] += 1
            if i >= len(s1):
                have[ord(s2[i - len(s1)]) - 97] -= 1
            if have == need:
                return True
        return False

Complexity

Comments


27.5 Subarrays with K Different Integers (LC 992)

Problem

Given nums and k, count subarrays with exactly k distinct integers.

Example

Input:  nums = [1,2,1,2,3], k = 2   → 7
Input:  nums = [1,2,1,3,4], k = 3   → 3

Constraints

Clarifying questions

Approach

Trick: “exactly K” = “at most K” - “at most K - 1”.

at_most(k): sliding-window count of subarrays with ≤ k distinct integers.

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:

        def at_most(k: int) -> int:
            count: dict[int, int] = defaultdict(int)
            distinct = 0
            l = 0
            total = 0
            for r in range(len(nums)):
                if count[nums[r]] == 0:
                    distinct += 1
                count[nums[r]] += 1
                while distinct > k:
                    count[nums[l]] -= 1
                    if count[nums[l]] == 0:
                        distinct -= 1
                    l += 1
                total += r - l + 1
            return total

        return at_most(k) - at_most(k - 1)

Complexity

Comments


27.6 Sliding Window Maximum (LC 239) — recap

Fully solved in Chapter 18.4 (Monotonic Queue).

Code (Python 3)

from collections import deque
from typing import List


class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        dq: deque[int] = deque()  # stores **indices**, values in dq decreasing
        result: List[int] = []
        for i, x in enumerate(nums):
            while dq and nums[dq[-1]] <= x:
                dq.pop()
            dq.append(i)
            if dq[0] <= i - k:
                dq.popleft()
            if i >= k - 1:
                result.append(nums[dq[0]])
        return result

Connection

Complexity

Chapter summary & decisions

Sliding-window applicability

Minimum Window Substring (LC 76) — need / have trace

S = "ADOBECODEBANC", T = "ABC"
need = {A:1, B:1, C:1}; need_unique = 3
have_unique = characters fully matched in the window.

extend window until have_unique == need_unique
→ shrink from the left until losing a required char → update min.

Classic bug: confusing “fully matched” with “raw count exceeded”. Only increment have_unique when cnt[c] == need[c] (just matched), and decrement when cnt[c] < need[c] (just dropped below).

Exactly K = atMost(K) - atMost(K-1)

Fixed vs Variable vs atMost

Type Pattern Problems
Fixed k Window size k, slide one step LC 643, 567
Variable shrink on violation Extend r, shrink l until valid LC 3, 76, 209
atMost K Extend r, shrink l until < K features LC 992, 1248

Chapter 28 — Backtracking

Backtracking = DFS combined with undoing state on the way back up. This is the pattern to enumerate every valid configuration (permutations, subsets, combinations, Sudoku, N-Queens). The secret to efficient backtracking is pruning — cut branches early once you know they can’t lead to a solution.

12 problems — full template plus 12 classics from Medium to Hard.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Three components of backtracking: 1. Choice: what options exist at each step? 2. Constraint: which choices are valid? 3. Goal: when to stop and record the answer?

Template code

def backtrack(path, choices):
    if is_goal(path):
        result.append(path.copy())
        return
    for c in choices:
        if not valid(c, path):
            continue
        path.append(c)
        backtrack(path, next_choices(choices, c))
        path.pop()              # UNDO — the backtracking signature

End-of-chapter practice


28.1 Combinations (LC 77)

Problem

Given n and k, return all combinations of k numbers chosen from 1..n.

Example

Input:  n = 4, k = 2
Output: [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
        (output order of combinations may vary)

Constraints

Clarifying questions

Approach

Backtracking with start: at each step pick a number ≥ start to avoid duplicates.

def backtrack(start):
    if len(path) == k:
        result.append(path.copy()); return
    for i in range(start, n + 1):
        path.append(i)
        backtrack(i + 1)
        path.pop()

Code (Python 3)

from typing import List

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(start: int) -> None:
            if len(path) == k:
                result.append(path.copy())
                return
            # Pruning: need (k - len(path)) more, max start = n - (k - len(path)) + 1.
            for i in range(start, n - (k - len(path)) + 2):
                path.append(i)
                backtrack(i + 1)
                path.pop()

        backtrack(1)
        return result

Complexity

Comments


28.2 Subsets II (LC 90)

Problem

Given nums (may contain duplicates), return all unique subsets.

Example

Input:  nums = [1, 2, 2]   (has duplicates)
Output: [[], [1], [1,2], [1,2,2], [2], [2,2]]
        (every unique subset; the output array order is flexible)

Constraints

Clarifying questions

Approach

Sort nums. At each level, skip duplicates with if i > start and nums[i] == nums[i-1]: continue.

Code (Python 3)

from typing import List

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(start: int) -> None:
            result.append(path.copy())
            for i in range(start, len(nums)):
                if i > start and nums[i] == nums[i - 1]:
                    continue
                path.append(nums[i])
                backtrack(i + 1)
                path.pop()

        backtrack(0)
        return result

Complexity

Comments


28.3 Permutations II (LC 47)

Problem

Given nums with duplicates, return all distinct permutations.

Example

Input:  nums = [1, 1, 2]   (has duplicates)
Output: [[1,1,2], [1,2,1], [2,1,1]]
        (every unique permutation)

Constraints

Clarifying questions

Approach

Sort + skip duplicates. A used array. Trick: if nums[i] == nums[i-1] and nums[i-1] is unused → skip (forces consumption in index order).

Code (Python 3)

from typing import List

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        result: list[list[int]] = []
        path: list[int] = []
        used = [False] * len(nums)

        def backtrack() -> None:
            if len(path) == len(nums):
                result.append(path.copy())
                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
                used[i] = True
                path.append(nums[i])
                backtrack()
                path.pop()
                used[i] = False

        backtrack()
        return result

Complexity

Comments


28.4 Letter Combinations of a Phone Number (LC 17)

Problem

Given a digit string 2..9, return all letter combinations from the classic phone keypad.

Example

Input:  digits = "23"
Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
        ('2' → "abc"; '3' → "def"; order may vary)

Input:  digits = ""
Output: []

Constraints

Clarifying questions

Approach

Mapping {'2':'abc', ..., '9':'wxyz'}. Backtrack picks one letter per digit.

Code (Python 3)

from typing import List

class Solution:
    MAP = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'}

    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:
            return []
        result: list[str] = []
        path: list[str] = []

        def backtrack(i: int) -> None:
            if i == len(digits):
                result.append(''.join(path))
                return
            for ch in self.MAP[digits[i]]:
                path.append(ch)
                backtrack(i + 1)
                path.pop()

        backtrack(0)
        return result

Complexity

Comments


28.5 Combination Sum (LC 39)

Problem

Given candidates (distinct) and target, find every combination summing to target. Each number can be reused.

Example

Input:  candidates=[2,3,6,7], target=7
Output: [[2,2,3],[7]]

Constraints

Clarifying questions

Approach

Backtrack with start. Allow reuse → recurse backtrack(i) instead of i+1.

Code (Python 3)

from typing import List

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        candidates.sort()
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(start: int, remain: int) -> None:
            if remain == 0:
                result.append(path.copy())
                return
            for i in range(start, len(candidates)):
                if candidates[i] > remain:
                    break               # sorted → everything after is larger
                path.append(candidates[i])
                backtrack(i, remain - candidates[i])
                path.pop()

        backtrack(0, target)
        return result

Complexity

Comments


28.6 Palindrome Partitioning (LC 131)

Problem

Given s, return every partition of s such that each piece is a palindrome.

Example

Input:  s = "aab"
Output: [["a","a","b"], ["aa","b"]]
        (every partition where each piece is a palindrome; order may vary)

Constraints

Clarifying questions

Approach

Backtrack: at each start, try every end such that s[start..end] is a palindrome; recurse with start = end + 1.

Code (Python 3)

from typing import List

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        result: list[list[str]] = []
        path: list[str] = []

        def is_pal(l: int, r: int) -> bool:
            while l < r:
                if s[l] != s[r]:
                    return False
                l += 1; r -= 1
            return True

        def backtrack(start: int) -> None:
            if start == len(s):
                result.append(path.copy())
                return
            for end in range(start, len(s)):
                if is_pal(start, end):
                    path.append(s[start:end + 1])
                    backtrack(end + 1)
                    path.pop()

        backtrack(0)
        return result

Complexity

Comments


28.7 N-Queens (LC 51)

Problem

Place n queens on an n × n board so that no two attack each other. Return all configurations (each as a list of strings).

Example

Input:  n=4
Output: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]

Constraints

Clarifying questions

Approach

Backtrack row by row. At row r, try each column c. Ensure c is unused, diagonal (r - c) unused, diagonal (r + c) unused.

Code (Python 3)

from typing import List

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        cols = set()
        diag1 = set()       # r - c
        diag2 = set()       # r + c
        board = [['.'] * n for _ in range(n)]
        result: List[List[str]] = []

        def backtrack(r: int) -> None:
            if r == n:
                result.append([''.join(row) for row in board])
                return
            for c in range(n):
                if c in cols or (r - c) in diag1 or (r + c) in diag2:
                    continue
                cols.add(c); diag1.add(r - c); diag2.add(r + c)
                board[r][c] = 'Q'
                backtrack(r + 1)
                board[r][c] = '.'
                cols.discard(c); diag1.discard(r - c); diag2.discard(r + c)

        backtrack(0)
        return result

Complexity

Comments


28.8 Sudoku Solver (LC 37)

Problem

Solve a 9×9 Sudoku in place. Empty cells are '.'.

Example

Input:  board = (9×9 matrix, '.' for empty, '1'..'9' for filled)
  [['5','3','.','.','7','.','.','.','.'],
   ['6','.','.','1','9','5','.','.','.'],
   ['.','9','8','.','.','.','.','6','.'],
   ['8','.','.','.','6','.','.','.','3'],
   ['4','.','.','8','.','3','.','.','1'],
   ['7','.','.','.','2','.','.','.','6'],
   ['.','6','.','.','.','.','2','8','.'],
   ['.','.','.','4','1','9','.','.','5'],
   ['.','.','.','.','8','.','.','7','9']]

Output: board mutated in place to a valid solution
  [['5','3','4','6','7','8','9','1','2'],
   ['6','7','2','1','9','5','3','4','8'],
   ['1','9','8','3','4','2','5','6','7'],
   ['8','5','9','7','6','1','4','2','3'],
   ['4','2','6','8','5','3','7','9','1'],
   ['7','1','3','9','2','4','8','5','6'],
   ['9','6','1','5','3','7','2','8','4'],
   ['2','8','7','4','1','9','6','3','5'],
   ['3','4','5','2','8','6','1','7','9']]

(Function returns nothing; only mutates `board`. Every row, column,
 and 3×3 sub-grid contains '1'..'9' exactly once.)

Constraints

Clarifying questions

Approach

Backtrack every empty cell. Track three sets: row, col, 3×3 box.

Code (Python 3)

from typing import List

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        rows = [set() for _ in range(9)]
        cols = [set() for _ in range(9)]
        boxes = [set() for _ in range(9)]
        empty: list[tuple[int, int]] = []
        for r in range(9):
            for c in range(9):
                if board[r][c] != '.':
                    d = board[r][c]
                    rows[r].add(d); cols[c].add(d); boxes[(r//3)*3 + c//3].add(d)
                else:
                    empty.append((r, c))

        def backtrack(i: int) -> bool:
            if i == len(empty):
                return True
            r, c = empty[i]
            b = (r // 3) * 3 + c // 3
            for d in '123456789':
                if d in rows[r] or d in cols[c] or d in boxes[b]:
                    continue
                rows[r].add(d); cols[c].add(d); boxes[b].add(d)
                board[r][c] = d
                if backtrack(i + 1):
                    return True
                rows[r].discard(d); cols[c].discard(d); boxes[b].discard(d)
                board[r][c] = '.'
            return False

        backtrack(0)

Complexity

Comments


28.9 Word Search (LC 79)

Problem

Given a board and word, check whether word can be built from 4-directional paths without revisiting a cell.

Example

Input:  board=[["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word="ABCCED"
Output: True

Constraints

Clarifying questions

Approach

DFS from every cell with board[r][c] == word[0]. Backtrack: mark # then restore.

Code (Python 3)

from typing import List

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        rows, cols = len(board), len(board[0])
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]

        def dfs(r: int, c: int, i: int) -> bool:
            if i == len(word):
                return True
            if not (0 <= r < rows and 0 <= c < cols) or board[r][c] != word[i]:
                return False
            board[r][c] = '#'
            found = any(dfs(r+dr, c+dc, i+1) for dr, dc in DIRS)
            board[r][c] = word[i]
            return found

        return any(dfs(r, c, 0) for r in range(rows) for c in range(cols))

Complexity

Comments


28.10 Word Break II (LC 140)

Problem

Given s and wordDict, return every way to split s into dictionary words (separated by spaces).

Example

Input:  s="catsanddog", wordDict=["cat","cats","and","sand","dog"]
Output: ["cats and dog","cat sand dog"]

Constraints

Clarifying questions

Approach

Backtrack with memoization (cache by start).

Code (Python 3)

from functools import cache
from typing import List

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
        words = set(wordDict)

        @cache
        def helper(start: int) -> List[str]:
            if start == len(s):
                return [""]
            result = []
            for end in range(start + 1, len(s) + 1):
                if s[start:end] in words:
                    for rest in helper(end):
                        result.append(s[start:end] + ("" if not rest else " " + rest))
            return result

        return helper(0)

Complexity

Comments


28.11 Restore IP Addresses (LC 93)

Problem

Given a digit string, list every valid IP (4 numbers 0..255; no leading zeros unless the number itself is “0”).

Example

Input:  s = "25525511135"   (digits only)
Output: ["255.255.11.135", "255.255.111.35"]
        (every valid IPv4 formed by inserting 3 dots; no leading zeros)

Constraints

Clarifying questions

Approach

Backtrack splitting into 4 parts. Each part has length 1, 2, or 3 and satisfies the constraints.

Code (Python 3)

from typing import List

class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        result: list[str] = []
        path: list[str] = []

        def is_valid(p: str) -> bool:
            if len(p) > 3 or not p:
                return False
            if p[0] == '0' and len(p) > 1:
                return False
            return int(p) <= 255

        def backtrack(start: int) -> None:
            if len(path) == 4:
                if start == len(s):
                    result.append('.'.join(path))
                return
            for length in (1, 2, 3):
                if start + length > len(s):
                    break
                p = s[start:start + length]
                if is_valid(p):
                    path.append(p)
                    backtrack(start + length)
                    path.pop()

        backtrack(0)
        return result

Complexity

Comments


28.12 Expression Add Operators (LC 282)

Problem

Given a digit string num and target, insert +, -, * between digits so the expression evaluates to target. Return every such expression.

Example

Input:  num="123", target=6
Output: ["1+2+3","1*2*3"]

Constraints

Clarifying questions

Approach

Backtrack with two auxiliary states: - cur = expression value so far. - prev = value of the last term (needed for * precedence).

On +: cur += x, prev = x. On -: cur -= x, prev = -x. On *: cur = cur - prev + prev * x, prev = prev * x.

Code (Python 3)

from typing import List

class Solution:
    def addOperators(self, num: str, target: int) -> List[str]:
        result: list[str] = []

        def backtrack(i: int, expr: str, cur: int, prev: int) -> None:
            if i == len(num):
                if cur == target:
                    result.append(expr)
                return
            for j in range(i + 1, len(num) + 1):
                if j > i + 1 and num[i] == '0':
                    break               # leading zero
                x = int(num[i:j])
                if i == 0:
                    backtrack(j, str(x), x, x)
                else:
                    backtrack(j, expr + '+' + str(x), cur + x, x)
                    backtrack(j, expr + '-' + str(x), cur - x, -x)
                    backtrack(j, expr + '*' + str(x), cur - prev + prev * x, prev * x)

        backtrack(0, "", 0, 0)
        return result

Complexity

Comments

Chapter summary & decisions

Backtracking schema

Component Questions to answer before coding
Path (current state) List / string / mask?
Choice list From n elements, indices, or digits 1..9?
Goal test When to record / return?
Pruning / constraints Can we cut early? Sort first to skip duplicates?
Undo Pop list, remove from set, restore mask?

Decision tree (Permutations of [1,2,3])

            []
       /   |    \
      1    2     3
    / |    | \   | \
   2  3    1  3  1  2
   |  |    |  |  |  |
   3  2    3  1  2  1

Each root→leaf path = one permutation. Backtrack = DFS on this imaginary tree.

Duplicate handling (Subsets II / Permutations II)

Subsets II (sort + same-level dup skip):

nums.sort()
for i in range(start, n):
    if i > start and nums[i] == nums[i-1]: continue   # same level, skip
    path.append(nums[i])
    dfs(i + 1)
    path.pop()

Permutations II (used array):

nums.sort()
for i in range(n):
    if used[i]: continue
    if i > 0 and nums[i] == nums[i-1] and not used[i-1]: continue  # preserve order

N-Queens / Sudoku constraint sets

Expression Add Operators (LC 282) — prev_operand

Because * has higher precedence than +/-, when multiplying we must retract prev then multiply:

cur_total - prev_operand + prev_operand * num

Chapter 29 — Dynamic Programming

Dynamic Programming (DP) = recursion + memoization (or bottom-up). Requires two properties: optimal substructure (the optimum of a big problem is built from the optima of its subproblems) + overlapping subproblems (subproblems repeat). The biggest chapter in the book — 18 problems split into 3 groups:

Chapter objectives

After this chapter, you will be able to:

Taxonomy of the 4 DP families in this chapter

The longest chapter in the book. To stay oriented, every problem fits one of four families:

Family Problems (LC) Common feature
DP I — Sequence DP (2D) 29.1 LCS, 29.2 LIS, 29.3 Edit Distance State = (i, j) over two prefixes
DP I — Knapsack 29.4 0/1 Knapsack, 29.5 Partition Equal Sum, 29.6 Russian Doll State = (i, capacity)
DP II — Linear DP 29.7 Coin Change, 29.8 Coin Change II, 29.9 Stock Cooldown, 29.10 Stock IV, 29.11 House Robber II, 29.12 Max Product State = dp[i] or dp[i][k]
DP III — Interval / Partition DP 29.13 Palindrome Partition II, 29.14 Burst Balloons, 29.15 MCM, 29.16 Cut Stick, 29.17 Stone Game VII, 29.18 Strange Printer State = dp[l][r], split at k

State/transition table per problem

When you meet a DP problem, the four core questions are: (1) What is the state? (2) What is the transition? (3) Base case? (4) Where is the answer?

Problem State Transition Base Answer
29.1 LCS dp[i][j] = LCS of s1[..i], s2[..j] Match: dp[i-1][j-1]+1; Else: max(dp[i-1][j], dp[i][j-1]) dp[0][j] = dp[i][0] = 0 dp[m][n]
29.2 LIS tails[k] = smallest tail of an LIS of length k+1 bisect_left + replace/append tails = [] len(tails)
29.3 Edit Distance dp[i][j] = min edits s1[..i] → s2[..j] Match: dp[i-1][j-1]; Else: 1 + min(3 ways) dp[i][0]=i, dp[0][j]=j dp[m][n]
29.4 0/1 Knapsack dp[w] = max value with capacity w dp[w] = max(dp[w], dp[w-wi]+vi) (iterate descending) dp[0..W] = 0 dp[W]
29.5 Partition Equal Sum dp[w] = is there a subset summing to w? dp[w] = dp[w] or dp[w-x] dp[0] = True dp[sum/2]
29.6 Russian Doll Sort 2D + LIS on h Same as LIS (29.2) len(tails)
29.7 Coin Change dp[a] = min coins summing to a dp[a] = min(dp[a-c]+1) dp[0] = 0 dp[amount]
29.8 Coin Change II dp[a] = number of ways dp[a] += dp[a-c] (outer = coin) dp[0] = 1 dp[amount]
29.9 Stock Cooldown hold[i], sold[i], rest[i] 3 transitions between states hold[0] = -prices[0] max(sold[-1], rest[-1])
29.10 Stock IV dp[t][i] = max profit ≤ t txns by day i max(dp[t][i-1], price[i]+max_diff) dp[0][i] = 0 dp[k][n-1]
29.11 House Robber II Split into 2 cases (rob 0 / not) Same as Robber I max(case_1, case_2)
29.12 Max Product rolling cur_max, cur_min Swap on x < 0 Start = nums[0] max(best)
29.13 Palindrome Partition II dp[i] = min cuts for s[..i] dp[i] = min(dp[j-1]+1) if s[j..i] palindrome dp[i]=0 if s[0..i] palindrome dp[n-1]
29.14 Burst Balloons dp[i][j] = max coins on (i, j) exclusive max(arr[i]*arr[k]*arr[j] + dp[i][k] + dp[k][j]) dp[i][i+1] = 0 dp[0][n-1]
29.15 MCM dp[i][j] = min multiplications Ai..Aj min(dp[i][k]+dp[k+1][j]+p[i-1]*p[k]*p[j]) dp[i][i]=0 dp[1][n]
29.16 Cut Stick dp[i][j] = min cost cuts in (cuts[i], cuts[j]) min(dp[i][k]+dp[k][j]) + cuts[j]-cuts[i] dp[i][i+1]=0 dp[0][m-1]
29.17 Stone Game VII dp[l][r] = max diff for the current player max(sum-stones[l] - dp[l+1][r], sum-stones[r] - dp[l][r-1]) dp[i][i]=0 dp[0][n-1]
29.18 Strange Printer dp[i][j] = min turns in s[i..j] min(dp[i][j-1]+1, dp[i][k] + dp[k+1][j-1] when s[k]==s[j]) dp[i][i]=1 dp[0][n-1]

Reading roadmap (18 problems)

If you have only one day, follow this priority:

  1. Foundations (must): 29.1 LCS, 29.2 LIS, 29.7 Coin Change — gateways to every other DP.
  2. Stock family (very common): 29.9 Stock Cooldown, 29.10 Stock
  3. Knapsack (super-popular LC tag): 29.4 0/1, 29.5 Partition.
  4. Interval DP (usually Hard): 29.14 Burst Balloons → 29.13 Palindrome Partition II.
  5. Rest: skip if time-pressed.

When to use this pattern?

3 steps to build a DP: 1. Define state: dp[i][j] = ? 2. Transition: dp[i][j] = f(dp[i-1][j], dp[i][j-1], ...) 3. Base case + iteration order: small → large.

Top-down (memo) vs Bottom-up (tabulation): - Top-down: write recursion + @cache. Easy to see the transition. - Bottom-up: iterate from small. Saves stack and is easier to space- optimise.

Template code

from functools import cache

# 1) Top-down
@cache
def dp(*state):
    if base_condition(*state):
        return base_value
    return combine([dp(*sub_state) for sub_state in transitions(*state)])


# 2) Bottom-up 2D
dp = [[0] * cols for _ in range(rows)]
for i in range(rows):
    for j in range(cols):
        if base(i, j):
            dp[i][j] = base_value
        else:
            dp[i][j] = f(dp[i-1][j], dp[i][j-1], ...)
return dp[-1][-1]


# 3) Space-optimised 1D (transition uses only the previous row)
prev = [0] * cols
for i in range(rows):
    cur = [0] * cols
    for j in range(cols):
        cur[j] = f(prev[j], cur[j-1], ...)
    prev = cur

End-of-chapter practice


DP I — Classic 2D DP (LCS, LIS, Knapsack)

29.1 Longest Common Subsequence (LC 1143)

Problem

Given strings s1, s2, find the length of the LCS (longest common subsequence).

Example

Input:  text1 = "abcde", text2 = "ace"
Output: 3
        (longest LCS is "ace")

Input:  text1 = "abc", text2 = "def"
Output: 0
        (no common characters)

Constraints

Clarifying questions

Approach

dp[i][j] = LCS of s1[0..i-1] and s2[0..j-1].

Code (Python 3)

class Solution:
    def longestCommonSubsequence(self, s1: str, s2: str) -> int:
        m, n = len(s1), len(s2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        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] + 1
                else:
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
        return dp[m][n]

Complexity

Comments


29.2 Longest Increasing Subsequence (LC 300) — O(n log n)

Problem

Find the length of the strictly-increasing LIS of nums.

Example

Input:  nums = [10, 9, 2, 5, 3, 7, 101, 18]
Output: 4
        (one longest LIS: [2, 3, 7, 18]; multiple exist)

Input:  nums = [0, 1, 0, 3, 2, 3]
Output: 4
        (one longest LIS: [0, 1, 2, 3])

Constraints

Clarifying questions

Approach

O(n²) DPdp[i] = max(dp[j]) + 1 for j < i, nums[j] < nums[i].

O(n log n)Patience Sort: - tails[i] = smallest tail value of an LIS of length i + 1. - Walk nums, use bisect_left to locate replace/extend index.

Code (Python 3)

from bisect import bisect_left
from typing import List

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        tails: list[int] = []
        for x in nums:
            i = bisect_left(tails, x)
            if i == len(tails):
                tails.append(x)
            else:
                tails[i] = x
        return len(tails)

Complexity

Comments


29.3 Edit Distance (LC 72)

Problem

Given word1, word2, return the minimum number of operations (insert, delete, replace) to convert word1 → word2.

Example

Input:  word1="horse", word2="ros"
Output: 3
Explanation: horse → rorse → rose → ros (3 ops)

Constraints

Clarifying questions

Approach

dp[i][j] = edit distance between the i-prefix of word1 and the j-prefix of word2.

Code (Python 3)

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
        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]
                else:
                    dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
        return dp[m][n]

Complexity

Comments


29.4 0/1 Knapsack (classic)

Problem

Given n items, each with weight[i] and value[i]. Bag capacity W. Choose items to maximise total value, each item used ≤ once.

Example

Input:  weights=[1,3,4,5], values=[1,4,5,7], W=7
Output: 9

Constraints

Clarifying questions

Approach

dp[i][w] = max value using the first i items with capacity w.

dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i-1]] + value[i-1]) (if it fits).

Code (Python 3)

from typing import List

def knapsack_01(weights: List[int], values: List[int], W: int) -> int:
    n = len(weights)
    dp = [0] * (W + 1)
    for i in range(n):
        # Iterate descending so each item is used ≤ once.
        for w in range(W, weights[i] - 1, -1):
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    return dp[W]

Complexity

Comments


29.5 Partition Equal Subset Sum (LC 416)

Problem

Given positive nums, can it be split into two subsets with equal sums?

Example

Input:  nums = [1, 5, 11, 5]
Output: True
(splits into [1,5,5] and [11], each sum 11)

Constraints

Clarifying questions

Approach

Let S = sum(nums). If S is odd → False. Find a subset summing to S / 2boolean knapsack DP.

dp[w] = True/False (is there a subset summing to w?).

Code (Python 3)

from typing import List

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total = sum(nums)
        if total % 2: return False
        target = total // 2
        dp = [False] * (target + 1)
        dp[0] = True
        for x in nums:
            for w in range(target, x - 1, -1):
                dp[w] = dp[w] or dp[w - x]
        return dp[target]

Complexity

Comments


29.6 Russian Doll Envelopes (LC 354)

Problem

Given envelopes[i] = [w, h]. Envelope A fits inside B if A.w < B.w and A.h < B.h. Find the maximum chain length.

Example

Input:  envelopes = [[5,4], [6,4], [6,7], [2,3]]
        (each entry [width, height])
Output: 3   (chain [2,3] ⊂ [5,4] ⊂ [6,7])

Constraints

Clarifying questions

Approach

“2D sort” trick: sort by w ascending, h descending for equal w (so two envelopes with the same w don’t nest). Then run LIS on the h sequence.

Code (Python 3)

from bisect import bisect_left
from typing import List

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        envelopes.sort(key=lambda e: (e[0], -e[1]))
        tails: list[int] = []
        for _, h in envelopes:
            i = bisect_left(tails, h)
            if i == len(tails):
                tails.append(h)
            else:
                tails[i] = h
        return len(tails)

Complexity

Comments


DP II — Coin Change & Stock Trading

29.7 Coin Change (LC 322)

Problem

Given coin denominations and amount, find the minimum number of coins summing to amount. Return -1 if impossible.

Example

Input:  coins=[1,2,5], amount=11
Output: 3
Explanation: 11 = 5+5+1

Constraints

Clarifying questions

Approach

dp[a] = min coins summing to a. dp[0] = 0. dp[a] = min(dp[a - c] + 1 for c in coins if c <= a).

Code (Python 3)

from typing import List

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        INF = amount + 1
        dp = [0] + [INF] * amount
        for a in range(1, amount + 1):
            for c in coins:
                if c <= a:
                    dp[a] = min(dp[a], dp[a - c] + 1)
        return -1 if dp[amount] > amount else dp[amount]

Complexity

Comments


29.8 Coin Change II (LC 518)

Problem

Count the number of ways to sum amount using unbounded coins.

Example

Input:  amount=5, coins=[1,2,5]
Output: 4

Constraints

Clarifying questions

Approach

dp[a] = number of ways to sum a.

Loop order matters: outer loop coins, inner loop amount → counts each combination once (no over-count).

Code (Python 3)

from typing import List

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0] * (amount + 1)
        dp[0] = 1
        for c in coins:
            for a in range(c, amount + 1):
                dp[a] += dp[a - c]
        return dp[amount]

Complexity

Comments


29.9 Best Time to Buy and Sell Stock with Cooldown (LC 309)

Problem

Buy/sell multiple times; after selling you must cooldown for one day before buying again.

Example

Input:  prices = [1, 2, 3, 0, 2]   (prices[i] = price on day i)
Output: 3   (transactions: buy@1 sell@2; cooldown; buy@0 sell@2)

Constraints

Clarifying questions

Approach

Three states per day: hold (currently holding), sold (just sold today), rest (idle).

Answer = max(sold[-1], rest[-1]).

Code (Python 3)

from typing import List

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices: return 0
        hold = -prices[0]
        sold = 0
        rest = 0
        for p in prices[1:]:
            prev_sold = sold
            sold = hold + p
            hold = max(hold, rest - p)
            rest = max(rest, prev_sold)
        return max(sold, rest)

Complexity

Comments


29.10 Best Time to Buy and Sell Stock IV (LC 188)

Problem

At most k transactions. Find the max profit.

Example

Input:  k=2, prices=[3,2,6,5,0,3]
Output: 7

Constraints

Clarifying questions

Approach

dp[t][i] = max profit using ≤ t transactions through day i.

dp[t][i] = max(dp[t][i-1], max(price[i] - price[j] + dp[t-1][j-1]) for j < i).

Optimisation: track max_diff = max(dp[t-1][j-1] - price[j]) while iterating.

Code (Python 3)

from typing import List

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        n = len(prices)
        if n == 0 or k == 0: return 0
        if k >= n // 2:
            # Unlimited transactions.
            return sum(max(prices[i] - prices[i - 1], 0) for i in range(1, n))
        dp = [[0] * n for _ in range(k + 1)]
        for t in range(1, k + 1):
            max_diff = -prices[0]
            for i in range(1, n):
                dp[t][i] = max(dp[t][i - 1], prices[i] + max_diff)
                max_diff = max(max_diff, dp[t - 1][i] - prices[i])
        return dp[k][n - 1]

Complexity

Comments


29.11 House Robber II (LC 213)

Problem

The thief cannot rob two adjacent houses; houses are arranged in a circle (nums[0] and nums[n-1] are neighbours).

Example

Input:  nums = [2, 3, 2]   (money at each house; circular layout)
Output: 3   (can rob only one of the two end houses; here rob the 3)

Constraints

Clarifying questions

Approach

Split into two cases: rob or skip the first house. - Case 1: rob house 0 → cannot rob house n-1 → House Robber on nums[0..n-2]. - Case 2: skip house 0 → House Robber on nums[1..n-1].

Answer = max of the two.

Code (Python 3)

from typing import List

class Solution:
    def rob(self, nums: List[int]) -> int:
        def rob_linear(arr: List[int]) -> int:
            prev = curr = 0
            for x in arr:
                prev, curr = curr, max(curr, prev + x)
            return curr

        if len(nums) == 1: return nums[0]
        return max(rob_linear(nums[:-1]), rob_linear(nums[1:]))

Complexity

Comments


29.12 Maximum Product Subarray (LC 152)

Problem

Given nums, find the contiguous subarray with the maximum product.

Example

Input:  nums = [2, 3, -2, 4]
Output: 6   (subarray [2, 3] has product 6, the largest contiguous-subarray product)

Constraints

Clarifying questions

Approach

Track both max and min at each i (because two negatives multiply to a positive): - cur_max = max(x, x * prev_max, x * prev_min) - cur_min = min(x, x * prev_max, x * prev_min)

Code (Python 3)

from typing import List

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        cur_max = cur_min = best = nums[0]
        for x in nums[1:]:
            if x < 0:
                cur_max, cur_min = cur_min, cur_max
            cur_max = max(x, cur_max * x)
            cur_min = min(x, cur_min * x)
            best = max(best, cur_max)
        return best

Complexity

Comments


DP III — Partition / Interval DP

29.13 Palindrome Partitioning II (LC 132)

Problem

Given s, find the minimum number of cuts to split s into all palindromic pieces.

Example

Input:  s = "aab"
Output: 1
(cut once into ["aa", "b"], two palindromic pieces)
Explanation: cut into ["aa", "b"]

Constraints

Clarifying questions

Approach

dp[i] = min cuts for s[0..i]. Pre-compute is_pal[i][j]. Transition: - If s[0..i] is already a palindrome → dp[i] = 0. - Else: dp[i] = min(dp[j-1] + 1 for j <= i if s[j..i] is palindrome).

Code (Python 3)

class Solution:
    def minCut(self, s: str) -> int:
        n = len(s)
        is_pal = [[False] * n for _ in range(n)]
        for i in range(n):
            for j in range(i + 1):
                if s[j] == s[i] and (i - j < 2 or is_pal[j + 1][i - 1]):
                    is_pal[j][i] = True

        dp = [0] * n
        for i in range(n):
            if is_pal[0][i]:
                dp[i] = 0
            else:
                dp[i] = min(dp[j - 1] + 1 for j in range(1, i + 1) if is_pal[j][i])
        return dp[n - 1]

Complexity

Comments


29.14 Burst Balloons (LC 312)

Problem

Given nums representing balloons. Bursting balloon i earns nums[i-1] * nums[i] * nums[i+1] (boundaries treated as 1). Maximise the total score.

Example

Input:  nums = [3, 1, 5, 8]   (balloons left to right)
Output: 167   (max coins by bursting everything; each burst i earns nums[L]*nums[i]*nums[R])

Constraints

Clarifying questions

Approach

Inverted interval DP: instead of asking “which to burst first?”, ask “which is the last balloon burst inside (i, j)?”.

dp[i][j] = max score from bursting all balloons in (i, j) exclusive.

dp[i][j] = max(nums[i] * nums[k] * nums[j] + dp[i][k] + dp[k][j]) for k ∈ (i, j).

Pad nums with 1 on both ends.

Code (Python 3)

from typing import List

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        arr = [1] + nums + [1]
        n = len(arr)
        dp = [[0] * n for _ in range(n)]
        for length in range(2, n):
            for i in range(n - length):
                j = i + length
                for k in range(i + 1, j):
                    dp[i][j] = max(dp[i][j],
                                   arr[i] * arr[k] * arr[j] + dp[i][k] + dp[k][j])
        return dp[0][n - 1]

Complexity

Comments


29.15 Matrix Chain Multiplication (classic)

Problem

Given n matrices A1 · A2 · ... · An with dimensions p[i-1] × p[i]. Find the optimal parenthesisation minimising the number of multiplications.

Example

Input:  p=[10,20,30,40,30]
Output: 30000

Constraints

Clarifying questions

Approach

dp[i][j] = min cost to multiply A_i · A_{i+1} · ... · A_j. Split at k: dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j]).

Code (Python 3)

from typing import List

def matrix_chain(p: List[int]) -> int:
    n = len(p) - 1
    dp = [[0] * (n + 1) for _ in range(n + 1)]
    for length in range(2, n + 1):
        for i in range(1, n - length + 2):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])
    return dp[1][n]

Complexity

Comments


29.16 Minimum Cost to Cut a Stick (LC 1547)

Problem

A stick of length n. Array cuts lists the positions to cut. Each cut costs the current stick length. Find the minimum total cost.

Example

Input:  n=7, cuts=[1,3,4,5]
Output: 16

Constraints

Clarifying questions

Approach

Sort cuts and add boundaries [0, n]. dp[i][j] = min cost to cut the stick between cuts[i] and cuts[j]. Try every cut k ∈ (i, j): dp[i][j] = min(dp[i][k] + dp[k][j] + cuts[j] - cuts[i]).

Code (Python 3)

from typing import List

class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        cuts = sorted([0] + cuts + [n])
        m = len(cuts)
        dp = [[0] * m for _ in range(m)]
        for length in range(2, m):
            for i in range(m - length):
                j = i + length
                dp[i][j] = min(dp[i][k] + dp[k][j] for k in range(i + 1, j)) + cuts[j] - cuts[i]
        return dp[0][m - 1]

Complexity

Comments


29.17 Stone Game VII (LC 1690)

Problem

Alice and Bob alternately pick stones from either end of the array. The player scores the sum of the remaining stones. Both play optimally. Find Alice - Bob.

Example

Input:  stones = [5, 3, 1, 4, 2]   (per-stone values; only ends are pickable)
Output: 6   (Alice - Bob with both playing optimally)

Constraints

Clarifying questions

Approach

Game theory + Interval DP. dp[i][j] = optimal difference when considering stones [i..j]. The current player picks gain sum[i+1..j] (drop stones[i]) or sum[i..j-1] (drop stones[j]) minus dp[next][next].

Code (Python 3)

from functools import cache
from typing import List

class Solution:
    def stoneGameVII(self, stones: List[int]) -> int:
        n = len(stones)
        prefix = [0] * (n + 1)
        for i, s in enumerate(stones):
            prefix[i + 1] = prefix[i] + s

        @cache
        def dp(i: int, j: int) -> int:
            if i >= j: return 0
            # Remove stones[i]: gain = sum of [i+1..j]
            gain_l = prefix[j + 1] - prefix[i + 1]
            # Remove stones[j]: gain = sum of [i..j-1]
            gain_r = prefix[j] - prefix[i]
            return max(gain_l - dp(i + 1, j), gain_r - dp(i, j - 1))

        return dp(0, n - 1)

Complexity

Comments


29.18 Strange Printer (LC 664)

Problem

Given s, a printer prints in turns; each turn prints a substring of identical characters that may overwrite a previous one. Find the minimum number of turns to print s.

Example

Input:  s = "aaabbb"
Output: 2   (the printer prints only one run of same char per turn; min 2 turns)

Constraints

Clarifying questions

Approach

Interval DP. dp[i][j] = min turns to print s[i..j].

Code (Python 3)

class Solution:
    def strangePrinter(self, s: str) -> int:
        n = len(s)
        dp = [[0] * n for _ in range(n)]
        for i in range(n):
            dp[i][i] = 1
        for length in range(2, n + 1):
            for i in range(n - length + 1):
                j = i + length - 1
                dp[i][j] = dp[i][j - 1] + 1
                for k in range(i, j):
                    if s[k] == s[j]:
                        cost = dp[i][k] + (dp[k + 1][j - 1] if k + 1 <= j - 1 else 0)
                        dp[i][j] = min(dp[i][j], cost)
        return dp[0][n - 1]

Complexity

Comments

Chapter summary & decisions

Reading roadmap (pick 8 if time-pressed)

  1. House Robber (LC 198) — basic 1D sequence DP.
  2. Coin Change (LC 322) — unbounded knapsack.
  3. Longest Increasing Subsequence (LC 300) — patience.
  4. LCS (LC 1143) — 2D DP on two strings.
  5. Edit Distance (LC 72) — classic.
  6. Best Time IV (LC 188) — stock DP with k transactions.
  7. Burst Balloons (LC 312) — “pick last” interval DP.
  8. Stone Game (LC 877) — minimax game DP.

Interval DP — “choose last operation” framing

Burst Balloons / Strange Printer / Matrix Chain: - dp[i][j] = optimal answer for range [i..j]. - Ask: which operation is performed LAST in this range? Choice k ∈ [i..j] splits into [i..k-1] and [k+1..j] already solved. - Differs from “pick first” — most of the time “pick last” gives the cleanest recurrence.

State-table sample — LCS (LC 1143)

"" a b c d e
"" 0 0 0 0 0 0
a 0 1 1 1 1 1
c 0 1 1 2 2 2
e 0 1 1 2 2 3

Edit Distance state table — "horse" → "ros"

"" r o s
"" 0 1 2 3
h 1 1 2 3
o 2 2 1 2
r 3 2 2 2
s 4 3 3 2
e 5 4 4 3

Three operations min(insert, delete, replace) + 1; matching characters inherit diagonally.


Chapter 30 — Dijkstra

Dijkstra finds the shortest path from one source to every vertex in a graph with non-negative edge weights. With negative edges → Bellman-Ford. With uniform weights (= 1) → BFS already suffices. Heap-based Dijkstra runs in O((V + E) log V).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

import heapq
from collections import defaultdict

def dijkstra(graph: dict, source: int, n: int) -> list[float]:
    INF = float('inf')
    dist = [INF] * n
    dist[source] = 0
    heap = [(0, source)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:                # outdated entry
            continue
        for v, w in graph[u]:
            nd = d + w
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return dist

End-of-chapter practice


30.1 Network Delay Time (LC 743)

Problem

Given times[i] = [u, v, w] (directed weighted), n nodes, source k. Find the time for the signal to reach every node, or -1.

Example

Input:  times=[[2,1,1],[2,3,1],[3,4,1]], n=4, k=2
Output: 2

Constraints

Clarifying questions

Approach

Dijkstra from k. Answer = max of dist[].

Code (Python 3)

import heapq
from collections import defaultdict
from typing import List

class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        graph = defaultdict(list)
        for u, v, w in times:
            graph[u].append((v, w))
        INF = float('inf')
        dist = {i: INF for i in range(1, n + 1)}
        dist[k] = 0
        heap = [(0, k)]
        while heap:
            d, u = heapq.heappop(heap)
            if d > dist[u]: continue
            for v, w in graph[u]:
                if d + w < dist[v]:
                    dist[v] = d + w
                    heapq.heappush(heap, (d + w, v))
        ans = max(dist.values())
        return ans if ans < INF else -1

Complexity

Comments


30.2 Path With Minimum Effort (LC 1631)

Problem

A heights grid. Walk from (0,0) to (m-1,n-1) (4 directions). The effort of a path = max |h(u) - h(v)| across its edges. Find the minimum effort.

Example

Input:  heights = [[1,2,2],
                   [3,8,2],
                   [5,3,5]]   (m×n grid, 4-dir walk (0,0) → (m-1,n-1))
Output: 2   (min effort = max |h[u] - h[v]| on the best path)

Constraints

Clarifying questions

Approach

Dijkstra with “path weight” = max edge weight instead of sum. nd = max(d, |h(u) - h(v)|).

Code (Python 3)

import heapq
from typing import List

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        rows, cols = len(heights), len(heights[0])
        INF = float('inf')
        effort = [[INF] * cols for _ in range(rows)]
        effort[0][0] = 0
        heap = [(0, 0, 0)]      # (effort, r, c)
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
        while heap:
            e, r, c = heapq.heappop(heap)
            if (r, c) == (rows - 1, cols - 1):
                return e
            if e > effort[r][c]: continue
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols:
                    ne = max(e, abs(heights[nr][nc] - heights[r][c]))
                    if ne < effort[nr][nc]:
                        effort[nr][nc] = ne
                        heapq.heappush(heap, (ne, nr, nc))
        return 0

Complexity

Comments


30.3 Cheapest Flights Within K Stops (LC 787)

Problem

Given flights[i] = [u, v, price], n nodes, source src, destination dst, k stops. Find the cheapest flight using ≤ k stops.

Example

Input:  n=3, flights=[[0,1,100],[1,2,100],[0,2,500]], src=0, dst=2, k=1
Output: 200

Constraints

Clarifying questions

Approach

Bellman-Ford with k + 1 iterations is the cleanest approach.

Dijkstra also works, but the state must expand to (cost, node, stops) — more complex due to 2D state.

Code (Python 3)

from typing import List

class Solution:
    def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
        INF = float('inf')
        dist = [INF] * n
        dist[src] = 0
        for _ in range(k + 1):
            new_dist = dist.copy()
            for u, v, p in flights:
                if dist[u] + p < new_dist[v]:
                    new_dist[v] = dist[u] + p
            dist = new_dist
        return dist[dst] if dist[dst] < INF else -1

Complexity

Comments


30.4 Swim in Rising Water (LC 778) — recap

Fully solved in Chapter 24.6 (offline DSU). Also solvable with Dijkstra: path weight = max elevation encountered.

Code (Python 3)

import heapq
class Solution:
    def swimInWater(self, grid):
        n = len(grid)
        dist = [[float('inf')] * n for _ in range(n)]
        dist[0][0] = grid[0][0]
        heap = [(grid[0][0], 0, 0)]
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
        while heap:
            d, r, c = heapq.heappop(heap)
            if (r, c) == (n-1, n-1): return d
            for dr, dc in DIRS:
                nr, nc = r+dr, c+dc
                if 0 <= nr < n and 0 <= nc < n:
                    nd = max(d, grid[nr][nc])
                    if nd < dist[nr][nc]:
                        dist[nr][nc] = nd
                        heapq.heappush(heap, (nd, nr, nc))
        return -1

Complexity


30.5 Shortest Path in a Grid with Obstacles Elimination (LC 1293)

Problem

Input: grid: List[List[int]] (m × n, 0 = walkable, 1 = obstacle) and integer k. Walk 4-directionally from (0,0)(m-1, n-1), allowed to break at most k obstacles. Find the shortest path from (0,0) to (m-1,n-1).

Example

Input:  grid=[[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]], k=1
Output: 6

Constraints

Clarifying questions

Approach

BFS with state (r, c, eliminations_left) (because the edge weight is 1). Dijkstra also works but is overkill.

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def shortestPath(self, grid: List[List[int]], k: int) -> int:
        rows, cols = len(grid), len(grid[0])
        if rows == 1 and cols == 1: return 0
        # Optimisation: large k → plain BFS.
        if k >= rows + cols - 2:
            return rows + cols - 2

        visited = set([(0, 0, k)])
        queue = deque([(0, 0, k, 0)])    # (r, c, eliminations, steps)
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
        while queue:
            r, c, e, steps = queue.popleft()
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if not (0 <= nr < rows and 0 <= nc < cols): continue
                ne = e - grid[nr][nc]
                if ne < 0: continue
                if (nr, nc) == (rows - 1, cols - 1): return steps + 1
                if (nr, nc, ne) not in visited:
                    visited.add((nr, nc, ne))
                    queue.append((nr, nc, ne, steps + 1))
        return -1

Complexity

Comments


30.6 Minimum Cost to Make at Least One Valid Path in a Grid (LC 1368)

Problem

A grid where each cell has an arrow (4 directions). Following an arrow is free; flipping an arrow costs 1. Find the min cost so that a valid path exists from (0,0) to (m-1,n-1).

Example

Input:  grid = [[1,1,1,1],
                [2,2,2,2],
                [1,1,1,1],
                [2,2,2,2]]
        (1=right, 2=left, 3=down, 4=up; following arrows is free, changing arrow costs 1)
Output: 3   (need to flip 3 cells to create a valid path)

Constraints

Clarifying questions

Approach

0-1 BFS (Dijkstra with a deque): - “Follow arrow” edge = 0 → push to the front of the deque. - “Flip arrow” edge = 1 → push to the back.

O(V + E).

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def minCost(self, grid: List[List[int]]) -> int:
        rows, cols = len(grid), len(grid[0])
        INF = float('inf')
        dist = [[INF] * cols for _ in range(rows)]
        dist[0][0] = 0
        # 1: right, 2: left, 3: down, 4: up
        DIRS = {1: (0,1), 2: (0,-1), 3: (1,0), 4: (-1,0)}
        dq = deque([(0, 0, 0)])      # (cost, r, c)
        while dq:
            cost, r, c = dq.popleft()
            if cost > dist[r][c]: continue
            for d, (dr, dc) in DIRS.items():
                nr, nc = r + dr, c + dc
                if not (0 <= nr < rows and 0 <= nc < cols): continue
                nc_cost = cost + (0 if grid[r][c] == d else 1)
                if nc_cost < dist[nr][nc]:
                    dist[nr][nc] = nc_cost
                    if grid[r][c] == d:
                        dq.appendleft((nc_cost, nr, nc))
                    else:
                        dq.append((nc_cost, nr, nc))
        return dist[rows - 1][cols - 1]

Complexity

Comments

Chapter summary & decisions

Shortest path algorithm chooser

Graph property Algorithm Time
Unweighted (every edge = 1) BFS O(V + E)
Edge weight ∈ {0, 1} 0-1 BFS (deque, push_front for 0, push_back for 1) O(V + E)
Edge weight ≥ 0 Dijkstra (heap) O((V + E) log V)
Negative edges, no negative cycle Bellman-Ford O(V · E)
Negative edges, must detect negative cycle Bellman-Ford + V-th loop check O(V · E)
All-pairs, V ≤ 500 Floyd-Warshall O(V³)
DAG Topo + relax O(V + E)

Cheapest Flights with K Stops (LC 787)

Minimum Cost Valid Path (LC 1368) — 0-1 BFS


Chapter 31 — Game Theory

Game theory in coding interviews means two players alternating turns, each playing optimally. Common pattern: DP minimax — at each state the player to move tries to maximise their own score or minimise their opponent’s. When the state graph has cycles → use BFS multi-source from terminal states.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

from functools import cache

@cache
def dp(state, is_alice_turn):
    if terminal(state):
        return score(state)
    if is_alice_turn:
        return max(dp(next_state(s), False) - delta for s in moves(state))
    else:
        return min(dp(next_state(s), True) + delta for s in moves(state))

End-of-chapter practice


31.1 Nim Game (LC 292)

Problem

There are n stones. Each turn, take 1, 2, or 3 stones. Whoever takes the last stone wins. Alice plays first. Does Alice win?

Example

Input:  n=4
Output: False

Constraints

Clarifying questions

Approach

Observation (induction): - n ∈ {1,2,3}: Alice takes them all → wins. - n == 4: whatever Alice takes (1/2/3), Bob faces {1,2,3} → Bob wins. - General: Alice wins ↔︎ n % 4 != 0.

Code (Python 3)

class Solution:
    def canWinNim(self, n: int) -> bool:
        return n % 4 != 0

Complexity

Comments


31.2 Stone Game (LC 877)

Problem

Array piles (even length, odd total). Alice and Bob alternately take a pile from either end. Alice first. Both play optimally. Does Alice win?

Example

Input:  piles = [5, 3, 4, 5]   (even number of piles, odd total; only ends are pickable)
Output: True   (Alice always wins under optimal play)

Constraints

Clarifying questions

Approach

Math trick: with n even, Alice can adopt the strategy of always taking even-indexed piles or always odd-indexed, picking whichever group has the bigger total. Since the total is odd, one of the groups exceeds half.

→ Alice always wins.

Code (Python 3)

from typing import List

class Solution:
    def stoneGame(self, piles: List[int]) -> bool:
        return True

class SolutionDP:
    """DP version — the canonical pattern for variants."""

    def stoneGame(self, piles: List[int]) -> bool:
        from functools import cache
        n = len(piles)

        @cache
        def diff(l: int, r: int) -> int:
            if l > r: return 0
            return max(piles[l] - diff(l + 1, r), piles[r] - diff(l, r - 1))

        return diff(0, n - 1) > 0

Complexity

Comments


31.3 Predict the Winner (LC 486)

Problem

Same as Stone Game but n is arbitrary (possibly odd) and the total is arbitrary. Alice wins if score(Alice) ≥ score(Bob).

Example

Input:  nums = [1, 5, 2]   (two players alternately take from either end)
Output: False   (Player 1 cannot win under optimal play)

Constraints

Clarifying questions

Approach

DP diff(l, r) identical to 31.2.

Code (Python 3)

from functools import cache
from typing import List

class Solution:
    def predictTheWinner(self, nums: List[int]) -> bool:

        @cache
        def diff(l: int, r: int) -> int:
            if l == r: return nums[l]
            return max(nums[l] - diff(l + 1, r), nums[r] - diff(l, r - 1))

        return diff(0, len(nums) - 1) >= 0

Complexity

Comments


31.4 Stone Game II (LC 1140)

Problem

Piles. Alice first. Each turn take X piles from the front where 1 <= X <= 2*M (M starts at 1, and after each turn M = max(M, X)). Maximise the number of stones Alice can take.

Example

Input:  piles = [2, 7, 9, 4, 4]   (M starts at 1; player takes X piles with 1 ≤ X ≤ 2M, then M = max(M, X))
Output: 10   (max stones Alice can take)

Constraints

Clarifying questions

Approach

DP dfs(i, M) = stones the current player can take from piles[i:] with current M. Maximise over X.

Code (Python 3)

from functools import cache
from typing import List

class Solution:
    def stoneGameII(self, piles: List[int]) -> int:
        n = len(piles)
        suffix = [0] * (n + 1)
        for i in range(n - 1, -1, -1):
            suffix[i] = suffix[i + 1] + piles[i]

        @cache
        def dfs(i: int, M: int) -> int:
            if i + 2 * M >= n:
                return suffix[i]
            return suffix[i] - min(dfs(i + x, max(M, x)) for x in range(1, 2 * M + 1))

        return dfs(0, 1)

Complexity

Comments


31.5 Cat and Mouse (LC 913)

Problem

Input: graph: List[List[int]]adjacency list of an undirected graph, graph[i] are the neighbours of node i. Nodes are labelled 0..n-1.

Game rules: - Mouse starts at node 1, Cat at node 2, hole is node 0. - Turn 1: mouse moves; turn 2: cat moves (each must move to a neighbour). - Cat cannot enter the hole (node 0). - Mouse wins on reaching the hole; Cat wins on landing on the Mouse; draw if a state repeats.

Return one of: 1 = MOUSE_WIN, 2 = CAT_WIN, 0 = DRAW.

Example

Input:  graph = [[2,5], [3], [0,4,5], [1,4,5], [2,3], [0,2,3]]
        (adjacency list of an undirected graph)
        The graph has 6 nodes 0..5; node 0 is the hole.
Output: 0
        (0 = DRAW, 1 = MOUSE_WIN, 2 = CAT_WIN)

Constraints

Clarifying questions

Approach

BFS from terminal states (retrograde analysis). This differs from DP because the state graph has cycles (cat & mouse can shuttle back and forth) ⇒ top-down memo wouldn’t terminate.

State = (mouse, cat, turn) with turn ∈ {0=mouse, 1=cat}. Total O(n²) states.

Terminal: - mouse == 0 (mouse in hole) ⇒ MOUSE_WIN. - mouse == cat (cat catches mouse) ⇒ CAT_WIN.

Outcome propagation (retrograde): for each state with known outcome, walk backwards (parents that could reach this state) and deduce: - If the winner’s turn is in the parent (e.g. parent is mouse’s turn and current state is MOUSE_WIN) ⇒ parent is also WIN (one winning move is enough). - If the loser’s turn is in the parent ⇒ only mark parent loss when every move leads to loss (count via degree).

State/outcome diagram (simplified):

Terminal layer:
  (0, *, *)        ─────────────► MOUSE_WIN
  (k, k, *)  k>0   ─────────────► CAT_WIN

Retrograde propagation (BFS):

  state S with known outcome = X
       │
       ▼ for each parent P of S (flip turn):
       │
       ├── turn(P) is "X's winner" ?  YES → P = X (push)
       │
       └── turn(P) is "X's loser" ?
              degree[P] -= 1
              if degree[P] == 0:
                  P = X (push)     ← every move loses
  states still unset after BFS → DRAW.

This is a notoriously Hard LeetCode problem; the chapter focuses on the pattern (retrograde BFS); the full implementation is below.

Code (Python 3)

from collections import deque
from typing import List

MOUSE_WIN, CAT_WIN, DRAW = 1, 2, 0

class Solution:
    def catMouseGame(self, graph: List[List[int]]) -> int:
        n = len(graph)
        # state: (mouse, cat, turn) — turn 0=mouse, 1=cat
        color = {}
        degree = {}
        for m in range(n):
            for c in range(n):
                degree[(m, c, 0)] = len(graph[m])
                degree[(m, c, 1)] = len(graph[c]) - (0 in graph[c])

        q = deque()
        for c in range(n):
            for t in range(2):
                color[(0, c, t)] = MOUSE_WIN
                q.append((0, c, t, MOUSE_WIN))
            color[(c, c, 0)] = color[(c, c, 1)] = CAT_WIN if c != 0 else MOUSE_WIN
            for t in range(2):
                q.append((c, c, t, color[(c, c, t)]))

        def parents(m: int, c: int, t: int):
            prev_turn = 1 - t
            if prev_turn == 0:           # mouse just moved
                for prev_m in graph[m]:
                    yield (prev_m, c, prev_turn)
            else:
                for prev_c in graph[c]:
                    if prev_c == 0: continue
                    yield (m, prev_c, prev_turn)

        while q:
            m, c, t, col = q.popleft()
            for pm, pc, pt in parents(m, c, t):
                if (pm, pc, pt) in color: continue
                if (pt == 0 and col == MOUSE_WIN) or (pt == 1 and col == CAT_WIN):
                    color[(pm, pc, pt)] = col
                    q.append((pm, pc, pt, col))
                else:
                    degree[(pm, pc, pt)] -= 1
                    if degree[(pm, pc, pt)] == 0:
                        color[(pm, pc, pt)] = col
                        q.append((pm, pc, pt, col))
        return color.get((1, 2, 0), DRAW)

Complexity

Comments


31.6 Can I Win (LC 464)

Problem

You have 1..maxChoosable. Each turn the current player picks an unused number. The first to make the running total ≥ desiredTotal wins. Alice plays first. Does Alice win?

Example

Input:  maxChoosable=10, desiredTotal=11
Output: False

Constraints

Clarifying questions

Approach

Bitmask DP — state = set of used numbers (bitmask) + current sum. @cache memoizes.

Code (Python 3)

from functools import cache

class Solution:
    def canIWin(self, maxChoosable: int, desiredTotal: int) -> bool:
        if (1 + maxChoosable) * maxChoosable // 2 < desiredTotal:
            return False

        @cache
        def dfs(mask: int, remaining: int) -> bool:
            for i in range(1, maxChoosable + 1):
                bit = 1 << i
                if mask & bit:
                    continue
                if i >= remaining:
                    return True
                if not dfs(mask | bit, remaining - i):
                    return True             # opponent loses → we win
            return False

        return dfs(0, desiredTotal)

Complexity

Comments

Chapter summary & decisions

Game taxonomy

Kind Hallmark Problems
Math trick Parity / sum / “always wins” analysis LC 877 (Stone Game I), LC 292 Nim
Minimax DP dp[state] = optimal value for the player to move LC 486, 1140
Memoized state graph Discrete state, complex transitions LC 464 (Can I Win)
Retrograde BFS Propagate outcomes from terminals backwards LC 913 (Cat and Mouse)

Stone Game (LC 877) — why Alice always wins

Can I Win (LC 464) — bitmask state

Cat and Mouse (LC 913) — retrograde


Chapter 32 — String Parser (Stack, State Machine, Regex)

“String parser” problems ask you to implement a mini-language: calculator, atom formula, lisp, … Common patterns: (i) Stack for nested structures, (ii) State machine for token parsing, (iii) Recursive descent for grammars with nested rules. Problems 8.6 (Decode String) and 2.4 (atoi) already teased.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

3 common templates:

Template Example problems
Stack with operators Basic Calculator I/II
FSM tokens atoi, Valid Number
Recursive descent Parse Lisp, Add Operators

End-of-chapter practice


32.1 Basic Calculator II (LC 227)

Problem

Given a string s containing numbers, +, -, *, /, and whitespace. Evaluate the expression. Division truncates toward 0. No parentheses.

Example

Input:  s = " 3+5 / 2 "
Output: 5
        (3 + (5/2) = 3 + 2 = 5; integer division)

Input:  s = " 14-3/2 "
Output: 13
        (14 - (3/2) = 14 - 1 = 13)

Constraints

Clarifying questions

Approach

Stack of “terms” built so far. On encountering a new operator: - +x → push x. - -x → push -x. - *x → top = top * x. - /x → top = int(top / x).

At the end: sum the stack.

Code (Python 3)

class Solution:
    def calculate(self, s: str) -> int:
        s += '+'                  # sentinel
        stack: list[int] = []
        num = 0
        op = '+'
        for ch in s:
            if ch.isdigit():
                num = num * 10 + int(ch)
            elif ch in '+-*/':
                if op == '+': stack.append(num)
                elif op == '-': stack.append(-num)
                elif op == '*': stack[-1] *= num
                else: stack[-1] = int(stack[-1] / num)    # truncate to 0
                op = ch
                num = 0
        return sum(stack)

Complexity

Comments


32.2 Decode String (LC 394) — recap

Fully solved in Chapter 8.6. Pattern: nested stack with (prev_str, k).

This is the gateway to String Parser — nested structure → stack. The whole of Chapter 32 extends this pattern: Calculator (operator stack), Number of Atoms (dict stack), Regex (DP table), Valid Number (FSM).

Code (Python 3) (recap)

class Solution:
    def decodeString(self, s: str) -> str:
        stack: list[tuple[str, int]] = []
        cur, k = "", 0
        for ch in s:
            if ch.isdigit():
                k = k * 10 + int(ch)
            elif ch == '[':
                stack.append((cur, k))
                cur, k = "", 0
            elif ch == ']':
                prev_str, prev_k = stack.pop()
                cur = prev_str + cur * prev_k
            else:
                cur += ch
        return cur

Complexity


32.3 Number of Atoms (LC 726)

Problem

Given a chemical formula (e.g. "K4(ON(SO3)2)2"), return the canonical form K4N2O14S4 — alphabetised atomic names + counts.

Example

Input:  formula = "H2O"
Output: "H2O"

Input:  formula = "Mg(OH)2"
Output: "H2MgO2"
        (Mg×1, O×2, H×2; sort alphabetic; count 1 omitted)

Input:  formula = "K4(ON(SO3)2)2"
Output: "K4N2O14S4"
        (K×4, N×2, O×14, S×4; sort alphabetic)

Constraints

Clarifying questions

Approach

Stack of dicts. Each ( opens a new dict. Each ) + multiplier → multiply the dict and merge into the parent. Each atom name + count → add to the top dict.

Code (Python 3)

from collections import Counter, defaultdict

class Solution:
    def countOfAtoms(self, formula: str) -> str:
        stack: list[dict] = [defaultdict(int)]
        i, n = 0, len(formula)
        while i < n:
            ch = formula[i]
            if ch == '(':
                stack.append(defaultdict(int))
                i += 1
            elif ch == ')':
                i += 1
                j = i
                while j < n and formula[j].isdigit():
                    j += 1
                mult = int(formula[i:j]) if j > i else 1
                i = j
                top = stack.pop()
                for atom, cnt in top.items():
                    stack[-1][atom] += cnt * mult
            else:
                j = i + 1
                while j < n and formula[j].islower():
                    j += 1
                atom = formula[i:j]
                i = j
                k = i
                while k < n and formula[k].isdigit():
                    k += 1
                count = int(formula[i:k]) if k > i else 1
                i = k
                stack[-1][atom] += count
        result = stack[-1]
        return ''.join(
            atom + (str(cnt) if cnt > 1 else '')
            for atom, cnt in sorted(result.items())
        )

Complexity

Comments


32.4 Regular Expression Matching (LC 10)

Problem

Check whether string s matches pattern p. p may contain . (matches any single char) and * (matches 0+ of the preceding character).

Example

Input:  s = "aa", p = "a*"
Output: True
        (`a*` matches 0+ 'a' → matches "aa")

Input:  s = "mississippi", p = "mis*is*p*."
Output: False

Input:  s = "ab", p = ".*"
Output: True
        (`.*` matches 0+ of any character → matches "ab")

Constraints

Clarifying questions

Approach

2D DP. dp[i][j] = does s[:i] match p[:j]?

Code (Python 3)

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[0][0] = True
        for j in range(2, n + 1):
            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]
                    if p[j - 2] == '.' or p[j - 2] == s[i - 1]:
                        dp[i][j] = dp[i][j] or dp[i - 1][j]
                else:
                    if p[j - 1] == '.' or p[j - 1] == s[i - 1]:
                        dp[i][j] = dp[i - 1][j - 1]
        return dp[m][n]

Complexity

Comments


32.5 Valid Number (LC 65)

Problem

Check whether a string s is a valid number. Valid forms include integers, decimals, and scientific notation, e.g. "2", "-2.5", "3e+10", "-.5".

Example

Input:  s = "0"             → Output: True
Input:  s = "e"             → Output: False  (only the letter e, no digits)
Input:  s = "."             → Output: False  (only a dot, no digits)
Input:  s = "+6e-1"         → Output: True   (valid scientific notation)

Constraints

Clarifying questions

Approach

FSM with 9 states or use regex. The FSM is concise and easier to explain in interviews.

Code (Python 3) (using regex)

import re

class Solution:
    def isNumber(self, s: str) -> bool:
        pattern = r'^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$'
        return bool(re.match(pattern, s.strip()))

Complexity

Comments


32.6 Integer to English Words (LC 273)

Problem

Convert num (≤ 2³¹-1) into English text.

Example

Input:  num = 123
Output: "One Hundred Twenty Three"

Input:  num = 12345
Output: "Twelve Thousand Three Hundred Forty Five"

Input:  num = 0
Output: "Zero"

Constraints

Clarifying questions

Approach

Split into groups of three digits (units, thousands, millions, billions). Each group has the pattern: hundreds + tens-ones.

Code (Python 3)

class Solution:
    UNDER_20 = ["", "One","Two","Three","Four","Five","Six","Seven","Eight","Nine",
                "Ten","Eleven","Twelve","Thirteen","Fourteen","Fifteen","Sixteen",
                "Seventeen","Eighteen","Nineteen"]
    TENS = ["", "", "Twenty","Thirty","Forty","Fifty","Sixty","Seventy","Eighty","Ninety"]
    THOUSANDS = ["", "Thousand", "Million", "Billion"]

    def numberToWords(self, num: int) -> str:
        if num == 0: return "Zero"

        def under_thousand(n: int) -> str:
            if n == 0: return ""
            if n < 20: return self.UNDER_20[n] + " "
            if n < 100: return self.TENS[n // 10] + " " + under_thousand(n % 10)
            return self.UNDER_20[n // 100] + " Hundred " + under_thousand(n % 100)

        parts = []
        i = 0
        while num > 0:
            if num % 1000 != 0:
                parts.append((under_thousand(num % 1000) + self.THOUSANDS[i]).strip())
            num //= 1000
            i += 1
        return ' '.join(reversed(parts)).strip()

Complexity

Comments

Chapter summary & decisions

Parser taxonomy

Kind Hallmark Problems
Stack-based Nested structures (paren, brackets) LC 224, 394, 726
FSM / state machine Linear tokens with distinct states LC 8, 65
Recursive descent Grammar with recursive structure LC 736, 770
DP on string Pattern matching (., *) LC 10, 44

Valid Number (LC 65) — FSM table

States: 0 start, 1 sign, 2 int, 3 dot_after_int, 4 dot_before_int (needs frac), 5 frac, 6 e/E, 7 exp_sign, 8 exp_int, 9 end (whitespace/error). | State | digit | +/- | . | e/E | |—|—|—|—|—| | 0 (start) | 2 | 1 | 4 | – | | 1 (sign) | 2 | – | 4 | – | | 2 (int) | 2 | – | 3 | 6 | | 3 (dot after int) | 5 | – | – | 6 | | 4 (dot only) | 5 | – | – | – | | 5 (frac) | 5 | – | – | 6 | | 6 (e) | 8 | 7 | – | – | | 7 (exp sign) | 8 | – | – | – | | 8 (exp int) | 8 | – | – | – |

Accept states: {2, 3, 5, 8}.

Regex Matching (LC 10) — why DP?

Number of Atoms (LC 726) — nested stack trace

"K4(ON(SO3)2)2":

stack = [Counter()]
'K' '4'  → top {K:4}
'('      → push new {}
'O' 'N'  → top {O:1, N:1}
'('      → push
'S' 'O' '3' → top {S:1, O:3}
')' '2'  → pop, multiply by 2: {S:2, O:6} → merge into below {O:1,N:1,S:2,O:7} ⇒ {O:7, N:1, S:2}
')' '2'  → pop, multiply by 2, merge into base {K:4} → {K:4, O:14, N:2, S:4}

Chapter 33 — Minimum Spanning Tree (MST)

MST = a subset of edges connecting all vertices with minimum total weight and no cycles. Two algorithms: Kruskal (sort edges + DSU) and Prim (heap-grown from a vertex). Both are O(E log E).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

# Kruskal: sort edges + DSU
def kruskal(n, edges):
    edges.sort(key=lambda e: e[2])
    dsu = DSU(n)
    total = 0
    for u, v, w in edges:
        if dsu.union(u, v):
            total += w
    return total


# Prim: heap-based growth
import heapq
def prim(n, graph):
    visited = [False] * n
    heap = [(0, 0)]                # (weight, node)
    total = 0
    while heap:
        w, u = heapq.heappop(heap)
        if visited[u]: continue
        visited[u] = True
        total += w
        for v, weight in graph[u]:
            if not visited[v]:
                heapq.heappush(heap, (weight, v))
    return total

End-of-chapter practice


33.1 Min Cost to Connect All Points (LC 1584)

Problem

Given 2D points, the cost of connecting two points is their Manhattan distance. Find the minimum cost to connect all points.

Example

Input:  points = [[0,0], [2,2], [3,10], [5,2], [7,0]]
        (each point [x, y]; cost between two = |x1-x2| + |y1-y2| Manhattan)
Output: 20   (MST total cost connecting every point)

Constraints

Clarifying questions

Approach

Kruskal: build all O(n²) edges, sort, DSU. Prim: better for dense graphs (every pair is an edge) — O(n²).

Code (Python 3) (Prim)

import heapq
from typing import List

class Solution:
    def minCostConnectPoints(self, points: List[List[int]]) -> int:
        n = len(points)
        visited = [False] * n
        heap = [(0, 0)]
        total = 0
        count = 0
        while count < n:
            w, u = heapq.heappop(heap)
            if visited[u]: continue
            visited[u] = True
            total += w
            count += 1
            for v in range(n):
                if not visited[v]:
                    dist = abs(points[u][0]-points[v][0]) + abs(points[u][1]-points[v][1])
                    heapq.heappush(heap, (dist, v))
        return total

Complexity

Comments


33.2 Connecting Cities With Minimum Cost (LC 1135)

Problem

Given cities 1..n and connections[i] = [a, b, cost]. Minimum cost to connect all cities, or -1 if impossible.

Example

Input:  n=3, connections=[[1,2,5],[1,3,6],[2,3,1]]
Output: 6

Constraints

Clarifying questions

Approach

Plain Kruskal. Sort edges ascending by weight; use DSU to accept only those that join different components; after the sweep, if the DSU still has one component, return the total weight.

Code (Python 3)

from typing import List

class Solution:
    def minimumCost(self, n: int, connections: List[List[int]]) -> int:
        connections.sort(key=lambda c: c[2])
        parent = list(range(n + 1))
        def find(x):
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        total = 0
        used = 0
        for a, b, c in connections:
            ra, rb = find(a), find(b)
            if ra != rb:
                parent[ra] = rb
                total += c
                used += 1
                if used == n - 1:
                    return total
        return -1

Complexity

Comments


33.3 Optimize Water Distribution in a Village (LC 1168)

Problem

n houses. Each house can either (a) dig its own well costing wells[i], or (b) connect to another via pipes[j] = [u, v, c]. Minimum total cost to supply water everywhere.

Example

Input:  n=3, wells=[1,2,2], pipes=[[1,2,1],[2,3,1]]
Output: 3

Constraints

Clarifying questions

Approach

Virtual-node 0 trick: add node 0 and edges (0, i, wells[i]) for each house. Run MST on n+1 nodes.

Code (Python 3)

from typing import List

class Solution:
    def minCostToSupplyWater(self, n: int, wells: List[int], pipes: List[List[int]]) -> int:
        edges = pipes[:]
        for i, w in enumerate(wells):
            edges.append([0, i + 1, w])
        edges.sort(key=lambda e: e[2])
        parent = list(range(n + 1))
        def find(x):
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x
        total = 0
        for u, v, c in edges:
            ru, rv = find(u), find(v)
            if ru != rv:
                parent[ru] = rv
                total += c
        return total

Complexity

Comments


33.4 Critical and Pseudo-Critical Edges in MST (LC 1489)

Problem

Input: n (number of vertices) and edges: List[List[int]] — each edge [u, v, w] is undirected with weight w. Find every critical edge (removing it raises the MST cost) and pseudo-critical (can appear in some MST).

Example

Input:  n=5, edges=[[0,1,1],[1,2,1],[2,3,2],[0,3,2],[0,4,3],[3,4,3],[1,4,6]]
Output: [[0,1],[2,3,4,5]]

Constraints

Clarifying questions

Approach

  1. Compute the baseline mst_cost.
  2. For each edge e:

Code (Python 3)

from typing import List

class Solution:
    def findCriticalAndPseudoCriticalEdges(self, n: int, edges: List[List[int]]) -> List[List[int]]:
        # Track original indices after sorting.
        indexed = [e + [i] for i, e in enumerate(edges)]
        indexed.sort(key=lambda e: e[2])

        def kruskal(skip=-1, force=-1) -> int:
            parent = list(range(n))
            def find(x):
                while parent[x] != x:
                    parent[x] = parent[parent[x]]
                    x = parent[x]
                return x
            total = used = 0
            if force != -1:
                u, v, w, _ = indexed[force]
                parent[find(u)] = find(v)
                total = w
                used = 1
            for i, (u, v, w, _) in enumerate(indexed):
                if i == skip or i == force: continue
                ru, rv = find(u), find(v)
                if ru != rv:
                    parent[ru] = rv
                    total += w
                    used += 1
            return total if used == n - 1 else float('inf')

        base = kruskal()
        critical, pseudo = [], []
        for i, (u, v, w, orig) in enumerate(indexed):
            if kruskal(skip=i) > base:
                critical.append(orig)
            elif kruskal(force=i) == base:
                pseudo.append(orig)
        return [critical, pseudo]

Complexity

Comments


33.5 Checking Existence of Edge Length Limited Paths (LC 1697)

Problem

Given an undirected graph with edgeList[i] = [u, v, dist] and queries queries[j] = [p, q, limit]. For each query, return True if there exists a path between p, q whose every edge has dist < limit.

Example

Input:  n=3, edges=[[0,1,2],[1,2,4],[2,0,8],[1,0,16]]
        queries=[[0,1,2],[0,2,5]]
Output: [False, True]

Constraints

Clarifying questions

Approach

Brute force: BFS/DFS per query with dist < limit. O(Q · E). TLE.

Optimal — Offline Kruskal-style — O((E + Q) log).

Sort both edgeList (by dist) and queries (by limit). Process queries in increasing order: for each query, union every edge with dist < limit in the DSU. Then check find(p) == find(q).

Code (Python 3)

from typing import List

class Solution:
    def distanceLimitedPathsExist(self, n: int, edgeList: List[List[int]],
                                   queries: List[List[int]]) -> List[bool]:
        parent = list(range(n))
        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x
        def union(x: int, y: int) -> None:
            rx, ry = find(x), find(y)
            if rx != ry:
                parent[rx] = ry

        edgeList.sort(key=lambda e: e[2])
        # Tag queries with original index to output in order.
        indexed_q = sorted(enumerate(queries), key=lambda kv: kv[1][2])

        result = [False] * len(queries)
        i = 0
        for q_idx, (p, q, limit) in indexed_q:
            while i < len(edgeList) and edgeList[i][2] < limit:
                u, v, _ = edgeList[i]
                union(u, v)
                i += 1
            result[q_idx] = (find(p) == find(q))
        return result

Complexity

Comments


33.6 Path With Maximum Minimum Value (LC 1102)

Problem

Grid m × n. Find a path from (0,0) to (m-1,n-1) (4-directional) maximising the minimum value on the path. Return that value.

Example

Input:  grid = [[5, 4, 5],
                [1, 2, 6],
                [7, 4, 6]]   (m × n grid, 4-dir walk (0,0) → (m-1, n-1))
Output: 4   (the MIN value on the best path is maximised = 4; e.g. 5→4→5→6→6 has min = 4)

Constraints

Clarifying questions

Approach

“Reverse Kruskal” pattern — activate cells in decreasing value order. When (0,0) and (m-1,n-1) share a component → the value of the cell just activated is the answer (it is the minimum along the discovered path).

Same family as Swim in Rising Water (Ch 24.6) but reversed: instead of “lowest elevation first” → “highest value first”.

Code (Python 3)

from typing import List

class Solution:
    def maximumMinimumPath(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        parent = list(range(m * n))
        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x
        def union(x: int, y: int) -> None:
            rx, ry = find(x), find(y)
            if rx != ry:
                parent[rx] = ry

        # Sort cells in DESCENDING value.
        cells = sorted(
            ((grid[r][c], r, c) for r in range(m) for c in range(n)),
            reverse=True,
        )
        active = [[False] * n for _ in range(m)]
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        for val, r, c in cells:
            active[r][c] = True
            idx = r * n + c
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < m and 0 <= nc < n and active[nr][nc]:
                    union(idx, nr * n + nc)
            if find(0) == find(m * n - 1):
                return val
        return -1

Complexity

Comments

Chapter summary & decisions

MST proof principles

Kruskal vs Prim

Kruskal Prim
Data structures DSU + sorted edges Heap + visited
Time O(E log E) O(E log V)
Sparse graph ✅ Good OK
Dense graph OK ✅ Better
Need edge list Needs adjacency list
Streaming edges ✅ (process incrementally)

Kruskal mental model

“Sort edges by weight → for each edge, if it links two different components, take it. DSU tracks connectivity incrementally.”

LC 1697 — Offline connectivity threshold (not MST but in the family)

Path Maximum Minimum / Bottleneck path


Chapter 34 — Rolling Hash

Rolling Hash = a string hash that you can “slide a window” over with O(1) updates. This pattern turns O(n · m) into O(n + m) for substring matching, duplicate detection, etc.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Pattern (Rabin-Karp):

hash(s[l..r]) = (s[l]·base^(r-l) + s[l+1]·base^(r-l-1) + ... + s[r]) mod M

When sliding: hash_new = (hash_old · base + s[r+1] - s[l] · base^(r-l+1)) mod M

Collision pitfall. A prime modulus 10^9 + 7 is usually safe for LC. For adversarial inputs use two different hashes (double hashing).

Template code

def rolling_hash(s: str, length: int) -> set[int]:
    BASE = 26
    MOD = (1 << 61) - 1
    n = len(s)
    base_pow = pow(BASE, length, MOD)
    h = 0
    seen = set()
    for i in range(n):
        h = (h * BASE + ord(s[i])) % MOD
        if i >= length:
            h = (h - ord(s[i - length]) * base_pow) % MOD
        if i >= length - 1:
            seen.add(h)
    return seen

End-of-chapter practice


34.1 Repeated DNA Sequences (LC 187)

Problem

Given a DNA string s (containing A, C, G, T), return every length-10 substring that appears at least twice.

Example

Input:  s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"   (string of A/C/G/T)
Output: ["AAAAACCCCC", "CCCCCAAAAA"]
        (every length-10 substring appearing ≥ 2 times; order doesn't matter)

Constraints

Clarifying questions

Approach

Brute force: set of length-10 substrings. O(n · 10) time, O(n · 10) space.

Rolling hash: O(n) time, O(n) space (smaller memory because hashes are ints).

Code (Python 3)

from typing import List

class Solution:
    def findRepeatedDnaSequences(self, s: str) -> List[str]:
        seen: dict[str, int] = {}
        result: list[str] = []
        for i in range(len(s) - 9):
            sub = s[i:i + 10]
            seen[sub] = seen.get(sub, 0) + 1
            if seen[sub] == 2:
                result.append(sub)
        return result

Complexity

Comments


34.2 Longest Duplicate Substring (LC 1044)

Problem

Given s, find the longest substring appearing at least twice (overlap allowed). If multiple, return any.

Example

Input:  s = "banana"
Output: "ana"   (longest repeated substring; if multiple, return any)

Constraints

Clarifying questions

Approach

Binary search on length + rolling-hash check.

check(L): rolling hash + set; collisions → duplicate exists.

Code (Python 3)

class Solution:
    def longestDupSubstring(self, s: str) -> str:
        BASE = 26
        MOD = (1 << 61) - 1
        n = len(s)
        nums = [ord(c) - ord('a') for c in s]

        def search(L: int) -> int:
            base_pow = pow(BASE, L, MOD)
            h = 0
            for i in range(L):
                h = (h * BASE + nums[i]) % MOD
            seen = {h: 0}
            for i in range(1, n - L + 1):
                h = (h * BASE - nums[i - 1] * base_pow + nums[i + L - 1]) % MOD
                if h in seen:
                    return i
                seen[h] = i
            return -1

        lo, hi = 1, n - 1
        start = 0
        best_len = 0
        while lo <= hi:
            mid = (lo + hi) // 2
            idx = search(mid)
            if idx != -1:
                if mid > best_len:
                    best_len = mid
                    start = idx
                lo = mid + 1
            else:
                hi = mid - 1
        return s[start:start + best_len]

Complexity

Comments


34.3 Distinct Echo Substrings (LC 1316)

Problem

Count the number of distinct substrings of the form a + a (a string concatenated with itself).

Example

Input:  text = "abcabcabc"
Output: 3
(distinct echo substrings; echo = a+a;
 here "abcabc", "bcabca", "cabcab")

Constraints

Clarifying questions

Approach

For each L (length of a), check every position where s[i..i+L-1] == s[i+L..i+2L-1]. Rolling hash makes the comparison O(1). Collect seen hashes.

Code (Python 3)

class Solution:
    def distinctEchoSubstrings(self, s: str) -> int:
        n = len(s)
        BASE = 26
        MOD = (1 << 61) - 1
        nums = [ord(c) - ord('a') for c in s]

        # Precompute prefix hash + power.
        h = [0] * (n + 1)
        p = [1] * (n + 1)
        for i in range(n):
            h[i + 1] = (h[i] * BASE + nums[i]) % MOD
            p[i + 1] = (p[i] * BASE) % MOD

        def get_hash(l: int, r: int) -> int:
            return (h[r + 1] - h[l] * p[r - l + 1]) % MOD

        seen: set[int] = set()
        for L in range(1, n // 2 + 1):
            for i in range(n - 2 * L + 1):
                h1 = get_hash(i, i + L - 1)
                h2 = get_hash(i + L, i + 2 * L - 1)
                if h1 == h2:
                    seen.add(h1)
        return len(seen)

Complexity

Comments


34.4 Shortest Palindrome (LC 214) — Rolling Hash version

Problem

Given s, prepend the fewest characters so it becomes a palindrome.

Example

Input:  s = "aacecaaa"
Output: "aaacecaaa"   (prepend the FEWEST chars to make s a palindrome)

Constraints

Clarifying questions

Approach

Find the longest prefix of s that is a palindrome. Reverse the rest and prepend.

Rolling-hash approach: compute hashes of s and reverse(s). Find the largest L such that hash(s[:L]) == hash(rev_s[n-L:]) (= s[:L] is a palindrome).

Code (Python 3)

class Solution:
    def shortestPalindrome(self, s: str) -> str:
        n = len(s)
        if n <= 1: return s
        BASE = 131
        MOD = 10**9 + 7
        h1 = h2 = 0
        power = 1
        best = 0
        for i, ch in enumerate(s):
            h1 = (h1 * BASE + ord(ch)) % MOD
            h2 = (h2 + ord(ch) * power) % MOD
            power = power * BASE % MOD
            if h1 == h2:
                best = i + 1
        return s[best:][::-1] + s

Complexity

Comments


34.5 Strings Differ by One Character (LC 1638)

Problem

Given a list of equal-length strings. Return True if any two differ in exactly one position.

Example

Input:  dict = ["abcd", "acbd", "aacd"]
Output: True   (two strings differ by exactly one position; e.g. "abcd" vs "aacd")

Constraints

Clarifying questions

Approach

For each string and each position i, “mask” the character at i and hash the rest. If a collision occurs → there’s a pair differing in exactly one position.

Code (Python 3)

from typing import List

class Solution:
    def differByOne(self, dict: List[str]) -> bool:
        n_words = len(dict)
        L = len(dict[0])
        seen: set[tuple[int, str, int]] = set()
        for w in dict:
            for i in range(L):
                key = (i, w[:i] + w[i+1:])
                if key in seen:
                    return True
                seen.add(key)
        return False

Complexity

Comments


34.6 Sum of Scores of Built Strings (LC 2223)

Problem

Build s by prepending characters one at a time. At step i, the “score” = the longest prefix of s_i that is also a suffix of s_i. Return the sum of all scores.

Example

Input:  s = "babab"
Output: 9   (score[i] = LCP(s, s[i:]); sum scores over all i)

Constraints

Clarifying questions

Approach

Z function in Chapter 36 is the main pattern. Rolling hash also works: for each i, binary-search the longest matching prefix-suffix via hash equality.

Code (Python 3) (Z function — preview)

class Solution:
    def sumScores(self, s: str) -> int:
        # Z function — details in Chapter 36.
        n = len(s)
        z = [0] * n
        z[0] = n
        l = r = 0
        for i in range(1, n):
            if i < r:
                z[i] = min(r - i, z[i - l])
            while i + z[i] < n and s[z[i]] == s[i + z[i]]:
                z[i] += 1
            if i + z[i] > r:
                l, r = i, i + z[i]
        return sum(z)

Complexity

Comments

Chapter summary & decisions

Rolling-hash correctness — two levels

Goal Enough
LC accepted 1 hash with a large prime modulus (10⁹+7 or (1<<61) - 1)
Production / robust Double hash (two (base, mod) pairs) or re-verify the substring on collision
Absolutely safe Store the substring instead of the hash → costs memory

Prefix-hash convention [l, r)

hash[i] = (hash[i-1] * base + ord(s[i-1])) mod M
substring s[l:r] hash = (hash[r] - hash[l] * pow_base[r - l]) mod M

Note in Python: (a - b) % M is always correct (negative-safe). Java/C++ need ((... % M) + M) % M.

Distinct Echo Substrings (LC 1316) — collision

Rolling hash vs KMP/Z

Rolling hash KMP / Z
Find pattern P in text T O(|T|) avg, possible collision O(|T| + |P|) guaranteed
Many queries (multi-pattern search) Index text hashes Build per pattern
General substring comparison ✅ Hash any range KMP/Z don’t directly support
Collision risk Yes No

Chapter 35 — KMP (Knuth–Morris–Pratt)

KMP solves substring matching in O(n + m), the heart of which is the LPS array (Longest Prefix Suffix) — for each index i in the pattern, lps[i] = the length of the longest prefix of pattern[0..i] that is also a suffix of it (not itself).

LPS — visualised with ababaca

LPS is KMP’s trickiest concept. Hand-trace with pattern = "ababaca":

index  :   0    1    2    3    4    5    6
char   :   a    b    a    b    a    c    a
LPS    :   0    0    1    2    3    0    1

Cell-by-cell explanation:
  LPS[0]=0  (one character, no proper prefix)
  LPS[1]=0  "ab" — prefix "a" ≠ suffix "b"
  LPS[2]=1  "aba" — prefix "a" = suffix "a" → 1
  LPS[3]=2  "abab" — prefix "ab" = suffix "ab" → 2
  LPS[4]=3  "ababa" — prefix "aba" = suffix "aba" → 3
  LPS[5]=0  "ababac" — no prefix matches suffix ending in "c" → 0
  LPS[6]=1  "ababaca" — prefix "a" = suffix "a" → 1

Intuition: LPS[i] tells us that when a mismatch happens at pattern[i+1], we don’t have to restart from the beginning — we can jump to pattern[LPS[i]] because pattern[0..LPS[i]-1] must already match (it is also a suffix of what we’ve matched).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

def build_lps(p: str) -> list[int]:
    n = len(p)
    lps = [0] * n
    length = 0
    i = 1
    while i < n:
        if p[i] == p[length]:
            length += 1
            lps[i] = length
            i += 1
        elif length > 0:
            length = lps[length - 1]
        else:
            lps[i] = 0
            i += 1
    return lps


def kmp_search(text: str, p: str) -> list[int]:
    lps = build_lps(p)
    result = []
    i = j = 0
    while i < len(text):
        if text[i] == p[j]:
            i += 1; j += 1
            if j == len(p):
                result.append(i - j)
                j = lps[j - 1]
        elif j > 0:
            j = lps[j - 1]
        else:
            i += 1
    return result

End-of-chapter practice


35.1 Implement strStr() (LC 28)

Problem

Find the first index of needle inside haystack, or -1.

Example

Input:  haystack="sadbutsad", needle="sad"
Output: 0

Constraints

Clarifying questions

Approach

KMP in O(n + m) time, O(m) space for lps.

Code (Python 3)

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        n, m = len(haystack), len(needle)
        if m == 0: return 0
        lps = [0] * m
        length = 0
        i = 1
        while i < m:
            if needle[i] == needle[length]:
                length += 1
                lps[i] = length
                i += 1
            elif length > 0:
                length = lps[length - 1]
            else:
                i += 1

        i = j = 0
        while i < n:
            if haystack[i] == needle[j]:
                i += 1; j += 1
                if j == m:
                    return i - j
            elif j > 0:
                j = lps[j - 1]
            else:
                i += 1
        return -1

Complexity

Comments


35.2 Shortest Palindrome (LC 214) — KMP version

Problem

Same as Chapter 34.4 but with KMP.

Example

Input:  s = "aacecaaa"
Output: "aaacecaaa"   (prepend the FEWEST chars so s becomes a palindrome)

Constraints

Clarifying questions

Approach

Construct combined = s + '#' + reverse(s). Compute lps of combined; lps[-1] is precisely the length of the longest prefix of s that is also a suffix of reverse(s) = the longest palindromic prefix.

Code (Python 3)

class Solution:
    def shortestPalindrome(self, s: str) -> str:
        combined = s + '#' + s[::-1]
        lps = [0] * len(combined)
        length = 0
        for i in range(1, len(combined)):
            while length > 0 and combined[i] != combined[length]:
                length = lps[length - 1]
            if combined[i] == combined[length]:
                length += 1
            lps[i] = length
        return s[lps[-1]:][::-1] + s

Complexity

Comments


35.3 Repeated Substring Pattern (LC 459)

Problem

Check whether s can be built by concatenating multiple copies of some substring.

Example

Input:  s = "abab"
Output: True
("abab" = "ab" repeated twice ⇒ True)

Constraints

Clarifying questions

Approach

KMP trick: if s = pattern * k (k ≥ 2), then lps[-1] > 0 and n - lps[-1] divides n.

Specifically: len(pattern) = n - lps[-1].

Code (Python 3)

class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        n = len(s)
        lps = [0] * n
        length = 0
        for i in range(1, n):
            while length > 0 and s[i] != s[length]:
                length = lps[length - 1]
            if s[i] == s[length]:
                length += 1
            lps[i] = length
        return lps[-1] > 0 and n % (n - lps[-1]) == 0

Complexity

Comments


35.4 Find All Anagrams in a String (LC 438)

Problem

Find every start index in s such that the length-|p| substring is an anagram of p.

Example

Input:  s="cbaebabacd", p="abc"
Output: [0, 6]

Constraints

Clarifying questions

Approach

Sliding window + Counter. KMP does not directly solve this — anagrams aren’t substring matches. Still listed because it belongs to the “string pattern” family.

Code (Python 3)

from collections import Counter
from typing import List

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        if len(s) < len(p): return []
        need = Counter(p)
        have = Counter(s[:len(p)])
        result = []
        if have == need:
            result.append(0)
        for i in range(len(p), len(s)):
            have[s[i]] += 1
            have[s[i - len(p)]] -= 1
            if have[s[i - len(p)]] == 0:
                del have[s[i - len(p)]]
            if have == need:
                result.append(i - len(p) + 1)
        return result

Complexity

Comments


35.5 Maximum Number of Occurrences of a Substring (LC 1297)

Problem

Given s, maxLetters, minSize, maxSize. Find the maximum occurrence count of a substring with length ∈ [minSize, maxSize] and distinct characters ≤ maxLetters.

Example

Input:  s="aababcaab", maxLetters=2, minSize=3, maxSize=4
Output: 2

Constraints

Clarifying questions

Approach

Trick: we only need to consider length = minSize (a longer substring can’t occur more often than its prefix of length minSize).

Sliding window + Counter.

Code (Python 3)

from collections import Counter, defaultdict

class Solution:
    def maxFreq(self, s: str, maxLetters: int, minSize: int, maxSize: int) -> int:
        freq: dict[str, int] = defaultdict(int)
        for i in range(len(s) - minSize + 1):
            sub = s[i:i + minSize]
            if len(set(sub)) <= maxLetters:
                freq[sub] += 1
        return max(freq.values(), default=0)

Complexity

Comments


35.6 Longest Happy Prefix (LC 1392)

Problem

A “happy prefix” is a non-trivial prefix that is also a suffix. Return the longest one.

Example

Input:  s = "level"
Output: "l"   (longest prefix of s that is also a suffix of s and NOT equal to s itself;
              empty string if none exists)

Constraints

Clarifying questions

Approach

Directly lps[-1] of s is the answer.

Code (Python 3)

class Solution:
    def longestPrefix(self, s: str) -> str:
        n = len(s)
        lps = [0] * n
        length = 0
        for i in range(1, n):
            while length > 0 and s[i] != s[length]:
                length = lps[length - 1]
            if s[i] == s[length]:
                length += 1
            lps[i] = length
        return s[:lps[-1]]

Complexity

Comments

Chapter summary & decisions

LPS trace — ababaca

i char j (prev LPS) lps[i]
0 a 0
1 b lps[0]=0; s[0]!=s[1] 0
2 a j=0; s[0]==s[2]j=1 1
3 b j=1; s[1]==s[3]j=2 2
4 a j=2; s[2]==s[4]j=3 3
5 c j=3; s[3]!=s[5]; fall back j=lps[2]=1; s[1]!=s[5]; fall back j=lps[0]=0; s[0]!=s[5] 0
6 a j=0; s[0]==s[6]j=1 1

lps = [0, 0, 1, 2, 3, 0, 1].

Sentinel safety

When concatenating P + '#' + T (the LC reference), # must not appear in the allowed charset. If the input may contain any Unicode character, use a pair of two sentinels or use the Z-function (Chapter 36) directly.

Find All Anagrams (LC 438) ≠ KMP

KMP vs Z (Chapter 36)

KMP Z
Preprocess LPS array of P Z array of P + '#' + T
Mindset “Failure → smart fall-back” “Match prefix at every index”
Implementation Failure function, 2 pointers One [l, r) box, easy off-by-one
Niche uses String periodicity, find pattern LCP, distinct substrings, Z-array properties

Chapter 36 — Z Function

The Z function of s: z[i] = the length of the longest segment starting at s[i] that is also a prefix of s. By convention z[0] = n. Computable in O(n).

Z-box invariant

The Z function uses the “Z-box” [l, r) = the most recent matched segment of s with the prefix of s. This is the key to seeing why it runs in O(n).

              [l ........... r)
s :  ┌───┐    ┌─────────────┐
     │ A │ ...│  B = prefix  │  ...
     └───┘    └─────────────┘
     prefix    matched with prefix
       │              │
       └──────────────┴── this segment = prefix of s, length r - l

When evaluating z[i] with i ∈ [l, r):
  • We already know z[i - l] (because s[l..r) = s[0..r-l) by alignment).
  • Lower bound: z[i] = min(r - i, z[i - l]).
  • Then extend by character comparison.

When i ≥ r:
  • No info → compare from s[0].

Update Z-box when a longer match is found (i + z[i] > r).

Trace with s = "aabxaabxaab":

i  l  r  z[i] computed from
0   0  0   11     (z[0] = n)
1   0  0   ?      i ≥ r, compare from start
              s[1]=a, s[0]=a → match. z[1]=1 (s[2]=b ≠ s[1]=a)
              Update Z-box: l=1, r=2.
2   1  2   ?      i = r, no info → compare from start.
              s[2]=b ≠ s[0]=a → z[2]=0.
3   1  2   0      Similarly, z[3]=0.
4   1  2   ?      i ≥ r, compare: s[4..]=aabxaab, prefix=aabxaab
              Match all 7 → z[4]=7. Update Z-box: l=4, r=11.
5   4 11   ?      i ∈ [4, 11). z[5] = min(11-5, z[5-4]) = min(6, z[1]) = min(6, 1) = 1.
              Extend: s[6]=b ≠ s[1]=a → stop. z[5]=1.
6   4 11   ?      z[6] = min(5, z[2]) = min(5, 0) = 0.
7   4 11   ?      z[7] = min(4, z[3]) = min(4, 0) = 0.
8   4 11   ?      z[8] = min(3, z[4]) = min(3, 7) = 3.
              Extend: i+z[i]=11=r → could exceed? s[11] out of bound. z[8]=3.
9   4 11   ?      z[9] = min(2, z[5]) = min(2, 1) = 1.
10  4 11   ?      z[10] = min(1, z[6]) = min(1, 0) = 0.

Final z: [11, 1, 0, 0, 7, 1, 0, 0, 3, 1, 0]

Amortised O(n) proof: every “compare extend” inside the while loop increases r. r is monotonically non-decreasing and ≤ n → total extensions ≤ n. Adding O(1) for each non-extending iteration → total O(n).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

def z_function(s: str) -> list[int]:
    n = len(s)
    z = [0] * n
    z[0] = n
    l = r = 0
    for i in range(1, n):
        if i < r:
            z[i] = min(r - i, z[i - l])
        while i + z[i] < n and s[z[i]] == s[i + z[i]]:
            z[i] += 1
        if i + z[i] > r:
            l, r = i, i + z[i]
    return z

End-of-chapter practice


36.1 Z-function — implementation & visualisation

Problem

Compute the z[] array for the string s. This is the building block every other Z-function problem relies on.

Example

Input:  s = "aabxaabxaab"
Output: [11, 1, 0, 0, 7, 1, 0, 0, 3, 1, 0]

Explanation of a few representative values (see the full trace in the "Z-box invariant" section):
  z[0] = 11   (convention: equals the string length)
  z[4] = 7    (s[4..10] = "aabxaab" matches the prefix "aabxaab")
  z[8] = 3    (s[8..10] = "aab" matches the prefix "aab")

Constraints

Clarifying questions

Approach

Maintain [l, r] = the most recent matched segment with the prefix. For each i: - If i < r: reuse the known z[i - l] to skip work. - Extend z[i] by comparing s[z[i]] with s[i + z[i]]. - Update [l, r] if the new matched segment exceeds the old one.

Amortised: each character is “extended” at most once → O(n).

Code (Python 3)

def z_function(s: str) -> list[int]:
    n = len(s)
    z = [0] * n
    z[0] = n
    l = r = 0
    for i in range(1, n):
        if i < r:
            z[i] = min(r - i, z[i - l])
        while i + z[i] < n and s[z[i]] == s[i + z[i]]:
            z[i] += 1
        if i + z[i] > r:
            l, r = i, i + z[i]
    return z

Intuition for the Z function with s = "aabcaabxaaaz":

i :  0  1  2  3  4  5  6  7  8  9 10 11
s :  a  a  b  c  a  a  b  x  a  a  a  z
z : 12  1  0  0  3  1  0  0  2  2  1  0

Complexity

Comments


36.2 Find the Index of the First Occurrence (LC 28) — Z version

Problem

Find the first index of needle in haystack, or -1. This chapter uses Z function instead of KMP (Chapter 35.1).

Example

Input:  haystack="sadbutsad", needle="sad"
Output: 0

Constraints

Clarifying questions

Approach

combined = pattern + '#' + text. Compute z. Find the first i with z[i] == len(pattern); return i - len(pattern) - 1 (position in text).

Code (Python 3)

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        if not needle: return 0
        combined = needle + '#' + haystack
        z = self.z_function(combined)
        target = len(needle)
        for i, v in enumerate(z):
            if v == target:
                return i - target - 1
        return -1

    @staticmethod
    def z_function(s: str) -> list[int]:
        n = len(s)
        z = [0] * n
        z[0] = n
        l = r = 0
        for i in range(1, n):
            if i < r:
                z[i] = min(r - i, z[i - l])
            while i + z[i] < n and s[z[i]] == s[i + z[i]]:
                z[i] += 1
            if i + z[i] > r:
                l, r = i, i + z[i]
        return z

Complexity

Comments


36.3 Sum of Scores of Built Strings (LC 2223) — recap

Fully solved in Chapter 34.6 (uses the Z function).

Insight

score(s_i) = z[n - len(s_i)] (matched-prefix length as we build). Summing gives sum(z[i] for i in range(1, n)) + n (counting z[0] = n).

Code (Python 3) (recap)

class Solution:
    def sumScores(self, s: str) -> int:
        n = len(s)
        z = [0] * n
        z[0] = n
        l = r = 0
        for i in range(1, n):
            if i < r:
                z[i] = min(r - i, z[i - l])
            while i + z[i] < n and s[z[i]] == s[i + z[i]]:
                z[i] += 1
            if i + z[i] > r:
                l, r = i, i + z[i]
        return sum(z)

Complexity


36.4 Maximum Deletions on a String (LC 2430)

Problem

Given s. Each operation chooses one of two moves:

  1. Pick an index i with 1 ≤ i ≤ len(s) / 2 such that s[0..i-1] == s[i..2i-1] (prefix of length i matches the following block). Delete the prefix of length i; the remaining string is s[i:].
  2. If no such i exists, delete the entire s (end).

Return the maximum number of operations until s is empty.

Example

Input:  s = "abcabcdabc"
Output: 2   (max deletions; each deletion removes a prefix equal to the next block)

Constraints

Clarifying questions

Approach

DP from right to left. dp[i] = max turns from s[i..].

Transition: dp[i] = max(1, max(dp[i + j] + 1 for j with s[i..i+j-1] == s[i+j..i+2j-1])).

Compare substrings quickly using the Z function or a precomputed LCP table.

Code (Python 3)

class Solution:
    def deleteString(self, s: str) -> int:
        n = len(s)
        # lcp[i][j] = LCP of s[i:] and s[j:].
        lcp = [[0] * (n + 1) for _ in range(n + 1)]
        for i in range(n - 1, -1, -1):
            for j in range(n - 1, -1, -1):
                if s[i] == s[j]:
                    lcp[i][j] = lcp[i + 1][j + 1] + 1
        dp = [1] * (n + 1)
        dp[n] = 0
        for i in range(n - 1, -1, -1):
            for j in range(1, (n - i) // 2 + 1):
                if lcp[i][i + j] >= j:
                    dp[i] = max(dp[i], dp[i + j] + 1)
        return dp[0]

Complexity

Comments


36.5 Match Substring After Replacement (LC 2301)

Problem

Given s, sub, and mappings[i] = [old, new] (each character old may be changed to new at most once when matching). Check whether sub (after applying any subset of mappings) is a substring of s.

Example

Input:  s="fool3e7bar", sub="leet", mappings=[["e","3"],["t","7"],["t","8"]]
Output: True

Constraints

Clarifying questions

Approach

Brute force O(n · m) with a smart check (each char matches if equal or via a mapping).

The Z function or KMP can be adapted — more complex.

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def matchReplacement(self, s: str, sub: str, mappings: List[List[str]]) -> bool:
        m: dict[str, set[str]] = defaultdict(set)
        for a, b in mappings:
            m[a].add(b)
        def matches(a: str, b: str) -> bool:
            return a == b or b in m[a]
        n, ns = len(s), len(sub)
        for i in range(n - ns + 1):
            if all(matches(sub[j], s[i + j]) for j in range(ns)):
                return True
        return False

Complexity

Comments


36.6 Distinct Echo Substrings (LC 1316) — recap

Same as problem 34.3, but uses the Z function instead of rolling hash to avoid collisions.

Problem

Count the number of distinct substrings of the form a + a.

Example

Input:  text = "aaaa"
Output: 1   (the only distinct echo substring is "aa"; "aaaa" is a+a but counted distinctly)

Constraints

Clarifying questions

Approach

For each i, compute z on s[i:]. Find j with z[j] >= j (= an echo of length j at position i). Track distinct echoes using a set of (start_i, len).

Code (Python 3)

class Solution:
    def distinctEchoSubstrings(self, s: str) -> int:
        def z_function(t: str) -> list[int]:
            n = len(t)
            z = [0] * n
            z[0] = n
            l = r = 0
            for i in range(1, n):
                if i < r:
                    z[i] = min(r - i, z[i - l])
                while i + z[i] < n and t[z[i]] == t[i + z[i]]:
                    z[i] += 1
                if i + z[i] > r:
                    l, r = i, i + z[i]
            return z

        seen: set[tuple[int, int]] = set()
        n = len(s)
        for i in range(n):
            z = z_function(s[i:])
            for j in range(1, len(z)):
                if z[j] >= j:
                    seen.add((i, j))   # echo "aa" with each a = s[i..i+j-1]
        # Filter by content uniqueness.
        return len({s[i:i + 2 * j] for i, j in seen})

O(n²) worst case.

Complexity

Chapter summary & decisions

Z-box visualisation — aabcaabxaaaz

i:    0 1 2 3 4 5 6 7 8 9 10 11
s:    a a b c a a b x a a  a  z
Z:    – 1 0 0 3 1 0 0 2 1  1  0

The box [l, r) is the currently-matched prefix region. For each i ∈ [l, r): - If i + Z[i-l] < r: copy Z[i] = Z[i-l]. - Otherwise: brute-extend from Z[i] = r - i.

Update the box when extending succeeds:

i = 4: no box, brute: s[4..]=aabxaaaz vs s[0..]=aabcaab... ⇒ matches 3 ⇒ Z[4]=3, [l,r)=[4,7)
i = 5: i-l=1, Z[1]=1, i+Z[1]=6 < 7 ⇒ Z[5] = 1 (copy)
i = 6: i-l=2, Z[2]=0, 6+0=6 < 7 ⇒ Z[6] = 0
i = 7: outside box, brute: Z[7]=0
i = 8: brute, match aaz vs aab → 2 ⇒ Z[8]=2, [l,r)=[8,10)

Maximum Deletions (LC 2430) — LCP DP, not pure Z

Match Replacement contrast

KMP/Z assume equality comparison. When the matching relation is not symmetric (e.g. constrained wildcards), the failure function cannot be reused — DP is needed instead.

Distinct Echo recap

Full solution in Chapter 34 (rolling hash). Recapping here to contrast: Z-function can count an echo s[i:i+L] == s[i+L:i+2L] by checking Z[i+L] ≥ L.


Chapter 37 — Binary Search combined with Graph Traversal

When a problem looks like “min of max” or “max of min” on a graph/grid, we can binary-search the answer + check feasibility via BFS/DFS. The pattern shines when we want a simple O((V + E) · log range) solution instead of a fancier Dijkstra or MST.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

def search_on_answer_graph(lo, hi, can_reach):
    while lo < hi:
        mid = (lo + hi) // 2
        if can_reach(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

End-of-chapter practice


37.1 Swim in Rising Water (LC 778) — recap

Solved two ways (offline DSU, Dijkstra) in Chapters 24.6 and 30.4. Here is a third way: binary search on t + BFS check.

Problem

An n × n elevation grid. At time t, every cell with elevation ≤ t is covered with water. Find the smallest t such that you can swim from (0,0) to (n-1,n-1).

Approach

Binary search on t. For each t, BFS to check whether a path exists through cells of elevation ≤ t. The predicate is monotonic: a larger t lets us reach more cells → it’s easier to find a path.

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def swimInWater(self, grid: List[List[int]]) -> int:
        n = len(grid)
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]

        def can_reach(t: int) -> bool:
            if grid[0][0] > t: return False
            visited = {(0, 0)}
            queue = deque([(0, 0)])
            while queue:
                r, c = queue.popleft()
                if (r, c) == (n - 1, n - 1): return True
                for dr, dc in DIRS:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] <= t and (nr, nc) not in visited:
                        visited.add((nr, nc))
                        queue.append((nr, nc))
            return False

        lo, hi = 0, n * n - 1
        while lo < hi:
            mid = (lo + hi) // 2
            if can_reach(mid): hi = mid
            else: lo = mid + 1
        return lo

Complexity


37.2 Path With Minimum Effort (LC 1631) — recap

Solved via Dijkstra in Chapter 30.2. BS version: binary search on effort, BFS check.

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        rows, cols = len(heights), len(heights[0])
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]

        def can_reach(e: int) -> bool:
            visited = {(0, 0)}
            queue = deque([(0, 0)])
            while queue:
                r, c = queue.popleft()
                if (r, c) == (rows - 1, cols - 1): return True
                for dr, dc in DIRS:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in visited:
                        if abs(heights[nr][nc] - heights[r][c]) <= e:
                            visited.add((nr, nc))
                            queue.append((nr, nc))
            return False

        lo, hi = 0, 10**6
        while lo < hi:
            mid = (lo + hi) // 2
            if can_reach(mid): hi = mid
            else: lo = mid + 1
        return lo

Complexity


37.3 Last Day Where You Can Still Cross (LC 1970)

Problem

A grid row × col. Each day one water cell cells[i] becomes wet. Find the last day on which you can still cross from row 0 to row n-1 over land cells.

Example

Input:  row=2, col=2, cells=[[1,1],[2,1],[1,2],[2,2]]
Output: 2

Constraints

Clarifying questions

Approach

Binary search on d + BFS/DFS check: after d days, is the grid still passable?

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def latestDayToCross(self, row: int, col: int, cells: List[List[int]]) -> int:

        def can_cross(d: int) -> bool:
            grid = [[0] * col for _ in range(row)]
            for i in range(d):
                r, c = cells[i]
                grid[r - 1][c - 1] = 1   # water
            queue = deque()
            visited = set()
            for c in range(col):
                if grid[0][c] == 0:
                    queue.append((0, c))
                    visited.add((0, c))
            DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
            while queue:
                r, c = queue.popleft()
                if r == row - 1: return True
                for dr, dc in DIRS:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < row and 0 <= nc < col and grid[nr][nc] == 0 and (nr, nc) not in visited:
                        visited.add((nr, nc))
                        queue.append((nr, nc))
            return False

        lo, hi = 0, len(cells)
        while lo < hi:
            mid = (lo + hi + 1) // 2
            if can_cross(mid): lo = mid
            else: hi = mid - 1
        return lo

Complexity

Comments


37.4 Trapping Rain Water II (LC 407)

Problem

2D extension of Trapping Rain Water. Compute the total water trapped on a heights grid.

Example

Input:  heightMap = [[1,4,3,1,3,2],
                     [3,2,1,3,2,4],
                     [2,3,3,2,3,1]]   (m × n grid; each cell is a column height)
Output: 4   (total trapped water after rain)

Constraints

Clarifying questions

Approach

Dijkstra/BFS from the border with a min-heap: instead of binary search, pop the lowest border cell. Each pop raises the water level for its neighbours.

Code (Python 3)

import heapq
from typing import List

class Solution:
    def trapRainWater(self, heights: List[List[int]]) -> int:
        rows, cols = len(heights), len(heights[0])
        visited = [[False] * cols for _ in range(rows)]
        heap = []
        for r in range(rows):
            for c in range(cols):
                if r == 0 or r == rows - 1 or c == 0 or c == cols - 1:
                    heapq.heappush(heap, (heights[r][c], r, c))
                    visited[r][c] = True
        water = 0
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
        while heap:
            h, r, c = heapq.heappop(heap)
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc]:
                    visited[nr][nc] = True
                    water += max(0, h - heights[nr][nc])
                    heapq.heappush(heap, (max(h, heights[nr][nc]), nr, nc))
        return water

Complexity

Comments


37.5 Minimize the Maximum of Two Arrays (LC 2513)

Problem

Input: four integers divisor1, divisor2, uniqueCnt1, uniqueCnt2.

We need to build two arrays arr1 (length uniqueCnt1) and arr2 (length uniqueCnt2) of positive integers such that: - Every element of arr1 is not divisible by divisor1. - Every element of arr2 is not divisible by divisor2. - arr1 ∪ arr2 consists of distinct values (no duplicates between arrays).

Return the smallest possible maximum value in arr1 ∪ arr2.

Example

Input:  divisor1 = 2, divisor2 = 7, uniqueCnt1 = 1, uniqueCnt2 = 3
Output: 4

Explanation:
  arr1 = [1]         (1 not divisible by 2)
  arr2 = [2, 3, 4]   (none divisible by 7)
  max(arr1 ∪ arr2) = 4, minimised.

Constraints

Clarifying questions

Approach

Binary search the answer X: - Numbers in [1, X] not divisible by d1: X - X // d1. - Similarly for d2. - Only divisible by lcm(d1, d2): shared budget — decide who takes what.

Inclusion-exclusion + binary search.

Code (Python 3)

from math import lcm

class Solution:
    def minimizeSet(self, divisor1: int, divisor2: int, uniqueCnt1: int, uniqueCnt2: int) -> int:
        L = lcm(divisor1, divisor2)

        def enough(x: int) -> bool:
            # Numbers ≤ x divisible by d1: x // d1; by d2: x // d2; by L: x // L
            available1 = x - x // divisor1
            available2 = x - x // divisor2
            available_both = x - x // L
            # arr1 needs available1 for itself but may share with arr2.
            need1 = max(0, uniqueCnt1 - (available1 - (x // divisor2 - x // L)))
            # ... brain teaser; the formula version is cleaner.
            shared = available_both
            return available1 >= uniqueCnt1 and available2 >= uniqueCnt2 and \
                   shared >= max(0, uniqueCnt1 - (x - x // divisor1 - (x // divisor2 - x // L))) + \
                              max(0, uniqueCnt2 - (x - x // divisor2 - (x // divisor1 - x // L)))

        lo, hi = 1, 10**11
        while lo < hi:
            mid = (lo + hi) // 2
            if enough(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Complexity

Comments


37.6 Find a Peak Element II (LC 1901)

Problem

A matrix. A peak is a cell greater than its 4 neighbours. Return [r, c] of any peak. O(m log n).

Example

Input:  mat = [[1, 4],
               [3, 2]]   (all values distinct; boundary treated as -∞)
Output: [0, 1]   (one valid peak: larger than 4 neighbours; multiple may exist, return any)

Constraints

Clarifying questions

Approach

Binary search on columns. Pick the middle column, find the max in it → that row “owns the peak”. Compare with the two adjacent columns: if the max < the left column → the peak lies on the left; similarly for the right.

Code (Python 3)

from typing import List

class Solution:
    def findPeakGrid(self, mat: List[List[int]]) -> List[int]:
        rows, cols = len(mat), len(mat[0])
        lo, hi = 0, cols - 1
        while lo <= hi:
            mid = (lo + hi) // 2
            max_row = max(range(rows), key=lambda r: mat[r][mid])
            left = mat[max_row][mid - 1] if mid > 0 else -1
            right = mat[max_row][mid + 1] if mid < cols - 1 else -1
            if mat[max_row][mid] > left and mat[max_row][mid] > right:
                return [max_row, mid]
            if left > mat[max_row][mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        return [-1, -1]

Complexity

Comments

Chapter summary & decisions

BS + Graph template

def solve():
    lo, hi = min_possible, max_possible
    while lo < hi:
        mid = (lo + hi) // 2
        if can_reach(mid):     # reachability with threshold/capacity mid
            hi = mid
        else:
            lo = mid + 1
    return lo

Predicate table (this chapter)

Problem Answer can(x) Monotonic
Path with Min Effort (LC 1631) max edge diff BFS through edges ≤ x True as x ↗
Swim in Rising Water (LC 778) min threshold BFS through cells ≤ x True as x ↗
Min Bridges (LC 1102) max path min DFS through cells ≥ x True as x ↘
Aggressive Cows / Magnetic Force distance Greedy placement True as x ↘

vs Dijkstra / DSU

Trapping Rain Water II (LC 407) — why this chapter?

At its core this is Dijkstra-like with a priority queue (pick the lowest boundary cell next). Placed in Chapter 37 to contrast “BS + Graph” with “Min-heap traversal” — both belong to the “answer = threshold” family.


Chapter 38 — Binary Search combined with Dynamic Programming

When a DP transition needs to “quickly find the optimal value across already-considered states”, binary searching into the prefix optimum drops O(n²) to O(n log n). The pattern appears in LIS, Russian Doll Envelopes, Constrained Subsequence Sum.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

from bisect import bisect_left, bisect_right

# DP with binary search lookup
tails = []
for x in arr:
    idx = bisect_left(tails, x)
    if idx == len(tails):
        tails.append(x)
    else:
        tails[idx] = x

End-of-chapter practice


38.1 Russian Doll Envelopes (LC 354) — recap

Fully solved in Chapter 29.6. Sort 2D + LIS with binary search.

This is the gateway to BS + DP: after sorting 2D, the LIS transition (tails[bisect_left(tails, x)] = x) is literally binary search over the DP structure.

Code (Python 3) (recap)

from bisect import bisect_left
from typing import List

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        envelopes.sort(key=lambda e: (e[0], -e[1]))
        tails: list[int] = []
        for _, h in envelopes:
            i = bisect_left(tails, h)
            if i == len(tails):
                tails.append(h)
            else:
                tails[i] = h
        return len(tails)

Complexity


38.2 Longest Increasing Subsequence II (LC 2407)

Problem

LIS with the constraint nums[i+1] - nums[i] <= k. Find the maximum length.

Example

Input:  nums=[4,2,1,4,3,4,5,8,15], k=3
Output: 5

Constraints

Clarifying questions

Approach

Segment Tree with max query (range max over dp values).

dp[v] = max LIS ending at value v. Transition: dp[v] = max(dp[v-k..v-1]) + 1.

A segment tree supports the range-max query in O(log V) per step. Total O(n log V).

Code (Python 3)

from typing import List

class Solution:
    def lengthOfLIS(self, nums: List[int], k: int) -> int:
        n = max(nums)
        size = 1
        while size < n + 1: size <<= 1
        tree = [0] * (2 * size)

        def update(i: int, val: int):
            i += size
            tree[i] = max(tree[i], val)
            while i > 1:
                i //= 2
                tree[i] = max(tree[2*i], tree[2*i+1])

        def query(l: int, r: int) -> int:
            res = 0
            l += size; r += size + 1
            while l < r:
                if l & 1: res = max(res, tree[l]); l += 1
                if r & 1: r -= 1; res = max(res, tree[r])
                l //= 2; r //= 2
            return res

        best = 0
        for x in nums:
            lo = max(1, x - k)
            cur = query(lo, x - 1) + 1
            update(x, cur)
            best = max(best, cur)
        return best

Complexity

Comments


38.3 Number of Longest Increasing Subsequence (LC 673)

Problem

Count the number of LISes of nums.

Example

Input:  nums = [1, 3, 5, 4, 7]
Output: 2   (longest LISes: [1,3,5,7] and [1,3,4,7])

Constraints

Clarifying questions

Approach

O(n²) DP: dp[i] = (length, count) — length of the LIS ending at i and its count.

An O(n log n) version with a segment tree exists.

Code (Python 3)

from typing import List

class Solution:
    def findNumberOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        length = [1] * n
        count = [1] * n
        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]
                    elif length[j] + 1 == length[i]:
                        count[i] += count[j]
        max_len = max(length)
        return sum(c for l, c in zip(length, count) if l == max_len)

Complexity

Comments


38.4 Max Sum of Rectangle No Larger Than K (LC 363)

Problem

A matrix. Find the submatrix with maximum sum ≤ k.

Example

Input:  matrix=[[1,0,1],[0,-2,3]], k=2
Output: 2

Constraints

Clarifying questions

Approach

Fix two columns (c1, c2). Row sums between them → 1D array. Find the subarray sum closest to k but ≤ k: prefix sum + SortedList + binary search.

Code (Python 3)

from sortedcontainers import SortedList
from typing import List

class Solution:
    def maxSumSubmatrix(self, matrix: List[List[int]], k: int) -> int:
        rows, cols = len(matrix), len(matrix[0])
        result = -float('inf')
        for c1 in range(cols):
            row_sums = [0] * rows
            for c2 in range(c1, cols):
                for r in range(rows):
                    row_sums[r] += matrix[r][c2]
                # Find the largest subarray sum ≤ k.
                sl = SortedList([0])
                prefix = 0
                for v in row_sums:
                    prefix += v
                    # Find prefix' ≥ prefix - k → subarray sum = prefix - prefix' ≤ k.
                    idx = sl.bisect_left(prefix - k)
                    if idx < len(sl):
                        result = max(result, prefix - sl[idx])
                    sl.add(prefix)
        return result

Complexity

Comments


38.5 Constrained Subsequence Sum (LC 1425)

Problem

Given nums and k, find the max sum of a subsequence such that consecutive chosen indices differ by ≤ k.

Example

Input:  nums=[10,2,-10,5,20], k=2
Output: 37

Constraints

Clarifying questions

Approach

DP dp[i] = nums[i] + max(0, max(dp[i-k..i-1])).

The max(dp[i-k..i-1]) is computed via sliding window max (Chapter 18.4) → O(n).

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def constrainedSubsetSum(self, nums: List[int], k: int) -> int:
        n = len(nums)
        dp = [0] * n
        dq: deque[int] = deque()
        best = -float('inf')
        for i in range(n):
            window_max = dp[dq[0]] if dq else 0
            dp[i] = nums[i] + max(0, window_max)
            best = max(best, dp[i])
            while dq and dp[dq[-1]] <= dp[i]:
                dq.pop()
            dq.append(i)
            if dq[0] == i - k:
                dq.popleft()
        return best

Complexity

Comments


38.6 Allocate Mailboxes (LC 1478)

Problem

Given houses (positions). Place k mailboxes minimising the sum of distance(house → nearest mailbox).

Example

Input:  houses=[1,4,8,10,20], k=3
Output: 5

Constraints

Clarifying questions

Approach

DP: dp[i][j] = min cost using j mailboxes for houses[:i].

Transition: dp[i][j] = min(dp[p][j-1] + cost(houses[p:i])) where cost is the total distance from placing 1 mailbox on the range — = sum |x - median|.

O(k · n²). Can be sped up with divide-conquer DP optimisation → O(k · n log n).

Code (Python 3)

from typing import List

class Solution:
    def minDistance(self, houses: List[int], k: int) -> int:
        houses.sort()
        n = len(houses)
        # cost[l][r] = cost of placing 1 mailbox for houses[l..r]
        cost = [[0] * n for _ in range(n)]
        for l in range(n):
            for r in range(l + 1, n):
                mid = houses[(l + r) // 2]
                cost[l][r] = sum(abs(houses[i] - mid) for i in range(l, r + 1))

        INF = float('inf')
        dp = [[INF] * (k + 1) for _ in range(n + 1)]
        dp[0][0] = 0
        for i in range(1, n + 1):
            for j in range(1, k + 1):
                for p in range(j - 1, i):
                    dp[i][j] = min(dp[i][j], dp[p][j - 1] + cost[p][i - 1])
        return dp[n][k]

Complexity

Comments

Chapter summary & decisions

“BS as a lookup inside DP/Optimization”

Range max query — Segment Tree

LC 2407 (Longest Increasing Subsequence II): plain patience doesn’t work because of the ≤ k gap constraint ⇒ we need a segment tree lookup over dp[v - k .. v - 1].

Max Sum Rectangle ≤ K (LC 363)

Allocate Mailboxes (LC 1478) — median cost precompute


Chapter 39 — Sorting combined with Dynamic Programming

Sort + DP is a powerful combination when item order matters and DP proceeds in that order. A common pattern: sort by one attribute, then LIS-like DP on another.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

End-of-chapter practice


39.1 Largest Divisible Subset (LC 368)

Problem

Given nums (distinct). Find the largest subset such that for every pair (a, b) either a % b == 0 or b % a == 0.

Example

Input:  nums = [1, 2, 4, 8]
Output: [1, 2, 4, 8]   (largest subset where every pair is divisible)

Constraints

Clarifying questions

Approach

Sort ascending. dp[i] = largest subset ending at nums[i].

dp[i] = max(dp[j] + 1 for j < i if nums[i] % nums[j] == 0).

Track parent[] to reconstruct the subset.

Code (Python 3)

from typing import List

class Solution:
    def largestDivisibleSubset(self, nums: List[int]) -> List[int]:
        nums.sort()
        n = len(nums)
        dp = [1] * n
        parent = [-1] * n
        best_idx = 0
        for i in range(n):
            for j in range(i):
                if nums[i] % nums[j] == 0 and dp[j] + 1 > dp[i]:
                    dp[i] = dp[j] + 1
                    parent[i] = j
            if dp[i] > dp[best_idx]:
                best_idx = i
        result = []
        while best_idx != -1:
            result.append(nums[best_idx])
            best_idx = parent[best_idx]
        return result[::-1]

Complexity

Comments


39.2 Russian Doll Envelopes (LC 354) — recap

Fully solved in Chapter 29.6. Sort 2D + LIS with binary search.

The “sort 2D + LIS on the other dimension” pattern reappears in 39.3 (Stacking Cuboids, 3D), 39.5 (Visible People), and beyond.

Code (Python 3) (recap)

from bisect import bisect_left
from typing import List

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        envelopes.sort(key=lambda e: (e[0], -e[1]))
        tails: list[int] = []
        for _, h in envelopes:
            i = bisect_left(tails, h)
            if i == len(tails):
                tails.append(h)
            else:
                tails[i] = h
        return len(tails)

Complexity


39.3 Maximum Height by Stacking Cuboids (LC 1691)

Problem

Given cuboids[i] = [w, l, h]. Each cuboid can be rotated (any of the three dimensions can serve as height). Cuboid i can be stacked on j if every dimension of i ≤ every dimension of j. Find the max total height.

Example

Input:  cuboids = [[50,45,20], [95,37,53], [45,23,12]]
        (each cuboid [w, l, h]; rotations allowed; stack if w1≤w2, l1≤l2, h1≤h2)
Output: 190   (the maximum stack height)

Constraints

Clarifying questions

Approach

Insight: sort each cuboid’s three dimensions → cuboid A can stack on B ↔︎ A’s sorted dims ≤ B’s sorted dims (after sorting all cuboids ascending).

Then the problem becomes a 3D LIS.

Code (Python 3)

from typing import List

class Solution:
    def maxHeight(self, cuboids: List[List[int]]) -> int:
        for c in cuboids:
            c.sort()
        cuboids.sort()
        n = len(cuboids)
        dp = [c[2] for c in cuboids]
        for i in range(n):
            for j in range(i):
                if all(cuboids[j][k] <= cuboids[i][k] for k in range(3)):
                    dp[i] = max(dp[i], dp[j] + cuboids[i][2])
        return max(dp)

Complexity

Comments


39.4 Maximum Profit in Job Scheduling (LC 1235)

Problem

Given jobs = [(start, end, profit)]. Pick non-overlapping jobs to maximise total profit.

Example

Input:  startTime=[1,2,3,3], endTime=[3,4,5,6], profit=[50,10,40,70]
Output: 120

Constraints

Clarifying questions

Approach

Sort by end. dp[i] = max profit using jobs [0..i].

dp[i] = max(dp[i-1], dp[k] + jobs[i].profit) where k = the last job with end <= jobs[i].start.

Binary search to find kO(n log n).

Code (Python 3)

from bisect import bisect_right
from typing import List

class Solution:
    def jobScheduling(self, startTime: List[int], endTime: List[int], profit: List[int]) -> int:
        jobs = sorted(zip(endTime, startTime, profit))
        ends = [j[0] for j in jobs]
        dp = [0] * (len(jobs) + 1)
        for i, (end, start, p) in enumerate(jobs):
            k = bisect_right(ends, start)
            dp[i + 1] = max(dp[i], dp[k] + p)
        return dp[-1]

Complexity

Comments


39.5 Number of Visible People in a Queue (LC 1944)

Problem

Given heights. Each person sees the people behind them in the queue until they hit someone taller than themselves or taller than the one they just saw. Count visible people per person.

Example

Input:  heights = [10, 6, 8, 5, 11, 9]
        (heights[i] = height of the i-th person in the queue; all distinct)
Output: [3, 1, 2, 1, 1, 0]
        (answer[i] = number of people on the right that the i-th person can see)

Constraints

Clarifying questions

Approach

Monotonic stack from right to left. For each person: pop everyone shorter (count them), and add 1 if the stack still has someone taller.

Code (Python 3)

from typing import List

class Solution:
    def canSeePersonsCount(self, heights: List[int]) -> List[int]:
        n = len(heights)
        result = [0] * n
        stack: list[int] = []
        for i in range(n - 1, -1, -1):
            while stack and heights[i] > stack[-1]:
                stack.pop()
                result[i] += 1
            if stack:
                result[i] += 1
            stack.append(heights[i])
        return result

Complexity

Comments


39.6 Minimum Number of Taps to Open to Water a Garden (LC 1326)

Problem

A garden of length n. Tap i at position i waters [i - r, i + r]. Find the minimum number of taps that fully cover the garden.

Example

Input:  n=5, ranges=[3,4,1,1,0,0]
Output: 1

Constraints

Clarifying questions

Approach

Reduce to Jump Game II (16.2): for each position i, farthest[i] = the farthest position reachable starting from i. Greedy.

Code (Python 3)

from typing import List

class Solution:
    def minTaps(self, n: int, ranges: List[int]) -> int:
        farthest = [0] * (n + 1)
        for i, r in enumerate(ranges):
            left = max(0, i - r)
            right = min(n, i + r)
            farthest[left] = max(farthest[left], right)
        taps = 0
        current_end = 0
        next_end = 0
        for i in range(n + 1):
            if i > next_end:
                return -1
            if i > current_end:
                taps += 1
                current_end = next_end
            next_end = max(next_end, farthest[i])
        return taps

Complexity

Comments

Chapter summary & decisions

“Sort as preprocessing” — broader than just DP

This chapter collects problems whose first step is sort, followed by: - DP (Russian Doll, Job Scheduling). - Monotonic stack (LC 1944 Visible People — standalone but order-dependent). - Greedy intervals (LC 1326 Minimum Taps).

The chapter label is broader than rigid “Sort + DP”; think of it as “Sort preprocessing patterns”.

Russian Doll Envelopes (LC 354) — different lenses

Job Scheduling (LC 1235) — timeline + previous-compatible

jobs sorted by endTime: J1=[1,3,50], J2=[2,4,10], J3=[3,5,40], J4=[3,6,70]
                                ↑           ↑           ↑
                              end=3        end=4       end=5,6

dp[i] = max(dp[i-1], jobs[i].profit + dp[prev(i)])
prev(i) = last job with endTime ≤ jobs[i].startTime  ← binary search!

Visible People In Queue (LC 1944) — monotonic stack

Minimum Taps (LC 1326) — greedy intervals


Chapter 40 — Bitmask Dynamic Programming

When the state is a subset of a small set (≤ 20 elements), we encode the subset as a 32-bit int bitmask and DP over the bitmask. The state space is O(2^n). The pattern powers TSP-style, set cover, and partition problems.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

from functools import cache

@cache
def dp(mask, *extra_state):
    if mask == 0:        # base case (or full = (1 << n) - 1)
        return base_value
    return best([dp(mask ^ (1 << i), ...) for i in iterate_bits(mask)])

End-of-chapter practice


40.1 Partition to K Equal Sum Subsets (LC 698)

Problem

Given nums and k. Can we partition into k subsets with equal sums?

Example

Input:  nums=[4,3,2,3,5,2,1], k=4
Output: True

Constraints

Clarifying questions

Approach

dp[mask] = boolean “subset indicated by bitmask can be partitioned into valid groups”.

total = sum(nums). Each subset must sum to total / k.

Walk: for every mask, if dp[mask] is True and current_subset_sum % target == 0, try adding unused elements.

Code (Python 3)

from functools import cache
from typing import List

class Solution:
    def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
        total = sum(nums)
        if total % k: return False
        target = total // k
        nums.sort(reverse=True)
        if nums[0] > target: return False
        n = len(nums)

        @cache
        def dfs(mask: int, current_sum: int) -> bool:
            if mask == (1 << n) - 1:
                return True
            for i in range(n):
                if mask & (1 << i): continue
                new_sum = current_sum + nums[i]
                if new_sum > target: continue
                next_sum = new_sum if new_sum < target else 0
                if dfs(mask | (1 << i), next_sum):
                    return True
            return False

        return dfs(0, 0)

Complexity

Comments


40.2 Shortest Path Visiting All Nodes (LC 847)

Problem

Input: graph: List[List[int]] — adjacency list of a connected undirected graph with n nodes (labelled 0..n-1). Find the shortest path (edge count) that visits every node; you may start and end anywhere, and revisit nodes.

Example

Input:  graph = [[1,2,3], [0], [0], [0]]
        (undirected adjacency list; graph[i] = neighbours of node i; start/end anywhere)
Output: 4   (shortest edge count visiting every node; revisits allowed)

Constraints

Clarifying questions

Approach

BFS with state (node, visited_mask). The answer is the first step count that reaches a state (_, full_mask).

Code (Python 3)

from collections import deque
from typing import List

class Solution:
    def shortestPathLength(self, graph: List[List[int]]) -> int:
        n = len(graph)
        full = (1 << n) - 1
        if n == 1: return 0
        queue = deque((i, 1 << i, 0) for i in range(n))   # (node, mask, dist)
        visited = {(i, 1 << i) for i in range(n)}
        while queue:
            node, mask, dist = queue.popleft()
            if mask == full: return dist
            for nb in graph[node]:
                new_mask = mask | (1 << nb)
                if (nb, new_mask) not in visited:
                    visited.add((nb, new_mask))
                    queue.append((nb, new_mask, dist + 1))
        return -1

Complexity

Comments


40.3 Smallest Sufficient Team (LC 1125)

Problem

Given req_skills and people[i] = list of skill. Find the smallest subset of people covering every required skill.

Example

Input:  req_skills=["java","nodejs","reactjs"], people=[["java"],["nodejs"],["nodejs","reactjs"]]
Output: [0,2]

Constraints

Clarifying questions

Approach

Bitmask DP over skills. dp[mask] = smallest team covering skills in mask.

dp[mask | skills_of_p] = min(dp[mask | skills_of_p], dp[mask] + [p]).

Code (Python 3)

from typing import List

class Solution:
    def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
        skill_idx = {s: i for i, s in enumerate(req_skills)}
        n = len(req_skills)
        full = (1 << n) - 1
        people_mask = []
        for p in people:
            mask = 0
            for s in p:
                if s in skill_idx:
                    mask |= 1 << skill_idx[s]
            people_mask.append(mask)

        dp: dict[int, list[int]] = {0: []}
        for i, mask in enumerate(people_mask):
            for cur_mask, team in list(dp.items()):
                new_mask = cur_mask | mask
                if new_mask == cur_mask: continue
                if new_mask not in dp or len(team) + 1 < len(dp[new_mask]):
                    dp[new_mask] = team + [i]
        return dp[full]

Complexity

Comments


40.4 Find the Shortest Superstring (LC 943)

Problem

Given an array of strings, find the shortest string that contains all of them as substrings.

Example

Input:  words = ["alex", "loves", "leetcode"]
Output: "alexlovesleetcode"   (shortest string containing every word as substring;
                               multiple answers may exist)

Constraints

Clarifying questions

Approach

Bitmask DP combined with TSP. State (mask, last_string_idx) = shortest string visiting set mask and ending at last_idx.

Precompute overlap[i][j] = max overlap when concatenating words[i] + words[j].

Code (Python 3)

from typing import List

class Solution:
    def shortestSuperstring(self, words: List[str]) -> str:
        n = len(words)
        # overlap[i][j] = max k such that words[i] ends with the length-k prefix of words[j].
        overlap = [[0] * n for _ in range(n)]
        for i in range(n):
            for j in range(n):
                if i != j:
                    for k in range(min(len(words[i]), len(words[j])), 0, -1):
                        if words[i].endswith(words[j][:k]):
                            overlap[i][j] = k
                            break

        INF = float('inf')
        # dp[mask][i] = length of the shortest superstring visiting set mask, ending at i.
        dp = [[INF] * n for _ in range(1 << n)]
        parent = [[-1] * n for _ in range(1 << n)]
        for i in range(n):
            dp[1 << i][i] = len(words[i])

        for mask in range(1 << n):
            for i in range(n):
                if not (mask & (1 << i)) or dp[mask][i] == INF:
                    continue
                for j in range(n):
                    if mask & (1 << j):
                        continue
                    new_mask = mask | (1 << j)
                    new_len = dp[mask][i] + len(words[j]) - overlap[i][j]
                    if new_len < dp[new_mask][j]:
                        dp[new_mask][j] = new_len
                        parent[new_mask][j] = i

        # Trace path: find end-state with minimum length.
        full = (1 << n) - 1
        last = min(range(n), key=lambda i: dp[full][i])

        # Reconstruct order backwards.
        order: list[int] = []
        mask = full
        cur = last
        while cur != -1:
            order.append(cur)
            prev = parent[mask][cur]
            mask ^= 1 << cur
            cur = prev
        order.reverse()

        # Build the superstring.
        result = words[order[0]]
        for idx in range(1, len(order)):
            prev_i, cur_i = order[idx - 1], order[idx]
            result += words[cur_i][overlap[prev_i][cur_i]:]
        return result

Complexity

Comments


40.5 Maximum Students Taking Exam (LC 1349)

Problem

An exam room m × n. seats[i][j] = . (OK) or # (broken). Two students cannot sit immediately left/right of each other or diagonally adjacent (they could copy). Maximise the number of students seated.

Example

Input:  seats = [["#",".","#","#",".","#"],
                 [".","#","#","#","#","."],
                 ["#",".","#","#",".","#"]]
        ("." = OK seat, "#" = broken; students can see neighbours in 4 diagonals + sides)
Output: 4   (max students placed without cheating)

Constraints

Clarifying questions

Approach

Row DP bitmask. dp[i][mask] = max in row i with students seated according to mask.

Each mask must be valid (no two adjacent bits) and not overlap broken cells.

Transition: dp[i][mask] = max(dp[i-1][prev]) + popcount(mask) for prev not diagonally conflicting with mask.

Code (Python 3)

from typing import List

class Solution:
    def maxStudents(self, seats: List[List[str]]) -> int:
        m, n = len(seats), len(seats[0])
        # row_bad[i] = bitmask of '#' cells in row i.
        row_bad = [0] * m
        for i in range(m):
            for j in range(n):
                if seats[i][j] == '#':
                    row_bad[i] |= 1 << j

        # Valid masks: no two adjacent bits.
        valid = [m for m in range(1 << n) if (m & (m << 1)) == 0]

        dp = {0: 0}     # mask -> max students
        for i in range(m):
            new_dp = {}
            for mask in valid:
                if mask & row_bad[i]: continue
                cnt = bin(mask).count('1')
                best = 0
                for prev, prev_cnt in dp.items():
                    if (mask & (prev << 1)) or (mask & (prev >> 1)): continue
                    best = max(best, prev_cnt)
                new_dp[mask] = best + cnt
            dp = new_dp
        return max(dp.values(), default=0)

Complexity

Comments


40.6 Minimum XOR Sum of Two Arrays (LC 1879)

Problem

Two arrays of length n. Permute nums2 to minimise Σ nums1[i] ^ nums2[π(i)].

Example

Input:  nums1=[1,2], nums2=[2,3]
Output: 2

Constraints

Clarifying questions

Approach

Bitmask DP in O(n · 2^n). dp[mask] = min sum once we’ve matched the indices in mask (popcount = number of i’s processed) with the same number of nums2 elements.

Code (Python 3)

from typing import List

class Solution:
    def minimumXORSum(self, nums1: List[int], nums2: List[int]) -> int:
        n = len(nums1)
        INF = float('inf')
        dp = [INF] * (1 << n)
        dp[0] = 0
        for mask in range(1 << n):
            i = bin(mask).count('1')
            if i >= n: continue
            for j in range(n):
                if mask & (1 << j): continue
                new_mask = mask | (1 << j)
                dp[new_mask] = min(dp[new_mask], dp[mask] + (nums1[i] ^ nums2[j]))
        return dp[(1 << n) - 1]

Complexity

Comments

Chapter summary & decisions

Bitmask cookbook

Operation Code
Is bit i set? (mask >> i) & 1
Set bit i mask | (1 << i)
Clear bit i mask & ~(1 << i)
Toggle bit i mask ^ (1 << i)
Lowest set bit mask & -mask
Pop count bin(mask).count('1') or int.bit_count() (Py 3.10+)
Iterate submasks sub = mask; while sub: ...; sub = (sub - 1) & mask (final iteration still has sub=0)
Iterate set bits while mask: i = (mask & -mask).bit_length() - 1; mask &= mask - 1

Constraint feasibility table

n Bitmask 2^n Common DP shape Time
≤ 20 ≤ 10⁶ dp[mask] (TSP, Assignment) O(2^n · n)
≤ 16 ≤ 65k dp[mask][i] (TSP with endpoint) O(2^n · n²)
≤ 22-25 ≤ 33M Need optimisation or submask enumeration O(3^n) for subset-sum DP

Shortest Superstring (LC 943) — duplicate words

Maximum Students (LC 1349) — row mask conflict

Row mask s (bit i = 1 if a student sits in column i):
- Valid within the row: s & (s << 1) == 0 (no two students adjacent).
- Compatible with previous mask p:
    (s & (p << 1)) == 0 AND (s & (p >> 1)) == 0
- Must respect broken cells: s & broken_mask[row] == 0.

Chapter 41 — Bitmask combined with Trie

When a problem needs “max XOR” with numbers from an array, we build a binary Trie of the numbers (one level per bit). The max XOR query is a greedy DFS that follows the bit opposite to the current number.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

Note: each ### Code (Python 3) below is standalone — paste the whole block into LeetCode. If you just want a reference template, use this class:

class BinaryTrie:
    """Binary Trie for 32-bit ints. Supports insert / max_xor."""

    def __init__(self):
        self.children = [None, None]    # 0, 1

    def insert(self, x: int, bits: int = 31) -> None:
        node = self
        for b in range(bits, -1, -1):
            bit = (x >> b) & 1
            if not node.children[bit]:
                node.children[bit] = BinaryTrie()
            node = node.children[bit]

    def max_xor(self, x: int, bits: int = 31) -> int:
        node = self
        out = 0
        for b in range(bits, -1, -1):
            bit = (x >> b) & 1
            opposite = 1 - bit
            if node.children[opposite]:
                out |= 1 << b
                node = node.children[opposite]
            else:
                node = node.children[bit]
        return out

End-of-chapter practice


41.1 Maximum XOR of Two Numbers (LC 421) — Trie version

Solved with “greedy bit + set” in Chapter 21.6. The Trie way is more elegant.

Problem

Given nums, return the max XOR of two distinct elements.

Example

Input:  nums = [3, 10, 5, 25, 2, 8]
Output: 28   (max XOR between any two elements: 5 XOR 25 = 28)

Constraints

Clarifying questions

Approach

Build the binary Trie of every number. For each x, query max_xor(x) (DFS along the opposite bit). Take the max over all x.

Code (Python 3)

from typing import List

class _Trie:
    __slots__ = ("children",)
    def __init__(self):
        self.children = [None, None]


class Solution:
    BITS = 31

    def findMaximumXOR(self, nums: List[int]) -> int:
        root = _Trie()
        for x in nums:
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                if not node.children[bit]:
                    node.children[bit] = _Trie()
                node = node.children[bit]

        best = 0
        for x in nums:
            node = root
            cur = 0
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                opp = 1 - bit
                if node.children[opp]:
                    cur |= 1 << b
                    node = node.children[opp]
                else:
                    node = node.children[bit]
            best = max(best, cur)
        return best

Complexity

Comments


41.2 Maximum XOR With an Element From Array (LC 1707)

Problem

Given nums and queries[i] = [x, m]. For each query, return the maximum XOR between x and any y ∈ nums with y ≤ m. No such y → -1.

Example

Input:  nums = [0,1,2,3,4], queries = [[3,1],[1,3],[5,6]]
Output: [3, 3, 7]

Constraints

Clarifying questions

Approach

Offline + sort + Trie.

Code (Python 3)

from typing import List

class _Trie:
    __slots__ = ("children",)
    def __init__(self):
        self.children = [None, None]


class Solution:
    BITS = 30  # nums, x ≤ 10^9 < 2^30

    def maximizeXor(self, nums: List[int], queries: List[List[int]]) -> List[int]:
        def insert(root, x):
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                if not node.children[bit]:
                    node.children[bit] = _Trie()
                node = node.children[bit]

        def max_xor(root, x):
            node = root
            out = 0
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                opp = 1 - bit
                if node.children[opp]:
                    out |= 1 << b
                    node = node.children[opp]
                else:
                    node = node.children[bit]
            return out

        nums.sort()
        indexed = sorted(enumerate(queries), key=lambda kv: kv[1][1])
        result = [-1] * len(queries)
        root = _Trie()
        i = 0
        for q_idx, (x, m) in indexed:
            while i < len(nums) and nums[i] <= m:
                insert(root, nums[i])
                i += 1
            if i == 0:
                result[q_idx] = -1
            else:
                result[q_idx] = max_xor(root, x)
        return result

Complexity

Comments


41.3 Count Pairs With XOR in a Range (LC 1803)

Problem

Given nums and [low, high], count pairs (i, j) with i < j and low <= nums[i] ^ nums[j] <= high.

Example

Input:  nums = [1, 4, 2, 7], low = 2, high = 6
Output: 6

Explanation of the 6 pairs (i, j) with 2 ≤ nums[i] ^ nums[j] ≤ 6:
  (0,1) 1 ^ 4 = 5      ✓
  (0,2) 1 ^ 2 = 3      ✓
  (0,3) 1 ^ 7 = 6      ✓
  (1,2) 4 ^ 2 = 6      ✓
  (1,3) 4 ^ 7 = 3      ✓
  (2,3) 2 ^ 7 = 5      ✓

Constraints

Clarifying questions

Approach

count_xor_less(x) = pairs (num, y) (y was inserted earlier) with num ^ y < x. Answer = count_xor_less(high + 1) - count_xor_less(low).

The Trie also stores a count at each node = how many ys passed through it. For each num, walk top bit to low; at the current bit let bit_n = bit of num, bit_x = bit of x:

In short: “same bit ⇒ XOR = 0” and “opposite bit ⇒ XOR = 1”. The code counts the same-bit subtree (XOR = 0) when bit_x = 1, then moves the pointer to the opposite branch to continue.

Code (Python 3)

from typing import List

class _TrieCnt:
    __slots__ = ("children", "count")
    def __init__(self):
        self.children = [None, None]
        self.count = 0


class Solution:
    BITS = 15  # nums[i] ≤ 2·10^4 < 2^15

    def countPairs(self, nums: List[int], low: int, high: int) -> int:

        def count_less(target: int) -> int:
            root = _TrieCnt()
            total = 0
            for num in nums:
                node = root
                for b in range(self.BITS, -1, -1):
                    bit_n = (num >> b) & 1
                    bit_x = (target >> b) & 1
                    if bit_x == 1:
                        # XOR bit = 0 (same bit) → count the entire subtree.
                        if node.children[bit_n]:
                            total += node.children[bit_n].count
                        # Descend into the XOR-bit = 1 branch (= opposite).
                        if node.children[1 - bit_n]:
                            node = node.children[1 - bit_n]
                        else:
                            node = None
                            break
                    else:
                        # Must keep XOR bit = 0 → walk down the same branch.
                        if node.children[bit_n]:
                            node = node.children[bit_n]
                        else:
                            node = None
                            break
                # Insert num into the Trie after counting.
                node = root
                for b in range(self.BITS, -1, -1):
                    bit_n = (num >> b) & 1
                    if not node.children[bit_n]:
                        node.children[bit_n] = _TrieCnt()
                    node = node.children[bit_n]
                    node.count += 1
            return total

        return count_less(high + 1) - count_less(low)

Complexity

Comments


41.4 Maximum Genetic Difference Query (LC 1938)

Problem

Tree given via parents[i] (parent of i, -1 for root). On LC this problem assigns no separate value to each node — the node’s “value” = the node index itself. Each query (node, val): find max val ^ x where x is the index of an ancestor (including node itself). Return the array of maxima in query order.

Example

Input:  parents = [-1, 0, 1, 1]
        queries = [[0, 2], [3, 2], [2, 5]]

        The tree (parents[0] = -1 makes 0 the root):
              0
              |
              1
             / \
            2   3

Output: [2, 3, 7]

Explanation:
  query (0, 2): ancestors of 0 are {0};      max(2^0) = 2.
  query (3, 2): ancestors of 3 are {3,1,0};  max(2^3, 2^1, 2^0) = max(1,3,2) = 3.
  query (2, 5): ancestors of 2 are {2,1,0};  max(5^2, 5^1, 5^0) = max(7,4,5) = 7.

Constraints

Clarifying questions

Approach

Offline DFS on the tree. Maintain a Trie containing the indices of ancestors of the current DFS path (including the current node).

The Trie must support remove — track count per node so we know when to prune.

Code (Python 3)

import sys
from collections import defaultdict
from typing import List

class _TrieX:
    __slots__ = ("children", "count")
    def __init__(self):
        self.children = [None, None]
        self.count = 0


class Solution:
    BITS = 17  # node indices ≤ n - 1 ≤ 10^5 - 1 < 2^17

    def maxGeneticDifference(self, parents: List[int], queries: List[List[int]]) -> List[int]:
        sys.setrecursionlimit(10**6)
        n = len(parents)
        children = defaultdict(list)
        root_node = -1
        for v, p in enumerate(parents):
            if p == -1:
                root_node = v
            else:
                children[p].append(v)

        # Group queries by node.
        node_queries = defaultdict(list)
        for q_idx, (node, val) in enumerate(queries):
            node_queries[node].append((q_idx, val))

        root = _TrieX()
        result = [0] * len(queries)

        def insert(x: int) -> None:
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                if not node.children[bit]:
                    node.children[bit] = _TrieX()
                node = node.children[bit]
                node.count += 1

        def remove(x: int) -> None:
            node = root
            path = []
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                path.append((node, bit))
                node = node.children[bit]
                node.count -= 1
            # Prune branches whose count drops to 0.
            for parent, bit in reversed(path):
                if parent.children[bit] and parent.children[bit].count == 0:
                    parent.children[bit] = None

        def query_max(val: int) -> int:
            node = root
            out = 0
            for b in range(self.BITS, -1, -1):
                bit = (val >> b) & 1
                opp = 1 - bit
                if node.children[opp] and node.children[opp].count > 0:
                    out |= 1 << b
                    node = node.children[opp]
                else:
                    node = node.children[bit]
            return out

        def dfs(u: int) -> None:
            insert(u)
            for q_idx, val in node_queries[u]:
                result[q_idx] = query_max(val)
            for v in children[u]:
                dfs(v)
            remove(u)

        dfs(root_node)
        return result

Complexity

Comments


41.5 Maximum Strong Pair XOR II (LC 2935)

Problem

A pair (x, y) is “strong” ↔︎ |x - y| <= min(x, y). Given nums, find the max XOR over strong pairs.

Example

Input:  nums = [1, 2, 3, 4, 5]
Output: 7   ("strong" pairs (x, y) satisfy |x - y| ≤ min(x, y); max XOR = 3 XOR 4 = 7)

Constraints

Clarifying questions

Approach

Observation: |x - y| ≤ min(x, y) ↔︎ max(x, y) ≤ 2 · min(x, y) (assuming both ≥ 0). Sort nums, sliding window: for each r, advance l until nums[r] > 2 · nums[l].

Within the window [l, r], max XOR = Trie query. Sliding Trie: when l advances, remove nums[l]. When a new r arrives, insert nums[r].

Code (Python 3)

from typing import List

class _TrieRem:
    __slots__ = ("children", "count")
    def __init__(self):
        self.children = [None, None]
        self.count = 0


class Solution:
    BITS = 20  # nums ≤ 2·10^5 < 2^20

    def maximumStrongPairXor(self, nums: List[int]) -> int:
        nums.sort()
        root = _TrieRem()

        def insert(x: int) -> None:
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                if not node.children[bit]:
                    node.children[bit] = _TrieRem()
                node = node.children[bit]
                node.count += 1

        def remove(x: int) -> None:
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                node = node.children[bit]
                node.count -= 1

        def query_max(x: int) -> int:
            node = root
            out = 0
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                opp = 1 - bit
                if node.children[opp] and node.children[opp].count > 0:
                    out |= 1 << b
                    node = node.children[opp]
                else:
                    node = node.children[bit]
            return out

        l = 0
        best = 0
        for r in range(len(nums)):
            insert(nums[r])
            while nums[r] > 2 * nums[l]:
                remove(nums[l])
                l += 1
            best = max(best, query_max(nums[r]))
        return best

Complexity

Comments


41.6 Maximum XOR After Operations (LC 2317)

Problem

Given nums. Operation: pick nums[i] and any x ≥ 0, set nums[i] = nums[i] AND (nums[i] XOR x). Find the max XOR of all elements after any number of operations.

Example

Input:  nums = [3, 2, 4, 6]
Output: 7   (each op: pick i, x; replace nums[i] = nums[i] AND (nums[i] XOR x);
             max XOR over the whole array = OR of all nums[i] = 3|2|4|6 = 7)

Constraints

Clarifying questions

Approach

Insight: the operation lets us turn off any bit of nums[i] (keep existing bits). Because each bit can be toggled freely on at least one number, the XOR of all = OR of all → max = OR of all.

Code (Python 3)

from typing import List

class Solution:
    def maximumXOR(self, nums: List[int]) -> int:
        result = 0
        for x in nums:
            result |= x
        return result

Complexity

Comments

Chapter summary & decisions

Chapter name — “Binary Trie for XOR”

A fuller name: Binary Trie to optimise XOR/operations bit by bit. “Bitmask combined with Trie” in the folder is just shorthand.

Count Pairs With XOR in Range (LC 1803) — bit-level

Genetic Difference (LC 1938) — node value = node index

LC’s setup uses parents[i] to build the tree; the value of node i is simply i. The Trie stores i along the root → query path.

Strong Pair (LC 2941) — derivation


Chapter 42 — Tree Dynamic Programming

Tree DP = bottom-up DFS on a tree. Each node computes its value from its children. A special pattern: re-rooting — compute the answer for every node as root in O(n) instead of O(n²).

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

# 1) Bottom-up tree DP
def dfs(node, parent):
    state = base
    for child in graph[node]:
        if child == parent: continue
        sub = dfs(child, node)
        state = combine(state, sub)
    return state

# 2) Re-rooting (see 42.5 for the full implementation)
def re_root(graph, n):
    # 1st DFS: compute "down" values (subtree rooted at i).
    # 2nd DFS: propagate "up" values from parent to children.
    pass

End-of-chapter practice


42.1 House Robber III (LC 337) — recap

Fully solved in Chapter 11.5. Tree DP with two states per node: rob_this, skip_this.

This is the gateway to Tree DP — the “return a 2-tuple from each child” pattern. The rest of Chapter 42 extends it: Binary Tree Cameras (3 states), Diameter (track best across subtrees), Sum of Distances (re-rooting).

Code (Python 3) (recap)

from typing import Optional, Tuple

class Solution:
    def rob(self, root) -> int:
        def dfs(node) -> Tuple[int, int]:
            """Return (rob_this, skip_this)."""
            if not node:
                return 0, 0
            l_rob, l_skip = dfs(node.left)
            r_rob, r_skip = dfs(node.right)
            rob_this = node.val + l_skip + r_skip
            skip_this = max(l_rob, l_skip) + max(r_rob, r_skip)
            return rob_this, skip_this
        return max(dfs(root))

Complexity


42.2 Binary Tree Cameras (LC 968)

Problem

Place cameras on the tree so every node is “covered” (camera on or adjacent to it). Minimise the number of cameras.

Example

Input:  root = [0, 0, null, 0, 0]
        (LC level-order serialisation; 'null' = absent node;
         node values are irrelevant here — only the structure matters.)
Output: 1   (minimum cameras covering all nodes)

Constraints

Clarifying questions

Approach

Three states per node: - 0: not yet covered. - 1: covered (no camera here but a child has one). - 2: has a camera here.

Greedy bottom-up: if any child is 0 → install a camera at the current node.

Code (Python 3)

class Solution:
    def minCameraCover(self, root) -> int:
        self.cnt = 0

        def dfs(node) -> int:
            if not node: return 1   # null counted as "covered"
            l = dfs(node.left)
            r = dfs(node.right)
            if l == 0 or r == 0:
                self.cnt += 1
                return 2
            if l == 2 or r == 2:
                return 1
            return 0

        if dfs(root) == 0:
            self.cnt += 1
        return self.cnt

Complexity

Comments


42.3 Diameter of Binary Tree (LC 543)

Problem

The longest path between any two nodes in the tree (counted in edges).

Example

Input:  root = [1, 2, 3, 4, 5]   (LC level-order; left → right, top tier → bottom tier)
Output: 3   (paths 4 → 2 → 1 → 3 or 5 → 2 → 1 → 3 contain 3 edges)

Constraints

Clarifying questions

Approach

Bottom-up DFS. Each node returns its depth downward. The diameter through a node = left_depth + right_depth.

Code (Python 3)

class Solution:
    def diameterOfBinaryTree(self, root) -> int:
        self.best = 0

        def depth(node) -> int:
            if not node: return 0
            l = depth(node.left)
            r = depth(node.right)
            self.best = max(self.best, l + r)
            return 1 + max(l, r)

        depth(root)
        return self.best

Complexity

Comments


42.4 Longest Path With Different Adjacent Characters (LC 2246)

Problem

Input: parent: List[int] (parent array, parent[0] = -1 is root) and s: str — node i has character s[i]. Tree with n nodes labelled 0..n-1. Find the longest path (counted in nodes) where adjacent nodes on the path have different characters.

Example

Input:  parent=[-1,0,0,1,1,2], s="abacbe"
Output: 3

Constraints

Clarifying questions

Approach

Tree DP. Per node u: longest downward chain = max(1, 1 + max chain of children with different char). Update best with the top-2 chains.

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def longestPath(self, parent: List[int], s: str) -> int:
        n = len(parent)
        children = defaultdict(list)
        for i in range(1, n):
            children[parent[i]].append(i)
        self.best = 1

        def dfs(u: int) -> int:
            chains = [0]
            for v in children[u]:
                sub = dfs(v)
                if s[v] != s[u]:
                    chains.append(sub)
            chains.sort(reverse=True)
            top1 = chains[0] if chains else 0
            top2 = chains[1] if len(chains) > 1 else 0
            self.best = max(self.best, top1 + top2 + 1)
            return top1 + 1

        dfs(0)
        return self.best

Complexity

Comments


42.5 Sum of Distances in Tree (LC 834) — Re-rooting

Problem

Input: n (number of nodes) and edges: List[List[int]]n-1 undirected tree edges [u, v]. Return result: List[int] where result[i] = sum of distances from node i to every other node.

Example

Input:  n=6, edges=[[0,1],[0,2],[2,3],[2,4],[2,5]]
Output: [8,12,6,10,10,10]

Constraints

Clarifying questions

Approach

Classic re-rooting.

  1. First DFS: compute count[u] = subtree size of u; result[0] = total distance from root 0.
  2. Second DFS: update result[v] from result[u] (v’s parent): result[v] = result[u] - count[v] + (n - count[v]).

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]:
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        count = [1] * n
        answer = [0] * n

        def dfs1(u: int, parent: int) -> None:
            for v in graph[u]:
                if v != parent:
                    dfs1(v, u)
                    count[u] += count[v]
                    answer[u] += answer[v] + count[v]

        def dfs2(u: int, parent: int) -> None:
            for v in graph[u]:
                if v != parent:
                    answer[v] = answer[u] - count[v] + (n - count[v])
                    dfs2(v, u)

        dfs1(0, -1)
        dfs2(0, -1)
        return answer

Complexity

Comments


42.6 Minimum Edge Reversals So Every Node Is Reachable (LC 2858)

Problem

Input: n and edges: List[List[int]] — each [u, v] is a directed edge u → v. Ignoring direction the graph is a tree (n-1 edges, connected). For each node, count the edges that need reversing for that node to reach every other node.

Example

Input:  n=4, edges=[[2,0],[2,1],[1,3]]
Output: [1,1,0,2]

Constraints

Clarifying questions

Approach

Re-rooting with “flip cost”.

  1. First DFS from node 0: for each edge, if the original direction opposes our DFS direction → cost += 1. result[0] = total cost.
  2. Second DFS: for each tree edge u → v (undirected), check the original direction: if original is u → v → `result[v] = result[u]

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    def minEdgeReversals(self, n: int, edges: List[List[int]]) -> List[int]:
        # graph[u] = list of (v, cost) where cost = 0 if directed u→v, 1 if must reverse.
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append((v, 0))    # u → v: forward
            graph[v].append((u, 1))    # v → u: reverse needed to go from v to u

        result = [0] * n
        def dfs1(u: int, parent: int) -> int:
            cost = 0
            for v, c in graph[u]:
                if v != parent:
                    cost += c + dfs1(v, u)
            return cost
        result[0] = dfs1(0, -1)

        def dfs2(u: int, parent: int) -> None:
            for v, c in graph[u]:
                if v != parent:
                    # When the root moves u → v: the edge u-v with cost c flips.
                    # Original cost = c → after re-rooting at v: 1 - c.
                    result[v] = result[u] + (1 - 2 * c)
                    dfs2(v, u)
        dfs2(0, -1)
        return result

Complexity

Comments

Chapter summary & decisions

Generic re-rooting template

Pass 1 (post-order): compute down[v] = answer for v’s subtree when rooted at v. Pass 2 (pre-order from root): move the root from parent p to child c:

ans[c] = ans[p] - contribution_of_c_to_p_subtree + contribution_of_rest_to_c_subtree

For “sum of distances”:

ans[c] = ans[p] - size[c] + (n - size[c])

Binary Tree Cameras (LC 968) — state 0/1/2

Sum of Distances (LC 834) — trace

Tree:

   0
  /|\
 1 2 3
 |
 4

Minimum Edge Reversals (LC 2858)


Chapter 43 — Topological Sort combined with Dynamic Programming

DP on a DAG = topological sort + linear DP in topo order. The state at a node depends on the nodes that come “before” it in topo order.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

End-of-chapter practice


43.1 Longest Increasing Path in a Matrix (LC 329)

Problem

A matrix. Find the longest strictly-increasing path (4 directions).

Example

Input:  matrix = [[9, 9, 4],
                  [6, 6, 8],
                  [2, 1, 1]]   (m × n grid, 4-dir walk, values must STRICTLY increase)
Output: 4   (longest increasing path: 1 → 2 → 6 → 9)

Constraints

Clarifying questions

Approach

Implicit DAG: each cell is a node, an edge u → v exists if mat[v] > mat[u]. DFS with memo.

Code (Python 3)

from functools import cache
from typing import List

class Solution:
    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        rows, cols = len(matrix), len(matrix[0])
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]

        @cache
        def dfs(r: int, c: int) -> int:
            best = 1
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
                    best = max(best, 1 + dfs(nr, nc))
            return best

        return max(dfs(r, c) for r in range(rows) for c in range(cols))

Complexity

Comments


43.2 Parallel Courses III (LC 2050)

Problem

Input: n (number of courses, labelled 1..n), relations: List[List[int]] — each [a, b] means must finish course a before b (DAG edge a → b), and time: List[int] with time[i] = time to learn course i+1. Courses can be taken in parallel if their prerequisites are done. Min total time.

Example

Input:  n=3, relations=[[1,3],[2,3]], time=[3,2,5]
Output: 8

Constraints

Clarifying questions

Approach

Topo sort + DP. dp[u] = time[u] + max(dp[v] for v a prerequisite).

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def minimumTime(self, n: int, relations: List[List[int]], time: List[int]) -> int:
        graph = defaultdict(list)
        indeg = [0] * (n + 1)
        for u, v in relations:
            graph[u].append(v)
            indeg[v] += 1

        dp = [0] * (n + 1)
        queue = deque()
        for i in range(1, n + 1):
            if indeg[i] == 0:
                dp[i] = time[i - 1]
                queue.append(i)
        while queue:
            u = queue.popleft()
            for v in graph[u]:
                dp[v] = max(dp[v], dp[u] + time[v - 1])
                indeg[v] -= 1
                if indeg[v] == 0:
                    queue.append(v)
        return max(dp)

Complexity

Comments


43.3 Largest Color Value in a Directed Graph (LC 1857)

Problem

Input: colors: str (colors[i] = colour of node i, 'a'..'z') and edges: List[List[int]]directed edges [u, v]. Find the path maximising the number of nodes of any single colour on the path.

Example

Input:  colors="abaca", edges=[[0,1],[0,2],[2,3],[3,4]]
Output: 3

Constraints

Clarifying questions

Approach

Topo + DP dp[u][c] = max number of colour-c nodes on a path ending at u.

dp[v][c] = max(dp[v][c], dp[u][c] + (1 if color[v] == c else 0)).

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def largestPathValue(self, colors: str, edges: List[List[int]]) -> int:
        n = len(colors)
        graph = defaultdict(list)
        indeg = [0] * n
        for u, v in edges:
            graph[u].append(v)
            indeg[v] += 1
        dp = [[0] * 26 for _ in range(n)]
        queue = deque()
        for i in range(n):
            if indeg[i] == 0:
                queue.append(i)
                dp[i][ord(colors[i]) - 97] = 1
        visited = 0
        best = 0
        while queue:
            u = queue.popleft()
            visited += 1
            best = max(best, max(dp[u]))
            for v in graph[u]:
                for c in range(26):
                    add = 1 if (ord(colors[v]) - 97) == c else 0
                    if dp[u][c] + add > dp[v][c]:
                        dp[v][c] = dp[u][c] + add
                indeg[v] -= 1
                if indeg[v] == 0:
                    queue.append(v)
        return best if visited == n else -1

Complexity

Comments


43.4 Build a Matrix With Conditions (LC 2392)

Problem

Given k (size) and row/col conditions (a must be above/before b). Return a k×k matrix containing each number 1..k exactly once that satisfies all conditions.

Example

Input:  k=3, rowConditions=[[1,2],[3,2]], colConditions=[[2,1],[3,2]]
Output: [[3,0,0],[0,0,1],[0,2,0]]

Constraints

Clarifying questions

Approach

Two topo sorts: row conditions → row position; col conditions → col position. Place each number at (row_pos, col_pos).

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def buildMatrix(self, k: int, rowConditions: List[List[int]], colConditions: List[List[int]]) -> List[List[int]]:
        def topo(conditions):
            graph = defaultdict(list)
            indeg = [0] * (k + 1)
            for u, v in conditions:
                graph[u].append(v)
                indeg[v] += 1
            queue = deque(i for i in range(1, k + 1) if indeg[i] == 0)
            order = []
            while queue:
                u = queue.popleft()
                order.append(u)
                for v in graph[u]:
                    indeg[v] -= 1
                    if indeg[v] == 0:
                        queue.append(v)
            return order if len(order) == k else []

        rows = topo(rowConditions)
        cols = topo(colConditions)
        if not rows or not cols: return []
        row_pos = {x: i for i, x in enumerate(rows)}
        col_pos = {x: i for i, x in enumerate(cols)}
        result = [[0] * k for _ in range(k)]
        for x in range(1, k + 1):
            result[row_pos[x]][col_pos[x]] = x
        return result

Complexity

Comments


43.5 Course Schedule IV (LC 1462)

Problem

Given numCourses and prerequisites[i] = [a, b] meaning “must finish course a before course b (i.e. a is a prerequisite of b; the DAG edge is a → b). For each queries[i] = [u, v], return True if u is a (direct or indirect) prerequisite of v — that is, a path u → ... → v exists in the DAG.

Edge-direction convention (see Chapter 13): “u before v” ⇔ edge u → v. Don’t reverse it — this is the classic bug in the Course Schedule family.

Example

Input:  numCourses = 3, prerequisites = [[1,2],[1,0],[2,0]], queries = [[1,0],[1,2]]
Output: [True, True]

Constraints

Clarifying questions

Approach

Brute force: per query, BFS/DFS from u and check for v. O(Q · (V + E)).

Optimal — Topo + DP closure: reach[u] = set of vertices v reachable from u. Walk reverse topo order and set reach[u] = {u} ∪ union(reach[v] for v in adj[u]).

After preprocessing each query is O(1).

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def checkIfPrerequisite(self, numCourses: int,
                            prerequisites: List[List[int]],
                            queries: List[List[int]]) -> List[bool]:
        graph = defaultdict(list)
        indeg = [0] * numCourses
        for a, b in prerequisites:
            graph[a].append(b)
            indeg[b] += 1

        # Topo order.
        queue = deque(i for i in range(numCourses) if indeg[i] == 0)
        topo = []
        while queue:
            u = queue.popleft()
            topo.append(u)
            for v in graph[u]:
                indeg[v] -= 1
                if indeg[v] == 0:
                    queue.append(v)

        # reach[u] = set of vertices reachable from u.
        reach = [set() for _ in range(numCourses)]
        # Process in reverse topo: children already have their reach computed.
        for u in reversed(topo):
            for v in graph[u]:
                reach[u].add(v)
                reach[u] |= reach[v]

        return [v in reach[u] for u, v in queries]

Complexity

Comments


43.6 Find All Possible Recipes from Given Supplies (LC 2115)

Problem

Given recipes (each requiring ingredients), supplies (available ingredients). Return the recipes that can be made.

Example

Input:  recipes=["bread"], ingredients=[["yeast","flour"]], supplies=["yeast","flour","corn"]
Output: ["bread"]

Constraints

Clarifying questions

Approach

Topo sort: edges ingredient → recipes needing it. BFS from supplies; when all ingredients of a recipe are available → the recipe is “available”.

Code (Python 3)

from collections import defaultdict, deque
from typing import List

class Solution:
    def findAllRecipes(self, recipes: List[str], ingredients: List[List[str]], supplies: List[str]) -> List[str]:
        graph = defaultdict(list)
        indeg = {}
        for recipe, ings in zip(recipes, ingredients):
            indeg[recipe] = len(ings)
            for ing in ings:
                graph[ing].append(recipe)

        queue = deque(supplies)
        available = set(supplies)
        result = []
        recipe_set = set(recipes)
        while queue:
            ing = queue.popleft()
            for r in graph[ing]:
                indeg[r] -= 1
                if indeg[r] == 0:
                    result.append(r)
                    available.add(r)
                    queue.append(r)
        return result

Complexity

Comments

Chapter summary & decisions

Topo + DP taxonomy

Kind Representative problem
Longest path on DAG LC 329 Longest Increasing Path
Counting paths LC 1976 Number of Ways to Arrive
Reachability closure Transitive closure / “can reach from A to B”
Dependency scheduling LC 1136 Parallel Courses, LC 2050
Aggregation along DAG LC 1857 Largest Color Value

Largest Color Value (LC 1857)

LC 1462 — Course Schedule IV

Recipes (LC 2115)

Direction consistency (cross-reference Chapter 13)

LC 1462: prerequisites[i] = [u, v] means “must finish u before v” ⇒ edge u → v (matches Chapter 13). Don’t reverse!


Chapter 44 — Combinatorics combined with Dynamic Programming

The last chapter — counting DP. The pattern: a counting state with additive transitions. mod 10^9 + 7 appears in every problem because the answers are huge.

Chapter objectives

After this chapter, you will be able to:

When to use this pattern?

Template code

MOD = 10**9 + 7

def dp_count(states):
    table = [0] * len(states)
    table[0] = base
    for i in range(1, len(states)):
        table[i] = (sum(table[j] for j in transitions(i)) % MOD)
    return table[-1]

End-of-chapter practice


44.1 Unique Paths (LC 62)

Problem

An m × n grid. A robot starts at (0,0) and moves to (m-1,n-1), right or down only. Count paths.

Example

Input:  m=3, n=7
Output: 28

Constraints

Clarifying questions

Approach

DP dp[i][j] = dp[i-1][j] + dp[i][j-1]. Or via combinatorics: C(m+n-2, m-1).

Code (Python 3)

from math import comb

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        return comb(m + n - 2, m - 1)

Complexity

Comments


44.2 Unique Paths II (LC 63)

Problem

Same as 44.1 but with obstacles. 1 = blocked.

Example

Input:  obstacleGrid = [[0, 0, 0],
                        [0, 1, 0],
                        [0, 0, 0]]   (0 = empty, 1 = obstacle)
Output: 2   (number of paths from (0,0) to (m-1, n-1); right/down only)

Constraints

Clarifying questions

Approach

DP. If a cell is blocked → dp[i][j] = 0.

Code (Python 3)

from typing import List

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        if obstacleGrid[0][0] == 1: return 0
        dp = [[0] * n for _ in range(m)]
        dp[0][0] = 1
        for i in range(m):
            for j in range(n):
                if obstacleGrid[i][j] == 1:
                    dp[i][j] = 0
                    continue
                if i > 0: dp[i][j] += dp[i - 1][j]
                if j > 0: dp[i][j] += dp[i][j - 1]
        return dp[-1][-1]

Complexity

Comments


44.3 Knight Dialer (LC 935)

Problem

On a numeric keypad (3×4), a chess knight makes n - 1 moves (each a chess knight move). Count the number of strings producible. Mod 10^9 + 7.

Example

Input:  n=2
Output: 20

Constraints

Clarifying questions

Approach

Neighbour map for the keypad. DP dp[i][digit] = strings of length i ending at digit.

Code (Python 3)

class Solution:
    MOD = 10**9 + 7
    NEIGHBORS = {
        0: [4, 6], 1: [6, 8], 2: [7, 9], 3: [4, 8], 4: [0, 3, 9],
        5: [], 6: [0, 1, 7], 7: [2, 6], 8: [1, 3], 9: [2, 4]
    }

    def knightDialer(self, n: int) -> int:
        dp = [1] * 10
        for _ in range(n - 1):
            new_dp = [0] * 10
            for d in range(10):
                for nb in self.NEIGHBORS[d]:
                    new_dp[nb] = (new_dp[nb] + dp[d]) % self.MOD
            dp = new_dp
        return sum(dp) % self.MOD

Complexity

Comments


44.4 Count Vowels Permutation (LC 1220)

Problem

Count vowel strings of length n obeying: - ‘a’ may only follow ‘e’. - ‘e’ may only follow ‘a’ or ‘i’. - ‘i’ may not follow ‘a’ or ‘i’. - ‘o’ may only follow ‘i’ or ‘u’. - ‘u’ may only follow ‘i’.

Example

Input:  n=1
Output: 5

Constraints

Clarifying questions

Approach

DP dp[i][v] = number of length-i strings ending at vowel v.

Code (Python 3)

class Solution:
    MOD = 10**9 + 7

    def countVowelPermutation(self, n: int) -> int:
        # Index: 0=a, 1=e, 2=i, 3=o, 4=u
        # prev_can[v] = vowels that may immediately precede v.
        prev_can = {
            0: [1, 2, 4],     # a after e, i, u
            1: [0, 2],         # e after a, i
            2: [1, 3],         # i after e, o
            3: [2],            # o after i
            4: [2, 3],         # u after i, o
        }
        dp = [1] * 5
        for _ in range(n - 1):
            new_dp = [0] * 5
            for v in range(5):
                for prev in prev_can[v]:
                    new_dp[v] = (new_dp[v] + dp[prev]) % self.MOD
            dp = new_dp
        return sum(dp) % self.MOD

Complexity

Comments


44.5 Number of Ways to Wear Different Hats to Each Other (LC 1434)

Problem

40 hats, n people (n ≤ 10), each person has a list of hats they like. Count assignments where each person gets a distinct hat.

Example

Input:  hats = [[3, 4], [4, 5], [5]]
        (hats[i] = list of hats person i likes; hat labels ∈ [1, 40])
Output: 1   (number of ways to assign each person one hat with distinct hats; mod 10^9 + 7)

Constraints

Clarifying questions

Approach

Bitmask DP over people, iterating over hats (people ≤ 10, hats ≤ 40):

dp[hat][mask] = ways to use hats <= hat covering the people in mask.

Code (Python 3)

from collections import defaultdict
from typing import List

class Solution:
    MOD = 10**9 + 7

    def numberWays(self, hats: List[List[int]]) -> int:
        n = len(hats)
        hat_to_people = defaultdict(list)
        for p, hs in enumerate(hats):
            for h in hs:
                hat_to_people[h].append(p)

        full = (1 << n) - 1
        dp = [0] * (1 << n)
        dp[0] = 1
        for h in range(1, 41):
            new_dp = dp[:]
            for mask in range(1 << n):
                for p in hat_to_people[h]:
                    if mask & (1 << p): continue
                    new_dp[mask | (1 << p)] = (new_dp[mask | (1 << p)] + dp[mask]) % self.MOD
            dp = new_dp
        return dp[full]

Complexity

Comments


44.6 Number of Music Playlists (LC 920)

Problem

n songs, listen to goal of them. Each song must be heard at least once. Two plays of the same song are separated by ≥ k other songs. Count playlists.

Example

Input:  n=3, goal=3, k=1
Output: 6

Constraints

Clarifying questions

Approach

DP dp[i][j] = playlists of length i using j distinct songs.

Code (Python 3)

class Solution:
    MOD = 10**9 + 7

    def numMusicPlaylists(self, n: int, goal: int, k: int) -> int:
        dp = [[0] * (n + 1) for _ in range(goal + 1)]
        dp[0][0] = 1
        for i in range(1, goal + 1):
            for j in range(1, n + 1):
                dp[i][j] = dp[i - 1][j - 1] * (n - j + 1) % self.MOD
                if j > k:
                    dp[i][j] = (dp[i][j] + dp[i - 1][j] * (j - k)) % self.MOD
        return dp[goal][n]

Complexity

Comments


🎉 End of Level 3!

You have just finished the book 288 DSA coding-interview questions, from Easy to Hard, covering 44 patterns. Best of luck passing every Big Tech interview round!

“The best way to learn is to teach.” — Try explaining a chapter to a friend. If your explanation is clear, you really know the material.

Chapter summary & decisions

Counting DP framework

Unique Paths (LC 62) — two ways

Approach Time Space When
Grid DP dp[i][j] = dp[i-1][j] + dp[i][j-1] O(mn) O(mn) or O(min(m,n)) With obstacles (LC 63) or extensions
Formula C(m+n-2, m-1) O(min(m,n)) O(1) No obstacles

Number of Music Playlists (LC 920) — state

Find All Good Strings (LC 1397) — digit DP + KMP

Count Vowels Permutation (LC 1220) — transition graph

a → e
e → a, i
i → a, e, o, u
o → i, u
u → a

DP dp[i][v] = strings of length i ending in vowel v.


Appendix

Appendix A — Pattern → problem-type lookup table

Use this table for reverse lookup: “my problem looks like this — what pattern should I pick?”

Problem signature Pattern (Chapter)
“Find pair / triplet with sum” Two Pointers (26), Hash (6)
“Longest / shortest substring with condition” Sliding Window (27)
“Find element in sorted array” Binary Search (5)
“Min/max X such that …” Search on Answer (25)
“Number of subarray …” Prefix Sum + Hash (19, 6)
“Connected components / cycle in undirected” Union Find (24), DFS/BFS
“Shortest path unweighted” BFS (10)
“Shortest path weighted ≥ 0” Dijkstra (30)
“Shortest path with negative” Bellman-Ford
“Topological order / cycle in directed” Topo Sort (13)
“Min cost to connect all” MST (33)
“Next greater / smaller element” Monotonic Stack (18, 8.5)
“Sliding window max/min” Monotonic Deque (18.4)
“Substring matching” KMP (35), Rolling Hash (34), Z (36)
“Prefix matching, dictionary lookup” Trie (23)
“Max XOR pair” Binary Trie (41)
“All permutations / subsets / combinations” Backtracking (3, 28)
“Min steps / number of ways” DP (29), Combinatorics DP (44)
“Game with 2 players optimal” Game Theory DP (31)
“Path in tree / depth/diameter/LCA” Tree DFS (11), Tree DP (42)
“Range sum / update” Prefix Sum (19), Fenwick/Segment (22)
“Median / kth largest stream” Heap (15)
“n ≤ 20 with subset” Bitmask DP (40)

Easy-to-confuse pattern table

BFS vs DFS vs Union Find (for connectivity / components):

Pattern When Time Space
BFS Shortest path unweighted, level order O(V + E) O(V)
DFS Connectivity, cycle detection, topo sort, any path O(V + E) O(V) recursion stack
Union Find Online add edges, “connected?” queries, offline edge sort O((V + E) · α) O(V)

Plain Binary Search vs Search on Answer:

Pattern Search on Predicate
Plain Binary Search (Ch 5) Index in a sorted array nums[mid] vs target
Search on Answer (Ch 25) The answer value check(mid) monotonic T/F

Sliding Window vs Two Pointers:

Pattern When Window
Two-end Two Pointers (Ch 26) Sort + find pair (sum, distance) Converging from both sides
Same-direction Sliding Window (Ch 27) Longest/shortest subarray with monotonic condition Extend r, shrink l

KMP vs Z vs Rolling Hash:

Pattern Preprocess Good for Drawback
KMP (Ch 35) LPS array O(m) Deterministic, in place LPS is conceptually trickier
Z (Ch 36) Z array O(n) Easier to visualise than LPS Same perf as KMP
Rolling Hash (Ch 34) Powers + prefix hash Multi-pattern, distinct count Collision risk

Dijkstra vs BFS+BS vs offline DSU (for min/max path):

Pattern When Complexity
Dijkstra (Ch 30) Non-negative weights, online queries O((V+E) log V)
BS + BFS (Ch 37) Monotonic predicate on threshold O(log · (V+E))
Offline DSU (Ch 24, 33) Process queries in order + sort edges O((V+E) · α)

Recursion vs Backtracking vs DFS vs Top-down DP:

Pattern Hallmark
Recursion (Ch 3) Pure recursion, no choice/state tracking
Backtracking (Ch 28) choose → explore → unchoose, enumerate everything
DFS (Ch 11, 12) Traverse graph/tree, mark visited
Top-down DP (Ch 29) Recursion + @cache, overlapping subproblems

Appendix B — Python 3 cookbook (35 handy snippets)

# 1. Counter
from collections import Counter
Counter("anagram").most_common(2)        # [('a',3), ('n',1)]

# 2. defaultdict
from collections import defaultdict
groups = defaultdict(list)
groups[key].append(val)

# 3. deque
from collections import deque
dq = deque([1,2,3]); dq.appendleft(0); dq.pop()

# 4. heapq (min-heap; max-heap = negate)
import heapq
h = []; heapq.heappush(h, (priority, value))
heapq.nsmallest(3, lst, key=lambda x: x.score)
# Max-heap idiom:
heapq.heappush(h, -value)
neg = heapq.heappop(h); val = -neg

# 5. bisect (binary search)
from bisect import bisect_left, insort
i = bisect_left(sorted_lst, x)
insort(sorted_lst, x)        # insert while maintaining sort

# 6. functools.cache (memoization)
from functools import cache
@cache
def dp(state): ...
# Caveat: the state must be hashable. List/dict → tuple/frozenset.

# 7. itertools
from itertools import (
    combinations, permutations, product, accumulate, pairwise
)
list(combinations([1,2,3], 2))      # [(1,2),(1,3),(2,3)]
list(accumulate([1,2,3,4]))         # [1, 3, 6, 10]
list(pairwise([1,2,3,4]))           # [(1,2),(2,3),(3,4)]

# 8. cmp_to_key for custom sort
from functools import cmp_to_key
arr.sort(key=cmp_to_key(lambda a, b: a-b))

# 9. math
from math import gcd, lcm, comb, factorial, inf, log2, ceil, floor
gcd(12, 18)         # 6
lcm(4, 6)           # 12 (Python 3.9+)
comb(5, 2)          # 10
log2(8)             # 3.0

# 10. SortedList (requires pip install sortedcontainers)
from sortedcontainers import SortedList
sl = SortedList()
sl.add(x); sl.remove(x)            # O(log n)
sl.bisect_left(x)

# 11. Increase recursion limit
import sys
sys.setrecursionlimit(10**6)

# 12. Bit operations
bin(12)             # '0b1100'
(12).bit_count()    # 2 (Python 3.10+)
(12 & -12)          # 4 (lowest set bit)
(12 & (12 - 1))     # 8 (clear lowest set bit)

# 13. String / character
chr(97); ord('a')   # 97; 'a'
'abc'.zfill(5)      # '00abc'
str(123).rjust(5)   # '  123'

# 14. List unpacking
a, *rest, b = [1, 2, 3, 4, 5]   # a=1, rest=[2,3,4], b=5

# 15. Walrus operator (Python 3.8+)
if (n := len(lst)) > 10:
    print(f"too long: {n}")

(Snippets 16–35 can be found in each chapter’s code samples.)


Appendix C — Template code for 20 patterns

Template code lives at the top of each chapter (the “Template code” section). Here is a quick index:

# Pattern Template in chapter
1 Two Pointers Ch. 26
2 Sliding Window Ch. 27
3 Binary Search Ch. 5
4 Search on Answer Ch. 25
5 Prefix Sum Ch. 19
6 Monotonic Stack/Deque Ch. 18
7 BFS Ch. 10
8 DFS Ch. 11
9 Backtracking Ch. 28
10 Union Find Ch. 24
11 Topological Sort Ch. 13
12 Dijkstra Ch. 30
13 MST (Kruskal+Prim) Ch. 33
14 Trie Ch. 23
15 Fenwick / Segment Ch. 22
16 LCS / LIS Ch. 29
17 Knapsack Ch. 29
18 Tree DP Ch. 42
19 Bitmask DP Ch. 40
20 KMP / Z Ch. 35, 36

Appendix D — 50 must-do problems one week before the interview

Trimmed and deduplicated by chapter. Each LC problem appears only once.

Array & String (8): LC 1, 121, 238, 11, 53, 49, 76, 3.

Linked List (5): LC 206, 21, 141, 25, 146.

Tree (5): LC 104, 102, 98, 236, 124.

Graph & BFS/DFS (7): LC 200, 207, 269, 994, 127, 133, 743.

Heap & Sort (3): LC 215, 23, 56.

Hash & Sliding Window (4): LC 49 (already in Array, skip), LC 76 (already in Array, skip), LC 424, 560.

Binary Search (3): LC 33, 34, 875.

DP (8): LC 70, 198, 322, 300, 72, 152, 416, 309.

Backtracking (3): LC 46, 78, 22.

Bit / String parser (4): LC 136, 421, 224, 8.

Note: LC 121 (Stock I) is prioritised over LC 207 (Course Schedule) for the first day — Stock is a gentle warm-up before graph problems. LC 49 and LC 76 appear in both Array and Sliding Window families — counted once under Array (the earlier tier).


Appendix E — Behavioral interview with the STAR framework

STAR framework

S | Situation | Describe the context (1-2 sentences) |
T | Task | The problem or expectation |
A | Action | What you specifically did (the focus!) |
R | Result | The outcome + lesson |

10 common questions

  1. “Tell me about a challenging bug you solved.”
  2. “Describe a conflict with a coworker.”
  3. “What’s your biggest weakness?”
  4. “Why do you want to work at [company]?”
  5. “Tell me about a project you’re proud of.”
  6. “Describe a time you missed a deadline.”
  7. “How do you prioritise tasks?”
  8. “Tell me about a time you took initiative.”
  9. “Describe a time you received critical feedback.”
  10. “Why are you leaving your current job?”

Tips


Appendix F — System-design + coding hybrid

Some interview rounds combine design + implementation. Examples:

Problem Related chapters
LRU Cache (LC 146) Ch. 6, 7
LFU Cache (LC 460) Ch. 7
Snake Game (LC 353) Ch. 7 (deque)
Rate Limiter Ch. 14 (interval)
Twitter Feed (LC 355) Ch. 15 (heap)
Search Autocomplete (LC 642) Ch. 23 (Trie)
Tic-Tac-Toe (LC 348) Ch. 6 (hash)
File System (LC 588) Ch. 23
Stack with Pop Middle Ch. 22 (DLL)

Process: clarify the scope → design the class/interfaces → implement the methods → test.


Appendix G — Further reading and follow-up

Classic books

Online platforms

Vietnamese resources

Newsletters & blogs

YouTube channels (English)

Final advice


Appendix H — Vietnamese-English glossary

English Vietnamese Short definition
Invariant Bất biến A property that always holds inside a loop/recursion
State Trạng thái A quantity sufficient to describe a subproblem (DP, game theory)
Transition Bước chuyển dp[next] = f(dp[curr]) — the relation between states
Subproblem Bài con A smaller problem used to build the larger one (DP, D&C)
Optimal substructure Cấu trúc con tối ưu The optimum is built from optimal substructures
Overlapping subproblems Bài con trùng lặp Subproblems repeat → cacheable
Monotonic Đơn điệu Increasing / decreasing along one direction (sort, stack, BS predicate)
Amortized Khấu hao Average op is O(1) even if worst-case is O(n)
Greedy Tham lam Pick the locally best choice each step
Heuristic Heuristic A rule that helps choose; no optimality guarantee
Trie Trie (prefix tree) Tree where each node = 1 char, root → node path = 1 prefix
Adjacency list Danh sách kề graph[u] = [v1, v2, ...]
In-place In-place Mutate the input directly, no aux data structure
Stable sort Sort ổn định Preserves the original order of equal elements (Python’s sorted is stable)
Sentinel Sentinel A fake boundary element to avoid special cases (dummy head, [-1, n])
Pivot Pivot A reference element for partitioning (quicksort, quickselect)
Backtracking Backtracking Recursion + undo state on the way back
Memoization Memoization Cache subproblem results (top-down DP)
Tabulation Tabulation Build the DP table bottom-up
Bitmask Bitmask Encode a subset via an int (bit i = whether element i is in)
LIS LIS Longest Increasing Subsequence
LCS LCS Longest Common Subsequence
LPS LPS Longest Prefix Suffix (KMP)
MST MST Minimum Spanning Tree
DSU DSU Disjoint Set Union (Union Find)
DAG DAG Directed Acyclic Graph
BST BST Binary Search Tree
Big-O Big-O Asymptotic complexity notation
α(n) Alpha(n) Inverse Ackermann function (≈ 4 for any practical n)

Appendix I — Index by LC number

Look up by LeetCode number → location in the book. Supports quick reverse lookup.

LC # Problem Chapter Section
1 Two Sum 1 1.1
2 Add Two Numbers 7 7.7
3 Longest Substring Without Repeating 27 27.1
4 Median of Two Sorted Arrays 25 25.5
8 String to Integer (atoi) 2 2.4
10 Regular Expression Matching 32 32.4
11 Container With Most Water 1, 26 1.5, 26.3
14 Longest Common Prefix 2 2.3
15 3Sum 26 26.1
17 Letter Combinations Phone 28 28.4
18 4Sum 26 26.6
19 Remove Nth From End 7 7.5
20 Valid Parentheses 8 8.1
21 Merge Two Sorted Lists 7 7.2
22 Generate Parentheses 3 3.4
23 Merge k Sorted Lists 15 15.5
25 Reverse Nodes in k-Group 7 7.9
28 strStr() 35, 36 35.1, 36.2
33 Search Rotated Sorted Array 5 5.5
34 Find First/Last Position 5 5.4
35 Search Insert Position 5 5.2
37 Sudoku Solver 28 28.8
42 Trapping Rain Water 26 26.2
45 Jump Game II 16 16.2
46 Permutations 3 3.5
47 Permutations II 28 28.3
49 Group Anagrams 2 2.5
50 Pow(x, n) 3, 17 3.2, 17.4
51 N-Queens 28 28.7
53 Maximum Subarray 17 17.1
55 Jump Game 16 16.1
56 Merge Intervals 4, 14 4.2, 14.1
57 Insert Interval 14 14.2
62 Unique Paths 44 44.1
63 Unique Paths II 44 44.2
65 Valid Number 32 32.5
69 Sqrt(x) 5 5.6
72 Edit Distance 29 29.3
75 Sort Colors 4, 26 4.1, 26.4
76 Minimum Window Substring 27 27.2
77 Combinations 28 28.1
78 Subsets 3 3.6
79 Word Search 28 28.9
80 Remove Duplicates Sorted II 26 26.5
84 Largest Rectangle Histogram 18 18.3
90 Subsets II 28 28.2
93 Restore IP Addresses 28 28.11
98 Validate BST 11, 22 11.4, 22.1
99 Recover BST 22 22.2
102 Level Order Traversal 10 10.1
104 Max Depth Binary Tree 11 11.1
113 Path Sum II 11 11.2
121 Best Time to Buy/Sell 1 1.2
124 Max Path Sum 22 22.4
125 Valid Palindrome 2 2.2
127 Word Ladder 10 10.3
128 Longest Consecutive 6 6.2
131 Palindrome Partitioning 28 28.6
132 Palindrome Partitioning II 29 29.13
133 Clone Graph 9 9.2
134 Gas Station 16 16.3
135 Candy 16 16.6
136 Single Number 21 21.1
138 Copy List Random 7 7.8
140 Word Break II 28 28.10
141 Linked List Cycle 7 7.3
143 Reorder List 7 7.12
146 LRU Cache 6, 7 6.6, 7.11
148 Sort List 7 7.10
150 Eval RPN 8 8.4
151 Reverse Words 2 2.6
152 Max Product Subarray 29 29.12
155 Min Stack 8 8.2
179 Largest Number 4 4.3
187 Repeated DNA 34 34.1
188 Stock IV 29 29.10
189 Rotate Array 1 1.6
191 Number of 1 Bits 21 21.2
198 House Robber I (practice for Ch 29)
200 Number of Islands 12 12.1
201 Bitwise AND Range 21 21.5
204 Count Primes 20 20.1
205 Isomorphic Strings 6 6.5
206 Reverse Linked List 3, 7 3.3, 7.1
207 Course Schedule 9 9.4
208 Implement Trie 23 23.1
210 Course Schedule II 13 13.1
211 Add and Search Word 23 23.2
212 Word Search II 23 23.3
213 House Robber II 29 29.11
215 Kth Largest 15, 17 15.1, 17.3
217 Contains Duplicate 6 6.1
224 Basic Calculator (see Ch 32)
227 Basic Calculator II 32 32.1
232 Implement Queue Stacks 8 8.3
234 Palindrome Linked List 7 7.6
236 LCA Binary Tree 11 11.6
238 Product Except Self 1, 19 1.3, 19.5
239 Sliding Window Maximum 18, 27 18.4, 27.6
241 Different Ways Parentheses 17 17.5
242 Valid Anagram 2 2.1
253 Meeting Rooms II 4, 14 4.4, 14.4
264 Ugly Number II 20 20.2
269 Alien Dictionary 13 13.2
273 Integer to English Words 32 32.6
278 First Bad Version 5 5.3
280 Wiggle Sort 4 4.6
282 Expression Add Operators 28 28.12
283 Move Zeroes 1 1.4
286 Walls and Gates 12 12.5
292 Nim Game 31 31.1
295 Find Median Stream 15 15.3
297 Serialize Tree 22 22.3
300 LIS 29 29.2
303 Range Sum Immutable 19 19.1
304 Range Sum 2D 19 19.4
305 Number Islands II 24 24.4
307 Range Sum Mutable 22 22.6
309 Stock Cooldown 29 29.9
310 Min Height Trees 13 13.3
312 Burst Balloons 29 29.14
315 Count Smaller After 22 22.5
322 Coin Change 29 29.7
323 Connected Components 9 9.3
329 Longest Increasing Path 43 43.1
332 Reconstruct Itinerary (Ch 11 follow-up)
337 House Robber III 11, 42 11.5, 42.1
338 Counting Bits 21 21.3
347 Top K Frequent 6 6.3
354 Russian Doll Envelopes 29, 38, 39 29.6, 38.1, 39.2
363 Max Sum Rectangle ≤ K 38 38.4
368 Largest Divisible Subset 39 39.1
371 Sum of Two Integers 21 21.4
394 Decode String 8, 32 8.6, 32.2
399 Evaluate Division 9 9.6
402 Remove K Digits 18 18.6
407 Trapping Rain Water II 37 37.4
410 Split Array Largest Sum 25 25.3
416 Partition Equal Subset Sum 29 29.5
417 Pacific Atlantic 12 12.4
421 Max XOR Pair 21, 41 21.6, 41.1
424 Char Replacement 27 27.3
435 Non-overlapping Intervals 14 14.3
438 Find All Anagrams 35 35.4
444 Sequence Reconstruction 13 13.5
452 Min Arrows Burst Balloons 14 14.5
455 Assign Cookies 16 16.4
459 Repeated Substring 35 35.3
464 Can I Win 31 31.6
486 Predict the Winner 31 31.3
503 Next Greater II 18 18.2
509 Fibonacci 3 3.1
518 Coin Change II 29 29.8
523 Continuous Subarray Sum 19 19.3
542 01 Matrix 12 12.6
543 Diameter Tree 42 42.3
547 Number of Provinces 24 24.1
560 Subarray Sum K 6, 19 6.4, 19.2
567 Permutation in String 27 27.4
591 Tag Validator (Ch 32)
621 Task Scheduler 15 15.6
648 Replace Words 23 23.5
664 Strange Printer 29 29.18
673 Number of LIS 38 38.3
684 Redundant Connection 24 24.2
692 Top K Frequent Words 15 15.2
695 Max Area Island 12 12.2
698 Partition K Equal Subsets 40 40.1
704 Binary Search 5 5.1
719 Find K-th Smallest Pair Dist 25 25.4
720 Longest Word 23 23.4
721 Accounts Merge 24 24.3
724 Find Pivot Index 19 19.6
726 Number of Atoms 32 32.3
736 Parse Lisp (Ch 32 ref)
739 Daily Temperatures 8, 18 8.5, 18.1
743 Network Delay Time 30 30.1
752 Open the Lock 10 10.4
759 Employee Free Time 14 14.6
763 Partition Labels 16 16.5
778 Swim Rising Water 24, 30, 37 24.6, 30.4, 37.1
785 Bipartite Graph 9 9.5
787 Cheapest Flights K Stops 30 30.3
791 Custom Sort String 4 4.5
797 All Paths Source Target 11 11.3
834 Sum of Distances Tree 42 42.5
841 Keys and Rooms 9 (practice)
847 Shortest Path Visit All 40 40.2
876 Middle Linked List 7 7.4
877 Stone Game 31 31.2
907 Sum Subarray Mins 18 18.5
909 Snakes and Ladders 10 10.6
913 Cat and Mouse 31 31.5
920 Music Playlists 44 44.6
935 Knight Dialer 44 44.3
943 Shortest Superstring 40 40.4
947 Most Stones Removed 9 (practice)
952 Largest Comp by Factor 20 20.5
968 Binary Tree Cameras 42 42.2
973 K Closest Points 15 15.4
990 Equation Satisfiability 24 24.5
992 Subarrays K Distinct 27 27.5
994 Rotting Oranges 10 10.2
1011 Capacity Ship Packages 25 25.2
1032 Stream of Characters 23 23.6
1044 Longest Duplicate Substring 34 34.2
1091 Shortest Path Binary Matrix 10 10.5
1102 Path Max Min Value 33 33.6
1125 Smallest Sufficient Team 40 40.3
1135 Connecting Cities 33 33.2
1136 Parallel Courses 13 13.6
1140 Stone Game II 31 31.4
1143 LCS 29 29.1
1168 Optimize Water 33 33.3
1175 Prime Arrangements 20 20.3
1203 Sort Items Groups 13 13.4
1220 Count Vowels Permutation 44 44.4
1235 Job Scheduling 39 39.4
1293 Shortest Path Obstacles 30 30.5
1297 Max Occurrences Substring 35 35.5
1316 Distinct Echo Substrings 34, 36 34.3, 36.6
1326 Min Taps Garden 39 39.6
1349 Max Students Exam 40 40.5
1368 Min Cost Valid Path 30 30.6
1392 Longest Happy Prefix 35 35.6
1425 Constrained Subseq Sum 38 38.5
1434 Hats Permutation 44 44.5
1462 Course Schedule IV 43 43.5
1478 Allocate Mailboxes 38 38.6
1489 Critical MST Edges 33 33.4
1547 Min Cost Cut Stick 29 29.16
1557 Min Vertices Reach 9 (practice)
1584 Min Cost Connect Points 33 33.1
1631 Path Min Effort 30, 37 30.2, 37.2
1638 Strings Differ One Char 34 34.5
1690 Stone Game VII 29 29.17
1691 Stacking Cuboids 39 39.3
1697 Edge Length Limited Paths 33 33.5
1707 Max XOR With Element 41 41.2
1791 Center Star Graph 9 (practice)
1803 Count Pairs XOR Range 41 41.3
1857 Largest Color Value 43 43.3
1879 Min XOR Sum 40 40.6
1901 Peak Element II 37 37.6
1938 Genetic Difference Query 41 41.4
1944 Visible People in Queue 39 39.5
1970 Last Day Cross 37 37.3
1971 Path Exists Graph 9 9.1
2050 Parallel Courses III 43 43.2
2115 Find Recipes 43 43.6
2223 Sum of Scores Built Strings 34, 36 34.6, 36.3
2246 Longest Path Diff Adj 42 42.4
2301 Match Substring Replace 36 36.5
2317 Max XOR After Ops 41 41.6
2392 Build Matrix Conditions 43 43.4
2407 LIS II 38 38.2
2430 Max Deletions String 36 36.4
2513 Min Max Two Arrays 37 37.5
2521 Distinct Prime Factors 20 20.6
2523 Closest Primes Range 20 20.4
2589 (replaced by LC 1462)
2858 Min Edge Reversals 42 42.6
2935 Max Strong Pair XOR II 41 41.5

Appendix J — Problems appearing across multiple chapters (recap map)

Several problems appear in multiple chapters from different angles. Here is a quick map to avoid confusion:

LC Full version Recap in New angle in the recap
LC 56 Merge Intervals Ch 4.2 (Sorting) Ch 14.1 (Interval) Sort vs interval pattern
LC 75 Sort Colors Ch 4.1 (Sorting Dutch flag) Ch 26.4 (Two Pointers) Sort vs 3-pointer
LC 11 Container Most Water Ch 1.5 (Array) Ch 26.3 (Two Pointers) Array trick vs converging pointers
LC 98 Validate BST Ch 11.4 (DFS) Ch 22.1 (Advanced Tree) DFS vs BST property
LC 146 LRU Cache Ch 6.6 (Hash Table) Ch 7.11 (Linked List) Hash + DLL vs DLL pattern
LC 206 Reverse LL Ch 7.1 (iterative) Ch 3.3 (recursive) Two different approaches
LC 50 Pow(x, n) Ch 3.2 (Recursion) Ch 17.4 (D&C) Recursion vs D&C framework
LC 215 Kth Largest Ch 15.1 (Heap) Ch 17.3 (D&C) Heap vs quickselect
LC 239 Sliding Window Max Ch 18.4 (Monotonic) Ch 27.6 (Sliding Window) Deque vs window
LC 253 Meeting Rooms II Ch 4.4 (Sorting) Ch 14.4 (Interval) Heap vs sweep line
LC 354 Russian Doll Ch 29.6 (DP) Ch 38.1, 39.2 Three angles: pure DP, BS + DP, sort + DP
LC 421 Max XOR Ch 21.6 (Bit) Ch 41.1 (Trie) Greedy bit vs binary trie
LC 560 Subarray Sum K Ch 6.4 (Hash) Ch 19.2 (Prefix Sum) Hash vs prefix sum
LC 778 Swim Water Ch 24.6 (DSU offline) Ch 30.4, 37.1 Three ways: DSU, Dijkstra, BS + BFS
LC 1316 Distinct Echo Ch 34.3 (Rolling Hash) Ch 36.6 (Z function) Two different algorithms
LC 1631 Path Min Effort Ch 30.2 (Dijkstra) Ch 37.2 (BS + BFS) Two approaches
LC 2223 Sum of Scores Ch 34.6 (Rolling Hash) Ch 36.3 (Z) Two algorithms

How to read recaps: focus on the “new angle” — how the current pattern looks at the old problem differently, not re-read the solution verbatim.


Appendix K — Pre-interview checklist

24 hours before: - [ ] Re-read Frontmatter 0.2 (UMPIRE) — revisit the 5 steps. - [ ] Re-read Appendix E (Behavioral) — prepare 3–4 STAR stories. - [ ] Sleep 7–8 hours. Don’t pull an all-nighter coding. - [ ] Check the equipment (mic, camera, screen share, IDE / online editor).

One week before: - [ ] Finish the 50 must-do problems (Appendix D). - [ ] 2–3 mock interviews (Pramp / interviewing.io / friends). - [ ] Study company-specific patterns (LeetCode tag by company). - [ ] Review your CV/resume — story behind every project.

One month before: - [ ] Finish Levels 1 + 2 (Chapters 1–32). - [ ] Regular mock interviews 1–2 sessions / week. - [ ] Learn the behavioural framework + prepare 10 STAR stories. - [ ] Apply broadly (5–10 companies) to get more practice rounds.


Best of luck on your interview journey! Thanks for reading this book.