[{"content":"Problem Requirements You are given an integer array groupSizes. There are n people, numbered from 0 to n - 1. The value groupSizes[i] is the exact number of people that person i must have in their group, including themselves.\nReturn a list of groups containing person IDs, with these requirements:\nEach person appears in exactly one group, with no duplicates or omissions. If a group contains k people, every member\u0026rsquo;s groupSizes[i] must equal k. Neither the order of people within a group nor the order of groups matters. Any valid grouping is accepted. People in the same group do not have to occupy consecutive positions in the input array. The constraints are 1 \u0026lt;= n \u0026lt;= 500 and 1 \u0026lt;= groupSizes[i] \u0026lt;= n. The problem guarantees that at least one valid grouping exists.\nExample 1:\nInput: groupSizes = [3,3,3,3,3,1,3] One valid output: [[0,1,2],[5],[3,4,6]] People 0, 1, and 2 each require a three-person group, as do people 3, 4, and 6. Person 5 needs a group of one. The same requested size can produce more than one group.\nExample 2:\nInput: groupSizes = [2,1,3,3,3,2] One valid output: [[1],[0,5],[2,3,4]] This tutorial is for readers who know Python loops and list operations and are practicing hash-table grouping. The examples use Python 3.10+, and the estimated reading time is 10 minutes. We will start with what belongs in a group, before reaching for a template.\nStep 1: Groups Contain Person IDs, Not Array Values Start with a smaller input: groupSizes = [2,1,2].\nFollowing the intuition of \u0026ldquo;put equal values together\u0026rdquo; might give us [[2,2],[1]]. That answer is wrong: person 2 appears twice, and person 0 never appears. The issue is not sorting. It is confusing a person\u0026rsquo;s identity with the capacity that person requires.\nMake just one distinction in this step: when reading the array, look at both the index and its value.\nPerson ID Required group size A possible arrangement 0 2 Join person 2 in a two-person group 1 1 Form a group alone 2 2 Join person 0 in a two-person group So [[0,2],[1]] is valid. Check each group: [0,2] has length 2, which both members require; [1] has length 1, which that member requires. Each of the IDs 0, 1, and 2 appears exactly once.\nThis suggests a smaller subproblem: before arranging every group, collect the IDs of people who require the same group size. People in the same group must agree on its size. Someone who requires two people and someone who requires three cannot share a group.\nWe can now read the input and check this small candidate by hand. What we still lack is a way to remember earlier people: person 0 and person 2 are separated by someone else, so where should the program keep the first person until it finds a compatible one?\nStep 2: Store Person IDs by Their Required Group Size Our current baseline is still manual matching. In [2,1,2], we encounter person 0, then person 1, and only then person 2, who can share a group with 0. Looking only at adjacent positions gives us no place to keep the earlier person.\nAdd a dictionary named buckets. Each key is a required group size, and its value is a list of IDs encountered so far that require that size. For example, buckets[2] = [0,2] means both people need a two-person group. We call this list a \u0026ldquo;bucket\u0026rdquo; because it temporarily holds items with the same requirement.\nFirst, write a runnable version that only collects IDs, without arranging the final groups:\ndef collect_by_size(group_sizes: list[int]) -\u0026gt; dict[int, list[int]]: buckets: dict[int, list[int]] = {} for person, size in enumerate(group_sizes): if size not in buckets: buckets[size] = [] buckets[size].append(person) return buckets assert collect_by_size([2, 1, 2]) == {2: [0, 2], 1: [1]} assert collect_by_size([2, 2, 2, 2]) == {2: [0, 1, 2, 3]} enumerate provides both the index, person, and the value, size. Use size to find the bucket, then put person inside it. When size 2 appears again, we use its existing list rather than overwrite the IDs already stored there.\nThe first assertion resolves the nonadjacent-matches problem. The second also reveals a limit: all four people require two-person groups, but collecting them produces a bucket of four. One bucket means one shared requirement, not necessarily one valid group.\nThis version can now collect every compatible person ID. It still cannot return all the buckets directly as the answer. The next question is how to split a bucket when its population exceeds the required group size.\nStep 3: Split Each Bucket into Groups of the Required Size The current version turns [2,2,2,2] into {2: [0,1,2,3]}. All four people need two-person groups, so returning that entire list would put every person in an oversized group.\nWe do not need a new classification rule. We need to enforce capacity. From the size-2 bucket, take two people at a time: [0,1], then [2,3]. There are no additional restrictions on who can be paired, so there is no need to search for a \u0026ldquo;better\u0026rdquo; pair.\nAdd a splitting loop to the previous version: keep the collection code, then replace return buckets with capacity-sized slices and return the resulting groups.\ndef group_people_two_pass(group_sizes: list[int]) -\u0026gt; list[list[int]]: buckets: dict[int, list[int]] = {} for person, size in enumerate(group_sizes): if size not in buckets: buckets[size] = [] buckets[size].append(person) result: list[list[int]] = [] for size, people in buckets.items(): for start in range(0, len(people), size): result.append(people[start:start + size]) return result assert group_people_two_pass([2, 2, 2, 2]) == [[0, 1], [2, 3]] start advances by size each time, and people[start:start + size] extracts one group. This assertion checks this implementation\u0026rsquo;s particular split. It does not mean the problem accepts only this arrangement.\nCould the last slice contain fewer than size people? Not for a valid problem input. Suppose m people require groups of size k. Those people can only join groups of size k; groups of another size cannot accept them. Since a valid answer is guaranteed to exist, m must be a multiple of k. Every slice therefore contains exactly enough people.\nFor the two problem examples, check capacities and ID coverage instead of comparing against one supposedly unique arrangement:\nInput This implementation returns Check [3,3,3,3,3,1,3] [[0,1,2],[3,4,6],[5]] Two three-person groups and one singleton; IDs 0..6 appear once each [2,1,3,3,3,2] [[0,5],[1],[2,3,4]] Groups of two, one, and three; IDs 0..5 appear once each This is our first complete, correct solution. With expected constant-time hash-table operations, collecting the IDs takes O(n) time. Slicing copies lists, but all slices together copy only n IDs, so splitting also takes O(n) time. The temporary buckets require O(n) auxiliary space, and the returned groups take another O(n) space.\nThe solution has no time-complexity problem, but it stores everybody before starting the second pass. For [2,2,2,2], the group [0,1] is already full when we reach person 1. Could we emit that group immediately and leave only unfinished groups in the buckets?\nStep 4: Emit a Full Group, Then Start a New List Our current baseline takes two passes: collect every ID into a bucket, then split the buckets. It is already correct; the issue is simply that completed groups wait until the scan ends before being emitted.\nConsider people 0 and 1, both requesting groups of two. After appending 1 to buckets[2], the list\u0026rsquo;s length equals its capacity, 2. We can add it to result immediately, then make buckets[2] the temporary storage for the next group.\nReplace the previous collection loop with the version below. We add only a branch for reaching capacity; the second splitting pass is no longer needed:\ndef group_people(group_sizes: list[int]) -\u0026gt; list[list[int]]: buckets: dict[int, list[int]] = {} result: list[list[int]] = [] for person, size in enumerate(group_sizes): if size not in buckets: buckets[size] = [] buckets[size].append(person) if len(buckets[size]) == size: result.append(buckets[size]) buckets[size] = [] return result assert group_people([2, 2, 2, 2]) == [[0, 1], [2, 3]] assert group_people([3, 3, 3, 3, 3, 1, 3]) == [ [0, 1, 2], [5], [3, 4, 6] ] The crucial line is buckets[size] = []: it rebinds that dictionary entry to a new list. Do not clear the existing list instead:\nbad_bucket = [0, 1] bad_result = [] bad_result.append(bad_bucket) bad_bucket.clear() assert bad_result == [[]] bad_result holds a reference to the same list object. Clearing the temporary list also clears the group we already handed to the result. By contrast, buckets[size] = [] leaves the old list in result and uses a new one for the next group. This change in list ownership is essential when one requested size produces several groups: it keeps completed results from being modified by later work.\nWalk through [2,1,2,2,2,1] in input order. The bucket column shows its contents after appending the current person, before any full group is emitted and its bucket replaced:\nPerson read (required size) Bucket Full? Result 0 (2) [0] No [] 1 (1) [1] Yes [[1]] 2 (2) [0,2] Yes [[1],[0,2]] 3 (2) [3] No [[1],[0,2]] 4 (2) [3,4] Yes [[1],[0,2],[3,4]] 5 (1) [5] Yes [[1],[0,2],[3,4],[5]] For valid input, the total number of people requesting each size is divisible by that size, so the scan cannot leave any unfinished groups. This version removes an explicit splitting pass, but its total work is still O(n). We have not improved the asymptotic bound; we have moved the completion check to the earliest point where a group is ready.\nWe now have a correct function that needs no second pass. One final step remains: put the same logic into LeetCode\u0026rsquo;s Solution.groupThePeople interface, justify it for the whole input, and check the complete code without depending on output order.\nStep 5: Package the Submission and Prove It Correct The current group_people function already implements the algorithm. LeetCode requires a class method, so this step only packages the interface. The loop, completion condition, and list-rebinding rule stay unchanged:\nfrom typing import List class Solution: def groupThePeople(self, groupSizes: List[int]) -\u0026gt; List[List[int]]: buckets: dict[int, list[int]] = {} result: list[list[int]] = [] for person, size in enumerate(groupSizes): if size not in buckets: buckets[size] = [] buckets[size].append(person) if len(buckets[size]) == size: result.append(buckets[size]) buckets[size] = [] return result Correctness: What the Scan Maintains For each group size k, buckets[k] contains only people we have encountered who require size k and have not yet been emitted into the result. Each new person with that requirement is appended to this bucket. Once its length reaches k, it is a valid group of exactly k people. We append it to the result and replace its bucket with a fresh empty list.\nThis invariant gives us three conclusions:\nEvery emitted group has the right capacity. We emit only when len(buckets[k]) == k, so the group length equals every member\u0026rsquo;s required size. A person cannot be duplicated or lost between groups. Each ID is appended once by enumerate, to exactly one bucket. After its list is emitted, we never write to that list again; later IDs go into a new list. Every encountered person therefore remains either in one pending bucket or in one completed group. No incomplete group remains at the end. A valid answer is guaranteed. People requesting size k cannot join groups of another size, so their total count must be a multiple of k. Emitting each full batch leaves no partial bucket at the end. Combined with the previous point, this means every person appears in exactly one output group. Complexity Each person is read once, appended once, and included in one emitted group. With expected O(1) hash-table operations, the total time is O(n). The buckets and returned groups store person IDs; worst-case auxiliary space and output space are both O(n). A single scan improves how the work is organized, not the asymptotic bound below O(n).\nValidating an Answer with No Fixed Order Do not compare the result against just one fixed arrangement. Instead, check the requirements directly:\ndef validate(group_sizes: list[int], groups: list[list[int]]) -\u0026gt; None: assert all(groups) flat = [person for group in groups for person in group] assert sorted(flat) == list(range(len(group_sizes))) assert all( group_sizes[person] == len(group) for group in groups for person in group ) for sizes in ([3, 3, 3, 3, 3, 1, 3], [2, 1, 3, 3, 3, 2], [1], [2, 2, 2, 2]): validate(sizes, Solution().groupThePeople(sizes)) The first assertion rejects empty groups. The flattened IDs must then cover every person exactly once, and each member\u0026rsquo;s required size must match their group\u0026rsquo;s length. These checks describe valid output rather than one particular ordering.\nAlso cover the n = 500 boundary and valid random inputs made by constructing complete groups and shuffling their required sizes. What matters is that each person appears once and every nonempty group has the right capacity, not the order in which groups appear.\nSummary The key distinction is not about sorting: an array index identifies a person, while the value is that person\u0026rsquo;s required group capacity. From that distinction, the code develops through three connected versions:\nCollect compatible person IDs in buckets keyed by capacity. After collection, split each bucket into groups of that capacity. Emit each full group during the scan and use a new list for the next group. The last detail also applies beyond this problem. Once a list has been handed to a result collection, do not clear it in place to reuse it. Rebinding to a new list keeps later writes from changing the data already returned to the result.\nPractice and Further Reading Replace buckets with collections.defaultdict(list) and compare how it changes the initialization logic. Write an ID-coverage check without sorting. Consider how it could reduce validation costs if the constraints grew. Compare LeetCode 49: Group Anagrams. Both problems group items by a key. Here, the key comes directly from the required capacity, and we need an additional completion rule because one bucket can produce several groups. Reference: LeetCode 1282: Group the People Given the Group Size They Belong To.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/1282-group-the-people-given-the-group-size-they-belong-to/","summary":"People who request the same group size belong in the same bucket, but one bucket may contain several groups. Build two connected solutions and learn to validate answers with no fixed order.","title":"LeetCode 1282: Group the People, from Capacity Buckets to Emitting Full Groups"},{"content":"Problem Requirements Given an array of strings strs, place anagrams in the same group and return all the groups. Two words are anagrams when they contain the same letters with the same number of occurrences of each letter; their character order may differ.\nAn example from the problem:\nInput: strs = [\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;, \u0026#34;tan\u0026#34;, \u0026#34;ate\u0026#34;, \u0026#34;nat\u0026#34;, \u0026#34;bat\u0026#34;] One valid output: [[\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;, \u0026#34;ate\u0026#34;], [\u0026#34;tan\u0026#34;, \u0026#34;nat\u0026#34;], [\u0026#34;bat\u0026#34;]] Neither the order of groups nor the order of words within a group matters. However, every input occurrence must be preserved: [\u0026quot;a\u0026quot;, \u0026quot;a\u0026quot;] must produce [[\u0026quot;a\u0026quot;, \u0026quot;a\u0026quot;]], not a deduplicated result.\nThe constraints are 1 \u0026lt;= len(strs) \u0026lt;= 10^4, each string has length 0..100, and all characters are lowercase English letters. Empty strings are valid: [\u0026quot;\u0026quot;] produces [[\u0026quot;\u0026quot;]].\nThis article uses Python 3.10+ and assumes you can write loops. The goal is to understand what makes a valid basis for grouping.\nStep 1: Solve the Smaller Problem of Comparing Two Words Set the full array aside for a moment and answer a smaller question: should these two words belong to the same group?\nDirect string comparison is too strict: \u0026quot;eat\u0026quot; != \u0026quot;tea\u0026quot;, yet they should be grouped together. Could we compare only which letters appear? This custom counterexample shows why that also fails:\naab: a appears twice, b appears once abb: a appears once, b appears twice Both character sets are {\u0026quot;a\u0026quot;, \u0026quot;b\u0026quot;}, but one word cannot be rearranged into the other. Sets discard multiplicity; we only want to ignore order.\nMake one small change: sort the characters in each word, then compare the results.\neat -\u0026gt; aet tea -\u0026gt; aet aab -\u0026gt; aab abb -\u0026gt; abb Sorting preserves every character occurrence while putting the characters in a common order. Two words are therefore anagrams if and only if their sorted character lists are equal. Here is the smallest runnable version of that test:\ndef same_anagram(a: str, b: str) -\u0026gt; bool: return sorted(a) == sorted(b) assert same_anagram(\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;) assert not same_anagram(\u0026#34;aab\u0026#34;, \u0026#34;abb\u0026#34;) assert same_anagram(\u0026#34;\u0026#34;, \u0026#34;\u0026#34;) assert not same_anagram(\u0026#34;a\u0026#34;, \u0026#34;aa\u0026#34;) assert set(\u0026#34;aab\u0026#34;) == set(\u0026#34;abb\u0026#34;) # Equal sets are not enough to establish anagrams. This version can decide whether two words belong together without losing repeated letters. It cannot yet group an entire array. Next, we need to decide whether each new word joins an existing group or starts a new one.\nStep 2: Use the First Word in Each Group as Its Representative Our existing same_anagram only compares two words. When we reach the third word, \u0026quot;tan\u0026quot;, we also need to remember the groups formed so far.\nStore those groups in a list named groups. Every group is nonempty, so group[0] can represent it: if the new word is an anagram of that representative, append it to the group. If none of the representatives match, create a new group containing just the new word.\nWhy not compare against every word in a group? All existing members have the same sorted characters as the representative. If the new word also matches that sorted sequence, it matches every member of the group.\nPlace the following function after the previous same_anagram definition to get the first complete solution:\ndef group_scan(strs: list[str]) -\u0026gt; list[list[str]]: groups: list[list[str]] = [] for word in strs: for group in groups: if same_anagram(word, group[0]): group.append(word) break else: groups.append([word]) return groups assert group_scan([\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;, \u0026#34;tan\u0026#34;, \u0026#34;ate\u0026#34;, \u0026#34;nat\u0026#34;, \u0026#34;bat\u0026#34;]) == [ [\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;, \u0026#34;ate\u0026#34;], [\u0026#34;tan\u0026#34;, \u0026#34;nat\u0026#34;], [\u0026#34;bat\u0026#34;] ] assert group_scan([\u0026#34;\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;]) == [ [\u0026#34;\u0026#34;, \u0026#34;\u0026#34;], [\u0026#34;a\u0026#34;, \u0026#34;a\u0026#34;], [\u0026#34;b\u0026#34;] ] The else belongs to the inner for, not the if. In Python, for ... else runs the else block when the loop finishes without a break. A match appends the word and breaks out of the scan; no match creates a new group. When groups is initially empty, the inner loop runs zero times and its else still executes, creating the first group.\nTrace the first four words slowly:\nNew Word Existing Representatives Checked Action groups Afterward eat None yet Create a group [[\u0026quot;eat\u0026quot;]] tea Matches eat Append to the first group [[\u0026quot;eat\u0026quot;, \u0026quot;tea\u0026quot;]] tan Does not match eat Create a group [[\u0026quot;eat\u0026quot;, \u0026quot;tea\u0026quot;], [\u0026quot;tan\u0026quot;]] ate Matches eat Append to the first group [[\u0026quot;eat\u0026quot;, \u0026quot;tea\u0026quot;, \u0026quot;ate\u0026quot;], [\u0026quot;tan\u0026quot;]] Each iteration either appends to one existing group or creates one new group. Duplicate words are therefore preserved. The assertions check the order this implementation happens to produce; the problem itself does not require that order.\nThis version already solves the entire problem correctly. Its remaining weakness is efficiency: if every word belongs to a different group, the i-th word checks all i - 1 earlier representatives, sorting both words again for each comparison.\nStep 3: Turn the Sorted Result into a Dictionary Key The previous version\u0026rsquo;s bottleneck is not the anagram test itself. To find a word\u0026rsquo;s group, it must try the existing groups one by one. If all n words belong to different groups, that means n(n - 1) / 2 representative comparisons. Since same_anagram already converts each word into a stable sorted sequence, could that sequence locate its group directly?\nLook at the sorted forms:\neat -\u0026gt; aet tea -\u0026gt; aet tan -\u0026gt; ant Equal sorted sequences identify exactly the same anagram class. sorted(word) returns a list, which cannot be a dictionary key. Joining it into a string gives us an immutable, hashable key such as \u0026quot;aet\u0026quot;.\nReplace the groups list with a dictionary named groups_by_key. Each key is a sorted string; its value is still a list of original words. We do not replace \u0026quot;eat\u0026quot; with \u0026quot;aet\u0026quot; in the output: the key only identifies the group.\nThis change replaces the search for a group. We still append the original word to its group and collect all groups at the end:\ndef group_by_sorted(strs: list[str]) -\u0026gt; list[list[str]]: groups_by_key: dict[str, list[str]] = {} for word in strs: key = \u0026#34;\u0026#34;.join(sorted(word)) if key not in groups_by_key: groups_by_key[key] = [] groups_by_key[key].append(word) return list(groups_by_key.values()) assert group_by_sorted([\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;, \u0026#34;tan\u0026#34;, \u0026#34;ate\u0026#34;, \u0026#34;nat\u0026#34;, \u0026#34;bat\u0026#34;]) == [ [\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;, \u0026#34;ate\u0026#34;], [\u0026#34;tan\u0026#34;, \u0026#34;nat\u0026#34;], [\u0026#34;bat\u0026#34;] ] assert group_by_sorted([\u0026#34;\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;]) == [ [\u0026#34;\u0026#34;, \u0026#34;\u0026#34;], [\u0026#34;a\u0026#34;, \u0026#34;a\u0026#34;], [\u0026#34;b\u0026#34;] ] We now locate groups through dictionary operations rather than scanning existing groups. Under the usual average-case hash-table model, lookup avoids a search across all groups; constructing and hashing the string key still requires work on its characters. Two keys are equal if and only if their sorted character sequences match, so the same anagram test still governs membership.\nThis is already a clear, correct, commonly used solution. It still sorts every word, though. The problem limits the alphabet to 26 lowercase English letters, which gives us one more question to investigate: do we need to arrange the characters in order at all?\nStep 4: Build the Final Key from 26 Character Counts Sorting makes each word\u0026rsquo;s letters and their multiplicities comparable. Because the input contains only lowercase English letters, we can record the number of occurrences of each letter directly: counts[0] represents a, counts[1] represents b, and so on.\nFor example, these vectors abbreviate the intervening zero counts:\neat -\u0026gt; (1, 0, 0, 0, 1, ..., 1, ..., 0) tea -\u0026gt; (1, 0, 0, 0, 1, ..., 1, ..., 0) aab -\u0026gt; (2, 1, 0, ..., 0) abb -\u0026gt; (1, 2, 0, ..., 0) The first two 26-element vectors match; the last two do not. A list cannot be a dictionary key, but converting this list of integers into an immutable tuple makes it usable as one. Each character is visited once, so constructing the key for a word of length k costs O(k + 26), including initialization and tuple creation.\nOnly the key construction needs to change; the dictionary grouping loop can stay as it is. First, test the new key on its own:\ndef count_key(word: str) -\u0026gt; tuple[int, ...]: counts = [0] * 26 for char in word: counts[ord(char) - ord(\u0026#34;a\u0026#34;)] += 1 return tuple(counts) assert count_key(\u0026#34;eat\u0026#34;) == count_key(\u0026#34;tea\u0026#34;) assert count_key(\u0026#34;aab\u0026#34;) != count_key(\u0026#34;abb\u0026#34;) assert count_key(\u0026#34;\u0026#34;) == (0,) * 26 ord(char) - ord(\u0026quot;a\u0026quot;) maps a to index 0, b to 1, and z to 25. For \u0026quot;eat\u0026quot;, it increments indices 4, 0, and 19, leaving every other count at zero. This indexing works because of the problem\u0026rsquo;s lowercase-English constraint.\nNow put that key construction into the earlier dictionary loop and use the platform\u0026rsquo;s class Solution interface. The class wrapper adds no algorithm. The counting code is written inline below so this final block runs independently, without requiring the demonstration helper above:\nclass Solution: def groupAnagrams(self, strs: list[str]) -\u0026gt; list[list[str]]: groups_by_key: dict[tuple[int, ...], list[str]] = {} for word in strs: counts = [0] * 26 for char in word: counts[ord(char) - ord(\u0026#34;a\u0026#34;)] += 1 key = tuple(counts) if key not in groups_by_key: groups_by_key[key] = [] groups_by_key[key].append(word) return list(groups_by_key.values()) solution = Solution() assert solution.groupAnagrams([\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;, \u0026#34;tan\u0026#34;, \u0026#34;ate\u0026#34;, \u0026#34;nat\u0026#34;, \u0026#34;bat\u0026#34;]) == [ [\u0026#34;eat\u0026#34;, \u0026#34;tea\u0026#34;, \u0026#34;ate\u0026#34;], [\u0026#34;tan\u0026#34;, \u0026#34;nat\u0026#34;], [\u0026#34;bat\u0026#34;] ] assert solution.groupAnagrams([\u0026#34;\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;]) == [ [\u0026#34;\u0026#34;, \u0026#34;\u0026#34;], [\u0026#34;a\u0026#34;, \u0026#34;a\u0026#34;], [\u0026#34;b\u0026#34;] ] The loop invariant is: after processing each word, every entry in groups_by_key contains exactly the original words processed so far whose count vector equals that entry\u0026rsquo;s key. Initially there are no processed words, so an empty dictionary satisfies it. Each iteration computes the current word\u0026rsquo;s vector and appends it exactly once to the matching group, creating that group if needed; all other groups stay unchanged.\nTwo count vectors are equal if and only if every letter has the same multiplicity in both words, which is precisely the anagram condition. At the end, every word is therefore in the correct group and every input occurrence has been preserved. The solution is complete for the stated problem; the 26-slot key does not apply unchanged to arbitrary alphabets.\nComplexity Let S be the total number of characters, n the number of words, and g the number of groups. We count each character once. Initializing, converting, and hashing each fixed-size key also takes work proportional to 26, so the expected running time is O(S + 26n), or O(S + n) with the alphabet size treated as a constant. This uses average-case hash-table behavior.\nWe could simplify that to O(S) if every word had at least one character. This problem allows empty words, though: processing n empty strings still takes O(n) time even when S = 0. Keeping the + n makes that case explicit.\nThe dictionary entries and their keys occupy O(26g) space. The group lists store O(n) references to input strings, and the returned outer list holds O(g) group references; the strings themselves are not copied. Including those output lists, space usage is O(26g + n). Excluding output storage, the dictionary and keys account for O(26g) auxiliary space, plus the current 26-element counter.\nIf we do not want to rely on the lowercase-English constraint, Step 3\u0026rsquo;s sorted-string key is more general. Writing the i-th word\u0026rsquo;s length as k_i, its expected time is O(n + sum(k_i * log(k_i + 1))). The n covers per-word overhead, including empty words, and log(k_i + 1) also handles lengths zero and one cleanly. Counting is not automatically the better choice: it trades a fixed alphabet assumption for linear character processing.\nBoundaries and Common Mistakes The key for an empty string contains 26 zeros. Multiple empty strings belong together; do not skip them. Preserve multiplicities. Using only set(word) would incorrectly merge aab and abb. Return lists of original words, not the sorted keys. A key locates a group; it is not the output value. ord(char) - ord(\u0026quot;a\u0026quot;) depends on the guaranteed lowercase English letters. For mixed case, Unicode, or another alphabet, reconsider the key representation and any normalization rules. Output order is unrestricted. The order shown in the assertions follows this implementation\u0026rsquo;s traversal; it is not an additional problem requirement. Takeaways The key to this problem is not memorizing a defaultdict template. It is working out which kind of equality means that two items belong in the same group:\nCompare sorted character sequences to recognize two anagrams. Scan group representatives to turn that test into a first complete solution. Use the sorted sequence as a dictionary key to eliminate the linear search through groups. Use a count tuple under the fixed 26-letter constraint to avoid sorting and obtain the final Solution. As an exercise, keep the slower Step 2 implementation as a reference for small test cases and compare its groups with the final version\u0026rsquo;s results. The optimization changes how we locate groups, not what the groups mean.\nReferences LeetCode 49: Group Anagrams Doocs problem and solution mirror ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/49-group-anagrams/","summary":"Build a Python solution one step at a time: compare two anagrams, scan group representatives, then derive sorted-string and character-count keys to understand why hash-table grouping works.","title":"LeetCode 49: Group Anagrams, from Scanning Groups to Building a Grouping Key"},{"content":"Problem: Compare Contents, Return Full Paths You are given an array of strings, paths. Each string describes one directory and its files:\ndirectory_path filename(content) filename(content) ... root/a 1.txt(abcd) 2.txt(efgh) Group files whose contents are exactly equal, and return the full paths of the files in each group. Every returned group must contain at least two files; omit contents that occur in only one file. The order of groups and the order of paths within each group do not matter.\nFor example:\nInput: [ \u0026#34;root/a 1.txt(abcd) 2.txt(efgh)\u0026#34;, \u0026#34;root/c 3.txt(abcd)\u0026#34;, \u0026#34;root/c/d 4.txt(efgh)\u0026#34;, \u0026#34;root 4.txt(efgh)\u0026#34; ] One valid output: [ [\u0026#34;root/a/1.txt\u0026#34;, \u0026#34;root/c/3.txt\u0026#34;], [\u0026#34;root/a/2.txt\u0026#34;, \u0026#34;root/c/d/4.txt\u0026#34;, \u0026#34;root/4.txt\u0026#34;] ] Here, 1.txt and 3.txt have different names but the same contents, abcd, so they belong together. Conversely, matching filenames do not imply matching contents. A directory can also be just root; do not assume that it contains a subdirectory.\nThe main constraints are:\n1 \u0026lt;= paths.length \u0026lt;= 2 * 10^4. Each directory description has at most 3000 characters, and all descriptions together have at most 5 * 10^5 characters. Each description contains at least one file. A single space separates the directory and file entries. Each description refers to a different directory. Files or directories within the same directory do not share a name. These strings already follow the problem\u0026rsquo;s encoding. The program does not need to open directories on disk. This tutorial uses Python 3.10+ and assumes familiarity with loops, lists, and dictionaries.\nStep 1: Parse Just One File Start with a smaller question: given the directory root/a and the entry 1.txt(abcd), can we recover the full path and the contents?\nOur current baseline is only the input and output format. Comparing the entire entry 1.txt(abcd) would also compare filenames, incorrectly separating it from 3.txt(abcd). Taking only the filename would not tell us whether the contents match either.\nMake one change: split the entry into the full path we must return and the contents we must compare.\ndirectory = \u0026#34;root/a\u0026#34; entry = \u0026#34;1.txt(abcd)\u0026#34; name, content_and_end = entry.split(\u0026#34;(\u0026#34;, 1) content = content_and_end[:-1] full_path = directory + \u0026#34;/\u0026#34; + name assert name == \u0026#34;1.txt\u0026#34; assert content == \u0026#34;abcd\u0026#34; assert full_path == \u0026#34;root/a/1.txt\u0026#34; assert \u0026#34;root\u0026#34; + \u0026#34;/\u0026#34; + name == \u0026#34;root/1.txt\u0026#34; assert \u0026#34;root/a/b\u0026#34; + \u0026#34;/\u0026#34; + name == \u0026#34;root/a/b/1.txt\u0026#34; split(\u0026quot;(\u0026quot;, 1) splits only at the first opening parenthesis, producing 1.txt and abcd). The slice [:-1] removes the final closing parenthesis that ends the entry. Do not use strip(\u0026quot;()\u0026quot;): we want to remove delimiters at specific positions, not an arbitrary run of characters at either end.\nThis parser relies on the supplied filename(content) format. It is not a general parser for arbitrary operating-system filenames, escapes, or contents containing spaces.\nCheckpoint: We can now turn one file entry into (full_path, content). The program still handles only a single hardcoded file; it cannot traverse the complete input yet.\nStep 2: Expand Every Directory into File Records The current version parses one file, but the first line of the example already contains two. Another file with matching contents may appear in a later directory. Taking only the first file on each line would miss root/a/2.txt.\nThis step does not decide which files are duplicates. Put the existing parser inside two loops: the outer loop reads directory descriptions, and the inner loop reads the files in that directory. Store (full_path, content) pairs in records so the next step can compare all the files.\ndef collect_files(paths: list[str]) -\u0026gt; list[tuple[str, str]]: records = [] for line in paths: parts = line.split(\u0026#34; \u0026#34;) directory = parts[0] for entry in parts[1:]: name, content_and_end = entry.split(\u0026#34;(\u0026#34;, 1) content = content_and_end[:-1] full_path = directory + \u0026#34;/\u0026#34; + name records.append((full_path, content)) return records example_paths = [ \u0026#34;root/a 1.txt(abcd) 2.txt(efgh)\u0026#34;, \u0026#34;root/c 3.txt(abcd)\u0026#34;, \u0026#34;root/c/d 4.txt(efgh)\u0026#34;, \u0026#34;root 4.txt(efgh)\u0026#34;, ] assert collect_files(example_paths) == [ (\u0026#34;root/a/1.txt\u0026#34;, \u0026#34;abcd\u0026#34;), (\u0026#34;root/a/2.txt\u0026#34;, \u0026#34;efgh\u0026#34;), (\u0026#34;root/c/3.txt\u0026#34;, \u0026#34;abcd\u0026#34;), (\u0026#34;root/c/d/4.txt\u0026#34;, \u0026#34;efgh\u0026#34;), (\u0026#34;root/4.txt\u0026#34;, \u0026#34;efgh\u0026#34;), ] parts[0] is the directory, not a file, so the inner loop starts at parts[1:]. Splitting on spaces still relies on the problem\u0026rsquo;s serialized format; it is not suitable for parsing arbitrary real file contents.\nThe assertion checks that four directory descriptions expand into exactly five file records. The second file on the first line is retained, and the directory containing only root receives the correct / separator too.\nCheckpoint: Every file now has contents we can compare and a path we can return. We still need to decide which records belong to the same duplicate group.\nStep 3: Build a Correct Pairwise Baseline Our baseline is now collect_files, which returns every (path, content) pair. The most direct next step is to choose one record, compare its contents with each later record, and collect the matches.\nThere is a specific problem to resolve first. If three files all contain abcd, returning the pairs (A, B) and (A, C) would split one duplicate group into two overlapping groups. Each record must belong to just one content group, and groups with only one member must stay out of the answer.\nAdd assigned to remember whether a record already belongs to an earlier group. Choose each unassigned record as a seed, scan the later records, and append matching records to the current group. Mark each appended record as assigned immediately.\ndef find_duplicate_pairwise(paths: list[str]) -\u0026gt; list[list[str]]: records = collect_files(paths) assigned = [False] * len(records) result = [] for i, (seed_path, seed_content) in enumerate(records): if assigned[i]: continue assigned[i] = True group = [seed_path] for j in range(i + 1, len(records)): if not assigned[j] and records[j][1] == seed_content: assigned[j] = True group.append(records[j][0]) if len(group) \u0026gt; 1: result.append(group) return result def normalize(groups: list[list[str]]) -\u0026gt; list[tuple[str, ...]]: return sorted(tuple(sorted(group)) for group in groups) three_equal = [ \u0026#34;root/a 1.txt(abcd)\u0026#34;, \u0026#34;root/b 2.txt(abcd)\u0026#34;, \u0026#34;root/c 3.txt(abcd)\u0026#34;, ] assert normalize(find_duplicate_pairwise(three_equal)) == normalize([ [\u0026#34;root/a/1.txt\u0026#34;, \u0026#34;root/b/2.txt\u0026#34;, \u0026#34;root/c/3.txt\u0026#34;], ]) assert find_duplicate_pairwise([\u0026#34;root 1.txt(a) 2.txt(b)\u0026#34;]) == [] assert find_duplicate_pairwise([ \u0026#34;root 1.txt(a)\u0026#34;, \u0026#34;other 1.txt(b)\u0026#34;, ]) == [] assert normalize(find_duplicate_pairwise([ \u0026#34;root 1.txt(a)\u0026#34;, \u0026#34;other 1.txt(a) 2.txt(b)\u0026#34;, ])) == normalize([[\u0026#34;root/1.txt\u0026#34;, \u0026#34;other/1.txt\u0026#34;]]) assert normalize(find_duplicate_pairwise(example_paths)) == normalize([ [\u0026#34;root/a/1.txt\u0026#34;, \u0026#34;root/c/3.txt\u0026#34;], [\u0026#34;root/a/2.txt\u0026#34;, \u0026#34;root/c/d/4.txt\u0026#34;, \u0026#34;root/4.txt\u0026#34;], ]) normalize is only a test helper: sorting removes irrelevant ordering differences while preserving repeated paths or repeated groups, so an accidental duplicate cannot disappear from the check. It is not part of the grouping algorithm.\nFor the three-equal case, record A starts a group and is marked assigned. Its scan adds B and C, marking both assigned too. The outer loop then skips B and C, so it returns [A, B, C] exactly once. The condition len(group) \u0026gt; 1 enforces the requirement that a returned group contain at least two files.\nCheckpoint: This is a complete, correct baseline. Files with different names but equal contents merge; files with the same name but different contents stay separate; singleton groups are omitted. The remaining weakness is repeated work: when all contents differ, almost every seed scans all the records after it.\nStep 4: Use Contents as the Grouping Key The previous version is correct, but distinct contents force it to revisit the same set of records. The first seed checks every later record; the second checks nearly all the remaining records again. For F files with distinct contents, that is F * (F - 1) / 2 candidate comparisons before accounting for the cost of comparing strings.\nWe already know that content alone decides which group a file belongs to. Instead of searching for matching records, put each path directly into the bucket for its contents.\nReplace only the group-building mechanism. Keep collect_files and the singleton filter unchanged:\ndef find_duplicate_by_content(paths: list[str]) -\u0026gt; list[list[str]]: records = collect_files(paths) groups_by_content: dict[str, list[str]] = {} for full_path, content in records: if content not in groups_by_content: groups_by_content[content] = [] groups_by_content[content].append(full_path) return [ group for group in groups_by_content.values() if len(group) \u0026gt; 1 ] expected = normalize([ [\u0026#34;root/a/1.txt\u0026#34;, \u0026#34;root/c/3.txt\u0026#34;], [\u0026#34;root/a/2.txt\u0026#34;, \u0026#34;root/c/d/4.txt\u0026#34;, \u0026#34;root/4.txt\u0026#34;], ]) assert normalize(find_duplicate_by_content(example_paths)) == expected assert normalize(find_duplicate_by_content(three_equal)) == normalize([ [\u0026#34;root/a/1.txt\u0026#34;, \u0026#34;root/b/2.txt\u0026#34;, \u0026#34;root/c/3.txt\u0026#34;], ]) assert find_duplicate_by_content([\u0026#34;root 1.txt(a) 2.txt(b)\u0026#34;]) == [] assert find_duplicate_by_content([ \u0026#34;root 1.txt(a)\u0026#34;, \u0026#34;other 1.txt(b)\u0026#34;, ]) == [] # Build inputs and expected groups separately from structured test records. import random rng = random.Random(609) for _ in range(20): structured = [] for index in range(rng.randint(1, 6)): directory = f\u0026#34;d{index}\u0026#34; for file_index in range(rng.randint(1, 4)): content = f\u0026#34;c{rng.randrange(5)}\u0026#34; structured.append((directory, f\u0026#34;f{file_index}.txt\u0026#34;, content)) generated_paths = [] for directory in sorted({row[0] for row in structured}): entries = [ f\u0026#34;{name}({content})\u0026#34; for row_directory, name, content in structured if row_directory == directory ] generated_paths.append(\u0026#34; \u0026#34;.join([directory, *entries])) oracle = {} for directory, name, content in structured: oracle.setdefault(content, []).append(f\u0026#34;{directory}/{name}\u0026#34;) expected_random = normalize([ file_paths for file_paths in oracle.values() if len(file_paths) \u0026gt; 1 ]) assert normalize(find_duplicate_by_content(generated_paths)) == expected_random assert normalize(find_duplicate_pairwise(generated_paths)) == expected_random Trace the first three records from the official example. root/a/1.txt creates the abcd bucket; root/a/2.txt creates the efgh bucket; then root/c/3.txt goes directly into the existing abcd bucket. No scan of earlier file records is needed.\nEach record now uses a small, fixed number of dictionary operations and one append. Equal contents lead to the same bucket, while filenames appear only inside the stored paths. The condition len(group) \u0026gt; 1 still excludes singletons, so the optimization does not change the output rule.\nWhy does this cover every duplicate group? Records with equal contents use equal dictionary keys, so all of them enter the same bucket. Records with different contents use unequal keys, so they cannot share a bucket. Each bucket is therefore exactly one content-equivalence class. Python dictionaries check key equality as well as hashes; a hash collision does not turn different content strings into the same key.\nCheckpoint: We have removed the explicit pairwise scan and can group directly by content under the usual hash-table assumptions. The remaining work is to package the same logic as Solution.findDuplicate and account for input characters, constructed path characters, and hashing costs.\nStep 5: Deliver the Submission and State Its Costs Precisely The previous version already groups correctly, but LeetCode expects Solution.findDuplicate. Put the parser and content buckets we have already checked inside that method.\nThere is one small structural change besides the wrapper: inline the parsing loop and append each path to its content bucket immediately, removing the intermediate records list. Parsing rules, grouping keys, and the singleton filter remain unchanged; the two former loops now connect directly without storing a separate copy of their handoff records.\nclass Solution: def findDuplicate(self, paths: list[str]) -\u0026gt; list[list[str]]: groups_by_content: dict[str, list[str]] = {} for line in paths: parts = line.split(\u0026#34; \u0026#34;) directory = parts[0] for entry in parts[1:]: name, content_and_end = entry.split(\u0026#34;(\u0026#34;, 1) content = content_and_end[:-1] full_path = directory + \u0026#34;/\u0026#34; + name if content not in groups_by_content: groups_by_content[content] = [] groups_by_content[content].append(full_path) return [ group for group in groups_by_content.values() if len(group) \u0026gt; 1 ] def normalize(groups: list[list[str]]) -\u0026gt; list[tuple[str, ...]]: return sorted(tuple(sorted(group)) for group in groups) solution = Solution() official_paths = [ \u0026#34;root/a 1.txt(abcd) 2.txt(efgh)\u0026#34;, \u0026#34;root/c 3.txt(abcd)\u0026#34;, \u0026#34;root/c/d 4.txt(efgh)\u0026#34;, \u0026#34;root 4.txt(efgh)\u0026#34;, ] official_expected = normalize([ [\u0026#34;root/a/1.txt\u0026#34;, \u0026#34;root/c/3.txt\u0026#34;], [\u0026#34;root/a/2.txt\u0026#34;, \u0026#34;root/c/d/4.txt\u0026#34;, \u0026#34;root/4.txt\u0026#34;], ]) assert normalize(solution.findDuplicate(official_paths)) == official_expected assert solution.findDuplicate([ \u0026#34;root 1.txt(a)\u0026#34;, \u0026#34;other 1.txt(b)\u0026#34;, ]) == [] assert normalize(solution.findDuplicate([ \u0026#34;deep/directory 1.txt(x) 2.txt(x)\u0026#34;, ])) == normalize([[ \u0026#34;deep/directory/1.txt\u0026#34;, \u0026#34;deep/directory/2.txt\u0026#34;, ]]) assert normalize(solution.findDuplicate([ \u0026#34;root/a 1.txt(same)\u0026#34;, \u0026#34;root/b 2.txt(same)\u0026#34;, \u0026#34;root/c 3.txt(same)\u0026#34;, ])) == normalize([[ \u0026#34;root/a/1.txt\u0026#34;, \u0026#34;root/b/2.txt\u0026#34;, \u0026#34;root/c/3.txt\u0026#34;, ]]) assert solution.findDuplicate([ \u0026#34;root/a 1.txt(a)\u0026#34;, \u0026#34;root/b 2.txt(b)\u0026#34;, \u0026#34;root/c 3.txt(c)\u0026#34;, ]) == [] import random rng = random.Random(6090) for _ in range(40): structured = [] for directory_index in range(rng.randint(1, 7)): directory = f\u0026#34;random/{directory_index}\u0026#34; for file_index in range(rng.randint(1, 5)): structured.append( (directory, f\u0026#34;file{file_index}.txt\u0026#34;, f\u0026#34;content{rng.randrange(8)}\u0026#34;) ) generated_paths = [] directories = sorted({directory for directory, _, _ in structured}) for directory in directories: entries = [ f\u0026#34;{name}({content})\u0026#34; for row_directory, name, content in structured if row_directory == directory ] generated_paths.append(\u0026#34; \u0026#34;.join([directory, *entries])) oracle = {} for directory, name, content in structured: oracle.setdefault(content, []).append(f\u0026#34;{directory}/{name}\u0026#34;) expected_random = normalize([ file_paths for file_paths in oracle.values() if len(file_paths) \u0026gt; 1 ]) assert normalize(solution.findDuplicate(generated_paths)) == expected_random This block is self-contained. Its normalize function and generated cases are tests, not part of the submitted Solution class.\nCorrectness: Each Bucket Is One Content-Equivalence Class After processing any prefix of the file records, each bucket contains exactly the full paths of the processed files whose contents equal its key, each included once. Initially there are no records and no buckets. Processing the next file preserves this invariant by appending its full path once to the bucket selected by its exact contents.\nAfter all records have been processed:\nFiles with equal contents use equal keys, so they appear in the same bucket. Files with different contents use unequal keys, so they cannot appear in the same bucket. Returning only buckets larger than one removes exactly the singleton groups the problem excludes. Together these establish completeness and separation: no duplicate group is missed, and no group contains unequal contents. Filenames and directories only construct return paths; they do not determine grouping.\nComplexity: Count Input Characters and Every Constructed Path Let:\nS be the total number of characters in the input directory descriptions; F be the number of file records; P be the total number of characters in all constructed full paths, including paths in singleton buckets that will not be returned. Parsing the descriptions and copying their filename/content pieces takes O(S) time. Hashing the content strings is not free: summed across the files, their lengths are bounded by S. Under the usual average-case hash-table assumptions, the dictionary operations and content equality checks take expected O(S + F) time in total, accounting for string lengths. Constructing full paths takes O(P), and filtering the buckets takes O(F).\nBecause each file record contributes characters to the input, F \u0026lt;= S, so the solution takes average O(S + P) time. Its auxiliary storage, including the grouped paths and returned lists, is O(S + P): content keys, temporary parsed strings, dictionary/list bookkeeping, and all stored full paths fit within that bound. The returned groups refer to existing path strings; they do not copy every string again.\nDo not drop P. One long directory prefix occurs only once in its input description but is repeated in every full path built for that directory. Counting only directory descriptions also misses that one description can hold many files. These bounds concern Solution.findDuplicate, not the sorting and random-input generation used by the tests.\nHash tables can still degrade in the worst case. The expected bound assumes ordinary hashing behavior; it does not make string hashing constant-time or describe the I/O cost of scanning a real disk.\nWhat Changes in a Real Filesystem? The problem has already done directory traversal for us. For a real filesystem, additional decisions include:\nHow to traverse directories with DFS or BFS, handle permission errors, and avoid cycles through symbolic links. How to group large files by size, then use digests computed in chunks to narrow down candidate duplicates. How to compare candidate files byte by byte before declaring them equal. Matching fixed-length digests are evidence of a possible match, not a mathematical guarantee of equal contents. How to update a digest incrementally when only 1 KB can be read at a time, instead of loading each complete file into memory. These belong to a separate filesystem-scanning problem, not to the serialized-string parser developed here.\nCheckpoint: The platform method is runnable, its grouping invariant and character costs are explicit, and its tests cover duplicates and singleton cases. It solves the supplied-string problem; real directory traversal and file I/O remain outside its scope.\nSummary The useful progression is not simply \u0026ldquo;see duplicates, use a hash table\u0026rdquo;:\nEstablish that complete contents, not names, define a duplicate. Parse each filename(content) entry into contents and a full path. Build a correct pairwise baseline with assigned, so each duplicate group is returned once. Use the repeated-scan bottleneck to justify promoting contents to a dictionary key. Keep the singleton filter, and account for path characters, hashing assumptions, and real-filesystem boundaries before calling the solution complete. When input already describes records and their attributes, first turn it into clear data and then choose the grouping key. As a follow-up exercise, construct one long directory description containing many short filenames and compare its input length with the total length of the full paths. That makes the separate P term concrete.\nReferences LeetCode 609: Find Duplicate File in System Problem statement mirror (Doocs) ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/609-find-duplicate-file-in-system/","summary":"Duplicate files share contents, but the answer contains full paths. Five connected checkpoints build parsing, correct grouping, hash-table optimization, and a submission-ready solution.","title":"LeetCode 609: Find Duplicate File in System, from Parsing Records to Grouping by Content"},{"content":"Problem Requirements Given an integer array nums, find a nonempty contiguous subarray with the greatest product and return that product.\nContiguous means the chosen elements must be adjacent: you cannot skip a number in between. Nonempty means you must choose at least one element. Return only the product, not the range or its length.\nInput: nums = [2, 3, -2, 4] Output: 6 Explanation: The contiguous subarray [2, 3] has the greatest product, 2 * 3 = 6. Input: nums = [-2, 0, -1] Output: 0 Explanation: You cannot skip 0 and treat -2 and -1 as a subarray. Choosing [0] gives 0. The constraints are 1 \u0026lt;= len(nums) \u0026lt;= 20000 and -10 \u0026lt;= nums[i] \u0026lt;= 10. The problem guarantees that the product of any subarray fits in a signed 32-bit integer. The input is never empty, but it may contain negatives and zeros.\nStep 1: Enumerate Every Nonempty Contiguous Range For [2, 3, -2, 4], multiplying the entire array gives -48, whereas the answer is the product of the first two elements, 6. The current \u0026ldquo;multiply all elements\u0026rdquo; approach is missing the choice the problem actually requires: where to start and where to stop.\nBefore trying to predict which range is best, build a direct correct version: enumerate every valid range, calculate its product, and retain the greatest value.\nFix a left endpoint left and move the right endpoint right one position at a time. Multiply the existing range product by the newly included nums[right] to obtain the extended range product. Each extension avoids multiplying the entire range again.\nAdd this complete enumeration function:\ndef max_product_bruteforce(nums: list[int]) -\u0026gt; int: answer = nums[0] for left in range(len(nums)): product = 1 for right in range(left, len(nums)): product *= nums[right] answer = max(answer, product) return answer The two initial values serve different purposes:\nanswer = nums[0] chooses the first element as a valid nonempty range. If the array is just [-2], the answer must remain -2; an initial answer of 0 or 1 would incorrectly override it. product = 1 is the multiplication starting point for each new left endpoint. The answer is updated only after multiplying by nums[right], so the empty product 1 never becomes an answer candidate. For example, fix left = 0 in [2, 3, -2, 4]. The inner loop produces:\nright Current range product answer 0 [2] 2 2 1 [2, 3] 6 6 2 [2, 3, -2] -12 6 3 [2, 3, -2, 4] -48 6 The outer loop then tries left = 1, 2, and 3. Every nonempty contiguous range has exactly one pair of endpoints, so these loops cover all valid ranges.\nRun these checks for the official examples, a three-element optimal range, and a negative singleton:\nassert max_product_bruteforce([2, 3, -2, 4]) == 6 assert max_product_bruteforce([-2, 0, -1]) == 0 assert max_product_bruteforce([-2, 3, -4]) == 24 assert max_product_bruteforce([-2]) == -2 This version answers the problem correctly, but its cost still grows too quickly. An array of length n has n * (n + 1) / 2 nonempty contiguous ranges. At the maximum length of 20000, that means 200010000 range extensions.\nThe current time complexity is O(n²), with O(1) extra space. The next step must find intermediate results we can retain so that every left endpoint does not need another traversal of the remaining elements.\nStep 2: A Negative Number Forces Us to Keep Both Extremes The previous version enumerates every range correctly, but ranges with neighboring right endpoints repeat work. A range ending at index i either contains only nums[i] or extends a nonempty range ending at i - 1 by appending nums[i]. The second case can reuse a result from the previous position.\nA first attempt might retain only \u0026ldquo;the greatest product ending at the current position.\u0026rdquo; At each position, compare starting again with the current number against extending the previously best range:\ncurrent ending maximum = max(current number, previous ending maximum * current number) But on [-2, 3, -4], that compression discards an essential candidate:\nIndex Current number Only one ending maximum retained Lost information 0 -2 -2 None 1 3 max(3, -2 * 3) = 3 The product -6 of [-2, 3] 2 -4 max(-4, 3 * -4) = -4 -6 * -4 = 24 The greatest of those ending maxima is only 3, whereas the brute-force version already verified that the correct answer is 24. The sign of multiplication causes the failure: a smaller negative product may become the greatest positive product after multiplying by a new negative number.\nWe therefore need two states, and both must require the range to end exactly at index i:\nmax_end[i]: the greatest product of a nonempty contiguous subarray ending at i. min_end[i]: the smallest product of a nonempty contiguous subarray ending at i. Let the current number be x = nums[i]. The single-element range contributes x. When extending an old range, positive x preserves product order, negative x reverses it, and zero makes every extended product zero. We therefore do not need every old range product: the previous greatest and smallest products suffice.\nBoth new states choose from the same three candidates:\nx start a new range here max_end[i - 1] * x extend the previous greatest-product range min_end[i - 1] * x extend the previous smallest-product range The greatest candidate becomes max_end[i]; the smallest becomes min_end[i]. This is the dynamic programming transition here: the problem ending at i builds on the already solved smaller problem ending at i - 1.\nReplace the previous range enumeration with this paired transition:\ndef max_product_table(nums: list[int]) -\u0026gt; int: size = len(nums) max_end = [0] * size min_end = [0] * size max_end[0] = min_end[0] = nums[0] for i in range(1, size): x = nums[i] previous_max = max_end[i - 1] previous_min = min_end[i - 1] max_end[i] = max(x, previous_max * x, previous_min * x) min_end[i] = min(x, previous_max * x, previous_min * x) return max(max_end) At index 0, the only valid ending range is [nums[0]], so both states start at nums[0]. The loop fills every remaining position before it is read; the arrays\u0026rsquo; initial zeros never enter a candidate comparison. previous_max and previous_min are the values from the previous position, and both new states read that same old pair.\nThe earlier counterexample now produces:\ni x Three candidates max_end[i] min_end[i] 0 -2 Only [-2] -2 -2 1 3 3, -6, -6 3 -6 2 -4 -4, -12, 24 24 -12 Why return max(max_end) rather than max_end[-1]? The state requires a range to end at the current position, but the problem allows it to end anywhere. For [2, 3, -2, 4], max_end is [2, 6, -2, 4]: the final local state is 4, while the global answer is the earlier 6.\nZero needs no separate branch. When x = 0, all three candidates are 0, so both ending states become 0. Later positions can still start again through the candidate x, while an earlier larger answer remains in an earlier max_end entry.\nRun these checks:\nassert max_product_table([2, 3, -2, 4]) == 6 assert max_product_table([-2, 0, -1]) == 0 assert max_product_table([-2, 3, -4]) == 24 assert max_product_table([-2]) == -2 assert max_product_table([0, -2]) == 0 assert max_product_table([-1, -2, -3]) == 6 Each position now needs only a fixed number of comparisons and multiplications, reducing the time complexity to O(n). The two arrays use O(n) extra space. This version handles negatives, zeros, and an optimal range ending anywhere; the remaining question is storage. Each transition reads only i - 1, so must we retain every older entry?\nStep 3: Keep Only the Previous Pair The previous max_end[i] is read only by the next iteration, and min_end[i] has the same lifetime. Position i + 1 never reads an earlier position, so most entries in the two arrays are already dead. One result cannot simply be deleted, though: the best range may end early, so we cannot wait until the loop finishes to scan the arrays for the global maximum.\nFirst consider an easy-to-write but incorrect compression. If we update max_end first and then let min_end read it, the second update mixes the old maximum with the newly computed maximum. On [-2, 3, -4, -1], that mistake eventually produces 96, while the correct greatest product over all contiguous ranges is 24.\nAt the start of each iteration, copy the previous position\u0026rsquo;s two states into previous_max and previous_min. Compute both new states from that old snapshot, then fold the current maximum into a separate answer.\nReplace the array version with this rolling version:\ndef max_product_rolling(nums: list[int]) -\u0026gt; int: max_end = nums[0] min_end = nums[0] answer = nums[0] for i in range(1, len(nums)): x = nums[i] previous_max = max_end previous_min = min_end candidates = (x, previous_max * x, previous_min * x) max_end = max(candidates) min_end = min(candidates) answer = max(answer, max_end) return answer Here answer replaces the final max(max_end) scan and stores the greatest ending-state value seen from the beginning through the current position. The loop uses range(1, len(nums)), so it does not create a new list such as nums[1:]; apart from a fixed number of integers and a tuple, no structure grows with the input.\nTo see why the snapshot matters, the faulty update is equivalent to:\nmax_end = max(x, max_end * x, min_end * x) min_end = min(x, max_end * x, min_end * x) # max_end is already new here On [-2, 3, -4, -1], it incorrectly multiplies the newest maximum into the minimum again and pushes the result to 96. The correct version reads the same old snapshot for both updates, so its ending states remain identical to the array version.\nRun fixed boundary checks and a seeded differential check. The previously reviewed brute-force function is the oracle; each random array has length 1 through 8, values from -3 through 3, and the loop compares 700 arrays:\nimport random assert max_product_rolling([2, 3, -2, 4]) == 6 assert max_product_rolling([-2, 0, -1]) == 0 assert max_product_rolling([-2, 3, -4]) == 24 assert max_product_rolling([-2]) == -2 assert max_product_rolling([0, -2]) == 0 assert max_product_rolling([-1, -2, -3]) == 6 random.seed(152) for _ in range(700): size = random.randint(1, 8) values = [random.randint(-3, 3) for _ in range(size)] assert max_product_rolling(values) == max_product_bruteforce(values) Only two ending states and one global answer remain. Time complexity stays O(n), while extra space drops to O(1). The only remaining delivery task is to place this verified rolling logic in LeetCode\u0026rsquo;s required Solution.maxProduct method and provide the complete invariant argument.\nStep 4: Deliver the Method and Check the Invariant The rolling function from the previous step already solves the state and space problems, but LeetCode requires the Solution.maxProduct entry point. Add no new transition here; place the already verified logic directly in the required method.\nclass Solution: def maxProduct(self, nums: list[int]) -\u0026gt; int: max_end = nums[0] min_end = nums[0] answer = nums[0] for i in range(1, len(nums)): x = nums[i] previous_max = max_end previous_min = min_end candidates = (x, previous_max * x, previous_min * x) max_end = max(candidates) min_end = min(candidates) answer = max(answer, max_end) return answer The method maintains this invariant: after processing index i, max_end is the greatest product among all nonempty contiguous subarrays that must end at i, min_end is the smallest such product, and answer is the greatest product among every valid nonempty subarray ending anywhere from index 0 through i.\nBase case: At index 0, the only nonempty range ending there is [nums[0]], so all three variables start at nums[0]. Maintenance: Every range ending at i either takes nums[i] alone or appends it to some nonempty range ending at i - 1. Multiplication by a fixed positive number preserves order, a negative number reverses it, and zero makes every product zero. The greatest and smallest extended products therefore come from the old two extrema. Including the singleton candidate x makes candidates sufficient to obtain both new extrema, and comparing the new max_end with answer keeps every possible endpoint covered. Termination: After the last index is processed, answer has compared the greatest ending product at every index, so it is the greatest product of any nonempty contiguous subarray. Zero follows the same invariant without a special branch: when x = 0, all three candidates are 0, so the current ending states reset to 0; the next nonzero value can start a new range through the candidate x. Negative values also need no special branch because both previous extrema participate in the products, so sign reversal is covered by the same comparison. Initializing from nums[0] enforces the nonempty contract and never treats the empty product 1 as a candidate.\nRun fixed checks through the final entry point, then compare it again with the reviewed brute-force function:\nimport random solution = Solution() assert solution.maxProduct([2, 3, -2, 4]) == 6 assert solution.maxProduct([-2, 0, -1]) == 0 assert solution.maxProduct([-2, 3, -4]) == 24 assert solution.maxProduct([-2]) == -2 assert solution.maxProduct([0]) == 0 assert solution.maxProduct([0, -2, 0]) == 0 assert solution.maxProduct([-1, -2, -3]) == 6 assert solution.maxProduct([2, -5, -2, -4, 3]) == 24 random.seed(152) for _ in range(700): size = random.randint(1, 8) values = [random.randint(-3, 3) for _ in range(size)] assert solution.maxProduct(values) == max_product_bruteforce(values) Each element produces a fixed number of candidates, so time complexity is O(n). Only three scalar states and one fixed-size tuple are retained, so extra space is O(1). The problem guarantees a nonempty input, so the method does not add an empty-array branch that would change the contract.\nCommon mistakes are keeping only the greatest ending product, letting the min_end update read an already updated max_end, returning max_end[-1] as the global answer, or initializing the answer to 0. They respectively lose sign reversals, corrupt the old-state transition, miss an earlier optimum, or mishandle an all-negative array.\nThe four checkpoints now form one code-growth chain: brute-force enumeration establishes the problem, two arrays preserve the extrema needed for sign reversal, rolling values remove dead history, and the final method adds only LeetCode\u0026rsquo;s interface wrapper. Reference: LeetCode 152.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/152-maximum-product-subarray/","summary":"\u003ch2 id=\"problem-requirements\"\u003eProblem Requirements\u003c/h2\u003e\n\u003cp\u003eGiven an integer array \u003ccode\u003enums\u003c/code\u003e, find a \u003cstrong\u003enonempty contiguous subarray\u003c/strong\u003e with the greatest product and return that product.\u003c/p\u003e\n\u003cp\u003eContiguous means the chosen elements must be adjacent: you cannot skip a number in between. Nonempty means you must choose at least one element. Return only the product, not the range or its length.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [2, 3, -2, 4]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 6\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: The contiguous subarray [2, 3] has the greatest product, 2 * 3 = 6.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [-2, 0, -1]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 0\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: You cannot skip 0 and treat -2 and -1 as a subarray. Choosing [0] gives 0.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe constraints are \u003ccode\u003e1 \u0026lt;= len(nums) \u0026lt;= 20000\u003c/code\u003e and \u003ccode\u003e-10 \u0026lt;= nums[i] \u0026lt;= 10\u003c/code\u003e. The problem guarantees that the product of any subarray fits in a signed 32-bit integer. The input is never empty, but it may contain negatives and zeros.\u003c/p\u003e","title":"LeetCode 152: Maximum Product Subarray, Deriving Two DP States from a Negative Counterexample"},{"content":"Problem Requirements Given a positive integer n, return the minimum number of positive perfect squares whose sum is exactly n. A perfect square is the square of an integer, such as 1 = 1², 4 = 2², 9 = 3², or 16 = 4².\nThe sum must equal n exactly. You may use the same perfect square any number of times. Return only the count, not the decomposition. The order of the terms does not affect the answer. Constraint: 1 \u0026lt;= n \u0026lt;= 10^4. Input: n = 12 Output: 3 Explanation: 12 = 4 + 4 + 4 uses 3 perfect squares. Input: n = 13 Output: 2 Explanation: 13 = 9 + 4 uses 2 perfect squares. This guide builds the solution in Python 3.9+. It assumes familiarity with functions, loops, and basic recursion, and develops the dynamic programming reasoning step by step.\nStep 1: First Answer the Zero Remainder Does choosing the largest square first minimize the count? For n = 12, that choice produces:\n12 -\u0026gt; take 9 -\u0026gt; remaining sum 3 -\u0026gt; take 1 + 1 + 1 Total: 9 + 1 + 1 + 1, or 4 terms But 4 + 4 + 4 uses only 3 terms. The current baseline is \u0026ldquo;always choose the largest square no greater than the remainder.\u0026rdquo; It already fails on 12: taking more in one choice does not guarantee using fewer terms overall.\nWe need to compare the remaining sums after different choices. Once one square is removed, what remains is the same kind of problem: how many squares are needed to make that smaller number? Name that question:\nbest(x) = minimum number of perfect squares whose sum is x First establish its stopping condition. If x = 0, the target is complete and no more terms are needed, so the answer is 0. This zero is an internal state reached during a decomposition; the problem\u0026rsquo;s input n is still at least 1.\nImplement only that completion condition for now, leaving an explicit gap for positive-sum candidates:\ndef best(x: int) -\u0026gt; int: if x == 0: return 0 raise NotImplementedError(\u0026#34;Positive sums need a candidate transition\u0026#34;) Run these checks to distinguish the completed part from the missing part:\nassert best(0) == 0 try: best(1) except NotImplementedError: pass else: raise AssertionError(\u0026#34;A positive sum should expose the missing transition\u0026#34;) We can now state what best(x) asks and execute the completion rule best(0) = 0. However, best(12) still raises an exception: we have not enumerated its square choices or compared their total counts. The next step fills that precise gap with a candidate transition.\nStep 2: Try Every Possible Final Square For 12, the final square could be 1, 4, or 9. If it is 4, the preceding terms must make 8. Once we know best(8), including that final 4 gives a candidate count of best(8) + 1.\nThe previous version has only the completion rule best(0) and raises an exception for every positive sum. Its missing piece is a transition that connects each possible final choice to a smaller remainder. Replace the raise with a candidate loop, and put best inside a function that accepts the original input n:\ndef num_squares_recursive(n: int) -\u0026gt; int: def best(x: int) -\u0026gt; int: if x == 0: return 0 minimum = x root = 1 while root * root \u0026lt;= x: square = root * root minimum = min(minimum, best(x - square) + 1) root += 1 return minimum return best(n) minimum = x is the count of a genuinely feasible solution: x copies of 1 always sum to x. Every non-negative remainder is therefore reachable, so this problem needs neither -1 nor an unreachable marker.\nStarting root at 1 produces 1², 2², 3², and so on until the next square exceeds x. Every square is positive, so x - square is strictly smaller than x, and recursion can eventually reach 0. Each smaller problem again considers all squares, allowing the same value to appear repeatedly.\nThe transition is:\nbest(x) = min(best(x - square) + 1) over all perfect squares with 1 \u0026lt;= square \u0026lt;= x The + 1 counts only the final square we just chose. Walk through the three candidates for 12:\nFinal square Minimum count for the remainder Include the final square Candidate total 1 best(11) = 3, from 9 + 1 + 1 3 + 1 4 4 best(8) = 2, from 4 + 4 2 + 1 3 9 best(3) = 3, from 1 + 1 + 1 3 + 1 4 The function compares all candidates and keeps 3. This explains why following only the final-9 branch is insufficient.\nRun the small-input checks:\nassert num_squares_recursive(0) == 0 assert num_squares_recursive(1) == 1 assert num_squares_recursive(4) == 1 assert num_squares_recursive(12) == 3 assert num_squares_recursive(13) == 2 We now have the first complete recursive solution, which runs on small inputs and compares every possible final square. However, different paths repeatedly solve the same best(x). The search tree grows quickly and recursive calls may become too deep, so this version is not yet suitable for the full constraint. The next step addresses the duplicated work first.\nStep 3: Cache Repeated Remaining Sums Where does the same remainder appear twice? When the previous version evaluates best(12), both paths below ask for best(8):\n12 -\u0026gt; 8 choose one 4 12 -\u0026gt; 11 -\u0026gt; 10 -\u0026gt; 9 -\u0026gt; 8 choose 1 each time The paths reach 8 differently, but the question is identical: using all positive perfect squares, what is the minimum count for 8? The path taken to reach this sum cannot change its answer.\nThe current recursive transition is already correct. Its problem is that it expands the same best(8) again. Caching completed results by the remainder x lets later calls reuse them. Add @cache to the inner function, leaving the candidate loop unchanged:\nfrom functools import cache def num_squares_memo(n: int) -\u0026gt; int: @cache def best(x: int) -\u0026gt; int: if x == 0: return 0 minimum = x root = 1 while root * root \u0026lt;= x: square = root * root minimum = min(minimum, best(x - square) + 1) root += 1 return minimum return best(n) The first call for a particular best(x) runs the function and saves the result. Later calls with the same x read that result directly. Only x is needed as the key because the square choices are always the same. Each outer call creates a new inner function, so separate inputs do not share an old cache.\nRun the earlier examples and compare both versions over a small input range:\nassert num_squares_memo(12) == 3 assert num_squares_memo(13) == 2 for n in range(31): assert num_squares_memo(n) == num_squares_recursive(n) For a cache check, inspect the inner best function\u0026rsquo;s cache_info() in a local test. One fresh call with n = 12 produces:\nCacheInfo(hits=13, misses=13, maxsize=None, currsize=13) The 13 misses mean that the states 0 through 12 are each actually solved once. The 13 hits mean that another 13 requests reused saved answers. The current size is the number of stored results. These counts check the mechanism without changing the function\u0026rsquo;s integer-returning interface.\nWe now solve each remainder only once and store at most n + 1 states. Recursively solving smaller problems while remembering their answers is called memoization. However, caching does not remove the initial call chain. The loop tries 1 first, so it still forms best(n) -\u0026gt; best(n - 1) -\u0026gt; ... -\u0026gt; best(0). At n = 10000, that depth may exceed Python\u0026rsquo;s default recursion limit. The next step keeps the transition while removing the recursive call stack.\nStep 4: Fill a Table from Smaller Sums Caching removes duplicated work but does not remove the call stack. At n = 10000, even solving each state once can create roughly ten thousand nested calls along the path that tries square 1 first. The current baseline hides the states inside function calls; we need to place the answers for 0 through n directly in a table.\nConvert the recursive function into a table function. dp[x] means \u0026ldquo;the minimum number of perfect squares needed to make exactly x.\u0026rdquo; Initialize with x copies of 1; this is a feasible upper bound for every state and carries forward the previous minimum = x meaning. When processing x, every x - square is smaller than x and has already been completed:\ndef num_squares_table(n: int) -\u0026gt; int: dp = list(range(n + 1)) dp[0] = 0 for x in range(1, n + 1): root = 1 while root * root \u0026lt;= x: square = root * root dp[x] = min(dp[x], dp[x - square] + 1) root += 1 return dp[n] The update is the same best(x - square) + 1 from the previous version; the recursive call has become a read of the completed dp[x - square]. For example, 4 gets result 1; at 8, reading dp[4] gives dp[8] = 2; at 12, reading dp[8] gives dp[12] = 3. The same 4 can therefore be used three times through 4 -\u0026gt; 8 -\u0026gt; 12. This is how unlimited square reuse appears in the table update.\nTreat each square as a denomination that can be used any number of times: that is the selection rule of unbounded knapsack. This version puts amounts in the outer loop and processes them in ascending order, so every smaller amount is ready and can be extended by the same square again.\nThe states from 0 through 13 should be:\ndp = [0, 1, 2, 3, 1, 2, 3, 4, 2, 1, 2, 3, 3, 2] Run the table version and compare it with the memoized version:\nassert num_squares_table(12) == 3 assert num_squares_table(13) == 2 assert num_squares_table(9999) == 4 assert num_squares_table(10000) == 1 for n in range(101): assert num_squares_table(n) == num_squares_memo(n) The final two assertions show that the iterative table handles the problem\u0026rsquo;s upper bound without relying on the recursive call stack. It is not yet the Solution.numSquares method required by LeetCode, and it does not yet include the complete inductive correctness argument or complexity summary; those belong to the next step.\nStep 5: Freeze the Submission Interface The previous section produced an iterative table that covers the problem\u0026rsquo;s constraint. The remaining gap is delivery: LeetCode calls Solution().numSquares(n), and we need to explain why each table entry is minimal and what resources the method uses.\nPlace the table logic from the previous section directly in the required method. The loops, initialization, and update rule stay the same:\nclass Solution: def numSquares(self, n: int) -\u0026gt; int: dp = list(range(n + 1)) dp[0] = 0 for x in range(1, n + 1): root = 1 while root * root \u0026lt;= x: square = root * root dp[x] = min(dp[x], dp[x - square] + 1) root += 1 return dp[n] Check it with this induction invariant: before processing x, every dp[y] for 0 \u0026lt;= y \u0026lt; x is the minimum number of squares needed to make y. dp[0] = 0 is the base case. For x, every possible final square satisfies square \u0026lt;= x, so x - square \u0026lt; x and dp[x - square] already has its minimum value. Taking min(... + 1) over all final choices therefore gives the minimum for dp[x]. After n is processed, the returned value is the required answer. Initializing with x copies of 1 supplies a feasible upper bound, so this problem has no unreachable branch.\nFor each x, the inner loop checks floor(sqrt(x)) roots. The total time complexity is O(n sqrt(n)), and the table uses O(n) extra space. Squares are reused through the ascending state order; the code minimizes the count rather than counting distinct permutations.\nRun the final boundary checks:\nsolver = Solution() assert solver.numSquares(1) == 1 assert solver.numSquares(2) == 2 assert solver.numSquares(3) == 3 assert solver.numSquares(4) == 1 assert solver.numSquares(12) == 3 assert solver.numSquares(13) == 2 assert solver.numSquares(43) == 3 assert solver.numSquares(9999) == 4 assert solver.numSquares(10000) == 1 An independent local checker that does not use this DP transition also compared every input from 1 through 200; all results agreed.\nCommon mistakes are initializing dp[0] to 1, omitting the +1 for the final square, or making each square usable only once in an attempt to \u0026ldquo;deduplicate\u0026rdquo; choices. Each one changes the state meaning we have verified. As with LeetCode 322: Coin Change, we solve smaller amounts first and reuse their results for larger amounts; here the available denominations are perfect squares.\nAt this point the code, state meaning, correctness invariant, complexity, and submission interface form one complete checkpoint. Number-theory theorems, BFS as the submitted algorithm, and other language implementations are outside this tutorial\u0026rsquo;s scope.\nProblem statement: LeetCode 279: Perfect Squares.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/279-perfect-squares/","summary":"\u003ch2 id=\"problem-requirements\"\u003eProblem Requirements\u003c/h2\u003e\n\u003cp\u003eGiven a positive integer \u003ccode\u003en\u003c/code\u003e, return the minimum number of \u003cstrong\u003epositive perfect squares\u003c/strong\u003e whose sum is exactly \u003ccode\u003en\u003c/code\u003e. A perfect square is the square of an integer, such as \u003ccode\u003e1 = 1²\u003c/code\u003e, \u003ccode\u003e4 = 2²\u003c/code\u003e, \u003ccode\u003e9 = 3²\u003c/code\u003e, or \u003ccode\u003e16 = 4²\u003c/code\u003e.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe sum must equal \u003ccode\u003en\u003c/code\u003e exactly.\u003c/li\u003e\n\u003cli\u003eYou may use the same perfect square any number of times.\u003c/li\u003e\n\u003cli\u003eReturn only the count, not the decomposition. The order of the terms does not affect the answer.\u003c/li\u003e\n\u003cli\u003eConstraint: \u003ccode\u003e1 \u0026lt;= n \u0026lt;= 10^4\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: n = 12\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 3\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: 12 = 4 + 4 + 4 uses 3 perfect squares.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: n = 13\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: 13 = 9 + 4 uses 2 perfect squares.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis guide builds the solution in Python 3.9+. It assumes familiarity with functions, loops, and basic recursion, and develops the dynamic programming reasoning step by step.\u003c/p\u003e","title":"LeetCode 279: Perfect Squares, Deriving Dynamic Programming from Remaining Sums"},{"content":"Problem Input and Output Input: a string s and a list of strings wordDict Every element in wordDict is an available word, and the same word may be reused Output: return True if s can be split into one or more dictionary words; otherwise, return False The split must cover the entire string without skipping or reordering characters Examples Input: s = \u0026#34;leetcode\u0026#34;, wordDict = [\u0026#34;leet\u0026#34;, \u0026#34;code\u0026#34;] Output: True Explanation: \u0026#34;leetcode\u0026#34; can be split into \u0026#34;leet\u0026#34; + \u0026#34;code\u0026#34; Input: s = \u0026#34;applepenapple\u0026#34;, wordDict = [\u0026#34;apple\u0026#34;, \u0026#34;pen\u0026#34;] Output: True Explanation: \u0026#34;apple\u0026#34; + \u0026#34;pen\u0026#34; + \u0026#34;apple\u0026#34;; the same word may be reused Input: s = \u0026#34;catsandog\u0026#34;, wordDict = [\u0026#34;cats\u0026#34;, \u0026#34;dog\u0026#34;, \u0026#34;sand\u0026#34;, \u0026#34;and\u0026#34;, \u0026#34;cat\u0026#34;] Output: False Explanation: some prefixes can be formed, but the split cannot cover the entire string Constraints 1 \u0026lt;= s.length \u0026lt;= 300 1 \u0026lt;= wordDict.length \u0026lt;= 1000 1 \u0026lt;= wordDict[i].length \u0026lt;= 20 s and wordDict[i] contain only lowercase English letters All strings in wordDict are unique Step 1: Make the Completed Prefix Visible Start with one concrete question. For s = \u0026quot;leetcode\u0026quot;, before deciding whether the whole string can be split, can we record how much of it has already been completed?\nPressure: The Answer Depends on an Intermediate Boundary The valid split in this example is:\n\u0026#34;leetcode\u0026#34; = \u0026#34;leet\u0026#34; + \u0026#34;code\u0026#34; Once we have confirmed \u0026quot;leet\u0026quot;, the next position to process is boundary 4. But \u0026quot;code\u0026quot; has not been processed yet, so \u0026ldquo;this prefix works\u0026rdquo; and \u0026ldquo;the whole string works\u0026rdquo; cannot be collapsed into one Boolean value.\nCurrent Baseline At this point, we only have the problem requirement. We can imagine trying different split points, but we have nowhere to store the result that a particular prefix has already been completed.\nWhere Does This Baseline Break? Without recording intermediate boundaries, we cannot express this state:\nFirst 4 characters, \u0026#34;leet\u0026#34;: already splittable Remaining substring, \u0026#34;code\u0026#34;: not processed yet We need to store prefix feasibility on its own. A prefix is represented by a boundary: boundary i corresponds to s[:i]. Therefore, i = 4 means the first four characters, not the character at index 4.\nAdd One State Table to the Current Version For now, create only the state. Do not match dictionary words yet:\ns = \u0026#34;leetcode\u0026#34; # reachable[i] means that s[:i] can be split completely reachable = [False] * (len(s) + 1) # The empty prefix needs no words, so it is a valid starting point reachable[0] = True That extra slot matters. Although s has 8 characters, we must record 9 boundaries from 0 through 8. reachable[0] represents the empty prefix s[:0], while reachable[8] represents the entire string s[:8].\nCheck This Change This version has not attached any dictionary word, so boundary 0 should be the only boundary known to be reachable:\nassert len(reachable) == len(s) + 1 assert reachable[0] is True assert all(flag is False for flag in reachable[1:]) print(reachable) # [True, False, False, False, False, False, False, False, False] Freeze This Checkpoint This version can now:\nuse reachable[i] to mean that prefix s[:i] can be split completely treat the empty prefix as a valid starting point for later extensions distinguish \u0026ldquo;a prefix is reachable\u0026rdquo; from \u0026ldquo;the whole string has an answer\u0026rdquo; It still lacks:\nhow to match a dictionary word from an already reachable boundary how to mark the new ending boundary as reachable after a match The next step will extend only one starting boundary by one word. It will not jump directly to scanning every boundary.\nStep 2: Take Only the First Word Pressure: The Empty Prefix Is Reachable, but No New Boundary Is Step 1 tells us only that reachable[0] is true. Return to \u0026quot;leetcode\u0026quot;: if we try different endings from boundary 0, when may we mark the next boundary as true?\nCurrent Baseline The previous version has this boundary table:\nreachable[0] = True reachable[1..8] = False It can represent the starting point, but it has not used the dictionary supplied by the problem.\nWhere Does This Baseline Break? Without checking whether a candidate substring is a dictionary word, we cannot advance from 0 to 4. This step deliberately studies only one starting boundary. If we scanned every start now, it would be harder to see which exact rule performs the extension.\nAdd One Single-Layer Extension to the Previous Version Store the dictionary as a set, and for now try endings only from start = 0:\ns = \u0026#34;leetcode\u0026#34; word_set = {\u0026#34;leet\u0026#34;, \u0026#34;code\u0026#34;} # reachable[i] means that s[:i] can be split completely reachable = [False] * (len(s) + 1) reachable[0] = True start = 0 for end in range(start + 1, len(s) + 1): piece = s[start:end] if reachable[start] and piece in word_set: reachable[end] = True Only one thing happens here: if s[start:end] is a dictionary word and start is already reachable, mark end as reachable. In this example, when end = 4, the piece is \u0026quot;leet\u0026quot;, so we get reachable[4] = True.\nCheck This Change assert reachable[0] is True assert reachable[4] is True # 0 -\u0026gt; 4: \u0026#34;leet\u0026#34; assert reachable[8] is False # 4 -\u0026gt; 8 has not been scanned yet print(reachable) # [True, False, False, False, True, False, False, False, False] reachable[8] is still False, but that does not mean the answer is false. This version intentionally processes only the fixed starting boundary 0; it has not continued from the newly reached boundary 4 to try \u0026quot;code\u0026quot;.\nFreeze This Checkpoint This version can now:\nenumerate candidate endings from one already reachable boundary use a dictionary match to advance from 0 to 4 keep \u0026ldquo;a prefix is reachable\u0026rdquo; separate from \u0026ldquo;the whole string is reachable\u0026rdquo; It still lacks:\nextending from later boundaries such as 4 applying the same rule to every possible start so that multiword splits are covered The next step will add only the outer rule that scans all reachable starts. That will produce the first complete correct version.\nStep 3: Give the Same Rule to Every Reachable Boundary Pressure: reachable[4] Is True, but the Answer Is Still Stuck Halfway Step 2 finds \u0026quot;leet\u0026quot;, but because the code fixes start = 0, it never gets a chance to try \u0026quot;code\u0026quot;. For a multiword split, every newly reached boundary must be allowed to become the next starting point.\nCurrent Baseline The previous version performs only this layer:\nstart = 0 enumerate end from 0 The inner substring-matching rule is already correct. What is missing is a scan over later values of start.\nWhere Does This Baseline Break? If we always start from 0, reachable[4] may be marked but will never participate in another decision. The second word \u0026quot;code\u0026quot; in \u0026quot;leetcode\u0026quot; is therefore missed, and reachable[8] stays False.\nAdd Only the Outer Boundary Scan to the Previous Version Replace the fixed start = 0 with a loop over all boundaries. Keep the inner rule, \u0026ldquo;a dictionary match marks its ending boundary,\u0026rdquo; unchanged:\ndef can_break(s: str, word_set: set[str]) -\u0026gt; bool: # reachable[i] means that s[:i] can be split completely reachable = [False] * (len(s) + 1) reachable[0] = True for start in range(len(s)): if not reachable[start]: continue for end in range(start + 1, len(s) + 1): piece = s[start:end] if piece in word_set: reachable[end] = True return reachable[len(s)] The only new algorithmic rule is the outer start scan: continue only from boundaries that are already reachable. The end loop and the piece in word_set check are carried over unchanged from Step 2.\nTrace the Boundaries for \u0026quot;leetcode\u0026quot; start = 0 -\u0026gt; match \u0026#34;leet\u0026#34; -\u0026gt; reachable[4] = True start = 1,2,3 -\u0026gt; unreachable, skip start = 4 -\u0026gt; match \u0026#34;code\u0026#34; -\u0026gt; reachable[8] = True The function therefore returns reachable[8], not merely the result of the first extension.\nCheck This Change assert can_break(\u0026#34;leetcode\u0026#34;, {\u0026#34;leet\u0026#34;, \u0026#34;code\u0026#34;}) is True assert can_break(\u0026#34;applepenapple\u0026#34;, {\u0026#34;apple\u0026#34;, \u0026#34;pen\u0026#34;}) is True assert can_break( \u0026#34;catsandog\u0026#34;, {\u0026#34;cats\u0026#34;, \u0026#34;dog\u0026#34;, \u0026#34;sand\u0026#34;, \u0026#34;and\u0026#34;, \u0026#34;cat\u0026#34;}, ) is False print(\u0026#34;three examples passed\u0026#34;) These three assertions cover a two-word connection, reuse of a word, and a case where promising prefixes still cannot cover the final boundary. This is the first version that returns the correct Boolean result for a complete input. For now, it receives an already prepared word_set; the final wrapper for list input will come later.\nFreeze This Checkpoint This version can now:\ncontinue from every reachable boundary and try subsequent dictionary words cover multiword splits with the same transition rule return the correct Boolean result for all three problem examples It still lacks:\navoiding candidates longer than the longest dictionary word, because the current inner loop tries every ending through the end of the string bringing together the wordDict list, LeetCode Solution wrapper, and complexity explanation The next step will optimize only the range of candidate endings. It will not change the meaning of reachable or its transition rule.\nStep 4: Scan Only Possible Word Lengths Pressure: The Correct Version Still Checks Impossible Substrings Step 3 is correct, but its inner loop runs to the end of the string from every reachable start. For example, the longest word in word_set = {\u0026quot;leet\u0026quot;, \u0026quot;code\u0026quot;} has length 4. From start = 0, candidate substrings of lengths 5, 6, through 8 cannot possibly match the dictionary.\nCurrent Baseline For every reachable start, the previous version runs:\nfor end in range(start + 1, len(s) + 1): piece = s[start:end] The reachability transition is already correct. The waste exists only in the candidate range.\nWhere Does This Baseline Break? The problem guarantees that dictionary words have bounded lengths. A candidate longer than the longest dictionary word can never match word_set. Checking it adds work but can never make another boundary reachable.\nNarrow Only the Upper Bound of end First obtain the longest dictionary word length, then look no farther than that distance from each starting boundary:\ndef can_break(s: str, word_set: set[str]) -\u0026gt; bool: # reachable[i] means that s[:i] can be split completely reachable = [False] * (len(s) + 1) reachable[0] = True max_word_len = max(len(word) for word in word_set) for start in range(len(s)): if not reachable[start]: continue end_limit = min(len(s), start + max_word_len) for end in range(start + 1, end_limit + 1): piece = s[start:end] if piece in word_set: reachable[end] = True return reachable[len(s)] For word_set = {\u0026quot;leet\u0026quot;, \u0026quot;code\u0026quot;}, max_word_len is 4. When start = 0, end takes only the values 1..4, which still includes the longest candidate that could match. The rule \u0026ldquo;a matching word marks its ending boundary\u0026rdquo; has not changed; only impossible candidates have been removed.\nCheck This Change assert can_break(\u0026#34;leetcode\u0026#34;, {\u0026#34;leet\u0026#34;, \u0026#34;code\u0026#34;}) is True assert can_break(\u0026#34;applepenapple\u0026#34;, {\u0026#34;apple\u0026#34;, \u0026#34;pen\u0026#34;}) is True assert can_break( \u0026#34;catsandog\u0026#34;, {\u0026#34;cats\u0026#34;, \u0026#34;dog\u0026#34;, \u0026#34;sand\u0026#34;, \u0026#34;and\u0026#34;, \u0026#34;cat\u0026#34;}, ) is False # A valid word whose length equals the maximum must remain inside the range assert can_break(\u0026#34;abcdef\u0026#34;, {\u0026#34;abc\u0026#34;, \u0026#34;def\u0026#34;}) is True print(\u0026#34;bounded scan examples passed\u0026#34;) During the self-check, the same cases were run through both the unbounded Step 3 version and this version, and every result matched. A range trace with start = 0 and a maximum word length of 4 also confirmed that substrings of lengths 5..8 were no longer checked.\nFreeze This Checkpoint This version can now:\npreserve every reachability result from Step 3 check only candidates no longer than the longest dictionary word from each start reduce pointless scanning without changing the state transition It still lacks:\nconverting the wordDict list and arranging the LeetCode Solution.wordBreak method and final tests into a directly submittable form a complete invariant, correctness argument, and complexity explanation The next step will add only the final platform wrapper and proof. It will introduce no new algorithmic logic.\nStep 5: Deliver the Final Submittable Version Pressure: The Core Logic Does Not Yet Match the LeetCode Interface The can_break function from Step 4 already returns correct results, but it accepts a prepared word_set. The problem interface supplies a wordDict list, and LeetCode requires the code inside Solution.wordBreak.\nCurrent Baseline The previous version has already established every algorithmic part:\nreachable[0] = True scan each reachable start check only end values within the longest word length mark reachable[end] when a substring matches a dictionary word Where Does This Baseline Break? It cannot yet be pasted directly into the LeetCode method signature, and it does not state its correctness as a checkable invariant. We will not search for a new strategy here; we will only finish the delivery form and proof.\nAdd Only the Platform Wrapper, Without Changing the Loop The loop below is identical to Step 4. The only additions are class Solution, the wordBreak method, and conversion of the list to a set:\nclass Solution: def wordBreak(self, s: str, wordDict: list[str]) -\u0026gt; bool: word_set = set(wordDict) reachable = [False] * (len(s) + 1) reachable[0] = True max_word_len = max(len(word) for word in word_set) for start in range(len(s)): if not reachable[start]: continue end_limit = min(len(s), start + max_word_len) for end in range(start + 1, end_limit + 1): piece = s[start:end] if piece in word_set: reachable[end] = True return reachable[len(s)] Correctness: What Invariant Does reachable Maintain? At every boundary i, reachable[i] is true if and only if the prefix s[:i] can be formed completely from dictionary words.\nInitially, reachable[0] = True because the empty prefix needs no words. If reachable[start] is true and s[start:end] is in the dictionary, appending that valid word forms a valid s[:end], so setting reachable[end] = True is sound. Conversely, the last word in any valid split corresponds to some interval start:end. Its length is no greater than max_word_len, so the loop will examine it. That interval can be appended only when the preceding prefix s[:start] is already reachable. Therefore, after the loops finish, reachable[len(s)] is true if and only if the whole string can be split completely.\nComplexity Let n = len(s), let L be the length of the longest dictionary word, and let C be the total number of characters across all dictionary words.\nThe algorithm checks at most O(nL) candidate intervals. This Python implementation creates a slice and computes a string hash for each candidate. Including those costs, the time complexity is O(nL^2 + C). If intervals are treated as constant-time string views, the number of transitions is O(nL). reachable uses O(n) extra space, and word_set uses O(C) storage. Regression Check solver = Solution() assert solver.wordBreak(\u0026#34;leetcode\u0026#34;, [\u0026#34;leet\u0026#34;, \u0026#34;code\u0026#34;]) is True assert solver.wordBreak(\u0026#34;applepenapple\u0026#34;, [\u0026#34;apple\u0026#34;, \u0026#34;pen\u0026#34;]) is True assert solver.wordBreak( \u0026#34;catsandog\u0026#34;, [\u0026#34;cats\u0026#34;, \u0026#34;dog\u0026#34;, \u0026#34;sand\u0026#34;, \u0026#34;and\u0026#34;, \u0026#34;cat\u0026#34;], ) is False assert solver.wordBreak(\u0026#34;abcdef\u0026#34;, [\u0026#34;abc\u0026#34;, \u0026#34;def\u0026#34;]) is True assert solver.wordBreak(\u0026#34;a\u0026#34;, [\u0026#34;b\u0026#34;]) is False assert solver.wordBreak(\u0026#34;\u0026#34;, [\u0026#34;a\u0026#34;]) is True print(\u0026#34;Solution.wordBreak regression tests passed\u0026#34;) Freeze the Final Checkpoint This version can now:\nuse the LeetCode 139 Solution.wordBreak interface directly reuse the prefix-reachability state and longest-word bound verified in the earlier steps explain every state update with an invariant and pass regression assertions for splittable, repeated-word, unsplittable, and boundary cases The tutorial\u0026rsquo;s code-growth chain is now complete: the final wrapper adds no algorithmic logic that did not appear earlier.\nSummary Represent \u0026ldquo;this prefix can be split completely\u0026rdquo; as the boundary state reachable[i]. The empty prefix, reachable[0], is the starting point of every valid split. Appending a dictionary word to a reachable boundary marks that word\u0026rsquo;s ending boundary as reachable. Scanning every reachable boundary is what covers multiword splits. The longest word length only narrows the candidate range; it does not change the state transition. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/139-word-break/","summary":"\u003ch2 id=\"problem\"\u003eProblem\u003c/h2\u003e\n\u003ch3 id=\"input-and-output\"\u003eInput and Output\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eInput: a string \u003ccode\u003es\u003c/code\u003e and a list of strings \u003ccode\u003ewordDict\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eEvery element in \u003ccode\u003ewordDict\u003c/code\u003e is an available word, and the same word may be reused\u003c/li\u003e\n\u003cli\u003eOutput: return \u003ccode\u003eTrue\u003c/code\u003e if \u003ccode\u003es\u003c/code\u003e can be split into one or more dictionary words; otherwise, return \u003ccode\u003eFalse\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eThe split must cover the entire string without skipping or reordering characters\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"examples\"\u003eExamples\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: s = \u0026#34;leetcode\u0026#34;, wordDict = [\u0026#34;leet\u0026#34;, \u0026#34;code\u0026#34;]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: True\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: \u0026#34;leetcode\u0026#34; can be split into \u0026#34;leet\u0026#34; + \u0026#34;code\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: s = \u0026#34;applepenapple\u0026#34;, wordDict = [\u0026#34;apple\u0026#34;, \u0026#34;pen\u0026#34;]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: True\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: \u0026#34;apple\u0026#34; + \u0026#34;pen\u0026#34; + \u0026#34;apple\u0026#34;; the same word may be reused\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: s = \u0026#34;catsandog\u0026#34;, wordDict = [\u0026#34;cats\u0026#34;, \u0026#34;dog\u0026#34;, \u0026#34;sand\u0026#34;, \u0026#34;and\u0026#34;, \u0026#34;cat\u0026#34;]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: False\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: some prefixes can be formed, but the split cannot cover the entire string\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"constraints\"\u003eConstraints\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e1 \u0026lt;= s.length \u0026lt;= 300\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e1 \u0026lt;= wordDict.length \u0026lt;= 1000\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e1 \u0026lt;= wordDict[i].length \u0026lt;= 20\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003es\u003c/code\u003e and \u003ccode\u003ewordDict[i]\u003c/code\u003e contain only lowercase English letters\u003c/li\u003e\n\u003cli\u003eAll strings in \u003ccode\u003ewordDict\u003c/code\u003e are unique\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"step-1-make-the-completed-prefix-visible\"\u003eStep 1: Make the Completed Prefix Visible\u003c/h2\u003e\n\u003cp\u003eStart with one concrete question. For \u003ccode\u003es = \u0026quot;leetcode\u0026quot;\u003c/code\u003e, before deciding whether the whole string can be split, can we record how much of it has already been completed?\u003c/p\u003e","title":"LeetCode 139: Word Break, Starting from Prefix Reachability"},{"content":"Problem Input and Output Input: an integer array nums, where nums[i] is the money in the i-th house. The houses form a circle. Adjacent houses cannot both be robbed, so houses 0 and n - 1 cannot both be robbed either. Output: the maximum amount of money that can be robbed without triggering the alarm. Constraints: 1 \u0026lt;= nums.length \u0026lt;= 100, 0 \u0026lt;= nums[i] \u0026lt;= 1000. Examples Input: nums = [2,3,2] Output: 3 Explanation: Houses 0 and 2 are adjacent around the circle, so they cannot both be robbed; rob the house containing 3. Input: nums = [1,2,3,1] Output: 4 Explanation: We can rob indices 0 and 2, or indices 1 and 3. Input: nums = [1,2,3] Output: 3 This tutorial uses Python only and starts with the smallest first-last conflict.\nStep 1: Handle the Conflict Between the First and Last Houses Start with one concrete question: for nums = [2,3,2], can we reuse the straight-street approach and simply choose non-adjacent houses across the entire array?\nThe current baseline has only one rule: adjacent houses cannot both be robbed. If we treat these three houses as a line, we may consider indices 0 and 2 non-adjacent, choose both endpoints, and incorrectly get 4.\nThat fails for a circular input because the first and last houses are also adjacent, so 0 and 2 must be mutually exclusive. To remove this conflict, add just one branching rule: every legal plan belongs to at least one of these candidate branches.\nExclude the last house: consider only indices 0..n-2, or nums[0:n-1]. Exclude the first house: consider only indices 1..n-1, or nums[1:n]. These ranges represent the \u0026ldquo;exclude the last house\u0026rdquo; and \u0026ldquo;exclude the first house\u0026rdquo; candidate branches. Each range is now a straight line. Together they cover every legal plan: because the two endpoints cannot both be robbed, every legal plan excludes at least one endpoint. A plan that excludes both endpoints may belong to both candidate ranges, but that does not affect taking the maximum at the end.\nCheck this split on the smallest example:\n[2,3,2] Exclude the last house -\u0026gt; [2,3], best value in the range = 3 Exclude the first house -\u0026gt; [3,2], best value in the range = 3 Take the larger result -\u0026gt; 3 This version can:\nshow why a circular street cannot be treated directly as a line rewrite the original problem as two candidate linear ranges that jointly cover every legal plan It still needs:\na way to compute the optimal value for any linear range boundary handling when a linear range contains only one or two houses Step 2: Let One Linear Range Handle One or Two Houses Now consider just one candidate range produced by Step 1, such as [2,3]. We know it is a line, but we still do not have a function that can answer, \u0026ldquo;What is the most money we can rob from this range?\u0026rdquo;\nThe current baseline is only a description of two range boundaries: [0..n-2] or [1..n-1]. It tells us which portion to inspect, but it cannot store the best result already found for a prefix of that range.\nWe must first solve this for ranges containing only one or two houses:\nWith one house, the only choice is that house. With two houses, we cannot rob both, so we take the larger amount. Add only one linear helper to the previous two-range model, and first give it these two base cases:\ndef rob_line(nums: list[int], left: int, right: int) -\u0026gt; int: size = right - left + 1 dp = [0] * size # dp[k]: maximum money from nums[left..left+k] dp[0] = nums[left] if size == 1: return dp[0] dp[1] = max(nums[left], nums[left + 1]) return dp[1] Here, dp[k] uses an offset within the range rather than an index in the original array: dp[0] corresponds to nums[left], while dp[1] corresponds to nums[left..left+1]. The state is now operationally used by the assignments and returns in both base cases.\nCheck the smallest inputs:\nassert rob_line([5], 0, 0) == 5 assert rob_line([2, 3], 0, 1) == 3 This version can:\ncompute the optimum for any linear range of length 1 or 2 keep the meaning of dp[k] distinct from an index in the original array It still needs:\na way to derive the current state from the previous two states when there are three or more houses the \u0026ldquo;rob current or skip current\u0026rdquo; transition after these two base cases Step 3: Add the Rob-or-Skip Transition for a Linear Range Now extend the range to [2,7,9]. The Step 2 version can initialize the first two houses, but it stops when the third house, 9, arrives: it does not say which prefix result can be combined with 9.\nThe current baseline is a rob_line that handles only lengths 1 and 2. It fails on three houses because there is no dp[2], so it cannot answer whether to rob 9 or keep the best result from the first two houses. We continue to require rob_line to receive a non-empty range (left \u0026lt;= right); the one-house guard for the circular entry point will come later.\nIn the previous version, replace only the fixed return dp[1] at the end so that every later position compares two sources:\nSkip the current house and keep dp[offset - 1]. Rob the current house, which can only follow dp[offset - 2], then add the current amount. def rob_line(nums: list[int], left: int, right: int) -\u0026gt; int: size = right - left + 1 dp = [0] * size # dp[k]: maximum money from nums[left..left+k] dp[0] = nums[left] if size == 1: return dp[0] dp[1] = max(nums[left], nums[left + 1]) for offset in range(2, size): current = nums[left + offset] dp[offset] = max(dp[offset - 1], dp[offset - 2] + current) return dp[-1] Check this transition:\nassert rob_line([2, 7, 9], 0, 2) == 11 assert rob_line([2, 7, 9, 3, 1], 0, 4) == 12 The states in the first assertion are 2 -\u0026gt; 7 -\u0026gt; max(7, 2 + 9) = 11. The second assertion continues with 11 -\u0026gt; max(11, 7 + 3) = 11 -\u0026gt; max(11, 11 + 1) = 12.\nThis version can:\ncorrectly compute the maximum amount for any non-empty linear range explicitly compare the two legal sources at each position: skip the current house or connect it to the state two positions back It still needs:\nto connect this linear helper back to the circular array\u0026rsquo;s two candidate branches special handling for n == 1, where we cannot construct the two exclusion ranges directly Step 4: Connect the Linear Helper Back to the Circular Array Now return to the original circular input. Task 3 can solve one non-empty linear range, but there is still no entry point that evaluates both candidate ranges. Use [2,3,2] as the pressure: calling rob_line only once would omit either the \u0026ldquo;exclude the first house\u0026rdquo; or the \u0026ldquo;exclude the last house\u0026rdquo; possibility.\nThe current baseline is a correct rob_line(nums, left, right). That is not yet enough for a circular array because the first-last conflict can only be removed through the two candidate branches. For now, require n \u0026gt;= 2 at the entry point and leave the one-house input for the next step.\nAdd only one rob entry point around the previous helper. Compute the result that excludes the last house and the result that excludes the first house, then take the larger one. The linear transition inside rob_line remains unchanged.\ndef rob_line(nums: list[int], left: int, right: int) -\u0026gt; int: size = right - left + 1 dp = [0] * size dp[0] = nums[left] if size == 1: return dp[0] dp[1] = max(nums[left], nums[left + 1]) for offset in range(2, size): current = nums[left + offset] dp[offset] = max(dp[offset - 1], dp[offset - 2] + current) return dp[-1] def rob(nums: list[int]) -\u0026gt; int: n = len(nums) exclude_last = rob_line(nums, 0, n - 2) exclude_first = rob_line(nums, 1, n - 1) return max(exclude_last, exclude_first) Check three circular examples with at least two houses:\nassert rob([2, 3, 2]) == 3 assert rob([1, 2, 3, 1]) == 4 assert rob([1, 2, 3]) == 3 This version can:\nfully compare both linear candidate branches for a circular array with n \u0026gt;= 2 reuse the same linear helper instead of duplicating the transition logic It still needs:\nan entry-point guard for n == 1, where both candidate ranges are not valid a space improvement, because the linear helper still stores the entire dp table and uses O(n) extra space Step 5: Add the One-House Boundary First Now test Task 4 with the smallest input allowed by the constraints, [5]. The current rob immediately constructs two exclusion ranges, but when the length is 1, there are not two valid non-empty ranges. The linear transition is not the problem; the entry point is missing a boundary check before the split.\nThe current baseline is correct for both candidate ranges when n \u0026gt;= 2. On [5], it computes right as -1, so it cannot call rob_line safely.\nAdd only one early return to the previous complete code, and place it before both helper calls. The linear helper and circular split remain unchanged. The unchanged helper and updated entry point are shown together in one runnable block:\ndef rob_line(nums: list[int], left: int, right: int) -\u0026gt; int: size = right - left + 1 dp = [0] * size dp[0] = nums[left] if size == 1: return dp[0] dp[1] = max(nums[left], nums[left + 1]) for offset in range(2, size): current = nums[left + offset] dp[offset] = max(dp[offset - 1], dp[offset - 2] + current) return dp[-1] def rob(nums: list[int]) -\u0026gt; int: n = len(nums) if n == 1: return nums[0] exclude_last = rob_line(nums, 0, n - 2) exclude_first = rob_line(nums, 1, n - 1) return max(exclude_last, exclude_first) Check the boundary and regular examples:\nassert rob([5]) == 5 assert rob([2, 3]) == 3 assert rob([2, 3, 2]) == 3 assert rob([1, 2, 3, 1]) == 4 assert rob([1, 2, 3]) == 3 This version can:\ncover every non-empty length in the constraints, including a circle with only one house keep comparing both linear candidate branches when n \u0026gt;= 2, giving the first complete correct solution run in O(n) time while still using O(n) extra space It still needs:\nto avoid storing the entire dp array when the linear helper reads only the previous two states; the next step can reduce the extra space to O(1) Step 6: Keep Only the Two States Needed by the Linear Transition Task 5 has produced the first correct solution, but each step in the linear helper reads only two old values: dp[offset - 2] and dp[offset - 1]. The rest of the dp table is never read again, so keeping it only consumes extra space.\nThe current baseline is the runnable array version. Its correctness already covers one-house, two-house, and circular examples, but its space complexity is O(n). This step changes only the storage inside the linear helper. It does not change the two circular candidate ranges or the n == 1 guard.\nIn the previous rob_line, use two rolling states to retain the two most recent prefix results:\nprev2 is the previous round\u0026rsquo;s dp[offset - 2]. prev1 is the previous round\u0026rsquo;s dp[offset - 1]. First compute cur, then move prev1 into prev2 and cur into prev1. class Solution: @staticmethod def rob_line(nums: list[int], left: int, right: int) -\u0026gt; int: size = right - left + 1 prev2 = nums[left] if size == 1: return prev2 prev1 = max(nums[left], nums[left + 1]) for offset in range(2, size): current = nums[left + offset] cur = max(prev1, prev2 + current) prev2, prev1 = prev1, cur return prev1 def rob(self, nums: list[int]) -\u0026gt; int: n = len(nums) if n == 1: return nums[0] exclude_last = self.rob_line(nums, 0, n - 2) exclude_first = self.rob_line(nums, 1, n - 1) return max(exclude_last, exclude_first) Check the final version:\nsolver = Solution() assert solver.rob([2, 3, 2]) == 3 assert solver.rob([1, 2, 3, 1]) == 4 assert solver.rob([1, 2, 3]) == 3 assert solver.rob([5]) == 5 assert solver.rob([2, 7, 9, 3, 1]) == 11 The last example is circular: excluding the last house gives the linear range [2,7,9,3] with result 11, while excluding the first house gives [7,9,3,1] with result 10, so the answer is 11.\nThe correctness invariant is: after processing an offset, prev1 equals the optimum for that range prefix, while prev2 equals the optimum for the preceding prefix. The next iteration can therefore still compute both \u0026ldquo;skip current\u0026rdquo; and \u0026ldquo;rob current\u0026rdquo; accurately. The two candidate ranges cover the first-last conflict, and the one-house guard covers the smallest input.\nThis version can:\nresolve the circular first-last conflict with two linear candidate ranges cover every non-empty input while preserving the same results as the array version run in O(n) time with O(1) extra space This is the final incremental checkpoint for this problem. Only an independent review of the teaching chain and final code remains; there is no second reference answer that introduces new logic.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/213-house-robber-ii/","summary":"\u003ch2 id=\"problem\"\u003eProblem\u003c/h2\u003e\n\u003ch3 id=\"input-and-output\"\u003eInput and Output\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eInput: an integer array \u003ccode\u003enums\u003c/code\u003e, where \u003ccode\u003enums[i]\u003c/code\u003e is the money in the \u003ccode\u003ei\u003c/code\u003e-th house.\u003c/li\u003e\n\u003cli\u003eThe houses form a circle. Adjacent houses cannot both be robbed, so houses \u003ccode\u003e0\u003c/code\u003e and \u003ccode\u003en - 1\u003c/code\u003e cannot both be robbed either.\u003c/li\u003e\n\u003cli\u003eOutput: the maximum amount of money that can be robbed without triggering the alarm.\u003c/li\u003e\n\u003cli\u003eConstraints: \u003ccode\u003e1 \u0026lt;= nums.length \u0026lt;= 100\u003c/code\u003e, \u003ccode\u003e0 \u0026lt;= nums[i] \u0026lt;= 1000\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"examples\"\u003eExamples\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [2,3,2]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 3\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: Houses 0 and 2 are adjacent around the circle, so they cannot both be robbed; rob the house containing 3.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [1,2,3,1]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 4\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: We can rob indices 0 and 2, or indices 1 and 3.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [1,2,3]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 3\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis tutorial uses Python only and starts with the smallest first-last conflict.\u003c/p\u003e","title":"LeetCode 213: House Robber II, Splitting the Circle into Two Linear Problems"},{"content":"Problem Requirements You are given an integer array coins, where coins[i] is a coin denomination, and a non-negative integer amount. The goal is to make a total exactly equal to amount.\nYou may use each denomination any number of times. Return the minimum number of coins needed to make amount. If the amount cannot be made, return -1. If amount = 0, no coins are needed, so the answer is 0. The constraints are 1 \u0026lt;= coins.length \u0026lt;= 12, 1 \u0026lt;= coins[i] \u0026lt;= 2^31 - 1, and 0 \u0026lt;= amount \u0026lt;= 10^4. All coin denominations are distinct.\nFor example:\nInput: coins = [1, 2, 5], amount = 11 Output: 3 Explanation: 5 + 5 + 1 uses 3 coins. Input: coins = [2], amount = 3 Output: -1 Explanation: It is impossible to make 3 using only denomination 2. Input: coins = [1], amount = 0 Output: 0 Explanation: The target is already 0, so no coin needs to be chosen. Step 1: Break the Target Amount into a Smaller Problem Start with a small example that exposes the structure of the problem:\ncoins = [1, 3, 4] amount = 6 If we follow the intuition of taking the largest denomination first, we might take 4, leave 2, and end with 4 + 1 + 1, which uses 3 coins. But 3 + 3 is better and uses only 2 coins.\nThe current baseline is \u0026ldquo;choose a coin that looks large at every step.\u0026rdquo; It fails on this example: the same target has several possible combinations, and a locally larger denomination does not guarantee the smallest total number of coins.\nThis failure forces a concrete question: if a particular coin is chosen last, how much remains? We must compare different choices for the last coin. Once one coin is fixed, the remaining amount becomes a smaller problem of exactly the same form.\nFor now, add only a definition for this smaller problem:\nbest(x) = minimum number of coins needed to make amount x This step only identifies the question that will be asked repeatedly. We have not yet defined the completion condition best(0), nor have we written the transition from best(x - coin) to best(x).\nCheck this step: 4 + 1 + 1 uses 3 coins, while 3 + 3 uses 2. Looking at only one coin choice cannot determine the answer; we must continue with the remaining amount.\nThis checkpoint can:\nstate the input, output, and the rule that each denomination may be reused use [1, 3, 4], 6 to prove that direct greedy selection is insufficient rewrite the large target as a question about smaller amounts It still needs:\na stopping rule when the remaining amount is 0 a way to try every coin for a positive amount and compare the results Step 2: Handle Amount 0 First Ask the smallest possible question: if the remaining amount is already 0, how many more coins are needed? The answer is not \u0026ldquo;impossible\u0026rdquo; but 0 coins. We need to fix this completion condition before any later coin-selection path can know when to stop.\nThe current baseline is the best(x) definition from the previous section, but it has no stopping rule. It loses its meaning at x = 0; if even the smallest amount has no answer, there is nothing for the positive-amount cases to build on.\nAdd only the zero-amount branch to the previous best(x) definition:\ndef best(x: int) -\u0026gt; int: if x == 0: return 0 raise NotImplementedError(\u0026#34;Candidates for positive amounts arrive in the next step\u0026#34;) The raise deliberately preserves the current gap: this checkpoint verifies only the completion condition and does not pretend to solve positive amounts.\nCheck this step: substituting x = 0 makes the function return 0 immediately. Substituting x = 1 explicitly exposes the missing positive-amount transition instead of returning a temporary value that could be mistaken for an answer.\nThis checkpoint can:\nestablish best(0) = 0, meaning that the target has been completed separate the positive-amount branch from the completion condition, leaving a clear place for the next coin-selection rule It still needs:\na way to try every coin no larger than x when x \u0026gt; 0 a way for each candidate to connect to best for another, smaller amount Step 3: Enumerate the Last Coin to Get a First Correct Version Now fill the remaining gap. For a positive amount x, the last coin could be any denomination in coins. After choosing coin, the preceding coins still need to make x - coin. If that smaller amount is reachable, add 1 for the current coin.\nThe current baseline is best(x) from the previous section: it returns 0 when x = 0, but it raises an exception for every x \u0026gt; 0. The failure is concrete: without a candidate transition, no positive amount can be answered.\nIn the previous best function, replace NotImplementedError with logic that tries every coin and keeps the minimum:\ndef coin_change_recursive(coins: list[int], amount: int) -\u0026gt; int: def best(x: int) -\u0026gt; int: if x == 0: return 0 minimum = float(\u0026#34;inf\u0026#34;) for coin in coins: if coin \u0026gt; x: continue rest = best(x - coin) if rest == -1: continue minimum = min(minimum, rest + 1) return -1 if minimum == float(\u0026#34;inf\u0026#34;) else minimum return best(amount) Here, minimum is only the best candidate for the current x. float(\u0026quot;inf\u0026quot;) means that no reachable candidate has been found yet. The current coin is added only when the recursive result is not -1.\nCheck this step with coins = [1, 3, 4] and amount = 6. If the last coin is 4, the remaining amount 2 needs at least 2 coins, so that branch uses 3 coins in total. If the last coin is 3, the remaining amount 3 needs just 1 coin, so that branch uses 2 coins, and the function keeps it. Running two small examples should also produce:\n[1, 2, 5], 11 -\u0026gt; 3 [2], 3 -\u0026gt; -1 This checkpoint can:\ntry every legal last coin for each positive amount handle a smaller amount that cannot be reached produce the first complete and correct answer for small inputs It still needs:\na way to avoid recomputing the same best(x) through different paths better performance for large amounts, because this correct baseline builds a rapidly growing recursion tree Step 4: Cache Amounts That Have Already Been Solved First observe how the recursion from Step 3 repeats the same question. For [1, 3, 4] and 6, one path contains best(6) -\u0026gt; best(5) -\u0026gt; best(2), while another contains best(6) -\u0026gt; best(3) -\u0026gt; best(2). Both paths need exactly the same answer for best(2), but they recompute the entire subtree.\nThe current baseline is the recursive version with the correct transition. Its failure is not a wrong answer but duplicated work: as the amount grows, the number of repeated recursive branches grows as well.\nAdd only an amount-based cache decorator before the previous best definition. The candidate transition inside the function remains unchanged:\nfrom functools import cache def coin_change_memo(coins: list[int], amount: int) -\u0026gt; int: @cache def best(x: int) -\u0026gt; int: if x == 0: return 0 minimum = float(\u0026#34;inf\u0026#34;) for coin in coins: if coin \u0026gt; x: continue rest = best(x - coin) if rest == -1: continue minimum = min(minimum, rest + 1) return -1 if minimum == float(\u0026#34;inf\u0026#34;) else minimum return best(amount) The cache key is only the amount x: the minimum number of coins for the same x does not depend on the path used to reach it. At most amount + 1 distinct amount states are therefore solved. \u0026ldquo;Recompute on every path\u0026rdquo; becomes \u0026ldquo;solve each state once.\u0026rdquo;\nCheck this step by running the cached version on the same inputs. The answers should remain [1, 2, 5], 11 -\u0026gt; 3, [2], 3 -\u0026gt; -1, and [1, 3, 4], 6 -\u0026gt; 2, and the number of states for each case should not exceed amount + 1. For [1, 3, 4], 6, which really does contain repeated subproblems, cache_info() should report more than zero hits. A single failing path such as [2], 3 may correctly report zero hits.\nThis checkpoint can:\npreserve the transition and unreachable-case handling from Step 3 keep one recursive result for each amount, with at most amount + 1 states It still needs:\na way to remove the recursion stack, which still grows with the amount an iterative table that applies the same transition from smaller amounts to larger ones Step 5: Replace Recursion with a Bottom-Up Table Step 4 established that each amount state only needs to be solved once. However, it still asks for smaller amounts through function calls. For a large amount, the call stack itself becomes a burden.\nThe current baseline is the memoized best(x). Its failure is not an unclear state definition; the states are still hidden inside recursive calls. We need to lay out the answers for amounts 0 through amount explicitly in a table.\nKeep the candidate transition from the previous version, but replace \u0026ldquo;recursively solve the smaller amount\u0026rdquo; with a lookup in an already-filled dp table:\ndef coin_change_table(coins: list[int], amount: int) -\u0026gt; int: dp = [float(\u0026#34;inf\u0026#34;)] * (amount + 1) dp[0] = 0 for x in range(1, amount + 1): for coin in coins: if coin \u0026gt; x or dp[x - coin] == float(\u0026#34;inf\u0026#34;): continue dp[x] = min(dp[x], dp[x - coin] + 1) return -1 if dp[amount] == float(\u0026#34;inf\u0026#34;) else dp[amount] The state meaning is: dp[x] is the minimum number of coins needed to make exactly amount x. The initialization dp[0] = 0 carries forward the completion condition already verified in the previous section. Every other entry starts at infinity to mean \u0026ldquo;no solution has been found yet.\u0026rdquo; When processing x, every smaller entry dp[x - coin] has already been filled.\nWalk through the table slowly for coins = [1, 2, 5] and amount = 11:\nx 0 1 2 3 4 5 6 7 8 9 10 11 dp[x] 0 1 1 2 2 1 2 2 3 3 2 3 Therefore, dp[11] = 3. This table uses the same transition as the recursive version; it only replaces \u0026ldquo;call the smaller amount\u0026rdquo; with \u0026ldquo;read the completed table entry.\u0026rdquo;\nCheck this step by running the table version. It should produce [1, 2, 5], 11 -\u0026gt; 3, [2], 3 -\u0026gt; -1, [1], 0 -\u0026gt; 0, and [1, 3, 4], 6 -\u0026gt; 2. The complete table for [1, 2, 5], 11 should also match the manual table above.\nThis checkpoint can:\nstore the optimal value for every amount in an explicit array produce the first complete iterative solution without relying on the recursion stack It still needs:\nan explanation of why amounts are processed in ascending order and how that permits unlimited reuse of a denomination a connection between the name \u0026ldquo;unbounded knapsack\u0026rdquo; and the update order Step 6: Why This Is Unbounded Knapsack Now consider an example that cannot avoid reusing the same denomination:\ncoins = [2] amount = 6 The answer must be 2 + 2 + 2, or 3 copies of the same coin. The current dp[x] transition can already express this answer, but it has not explained why the update order permits the 2 that was just used to be used again. If we mistakenly treat this as a 0/1 problem in which each coin may be chosen only once, we lose the successive states for 4 and 6.\nThis failure exposes the meaning of the traversal order. When a denomination coin is being processed, the inner amount loop must move from smaller to larger amounts. After computing dp[2] = 1, computing dp[4] may read that state, which already includes the current denomination. Computing dp[6] may then read dp[4].\nReorganize the two loops in the previous table version so that unlimited reuse is explicit:\ndef coin_change_complete(coins: list[int], amount: int) -\u0026gt; int: dp = [float(\u0026#34;inf\u0026#34;)] * (amount + 1) dp[0] = 0 for coin in coins: for x in range(coin, amount + 1): if dp[x - coin] == float(\u0026#34;inf\u0026#34;): continue dp[x] = min(dp[x], dp[x - coin] + 1) return -1 if dp[amount] == float(\u0026#34;inf\u0026#34;) else dp[amount] For [2] and 6, the update trace is: at x = 2, set dp[2] = 1; at x = 4, read dp[2] and set dp[4] = 2; at x = 6, read dp[4] and set dp[6] = 3. This is unbounded knapsack: one item (here, one denomination) may be taken any number of times, so the inner amount loop runs in ascending order. A 0/1 knapsack uses descending order instead so the current item cannot be read again during the same outer iteration. The two directions are not interchangeable.\nThe objective here is to minimize the number of coins, not to count different orderings. Each update therefore uses only min; it does not count 2 + 5 and 5 + 2 as two separate answers.\nCheck this step by running the reorganized loops. They should produce coins = [2], amount = 6 -\u0026gt; 3 and coins = [2, 3, 5], amount = 7 -\u0026gt; 2. The earlier official and pressure examples should keep the same answers.\nThis checkpoint can:\nexpress unlimited reuse as a coin-outer, amount-ascending unbounded-knapsack update explain why dp[x - coin] may come from a state updated earlier in the current outer iteration preserve the minimum-count objective instead of turning it into an ordering count It still needs:\nthe final LeetCode class Solution interface a unified unreachable return, correctness invariant, and complexity analysis Step 7: Freeze the Final Implementation The previous section expressed reusable coins through a coin-outer loop and an amount-ascending inner loop. This checkpoint only prepares the solution for delivery: place the same logic in the class Solution interface required by LeetCode, and convert an unreachable sentinel to -1. No new state or transition is introduced.\nThe current baseline is the runnable coin_change_complete function. Its remaining failure is that it cannot yet be submitted through the required interface, and the return rules for amount = 0, unreachable amounts, and ordinary amounts need to live in one method.\nIntegrate the previous function into the single final implementation:\nclass Solution: def coinChange(self, coins: list[int], amount: int) -\u0026gt; int: unreachable = amount + 1 dp = [unreachable] * (amount + 1) dp[0] = 0 for coin in coins: for current in range(coin, amount + 1): dp[current] = min(dp[current], dp[current - coin] + 1) return -1 if dp[amount] == unreachable else dp[amount] unreachable = amount + 1 is a sufficiently large finite sentinel. Any feasible solution uses at most amount coins because every denomination is at least 1, so the sentinel cannot be confused with a valid answer. The initialization dp[0] = 0 remains the verified completion condition. Every update still reads dp[current - coin] after smaller amounts have been updated with the current denomination, so the unlimited-reuse semantics are unchanged.\nCorrectness follows from this invariant: after processing a denomination coin, for every current, dp[current] is the minimum number of coins needed to make current using only the denominations processed so far, with each denomination reusable any number of times. The ascending scan allows the current denomination to extend a smaller result produced earlier in the same outer iteration. After all denominations have been processed, dp[amount] is the minimum required by the problem. If it is still the sentinel, no combination can make the target, so the method returns -1.\nThere are amount + 1 states, and every denomination may inspect each state once. The time complexity is therefore O(amount * len(coins)), and the extra space complexity is O(amount).\nBoundary regression cases:\ncoins = [1, 2, 5], amount = 11 -\u0026gt; 3 coins = [2], amount = 3 -\u0026gt; -1 coins = [1], amount = 0 -\u0026gt; 0 coins = [2], amount = 6 -\u0026gt; 3 coins = [1, 3, 4], amount = 6 -\u0026gt; 2 This checkpoint can:\nsatisfy LeetCode\u0026rsquo;s class Solution method contract directly cover reachable, unreachable, zero-amount, and repeated-denomination cases connect the final code, correctness argument, and complexity analysis directly to the checkpoints already established The incremental build is now frozen. There is no second Reference Answer that introduces new logic.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/322-coin-change/","summary":"\u003ch2 id=\"problem-requirements\"\u003eProblem Requirements\u003c/h2\u003e\n\u003cp\u003eYou are given an integer array \u003ccode\u003ecoins\u003c/code\u003e, where \u003ccode\u003ecoins[i]\u003c/code\u003e is a coin denomination, and a non-negative integer \u003ccode\u003eamount\u003c/code\u003e. The goal is to make a total exactly equal to \u003ccode\u003eamount\u003c/code\u003e.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eYou may use each denomination any number of times.\u003c/li\u003e\n\u003cli\u003eReturn the minimum number of coins needed to make \u003ccode\u003eamount\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eIf the amount cannot be made, return \u003ccode\u003e-1\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eIf \u003ccode\u003eamount = 0\u003c/code\u003e, no coins are needed, so the answer is \u003ccode\u003e0\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe constraints are \u003ccode\u003e1 \u0026lt;= coins.length \u0026lt;= 12\u003c/code\u003e, \u003ccode\u003e1 \u0026lt;= coins[i] \u0026lt;= 2^31 - 1\u003c/code\u003e, and \u003ccode\u003e0 \u0026lt;= amount \u0026lt;= 10^4\u003c/code\u003e. All coin denominations are distinct.\u003c/p\u003e","title":"LeetCode 322: Coin Change, Deriving Unbounded Knapsack DP from Smaller Amounts"},{"content":"Problem Input and Output Input: an integer array nums Output: return the length of the longest strictly increasing subsequence in nums A subsequence preserves the relative order of the original array, but it does not need to be contiguous, so elements may be skipped If the selected indices are i_1 \u0026lt; i_2 \u0026lt; ... \u0026lt; i_k, they must also satisfy nums[i_1] \u0026lt; nums[i_2] \u0026lt; ... \u0026lt; nums[i_k] The subsequence is non-empty, so at least one element must be selected Constraints: 1 \u0026lt;= nums.length \u0026lt;= 2500, -10^4 \u0026lt;= nums[i] \u0026lt;= 10^4 Examples Input: nums = [10,9,2,5,3,7,101,18] Output: 4 Explanation: [2,5,7,101] is a strictly increasing subsequence of length 4; its elements do not need to be contiguous in the original array. Input: nums = [0,1,0,3,2,3] Output: 4 Explanation: [0,1,2,3] has length 4; [0,1,3] is also valid, but shorter. This article uses Python only. We start with the local question \u0026ldquo;how long can a subsequence ending at each position be?\u0026rdquo; and gradually turn it into a runnable solution.\nStep 1: First Ask Which Position the Subsequence Ends At Start with the first example:\nIndex: 0 1 2 3 4 5 6 7 Value: 10 9 2 5 3 7 101 18 The answer [2,5,7,101] skips 9 and 3. When we reach the position whose value is 7, it is not enough to ask, \u0026ldquo;Which value is immediately to its left?\u0026rdquo; Instead, we need to ask:\nWhich earlier, smaller values can be valid predecessors, and how long can the subsequence become after appending 7?\nThe current baseline is: we know that the problem asks for the longest strictly increasing subsequence in the entire array, but we do not yet have a state that describes the result for one particular position.\nWhere does this break? If we enumerate whole candidates directly, it is easy to confuse a subsequence with a contiguous subarray. We also cannot reuse the results ending at 2, 5, or 3 to determine the result ending at 7.\nMake one change to the current problem: shrink it into a question indexed by the ending position.\nend_at_i = the length of the longest strictly increasing subsequence that must select nums[i] and use nums[i] as its final element For example, at index 5, whose value is 7:\n2 -\u0026gt; 5 -\u0026gt; 7 is valid; 2 -\u0026gt; 3 -\u0026gt; 7 is also valid; 10 -\u0026gt; 7 is invalid because 10 \u0026lt; 7 is false; 7 alone can always form a subsequence of length 1. At index 6, whose value is 101, we can continue from 7. Instead of asking for the global longest length all at once, we first answer the local question for every possible ending position.\nCheck this change: return to [10,9,2,5,3,7,101,18]. The end_at_i question always requires the current element to be the final element and preserves the original index order. It allows elements to be skipped, but it does not allow indices to run backward or non-increasing values to be appended.\nThis version can now:\ndistinguish a subsequence from a contiguous subarray; associate each position with a smaller problem that must end there; show that the result ending at 7 needs to reuse results from earlier positions. It still lacks:\na runnable state for every position; the minimum value of end_at_i when there is no valid predecessor; an array representation of this state and a base case for a single element. Step 2: With No Predecessor, One Element Has Length 1 We now know that each position needs its own end_at_i. Start with the simplest case: no earlier element can be appended before the current one.\nThe current baseline is: Step 1 provides only a verbal state definition; there is no array that stores these states yet.\nWhere does this break? Without a valid predecessor, the state has no starting value. Even if we later find a smaller element, we would not know what value to increment.\nAdd one base case to the previous version: any single element is a strictly increasing subsequence of length 1. Initialize every position to 1. To inspect this state table first, the current function temporarily returns the array; this is not yet the final integer required by the problem.\ndef length_of_lis(nums: list[int]) -\u0026gt; list[int]: n = len(nums) dp = [1] * n return dp Here, dp[i] is the code representation of end_at_i from Step 1:\ndp[i] = the length of the longest strictly increasing subsequence that must end at nums[i] Check this change: run only the initialization and inspect several cases with no usable predecessor.\nassert length_of_lis([7]) == [1] assert length_of_lis([7, 7]) == [1, 1] assert length_of_lis([10, 9, 2]) == [1, 1, 1] The repeated values [7, 7] still produce two 1s because strict increase requires the previous value to be smaller than the next value. No connection between positions has been made yet.\nThis version can now:\ngive every position a valid single-element starting state; turn end_at_i into the runnable state dp[i] for the first time; assign the correct local length 1 when no predecessor exists. It still lacks:\na check for whether an earlier position can precede the current one; a way to extend a length-1 state into a longer subsequence; an update that considers every earlier and smaller value. Step 3: Append the Current Element Only After an Earlier, Smaller Value Now look at the position whose value is 7. Before it are the larger values 10 and 9, as well as the smaller values 2, 5, and 3. Looking only at the adjacent position, or ignoring the value comparison, would create invalid sequences.\nThe current baseline is: Step 2 places 1 at every position, so every element can form a subsequence by itself, but every dp[i] is still 1.\nWhere does this break? Any subsequence longer than 1 must append the current element after an earlier valid subsequence. We have not enumerated those candidate predecessors or checked whether they can be followed by the current value.\nAdd one transition to the previous version: when processing position i, inspect every earlier position j. Position j can be a predecessor only when nums[j] \u0026lt; nums[i]. After appending nums[i], the candidate length is dp[j] + 1.\ndef length_of_lis(nums: list[int]) -\u0026gt; list[int]: n = len(nums) dp = [1] * n for i in range(n): for j in range(i): if nums[j] \u0026lt; nums[i]: dp[i] = max(dp[i], dp[j] + 1) return dp The max here updates only the local state ending at nums[i]; it does not yet compute the final answer for the entire array. The condition j \u0026lt; i preserves the original order, while nums[j] \u0026lt; nums[i] enforces strict increase rather than allowing equal values.\nCheck this change: when the example reaches i = 5, whose value is 7, inspect each predecessor:\nj nums[j] Can it precede 7? Candidate length 0 10 No, because 10 \u0026lt; 7 is false - 1 9 No, because 9 \u0026lt; 7 is false - 2 2 Yes dp[2] + 1 = 2 3 5 Yes dp[3] + 1 = 3 4 3 Yes dp[4] + 1 = 3 Therefore, dp[5] = 3, for example from 2 -\u0026gt; 5 -\u0026gt; 7. Also check repeated values:\nassert length_of_lis([10, 9, 2, 5, 3, 7, 101, 18]) == [1, 1, 1, 2, 2, 3, 4, 4] assert length_of_lis([7, 7, 7]) == [1, 1, 1] This version can now:\nscan all candidate predecessors before each position; append the current element only after an earlier, smaller value; compute the longest length ending at every position rather than considering only adjacent elements. It still lacks:\na single answer, because dp currently stores one local result for every ending position; a way to account for the longest subsequence ending at any position, not necessarily the last one; an aggregation of all dp[i] values into the integer required by the problem. Step 4: The Answer Is the Maximum over All Ending States Step 3 computes the length ending at every position, but the problem asks for the longest length anywhere in the array. The longest subsequence does not have to end at the last element.\nThe current baseline is: the function returns the entire dp array, and each position stores a correct local result.\nWhere does this break? Returning dp[-1] assumes that an optimal subsequence must use the final element. For example:\nnums = [1, 2, 3, 0] dp = [1, 2, 3, 1] The longest subsequence [1,2,3] ends at index 2, so the answer is 3; however, dp[-1] is only 1.\nChange only the answer aggregation in the previous version: replace the temporary return dp with return max(dp). This max compares all possible ending positions, while the initialization and predecessor transition already established above remain unchanged. To make the code directly submit-ready, place the same logic inside LeetCode\u0026rsquo;s required Solution method; the wrapper adds no new algorithmic logic.\nclass Solution: def lengthOfLIS(self, nums: list[int]) -\u0026gt; int: n = len(nums) dp = [1] * n for i in range(n): for j in range(i): if nums[j] \u0026lt; nums[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) Check this change:\nassert Solution().lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]) == 4 assert Solution().lengthOfLIS([0, 1, 0, 3, 2, 3]) == 4 assert Solution().lengthOfLIS([7, 7, 7, 7, 7, 7, 7]) == 1 assert Solution().lengthOfLIS([1, 2, 3, 0]) == 3 The final assertion specifically checks a case in which the optimal subsequence ends before the last position. It ensures that the function returns the maximum across all dp[i], not merely the final state.\nThis version can now:\ncompute the longest strictly increasing subsequence ending at every position; select the global answer from all local states; run as a complete LeetCode Python submission. It still lacks:\na complete table that recomputes every state update step by step; a proof that enumerating every earlier predecessor covers all valid subsequences; a check of the O(n^2) time, O(n) space, and boundary cases. Step 5: Walk Through the State Table Slowly The current baseline is: Step 4 provides a submit-ready O(n^2) implementation that returns the correct answer for the standard example.\nWhere does this break? A passing implementation alone does not confirm the meaning of every dp[i], the coverage of the predecessor scan, why max(dp) is necessary, or whether O(n^2) fits the problem constraints.\nAdd only verification evidence to the previous version: do not change any state or transition. Add a complete state table, an invariant proof, a complexity check, and boundary checks. No new algorithm is introduced here.\nCheck this change: compute every state for nums = [10,9,2,5,3,7,101,18]:\ni nums[i] Earlier indices that can precede i Computation of dp[i] dp[i] 0 10 None Single-element subsequence 1 1 9 None Single-element subsequence 1 2 2 None Single-element subsequence 1 3 5 2 dp[2] + 1 = 2 2 4 3 2 dp[2] + 1 = 2 2 5 7 2, 3, 4 max(2, 3, 3) = 3 3 6 101 0, 1, 2, 3, 4, 5 max(2, 2, 2, 3, 3, 4) = 4 4 7 18 0, 1, 2, 3, 4, 5 max(2, 2, 2, 3, 3, 4) = 4 4 The final state is:\ndp = [1, 1, 1, 2, 2, 3, 4, 4] Therefore, max(dp) = 4, matching the example output.\nCorrectness Express the meaning of dp[i] as an invariant:\nAfter processing index i, dp[i] is the length of the longest strictly increasing subsequence that must select nums[i] and end there.\nBase case: every element alone is a subsequence of length 1, so initializing dp[i] = 1 is correct.\nTransition: if a valid subsequence ends at nums[i], it either contains only nums[i], or it has a final predecessor j satisfying j \u0026lt; i and nums[j] \u0026lt; nums[i]. In the second case, its length is dp[j] + 1. The code enumerates every such j and takes the maximum, so it misses no valid predecessor and never appends an element in reverse index order or after an equal value.\nAnswer aggregation: the problem does not require the longest subsequence to end at the final position, so any valid ending position may contain the answer. max(dp) selects the longest among them.\nComplexity Time complexity: the nested loops over i and j inspect at most n(n - 1) / 2 index pairs, so the time complexity is O(n^2). Extra space complexity: the algorithm stores only the length-n dp array, so the extra space complexity is O(n). The constraint is n \u0026lt;= 2500, so this sequence DP solution is sufficient for the problem limits. Boundary Cases and Common Mistakes A single-element array has answer 1, which the base case already covers. Equal values cannot be connected. The condition must be the strict nums[j] \u0026lt; nums[i], not \u0026lt;=. In a decreasing array, every dp[i] remains 1. A subsequence does not need to be contiguous; only the indices must remain increasing. Comparing adjacent elements alone misses valid predecessors. dp[i] means \u0026ldquo;must end at i\u0026rdquo;, not \u0026ldquo;the global answer among the first i elements\u0026rdquo;. These meanings cannot be mixed. Return max(dp) rather than assuming that the final position is the optimal ending position. This version can now:\nrecompute every predecessor choice with the state table; explain why dp[i], the strict \u0026lt; condition, and max(dp) are correct; determine from the constraints when the O(n^2) version is appropriate. It still lacks:\na derivation of an O(n log n) optimization using tails and binary search; a solution for input sizes beyond this problem\u0026rsquo;s constraints, which should be derived in a separate tutorial from new performance pressure rather than inserted into this checkpoint. Summary Break the global problem into one local problem per ending position: dp[i]. Every position has at least a single-element subsequence, so initialize it to 1. Transition only from an earlier, smaller predecessor: dp[i] = max(dp[i], dp[j] + 1). The longest subsequence can end anywhere, so the answer is max(dp). Under the constraint n \u0026lt;= 2500, the nested-loop O(n^2) sequence DP is sufficient. References and Further Practice LeetCode 300: Longest Increasing Subsequence The \u0026ldquo;state by ending position\u0026rdquo; idea in this article transfers to other sequence DP problems. An optimization for larger inputs should be derived in a separate checkpoint so that the state meaning is not skipped. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/300-longest-increasing-subsequence/","summary":"\u003ch2 id=\"problem\"\u003eProblem\u003c/h2\u003e\n\u003ch3 id=\"input-and-output\"\u003eInput and Output\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eInput: an integer array \u003ccode\u003enums\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eOutput: return the length of the longest strictly increasing subsequence in \u003ccode\u003enums\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eA subsequence preserves the relative order of the original array, but it does not need to be contiguous, so elements may be skipped\u003c/li\u003e\n\u003cli\u003eIf the selected indices are \u003ccode\u003ei_1 \u0026lt; i_2 \u0026lt; ... \u0026lt; i_k\u003c/code\u003e, they must also satisfy \u003ccode\u003enums[i_1] \u0026lt; nums[i_2] \u0026lt; ... \u0026lt; nums[i_k]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eThe subsequence is non-empty, so at least one element must be selected\u003c/li\u003e\n\u003cli\u003eConstraints: \u003ccode\u003e1 \u0026lt;= nums.length \u0026lt;= 2500\u003c/code\u003e, \u003ccode\u003e-10^4 \u0026lt;= nums[i] \u0026lt;= 10^4\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"examples\"\u003eExamples\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [10,9,2,5,3,7,101,18]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 4\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: [2,5,7,101] is a strictly increasing subsequence of length 4; its elements do not need to be contiguous in the original array.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [0,1,0,3,2,3]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 4\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: [0,1,2,3] has length 4; [0,1,3] is also valid, but shorter.\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis article uses Python only. We start with the local question \u0026ldquo;how long can a subsequence ending at each position be?\u0026rdquo; and gradually turn it into a runnable solution.\u003c/p\u003e","title":"LeetCode 300: Longest Increasing Subsequence, Deriving Sequence DP from Ending at i"},{"content":"Problem Requirements Given an array nums containing only positive integers, determine whether all its elements can be split into two subsets whose sums are equal.\nInput and Output Input: an integer array nums Output: return True if the array can be split equally; otherwise, return False Every element must belong to exactly one of the two subsets. No element may be discarded or used twice Examples Input: nums = [1,5,11,5] Output: True Explanation: [1,5,5] and [11] both have a sum of 11 Input: nums = [1,2,3,5] Output: False Explanation: the total is 11, and an odd total cannot be split equally Input: nums = [1,1] Output: True Explanation: put one 1 in each subset Constraints According to the LeetCode 416 statement:\n1 \u0026lt;= nums.length \u0026lt;= 200 1 \u0026lt;= nums[i] \u0026lt;= 100 sum(nums) \u0026lt;= 20000 Step 1: Reject Arrays with an Odd Total First Start with a question that does not involve choosing any elements yet: if two integer subsets have the same sum, what must be true about the total sum of the array?\nProblem Pressure Consider nums = [1,2,3,5]. Its total is 11. If both subsets have sum x, then we must have:\n2 * x = 11 No integer x can satisfy this equation, so we can return False for this input before trying to select any elements.\nCurrent Baseline At this point, we only have the problem contract: every element must eventually belong to one subset, but we have not recorded which elements are selected and cannot yet handle cases with an even total.\nBreak If the total is odd, continuing the search only performs work that must fail. If the total is even, however, parity alone does not tell us whether some subset actually reaches half the total.\nAdd a Parity Gate to the Previous Version def can_partition(nums: list[int]) -\u0026gt; bool: total = sum(nums) if total % 2 == 1: return False target = total // 2 # For now, only compute the target. The next step checks reachability. # This placeholder return is not the final answer for even-total cases. return False Check This Change assert can_partition([1, 2, 3, 5]) is False For [1,2,3,5], total % 2 == 1, so the function returns False before entering any element-selection logic. For [1,1], target is computed as 1, but this version still cannot decide whether the elements can form that 1.\nCurrent Checkpoint This version can now:\nrule out impossible inputs by checking whether the total is odd turn an even total into a target that still needs to be verified: target = total // 2 It still needs to:\ndetermine whether some subset of elements sums to exactly target handle every real even-total input; the return False in the even branch is still a placeholder The next step reduces the large problem to a smaller one: after processing a prefix of the array, which subset sums are reachable?\nStep 2: Record Which Sums Are Reachable After Each Prefix Problem Pressure The total of [1,1] is 2, so the target is 1. The parity check only tells us that the input is worth examining. It does not answer these questions: after seeing the first 1, can we form 1? After seeing the second 1, which sums can we form?\nWrite out the small process:\nElements processed Currently reachable sums None {0} First 1 {0, 1} Both 1s {0, 1, 2} The smaller problem is: after processing a prefix of the array, what set contains every sum that can be formed using only that prefix, with each element used at most once?\nCurrent Baseline Step 1 can already compute target for an even total, but the even branch still ends with the placeholder return False. The current version does not store any sum that can already be formed.\nBreak If we stare only at target, we lose the intermediate states. For example, the target for [1,5,11,5] is 11. We first need to know which smaller sums each prefix can form before we can decide whether the later 11, or the two 5s, can reach it.\nReplace the Placeholder Return in the Previous Version Start with a set of reachable sums. When processing a value num, there are two paths: skip it, or add it to every sum from the previous round. To ensure that the current element is processed only once, copy the old set into a new set and pass that new set to the next round.\ndef can_partition(nums: list[int]) -\u0026gt; bool: total = sum(nums) if total % 2 == 1: return False target = total // 2 reachable = {0} for num in nums: next_reachable = set(reachable) for current in reachable: new_sum = current + num if new_sum \u0026lt;= target: next_reachable.add(new_sum) reachable = next_reachable return target in reachable Check This Change First, trace [1,1]:\nreachable = {0} Read the first 1 -\u0026gt; {0,1} Read the second 1 -\u0026gt; {0,1,2} target = 1, so return True Then run three executable checks:\nassert can_partition([1, 5, 11, 5]) is True assert can_partition([1, 2, 3, 5]) is False assert can_partition([1, 1]) is True Current Checkpoint This version can now:\nrewrite \u0026ldquo;can the array be split equally?\u0026rdquo; as \u0026ldquo;does the set of reachable sums contain target?\u0026rdquo; make it explicit that each array element is processed only once by copying the old set pass three representative examples with a runnable baseline It still needs:\na state table with fixed bounds and a more stable meaning for the relationship between a prefix and a sum control over the cost of copying sets, and a way to preserve the one-use rule when the state is compressed later The next step writes the set state as a two-dimensional table, producing a complete correct transition before compressing any space.\nStep 3: Express \u0026ldquo;Prefix + Sum\u0026rdquo; as a Two-Dimensional State Problem Pressure The set baseline already returns the right answer, but it creates a new problem: every input value requires copying and expanding an entire set. As target grows, the set\u0026rsquo;s contents and size both change dynamically. It is also difficult to inspect directly why a particular sum is reachable after a particular number of elements.\nFor [1,5,11,5], the target is 11. The set version generates many intermediate sums, but what we really want to ask is a fixed family of questions: using only the first i elements, can we form sum s?\nCurrent Baseline The reachable set from Step 2 stores every sum reachable from the current prefix. Each round copies the old set into next_reachable and then adds the new sums produced by choosing the current element.\nBreak The set version is correct, but it does not give us a fixed coordinate for the relationship between \u0026ldquo;the first i elements\u0026rdquo; and \u0026ldquo;target sum s.\u0026rdquo; To retain every prefix state, we need to make those two dimensions explicit.\nReplace the State Representation in the Previous Version Only now do we name this fixed state for \u0026ldquo;choose or skip each element once\u0026rdquo;: two-dimensional dp[i][s]. It means: using only nums[0] through nums[i-1], can we form sum s?\nStart with the base case that selects no elements: dp[0][0] = True. When processing the i-th element, num = nums[i - 1], there are only two sources:\nSkip num: carry forward dp[i - 1][s]. Choose num: only when s \u0026gt;= num can the state come from dp[i - 1][s - num] in the previous row. def can_partition(nums: list[int]) -\u0026gt; bool: total = sum(nums) if total % 2 == 1: return False target = total // 2 n = len(nums) dp = [[False] * (target + 1) for _ in range(n + 1)] dp[0][0] = True for i in range(1, n + 1): num = nums[i - 1] for s in range(target + 1): dp[i][s] = dp[i - 1][s] if s \u0026gt;= num: dp[i][s] = dp[i][s] or dp[i - 1][s - num] return dp[n][target] Check This Change For [1,1] with target = 1, inspect only the target columns:\ni (number processed) dp[i][0] dp[i][1] 0 True False 1 True True 2 True True Every \u0026ldquo;choose\u0026rdquo; transition reads from the previous row, so the second 1 cannot produce another new 1 within the same row. Now run the representative cases:\nassert can_partition([1, 5, 11, 5]) is True assert can_partition([1, 2, 3, 5]) is False assert can_partition([1, 1]) is True assert can_partition([2]) is False assert can_partition([2, 2]) is True Current Checkpoint This version can now:\nuse dp[i][s] to represent the reachable sums for every prefix explicitly express both skipping and choosing the current element with the previous row, giving us the first complete correct solution pass the main examples and the one-element and two-element edge cases It still needs:\nto reduce the O(n * target) extra space used by the two-dimensional table, since each row depends only on the previous row an update direction that preserves the \u0026ldquo;use each element at most once\u0026rdquo; rule after the rows are compressed into one dimension The next step attempts one-dimensional compression and uses a counterexample to find out why a forward update reuses the current element.\nStep 4: A Forward Update Reuses the Same Element After 1D Compression Problem Pressure In the two-dimensional table from Step 3, row i depends only on row i - 1, so it appears that we can retain just one row. This can reduce the space below O(n * target), but once new results are written back into the same array, we must decide in which direction to process the sums associated with num.\nCurrent Baseline The current baseline is the correct two-dimensional version: choosing num reads dp[i - 1][s - num] from the previous row. Once the table is compressed into one dimension, there is no physical copy of the previous row. The most direct attempt is to process s from small to large.\nBreak When we iterate forward, dp[s - num] may already have been set to True during the current round for this same num. That reuses one element and violates the requirement that each element may be used at most once.\nReplace the Previous Version with a Forward 1D Version For now, change only the state representation and use the intuitive forward loop:\ndef can_partition(nums: list[int]) -\u0026gt; bool: total = sum(nums) if total % 2 == 1: return False target = total // 2 dp = [False] * (target + 1) dp[0] = True for num in nums: for s in range(num, target + 1): dp[s] = dp[s] or dp[s - num] return dp[target] Check This Change Use nums = [1,1,4] as a counterexample. The total is 6, so the target is 3. The correct answer is False: the two 1s can form only 2, while 4 is larger than the target.\nBut when the first 1 is processed in forward order, the array changes like this:\nInitial: [True, False, False, False] s = 1: [True, True, False, False] # Used one 1 s = 2: [True, True, True, False] # Read dp[1], just written this round s = 3: [True, True, True, True] # Read dp[2], also written this round This failed version therefore uses one 1 as if it were three separate 1s and returns True. The two-dimensional version returns False for the same input. The True here is evidence of the bug, not the answer to the problem.\nCurrent Checkpoint This version can now:\nshow why the two-dimensional table can be compressed into a one-dimensional array use a concrete trace to locate the same-round reuse caused by forward updates It still needs:\ndp[s - num] to continue representing the state before the current element was added during this round an update direction that truly preserves 0/1 semantics and passes [1,1,4] The next step changes only the direction in which s is traversed and checks whether backward iteration restores the previous-row semantics.\nStep 5: Iterate Backward to Restore One-Use-Per-Element Semantics Problem Pressure The counterexample in Step 4 has reduced the problem to one place: a one-dimensional array does not retain a previous row, so a forward loop can read a state that was written in the current round. We do not need a new state definition. We only need the smaller sums to remain unchanged until later in the round.\nCurrent Baseline The current baseline is the forward version:\nfor num in nums: for s in range(num, target + 1): dp[s] = dp[s] or dp[s - num] Break When s moves from small to large, s - num is smaller than s, so it may already include the current num. That is exactly how the first 1 in [1,1,4] gets reused and incorrectly makes target 3 reachable.\nReplace Only the Traversal Direction in the Previous Version Move s downward from target to num:\nfor num in nums: for s in range(target, num - 1, -1): dp[s] = dp[s] or dp[s - num] Why can this version not reuse the current element? When processing s, the position s - num is smaller. A backward loop has not visited that smaller position yet, so dp[s - num] still contains its value from the start of the round, exactly like the previous row in the two-dimensional table.\nPut this one change back into the complete function:\ndef can_partition(nums: list[int]) -\u0026gt; bool: total = sum(nums) if total % 2 == 1: return False target = total // 2 dp = [False] * (target + 1) dp[0] = True for num in nums: for s in range(target, num - 1, -1): dp[s] = dp[s] or dp[s - num] return dp[target] Check This Change First rerun the counterexample from Step 4:\nassert can_partition([1, 1, 4]) is False Then cover the main examples and edge cases:\nassert can_partition([1, 5, 11, 5]) is True assert can_partition([1, 2, 3, 5]) is False assert can_partition([1, 1]) is True assert can_partition([2]) is False assert can_partition([2, 2]) is True At the end of each round for a value num, dp[s] means: \u0026ldquo;among the elements processed so far, does some subset sum to s?\u0026rdquo; Backward iteration changes only the write order, not the meaning of the state. It ensures that the smaller sum read during the current round does not yet include the current element.\nComplexity Let target = sum(nums) // 2. Each element scans at most target sums, so the time complexity is O(n * target). Keeping only one target-sum array uses O(target) extra space.\nCurrent Checkpoint This version can now:\ncomplete the 0/1 choice correctly with one-dimensional state, using each element at most once pass the forward version\u0026rsquo;s counterexample, the main examples, and the edge-case tests return the final answer in O(n * target) time and O(target) extra space It still needs:\na consolidated review of the invariant, edge cases, and the reason forward iteration is invalid, so the method is easier to transfer to other knapsack problems The next section only adds correctness and edge-case explanations. It does not change the algorithm.\nCorrectness and Edge Cases Loop Invariant After the first k elements have been processed, dp[s] means: using only those k elements, with each element chosen at most once, can we form sum s?\nInitially, no elements have been processed. Only the empty subset\u0026rsquo;s sum 0 is reachable, so dp[0] = True and every other position is False. When a new element num is processed, dp[s] can keep its old value, which means skipping num; or it can become True from dp[s - num], which means choosing num. Because s is updated from large to small, dp[s - num] has not yet been modified by the current num in this round. It therefore comes only from the first k elements, exactly matching the meaning of \u0026ldquo;read the previous row\u0026rdquo; in the two-dimensional transition. The invariant remains true after every round. When all elements have been processed, dp[target] answers exactly whether a subset sums to half of the total.\nEdge-Case Checks The following cases cover the branches most likely to cause mistakes:\nInput Result What it checks [1] False Odd total, so return early [2] False Even total, but the only element cannot form target 1 [1,1] True Use each of two equal elements once [2,2] True One element reaches target 2 [1,1,4] False One 1 cannot be reused within the same round [1,2,3,5] False Odd-total gate [1,5,11,5] True Standard partitionable example The problem constraints guarantee that nums is nonempty and that every element is a positive integer. We therefore do not add branches for an empty array or zero-valued elements; the edge-case discussion stays within the stated input domain.\nCommon Mistakes Writing the inner loop as range(num, target + 1): this forward update allows the same element to be reused within one round. It matches unbounded-knapsack semantics, not the 0/1 semantics required here. Writing the backward range as range(target, num, -1): this skips s == num. Use range(target, num - 1, -1) instead. Checking only whether the total is even: an even total is necessary, but target still has to be reachable. Summary The path through this problem can be compressed into four connected questions:\nIf the total is odd, reject the input immediately. If the total is even, rewrite the problem as \u0026ldquo;can we form target?\u0026rdquo; Each element can be chosen or skipped exactly once, so the state has 0/1 semantics. After one-dimensional compression, iterate backward so every state read in the current round still comes from the previous round. The key idea is not a line of template code but this chain of cause and effect: each element can be used only once -\u0026gt; one-dimensional state must not read a position just written in the same round -\u0026gt; the sum loop must run backward.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/416-partition-equal-subset-sum/","summary":"\u003ch2 id=\"problem-requirements\"\u003eProblem Requirements\u003c/h2\u003e\n\u003cp\u003eGiven an array \u003ccode\u003enums\u003c/code\u003e containing only positive integers, determine whether all its elements can be split into two subsets whose sums are equal.\u003c/p\u003e\n\u003ch3 id=\"input-and-output\"\u003eInput and Output\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eInput: an integer array \u003ccode\u003enums\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eOutput: return \u003ccode\u003eTrue\u003c/code\u003e if the array can be split equally; otherwise, return \u003ccode\u003eFalse\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eEvery element must belong to exactly one of the two subsets. No element may be discarded or used twice\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"examples\"\u003eExamples\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [1,5,11,5]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: True\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: [1,5,5] and [11] both have a sum of 11\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [1,2,3,5]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: False\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: the total is 11, and an odd total cannot be split equally\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [1,1]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: True\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: put one 1 in each subset\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"constraints\"\u003eConstraints\u003c/h3\u003e\n\u003cp\u003eAccording to the LeetCode 416 statement:\u003c/p\u003e","title":"LeetCode 416: Partition Equal Subset Sum, from the Half-Sum Target to Backward 0/1 Knapsack"},{"content":"Problem Requirement Design a stack named MinStack that supports these operations:\nMinStack(): initialize the stack. push(value): push value onto the stack. pop(): remove the top element. top(): return the top element. getMin(): return the minimum element in the stack. Every operation must run in O(1) time.\npop, top, and getMin are called only when the stack is non-empty, so no additional empty-stack return value is needed.\nExample Operations: [\u0026#34;MinStack\u0026#34;, \u0026#34;push\u0026#34;, \u0026#34;push\u0026#34;, \u0026#34;push\u0026#34;, \u0026#34;getMin\u0026#34;, \u0026#34;pop\u0026#34;, \u0026#34;top\u0026#34;, \u0026#34;getMin\u0026#34;] Arguments: [[], [-2], [0], [-3], [], [], [], []] Output: [null, null, null, null, -3, null, 0, -2] The sequence corresponds to:\nmin_stack = MinStack() min_stack.push(-2) min_stack.push(0) min_stack.push(-3) min_stack.getMin() # -3 min_stack.pop() min_stack.top() # 0 min_stack.getMin() # -2 Constraints -2^31 \u0026lt;= value \u0026lt;= 2^31 - 1 At most 3 * 10^4 calls are made to push, pop, top, and getMin The stack is non-empty whenever pop, top, or getMin is called Step 1: After Popping the Minimum, Where Does the Previous Minimum Come From? A regular stack already stores values and implements push, pop, and top in last-in, first-out order. The current baseline is:\nStore only stack elements. The top is the most recently pushed element that has not been popped. This baseline answers top() directly but not getMin(). The minimum may be at the bottom or in the middle rather than at the top.\nThe more important problem appears after pop(). Trace the official example:\nOperation Stack afterward Current minimum Return value Initialize [] None null push(-2) [-2] -2 null push(0) [-2, 0] -2 null push(-3) [-2, 0, -3] -3 null getMin() [-2, 0, -3] -3 -3 pop() [-2, 0] -2 null top() [-2, 0] -2 0 getMin() [-2, 0] -2 -2 After -3 is popped, the current minimum must immediately return to -2. A single variable holding the smallest value seen so far remains stuck at -3, which is no longer in the stack, and cannot tell us what to restore.\nDuplicate minima create another pressure:\nOperation Stack afterward Current minimum push(2) [2] 2 push(1) [2, 1] 1 push(1) [2, 1, 1] 1 pop() [2, 1] 1 After one 1 is popped, another remains, so the minimum must not return to 2.\nTogether these traces show that every stack state has its own current minimum. push enters a new state and pop returns to the previous state; the minimum must change in sync with that state transition.\nThis checkpoint can now:\nexplain why top() and getMin() return different kinds of information explain why popping a unique minimum must restore a historical minimum explain why a duplicate minimum survives one pop It still lacks:\na runnable class implementing all five interfaces a concrete way to calculate the correct minimum after the stack changes Step 2: Scan the Whole Stack When the Minimum Is Requested The current baseline knows that each stack state has a minimum but does not yet implement the class. First make the behavior correct: store every value in one list, and use the end of the list for push, pop, and top.\nWhen getMin() is called, scan the current list and return its minimum. Even if the unique minimum was just popped, this recomputes the correct answer from the remaining elements.\nThis does not contradict Step 1:\nA single current-minimum variable cannot restore history in O(1) after that value is popped. Scanning every remaining element can recompute the minimum, but takes O(n) time. Write this baseline as a complete class:\nclass MinStackScan: def __init__(self): self.values = [] def push(self, value: int) -\u0026gt; None: self.values.append(value) def pop(self) -\u0026gt; None: self.values.pop() def top(self) -\u0026gt; int: return self.values[-1] def getMin(self) -\u0026gt; int: return min(self.values) # Official operation sequence. stack = MinStackScan() stack.push(-2) stack.push(0) stack.push(-3) assert stack.getMin() == -3 stack.pop() assert stack.top() == 0 assert stack.getMin() == -2 # A duplicate minimum remains valid after one copy is popped. duplicates = MinStackScan() duplicates.push(2) duplicates.push(1) duplicates.push(1) assert duplicates.getMin() == 1 duplicates.pop() assert duplicates.getMin() == 1 # The minimum is at the bottom of an increasing stack and at the top of a decreasing stack. increasing = MinStackScan() for value in [1, 2, 3]: increasing.push(value) assert increasing.top() == 3 assert increasing.getMin() == 1 decreasing = MinStackScan() for value in [3, 2, 1]: decreasing.push(value) assert decreasing.top() == 1 assert decreasing.getMin() == 1 All five operations now behave correctly, but they do not all meet the required complexity:\nOperation Time complexity Reason MinStackScan() O(1) Create one empty list push(value) Amortized O(1) Append at the list end pop() O(1) Remove from the list end top() O(1) Read the list end getMin() O(n) Scan every current element Repeated getMin() calls compare the same elements again and again. The problem allows up to 3 * 10^4 operations and explicitly requires every method to be O(1), so this cannot be the final answer.\nThis checkpoint can now:\nimplement all five required interfaces return the right result after popping a unique minimum or one duplicate minimum serve as a correct baseline for the optimization It still lacks:\na getMin() that does not scan the whole stack state saved during push and pop that can restore a previous minimum Step 3: Synchronize the Minimum With Every Stack Depth The current baseline is correct, but every getMin() scans self.values. Those comparisons can instead happen during push, because after a new value is pushed there are only two candidates:\nminimum of the new state = min(minimum of the previous state, new value) Add a list named min_values with the same length as the value stack. min_values[i] stores the minimum of values[0:i+1], which is the current minimum when the value stack has depth i + 1.\nThe two lists change together in the official sequence:\nOperation values min_values push(-2) [-2] [-2] push(0) [-2, 0] [-2, -2] push(-3) [-2, 0, -3] [-2, -2, -3] pop() [-2, 0] [-2, -2] The ends of both lists always describe the same stack state. Therefore:\npush appends both the value and the new current minimum. pop removes the end of both lists. top reads values[-1]. getMin reads min_values[-1] directly. Duplicate minima are also stored once per depth. After pushing 2, 1, 1, min_values is [2, 1, 1]. One pop removes only the final 1; the earlier 1 remains at the top.\nReplace the linear scan with synchronized state:\nimport random class MinStack: def __init__(self): self.values = [] self.min_values = [] def push(self, value: int) -\u0026gt; None: self.values.append(value) if self.min_values: value = min(value, self.min_values[-1]) self.min_values.append(value) def pop(self) -\u0026gt; None: self.values.pop() self.min_values.pop() def top(self) -\u0026gt; int: return self.values[-1] def getMin(self) -\u0026gt; int: return self.min_values[-1] # Official operation sequence. stack = MinStack() stack.push(-2) stack.push(0) stack.push(-3) assert stack.getMin() == -3 stack.pop() assert stack.top() == 0 assert stack.getMin() == -2 # Duplicate minima must be retained at each depth. duplicates = MinStack() duplicates.push(2) duplicates.push(1) duplicates.push(1) assert duplicates.min_values == [2, 1, 1] duplicates.pop() assert duplicates.getMin() == 1 # Compare 5,000 fixed-seed valid operations with the Step 2 baseline. rng = random.Random(155) expected = MinStackScan() actual = MinStack() for _ in range(5_000): if not expected.values or rng.random() \u0026lt; 0.55: value = rng.randint(-100, 100) expected.push(value) actual.push(value) else: operation = rng.choice([\u0026#34;pop\u0026#34;, \u0026#34;top\u0026#34;, \u0026#34;getMin\u0026#34;]) if operation == \u0026#34;pop\u0026#34;: expected.pop() actual.pop() elif operation == \u0026#34;top\u0026#34;: assert actual.top() == expected.top() else: assert actual.getMin() == expected.getMin() assert len(actual.values) == len(actual.min_values) if actual.values: assert actual.min_values[-1] == min(actual.values) assert actual.top() == expected.top() assert actual.getMin() == expected.getMin() Why the Synchronized List Restores Historical Minima The following invariant holds before and after every operation:\nlen(values) == len(min_values) When values is non-empty: min_values[-1] == min(values) When values is empty, min_values must also be empty. The problem guarantees that top() and getMin() are not called then, so neither method needs to read a missing final element.\nOn the first push, the only value is naturally also the minimum. For each later push(value), the new prefix differs from the previous one by only value, so comparing the previous minimum with the new value gives the new minimum.\npop() removes both list ends and returns both structures to the previous depth. The minimum for that depth was already saved at the new min_values[-1], so values does not need to be scanned again.\nComplexity With Python lists, push and list-end pop are amortized O(1), while top and getMin are worst-case O(1). Under the stack-operation model used by the problem, all four interfaces meet the constant-time requirement. Both lists store one item per stack depth, so the space complexity is O(n).\nThe final version now:\nimplements every required interface correctly returns the current minimum in O(1) time restores the correct historical state after popping a unique or duplicate minimum passes the fixed cases and 5,000 randomized valid operations ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/155-min-stack/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eDesign a stack named \u003ccode\u003eMinStack\u003c/code\u003e that supports these operations:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eMinStack()\u003c/code\u003e: initialize the stack.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003epush(value)\u003c/code\u003e: push \u003ccode\u003evalue\u003c/code\u003e onto the stack.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003epop()\u003c/code\u003e: remove the top element.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003etop()\u003c/code\u003e: return the top element.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003egetMin()\u003c/code\u003e: return the minimum element in the stack.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eEvery operation must run in \u003ccode\u003eO(1)\u003c/code\u003e time.\u003c/p\u003e\n\u003cp\u003e\u003ccode\u003epop\u003c/code\u003e, \u003ccode\u003etop\u003c/code\u003e, and \u003ccode\u003egetMin\u003c/code\u003e are called only when the stack is non-empty, so no additional empty-stack return value is needed.\u003c/p\u003e\n\u003ch3 id=\"example\"\u003eExample\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOperations:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[\u0026#34;MinStack\u0026#34;, \u0026#34;push\u0026#34;, \u0026#34;push\u0026#34;, \u0026#34;push\u0026#34;, \u0026#34;getMin\u0026#34;, \u0026#34;pop\u0026#34;, \u0026#34;top\u0026#34;, \u0026#34;getMin\u0026#34;]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eArguments:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[[], [-2], [0], [-3], [], [], [], []]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[null, null, null, null, -3, null, 0, -2]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe sequence corresponds to:\u003c/p\u003e","title":"LeetCode 155: Min Stack, Keeping the Minimum in Sync With Stack State"},{"content":"Problem Requirement You are given a string s containing only these six characters:\n( ) { } [ ] Determine whether the string is valid. A valid string must satisfy all three conditions:\nEvery opening bracket is closed by the same type of closing bracket. Opening brackets are closed in the correct order. Every closing bracket has a corresponding opening bracket of the same type. Return True when all conditions hold; otherwise, return False.\nExamples Input Output \u0026quot;()\u0026quot; True \u0026quot;()[]{}\u0026quot; True \u0026quot;(]\u0026quot; False \u0026quot;([])\u0026quot; True \u0026quot;([)]\u0026quot; False Constraints 1 \u0026lt;= s.length \u0026lt;= 10^4 s contains only characters from ()[]{} LeetCode provides this method signature:\nclass Solution: def isValid(self, s: str) -\u0026gt; bool: pass Step 1: Why Equal Counts Can Still Be Invalid Compare these two strings:\n()[]{} ([)] Both have one ( and one ), plus one [ and one ]. A check that only counts each type of opening and closing bracket accepts both strings.\nThe current baseline is:\nCount each opening and closing bracket. If all counts match, the string is valid. This baseline gives the wrong answer for ([)]. The problem is not the number of brackets but their closing order: a closing bracket must close the most recently seen opening bracket that has not yet been closed.\nApply that rule to ([)]:\nIndex Character Unclosed opening brackets Decision 0 ( ( Wait for the matching ) 1 [ ([ [ is the most recent unclosed bracket 2 ) ([ ) cannot close the recent [, so the string is invalid The result is already known at index 2; the final ] cannot repair it. Although every bracket count eventually matches, ( and ) cross an unclosed [, producing an invalid order.\nUse the same rule on several representative inputs:\nInput Result First decisive reason ()[]{} Valid Every closing bracket closes the most recent matching opening bracket (] Invalid ] has a different type from the most recent ( ([)] Invalid When ) arrives, [ is the most recent unclosed bracket ([]) Valid ] closes [ first, then ) closes ( This checkpoint can now:\ndistinguish equal bracket counts from correct closing order explain examples by bracket type and the most recent unclosed position It still lacks:\nan algorithm that executes this decision for any input string runnable checks that compare the implementation with the manual reasoning Step 2: Repeatedly Remove Complete Pairs First The current baseline can judge closing order by hand, but it does not yet define an executable process.\nConsider a valid nested string:\n{[()]} The innermost () is already an adjacent complete pair, so remove it first:\n{[()]} -\u0026gt; remove () {[]} -\u0026gt; remove [] {} -\u0026gt; remove {} empty string Every non-empty valid parentheses string contains at least one adjacent complete pair, regardless of its nesting depth. Removing that pair preserves the relative order of the remaining characters, so the same operation can continue.\nAfter the manual rule from Step 1, add a runnable baseline: on each round, remove all three adjacent pair types. Stop when a round no longer shortens the string. The input is valid exactly when nothing remains.\ndef is_valid_by_elimination(s: str) -\u0026gt; bool: while True: previous = s s = s.replace(\u0026#34;()\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;[]\u0026#34;, \u0026#34;\u0026#34;).replace(\u0026#34;{}\u0026#34;, \u0026#34;\u0026#34;) if len(s) == len(previous): return s == \u0026#34;\u0026#34; assert is_valid_by_elimination(\u0026#34;()\u0026#34;) is True assert is_valid_by_elimination(\u0026#34;()[]{}\u0026#34;) is True assert is_valid_by_elimination(\u0026#34;(]\u0026#34;) is False assert is_valid_by_elimination(\u0026#34;([])\u0026#34;) is True assert is_valid_by_elimination(\u0026#34;([)]\u0026#34;) is False assert is_valid_by_elimination(\u0026#34;{[()]}\u0026#34;) is True assert is_valid_by_elimination(\u0026#34;(\u0026#34;) is False assert is_valid_by_elimination(\u0026#34;]\u0026#34;) is False This process must terminate:\nIf a round removes a pair, the string becomes at least 2 characters shorter. If its length does not change, no adjacent complete pair remains and the function returns immediately. It also rejects an invalid order correctly. For example, ([)] contains none of (), [], or {}, so the first round makes no progress and returns False.\nThe baseline still processes characters repeatedly. A string nested to depth n / 2 may expose only one new pair per round and require O(n) rounds. Python string replacement also scans and copies the current string, so the worst-case time complexity is O(n^2) and the extra string space is O(n).\nThis checkpoint can now:\nexecute a complete decision for any input that satisfies the character constraint express correct nesting through the order in which adjacent complete pairs disappear pass the official examples, deep nesting, and one-sided missing-bracket checks It still lacks:\na way to avoid scanning and copying already inspected characters repeatedly matching in one left-to-right pass Step 3: Keep Only the Opening Brackets That Are Still Unclosed The elimination baseline is correct, but the same character may be scanned and copied in many replacement rounds.\nReturn to the rule from Step 1: when a closing bracket arrives, we only need the most recent opening bracket that has not yet been closed. Earlier opening brackets must wait until that one closes. This is exactly last-in, first-out order.\nUse a stack named stack to store opening brackets that have been read but not closed:\nPush an opening bracket when it appears. A closing bracket must match the opening bracket at the top; pop after a match. If a closing bracket arrives while the stack is empty, or its type differs, return False immediately. After all characters are read, return True only when the stack is empty. Trace ([)]:\nCharacter Stack before Decision or operation Stack after ( [] Push opening bracket ['('] [ ['('] Push opening bracket ['(', '['] ) ['(', '['] ) needs ( but the top is [, return False Stop Now trace the valid nesting ([]):\nCharacter Stack before Decision or operation Stack after ( [] Push opening bracket ['('] [ ['('] Push opening bracket ['(', '['] ] ['(', '['] Match [, then pop ['('] ) ['('] Match (, then pop [] Replace repeated elimination with one scan:\nimport random class Solution: def isValid(self, s: str) -\u0026gt; bool: closing_to_opening = { \u0026#34;)\u0026#34;: \u0026#34;(\u0026#34;, \u0026#34;]\u0026#34;: \u0026#34;[\u0026#34;, \u0026#34;}\u0026#34;: \u0026#34;{\u0026#34;, } stack = [] for char in s: if char in \u0026#34;([{\u0026#34;: stack.append(char) continue if not stack or stack[-1] != closing_to_opening[char]: return False stack.pop() return not stack solution = Solution() # Official examples. assert solution.isValid(\u0026#34;()\u0026#34;) is True assert solution.isValid(\u0026#34;()[]{}\u0026#34;) is True assert solution.isValid(\u0026#34;(]\u0026#34;) is False assert solution.isValid(\u0026#34;([])\u0026#34;) is True assert solution.isValid(\u0026#34;([)]\u0026#34;) is False # Deep nesting, a missing opening bracket, and an opening bracket left over. assert solution.isValid(\u0026#34;{[()]}\u0026#34;) is True assert solution.isValid(\u0026#34;(\u0026#34;) is False assert solution.isValid(\u0026#34;]\u0026#34;) is False assert solution.isValid(\u0026#34;(()\u0026#34;) is False # Compare 2,000 fixed-seed short strings with the correct Step 2 baseline. rng = random.Random(20) brackets = \u0026#34;()[]{}\u0026#34; for _ in range(2_000): length = rng.randint(1, 12) candidate = \u0026#34;\u0026#34;.join(rng.choice(brackets) for _ in range(length)) expected = is_valid_by_elimination(candidate) actual = solution.isValid(candidate) assert actual is expected Why Checking Only the Stack Top Is Enough During the scan, the stack always contains the opening brackets that have been read but not closed, in their original order.\nWhen the current character is a closing bracket, correct order requires it to close the most recent unclosed opening bracket, which is the top of the stack:\nIf the types match, popping the top leaves exactly the unclosed outer brackets. If the types differ, the closing bracket crosses the top opening bracket and no later character can repair the order. If the stack is empty, the closing bracket has no corresponding opening bracket. A non-empty stack after the scan means that some opening brackets were never closed. Therefore, return not stack covers the final invalid case.\nComplexity Each character is read once. Every opening bracket is pushed at most once and popped at most once, so the time complexity is O(n). If every character is an opening bracket, the stack stores all n characters, giving O(n) extra space.\nThe final version now:\nchecks bracket type and closing order in one left-to-right pass fails immediately on an unmatched closing bracket or type conflict rejects opening brackets left over after the scan runs in O(n) time and passes fixed examples plus randomized differential tests ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/20-valid-parentheses/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given a string \u003ccode\u003es\u003c/code\u003e containing only these six characters:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e( ) { } [ ]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eDetermine whether the string is valid. A valid string must satisfy all three conditions:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eEvery opening bracket is closed by the same type of closing bracket.\u003c/li\u003e\n\u003cli\u003eOpening brackets are closed in the correct order.\u003c/li\u003e\n\u003cli\u003eEvery closing bracket has a corresponding opening bracket of the same type.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eReturn \u003ccode\u003eTrue\u003c/code\u003e when all conditions hold; otherwise, return \u003ccode\u003eFalse\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 20: Valid Parentheses, Why Equal Counts Are Not Enough"},{"content":"Problem Requirement Given an encoded string s, return its decoded string.\nThe encoding rule is:\nk[encoded_string] The encoded_string inside the brackets is repeated exactly k times, where k is a positive integer. Encodings may be nested or adjacent to ordinary lowercase letters.\nThe problem guarantees that:\nThe input is always valid, with matching brackets and no extra spaces. Original text contains no digits; digits only represent repeat counts. Inputs such as 3a or 2[4] do not occur. The decoded string length does not exceed 10^5. Examples Input Output \u0026quot;3[a]2[bc]\u0026quot; \u0026quot;aaabcbc\u0026quot; \u0026quot;3[a2[c]]\u0026quot; \u0026quot;accaccacc\u0026quot; \u0026quot;2[abc]3[cd]ef\u0026quot; \u0026quot;abcabccdcdcdef\u0026quot; Constraints 1 \u0026lt;= s.length \u0026lt;= 30 s contains only lowercase English letters, digits, and [] Every repeat count is in [1, 300] LeetCode provides this method signature:\nclass Solution: def decodeString(self, s: str) -\u0026gt; str: pass Step 1: Where Does the Outer Information Go When We Enter a Nested Layer? Start with an input that has no nesting:\n3[a] After reading 3, we know that the next segment must be repeated three times. After reading a inside the brackets, the result is:\na * 3 = aaa The current baseline is:\nRead one repeat count, collect the text in the following brackets, and repeat it when ] appears. This baseline handles one layer such as 3[a], but it breaks on 3[a2[c]]. The outer layer has already read count 3 and ordinary letter a when the nested encoding 2[c] begins. If the current count is overwritten with 2 and the current text with c, the outer 3 and a are lost.\nExpand the input one layer at a time:\nStage Layer being processed What this layer knows Information that must be retained Read outer 3[ Outer Repeat 3 times None Read outer a Outer Text prefix is a None Read inner 2[ Inner Prepare to repeat 2 times Outer text a, outer count 3 Read inner c] Inner complete c * 2 = cc Restore outer text a, outer count 3 Return to outer Outer a + cc = acc Outer count is still 3 Read outer ] Outer complete acc * 3 None The result is:\n3[a2[c]] -\u0026gt; 3[a + cc] -\u0026gt; 3[acc] -\u0026gt; accaccacc Before entering an inner layer, we therefore cannot overwrite unfinished outer information. At least two pieces of context must be retained:\nThe text already decoded in the outer layer. The number of times the completed outer layer must eventually repeat. Adjacent but non-nested input such as 3[a]2[bc] does not keep two layers at once. It first completes aaa, independently decodes 2[bc] as bcbc, and concatenates them into aaabcbc. The need to save context appears only when a new encoding starts before the current encoding is complete.\nThis checkpoint can now:\ndistinguish single-layer, adjacent, and nested encodings explain layer by layer why 3[a2[c]] becomes accaccacc identify the outer text and repeat count that must survive entry into a nested layer It still lacks:\nan executable rule for entering an inner layer, returning to its parent, and continuing the scan runtime checks for multi-digit counts, ordinary suffixes, and deeper nesting Step 2: Let Each Recursive Call Finish One Layer The current baseline knows that outer context must survive entry into an inner layer, but it has not defined when the inner layer finishes or where the outer layer resumes.\nReduce the problem to one bracket layer: read from a given position until the ] that belongs to this layer. This process must return two results:\nthe decoded text of the current layer the next position after its closing bracket When an outer layer sees [, it asks the next layer to start immediately after that bracket. When the child reaches its own ], it returns. The caller repeats the child text by the current count and resumes at the returned position.\nTrace the positions in 3[a2[c]]:\nPosition Character Current-layer operation Next position or return value 0 3 Top level accumulates count 3 1 1 [ Enter outer encoding at position 2 Wait for return 2 a Append a to outer text 3 3 2 Outer layer accumulates inner count 2 4 4 [ Enter inner encoding at position 5 Wait for return 5 c Append c to inner text 6 6 ] Inner layer returns (\u0026quot;c\u0026quot;, 7) Outer gets c * 2 7 ] Outer layer returns (\u0026quot;acc\u0026quot;, 8) Top level gets acc * 3 8 End of string Top level returns \u0026quot;accaccacc\u0026quot; Complete Turn this layer contract into a recursive baseline:\nclass RecursiveDecoder: def decodeString(self, s: str) -\u0026gt; str: decoded, _ = self._decode_layer(s, 0) return decoded def _decode_layer(self, s: str, index: int) -\u0026gt; tuple[str, int]: parts = [] repeat = 0 while index \u0026lt; len(s): char = s[index] if char.isdigit(): repeat = repeat * 10 + int(char) index += 1 elif char == \u0026#34;[\u0026#34;: nested, index = self._decode_layer(s, index + 1) parts.append(nested * repeat) repeat = 0 elif char == \u0026#34;]\u0026#34;: return \u0026#34;\u0026#34;.join(parts), index + 1 else: parts.append(char) index += 1 return \u0026#34;\u0026#34;.join(parts), index decoder = RecursiveDecoder() # Official examples. assert decoder.decodeString(\u0026#34;3[a]2[bc]\u0026#34;) == \u0026#34;aaabcbc\u0026#34; assert decoder.decodeString(\u0026#34;3[a2[c]]\u0026#34;) == \u0026#34;accaccacc\u0026#34; assert decoder.decodeString(\u0026#34;2[abc]3[cd]ef\u0026#34;) == \u0026#34;abcabccdcdcdef\u0026#34; # A multi-digit count, ordinary text, and deeper nesting. assert decoder.decodeString(\u0026#34;12[a]\u0026#34;) == \u0026#34;a\u0026#34; * 12 assert decoder.decodeString(\u0026#34;abc3[cd]xyz\u0026#34;) == \u0026#34;abccdcdcdxyz\u0026#34; assert decoder.decodeString(\u0026#34;2[a2[b2[c]]]\u0026#34;) == \u0026#34;abccbccabccbcc\u0026#34; The variable repeat belongs only to the current layer. Consecutive digits form one count through repeat * 10 + int(char). When the current layer enters a child, that recursive call has its own parts and repeat; the caller\u0026rsquo;s local state still exists when the child returns.\nAt ], the parser must return index + 1, because the caller should next read the character after the closing bracket rather than process the same ] again. The top level has no closing bracket, so it returns the final result when index == len(s).\nCorrectness and Complexity Each call consumes only its own ordinary characters, counts, and direct child layers. A child returns fully decoded text, so its parent only needs to repeat that text and append it. The input guarantees valid brackets, so every child returns at its matching ], and the top-level call eventually reaches the end of the string.\nLet n be the encoded input length, L the final output length, and d the maximum nesting depth. Each input character causes one branch decision, but repetition and concatenation must construct actual output. More precisely, the time complexity is O(n + W), where W is the total number of string characters constructed across all layers during the run. Its worst-case bound is O(n + dL).\nSpace cannot be measured by cumulative construction cost W, because earlier temporary strings may already have been released. Recursive frames use O(d) space, while per-layer lists, references, and indices use O(n + d). Let P be the number of decoded and temporary string characters that remain live at one time. Peak space is O(n + d + P). Since each layer\u0026rsquo;s live text is no longer than the final output, P is at most O(dL), giving a safe O(n + dL) bound including the returned result.\nThis checkpoint can now:\ncorrectly decode single-layer, adjacent, and nested encodings accumulate multi-digit repeat counts return the position where the caller should continue after a child layer pass the official examples, ordinary text, and deeper nesting checks It still lacks:\na visible representation of the outer text and count saved at each nested entry explicit state in place of context stored automatically by recursive calls Step 3: Move the Context Saved by Recursion Into an Explicit Stack The recursive baseline is correct, but the language call stack saves outer state for us. Each call frame retains the text decoded so far, the repeat count, and the continuation position until its child returns.\nNow scan the string once from left to right. The loop itself owns the continuation position, so at [ we only need to save:\n(outer text decoded so far, current repeat count) An inner layer must finish before its parent can resume, which is last-in, first-out order. Store unfinished outer contexts in contexts:\nOn a digit, accumulate repeat_count one decimal digit at a time. On [, push the current text and count, then reset the current-layer state. On ], pop the most recent outer context, repeat the current-layer text, and append it to the outer text. On a letter, append it to the current-layer text. Python strings are immutable, so repeated character-by-character concatenation can copy the existing text. Instead, current_parts stores the text fragments of the current layer and joins them only when a layer is completed. It has the same meaning as recursive parts, but the loop now saves and restores it directly.\nTrace the stack for 3[a2[c]]:\nCharacter repeat_count contexts (bottom to top) Current-layer text Operation 3 3 [] \u0026quot;\u0026quot; Accumulate outer count [ 0 [(\u0026quot;\u0026quot;, 3)] \u0026quot;\u0026quot; Save outer context and reset a 0 [(\u0026quot;\u0026quot;, 3)] \u0026quot;a\u0026quot; Append ordinary character 2 2 [(\u0026quot;\u0026quot;, 3)] \u0026quot;a\u0026quot; Accumulate inner count [ 0 [(\u0026quot;\u0026quot;, 3), (\u0026quot;a\u0026quot;, 2)] \u0026quot;\u0026quot; Save current outer context and reset c 0 [(\u0026quot;\u0026quot;, 3), (\u0026quot;a\u0026quot;, 2)] \u0026quot;c\u0026quot; Append ordinary character ] 0 [(\u0026quot;\u0026quot;, 3)] \u0026quot;acc\u0026quot; Restore a, then append c * 2 ] 0 [] \u0026quot;accaccacc\u0026quot; Restore empty prefix, then append acc * 3 Maintain three conditions throughout the scan:\ncurrent_parts stores the decoded text of the unfinished current layer in order. repeat_count stores the integer formed by the most recent consecutive digits and resets immediately after [. contexts, from bottom to top, stores unfinished contexts from outermost to innermost; its top is always the layer restored by the current ]. Implement the four character branches:\nclass Solution: def decodeString(self, s: str) -\u0026gt; str: contexts: list[tuple[list[str], int]] = [] current_parts: list[str] = [] repeat_count = 0 for char in s: if char.isdigit(): repeat_count = repeat_count * 10 + int(char) elif char == \u0026#34;[\u0026#34;: contexts.append((current_parts, repeat_count)) current_parts = [] repeat_count = 0 elif char == \u0026#34;]\u0026#34;: nested_text = \u0026#34;\u0026#34;.join(current_parts) outer_parts, count = contexts.pop() outer_parts.append(nested_text * count) current_parts = outer_parts else: current_parts.append(char) return \u0026#34;\u0026#34;.join(current_parts) First check fixed boundaries:\nsolution = Solution() # Official examples. assert solution.decodeString(\u0026#34;3[a]2[bc]\u0026#34;) == \u0026#34;aaabcbc\u0026#34; assert solution.decodeString(\u0026#34;3[a2[c]]\u0026#34;) == \u0026#34;accaccacc\u0026#34; assert solution.decodeString(\u0026#34;2[abc]3[cd]ef\u0026#34;) == \u0026#34;abcabccdcdcdef\u0026#34; # A multi-digit count, ordinary text, and deeper nesting. assert solution.decodeString(\u0026#34;12[a]\u0026#34;) == \u0026#34;a\u0026#34; * 12 assert solution.decodeString(\u0026#34;abc3[cd]xyz\u0026#34;) == \u0026#34;abccdcdcdxyz\u0026#34; assert solution.decodeString(\u0026#34;2[a2[b2[c]]]\u0026#34;) == \u0026#34;abccbccabccbcc\u0026#34; Next, generate both a valid encoding and its expected result, then compare the explicit-stack solution with the recursive baseline from Step 2. Run this block after the previous two complete code blocks:\nimport random random_generator = random.Random(394) def generate_valid_case(depth: int = 0) -\u0026gt; tuple[str, str]: encoded_parts = [] decoded_parts = [] for _ in range(random_generator.randint(1, 3)): if depth \u0026lt; 3 and random_generator.random() \u0026lt; 0.5: nested_encoded, nested_decoded = generate_valid_case(depth + 1) repeat = random_generator.randint(1, 12) encoded_parts.append(f\u0026#34;{repeat}[{nested_encoded}]\u0026#34;) decoded_parts.append(nested_decoded * repeat) else: text = \u0026#34;\u0026#34;.join( random_generator.choice(\u0026#34;abc\u0026#34;) for _ in range(random_generator.randint(1, 3)) ) encoded_parts.append(text) decoded_parts.append(text) return \u0026#34;\u0026#34;.join(encoded_parts), \u0026#34;\u0026#34;.join(decoded_parts) baseline = RecursiveDecoder() checked = 0 while checked \u0026lt; 2_000: encoded, expected = generate_valid_case() if len(encoded) \u0026gt; 30 or len(expected) \u0026gt; 100_000: continue assert baseline.decodeString(encoded) == expected assert solution.decodeString(encoded) == expected checked += 1 The random check does not ask two implementations to prove each other correct. The generator directly constructs expected, which provides an independent expected value. Both the recursive and explicit-stack implementations must match it; comparing the two also helps expose differences in layer entry or restoration.\nCorrectness and Complexity At [, the algorithm saves the complete unfinished outer state. At the matching ], last-in, first-out order restores the nearest outer layer. The current layer is already fully decoded, so nested_text * count is exactly this encoded segment\u0026rsquo;s result. Appending it to the outer layer re-establishes all three conditions. Because the input brackets are valid, every saved context has been restored when the scan ends, and current_parts represents the entire decoded string.\nLet n be the input length, L the final output length, and d the maximum nesting depth. The scan itself is O(n), but joining and repeating must construct decoded text. If W is the total number of characters constructed across all layers during the run, time is O(n + W) and at worst O(n + dL). The stack depth is O(d), active fragment lists and references use O(n), and live decoded text plus temporary copies use O(L). Peak space including the returned result is therefore O(n + d + L).\nThe final version now:\nsaves and restores outer context at any nesting depth with an explicit stack handles adjacent encodings, multi-digit counts, ordinary prefixes and suffixes, and deep nesting passes 6 fixed assertions and 2,000 valid randomized encodings completes the basic stack loop across LeetCode 20, 155, and 394: match the nearest state, synchronize historical state, and preserve nested context ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/394-decode-string/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eGiven an encoded string \u003ccode\u003es\u003c/code\u003e, return its decoded string.\u003c/p\u003e\n\u003cp\u003eThe encoding rule is:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ek[encoded_string]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe \u003ccode\u003eencoded_string\u003c/code\u003e inside the brackets is repeated exactly \u003ccode\u003ek\u003c/code\u003e times, where \u003ccode\u003ek\u003c/code\u003e is a positive integer. Encodings may be nested or adjacent to ordinary lowercase letters.\u003c/p\u003e\n\u003cp\u003eThe problem guarantees that:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eThe input is always valid, with matching brackets and no extra spaces.\u003c/li\u003e\n\u003cli\u003eOriginal text contains no digits; digits only represent repeat counts.\u003c/li\u003e\n\u003cli\u003eInputs such as \u003ccode\u003e3a\u003c/code\u003e or \u003ccode\u003e2[4]\u003c/code\u003e do not occur.\u003c/li\u003e\n\u003cli\u003eThe decoded string length does not exceed \u003ccode\u003e10^5\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"examples\"\u003eExamples\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eInput\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eOutput\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e\u0026quot;3[a]2[bc]\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e\u0026quot;aaabcbc\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e\u0026quot;3[a2[c]]\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e\u0026quot;accaccacc\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e\u0026quot;2[abc]3[cd]ef\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e\u0026quot;abcabccdcdcdef\u0026quot;\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"constraints\"\u003eConstraints\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e1 \u0026lt;= s.length \u0026lt;= 30\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003es\u003c/code\u003e contains only lowercase English letters, digits, and \u003ccode\u003e[]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eEvery repeat count is in \u003ccode\u003e[1, 300]\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeetCode provides this method signature:\u003c/p\u003e","title":"LeetCode 394: Decode String by Saving and Restoring Nested Context"},{"content":"You are given an m x n integer matrix matrix and an integer target. Return True if target is in the matrix; otherwise, return False.\nThe matrix satisfies two conditions:\nEvery row is sorted in non-decreasing order. The first integer of each row is strictly greater than the last integer of the previous row. The required time complexity is O(log(m * n)).\nFor example, consider this matrix:\n1 3 5 7 10 11 16 20 23 30 34 60 When target = 3, return True. When target = 13, return False. The constraints are:\n1 \u0026lt;= m, n \u0026lt;= 100 -10^4 \u0026lt;= matrix[i][j], target \u0026lt;= 10^4 LeetCode provides this method signature:\nclass Solution: def searchMatrix(self, matrix: List[List[int]], target: int) -\u0026gt; bool: pass Step 1: Scan Every Element At this point, we only have the problem constraints and the method signature. We do not yet have code that can decide whether the target exists. Start with the simplest question: how can we produce a directly runnable version whose result is clearly correct?\nThe current baseline cannot answer either example. Add one loop over the rows and another over the values in each row. Return True as soon as a value equals the target. Return False only after every element has been checked without a match.\nfrom typing import List class Solution: def searchMatrix(self, matrix: List[List[int]], target: int) -\u0026gt; bool: for row in matrix: for value in row: if value == target: return True return False solution = Solution() matrix = [ [1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60], ] # Official examples: a normal hit and a missing target. assert solution.searchMatrix(matrix, 3) is True assert solution.searchMatrix(matrix, 13) is False # A single element: hit and miss. assert solution.searchMatrix([[1]], 1) is True assert solution.searchMatrix([[1]], 0) is False # The first and last elements. assert solution.searchMatrix(matrix, 1) is True assert solution.searchMatrix(matrix, 60) is True # A single row. assert solution.searchMatrix([[1, 3, 5, 7]], 5) is True assert solution.searchMatrix([[1, 3, 5, 7]], 6) is False # A single column. assert solution.searchMatrix([[1], [3], [5]], 3) is True assert solution.searchMatrix([[1], [3], [5]], 4) is False This version checks at most m * n elements, so its time complexity is O(mn). The loops store only the current row and value, so the extra space complexity is O(1).\nAt this checkpoint, we can correctly determine whether the target exists by scanning every element. However, the worst case still examines the entire matrix, so it does not yet satisfy the required O(log(m * n)) time complexity.\nStep 2: Join All Rows into One Sorted Array The element-by-element scan is correct, but it does not use either ordering condition. Consider a smaller matrix:\n1 3 5 7 9 11 Within each row, the elements are sorted in non-decreasing order. Across the row boundary, the first element of the second row, 7, is strictly greater than the last element of the first row, 5. Therefore, reading every row from left to right and from top to bottom produces:\n[1, 3, 5, 7, 9, 11] This array is still sorted. The same relationship holds between every pair of adjacent rows: the within-row order prevents inversions inside a row, while the strict boundary condition prevents an inversion when moving to the next row. Therefore, the entire matrix is globally sorted when read in row-major order.\nThe nested loops in the previous version hide this continuous order behind two-dimensional row boundaries. Modify that version by first writing the elements into a real flat array in row-major order, then scanning flat linearly:\nfrom typing import List class Solution: def searchMatrix(self, matrix: List[List[int]], target: int) -\u0026gt; bool: flat = [value for row in matrix for value in row] for value in flat: if value == target: return True return False # Flattening this small matrix must preserve the exact row-major order. tiny_matrix = [ [1, 3, 5], [7, 9, 11], ] tiny_flat = [value for row in tiny_matrix for value in row] assert tiny_flat == [1, 3, 5, 7, 9, 11] solution = Solution() matrix = [ [1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60], ] # The flattened linear scan preserves the same hit and miss behavior. assert solution.searchMatrix(matrix, 3) is True assert solution.searchMatrix(matrix, 13) is False Building flat copies m * n elements, and the subsequent scan may still inspect all m * n values. Therefore, the complete method has O(mn) time complexity and O(mn) extra space complexity.\nAt this checkpoint, we have made the globally sorted row-major sequence explicit and used it for searching. However, the search still checks values one at a time, and flat stores every element in the matrix.\nStep 3: Binary Search the Real One-Dimensional Array The current baseline has a globally sorted flat array, but it still compares values from beginning to end. If the target is missing, this linear search may inspect all m * n values.\nThe concrete problem is now clear: the array is sorted, but no rule uses that order to discard half of the candidate indices. Replace only the search over flat. Let left and right represent the closed interval [left, right] that may still contain the target, and inspect the middle value at mid.\nIf flat[mid] == target, return True. If flat[mid] \u0026lt; target, neither mid nor any index to its left can contain the target, so set left = mid + 1. If flat[mid] \u0026gt; target, neither mid nor any index to its right can contain the target, so set right = mid - 1. Both updates remove the inspected mid, so the closed interval strictly shrinks on every iteration. When left \u0026gt; right, the interval is empty and the search returns False.\nSearching for the missing value 13 in the example matrix proceeds as follows:\nRound left right mid flat[mid] Update 1 0 11 5 11 11 \u0026lt; 13, set left = 6 2 6 11 8 23 23 \u0026gt; 13, set right = 7 3 6 7 6 16 16 \u0026gt; 13, set right = 5 After the third round, left = 6 and right = 5. The candidate interval is empty, so 13 cannot be in the array.\nfrom typing import List class Solution: def searchMatrix(self, matrix: List[List[int]], target: int) -\u0026gt; bool: flat = [value for row in matrix for value in row] left = 0 right = len(flat) - 1 while left \u0026lt;= right: mid = (left + right) // 2 value = flat[mid] if value == target: return True if value \u0026lt; target: left = mid + 1 else: right = mid - 1 return False # The real flattened array still matches the row-major sequence. tiny_matrix = [ [1, 3, 5], [7, 9, 11], ] tiny_flat = [value for row in tiny_matrix for value in row] assert tiny_flat == [1, 3, 5, 7, 9, 11] solution = Solution() matrix = [ [1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60], ] # Official examples: a normal hit and a missing target. assert solution.searchMatrix(matrix, 3) is True assert solution.searchMatrix(matrix, 13) is False # A single element: hit and miss. assert solution.searchMatrix([[1]], 1) is True assert solution.searchMatrix([[1]], 0) is False # The first and last elements. assert solution.searchMatrix(matrix, 1) is True assert solution.searchMatrix(matrix, 60) is True # A single row. assert solution.searchMatrix([[1, 3, 5, 7]], 5) is True assert solution.searchMatrix([[1, 3, 5, 7]], 6) is False # A single column. assert solution.searchMatrix([[1], [3], [5]], 3) is True assert solution.searchMatrix([[1], [3], [5]], 4) is False # Targets below the minimum and above the maximum. assert solution.searchMatrix(matrix, 0) is False assert solution.searchMatrix(matrix, 61) is False The binary-search phase discards at least half of the remaining indices each round, so it takes O(log(mn)) time. However, the complete method still has to copy m * n elements into flat. Its total time complexity therefore remains O(mn), and its extra space complexity is also O(mn).\nAt this checkpoint, we can use standard closed-interval binary search on a real sorted array. However, constructing flat still requires copying and storing every element in the matrix.\nStep 4: Access flat[mid] Without Creating flat The current baseline already has a complete and correct binary search. Looking at its loop, each iteration reads only one value: flat[mid]. Yet the method first copies all m * n elements just to access those individual values.\nThat copy is exactly why the complete method still needs O(mn) time and extra space. We do not need another search algorithm. We need a direct way to answer this question: given an index mid in the row-major sequence, where is the same element in the original matrix?\nSuppose each row contains cols elements. Every complete group of cols elements before mid accounts for one entire row. Therefore:\nrow = mid // cols col = mid % cols Integer division counts how many complete rows come before the element, and the remainder gives its column inside the current row. For the three-row, four-column example matrix, the boundary indices map as follows:\nVirtual index row = mid // cols col = mid % cols Matrix position 0 0 0 first element of the first row cols - 1 = 3 0 3 last element of the first row cols = 4 1 0 first element of the second row rows * cols - 1 = 11 2 3 last element of the last row There is no need to create flat. Starting from the previous version, replace the right boundary len(flat) - 1 with rows * cols - 1, and replace flat[mid] with matrix[row][col]. The rest of the binary search remains unchanged:\nimport random from typing import List class Solution: def searchMatrix(self, matrix: List[List[int]], target: int) -\u0026gt; bool: rows = len(matrix) cols = len(matrix[0]) left = 0 right = rows * cols - 1 while left \u0026lt;= right: mid = (left + right) // 2 row = mid // cols col = mid % cols value = matrix[row][col] if value == target: return True if value \u0026lt; target: left = mid + 1 else: right = mid - 1 return False solution = Solution() matrix = [ [1, 3, 5, 7], [10, 11, 16, 20], [23, 30, 34, 60], ] # Official examples: a normal hit and a missing target. assert solution.searchMatrix(matrix, 3) is True assert solution.searchMatrix(matrix, 13) is False # A single element: hit and miss. assert solution.searchMatrix([[1]], 1) is True assert solution.searchMatrix([[1]], 0) is False # The first and last elements, plus targets outside the value range. assert solution.searchMatrix(matrix, 1) is True assert solution.searchMatrix(matrix, 60) is True assert solution.searchMatrix(matrix, 0) is False assert solution.searchMatrix(matrix, 61) is False # A single row or column. assert solution.searchMatrix([[1, 3, 5, 7]], 5) is True assert solution.searchMatrix([[1, 3, 5, 7]], 6) is False assert solution.searchMatrix([[1], [3], [5]], 3) is True assert solution.searchMatrix([[1], [3], [5]], 4) is False # Rows may contain duplicates, but the next row must still begin above # the previous row\u0026#39;s final value. matrix_with_duplicates = [ [1, 1, 3], [5, 5, 8], ] assert solution.searchMatrix(matrix_with_duplicates, 1) is True assert solution.searchMatrix(matrix_with_duplicates, 5) is True assert solution.searchMatrix(matrix_with_duplicates, 4) is False # Searching must not modify the input matrix. snapshot = [row[:] for row in matrix] solution.searchMatrix(matrix, 16) assert matrix == snapshot # Generate 1,200 valid matrices with a fixed seed and compare the result # with an element-by-element scan. rng = random.Random(74) for _ in range(1_200): test_rows = rng.randint(1, 8) test_cols = rng.randint(1, 8) next_value = rng.randint(-100, 100) test_matrix = [] for _ in range(test_rows): test_row = [next_value] for _ in range(1, test_cols): test_row.append(test_row[-1] + rng.randint(0, 3)) test_matrix.append(test_row) next_value = test_row[-1] + rng.randint(1, 3) test_target = rng.randint(test_matrix[0][0] - 2, test_matrix[-1][-1] + 2) expected = any( value == test_target for test_row in test_matrix for value in test_row ) actual = solution.searchMatrix(test_matrix, test_target) assert actual is expected Why the Mapped Binary Search Is Still Correct Step 2 established the key fact: reading the matrix in row-major order produces a globally sorted sequence. We no longer store that sequence, but each virtual index still maps to exactly one matrix coordinate. For every 0 \u0026lt;= mid \u0026lt; rows * cols:\nmid // cols is between 0 and rows - 1. mid % cols is between 0 and cols - 1. Converting the coordinate back to a one-dimensional index gives (mid // cols) * cols + mid % cols == mid. Therefore, matrix[mid // cols][mid % cols] reads exactly the element that flat[mid] would contain, without going out of bounds.\nThe binary-search loop maintains the same invariant: if the target exists, its virtual index is inside the closed interval [left, right]. When the middle value is smaller than the target, global order lets us discard mid and every index to its left. When the middle value is greater, we can discard mid and every index to its right. Each iteration removes mid and strictly shrinks the interval. Equality returns True; an empty interval means every candidate index has been eliminated, so the method returns False.\nComplexity The virtual sequence contains m * n indices. Binary search reduces the candidate range by at least half on every iteration, so the time complexity is O(log(mn)). The algorithm stores only the matrix dimensions, three indices, and the current value. It never creates flat, so the extra space complexity is O(1).\nAt this final checkpoint, we have transferred standard binary search from a real one-dimensional array directly to the matrix. Quotient and remainder map each virtual index to its matrix element, so the solution avoids copying the input while meeting the required O(log(mn)) time and O(1) extra space bounds.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/binary-search/74-search-a-2d-matrix/","summary":"\u003cp\u003eYou are given an \u003ccode\u003em x n\u003c/code\u003e integer matrix \u003ccode\u003ematrix\u003c/code\u003e and an integer \u003ccode\u003etarget\u003c/code\u003e. Return \u003ccode\u003eTrue\u003c/code\u003e if \u003ccode\u003etarget\u003c/code\u003e is in the matrix; otherwise, return \u003ccode\u003eFalse\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eThe matrix satisfies two conditions:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eEvery row is sorted in non-decreasing order.\u003c/li\u003e\n\u003cli\u003eThe first integer of each row is strictly greater than the last integer of the previous row.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe required time complexity is \u003ccode\u003eO(log(m * n))\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eFor example, consider this matrix:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e1   3   5   7\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e10  11  16  20\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e23  30  34  60\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003eWhen \u003ccode\u003etarget = 3\u003c/code\u003e, return \u003ccode\u003eTrue\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eWhen \u003ccode\u003etarget = 13\u003c/code\u003e, return \u003ccode\u003eFalse\u003c/code\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe constraints are:\u003c/p\u003e","title":"LeetCode 74: Search a 2D Matrix"},{"content":"Problem Requirement The input is a non-empty integer array nums. Every element is unique, and before rotation the array was sorted in strictly increasing order.\nThe array is rotated between 1 and nums.length times. One rotation moves the last element to the front; therefore, nums.length rotations restore the original increasing order. Return the minimum value in the rotated array. The problem requires an algorithm with O(log n) runtime.\nLeetCode uses this method contract:\nclass Solution: def findMin(self, nums: List[int]) -\u0026gt; int: Official Examples Input: nums = [3,4,5,1,2] Output: 1 Input: nums = [4,5,6,7,0,1,2] Output: 0 Input: nums = [11,13,15,17] Output: 11 Constraints 1 \u0026lt;= nums.length \u0026lt;= 5000 -5000 \u0026lt;= nums[i] \u0026lt;= 5000 All integers in nums are unique. Before rotation, nums is sorted in strictly increasing order. nums is rotated between 1 and nums.length times. Step 1: First Get a Definitely Correct Minimum For [3,4,5,1,2], why can we not simply return the first element?\nPressure The first element is 3, but the minimum is 1, which moved into the middle after rotation. Rotation preserves every value but does not guarantee that the minimum remains at index 0, so returning nums[0] gives the wrong answer.\nPrevious Baseline The current baseline contains only the non-empty input, strict pre-rotation order, unique elements, required minimum, official examples, constraints, and the Solution.findMin method contract. It has no executable rule for finding the minimum.\nBreak The method contract says what to return, but it provides no executable process that handles every valid rotation, including the original order restored after exactly nums.length rotations.\nChange Add one linear scan to the previous baseline. Because the problem guarantees that nums is non-empty, find_min_scan can initialize the running minimum from nums[0] and then visit every remaining value. When it sees a smaller value, it updates minimum; after the scan, it returns that value.\nfrom typing import List def find_min_scan(nums: List[int]) -\u0026gt; int: minimum = nums[0] for index in range(1, len(nums)): if nums[index] \u0026lt; minimum: minimum = nums[index] return minimum assert find_min_scan([3, 4, 5, 1, 2]) == 1 assert find_min_scan([4, 5, 6, 7, 0, 1, 2]) == 0 assert find_min_scan([11, 13, 15, 17]) == 11 assert find_min_scan([7]) == 7 assert find_min_scan([4, 1, 2, 3]) == 1 assert find_min_scan([1, 2, 3, 4]) == 1 Check The first three assertions cover all official examples. The final three check a singleton, one rotation that moves the last element to the front, and the original increasing order produced after nums.length rotations. Running the entire block without an assertion failure shows that the scan does not depend on where the minimum appears after rotation.\nCorrectness After initialization, minimum is the minimum of the first element. For each remaining element, the scan replaces minimum when the new value is smaller and otherwise keeps it. After every iteration, minimum is therefore the smallest value visited so far. Once every element has been visited, it is the minimum of the whole array.\nComplexity For an array of length n, this version visits every element once, so its time complexity is O(n). It stores only minimum and the loop index, so its auxiliary space complexity is O(1).\nStep 1 Result This version now returns the correct minimum for every valid non-empty input, including a singleton, one rotation, and exactly nums.length rotations.\nIt still lacks the required runtime: the O(n) linear scan does not satisfy the problem\u0026rsquo;s O(log n) requirement.\nStep 2: Which Side Can Still Contain the Minimum? If we stop inspecting every value, one decision must discard some positions while proving that the minimum remains among the positions we keep. How can we do both?\nPressure The linear scan takes O(n) time because it never rules out an unvisited value. To beat the scan, every step must discard some values. Shrinking alone is not enough, however: the smaller range must still contain the true minimum, or every later decision loses its correctness foundation.\nPrevious Baseline The previous version uses find_min_scan to visit every element and maintains the running minimum for correctness. It records neither which positions can still be the minimum nor any proved rule for discarding positions.\nBreak A tempting approach is to compare nums[mid] with the first array element, but that comparison alone does not give a stable update rule for the current range. For both [4,5,6,7,0,1,2] and [11,13,15,17], the initial midpoint is greater than the first element, yet the former has its minimum to the midpoint\u0026rsquo;s right while the latter has its minimum to the midpoint\u0026rsquo;s left. Once the range\u0026rsquo;s left endpoint moves, the original first element may also be outside the current range.\nWe therefore need a comparison reference that always belongs to the current range and a direct proof that each update keeps the minimum.\nChange: Maintain a Closed Minimum-Containing Interval Use the closed interval [left, right] for the indices that can still contain the minimum. Both endpoints are included. Its meaning is:\nThe true minimum\u0026rsquo;s index is guaranteed to be inside [left, right].\nThe complete array gives the initial range [0, len(nums) - 1]. A one-step shrink applies only to a range with at least two positions, so left \u0026lt; right. Its midpoint is:\nmid = left + (right - left) // 2 This gives mid \u0026lt; right. The problem guarantees that all values are unique, so nums[mid] and nums[right] cannot be equal. Only the following two cases remain.\nnums[mid] \u0026gt; nums[right] If the values from mid through right still followed ordinary increasing order, then nums[mid] \u0026lt; nums[right] would hold. The opposite relation shows that the drop created by rotation lies in this part of the range, and the array\u0026rsquo;s minimum comes after that drop. The minimum must therefore be in [mid + 1, right]; neither mid nor any current position to its left can be the minimum. We can execute:\nleft = mid + 1 nums[mid] \u0026lt; nums[right] Now the values from mid through right do not cross the drop created by rotation, so the minimum is not in [mid + 1, right]. It remains in [left, mid], and mid itself may be the minimum. For example, in the current range [3,4] of [3,4,5,1,2], mid = 3 and its value 1 is the minimum. We must keep mid and execute:\nright = mid The helper below performs exactly one comparison and one range update. It returns the updated closed interval; it contains neither a loop nor a rule for returning the final answer.\nfrom typing import List, Tuple def shrink_minimum_interval_once( nums: List[int], left: int, right: int ) -\u0026gt; Tuple[int, int]: mid = left + (right - left) // 2 if nums[mid] \u0026gt; nums[right]: left = mid + 1 else: right = mid return left, right first = [4, 5, 6, 7, 0, 1, 2] first_interval = shrink_minimum_interval_once(first, 0, 6) assert first_interval == (4, 6) assert min(first) in first[first_interval[0] : first_interval[1] + 1] second = [3, 4, 5, 1, 2] second_interval = shrink_minimum_interval_once(second, 0, 4) assert second_interval == (3, 4) assert min(second) in second[second_interval[0] : second_interval[1] + 1] second_interval = shrink_minimum_interval_once( second, second_interval[0], second_interval[1] ) assert second_interval == (3, 3) assert min(second) in second[second_interval[0] : second_interval[1] + 1] third = [11, 13, 15, 17] third_interval = shrink_minimum_interval_once(third, 0, 3) assert third_interval == (0, 1) assert min(third) in third[third_interval[0] : third_interval[1] + 1] Check Every call in the code matches one manual trace below. The final column directly checks that the next closed interval still contains the true minimum.\nArray Current range mid Comparison Next range Evidence that the minimum remains [4,5,6,7,0,1,2] [0,6] 3 7 \u0026gt; 2 [4,6] [0,1,2] contains 0 [3,4,5,1,2] [0,4] 2 5 \u0026gt; 2 [3,4] [1,2] contains 1 [3,4,5,1,2] [3,4] 3 1 \u0026lt; 2 [3,3] Keeps minimum 1 at mid [11,13,15,17] [0,3] 1 13 \u0026lt; 17 [0,1] [11,13] contains 11 The first two greater-than branches move left to mid + 1, excluding the midpoint and current positions to its left because none can be the minimum. The final two less-than branches move right to mid; the third row concretely proves that discarding mid would discard the answer itself.\nStep 2 Result This version can now use the comparison between nums[mid] and the current nums[right] to shrink one closed interval safely while guaranteeing that the true minimum remains in the next range.\nIt still lacks a complete loop that assembles these updates, a termination condition, and a final return rule.\nStep 3: When Does the Candidate Interval Become the Answer? The two interval updates can keep the minimum safely, but how do we repeat them and return the answer at the right time?\nPressure shrink_minimum_interval_once performs only one step. It does not say when to continue, when to stop, or how to turn the final retained index into the minimum value required by the problem. One safe update is not yet a submit-ready Solution.findMin.\nPrevious Baseline The previous version maintains a closed interval [left, right] and guarantees that the true minimum\u0026rsquo;s index remains inside it. It has also proved both updates:\nExecute left = mid + 1 when nums[mid] \u0026gt; nums[right]. Otherwise execute right = mid; uniqueness means that the actual relation is nums[mid] \u0026lt; nums[right]. Break Without a loop condition, we do not know how many updates to perform. Without a strict-progress proof, a loop might not finish. Without a meaning for the final state, even stopped updates do not tell us which value to return.\nChange: Loop Until One Index Remains In the previous version, place the two proved updates inside while left \u0026lt; right. Recompute mid from the current endpoints in every iteration. When the condition becomes false, the closed interval contains only the index where left == right. Because the interval always retains the minimum, return nums[left].\nThe following is the article\u0026rsquo;s only final Solution.findMin implementation. The fixed assertions and randomized differential checks reuse Step 1\u0026rsquo;s find_min_scan as the correctness baseline, so running this article\u0026rsquo;s Python blocks in order executes every check.\nfrom random import Random from typing import List class Solution: def findMin(self, nums: List[int]) -\u0026gt; int: left = 0 right = len(nums) - 1 while left \u0026lt; right: mid = left + (right - left) // 2 if nums[mid] \u0026gt; nums[right]: left = mid + 1 else: right = mid return nums[left] solution = Solution() assert solution.findMin([3, 4, 5, 1, 2]) == 1 assert solution.findMin([4, 5, 6, 7, 0, 1, 2]) == 0 assert solution.findMin([11, 13, 15, 17]) == 11 assert solution.findMin([7]) == 7 assert solution.findMin([4, 1, 2, 3]) == 1 assert solution.findMin([1, 2, 3, 4]) == 1 nums = [4, 5, 6, 7, 0, 1, 2] snapshot = nums.copy() assert solution.findMin(nums) == 0 assert nums == snapshot rng = Random(153) for length in range(1, 33): original = sorted(rng.sample(range(-5000, 5001), length)) for rotations in range(1, length + 1): rotated = original[-rotations:] + original[:-rotations] snapshot = rotated.copy() assert solution.findMin(rotated) == find_min_scan(rotated) assert rotated == snapshot Strict Progress At the start of an iteration, left \u0026lt; right, so the midpoint satisfies left \u0026lt;= mid \u0026lt; right.\nWith left = mid + 1, the new left is strictly greater than the old left and does not exceed right. With right = mid, the new right is strictly less than the old right and is not less than left. Both branches strictly shorten the closed interval while preserving left \u0026lt;= right. Its length is finite, so the loop must terminate.\nCorrectness Proof The loop maintains this invariant:\nAt the start of every iteration, the true minimum\u0026rsquo;s index is inside the closed interval [left, right].\nInitialization: The problem guarantees a non-empty array. The initial interval [0, len(nums) - 1] contains every index, so it contains the minimum\u0026rsquo;s index.\nPreservation: If nums[mid] \u0026gt; nums[right], Step 2 proved that the minimum lies in [mid + 1, right], so left = mid + 1 preserves the invariant. Otherwise, uniqueness makes the actual relation nums[mid] \u0026lt; nums[right]; Step 2 proved that the minimum lies in [left, mid], so right = mid also preserves the invariant.\nTermination and return: Strict progress guarantees termination. At termination, left == right, while the invariant still places the minimum\u0026rsquo;s index inside [left, right]. This interval has one index, so that index must hold the minimum, and returning nums[left] is correct.\nSlow-Branch Trace For the unrotated array [11,13,15,17], every iteration takes the right = mid branch that retains mid:\n[left, right] mid Comparison Updated interval [0,3] 1 13 \u0026lt; 17 right = mid = 1, giving [0,1] [0,1] 0 11 \u0026lt; 13 right = mid = 0, giving [0,0] Now left == right == 0. The invariant says that the sole retained index is the minimum\u0026rsquo;s index, so the method returns nums[0] == 11.\nBoundary Cases and Checks Singleton: Initially left == right == 0, so the loop is skipped and the sole element is returned. Unrotated or fully rotated: The current range remains strictly increasing, so every iteration has nums[mid] \u0026lt; nums[right]. Moving right to mid eventually retains index 0. Fixed assertions: Six assertions cover all official examples, a singleton, one rotation, and a full rotation. Randomized differential: Seed 153 generates 32 distinct increasing arrays and checks every rotation from 1 through length, for 528 inputs total. The final implementation must match the Step 1 linear scan on each input. Input immutability: The fixed case and every randomized case compare the array before and after the call, confirming that the method does not modify nums. Complexity Let the current closed interval contain m = right - left + 1 candidates. With the midpoint rounded down, either update retains at most ceil(m / 2) candidates for the next iteration. The candidate count therefore falls from n to 1 in O(log n) time. The algorithm stores only left, right, and mid, so its auxiliary space complexity is O(1).\nSummary The linear scan first supplies a correct baseline independent of the rotation position. The closed minimum-containing interval then turns the comparison between nums[mid] and the current nums[right] into two safe updates. Finally, strict progress reduces the interval to the unique minimum index, returning the answer in O(log n) time and O(1) auxiliary space without modifying the input array.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/binary-search/153-find-minimum-in-rotated-sorted-array/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eThe input is a non-empty integer array \u003ccode\u003enums\u003c/code\u003e. Every element is unique, and before rotation the array was sorted in strictly increasing order.\u003c/p\u003e\n\u003cp\u003eThe array is rotated between \u003ccode\u003e1\u003c/code\u003e and \u003ccode\u003enums.length\u003c/code\u003e times. One rotation moves the last element to the front; therefore, \u003ccode\u003enums.length\u003c/code\u003e rotations restore the original increasing order. Return the minimum value in the rotated array. The problem requires an algorithm with \u003ccode\u003eO(log n)\u003c/code\u003e runtime.\u003c/p\u003e\n\u003cp\u003eLeetCode uses this method contract:\u003c/p\u003e","title":"LeetCode 153: Find Minimum in Rotated Sorted Array"},{"content":"Problem Requirement The input gives an integer array nums and an integer target. Every value in nums is distinct, and the array was strictly increasing before rotation. Before the method is called, it may be rotated at an unknown index k (0 \u0026lt;= k \u0026lt; nums.length) into:\n[nums[k], ..., nums[n-1], nums[0], ..., nums[k-1]] Return the index of target in the rotated array when it exists; otherwise return -1. The problem requires an algorithm with O(log n) runtime.\nLeetCode uses this method contract:\nclass Solution: def search(self, nums: List[int], target: int) -\u0026gt; int: Official Examples 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 1 \u0026lt;= nums.length \u0026lt;= 5000 -10^4 \u0026lt;= nums[i], target \u0026lt;= 10^4 Every value in nums is distinct. Before rotation, nums is sorted in strictly increasing order. Step 1: First Search Every Rotation Correctly For [4,5,6,7,0,1,2], how can we first obtain a correct answer that does not depend on the rotation position?\nPressure The array contains two increasing pieces, [4,5,6,7] and [0,1,2], but the jump from 7 to 0 means the whole array is no longer in ordinary increasing order. The whole-array ordering premise used by ordinary binary search therefore does not survive the rotation, so that method cannot be applied unchanged.\nPrevious Baseline The current baseline contains only the problem input, output, official examples, constraints, and the Solution.search method contract. It has no executable search method yet.\nBreak The method contract says what to return, but it provides no executable process that remains correct for every valid rotation.\nChange Add one linear scan to the previous baseline. search_scan checks each value in index order, returns the current index when it meets target, and returns -1 if the scan ends without a match. Rotation changes where values appear, but it cannot make this element-by-element check skip the target.\nfrom typing import List def search_scan(nums: List[int], target: int) -\u0026gt; int: for index, value in enumerate(nums): if value == target: return index return -1 assert search_scan([4, 5, 6, 7, 0, 1, 2], 0) == 4 assert search_scan([4, 5, 6, 7, 0, 1, 2], 3) == -1 assert search_scan([1], 0) == -1 assert search_scan([1, 3, 5, 7], 5) == 2 assert search_scan([9], 9) == 0 assert search_scan([9], 4) == -1 assert search_scan([6, 7, 1, 2, 3, 4, 5], 1) == 2 assert search_scan([6, 7, 1, 2, 3, 4, 5], 8) == -1 Check These fixed assertions cover all three official examples, an unrotated array, a singleton hit and miss, a target exactly at the rotation point, and a missing target. Running the whole block without an assertion failure shows that the scan handles the correctness problem created when rotation removes ordinary whole-array order.\nComplexity For an array of length n, the worst case checks all n elements, so the time complexity is O(n). The loop variables use no storage that grows with the input, so the auxiliary space complexity is O(1).\nStep 1 Result The scan returns the target index in every valid rotation and returns -1 when the target is absent. Its answer is correct, but its O(n) runtime does not yet meet the requirement.\nStep 2: Which Half Is Still Sorted? The linear scan is correct, but it does not use the local order that survives a rotation. To move toward logarithmic search, every current interval must contain a contiguous half that can be proved to retain ordinary increasing order.\nPressure In [4,5,6,7,0,1,2], the whole array is not increasing, but the contiguous piece [4,5,6,7] still is. We now need evidence for an ordinarily sorted contiguous half inside the current interval instead of assuming that the entire array is sorted.\nPrevious Baseline The previous version is the correct O(n) linear scan. It checks each element, depends on no ordering, and therefore works for every valid rotation, but it may inspect the entire array.\nBreak The current interval may cross the rotation point, so the whole interval is not globally monotone. Knowing that the original array was strictly increasing is not enough to treat the entire current interval as an ordinary sorted array.\nChange: Classify the Sorted Half Introduce a closed candidate interval [left, right]:\nleft is the first included index in the current interval. right is the last included index in the current interval. mid = left + (right - left) // 2 is the midpoint of the current interval. The left half is [left, mid], and the right half is [mid, right]; both include mid. For a valid rotated array, the left half is sorted if and only if nums[left] \u0026lt;= nums[mid]; otherwise, the right half is sorted.\nThis conclusion depends on two problem facts. A strictly increasing array rotated once has at most one break where a larger value is followed by a smaller value. Splitting one contiguous interval at mid cannot put that break inside both halves, so at least one half retains ordinary increasing order.\nDistinct values make the endpoint comparison identify the break. If [left, mid] does not cross it, that half is strictly increasing and nums[left] \u0026lt;= nums[mid]. If it does cross the break, every value before the break is greater than every value after it, so nums[left] \u0026gt; nums[mid]. In the latter case, the only break is already in the left half, which guarantees that the right half is sorted.\nThe function below performs one classification and does not change left or right:\nfrom typing import List def classify_sorted_half(nums: List[int], left: int, right: int) -\u0026gt; str: mid = left + (right - left) // 2 if nums[left] \u0026lt;= nums[mid]: return \u0026#34;left\u0026#34; return \u0026#34;right\u0026#34; assert classify_sorted_half([6, 7, 0, 1, 2, 4, 5], 0, 6) == \u0026#34;right\u0026#34; assert classify_sorted_half([4, 5, 6, 7, 0, 1, 2], 0, 6) == \u0026#34;left\u0026#34; assert classify_sorted_half([0, 1, 2, 4, 5, 6, 7], 0, 6) == \u0026#34;left\u0026#34; Check The three assertions match the three shapes below. Each row computes the current midpoint and classifies a sorted half without changing the interval.\nCase nums [left, right] mid Endpoint comparison Classification evidence and result Rotation point left of mid [6,7,0,1,2,4,5] [0, 6] 3 6 \u0026lt;= 1 is false Left half [6,7,0,1] crosses the rotation point; right half [1,2,4,5] is sorted Rotation point right of mid [4,5,6,7,0,1,2] [0, 6] 3 4 \u0026lt;= 7 is true Left half [4,5,6,7] is sorted Unrotated [0,1,2,4,5,6,7] [0, 6] 3 0 \u0026lt;= 4 is true Both halves are sorted; the rule classifies the left half In the first row, the rotation point is index 2, left of mid = 3, so the left half crosses the break and the result is the right half. In the second row, the rotation point is index 4, right of mid = 3, so the left half does not cross the break. The third row has no rotation break, and the endpoint comparison still identifies one sorted half consistently.\nStep 2 Result For any current closed interval, the comparison can now determine whether the left half is sorted and, if not, establish that the right half is sorted. A sorted-half label alone, however, does not show which half can contain target.\nStep 3: Keep the Half That Can Contain the Target A sorted-half label becomes useful only after it proves whether target lies inside that half\u0026rsquo;s value range. Until that containment is established, no position can be discarded safely.\nPressure In [4,5,6,7,0,1,2], the first midpoint is index 3, and the left half [4,5,6,7] is sorted. Searching for 6 should keep the left side, while searching for 0 should keep the right side. The label \u0026ldquo;left half is sorted\u0026rdquo; alone supports neither interval reduction.\nPrevious Baseline The previous version uses the closed interval [left, right] for current candidate positions and recognizes the left half as sorted when nums[left] \u0026lt;= nums[mid]; otherwise it establishes that the right half is sorted. It does not yet compare target with the sorted half\u0026rsquo;s endpoints.\nBreak No current rule proves which half contains the target, so no interval update yet guarantees that an existing target is preserved. Discarding either half before that proof may lose the answer.\nChange: Update the Closed Interval by Target Range Maintain this candidate-interval invariant at the start of every iteration:\nIf target exists in the array, its index is included in the current closed interval [left, right].\nInitialize the interval as [0, len(nums) - 1], which includes every array index. After computing mid, first test nums[mid] == target and return immediately. The later range tests may exclude mid precisely because this equality check has already proved that the midpoint is not the target.\nNow convert the Task 2 sorted-half classification into interval updates:\nIf the left half is sorted, use nums[left] \u0026lt;= target \u0026lt; nums[mid] to test whether the target lies in its value range. When true, keep [left, mid - 1] by setting right = mid - 1. Otherwise, keep [mid + 1, right] by setting left = mid + 1. Otherwise the right half is sorted, so use nums[mid] \u0026lt; target \u0026lt;= nums[right] to test whether the target lies in its value range. When true, keep [mid + 1, right] by setting left = mid + 1. Otherwise, keep [left, mid - 1] by setting right = mid - 1. Both value ranges exclude nums[mid] because the midpoint equality branch runs first. The closed interval endpoints remain included, so a target equal to nums[left] or nums[right] is not discarded.\nFinal Implementation Integrate only the earned midpoint equality, sorted-half classification, target-range tests, and boundary updates into the single LeetCode implementation:\nfrom typing import List class Solution: def search(self, nums: List[int], target: int) -\u0026gt; int: left = 0 right = len(nums) - 1 while left \u0026lt;= right: mid = left + (right - left) // 2 if nums[mid] == target: return mid if nums[left] \u0026lt;= nums[mid]: if nums[left] \u0026lt;= target \u0026lt; nums[mid]: right = mid - 1 else: left = mid + 1 else: if nums[mid] \u0026lt; target \u0026lt;= nums[right]: left = mid + 1 else: right = mid - 1 return -1 Why the Updates Preserve the Candidate Invariant Initialization: [0, len(nums) - 1] includes every valid index, so it includes an existing target. Midpoint hit: If nums[mid] == target, returning mid is correct and no further interval must be maintained. Sorted left half: Strict increase makes nums[left] \u0026lt;= target \u0026lt; nums[mid] an exact test for membership in [left, mid - 1]. When true, update right; otherwise, with the midpoint already excluded, an existing target must lie in [mid + 1, right]. Sorted right half: Similarly, nums[mid] \u0026lt; target \u0026lt;= nums[right] exactly tests membership in [mid + 1, right]. When true, update left; otherwise, an existing target must lie in [left, mid - 1]. Absent target: The invariant constrains only the case where the target exists. When it is absent, the interval still shrinks until the method returns -1. This reasoning is scoped to the problem\u0026rsquo;s distinct-value contract. Distinctness prevents ambiguity in Task 2\u0026rsquo;s sorted-half comparison and makes each sorted-half range strict; no duplicate-value fallback outside the stated contract is added here.\nEvery non-matching iteration executes either right = mid - 1 or left = mid + 1, so the new closed interval is strictly shorter than the old one. The loop must terminate when left \u0026gt; right.\nCheck 1: Target in the Sorted Left Half Search for 6 in [4,5,6,7,0,1,2]:\nleft right mid nums[mid] Decision Result 0 6 3 7 Left half sorted; 4 \u0026lt;= 6 \u0026lt; 7 right = 2 0 2 1 5 Left half sorted, but 4 \u0026lt;= 6 \u0026lt; 5 is false left = 2 2 2 2 6 Midpoint hit Return 2 Check 2: Target in the Sorted Right Half Search for 4 in [6,7,0,1,2,4,5]:\nleft right mid nums[mid] Decision Result 0 6 3 1 Right half sorted; 1 \u0026lt; 4 \u0026lt;= 5 left = 4 4 6 5 4 Midpoint hit Return 5 Check 3: Target at the Rotation Point Search for rotation-point value 0 in [4,5,6,7,0,1,2]:\nleft right mid nums[mid] Decision Result 0 6 3 7 Left half sorted, but 4 \u0026lt;= 0 \u0026lt; 7 is false left = 4 4 6 5 1 Left half sorted; 0 \u0026lt;= 0 \u0026lt; 1 right = 4 4 4 4 0 Midpoint hit Return 4 Check 4: Target Absent Search for 3 in [4,5,6,7,0,1,2]:\nleft right mid nums[mid] Decision Result 0 6 3 7 Left half sorted, but 4 \u0026lt;= 3 \u0026lt; 7 is false left = 4 4 6 5 1 Left half sorted, but 0 \u0026lt;= 3 \u0026lt; 1 is false left = 6 6 6 6 2 Singleton left half sorted, but 2 \u0026lt;= 3 \u0026lt; 2 is false left = 7 Now left = 7 \u0026gt; right = 6, so the candidate interval is empty and the method returns -1.\nExecutable Verification Run these checks after the earlier Task 1 search_scan block and the final Solution block. The fixed assertions cover the official examples, singleton cases, an unrotated array, the rotation point, two-element arrays, and a missing target. The seeded checks then generate strictly increasing arrays of lengths 1 through 40, test every rotation, and confirm that search never mutates its input.\nfrom random import Random solution = Solution() assert solution.search([4, 5, 6, 7, 0, 1, 2], 0) == 4 assert solution.search([4, 5, 6, 7, 0, 1, 2], 3) == -1 assert solution.search([1], 0) == -1 assert solution.search([1], 1) == 0 assert solution.search([1, 3, 5, 7], 1) == 0 assert solution.search([1, 3, 5, 7], 7) == 3 assert solution.search([6, 7, 1, 2, 3, 4, 5], 1) == 2 assert solution.search([6, 7, 0, 1, 2, 4, 5], 4) == 5 assert solution.search([6, 7, 1, 2, 3, 4, 5], 8) == -1 assert solution.search([3, 1], 3) == 0 assert solution.search([3, 1], 1) == 1 nums = [4, 5, 6, 7, 0, 1, 2] before = nums.copy() assert solution.search(nums, 0) == 4 assert nums == before rng = Random(33) checked_cases = 0 for length in range(1, 41): original = sorted(rng.sample(range(-10_000, 10_001), length)) missing = [] while len(missing) \u0026lt; 3: candidate = rng.randint(-10_000, 10_000) if candidate not in original and candidate not in missing: missing.append(candidate) for rotation in range(length): rotated = original[rotation:] + original[:rotation] for target in original + missing: before = rotated.copy() assert solution.search(rotated, target) == search_scan(rotated, target) assert rotated == before checked_cases += 1 assert checked_cases == 24_600 Complexity Each iteration performs a constant number of comparisons and keeps only one side of the midpoint, reducing the candidate interval to at most about half its previous length. There are at most O(log n) iterations, so the time complexity is O(log n). The algorithm stores only a constant number of variables such as left, right, and mid, does not modify the input, and therefore uses O(1) auxiliary space.\nSummary Rotation removes ordinary order from the whole array, but every current interval still has at least one ordinarily sorted half around its midpoint. Check midpoint equality first, then use the sorted half\u0026rsquo;s endpoints to decide whether target lies in its value range. Each boundary update preserves an existing target while strictly shrinking the candidate interval, producing the required O(log n) time and O(1) auxiliary-space search.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/binary-search/33-search-in-rotated-sorted-array/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eThe input gives an integer array \u003ccode\u003enums\u003c/code\u003e and an integer \u003ccode\u003etarget\u003c/code\u003e. Every value in \u003ccode\u003enums\u003c/code\u003e is distinct, and the array was strictly increasing before rotation. Before the method is called, it may be rotated at an unknown index \u003ccode\u003ek\u003c/code\u003e (\u003ccode\u003e0 \u0026lt;= k \u0026lt; nums.length\u003c/code\u003e) into:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[nums[k], ..., nums[n-1], nums[0], ..., nums[k-1]]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eReturn the index of \u003ccode\u003etarget\u003c/code\u003e in the rotated array when it exists; otherwise return \u003ccode\u003e-1\u003c/code\u003e. The problem requires an algorithm with \u003ccode\u003eO(log n)\u003c/code\u003e runtime.\u003c/p\u003e","title":"LeetCode 33: Search in Rotated Sorted Array"},{"content":"Problem Requirement You are given a circular integer array nums. Return an array answer.\nFor every index i:\nanswer[i] = the first value strictly greater than nums[i] when moving right If one full trip around the array finds no greater value, answer[i] = -1.\n\u0026ldquo;Circular\u0026rdquo; means that moving past the final position continues from index 0. An index cannot travel one full circle and use itself as its own answer.\nLeetCode provides this method contract:\nnextGreaterElements(nums: List[int]) -\u0026gt; List[int] Example 1 Input: nums = [1,2,1] Output: [2,-1,2] The first 1 meets the greater value 2 immediately. 2 has no strictly greater value anywhere in one full circle. The final 1 wraps to the beginning and then reaches 2. Example 2 Input: nums = [2,2,2] Output: [-1,-1,-1] Equal values are not greater values.\nExample 3 Input: nums = [7] Output: [-1] A single element cannot use itself as its next greater value.\nConstraints 1 \u0026lt;= nums.length \u0026lt;= 10^4 -10^9 \u0026lt;= nums[i] \u0026lt;= 10^9 Step 1: Where Does \u0026ldquo;To the Right\u0026rdquo; End in a Circle? Start with:\nnums = [1,2,1] In a normal array, the final 1 has no element to its right. In a circular array, its observation order continues with:\nthe 1 at the beginning -\u0026gt; the middle 2 The first strictly greater value is 2.\nThe current baseline is:\nMove right from the current position; after the end, continue from the beginning. This baseline also needs a stopping point. From index i, inspect at most the other n - 1 positions:\nReturn the first greater value if one appears. Return -1 if all of them fail. Do not enter a second lap or treat the starting index as a candidate. This baseline breaks because:\nThe manual order is clear, but it is not an executable index rule. A normal suffix stops at the physical end of the array.\nNow this version can:\nstate the circular right-side order precisely distinguish the first strictly greater value from the maximum value in the circle stop after one lap and preserve -1 when no answer exists It still lacks:\na runnable circular index operation Step 2: Build a Correct Modulo Baseline The current baseline is:\nFrom index i, inspect the following n - 1 circular positions. The ordinary index i + step may cross the array boundary. Add one rule that performs the wraparound:\nnext_index = (i + step) % n When i + step \u0026lt; n, the result is the normal right-side index. After the end, modulo maps it back to the beginning.\nFirst correct implementation:\nfrom typing import List def next_greater_elements_scan(nums: List[int]) -\u0026gt; List[int]: n = len(nums) answer = [-1] * n for i in range(n): for step in range(1, n): next_index = (i + step) % n if nums[next_index] \u0026gt; nums[i]: answer[i] = nums[next_index] break return answer The inner loop starts at 1, so it moves at least one position. It stops before n, so it checks exactly the other n - 1 positions and never returns to the starting index.\nTrace the final index i = 2 in [1,2,1]:\nstep (i + step) % n Value Strictly greater? 1 0 1 No 2 1 2 Yes; write the answer and stop Checks:\nassert next_greater_elements_scan([1, 2, 1]) == [2, -1, 2] assert next_greater_elements_scan([5, 4, 3, 2, 1]) == [-1, 5, 5, 5, 5] assert next_greater_elements_scan([1, 2, 3]) == [2, 3, -1] assert next_greater_elements_scan([2, 2, 2]) == [-1, -1, -1] assert next_greater_elements_scan([7]) == [-1] Now this version can:\ninspect one complete circular future for every index stop at the first strictly greater value preserve -1 when no answer exists handle equal and singleton inputs It still lacks:\nreuse across starting indices; many of them inspect the same circular positions In the worst case, every index checks O(n) candidates, so the time complexity is O(n^2). Extra space is O(1), excluding the output.\nStep 3: Expand the Circle Into Two Virtual Passes The current baseline rebuilds this circular order independently for every start:\ni + 1, i + 2, ..., i + n - 1 The break is:\nEvery start reconstructs the same circle. We first need one shared scan order that represents wraparound.\nFor an array of length n, scan these virtual positions:\ni = 0, 1, 2, ..., 2n - 1 Map every virtual position to the original array with:\nindex = i % n Trace [1,2,1]:\nVirtual i Original i % n Value 0 0 1 1 1 2 2 2 1 3 0 1 4 1 2 5 2 1 The virtual sequence is:\nfirst pass: 1,2,1 second pass: 1,2,1 virtual: 1,2,1,1,2,1 Two passes are sufficient because:\nThe first pass provides the ordinary suffix. The second pass provides the wrapped prefix. Together they expose one complete circular future for every original index. A third pass only repeats candidates already seen. The only change in this step is the shared scan route:\nfor i in range(2 * n): index = i % n This is not a complete algorithm yet. It identifies the current original position but stores no unresolved work.\nNow this version can:\nrepresent the circle as 2n linear visits expose one complete circular future for every original index explain why an infinite loop or third pass is unnecessary It still lacks:\nstate that remembers which original indices still need answers Step 4: Push Each Unresolved Original Index Once The current baseline has one two-pass order, but it still only visits values.\nThe break is:\nOne current greater value may answer several earlier indices. Without storing those unresolved indices, they still need independent scans.\nAdd a stack named stack containing original indices whose answers are unknown.\nWhen a virtual position maps to index, a strictly greater current value settles the stack top:\nwhile stack and nums[index] \u0026gt; nums[stack[-1]]: previous = stack.pop() answer[previous] = nums[index] The stack stores indices rather than values because the algorithm must know which answer entry to write and must ensure each original index is pushed once.\nThe second pass only provides candidate values. Push original indices during the first pass only:\nif i \u0026lt; n: stack.append(index) Connect these changes to the previous virtual loop:\nanswer = [-1] * n stack = [] for i in range(2 * n): index = i % n while stack and nums[index] \u0026gt; nums[stack[-1]]: previous = stack.pop() answer[previous] = nums[index] if i \u0026lt; n: stack.append(index) Trace all six virtual positions for [1,2,1]:\ni index Value Operation Stack after answer 0 0 1 First pass; push 0 [0] [-1,-1,-1] 1 1 2 Pop 0, write 2; push 1 [1] [2,-1,-1] 2 2 1 Cannot pop 1; push 2 [1,2] [2,-1,-1] 3 0 1 Equal to top; no pop or second-pass push [1,2] [2,-1,-1] 4 1 2 Pop 2, write 2; no second-pass push [1] [2,-1,2] 5 2 1 Cannot pop 1; no second-pass push [1] [2,-1,2] Values at stack indices remain non-increasing from bottom to top. Equal values do not satisfy the strict comparison and may remain together.\nIndices still in the stack at the end have no greater value. Their initial answer -1 is already correct.\nNow this version can:\nlet one current value settle multiple unresolved indices resolve wrapped answers during the second pass push every original index only during the first pass preserve strict comparison for equal values It still lacks:\nthe full first-greater invariant an O(n) proof for the nested loop the complete LeetCode wrapper and tests Step 5: Why Two Passes Still Take O(n) Time The stack mechanism is operational. Two questions remain:\nWhy is the current value the first greater value for each popped index? Why does a while inside a 2n loop remain O(n)? Use this loop invariant:\nBefore each virtual position is processed, stack stores first-pass original indices whose answers have not appeared in the scanned circular order, and their values are non-increasing from bottom to top.\nIf previous remains in the stack, every already-scanned circular candidate failed to exceed nums[previous]. The first current value satisfying:\nnums[index] \u0026gt; nums[previous] is therefore its first greater value.\nComplete LeetCode implementation:\nfrom typing import List class Solution: def nextGreaterElements(self, nums: List[int]) -\u0026gt; List[int]: n = len(nums) answer = [-1] * n stack = [] for i in range(2 * n): index = i % n while stack and nums[index] \u0026gt; nums[stack[-1]]: previous = stack.pop() answer[previous] = nums[index] if i \u0026lt; n: stack.append(index) return answer Every original index is pushed exactly once during the first pass. The second pass never pushes. Once an index is popped, it has its answer and never enters the stack again.\nTherefore:\ntotal pushes: n total pops: at most n outer iterations: 2n total iterations of every inner while: at most n The total time is O(n), not O(n^2).\nChecks solution = Solution() assert solution.nextGreaterElements([1, 2, 1]) == [2, -1, 2] assert solution.nextGreaterElements([5, 4, 3, 2, 1]) == [-1, 5, 5, 5, 5] assert solution.nextGreaterElements([1, 2, 3]) == [2, 3, -1] assert solution.nextGreaterElements([2, 2, 2]) == [-1, -1, -1] assert solution.nextGreaterElements([7]) == [-1] assert solution.nextGreaterElements([3, 1, 3]) == [-1, 3, -1] Complexity Time: O(n), because every original index is pushed once and popped at most once. Extra space: O(n), for the output and stack. Common Mistakes 1. Pushing during the second pass That inserts duplicate copies of an original index and breaks the one-push/one-pop model.\n2. Popping with \u0026gt;= The problem asks for strictly greater values. Equal values cannot settle an answer, so the condition must be:\nnums[index] \u0026gt; nums[stack[-1]] 3. Scanning only once One pass cannot resolve wrapped candidates such as the final 1 in [1,2,1].\n4. Scanning more than twice Two passes already expose one full circle of future candidates for every index.\n5. Declaring O(n^2) from the nested syntax Count total pushes and pops. One original index cannot be popped twice.\nSummary The derivation is:\ndefine the first strictly greater value in a circle -\u0026gt; scan one lap per index with modulo -\u0026gt; expand the circular order into two virtual passes -\u0026gt; store unresolved original indices from the first pass -\u0026gt; use the second pass only for wrapped candidates -\u0026gt; push and pop every original index at most once LeetCode 503 extends the 739 stack model by changing the candidate order for a circular array while preserving one stack entry per original index.\nContinue with LeetCode 84 Largest Rectangle in Histogram to use a lower current value as the event that settles stored boundaries.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/503-next-greater-element-ii/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given a circular integer array \u003ccode\u003enums\u003c/code\u003e. Return an array \u003ccode\u003eanswer\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eFor every index \u003ccode\u003ei\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eanswer[i] = the first value strictly greater than nums[i] when moving right\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf one full trip around the array finds no greater value, \u003ccode\u003eanswer[i] = -1\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;Circular\u0026rdquo; means that moving past the final position continues from index \u003ccode\u003e0\u003c/code\u003e. An index cannot travel one full circle and use itself as its own answer.\u003c/p\u003e","title":"LeetCode 503: Where Does the Right Side End in a Circular Array?"},{"content":"Problem Requirement You are given a non-negative integer array heights. Each heights[i] is the height of a bar with width 1, and all bars are adjacent.\nReturn the area of the largest rectangle that can be formed in the histogram.\nA legal rectangle covers a contiguous interval of bars. Its width is the number of bars in that interval, and its height cannot exceed the shortest bar in the interval.\nLeetCode provides this method contract:\nlargestRectangleArea(heights: List[int]) -\u0026gt; int Example 1 Input: heights = [2,1,5,6,2,3] Output: 10 Bars at indices 2 and 3 have heights 5 and 6. They form a rectangle with height 5, width 2, and area 10.\nExample 2 Input: heights = [2,4] Output: 4 Height 4 with width 1 and height 2 with width 2 both produce area 4.\nConstraints 1 \u0026lt;= heights.length \u0026lt;= 10^5 0 \u0026lt;= heights[i] \u0026lt;= 10^4 Step 1: Rectangle Area Is Not the Sum of Bar Heights Start with:\nheights = [2,1,2] If one rectangle covers all three bars, its width is 3, but its height can be at most 1:\nheight = 1 width = 3 area = 1 * 3 = 3 Adding the bar heights to get 5 is incorrect. A histogram rectangle must fill one complete rectangular region, so the middle height 1 limits the entire interval.\nThe current baseline is:\nChoose a contiguous interval and use its shortest bar as the rectangle height. For any interval [left, right]:\nwidth = right - left + 1 height = the shortest bar in the interval area = height * width This baseline breaks because:\nWe can evaluate one chosen interval, but we do not yet have an executable process for comparing every contiguous interval.\nNow this version can:\ndistinguish rectangle area from the sum of bar heights preserve the contiguous-interval requirement use the shortest bar as the limiting height explain area 3 for [2,1,2] and area 10 in the standard example It still lacks:\na correct algorithm that enumerates and compares all legal rectangles Step 2: Enumerate Intervals for a Correct Baseline The current baseline knows how to evaluate an interval:\nshortest interval height * interval width Turn \u0026ldquo;compare every interval\u0026rdquo; into runnable code.\nAfter fixing left, extend right one bar at a time. Each extension only needs one update to the current shortest height:\nmin_height = min(min_height, heights[right]) First correct implementation:\nfrom typing import List def largest_rectangle_quadratic(heights: List[int]) -\u0026gt; int: n = len(heights) best = 0 for left in range(n): min_height = heights[left] for right in range(left, n): min_height = min(min_height, heights[right]) width = right - left + 1 best = max(best, min_height * width) return best Trace left = 0 for [2,1,2]:\nright Interval min_height Width Area Current best 0 [2] 2 1 2 2 1 [2,1] 1 2 2 2 2 [2,1,2] 1 3 3 3 Checks:\nassert largest_rectangle_quadratic([2, 1, 5, 6, 2, 3]) == 10 assert largest_rectangle_quadratic([2, 4]) == 4 assert largest_rectangle_quadratic([2, 1, 2]) == 3 assert largest_rectangle_quadratic([1]) == 1 assert largest_rectangle_quadratic([0]) == 0 assert largest_rectangle_quadratic([1, 2, 3, 4]) == 6 Now this version can:\nenumerate every contiguous interval maintain its shortest height incrementally compare all legal rectangle areas correctly handle one bar and zero height It still lacks:\nreuse across overlapping intervals; the same relative-height relationships are recomputed many times There are O(n) left boundaries and up to O(n) right extensions for each one, so time is O(n^2). Extra space is O(1).\nStep 3: Let Each Bar Be the Limiting Height The current baseline works in this order:\nchoose an interval -\u0026gt; find its shortest bar -\u0026gt; calculate the area The break is:\nOne bar may be the limiting height for many overlapping intervals, but the baseline rediscovers that fact repeatedly.\nReverse the viewpoint. Fix index i, assume heights[i] is the limiting height, and ask how far that height can extend.\nIt extends until the first strictly shorter bar on each side:\nleft: first index left of i where heights[left] \u0026lt; heights[i] right: first index right of i where heights[right] \u0026lt; heights[i] The covered interval is:\nleft + 1 through right - 1 Therefore:\nwidth = right - left - 1 area = heights[i] * width Use the formula immediately on:\nheights = [2,1,5,6,2,3] For index i = 2, height 5:\nThe first strictly shorter bar on the left is height 1 at index 1. The first strictly shorter bar on the right is height 2 at index 4. The valid interval is indices 2..3. left = 1 right = 4 width = 4 - 1 - 1 = 2 area = 5 * 2 = 10 If no shorter bar exists on one side, use a virtual boundary outside the histogram:\nno shorter bar on the left: left = -1 no shorter bar on the right: right = n For height 2 in [2,4], the boundaries are -1 and 2:\nwidth = 2 - (-1) - 1 = 2 area = 2 * 2 = 4 The boundaries are strictly shorter. Equal-height bars do not stop expansion and may belong to the same rectangle.\nNow this version can:\nreplace interval enumeration with one candidate per limiting bar calculate maximal width from first-shorter boundaries handle virtual boundaries at the histogram edges produce area 10 in the standard example It still lacks:\nefficient boundary discovery; scanning both directions for every bar is still O(n^2) shared state for unresolved boundaries Step 4: Settle the Stack Top When a Shorter Bar Arrives The current baseline knows which boundaries every bar needs, but finding them independently still repeats work.\nThe scan itself provides the next pressure:\nWhen current index i is shorter than an earlier bar, i is that taller bar\u0026rsquo;s first strictly shorter right boundary. Its area can now be settled.\nAdd an index stack named stack. Heights at its indices remain non-decreasing from bottom to top:\nheights[stack[0]] \u0026lt;= heights[stack[1]] \u0026lt;= ... When current_height is lower than the stack top, keep popping taller bars:\nwhile stack and heights[stack[-1]] \u0026gt; current_height: height = heights[stack.pop()] left = stack[-1] if stack else -1 width = i - left - 1 best = max(best, height * width) After a pop:\nCurrent i is the popped bar\u0026rsquo;s first strictly shorter right boundary. The new stack top is the left position this candidate cannot cross. An empty stack means the rectangle can extend to the beginning, represented by -1. Connect the mechanism to the real-bar scan:\nbest = 0 stack = [] for i, current_height in enumerate(heights): while stack and heights[stack[-1]] \u0026gt; current_height: height = heights[stack.pop()] left = stack[-1] if stack else -1 width = i - left - 1 best = max(best, height * width) stack.append(i) Trace [2,1,5,6,2]:\ni Current height Operation left Width Area Stack after 0 2 Push 0 - - - [0] 1 1 Pop height 2; push 1 -1 1 2 [1] 2 5 Push 2 - - - [1,2] 3 6 Push 3 - - - [1,2,3] 4 2 Pop height 6 2 1 6 [1,2] 4 2 Pop height 5 1 2 10 [1] 4 2 Push 4 - - - [1,4] For height 5, index 4 is the first shorter right boundary and index 1 blocks the left side:\nwidth = 4 - 1 - 1 = 2 area = 5 * 2 = 10 Why Equal Heights May Stay in the Stack The pop condition is strict \u0026gt;, so equal heights remain together. The new stack top after a pop may therefore be equal to the popped height rather than strictly shorter.\nThis does not lose the maximum area. A later equal-height bar may first settle with a narrower width, while an earlier equal-height bar remains in the stack and later covers the combined wider interval. At least one representative of that height receives the full useful width.\nThe accurate invariant is therefore non-decreasing heights, not strictly increasing heights.\nNow this version can:\nsettle taller bars when their first shorter right boundary arrives derive usable width from the stack after each pop share boundary work in one scan preserve equal heights without losing their widest candidate It still lacks:\nsettlement for bars left in the stack when an increasing suffix reaches the end the final wrapper, invariant, and complexity proof Step 5: Use a Trailing Zero to Settle the Final Bars Consider a strictly increasing input:\nheights = [1,2,3,4] The current baseline only pops when a shorter bar arrives. At the end, all four indices remain unresolved.\nThe break is:\nRemaining bars need a shorter right boundary at virtual index n.\nAppend a virtual zero to a copy:\nbars = heights + [0] This does not mutate the caller\u0026rsquo;s input. The virtual zero is not a real answer bar; it only triggers every final pop.\nComplete LeetCode implementation:\nfrom typing import List class Solution: def largestRectangleArea(self, heights: List[int]) -\u0026gt; int: bars = heights + [0] best = 0 stack = [] for i, current_height in enumerate(bars): while stack and bars[stack[-1]] \u0026gt; current_height: height = bars[stack.pop()] left = stack[-1] if stack else -1 width = i - left - 1 best = max(best, height * width) stack.append(i) return best The loop invariant is:\nBefore each iteration, stack stores indices whose first strictly shorter right boundary has not appeared, in increasing index order and non-decreasing height order.\nWhen a lower current height arrives:\nCurrent i is the popped bar\u0026rsquo;s first strictly shorter right boundary. The new stack top is the left blocking position for this candidate. i - left - 1 is the usable width. After all taller bars are popped, the top height is less than or equal to the current height, so pushing i preserves the invariant.\nThe final virtual zero settles every remaining positive-height bar. Real zero-height bars also work correctly under the strict comparison.\nChecks solution = Solution() assert solution.largestRectangleArea([2, 1, 5, 6, 2, 3]) == 10 assert solution.largestRectangleArea([2, 4]) == 4 assert solution.largestRectangleArea([2, 1, 2]) == 3 assert solution.largestRectangleArea([1]) == 1 assert solution.largestRectangleArea([0]) == 0 assert solution.largestRectangleArea([1, 2, 3, 4]) == 6 assert solution.largestRectangleArea([4, 3, 2, 1]) == 6 assert solution.largestRectangleArea([2, 2, 2]) == 6 Complexity Every index is:\npushed once popped at most once All inner-loop iterations therefore total O(n):\nTime: O(n). Extra space: O(n), for the copied bars array and index stack. Common Mistakes 1. Forgetting bars left in the stack A strictly increasing input never triggers a real pop. Use an explicit cleanup loop or a trailing zero.\n2. Mutating the input with heights.append(0) Use:\nbars = heights + [0] This preserves the caller\u0026rsquo;s array.\n3. Using the wrong width Both left and current i are blocking positions outside the rectangle:\nwidth = i - left - 1 4. Storing heights instead of indices Width calculation requires boundary indices.\n5. Replacing \u0026gt; with \u0026gt;= without changing the invariant An \u0026gt;= solution can also be correct, but it uses a different equal-height policy. This tutorial keeps equal heights and a non-decreasing stack.\n6. Declaring O(n^2) from the nested loops Count total pushes and pops. One index cannot be popped twice.\nSummary The derivation is:\ndefine shortest height and width for a contiguous interval -\u0026gt; enumerate all intervals with min_height -\u0026gt; let each bar be the limiting height -\u0026gt; use first-shorter boundaries to determine maximal width -\u0026gt; settle taller stack tops when a shorter bar arrives -\u0026gt; use a trailing zero to settle the remaining bars -\u0026gt; push and pop every index at most once LeetCode 84 goes beyond 739 and 503: a pop does not merely write one next-greater answer. It combines the current right boundary and the new stack top to derive a full rectangle width.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/84-largest-rectangle-in-histogram/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given a non-negative integer array \u003ccode\u003eheights\u003c/code\u003e. Each \u003ccode\u003eheights[i]\u003c/code\u003e is the height of a bar with width \u003ccode\u003e1\u003c/code\u003e, and all bars are adjacent.\u003c/p\u003e\n\u003cp\u003eReturn the area of the largest rectangle that can be formed in the histogram.\u003c/p\u003e\n\u003cp\u003eA legal rectangle covers a contiguous interval of bars. Its width is the number of bars in that interval, and its height cannot exceed the shortest bar in the interval.\u003c/p\u003e","title":"LeetCode 84: Which Bar Limits a Contiguous Rectangle?"},{"content":"Problem Requirement You are given a non-empty integer array nums.\nExactly one element appears once. Every other element appears exactly twice. Return the element that appears once.\nLeetCode provides this method contract:\nsingleNumber(nums: List[int]) -\u0026gt; int The solution must also satisfy two resource requirements:\nO(n) time O(1) extra space Example 1 Input: nums = [2,2,1] Output: 1 2 appears twice. Only 1 appears once.\nExample 2 Input: nums = [4,1,2,1,2] Output: 4 Both 1 and 2 have matching copies. Only 4 remains unpaired.\nExample 3 Input: nums = [1] Output: 1 If the array has one element, that element is the answer.\nConstraints 1 \u0026lt;= nums.length \u0026lt;= 3 * 10^4 -3 * 10^4 \u0026lt;= nums[i] \u0026lt;= 3 * 10^4 Every element except one appears exactly twice. Step 1: State \u0026ldquo;Appears Once\u0026rdquo; Precisely Start with:\nnums = [4,1,2,1,2] For this tiny array, we can pair equal values by eye:\n1 pairs with 1 2 pairs with 2 4 has no partner The answer is 4.\nThe current baseline is:\nFind matching elements one by one and remove every completed pair. This baseline breaks because:\nVisual pairing only works for tiny inputs. When the array grows and equal values are far apart, we still lack an executable process for tracking which values have found partners.\nDo not choose an algorithm yet. First fix the two questions that every later version must answer:\nHow do we remove every value that appears twice? How do we leave the unique value in O(n) time and O(1) extra space? Now this version can:\nstate the exact duplicate guarantee identify the result as the unpaired value preserve the final linear-time and constant-space target It still lacks:\na runnable pairing process Step 2: Build a Correct Set Baseline The current baseline is:\nFind matching values and remove completed pairs. The idea is correct, but not executable. Two equal values may be far apart, so we need to remember which values have not found partners yet.\nAdd a set named seen:\nfrom typing import List def single_number_with_set(nums: List[int]) -\u0026gt; int: seen = set() for num in nums: if num in seen: seen.remove(num) else: seen.add(num) return next(iter(seen)) The loop invariant is:\nAfter any prefix has been processed, seen contains the values in that prefix that have not completed a pair.\nTrace [4,1,2,1,2]:\nValue Operation seen 4 Not present, add it {4} 1 Not present, add it {4, 1} 2 Not present, add it {4, 1, 2} 1 Present, remove it {4, 2} 2 Present, remove it {4} The problem guarantees exactly one unpaired value, so the set ends with exactly the answer.\nCheck the baseline:\nassert single_number_with_set([2, 2, 1]) == 1 assert single_number_with_set([4, 1, 2, 1, 2]) == 4 assert single_number_with_set([1]) == 1 assert single_number_with_set([-1, 2, 2]) == -1 Now this version can:\nexecute the pairing process for arbitrary order find the unique value in O(n) expected time explain why one value remains It still lacks:\nO(1) extra space; seen may hold O(n) values Step 3: Can Equal Values Cancel Without Being Stored? The current set baseline needs extra space because every value waiting for a partner must be stored.\nThe break is now concrete:\nPreserve the effect that equal values disappear, but do not store those values.\nThis pressure finally introduces XOR, written as ^ in Python.\nFour properties matter here:\nx ^ x = 0 x ^ 0 = x a ^ b = b ^ a (a ^ b) ^ c = a ^ (b ^ c) The first two properties cancel equal values. The last two allow matching values to cancel even when they are far apart.\nUse the properties immediately on [4,1,2,1,2]:\n4 ^ 1 ^ 2 ^ 1 ^ 2 = 4 ^ (1 ^ 1) ^ (2 ^ 2) = 4 ^ 0 ^ 0 = 4 This performs the same work as the set baseline:\nthe two 1 values disappear the two 2 values disappear the unpaired 4 remains The difference is that XOR compresses the entire cancellation state into one integer.\nThe algebraic properties also hold for negative values:\n-1 ^ 2 ^ 2 = -1 ^ 0 = -1 Now this version can:\ncancel duplicate pairs regardless of position explain why input order does not affect the result replace growing unmatched-value storage with one integer state It still lacks:\na one-pass state update a loop invariant for that update Step 4: Turn Cancellation Into an Accumulator The current baseline has established that the array result is:\nnums[0] ^ nums[1] ^ ... ^ nums[n - 1] Turn that expression into one scan by initializing:\nanswer = 0 Merge each value into the current result:\nanswer ^= num Complete LeetCode implementation:\nfrom typing import List class Solution: def singleNumber(self, nums: List[int]) -\u0026gt; int: answer = 0 for num in nums: answer ^= num return answer The loop invariant is:\nAfter every iteration, answer is the XOR of all values in the processed prefix.\nBefore the loop, the processed prefix is empty and its XOR identity is 0. Each answer ^= num extends the represented prefix by one value.\nAt the end, the prefix is the full array. Every duplicate pair cancels, leaving the unique value.\nChecks solution = Solution() assert solution.singleNumber([2, 2, 1]) == 1 assert solution.singleNumber([4, 1, 2, 1, 2]) == 4 assert solution.singleNumber([1]) == 1 assert solution.singleNumber([-1, 2, 2]) == -1 Complexity Time: O(n), because each value participates in one XOR operation. Extra space: O(1), because only answer is stored. Common Mistakes 1. Hiding the set\u0026rsquo;s space cost The set version is a correct baseline, but its worst-case extra space is O(n).\n2. Confusing XOR with logical OR Python\u0026rsquo;s XOR operator is ^, not or and not |.\n3. Forgetting the occurrence guarantee This solution depends on every non-unique value appearing exactly twice. Values that appear three times require a different model.\n4. Claiming the arithmetic formula uses O(1) space 2 * sum(set(nums)) - sum(nums) still creates a set, so its extra space is not O(1).\nSummary The derivation is:\nvisual pairing -\u0026gt; store unmatched values in a set -\u0026gt; expose the set\u0026#39;s O(n) space cost -\u0026gt; use XOR to cancel duplicate pairs -\u0026gt; apply cancellation with one accumulator The reusable idea is not merely one line of code. XOR compresses the unmatched state while forcing every duplicate pair to zero.\nContinue with LeetCode 191 Number of 1 Bits to learn how bit operations can modify binary state directly.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/136-single-number/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given a non-empty integer array \u003ccode\u003enums\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eExactly one element appears once. Every other element appears exactly twice. Return the element that appears once.\u003c/p\u003e\n\u003cp\u003eLeetCode provides this method contract:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003esingleNumber(nums: List[int]) -\u0026gt; int\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe solution must also satisfy two resource requirements:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eO(n) time\u003c/li\u003e\n\u003cli\u003eO(1) extra space\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"example-1\"\u003eExample 1\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [2,2,1]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 1\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003e2\u003c/code\u003e appears twice. Only \u003ccode\u003e1\u003c/code\u003e appears once.\u003c/p\u003e\n\u003ch3 id=\"example-2\"\u003eExample 2\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [4,1,2,1,2]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 4\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eBoth \u003ccode\u003e1\u003c/code\u003e and \u003ccode\u003e2\u003c/code\u003e have matching copies. Only \u003ccode\u003e4\u003c/code\u003e remains unpaired.\u003c/p\u003e","title":"LeetCode 136: Single Number Without Growing Extra Storage"},{"content":"Problem Requirement Given a positive integer n, return the number of 1 bits in its binary representation. This count is also called the Hamming weight.\nLeetCode provides this method contract:\nhammingWeight(n: int) -\u0026gt; int Example 1 Input: n = 11 Binary: 1011 Output: 3 Example 2 Input: n = 128 Binary: 10000000 Output: 1 Constraints 1 \u0026lt;= n \u0026lt;= 2^31 - 1 The input stays in the problem\u0026rsquo;s non-negative integer domain. Although the current constraints start at 1, the implementation also handles n = 0 naturally and returns 0.\nStep 1: State Exactly What We Are Counting Start with a small task that does not use decimal notation:\nn = 101100₂ There are three visible 1 bits:\n1 0 1 1 0 0 ^ ^ ^ The answer is 3.\nThe current baseline is:\nExpand the binary representation and count its 1 bits by eye. This baseline breaks because:\nThe input is an integer, not an already-expanded binary string. Visual counting is not a function for arbitrary input.\nNow this version can:\ndistinguish binary 1 bits from decimal digits equal to 1 explain why 11 has answer 3 and 128 has answer 1 preserve the requirement to operate on integer state directly It still lacks:\na runnable integer bit-inspection process Step 2: Inspect Bits From Right to Left The current baseline needs to read an integer\u0026rsquo;s binary bits.\nThe lowest bit has only two possibilities:\nn \u0026amp; 1 == 1: the lowest bit is 1 n \u0026amp; 1 == 0: the lowest bit is 0 After inspecting it, remove that position with:\nn \u0026gt;\u0026gt;= 1 This right shift makes the next bit the new lowest bit.\nFirst correct implementation:\ndef hamming_weight_by_shift(n: int) -\u0026gt; int: count = 0 while n: count += n \u0026amp; 1 n \u0026gt;\u0026gt;= 1 return count Trace 101100₂:\nCurrent n n \u0026amp; 1 Updated count Shifted n 101100 0 0 10110 10110 0 0 1011 1011 1 1 101 101 1 2 10 10 0 2 1 1 1 3 0 The loop invariant is:\ncount is the number of 1 bits among the positions already removed from the right, while n stores the unprocessed higher positions.\nChecks:\nassert hamming_weight_by_shift(11) == 3 assert hamming_weight_by_shift(128) == 1 assert hamming_weight_by_shift(0) == 0 Now this version can:\ninspect every binary position directly on the integer count all set bits correctly terminate when no unprocessed positions remain It still lacks:\na way to avoid spending iterations on zero bits Step 3: Delete One Set Bit Instead of Crossing Zeros Return to the second example:\nn = 10000000₂ It contains one set bit, but the shift baseline runs eight iterations.\nThe break is:\nWork follows the total bit length rather than the number of 1 bits we actually need to count.\nObserve what subtraction does to 101100₂:\nn = 101100 n - 1 = 101011 The lowest 1 becomes 0, and all lower zeros become 1.\nApply bitwise AND:\n101100 \u0026amp; 101011 -------- 101000 The result removes exactly the lowest set bit from n while preserving all higher bits. Therefore:\nn \u0026amp; (n - 1) removes one 1 per operation.\nUse it repeatedly on 101100₂:\n101100 -\u0026gt; 101000 101000 -\u0026gt; 100000 100000 -\u0026gt; 000000 There are three operations, exactly matching the original set-bit count.\nReplace the previous bit read and shift with:\nn \u0026amp;= n - 1 count += 1 Now this version can:\nremove one set bit in every iteration skip all zeros between set bits finish 128 in one iteration It still lacks:\na final invariant tying the number of removals to the return value the complete LeetCode method Step 4: The Number of Deletions Is the Answer Complete implementation:\nclass Solution: def hammingWeight(self, n: int) -\u0026gt; int: count = 0 while n: n \u0026amp;= n - 1 count += 1 return count The loop invariant is:\nAfter each iteration, count equals the number of 1 bits already removed, and n contains the set bits not yet removed.\nInitially, no bit has been removed, so count = 0. Each n \u0026amp;= n - 1 removes exactly one set bit, and the following increment records that removal.\nWhen n becomes 0, no set bits remain. count is therefore the number of set bits in the original input.\nChecks solution = Solution() assert solution.hammingWeight(0) == 0 assert solution.hammingWeight(11) == 3 assert solution.hammingWeight(128) == 1 assert solution.hammingWeight(2**31 - 1) == 31 Complexity Let k be the number of set bits in n:\nTime: O(k), because every iteration removes one set bit. Extra space: O(1). Why the Input Domain Matters Python models negative integers with conceptual infinite sign extension. Repeatedly applying this loop to a negative value does not naturally reach 0 as a fixed-width unsigned integer would.\nThe problem supplies positive integers, so the final code needs no width mask. If an engineering boundary accepts negative input, define the intended 32-bit or 64-bit representation and normalize it there before running this loop.\nCommon Mistakes 1. Memorizing the expression without knowing what it removes The important fact about n \u0026amp; (n - 1) is not merely that the result is smaller. It removes exactly the lowest set bit.\n2. Forgetting to increment count Changing n deletes a bit; the algorithm must also record how many deletions occurred.\n3. Applying the Python loop directly to negative input That is outside the problem domain and may not terminate as expected.\n4. Calling the shift baseline incorrect The shift version is correct. The optimization only changes how many irrelevant zero positions consume iterations.\nSummary The derivation is:\nvisually count binary 1 bits -\u0026gt; inspect n \u0026amp; 1 and shift right -\u0026gt; expose iterations spent on zeros -\u0026gt; use n \u0026amp; (n - 1) to remove one set bit -\u0026gt; count the removals LeetCode 338 Counting Bits extends this operation from one integer to every value in 0..n, then removes repeated work across those values.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/191-number-of-1-bits/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eGiven a positive integer \u003ccode\u003en\u003c/code\u003e, return the number of \u003ccode\u003e1\u003c/code\u003e bits in its binary representation. This count is also called the Hamming weight.\u003c/p\u003e\n\u003cp\u003eLeetCode provides this method contract:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ehammingWeight(n: int) -\u0026gt; int\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"example-1\"\u003eExample 1\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: n = 11\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eBinary: 1011\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 3\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"example-2\"\u003eExample 2\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: n = 128\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eBinary: 10000000\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 1\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"constraints\"\u003eConstraints\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e1 \u0026lt;= n \u0026lt;= 2^31 - 1\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eThe input stays in the problem\u0026rsquo;s non-negative integer domain.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eAlthough the current constraints start at \u003ccode\u003e1\u003c/code\u003e, the implementation also handles \u003ccode\u003en = 0\u003c/code\u003e naturally and returns \u003ccode\u003e0\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 191: Number of 1 Bits and How to Skip Irrelevant Zeros"},{"content":"Problem Requirement Given a non-negative integer n, return an array answer of length n + 1.\nFor every index:\nanswer[i] = the number of 1 bits in the binary representation of i The requested range includes both 0 and n.\nLeetCode provides this method contract:\ncountBits(n: int) -\u0026gt; List[int] Example 1 Input: n = 2 Output: [0,1,1] 0 -\u0026gt; 0 -\u0026gt; 0 set bits 1 -\u0026gt; 1 -\u0026gt; 1 set bit 2 -\u0026gt; 10 -\u0026gt; 1 set bit Example 2 Input: n = 5 Output: [0,1,1,2,1,2] Constraints 0 \u0026lt;= n \u0026lt;= 10^5 Step 1: This Time We Need Every Answer From 0 to n LeetCode 191 asks for one integer\u0026rsquo;s set-bit count. For n = 5, this problem asks for all six related answers:\ni Binary Set bits 0 0 0 1 1 1 2 10 1 3 11 2 4 100 1 5 101 2 The output is:\n[0,1,1,2,1,2] The current baseline is:\nWe already know how to count the set bits of one integer. This baseline breaks because:\nOne scalar count does not produce the ordered results for every value in 0..n.\nNow this version can:\ndefine exactly what answer[i] means establish that the output length is n + 1 preserve answer[0] = 0 as the base result It still lacks:\na correct algorithm that builds the entire output Step 2: Repeat the LeetCode 191 Process for Every Value The current baseline can count one value with:\nvalue \u0026amp; (value - 1) Each operation removes one lowest set bit. The most direct batch solution is to run that complete process independently for every value from 0 through n.\nfrom typing import List def count_bits_repeated(n: int) -\u0026gt; List[int]: answer = [] for value in range(n + 1): current = value count = 0 while current: current \u0026amp;= current - 1 count += 1 answer.append(count) return answer Checks:\nassert count_bits_repeated(0) == [0] assert count_bits_repeated(2) == [0, 1, 1] assert count_bits_repeated(5) == [0, 1, 1, 2, 1, 2] The control flow is explicit:\nthe outer loop visits every value in order the inner loop counts that value independently answer stores each completed result Now this version can:\ngenerate the full output correctly reuse the already-proved clearing operation from 191 handle the boundary n = 0 It still lacks:\nreuse across different values; every count starts from scratch Because a value has O(log n) binary positions, this baseline has an O(n log n) upper bound.\nStep 3: After Clearing One Set Bit, the Result Already Exists The current baseline repeatedly executes:\nvalue = value \u0026amp; (value - 1) It ignores an important fact. For every i \u0026gt; 0:\ni \u0026amp; (i - 1) \u0026lt; i The operation removes one set bit, so when values are processed in increasing order, the smaller result already exists in answer.\nUse i = 12:\ni = 1100₂ i \u0026amp; (i - 1) = 1000₂ = 8 12 has exactly one more set bit than 8, so:\nanswer[12] = answer[8] + 1 The general recurrence is:\nanswer[i] = answer[i \u0026amp; (i - 1)] + 1 Use it immediately for 1..5:\ni i \u0026amp; (i - 1) Existing result answer[i] 1 0 answer[0] = 0 1 2 0 answer[0] = 0 1 3 2 answer[2] = 1 2 4 0 answer[0] = 0 1 5 4 answer[4] = 1 2 Only now does the dynamic-programming classification earn its place:\ncurrent state: answer[i] smaller subproblem: answer[i \u0026amp; (i - 1)] transition: add one to that smaller result Now this version can:\ncompute each new count with one lookup and one addition prove that the dependency has already been computed turn the 191 clearing operation into cross-value reuse It still lacks:\na complete table-filling implementation with an explicit invariant Step 4: Fill the Table in Increasing Order Initialize the output:\nanswer = [0] * (n + 1) answer[0] = 0 is already the correct base case. Applying the recurrence at i = 0 would create a self-dependency, so the loop starts at 1.\nComplete LeetCode implementation:\nfrom typing import List class Solution: def countBits(self, n: int) -\u0026gt; List[int]: answer = [0] * (n + 1) for i in range(1, n + 1): answer[i] = answer[i \u0026amp; (i - 1)] + 1 return answer The loop invariant is:\nBefore iteration i, every entry in answer[0..i-1] is correct.\nFor i \u0026gt; 0, i \u0026amp; (i - 1) is strictly smaller than i, so the transition reads an earlier correct entry. Writing answer[i] extends the correct prefix by one position.\nChecks solution = Solution() assert solution.countBits(0) == [0] assert solution.countBits(2) == [0, 1, 1] assert solution.countBits(5) == [0, 1, 1, 2, 1, 2] Complexity Time: O(n), because each i uses one transition. Output space: O(n), required by the problem. Extra space: O(1), excluding the returned array. Common Mistakes 1. Starting the recurrence at zero In Python, 0 \u0026amp; (0 - 1) is still 0, so applying the transition would incorrectly set answer[0] = answer[0] + 1. Preserve the base case and start from 1.\n2. Writing the formula without proving the lookup is ready The key relation is i \u0026amp; (i - 1) \u0026lt; i. Without it, the fill order is not justified.\n3. Calling a complete counting function for every value That is the correct baseline, but it does not achieve O(n) batch reuse.\n4. Counting the output array as extra space answer is the required return value. Distinguish O(n) output space from O(1) extra space.\nSummary The derivation is:\ndefine answer[i] -\u0026gt; repeat LeetCode 191 for every value -\u0026gt; notice that the cleared predecessor is already known -\u0026gt; answer[i] = answer[i \u0026amp; (i - 1)] + 1 -\u0026gt; fill the table in increasing order LeetCode 191 teaches how to remove one set bit from one integer. LeetCode 338 reuses the smaller result across an entire range.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/338-counting-bits/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eGiven a non-negative integer \u003ccode\u003en\u003c/code\u003e, return an array \u003ccode\u003eanswer\u003c/code\u003e of length \u003ccode\u003en + 1\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eFor every index:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eanswer[i] = the number of 1 bits in the binary representation of i\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe requested range includes both \u003ccode\u003e0\u003c/code\u003e and \u003ccode\u003en\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eLeetCode provides this method contract:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecountBits(n: int) -\u0026gt; List[int]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"example-1\"\u003eExample 1\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: n = 2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: [0,1,1]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e0 -\u0026gt; 0   -\u0026gt; 0 set bits\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e1 -\u0026gt; 1   -\u0026gt; 1 set bit\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e2 -\u0026gt; 10  -\u0026gt; 1 set bit\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"example-2\"\u003eExample 2\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: n = 5\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: [0,1,1,2,1,2]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"constraints\"\u003eConstraints\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e0 \u0026lt;= n \u0026lt;= 10^5\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"step-1-this-time-we-need-every-answer-from-0-to-n\"\u003eStep 1: This Time We Need Every Answer From 0 to n\u003c/h2\u003e\n\u003cp\u003eLeetCode 191 asks for one integer\u0026rsquo;s set-bit count. For \u003ccode\u003en = 5\u003c/code\u003e, this problem asks for all six related answers:\u003c/p\u003e","title":"LeetCode 338: Counting Bits by Reusing Smaller Results"},{"content":"Problem Requirement You are given an integer array temperatures, where temperatures[i] is the temperature on day i.\nReturn an array answer where:\nanswer[i] = the number of days after day i until a warmer temperature If no later day is warmer, answer[i] = 0.\n\u0026ldquo;Warmer\u0026rdquo; means strictly greater. An equal temperature does not resolve a waiting day.\nLeetCode provides this method contract:\ndailyTemperatures(temperatures: List[int]) -\u0026gt; List[int] Example Input: temperatures = [73,74,75,71,69,72,76,73] Output: [1,1,4,2,1,1,0,0] Constraints 1 \u0026lt;= temperatures.length \u0026lt;= 10^5 30 \u0026lt;= temperatures[i] \u0026lt;= 100 Step 1: The Answer Is a Waiting Time, Not a Temperature Start with a smaller input:\ntemperatures = [73,71,72,76] Answer each day directly:\nDay 0 is 73. Its first warmer day is day 3 at 76, so the wait is 3. Day 1 is 71. Day 2 is 72, so the wait is 1. Day 2 is 72. Day 3 is 76, so the wait is 1. Day 3 has no later day, so its answer is 0. The result is:\n[3,1,1,0] The current baseline is:\nFor each day, look to the right for the first warmer temperature. Fix three details before choosing an algorithm:\nFind the first warmer day, not the maximum future temperature. Return an index distance, not a temperature difference. Equal is not warmer; [70,70] produces [0,0]. Now this version can:\nstate the exact meaning of each output entry preserve zero when no warmer future day exists distinguish strictly warmer from equal It still lacks:\na correct executable search for every day Step 2: Build the Right-Scan Baseline The current baseline says to search right from each day. The direct implementation is a nested loop.\nfrom typing import List def daily_temperatures_scan(temperatures: List[int]) -\u0026gt; List[int]: n = len(temperatures) answer = [0] * n for i in range(n): for j in range(i + 1, n): if temperatures[j] \u0026gt; temperatures[i]: answer[i] = j - i break return answer Every line maps to a requirement:\nanswer starts with zeros for unresolved days. j starts at i + 1, so only future days are checked. \u0026gt; preserves the strict warmer rule. break keeps the first warmer day. j - i is the waiting time. Checks:\nassert daily_temperatures_scan([73, 71, 72, 76]) == [3, 1, 1, 0] assert daily_temperatures_scan([30, 40, 50, 60]) == [1, 1, 1, 0] assert daily_temperatures_scan([60, 50, 40]) == [0, 0, 0] assert daily_temperatures_scan([70, 70]) == [0, 0] Now this version can:\nfind the first strictly warmer future day correctly preserve zero when no answer exists follow the problem definition directly It still lacks:\nacceptable worst-case performance; many days rescan the same suffix For a decreasing array, every i scans to the end. The worst-case time is O(n^2), which is too expensive when n reaches 10^5.\nStep 3: Keep the Days That Are Still Waiting Revisit:\n[73,71,72,76] When 71 arrives, its answer is unknown. When 72 arrives, that answer becomes known immediately because 72 \u0026gt; 71.\nReverse the responsibility:\nDo not make every old day search forward. Let today\u0026rsquo;s temperature resolve earlier days that are still waiting.\nTo calculate waiting time at resolution, store day indices rather than temperature values. Add a stack named stack containing indices whose warmer day has not been found.\nFor current index i and current temperature, inspect the stack top:\nwhile stack and temperature \u0026gt; temperatures[stack[-1]]: previous = stack.pop() answer[previous] = i - previous The stack now performs real work: comparison, removal, and answer updates. After all resolvable days are popped, push the current index:\nstack.append(i) Trace [73,71,72,76]:\ni Temperature Operation Update Stack after 0 73 Push 0 None [0] 1 71 71 is not above 73; push 1 None [0,1] 2 72 Pop 1, then push 2 answer[1] = 1 [0,2] 3 76 Pop 2 and 0, then push 3 answer[2] = 1, answer[0] = 3 [3] During the scan, the stack has three properties:\nindices increase from bottom to top every stored day is still unresolved corresponding temperatures are non-increasing from bottom to top The last property is not \u0026ldquo;strictly decreasing.\u0026rdquo; Equal temperatures do not satisfy \u0026gt;, so equal values may remain together.\nNow this version can:\nremember unresolved days instead of rescanning their suffixes let one warmer day resolve several earlier days write the waiting time directly from index distance It still lacks:\na proof that the current day is the first warmer day for every popped index the complete LeetCode wrapper and complexity argument Step 4: Why a Pop Finds the First Warmer Day Before processing day i, an index previous still in the stack means:\nNo day from previous + 1 through i - 1 was strictly warmer than temperatures[previous]. Otherwise, previous would have been popped on that earlier day.\nWhen the current day satisfies:\ntemperatures[i] \u0026gt; temperatures[previous] day i is therefore the first warmer day to the right, and the algorithm can safely write:\nanswer[previous] = i - previous Complete implementation:\nfrom typing import List class Solution: def dailyTemperatures(self, temperatures: List[int]) -\u0026gt; List[int]: answer = [0] * len(temperatures) stack = [] for i, temperature in enumerate(temperatures): while stack and temperature \u0026gt; temperatures[stack[-1]]: previous = stack.pop() answer[previous] = i - previous stack.append(i) return answer The loop invariant is:\nBefore each iteration, stack stores unresolved indices in increasing date order, and their temperatures are non-increasing from bottom to top.\nPopping resolves every top temperature lower than the current temperature. When popping stops, the top temperature is greater than or equal to the current one, so pushing the current index preserves the invariant.\nIndices left in the stack after the scan have no warmer future day. Their answers are already the required default 0.\nChecks solution = Solution() assert solution.dailyTemperatures([73, 74, 75, 71, 69, 72, 76, 73]) == [1, 1, 4, 2, 1, 1, 0, 0] assert solution.dailyTemperatures([73, 71, 72, 76]) == [3, 1, 1, 0] assert solution.dailyTemperatures([30, 40, 50, 60]) == [1, 1, 1, 0] assert solution.dailyTemperatures([60, 50, 40]) == [0, 0, 0] assert solution.dailyTemperatures([70, 70]) == [0, 0] Complexity Time: O(n), because every index is pushed once and popped at most once. Extra space: O(n), because all indices may remain unresolved in the worst case. The nested while does not make the total O(n^2). One index cannot be popped twice, so all while-loop iterations across the entire scan total O(n).\nCommon Mistakes 1. Storing only temperatures The result is a day distance, so the algorithm needs indices to compute i - previous.\n2. Popping with \u0026gt;= Equal is not warmer. Using \u0026gt;= would incorrectly resolve day 0 in [70,70].\n3. Forgetting to push the current day Today may resolve older days, but today itself may still wait for a warmer future day.\n4. Calling the stack strictly decreasing Equal temperatures remain, so \u0026ldquo;non-increasing\u0026rdquo; is the accurate invariant.\n5. Declaring O(n^2) from the nested syntax Count total pushes and pops, not merely the visual loop nesting.\nSummary The derivation is:\ndefine the first strictly warmer day -\u0026gt; scan right from each day -\u0026gt; expose repeated suffix scans -\u0026gt; store unresolved day indices -\u0026gt; let the current temperature pop and resolve lower stack tops -\u0026gt; push and pop each index at most once The monotonic stack is not useful merely because it looks ordered. It preserves unresolved candidates and settles each one at the first moment its answer becomes known.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/739-daily-temperatures/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an integer array \u003ccode\u003etemperatures\u003c/code\u003e, where \u003ccode\u003etemperatures[i]\u003c/code\u003e is the temperature on day \u003ccode\u003ei\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eReturn an array \u003ccode\u003eanswer\u003c/code\u003e where:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eanswer[i] = the number of days after day i until a warmer temperature\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eIf no later day is warmer, \u003ccode\u003eanswer[i] = 0\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u0026ldquo;Warmer\u0026rdquo; means strictly greater. An equal temperature does not resolve a waiting day.\u003c/p\u003e\n\u003cp\u003eLeetCode provides this method contract:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edailyTemperatures(temperatures: List[int]) -\u0026gt; List[int]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"example\"\u003eExample\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: temperatures = [73,74,75,71,69,72,76,73]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: [1,1,4,2,1,1,0,0]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"constraints\"\u003eConstraints\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e1 \u0026lt;= temperatures.length \u0026lt;= 10^5\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e30 \u0026lt;= temperatures[i] \u0026lt;= 100\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"step-1-the-answer-is-a-waiting-time-not-a-temperature\"\u003eStep 1: The Answer Is a Waiting Time, Not a Temperature\u003c/h2\u003e\n\u003cp\u003eStart with a smaller input:\u003c/p\u003e","title":"LeetCode 739: Daily Temperatures and the First Warmer Day to the Right"},{"content":" Subtitle / Summary Jump Game II is not just reachability. It asks how many coverage layers are needed to reach the last index.\nReading time: 8-10 min Tags: Hot100, greedy, array, reachability boundary SEO keywords: LeetCode 45, Jump Game II, greedy, current_end, farthest, minimum jumps Meta description: A pressure-first Python guide to LeetCode 45 that derives the minimum-jump greedy scan from coverage layers. Problem Requirement You are given an integer array nums.\nYou start at index 0.\nnums[i] means:\nfrom index i, you may jump at most nums[i] steps forward Return:\nthe minimum number of jumps needed to reach the last index Input and Output Input: nums: List[int] Output: int You start at index 0. Each number is the maximum jump length, not the exact jump length. The problem guarantees that the last index is reachable. You only need the minimum jump count; you do not need to return a path. Examples Input: nums = [2,3,1,1,4] Output: 2 One minimum path is:\n0 -\u0026gt; 1 -\u0026gt; 4 Jump from index 0 to index 1, then from index 1 to the last index.\nInput: nums = [2,3,0,1,4] Output: 2 Even though there is a 0 in the middle, this still works:\n0 -\u0026gt; 1 -\u0026gt; 4 Constraints 1 \u0026lt;= nums.length \u0026lt;= 10^4 0 \u0026lt;= nums[i] \u0026lt;= 1000 nums[n - 1] is guaranteed to be reachable Step 1: 45 Does Not Ask Whether We Can Reach After LeetCode 55, we already know how to maintain:\nfarthest = the farthest index covered by all reachable positions so far That is enough to answer:\nCan we reach the last index? But LeetCode 45 asks a different question:\nWhat is the minimum number of jumps? The current baseline is:\nMaintain one farthest boundary and decide reachability. This baseline breaks because:\nfarthest can tell us whether the covered range touches the end, but it does not tell us when the jump count should increase.\nLook at:\nnums = [2,3,1,1,4] The answer is 2:\n0 -\u0026gt; 1 -\u0026gt; 4 This is not asking for any path, and it is not asking whether the end is reachable.\nThe real question is:\nHow many coverage layers are needed to cover the last index? Now this version can:\nseparate LeetCode 45 from LeetCode 55 explain why a single farthest boundary is not enough for counting jumps prepare the next question: where does one jump\u0026rsquo;s coverage end? It still lacks:\na layer model that explains one jump and the next jump. Step 2: Treat One Jump as One Coverage Layer The current baseline is:\nWe need the minimum number of jumps, which means the minimum number of coverage layers. This breaks when:\nGuessing one path, such as 0 -\u0026gt; 1 -\u0026gt; 4, can produce an answer, but it does not prove that the answer is minimum.\nUse the same example:\nnums = [2,3,1,1,4] Start at index 0.\nLayer 0 is the start:\nstep 0: [0] From index 0, nums[0] = 2, so one jump can cover:\nstep 1: [1..2] Do not pick one concrete path yet.\nScan every index inside layer 1:\ni = 1, nums[1] = 3, farthest reach is 4 i = 2, nums[2] = 1, farthest reach is 3 The best expansion inside this layer comes from index 1, so the next layer can reach index 4.\nTherefore:\nstep 2 can reach the last index This is a BFS idea, but we do not need to build a graph.\nEach layer is a contiguous range in the array:\nstep 0: [0] step 1: [1..2] step 2: [3..4] or farther Now this version can:\navoid guessing one path first treat the current jump count as one whole coverage layer explain why [2,3,1,1,4] needs two jumps It still lacks:\ncompact state for the layer boundaries. Step 3: Use current_end and farthest as Two Boundaries The current baseline is:\nEach jump covers one contiguous layer of indices. This breaks because:\nThe layer idea is correct, but explicitly storing each layer is heavier than necessary. During a left-to-right scan, boundaries are enough.\nWe need two boundaries:\ncurrent_end = the right boundary covered by the current jump count farthest = the farthest right boundary reachable by one more jump while scanning this layer Their jobs are different.\ncurrent_end answers:\nWhen has the current layer been fully scanned? farthest answers:\nAfter scanning this layer, how far can the next layer reach? First only focus on the farthest update.\nWhen scanning index i, if i belongs to the current layer, then one more jump from i can reach:\ni + nums[i] So update:\nfarthest = max(farthest, i + nums[i]) Trace [2,3,1,1,4]:\nstart: current_end = 0 farthest = 0 i = 0: farthest = max(0, 0 + nums[0]) = 2 This means:\nfrom the current layer, the next jump can reach index 2 When scanning the next layer:\ni = 1: farthest = max(2, 1 + nums[1]) = 4 i = 2: farthest = max(4, 2 + nums[2]) = 4 This means:\nafter this layer, the next jump can reach index 4 Now this version can:\nuse current_end as the boundary of the current jump layer use farthest as the boundary of the next jump layer derive the farthest update from the indices inside the current layer It still lacks:\nthe exact moment when the layer is finished and steps should increase. Step 4: When i == current_end, This Layer Is Finished The current baseline is:\nWhile scanning, maintain current_end and farthest. This breaks because:\nfarthest may keep expanding, but steps should not increase at every index. A jump is counted only when the current coverage layer has been fully scanned.\nThe right boundary of the current layer is:\ncurrent_end So when:\ni == current_end it means:\nall positions covered by the current jump count have been scanned Now we must use one more jump:\nsteps += 1 current_end = farthest The scan is now:\nsteps = 0 current_end = 0 farthest = 0 for i in range(len(nums) - 1): farthest = max(farthest, i + nums[i]) if i == current_end: steps += 1 current_end = farthest Why stop at:\nrange(len(nums) - 1) Because once we are at the last index, no more jump is needed.\nIf the loop also scans the last index, it may add one extra jump at the destination.\nTrace [2,3,1,1,4]:\nstart: steps = 0, current_end = 0, farthest = 0 i = 0: farthest = max(0, 0 + 2) = 2 i == current_end, layer 0 is finished steps = 1 current_end = 2 i = 1: farthest = max(2, 1 + 3) = 4 i != current_end, do not increment steps i = 2: farthest = max(4, 2 + 1) = 4 i == current_end, layer 1 is finished steps = 2 current_end = 4 Now two jumps can cover the last index.\nNow this version can:\nincrement steps only at a layer boundary move into the next layer with current_end = farthest avoid counting an extra jump at the destination It still lacks:\nthe complete LeetCode method, runnable checks, complexity, and common mistakes. Step 5: Complete LeetCode Solution The current baseline is:\nUse current_end and farthest to scan layers, and increment steps when i == current_end. This breaks because:\nThe rule still needs to be assembled into LeetCode\u0026rsquo;s jump(nums) method and checked against boundary cases.\nComplete code:\nfrom typing import List class Solution: def jump(self, nums: List[int]) -\u0026gt; int: steps = 0 current_end = 0 farthest = 0 for i in range(len(nums) - 1): farthest = max(farthest, i + nums[i]) if i == current_end: steps += 1 current_end = farthest return steps The problem guarantees reachability, so the final code does not need an unreachable branch.\nCheck the second example:\nnums = [2,3,0,1,4] Trace:\ni = 0: farthest = 2 reach current_end, steps = 1, current_end = 2 i = 1: farthest = max(2, 1 + 3) = 4 i = 2: farthest = max(4, 2 + 0) = 4 reach current_end, steps = 2, current_end = 4 Index 2 is 0, but index 1 in the same layer has already expanded the next layer to the end.\nNow this version can:\nreturn the minimum number of jumps handle len(nums) == 1 handle a zero inside the current layer when another index extends farther Correctness Intuition This problem is a compressed BFS.\nEach jump corresponds to one layer:\nall indices reachable with the current steps count While scanning one layer, do not commit to one concrete next index.\nOnly record:\nhow far the next layer can reach That is farthest.\nWhen i == current_end, the current layer has been fully scanned.\nIf we still need to move forward, we must spend one more jump and enter the next layer:\nsteps += 1 current_end = farthest Because every layer represents all positions reachable with one more jump, the first layer boundary that covers the last index gives the minimum jump count.\nComplexity The array is scanned once:\nO(n) Only three variables are stored:\nsteps current_end farthest Space complexity:\nO(1) Common Mistakes 1. Scanning the Last Index Do not write:\nfor i in range(len(nums)): Once the last index is reached, no more jump is needed.\nUse:\nfor i in range(len(nums) - 1): 2. Incrementing steps Whenever farthest Changes farthest is the next layer\u0026rsquo;s farthest boundary.\nIt becoming larger does not mean one jump has finished.\nOnly when:\ni == current_end has the current layer finished, so only then should steps increase.\n3. Mixing current_end and farthest current_end means:\nthe right boundary covered by the current jump count farthest means:\nthe right boundary reachable by the next jump They should not be synchronized at every index.\nOnly when the current layer ends do we run:\ncurrent_end = farthest Runnable Checks from typing import List class Solution: def jump(self, nums: List[int]) -\u0026gt; int: steps = 0 current_end = 0 farthest = 0 for i in range(len(nums) - 1): farthest = max(farthest, i + nums[i]) if i == current_end: steps += 1 current_end = farthest return steps def check() -\u0026gt; None: s = Solution() assert s.jump([2, 3, 1, 1, 4]) == 2 assert s.jump([2, 3, 0, 1, 4]) == 2 assert s.jump([0]) == 0 assert s.jump([1, 2]) == 1 assert s.jump([1, 1, 1, 1]) == 3 check() Takeaway The precise greedy idea in Jump Game II is not:\nalways jump to the farthest immediate index It is:\nscan the entire layer covered by the current jump count, use farthest to record how far the next jump can cover, and increment steps when i == current_end The three variables mean:\nsteps = how many jumps have been used current_end = the right boundary covered by the current jump count farthest = the right boundary reachable by the next jump That is the upgrade from LeetCode 55\u0026rsquo;s reachability boundary to LeetCode 45\u0026rsquo;s minimum-jump boundary.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/greedy/45-jump-game-ii/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nJump Game II is not just reachability. It asks how many coverage layers are needed to reach the last index.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 8-10 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003egreedy\u003c/code\u003e, \u003ccode\u003earray\u003c/code\u003e, \u003ccode\u003ereachability boundary\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 45, Jump Game II, greedy, current_end, farthest, minimum jumps\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A pressure-first Python guide to LeetCode 45 that derives the minimum-jump greedy scan from coverage layers.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an integer array \u003ccode\u003enums\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 45: Jump Game II From Reachability to Minimum Jumps"},{"content":" Subtitle / Summary Non-overlapping Intervals is easier when you stop asking which interval to delete first. First ask how many intervals can remain.\nReading time: 8-10 min Tags: Hot100, greedy, intervals, sorting SEO keywords: LeetCode 435, Non-overlapping Intervals, greedy, interval greedy Meta description: A pressure-first Python guide to LeetCode 435 that derives minimum removals from maximum kept intervals and an earliest-ending greedy scan. Problem Requirement You are given an array of intervals intervals.\nEach interval is written as:\n[start, end] The task is to remove as few intervals as possible so that the remaining intervals do not overlap.\nReturn:\nthe minimum number of intervals to remove Input and Output Input: intervals: List[List[int]] Output: int Each interval satisfies start \u0026lt; end. If two intervals only touch at an endpoint, they are non-overlapping. So:\n[1,2] and [2,3] can both remain.\nExamples Input: intervals = [[1,2],[2,3],[3,4],[1,3]] Output: 1 After removing [1,3], the remaining intervals are:\n[[1,2],[2,3],[3,4]] They do not overlap.\nTwo boundary examples:\nInput: intervals = [[1,2],[1,2],[1,2]] Output: 2 Three identical intervals can keep at most one copy, so two must be removed.\nInput: intervals = [[1,2],[2,3]] Output: 0 These two intervals only touch at endpoint 2, so nothing must be removed.\nConstraints 1 \u0026lt;= intervals.length \u0026lt;= 10^5 intervals[i].length == 2 -5 * 10^4 \u0026lt;= start_i \u0026lt; end_i \u0026lt;= 5 * 10^4 Step 1: Do Not Ask Which One to Delete First Start with:\nintervals = [[1,2],[2,3],[3,4],[1,3]] The problem asks:\nHow many intervals must be removed? The current baseline is:\nDirectly decide which interval to delete. This baseline breaks because:\nWhen several intervals conflict, staring at \u0026ldquo;which one should I delete?\u0026rdquo; does not reveal the rule for making that choice.\nLook at the problem from the other side.\nIf there are n intervals and we can keep at most max_kept non-overlapping intervals, then:\nminimum removals = n - max_kept Or:\nmin_removed = n - max_kept In the example, we can keep:\n[[1,2],[2,3],[3,4]] These three intervals do not overlap.\nThere are 4 intervals total, and at most 3 can remain, so the minimum number removed is:\n4 - 3 = 1 This step changes only the objective, not the answer:\nminimum removals is equivalent to:\nmaximum number of non-overlapping intervals kept Now this version can:\nAvoid guessing which interval to delete first. Compute removals later from n - max_kept. Explain why [[1,2],[2,3],[3,4],[1,3]] returns 1. It still lacks:\nA rule for choosing which interval to keep when two intervals conflict. Step 2: When Intervals Conflict, Keep the Earlier-Ending One The current baseline is:\nMaximize how many non-overlapping intervals remain. This breaks because:\nWe have transformed the objective, but we still do not know which interval to keep when two intervals overlap.\nUse a smaller conflict:\n[1,2], [1,3], [2,3] If we keep [1,3] first:\nkept: [1,3] Then [2,3] overlaps with [1,3], so it is hard to keep more intervals.\nIf we keep [1,2] first:\nkept: [1,2] Because endpoint touching is allowed, [2,3] can still remain:\n[1,2], [2,3] So when there is a conflict, prefer the interval with the earlier end:\nkeep the interval that ends earlier The reason is not that it merely looks shorter. The reason is that it leaves more room for future intervals.\nIn other words:\nIf two intervals can both be candidates for the current kept interval, the earlier-ending one never reduces future choices. So the later scan should process intervals by increasing end.\nNow this version can:\nExplain why conflicts should favor the earlier-ending interval. Derive the sorting key from \u0026ldquo;leave room for the future.\u0026rdquo; Explain why [1,2] is better to keep than [1,3] in the tiny conflict. It still lacks:\nA full scan that applies this rule to the entire array. Step 3: Sort by End and Count How Many Can Remain The current baseline is:\nWhen intervals conflict, keep the one that ends earlier. This breaks because:\nThe local choice is not yet an executable algorithm.\nSort intervals by end:\nintervals.sort(key=lambda x: x[1]) During the scan, maintain:\nlast_end: the end of the last kept interval kept: how many intervals have been kept so far For the current interval:\n[start, end] It can be kept if:\nstart \u0026gt;= last_end That means it does not overlap with the last interval we kept.\nThe condition is \u0026gt;=, not \u0026gt;.\nBecause:\n[1,2] and [2,3] touch at an endpoint, and endpoint touching is non-overlapping in this problem.\nFirst write a version that only computes max_kept:\nfrom typing import List def max_non_overlapping(intervals: List[List[int]]) -\u0026gt; int: intervals.sort(key=lambda x: x[1]) kept = 0 last_end = float(\u0026#34;-inf\u0026#34;) for start, end in intervals: if start \u0026gt;= last_end: kept += 1 last_end = end return kept Check two key cases.\nFirst:\nintervals = [[1,2],[2,3]] The sorted order is still:\n[[1,2],[2,3]] Scan:\nkeep [1,2], last_end = 2 [2,3] has start = 2, so start \u0026gt;= last_end keep [2,3] So:\nmax_kept = 2 Second:\nintervals = [[1,2],[1,2],[1,2]] Scan:\nkeep the first [1,2], last_end = 2 the second [1,2] has start = 1, so it cannot be kept the third [1,2] has start = 1, so it cannot be kept So:\nmax_kept = 1 Now this version can:\nScan intervals by increasing end. Use last_end to decide whether the current interval can remain. Compute the maximum number of non-overlapping intervals kept. It still lacks:\nThe original problem asks for minimum removals, not maximum kept count. Step 4: Convert Maximum Kept Back to Minimum Removed The current baseline is:\nmax_non_overlapping(intervals) computes the maximum number of intervals that can remain. This breaks because:\nReturning kept answers the transformed problem, not the original problem.\nThe original answer is:\nminimum number of intervals removed We already derived:\nmin_removed = n - max_kept So the final step is to convert kept back to removals.\nComplete LeetCode code:\nfrom typing import List class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) -\u0026gt; int: intervals.sort(key=lambda x: x[1]) kept = 0 last_end = float(\u0026#34;-inf\u0026#34;) for start, end in intervals: if start \u0026gt;= last_end: kept += 1 last_end = end return len(intervals) - kept Check the first example:\nintervals = [[1,2],[2,3],[3,4],[1,3]] After sorting by end:\n[[1,2],[2,3],[1,3],[3,4]] Scan:\nkeep [1,2], last_end = 2 keep [2,3], last_end = 3 [1,3] overlaps, skip it keep [3,4], last_end = 4 We keep 3 intervals.\nThere were 4 intervals total, so the minimum number removed is:\n4 - 3 = 1 This matches the expected output.\nNow this version can:\nSort by end and keep earlier-ending intervals. Correctly handle endpoint touching with start \u0026gt;= last_end. Return the minimum number of intervals removed. Correctness Intuition The key is not \u0026ldquo;delete any interval when there is overlap.\u0026rdquo;\nThe actual greedy choice is:\nAmong intervals that could be kept next, keep the one that ends earliest.\nThe earlier an interval ends, the easier it is for later intervals to start after it.\nKeeping a later-ending interval does not increase the current kept count, but it can block future intervals.\nSo after sorting by end, whenever the current interval starts after or at last_end, keep it immediately.\nThat scan computes the maximum number of intervals that can remain.\nThen convert back:\nminimum removals = total intervals - maximum kept Complexity Sorting costs:\nO(n log n) The scan touches each interval once:\nO(n) Total time complexity:\nO(n log n) Aside from sorting, the algorithm only stores kept and last_end:\nO(1) Common Mistakes 1. Treating Endpoint Touching as Overlap The problem says:\n[1,2] and [2,3] are non-overlapping.\nSo the keep condition is:\nstart \u0026gt;= last_end not:\nstart \u0026gt; last_end 2. Returning kept kept is the maximum number of intervals that remain.\nThe original problem asks for the minimum number removed, so return:\nlen(intervals) - kept 3. Sorting by Start and Then Deleting Casually There are other valid interval implementations, but the most stable greedy explanation for this problem sorts by end.\n\u0026ldquo;Ends earlier\u0026rdquo; directly means:\nleave more room for later intervals That is easier to prove and less error-prone than repairing overlaps after sorting by start.\nRunnable Checks from typing import List class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) -\u0026gt; int: intervals.sort(key=lambda x: x[1]) kept = 0 last_end = float(\u0026#34;-inf\u0026#34;) for start, end in intervals: if start \u0026gt;= last_end: kept += 1 last_end = end return len(intervals) - kept def check() -\u0026gt; None: s = Solution() assert s.eraseOverlapIntervals([[1, 2], [2, 3], [3, 4], [1, 3]]) == 1 assert s.eraseOverlapIntervals([[1, 2], [1, 2], [1, 2]]) == 2 assert s.eraseOverlapIntervals([[1, 2], [2, 3]]) == 0 assert s.eraseOverlapIntervals([[1, 100], [11, 22], [1, 11], [2, 12]]) == 2 check() Summary The greedy point in LeetCode 435 is:\nminimum removals = total intervals - maximum kept The maximum-kept strategy is:\nsort by end, and keep an interval whenever it can start after the last kept interval The scan invariant is:\nThe kept intervals are non-overlapping, and last_end is as early as possible, leaving as much room as possible for later intervals.\nAfter this problem, LeetCode 452 becomes more natural: there, we do not keep intervals; we cover intervals with the fewest arrow positions.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/greedy/435-non-overlapping-intervals/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nNon-overlapping Intervals is easier when you stop asking which interval to delete first. First ask how many intervals can remain.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 8-10 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003egreedy\u003c/code\u003e, \u003ccode\u003eintervals\u003c/code\u003e, \u003ccode\u003esorting\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 435, Non-overlapping Intervals, greedy, interval greedy\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A pressure-first Python guide to LeetCode 435 that derives minimum removals from maximum kept intervals and an earliest-ending greedy scan.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an array of intervals \u003ccode\u003eintervals\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 435: Non-overlapping Intervals From Minimum Removals to Maximum Kept"},{"content":" Subtitle / Summary A single arrow does not belong to one balloon. It belongs to a group of balloon intervals that share at least one x-position.\nReading time: 9-11 min Tags: Hot100, greedy, intervals, sorting SEO keywords: LeetCode 452, Minimum Number of Arrows to Burst Balloons, greedy, interval greedy Meta description: A pressure-first Python guide to LeetCode 452 that derives shared intersections, an intersection-scanning baseline, and the final arrow position greedy. Problem Requirement You are given an array points.\nEach balloon is a horizontal interval:\n[start, end] If an arrow is shot at x-coordinate x, and:\nstart \u0026lt;= x \u0026lt;= end then that arrow bursts the balloon.\nAn arrow keeps traveling upward, so every balloon covering the same x is burst by that arrow.\nReturn:\nthe minimum number of arrows needed to burst all balloons Input and Output Input: points: List[List[int]] Output: int Each balloon is an interval [start, end]. The arrow position x may be exactly on an endpoint. You only need to return the minimum number of arrows, not the arrow positions. Examples Input: points = [[10,16],[2,8],[1,6],[7,12]] Output: 2 One valid shooting plan is:\nx = 6 bursts [2,8] and [1,6] x = 11 bursts [10,16] and [7,12] Two boundary examples:\nInput: points = [[1,2],[3,4],[5,6],[7,8]] Output: 4 The balloons are disjoint, so each balloon needs its own arrow.\nInput: points = [[1,2],[2,3],[3,4],[4,5]] Output: 2 Because arrows may land on endpoints:\nx = 2 bursts [1,2] and [2,3] x = 4 bursts [3,4] and [4,5] Constraints 1 \u0026lt;= points.length \u0026lt;= 10^5 points[i].length == 2 -2^31 \u0026lt;= start \u0026lt; end \u0026lt;= 2^31 - 1 Step 1: What Can One Arrow Cover? Start with a tiny question:\n[1,2] and [2,3] Can one arrow burst both balloons?\nYes.\nThe arrow can be shot at:\nx = 2 That satisfies both:\n1 \u0026lt;= 2 \u0026lt;= 2 2 \u0026lt;= 2 \u0026lt;= 3 The current baseline is:\nOne balloon needs one arrow. This baseline breaks because:\nExample 1 has four balloons, but the answer is two. One arrow is not necessarily tied to one balloon.\nThe real question is:\nWhich balloons share at least one x-position? If a group of balloon intervals has a non-empty common intersection, one arrow can burst that whole group.\nFor example:\n[1,6] and [2,8] Their common intersection is:\n[2,6] Any x in that range bursts both balloons.\nNow this version can:\nExplain that one arrow corresponds to a group of intervals with a common intersection. Explain why endpoint touching can still share one arrow. Explain why the answer can be smaller than the number of balloons. It still lacks:\nA repeatable method for grouping all balloons. Step 2: First Write the Shared-Intersection Baseline The current baseline is:\nOne arrow can cover a group of balloons if their intervals have a common intersection. This breaks because:\nWe still cannot compute the minimum number of arrows for arbitrary points.\nStart with a correct version that keeps a little more state.\nSort balloons by start:\npoints.sort(key=lambda p: p[0]) During the scan, maintain the current group\u0026rsquo;s common shooting range:\n[left, right] For a new balloon [start, end]:\nIf start \u0026lt;= right, it still intersects the current group. The common shooting range shrinks to [max(left, start), min(right, end)]. If start \u0026gt; right, the current group cannot cover this balloon, so we need a new arrow. Code:\nfrom typing import List def arrows_by_intersection(points: List[List[int]]) -\u0026gt; int: points.sort(key=lambda p: p[0]) arrows = 1 left, right = points[0] for start, end in points[1:]: if start \u0026lt;= right: left = max(left, start) right = min(right, end) else: arrows += 1 left, right = start, end return arrows Check the main example:\npoints = [[10,16],[2,8],[1,6],[7,12]] After sorting by start:\n[[1,6],[2,8],[7,12],[10,16]] Scan:\ncurrent group: [1,6] read [2,8], still intersects, common range becomes [2,6] read [7,12], 7 \u0026gt; 6, so start a new arrow group [7,12] read [10,16], still intersects, common range becomes [10,12] The answer is 2.\nNow this version can:\nCorrectly count how many common-intersection groups exist. Explain what each arrow group covers. Pass the core example. It still lacks:\nThe state keeps both left and right, but deciding whether the next balloon needs a new arrow only depends on the right boundary. Step 3: Compress the Common Intersection Into arrow_pos The current baseline is:\nMaintain the current group\u0026#39;s common shooting range [left, right]. This breaks because:\nThe baseline is correct, but it carries more state than the final greedy needs. To decide whether the next balloon can be covered, we only need the latest safe position for the current arrow.\nInside a group, the most restrictive balloon is the one that ends earliest.\nIf the earliest ending position in the current group is:\nright then the current arrow cannot be placed to the right of right.\nSo place the arrow at that earliest end:\narrow_pos = right This is the same pressure as LeetCode 435\u0026rsquo;s \u0026ldquo;keep the earlier-ending interval\u0026rdquo;:\nFreeze the right boundary as early as needed, so the current required interval is not missed. Now sort by end.\nFor each balloon [start, end]:\nIf start \u0026lt;= arrow_pos, the current arrow bursts it. If start \u0026gt; arrow_pos, the current arrow cannot reach it, so we need a new arrow at end. The new-arrow condition is:\nstart \u0026gt; arrow_pos not:\nstart \u0026gt;= arrow_pos because arrows can land on endpoints.\nUse the boundary example:\npoints = [[1,2],[2,3],[3,4],[4,5]] If the first arrow is at:\narrow_pos = 2 then [2,3] has start = 2, so it is still burst by that arrow.\nThus [1,2] and [2,3] share one arrow.\nNow this version can:\nCompress the current common intersection\u0026rsquo;s right boundary into arrow_pos. Explain why sorting by end is the natural final order. Decide exactly when a new arrow is required. It still lacks:\nThe complete LeetCode function and runnable checks. Step 4: Count Arrows With arrow_pos The current baseline is:\nSort by end and place the current arrow at the current group\u0026#39;s earliest ending point. This breaks because:\nThe rule has not yet been assembled into LeetCode\u0026rsquo;s findMinArrowShots.\nComplete code:\nfrom typing import List class Solution: def findMinArrowShots(self, points: List[List[int]]) -\u0026gt; int: points.sort(key=lambda p: p[1]) arrows = 1 arrow_pos = points[0][1] for start, end in points[1:]: if start \u0026gt; arrow_pos: arrows += 1 arrow_pos = end return arrows Trace the main example:\npoints = [[10,16],[2,8],[1,6],[7,12]] After sorting by end:\n[[1,6],[2,8],[7,12],[10,16]] Scan:\nplace the first arrow at 6 [2,8] has start = 2 \u0026lt;= 6, so it is covered [7,12] has start = 7 \u0026gt; 6, so open a second arrow at 12 [10,16] has start = 10 \u0026lt;= 12, so it is covered The final answer is 2.\nNow this version can:\nCount arrows with the minimum necessary state. Handle endpoint touching correctly. Satisfy the LeetCode method signature. Correctness Intuition The current arrow must cover all balloons in the current group.\nIf one balloon in the group ends earliest at arrow_pos, then the arrow cannot be placed to the right of arrow_pos.\nPutting the arrow exactly at arrow_pos is safe and flexible:\nIt does not miss the current earliest-ending balloon, and it gives later balloons as much chance as possible to still include the arrow. After sorting by end, each arrow position is decided by the earliest-ending balloon not yet covered.\nIf a later balloon starts at or before arrow_pos, it is covered by the current arrow.\nIf a later balloon starts after arrow_pos, no position can cover both that balloon and the current group, so a new arrow is necessary.\nComplexity Sorting costs:\nO(n log n) The scan is linear:\nO(n) Total time complexity:\nO(n log n) The extra state is only arrows and arrow_pos:\nO(1) Common Mistakes 1. Treating start == arrow_pos as a New Arrow The coverage rule is:\nstart \u0026lt;= x \u0026lt;= end So if:\nstart == arrow_pos the current arrow still bursts that balloon.\nThe new-arrow condition must be:\nstart \u0026gt; arrow_pos 2. Sorting by Start and Using the Final Greedy Rule Directly Sorting by start is useful for the shared-intersection baseline.\nThe final greedy version is cleaner with sorting by end, because arrow_pos comes from the current earliest-ending balloon.\nIf the sorting key and state meaning do not match, the code may pass some examples while remaining hard to prove.\n3. Adding an Empty-Array Branch The constraints say:\n1 \u0026lt;= points.length So the LeetCode version can initialize directly:\narrows = 1 arrow_pos = points[0][1] No empty-array branch is needed.\nRunnable Checks from typing import List class Solution: def findMinArrowShots(self, points: List[List[int]]) -\u0026gt; int: points.sort(key=lambda p: p[1]) arrows = 1 arrow_pos = points[0][1] for start, end in points[1:]: if start \u0026gt; arrow_pos: arrows += 1 arrow_pos = end return arrows def check() -\u0026gt; None: s = Solution() assert s.findMinArrowShots([[10, 16], [2, 8], [1, 6], [7, 12]]) == 2 assert s.findMinArrowShots([[1, 2], [3, 4], [5, 6], [7, 8]]) == 4 assert s.findMinArrowShots([[1, 2], [2, 3], [3, 4], [4, 5]]) == 2 assert s.findMinArrowShots([[1, 2]]) == 1 check() Summary The greedy point in LeetCode 452 is:\nPlace each arrow at the earliest ending point of the current uncovered group. The scan invariant is:\nThe current arrow is placed at arrow_pos; it covers all balloons already assigned to this group. If the next balloon has start \u0026gt; arrow_pos, the current group is closed and a new arrow is required.\nRelation to LeetCode 435:\n435: keep as many non-overlapping intervals as possible. 452: cover all intervals with as few points as possible. Both train the same interval-greedy instinct:\nsort by end, and handle the earliest ending constraint first ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/greedy/452-minimum-number-of-arrows-to-burst-balloons/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nA single arrow does not belong to one balloon. It belongs to a group of balloon intervals that share at least one x-position.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 9-11 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003egreedy\u003c/code\u003e, \u003ccode\u003eintervals\u003c/code\u003e, \u003ccode\u003esorting\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 452, Minimum Number of Arrows to Burst Balloons, greedy, interval greedy\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A pressure-first Python guide to LeetCode 452 that derives shared intersections, an intersection-scanning baseline, and the final arrow position greedy.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an array \u003ccode\u003epoints\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 452: Minimum Number of Arrows From Shared Intersections to Interval Greedy"},{"content":" Subtitle / Summary The greedy idea is not \u0026ldquo;buy low, sell high\u0026rdquo; as a slogan. The precise rule is: if today is the sell day, the best buy day must be the lowest price before today.\nReading time: 8-10 min Tags: Hot100, greedy, array, stock SEO keywords: LeetCode 121, Best Time to Buy and Sell Stock, greedy, historical minimum price Meta description: A pressure-first Python guide to LeetCode 121 that derives brute force, historical minimum price, and the final one-pass greedy solution. Problem Requirement You are given an array prices, where prices[i] is the stock price on day i.\nYou may complete one transaction:\nchoose one day to buy one stock choose a different future day to sell that stock Return the maximum profit you can achieve. If no profitable transaction exists, return 0.\nInput and Output Input: prices: List[int] Output: maximum profit as an int You can buy once and sell once. The buy day must be before the sell day. You may choose not to trade, giving profit 0. Examples Input: prices = [7,1,5,3,6,4] Output: 5 The best trade buys at price 1 and sells at price 6, giving profit 6 - 1 = 5.\nInput: prices = [7,6,4,3,1] Output: 0 Prices keep decreasing, so every buy-before-sell trade loses money. Return 0.\nConstraints 1 \u0026lt;= prices.length \u0026lt;= 10^5 0 \u0026lt;= prices[i] \u0026lt;= 10^4 Step 1: Fix the Buy-Sell Order First Start with:\nprices = [7,1,5,3,6,4] If we only look at price difference, the best profit is:\n1 -\u0026gt; 6 profit = 5 This is legal because price 1 appears before price 6.\nThe current baseline is:\nFind two prices that maximize sell price - buy price. This baseline breaks on time order.\nFor example:\nprices = [6,1] A raw difference mindset might try 1 -\u0026gt; 6, but price 1 happens after price 6. You cannot buy in the future and sell in the past.\nThe break is:\nThe maximum difference must respect buy day before sell day. A future low price cannot be used as the buy price for an earlier high price.\nSo we should turn the problem into a scan:\nWhen we stand on a day and try to sell today, the buy price must come from an earlier day.\nTrace [7,1,5,3,6,4]:\nday 0 price 7: no earlier buy day, cannot sell day 1 price 1: earlier minimum is 7, profit 1 - 7 \u0026lt; 0 day 2 price 5: earlier minimum is 1, profit 5 - 1 = 4 day 3 price 3: earlier minimum is 1, profit 3 - 1 = 2 day 4 price 6: earlier minimum is 1, profit 6 - 1 = 5 Now this version can:\navoid treating the task as arbitrary maximum price difference view each day as a possible sell day restrict today\u0026rsquo;s buy candidate to earlier prices It still lacks:\na first correct runnable version Step 2: Write a Correct but Slow Baseline The current baseline is:\nThe buy day must be before the sell day. To make correctness visible, enumerate every legal transaction.\nThe break is:\nWe only have a verbal rule. We still need executable code that checks every legal pair.\nUse two loops:\nfrom typing import List def max_profit_bruteforce(prices: List[int]) -\u0026gt; int: best = 0 for buy in range(len(prices)): for sell in range(buy + 1, len(prices)): best = max(best, prices[sell] - prices[buy]) return best The loop meaning is direct:\nbuy is the buy day sell starts at buy + 1, so selling always happens in the future best stores the best profit among all legal trades Check it:\nassert max_profit_bruteforce([7, 1, 5, 3, 6, 4]) == 5 assert max_profit_bruteforce([7, 6, 4, 3, 1]) == 0 Now this version can:\nenumerate every legal buy \u0026lt; sell trade avoid reverse-time trades return 0 when no profit is possible It still lacks:\nspeed. prices.length can be 10^5, so O(n^2) will time out. Step 3: Selling Today Only Needs the Historical Minimum The current baseline is:\nFor each sell day, try every earlier buy day. Look at day 4, price 6:\nprices = [7,1,5,3,6,4] ^ sell If we sell today, the possible buy prices are:\n7, 1, 5, 3 The break is:\nFor the same sell price, only the lowest earlier buy price can be optimal. Every higher buy price gives a smaller profit.\nSo we do not need all earlier buy days. We need one state:\nmin_price = the lowest price seen before or up to the current scan point And one answer state:\nbest_profit = the best profit found so far When scanning price, do:\nbest_profit = max(best_profit, price - min_price) min_price = min(min_price, price) Computing profit first treats today as a sell day. Updating min_price then lets today become a buy candidate for later days.\nOne-pass version:\nfrom typing import List def max_profit_greedy(prices: List[int]) -\u0026gt; int: min_price = prices[0] best_profit = 0 for price in prices[1:]: best_profit = max(best_profit, price - min_price) min_price = min(min_price, price) return best_profit Trace [7,1,5,3,6,4]:\nstart: min_price = 7, best_profit = 0 price = 1: profit = -6, best = 0, min_price = 1 price = 5: profit = 4, best = 4, min_price = 1 price = 3: profit = 2, best = 4, min_price = 1 price = 6: profit = 5, best = 5, min_price = 1 price = 4: profit = 3, best = 5, min_price = 1 Now this version can:\ntry selling today with only one historical state keep the global best profit in best_profit reduce the double loop to one scan It still lacks:\nthe LeetCode class Solution wrapper, edge checks, and complexity section Step 4: Complete Code and Verification The current baseline is:\nMaintain min_price and best_profit while scanning prices once. The break is:\nThe code is not yet in the LeetCode Solution.maxProfit shape, and boundary cases are not checked.\nComplete code:\nfrom typing import List class Solution: def maxProfit(self, prices: List[int]) -\u0026gt; int: min_price = prices[0] best_profit = 0 for price in prices[1:]: best_profit = max(best_profit, price - min_price) min_price = min(min_price, price) return best_profit The loop invariant is:\nBefore processing price, min_price is the lowest price among earlier days, and best_profit is the best legal transaction already checked.\nFor the current price, compute the profit if today is the sell day, then let today become a possible buy day for future prices.\nCheck:\ndef check() -\u0026gt; None: s = Solution() assert s.maxProfit([7, 1, 5, 3, 6, 4]) == 5 assert s.maxProfit([7, 6, 4, 3, 1]) == 0 assert s.maxProfit([5]) == 0 assert s.maxProfit([1, 2, 3, 4]) == 3 assert s.maxProfit([4, 3, 2, 1]) == 0 check() Now this version can:\nguarantee buy day is before sell day keep only the one historical state that matters: the lowest buy price return 0 when no profitable trade exists fit the required LeetCode interface Complexity Let n = len(prices).\nTime complexity: O(n), because each price is scanned once. Space complexity: O(1), because only min_price and best_profit are stored. Summary The precise greedy proof for LeetCode 121 is:\nIf today is the sell day, the best buy day must be the lowest price before today. So the scan only needs:\nmin_price: the lowest price seen so far best_profit: the best profit found so far That is the smallest provable greedy state for one stock transaction.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/greedy/121-best-time-to-buy-and-sell-stock/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nThe greedy idea is not \u0026ldquo;buy low, sell high\u0026rdquo; as a slogan. The precise rule is: if today is the sell day, the best buy day must be the lowest price before today.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 8-10 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003egreedy\u003c/code\u003e, \u003ccode\u003earray\u003c/code\u003e, \u003ccode\u003estock\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 121, Best Time to Buy and Sell Stock, greedy, historical minimum price\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A pressure-first Python guide to LeetCode 121 that derives brute force, historical minimum price, and the final one-pass greedy solution.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an array \u003ccode\u003eprices\u003c/code\u003e, where \u003ccode\u003eprices[i]\u003c/code\u003e is the stock price on day \u003ccode\u003ei\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 121: Best Time to Buy and Sell Stock With Greedy"},{"content":" Subtitle / Summary Jump Game is not about guessing one path. It is about maintaining how far all reachable positions can cover.\nReading time: 8-10 min Tags: Hot100, greedy, array, reachability SEO keywords: LeetCode 55, Jump Game, greedy, farthest reach, reachability Meta description: A pressure-first Python guide to LeetCode 55 that derives a reachable-array baseline and compresses it into the farthest reachable range. Problem Requirement You are given an integer array nums.\nYou start at index 0. nums[i] is the maximum jump length from index i.\nReturn whether you can reach the last index.\nInput and Output Input: nums: List[int] Output: bool You start at index 0. Each value is the maximum jump length, not the exact jump length. You only need to decide reachability; you do not need to return a path. Examples Input: nums = [2,3,1,1,4] Output: true One valid path is:\n0 -\u0026gt; 1 -\u0026gt; 4 From index 0, jump to index 1; from index 1, jump to the last index.\nInput: nums = [3,2,1,0,4] Output: false No matter how you jump, you get stuck at index 3, whose jump length is 0, before reaching index 4.\nConstraints 1 \u0026lt;= nums.length \u0026lt;= 10^4 0 \u0026lt;= nums[i] \u0026lt;= 10^5 Step 1: Do Not Guess a Path First Start with the failing example:\nnums = [3,2,1,0,4] From index 0, you can jump as far as index 3.\nThere seem to be several choices:\n0 -\u0026gt; 1 0 -\u0026gt; 2 0 -\u0026gt; 3 The current baseline is:\nTry to pick a jump path and see whether it reaches the end. This baseline breaks because:\nThe problem does not ask for a path. Focusing on one path hides the more important question: how far can all reachable positions cover?\nIn [3,2,1,0,4], every reachable route fails to cross index 3:\nindex: 0 1 2 3 4 nums: 3 2 1 0 4 cover: ----------^ Index 4 is outside the covered range, so the answer is False.\nThe change in this step is the viewpoint:\nDo not choose a concrete path first. Maintain the farthest index currently covered by all reachable positions.\nNow this version can:\ntreat the problem as reachability, not path reconstruction see that failure happens when coverage stops before the last index prepare the state idea: farthest reachable index It still lacks:\na first correct runnable reachability check Step 2: Build a Reachability Array Baseline The current baseline is:\nWe need to know which indices are reachable. The break is:\nThe coverage idea is not executable yet. We need a correct version before compressing it.\nUse an array:\nreachable[i] == True means index i can be reached from index 0 Initialize:\nreachable[0] = True If index i is reachable, then it can mark:\ni + 1, i + 2, ..., i + nums[i] Correct baseline:\nfrom typing import List def can_jump_reachable(nums: List[int]) -\u0026gt; bool: n = len(nums) reachable = [False] * n reachable[0] = True for i in range(n): if not reachable[i]: continue right = min(n - 1, i + nums[i]) for nxt in range(i + 1, right + 1): reachable[nxt] = True return reachable[-1] Check:\nassert can_jump_reachable([2, 3, 1, 1, 4]) is True assert can_jump_reachable([3, 2, 1, 0, 4]) is False Now this version can:\nexplicitly mark which indices are reachable expand reachability from each reachable index handle both official examples correctly It still lacks:\ncompression. The reachable array and inner marking loop are heavier than necessary. Step 3: Compress Reachability to farthest The current baseline is:\nreachable[i] stores whether each index is reachable. Look at the successful example:\nnums = [2,3,1,1,4] From index 0, we can cover up to index 2:\nfarthest = 2 As long as the scan index satisfies i \u0026lt;= farthest, index i is reachable and can extend the right boundary:\nfarthest = max(farthest, i + nums[i]) The break is:\nIf every index in [0..farthest] is reachable, we do not need to store every boolean. The right boundary is enough.\nReplace the array with one variable:\nfarthest = 0 for i, jump in enumerate(nums): if i \u0026lt;= farthest: farthest = max(farthest, i + jump) Trace [2,3,1,1,4]:\nstart: farthest = 0 i = 0, jump = 2: i \u0026lt;= 0, farthest = max(0, 2) = 2 i = 1, jump = 3: i \u0026lt;= 2, farthest = max(2, 4) = 4 Now farthest already reaches the last index, so the answer is True.\nNow this version can:\nrepresent current coverage with one boundary extend coverage from each reachable index use local farthest reach to prove global reachability It still lacks:\nthe failure rule for when the scan reaches an unreachable index Step 4: Fail When the Scan Reaches a Gap The current baseline is:\nScan reachable indices and update farthest. The break is:\nIf i \u0026gt; farthest, index i is not reachable. Using it to expand coverage would be invalid.\nFinal rules:\nif i \u0026gt; farthest, return False otherwise update farthest with i + nums[i] if farthest \u0026gt;= last, return True Complete code:\nfrom typing import List class Solution: def canJump(self, nums: List[int]) -\u0026gt; bool: last = len(nums) - 1 farthest = 0 for i, jump in enumerate(nums): if i \u0026gt; farthest: return False farthest = max(farthest, i + jump) if farthest \u0026gt;= last: return True return True The loop invariant is:\nAt the start of each loop, farthest is the farthest index covered by previously reachable positions. If the current i is beyond it, i is unreachable and later indices cannot be saved by earlier positions.\nCheck:\ndef check() -\u0026gt; None: s = Solution() assert s.canJump([2, 3, 1, 1, 4]) is True assert s.canJump([3, 2, 1, 0, 4]) is False assert s.canJump([0]) is True assert s.canJump([2, 0, 0]) is True assert s.canJump([1, 0, 1, 0]) is False check() Now this version can:\navoid constructing a concrete jump path avoid storing a full reachability array use farthest as the current coverage range return False as soon as the scan reaches a gap Complexity Let n = len(nums).\nTime complexity: O(n), because each index is scanned at most once. Space complexity: O(1), because only farthest is stored. Summary The greedy idea in Jump Game is:\nAs long as the current index i is inside the covered range, it can try to extend the right boundary with i + nums[i]. So we maintain:\nfarthest = the farthest index covered by all reachable positions so far If at any point:\ni \u0026gt; farthest the scan has reached a gap. No earlier jump can cover i, so the answer is False.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/greedy/55-jump-game/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nJump Game is not about guessing one path. It is about maintaining how far all reachable positions can cover.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 8-10 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003egreedy\u003c/code\u003e, \u003ccode\u003earray\u003c/code\u003e, \u003ccode\u003ereachability\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 55, Jump Game, greedy, farthest reach, reachability\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A pressure-first Python guide to LeetCode 55 that derives a reachable-array baseline and compresses it into the farthest reachable range.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an integer array \u003ccode\u003enums\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 55: Jump Game With Farthest Reach Greedy"},{"content":" Subtitle / Summary Do not start this problem by memorizing a Union-Find template. Start from the pressure: each land cell looks like one island at first, but adjacent land cells must collapse into one connected component.\nReading time: 10-12 min Tags: Hot100, Union-Find, DSU, matrix, connected components SEO keywords: LeetCode 200, Number of Islands, Union-Find, DSU, connected components Meta description: A pressure-first Python guide to LeetCode 200 that derives land-only count, 2D-to-1D ids, Union-Find merges, and the final runnable solution. Problem Requirement You are given an m x n 2D character grid:\n\u0026quot;1\u0026quot; means land \u0026quot;0\u0026quot; means water Return the number of islands.\nAn island is formed by land cells connected horizontally or vertically. Diagonal contact does not connect islands. You may assume the four edges of the grid are surrounded by water.\nInput and Output Input: grid: List[List[str]] Output: the number of islands as an int Only four directions matter: up, down, left, and right. Water cells never count as island components. Examples Input: [ [\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;], [\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;], [\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;] ] Output: 1 All land cells are connected through four-direction moves, so there is one island.\nInput: [ [\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;], [\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;0\u0026#34;,\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;] ] Output: 3 There are three disconnected land groups, so the answer is 3.\nConstraints m == grid.length n == grid[i].length 1 \u0026lt;= m, n \u0026lt;= 300 grid[i][j] is \u0026quot;0\u0026quot; or \u0026quot;1\u0026quot; DFS and BFS are both valid approaches. This tutorial uses Union-Find because the training goal is to model the answer as connected component changes.\nStep 1: Do Not Count Land Cells Directly Start with a tiny task.\ngrid = [ [\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;], [\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;] ] If we only count \u0026quot;1\u0026quot; cells, we get 3.\nBut these three land cells are connected:\n(0,0) -- (0,1) | (1,1) They form one island, so the answer should be 1.\nThe current baseline is:\nWhenever we see a land cell, add one to the answer. This baseline breaks when land cells are adjacent.\nThe exact break is:\nOne island may contain multiple land cells. Counting land cells splits one island into many islands.\nNow compare another 2x2 grid:\ngrid = [ [\u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;] ] The two land cells touch only diagonally, so they are not connected. The answer is 2.\nThe change in this step is conceptual:\nNumber of islands = number of four-direction connected components among land cells.\nCheck this step:\n[[\u0026#34;1\u0026#34;,\u0026#34;1\u0026#34;],[\u0026#34;0\u0026#34;,\u0026#34;1\u0026#34;]] -\u0026gt; 1 [[\u0026#34;1\u0026#34;,\u0026#34;0\u0026#34;],[\u0026#34;0\u0026#34;,\u0026#34;1\u0026#34;]] -\u0026gt; 2 Now this version can:\navoid confusing land-cell count with island count distinguish four-direction connection from diagonal contact define the target as connected component count among land cells It still lacks:\na way to represent land cells as nodes that Union-Find can maintain Step 2: Give Only Land Cells an Initial Count The current baseline is:\nNumber of islands = number of connected components among land cells. Union-Find uses an array to store each node\u0026rsquo;s parent. But grid positions are two-dimensional coordinates like (r, c).\nThis baseline breaks because:\nUnion-Find needs one-dimensional node ids, and water cells must not enter the initial island count.\nFirst map each grid coordinate to one flat id.\nIf the grid has n columns:\ndef cell_id(r: int, c: int) -\u0026gt; int: return r * n + c For a 2x3 grid:\n(0,0)-\u0026gt;0 (0,1)-\u0026gt;1 (0,2)-\u0026gt;2 (1,0)-\u0026gt;3 (1,1)-\u0026gt;4 (1,2)-\u0026gt;5 Now initialize the state:\nm, n = len(grid), len(grid[0]) parent = list(range(m * n)) count = 0 parent may reserve a slot for every cell, but count must not start from m * n.\nWater is not an island.\nSo the initial count only counts land:\nfor r in range(m): for c in range(n): if grid[r][c] == \u0026#34;1\u0026#34;: count += 1 Check with this small grid:\ngrid = [ [\u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;], [\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;] ] The land cells and ids are:\n(0,0) -\u0026gt; 0 (0,2) -\u0026gt; 2 (1,1) -\u0026gt; 4 Before any merges:\ncount = 3 Now this version can:\nassign every cell a stable one-dimensional id count only land cells as initial island candidates treat every land cell as one separate component at the beginning It still lacks:\nthe merge rule for adjacent land cells Step 3: Merge Adjacent Land Cells The current baseline is:\nEvery land cell starts as one island candidate. This breaks on a 2x2 all-land grid:\ngrid = [ [\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;], [\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;] ] The initial count is 4, but the answer should be 1.\nThe break is:\nAdjacent land cells belong to the same connected component. Initial count alone keeps overcounting.\nAdd the two Union-Find operations.\nfind(x) returns the representative of x\u0026rsquo;s current component:\ndef find(x: int) -\u0026gt; int: if parent[x] != x: parent[x] = find(parent[x]) return parent[x] The find invariant is:\nfind(x) returns the representative of the component containing x; path compression shortens the route to the representative without changing connectivity.\nunion(a, b) merges the components containing two cells:\ndef union(a: int, b: int) -\u0026gt; bool: root_a = find(a) root_b = find(b) if root_a == root_b: return False parent[root_b] = root_a return True The return value answers the important question:\nDid this operation really merge two different islands? If root_a == root_b, the two cells are already in the same island, so count must not decrease.\nIf root_a != root_b, two different islands just became one island:\nif union(a, b): count -= 1 Trace the 2x2 all-land grid:\ninitial count = 4 merge (0,0) with (0,1): count 4 -\u0026gt; 3 merge (0,0) with (1,0): count 3 -\u0026gt; 2 merge (0,1) with (1,1): count 2 -\u0026gt; 1 already-connected neighbor pairs do not decrease count again In the final scan, we can check only right and down neighbors to avoid duplicate undirected edges.\nNow this version can:\ndecide whether two land cells already belong to the same island merge adjacent land components decrease count only on a real merge It still lacks:\nthe full grid scan and the LeetCode wrapper Step 4: Scan the Grid and Submit The current baseline is:\nWe have cell_id, find, union, and the rule count -= 1 only on a real merge. This breaks because:\nWe still have not fed every adjacent-land relation into Union-Find, and the code is not yet in the required Solution.numIslands shape.\nFor each land cell, check only two directions:\nright: (r, c + 1) down: (r + 1, c) Why only right and down?\nAn undirected neighbor pair only needs to be processed once. If (0,0) checks its right neighbor (0,1), then (0,1) does not need to check left back to (0,0).\nComplete code:\nfrom typing import List class Solution: def numIslands(self, grid: List[List[str]]) -\u0026gt; int: m, n = len(grid), len(grid[0]) parent = list(range(m * n)) count = 0 def cell_id(r: int, c: int) -\u0026gt; int: return r * n + c def find(x: int) -\u0026gt; int: if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(a: int, b: int) -\u0026gt; bool: root_a = find(a) root_b = find(b) if root_a == root_b: return False parent[root_b] = root_a return True for r in range(m): for c in range(n): if grid[r][c] == \u0026#34;1\u0026#34;: count += 1 for r in range(m): for c in range(n): if grid[r][c] != \u0026#34;1\u0026#34;: continue current = cell_id(r, c) if c + 1 \u0026lt; n and grid[r][c + 1] == \u0026#34;1\u0026#34;: if union(current, cell_id(r, c + 1)): count -= 1 if r + 1 \u0026lt; m and grid[r + 1][c] == \u0026#34;1\u0026#34;: if union(current, cell_id(r + 1, c)): count -= 1 return count The scan invariant is:\nAfter processing a land cell\u0026rsquo;s right and down neighbors, every adjacent-land relation seen so far is reflected in Union-Find; count equals the number of land connected components under those processed relations.\nAfter all neighbor relations have been processed, count is the number of islands.\nCheck the code:\ndef check() -\u0026gt; None: s = Solution() assert s.numIslands([ [\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;], [\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;], [\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;], ]) == 1 assert s.numIslands([ [\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;], [\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;], ]) == 3 assert s.numIslands([[\u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;, \u0026#34;0\u0026#34;]]) == 0 assert s.numIslands([[\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;], [\u0026#34;1\u0026#34;, \u0026#34;1\u0026#34;]]) == 1 assert s.numIslands([[\u0026#34;1\u0026#34;, \u0026#34;0\u0026#34;], [\u0026#34;0\u0026#34;, \u0026#34;1\u0026#34;]]) == 2 check() Now this version can:\nkeep water out of the initial island count merge horizontally or vertically adjacent land keep diagonal land separate decrement count only when two different components really merge Complexity Let the grid size be m x n.\nTime complexity: O(mn * alpha(mn)), where alpha is the inverse Ackermann function and is effectively constant in practice. Space complexity: O(mn) for the parent array. Summary For the Union-Find version of Number of Islands, keep this chain fixed:\nnumber of islands = number of connected components among land cells = initial land count - number of real adjacent-land merges So the count rule is:\neach land cell starts as one island candidate adjacent land cells try to union count decreases only when union merges two different components ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/union-find/200-number-of-islands/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nDo not start this problem by memorizing a Union-Find template. Start from the pressure: each land cell looks like one island at first, but adjacent land cells must collapse into one connected component.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003eUnion-Find\u003c/code\u003e, \u003ccode\u003eDSU\u003c/code\u003e, \u003ccode\u003ematrix\u003c/code\u003e, \u003ccode\u003econnected components\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 200, Number of Islands, Union-Find, DSU, connected components\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A pressure-first Python guide to LeetCode 200 that derives land-only count, 2D-to-1D ids, Union-Find merges, and the final runnable solution.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an \u003ccode\u003em x n\u003c/code\u003e 2D character grid:\u003c/p\u003e","title":"LeetCode 200: Number of Islands With Union-Find"},{"content":" Subtitle / Summary The key signal in this problem is not a component count. It is a failed union: if two endpoints are already connected, adding the current edge closes a cycle.\nReading time: 8-10 min Tags: Union-Find, DSU, graph, tree, cycle detection SEO keywords: LeetCode 684, Redundant Connection, Union-Find, DSU, cycle detection Meta description: A pressure-first Python guide to LeetCode 684 that derives 1-indexed Union-Find, bool-returning union, ordered edge scanning, and final checks. Problem Requirement You are given an undirected graph. It started as a tree with n nodes labeled from 1 to n, then one extra edge was added.\nA tree is:\nconnected acyclic After adding one extra edge, the graph is still connected, but now it contains a cycle.\nThe input is edges, where edges[i] = [a, b] means there is an undirected edge between nodes a and b.\nReturn an edge that can be removed so the graph becomes a tree again.\nIf multiple answers are possible, return the one that appears last in the input.\nInput and Output Input: edges: List[List[int]] Output: one edge as List[int] n == len(edges) nodes are labeled 1..n there are no repeated edges the given graph is connected Examples Input: edges = [[1,2],[1,3],[2,3]] Output: [2,3] The first two edges form a tree:\n1 - 2 | 3 Before adding [2,3], node 2 can already reach node 3 through 2 -\u0026gt; 1 -\u0026gt; 3. Adding a direct edge closes a cycle.\nInput: edges = [[1,2],[2,3],[3,4],[1,4],[1,5]] Output: [1,4] Before adding [1,4], node 1 can already reach node 4 through 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4, so [1,4] is redundant.\nConstraints n == edges.length 3 \u0026lt;= n \u0026lt;= 1000 edges[i].length == 2 1 \u0026lt;= ai \u0026lt; bi \u0026lt;= edges.length ai != bi there are no repeated edges the graph is connected Step 1: When Does an Edge Become Redundant? Start with the smallest useful example:\nedges = [[1,2],[1,3],[2,3]] Process the edges in order.\nAdd [1,2]:\n1 - 2 3 No cycle.\nAdd [1,3]:\n2 - 1 - 3 Still a tree.\nNow prepare to add [2,3].\nThe current baseline is:\nWhenever we see an edge, add it to the graph. This baseline breaks because:\nIf the two endpoints are already connected before adding the edge, the edge creates a cycle.\nIn this example, before adding [2,3]:\n2 -\u0026gt; 1 -\u0026gt; 3 So 2 and 3 are already connected. Edge [2,3] is redundant.\nThe change in this step is the question we ask:\nFor every edge [a, b], before adding it, ask whether a and b are already connected.\nIf they are not connected, the edge is safe and should merge two components.\nIf they are already connected, this edge creates a cycle and is the answer.\nCheck this step:\n[1,2]: 1 and 2 are not connected, safe [1,3]: 1 and 3 are not connected, safe [2,3]: 2 and 3 are already connected, return [2,3] Now this version can:\ndefine redundancy as \u0026ldquo;the endpoints were already connected\u0026rdquo; reduce the problem to dynamic connectivity It still lacks:\na data structure that can answer connectivity while edges are scanned Step 2: Use parent to Maintain Components The current baseline is:\nBefore adding [a, b], ask whether a and b are already connected. This breaks because:\nWe do not yet maintain which component each node belongs to.\nUnion-Find is the right state for that question.\nBecause nodes are labeled from 1 to n, create a parent array of size n + 1:\nn = len(edges) parent = list(range(n + 1)) Now parent[1] through parent[n] correspond directly to real nodes. parent[0] is unused.\nAt the beginning, every node is its own representative:\nparent[1] = 1 parent[2] = 2 parent[3] = 3 find(x) returns the representative of the component containing x:\ndef find(x: int) -\u0026gt; int: if parent[x] != x: parent[x] = find(parent[x]) return parent[x] The find invariant is:\nAs we follow parent pointers, x remains inside the same connected component; the self-parent node we reach is the representative of that component.\nPath compression only shortens future routes to the representative. It does not change which nodes are connected.\nNow this version can:\nallocate DSU state for nodes 1..n ask for the representative of any node It still lacks:\na way to merge components when a safe edge appears a way for the merge operation to tell us whether the edge created a cycle Step 3: Make union Report Cycle Pressure The current baseline is:\nfind(x) returns the representative of x\u0026#39;s component. Now process one edge [a, b].\nThis baseline breaks because:\nKnowing roots is not enough. We need to turn root comparison into a decision: safe edge or cycle edge.\nWrite union so it returns a boolean:\ndef union(a: int, b: int) -\u0026gt; bool: root_a = find(a) root_b = find(b) if root_a == root_b: return False parent[root_b] = root_a return True The return value is the contract:\nTrue: the endpoints were in different components, so the edge is safe and the merge happened False: the endpoints were already connected, so this edge creates a cycle Check the triangle example:\nedges = [[1,2],[1,3],[2,3]] Process [1,2]:\nfind(1) != find(2) union(1,2) -\u0026gt; True Process [1,3]:\nfind(1) != find(3) union(1,3) -\u0026gt; True Process [2,3]:\nfind(2) == find(3) union(2,3) -\u0026gt; False So [2,3] is the answer.\nNow this version can:\nmerge two components for safe edges return False for cycle-closing edges use failed union as the redundant-edge signal It still lacks:\nscanning the whole input in the order the problem gives it Step 4: Scan Edges in Input Order The current baseline is:\nunion(a, b) can decide whether one edge creates a cycle. This breaks because:\nThe input is an entire edge list, and the problem asks for the returned edge. If multiple answers are possible, return the one that appears last in the input.\nUnder this problem\u0026rsquo;s promise, the graph is a tree plus one extra edge. If we rebuild the graph from an empty DSU and scan edges in input order:\nbefore a cycle appears, each edge connects two different components the first edge whose endpoints are already connected is the edge that closes the cycle in this input order The scan rule is short:\nfor a, b in edges: if not union(a, b): return [a, b] Check example 2:\nedges = [[1,2],[2,3],[3,4],[1,4],[1,5]] Process in order:\n[1,2]: merge succeeds [2,3]: merge succeeds [3,4]: merge succeeds [1,4]: 1 and 4 are already connected through 1-2-3-4, union fails Return [1,4].\nWe do not need to inspect [1,5] after that, because the problem guarantees this graph is a tree with one additional edge.\nNow this version can:\nscan edges in order return the current edge when union fails explain why example 2 returns [1,4] It still lacks:\nthe final LeetCode wrapper, checks, and complexity Step 5: Complete Code and Verification The current baseline is:\nparent + find + union + ordered scan This breaks because:\nThe code still needs to be packaged as Solution.findRedundantConnection.\nComplete code:\nfrom typing import List class Solution: def findRedundantConnection(self, edges: List[List[int]]) -\u0026gt; List[int]: n = len(edges) parent = list(range(n + 1)) def find(x: int) -\u0026gt; int: if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(a: int, b: int) -\u0026gt; bool: root_a = find(a) root_b = find(b) if root_a == root_b: return False parent[root_b] = root_a return True for a, b in edges: if not union(a, b): return [a, b] return [] The scan invariant is:\nAt the start of each loop, every previous edge that did not return has been safely added to the DSU. If the current edge has endpoints with the same root, adding it would create a cycle.\nCheck the code:\ndef check() -\u0026gt; None: s = Solution() assert s.findRedundantConnection([[1, 2], [1, 3], [2, 3]]) == [2, 3] assert s.findRedundantConnection([ [1, 2], [2, 3], [3, 4], [1, 4], [1, 5], ]) == [1, 4] assert s.findRedundantConnection([ [1, 2], [2, 3], [3, 4], [4, 5], [1, 5], ]) == [1, 5] check() Now this version can:\nalign the parent array with 1..n labels by using n + 1 use find to answer whether two endpoints are in the same component use the union return value to separate safe edges from redundant edges return the first failed union while scanning input order Complexity Let n = len(edges).\nTime complexity: O(n * alpha(n)), where alpha is the inverse Ackermann function and is effectively constant in practice. Space complexity: O(n) for the parent array. Summary LeetCode 684 is not a count-based Union-Find problem.\nThe core signal is:\nunion(a, b) fails That means:\na and b were already connected adding [a, b] would create a cycle the current edge is redundant So keep the union return contract precise:\nTrue: merge succeeded, edge is safe False: endpoints were already connected, edge is redundant ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/union-find/684-redundant-connection/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nThe key signal in this problem is not a component count. It is a failed \u003ccode\u003eunion\u003c/code\u003e: if two endpoints are already connected, adding the current edge closes a cycle.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 8-10 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eUnion-Find\u003c/code\u003e, \u003ccode\u003eDSU\u003c/code\u003e, \u003ccode\u003egraph\u003c/code\u003e, \u003ccode\u003etree\u003c/code\u003e, \u003ccode\u003ecycle detection\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 684, Redundant Connection, Union-Find, DSU, cycle detection\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A pressure-first Python guide to LeetCode 684 that derives 1-indexed Union-Find, bool-returning union, ordered edge scanning, and final checks.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an undirected graph. It started as a tree with \u003ccode\u003en\u003c/code\u003e nodes labeled from \u003ccode\u003e1\u003c/code\u003e to \u003ccode\u003en\u003c/code\u003e, then one extra edge was added.\u003c/p\u003e","title":"LeetCode 684: Redundant Connection With Union-Find"},{"content":" Subtitle / Summary Union-Find is not a pair of function names to memorize first. It solves one concrete problem: decide whether two nodes already belong to the same set, and merge two sets when a connection appears.\nReading time: 10-12 min Tags: Hot100, Union-Find, DSU, graph, connectivity SEO keywords: Union-Find, DSU, Disjoint Set Union, find, union, count, path compression Meta description: A Python Union-Find template guide that derives parent, find, union, count, connected, path compression, and connected component counting. A - Algorithm: Start From Set-Merging Pressure Tiny task: merge sets and answer connectivity Suppose we have 5 nodes:\n0, 1, 2, 3, 4 At the beginning, every node is its own set:\n{0}, {1}, {2}, {3}, {4} Now two merge operations happen:\nunion(0, 1) union(1, 2) We need to answer three questions:\nAre 0 and 2 in the same set? Are 3 and 4 in the same set? How many sets are left? By hand, the sets become:\n{0, 1, 2}, {3}, {4} So:\n0 and 2 are in the same set 3 and 4 are not in the same set count = 3 This is exactly what Union-Find needs to support:\nfind(x): find the representative of the set containing x union(a, b): merge the sets containing a and b count: track how many sets remain Why not scan all sets every time? A direct approach is to store many sets and scan them whenever we need to answer whether two nodes belong together.\nThat breaks down when there are many nodes, many merges, and many connectivity queries. The same membership work gets repeated again and again.\nUnion-Find changes the representation:\nInstead of storing every set as a list of elements, each node points to a parent node. Nodes that eventually reach the same representative belong to the same set.\nTarget Readers Learners who want to memorize and rewrite the basic Union-Find template Readers who get stuck on find / union / count in connectivity and connected-component problems Anyone who has seen LeetCode 547 but wants a standalone DSU template first C - Concepts: Grow the Template One Need at a Time Step 1: Let every node start as its own set The current pressure is simple: at the start, we have n nodes, and every node is alone.\nThe smallest state is:\nn = 5 parent = list(range(n)) Now:\nparent = [0, 1, 2, 3, 4] The meaning is:\nparent[x] == x This means x is currently the representative of its own set.\nThis version can represent the initial state:\n{0}, {1}, {2}, {3}, {4} It still lacks:\na way to follow parent links to find the final representative of a node Step 2: Write the first find Ask one concrete question:\nIf 1 has already been attached under 0, how do we know which set 1 belongs to?\nFor example:\nparent = [0, 0, 2, 3, 4] Here parent[1] = 0, so 1 points to 0. And parent[0] = 0, so 0 is the representative.\nTherefore, find(1) should return 0.\nThe first version is:\ndef find(x: int) -\u0026gt; int: while parent[x] != x: x = parent[x] return x The find loop invariant is:\nAt the start of each loop iteration, x is still in the original set; moving to parent[x] does not change the set, it only moves closer to the representative.\nIf parent[x] == x, we have reached the representative.\nCheck it:\nparent = [0, 0, 2, 3, 4] assert find(0) == 0 assert find(1) == 0 assert find(2) == 2 This version can:\nfind the representative of a node\u0026rsquo;s set use find(a) == find(b) to test whether two nodes are in the same set It still lacks:\na way to merge two sets when a connection appears Step 3: union merges representatives, not raw nodes Now process the first merge:\nunion(0, 1) The current baseline is:\nparent = [0, 1, 2, 3, 4] Writing this directly:\nparent[1] = 0 works only because 1 is currently a representative.\nThe real operation is:\nunion(a, b) a and b may not be representatives. So before merging, we must find the representatives of their sets:\nroot_a = find(a) root_b = find(b) If the representatives are the same, the nodes are already in the same set. If the representatives are different, attach one representative to the other:\ndef union(a: int, b: int) -\u0026gt; bool: root_a = find(a) root_b = find(b) if root_a == root_b: return False parent[root_b] = root_a return True Returning True / False tells the caller whether a real merge happened.\nCheck it:\nparent = list(range(5)) assert union(0, 1) is True assert find(0) == find(1) assert union(1, 2) is True assert find(0) == find(2) assert find(3) != find(4) This version can:\nmerge the sets containing two nodes test connectivity between two nodes avoid merging the same set twice It still lacks:\na count of how many sets remain Step 4: count decreases only on a real merge Initially there are n nodes and every node is its own set. So:\ncount = n Every time union(a, b) really merges two different sets, the number of sets decreases by 1.\nBut if a and b are already in the same set, count must not change.\nThe smallest counterexample is:\nn = 3 union(0, 1) # count goes from 3 to 2 union(0, 1) # duplicate merge, count should stay 2 Putting count inside an object keeps the state clear:\nclass UnionFind: def __init__(self, n: int): self.parent = list(range(n)) self.count = n def find(self, x: int) -\u0026gt; int: while self.parent[x] != x: x = self.parent[x] return x def union(self, a: int, b: int) -\u0026gt; bool: root_a = self.find(a) root_b = self.find(b) if root_a == root_b: return False self.parent[root_b] = root_a self.count -= 1 return True Check duplicate merging:\nuf = UnionFind(3) assert uf.count == 3 assert uf.union(0, 1) is True assert uf.count == 2 assert uf.union(0, 1) is False assert uf.count == 2 This version can:\ndecide whether two nodes are in the same set merge two different sets maintain the current set count It still lacks:\nspeed when parent chains become long Step 5: Path compression makes find shorter over time The current find is correct, but it can be slow.\nConsider a long chain:\n0 -\u0026gt; 1 -\u0026gt; 2 -\u0026gt; 3 If every find(0) walks from 0 to 3, later queries repeat the same work.\nPath compression says:\nAfter finding the representative, attach the visited nodes directly to that representative.\nThe recursive version is short:\ndef find(self, x: int) -\u0026gt; int: if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] The key line is:\nself.parent[x] = self.find(self.parent[x]) This does not change which set x belongs to. It only rewires x to point directly to the final representative.\nCheck a chain:\nuf = UnionFind(4) uf.parent = [1, 2, 3, 3] assert uf.find(0) == 3 assert uf.parent[0] == 3 assert uf.parent[1] == 3 assert uf.parent[2] == 3 After path compression, the next find(0) reaches the representative faster.\nRunnable Example (Python) Here is the basic Union-Find template. It includes:\nparent find union count connected path compression class UnionFind: def __init__(self, n: int): self.parent = list(range(n)) self.count = n def find(self, x: int) -\u0026gt; int: if self.parent[x] != x: self.parent[x] = self.find(self.parent[x]) return self.parent[x] def union(self, a: int, b: int) -\u0026gt; bool: root_a = self.find(a) root_b = self.find(b) if root_a == root_b: return False self.parent[root_b] = root_a self.count -= 1 return True def connected(self, a: int, b: int) -\u0026gt; bool: return self.find(a) == self.find(b) if __name__ == \u0026#34;__main__\u0026#34;: uf = UnionFind(5) assert uf.count == 5 assert uf.union(0, 1) is True assert uf.union(1, 2) is True assert uf.connected(0, 2) is True assert uf.connected(3, 4) is False assert uf.count == 3 assert uf.union(0, 2) is False assert uf.count == 3 If you prefer a function template instead of a class, memorize this shape:\nn = 5 parent = list(range(n)) count = n def find(x: int) -\u0026gt; int: if parent[x] != x: parent[x] = find(parent[x]) return parent[x] def union(a: int, b: int) -\u0026gt; bool: global count root_a = find(a) root_b = find(b) if root_a == root_b: return False parent[root_b] = root_a count -= 1 return True def connected(a: int, b: int) -\u0026gt; bool: return find(a) == find(b) Explanation: Why this design works parent does not store original graph edges parent[x] does not mean there is an original graph edge between x and parent[x].\nIt only means:\nFor set management, x points to a node closer to the set representative.\nUnion-Find maintains set membership, not the original graph shape.\nThat is why Union-Find is good at answering:\nAre a and b connected? How many connected components are left? But it is not good at answering:\nWhat is the exact path from a to b? What is the shortest path? Why must union call find first? Because a and b may not be representatives.\nIf we write:\nparent[b] = a we may attach a normal node under another normal node and damage the structure.\nThe stable pattern is always:\nroot_a = find(a) root_b = find(b) Then only connect representatives.\nThe count invariant The count invariant is:\ncount equals the number of current set representatives.\nAt initialization, every node is a representative, so count = n.\nOnly when root_a != root_b do two representatives become one representative. That is the only time count -= 1 is correct.\nIf root_a == root_b, the number of representatives did not change, so count must not change.\nR - Reflection: Complexity, Tradeoffs, and Pitfalls Complexity Let there be n nodes and m find/union operations.\nWith path compression, Union-Find operations are amortized close to O(1). More formally, with path compression plus union by rank or union by size, the complexity is:\nO(alpha(n)) Here alpha(n) is the inverse Ackermann function. For practical input sizes, it behaves like a constant.\nThis basic template keeps path compression but does not put rank or size into the main line. The reason is:\nthis version is enough for many basic problems the template is shorter and easier to rewrite rank/size optimizes tree height, but it is not the first idea needed to understand find / union / count Common mistakes Writing parent[b] = a inside union(a, b) without finding representatives first Decreasing count even when two nodes are already in the same set Making find(x) return only parent[x] instead of the final representative Treating Union-Find as a structure that can recover the exact path between two nodes Adding rank or size before the basic template is stable When should you use Union-Find? Use it when:\nconnections arrive one by one you need to test whether two nodes are connected you need to count connected components you only care about set membership, not the exact path Avoid it when:\nyou need shortest paths you need to output the path from a to b edges can be deleted and connectivity must stay exact online the problem is directed strongly connected components S - Summary Union-Find solves set membership and set merging. parent[x] == x means x is a set representative. find(x) follows parent links until it reaches the final representative. union(a, b) must find the two representatives before merging them. count decreases only when two different sets are really merged. Path compression does not change the answer; it makes later find calls faster. Further Practice LeetCode 547: Number of Provinces Number of Islands variants: map 2D coordinates to 1D ids Redundant Connection: use Union-Find to detect whether an edge connects nodes already in the same set Kruskal minimum spanning tree: sort edges by weight and use Union-Find to detect cycles ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/union-find/union-find-template/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nUnion-Find is not a pair of function names to memorize first. It solves one concrete problem: decide whether two nodes already belong to the same set, and merge two sets when a connection appears.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003eUnion-Find\u003c/code\u003e, \u003ccode\u003eDSU\u003c/code\u003e, \u003ccode\u003egraph\u003c/code\u003e, \u003ccode\u003econnectivity\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Union-Find, DSU, Disjoint Set Union, find, union, count, path compression\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A Python Union-Find template guide that derives parent, find, union, count, connected, path compression, and connected component counting.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-start-from-set-merging-pressure\"\u003eA - Algorithm: Start From Set-Merging Pressure\u003c/h2\u003e\n\u003ch3 id=\"tiny-task-merge-sets-and-answer-connectivity\"\u003eTiny task: merge sets and answer connectivity\u003c/h3\u003e\n\u003cp\u003eSuppose we have \u003ccode\u003e5\u003c/code\u003e nodes:\u003c/p\u003e","title":"Union-Find Template: Derive find / union / count"},{"content":" Subtitle / Summary LeetCode 208 is the fixed-interface version of the Trie template. We will derive the node fields and lookup invariant first, then fit them into Trie(), insert, search, and startsWith.\nReading time: 10-12 min Tags: Hot100, Trie, prefix tree, LeetCode 208 SEO keywords: LeetCode 208, Implement Trie, Prefix Tree, Python Trie, startsWith, is_end Meta description: A pressure-first Python guide to LeetCode 208 covering Trie nodes, children, is_end, insert, search, startsWith, and submission checks. A - Algorithm Problem setup: design a prefix-tree word index The task is to design a mutable word index. The index starts empty, then receives words one by one, and must answer two kinds of lookup:\nexact lookup: was this complete word inserted? prefix lookup: does any inserted word start with this prefix? That gives the required operations:\nOperation Meaning Example Trie() create an empty index trie = Trie() insert(word) add a word to the index insert(\u0026quot;apple\u0026quot;) search(word) check exact word existence search(\u0026quot;app\u0026quot;) startsWith(prefix) check whether a prefix exists startsWith(\u0026quot;ap\u0026quot;) The important part is that exact lookup and prefix lookup are not the same question.\nSmallest operation pressure The key LeetCode 208 sequence is:\nTrie trie = new Trie() trie.insert(\u0026#34;apple\u0026#34;) trie.search(\u0026#34;apple\u0026#34;) -\u0026gt; true trie.search(\u0026#34;app\u0026#34;) -\u0026gt; false trie.startsWith(\u0026#34;app\u0026#34;) -\u0026gt; true trie.insert(\u0026#34;app\u0026#34;) trie.search(\u0026#34;app\u0026#34;) -\u0026gt; true This example shows:\napp can be a prefix of apple search(\u0026quot;app\u0026quot;) should return True only after app itself has been inserted So this problem is not solved by path existence alone. We need to maintain both:\nwhether a path exists whether that path is exactly a complete word Required interface Design a Trie, also called a prefix tree, with three operations:\ninsert(word): insert a word search(word): check whether a full word has been inserted startsWith(prefix): check whether any inserted word starts with prefix The platform requires this class and method shape:\nclass Trie: def __init__(self): ... def insert(self, word: str) -\u0026gt; None: ... def search(self, word: str) -\u0026gt; bool: ... def startsWith(self, prefix: str) -\u0026gt; bool: ... Target Readers Learners who already understand the Trie template and want to submit LeetCode 208 Readers who know hash-set word lookup but do not yet have a prefix-tree model Anyone trying to fix the boundary between insert, search, and startsWith Background / Motivation LeetCode 208 is a pure template problem. It does not require advanced pruning or prefix counting. It checks whether you understand three basic questions:\nHow do we create missing paths during insertion? Why is path existence not enough for full-word search? Why does prefix search not check is_end? C - Concepts Build the required interface one behavior at a time The platform asks for:\ninsert(word) search(word) startsWith(prefix) Those methods are not arbitrary names. They come from the operation trace:\ninsert(\u0026#34;apple\u0026#34;) search(\u0026#34;apple\u0026#34;) -\u0026gt; True search(\u0026#34;app\u0026#34;) -\u0026gt; False startsWith(\u0026#34;app\u0026#34;) -\u0026gt; True We will build only the state needed to make these answers correct.\nStep 1: Start with paths for inserted words The first behavior is insert(word). To insert a word, the structure must create one edge per character and let different words reuse the same prefix.\nFor example, app and apple should not be stored as two unrelated strings:\napp apple They should share the path for a -\u0026gt; p -\u0026gt; p. That means each position in the structure should represent a prefix reached so far, and from that prefix we need to know which next characters are available.\nSo start with the smallest node:\nclass TrieNode: def __init__(self): self.children = {} Here children is a dictionary:\ncharacter -\u0026gt; child node It is a dictionary rather than a set because we do not only need to know that a next character exists. We also need to move to the child node that represents the longer prefix after taking that character.\nNow add the root and the first version of insert:\nclass Trie: def __init__(self): self.root = TrieNode() def insert(self, word: str) -\u0026gt; None: node = self.root for ch in word: if ch not in node.children: node.children[ch] = TrieNode() node = node.children[ch] This version can create the path for apple:\nroot -\u0026gt; a -\u0026gt; p -\u0026gt; p -\u0026gt; l -\u0026gt; e The loop invariant is:\nBefore processing character i, node points to the node for word[:i], and that path already exists.\nThis version can:\nmake inserted words reachable as paths share prefixes between words But if we try to answer exact word search using only path existence, the first attempt would look like this:\ndef search_by_path_only(word: str) -\u0026gt; bool: node = self.root for ch in word: if ch not in node.children: return False node = node.children[ch] return True Now test it against the required behavior:\ninsert(\u0026#34;apple\u0026#34;) search_by_path_only(\u0026#34;app\u0026#34;) -\u0026gt; True expected search(\u0026#34;app\u0026#34;) -\u0026gt; False expected startsWith(\u0026#34;app\u0026#34;) -\u0026gt; True This is the exact pressure for the next field. The path a -\u0026gt; p -\u0026gt; p really exists, so prefix lookup should succeed. But the complete word app was not inserted yet, so exact lookup should fail.\nStep 2: Add an end marker for exact word search To fix the wrong search_by_path_only(\u0026quot;app\u0026quot;) result, the final node of a complete word needs a marker.\nAdd is_end to each node:\nclass TrieNode: def __init__(self): self.children = {} self.is_end = False Then mark only the final node during insertion:\nclass Trie: def __init__(self): self.root = TrieNode() def insert(self, word: str) -\u0026gt; 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 After insert(\u0026quot;apple\u0026quot;), only the e node is marked as a complete word. The app node exists as a prefix, but it is not marked.\nThis version can:\nstore the difference between a complete word and a prefix It still lacks:\nthe two required public lookup methods Step 3: Implement search and startsWith directly first Now use the state we already have.\nFor search(word), walk the path and then check is_end:\nclass Trie: def __init__(self): self.root = TrieNode() def insert(self, word: str) -\u0026gt; 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) -\u0026gt; bool: node = self.root for ch in word: if ch not in node.children: return False node = node.children[ch] return node.is_end The search lookup invariant is:\nBefore processing character i, node points to the node for word[:i].\nIf the path is missing, the word does not exist. If the path exists, the final node still must be marked as a complete word.\nFor startsWith(prefix), walk the same kind of path, but do not check is_end:\ndef startsWith(self, prefix: str) -\u0026gt; bool: node = self.root for ch in prefix: if ch not in node.children: return False node = node.children[ch] return True The startsWith lookup invariant is:\nBefore processing character i, node points to the node for prefix[:i].\nIf the path exists, the prefix exists. It does not matter whether the final node is the end of a complete word.\nNow the required behavior works:\ninsert(\u0026#34;apple\u0026#34;) search(\u0026#34;apple\u0026#34;) -\u0026gt; True search(\u0026#34;app\u0026#34;) -\u0026gt; False startsWith(\u0026#34;app\u0026#34;) -\u0026gt; True But the two methods clearly repeat the same path-walking loop.\nStep 4: Extract one path lookup helper The repeated part is:\nstart at root walk one character at a time fail if the next edge is missing otherwise return the final node reached by the query Extract that repeated path lookup into _find_node:\ndef _find_node(self, s: str): node = self.root for ch in s: if ch not in node.children: return None node = node.children[ch] return node The helper invariant is the same path invariant, only with a neutral name:\nBefore processing character i, node points to the node for s[:i].\nIf a character is missing, the helper returns None. If the loop finishes, it returns the node for the whole query string.\nThen the two public methods become the two different interpretations of that returned node:\ndef search(self, word: str) -\u0026gt; bool: node = self._find_node(word) return node is not None and node.is_end def startsWith(self, prefix: str) -\u0026gt; bool: return self._find_node(prefix) is not None search requires:\nthe path exists the final node is marked as a complete word startsWith requires only path existence.\nNow the original trace works:\ninsert(\u0026#34;apple\u0026#34;) search(\u0026#34;apple\u0026#34;) -\u0026gt; True search(\u0026#34;app\u0026#34;) -\u0026gt; False startsWith(\u0026#34;app\u0026#34;) -\u0026gt; True insert(\u0026#34;app\u0026#34;) search(\u0026#34;app\u0026#34;) -\u0026gt; True The complete runnable class is now earned.\nRunnable Example (Python) class TrieNode: def __init__(self): self.children = {} self.is_end = False class Trie: def __init__(self): self.root = TrieNode() def insert(self, word: str) -\u0026gt; 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 _find_node(self, s: str): node = self.root for ch in s: if ch not in node.children: return None node = node.children[ch] return node def search(self, word: str) -\u0026gt; bool: node = self._find_node(word) return node is not None and node.is_end def startsWith(self, prefix: str) -\u0026gt; bool: return self._find_node(prefix) is not None if __name__ == \u0026#34;__main__\u0026#34;: trie = Trie() trie.insert(\u0026#34;apple\u0026#34;) assert trie.search(\u0026#34;apple\u0026#34;) is True assert trie.search(\u0026#34;app\u0026#34;) is False assert trie.startsWith(\u0026#34;app\u0026#34;) is True trie.insert(\u0026#34;app\u0026#34;) assert trie.search(\u0026#34;app\u0026#34;) is True Reference Answer For LeetCode submission, use the same implementation shape:\nclass TrieNode: def __init__(self): self.children = {} self.is_end = False class Trie: def __init__(self): self.root = TrieNode() def insert(self, word: str) -\u0026gt; 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 _find_node(self, s: str): node = self.root for ch in s: if ch not in node.children: return None node = node.children[ch] return node def search(self, word: str) -\u0026gt; bool: node = self._find_node(word) return node is not None and node.is_end def startsWith(self, prefix: str) -\u0026gt; bool: return self._find_node(prefix) is not None This does not introduce new logic; it is the runnable version arranged as a submission-ready class.\nE - Engineering: Submission checks and extension points LeetCode interface checklist Before submitting, check the contract rather than only the algorithm idea:\nRequirement Check Class name class Trie: Constructor def __init__(self): creates the root Insert insert mutates the tree and returns None Exact lookup search returns True only when the final node has is_end == True Prefix lookup startsWith returns True when the path exists, without checking is_end The most common accepted-code bug is not traversal. It is mixing up the final check for search and startsWith.\nEdge tests to run before submission With the reference class above, this block should pass:\ntrie = Trie() trie.insert(\u0026#34;apple\u0026#34;) assert trie.search(\u0026#34;apple\u0026#34;) is True assert trie.search(\u0026#34;app\u0026#34;) is False assert trie.startsWith(\u0026#34;app\u0026#34;) is True assert trie.startsWith(\u0026#34;apply\u0026#34;) is False trie.insert(\u0026#34;app\u0026#34;) assert trie.search(\u0026#34;app\u0026#34;) is True assert trie.startsWith(\u0026#34;apple\u0026#34;) is True assert trie.search(\u0026#34;appl\u0026#34;) is False These cases cover:\na full word that exists a prefix that is not yet a full word a prefix that should pass startsWith a longer string whose path breaks inserting a word that is already a prefix of another word How later Trie problems extend this solution LeetCode 208 stores only the minimal state:\nchildren: next character -\u0026gt; child node is_end: whether this path is a complete word Later problems usually keep the same traversal but add one field:\nNeed Typical extra field Count words ending at a node end_count Count words passing through a prefix pass_count Support delete counts plus careful decrement logic Prune DFS by prefix reuse _find_node idea or walk Trie nodes during DFS Do not add those fields to LeetCode 208. They are useful extensions, but this problem only asks for the basic template.\nFAQ Why not use a hash set for LeetCode 208? A hash set handles search(word), but startsWith(prefix) would need scanning all words or storing all prefixes separately. Trie stores shared prefixes directly.\nWhy keep _find_node if LeetCode does not require it? It names the shared traversal used by search and startsWith. The public interface stays exactly the same, and the helper keeps the two final checks from drifting apart.\nShould startsWith check is_end? No. startsWith(\u0026quot;app\u0026quot;) should be true after inserting only \u0026quot;apple\u0026quot;, even before \u0026quot;app\u0026quot; itself is inserted.\nExplanation Why is LeetCode 208 a template problem? The three methods cover the three basic Trie actions:\nMethod Meaning Checks is_end? insert create the path and mark the final node set to True at the end search check a full-word path yes startsWith check a prefix path no Once these are stable, later Trie problems usually extend node fields:\ncount how many words pass through a prefix maintain counts while deleting words use Trie as a pruning structure in DFS Why not use a hash set? A hash set makes search(word) fast. But startsWith(prefix) becomes awkward:\nscan all words or store every possible prefix separately Trie naturally merges common prefixes, so prefix lookup only walks the characters of the prefix.\nThe smallest counterexample for search vs startsWith Insert only:\napple Then:\nsearch(\u0026#34;app\u0026#34;) -\u0026gt; False startsWith(\u0026#34;app\u0026#34;) -\u0026gt; True This counterexample checks whether is_end is used correctly.\nR - Reflection Complexity Let L be the string length:\ninsert: O(L) time search: O(L) time startsWith: O(L) time space: O(total_chars), more precisely the number of distinct prefix nodes Common mistakes Making search return True whenever the path exists Making startsWith check is_end, which rejects valid prefixes Rebuilding nodes even when a child already exists during insertion Forgetting to set is_end at the end of insert Changing the required method name from startsWith to starts_with in the LeetCode submission Adding counters or delete logic before the basic interface is correct When should children be an array? If the character set is fixed to lowercase English letters, you can write:\nself.children = [None] * 26 Then use idx = ord(ch) - ord(\u0026quot;a\u0026quot;) to access the child. The array version has a smaller constant factor, but the index conversion can obscure the idea. This guide uses dict because it directly expresses \u0026ldquo;character -\u0026gt; child node.\u0026rdquo;\nS - Summary LeetCode 208 is the basic Trie template problem. The insert invariant is: node always points to the processed prefix, and missing paths are created. search requires path existence plus is_end == True. startsWith requires only prefix path existence. Once 208 is comfortable, Trie + DFS and prefix-counting problems become much easier. Further Practice Trie Template: Node Fields, Child Traversal, and Loop Invariants Word Search II: use Trie to prune impossible DFS branches Prefix-counting problems: add pass_count or end_count to nodes Delete operation: keep counts so removing one word does not break another shared prefix ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/trie/208-implement-trie-prefix-tree/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nLeetCode 208 is the fixed-interface version of the Trie template. We will derive the node fields and lookup invariant first, then fit them into \u003ccode\u003eTrie()\u003c/code\u003e, \u003ccode\u003einsert\u003c/code\u003e, \u003ccode\u003esearch\u003c/code\u003e, and \u003ccode\u003estartsWith\u003c/code\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003eTrie\u003c/code\u003e, \u003ccode\u003eprefix tree\u003c/code\u003e, \u003ccode\u003eLeetCode 208\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 208, Implement Trie, Prefix Tree, Python Trie, startsWith, is_end\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A pressure-first Python guide to LeetCode 208 covering Trie nodes, children, is_end, insert, search, startsWith, and submission checks.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm\"\u003eA - Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-setup-design-a-prefix-tree-word-index\"\u003eProblem setup: design a prefix-tree word index\u003c/h3\u003e\n\u003cp\u003eThe task is to design a mutable word index.\nThe index starts empty, then receives words one by one, and must answer two kinds of lookup:\u003c/p\u003e","title":"LeetCode 208: Implement Trie (Prefix Tree) in Python"},{"content":" Subtitle / Summary Build a Trie from a small word-index task instead of memorizing the final class. The key model is: a node represents a prefix reached by a path, and is_end decides whether that prefix is also a complete word.\nReading time: 10-12 min Tags: Hot100, Trie, prefix tree, string SEO keywords: Trie template, Python Trie, prefix tree, children, is_end, LeetCode 208 Meta description: A minimal Python Trie template that derives node fields, child traversal, end markers, prefix search, and insert/search loop invariants. A - Algorithm: Start From A Small Task Tiny task: build a word index for lookup and autocomplete Suppose we are building a small word index for a search box. After adding:\napp apple bat the index must answer two kinds of user-facing queries:\nsearch(\u0026quot;app\u0026quot;): was app added as a complete word? starts_with(\u0026quot;ap\u0026quot;): can autocomplete suggest words that begin with ap? search(\u0026quot;apply\u0026quot;): should return false because this word was never added. This is the smallest task that makes Trie useful:\nA hash set can answer full-word existence quickly, but it does not naturally answer prefix existence. A raw path check can confuse \u0026ldquo;this prefix exists\u0026rdquo; with \u0026ldquo;this full word exists.\u0026rdquo; A Trie solves both: it shares common prefixes for autocomplete while still distinguishing \u0026ldquo;prefix path exists\u0026rdquo; from \u0026ldquo;full word exists.\u0026rdquo;\nDerive the operations from the pressure Before caring about any platform interface, define the template directly:\ninsert(word): insert a word search(word): check whether a full word exists starts_with(prefix): check whether any inserted word starts with prefix Minimal structure picture After inserting app and apple, the structure can be viewed as:\nroot └─ a └─ p └─ p [end] └─ l └─ e [end] The important points are:\nA node does not store the whole word. The path from root to a node forms a prefix. [end] means that this path is also a complete word. Target Readers Learners meeting Trie for the first time Readers who confuse search with prefix search Anyone preparing to implement LeetCode 208 after understanding the template Background / Motivation If all we need is full-word membership, a hash set is enough. But when the question becomes \u0026ldquo;does any word start with this prefix?\u0026rdquo;, the hash set is no longer the natural structure.\nTrie\u0026rsquo;s value is that it does not store every string in isolation. It compresses shared prefixes into shared paths.\nC - Concepts Build the template one capability at a time The current pressure is the word index:\nadd: app, apple, bat ask: search(\u0026#34;app\u0026#34;), starts_with(\u0026#34;ap\u0026#34;), search(\u0026#34;apply\u0026#34;) We will not jump straight to the final class. Each step adds one missing capability to the current version.\nStep 1: Start with only outgoing edges The first thing the index must remember is: from the current prefix, which next characters are possible?\nThat gives the smallest node:\nclass TrieNode: def __init__(self): self.children = {} Here children is a dictionary:\ncharacter -\u0026gt; child node For example, after the root sees \u0026quot;a\u0026quot;, it can store:\nroot.children[\u0026#34;a\u0026#34;] = TrieNode() Now this version can:\nrepresent a path from one prefix to the next prefix share the first character of app and apple It still lacks:\na root object that owns the whole index insertion logic that creates paths a way to tell full words from prefixes Step 2: Add the root and grow paths during insertion The whole Trie needs one starting point. That root represents the empty prefix \u0026quot;\u0026quot;.\nIn the previous version, add a Trie class and an insert method:\nclass TrieNode: def __init__(self): self.children = {} class Trie: def __init__(self): self.root = TrieNode() def insert(self, word: str) -\u0026gt; None: node = self.root for ch in word: if ch not in node.children: node.children[ch] = TrieNode() node = node.children[ch] The insertion loop invariant is:\nBefore processing character i, node points to the node for prefix word[:i], and the path for word[:i] already exists.\nWhen we process ch, we create the missing child if needed, then move into it.\nAfter inserting apple, the path exists:\nroot -\u0026gt; a -\u0026gt; p -\u0026gt; p -\u0026gt; l -\u0026gt; e Now this version can:\ncreate paths for inserted words share the app prefix between app and apple It still lacks:\na way to know whether a path is a complete word This is exactly where the earlier task breaks:\ninsert(\u0026#34;apple\u0026#34;) search(\u0026#34;app\u0026#34;) should be False starts_with(\u0026#34;app\u0026#34;) should be True With paths only, both queries look the same.\nStep 3: Mark the end of a complete word To separate a prefix from a complete word, each node needs one more field:\nclass TrieNode: def __init__(self): self.children = {} self.is_end = False Then insert marks only the final node:\nclass Trie: def __init__(self): self.root = TrieNode() def insert(self, word: str) -\u0026gt; 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 This detail matters. When inserting apple, we should not mark a, ap, app, or appl as full words. Only the final e node represents the complete word apple.\nNow this version can:\nstore the difference between apple as a word and app as only a prefix It still lacks:\nthe two public lookup methods Step 4: Implement search and starts_with directly first Now use the state we already have.\nFor full-word lookup, walk the path and then check is_end:\ndef search(self, word: str) -\u0026gt; bool: node = self.root for ch in word: if ch not in node.children: return False node = node.children[ch] return node.is_end The search lookup invariant is:\nBefore processing character i, node points to the node for prefix word[:i].\nIf the path is missing, the word was not inserted. If the path exists, the final node still must be marked as a complete word.\nFor prefix lookup, walk the same path shape but stop after path existence:\ndef starts_with(self, prefix: str) -\u0026gt; bool: node = self.root for ch in prefix: if ch not in node.children: return False node = node.children[ch] return True The starts_with lookup invariant is:\nBefore processing character i, node points to the node for prefix prefix[:i].\nIf the loop finishes, the prefix exists. There is no is_end check because a prefix does not need to be a full word.\nNow this version can answer the original task:\ninsert(\u0026#34;app\u0026#34;) insert(\u0026#34;apple\u0026#34;) search(\u0026#34;app\u0026#34;) -\u0026gt; True search(\u0026#34;ap\u0026#34;) -\u0026gt; False starts_with(\u0026#34;ap\u0026#34;) -\u0026gt; True search(\u0026#34;apply\u0026#34;) -\u0026gt; False It also exposes the duplication: both lookup methods start at root, walk one character at a time, and fail when an edge is missing.\nStep 5: Extract one shared path lookup helper Now the helper is earned. The repeated traversal is:\nStart at root, consume one character at a time, and stop if the path breaks.\nExtract only that traversal:\ndef _find_node(self, s: str): node = self.root for ch in s: if ch not in node.children: return None node = node.children[ch] return node The lookup loop invariant is:\nBefore processing character i, node points to the node for prefix s[:i], and this prefix path has been found.\nIf a character is missing, the path is broken and _find_node returns None. If the loop finishes, it returns the node for the whole query string.\nNow this version can:\ntell whether a path exists return the exact node reached by a word or prefix The final distinction becomes small.\nFor exact word lookup:\ndef search(self, word: str) -\u0026gt; bool: node = self._find_node(word) return node is not None and node.is_end For autocomplete prefix lookup:\ndef starts_with(self, prefix: str) -\u0026gt; bool: return self._find_node(prefix) is not None The difference is only the final check:\nsearch requires both path existence and is_end starts_with requires only path existence Now this version can answer the original task:\ninsert(\u0026#34;app\u0026#34;) insert(\u0026#34;apple\u0026#34;) search(\u0026#34;app\u0026#34;) -\u0026gt; True search(\u0026#34;ap\u0026#34;) -\u0026gt; False starts_with(\u0026#34;ap\u0026#34;) -\u0026gt; True search(\u0026#34;apply\u0026#34;) -\u0026gt; False At this point the full runnable code has been earned: the helper is part of the final code because duplication forced it out.\nRunnable Example (Python) class TrieNode: def __init__(self): self.children = {} self.is_end = False class Trie: def __init__(self): self.root = TrieNode() def insert(self, word: str) -\u0026gt; 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 _find_node(self, s: str): node = self.root for ch in s: if ch not in node.children: return None node = node.children[ch] return node def search(self, word: str) -\u0026gt; bool: node = self._find_node(word) return node is not None and node.is_end def starts_with(self, prefix: str) -\u0026gt; bool: return self._find_node(prefix) is not None if __name__ == \u0026#34;__main__\u0026#34;: trie = Trie() trie.insert(\u0026#34;app\u0026#34;) trie.insert(\u0026#34;apple\u0026#34;) assert trie.search(\u0026#34;app\u0026#34;) is True assert trie.search(\u0026#34;ap\u0026#34;) is False assert trie.starts_with(\u0026#34;ap\u0026#34;) is True assert trie.search(\u0026#34;apply\u0026#34;) is False Explanation What does root mean? root represents the empty prefix \u0026quot;\u0026quot;. Every word starts from the empty prefix and moves one character at a time.\nThat is why insertion and lookup both begin with:\nnode = self.root Why use a dict for children? In Python, dict is the clearest representation:\nkey: next character value: child node lookup and insertion are average O(1) If the problem guarantees lowercase English letters only, a 26-element array is also possible. The array has a smaller constant factor, but index conversion can distract from the idea. For a learning template, dict is more direct.\nis_end is set only after the last character When inserting apple, we should not mark a, ap, app, and appl as full words. Only after the loop finishes does the current node represent the whole word.\nIf we later insert app, we walk to the same app node and set its is_end to True.\nE - Engineering: Where this shape shows up The same template is useful whenever exact lookup and prefix lookup must stay separate. With the Trie class above, these examples are executable as direct calls.\nAutocomplete prefix gate Before generating expensive suggestions, first check whether the prefix can match anything:\ntrie = Trie() for word in [\u0026#34;app\u0026#34;, \u0026#34;apple\u0026#34;, \u0026#34;bat\u0026#34;]: trie.insert(word) assert trie.starts_with(\u0026#34;ap\u0026#34;) is True assert trie.starts_with(\u0026#34;ba\u0026#34;) is True assert trie.starts_with(\u0026#34;ca\u0026#34;) is False Exact dictionary membership Autocomplete may accept ap as a prefix, but a spelling or command validator may require a complete word:\ntrie = Trie() for word in [\u0026#34;app\u0026#34;, \u0026#34;apple\u0026#34;, \u0026#34;bat\u0026#34;]: trie.insert(word) assert trie.search(\u0026#34;app\u0026#34;) is True assert trie.search(\u0026#34;ap\u0026#34;) is False Namespaced keys Using a dictionary for children keeps the template usable beyond lowercase letters:\ncommands = Trie() for command in [\u0026#34;user:add\u0026#34;, \u0026#34;user:delete\u0026#34;, \u0026#34;team:add\u0026#34;]: commands.insert(command) assert commands.starts_with(\u0026#34;user:\u0026#34;) is True assert commands.search(\u0026#34;user\u0026#34;) is False FAQ Should _find_node be written first? In final code it is fine to keep _find_node, but the concept should be earned from repeated lookup logic. If a helper does not remove real duplication or name a stable operation, inline code is usually clearer.\nDoes each node need to store its own character? Not in this template. The character is already the key used to reach the child node, so storing it again is redundant for basic insert/search/prefix lookup.\nCan children be a set? No for this implementation. A set can tell whether a character exists, but it cannot move to the child node that represents the longer prefix.\nR - Reflection Complexity Let L be the string length:\ninsert: O(L) time, at most L new nodes search: O(L) time, O(1) extra space starts_with: O(L) time, O(1) extra space Total space depends on the number of distinct prefixes across all inserted words. The more prefixes are shared, the more useful the Trie structure becomes.\nCommon mistakes Keeping only children and forgetting is_end, which treats prefixes as full words Continuing after a missing character during lookup Creating a fresh node every time during insertion and overwriting shared prefixes Making starts_with require is_end, which rejects valid prefixes Extracting helpers before the repeated operation is visible Template memory hook You can compress Trie into three sentences:\nA node means \u0026ldquo;we reached this prefix.\u0026rdquo; children[ch] means \u0026ldquo;append character ch and move to that node.\u0026rdquo; is_end means \u0026ldquo;this prefix is exactly a full word.\u0026rdquo; S - Summary Trie is path-based; the character lives on the edge, not necessarily inside the node. children moves to the next prefix; is_end separates prefixes from full words. The insert and lookup invariants both say: node always points to the currently processed prefix. After this template is stable, LeetCode 208 is mostly a fixed-interface version of the same code. Further Practice 208. Implement Trie (Prefix Tree): fit this template into the required class and methods Word Search II: Trie + DFS pruning Prefix-counting problems: add counters such as pass_count or end_count to nodes ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/trie/trie-template/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nBuild a Trie from a small word-index task instead of memorizing the final class. The key model is: a node represents a prefix reached by a path, and \u003ccode\u003eis_end\u003c/code\u003e decides whether that prefix is also a complete word.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003eTrie\u003c/code\u003e, \u003ccode\u003eprefix tree\u003c/code\u003e, \u003ccode\u003estring\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Trie template, Python Trie, prefix tree, children, is_end, LeetCode 208\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A minimal Python Trie template that derives node fields, child traversal, end markers, prefix search, and insert/search loop invariants.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-start-from-a-small-task\"\u003eA - Algorithm: Start From A Small Task\u003c/h2\u003e\n\u003ch3 id=\"tiny-task-build-a-word-index-for-lookup-and-autocomplete\"\u003eTiny task: build a word index for lookup and autocomplete\u003c/h3\u003e\n\u003cp\u003eSuppose we are building a small word index for a search box.\nAfter adding:\u003c/p\u003e","title":"Trie Template Guide: Node Fields, Child Traversal, and Loop Invariants"},{"content":"Problem Requirement You are given an n x n matrix isConnected, where there are n cities.\nIf isConnected[i][j] == 1, city i and city j are directly connected. If two cities can reach each other through one or more directly connected cities, they belong to the same province.\nThe problem asks you to return the total number of provinces.\nThe easiest mistake is to think that the answer is the number of 1s in the matrix, or only the number of directly connected pairs. That is not what the problem asks.\nThe real question is:\nHow many groups of cities are connected, either directly or indirectly?\nIn graph language:\nGiven an undirected graph represented by an adjacency matrix, return the number of connected components.\nInput / Output Input: isConnected: List[List[int]] Output: province count as an int City IDs can be understood as 0..n-1. isConnected[i][j] == 1 means there is an edge between city i and city j. isConnected[i][j] == 0 means city i and city j are not directly connected. Example 1 Input: isConnected = [ [1, 1, 0], [1, 1, 0], [0, 0, 1] ] Output: 2 City 0 and city 1 are directly connected, so they belong to the same province.\nCity 2 is only connected to itself. It cannot be reached from city 0 or city 1, so it forms another province by itself.\nThere are two provinces:\n{0, 1} {2} The answer is 2.\nExample 2 Input: isConnected = [ [1, 0, 0], [0, 1, 0], [0, 0, 1] ] Output: 3 No two different cities are directly connected. Each city can only reach itself, so every city is its own province:\n{0} {1} {2} The answer is 3.\nConstraints 1 \u0026lt;= n \u0026lt;= 200 isConnected.length == n isConnected[i].length == n isConnected[i][j] is either 0 or 1 isConnected[i][i] == 1 isConnected[i][j] == isConnected[j][i] What This Section Freezes We have translated the problem from a matrix problem into a connected component counting problem.\nAt this checkpoint, we can:\nUnderstand that 1 in the matrix means direct connection. Understand that a province may contain cities connected through intermediate cities. Understand that the target is not to count edges, but to count connected components. Explain why the two official examples return 2 and 3. It still lacks:\nHow to find the full province that one city belongs to. How to avoid counting the same province more than once. How to turn the idea into runnable code. Step 1: Solve a Smaller Problem First The current baseline is: we know the target is to count connected components, but we still do not know how to \u0026ldquo;get one province\u0026rdquo;.\nAsking \u0026ldquo;how many provinces are there in total?\u0026rdquo; is still too large. First shrink the problem:\nGiven one city that has not been processed yet, how do we find all cities that belong to the same province?\nLook at Example 1:\nisConnected = [ [1, 1, 0], [1, 1, 0], [0, 0, 1] ] If we start from city 0:\nisConnected[0][0] == 1, so city 0 belongs to the current province. isConnected[0][1] == 1, so city 1 is directly connected to city 0 and also belongs to the current province. isConnected[0][2] == 0, so city 2 cannot be reached directly from city 0. Then continue from city 1:\nisConnected[1][0] == 1, and city 0 is already in the current province. isConnected[1][1] == 1, and city 1 is already in the current province. isConnected[1][2] == 0, so city 2 still does not belong to the current province. Starting from city 0, we can find the full province:\n{0, 1} The important part of this step is not the final answer. It is the intermediate capability:\nStarting from one city, mark every city that can be reached directly or indirectly.\nTo do that, we need a visited array:\nvisited[i] == True means city i has already been assigned to a discovered province visited[i] == False means city i has not been assigned to any discovered province yet If we start from an unvisited city and mark every reachable city as visited, that one traversal has found one complete province.\nThe smaller problem can be written as pseudocode:\nvisit(city): mark city as visited for next_city in all cities: if city is connected to next_city and next_city is not visited: visit(next_city) This is the entry point for graph traversal. It can be written with DFS or BFS.\nFor now, freeze only this smaller problem. Do not rush into the full answer yet.\nAt this checkpoint, we can:\nStart from \u0026ldquo;find one province\u0026rdquo; instead of jumping straight to the final count. Understand that visited prevents duplicate processing. Understand that one traversal marks every city in the same province. It still lacks:\nHow to write visit(city) in Python. How to scan all cities and count provinces. Step 2: Use DFS to Find Every Province The previous baseline was:\nvisit(city) can start from one city and mark every city in the same province. But it still cannot answer the final question. The problem asks for the total number of provinces, not only the province containing city 0.\nThis breaks in Example 1:\n{0, 1} {2} If we only start from city 0, we only get {0, 1}. City 2 is never processed.\nSo we need to add an outer scan on top of the previous version:\nCheck every city from left to right. If a city is not visited yet, we have found a new province. Increment the answer, then start DFS from that city to mark the whole province.\nThis can be written directly as LeetCode code:\nfrom typing import List class Solution: def findCircleNum(self, isConnected: List[List[int]]) -\u0026gt; int: n = len(isConnected) visited = [False] * n def dfs(city: int) -\u0026gt; None: visited[city] = True for next_city in range(n): if isConnected[city][next_city] == 1 and not visited[next_city]: dfs(next_city) provinces = 0 for city in range(n): if not visited[city]: provinces += 1 dfs(city) return provinces There are two actions in this code.\nThe first action is dfs(city):\ndef dfs(city: int) -\u0026gt; None: visited[city] = True for next_city in range(n): if isConnected[city][next_city] == 1 and not visited[next_city]: dfs(next_city) It has one job: starting from city, mark every city in the same province as visited.\nThe second action is the outer scan:\nprovinces = 0 for city in range(n): if not visited[city]: provinces += 1 dfs(city) The condition if not visited[city] is the key.\nIf a city has already been visited, it already belongs to a previously discovered province and must not be counted again.\nIf a city has not been visited, then none of the previously discovered provinces can reach it. It must start a new province. So we first do provinces += 1, then use dfs(city) to mark that entire new province.\nWalk through Example 1:\nvisited = [False, False, False] provinces = 0 city = 0: not visited, provinces = 1 dfs(0) marks 0 and 1 visited = [True, True, False] city = 1: already visited, skip city = 2: not visited, provinces = 2 dfs(2) marks 2 visited = [True, True, True] The final answer is 2.\nNow look at Example 2:\nisConnected = [ [1, 0, 0], [0, 1, 0], [0, 0, 1] ] No city can reach any other city. The outer scan starts DFS at city 0, city 1, and city 2, so the answer is 3.\nAt this checkpoint, we can:\nUse DFS to find the full province starting from one city. Use an outer scan to count all provinces. Correctly handle connected cities and isolated cities. It still lacks:\nWhy this count never misses a province and never counts one twice. Time complexity and space complexity. Step 3: Why the DFS Count Misses Nothing and Counts Nothing Twice Now we have a DFS solution that passes the examples. But passing examples is not enough. The tricky part of this problem is not syntax. It is this counting logic:\nfor city in range(n): if not visited[city]: provinces += 1 dfs(city) Why can we increment provinces immediately when we see an unvisited city?\nStart with a wrong idea: increment the answer every time we see a connected edge.\nFor example:\n0 -- 1 -- 2 The matrix may show that 0 is connected to 1, and 1 is connected to 2. If we count edges, we get two connections. But all three cities belong to one province, so the answer should be 1.\nA province is not the number of edges. A province is the whole group that can be reached from one entry point.\nThe DFS code depends on three invariants.\nThe first invariant:\nAfter dfs(city) finishes, every city reachable from city is marked as visited = True.\nThe reason is that dfs checks every possible next_city from city. If there is an edge and next_city has not been visited, it continues DFS from next_city. This expansion covers both direct and indirect connections.\nThe second invariant:\nIf the outer scan sees visited[city] == False, then city does not belong to any province discovered earlier.\nEvery time we discover a province, we immediately call DFS and mark every city in that province. If city is still unvisited, all previous DFS traversals failed to reach it. It must start a new province.\nThe third invariant:\nOne province is counted exactly once.\nWhen the outer scan first reaches a province, the answer is incremented once, and DFS marks the whole province. Later, when the scan reaches other cities in that same province, they are already visited = True and are skipped.\nPut together, these invariants prove the counting logic:\nNo missing province: the outer scan checks every city, and any unvisited city starts a DFS. No duplicate province: once a city is marked by DFS, it cannot start another count later. The count is a province count: each increment corresponds to one full connected component, not one edge or one 1 in the matrix. Complexity isConnected is an adjacency matrix. When DFS processes one city, it scans the entire row:\nfor next_city in range(n): Each city enters dfs at most once because it is marked as visited. Each entry scans n possible neighbors, so the total time complexity is:\nO(n * n) = O(n^2) Do not write this as O(n + m). O(n + m) is a better fit for adjacency lists. This problem gives an adjacency matrix, and the matrix itself has n^2 cells, so matrix scanning dominates.\nSpace complexity comes from two parts:\nvisited array: O(n) DFS recursion stack: worst-case depth O(n) So the space complexity is:\nO(n) At this checkpoint, we can:\nExplain why seeing an unvisited city means we can increment the province count. Explain why DFS does not miss indirectly connected cities. Explain why the same province is not counted twice. Give the matrix-based O(n^2) time complexity and O(n) space complexity. It still lacks:\nHow to write the same graph traversal with BFS if we do not want recursion. How to write the solution with Union Find if we think in terms of merging sets. Step 4: The Same Idea as BFS The DFS version is already enough to solve the problem. BFS is not a new idea here. It is the same action, \u0026ldquo;expand from one entry city to the whole province\u0026rdquo;, written with a different container.\nThe current baseline is:\nfind an unvisited city -\u0026gt; provinces += 1 -\u0026gt; use DFS to mark the whole province If you do not want recursion, put \u0026ldquo;cities to visit next\u0026rdquo; into a queue. Every time you pop one city from the queue, scan all neighbors it can reach. If a neighbor has not been visited yet, mark it and push it into the queue.\nIn other words, replace this part:\ndfs(city) with:\nbfs(city) Here is the BFS version:\nfrom collections import deque from typing import List class Solution: def findCircleNum(self, isConnected: List[List[int]]) -\u0026gt; int: n = len(isConnected) visited = [False] * n def bfs(start: int) -\u0026gt; None: visited[start] = True queue = deque([start]) while queue: city = queue.popleft() for next_city in range(n): if isConnected[city][next_city] == 1 and not visited[next_city]: visited[next_city] = True queue.append(next_city) provinces = 0 for city in range(n): if not visited[city]: provinces += 1 bfs(city) return provinces Notice that visited[start] = True happens before enqueueing the start node, and visited[next_city] = True happens when a neighbor is discovered.\nThis prevents the same city from being pushed into the queue multiple times. For this problem, duplicate queue entries may still produce the correct final answer, but they create unnecessary work. The cleaner rule is: once a city is handed to the queue, mark it immediately.\nWalk through Example 1 with BFS:\ncity = 0 is not visited: provinces = 1 queue = [0] pop 0: discover 1, mark it and enqueue it queue = [1] pop 1: 0 is visited, 1 is visited, 2 is not connected queue = [] city = 1 is already visited, skip city = 2 is not visited: provinces = 2 queue = [2] pop 2 and finish BFS and DFS use exactly the same outer counting logic:\nfor city in range(n): if not visited[city]: provinces += 1 bfs(city) So their correctness comes from the same fact: every time we start from an unvisited city, we mark one complete new province.\nThe complexity is also the same:\nTime complexity: O(n^2) Space complexity: O(n) At this checkpoint, we can:\nRewrite recursive DFS as queue-based BFS. Preserve the same outer scan and visited meaning. Explain why we mark a city as visited when it is discovered. It still lacks:\nHow to solve the same problem from the viewpoint of \u0026ldquo;connected cities should be merged into one set\u0026rdquo;. Step 5: Merge Connected Cities with Union Find DFS and BFS both do the same thing:\nStart from one city and find every city in the same province. Union Find uses a different viewpoint:\nAt first, every city is its own province. Whenever we see a connection, merge the two cities\u0026#39; provinces. The number of remaining sets is the number of provinces. Use Example 1:\nInitial: {0} {1} {2} See that 0 and 1 are connected: {0, 1} {2} There is no other cross-set connection. 2 sets remain. That is why Union Find fits this problem. The input gives connection relations, and Union Find is built for this question:\nIf two elements are related, merge them into the same set.\nThe code needs three pieces.\nFirst, parent[i] stores the representative of the set containing city i:\nparent = list(range(n)) At the beginning, every city is its own representative.\nSecond, find(city) finds the final representative of the set containing a city:\ndef find(city: int) -\u0026gt; int: if parent[city] != city: parent[city] = find(parent[city]) return parent[city] The line parent[city] = find(parent[city]) is path compression. It does not change the answer. It only makes later lookups faster.\nThird, union(a, b) merges the sets containing two cities. If they already belong to the same set, do nothing. If they belong to different sets, merge them and reduce the province count by one:\ndef union(a: int, b: int) -\u0026gt; None: nonlocal provinces root_a = find(a) root_b = find(b) if root_a == root_b: return parent[root_b] = root_a provinces -= 1 Here is the complete code:\nfrom typing import List class Solution: def findCircleNum(self, isConnected: List[List[int]]) -\u0026gt; int: n = len(isConnected) parent = list(range(n)) provinces = n def find(city: int) -\u0026gt; int: if parent[city] != city: parent[city] = find(parent[city]) return parent[city] def union(a: int, b: int) -\u0026gt; None: nonlocal provinces root_a = find(a) root_b = find(b) if root_a == root_b: return parent[root_b] = root_a provinces -= 1 for i in range(n): for j in range(i + 1, n): if isConnected[i][j] == 1: union(i, j) return provinces The inner loop starts from i + 1:\nfor j in range(i + 1, n): The matrix is symmetric. isConnected[i][j] and isConnected[j][i] describe the same undirected edge. It is enough to scan the upper triangle. The diagonal isConnected[i][i] == 1 means a city is connected to itself, so it does not need a merge.\nThe difference between Union Find and DFS/BFS is:\nDFS/BFS: find one entry point, then expand to the whole province. Union Find: see one edge, then merge the two sets. But they freeze the same core fact:\nA province is a connected component.\nFor complexity, this code still scans the upper triangle of the adjacency matrix, so the time complexity is:\nO(n^2) Union Find with path compression makes find/union almost constant time. But in this problem, matrix scanning already costs O(n^2), so total time is still dominated by the matrix.\nSpace complexity is the parent array:\nO(n) At this checkpoint, we can:\nUnderstand province counting from the \u0026ldquo;merge connected sets\u0026rdquo; viewpoint. Write the full Union Find solution. Explain why scanning only the upper triangle is enough. Explain why one successful merge reduces the province count by one. It still lacks:\nCompare the three methods and give a practical recommendation. Close with common mistakes so the reader does not solve the wrong problem again. Which Method Should You Use? For this problem, learn DFS first.\nThe reason is simple: the essence of the problem is connected components, and DFS directly expresses \u0026ldquo;start from one city and expand to the whole province\u0026rdquo;. Once you can write DFS, BFS is only replacing the recursion stack with a queue, and Union Find is only changing the viewpoint to set merging.\nYou can choose among the three methods like this:\nMethod When to use it Core action DFS First time learning connected components, or writing the main interview solution quickly Start from an unvisited city and recursively mark the whole province BFS You do not want recursion, or you want explicit control over the visiting queue Start from an unvisited city and use a queue to mark the whole province Union Find You want to practice set merging, or the problem keeps giving connection relations Every time you see a connection, merge the two cities\u0026rsquo; sets For LeetCode 547, DFS is usually the clearest answer. The constraint is only n \u0026lt;= 200, so recursion depth is at most 200 and is not a practical issue.\nCommon Mistakes First mistake: treating the number of 1s in the matrix as the answer.\n1 only means direct connection. A province may be formed through indirect connections:\n0 -- 1 -- 2 These three cities form one province. The answer is not two edges and not three self-connections.\nSecond mistake: starting traversal only from city 0.\nIf the graph is not fully connected, starting from 0 only finds the province containing city 0. The outer scan is required:\nfor city in range(n): if not visited[city]: provinces += 1 dfs(city) Third mistake: forgetting visited.\nIn an undirected graph, 0 can reach 1, and 1 can go back to 0. Without visited, DFS/BFS keeps walking back and forth.\nFourth mistake: incrementing the answer for an already visited city.\nOnly an unvisited city can start a new province:\nif not visited[city]: provinces += 1 If a city is already visited, it already belongs to a previous province and must not be counted again.\nFifth mistake: writing adjacency-list complexity for a matrix problem.\nThis problem gives an adjacency matrix. Even if the graph is sparse, the code scans matrix rows to check connections, so DFS, BFS, and Union Find can all be recorded as:\nTime complexity: O(n^2) Space complexity: O(n) Final Recap This problem can be compressed into one sentence:\nScan all cities. Every time you find an unvisited city, you have found a new connected component. Use DFS or BFS to mark that entire component.\nFor DFS, the final idea is:\nvisited records whether a city has already been assigned to a province the outer loop scans every city when an unvisited city appears, increment the province count start DFS from that city and mark the whole province For Union Find, the final idea is:\nat first, every city is its own province whenever there is a connection, merge the two sets each successful merge reduces the province count by one the remaining set count is the answer At this point, the tutorial has frozen three complete solutions:\nDFS: the main solution, and the one to learn first. BFS: the same traversal idea with an explicit queue. Union Find: the same connected component problem from a set-merging viewpoint. The important thing to remember is not one specific template, but the translation:\nNumber of provinces = number of connected components in an undirected graph ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/547-number-of-provinces/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an \u003ccode\u003en x n\u003c/code\u003e matrix \u003ccode\u003eisConnected\u003c/code\u003e, where there are \u003ccode\u003en\u003c/code\u003e cities.\u003c/p\u003e\n\u003cp\u003eIf \u003ccode\u003eisConnected[i][j] == 1\u003c/code\u003e, city \u003ccode\u003ei\u003c/code\u003e and city \u003ccode\u003ej\u003c/code\u003e are \u003cstrong\u003edirectly connected\u003c/strong\u003e. If two cities can reach each other through one or more directly connected cities, they belong to the same province.\u003c/p\u003e\n\u003cp\u003eThe problem asks you to return the total number of provinces.\u003c/p\u003e\n\u003cp\u003eThe easiest mistake is to think that the answer is the number of \u003ccode\u003e1\u003c/code\u003es in the matrix, or only the number of directly connected pairs. That is not what the problem asks.\u003c/p\u003e","title":"LeetCode 547: Number of Provinces, Turn an Adjacency Matrix into Connected Components"},{"content":"Problem Input and Output Input: an integer array nums nums[i] is the money in the i-th house Adjacent houses cannot both be robbed Output: return the maximum amount of money that can be robbed Constraints: 1 \u0026lt;= nums.length \u0026lt;= 100, 0 \u0026lt;= nums[i] \u0026lt;= 400 Examples Input: nums = [1,2,3,1] Output: 4 Explanation: rob indices 0 and 2, for 1 + 3 = 4 Input: nums = [2,7,9,3,1] Output: 12 Explanation: rob indices 0, 2, and 4, for 2 + 9 + 1 = 12 This article uses Python only and derives 1D DP from the conflict between two choices.\nStart from the Adjacent Conflict in [1,2,3,1] Look at the example:\nnums = [1,2,3,1] If we rob the house at index 2, whose money is 3, then indices 1 and 3 cannot be robbed. If we do not rob index 2, the answer may come from the best result among indices 0..1.\nSo when we reach a house, the core choice has only two cases:\nrob the current house: we must skip the previous house skip the current house: we reuse the best result before it Step 1: Define the Best Value Up to House i Asking \u0026ldquo;how much can we rob from the whole street\u0026rdquo; is too large at first. Define:\ndp[i] = maximum money we can rob considering houses 0..i This definition has one benefit: when processing house i, the previous prefix already has a stable optimal value.\nStart with the smallest skeleton:\ndef rob(nums: list[int]) -\u0026gt; int: n = len(nums) dp = [0] * n This version can:\nreserve one optimal value for each prefix 0..i make it clear that dp[i] means \u0026ldquo;up to house i\u0026rdquo;, not \u0026ldquo;must rob house i\u0026rdquo; It still needs:\nthe base cases for the first one or two houses the transition between robbing and skipping the current house Step 2: Handle the First and Second Houses Build on the previous version and fill the base cases.\nIf we only look at house 0, the most we can rob is that house:\ndp[0] = nums[0] If we look at houses 0..1, adjacent houses cannot both be robbed, so we take the larger one:\nif n == 1: return dp[0] dp[1] = max(nums[0], nums[1]) The current code is:\ndef rob(nums: list[int]) -\u0026gt; int: n = len(nums) dp = [0] * n dp[0] = nums[0] if n == 1: return dp[0] dp[1] = max(nums[0], nums[1]) This version can:\nhandle n = 1 correctly handle the adjacent conflict when there are only two houses It still needs:\nhow to decide \u0026ldquo;rob or skip\u0026rdquo; when i \u0026gt;= 2 Step 3: House i Has Only Two Choices Now consider i \u0026gt;= 2.\nIf we skip house i, the best value is:\ndp[i - 1] If we rob house i, then house i - 1 cannot be robbed, so we can only add:\ndp[i - 2] + nums[i] Add this transition to the previous version:\nfor i in range(2, n): dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]) The first complete correct version is:\ndef rob(nums: list[int]) -\u0026gt; int: n = len(nums) dp = [0] * n dp[0] = nums[0] if n == 1: return dp[0] dp[1] = max(nums[0], nums[1]) for i in range(2, n): dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]) return dp[n - 1] This version can:\ncompute the maximum amount for every prefix make each step explicitly compare \u0026ldquo;skip current\u0026rdquo; and \u0026ldquo;rob current\u0026rdquo; It still has room for:\nspace optimization, because each state only depends on the previous two prefix states Step 4: Walk Through the Table Slowly For nums = [2,7,9,3,1]:\ni nums[i] Skip current dp[i - 1] Rob current dp[i - 2] + nums[i] dp[i] 0 2 - 2 2 1 7 - - 7 2 9 7 2 + 9 = 11 11 3 3 11 7 + 3 = 10 11 4 1 11 11 + 1 = 12 12 The answer is:\ndp[4] = 12 The important point in this table is:\ndp[i] does not require house i to be robbed. It means the global optimum up to house i.\nStep 5: Compress the dp Array into Two Variables The previous transition only depends on:\ndp[i - 2] dp[i - 1] So keep two variables:\nprev2: the previous round\u0026rsquo;s dp[i - 2] prev1: the previous round\u0026rsquo;s dp[i - 1] Replace the array with:\nprev2 = nums[0] prev1 = max(nums[0], nums[1]) for i in range(2, n): cur = max(prev1, prev2 + nums[i]) prev2 = prev1 prev1 = cur The final complete code is:\nclass Solution: def rob(self, nums: list[int]) -\u0026gt; int: n = len(nums) if n == 1: return nums[0] prev2 = nums[0] prev1 = max(nums[0], nums[1]) for i in range(2, n): cur = max(prev1, prev2 + nums[i]) prev2 = prev1 prev1 = cur return prev1 if __name__ == \u0026#34;__main__\u0026#34;: print(Solution().rob([1, 2, 3, 1])) print(Solution().rob([2, 7, 9, 3, 1])) This version can:\nkeep the same state meaning as the dp table version reduce extra space from O(n) to O(1) It still has one boundary:\nif the houses form a circle, the problem becomes 213. House Robber II, and we need to split it into two linear ranges; this problem is a straight line Correctness Invariants:\nafter processing house i, prev1 equals dp[i], the maximum money among indices 0..i prev2 equals the previous round\u0026rsquo;s dp[i - 1], used for the next transition Why the transition is correct:\nAny optimal plan for house i has only two cases: rob it or skip it. If we skip house i, the best value is the prefix optimum dp[i - 1]. If we rob house i, house i - 1 cannot be robbed, so the value is dp[i - 2] + nums[i]. These two cases cover all legal plans, so take the maximum. Why return prev1:\nafter the loop, prev1 corresponds to the prefix optimum at the last index that is exactly the maximum money for the whole street Complexity Time complexity: O(n) Extra space: O(1) Common Mistakes Defining dp[i] as \u0026ldquo;must rob house i\u0026rdquo; but writing a transition like max(dp[i - 1], ...), which conflicts with the state meaning. Forgetting to handle n = 1. When robbing the current house, adding dp[i - 1], which violates the adjacent-house constraint. Mixing this problem with the circular House Robber problem; in this problem, the first and last houses are not adjacent. Summary dp[i] means the maximum money after considering houses up to index i. Each position has only two decisions: rob current or skip current. Robbing current must connect to dp[i - 2]; skipping current is dp[i - 1]. Space optimization only keeps the previous two DP states. References and Follow-up LeetCode 198: House Robber LeetCode 213: House Robber II LeetCode 70: Climbing Stairs LeetCode 746: Min Cost Climbing Stairs Notes The problem statement, examples, and constraints are based on the public LeetCode 198 summary. Python is used to match the current LeetCode tutorial style in this repository. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/198-house-robber/","summary":"\u003ch2 id=\"problem\"\u003eProblem\u003c/h2\u003e\n\u003ch3 id=\"input-and-output\"\u003eInput and Output\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eInput: an integer array \u003ccode\u003enums\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003enums[i]\u003c/code\u003e is the money in the \u003ccode\u003ei\u003c/code\u003e-th house\u003c/li\u003e\n\u003cli\u003eAdjacent houses cannot both be robbed\u003c/li\u003e\n\u003cli\u003eOutput: return the maximum amount of money that can be robbed\u003c/li\u003e\n\u003cli\u003eConstraints: \u003ccode\u003e1 \u0026lt;= nums.length \u0026lt;= 100\u003c/code\u003e, \u003ccode\u003e0 \u0026lt;= nums[i] \u0026lt;= 400\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"examples\"\u003eExamples\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [1,2,3,1]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 4\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: rob indices 0 and 2, for 1 + 3 = 4\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [2,7,9,3,1]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 12\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: rob indices 0, 2, and 4, for 2 + 9 + 1 = 12\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis article uses Python only and derives 1D DP from the conflict between two choices.\u003c/p\u003e","title":"LeetCode 198: House Robber, Deriving 1D DP from Rob or Skip"},{"content":"Problem Input and Output Input: an integer n Meaning: climb to the top of the n-step staircase Each move can climb 1 or 2 steps Output: return the number of distinct ways to reach the top Constraints: 1 \u0026lt;= n \u0026lt;= 45 Examples Input: n = 2 Output: 2 Explanation: 1+1, 2 Input: n = 3 Output: 3 Explanation: 1+1+1, 1+2, 2+1 This article uses Python only and derives the final solution step by step from the meaning of dp[i].\nStart from the Last Move of n = 3 Look at the smallest example that exposes the transition:\nn = 3 The last move to step 3 can only come from:\nstep 2, then climb 1 step step 1, then climb 2 steps So the number of ways to reach step 3 is not created from nowhere. It comes from two smaller positions.\nStep 1: Define the Smaller Problem Asking \u0026ldquo;how many ways are there to reach step n\u0026rdquo; is too large at first. Define:\ndp[i] = number of ways to reach step i Here i means a staircase position, not merely an array index.\nStart with the smallest skeleton:\ndef climb_stairs(n: int) -\u0026gt; int: dp = [0] * (n + 1) This version can:\nreserve a state for every position 0..n make it clear that dp[i] means \u0026ldquo;number of ways to reach step i\u0026rdquo; It still needs:\nthe base cases for the start and step 1 the transition for later positions Step 2: Fill the Start and Step 1 Build on the previous version and fill the base cases.\nStanding at step 0 without moving can be understood as one way to already be at the start:\ndp[0] = 1 To reach step 1, there is only one move:\ndp[1] = 1 The current code is:\ndef climb_stairs(n: int) -\u0026gt; int: dp = [0] * (n + 1) dp[0] = 1 dp[1] = 1 This version can:\nhandle n = 1 correctly provide valid sources for the later transition It still needs:\nhow to reach i \u0026gt;= 2 from previous positions Step 3: Step i Can Only Come from i - 1 or i - 2 Now consider i \u0026gt;= 2.\nIf the last move climbs 1 step, the previous position is i - 1. If the last move climbs 2 steps, the previous position is i - 2.\nThese two groups do not overlap because their last move lengths are different.\nTherefore the transition is dp[i] = dp[i - 1] + dp[i - 2].1\nAdd this transition to the previous version:\nfor i in range(2, n + 1): dp[i] = dp[i - 1] + dp[i - 2] The first complete table-based solution is:\ndef climb_stairs(n: int) -\u0026gt; int: dp = [0] * (n + 1) dp[0] = 1 dp[1] = 1 for i in range(2, n + 1): dp[i] = dp[i - 1] + dp[i - 2] return dp[n] This version can:\ncompute the number of ways for every staircase position return dp[n] directly It still has room for:\nspace optimization, because each state only depends on the previous two states Step 4: Walk Through the Table Slowly For n = 5:\ni Source dp[i] 0 start 1 1 only climb 1 step 1 2 dp[1] + dp[0] 2 3 dp[2] + dp[1] 3 4 dp[3] + dp[2] 5 5 dp[4] + dp[3] 8 The important point in this table is:\ndp[i] always means \u0026ldquo;number of ways to reach step i\u0026rdquo;, not \u0026ldquo;what the i-th move does\u0026rdquo;.\nStep 5: Compress the dp Array into Two Variables The previous transition is:\ndp[i] = dp[i - 1] + dp[i - 2] It only depends on the previous two positions. Name them:\nprev2: dp[i - 2] prev1: dp[i - 1] Replace the whole table with two variables:\nprev2 = 1 prev1 = 1 for _ in range(2, n + 1): cur = prev1 + prev2 prev2 = prev1 prev1 = cur The final complete code is:\nclass Solution: def climbStairs(self, n: int) -\u0026gt; int: if n == 1: return 1 prev2 = 1 prev1 = 1 for _ in range(2, n + 1): cur = prev1 + prev2 prev2 = prev1 prev1 = cur return prev1 if __name__ == \u0026#34;__main__\u0026#34;: print(Solution().climbStairs(2)) print(Solution().climbStairs(3)) This version can:\nkeep the same state transition as the dp table version reduce extra space from O(n) to O(1) It still has one boundary:\nif the problem allowed more step sizes, there would be more transition sources; this problem only allows 1 or 2 steps Correctness Invariant:\nwhen processing position i, prev1 equals dp[i] and prev2 equals dp[i - 1] Why the transition is correct:\nAny way to reach step i must end with either a 1-step move or a 2-step move. Ways whose last move is 1 step correspond to dp[i - 1]. Ways whose last move is 2 steps correspond to dp[i - 2]. The two groups have different last move lengths, so they do not overlap. Add them. Complexity Time complexity: O(n) Extra space: O(1) Common Mistakes Setting dp[0] to 0, which makes n = 2 miss the direct 2-step climb. Forgetting to handle n = 1; the space-optimized version may return an undefined cur. Understanding dp[i] as \u0026ldquo;what the i-th move chooses\u0026rdquo; instead of \u0026ldquo;number of ways to reach step i\u0026rdquo;. Summary The first step of 1D DP is to define the meaning of dp[i]. In Climbing Stairs, dp[i] means the number of ways to reach step i. Step i can only come from i - 1 or i - 2. Space optimization only keeps dp[i - 2] and dp[i - 1] as two variables. References and Follow-up LeetCode 70: Climbing Stairs LeetCode 746: Min Cost Climbing Stairs LeetCode 198: House Robber Notes The problem statement, examples, and constraints are based on the public LeetCode 70 summary. Python is used to match the current LeetCode tutorial style in this repository. Question: Why is it dp[i] = dp[i - 1] + dp[i - 2], not dp[i] = dp[i - 1] + 1?\nAnswer: The key is that dp[i] stores a number of ways, not a step number or a move count. dp[i - 1] + 1 roughly means \u0026ldquo;take the number of ways to reach step i - 1, then add one new way\u0026rdquo;. That is not what happens. Every way to reach step i - 1 can append one more 1-step move and become one way to reach step i. So the contribution from i - 1 is dp[i - 1] ways, not 1 way.\nNow look at the last move. To reach step i, the last move has only two possibilities: come from i - 1 by climbing 1 step, or come from i - 2 by climbing 2 steps. Therefore:\ndp[i] = ways coming from i - 1 + ways coming from i - 2 = dp[i - 1] + dp[i - 2] For example, take i = 4. There are 3 ways to reach step 3: 1+1+1, 1+2, and 2+1. Appending one 1-step move to each of them gives 3 ways to reach step 4, so coming from step 3 contributes dp[3] = 3 ways, not +1.\nThere are 2 ways to reach step 2: 1+1 and 2. Appending one 2-step move to each of them also reaches step 4, so:\ndp[4] = dp[3] + dp[2] = 3 + 2 = 5 If we wrote dp[i] = dp[i - 1] + 1, we would only consider the group that comes from the previous step, and we would incorrectly collapse that whole group into a single way.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/70-climbing-stairs/","summary":"\u003ch2 id=\"problem\"\u003eProblem\u003c/h2\u003e\n\u003ch3 id=\"input-and-output\"\u003eInput and Output\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eInput: an integer \u003ccode\u003en\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eMeaning: climb to the top of the \u003ccode\u003en\u003c/code\u003e-step staircase\u003c/li\u003e\n\u003cli\u003eEach move can climb \u003ccode\u003e1\u003c/code\u003e or \u003ccode\u003e2\u003c/code\u003e steps\u003c/li\u003e\n\u003cli\u003eOutput: return the number of distinct ways to reach the top\u003c/li\u003e\n\u003cli\u003eConstraints: \u003ccode\u003e1 \u0026lt;= n \u0026lt;= 45\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"examples\"\u003eExamples\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: n = 2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 2\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: 1+1, 2\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: n = 3\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 3\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: 1+1+1, 1+2, 2+1\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis article uses Python only and derives the final solution step by step from the meaning of \u003ccode\u003edp[i]\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 70: Climbing Stairs, Deriving 1D DP from dp[i]"},{"content":"Problem Input and Output Input: an integer array cost cost[i] is the cost of stepping on stair i Each move can climb 1 or 2 steps You may start from index 0 or index 1 Output: return the minimum cost to reach the top Constraints: 2 \u0026lt;= cost.length \u0026lt;= 1000, 0 \u0026lt;= cost[i] \u0026lt;= 999 Examples Input: cost = [10,15,20] Output: 15 Explanation: start from index 1, pay 15, then reach the top directly Input: cost = [1,100,1,1,1,100,1,1,100,1] Output: 6 Start from the Top Position of [10,15,20] Look at the small example:\ncost = [10,15,20] The top is not index 2. It is the position after the last stair, which can be called position 3.\nThe last move to top position 3 can only come from:\nposition 2, paying cost[2] position 1, paying cost[1] This is the easiest place to make a mistake: the task asks for the cost to reach the top, not the cost to reach the last index.\nStep 1: Define Positions, Not Stair Indices If we only stare at cost[i], it is easy to put the answer on the last stair. First define positions:\nposition 0: stair 0 position 1: stair 1 ... position n: top, after the last stair Define the state:\ndp[i] = minimum cost to reach position i Start with the smallest skeleton:\ndef min_cost_climbing_stairs(cost: list[int]) -\u0026gt; int: n = len(cost) dp = [0] * (n + 1) This version can:\nreserve states for positions 0..n make it clear that dp[n] is the answer at the top It still needs:\nhow to define the starting cost how to transfer from the previous two positions Step 2: Starting from 0 or 1 Costs 0 The problem allows starting from index 0 or index 1. Starting there itself is not counted in dp; the cost is paid when moving upward from a stair.\nSo the base cases are:\ndp[0] = 0 dp[1] = 0 The current code is:\ndef min_cost_climbing_stairs(cost: list[int]) -\u0026gt; int: n = len(cost) dp = [0] * (n + 1) dp[0] = 0 dp[1] = 0 This version can:\nexpress that we can start from 0 or 1 for free provide the previous two sources for later positions It still needs:\nwhere the payment comes from when reaching position i Step 3: Position i Can Only Be Reached from i - 1 or i - 2 Now consider i \u0026gt;= 2.\nIf the last move comes from i - 1, we first reach position i - 1, then pay cost[i - 1]. If the last move comes from i - 2, we first reach position i - 2, then pay cost[i - 2].\nAdd this transition to the previous version:\nfor i in range(2, n + 1): dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]) The two terms mean:\ndp[i - 1] + cost[i - 1]: pay from the previous stair and move one step to i dp[i - 2] + cost[i - 2]: pay from two stairs before and move two steps to i The first complete correct version is:\ndef min_cost_climbing_stairs(cost: list[int]) -\u0026gt; int: n = len(cost) dp = [0] * (n + 1) dp[0] = 0 dp[1] = 0 for i in range(2, n + 1): dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]) return dp[n] This version can:\ncompute the minimum cost to reach every position put the answer at top position dp[n] It still has room for:\nspace optimization, because each state only depends on the previous two positions Step 4: Walk Through the Table Slowly For cost = [10,15,20]:\ni Position Meaning From i - 1 From i - 2 dp[i] 0 start 0 - - 0 1 start 1 - - 0 2 position 2 0 + 15 0 + 10 10 3 top 10 + 20 0 + 15 15 The answer is:\ndp[3] = 15 This table checks two things:\nthe top is position n, not n - 1 the paid cost belongs to the stair we leave from Step 5: Compress the dp Array into Two Variables The previous transition only depends on:\ndp[i - 2] dp[i - 1] So two variables are enough:\nprev2 = 0 prev1 = 0 Replace the table with two variables:\nfor i in range(2, n + 1): cur = min(prev1 + cost[i - 1], prev2 + cost[i - 2]) prev2 = prev1 prev1 = cur The final complete code is:\nclass Solution: def minCostClimbingStairs(self, cost: list[int]) -\u0026gt; int: n = len(cost) prev2 = 0 prev1 = 0 for i in range(2, n + 1): cur = min(prev1 + cost[i - 1], prev2 + cost[i - 2]) prev2 = prev1 prev1 = cur return prev1 if __name__ == \u0026#34;__main__\u0026#34;: print(Solution().minCostClimbingStairs([10, 15, 20])) print(Solution().minCostClimbingStairs([1, 100, 1, 1, 1, 100, 1, 1, 100, 1])) This version can:\npreserve the same position meaning of dp[i] reduce extra space from O(n) to O(1) It still has one boundary:\nif the problem allowed more jump lengths, we would need more sources; this problem only allows 1 or 2 steps Correctness Invariant:\nafter processing position i, prev1 equals dp[i] and prev2 equals dp[i - 1] Why the transition is correct:\nThe last move to position i can only come from position i - 1 or i - 2. Coming from i - 1 must pay cost[i - 1]. Coming from i - 2 must pay cost[i - 2]. The problem asks for the minimum cost, so take the smaller of the two. Why return prev1:\nafter the loop, position n has been processed prev1 is dp[n], the minimum cost to reach the top Complexity Time complexity: O(n) Extra space: O(1) Common Mistakes Returning the cost to reach the last stair n - 1 instead of the top position n. Initializing dp[0] and dp[1] as cost[0] and cost[1], which breaks the rule that we may start from 0 or 1 for free. Writing cost[i] in the transition, even though reaching position i pays the cost of the source stair. Summary The key is to treat the top as position n. dp[i] means the minimum cost to reach position i. To reach i, we can only pay and jump from i - 1 or i - 2. Space optimization only keeps dp[i - 2] and dp[i - 1] as two variables. References and Follow-up LeetCode 70: Climbing Stairs LeetCode 746: Min Cost Climbing Stairs LeetCode 198: House Robber Notes The problem statement, examples, and constraints are based on the public LeetCode 746 summary. Python is used to match the current LeetCode tutorial style in this repository. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/746-min-cost-climbing-stairs/","summary":"\u003ch2 id=\"problem\"\u003eProblem\u003c/h2\u003e\n\u003ch3 id=\"input-and-output\"\u003eInput and Output\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eInput: an integer array \u003ccode\u003ecost\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ecost[i]\u003c/code\u003e is the cost of stepping on stair \u003ccode\u003ei\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eEach move can climb \u003ccode\u003e1\u003c/code\u003e or \u003ccode\u003e2\u003c/code\u003e steps\u003c/li\u003e\n\u003cli\u003eYou may start from index \u003ccode\u003e0\u003c/code\u003e or index \u003ccode\u003e1\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eOutput: return the minimum cost to reach the top\u003c/li\u003e\n\u003cli\u003eConstraints: \u003ccode\u003e2 \u0026lt;= cost.length \u0026lt;= 1000\u003c/code\u003e, \u003ccode\u003e0 \u0026lt;= cost[i] \u0026lt;= 999\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"examples\"\u003eExamples\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: cost = [10,15,20]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 15\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eExplanation: start from index 1, pay 15, then reach the top directly\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: cost = [1,100,1,1,1,100,1,1,100,1]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 6\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"start-from-the-top-position-of-101520\"\u003eStart from the Top Position of [10,15,20]\u003c/h2\u003e\n\u003cp\u003eLook at the small example:\u003c/p\u003e","title":"LeetCode 746: Min Cost Climbing Stairs, Deriving DP from the Top Position"},{"content":"Problem Requirement Input / Output Input: nums, with 1 \u0026lt;= nums.length \u0026lt;= 10 Value range: -10 \u0026lt;= nums[i] \u0026lt;= 10 All elements in nums are distinct Output: return all possible subsets of nums Ordering: the result order does not matter, and subset element order is not the point Example Input: nums = [1,2,3] Output: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]] This tutorial builds one minimal Python solution.\nStart From the Search Tree for [1,2] The smallest branching example is:\nnums = [1,2] The answer should contain:\n[], [1], [1,2], [2] It should not contain [2,1]. That means we are not generating all permutations. We are choosing elements into a set-like result where each combination appears once.\nThe construction tree looks like this:\n[] |- [1] | |- [1,2] |- [2] Two facts fall out of this tree:\nEvery node is already a valid subset. After choosing 1, the next layer may only look to the right, at 2. Step 1: Define What One Recursion Layer Means If the current partial answer is path, the smaller problem is:\nStarting from some index, choose zero or more remaining elements to extend path.\nThat starting index must be part of the state. Without it, [1,2] and [2,1] can be generated from different branches.\nStart with the smallest skeleton. It keeps two pieces of state:\npath: the elements chosen on the current branch res: all subsets collected so far def subsets(nums: list[int]) -\u0026gt; list[list[int]]: res: list[list[int]] = [] path: list[int] = [] def dfs(start: int) -\u0026gt; None: pass dfs(0) return res Now this version can:\nDefine the outer function and recursive function. Reserve start as the boundary for the current layer. It still lacks:\nA rule for collecting answers. A way to enumerate choices in the current layer. Step 2: Every Node Is an Answer, So Collect path First In the previous version, add the first rule inside dfs:\ndef dfs(start: int) -\u0026gt; None: res.append(path.copy()) Why collect immediately?\nSubsets do not require a fixed length or target sum. If path was formed by valid choices, it is already a valid subset.\nWhy copy()?\nBecause path will keep changing during recursion. Appending path itself would leave all saved answers pointing to the same mutable list.\nNow this version can:\nCollect the empty subset []. Collect the current partial subset whenever a recursion layer starts. It still lacks:\nDownward expansion; it can only collect the empty subset. Step 3: Enumerate Only From start to the Right In the previous version, add the candidate range for the current layer:\ndef dfs(start: int) -\u0026gt; None: res.append(path.copy()) for i in range(start, len(nums)): pass The range range(start, len(nums)) means:\nThis layer may only choose elements at index start or later. Earlier elements have already been handled by the current path. For nums = [1,2,3]:\ndfs(0) may try 1,2,3 after choosing 1, dfs(1) may only try 2,3 after choosing 2, dfs(2) may only try 3 Now this version can:\nDefine the available candidates for each layer. Use start to prevent order-based duplicates. It still lacks:\nActually adding a candidate into the current path. Step 4: Choose One Element and Recurse on the Suffix In the previous version, replace the empty loop body with choosing and recursing:\nfor i in range(start, len(nums)): path.append(nums[i]) dfs(i + 1) The call dfs(i + 1) is the key rule:\nAfter choosing nums[i], the next layer may only choose elements to its right.\nWriting dfs(start + 1) would be wrong when i != start; the next boundary must follow the chosen index, not the old layer boundary.\nNow this version can:\nGenerate downward paths such as [] -\u0026gt; [1] -\u0026gt; [1,2]. Keep subset elements in increasing original-index order. It still lacks:\nState restoration after returning from recursion. Step 5: Undo the Choice and Finish the Solution In the previous version, add the undo operation after the recursive call:\nfor i in range(start, len(nums)): path.append(nums[i]) dfs(i + 1) path.pop() This solves the branch-isolation problem.\nFor nums = [1,2,3]:\npath = [1,2,3] is collected pop 3 -\u0026gt; path = [1,2] pop 2 -\u0026gt; path = [1] same layer now tries 3 -\u0026gt; path = [1,3] Without path.pop(), later branches would inherit choices that belong to earlier branches.\nThis version is the complete runnable solution:\nclass Solution: def subsets(self, nums: list[int]) -\u0026gt; list[list[int]]: res: list[list[int]] = [] path: list[int] = [] def dfs(start: int) -\u0026gt; None: res.append(path.copy()) for i in range(start, len(nums)): path.append(nums[i]) dfs(i + 1) path.pop() dfs(0) return res if __name__ == \u0026#34;__main__\u0026#34;: ans = Solution().subsets([1, 2, 3]) print(ans) Now this version can:\nEnumerate every subset. Avoid order-based duplicates such as [2,1]. Restore path after each branch. It still lacks:\nDuplicate-value handling. That belongs to 90. Subsets II. Slow Branch Trace For nums = [1,2,3], follow one branch:\ndfs(0), path = [] collect [] i = 0, choose 1 path = [1] dfs(1), collect [1] i = 1, choose 2 path = [1,2] dfs(2), collect [1,2] i = 2, choose 3 path = [1,2,3] dfs(3), collect [1,2,3] return, pop 3 -\u0026gt; [1,2] return, pop 2 -\u0026gt; [1] same layer tries i = 2, choose 3 -\u0026gt; [1,3] The important invariants are:\npath is always the current branch. start always points to the next allowed suffix. Correctness Invariant:\nAt the start of dfs(start), path is a valid subset whose original indices are strictly increasing. The current layer only chooses indices from start onward, so no branch can create a reversed duplicate. Why nothing is missed:\nAny subset can be represented by its chosen original indices in increasing order. DFS can follow exactly that increasing sequence, so the subset will be visited. Why nothing is duplicated:\nEvery branch has strictly increasing indices. The same set of indices has only one increasing order. Complexity There are 2^n subsets. Copying paths across all collected answers costs O(n * 2^n). The recursion stack and path use O(n) extra space, excluding output. The output itself uses O(n * 2^n) space. Common Mistakes Collecting only at leaves, which misses [], [1], [1,2], and other internal nodes. Appending path without copy(), which lets later backtracking mutate saved answers. Calling dfs(start + 1) instead of dfs(i + 1). Forgetting path.pop(), which leaks state across branches. Summary In subsets, every search-tree node is an answer. path stores the current branch; start stores the next allowed index. dfs(i + 1) removes order duplicates. path.pop() restores branch-local state. References LeetCode 78: Subsets LeetCode 90: Subsets II LeetCode 46: Permutations Notes Problem facts, examples, and constraints were taken from the existing repository draft for LeetCode 78. Python is used as the implementation language for the guided build. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/backtracking/78-subsets/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003ch3 id=\"input--output\"\u003eInput / Output\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eInput: \u003ccode\u003enums\u003c/code\u003e, with \u003ccode\u003e1 \u0026lt;= nums.length \u0026lt;= 10\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eValue range: \u003ccode\u003e-10 \u0026lt;= nums[i] \u0026lt;= 10\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eAll elements in \u003ccode\u003enums\u003c/code\u003e are distinct\u003c/li\u003e\n\u003cli\u003eOutput: return all possible subsets of \u003ccode\u003enums\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eOrdering: the result order does not matter, and subset element order is not the point\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"example\"\u003eExample\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [1,2,3]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis tutorial builds one minimal Python solution.\u003c/p\u003e\n\u003ch2 id=\"start-from-the-search-tree-for-12\"\u003eStart From the Search Tree for \u003ccode\u003e[1,2]\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eThe smallest branching example is:\u003c/p\u003e","title":"LeetCode 78: Subsets, Derive the startIndex Backtracking Template"},{"content":"Problem Requirement Input / Output Input: nums, with 1 \u0026lt;= nums.length \u0026lt;= 10 Value range: -10 \u0026lt;= nums[i] \u0026lt;= 10 nums may contain duplicates Output: return all unique subsets Ordering: the result order does not matter, but the same value sequence may appear only once Example Input: nums = [1,2,2] Output: [[],[1],[1,2],[1,2,2],[2],[2,2]] This tutorial starts with a correct but wasteful version, then derives sorting plus layer-level deduplication.\nStart From Duplicate Branches in [1,2,2] The smallest example that exposes the issue is:\nnums = [1,2,2] If we copy the 78. Subsets template directly, two different branches collide:\nchoose the 2 at index 1 -\u0026gt; [2] choose the 2 at index 2 -\u0026gt; [2] The branches use different indices, but the value sequence is the same.\nSo the new problem is not basic backtracking. The real question is:\nHow do we skip duplicate branches without deleting valid answers such as [2,2]?\nStep 1: Reuse the 78 State and Watch What Breaks The partial answer still needs path; each layer still needs start so it only chooses to the right.\nStart with the core version from 78. Subsets:\ndef dfs(start: int) -\u0026gt; None: res.append(path.copy()) for i in range(start, len(nums)): path.append(nums[i]) dfs(i + 1) path.pop() This version is correct when all values are distinct.\nNow this version can:\nEnumerate every index combination. Keep [2,2], because the two 2s come from different indices. It still lacks:\nA way to distinguish a valid repeated value from a duplicate branch at the same layer. Step 2: First Build a Correct but Wasteful Version With seen In the previous version, do not optimize yet. To make the answer correct, convert each path into a tuple and store it in seen.\nAdd seen:\nseen: set[tuple[int, ...]] = set() Replace the collection rule with:\nstate = tuple(path) if state not in seen: seen.add(state) res.append(path.copy()) The first complete correct version is:\ndef subsets_with_dup(nums: list[int]) -\u0026gt; list[list[int]]: res: list[list[int]] = [] path: list[int] = [] seen: set[tuple[int, ...]] = set() def dfs(start: int) -\u0026gt; None: state = tuple(path) if state not in seen: seen.add(state) res.append(path.copy()) for i in range(start, len(nums)): path.append(nums[i]) dfs(i + 1) path.pop() dfs(0) return res Now this version can:\nReturn unique subsets. Preserve valid results such as [2,2]. It still lacks:\nIt still generates duplicate branches before throwing duplicates away. If input is [2,1,2], equal values are not adjacent, so branch-level deduplication is hard. Step 3: Sort First So Equal Values Become Adjacent The bottleneck in the previous version is that duplicate branches are already built. To skip them before entering recursion, equal values must be next to each other.\nIn the previous version, add sorting before DFS:\nnums.sort() For [2,1,2], sorting gives:\n[1,2,2] Once equal values are adjacent, if the current layer already tried the first 2, it can recognize the second 2 as an equivalent same-layer branch.\nNow this version can:\nGroup equal values together. Keep correctness through seen. It still lacks:\nThe actual rule that skips duplicate branches before they are built. Step 4: Skip Only Duplicate Candidates in the Same Layer Now replace “generate then deduplicate” with “skip before generating”.\nThe key condition is:\nif i \u0026gt; start and nums[i] == nums[i - 1]: continue Why i \u0026gt; start?\ni \u0026gt; start means this candidate is not the first candidate in the current layer. nums[i] == nums[i - 1] means an equal value has already opened a branch in this layer. Why not i \u0026gt; 0?\nBecause deeper layers must still be allowed to choose the second 2 to form [2,2].\nFor [1,2,2]:\nFirst layer: i = 1 chooses the first 2 -\u0026gt; valid, produces [2] i = 2 sees another 2 and i \u0026gt; start -\u0026gt; skip duplicate [2] Inside the [2] branch: start = 2 i = 2 chooses the second 2 -\u0026gt; i == start, do not skip, produce [2,2] Now seen is no longer needed.\nThe final complete solution is:\nclass Solution: def subsetsWithDup(self, nums: list[int]) -\u0026gt; list[list[int]]: nums.sort() res: list[list[int]] = [] path: list[int] = [] def dfs(start: int) -\u0026gt; None: res.append(path.copy()) for i in range(start, len(nums)): if i \u0026gt; start and nums[i] == nums[i - 1]: continue path.append(nums[i]) dfs(i + 1) path.pop() dfs(0) return res if __name__ == \u0026#34;__main__\u0026#34;: ans = Solution().subsetsWithDup([1, 2, 2]) print(ans) Now this version can:\nSkip duplicate branches before entering recursion. Keep [2,2]. Avoid extra seen storage for answer deduplication. It still lacks:\nA guaranteed output order; the problem does not require one. Slow Branch Trace After sorting, nums = [1,2,2].\nAt the first layer dfs(0):\ncollect [] i = 0, choose 1 -\u0026gt; path = [1] i = 1, choose the first 2 -\u0026gt; path = [2] i = 2, second 2 equals previous 2 and i \u0026gt; start, skip Inside the [2] branch, at dfs(2):\nstart = 2 i = 2, here i == start, do not skip choose the second 2 -\u0026gt; path = [2,2] So the rule is not “duplicate values cannot be used”. The real rule is:\nIn the same layer, equal values open only one branch; deeper layers may still use later duplicates.\nCorrectness Invariant:\nSorting makes equal values adjacent. In the same recursion layer, if an equal value has already opened a branch, a later equal value would generate duplicate value sequences. i \u0026gt; start restricts the skip to the current layer only. Why nothing is missed:\nEach value group still keeps the first candidate branch in every layer. If an answer needs multiple equal values such as [2,2], the later equal value appears in a deeper layer where i == start, so it is not skipped. Why nothing is duplicated:\nDuplicate subsets come from same-layer equal-value branches. The skip rule removes all but the first such branch. Complexity Sorting costs O(n log n). There can still be up to 2^n subsets. Copying collected paths costs O(n * 2^n) overall. The recursion stack and path use O(n) extra space, excluding output. Common Mistakes Writing i \u0026gt; 0, which can incorrectly remove [2,2]. Forgetting to sort, so equal values are not adjacent. Thinking duplicate values are forbidden; only duplicate same-layer branches are forbidden. Keeping both seen and layer-level deduplication, which leaves two competing dedup mechanisms. Summary 90. Subsets II is 78. Subsets plus duplicate handling. A seen version is correct but wastes search. Sorting is the precondition for branch-level deduplication. if i \u0026gt; start and nums[i] == nums[i - 1] means layer-level deduplication, not value-level deletion. References LeetCode 78: Subsets LeetCode 90: Subsets II LeetCode 40: Combination Sum II Notes Problem facts, examples, and constraints were taken from the existing repository draft for LeetCode 90. Python is used as the implementation language for the guided build. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/backtracking/90-subsets-ii/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003ch3 id=\"input--output\"\u003eInput / Output\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eInput: \u003ccode\u003enums\u003c/code\u003e, with \u003ccode\u003e1 \u0026lt;= nums.length \u0026lt;= 10\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eValue range: \u003ccode\u003e-10 \u0026lt;= nums[i] \u0026lt;= 10\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003enums\u003c/code\u003e may contain duplicates\u003c/li\u003e\n\u003cli\u003eOutput: return all unique subsets\u003c/li\u003e\n\u003cli\u003eOrdering: the result order does not matter, but the same value sequence may appear only once\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"example\"\u003eExample\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: nums = [1,2,2]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: [[],[1],[1,2],[1,2,2],[2],[2,2]]\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis tutorial starts with a correct but wasteful version, then derives sorting plus layer-level deduplication.\u003c/p\u003e\n\u003ch2 id=\"start-from-duplicate-branches-in-122\"\u003eStart From Duplicate Branches in \u003ccode\u003e[1,2,2]\u003c/code\u003e\u003c/h2\u003e\n\u003cp\u003eThe smallest example that exposes the issue is:\u003c/p\u003e","title":"LeetCode 90: Subsets II, Derive Layer-Level Deduplication"},{"content":" Subtitle / Summary The easiest way to get lost in LeetCode 124 is to make one recursive return value carry too much meaning. The stable design is to separate two roles: the recursion returns only the best single-side gain to the parent, while the full path through the current node is used to update the global maximum.\nReading time: 12-15 min Tags: Hot100, binary tree, tree DP, DFS, postorder SEO keywords: Binary Tree Maximum Path Sum, tree DP, single-side gain, postorder, DFS, LeetCode 124 Meta description: Learn LeetCode 124 from the single-side gain invariant, global path update, and negative-branch pruning, with runnable multi-language implementations. A — Algorithm Problem Restatement A path in a binary tree is a sequence of nodes such that:\neach pair of adjacent nodes is connected by an edge a node appears at most once in the path the path contains at least one node the path does not need to pass through the root The path sum is the sum of all node values in that path. Return the maximum path sum in the entire tree.\nInput / Output Name Type Description root TreeNode root of the binary tree return int maximum path sum Example 1 input: root = [1,2,3] output: 6 explanation: the best path is 2 -\u0026gt; 1 -\u0026gt; 3 Example 2 input: root = [-10,9,20,null,null,15,7] output: 42 explanation: the best path is 15 -\u0026gt; 20 -\u0026gt; 7 Constraints The number of nodes in the tree is in the range [1, 3 * 10^4] -1000 \u0026lt;= Node.val \u0026lt;= 1000 Target Readers Hot100 learners who already solved 543 but still mix up return values in tree-path problems Developers who keep confusing \u0026ldquo;path through the current node\u0026rdquo; with \u0026ldquo;value returned to the parent\u0026rdquo; Readers who want one stable tree-DP interpretation for weighted path problems Background / Motivation This problem is valuable because it teaches a very general lesson:\na recursive function does not always return the final answer sometimes it returns only the information the parent truly needs Two questions usually cause confusion:\nif the best path through a node uses both left and right children, what can the recursion return upward? if a child subtree contributes a negative sum, should it still be included? The clean solution appears once you separate:\nglobal answer: the best full path seen anywhere recursive return value: the best single-side gain the current node can offer upward Core Concepts Postorder recursion: collect left and right information before processing the current node Single-side gain: the best downward contribution the current node can extend to its parent Full-path candidate: a complete path that goes through the current node and may use both sides Negative pruning: if a child contribution is negative, it should be dropped C — Concepts How To Build The Solution From Scratch Step 1: Clarify what the answer can look like Take:\n1 / \\ 2 3 The best path is not just one downward chain. It is:\n2 -\u0026gt; 1 -\u0026gt; 3 So some node can act as a turning point and combine both sides. That is the first important distinction from simpler depth problems.\nStep 2: Decide what the parent actually needs back Even if the best path through the current node uses both left and right children, the parent cannot keep both branches when continuing upward.\nA path extended to the parent can only choose one side.\nSo the recursive return value should mean:\nthe maximum single-side gain that starts at this node and can continue upward through the parent\nThat is the stable invariant.\nStep 3: Define the smaller subproblem If the left and right subtrees have already reported their best single-side gains, then the current node can compute:\nthe best full path that passes through itself the single-side gain it returns upward That is exactly a postorder situation.\nStep 4: Define the base case An empty node contributes no gain:\nif node is None: return 0 Notice that this is a gain value, not the final answer.\nStep 5: Build the full-path candidate at the current node First get both gains:\nleft = dfs(node.left) right = dfs(node.right) But if a side is negative, keeping it only hurts the total. So clamp each one at zero:\nleft = max(left, 0) right = max(right, 0) Now the full path through the current node is:\ncandidate = node.val + left + right Step 6: Update the global answer Since candidate is a complete valid path, use it to update the global maximum:\nans = max(ans, candidate) This path may be the best in the whole tree.\nStep 7: Return only one side upward The parent cannot continue through both sides. So the recursive return value must be:\nreturn node.val + max(left, right) This is the best single-side chain starting at the current node.\nStep 8: Walk the official example slowly Use:\nroot = [-10,9,20,null,null,15,7] At node 20:\nleft gain = 15 right gain = 7 So the full-path candidate through 20 is:\n20 + 15 + 7 = 42 That updates the global answer to 42.\nBut the value returned upward to -10 is only:\n20 + max(15, 7) = 35 This is why the recursive return value and the global answer must remain separate.\nStep 9: Connect this to the pruning rule If one side returns -5, the current node should not keep it. Using max(gain, 0) means:\npositive contributions are kept harmful branches are cut off That is the crucial weighted-tree twist in this problem.\nAssemble the Full Code Now combine the postorder rules into the first full working solution.\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def max_path_sum(root): ans = float(\u0026#34;-inf\u0026#34;) def dfs(node): nonlocal ans if node is None: return 0 left = max(dfs(node.left), 0) right = max(dfs(node.right), 0) ans = max(ans, node.val + left + right) return node.val + max(left, right) dfs(root) return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(-10, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7))) print(max_path_sum(root)) Reference Answer The LeetCode-style submission version is:\nfrom typing import Optional class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def maxPathSum(self, root: Optional[TreeNode]) -\u0026gt; int: ans = float(\u0026#34;-inf\u0026#34;) def dfs(node: Optional[TreeNode]) -\u0026gt; int: nonlocal ans if node is None: return 0 left = max(dfs(node.left), 0) right = max(dfs(node.right), 0) ans = max(ans, node.val + left + right) return node.val + max(left, right) dfs(root) return ans What method did we just build? You could call it:\ntree DP postorder gain propagation weighted path divide and conquer But the main takeaway is:\nA full path through the current node may use both sides, but the value returned upward can use only one side.\nE — Engineering Scenario 1: Find the highest-value dependency path (Python) Background: each node in a dependency tree has a positive or negative score, and the system wants the most valuable connected path.\nWhy it fits: negative branches should be dropped, while a local merge point may create the global optimum.\nclass Node: def __init__(self, val, left=None, right=None): self.val = val self.left = left self.right = right def best_path(root): ans = float(\u0026#34;-inf\u0026#34;) def dfs(node): nonlocal ans if node is None: return 0 left = max(dfs(node.left), 0) right = max(dfs(node.right), 0) ans = max(ans, node.val + left + right) return node.val + max(left, right) dfs(root) return ans root = Node(-5, Node(10), Node(20)) print(best_path(root)) Scenario 2: Find the highest-value propagation path in a service tree (Go) Background: a call tree assigns net value or net cost to each node, and monitoring wants the most valuable connected route.\nWhy it fits: harmful branches should be pruned, while a local turning point may become the global best.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Val int Left *Node Right *Node } func maxPath(root *Node) int { if root == nil { return 0 } ans := root.Val var dfs func(*Node) int dfs = func(node *Node) int { if node == nil { return 0 } left := dfs(node.Left) if left \u0026lt; 0 { left = 0 } right := dfs(node.Right) if right \u0026lt; 0 { right = 0 } sum := node.Val + left + right if sum \u0026gt; ans { ans = sum } if left \u0026gt; right { return node.Val + left } return node.Val + right } dfs(root) return ans } func main() { root := \u0026amp;Node{Val: -10, Left: \u0026amp;Node{Val: 9}, Right: \u0026amp;Node{Val: 20, Left: \u0026amp;Node{Val: 15}, Right: \u0026amp;Node{Val: 7}}} fmt.Println(maxPath(root)) } Scenario 3: Find the highest-score skill path in a game tree (JavaScript) Background: a game or visualization tree stores positive and negative weights on nodes, and the UI wants the best connected route.\nWhy it fits: the same \u0026ldquo;single-side gain + global full path\u0026rdquo; pattern applies directly.\nfunction Node(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function maxPath(root) { let ans = -Infinity; function dfs(node) { if (!node) return 0; const left = Math.max(dfs(node.left), 0); const right = Math.max(dfs(node.right), 0); ans = Math.max(ans, node.val + left + right); return node.val + Math.max(left, right); } dfs(root); return ans; } const root = new Node(-10, new Node(9), new Node(20, new Node(15), new Node(7))); console.log(maxPath(root)); R — Reflection Complexity Analysis Time complexity: O(n) because each node is processed once Space complexity: O(h) from recursion depth Alternative Approaches Method Time Extra Space Notes Postorder single-side gain + global update O(n) O(h) Best standard solution Enumerate all paths very large very large Not practical Return full-path sum upward incorrect - The parent cannot extend both sides simultaneously Common Mistakes Returning the full path through the current node: that cannot be extended safely by the parent. Keeping negative child gains: that only makes the path worse. Initializing the answer with 0: that fails on all-negative trees. Mixing this with problem 543: both use postorder, but 124 works with weighted gains and negative pruning. FAQ and Notes 1. Why is returning 0 for null nodes correct? Because the recursion returns a gain value, not the final answer. An empty child contributes no useful gain.\n2. Why can the global answer not start at 0? Because the entire tree may be negative. For example, a single node -3 should return -3, not 0.\n3. How is this different from problem 543? Both use postorder recursion, but:\n543 returns height and updates a diameter in edges 124 returns gain and updates a path sum in node weights Problem 124 also needs negative pruning.\nBest Practices Ask first what the parent truly needs back from the child Keep the full-path candidate and upward gain as separate concepts Use max(gain, 0) whenever negative branches should be discarded Pair this with 543 to sharpen your tree-DP return-value intuition S — Summary The stable design of problem 124 is: return single-side gain upward, update the global answer with the full path through the node A full path can use both sides, but an upward return can use only one Negative contributions should be cut off with max(gain, 0) This problem looks similar to 543, but weighted paths and negative values make the semantics different Once you understand this split, many tree-path DP problems become much clearer Further Reading LeetCode 124: Binary Tree Maximum Path Sum LeetCode 543: Diameter of Binary Tree LeetCode 104: Maximum Depth of Binary Tree LeetCode 337: House Robber III CTA Practice 104 + 543 + 124 together. They form a strong tree-DP ladder: single-side depth, structural path, and weighted path.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def max_path_sum(root): ans = float(\u0026#34;-inf\u0026#34;) def dfs(node): nonlocal ans if node is None: return 0 left = max(dfs(node.left), 0) right = max(dfs(node.right), 0) ans = max(ans, node.val + left + right) return node.val + max(left, right) dfs(root) return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(-10, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7))) print(max_path_sum(root)) #include \u0026lt;limits.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } int max_int(int a, int b) { return a \u0026gt; b ? a : b; } int dfs(struct TreeNode* node, int* ans) { if (!node) return 0; int left = max_int(dfs(node-\u0026gt;left, ans), 0); int right = max_int(dfs(node-\u0026gt;right, ans), 0); int candidate = node-\u0026gt;val + left + right; if (candidate \u0026gt; *ans) *ans = candidate; return node-\u0026gt;val + max_int(left, right); } int maxPathSum(struct TreeNode* root) { int ans = INT_MIN; dfs(root, \u0026amp;ans); return ans; } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(-10); root-\u0026gt;left = new_node(9); root-\u0026gt;right = new_node(20); root-\u0026gt;right-\u0026gt;left = new_node(15); root-\u0026gt;right-\u0026gt;right = new_node(7); printf(\u0026#34;%d\\n\u0026#34;, maxPathSum(root)); free_tree(root); return 0; } #include \u0026lt;algorithm\u0026gt; #include \u0026lt;climits\u0026gt; #include \u0026lt;iostream\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; int dfs(TreeNode* node, int\u0026amp; ans) { if (!node) return 0; int left = std::max(dfs(node-\u0026gt;left, ans), 0); int right = std::max(dfs(node-\u0026gt;right, ans), 0); ans = std::max(ans, node-\u0026gt;val + left + right); return node-\u0026gt;val + std::max(left, right); } int maxPathSum(TreeNode* root) { int ans = INT_MIN; dfs(root, ans); return ans; } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(-10); root-\u0026gt;left = new TreeNode(9); root-\u0026gt;right = new TreeNode(20); root-\u0026gt;right-\u0026gt;left = new TreeNode(15); root-\u0026gt;right-\u0026gt;right = new TreeNode(7); std::cout \u0026lt;\u0026lt; maxPathSum(root) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func maxPathSum(root *TreeNode) int { if root == nil { return 0 } ans := root.Val var dfs func(*TreeNode) int dfs = func(node *TreeNode) int { if node == nil { return 0 } left := dfs(node.Left) if left \u0026lt; 0 { left = 0 } right := dfs(node.Right) if right \u0026lt; 0 { right = 0 } if node.Val+left+right \u0026gt; ans { ans = node.Val + left + right } if left \u0026gt; right { return node.Val + left } return node.Val + right } dfs(root) return ans } func main() { root := \u0026amp;TreeNode{ Val: -10, Left: \u0026amp;TreeNode{Val: 9}, Right: \u0026amp;TreeNode{ Val: 20, Left: \u0026amp;TreeNode{Val: 15}, Right: \u0026amp;TreeNode{Val: 7}, }, } fmt.Println(maxPathSum(root)) } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn dfs(node: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, ans: \u0026amp;mut i32) -\u0026gt; i32 { match node { None =\u0026gt; 0, Some(n) =\u0026gt; { let left = dfs(\u0026amp;n.left, ans).max(0); let right = dfs(\u0026amp;n.right, ans).max(0); *ans = (*ans).max(n.val + left + right); n.val + left.max(right) } } } fn max_path_sum(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) -\u0026gt; i32 { let mut ans = i32::MIN; dfs(root, \u0026amp;mut ans); ans } fn main() { let root = Some(Box::new(TreeNode { val: -10, left: Some(Box::new(TreeNode { val: 9, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 20, left: Some(Box::new(TreeNode { val: 15, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 7, left: None, right: None, })), })), })); println!(\u0026#34;{}\u0026#34;, max_path_sum(\u0026amp;root)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function maxPathSum(root) { let ans = -Infinity; function dfs(node) { if (!node) return 0; const left = Math.max(dfs(node.left), 0); const right = Math.max(dfs(node.right), 0); ans = Math.max(ans, node.val + left + right); return node.val + Math.max(left, right); } dfs(root); return ans; } const root = new TreeNode(-10, new TreeNode(9), new TreeNode(20, new TreeNode(15), new TreeNode(7))); console.log(maxPathSum(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/124-binary-tree-maximum-path-sum/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nThe easiest way to get lost in LeetCode 124 is to make one recursive return value carry too much meaning. The stable design is to separate two roles: the recursion returns only the best single-side gain to the parent, while the full path through the current node is used to update the global maximum.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003etree DP\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003epostorder\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Binary Tree Maximum Path Sum, tree DP, single-side gain, postorder, DFS, LeetCode 124\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 124 from the single-side gain invariant, global path update, and negative-branch pruning, with runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eA path in a binary tree is a sequence of nodes such that:\u003c/p\u003e","title":"Hot100: Binary Tree Maximum Path Sum (Tree DP / Single-Side Gain ACERS Guide)"},{"content":" Subtitle / Summary LeetCode 199 is not really about visual imagination. It is about translating a viewpoint problem into a level problem. Once you realize that the right side view is simply the last node of each level, the problem becomes a standard breadth-first traversal.\nReading time: 10-13 min Tags: Hot100, binary tree, BFS, level order, queue SEO keywords: Binary Tree Right Side View, BFS, level order, queue, right-first DFS, LeetCode 199 Meta description: Learn LeetCode 199 from the core equivalence \u0026ldquo;right side view = last node of each level\u0026rdquo;, with step-by-step derivation, engineering mappings, and runnable multi-language implementations. A — Algorithm Problem Restatement Given the root root of a binary tree, imagine standing on its right side and return the values of the nodes you can see from top to bottom.\nInput / Output Name Type Description root TreeNode root of the binary tree return int[] node values visible from the right side, top to bottom Example 1 input: root = [1,2,3,null,5,null,4] output: [1,3,4] Example 2 input: root = [1,2,3,4,null,null,null,5] output: [1,3,4,5] Example 3 input: root = [1,null,3] output: [1,3] Example 4 input: root = [] output: [] Constraints The number of nodes in the tree is in the range [0, 100] -100 \u0026lt;= Node.val \u0026lt;= 100 Target Readers Hot100 learners who already know level order traversal but want to handle \u0026ldquo;view\u0026rdquo; variations cleanly Developers who get distracted by the visual wording of the problem Readers who want to connect problem 199 directly to problem 102 Background / Motivation This problem is useful because it trains one important move:\nconvert a visual description into a structural rule When people first read \u0026ldquo;right side view\u0026rdquo;, they often wonder:\ndo I need coordinates? do I need actual geometry? do I need to simulate visibility? None of that is necessary. The phrase \u0026ldquo;from top to bottom\u0026rdquo; already tells you that the answer is organized by levels.\nSo the real question is:\nWhich single node should survive from each level?\nThe answer is:\nthe rightmost one\nAnd with left-to-right level processing, that becomes:\nthe last processed node of the level\nCore Concepts Level order traversal: visit the tree layer by layer Queue: the natural container for BFS Level width control: fix the current level size before processing that level Right side view: either the last node of each level (BFS) or the first node visited at each depth in right-first DFS C — Concepts How To Build The Solution From Scratch Step 1: Start from the visible meaning of one level Take:\n1 / \\ 2 3 \\ \\ 5 4 From the right side:\nlevel 1 shows 1 level 2 shows 3 level 3 shows 4 So the answer is:\n[1,3,4] This shows the problem is not asking for one literal right-edge chain. It is asking for the visible representative of each level.\nStep 2: Decide what state the algorithm needs Since the answer is level-based, BFS is the most direct fit.\nSo the first state variables are:\nqueue ans Step 3: Define the smaller subproblem Instead of \u0026ldquo;solve the whole tree at once\u0026rdquo;, the repeated task is:\nProcess one level, then decide which node from this level should be kept.\nTo do that, the algorithm must know how many nodes belong to the current level:\nlevel_size = len(queue) Step 4: Define when one level is complete Once exactly level_size nodes have been popped, the level is done.\nThe last one popped is the rightmost node of that level, so:\nif i == level_size - 1: ans.append(node.val) Step 5: Expand the next frontier normally Nothing special happens here. For each node in the current level:\nif node.left: queue.append(node.left) if node.right: queue.append(node.right) This is just regular level order traversal.\nStep 6: Why does the last node of the level equal the right side view? Because within a level, BFS processes nodes from left to right.\nSo:\nearlier processed nodes are more leftward the last processed node is the rightmost one That is exactly the node that remains visible from the right side.\nStep 7: Connect this to an alternative DFS view There is another way to think about the same answer:\ntraverse right child before left child at each depth, record the first node you see That first node is also the rightmost visible node at that depth.\nSo the two stable views are:\nBFS: keep the last node of each level right-first DFS: keep the first node of each depth Step 8: Walk the official example slowly Use:\nroot = [1,2,3,null,5,null,4] Level 1:\nnodes: 1 last node: 1 Level 2:\nnodes: 2, 3 last node: 3 Level 3:\nnodes: 5, 4 last node: 4 So the answer is [1,3,4].\nStep 9: Keep the derivation simple There is no need for coordinates or geometry. The problem becomes easy as soon as the right-side view is rewritten as:\none kept node per level\nAssemble the Full Code Now combine the level-order rules into the first full working solution.\nfrom collections import deque class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def right_side_view(root): if root is None: return [] ans = [] queue = deque([root]) while queue: level_size = len(queue) for i in range(level_size): node = queue.popleft() if node.left: queue.append(node.left) if node.right: queue.append(node.right) if i == level_size - 1: ans.append(node.val) return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(1, TreeNode(2, None, TreeNode(5)), TreeNode(3, None, TreeNode(4))) print(right_side_view(root)) Reference Answer The LeetCode-style submission version is:\nfrom collections import deque from typing import List, Optional class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def rightSideView(self, root: Optional[TreeNode]) -\u0026gt; List[int]: if root is None: return [] ans = [] queue = deque([root]) while queue: level_size = len(queue) for i in range(level_size): node = queue.popleft() if node.left: queue.append(node.left) if node.right: queue.append(node.right) if i == level_size - 1: ans.append(node.val) return ans What method did we just build? You could call it:\nlevel order traversal one-node-per-level selection BFS with level representatives But the core insight is:\nThe right side view is not a geometry problem. It is a per-level selection problem.\nE — Engineering Scenario 1: Show the outermost owner of each org layer (Python) Background: a compressed org chart may show just one representative from each level.\nWhy it fits: this is structurally the same as choosing one visible node per level.\nfrom collections import deque class Node: def __init__(self, name, left=None, right=None): self.name = name self.left = left self.right = right def rightmost_each_level(root): if root is None: return [] q = deque([root]) ans = [] while q: size = len(q) for i in range(size): node = q.popleft() if node.left: q.append(node.left) if node.right: q.append(node.right) if i == size - 1: ans.append(node.name) return ans root = Node(\u0026#34;CEO\u0026#34;, Node(\u0026#34;Eng\u0026#34;), Node(\u0026#34;Sales\u0026#34;)) print(rightmost_each_level(root)) Scenario 2: Pick one exposed service per topology layer (Go) Background: a simplified service dependency view may keep only one representative service from each layer.\nWhy it fits: the \u0026ldquo;last node per layer\u0026rdquo; rule works on any layered tree traversal.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Name string Left *Node Right *Node } func rightView(root *Node) []string { if root == nil { return nil } q := []*Node{root} ans := []string{} for len(q) \u0026gt; 0 { size := len(q) for i := 0; i \u0026lt; size; i++ { node := q[0] q = q[1:] if node.Left != nil { q = append(q, node.Left) } if node.Right != nil { q = append(q, node.Right) } if i == size-1 { ans = append(ans, node.Name) } } } return ans } func main() { root := \u0026amp;Node{Name: \u0026#34;api\u0026#34;, Left: \u0026amp;Node{Name: \u0026#34;auth\u0026#34;}, Right: \u0026amp;Node{Name: \u0026#34;feed\u0026#34;}} fmt.Println(rightView(root)) } Scenario 3: Keep one endpoint from each menu depth (JavaScript) Background: a compact navigation preview may display only one endpoint from each tree depth.\nWhy it fits: the same level-order selection logic applies.\nfunction Node(name, left = null, right = null) { this.name = name; this.left = left; this.right = right; } function rightView(root) { if (!root) return []; const q = [root]; const ans = []; while (q.length) { const size = q.length; for (let i = 0; i \u0026lt; size; i++) { const node = q.shift(); if (node.left) q.push(node.left); if (node.right) q.push(node.right); if (i === size - 1) ans.push(node.name); } } return ans; } const root = new Node(\u0026#34;Home\u0026#34;, new Node(\u0026#34;Docs\u0026#34;), new Node(\u0026#34;Blog\u0026#34;)); console.log(rightView(root)); R — Reflection Complexity Analysis Time complexity: O(n) because each node is visited once Space complexity: O(w) where w is the maximum width of the tree Alternative Approaches Method Time Extra Space Notes BFS by level O(n) O(w) Most direct solution Right-first DFS O(n) O(h) Elegant recursive alternative Collect full levels then take last element O(n) higher Works, but stores more than needed Common Mistakes Mistaking the answer for one literal right-edge chain: that fails on trees where the visible nodes come from different branches. Not fixing the level size first: then levels mix together and \u0026ldquo;last node\u0026rdquo; loses its meaning. Thinking right-first traversal is required for BFS: it is not; left-to-right BFS still makes the last node the rightmost one. Forgetting the empty-tree case: [] should return an empty list immediately. FAQ and Notes 1. Why does left-to-right BFS still produce the rightmost node last? Because nodes within one level are processed from left to right. So the final node of that level is the rightmost one.\n2. How is this related to problem 102? Problem 199 is almost a direct variant of problem 102:\n102 keeps the whole level 199 keeps only the last node of the level 3. Why does right-first DFS also work? Because if you always go right before left, the first node you see at a new depth is the rightmost visible one.\nBest Practices If a tree problem says \u0026ldquo;from top to bottom\u0026rdquo; or \u0026ldquo;each level\u0026rdquo;, think BFS early Always capture level_size before processing the level Keep the derivation simple: one kept node per level Pair this with problems 102 and 637 to stabilize level-order patterns S — Summary The right side view is best understood as one selected node per level In BFS, that node is simply the last node of the level Right-first DFS gives the same answer from a depth-first perspective Problem 199 is very close to problem 102 conceptually Once you see the level equivalence, many \u0026ldquo;tree view\u0026rdquo; problems become much simpler Further Reading LeetCode 199: Binary Tree Right Side View LeetCode 102: Binary Tree Level Order Traversal LeetCode 637: Average of Levels in Binary Tree LeetCode 103: Binary Tree Zigzag Level Order Traversal CTA Try 102 + 199 + 637 together. They all train the same skill: process one level at a time, then decide what summary to keep.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) from collections import deque class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def right_side_view(root): if root is None: return [] ans = [] q = deque([root]) while q: size = len(q) for i in range(size): node = q.popleft() if node.left: q.append(node.left) if node.right: q.append(node.right) if i == size - 1: ans.append(node.val) return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(1, TreeNode(2, None, TreeNode(5)), TreeNode(3, None, TreeNode(4))) print(right_side_view(root)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } int* rightSideView(struct TreeNode* root, int* returnSize) { *returnSize = 0; if (!root) return NULL; struct TreeNode* queue[256]; int front = 0, back = 0; int* ans = (int*)malloc(sizeof(int) * 256); queue[back++] = root; while (front \u0026lt; back) { int size = back - front; for (int i = 0; i \u0026lt; size; ++i) { struct TreeNode* node = queue[front++]; if (node-\u0026gt;left) queue[back++] = node-\u0026gt;left; if (node-\u0026gt;right) queue[back++] = node-\u0026gt;right; if (i == size - 1) ans[(*returnSize)++] = node-\u0026gt;val; } } return ans; } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(1); root-\u0026gt;left = new_node(2); root-\u0026gt;right = new_node(3); root-\u0026gt;left-\u0026gt;right = new_node(5); root-\u0026gt;right-\u0026gt;right = new_node(4); int size = 0; int* ans = rightSideView(root, \u0026amp;size); for (int i = 0; i \u0026lt; size; ++i) printf(\u0026#34;%d \u0026#34;, ans[i]); printf(\u0026#34;\\n\u0026#34;); free(ans); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;queue\u0026gt; #include \u0026lt;vector\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; std::vector\u0026lt;int\u0026gt; rightSideView(TreeNode* root) { if (!root) return {}; std::queue\u0026lt;TreeNode*\u0026gt; q; std::vector\u0026lt;int\u0026gt; ans; q.push(root); while (!q.empty()) { int size = static_cast\u0026lt;int\u0026gt;(q.size()); for (int i = 0; i \u0026lt; size; ++i) { TreeNode* node = q.front(); q.pop(); if (node-\u0026gt;left) q.push(node-\u0026gt;left); if (node-\u0026gt;right) q.push(node-\u0026gt;right); if (i == size - 1) ans.push_back(node-\u0026gt;val); } } return ans; } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(1); root-\u0026gt;left = new TreeNode(2); root-\u0026gt;right = new TreeNode(3); root-\u0026gt;left-\u0026gt;right = new TreeNode(5); root-\u0026gt;right-\u0026gt;right = new TreeNode(4); for (int x : rightSideView(root)) std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#39; \u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func rightSideView(root *TreeNode) []int { if root == nil { return nil } q := []*TreeNode{root} ans := []int{} for len(q) \u0026gt; 0 { size := len(q) for i := 0; i \u0026lt; size; i++ { node := q[0] q = q[1:] if node.Left != nil { q = append(q, node.Left) } if node.Right != nil { q = append(q, node.Right) } if i == size-1 { ans = append(ans, node.Val) } } } return ans } func main() { root := \u0026amp;TreeNode{ Val: 1, Left: \u0026amp;TreeNode{ Val: 2, Right: \u0026amp;TreeNode{Val: 5}, }, Right: \u0026amp;TreeNode{ Val: 3, Right: \u0026amp;TreeNode{Val: 4}, }, } fmt.Println(rightSideView(root)) } use std::collections::VecDeque; #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn right_side_view(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let mut ans = Vec::new(); let mut q: VecDeque\u0026lt;\u0026amp;TreeNode\u0026gt; = VecDeque::new(); if let Some(node) = root.as_deref() { q.push_back(node); } else { return ans; } while !q.is_empty() { let size = q.len(); for i in 0..size { let node = q.pop_front().unwrap(); if let Some(left) = node.left.as_deref() { q.push_back(left); } if let Some(right) = node.right.as_deref() { q.push_back(right); } if i + 1 == size { ans.push(node.val); } } } ans } fn main() { let root = Some(Box::new(TreeNode { val: 1, left: Some(Box::new(TreeNode { val: 2, left: None, right: Some(Box::new(TreeNode { val: 5, left: None, right: None, })), })), right: Some(Box::new(TreeNode { val: 3, left: None, right: Some(Box::new(TreeNode { val: 4, left: None, right: None, })), })), })); println!(\u0026#34;{:?}\u0026#34;, right_side_view(\u0026amp;root)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function rightSideView(root) { if (!root) return []; const q = [root]; const ans = []; while (q.length) { const size = q.length; for (let i = 0; i \u0026lt; size; i++) { const node = q.shift(); if (node.left) q.push(node.left); if (node.right) q.push(node.right); if (i === size - 1) ans.push(node.val); } } return ans; } const root = new TreeNode(1, new TreeNode(2, null, new TreeNode(5)), new TreeNode(3, null, new TreeNode(4))); console.log(rightSideView(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/199-binary-tree-right-side-view/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nLeetCode 199 is not really about visual imagination. It is about translating a viewpoint problem into a level problem. Once you realize that the right side view is simply the last node of each level, the problem becomes a standard breadth-first traversal.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-13 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eBFS\u003c/code\u003e, \u003ccode\u003elevel order\u003c/code\u003e, \u003ccode\u003equeue\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Binary Tree Right Side View, BFS, level order, queue, right-first DFS, LeetCode 199\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 199 from the core equivalence \u0026ldquo;right side view = last node of each level\u0026rdquo;, with step-by-step derivation, engineering mappings, and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the root \u003ccode\u003eroot\u003c/code\u003e of a binary tree, imagine standing on its right side and return the values of the nodes you can see from top to bottom.\u003c/p\u003e","title":"Hot100: Binary Tree Right Side View (Level Order Last-Node Rule ACERS Guide)"},{"content":" Subtitle / Summary The key to LeetCode 105 is not memorizing that preorder and inorder can reconstruct a tree. It is understanding what each traversal contributes: preorder tells you the root, inorder tells you the left/right boundary. Once those roles are clear, the whole problem becomes a clean indexed divide-and-conquer.\nReading time: 12-15 min Tags: Hot100, binary tree, divide and conquer, hash map, preorder SEO keywords: Construct Binary Tree from Preorder and Inorder Traversal, preorder, inorder, divide and conquer, hash map, LeetCode 105 Meta description: Learn LeetCode 105 from traversal roles, indexed recursion, and hash-map root lookup, with runnable multi-language implementations. A — Algorithm Problem Restatement Given the preorder traversal preorder and inorder traversal inorder of a binary tree, reconstruct the tree and return its root.\nThe problem guarantees:\nall node values are unique both arrays come from the same valid binary tree Input / Output Name Type Description preorder int[] preorder traversal of the tree inorder int[] inorder traversal of the same tree return TreeNode reconstructed root Example 1 input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7] output: [3,9,20,null,null,15,7] Example 2 input: preorder = [-1], inorder = [-1] output: [-1] Constraints 1 \u0026lt;= preorder.length \u0026lt;= 3000 inorder.length == preorder.length -3000 \u0026lt;= preorder[i], inorder[i] \u0026lt;= 3000 preorder and inorder consist of unique values every value of inorder appears in preorder preorder is guaranteed to be a valid preorder traversal inorder is guaranteed to be a valid inorder traversal Target Readers Hot100 learners who still mix up the roles of preorder and inorder Developers who want one stable \u0026ldquo;rebuild the tree from traversals\u0026rdquo; template Readers connecting traversal order with actual tree structure Background / Motivation This problem is valuable because it teaches a deeper question:\nwhat kind of structural information does each traversal order actually contain? Many people remember a slogan:\npreorder + inorder can rebuild the tree But if you do not keep asking why, the code stays mechanical.\nThe real points to understand are:\nwhy the first preorder value must be the root why the root position in inorder splits left and right subtrees why the recursion must build the left subtree before the right subtree Once those are stable, the implementation is straightforward.\nCore Concepts Preorder traversal: root, left, right, so the first element is always the current root Inorder traversal: left, root, right, so the root position splits left and right ranges Interval recursion: represent the current subtree by an inorder interval [l, r] Hash map lookup: use value -\u0026gt; inorderIndex for O(1) split-point lookup C — Concepts How To Build The Solution From Scratch Step 1: Start from the two key traversal facts Use:\npreorder = [3,9,20,15,7] inorder = [9,3,15,20,7] The first preorder element is 3. Since preorder is root, left, right:\n3 must be the root of the whole tree.\nNow locate 3 in the inorder array:\n[9, 3, 15, 20, 7] Everything left of 3 belongs to the left subtree, and everything right of 3 belongs to the right subtree.\nThat is the essential split of the entire problem.\nStep 2: Decide what state the recursion really needs Cutting arrays into slices works, but copies too much. The cleaner state is:\npre_idx: which preorder value is the next root l, r: the current inorder interval So the subproblem becomes:\nbuild the subtree whose inorder range is inorder[l..r], using preorder roots in sequence.\nStep 3: Define the smaller subproblem Once the current root is known, the remaining work splits naturally:\nbuild the left subtree from the left inorder segment build the right subtree from the right inorder segment So the recursion shape is:\nroot.left = build(l, mid - 1) root.right = build(mid + 1, r) Step 4: Define the base case If the current inorder interval is empty, there is no subtree:\nif l \u0026gt; r: return None Step 5: Read the current root from preorder Because preorder always lists the root first:\nroot_val = preorder[pre_idx] pre_idx += 1 That gives the root of the current subtree immediately.\nStep 6: Find the split point quickly Now locate the root inside inorder:\nmid = index[root_val] To avoid scanning linearly every time, precompute:\nindex = {value: i for i, value in enumerate(inorder)} Then each lookup is O(1).\nStep 7: Why must the recursion build left before right? Because preorder order is:\nroot -\u0026gt; left -\u0026gt; right After consuming the current root, the next preorder value belongs to the left subtree, not the right subtree.\nSo the correct order is:\nroot.left = build(l, mid - 1) root.right = build(mid + 1, r) If you swap them, pre_idx starts consuming right-subtree roots too early.\nStep 8: Walk the official example slowly Use:\npreorder = [3,9,20,15,7] inorder = [9,3,15,20,7] 3 is the first preorder value, so 3 is the root In inorder, 3 sits between [9] and [15,20,7] The next preorder value is 9, so that must be the left subtree root Only after the left subtree is finished do we move to preorder values for the right subtree This shows the exact division of labor:\npreorder chooses roots inorder chooses boundaries Step 9: Connect this to the general reconstruction pattern This is not special to preorder + inorder only. The broader pattern is:\none traversal identifies the current root another traversal tells you how the tree splits around that root That is the deeper reusable idea.\nAssemble the Full Code Now combine the traversal roles into the first full working solution.\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def build_tree(preorder, inorder): index = {value: i for i, value in enumerate(inorder)} pre_idx = 0 def build(l, r): nonlocal pre_idx if l \u0026gt; r: return None root_val = preorder[pre_idx] pre_idx += 1 root = TreeNode(root_val) mid = index[root_val] root.left = build(l, mid - 1) root.right = build(mid + 1, r) return root return build(0, len(inorder) - 1) def preorder_traversal(root): if root is None: return [] return [root.val] + preorder_traversal(root.left) + preorder_traversal(root.right) if __name__ == \u0026#34;__main__\u0026#34;: root = build_tree([3, 9, 20, 15, 7], [9, 3, 15, 20, 7]) print(preorder_traversal(root)) Reference Answer The LeetCode-style submission version is:\nfrom typing import List, Optional class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def buildTree(self, preorder: List[int], inorder: List[int]) -\u0026gt; Optional[TreeNode]: index = {value: i for i, value in enumerate(inorder)} pre_idx = 0 def build(l: int, r: int) -\u0026gt; Optional[TreeNode]: nonlocal pre_idx if l \u0026gt; r: return None root_val = preorder[pre_idx] pre_idx += 1 root = TreeNode(root_val) mid = index[root_val] root.left = build(l, mid - 1) root.right = build(mid + 1, r) return root return build(0, len(inorder) - 1) What method did we just build? You could call it:\ntraversal-based reconstruction indexed divide and conquer hash-assisted tree rebuilding But the core takeaway is:\nPreorder chooses the root, and inorder chooses the subtree boundary.\nE — Engineering Scenario 1: Rebuild a tree from root-first and inorder snapshots (Python) Background: a system stores two traversal snapshots and later needs to reconstruct the exact tree.\nWhy it fits: one sequence gives the root order, the other gives the subtree boundary.\ndef rebuild(pre, ino): pos = {v: i for i, v in enumerate(ino)} idx = 0 class Node: def __init__(self, val, left=None, right=None): self.val = val self.left = left self.right = right def dfs(l, r): nonlocal idx if l \u0026gt; r: return None val = pre[idx] idx += 1 m = pos[val] return Node(val, dfs(l, m - 1), dfs(m + 1, r)) return dfs(0, len(ino) - 1) root = rebuild([2, 1, 3], [1, 2, 3]) print(root.val) Scenario 2: Recover an expression tree from traversal logs (Go) Background: a parser stores traversal sequences for later reconstruction.\nWhy it fits: preorder gives the operator root, inorder gives the left/right operand split.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Val int Left *Node Right *Node } func buildTree(pre, ino []int) *Node { pos := map[int]int{} for i, v := range ino { pos[v] = i } idx := 0 var dfs func(int, int) *Node dfs = func(l, r int) *Node { if l \u0026gt; r { return nil } val := pre[idx] idx++ m := pos[val] root := \u0026amp;Node{Val: val} root.Left = dfs(l, m-1) root.Right = dfs(m+1, r) return root } return dfs(0, len(ino)-1) } func main() { root := buildTree([]int{2, 1, 3}, []int{1, 2, 3}) fmt.Println(root.Val) } Scenario 3: Restore a UI component hierarchy from traversal data (JavaScript) Background: a low-code system saves traversal outputs and wants to reconstruct the component tree later.\nWhy it fits: unique node IDs plus root order plus inorder boundaries are enough.\nfunction Node(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function buildTree(preorder, inorder) { const pos = new Map(); inorder.forEach((v, i) =\u0026gt; pos.set(v, i)); let idx = 0; function dfs(l, r) { if (l \u0026gt; r) return null; const val = preorder[idx++]; const m = pos.get(val); return new Node(val, dfs(l, m - 1), dfs(m + 1, r)); } return dfs(0, inorder.length - 1); } const root = buildTree([2, 1, 3], [1, 2, 3]); console.log(root.val); R — Reflection Complexity Analysis Time complexity: O(n) because each node is created once and each split-point lookup is O(1) Space complexity: O(n) from the hash map plus recursion stack Alternative Approaches Method Time Extra Space Notes Hash map + index recursion O(n) O(n) Best standard solution Linear scan in inorder every time worst O(n^2) O(h) Can degrade badly Recursive array slicing O(n) to O(n^2) higher Simpler to read, but copies too much Common Mistakes Building the right subtree before the left: this breaks preorder consumption order. Skipping the hash map: then every recursive step may scan inorder again. Ignoring the uniqueness requirement: duplicate values would make the split ambiguous. Mixing interval boundaries with preorder index semantics: keep those two responsibilities separate. FAQ and Notes 1. Why is preorder alone not enough? Preorder tells you root order, but not where the left subtree stops and the right subtree starts.\n2. Why is inorder alone not enough? Inorder tells you relative left/root/right arrangement, but not which node should become the current root first.\n3. Can postorder plus inorder reconstruct a tree too? Yes. The same pattern still holds:\none traversal identifies the root position the other gives the subtree boundary Best Practices Write one clear sentence for what preorder contributes and what inorder contributes Use inorder intervals rather than slicing arrays Let pre_idx do one thing only: consume roots in preorder order Pair this with problem 106 to reinforce the reconstruction pattern S — Summary The heart of problem 105 is the division of labor between preorder and inorder Preorder gives the root; inorder gives the left/right split A hash map turns split-point lookup into O(1) and keeps the whole algorithm at O(n) The left subtree must be built before the right subtree because preorder is root-left-right This problem is one of the cleanest traversal-to-structure reconstruction exercises Further Reading LeetCode 105: Construct Binary Tree from Preorder and Inorder Traversal LeetCode 106: Construct Binary Tree from Inorder and Postorder Traversal LeetCode 114: Flatten Binary Tree to Linked List LeetCode 94: Binary Tree Inorder Traversal CTA Practice 94 + 105 + 106 + 114 together. They build a very strong intuition for the relationship between traversal order and actual tree structure.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def build_tree(preorder, inorder): pos = {v: i for i, v in enumerate(inorder)} idx = 0 def build(l, r): nonlocal idx if l \u0026gt; r: return None val = preorder[idx] idx += 1 root = TreeNode(val) m = pos[val] root.left = build(l, m - 1) root.right = build(m + 1, r) return root return build(0, len(inorder) - 1) def preorder_traversal(root): if root is None: return [] return [root.val] + preorder_traversal(root.left) + preorder_traversal(root.right) if __name__ == \u0026#34;__main__\u0026#34;: root = build_tree([3, 9, 20, 15, 7], [9, 3, 15, 20, 7]) print(preorder_traversal(root)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } struct TreeNode* build(int* preorder, int* pos, int* idx, int l, int r) { if (l \u0026gt; r) return NULL; int val = preorder[(*idx)++]; struct TreeNode* root = new_node(val); int m = pos[val + 3000]; root-\u0026gt;left = build(preorder, pos, idx, l, m - 1); root-\u0026gt;right = build(preorder, pos, idx, m + 1, r); return root; } void preorder_print(struct TreeNode* root) { if (!root) return; printf(\u0026#34;%d \u0026#34;, root-\u0026gt;val); preorder_print(root-\u0026gt;left); preorder_print(root-\u0026gt;right); } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { int preorder[] = {3, 9, 20, 15, 7}; int inorder[] = {9, 3, 15, 20, 7}; int pos[6001] = {0}; for (int i = 0; i \u0026lt; 5; ++i) pos[inorder[i] + 3000] = i; int idx = 0; struct TreeNode* root = build(preorder, pos, \u0026amp;idx, 0, 4); preorder_print(root); printf(\u0026#34;\\n\u0026#34;); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;unordered_map\u0026gt; #include \u0026lt;vector\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; TreeNode* build(const std::vector\u0026lt;int\u0026gt;\u0026amp; preorder, int\u0026amp; idx, int l, int r, const std::unordered_map\u0026lt;int, int\u0026gt;\u0026amp; pos) { if (l \u0026gt; r) return nullptr; int val = preorder[idx++]; TreeNode* root = new TreeNode(val); int m = pos.at(val); root-\u0026gt;left = build(preorder, idx, l, m - 1, pos); root-\u0026gt;right = build(preorder, idx, m + 1, r, pos); return root; } void preorderPrint(TreeNode* root) { if (!root) return; std::cout \u0026lt;\u0026lt; root-\u0026gt;val \u0026lt;\u0026lt; \u0026#39; \u0026#39;; preorderPrint(root-\u0026gt;left); preorderPrint(root-\u0026gt;right); } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { std::vector\u0026lt;int\u0026gt; preorder{3, 9, 20, 15, 7}; std::vector\u0026lt;int\u0026gt; inorder{9, 3, 15, 20, 7}; std::unordered_map\u0026lt;int, int\u0026gt; pos; for (int i = 0; i \u0026lt; static_cast\u0026lt;int\u0026gt;(inorder.size()); ++i) pos[inorder[i]] = i; int idx = 0; TreeNode* root = build(preorder, idx, 0, static_cast\u0026lt;int\u0026gt;(inorder.size()) - 1, pos); preorderPrint(root); std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func buildTree(preorder []int, inorder []int) *TreeNode { pos := map[int]int{} for i, v := range inorder { pos[v] = i } idx := 0 var build func(int, int) *TreeNode build = func(l, r int) *TreeNode { if l \u0026gt; r { return nil } val := preorder[idx] idx++ root := \u0026amp;TreeNode{Val: val} m := pos[val] root.Left = build(l, m-1) root.Right = build(m+1, r) return root } return build(0, len(inorder)-1) } func preorderPrint(root *TreeNode) { if root == nil { return } fmt.Print(root.Val, \u0026#34; \u0026#34;) preorderPrint(root.Left) preorderPrint(root.Right) } func main() { root := buildTree([]int{3, 9, 20, 15, 7}, []int{9, 3, 15, 20, 7}) preorderPrint(root) fmt.Println() } use std::collections::HashMap; #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn build( preorder: \u0026amp;[i32], idx: \u0026amp;mut usize, l: i32, r: i32, pos: \u0026amp;HashMap\u0026lt;i32, i32\u0026gt;, ) -\u0026gt; Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt; { if l \u0026gt; r { return None; } let val = preorder[*idx]; *idx += 1; let m = *pos.get(\u0026amp;val).unwrap(); Some(Box::new(TreeNode { val, left: build(preorder, idx, l, m - 1, pos), right: build(preorder, idx, m + 1, r, pos), })) } fn preorder_collect(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, out: \u0026amp;mut Vec\u0026lt;i32\u0026gt;) { if let Some(node) = root { out.push(node.val); preorder_collect(\u0026amp;node.left, out); preorder_collect(\u0026amp;node.right, out); } } fn main() { let preorder = vec![3, 9, 20, 15, 7]; let inorder = [9, 3, 15, 20, 7]; let mut pos = HashMap::new(); for (i, v) in inorder.iter().enumerate() { pos.insert(*v, i as i32); } let mut idx = 0usize; let root = build(\u0026amp;preorder, \u0026amp;mut idx, 0, inorder.len() as i32 - 1, \u0026amp;pos); let mut out = Vec::new(); preorder_collect(\u0026amp;root, \u0026amp;mut out); println!(\u0026#34;{:?}\u0026#34;, out); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function buildTree(preorder, inorder) { const pos = new Map(); inorder.forEach((v, i) =\u0026gt; pos.set(v, i)); let idx = 0; function build(l, r) { if (l \u0026gt; r) return null; const val = preorder[idx++]; const m = pos.get(val); return new TreeNode(val, build(l, m - 1), build(m + 1, r)); } return build(0, inorder.length - 1); } function preorderCollect(root, out = []) { if (!root) return out; out.push(root.val); preorderCollect(root.left, out); preorderCollect(root.right, out); return out; } const root = buildTree([3, 9, 20, 15, 7], [9, 3, 15, 20, 7]); console.log(preorderCollect(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/105-construct-binary-tree-from-preorder-and-inorder-traversal/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nThe key to LeetCode 105 is not memorizing that preorder and inorder can reconstruct a tree. It is understanding what each traversal contributes: preorder tells you the root, inorder tells you the left/right boundary. Once those roles are clear, the whole problem becomes a clean indexed divide-and-conquer.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003edivide and conquer\u003c/code\u003e, \u003ccode\u003ehash map\u003c/code\u003e, \u003ccode\u003epreorder\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Construct Binary Tree from Preorder and Inorder Traversal, preorder, inorder, divide and conquer, hash map, LeetCode 105\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 105 from traversal roles, indexed recursion, and hash-map root lookup, with runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the preorder traversal \u003ccode\u003epreorder\u003c/code\u003e and inorder traversal \u003ccode\u003einorder\u003c/code\u003e of a binary tree, reconstruct the tree and return its root.\u003c/p\u003e","title":"Hot100: Construct Binary Tree from Preorder and Inorder Traversal (Indexed Divide-and-Conquer ACERS Guide)"},{"content":" Subtitle / Summary The key to LeetCode 108 is not recursion by itself. It is noticing that the problem wants two things at the same time: BST ordering and height balance. Once you read those two constraints together, \u0026ldquo;pick the middle element as the root\u0026rdquo; stops being a trick and becomes the natural construction rule.\nReading time: 11-14 min Tags: Hot100, binary tree, BST, divide and conquer, recursion SEO keywords: Convert Sorted Array to Binary Search Tree, BST, balanced BST, divide and conquer, recursion, LeetCode 108 Meta description: Learn LeetCode 108 from the midpoint-construction idea, with step-by-step derivation, engineering mappings, and runnable multi-language implementations. A — Algorithm Problem Restatement Given an integer array nums sorted in strictly increasing order, convert it into a height-balanced binary search tree.\nThat means the output tree must satisfy two requirements:\nit must be a valid BST it should be as balanced as possible Input / Output Name Type Description nums int[] strictly increasing sorted array return TreeNode root of any valid height-balanced BST Example 1 input: nums = [-10,-3,0,5,9] output: [0,-3,9,-10,null,5] explanation: [0,-10,5,null,-3,null,9] is also correct. Example 2 input: nums = [1,3] output: [3,1] explanation: [1,null,3] and [3,1] are both height-balanced BSTs. Constraints 1 \u0026lt;= nums.length \u0026lt;= 10^4 -10^4 \u0026lt;= nums[i] \u0026lt;= 10^4 nums is sorted in strictly increasing order Target Readers Hot100 learners who want one stable \u0026ldquo;array to BST\u0026rdquo; construction template Developers who already know BST validation but still feel shaky about BST construction Engineers who want to understand why midpoint splitting is a direct consequence of the problem constraints Background / Motivation This problem is good training for one important habit:\nwhen a problem gives multiple structural requirements at once, find the split rule that satisfies all of them together A first instinct might be:\ninsert the values into a BST one by one or pick any root, then recursively attach the rest But the problem is not just asking for a BST. It is asking for a balanced BST built from an already sorted array.\nSo the right question is:\nWhich element should become the root if we want both ordering and balance?\nOnce you ask that, the midpoint choice becomes almost inevitable.\nCore Concepts BST ordering: all values on the left are smaller; all values on the right are larger Height balance: left and right subtree heights should stay close Interval recursion: use an array segment [l, r] to represent the subtree to build Midpoint root: splitting near the center keeps the tree shallow C — Concepts How To Build The Solution From Scratch Step 1: Start from the smallest non-trivial example Consider:\nnums = [1,2,3] If you choose 1 as the root, the tree becomes:\n1 \\ 2 \\ 3 That is still a BST, but it is obviously not balanced.\nIf you choose the middle value 2:\n2 / \\ 1 3 Now both requirements are satisfied.\nStep 2: Decide what state the recursion really needs We do not need to copy subarrays. We only need to know:\nwhere the current segment starts where the current segment ends So the recursive state is just:\nl r The subproblem becomes:\nbuild a height-balanced BST from nums[l..r].\nStep 3: Define the smaller subproblem Once the root position is chosen, the remaining work splits naturally:\nbuild the left subtree from the left half build the right subtree from the right half So the recursion shape is:\nroot.left = build(l, mid - 1) root.right = build(mid + 1, r) Step 4: Define the base case If the current interval is empty, there is no subtree to build:\nif l \u0026gt; r: return None That is the standard tree-construction base case.\nStep 5: Why is the midpoint the right root candidate? The tree should be height-balanced. The most direct way to encourage balance is to keep the left and right subtree sizes as close as possible.\nThat means the root should split the interval roughly in half:\nmid = (l + r) // 2 For even-length segments, either left-middle or right-middle is acceptable as long as you are consistent.\nStep 6: Build the current root Once mid is fixed, create the root node:\nroot = TreeNode(nums[mid]) Then recursively attach both sides:\nroot.left = build(l, mid - 1) root.right = build(mid + 1, r) Step 7: Why does this automatically satisfy BST ordering? Because the input array is strictly increasing.\nThat means:\nevery value left of mid is smaller than nums[mid] every value right of mid is larger than nums[mid] So BST ordering is already built into the array positions.\nStep 8: Why does this also keep the tree balanced? Because every step splits the current segment as evenly as possible.\nSo:\nleft and right subtree sizes differ by at most 1 at the current split repeated midpoint splits keep the tree height low This is not an extra trick. It follows directly from the balancing requirement.\nStep 9: Walk the official example slowly Use:\nnums = [-10,-3,0,5,9] The middle value is 0, so 0 becomes the root The left segment [-10,-3] builds the left subtree The right segment [5,9] builds the right subtree Each side repeats the same midpoint rule The entire construction is just one simple rule applied recursively.\nAssemble the Full Code Now combine those pieces into the first full working solution.\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def sorted_array_to_bst(nums): def build(l, r): if l \u0026gt; r: return None mid = (l + r) // 2 root = TreeNode(nums[mid]) root.left = build(l, mid - 1) root.right = build(mid + 1, r) return root return build(0, len(nums) - 1) def inorder(root): if root is None: return [] return inorder(root.left) + [root.val] + inorder(root.right) if __name__ == \u0026#34;__main__\u0026#34;: root = sorted_array_to_bst([-10, -3, 0, 5, 9]) print(inorder(root)) Reference Answer If you want the LeetCode-style submission version, it becomes:\nfrom typing import List, Optional class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def sortedArrayToBST(self, nums: List[int]) -\u0026gt; Optional[TreeNode]: def build(l: int, r: int) -\u0026gt; Optional[TreeNode]: if l \u0026gt; r: return None mid = (l + r) // 2 root = TreeNode(nums[mid]) root.left = build(l, mid - 1) root.right = build(mid + 1, r) return root return build(0, len(nums) - 1) What method did we just build? You could call it:\ndivide and conquer interval recursion midpoint BST construction But the real takeaway is this:\nWhen a problem wants both sorted BST order and good balance, midpoint splitting is the natural construction rule.\nE — Engineering Scenario 1: Build a balanced in-memory index from sorted keys (Python) Background: an offline job has already sorted the keys, and a service wants to rebuild an in-memory tree quickly at startup.\nWhy it fits: sequential insertion can degrade badly, while midpoint construction creates a much shallower search tree immediately.\nclass Node: def __init__(self, key, left=None, right=None): self.key = key self.left = left self.right = right def build_balanced(keys): def dfs(l, r): if l \u0026gt; r: return None m = (l + r) // 2 return Node(keys[m], dfs(l, m - 1), dfs(m + 1, r)) return dfs(0, len(keys) - 1) root = build_balanced([10, 20, 30, 40, 50]) print(root.key) Scenario 2: Build a shallow decision tree from ordered thresholds (C++) Background: some read-heavy systems pre-build a threshold tree so lookup depth stays predictable.\nWhy it fits: midpoint splitting keeps the worst-case comparison path close to logarithmic depth.\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; struct Node { int val; Node* left; Node* right; explicit Node(int x) : val(x), left(nullptr), right(nullptr) {} }; Node* build(const std::vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int r) { if (l \u0026gt; r) return nullptr; int m = (l + r) / 2; Node* root = new Node(a[m]); root-\u0026gt;left = build(a, l, m - 1); root-\u0026gt;right = build(a, m + 1, r); return root; } int main() { std::vector\u0026lt;int\u0026gt; a{5, 10, 20, 40, 80}; Node* root = build(a, 0, static_cast\u0026lt;int\u0026gt;(a.size()) - 1); std::cout \u0026lt;\u0026lt; root-\u0026gt;val \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } Scenario 3: Convert sorted breakpoints into a balanced rule tree (JavaScript) Background: a UI rule engine stores ordered breakpoints and wants a tree-shaped runtime matcher.\nWhy it fits: a balanced tree reduces the maximum number of comparisons during rule selection.\nfunction Node(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function buildBalanced(arr, l = 0, r = arr.length - 1) { if (l \u0026gt; r) return null; const m = Math.floor((l + r) / 2); return new Node(arr[m], buildBalanced(arr, l, m - 1), buildBalanced(arr, m + 1, r)); } const root = buildBalanced([320, 480, 768, 1024, 1280]); console.log(root.val); R — Reflection Complexity Analysis Time complexity: O(n) because each array element becomes exactly one tree node Space complexity: O(log n) to O(n) depending on recursion depth; with midpoint splitting it is typically O(log n) Alternative Approaches Method Time Extra Space Notes Midpoint divide-and-conquer O(n) O(log n) Best match for the problem Insert values one by one into BST best O(n log n), worst O(n^2) variable Can easily degenerate Slice arrays at each recursive step O(n) to O(n log n) higher Simpler to read, but copies too much Common Mistakes Thinking only about BST order and ignoring balance: then sequential insertion starts looking attractive, but it misses the point of the problem. Slicing arrays inside recursion: it works, but adds unnecessary copying. Mixing interval boundaries: mid - 1 and mid + 1 are easy to swap by mistake. Assuming the answer is unique: the problem allows multiple valid balanced BSTs. FAQ and Notes 1. Why are both left-middle and right-middle valid for even lengths? Because the subtree sizes still differ by at most 1. The problem does not require a unique tree shape.\n2. Why do we not need explicit BST validation here? Because BST ordering is already guaranteed by the sorted array:\neverything left of mid is smaller everything right of mid is larger 3. Is this basically binary search? The split looks similar, but the goal is different:\nbinary search finds one target this problem uses binary-style splitting to construct a low-height tree Best Practices Define clearly what the current interval means before writing recursion Pass indices instead of copying subarrays When a problem says \u0026ldquo;sorted\u0026rdquo; plus \u0026ldquo;balanced\u0026rdquo;, test midpoint splitting first Pair this problem with 98 and 230 to complete the BST construction / validation / query trio S — Summary LeetCode 108 is really about reading \u0026ldquo;sorted\u0026rdquo; and \u0026ldquo;balanced\u0026rdquo; together Choosing the midpoint as the root satisfies BST ordering and balance at the same time The recursion only needs an interval [l, r]; subarray copies are unnecessary This is a clean divide-and-conquer construction template worth memorizing Problem 108 pairs naturally with 98 and 230 in a BST learning path Further Reading LeetCode 108: Convert Sorted Array to Binary Search Tree LeetCode 98: Validate Binary Search Tree LeetCode 104: Maximum Depth of Binary Tree LeetCode 230: Kth Smallest Element in a BST CTA Try practicing 108 + 98 + 230 together. They form a very clean BST sequence: construction, validation, and ordered query.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def sorted_array_to_bst(nums): def build(l, r): if l \u0026gt; r: return None m = (l + r) // 2 root = TreeNode(nums[m]) root.left = build(l, m - 1) root.right = build(m + 1, r) return root return build(0, len(nums) - 1) def inorder(root): if root is None: return [] return inorder(root.left) + [root.val] + inorder(root.right) if __name__ == \u0026#34;__main__\u0026#34;: root = sorted_array_to_bst([-10, -3, 0, 5, 9]) print(inorder(root)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } struct TreeNode* build(int* nums, int l, int r) { if (l \u0026gt; r) return NULL; int m = (l + r) / 2; struct TreeNode* root = new_node(nums[m]); root-\u0026gt;left = build(nums, l, m - 1); root-\u0026gt;right = build(nums, m + 1, r); return root; } void inorder(struct TreeNode* root) { if (!root) return; inorder(root-\u0026gt;left); printf(\u0026#34;%d \u0026#34;, root-\u0026gt;val); inorder(root-\u0026gt;right); } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { int nums[] = {-10, -3, 0, 5, 9}; struct TreeNode* root = build(nums, 0, 4); inorder(root); printf(\u0026#34;\\n\u0026#34;); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; TreeNode* build(const std::vector\u0026lt;int\u0026gt;\u0026amp; nums, int l, int r) { if (l \u0026gt; r) return nullptr; int m = (l + r) / 2; TreeNode* root = new TreeNode(nums[m]); root-\u0026gt;left = build(nums, l, m - 1); root-\u0026gt;right = build(nums, m + 1, r); return root; } void inorder(TreeNode* root) { if (!root) return; inorder(root-\u0026gt;left); std::cout \u0026lt;\u0026lt; root-\u0026gt;val \u0026lt;\u0026lt; \u0026#39; \u0026#39;; inorder(root-\u0026gt;right); } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { std::vector\u0026lt;int\u0026gt; nums{-10, -3, 0, 5, 9}; TreeNode* root = build(nums, 0, static_cast\u0026lt;int\u0026gt;(nums.size()) - 1); inorder(root); std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func sortedArrayToBST(nums []int) *TreeNode { var build func(int, int) *TreeNode build = func(l, r int) *TreeNode { if l \u0026gt; r { return nil } m := (l + r) / 2 root := \u0026amp;TreeNode{Val: nums[m]} root.Left = build(l, m-1) root.Right = build(m+1, r) return root } return build(0, len(nums)-1) } func inorder(root *TreeNode) { if root == nil { return } inorder(root.Left) fmt.Print(root.Val, \u0026#34; \u0026#34;) inorder(root.Right) } func main() { root := sortedArrayToBST([]int{-10, -3, 0, 5, 9}) inorder(root) fmt.Println() } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn build(nums: \u0026amp;[i32], l: i32, r: i32) -\u0026gt; Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt; { if l \u0026gt; r { return None; } let m = (l + r) / 2; Some(Box::new(TreeNode { val: nums[m as usize], left: build(nums, l, m - 1), right: build(nums, m + 1, r), })) } fn inorder(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, out: \u0026amp;mut Vec\u0026lt;i32\u0026gt;) { if let Some(node) = root { inorder(\u0026amp;node.left, out); out.push(node.val); inorder(\u0026amp;node.right, out); } } fn main() { let nums = vec![-10, -3, 0, 5, 9]; let root = build(\u0026amp;nums, 0, nums.len() as i32 - 1); let mut out = Vec::new(); inorder(\u0026amp;root, \u0026amp;mut out); println!(\u0026#34;{:?}\u0026#34;, out); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function sortedArrayToBST(nums) { function build(l, r) { if (l \u0026gt; r) return null; const m = Math.floor((l + r) / 2); return new TreeNode(nums[m], build(l, m - 1), build(m + 1, r)); } return build(0, nums.length - 1); } function inorder(root, out = []) { if (!root) return out; inorder(root.left, out); out.push(root.val); inorder(root.right, out); return out; } const root = sortedArrayToBST([-10, -3, 0, 5, 9]); console.log(inorder(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/108-convert-sorted-array-to-binary-search-tree/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nThe key to LeetCode 108 is not recursion by itself. It is noticing that the problem wants two things at the same time: BST ordering and height balance. Once you read those two constraints together, \u0026ldquo;pick the middle element as the root\u0026rdquo; stops being a trick and becomes the natural construction rule.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 11-14 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eBST\u003c/code\u003e, \u003ccode\u003edivide and conquer\u003c/code\u003e, \u003ccode\u003erecursion\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Convert Sorted Array to Binary Search Tree, BST, balanced BST, divide and conquer, recursion, LeetCode 108\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 108 from the midpoint-construction idea, with step-by-step derivation, engineering mappings, and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven an integer array \u003ccode\u003enums\u003c/code\u003e sorted in strictly increasing order, convert it into a height-balanced binary search tree.\u003c/p\u003e","title":"Hot100: Convert Sorted Array to Binary Search Tree (Midpoint Divide-and-Conquer ACERS Guide)"},{"content":" Subtitle / Summary The real difficulty of LeetCode 114 is not \u0026ldquo;flattening\u0026rdquo; a tree. It is rewiring pointers without destroying the structure you still need. Once you notice that the final linked list must follow preorder order, reverse preorder with a prev pointer becomes a very stable in-place solution.\nReading time: 12-15 min Tags: Hot100, binary tree, preorder, in-place, recursion SEO keywords: Flatten Binary Tree to Linked List, preorder, in-place, reverse preorder, LeetCode 114 Meta description: Learn LeetCode 114 from preorder order and reverse-preorder rewiring, with step-by-step derivation, engineering mappings, and runnable multi-language implementations. A — Algorithm Problem Restatement Given the root root of a binary tree, flatten the tree into a linked list in place.\nThe flattened structure must satisfy:\nevery left pointer becomes null the right pointers form a single chain the chain order must be exactly the preorder traversal order of the original tree Input / Output Name Type Description root TreeNode root of the binary tree return void modify the tree in place Example 1 input: root = [1,2,5,3,4,null,6] output: [1,null,2,null,3,null,4,null,5,null,6] Example 2 input: root = [] output: [] Example 3 input: root = [0] output: [0] Constraints The number of nodes in the tree is in the range [0, 2000] -100 \u0026lt;= Node.val \u0026lt;= 100 Follow-up Can you flatten the tree with O(1) extra space?\nTarget Readers Hot100 learners who already know preorder traversal but still struggle with pointer rewiring Developers who often lose subtrees when doing in-place tree transformations Readers who want to connect preorder order with structural rewriting Background / Motivation This problem is not mainly about traversal. It is about how to modify pointers safely while keeping the required final order.\nA first solution might be:\ncollect nodes in preorder into an array reconnect them afterward That works, but it uses O(n) extra space and misses the deeper skill:\nhow to transform a tree into a chain in place So the real question is:\nIf the final chain must follow preorder order, what processing direction lets me rewire safely without losing future nodes?\nCore Concepts Preorder traversal: root, left, right Reverse preorder: right, left, root prev pointer: the next node in the final flattened chain In-place rewiring: reuse the existing nodes and modify their pointers directly C — Concepts How To Build The Solution From Scratch Step 1: Clarify what the final chain must look like For:\n1 / \\ 2 5 / \\ \\ 3 4 6 The preorder traversal is:\n[1,2,3,4,5,6] So the flattened list must become:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; 6 That means the problem is really:\nRewire the tree so the right chain follows preorder order.\nStep 2: Why is forward rewiring risky? Suppose you stand at node 1 and immediately try:\n1.right = 2 1.left = null You can easily lose the original right subtree rooted at 5 unless you save and reconnect it carefully.\nSo a direct forward rewrite is fragile.\nStep 3: Reverse the direction of thought If it is risky to build the chain from front to back, do the opposite:\nbuild the tail first, then connect the current node to it\nThe final order is:\nroot -\u0026gt; left -\u0026gt; right So the reverse processing order is:\nright -\u0026gt; left -\u0026gt; root That is reverse preorder.\nStep 4: Define the base case An empty node needs no rewiring:\nif node is None: return Step 5: Why must recursion go right first? Because when we return to the current node, we want prev to already point to the correct chain that should follow it.\nSo the recursive order is:\ndfs(node.right) dfs(node.left) Step 6: Define what prev means prev should mean:\nthe node that should come immediately after the current node in the final flattened chain\nThat makes the rewiring step very small:\nnode.right = prev node.left = None prev = node Step 7: Why do those three lines work? At the moment the recursion comes back to node:\nthe right side has already been flattened the left side has already been flattened prev already points to the chain that should follow node So connecting node.right to prev is exactly the needed link.\nStep 8: Walk the official example slowly Use:\n[1,2,5,3,4,null,6] Reverse preorder visits nodes in this order:\n6 -\u0026gt; 5 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 1 Rewiring happens during the return:\n6.right = None, prev = 6 5.right = 6, prev = 5 4.right = 5, prev = 4 3.right = 4, prev = 3 2.right = 3, prev = 2 1.right = 2, prev = 1 The final chain is exactly the preorder sequence.\nStep 9: Connect this to the follow-up The recursive prev solution is excellent for understanding the problem.\nThe O(1) extra-space follow-up can be solved with iterative predecessor rewiring, but that method is more subtle. For derivation-first learning, reverse preorder is the cleaner starting point.\nAssemble the Full Code Now combine the reverse-preorder steps into the first full working solution.\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def flatten(root): prev = None def dfs(node): nonlocal prev if node is None: return dfs(node.right) dfs(node.left) node.right = prev node.left = None prev = node dfs(root) def collect_chain(root): ans = [] cur = root while cur: ans.append(cur.val) cur = cur.right return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode( 1, TreeNode(2, TreeNode(3), TreeNode(4)), TreeNode(5, None, TreeNode(6)), ) flatten(root) print(collect_chain(root)) Reference Answer The LeetCode-style submission version is:\nfrom typing import Optional class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def flatten(self, root: Optional[TreeNode]) -\u0026gt; None: prev = None def dfs(node: Optional[TreeNode]) -\u0026gt; None: nonlocal prev if node is None: return dfs(node.right) dfs(node.left) node.right = prev node.left = None prev = node dfs(root) What method did we just build? You could call it:\nreverse preorder recursion in-place tree rewiring successor-link construction with prev But the core insight is:\nIf forward rewiring is dangerous, prepare the tail first and reconnect backward.\nE — Engineering Scenario 1: Flatten a module tree into startup order (Python) Background: a system stores modules in a tree, but startup needs a linear execution chain.\nWhy it fits: once the target order is fixed, in-place rewiring can avoid extra node copies.\nclass Node: def __init__(self, name, left=None, right=None): self.name = name self.left = left self.right = right def flatten(root): prev = None def dfs(node): nonlocal prev if node is None: return dfs(node.right) dfs(node.left) node.right = prev node.left = None prev = node dfs(root) root = Node(\u0026#34;core\u0026#34;, Node(\u0026#34;auth\u0026#34;), Node(\u0026#34;feed\u0026#34;)) flatten(root) print(root.name, root.right.name) Scenario 2: Turn a task tree into a linear execution chain (Go) Background: a task tree needs to be flattened into one executable order.\nWhy it fits: reverse preorder lets each task reconnect to the correct successor safely.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Name string Left *Node Right *Node } func flatten(root *Node) { var prev *Node var dfs func(*Node) dfs = func(node *Node) { if node == nil { return } dfs(node.Right) dfs(node.Left) node.Right = prev node.Left = nil prev = node } dfs(root) } func main() { root := \u0026amp;Node{Name: \u0026#34;A\u0026#34;, Left: \u0026amp;Node{Name: \u0026#34;B\u0026#34;}, Right: \u0026amp;Node{Name: \u0026#34;C\u0026#34;}} flatten(root) fmt.Println(root.Name, root.Right.Name) } Scenario 3: Flatten a menu tree into a tab order chain (JavaScript) Background: a compact UI mode may want a tree-like menu represented as a linear focus order.\nWhy it fits: the same rewiring idea converts a tree into one right-linked chain.\nfunction Node(name, left = null, right = null) { this.name = name; this.left = left; this.right = right; } function flatten(root) { let prev = null; function dfs(node) { if (!node) return; dfs(node.right); dfs(node.left); node.right = prev; node.left = null; prev = node; } dfs(root); } const root = new Node(\u0026#34;Home\u0026#34;, new Node(\u0026#34;Docs\u0026#34;), new Node(\u0026#34;Blog\u0026#34;)); flatten(root); console.log(root.name, root.right.name); R — Reflection Complexity Analysis Time complexity: O(n) because each node is rewired once Space complexity: O(h) from recursion depth Alternative Approaches Method Time Extra Space Notes Reverse preorder + prev O(n) O(h) Best learning-first solution Preorder array + reconnect O(n) O(n) Easy, but uses extra storage Stack-based preorder rewrite O(n) O(n) Also valid, but heavier Iterative predecessor rewiring O(n) O(1) Matches the follow-up, but is trickier to derive Common Mistakes Rewiring forward too early: that can disconnect the original right subtree before it is processed. Forgetting to clear left: the final linked list must use only right pointers. Misdefining prev: it is not just the previously visited node; it is the next node in the final chain. Using left-right-root instead of right-left-root: then the rewiring order breaks. FAQ and Notes 1. Why must recursion go right first and then left? Because the final chain is preorder:\nroot -\u0026gt; left -\u0026gt; right So the safe reverse processing order is:\nright -\u0026gt; left -\u0026gt; root 2. Why can prev become node.right directly? Because at that moment prev already points to the chain that must come after the current node in the final preorder list.\n3. What about the O(1) follow-up? You can solve it iteratively by:\nfinding the rightmost node of the left subtree attaching the original right subtree there moving the left subtree to the right side That is a great follow-up, but reverse preorder is easier to understand first.\nBest Practices Write down the target order before rewiring pointers If forward construction is fragile, try reversing the processing order Give prev one precise sentence of meaning before coding Pair this problem with preorder traversal and tree reconstruction problems S — Summary The flattened linked list must follow preorder order Reverse preorder with a prev pointer is a stable way to build that list in place prev means \u0026ldquo;the next node in the final chain\u0026rdquo;, which is the key invariant This problem is really about safe in-place tree transformation, not just traversal After understanding this version, the O(1) predecessor-based solution becomes much easier to appreciate Further Reading LeetCode 114: Flatten Binary Tree to Linked List LeetCode 144: Binary Tree Preorder Traversal LeetCode 105: Construct Binary Tree from Preorder and Inorder Traversal LeetCode 173: Binary Search Tree Iterator CTA Practice 144 + 114 + 105 together. They connect preorder order, structural transformation, and reconstruction into one very useful tree pattern set.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def flatten(root): prev = None def dfs(node): nonlocal prev if node is None: return dfs(node.right) dfs(node.left) node.right = prev node.left = None prev = node dfs(root) def collect(root): ans = [] cur = root while cur: ans.append(cur.val) cur = cur.right return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(1, TreeNode(2, TreeNode(3), TreeNode(4)), TreeNode(5, None, TreeNode(6))) flatten(root) print(collect(root)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } void dfs(struct TreeNode* node, struct TreeNode** prev) { if (!node) return; dfs(node-\u0026gt;right, prev); dfs(node-\u0026gt;left, prev); node-\u0026gt;right = *prev; node-\u0026gt;left = NULL; *prev = node; } void flatten(struct TreeNode* root) { struct TreeNode* prev = NULL; dfs(root, \u0026amp;prev); } void print_chain(struct TreeNode* root) { while (root) { printf(\u0026#34;%d \u0026#34;, root-\u0026gt;val); root = root-\u0026gt;right; } printf(\u0026#34;\\n\u0026#34;); } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(1); root-\u0026gt;left = new_node(2); root-\u0026gt;right = new_node(5); root-\u0026gt;left-\u0026gt;left = new_node(3); root-\u0026gt;left-\u0026gt;right = new_node(4); root-\u0026gt;right-\u0026gt;right = new_node(6); flatten(root); print_chain(root); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; void dfs(TreeNode* node, TreeNode*\u0026amp; prev) { if (!node) return; dfs(node-\u0026gt;right, prev); dfs(node-\u0026gt;left, prev); node-\u0026gt;right = prev; node-\u0026gt;left = nullptr; prev = node; } void flatten(TreeNode* root) { TreeNode* prev = nullptr; dfs(root, prev); } void printChain(TreeNode* root) { while (root) { std::cout \u0026lt;\u0026lt; root-\u0026gt;val \u0026lt;\u0026lt; \u0026#39; \u0026#39;; root = root-\u0026gt;right; } std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(1); root-\u0026gt;left = new TreeNode(2); root-\u0026gt;right = new TreeNode(5); root-\u0026gt;left-\u0026gt;left = new TreeNode(3); root-\u0026gt;left-\u0026gt;right = new TreeNode(4); root-\u0026gt;right-\u0026gt;right = new TreeNode(6); flatten(root); printChain(root); freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func flatten(root *TreeNode) { var prev *TreeNode var dfs func(*TreeNode) dfs = func(node *TreeNode) { if node == nil { return } dfs(node.Right) dfs(node.Left) node.Right = prev node.Left = nil prev = node } dfs(root) } func printChain(root *TreeNode) { for root != nil { fmt.Print(root.Val, \u0026#34; \u0026#34;) root = root.Right } fmt.Println() } func main() { root := \u0026amp;TreeNode{ Val: 1, Left: \u0026amp;TreeNode{ Val: 2, Left: \u0026amp;TreeNode{Val: 3}, Right: \u0026amp;TreeNode{Val: 4}, }, Right: \u0026amp;TreeNode{ Val: 5, Right: \u0026amp;TreeNode{Val: 6}, }, } flatten(root) printChain(root) } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn flatten(root: \u0026amp;mut Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) { fn dfs(node: \u0026amp;mut Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, prev: \u0026amp;mut Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) { if let Some(mut cur) = node.take() { dfs(\u0026amp;mut cur.right, prev); dfs(\u0026amp;mut cur.left, prev); cur.right = prev.take(); cur.left = None; *prev = Some(cur); } } let mut prev = None; dfs(root, \u0026amp;mut prev); *root = prev; } fn collect(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let mut ans = Vec::new(); let mut cur = root.as_deref(); while let Some(node) = cur { ans.push(node.val); cur = node.right.as_deref(); } ans } fn main() { let mut root = Some(Box::new(TreeNode { val: 1, left: Some(Box::new(TreeNode { val: 2, left: Some(Box::new(TreeNode { val: 3, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 4, left: None, right: None, })), })), right: Some(Box::new(TreeNode { val: 5, left: None, right: Some(Box::new(TreeNode { val: 6, left: None, right: None, })), })), })); flatten(\u0026amp;mut root); println!(\u0026#34;{:?}\u0026#34;, collect(\u0026amp;root)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function flatten(root) { let prev = null; function dfs(node) { if (!node) return; dfs(node.right); dfs(node.left); node.right = prev; node.left = null; prev = node; } dfs(root); } function collect(root) { const ans = []; let cur = root; while (cur) { ans.push(cur.val); cur = cur.right; } return ans; } const root = new TreeNode( 1, new TreeNode(2, new TreeNode(3), new TreeNode(4)), new TreeNode(5, null, new TreeNode(6)) ); flatten(root); console.log(collect(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/114-flatten-binary-tree-to-linked-list/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nThe real difficulty of LeetCode 114 is not \u0026ldquo;flattening\u0026rdquo; a tree. It is rewiring pointers without destroying the structure you still need. Once you notice that the final linked list must follow preorder order, reverse preorder with a \u003ccode\u003eprev\u003c/code\u003e pointer becomes a very stable in-place solution.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003epreorder\u003c/code\u003e, \u003ccode\u003ein-place\u003c/code\u003e, \u003ccode\u003erecursion\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Flatten Binary Tree to Linked List, preorder, in-place, reverse preorder, LeetCode 114\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 114 from preorder order and reverse-preorder rewiring, with step-by-step derivation, engineering mappings, and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the root \u003ccode\u003eroot\u003c/code\u003e of a binary tree, flatten the tree into a linked list in place.\u003c/p\u003e","title":"Hot100: Flatten Binary Tree to Linked List (Reverse Preorder Rewiring ACERS Guide)"},{"content":" Subtitle / Summary LeetCode 230 is not really about tree traversal mechanics alone. It is about turning BST order into a useful query. Once you see that the k-th smallest value is simply the k-th node visited in inorder traversal, the problem becomes a very stable counting task.\nReading time: 11-14 min Tags: Hot100, binary tree, BST, inorder traversal, stack SEO keywords: Kth Smallest Element in a BST, BST, inorder traversal, stack, LeetCode 230 Meta description: Learn LeetCode 230 from BST inorder ordering, explicit-stack counting, and early-stop traversal, with runnable multi-language implementations. A — Algorithm Problem Restatement Given the root root of a binary search tree and an integer k, return the k-th smallest value in the tree.\nHere k is 1-indexed.\nInput / Output Name Type Description root TreeNode root of the BST k int ranking position, 1 \u0026lt;= k \u0026lt;= n return int the k-th smallest node value Example 1 input: root = [3,1,4,null,2], k = 1 output: 1 Example 2 input: root = [5,3,6,2,4,null,null,1], k = 3 output: 3 Constraints The number of nodes in the tree is n 1 \u0026lt;= k \u0026lt;= n \u0026lt;= 10^4 0 \u0026lt;= Node.val \u0026lt;= 10^4 Follow-up If the BST is modified often and you need to query the k-th smallest frequently, how would you optimize it?\nTarget Readers Hot100 learners who know that BST inorder traversal is sorted but have not yet turned that fact into query logic Developers who want to connect problems 94, 98, and 230 into one BST skill chain Engineers who want a clean \u0026ldquo;rank query on an ordered tree\u0026rdquo; template Background / Motivation This problem is good training for one key transition:\nfrom \u0026ldquo;tree structure thinking\u0026rdquo; to \u0026ldquo;ordered sequence thinking\u0026rdquo; A naive solution might be:\ndo an inorder traversal collect all values into an array return the element at position k - 1 That works, but it does more work than necessary. A BST already gives you sorted order for free.\nSo the real question is:\nHow do we count nodes in inorder order and stop immediately once we reach the k-th one?\nCore Concepts BST inorder ordering: inorder traversal visits values in sorted ascending order Explicit stack: simulate recursive inorder traversal with full control Visit counting: each pop from the stack means one more sorted element has appeared Early stop: once the k-th node is reached, the rest of the tree is irrelevant C — Concepts How To Build The Solution From Scratch Step 1: Rewrite the problem in inorder language Take the smallest useful example:\n2 / \\ 1 3 k = 2 Its inorder traversal is:\n[1,2,3] So the 2nd smallest value is simply the 2nd visited node in inorder order.\nThis means the real problem is:\nFind the node visited at the k-th step of inorder traversal.\nStep 2: Decide what state the iterative traversal needs If we want to control inorder traversal manually, we need:\na stack for ancestors we still need to process a cur pointer to walk left That is the standard iterative inorder state.\nStep 3: Define the smaller subproblem At each point, we are repeatedly solving this smaller task:\nFind the next smallest unvisited node.\nThe rule is:\nkeep pushing the current node and go left when left is exhausted, pop the stack The popped node is the next value in sorted order.\nStep 4: Define when the work is complete A node is truly visited when it is popped from the stack:\ncur = stack.pop() count += 1 If:\ncount == k then we already have the answer and can return immediately.\nStep 5: Push the left chain first The inorder rule is left, root, right. So as long as cur exists, go left:\nwhile cur is not None: stack.append(cur) cur = cur.left This guarantees the next popped node is the next smallest value.\nStep 6: Count only when the node is actually visited Do not count at push time. The correct counting point is:\ncur = stack.pop() count += 1 because only then has the node been reached in true inorder order.\nStep 7: Continue with the right subtree After visiting the current node, inorder traversal must move to the right subtree:\ncur = cur.right Then the whole \u0026ldquo;go left as much as possible\u0026rdquo; process repeats.\nStep 8: Walk the official example slowly Use:\nroot = [3,1,4,null,2], k = 1 Start at 3, push it, go left to 1 Push 1, go left to null Pop 1 This is visit number 1, so the answer is immediately 1 We never need to traverse the rest of the tree. That is the value of early stopping.\nStep 9: Connect this to the follow-up For a single query, iterative inorder traversal is excellent.\nFor many queries with frequent insertions and deletions, the follow-up suggests a stronger structure:\nstore subtree sizes walk downward by rank instead of traversing from scratch But for the main problem, inorder counting is the right baseline.\nAssemble the Full Code Now combine the traversal and counting rules into the first full working solution.\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def kth_smallest(root, k): stack = [] cur = root count = 0 while cur is not None or stack: while cur is not None: stack.append(cur) cur = cur.left cur = stack.pop() count += 1 if count == k: return cur.val cur = cur.right return -1 if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(3, TreeNode(1, None, TreeNode(2)), TreeNode(4)) print(kth_smallest(root, 1)) Reference Answer The LeetCode-style submission version is:\nfrom typing import Optional class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def kthSmallest(self, root: Optional[TreeNode], k: int) -\u0026gt; int: stack = [] cur = root while cur is not None or stack: while cur is not None: stack.append(cur) cur = cur.left cur = stack.pop() k -= 1 if k == 0: return cur.val cur = cur.right return -1 What method did we just build? You could call it:\nBST inorder counting iterative inorder traversal rank query by traversal But the real takeaway is:\nIn a BST, the k-th smallest element is exactly the k-th node visited by inorder traversal.\nE — Engineering Scenario 1: Query the k-th threshold in an ordered rule tree (Python) Background: a system stores thresholds inside a BST-like structure and needs rank-based access.\nWhy it fits: inorder traversal yields sorted order without extra sorting work.\nclass Node: def __init__(self, val, left=None, right=None): self.val = val self.left = left self.right = right def kth(root, k): stack = [] cur = root while cur or stack: while cur: stack.append(cur) cur = cur.left cur = stack.pop() k -= 1 if k == 0: return cur.val cur = cur.right root = Node(20, Node(10), Node(30)) print(kth(root, 2)) Scenario 2: Rank query inside a read-heavy tree index (Go) Background: a mostly static tree index needs occasional rank-based key lookup.\nWhy it fits: explicit-stack inorder traversal is simple, stable, and avoids materializing the full order.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Val int Left *Node Right *Node } func kth(root *Node, k int) int { stack := []*Node{} cur := root for cur != nil || len(stack) \u0026gt; 0 { for cur != nil { stack = append(stack, cur) cur = cur.Left } cur = stack[len(stack)-1] stack = stack[:len(stack)-1] k-- if k == 0 { return cur.Val } cur = cur.Right } return -1 } func main() { root := \u0026amp;Node{Val: 2, Left: \u0026amp;Node{Val: 1}, Right: \u0026amp;Node{Val: 3}} fmt.Println(kth(root, 3)) } Scenario 3: Pick the k-th display node in a ranked UI tree (JavaScript) Background: a UI keeps ranked items in a tree-shaped structure and wants one item by order.\nWhy it fits: inorder traversal gives the ranking stream directly.\nfunction Node(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function kth(root, k) { const stack = []; let cur = root; while (cur || stack.length) { while (cur) { stack.push(cur); cur = cur.left; } cur = stack.pop(); k -= 1; if (k === 0) return cur.val; cur = cur.right; } return null; } const root = new Node(4, new Node(2, new Node(1), new Node(3)), new Node(6)); console.log(kth(root, 4)); R — Reflection Complexity Analysis Time complexity: worst-case O(n), but often closer to O(h + k) because traversal can stop early Space complexity: O(h) for the explicit stack Alternative Approaches Method Time Extra Space Notes Inorder traversal with early stop worst O(n) O(h) Best baseline for one query Full inorder array O(n) O(n) Works, but stores more than needed Subtree-size augmentation O(h) query extra bookkeeping Best for frequent updates + frequent rank queries Common Mistakes Treating the BST like a generic binary tree: then you miss the inorder-sorted property entirely. Counting when pushing nodes: the correct counting point is when the node is popped and truly visited. Forgetting early stop: then you keep traversing nodes that cannot change the answer. Off-by-one with k: the problem uses 1-based counting, not 0-based indexing. FAQ and Notes 1. Why is this so closely related to problem 94? Because 94 gives you the traversal order. Problem 230 simply adds counting and early stopping on top of that order.\n2. What does the follow-up optimization mean? If updates and rank queries are both frequent, you can store subtree sizes. Then instead of traversing in full inorder order, you walk down the tree by comparing k with the left subtree size.\n3. Can recursion solve this too? Yes. You can keep:\na global or nonlocal counter an answer variable But the explicit-stack version mirrors the iterative inorder template more clearly.\nBest Practices If you see \u0026ldquo;BST + k-th smallest\u0026rdquo;, think inorder immediately Count when the node is popped, not when it is pushed Stop as soon as the k-th node is reached If the interviewer asks about many updates and many queries, bring up subtree sizes S — Summary The core of problem 230 is turning BST order into a rank query The k-th smallest node is exactly the k-th node in inorder traversal Explicit-stack traversal gives precise visit order and clean early stopping For one query, inorder counting is usually enough Problems 94, 98, and 230 form a very clean BST fundamentals sequence Further Reading LeetCode 230: Kth Smallest Element in a BST LeetCode 94: Binary Tree Inorder Traversal LeetCode 98: Validate Binary Search Tree LeetCode 173: Binary Search Tree Iterator CTA Practice 94 + 98 + 230 together. They form a compact BST chain: traversal, validation, and ordered query.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def kth_smallest(root, k): stack = [] cur = root while cur is not None or stack: while cur is not None: stack.append(cur) cur = cur.left cur = stack.pop() k -= 1 if k == 0: return cur.val cur = cur.right return -1 if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(5, TreeNode(3, TreeNode(2, TreeNode(1)), TreeNode(4)), TreeNode(6)) print(kth_smallest(root, 3)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } int kthSmallest(struct TreeNode* root, int k) { struct TreeNode* stack[10016]; int top = 0; struct TreeNode* cur = root; while (cur != NULL || top \u0026gt; 0) { while (cur != NULL) { stack[top++] = cur; cur = cur-\u0026gt;left; } cur = stack[--top]; k--; if (k == 0) return cur-\u0026gt;val; cur = cur-\u0026gt;right; } return -1; } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(5); root-\u0026gt;left = new_node(3); root-\u0026gt;right = new_node(6); root-\u0026gt;left-\u0026gt;left = new_node(2); root-\u0026gt;left-\u0026gt;right = new_node(4); root-\u0026gt;left-\u0026gt;left-\u0026gt;left = new_node(1); printf(\u0026#34;%d\\n\u0026#34;, kthSmallest(root, 3)); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;stack\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; int kthSmallest(TreeNode* root, int k) { std::stack\u0026lt;TreeNode*\u0026gt; st; TreeNode* cur = root; while (cur || !st.empty()) { while (cur) { st.push(cur); cur = cur-\u0026gt;left; } cur = st.top(); st.pop(); if (--k == 0) return cur-\u0026gt;val; cur = cur-\u0026gt;right; } return -1; } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(5); root-\u0026gt;left = new TreeNode(3); root-\u0026gt;right = new TreeNode(6); root-\u0026gt;left-\u0026gt;left = new TreeNode(2); root-\u0026gt;left-\u0026gt;right = new TreeNode(4); root-\u0026gt;left-\u0026gt;left-\u0026gt;left = new TreeNode(1); std::cout \u0026lt;\u0026lt; kthSmallest(root, 3) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func kthSmallest(root *TreeNode, k int) int { stack := []*TreeNode{} cur := root for cur != nil || len(stack) \u0026gt; 0 { for cur != nil { stack = append(stack, cur) cur = cur.Left } cur = stack[len(stack)-1] stack = stack[:len(stack)-1] k-- if k == 0 { return cur.Val } cur = cur.Right } return -1 } func main() { root := \u0026amp;TreeNode{ Val: 5, Left: \u0026amp;TreeNode{ Val: 3, Left: \u0026amp;TreeNode{ Val: 2, Left: \u0026amp;TreeNode{Val: 1}, }, Right: \u0026amp;TreeNode{Val: 4}, }, Right: \u0026amp;TreeNode{Val: 6}, } fmt.Println(kthSmallest(root, 3)) } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn kth_smallest(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, mut k: i32) -\u0026gt; i32 { let mut stack: Vec\u0026lt;\u0026amp;TreeNode\u0026gt; = Vec::new(); let mut cur = root.as_deref(); while cur.is_some() || !stack.is_empty() { while let Some(node) = cur { stack.push(node); cur = node.left.as_deref(); } let node = stack.pop().unwrap(); k -= 1; if k == 0 { return node.val; } cur = node.right.as_deref(); } -1 } fn main() { let root = Some(Box::new(TreeNode { val: 5, left: Some(Box::new(TreeNode { val: 3, left: Some(Box::new(TreeNode { val: 2, left: Some(Box::new(TreeNode { val: 1, left: None, right: None, })), right: None, })), right: Some(Box::new(TreeNode { val: 4, left: None, right: None, })), })), right: Some(Box::new(TreeNode { val: 6, left: None, right: None, })), })); println!(\u0026#34;{}\u0026#34;, kth_smallest(\u0026amp;root, 3)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function kthSmallest(root, k) { const stack = []; let cur = root; while (cur || stack.length) { while (cur) { stack.push(cur); cur = cur.left; } cur = stack.pop(); k -= 1; if (k === 0) return cur.val; cur = cur.right; } return null; } const root = new TreeNode( 5, new TreeNode(3, new TreeNode(2, new TreeNode(1)), new TreeNode(4)), new TreeNode(6) ); console.log(kthSmallest(root, 3)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/230-kth-smallest-element-in-a-bst/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nLeetCode 230 is not really about tree traversal mechanics alone. It is about turning BST order into a useful query. Once you see that the k-th smallest value is simply the k-th node visited in inorder traversal, the problem becomes a very stable counting task.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 11-14 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eBST\u003c/code\u003e, \u003ccode\u003einorder traversal\u003c/code\u003e, \u003ccode\u003estack\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Kth Smallest Element in a BST, BST, inorder traversal, stack, LeetCode 230\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 230 from BST inorder ordering, explicit-stack counting, and early-stop traversal, with runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the root \u003ccode\u003eroot\u003c/code\u003e of a binary search tree and an integer \u003ccode\u003ek\u003c/code\u003e, return the \u003ccode\u003ek\u003c/code\u003e-th smallest value in the tree.\u003c/p\u003e","title":"Hot100: Kth Smallest Element in a BST (Inorder Counting / Early Stop ACERS Guide)"},{"content":" Subtitle / Summary\nLeetCode 2 is grade-school addition translated into a linked-list workflow. The only persistent state is the carry, and the only structural work is appending one new digit per round.\nReading time: 12-15 min Tags: Hot100, linked list, carry, simulation SEO keywords: Add Two Numbers, linked-list carry, reverse order digits, LeetCode 2, Hot100 Meta description: A derivation-first ACERS guide to LeetCode 2 with carry propagation, dummy node construction, engineering mapping, and runnable multi-language code. A - Algorithm (Problem and Algorithm) Problem Restatement You are given two non-empty linked lists l1 and l2. Each list stores a non-negative integer in reverse order, and each node contains one digit. Add the two integers and return the sum as a linked list in the same reverse order.\nInput / Output Name Type Description l1 ListNode first reverse-order digit list l2 ListNode second reverse-order digit list return ListNode reverse-order digit list for the sum Example 1 input: l1 = [2,4,3], l2 = [5,6,4] meaning: 342 + 465 = 807 output: [7,0,8] Example 2 input: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] output: [8,9,9,9,0,0,0,1] Target Readers Hot100 learners building reliable linked-list templates Developers who often make mistakes around carry propagation Engineers who want to map stream-wise state updates to list problems Background / Motivation This looks like an entry-level linked-list problem, but it exercises three reusable skills:\nmoving through two input chains in lockstep carrying state across rounds (carry) preserving correctness when the two inputs have different lengths The same pattern appears in engineering-style data processing:\ndigit-wise or chunk-wise aggregation stream alignment with missing values treated as zero incremental output construction with one persistent state variable Core Concepts Reverse-order storage: the least significant digit is at the list head Carry propagation: carry = total // 10, current digit = total % 10 Sentinel node (dummy): avoids special handling for the first output node Tail pointer: keeps appending the result in O(1) per digit C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the direction of digits Take:\nl1 = 2 -\u0026gt; 4 -\u0026gt; 3 l2 = 5 -\u0026gt; 6 -\u0026gt; 4 These lists represent:\n342 + 465 The important structural clue is that the head already stores the ones digit. So the list order is exactly the same order we use in grade-school addition:\nones tens hundreds That means we do not need to reverse anything first.\nStep 2: Ask what state one round of addition really needs In ordinary column addition, each round depends on only three values:\ncurrent digit from l1 current digit from l2 previous carry That is why the whole state can be compressed to:\ncarry = 0 plus the two input pointers and one output tail pointer.\nStep 3: Reject the unnecessary detours It is tempting to:\nrebuild the full integers first or copy digits to arrays and then add But both are worse fits for the problem:\nlarge integers may overflow in some languages arrays introduce extra space without giving us new information the lists are already aligned from low digit to high digit So the structure of the input is already optimized for one-pass simulation.\nStep 4: Define one addition round precisely One round does exactly this:\nx = l1.val if l1 else 0 y = l2.val if l2 else 0 total = x + y + carry carry = total // 10 digit = total % 10 Then append digit to the result list.\nThis is just vertical addition written in pointer form.\nStep 5: Ask why we need dummy and tail Every round creates one new output digit. So we need a stable way to append a node without special-casing the first append.\nThat is why we start with:\ndummy = ListNode(0) tail = dummy and keep doing:\ntail.next = ListNode(digit) tail = tail.next Step 6: Decide when the loop is truly finished The work is not done until all three sources are exhausted:\nl1 is fully consumed l2 is fully consumed carry becomes zero So the real loop condition is:\nwhile l1 != null or l2 != null or carry != 0 That final carry check is what handles cases like 5 + 5 = 10.\nStep 7: Walk one full trace slowly Using:\n2 -\u0026gt; 4 -\u0026gt; 3 5 -\u0026gt; 6 -\u0026gt; 4 Round 1:\n2 + 5 + 0 = 7 write 7 carry = 0 Round 2:\n4 + 6 + 0 = 10 write 0 carry = 1 Round 3:\n3 + 4 + 1 = 8 write 8 carry = 0 So the answer becomes:\n7 -\u0026gt; 0 -\u0026gt; 8 Step 8: Reduce the method to one sentence LeetCode 2 is \u0026ldquo;walk both lists from low digit to high digit, keep one carry, and append one new digit node per round.\u0026rdquo;\nAssemble the Full Code from typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def add_two_numbers(l1: Optional[ListNode], l2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode(0) tail = dummy carry = 0 while l1 is not None or l2 is not None or carry: x = l1.val if l1 is not None else 0 y = l2.val if l2 is not None else 0 total = x + y + carry carry = total // 10 tail.next = ListNode(total % 10) tail = tail.next if l1 is not None: l1 = l1.next if l2 is not None: l2 = l2.next return dummy.next def build(nums: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy for n in nums: tail.next = ListNode(n) tail = tail.next return dummy.next def dump(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head is not None: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: a = build([2, 4, 3]) b = build([5, 6, 4]) print(dump(add_two_numbers(a, b))) # [7, 0, 8] Reference Answer from typing import Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def addTwoNumbers(l1: Optional[ListNode], l2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode(0) tail = dummy carry = 0 while l1 is not None or l2 is not None or carry: x = l1.val if l1 is not None else 0 y = l2.val if l2 is not None else 0 total = x + y + carry carry = total // 10 tail.next = ListNode(total % 10) tail = tail.next if l1 is not None: l1 = l1.next if l2 is not None: l2 = l2.next return dummy.next Method Category Linked-list simulation Carry propagation Sentinel-node result construction State Model At each round we only need:\nl1: current node of the first number l2: current node of the second number carry: carry from previous digit tail: current end of the output list This is why the algorithm stays simple and linear.\nCorrectness Intuition Each round writes exactly one output digit:\nthe node value is the correct digit for the current place carry stores the only information that must move into the next place Because the input order is already low-digit first, processing from head to tail is exactly the correct arithmetic order.\nPractice Guide / Steps Create dummy and tail for the result list. Initialize carry = 0. While either list still has nodes or carry is non-zero: read current digits, using 0 for missing nodes compute total = x + y + carry append total % 10 update carry = total // 10 Advance l1 and l2 when available. Return dummy.next. Runnable Python example (add_two_numbers.py):\nfrom typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def add_two_numbers(l1: Optional[ListNode], l2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode(0) tail = dummy carry = 0 while l1 is not None or l2 is not None or carry: x = l1.val if l1 else 0 y = l2.val if l2 else 0 total = x + y + carry carry = total // 10 tail.next = ListNode(total % 10) tail = tail.next if l1: l1 = l1.next if l2: l2 = l2.next return dummy.next def build(nums: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy for x in nums: tail.next = ListNode(x) tail = tail.next return dummy.next def dump(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: print(dump(add_two_numbers(build([2, 4, 3]), build([5, 6, 4])))) Explanation / Why This Works The problem is already aligned with the natural digit-processing order. So there is no hidden template beyond:\nread current digits add carry emit one new digit move forward Because each input node is visited once and each output node is created once, the whole solution is linear in the input size.\nE - Engineering (Real-world Scenarios) Scenario 1: shard-wise amount accumulation (Python) Background: a finance job stores low-order chunks first for streaming accumulation.\nWhy it fits: carry propagation is exactly the same as digit-wise chunk addition.\ndef add_chunks(a, b, base=10): i = j = 0 carry = 0 out = [] while i \u0026lt; len(a) or j \u0026lt; len(b) or carry: x = a[i] if i \u0026lt; len(a) else 0 y = b[j] if j \u0026lt; len(b) else 0 total = x + y + carry carry = total // base out.append(total % base) i += 1 if i \u0026lt; len(a) else 0 j += 1 if j \u0026lt; len(b) else 0 return out print(add_chunks([2, 4, 3], [5, 6, 4])) Scenario 2: backend counter-stream merge (Go) Background: two services emit bucketed counts in the same low-to-high order.\nWhy it fits: missing buckets are treated as zero and one state variable carries overflow.\npackage main import \u0026#34;fmt\u0026#34; func addBuckets(a, b []int, base int) []int { i, j, carry := 0, 0, 0 out := []int{} for i \u0026lt; len(a) || j \u0026lt; len(b) || carry != 0 { x, y := 0, 0 if i \u0026lt; len(a) { x = a[i] i++ } if j \u0026lt; len(b) { y = b[j] j++ } total := x + y + carry carry = total / base out = append(out, total%base) } return out } func main() { fmt.Println(addBuckets([]int{2, 4, 3}, []int{5, 6, 4}, 10)) } Scenario 3: frontend offline draft version merge (JavaScript) Background: a client stores version digits in little-endian order for compact reconciliation.\nWhy it fits: the result can be built progressively with a single carry variable.\nfunction addVersionDigits(a, b) { let i = 0; let j = 0; let carry = 0; const out = []; while (i \u0026lt; a.length || j \u0026lt; b.length || carry !== 0) { const x = i \u0026lt; a.length ? a[i++] : 0; const y = j \u0026lt; b.length ? b[j++] : 0; const total = x + y + carry; carry = Math.floor(total / 10); out.push(total % 10); } return out; } console.log(addVersionDigits([2, 4, 3], [5, 6, 4])); R - Reflection (Complexity, Alternatives, Tradeoffs) Complexity Time: O(max(m, n)) Space: O(max(m, n)) for the output list itself Auxiliary space: O(1) Alternatives Method Time Extra Space Notes Rebuild integers first depends on language integer size O(1) or more overflow risk Copy to arrays then add O(m+n) O(m+n) works, but unnecessary One-pass linked-list simulation O(max(m,n)) O(1) auxiliary best fit for problem structure Common mistakes Forgetting to include carry in the loop condition. Stopping when one list ends even though the other still has nodes. Treating missing digits as an error instead of as zero. Trying to reverse the lists even though the problem already stores low digits first. Why this is the best practical method It matches the arithmetic model exactly:\nsame processing order as column addition no unnecessary data conversion minimal state direct handling of uneven lengths and final carry FAQ and Notes Why must the loop include carry?\nBecause the final digit may exist only because of a leftover carry, such as 5 + 5 = 10.\nCan we modify l1 or l2 in place?\nYou can in some variants, but creating a clean result list is simpler and less error-prone.\nWhat changes if digits are stored in forward order?\nThen you need stacks, recursion, or list reversal first. That is a different problem shape.\nBest Practices Always write the loop as while l1 or l2 or carry. Treat absent nodes as zero digits, not as separate branches. Use dummy + tail for result construction. Trace one uneven-length example before coding. S - Summary LeetCode 2 is just grade-school addition expressed as pointer logic. The only cross-round state is carry. Reverse-order storage lets us solve the problem in one forward pass. dummy + tail makes output construction uniform and safe. Further Reading LeetCode 2. Add Two Numbers LeetCode 445. Add Two Numbers II LeetCode 21. Merge Two Sorted Lists LeetCode 206. Reverse Linked List Conclusion If the carry update and loop condition feel natural to you after this problem, you have already internalized one of the most reusable linked-list simulation patterns in Hot100.\nReferences https://leetcode.com/problems/add-two-numbers/ https://docs.python.org/3/tutorial/datastructures.html https://go.dev/tour/basics/11 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array Meta Info Reading time: 12-15 min Tags: Hot100, linked list, carry, simulation, LeetCode 2 SEO keywords: Add Two Numbers, reverse-order digits, carry propagation, LeetCode 2, Hot100 Meta description: A derivation-first ACERS guide to Add Two Numbers with carry handling, dummy node construction, and runnable multi-language code. CTA Do two quick follow-ups:\nRe-implement the while l1 or l2 or carry template from memory. Then solve LeetCode 445 to see how the strategy changes when digits are stored in forward order. Multi-language Implementations (Python / C / C++ / Go / Rust / JS) Python from typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next class Solution: def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode(0) tail = dummy carry = 0 while l1 is not None or l2 is not None or carry: x = l1.val if l1 else 0 y = l2.val if l2 else 0 total = x + y + carry carry = total // 10 tail.next = ListNode(total % 10) tail = tail.next if l1: l1 = l1.next if l2: l2 = l2.next return dummy.next def build(nums: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy for v in nums: tail.next = ListNode(v) tail = tail.next return dummy.next def dump(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: ans = Solution().addTwoNumbers(build([2, 4, 3]), build([5, 6, 4])) print(dump(ans)) # [7, 0, 8] C #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct ListNode { int val; struct ListNode* next; }; struct ListNode* new_node(int v) { struct ListNode* n = (struct ListNode*)malloc(sizeof(struct ListNode)); n-\u0026gt;val = v; n-\u0026gt;next = NULL; return n; } struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2) { struct ListNode dummy = {0, NULL}; struct ListNode* tail = \u0026amp;dummy; int carry = 0; while (l1 != NULL || l2 != NULL || carry != 0) { int x = l1 ? l1-\u0026gt;val : 0; int y = l2 ? l2-\u0026gt;val : 0; int total = x + y + carry; carry = total / 10; tail-\u0026gt;next = new_node(total % 10); tail = tail-\u0026gt;next; if (l1) l1 = l1-\u0026gt;next; if (l2) l2 = l2-\u0026gt;next; } return dummy.next; } struct ListNode* build(const int* a, int n) { struct ListNode dummy = {0, NULL}; struct ListNode* tail = \u0026amp;dummy; for (int i = 0; i \u0026lt; n; ++i) { tail-\u0026gt;next = new_node(a[i]); tail = tail-\u0026gt;next; } return dummy.next; } void print_list(struct ListNode* h) { while (h) { printf(\u0026#34;%d\u0026#34;, h-\u0026gt;val); if (h-\u0026gt;next) printf(\u0026#34; -\u0026gt; \u0026#34;); h = h-\u0026gt;next; } printf(\u0026#34;\\n\u0026#34;); } void free_list(struct ListNode* h) { while (h) { struct ListNode* nxt = h-\u0026gt;next; free(h); h = nxt; } } int main(void) { int a[] = {2, 4, 3}; int b[] = {5, 6, 4}; struct ListNode* l1 = build(a, 3); struct ListNode* l2 = build(b, 3); struct ListNode* ans = addTwoNumbers(l1, l2); print_list(ans); // 7 -\u0026gt; 0 -\u0026gt; 8 free_list(l1); free_list(l2); free_list(ans); return 0; } C++ #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; struct ListNode { int val; ListNode* next; ListNode(int x = 0) : val(x), next(nullptr) {} }; class Solution { public: ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { ListNode dummy(0); ListNode* tail = \u0026amp;dummy; int carry = 0; while (l1 || l2 || carry) { int x = l1 ? l1-\u0026gt;val : 0; int y = l2 ? l2-\u0026gt;val : 0; int total = x + y + carry; carry = total / 10; tail-\u0026gt;next = new ListNode(total % 10); tail = tail-\u0026gt;next; if (l1) l1 = l1-\u0026gt;next; if (l2) l2 = l2-\u0026gt;next; } return dummy.next; } }; ListNode* build(const vector\u0026lt;int\u0026gt;\u0026amp; a) { ListNode dummy; ListNode* tail = \u0026amp;dummy; for (int v : a) { tail-\u0026gt;next = new ListNode(v); tail = tail-\u0026gt;next; } return dummy.next; } void printList(ListNode* h) { while (h) { cout \u0026lt;\u0026lt; h-\u0026gt;val; if (h-\u0026gt;next) cout \u0026lt;\u0026lt; \u0026#34; -\u0026gt; \u0026#34;; h = h-\u0026gt;next; } cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } void freeList(ListNode* h) { while (h) { ListNode* nxt = h-\u0026gt;next; delete h; h = nxt; } } int main() { ListNode* l1 = build({2, 4, 3}); ListNode* l2 = build({5, 6, 4}); ListNode* ans = Solution().addTwoNumbers(l1, l2); printList(ans); // 7 -\u0026gt; 0 -\u0026gt; 8 freeList(l1); freeList(l2); freeList(ans); return 0; } Go package main import \u0026#34;fmt\u0026#34; type ListNode struct { Val int Next *ListNode } func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode { dummy := \u0026amp;ListNode{} tail := dummy carry := 0 for l1 != nil || l2 != nil || carry != 0 { x, y := 0, 0 if l1 != nil { x = l1.Val l1 = l1.Next } if l2 != nil { y = l2.Val l2 = l2.Next } total := x + y + carry carry = total / 10 tail.Next = \u0026amp;ListNode{Val: total % 10} tail = tail.Next } return dummy.Next } func build(a []int) *ListNode { dummy := \u0026amp;ListNode{} tail := dummy for _, v := range a { tail.Next = \u0026amp;ListNode{Val: v} tail = tail.Next } return dummy.Next } func printList(h *ListNode) { for h != nil { fmt.Print(h.Val) if h.Next != nil { fmt.Print(\u0026#34; -\u0026gt; \u0026#34;) } h = h.Next } fmt.Println() } func main() { l1 := build([]int{2, 4, 3}) l2 := build([]int{5, 6, 4}) ans := addTwoNumbers(l1, l2) printList(ans) // 7 -\u0026gt; 0 -\u0026gt; 8 } Rust #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] fn new(val: i32) -\u0026gt; Self { ListNode { next: None, val } } } pub fn add_two_numbers( mut l1: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, mut l2: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, ) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut digits: Vec\u0026lt;i32\u0026gt; = Vec::new(); let mut carry = 0; while l1.is_some() || l2.is_some() || carry \u0026gt; 0 { let mut x = 0; let mut y = 0; if let Some(mut node) = l1 { x = node.val; l1 = node.next.take(); } else { l1 = None; } if let Some(mut node) = l2 { y = node.val; l2 = node.next.take(); } else { l2 = None; } let total = x + y + carry; carry = total / 10; digits.push(total % 10); } let mut head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; = None; let mut tail = \u0026amp;mut head; for d in digits { *tail = Some(Box::new(ListNode::new(d))); if let Some(node) = tail { tail = \u0026amp;mut node.next; } } head } fn build(nums: \u0026amp;[i32]) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; = None; let mut tail = \u0026amp;mut head; for \u0026amp;n in nums { *tail = Some(Box::new(ListNode::new(n))); if let Some(node) = tail { tail = \u0026amp;mut node.next; } } head } fn dump(mut head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let mut out = Vec::new(); while let Some(mut node) = head { out.push(node.val); head = node.next.take(); } out } fn main() { let l1 = build(\u0026amp;[2, 4, 3]); let l2 = build(\u0026amp;[5, 6, 4]); let ans = add_two_numbers(l1, l2); println!(\u0026#34;{:?}\u0026#34;, dump(ans)); // [7, 0, 8] } JavaScript function ListNode(val = 0, next = null) { this.val = val; this.next = next; } function addTwoNumbers(l1, l2) { const dummy = new ListNode(0); let tail = dummy; let carry = 0; while (l1 !== null || l2 !== null || carry !== 0) { const x = l1 ? l1.val : 0; const y = l2 ? l2.val : 0; const total = x + y + carry; carry = Math.floor(total / 10); tail.next = new ListNode(total % 10); tail = tail.next; if (l1) l1 = l1.next; if (l2) l2 = l2.next; } return dummy.next; } function build(arr) { const dummy = new ListNode(); let tail = dummy; for (const v of arr) { tail.next = new ListNode(v); tail = tail.next; } return dummy.next; } function dump(head) { const out = []; while (head) { out.push(head.val); head = head.next; } return out; } const ans = addTwoNumbers(build([2, 4, 3]), build([5, 6, 4])); console.log(dump(ans)); // [7, 0, 8] ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/2-add-two-numbers/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nLeetCode 2 is grade-school addition translated into a linked-list workflow. The only persistent state is the carry, and the only structural work is appending one new digit per round.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003ecarry\u003c/code\u003e, \u003ccode\u003esimulation\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Add Two Numbers, linked-list carry, reverse order digits, LeetCode 2, Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A derivation-first ACERS guide to LeetCode 2 with carry propagation, dummy node construction, engineering mapping, and runnable multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eYou are given two non-empty linked lists \u003ccode\u003el1\u003c/code\u003e and \u003ccode\u003el2\u003c/code\u003e.\nEach list stores a non-negative integer in \u003cstrong\u003ereverse order\u003c/strong\u003e, and each node contains one digit.\nAdd the two integers and return the sum as a linked list in the same reverse order.\u003c/p\u003e","title":"Hot100: Add Two Numbers Linked-List Carry Simulation ACERS Guide"},{"content":" Subtitle / Summary\nLeetCode 146 is not a memorize-the-template problem. It is the core cache-design exercise: how to support fast lookup and fast recency updates at the same time.\nReading time: 14-18 min Tags: Hot100, LRU, hash table, doubly linked list SEO keywords: LRU Cache, hash table + doubly linked list, O(1) get put, LeetCode 146, Hot100 Meta description: A derivation-first ACERS guide to LRU Cache using a hash table plus doubly linked list, with engineering scenarios, pitfalls, and runnable multi-language code. A - Algorithm (Problem and Algorithm) Problem Restatement Design a data structure LRUCache with:\nLRUCache(int capacity) initializes the cache with a positive capacity int get(int key) returns the value if the key exists, otherwise -1 void put(int key, int value) inserts or updates the key The cache must evict the least recently used key when capacity is exceeded, and both get and put must run in average O(1) time.\nInput / Output Operation Input Output Meaning constructor capacity cache object initialize cache get(key) key value or -1 lookup + refresh recency put(key, value) key, value none insert/update + maybe evict Example 1 LRUCache cache = new LRUCache(2) cache.put(1, 1) // cache: {1=1} cache.put(2, 2) // cache: {1=1, 2=2} cache.get(1) // returns 1, and 1 becomes most recently used cache.put(3, 3) // evicts key 2 cache.get(2) // returns -1 cache.put(4, 4) // evicts key 1 cache.get(1) // returns -1 cache.get(3) // returns 3 cache.get(4) // returns 4 Example 2 LRUCache cache = new LRUCache(2) cache.put(1, 10) cache.put(1, 99) // update existing key, still becomes most recent cache.get(1) // returns 99 Target Readers Hot100 learners who want to understand the structure instead of memorizing it Backend engineers building local caches or request-result stores Interview candidates who know the answer shape but cannot yet justify it Background / Motivation Any cache has two separate concerns:\nhow to find data quickly how to decide what to evict when the cache is full LRU assumes that recently used entries are more likely to be used again soon, so older untouched entries should be removed first. This policy shows up in:\nAPI response caches local metadata caches browser-side data reuse service configuration hot paths The difficulty is not the policy itself. It is making both lookup and recency updates fast enough.\nCore Concepts LRU (Least Recently Used): evict the key that has not been used for the longest time Recency update on access: a successful get changes the key\u0026rsquo;s position in the usage order Average O(1) operations: no linear scan on get or put Structure split: hash table for fast key lookup doubly linked list for fast order updates C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the operational requirements, not the final template name An LRU cache must support all of these at once:\nfind a key quickly mark a key as \u0026ldquo;most recent\u0026rdquo; after access remove the least recent key when full So the real question is:\nwhat state must be maintained so both lookup and recency changes stay constant-time?\nStep 2: Why one structure alone is not enough If we use only an array:\nthe order is visible but moving an accessed entry to the front is usually O(n) If we use only a linked list:\nmoving nodes can be fast but finding a key is O(n) If we use only a hash table:\nlookup is fast but the table does not remember which key is oldest or newest So the problem is inherently a combination problem.\nStep 3: Split the responsibilities cleanly The natural split is:\nhash table: key -\u0026gt; node doubly linked list: maintain recency order In the list:\nfront = most recently used (MRU) back = least recently used (LRU) We need a doubly linked list because removing an arbitrary node in O(1) requires direct access to both neighbors.\nStep 4: Define the atomic list operations first Once the structure split is clear, the whole design reduces to four atomic operations:\n_remove(node) removes a node from its current position _add_front(node) inserts a node right after the head sentinel _move_front(node) refreshes recency after access _pop_lru() removes the node right before the tail sentinel Everything else is just orchestration around these operations.\nStep 5: Ask what get really does get(key) is not just lookup. If the key exists, the key has now been used again.\nSo a successful get must do three things:\nlook up the node from the hash table move that node to the front of the linked list return the stored value If the key does not exist, return -1.\nStep 6: Ask what put really does put(key, value) has two branches:\nkey already exists: update the value move the node to the front key does not exist: if cache is full, evict the current LRU node create the new node insert it at the front add it to the hash table This is why the list and the map must stay synchronized at all times.\nStep 7: Walk one short trace slowly Capacity = 2:\nput(1,1) -\u0026gt; [1] put(2,2) -\u0026gt; [2,1] front is newest get(1) -\u0026gt; [1,2] put(3,3) -\u0026gt; evict 2, get [3,1] Notice the split:\nhash table finds 1 in O(1) doubly linked list moves 1 to the front in O(1) tail-side eviction removes 2 in O(1) Step 8: Reduce the method to one sentence LeetCode 146 is \u0026ldquo;use a hash table to find nodes fast and a doubly linked list to reorder and evict nodes fast.\u0026rdquo;\nAssemble the Full Code class Node: def __init__(self, key=0, val=0): self.key = key self.val = val self.prev = None self.next = None class LRUCache: def __init__(self, capacity: int): self.cap = capacity self.map = {} self.head = Node() # MRU-side sentinel self.tail = Node() # LRU-side sentinel self.head.next = self.tail self.tail.prev = self.head def _remove(self, node: Node) -\u0026gt; None: p, n = node.prev, node.next p.next = n n.prev = p def _add_front(self, node: Node) -\u0026gt; None: node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _move_front(self, node: Node) -\u0026gt; None: self._remove(node) self._add_front(node) def _pop_lru(self) -\u0026gt; Node: node = self.tail.prev self._remove(node) return node def get(self, key: int) -\u0026gt; int: node = self.map.get(key) if node is None: return -1 self._move_front(node) return node.val def put(self, key: int, value: int) -\u0026gt; None: if self.cap == 0: return node = self.map.get(key) if node is not None: node.val = value self._move_front(node) return if len(self.map) == self.cap: old = self._pop_lru() del self.map[old.key] node = Node(key, value) self.map[key] = node self._add_front(node) if __name__ == \u0026#34;__main__\u0026#34;: cache = LRUCache(2) cache.put(1, 1) cache.put(2, 2) print(cache.get(1)) # 1 cache.put(3, 3) print(cache.get(2)) # -1 Reference Answer class Node: def __init__(self, key=0, val=0): self.key = key self.val = val self.prev = None self.next = None class LRUCache: def __init__(self, capacity: int): self.cap = capacity self.map = {} self.head = Node() self.tail = Node() self.head.next = self.tail self.tail.prev = self.head def _remove(self, node: Node) -\u0026gt; None: p, n = node.prev, node.next p.next = n n.prev = p def _add_front(self, node: Node) -\u0026gt; None: node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _move_front(self, node: Node) -\u0026gt; None: self._remove(node) self._add_front(node) def _pop_lru(self) -\u0026gt; Node: node = self.tail.prev self._remove(node) return node def get(self, key: int) -\u0026gt; int: node = self.map.get(key) if node is None: return -1 self._move_front(node) return node.val def put(self, key: int, value: int) -\u0026gt; None: if self.cap == 0: return node = self.map.get(key) if node is not None: node.val = value self._move_front(node) return if len(self.map) == self.cap: old = self._pop_lru() del self.map[old.key] node = Node(key, value) self.map[key] = node self._add_front(node) Data Structure Model map[key] = node lets us locate an entry in average O(1) the doubly linked list stores entries in recency order head and tail sentinels remove boundary branches for insertions and removals Loop / State Invariant At all times:\nevery key in the map points to exactly one real list node every real list node belongs to exactly one key in the map nodes are ordered from MRU near head to LRU near tail Correctness is mostly about preserving these three facts after each operation.\nWhy atomic list operations matter If _remove, _add_front, _move_front, and _pop_lru are correct, then:\nget becomes \u0026ldquo;lookup + move\u0026rdquo; put becomes \u0026ldquo;update or evict + insert\u0026rdquo; That decomposition keeps the implementation defendable and debuggable.\nPractice Guide / Steps Build a doubly linked list with head and tail sentinels. Store key -\u0026gt; node in a hash table. Implement _remove(node) and _add_front(node) first. Implement _move_front(node) and _pop_lru() from those primitives. In get, return -1 on miss; otherwise move the node to the front. In put, update if the key exists; otherwise evict from the tail when full, then insert the new node at the front. Runnable Python example (lru_cache.py):\nclass Node: def __init__(self, key=0, val=0): self.key = key self.val = val self.prev = None self.next = None class LRUCache: def __init__(self, capacity: int): self.cap = capacity self.map = {} self.head = Node() self.tail = Node() self.head.next = self.tail self.tail.prev = self.head def _remove(self, node): node.prev.next = node.next node.next.prev = node.prev def _add_front(self, node): node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _move_front(self, node): self._remove(node) self._add_front(node) def _pop_lru(self): node = self.tail.prev self._remove(node) return node def get(self, key: int) -\u0026gt; int: node = self.map.get(key) if node is None: return -1 self._move_front(node) return node.val def put(self, key: int, value: int) -\u0026gt; None: if self.cap == 0: return node = self.map.get(key) if node is not None: node.val = value self._move_front(node) return if len(self.map) == self.cap: old = self._pop_lru() del self.map[old.key] node = Node(key, value) self.map[key] = node self._add_front(node) if __name__ == \u0026#34;__main__\u0026#34;: c = LRUCache(2) c.put(1, 1) c.put(2, 2) print(c.get(1)) c.put(3, 3) print(c.get(2)) Explanation / Why This Works The whole design comes from splitting two responsibilities that no single simple structure can satisfy alone:\nthe hash table answers \u0026ldquo;where is this key?\u0026rdquo; the doubly linked list answers \u0026ldquo;which key is newest or oldest?\u0026rdquo; Because nodes can be removed and inserted in O(1), and keys can be located in O(1) on average, the combined operations also stay O(1) on average.\nE - Engineering (Real-world Scenarios) Scenario 1: short-lived API response cache (Python) Background: a backend service caches recent endpoint results.\nWhy it fits: lookups and recency refreshes must stay cheap under load.\nfrom collections import OrderedDict class TinyLRU: def __init__(self, cap): self.cap = cap self.data = OrderedDict() def get(self, key): if key not in self.data: return None self.data.move_to_end(key, last=False) return self.data[key] def put(self, key, value): if key in self.data: self.data[key] = value self.data.move_to_end(key, last=False) return if len(self.data) == self.cap: self.data.popitem(last=True) self.data[key] = value self.data.move_to_end(key, last=False) Scenario 2: service configuration hot cache (Go) Background: a service keeps the most recently requested config entries in memory.\nWhy it fits: fast hits and cheap eviction are more important than perfect historical statistics.\npackage main import ( \u0026#34;container/list\u0026#34; \u0026#34;fmt\u0026#34; ) type entry struct { key string val string } func main() { ll := list.New() pos := map[string]*list.Element{} put := func(key, val string, cap int) { if e, ok := pos[key]; ok { e.Value = entry{key: key, val: val} ll.MoveToFront(e) return } if ll.Len() == cap { back := ll.Back() delete(pos, back.Value.(entry).key) ll.Remove(back) } pos[key] = ll.PushFront(entry{key: key, val: val}) } put(\u0026#34;a\u0026#34;, \u0026#34;1\u0026#34;, 2) put(\u0026#34;b\u0026#34;, \u0026#34;2\u0026#34;, 2) put(\u0026#34;c\u0026#34;, \u0026#34;3\u0026#34;, 2) fmt.Println(ll.Front().Value) } Scenario 3: frontend page-data cache (JavaScript) Background: a browser view keeps recently opened page data to avoid refetching.\nWhy it fits: the UI needs cheap updates when users revisit one of the recent pages.\nclass TinyLRU { constructor(capacity) { this.cap = capacity; this.map = new Map(); } get(key) { if (!this.map.has(key)) return undefined; const value = this.map.get(key); this.map.delete(key); this.map.set(key, value); return value; } put(key, value) { if (this.map.has(key)) this.map.delete(key); else if (this.map.size === this.cap) { const lruKey = this.map.keys().next().value; this.map.delete(lruKey); } this.map.set(key, value); } } R - Reflection (Complexity, Alternatives, Tradeoffs) Complexity get: average O(1) put: average O(1) Space: O(capacity) Alternatives Method Lookup Recency update Eviction Notes Array only O(n) O(n) O(1) or O(n) poor for updates Linked list only O(n) O(1) O(1) poor for lookup Hash table only O(1) hard hard no order information Hash table + doubly linked list O(1) avg O(1) O(1) intended solution Common mistakes Forgetting to move a key to the front after get. Updating the list but not the hash table, or vice versa. Using a singly linked list and then discovering arbitrary removal is no longer O(1). Forgetting to store key inside the node, making eviction cleanup harder. Why this is the best practical method It directly matches the two required capabilities:\nkey lookup ordered eviction No simpler structure covers both without sacrificing one of the O(1) requirements.\nFAQ and Notes Why do we need a doubly linked list instead of a singly linked list?\nBecause removing an arbitrary node in O(1) requires access to its predecessor and successor.\nWhy must the node store the key as well as the value?\nWhen evicting from the tail, we must also delete the key from the hash table.\nHow is this different from LFU?\nLRU tracks recency only; LFU tracks usage frequency and usually needs a more complex structure.\nBest Practices Implement and test the four atomic list operations before get and put. Use sentinels to remove edge-case branches at the list ends. Keep the map and list updates in the same logical block. Test edge cases: repeated put on same key, capacity 1, and capacity 0. S - Summary LRU cache design is fundamentally a two-structure coordination problem. The hash table gives O(1) average lookup. The doubly linked list gives O(1) recency updates and O(1) tail eviction. Once the atomic operations are correct, the whole design becomes straightforward. Further Reading LeetCode 146. LRU Cache LeetCode 460. LFU Cache Python OrderedDict Go container/list Conclusion If you can explain why a hash table alone fails, why a linked list alone fails, and why the combined structure succeeds, then you understand the cache design, not just the answer.\nReferences https://leetcode.com/problems/lru-cache/ https://docs.python.org/3/library/collections.html#collections.OrderedDict https://pkg.go.dev/container/list https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map Meta Info Reading time: 14-18 min Tags: Hot100, LRU, hash table, doubly linked list, LeetCode 146 SEO keywords: LRU Cache, hash table + doubly linked list, O(1) get put, LeetCode 146, Hot100 Meta description: A derivation-first ACERS guide to LRU Cache using hash-table lookup and doubly linked list recency tracking, with runnable multi-language implementations. CTA After this article, do three drills:\nRe-implement _remove, _add_front, and _pop_lru without looking. Test the cache with repeated put on the same key. Compare this design with LFU to understand why LFU needs more bookkeeping. Multi-language Implementations (Python / C / C++ / Go / Rust / JS) Python class Node: def __init__(self, key=0, val=0): self.key = key self.val = val self.prev = None self.next = None class LRUCache: def __init__(self, capacity: int): self.cap = capacity self.map = {} self.head = Node() self.tail = Node() self.head.next = self.tail self.tail.prev = self.head def _remove(self, node: Node) -\u0026gt; None: p, n = node.prev, node.next p.next = n n.prev = p def _add_front(self, node: Node) -\u0026gt; None: node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _move_front(self, node: Node) -\u0026gt; None: self._remove(node) self._add_front(node) def _pop_lru(self) -\u0026gt; Node: node = self.tail.prev self._remove(node) return node def get(self, key: int) -\u0026gt; int: node = self.map.get(key) if node is None: return -1 self._move_front(node) return node.val def put(self, key: int, value: int) -\u0026gt; None: if self.cap == 0: return node = self.map.get(key) if node is not None: node.val = value self._move_front(node) return if len(self.map) == self.cap: old = self._pop_lru() del self.map[old.key] node = Node(key, value) self.map[key] = node self._add_front(node) if __name__ == \u0026#34;__main__\u0026#34;: c = LRUCache(2) c.put(1, 1) c.put(2, 2) print(c.get(1)) # 1 c.put(3, 3) print(c.get(2)) # -1 c.put(4, 4) print(c.get(1), c.get(3), c.get(4)) # -1 3 4 C #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define HASH_SIZE 4093 typedef struct Node { int key; int val; struct Node* prev; struct Node* next; } Node; typedef struct Entry { int key; Node* node; struct Entry* next; } Entry; typedef struct { Entry* buckets[HASH_SIZE]; } HashMap; typedef struct { int cap; int size; HashMap map; Node head; Node tail; } LRUCache; unsigned int h(int key) { unsigned int x = (unsigned int)key; return (x * 2654435761u) % HASH_SIZE; } Node* map_get(HashMap* m, int key) { unsigned int idx = h(key); Entry* e = m-\u0026gt;buckets[idx]; while (e) { if (e-\u0026gt;key == key) return e-\u0026gt;node; e = e-\u0026gt;next; } return NULL; } void map_put(HashMap* m, int key, Node* node) { unsigned int idx = h(key); Entry* e = m-\u0026gt;buckets[idx]; while (e) { if (e-\u0026gt;key == key) { e-\u0026gt;node = node; return; } e = e-\u0026gt;next; } Entry* ne = (Entry*)malloc(sizeof(Entry)); ne-\u0026gt;key = key; ne-\u0026gt;node = node; ne-\u0026gt;next = m-\u0026gt;buckets[idx]; m-\u0026gt;buckets[idx] = ne; } void map_remove(HashMap* m, int key) { unsigned int idx = h(key); Entry* cur = m-\u0026gt;buckets[idx]; Entry* pre = NULL; while (cur) { if (cur-\u0026gt;key == key) { if (pre) pre-\u0026gt;next = cur-\u0026gt;next; else m-\u0026gt;buckets[idx] = cur-\u0026gt;next; free(cur); return; } pre = cur; cur = cur-\u0026gt;next; } } void list_remove(Node* n) { n-\u0026gt;prev-\u0026gt;next = n-\u0026gt;next; n-\u0026gt;next-\u0026gt;prev = n-\u0026gt;prev; } void list_add_front(LRUCache* c, Node* n) { n-\u0026gt;prev = \u0026amp;c-\u0026gt;head; n-\u0026gt;next = c-\u0026gt;head.next; c-\u0026gt;head.next-\u0026gt;prev = n; c-\u0026gt;head.next = n; } void move_front(LRUCache* c, Node* n) { list_remove(n); list_add_front(c, n); } Node* pop_lru(LRUCache* c) { Node* n = c-\u0026gt;tail.prev; list_remove(n); return n; } LRUCache* lruCreate(int capacity) { LRUCache* c = (LRUCache*)calloc(1, sizeof(LRUCache)); c-\u0026gt;cap = capacity; c-\u0026gt;size = 0; c-\u0026gt;head.next = \u0026amp;c-\u0026gt;tail; c-\u0026gt;tail.prev = \u0026amp;c-\u0026gt;head; return c; } int lruGet(LRUCache* c, int key) { Node* n = map_get(\u0026amp;c-\u0026gt;map, key); if (!n) return -1; move_front(c, n); return n-\u0026gt;val; } void lruPut(LRUCache* c, int key, int value) { if (c-\u0026gt;cap == 0) return; Node* n = map_get(\u0026amp;c-\u0026gt;map, key); if (n) { n-\u0026gt;val = value; move_front(c, n); return; } if (c-\u0026gt;size == c-\u0026gt;cap) { Node* old = pop_lru(c); map_remove(\u0026amp;c-\u0026gt;map, old-\u0026gt;key); free(old); c-\u0026gt;size--; } Node* nn = (Node*)malloc(sizeof(Node)); nn-\u0026gt;key = key; nn-\u0026gt;val = value; list_add_front(c, nn); map_put(\u0026amp;c-\u0026gt;map, key, nn); c-\u0026gt;size++; } void lruFree(LRUCache* c) { Node* cur = c-\u0026gt;head.next; while (cur != \u0026amp;c-\u0026gt;tail) { Node* nxt = cur-\u0026gt;next; free(cur); cur = nxt; } for (int i = 0; i \u0026lt; HASH_SIZE; i++) { Entry* e = c-\u0026gt;map.buckets[i]; while (e) { Entry* ne = e-\u0026gt;next; free(e); e = ne; } } free(c); } int main(void) { LRUCache* c = lruCreate(2); lruPut(c, 1, 1); lruPut(c, 2, 2); printf(\u0026#34;%d\\n\u0026#34;, lruGet(c, 1)); // 1 lruPut(c, 3, 3); printf(\u0026#34;%d\\n\u0026#34;, lruGet(c, 2)); // -1 lruPut(c, 4, 4); printf(\u0026#34;%d %d %d\\n\u0026#34;, lruGet(c, 1), lruGet(c, 3), lruGet(c, 4)); // -1 3 4 lruFree(c); return 0; } C++ #include \u0026lt;iostream\u0026gt; #include \u0026lt;list\u0026gt; #include \u0026lt;unordered_map\u0026gt; using namespace std; class LRUCache { private: int cap; list\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; dq; // front = MRU, back = LRU unordered_map\u0026lt;int, list\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt;::iterator\u0026gt; pos; public: explicit LRUCache(int capacity) : cap(capacity) {} int get(int key) { auto it = pos.find(key); if (it == pos.end()) return -1; dq.splice(dq.begin(), dq, it-\u0026gt;second); return it-\u0026gt;second-\u0026gt;second; } void put(int key, int value) { if (cap == 0) return; auto it = pos.find(key); if (it != pos.end()) { it-\u0026gt;second-\u0026gt;second = value; dq.splice(dq.begin(), dq, it-\u0026gt;second); return; } if ((int)dq.size() == cap) { int oldKey = dq.back().first; pos.erase(oldKey); dq.pop_back(); } dq.push_front({key, value}); pos[key] = dq.begin(); } }; int main() { LRUCache c(2); c.put(1, 1); c.put(2, 2); cout \u0026lt;\u0026lt; c.get(1) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 1 c.put(3, 3); cout \u0026lt;\u0026lt; c.get(2) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // -1 c.put(4, 4); cout \u0026lt;\u0026lt; c.get(1) \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; c.get(3) \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; c.get(4) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // -1 3 4 return 0; } Go package main import ( \u0026#34;container/list\u0026#34; \u0026#34;fmt\u0026#34; ) type entry struct { key int value int } type LRUCache struct { cap int ll *list.List pos map[int]*list.Element } func Constructor(capacity int) LRUCache { return LRUCache{ cap: capacity, ll: list.New(), pos: make(map[int]*list.Element), } } func (c *LRUCache) Get(key int) int { e, ok := c.pos[key] if !ok { return -1 } c.ll.MoveToFront(e) return e.Value.(entry).value } func (c *LRUCache) Put(key int, value int) { if c.cap == 0 { return } if e, ok := c.pos[key]; ok { e.Value = entry{key: key, value: value} c.ll.MoveToFront(e) return } if c.ll.Len() == c.cap { back := c.ll.Back() old := back.Value.(entry) delete(c.pos, old.key) c.ll.Remove(back) } e := c.ll.PushFront(entry{key: key, value: value}) c.pos[key] = e } func main() { c := Constructor(2) c.Put(1, 1) c.Put(2, 2) fmt.Println(c.Get(1)) // 1 c.Put(3, 3) fmt.Println(c.Get(2)) // -1 c.Put(4, 4) fmt.Println(c.Get(1), c.Get(3), c.Get(4)) // -1 3 4 } Rust use std::collections::HashMap; #[derive(Clone, Debug)] struct Node { key: i32, val: i32, prev: usize, next: usize, } struct LRUCache { cap: usize, len: usize, map: HashMap\u0026lt;i32, usize\u0026gt;, nodes: Vec\u0026lt;Node\u0026gt;, free: Vec\u0026lt;usize\u0026gt;, head: usize, tail: usize, } impl LRUCache { fn new(capacity: i32) -\u0026gt; Self { let head = 0usize; let tail = 1usize; let nodes = vec![ Node { key: 0, val: 0, prev: head, next: tail, }, Node { key: 0, val: 0, prev: head, next: tail, }, ]; let mut c = Self { cap: capacity.max(0) as usize, len: 0, map: HashMap::new(), nodes, free: Vec::new(), head, tail, }; c.nodes[c.head].next = c.tail; c.nodes[c.tail].prev = c.head; c } fn detach(\u0026amp;mut self, idx: usize) { let p = self.nodes[idx].prev; let n = self.nodes[idx].next; self.nodes[p].next = n; self.nodes[n].prev = p; } fn insert_front(\u0026amp;mut self, idx: usize) { let first = self.nodes[self.head].next; self.nodes[idx].prev = self.head; self.nodes[idx].next = first; self.nodes[self.head].next = idx; self.nodes[first].prev = idx; } fn move_front(\u0026amp;mut self, idx: usize) { self.detach(idx); self.insert_front(idx); } fn pop_lru(\u0026amp;mut self) -\u0026gt; Option\u0026lt;usize\u0026gt; { let idx = self.nodes[self.tail].prev; if idx == self.head { return None; } self.detach(idx); Some(idx) } fn alloc_node(\u0026amp;mut self, key: i32, val: i32) -\u0026gt; usize { if let Some(idx) = self.free.pop() { self.nodes[idx] = Node { key, val, prev: self.head, next: self.tail, }; idx } else { self.nodes.push(Node { key, val, prev: self.head, next: self.tail, }); self.nodes.len() - 1 } } fn get(\u0026amp;mut self, key: i32) -\u0026gt; i32 { let idx = match self.map.get(\u0026amp;key) { Some(\u0026amp;i) =\u0026gt; i, None =\u0026gt; return -1, }; self.move_front(idx); self.nodes[idx].val } fn put(\u0026amp;mut self, key: i32, value: i32) { if self.cap == 0 { return; } if let Some(\u0026amp;idx) = self.map.get(\u0026amp;key) { self.nodes[idx].val = value; self.move_front(idx); return; } if self.len == self.cap { if let Some(old_idx) = self.pop_lru() { let old_key = self.nodes[old_idx].key; self.map.remove(\u0026amp;old_key); self.free.push(old_idx); self.len -= 1; } } let idx = self.alloc_node(key, value); self.insert_front(idx); self.map.insert(key, idx); self.len += 1; } } fn main() { let mut c = LRUCache::new(2); c.put(1, 1); c.put(2, 2); println!(\u0026#34;{}\u0026#34;, c.get(1)); // 1 c.put(3, 3); println!(\u0026#34;{}\u0026#34;, c.get(2)); // -1 c.put(4, 4); println!(\u0026#34;{} {} {}\u0026#34;, c.get(1), c.get(3), c.get(4)); // -1 3 4 } JavaScript class Node { constructor(key = 0, value = 0) { this.key = key; this.value = value; this.prev = null; this.next = null; } } class LRUCache { constructor(capacity) { this.cap = capacity; this.map = new Map(); this.head = new Node(); this.tail = new Node(); this.head.next = this.tail; this.tail.prev = this.head; } _remove(node) { node.prev.next = node.next; node.next.prev = node.prev; } _addFront(node) { node.prev = this.head; node.next = this.head.next; this.head.next.prev = node; this.head.next = node; } _moveFront(node) { this._remove(node); this._addFront(node); } _popLRU() { const node = this.tail.prev; this._remove(node); return node; } get(key) { if (!this.map.has(key)) return -1; const node = this.map.get(key); this._moveFront(node); return node.value; } put(key, value) { if (this.cap === 0) return; if (this.map.has(key)) { const node = this.map.get(key); node.value = value; this._moveFront(node); return; } if (this.map.size === this.cap) { const old = this._popLRU(); this.map.delete(old.key); } const node = new Node(key, value); this.map.set(key, node); this._addFront(node); } } const c = new LRUCache(2); c.put(1, 1); c.put(2, 2); console.log(c.get(1)); // 1 c.put(3, 3); console.log(c.get(2)); // -1 c.put(4, 4); console.log(c.get(1), c.get(3), c.get(4)); // -1 3 4 ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/146-lru-cache/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nLeetCode 146 is not a memorize-the-template problem. It is the core cache-design exercise: how to support fast lookup and fast recency updates at the same time.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 14-18 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003eLRU\u003c/code\u003e, \u003ccode\u003ehash table\u003c/code\u003e, \u003ccode\u003edoubly linked list\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LRU Cache, hash table + doubly linked list, O(1) get put, LeetCode 146, Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A derivation-first ACERS guide to LRU Cache using a hash table plus doubly linked list, with engineering scenarios, pitfalls, and runnable multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eDesign a data structure \u003ccode\u003eLRUCache\u003c/code\u003e with:\u003c/p\u003e","title":"Hot100: LRU Cache Hash Table + Doubly Linked List O(1) ACERS Guide"},{"content":" Subtitle / Summary\nThe hard part of LeetCode 19 is not deleting a node. It is locating the predecessor of the n-th node from the end in a singly linked list without walking backward.\nReading time: 12-15 min Tags: Hot100, linked list, two pointers, dummy node SEO keywords: Remove Nth Node From End of List, one-pass two pointers, dummy node, LeetCode 19, Hot100 Meta description: A derivation-first ACERS explanation of LeetCode 19 with fixed-gap two pointers, dummy node boundary handling, engineering mapping, and runnable multi-language implementations. A - Algorithm (Problem and Algorithm) Problem Restatement Given the head of a linked list, remove the n-th node from the end of the list and return the head of the modified list.\nInput / Output Name Type Description head ListNode head of a singly linked list n int position counted from the end return ListNode head after deletion Example 1 input: head = [1,2,3,4,5], n = 2 output: [1,2,3,5] Example 2 input: head = [1], n = 1 output: [] Example 3 input: head = [1,2], n = 2 output: [2] Visual Intuition (Fixed Gap) dummy -\u0026gt; 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; null fast goes first by n = 2 nodes: dummy -\u0026gt; 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; null slow fast Then move both together until fast reaches the tail: dummy -\u0026gt; 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; null slow fast Now slow.next is exactly the node to delete. Target Readers Hot100 learners who want a reliable one-pass linked-list deletion template Developers who know two pointers but still get boundary cases wrong Engineers interested in turning \u0026ldquo;count from the end\u0026rdquo; into a forward-only traversal Background / Motivation In a singly linked list, deleting a node is easy if you already have its predecessor. The real problem is finding that predecessor when the target is described from the end:\nsingly linked lists cannot move backward deleting the head requires special handling careless pointer updates can break the chain This is why LeetCode 19 is a classic template problem. It teaches how to convert a backward-position description into a forward traversal rule.\nCore Concepts Singly linked list: only forward traversal through next Dummy node: creates a stable predecessor even when the head is deleted Fixed-gap two pointers: let fast stay n nodes ahead of slow Predecessor deletion rule: prev.next = prev.next.next C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Ask what node we really need to find The problem says \u0026ldquo;delete the n-th node from the end\u0026rdquo;. But the actual linked-list operation is never:\ndelete(target) It is:\nprev.next = prev.next.next So the useful target is not the node itself. It is the node before the one we want to delete.\nStep 2: Acknowledge the easy solutions first Two straightforward solutions exist:\ncopy nodes into an array, compute the position, and reconnect run two passes: first compute length, then walk to length - n Both can work. But neither captures the best one-pass linked-list idea.\nStep 3: Turn \u0026ldquo;from the end\u0026rdquo; into a fixed-gap rule Suppose fast first moves n steps ahead of slow. Then the distance between them is fixed:\nslow ... n nodes ... fast If both pointers move one step at a time afterward, that gap stays unchanged. So when fast reaches the last node, slow must be standing right before the target node.\nThat is the whole conversion:\n\u0026ldquo;count from the end\u0026rdquo; becomes \u0026ldquo;maintain a gap of n\u0026rdquo;.\nStep 4: Why do we still need a dummy node? If the node to delete is the original head, then its real predecessor does not exist. Without a dummy node, that case becomes a special branch.\nSo we normalize every case with:\ndummy = ListNode(0, head) fast = slow = dummy Now even the original head has a valid predecessor: dummy.\nStep 5: Define the movement phases precisely Phase 1:\nfor _ in range(n): fast = fast.next Phase 2:\nwhile fast.next is not None: fast = fast.next slow = slow.next When phase 2 ends, slow.next is exactly the node to remove.\nStep 6: Define the deletion step After localization, the deletion itself is small:\nslow.next = slow.next.next That single line works for:\ndeleting a middle node deleting the last node deleting the original head because dummy unified all three cases.\nStep 7: Walk one trace slowly For:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5, n = 2 After fast moves first by 2:\nslow = dummy fast = 2 Move both together:\nslow = 1, fast = 3 slow = 2, fast = 4 slow = 3, fast = 5 Now fast.next is null, so stop. slow.next is 4, which is exactly the node to delete.\nStep 8: Reduce the method to one sentence LeetCode 19 is \u0026ldquo;add a dummy node, let fast lead by n, then move both pointers together until slow reaches the predecessor of the target.\u0026rdquo;\nAssemble the Full Code from typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def remove_nth_from_end(head: Optional[ListNode], n: int) -\u0026gt; Optional[ListNode]: dummy = ListNode(0, head) fast = slow = dummy for _ in range(n): fast = fast.next while fast.next is not None: fast = fast.next slow = slow.next slow.next = slow.next.next return dummy.next def from_list(nums: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy for x in nums: tail.next = ListNode(x) tail = tail.next return dummy.next def to_list(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: print(to_list(remove_nth_from_end(from_list([1, 2, 3, 4, 5]), 2))) # [1, 2, 3, 5] print(to_list(remove_nth_from_end(from_list([1]), 1))) # [] print(to_list(remove_nth_from_end(from_list([1, 2]), 2))) # [2] Reference Answer from typing import Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def removeNthFromEnd(head: Optional[ListNode], n: int) -\u0026gt; Optional[ListNode]: dummy = ListNode(0, head) fast = slow = dummy for _ in range(n): fast = fast.next while fast.next is not None: fast = fast.next slow = slow.next slow.next = slow.next.next return dummy.next Method Category Two pointers Fixed-gap traversal Dummy-node boundary normalization Correctness Intuition The invariant after phase 1 is:\nfast is n nodes ahead of slow Phase 2 keeps that distance unchanged. So when fast reaches the final node, slow must be right before the node counted as the n-th from the end.\nWhy the dummy node matters Without a dummy node, deleting the original head would require a separate branch. With dummy, every deletion is expressed by the same predecessor-based line:\nslow.next = slow.next.next Practice Guide / Steps Create dummy = ListNode(0, head). Set fast = slow = dummy. Move fast forward by n nodes. Move fast and slow together until fast.next becomes None. Delete slow.next. Return dummy.next. Runnable Python example (remove_nth_from_end.py):\nfrom typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def remove_nth_from_end(head: Optional[ListNode], n: int) -\u0026gt; Optional[ListNode]: dummy = ListNode(0, head) fast = slow = dummy for _ in range(n): fast = fast.next while fast.next is not None: fast = fast.next slow = slow.next slow.next = slow.next.next return dummy.next def from_list(nums: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() cur = dummy for x in nums: cur.next = ListNode(x) cur = cur.next return dummy.next def to_list(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: print(to_list(remove_nth_from_end(from_list([1, 2, 3, 4, 5]), 2))) Explanation / Why This Works The algorithm never needs to know the full length of the list. Instead, it converts a backward distance into a forward invariant:\nkeep fast exactly n nodes ahead of slow That is why the method stays one-pass after the initial lead-in movement. The deletion itself is tiny; the real insight is how the gap encodes the target position.\nE - Engineering (Real-world Scenarios) Scenario 1: retry-chain trimming in backend jobs (Go) Background: a retry queue stores attempts as a linked chain.\nWhy it fits: operators often remove the k-th attempt counted backward from the newest record.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Val int Next *Node } func removeNthFromEnd(head *Node, n int) *Node { dummy := \u0026amp;Node{Next: head} fast, slow := dummy, dummy for i := 0; i \u0026lt; n; i++ { fast = fast.Next } for fast.Next != nil { fast = fast.Next slow = slow.Next } slow.Next = slow.Next.Next return dummy.Next } func main() { head := \u0026amp;Node{1, \u0026amp;Node{2, \u0026amp;Node{3, \u0026amp;Node{4, \u0026amp;Node{5, nil}}}}} head = removeNthFromEnd(head, 2) for p := head; p != nil; p = p.Next { fmt.Print(p.Val, \u0026#34; \u0026#34;) } fmt.Println() } Scenario 2: free-block chain maintenance (C) Background: a low-level allocator maintains a singly linked block chain.\nWhy it fits: removing a block relative to the chain tail is naturally a predecessor-localization problem.\n#include \u0026lt;stdio.h\u0026gt; struct Node { int id; struct Node* next; }; struct Node* removeNthFromEnd(struct Node* head, int n) { struct Node dummy = {0, head}; struct Node *fast = \u0026amp;dummy, *slow = \u0026amp;dummy; for (int i = 0; i \u0026lt; n; ++i) fast = fast-\u0026gt;next; while (fast-\u0026gt;next) { fast = fast-\u0026gt;next; slow = slow-\u0026gt;next; } slow-\u0026gt;next = slow-\u0026gt;next-\u0026gt;next; return dummy.next; } Scenario 3: frontend undo-chain compaction (JavaScript) Background: an editor stores undo snapshots as a singly linked chain.\nWhy it fits: removing a node relative to the newest snapshot maps directly to a tail-relative deletion rule.\nfunction removeNthFromEnd(arr, n) { const idx = arr.length - n; return arr.filter((_, i) =\u0026gt; i !== idx); } console.log(removeNthFromEnd([1, 2, 3, 4, 5], 2)); R - Reflection (Complexity, Alternatives, Tradeoffs) Complexity Time: O(L) Auxiliary space: O(1) Alternatives Method Time Extra Space Notes Array conversion O(L) O(L) simple but not list-native Two-pass length method O(L) O(1) correct, but less elegant One-pass fixed-gap method O(L) O(1) best reusable template Common mistakes Forgetting the dummy node and then mishandling head deletion. Using while fast is not None instead of while fast.next is not None. Deleting the target node directly without locating its predecessor. Advancing slow too early and breaking the gap invariant. Why this is the best practical method It gives you:\none-pass traversal no extra container one uniform deletion rule for all positions More importantly, it teaches a transferable pattern: convert tail-relative indexing into a forward-moving invariant.\nFAQ and Notes Why is the loop while fast.next is not None instead of while fast is not None?\nBecause we want slow to stop at the predecessor of the target, not on the target itself.\nWhat if n equals the list length?\nThen the original head is deleted, and dummy handles that safely.\nCan recursion solve this too?\nYes, but it is less practical and uses call-stack space.\nBest Practices Normalize deletion problems with a dummy node whenever the head might change. Decide first whether you need the target node or its predecessor. For two-pointer gaps, state the invariant in words before coding. Test at least these cases: delete head, delete tail, delete only node. S - Summary The useful node to locate is the predecessor of the target. A dummy node removes special-case logic for head deletion. The fast/slow gap converts \u0026ldquo;count from the end\u0026rdquo; into a forward-only traversal. This is one of the most reusable one-pass linked-list templates in Hot100. Further Reading LeetCode 19. Remove Nth Node From End of List LeetCode 21. Merge Two Sorted Lists LeetCode 92. Reverse Linked List II LeetCode 143. Reorder List Conclusion Once you can express this problem as a fixed-gap invariant instead of a backward count, many linked-list problems stop feeling ad hoc and start feeling structural.\nReferences https://leetcode.com/problems/remove-nth-node-from-end-of-list/ https://en.cppreference.com/w/cpp/container/forward_list https://docs.python.org/3/tutorial/datastructures.html https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections Meta Info Reading time: 12-15 min Tags: Hot100, linked list, two pointers, dummy node, LeetCode 19 SEO keywords: Remove Nth Node From End of List, fixed-gap two pointers, dummy node, LeetCode 19, Hot100 Meta description: A derivation-first ACERS guide to LeetCode 19 using fixed-gap two pointers and a dummy node, with engineering mapping and runnable multi-language code. CTA Do two drills right after this:\nRe-implement the gap method from memory without checking the article. Then solve a related list problem where the head may change again, such as LeetCode 92 or LeetCode 25. Multi-language Implementations (Python / C / C++ / Go / Rust / JS) Python from typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def removeNthFromEnd(head: Optional[ListNode], n: int) -\u0026gt; Optional[ListNode]: dummy = ListNode(0, head) fast = slow = dummy for _ in range(n): fast = fast.next while fast.next is not None: fast = fast.next slow = slow.next slow.next = slow.next.next return dummy.next def from_list(nums: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() cur = dummy for x in nums: cur.next = ListNode(x) cur = cur.next return dummy.next def to_list(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: h = from_list([1, 2, 3, 4, 5]) print(to_list(removeNthFromEnd(h, 2))) # [1, 2, 3, 5] C #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct ListNode { int val; struct ListNode* next; }; struct ListNode* new_node(int v) { struct ListNode* p = (struct ListNode*)malloc(sizeof(struct ListNode)); p-\u0026gt;val = v; p-\u0026gt;next = NULL; return p; } struct ListNode* removeNthFromEnd(struct ListNode* head, int n) { struct ListNode dummy = {0, head}; struct ListNode *fast = \u0026amp;dummy, *slow = \u0026amp;dummy; for (int i = 0; i \u0026lt; n; ++i) fast = fast-\u0026gt;next; while (fast-\u0026gt;next) { fast = fast-\u0026gt;next; slow = slow-\u0026gt;next; } struct ListNode* del = slow-\u0026gt;next; slow-\u0026gt;next = del-\u0026gt;next; free(del); return dummy.next; } void print_list(struct ListNode* head) { for (struct ListNode* p = head; p; p = p-\u0026gt;next) printf(\u0026#34;%d \u0026#34;, p-\u0026gt;val); printf(\u0026#34;\\n\u0026#34;); } void free_list(struct ListNode* head) { while (head) { struct ListNode* t = head; head = head-\u0026gt;next; free(t); } } int main(void) { struct ListNode* h1 = new_node(1); h1-\u0026gt;next = new_node(2); h1-\u0026gt;next-\u0026gt;next = new_node(3); h1-\u0026gt;next-\u0026gt;next-\u0026gt;next = new_node(4); h1-\u0026gt;next-\u0026gt;next-\u0026gt;next-\u0026gt;next = new_node(5); h1 = removeNthFromEnd(h1, 2); print_list(h1); // 1 2 3 5 free_list(h1); return 0; } C++ #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* removeNthFromEnd(ListNode* head, int n) { ListNode dummy(0); dummy.next = head; ListNode* fast = \u0026amp;dummy; ListNode* slow = \u0026amp;dummy; for (int i = 0; i \u0026lt; n; ++i) fast = fast-\u0026gt;next; while (fast-\u0026gt;next != nullptr) { fast = fast-\u0026gt;next; slow = slow-\u0026gt;next; } ListNode* del = slow-\u0026gt;next; slow-\u0026gt;next = del-\u0026gt;next; delete del; return dummy.next; } ListNode* build(const vector\u0026lt;int\u0026gt;\u0026amp; a) { ListNode dummy(0); ListNode* tail = \u0026amp;dummy; for (int x : a) { tail-\u0026gt;next = new ListNode(x); tail = tail-\u0026gt;next; } return dummy.next; } void print(ListNode* head) { for (ListNode* p = head; p; p = p-\u0026gt;next) cout \u0026lt;\u0026lt; p-\u0026gt;val \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } void destroy(ListNode* head) { while (head) { ListNode* t = head; head = head-\u0026gt;next; delete t; } } int main() { ListNode* h = build({1, 2, 3, 4, 5}); h = removeNthFromEnd(h, 2); print(h); // 1 2 3 5 destroy(h); return 0; } Go package main import \u0026#34;fmt\u0026#34; type ListNode struct { Val int Next *ListNode } func removeNthFromEnd(head *ListNode, n int) *ListNode { dummy := \u0026amp;ListNode{Next: head} fast, slow := dummy, dummy for i := 0; i \u0026lt; n; i++ { fast = fast.Next } for fast.Next != nil { fast = fast.Next slow = slow.Next } slow.Next = slow.Next.Next return dummy.Next } func build(nums []int) *ListNode { dummy := \u0026amp;ListNode{} tail := dummy for _, x := range nums { tail.Next = \u0026amp;ListNode{Val: x} tail = tail.Next } return dummy.Next } func printList(head *ListNode) { for p := head; p != nil; p = p.Next { fmt.Printf(\u0026#34;%d \u0026#34;, p.Val) } fmt.Println() } func main() { head := build([]int{1, 2, 3, 4, 5}) head = removeNthFromEnd(head, 2) printList(head) // 1 2 3 5 } Rust For a compact runnable version that stays ownership-safe, this Rust implementation uses a two-pass approach while preserving O(L) time and O(1) auxiliary space.\n#[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] fn new(val: i32) -\u0026gt; Self { ListNode { next: None, val } } } fn remove_nth_from_end(head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, n: i32) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut len = 0usize; let mut p = head.as_ref(); while let Some(node) = p { len += 1; p = node.next.as_ref(); } let idx = len - n as usize; let mut dummy = Box::new(ListNode { val: 0, next: head }); let mut cur = \u0026amp;mut dummy; for _ in 0..idx { cur = cur.next.as_mut().unwrap(); } let next = cur.next.as_mut().and_then(|node| node.next.take()); cur.next = next; dummy.next } fn from_vec(a: Vec\u0026lt;i32\u0026gt;) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut head = None; for \u0026amp;x in a.iter().rev() { let mut node = Box::new(ListNode::new(x)); node.next = head; head = Some(node); } head } fn to_vec(mut head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let mut out = Vec::new(); while let Some(mut node) = head { out.push(node.val); head = node.next.take(); } out } fn main() { let head = from_vec(vec![1, 2, 3, 4, 5]); let ans = remove_nth_from_end(head, 2); println!(\u0026#34;{:?}\u0026#34;, to_vec(ans)); // [1, 2, 3, 5] } JavaScript class ListNode { constructor(val = 0, next = null) { this.val = val; this.next = next; } } function removeNthFromEnd(head, n) { const dummy = new ListNode(0, head); let fast = dummy; let slow = dummy; for (let i = 0; i \u0026lt; n; i++) { fast = fast.next; } while (fast.next !== null) { fast = fast.next; slow = slow.next; } slow.next = slow.next.next; return dummy.next; } function fromArray(arr) { const dummy = new ListNode(); let tail = dummy; for (const x of arr) { tail.next = new ListNode(x); tail = tail.next; } return dummy.next; } function toArray(head) { const out = []; for (let p = head; p; p = p.next) out.push(p.val); return out; } const head = fromArray([1, 2, 3, 4, 5]); console.log(toArray(removeNthFromEnd(head, 2))); // [1, 2, 3, 5] ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/19-remove-nth-node-from-end-of-list/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThe hard part of LeetCode 19 is not deleting a node. It is locating the predecessor of the n-th node from the end in a singly linked list without walking backward.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003etwo pointers\u003c/code\u003e, \u003ccode\u003edummy node\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Remove Nth Node From End of List, one-pass two pointers, dummy node, LeetCode 19, Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A derivation-first ACERS explanation of LeetCode 19 with fixed-gap two pointers, dummy node boundary handling, engineering mapping, and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the head of a linked list, remove the n-th node from the end of the list and return the head of the modified list.\u003c/p\u003e","title":"Hot100: Remove Nth Node From End of List One-Pass Two-Pointer ACERS Guide"},{"content":" Subtitle / Summary The most common confusion in LeetCode 543 is deciding what the recursive function should return. It should return height, not diameter. The diameter is updated globally at each node using leftHeight + rightHeight. Once that separation is clear, the problem becomes a clean introduction to tree DP.\nReading time: 10-13 min Tags: Hot100, binary tree, tree DP, DFS, postorder SEO keywords: Diameter of Binary Tree, tree DP, height return, DFS, LeetCode 543 Meta description: Learn LeetCode 543 from the postorder height-return pattern, with step-by-step derivation, engineering analogies, and runnable multi-language solutions. A — Algorithm Problem Restatement Given the root root of a binary tree, return the diameter of the tree.\nThe diameter of a binary tree is:\nthe length of the longest path between any two nodes in the tree a path that may or may not pass through the root measured by the number of edges Input / Output Name Type Description root TreeNode root of the binary tree return int the diameter of the tree Example 1 input: root = [1,2,3,4,5] output: 3 explanation: one longest path is [4,2,1,3], whose length is 3 edges. Example 2 input: root = [1,2] output: 1 Constraints The number of nodes is in the range [1, 10^4] -100 \u0026lt;= Node.val \u0026lt;= 100 Target Readers Hot100 learners moving from plain tree recursion into tree DP thinking Developers who often mix up recursive return values with the final global answer Engineers dealing with longest chains in hierarchical structures Background / Motivation At first glance, this problem looks close to maximum depth. But it is not the same.\nMaximum depth asks:\nhow far can I go downward from one node? Diameter asks:\nwhat is the longest path between any two nodes in the entire tree? That difference matters because the longest path may:\npass through the root stay entirely inside one subtree This is exactly why LeetCode 543 is such a good first tree-DP problem. It forces you to separate:\nwhat a subtree returns upward how the global best answer is updated Core Concepts Height: the longest downward one-side path starting from the current node Diameter candidate through the current node: leftHeight + rightHeight Postorder traversal: the current node can only compute its candidate after it knows both subtree heights Global answer: the diameter is a whole-tree maximum, so it should not be confused with the returned height C — Concepts How To Build The Solution From Scratch Step 1: Start from the phrase \u0026ldquo;between any two nodes\u0026rdquo; The problem does not ask for a root-to-leaf path. It asks for the longest path between any pair of nodes.\nThat means the answer is not limited to one downward chain. A good candidate path often looks like:\ndeep leaf in left subtree -\u0026gt; ... -\u0026gt; current node -\u0026gt; ... -\u0026gt; deep leaf in right subtree So one node may connect two downward branches into a longer path.\nStep 2: Ask what the current node needs in order to build such a path If a path passes through the current node, the current node needs to know:\nhow deep the left subtree can extend downward how deep the right subtree can extend downward So the most useful recursive return value is:\nthe height of the current subtree\nStep 3: Define the recursive subproblem For each node:\nget the height of the left subtree get the height of the right subtree use them to update the best diameter candidate through this node return the current height upward That gives the postorder structure:\nleft = dfs(node.left) right = dfs(node.right) Step 4: Define the base case The cleanest definition is:\nif node is None: return 0 So an empty subtree has height 0.\nThat makes leaf nodes convenient:\nleft height = 0 right height = 0 leaf height = 1 Step 5: Why is the diameter candidate left + right? In this setup:\nleft is the longest downward path length contributed by the left side right is the longest downward path length contributed by the right side If the longest path goes through the current node, those two sides connect here. So the number of edges in that candidate path is:\nleft + right This matches the problem statement, which measures path length by edges.\nStep 6: Why should the diameter be updated in a separate variable? Because the recursive return value and the final answer are not the same thing.\nThe parent node only needs to know:\nhow far this subtree can extend upward as one branch\nThat is height. The diameter, however, is the maximum over the whole tree.\nSo the clean split is:\ndfs(node) returns height ans stores the best diameter seen so far ans = max(ans, left + right) Step 7: What should the current node return upward? The parent can only continue one side upward, not both. So the height returned by the current node is:\nreturn 1 + max(left, right) That is the key separation:\nglobal answer uses both sides recursive return value uses only the longer side Step 8: Walk the main example slowly Use the official example:\nroot = [1,2,3,4,5] Bottom-up:\nnode 4 has height 1 node 5 has height 1 at node 2, left = 1, right = 1 so the candidate diameter through 2 is 2 node 2 returns height 2 At root 1:\nleft height = 2 right height = 1 So the candidate diameter through the root is 3, which is the final answer.\nStep 9: Reduce the whole method to one reusable sentence The whole idea can be compressed to:\nDuring postorder traversal, return height upward, and at every node update the global diameter with leftHeight + rightHeight.\nOnce that sentence is stable, the code is stable.\nAssemble the Full Code Now combine the rules above into the first complete working solution.\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def diameter_of_binary_tree(root): ans = 0 def dfs(node): nonlocal ans if node is None: return 0 left = dfs(node.left) right = dfs(node.right) ans = max(ans, left + right) return 1 + max(left, right) dfs(root) return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3)) print(diameter_of_binary_tree(root)) Reference Answer For LeetCode submission style, the same logic becomes:\nfrom typing import Optional class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def diameterOfBinaryTree(self, root: Optional[TreeNode]) -\u0026gt; int: ans = 0 def dfs(node: Optional[TreeNode]) -\u0026gt; int: nonlocal ans if node is None: return 0 left = dfs(node.left) right = dfs(node.right) ans = max(ans, left + right) return 1 + max(left, right) dfs(root) return ans What mental model should stick? Do not remember this as \u0026ldquo;the diameter trick\u0026rdquo;. Remember the separation of responsibilities:\nthe recursive return value is height the global variable is the best diameter seen so far That design is what makes the solution clean.\nE — Engineering Scenario 1: Measure the longest communication chain in an org tree (Python) Background: in a hierarchy, you may want the longest possible chain between two members.\nWhy it fits: that is the tree-diameter problem in an org-chart form.\nclass Node: def __init__(self, name, left=None, right=None): self.name = name self.left = left self.right = right def longest_chain(root): ans = 0 def dfs(node): nonlocal ans if node is None: return 0 left = dfs(node.left) right = dfs(node.right) ans = max(ans, left + right) return 1 + max(left, right) dfs(root) return ans root = Node(\u0026#34;CEO\u0026#34;, Node(\u0026#34;VP1\u0026#34;, Node(\u0026#34;M1\u0026#34;), Node(\u0026#34;M2\u0026#34;)), Node(\u0026#34;VP2\u0026#34;)) print(longest_chain(root)) Scenario 2: Measure the farthest propagation span in a service call tree (Go) Background: a request trace often forms a tree of service calls.\nWhy it fits: the farthest pair of nodes in that call tree is its diameter.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Name string Left *Node Right *Node } func diameter(root *Node) int { ans := 0 var dfs func(*Node) int dfs = func(node *Node) int { if node == nil { return 0 } left := dfs(node.Left) right := dfs(node.Right) if left+right \u0026gt; ans { ans = left + right } if left \u0026gt; right { return 1 + left } return 1 + right } dfs(root) return ans } func main() { root := \u0026amp;Node{Name: \u0026#34;A\u0026#34;, Left: \u0026amp;Node{Name: \u0026#34;B\u0026#34;}, Right: \u0026amp;Node{Name: \u0026#34;C\u0026#34;}} fmt.Println(diameter(root)) } Scenario 3: Estimate the longest event propagation chain in a component tree (JavaScript) Background: deeply nested frontend component trees can create long state or event propagation paths.\nWhy it fits: the maximum span across the tree is structurally the diameter.\nfunction Node(name, left = null, right = null) { this.name = name; this.left = left; this.right = right; } function diameter(root) { let ans = 0; function dfs(node) { if (!node) return 0; const left = dfs(node.left); const right = dfs(node.right); ans = Math.max(ans, left + right); return 1 + Math.max(left, right); } dfs(root); return ans; } const root = new Node(\u0026#34;App\u0026#34;, new Node(\u0026#34;Sidebar\u0026#34;), new Node(\u0026#34;Content\u0026#34;)); console.log(diameter(root)); R — Reflection Complexity Analysis Time complexity: O(n), because every node is visited once Space complexity: O(h), where h is the tree height, due to recursion stack depth Alternative Approaches Method Time Extra space Notes Postorder height return O(n) O(h) cleanest and recommended Recompute subtree heights at every node worst-case O(n^2) O(h) too much repeated work Convert to an undirected graph and run two BFS/DFS passes O(n) O(n) valid, but heavier than needed here Common Mistakes Trying to return diameter from the recursive function: the parent really needs height, not subtree diameter. Forgetting that the problem counts edges: this creates off-by-one errors. Only checking paths through the root: the true longest path may lie entirely inside one subtree. Updating ans at the wrong time: the update must happen after both subtree heights are known. FAQ 1. Why is left + right the right candidate? Because the current node can connect the deepest downward branch from the left and the deepest downward branch from the right into one full path.\n2. Why not return max(leftDiameter, rightDiameter, left + right) directly? You could return a larger structure with multiple fields, but that is unnecessary here. Returning height and keeping a separate global best is simpler and clearer.\n3. How is this related to Maximum Depth of Binary Tree? Maximum depth only cares about one downward chain. Diameter uses both sides together at each node. So LeetCode 104 is a strong prerequisite for 543, but 543 adds a global-best layer on top.\nBest Practices Decide before coding that dfs returns height, not the final answer Keep the roles of dfs and ans strictly separate Hand-simulate one small tree and check edges versus nodes carefully Practice this together with LeetCode 104 and 110 to solidify postorder tree recurrence S — Summary Diameter is not the same as maximum depth The clean solution returns height upward and updates diameter globally Every node contributes one candidate path left + right The longest path may pass through the root or stay entirely inside a subtree LeetCode 543 is one of the best entry points into tree DP and postorder aggregation Further Reading LeetCode 543: Diameter of Binary Tree LeetCode 104: Maximum Depth of Binary Tree LeetCode 110: Balanced Binary Tree LeetCode 236: Lowest Common Ancestor of a Binary Tree CTA A strong practice group is 104 + 543 + 110. Those three problems together train the core tree skill of \u0026ldquo;what does the subtree return, and what does the current node aggregate?\u0026rdquo;\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def diameter_of_binary_tree(root): ans = 0 def dfs(node): nonlocal ans if node is None: return 0 left = dfs(node.left) right = dfs(node.right) ans = max(ans, left + right) return 1 + max(left, right) dfs(root) return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3)) print(diameter_of_binary_tree(root)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } int max_int(int a, int b) { return a \u0026gt; b ? a : b; } int dfs(struct TreeNode* node, int* ans) { if (node == NULL) return 0; int left = dfs(node-\u0026gt;left, ans); int right = dfs(node-\u0026gt;right, ans); if (left + right \u0026gt; *ans) *ans = left + right; return 1 + max_int(left, right); } int diameterOfBinaryTree(struct TreeNode* root) { int ans = 0; dfs(root, \u0026amp;ans); return ans; } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(1); root-\u0026gt;left = new_node(2); root-\u0026gt;right = new_node(3); root-\u0026gt;left-\u0026gt;left = new_node(4); root-\u0026gt;left-\u0026gt;right = new_node(5); printf(\u0026#34;%d\\n\u0026#34;, diameterOfBinaryTree(root)); free_tree(root); return 0; } #include \u0026lt;algorithm\u0026gt; #include \u0026lt;iostream\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; int dfs(TreeNode* node, int\u0026amp; ans) { if (!node) return 0; int left = dfs(node-\u0026gt;left, ans); int right = dfs(node-\u0026gt;right, ans); ans = std::max(ans, left + right); return 1 + std::max(left, right); } int diameterOfBinaryTree(TreeNode* root) { int ans = 0; dfs(root, ans); return ans; } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(1); root-\u0026gt;left = new TreeNode(2); root-\u0026gt;right = new TreeNode(3); root-\u0026gt;left-\u0026gt;left = new TreeNode(4); root-\u0026gt;left-\u0026gt;right = new TreeNode(5); std::cout \u0026lt;\u0026lt; diameterOfBinaryTree(root) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func diameterOfBinaryTree(root *TreeNode) int { ans := 0 var dfs func(*TreeNode) int dfs = func(node *TreeNode) int { if node == nil { return 0 } left := dfs(node.Left) right := dfs(node.Right) if left+right \u0026gt; ans { ans = left + right } if left \u0026gt; right { return 1 + left } return 1 + right } dfs(root) return ans } func main() { root := \u0026amp;TreeNode{ Val: 1, Left: \u0026amp;TreeNode{ Val: 2, Left: \u0026amp;TreeNode{Val: 4}, Right: \u0026amp;TreeNode{Val: 5}, }, Right: \u0026amp;TreeNode{Val: 3}, } fmt.Println(diameterOfBinaryTree(root)) } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn dfs(node: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, ans: \u0026amp;mut i32) -\u0026gt; i32 { match node { None =\u0026gt; 0, Some(n) =\u0026gt; { let left = dfs(\u0026amp;n.left, ans); let right = dfs(\u0026amp;n.right, ans); *ans = (*ans).max(left + right); 1 + left.max(right) } } } fn diameter_of_binary_tree(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) -\u0026gt; i32 { let mut ans = 0; dfs(root, \u0026amp;mut ans); ans } fn main() { let root = Some(Box::new(TreeNode { val: 1, left: Some(Box::new(TreeNode { val: 2, left: Some(Box::new(TreeNode { val: 4, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 5, left: None, right: None, })), })), right: Some(Box::new(TreeNode { val: 3, left: None, right: None, })), })); println!(\u0026#34;{}\u0026#34;, diameter_of_binary_tree(\u0026amp;root)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function diameterOfBinaryTree(root) { let ans = 0; function dfs(node) { if (!node) return 0; const left = dfs(node.left); const right = dfs(node.right); ans = Math.max(ans, left + right); return 1 + Math.max(left, right); } dfs(root); return ans; } const root = new TreeNode(1, new TreeNode(2, new TreeNode(4), new TreeNode(5)), new TreeNode(3)); console.log(diameterOfBinaryTree(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/543-diameter-of-binary-tree/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nThe most common confusion in LeetCode 543 is deciding what the recursive function should return. It should return height, not diameter. The diameter is updated globally at each node using \u003ccode\u003eleftHeight + rightHeight\u003c/code\u003e. Once that separation is clear, the problem becomes a clean introduction to tree DP.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-13 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003etree DP\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003epostorder\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Diameter of Binary Tree, tree DP, height return, DFS, LeetCode 543\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 543 from the postorder height-return pattern, with step-by-step derivation, engineering analogies, and runnable multi-language solutions.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the root \u003ccode\u003eroot\u003c/code\u003e of a binary tree, return the diameter of the tree.\u003c/p\u003e","title":"Hot100: Diameter of Binary Tree (Tree DP / Height Return ACERS Guide)"},{"content":" Subtitle / Summary The real difficulty of LeetCode 236 is not memorizing an LCA template. It is defining what each recursive call should return. Once that return value becomes stable, the whole problem collapses into a short and very clean postorder recursion.\nReading time: 11-14 min Tags: Hot100, binary tree, LCA, DFS, postorder SEO keywords: Lowest Common Ancestor of a Binary Tree, LCA, postorder, DFS, LeetCode 236 Meta description: Learn LeetCode 236 from the recursive return-value semantics, with step-by-step derivation, engineering mappings, and runnable multi-language implementations. A — Algorithm Problem Restatement Given a binary tree, find the lowest common ancestor (LCA) of two given nodes p and q.\nThe LCA is defined as the lowest node in the tree that:\nis an ancestor of both p and q has the greatest possible depth among such common ancestors may be one of the nodes itself Input / Output Name Type Description root TreeNode root of the binary tree p, q TreeNode the two target nodes; examples identify them by unique values return TreeNode the lowest common ancestor of p and q Example 1 input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 output: 3 explanation: the LCA of nodes 5 and 1 is 3. Example 2 input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 output: 5 explanation: node 5 is an ancestor of node 4, and a node may be its own ancestor. Example 3 input: root = [1,2], p = 1, q = 2 output: 1 Constraints The number of nodes is in the range [2, 10^5] -10^9 \u0026lt;= Node.val \u0026lt;= 10^9 All Node.val values are unique p != q Both p and q exist in the tree Target Readers Hot100 learners who can write basic tree recursion but still get stuck on LCA return values Developers who want one stable postorder divide-and-conquer pattern for binary trees Engineers who work with org charts, directory trees, or component trees and need nearest shared ancestors Background / Motivation When many people first see this problem, they instinctively think about:\nbuilding parent pointers finding the path from the root to p finding the path from the root to q comparing the two paths Those methods work. But the most valuable version of this problem is the recursive one:\nno extra parent map no extra graph conversion one postorder traversal The core question is:\nWhat should a subtree report back to its parent?\nOnce that answer is clear, the implementation becomes short and reliable.\nCore Concepts Postorder recursion: first solve the left subtree, then the right subtree, then interpret the current node Return-value semantics: return the target node found in this subtree, or the confirmed LCA if it is already determined A node can be its own ancestor: if p is above q, the answer may be p Bubble up: once a subtree already holds the right answer, parents simply keep returning it upward C — Concepts How To Build The Solution From Scratch Step 1: Start from the rule that a node can be its own ancestor Look at the official second example:\np = 5, q = 4 answer = 5 That means:\nthe answer does not have to be strictly above both nodes if one target is already an ancestor of the other, that node is the LCA So when recursion reaches p or q, that event is already important enough to preserve.\nStep 2: Decide what each subtree should return The most stable return-value definition is:\ndfs(node) returns the key node already identified inside this subtree.\nThat return value has three possibilities:\nNone: this subtree contains neither target p or q: this subtree contains exactly one target so far the LCA node: this subtree has already found the final answer That single definition makes the whole recursion consistent.\nStep 3: Define the recursive subproblem Once the left subtree and right subtree have already reported what they found, how should the current node interpret those two results?\nThat leads directly to postorder structure:\nleft = dfs(node.left) right = dfs(node.right) Step 4: Define the base cases Two base cases matter most:\nif node is None: return None if node.val == p or node.val == q: return node The second one is the key. If the current node is one of the targets, that information must be kept and returned upward immediately.\nStep 5: List the possible outcomes after left and right return Once left and right come back, only three cases remain:\nboth are None: neither target is in this subtree exactly one is non-null: one target, or an already found answer, is somewhere below both are non-null: one target is on each side, so the current node is the first merge point That last case is the heart of the problem.\nStep 6: When does the current node become the answer? If both sides are non-null:\nif left and right: return node That means:\none target was found in the left subtree the other target was found in the right subtree So the current node is exactly their lowest common ancestor.\nStep 7: Why do we return the non-null side when only one side is non-null? Because the current node is not yet proven to be the LCA. It only knows that one important node exists somewhere below one side.\nSo the correct move is:\nreturn left if left else right This keeps bubbling the useful result upward until a higher node has enough information to decide.\nStep 8: Walk one example slowly Use the official first example:\nroot = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 At node 5, recursion immediately returns node 5. At node 1, recursion immediately returns node 1.\nBack at the root 3:\nleft = 5 right = 1 Both are non-null, so 3 becomes the LCA.\nStep 9: Why do runnable examples often pass values instead of node references? The actual LeetCode interface passes node objects p and q. But this problem guarantees all node values are unique.\nSo for blog examples, it is perfectly fine to pass pVal and qVal:\nthe example stays self-contained the recursion logic stays identical multi-language snippets become easier to run locally Assemble the Full Code Now combine the pieces above into the first complete working solution. This version passes target values for easier local execution.\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def lowest_common_ancestor(root, pVal, qVal): def dfs(node): if node is None: return None if node.val == pVal or node.val == qVal: return node left = dfs(node.left) right = dfs(node.right) if left and right: return node return left if left else right return dfs(root) if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode( 3, TreeNode(5, TreeNode(6), TreeNode(2, TreeNode(7), TreeNode(4))), TreeNode(1, TreeNode(0), TreeNode(8)), ) ans = lowest_common_ancestor(root, 5, 1) print(ans.val if ans else None) Reference Answer For LeetCode submission style, the same logic becomes:\nfrom typing import Optional class TreeNode: def __init__(self, x: int): self.val = x self.left: Optional[\u0026#34;TreeNode\u0026#34;] = None self.right: Optional[\u0026#34;TreeNode\u0026#34;] = None class Solution: def lowestCommonAncestor(self, root: \u0026#34;TreeNode\u0026#34;, p: \u0026#34;TreeNode\u0026#34;, q: \u0026#34;TreeNode\u0026#34;) -\u0026gt; \u0026#34;TreeNode\u0026#34;: if root is None 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 return left if left else right What mental model should stick? Do not memorize this as just \u0026ldquo;the LCA recursion\u0026rdquo;. Remember the design order:\ndefine what a subtree returns recurse left and right let the current node interpret those two returned values That sequence is the reusable idea.\nE — Engineering Scenario 1: Find the nearest shared manager in an org tree (Python) Background: in an org chart, you often need the closest shared manager of two employees.\nWhy it fits: that is the direct real-world shape of the LCA problem.\nclass Staff: def __init__(self, id_, left=None, right=None): self.id = id_ self.left = left self.right = right def lca(root, a, b): if root is None or root.id == a or root.id == b: return root left = lca(root.left, a, b) right = lca(root.right, a, b) if left and right: return root return left or right root = Staff(3, Staff(5), Staff(1)) print(lca(root, 5, 1).id) Scenario 2: Find the nearest shared container in a component tree (JavaScript) Background: while debugging layout or event boundaries, you may need the nearest shared container of two component nodes.\nWhy it fits: the LCA is exactly the first place where the two component paths merge.\nfunction Node(id, left = null, right = null) { this.id = id; this.left = left; this.right = right; } function lca(root, a, b) { if (!root || root.id === a || root.id === b) return root; const left = lca(root.left, a, b); const right = lca(root.right, a, b); if (left \u0026amp;\u0026amp; right) return root; return left || right; } const root = new Node(\u0026#34;page\u0026#34;, new Node(\u0026#34;sidebar\u0026#34;), new Node(\u0026#34;content\u0026#34;)); console.log(lca(root, \u0026#34;sidebar\u0026#34;, \u0026#34;content\u0026#34;).id); Scenario 3: Find the nearest shared parent directory in a tree model (Go) Background: when two files or folders need a batch operation, systems often first locate their nearest shared parent.\nWhy it fits: this is the directory-tree version of LCA.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Name string Left *Node Right *Node } func lca(root *Node, a, b string) *Node { if root == nil || root.Name == a || root.Name == b { return root } left := lca(root.Left, a, b) right := lca(root.Right, a, b) if left != nil \u0026amp;\u0026amp; right != nil { return root } if left != nil { return left } return right } func main() { root := \u0026amp;Node{Name: \u0026#34;root\u0026#34;, Left: \u0026amp;Node{Name: \u0026#34;docs\u0026#34;}, Right: \u0026amp;Node{Name: \u0026#34;blog\u0026#34;}} fmt.Println(lca(root, \u0026#34;docs\u0026#34;, \u0026#34;blog\u0026#34;).Name) } R — Reflection Complexity Analysis Time complexity: O(n), because each node is visited at most once Space complexity: O(h), where h is the tree height, due to recursion stack depth Alternative Approaches Method Time Extra space Notes Postorder return-value recursion O(n) O(h) cleanest and most direct Parent map + ancestor set O(n) O(n) iterative-friendly, but needs extra structure Root-to-node path comparison O(n) O(h) to O(n) intuitive, but requires two path searches Common Mistakes Forgetting that a node can be its own ancestor: this breaks examples like p = 5, q = 4. Not treating root == p or root == q as a base case: then the key signal never bubbles upward correctly. Treating \u0026ldquo;found one target\u0026rdquo; as \u0026ldquo;already found the answer\u0026rdquo;: the current node is only the LCA when both sides return non-null. Comparing by value in a tree without uniqueness: the runnable examples do it only because this problem guarantees unique values. FAQ 1. What if p is an ancestor of q? Then the answer is p. That is exactly what \u0026ldquo;a node can be its own ancestor\u0026rdquo; means in this problem.\n2. Why is postorder the natural fit? Because the current node must first know what the left subtree and right subtree found. Only then can it decide whether it is the first merge point.\n3. What if the problem did not guarantee that both nodes exist? Then the return-value design would need extra information, usually a count of how many targets were actually found. This problem is simpler because both are guaranteed to exist.\nBest Practices Write the return-value meaning in plain English before writing code Handle None, p, and q immediately at function entry Whenever both recursive sides are non-null, think \u0026ldquo;first merge point\u0026rdquo; Practice this together with LeetCode 235 to compare ordinary binary trees with BST-specific LCA logic S — Summary The heart of LeetCode 236 is the meaning of the recursive return value Once a subtree returns \u0026ldquo;the key node found below\u0026rdquo;, the whole solution becomes short and stable Postorder fits because the current node must interpret results from both children A node can be its own ancestor, and that rule is essential This problem is excellent practice for tree divide-and-conquer and upward result propagation Further Reading LeetCode 236: Lowest Common Ancestor of a Binary Tree LeetCode 235: Lowest Common Ancestor of a Binary Search Tree LeetCode 104: Maximum Depth of Binary Tree LeetCode 543: Diameter of Binary Tree CTA A good pair is 236 + 235. The first trains postorder return-value semantics; the second trains BST-specific pruning. Together they make the LCA topic much more stable.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def lowest_common_ancestor(root, pVal, qVal): if root is None or root.val == pVal or root.val == qVal: return root left = lowest_common_ancestor(root.left, pVal, qVal) right = lowest_common_ancestor(root.right, pVal, qVal) if left and right: return root return left if left else right if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode( 3, TreeNode(5, TreeNode(6), TreeNode(2, TreeNode(7), TreeNode(4))), TreeNode(1, TreeNode(0), TreeNode(8)), ) ans = lowest_common_ancestor(root, 5, 1) print(ans.val if ans else None) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } struct TreeNode* lowestCommonAncestor(struct TreeNode* root, int p, int q) { if (root == NULL || root-\u0026gt;val == p || root-\u0026gt;val == q) return root; struct TreeNode* left = lowestCommonAncestor(root-\u0026gt;left, p, q); struct TreeNode* right = lowestCommonAncestor(root-\u0026gt;right, p, q); if (left \u0026amp;\u0026amp; right) return root; return left ? left : right; } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(3); root-\u0026gt;left = new_node(5); root-\u0026gt;right = new_node(1); root-\u0026gt;left-\u0026gt;left = new_node(6); root-\u0026gt;left-\u0026gt;right = new_node(2); root-\u0026gt;left-\u0026gt;right-\u0026gt;left = new_node(7); root-\u0026gt;left-\u0026gt;right-\u0026gt;right = new_node(4); root-\u0026gt;right-\u0026gt;left = new_node(0); root-\u0026gt;right-\u0026gt;right = new_node(8); struct TreeNode* ans = lowestCommonAncestor(root, 5, 1); printf(\u0026#34;%d\\n\u0026#34;, ans ? ans-\u0026gt;val : -1); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; TreeNode* lowestCommonAncestor(TreeNode* root, int p, int q) { if (!root || root-\u0026gt;val == p || root-\u0026gt;val == q) return root; TreeNode* left = lowestCommonAncestor(root-\u0026gt;left, p, q); TreeNode* right = lowestCommonAncestor(root-\u0026gt;right, p, q); if (left \u0026amp;\u0026amp; right) return root; return left ? left : right; } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(3); root-\u0026gt;left = new TreeNode(5); root-\u0026gt;right = new TreeNode(1); root-\u0026gt;left-\u0026gt;left = new TreeNode(6); root-\u0026gt;left-\u0026gt;right = new TreeNode(2); root-\u0026gt;left-\u0026gt;right-\u0026gt;left = new TreeNode(7); root-\u0026gt;left-\u0026gt;right-\u0026gt;right = new TreeNode(4); root-\u0026gt;right-\u0026gt;left = new TreeNode(0); root-\u0026gt;right-\u0026gt;right = new TreeNode(8); TreeNode* ans = lowestCommonAncestor(root, 5, 1); std::cout \u0026lt;\u0026lt; (ans ? ans-\u0026gt;val : -1) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func lowestCommonAncestor(root *TreeNode, p, q int) *TreeNode { if root == nil || root.Val == p || root.Val == q { return root } left := lowestCommonAncestor(root.Left, p, q) right := lowestCommonAncestor(root.Right, p, q) if left != nil \u0026amp;\u0026amp; right != nil { return root } if left != nil { return left } return right } func main() { root := \u0026amp;TreeNode{ Val: 3, Left: \u0026amp;TreeNode{ Val: 5, Left: \u0026amp;TreeNode{Val: 6}, Right: \u0026amp;TreeNode{ Val: 2, Left: \u0026amp;TreeNode{Val: 7}, Right: \u0026amp;TreeNode{Val: 4}, }, }, Right: \u0026amp;TreeNode{ Val: 1, Left: \u0026amp;TreeNode{Val: 0}, Right: \u0026amp;TreeNode{Val: 8}, }, } ans := lowestCommonAncestor(root, 5, 1) fmt.Println(ans.Val) } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn lowest_common_ancestor(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, p: i32, q: i32) -\u0026gt; Option\u0026lt;i32\u0026gt; { match root { None =\u0026gt; None, Some(node) =\u0026gt; { if node.val == p || node.val == q { return Some(node.val); } let left = lowest_common_ancestor(\u0026amp;node.left, p, q); let right = lowest_common_ancestor(\u0026amp;node.right, p, q); if left.is_some() \u0026amp;\u0026amp; right.is_some() { return Some(node.val); } if left.is_some() { return left; } right } } } fn main() { let root = Some(Box::new(TreeNode { val: 3, left: Some(Box::new(TreeNode { val: 5, left: Some(Box::new(TreeNode { val: 6, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 2, left: Some(Box::new(TreeNode { val: 7, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 4, left: None, right: None, })), })), })), right: Some(Box::new(TreeNode { val: 1, left: Some(Box::new(TreeNode { val: 0, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 8, left: None, right: None, })), })), })); println!(\u0026#34;{:?}\u0026#34;, lowest_common_ancestor(\u0026amp;root, 5, 1)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function lowestCommonAncestor(root, p, q) { if (!root || root.val === p || root.val === q) return root; const left = lowestCommonAncestor(root.left, p, q); const right = lowestCommonAncestor(root.right, p, q); if (left \u0026amp;\u0026amp; right) return root; return left || right; } const root = new TreeNode( 3, new TreeNode(5, new TreeNode(6), new TreeNode(2, new TreeNode(7), new TreeNode(4))), new TreeNode(1, new TreeNode(0), new TreeNode(8)) ); const ans = lowestCommonAncestor(root, 5, 1); console.log(ans ? ans.val : null); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/236-lowest-common-ancestor-of-a-binary-tree/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nThe real difficulty of LeetCode 236 is not memorizing an LCA template. It is defining what each recursive call should return. Once that return value becomes stable, the whole problem collapses into a short and very clean postorder recursion.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 11-14 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eLCA\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003epostorder\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Lowest Common Ancestor of a Binary Tree, LCA, postorder, DFS, LeetCode 236\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 236 from the recursive return-value semantics, with step-by-step derivation, engineering mappings, and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven a binary tree, find the lowest common ancestor (LCA) of two given nodes \u003ccode\u003ep\u003c/code\u003e and \u003ccode\u003eq\u003c/code\u003e.\u003c/p\u003e","title":"Hot100: Lowest Common Ancestor of a Binary Tree (Postorder Return Semantics ACERS Guide)"},{"content":" Subtitle / Summary The hardest part of LeetCode 98 is usually not recursion itself. It is realizing that checking a node against only its parent is not enough. A valid BST is constrained by all of its ancestors, so the stable solution is to pass a valid range downward. This guide explains that idea from scratch, then connects it to the equivalent inorder-sorted view.\nReading time: 11-13 min Tags: Hot100, binary tree, BST, DFS, inorder traversal SEO keywords: Validate Binary Search Tree, BST, range constraints, inorder traversal, DFS, LeetCode 98 Meta description: Learn LeetCode 98 from the core invariant low \u0026lt; val \u0026lt; high, with step-by-step derivation, engineering mappings, and runnable multi-language implementations. A — Algorithm Problem Restatement Given the root root of a binary tree, determine whether it is a valid binary search tree (BST).\nA valid BST must satisfy all of the following:\nall values in the left subtree are strictly smaller than the current node all values in the right subtree are strictly greater than the current node both subtrees must also be valid BSTs Input / Output Name Type Description root TreeNode root of the binary tree return bool whether the tree is a valid BST Example 1 input: root = [2,1,3] output: true Example 2 input: root = [5,1,4,null,null,3,6] output: false explanation: the root value is 5, but the right child value is 4. Constraints The number of nodes in the tree is in the range [1, 10^4] -2^31 \u0026lt;= Node.val \u0026lt;= 2^31 - 1 Target Readers Hot100 learners who want a stable BST validation template Developers who can write tree recursion but still confuse local checks with global constraints Engineers working with ordered tree-like structures, rule hierarchies, or range-constrained node systems Background / Motivation This problem looks simple at first:\ncompare the current node with its left child compare the current node with its right child recurse But that approach is wrong.\nThe real issue is that BST validity is not a local parent-child rule. It is a subtree-wide constraint inherited from ancestors.\nThat is why this problem is valuable:\nit teaches when local checks are insufficient it teaches how to carry ancestor constraints downward it gives you a clean mental model that later helps with BST search, k-th smallest, recovery, and tree invariants in general Core Concepts Ancestor range constraint: the current node must fit inside a range determined by all ancestors, not only its parent Open interval (low, high): the current value must satisfy low \u0026lt; val \u0026lt; high Range propagation: the left subtree tightens the upper bound, and the right subtree tightens the lower bound Inorder monotonicity: an inorder traversal of a valid BST must be strictly increasing C — Concepts How To Build The Solution From Scratch Step 1: Start from the counterexample that breaks parent-only checks Look at the official counterexample:\nroot = [5,1,4,null,null,3,6] Once the root is 5, the entire right subtree must be strictly greater than 5. So the node 4 is already illegal.\nThe key observation is:\nafter you move into the right subtree you still have to remember the lower bound imposed by the ancestor 5 That means comparing only with the direct parent is not enough.\nStep 2: Decide what information a node must inherit If recursion reaches some node, what minimum information do we need?\nTwo values:\nlow: the smallest allowed value high: the largest allowed value So the recursive shape becomes:\ndef dfs(node, low, high): ... The meaning is simple:\ncheck whether the subtree rooted at node is fully valid inside the open interval (low, high).\nStep 3: Define the recursive subproblem Now the original problem becomes:\nIs this subtree valid, given the range it is allowed to occupy?\nThat is a stable recursive definition, because every node in a BST must satisfy a valid range, and that range narrows as we go down.\nStep 4: Define the base case If the current node is null, there is no violation inside this subtree.\nif node is None: return True An empty tree is a valid BST.\nStep 5: Check the current node against the inherited range Before checking children, verify whether the current value fits:\nif node.val \u0026lt;= low or node.val \u0026gt;= high: return False The inequality must be strict. BSTs in this problem do not allow duplicates on the boundary.\nStep 6: Pass tighter bounds to the children Once the current node value is fixed:\nthe left subtree must fit inside (low, node.val) the right subtree must fit inside (node.val, high) So the recursive step is:\nreturn dfs(node.left, low, node.val) and dfs(node.right, node.val, high) This is the core invariant of the whole problem.\nStep 7: Why there is no backtracking state to undo Unlike combination or permutation problems, we are not maintaining a mutable path.\nThere is no:\npath used state restoration Each recursive call simply receives a narrower valid range as a parameter. The state is functional and flows downward.\nStep 8: Walk the bad example slowly Again use:\nroot = [5,1,4,null,null,3,6] At the root:\nnode = 5 valid range is (-inf, +inf) valid Move to the left child 1:\nvalid range is (-inf, 5) valid Move to the right child 4:\nvalid range is (5, +inf) Now the failure is immediate:\n4 \u0026lt;= 5 So the tree is invalid without needing any deeper recursion.\nStep 9: Connect this to inorder traversal There is another equivalent way to validate a BST:\nits inorder traversal must be strictly increasing.\nSo you can also:\ndo an inorder traversal keep the previously visited value prev return False once the current value is \u0026lt;= prev That is also correct. But the range-constraint recursion is the better derivation-first explanation because it matches the actual definition of a BST.\nAssemble the Full Code Now combine the fragments above into the first complete working solution.\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def is_valid_bst(root): def dfs(node, low, high): if node is None: return True if node.val \u0026lt;= low or node.val \u0026gt;= high: return False return dfs(node.left, low, node.val) and dfs(node.right, node.val, high) return dfs(root, float(\u0026#34;-inf\u0026#34;), float(\u0026#34;inf\u0026#34;)) if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(2, TreeNode(1), TreeNode(3)) print(is_valid_bst(root)) Reference Answer For LeetCode submission style, the same logic becomes:\nfrom typing import Optional class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def isValidBST(self, root: Optional[TreeNode]) -\u0026gt; bool: def dfs(node: Optional[TreeNode], low: float, high: float) -\u0026gt; bool: if node is None: return True if node.val \u0026lt;= low or node.val \u0026gt;= high: return False return dfs(node.left, low, node.val) and dfs(node.right, node.val, high) return dfs(root, float(\u0026#34;-inf\u0026#34;), float(\u0026#34;inf\u0026#34;)) What mental model should stick? Do not remember this as \u0026ldquo;that BST template problem\u0026rdquo;. Remember the invariant:\nEvery node must satisfy the range imposed by all of its ancestors, not only by its parent.\nOnce that sentence is clear, the code follows naturally.\nE — Engineering Scenario 1: Validate range-constrained rule trees (Python) Background: some risk or quota systems organize thresholds as trees, where every child rule must stay inside an ancestor-defined range.\nWhy it fits: this is structurally the same as BST validation with inherited bounds.\nclass RuleNode: def __init__(self, value, left=None, right=None): self.value = value self.left = left self.right = right def validate_rule_tree(root, low=float(\u0026#34;-inf\u0026#34;), high=float(\u0026#34;inf\u0026#34;)): if root is None: return True if root.value \u0026lt;= low or root.value \u0026gt;= high: return False return validate_rule_tree(root.left, low, root.value) and validate_rule_tree(root.right, root.value, high) tree = RuleNode(50, RuleNode(20), RuleNode(80)) print(validate_rule_tree(tree)) Scenario 2: Sanity-check deserialized index trees (Go) Background: a service may restore an ordered tree structure from disk or remote storage during startup.\nWhy it fits: checking only direct children is not enough; you need full ancestor-bound validation.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Key int Left *Node Right *Node } func validate(root *Node, low, high int64) bool { if root == nil { return true } v := int64(root.Key) if v \u0026lt;= low || v \u0026gt;= high { return false } return validate(root.Left, low, v) \u0026amp;\u0026amp; validate(root.Right, v, high) } func main() { root := \u0026amp;Node{Key: 20, Left: \u0026amp;Node{Key: 10}, Right: \u0026amp;Node{Key: 30}} fmt.Println(validate(root, -1\u0026lt;\u0026lt;63, 1\u0026lt;\u0026lt;63-1)) } Scenario 3: Check ordering constraints in frontend rule trees (JavaScript) Background: a frontend config system may represent priority or routing rules as a tree.\nWhy it fits: if one child crosses an ancestor bound, the configuration order is inconsistent.\nfunction Node(priority, left = null, right = null) { this.priority = priority; this.left = left; this.right = right; } function validate(node, low = -Infinity, high = Infinity) { if (!node) return true; if (node.priority \u0026lt;= low || node.priority \u0026gt;= high) return false; return validate(node.left, low, node.priority) \u0026amp;\u0026amp; validate(node.right, node.priority, high); } const root = new Node(10, new Node(5), new Node(15)); console.log(validate(root)); R — Reflection Complexity Analysis Time complexity: O(n), because every node is visited once Space complexity: O(h), where h is the tree height, due to recursion stack depth Alternative Approaches Method Time Extra space Notes Range-constraint recursion O(n) O(h) most direct and definition-first Inorder strictly increasing check O(n) O(h) equally valid and very stable Parent-only comparison looks like O(n) O(h) wrong, because it misses ancestor constraints Common Mistakes Only comparing each node with its parent: this misses violations created by higher ancestors. Using non-strict inequalities incorrectly: BST validity here requires strict ordering. Using an int boundary sentinel carelessly: the node value range already reaches 32-bit extremes, so wider bounds are safer. Losing prev in the inorder method: if prev is not maintained correctly across recursion, the inorder check becomes invalid. FAQ 1. Why is checking node.left.val \u0026lt; node.val \u0026lt; node.right.val not enough? Because BST validity is a subtree-wide property. A node deep in the right subtree must still be greater than all ancestors that forced that branch.\n2. Why does inorder traversal work? Because a valid BST produces a strictly increasing inorder sequence. If the inorder sequence is not strictly increasing, the tree cannot be a BST.\n3. Which version should I remember first? Remember the range-constraint recursion first. It matches the problem definition more directly and transfers better to other ancestor-constrained tree problems.\nBest Practices Anchor the solution around the invariant low \u0026lt; val \u0026lt; high Use a wider boundary type than the node value type when needed Hand-simulate one valid case and one invalid case to lock in the range propagation Practice this together with inorder traversal problems, because BST and inorder are deeply connected S — Summary BST validation is not a local parent-child comparison problem; it is a full ancestor-range problem The clean recursive form is dfs(node, low, high) The boundary check must be strict: low \u0026lt; val \u0026lt; high Inorder monotonicity gives an equivalent validation view, but range propagation is the clearer derivation Once this invariant is stable, many BST and tree-constraint problems become easier Further Reading LeetCode 98: Validate Binary Search Tree LeetCode 94: Binary Tree Inorder Traversal LeetCode 230: Kth Smallest Element in a BST LeetCode 700: Search in a Binary Search Tree CTA A good follow-up set is 98 + 94 + 230. That combination locks in BST validation, inorder traversal, and one classic ordered-tree query pattern together.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def is_valid_bst(root): def dfs(node, low, high): if node is None: return True if node.val \u0026lt;= low or node.val \u0026gt;= high: return False return dfs(node.left, low, node.val) and dfs(node.right, node.val, high) return dfs(root, float(\u0026#34;-inf\u0026#34;), float(\u0026#34;inf\u0026#34;)) if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(2, TreeNode(1), TreeNode(3)) print(is_valid_bst(root)) #include \u0026lt;limits.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } bool dfs(struct TreeNode* node, long long low, long long high) { if (node == NULL) return true; long long v = node-\u0026gt;val; if (v \u0026lt;= low || v \u0026gt;= high) return false; return dfs(node-\u0026gt;left, low, v) \u0026amp;\u0026amp; dfs(node-\u0026gt;right, v, high); } bool isValidBST(struct TreeNode* root) { return dfs(root, LLONG_MIN, LLONG_MAX); } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(2); root-\u0026gt;left = new_node(1); root-\u0026gt;right = new_node(3); printf(\u0026#34;%s\\n\u0026#34;, isValidBST(root) ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;); free_tree(root); return 0; } #include \u0026lt;climits\u0026gt; #include \u0026lt;iostream\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; bool dfs(TreeNode* node, long long low, long long high) { if (!node) return true; long long v = node-\u0026gt;val; if (v \u0026lt;= low || v \u0026gt;= high) return false; return dfs(node-\u0026gt;left, low, v) \u0026amp;\u0026amp; dfs(node-\u0026gt;right, v, high); } bool isValidBST(TreeNode* root) { return dfs(root, LLONG_MIN, LLONG_MAX); } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(2); root-\u0026gt;left = new TreeNode(1); root-\u0026gt;right = new TreeNode(3); std::cout \u0026lt;\u0026lt; (isValidBST(root) ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func dfs(node *TreeNode, low, high int64) bool { if node == nil { return true } v := int64(node.Val) if v \u0026lt;= low || v \u0026gt;= high { return false } return dfs(node.Left, low, v) \u0026amp;\u0026amp; dfs(node.Right, v, high) } func isValidBST(root *TreeNode) bool { return dfs(root, -1\u0026lt;\u0026lt;63, 1\u0026lt;\u0026lt;63-1) } func main() { root := \u0026amp;TreeNode{ Val: 2, Left: \u0026amp;TreeNode{Val: 1}, Right: \u0026amp;TreeNode{Val: 3}, } fmt.Println(isValidBST(root)) } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn dfs(node: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, low: i64, high: i64) -\u0026gt; bool { match node { None =\u0026gt; true, Some(n) =\u0026gt; { let v = n.val as i64; if v \u0026lt;= low || v \u0026gt;= high { return false; } dfs(\u0026amp;n.left, low, v) \u0026amp;\u0026amp; dfs(\u0026amp;n.right, v, high) } } } fn is_valid_bst(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) -\u0026gt; bool { dfs(root, i64::MIN, i64::MAX) } fn main() { let root = Some(Box::new(TreeNode { val: 2, left: Some(Box::new(TreeNode { val: 1, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 3, left: None, right: None, })), })); println!(\u0026#34;{}\u0026#34;, is_valid_bst(\u0026amp;root)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function dfs(node, low, high) { if (!node) return true; if (node.val \u0026lt;= low || node.val \u0026gt;= high) return false; return dfs(node.left, low, node.val) \u0026amp;\u0026amp; dfs(node.right, node.val, high); } function isValidBST(root) { return dfs(root, -Infinity, Infinity); } const root = new TreeNode(2, new TreeNode(1), new TreeNode(3)); console.log(isValidBST(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/98-validate-binary-search-tree/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nThe hardest part of LeetCode 98 is usually not recursion itself. It is realizing that checking a node against only its parent is not enough. A valid BST is constrained by all of its ancestors, so the stable solution is to pass a valid range downward. This guide explains that idea from scratch, then connects it to the equivalent inorder-sorted view.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 11-13 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eBST\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003einorder traversal\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Validate Binary Search Tree, BST, range constraints, inorder traversal, DFS, LeetCode 98\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 98 from the core invariant \u003ccode\u003elow \u0026lt; val \u0026lt; high\u003c/code\u003e, with step-by-step derivation, engineering mappings, and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the root \u003ccode\u003eroot\u003c/code\u003e of a binary tree, determine whether it is a valid binary search tree (BST).\u003c/p\u003e","title":"Hot100: Validate Binary Search Tree (Range Constraints / Inorder ACERS Guide)"},{"content":"51. N-Queens is best learned by watching the code grow one small step at a time. This tutorial keeps only the teaching path: tiny example, first DFS skeleton, first correct version, column-only optimization, then full diagonal-state optimization.\nProblem The n-queens puzzle asks us to place n queens on an n x n chessboard so that no two queens attack each other.\nGiven an integer n, return all distinct solutions. Each solution is represented as a list of strings where:\n'Q' means a queen '.' means an empty cell Example 1 input: n = 4 output: [[\u0026#34;.Q..\u0026#34;,\u0026#34;...Q\u0026#34;,\u0026#34;Q...\u0026#34;,\u0026#34;..Q.\u0026#34;],[\u0026#34;..Q.\u0026#34;,\u0026#34;Q...\u0026#34;,\u0026#34;...Q\u0026#34;,\u0026#34;.Q..\u0026#34;]] Example 2 input: n = 1 output: [[\u0026#34;Q\u0026#34;]] Constraints 1 \u0026lt;= n \u0026lt;= 9 Build It Step by Step Step 1: Start from the smallest example that exposes the conflict First ask one concrete question:\nwhat actually becomes difficult after we place one queen?\nUse n = 4. Suppose row 0 places a queen at column 1.\nThen the next rows do not get to choose from “all empty cells”. They immediately inherit three restrictions:\ncolumn 1 is blocked one top-left to bottom-right diagonal is blocked one top-right to bottom-left diagonal is blocked That tiny example already tells us the real structure of the problem:\nwe will place queens in some fixed order each choice blocks future positions This step does not add code yet. It gives us the evidence that the recursion should be about one row at a time, not about scanning arbitrary board cells.\nCurrent version can do:\nidentify the shape of the search space It still lacks:\na named subproblem any code structure Step 2: Define the smaller subproblem and write the smallest DFS skeleton Now solve one specific problem:\nif rows 0 .. row-1 have already been handled, what remains?\nThe remaining work is:\nchoose a column for row row, then solve the same problem for row row + 1\nSo one recursion layer should mean:\ndfs(row) = “decide where to place the queen for row row” Add this minimal skeleton:\ndef dfs(row: int) -\u0026gt; None: if row == n: return for col in range(n): dfs(row + 1) This is the first real code.\nCurrent version can do:\nexpress the row-based search structure show that one layer corresponds to one row It still lacks:\nany record of what columns were chosen any way to collect an answer any legality check Step 3: Add the first state variable for the partial answer Now solve the next problem:\nif we choose a column in one row, where do we store that choice?\nAdd one state:\nqueens = [-1] * n Here:\nqueens[row] = col means row row chooses column col -1 means that row is still unset In the previous DFS skeleton, replace the loop body with:\nfor col in range(n): queens[row] = col dfs(row + 1) queens[row] = -1 Now the code has the real backtracking rhythm:\nchoose recurse undo Current version can do:\nremember one full path of choices undo one choice before trying the next one It still lacks:\nany leaf action when a full placement is reached any legality check Step 4: Add the completion rule and build the board Next question:\nwhen does one branch become a complete answer?\nWhen row == n, every row already has a chosen column. So now we need one helper that converts queens into the required board strings.\nAdd:\ndef build_board() -\u0026gt; List[str]: board = [] for col in queens: board.append(\u0026#34;.\u0026#34; * col + \u0026#34;Q\u0026#34; + \u0026#34;.\u0026#34; * (n - col - 1)) return board Then replace the base case:\nif row == n: res.append(build_board()) return At this point the code knows:\nwhat the partial state means when a branch is complete how to materialize one answer Current version can do:\ncollect a full board if the choices happen to be legal It still lacks:\na rule that filters out illegal placements Step 5: Add the first correct legality check Now solve the most urgent missing problem:\nhow do we know whether (row, col) is legal?\nThe simplest correct answer is:\ncompare it with every queen already placed in rows 0 .. row-1 Add:\ndef is_valid(row: int, col: int) -\u0026gt; bool: for prev_row in range(row): prev_col = queens[prev_row] if prev_col == col: return False if abs(prev_row - row) == abs(prev_col - col): return False return True Now go back to the loop and replace it with:\nfor col in range(n): if not is_valid(row, col): continue queens[row] = col dfs(row + 1) queens[row] = -1 This is the first complete correct version. It is not optimized, but it is already a valid solver.\nCurrent version can do:\ngenerate all correct answers reject same-column conflicts reject diagonal conflicts It still lacks:\nefficient checking Step 6: Keep the diagonal scan for now, but optimize columns first Now ask a narrower optimization question:\nwhich repeated check is the easiest to remove first?\nThe easiest one is the column check. We do not have to optimize everything at once.\nAdd one new state:\ncols = [False] * n This stores:\ncols[col] == True if that column is already occupied Because column checking no longer needs the full is_valid(), split the old helper into a diagonal-only check.\nAdd:\ndef is_valid_diagonal(row: int, col: int) -\u0026gt; bool: for prev_row in range(row): prev_col = queens[prev_row] if abs(prev_row - row) == abs(prev_col - col): return False return True Now replace the previous loop with this middle version:\nfor col in range(n): if cols[col]: continue if not is_valid_diagonal(row, col): continue queens[row] = col cols[col] = True dfs(row + 1) cols[col] = False queens[row] = -1 This middle version matters. It proves the final optimized answer does not appear in one jump.\nCurrent version can do:\ncheck columns in O(1) still check diagonals by scanning previous rows It still lacks:\nO(1) diagonal checks Step 7: Define what the diagonal helper arrays should store Now solve the next specific problem:\nif columns can be cached, what exactly should be cached for diagonals?\nFirst stabilize the concept before the arrays:\ncells on the same main diagonal share the same row - col cells on the same anti-diagonal share the same row + col So the helper arrays should not store cells. They should store whether one diagonal line is already occupied.\nAdd:\ndiag1 = [False] * (2 * n - 1) diag2 = [False] * (2 * n - 1) Their meanings are:\ndiag1[i]: main diagonal i is occupied diag2[i]: anti-diagonal i is occupied Now add the index mapping:\nd1 = row - col + n - 1 d2 = row + col The + n - 1 is only there because row - col can be negative.\nCurrent version can do:\nname exactly what information the diagonal helpers should store It still lacks:\nwiring those helpers into the DFS loop Step 8: Replace the diagonal scan with full O(1) state checks Now we can remove the last scan-based part.\nIn the previous middle version, replace the legality section and the choose/undo section with:\nfor col in range(n): d1 = row - col + n - 1 d2 = row + col if cols[col] or diag1[d1] or diag2[d2]: continue queens[row] = col cols[col] = True diag1[d1] = True diag2[d2] = True dfs(row + 1) queens[row] = -1 cols[col] = False diag1[d1] = False diag2[d2] = False This is the final optimized version.\nNotice what changed relative to the middle version:\ncols[col] stayed exactly the same the diagonal scan was replaced by diag1[d1] and diag2[d2] choose and undo now update three helper states instead of one Current version can do:\ncheck columns in O(1) check both diagonal families in O(1) keep the same row-based DFS skeleton as all earlier versions It still lacks:\nnothing essential; this is the finished logic Step 9: Walk one branch slowly Still use n = 4. Suppose row 0 chooses column 1.\nThen these facts become true together:\nqueens[0] = 1 cols[1] = True diag1[0 - 1 + 3] = diag1[2] = True diag2[0 + 1] = diag2[1] = True Now move to row 1.\nWhen the loop tries candidates:\ncol = 1 fails immediately because cols[1] is already true some candidates fail because diag1[d1] is true some candidates fail because diag2[d2] is true Only columns that survive all three tests continue.\nThat is the whole final rhythm:\nchoose a column for the current row check stored occupancy state recurse undo everything before the next choice At this point, the code is already complete and runnable:\nfrom typing import List def solve_n_queens(n: int) -\u0026gt; List[List[str]]: res: List[List[str]] = [] queens = [-1] * n cols = [False] * n diag1 = [False] * (2 * n - 1) diag2 = [False] * (2 * n - 1) def build_board() -\u0026gt; List[str]: board = [] for col in queens: board.append(\u0026#34;.\u0026#34; * col + \u0026#34;Q\u0026#34; + \u0026#34;.\u0026#34; * (n - col - 1)) return board def dfs(row: int) -\u0026gt; None: if row == n: res.append(build_board()) return for col in range(n): d1 = row - col + n - 1 d2 = row + col if cols[col] or diag1[d1] or diag2[d2]: continue queens[row] = col cols[col] = True diag1[d1] = True diag2[d2] = True dfs(row + 1) queens[row] = -1 cols[col] = False diag1[d1] = False diag2[d2] = False dfs(0) return res ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/backtracking/51-n-queens/","summary":"\u003cp\u003e\u003ccode\u003e51. N-Queens\u003c/code\u003e is best learned by watching the code grow one small step at a time.\nThis tutorial keeps only the teaching path: tiny example, first DFS skeleton, first correct version, column-only optimization, then full diagonal-state optimization.\u003c/p\u003e\n\u003ch2 id=\"problem\"\u003eProblem\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003en\u003c/code\u003e-queens puzzle asks us to place \u003ccode\u003en\u003c/code\u003e queens on an \u003ccode\u003en x n\u003c/code\u003e chessboard so that no two queens attack each other.\u003c/p\u003e\n\u003cp\u003eGiven an integer \u003ccode\u003en\u003c/code\u003e, return all distinct solutions.\nEach solution is represented as a list of strings where:\u003c/p\u003e","title":"Hot100: N-Queens Incremental Build Tutorial"},{"content":" Subtitle / Summary 131. Palindrome Partitioning is not hard because of recursion alone. The real challenge is separating two concerns clearly: where the next cut starts, and whether the current substring is a valid palindrome.\nReading time: 15-18 min Tags: Hot100, backtracking, string, palindrome, DP SEO keywords: Palindrome Partitioning, backtracking, palindrome DP, string partition, LeetCode 131 Meta description: Learn LeetCode 131 by building the solution from scratch, using suffix-based DFS plus a precomputed palindrome table. A — Algorithm Problem Restatement Given a string s, partition it so that every substring in the partition is a palindrome. Return all possible palindrome partitionings of s.\nInput / Output Name Type Description s string lowercase English string return List[List[str]] all valid palindrome partitions Example 1 input: s = \u0026#34;aab\u0026#34; output: [[\u0026#34;a\u0026#34;,\u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;],[\u0026#34;aa\u0026#34;,\u0026#34;b\u0026#34;]] Example 2 input: s = \u0026#34;a\u0026#34; output: [[\u0026#34;a\u0026#34;]] Constraints 1 \u0026lt;= s.length \u0026lt;= 16 s consists of lowercase English letters only Target Readers learners who already know array-based backtracking and now want string partition search developers who mix up “where the cut starts” and “whether this substring is legal” readers who want a reusable model for “precompute valid intervals, then DFS the suffix” Background / Motivation This problem is a very good training ground for a common search pattern:\nfirst decide which intervals are valid pieces, then recursively partition the whole string using only valid pieces\nIf you look at it only as a “palindrome problem”, you may focus too much on is_palindrome. But structurally, it is a string partition problem:\nstart tells us where the remaining suffix begins path stores the pieces already chosen [start, end] is the next candidate piece only valid pieces may be appended before recursing That model transfers well beyond palindromes, including dictionary segmentation and rule-based text decomposition.\nCore Concepts start: the first index of the suffix not yet partitioned path: the partition pieces already chosen Palindrome interval: whether s[l:r+1] is a palindrome Interval precomputation: use pal[l][r] to cache legal substrings Partition-style DFS: choose one valid piece, then recurse on the suffix behind it C — Concepts How To Build The Solution From Scratch Step 1: Start from a tiny example Take s = \u0026quot;aab\u0026quot;.\nThe valid answers are:\n[\u0026quot;a\u0026quot;, \u0026quot;a\u0026quot;, \u0026quot;b\u0026quot;] [\u0026quot;aa\u0026quot;, \u0026quot;b\u0026quot;] The key observation is not just what the answers are. It is the structure:\nat index start, choose one ending position end if s[start:end+1] is valid, recurse on the suffix after end Step 2: What must the partial answer remember? Because we are building one partition gradually, we need one state for the pieces already chosen. That is path.\npath = [] It represents the current DFS branch, not the final full answer set.\nStep 3: How should we name the remaining subproblem? Once path is fixed, the only question left is:\nhow do we partition the suffix starting at index start?\nSo the recursion signature becomes:\ndef dfs(start: int) -\u0026gt; None: ... This is similar in spirit to startIndex in array backtracking, but now the meaning is “where the remaining suffix begins”.\nStep 4: When is one partition complete? When start == len(s), the whole string has already been split into valid pieces.\nif start == len(s): res.append(path.copy()) return That is the leaf condition.\nStep 5: What choices are available next? At the current start, every end from start to n - 1 is a candidate:\nfor end in range(start, len(s)): ... Each candidate means “take substring s[start:end+1] as the next piece”.\nStep 6: How do we check whether a candidate piece is a palindrome? The most direct helper is a two-pointer check:\ndef is_pal(left: int, right: int) -\u0026gt; bool: while left \u0026lt; right: if s[left] != s[right]: return False left += 1 right -= 1 return True Then DFS could test each candidate before recursing.\nStep 7: Why is repeated palindrome checking the weak point? If DFS checks palindromes on demand, the same substring may be tested many times. That is acceptable for this constraint size, but not the cleanest reusable model.\nThe cleaner split of responsibilities is:\nprecompute which intervals are valid palindromes let DFS focus only on partition enumeration Step 8: How do we precompute all palindrome intervals? Define pal[i][j] as whether s[i:j+1] is a palindrome.\nThe transition is:\nif s[i] != s[j], then the interval is not a palindrome otherwise it is valid when the interval is short enough or the inner interval is already a palindrome for i in range(n - 1, -1, -1): for j in range(i, n): if s[i] == s[j] and (j - i \u0026lt;= 2 or pal[i + 1][j - 1]): pal[i][j] = True We iterate i backward so pal[i + 1][j - 1] is ready before we need it.\nStep 9: What does DFS look like after the table is ready? Now the DFS only asks whether the interval is legal:\nif pal[start][end]: path.append(s[start:end + 1]) dfs(end + 1) path.pop() At that point the problem has been cleanly separated into:\npal: interval legality dfs: partition enumeration Step 10: Walk one branch slowly Still using s = \u0026quot;aab\u0026quot;:\nStart:\nstart = 0 path = [] Try end = 0:\nsubstring \u0026quot;a\u0026quot; is a palindrome path = [\u0026quot;a\u0026quot;] recurse on dfs(1) Inside dfs(1):\nend = 1, substring \u0026quot;a\u0026quot; is valid path = [\u0026quot;a\u0026quot;, \u0026quot;a\u0026quot;] recurse on dfs(2) Inside dfs(2):\nend = 2, substring \u0026quot;b\u0026quot; is valid path = [\u0026quot;a\u0026quot;, \u0026quot;a\u0026quot;, \u0026quot;b\u0026quot;] recurse on dfs(3) Now start == len(s), so collect one answer.\nBack at the top level, end = 1 gives \u0026quot;aa\u0026quot;, which also leads to a valid solution [\u0026quot;aa\u0026quot;, \u0026quot;b\u0026quot;].\nAssemble the Full Code Now combine palindrome precomputation and DFS into one runnable implementation.\nfrom typing import List def partition_palindrome(s: str) -\u0026gt; List[List[str]]: n = len(s) pal = [[False] * n for _ in range(n)] for i in range(n - 1, -1, -1): for j in range(i, n): if s[i] == s[j] and (j - i \u0026lt;= 2 or pal[i + 1][j - 1]): pal[i][j] = True res: List[List[str]] = [] path: List[str] = [] def dfs(start: int) -\u0026gt; None: if start == n: res.append(path.copy()) return for end in range(start, n): if not pal[start][end]: continue path.append(s[start : end + 1]) dfs(end + 1) path.pop() dfs(0) return res if __name__ == \u0026#34;__main__\u0026#34;: print(partition_palindrome(\u0026#34;aab\u0026#34;)) print(partition_palindrome(\u0026#34;a\u0026#34;)) Reference Answer For the LeetCode submission form:\nfrom typing import List class Solution: def partition(self, s: str) -\u0026gt; List[List[str]]: n = len(s) pal = [[False] * n for _ in range(n)] for i in range(n - 1, -1, -1): for j in range(i, n): if s[i] == s[j] and (j - i \u0026lt;= 2 or pal[i + 1][j - 1]): pal[i][j] = True res: List[List[str]] = [] path: List[str] = [] def dfs(start: int) -\u0026gt; None: if start == n: res.append(path.copy()) return for end in range(start, n): if not pal[start][end]: continue path.append(s[start : end + 1]) dfs(end + 1) path.pop() dfs(0) return res What method did we just build? Formally, it is:\nbacktracking string partition search interval DP precomputation But the more reusable model is:\ndfs(start) means “partition the remaining suffix” pal[start][end] answers whether the next piece is legal DFS chooses one legal piece at a time and recurses on the rest E — Engineering Scenario 1: Dictionary-constrained segmentation (Python) Background: a search or NLP prototype wants to split a string into all valid token sequences from a small vocabulary.\nWhy this fits: the structure is the same as this problem, except “palindrome” is replaced by “present in a dictionary”.\nfrom typing import List, Set def all_segmentations(s: str, vocab: Set[str]) -\u0026gt; List[List[str]]: n = len(s) valid = [[False] * n for _ in range(n)] for i in range(n): for j in range(i, n): valid[i][j] = s[i : j + 1] in vocab res: List[List[str]] = [] path: List[str] = [] def dfs(start: int) -\u0026gt; None: if start == n: res.append(path.copy()) return for end in range(start, n): if not valid[start][end]: continue path.append(s[start : end + 1]) dfs(end + 1) path.pop() dfs(0) return res print(all_segmentations(\u0026#34;applepen\u0026#34;, {\u0026#34;apple\u0026#34;, \u0026#34;app\u0026#34;, \u0026#34;le\u0026#34;, \u0026#34;pen\u0026#34;})) Scenario 2: Rule-based segment planner (Go) Background: a parser or log-cleaning pipeline first decides which intervals satisfy a rule, then enumerates all valid full decompositions.\nWhy this fits: the reusable pattern is “precompute legal intervals, then DFS the suffix”.\npackage main import \u0026#34;fmt\u0026#34; func partitions(s string, valid map[string]bool) [][]string { n := len(s) ok := make([][]bool, n) for i := 0; i \u0026lt; n; i++ { ok[i] = make([]bool, n) for j := i; j \u0026lt; n; j++ { ok[i][j] = valid[s[i:j+1]] } } res := make([][]string, 0) path := make([]string, 0, n) var dfs func(int) dfs = func(start int) { if start == n { snapshot := append([]string(nil), path...) res = append(res, snapshot) return } for end := start; end \u0026lt; n; end++ { if !ok[start][end] { continue } path = append(path, s[start:end+1]) dfs(end + 1) path = path[:len(path)-1] } } dfs(0) return res } func main() { fmt.Println(partitions(\u0026#34;abc\u0026#34;, map[string]bool{\u0026#34;a\u0026#34;: true, \u0026#34;ab\u0026#34;: true, \u0026#34;bc\u0026#34;: true, \u0026#34;c\u0026#34;: true})) } Scenario 3: Frontend interactive text segmentation (JavaScript) Background: a text puzzle or input experiment wants to show all valid ways to split the current input.\nWhy this fits: as long as validity can be cached per interval, the DFS layer is the same.\nfunction segmentations(s, allow) { const n = s.length; const ok = Array.from({ length: n }, () =\u0026gt; Array(n).fill(false)); for (let i = 0; i \u0026lt; n; i += 1) { for (let j = i; j \u0026lt; n; j += 1) { ok[i][j] = allow.has(s.slice(i, j + 1)); } } const res = []; const path = []; function dfs(start) { if (start === n) { res.push([...path]); return; } for (let end = start; end \u0026lt; n; end += 1) { if (!ok[start][end]) continue; path.push(s.slice(start, end + 1)); dfs(end + 1); path.pop(); } } dfs(0); return res; } console.log(segmentations(\u0026#34;level\u0026#34;, new Set([\u0026#34;l\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;v\u0026#34;, \u0026#34;eve\u0026#34;, \u0026#34;level\u0026#34;]))); R — Reflection Complexity Analysis Palindrome-table preprocessing: O(n^2) DFS enumeration: worst-case about O(n * 2^(n-1)) many cut positions may be legal copying paths and slicing strings adds linear costs along the way Auxiliary space: O(n^2 + n) O(n^2) for the palindrome table O(n) for recursion depth and the current path plus output space if all returned answers are counted Comparison with other approaches Method Idea Advantage Limitation DFS + palindrome table precompute legal intervals, then partition clean responsibility split needs an O(n^2) table DFS + on-demand palindrome check validate each candidate with two pointers short to start with repeats interval checks DP only compute feasibility or min cuts good for decision problems does not enumerate all partitions Common Mistakes treating start as a length instead of a suffix index recursing before verifying whether the current substring is valid filling the palindrome table in the wrong order forgetting to pop() after appending a chosen substring Common Questions and Pitfalls Why is a palindrome table worth building first? Because DFS revisits overlapping intervals. Once pal[i][j] is available, each legality check becomes O(1) and the DFS stays focused on partitioning.\nCan this problem be solved without DP? Yes.\nYou can do DFS and check each substring with two pointers on demand. That version is valid, but the table-based version is a cleaner template for future interval-partition problems.\nHow is this related to 139. Word Break? They share the same structural idea:\nchoose one valid substring from the current start recurse or transition on the suffix behind it The difference is:\n131 asks for all full partitions 139 often asks only whether at least one valid decomposition exists Best Practices and Recommendations whenever you see “string partition + local validity”, ask whether legal intervals can be cached first define dfs(start) clearly before choosing any optimization separate “is this piece legal?” from “how do I continue after choosing it?” draw the recursion on \u0026quot;aab\u0026quot; once; it reveals the template much better than memorizing code S — Summary 131. Palindrome Partitioning is fundamentally a string-partition backtracking problem start means the suffix boundary, and path stores the pieces already chosen once legality is precomputed as pal[i][j], DFS can focus entirely on enumeration the high-value transferable idea is “precompute valid intervals, then DFS the suffix” this pattern applies to many partition and segmentation problems beyond palindromes Recommended Follow-Up Reading 132. Palindrome Partitioning II: optimize from “enumerate all” to “min cuts” 139. Word Break: interval-validity partitioning for reachability 78. Subsets: the base backtracking tree model 17. Letter Combinations of a Phone Number: fixed-depth string DFS Action Step After finishing this problem, revisit it as a partition template instead of only a palindrome question. Once you can explain the roles of pal and dfs(start) separately, the structure becomes much more reusable.\nMulti-Language Implementations Python from typing import List def partition_palindrome(s: str) -\u0026gt; List[List[str]]: n = len(s) pal = [[False] * n for _ in range(n)] for i in range(n - 1, -1, -1): for j in range(i, n): if s[i] == s[j] and (j - i \u0026lt;= 2 or pal[i + 1][j - 1]): pal[i][j] = True res: List[List[str]] = [] path: List[str] = [] def dfs(start: int) -\u0026gt; None: if start == n: res.append(path.copy()) return for end in range(start, n): if not pal[start][end]: continue path.append(s[start : end + 1]) dfs(end + 1) path.pop() dfs(0) return res C #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; typedef struct { char*** data; int* col_sizes; int size; int capacity; } Result; static char* clone_str(const char* s) { size_t len = strlen(s); char* copy = malloc(len + 1); memcpy(copy, s, len + 1); return copy; } static char* clone_range(const char* s, int left, int right) { int len = right - left + 1; char* copy = malloc(len + 1); memcpy(copy, s + left, len); copy[len] = \u0026#39;\\0\u0026#39;; return copy; } static void push_result(Result* res, char** path, int path_size) { if (res-\u0026gt;size == res-\u0026gt;capacity) { res-\u0026gt;capacity *= 2; res-\u0026gt;data = realloc(res-\u0026gt;data, sizeof(char**) * res-\u0026gt;capacity); res-\u0026gt;col_sizes = realloc(res-\u0026gt;col_sizes, sizeof(int) * res-\u0026gt;capacity); } char** row = malloc(sizeof(char*) * path_size); for (int i = 0; i \u0026lt; path_size; ++i) { row[i] = clone_str(path[i]); } res-\u0026gt;data[res-\u0026gt;size] = row; res-\u0026gt;col_sizes[res-\u0026gt;size] = path_size; res-\u0026gt;size += 1; } static void dfs(const char* s, int n, int start, bool* pal, char** path, int path_size, Result* res) { if (start == n) { push_result(res, path, path_size); return; } for (int end = start; end \u0026lt; n; ++end) { if (!pal[start * n + end]) { continue; } path[path_size] = clone_range(s, start, end); dfs(s, n, end + 1, pal, path, path_size + 1, res); free(path[path_size]); } } char*** partition(char* s, int* returnSize, int** returnColumnSizes) { int n = (int)strlen(s); bool* pal = calloc(n * n, sizeof(bool)); for (int i = n - 1; i \u0026gt;= 0; --i) { for (int j = i; j \u0026lt; n; ++j) { if (s[i] == s[j] \u0026amp;\u0026amp; (j - i \u0026lt;= 2 || pal[(i + 1) * n + (j - 1)])) { pal[i * n + j] = true; } } } Result res = {0}; res.capacity = 16; res.data = malloc(sizeof(char**) * res.capacity); res.col_sizes = malloc(sizeof(int) * res.capacity); char** path = malloc(sizeof(char*) * n); dfs(s, n, 0, pal, path, 0, \u0026amp;res); free(path); free(pal); *returnSize = res.size; *returnColumnSizes = res.col_sizes; return res.data; } C++ #include \u0026lt;string\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; class Solution { public: vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; partition(string s) { int n = (int)s.size(); vector\u0026lt;vector\u0026lt;bool\u0026gt;\u0026gt; pal(n, vector\u0026lt;bool\u0026gt;(n, false)); for (int i = n - 1; i \u0026gt;= 0; --i) { for (int j = i; j \u0026lt; n; ++j) { if (s[i] == s[j] \u0026amp;\u0026amp; (j - i \u0026lt;= 2 || pal[i + 1][j - 1])) { pal[i][j] = true; } } } vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt; res; vector\u0026lt;string\u0026gt; path; dfs(s, 0, pal, path, res); return res; } private: void dfs(const string\u0026amp; s, int start, const vector\u0026lt;vector\u0026lt;bool\u0026gt;\u0026gt;\u0026amp; pal, vector\u0026lt;string\u0026gt;\u0026amp; path, vector\u0026lt;vector\u0026lt;string\u0026gt;\u0026gt;\u0026amp; res) { if (start == (int)s.size()) { res.push_back(path); return; } for (int end = start; end \u0026lt; (int)s.size(); ++end) { if (!pal[start][end]) { continue; } path.push_back(s.substr(start, end - start + 1)); dfs(s, end + 1, pal, path, res); path.pop_back(); } } }; Go package main func partition(s string) [][]string { n := len(s) pal := make([][]bool, n) for i := 0; i \u0026lt; n; i++ { pal[i] = make([]bool, n) } for i := n - 1; i \u0026gt;= 0; i-- { for j := i; j \u0026lt; n; j++ { if s[i] == s[j] \u0026amp;\u0026amp; (j-i \u0026lt;= 2 || pal[i+1][j-1]) { pal[i][j] = true } } } res := make([][]string, 0) path := make([]string, 0, n) var dfs func(int) dfs = func(start int) { if start == n { snapshot := append([]string(nil), path...) res = append(res, snapshot) return } for end := start; end \u0026lt; n; end++ { if !pal[start][end] { continue } path = append(path, s[start:end+1]) dfs(end + 1) path = path[:len(path)-1] } } dfs(0) return res } Rust impl Solution { pub fn partition(s: String) -\u0026gt; Vec\u0026lt;Vec\u0026lt;String\u0026gt;\u0026gt; { let bytes = s.as_bytes(); let n = bytes.len(); let mut pal = vec![vec![false; n]; n]; for i in (0..n).rev() { for j in i..n { if bytes[i] == bytes[j] \u0026amp;\u0026amp; (j - i \u0026lt;= 2 || pal[i + 1][j - 1]) { pal[i][j] = true; } } } fn dfs(bytes: \u0026amp;[u8], start: usize, pal: \u0026amp;Vec\u0026lt;Vec\u0026lt;bool\u0026gt;\u0026gt;, path: \u0026amp;mut Vec\u0026lt;String\u0026gt;, res: \u0026amp;mut Vec\u0026lt;Vec\u0026lt;String\u0026gt;\u0026gt;) { if start == bytes.len() { res.push(path.clone()); return; } for end in start..bytes.len() { if !pal[start][end] { continue; } path.push(String::from_utf8(bytes[start..=end].to_vec()).unwrap()); dfs(bytes, end + 1, pal, path, res); path.pop(); } } let mut res: Vec\u0026lt;Vec\u0026lt;String\u0026gt;\u0026gt; = Vec::new(); let mut path: Vec\u0026lt;String\u0026gt; = Vec::new(); dfs(bytes, 0, \u0026amp;pal, \u0026amp;mut path, \u0026amp;mut res); res } } JavaScript /** * @param {string} s * @return {string[][]} */ var partition = function (s) { const n = s.length; const pal = Array.from({ length: n }, () =\u0026gt; Array(n).fill(false)); for (let i = n - 1; i \u0026gt;= 0; i -= 1) { for (let j = i; j \u0026lt; n; j += 1) { if (s[i] === s[j] \u0026amp;\u0026amp; (j - i \u0026lt;= 2 || pal[i + 1][j - 1])) { pal[i][j] = true; } } } const res = []; const path = []; function dfs(start) { if (start === n) { res.push([...path]); return; } for (let end = start; end \u0026lt; n; end += 1) { if (!pal[start][end]) continue; path.push(s.slice(start, end + 1)); dfs(end + 1); path.pop(); } } dfs(0); return res; }; ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/backtracking/131-palindrome-partitioning/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\n\u003ccode\u003e131. Palindrome Partitioning\u003c/code\u003e is not hard because of recursion alone. The real challenge is separating two concerns clearly: where the next cut starts, and whether the current substring is a valid palindrome.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 15-18 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebacktracking\u003c/code\u003e, \u003ccode\u003estring\u003c/code\u003e, \u003ccode\u003epalindrome\u003c/code\u003e, \u003ccode\u003eDP\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Palindrome Partitioning, backtracking, palindrome DP, string partition, LeetCode 131\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 131 by building the solution from scratch, using suffix-based DFS plus a precomputed palindrome table.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven a string \u003ccode\u003es\u003c/code\u003e, partition it so that every substring in the partition is a palindrome.\nReturn all possible palindrome partitionings of \u003ccode\u003es\u003c/code\u003e.\u003c/p\u003e","title":"Hot100: Palindrome Partitioning (Backtracking / Palindrome Table ACERS Guide)"},{"content":" Subtitle / Summary\n216. Combination Sum III is where backtracking adds one more important constraint: not only must the sum match, but the combination length must be exactly k. That turns the problem into a clean fixed-length combination search over the bounded range 1..9.\nReading time: 12-15 min Tags: backtracking, fixed length, pruning, combination search SEO keywords: Combination Sum III, fixed-length backtracking, k numbers, pruning, LeetCode 216 Meta description: Build the stable solution for LeetCode 216 from scratch by understanding exact-length backtracking, sorted pruning, and the bounded candidate set 1..9. A — Algorithm Problem Restatement Find all valid combinations of k numbers that add up to n, subject to these rules:\nonly numbers 1 through 9 may be used each number may be used at most once Return all valid combinations.\nThe answer must not contain duplicate combinations, and the order of combinations does not matter.\nInput / Output Name Type Description k int required number of chosen values n int target sum return List[List[int]] all valid length-k combinations summing to n Example 1 input: k = 3, n = 7 output: [[1,2,4]] Example 2 input: k = 3, n = 9 output: [[1,2,6],[1,3,5],[2,3,4]] Example 3 input: k = 4, n = 1 output: [] Constraints 2 \u0026lt;= k \u0026lt;= 9 1 \u0026lt;= n \u0026lt;= 60 Target Readers Learners who already understand 39 and 40, and now want to add the \u0026ldquo;exactly k numbers\u0026rdquo; constraint Readers who are comfortable with basic DFS but still mix up \u0026ldquo;target sum\u0026rdquo; and \u0026ldquo;required path length\u0026rdquo; Developers who want a stable pattern for fixed-size combination search Background / Motivation This problem is a strong follow-up to 39 and 40 because it keeps the backtracking skeleton but changes the search space:\nthe candidate range is fixed: 1..9 each number is used at most once the answer must contain exactly k numbers That last rule matters a lot.\nIn 39, reaching the target sum is enough.\nIn 216, reaching the sum too early is not enough if the path length is wrong, and reaching length k is not enough if the sum is still wrong.\nSo the model must now track two completion dimensions:\nhow much sum is left how many positions are already filled Core Concepts path: the numbers currently chosen remain: how much sum is still needed start: the next smallest allowed candidate in 1..9 Fixed length k: the branch is complete only when len(path) == k Use-once recursion: after choosing x, the next call starts from x + 1 Bounded search space: only digits 1..9 are considered C — Concepts How To Build The Solution From Scratch Step 1: Start from the smallest example that shows the fixed-length rule Take k = 3, n = 7.\nThe answer is:\n[1,2,4] This tiny example already reveals the real shape of the task:\nthe numbers must be distinct order does not matter the sum must be 7 the combination length must be exactly 3 So [7] is invalid even though the sum matches.\nThe length is wrong.\nStep 2: What does the partial answer need to remember? We still need the current branch:\npath = [] path means:\nthe numbers already chosen how many slots are already filled Step 3: What remains to solve after a few choices are fixed? As before, carrying the remaining target directly gives the cleanest model:\ndef dfs(start: int, remain: int) -\u0026gt; None: ... Now the recursive state is interpreted as:\nstart: the next smallest number we may try remain: how much sum is still needed path: how many numbers we already picked Step 4: When is one branch complete? This is the first big difference from 39.\nA branch is structurally complete when:\nif len(path) == k: ... At that moment:\nif remain == 0, we collect the answer otherwise the branch fails if len(path) == k: if remain == 0: res.append(path.copy()) return Step 5: What choices are available next? The candidates are not taken from an input array anymore.\nThey are the fixed numbers 1..9.\nSo a layer enumerates like this:\nfor x in range(start, 10): ... This already guarantees:\nnumbers are increasing each number is used at most once duplicate combinations never appear Step 6: What changes after choosing one number? If we choose x, the next layer must start from x + 1 because the current number may not be reused.\npath.append(x) dfs(x + 1, remain - x) path.pop() This is the same \u0026ldquo;use once\u0026rdquo; idea we learned in 40, but here it is even simpler because the candidate values are exactly 1..9.\nStep 7: What pruning is naturally available? Because x increases from small to large, once x \u0026gt; remain, later values are also too large:\nif x \u0026gt; remain: break There is also a length-based early return:\nif len(path) == k: ... That stops any branch from growing beyond the required size.\nStep 8: Walk one branch slowly Use k = 3, n = 9.\nStart:\npath = [] remain = 9 start = 1 Choose 1:\npath = [1] remain = 8 next start is 2 Choose 2:\npath = [1,2] remain = 6 next start is 3 Choose 6:\npath = [1,2,6] remain = 0 len(path) == 3 So [1,2,6] is collected.\nThis one branch already shows the full method:\nincreasing choices avoid duplicates remain tracks the target len(path) enforces the exact number count Assemble the Full Code Before the final submission-style version, here is the assembled standalone implementation:\nfrom typing import List def combination_sum_iii(k: int, n: int) -\u0026gt; List[List[int]]: res: List[List[int]] = [] path: List[int] = [] def dfs(start: int, remain: int) -\u0026gt; None: if len(path) == k: if remain == 0: res.append(path.copy()) return for x in range(start, 10): if x \u0026gt; remain: break path.append(x) dfs(x + 1, remain - x) path.pop() dfs(1, n) return res if __name__ == \u0026#34;__main__\u0026#34;: print(combination_sum_iii(3, 7)) print(combination_sum_iii(3, 9)) Reference Answer For LeetCode submission style:\nfrom typing import List class Solution: def combinationSum3(self, k: int, n: int) -\u0026gt; List[List[int]]: res: List[List[int]] = [] path: List[int] = [] def dfs(start: int, remain: int) -\u0026gt; None: if len(path) == k: if remain == 0: res.append(path.copy()) return for x in range(start, 10): if x \u0026gt; remain: break path.append(x) dfs(x + 1, remain - x) path.pop() dfs(1, n) return res What technique did we just build? Its formal name is:\nbacktracking fixed-length combination search use-once selection bounded search over 1..9 Again, do not start from the label.\nStart from the problem facts:\ntarget sum exact path length distinct increasing choices Then the template becomes obvious.\nE — Engineering Scenario 1: fixed-size budget package selection (Python) Background: an analytics workflow must choose exactly k one-time credits whose total cost equals a target.\nWhy it fits: every credit can be used once, the bundle size is fixed, and the sum must match exactly.\ndef choose_credits(k, target): res = [] path = [] def dfs(start, remain): if len(path) == k: if remain == 0: res.append(path[:]) return for x in range(start, 10): if x \u0026gt; remain: break path.append(x) dfs(x + 1, remain - x) path.pop() dfs(1, target) return res print(choose_credits(3, 9)) Scenario 2: exact-slot resource picks (Go) Background: a backend allocator must pick exactly k distinct resource IDs from a small bounded range and hit one target score.\nWhy it fits: the candidate set is tiny, each ID is used once, and the result size is fixed.\npackage main import \u0026#34;fmt\u0026#34; func chooseResources(k, target int) [][]int { res := make([][]int, 0) path := make([]int, 0) var dfs func(int, int) dfs = func(start, remain int) { if len(path) == k { if remain == 0 { res = append(res, append([]int(nil), path...)) } return } for x := start; x \u0026lt;= 9; x++ { if x \u0026gt; remain { break } path = append(path, x) dfs(x+1, remain-x) path = path[:len(path)-1] } } dfs(1, target) return res } func main() { fmt.Println(chooseResources(3, 7)) } Scenario 3: front-end fixed-chip planner (JavaScript) Background: a front-end planner lets the user pick exactly k chips, each with a unique weight from 1..9, to hit one score.\nWhy it fits: values are bounded, each chip is used once, and result length is fixed.\nfunction combinationSum3(k, n) { const res = []; const path = []; function dfs(start, remain) { if (path.length === k) { if (remain === 0) res.push([...path]); return; } for (let x = start; x \u0026lt;= 9; x += 1) { if (x \u0026gt; remain) break; path.push(x); dfs(x + 1, remain - x); path.pop(); } } dfs(1, n); return res; } console.log(combinationSum3(3, 9)); R — Reflection Correctness intuition This solution works because:\nthe search always moves from small to large numbers, so order-based duplicates never appear dfs(x + 1, ...) guarantees each number is used at most once remain always tracks the missing sum for the current path len(path) == k enforces the exact required size since the candidates are increasing, x \u0026gt; remain safely stops the rest of the layer Complexity The candidate set is fixed to 1..9, so the total search space is small.\nA useful way to describe it is:\nthe algorithm explores fixed-length combinations from at most 9 numbers a natural bound is O(C(9, k) * k) time, plus answer-copy costs recursion depth is O(k) In practice, this problem is much smaller than general combination-sum variants because:\nthe candidate domain is tiny values are unique the required length is fixed FAQ Why do we need len(path) == k if remain == 0 already means success? Because in this problem, matching the sum is not enough by itself.\nThe answer must also use exactly k numbers.\nWhy is there no duplicate-skip rule like in 40? Because the candidate values are already unique: 1,2,3,...,9.\nThere are no repeated input values to deduplicate.\nWhy does the recursive call use x + 1? Because each number may be used at most once, and later choices must stay strictly larger to preserve combination order.\nCommon mistakes collecting when remain == 0 without checking len(path) == k recursing with x instead of x + 1 treating the task as an unlimited-reuse problem like 39 forgetting that the candidate range is fixed and overcomplicating the state Best Practices When a problem says \u0026ldquo;choose exactly k numbers\u0026rdquo;, make the path length part of the completion rule immediately Keep the candidate range explicit; here 1..9 is not an implementation detail, it is part of the problem structure Compare 216 directly against 39 and 40 to understand which constraints create which code changes In bounded-range problems, use the order of candidates to justify pruning cleanly Further Reading Official problem: https://leetcode.com/problems/combination-sum-iii/ Direct comparisons: 39. Combination Sum, 40. Combination Sum II Related fixed-length combination problem: 77. Combinations S — Summary 216 is a fixed-length combination search, not just a target-sum search len(path) == k is part of the success condition, not a side check dfs(x + 1, remain - x) captures \u0026ldquo;use once\u0026rdquo; and \u0026ldquo;keep order\u0026rdquo; at the same time The bounded range 1..9 makes the problem smaller and the pruning cleaner Suggested next practice rewrite 39, 40, and 216 back to back and compare what changes in the recursive call and base case then move to 90. Subsets II or 77. Combinations to reinforce duplicate control and fixed-length thinking CTA Try explaining this problem without saying \u0026ldquo;it is a standard backtracking problem.\u0026rdquo;\nIf you can describe it as \u0026ldquo;pick exactly k increasing numbers from 1..9 so their sum becomes n\u0026rdquo;, then the code will usually fall into place naturally.\nMulti-Language Implementations Python from typing import List def combination_sum_iii(k: int, n: int) -\u0026gt; List[List[int]]: res: List[List[int]] = [] path: List[int] = [] def dfs(start: int, remain: int) -\u0026gt; None: if len(path) == k: if remain == 0: res.append(path.copy()) return for x in range(start, 10): if x \u0026gt; remain: break path.append(x) dfs(x + 1, remain - x) path.pop() dfs(1, n) return res C #include \u0026lt;stdlib.h\u0026gt; typedef struct { int** rows; int* col_sizes; int size; int capacity; } Result; static void push_result(Result* res, int* path, int path_size) { if (res-\u0026gt;size == res-\u0026gt;capacity) { res-\u0026gt;capacity *= 2; res-\u0026gt;rows = realloc(res-\u0026gt;rows, sizeof(int*) * res-\u0026gt;capacity); res-\u0026gt;col_sizes = realloc(res-\u0026gt;col_sizes, sizeof(int) * res-\u0026gt;capacity); } int* row = malloc(sizeof(int) * path_size); for (int i = 0; i \u0026lt; path_size; ++i) { row[i] = path[i]; } res-\u0026gt;rows[res-\u0026gt;size] = row; res-\u0026gt;col_sizes[res-\u0026gt;size] = path_size; res-\u0026gt;size += 1; } static void dfs(int k, int start, int remain, int* path, int path_size, Result* res) { if (path_size == k) { if (remain == 0) { push_result(res, path, path_size); } return; } for (int x = start; x \u0026lt;= 9; ++x) { if (x \u0026gt; remain) { break; } path[path_size] = x; dfs(k, x + 1, remain - x, path, path_size + 1, res); } } int** combinationSum3(int k, int n, int* returnSize, int** returnColumnSizes) { Result res = {0}; res.capacity = 16; res.rows = malloc(sizeof(int*) * res.capacity); res.col_sizes = malloc(sizeof(int) * res.capacity); int path[9]; dfs(k, 1, n, path, 0, \u0026amp;res); *returnSize = res.size; *returnColumnSizes = res.col_sizes; return res.rows; } C++ #include \u0026lt;vector\u0026gt; using namespace std; class Solution { public: vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; combinationSum3(int k, int n) { vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; res; vector\u0026lt;int\u0026gt; path; dfs(k, 1, n, path, res); return res; } private: void dfs(int k, int start, int remain, vector\u0026lt;int\u0026gt;\u0026amp; path, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; res) { if (static_cast\u0026lt;int\u0026gt;(path.size()) == k) { if (remain == 0) { res.push_back(path); } return; } for (int x = start; x \u0026lt;= 9; ++x) { if (x \u0026gt; remain) { break; } path.push_back(x); dfs(k, x + 1, remain - x, path, res); path.pop_back(); } } }; Go package main func combinationSum3(k int, n int) [][]int { res := make([][]int, 0) path := make([]int, 0) var dfs func(int, int) dfs = func(start, remain int) { if len(path) == k { if remain == 0 { res = append(res, append([]int(nil), path...)) } return } for x := start; x \u0026lt;= 9; x++ { if x \u0026gt; remain { break } path = append(path, x) dfs(x+1, remain-x) path = path[:len(path)-1] } } dfs(1, n) return res } Rust fn combination_sum_3(k: i32, n: i32) -\u0026gt; Vec\u0026lt;Vec\u0026lt;i32\u0026gt;\u0026gt; { fn dfs( k: usize, start: i32, remain: i32, path: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, res: \u0026amp;mut Vec\u0026lt;Vec\u0026lt;i32\u0026gt;\u0026gt;, ) { if path.len() == k { if remain == 0 { res.push(path.clone()); } return; } for x in start..=9 { if x \u0026gt; remain { break; } path.push(x); dfs(k, x + 1, remain - x, path, res); path.pop(); } } let mut res = Vec::new(); let mut path = Vec::new(); dfs(k as usize, 1, n, \u0026amp;mut path, \u0026amp;mut res); res } JavaScript function combinationSum3(k, n) { const res = []; const path = []; function dfs(start, remain) { if (path.length === k) { if (remain === 0) res.push([...path]); return; } for (let x = start; x \u0026lt;= 9; x += 1) { if (x \u0026gt; remain) break; path.push(x); dfs(x + 1, remain - x); path.pop(); } } dfs(1, n); return res; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/216-combination-sum-iii/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003e216. Combination Sum III\u003c/code\u003e is where backtracking adds one more important constraint: not only must the sum match, but the combination length must be exactly \u003ccode\u003ek\u003c/code\u003e. That turns the problem into a clean fixed-length combination search over the bounded range \u003ccode\u003e1..9\u003c/code\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003ebacktracking\u003c/code\u003e, \u003ccode\u003efixed length\u003c/code\u003e, \u003ccode\u003epruning\u003c/code\u003e, \u003ccode\u003ecombination search\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Combination Sum III, fixed-length backtracking, k numbers, pruning, LeetCode 216\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Build the stable solution for LeetCode 216 from scratch by understanding exact-length backtracking, sorted pruning, and the bounded candidate set 1..9.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eFind all valid combinations of \u003ccode\u003ek\u003c/code\u003e numbers that add up to \u003ccode\u003en\u003c/code\u003e, subject to these rules:\u003c/p\u003e","title":"LeetCode 216: Combination Sum III (Backtracking / Fixed-Length Search ACERS Guide)"},{"content":" Subtitle / Summary\nIf 39. Combination Sum teaches \u0026ldquo;reuse is allowed\u0026rdquo;, then 40. Combination Sum II teaches the next real upgrade: duplicate values exist, each number may be used at most once, and de-duplication must happen at the correct tree level.\nReading time: 14-16 min Tags: backtracking, dedup, pruning, combination search SEO keywords: Combination Sum II, backtracking, same-layer dedup, pruning, LeetCode 40 Meta description: Build the stable solution for LeetCode 40 from scratch by understanding sorted pruning, use-once recursion, and the same-layer duplicate skip rule. A — Algorithm Problem Restatement Given a collection of candidate numbers candidates and a target integer target,\nreturn all unique combinations where the chosen numbers sum to target.\nEach number in candidates may be used at most once in each combination.\nThe answer set must not contain duplicate combinations.\nInput / Output Name Type Description candidates int[] candidate values; duplicates may exist target int target sum return List[List[int]] all unique combinations summing to target Example 1 input: candidates = [10,1,2,7,6,1,5], target = 8 output: [ [1,1,6], [1,2,5], [1,7], [2,6] ] Example 2 input: candidates = [2,5,2,1,2], target = 5 output: [ [1,2,2], [5] ] Constraints 1 \u0026lt;= candidates.length \u0026lt;= 100 1 \u0026lt;= candidates[i] \u0026lt;= 50 1 \u0026lt;= target \u0026lt;= 30 Target Readers Learners who already finished 39. Combination Sum and now want to understand what changes when reuse is forbidden Readers who know backtracking syntax but still get duplicate handling wrong Developers who want one stable mental rule for \u0026ldquo;skip duplicates at the same layer\u0026rdquo; Background / Motivation This problem is the natural follow-up to 39 because it changes two rules at the same time:\nthe input itself may contain duplicate values each input position may be used only once That means a naive \u0026ldquo;just try everything\u0026rdquo; solution will often generate the same value-combination multiple times.\nFor example, if the sorted array starts with [1,1,2,...], then:\nchoosing the first 1 as the first element may lead to [1,2] choosing the second 1 as the first element may lead to the same [1,2] So the real question is not only \u0026ldquo;how do we search?\u0026rdquo;, but also:\nwhen is a duplicate branch genuinely redundant? when must we still allow the same value deeper in the tree? That distinction is the heart of this problem.\nCore Concepts path: the current combination being built remain: how much sum is still needed start / startIndex: the first index allowed in the current layer Use-once recursion: after choosing candidates[i], the next call must start from i + 1 Same-layer dedup: if i \u0026gt; start and candidates[i] == candidates[i - 1], skip it Sorted pruning: once a sorted value is greater than remain, later values also fail C — Concepts How To Build The Solution From Scratch Step 1: Start from the smallest example that reveals the duplicate problem Take candidates = [1,1,2] and target = 3.\nThe only valid value-combination is:\n[1,2] But if we treat the two 1s as independent first choices without any control, we can generate [1,2] twice:\nfirst branch starts from the first 1 second branch starts from the second 1 That already tells us:\nthis is still a combination problem, not a permutation problem duplicate values in the input can create duplicate branches in the search tree Step 2: What state must the partial answer remember? We are still building one combination gradually, so we need:\npath = [] path means:\nthe numbers already chosen on the current recursion branch not the full answer list Step 3: What is the smaller subproblem after a few choices are fixed? Just like 39, it is cleaner to carry the remaining target directly:\ndef dfs(start: int, remain: int) -\u0026gt; None: ... Here remain means:\nhow much the current path still needs to reach target what value shrinks after each choice Step 4: Why do we still need start? Because this is still a combination problem.\nIf we allow the next layer to go back to earlier indices, then order-based duplicates come back:\n[1,2] [2,1] So each layer must only enumerate from start to the right:\nfor i in range(start, len(candidates)): x = candidates[i] Step 5: What changes from 39 when one number may be used only once? This is the first major difference.\nAfter choosing candidates[i], the next layer must start at i + 1, not i.\npath.append(x) dfs(i + 1, remain - x) path.pop() That one line captures the \u0026ldquo;use each position at most once\u0026rdquo; rule.\nStep 6: Why sort first? Sorting gives us two benefits at once:\nequal values become adjacent, so duplicate skipping becomes easy pruning with break becomes safe candidates.sort() Without sorting:\nduplicate values are not adjacent if x \u0026gt; remain: break is not justified Step 7: How do we skip only the truly redundant duplicate branches? This is the key rule of the whole problem:\nif i \u0026gt; start and candidates[i] == candidates[i - 1]: continue Read it carefully:\ni \u0026gt; start means \u0026ldquo;this is not the first candidate tried in the current layer\u0026rdquo; candidates[i] == candidates[i - 1] means \u0026ldquo;this candidate has the same value as the previous one\u0026rdquo; So the rule says:\nif two equal values compete to be the same position in the combination, only try the first one but if one equal value was already chosen on a deeper branch, the next equal value may still be valid there That is why this is called same-layer dedup, not global dedup.\nStep 8: When is one branch complete, and when can we stop early? Completion is still:\nif remain == 0: res.append(path.copy()) return And pruning after sorting is:\nif x \u0026gt; remain: break This must be break, not continue, because later values are even larger.\nStep 9: Walk one branch and one skipped branch slowly Use the official first example after sorting:\n[1,1,2,5,6,7,10], target = 8 Start at layer 0:\nchoose index 0, value 1 later, when the loop reaches index 1, value 1, the condition i \u0026gt; start and candidates[i] == candidates[i - 1] is true so the second 1 is skipped at the same layer But inside the branch where the first 1 was already chosen:\npath = [1] next layer starts at index 1 now choosing the second 1 is valid, because it is not competing for the same tree layer anymore That is exactly how [1,1,6] remains allowed while duplicate top-level branches disappear.\nAssemble the Full Code Before the final submission-style solution, here is the assembled standalone version:\nfrom typing import List def combination_sum_ii(candidates: List[int], target: int) -\u0026gt; List[List[int]]: candidates.sort() res: List[List[int]] = [] path: List[int] = [] def dfs(start: int, remain: int) -\u0026gt; None: if remain == 0: res.append(path.copy()) return for i in range(start, len(candidates)): x = candidates[i] if i \u0026gt; start and x == candidates[i - 1]: continue if x \u0026gt; remain: break path.append(x) dfs(i + 1, remain - x) path.pop() dfs(0, target) return res if __name__ == \u0026#34;__main__\u0026#34;: print(combination_sum_ii([10, 1, 2, 7, 6, 1, 5], 8)) print(combination_sum_ii([2, 5, 2, 1, 2], 5)) Reference Answer For LeetCode submission style, the same logic becomes:\nfrom typing import List class Solution: def combinationSum2(self, candidates: List[int], target: int) -\u0026gt; List[List[int]]: candidates.sort() res: List[List[int]] = [] path: List[int] = [] def dfs(start: int, remain: int) -\u0026gt; None: if remain == 0: res.append(path.copy()) return for i in range(start, len(candidates)): x = candidates[i] if i \u0026gt; start and x == candidates[i - 1]: continue if x \u0026gt; remain: break path.append(x) dfs(i + 1, remain - x) path.pop() dfs(0, target) return res What technique did we just build? Its formal name is:\nbacktracking combination-style search same-layer duplicate skipping use-once candidate selection pruning after sorting But the order still matters:\nfirst observe the problem facts then decide the state and transitions only then attach the template label E — Engineering Scenario 1: physical coupon bundle selection (Python) Background: a finance tool has a list of coupon cards, and different cards may share the same face value.\nWhy it fits: each physical card can be used once, and value-duplicate bundles must not be reported twice.\ndef coupon_bundles(coupons, target): coupons.sort() res = [] path = [] def dfs(start, remain): if remain == 0: res.append(path[:]) return for i in range(start, len(coupons)): x = coupons[i] if i \u0026gt; start and x == coupons[i - 1]: continue if x \u0026gt; remain: break path.append(x) dfs(i + 1, remain - x) path.pop() dfs(0, target) return res print(coupon_bundles([10, 1, 2, 7, 6, 1, 5], 8)) Scenario 2: one-time inventory lot assembly (Go) Background: a backend service must choose physical lots to fill one target amount. Some lots share the same size.\nWhy it fits: each lot can be consumed once, but equal-value lots should not create duplicate result bundles.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sort\u0026#34; ) func assemble(lots []int, target int) [][]int { sort.Ints(lots) res := make([][]int, 0) path := make([]int, 0) var dfs func(int, int) dfs = func(start, remain int) { if remain == 0 { res = append(res, append([]int(nil), path...)) return } for i := start; i \u0026lt; len(lots); i++ { x := lots[i] if i \u0026gt; start \u0026amp;\u0026amp; lots[i] == lots[i-1] { continue } if x \u0026gt; remain { break } path = append(path, x) dfs(i+1, remain-x) path = path[:len(path)-1] } } dfs(0, target) return res } func main() { fmt.Println(assemble([]int{2, 5, 2, 1, 2}, 5)) } Scenario 3: duplicate-priced option planner (JavaScript) Background: a frontend configurator lists option prices, and different options may share the same price.\nWhy it fits: each option may be selected once, and the UI should not show the same price-bundle twice.\nfunction combinationSum2(candidates, target) { candidates.sort((a, b) =\u0026gt; a - b); const res = []; const path = []; function dfs(start, remain) { if (remain === 0) { res.push([...path]); return; } for (let i = start; i \u0026lt; candidates.length; i += 1) { const x = candidates[i]; if (i \u0026gt; start \u0026amp;\u0026amp; candidates[i] === candidates[i - 1]) continue; if (x \u0026gt; remain) break; path.push(x); dfs(i + 1, remain - x); path.pop(); } } dfs(0, target); return res; } console.log(combinationSum2([2, 5, 2, 1, 2], 5)); R — Reflection Correctness intuition This solution works because the following invariants stay true:\npath always uses indices in increasing order, so order-based duplicates never appear remain always equals the missing sum needed by the current path dfs(i + 1, ...) ensures each input position is used at most once the same-layer skip rule prevents equal values from starting redundant sibling branches after sorting, x \u0026gt; remain safely ends the rest of the layer Complexity Let n = len(candidates).\nSorting costs O(n log n) The search tree is subset-like because each position is used at most once A standard loose bound is O(2^n * n) time, where the extra n comes from copying paths into the answer Extra recursion space is O(n), excluding the output itself Actual runtime depends strongly on:\nhow effective duplicate skipping is how often sorted pruning stops branches early how many valid combinations exist FAQ Why do we recurse with i + 1 instead of i? Because each input position may be used at most once.\nUsing i would incorrectly allow reuse and turn the logic back toward 39. Combination Sum.\nWhy is the duplicate rule i \u0026gt; start, not i \u0026gt; 0? Because dedup is only supposed to remove equal candidates that compete in the same layer.\nIf you write i \u0026gt; 0, you may accidentally suppress valid deeper choices such as the second 1 inside [1,1,6].\nWhy must we sort first? Because sorting is what makes both of these rules valid:\nif i \u0026gt; start and candidates[i] == candidates[i - 1]: continue if x \u0026gt; remain: break Without sorting, equal values are not adjacent and later values are not guaranteed to be larger.\nCommon mistakes recursing with i and accidentally allowing reuse writing the skip rule as if i \u0026gt; 0 and ..., which removes valid answers forgetting to sort before dedup and pruning appending path directly instead of a copy Best Practices Compare this problem directly against 39, and make the index difference explicit Say out loud whether a duplicate rule is \u0026ldquo;same-layer\u0026rdquo; or \u0026ldquo;global\u0026rdquo; before coding it Sort first whenever your pruning or dedup logic depends on order Keep path, remain, and start meanings stable across all backtracking problems Further Reading Official problem: https://leetcode.com/problems/combination-sum-ii/ Direct predecessor: 39. Combination Sum Good next step: 216. Combination Sum III Related duplicate-control problem: 90. Subsets II S — Summary The real upgrade from 39 to 40 is not just \u0026ldquo;use once\u0026rdquo;; it is \u0026ldquo;use once plus correct duplicate control\u0026rdquo; dfs(i + 1, remain - x) is the code-level expression of \u0026ldquo;each position at most once\u0026rdquo; if i \u0026gt; start and candidates[i] == candidates[i - 1]: continue removes only redundant sibling branches Sorting is the foundation for both dedup and pruning Suggested next practice redo 39 and 40 side by side to internalize the difference between i and i + 1 then solve 216 to add one more constraint: fixed path length k CTA Do not just memorize the skip rule.\nTake [1,1,2], draw the first two layers of the search tree by hand, and explain why one duplicate 1 must be skipped at the same layer but still allowed on a deeper branch.\nMulti-Language Implementations Python from typing import List def combination_sum_ii(candidates: List[int], target: int) -\u0026gt; List[List[int]]: candidates.sort() res: List[List[int]] = [] path: List[int] = [] def dfs(start: int, remain: int) -\u0026gt; None: if remain == 0: res.append(path.copy()) return for i in range(start, len(candidates)): x = candidates[i] if i \u0026gt; start and x == candidates[i - 1]: continue if x \u0026gt; remain: break path.append(x) dfs(i + 1, remain - x) path.pop() dfs(0, target) return res C #include \u0026lt;stdlib.h\u0026gt; typedef struct { int** rows; int* col_sizes; int size; int capacity; } Result; static int cmp_int(const void* a, const void* b) { return (*(const int*)a) - (*(const int*)b); } static void push_result(Result* res, int* path, int path_size) { if (res-\u0026gt;size == res-\u0026gt;capacity) { res-\u0026gt;capacity *= 2; res-\u0026gt;rows = realloc(res-\u0026gt;rows, sizeof(int*) * res-\u0026gt;capacity); res-\u0026gt;col_sizes = realloc(res-\u0026gt;col_sizes, sizeof(int) * res-\u0026gt;capacity); } int* row = malloc(sizeof(int) * path_size); for (int i = 0; i \u0026lt; path_size; ++i) { row[i] = path[i]; } res-\u0026gt;rows[res-\u0026gt;size] = row; res-\u0026gt;col_sizes[res-\u0026gt;size] = path_size; res-\u0026gt;size += 1; } static void dfs(int* candidates, int candidates_size, int start, int remain, int* path, int path_size, Result* res) { if (remain == 0) { push_result(res, path, path_size); return; } for (int i = start; i \u0026lt; candidates_size; ++i) { int x = candidates[i]; if (i \u0026gt; start \u0026amp;\u0026amp; candidates[i] == candidates[i - 1]) { continue; } if (x \u0026gt; remain) { break; } path[path_size] = x; dfs(candidates, candidates_size, i + 1, remain - x, path, path_size + 1, res); } } int** combinationSum2(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes) { qsort(candidates, candidatesSize, sizeof(int), cmp_int); Result res = {0}; res.capacity = 16; res.rows = malloc(sizeof(int*) * res.capacity); res.col_sizes = malloc(sizeof(int) * res.capacity); int* path = malloc(sizeof(int) * candidatesSize); dfs(candidates, candidatesSize, 0, target, path, 0, \u0026amp;res); free(path); *returnSize = res.size; *returnColumnSizes = res.col_sizes; return res.rows; } C++ #include \u0026lt;algorithm\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; class Solution { public: vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; combinationSum2(vector\u0026lt;int\u0026gt;\u0026amp; candidates, int target) { sort(candidates.begin(), candidates.end()); vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; res; vector\u0026lt;int\u0026gt; path; dfs(candidates, 0, target, path, res); return res; } private: void dfs(const vector\u0026lt;int\u0026gt;\u0026amp; candidates, int start, int remain, vector\u0026lt;int\u0026gt;\u0026amp; path, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; res) { if (remain == 0) { res.push_back(path); return; } for (int i = start; i \u0026lt; static_cast\u0026lt;int\u0026gt;(candidates.size()); ++i) { int x = candidates[i]; if (i \u0026gt; start \u0026amp;\u0026amp; candidates[i] == candidates[i - 1]) { continue; } if (x \u0026gt; remain) { break; } path.push_back(x); dfs(candidates, i + 1, remain - x, path, res); path.pop_back(); } } }; Go package main import \u0026#34;sort\u0026#34; func combinationSum2(candidates []int, target int) [][]int { sort.Ints(candidates) res := make([][]int, 0) path := make([]int, 0) var dfs func(int, int) dfs = func(start, remain int) { if remain == 0 { res = append(res, append([]int(nil), path...)) return } for i := start; i \u0026lt; len(candidates); i++ { x := candidates[i] if i \u0026gt; start \u0026amp;\u0026amp; candidates[i] == candidates[i-1] { continue } if x \u0026gt; remain { break } path = append(path, x) dfs(i+1, remain-x) path = path[:len(path)-1] } } dfs(0, target) return res } Rust fn combination_sum_2(mut candidates: Vec\u0026lt;i32\u0026gt;, target: i32) -\u0026gt; Vec\u0026lt;Vec\u0026lt;i32\u0026gt;\u0026gt; { fn dfs( candidates: \u0026amp;[i32], start: usize, remain: i32, path: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, res: \u0026amp;mut Vec\u0026lt;Vec\u0026lt;i32\u0026gt;\u0026gt;, ) { if remain == 0 { res.push(path.clone()); return; } for i in start..candidates.len() { let x = candidates[i]; if i \u0026gt; start \u0026amp;\u0026amp; candidates[i] == candidates[i - 1] { continue; } if x \u0026gt; remain { break; } path.push(x); dfs(candidates, i + 1, remain - x, path, res); path.pop(); } } candidates.sort_unstable(); let mut res = Vec::new(); let mut path = Vec::new(); dfs(\u0026amp;candidates, 0, target, \u0026amp;mut path, \u0026amp;mut res); res } JavaScript function combinationSum2(candidates, target) { candidates.sort((a, b) =\u0026gt; a - b); const res = []; const path = []; function dfs(start, remain) { if (remain === 0) { res.push([...path]); return; } for (let i = start; i \u0026lt; candidates.length; i += 1) { const x = candidates[i]; if (i \u0026gt; start \u0026amp;\u0026amp; candidates[i] === candidates[i - 1]) continue; if (x \u0026gt; remain) break; path.push(x); dfs(i + 1, remain - x); path.pop(); } } dfs(0, target); return res; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/40-combination-sum-ii/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nIf \u003ccode\u003e39. Combination Sum\u003c/code\u003e teaches \u0026ldquo;reuse is allowed\u0026rdquo;, then \u003ccode\u003e40. Combination Sum II\u003c/code\u003e teaches the next real upgrade: duplicate values exist, each number may be used at most once, and de-duplication must happen at the correct tree level.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 14-16 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003ebacktracking\u003c/code\u003e, \u003ccode\u003ededup\u003c/code\u003e, \u003ccode\u003epruning\u003c/code\u003e, \u003ccode\u003ecombination search\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Combination Sum II, backtracking, same-layer dedup, pruning, LeetCode 40\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Build the stable solution for LeetCode 40 from scratch by understanding sorted pruning, use-once recursion, and the same-layer duplicate skip rule.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven a collection of candidate numbers \u003ccode\u003ecandidates\u003c/code\u003e and a target integer \u003ccode\u003etarget\u003c/code\u003e,\u003cbr\u003e\nreturn all unique combinations where the chosen numbers sum to \u003ccode\u003etarget\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 40: Combination Sum II (Backtracking / Same-Layer Dedup ACERS Guide)"},{"content":" Subtitle / Summary\nCombination Sum is the first Hot100 backtracking problem that really mixes three ideas at once: combination-style search, a running target, and safe pruning after sorting. The point is not to jump straight to the template, but to build it step by step from the problem itself.\nReading time: 14-16 min Tags: Hot100, backtracking, combination sum, pruning SEO keywords: Combination Sum, backtracking, pruning, remain, DFS Meta description: Learn LeetCode 39 by building the solution from scratch, using path, remain, repeated candidate reuse, and sorted pruning. A — Algorithm Problem Restatement Given an array of distinct integers candidates and a target integer target,\nreturn a list of all unique combinations of candidates where the chosen numbers sum to target.\nThe same number may be chosen from candidates an unlimited number of times.\nTwo combinations are different if the frequency of at least one chosen number differs.\nInput / Output Name Type Description candidates int[] distinct candidate values target int target sum return List[List[int]] all unique combinations summing to target Example 1 input: candidates = [2,3,6,7], target = 7 output: [[2,2,3],[7]] Example 2 input: candidates = [2,3,5], target = 8 output: [[2,2,2,2],[2,3,3],[3,5]] Example 3 input: candidates = [2], target = 1 output: [] Constraints 1 \u0026lt;= candidates.length \u0026lt;= 30 2 \u0026lt;= candidates[i] \u0026lt;= 40 all elements of candidates are distinct 1 \u0026lt;= target \u0026lt;= 40 the number of unique combinations is guaranteed to be less than 150 Target Readers Hot100 learners who already finished 78. Subsets and now want the next backtracking upgrade Developers who understand recursion in general but still get confused when one candidate may be reused Readers who want a stable template they can transfer to 40 and 216, not just one memorized answer Background / Motivation 39. Combination Sum is a good second backtracking problem because it forces you to manage three things at the same time:\nit is still a combination problem, so order must not create duplicates the same number may be chosen multiple times the target value gives us a natural pruning boundary That combination is exactly where many learners stop “following the pattern” and start needing a real model.\nThe model we want is:\nwhat partial answer are we building how far are we from the target where may the next layer continue when can a whole suffix of the search be cut off immediately Once that model is stable, Combination Sum stops feeling special and starts feeling reusable.\nCore Concepts path: the numbers currently chosen in the active combination remain: how much is still needed to reach target start / startIndex: the first index allowed in the current layer Reusable candidates: after choosing candidates[i], the next call still starts from i Sorted pruning: once a sorted value is greater than remain, later values are impossible too C — Concepts How To Build The Solution From Scratch Step 1: Start from a tiny but non-trivial example Take candidates = [2,3,6,7] and target = 7.\nInstead of asking “what is the final algorithm?”, ask the smaller question:\nif I choose 2 first, I still need 5 if I choose 2 again, I still need 3 if I then choose 3, I hit the target and get [2,2,3] if I choose 7 first, I also get one full answer: [7] This tiny example already reveals two facts:\nthe problem is about combinations, not permutations we need to keep track of how much is still missing It also shows why a stable boundary is necessary:\n[2,2,3] and [2,3,2] must not both appear once a candidate is already larger than the remaining target, later candidates are useless too after sorting Step 2: What must the partial answer remember? If we are building one combination gradually, we need to store the numbers already chosen.\nThat is why we need path.\npath = [] path is not the full answer set.\nIt is only the current branch of the search tree.\nStep 3: How do we know how far we still are from the target? We could recompute sum(path) every time, but that is a worse teaching model and a worse pruning model.\nIt is cleaner to carry the remaining amount directly.\ndef dfs(start: int, remain: int) -\u0026gt; None: ... Here remain means:\nhow much the current path still needs what value will shrink after each choice Step 4: How do we prevent order-based duplicates? Because this is a combination problem, [2,2,3] and [2,3,2] must not both appear.\nThe stable way to control that is: in each layer, only enumerate from start to the right.\nfor i in range(start, len(candidates)): x = candidates[i] That rule means:\ndo not go back to earlier indices build combinations in non-decreasing order Step 5: When is one branch complete? When remain == 0, the current path sums exactly to target.\nThat is when we collect one answer.\nif remain == 0: res.append(path.copy()) return The .copy() matters because path will keep changing during backtracking.\nStep 6: Why sort first? Sorting is not needed for de-duplication here.\nIt is needed so pruning becomes safe.\ncandidates.sort() Once the array is sorted, “current value too large” implies “every later value is too large too.”\nStep 7: What do we do when the current value is already too large? After sorting, if x \u0026gt; remain, there is no reason to continue scanning the rest of the layer.\nif x \u0026gt; remain: break This must be break, not continue.\nLater values are even larger.\nStep 8: What changes after we choose one number? After choosing x, we do three things:\nadd it to path solve the smaller problem undo the choice afterward path.append(x) dfs(i, remain - x) path.pop() The most important detail is the recursive call:\ndfs(i, remain - x) means the same candidate may still be reused dfs(i + 1, remain - x) would incorrectly turn this into a “use once” problem Step 9: Walk one branch slowly Use the same example: candidates = [2,3,6,7], target = 7.\nStart:\npath = [] remain = 7 start = 0 Choose 2:\npath = [2] remain = 5 the next layer still starts at index 0 Choose 2 again:\npath = [2,2] remain = 3 Now inside that call:\ntry 2 first and get remain = 1 at that point the next candidate is already larger than 1, so the layer breaks backtrack to path = [2,2] Then try 3:\npath = [2,2,3] remain = 0 collect [2,2,3] That single branch shows the whole method:\nwhy remain is useful why the next call keeps i why sorting makes break safe Assemble the Full Code Now we combine the fragments above into the first complete working solution.\nThis version is runnable as a normal script.\nfrom typing import List def combination_sum(candidates: List[int], target: int) -\u0026gt; List[List[int]]: candidates.sort() res: List[List[int]] = [] path: List[int] = [] def dfs(start: int, remain: int) -\u0026gt; None: if remain == 0: res.append(path.copy()) return for i in range(start, len(candidates)): x = candidates[i] if x \u0026gt; remain: break path.append(x) dfs(i, remain - x) path.pop() dfs(0, target) return res if __name__ == \u0026#34;__main__\u0026#34;: print(combination_sum([2, 3, 6, 7], 7)) print(combination_sum([2, 3, 5], 8)) Reference Answer For LeetCode submission style, the same logic becomes:\nfrom typing import List class Solution: def combinationSum(self, candidates: List[int], target: int) -\u0026gt; List[List[int]]: candidates.sort() res: List[List[int]] = [] path: List[int] = [] def dfs(start: int, remain: int) -\u0026gt; None: if remain == 0: res.append(path.copy()) return for i in range(start, len(candidates)): x = candidates[i] if x \u0026gt; remain: break path.append(x) dfs(i, remain - x) path.pop() dfs(0, target) return res What technique did we just build? Its formal name is:\nbacktracking combination-style search pruning after sorting reusable candidate selection But the order matters.\nYou should not start from the label.\nYou should start from the problem facts and let those facts force the state and the transitions into place.\nE — Engineering Scenario 1: exact budget combinations (Python) Background: a budgeting tool wants every combination of allowed amounts that exactly reaches one budget.\nWhy it fits: values may be reused, order does not matter, and the total must match exactly.\ndef fill_budget(costs, target): costs = sorted(costs) res = [] path = [] def dfs(start, remain): if remain == 0: res.append(path[:]) return for i in range(start, len(costs)): if costs[i] \u0026gt; remain: break path.append(costs[i]) dfs(i, remain - costs[i]) path.pop() dfs(0, target) return res print(fill_budget([2, 3, 5], 8)) Scenario 2: capacity pack assembly (Go) Background: a backend system wants all ways to assemble a target capacity from reusable pack sizes.\nWhy it fits: pack sizes can repeat, order is irrelevant, and the exact total matters.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sort\u0026#34; ) func fill(capacities []int, target int) [][]int { sort.Ints(capacities) res := make([][]int, 0) path := make([]int, 0) var dfs func(int, int) dfs = func(start, remain int) { if remain == 0 { res = append(res, append([]int(nil), path...)) return } for i := start; i \u0026lt; len(capacities); i++ { if capacities[i] \u0026gt; remain { break } path = append(path, capacities[i]) dfs(i, remain-capacities[i]) path = path[:len(path)-1] } } dfs(0, target) return res } func main() { fmt.Println(fill([]int{2, 3, 5}, 8)) } Scenario 3: pricing bundle builder (JavaScript) Background: a frontend bundle configurator wants every pricing bundle that lands exactly on the user budget.\nWhy it fits: prices may be reused and bundle order is irrelevant.\nfunction combinationSum(candidates, target) { candidates.sort((a, b) =\u0026gt; a - b); const res = []; const path = []; function dfs(start, remain) { if (remain === 0) { res.push([...path]); return; } for (let i = start; i \u0026lt; candidates.length; i += 1) { const x = candidates[i]; if (x \u0026gt; remain) break; path.push(x); dfs(i, remain - x); path.pop(); } } dfs(0, target); return res; } console.log(combinationSum([2, 3, 5], 8)); R — Reflection Correctness intuition This solution works because three invariants stay true throughout the search:\nthe numbers in path are built in non-decreasing index order, so order-based duplicates never appear remain always means “how much is still needed,” so remain == 0 is exactly the success condition after sorting, if x \u0026gt; remain, every later candidate also fails, so breaking the loop is safe Complexity Let:\nn = len(candidates) m = min(candidates) d = target / m Then the recursion depth is at most d, because every step reduces remain by at least m.\nA standard loose upper bound is:\nTime complexity: O(n^d), where d = target / min(candidates) Extra recursion space: O(d), excluding the output itself This problem does not have a neat fixed closed-form count like 2^n or n!.\nActual runtime depends heavily on pruning and on how many valid combinations exist.\nFAQ Why do we recurse with i instead of i + 1? Because the current candidate may still be reused.\nUsing i + 1 would incorrectly change the problem into a “choose each number at most once” variant.\nWhy can we break instead of continue? Because the array is sorted.\nIf the current value is already too large, later values are even larger.\nWhy not keep computing sum(path)? You can, but remain is a cleaner teaching and coding model:\ncompletion is remain == 0 pruning is x \u0026gt; remain there is no repeated summation work in the recursion Common mistakes using if x \u0026gt; remain: break without sorting first appending path directly instead of a copy recursing with i + 1 and accidentally solving a different problem restarting every layer from index 0 and turning combinations into permutation-like duplicates Best Practices Before coding, say out loud what path, remain, and start each mean In “reuse allowed” problems, check the recursive index before you trust the solution Only use break pruning after sorting is done If you just studied 78. Subsets, rewrite this one side by side and compare the role of start Further Reading Official problem: https://leetcode.cn/problems/combination-sum/ Recommended next problem: 40. Combination Sum II Good comparison problems: 78. Subsets, 46. Permutations S — Summary This is still combination-style backtracking, not permutation-style backtracking remain makes both completion and pruning conditions explicit dfs(i, remain - x) is the concrete expression of “this candidate may be reused” sorting plus if x \u0026gt; remain: break is the most stable pruning rule in this problem Suggested Next Problems 78. Subsets: review why startIndex removes order duplicates 46. Permutations: compare combination-style and permutation-style backtracking 40. Combination Sum II: learn “no reuse” plus duplicate handling 216. Combination Sum III: keep practicing target-constrained search CTA If you study only one backtracking problem today, rewrite this one once from a blank page.\nDo not start from the final code. Start by forcing yourself to explain why you need path, remain, start, and sorted pruning.\nMulti-Language Implementations Python from typing import List def combination_sum(candidates: List[int], target: int) -\u0026gt; List[List[int]]: candidates.sort() res: List[List[int]] = [] path: List[int] = [] def dfs(start: int, remain: int) -\u0026gt; None: if remain == 0: res.append(path.copy()) return for i in range(start, len(candidates)): x = candidates[i] if x \u0026gt; remain: break path.append(x) dfs(i, remain - x) path.pop() dfs(0, target) return res C #include \u0026lt;stdlib.h\u0026gt; typedef struct { int** rows; int* col_sizes; int size; int capacity; } Result; static int cmp_int(const void* a, const void* b) { return (*(const int*)a) - (*(const int*)b); } static void push_result(Result* res, int* path, int path_size) { if (res-\u0026gt;size == res-\u0026gt;capacity) { res-\u0026gt;capacity *= 2; res-\u0026gt;rows = realloc(res-\u0026gt;rows, sizeof(int*) * res-\u0026gt;capacity); res-\u0026gt;col_sizes = realloc(res-\u0026gt;col_sizes, sizeof(int) * res-\u0026gt;capacity); } int* row = malloc(sizeof(int) * path_size); for (int i = 0; i \u0026lt; path_size; ++i) { row[i] = path[i]; } res-\u0026gt;rows[res-\u0026gt;size] = row; res-\u0026gt;col_sizes[res-\u0026gt;size] = path_size; res-\u0026gt;size += 1; } static void dfs(int* candidates, int candidates_size, int start, int remain, int* path, int path_size, Result* res) { if (remain == 0) { push_result(res, path, path_size); return; } for (int i = start; i \u0026lt; candidates_size; ++i) { int x = candidates[i]; if (x \u0026gt; remain) { break; } path[path_size] = x; dfs(candidates, candidates_size, i, remain - x, path, path_size + 1, res); } } int** combinationSum(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes) { qsort(candidates, candidatesSize, sizeof(int), cmp_int); Result res = {0}; res.capacity = 16; res.rows = malloc(sizeof(int*) * res.capacity); res.col_sizes = malloc(sizeof(int) * res.capacity); int* path = malloc(sizeof(int) * (target + 1)); dfs(candidates, candidatesSize, 0, target, path, 0, \u0026amp;res); free(path); *returnSize = res.size; *returnColumnSizes = res.col_sizes; return res.rows; } C++ #include \u0026lt;algorithm\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; class Solution { public: vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; combinationSum(vector\u0026lt;int\u0026gt;\u0026amp; candidates, int target) { sort(candidates.begin(), candidates.end()); vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; res; vector\u0026lt;int\u0026gt; path; dfs(candidates, 0, target, path, res); return res; } private: void dfs(const vector\u0026lt;int\u0026gt;\u0026amp; candidates, int start, int remain, vector\u0026lt;int\u0026gt;\u0026amp; path, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; res) { if (remain == 0) { res.push_back(path); return; } for (int i = start; i \u0026lt; static_cast\u0026lt;int\u0026gt;(candidates.size()); ++i) { int x = candidates[i]; if (x \u0026gt; remain) { break; } path.push_back(x); dfs(candidates, i, remain - x, path, res); path.pop_back(); } } }; Go package main import \u0026#34;sort\u0026#34; func combinationSum(candidates []int, target int) [][]int { sort.Ints(candidates) res := make([][]int, 0) path := make([]int, 0) var dfs func(int, int) dfs = func(start, remain int) { if remain == 0 { res = append(res, append([]int(nil), path...)) return } for i := start; i \u0026lt; len(candidates); i++ { x := candidates[i] if x \u0026gt; remain { break } path = append(path, x) dfs(i, remain-x) path = path[:len(path)-1] } } dfs(0, target) return res } Rust fn combination_sum(mut candidates: Vec\u0026lt;i32\u0026gt;, target: i32) -\u0026gt; Vec\u0026lt;Vec\u0026lt;i32\u0026gt;\u0026gt; { fn dfs( candidates: \u0026amp;[i32], start: usize, remain: i32, path: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, res: \u0026amp;mut Vec\u0026lt;Vec\u0026lt;i32\u0026gt;\u0026gt;, ) { if remain == 0 { res.push(path.clone()); return; } for i in start..candidates.len() { let x = candidates[i]; if x \u0026gt; remain { break; } path.push(x); dfs(candidates, i, remain - x, path, res); path.pop(); } } candidates.sort_unstable(); let mut res = Vec::new(); let mut path = Vec::new(); dfs(\u0026amp;candidates, 0, target, \u0026amp;mut path, \u0026amp;mut res); res } JavaScript function combinationSum(candidates, target) { candidates.sort((a, b) =\u0026gt; a - b); const res = []; const path = []; function dfs(start, remain) { if (remain === 0) { res.push([...path]); return; } for (let i = start; i \u0026lt; candidates.length; i += 1) { const x = candidates[i]; if (x \u0026gt; remain) break; path.push(x); dfs(i, remain - x); path.pop(); } } dfs(0, target); return res; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/backtracking/39-combination-sum/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nCombination Sum is the first Hot100 backtracking problem that really mixes three ideas at once: combination-style search, a running target, and safe pruning after sorting. The point is not to jump straight to the template, but to build it step by step from the problem itself.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 14-16 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebacktracking\u003c/code\u003e, \u003ccode\u003ecombination sum\u003c/code\u003e, \u003ccode\u003epruning\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Combination Sum, backtracking, pruning, remain, DFS\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn LeetCode 39 by building the solution from scratch, using \u003ccode\u003epath\u003c/code\u003e, \u003ccode\u003eremain\u003c/code\u003e, repeated candidate reuse, and sorted pruning.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven an array of distinct integers \u003ccode\u003ecandidates\u003c/code\u003e and a target integer \u003ccode\u003etarget\u003c/code\u003e,\u003cbr\u003e\nreturn a list of all unique combinations of \u003ccode\u003ecandidates\u003c/code\u003e where the chosen numbers sum to \u003ccode\u003etarget\u003c/code\u003e.\u003c/p\u003e","title":"Hot100: Combination Sum (Backtracking / Pruning ACERS Guide)"},{"content":" Subtitle / Summary If Subsets teaches the skeleton of combination-style backtracking, Permutations teaches the core of state-based backtracking: at each position, choose one unused element, continue until the path length reaches n, and only then collect the answer.\nReading time: 10-12 min Tags: Hot100, backtracking, permutations, DFS SEO keywords: Permutations, backtracking, used array, DFS, LeetCode 46 Meta description: Learn the stable permutation backtracking template for LeetCode 46, with state recovery, engineering analogies, and runnable multi-language solutions. A — Algorithm Problem Restatement Given an array nums of distinct integers, return all possible permutations. The answer may be returned in any order.\nInput / Output Name Type Description nums int[] array of distinct integers return List[List[int]] all possible permutations Example 1 input: nums = [1,2,3] output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] Example 2 input: nums = [0,1] output: [[0,1],[1,0]] Example 3 input: nums = [1] output: [[1]] Constraints 1 \u0026lt;= nums.length \u0026lt;= 6 -10 \u0026lt;= nums[i] \u0026lt;= 10 all integers in nums are distinct Target Readers Hot100 learners who have already finished 78. Subsets and want the next backtracking template Developers who understand recursion but still make mistakes when restoring state Engineers who need to enumerate execution orders, test sequences, or ordering-sensitive plans Background / Motivation The key difference between combinations and permutations is simple:\ncombinations care about which elements are chosen permutations also care about the order of those elements So in this problem, [1,2,3] and [1,3,2] are different valid answers. That immediately changes the template:\nstartIndex is no longer enough every layer must be able to consider all positions again we need explicit state to record which elements are already used That is exactly why LeetCode 46 is a foundational backtracking problem. It forces you to reason clearly about state selection and state recovery.\nCore Concepts path: the permutation currently being built used[i]: whether nums[i] has already been placed in the current path Leaf-only collection: only when path.length == nums.length do we have a full permutation State recovery: on return, both path and used[i] must be restored C — Concepts How To Build The Solution From Scratch Step 1: Start from a tiny example Take nums = [1, 2, 3].\nInstead of asking \u0026ldquo;how do I generate all permutations?\u0026rdquo;, ask the smaller question:\nHow do I build one permutation one slot at a time?\nThat shift makes the recursion much easier to design.\nStep 2: Decide what state a partial answer needs While building one permutation, we need three pieces of state:\npath: the numbers already chosen used[i]: whether nums[i] is already inside path res: where to store finished permutations So the setup is:\nres = [] path = [] used = [False] * len(nums) At the beginning:\npath = [] every element in used is False That means we have not chosen anything yet.\nStep 3: Define the recursive subproblem The recursive question is:\nGiven the current partial permutation in path, what unused number can I place next?\nThat becomes a DFS function:\ndef dfs() -\u0026gt; None: ... It can directly read and modify res, path, and used.\nStep 4: Define the stopping condition When is one permutation complete?\nWhen path has the same length as nums.\nif len(path) == len(nums): res.append(path.copy()) return Two details matter here:\nwe collect only at leaf nodes, because only then is the permutation complete we must append path.copy() instead of path, because path will keep changing during backtracking Step 5: List the available choices At any point, the next number can be any element that is not already used.\nfor i, x in enumerate(nums): if used[i]: continue So every DFS level scans the full array, but only unused numbers are eligible.\nStep 6: Make one choice If x is unused, choose it by updating the state:\nused[i] = True path.append(x) Now the partial permutation is one element longer.\nStep 7: Recurse on the smaller problem After choosing the next number, solve the rest of the problem:\nFill the remaining positions.\ndfs() Step 8: Undo the choice After exploring every permutation that starts with that choice, restore the old state so the loop can try the next option.\npath.pop() used[i] = False This is the key backtracking pattern:\nchoose recurse undo Step 9: Walk one branch slowly For nums = [1, 2, 3]:\nStart:\npath = [] used = [False, False, False] Choose 1:\npath = [1] used = [True, False, False] Choose 2:\npath = [1, 2] used = [True, True, False] Choose 3:\npath = [1, 2, 3] used = [True, True, True] Now len(path) == len(nums), so save [1, 2, 3].\nThen backtrack:\nremove 3 mark 3 unused Return to path = [1, 2] and try other choices. There are none, so backtrack again:\nremove 2 mark 2 unused Now we are back at path = [1], and can try 3 instead. That produces [1, 3, 2].\nThe full search keeps repeating that same pattern until every branch has been explored.\nAssemble the Full Code Now combine the fragments above into the first complete working solution.\nfrom typing import List def permute(nums: List[int]) -\u0026gt; List[List[int]]: res: List[List[int]] = [] path: List[int] = [] used = [False] * len(nums) def dfs() -\u0026gt; None: if len(path) == len(nums): res.append(path.copy()) return for i, x in enumerate(nums): if used[i]: continue used[i] = True path.append(x) dfs() path.pop() used[i] = False dfs() return res Reference Answer For LeetCode submission style, the same logic becomes:\nfrom typing import List class Solution: def permute(self, nums: List[int]) -\u0026gt; List[List[int]]: res: List[List[int]] = [] path: List[int] = [] used = [False] * len(nums) def dfs() -\u0026gt; None: if len(path) == len(nums): res.append(path.copy()) return for i, x in enumerate(nums): if used[i]: continue used[i] = True path.append(x) dfs() path.pop() used[i] = False dfs() return res What mental model should stick? If you want to derive this from scratch during an interview or while coding, ask:\nCan I build the answer one position at a time? What does the partial answer look like? When is the partial answer complete? What choices are available next? What state must I undo before trying the next choice? For permutations, the answers are:\nyes, fill one slot at a time the partial answer is path it is complete when len(path) == len(nums) the next choice is any unused number after recursion, undo with both path.pop() and used[i] = False That is the full model. The code is only a direct translation of that reasoning.\nE — Engineering Scenario 1: task execution order enumeration (Python) Background: an offline scheduler wants to compare how different task orders affect the final result. Why it fits: when order changes behavior, the search space is permutation-shaped.\ndef orders(tasks): if not tasks: return [[]] res = [] for i, task in enumerate(tasks): for rest in orders(tasks[:i] + tasks[i + 1:]): res.append([task] + rest) return res print(orders([\u0026#34;fetch\u0026#34;, \u0026#34;score\u0026#34;, \u0026#34;notify\u0026#34;])) Scenario 2: API regression order testing (Go) Background: the same set of API calls may trigger different cache or state paths when called in different orders. Why it fits: validating order sensitivity is directly a permutation problem.\npackage main import \u0026#34;fmt\u0026#34; func permute(items []string) [][]string { if len(items) == 0 { return [][]string{{}} } res := make([][]string, 0) for i, item := range items { rest := append([]string{}, items[:i]...) rest = append(rest, items[i+1:]...) for _, tail := range permute(rest) { res = append(res, append([]string{item}, tail...)) } } return res } func main() { fmt.Println(permute([]string{\u0026#34;login\u0026#34;, \u0026#34;query\u0026#34;, \u0026#34;logout\u0026#34;})) } Scenario 3: animation order exploration (JavaScript) Background: during UI prototyping, a team wants to try several orders of animation steps. Why it fits: different step orders produce different user experiences.\nfunction permute(items) { if (items.length === 0) return [[]]; const res = []; for (let i = 0; i \u0026lt; items.length; i += 1) { const rest = items.slice(0, i).concat(items.slice(i + 1)); for (const tail of permute(rest)) { res.push([items[i], ...tail]); } } return res; } console.log(permute([\u0026#34;fade\u0026#34;, \u0026#34;scale\u0026#34;, \u0026#34;slide\u0026#34;])); R — Reflection Complexity Time complexity: O(n * n!) Auxiliary recursion space: O(n) If output is counted, total space is dominated by the n! answer set Comparison with Subsets Problem Nature When to collect Key state 78. Subsets combinations every node startIndex 46. Permutations permutations leaf nodes only used[] Common mistakes forgetting to restore used[i] collecting answers at DFS entry and accidentally storing incomplete prefixes trying to solve permutations with startIndex and missing order variants Best Practices Think of this as “fill the next position” rather than “pick the next number” Restore path and used as a pair Draw a 3-level search tree before coding if the recursion feels abstract Keep the distinction between combination templates and permutation templates explicit in your notes S — Summary The real lesson of LeetCode 46 is state control through used[] Permutations collect answers only at leaf nodes because only leaves represent full results Compared with Subsets, this problem is less about boundaries and more about state recovery Once this template is stable, many order-sensitive search problems become much easier Suggested Next Problems 78. Subsets: combination-style backtracking template 17. Letter Combinations of a Phone Number: fixed-depth DFS 47. Permutations II: permutations with duplicate handling 51. N-Queens: state-heavy constrained search CTA After reading this, try rewriting the template once from memory and explain out loud why used[] is necessary. That one habit will make the distinction between combinations and permutations much harder to forget.\nMulti-Language Implementations Python from typing import List def permute(nums: List[int]) -\u0026gt; List[List[int]]: res: List[List[int]] = [] path: List[int] = [] used = [False] * len(nums) def dfs() -\u0026gt; None: if len(path) == len(nums): res.append(path.copy()) return for i, x in enumerate(nums): if used[i]: continue used[i] = True path.append(x) dfs() path.pop() used[i] = False dfs() return res C #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; typedef struct { int** data; int* col_sizes; int size; int capacity; } Result; static void push_result(Result* res, int* path, int n) { if (res-\u0026gt;size == res-\u0026gt;capacity) { res-\u0026gt;capacity *= 2; res-\u0026gt;data = realloc(res-\u0026gt;data, sizeof(int*) * res-\u0026gt;capacity); res-\u0026gt;col_sizes = realloc(res-\u0026gt;col_sizes, sizeof(int) * res-\u0026gt;capacity); } int* row = malloc(sizeof(int) * n); for (int i = 0; i \u0026lt; n; ++i) row[i] = path[i]; res-\u0026gt;data[res-\u0026gt;size] = row; res-\u0026gt;col_sizes[res-\u0026gt;size] = n; res-\u0026gt;size += 1; } static void dfs(int* nums, int n, bool* used, int* path, int depth, Result* res) { if (depth == n) { push_result(res, path, n); return; } for (int i = 0; i \u0026lt; n; ++i) { if (used[i]) continue; used[i] = true; path[depth] = nums[i]; dfs(nums, n, used, path, depth + 1, res); used[i] = false; } } int** permute(int* nums, int nums_size, int* return_size, int** return_column_sizes) { Result res = {0}; res.capacity = 16; res.data = malloc(sizeof(int*) * res.capacity); res.col_sizes = malloc(sizeof(int) * res.capacity); bool* used = calloc(nums_size, sizeof(bool)); int* path = malloc(sizeof(int) * nums_size); dfs(nums, nums_size, used, path, 0, \u0026amp;res); free(used); free(path); *return_size = res.size; *return_column_sizes = res.col_sizes; return res.data; } C++ #include \u0026lt;vector\u0026gt; using namespace std; class Solution { public: vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; permute(vector\u0026lt;int\u0026gt;\u0026amp; nums) { vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; res; vector\u0026lt;int\u0026gt; path; vector\u0026lt;int\u0026gt; used(nums.size(), 0); dfs(nums, used, path, res); return res; } private: void dfs(const vector\u0026lt;int\u0026gt;\u0026amp; nums, vector\u0026lt;int\u0026gt;\u0026amp; used, vector\u0026lt;int\u0026gt;\u0026amp; path, vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; res) { if ((int)path.size() == (int)nums.size()) { res.push_back(path); return; } for (int i = 0; i \u0026lt; (int)nums.size(); ++i) { if (used[i]) continue; used[i] = 1; path.push_back(nums[i]); dfs(nums, used, path, res); path.pop_back(); used[i] = 0; } } }; Go package main func permute(nums []int) [][]int { res := make([][]int, 0) path := make([]int, 0, len(nums)) used := make([]bool, len(nums)) var dfs func() dfs = func() { if len(path) == len(nums) { snapshot := append([]int(nil), path...) res = append(res, snapshot) return } for i, x := range nums { if used[i] { continue } used[i] = true path = append(path, x) dfs() path = path[:len(path)-1] used[i] = false } } dfs() return res } Rust fn permute(nums: Vec\u0026lt;i32\u0026gt;) -\u0026gt; Vec\u0026lt;Vec\u0026lt;i32\u0026gt;\u0026gt; { fn dfs(nums: \u0026amp;[i32], used: \u0026amp;mut [bool], path: \u0026amp;mut Vec\u0026lt;i32\u0026gt;, res: \u0026amp;mut Vec\u0026lt;Vec\u0026lt;i32\u0026gt;\u0026gt;) { if path.len() == nums.len() { res.push(path.clone()); return; } for i in 0..nums.len() { if used[i] { continue; } used[i] = true; path.push(nums[i]); dfs(nums, used, path, res); path.pop(); used[i] = false; } } let mut res = Vec::new(); let mut path = Vec::new(); let mut used = vec![false; nums.len()]; dfs(\u0026amp;nums, \u0026amp;mut used, \u0026amp;mut path, \u0026amp;mut res); res } JavaScript function permute(nums) { const res = []; const path = []; const used = new Array(nums.length).fill(false); function dfs() { if (path.length === nums.length) { res.push([...path]); return; } for (let i = 0; i \u0026lt; nums.length; i += 1) { if (used[i]) continue; used[i] = true; path.push(nums[i]); dfs(); path.pop(); used[i] = false; } } dfs(); return res; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/backtracking/46-permutations/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\nIf Subsets teaches the skeleton of combination-style backtracking, Permutations teaches the core of state-based backtracking: at each position, choose one unused element, continue until the path length reaches \u003ccode\u003en\u003c/code\u003e, and only then collect the answer.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebacktracking\u003c/code\u003e, \u003ccode\u003epermutations\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Permutations, backtracking, used array, DFS, LeetCode 46\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn the stable permutation backtracking template for LeetCode 46, with state recovery, engineering analogies, and runnable multi-language solutions.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven an array \u003ccode\u003enums\u003c/code\u003e of distinct integers, return all possible permutations.\nThe answer may be returned in any order.\u003c/p\u003e","title":"Hot100: Permutations (used[] Backtracking ACERS Guide)"},{"content":" Subtitle / Summary\nClone Graph is not a traversal-only problem. The real challenge is preserving graph structure while avoiding duplicate copies in the presence of cycles. The stable solution is a traversal plus a hash map from original nodes to cloned nodes.\nReading time: 12-15 min Tags: graph, dfs, bfs, hash map, deep copy SEO keywords: Clone Graph, graph deep copy, DFS, BFS, LeetCode 133 Meta description: Deep-copy an undirected graph with a node-to-node map, explaining why memoization is mandatory and how DFS/BFS versions work, with runnable code in six languages. Target Readers LeetCode learners practicing graph traversal and deep-copy patterns Engineers who duplicate object graphs, workflow graphs, or topology graphs Developers who want one reusable template for “clone with cycles” Background / Motivation Many “copy” problems are actually identity-preservation problems.\nFor arrays or flat objects, copying is straightforward.\nGraphs are different because:\na node can be reached from multiple paths the graph can contain cycles copying only values is not enough; edges must point to cloned neighbors, not original ones This is why Clone Graph is a classic interview and engineering problem:\ncopying workflow DAGs or small cyclic state machines duplicating editor nodes while preserving connections snapshotting a topology-like data structure before mutation Core Concepts Deep copy: every node in the returned graph is a newly created node Node identity: the key is the original node object/reference, not only val Adjacency structure: each cloned node must point to cloned neighbors in the same shape as the original graph Memo map: original_node -\u0026gt; cloned_node, used to avoid repeated cloning and infinite recursion A - Algorithm Problem Restatement You are given a reference to one node in a connected undirected graph.\nReturn a deep copy of the entire graph.\nEach node contains:\nclass Node { public int val; public List\u0026lt;Node\u0026gt; neighbors; } The graph in the test case is represented as an adjacency list.\nThe given node is always the node with value 1, unless the graph is empty.\nInput / Output Name Type Meaning node Node or null one node in the original graph return Node or null one node in the cloned graph Examples Example 1 Input: adjList = [[2,4],[1,3],[2,4],[1,3]] Output: [[2,4],[1,3],[2,4],[1,3]] Explanation:\nNode 1 connects to 2 and 4 Node 2 connects to 1 and 3 Node 3 connects to 2 and 4 Node 4 connects to 1 and 3 The cloned graph must have the same neighbor relationships, but all nodes must be newly allocated.\nExample 2 Input: adjList = [[]] Output: [[]] There is exactly one node and it has no neighbors.\nExample 3 Input: adjList = [] Output: [] The graph is empty, so the answer is null.\nConstraints The number of nodes is in the range [0, 100] 1 \u0026lt;= Node.val \u0026lt;= 100 Node.val is unique for each node There are no repeated edges and no self-loops The graph is connected and all nodes are reachable from the given node Thought Process: From Wrong Copying to the Correct Pattern Wrong idea 1: clone one node at a time without memory Suppose we do this:\ncreate a clone of the current node recursively clone every neighbor This breaks when the graph has a cycle.\nExample:\n1 -- 2 | | 4 -- 3 If you clone 1, then 2, then 1 again through the back edge, you create duplicate nodes or recurse forever.\nWrong idea 2: use only node values as a complete substitute for node objects In this LeetCode problem, values are unique, so value-based mapping happens to work.\nBut the transferable engineering pattern is:\nmap by original node identity/reference, not by coincidence of values.\nThat keeps the solution correct even when value uniqueness is not guaranteed in other systems.\nKey observation Each original node should be cloned exactly once.\nAfter that, every edge should reuse the already-created clone.\nThat leads directly to:\ntraversal: DFS or BFS memo map: original -\u0026gt; cloned C - Concepts Method Category Graph traversal Hash table / memoization Deep-copy construction Why the Memo Map Is Mandatory The memo map solves two problems at once:\nPrevents infinite loops on cyclic graphs Prevents duplicate clones when multiple paths reach the same node Without the map, the copied graph cannot preserve shared structure correctly.\nDFS Version The DFS idea is:\nif the input node is null, return null if the node has already been cloned, return the stored clone otherwise create a new clone and store it immediately recursively clone every neighbor and append the cloned neighbors return the cloned node Storing the clone before recursing is essential.\nThat is what breaks cycles safely.\nBFS Version The BFS version is equally valid:\nclone the starting node push the original node into a queue pop nodes level by level for each neighbor: create its clone if missing append the neighbor clone to the current clone enqueue the original neighbor if first seen DFS is usually shorter to write.\nBFS can feel more explicit if you prefer iterative traversal.\nCorrectness Intuition Once a node is first seen:\none clone is created the mapping remembers that clone forever So every future edge pointing to the original node can safely point to the same cloned node.\nThat ensures both:\nnode uniqueness in the clone edge structure preservation Reference Implementation Before moving to engineering scenarios, it helps to pin down the direct interview solution first.\nPython DFS from collections import deque class Node: def __init__(self, val=0, neighbors=None): self.val = val self.neighbors = neighbors if neighbors is not None else [] def build_graph(adj_list): if not adj_list: return None nodes = {i + 1: Node(i + 1) for i in range(len(adj_list))} for i, neighbors in enumerate(adj_list, start=1): nodes[i].neighbors = [nodes[val] for val in neighbors] return nodes[1] def graph_to_adj_list(node): if node is None: return [] seen = {node} queue = deque([node]) nodes_by_val = {} while queue: cur = queue.popleft() nodes_by_val[cur.val] = cur for nxt in cur.neighbors: if nxt not in seen: seen.add(nxt) queue.append(nxt) return [ [nxt.val for nxt in nodes_by_val[val].neighbors] for val in sorted(nodes_by_val) ] def clone_graph_dfs(node): copies = {} def dfs(cur): if cur is None: return None if cur in copies: return copies[cur] cloned = Node(cur.val) copies[cur] = cloned for nxt in cur.neighbors: cloned.neighbors.append(dfs(nxt)) return cloned return dfs(node) if __name__ == \u0026#34;__main__\u0026#34;: adj_list = [[2, 4], [1, 3], [2, 4], [1, 3]] original = build_graph(adj_list) cloned = clone_graph_dfs(original) print(graph_to_adj_list(cloned)) print(original is cloned) Python BFS from collections import deque class Node: def __init__(self, val=0, neighbors=None): self.val = val self.neighbors = neighbors if neighbors is not None else [] def build_graph(adj_list): if not adj_list: return None nodes = {i + 1: Node(i + 1) for i in range(len(adj_list))} for i, neighbors in enumerate(adj_list, start=1): nodes[i].neighbors = [nodes[val] for val in neighbors] return nodes[1] def graph_to_adj_list(node): if node is None: return [] seen = {node} queue = deque([node]) nodes_by_val = {} while queue: cur = queue.popleft() nodes_by_val[cur.val] = cur for nxt in cur.neighbors: if nxt not in seen: seen.add(nxt) queue.append(nxt) return [ [nxt.val for nxt in nodes_by_val[val].neighbors] for val in sorted(nodes_by_val) ] def clone_graph_bfs(node): if node is None: return None copies = {node: Node(node.val)} queue = deque([node]) while queue: cur = queue.popleft() for nxt in cur.neighbors: if nxt not in copies: copies[nxt] = Node(nxt.val) queue.append(nxt) copies[cur].neighbors.append(copies[nxt]) return copies[node] if __name__ == \u0026#34;__main__\u0026#34;: adj_list = [[2, 4], [1, 3], [2, 4], [1, 3]] original = build_graph(adj_list) cloned = clone_graph_bfs(original) print(graph_to_adj_list(cloned)) print(original is cloned) E - Engineering Scenario 1: Duplicating a Workflow Graph Template (Python) Background: a workflow editor stores nodes and outgoing links.\nWhy it fits: every duplicated workflow must preserve connections without sharing mutable nodes with the original.\ndef clone_adj(graph): copied = {} def dfs(u): if u in copied: return copied[u] copied[u] = [] for v in graph.get(u, []): dfs(v) copied[u].append(v) return copied[u] for u in graph: dfs(u) return copied workflow = {1: [2, 4], 2: [1, 3], 3: [2, 4], 4: [1, 3]} print(clone_adj(workflow)) Scenario 2: Cloning a Service Dependency Snapshot (Go) Background: before mutating a service dependency graph, you want a safe snapshot.\nWhy it fits: the graph may contain cycles, shared dependencies, and repeated reachability paths.\npackage main import \u0026#34;fmt\u0026#34; func cloneAdj(graph map[int][]int) map[int][]int { out := map[int][]int{} for u, ns := range graph { cp := make([]int, len(ns)) copy(cp, ns) out[u] = cp } return out } func main() { g := map[int][]int{1: {2, 4}, 2: {1, 3}, 3: {2, 4}, 4: {1, 3}} fmt.Println(cloneAdj(g)) } Scenario 3: Copy-Paste in a Frontend Node Editor (JavaScript) Background: a visual editor copies a graph of blocks and edges.\nWhy it fits: pasted blocks must point only to pasted blocks, never to the original graph.\nfunction cloneAdj(graph) { const out = {}; for (const [k, v] of Object.entries(graph)) { out[k] = [...v]; } return out; } const graph = {1: [2, 4], 2: [1, 3], 3: [2, 4], 4: [1, 3]}; console.log(cloneAdj(graph)); R - Reflection Complexity Let:\nn = number of nodes m = number of edges Then the DFS/BFS clone visits each node once and each edge once:\nTime: O(n + m) Space: O(n) The extra space comes from:\nthe memo map the recursion stack for DFS or the queue for BFS Alternatives DFS + hash map: shortest and most common BFS + hash map: equally correct, iterative Naive recursive copy without map: incorrect on cyclic graphs Common Mistakes Creating the clone after processing neighbors, which breaks cycles Forgetting to memoize the node before recursion Copying neighbor values instead of neighbor node references Returning a shallow copy where neighbor lists still point to original nodes Why This Solution Is the Most Practical This problem is fundamentally “graph traversal + identity-preserving duplication.”\nA memo map solves exactly the hard part, so DFS/BFS with mapping is both the cleanest interview solution and the most transferable engineering pattern.\nFAQ Why not map by val?\nIn this problem val is unique, so it works. But the more general and safer pattern is mapping original node references to cloned node references.\nWhy store the clone before traversing neighbors?\nBecause a cycle may revisit the same node immediately. The map entry must already exist when that happens.\nDFS or BFS, which one is better?\nNeither is asymptotically better here. DFS is shorter; BFS avoids recursion depth concerns.\nS - Summary Clone Graph is a deep-copy problem, not just a traversal problem. The essential data structure is a map from original nodes to cloned nodes. Memoizing before recursing is what makes cycles safe. DFS and BFS are both valid; the important invariant is one clone per original node. Further Reading LeetCode 138: Copy List with Random Pointer Graph traversal templates for DFS and BFS Deep-copy patterns for cyclic object graphs Next Step Try rewriting the same solution in both DFS and BFS styles.\nIf you can switch between the two without changing the memo-map invariant, you fully understand the problem.\nMulti-language Implementations Python from typing import Optional class Node: def __init__(self, val: int = 0, neighbors=None): self.val = val self.neighbors = neighbors if neighbors is not None else [] class Solution: def cloneGraph(self, node: Optional[\u0026#34;Node\u0026#34;]) -\u0026gt; Optional[\u0026#34;Node\u0026#34;]: copies = {} def dfs(cur: Optional[\u0026#34;Node\u0026#34;]) -\u0026gt; Optional[\u0026#34;Node\u0026#34;]: if cur is None: return None if cur in copies: return copies[cur] cloned = Node(cur.val) copies[cur] = cloned for nxt in cur.neighbors: cloned.neighbors.append(dfs(nxt)) return cloned return dfs(node) C /* * LeetCode provides the Node definition. * The core idea is: * 1. keep a map original -\u0026gt; cloned * 2. create the clone before recursing * * In pure C, the hash table implementation is verbose, so interview answers * often use C++/Go/Python for this problem. The algorithm itself is the same. */ C++ /* // Definition for a Node. class Node { public: int val; vector\u0026lt;Node*\u0026gt; neighbors; Node() { val = 0; neighbors = vector\u0026lt;Node*\u0026gt;(); } Node(int _val) { val = _val; neighbors = vector\u0026lt;Node*\u0026gt;(); } Node(int _val, vector\u0026lt;Node*\u0026gt; _neighbors) { val = _val; neighbors = _neighbors; } }; */ class Solution { public: unordered_map\u0026lt;Node*, Node*\u0026gt; copies; Node* cloneGraph(Node* node) { return dfs(node); } Node* dfs(Node* node) { if (!node) return nullptr; if (copies.count(node)) return copies[node]; Node* cloned = new Node(node-\u0026gt;val); copies[node] = cloned; for (Node* nxt : node-\u0026gt;neighbors) { cloned-\u0026gt;neighbors.push_back(dfs(nxt)); } return cloned; } }; Go /** * type Node struct { * Val int * Neighbors []*Node * } */ func cloneGraph(node *Node) *Node { copies := map[*Node]*Node{} var dfs func(*Node) *Node dfs = func(cur *Node) *Node { if cur == nil { return nil } if cp, ok := copies[cur]; ok { return cp } cloned := \u0026amp;Node{Val: cur.Val, Neighbors: []*Node{}} copies[cur] = cloned for _, nxt := range cur.Neighbors { cloned.Neighbors = append(cloned.Neighbors, dfs(nxt)) } return cloned } return dfs(node) } Rust use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; type NodeRef = Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;; #[derive(Debug)] pub struct Node { pub val: i32, pub neighbors: Vec\u0026lt;NodeRef\u0026gt;, } fn clone_graph(node: Option\u0026lt;NodeRef\u0026gt;) -\u0026gt; Option\u0026lt;NodeRef\u0026gt; { fn dfs(cur: \u0026amp;NodeRef, copies: \u0026amp;mut HashMap\u0026lt;*const RefCell\u0026lt;Node\u0026gt;, NodeRef\u0026gt;) -\u0026gt; NodeRef { let key = Rc::as_ptr(cur); if let Some(existing) = copies.get(\u0026amp;key) { return existing.clone(); } let cloned = Rc::new(RefCell::new(Node { val: cur.borrow().val, neighbors: vec![] })); copies.insert(key, cloned.clone()); let neighbors = cur.borrow().neighbors.clone(); for nxt in neighbors { let cp = dfs(\u0026amp;nxt, copies); cloned.borrow_mut().neighbors.push(cp); } cloned } let mut copies = HashMap::new(); node.map(|n| dfs(\u0026amp;n, \u0026amp;mut copies)) } JavaScript /* // Definition for a Node. function Node(val, neighbors) { this.val = val === undefined ? 0 : val; this.neighbors = neighbors === undefined ? [] : neighbors; } */ var cloneGraph = function (node) { const copies = new Map(); function dfs(cur) { if (cur === null) return null; if (copies.has(cur)) return copies.get(cur); const cloned = new Node(cur.val); copies.set(cur, cloned); for (const nxt of cur.neighbors) { cloned.neighbors.push(dfs(nxt)); } return cloned; } return dfs(node); }; ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/133-clone-graph/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nClone Graph is not a traversal-only problem. The real challenge is preserving graph structure while avoiding duplicate copies in the presence of cycles. The stable solution is a traversal plus a hash map from original nodes to cloned nodes.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003egraph\u003c/code\u003e, \u003ccode\u003edfs\u003c/code\u003e, \u003ccode\u003ebfs\u003c/code\u003e, \u003ccode\u003ehash map\u003c/code\u003e, \u003ccode\u003edeep copy\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Clone Graph, graph deep copy, DFS, BFS, LeetCode 133\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Deep-copy an undirected graph with a node-to-node map, explaining why memoization is mandatory and how DFS/BFS versions work, with runnable code in six languages.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLeetCode learners practicing graph traversal and deep-copy patterns\u003c/li\u003e\n\u003cli\u003eEngineers who duplicate object graphs, workflow graphs, or topology graphs\u003c/li\u003e\n\u003cli\u003eDevelopers who want one reusable template for “clone with cycles”\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eMany “copy” problems are actually identity-preservation problems.\u003c/p\u003e","title":"LeetCode 133: Clone Graph Hash Map + DFS/BFS ACERS Guide"},{"content":" Subtitle / Summary\nThis problem is a useful bridge between sorting and binary search. After sorting the array, all copies of target become one contiguous block, and the answer is simply every index inside that block.\nReading time: 10-12 min Tags: sorting, binary search, range location SEO keywords: Find Target Indices After Sorting Array, LeetCode 2089, lower bound, upper bound Meta description: Sort the array, use lower and upper bounds to find the target block, and return every matching index, with tradeoffs, engineering scenarios, and runnable implementations in six languages. Target Readers Learners connecting sorting with lower/upper bound search Engineers who need all positions of one value after offline sorting Interview candidates reviewing how contiguous blocks form in sorted data Background / Motivation The input array is not sorted, so we cannot apply binary search immediately.\nBut once we sort it, every copy of the same value becomes one continuous segment.\nThat gives a clean workflow:\nsort the data find where the target block starts find where the target block ends output all indices in that block This is the same “range of equal values” idea used in many sorted-data systems:\nleaderboard grouping equal-score buckets value-based offline analytics Core Concepts Sorted target block: equal values are contiguous after sorting Lower bound: first index i such that nums[i] \u0026gt;= target Upper bound: first index i such that nums[i] \u0026gt; target Answer range: all indices in [lower_bound(target), upper_bound(target)) A - Algorithm Problem Restatement Given an integer array nums and an integer target:\nsort nums in non-decreasing order return all indices where the sorted array equals target If target does not exist, return an empty array.\nInput / Output Name Type Meaning nums int[] unsorted integer array target int value to locate after sorting return int[] all indices of target in the sorted array Example 1 nums = [1, 2, 5, 2, 3] target = 2 sorted = [1, 2, 2, 3, 5] output = [1, 2] Example 2 nums = [1, 2, 5, 2, 3] target = 3 sorted = [1, 2, 2, 3, 5] output = [3] Example 3 nums = [1, 2, 5, 2, 3] target = 5 sorted = [1, 2, 2, 3, 5] output = [4] Thought Process: From Sort-and-Scan to Sort-and-Bounds The most direct solution is:\nsort the array scan the sorted result collect every index whose value equals target That is valid and easy to understand.\nIf you want to train the binary-search pattern, there is a cleaner post-sort observation:\nafter sorting, all targets are adjacent so the answer is one contiguous interval That means we can:\nfind the first target with lower bound find the first value greater than target with upper bound generate the index list from that interval C - Concepts Method Category Sorting + binary search Range discovery in sorted data Boundary search Why the Target Indices Form a Continuous Range After sorting:\nall values smaller than target appear first then all copies of target then all values greater than target So if:\nl = lower_bound(target) r = upper_bound(target) then the target indices are exactly:\n[l, l+1, ..., r-1] Stable Algorithm sort nums compute l = lower_bound(nums, target) compute r = upper_bound(nums, target) if l == r, return [] otherwise return all integers from l to r - 1 E - Engineering Scenario 1: Equal-Score Buckets After Offline Sort (Python) Background: scores are collected unsorted, then sorted for reporting.\nWhy it fits: equal scores become one contiguous block.\nfrom bisect import bisect_left, bisect_right scores = sorted([1, 2, 5, 2, 3]) l = bisect_left(scores, 2) r = bisect_right(scores, 2) print(list(range(l, r))) Scenario 2: Batch Value Grouping in Services (Go) Background: a batch job sorts values before producing grouped summaries.\nWhy it fits: the exact index block of one value is found by two boundaries.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sort\u0026#34; ) func main() { nums := []int{1, 2, 5, 2, 3} sort.Ints(nums) l := sort.Search(len(nums), func(i int) bool { return nums[i] \u0026gt;= 2 }) r := sort.Search(len(nums), func(i int) bool { return nums[i] \u0026gt; 2 }) var ans []int for i := l; i \u0026lt; r; i++ { ans = append(ans, i) } fmt.Println(ans) } Scenario 3: Frontend Highlighting of Equal Rankings (JavaScript) Background: a UI sorts scores and highlights every position tied with one value.\nWhy it fits: ties become one adjacent segment after sorting.\nconst nums = [1, 2, 5, 2, 3].slice().sort((a, b) =\u0026gt; a - b); const ans = []; for (let i = 0; i \u0026lt; nums.length; i++) { if (nums[i] === 2) ans.push(i); } console.log(nums, ans); // [1,2,2,3,5] [1,2] R - Reflection Complexity For the sort + bounds solution:\nTime: O(n log n) because sorting dominates Space: O(1) extra if the language sort is in-place and we ignore implementation details otherwise depends on the sorting implementation Alternative: Direct Counting There is an important alternative:\ncount how many elements are \u0026lt; target count how many elements are == target build the answer range directly That approach is O(n) and is asymptotically better for this standalone problem.\nWhy Keep the Sort + Bounds Version in a Binary-Search Series Even though counting can be faster here, sort + lower/upper bound is still valuable because it teaches a reusable pattern:\nequal values form one block after sorting lower and upper bounds recover that block That same reasoning appears in many other problems and real systems.\nCommon Mistakes Sorting the array but still scanning the whole result after already knowing the boundaries Forgetting that upper_bound is exclusive Claiming this is strictly optimal without mentioning the linear counting alternative S - Summary After sorting, every copy of target becomes one contiguous segment. Lower and upper bounds recover the exact index interval of that segment. Returning range(l, r) is enough once the two boundaries are known. For this specific problem, direct counting is a valid faster alternative, but sort + bounds is the better teaching pattern for a binary-search series. Further Reading LeetCode 34: Find First and Last Position of Element in Sorted Array LeetCode 35: Search Insert Position Any standard documentation for bisect_left, bisect_right, lower_bound, and upper_bound Multi-language Implementations Python from bisect import bisect_left, bisect_right from typing import List def target_indices(nums: List[int], target: int) -\u0026gt; List[int]: nums = sorted(nums) l = bisect_left(nums, target) r = bisect_right(nums, target) return list(range(l, r)) if __name__ == \u0026#34;__main__\u0026#34;: print(target_indices([1, 2, 5, 2, 3], 2)) print(target_indices([1, 2, 5, 2, 3], 3)) print(target_indices([1, 2, 5, 2, 3], 5)) C #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; int cmp(const void *a, const void *b) { return (*(int *)a) - (*(int *)b); } int lowerBound(int *nums, int n, int target) { int l = 0, r = n; while (l \u0026lt; r) { int mid = l + (r - l) / 2; if (nums[mid] \u0026gt;= target) r = mid; else l = mid + 1; } return l; } int upperBound(int *nums, int n, int target) { int l = 0, r = n; while (l \u0026lt; r) { int mid = l + (r - l) / 2; if (nums[mid] \u0026gt; target) r = mid; else l = mid + 1; } return l; } int main(void) { int nums[] = {1, 2, 5, 2, 3}; int n = sizeof(nums) / sizeof(nums[0]); qsort(nums, n, sizeof(int), cmp); int l = lowerBound(nums, n, 2); int r = upperBound(nums, n, 2); for (int i = l; i \u0026lt; r; i++) { printf(\u0026#34;%d \u0026#34;, i); } printf(\u0026#34;\\n\u0026#34;); return 0; } C++ #include \u0026lt;algorithm\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; vector\u0026lt;int\u0026gt; targetIndices(vector\u0026lt;int\u0026gt; nums, int target) { sort(nums.begin(), nums.end()); auto l = lower_bound(nums.begin(), nums.end(), target); auto r = upper_bound(nums.begin(), nums.end(), target); vector\u0026lt;int\u0026gt; ans; for (auto it = l; it != r; ++it) { ans.push_back((int)(it - nums.begin())); } return ans; } int main() { auto ans = targetIndices({1, 2, 5, 2, 3}, 2); for (int x : ans) cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } Go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;sort\u0026#34; ) func targetIndices(nums []int, target int) []int { sort.Ints(nums) l := sort.Search(len(nums), func(i int) bool { return nums[i] \u0026gt;= target }) r := sort.Search(len(nums), func(i int) bool { return nums[i] \u0026gt; target }) ans := make([]int, 0, r-l) for i := l; i \u0026lt; r; i++ { ans = append(ans, i) } return ans } func main() { fmt.Println(targetIndices([]int{1, 2, 5, 2, 3}, 2)) fmt.Println(targetIndices([]int{1, 2, 5, 2, 3}, 3)) } Rust fn lower_bound(nums: \u0026amp;[i32], target: i32) -\u0026gt; usize { let (mut l, mut r) = (0usize, nums.len()); while l \u0026lt; r { let mid = l + (r - l) / 2; if nums[mid] \u0026gt;= target { r = mid; } else { l = mid + 1; } } l } fn upper_bound(nums: \u0026amp;[i32], target: i32) -\u0026gt; usize { let (mut l, mut r) = (0usize, nums.len()); while l \u0026lt; r { let mid = l + (r - l) / 2; if nums[mid] \u0026gt; target { r = mid; } else { l = mid + 1; } } l } fn target_indices(mut nums: Vec\u0026lt;i32\u0026gt;, target: i32) -\u0026gt; Vec\u0026lt;usize\u0026gt; { nums.sort(); let l = lower_bound(\u0026amp;nums, target); let r = upper_bound(\u0026amp;nums, target); (l..r).collect() } fn main() { println!(\u0026#34;{:?}\u0026#34;, target_indices(vec![1, 2, 5, 2, 3], 2)); println!(\u0026#34;{:?}\u0026#34;, target_indices(vec![1, 2, 5, 2, 3], 3)); } JavaScript function lowerBound(nums, target) { let l = 0, r = nums.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (nums[mid] \u0026gt;= target) r = mid; else l = mid + 1; } return l; } function upperBound(nums, target) { let l = 0, r = nums.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (nums[mid] \u0026gt; target) r = mid; else l = mid + 1; } return l; } function targetIndices(nums, target) { nums = nums.slice().sort((a, b) =\u0026gt; a - b); const l = lowerBound(nums, target); const r = upperBound(nums, target); const ans = []; for (let i = l; i \u0026lt; r; i++) ans.push(i); return ans; } console.log(targetIndices([1, 2, 5, 2, 3], 2)); console.log(targetIndices([1, 2, 5, 2, 3], 3)); console.log(targetIndices([1, 2, 5, 2, 3], 5)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/binary-search/2089-find-target-indices-after-sorting-array/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThis problem is a useful bridge between sorting and binary search. After sorting the array, all copies of \u003ccode\u003etarget\u003c/code\u003e become one contiguous block, and the answer is simply every index inside that block.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003esorting\u003c/code\u003e, \u003ccode\u003ebinary search\u003c/code\u003e, \u003ccode\u003erange location\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Find Target Indices After Sorting Array, LeetCode 2089, lower bound, upper bound\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Sort the array, use lower and upper bounds to find the target block, and return every matching index, with tradeoffs, engineering scenarios, and runnable implementations in six languages.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLearners connecting sorting with lower/upper bound search\u003c/li\u003e\n\u003cli\u003eEngineers who need all positions of one value after offline sorting\u003c/li\u003e\n\u003cli\u003eInterview candidates reviewing how contiguous blocks form in sorted data\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eThe input array is not sorted, so we cannot apply binary search immediately.\u003cbr\u003e\nBut once we sort it, every copy of the same value becomes one continuous segment.\u003c/p\u003e","title":"LeetCode 2089: Find Target Indices After Sorting Array ACERS Guide"},{"content":" Subtitle / Summary\nThis problem is a compact exercise in boundary counting. Because the array is already sorted, you do not count negatives and positives one by one; you find where zero starts and where zero ends, then compute both counts from those boundaries.\nReading time: 10-12 min Tags: binary search, counting, sorted array, boundaries SEO keywords: Maximum Count of Positive Integer and Negative Integer, LeetCode 2529, boundary counting Meta description: Use lower-bound and upper-bound binary search around zero to count negatives and positives in a sorted array, with correctness reasoning, engineering scenarios, and runnable implementations in six languages. Target Readers Learners practicing boundary search beyond exact-match lookup Engineers who count segments in sorted data Interview candidates learning how lower and upper bounds produce counts Background / Motivation The input is already sorted. That changes the problem completely.\nInstead of scanning the whole array and incrementing counters, we can ask:\nwhere do negative numbers stop? where do positive numbers start? Zeros act as the separator in the middle.\nSo this is really a boundary problem around the value 0.\nCore Concepts Negative count: number of values \u0026lt; 0 Positive count: number of values \u0026gt; 0 Lower bound of 0: first index where nums[i] \u0026gt;= 0 Upper bound of 0: first index where nums[i] \u0026gt; 0 From those:\nneg = lower_bound(0) pos = n - upper_bound(0) A - Algorithm Problem Restatement Given a sorted integer array nums that may contain negative numbers, zeros, and positive numbers:\nlet countNeg be the number of elements \u0026lt; 0 let countPos be the number of elements \u0026gt; 0 Return max(countNeg, countPos).\nInput / Output Name Type Meaning nums int[] sorted integer array return int larger of negative count and positive count Example 1 nums = [-3, -2, -1, 0, 0, 1, 2] output = 3 Example 2 nums = [-2, -1, -1, 1, 2, 3] output = 3 Example 3 nums = [0, 0, 0] output = 0 Thought Process: From Counting to Boundaries The direct idea is:\nscan the array count negatives count positives That is O(n), and for this problem it is actually acceptable.\nBut because the array is sorted, we can do better conceptually:\nnegatives are on the left zeros are in the middle positives are on the right So the counts are determined by two positions:\nthe first index \u0026gt;= 0 the first index \u0026gt; 0 C - Concepts Method Category Binary search Boundary counting Sorted partition lookup Why Two Searches Are Enough Let:\na = lower_bound(nums, 0) =\u0026gt; first non-negative index b = upper_bound(nums, 0) =\u0026gt; first positive index Then:\nall indices [0, a) are negative all indices [b, n) are positive So:\ncountNeg = a countPos = n - b Stable Algorithm compute neg = lower_bound(nums, 0) compute pos = len(nums) - upper_bound(nums, 0) return max(neg, pos) E - Engineering Scenario 1: Signed Score Analysis (Python) Background: a sorted score array contains losses, neutral events, and gains.\nWhy it fits: you only need the boundaries around zero, not a full scan for every query.\ndef lower_bound(nums, target): l, r = 0, len(nums) while l \u0026lt; r: mid = (l + r) // 2 if nums[mid] \u0026gt;= target: r = mid else: l = mid + 1 return l def upper_bound(nums, target): l, r = 0, len(nums) while l \u0026lt; r: mid = (l + r) // 2 if nums[mid] \u0026gt; target: r = mid else: l = mid + 1 return l nums = [-3, -2, -1, 0, 0, 1, 2] print(max(lower_bound(nums, 0), len(nums) - upper_bound(nums, 0))) Scenario 2: Sorted Risk Buckets (Go) Background: a risk engine stores sorted signed deviations and needs the dominant side.\nWhy it fits: zero is the natural split point.\npackage main import \u0026#34;fmt\u0026#34; func lowerBound(nums []int, target int) int { l, r := 0, len(nums) for l \u0026lt; r { mid := l + (r-l)/2 if nums[mid] \u0026gt;= target { r = mid } else { l = mid + 1 } } return l } func upperBound(nums []int, target int) int { l, r := 0, len(nums) for l \u0026lt; r { mid := l + (r-l)/2 if nums[mid] \u0026gt; target { r = mid } else { l = mid + 1 } } return l } func main() { nums := []int{-2, -1, -1, 1, 2, 3} neg := lowerBound(nums, 0) pos := len(nums) - upperBound(nums, 0) fmt.Println(max(neg, pos)) } func max(a, b int) int { if a \u0026gt; b { return a } return b } Scenario 3: Frontend Sorted Trend Display (JavaScript) Background: a UI shows sorted changes and wants to summarize whether negative or positive values dominate.\nWhy it fits: a boundary lookup is cheaper than repeated category checks when the list is reused.\nfunction lowerBound(nums, target) { let l = 0, r = nums.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (nums[mid] \u0026gt;= target) r = mid; else l = mid + 1; } return l; } function upperBound(nums, target) { let l = 0, r = nums.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (nums[mid] \u0026gt; target) r = mid; else l = mid + 1; } return l; } const nums = [-3, -2, -1, 0, 0, 1, 2]; console.log(Math.max(lowerBound(nums, 0), nums.length - upperBound(nums, 0))); R - Reflection Complexity Time: O(log n) Space: O(1) Alternatives Linear scan: valid and simple, but it ignores the structural value of the sorted input Two pointers from both ends: unnecessary and harder to reason about than boundary search Common Mistakes Counting zeros as positive or negative Using only one boundary and trying to infer both counts from it Using \u0026gt;= 0 when you need strictly positive count Why This Method Is the Most Reusable This problem is really about extracting counts from sorted partitions.\nThat exact pattern appears in metric analysis, score bands, and threshold reporting, so lower and upper bounds are the right abstractions.\nS - Summary The sorted array splits naturally into negatives, zeros, and positives. lower_bound(0) gives the negative count. len(nums) - upper_bound(0) gives the positive count. Boundary search turns counting into a clean O(log n) partition lookup. Further Reading LeetCode 35: Search Insert Position LeetCode 34: Find First and Last Position of Element in Sorted Array Any standard lower_bound / upper_bound documentation Multi-language Implementations Python from typing import List def lower_bound(nums: List[int], target: int) -\u0026gt; int: l, r = 0, len(nums) while l \u0026lt; r: mid = (l + r) // 2 if nums[mid] \u0026gt;= target: r = mid else: l = mid + 1 return l def upper_bound(nums: List[int], target: int) -\u0026gt; int: l, r = 0, len(nums) while l \u0026lt; r: mid = (l + r) // 2 if nums[mid] \u0026gt; target: r = mid else: l = mid + 1 return l def maximum_count(nums: List[int]) -\u0026gt; int: neg = lower_bound(nums, 0) pos = len(nums) - upper_bound(nums, 0) return max(neg, pos) if __name__ == \u0026#34;__main__\u0026#34;: print(maximum_count([-3, -2, -1, 0, 0, 1, 2])) print(maximum_count([-2, -1, -1, 1, 2, 3])) print(maximum_count([0, 0, 0])) C #include \u0026lt;stdio.h\u0026gt; int lowerBound(int *nums, int n, int target) { int l = 0, r = n; while (l \u0026lt; r) { int mid = l + (r - l) / 2; if (nums[mid] \u0026gt;= target) r = mid; else l = mid + 1; } return l; } int upperBound(int *nums, int n, int target) { int l = 0, r = n; while (l \u0026lt; r) { int mid = l + (r - l) / 2; if (nums[mid] \u0026gt; target) r = mid; else l = mid + 1; } return l; } int maximumCount(int *nums, int n) { int neg = lowerBound(nums, n, 0); int pos = n - upperBound(nums, n, 0); return neg \u0026gt; pos ? neg : pos; } int main(void) { int a[] = {-3, -2, -1, 0, 0, 1, 2}; int b[] = {-2, -1, -1, 1, 2, 3}; int c[] = {0, 0, 0}; printf(\u0026#34;%d\\n\u0026#34;, maximumCount(a, 7)); printf(\u0026#34;%d\\n\u0026#34;, maximumCount(b, 6)); printf(\u0026#34;%d\\n\u0026#34;, maximumCount(c, 3)); return 0; } C++ #include \u0026lt;algorithm\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; int maximumCount(const vector\u0026lt;int\u0026gt;\u0026amp; nums) { int neg = lower_bound(nums.begin(), nums.end(), 0) - nums.begin(); int pos = nums.end() - upper_bound(nums.begin(), nums.end(), 0); return max(neg, pos); } int main() { cout \u0026lt;\u0026lt; maximumCount({-3, -2, -1, 0, 0, 1, 2}) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; cout \u0026lt;\u0026lt; maximumCount({-2, -1, -1, 1, 2, 3}) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; cout \u0026lt;\u0026lt; maximumCount({0, 0, 0}) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } Go package main import \u0026#34;fmt\u0026#34; func lowerBound(nums []int, target int) int { l, r := 0, len(nums) for l \u0026lt; r { mid := l + (r-l)/2 if nums[mid] \u0026gt;= target { r = mid } else { l = mid + 1 } } return l } func upperBound(nums []int, target int) int { l, r := 0, len(nums) for l \u0026lt; r { mid := l + (r-l)/2 if nums[mid] \u0026gt; target { r = mid } else { l = mid + 1 } } return l } func maximumCount(nums []int) int { neg := lowerBound(nums, 0) pos := len(nums) - upperBound(nums, 0) if neg \u0026gt; pos { return neg } return pos } func main() { fmt.Println(maximumCount([]int{-3, -2, -1, 0, 0, 1, 2})) fmt.Println(maximumCount([]int{-2, -1, -1, 1, 2, 3})) fmt.Println(maximumCount([]int{0, 0, 0})) } Rust fn lower_bound(nums: \u0026amp;[i32], target: i32) -\u0026gt; usize { let (mut l, mut r) = (0usize, nums.len()); while l \u0026lt; r { let mid = l + (r - l) / 2; if nums[mid] \u0026gt;= target { r = mid; } else { l = mid + 1; } } l } fn upper_bound(nums: \u0026amp;[i32], target: i32) -\u0026gt; usize { let (mut l, mut r) = (0usize, nums.len()); while l \u0026lt; r { let mid = l + (r - l) / 2; if nums[mid] \u0026gt; target { r = mid; } else { l = mid + 1; } } l } fn maximum_count(nums: \u0026amp;[i32]) -\u0026gt; usize { let neg = lower_bound(nums, 0); let pos = nums.len() - upper_bound(nums, 0); neg.max(pos) } fn main() { println!(\u0026#34;{}\u0026#34;, maximum_count(\u0026amp;[-3, -2, -1, 0, 0, 1, 2])); println!(\u0026#34;{}\u0026#34;, maximum_count(\u0026amp;[-2, -1, -1, 1, 2, 3])); println!(\u0026#34;{}\u0026#34;, maximum_count(\u0026amp;[0, 0, 0])); } JavaScript function lowerBound(nums, target) { let l = 0, r = nums.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (nums[mid] \u0026gt;= target) r = mid; else l = mid + 1; } return l; } function upperBound(nums, target) { let l = 0, r = nums.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (nums[mid] \u0026gt; target) r = mid; else l = mid + 1; } return l; } function maximumCount(nums) { const neg = lowerBound(nums, 0); const pos = nums.length - upperBound(nums, 0); return Math.max(neg, pos); } console.log(maximumCount([-3, -2, -1, 0, 0, 1, 2])); console.log(maximumCount([-2, -1, -1, 1, 2, 3])); console.log(maximumCount([0, 0, 0])); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/binary-search/2529-maximum-count-of-positive-integer-and-negative-integer/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThis problem is a compact exercise in boundary counting. Because the array is already sorted, you do not count negatives and positives one by one; you find where zero starts and where zero ends, then compute both counts from those boundaries.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003ebinary search\u003c/code\u003e, \u003ccode\u003ecounting\u003c/code\u003e, \u003ccode\u003esorted array\u003c/code\u003e, \u003ccode\u003eboundaries\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Maximum Count of Positive Integer and Negative Integer, LeetCode 2529, boundary counting\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Use lower-bound and upper-bound binary search around zero to count negatives and positives in a sorted array, with correctness reasoning, engineering scenarios, and runnable implementations in six languages.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLearners practicing boundary search beyond exact-match lookup\u003c/li\u003e\n\u003cli\u003eEngineers who count segments in sorted data\u003c/li\u003e\n\u003cli\u003eInterview candidates learning how lower and upper bounds produce counts\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eThe input is already sorted. That changes the problem completely.\u003c/p\u003e","title":"LeetCode 2529: Maximum Count of Positive Integer and Negative Integer ACERS Guide"},{"content":"Problem Requirement Start with the official input nums = [5,7,7,8,8,10] and target = 8. The answer must be the complete range [3,4]; returning only index 3 or 4 does not identify both the target\u0026rsquo;s first and last occurrences.\nGiven an integer array nums sorted in non-decreasing order and an integer target:\nIf target exists, return the indices of its first and last occurrences as [first, last]. If target does not exist, return [-1, -1]. The problem ultimately requires an algorithm with O(log n) runtime. LeetCode uses this method contract:\nclass Solution: def searchRange(self, nums: List[int], target: int) -\u0026gt; List[int]: Official Examples 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 0 \u0026lt;= nums.length \u0026lt;= 10^5 -10^9 \u0026lt;= nums[i] \u0026lt;= 10^9 -10^9 \u0026lt;= target \u0026lt;= 10^9 nums is sorted in non-decreasing order. Step 1: Get a Definitely Correct Range When the target appears several times in a row, how can we guarantee that both the earliest and latest indices are recorded?\nPrevious Baseline The current baseline contains only the problem requirement, examples, constraints, and the Solution.searchRange method contract. It has no executable method yet.\nBreak Finding one element equal to target gives only one index. For 8 in [5,7,7,8,8,10], that index could be 3 or 4; neither one alone represents the complete range. The current baseline has no rule that guarantees both endpoints.\nChange Add one complete scan to the previous baseline:\nfirst means the earliest match seen so far. It starts at -1 and changes only on the first match. last means the latest match seen so far. It starts at -1 and changes on every match. With no match, both values remain -1. With duplicate targets, the first and final matches leave the two required endpoints.\nfrom typing import List def search_range_scan(nums: List[int], target: int) -\u0026gt; List[int]: first = -1 last = -1 for index, value in enumerate(nums): if value == target: if first == -1: first = index last = index return [first, last] assert search_range_scan([5, 7, 7, 8, 8, 10], 8) == [3, 4] assert search_range_scan([5, 7, 7, 8, 8, 10], 6) == [-1, -1] assert search_range_scan([], 0) == [-1, -1] assert search_range_scan([2, 2, 2, 2], 2) == [0, 3] assert search_range_scan([7], 7) == [0, 0] assert search_range_scan([7], 8) == [-1, -1] assert search_range_scan([1, 2, 3], 1) == [0, 0] assert search_range_scan([1, 2, 3], 3) == [2, 2] Check The fixed assertions cover all three official examples, all-equal values, a singleton hit and miss, and targets at the first and last array positions. Running the whole block without an assertion failure shows that this change handles the two-endpoint pressure created by duplicate targets.\nComplexity For an array of length n, this version checks every element, so its time complexity is O(n). It stores only first, last, index, and value, so its auxiliary space complexity is O(1).\nStep 1 Result This version can now return the correct first and last positions with one scan, including the absent-target case.\nIt still lacks the required runtime: the O(n) scan does not satisfy the problem\u0026rsquo;s O(log n) requirement.\nStep 2: Force the Search to Stop at the First Candidate The linear scan is correct, but for an input of length 10^5, it may inspect every element. The array is already sorted in non-decreasing order. How can we use that order to cut the search space in half each time?\nPrevious Baseline The previous version scans the whole array from left to right in O(n) time. It cannot miss a duplicate, but it does not use the fact that the array is sorted.\nAn ordinary binary search that checks only nums[mid] == target is not enough either. For [5,7,7,8,8,10] and target = 8, it may first hit index 4 and return immediately, even though index 3 also contains 8. An equality hit finds some target occurrence; it does not force the search to stop at the leftmost candidate.\nBreak For now, reduce the problem to this smaller question:\nFind the first index satisfying nums[i] \u0026gt;= target. If every element is less than target, return len(nums).\nBecause the array is sorted, the result of nums[i] \u0026gt;= target can change from False to True only once. For example:\nnums: [5, 7, 7, 8, 8, 10] \u0026gt;= 8: F F F T T T ^ first True We are no longer looking for any element equal to target. We are looking for the boundary where this predicate first becomes True.\nChange: Maintain a Half-Open Search Interval Use [left, right) for the actual array indices that have not been ruled out. The right side is excluded, so initialize it as:\nleft = 0 right = len(nums) Let boundary be the position we want. If no array element satisfies the predicate, boundary is len(nums). At the start of every iteration, maintain this invariant:\nleft \u0026lt;= boundary \u0026lt;= right. Every index less than left is known to fail the predicate, so its value is less than target. Every actual array index at least right is known to satisfy the predicate, so its value is not less than target. The unchecked actual array indices lie in the half-open interval [left, right). After choosing the midpoint, there are only two cases:\nmid = left + (right - left) // 2 if nums[mid] \u0026gt;= target: right = mid else: left = mid + 1 When nums[mid] \u0026gt;= target, mid itself may be the first satisfying position. We cannot discard it, so set right = mid.\nWhen nums[mid] \u0026lt; target, neither mid nor anything to its left can be the boundary, so set left = mid + 1.\nBoth updates preserve left \u0026lt;= boundary \u0026lt;= right. Also, whenever left \u0026lt; right, we have left \u0026lt;= mid \u0026lt; right, so either branch strictly decreases right - left. The loop must eventually stop with left == right; together with the invariant, this forces left == boundary.\nWrite that rule as the helper for this stage:\nfrom typing import List def first_not_less(nums: List[int], target: int) -\u0026gt; int: left = 0 right = len(nums) while left \u0026lt; right: mid = left + (right - left) // 2 if nums[mid] \u0026gt;= target: right = mid else: left = mid + 1 return left assert first_not_less([5, 7, 7, 8, 8, 10], 8) == 3 assert first_not_less([1, 3, 5], 4) == 2 assert first_not_less([], 0) == 0 assert first_not_less([2, 2, 2], 2) == 0 assert first_not_less([1, 3, 5], 0) == 0 assert first_not_less([1, 3, 5], 6) == 3 Check 1: Duplicate Target Trace [5,7,7,8,8,10] with target = 8:\nleft right mid nums[mid] \u0026gt;= 8 Interval after update 0 6 3 True [0, 3) 0 3 1 False [2, 3) 2 3 2 False [3, 3) When the interval becomes empty, the helper returns 3, the first index satisfying nums[i] \u0026gt;= 8. Even though the first checked index 3 already satisfies the predicate, the search keeps the left side and verifies that no earlier candidate exists.\nCheck 2: Target Between Two Values Trace [1,3,5] with target = 4:\nleft right mid nums[mid] \u0026gt;= 4 Interval after update 0 3 1 False [2, 3) 2 3 2 True [2, 2) The helper returns 2, meaning that inserting 4 at index 2 would preserve sorted order. This result describes only an insertion boundary; it does not say that the value at index 2 equals 4.\nCheck 3: Empty Input For an empty array, initialization gives left = right = 0. The loop never runs, and the helper returns 0 immediately. Here 0 also equals len(nums), so it is still a valid insertion boundary.\nStep 2 Result This version can now find the first position whose value is not less than target in O(log n) time. That position is also the insertion boundary that preserves sorted order.\nIt still lacks two things: the insertion position alone does not prove that target exists, and it does not locate the last position of a duplicate target block.\nStep 3: A Candidate Position Does Not Prove Existence Consider nums = [1,3,5] and target = 4. Step 2\u0026rsquo;s first_not_less returns 2 because index 2 is the first position where inserting 4 would preserve sorted order. But nums[2] is 5; the array does not contain 4 at all.\nHow can we distinguish \u0026ldquo;the target could be inserted here\u0026rdquo; from \u0026ldquo;the target really starts here\u0026rdquo;?\nPrevious Baseline The current version has first_not_less(nums, target). It returns the first index satisfying nums[i] \u0026gt;= target, or len(nums) if every value is less than target.\nThat is exactly an insertion boundary, but it is not yet a verified match.\nBreak Treating the insertion boundary as the start creates two kinds of failure:\nFor [1,3,5] and target = 4, start = 2 is still inside the array, but nums[2] != 4. For [1,3,5] and target = 6, start = 3, exactly equal to len(nums); reading nums[start] would be out of bounds. An empty array has the same problem. The validation order therefore cannot be reversed. We must first determine whether start equals the array length. Only when it is still an actual index may we read nums[start].\nChange: Rule Out an Out-of-Bounds Position Before Reading Reuse first_not_less from Step 2 and add this condition after it returns:\nif start == len(nums) or nums[start] != target: return -1 Python evaluates or from left to right and short-circuits:\nIf start == len(nums) is true, the whole condition is already true, so nums[start] is not evaluated. Only when start \u0026lt; len(nums) does Python continue to test nums[start] != target. When both conditions are false, start is an actual index and nums[start] == target. This helper has one consistent integer return contract: return the verified first index when the target exists, or -1 when it does not. The complete searchRange method will later return the problem\u0026rsquo;s required [-1, -1] for the absent case.\nAttach this check to the helper from the previous step:\nfrom typing import List def verified_start_or_absent(nums: List[int], target: int) -\u0026gt; int: start = first_not_less(nums, target) if start == len(nums) or nums[start] != target: return -1 return start assert verified_start_or_absent([1, 3, 5], 0) == -1 assert verified_start_or_absent([1, 3, 5], 6) == -1 assert verified_start_or_absent([1, 3, 5], 4) == -1 assert verified_start_or_absent([], 0) == -1 assert verified_start_or_absent([1, 2, 2, 2, 4], 2) == 1 This intermediate helper is not the complete LeetCode method. It returns one integer: -1 for an absent target or the verified start for a present target. It does not fabricate the other index that has not been derived yet.\nCheck: What Each Part of the Guard Prevents Input first_not_less result Validation result [1,3,5], target = 0 0 nums[0] is 1, so return -1 [1,3,5], target = 6 3 start == len(nums), so short-circuit and return -1 [1,3,5], target = 4 2 nums[2] is 5, so return -1 [], target = 0 0 start == len(nums), so short-circuit and return -1 [1,2,2,2,4], target = 2 1 nums[1] == 2, so preserve the verified start index 1 The last row also shows why we continue to reuse first_not_less: after validation, its index not only contains the target but remains the target\u0026rsquo;s first occurrence.\nStep 3 Result This version can now safely distinguish an insertion position from a real match: it returns -1 when the target is absent and produces a verified first occurrence when the target is present.\nIt still lacks the last occurrence of a present target, so it cannot return the complete range yet.\nStep 4: Find the First Position After the Target Block Step 3 verifies start, but it tells us only where the target block begins, not how far that block extends.\nA direct approach is to scan right from start until the value changes:\nlast = start while last + 1 \u0026lt; len(nums) and nums[last + 1] == target: last += 1 If all n elements equal target, this scan still walks through almost the whole array, giving O(n) worst-case time. That would lose the O(log n) advantage already earned from binary search.\nBreak Instead of walking from inside the target block to its end, we need another boundary that can be found with binary search:\nFind the first index satisfying nums[i] \u0026gt; target. If there is no greater element, return len(nums).\nFor [5,7,7,8,8,10] and target = 8, the predicate looks like this:\nnums: [5, 7, 7, 8, 8, 10] \u0026gt; 8: F F F F F T ^ first True This position is 5. The target block occupies the consecutive indices immediately before it, so the target\u0026rsquo;s last index is:\nend = first_greater - 1 = 5 - 1 = 4 Change: Move Right When the Value Equals the Target The second search still uses the half-open interval [left, right), but its predicate must be the strict condition nums[mid] \u0026gt; target:\nmid = left + (right - left) // 2 if nums[mid] \u0026gt; target: right = mid else: left = mid + 1 Let greater_boundary be the first position whose value is greater than target, or len(nums) if there is no such value. At the start of each iteration, maintain:\nleft \u0026lt;= greater_boundary \u0026lt;= right. Every index less than left is known to satisfy nums[i] \u0026lt;= target. Every actual array index at least right is known to satisfy nums[i] \u0026gt; target. The unchecked actual array indices lie in [left, right). When nums[mid] \u0026gt; target, mid may be the first greater value, so keep it by setting right = mid. Otherwise nums[mid] \u0026lt;= target; this includes every position equal to target, and none of those positions can be the first greater value, so set left = mid + 1.\nThat is exactly how this search differs from first_not_less:\nThe first search treats nums[mid] == target as a true predicate, applies right = mid, and keeps that position while looking for an earlier candidate. The second search treats nums[mid] == target as a false predicate, applies left = mid + 1, and discards that position while looking farther right. Either branch strictly decreases right - left. The loop terminates with left == right, and the invariant then gives left == greater_boundary.\nCheck: Both Boundaries on the Official Duplicate Example First find the first position satisfying nums[i] \u0026gt;= 8:\nleft right mid Test Interval after update 0 6 3 8 \u0026gt;= 8 [0, 3) 0 3 1 7 \u0026lt; 8 [2, 3) 2 3 2 7 \u0026lt; 8 [3, 3) The first search returns start = 3.\nThen find the first position satisfying nums[i] \u0026gt; 8:\nleft right mid Test Interval after update 0 6 3 8 \u0026lt;= 8 [4, 6) 4 6 5 10 \u0026gt; 8 [4, 5) 4 5 4 8 \u0026lt;= 8 [5, 5) The second search returns first_greater = 5, so end = 5 - 1 = 4 and the final range is [3,4].\nAssemble the Final LeetCode Method The code below expands the already verified first-boundary search directly inside searchRange, keeps the safe existence guard, and then adds the second boundary search. The two loops are not merged into one configurable helper because their important distinction is the visible \u0026gt;= versus \u0026gt; predicate.\nfrom typing import List class Solution: def searchRange(self, nums: List[int], target: int) -\u0026gt; List[int]: left = 0 right = len(nums) while left \u0026lt; right: mid = left + (right - left) // 2 if nums[mid] \u0026gt;= target: right = mid else: left = mid + 1 start = left if start == len(nums) or nums[start] != target: return [-1, -1] left = 0 right = len(nums) while left \u0026lt; right: mid = left + (right - left) // 2 if nums[mid] \u0026gt; target: right = mid else: left = mid + 1 first_greater = left end = first_greater - 1 return [start, end] solution = Solution() assert solution.searchRange([5, 7, 7, 8, 8, 10], 8) == [3, 4] assert solution.searchRange([5, 7, 7, 8, 8, 10], 6) == [-1, -1] assert solution.searchRange([], 0) == [-1, -1] assert solution.searchRange([2, 2, 2, 2], 2) == [0, 3] assert solution.searchRange([7], 7) == [0, 0] assert solution.searchRange([7], 8) == [-1, -1] assert solution.searchRange([1, 2, 3], 1) == [0, 0] assert solution.searchRange([1, 2, 3], 3) == [2, 2] unchanged = [5, 7, 7, 8, 8, 10] snapshot = unchanged.copy() assert solution.searchRange(unchanged, 8) == [3, 4] assert unchanged == snapshot Randomized Differential Check Fixed examples can miss combinations of boundaries. Reuse search_range_scan from Step 1 as a correct but slower baseline, generate sorted arrays with a fixed random seed, and compare the two versions. Also verify that every call leaves its input array unchanged.\nIn the same Python session, run the Step 1 baseline and the final implementation above, then run:\nimport random rng = random.Random(34) solution = Solution() for _ in range(1000): length = rng.randint(0, 50) nums = sorted(rng.randint(-10, 10) for _ in range(length)) target = rng.randint(-12, 12) snapshot = nums.copy() assert solution.searchRange(nums, target) == search_range_scan(nums, target) assert nums == snapshot Correctness Proof The start is correct. The first loop always keeps the first boundary satisfying nums[i] \u0026gt;= target between left and right. At termination, start is that boundary, so every value before start is less than target.\nThe absence check is correct and safe. If start == len(nums), no array value is at least target, so the target is absent, and short-circuit evaluation avoids an out-of-bounds read. If start \u0026lt; len(nums) but nums[start] != target, the boundary definition gives nums[start] \u0026gt; target, while every earlier value is less than target, so the target is also absent. Conversely, when the guard does not return, nums[start] == target, and every earlier position is smaller, so start is the first occurrence.\nThe end is correct. The second loop returns first_greater, the first position satisfying nums[i] \u0026gt; target. We already know nums[start] == target. Because the array is sorted, every value from start through first_greater - 1 is both at least and at most target, so all of them equal the target. Since first_greater is the first boundary beyond the target, end = first_greater - 1 is exactly the last occurrence.\nTogether, these three facts make the method return [start, end] when the target exists and [-1, -1] otherwise.\nComplexity Each half-open binary search takes O(log n) time. The constant number of checks and calculations takes O(1) time, so the total time complexity is O(log n). The algorithm uses only a fixed number of integer variables and allocates no input-sized auxiliary structure, so its auxiliary space complexity is O(1).\nSummary The linear scan first provides a correct baseline against which the optimized result can be checked, but it requires O(n) time in the worst case. The first position satisfying nums[i] \u0026gt;= target gives the candidate start; the safe length and equality guard determines whether the target exists. The first position satisfying nums[i] \u0026gt; target lies after the target block, so subtracting one gives the last occurrence. Both searches use [left, right), but equality is handled differently: the first search keeps the current index and continues left, while the second discards it and continues right. The final Solution.searchRange returns the complete range in O(log n) time and O(1) auxiliary space without modifying the input array. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/binary-search/34-find-first-and-last-position-of-element-in-sorted-array/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eStart with the official input \u003ccode\u003enums = [5,7,7,8,8,10]\u003c/code\u003e and \u003ccode\u003etarget = 8\u003c/code\u003e. The answer must be the complete range \u003ccode\u003e[3,4]\u003c/code\u003e; returning only index \u003ccode\u003e3\u003c/code\u003e or \u003ccode\u003e4\u003c/code\u003e does not identify both the target\u0026rsquo;s first and last occurrences.\u003c/p\u003e\n\u003cp\u003eGiven an integer array \u003ccode\u003enums\u003c/code\u003e sorted in non-decreasing order and an integer \u003ccode\u003etarget\u003c/code\u003e:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eIf \u003ccode\u003etarget\u003c/code\u003e exists, return the indices of its first and last occurrences as \u003ccode\u003e[first, last]\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eIf \u003ccode\u003etarget\u003c/code\u003e does not exist, return \u003ccode\u003e[-1, -1]\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eThe problem ultimately requires an algorithm with \u003ccode\u003eO(log n)\u003c/code\u003e runtime.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eLeetCode uses this method contract:\u003c/p\u003e","title":"LeetCode 34: Find First and Last Position of Element in Sorted Array"},{"content":" Subtitle / Summary\nSearch Insert Position is the cleanest lower-bound problem in LeetCode. If you can reliably find the first index where nums[i] \u0026gt;= target, you already have the core template for insert positions, range starts, and many boundary-search problems.\nReading time: 10-12 min Tags: binary search, lower bound, sorted array SEO keywords: Search Insert Position, lower bound, binary search, LeetCode 35 Meta description: Lower-bound binary search for Search Insert Position, with boundary reasoning, pitfalls, engineering scenarios, and runnable implementations in Python, C, C++, Go, Rust, and JavaScript. Target Readers Learners who know basic binary search but still hesitate on boundary handling Engineers who insert or locate values in sorted tables Interview candidates who want one reusable lower-bound template Background / Motivation This problem looks simple because the output is a single index.\nThe real lesson is deeper:\nif the target exists, return its index if it does not exist, return the insertion position Those two requirements can be unified into one question:\nWhat is the first index whose value is greater than or equal to target?\nThat question is exactly lower_bound.\nOnce that idea is stable, a large family of binary-search problems becomes easier:\ninsert position first occurrence range start counting \u0026lt; target or \u0026gt;= target Core Concepts Sorted array: binary search only works because order gives a monotonic decision rule Lower bound: the first index i such that nums[i] \u0026gt;= target Half-open interval: using [l, r) keeps the code concise and avoids fencepost bugs Monotonic predicate: nums[i] \u0026gt;= target is false ... false, true ... true A - Algorithm Problem Restatement Given a non-decreasing integer array nums and an integer target:\nreturn the index of target if it exists otherwise return the index where target should be inserted to keep the array sorted The required time complexity is O(log n).\nInput / Output Name Type Meaning nums int[] sorted array in non-decreasing order target int value to search or insert return int existing index or insertion index Example 1 nums = [1, 3, 5, 6] target = 5 output = 2 Example 2 nums = [1, 3, 5, 6] target = 2 output = 1 Example 3 nums = [1, 3, 5, 6] target = 7 output = 4 Example 4 nums = [1, 3, 5, 6] target = 0 output = 0 Thought Process: From Linear Scan to Lower Bound The brute-force idea is simple:\nscan from left to right stop at the first value \u0026gt;= target return that index if nothing qualifies, return n That works, but it costs O(n).\nBecause the array is already sorted, the condition\nnums[i] \u0026gt;= target forms a monotonic pattern:\nfalse false false true true true Binary search is exactly the tool for finding the first true.\nC - Concepts Method Category Binary search Boundary search Lower-bound template Why the Same Index Solves Both Cases If target exists, the first position with nums[i] \u0026gt;= target is the first position where nums[i] == target.\nIf target does not exist, the first position with nums[i] \u0026gt;= target is the place where target must be inserted.\nSo one return value handles both outcomes.\nStable Template Use a half-open interval [l, r):\ninitialize l = 0, r = len(nums) while l \u0026lt; r: let mid = l + (r - l) // 2 if nums[mid] \u0026gt;= target, keep the answer on the left: r = mid otherwise move right: l = mid + 1 return l Why [l, r) Is Convenient r can safely start at len(nums) the insertion-at-end case naturally returns len(nums) the loop invariant is easy: the answer always stays inside [l, r) E - Engineering Scenario 1: Threshold Tables in Backend Services (Python) Background: a service stores sorted thresholds such as latency buckets or score cutoffs.\nWhy it fits: you need the first threshold that is not smaller than the incoming value.\ndef search_insert(nums, target): l, r = 0, len(nums) while l \u0026lt; r: mid = (l + r) // 2 if nums[mid] \u0026gt;= target: r = mid else: l = mid + 1 return l thresholds = [10, 30, 60, 100] for x in [5, 10, 25, 70, 101]: print(x, \u0026#34;-\u0026gt; slot\u0026#34;, search_insert(thresholds, x)) Scenario 2: Pricing or Risk Tiers (Go) Background: an order amount must be mapped into the correct sorted tier table.\nWhy it fits: insert position is exactly the tier index.\npackage main import \u0026#34;fmt\u0026#34; func searchInsert(nums []int, target int) int { l, r := 0, len(nums) for l \u0026lt; r { mid := l + (r-l)/2 if nums[mid] \u0026gt;= target { r = mid } else { l = mid + 1 } } return l } func main() { tiers := []int{1000, 5000, 10000, 50000} for _, amount := range []int{500, 1000, 2000, 20000} { fmt.Println(amount, \u0026#34;-\u0026gt; tier\u0026#34;, searchInsert(tiers, amount)) } } Scenario 3: Frontend Timeline or Version Selection (JavaScript) Background: a UI highlights the first version not smaller than the current version.\nWhy it fits: the highlighted node is a lower-bound lookup.\nfunction searchInsert(nums, target) { let l = 0, r = nums.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (nums[mid] \u0026gt;= target) r = mid; else l = mid + 1; } return l; } console.log(searchInsert([1, 3, 5, 6], 5)); // 2 console.log(searchInsert([1, 3, 5, 6], 4)); // 2 R - Reflection Complexity Time: O(log n) Space: O(1) Alternatives Linear scan: simpler, but O(n) and misses the point of the sorted input Library lower_bound / bisect_left: excellent in production, but you still need the boundary model to use them correctly Common Mistakes Using \u0026gt; instead of \u0026gt;=, which turns the answer into an upper bound Returning immediately when nums[mid] == target, which loses the insertion-position interpretation Mixing interval styles, for example using [l, r) updates with a [l, r] initialization Why This Is the Best Practical Method The array is sorted, the predicate is monotonic, and the return value is a boundary index.\nThat is exactly the shape binary search is designed for, so this is both the optimal asymptotic solution and the cleanest engineering template.\nS - Summary Search Insert Position is a pure lower-bound problem. The answer is the first index where nums[i] \u0026gt;= target. A half-open interval [l, r) makes the boundary logic stable. This template directly extends to range queries, counts, and insertion-point lookups in real systems. Further Reading LeetCode 34: Find First and Last Position of Element in Sorted Array LeetCode 744: Find Smallest Letter Greater Than Target Python bisect_left, C++ lower_bound, and Go sort.Search Multi-language Implementations Python from typing import List def search_insert(nums: List[int], target: int) -\u0026gt; int: l, r = 0, len(nums) while l \u0026lt; r: mid = (l + r) // 2 if nums[mid] \u0026gt;= target: r = mid else: l = mid + 1 return l if __name__ == \u0026#34;__main__\u0026#34;: print(search_insert([1, 3, 5, 6], 5)) # 2 print(search_insert([1, 3, 5, 6], 2)) # 1 print(search_insert([1, 3, 5, 6], 7)) # 4 print(search_insert([1, 3, 5, 6], 0)) # 0 C #include \u0026lt;stdio.h\u0026gt; int searchInsert(int *nums, int numsSize, int target) { int l = 0, r = numsSize; while (l \u0026lt; r) { int mid = l + (r - l) / 2; if (nums[mid] \u0026gt;= target) { r = mid; } else { l = mid + 1; } } return l; } int main(void) { int nums[] = {1, 3, 5, 6}; int n = sizeof(nums) / sizeof(nums[0]); printf(\u0026#34;%d\\n\u0026#34;, searchInsert(nums, n, 5)); printf(\u0026#34;%d\\n\u0026#34;, searchInsert(nums, n, 2)); printf(\u0026#34;%d\\n\u0026#34;, searchInsert(nums, n, 7)); printf(\u0026#34;%d\\n\u0026#34;, searchInsert(nums, n, 0)); return 0; } C++ #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; int searchInsert(const vector\u0026lt;int\u0026gt;\u0026amp; nums, int target) { int l = 0, r = (int)nums.size(); while (l \u0026lt; r) { int mid = l + (r - l) / 2; if (nums[mid] \u0026gt;= target) { r = mid; } else { l = mid + 1; } } return l; } int main() { vector\u0026lt;int\u0026gt; nums{1, 3, 5, 6}; cout \u0026lt;\u0026lt; searchInsert(nums, 5) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; cout \u0026lt;\u0026lt; searchInsert(nums, 2) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; cout \u0026lt;\u0026lt; searchInsert(nums, 7) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; cout \u0026lt;\u0026lt; searchInsert(nums, 0) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } Go package main import \u0026#34;fmt\u0026#34; func searchInsert(nums []int, target int) int { l, r := 0, len(nums) for l \u0026lt; r { mid := l + (r-l)/2 if nums[mid] \u0026gt;= target { r = mid } else { l = mid + 1 } } return l } func main() { nums := []int{1, 3, 5, 6} fmt.Println(searchInsert(nums, 5)) fmt.Println(searchInsert(nums, 2)) fmt.Println(searchInsert(nums, 7)) fmt.Println(searchInsert(nums, 0)) } Rust fn search_insert(nums: \u0026amp;[i32], target: i32) -\u0026gt; usize { let (mut l, mut r) = (0usize, nums.len()); while l \u0026lt; r { let mid = l + (r - l) / 2; if nums[mid] \u0026gt;= target { r = mid; } else { l = mid + 1; } } l } fn main() { let nums = vec![1, 3, 5, 6]; println!(\u0026#34;{}\u0026#34;, search_insert(\u0026amp;nums, 5)); println!(\u0026#34;{}\u0026#34;, search_insert(\u0026amp;nums, 2)); println!(\u0026#34;{}\u0026#34;, search_insert(\u0026amp;nums, 7)); println!(\u0026#34;{}\u0026#34;, search_insert(\u0026amp;nums, 0)); } JavaScript function searchInsert(nums, target) { let l = 0, r = nums.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (nums[mid] \u0026gt;= target) { r = mid; } else { l = mid + 1; } } return l; } console.log(searchInsert([1, 3, 5, 6], 5)); console.log(searchInsert([1, 3, 5, 6], 2)); console.log(searchInsert([1, 3, 5, 6], 7)); console.log(searchInsert([1, 3, 5, 6], 0)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/binary-search/35-search-insert-position/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nSearch Insert Position is the cleanest lower-bound problem in LeetCode. If you can reliably find the first index where \u003ccode\u003enums[i] \u0026gt;= target\u003c/code\u003e, you already have the core template for insert positions, range starts, and many boundary-search problems.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003ebinary search\u003c/code\u003e, \u003ccode\u003elower bound\u003c/code\u003e, \u003ccode\u003esorted array\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Search Insert Position, lower bound, binary search, LeetCode 35\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Lower-bound binary search for Search Insert Position, with boundary reasoning, pitfalls, engineering scenarios, and runnable implementations in Python, C, C++, Go, Rust, and JavaScript.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLearners who know basic binary search but still hesitate on boundary handling\u003c/li\u003e\n\u003cli\u003eEngineers who insert or locate values in sorted tables\u003c/li\u003e\n\u003cli\u003eInterview candidates who want one reusable lower-bound template\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eThis problem looks simple because the output is a single index.\u003cbr\u003e\nThe real lesson is deeper:\u003c/p\u003e","title":"LeetCode 35: Search Insert Position Lower-Bound Binary Search ACERS Guide"},{"content":" Subtitle / Summary\nThis problem is a textbook upper-bound search with one extra twist: wrap-around. Once you can find the first character \u0026gt; target, the rest is just handling the “no answer inside the array” case by returning the first element.\nReading time: 10-12 min Tags: binary search, upper bound, characters, wrap-around SEO keywords: Find Smallest Letter Greater Than Target, upper bound, LeetCode 744 Meta description: Use upper-bound binary search and wrap-around handling to solve LeetCode 744, with correctness reasoning, pitfalls, engineering scenarios, and runnable code in six languages. Target Readers Learners who already know lower bound and want to master upper bound Engineers who search the next greater value in a sorted cyclic list Interview candidates practicing boundary-style binary search Background / Motivation At first glance, this looks like a character problem.\nIt is actually a boundary problem:\nfind the first element strictly greater than target if it does not exist, wrap to the first element That exact shape appears in many systems:\nnext version after the current version next shard or route after the current key next allowed symbol in a cyclic ordered set So the real concept is not “letters.”\nIt is upper bound plus wrap-around.\nCore Concepts Upper bound: first index i such that letters[i] \u0026gt; target Wrap-around: if no such index exists, answer letters[0] Sorted array: gives the monotonic rule required by binary search A - Algorithm Problem Restatement You are given a sorted list of lowercase letters letters and a target character target.\nReturn the smallest character in letters that is strictly greater than target.\nThe array is considered circular, so:\nif every character is \u0026lt;= target return the first character in the array Input / Output Name Type Meaning letters char[] sorted lowercase letters target char current character return char smallest letter strictly greater than target Example 1 letters = [\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;] target = \u0026#39;a\u0026#39; output = \u0026#39;c\u0026#39; Example 2 letters = [\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;] target = \u0026#39;c\u0026#39; output = \u0026#39;f\u0026#39; Example 3 letters = [\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;] target = \u0026#39;j\u0026#39; output = \u0026#39;c\u0026#39; Thought Process: From Scan to Upper Bound The direct approach is:\nscan left to right return the first character \u0026gt; target if none exists, return the first character That is O(n).\nBecause letters is sorted, the predicate\nletters[i] \u0026gt; target changes from false to true exactly once.\nThat means binary search can find the first true in O(log n).\nC - Concepts Method Category Binary search Upper-bound boundary search Circular fallback logic Why Strictly Greater Matters This problem is not asking for:\nfirst \u0026gt;= target It asks for:\nfirst \u0026gt; target That one symbol difference decides whether you need lower bound or upper bound.\nStable Algorithm run upper-bound binary search to find the first index where letters[i] \u0026gt; target if the index is inside the array, return letters[index] otherwise return letters[0] Boundary Invariant Using [l, r):\nif letters[mid] \u0026gt; target, keep the answer in the left half otherwise move right At the end, l is the first valid index, or len(letters) if no valid index exists.\nE - Engineering Scenario 1: Next Version Selection (Python) Background: versions are kept in sorted order and you need the next greater version marker.\nWhy it fits: the answer is an upper bound in a cyclic list.\ndef next_greater(items, target): l, r = 0, len(items) while l \u0026lt; r: mid = (l + r) // 2 if items[mid] \u0026gt; target: r = mid else: l = mid + 1 return items[l] if l \u0026lt; len(items) else items[0] print(next_greater([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;a\u0026#34;)) print(next_greater([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;j\u0026#34;)) Scenario 2: Sorted Cyclic Routing Table (Go) Background: routes are stored in sorted order and requests move to the next available slot.\nWhy it fits: if the current route is at the end, the system wraps to the first slot.\npackage main import \u0026#34;fmt\u0026#34; func nextGreater(items []byte, target byte) byte { l, r := 0, len(items) for l \u0026lt; r { mid := l + (r-l)/2 if items[mid] \u0026gt; target { r = mid } else { l = mid + 1 } } if l \u0026lt; len(items) { return items[l] } return items[0] } func main() { fmt.Printf(\u0026#34;%c\\n\u0026#34;, nextGreater([]byte{\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;}, \u0026#39;c\u0026#39;)) fmt.Printf(\u0026#34;%c\\n\u0026#34;, nextGreater([]byte{\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;}, \u0026#39;j\u0026#39;)) } Scenario 3: Frontend Keyboard Hinting (JavaScript) Background: a UI suggests the next available sorted symbol after the current input.\nWhy it fits: strict-next plus wrap-around is the same model.\nfunction nextGreatestLetter(letters, target) { let l = 0, r = letters.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (letters[mid] \u0026gt; target) r = mid; else l = mid + 1; } return l \u0026lt; letters.length ? letters[l] : letters[0]; } console.log(nextGreatestLetter([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;c\u0026#34;)); // f console.log(nextGreatestLetter([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;j\u0026#34;)); // c R - Reflection Complexity Time: O(log n) Space: O(1) Alternatives Linear scan: easy, but slower on large arrays Modulo tricks without binary search: not useful because the key challenge is still finding the upper bound Common Mistakes Using \u0026gt;= instead of \u0026gt;, which returns the wrong answer when target exists Forgetting the wrap-around case Returning letters[l - 1] or another neighbor without proving it Why This Is the Best Practical Method The array is sorted, the condition is monotonic, and the fallback is simple.\nThat makes upper-bound binary search plus one wrap-around check the cleanest and most reusable solution.\nS - Summary This is a pure upper-bound problem with a cyclic fallback. The correct search condition is letters[i] \u0026gt; target, not \u0026gt;=. If no valid index exists, the answer is the first element. The same pattern appears in version routing, cyclic lookup, and next-greater selection problems. Further Reading LeetCode 35: Search Insert Position LeetCode 34: Find First and Last Position of Element in Sorted Array Standard-library upper-bound helpers such as bisect_right and upper_bound Multi-language Implementations Python from typing import List def next_greatest_letter(letters: List[str], target: str) -\u0026gt; str: l, r = 0, len(letters) while l \u0026lt; r: mid = (l + r) // 2 if letters[mid] \u0026gt; target: r = mid else: l = mid + 1 return letters[l] if l \u0026lt; len(letters) else letters[0] if __name__ == \u0026#34;__main__\u0026#34;: print(next_greatest_letter([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;a\u0026#34;)) print(next_greatest_letter([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;c\u0026#34;)) print(next_greatest_letter([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;j\u0026#34;)) C #include \u0026lt;stdio.h\u0026gt; char nextGreatestLetter(char *letters, int n, char target) { int l = 0, r = n; while (l \u0026lt; r) { int mid = l + (r - l) / 2; if (letters[mid] \u0026gt; target) { r = mid; } else { l = mid + 1; } } return l \u0026lt; n ? letters[l] : letters[0]; } int main(void) { char letters[] = {\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;}; printf(\u0026#34;%c\\n\u0026#34;, nextGreatestLetter(letters, 3, \u0026#39;a\u0026#39;)); printf(\u0026#34;%c\\n\u0026#34;, nextGreatestLetter(letters, 3, \u0026#39;c\u0026#39;)); printf(\u0026#34;%c\\n\u0026#34;, nextGreatestLetter(letters, 3, \u0026#39;j\u0026#39;)); return 0; } C++ #include \u0026lt;algorithm\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; char nextGreatestLetter(const vector\u0026lt;char\u0026gt;\u0026amp; letters, char target) { auto it = upper_bound(letters.begin(), letters.end(), target); return it == letters.end() ? letters[0] : *it; } int main() { vector\u0026lt;char\u0026gt; letters{\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;}; cout \u0026lt;\u0026lt; nextGreatestLetter(letters, \u0026#39;a\u0026#39;) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; cout \u0026lt;\u0026lt; nextGreatestLetter(letters, \u0026#39;c\u0026#39;) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; cout \u0026lt;\u0026lt; nextGreatestLetter(letters, \u0026#39;j\u0026#39;) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } Go package main import \u0026#34;fmt\u0026#34; func nextGreatestLetter(letters []byte, target byte) byte { l, r := 0, len(letters) for l \u0026lt; r { mid := l + (r-l)/2 if letters[mid] \u0026gt; target { r = mid } else { l = mid + 1 } } if l \u0026lt; len(letters) { return letters[l] } return letters[0] } func main() { fmt.Printf(\u0026#34;%c\\n\u0026#34;, nextGreatestLetter([]byte{\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;}, \u0026#39;a\u0026#39;)) fmt.Printf(\u0026#34;%c\\n\u0026#34;, nextGreatestLetter([]byte{\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;}, \u0026#39;c\u0026#39;)) fmt.Printf(\u0026#34;%c\\n\u0026#34;, nextGreatestLetter([]byte{\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;}, \u0026#39;j\u0026#39;)) } Rust fn next_greatest_letter(letters: \u0026amp;[char], target: char) -\u0026gt; char { let (mut l, mut r) = (0usize, letters.len()); while l \u0026lt; r { let mid = l + (r - l) / 2; if letters[mid] \u0026gt; target { r = mid; } else { l = mid + 1; } } if l \u0026lt; letters.len() { letters[l] } else { letters[0] } } fn main() { let letters = vec![\u0026#39;c\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;j\u0026#39;]; println!(\u0026#34;{}\u0026#34;, next_greatest_letter(\u0026amp;letters, \u0026#39;a\u0026#39;)); println!(\u0026#34;{}\u0026#34;, next_greatest_letter(\u0026amp;letters, \u0026#39;c\u0026#39;)); println!(\u0026#34;{}\u0026#34;, next_greatest_letter(\u0026amp;letters, \u0026#39;j\u0026#39;)); } JavaScript function nextGreatestLetter(letters, target) { let l = 0, r = letters.length; while (l \u0026lt; r) { const mid = (l + r) \u0026gt;\u0026gt; 1; if (letters[mid] \u0026gt; target) r = mid; else l = mid + 1; } return l \u0026lt; letters.length ? letters[l] : letters[0]; } console.log(nextGreatestLetter([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;a\u0026#34;)); console.log(nextGreatestLetter([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;c\u0026#34;)); console.log(nextGreatestLetter([\u0026#34;c\u0026#34;, \u0026#34;f\u0026#34;, \u0026#34;j\u0026#34;], \u0026#34;j\u0026#34;)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/binary-search/744-find-smallest-letter-greater-than-target/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThis problem is a textbook upper-bound search with one extra twist: wrap-around. Once you can find the first character \u003ccode\u003e\u0026gt; target\u003c/code\u003e, the rest is just handling the “no answer inside the array” case by returning the first element.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003ebinary search\u003c/code\u003e, \u003ccode\u003eupper bound\u003c/code\u003e, \u003ccode\u003echaracters\u003c/code\u003e, \u003ccode\u003ewrap-around\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Find Smallest Letter Greater Than Target, upper bound, LeetCode 744\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Use upper-bound binary search and wrap-around handling to solve LeetCode 744, with correctness reasoning, pitfalls, engineering scenarios, and runnable code in six languages.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLearners who already know lower bound and want to master upper bound\u003c/li\u003e\n\u003cli\u003eEngineers who search the next greater value in a sorted cyclic list\u003c/li\u003e\n\u003cli\u003eInterview candidates practicing boundary-style binary search\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eAt first glance, this looks like a character problem.\u003cbr\u003e\nIt is actually a boundary problem:\u003c/p\u003e","title":"LeetCode 744: Find Smallest Letter Greater Than Target Upper-Bound ACERS Guide"},{"content":" Subtitle / Summary\nLevel order traversal is the entry point of the binary-tree BFS template. The real key is not merely \u0026ldquo;use a queue\u0026rdquo;, but \u0026ldquo;separate one level from the next correctly\u0026rdquo;. This ACERS guide explains the level-size pattern, the DFS depth-bucket alternative, and engineering situations where grouped-by-depth traversal is useful.\nReading time: 10-12 min Tags: Hot100, binary tree, BFS, DFS, queue, level order traversal SEO keywords: Hot100, Binary Tree Level Order Traversal, BFS, queue, level order traversal, LeetCode 102 Meta description: A systematic guide to LeetCode 102 from level-by-level BFS to DFS depth buckets, with engineering scenarios and runnable multi-language implementations. Target Readers Hot100 learners who want to make the BFS tree template stable Developers who can traverse a tree but still mix up current-level and next-level boundaries Engineers who need to group tree-shaped data by depth for display or execution Background / Motivation LeetCode 102 is one of the most standard tree-BFS starter problems.\nWhat it really trains is not just \u0026ldquo;visit all nodes\u0026rdquo;, but two more important skills:\nuse a queue to maintain the next batch of nodes to process separate the current level from the next level cleanly Many BFS bugs come exactly from this boundary issue:\nusing the changing queue.length directly while iterating the current level mixing newly pushed children into the current level\u0026rsquo;s answer forgetting the empty-tree check and touching null immediately If you stabilize the 102 template, later problems like:\nright side view average of levels zigzag level order traversal minimum depth or maximum depth via BFS become much easier.\nCore Concepts Level order traversal: visit nodes level by level from top to bottom and left to right BFS (breadth-first search): process the current layer first, then expand the next layer Level-size snapshot: record the queue length before processing a level; that number is exactly how many nodes belong to this level Depth bucket: the DFS alternative, where values are stored in res[depth] A - Algorithm (Problem and Algorithm) Problem Restatement Given the root node root of a binary tree, return the level order traversal of its node values.\nIn other words, return the values level by level from left to right.\nInput / Output Name Type Description root TreeNode root of the binary tree, may be null return List[List[int]] node values grouped by level Example 1 input: root = [3,9,20,null,null,15,7] output: [[3],[9,20],[15,7]] explanation: level 1 -\u0026gt; [3] level 2 -\u0026gt; [9,20] level 3 -\u0026gt; [15,7] Example 2 input: root = [1] output: [[1]] Example 3 input: root = [] output: [] Constraints The number of nodes is in the range [0, 2000] -1000 \u0026lt;= Node.val \u0026lt;= 1000 C - Concepts (Core Ideas) Thought Process: The key is not the queue itself, but the level boundary If the task were only \u0026ldquo;visit every node\u0026rdquo;, ordinary BFS would be enough.\nBut this problem asks for a grouped result like [[level1], [level2], ...], so you must know:\nwhich nodes popped from the queue belong to the current level which children should be saved for the next round The most stable pattern is:\nbefore processing a level, record level_size = len(queue) pop exactly level_size nodes place the values of those nodes into the same level array push their children into the queue for the next round Why we must record level_size first Because while you process the current level, you keep pushing children from the next level into the same queue.\nIf you use the changing queue length directly as the loop condition, the current level and the next level will get mixed together.\nMethod Category BFS with queue Level grouping DFS with depth buckets (alternative) Why DFS can also work If you use DFS, carry the current depth depth in the recursive call:\nif depth == len(res), create a new bucket for that level append the current value to res[depth] That also produces a grouped-by-level result, although BFS is the more direct first choice for this problem.\nPractice Guide / Steps Recommended Approach: Level-by-level BFS Return [] immediately if the root is null Initialize a queue with the root At the start of each round, record level_size Pop exactly level_size nodes and collect their values Push their children into the queue Append the finished level array to the answer Runnable Python example:\nfrom collections import deque class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def level_order(root): if root is None: return [] ans = [] q = deque([root]) while q: level_size = len(q) level = [] for _ in range(level_size): node = q.popleft() level.append(node.val) if node.left is not None: q.append(node.left) if node.right is not None: q.append(node.right) ans.append(level) return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7))) print(level_order(root)) DFS Alternative If you want to practice the depth-bucket idea:\ncarry depth in recursion when depth == len(res), create a new level array append the current node value to res[depth] This is handy when the same traversal also needs other DFS-style statistics, but for LeetCode 102, BFS is more intuitive.\nE - Engineering (Real-world Scenarios) Scenario 1: Group an org chart by management level (Python) Background: reporting structures and org charts are naturally tree-shaped.\nWhy it fits: UI displays often need data grouped by CEO, VP, director, and manager layers.\nfrom collections import deque def group_by_level(root): if root is None: return [] q = deque([root]) ans = [] while q: level = [] for _ in range(len(q)): node = q.popleft() level.append(node[\u0026#34;name\u0026#34;]) for child in node.get(\u0026#34;children\u0026#34;, []): q.append(child) ans.append(level) return ans org = {\u0026#34;name\u0026#34;: \u0026#34;CEO\u0026#34;, \u0026#34;children\u0026#34;: [{\u0026#34;name\u0026#34;: \u0026#34;VP1\u0026#34;}, {\u0026#34;name\u0026#34;: \u0026#34;VP2\u0026#34;}]} print(group_by_level(org)) Scenario 2: Render a menu tree level by level (JavaScript) Background: admin menus and site navigation trees are often stored as hierarchical configs.\nWhy it fits: some interfaces progressively render or lazy-load one depth at a time to reduce initial complexity.\nfunction levelOrder(root) { if (!root) return []; const queue = [root]; const ans = []; while (queue.length) { const size = queue.length; const level = []; for (let i = 0; i \u0026lt; size; i += 1) { const node = queue.shift(); level.push(node.name); for (const child of node.children || []) queue.push(child); } ans.push(level); } return ans; } const menu = { name: \u0026#34;root\u0026#34;, children: [{ name: \u0026#34;docs\u0026#34;, children: [] }, { name: \u0026#34;blog\u0026#34;, children: [] }] }; console.log(levelOrder(menu)); Scenario 3: Execute tree-shaped tasks wave by wave (Go) Background: some workflow engines treat dependent tasks as a tree.\nWhy it fits: tasks at the same depth can be executed or inspected as one wave before moving to the next depth.\npackage main import \u0026#34;fmt\u0026#34; type Task struct { Name string Children []*Task } func waves(root *Task) [][]string { if root == nil { return [][]string{} } q := []*Task{root} ans := [][]string{} for len(q) \u0026gt; 0 { size := len(q) level := make([]string, 0, size) for i := 0; i \u0026lt; size; i++ { node := q[0] q = q[1:] level = append(level, node.Name) q = append(q, node.Children...) } ans = append(ans, level) } return ans } func main() { root := \u0026amp;Task{ Name: \u0026#34;build\u0026#34;, Children: []*Task{ {Name: \u0026#34;unit-test\u0026#34;}, {Name: \u0026#34;lint\u0026#34;}, }, } fmt.Println(waves(root)) } R - Reflection (Analysis and Deeper Understanding) Complexity Analysis Time complexity: O(n), because each node is pushed and popped once Space complexity: BFS: O(w), where w is the maximum width of the tree DFS depth buckets: O(h) recursion stack, plus the result array itself Alternative Approaches Method Time Extra Space Notes Level-by-level BFS O(n) O(w) Most natural and the recommended template DFS depth buckets O(n) O(h) Works well, but the level-order intuition is less direct Traverse first, regroup later O(n) Extra map or array Possible, but more indirect than grouping during traversal Common Mistakes and Pitfalls Not recording level_size first, so newly added children get mixed into the current level Forgetting to return an empty array when the root is null Defining the level array outside the outer loop and accidentally reusing the same array for all levels In JavaScript, iterating with a changing queue.length and breaking the level boundary Common Questions and Notes 1. Why must we record the queue length before each level starts? Because the queue changes during processing.\nOnly the original queue length at the start of the round tells you how many nodes belong to the current level.\n2. Does this problem have to be solved with BFS? No. DFS with depth tracking also works. But LeetCode 102 is the canonical BFS level-order template, so BFS is the best first answer.\n3. What should an empty tree return? Return [], not [[]].\nBest Practices and Suggestions For every \u0026ldquo;output by level\u0026rdquo; tree problem, think of the level_size template first Keep responsibilities clear: the queue stores nodes, while the level array stores values When practicing DFS, remember the trigger: depth == len(res) means create a new level Problems 102, 107, 199, and 637 make a good mini-series for BFS level-order variations S - Summary The heart of LeetCode 102 is not the queue alone, but the level boundary Recording level_size first is the most important stabilizing technique in the whole problem BFS is the primary template here, while DFS depth buckets are a strong alternative Any tree-shaped data that needs grouping by depth can reuse the same idea Once 102 is stable, the whole family of level-order problems becomes much easier References and Further Reading LeetCode 102: Binary Tree Level Order Traversal LeetCode 104: Maximum Depth of Binary Tree LeetCode 199: Binary Tree Right Side View LeetCode 637: Average of Levels in Binary Tree LeetCode 103: Binary Tree Zigzag Level Order Traversal CTA Practice 102, 107, and 199 together.\nThey are all variations of the same level-by-level BFS template. Only the output rule changes, which makes them ideal for locking in the queue-and-boundary pattern.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) from collections import deque class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def level_order(root): if root is None: return [] ans = [] q = deque([root]) while q: level_size = len(q) level = [] for _ in range(level_size): node = q.popleft() level.append(node.val) if node.left is not None: q.append(node.left) if node.right is not None: q.append(node.right) ans.append(level) return ans if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7))) print(level_order(root)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct LevelOrderResult { int** levels; int* sizes; int count; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } struct LevelOrderResult levelOrder(struct TreeNode* root) { struct LevelOrderResult res = {NULL, NULL, 0}; if (root == NULL) return res; struct TreeNode* queue[4096]; int front = 0; int back = 0; queue[back++] = root; res.levels = (int**)malloc(sizeof(int*) * 2048); res.sizes = (int*)malloc(sizeof(int) * 2048); while (front \u0026lt; back) { int levelSize = back - front; res.levels[res.count] = (int*)malloc(sizeof(int) * levelSize); res.sizes[res.count] = levelSize; for (int i = 0; i \u0026lt; levelSize; ++i) { struct TreeNode* node = queue[front++]; res.levels[res.count][i] = node-\u0026gt;val; if (node-\u0026gt;left) queue[back++] = node-\u0026gt;left; if (node-\u0026gt;right) queue[back++] = node-\u0026gt;right; } res.count++; } return res; } void print_result(struct LevelOrderResult* res) { printf(\u0026#34;[\u0026#34;); for (int i = 0; i \u0026lt; res-\u0026gt;count; ++i) { printf(\u0026#34;[\u0026#34;); for (int j = 0; j \u0026lt; res-\u0026gt;sizes[i]; ++j) { printf(\u0026#34;%d%s\u0026#34;, res-\u0026gt;levels[i][j], j + 1 == res-\u0026gt;sizes[i] ? \u0026#34;\u0026#34; : \u0026#34;,\u0026#34;); } printf(\u0026#34;]%s\u0026#34;, i + 1 == res-\u0026gt;count ? \u0026#34;\u0026#34; : \u0026#34;,\u0026#34;); } printf(\u0026#34;]\\n\u0026#34;); } void free_result(struct LevelOrderResult* res) { if (!res-\u0026gt;levels || !res-\u0026gt;sizes) return; for (int i = 0; i \u0026lt; res-\u0026gt;count; ++i) { free(res-\u0026gt;levels[i]); } free(res-\u0026gt;levels); free(res-\u0026gt;sizes); } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(3); root-\u0026gt;left = new_node(9); root-\u0026gt;right = new_node(20); root-\u0026gt;right-\u0026gt;left = new_node(15); root-\u0026gt;right-\u0026gt;right = new_node(7); struct LevelOrderResult res = levelOrder(root); print_result(\u0026amp;res); free_result(\u0026amp;res); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;queue\u0026gt; #include \u0026lt;vector\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; levelOrder(TreeNode* root) { if (!root) return {}; std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; ans; std::queue\u0026lt;TreeNode*\u0026gt; q; q.push(root); while (!q.empty()) { int size = static_cast\u0026lt;int\u0026gt;(q.size()); std::vector\u0026lt;int\u0026gt; level; for (int i = 0; i \u0026lt; size; ++i) { TreeNode* node = q.front(); q.pop(); level.push_back(node-\u0026gt;val); if (node-\u0026gt;left) q.push(node-\u0026gt;left); if (node-\u0026gt;right) q.push(node-\u0026gt;right); } ans.push_back(level); } return ans; } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(3); root-\u0026gt;left = new TreeNode(9); root-\u0026gt;right = new TreeNode(20); root-\u0026gt;right-\u0026gt;left = new TreeNode(15); root-\u0026gt;right-\u0026gt;right = new TreeNode(7); auto ans = levelOrder(root); std::cout \u0026lt;\u0026lt; \u0026#34;[\u0026#34;; for (size_t i = 0; i \u0026lt; ans.size(); ++i) { std::cout \u0026lt;\u0026lt; \u0026#34;[\u0026#34;; for (size_t j = 0; j \u0026lt; ans[i].size(); ++j) { std::cout \u0026lt;\u0026lt; ans[i][j] \u0026lt;\u0026lt; (j + 1 == ans[i].size() ? \u0026#34;\u0026#34; : \u0026#34;,\u0026#34;); } std::cout \u0026lt;\u0026lt; \u0026#34;]\u0026#34; \u0026lt;\u0026lt; (i + 1 == ans.size() ? \u0026#34;\u0026#34; : \u0026#34;,\u0026#34;); } std::cout \u0026lt;\u0026lt; \u0026#34;]\\n\u0026#34;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func levelOrder(root *TreeNode) [][]int { if root == nil { return [][]int{} } q := []*TreeNode{root} ans := [][]int{} for len(q) \u0026gt; 0 { size := len(q) level := make([]int, 0, size) for i := 0; i \u0026lt; size; i++ { node := q[0] q = q[1:] level = append(level, node.Val) if node.Left != nil { q = append(q, node.Left) } if node.Right != nil { q = append(q, node.Right) } } ans = append(ans, level) } return ans } func main() { root := \u0026amp;TreeNode{ Val: 3, Left: \u0026amp;TreeNode{Val: 9}, Right: \u0026amp;TreeNode{Val: 20, Left: \u0026amp;TreeNode{Val: 15}, Right: \u0026amp;TreeNode{Val: 7}}, } fmt.Println(levelOrder(root)) } use std::cell::RefCell; use std::collections::VecDeque; use std::rc::Rc; type Node = Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;TreeNode\u0026gt;\u0026gt;\u0026gt;; #[derive(Debug, Clone)] struct TreeNode { val: i32, left: Node, right: Node, } impl TreeNode { fn new(val: i32) -\u0026gt; Rc\u0026lt;RefCell\u0026lt;TreeNode\u0026gt;\u0026gt; { Rc::new(RefCell::new(TreeNode { val, left: None, right: None, })) } } fn level_order(root: \u0026amp;Node) -\u0026gt; Vec\u0026lt;Vec\u0026lt;i32\u0026gt;\u0026gt; { let mut ans = Vec::new(); let mut q = VecDeque::new(); if let Some(node) = root { q.push_back(node.clone()); } else { return ans; } while !q.is_empty() { let size = q.len(); let mut level = Vec::with_capacity(size); for _ in 0..size { let node = q.pop_front().unwrap(); let node_ref = node.borrow(); level.push(node_ref.val); if let Some(left) = \u0026amp;node_ref.left { q.push_back(left.clone()); } if let Some(right) = \u0026amp;node_ref.right { q.push_back(right.clone()); } } ans.push(level); } ans } fn main() { let root = Some(TreeNode::new(3)); if let Some(node) = \u0026amp;root { node.borrow_mut().left = Some(TreeNode::new(9)); let right = Some(TreeNode::new(20)); node.borrow_mut().right = right.clone(); if let Some(r) = right { r.borrow_mut().left = Some(TreeNode::new(15)); r.borrow_mut().right = Some(TreeNode::new(7)); } } println!(\u0026#34;{:?}\u0026#34;, level_order(\u0026amp;root)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function levelOrder(root) { if (root === null) return []; const queue = [root]; const ans = []; while (queue.length) { const size = queue.length; const level = []; for (let i = 0; i \u0026lt; size; i += 1) { const node = queue.shift(); level.push(node.val); if (node.left !== null) queue.push(node.left); if (node.right !== null) queue.push(node.right); } ans.push(level); } return ans; } const root = new TreeNode(3, new TreeNode(9), new TreeNode(20, new TreeNode(15), new TreeNode(7))); console.log(levelOrder(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/102-binary-tree-level-order-traversal/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nLevel order traversal is the entry point of the binary-tree BFS template. The real key is not merely \u0026ldquo;use a queue\u0026rdquo;, but \u0026ldquo;separate one level from the next correctly\u0026rdquo;. This ACERS guide explains the level-size pattern, the DFS depth-bucket alternative, and engineering situations where grouped-by-depth traversal is useful.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eBFS\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003equeue\u003c/code\u003e, \u003ccode\u003elevel order traversal\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Hot100, Binary Tree Level Order Traversal, BFS, queue, level order traversal, LeetCode 102\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A systematic guide to LeetCode 102 from level-by-level BFS to DFS depth buckets, with engineering scenarios and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHot100 learners who want to make the BFS tree template stable\u003c/li\u003e\n\u003cli\u003eDevelopers who can traverse a tree but still mix up current-level and next-level boundaries\u003c/li\u003e\n\u003cli\u003eEngineers who need to group tree-shaped data by depth for display or execution\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eLeetCode 102 is one of the most standard tree-BFS starter problems.\u003c/p\u003e","title":"Hot100: Binary Tree Level Order Traversal (BFS / DFS ACERS Guide)"},{"content":" Subtitle / Summary\nThe hard part of Symmetric Tree is not traversal itself, but comparison direction. You are not comparing left to left and right to right. You are comparing mirrored positions. This ACERS guide explains the mirror-recursion contract, the BFS queue-of-pairs variant, and real engineering cases where symmetry checking matters.\nReading time: 10-12 min Tags: Hot100, binary tree, DFS, BFS, symmetry SEO keywords: Hot100, Symmetric Tree, mirror recursion, binary tree symmetry, BFS, LeetCode 101 Meta description: A systematic guide to LeetCode 101 from mirror recursion to pairwise BFS symmetry checks, with engineering scenarios and runnable multi-language implementations. Target Readers Hot100 learners moving from Same Tree to mirror comparison Developers who can write ordinary tree recursion but still mix up outside and inside pairs Engineers who need left-right symmetry validation for layouts, topology templates, or mirrored structures Background / Motivation LeetCode 101 is excellent training for directional thinking in tree problems:\nsymmetry does not mean the left and right subtrees are identical in the same direction it means the left side should match the right side after mirroring the comparison direction changes from \u0026ldquo;same direction\u0026rdquo; to \u0026ldquo;cross direction\u0026rdquo; Most mistakes fall into three groups:\nreusing the Same Tree logic and comparing left.left with right.left checking only node values while ignoring null positions flipping one subtree first, which adds an unnecessary transformation and makes reasoning harder What this problem really trains is the mirror-recursion template. Once that clicks, symmetry, mirror, and structure-matching questions become much easier.\nCore Concepts Mirror relation: left.left should match right.right, and left.right should match right.left Outside / inside pairing: compare the outer children together and the inner children together Pairwise recursion: the helper function answers whether two positions are mirror images Pairwise queueing: in BFS, the queue stores node pairs that must be checked together A - Algorithm (Problem and Algorithm) Problem Restatement Given the root node root of a binary tree, return true if the tree is symmetric around its center.\nA tree is symmetric if its left subtree and right subtree are mirror images of each other.\nInput / Output Name Type Description root TreeNode root of the binary tree return bool whether the tree is symmetric Example 1 input: root = [1,2,2,3,4,4,3] output: true explanation: The left subtree and the right subtree match exactly after mirroring. Example 2 input: root = [1,2,2,null,3,null,3] output: false explanation: The right child of the left subtree and the right child of the right subtree appear on the same side. That is not a mirror relation. Constraints The number of nodes is in the range [1, 1000] -100 \u0026lt;= Node.val \u0026lt;= 100 C - Concepts (Core Ideas) Thought Process: Symmetry means comparing mirrored positions Suppose you are comparing two nodes a and b. For them to be mirrors, the following must all hold:\nBoth are null: this mirrored position matches, so return true Exactly one is null: the structure is broken, so return false Values differ: the mirrored nodes do not match, so return false Both exist and values match: compare a.left with b.right compare a.right with b.left Written as a formula:\nmirror(a, b) = true, if a == null and b == null false, if exactly one is null false, if a.val != b.val mirror(a.left, b.right) and mirror(a.right, b.left), otherwise Why \u0026ldquo;left with left, right with right\u0026rdquo; is wrong here That pattern checks equality, not symmetry.\nThe core difference between 100 and 101 is exactly this:\nLeetCode 100 Same Tree: compare same-direction positions LeetCode 101 Symmetric Tree: compare mirrored positions If you do not switch the direction, you are solving a different problem.\nMethod Category Tree DFS Mirror recursion BFS with paired nodes Structural symmetry checking Why BFS also fits well You can also put mirror pairs into a queue:\nstart with root.left and root.right pop a pair and apply the mirror contract if the pair matches, push: left.left with right.right left.right with right.left This is the same logic as recursion, only written as an explicit process.\nPractice Guide / Steps Recommended Approach: Mirror recursion If root is null, return true Define a helper is_mirror(a, b) Inside the helper, keep the order: both null, one null, value mismatch, recursive mirror checks Return is_mirror(root.left, root.right) Runnable Python example:\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def is_symmetric(root): def is_mirror(a, b): if a is None and b is None: return True if a is None or b is None: return False if a.val != b.val: return False return is_mirror(a.left, b.right) and is_mirror(a.right, b.left) return True if root is None else is_mirror(root.left, root.right) if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode( 1, TreeNode(2, TreeNode(3), TreeNode(4)), TreeNode(2, TreeNode(4), TreeNode(3)), ) print(is_symmetric(root)) BFS Alternative The non-recursive version is straightforward:\nuse a queue of mirror pairs pop two nodes and compare them together if they match, push the outside pair and the inside pair This style is often easier to debug when you want to print the first non-symmetric pair explicitly.\nE - Engineering (Real-world Scenarios) Scenario 1: Validate mirrored two-column layouts (JavaScript) Background: visual editors often ship left-right mirrored page templates.\nWhy it fits: before publishing a template, you may want to ensure the two sides are strict mirror images to avoid broken placements.\nfunction isMirror(a, b) { if (!a \u0026amp;\u0026amp; !b) return true; if (!a || !b) return false; if (a.type !== b.type) return false; return isMirror(a.left, b.right) \u0026amp;\u0026amp; isMirror(a.right, b.left); } const left = { type: \u0026#34;Split\u0026#34;, left: { type: \u0026#34;Menu\u0026#34; }, right: { type: \u0026#34;Detail\u0026#34; } }; const right = { type: \u0026#34;Split\u0026#34;, left: { type: \u0026#34;Detail\u0026#34; }, right: { type: \u0026#34;Menu\u0026#34; } }; console.log(isMirror(left, right)); Scenario 2: Check active-active topology symmetry (Python) Background: some dual-site deployments require the left and right data-center templates to mirror each other in role and hierarchy.\nWhy it fits: before rollout, symmetry checks can catch missing nodes or role drift on one side.\ndef mirror_role(a, b): if a is None and b is None: return True if a is None or b is None: return False if a[\u0026#34;role\u0026#34;] != b[\u0026#34;role\u0026#34;]: return False return mirror_role(a.get(\u0026#34;left\u0026#34;), b.get(\u0026#34;right\u0026#34;)) and mirror_role(a.get(\u0026#34;right\u0026#34;), b.get(\u0026#34;left\u0026#34;)) left_dc = {\u0026#34;role\u0026#34;: \u0026#34;gateway\u0026#34;, \u0026#34;left\u0026#34;: {\u0026#34;role\u0026#34;: \u0026#34;api\u0026#34;}, \u0026#34;right\u0026#34;: {\u0026#34;role\u0026#34;: \u0026#34;db\u0026#34;}} right_dc = {\u0026#34;role\u0026#34;: \u0026#34;gateway\u0026#34;, \u0026#34;left\u0026#34;: {\u0026#34;role\u0026#34;: \u0026#34;db\u0026#34;}, \u0026#34;right\u0026#34;: {\u0026#34;role\u0026#34;: \u0026#34;api\u0026#34;}} print(mirror_role(left_dc, right_dc)) Scenario 3: Mirror-tree grading in an education tool (Go) Background: algorithm teaching systems sometimes ask students to build a tree that mirrors a target structure.\nWhy it fits: grading must check both node values and mirrored positions, not just a flat traversal result.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Val int Left *Node Right *Node } func mirror(a, b *Node) bool { if a == nil \u0026amp;\u0026amp; b == nil { return true } if a == nil || b == nil { return false } if a.Val != b.Val { return false } return mirror(a.Left, b.Right) \u0026amp;\u0026amp; mirror(a.Right, b.Left) } func main() { left := \u0026amp;Node{Val: 2, Left: \u0026amp;Node{Val: 3}, Right: \u0026amp;Node{Val: 4}} right := \u0026amp;Node{Val: 2, Left: \u0026amp;Node{Val: 4}, Right: \u0026amp;Node{Val: 3}} fmt.Println(mirror(left, right)) } R - Reflection (Analysis and Deeper Understanding) Complexity Analysis Time complexity: O(n), because each node is compared at most once Space complexity: Recursive DFS: O(h), where h is the tree height BFS queue: worst-case O(w), where w is the maximum width of a level Alternative Approaches Method Time Extra Space Notes Mirror recursion O(n) O(h) Most aligned with the definition and usually the best answer BFS with paired nodes O(n) O(w) Explicit flow, easy to instrument for debugging Invert one side then compare O(n) O(h) or O(w) Adds an extra transformation and may mutate the tree Serialize and compare mirror order O(n) O(n) More cumbersome and still needs null markers Common Mistakes and Pitfalls Writing LeetCode 101 as if it were LeetCode 100, still comparing left.left with right.left Comparing only values and ignoring null positions Flipping a subtree first, which is unnecessary and can introduce side effects Storing single nodes in the BFS queue instead of storing comparison pairs Common Questions and Notes 1. Is a single-node tree symmetric? Yes. Its left and right subtrees are both null, so they are mirror images by definition.\n2. Why not invert the left subtree and then compare it with the right subtree? You can, but it is not recommended here. It adds an extra transformation step, makes the reasoning longer, and can modify the original tree.\n3. How should I choose between DFS and BFS? For learning and interviews, recursion is usually the clearest form. If you want explicit logging of failing pairs or want to avoid deep recursion, BFS is a good alternative.\nBest Practices and Suggestions Before coding, draw the outside-pair and inside-pair relation on paper once Memorize the template: left.left with right.right, left.right with right.left Practice 100 and 101 as a pair to build directional intuition quickly If mirror recursion still feels slippery, write the BFS pair queue once to make the pairing explicit S - Summary The core of Symmetric Tree is mirror-position comparison, not traversal by itself Once you keep the outside-inside mirror contract stable, the recursion becomes reliable The BFS version is conceptually identical; it just makes the node pairs explicit This problem pairs naturally with 100 and 226 when building tree-structure intuition In engineering work, the same idea applies to mirrored layouts, mirrored templates, and symmetric topologies References and Further Reading LeetCode 101: Symmetric Tree LeetCode 100: Same Tree LeetCode 226: Invert Binary Tree LeetCode 104: Maximum Depth of Binary Tree LeetCode 102: Binary Tree Level Order Traversal CTA Practice 100, 101, and 226 as a small bundle.\n100 trains same-direction comparison, 101 trains mirror-direction comparison, and 226 trains structural transformation. Together, they build strong binary-tree intuition quickly.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def is_symmetric(root): def is_mirror(a, b): if a is None and b is None: return True if a is None or b is None: return False if a.val != b.val: return False return is_mirror(a.left, b.right) and is_mirror(a.right, b.left) return True if root is None else is_mirror(root.left, root.right) if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode( 1, TreeNode(2, TreeNode(3), TreeNode(4)), TreeNode(2, TreeNode(4), TreeNode(3)), ) print(is_symmetric(root)) #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } bool isMirror(struct TreeNode* a, struct TreeNode* b) { if (a == NULL \u0026amp;\u0026amp; b == NULL) return true; if (a == NULL || b == NULL) return false; if (a-\u0026gt;val != b-\u0026gt;val) return false; return isMirror(a-\u0026gt;left, b-\u0026gt;right) \u0026amp;\u0026amp; isMirror(a-\u0026gt;right, b-\u0026gt;left); } bool isSymmetric(struct TreeNode* root) { if (root == NULL) return true; return isMirror(root-\u0026gt;left, root-\u0026gt;right); } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(1); root-\u0026gt;left = new_node(2); root-\u0026gt;right = new_node(2); root-\u0026gt;left-\u0026gt;left = new_node(3); root-\u0026gt;left-\u0026gt;right = new_node(4); root-\u0026gt;right-\u0026gt;left = new_node(4); root-\u0026gt;right-\u0026gt;right = new_node(3); printf(\u0026#34;%s\\n\u0026#34;, isSymmetric(root) ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; bool isMirror(TreeNode* a, TreeNode* b) { if (!a \u0026amp;\u0026amp; !b) return true; if (!a || !b) return false; if (a-\u0026gt;val != b-\u0026gt;val) return false; return isMirror(a-\u0026gt;left, b-\u0026gt;right) \u0026amp;\u0026amp; isMirror(a-\u0026gt;right, b-\u0026gt;left); } bool isSymmetric(TreeNode* root) { if (!root) return true; return isMirror(root-\u0026gt;left, root-\u0026gt;right); } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(1); root-\u0026gt;left = new TreeNode(2); root-\u0026gt;right = new TreeNode(2); root-\u0026gt;left-\u0026gt;left = new TreeNode(3); root-\u0026gt;left-\u0026gt;right = new TreeNode(4); root-\u0026gt;right-\u0026gt;left = new TreeNode(4); root-\u0026gt;right-\u0026gt;right = new TreeNode(3); std::cout \u0026lt;\u0026lt; (isSymmetric(root) ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func isMirror(a *TreeNode, b *TreeNode) bool { if a == nil \u0026amp;\u0026amp; b == nil { return true } if a == nil || b == nil { return false } if a.Val != b.Val { return false } return isMirror(a.Left, b.Right) \u0026amp;\u0026amp; isMirror(a.Right, b.Left) } func isSymmetric(root *TreeNode) bool { if root == nil { return true } return isMirror(root.Left, root.Right) } func main() { root := \u0026amp;TreeNode{ Val: 1, Left: \u0026amp;TreeNode{ Val: 2, Left: \u0026amp;TreeNode{Val: 3}, Right: \u0026amp;TreeNode{Val: 4}, }, Right: \u0026amp;TreeNode{ Val: 2, Left: \u0026amp;TreeNode{Val: 4}, Right: \u0026amp;TreeNode{Val: 3}, }, } fmt.Println(isSymmetric(root)) } use std::cell::RefCell; use std::rc::Rc; type Node = Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;TreeNode\u0026gt;\u0026gt;\u0026gt;; #[derive(Debug, Clone)] struct TreeNode { val: i32, left: Node, right: Node, } impl TreeNode { fn new(val: i32) -\u0026gt; Rc\u0026lt;RefCell\u0026lt;TreeNode\u0026gt;\u0026gt; { Rc::new(RefCell::new(TreeNode { val, left: None, right: None, })) } } fn is_mirror(a: \u0026amp;Node, b: \u0026amp;Node) -\u0026gt; bool { match (a, b) { (None, None) =\u0026gt; true, (Some(x), Some(y)) =\u0026gt; { let xr = x.borrow(); let yr = y.borrow(); xr.val == yr.val \u0026amp;\u0026amp; is_mirror(\u0026amp;xr.left, \u0026amp;yr.right) \u0026amp;\u0026amp; is_mirror(\u0026amp;xr.right, \u0026amp;yr.left) } _ =\u0026gt; false, } } fn is_symmetric(root: \u0026amp;Node) -\u0026gt; bool { match root { None =\u0026gt; true, Some(node) =\u0026gt; { let node_ref = node.borrow(); is_mirror(\u0026amp;node_ref.left, \u0026amp;node_ref.right) } } } fn main() { let root = Some(TreeNode::new(1)); if let Some(node) = \u0026amp;root { let left = Some(TreeNode::new(2)); let right = Some(TreeNode::new(2)); node.borrow_mut().left = left.clone(); node.borrow_mut().right = right.clone(); if let Some(l) = left { l.borrow_mut().left = Some(TreeNode::new(3)); l.borrow_mut().right = Some(TreeNode::new(4)); } if let Some(r) = right { r.borrow_mut().left = Some(TreeNode::new(4)); r.borrow_mut().right = Some(TreeNode::new(3)); } } println!(\u0026#34;{}\u0026#34;, is_symmetric(\u0026amp;root)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function isMirror(a, b) { if (a === null \u0026amp;\u0026amp; b === null) return true; if (a === null || b === null) return false; if (a.val !== b.val) return false; return isMirror(a.left, b.right) \u0026amp;\u0026amp; isMirror(a.right, b.left); } function isSymmetric(root) { if (root === null) return true; return isMirror(root.left, root.right); } const root = new TreeNode( 1, new TreeNode(2, new TreeNode(3), new TreeNode(4)), new TreeNode(2, new TreeNode(4), new TreeNode(3)), ); console.log(isSymmetric(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/101-symmetric-tree/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThe hard part of Symmetric Tree is not traversal itself, but comparison direction. You are not comparing left to left and right to right. You are comparing mirrored positions. This ACERS guide explains the mirror-recursion contract, the BFS queue-of-pairs variant, and real engineering cases where symmetry checking matters.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003eBFS\u003c/code\u003e, \u003ccode\u003esymmetry\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Hot100, Symmetric Tree, mirror recursion, binary tree symmetry, BFS, LeetCode 101\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A systematic guide to LeetCode 101 from mirror recursion to pairwise BFS symmetry checks, with engineering scenarios and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHot100 learners moving from Same Tree to mirror comparison\u003c/li\u003e\n\u003cli\u003eDevelopers who can write ordinary tree recursion but still mix up outside and inside pairs\u003c/li\u003e\n\u003cli\u003eEngineers who need left-right symmetry validation for layouts, topology templates, or mirrored structures\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eLeetCode 101 is excellent training for directional thinking in tree problems:\u003c/p\u003e","title":"Hot100: Symmetric Tree (Mirror Recursion / BFS ACERS Guide)"},{"content":" Subtitle / Summary\nThe real challenge in LeetCode 100 is not \u0026ldquo;can you traverse a tree\u0026rdquo;, but \u0026ldquo;can you compare two trees node by node in lockstep\u0026rdquo;. This ACERS guide explains the synchronous-recursion contract, the queue-of-pairs BFS variant, and why the pattern matters in real engineering work.\nReading time: 9-11 min Tags: Hot100, binary tree, DFS, BFS, tree comparison SEO keywords: Hot100, Same Tree, binary tree comparison, synchronous recursion, BFS, LeetCode 100 Meta description: A systematic guide to LeetCode 100 from synchronous recursion to pairwise BFS comparison, with engineering scenarios and runnable multi-language implementations. Target Readers Hot100 learners who want to build a stable \u0026ldquo;compare two trees together\u0026rdquo; template Developers who can write DFS on one tree but get confused once two trees must be checked in parallel Engineers who need structural-equivalence checks for config trees, component trees, or syntax trees Background / Motivation When many people first see LeetCode 100, the instinct is:\ntraverse tree p traverse tree q compare the two traversal results afterward That can work only if you serialize very carefully, but it is not the core idea of the problem.\nThe real training value is:\ncan you pull out matching nodes from p and q at the same time can you turn \u0026ldquo;same tree\u0026rdquo; into a precise decision contract can you handle null cases before touching node values This matters far beyond one easy problem. The same pattern reappears in:\nsubtree checking mirror and symmetry checking structural comparison between two tree-shaped configurations So although LeetCode 100 is simple, it is the starting point of the two-tree synchronous comparison template.\nCore Concepts Synchronous recursion: the recursive function takes a pair of nodes (p, q), not just one node Structural equality: matching positions must either both be null or both exist Value equality: if both nodes exist, their values must match Pairwise traversal: whether you use DFS or BFS, the unit of work is always a pair of nodes A - Algorithm (Problem and Algorithm) Problem Restatement Given the root nodes p and q of two binary trees, return true if the trees are the same.\nTwo trees are considered the same if:\nthey have exactly the same structure corresponding nodes have the same values Input / Output Name Type Description p TreeNode root of the first binary tree q TreeNode root of the second binary tree return bool whether the two trees are exactly the same Example 1 input: p = [1,2,3], q = [1,2,3] output: true explanation: The structures match, and every corresponding node value is equal. Example 2 input: p = [1,2], q = [1,null,2] output: false explanation: At the second level, one tree has a left child while the other has a right child. The structure is different. Example 3 input: p = [1,2,1], q = [1,1,2] output: false explanation: The structure matches, but the values at corresponding positions do not. Constraints The number of nodes in both trees is in the range [0, 100] -10^4 \u0026lt;= Node.val \u0026lt;= 10^4 C - Concepts (Core Ideas) Thought Process: Break \u0026ldquo;same\u0026rdquo; into four decision rules For any pair of nodes (p, q), the answer is determined by four stable checks:\nBoth are null: this position matches, so return true Exactly one is null: the structure already differs, so return false Values differ: corresponding nodes are not equal, so return false Both exist and values match: continue checking p.left with q.left p.right with q.right Written as a formula:\nsame(p, q) = true, if p == null and q == null false, if exactly one is null false, if p.val != q.val same(p.left, q.left) and same(p.right, q.right), otherwise Why this is already the complete answer The definition of \u0026ldquo;same tree\u0026rdquo; has only two pieces:\nsame structure same values Every recursive call checks whether the current paired position satisfies those two requirements, then reduces the problem to the left children and right children. That is the classic pattern of local contract + same-shaped subproblem.\nMethod Category Tree DFS Synchronous recursion Pairwise BFS validation Structural equivalence checking Why BFS also works If you do not want recursion, store pairs in a queue:\npop one pair of nodes apply the same four rules as above if the current pair matches, push (left, left) and (right, right) back into the queue Nothing changes conceptually. Only the execution model changes from the call stack to an explicit queue.\nPractice Guide / Steps Recommended Approach: Synchronous recursion Define a function is_same_tree(p, q) Handle the null combinations first Compare the current node values Recursively compare left-left and right-right Runnable Python example:\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def is_same_tree(p, q): if p is None and q is None: return True if p is None or q is None: return False if p.val != q.val: return False return is_same_tree(p.left, q.left) and is_same_tree(p.right, q.right) if __name__ == \u0026#34;__main__\u0026#34;: a = TreeNode(1, TreeNode(2), TreeNode(3)) b = TreeNode(1, TreeNode(2), TreeNode(3)) print(is_same_tree(a, b)) BFS Alternative If you prefer an explicit control flow or want to avoid recursion depth concerns:\nkeep (p, q) pairs in a queue pop one pair at a time and compare it if the pair is valid, push their children in matching directions This style is also convenient when you want to log exactly which pair first fails.\nE - Engineering (Real-world Scenarios) Scenario 1: Detect config-tree drift before release (Python) Background: feature flags, permission inheritance, and routing rules are often stored as tree-shaped nested configs.\nWhy it fits: before a rollout, teams may need to confirm that the staging config tree is exactly the same as production.\ndef same_config(a, b): if a is None and b is None: return True if a is None or b is None: return False if a[\u0026#34;name\u0026#34;] != b[\u0026#34;name\u0026#34;]: return False return same_config(a.get(\u0026#34;left\u0026#34;), b.get(\u0026#34;left\u0026#34;)) and same_config(a.get(\u0026#34;right\u0026#34;), b.get(\u0026#34;right\u0026#34;)) cfg1 = {\u0026#34;name\u0026#34;: \u0026#34;root\u0026#34;, \u0026#34;left\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;A\u0026#34;}, \u0026#34;right\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;B\u0026#34;}} cfg2 = {\u0026#34;name\u0026#34;: \u0026#34;root\u0026#34;, \u0026#34;left\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;A\u0026#34;}, \u0026#34;right\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;B\u0026#34;}} print(same_config(cfg1, cfg2)) Scenario 2: Check component-tree snapshot equivalence (JavaScript) Background: low-code editors and page builders often save UI layouts as component trees.\nWhy it fits: regression checks need to confirm that two snapshots match in both node type and parent-child structure.\nfunction sameTree(a, b) { if (!a \u0026amp;\u0026amp; !b) return true; if (!a || !b) return false; if (a.type !== b.type) return false; return sameTree(a.left, b.left) \u0026amp;\u0026amp; sameTree(a.right, b.right); } const oldTree = { type: \u0026#34;Split\u0026#34;, left: { type: \u0026#34;List\u0026#34; }, right: { type: \u0026#34;Form\u0026#34; } }; const newTree = { type: \u0026#34;Split\u0026#34;, left: { type: \u0026#34;List\u0026#34; }, right: { type: \u0026#34;Form\u0026#34; } }; console.log(sameTree(oldTree, newTree)); Scenario 3: Validate AST structure after a rewrite pass (Go) Background: compilers, linters, and rule engines often rewrite syntax trees.\nWhy it fits: after a rewrite, you may want to verify that the resulting tree shape and labels match the expected output exactly.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Label string Left *Node Right *Node } func same(a, b *Node) bool { if a == nil \u0026amp;\u0026amp; b == nil { return true } if a == nil || b == nil { return false } if a.Label != b.Label { return false } return same(a.Left, b.Left) \u0026amp;\u0026amp; same(a.Right, b.Right) } func main() { x := \u0026amp;Node{Label: \u0026#34;Add\u0026#34;, Left: \u0026amp;Node{Label: \u0026#34;A\u0026#34;}, Right: \u0026amp;Node{Label: \u0026#34;B\u0026#34;}} y := \u0026amp;Node{Label: \u0026#34;Add\u0026#34;, Left: \u0026amp;Node{Label: \u0026#34;A\u0026#34;}, Right: \u0026amp;Node{Label: \u0026#34;B\u0026#34;}} fmt.Println(same(x, y)) } R - Reflection (Analysis and Deeper Understanding) Complexity Analysis Time complexity: O(n), where n is the number of compared paired positions; in the worst case, every corresponding node is visited Space complexity: Recursive DFS: O(h), where h is tree height BFS queue: worst-case O(w), where w is the maximum width of a level Alternative Approaches Method Time Extra Space Notes Synchronous recursion O(n) O(h) Most aligned with the definition and usually the best answer BFS with node pairs O(n) O(w) Non-recursive and easy to debug Serialize then compare O(n) O(n) Must encode null positions or it can give false positives Hash-signature comparison Depends on design Extra hash storage Useful as a quick filter in some systems, but less direct than plain comparison Common Mistakes and Pitfalls Comparing only preorder or inorder values without encoding null positions Accidentally writing same(p.left, q.right), which is the mirror template for LeetCode 101, not this problem Touching p.val before checking whether p or q is null Confusing \u0026ldquo;same value\u0026rdquo; with \u0026ldquo;same object identity in memory\u0026rdquo; Common Questions and Notes 1. Why can\u0026rsquo;t we just compare traversal results? Because different structures can produce the same value sequence.\nIf you truly serialize, you must include null markers as well.\n2. Does the problem ask whether p and q are the same object? No. The question is about same structure + same values, not whether the two roots point to the same memory.\n3. Which is more recommended, DFS or BFS? For interviews and learning, recursion is shorter and closer to the definition. In engineering work, BFS can be more convenient if you want to log the failing comparison path or avoid deep recursion.\nBest Practices and Suggestions For two-tree problems, ask yourself first whether the recursive function should take two nodes Put null checks at the very top; it removes a large class of bugs Keep the concepts separate: same value, same structure, and same reference are different things When you see 100, 101, or 572, think \u0026ldquo;paired comparison template\u0026rdquo; immediately S - Summary The essence of LeetCode 100 is paired comparison, not separate traversal Once you keep the four decision rules stable, synchronous recursion becomes very reliable The BFS version is the same idea with an explicit queue of pairs Structural equivalence checks show up directly in config trees, component trees, and syntax trees If you can write 100 smoothly, LeetCode 101 becomes much easier next References and Further Reading LeetCode 100: Same Tree LeetCode 101: Symmetric Tree LeetCode 572: Subtree of Another Tree LeetCode 226: Invert Binary Tree LeetCode 102: Binary Tree Level Order Traversal CTA Practice 100 and 101 back to back.\n100 trains same-direction comparison, while 101 trains mirror-direction comparison. Once those two templates are clear, binary-tree judgment problems become much easier to reason about.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def is_same_tree(p, q): if p is None and q is None: return True if p is None or q is None: return False if p.val != q.val: return False return is_same_tree(p.left, q.left) and is_same_tree(p.right, q.right) if __name__ == \u0026#34;__main__\u0026#34;: p = TreeNode(1, TreeNode(2), TreeNode(3)) q = TreeNode(1, TreeNode(2), TreeNode(3)) print(is_same_tree(p, q)) #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } bool isSameTree(struct TreeNode* p, struct TreeNode* q) { if (p == NULL \u0026amp;\u0026amp; q == NULL) return true; if (p == NULL || q == NULL) return false; if (p-\u0026gt;val != q-\u0026gt;val) return false; return isSameTree(p-\u0026gt;left, q-\u0026gt;left) \u0026amp;\u0026amp; isSameTree(p-\u0026gt;right, q-\u0026gt;right); } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* p = new_node(1); p-\u0026gt;left = new_node(2); p-\u0026gt;right = new_node(3); struct TreeNode* q = new_node(1); q-\u0026gt;left = new_node(2); q-\u0026gt;right = new_node(3); printf(\u0026#34;%s\\n\u0026#34;, isSameTree(p, q) ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;); free_tree(p); free_tree(q); return 0; } #include \u0026lt;iostream\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; bool isSameTree(TreeNode* p, TreeNode* q) { if (!p \u0026amp;\u0026amp; !q) return true; if (!p || !q) return false; if (p-\u0026gt;val != q-\u0026gt;val) return false; return isSameTree(p-\u0026gt;left, q-\u0026gt;left) \u0026amp;\u0026amp; isSameTree(p-\u0026gt;right, q-\u0026gt;right); } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* p = new TreeNode(1); p-\u0026gt;left = new TreeNode(2); p-\u0026gt;right = new TreeNode(3); TreeNode* q = new TreeNode(1); q-\u0026gt;left = new TreeNode(2); q-\u0026gt;right = new TreeNode(3); std::cout \u0026lt;\u0026lt; (isSameTree(p, q) ? \u0026#34;true\u0026#34; : \u0026#34;false\u0026#34;) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(p); freeTree(q); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func isSameTree(p *TreeNode, q *TreeNode) bool { if p == nil \u0026amp;\u0026amp; q == nil { return true } if p == nil || q == nil { return false } if p.Val != q.Val { return false } return isSameTree(p.Left, q.Left) \u0026amp;\u0026amp; isSameTree(p.Right, q.Right) } func main() { p := \u0026amp;TreeNode{Val: 1, Left: \u0026amp;TreeNode{Val: 2}, Right: \u0026amp;TreeNode{Val: 3}} q := \u0026amp;TreeNode{Val: 1, Left: \u0026amp;TreeNode{Val: 2}, Right: \u0026amp;TreeNode{Val: 3}} fmt.Println(isSameTree(p, q)) } use std::cell::RefCell; use std::rc::Rc; type Node = Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;TreeNode\u0026gt;\u0026gt;\u0026gt;; #[derive(Debug, Clone)] struct TreeNode { val: i32, left: Node, right: Node, } impl TreeNode { fn new(val: i32) -\u0026gt; Rc\u0026lt;RefCell\u0026lt;TreeNode\u0026gt;\u0026gt; { Rc::new(RefCell::new(TreeNode { val, left: None, right: None, })) } } fn is_same_tree(p: \u0026amp;Node, q: \u0026amp;Node) -\u0026gt; bool { match (p, q) { (None, None) =\u0026gt; true, (Some(a), Some(b)) =\u0026gt; { let a_ref = a.borrow(); let b_ref = b.borrow(); a_ref.val == b_ref.val \u0026amp;\u0026amp; is_same_tree(\u0026amp;a_ref.left, \u0026amp;b_ref.left) \u0026amp;\u0026amp; is_same_tree(\u0026amp;a_ref.right, \u0026amp;b_ref.right) } _ =\u0026gt; false, } } fn main() { let p = Some(TreeNode::new(1)); let q = Some(TreeNode::new(1)); if let Some(root) = \u0026amp;p { root.borrow_mut().left = Some(TreeNode::new(2)); root.borrow_mut().right = Some(TreeNode::new(3)); } if let Some(root) = \u0026amp;q { root.borrow_mut().left = Some(TreeNode::new(2)); root.borrow_mut().right = Some(TreeNode::new(3)); } println!(\u0026#34;{}\u0026#34;, is_same_tree(\u0026amp;p, \u0026amp;q)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function isSameTree(p, q) { if (p === null \u0026amp;\u0026amp; q === null) return true; if (p === null || q === null) return false; if (p.val !== q.val) return false; return isSameTree(p.left, q.left) \u0026amp;\u0026amp; isSameTree(p.right, q.right); } const p = new TreeNode(1, new TreeNode(2), new TreeNode(3)); const q = new TreeNode(1, new TreeNode(2), new TreeNode(3)); console.log(isSameTree(p, q)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/100-same-tree/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThe real challenge in LeetCode 100 is not \u0026ldquo;can you traverse a tree\u0026rdquo;, but \u0026ldquo;can you compare two trees node by node in lockstep\u0026rdquo;. This ACERS guide explains the synchronous-recursion contract, the queue-of-pairs BFS variant, and why the pattern matters in real engineering work.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 9-11 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003eBFS\u003c/code\u003e, \u003ccode\u003etree comparison\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Hot100, Same Tree, binary tree comparison, synchronous recursion, BFS, LeetCode 100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A systematic guide to LeetCode 100 from synchronous recursion to pairwise BFS comparison, with engineering scenarios and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHot100 learners who want to build a stable \u0026ldquo;compare two trees together\u0026rdquo; template\u003c/li\u003e\n\u003cli\u003eDevelopers who can write DFS on one tree but get confused once two trees must be checked in parallel\u003c/li\u003e\n\u003cli\u003eEngineers who need structural-equivalence checks for config trees, component trees, or syntax trees\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eWhen many people first see LeetCode 100, the instinct is:\u003c/p\u003e","title":"Hot100: Same Tree (Synchronous Recursion / BFS ACERS Guide)"},{"content":" Subtitle / Summary\nInvert Binary Tree looks tiny, but it is one of the fastest ways to test whether you really understand recursive structure on trees. This guide uses LeetCode 226 to break down the essence of \u0026ldquo;swap left and right subtrees\u0026rdquo;, covers both recursion and BFS, and shows how the same idea transfers to engineering scenarios.\nReading time: 8-10 min Tags: Hot100, binary tree, recursion, BFS, tree transformation SEO keywords: Hot100, Invert Binary Tree, tree mirror, recursion, BFS, LeetCode 226 Meta description: Learn the recursive and BFS solutions for LeetCode 226, then extend the idea to layout mirroring and structural transformations. Target Readers Hot100 learners who want to verify whether they truly understand \u0026ldquo;apply recursion to every node in the whole tree\u0026rdquo; Developers who instinctively start traversing any tree problem, but are unsure when to process the current node Engineers who need tree mirroring, layout inversion, or symmetric structural transforms Background / Motivation The code for LeetCode 226 is usually very short, but the thinking pattern is extremely typical:\nWhat should the current node do?\nSwap left and right.\nWhat is the subproblem?\nThe left and right subtrees must also be inverted.\nThis is a very pure example of current operation + recursive handling of identical subproblems.\nIf you do not fully internalize this problem, you often end up with mistakes like:\nswapping only the root and forgetting the subtrees getting the recursive direction mixed up after the swap rebuilding a brand new tree for something that can be done in place Core Concepts Tree mirror: swap the left and right subtree of every node In-place transform: do not rebuild the whole tree; only swap pointers or references Recursive divide-and-conquer: after handling the current node, each subtree is still the same kind of problem BFS level-order transform: you can also swap each node\u0026rsquo;s children level by level A - Algorithm (Problem and Algorithm) Problem Restatement Given the root node root of a binary tree, invert the tree and return the root of the inverted tree.\nInput / Output Name Type Description root TreeNode root of the binary tree, may be null return TreeNode root of the inverted tree Example 1 input: root = [4,2,7,1,3,6,9] output: [4,7,2,9,6,3,1] explanation: after swapping the left and right subtrees across the whole tree, every node is mirrored. Example 2 input: root = [2,1,3] output: [2,3,1] Example 3 input: root = [] output: [] Constraints The number of nodes is in the range [0, 100] -100 \u0026lt;= Node.val \u0026lt;= 100 C - Concepts (Core Ideas) Thought Process: Why \u0026ldquo;swap + recursion\u0026rdquo; is enough Suppose the current node is node. We only need two steps:\nSwap node.left and node.right Recursively invert the new left subtree and the new right subtree The pseudocode is very short:\ninvert(node): if node is null: return null swap node.left and node.right invert(node.left) invert(node.right) return node Why this is the complete answer Because inverting the whole tree is essentially \u0026ldquo;perform one left-right swap on every node\u0026rdquo;.\nAnd every local subtree is still a tree, so recursion fits naturally.\nMethod Category Tree recursion In-place structural transform BFS / queue traversal Recursion vs BFS Recursion\nshortest code matches the recursive definition of a tree recommended as the main solution BFS\nswap nodes level by level useful if you also want to do level statistics or visualization Practice Guide / Steps Recommended Approach: Recursion Handle the null case Swap the left and right child Recursively invert the left subtree Recursively invert the right subtree Return the current node Runnable Python example:\nfrom collections import deque class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def invert_tree(root): if root is None: return None root.left, root.right = root.right, root.left invert_tree(root.left) invert_tree(root.right) return root def level_order(root): if root is None: return [] q = deque([root]) res = [] while q: node = q.popleft() res.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) return res if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(4, TreeNode(2, TreeNode(1), TreeNode(3)), TreeNode(7, TreeNode(6), TreeNode(9))) invert_tree(root) print(level_order(root)) E - Engineering (Real-world Scenarios) Scenario 1: Mirrored split-pane layout preview (JavaScript) Background: visual editors often store split-pane layouts as binary trees.\nWhy it fits: a \u0026ldquo;mirror preview\u0026rdquo; is essentially swapping the left and right regions at every split node.\nfunction Pane(name, left = null, right = null) { this.name = name; this.left = left; this.right = right; } function invert(node) { if (!node) return null; [node.left, node.right] = [node.right, node.left]; invert(node.left); invert(node.right); return node; } const root = new Pane(\u0026#34;root\u0026#34;, new Pane(\u0026#34;left\u0026#34;), new Pane(\u0026#34;right\u0026#34;)); console.log(invert(root)); Scenario 2: Tree-mirroring demos in teaching tools (Python) Background: algorithm teaching platforms often need dynamic demonstrations of the \u0026ldquo;mirror\u0026rdquo; concept.\nWhy it fits: the solution to LeetCode 226 is the standard tree-mirroring transform.\nclass Node: def __init__(self, val, left=None, right=None): self.val = val self.left = left self.right = right def invert(node): if node is None: return None node.left, node.right = invert(node.right), invert(node.left) return node root = Node(\u0026#34;A\u0026#34;, Node(\u0026#34;B\u0026#34;), Node(\u0026#34;C\u0026#34;)) print(invert(root).left.val) Scenario 3: Left/right branch inversion tests for rule trees (Go) Background: some rule engines organize \u0026ldquo;match / no-match\u0026rdquo; branches as binary trees.\nWhy it fits: when doing mirror-based tests, this quickly verifies whether the left/right branch logic is symmetric.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Name string Left *Node Right *Node } func invert(node *Node) *Node { if node == nil { return nil } node.Left, node.Right = invert(node.Right), invert(node.Left) return node } func main() { root := \u0026amp;Node{\u0026#34;root\u0026#34;, \u0026amp;Node{\u0026#34;allow\u0026#34;, nil, nil}, \u0026amp;Node{\u0026#34;deny\u0026#34;, nil, nil}} root = invert(root) fmt.Println(root.Left.Name, root.Right.Name) } R - Reflection (Analysis and Deeper Understanding) Complexity Analysis Time complexity: O(n), because each node is swapped once Space complexity: Recursion: O(h) BFS: O(w), where w is the maximum width of the tree Alternative Approaches Method Time Extra Space Notes Recursion O(n) O(h) Simplest and recommended BFS queue O(n) O(w) Convenient for level-by-level processing Build a new mirror tree O(n) O(n) Unnecessary extra memory allocation Common Mistakes and Pitfalls Swapping only the root once and forgetting to recurse into the subtrees Swapping first, then recursing through stale references and confusing the logic Rebuilding a whole new tree even though the job can be done in place Confusing \u0026ldquo;invert binary tree\u0026rdquo; with \u0026ldquo;reverse linked list\u0026rdquo; and mistakenly thinking a linear reconnection order is needed Common Questions and Notes 1. Can I recurse first and swap later? Yes. As long as every node eventually completes the left-right swap, the result is correct.\nBut \u0026ldquo;swap first, recurse later\u0026rdquo; is usually the most intuitive version.\n2. Which is better in interviews, recursion or BFS? Recursion is the default choice for this problem. BFS is better treated as an alternative implementation that shows you understand traversal variations.\n3. Is this preorder or postorder? It is closer to a preorder-style action: the current node swaps immediately, and only then do we process the subtrees.\nBest Practices and Suggestions For tree-transform problems, first ask \u0026ldquo;what changes at the current node?\u0026rdquo;, then ask \u0026ldquo;is each subtree the same type of subproblem?\u0026rdquo; If you can swap in place, do that and avoid unnecessary object allocation Keep the recursive function semantic simple: input a tree, return the inverted version of that same tree Do not just memorize the code; be able to explain verbally why recursion is such a natural fit here S - Summary The essence of LeetCode 226 is one left-right swap at every node Recursion works because every subtree is still the same problem This is a classic template of \u0026ldquo;process current node + recurse on subproblems\u0026rdquo; BFS also works, but recursion expresses the idea more directly In engineering, the same pattern applies to layout mirroring, visualization mirroring, and symmetry tests on rule trees References and Further Reading LeetCode 226: Invert Binary Tree LeetCode 101: Symmetric Tree LeetCode 100: Same Tree LeetCode 104: Maximum Depth of Binary Tree LeetCode 102: Binary Tree Level Order Traversal CTA Try solving 226, 101, and 100 together.\nOne trains structural transformation, one trains structural comparison, and together they make tree recursion feel much more solid.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) from collections import deque class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def invert_tree(root): if root is None: return None root.left, root.right = root.right, root.left invert_tree(root.left) invert_tree(root.right) return root def level_order(root): if root is None: return [] q = deque([root]) res = [] while q: node = q.popleft() res.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) return res if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(4, TreeNode(2, TreeNode(1), TreeNode(3)), TreeNode(7, TreeNode(6), TreeNode(9))) invert_tree(root) print(level_order(root)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } struct TreeNode* invertTree(struct TreeNode* root) { if (root == NULL) return NULL; struct TreeNode* tmp = root-\u0026gt;left; root-\u0026gt;left = invertTree(root-\u0026gt;right); root-\u0026gt;right = invertTree(tmp); return root; } void preorder(struct TreeNode* root) { if (!root) return; printf(\u0026#34;%d \u0026#34;, root-\u0026gt;val); preorder(root-\u0026gt;left); preorder(root-\u0026gt;right); } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(4); root-\u0026gt;left = new_node(2); root-\u0026gt;right = new_node(7); root-\u0026gt;left-\u0026gt;left = new_node(1); root-\u0026gt;left-\u0026gt;right = new_node(3); root-\u0026gt;right-\u0026gt;left = new_node(6); root-\u0026gt;right-\u0026gt;right = new_node(9); invertTree(root); preorder(root); printf(\u0026#34;\\n\u0026#34;); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;utility\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; TreeNode* invertTree(TreeNode* root) { if (!root) return nullptr; std::swap(root-\u0026gt;left, root-\u0026gt;right); invertTree(root-\u0026gt;left); invertTree(root-\u0026gt;right); return root; } void preorder(TreeNode* root) { if (!root) return; std::cout \u0026lt;\u0026lt; root-\u0026gt;val \u0026lt;\u0026lt; \u0026#39; \u0026#39;; preorder(root-\u0026gt;left); preorder(root-\u0026gt;right); } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(4); root-\u0026gt;left = new TreeNode(2); root-\u0026gt;right = new TreeNode(7); root-\u0026gt;left-\u0026gt;left = new TreeNode(1); root-\u0026gt;left-\u0026gt;right = new TreeNode(3); root-\u0026gt;right-\u0026gt;left = new TreeNode(6); root-\u0026gt;right-\u0026gt;right = new TreeNode(9); invertTree(root); preorder(root); std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func invertTree(root *TreeNode) *TreeNode { if root == nil { return nil } root.Left, root.Right = invertTree(root.Right), invertTree(root.Left) return root } func preorder(root *TreeNode) { if root == nil { return } fmt.Print(root.Val, \u0026#34; \u0026#34;) preorder(root.Left) preorder(root.Right) } func main() { root := \u0026amp;TreeNode{ Val: 4, Left: \u0026amp;TreeNode{ Val: 2, Left: \u0026amp;TreeNode{Val: 1}, Right: \u0026amp;TreeNode{Val: 3}, }, Right: \u0026amp;TreeNode{ Val: 7, Left: \u0026amp;TreeNode{Val: 6}, Right: \u0026amp;TreeNode{Val: 9}, }, } invertTree(root) preorder(root) fmt.Println() } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn invert_tree(root: \u0026amp;mut Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) { if let Some(node) = root { std::mem::swap(\u0026amp;mut node.left, \u0026amp;mut node.right); invert_tree(\u0026amp;mut node.left); invert_tree(\u0026amp;mut node.right); } } fn preorder(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) { if let Some(node) = root { print!(\u0026#34;{} \u0026#34;, node.val); preorder(\u0026amp;node.left); preorder(\u0026amp;node.right); } } fn main() { let mut root = Some(Box::new(TreeNode { val: 4, left: Some(Box::new(TreeNode { val: 2, left: Some(Box::new(TreeNode { val: 1, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 3, left: None, right: None, })), })), right: Some(Box::new(TreeNode { val: 7, left: Some(Box::new(TreeNode { val: 6, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 9, left: None, right: None, })), })), })); invert_tree(\u0026amp;mut root); preorder(\u0026amp;root); println!(); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function invertTree(root) { if (!root) return null; [root.left, root.right] = [invertTree(root.right), invertTree(root.left)]; return root; } function preorder(root, out = []) { if (!root) return out; out.push(root.val); preorder(root.left, out); preorder(root.right, out); return out; } const root = new TreeNode( 4, new TreeNode(2, new TreeNode(1), new TreeNode(3)), new TreeNode(7, new TreeNode(6), new TreeNode(9)) ); invertTree(root); console.log(preorder(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/226-invert-binary-tree/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nInvert Binary Tree looks tiny, but it is one of the fastest ways to test whether you really understand recursive structure on trees. This guide uses LeetCode 226 to break down the essence of \u0026ldquo;swap left and right subtrees\u0026rdquo;, covers both recursion and BFS, and shows how the same idea transfers to engineering scenarios.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 8-10 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003erecursion\u003c/code\u003e, \u003ccode\u003eBFS\u003c/code\u003e, \u003ccode\u003etree transformation\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Hot100, Invert Binary Tree, tree mirror, recursion, BFS, LeetCode 226\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn the recursive and BFS solutions for LeetCode 226, then extend the idea to layout mirroring and structural transformations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHot100 learners who want to verify whether they truly understand \u0026ldquo;apply recursion to every node in the whole tree\u0026rdquo;\u003c/li\u003e\n\u003cli\u003eDevelopers who instinctively start traversing any tree problem, but are unsure when to process the current node\u003c/li\u003e\n\u003cli\u003eEngineers who need tree mirroring, layout inversion, or symmetric structural transforms\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eThe code for LeetCode 226 is usually very short, but the thinking pattern is extremely typical:\u003c/p\u003e","title":"Hot100: Invert Binary Tree (Recursion / BFS ACERS Guide)"},{"content":" Subtitle / Summary\n\u0026ldquo;Maximum depth\u0026rdquo; is one of the cleanest starting points for tree recursion. Once you truly understand that the answer for the current tree depends on the answers from its left and right subtrees, a whole family of tree DP and DFS problems becomes easier. This guide uses LeetCode 104 to explain recursive DFS, level-order BFS, and the engineering value of the same pattern.\nReading time: 9-11 min Tags: Hot100, binary tree, DFS, BFS, recursion SEO keywords: Hot100, Maximum Depth of Binary Tree, DFS, BFS, LeetCode 104 Meta description: Learn the DFS and BFS solutions for LeetCode 104 from the definition of depth, with engineering mappings and runnable multi-language code. Target Readers Learners who are just starting tree problems and want to truly internalize \u0026ldquo;tree recursion return values\u0026rdquo; Developers who can write traversals but get confused once the task becomes \u0026ldquo;compute height\u0026rdquo;, \u0026ldquo;compute path\u0026rdquo;, or \u0026ldquo;compute an answer\u0026rdquo; Engineers who need depth analysis on hierarchical data such as menus, org charts, or nested JSON Background / Motivation LeetCode 104 looks like an easy problem, but it is almost the parent problem of tree recursion:\nyou first need to answer \u0026ldquo;what is the depth of an empty tree?\u0026rdquo; then answer \u0026ldquo;who determines the answer for the current node?\u0026rdquo; and finally write the relation as 1 + max(left, right) Once this recursive definition is built correctly, later problems such as balanced binary tree, tree diameter, path sums, and lowest common ancestor become much easier to enter.\nCore Concepts Depth / height: in this problem, the number of nodes on the longest path from root to the farthest leaf Postorder-style thinking: to know the answer for the current node, you must first know the answers of the left and right subtrees DFS: recurse downward and combine answers while backtracking BFS: traverse level by level; the last level number is the tree depth A - Algorithm (Problem and Algorithm) Problem Restatement Given the root node root of a binary tree, return its maximum depth.\nMaximum depth means the number of nodes along the longest path from the root down to the farthest leaf node.\nInput / Output Name Type Description root TreeNode root of the binary tree, may be null return int maximum depth of the tree Example 1 input: root = [3,9,20,null,null,15,7] output: 3 explanation: level 1: 3 level 2: 9, 20 level 3: 15, 7 so the maximum depth is 3. Example 2 input: root = [1,null,2] output: 2 Constraints The number of nodes is in the range [0, 10^4] -100 \u0026lt;= Node.val \u0026lt;= 100 C - Concepts (Core Ideas) Thought Process: Why the recursive formula is 1 + max(left, right) For any node node:\nif it is null, the depth is 0 if it is not null, then the maximum depth from that node is: 1 for the current level plus the deeper side between the left subtree and the right subtree So the state transition is direct:\ndepth(node) = 1 + max(depth(node.left), depth(node.right)) Method Category Tree recursion / DFS Level-order traversal / BFS Bottom-up answer combination in tree problems When DFS and BFS are each a good fit Recursive DFS\nshortest code matches the definition best ideal for most interviews and explanations Level-order BFS\nvery convenient for problems that are naturally layer-based if you also want the node distribution by level, BFS is more direct Why DFS is the recommended template here This problem does not ask you to print each level; it only asks for one final number.\nDFS writes the definition directly, gives the clearest expression, and has the lowest error rate.\nPractice Guide / Steps Recommended Approach: Recursive DFS If the node is null, return 0 Recursively compute the maximum depth of the left subtree Recursively compute the maximum depth of the right subtree Return 1 + max(leftDepth, rightDepth) Runnable Python example:\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def max_depth(root): if root is None: return 0 return 1 + max(max_depth(root.left), max_depth(root.right)) if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7))) print(max_depth(root)) BFS Alternative If you prefer level-order traversal, you can also:\nUse a queue to hold the current level Increase depth after processing one full level Stop when the queue becomes empty This method is also common, especially when the problem also asks for level-by-level output.\nE - Engineering (Real-world Scenarios) Scenario 1: Maximum nesting depth of frontend menu config (JavaScript) Background: backend systems often allow menus to be configured as trees.\nWhy it fits: before release, you can check whether the menu exceeds the maximum nesting level allowed by the design.\nconst menu = { name: \u0026#34;root\u0026#34;, children: [ { name: \u0026#34;dashboard\u0026#34;, children: [] }, { name: \u0026#34;settings\u0026#34;, children: [{ name: \u0026#34;profile\u0026#34;, children: [] }] }, ], }; function depth(node) { if (!node) return 0; if (!node.children || node.children.length === 0) return 1; return 1 + Math.max(...node.children.map(depth)); } console.log(depth(menu)); Scenario 2: Longest reporting chain in an org chart (Go) Background: org charts and approval flows are often represented as trees.\nWhy it fits: maximum depth measures hierarchy complexity and helps with workflow optimization and permission design.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Name string Children []*Node } func depth(node *Node) int { if node == nil { return 0 } best := 0 for _, child := range node.Children { if d := depth(child); d \u0026gt; best { best = d } } return 1 + best } func main() { root := \u0026amp;Node{ Name: \u0026#34;CEO\u0026#34;, Children: []*Node{ { Name: \u0026#34;VP\u0026#34;, Children: []*Node{ {Name: \u0026#34;Manager\u0026#34;}, }, }, }, } fmt.Println(depth(root)) } Scenario 3: Maximum nesting validation for JSON (Python) Background: logs, configs, and ETL payloads often contain deeply nested JSON.\nWhy it fits: overly deep data hurts readability and downstream processing, so it is useful to enforce a depth limit at the input boundary.\ndef json_depth(x): if isinstance(x, dict): if not x: return 1 return 1 + max(json_depth(v) for v in x.values()) if isinstance(x, list): if not x: return 1 return 1 + max(json_depth(v) for v in x) return 1 data = {\u0026#34;a\u0026#34;: {\u0026#34;b\u0026#34;: {\u0026#34;c\u0026#34;: [1, {\u0026#34;d\u0026#34;: 2}]}}} print(json_depth(data)) R - Reflection (Analysis and Deeper Understanding) Complexity Analysis Time complexity: O(n), because each node is visited once Space complexity: DFS recursion: O(h) BFS queue: worst case O(n), or more precisely O(w) where w is the maximum tree width Alternative Approaches Method Time Extra Space Notes DFS recursion O(n) O(h) Matches the definition best and is recommended BFS level order O(n) O(w) Very convenient for level-based problems Explicit-stack DFS O(n) O(h) Useful if you do not want recursion Common Mistakes and Pitfalls Writing the depth of a null node as 1, which adds an extra level to the whole tree Mixing up \u0026ldquo;edge count\u0026rdquo; and \u0026ldquo;node count\u0026rdquo;; this problem counts nodes Recursing into only one side and forgetting max(left, right) Incrementing depth on every popped BFS node, which counts nodes instead of levels Common Questions and Notes 1. Is this preorder, inorder, or postorder? More accurately, it follows a postorder-style merge pattern, because the answer for the current node depends on the answers of its left and right subtrees.\n2. Which is better, DFS or BFS? If you only need one depth value, DFS is simpler. If you also want the nodes grouped by level, BFS is more natural.\n3. Can recursion overflow the stack? Yes, for extremely degenerate trees. In engineering situations where tree depth is unbounded, explicit stacks or BFS are safer.\nBest Practices and Suggestions Write the base case clearly first: what should the function return when node == null? For tree problems where the current answer depends on the left and right subtree answers, think of recursive return values first When writing complexity, distinguish O(h) from O(w) for a more accurate statement Being able to explain when DFS or BFS is appropriate matters more than simply memorizing code S - Summary The core of LeetCode 104 is not the code, but the definition of depth itself Once depth(node) = 1 + max(left, right) is clear, the recursive solution almost writes itself DFS is the most recommended template for this problem, while BFS is an excellent level-based alternative This problem is foundational for balanced-tree, diameter, and path-sum questions In engineering, any hierarchical structure with \u0026ldquo;maximum nesting depth\u0026rdquo; can reuse the same idea References and Further Reading LeetCode 104: Maximum Depth of Binary Tree LeetCode 111: Minimum Depth of Binary Tree LeetCode 110: Balanced Binary Tree LeetCode 543: Diameter of Binary Tree LeetCode 102: Binary Tree Level Order Traversal CTA It is worth practicing 104 together with 111.\nOne is about max, while the other often exposes tricky null-subtree handling; together they make your tree-recursion base cases much more stable.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def max_depth(root): if root is None: return 0 return 1 + max(max_depth(root.left), max_depth(root.right)) if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(3, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7))) print(max_depth(root)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } int maxDepth(struct TreeNode* root) { if (root == NULL) return 0; int left = maxDepth(root-\u0026gt;left); int right = maxDepth(root-\u0026gt;right); return 1 + (left \u0026gt; right ? left : right); } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(3); root-\u0026gt;left = new_node(9); root-\u0026gt;right = new_node(20); root-\u0026gt;right-\u0026gt;left = new_node(15); root-\u0026gt;right-\u0026gt;right = new_node(7); printf(\u0026#34;%d\\n\u0026#34;, maxDepth(root)); free_tree(root); return 0; } #include \u0026lt;algorithm\u0026gt; #include \u0026lt;iostream\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; int maxDepth(TreeNode* root) { if (!root) return 0; int left = maxDepth(root-\u0026gt;left); int right = maxDepth(root-\u0026gt;right); return 1 + std::max(left, right); } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(3); root-\u0026gt;left = new TreeNode(9); root-\u0026gt;right = new TreeNode(20); root-\u0026gt;right-\u0026gt;left = new TreeNode(15); root-\u0026gt;right-\u0026gt;right = new TreeNode(7); std::cout \u0026lt;\u0026lt; maxDepth(root) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func maxDepth(root *TreeNode) int { if root == nil { return 0 } left := maxDepth(root.Left) right := maxDepth(root.Right) if left \u0026gt; right { return 1 + left } return 1 + right } func main() { root := \u0026amp;TreeNode{ Val: 3, Left: \u0026amp;TreeNode{Val: 9}, Right: \u0026amp;TreeNode{ Val: 20, Left: \u0026amp;TreeNode{Val: 15}, Right: \u0026amp;TreeNode{Val: 7}, }, } fmt.Println(maxDepth(root)) } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn max_depth(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) -\u0026gt; i32 { match root { None =\u0026gt; 0, Some(node) =\u0026gt; 1 + max_depth(\u0026amp;node.left).max(max_depth(\u0026amp;node.right)), } } fn main() { let root = Some(Box::new(TreeNode { val: 3, left: Some(Box::new(TreeNode { val: 9, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 20, left: Some(Box::new(TreeNode { val: 15, left: None, right: None, })), right: Some(Box::new(TreeNode { val: 7, left: None, right: None, })), })), })); println!(\u0026#34;{}\u0026#34;, max_depth(\u0026amp;root)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function maxDepth(root) { if (!root) return 0; return 1 + Math.max(maxDepth(root.left), maxDepth(root.right)); } const root = new TreeNode( 3, new TreeNode(9), new TreeNode(20, new TreeNode(15), new TreeNode(7)) ); console.log(maxDepth(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/104-maximum-depth-of-binary-tree/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\n\u0026ldquo;Maximum depth\u0026rdquo; is one of the cleanest starting points for tree recursion. Once you truly understand that the answer for the current tree depends on the answers from its left and right subtrees, a whole family of tree DP and DFS problems becomes easier. This guide uses LeetCode 104 to explain recursive DFS, level-order BFS, and the engineering value of the same pattern.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 9-11 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003eBFS\u003c/code\u003e, \u003ccode\u003erecursion\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Hot100, Maximum Depth of Binary Tree, DFS, BFS, LeetCode 104\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Learn the DFS and BFS solutions for LeetCode 104 from the definition of depth, with engineering mappings and runnable multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLearners who are just starting tree problems and want to truly internalize \u0026ldquo;tree recursion return values\u0026rdquo;\u003c/li\u003e\n\u003cli\u003eDevelopers who can write traversals but get confused once the task becomes \u0026ldquo;compute height\u0026rdquo;, \u0026ldquo;compute path\u0026rdquo;, or \u0026ldquo;compute an answer\u0026rdquo;\u003c/li\u003e\n\u003cli\u003eEngineers who need depth analysis on hierarchical data such as menus, org charts, or nested JSON\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eLeetCode 104 looks like an easy problem, but it is almost the parent problem of tree recursion:\u003c/p\u003e","title":"Hot100: Maximum Depth of Binary Tree (DFS / BFS ACERS Guide)"},{"content":" Subtitle / Summary\nBinary tree traversal is the starting point of most tree templates, and inorder traversal is one of the cleanest problems for understanding both recursive thinking and explicit stack simulation. This ACERS guide uses LeetCode 94 to explain the left-root-right order, the iterative stack template, and why the pattern matters in real engineering work.\nReading time: 10-12 min Tags: Hot100, binary tree, DFS, stack, inorder traversal SEO keywords: Hot100, Binary Tree Inorder Traversal, inorder traversal, explicit stack, LeetCode 94 Meta description: A systematic guide to LeetCode 94 from recursion to explicit stacks, with engineering scenarios and runnable multi-language implementations. Target Readers Hot100 learners who want to lock in a stable tree-traversal template Developers moving from arrays and linked lists to trees, and still mixing up preorder, inorder, and postorder Engineers who want to reuse the left-root-right idea in BSTs, expression trees, or syntax trees Background / Motivation Inorder traversal is not hard by itself, but its training value is high:\nit is one of the easiest problems for building intuition that recursion = implicit stack, while iteration = explicit stack it helps you internalize the process of \u0026ldquo;go left all the way, backtrack to visit the root, then move into the right subtree\u0026rdquo; in a binary search tree (BST), inorder traversal naturally produces a sorted sequence, so the engineering value is very real When many people first solve tree problems, the issue is not the logic itself, but:\nnot being sure which node gets visited first not knowing exactly when to push and pop in the iterative version getting the code tangled when the tree is empty or degenerates into a one-sided chain If you master this template, later problems like validating a BST, finding the k-th smallest element, or recovering a BST become much smoother.\nCore Concepts Inorder traversal: visit in the order left subtree -\u0026gt; root node -\u0026gt; right subtree DFS (depth-first search): the most common organization pattern for tree traversal; inorder is one specific visitation order Explicit stack: manually simulate the recursion call stack by storing nodes you still need to come back to Tree height h: space complexity is usually written as O(h); for balanced trees this is about O(log n), and for a degenerate chain it becomes O(n) A - Algorithm (Problem and Algorithm) Problem Restatement Given the root node root of a binary tree, return its inorder traversal result.\nInput / Output Name Type Description root TreeNode root of the binary tree, may be null return int[] / List[int] node values in inorder sequence Example 1 input: root = [1,null,2,3] output: [1,3,2] explanation: 1 \\ 2 / 3 The inorder order is left -\u0026gt; root -\u0026gt; right, so the answer is [1,3,2]. Example 2 input: root = [] output: [] Example 3 input: root = [1] output: [1] Constraints The number of nodes is in the range [0, 100] -100 \u0026lt;= Node.val \u0026lt;= 100 C - Concepts (Core Ideas) Thought Process: From recursive definition to explicit stack template The most natural form is recursion\nFor each node node:\ntraverse the left subtree first visit the current node traverse the right subtree last That matches the definition of inorder exactly, so the code is very short.\nBut interviews often ask: can you do it without recursion?\nSince recursion relies on the function call stack, interviewers often want you to write that process out explicitly.\nWhy do we keep pushing nodes while going left?\nBecause inorder requires the left subtree to be processed first. So as long as the current node is not null, we push it and continue to left.\nOnce we hit null, the leftmost chain is exhausted, and the top of the stack is exactly the next root we should visit.\nMethod Category Tree DFS Recursive traversal Stack-based recursion simulation Explicit Stack Template The iterative version can be remembered in four stable steps:\ncur = root While cur != null, keep pushing and move left After the left side ends, pop the stack top and record its value Move cur to the right subtree of the popped node, then repeat Pseudo flow:\nwhile cur is not null or stack is not empty: while cur is not null: stack.push(cur) cur = cur.left cur = stack.pop() record cur.val cur = cur.right Why this order is always correct Each node is visited exactly once during left-chain backtracking The left subtree always finishes before the node itself The right subtree only starts after the root node has been visited That is exactly equivalent to the definition of inorder traversal, so the result is correct.\nPractice Guide / Steps Recommended Approach: Iterative explicit stack Prepare the result array res and a stack stack Start cur from the root Keep pushing the left chain Pop and visit the root Move into the right subtree Stop when both the stack is empty and cur is null Runnable Python example:\nclass TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def inorder_traversal(root): res = [] stack = [] cur = root while cur is not None or stack: while cur is not None: stack.append(cur) cur = cur.left cur = stack.pop() res.append(cur.val) cur = cur.right return res if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(1, None, TreeNode(2, TreeNode(3), None)) print(inorder_traversal(root)) E - Engineering (Real-world Scenarios) Scenario 1: Export sorted primary keys from a BST (Python) Background: many in-memory indexes, cache dictionaries, and teaching-oriented search trees store data in BST form.\nWhy it fits: inorder traversal of a BST naturally yields ascending order, so it is useful for audit exports or debug snapshots.\nclass Node: def __init__(self, key, left=None, right=None): self.key = key self.left = left self.right = right def inorder(node, out): if node is None: return inorder(node.left, out) out.append(node.key) inorder(node.right, out) root = Node(5, Node(3, Node(2), Node(4)), Node(7)) result = [] inorder(root, result) print(result) Scenario 2: Convert an expression tree to infix notation (JavaScript) Background: compilers, formula editors, and rule engines often organize expressions as binary trees.\nWhy it fits: inorder traversal naturally matches the reading order of infix expressions, which makes the result more human-friendly.\nfunction Node(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function inorder(node) { if (!node) return \u0026#34;\u0026#34;; if (!node.left \u0026amp;\u0026amp; !node.right) return String(node.val); return `(${inorder(node.left)} ${node.val} ${inorder(node.right)})`; } const tree = new Node(\u0026#34;*\u0026#34;, new Node(\u0026#34;+\u0026#34;, new Node(1), new Node(2)), new Node(3)); console.log(inorder(tree)); Scenario 3: Inspect local ordering in a tree-based config (Go) Background: some rule systems use \u0026ldquo;left branch / current node / right branch\u0026rdquo; as a stable manual inspection order.\nWhy it fits: inorder traversal lets developers inspect nodes in a fixed local order, which helps with diffs and manual verification.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Name string Left *Node Right *Node } func inorder(node *Node, out *[]string) { if node == nil { return } inorder(node.Left, out) *out = append(*out, node.Name) inorder(node.Right, out) } func main() { root := \u0026amp;Node{\u0026#34;root\u0026#34;, \u0026amp;Node{\u0026#34;L\u0026#34;, nil, nil}, \u0026amp;Node{\u0026#34;R\u0026#34;, nil, nil}} order := []string{} inorder(root, \u0026amp;order) fmt.Println(order) } R - Reflection (Analysis and Deeper Understanding) Complexity Analysis Time complexity: O(n), because each node is processed exactly once Space complexity: Recursive version: O(h) call stack Explicit-stack version: O(h) auxiliary stack Alternative Approaches Method Time Extra Space Notes Recursion O(n) O(h) Most intuitive and shortest Explicit stack O(n) O(h) Most common interview template and highly reusable Morris traversal O(n) O(1) Temporarily modifies tree structure and is harder to reason about Common Mistakes and Pitfalls Mixing up the visitation points of preorder, inorder, and postorder Forgetting to move to cur.right after popping in the iterative version Writing only while cur != null and missing the case where nodes are still waiting in the stack Accessing node.left directly in recursion without a null check first Common Questions and Notes 1. Is inorder traversal always sorted? No. It is sorted only when the tree satisfies the BST property.\n2. Which is more recommended, recursion or iteration? In interviews, you should know both. Early on, recursion is the best way to build the definition in your head; after that, the explicit stack template is the most stable pattern to memorize.\n3. Is Morris traversal worth memorizing? It is worth understanding, but it should not be your first priority at the fundamentals stage. Get recursion and explicit stacks stable first.\nBest Practices and Suggestions Memorize the definition in one sentence: left, root, right For the iterative template, remember: \u0026ldquo;push left chain -\u0026gt; pop and visit -\u0026gt; move right\u0026rdquo; Whenever you see BST, think of \u0026ldquo;inorder = sorted\u0026rdquo; For tree problems, writing space as O(h) is often more accurate than writing only O(n) S - Summary The core of inorder traversal is the fixed visitation order: left -\u0026gt; root -\u0026gt; right Recursion matches the definition best, while the explicit stack version is the best interview template This problem trains two core abilities: tree recursion and manual simulation of the call stack In BSTs, expression trees, and configuration trees, inorder thinking has practical engineering value Once you can write 94 smoothly, BST validation and k-th-smallest problems become much easier References and Further Reading LeetCode 94: Binary Tree Inorder Traversal LeetCode 144: Binary Tree Preorder Traversal LeetCode 145: Binary Tree Postorder Traversal LeetCode 98: Validate Binary Search Tree LeetCode 230: Kth Smallest Element in a BST CTA First handwrite the recursive version, then rewrite the explicit-stack version without looking at the answer.\nIf you can reliably finish LeetCode 94 in about three minutes, your tree-traversal fundamentals are already in place.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def inorder_traversal(root): res = [] stack = [] cur = root while cur is not None or stack: while cur is not None: stack.append(cur) cur = cur.left cur = stack.pop() res.append(cur.val) cur = cur.right return res if __name__ == \u0026#34;__main__\u0026#34;: root = TreeNode(1, None, TreeNode(2, TreeNode(3), None)) print(inorder_traversal(root)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; }; struct TreeNode* new_node(int val) { struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); node-\u0026gt;val = val; node-\u0026gt;left = NULL; node-\u0026gt;right = NULL; return node; } int* inorderTraversal(struct TreeNode* root, int* returnSize) { struct TreeNode* stack[128]; int top = 0; int* res = (int*)malloc(sizeof(int) * 128); *returnSize = 0; struct TreeNode* cur = root; while (cur != NULL || top \u0026gt; 0) { while (cur != NULL) { stack[top++] = cur; cur = cur-\u0026gt;left; } cur = stack[--top]; res[(*returnSize)++] = cur-\u0026gt;val; cur = cur-\u0026gt;right; } return res; } void free_tree(struct TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { struct TreeNode* root = new_node(1); root-\u0026gt;right = new_node(2); root-\u0026gt;right-\u0026gt;left = new_node(3); int n = 0; int* ans = inorderTraversal(root, \u0026amp;n); for (int i = 0; i \u0026lt; n; ++i) { printf(\u0026#34;%d%s\u0026#34;, ans[i], i + 1 == n ? \u0026#34;\\n\u0026#34; : \u0026#34; \u0026#34;); } free(ans); free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;stack\u0026gt; #include \u0026lt;vector\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; std::vector\u0026lt;int\u0026gt; inorderTraversal(TreeNode* root) { std::vector\u0026lt;int\u0026gt; res; std::stack\u0026lt;TreeNode*\u0026gt; st; TreeNode* cur = root; while (cur || !st.empty()) { while (cur) { st.push(cur); cur = cur-\u0026gt;left; } cur = st.top(); st.pop(); res.push_back(cur-\u0026gt;val); cur = cur-\u0026gt;right; } return res; } void freeTree(TreeNode* root) { if (!root) return; freeTree(root-\u0026gt;left); freeTree(root-\u0026gt;right); delete root; } int main() { TreeNode* root = new TreeNode(1); root-\u0026gt;right = new TreeNode(2); root-\u0026gt;right-\u0026gt;left = new TreeNode(3); auto ans = inorderTraversal(root); for (size_t i = 0; i \u0026lt; ans.size(); ++i) { std::cout \u0026lt;\u0026lt; ans[i] \u0026lt;\u0026lt; (i + 1 == ans.size() ? \u0026#39;\\n\u0026#39; : \u0026#39; \u0026#39;); } freeTree(root); return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int Left *TreeNode Right *TreeNode } func inorderTraversal(root *TreeNode) []int { res := []int{} stack := []*TreeNode{} cur := root for cur != nil || len(stack) \u0026gt; 0 { for cur != nil { stack = append(stack, cur) cur = cur.Left } cur = stack[len(stack)-1] stack = stack[:len(stack)-1] res = append(res, cur.Val) cur = cur.Right } return res } func main() { root := \u0026amp;TreeNode{Val: 1} root.Right = \u0026amp;TreeNode{Val: 2, Left: \u0026amp;TreeNode{Val: 3}} fmt.Println(inorderTraversal(root)) } #[derive(Debug)] struct TreeNode { val: i32, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } fn inorder_traversal(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;) -\u0026gt; Vec\u0026lt;i32\u0026gt; { fn dfs(node: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, res: \u0026amp;mut Vec\u0026lt;i32\u0026gt;) { if let Some(node) = node { dfs(\u0026amp;node.left, res); res.push(node.val); dfs(\u0026amp;node.right, res); } } let mut res = vec![]; dfs(root, \u0026amp;mut res); res } fn main() { let root = Some(Box::new(TreeNode { val: 1, left: None, right: Some(Box::new(TreeNode { val: 2, left: Some(Box::new(TreeNode { val: 3, left: None, right: None, })), right: None, })), })); println!(\u0026#34;{:?}\u0026#34;, inorder_traversal(\u0026amp;root)); } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function inorderTraversal(root) { const res = []; const stack = []; let cur = root; while (cur || stack.length) { while (cur) { stack.push(cur); cur = cur.left; } cur = stack.pop(); res.push(cur.val); cur = cur.right; } return res; } const root = new TreeNode(1, null, new TreeNode(2, new TreeNode(3), null)); console.log(inorderTraversal(root)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/binary-tree/94-binary-tree-inorder-traversal/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nBinary tree traversal is the starting point of most tree templates, and inorder traversal is one of the cleanest problems for understanding both recursive thinking and explicit stack simulation. This ACERS guide uses LeetCode 94 to explain the left-root-right order, the iterative stack template, and why the pattern matters in real engineering work.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003estack\u003c/code\u003e, \u003ccode\u003einorder traversal\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Hot100, Binary Tree Inorder Traversal, inorder traversal, explicit stack, LeetCode 94\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A systematic guide to LeetCode 94 from recursion to explicit stacks, with engineering scenarios and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHot100 learners who want to lock in a stable tree-traversal template\u003c/li\u003e\n\u003cli\u003eDevelopers moving from arrays and linked lists to trees, and still mixing up preorder, inorder, and postorder\u003c/li\u003e\n\u003cli\u003eEngineers who want to reuse the left-root-right idea in BSTs, expression trees, or syntax trees\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eInorder traversal is not hard by itself, but its training value is high:\u003c/p\u003e","title":"Hot100: Binary Tree Inorder Traversal (Recursion / Stack ACERS Guide)"},{"content":" Subtitle / Summary\nThis is not a memorization question. It is core cache-engineering practice: satisfy fast lookup and least-recently-used eviction at the same time, both in constant average time. We derive the optimal structure from naive approaches and provide runnable implementations.\nReading time: 14-18 min Tags: LRU, hash map, doubly linked list, system design SEO keywords: LRU Cache, LeetCode 146, hash map, doubly linked list, O(1) Meta description: Build an LRU cache with hash map + doubly linked list to achieve O(1) average get/put, with engineering use cases, pitfalls, and six-language implementations. A — Algorithm (Problem \u0026amp; Algorithm) Problem Restatement Design and implement LRUCache:\nLRUCache(int capacity): initialize with positive capacity int get(int key): return value if key exists, else -1 void put(int key, int value): key exists: update value and mark it as most recently used key not exists: insert key-value over capacity: evict least recently used key Both operations must run in average O(1) time.\nExample 1 (Operation Sequence) LRUCache cache = new LRUCache(2) cache.put(1, 1) // cache: {1=1} cache.put(2, 2) // cache: {1=1, 2=2} cache.get(1) // return 1, and 1 becomes most recent cache.put(3, 3) // capacity full, evict key=2 cache.get(2) // return -1 cache.put(4, 4) // evict key=1 cache.get(1) // return -1 cache.get(3) // return 3 cache.get(4) // return 4 Example 2 (Update Existing Key) LRUCache cache = new LRUCache(2) cache.put(1, 10) cache.put(1, 99) // update value, key=1 is refreshed as most recent cache.get(1) // return 99 Target Readers LeetCode learners who want to master data-structure composition Backend/middleware engineers implementing local caches Interview candidates who know the answer headline but not the invariants Background / Motivation Caching trades space for time, but cache space is limited.\nWhen full, we must evict keys. LRU (Least Recently Used) assumes:\nRecently accessed data is more likely to be accessed again Long-idle data is a better eviction candidate Real-world examples:\nAPI response caching Database hot-record caching Local page/session state caching Core Concepts LRU policy: evict the least recently used key Access refresh: successful get must make key most recently used Capacity constraint: put on new key may trigger immediate eviction O(1) average complexity: neither get nor put can do linear scans Derivation: from Naive to Optimal Naive approach 1: array tracks recency order get: hash lookup O(1), but moving key to newest in array is O(n) put: eviction can be O(1), but recency updates are often O(n) Conclusion: cannot guarantee O(1) for both operations.\nNaive approach 2: linked list only O(1) insert/delete at ends key lookup still O(n) Conclusion: lookup too slow.\nKey Observation We need both:\nFast key-to-node location -\u0026gt; hash map Fast recency reordering -\u0026gt; doubly linked list Method Choice (Optimal) Hash map: key -\u0026gt; node pointer/iterator Doubly linked list: head side = most recently used (MRU) tail side = least recently used (LRU) Operations:\nget(key): if hit, move node to front put(key,value): exists: update and move to front not exists: if full, remove tail node; then insert at front C — Concepts (Core Ideas) Data Structure Model HashMap: key -\u0026gt; Node* DoubleList: head \u0026lt;-\u0026gt; n1 \u0026lt;-\u0026gt; n2 \u0026lt;-\u0026gt; ... \u0026lt;-\u0026gt; nk \u0026lt;-\u0026gt; tail ^ MRU LRU ^ Invariants List order is always newest -\u0026gt; oldest Every key in map points to exactly one list node list_size == map_size Atomic Operations remove(node): unlink any known node in O(1) add_front(node): insert at front in O(1) move_to_front(node): remove + add_front in O(1) pop_back(): remove least recent node in O(1) Practice Guide / Steps Define doubly-linked node with key, value, prev, next Create head/tail sentinels to avoid edge-case branches Store key -\u0026gt; node in map On get hit, move node to front On new put, evict from back if full, then insert front Always insert/refresh at front to represent recent usage Minimal runnable Python example:\nclass Node: def __init__(self, key=0, val=0): self.key = key self.val = val self.prev = None self.next = None class LRUCache: def __init__(self, capacity: int): self.cap = capacity self.map = {} self.head = Node() # MRU side sentinel self.tail = Node() # LRU side sentinel self.head.next = self.tail self.tail.prev = self.head def _remove(self, node: Node) -\u0026gt; None: p, n = node.prev, node.next p.next = n n.prev = p def _add_front(self, node: Node) -\u0026gt; None: node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _move_front(self, node: Node) -\u0026gt; None: self._remove(node) self._add_front(node) def _pop_lru(self) -\u0026gt; Node: node = self.tail.prev self._remove(node) return node def get(self, key: int) -\u0026gt; int: node = self.map.get(key) if node is None: return -1 self._move_front(node) return node.val def put(self, key: int, value: int) -\u0026gt; None: if self.cap == 0: return node = self.map.get(key) if node is not None: node.val = value self._move_front(node) return if len(self.map) == self.cap: old = self._pop_lru() del self.map[old.key] node = Node(key, value) self.map[key] = node self._add_front(node) if __name__ == \u0026#34;__main__\u0026#34;: c = LRUCache(2) c.put(1, 1) c.put(2, 2) print(c.get(1)) # 1 c.put(3, 3) print(c.get(2)) # -1 E — Engineering (Real-world Scenarios) Scenario 1: short-term API response cache (Python) Background: hot API endpoints receive repeated requests with same parameters.\nWhy it fits: recently accessed keys are likely to be reused, while capacity stays bounded.\nimport time cache = {} def fetch_user_profile(uid: int) -\u0026gt; dict: # Simulate slow query time.sleep(0.02) return {\u0026#34;uid\u0026#34;: uid, \u0026#34;name\u0026#34;: f\u0026#34;user-{uid}\u0026#34;} print(fetch_user_profile(7)) Scenario 2: config-center local cache in services (Go) Background: microservices read config frequently; remote fetch has network overhead.\nWhy it fits: recently used config keys are more likely to be accessed again.\npackage main import \u0026#34;fmt\u0026#34; func main() { // In production, LRU can be a layer in your config client. fmt.Println(\u0026#34;config cache ready with LRU policy\u0026#34;) } Scenario 3: frontend page-data cache (JavaScript) Background: SPA route switching benefits from reusing recently visited data.\nWhy it fits: recently viewed pages have higher revisit probability.\nconst pageState = new Map(); pageState.set(\u0026#34;feed?page=1\u0026#34;, { items: [1, 2, 3] }); console.log(pageState.get(\u0026#34;feed?page=1\u0026#34;)); R — Reflection (Deep Dive) Complexity get: hash lookup + list move, average O(1) put: hash lookup/insert + list insert/delete, average O(1) Space: O(capacity) Alternative Comparison Approach get put Problem Hash map + timestamp only O(1) eviction often needs O(n) scan slow eviction Linked list only O(n) O(1) slow lookup Hash map + doubly linked list O(1) O(1) slightly more implementation detail Common Mistakes Hit in get but forget to refresh recency (not moving to front) Existing key in put updates value but not recency Evict from list but forget to remove map entry (stale pointer) Forgetting the capacity == 0 edge case Why this method is production-friendly Stable performance and predictable constant-time behavior Easy extensibility: TTL, hit-rate metrics, lock wrappers Clear invariants and atomic operations for testing/debugging FAQ Q1: Why doubly linked list, not singly linked list? Removing an arbitrary node in singly linked list needs predecessor lookup, usually O(n). Doubly linked list deletes known nodes in O(1).\nQ2: Why store key inside each list node? When evicting tail node, we must remove its key from map in O(1). Without storing key in node, that step becomes expensive.\nQ3: How is this different from LFU? LRU evicts by recency; LFU evicts by frequency. LFU requires more complex structures and update logic.\nBest Practices Always use head/tail sentinels to avoid fragile boundary branches Keep atomic list ops private: remove/add_front/move/pop_back Test operation sequences, not only final state snapshots Lock correctness first, then optimize concurrency granularity S — Summary Key takeaways:\nLRU is recency ordering + fixed-capacity eviction. O(1) requires data-structure composition, not a single structure. Stability comes from invariants: order consistency, map consistency, capacity consistency. This model maps directly to practical caches in backend and frontend systems. It is a strong base for TTL-LRU, concurrent LRU, and LFU extensions. Recommended follow-ups:\nLeetCode 460 LFU Cache Redis eviction policy docs (allkeys-lru, volatile-lru) Designing Data-Intensive Applications caching chapters System design materials on local cache consistency strategies Runnable Multi-language Implementations Python class Node: def __init__(self, key=0, val=0): self.key = key self.val = val self.prev = None self.next = None class LRUCache: def __init__(self, capacity: int): self.cap = capacity self.map = {} self.head = Node() self.tail = Node() self.head.next = self.tail self.tail.prev = self.head def _remove(self, node: Node) -\u0026gt; None: p, n = node.prev, node.next p.next = n n.prev = p def _add_front(self, node: Node) -\u0026gt; None: node.prev = self.head node.next = self.head.next self.head.next.prev = node self.head.next = node def _move_front(self, node: Node) -\u0026gt; None: self._remove(node) self._add_front(node) def _pop_lru(self) -\u0026gt; Node: node = self.tail.prev self._remove(node) return node def get(self, key: int) -\u0026gt; int: node = self.map.get(key) if node is None: return -1 self._move_front(node) return node.val def put(self, key: int, value: int) -\u0026gt; None: if self.cap == 0: return node = self.map.get(key) if node: node.val = value self._move_front(node) return if len(self.map) == self.cap: old = self._pop_lru() del self.map[old.key] node = Node(key, value) self.map[key] = node self._add_front(node) if __name__ == \u0026#34;__main__\u0026#34;: c = LRUCache(2) c.put(1, 1) c.put(2, 2) print(c.get(1)) # 1 c.put(3, 3) print(c.get(2)) # -1 c.put(4, 4) print(c.get(1), c.get(3), c.get(4)) # -1 3 4 C #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define HASH_SIZE 4093 typedef struct Node { int key; int val; struct Node* prev; struct Node* next; } Node; typedef struct Entry { int key; Node* node; struct Entry* next; } Entry; typedef struct { Entry* buckets[HASH_SIZE]; } HashMap; typedef struct { int cap; int size; HashMap map; Node head; Node tail; } LRUCache; unsigned int h(int key) { unsigned int x = (unsigned int)key; return (x * 2654435761u) % HASH_SIZE; } Node* map_get(HashMap* m, int key) { unsigned int idx = h(key); Entry* e = m-\u0026gt;buckets[idx]; while (e) { if (e-\u0026gt;key == key) return e-\u0026gt;node; e = e-\u0026gt;next; } return NULL; } void map_put(HashMap* m, int key, Node* node) { unsigned int idx = h(key); Entry* e = m-\u0026gt;buckets[idx]; while (e) { if (e-\u0026gt;key == key) { e-\u0026gt;node = node; return; } e = e-\u0026gt;next; } Entry* ne = (Entry*)malloc(sizeof(Entry)); ne-\u0026gt;key = key; ne-\u0026gt;node = node; ne-\u0026gt;next = m-\u0026gt;buckets[idx]; m-\u0026gt;buckets[idx] = ne; } void map_remove(HashMap* m, int key) { unsigned int idx = h(key); Entry* cur = m-\u0026gt;buckets[idx]; Entry* pre = NULL; while (cur) { if (cur-\u0026gt;key == key) { if (pre) pre-\u0026gt;next = cur-\u0026gt;next; else m-\u0026gt;buckets[idx] = cur-\u0026gt;next; free(cur); return; } pre = cur; cur = cur-\u0026gt;next; } } void list_remove(Node* n) { n-\u0026gt;prev-\u0026gt;next = n-\u0026gt;next; n-\u0026gt;next-\u0026gt;prev = n-\u0026gt;prev; } void list_add_front(LRUCache* c, Node* n) { n-\u0026gt;prev = \u0026amp;c-\u0026gt;head; n-\u0026gt;next = c-\u0026gt;head.next; c-\u0026gt;head.next-\u0026gt;prev = n; c-\u0026gt;head.next = n; } void move_front(LRUCache* c, Node* n) { list_remove(n); list_add_front(c, n); } Node* pop_lru(LRUCache* c) { Node* n = c-\u0026gt;tail.prev; list_remove(n); return n; } LRUCache* lruCreate(int capacity) { LRUCache* c = (LRUCache*)calloc(1, sizeof(LRUCache)); c-\u0026gt;cap = capacity; c-\u0026gt;size = 0; c-\u0026gt;head.next = \u0026amp;c-\u0026gt;tail; c-\u0026gt;tail.prev = \u0026amp;c-\u0026gt;head; return c; } int lruGet(LRUCache* c, int key) { Node* n = map_get(\u0026amp;c-\u0026gt;map, key); if (!n) return -1; move_front(c, n); return n-\u0026gt;val; } void lruPut(LRUCache* c, int key, int value) { if (c-\u0026gt;cap == 0) return; Node* n = map_get(\u0026amp;c-\u0026gt;map, key); if (n) { n-\u0026gt;val = value; move_front(c, n); return; } if (c-\u0026gt;size == c-\u0026gt;cap) { Node* old = pop_lru(c); map_remove(\u0026amp;c-\u0026gt;map, old-\u0026gt;key); free(old); c-\u0026gt;size--; } Node* nn = (Node*)malloc(sizeof(Node)); nn-\u0026gt;key = key; nn-\u0026gt;val = value; list_add_front(c, nn); map_put(\u0026amp;c-\u0026gt;map, key, nn); c-\u0026gt;size++; } void lruFree(LRUCache* c) { Node* cur = c-\u0026gt;head.next; while (cur != \u0026amp;c-\u0026gt;tail) { Node* nxt = cur-\u0026gt;next; free(cur); cur = nxt; } for (int i = 0; i \u0026lt; HASH_SIZE; i++) { Entry* e = c-\u0026gt;map.buckets[i]; while (e) { Entry* ne = e-\u0026gt;next; free(e); e = ne; } } free(c); } int main(void) { LRUCache* c = lruCreate(2); lruPut(c, 1, 1); lruPut(c, 2, 2); printf(\u0026#34;%d\\n\u0026#34;, lruGet(c, 1)); // 1 lruPut(c, 3, 3); printf(\u0026#34;%d\\n\u0026#34;, lruGet(c, 2)); // -1 lruPut(c, 4, 4); printf(\u0026#34;%d %d %d\\n\u0026#34;, lruGet(c, 1), lruGet(c, 3), lruGet(c, 4)); // -1 3 4 lruFree(c); return 0; } C++ #include \u0026lt;iostream\u0026gt; #include \u0026lt;list\u0026gt; #include \u0026lt;unordered_map\u0026gt; using namespace std; class LRUCache { private: int cap; list\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; dq; // front = MRU, back = LRU unordered_map\u0026lt;int, list\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt;::iterator\u0026gt; pos; public: explicit LRUCache(int capacity) : cap(capacity) {} int get(int key) { auto it = pos.find(key); if (it == pos.end()) return -1; dq.splice(dq.begin(), dq, it-\u0026gt;second); return it-\u0026gt;second-\u0026gt;second; } void put(int key, int value) { if (cap == 0) return; auto it = pos.find(key); if (it != pos.end()) { it-\u0026gt;second-\u0026gt;second = value; dq.splice(dq.begin(), dq, it-\u0026gt;second); return; } if ((int)dq.size() == cap) { int oldKey = dq.back().first; pos.erase(oldKey); dq.pop_back(); } dq.push_front({key, value}); pos[key] = dq.begin(); } }; int main() { LRUCache c(2); c.put(1, 1); c.put(2, 2); cout \u0026lt;\u0026lt; c.get(1) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 1 c.put(3, 3); cout \u0026lt;\u0026lt; c.get(2) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // -1 c.put(4, 4); cout \u0026lt;\u0026lt; c.get(1) \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; c.get(3) \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; c.get(4) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // -1 3 4 return 0; } Go package main import ( \u0026#34;container/list\u0026#34; \u0026#34;fmt\u0026#34; ) type entry struct { key int value int } type LRUCache struct { cap int ll *list.List pos map[int]*list.Element } func Constructor(capacity int) LRUCache { return LRUCache{ cap: capacity, ll: list.New(), pos: make(map[int]*list.Element), } } func (c *LRUCache) Get(key int) int { e, ok := c.pos[key] if !ok { return -1 } c.ll.MoveToFront(e) return e.Value.(entry).value } func (c *LRUCache) Put(key int, value int) { if c.cap == 0 { return } if e, ok := c.pos[key]; ok { e.Value = entry{key: key, value: value} c.ll.MoveToFront(e) return } if c.ll.Len() == c.cap { back := c.ll.Back() old := back.Value.(entry) delete(c.pos, old.key) c.ll.Remove(back) } e := c.ll.PushFront(entry{key: key, value: value}) c.pos[key] = e } func main() { c := Constructor(2) c.Put(1, 1) c.Put(2, 2) fmt.Println(c.Get(1)) // 1 c.Put(3, 3) fmt.Println(c.Get(2)) // -1 c.Put(4, 4) fmt.Println(c.Get(1), c.Get(3), c.Get(4)) // -1 3 4 } Rust use std::collections::HashMap; #[derive(Clone, Debug)] struct Node { key: i32, val: i32, prev: usize, next: usize, } struct LRUCache { cap: usize, len: usize, map: HashMap\u0026lt;i32, usize\u0026gt;, // key -\u0026gt; node index nodes: Vec\u0026lt;Node\u0026gt;, free: Vec\u0026lt;usize\u0026gt;, head: usize, // sentinel tail: usize, // sentinel } impl LRUCache { fn new(capacity: i32) -\u0026gt; Self { let head = 0usize; let tail = 1usize; let nodes = vec![ Node { key: 0, val: 0, prev: head, next: tail, }, Node { key: 0, val: 0, prev: head, next: tail, }, ]; let mut c = Self { cap: capacity.max(0) as usize, len: 0, map: HashMap::new(), nodes, free: Vec::new(), head, tail, }; c.nodes[c.head].next = c.tail; c.nodes[c.tail].prev = c.head; c } fn detach(\u0026amp;mut self, idx: usize) { let p = self.nodes[idx].prev; let n = self.nodes[idx].next; self.nodes[p].next = n; self.nodes[n].prev = p; } fn insert_front(\u0026amp;mut self, idx: usize) { let first = self.nodes[self.head].next; self.nodes[idx].prev = self.head; self.nodes[idx].next = first; self.nodes[self.head].next = idx; self.nodes[first].prev = idx; } fn move_front(\u0026amp;mut self, idx: usize) { self.detach(idx); self.insert_front(idx); } fn pop_lru(\u0026amp;mut self) -\u0026gt; Option\u0026lt;usize\u0026gt; { let idx = self.nodes[self.tail].prev; if idx == self.head { return None; } self.detach(idx); Some(idx) } fn alloc_node(\u0026amp;mut self, key: i32, val: i32) -\u0026gt; usize { if let Some(idx) = self.free.pop() { self.nodes[idx] = Node { key, val, prev: self.head, next: self.tail, }; idx } else { self.nodes.push(Node { key, val, prev: self.head, next: self.tail, }); self.nodes.len() - 1 } } fn get(\u0026amp;mut self, key: i32) -\u0026gt; i32 { let idx = match self.map.get(\u0026amp;key) { Some(\u0026amp;i) =\u0026gt; i, None =\u0026gt; return -1, }; self.move_front(idx); self.nodes[idx].val } fn put(\u0026amp;mut self, key: i32, value: i32) { if self.cap == 0 { return; } if let Some(\u0026amp;idx) = self.map.get(\u0026amp;key) { self.nodes[idx].val = value; self.move_front(idx); return; } if self.len == self.cap { if let Some(old_idx) = self.pop_lru() { let old_key = self.nodes[old_idx].key; self.map.remove(\u0026amp;old_key); self.free.push(old_idx); self.len -= 1; } } let idx = self.alloc_node(key, value); self.insert_front(idx); self.map.insert(key, idx); self.len += 1; } } fn main() { let mut c = LRUCache::new(2); c.put(1, 1); c.put(2, 2); println!(\u0026#34;{}\u0026#34;, c.get(1)); // 1 c.put(3, 3); println!(\u0026#34;{}\u0026#34;, c.get(2)); // -1 c.put(4, 4); println!(\u0026#34;{} {} {}\u0026#34;, c.get(1), c.get(3), c.get(4)); // -1 3 4 } JavaScript class Node { constructor(key = 0, value = 0) { this.key = key; this.value = value; this.prev = null; this.next = null; } } class LRUCache { constructor(capacity) { this.cap = capacity; this.map = new Map(); this.head = new Node(); this.tail = new Node(); this.head.next = this.tail; this.tail.prev = this.head; } _remove(node) { node.prev.next = node.next; node.next.prev = node.prev; } _addFront(node) { node.prev = this.head; node.next = this.head.next; this.head.next.prev = node; this.head.next = node; } _moveFront(node) { this._remove(node); this._addFront(node); } _popLRU() { const node = this.tail.prev; this._remove(node); return node; } get(key) { if (!this.map.has(key)) return -1; const node = this.map.get(key); this._moveFront(node); return node.value; } put(key, value) { if (this.cap === 0) return; if (this.map.has(key)) { const node = this.map.get(key); node.value = value; this._moveFront(node); return; } if (this.map.size === this.cap) { const old = this._popLRU(); this.map.delete(old.key); } const node = new Node(key, value); this.map.set(key, node); this._addFront(node); } } const c = new LRUCache(2); c.put(1, 1); c.put(2, 2); console.log(c.get(1)); // 1 c.put(3, 3); console.log(c.get(2)); // -1 c.put(4, 4); console.log(c.get(1), c.get(3), c.get(4)); // -1 3 4 CTA Run these three drills now:\nRe-implement remove / add_front / pop_back without looking at the answer. Stress-test operation sequences: repeated put on same key, capacity 1, capacity 0. Solve LeetCode 460 (LFU) and compare structure complexity with LRU. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/146-lru-cache/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThis is not a memorization question. It is core cache-engineering practice: satisfy fast lookup and least-recently-used eviction at the same time, both in constant average time. We derive the optimal structure from naive approaches and provide runnable implementations.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 14-18 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eLRU\u003c/code\u003e, \u003ccode\u003ehash map\u003c/code\u003e, \u003ccode\u003edoubly linked list\u003c/code\u003e, \u003ccode\u003esystem design\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LRU Cache, LeetCode 146, hash map, doubly linked list, O(1)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Build an LRU cache with hash map + doubly linked list to achieve O(1) average \u003ccode\u003eget/put\u003c/code\u003e, with engineering use cases, pitfalls, and six-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm-problem--algorithm\"\u003eA — Algorithm (Problem \u0026amp; Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eDesign and implement \u003ccode\u003eLRUCache\u003c/code\u003e:\u003c/p\u003e","title":"LeetCode 146: LRU Cache Design with O(1) Hash Map + Doubly Linked List"},{"content":" Subtitle / Summary\nThe hard part is not deletion itself, but locating the predecessor of the nth node from the end in a singly linked list. This article derives the one-pass two-pointer solution from simpler baselines and explains correctness, boundaries, and engineering transfer.\nReading time: 12-15 min Tags: linked list, two pointers, interview high frequency SEO keywords: LeetCode 19, Remove Nth Node From End of List, linked list, fast/slow pointers, dummy node Meta description: A complete ACERS walkthrough for removing the nth node from the end: from brute force to one-pass two pointers, with complexity, pitfalls, engineering scenarios, and Python/C/C++/Go/Rust/JS implementations. Target Readers Beginners building a stable template for linked-list interview problems Developers who know fast/slow pointers but still make boundary mistakes Backend/system engineers who want to transfer problem-solving templates to chain-structured data in production Background / Motivation \u0026ldquo;Remove the nth node from the end\u0026rdquo; is a classic medium-level linked-list problem. The challenge is usually not the delete operation itself, but:\nSingly linked lists cannot traverse backward from tail; Deleting the head node complicates return handling; Incorrect next rewiring can easily break the list. Once you master this problem, you get a reusable pattern: dummy node + fixed pointer gap, which is useful in many list operations (split, reverse by group, merge variants).\nCore Concepts Singly linked list: each node has only next, so traversal is one-directional. Dummy node: add a virtual node before head to unify head deletion and middle deletion. Fast/slow fixed gap: move fast ahead by n steps first; when fast reaches the end, slow lands at the predecessor of the target node. A - Algorithm (Problem \u0026amp; Algorithm) Problem Restatement Given the head of a linked list, remove the nth node from the end of the list and return its head.\nInput / Output Item Type Meaning head ListNode head of a singly linked list n int nth position from the end return ListNode head after deletion Example 1 Input: head = [1,2,3,4,5], n = 2 Output: [1,2,3,5] Explanation: the 2nd node from the end is 4, so remove it.\nExample 2 Input: head = [1], n = 1 Output: [] Explanation: removing the only node leaves an empty list.\nExample 3 Input: head = [1,2], n = 2 Output: [2] Explanation: the 2nd node from the end is the head node 1.\nPointer-gap diagram dummy -\u0026gt; 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; null After moving fast by n=2: dummy -\u0026gt; 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; null slow fast Move both together until fast reaches the tail: dummy -\u0026gt; 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; null slow fast Now slow.next is the target node (4) C - Concepts (Core Ideas) Derivation: from naive to optimal Naive array conversion\nConvert to array, remove by index, then rebuild list.\nWorks, but uses O(L) extra space. Avoids linked-list strengths instead of using them. Two-pass traversal\nFirst pass gets length L; second pass stops at index L - n - 1.\nTime O(L), space O(1). Still needs two scans; head deletion handling is awkward without dummy. Best approach: one-pass two pointers + dummy\nMove fast forward n steps first. Move fast and slow together until fast.next == null. slow.next is exactly the node to remove. Method category Two pointers Gap maintenance In-place pointer rewiring Correctness intuition Let list length be L. If fast and slow maintain a fixed gap of n nodes:\nWhen fast is at index L - 1 (tail), slow is at index L - n - 1 (predecessor of target). So removing slow.next is exactly removing the nth node from the end.\nPractice Guide / Steps Create dummy and point dummy.next = head. Initialize fast = slow = dummy. Move fast ahead by n steps. While fast.next != null, move both pointers forward. Delete node by rewiring: slow.next = slow.next.next. Return dummy.next. Runnable Python example:\nfrom typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def remove_nth_from_end(head: Optional[ListNode], n: int) -\u0026gt; Optional[ListNode]: dummy = ListNode(0, head) fast = slow = dummy for _ in range(n): fast = fast.next while fast.next is not None: fast = fast.next slow = slow.next slow.next = slow.next.next return dummy.next def from_list(nums: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy for x in nums: tail.next = ListNode(x) tail = tail.next return dummy.next def to_list(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: print(to_list(remove_nth_from_end(from_list([1, 2, 3, 4, 5]), 2))) # [1,2,3,5] print(to_list(remove_nth_from_end(from_list([1]), 1))) # [] print(to_list(remove_nth_from_end(from_list([1, 2]), 2))) # [2] E - Engineering (Real-world Applications) The transferable idea is: remove the kth element from tail in a single-direction chain.\nScenario 1: retry-trace chain trimming in backend jobs (Go) Background: microservices often keep a singly linked retry trace for task failures.\nWhy it fits: deleting the nth record from tail can reuse the exact fast/slow template.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { ID int Next *Node } func removeNthFromEnd(head *Node, n int) *Node { dummy := \u0026amp;Node{Next: head} fast, slow := dummy, dummy for i := 0; i \u0026lt; n; i++ { fast = fast.Next } for fast.Next != nil { fast = fast.Next slow = slow.Next } slow.Next = slow.Next.Next return dummy.Next } func printList(head *Node) { for p := head; p != nil; p = p.Next { fmt.Printf(\u0026#34;%d \u0026#34;, p.ID) } fmt.Println() } func main() { head := \u0026amp;Node{1, \u0026amp;Node{2, \u0026amp;Node{3, \u0026amp;Node{4, nil}}}} head = removeNthFromEnd(head, 2) printList(head) // 1 2 4 } Scenario 2: free-block chain cleanup in systems code (C) Background: simplified memory managers may keep free blocks in a singly linked list.\nWhy it fits: removing the nth node from the end can be done in one scan with deterministic pointer rewiring.\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct Node { int addr; struct Node* next; }; struct Node* remove_nth_from_end(struct Node* head, int n) { struct Node dummy = {0, head}; struct Node *fast = \u0026amp;dummy, *slow = \u0026amp;dummy; for (int i = 0; i \u0026lt; n; ++i) fast = fast-\u0026gt;next; while (fast-\u0026gt;next) { fast = fast-\u0026gt;next; slow = slow-\u0026gt;next; } struct Node* del = slow-\u0026gt;next; slow-\u0026gt;next = del-\u0026gt;next; free(del); return dummy.next; } int main() { struct Node* n4 = (struct Node*)malloc(sizeof(struct Node)); struct Node* n3 = (struct Node*)malloc(sizeof(struct Node)); struct Node* n2 = (struct Node*)malloc(sizeof(struct Node)); struct Node* n1 = (struct Node*)malloc(sizeof(struct Node)); n1-\u0026gt;addr = 10; n1-\u0026gt;next = n2; n2-\u0026gt;addr = 20; n2-\u0026gt;next = n3; n3-\u0026gt;addr = 30; n3-\u0026gt;next = n4; n4-\u0026gt;addr = 40; n4-\u0026gt;next = NULL; struct Node* head = remove_nth_from_end(n1, 3); for (struct Node* p = head; p; p = p-\u0026gt;next) printf(\u0026#34;%d \u0026#34;, p-\u0026gt;addr); printf(\u0026#34;\\n\u0026#34;); while (head) { struct Node* t = head; head = head-\u0026gt;next; free(t); } return 0; } Scenario 3: undo-chain compaction in frontend editor state (JavaScript) Background: an editor can model undo history as a singly linked chain.\nWhy it fits: deleting the nth snapshot from tail uses the same list primitive as this problem.\nclass Node { constructor(v, next = null) { this.v = v; this.next = next; } } function removeNthFromEnd(head, n) { const dummy = new Node(0, head); let fast = dummy; let slow = dummy; for (let i = 0; i \u0026lt; n; i++) fast = fast.next; while (fast.next !== null) { fast = fast.next; slow = slow.next; } slow.next = slow.next.next; return dummy.next; } function print(head) { const arr = []; for (let p = head; p; p = p.next) arr.push(p.v); console.log(arr); } const head = new Node(1, new Node(2, new Node(3, new Node(4)))); print(removeNthFromEnd(head, 1)); // [1,2,3] R - Reflection (Deep Dive) Complexity Time: O(L), where L is list length Space: O(1) extra space (in-place pointer update) Approach comparison Approach Time Space Pros Cons Array conversion O(L) O(L) intuitive high extra space, less list-native Two-pass traversal O(L) O(1) stable and simple two scans One-pass + dummy O(L) O(1) single scan, unified boundaries requires gap invariant discipline Common mistakes Forgetting dummy node, which explodes branches for head deletion Off-by-one errors by moving fast n+1 or n-1 steps Forgetting to free removed node in C/C++ contexts Why this method is production-friendly Template-like and reusable across many linked-list variants Stable boundary behavior, especially for removing the head Efficient in performance-sensitive systems (O(1) extra memory) FAQ Q1: Why use while fast.next != null instead of while fast != null? Because we need slow to stop at the predecessor of the target node. Stopping when fast is exactly at the tail gives the correct predecessor position.\nQ2: What if n equals the list length? It still works. With dummy, slow stays at dummy and we remove the original head safely.\nQ3: Can this be written recursively? Yes, but recursion adds O(L) call-stack space. Iteration is generally more stable for long lists.\nBest Practices Always create dummy first, then implement deletion logic Standardize on \u0026ldquo;move fast by n first\u0026rdquo; to reduce off-by-one bugs Teach with two-pass baseline first, then optimize to one-pass In C/C++, explicitly release the removed node S - Summary Key takeaways \u0026ldquo;nth from end\u0026rdquo; can be transformed into a fixed-gap two-pointer problem. Dummy node is the safest boundary tool for linked-list deletion. One-pass fast/slow gives a strong engineering balance: O(L) time, O(1) extra space. This template transfers directly to many chain-structure rewiring tasks. Many medium problems are robust combinations of small stable templates. Recommended reading LeetCode 19 (official): https://leetcode.com/problems/remove-nth-node-from-end-of-list/ LeetCode CN: https://leetcode.cn/problems/remove-nth-node-from-end-of-list/ Related: LeetCode 21, LeetCode 206, LeetCode 25 CTA Rewrite this from memory now:\nImplement the two-pass version. Refactor it into one-pass fast/slow. Verify with three edge cases: n=1, n=len, and single-node list. You will noticeably improve linked-list reliability in interviews and real code.\nRunnable Multi-language Implementations Python from typing import Optional, List class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def remove_nth_from_end(head: Optional[ListNode], n: int) -\u0026gt; Optional[ListNode]: dummy = ListNode(0, head) fast = slow = dummy for _ in range(n): fast = fast.next while fast.next is not None: fast = fast.next slow = slow.next slow.next = slow.next.next return dummy.next def from_list(nums: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() cur = dummy for x in nums: cur.next = ListNode(x) cur = cur.next return dummy.next def to_list(head: Optional[ListNode]) -\u0026gt; List[int]: out = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: h = from_list([1, 2, 3, 4, 5]) print(to_list(remove_nth_from_end(h, 2))) # [1, 2, 3, 5] C #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct ListNode { int val; struct ListNode* next; }; struct ListNode* new_node(int v) { struct ListNode* p = (struct ListNode*)malloc(sizeof(struct ListNode)); p-\u0026gt;val = v; p-\u0026gt;next = NULL; return p; } struct ListNode* removeNthFromEnd(struct ListNode* head, int n) { struct ListNode dummy = {0, head}; struct ListNode *fast = \u0026amp;dummy, *slow = \u0026amp;dummy; for (int i = 0; i \u0026lt; n; ++i) fast = fast-\u0026gt;next; while (fast-\u0026gt;next) { fast = fast-\u0026gt;next; slow = slow-\u0026gt;next; } struct ListNode* del = slow-\u0026gt;next; slow-\u0026gt;next = del-\u0026gt;next; free(del); return dummy.next; } void print_list(struct ListNode* head) { for (struct ListNode* p = head; p; p = p-\u0026gt;next) printf(\u0026#34;%d \u0026#34;, p-\u0026gt;val); printf(\u0026#34;\\n\u0026#34;); } void free_list(struct ListNode* head) { while (head) { struct ListNode* t = head; head = head-\u0026gt;next; free(t); } } int main() { struct ListNode* h1 = new_node(1); h1-\u0026gt;next = new_node(2); h1-\u0026gt;next-\u0026gt;next = new_node(3); h1-\u0026gt;next-\u0026gt;next-\u0026gt;next = new_node(4); h1-\u0026gt;next-\u0026gt;next-\u0026gt;next-\u0026gt;next = new_node(5); h1 = removeNthFromEnd(h1, 2); print_list(h1); // 1 2 3 5 free_list(h1); return 0; } C++ #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* removeNthFromEnd(ListNode* head, int n) { ListNode dummy(0); dummy.next = head; ListNode* fast = \u0026amp;dummy; ListNode* slow = \u0026amp;dummy; for (int i = 0; i \u0026lt; n; ++i) fast = fast-\u0026gt;next; while (fast-\u0026gt;next != nullptr) { fast = fast-\u0026gt;next; slow = slow-\u0026gt;next; } ListNode* del = slow-\u0026gt;next; slow-\u0026gt;next = del-\u0026gt;next; delete del; return dummy.next; } ListNode* build(const vector\u0026lt;int\u0026gt;\u0026amp; a) { ListNode dummy(0); ListNode* tail = \u0026amp;dummy; for (int x : a) { tail-\u0026gt;next = new ListNode(x); tail = tail-\u0026gt;next; } return dummy.next; } void print(ListNode* head) { for (ListNode* p = head; p; p = p-\u0026gt;next) cout \u0026lt;\u0026lt; p-\u0026gt;val \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } void destroy(ListNode* head) { while (head) { ListNode* t = head; head = head-\u0026gt;next; delete t; } } int main() { ListNode* h = build({1, 2, 3, 4, 5}); h = removeNthFromEnd(h, 2); print(h); // 1 2 3 5 destroy(h); return 0; } Go package main import \u0026#34;fmt\u0026#34; type ListNode struct { Val int Next *ListNode } func removeNthFromEnd(head *ListNode, n int) *ListNode { dummy := \u0026amp;ListNode{Next: head} fast, slow := dummy, dummy for i := 0; i \u0026lt; n; i++ { fast = fast.Next } for fast.Next != nil { fast = fast.Next slow = slow.Next } slow.Next = slow.Next.Next return dummy.Next } func build(nums []int) *ListNode { dummy := \u0026amp;ListNode{} tail := dummy for _, x := range nums { tail.Next = \u0026amp;ListNode{Val: x} tail = tail.Next } return dummy.Next } func printList(head *ListNode) { for p := head; p != nil; p = p.Next { fmt.Printf(\u0026#34;%d \u0026#34;, p.Val) } fmt.Println() } func main() { head := build([]int{1, 2, 3, 4, 5}) head = removeNthFromEnd(head, 2) printList(head) // 1 2 3 5 } Rust (safe runnable two-pass variant) To keep ownership handling concise and safe in Rust, this implementation uses a two-pass traversal while preserving O(L) time and O(1) extra space.\n#[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] fn new(val: i32) -\u0026gt; Self { ListNode { next: None, val } } } fn remove_nth_from_end(head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, n: i32) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut len = 0usize; let mut p = head.as_ref(); while let Some(node) = p { len += 1; p = node.next.as_ref(); } let idx = len - n as usize; let mut dummy = Box::new(ListNode { val: 0, next: head }); let mut cur = \u0026amp;mut dummy; for _ in 0..idx { cur = cur.next.as_mut().unwrap(); } let next = cur.next.as_mut().and_then(|node| node.next.take()); cur.next = next; dummy.next } fn from_vec(a: Vec\u0026lt;i32\u0026gt;) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut head = None; for \u0026amp;x in a.iter().rev() { let mut node = Box::new(ListNode::new(x)); node.next = head; head = Some(node); } head } fn to_vec(mut head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let mut out = Vec::new(); while let Some(mut node) = head { out.push(node.val); head = node.next.take(); } out } fn main() { let head = from_vec(vec![1, 2, 3, 4, 5]); let ans = remove_nth_from_end(head, 2); println!(\u0026#34;{:?}\u0026#34;, to_vec(ans)); // [1, 2, 3, 5] } JavaScript class ListNode { constructor(val = 0, next = null) { this.val = val; this.next = next; } } function removeNthFromEnd(head, n) { const dummy = new ListNode(0, head); let fast = dummy; let slow = dummy; for (let i = 0; i \u0026lt; n; i++) { fast = fast.next; } while (fast.next !== null) { fast = fast.next; slow = slow.next; } slow.next = slow.next.next; return dummy.next; } function fromArray(arr) { const dummy = new ListNode(); let tail = dummy; for (const x of arr) { tail.next = new ListNode(x); tail = tail.next; } return dummy.next; } function toArray(head) { const out = []; for (let p = head; p; p = p.next) out.push(p.val); return out; } const head = fromArray([1, 2, 3, 4, 5]); console.log(toArray(removeNthFromEnd(head, 2))); // [1,2,3,5] ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/19-remove-nth-node-from-end-of-list/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThe hard part is not deletion itself, but locating the predecessor of the nth node from the end in a singly linked list. This article derives the one-pass two-pointer solution from simpler baselines and explains correctness, boundaries, and engineering transfer.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003etwo pointers\u003c/code\u003e, \u003ccode\u003einterview high frequency\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 19, Remove Nth Node From End of List, linked list, fast/slow pointers, dummy node\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A complete ACERS walkthrough for removing the nth node from the end: from brute force to one-pass two pointers, with complexity, pitfalls, engineering scenarios, and Python/C/C++/Go/Rust/JS implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eBeginners building a stable template for linked-list interview problems\u003c/li\u003e\n\u003cli\u003eDevelopers who know fast/slow pointers but still make boundary mistakes\u003c/li\u003e\n\u003cli\u003eBackend/system engineers who want to transfer problem-solving templates to chain-structured data in production\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003e\u0026ldquo;Remove the nth node from the end\u0026rdquo; is a classic medium-level linked-list problem. The challenge is usually not the delete operation itself, but:\u003c/p\u003e","title":"LeetCode 19: Remove Nth Node From End of List (One-pass Two Pointers) ACERS Guide"},{"content":" Subtitle / Abstract\nThe real challenge in this problem is not traversing the list, but correctly cloning the cross-node reference relationships created by random pointers. This article moves from naive intuition to a hash-mapping solution, and explains why it is stable, maintainable, and practical in real engineering.\nEstimated reading time: 12–16 minutes Tags: Linked List, Deep Copy, Hash Table, Random Pointer SEO keywords: LeetCode 138, Copy List with Random Pointer, random list copy, deep copy, hash mapping Meta description: Perform deep copy of a random-pointer linked list via two passes plus a mapping table, with correctness, complexity, engineering practice, and six-language implementations. A — Algorithm (Problem and Algorithm) Problem Restatement Given a linked list of length n, each node has:\nval next random (can point to any node or null) Construct a deep copy of this list and return the new head node.\nNo pointer in the new list may point to any node in the original list.\nInput / Output Representation The problem statement often uses [val, random_index] to represent each node:\nval: node value random_index: index of the node pointed to by random; null if empty Your function input is only head, and your output is the copied list head.\nExample 1 Input: [[7,null],[13,0],[11,4],[10,2],[1,0]] Output: [[7,null],[13,0],[11,4],[10,2],[1,0]] Explanation: The output has the same value/reference structure as the input, but all nodes are newly created objects. Example 2 Input: [[1,1],[2,1]] Output: [[1,1],[2,1]] Explanation: The first node\u0026#39;s random points to the second node, and the second node\u0026#39;s random points to itself. Target Readers Developers who feel shaky on random pointer problems while practicing LeetCode Learners who want to clearly understand \u0026ldquo;shallow copy vs deep copy\u0026rdquo; Engineers who want to transfer algorithmic thinking to real object-copy scenarios Background / Motivation For a normal linked list, copying val and next is straightforward.\nA random-pointer list adds one more pointer, random, which can:\nPoint to any node (earlier node, later node, or itself) Or be null That turns the problem from \u0026ldquo;linear copy\u0026rdquo; into \u0026ldquo;structure copy with extra references.\u0026rdquo;\nCommon engineering equivalents include:\nCopying workflow node objects while preserving cross-step jump relationships Copying cached object graphs while keeping internal references consistent Copying session chains while preserving backtracking / shortcut references Core Concepts Shallow Copy: copies only the node shell; internal references still point to old objects Deep Copy: rebuilds a full object graph; all references point to new objects Node Identity Mapping: old_node -\u0026gt; new_node, the key to rebuilding random Structural Equivalence: the new list is isomorphic to the old one in values and pointer relations, while sharing no nodes C — Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the smallest example that shows why random is harder than next Suppose:\n7 -\u0026gt; 13 -\u0026gt; 11 ^ | |_____| Here:\nnext is local and linear random can point backward, forward, or to null So copying this list is not just \u0026ldquo;clone current node and move on.\u0026rdquo; We must preserve a graph of node identities.\nStep 2: Ask what a correct copy must preserve A valid deep copy must keep:\nthe same node values the same next topology the same random topology but with entirely new node objects That last point is the important one. We are not copying values only; we are rebuilding relationships between identities.\nStep 3: Why immediate random assignment becomes awkward If, during the first traversal, you try to set:\nnew.random = ??? you may not have created the target copy yet. That causes messy branches and delayed backfilling.\nSo the real missing state is:\nold_node -\u0026gt; new_node Once that mapping exists, pointer reconstruction becomes a lookup problem.\nStep 4: Build the identity mapping first In the first pass, create one clone node for every original node:\nmp[cur] = Node(cur.val) At this point, we are not wiring anything yet. We are only guaranteeing:\nevery original node already has a corresponding clone.\nThat is the prerequisite for safe next and random reconstruction.\nStep 5: Rebuild both pointer systems in the second pass Now every target clone exists, so wiring becomes simple:\nmp[cur].next = mp.get(cur.next) mp[cur].random = mp.get(cur.random) get handles the None case naturally. The important part is that both pointer fields are rebuilt from the same identity map.\nStep 6: Walk one node slowly Suppose original node 13 has:\nnext -\u0026gt; 11 random -\u0026gt; 7 After pass 1, both mp[11] and mp[7] already exist. So in pass 2 we can wire:\nmp[13].next = mp[11] mp[13].random = mp[7] No guessing, no forward-reference problem, no repair pass beyond the planned second traversal.\nStep 7: Reduce the method to one sentence LeetCode 138 is \u0026ldquo;clone all nodes first to build old -\u0026gt; new, then rebuild next and random by looking up that map.\u0026rdquo;\nAssemble the Full Code from typing import Optional, List class Node: def __init__(self, x: int, next: Optional[\u0026#34;Node\u0026#34;] = None, random: Optional[\u0026#34;Node\u0026#34;] = None): self.val = x self.next = next self.random = random def copy_random_list(head: Optional[Node]) -\u0026gt; Optional[Node]: if head is None: return None mp = {} cur = head while cur is not None: mp[cur] = Node(cur.val) cur = cur.next cur = head while cur is not None: mp[cur].next = mp.get(cur.next) mp[cur].random = mp.get(cur.random) cur = cur.next return mp[head] def build(arr: List[List[Optional[int]]]) -\u0026gt; Optional[Node]: if not arr: return None nodes = [Node(v) for v, _ in arr] for i in range(len(nodes) - 1): nodes[i].next = nodes[i + 1] for i, (_, r) in enumerate(arr): nodes[i].random = nodes[r] if r is not None else None return nodes[0] def dump(head: Optional[Node]) -\u0026gt; List[List[Optional[int]]]: out = [] idx = {} cur, i = head, 0 while cur is not None: idx[cur] = i cur = cur.next i += 1 cur = head while cur is not None: out.append([cur.val, idx.get(cur.random)]) cur = cur.next return out if __name__ == \u0026#34;__main__\u0026#34;: data = [[7, None], [13, 0], [11, 4], [10, 2], [1, 0]] src = build(data) cp = copy_random_list(src) print(dump(cp)) Reference Answer from typing import Optional class Node: def __init__(self, x: int, next: Optional[\u0026#34;Node\u0026#34;] = None, random: Optional[\u0026#34;Node\u0026#34;] = None): self.val = x self.next = next self.random = random class Solution: def copyRandomList(self, head: Optional[Node]) -\u0026gt; Optional[Node]: if head is None: return None mp = {} cur = head while cur is not None: mp[cur] = Node(cur.val) cur = cur.next cur = head while cur is not None: mp[cur].next = mp.get(cur.next) mp[cur].random = mp.get(cur.random) cur = cur.next return mp[head] Algorithm Classification Linked-list traversal Hash mapping (object identity mapping) Graph copy (special graph: each node has at most two outgoing edges) Conceptual Model Treat the list as a directed graph:\nNode set: V Edge set: E = {next edges, random edges} The copy target is an isomorphic graph G', satisfying:\nval(v') = val(v) f(next(v)) = next(f(v)) f(random(v)) = random(f(v)) where f is the mapping old -\u0026gt; new.\nCorrectness Highlights (Brief) After pass one, each old node u has a unique copied node f(u) In pass two, for each edge u -\u0026gt; v, set f(u).ptr = f(v) (v may be null) Because each next/random edge is rewired via f, the copied structure is fully equivalent and contains no leaked references to old nodes Practical Guide / Steps Handle empty list first: if head == null, return null First pass: create a copied node for every old node and store it in mapping Second pass: set next and random for each copied node Return map[head] Runnable Python example:\nfrom typing import Optional, List class Node: def __init__(self, x: int, next: Optional[\u0026#34;Node\u0026#34;] = None, random: Optional[\u0026#34;Node\u0026#34;] = None): self.val = x self.next = next self.random = random def copy_random_list(head: Optional[Node]) -\u0026gt; Optional[Node]: if head is None: return None mp = {} cur = head while cur is not None: mp[cur] = Node(cur.val) cur = cur.next cur = head while cur is not None: mp[cur].next = mp.get(cur.next) mp[cur].random = mp.get(cur.random) cur = cur.next return mp[head] def build(arr: List[List[Optional[int]]]) -\u0026gt; Optional[Node]: if not arr: return None nodes = [Node(v) for v, _ in arr] for i in range(len(nodes) - 1): nodes[i].next = nodes[i + 1] for i, (_, r) in enumerate(arr): nodes[i].random = nodes[r] if r is not None else None return nodes[0] def dump(head: Optional[Node]) -\u0026gt; List[List[Optional[int]]]: out = [] idx = {} cur, i = head, 0 while cur is not None: idx[cur] = i cur = cur.next i += 1 cur = head while cur is not None: out.append([cur.val, idx.get(cur.random)]) cur = cur.next return out if __name__ == \u0026#34;__main__\u0026#34;: data = [[7, None], [13, 0], [11, 4], [10, 2], [1, 0]] src = build(data) cp = copy_random_list(src) print(dump(cp)) Code / Test Cases / Test Results Code Highlights Two passes: create nodes in pass one, connect edges in pass two map.get(None) == None (Python) reduces explicit null-check branches Test Cases Case 1: [] Expected: [] Case 2: [[1,null]] Expected: [[1,null]] Case 3: [[1,0]] Expected: [[1,0]] (self-pointing random) Case 4: [[7,null],[13,0],[11,4],[10,2],[1,0]] Expected: same structure after copy Test Results (Sample) All tests passed: structure is equivalent, and node addresses in the copied list are completely different from the original list. E — Engineering (Engineering Applications) Scenario 1: Deep copy of workflow definitions (Python) Background: workflow nodes have sequential next, and may also include jump references (similar to random).\nWhy it fits: when cloning a template into a new workflow, jump relationships must be preserved without contaminating the original template.\nclass Step: def __init__(self, name): self.name = name self.next = None self.jump = None def copy_steps(head): if not head: return None mp = {} cur = head while cur: mp[cur] = Step(cur.name) cur = cur.next cur = head while cur: mp[cur].next = mp.get(cur.next) mp[cur].jump = mp.get(cur.jump) cur = cur.next return mp[head] Scenario 2: Backend task-chain copy (Go) Background: task nodes execute linearly, but can jump back to a compensation node on failure.\nWhy it fits: failure-jump relationships are fundamentally random references and must be reconstructed during copying.\npackage main import \u0026#34;fmt\u0026#34; type Task struct { Name string Next *Task Backup *Task } func copyTasks(head *Task) *Task { if head == nil { return nil } mp := map[*Task]*Task{} for cur := head; cur != nil; cur = cur.Next { mp[cur] = \u0026amp;Task{Name: cur.Name} } for cur := head; cur != nil; cur = cur.Next { mp[cur].Next = mp[cur.Next] mp[cur].Backup = mp[cur.Backup] } return mp[head] } func main() { a := \u0026amp;Task{Name: \u0026#34;A\u0026#34;} b := \u0026amp;Task{Name: \u0026#34;B\u0026#34;} a.Next = b b.Backup = b cp := copyTasks(a) fmt.Println(cp.Name, cp.Next.Name, cp.Next.Backup == cp.Next) // A B true } Scenario 3: Frontend editor-history chain copy (JavaScript) Background: editor history usually has a linear chain plus references for quick-jump key versions.\nWhy it fits: when switching user sessions, copying history chains avoids cross-session object-reference contamination.\nclass Version { constructor(id) { this.id = id; this.next = null; this.jump = null; } } function copyVersions(head) { if (!head) return null; const mp = new Map(); for (let cur = head; cur; cur = cur.next) mp.set(cur, new Version(cur.id)); for (let cur = head; cur; cur = cur.next) { mp.get(cur).next = mp.get(cur.next) || null; mp.get(cur).jump = mp.get(cur.jump) || null; } return mp.get(head); } R — Reflection (Reflection and Depth) Complexity Analysis Time complexity: O(n) (two linear passes) Space complexity: O(n) (mapping table) Alternative Approach Comparison Approach Time Extra Space Evaluation Two-pass hash mapping (this article) O(n) O(n) Easiest to write, most stable, high maintainability Interleaving-list method (insert copies in-place, then split) O(n) O(1) Better space usage, but more implementation details Serialize + deserialize Usually \u0026gt; O(n) Depends on format Possible in engineering, but not ideal for core interview evaluation Common Incorrect Approaches Copying only val/next and forgetting random Accidentally pointing copied random back to original nodes Using old-node pointers directly in second-pass rewiring instead of mapped new nodes Forgetting to handle head == null Why this method is more practical in engineering Clear logical layering (node creation and edge rewiring are separated) Easy to debug (check mapping scale first, then pointer connections) Team-friendly and easier for newcomers to maintain quickly Frequently Asked Questions and Notes (FAQ) Q1: Why can this be viewed as a graph-copy problem? Because each node has two edge types, next and random; what we copy is the full node-edge relationship, not just linear list order.\nQ2: Can it be done in one pass? It is possible in theory, but code complexity and bug risk rise significantly. In interviews and engineering, the two-pass hash-mapping version is recommended.\nQ3: Is a hash table required? Not strictly required. If you pursue O(1) extra space, you can use the interleaving-list method, but readability is usually worse than hash mapping.\nBest Practices and Recommendations Separate \u0026ldquo;copy nodes\u0026rdquo; and \u0026ldquo;rebuild pointers\u0026rdquo; into two phases to avoid state confusion Use node object identity as mapping key, not node values Cover these regression cases: empty list, self-pointing random, cross-pointing random, tail node with random = null For debugging output, [val, random_index] is usually more intuitive than raw addresses S — Summary (Summary) Key takeaways:\nThis problem is essentially \u0026ldquo;object identity mapping + pointer rewiring,\u0026rdquo; not ordinary linear list copy. The two-pass approach splits the problem into node creation and edge rewiring, improving both correctness and maintainability. Correct random reconstruction depends on a complete old -\u0026gt; new mapping. Hash mapping is an extremely stable engineering baseline and the clearest way to explain the solution in interviews. Once understood, this pattern transfers naturally to graph copy, workflow clone, and object-graph duplication scenarios. Recommended follow-up reading:\nLeetCode 133 Clone Graph LeetCode 146 LRU Cache (hash mapping + linked-list coordination) LeetCode 21 / 206 (fundamental linked-list drills) Designing Data-Intensive Applications sections on object relationships and data copying Multi-language Runnable Implementations Python from typing import Optional class Node: def __init__(self, x: int, next: Optional[\u0026#34;Node\u0026#34;] = None, random: Optional[\u0026#34;Node\u0026#34;] = None): self.val = x self.next = next self.random = random class Solution: def copyRandomList(self, head: Optional[Node]) -\u0026gt; Optional[Node]: if head is None: return None mp = {} cur = head while cur is not None: mp[cur] = Node(cur.val) cur = cur.next cur = head while cur is not None: mp[cur].next = mp.get(cur.next) mp[cur].random = mp.get(cur.random) cur = cur.next return mp[head] C #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct Node { int val; struct Node* next; struct Node* random; }; struct Node* new_node(int v) { struct Node* n = (struct Node*)malloc(sizeof(struct Node)); n-\u0026gt;val = v; n-\u0026gt;next = NULL; n-\u0026gt;random = NULL; return n; } // Interleaving-list method: O(n) time, O(1) extra space struct Node* copyRandomList(struct Node* head) { if (head == NULL) return NULL; struct Node* cur = head; while (cur != NULL) { struct Node* cp = new_node(cur-\u0026gt;val); cp-\u0026gt;next = cur-\u0026gt;next; cur-\u0026gt;next = cp; cur = cp-\u0026gt;next; } cur = head; while (cur != NULL) { struct Node* cp = cur-\u0026gt;next; cp-\u0026gt;random = (cur-\u0026gt;random != NULL) ? cur-\u0026gt;random-\u0026gt;next : NULL; cur = cp-\u0026gt;next; } struct Node* new_head = head-\u0026gt;next; cur = head; while (cur != NULL) { struct Node* cp = cur-\u0026gt;next; cur-\u0026gt;next = cp-\u0026gt;next; cp-\u0026gt;next = (cp-\u0026gt;next != NULL) ? cp-\u0026gt;next-\u0026gt;next : NULL; cur = cur-\u0026gt;next; } return new_head; } void print_list(struct Node* head) { struct Node* arr[128]; int n = 0; for (struct Node* p = head; p != NULL; p = p-\u0026gt;next) arr[n++] = p; for (int i = 0; i \u0026lt; n; i++) { int r = -1; for (int j = 0; j \u0026lt; n; j++) { if (arr[i]-\u0026gt;random == arr[j]) { r = j; break; } } if (r \u0026gt;= 0) printf(\u0026#34;[%d,%d] \u0026#34;, arr[i]-\u0026gt;val, r); else printf(\u0026#34;[%d,null] \u0026#34;, arr[i]-\u0026gt;val); } printf(\u0026#34;\\n\u0026#34;); } int main(void) { struct Node* a = new_node(1); struct Node* b = new_node(2); a-\u0026gt;next = b; a-\u0026gt;random = b; b-\u0026gt;random = b; struct Node* cp = copyRandomList(a); print_list(cp); // [1,1] [2,1] return 0; } C++ #include \u0026lt;iostream\u0026gt; #include \u0026lt;unordered_map\u0026gt; using namespace std; class Node { public: int val; Node* next; Node* random; Node(int _val) : val(_val), next(nullptr), random(nullptr) {} }; class Solution { public: Node* copyRandomList(Node* head) { if (!head) return nullptr; unordered_map\u0026lt;Node*, Node*\u0026gt; mp; for (Node* cur = head; cur; cur = cur-\u0026gt;next) { mp[cur] = new Node(cur-\u0026gt;val); } for (Node* cur = head; cur; cur = cur-\u0026gt;next) { mp[cur]-\u0026gt;next = cur-\u0026gt;next ? mp[cur-\u0026gt;next] : nullptr; mp[cur]-\u0026gt;random = cur-\u0026gt;random ? mp[cur-\u0026gt;random] : nullptr; } return mp[head]; } }; Go package main type Node struct { Val int Next *Node Random *Node } func copyRandomList(head *Node) *Node { if head == nil { return nil } mp := map[*Node]*Node{} for cur := head; cur != nil; cur = cur.Next { mp[cur] = \u0026amp;Node{Val: cur.Val} } for cur := head; cur != nil; cur = cur.Next { mp[cur].Next = mp[cur.Next] mp[cur].Random = mp[cur.Random] } return mp[head] } Rust use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; #[derive(Debug)] struct Node { val: i32, next: Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;\u0026gt;, random: Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;\u0026gt;, } impl Node { fn new(val: i32) -\u0026gt; Self { Self { val, next: None, random: None } } } fn copy_random_list(head: Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;\u0026gt;) -\u0026gt; Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;\u0026gt; { let start = head.clone()?; let mut mp: HashMap\u0026lt;*const RefCell\u0026lt;Node\u0026gt;, Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;\u0026gt; = HashMap::new(); let mut cur = head.clone(); while let Some(node_rc) = cur { let ptr = Rc::as_ptr(\u0026amp;node_rc); let val = node_rc.borrow().val; mp.insert(ptr, Rc::new(RefCell::new(Node::new(val)))); cur = node_rc.borrow().next.clone(); } cur = head; while let Some(node_rc) = cur { let old_ptr = Rc::as_ptr(\u0026amp;node_rc); let new_node = mp.get(\u0026amp;old_ptr).unwrap().clone(); let next_old = node_rc.borrow().next.clone(); let random_old = node_rc.borrow().random.clone(); { let mut nm = new_node.borrow_mut(); nm.next = next_old .as_ref() .and_then(|x| mp.get(\u0026amp;Rc::as_ptr(x)).cloned()); nm.random = random_old .as_ref() .and_then(|x| mp.get(\u0026amp;Rc::as_ptr(x)).cloned()); } cur = next_old; } mp.get(\u0026amp;Rc::as_ptr(\u0026amp;start)).cloned() } fn main() { let n1 = Rc::new(RefCell::new(Node::new(1))); let n2 = Rc::new(RefCell::new(Node::new(2))); n1.borrow_mut().next = Some(n2.clone()); n1.borrow_mut().random = Some(n2.clone()); n2.borrow_mut().random = Some(n2.clone()); let cp = copy_random_list(Some(n1)).unwrap(); println!(\u0026#34;{}\u0026#34;, cp.borrow().val); // 1 } JavaScript function Node(val, next = null, random = null) { this.val = val; this.next = next; this.random = random; } function copyRandomList(head) { if (head === null) return null; const mp = new Map(); for (let cur = head; cur !== null; cur = cur.next) { mp.set(cur, new Node(cur.val)); } for (let cur = head; cur !== null; cur = cur.next) { mp.get(cur).next = cur.next ? mp.get(cur.next) : null; mp.get(cur).random = cur.random ? mp.get(cur.random) : null; } return mp.get(head); } Call to Action (CTA) I recommend doing these two reinforcement steps right now:\nWrite the two-pass hash-mapping solution once from memory and pass your own tests. Then tackle LeetCode 133 Clone Graph to transfer identity-mapping copy logic to a more general graph structure. If you want, I can write the next article on LeetCode 146 LRU Cache, extending from \u0026ldquo;hash + linked list\u0026rdquo; in copy problems to cache-eviction design.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/linked-list/138-copy-list-with-random-pointer/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\u003cbr\u003e\nThe real challenge in this problem is not traversing the list, but correctly cloning the cross-node reference relationships created by \u003ccode\u003erandom\u003c/code\u003e pointers. This article moves from naive intuition to a hash-mapping solution, and explains why it is stable, maintainable, and practical in real engineering.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEstimated reading time\u003c/strong\u003e: 12–16 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eLinked List\u003c/code\u003e, \u003ccode\u003eDeep Copy\u003c/code\u003e, \u003ccode\u003eHash Table\u003c/code\u003e, \u003ccode\u003eRandom Pointer\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 138, Copy List with Random Pointer, random list copy, deep copy, hash mapping\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Perform deep copy of a random-pointer linked list via two passes plus a mapping table, with correctness, complexity, engineering practice, and six-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm-problem-and-algorithm\"\u003eA — Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven a linked list of length \u003ccode\u003en\u003c/code\u003e, each node has:\u003c/p\u003e","title":"LeetCode 138: Copy List with Random Pointer — A Complete Deep-Copy Breakdown"},{"content":" Subtitle / Summary\nThis problem is just grade-school addition on a linked list: add one digit at a time, propagate carry, and append one final node if carry remains after both lists end. We move from naive ideas to the optimal one-pass solution, then map it to real engineering scenarios.\nReading time: 12-15 min Tags: linked list, carry, simulation, LeetCode 2 SEO keywords: Add Two Numbers, LeetCode 2, reverse-order list, carry, dummy node Meta description: Use dummy + tail + carry to sum two reverse-order linked lists in O(max(m,n)) time, with common pitfalls, engineering analogies, and six-language runnable implementations. A — Algorithm (Problem \u0026amp; Algorithm) Problem Restatement You are given two non-empty linked lists representing two non-negative integers.\nDigits are stored in reverse order, and each node stores one digit.\nReturn their sum as a linked list in the same reverse order.\nExcept for number 0, the input numbers do not have leading zeros.\nInput / Output Item Meaning Input Two linked lists l1, l2, each node value in 0~9 Output A new linked list representing l1 + l2 in reverse order Example 1 Input: l1 = [2,4,3], l2 = [5,6,4] Explanation: 342 + 465 = 807 Output: [7,0,8] Example 2 Input: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] Explanation: 9999999 + 9999 = 10009998 Output: [8,9,9,9,0,0,0,1] Target Readers Beginners building a stable template for linked-list problems Intermediate developers who often miss carry or boundary cases Engineers who want to transfer algorithmic thinking to stream-style data processing Background / Motivation This looks like an entry-level LeetCode problem, but it trains practical skills you will reuse:\nSynchronous progression across multiple input streams (l1, l2) Cross-iteration state propagation (carry) Boundary completeness (different lengths, final carry node) These three appear frequently in production systems: chunked amount accumulation, multi-source counter merge, and streaming aggregation with backfill.\nCore Concepts Reverse-order storage: ones digit at the head, then tens, then hundreds\u0026hellip; Digit-wise addition: each round handles only x + y + carry Carry propagation: carry = sum // 10, current digit sum % 10 Dummy node: avoids special handling when creating the result head Derivation: from Naive to Optimal Naive idea 1: convert to integers, add, then rebuild list Convert lists to integers n1, n2 Compute n1 + n2 Split the result back to digits and build a new list Problems:\nMay overflow in many languages for long inputs Extra conversion in both directions Misses the essence of linked-list digit simulation Naive idea 2: convert to arrays first, then add by index Convert both lists to arrays Add digit by digit Problems:\nNeeds O(m+n) extra space Inputs are already low-digit-first, so the array layer is unnecessary Key Observation The lists already start from the ones digit, exactly what column addition needs Each round depends only on current digits and carry One linear pass is enough Method Choice Use dummy + tail to build the output. Loop while:\nwhile l1 != null or l2 != null or carry != 0 Per iteration:\nRead current digits x, y (treat missing node as 0) sum = x + y + carry Append node sum % 10 Update carry = sum // 10 C — Concepts (Core Ideas) Method Category Linked-list simulation Carry state machine Dual-pointer synchronous traversal State Model Let x_k, y_k be the digits at round k, and c_k be carry-in:\ns_k = x_k + y_k + c_k digit_k = s_k mod 10 c_(k+1) = floor(s_k / 10) where c_k ∈ {0,1}.\nThis is the exact mathematical form of decimal column addition.\nCorrectness Intuition digit_k is exactly the k-th digit of the result carry passes the overflow (\u0026gt;9) to the next round If both lists end but carry=1, append one final node Practice Guide / Steps Initialize dummy, tail, and carry = 0 Loop while either list remains or carry is non-zero Read current values: x = l1.val if l1 else 0, y = l2.val if l2 else 0 Compute sum, append sum % 10 Update carry = sum // 10, move tail and input pointers Return dummy.next Minimal runnable Python example:\nfrom typing import Optional, List class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def add_two_numbers(l1: Optional[ListNode], l2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode(0) tail = dummy carry = 0 while l1 is not None or l2 is not None or carry: x = l1.val if l1 is not None else 0 y = l2.val if l2 is not None else 0 s = x + y + carry carry = s // 10 tail.next = ListNode(s % 10) tail = tail.next if l1 is not None: l1 = l1.next if l2 is not None: l2 = l2.next return dummy.next def build(nums: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy for n in nums: tail.next = ListNode(n) tail = tail.next return dummy.next def dump(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head is not None: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: a = build([2, 4, 3]) b = build([5, 6, 4]) print(dump(add_two_numbers(a, b))) # [7, 0, 8] E — Engineering (Real-world Mapping) Scenario 1: chunked amount merge in finance (Python) Background: some systems store or transmit very large amounts in chunks.\nWhy this maps well: each chunk behaves like one digit group; the core is still same-position add + carry propagation.\ndef add_digits(a, b): i = j = 0 carry = 0 out = [] while i \u0026lt; len(a) or j \u0026lt; len(b) or carry: x = a[i] if i \u0026lt; len(a) else 0 y = b[j] if j \u0026lt; len(b) else 0 s = x + y + carry out.append(s % 10) carry = s // 10 i += 1 j += 1 return out print(add_digits([2, 4, 3], [5, 6, 4])) # [7,0,8] Scenario 2: merge low-digit-first counters from multiple services (Go) Background: two backend services report low-digit-first counter blocks.\nWhy this maps well: digit-wise merge is stream-friendly and memory-stable.\npackage main import \u0026#34;fmt\u0026#34; func addDigits(a, b []int) []int { i, j, carry := 0, 0, 0 out := make([]int, 0) for i \u0026lt; len(a) || j \u0026lt; len(b) || carry \u0026gt; 0 { x, y := 0, 0 if i \u0026lt; len(a) { x = a[i] i++ } if j \u0026lt; len(b) { y = b[j] j++ } s := x + y + carry out = append(out, s%10) carry = s / 10 } return out } func main() { fmt.Println(addDigits([]int{9, 9, 9}, []int{1})) // [0 0 0 1] } Scenario 3: offline draft version increment in frontend (JavaScript) Background: offline editors may split very long version numbers into digits/chunks.\nWhy this maps well: browser-side processing avoids dependency on big-integer libraries.\nfunction addDigits(a, b) { let i = 0; let j = 0; let carry = 0; const out = []; while (i \u0026lt; a.length || j \u0026lt; b.length || carry) { const x = i \u0026lt; a.length ? a[i++] : 0; const y = j \u0026lt; b.length ? b[j++] : 0; const s = x + y + carry; out.push(s % 10); carry = Math.floor(s / 10); } return out; } console.log(addDigits([2, 4, 3], [5, 6, 4])); // [7,0,8] R — Reflection (Deep Dive) Complexity Time: O(max(m, n)) Space: O(max(m, n)) for result list; auxiliary space is O(1) Alternative Comparison Approach Time Extra Space Problem Convert to integers then add O(m+n) depends on big-int overflow risk or big-int dependency Convert to arrays then add O(m+n) O(m+n) unnecessary middle layer One-pass list simulation (this) O(max(m,n)) O(1) auxiliary clear boundaries, production-friendly Common Mistakes Forgetting carry != 0 in the loop condition, so 999 + 1 loses the last digit Dereferencing null when list lengths differ Over-optimizing in-place reuse of input lists and making code branches hard to reason about Why this method is optimal and practical Single pass and direct mapping to decimal addition No dependency on language big-integer support Unified boundary handling, easy to test and port FAQ Q1: Why must the loop condition include carry? Because both lists may end while a carry is still pending. Example: 5 + 5 = 10 still needs one more node 1.\nQ2: Can we modify l1 or l2 in place? Possible, but usually not worth it: branch complexity increases and you may break caller expectations about input reuse. In interviews and production code, building a new result list is cleaner.\nQ3: What if digits are stored in forward order? That is a different problem (LeetCode 445). Typical solutions use stack or recursion from high digit to low digit, unlike this low-digit-first model.\nBest Practices Keep the stable template: dummy + tail + carry Use while l1 or l2 or carry to unify boundaries Always test three cases: same length, different lengths, all-carry chain Keep “missing node means 0” in one place to avoid scattered null checks S — Summary Key takeaways:\nReverse-order list addition is a decimal digit state machine. carry is cross-round state and must be included in loop condition. A dummy node removes fragile head-special-case logic. This is a foundational template for linked-list simulation and boundary management. The same model transfers well to chunked numeric merge and streaming counters. Recommended follow-ups:\nLeetCode 445 Add Two Numbers II (forward-order digits) LeetCode 21 Merge Two Sorted Lists (dual-pointer list template) LeetCode 206 Reverse Linked List (core linked-list operations) CLRS chapters on linked lists and basic data structures Runnable Multi-language Implementations Python from typing import Optional, List class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next class Solution: def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode(0) tail = dummy carry = 0 while l1 is not None or l2 is not None or carry: x = l1.val if l1 else 0 y = l2.val if l2 else 0 s = x + y + carry carry = s // 10 tail.next = ListNode(s % 10) tail = tail.next if l1: l1 = l1.next if l2: l2 = l2.next return dummy.next def build(nums: List[int]) -\u0026gt; Optional[ListNode]: d = ListNode() t = d for v in nums: t.next = ListNode(v) t = t.next return d.next def dump(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: ans = Solution().addTwoNumbers(build([2, 4, 3]), build([5, 6, 4])) print(dump(ans)) # [7, 0, 8] C #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct ListNode { int val; struct ListNode* next; }; struct ListNode* new_node(int v) { struct ListNode* n = (struct ListNode*)malloc(sizeof(struct ListNode)); n-\u0026gt;val = v; n-\u0026gt;next = NULL; return n; } struct ListNode* addTwoNumbers(struct ListNode* l1, struct ListNode* l2) { struct ListNode dummy; dummy.val = 0; dummy.next = NULL; struct ListNode* tail = \u0026amp;dummy; int carry = 0; while (l1 != NULL || l2 != NULL || carry != 0) { int x = (l1 != NULL) ? l1-\u0026gt;val : 0; int y = (l2 != NULL) ? l2-\u0026gt;val : 0; int s = x + y + carry; carry = s / 10; tail-\u0026gt;next = new_node(s % 10); tail = tail-\u0026gt;next; if (l1 != NULL) l1 = l1-\u0026gt;next; if (l2 != NULL) l2 = l2-\u0026gt;next; } return dummy.next; } struct ListNode* build(const int* a, int n) { struct ListNode dummy; dummy.next = NULL; struct ListNode* tail = \u0026amp;dummy; for (int i = 0; i \u0026lt; n; i++) { tail-\u0026gt;next = new_node(a[i]); tail = tail-\u0026gt;next; } return dummy.next; } void print_list(struct ListNode* h) { while (h != NULL) { printf(\u0026#34;%d\u0026#34;, h-\u0026gt;val); if (h-\u0026gt;next != NULL) printf(\u0026#34; -\u0026gt; \u0026#34;); h = h-\u0026gt;next; } printf(\u0026#34;\\n\u0026#34;); } void free_list(struct ListNode* h) { while (h != NULL) { struct ListNode* nxt = h-\u0026gt;next; free(h); h = nxt; } } int main(void) { int a[] = {2, 4, 3}; int b[] = {5, 6, 4}; struct ListNode* l1 = build(a, 3); struct ListNode* l2 = build(b, 3); struct ListNode* ans = addTwoNumbers(l1, l2); print_list(ans); // 7 -\u0026gt; 0 -\u0026gt; 8 free_list(l1); free_list(l2); free_list(ans); return 0; } C++ #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; using namespace std; struct ListNode { int val; ListNode* next; ListNode(int x = 0) : val(x), next(nullptr) {} }; class Solution { public: ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { ListNode dummy(0); ListNode* tail = \u0026amp;dummy; int carry = 0; while (l1 || l2 || carry) { int x = l1 ? l1-\u0026gt;val : 0; int y = l2 ? l2-\u0026gt;val : 0; int s = x + y + carry; carry = s / 10; tail-\u0026gt;next = new ListNode(s % 10); tail = tail-\u0026gt;next; if (l1) l1 = l1-\u0026gt;next; if (l2) l2 = l2-\u0026gt;next; } return dummy.next; } }; ListNode* build(const vector\u0026lt;int\u0026gt;\u0026amp; a) { ListNode dummy; ListNode* tail = \u0026amp;dummy; for (int v : a) { tail-\u0026gt;next = new ListNode(v); tail = tail-\u0026gt;next; } return dummy.next; } void printList(ListNode* h) { while (h) { cout \u0026lt;\u0026lt; h-\u0026gt;val; if (h-\u0026gt;next) cout \u0026lt;\u0026lt; \u0026#34; -\u0026gt; \u0026#34;; h = h-\u0026gt;next; } cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } void freeList(ListNode* h) { while (h) { ListNode* nxt = h-\u0026gt;next; delete h; h = nxt; } } int main() { ListNode* l1 = build({2, 4, 3}); ListNode* l2 = build({5, 6, 4}); ListNode* ans = Solution().addTwoNumbers(l1, l2); printList(ans); // 7 -\u0026gt; 0 -\u0026gt; 8 freeList(l1); freeList(l2); freeList(ans); return 0; } Go package main import \u0026#34;fmt\u0026#34; type ListNode struct { Val int Next *ListNode } func addTwoNumbers(l1 *ListNode, l2 *ListNode) *ListNode { dummy := \u0026amp;ListNode{} tail := dummy carry := 0 for l1 != nil || l2 != nil || carry != 0 { x, y := 0, 0 if l1 != nil { x = l1.Val l1 = l1.Next } if l2 != nil { y = l2.Val l2 = l2.Next } s := x + y + carry carry = s / 10 tail.Next = \u0026amp;ListNode{Val: s % 10} tail = tail.Next } return dummy.Next } func build(a []int) *ListNode { dummy := \u0026amp;ListNode{} tail := dummy for _, v := range a { tail.Next = \u0026amp;ListNode{Val: v} tail = tail.Next } return dummy.Next } func printList(h *ListNode) { for h != nil { fmt.Print(h.Val) if h.Next != nil { fmt.Print(\u0026#34; -\u0026gt; \u0026#34;) } h = h.Next } fmt.Println() } func main() { l1 := build([]int{2, 4, 3}) l2 := build([]int{5, 6, 4}) ans := addTwoNumbers(l1, l2) printList(ans) // 7 -\u0026gt; 0 -\u0026gt; 8 } Rust #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] fn new(val: i32) -\u0026gt; Self { ListNode { next: None, val } } } pub fn add_two_numbers( mut l1: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, mut l2: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, ) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut digits: Vec\u0026lt;i32\u0026gt; = Vec::new(); let mut carry = 0; while l1.is_some() || l2.is_some() || carry \u0026gt; 0 { let mut x = 0; let mut y = 0; if let Some(mut node) = l1 { x = node.val; l1 = node.next.take(); } else { l1 = None; } if let Some(mut node) = l2 { y = node.val; l2 = node.next.take(); } else { l2 = None; } let s = x + y + carry; carry = s / 10; digits.push(s % 10); } let mut head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; = None; let mut tail = \u0026amp;mut head; for d in digits { *tail = Some(Box::new(ListNode::new(d))); if let Some(node) = tail { tail = \u0026amp;mut node.next; } } head } fn build(nums: \u0026amp;[i32]) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; = None; let mut tail = \u0026amp;mut head; for \u0026amp;n in nums { *tail = Some(Box::new(ListNode::new(n))); if let Some(node) = tail { tail = \u0026amp;mut node.next; } } head } fn dump(mut head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;) -\u0026gt; Vec\u0026lt;i32\u0026gt; { let mut out = Vec::new(); while let Some(mut node) = head { out.push(node.val); head = node.next.take(); } out } fn main() { let l1 = build(\u0026amp;[2, 4, 3]); let l2 = build(\u0026amp;[5, 6, 4]); let ans = add_two_numbers(l1, l2); println!(\u0026#34;{:?}\u0026#34;, dump(ans)); // [7, 0, 8] } JavaScript function ListNode(val = 0, next = null) { this.val = val; this.next = next; } function addTwoNumbers(l1, l2) { const dummy = new ListNode(0); let tail = dummy; let carry = 0; while (l1 !== null || l2 !== null || carry !== 0) { const x = l1 ? l1.val : 0; const y = l2 ? l2.val : 0; const s = x + y + carry; carry = Math.floor(s / 10); tail.next = new ListNode(s % 10); tail = tail.next; if (l1) l1 = l1.next; if (l2) l2 = l2.next; } return dummy.next; } function build(arr) { const dummy = new ListNode(); let tail = dummy; for (const v of arr) { tail.next = new ListNode(v); tail = tail.next; } return dummy.next; } function dump(head) { const out = []; while (head) { out.push(head.val); head = head.next; } return out; } const ans = addTwoNumbers(build([2, 4, 3]), build([5, 6, 4])); console.log(dump(ans)); // [7, 0, 8] CTA If you often get stuck on boundary conditions in this problem, do these two drills right now:\nRe-implement while l1 or l2 or carry from memory without looking. Then solve LeetCode 445 and compare forward-order vs reverse-order addition. You can also continue with LeetCode 25 or LeetCode 142 to strengthen linked-list pointer fundamentals.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/2-add-two-numbers/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThis problem is just grade-school addition on a linked list: add one digit at a time, propagate carry, and append one final node if carry remains after both lists end. We move from naive ideas to the optimal one-pass solution, then map it to real engineering scenarios.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003ecarry\u003c/code\u003e, \u003ccode\u003esimulation\u003c/code\u003e, \u003ccode\u003eLeetCode 2\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Add Two Numbers, LeetCode 2, reverse-order list, carry, dummy node\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Use \u003ccode\u003edummy + tail + carry\u003c/code\u003e to sum two reverse-order linked lists in \u003ccode\u003eO(max(m,n))\u003c/code\u003e time, with common pitfalls, engineering analogies, and six-language runnable implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm-problem--algorithm\"\u003eA — Algorithm (Problem \u0026amp; Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eYou are given two \u003cstrong\u003enon-empty\u003c/strong\u003e linked lists representing two non-negative integers.\u003cbr\u003e\nDigits are stored in \u003cstrong\u003ereverse order\u003c/strong\u003e, and each node stores one digit.\u003cbr\u003e\nReturn their sum as a linked list in the same reverse order.\u003cbr\u003e\nExcept for number \u003ccode\u003e0\u003c/code\u003e, the input numbers do not have leading zeros.\u003c/p\u003e","title":"LeetCode 2: Add Two Numbers from Naive to Optimal Carry Simulation"},{"content":" Subtitle / Summary\nLeetCode 148 is not about whether you can sort; it is about choosing the right sorting strategy for linked-list constraints. For singly linked lists, merge sort fits naturally: split by middle, sort recursively, merge linearly.\nReading time: 12-16 min Tags: Hot100, linked list, merge sort, divide and conquer SEO keywords: Sort List, linked list merge sort, LeetCode 148, Hot100 Meta description: A practical ACERS guide for LeetCode 148 with derivation, complexity analysis, engineering mappings, and runnable code in multiple languages. A - Algorithm (Problem and Algorithm) Problem Restatement Given the head of a linked list head, sort it in ascending order and return the sorted list. Required time complexity: O(n log n).\nInput / Output Name Type Description head ListNode head of a singly linked list (nullable) return ListNode head of sorted list Example 1 input: 4 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; 3 output: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 Example 2 input: -1 -\u0026gt; 5 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 0 output: -1 -\u0026gt; 0 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 Target Readers Hot100 learners building reusable linked-list templates Developers who struggle with split-and-reconnect pointer safety Engineers who want a clear answer to \u0026ldquo;why merge sort for linked lists\u0026rdquo; Background / Motivation Sorting linked structures appears in real systems:\npost-processing chained tasks by priority offline reordering of append-only linked logs memory-conscious restructuring with minimal copying If you directly copy array-sorting intuition to linked lists, you usually hit:\nno O(1) random access expensive and error-prone pointer shuffling for quicksort-style partitioning So this problem is fundamentally about algorithm-data-structure fit.\nCore Concepts Divide and Conquer: split list to subproblems, then merge upward Fast/slow middle finding: slow moves 1 step, fast moves 2 Linked-list merge: linear splice of two sorted sublists Stable sorting: equal keys keep relative order C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the smallest unsorted list that reveals the structure Take:\n4 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; 3 We want:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 For arrays, \u0026ldquo;sort\u0026rdquo; often means random access and swapping. For linked lists, those are awkward. So the first real question is:\nwhat kind of sorting work matches linked-list strengths?\nStep 2: Ask what a linked list is naturally good at A linked list is good at:\nfinding a middle by linear scan cutting into two pieces with one next = None merging two already sorted chains by pointer rewiring That is almost a perfect description of merge sort. So we should not force an array-style sorting mindset onto a linked list.\nStep 3: Define the smaller subproblem first Let:\nsort_list(head) mean:\nreturn the sorted list built from the same nodes starting at head.\nThen the smallest solved cases are obvious:\nif head is None or head.next is None: return head An empty list or single node is already sorted.\nStep 4: Decide how to split the list safely We need two roughly equal halves. The usual tool is fast/slow pointers:\nslow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next When the loop ends, slow sits at the tail of the left half. So we cut at:\nmid = slow.next slow.next = None Now one list becomes two smaller lists.\nStep 5: Decide how sorted halves should be combined Once both halves are sorted recursively, the remaining job is not \u0026ldquo;sort again.\u0026rdquo; It is just:\nmerge(left, right) This is the same linear two-list merge idea as LeetCode 21:\ncompare current heads append the smaller node move that pointer forward Step 6: Why merge sort fits better than most alternatives Merge sort works here because every expensive linked-list weakness is avoided:\nno random indexing no backward scan no large-scale node swapping Instead we only do:\nsplit recurse merge That gives O(n log n) time with clean pointer logic.\nStep 7: Walk one recursion tree slowly For:\n4 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; 3 split into:\n4 -\u0026gt; 2 1 -\u0026gt; 3 then into single nodes:\n4 | 2 | 1 | 3 merge upward:\n2 -\u0026gt; 4 1 -\u0026gt; 3 final merge:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 That is exactly the recursion we want the code to express.\nStep 8: Reduce the method to one sentence LeetCode 148 is \u0026ldquo;keep splitting the list around the middle until each piece is trivially sorted, then merge the pieces back upward.\u0026rdquo;\nAssemble the Full Code from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def sort_list(head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: if head is None or head.next is None: return head slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next mid = slow.next slow.next = None left = sort_list(head) right = sort_list(mid) return merge(left, right) def merge(a: Optional[ListNode], b: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy while a and b: if a.val \u0026lt;= b.val: tail.next, a = a, a.next else: tail.next, b = b, b.next tail = tail.next tail.next = a if a else b return dummy.next Reference Answer class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def sortList(head): if not head or not head.next: return head slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next mid = slow.next slow.next = None left = sortList(head) right = sortList(mid) return merge(left, right) def merge(a, b): dummy = ListNode() t = dummy while a and b: if a.val \u0026lt;= b.val: t.next, a = a, a.next else: t.next, b = b, b.next t = t.next t.next = a if a else b return dummy.next Method Category linked-list divide-and-conquer sorting fast/slow split merge-template reuse (same pattern as LeetCode 21) Correctness intuition Base case: empty or single-node list is already sorted Induction: recursively sorted left/right halves are each sorted Merge: linear merge of two sorted lists remains sorted Therefore, the final result is sorted.\nComplexity recurrence T(n) = 2T(n/2) + O(n)\nBy Master theorem:\nT(n) = O(n log n) Practice Guide / Steps Return directly for 0/1 node list Find middle with fast/slow pointers and cut list into two halves Recursively sort left and right halves Merge two sorted halves with sentinel-node merge Return merged head Runnable Python example (sort_list.py):\nfrom typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def sort_list(head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: if head is None or head.next is None: return head slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next mid = slow.next slow.next = None left = sort_list(head) right = sort_list(mid) return merge(left, right) def merge(a: Optional[ListNode], b: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy while a and b: if a.val \u0026lt;= b.val: tail.next, a = a, a.next else: tail.next, b = b, b.next tail = tail.next tail.next = a if a else b return dummy.next E - Engineering (Real-world Scenarios) Scenario 1: Task-chain reordering by priority (Go) Background: tasks are chained in insertion order but must run by priority.\nWhy it fits: linked-list split+merge avoids repeated array conversion.\ntype Task struct { Priority int Next *Task } func merge(a, b *Task) *Task { d := \u0026amp;Task{} t := d for a != nil \u0026amp;\u0026amp; b != nil { if a.Priority \u0026lt;= b.Priority { t.Next, a = a, a.Next } else { t.Next, b = b, b.Next } t = t.Next } if a != nil { t.Next = a } else { t.Next = b } return d.Next } Scenario 2: Offline chained log normalization (Python) Background: append-order logs need timestamp-order output for auditing.\nWhy it fits: merge-friendly linear passes scale predictably.\ndef merge_sorted_logs(a, b): i = j = 0 out = [] while i \u0026lt; len(a) and j \u0026lt; len(b): if a[i][0] \u0026lt;= b[j][0]: out.append(a[i]); i += 1 else: out.append(b[j]); j += 1 out.extend(a[i:]) out.extend(b[j:]) return out Scenario 3: Frontend incremental feed merge (JavaScript) Background: cached and remote pages are already sorted and need merged rendering.\nWhy it fits: merge gives deterministic linear behavior and stable ordering.\nfunction mergeByScore(a, b) { let i = 0, j = 0; const out = []; while (i \u0026lt; a.length \u0026amp;\u0026amp; j \u0026lt; b.length) { if (a[i].score \u0026lt;= b[j].score) out.push(a[i++]); else out.push(b[j++]); } while (i \u0026lt; a.length) out.push(a[i++]); while (j \u0026lt; b.length) out.push(b[j++]); return out; } R - Reflection (Complexity, Alternatives, Tradeoffs) Complexity Time: O(n log n) Extra space: O(log n) recursion stack Alternatives Method Time Space Notes Array copy + sort O(n log n) O(n) easy but loses list advantages List quicksort avg O(n log n), worst O(n²) O(log n) partitioning is awkward on list List merge sort (this) O(n log n) O(log n) stable and structure-friendly Common mistakes Forgetting slow.next = None, causing infinite recursion Wrong fast/slow initialization for even lengths Missing tail attachment in merge Pointer loss during recursive split Why this is the practical default It matches linked-list properties directly:\nno random access dependency linear work per recursion level reusable merge logic across multiple linked-list problems FAQ and Notes Can this be strict O(1) extra space?\nTop-down recursion uses O(log n) stack. Strict O(1) needs bottom-up iterative merge sort.\nWhy not quicksort?\nLinked-list partitioning is less natural and worst-case risk is higher.\nDoes stability matter?\nYes when equal keys carry extra business metadata.\nBest Practices Treat split+merge as a reusable helper template Test null, one node, even/odd lengths, and duplicate values Prioritize pointer correctness before micro-optimization Learn bottom-up merge sort after mastering this version S - Summary Merge sort is the best default for linked-list sorting Core workflow is split -\u0026gt; sort halves -\u0026gt; merge Complexity satisfies the target: O(n log n) This template generalizes to many list-based merge tasks Further Reading LeetCode 21. Merge Two Sorted Lists LeetCode 23. Merge k Sorted Lists LeetCode 147. Insertion Sort List LeetCode 25. Reverse Nodes in k-Group References https://leetcode.com/problems/sort-list/ https://en.cppreference.com/w/cpp/algorithm/stable_sort https://docs.python.org/3/howto/sorting.html https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html Meta Info Reading time: 12-16 min Tags: Hot100, linked list, merge sort, divide and conquer SEO keywords: Sort List, linked list merge sort, LeetCode 148, Hot100 Meta description: A complete linked-list merge-sort guide for LeetCode 148 with derivation, complexity, and multi-language code. CTA Two practical next steps:\nRe-implement recursive list merge sort from scratch without notes Build the bottom-up iterative variant and compare space tradeoffs Multi-language Implementations (Python / C / C++ / Go / Rust / JS) class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def sortList(head): if not head or not head.next: return head slow, fast = head, head.next while fast and fast.next: slow = slow.next fast = fast.next.next mid = slow.next slow.next = None left = sortList(head) right = sortList(mid) return merge(left, right) def merge(a, b): dummy = ListNode() t = dummy while a and b: if a.val \u0026lt;= b.val: t.next, a = a, a.next else: t.next, b = b, b.next t = t.next t.next = a if a else b return dummy.next typedef struct ListNode { int val; struct ListNode* next; } ListNode; static ListNode* merge(ListNode* a, ListNode* b) { ListNode dummy = {0, NULL}; ListNode* t = \u0026amp;dummy; while (a \u0026amp;\u0026amp; b) { if (a-\u0026gt;val \u0026lt;= b-\u0026gt;val) { t-\u0026gt;next = a; a = a-\u0026gt;next; } else { t-\u0026gt;next = b; b = b-\u0026gt;next; } t = t-\u0026gt;next; } t-\u0026gt;next = a ? a : b; return dummy.next; } ListNode* sortList(ListNode* head) { if (!head || !head-\u0026gt;next) return head; ListNode* slow = head; ListNode* fast = head-\u0026gt;next; while (fast \u0026amp;\u0026amp; fast-\u0026gt;next) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; } ListNode* mid = slow-\u0026gt;next; slow-\u0026gt;next = NULL; return merge(sortList(head), sortList(mid)); } struct ListNode { int val; ListNode* next; ListNode(int x=0, ListNode* n=nullptr): val(x), next(n) {} }; class Solution { ListNode* merge(ListNode* a, ListNode* b) { ListNode dummy; ListNode* t = \u0026amp;dummy; while (a \u0026amp;\u0026amp; b) { if (a-\u0026gt;val \u0026lt;= b-\u0026gt;val) t-\u0026gt;next = a, a = a-\u0026gt;next; else t-\u0026gt;next = b, b = b-\u0026gt;next; t = t-\u0026gt;next; } t-\u0026gt;next = a ? a : b; return dummy.next; } public: ListNode* sortList(ListNode* head) { if (!head || !head-\u0026gt;next) return head; ListNode *slow = head, *fast = head-\u0026gt;next; while (fast \u0026amp;\u0026amp; fast-\u0026gt;next) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; } ListNode* mid = slow-\u0026gt;next; slow-\u0026gt;next = nullptr; return merge(sortList(head), sortList(mid)); } }; type ListNode struct { Val int Next *ListNode } func sortList(head *ListNode) *ListNode { if head == nil || head.Next == nil { return head } slow, fast := head, head.Next for fast != nil \u0026amp;\u0026amp; fast.Next != nil { slow = slow.Next fast = fast.Next.Next } mid := slow.Next slow.Next = nil left := sortList(head) right := sortList(mid) return merge(left, right) } func merge(a, b *ListNode) *ListNode { dummy := \u0026amp;ListNode{} t := dummy for a != nil \u0026amp;\u0026amp; b != nil { if a.Val \u0026lt;= b.Val { t.Next = a a = a.Next } else { t.Next = b b = b.Next } t = t.Next } if a != nil { t.Next = a } else { t.Next = b } return dummy.Next } #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { fn new(val: i32) -\u0026gt; Self { Self { val, next: None } } } pub fn sort_list(head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut vals = Vec::new(); let mut cur = head; let mut p = cur; while let Some(mut node) = p { vals.push(node.val); p = node.next.take(); } vals.sort_unstable(); let mut ans = None; for v in vals.into_iter().rev() { let mut node = Box::new(ListNode::new(v)); node.next = ans; ans = Some(node); } ans } function ListNode(val, next = null) { this.val = val; this.next = next; } function sortList(head) { if (!head || !head.next) return head; let slow = head; let fast = head.next; while (fast \u0026amp;\u0026amp; fast.next) { slow = slow.next; fast = fast.next.next; } const mid = slow.next; slow.next = null; return merge(sortList(head), sortList(mid)); } function merge(a, b) { const dummy = new ListNode(0); let t = dummy; while (a \u0026amp;\u0026amp; b) { if (a.val \u0026lt;= b.val) { t.next = a; a = a.next; } else { t.next = b; b = b.next; } t = t.next; } t.next = a || b; return dummy.next; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/148-sort-list/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nLeetCode 148 is not about whether you can sort; it is about choosing the right sorting strategy for linked-list constraints. For singly linked lists, merge sort fits naturally: split by middle, sort recursively, merge linearly.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-16 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003emerge sort\u003c/code\u003e, \u003ccode\u003edivide and conquer\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Sort List, linked list merge sort, LeetCode 148, Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A practical ACERS guide for LeetCode 148 with derivation, complexity analysis, engineering mappings, and runnable code in multiple languages.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the head of a linked list \u003ccode\u003ehead\u003c/code\u003e, sort it in ascending order and return the sorted list.\nRequired time complexity: \u003ccode\u003eO(n log n)\u003c/code\u003e.\u003c/p\u003e","title":"Hot100: Sort List Linked-List Merge Sort ACERS Guide"},{"content":" Subtitle / Summary\nLeetCode 23 is a k-way merge problem, not just repeating LeetCode 21 in a loop. This ACERS guide derives the optimal structure, explains tradeoffs between divide-and-conquer and min-heap, and provides runnable implementations in multiple languages.\nReading time: 12-16 min Tags: Hot100, linked list, divide and conquer, merge SEO keywords: Merge K Sorted Lists, LeetCode 23, divide and conquer, O(N log k), Hot100 Meta description: A full ACERS explanation of Merge K Sorted Lists from naive ideas to O(N log k) divide-and-conquer, with engineering mapping and multi-language code. A - Algorithm (Problem and Algorithm) Problem Restatement Given an array lists of k sorted linked lists, merge them into one sorted linked list and return it.\nInput / Output Name Type Description lists ListNode[] k sorted lists, each can be null return ListNode merged sorted linked list head Example 1 input: lists = [[1,4,5],[1,3,4],[2,6]] output: [1,1,2,3,4,4,5,6] Example 2 input: lists = [] output: [] Target Readers Hot100 learners who have finished LeetCode 21 and want the next-level merge template Developers who need predictable performance for k-way ordered data merge Engineers preparing for linked-list and divide-and-conquer interview rounds Background / Motivation This problem appears in many production forms:\nmerge sorted outputs from multiple shards combine sorted event streams from multiple services aggregate sorted pagination slices The hard part is not correctness alone; it is controlling cost when k grows.\nCore Concepts N: total number of nodes across all lists k: number of lists Sequential merge: merge one list into the current result repeatedly Divide-and-conquer merge: merge in balanced rounds Min-heap k-way merge: keep current head from each list in a heap C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the smallest example that is no longer just \u0026ldquo;merge two lists\u0026rdquo; Take:\nl1: 1 -\u0026gt; 4 l2: 1 -\u0026gt; 3 l3: 2 -\u0026gt; 6 We already know how to merge two sorted lists from LeetCode 21. So this problem is not asking us to invent a brand-new local merge rule. It is asking a scheduling question:\nin what order should we reuse merge_two so the total work stays small?\nStep 2: Reject the tempting left-to-right schedule The most direct idea is:\n((l1 merge l2) merge l3) merge l4 ... This works, but the merged result keeps getting longer. That means early nodes can be scanned again and again.\nIf there are k lists of similar size, this repeated rescanning can drift toward O(Nk). So the merge rule is fine; the schedule is bad.\nStep 3: Ask what one reusable subproblem should mean We want a function that solves:\nmerge all lists inside an interval [l, r].\nIf we can solve the left half and the right half, then the remaining work is again just:\nmerge_two(left_result, right_result) That turns the whole problem back into the already-familiar two-list merge.\nStep 4: Balance the merge tree Instead of always merging into one growing result, merge in rounds:\n(l1,l2) (l3,l4) (l5,l6) ... then merge those results again.\nThis creates a balanced merge tree:\neach level processes about N nodes in total the number of levels is about log k So every node participates in only about log k merge rounds.\nStep 5: Define the smaller recursive problem Let:\nsolve(l, r) mean:\nthe fully merged sorted list built from lists[l] through lists[r].\nThen the base case is immediate:\nif l == r: return lists[l] because one list is already sorted.\nStep 6: Define how one level combines results Split the interval in the middle:\nm = (l + r) // 2 left = solve(l, m) right = solve(m + 1, r) return merge_two(left, right) This is the whole structure. The local merge logic still comes from LeetCode 21; divide-and-conquer only decides the merge order.\nStep 7: Walk one merge tree slowly For:\n[l1, l2, l3, l4] the recursion becomes:\nsolve(0, 3) ├─ solve(0, 1) -\u0026gt; merge_two(l1, l2) └─ solve(2, 3) -\u0026gt; merge_two(l3, l4) Then the final step is:\nmerge_two(merge(l1,l2), merge(l3,l4)) No list is forced to absorb all later lists by itself. That is exactly why the work stays balanced.\nStep 8: Reduce the method to one sentence LeetCode 23 is \u0026ldquo;reuse merge_two, but arrange the merges as a balanced tree instead of a long chain.\u0026rdquo;\nAssemble the Full Code from typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def merge_two(a: Optional[ListNode], b: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy while a and b: if a.val \u0026lt;= b.val: nxt = a.next tail.next = a a.next = None a = nxt else: nxt = b.next tail.next = b b.next = None b = nxt tail = tail.next tail.next = a if a else b return dummy.next def merge_k_lists(lists: List[Optional[ListNode]]) -\u0026gt; Optional[ListNode]: if not lists: return None def solve(l: int, r: int) -\u0026gt; Optional[ListNode]: if l == r: return lists[l] m = (l + r) // 2 return merge_two(solve(l, m), solve(m + 1, r)) return solve(0, len(lists) - 1) Reference Answer from typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def merge_two(a: Optional[ListNode], b: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy while a and b: if a.val \u0026lt;= b.val: nxt = a.next tail.next = a a.next = None a = nxt else: nxt = b.next tail.next = b b.next = None b = nxt tail = tail.next tail.next = a if a else b return dummy.next def merge_k_lists(lists: List[Optional[ListNode]]) -\u0026gt; Optional[ListNode]: if not lists: return None def solve(l: int, r: int) -\u0026gt; Optional[ListNode]: if l == r: return lists[l] m = (l + r) // 2 return merge_two(solve(l, m), solve(m + 1, r)) return solve(0, len(lists) - 1) Method Category Divide and conquer Linked-list in-place splicing Same complexity class as heap-based k-way merge Correctness Invariant For interval [l, r]:\nsolve(l, r) returns a fully sorted merge of all lists in that interval if left and right halves are correctly merged, mergeTwo(left, right) preserves sorted order and includes every node exactly once Practice Guide / Steps Reuse a stable mergeTwo helper (LeetCode 21 template) Build recursive solve(l, r): base: l == r return lists[l] split at mid, solve both halves Merge results from both halves Start from full range [0, k-1] Runnable Python example (merge_k_lists.py):\nfrom typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def merge_two(a: Optional[ListNode], b: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy while a and b: if a.val \u0026lt;= b.val: nxt = a.next tail.next = a a.next = None a = nxt else: nxt = b.next tail.next = b b.next = None b = nxt tail = tail.next tail.next = a if a else b return dummy.next def merge_k_lists(lists: List[Optional[ListNode]]) -\u0026gt; Optional[ListNode]: if not lists: return None def solve(l: int, r: int) -\u0026gt; Optional[ListNode]: if l == r: return lists[l] m = (l + r) // 2 return merge_two(solve(l, m), solve(m + 1, r)) return solve(0, len(lists) - 1) Explanation / Why This Works Balanced merging avoids repeatedly merging a huge partial result with small lists.\nwork per level: about N node operations number of levels: about log k So total time is O(N log k), while preserving in-place node reuse.\nE - Engineering (Real-world Scenarios) Scenario 1: Merge sorted shard timelines (Go) Background: each shard emits sorted events by timestamp.\nWhy it fits: divide-and-conquer merge is easy to parallelize by levels.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Ts int Next *Node } func mergeTwo(a, b *Node) *Node { dummy := \u0026amp;Node{} tail := dummy for a != nil \u0026amp;\u0026amp; b != nil { if a.Ts \u0026lt;= b.Ts { nxt := a.Next tail.Next = a a.Next = nil a = nxt } else { nxt := b.Next tail.Next = b b.Next = nil b = nxt } tail = tail.Next } if a != nil { tail.Next = a } else { tail.Next = b } return dummy.Next } func main() { a := \u0026amp;Node{1, \u0026amp;Node{4, \u0026amp;Node{9, nil}}} b := \u0026amp;Node{2, \u0026amp;Node{5, nil}} for p := mergeTwo(a, b); p != nil; p = p.Next { fmt.Print(p.Ts, \u0026#34; \u0026#34;) } fmt.Println() } Scenario 2: Offline merge of sorted rule outputs (Python) Background: multiple ranking pipelines output sorted IDs. Why it fits: divide-and-conquer gives stable performance for large k.\ndef merge_two(a, b): i = j = 0 out = [] while i \u0026lt; len(a) and j \u0026lt; len(b): if a[i] \u0026lt;= b[j]: out.append(a[i]); i += 1 else: out.append(b[j]); j += 1 out.extend(a[i:]) out.extend(b[j:]) return out def merge_k(arrays): if not arrays: return [] cur = arrays while len(cur) \u0026gt; 1: nxt = [] for i in range(0, len(cur), 2): if i + 1 \u0026lt; len(cur): nxt.append(merge_two(cur[i], cur[i + 1])) else: nxt.append(cur[i]) cur = nxt return cur[0] Scenario 3: Frontend unified feed from multiple sorted sources (JavaScript) Background: web app receives sorted cards from multiple APIs. Why it fits: deterministic merge order with no global re-sort.\nfunction mergeTwo(a, b) { let i = 0; let j = 0; const out = []; while (i \u0026lt; a.length \u0026amp;\u0026amp; j \u0026lt; b.length) { if (a[i].ts \u0026lt;= b[j].ts) out.push(a[i++]); else out.push(b[j++]); } while (i \u0026lt; a.length) out.push(a[i++]); while (j \u0026lt; b.length) out.push(b[j++]); return out; } R - Reflection (Complexity, Alternatives, Tradeoffs) Complexity Time: O(N log k) Space: O(log k) recursion stack Alternative Methods Method Time Space Notes Flatten + sort O(N log N) O(N) easiest, wastes structure Sequential merge near O(Nk) worst O(1) degrades as k grows Min-heap O(N log k) O(k) great for streaming inputs Divide-and-conquer O(N log k) O(log k) clean and reusable Common Mistakes Forgetting empty input handling (lists=[]) Assuming sequential merge is always efficient Losing nodes in mergeTwo pointer rewiring Incorrect base case in recursion Why this method is practical It balances performance and implementation simplicity. You can directly reuse LeetCode 21 helper logic and scale it to k-way merge.\nFAQ and Notes Divide-and-conquer or heap?\nBatch merge: divide-and-conquer is often cleaner. Streaming merge: heap is often better.\nCan this be fully in-place?\nYes, by rewiring next pointers (except helper/sentinel nodes).\nWhat if values repeat?\nNo issue. Use \u0026lt;= to keep stable tie behavior.\nBest Practices Treat mergeTwo as a shared utility function Never use sequential k-way merge for large k Validate with edge cases: empty lists, many null lists, uneven lengths Track k and N in production metrics for strategy switching S - Summary LeetCode 23 is a k-way merge scaling problem Divide-and-conquer reduces complexity to O(N log k) Heap and divide-and-conquer are both optimal in asymptotic time Mastering this pattern helps with many merge-based interview and production tasks Further Reading LeetCode 21. Merge Two Sorted Lists LeetCode 23. Merge K Sorted Lists LeetCode 148. Sort List LeetCode 632. Smallest Range Covering Elements from K Lists Conclusion The key upgrade from LeetCode 21 to 23 is structural thinking:\nmove from sequential accumulation to balanced reduction. This is a general engineering pattern for multi-source ordered data.\nReferences https://leetcode.com/problems/merge-k-sorted-lists/ https://docs.python.org/3/library/heapq.html https://en.cppreference.com/w/cpp/container/priority_queue https://pkg.go.dev/container/heap Meta Info Reading time: 12-16 min Tags: Hot100, linked list, divide and conquer, merge SEO keywords: Merge K Sorted Lists, LeetCode 23, O(N log k), Hot100 Meta description: End-to-end ACERS guide for LeetCode 23 with complexity derivation and multi-language implementations. CTA Implement mergeTwo + mergeK in your strongest language first, then re-implement in a second language. That cross-language pass is the fastest way to internalize pointer invariants.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) from typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def merge_two(a: Optional[ListNode], b: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode() tail = dummy while a and b: if a.val \u0026lt;= b.val: nxt = a.next tail.next = a a.next = None a = nxt else: nxt = b.next tail.next = b b.next = None b = nxt tail = tail.next tail.next = a if a else b return dummy.next def merge_k_lists(lists: List[Optional[ListNode]]) -\u0026gt; Optional[ListNode]: if not lists: return None def solve(l: int, r: int) -\u0026gt; Optional[ListNode]: if l == r: return lists[l] m = (l + r) // 2 return merge_two(solve(l, m), solve(m + 1, r)) return solve(0, len(lists) - 1) #include \u0026lt;stddef.h\u0026gt; typedef struct ListNode { int val; struct ListNode* next; } ListNode; ListNode* mergeTwo(ListNode* a, ListNode* b) { ListNode dummy; dummy.next = NULL; ListNode* tail = \u0026amp;dummy; while (a \u0026amp;\u0026amp; b) { if (a-\u0026gt;val \u0026lt;= b-\u0026gt;val) { ListNode* nxt = a-\u0026gt;next; tail-\u0026gt;next = a; a-\u0026gt;next = NULL; a = nxt; } else { ListNode* nxt = b-\u0026gt;next; tail-\u0026gt;next = b; b-\u0026gt;next = NULL; b = nxt; } tail = tail-\u0026gt;next; } tail-\u0026gt;next = a ? a : b; return dummy.next; } ListNode* solve(ListNode** lists, int l, int r) { if (l \u0026gt; r) return NULL; if (l == r) return lists[l]; int m = l + (r - l) / 2; ListNode* left = solve(lists, l, m); ListNode* right = solve(lists, m + 1, r); return mergeTwo(left, right); } ListNode* mergeKLists(ListNode** lists, int listsSize) { if (listsSize == 0) return NULL; return solve(lists, 0, listsSize - 1); } #include \u0026lt;vector\u0026gt; using namespace std; struct ListNode { int val; ListNode* next; ListNode(int x = 0, ListNode* n = nullptr) : val(x), next(n) {} }; class Solution { ListNode* mergeTwo(ListNode* a, ListNode* b) { ListNode dummy; ListNode* tail = \u0026amp;dummy; while (a \u0026amp;\u0026amp; b) { if (a-\u0026gt;val \u0026lt;= b-\u0026gt;val) { ListNode* nxt = a-\u0026gt;next; tail-\u0026gt;next = a; a-\u0026gt;next = nullptr; a = nxt; } else { ListNode* nxt = b-\u0026gt;next; tail-\u0026gt;next = b; b-\u0026gt;next = nullptr; b = nxt; } tail = tail-\u0026gt;next; } tail-\u0026gt;next = a ? a : b; return dummy.next; } ListNode* solve(vector\u0026lt;ListNode*\u0026gt;\u0026amp; lists, int l, int r) { if (l \u0026gt; r) return nullptr; if (l == r) return lists[l]; int m = l + (r - l) / 2; return mergeTwo(solve(lists, l, m), solve(lists, m + 1, r)); } public: ListNode* mergeKLists(vector\u0026lt;ListNode*\u0026gt;\u0026amp; lists) { if (lists.empty()) return nullptr; return solve(lists, 0, (int)lists.size() - 1); } }; package main type ListNode struct { Val int Next *ListNode } func mergeTwo(a, b *ListNode) *ListNode { dummy := \u0026amp;ListNode{} tail := dummy for a != nil \u0026amp;\u0026amp; b != nil { if a.Val \u0026lt;= b.Val { nxt := a.Next tail.Next = a a.Next = nil a = nxt } else { nxt := b.Next tail.Next = b b.Next = nil b = nxt } tail = tail.Next } if a != nil { tail.Next = a } else { tail.Next = b } return dummy.Next } func solve(lists []*ListNode, l, r int) *ListNode { if l \u0026gt; r { return nil } if l == r { return lists[l] } m := l + (r-l)/2 left := solve(lists, l, m) right := solve(lists, m+1, r) return mergeTwo(left, right) } func mergeKLists(lists []*ListNode) *ListNode { if len(lists) == 0 { return nil } return solve(lists, 0, len(lists)-1) } #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] fn new(val: i32) -\u0026gt; Self { ListNode { val, next: None } } } fn merge_two(a: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, b: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { match (a, b) { (None, x) =\u0026gt; x, (x, None) =\u0026gt; x, (Some(mut na), Some(mut nb)) =\u0026gt; { if na.val \u0026lt;= nb.val { let next = na.next.take(); na.next = merge_two(next, Some(nb)); Some(na) } else { let next = nb.next.take(); nb.next = merge_two(Some(na), next); Some(nb) } } } } fn solve(lists: \u0026amp;mut [Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;], l: usize, r: usize) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { if l == r { return lists[l].take(); } let m = (l + r) / 2; let left = solve(lists, l, m); let right = solve(lists, m + 1, r); merge_two(left, right) } pub fn merge_k_lists(mut lists: Vec\u0026lt;Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;\u0026gt;) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { if lists.is_empty() { return None; } let n = lists.len(); solve(\u0026amp;mut lists, 0, n - 1) } function ListNode(val, next = null) { this.val = val; this.next = next; } function mergeTwo(a, b) { const dummy = new ListNode(0); let tail = dummy; while (a \u0026amp;\u0026amp; b) { if (a.val \u0026lt;= b.val) { const nxt = a.next; tail.next = a; a.next = null; a = nxt; } else { const nxt = b.next; tail.next = b; b.next = null; b = nxt; } tail = tail.next; } tail.next = a || b; return dummy.next; } function solve(lists, l, r) { if (l \u0026gt; r) return null; if (l === r) return lists[l]; const m = (l + r) \u0026gt;\u0026gt; 1; return mergeTwo(solve(lists, l, m), solve(lists, m + 1, r)); } function mergeKLists(lists) { if (!lists || lists.length === 0) return null; return solve(lists, 0, lists.length - 1); } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/23-merge-k-sorted-lists/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nLeetCode 23 is a k-way merge problem, not just repeating LeetCode 21 in a loop. This ACERS guide derives the optimal structure, explains tradeoffs between divide-and-conquer and min-heap, and provides runnable implementations in multiple languages.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-16 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003edivide and conquer\u003c/code\u003e, \u003ccode\u003emerge\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Merge K Sorted Lists, LeetCode 23, divide and conquer, O(N log k), Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A full ACERS explanation of Merge K Sorted Lists from naive ideas to O(N log k) divide-and-conquer, with engineering mapping and multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven an array \u003ccode\u003elists\u003c/code\u003e of \u003ccode\u003ek\u003c/code\u003e sorted linked lists, merge them into one sorted linked list and return it.\u003c/p\u003e","title":"Hot100: Merge K Sorted Lists Divide-and-Conquer O(N log k) ACERS Guide"},{"content":" Subtitle / Summary\nLeetCode 142 upgrades cycle detection into cycle entry localization. The robust template is Floyd: first detect a meeting inside the cycle, then reset one pointer to head and move both by one step; the next meeting node is the cycle entry.\nReading time: 12-16 min Tags: Hot100, linked list, fast slow pointers, Floyd SEO keywords: Linked List Cycle II, cycle entry, Floyd, fast slow pointers, O(1) space, LeetCode 142, Hot100 Meta description: Floyd cycle detection + entry localization with proof intuition, engineering mapping, and runnable multi-language implementations in O(n) time and O(1) extra space. A - Algorithm (Problem and Algorithm) Problem Restatement Given head of a singly linked list, return the node where the cycle begins. If there is no cycle, return null.\nNotes:\npos in the statement is only for test-data construction pos is not a function argument list structure must not be modified Input / Output Name Type Description head ListNode head of singly linked list return ListNode / null entry node reference, or null Example 1 head: 3 -\u0026gt; 2 -\u0026gt; 0 -\u0026gt; -4 ^ | |_________| output: node(2) Example 2 head: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; null output: null Target Readers Hot100 learners who want to fully internalize the 141 -\u0026gt; 142 linked-list template family Developers who need to locate where a pointer chain becomes cyclic Interview candidates who want to explain why \u0026ldquo;reset to head\u0026rdquo; works Background / Motivation In real systems, cycle corruption in chain structures can cause:\nendless traversal loops stuck cleanup tasks misleadingly stable but non-progressing runtime behavior Detecting whether a cycle exists is helpful, but operations/debugging usually require more:\nwhere does the cycle begin? That exact requirement is modeled by LeetCode 142.\nCore Concepts Concept Meaning Why it matters Cycle Following next eventually revisits a node causes non-terminating traversals Entry node First node where linear prefix enters the loop required return value Floyd algorithm slow moves 1 step, fast moves 2 steps O(1) extra memory Meeting point First collision inside cycle bridge to entry localization Identity equality compare node reference, not node value value duplicates are common C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the smallest picture that separates \u0026ldquo;has cycle\u0026rdquo; from \u0026ldquo;where is the entry\u0026rdquo; Imagine:\nhead -\u0026gt; a -\u0026gt; b -\u0026gt; c -\u0026gt; d ^ | |_________| Here:\nthe list does have a cycle the answer is specifically node b So LeetCode 142 is stricter than LeetCode 141. Detection alone is not enough; we need the first node where the linear prefix enters the loop.\nStep 2: Ask what the most direct correct solution would remember The simplest approach is:\ntraverse node by node store each node reference in a set the first repeated node is the entry This is easy to reason about, but it costs O(n) extra space. The problem becomes interesting only when we try to keep memory at O(1).\nStep 3: Reuse Floyd for phase 1 and define when the work is settled With Floyd:\nslow moves 1 step fast moves 2 steps Then exactly one of two things happens:\nfast reaches null or fast.next reaches null, so there is no cycle slow and fast meet inside the cycle That meeting gives us more than a boolean result: it gives us a concrete node already inside the loop.\nStep 4: Ask what information the meeting point gives us Suppose:\na = distance from head to cycle entry b = distance from cycle entry to first meeting point c = cycle length At the first meeting:\nslow traveled a + b fast traveled 2(a + b) Their distance difference must be whole cycle lengths:\n2(a + b) - (a + b) = k * c =\u0026gt; a + b = k * c =\u0026gt; a = k * c - b This means:\nfrom head to entry: a steps from meeting point to entry: also a steps modulo the cycle That is the hidden structure we need for localization.\nStep 5: Turn the math into a second pointer phase Once slow meets fast, keep one pointer at that meeting node and reset the other to head:\np1 = head p2 = slow Now move both one step at a time. Because both are exactly a steps away from the entry in the modular sense, their next meeting point must be the entry.\nStep 6: Walk one concrete trace Take:\n3 -\u0026gt; 2 -\u0026gt; 0 -\u0026gt; -4 ^ | |_________| The entry is node 2. Floyd first meets somewhere inside the cycle, not necessarily at 2. But after resetting one pointer to head and moving both one step at a time, they align at 2.\nThat is why the algorithm returns the entry instead of an arbitrary cycle node.\nStep 7: Reduce the method to one sentence LeetCode 142 is \u0026ldquo;use Floyd to get one guaranteed node inside the cycle, then align head and meeting point to walk into the entry together.\u0026rdquo;\nAssemble the Full Code from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def detect_cycle(head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: slow = head fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: p1 = head p2 = slow while p1 is not p2: p1 = p1.next p2 = p2.next return p1 return None Reference Answer from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def detect_cycle(head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: slow = head fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: p1 = head p2 = slow while p1 is not p2: p1 = p1.next p2 = p2.next return p1 return None Method Category Two pointers (fast/slow) Floyd cycle detection Distance alignment after reset Why reset-to-head works Define:\na: distance from head to cycle entry b: distance from entry to first meeting point c: cycle length At first meeting:\nslow traveled a + b fast traveled 2(a + b) Difference is one whole number of cycle lengths:\n2(a + b) - (a + b) = k * c =\u0026gt; a + b = k * c =\u0026gt; a = k * c - b Meaning:\nfrom head to entry: a steps from meeting point to entry: also a steps modulo cycle length So moving one pointer from head and one from meeting, both one step each round, guarantees meeting at the entry.\nInvariant for localization phase During phase 2, both pointers have equal remaining distance (modulo cycle) to entry. Equal-speed synchronous movement preserves this equality, so collision point is entry.\nPractical Guide / Steps Initialize slow = head, fast = head Detection loop: slow = slow.next fast = fast.next.next if pointers meet, break If fast == null or fast.next == null, no cycle -\u0026gt; return null Set p1 = head, p2 = meeting Move both by one step until p1 == p2 Return p1 (entry) Runnable Example (Python) from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def detect_cycle(head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: slow = head fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: p1 = head p2 = slow while p1 is not p2: p1 = p1.next p2 = p2.next return p1 return None def build_cycle_list(values, pos): dummy = ListNode() tail = dummy entry = None for idx, x in enumerate(values): tail.next = ListNode(x) tail = tail.next if idx == pos: entry = tail if tail and pos \u0026gt;= 0: tail.next = entry return dummy.next if __name__ == \u0026#34;__main__\u0026#34;: h = build_cycle_list([3, 2, 0, -4], 1) e = detect_cycle(h) print(e.val if e else None) # 2 Explanation / Why This Works The algorithm is split by responsibility:\nphase 1 answers: cycle or no cycle phase 2 answers: where cycle starts This separation is important for correctness and debugging.\nSetting one pointer to head is not a trick; it is distance alignment derived from meeting-point equations. That is why this method is both memory-optimal and proof-friendly.\nE - Engineering (Real-world Scenarios) Scenario 1: asynchronous callback chain corruption check (Go) Background: callback chain unexpectedly loops in production.\nWhy it fits: no extra map allocation, works on large chains.\npackage main type Node struct { Val int Next *Node } func detectCycle(head *Node) *Node { slow, fast := head, head for fast != nil \u0026amp;\u0026amp; fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { p1, p2 := head, slow for p1 != p2 { p1 = p1.Next p2 = p2.Next } return p1 } } return nil } Scenario 2: ETL pointer-chain anomaly localization (Python) Background: transformed record chain can accidentally self-link.\nWhy it fits: allows locating the first bad join node directly.\n# Reuse detect_cycle(head) above and log entry node identity/value. Scenario 3: front-end linked-state graph guard (JavaScript) Background: linked state nodes in memory may form unintended cycles. Why it fits: fast runtime check in debug tooling.\nfunction detectCycle(head) { let slow = head; let fast = head; while (fast \u0026amp;\u0026amp; fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) { let p1 = head; let p2 = slow; while (p1 !== p2) { p1 = p1.next; p2 = p2.next; } return p1; } } return null; } R - Reflection Complexity Time: O(n) Extra space: O(1) Alternatives and Tradeoffs Method Time Space Notes visited hash set O(n) O(n) easy, but extra memory Floyd + reset O(n) O(1) optimal for constraints Common Mistakes forgetting fast \u0026amp;\u0026amp; fast.next null checks comparing node values (val) instead of references returning meeting point directly (meeting is not always entry) modifying list to mark visited nodes (forbidden by problem) Why this method is optimal in practice no structural mutation no extra memory pressure deterministic behavior under large lists strong proof story for interviews and code review FAQ and Notes Why is the first meeting point not necessarily the entry?\nBecause fast and slow can first collide anywhere inside the cycle.\nCan this fail with duplicate values?\nNo, if you compare references (is, == pointer) rather than values.\nWhat if list length is very small?\nNull checks naturally handle 0 or 1 node lists.\nCan we stop once cycle is detected?\nFor LeetCode 141 yes; for 142 you must run localization phase.\nBest Practices Implement 141 and 142 as a pair to reuse mental model Keep phase separation explicit in code (detect then locate) Add tests for: no cycle single-node self-cycle cycle entry at head cycle entry in middle In production debug tools, print entry node identity and predecessor info when possible S - Summary LeetCode 142 extends cycle detection to entry localization Floyd detects cycle with O(1) memory Reset-to-head works by distance alignment, not memorized magic Correct implementation depends on reference comparison and null-safe traversal This pattern is reusable for many chain-integrity diagnostics Recommended Follow-up LeetCode 141 — Linked List Cycle LeetCode 160 — Intersection of Two Linked Lists Floyd cycle-finding notes in pointer-heavy systems General linked-list invariants and mutation safety patterns Conclusion Once you understand \u0026ldquo;detect first, then reset and align distances\u0026rdquo;, Linked List Cycle II becomes a stable template rather than a memorized trick.\nReferences https://leetcode.com/problems/linked-list-cycle-ii/ https://en.wikipedia.org/wiki/Cycle_detection https://en.cppreference.com/w/cpp/language/pointer https://go.dev/doc/effective_go Meta Info Reading time: 12-16 min Tags: Hot100, linked list, fast/slow pointers, Floyd SEO keywords: Linked List Cycle II, cycle entry, Floyd, LeetCode 142 Meta description: O(n)/O(1) cycle entry localization with Floyd fast/slow pointers and reset alignment proof. Call To Action (CTA) Run this mini drill:\nRe-implement 141 and 142 back-to-back from memory Write the distance equation once (a+b = k*c) and explain it aloud Build four edge-case tests and verify pointer identity-based assertions Multi-language Implementations (Python / C / C++ / Go / Rust / JS) from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def detect_cycle(head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: slow = head fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: p1 = head p2 = slow while p1 is not p2: p1 = p1.next p2 = p2.next return p1 return None struct ListNode { int val; struct ListNode *next; }; struct ListNode *detectCycle(struct ListNode *head) { struct ListNode *slow = head; struct ListNode *fast = head; while (fast \u0026amp;\u0026amp; fast-\u0026gt;next) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; if (slow == fast) { struct ListNode *p1 = head; struct ListNode *p2 = slow; while (p1 != p2) { p1 = p1-\u0026gt;next; p2 = p2-\u0026gt;next; } return p1; } } return NULL; } class Solution { public: ListNode *detectCycle(ListNode *head) { ListNode *slow = head; ListNode *fast = head; while (fast \u0026amp;\u0026amp; fast-\u0026gt;next) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; if (slow == fast) { ListNode *p1 = head; ListNode *p2 = slow; while (p1 != p2) { p1 = p1-\u0026gt;next; p2 = p2-\u0026gt;next; } return p1; } } return nullptr; } }; func detectCycle(head *ListNode) *ListNode { slow, fast := head, head for fast != nil \u0026amp;\u0026amp; fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { p1, p2 := head, slow for p1 != p2 { p1 = p1.Next p2 = p2.Next } return p1 } } return nil } use std::cell::RefCell; use std::rc::Rc; type Link = Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;ListNode\u0026gt;\u0026gt;\u0026gt;; #[derive(Debug)] pub struct ListNode { pub val: i32, pub next: Link, } pub fn detect_cycle(head: Link) -\u0026gt; Link { let mut slow = head.clone(); let mut fast = head.clone(); loop { slow = match slow.clone() { Some(node) =\u0026gt; node.borrow().next.clone(), None =\u0026gt; return None, }; fast = match fast.clone() { Some(node) =\u0026gt; match node.borrow().next.clone() { Some(next1) =\u0026gt; next1.borrow().next.clone(), None =\u0026gt; return None, }, None =\u0026gt; return None, }; match (slow.clone(), fast.clone()) { (Some(s), Some(f)) if Rc::ptr_eq(\u0026amp;s, \u0026amp;f) =\u0026gt; break, (Some(_), Some(_)) =\u0026gt; {} _ =\u0026gt; return None, } } let mut p1 = head; let mut p2 = slow; loop { match (p1.clone(), p2.clone()) { (Some(a), Some(b)) =\u0026gt; { if Rc::ptr_eq(\u0026amp;a, \u0026amp;b) { return Some(a); } p1 = a.borrow().next.clone(); p2 = b.borrow().next.clone(); } _ =\u0026gt; return None, } } } function detectCycle(head) { let slow = head; let fast = head; while (fast \u0026amp;\u0026amp; fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) { let p1 = head; let p2 = slow; while (p1 !== p2) { p1 = p1.next; p2 = p2.next; } return p1; } } return null; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/142-linked-list-cycle-ii/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nLeetCode 142 upgrades cycle detection into cycle entry localization. The robust template is Floyd: first detect a meeting inside the cycle, then reset one pointer to \u003ccode\u003ehead\u003c/code\u003e and move both by one step; the next meeting node is the cycle entry.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-16 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003efast slow pointers\u003c/code\u003e, \u003ccode\u003eFloyd\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Linked List Cycle II, cycle entry, Floyd, fast slow pointers, O(1) space, LeetCode 142, Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Floyd cycle detection + entry localization with proof intuition, engineering mapping, and runnable multi-language implementations in O(n) time and O(1) extra space.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven \u003ccode\u003ehead\u003c/code\u003e of a singly linked list, return the node where the cycle begins.\nIf there is no cycle, return \u003ccode\u003enull\u003c/code\u003e.\u003c/p\u003e","title":"Hot100: Linked List Cycle II Floyd Detection + Entry Localization ACERS Guide"},{"content":" Subtitle / Summary\nThis problem is the linked-list version of merge-sort\u0026rsquo;s merge step. Use a sentinel node plus two pointers to splice nodes in ascending order in O(m+n), without rebuilding the list.\nReading time: 10-12 min Tags: Hot100, linked list, merge, two pointers SEO keywords: Merge Two Sorted Lists, sentinel node, linked list merge, LeetCode 21, Hot100 Meta description: A complete ACERS guide for LeetCode 21 with derivation, correctness invariants, pitfalls, and runnable multi-language code. A - Algorithm (Problem and Algorithm) Problem Restatement Given heads list1 and list2 of two sorted linked lists, merge them into one sorted linked list and return its head. The merged list should be formed by splicing together nodes from the original lists.\nInput / Output Name Type Description list1 ListNode Head of sorted list 1 (nullable) list2 ListNode Head of sorted list 2 (nullable) return ListNode Head of merged sorted list Example 1 list1: 1 -\u0026gt; 2 -\u0026gt; 4 list2: 1 -\u0026gt; 3 -\u0026gt; 4 output: 1 -\u0026gt; 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 4 Example 2 list1: null list2: 0 -\u0026gt; 5 output: 0 -\u0026gt; 5 Target Readers Hot100 learners preparing linked-list interview templates Developers who often lose nodes while rewiring next Engineers who need stable O(1)-extra-space merge patterns Background / Motivation This is a small problem with large transfer value:\nIt is a direct building block of merge k sorted lists It reinforces pointer safety under in-place rewiring It mirrors real-world merging of two already sorted streams If this template is stable in your hands, many linked-list and divide-and-conquer problems become easier.\nCore Concepts Sorted linked list: non-decreasing values along next Splicing merge: reuse original nodes by rewiring pointers Sentinel (dummy) node: avoids special handling for the head of result Tail pointer: always points to the last node in merged list C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the smallest merge that still shows the pattern Take:\nlist1: 1 -\u0026gt; 2 -\u0026gt; 4 list2: 1 -\u0026gt; 3 -\u0026gt; 4 The goal is not to sort again from scratch. The goal is to keep pulling the smallest current head from two lists that are already sorted.\nThat means the next output node must always be one of these two nodes:\nlist1 head list2 head Step 2: Decide what state the partial answer needs To build the result online, we need three moving pointers:\ntail: the end of the merged result p1: current head of the remaining part of list1 p2: current head of the remaining part of list2 tail = dummy p1 = list1 p2 = list2 Step 3: Ask why a sentinel node helps Without a sentinel, the very first append needs special handling:\nis this the first node? what is the result head? With a sentinel, every append becomes the same operation:\ntail.next = node tail = tail.next That removes head-specific branching from the main loop.\nStep 4: Define one merge step As long as both lists are non-empty, the next smallest node is whichever head has the smaller value:\nif p1.val \u0026lt;= p2.val: ... else: ... Then we append that node to tail.next.\nStep 5: What state changes after one choice? Suppose we choose p1. Then three things must happen together:\ntail.next = p1 p1 = p1.next tail = tail.next If we choose p2, it is the symmetric update.\nStep 6: When can comparison stop? The moment one list becomes empty, the remaining part of the other list is already sorted. So we can attach the whole remainder in one move:\ntail.next = p1 if p1 is not None else p2 That is why the full merge stays linear.\nStep 7: Walk one concrete trace Start:\np1 = 1 p2 = 1 tail = dummy Choose p1 first:\nresult: 1 p1 moves to 2 Now compare 2 and 1, so choose p2:\nresult: 1 -\u0026gt; 1 p2 moves to 3 Then compare 2 and 3, choose 2, and keep repeating the same local action.\nStep 8: Reduce the whole method to one sentence The stable template is:\nkeep tail at the end of the result, compare p1 and p2, append the smaller head, and finally attach the remaining suffix.\nAssemble the Full Code from typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def merge_two_lists(list1: Optional[ListNode], list2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode(0) tail = dummy p1, p2 = list1, list2 while p1 is not None and p2 is not None: if p1.val \u0026lt;= p2.val: nxt = p1.next tail.next = p1 p1.next = None p1 = nxt else: nxt = p2.next tail.next = p2 p2.next = None p2 = nxt tail = tail.next tail.next = p1 if p1 is not None else p2 return dummy.next def from_list(arr: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() cur = dummy for x in arr: cur.next = ListNode(x) cur = cur.next return dummy.next def to_list(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: l1 = from_list([1, 2, 4]) l2 = from_list([1, 3, 4]) print(to_list(merge_two_lists(l1, l2))) print(to_list(merge_two_lists(None, from_list([0, 5])))) Reference Answer class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def merge_two_lists(list1, list2): dummy = ListNode(0) tail = dummy p1, p2 = list1, list2 while p1 and p2: if p1.val \u0026lt;= p2.val: nxt = p1.next tail.next = p1 p1.next = None p1 = nxt else: nxt = p2.next tail.next = p2 p2.next = None p2 = nxt tail = tail.next tail.next = p1 if p1 else p2 return dummy.next Method Category Two-pointer merge In-place linked-list splicing Sentinel node pattern Loop Invariant Before each iteration:\ndummy.next ... tail is already sorted p1 and p2 point to the first unmerged nodes in each list All nodes before p1 and p2 have been merged exactly once After appending the smaller head, invariants still hold. When one list ends, append the rest of the other list directly.\nWhy appending the remainder is safe If p1 is null, all remaining nodes in p2 are already in sorted order and all are \u0026gt;= tail.val. So attaching tail.next = p2 preserves sorted order.\nPractice Guide / Steps Create dummy and set tail = dummy Initialize p1 = list1, p2 = list2 While both are non-null: if p1.val \u0026lt;= p2.val, append p1 else append p2 move tail Append the non-null remainder Return dummy.next Runnable Python example (merge_two_lists.py):\nfrom typing import List, Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def merge_two_lists(list1: Optional[ListNode], list2: Optional[ListNode]) -\u0026gt; Optional[ListNode]: dummy = ListNode(0) tail = dummy p1, p2 = list1, list2 while p1 is not None and p2 is not None: if p1.val \u0026lt;= p2.val: nxt = p1.next tail.next = p1 p1.next = None p1 = nxt else: nxt = p2.next tail.next = p2 p2.next = None p2 = nxt tail = tail.next tail.next = p1 if p1 is not None else p2 return dummy.next def from_list(arr: List[int]) -\u0026gt; Optional[ListNode]: dummy = ListNode() cur = dummy for x in arr: cur.next = ListNode(x) cur = cur.next return dummy.next def to_list(head: Optional[ListNode]) -\u0026gt; List[int]: out: List[int] = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: l1 = from_list([1, 2, 4]) l2 = from_list([1, 3, 4]) print(to_list(merge_two_lists(l1, l2))) print(to_list(merge_two_lists(None, from_list([0, 5])))) Explanation / Why This Works Each step picks the globally smallest remaining node between the two list heads. That is exactly the merge-sort merge principle.\nBecause every node is moved once, and no node is revisited:\ntime is linear in total node count space is constant (pointer variables + sentinel) E - Engineering (Real-world Scenarios) Scenario 1: Merge two ordered event streams (Go) Background: backend services often merge logs/events by timestamp. Why it fits: both inputs are already sorted; linear merge minimizes latency.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Ts int Next *Node } func merge(a, b *Node) *Node { dummy := \u0026amp;Node{} tail := dummy for a != nil \u0026amp;\u0026amp; b != nil { if a.Ts \u0026lt;= b.Ts { tail.Next = a a = a.Next } else { tail.Next = b b = b.Next } tail = tail.Next } if a != nil { tail.Next = a } else { tail.Next = b } return dummy.Next } func main() { a := \u0026amp;Node{1, \u0026amp;Node{3, \u0026amp;Node{7, nil}}} b := \u0026amp;Node{2, \u0026amp;Node{4, \u0026amp;Node{8, nil}}} for p := merge(a, b); p != nil; p = p.Next { fmt.Print(p.Ts, \u0026#34; \u0026#34;) } fmt.Println() } Scenario 2: Offline merge of sorted ID sets (Python) Background: analytics jobs frequently merge sorted outputs from two rules. Why it fits: pointer-based merge avoids expensive global sort for pre-sorted sources.\ndef merge_sorted(a, b): i = j = 0 out = [] while i \u0026lt; len(a) and j \u0026lt; len(b): if a[i] \u0026lt;= b[j]: out.append(a[i]); i += 1 else: out.append(b[j]); j += 1 out.extend(a[i:]) out.extend(b[j:]) return out print(merge_sorted([1, 3, 7], [2, 4, 8])) Scenario 3: Frontend timeline composition from two sorted feeds (JavaScript) Background: one feed from local cache and one from server response. Why it fits: stable merge keeps timeline sorted with predictable complexity.\nfunction mergeSortedFeeds(a, b) { let i = 0; let j = 0; const out = []; while (i \u0026lt; a.length \u0026amp;\u0026amp; j \u0026lt; b.length) { if (a[i].ts \u0026lt;= b[j].ts) out.push(a[i++]); else out.push(b[j++]); } while (i \u0026lt; a.length) out.push(a[i++]); while (j \u0026lt; b.length) out.push(b[j++]); return out; } console.log(mergeSortedFeeds([{ ts: 1 }, { ts: 5 }], [{ ts: 2 }, { ts: 4 }])); R - Reflection (Complexity, Alternatives, Tradeoffs) Complexity Time: O(m+n) Space: O(1) extra (iterative pointer solution) Alternatives Method Time Extra Space Notes Flatten + sort + rebuild O((m+n)log(m+n)) O(m+n) easy but not in-place Recursive merge O(m+n) O(m+n) stack worst-case concise but stack risk Sentinel iterative merge O(m+n) O(1) most practical in interviews and systems code Common mistakes Forgetting to move tail after attachment Losing list remainder by not attaching final non-null list Mishandling null inputs Creating new nodes unnecessarily Why this is the best practical method It matches constraints and is robust:\nlinear no extra container simple invariant-based correctness FAQ and Notes Do we need to allocate new nodes?\nNo, splicing existing nodes is enough.\nIs recursion acceptable?\nFunctionally yes, but iterative is safer for long lists.\nWhat if equal values appear?\nUse \u0026lt;= for stable preference from list1.\nBest Practices Always use dummy + tail for list-construction tasks Keep pointer updates in one consistent order Test edge cases: empty lists, one empty, all equal values Reuse this merge as a helper for merge k lists and list sorting S - Summary Merge Two Sorted Lists is exactly merge-sort\u0026rsquo;s merge step on linked lists Sentinel node removes head special-case complexity Two-pointer iterative merge is O(m+n) time and O(1) extra space This template is foundational for many advanced linked-list problems Further Reading LeetCode 21. Merge Two Sorted Lists LeetCode 23. Merge k Sorted Lists LeetCode 148. Sort List LeetCode 206. Reverse Linked List Conclusion If you can write this merge in one pass without pointer mistakes, your linked-list fundamentals are in good shape. Practice this with merge k lists next to make the pattern production-ready.\nReferences https://leetcode.com/problems/merge-two-sorted-lists/ https://en.cppreference.com/w/cpp/container/forward_list https://docs.python.org/3/tutorial/datastructures.html https://doc.rust-lang.org/std/option/ Meta Info Reading time: 10-12 min Tags: Hot100, linked list, merge, two pointers SEO keywords: Merge Two Sorted Lists, LeetCode 21, sentinel node, O(m+n) Meta description: A practical ACERS guide to sentinel-node linked-list merge with multi-language runnable code. CTA Try implementing this in under 10 minutes in your primary language. Then extend it to merge k sorted lists to lock in the divide-and-conquer upgrade path.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def merge_two_lists(list1, list2): dummy = ListNode(0) tail = dummy p1, p2 = list1, list2 while p1 and p2: if p1.val \u0026lt;= p2.val: nxt = p1.next tail.next = p1 p1.next = None p1 = nxt else: nxt = p2.next tail.next = p2 p2.next = None p2 = nxt tail = tail.next tail.next = p1 if p1 else p2 return dummy.next #include \u0026lt;stddef.h\u0026gt; typedef struct ListNode { int val; struct ListNode* next; } ListNode; ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { ListNode dummy; dummy.next = NULL; ListNode* tail = \u0026amp;dummy; while (list1 \u0026amp;\u0026amp; list2) { if (list1-\u0026gt;val \u0026lt;= list2-\u0026gt;val) { ListNode* nxt = list1-\u0026gt;next; tail-\u0026gt;next = list1; list1-\u0026gt;next = NULL; list1 = nxt; } else { ListNode* nxt = list2-\u0026gt;next; tail-\u0026gt;next = list2; list2-\u0026gt;next = NULL; list2 = nxt; } tail = tail-\u0026gt;next; } tail-\u0026gt;next = list1 ? list1 : list2; return dummy.next; } struct ListNode { int val; ListNode* next; ListNode(int x = 0, ListNode* n = nullptr) : val(x), next(n) {} }; class Solution { public: ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) { ListNode dummy; ListNode* tail = \u0026amp;dummy; while (list1 \u0026amp;\u0026amp; list2) { if (list1-\u0026gt;val \u0026lt;= list2-\u0026gt;val) { ListNode* nxt = list1-\u0026gt;next; tail-\u0026gt;next = list1; list1-\u0026gt;next = nullptr; list1 = nxt; } else { ListNode* nxt = list2-\u0026gt;next; tail-\u0026gt;next = list2; list2-\u0026gt;next = nullptr; list2 = nxt; } tail = tail-\u0026gt;next; } tail-\u0026gt;next = list1 ? list1 : list2; return dummy.next; } }; package main type ListNode struct { Val int Next *ListNode } func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode { dummy := \u0026amp;ListNode{} tail := dummy for list1 != nil \u0026amp;\u0026amp; list2 != nil { if list1.Val \u0026lt;= list2.Val { nxt := list1.Next tail.Next = list1 list1.Next = nil list1 = nxt } else { nxt := list2.Next tail.Next = list2 list2.Next = nil list2 = nxt } tail = tail.Next } if list1 != nil { tail.Next = list1 } else { tail.Next = list2 } return dummy.Next } #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] fn new(val: i32) -\u0026gt; Self { ListNode { val, next: None } } } pub fn merge_two_lists( mut list1: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, mut list2: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, ) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut dummy = Box::new(ListNode::new(0)); let mut tail = \u0026amp;mut dummy; while list1.is_some() \u0026amp;\u0026amp; list2.is_some() { let take_left = list1.as_ref().unwrap().val \u0026lt;= list2.as_ref().unwrap().val; if take_left { let mut node = list1.take().unwrap(); list1 = node.next.take(); tail.next = Some(node); } else { let mut node = list2.take().unwrap(); list2 = node.next.take(); tail.next = Some(node); } tail = tail.next.as_mut().unwrap(); } tail.next = if list1.is_some() { list1 } else { list2 }; dummy.next } function ListNode(val, next = null) { this.val = val; this.next = next; } function mergeTwoLists(list1, list2) { const dummy = new ListNode(0); let tail = dummy; let p1 = list1; let p2 = list2; while (p1 \u0026amp;\u0026amp; p2) { if (p1.val \u0026lt;= p2.val) { const nxt = p1.next; tail.next = p1; p1.next = null; p1 = nxt; } else { const nxt = p2.next; tail.next = p2; p2.next = null; p2 = nxt; } tail = tail.next; } tail.next = p1 || p2; return dummy.next; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/21-merge-two-sorted-lists/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThis problem is the linked-list version of merge-sort\u0026rsquo;s merge step. Use a sentinel node plus two pointers to splice nodes in ascending order in O(m+n), without rebuilding the list.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003emerge\u003c/code\u003e, \u003ccode\u003etwo pointers\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Merge Two Sorted Lists, sentinel node, linked list merge, LeetCode 21, Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A complete ACERS guide for LeetCode 21 with derivation, correctness invariants, pitfalls, and runnable multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven heads \u003ccode\u003elist1\u003c/code\u003e and \u003ccode\u003elist2\u003c/code\u003e of two sorted linked lists, merge them into one sorted linked list and return its head.\nThe merged list should be formed by \u003cstrong\u003esplicing together\u003c/strong\u003e nodes from the original lists.\u003c/p\u003e","title":"Hot100: Merge Two Sorted Lists Sentinel Two-Pointer Merge ACERS Guide"},{"content":" Subtitle / Summary\nFirst Missing Positive is a classic in-place indexing problem. Place each valid value x into slot x-1, then scan for the first mismatch. This ACERS guide explains the derivation, invariant, pitfalls, and production-style transfer.\nReading time: 12-15 min Tags: Hot100, array, in-place hashing SEO keywords: First Missing Positive, in-place hashing, index mapping, O(n), Hot100, LeetCode 41 Meta description: O(n)/O(1) solution for First Missing Positive using in-place index placement, with complexity analysis, engineering scenarios, and runnable multi-language code. Target Readers Hot100 learners building stable array templates Intermediate developers who want to master in-place indexing techniques Engineers who need linear-time, constant-space array normalization Background / Motivation \u0026ldquo;Find the smallest missing positive\u0026rdquo; is fundamentally a placement problem.\nIf value x is present and 1 \u0026lt;= x \u0026lt;= n, then in an ideal arrangement it should sit at index x-1. Once this structure is built, the answer is just the first index where the rule breaks.\nThe challenge is constraint-driven:\nO(n) time O(1) extra space That forces us to avoid sorting (O(n log n)) and extra hash sets (O(n) space), and use in-place swapping instead.\nCore Concepts Concept Meaning Why it matters In-place hashing Use array indices as hash buckets keeps extra space O(1) Index placement value x belongs to index x-1 transforms search into scan Swap-to-place repeatedly swap until stable enables O(n) normalization A - Algorithm (Problem and Algorithm) Problem Restatement Given an unsorted integer array nums, return the smallest missing positive integer. You must design an algorithm with O(n) time and O(1) extra space.\nInput / Output Name Type Description nums int[] unsorted integer array return int smallest missing positive integer Example 1 input: nums = [1, 2, 0] output: 3 Example 2 input: nums = [3, 4, -1, 1] output: 2 High-Level Procedure For each index i, keep swapping until nums[i] is either invalid or already in correct slot. After placement, scan from left to right. First index i where nums[i] != i + 1 gives answer i + 1. If all match, answer is n + 1. Thought Process: From Sorting/Hashing to In-Place Placement Naive option 1: brute-force candidate test Try 1, 2, 3, ... and scan the array each time. This is O(n^2), not acceptable for large inputs.\nNaive option 2: sort then scan Sorting makes order visible, but time becomes O(n log n), violating O(n) requirement.\nNaive option 3: hash set Hash set gives O(n) time, but costs O(n) extra memory, violating O(1) space.\nKey observation Only values in range [1, n] can affect the answer within [1, n+1]. So we can treat the array itself as buckets:\nvalue x should be placed at index x - 1 Once placement converges, the first broken bucket reveals the answer.\nC - Concepts (Core Ideas) Method Category In-place hashing (index-as-hash) Array normalization via swapping Linear validation scan Core Invariant After placement phase:\nif positive integer k exists in array and 1 \u0026lt;= k \u0026lt;= n, then ideally nums[k-1] == k first index i with nums[i] != i+1 means value i+1 is missing Why repeated swaps are still linear Each successful swap places at least one value into its final position. A value can be moved only a limited number of times before becoming stable. Total swaps across whole array remain O(n).\nPractical Guide / Steps Let n = len(nums), set pointer i = 0 While i \u0026lt; n: v = nums[i] if 1 \u0026lt;= v \u0026lt;= n and nums[v-1] != v, swap nums[i] with nums[v-1] else i += 1 Run a second scan: first i where nums[i] != i+1, return i+1 If no mismatch, return n+1 Runnable Example (Python) from typing import List def first_missing_positive(nums: List[int]) -\u0026gt; int: n = len(nums) i = 0 while i \u0026lt; n: v = nums[i] if 1 \u0026lt;= v \u0026lt;= n and nums[v - 1] != v: nums[i], nums[v - 1] = nums[v - 1], nums[i] else: i += 1 for i, v in enumerate(nums): if v != i + 1: return i + 1 return n + 1 if __name__ == \u0026#34;__main__\u0026#34;: print(first_missing_positive([1, 2, 0])) print(first_missing_positive([3, 4, -1, 1])) Run:\npython3 first_missing_positive.py Explanation / Why This Works This algorithm converts a search problem into a placement problem.\nValues outside [1, n] are irrelevant for first missing positive in [1, n+1] Valid value x is forced toward slot x-1 Placement phase builds a partially sorted-by-meaning layout Scan phase finds the first missing positive in one pass So we satisfy both constraints simultaneously: O(n) time and O(1) extra memory.\nE - Engineering (Real-world Scenarios) Scenario 1: next available compact ID in analytics batch (Python) Background: find smallest missing positive ID in a daily import batch.\nWhy it fits: no extra structures needed for large in-memory batches.\ndef next_missing_id(ids): n = len(ids) i = 0 while i \u0026lt; n: v = ids[i] if 1 \u0026lt;= v \u0026lt;= n and ids[v - 1] != v: ids[i], ids[v - 1] = ids[v - 1], ids[i] else: i += 1 for idx, val in enumerate(ids): if val != idx + 1: return idx + 1 return n + 1 print(next_missing_id([2, 1, 4, 6, 3])) Scenario 2: shard-index gap detection in backend config (Go) Background: detect smallest missing shard number in an unsorted config list.\nWhy it fits: linear-time check during startup validation.\npackage main import \u0026#34;fmt\u0026#34; func firstMissingPositive(nums []int) int { n := len(nums) i := 0 for i \u0026lt; n { v := nums[i] if v \u0026gt;= 1 \u0026amp;\u0026amp; v \u0026lt;= n \u0026amp;\u0026amp; nums[v-1] != v { nums[i], nums[v-1] = nums[v-1], nums[i] } else { i++ } } for i, v := range nums { if v != i+1 { return i + 1 } } return n + 1 } func main() { fmt.Println(firstMissingPositive([]int{3, 4, -1, 1})) } Scenario 3: front-end task sequence gap finder (JavaScript) Background: allocate the smallest missing positive sequence number on client side.\nWhy it fits: no dependency on backend call or extra cache.\nfunction firstMissingPositive(nums) { const n = nums.length; let i = 0; while (i \u0026lt; n) { const v = nums[i]; if (v \u0026gt;= 1 \u0026amp;\u0026amp; v \u0026lt;= n \u0026amp;\u0026amp; nums[v - 1] !== v) { const tmp = nums[v - 1]; nums[v - 1] = nums[i]; nums[i] = tmp; } else { i += 1; } } for (let j = 0; j \u0026lt; n; j += 1) { if (nums[j] !== j + 1) return j + 1; } return n + 1; } console.log(firstMissingPositive([1, 2, 0])); R - Reflection Complexity Time: O(n) Extra space: O(1) Alternative Comparison Method Time Space Issue brute-force candidate checks O(n^2) O(1) too slow sorting + scan O(n log n) O(1) / O(log n) violates linear-time target hash set O(n) O(n) violates constant-space target in-place index placement O(n) O(1) best fit for constraints Common Mistakes Forgetting duplicate guard nums[v-1] != v, causing endless swaps Advancing i after every swap (should re-check new value at same index) Trying to place values outside [1, n] Returning wrong fallback (should be n + 1 when all slots match) Why this method is practically optimal It converts strict constraints into structure:\nno extra memory allocations linear scan + bounded swapping deterministic and template-friendly for array normalization tasks FAQ and Notes Why ignore non-positive values and values \u0026gt; n?\nThey cannot be the first missing positive in [1, n+1].\nWhy can answer be n+1?\nIf all 1..n exist, the smallest missing positive is next one.\nWill duplicates break correctness?\nNo, as long as duplicate guard is present before swap.\nCan this be done without swaps?\nThere are marking-based variants, but swap placement is the most direct O(1)-space template.\nBest Practices Memorize the placement condition as one line: 1 \u0026lt;= v \u0026lt;= n \u0026amp;\u0026amp; nums[v-1] != v Keep two phases explicit: placement then scan Add tests for edge cases: all negatives already continuous [1..n] duplicates empty array Prefer stable variable names (v, n, i) for readability in pointer-style loops S - Summary First Missing Positive is an index-placement problem under strict constraints In-place hashing maps value x to slot x-1 Placement + validation scan gives O(n)/O(1) Duplicate guard and loop control are the two bug hotspots This pattern is highly reusable for array normalization tasks Recommended Follow-up LeetCode 448 — Find All Numbers Disappeared in an Array LeetCode 442 — Find All Duplicates in an Array LeetCode 287 — Find the Duplicate Number Cyclic sort / index placement pattern notes Conclusion Once you internalize \u0026ldquo;place each value to its index slot, then scan first mismatch\u0026rdquo;, LeetCode 41 becomes a reusable engineering pattern rather than an interview trick.\nReferences https://leetcode.com/problems/first-missing-positive/ https://en.cppreference.com/w/cpp/algorithm/swap https://docs.python.org/3/library/stdtypes.html#list https://go.dev/doc/effective_go Meta Info Reading time: 12-15 min Tags: Hot100, array, in-place hashing SEO keywords: First Missing Positive, in-place hashing, index mapping, LeetCode 41 Meta description: O(n)/O(1) first missing positive with in-place index placement and linear validation. Call To Action (CTA) Do this drill sequence now:\nRe-implement 41 from memory with duplicate guard Adapt same idea to 448 (missing numbers) Compare swap-based placement and sign-marking variants Multi-language Implementations (Python / C / C++ / Go / Rust / JS) from typing import List def first_missing_positive(nums: List[int]) -\u0026gt; int: n = len(nums) i = 0 while i \u0026lt; n: v = nums[i] if 1 \u0026lt;= v \u0026lt;= n and nums[v - 1] != v: nums[i], nums[v - 1] = nums[v - 1], nums[i] else: i += 1 for i, v in enumerate(nums): if v != i + 1: return i + 1 return n + 1 int firstMissingPositive(int* nums, int numsSize) { int i = 0; while (i \u0026lt; numsSize) { int v = nums[i]; if (v \u0026gt;= 1 \u0026amp;\u0026amp; v \u0026lt;= numsSize \u0026amp;\u0026amp; nums[v - 1] != v) { int tmp = nums[v - 1]; nums[v - 1] = nums[i]; nums[i] = tmp; } else { i++; } } for (i = 0; i \u0026lt; numsSize; ++i) { if (nums[i] != i + 1) return i + 1; } return numsSize + 1; } class Solution { public: int firstMissingPositive(vector\u0026lt;int\u0026gt;\u0026amp; nums) { int n = (int)nums.size(); int i = 0; while (i \u0026lt; n) { int v = nums[i]; if (v \u0026gt;= 1 \u0026amp;\u0026amp; v \u0026lt;= n \u0026amp;\u0026amp; nums[v - 1] != v) { swap(nums[i], nums[v - 1]); } else { i++; } } for (int i = 0; i \u0026lt; n; ++i) { if (nums[i] != i + 1) return i + 1; } return n + 1; } }; func firstMissingPositive(nums []int) int { n := len(nums) i := 0 for i \u0026lt; n { v := nums[i] if v \u0026gt;= 1 \u0026amp;\u0026amp; v \u0026lt;= n \u0026amp;\u0026amp; nums[v-1] != v { nums[i], nums[v-1] = nums[v-1], nums[i] } else { i++ } } for i, v := range nums { if v != i+1 { return i + 1 } } return n + 1 } pub fn first_missing_positive(nums: \u0026amp;mut Vec\u0026lt;i32\u0026gt;) -\u0026gt; i32 { let n = nums.len(); let mut i = 0usize; while i \u0026lt; n { let v = nums[i]; if v \u0026gt;= 1 \u0026amp;\u0026amp; (v as usize) \u0026lt;= n \u0026amp;\u0026amp; nums[(v - 1) as usize] != v { nums.swap(i, (v - 1) as usize); } else { i += 1; } } for (i, v) in nums.iter().enumerate() { if *v != (i as i32) + 1 { return (i as i32) + 1; } } (n as i32) + 1 } function firstMissingPositive(nums) { const n = nums.length; let i = 0; while (i \u0026lt; n) { const v = nums[i]; if (v \u0026gt;= 1 \u0026amp;\u0026amp; v \u0026lt;= n \u0026amp;\u0026amp; nums[v - 1] !== v) { [nums[i], nums[v - 1]] = [nums[v - 1], nums[i]]; } else { i += 1; } } for (let i = 0; i \u0026lt; n; i += 1) { if (nums[i] !== i + 1) return i + 1; } return n + 1; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/41-first-missing-positive/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nFirst Missing Positive is a classic in-place indexing problem. Place each valid value \u003ccode\u003ex\u003c/code\u003e into slot \u003ccode\u003ex-1\u003c/code\u003e, then scan for the first mismatch. This ACERS guide explains the derivation, invariant, pitfalls, and production-style transfer.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003earray\u003c/code\u003e, \u003ccode\u003ein-place hashing\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: First Missing Positive, in-place hashing, index mapping, O(n), Hot100, LeetCode 41\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: O(n)/O(1) solution for First Missing Positive using in-place index placement, with complexity analysis, engineering scenarios, and runnable multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHot100 learners building stable array templates\u003c/li\u003e\n\u003cli\u003eIntermediate developers who want to master in-place indexing techniques\u003c/li\u003e\n\u003cli\u003eEngineers who need linear-time, constant-space array normalization\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003e\u0026ldquo;Find the smallest missing positive\u0026rdquo; is fundamentally a \u003cstrong\u003eplacement\u003c/strong\u003e problem.\u003c/p\u003e","title":"Hot100: First Missing Positive In-Place Index Placement ACERS Guide"},{"content":" Subtitle / Summary\nLeetCode 25 is the upgrade path from 206 (full reversal) and 92 (interval reversal): split by groups, reverse inside each full group, reconnect safely, and keep the last incomplete group unchanged.\nReading time: 14-18 min Tags: Hot100, linked list, group reversal, dummy node SEO keywords: Reverse Nodes in k-Group, group reversal, LeetCode 25, Hot100 Meta description: In-place k-group linked-list reversal with dummy-node anchoring and safe reconnection, including pitfalls, complexity, and runnable multi-language implementations. A - Algorithm (Problem and Algorithm) Problem Restatement Given the head of a linked list and an integer k, reverse nodes in groups of size k and return the modified head.\nIf the number of remaining nodes is less than k, keep them in original order.\nYou must modify pointers, not just node values.\nInput / Output Name Type Description head ListNode head of singly linked list k int group size (k \u0026gt;= 1) return ListNode new head after group-wise reversal Example 1 input: head = 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5, k = 2 output: 2 -\u0026gt; 1 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 5 Example 2 input: head = 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5, k = 3 output: 3 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; 4 -\u0026gt; 5 Target Readers Hot100 learners who already finished 206/92 and want the next linked-list jump Developers who often fail at boundary handling and reconnection in pointer-heavy tasks Engineers building reusable templates for chunk-based list transformation Background / Motivation In real systems, chain structures are often processed in batches:\nreplay compensation tasks per fixed batch size local reorder of pipeline nodes by chunk in-place structure rewrite without reallocating all nodes These tasks require:\ntransformation inside each batch stable order between batches clear rule for incomplete tail batches (keep unchanged) LeetCode 25 models exactly this requirement.\nCore Concepts Dummy node: removes head-special branches groupPrev: predecessor of the current group kth probe: checks whether a full group of size k exists groupNext: first node after the current group In-place group reversal: reverse only [groupStart, kth] C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the smallest example that shows the real difficulty Take:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5, k = 3 The answer is:\n3 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; 4 -\u0026gt; 5 This immediately shows two constraints:\nfull groups of size k must be reversed the last incomplete group must stay unchanged So the problem is not \u0026ldquo;keep reversing forever.\u0026rdquo; It is \u0026ldquo;reverse only verified full blocks.\u0026rdquo;\nStep 2: Ask what each outer loop must know before touching pointers For the current group we need three boundary references:\ngroupPrev: the node before this group kth: the k-th node of this group groupNext: the first node after this group Those names already explain the whole control flow. Without them, reconnection becomes error-prone.\nStep 3: Why must we scan kth first? Suppose fewer than k nodes remain. Then the correct action is:\ndo nothing and stop So we must confirm group completeness before rewiring any pointer.\nThat is why every round starts with:\nkth = group_prev for _ in range(k): kth = kth.next if not kth: return dummy.next If this scan fails, the tail block is intentionally preserved.\nStep 4: Reuse the interval-reversal idea locally Once [groupPrev.next, kth] is confirmed to be a full group, the task becomes a local reversal problem.\nLet:\ngroup_next = kth.next prev = group_next cur = group_prev.next Now we reverse nodes until cur reaches group_next. Initializing prev with group_next is the key trick: it makes the reversed group reconnect to the remaining suffix automatically.\nStep 5: Define one reversal move inside the group Each inner-loop iteration moves one node from the untouched part of the group onto the reversed front:\nnxt = cur.next cur.next = prev prev = cur cur = nxt This is the same three-pointer reversal as LeetCode 206, but its stopping boundary is group_next, not None.\nStep 6: Reconnect and advance to the next group After the inner loop:\nprev is the new head of this reversed group group_prev.next is still the old group head, which has now become the new tail So:\nnew_group_tail = group_prev.next group_prev.next = prev group_prev = new_group_tail That last line is what advances the outer loop safely.\nStep 7: Walk one group slowly Still using:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5, k = 3 First round:\ngroupPrev = dummy kth = 3 groupNext = 4 Reverse [1,2,3] while pointing the new tail toward 4:\n3 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; 4 -\u0026gt; 5 Now groupPrev moves to 1. From there we try to scan another full group, fail because only 4 -\u0026gt; 5 remains, and stop.\nStep 8: Reduce the method to one sentence LeetCode 25 is \u0026ldquo;scan a full block first, reverse that block locally, reconnect it, then jump to the next block.\u0026rdquo;\nAssemble the Full Code from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_k_group(head: Optional[ListNode], k: int) -\u0026gt; Optional[ListNode]: if not head or k \u0026lt;= 1: return head dummy = ListNode(0, head) group_prev = dummy while True: kth = group_prev for _ in range(k): kth = kth.next if not kth: return dummy.next group_next = kth.next prev = group_next cur = group_prev.next while cur != group_next: nxt = cur.next cur.next = prev prev = cur cur = nxt new_group_tail = group_prev.next group_prev.next = prev group_prev = new_group_tail Reference Answer from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_k_group(head: Optional[ListNode], k: int) -\u0026gt; Optional[ListNode]: if not head or k \u0026lt;= 1: return head dummy = ListNode(0, head) group_prev = dummy while True: kth = group_prev for _ in range(k): kth = kth.next if not kth: return dummy.next group_next = kth.next prev = group_next cur = group_prev.next while cur != group_next: nxt = cur.next cur.next = prev prev = cur cur = nxt new_group_tail = group_prev.next group_prev.next = prev group_prev = new_group_tail Method Category In-place linked-list rewiring Chunk/batch processing Boundary locating with predecessor + kth probe Loop Invariant At the start of each iteration:\ngroupPrev.next points to the first node of the next unprocessed group all nodes before groupPrev are already in final form After one successful group operation:\ncurrent group is reversed and reconnected correctly groupPrev moves to the new tail of this reversed group (old group head) This guarantees stable forward progress without breaking previous groups.\nStructure Sketch (k=3) dummy -\u0026gt; a -\u0026gt; b -\u0026gt; c -\u0026gt; d -\u0026gt; e -\u0026gt; f -\u0026gt; g ^ ^ groupStart kth reverse [a,b,c] =\u0026gt; dummy -\u0026gt; c -\u0026gt; b -\u0026gt; a -\u0026gt; d -\u0026gt; e -\u0026gt; f -\u0026gt; g ^ new groupPrev Practical Guide / Steps Initialize dummy.next = head, set groupPrev = dummy Scan k steps from groupPrev to find kth if missing, stop (tail group is incomplete) Save groupNext = kth.next Reverse [groupPrev.next, kth] with prev = groupNext Reconnect: groupPrev.next -\u0026gt; new group head old group head becomes new group tail Move groupPrev to new group tail and continue Runnable Example (Python) from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_k_group(head: Optional[ListNode], k: int) -\u0026gt; Optional[ListNode]: if not head or k \u0026lt;= 1: return head dummy = ListNode(0, head) group_prev = dummy while True: kth = group_prev for _ in range(k): kth = kth.next if not kth: return dummy.next group_next = kth.next prev = group_next cur = group_prev.next while cur != group_next: nxt = cur.next cur.next = prev prev = cur cur = nxt new_group_tail = group_prev.next group_prev.next = prev group_prev = new_group_tail def build(nums): dummy = ListNode() tail = dummy for x in nums: tail.next = ListNode(x) tail = tail.next return dummy.next def to_list(head): ans = [] while head: ans.append(head.val) head = head.next return ans if __name__ == \u0026#34;__main__\u0026#34;: h = build([1, 2, 3, 4, 5]) print(to_list(reverse_k_group(h, 2))) # [2, 1, 4, 3, 5] Explanation (Why This Works) groupPrev acts as a stable anchor before the current group.\nInside each group, set prev = groupNext before reversal, so the reversed tail automatically reconnects to the next segment.\nThis removes extra branch logic and keeps each group operation structurally identical.\nE - Engineering (Real-world Scenarios) Scenario 1: Batch compensation-chain replay (Go) Background: reverse order inside each fixed-size task batch.\nWhy it fits: in-place, bounded memory, clear tail behavior.\npackage main type Node struct { Val int Next *Node } func reverseKGroup(head *Node, k int) *Node { if head == nil || k \u0026lt;= 1 { return head } dummy := \u0026amp;Node{Next: head} groupPrev := dummy for { kth := groupPrev for i := 0; i \u0026lt; k; i++ { kth = kth.Next if kth == nil { return dummy.Next } } groupNext := kth.Next prev, cur := groupNext, groupPrev.Next for cur != groupNext { nxt := cur.Next cur.Next = prev prev = cur cur = nxt } tail := groupPrev.Next groupPrev.Next = prev groupPrev = tail } } Scenario 2: Segment rollback in event chains (Python) Background: replay/rollback events by fixed group windows.\nWhy it fits: exact group control and deterministic pointer behavior.\n# Reuse reverse_k_group(head, k) from above. Scenario 3: Workflow editor chunk reversal (JavaScript) Background: UI supports \u0026ldquo;reverse every k selected nodes\u0026rdquo;.\nWhy it fits: efficient in-memory operation without rebuilding the whole chain.\nfunction reverseKGroup(head, k) { if (!head || k \u0026lt;= 1) return head; const dummy = { val: 0, next: head }; let groupPrev = dummy; while (true) { let kth = groupPrev; for (let i = 0; i \u0026lt; k; i += 1) { kth = kth.next; if (!kth) return dummy.next; } const groupNext = kth.next; let prev = groupNext; let cur = groupPrev.next; while (cur !== groupNext) { const nxt = cur.next; cur.next = prev; prev = cur; cur = nxt; } const newGroupTail = groupPrev.next; groupPrev.next = prev; groupPrev = newGroupTail; } } R - Reflection Complexity Analysis Time: O(n)\nEach node is visited and rewired a constant number of times. Space: O(1)\nOnly constant pointer variables are used. Alternatives and Tradeoffs Method Time Space Notes array conversion O(n) O(n) simpler, but not in-place recursive group reversal O(n) O(n/k)~O(n) stack elegant but stack-risky iterative in-place group reversal O(n) O(1) production-friendly default Common Mistakes not checking whether kth exists before reversing forgetting to move groupPrev after each group (can cause infinite loops) reversing incomplete tail group (violates problem requirement) swapping values instead of rewiring nodes Why this approach is optimal in practice linear time constant memory explicit and testable boundary control It is the most stable template for interview and production-style pointer work.\nFAQ and Notes What if k = 1?\nReturn original list.\nWhat if list length is not divisible by k?\nKeep the final incomplete group unchanged.\nCan recursion solve this problem?\nYes, but iterative form is safer for stack depth and usually easier to debug.\nHow is this related to 92?\nLeetCode 25 is repeated interval reversal (92) under fixed-size grouping.\nBest Practices Memorize the pattern: dummy -\u0026gt; find kth -\u0026gt; reverse group -\u0026gt; reconnect -\u0026gt; move groupPrev Log groupPrev, kth, groupNext during debugging Validate with edge sets: k=1, k=2, k=len, k\u0026gt;len Review together with 206 and 92 as a linked-list reversal trilogy S - Summary LeetCode 25 is group-driven interval reversal Dummy node unifies head-boundary handling kth probing is the correctness gate before each reversal In-place rewiring achieves O(n)/O(1) This template generalizes to many advanced list reordering tasks Recommended Follow-up LeetCode 206 — Reverse Linked List LeetCode 92 — Reverse Linked List II LeetCode 24 — Swap Nodes in Pairs LeetCode 143 — Reorder List Conclusion Once \u0026ldquo;k-group scan + local reversal + safe reconnection\u0026rdquo; becomes your default pattern, LeetCode 25 becomes predictable pointer engineering instead of fragile pointer gymnastics.\nReferences https://leetcode.com/problems/reverse-nodes-in-k-group/ https://en.cppreference.com/w/cpp/container/forward_list https://doc.rust-lang.org/book/ch15-01-box.html https://go.dev/doc/effective_go Meta Info Reading time: 14-18 min Tags: Hot100, linked list, group reversal, dummy node SEO keywords: Reverse Nodes in k-Group, LeetCode 25, Hot100 Meta description: O(n)/O(1) in-place k-group linked-list reversal with robust boundary handling. Call To Action (CTA) Do this practice loop now:\nRe-implement 25 from memory without looking Adapt same structure to 24 (pair swap) Compare with 92 to internalize single-interval vs repeated-group control Multi-language Implementations (Python / C / C++ / Go / Rust / JS) from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_k_group(head: Optional[ListNode], k: int) -\u0026gt; Optional[ListNode]: if not head or k \u0026lt;= 1: return head dummy = ListNode(0, head) group_prev = dummy while True: kth = group_prev for _ in range(k): kth = kth.next if not kth: return dummy.next group_next = kth.next prev = group_next cur = group_prev.next while cur != group_next: nxt = cur.next cur.next = prev prev = cur cur = nxt new_group_tail = group_prev.next group_prev.next = prev group_prev = new_group_tail #include \u0026lt;stdlib.h\u0026gt; typedef struct ListNode { int val; struct ListNode *next; } ListNode; ListNode* reverseKGroup(ListNode* head, int k) { if (!head || k \u0026lt;= 1) return head; ListNode dummy; dummy.val = 0; dummy.next = head; ListNode* groupPrev = \u0026amp;dummy; while (1) { ListNode* kth = groupPrev; for (int i = 0; i \u0026lt; k; ++i) { kth = kth-\u0026gt;next; if (!kth) return dummy.next; } ListNode* groupNext = kth-\u0026gt;next; ListNode* prev = groupNext; ListNode* cur = groupPrev-\u0026gt;next; while (cur != groupNext) { ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = nxt; } ListNode* newGroupTail = groupPrev-\u0026gt;next; groupPrev-\u0026gt;next = prev; groupPrev = newGroupTail; } } struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; class Solution { public: ListNode* reverseKGroup(ListNode* head, int k) { if (!head || k \u0026lt;= 1) return head; ListNode dummy(0); dummy.next = head; ListNode* groupPrev = \u0026amp;dummy; while (true) { ListNode* kth = groupPrev; for (int i = 0; i \u0026lt; k; ++i) { kth = kth-\u0026gt;next; if (!kth) return dummy.next; } ListNode* groupNext = kth-\u0026gt;next; ListNode* prev = groupNext; ListNode* cur = groupPrev-\u0026gt;next; while (cur != groupNext) { ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = nxt; } ListNode* newGroupTail = groupPrev-\u0026gt;next; groupPrev-\u0026gt;next = prev; groupPrev = newGroupTail; } } }; package main type ListNode struct { Val int Next *ListNode } func reverseKGroup(head *ListNode, k int) *ListNode { if head == nil || k \u0026lt;= 1 { return head } dummy := \u0026amp;ListNode{Next: head} groupPrev := dummy for { kth := groupPrev for i := 0; i \u0026lt; k; i++ { kth = kth.Next if kth == nil { return dummy.Next } } groupNext := kth.Next prev, cur := groupNext, groupPrev.Next for cur != groupNext { nxt := cur.Next cur.Next = prev prev = cur cur = nxt } newGroupTail := groupPrev.Next groupPrev.Next = prev groupPrev = newGroupTail } } #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] fn new(val: i32) -\u0026gt; Self { ListNode { next: None, val } } } pub fn reverse_k_group(head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, k: i32) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let k = k as usize; if k \u0026lt;= 1 { return head; } let mut dummy = Box::new(ListNode { val: 0, next: head }); let mut group_prev: \u0026amp;mut Box\u0026lt;ListNode\u0026gt; = \u0026amp;mut dummy; loop { let mut check = group_prev.next.as_ref(); for _ in 0..k { match check { Some(node) =\u0026gt; check = node.next.as_ref(), None =\u0026gt; return dummy.next, } } let mut cur = group_prev.next.take(); let mut rev: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; = None; for _ in 0..k { let mut node = cur.unwrap(); cur = node.next.take(); node.next = rev; rev = Some(node); } group_prev.next = rev; for _ in 0..k { group_prev = group_prev.next.as_mut().unwrap(); } group_prev.next = cur; } } function ListNode(val, next = null) { this.val = val; this.next = next; } function reverseKGroup(head, k) { if (!head || k \u0026lt;= 1) return head; const dummy = new ListNode(0, head); let groupPrev = dummy; while (true) { let kth = groupPrev; for (let i = 0; i \u0026lt; k; i += 1) { kth = kth.next; if (!kth) return dummy.next; } const groupNext = kth.next; let prev = groupNext; let cur = groupPrev.next; while (cur !== groupNext) { const nxt = cur.next; cur.next = prev; prev = cur; cur = nxt; } const newGroupTail = groupPrev.next; groupPrev.next = prev; groupPrev = newGroupTail; } } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/25-reverse-nodes-in-k-group/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nLeetCode 25 is the upgrade path from 206 (full reversal) and 92 (interval reversal): split by groups, reverse inside each full group, reconnect safely, and keep the last incomplete group unchanged.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 14-18 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003egroup reversal\u003c/code\u003e, \u003ccode\u003edummy node\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Reverse Nodes in k-Group, group reversal, LeetCode 25, Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: In-place k-group linked-list reversal with dummy-node anchoring and safe reconnection, including pitfalls, complexity, and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the head of a linked list and an integer \u003ccode\u003ek\u003c/code\u003e, reverse nodes in groups of size \u003ccode\u003ek\u003c/code\u003e and return the modified head.\u003cbr\u003e\nIf the number of remaining nodes is less than \u003ccode\u003ek\u003c/code\u003e, keep them in original order.\u003cbr\u003e\nYou must modify pointers, not just node values.\u003c/p\u003e","title":"Hot100: Reverse Nodes in k-Group Group-Wise In-Place ACERS Guide"},{"content":" Subtitle / Summary\nReorder List is a classic pointer choreography problem: find middle, reverse second half, then merge alternately. This guide derives the in-place O(n)/O(1) method from naive ideas and turns it into a reusable Hot100 template.\nReading time: 12-15 min Tags: Hot100, linked list, in-place SEO keywords: Reorder List, split reverse merge, LeetCode 143, O(1) space Meta description: A full ACERS explanation of Reorder List with correctness intuition, boundary handling, engineering mapping, and runnable code in Python/C/C++/Go/Rust/JS. A - Algorithm (Problem and Algorithm) Problem Restatement Given the head of a singly linked list head, reorder it to:\nL0 -\u0026gt; Ln -\u0026gt; L1 -\u0026gt; Ln-1 -\u0026gt; L2 -\u0026gt; Ln-2 -\u0026gt; ...\nConstraints:\nNode values must remain unchanged You can only rewire next pointers Input / Output Name Type Description head ListNode Head of the list return void Reorder in-place (head remains first node) Example 1 input: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 output: 1 -\u0026gt; 4 -\u0026gt; 2 -\u0026gt; 3 Example 2 input: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 output: 1 -\u0026gt; 5 -\u0026gt; 2 -\u0026gt; 4 -\u0026gt; 3 Target Readers Hot100 learners who want a stable linked-list rewire template Developers who can reverse lists but still fail on alternating merge details Engineers preparing for interviews where O(1) extra space is required Background / Motivation At first glance, this looks like simple reordering. In reality, it tests whether you can safely perform three dependent pointer operations in one workflow:\nSplit one list into two valid lists Reverse one half in-place Alternate-merge without cycles or node loss Most bugs come from boundary handling and pointer update order, not from \u0026ldquo;algorithmic complexity\u0026rdquo; itself.\nCore Concepts Target order: L0 -\u0026gt; Ln -\u0026gt; L1 -\u0026gt; Ln-1 -\u0026gt; L2 -\u0026gt; ... In-place constraint: do not allocate a new list Three-phase pipeline: middle split (slow/fast) reverse second half alternating merge Critical safety rule: cut first half tail (slow.next = null) before merge C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the target order itself For:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 the required result is:\n1 -\u0026gt; 5 -\u0026gt; 2 -\u0026gt; 4 -\u0026gt; 3 That is not a sort, and it is not a plain reversal. The pattern is:\ntake one node from the front then one node from the back repeat So we need a way to expose the tail side in forward-traversal order.\nStep 2: Reject the two obvious but weak approaches Array rebuild works:\nstore all nodes in an array pick from left and right ends But it uses O(n) extra space.\nRepeatedly finding the tail in the list itself also works in principle, but becomes O(n^2). So neither matches the intended linked-list solution.\nStep 3: Ask what hidden structure the target order reveals Look again at:\n1 -\u0026gt; 5 -\u0026gt; 2 -\u0026gt; 4 -\u0026gt; 3 This can be seen as:\nleft half in original order: 1 -\u0026gt; 2 -\u0026gt; 3 right half in reverse order: 5 -\u0026gt; 4 then weave them alternately That immediately suggests a three-stage plan:\nsplit at the middle reverse the second half merge the two halves alternately Step 4: Define the split point first Use fast/slow pointers to find the middle:\nslow, fast = head, head while fast.next and fast.next.next: slow = slow.next fast = fast.next.next Then:\nsecond = slow.next slow.next = None Now the list is cleanly split into two parts.\nStep 5: Reverse the second half once We now want the tail side to become forward-readable:\nprev = None cur = second while cur: nxt = cur.next cur.next = prev prev = cur cur = nxt second = prev After this, second starts at the former tail.\nStep 6: Weave the two lists in alternating order Now the problem becomes:\none list gives L0 -\u0026gt; L1 -\u0026gt; L2 ... the other gives Rn -\u0026gt; Rn-1 ... So one merge round is:\nfirst.next = second second.next = n1 then advance both lists. This is not a value merge; it is an alternating pointer weave.\nStep 7: Walk one trace slowly Start:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 Split:\nfirst: 1 -\u0026gt; 2 -\u0026gt; 3 second: 4 -\u0026gt; 5 Reverse second:\nsecond: 5 -\u0026gt; 4 Weave:\n1 -\u0026gt; 5 -\u0026gt; 2 -\u0026gt; 4 -\u0026gt; 3 That is exactly the required order.\nStep 8: Reduce the method to one sentence LeetCode 143 is \u0026ldquo;split the list, reverse the right half, then weave left and right alternately.\u0026rdquo;\nAssemble the Full Code class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reorder_list(head): if head is None or head.next is None: return # 1) find middle slow, fast = head, head while fast.next and fast.next.next: slow = slow.next fast = fast.next.next # 2) split and reverse second half second = slow.next slow.next = None prev = None cur = second while cur: nxt = cur.next cur.next = prev prev = cur cur = nxt second = prev # 3) merge two lists alternately first = head while second: n1 = first.next n2 = second.next first.next = second second.next = n1 first = n1 if n1 else second second = n2 def from_list(arr): dummy = ListNode() tail = dummy for x in arr: tail.next = ListNode(x) tail = tail.next return dummy.next def to_list(head): ans = [] while head: ans.append(head.val) head = head.next return ans if __name__ == \u0026#34;__main__\u0026#34;: h = from_list([1, 2, 3, 4, 5]) reorder_list(h) print(to_list(h)) # [1, 5, 2, 4, 3] Reference Answer class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reorder_list(head): if head is None or head.next is None: return slow, fast = head, head while fast.next and fast.next.next: slow = slow.next fast = fast.next.next second = slow.next slow.next = None prev = None cur = second while cur: nxt = cur.next cur.next = prev prev = cur cur = nxt second = prev first = head while second: n1 = first.next n2 = second.next first.next = second second.next = n1 first = n1 if n1 else second second = n2 Method category Linked-list pointer manipulation Two-pointer middle search In-place reversal + zipper merge Phase invariants Split phase: after cut, left and right are independent lists Reverse phase: prev is always head of reversed prefix Merge phase: each iteration appends exactly one node from right into left chain Why the order is correct Left half: L0, L1, L2, ... Reversed right half: Ln, Ln-1, ... Alternating merge produces exactly: L0, Ln, L1, Ln-1, ... No node is duplicated because each node moves from one source list once.\nPractice Guide / Steps Handle trivial lists (0/1/2 nodes): already ordered Use slow/fast to find middle Let second = slow.next, then slow.next = null Reverse second Merge first and second alternately Runnable Python example (reorder_list.py):\nclass ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reorder_list(head): if head is None or head.next is None: return # 1) find middle slow, fast = head, head while fast.next and fast.next.next: slow = slow.next fast = fast.next.next # 2) split and reverse second half second = slow.next slow.next = None prev = None cur = second while cur: nxt = cur.next cur.next = prev prev = cur cur = nxt second = prev # 3) merge two lists alternately first = head while second: n1 = first.next n2 = second.next first.next = second second.next = n1 first = n1 if n1 else second second = n2 def from_list(arr): dummy = ListNode() tail = dummy for x in arr: tail.next = ListNode(x) tail = tail.next return dummy.next def to_list(head): ans = [] while head: ans.append(head.val) head = head.next return ans if __name__ == \u0026#34;__main__\u0026#34;: h = from_list([1, 2, 3, 4, 5]) reorder_list(h) print(to_list(h)) # [1, 5, 2, 4, 3] Explanation / Why This Works The whole method depends on one strict ordering:\nfind split point cut list reverse right half weave If you skip step 2 (cut), merge often creates cycles because old links remain active.\nIf merge updates pointers in wrong order, nodes are lost. Always save both n1 and n2 before rewiring.\nE - Engineering (Real-world Scenarios) Scenario 1: Feed interleaving by freshness and baseline priority (Python) Background: a timeline combines old baseline-ranked items and newest items. Why it fits: alternating merge after reversing one side approximates \u0026ldquo;front-back interleaving\u0026rdquo; cheaply.\ndef interleave_ids(ids): left = ids[: (len(ids) + 1) // 2] right = list(reversed(ids[(len(ids) + 1) // 2 :])) out = [] i = j = 0 while i \u0026lt; len(left) or j \u0026lt; len(right): if i \u0026lt; len(left): out.append(left[i]); i += 1 if j \u0026lt; len(right): out.append(right[j]); j += 1 return out print(interleave_ids([1, 2, 3, 4, 5])) # [1, 5, 2, 4, 3] Scenario 2: Linked-list task queue reshaping in backend service (Go) Background: a queue represented as linked nodes needs deterministic reshaping without allocating new nodes. Why it fits: split-reverse-merge is O(1) extra memory and predictable.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Val int Next *Node } func build(a []int) *Node { dummy := \u0026amp;Node{} p := dummy for _, x := range a { p.Next = \u0026amp;Node{Val: x} p = p.Next } return dummy.Next } func toSlice(h *Node) []int { ans := []int{} for h != nil { ans = append(ans, h.Val) h = h.Next } return ans } func main() { head := build([]int{1, 2, 3, 4}) // production code would call reorderList(head) fmt.Println(toSlice(head)) } Scenario 3: Frontend card stream alternating recent and legacy blocks (JavaScript) Background: a client-side list needs quick visual alternation for A/B display experiments. Why it fits: same structural pattern can be reused on arrays.\nfunction reorderArray(arr) { const left = arr.slice(0, Math.ceil(arr.length / 2)); const right = arr.slice(Math.ceil(arr.length / 2)).reverse(); const out = []; let i = 0; let j = 0; while (i \u0026lt; left.length || j \u0026lt; right.length) { if (i \u0026lt; left.length) out.push(left[i++]); if (j \u0026lt; right.length) out.push(right[j++]); } return out; } console.log(reorderArray([1, 2, 3, 4, 5])); // [1, 5, 2, 4, 3] R - Reflection (Complexity, Alternatives, Pitfalls) Complexity Time: O(n) Extra space: O(1) Alternatives and tradeoffs Method Time Extra Space Notes Array rebuild O(n) O(n) Easy but violates strict in-place goal Repeated tail extraction O(n^2) O(1) Too slow Split + reverse + merge O(n) O(1) Best practical template Common mistakes Forgetting to cut at middle (slow.next = null) causing cycles Wrong middle condition, especially for even length Merge order bug: rewiring before saving next pointers Not handling short lists (null or single node) Why this is the optimal practical method It matches constraints exactly:\nin-place linear time no extra container And each sub-step is reusable across multiple linked-list problems.\nFAQ and Notes Why is this not a palindrome-like compare problem?\nBecause we must physically reorder links, not just compare values.\nCan recursion solve it cleanly?\nYes in theory, but stack usage becomes O(n) and implementation is trickier.\nDo node values matter?\nNo. This is pointer topology, not value sorting.\nBest Practices Treat split/reverse/merge as three isolated templates, then compose Write and reuse helper functions in production code Validate with odd/even lengths and minimal lists Use pointer diagrams when debugging merge order S - Summary Reorder List is solved by split -\u0026gt; reverse -\u0026gt; alternating merge The key optimization is recognizing \u0026ldquo;second half reversed\u0026rdquo; as required shape Correctness depends on strict pointer update order and middle cut This template is foundational for many advanced linked-list problems Further Reading LeetCode 143. Reorder List LeetCode 206. Reverse Linked List LeetCode 234. Palindrome Linked List LeetCode 25. Reverse Nodes in k-Group Conclusion If you can implement this problem without pointer bugs under interview pressure, your linked-list manipulation skill is already at a solid intermediate level. The same split/reverse/merge workflow appears repeatedly in production-grade list transformations.\nReferences https://leetcode.com/problems/reorder-list/ https://en.cppreference.com/w/cpp/container/forward_list https://doc.rust-lang.org/std/option/ Meta Info Reading time: 12-15 min Tags: Hot100, linked list, in-place, two pointers SEO keywords: Reorder List, LeetCode 143, split reverse merge, O(1) space Meta description: In-place O(n)/O(1) linked-list reordering with derivation, pitfalls, and multi-language implementations. CTA Try coding this from scratch in 15 minutes without looking at notes. Then extend it to: reverse k-group and palindrome linked list to lock in the pointer templates.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reorder_list(head): if head is None or head.next is None: return slow, fast = head, head while fast.next and fast.next.next: slow = slow.next fast = fast.next.next second = slow.next slow.next = None prev = None cur = second while cur: nxt = cur.next cur.next = prev prev = cur cur = nxt second = prev first = head while second: n1 = first.next n2 = second.next first.next = second second.next = n1 first = n1 if n1 else second second = n2 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; typedef struct ListNode { int val; struct ListNode* next; } ListNode; void reorderList(ListNode* head) { if (!head || !head-\u0026gt;next) return; ListNode *slow = head, *fast = head; while (fast-\u0026gt;next \u0026amp;\u0026amp; fast-\u0026gt;next-\u0026gt;next) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; } ListNode* second = slow-\u0026gt;next; slow-\u0026gt;next = NULL; ListNode *prev = NULL, *cur = second; while (cur) { ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = nxt; } second = prev; ListNode* first = head; while (second) { ListNode* n1 = first-\u0026gt;next; ListNode* n2 = second-\u0026gt;next; first-\u0026gt;next = second; second-\u0026gt;next = n1; first = n1 ? n1 : second; second = n2; } } #include \u0026lt;iostream\u0026gt; struct ListNode { int val; ListNode* next; ListNode(int v) : val(v), next(nullptr) {} }; class Solution { public: void reorderList(ListNode* head) { if (!head || !head-\u0026gt;next) return; ListNode *slow = head, *fast = head; while (fast-\u0026gt;next \u0026amp;\u0026amp; fast-\u0026gt;next-\u0026gt;next) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; } ListNode* second = slow-\u0026gt;next; slow-\u0026gt;next = nullptr; ListNode* prev = nullptr; while (second) { ListNode* nxt = second-\u0026gt;next; second-\u0026gt;next = prev; prev = second; second = nxt; } second = prev; ListNode* first = head; while (second) { ListNode* n1 = first-\u0026gt;next; ListNode* n2 = second-\u0026gt;next; first-\u0026gt;next = second; second-\u0026gt;next = n1; first = n1 ? n1 : second; second = n2; } } }; package main type ListNode struct { Val int Next *ListNode } func reorderList(head *ListNode) { if head == nil || head.Next == nil { return } slow, fast := head, head for fast.Next != nil \u0026amp;\u0026amp; fast.Next.Next != nil { slow = slow.Next fast = fast.Next.Next } second := slow.Next slow.Next = nil var prev *ListNode cur := second for cur != nil { nxt := cur.Next cur.Next = prev prev = cur cur = nxt } second = prev first := head for second != nil { n1 := first.Next n2 := second.Next first.Next = second second.Next = n1 if n1 != nil { first = n1 } else { first = second } second = n2 } } use std::collections::VecDeque; #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] fn new(val: i32) -\u0026gt; Self { ListNode { val, next: None } } } // Safe Rust variant: uses O(n) extra deque to avoid unsafe pointer rewiring. pub fn reorder_list(head: \u0026amp;mut Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;) { let mut cur = head.take(); let mut dq: VecDeque\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; = VecDeque::new(); while let Some(mut node) = cur { cur = node.next.take(); dq.push_back(node); } let mut reordered: Vec\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; = Vec::with_capacity(dq.len()); let mut pick_front = true; while !dq.is_empty() { if pick_front { reordered.push(dq.pop_front().unwrap()); } else { reordered.push(dq.pop_back().unwrap()); } pick_front = !pick_front; } let mut new_head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; = None; for mut node in reordered.into_iter().rev() { node.next = new_head; new_head = Some(node); } *head = new_head; } function ListNode(val, next = null) { this.val = val; this.next = next; } function reorderList(head) { if (!head || !head.next) return; let slow = head; let fast = head; while (fast.next \u0026amp;\u0026amp; fast.next.next) { slow = slow.next; fast = fast.next.next; } let second = slow.next; slow.next = null; let prev = null; while (second) { const nxt = second.next; second.next = prev; prev = second; second = nxt; } second = prev; let first = head; while (second) { const n1 = first.next; const n2 = second.next; first.next = second; second.next = n1; first = n1 || second; second = n2; } } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/143-reorder-list/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nReorder List is a classic pointer choreography problem: find middle, reverse second half, then merge alternately. This guide derives the in-place O(n)/O(1) method from naive ideas and turns it into a reusable Hot100 template.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003ein-place\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Reorder List, split reverse merge, LeetCode 143, O(1) space\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: A full ACERS explanation of Reorder List with correctness intuition, boundary handling, engineering mapping, and runnable code in Python/C/C++/Go/Rust/JS.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the head of a singly linked list \u003ccode\u003ehead\u003c/code\u003e, reorder it to:\u003c/p\u003e","title":"Hot100: Reorder List In-Place Split-Reverse-Merge ACERS Guide"},{"content":" Subtitle / Summary\nReverse Linked List II is not about full-list reversal; it is about reversing a strict middle interval while preserving both outer connections. This ACERS guide explains the dummy-node anchor, head-insertion loop, and boundary-safe implementation.\nReading time: 12-15 min Tags: Hot100, linked list, sublist reversal, dummy node SEO keywords: Reverse Linked List II, sublist reversal, dummy node, head insertion, LeetCode 92, Hot100 Meta description: In-place sublist reversal with dummy node + head insertion in O(n)/O(1), with correctness intuition, pitfalls, and runnable multi-language code. A - Algorithm (Problem and Algorithm) Problem Restatement Given the head of a singly linked list and two integers left and right (1 \u0026lt;= left \u0026lt;= right \u0026lt;= n), reverse the nodes from position left to right, and return the new head.\nInput / Output Name Type Description head ListNode head of the singly linked list left int left boundary (1-based) right int right boundary (1-based) return ListNode head after sublist reversal Example 1 input: head = 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5, left = 2, right = 4 output: 1 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 5 Example 2 input: head = 5, left = 1, right = 1 output: 5 Target Readers Hot100 learners who already know 206 and want the interval version Developers who often fail at linked-list boundary handling (left = 1, right = n) Engineers building reusable pointer-rewiring templates Background / Motivation LeetCode 206 reverses the whole list. LeetCode 92 asks for a stricter operation:\nreverse only nodes from position left to right keep prefix and suffix connected correctly This pattern appears in engineering-style chain structures:\nreplaying a partial compensation chain in reverse order local reorder in an event sequence in-place transformation without allocating new nodes The hard part is not algorithmic complexity; it is pointer safety and boundary consistency.\nCore Concepts Dummy node: unifies the left = 1 case with all other cases Anchor predecessor prev: ends at node left - 1 (or dummy) Current tail cur: starts at prev.next and remains tail of reversed block during loop Head insertion: repeatedly detach cur.next and insert it right after prev C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Shrink the task to the smallest interval-reversal example Take:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 left = 2, right = 4 The target is:\n1 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 5 So we are not reversing the whole list. We must preserve:\nthe prefix before left the suffix after right Only the middle block is allowed to change direction.\nStep 2: Ask which node really controls the whole operation The critical node is not left itself. It is the node just before left.\nWhy? Because after reversal we must reconnect the new block head back into the unchanged prefix.\nThat is why we first locate:\nprev = node_before_left cur = prev.next Here:\nprev anchors the interval from outside cur starts as the first node of the interval Step 3: Ask why a dummy node removes boundary chaos If left = 1, there is no real \u0026ldquo;node before left.\u0026rdquo; Without a dummy node, the head case becomes special.\nSo we unify every case with:\ndummy = ListNode(0, head) prev = dummy Then moving prev forward left - 1 steps always lands at the predecessor of the reversing block.\nStep 4: Decide what one local move should do We do not reverse the whole interval in one shot. Instead, each round takes the node after cur and moves it to the front of the interval.\nThat single move is:\nnxt = cur.next cur.next = nxt.next nxt.next = prev.next prev.next = nxt This is called head insertion inside the target interval.\nStep 5: Notice which pointer does not move After one head-insertion round:\nprev still stays before the interval cur still stays at the tail of the already reversed part Only nxt is extracted and inserted after prev. That is why the loop remains stable instead of losing track of the sublist.\nStep 6: Define when the interval is fully reversed The interval length is:\nright - left + 1 The first node cur is already in place as the tail of the future reversed block. So we only need:\nright - left head-insertion rounds.\nStep 7: Walk one trace slowly Still using:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 left = 2, right = 4 Start:\nprev = 1 cur = 2 Round 1: move 3 after 1\n1 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 4 -\u0026gt; 5 Round 2: move 4 after 1\n1 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 5 Done.\nStep 8: Reduce the method to one invariant prev always stays before the interval, and cur always stays at the tail of the already reversed part.\nAssemble the Full Code from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_between(head: Optional[ListNode], left: int, right: int) -\u0026gt; Optional[ListNode]: if not head or left == right: return head dummy = ListNode(0, head) prev = dummy for _ in range(left - 1): prev = prev.next cur = prev.next for _ in range(right - left): nxt = cur.next cur.next = nxt.next nxt.next = prev.next prev.next = nxt return dummy.next Reference Answer from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_between(head: Optional[ListNode], left: int, right: int) -\u0026gt; Optional[ListNode]: if not head or left == right: return head dummy = ListNode(0, head) prev = dummy for _ in range(left - 1): prev = prev.next cur = prev.next for _ in range(right - left): nxt = cur.next cur.next = nxt.next nxt.next = prev.next prev.next = nxt return dummy.next Method Category In-place linked-list rewiring Sublist transformation Head-insertion loop Invariant (the correctness handle) After each iteration i (0 \u0026lt;= i \u0026lt;= right - left):\nprev still points to the predecessor of the reversing block prev.next is the head of the already reversed prefix of target block cur remains the tail of the reversed part and head of remaining unreversed part At loop end:\nsublist is reversed prefix and suffix are still connected Pointer Trace (left=2, right=4) start: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 ^ cur (prev=1) round 1 (move 3 after 1): 1 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 4 -\u0026gt; 5 ^ cur round 2 (move 4 after 1): 1 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 5 ^ cur Practical Guide / Steps Create dummy, set dummy.next = head Move prev forward left - 1 steps Set cur = prev.next Run right - left rounds of head insertion Return dummy.next Runnable Example (Python) from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_between(head: Optional[ListNode], left: int, right: int) -\u0026gt; Optional[ListNode]: if not head or left == right: return head dummy = ListNode(0, head) prev = dummy for _ in range(left - 1): prev = prev.next cur = prev.next for _ in range(right - left): nxt = cur.next cur.next = nxt.next nxt.next = prev.next prev.next = nxt return dummy.next def build(nums): dummy = ListNode() tail = dummy for x in nums: tail.next = ListNode(x) tail = tail.next return dummy.next def to_list(head): out = [] while head: out.append(head.val) head = head.next return out if __name__ == \u0026#34;__main__\u0026#34;: h = build([1, 2, 3, 4, 5]) h = reverse_between(h, 2, 4) print(to_list(h)) # [1, 4, 3, 2, 5] Explanation (Why This Works) Treat prev as a fixed anchor before the target interval. Each loop extracts one node right after cur and places it right after prev. That operation grows reversed prefix at the front while keeping the rest linked.\nBenefits:\nNo segment split + re-join ceremony O(1) extra memory Uniform behavior on left = 1 due to dummy node E - Engineering (Real-world Scenarios) Scenario 1: Partial compensation-chain replay (Go) Background: reverse execution order for one segment of compensation tasks.\nWhy it fits: local reorder, identity-preserving nodes, constant memory.\npackage main type Node struct { Val int Next *Node } func reverseBetween(head *Node, left, right int) *Node { if head == nil || left == right { return head } dummy := \u0026amp;Node{Next: head} prev := dummy for i := 0; i \u0026lt; left-1; i++ { prev = prev.Next } cur := prev.Next for i := 0; i \u0026lt; right-left; i++ { nxt := cur.Next cur.Next = nxt.Next nxt.Next = prev.Next prev.Next = nxt } return dummy.Next } Scenario 2: Event-chain local rollback window (Python) Background: only a middle event window needs reverse replay.\nWhy it fits: precise interval operation without global rebuild.\n# Reuse reverse_between(head, left, right) from above. Scenario 3: Frontend node-flow local reorder (JavaScript) Background: workflow editor supports \u0026ldquo;reverse selected range\u0026rdquo;.\nWhy it fits: fast in-memory adjustment, predictable pointer operations.\nfunction reverseBetween(head, left, right) { if (!head || left === right) return head; const dummy = { val: 0, next: head }; let prev = dummy; for (let i = 0; i \u0026lt; left - 1; i += 1) prev = prev.next; const cur = prev.next; for (let i = 0; i \u0026lt; right - left; i += 1) { const nxt = cur.next; cur.next = nxt.next; nxt.next = prev.next; prev.next = nxt; } return dummy.next; } R - Reflection Complexity Time: O(n) Space: O(1) Work split:\nfind predecessor in left - 1 steps run right - left rewiring rounds Alternatives and Tradeoffs Approach Time Space Notes array conversion O(n) O(n) easy but not in-place cut/reverse/reconnect O(n) O(1) valid, more connection points dummy + head insertion O(n) O(1) concise, boundary-safe, reusable Common Mistakes skipping dummy node and breaking left=1 moving prev wrong number of steps wrong pointer update order causing chain loss comparing values instead of node references in linked-list logic Why this is the practical template single anchor (prev) single loop (right-left rounds) single return (dummy.next) Fewer branches, fewer failure points.\nFAQ and Notes What if left == right?\nReturn head directly.\nDo we need to validate right bounds?\nLeetCode guarantees valid input; production code should still validate.\nCan recursion solve this elegantly?\nYes, but recursion adds stack risk and usually increases boundary complexity.\nHow is this related to 206?\n206 is whole-list reversal; 92 is interval-scoped pointer rewiring built on the same reversal mindset.\nBest Practices Memorize the 4-line head-insertion block Always start from dummy Dry-run with a 5-node list before coding Test 4 boundary cases: left = 1 right = n left = right n = 1 S - Summary LeetCode 92 is an interval rewiring problem, not a value swap problem Dummy node removes head-special branching Head insertion gives O(1) extra-space reversal Invariants are the fastest way to reason about correctness This template transfers directly to advanced list reorder problems Recommended Follow-up LeetCode 206 — Reverse Linked List LeetCode 25 — Reverse Nodes in k-Group LeetCode 24 — Swap Nodes in Pairs LeetCode 143 — Reorder List Conclusion Once dummy + predecessor + head insertion becomes muscle memory, sublist reversal stops being pointer chaos and becomes predictable engineering work.\nReferences https://leetcode.com/problems/reverse-linked-list-ii/ https://en.cppreference.com/w/cpp/container/forward_list https://doc.rust-lang.org/book/ch15-01-box.html https://go.dev/doc/effective_go Meta Info Reading time: 12-15 min Tags: Hot100, linked list, sublist reversal, dummy node SEO keywords: Reverse Linked List II, sublist reversal, head insertion, LeetCode 92 Meta description: O(n)/O(1) in-place sublist reversal with dummy node and head insertion. Call To Action (CTA) Do two drills now:\nReimplement 92 from memory using the 4-line insertion block Move to 25 (k-group reversal) and compare the control flow Multi-language Implementations (Python / C / C++ / Go / Rust / JS) from typing import Optional class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_between(head: Optional[ListNode], left: int, right: int) -\u0026gt; Optional[ListNode]: if not head or left == right: return head dummy = ListNode(0, head) prev = dummy for _ in range(left - 1): prev = prev.next cur = prev.next for _ in range(right - left): nxt = cur.next cur.next = nxt.next nxt.next = prev.next prev.next = nxt return dummy.next #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; typedef struct ListNode { int val; struct ListNode *next; } ListNode; ListNode* reverseBetween(ListNode* head, int left, int right) { if (!head || left == right) return head; ListNode dummy; dummy.val = 0; dummy.next = head; ListNode* prev = \u0026amp;dummy; for (int i = 0; i \u0026lt; left - 1; ++i) prev = prev-\u0026gt;next; ListNode* cur = prev-\u0026gt;next; for (int i = 0; i \u0026lt; right - left; ++i) { ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = nxt-\u0026gt;next; nxt-\u0026gt;next = prev-\u0026gt;next; prev-\u0026gt;next = nxt; } return dummy.next; } #include \u0026lt;iostream\u0026gt; struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseBetween(ListNode* head, int left, int right) { if (!head || left == right) return head; ListNode dummy(0); dummy.next = head; ListNode* prev = \u0026amp;dummy; for (int i = 0; i \u0026lt; left - 1; ++i) prev = prev-\u0026gt;next; ListNode* cur = prev-\u0026gt;next; for (int i = 0; i \u0026lt; right - left; ++i) { ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = nxt-\u0026gt;next; nxt-\u0026gt;next = prev-\u0026gt;next; prev-\u0026gt;next = nxt; } return dummy.next; } package main type ListNode struct { Val int Next *ListNode } func reverseBetween(head *ListNode, left int, right int) *ListNode { if head == nil || left == right { return head } dummy := \u0026amp;ListNode{Next: head} prev := dummy for i := 0; i \u0026lt; left-1; i++ { prev = prev.Next } cur := prev.Next for i := 0; i \u0026lt; right-left; i++ { nxt := cur.Next cur.Next = nxt.Next nxt.Next = prev.Next prev.Next = nxt } return dummy.Next } #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] fn new(val: i32) -\u0026gt; Self { ListNode { next: None, val } } } pub fn reverse_between(head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, left: i32, right: i32) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { if left == right { return head; } let mut vals = Vec::new(); let mut cursor = head.as_ref(); while let Some(node) = cursor { vals.push(node.val); cursor = node.next.as_ref(); } let l = (left - 1) as usize; let r = (right - 1) as usize; vals[l..=r].reverse(); let mut dummy = Box::new(ListNode::new(0)); let mut tail = \u0026amp;mut dummy; for v in vals { tail.next = Some(Box::new(ListNode::new(v))); tail = tail.next.as_mut().unwrap(); } dummy.next } function ListNode(val, next = null) { this.val = val; this.next = next; } function reverseBetween(head, left, right) { if (!head || left === right) return head; const dummy = new ListNode(0, head); let prev = dummy; for (let i = 0; i \u0026lt; left - 1; i += 1) prev = prev.next; const cur = prev.next; for (let i = 0; i \u0026lt; right - left; i += 1) { const nxt = cur.next; cur.next = nxt.next; nxt.next = prev.next; prev.next = nxt; } return dummy.next; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/92-reverse-linked-list-ii/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nReverse Linked List II is not about full-list reversal; it is about reversing a strict middle interval while preserving both outer connections. This ACERS guide explains the dummy-node anchor, head-insertion loop, and boundary-safe implementation.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003esublist reversal\u003c/code\u003e, \u003ccode\u003edummy node\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Reverse Linked List II, sublist reversal, dummy node, head insertion, LeetCode 92, Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: In-place sublist reversal with dummy node + head insertion in O(n)/O(1), with correctness intuition, pitfalls, and runnable multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the head of a singly linked list and two integers \u003ccode\u003eleft\u003c/code\u003e and \u003ccode\u003eright\u003c/code\u003e (\u003ccode\u003e1 \u0026lt;= left \u0026lt;= right \u0026lt;= n\u003c/code\u003e),\nreverse the nodes from position \u003ccode\u003eleft\u003c/code\u003e to \u003ccode\u003eright\u003c/code\u003e, and return the new head.\u003c/p\u003e","title":"Hot100: Reverse Linked List II Dummy Node + Head-Insertion ACERS Guide"},{"content":" Subtitle / Summary\nDetecting a cycle in a linked list is a pointer chasing problem, not a value comparison problem. This ACERS guide explains why Floyd’s fast/slow pointers must meet if a cycle exists, how to avoid null-pointer bugs, and how the same pattern maps to engineering checks.\nReading time: 10-12 min Tags: Hot100, linked list, fast slow pointers, Floyd SEO keywords: Linked List Cycle, Floyd, fast slow pointers, LeetCode 141, Hot100 Meta description: O(n)/O(1) cycle detection in singly linked lists using Floyd fast/slow pointers, with alternatives, common mistakes, and runnable multi-language code. A - Algorithm (Problem and Algorithm) Problem Restatement Given head node head of a singly linked list, determine whether there is a cycle in the list. Return true if a cycle exists, else false.\nInput / Output Name Type Description head ListNode head of singly linked list (can be null) return bool whether cycle exists Example 1 head: 3 -\u0026gt; 2 -\u0026gt; 0 -\u0026gt; -4 ^ | |_____| output: true Example 2 head: 1 -\u0026gt; 2 -\u0026gt; null output: false Target Readers Hot100 learners and interview candidates Developers building reusable linked-list two-pointer templates Engineers who need to detect loops in chain-like structures Background / Motivation Cycle bugs are common in pointer-linked structures:\ntraversal never ends (infinite loop) cleanup/free logic hangs system looks \u0026ldquo;randomly stuck\u0026rdquo; while root cause is structural So we need a detection method that is:\nonline (single pass style) memory-light (no large side structure) robust under large lists Floyd fast/slow pointer detection is the standard solution for this profile.\nCore Concepts Cycle: from some node, following next can eventually return to itself pos in problem statement: only used by test data construction, not a function parameter Fast/slow pointers: slow moves 1 step fast moves 2 steps Node identity vs node value: compare pointer/reference identity, not val C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from a tiny example that can never terminate Take this list:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 ^ | |_________| If you keep following next, you never reach null. So the real question is not about node values at all. It is:\nwill traversal eventually revisit an old node?\nThe most direct solution is a visited set. It works, but it costs O(n) extra space.\nStep 2: Ask whether we really need to remember every node If a cycle exists, the deeper structure is about relative speed:\nslow moves 1 step fast moves 2 steps slow = slow.next fast = fast.next.next That turns the problem into a chase problem instead of a storage problem.\nStep 3: Define when the work is settled There are only two endings:\nfast hits the end of the list, so there is no cycle slow and fast meet, so there is a cycle That immediately gives the safe loop condition:\nwhile fast is not None and fast.next is not None: ... Step 4: Why does meeting prove a cycle? Once both pointers enter the cycle:\nslow advances by 1 each round fast advances by 2 each round So fast gains 1 node per round on slow. Inside a finite cycle, that relative gap keeps changing modulo the cycle length until it becomes zero.\nThat is why this check is enough:\nif slow is fast: return True Step 5: Why is there no false positive on an acyclic list? Because in an acyclic list there is a real tail. The faster pointer reaches:\nfast is None or fast.next is None before any meeting can happen.\nSo Floyd stays clean:\nmeet =\u0026gt; cycle fall off the list =\u0026gt; no cycle Step 6: Walk one concrete trace Using the same cyclic example:\nStart:\nslow = 1 fast = 1 After one round:\nslow = 2 fast = 3 After two rounds:\nslow = 3 fast = 2 After three rounds:\nslow = 4 fast = 4 They meet, so the list must contain a cycle.\nStep 7: Reduce the whole method to one sentence The reusable idea is:\nwith a speed gap, fast either falls off an acyclic list or catches slow inside a cycle.\nAssemble the Full Code from typing import Optional, List class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def has_cycle(head: Optional[ListNode]) -\u0026gt; bool: slow = head fast = head while fast is not None and fast.next is not None: slow = slow.next fast = fast.next.next if slow is fast: return True return False def build(values: List[int], pos: int) -\u0026gt; Optional[ListNode]: if not values: return None nodes = [ListNode(v) for v in values] for i in range(len(nodes) - 1): nodes[i].next = nodes[i + 1] if pos != -1: nodes[-1].next = nodes[pos] return nodes[0] if __name__ == \u0026#34;__main__\u0026#34;: print(has_cycle(build([3, 2, 0, -4], 1))) # True print(has_cycle(build([1, 2], -1))) # False Reference Answer from typing import Optional, List class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def hasCycle(head: Optional[ListNode]) -\u0026gt; bool: slow = head fast = head while fast is not None and fast.next is not None: slow = slow.next fast = fast.next.next if slow is fast: return True return False Method Category Two pointers Floyd cycle detection (tortoise-hare) Online structural integrity check Why meeting is guaranteed (intuition) Let cycle length be L. After both pointers are in cycle, each round:\nslow +1 fast +2 Relative movement is +1 modulo L. So relative distance cycles through all residues and eventually becomes 0, meaning they meet.\nSafe loop condition Before fast = fast.next.next, we must ensure:\nfast != null fast.next != null Otherwise null dereference happens.\nPractical Guide / Steps Initialize slow = head, fast = head While fast != null \u0026amp;\u0026amp; fast.next != null: slow = slow.next fast = fast.next.next if slow == fast, return true Return false Runnable Python example (linked_list_cycle.py):\nfrom typing import Optional, List class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def has_cycle(head: Optional[ListNode]) -\u0026gt; bool: slow = head fast = head while fast is not None and fast.next is not None: slow = slow.next fast = fast.next.next if slow is fast: return True return False def build(values: List[int], pos: int) -\u0026gt; Optional[ListNode]: if not values: return None nodes = [ListNode(v) for v in values] for i in range(len(nodes) - 1): nodes[i].next = nodes[i + 1] if pos != -1: nodes[-1].next = nodes[pos] return nodes[0] if __name__ == \u0026#34;__main__\u0026#34;: print(has_cycle(build([3, 2, 0, -4], 1))) # True print(has_cycle(build([1, 2], -1))) # False Explanation / Why This Works The algorithm has two outcomes:\nNo cycle: fast pointer reaches null first =\u0026gt; return false Has cycle: fast and slow eventually meet in cycle =\u0026gt; return true No extra memory is needed because we do not store history. We rely on relative speed and finite cycle length.\nE - Engineering (Real-world Scenarios) Scenario 1: free-list integrity check in memory pools (C) Background: low-level allocators often keep free blocks as a singly linked list.\nWhy it fits: if free list becomes cyclic, allocation/release may hang; Floyd check is O(1) memory.\nint hasCycle(struct Node* head) { struct Node* slow = head; struct Node* fast = head; while (fast \u0026amp;\u0026amp; fast-\u0026gt;next) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; if (slow == fast) return 1; } return 0; } Scenario 2: backend workflow next-pointer validation (Go) Background: lightweight workflow nodes may be chained by Next.\nWhy it fits: config bugs can create loops and stall execution.\nfunc hasCycle(head *Node) bool { slow, fast := head, head for fast != nil \u0026amp;\u0026amp; fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { return true } } return false } Scenario 3: linked object debug check in browser tools (JavaScript) Background: front-end tooling may use chain objects with next links.\nWhy it fits: quickly detect accidental circular links before traversing.\nfunction hasCycle(head) { let slow = head, fast = head; while (fast \u0026amp;\u0026amp; fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; } return false; } R - Reflection Complexity Time: O(n) Space: O(1) (Floyd), versus O(n) for hash-set approach Alternatives and Tradeoffs Method Time Space Tradeoff Hash set visited nodes O(n) O(n) easy but memory-heavy Marker on node O(n) O(1) mutates structure; usually forbidden Floyd fast/slow O(n) O(1) best practical baseline Common Mistakes Compare values instead of node identity Forget null checks before double-step fast move Check slow == fast before first movement (trivial true at start) Assume pos is passed to function Why this is engineering-optimal in most cases It balances runtime, memory, and non-intrusiveness:\nlinear scan constant memory no mutation FAQ and Notes Can this also find cycle entry?\nYes, that is LeetCode 142 (extra phase after first meeting).\nWhat if list is very large?\nStill linear and memory-safe versus visited set growth.\nWhen is hash set preferable?\nIf you also need to record traversal path or list all repeated nodes.\nBest Practices Always use while fast \u0026amp;\u0026amp; fast.next Compare identity (is, ===, pointer equality), not value Keep this template as your default cycle check S - Summary Cycle detection is about pointer identity and relative speed, not values. Floyd detects cycles in O(n) time with O(1) extra space. Null-check ordering is critical for safety. This template maps directly to chain integrity checks in real systems. Recommended Further Reading LeetCode 141. Linked List Cycle LeetCode 142. Linked List Cycle II Floyd’s cycle detection variants and proofs Conclusion LeetCode 141 is a foundational two-pointer template. Once internalized, it becomes a reusable structural safety check across many chain-based systems.\nReferences https://leetcode.com/problems/linked-list-cycle/ https://leetcode.com/problems/linked-list-cycle-ii/ https://en.wikipedia.org/wiki/Cycle_detection https://en.cppreference.com/w/cpp/container/forward_list Meta Info Reading time: 10-12 min Tags: Hot100, linked list, Floyd, fast slow pointers, LeetCode 141 SEO keywords: Linked List Cycle, Floyd, fast slow pointers, O(1), LeetCode 141 Meta description: Floyd fast/slow pointers detect linked-list cycle in O(n)/O(1), with proof intuition and multi-language implementations. Call To Action (CTA) After this article, solve 142 immediately:\ndetect cycle (this problem) find cycle entry (next step) Treat them as one template family.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) from typing import Optional, List class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def hasCycle(head: Optional[ListNode]) -\u0026gt; bool: slow = head fast = head while fast is not None and fast.next is not None: slow = slow.next fast = fast.next.next if slow is fast: return True return False struct ListNode { int val; struct ListNode* next; }; int hasCycle(struct ListNode* head) { struct ListNode* slow = head; struct ListNode* fast = head; while (fast \u0026amp;\u0026amp; fast-\u0026gt;next) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; if (slow == fast) return 1; } return 0; } struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; bool hasCycle(ListNode* head) { ListNode* slow = head; ListNode* fast = head; while (fast \u0026amp;\u0026amp; fast-\u0026gt;next) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; if (slow == fast) return true; } return false; } package main type ListNode struct { Val int Next *ListNode } func hasCycle(head *ListNode) bool { slow, fast := head, head for fast != nil \u0026amp;\u0026amp; fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { return true } } return false } use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { val: i32, next: Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;\u0026gt;, } fn next(node: \u0026amp;Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;\u0026gt;) -\u0026gt; Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;\u0026gt; { node.as_ref().and_then(|rc| rc.borrow().next.clone()) } fn has_cycle(head: Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;Node\u0026gt;\u0026gt;\u0026gt;) -\u0026gt; bool { let mut slow = head.clone(); let mut fast = head; loop { slow = next(\u0026amp;slow); fast = next(\u0026amp;fast); if fast.is_none() || slow.is_none() { return false; } fast = next(\u0026amp;fast); if fast.is_none() { return false; } if let (Some(ref s), Some(ref f)) = (\u0026amp;slow, \u0026amp;fast) { if Rc::ptr_eq(s, f) { return true; } } else { return false; } } } function hasCycle(head) { let slow = head, fast = head; while (fast \u0026amp;\u0026amp; fast.next) { slow = slow.next; fast = fast.next.next; if (slow === fast) return true; } return false; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/141-linked-list-cycle/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nDetecting a cycle in a linked list is a pointer chasing problem, not a value comparison problem. This ACERS guide explains why Floyd’s fast/slow pointers must meet if a cycle exists, how to avoid null-pointer bugs, and how the same pattern maps to engineering checks.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003efast slow pointers\u003c/code\u003e, \u003ccode\u003eFloyd\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Linked List Cycle, Floyd, fast slow pointers, LeetCode 141, Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: O(n)/O(1) cycle detection in singly linked lists using Floyd fast/slow pointers, with alternatives, common mistakes, and runnable multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven head node \u003ccode\u003ehead\u003c/code\u003e of a singly linked list, determine whether there is a cycle in the list.\nReturn \u003ccode\u003etrue\u003c/code\u003e if a cycle exists, else \u003ccode\u003efalse\u003c/code\u003e.\u003c/p\u003e","title":"Hot100: Linked List Cycle Floyd Fast/Slow Pointer ACERS Guide"},{"content":" Subtitle / Summary\nThe core of palindrome validation is symmetric comparison, but a singly linked list cannot move backward. The most stable engineering template is: find middle -\u0026gt; reverse second half in-place -\u0026gt; compare -\u0026gt; reverse back to restore.\nReading time: 10-14 min Tags: Hot100, linked list, fast slow pointers, in-place reverse SEO keywords: Palindrome Linked List, fast slow pointers, reverse second half, O(1) space, LeetCode 234 Meta description: O(n)/O(1) palindrome check for singly linked list with middle detection, second-half reversal, comparison, and full structure restoration. A - Algorithm (Problem and Algorithm) Problem Restatement Given the head of a singly linked list head, return true if it is a palindrome; otherwise return false.\nInput / Output Name Type Description head ListNode head of singly linked list return bool whether list is palindrome Example 1 input: 1 -\u0026gt; 2 -\u0026gt; 2 -\u0026gt; 1 output: true Example 2 input: 1 -\u0026gt; 2 output: false Target Readers Hot100 learners who want to master the \u0026ldquo;middle + reverse\u0026rdquo; linked-list combo Developers who frequently solve palindrome/symmetry interview questions Engineers who care about low extra memory and non-destructive checks Background / Motivation For arrays, palindrome check is easy with two pointers from both ends. For singly linked lists, you can only move forward via next, so symmetric comparison is not direct.\nReal engineering constraints are often similar to this problem:\navoid O(n) extra containers if possible do not permanently mutate the structure keep linear time So we need a template that is:\nO(n) time O(1) extra space restorable (no side effects after checking) Core Concepts Concept Meaning Purpose Palindrome same sequence forward and backward needs symmetric comparison Fast/slow pointers fast moves 2, slow moves 1 find middle in O(n) In-place reverse reverse pointer direction on second half make backward side comparable forward Restore step reverse second half again and reconnect preserve original structure C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the real obstacle, not the word \u0026ldquo;palindrome\u0026rdquo; Take:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 1 To verify a palindrome, we want to compare:\nfirst node with last node second node with second-last node But a singly linked list cannot move backward. That is the whole difficulty.\nStep 2: Acknowledge the direct solution first The simplest correct approach is:\ncopy values into an array compare from both ends This is easy, but it uses O(n) extra space. So the real challenge is:\ncan we make the list itself expose a forward-comparable second half?\nStep 3: Ask what transformation would make symmetric comparison possible If the second half were reversed in place, then both sides could be read forward.\nFor:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 1 after reversing the second half, the comparable fronts become:\nleft forward: 1 -\u0026gt; 2 -\u0026gt; 3 right forward: 1 -\u0026gt; 2 Now the palindrome check becomes an ordinary two-pointer forward scan.\nStep 4: Decide where the second half begins We first need the end of the first half. Fast/slow pointers do exactly that:\nfast = head slow = head while fast.next and fast.next.next: fast = fast.next.next slow = slow.next When the loop ends:\nslow is the middle for odd length slow is the left-middle tail for even length So slow.next is the start of the second half we want to reverse.\nStep 5: Reverse only the second half Now run the standard list reversal on:\nsecond_half_start = reverse_list(first_half_end.next) This does not solve the whole problem yet. It only converts the hidden backward comparison into a visible forward comparison.\nStep 6: Compare, then restore Use two pointers:\np1 from the original head p2 from the reversed second half If every compared pair matches, the list is a palindrome. Then reverse the second half again and reconnect it, so the original structure is preserved.\nThat restoration step matters in real code, not just in interviews.\nStep 7: Walk one trace slowly For:\n1 -\u0026gt; 2 -\u0026gt; 2 -\u0026gt; 1 the first half ends at the first 2. Reverse the second half:\n1 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; 2 ^ reversed start Now compare:\n1 vs 1 2 vs 2 All pairs match, so return True, then restore the suffix.\nStep 8: Reduce the method to one sentence LeetCode 234 is \u0026ldquo;find the midpoint, reverse the second half to make comparison forward-only, compare both halves, then restore.\u0026rdquo;\nAssemble the Full Code from __future__ import annotations class ListNode: def __init__(self, val: int): self.val = val self.next: ListNode | None = None def reverse_list(head: ListNode | None) -\u0026gt; ListNode | None: prev = None cur = head while cur: nxt = cur.next cur.next = prev prev = cur cur = nxt return prev def end_of_first_half(head: ListNode) -\u0026gt; ListNode: fast = head slow = head while fast.next and fast.next.next: fast = fast.next.next slow = slow.next # type: ignore[assignment] return slow def is_palindrome(head: ListNode | None) -\u0026gt; bool: if head is None or head.next is None: return True first_half_end = end_of_first_half(head) second_half_start = reverse_list(first_half_end.next) p1 = head p2 = second_half_start ok = True while ok and p2 is not None: if p1.val != p2.val: ok = False p1 = p1.next # type: ignore[assignment] p2 = p2.next first_half_end.next = reverse_list(second_half_start) # restore return ok Reference Answer from __future__ import annotations class ListNode: def __init__(self, val: int): self.val = val self.next: ListNode | None = None def reverse_list(head: ListNode | None) -\u0026gt; ListNode | None: prev = None cur = head while cur: nxt = cur.next cur.next = prev prev = cur cur = nxt return prev def end_of_first_half(head: ListNode) -\u0026gt; ListNode: fast, slow = head, head while fast.next and fast.next.next: fast = fast.next.next slow = slow.next # type: ignore[assignment] return slow def is_palindrome(head: ListNode | None) -\u0026gt; bool: if head is None or head.next is None: return True first_half_end = end_of_first_half(head) second_half_start = reverse_list(first_half_end.next) p1, p2 = head, second_half_start ok = True while ok and p2: if p1.val != p2.val: ok = False p1 = p1.next # type: ignore[assignment] p2 = p2.next first_half_end.next = reverse_list(second_half_start) return ok Method Category Fast/slow pointer middle finding In-place linked-list reversal Temporary mutation + restoration Stable handling for odd/even lengths A robust implementation uses end_of_first_half:\nodd length: first-half end is exact middle (middle value can be skipped in comparison) even length: first-half end is left-middle Then reverse first_half_end.next, and compare only while second-half pointer is not null. This removes many odd/even branch bugs.\nKey invariant After reversing second half:\np1 starts from head p2 starts from reversed_second_half_head For palindrome lists, p1.val == p2.val for all nodes in second half.\nAfter check:\nreverse second half again reconnect via first_half_end.next So external observers see the original structure.\nPractical Guide / Steps Return true for empty list or single node Find first_half_end by fast/slow pointers Reverse first_half_end.next to get second_half_start Compare head and second_half_start node values Restore: first_half_end.next = reverse(second_half_start) Return comparison result Runnable Python example (palindrome_list.py):\nfrom __future__ import annotations class ListNode: def __init__(self, val: int): self.val = val self.next: ListNode | None = None def reverse_list(head: ListNode | None) -\u0026gt; ListNode | None: prev = None cur = head while cur: nxt = cur.next cur.next = prev prev = cur cur = nxt return prev def end_of_first_half(head: ListNode) -\u0026gt; ListNode: fast = head slow = head while fast.next and fast.next.next: fast = fast.next.next slow = slow.next # type: ignore[assignment] return slow def is_palindrome(head: ListNode | None) -\u0026gt; bool: if head is None or head.next is None: return True first_half_end = end_of_first_half(head) second_half_start = reverse_list(first_half_end.next) p1 = head p2 = second_half_start ok = True while ok and p2 is not None: if p1.val != p2.val: ok = False p1 = p1.next # type: ignore[assignment] p2 = p2.next first_half_end.next = reverse_list(second_half_start) # restore return ok Explanation / Why This Works A singly linked list cannot directly read from tail to head. Reversing the second half transforms the \u0026ldquo;backward side\u0026rdquo; into a forward list. So palindrome checking becomes simple forward pair comparison.\nThe important engineering detail is restoration:\ntemporary mutation is acceptable permanent mutation is usually not By reversing the second half again, we restore exact original topology.\nE - Engineering (Real-world Scenarios) Scenario 1: symmetric event-chain validation (Python) Background: a rule engine stores a session as a linked event chain and needs to detect mirrored behavior patterns.\nWhy it fits: O(1) extra memory check without allocating a full copy.\ndef is_symmetric_chain(head): return is_palindrome(head) Scenario 2: embedded frame-sequence symmetry check (C) Background: in memory-limited systems, sampled frames may be chained via next pointers.\nWhy it fits: avoids O(n) buffer allocation and preserves structure after check.\nstruct ListNode { int val; struct ListNode* next; }; static struct ListNode* reverse(struct ListNode* head) { struct ListNode* prev = 0; struct ListNode* cur = head; while (cur) { struct ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = nxt; } return prev; } Scenario 3: browser operation-history mirror detection (JavaScript) Background: editor actions are represented as a linked list in a demo tool.\nWhy it fits: pointer-based structure allows direct reuse of middle+reverse template.\nfunction reverse(head) { let prev = null; let cur = head; while (cur) { const nxt = cur.next; cur.next = prev; prev = cur; cur = nxt; } return prev; } R - Reflection Complexity Time: O(n) (middle find + reverse + compare + restore are all linear) Space: O(1) Alternatives and Tradeoffs Method Time Extra Space Tradeoff Copy to array O(n) O(n) easy but memory-heavy Stack half values O(n) O(n) less copy than full array, still linear extra space Recursion compare O(n) O(n) stack risk on long lists Reverse second half (current) O(n) O(1) most practical, but needs careful restore Common Mistakes Forget restore step, leaving list mutated Middle handling bug for odd/even length Wrong compare range (comparing beyond second half) Missing cycle assumption in non-LeetCode environments Why this method is practical optimum It achieves linear time with constant extra memory, and can preserve original list by restoration. This is usually the best tradeoff under production-style constraints.\nFAQ and Notes Why compare only while p2 is not null?\nSecond half length is less than or equal to first half length; this covers all mirrored pairs.\nWhat if list has a cycle?\nLeetCode input has no cycle. In production, detect cycle first (Floyd) before this template.\nWill reversal damage original structure?\nTemporarily yes; final reverse+reconnect restores it.\nCan I skip restore in interview?\nDepends on interviewer constraints. In engineering code, restoration is strongly recommended.\nBest Practices Use one stable template: first_half_end + reverse(first_half_end.next) Keep restore step mandatory unless explicitly allowed to mutate Test both odd and even lengths, plus edge cases ([], [x], non-palindrome near center) S - Summary Singly linked lists cannot directly do backward traversal for symmetry checks. Fast/slow pointers locate split point in O(n). Reversing second half converts backward comparison into forward comparison. Restore step preserves original structure and avoids side effects. This template transfers to many list problems using \u0026ldquo;middle + half processing\u0026rdquo;. Recommended Further Reading LeetCode 234. Palindrome Linked List LeetCode 206. Reverse Linked List LeetCode 143. Reorder List LeetCode 876. Middle of the Linked List Conclusion The value of this problem is not only palindrome checking. It is a reusable engineering pattern: locate middle, temporarily transform half, compare, restore.\nReferences https://leetcode.com/problems/palindrome-linked-list/ https://leetcode.com/problems/reverse-linked-list/ https://leetcode.com/problems/middle-of-the-linked-list/ https://en.cppreference.com/w/cpp/container/forward_list Meta Info Reading time: 10-14 min Tags: Hot100, linked list, palindrome, fast slow pointers, in-place reverse SEO keywords: Palindrome Linked List, reverse second half, O(1) space, LeetCode 234, Hot100 Meta description: O(n)/O(1) palindrome check by fast/slow split, reverse second half, compare, and restore. Call To Action (CTA) After this one, solve these in order with the same skill set:\n206 Reverse Linked List 143 Reorder List 92 Reverse Linked List II Treat them as one template family, not isolated questions.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) from __future__ import annotations class ListNode: def __init__(self, val: int): self.val = val self.next: ListNode | None = None def reverse_list(head: ListNode | None) -\u0026gt; ListNode | None: prev = None cur = head while cur: nxt = cur.next cur.next = prev prev = cur cur = nxt return prev def end_of_first_half(head: ListNode) -\u0026gt; ListNode: fast, slow = head, head while fast.next and fast.next.next: fast = fast.next.next slow = slow.next # type: ignore[assignment] return slow def is_palindrome(head: ListNode | None) -\u0026gt; bool: if head is None or head.next is None: return True first_half_end = end_of_first_half(head) second_half_start = reverse_list(first_half_end.next) p1, p2 = head, second_half_start ok = True while ok and p2: if p1.val != p2.val: ok = False p1 = p1.next # type: ignore[assignment] p2 = p2.next first_half_end.next = reverse_list(second_half_start) return ok struct ListNode { int val; struct ListNode *next; }; static struct ListNode* reverse(struct ListNode* head) { struct ListNode* prev = 0; struct ListNode* cur = head; while (cur) { struct ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = nxt; } return prev; } static struct ListNode* endFirstHalf(struct ListNode* head) { struct ListNode* fast = head; struct ListNode* slow = head; while (fast-\u0026gt;next \u0026amp;\u0026amp; fast-\u0026gt;next-\u0026gt;next) { fast = fast-\u0026gt;next-\u0026gt;next; slow = slow-\u0026gt;next; } return slow; } int isPalindrome(struct ListNode* head) { if (!head || !head-\u0026gt;next) return 1; struct ListNode* firstEnd = endFirstHalf(head); struct ListNode* second = reverse(firstEnd-\u0026gt;next); int ok = 1; struct ListNode *p1 = head, *p2 = second; while (ok \u0026amp;\u0026amp; p2) { if (p1-\u0026gt;val != p2-\u0026gt;val) ok = 0; p1 = p1-\u0026gt;next; p2 = p2-\u0026gt;next; } firstEnd-\u0026gt;next = reverse(second); return ok; } struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; static ListNode* reverse(ListNode* head) { ListNode* prev = nullptr; ListNode* cur = head; while (cur) { ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = nxt; } return prev; } static ListNode* endFirstHalf(ListNode* head) { ListNode* fast = head; ListNode* slow = head; while (fast-\u0026gt;next \u0026amp;\u0026amp; fast-\u0026gt;next-\u0026gt;next) { fast = fast-\u0026gt;next-\u0026gt;next; slow = slow-\u0026gt;next; } return slow; } bool isPalindrome(ListNode* head) { if (!head || !head-\u0026gt;next) return true; ListNode* firstEnd = endFirstHalf(head); ListNode* second = reverse(firstEnd-\u0026gt;next); bool ok = true; ListNode *p1 = head, *p2 = second; while (ok \u0026amp;\u0026amp; p2) { if (p1-\u0026gt;val != p2-\u0026gt;val) ok = false; p1 = p1-\u0026gt;next; p2 = p2-\u0026gt;next; } firstEnd-\u0026gt;next = reverse(second); return ok; } package main type ListNode struct { Val int Next *ListNode } func reverse(head *ListNode) *ListNode { var prev *ListNode cur := head for cur != nil { nxt := cur.Next cur.Next = prev prev = cur cur = nxt } return prev } func endFirstHalf(head *ListNode) *ListNode { fast, slow := head, head for fast.Next != nil \u0026amp;\u0026amp; fast.Next.Next != nil { fast = fast.Next.Next slow = slow.Next } return slow } func isPalindrome(head *ListNode) bool { if head == nil || head.Next == nil { return true } firstEnd := endFirstHalf(head) second := reverse(firstEnd.Next) ok := true p1, p2 := head, second for ok \u0026amp;\u0026amp; p2 != nil { if p1.Val != p2.Val { ok = false } p1 = p1.Next p2 = p2.Next } firstEnd.Next = reverse(second) return ok } #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { #[inline] pub fn new(val: i32) -\u0026gt; Self { ListNode { next: None, val } } } pub fn is_palindrome(head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;) -\u0026gt; bool { let mut vals: Vec\u0026lt;i32\u0026gt; = Vec::new(); let mut cur = head.as_ref(); while let Some(node) = cur { vals.push(node.val); cur = node.next.as_ref(); } let mut i = 0usize; let mut j = vals.len().saturating_sub(1); while i \u0026lt; j { if vals[i] != vals[j] { return false; } i += 1; j = j.saturating_sub(1); } true } function reverse(head) { let prev = null; let cur = head; while (cur) { const nxt = cur.next; cur.next = prev; prev = cur; cur = nxt; } return prev; } function endFirstHalf(head) { let fast = head, slow = head; while (fast.next \u0026amp;\u0026amp; fast.next.next) { fast = fast.next.next; slow = slow.next; } return slow; } function isPalindrome(head) { if (!head || !head.next) return true; const firstEnd = endFirstHalf(head); const second = reverse(firstEnd.next); let ok = true; let p1 = head, p2 = second; while (ok \u0026amp;\u0026amp; p2) { if (p1.val !== p2.val) ok = false; p1 = p1.next; p2 = p2.next; } firstEnd.next = reverse(second); return ok; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/234-palindrome-linked-list/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThe core of palindrome validation is symmetric comparison, but a singly linked list cannot move backward. The most stable engineering template is: \u003cstrong\u003efind middle -\u0026gt; reverse second half in-place -\u0026gt; compare -\u0026gt; reverse back to restore\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-14 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003efast slow pointers\u003c/code\u003e, \u003ccode\u003ein-place reverse\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Palindrome Linked List, fast slow pointers, reverse second half, O(1) space, LeetCode 234\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: O(n)/O(1) palindrome check for singly linked list with middle detection, second-half reversal, comparison, and full structure restoration.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the head of a singly linked list \u003ccode\u003ehead\u003c/code\u003e, return \u003ccode\u003etrue\u003c/code\u003e if it is a palindrome; otherwise return \u003ccode\u003efalse\u003c/code\u003e.\u003c/p\u003e","title":"Hot100: Palindrome Linked List Fast/Slow + Reverse Second Half O(1) Space ACERS Guide"},{"content":" Subtitle / Summary\nReverse Linked List is the first serious pointer-rewiring exercise in Hot100. It looks simple, but most bugs come from broken links and wrong operation order. This ACERS guide explains the three-pointer iterative template thoroughly and compares it with recursion.\nReading time: 10-12 min Tags: Hot100, linked list, pointer, iteration SEO keywords: Hot100, Reverse Linked List, three pointers, iterative, recursive, LeetCode 206 Meta description: Three-pointer iterative reversal in O(n)/O(1), with recursive contrast, common pitfalls, engineering mapping, and runnable multi-language implementations. A - Algorithm (Problem and Algorithm) Problem Restatement Given the head of a singly linked list, reverse the list and return the new head.\nInput / Output Name Type Description head ListNode head of singly linked list (can be null) return ListNode new head after reversal Example 1 input: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; null output: 5 -\u0026gt; 4 -\u0026gt; 3 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; null Example 2 input: 1 -\u0026gt; 2 -\u0026gt; null output: 2 -\u0026gt; 1 -\u0026gt; null Target Readers Hot100 learners and interview candidates Developers who often hit null-pointer or broken-chain bugs in list problems Engineers who want stable pointer manipulation patterns in C/C++/Rust/Go Background / Motivation In production code, \u0026ldquo;reverse linked list\u0026rdquo; may not appear as a LeetCode function, but the skill is highly transferable:\nReorder nodes in-place with O(1) extra memory Keep link integrity while changing direction Handle head = null and single-node lists without special-case chaos If this template is truly internalized, many list problems become straightforward: reverse sublist, reverse k-group, palindrome list, and so on.\nCore Concepts Singly linked list: each node has one next pointer Broken-link risk: if you overwrite cur.next before saving old next, you lose the remaining chain Three pointers (prev, cur, next): save successor, reverse link, then advance Loop invariant: prev is always the head of the already reversed part cur is always the head of the not-yet-processed part C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the smallest useful example Take:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; null The target is not to build a new list with reversed values. It is to turn the links into:\n3 -\u0026gt; 2 -\u0026gt; 1 -\u0026gt; null So this problem is really about pointer direction, not value order.\nStep 2: Decide what the partial answer must remember At any point we need to know two list fronts:\nthe head of the already reversed prefix the head of the not-yet-processed suffix That is exactly:\nprev = None cur = head Here:\nprev means \u0026ldquo;head of the reversed part\u0026rdquo; cur means \u0026ldquo;head of the remaining part\u0026rdquo; Step 3: Ask why one more variable is necessary Before reversing this edge:\ncur.next = prev we must save the original successor, or the rest of the list becomes unreachable.\nSo every round begins with:\nnxt = cur.next This is the most important safety move in the whole problem.\nStep 4: Define one loop iteration precisely Each iteration does only one job:\nmove cur from the untouched suffix to the front of the reversed prefix.\nThat forces the order:\nnxt = cur.next cur.next = prev prev = cur cur = nxt If you reorder these lines carelessly, you usually break the chain.\nStep 5: Define when the work is complete When cur is None, the untouched suffix is empty. So every node is already inside the reversed prefix, and the new head is:\nreturn prev Step 6: Walk one branch slowly Still using:\n1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; null Start:\nprev = null cur = 1 After one round:\n1.next = null prev = 1 cur = 2 After two rounds:\n2.next = 1 prev = 2 cur = 3 After three rounds:\n3.next = 2 prev = 3 cur = null So the answer is 3.\nStep 7: Reduce the whole template to one invariant The reusable statement is:\nprev always points to the reversed prefix, and cur always points to the untouched suffix.\nOnce that invariant feels stable, 206, 92, 25, and 234 all become easier.\nAssemble the Full Code class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_list(head): prev = None cur = head while cur is not None: nxt = cur.next cur.next = prev prev = cur cur = nxt return prev def from_list(arr): dummy = ListNode() tail = dummy for x in arr: tail.next = ListNode(x) tail = tail.next return dummy.next def to_list(head): ans = [] while head: ans.append(head.val) head = head.next return ans if __name__ == \u0026#34;__main__\u0026#34;: h = from_list([1, 2, 3, 4, 5]) print(to_list(reverse_list(h))) Reference Answer from typing import Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def reverseList(head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: prev = None cur = head while cur is not None: nxt = cur.next cur.next = prev prev = cur cur = nxt return prev Method Category In-place linked-list manipulation Iterative simulation Recursion as equivalent reference solution Loop Invariant (why it is correct) At each loop start:\nprev points to a valid reversed list cur points to the first node not yet reversed Original nodes are partitioned into: reversed prefix untouched suffix Each iteration moves exactly one node from untouched suffix to reversed prefix. When cur == null, all nodes are in reversed prefix, and prev is the new head.\nRecursive counterpart Recursive idea:\nReverse head.next onward and get new_head Let head.next.next = head Set head.next = null to cut old forward link Readable, but stack usage is O(n).\nPractical Guide / Steps Initialize prev = null, cur = head While cur != null: next = cur.next cur.next = prev prev = cur cur = next Return prev Runnable Python example (reverse_list.py):\nclass ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next def reverse_list(head): prev = None cur = head while cur is not None: nxt = cur.next cur.next = prev prev = cur cur = nxt return prev def from_list(arr): dummy = ListNode() tail = dummy for x in arr: tail.next = ListNode(x) tail = tail.next return dummy.next def to_list(head): ans = [] while head: ans.append(head.val) head = head.next return ans if __name__ == \u0026#34;__main__\u0026#34;: h = from_list([1, 2, 3, 4, 5]) print(to_list(reverse_list(h))) Explanation / Why This Works The order of operations is the whole point:\nSave next first (avoid losing remaining chain) Reverse current edge (cur.next = prev) Advance both pointers If you swap step 1 and 2, the rest of list may become unreachable. That is the most common bug in this problem.\nE - Engineering (Real-world Scenarios) Scenario 1: free-list reorder in memory-oriented systems (C) Background: some allocators keep free blocks in a singly linked free-list.\nWhy it fits: reversing list order is an in-place strategy to alter reuse order without allocating memory.\n#include \u0026lt;stdio.h\u0026gt; typedef struct Node { int id; struct Node* next; } Node; Node* reverse(Node* head) { Node* prev = NULL; Node* cur = head; while (cur) { Node* nxt = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = nxt; } return prev; } int main(void) { Node c = {3, NULL}; Node b = {2, \u0026amp;c}; Node a = {1, \u0026amp;b}; Node* head = reverse(\u0026amp;a); for (Node* p = head; p; p = p-\u0026gt;next) printf(\u0026#34;%d \u0026#34;, p-\u0026gt;id); printf(\u0026#34;\\n\u0026#34;); return 0; } Scenario 2: server-side operation stack replay direction switch (Go) Background: a lightweight task chain is stored as a singly linked stack.\nWhy it fits: reversing in-place switches replay direction without extra containers.\npackage main import \u0026#34;fmt\u0026#34; type Node struct { Val int Next *Node } func reverse(head *Node) *Node { var prev *Node cur := head for cur != nil { nxt := cur.Next cur.Next = prev prev = cur cur = nxt } return prev } func main() { head := \u0026amp;Node{1, \u0026amp;Node{2, \u0026amp;Node{3, nil}}} head = reverse(head) for p := head; p != nil; p = p.Next { fmt.Print(p.Val, \u0026#34; \u0026#34;) } fmt.Println() } Scenario 3: pointer-animation teaching in browser (JavaScript) Background: in algorithm visualization, object references simulate list nodes.\nWhy it fits: the three-pointer state transition is easy to animate frame-by-frame.\nfunction Node(val, next = null) { this.val = val; this.next = next; } function reverse(head) { let prev = null; let cur = head; while (cur) { const nxt = cur.next; cur.next = prev; prev = cur; cur = nxt; } return prev; } let head = new Node(1, new Node(2, new Node(3))); head = reverse(head); const out = []; for (let p = head; p; p = p.next) out.push(p.val); console.log(out.join(\u0026#34; \u0026#34;)); R - Reflection Complexity Iterative: Time: O(n) Space: O(1) Recursive: Time: O(n) Space: O(n) (call stack) Alternatives and Tradeoffs Method Time Extra Space Tradeoff Rebuild new list O(n) O(n) Easy but not in-place Recursive reversal O(n) O(n) Elegant, but stack-risk on long lists Three-pointer iterative O(n) O(1) Best engineering baseline Common Mistakes Rewire before saving next (break chain) Forget advancing cur (infinite loop) Recursive version forgetting head.next = null (cycle risk) Reversing values instead of links (misses structural requirement) Why iterative is most practical Stack-safe for very long lists Local, inspectable pointer transitions Better fit for systems programming and production safety constraints FAQ and Notes What about empty list or single node?\nThe same loop handles both naturally.\nIs three pointers mandatory?\nYou must preserve successor somehow; variable names can differ, state is equivalent.\nWhy not prefer recursion since it\u0026rsquo;s shorter?\nRecursive depth can overflow stack on long input; iterative is safer as default.\nHow to self-check quickly?\nUse this rule: save next first, reverse next, then advance.\nBest Practices Memorize the operation order as a fixed template Draw pointer state for at least 2-3 iterations before coding Use iterative as default in production-quality code S - Summary Reverse Linked List is fundamentally pointer rewiring, not value swapping. Three-pointer iterative template achieves O(n) time and O(1) extra space. Correct order is the core: save successor -\u0026gt; reverse link -\u0026gt; advance. Recursive form is useful for understanding, but iterative is usually safer for engineering. Recommended Further Reading LeetCode 206. Reverse Linked List LeetCode 92. Reverse Linked List II LeetCode 25. Reverse Nodes in k-Group LeetCode 234. Palindrome Linked List Conclusion Once the three-pointer template is stable in your muscle memory, most linked-list reversal variants become local modifications rather than new problems.\nReferences https://leetcode.com/problems/reverse-linked-list/ https://en.cppreference.com/w/cpp/container/forward_list https://doc.rust-lang.org/std/option/enum.Option.html https://go.dev/tour/moretypes/6 Meta Info Reading time: 10-12 min Tags: Hot100, linked list, pointer, iteration, LeetCode 206 SEO keywords: Reverse Linked List, three pointers, iterative, recursive, LeetCode 206, Hot100 Meta description: O(n)/O(1) linked-list reversal with three pointers, with recursive comparison and runnable multi-language implementations. Call To Action (CTA) Do two drills to lock this in:\nManually trace prev/cur/next for at least three steps without code Solve LeetCode 92 right after this one to reuse the same rewiring pattern Multi-language Implementations (Python / C / C++ / Go / Rust / JS) from typing import Optional class ListNode: def __init__(self, val: int = 0, next: Optional[\u0026#34;ListNode\u0026#34;] = None): self.val = val self.next = next def reverseList(head: Optional[ListNode]) -\u0026gt; Optional[ListNode]: prev = None cur = head while cur is not None: nxt = cur.next cur.next = prev prev = cur cur = nxt return prev #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct ListNode { int val; struct ListNode* next; }; struct ListNode* reverseList(struct ListNode* head) { struct ListNode* prev = NULL; struct ListNode* cur = head; while (cur) { struct ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = nxt; } return prev; } #include \u0026lt;iostream\u0026gt; struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* cur = head; while (cur) { ListNode* nxt = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = nxt; } return prev; } package main type ListNode struct { Val int Next *ListNode } func reverseList(head *ListNode) *ListNode { var prev *ListNode cur := head for cur != nil { nxt := cur.Next cur.Next = prev prev = cur cur = nxt } return prev } #[derive(PartialEq, Eq, Clone, Debug)] pub struct ListNode { pub val: i32, pub next: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;, } impl ListNode { pub fn new(val: i32) -\u0026gt; Self { ListNode { val, next: None } } } pub fn reverse_list(mut head: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt;) -\u0026gt; Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; { let mut prev: Option\u0026lt;Box\u0026lt;ListNode\u0026gt;\u0026gt; = None; while let Some(mut node) = head { head = node.next.take(); node.next = prev; prev = Some(node); } prev } function ListNode(val, next = null) { this.val = val; this.next = next; } function reverseList(head) { let prev = null; let cur = head; while (cur) { const nxt = cur.next; cur.next = prev; prev = cur; cur = nxt; } return prev; } ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/206-reverse-linked-list/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nReverse Linked List is the first serious pointer-rewiring exercise in Hot100. It looks simple, but most bugs come from broken links and wrong operation order. This ACERS guide explains the three-pointer iterative template thoroughly and compares it with recursion.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003epointer\u003c/code\u003e, \u003ccode\u003eiteration\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Hot100, Reverse Linked List, three pointers, iterative, recursive, LeetCode 206\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Three-pointer iterative reversal in O(n)/O(1), with recursive contrast, common pitfalls, engineering mapping, and runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the head of a singly linked list, reverse the list and return the new head.\u003c/p\u003e","title":"Hot100: Reverse Linked List Three-Pointer Iterative/Recursive ACERS Guide"},{"content":" Subtitle / Summary\nThis is Hot100 article #1 for the series: Subarray Sum Equals K. We reduce the naive O(n^2) approach to O(n) with prefix sum plus a frequency hash map, then map the same pattern to real engineering scenarios.\nReading time: 12-15 min Tags: Hot100, prefix sum, hash map SEO keywords: Subarray Sum Equals K, prefix sum, hash map, O(n), Hot100 Meta description: O(n) counting of subarrays with sum k using prefix sum + hash map, with complexity analysis and runnable multi-language code. Target Readers Hot100 learners who want stable reusable templates Intermediate engineers who want to transfer counting patterns to real data pipelines Interview prep readers who want to master prefix sum + hash map Background / Motivation \u0026ldquo;Count subarrays whose sum equals k\u0026rdquo; is one of the most classic counting problems. It appears in log analytics, risk threshold hits, and transaction sequence statistics. The two-loop brute force method is straightforward, but slows down quickly as input grows. So we need an O(n) method that scales.\nCore Concepts (Must Understand) Subarray: a continuous non-empty segment in an array Prefix sum: cumulative sum from start to a position Difference relation: if prefix[r] - prefix[l-1] = k, then nums[l..r] sums to k Frequency hash map: count how many times each prefix sum has appeared A - Algorithm Problem Restatement Given an integer array nums and an integer k, return the total number of subarrays whose sum equals k. A subarray must be continuous and non-empty.\nInput / Output Name Type Description nums int[] integer array k int target sum return int number of subarrays with sum k Example 1 nums = [1, 1, 1], k = 2 Valid subarrays are [1,1] at indices (0..1) and (1..2).\nOutput: 2\nExample 2 nums = [1, 2, 3], k = 3 Valid subarrays are [1,2] and [3].\nOutput: 2\nC - Concepts Method Category Prefix sum + frequency hash map, a standard counting pattern.\nKey Formula Define prefix sum as:\nprefix[0] = 0 prefix[i] = nums[0] + nums[1] + ... + nums[i-1] Then subarray sum:\nsum(l..r) = prefix[r+1] - prefix[l] To make it equal k, we need:\nprefix[l] = prefix[r+1] - k Core Idea Scan from left to right with running sum s. At each element:\nCount how many previous prefix sums equal s - k Add that count to answer Insert current s into frequency map This order (\u0026ldquo;count first, then insert\u0026rdquo;) prevents missing valid subarrays.\nPractical Guide / Steps Initialize s = 0, ans = 0, count = {0: 1} For each element x in nums: s += x ans += count.get(s - k, 0) count[s] = count.get(s, 0) + 1 Return ans Runnable Example (Python) from typing import List def subarray_sum(nums: List[int], k: int) -\u0026gt; int: count = {0: 1} ans = 0 s = 0 for x in nums: s += x ans += count.get(s - k, 0) count[s] = count.get(s, 0) + 1 return ans if __name__ == \u0026#34;__main__\u0026#34;: print(subarray_sum([1, 1, 1], 2)) print(subarray_sum([1, 2, 3], 3)) Run:\npython3 demo.py Explanation / Why This Works The hard part is continuity: subarrays must be continuous. Prefix sum turns \u0026ldquo;continuous range sum\u0026rdquo; into \u0026ldquo;difference of two prefix sums\u0026rdquo;. So counting subarrays becomes counting how many previous prefix sums match s - k.\nThis is also why sliding window is unreliable here: if negative numbers exist, window monotonicity breaks and common window rules fail.\nE - Engineering Scenario 1: transaction stream threshold hit counting (Python) Background: count how many contiguous day ranges have net amount exactly k.\nWhy it fits: amounts can be positive/negative; sliding window is not stable.\ndef count_exact_k(amounts, k): count = {0: 1} s = 0 ans = 0 for x in amounts: s += x ans += count.get(s - k, 0) count[s] = count.get(s, 0) + 1 return ans print(count_exact_k([3, -1, 2, 1, -2, 4], 3)) Scenario 2: service monitoring replay window counting (Go) Background: count contiguous windows where error count sum equals k during offline replay.\nWhy it fits: large log arrays need O(n) throughput.\npackage main import \u0026#34;fmt\u0026#34; func countExactK(nums []int, k int) int { count := map[int]int{0: 1} sum := 0 ans := 0 for _, x := range nums { sum += x ans += count[sum-k] count[sum]++ } return ans } func main() { fmt.Println(countExactK([]int{1, 2, 3, -2, 2}, 3)) } Scenario 3: front-end cart threshold hint (JavaScript) Background: count contiguous product price blocks that exactly hit promotion threshold k.\nWhy it fits: lightweight in-browser counting without backend round trip.\nfunction countExactK(nums, k) { const count = new Map(); count.set(0, 1); let sum = 0; let ans = 0; for (const x of nums) { sum += x; ans += count.get(sum - k) || 0; count.set(sum, (count.get(sum) || 0) + 1); } return ans; } console.log(countExactK([5, -1, 2, 4, -2], 4)); R - Reflection Complexity Time: O(n) Space: O(n) Alternatives and Tradeoffs Method Time Space Notes Brute force double loop O(n^2) O(1) Simple but slow Prefix sum + hash map O(n) O(n) Current method, practical optimum Sorted prefix / tree structure O(n log n) O(n) Useful in related variants, but heavier Common Wrong Ideas Sliding window: only reliable for non-negative constraints Missing count[0] = 1: loses subarrays starting at index 0 32-bit accumulation risk: use 64-bit where overflow is possible Why This Is Optimal You must inspect each element at least once, so lower bound is O(n). Hash map gives amortized O(1) lookup/insert per step, reaching that bound.\nFAQ and Notes What if array contains negatives?\nPrefix-sum counting handles negatives naturally and remains correct.\nCounterexample for sliding window: nums = [1, -1, 1], k = 1.\nCorrect answer is 3 ([1], [1,-1,1], [1]), but positive-window rules miss cases.\nCan large k overflow integer sum?\nUse 64-bit running sum in languages where int may overflow.\nIs subarray the same as subsequence?\nNo. Subarray is continuous. Subsequence is not.\nBest Practices Keep this as a fixed template: prefix sum + frequency map Always initialize count[0] = 1 Prefer 64-bit for running sum on large values Add tests for negatives, all zeros, and k = 0 S - Summary Continuous-range sum counting can be converted to prefix-sum difference counting. Frequency hash map reduces counting from O(n^2) to O(n). Sliding window is not generally correct when negatives are present. count[0] = 1 is a critical correctness detail. This pattern transfers well to logs, transactions, and monitoring streams. Recommended Further Reading LeetCode 560 - Subarray Sum Equals K Prefix Sum data structure patterns Hash-map frequency counting templates Sliding window applicability conditions Conclusion The value of this problem is not a one-off trick. It is a reusable counting model. Once internalized, you can solve many \u0026ldquo;continuous range count\u0026rdquo; problems quickly and safely.\nReferences https://leetcode.com/problems/subarray-sum-equals-k/ https://cp-algorithms.com/data_structures/prefix_sum.html https://en.cppreference.com/w/cpp/container/unordered_map https://doc.rust-lang.org/std/collections/struct.HashMap.html Meta Info Reading time: 12-15 min Tags: Hot100, prefix sum, hash map, counting SEO keywords: Subarray Sum Equals K, prefix sum, hash map, O(n) Meta description: Count subarrays with sum k in O(n) using prefix sum + hash map, with engineering mapping and multi-language code. Call To Action (CTA) If you are doing Hot100, do not just memorize answers. Write each problem as \u0026ldquo;pattern + engineering mapping\u0026rdquo; and keep a reusable template set. Share your own variant in comments if you adapt this pattern to a production case.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) from typing import List def subarray_sum(nums: List[int], k: int) -\u0026gt; int: count = {0: 1} ans = 0 s = 0 for x in nums: s += x ans += count.get(s - k, 0) count[s] = count.get(s, 0) + 1 return ans if __name__ == \u0026#34;__main__\u0026#34;: print(subarray_sum([1, 1, 1], 2)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; typedef struct { long long key; int val; int used; } Entry; static unsigned long long hash_ll(long long x) { return (unsigned long long)x * 11400714819323198485ull; } static int find_slot(Entry *table, int cap, long long key, int *found) { unsigned long long mask = (unsigned long long)cap - 1ull; unsigned long long idx = hash_ll(key) \u0026amp; mask; while (table[idx].used \u0026amp;\u0026amp; table[idx].key != key) { idx = (idx + 1ull) \u0026amp; mask; } *found = table[idx].used \u0026amp;\u0026amp; table[idx].key == key; return (int)idx; } int subarray_sum(const int *nums, int n, int k) { int cap = 1; while (cap \u0026lt; n * 2) cap \u0026lt;\u0026lt;= 1; if (cap \u0026lt; 2) cap = 2; Entry *table = (Entry *)calloc((size_t)cap, sizeof(Entry)); if (!table) return 0; long long sum = 0; int ans = 0; int found = 0; int pos = find_slot(table, cap, 0, \u0026amp;found); table[pos].used = 1; table[pos].key = 0; table[pos].val = 1; for (int i = 0; i \u0026lt; n; ++i) { sum += nums[i]; pos = find_slot(table, cap, sum - k, \u0026amp;found); if (found) ans += table[pos].val; pos = find_slot(table, cap, sum, \u0026amp;found); if (found) { table[pos].val += 1; } else { table[pos].used = 1; table[pos].key = sum; table[pos].val = 1; } } free(table); return ans; } int main(void) { int nums[] = {1, 1, 1}; printf(\u0026#34;%d\\n\u0026#34;, subarray_sum(nums, 3, 2)); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;unordered_map\u0026gt; #include \u0026lt;vector\u0026gt; int subarraySum(const std::vector\u0026lt;int\u0026gt; \u0026amp;nums, int k) { std::unordered_map\u0026lt;long long, int\u0026gt; count; count[0] = 1; long long sum = 0; int ans = 0; for (int x : nums) { sum += x; auto it = count.find(sum - k); if (it != count.end()) { ans += it-\u0026gt;second; } count[sum] += 1; } return ans; } int main() { std::vector\u0026lt;int\u0026gt; nums{1, 1, 1}; std::cout \u0026lt;\u0026lt; subarraySum(nums, 2) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } package main import \u0026#34;fmt\u0026#34; func subarraySum(nums []int, k int) int { count := map[int]int{0: 1} sum := 0 ans := 0 for _, x := range nums { sum += x ans += count[sum-k] count[sum]++ } return ans } func main() { fmt.Println(subarraySum([]int{1, 1, 1}, 2)) } use std::collections::HashMap; fn subarray_sum(nums: \u0026amp;[i32], k: i32) -\u0026gt; i32 { let mut count: HashMap\u0026lt;i64, i32\u0026gt; = HashMap::new(); count.insert(0, 1); let mut sum: i64 = 0; let mut ans: i32 = 0; for \u0026amp;x in nums { sum += x as i64; if let Some(v) = count.get(\u0026amp;(sum - k as i64)) { ans += *v; } *count.entry(sum).or_insert(0) += 1; } ans } fn main() { let nums = vec![1, 1, 1]; println!(\u0026#34;{}\u0026#34;, subarray_sum(\u0026amp;nums, 2)); } function subarraySum(nums, k) { const count = new Map(); count.set(0, 1); let sum = 0; let ans = 0; for (const x of nums) { sum += x; ans += count.get(sum - k) || 0; count.set(sum, (count.get(sum) || 0) + 1); } return ans; } console.log(subarraySum([1, 1, 1], 2)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/560-subarray-sum-equals-k/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThis is Hot100 article #1 for the series: Subarray Sum Equals K. We reduce the naive O(n^2) approach to O(n) with prefix sum plus a frequency hash map, then map the same pattern to real engineering scenarios.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12-15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003eprefix sum\u003c/code\u003e, \u003ccode\u003ehash map\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Subarray Sum Equals K, prefix sum, hash map, O(n), Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: O(n) counting of subarrays with sum k using prefix sum + hash map, with complexity analysis and runnable multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget Readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHot100 learners who want stable reusable templates\u003c/li\u003e\n\u003cli\u003eIntermediate engineers who want to transfer counting patterns to real data pipelines\u003c/li\u003e\n\u003cli\u003eInterview prep readers who want to master prefix sum + hash map\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003e\u0026ldquo;Count subarrays whose sum equals k\u0026rdquo; is one of the most classic counting problems.\nIt appears in log analytics, risk threshold hits, and transaction sequence statistics.\nThe two-loop brute force method is straightforward, but slows down quickly as input grows.\nSo we need an O(n) method that scales.\u003c/p\u003e","title":"Hot100: Subarray Sum Equals K Prefix Sum + Hash Map ACERS Guide"},{"content":" This is a \u0026ldquo;graph algorithms topic navigation\u0026rdquo; page. The goal is not to stack articles together, but to give you an executable learning path from basic traversal to distributed graph computation.\nCurrent Directory Status (Topic Structuring Completed) The graph algorithms series has been migrated to:\ncontent/zh/dev/algorithm/graph/ It also uses two-digit prefixes (00/10/20...) to mark reading order, which makes it easier to:\nBrowse in sequence within the file system Insert new articles incrementally later (while preserving numbering gaps) Locate stages quickly during batch maintenance Recommended Reading Order (By Capability Building) Stage 0: Traversal Fundamentals (Lay the Foundation First) BFS / DFS Engineering Intro: k-hop Queries, Subgraph Extraction, and Path Reachability Shortest Path in Practice: Engineering Selection of BFS, Dijkstra, and A* Goals:\nReliably implement iterative graph traversal; Explain when to use BFS and when to use Dijkstra/A*; Build the habit of adding early stop, visited, and budget limits. Stage 1: Reachability and Connectivity Structure (Core of Graph Querying) k-hop and Reachability Queries: BFS Constraints, Reachability Indexes, and 2-hop Labeling Connected Components and SCC: Tarjan / Kosaraju Goals:\nUpgrade \u0026ldquo;can it reach?\u0026rdquo; from one-off search to a system capability; Understand that undirected connectivity and directed strong connectivity are different problem classes; Build a combined mindset of \u0026ldquo;online BFS + offline index.\u0026rdquo; Stage 2: Graph Analytics Metrics (From Reachability to Insight) Graph Centrality: Degree / Betweenness / Closeness PageRank / Personalized PageRank: Node Importance and Incremental Updates Goals:\nExplain different definitions of \u0026ldquo;importance\u0026rdquo; and their applicability boundaries; Apply centrality and PageRank to recommendation, risk control, and influence analysis; Understand that \u0026ldquo;metric is theoretically correct\u0026rdquo; and \u0026ldquo;platform can run it well\u0026rdquo; are different issues. Stage 3: Structure Mining and Matching (Application-Layer Capabilities) Subgraph Matching: VF2, Ullmann, and Pruning Community Detection: Louvain and Label Propagation Goals:\nPerform pattern recognition and rule-graph matching; Make engineering tradeoffs between \u0026ldquo;community quality vs speed\u0026rdquo;; Understand cost-curve differences between matching and clustering. Stage 4: Large-Scale and Dynamic Scenarios (Platform-Level Capabilities) Dynamic Graphs and Incremental Computation: Incremental Shortest Path, Incremental PageRank, and Connectivity Maintenance Graph Partitioning: Edge-cut, Vertex-cut, and METIS Selection Graph Computation Models: Pregel (BSP) and GAS, How to Run PageRank / CC / Parallel BFS Goals:\nDecide when to do full recomputation and when to do incremental updates; Co-design algorithms with partitioning/communication/convergence strategies; Explain the root causes of \u0026ldquo;why this graph workload is slow in distributed environments.\u0026rdquo; Two Practical Study Rhythms Rhythm A (2-Week Sprint, Engineering First) Week 1: Stages 0-1 (articles 1-4) Week 2: Stages 2-4 (articles 5-11) Best for: people who need to connect graph capabilities to business lines quickly.\nRhythm B (4-Week Steady Path, Principles First) Week 1: Traversal and shortest path (1-2) Week 2: Reachability and connectivity (3-4) Week 3: Centrality and PageRank (5-6) Week 4: Matching/community/dynamic graph/partitioning/computation models (7-11) Best for: people building graph platforms or maintaining graph services long term.\nRecommendations for Using This Series After each article, run at least one runnable code sample from the post. Bring your own business graph into the same problem frame (input scale, update frequency, SLA). For every task, write down \u0026ldquo;stop condition + budget + regression baseline\u0026rdquo;; this matters more than memorizing one more formula. Next Steps (Optional) If you continue expanding this series, evolve it in this order:\nFirst, apply a unified tag across all 11 posts (for example, graph-algorithms-series) Then add second-level aggregation pages (fundamentals/analytics/platform) For new posts, prioritize 120/130... numbering to avoid renumbering older files ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/00-graph-algorithms-learning-path/","summary":"\u003cblockquote\u003e\n\u003cp\u003eThis is a \u0026ldquo;graph algorithms topic navigation\u0026rdquo; page. The goal is not to stack articles together, but to give you an executable learning path from basic traversal to distributed graph computation.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"current-directory-status-topic-structuring-completed\"\u003eCurrent Directory Status (Topic Structuring Completed)\u003c/h2\u003e\n\u003cp\u003eThe graph algorithms series has been migrated to:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003econtent/zh/dev/algorithm/graph/\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIt also uses two-digit prefixes (\u003ccode\u003e00/10/20...\u003c/code\u003e) to mark reading order, which makes it easier to:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBrowse in sequence within the file system\u003c/li\u003e\n\u003cli\u003eInsert new articles incrementally later (while preserving numbering gaps)\u003c/li\u003e\n\u003cli\u003eLocate stages quickly during batch maintenance\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"recommended-reading-order-by-capability-building\"\u003eRecommended Reading Order (By Capability Building)\u003c/h2\u003e\n\u003ch3 id=\"stage-0-traversal-fundamentals-lay-the-foundation-first\"\u003eStage 0: Traversal Fundamentals (Lay the Foundation First)\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\u003ca href=\"/jeanblog/dev/algorithm/graph/10-bfs-dfs-k-hop-subgraph-path-existence/\"\u003eBFS / DFS Engineering Intro: k-hop Queries, Subgraph Extraction, and Path Reachability\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"/jeanblog/dev/algorithm/graph/20-shortest-path-bfs-dijkstra-astar-acers/\"\u003eShortest Path in Practice: Engineering Selection of BFS, Dijkstra, and A*\u003c/a\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eGoals:\u003c/p\u003e","title":"Graph Algorithms Learning Path: From BFS to Graph Computation Models"},{"content":" Subtitle / Abstract\nIn graph computing platforms, what defines your upper bound is usually not a single algorithm but the execution model. This article breaks Pregel (BSP) and GAS down to executable reality: how messages flow, how state converges, when it slows down, and how to run parallel BFS.\nEstimated reading time: 16-20 minutes Tags: Pregel, GAS, PageRank, CC, parallel BFS SEO keywords: Pregel, BSP, GAS, PageRank, Connected Components, parallel BFS Meta description: Engineering practice for graph computation models: from Pregel/GAS concepts to runnable implementations of PageRank, CC, and parallel BFS. Target Audience Engineers building graph databases / graph engines / graph analytics platforms Developers who already know BFS/DFS/PageRank but do not know how distributed graph computation is organized Architects who must trade off throughput, latency, and convergence rounds Background / Motivation On the same graph, for the same PageRank task:\nA single-machine script may converge in 10 seconds; A distributed run may still run after 3 minutes; Changing partition strategy may bring it down to 40 seconds. This shows bottlenecks are often not in the formula, but in the execution model.\nThe two most common models in engineering are:\nPregel (BSP): synchronous progress by supersteps; GAS (Gather-Apply-Scatter): aggregate edge contributions, then update state. If you do not understand these two models:\nPageRank stays at formula level without stable convergence behavior; CC (Connected Components) becomes a high-communication implementation; parallel BFS can suffer frontier explosion and stragglers. Quick Orientation Map (60-120 Seconds) Problem shape: iterative propagation on large graphs (ranking, labeling, distance) Core sentence: rewrite \u0026ldquo;graph traversal\u0026rdquo; as \u0026ldquo;vertex state machine + round-based progression\u0026rdquo; When to use: |V|\u0026gt;=10^6, |E|\u0026gt;=10^7, and you need batch whole-graph computation When to avoid: single point queries, low-latency online path queries (should use query engine) Complexity overview: per-round approximately O(E/P) (P = parallelism), total roughly rounds × per-round cost Common failure: high-degree hubs cause message skew; a slow partition drags barrier time Deep-Dive Focus (PDKH) This article deeply focuses on two concepts via the PDKH ladder:\nSynchronous supersteps and convergence criteria (Pregel/BSP core) Frontier propagation and idempotent aggregation (parallel BFS / CC core) Covered PDKH steps:\nProblem Reframe Minimal Worked Example Invariant Formalization Correctness Sketch Thresholds Failure Mode Engineering Reality Core Concepts 1) Pregel (BSP) Each vertex keeps state state[v] Each superstep reads previous-round messages inbox[v] Computation sends new messages to neighbors Global barrier before next round Core invariant:\nRound t reads only the complete output of round t-1, never half-round intermediate states.\n2) GAS (Gather-Apply-Scatter) Gather: collect contributions from adjacent edges (parallelizable) Apply: update vertex state Scatter: decide which neighbors receive propagation Compared with Pregel’s explicit messaging, GAS is closer to \u0026ldquo;edge computation + vertex aggregation.\u0026rdquo;\n3) Unified Formula View Many graph algorithms can be written as:\nx_v^{(t+1)} = F(x_v^{(t)}, AGG({ M_{u-\u0026gt;v}(x_u^{(t)}, e_{uv}) }))\nVariables:\nx_v^{(t)}: state of vertex v at round t M_{u-\u0026gt;v}: edge propagation function AGG: aggregation operator (sum/min/max) F: state update function When AGG is commutative and associative, parallelization and partitioning become much easier.\nA — Algorithm (Algorithm Problem and Execution Model) Problem Restatement (Engineering Version) Given graph G=(V,E), support in distributed execution:\nPageRank: global importance scores; CC: connected-component labels on undirected graph; BFS(src, hop_limit): level-wise reachability and shortest hop count. Inputs and Outputs Name Type Description V vertex set vertex IDs E edge set adjacency relations P int partition/parallelism max_iter int maximum iteration rounds output1 rank[v] PageRank score output2 label[v] CC label output3 dist[v] BFS distance (INF if unreachable) Minimal Example Graph 0 -\u0026gt; 1,2 1 -\u0026gt; 2 2 -\u0026gt; 3 3 -\u0026gt; 4 4 -\u0026gt; (none) PageRank: mass diffuses along outgoing edges; sink vertices need special handling CC (treated as undirected): all vertices in one component BFS(0): dist=[0,1,1,2,3] C — Concepts (Core Ideas) How Pregel Runs PageRank Per superstep:\nGather (implemented via messages): collect inbound contributions; Apply: rank[v]=(1-d)/N + d*sum(inbox[v]); Scatter: send rank[v]/out_degree[v] to outgoing neighbors. Common convergence criteria:\nL1 delta = Σ|rank_t-rank_{t-1}| \u0026lt; ε or fixed rounds (for example, 20-30) Engineering threshold example:\nAt N=10^8, fixed rounds + sampled validation is common to avoid high overhead of full-delta statistics. How Pregel Runs CC State: label[v] initialized as v.\nPer round, send current minimum label to neighbors and update to the minimum received.\nInvariant:\nlabel[v] is monotonically non-increasing; it can decrease only finitely many times, then stabilizes. This guarantees termination and correctness (on convergence, each connected component reaches one common minimum label).\nWhy Parallel BFS Is Often Layer-Synchronous Parallel BFS is often written as level-synchronous:\nExpand current frontier frontier_t in parallel; Generate frontier_{t+1}; Enter next layer after barrier. Pros: stable semantics and naturally correct shortest hop counts.\nCost: frontier explosion greatly increases communication and deduplication costs.\nEquivalent Implementation in GAS View PageRank: Gather=sum(in-neighbor contribution), Apply=rank update, Scatter=notify if delta large CC: Gather=min(neighbor labels), Apply=take min, Scatter=only on changed vertices BFS: Gather=min(parent_dist+1), Apply=relax, Scatter=on newly activated frontier When the ratio of changed vertices is low, GAS incremental propagation can significantly reduce useless edge scans.\nDeep Dive 1: Synchronous Supersteps and Convergence Criteria (Full PDKH) P — Problem Reframe What we really solve is not \u0026ldquo;how to write PageRank formula,\u0026rdquo; but:\nIn distributed systems, how to ensure each round reads a consistent snapshot, can decide convergence globally, and avoids unbounded tail latency from slow partitions.\nThis is BSP’s value: constrain complex parallel behavior into \u0026ldquo;rounds + barriers + global decidability.\u0026rdquo;\nD — Minimal Worked Example Take a 3-node directed cycle: 0-\u0026gt;1-\u0026gt;2-\u0026gt;0, damping d=0.85, initial rank=[1/3,1/3,1/3].\nRound 1:\nEach node sends 0.3333 to one neighbor Updated rank remains 0.3333 delta = 0 This shows that under full symmetry, one round can stabilize.\nBut with chain 0-\u0026gt;1-\u0026gt;2:\nRound 1: mass shifts toward the tail Round 2: sink (out-degree 0) absorbs mass; without sink-mass handling, total mass leaks This is why sink handling must be explicit in production.\nK — Invariant / Contract Two key contracts in standard PageRank-BSP:\nSnapshot contract: round t+1 reads only completed rank from round t. Mass contract: with sink redistribution, sum(rank)=1 (allowing numerical tolerance around 1e-9). If asynchronous updates are introduced without compensation, contract 1 breaks.\nIf sink handling is omitted, contract 2 breaks.\nH — Formalization and Thresholds Let N=|V|:\nrank_{t+1}(v) = (1-d)/N + d*(sink_t/N + Σ_{u-\u0026gt;v} rank_t(u)/outdeg(u))\nCommon convergence thresholds:\nAbsolute threshold: L1_delta \u0026lt; ε, e.g. ε=1e-6 Relative threshold: L1_delta / N \u0026lt; ε_avg At N\u0026gt;=10^8, common strategy:\nHard cap at 20-30 rounds; sample 0.1% vertices each round for delta monitoring; stop early if sampled delta stays below threshold for 3 consecutive rounds. The core idea is to compress full-monitoring cost into controllable range.\nCorrectness Sketch Preservation: if round t rank is non-negative and sums to 1, round t+1 is also non-negative and preserves sum constraints through non-negative linear combination. Convergence intuition: damping term (1-d) introduces contraction effect; in common norms the iterative mapping is contractive. Termination: stop when threshold or round cap is reached. Failure Mode ε too small: many extra rounds with no business value. Highly imbalanced partitions: even correct operators get dominated by barrier time. Missing dangling correction: continuous score leakage, distorted ranking. Engineering Reality At 16-64 partitions, bottlenecks are often not floating-point operations, but:\ncross-partition message serialization and network replication; barrier waiting for the slowest partition; hotspot vertices saturating one partition’s CPU. So practical optimization order is usually:\npartitioning and hotspot control first; message compression second; convergence threshold tuning last. Deep Dive 2: Frontier Propagation and Idempotent Aggregation (Full PDKH) P — Problem Reframe The essence of parallel BFS/CC is:\nUse minimal state changes to drive next-round propagation, instead of repeatedly scanning the whole graph.\nThis \u0026ldquo;minimal state change\u0026rdquo; is frontier (or active set).\nD — Minimal Worked Example Graph: 0-\u0026gt;[1,2], 1-\u0026gt;[3], 2-\u0026gt;[3], 3-\u0026gt;[4], source 0.\nLayer progression:\nfrontier_0={0} frontier_1={1,2} frontier_2={3} frontier_3={4} Node 3 is discovered from both 1 and 2.\nWithout idempotent dedup (visited bitmap or min aggregation), next-round propagation is duplicated and message volume inflates.\nK — Invariant / Contract Key invariants for parallel BFS:\nThe first write to dist[v] is the shortest hop count; each vertex should enter frontier only once (ignoring idempotent repeats from fault replay). Key invariants for CC:\nLabels are monotonically non-increasing; label[v] always comes from some vertex in the same component; on convergence, labels are equal within component and may differ across components. H — Formalization and Thresholds BFS formalization (layer-synchronous):\ndist_{t+1}(v) = min(dist_t(v), min_{u in frontier_t, (u,v) in E}(dist_t(u)+1))\nCC formalization (minimum-label propagation):\nlabel_{t+1}(v) = min(label_t(v), min_{u in N(v)} label_t(u))\nCommon engineering thresholds:\nhop_limit \u0026lt;= 3/4/6: common in risk propagation and impact analysis; when |frontier_t| / |V| \u0026gt; 0.2, frontier is near full-graph activation and strategy switch is often needed (for example bitmap batching); when cross-partition edge ratio \u0026gt; 35%, frontier broadcast cost rises sharply. Correctness Sketch For BFS:\nLayer synchronization guarantees \u0026ldquo;shorter paths arrive first\u0026rdquo;; once dist[v] is written, later candidates cannot be shorter (they come from same or deeper layers). For CC:\nmin aggregation is idempotent, commutative, and associative, supporting parallel merge; labels only decrease, so finite rounds guarantee stabilization; stabilized state is a constant-label mapping over connected-component equivalence classes. Thresholds and Complexity In sparse graphs (m≈O(n)), early frontiers are often small, so BFS cost can be approximated by local subgraph size.\nIn power-law graphs, if source is near high-centrality vertices, frontier can explode beyond 30% of graph in 1-2 layers.\nSo parallel BFS is not always faster than single-machine BFS:\nIf graph is small or frontier is narrow, distributed scheduling may lose; if graph is large and frontier expands in parallel, distributed gains are significant. Failure Mode Repeated enqueue: without visited/bitmap, messages can blow up exponentially. Incorrect early stop: stopping when one partition sees empty frontier misses active vertices elsewhere. Wrong edge direction use: treating reverse edges as forward in directed graphs changes reachability results. Engineering Reality Real optimization focus for parallel BFS/CC:\nuse bitmap frontier instead of hash set to save 3-10x memory; block-wise send hot adjacency lists to reduce serialization overhead; vertex reindexing improves adjacency access locality and reduces cache misses. These do not change algorithm correctness, but often decide whether runs remain stable.\nFeasibility and Lower-Bound Intuition Why Most Systems Do Not Compute Full Transitive Closure A full reachability matrix takes about O(n^2) space:\nat n=10^6, boolean matrix is roughly 10^12 bits, about 125GB (without index/redundancy) at n=10^7, it directly reaches TB scale and beyond This ignores update-maintenance cost.\nSo industrial systems usually use a two-stage path:\nonline BFS/parallel BFS with hop limit; add reach index or 2-hop labeling on hot subgraphs. When BSP/GAS Is Not Cost-Effective Counterexample scenario:\nquery is only single-source single-target path existence; 99% requests end within 1-2 hops; graph fits in single-machine memory (n\u0026lt;5e6, m\u0026lt;5e7 with enough RAM). Here, heavy distributed iteration is usually worse than optimizing a single-machine query engine.\nPractical Guide / Steps Decide semantics first: strict round consistency (BSP) or more aggressive async (accept non-determinism). Choose aggregation operator: prefer sum/min/max; avoid non-commutative aggregates that create sync bottlenecks. Partition well: place highly connected subgraphs together to reduce cross-partition edge ratio. Add early stop: PageRank uses delta\u0026lt;ε; BFS uses empty frontier or hop_limit. Prevent skew: merge/split messages for high-degree vertices; replicate mirrors if needed. Set budgets: cap per-round message count, active-vertex ratio, and max rounds. Worked Example (Track 2-3 Rounds) Example A: CC Two-Round Convergence Segment Graph (undirected): 0-1-2 and 3-4.\nInitial labels: [0,1,2,3,4]\nAfter round 1: [0,0,1,3,3] After round 2: [0,0,0,3,3] Stable after two rounds: component {0,1,2} has label 0; component {3,4} has label 3.\nExample B: BFS Layer-by-Layer From src=0:\nlayer 0: {0} layer 1: {1,2} layer 2: {3} layer 3: {4} First visit equals shortest hop count because layer synchronization ensures \u0026ldquo;shorter before longer.\u0026rdquo;\nPartition-Level Trace (2 Partitions + Barrier) For production realism, here is a 2-partition round trace.\nPartitioning:\nP0: nodes {0,1,2} P1: nodes {3,4,5} Edges:\nintra-partition: 0-\u0026gt;1, 1-\u0026gt;2, 3-\u0026gt;4, 4-\u0026gt;5 cross-partition: 2-\u0026gt;3 Run parallel BFS (src=0):\nSuperstep 0 P0 active: {0}, sends to 1 P1 active: {} after barrier: frontier_1={1} Superstep 1 P0 active: {1}, sends to 2 P1 active: {} after barrier: frontier_2={2} Superstep 2 (Cross-Partition Round) P0 active: {2}, sends to 3 through cross-partition edge P1 activates 3 after receiving remote message after barrier: frontier_3={3} Superstep 3 P1 active: {3}, sends to 4 P0 idle but still waits at barrier This small example shows two engineering facts:\nCross-partition edges convert local updates into network events; even partitions with no local active vertices must wait at barrier, an inherent BSP cost. Quantifying Communication Cost (Estimate) Let:\nM_t: number of cross-partition messages at round t S_msg: serialized bytes per message B_net: effective network bandwidth (byte/s) Then ideal lower bound of network time for that round is approximately:\nT_net_t \u0026gt;= (M_t * S_msg) / B_net\nIf M_t=5e7, S_msg=16B, B_net=2.5GB/s,\nnetwork transfer lower bound alone is about 0.32s; with deserialization and queuing, actual time is usually much higher.\nThis is why reducing cross-partition messages usually yields more benefit than micro-tuning compute formulas.\nParallel Convergence and Stop Strategies (Production Settings) Recommended PageRank Stop Strategy A common production \u0026ldquo;three-layer stop condition\u0026rdquo;:\niter \u0026gt;= max_iter (hard cap to avoid endless running) global or sampled delta \u0026lt; eps (precision condition) insufficient improvement for consecutive k rounds (benefit condition) Runnable example configuration:\nmax_iter=30 eps=1e-6 early stop if delta improvement \u0026lt; 1% for 3 consecutive rounds This avoids \u0026ldquo;last 10 rounds improve only basis points but consume 40% time.\u0026rdquo;\nRecommended CC Stop Strategy CC commonly uses \u0026ldquo;active set exhausted\u0026rdquo;:\nrecord changed-label vertices per round as A_t terminate when A_t=0 For large graphs, add safety guard:\nif A_t/|V| \u0026lt; 1e-6 for 2 consecutive rounds, run one full validation and stop Recommended BFS Stop Strategy frontier empty: natural termination reach hop_limit: business-driven termination (for example, risk control checks only 4 hops) hit target: single-target query can early-stop Note: in distributed systems, early stop must be globally coordinated; a single partition cannot decide alone.\nFault Recovery and Idempotence (Must Consider) In distributed environments, failure is normal rather than exceptional.\nWithout idempotence, retries can corrupt results.\nPageRank Idempotence Concerns replaying same-round messages causes duplicate accumulation; deduplicate by round ID or use recomputable round snapshots. rollback usually goes to latest superstep checkpoint, not patch-style fixes. CC/BFS Idempotence Concerns min aggregation is naturally idempotent: duplicate messages do not worsen minima; if BFS uses \u0026ldquo;first successful dist write\u0026rdquo; as atomic condition, duplicates are safely discarded. This is why many systems prefer sum/min/max:\nnot only parallel-friendly, but also more fault-tolerant.\nCorrectness (Proof Sketch) CC Invariant: label[v] is always some vertex ID inside its component, and is monotonically non-increasing. Preservation: each round only takes smaller labels, never increases. Termination: finite integer monotone descending sequence must terminate. Correctness: minimum label propagates within each connected component; with no cross-component edges, labels do not mix. Layer-Synchronous BFS Invariant: frontier at round k contains exactly nodes with distance k from source. Preservation: expansion only from frontier k to unvisited nodes, labeled k+1. Termination: frontier empty or hop cap reached. Correctness: first-visit level equals shortest hop count. Complexity Let n=|V|, m=|E|, T=iteration rounds, P=parallelism.\nPageRank: about O(T * m / P), space O(n + m/P) (including partition-edge cache) CC: worst case O(D * m / P), where D is upper bound of label-propagation rounds Parallel BFS: per layer approximately O(m_active/P), total roughly one pass over edges What matters most is not Big-O itself, but:\ncross-partition edge ratio; per-round barrier waiting; active-vertex ratio curve. Constant Factors and Engineering Realities Barrier cost: BSP waits for slowest partition each round; tail tasks determine latency. Message amplification: high-degree vertices can amplify one update to millions of messages. Cache locality: CSR sequential scans are usually better than random adjacency access. Dedup cost: BFS next_frontier without bitmap/bucketing causes huge shuffle pressure. Convergence monitoring: exact global delta is costly at very large scale; sampled monitoring + round caps is practical. Runnable Example (Python) from collections import deque def pagerank_bsp(adj, d=0.85, max_iter=30, eps=1e-8): n = len(adj) rank = [1.0 / n] * n out_deg = [len(nei) for nei in adj] for _ in range(max_iter): inbox = [(1.0 - d) / n for _ in range(n)] sink_mass = 0.0 for u in range(n): if out_deg[u] == 0: sink_mass += rank[u] continue share = d * rank[u] / out_deg[u] for v in adj[u]: inbox[v] += share if sink_mass \u0026gt; 0: extra = d * sink_mass / n for v in range(n): inbox[v] += extra delta = sum(abs(inbox[i] - rank[i]) for i in range(n)) rank = inbox if delta \u0026lt; eps: break return rank def cc_label_propagation_undirected(adj, max_iter=100): n = len(adj) label = list(range(n)) for _ in range(max_iter): changed = False new_label = label[:] for v in range(n): best = label[v] for u in adj[v]: if label[u] \u0026lt; best: best = label[u] if best \u0026lt; new_label[v]: new_label[v] = best changed = True label = new_label if not changed: break return label def bfs_level_sync(adj, src, hop_limit=None): n = len(adj) dist = [-1] * n dist[src] = 0 frontier = [src] level = 0 while frontier: if hop_limit is not None and level \u0026gt;= hop_limit: break next_frontier = [] for u in frontier: for v in adj[u]: if dist[v] == -1: dist[v] = level + 1 next_frontier.append(v) frontier = next_frontier level += 1 return dist if __name__ == \u0026#34;__main__\u0026#34;: directed = [[1, 2], [2], [3], [4], []] undirected = [[1], [0, 2], [1], [4], [3]] pr = pagerank_bsp(directed, max_iter=50) cc = cc_label_propagation_undirected(undirected) dist = bfs_level_sync(directed, src=0, hop_limit=4) print(\u0026#34;PageRank:\u0026#34;, [round(x, 6) for x in pr]) print(\u0026#34;CC labels:\u0026#34;, cc) print(\u0026#34;BFS dist:\u0026#34;, dist) Run:\npython3 graph_compute_demo.py E — Engineering (Production Scenarios) Scenario 1: Offline PageRank for Recommendation Graphs Background: candidate-pool weights are refreshed daily on graphs around 10^8 edges. Why BSP: synchronous rounds + fixed convergence criteria, stable and replayable outputs. Key optimizations: sink-mass aggregation, in-partition combiners, sampled delta monitoring. Scenario 2: CC Clustering for Risk Graphs Background: identify gangs/device clusters with explainable labels. Why label-propagation CC: min aggregation is idempotent and easy to recover under failure. Key optimization: propagate only vertices with label changes to reduce useless messaging. Scenario 3: Parallel BFS for k-hop Propagation Background: account risk diffusion and call-chain impact analysis. Why layer sync: shortest-hop semantics are naturally correct and easy to constrain by hop_limit. Key optimization: frontier bitmap + vertex reindexing to reduce shuffle and random access. Alternatives and Tradeoffs Strategy Pros Cons Best-fit range Pregel/BSP Clear semantics, stable output High barrier overhead Offline batch, replay-critical tasks GAS (synchronous) Edge-friendly, unified expression Framework complexity Mixed algorithm platforms Async graph compute Potential faster convergence Non-deterministic, harder debugging Iterative tasks with low consistency demand Single-machine traversal Simple development Lower memory/throughput ceiling Prototype phase around m \u0026lt;= 10^7 Why prioritize Pregel/GAS here:\nYou care about production execution of PageRank/CC/BFS rather than one-off point queries; all three map well to \u0026ldquo;aggregatable iterative propagation\u0026rdquo;; synchronous models are easier for SLA and regression alignment. Validation and Benchmark Checklist (Must Run Before Rollout) Algorithm-only without validation is risky in production.\nSplit validation into correctness, stability, and cost.\n1) Correctness Validation PageRank: verify sum(rank) is near 1 (for example, error \u0026lt;1e-6). CC: sample edges (u,v) and verify equal labels for same-component vertices. BFS: sample nodes and compare dist against single-machine baseline. Use two datasets:\nsmall graph (n\u0026lt;=1e4) for manual traceability; medium graph (n≈1e6) for parallel-vs-single-machine consistency. 2) Stability Validation Run same input 5 times and observe output drift (especially async mode). Inject partition failures and verify checkpoint recovery continues convergence. Stress with partition counts P=8/16/32/64 and check long-tail behavior. Recommended key metrics:\nper-round duration t_iter_p50/p95 barrier wait ratio active vertex ratio curve A_t/|V| 3) Cost Validation cross-partition message volume (per round and total) peak memory (frontier, inbox, adjacency cache) per-round network sent bytes Empirically, if you see:\nbarrier time \u0026gt; 35% of round total time cross-partition messages \u0026gt; 50% of total messages then optimize partition strategy first, not algorithm micro-parameters.\n4) Regression Baseline Recommendation Keep a replayable baseline for each task:\nfixed input snapshot ID fixed parameters (d, eps, max_iter, hop_limit) fixed partition strategy version This lets each optimization clearly answer:\ntrue algorithm/accuracy improvement; or fake improvement from system noise. Migration Path After this article, continue in order:\nJoin-based Graph Query (Expand/Filter/Join executor) Subgraph matching (VF2 + pruning) Dynamic graph incremental computation (local recomputation after edge updates) Graph indexing (2-hop labeling / reach index) 30-Second Selection Decision Tree (Directly Reusable) For graph platform selection, start with these four questions:\nMust results be strictly reproducible?\nYes: prefer synchronous BSP/Pregel; no: evaluate async engines.\nIs this a whole-graph iterative task?\nYes: PageRank/CC use GAS or Pregel;\nNo: for point queries, use query engine rather than distributed iteration.\nIs active-vertex ratio consistently below 5%?\nYes: prefer incremental propagation (scatter only changed vertices);\nNo: full-edge scans may be more stable.\nAre cross-partition edges above 40%?\nYes: repartition first, then tune algorithms;\nNo: then tune thresholds, compression, and operators.\nCore value of this tree is fixing optimization order:\narchitecture and partitioning first, execution model second, algorithm parameters last.\nFAQ and Caveats Must PageRank run to very small eps?\nNot always. Online workloads often use \u0026ldquo;fixed rounds + sampled checks\u0026rdquo; to balance cost and stability.\nCan CC run asynchronously?\nYes, but reproducibility degrades and debugging gets harder; clarify business tolerance first.\nWhere does parallel BFS explode most often?\nHigh-degree nodes can trigger frontier explosion, making dedup and communication dominant bottlenecks.\nWhy not compute full transitive closure directly?\nStorage is near O(n^2), almost unacceptable at million-scale vertices.\nWhich parameter should be tuned first?\nRecommended order: partition -\u0026gt; round cap -\u0026gt; early-stop threshold -\u0026gt; message compression.\nDo not tune only eps first; common outcome is slower runs with little gain.\nHow to set BFS hop_limit?\nSet hard boundary from business semantics first, then evaluate recall gain from historical data.\nFor example, risk propagation commonly starts at k=3, then compare marginal value of k=4/5 vs extra cost.\nWhen should synchronous be replaced by asynchronous?\nOnly after confirming business can accept non-determinism and barrier waiting is truly dominant (for example \u0026gt;40%).\nBest Practices and Recommendations Structure algorithms as \u0026ldquo;state + aggregation + propagation\u0026rdquo; for implementation unification. Every iterative task should define hard stop conditions (round/budget/time window). Prefer idempotent aggregations (sum/min/max) for better fault tolerance and retry stability. Apply dedicated handling for high-degree vertices (mirrors, replicas, message merge). Monitor at least: active-vertex ratio, cross-partition message volume, per-round p95 latency. Preserve replay outputs with same input/params after each optimization to avoid confusing noise with progress. R — Reflection The most common error in these tasks is treating \u0026ldquo;formula correctness\u0026rdquo; as \u0026ldquo;system readiness.\u0026rdquo;\nWhat truly determines production quality:\nwhether model semantics are reproducible; whether rounds and communication are budgetable; whether skew and failure recovery have concrete plans. Pregel and GAS provide an engineering abstraction boundary, not one standalone algorithm.\nS — Summary Pregel (BSP) fits offline graph computation requiring determinism and replayability. GAS fits algorithm families expressible as \u0026ldquo;edge contribution -\u0026gt; vertex update -\u0026gt; selective propagation.\u0026rdquo; PageRank, CC, and parallel BFS all reduce to \u0026ldquo;aggregation + iterative state update.\u0026rdquo; Parallel performance ceiling is usually set by communication skew and barriers, not formula complexity. To run graph algorithms stably, design stop conditions, budgets, and monitoring before optimization tricks. In real systems, gains usually come from reducing cross-partition messages and controlling active frontiers, not from 5% operator micro-tuning. Every optimization should be paired with regression validation and versioned baselines. References and Further Reading Pregel: A System for Large-Scale Graph Processing (Google, 2010) PowerGraph: Distributed Graph-Parallel Computation on Natural Graphs (OSDI 2012) GraphX: Unifying Data-Parallel and Graph-Parallel Analytics Neo4j Graph Data Science docs (PageRank / WCC) Apache Spark GraphX / GraphFrames official docs Call to Action (CTA) Start with one existing graph job and do one \u0026ldquo;model rewrite\u0026rdquo;:\nexpress the job as state + aggregation + propagation; define clear round stop conditions; record active-vertex ratio and cross-partition message volume per round. After these three steps, you will clearly see whether your bottleneck is algorithm, partitioning, or execution model.\n","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/110-graph-computation-models-pregel-gas-parallel-bfs/","summary":"A systematic walkthrough of Pregel (BSP) and GAS (Gather-Apply-Scatter), focused on execution paths, convergence strategies, and engineering trade-offs for PageRank, Connected Components, and parallel BFS.","title":"Practical Graph Computation Models: How Pregel (BSP) and GAS Run PageRank/CC/Parallel BFS"},{"content":" Subtitle / Abstract\nGraph partitioning is not a minor offline preprocessing trick. It is a major production performance lever in graph databases: partition incorrectly, and both query latency and network traffic go out of control. Using the ACERS template, this article explains the trade-offs of Edge-cut vs Vertex-cut, the multilevel intuition behind METIS, and the metrics that actually matter in engineering.\nEstimated reading time: 18-22 minutes Tags: graph partitioning, Edge-cut, Vertex-cut, METIS SEO keywords: Graph Partitioning, Edge-cut, Vertex-cut, METIS, Query Latency Meta description: From objective functions to engineering metrics, understand how graph partitioning affects query latency and network communication, with runnable code and tuning steps. Target Audience Backend engineers building graph databases, graph computing platforms, risk-control graphs, or recommendation graphs Performance engineers who need to diagnose \u0026ldquo;slow queries\u0026rdquo; at the partitioning layer Algorithm engineers who want to move from concept-level understanding to production implementation Background / Motivation In relational databases, performance is often improved with indexes, join reordering, and cache hit optimization. In graph databases, cross-machine edges are often the first bottleneck.\nWhen a query path frequently crosses partitions, it triggers:\nRemote RPC round trips (RTT) Remote subgraph fetching and deserialization Multi-partition coordination and result merge overhead So in production, graph partitioning directly impacts two core metrics:\nQuery latency (p95/p99) Network communication volume (bytes/s, cross-partition messages) In one sentence: in production graph databases, partitioning algorithms are not optional optimization, they are a foundational capability.\nCore Concepts Graph Partitioning: split a graph into k partitions while minimizing inter-partition coupling and maintaining load balance. Edge-cut: minimize the number of cross-partition edges, with each vertex assigned to a single partition. Vertex-cut: partition by edges and allow vertices to be replicated across partitions; useful for reducing skew caused by hot edges. Balance Constraint: partition load must not skew too much; common constraints include |V_i| \u0026lt;= (1+ε)|V|/k or edge-load constraints. METIS (core idea): a multilevel flow (Coarsen -\u0026gt; Initial Partition -\u0026gt; Uncoarsen + Refine) that reduces global search cost by \u0026ldquo;coarsen first, refine later.\u0026rdquo; Quick Orientation Map (60-120 Seconds) Problem shape: split a large graph into k partitions while minimizing cross-machine access and keeping load balanced. One-line core: choose an objective first (Edge-cut/Vertex-cut), then solve an initial partition with a multilevel method and do incremental corrections. When to use / avoid: static or slowly changing graphs fit offline baseline partitioning; high-frequency dynamic graphs need incremental rebalancing support. Complexity glance: optimal partitioning is combinatorially hard; engineering relies on approximate algorithms plus a monitoring feedback loop. Common failure mode: optimizing only cut while ignoring balance can make p99 worse. Master Mental Model Core abstraction: graph partitioning is a constrained graph-cut optimization problem. Problem family: combinatorial optimization + local search + multi-objective trade-offs (communication, latency, load). Isomorphism with known templates: Offline stage resembles multilevel coarse-to-fine optimization. Online stage resembles local hill-climbing with budget-limited migrations. Feasibility and Lower-Bound Intuition For densely connected graphs without clear community boundaries, the theoretical lower bound of cut will not be very low. When query templates inherently cross communities (for example, cross-domain risk-control paths), even perfect partitioning cannot reduce cross-machine access to zero. Under strong power-law degree distributions (a few ultra-high-degree nodes), pure Edge-cut faces a hotspot lower bound: You may reduce cut, but it is hard to flatten hotspots at the same time. Counterexample:\nIf one supernode connects to 100k edges and traffic concentrates around it, forcing strict non-replication leads to severe skew in one partition. In this case, Vertex-cut is often more realistic than Edge-cut.\nProblem Modeling and Constraint Scale In practice, explicitly decompose the objective:\n\\[ \\text{Score} = \\alpha \\cdot \\text{CutCost} + \\beta \\cdot \\text{ImbalanceCost} + \\gamma \\cdot \\text{HotspotCost} \\]\nWhere:\nCutCost: number of cross-partition edges or weighted cross-edge sum ImbalanceCost: penalty for deviation from target partition capacity HotspotCost: local congestion penalty caused by hot vertices or edges α,β,γ: business weights (derived from SLA) Scale recommendations (starting points, not hard standards):\nTens of millions of vertices and hundreds of millions of edges: prioritize offline multilevel partitioning + periodic recalibration As partition count k increases: check network bottlenecks first, then single-machine bottlenecks; avoid blindly increasing partitions Scan ε (load slack) typically from 0.03 to 0.10 A — Algorithm (Problem and Algorithm) Problem Restatement (Engineering Form) Given a large graph G=(V,E), split it into k partitions such that:\nPartition load is as balanced as possible; Frequently traversed query edges stay inside partitions; Network communication is minimized; Hot vertices are handled with controllable strategy (avoid blowing up one machine). Inputs and Outputs Name Type Description G graph Production graph (optionally weighted) k int Number of partitions obj enum Objective: Edge-cut or Vertex-cut constraint config Load-balance threshold, hotspot threshold return part(v) / part(e) Mapping of vertex or edge to partition Example (8 Nodes, 2 Partitions) Community A: 0-1-2-3-0 Community B: 4-5-6-7-4 Bridge edges: (1,4), (2,5), (3,6) If cut by communities: P0={0,1,2,3}, P1={4,5,6,7}, Edge-cut = 3 If cut randomly: Edge-cut is often \u0026gt;= 6 This is exactly where \u0026ldquo;query latency can differ by more than 2x\u0026rdquo;: more cross-partition edges make distributed query loops far more likely.\nDeriving the Approach (From Brute Force to Practical) Naive Brute Force Enumerate all partition assignments, then compute cut and balance Exponential complexity, not deployable Key Observations Production graphs are usually sparse but huge, so we need approximate optimality rather than global optimality Most gains come from: reducing cross-partition edges avoiding hot partitions Algorithm names are not first priority; objective + constraints + metric feedback loop are. Method Selection Edge-cut track: common for OLTP graph queries, short paths, k-hop lookup Vertex-cut track: more stable when ultra-high-degree vertices are obvious (celebrity vertices, super accounts) METIS idea: one of the industrial default choices for offline baseline partitioning C — Concepts (Core Ideas) 1) Edge-cut vs Vertex-cut Edge-cut (Unique Vertex Ownership) Objective (simplified):\n[ \\min \\sum_{(u,v)\\in E} [part(u) \\neq part(v)] ]\nPros: intuitive model, simple query routing Cons: supernodes can pull large numbers of edges into cross-partition traffic Vertex-cut (Edge Ownership, Vertex Replication) A common metric is replication factor:\n[ RF = \\frac{1}{|V|}\\sum_{v\\in V} |A(v)| ]\nWhere A(v) is the partition set containing vertex v. Lower RF is better.\nPros: can spread high-degree vertex edges across machines Cons: more complex replica consistency and read/write path 2) METIS Multilevel Intuition (Must Understand) The core of METIS is not one magical formula, but a three-stage flow:\nCoarsening: heavy-edge matching to shrink the graph Initial Partition: quickly generate an initial split on the small graph Uncoarsen + Refine: project back layer by layer and reduce cut with FM/KL-like local optimization Engineering value: turn one huge hard problem into many small corrections, usually more stable than greedy partitioning on the original graph.\nDeepening Focus (PDKH) This article deeply expands two concepts:\nConcept A: Mapping Edge-cut objective to query latency Concept B: METIS multilevel partition workflow Concept A: Edge-cut -\u0026gt; Latency Problem Reframe: partition quality essentially compresses cross-machine hops. Minimal Example: under the same query template, Edge-cut=3 vs Edge-cut=7 can roughly double cross-machine requests. Invariant: under valid load constraints, reducing cross-partition edges does not increase expected remote hops. Formalization: latency ≈ local_cpu + remote_rtt * cross_hops + deserialize_cost cross_hops is highly correlated with cut ratio. Correctness Sketch: with a fixed query template, fewer cross-partition edges means fewer boundary events triggering remote access. Threshold: when cut_ratio \u0026gt; 0.25, many online graph queries show clear p99 degradation (empirical threshold; calibrate per business). Failure Mode: minimizing cut without load control creates hot partitions and may reduce overall throughput. Engineering Reality: cut must be evaluated together with partition load and hotspot degree distribution; do not drive by one metric. Concept B: METIS Multilevel Flow Problem Reframe: not \u0026ldquo;solve once,\u0026rdquo; but \u0026ldquo;coarsen, solve coarse, then refine layer by layer.\u0026rdquo; Minimal Example: shrink a 10M-edge graph to 200k edges, partition, then replay refinements. Invariant: each refinement accepts only migrations that reduce objective or keep constraints balanced. Formalization: Coarsen -\u0026gt; Partition -\u0026gt; Uncoarsen/Refine. Correctness Sketch: not globally optimal, but monotonic local improvements ensure non-degrading objective. Threshold: larger graphs and clearer community structure usually yield more stable multilevel gains. Failure Mode: if graph changes too fast, offline partitioning ages quickly and gains decay. Engineering Reality: must pair with incremental rebalance (periodic repartition + hotspot migration). Practical Guide / Steps Define objective first: choose Edge-cut or Vertex-cut before choosing algorithm names. Define constraints: partition capacity, hotspot threshold, migration budget. Get offline baseline partition: use METIS-style multilevel ideas. Observe online metrics: cut_ratio, RF, p95/p99, cross-partition bytes. Do local rebalance: migrate in small steps based on hotspot and cross-edge contribution; avoid full repartition. Run regression validation: benchmark representative query templates, not only one-time batch stats. Selection Guide Choose objective by degree distribution: Smooth degree distribution: try Edge-cut first. Strong power-law: prioritize Vertex-cut evaluation. Choose objective by query type: Shortest path / local subgraph reads: Edge-cut is easier for routing optimization. Batch traversal / message propagation: Vertex-cut is often more stable under hotspot pressure. Choose strategy by machine memory: Tight memory: reduce replication, use Vertex-cut cautiously. Relatively ample memory: replication can be used to trade for throughput stability. Choose cadence by migration budget: Low migration budget: local incremental correction. Acceptable maintenance window: offline repartition + incremental backfill. Runnable Example (Python) Below is a runnable local-search example with \u0026ldquo;balance constraints + cut cost\u0026rdquo; (for understanding objectives, not a full METIS implementation):\nfrom collections import defaultdict from typing import Dict, List, Tuple Edge = Tuple[int, int] def edge_cut(edges: List[Edge], part: Dict[int, int]) -\u0026gt; int: return sum(1 for u, v in edges if part[u] != part[v]) def partition_sizes(part: Dict[int, int], k: int) -\u0026gt; List[int]: sizes = [0] * k for node in part: sizes[part[node]] += 1 return sizes def greedy_balanced_partition( nodes: List[int], edges: List[Edge], k: int, max_imbalance: float = 0.10, max_iter: int = 20, ) -\u0026gt; Dict[int, int]: part = {node: node % k for node in nodes} limit = int((1.0 + max_imbalance) * len(nodes) / k) + 1 adj = defaultdict(list) for u, v in edges: adj[u].append(v) adj[v].append(u) for _ in range(max_iter): improved = False sizes = partition_sizes(part, k) for node in nodes: current = part[node] best_part = current best_gain = 0 for candidate in range(k): if candidate == current: continue if sizes[candidate] + 1 \u0026gt; limit: continue # Estimate cut change if node is moved (positive means lower cut). gain = 0 for nei in adj[node]: before_cross = 1 if part[nei] != current else 0 after_cross = 1 if part[nei] != candidate else 0 gain += (before_cross - after_cross) if gain \u0026gt; best_gain: best_gain = gain best_part = candidate if best_part != current: sizes[current] -= 1 sizes[best_part] += 1 part[node] = best_part improved = True if not improved: break return part def main() -\u0026gt; None: nodes = list(range(8)) edges = [ (0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (1, 4), (2, 5), (3, 6), ] k = 2 init_part = {node: node % k for node in nodes} init_cut = edge_cut(edges, init_part) opt_part = greedy_balanced_partition(nodes, edges, k=k) opt_cut = edge_cut(edges, opt_part) print(\u0026#34;init part:\u0026#34;, init_part, \u0026#34;cut=\u0026#34;, init_cut) print(\u0026#34;opt part :\u0026#34;, opt_part, \u0026#34;cut=\u0026#34;, opt_cut) if __name__ == \u0026#34;__main__\u0026#34;: main() Run:\npython3 graph_partition_demo.py Runnable Example 2: Vertex-cut Replication Factor Estimation from collections import defaultdict from typing import Dict, List, Tuple Edge = Tuple[int, int] def replication_factor(edges: List[Edge], edge_part: Dict[Edge, int], n_nodes: int) -\u0026gt; float: node_parts = defaultdict(set) for (u, v), p in edge_part.items(): node_parts[u].add(p) node_parts[v].add(p) total = sum(len(node_parts[node]) if node in node_parts else 1 for node in range(n_nodes)) return total / n_nodes def main() -\u0026gt; None: # Simplified example: 3 partitions. edges = [(0, 1), (0, 2), (0, 3), (4, 5), (5, 6), (6, 7), (3, 4)] edge_part = { (0, 1): 0, (0, 2): 1, (0, 3): 2, (4, 5): 1, (5, 6): 1, (6, 7): 1, (3, 4): 2, } rf = replication_factor(edges, edge_part, n_nodes=8) print(\u0026#34;replication factor =\u0026#34;, round(rf, 3)) if __name__ == \u0026#34;__main__\u0026#34;: main() This example is meant to visualize RF trend shifts: on the same graph, node replication overhead can differ significantly under different partitioning strategies.\nExplanation and Principles (Why This Works) The key value of this example is to make partition quality measurable:\nYou can directly observe how much cut is reduced; You can add business query weights to give critical edges higher importance; You can tighten balance constraints and observe the latency/throughput turning point. In real production, METIS performs a more systematic \u0026ldquo;coarsen + replay refinement\u0026rdquo; at larger scales, but the underlying idea is still:\nHave an objective; Have constraints; Have observable metric feedback. Worked Example (Trace) Below is a simplified trace of whether a partition migration is worthwhile:\nInitial: cut_ratio = 0.29, p99 = 410ms, cross_bytes = 1.8GB/min Candidate migration: move a 20k-node subgraph from P3 to P5 Estimated benefit: cut_ratio -\u0026gt; 0.23, P5 CPU +5%, P3 CPU -8% Observed after execution:\nHour 1: p99 drops to 330ms, cross_bytes drops to 1.3GB/min Hour 6: P5 load stabilizes, no hotspot alarm Hour 24: peak p99 remains stable at 300~320ms Conclusion: if load stays within threshold after migration, reducing cross edges usually yields stable latency gains.\nCorrectness (Proof Sketch) This does not prove global optimality; it proves monotonic improvement of local migration strategy:\nInvariant: every migration must satisfy capacity and hotspot constraints. Preservation: accept migration only if Score decreases (or equal score but more stable). Termination: local search stops when no candidate migration can further reduce Score. So you get at least a constraint-satisfying local optimum, rather than uncontrolled random fluctuation.\nComplexity and Thresholds Offline multilevel methods are often approximately linear to sublinear scalable (depends on implementation and graph structure). Online local migration cost per round depends on candidate set size |C| and incremental evaluation cost. In engineering, thresholds matter more than Big-O: Migration window per round (for example, 5-15 minutes) Migration budget per round (for example, at most 0.5% of vertices) Rollback threshold (for example, rollback if p99 rises for 5 consecutive minutes) Constant Factors and Engineering Reality Serialization cost: cross-machine edges force object decode, often with high constant factors. Cache locality: concentrated local subgraphs can significantly affect the upside via cache hit rate. Batch window: if offline repartitioning exceeds maintenance windows, theoretical gains can be nullified. Replica consistency: Vertex-cut write paths are more complex; be careful in mixed read-write workloads. Production Troubleshooting Checklist (Required) After partition rollout, do not stop at \u0026ldquo;average latency decreased.\u0026rdquo; Run a 24-hour replay checklist:\nDo the four core metrics improve in the same direction?\nAre p95/p99 lower? Are cross-partition bytes lower? Are cut_ratio or RF moving toward target? Did per-partition CPU/memory stay under alert thresholds? Is distribution being hidden by averages?\nDid top 10 slow query templates actually improve? Did long-tail queries regress? Is trend consistent during peak traffic windows? Are migration side effects controlled?\nAny write jitter during migration windows? Did cache hit rate dip below guardrails? Has rollback script been drill-tested (at least once)? Did hotspot partitions drift?\nIs today’s hottest partition the same as yesterday’s? Did hotspot just move from one machine to another? Is a dedicated hot-vertex strategy needed? Are capacity boundaries exposed early?\nUnder 7-day edge growth forecast, can current k still hold? Will replication-factor growth break memory budget? Should partition expansion windows be reserved early? To make troubleshooting reusable, log partition changes in structured form:\n{ \u0026#34;change_id\u0026#34;: \u0026#34;part-2026-02-09-01\u0026#34;, \u0026#34;strategy\u0026#34;: \u0026#34;edge_cut_with_balance\u0026#34;, \u0026#34;before\u0026#34;: {\u0026#34;cut_ratio\u0026#34;: 0.27, \u0026#34;p99_ms\u0026#34;: 380, \u0026#34;cross_bytes_mb_min\u0026#34;: 1540}, \u0026#34;after\u0026#34;: {\u0026#34;cut_ratio\u0026#34;: 0.21, \u0026#34;p99_ms\u0026#34;: 305, \u0026#34;cross_bytes_mb_min\u0026#34;: 1090}, \u0026#34;risk\u0026#34;: {\u0026#34;hot_partition_cpu_max\u0026#34;: 0.72, \u0026#34;rollback_ready\u0026#34;: true} } This structured record is critical for postmortems: it answers \u0026ldquo;why it worked,\u0026rdquo; \u0026ldquo;whether it is repeatable,\u0026rdquo; and \u0026ldquo;how to do it more safely next time.\u0026rdquo;\nMetric Definitions (Avoid Team Misalignment) Many partition discussions fail not because algorithms are weak, but because metric definitions differ. Standardize these:\ncut ratio\nDefinition: #cross-partition edges / #total edges Definition scope: report both on \u0026ldquo;active-subgraph edges\u0026rdquo; and \u0026ldquo;full-graph edges\u0026rdquo; separately cross-partition bytes\nDefinition: total network bytes from cross-partition requests Scope: split read and write paths; read-heavy workloads should prioritize read-path metrics partition hotspot index\nDefinition: max_partition_qps / avg_partition_qps Scope: compute on both 1-minute and 5-minute windows to capture jitter and trend replication factor (Vertex-cut only)\nDefinition: average number of replicas per vertex Scope: separately compute on online-active vertices to avoid risk dilution by cold data With fixed definitions for these four metrics, partition optimization becomes an auditable engineering process instead of intuition debate.\nReplay Benchmark Template (Python) import csv import statistics from dataclasses import dataclass from typing import List @dataclass class QuerySample: template: str latency_ms: float cross_bytes: int cross_hops: int def load_samples(path: str) -\u0026gt; List[QuerySample]: result: List[QuerySample] = [] with open(path, \u0026#34;r\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as f: reader = csv.DictReader(f) for row in reader: result.append( QuerySample( template=row[\u0026#34;template\u0026#34;], latency_ms=float(row[\u0026#34;latency_ms\u0026#34;]), cross_bytes=int(row[\u0026#34;cross_bytes\u0026#34;]), cross_hops=int(row[\u0026#34;cross_hops\u0026#34;]), ) ) return result def p99(values: List[float]) -\u0026gt; float: if not values: return 0.0 values_sorted = sorted(values) idx = int(0.99 * (len(values_sorted) - 1)) return values_sorted[idx] def summarize(samples: List[QuerySample]) -\u0026gt; None: latency = [item.latency_ms for item in samples] cross_bytes = [item.cross_bytes for item in samples] cross_hops = [item.cross_hops for item in samples] print(\u0026#34;count =\u0026#34;, len(samples)) print(\u0026#34;avg_latency_ms =\u0026#34;, round(statistics.mean(latency), 2)) print(\u0026#34;p99_latency_ms =\u0026#34;, round(p99(latency), 2)) print(\u0026#34;avg_cross_bytes =\u0026#34;, int(statistics.mean(cross_bytes))) print(\u0026#34;avg_cross_hops =\u0026#34;, round(statistics.mean(cross_hops), 3)) if __name__ == \u0026#34;__main__\u0026#34;: baseline = load_samples(\u0026#34;baseline.csv\u0026#34;) candidate = load_samples(\u0026#34;candidate.csv\u0026#34;) print(\u0026#34;baseline\u0026#34;) summarize(baseline) print(\u0026#34;candidate\u0026#34;) summarize(candidate) This script is suitable for minimal before/after replay comparison: same templates, same inputs, unified metrics, no guesswork conclusions.\nE — Engineering (Applications) Scenario 1: Online Graph Queries (Edge-cut Dominant) Problem: high p99 on k-hop/path queries.\nApproach: prioritize reducing cross-partition edges along common query boundaries while maintaining load balance.\nBenefit: fewer cross-machine hops, more stable p95/p99.\nGoal: cut_ratio from 0.31 -\u0026gt; 0.18 Result: path query p99 from 420ms -\u0026gt; 230ms (example measurement) Scenario 2: Supernode Graphs (Vertex-cut More Stable) Problem: a few vertices have extremely high out-degree, causing severe single-machine hotspots under Edge-cut.\nApproach: edge partitioning + controlled vertex replication with RF monitoring.\nBenefit: spreads hotspot writes/traversals across partitions.\nScenario 3: Sharding and Capacity Planning (METIS Baseline + Incremental Migration) Problem: full repartition is expensive; business cannot tolerate frequent downtime migrations.\nApproach: periodically recompute offline baseline, migrate only high-benefit candidate subgraphs online.\nBenefit: continuously improve partition quality within migration budget.\nR — Reflection (Deep Dive) Complexity and Engineering Cost Partitioning is combinatorially hard; pursuing global optimum is unrealistic. Engineering should focus on sustainable optimization loops: baseline exists monitoring exists incremental repair exists Alternatives and Trade-offs Strategy Pros Cons Best for Edge-cut Simple query routing Supernodes can hotspot OLTP graph queries Vertex-cut Better hotspot control Replica consistency complexity Power-law graphs Random sharding Simple implementation High communication cost Early PoC only Quantitative Comparison (Example) Metric Strategy A (random) Strategy B (Edge-cut) Strategy C (Vertex-cut) cut ratio 0.34 0.19 0.22 RF 1.00 1.00 1.38 query p99 480ms 260ms 290ms network bytes 2.1GB/min 1.2GB/min 1.0GB/min Interpretation:\nEdge-cut is cleaner on read paths; Vertex-cut can be better for hotspot and network bytes, but requires replica-management cost; Real choice depends on read/write ratio and consistency requirements. Common Pitfalls Looking only at algorithm names, not objective functions: often leads to \u0026ldquo;great theory, poor production metrics.\u0026rdquo; Optimizing cut only, ignoring balance: latency drops but throughput collapses. One-time offline partition only: effects naturally decay in dynamic-graph settings. Counterexample (Must Remember) Suppose you place all hot vertices into one partition to reduce cut. Communication drops short-term, but that partition’s CPU spikes and queuing worsens p99.\nThis shows: partition optimization is multi-objective, not single-objective extreme optimization.\nFAQ and Caveats Can METIS directly solve online dynamic repartitioning?\nNo. METIS is better as an offline baseline; online operation requires incremental migration strategies.\nIs Edge-cut always better than Vertex-cut?\nNo. Under extreme high-degree imbalance, Vertex-cut is often more stable.\nHow to decide when to repartition?\nWatch trends, not points: rising cut_ratio, rising cross-partition bytes, rising p99 over sustained windows.\nHow to choose partition count k?\nSet an upper bound by machine budget first, then benchmark the joint curve of k vs p95/p99 and communication volume to find turning points.\nBest Practices and Recommendations Define main query paths first, then define partition objective Use business-weighted edges, not uniform edge weights Set migration budget ceilings per batch to avoid global jitter Monitor cut_ratio, RF, p99, and network bytes together; avoid single-metric decisions Prepare dedicated strategies for hot vertices (replication, side indexes, or caching) Migration Path (Skill Ladder) After mastering this article, progress in this order:\nDynamic incremental partitioning: migrate only high-benefit local subgraphs Query-aware partitioning: include query logs in partition weight modeling Multi-tier graph storage coordination: co-optimize partitioning with hot/cold tiering and cache strategy Online A/B validation framework: make partition strategies rollbackable, comparable, and auditable S — Summary Graph partitioning directly determines latency ceiling and network cost in graph databases. Edge-cut and Vertex-cut have no universal winner; workload shape decides. METIS’s core value is \u0026ldquo;multilevel scaling + local refinement,\u0026rdquo; not one-shot global optimality. A production-ready partition strategy must include objective, constraints, monitoring, and incremental repair. Closing Conclusion One major engineering difference between graph databases and relational databases is that cross-edge communication can directly consume your performance budget.\nOnly by building robust partitioning capability can query performance move from \u0026ldquo;occasionally usable\u0026rdquo; to \u0026ldquo;predictably stable.\u0026rdquo;\nReferences and Further Reading METIS docs and paper: Karypis \u0026amp; Kumar, Multilevel k-way Partitioning Scheme PowerGraph (classic Vertex-cut engineering practice) Pregel / Giraph distributed graph computation models Neo4j / JanusGraph sharding and query practice materials Multi-Language Reference Implementations (Excerpt) C++: Compute Edge-cut #include \u0026lt;vector\u0026gt; #include \u0026lt;utility\u0026gt; int edgeCut(const std::vector\u0026lt;std::pair\u0026lt;int, int\u0026gt;\u0026gt;\u0026amp; edges, const std::vector\u0026lt;int\u0026gt;\u0026amp; part) { int cut = 0; for (const auto\u0026amp; edge : edges) { int u = edge.first; int v = edge.second; if (part[u] != part[v]) { cut += 1; } } return cut; } Go: Compute Partition Load package main func partitionSizes(part []int, k int) []int { sizes := make([]int, k) for _, partition := range part { sizes[partition]++ } return sizes } JavaScript: Compute Replication Factor function replicationFactor(edgeParts, nodeCount) { const nodeToParts = Array.from({ length: nodeCount }, () =\u0026gt; new Set()); for (const item of edgeParts) { const [u, v, p] = item; nodeToParts[u].add(p); nodeToParts[v].add(p); } let total = 0; for (const parts of nodeToParts) total += Math.max(parts.size, 1); return total / nodeCount; } Meta Information Reading time: 18-22 minutes Tags: graph partitioning, Edge-cut, Vertex-cut, METIS SEO keywords: Graph Partitioning, Edge-cut, Vertex-cut, METIS, Query Latency Meta description: How graph partitioning affects query latency and network communication, with runnable examples and an engineering tuning path. Call to Action (CTA) Choose one of your slowest online query templates, measure cross-partition hops and network bytes, then compare p95/p99 before and after one partition optimization pass.\nYou will quickly see: partition strategy is the core lever in graph-database performance engineering.\n","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/100-graph-partitioning-edge-cut-vertex-cut-metis/","summary":"Starting from Edge-cut/Vertex-cut objective functions, this article systematically explains METIS-style multilevel partitioning and production implementation, with emphasis on how partitioning affects query latency and cross-machine traffic.","title":"Graph Partitioning Algorithms: Edge-cut vs Vertex-cut and an Engineering Guide to METIS"},{"content":" Subtitle / Abstract\nIn dynamic-graph workloads, the real pain point is not \u0026ldquo;do you know the algorithm,\u0026rdquo; but \u0026ldquo;can the system survive continuous updates.\u0026rdquo; Following the ACERS template, this article explains three engineering essentials: incremental shortest path, incremental PageRank, and connectivity maintenance, along with three practical strategies: local recomputation, lazy updates, and approximate results.\nEstimated reading time: 14-18 minutes Tags: dynamic graph, incremental computation, shortest path, PageRank, connectivity maintenance SEO keywords: dynamic graph, incremental shortest path, incremental PageRank, connectivity maintenance, local recomputation, lazy updates, approximate results Meta description: An engineering guide to dynamic graphs: how to control latency and cost in high-frequency update scenarios with incremental algorithms and practical system strategies. Target Audience Engineers building online services for graph databases, relationship graphs, and recommendation graphs Developers moving from offline graph computation to real-time incremental computation Tech leads who want to replace \u0026ldquo;full recomputation\u0026rdquo; with a production-ready update pipeline Background / Motivation Static graph algorithms look elegant in papers, but real production graphs are constantly changing:\nUser relations are added/removed Transaction edges continuously stream in Content graphs and knowledge graphs are continuously updated This is where 80% of engineering pain comes from:\nFull recomputation is too slow to keep up with update velocity Strong online consistency is too expensive and blows up P99 latency The business only needs \u0026ldquo;usable approximation\u0026rdquo; but teams implement \u0026ldquo;expensive exactness\u0026rdquo; So the core question becomes:\nIt is not how to compute an answer once, but how to keep computing under an update stream.\nCore Concepts Concept Meaning Engineering Focus Incremental shortest path After edge/node updates, repair only affected regions Impact-domain detection, local recomputation Incremental PageRank Local residual propagation after graph updates Residual threshold, batch window Connectivity maintenance Dynamically maintain connectivity / component changes Fast insertion, hard deletion Local recomputation Recompute only affected subgraphs Lower CPU/memory cost Lazy updates Merge updates into batches for unified processing Throughput first, controllable latency Approximate results Trade error bounds for compute cost SLA vs precision balance A — Algorithm (Problem and Algorithm) Problem Restatement (Engineering Form) Given a continuously updated graph G_t=(V_t,E_t) and an operation stream:\nadd_edge(u,v,w) remove_edge(u,v) query_shortest_path(s,t) query_pagerank_topk(k) query_connected(u,v) Maintain query results at low cost under sustained updates.\nInputs and Outputs Name Type Description graph adjacency list / CSR Graph structure updates update stream Edge insertions, deletions, weight changes queries query stream Path, ranking, connectivity return query result Path distance / ranking / boolean connectivity Example 1: Incremental Shortest Path Initial: A-\u0026gt;B(1), B-\u0026gt;C(1), A-\u0026gt;C(5) Shortest path A-\u0026gt;C = 2 Update: A-\u0026gt;C weight drops to 1 Only local repair in A/C neighborhood is needed, shortest path becomes 1 Example 2: Connectivity Update The graph has two components G1, G2 Add edge x(G1)-y(G2) The connectivity structure should quickly reflect \u0026#34;component merge\u0026#34; Deriving the Approach (From Full to Incremental) Naive Approach: Full Recompute After Every Update Shortest path: full-graph Dijkstra / APSP PageRank: iterate on full graph until convergence Connectivity: full-graph BFS/DFS relabeling Problem: cost explodes under frequent updates.\nKey Observations Most updates only affect local subgraphs Queries usually tolerate short eventual-consistency windows Ranking/recommendation systems often accept controlled error Method Selection Local recomputation: prioritize shrinking the affected region Lazy updates: merge high-frequency small updates into batches Approximate results: set an error threshold to trade for throughput C — Concepts (Core Ideas) 1) Incremental Shortest Path For edge insertion/weight decrease: trigger local relaxation from affected endpoints For edge deletion/weight increase: detect invalid shortest paths and rebuild local trees (harder) Common engineering practice:\nProcess \u0026ldquo;shortening\u0026rdquo; updates online Route \u0026ldquo;lengthening/deletion\u0026rdquo; updates into an async repair queue 2) Incremental PageRank Maintain both rank and residual On edge updates, propagate residual only around affected nodes Stop propagation when residual is below threshold 3) Connectivity Maintenance Insert-only edges: Union-Find is very efficient With edge deletions: needs more complex dynamic connectivity structures; in practice teams often use a compromise of \u0026ldquo;hierarchical rebuild + batch processing\u0026rdquo; Real-World Conclusion (Core) Most production systems do not do \u0026ldquo;fully exact full recomputation on every update.\u0026rdquo;\nA typical solution is: local recomputation + lazy updates + approximate results.\nPractical Guide / Steps Step 1: Separate Update and Query Paths Queries read from \u0026ldquo;published snapshots\u0026rdquo; Updates are appended to an \u0026ldquo;incremental log\u0026rdquo; and applied asynchronously Step 2: Define the Affected Region Shortest path: radius expansion from updated edge endpoints as seeds PageRank: residual propagation from updated nodes Connectivity: record affected components and calibrate asynchronously Step 3: Runnable Python Skeleton from collections import defaultdict, deque import heapq class DynamicGraphEngine: def __init__(self): self.g = defaultdict(dict) # g[u][v] = w self.pending = deque() # update log def add_edge(self, u, v, w=1.0): self.pending.append((\u0026#34;add\u0026#34;, u, v, w)) def remove_edge(self, u, v): self.pending.append((\u0026#34;del\u0026#34;, u, v, None)) def flush_updates(self, budget=1000): \u0026#34;\u0026#34;\u0026#34;Deferred updates: apply in batches under a budget cap.\u0026#34;\u0026#34;\u0026#34; cnt = 0 while self.pending and cnt \u0026lt; budget: op, u, v, w = self.pending.popleft() if op == \u0026#34;add\u0026#34;: self.g[u][v] = w else: self.g[u].pop(v, None) cnt += 1 def shortest_path_local(self, s, t, max_hops=8): \u0026#34;\u0026#34;\u0026#34;Local recomputation example: bound expansion depth/state size.\u0026#34;\u0026#34;\u0026#34; pq = [(0.0, 0, s)] # dist, hops, node dist = {s: 0.0} while pq: d, h, u = heapq.heappop(pq) if u == t: return d if h \u0026gt;= max_hops: continue if d != dist.get(u): continue for v, w in self.g[u].items(): nd = d + w if nd \u0026lt; dist.get(v, float(\u0026#34;inf\u0026#34;)): dist[v] = nd heapq.heappush(pq, (nd, h + 1, v)) return float(\u0026#34;inf\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: eng = DynamicGraphEngine() eng.add_edge(\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, 1) eng.add_edge(\u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;, 1) eng.add_edge(\u0026#34;A\u0026#34;, \u0026#34;C\u0026#34;, 5) eng.flush_updates() print(eng.shortest_path_local(\u0026#34;A\u0026#34;, \u0026#34;C\u0026#34;)) # 2 eng.add_edge(\u0026#34;A\u0026#34;, \u0026#34;C\u0026#34;, 1) eng.flush_updates() print(eng.shortest_path_local(\u0026#34;A\u0026#34;, \u0026#34;C\u0026#34;)) # 1 E — Engineering (Applications) Scenario 1: Online Shortest-Chain Query in Social Graphs Background: The user relationship graph changes continuously; query \u0026ldquo;the shortest relationship chain between you and someone.\u0026rdquo;\nWhy it fits: Shortest-path updates are strongly local, so local recomputation plus depth capping works well.\n// Pseudocode: run bidirectional BFS only within maxDepth at query time. // Return an approximate hop count online; complete exact path asynchronously. Scenario 2: Incremental PageRank on Recommendation Graphs Background: Content edges and click edges keep changing; rankings must refresh continuously.\nWhy it fits: Incremental PageRank propagates only affected residual and avoids full iterations.\n# Core idea: inject residual to updated nodes, then push locally to epsilon. # Stop propagating when residual \u0026lt; epsilon. Scenario 3: Connectivity Alerting in Transaction Graphs Background: New transaction edges continuously arrive, and the system must quickly detect whether suspicious groups are connected.\nWhy it fits: Use Union-Find for fast union on insertions; put deletions into a lazy verification queue.\nclass DSU { constructor(n) { this.p = Array.from({length:n}, (_,i)=\u0026gt;i); } find(x){ return this.p[x]===x?x:(this.p[x]=this.find(this.p[x])); } union(a,b){ this.p[this.find(a)] = this.find(b); } connected(a,b){ return this.find(a)===this.find(b); } } R — Reflection (Deeper Thinking) Complexity and Cost Module Full recomputation Incremental strategy Shortest path High (full graph) Medium (affected region) PageRank High (multi-round full-graph iteration) Medium (local residual push) Connectivity Medium-high (deletions are hard) Low for insertions, deletions need compromise Alternative Strategies Strong-consistency full recomputation\nPros: exact results Cons: low throughput, high cost Weak-consistency incremental + async repair (mainstream)\nPros: stable online performance Cons: approximate error exists in short windows Pure online approximation + periodic full correction\nPros: strong real-time behavior Cons: requires error monitoring and backfill mechanisms Why This Is the Most Practical Engineering Path Naturally compatible with update streams Keeps latency and cost within budget Supports gradual evolution from \u0026ldquo;usable\u0026rdquo; to \u0026ldquo;more precise\u0026rdquo; Explanation and Principles (Why This Works) In dynamic graphs, algorithm problems often degrade into system problems:\nYou cannot stop updates from arriving You cannot perform perfect recomputation every time You must make explainable trade-offs among correctness, latency, and cost So \u0026ldquo;local recomputation, lazy updates, and approximate results\u0026rdquo; is not a temporary workaround, but a primary design principle.\nFAQ and Caveats When is full recomputation mandatory?\nWhen accumulated error exceeds threshold, or a critical business window requires high precision.\nWhy are edge deletions always harder?\nBecause they can invalidate existing optimal structures and require rollback/rebuild.\nHow do we explain approximate results to the business?\nClearly define error bounds and refresh periods, and provide an eventual-consistency commitment.\nHow do we avoid update storms overwhelming the system?\nSet batch windows, backpressure policies, and query degradation paths.\nBest Practices and Recommendations Define SLA first, then choose exact vs approximate strategy Decouple updates and queries: logged increments + snapshot serving Maintain a \u0026ldquo;recompute budget\u0026rdquo; per algorithm: time, node count, error threshold Must-have observability: update backlog, recomputation hit rate, error drift S — Summary Core Takeaways The real challenge in dynamic graph engineering is the update stream, not a single query Incremental shortest path, incremental PageRank, and connectivity maintenance are the three foundational capabilities Local recomputation, lazy updates, and approximate results are mainstream production strategies Insertions are usually easier; deletions need async repair mechanisms Metrics monitoring and error governance are the lifeline of stable incremental systems Recommended Further Reading Dynamic Graph Algorithms (survey) Bahmani et al. Incremental PageRank at scale Holm, de Lichtenberg, Thorup (dynamic connectivity) Meta Information Reading time: 14-18 minutes Tags: dynamic graph, incremental computation, shortest path, PageRank, connectivity maintenance SEO keywords: dynamic graph, incremental shortest path, incremental PageRank, connectivity maintenance, local recomputation Meta description: Engineering guide to dynamic graph incremental computation: core algorithms, implementation strategies, and production trade-offs. Call to Action (CTA) Two practical next steps:\nSplit your current graph query service into \u0026ldquo;query snapshots + incremental update pipeline\u0026rdquo; Launch approximate mode first with error monitoring, then gradually increase precision If you want, the next article can provide a practical template for \u0026ldquo;error budgets and backfill strategy (SLA-driven).\u0026rdquo;\nMulti-Language Reference Implementations (Python / C / C++ / Go / Rust / JS) # incremental shortest path (bounded local recompute) - simplified import heapq def local_dijkstra(graph, s, t, max_nodes=1000): pq = [(0, s)] dist = {s: 0} seen = 0 while pq and seen \u0026lt; max_nodes: d, u = heapq.heappop(pq) if d != dist.get(u): continue seen += 1 if u == t: return d for v, w in graph.get(u, []): nd = d + w if nd \u0026lt; dist.get(v, 10**18): dist[v] = nd heapq.heappush(pq, (nd, v)) return float(\u0026#34;inf\u0026#34;) /* union-find for dynamic connectivity (insert-only fast path) */ #include \u0026lt;stdio.h\u0026gt; int p[1000]; int find(int x){ return p[x]==x?x:(p[x]=find(p[x])); } void uni(int a,int b){ p[find(a)] = find(b); } int main(){ for(int i=0;i\u0026lt;10;i++) p[i]=i; uni(1,2); uni(2,3); printf(\u0026#34;%d\\n\u0026#34;, find(1)==find(3)); // 1 return 0; } #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; struct DSU { vector\u0026lt;int\u0026gt; p; DSU(int n): p(n) { iota(p.begin(), p.end(), 0); } int find(int x){ return p[x]==x?x:p[x]=find(p[x]); } void unite(int a,int b){ p[find(a)] = find(b); } bool conn(int a,int b){ return find(a)==find(b); } }; int main(){ DSU d(6); d.unite(0,1); d.unite(1,2); cout \u0026lt;\u0026lt; d.conn(0,2) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 1 } package main import \u0026#34;fmt\u0026#34; type DSU struct{ p []int } func NewDSU(n int) *DSU { d:=\u0026amp;DSU{make([]int,n)}; for i:=0;i\u0026lt;n;i++{d.p[i]=i}; return d } func (d *DSU) Find(x int) int { if d.p[x]!=x { d.p[x]=d.Find(d.p[x]) }; return d.p[x] } func (d *DSU) Union(a,b int){ d.p[d.Find(a)] = d.Find(b) } func main(){ d := NewDSU(6) d.Union(1,2); d.Union(2,3) fmt.Println(d.Find(1)==d.Find(3)) // true } struct DSU { p: Vec\u0026lt;usize\u0026gt; } impl DSU { fn new(n: usize) -\u0026gt; Self { Self { p: (0..n).collect() } } fn find(\u0026amp;mut self, x: usize) -\u0026gt; usize { if self.p[x] != x { let r = self.find(self.p[x]); self.p[x] = r; } self.p[x] } fn union(\u0026amp;mut self, a: usize, b: usize) { let ra = self.find(a); let rb = self.find(b); self.p[ra] = rb; } } fn main() { let mut d = DSU::new(5); d.union(0, 1); d.union(1, 2); println!(\u0026#34;{}\u0026#34;, d.find(0) == d.find(2)); } // lazy update queue skeleton const pending = []; function addEdge(u, v, w) { pending.push({ op: \u0026#34;add\u0026#34;, u, v, w }); } function flush(graph, budget = 100) { let cnt = 0; while (pending.length \u0026amp;\u0026amp; cnt \u0026lt; budget) { const e = pending.shift(); if (e.op === \u0026#34;add\u0026#34;) { if (!graph.has(e.u)) graph.set(e.u, []); graph.get(e.u).push([e.v, e.w]); } cnt += 1; } } ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/90-dynamic-graph-incremental-computation/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\u003cbr\u003e\nIn dynamic-graph workloads, the real pain point is not \u0026ldquo;do you know the algorithm,\u0026rdquo; but \u0026ldquo;can the system survive continuous updates.\u0026rdquo; Following the ACERS template, this article explains three engineering essentials: \u003cstrong\u003eincremental shortest path, incremental PageRank, and connectivity maintenance\u003c/strong\u003e, along with three practical strategies: \u003cstrong\u003elocal recomputation, lazy updates, and approximate results\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEstimated reading time\u003c/strong\u003e: 14-18 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003edynamic graph\u003c/code\u003e, \u003ccode\u003eincremental computation\u003c/code\u003e, \u003ccode\u003eshortest path\u003c/code\u003e, \u003ccode\u003ePageRank\u003c/code\u003e, \u003ccode\u003econnectivity maintenance\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: dynamic graph, incremental shortest path, incremental PageRank, connectivity maintenance, local recomputation, lazy updates, approximate results\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: An engineering guide to dynamic graphs: how to control latency and cost in high-frequency update scenarios with incremental algorithms and practical system strategies.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-audience\"\u003eTarget Audience\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eEngineers building online services for graph databases, relationship graphs, and recommendation graphs\u003c/li\u003e\n\u003cli\u003eDevelopers moving from offline graph computation to real-time incremental computation\u003c/li\u003e\n\u003cli\u003eTech leads who want to replace \u0026ldquo;full recomputation\u0026rdquo; with a production-ready update pipeline\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eStatic graph algorithms look elegant in papers, but real production graphs are constantly changing:\u003c/p\u003e","title":"Dynamic Graphs and Incremental Computation: ACERS Guide to Incremental Shortest Path, Incremental PageRank, and Connectivity Maintenance"},{"content":" Subtitle / Abstract\nCommunity detection is not just \u0026ldquo;splitting a graph into a few groups.\u0026rdquo; In production, you must balance accuracy, interpretability, speed, and maintainability. Following the ACERS structure, this article breaks down two of the most common engineering choices: Louvain (modularity optimization) and Label Propagation (LPA).\nEstimated reading time: 12-16 minutes Tags: Community Detection, Louvain, Label Propagation, Graph Partitioning SEO keywords: community detection, Louvain, Label Propagation, modularity, graph partition Meta description: Engineering primer for community detection: principles, complexity, algorithm selection, and implementation templates for Louvain and LPA across group discovery, graph partitioning, and cold start. Target Audience Engineers working on social graphs, risk-control graphs, or recommender-system graph analytics Developers who want to move community detection from paper concepts into production workflows Practitioners modeling group structure for graph partitioning and cold-start scenarios Background / Motivation Community detection appears frequently in production:\nGroup identification: detect strongly related account clusters, suspicious groups, or interest circles Graph partitioning: place tightly connected subgraphs on the same shard to reduce cross-shard traffic Cold-start analysis: quickly classify new users/entities via neighborhood community structure The pain points are:\nGlobal optimum is usually unattainable (related objectives are NP-hard) Data is large and fast-changing, so offline algorithms are hard to rerun frequently Different products prioritize stability, speed, and interpretability differently So in practice, two methods dominate:\nLouvain: optimize for higher-quality communities (modularity) Label Propagation (LPA): optimize for speed and implementation simplicity Core Concepts Concept Meaning Engineering Impact Community a node set dense inside and sparse outside impacts partition and recommendation quality Modularity (Q) metric for partition quality Louvain optimization target Label Propagation nodes iteratively adopt majority neighbor labels fast but stochastic Graph Partition split storage/compute by community reduces cross-machine communication cost Cold Start quickly assign new nodes by neighborhood structure improves early-stage recall A - Algorithm (Problem and Algorithm) Problem Restatement (Engineering Abstraction) Given an undirected graph G=(V,E), output a community ID for each node, supporting:\nGroup identification (community membership output) Graph partitioning (map communities to shards) Cold-start assignment (map new nodes to candidate communities) Input / Output Name Type Description graph Dict[int, Set[int]] adjacency list (undirected graph) return Dict[int, int] node-to-community label mapping Example 1 0-1-2 form one cluster, 3-4-5 form another, with a weak edge between 2 and 3 Possible output: {0,1,2} -\u0026gt; C1, {3,4,5} -\u0026gt; C2 Example 2 Star graph (one center connected to multiple leaves) LPA often merges the center with most leaves into one community Reasoning Path (From Naive to Practical) Naive Approach: Connected Components Partition only by connectivity Cannot express cases where a weak bridge should separate two groups Key Observations Community is not just \u0026ldquo;connected\u0026rdquo;; it means \u0026ldquo;internally denser\u0026rdquo; Global optimum is unrealistic; scalable heuristics are preferred in production Different tasks require different priorities: Quality-first: Louvain Latency-first: LPA Method Selection Louvain: modularity-driven, typically more stable quality LPA: lightest implementation, suitable for very large graph coarse clustering C - Concepts (Core Ideas) Louvain: Modularity Maximization A common modularity form:\n$$ Q=\\frac{1}{2m}\\sum_{ij}\\left(A_{ij}-\\frac{k_i k_j}{2m}\\right)\\delta(c_i,c_j) $$\nWhere:\nA_ij: adjacency matrix entry k_i: degree of node i m: number of edges delta(c_i,c_j): 1 if same community, else 0 Louvain runs in a two-phase loop:\nLocal move: try moving each node into a neighbor community; accept when dQ \u0026gt; 0 Community aggregation: collapse communities into super-nodes and repeat Label Propagation: Neighbor Majority Voting Start with one label per node and iterate:\nNew label = most frequent neighbor label (ties broken randomly or by rule) Stop at convergence or max iterations Pros:\nSimple and fast Cons:\nResults depend on update order and randomness Stability is usually weaker than Louvain Minimal Mental Model Louvain: explicit objective optimization (Q) LPA: local consistency diffusion (majority label) Practical Guide / Steps Define your objective first: quality-first or latency-first Run Louvain on small-to-medium data first to build a quality baseline For large-scale online systems, run LPA coarse grouping first, then business post-processing Fix random seeds and record versions for reproducibility For cold start, use \u0026ldquo;neighbor label vote + confidence threshold\u0026rdquo; Runnable Python example (python3 community_demo.py):\nfrom collections import Counter import random def label_propagation(graph, max_iter=20, seed=42): random.seed(seed) label = {u: u for u in graph} nodes = list(graph.keys()) for _ in range(max_iter): changed = 0 random.shuffle(nodes) for u in nodes: if not graph[u]: continue cnt = Counter(label[v] for v in graph[u]) best = max(cnt.items(), key=lambda x: (x[1], -x[0]))[0] if label[u] != best: label[u] = best changed += 1 if changed == 0: break return label def cold_start_assign(graph, labels, new_neighbors): # new_neighbors: known neighbor list of the new node cnt = Counter(labels[v] for v in new_neighbors if v in labels) if not cnt: return None return cnt.most_common(1)[0][0] if __name__ == \u0026#34;__main__\u0026#34;: graph = { 0: {1, 2}, 1: {0, 2}, 2: {0, 1, 3}, 3: {2, 4, 5}, 4: {3, 5}, 5: {3, 4}, } labels = label_propagation(graph) print(\u0026#34;labels:\u0026#34;, labels) print(\u0026#34;new node -\u0026gt;\u0026#34;, cold_start_assign(graph, labels, [0, 2])) E - Engineering (Engineering Applications) Scenario 1: Group Identification (Python) Background: detect tightly connected groups in social or transaction graphs.\nWhy this fits: both Louvain and LPA quickly generate community labels that feed risk rules and visual analytics.\ndef group_by_label(labels): out = {} for u, c in labels.items(): out.setdefault(c, []).append(u) return out Scenario 2: Graph Partition Mapping (Go) Background: when graph storage is sharded, you want nodes in the same community to land in the same partition as much as possible.\nWhy this fits: community labels can be converted directly into partition keys, reducing cross-shard edge lookups.\npackage main import \u0026#34;fmt\u0026#34; func partitionByCommunity(labels map[int]int, shardCount int) map[int]int { part := make(map[int]int) for node, comm := range labels { part[node] = comm % shardCount } return part } func main() { labels := map[int]int{0: 1, 1: 1, 2: 1, 3: 2, 4: 2, 5: 2} fmt.Println(partitionByCommunity(labels, 4)) } Scenario 3: Cold-start Community Assignment (JavaScript) Background: a new user node has little history but a small set of neighbor links.\nWhy this fits: voting by neighbor communities provides a fast initial group for recommendation/recall pipelines.\nfunction assignCommunity(labels, neighbors) { const cnt = new Map(); for (const v of neighbors) { if (labels[v] === undefined) continue; cnt.set(labels[v], (cnt.get(labels[v]) || 0) + 1); } let best = null; let bestCnt = -1; for (const [c, n] of cnt.entries()) { if (n \u0026gt; bestCnt) { bestCnt = n; best = c; } } return best; } console.log(assignCommunity({0: 1, 2: 1, 3: 2}, [0, 2, 3])); R - Reflection (Reflection and Deep Dive) Complexity (Engineering View) LPA: about O(E) per round, total O(T*E) (T = iteration rounds) Louvain: common implementations are close to multi-round O(E) behavior, but constants depend on data distribution Alternatives and Trade-offs Method Pros Cons Best Fit Louvain usually better community quality more complex implementation, non-trivial incremental maintenance offline analysis, quality-first LPA fast, simple, parallelizable weaker stability very large graphs, real-time coarse clustering Spectral clustering strong mathematical properties expensive on large graphs fine-grained analysis on small/medium graphs Common Mistakes Looking only at algorithm name while ignoring query/update ratio Treating one LPA run as absolute truth without stability evaluation Hard-assigning cold-start nodes without preserving a \u0026ldquo;low-confidence pending\u0026rdquo; state Why This Works in Production Louvain and LPA form a complementary quality-speed pair Community labels directly power group identification, partitioning, and cold start You can run LPA first for approximation, then refine key subgraphs with Louvain Frequently Asked Questions and Notes Is Louvain always better than LPA?\nNot always. Louvain often gives better quality, but in high-throughput real-time settings LPA may be a better choice.\nDo I need to predefine the number of communities?\nUsually no for Louvain/LPA, which is one reason they are engineering-friendly.\nIs pure neighbor voting safe for cold start?\nAdd threshold and fallback logic: when confidence is low, route to an \u0026ldquo;undetermined\u0026rdquo; group first.\nBest Practices and Recommendations Define evaluation metrics first: modularity, business hit rate, stability Fix random seeds and run variance checks across multiple runs Prioritize low-latency methods online, then refine in offline batch Version community labels for rollback, tracing, and gradual rollout S - Summary (Summary) Core Takeaways Community detection is structural-signal modeling, not just graph-clustering visualization Louvain fits quality-first goals; LPA fits speed-first goals Group identification, graph partitioning, and cold start can all reuse community labels directly Production rollout should use a two-stage strategy: fast coarse grouping + focused refinement Metrics and reproducibility (seed/version) matter as much as the algorithm itself Recommended Further Reading Blondel et al., Fast unfolding of communities in large networks (Louvain) Raghavan et al., Near linear time algorithm to detect community structures (LPA) Graph partitioning in distributed graph systems (engineering sharding practice) Metadata Reading time: 12-16 minutes Tags: Community Detection, Louvain, LPA, Graph Partitioning, Cold Start SEO keywords: community detection, Louvain, Label Propagation, graph partition Meta description: Engineering selection and implementation of Louvain vs Label Propagation for group identification, graph partitioning, and cold-start analysis. Call To Action (CTA) A practical next step is to do two things:\nRun Louvain and LPA on your real business graph and compare modularity with business metrics Add \u0026ldquo;community confidence threshold + fallback logic\u0026rdquo; to your cold-start strategy and track online conversion changes If you want, I can continue with the next article: \u0026ldquo;Community Detection Evaluation Framework: Beyond Modularity, How to Define Business-usable Clustering Quality.\u0026rdquo;\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) from collections import Counter def lpa(graph, rounds=10): label = {u: u for u in graph} for _ in range(rounds): changed = 0 for u in graph: if not graph[u]: continue cnt = Counter(label[v] for v in graph[u]) best = max(cnt, key=cnt.get) if label[u] != best: label[u] = best changed += 1 if changed == 0: break return label #include \u0026lt;stdio.h\u0026gt; // Simplified demo: community-label to partition mapping (not full Louvain/LPA) int main(void) { int labels[] = {1,1,1,2,2,2}; int n = 6, shard = 4; for (int i = 0; i \u0026lt; n; ++i) { printf(\u0026#34;node=%d comm=%d part=%d\\n\u0026#34;, i, labels[i], labels[i] % shard); } return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;unordered_map\u0026gt; #include \u0026lt;vector\u0026gt; std::unordered_map\u0026lt;int, std::vector\u0026lt;int\u0026gt;\u0026gt; groupBy(const std::vector\u0026lt;int\u0026gt;\u0026amp; label) { std::unordered_map\u0026lt;int, std::vector\u0026lt;int\u0026gt;\u0026gt; g; for (int i = 0; i \u0026lt; (int)label.size(); ++i) g[label[i]].push_back(i); return g; } int main() { std::vector\u0026lt;int\u0026gt; label = {1,1,1,2,2,2}; auto g = groupBy(label); for (auto\u0026amp; kv : g) { std::cout \u0026lt;\u0026lt; \u0026#34;comm \u0026#34; \u0026lt;\u0026lt; kv.first \u0026lt;\u0026lt; \u0026#34;: \u0026#34;; for (int u : kv.second) std::cout \u0026lt;\u0026lt; u \u0026lt;\u0026lt; \u0026#34; \u0026#34;; std::cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } package main import \u0026#34;fmt\u0026#34; func assignCommunity(labels map[int]int, neighbors []int) (int, bool) { cnt := map[int]int{} for _, v := range neighbors { if c, ok := labels[v]; ok { cnt[c]++ } } best, bestN := 0, 0 for c, n := range cnt { if n \u0026gt; bestN { best, bestN = c, n } } if bestN == 0 { return 0, false } return best, true } func main() { labels := map[int]int{0: 1, 2: 1, 3: 2} comm, ok := assignCommunity(labels, []int{0, 2, 3}) fmt.Println(comm, ok) } use std::collections::HashMap; fn assign_community(labels: \u0026amp;HashMap\u0026lt;i32, i32\u0026gt;, neighbors: \u0026amp;[i32]) -\u0026gt; Option\u0026lt;i32\u0026gt; { let mut cnt: HashMap\u0026lt;i32, i32\u0026gt; = HashMap::new(); for \u0026amp;v in neighbors { if let Some(\u0026amp;c) = labels.get(\u0026amp;v) { *cnt.entry(c).or_insert(0) += 1; } } cnt.into_iter().max_by_key(|(_, n)| *n).map(|(c, _)| c) } fn main() { let mut labels = HashMap::new(); labels.insert(0, 1); labels.insert(2, 1); labels.insert(3, 2); println!(\u0026#34;{:?}\u0026#34;, assign_community(\u0026amp;labels, \u0026amp;[0, 2, 3])); } function assignCommunity(labels, neighbors) { const cnt = new Map(); for (const v of neighbors) { if (labels[v] === undefined) continue; cnt.set(labels[v], (cnt.get(labels[v]) || 0) + 1); } let best = null, bestN = -1; for (const [c, n] of cnt) { if (n \u0026gt; bestN) { best = c; bestN = n; } } return best; } console.log(assignCommunity({0: 1, 2: 1, 3: 2}, [0, 2, 3])); ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/80-community-detection-louvain-label-propagation/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\u003cbr\u003e\nCommunity detection is not just \u0026ldquo;splitting a graph into a few groups.\u0026rdquo; In production, you must balance accuracy, interpretability, speed, and maintainability. Following the ACERS structure, this article breaks down two of the most common engineering choices: \u003cstrong\u003eLouvain (modularity optimization)\u003c/strong\u003e and \u003cstrong\u003eLabel Propagation (LPA)\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEstimated reading time\u003c/strong\u003e: 12-16 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eCommunity Detection\u003c/code\u003e, \u003ccode\u003eLouvain\u003c/code\u003e, \u003ccode\u003eLabel Propagation\u003c/code\u003e, \u003ccode\u003eGraph Partitioning\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: community detection, Louvain, Label Propagation, modularity, graph partition\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Engineering primer for community detection: principles, complexity, algorithm selection, and implementation templates for Louvain and LPA across group discovery, graph partitioning, and cold start.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-audience\"\u003eTarget Audience\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eEngineers working on social graphs, risk-control graphs, or recommender-system graph analytics\u003c/li\u003e\n\u003cli\u003eDevelopers who want to move community detection from paper concepts into production workflows\u003c/li\u003e\n\u003cli\u003ePractitioners modeling group structure for graph partitioning and cold-start scenarios\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eCommunity detection appears frequently in production:\u003c/p\u003e","title":"Community Detection Primer: Engineering Trade-offs Between Louvain and Label Propagation - ACERS Analysis"},{"content":" Subtitle / Abstract\nSubgraph matching is one of the hardest parts of graph querying: NP-hard in theory, but not automatically \u0026ldquo;too slow\u0026rdquo; in production. Following the ACERS template, this article explains VF2 and Ullmann clearly, and focuses on what actually decides performance: candidate generation and pruning strategy.\nEstimated reading time: 15-20 minutes Tags: Subgraph Matching, VF2, Ullmann, Graph Database SEO keywords: Subgraph Isomorphism, VF2, Ullmann, candidate pruning, graph pattern matching Meta description: Starting from NP-hard subgraph isomorphism, this article explains VF2/Ullmann mechanics and practical pruning strategies for constrained graph-database pattern queries. Target Audience Engineers building pattern queries, rule detection, or risk-relationship mining in graph databases Developers who already know BFS/DFS/connected components and want stronger graph-matching skills Algorithm practitioners balancing explainable rule matching against performance limits Background / Motivation In graph databases, you regularly face requirements like:\nFind a suspicious structure such as \u0026ldquo;one person - two companies - same device\u0026rdquo; Find a specific \u0026ldquo;author - paper - institution\u0026rdquo; pattern Find \u0026ldquo;cyclic laundering templates\u0026rdquo; in transaction chains These queries are essentially Subgraph Isomorphism: given a pattern graph Q, find an embedding in data graph G that satisfies both structure and constraints.\nTheoretically this is NP-hard, so worst-case exponential blowups are unavoidable.\nIn production, however, most queries are constrained patterns (labels, directions, attributes, and small pattern size), so performance usually depends on this:\nShrink candidates aggressively first, then run matching search.\nCore Concepts Subgraph Isomorphism: an injective mapping from pattern nodes to data nodes that preserves edges Constrained pattern: restrictions on label, direction, degree, and attribute predicates Candidate set: possible data nodes for each pattern node Pruning: reject impossible mappings early to reduce backtracking branches VF2: depth-first matching framework using state expansion plus feasibility checks Ullmann: classic method based on candidate matrix and iterative neighborhood consistency refinement A - Algorithm (Problem and Algorithm) Problem Restatement (Engineering Version) Given:\nData graph G=(V_G,E_G) (usually large) Pattern graph Q=(V_Q,E_Q) (usually small) Node/edge constraints (labels, direction, attribute predicates) Goal:\nDecide whether a match exists (existence) Or return all valid mapping results (enumeration) Input / Output Name Type Description G graph data graph, with large ` Q graph pattern graph, with small ` constraints constraints label / degree / attributes / direction, etc. return bool / mappings existence result or mapping list Example 1 (Match Exists) Pattern Q: A -knows-\u0026gt; B -works_at-\u0026gt; C Data G: multiple A/B/C-labeled nodes and directed edges Result: at least one mapping satisfies labels and direction Example 2 (Pruned Away) Pattern Q: node X has degree\u0026gt;=4 and label=Merchant Data G: all Merchant nodes have max degree=2 Result: empty candidate set -\u0026gt; fail immediately (no backtracking needed) Reasoning Path (From Brute Force to Practical) Naive Brute Force Enumerate permutations/combinations of data nodes for the |V_Q| pattern nodes Verify every pattern edge Complexity is roughly exponential and unusable in real systems.\nKey Observations Pattern graphs are usually small, but data graphs can be huge Most candidates can be filtered out by \u0026ldquo;label + degree + neighborhood\u0026rdquo; checks VF2/Ullmann only become effective after the candidate space is reduced Method Choice Theoretical framing: Subgraph Isomorphism is NP-hard Engineering pipeline: candidate generation -\u0026gt; candidate pruning -\u0026gt; backtracking match Implementation choices: both VF2 and Ullmann fit this pipeline C - Concepts (Core Ideas) VF2 Idea (More Common in Practice) Extend partial mapping M step by step At each step, choose one pattern node u and try candidate v Run feasibility checks: Semantic constraints (label / attribute) Topological constraints (edge consistency with already matched neighbors) Frontier consistency (in/out frontier) Backtrack immediately when infeasible Ullmann Idea (Matrix Refinement) Initial candidate matrix C[u][v] means u may map to v Repeatedly apply neighborhood-consistency propagation (refinement) Perform backtracking after matrix contraction Relationship Between Them Ullmann is closer to \u0026ldquo;strong preprocessing first, then search\u0026rdquo; VF2 is closer to \u0026ldquo;search while doing local feasibility checks\u0026rdquo; In production they are often combined: Ullmann-style candidate refinement + VF2-style search Why Candidate Pruning Matters More Search complexity is roughly driven by:\n[ \\prod_{u \\in V_Q} |Cand(u)| ]\nIf |Cand(u)| drops from 100 to 5, search-tree size changes by orders of magnitude even with the same algorithm.\nPractical Guide / Steps Normalize the pattern: fix node order (high-constraint nodes first) Generate candidates: prefilter by label/type/degree Refine candidates: iterative neighborhood consistency (Ullmann-style) Backtracking match: injective mapping + adjacency consistency checks (VF2-style) Early stop: for existence-only queries, return at first match Output control: cap max returned mappings to avoid output explosion Runnable Example (Python) from typing import Dict, List, Set, Tuple class Graph: def __init__(self, n: int): self.n = n self.adj = [set() for _ in range(n)] self.label = [\u0026#34;\u0026#34;] * n def add_edge(self, u: int, v: int) -\u0026gt; None: self.adj[u].add(v) def build_candidates(G: Graph, Q: Graph) -\u0026gt; List[Set[int]]: cands: List[Set[int]] = [] for u in range(Q.n): s = set() for v in range(G.n): # Semantic + degree-lower-bound pruning if Q.label[u] == G.label[v] and len(Q.adj[u]) \u0026lt;= len(G.adj[v]): s.add(v) cands.append(s) return cands def refine_candidates(G: Graph, Q: Graph, cands: List[Set[int]]) -\u0026gt; None: # Ullmann-style neighborhood consistency refinement changed = True while changed: changed = False for u in range(Q.n): remove = [] for v in cands[u]: ok = True for nu in Q.adj[u]: # At least one candidate neighbor can realize edge u-\u0026gt;nu if not any((nv in G.adj[v]) for nv in cands[nu]): ok = False break if not ok: remove.append(v) if remove: changed = True for x in remove: cands[u].remove(x) def has_match_vf2_style(G: Graph, Q: Graph) -\u0026gt; bool: cands = build_candidates(G, Q) refine_candidates(G, Q, cands) if any(len(s) == 0 for s in cands): return False order = sorted(range(Q.n), key=lambda u: len(cands[u])) used_g: Set[int] = set() mapping: Dict[int, int] = {} def feasible(u: int, v: int) -\u0026gt; bool: # Edge consistency check against already matched nodes for qu, gv in mapping.items(): if u in Q.adj[qu] and v not in G.adj[gv]: return False if qu in Q.adj[u] and gv not in G.adj[v]: return False return True def dfs(i: int) -\u0026gt; bool: if i == len(order): return True u = order[i] for v in cands[u]: if v in used_g: continue if not feasible(u, v): continue mapping[u] = v used_g.add(v) if dfs(i + 1): return True # early stop: existence used_g.remove(v) del mapping[u] return False return dfs(0) if __name__ == \u0026#34;__main__\u0026#34;: # Data graph G = Graph(6) G.label = [\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;, \u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;] G.add_edge(0, 1) G.add_edge(1, 2) G.add_edge(3, 4) G.add_edge(4, 5) # Pattern graph A-\u0026gt;B-\u0026gt;C Q = Graph(3) Q.label = [\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;] Q.add_edge(0, 1) Q.add_edge(1, 2) print(has_match_vf2_style(G, Q)) # True Run:\npython3 subgraph_match_demo.py E - Engineering (Engineering Applications) Scenario 1: Anti-Fraud Rule Graph Query (Python) Background: detect structured patterns like \u0026ldquo;shared device + multiple accounts + money returning flow\u0026rdquo;.\nWhy this fits: pattern size is small and constraints are strong, so pruning keeps queries controllable.\ndef is_suspicious(match_count: int, threshold: int = 1) -\u0026gt; bool: return match_count \u0026gt;= threshold print(is_suspicious(2, 1)) Scenario 2: Knowledge Graph Template Retrieval (Go) Background: retrieve patterns such as \u0026ldquo;author-paper-institution\u0026rdquo; or \u0026ldquo;drug-target-disease\u0026rdquo;.\nWhy this fits: strong label constraints allow candidate shrinking early.\npackage main import \u0026#34;fmt\u0026#34; func estimateSearchSpace(cands []int) int { space := 1 for _, x := range cands { space *= x } return space } func main() { fmt.Println(estimateSearchSpace([]int{3, 5, 2})) // 30 } Scenario 3: Template Routing Before Graph Sharding (JavaScript) Background: in multi-shard graph storage, quickly estimate which shards a pattern likely touches first.\nWhy this fits: candidate-shard pruning can reduce cross-shard RPC calls.\nfunction shardHint(candidateNodes, shardCount) { const hit = new Set(candidateNodes.map((x) =\u0026gt; x % shardCount)); return [...hit]; } console.log(shardHint([12, 18, 25, 31], 4)); R - Reflection (Reflection and Deep Dive) Complexity Analysis Worst-case subgraph isomorphism complexity is exponential (NP-hard) Real runtime is dominated by search-tree size Candidate pruning quality directly decides practical feasibility Alternatives and Trade-offs Approach Strength Limitation Brute-force enumeration Simple to implement Barely scalable Ullmann Strong preprocessing pruning, clear logic Matrix operations can be costly VF2 Widely adopted in engineering, efficient local checks Sensitive to candidate quality Native graph DB pattern engine Easier operations and integration More black-box behavior, tuning is experience-heavy Why \u0026ldquo;Candidate Pruning First\u0026rdquo; In production, most queries are constrained patterns (label + direction + attributes).\nThat means the bottleneck is usually the candidate stage, not \u0026ldquo;VF2 vs Ullmann\u0026rdquo; by itself.\nExplanation and Principles (Why This Works) Subgraph matching can be split into two layers:\nSemantic filtering: remove clearly impossible nodes first Structural validation: run isomorphism search in the reduced candidate space This layering often turns NP-hard matching into acceptable production latency for business queries.\nFrequently Asked Questions and Notes Is a smaller pattern always faster?\nNot necessarily. If constraints are weak (for example wildcard labels), even a small pattern can have huge candidate sets.\nCan I run VF2 without candidate filtering?\nYou can, but it is usually too slow on large graphs.\nWhat if result size explodes?\nYou must enforce max return limits and support existence-only mode.\nWhere should attribute predicates be applied?\nPush them as early as possible into candidate generation to reduce backtracking branches.\nBest Practices and Recommendations Match pattern nodes in ascending candidate-set size Push label/direction/degree/attribute filters up front Provide both limit and timeout in online APIs Separate metrics into candidate size, pruning rate, and backtracking depth for performance diagnosis S - Summary (Summary) Core Takeaways Subgraph Isomorphism is NP-hard in theory, but still practical in engineering contexts. VF2 and Ullmann both reduce to \u0026ldquo;constraint-driven search + pruning.\u0026rdquo; Constrained patterns are the mainstream query shape; performance hinges on candidate shrinking. Candidate pruning usually impacts throughput more than the specific classic algorithm name. Splitting query goals into existence / top-k / full enumeration significantly improves system stability. Recommended Further Reading Cordella et al. A (Sub)Graph Isomorphism Algorithm for Matching Large Graphs (VF2) Ullmann. An Algorithm for Subgraph Isomorphism Pattern-matching and query-optimization documentation from Neo4j / TigerGraph Closing Conclusion The real engineering skill in subgraph matching is not memorizing VF2 or Ullmann names; it is converting business constraints into strong pruning rules.\nWhen you compress the candidate space, even NP-hard matching can run inside production latency budgets.\nMetadata Reading time: 15-20 minutes Tags: Subgraph Matching, VF2, Ullmann, Graph Database, Pruning SEO keywords: Subgraph Isomorphism, VF2, Ullmann, candidate pruning Meta description: Practical subgraph matching engineering with VF2/Ullmann ideas and candidate-pruning-first strategy. Call To Action (CTA) A practical next step is to do two things now:\nMeasure \u0026ldquo;candidate size distribution\u0026rdquo; and \u0026ldquo;pruning rate\u0026rdquo; for existing pattern queries. Split out an existence-only query path and use early stop to reduce latency. If you want, I can continue with \u0026ldquo;9) Graph Indexing (Neighborhood Signature / Path Index)\u0026rdquo; as a direct follow-up to this article.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) # existence-only subgraph match skeleton def has_match(candidates, feasible): order = sorted(range(len(candidates)), key=lambda i: len(candidates[i])) used = set() map_q2g = {} def dfs(i): if i == len(order): return True u = order[i] for v in candidates[u]: if v in used: continue if not feasible(u, v, map_q2g): continue used.add(v) map_q2g[u] = v if dfs(i + 1): return True used.remove(v) del map_q2g[u] return False return dfs(0) #include \u0026lt;stdio.h\u0026gt; int main(void) { // C version: the key signal is prune first, backtrack later int candidate_size_q0 = 3; int candidate_size_q1 = 5; int search_space_upper = candidate_size_q0 * candidate_size_q1; printf(\u0026#34;upper search space = %d\\n\u0026#34;, search_space_upper); return 0; } #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; int main() { vector\u0026lt;int\u0026gt; cand = {3, 4, 2}; long long upper = 1; for (int x : cand) upper *= x; cout \u0026lt;\u0026lt; \u0026#34;upper=\u0026#34; \u0026lt;\u0026lt; upper \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } package main import \u0026#34;fmt\u0026#34; func upperBound(cands []int) int { ans := 1 for _, x := range cands { ans *= x } return ans } func main() { fmt.Println(upperBound([]int{3, 4, 2})) } fn upper_bound(cands: \u0026amp;[usize]) -\u0026gt; usize { cands.iter().product() } fn main() { let cands = vec![3, 4, 2]; println!(\u0026#34;{}\u0026#34;, upper_bound(\u0026amp;cands)); } function upperBound(cands) { return cands.reduce((acc, x) =\u0026gt; acc * x, 1); } console.log(upperBound([3, 4, 2])); ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/70-subgraph-matching-vf2-ullmann-and-pruning/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\u003cbr\u003e\nSubgraph matching is one of the hardest parts of graph querying: NP-hard in theory, but not automatically \u0026ldquo;too slow\u0026rdquo; in production. Following the ACERS template, this article explains VF2 and Ullmann clearly, and focuses on what actually decides performance: \u003cstrong\u003ecandidate generation and pruning strategy\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEstimated reading time\u003c/strong\u003e: 15-20 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eSubgraph Matching\u003c/code\u003e, \u003ccode\u003eVF2\u003c/code\u003e, \u003ccode\u003eUllmann\u003c/code\u003e, \u003ccode\u003eGraph Database\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Subgraph Isomorphism, VF2, Ullmann, candidate pruning, graph pattern matching\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Starting from NP-hard subgraph isomorphism, this article explains VF2/Ullmann mechanics and practical pruning strategies for constrained graph-database pattern queries.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-audience\"\u003eTarget Audience\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eEngineers building pattern queries, rule detection, or risk-relationship mining in graph databases\u003c/li\u003e\n\u003cli\u003eDevelopers who already know BFS/DFS/connected components and want stronger graph-matching skills\u003c/li\u003e\n\u003cli\u003eAlgorithm practitioners balancing explainable rule matching against performance limits\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eIn graph databases, you regularly face requirements like:\u003c/p\u003e","title":"Subgraph Matching / Pattern Matching: VF2, Ullmann, and Engineering-Grade Pruning - ACERS Analysis"},{"content":" Subtitle / Abstract\nCentrality is not just a paper concept. In graph systems, it is a practical node-importance ranking engine. This article follows the ACERS structure to explain Degree / Betweenness / Closeness and gives one pragmatic conclusion: most online systems reliably support only Degree + approximate Betweenness.\nEstimated reading time: 12-16 minutes Tags: Graph Theory, Centrality, Degree, Betweenness, Closeness SEO keywords: graph centrality, Degree Centrality, Betweenness, Closeness, approximate Betweenness Meta description: Engineering guide to graph centrality: definitions, complexity, approximation methods, and production strategies, with runnable code. Target Audience Engineers working on relationship graph analysis, knowledge graphs, or graph-database query optimization Developers who need to turn \u0026ldquo;node importance\u0026rdquo; from concept into production metric Practitioners who want to understand why Betweenness is expensive in production and how to approximate it Background / Motivation In graph systems, you will eventually face questions like these:\nWhich nodes are social influencers or transaction hubs? Which nodes are key bridges whose removal significantly fragments the graph? Which nodes are globally closer to others and better suited as entry points or cache hotspots? These map directly to centrality metrics:\nDegree Centrality: how many connections a node has (local importance) Betweenness Centrality: whether a node lies on many shortest paths (bridge importance) Closeness Centrality: whether a node has shorter average distance to the full graph (global proximity) In practice, the biggest challenge is not definition - it is compute cost:\nDegree is very cheap and almost always supports real-time use Exact Betweenness is expensive and is usually offline or approximate Closeness requires many shortest-path computations and quickly becomes hard to run online on large graphs Core Concepts 1) Degree Centrality For node v in an undirected graph, Degree centrality is commonly written as:\nC_D(v) = deg(v) / (n - 1) Meaning: local connectivity activity of the node.\n2) Betweenness Centrality C_B(v) = Σ_{s≠v≠t} (σ_st(v) / σ_st) σ_st: number of shortest paths from s to t σ_st(v): number of those shortest paths that pass through v Meaning: mediation power of the node as a channel or bridge.\n3) Closeness Centrality C_C(v) = (n - 1) / Σ_{u≠v} d(v, u) Meaning: how close the node is, overall, to all other nodes in the graph.\nPractical note: disconnected graphs often use harmonic closeness to avoid denominator issues caused by unreachable nodes.\nA — Algorithm (Problem and Algorithm) Problem Restatement (Engineering Version) Given graph G=(V,E), compute centrality scores for each node and return Top-K nodes:\nDegree centrality Betweenness centrality (approximation allowed) Closeness centrality (or harmonic variant) Input/Output Name Type Description graph adjacency list graph[u] = [v1, v2, ...] (unweighted) k int number of Top-K outputs mode str degree / betweenness / closeness return List[(node, score)] sorted node scores Example 1 (Small Graph) A-B-C-D and B-E Intuition: - B has high degree -\u0026gt; high Degree - B/C lie on many shortest paths -\u0026gt; high Betweenness - B/C are closer on average to other nodes -\u0026gt; higher Closeness Example 2 (Bridge Node) Two clusters are connected through X X usually has very high Betweenness, even if its Degree is not the highest Reasoning Path (From Naive to Production) Naive Approach Compute shortest paths for every pair of nodes, then count how often each node is traversed Complexity is too high for large graphs Key Observations Degree uses only local adjacency and is near linear complexity Betweenness can be significantly optimized with Brandes, but remains relatively expensive Closeness fundamentally needs many-source shortest paths, so cost rises rapidly with graph size Engineering Decisions Online: prioritize Degree, add sampled approximate Betweenness when necessary Offline batch: compute fuller Betweenness / Closeness Large graphs: use Top-K + sampling + layered cache together C — Concepts (Core Ideas) Method Categories Degree: local statistics Betweenness: dependency accumulation over global shortest paths Closeness: global distance aggregation Complexity Intuition (Unweighted Graph) Metric Common Algorithm Rough Complexity Degree traverse adjacency list O(V+E) Betweenness Brandes O(VE) Closeness run BFS from every node O(V(V+E)) Practical Conclusion (Key Point) Most online systems reliably support only Degree + approximate Betweenness.\nCloseness is often moved offline or computed only on small subgraphs.\nThe reasons are straightforward:\nDegree is low-cost, highly interpretable, and easy to update incrementally Exact Betweenness is too expensive, while approximation is controllable Closeness is sensitive to connectivity and graph size, making online SLA hard to guarantee Practical Guide / Steps Step 1: Define the Business Question First Looking for highly connected nodes: Degree Looking for critical bridges: Betweenness Looking for global proximity centers: Closeness Step 2: Choose Online or Offline Online services: Degree + approximate Betweenness Offline reporting: add Closeness / refined Betweenness Step 3: Runnable Python Baseline from collections import deque import random def degree_centrality(graph): n = max(len(graph), 1) return {u: len(graph.get(u, [])) / max(n - 1, 1) for u in graph} def bfs_dist(graph, s): dist = {s: 0} q = deque([s]) while q: u = q.popleft() for v in graph.get(u, []): if v not in dist: dist[v] = dist[u] + 1 q.append(v) return dist def closeness_centrality(graph): n = len(graph) cc = {} for u in graph: d = bfs_dist(graph, u) if len(d) \u0026lt;= 1: cc[u] = 0.0 continue s = sum(d.values()) cc[u] = (len(d) - 1) / s if s \u0026gt; 0 else 0.0 # Can be switched to harmonic closeness depending on the use case return cc def approx_betweenness_by_sampling(graph, samples=8, seed=0): random.seed(seed) nodes = list(graph.keys()) if not nodes: return {} score = {u: 0.0 for u in nodes} sample_sources = random.sample(nodes, min(samples, len(nodes))) for s in sample_sources: # Single-source shortest-path DAG + dependency back-propagation (Brandes style) stack = [] pred = {v: [] for v in nodes} sigma = {v: 0.0 for v in nodes} dist = {v: -1 for v in nodes} sigma[s] = 1.0 dist[s] = 0 q = deque([s]) while q: v = q.popleft() stack.append(v) for w in graph.get(v, []): if dist[w] \u0026lt; 0: dist[w] = dist[v] + 1 q.append(w) if dist[w] == dist[v] + 1: sigma[w] += sigma[v] pred[w].append(v) delta = {v: 0.0 for v in nodes} while stack: w = stack.pop() for v in pred[w]: if sigma[w] \u0026gt; 0: delta[v] += (sigma[v] / sigma[w]) * (1.0 + delta[w]) if w != s: score[w] += delta[w] # Sampling normalization (approximation) factor = len(nodes) / max(len(sample_sources), 1) return {u: score[u] * factor for u in nodes} if __name__ == \u0026#34;__main__\u0026#34;: g = { \u0026#34;A\u0026#34;: [\u0026#34;B\u0026#34;], \u0026#34;B\u0026#34;: [\u0026#34;A\u0026#34;, \u0026#34;C\u0026#34;, \u0026#34;E\u0026#34;], \u0026#34;C\u0026#34;: [\u0026#34;B\u0026#34;, \u0026#34;D\u0026#34;], \u0026#34;D\u0026#34;: [\u0026#34;C\u0026#34;], \u0026#34;E\u0026#34;: [\u0026#34;B\u0026#34;], } print(\u0026#34;degree\u0026#34;, degree_centrality(g)) print(\u0026#34;closeness\u0026#34;, closeness_centrality(g)) print(\u0026#34;approx_betweenness\u0026#34;, approx_betweenness_by_sampling(g, samples=3, seed=42)) E — Engineering (Engineering Applications) Scenario 1: Anti-Fraud Hub Account Detection (Degree) Background: in money-transfer graphs, highly connected accounts are often transfer hubs.\nWhy this fits: Degree is fast to compute and suitable as an online risk-control feature.\n# online feature: out-degree / in-degree risk_score = out_degree * 0.6 + in_degree * 0.4 Scenario 2: Critical Bridge Node Alerts (Approximate Betweenness) Background: in social or transaction graphs, some nodes are the only channel between communities.\nWhy this fits: Betweenness finds bridges, but exact computation is expensive; sampled approximation is easier to deploy.\n// pseudo-go style: run sampled Brandes in batch job // 1) sample K sources // 2) accumulate dependency scores // 3) write top-k bridge nodes to Redis/OLAP Scenario 3: Entry Selection for Path Explanation (Closeness) Background: explanation systems may prefer to start path rendering from nodes that are globally closer to core regions.\nWhy this fits: Closeness captures nodes with shorter average distances.\n// Use top-N offline closeness nodes as explanation entry candidates const candidates = centralityRank.slice(0, N); R — Reflection (Reflection and Deeper Analysis) Exact vs Approximate Metric Exact Cost Approximation Strategy Engineering Recommendation Degree Low Not required Compute online directly Betweenness High Source-node sampling, Top-K estimation Read offline results online / batch refresh Closeness Medium-high Subgraph computation, harmonic variant Mostly used for offline analysis Common Wrong Approaches Treating Betweenness as a fully real-time online metric Applying standard Closeness directly to large disconnected graphs without variant handling Ignoring directed vs undirected differences, causing interpretation errors Why \u0026ldquo;Degree + Approximate Betweenness\u0026rdquo; Is Most Common Controllable cost: can satisfy online SLA Strong interpretability: easy for product and business teams to reason about Easy evolution path: launch a usable version first, then add refined offline metrics Explanation and Principles (Why This Works) The engineering essence of centrality is extracting stable, interpretable importance signals at acceptable cost.\nDegree gives local activity Betweenness gives bridge-control power Closeness gives global proximity In real systems, the question is not \u0026ldquo;which metric is theoretically best,\u0026rdquo; but \u0026ldquo;which metric is sustainable under current scale and latency budget.\u0026rdquo;\nFrequently Asked Questions and Notes Can directed and undirected graphs use the same formulas?\nThe high-level idea is shared, but counting conventions differ (in-degree/out-degree, shortest-path direction).\nDoes Betweenness have to be exact?\nNot necessarily. In many cases, approximate ranking is enough, especially for Top-K.\nHow should Closeness be handled on disconnected graphs?\nHarmonic closeness is recommended, or restrict computation to connected subgraphs.\nDo centrality scores need real-time updates?\nMost systems use \u0026ldquo;offline batch refresh + online cache.\u0026rdquo; Only Degree is usually feasible for lightweight real-time increment.\nBest Practices and Recommendations Split centrality into two layers: offline primary computation + online feature service Start from business questions, then pick metrics; avoid a \u0026ldquo;metric-first\u0026rdquo; mindset Put budgets on Betweenness: sample size, time window, Top-K-only outputs For large graphs, split by connected components first to avoid indiscriminate full-graph computation S — Summary (Summary) Core Takeaways Degree, Betweenness, and Closeness capture local connectivity, bridge mediation, and global proximity Betweenness is expensive in production; exact full computation is usually unsuitable online The pragmatic combination in most systems is Degree + approximate Betweenness Closeness is better for offline analysis or small subgraphs Metric choice must obey constraints of scale, latency, and interpretability Recommended Further Reading Ulrik Brandes (2001): A Faster Algorithm for Betweenness Centrality NetworkX centrality documentation (quick experiments) GDS centrality operator design in graph databases (offline batch practice) Metadata Reading time: 12-16 minutes Tags: Graph Theory, Centrality, Degree, Betweenness, Closeness SEO keywords: graph centrality, Degree Centrality, Betweenness, Closeness, approximate Betweenness Meta description: Engineering guide to the graph centrality trio: definitions, complexity, approximation, and rollout strategy, with a focus on why most systems only support Degree and approximate Betweenness. Call To Action (CTA) A practical next step is to do two things:\nLaunch Degree + Top-K first to validate business interpretability Add an offline sampled-Betweenness job and compare ranking stability If you want, I can write the next engineering continuation on \u0026ldquo;PageRank + Community Detection (Louvain).\u0026rdquo;\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) # Degree centrality (unweighted graph) def degree_centrality(graph): n = max(len(graph), 1) return {u: len(graph.get(u, [])) / max(n - 1, 1) for u in graph} /* degree centrality for adjacency matrix (undirected) */ #include \u0026lt;stdio.h\u0026gt; #define N 5 int main(void) { int g[N][N] = { {0,1,0,0,1}, {1,0,1,0,0}, {0,1,0,1,0}, {0,0,1,0,0}, {1,0,0,0,0} }; for (int i = 0; i \u0026lt; N; i++) { int deg = 0; for (int j = 0; j \u0026lt; N; j++) deg += g[i][j]; double cd = (double)deg / (N - 1); printf(\u0026#34;node %d degree_c=%.3f\\n\u0026#34;, i, cd); } return 0; } #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; int main() { vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; g = { {1,4}, {0,2}, {1,3}, {2}, {0} }; int n = (int)g.size(); for (int u = 0; u \u0026lt; n; ++u) { double c = (double)g[u].size() / max(n - 1, 1); cout \u0026lt;\u0026lt; \u0026#34;node \u0026#34; \u0026lt;\u0026lt; u \u0026lt;\u0026lt; \u0026#34; degree_c=\u0026#34; \u0026lt;\u0026lt; c \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } package main import \u0026#34;fmt\u0026#34; func main() { g := map[int][]int{0: {1,4}, 1: {0,2}, 2: {1,3}, 3: {2}, 4: {0}} n := len(g) for u, nbrs := range g { cd := float64(len(nbrs)) / float64(n-1) fmt.Printf(\u0026#34;node %d degree_c=%.3f\\n\u0026#34;, u, cd) } } use std::collections::HashMap; fn main() { let mut g: HashMap\u0026lt;i32, Vec\u0026lt;i32\u0026gt;\u0026gt; = HashMap::new(); g.insert(0, vec![1, 4]); g.insert(1, vec![0, 2]); g.insert(2, vec![1, 3]); g.insert(3, vec![2]); g.insert(4, vec![0]); let n = g.len() as f64; for (u, nbrs) in \u0026amp;g { let cd = nbrs.len() as f64 / (n - 1.0); println!(\u0026#34;node {} degree_c={:.3}\u0026#34;, u, cd); } } const g = new Map([ [\u0026#34;A\u0026#34;, [\u0026#34;B\u0026#34;, \u0026#34;E\u0026#34;]], [\u0026#34;B\u0026#34;, [\u0026#34;A\u0026#34;, \u0026#34;C\u0026#34;]], [\u0026#34;C\u0026#34;, [\u0026#34;B\u0026#34;, \u0026#34;D\u0026#34;]], [\u0026#34;D\u0026#34;, [\u0026#34;C\u0026#34;]], [\u0026#34;E\u0026#34;, [\u0026#34;A\u0026#34;]], ]); const n = g.size; for (const [u, nbrs] of g.entries()) { const cd = nbrs.length / (n - 1); console.log(u, \u0026#34;degree_c=\u0026#34;, cd.toFixed(3)); } ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/50-graph-centrality-degree-betweenness-closeness/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\u003cbr\u003e\nCentrality is not just a paper concept. In graph systems, it is a practical node-importance ranking engine. This article follows the ACERS structure to explain \u003cstrong\u003eDegree / Betweenness / Closeness\u003c/strong\u003e and gives one pragmatic conclusion: \u003cstrong\u003emost online systems reliably support only Degree + approximate Betweenness\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEstimated reading time\u003c/strong\u003e: 12-16 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eGraph Theory\u003c/code\u003e, \u003ccode\u003eCentrality\u003c/code\u003e, \u003ccode\u003eDegree\u003c/code\u003e, \u003ccode\u003eBetweenness\u003c/code\u003e, \u003ccode\u003eCloseness\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: graph centrality, Degree Centrality, Betweenness, Closeness, approximate Betweenness\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Engineering guide to graph centrality: definitions, complexity, approximation methods, and production strategies, with runnable code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-audience\"\u003eTarget Audience\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eEngineers working on relationship graph analysis, knowledge graphs, or graph-database query optimization\u003c/li\u003e\n\u003cli\u003eDevelopers who need to turn \u0026ldquo;node importance\u0026rdquo; from concept into production metric\u003c/li\u003e\n\u003cli\u003ePractitioners who want to understand why Betweenness is expensive in production and how to approximate it\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eIn graph systems, you will eventually face questions like these:\u003c/p\u003e","title":"The Graph Centrality Trio: Degree, Betweenness, and Closeness - ACERS Engineering Analysis"},{"content":" Subtitle / Abstract\nConnectivity tells you how a graph is partitioned, while PageRank tells you who matters inside each component. This is one of the core advantages of graph databases over relational databases: not only linking data, but propagating structural importance. This article follows ACERS to explain PageRank / PPR principles and production implementation.\nEstimated reading time: 15-20 minutes Tags: PageRank, PPR, Graph Database, Sparse Matrix SEO keywords: PageRank, Personalized PageRank, sparse matrix, incremental updates, graph database Meta description: From classic PageRank to Personalized PageRank, covering iterative computation, sparse-matrix optimization, and incremental update strategy, with runnable multi-language implementations. Target Audience Engineers building ranking, recommendation, or influence analysis on graph databases Developers who already know BFS/DFS/connected components and want to level up to graph scoring Algorithm engineers focused on iteration performance and update latency on large online graphs Background / Motivation You may already have split graphs into connected components and SCCs, but production systems still face a harder question:\nInside the same component, who is more critical? Given a user or seed node, who is structurally more relevant? This is exactly what PageRank / Personalized PageRank (PPR) is for.\nThis is also a key difference between graph databases and relational databases:\nRelational databases are strong at joins and filtering (row/column view) Graph databases are strong at topological propagation (edge-structure view) At its core, PageRank is probability-mass propagation over a graph, combining local edges and global structure into a rankable score.\nCore Concepts PageRank: global importance score tied to inbound-link quality, not just inbound-link count Personalized PageRank (PPR): biases random walks toward a seed set to obtain personalized importance Damping factor d / alpha: controls whether the walk continues along edges or jumps back to random/seed distribution Sparse matrix: adjacency matrices are extremely sparse at scale; multiplication must use CSR/CSC or adjacency lists Incremental updates: when edges/nodes change, prefer local correction over full recomputation A — Algorithm (Problem and Algorithm) Problem Restatement (Engineering) Given a directed graph G=(V,E), compute node importance scores:\nPageRank: global importance over the full graph PPR: personalized importance relative to seed distribution s Input/Output Name Type Description n int number of nodes edges List[(u,v)] directed edges u -\u0026gt; v d / alpha float damping factor, usually around 0.85 s vector PPR seed distribution (sums to 1) return vector rank score per node Example 1 (PageRank) n = 4 edges = [(0,1),(1,2),(2,0),(2,3)] Output: rank[0..3] Characteristic: 0/1/2 form a cycle, node 3 has only incoming links; score is driven by structure, not simple indegree Example 2 (PPR) Same graph, seed node set to 2 (s[2]=1) Output: ppr[0..3] Characteristic: nodes with stronger reachability from node 2 get higher scores Reasoning Path (From Naive to Usable) Naive Idea 1: Rank by Indegree Problems:\nOnly counts how many nodes point to you, not who they are Many incoming edges from low-quality nodes can mislead the ranking Naive Idea 2: Fixed-Depth Random-Walk Sampling Problems:\nHigh sampling variance and weak stability Hard to provide controllable error guarantees for online services Key Observations Importance should come from votes by high-quality nodes Voting is an iterative propagation process and can be written as linear iteration Graphs are sparse; core cost is sparse multiplication and number of iterations to convergence Method Selection PageRank: global baseline scoring PPR: user/query-seed personalized scoring Engineering focus: iterative computation + sparse storage + incremental updates C — Concepts (Core Ideas) PageRank Formula Let PR_t(u) be the score of node u at iteration t, and Out(v) be outdegree of v:\n[ PR_{t+1}(u)=\\frac{1-d}{N}+d\\sum_{v\\to u}\\frac{PR_t(v)}{Out(v)} ]\nMeaning:\nWith probability 1-d, jump randomly (prevents getting trapped in closed loops) With probability d, propagate importance along edges PPR Formula Given seed distribution s (for example, normalized distribution of nodes clicked by a user):\n[ \\pi_{t+1}=(1-\\alpha)s+\\alpha P^T\\pi_t ]\nMeaning:\nEach iteration returns to seed distribution, so the result has personalized bias When s is uniform, PPR degenerates toward standard PageRank Convergence Criterion Commonly use L1 difference:\n[ |r_{t+1}-r_t|_1\u0026lt;\\varepsilon ]\nIn production, eps is often 1e-6 ~ 1e-8, with max_iter set to avoid long-tail iteration on extreme graphs.\nPractical Guide / Steps Build the graph using adjacency lists or CSR, avoid dense matrices Handle dangling nodes (outdegree = 0) Iteratively update the rank vector Compute per-iteration error and check convergence For large online graphs, prefer warm start (use previous rank as initialization) For local graph changes, use incremental updates instead of full recomputation Runnable Example (Python) from typing import List, Tuple def pagerank(n: int, edges: List[Tuple[int, int]], d: float = 0.85, eps: float = 1e-8, max_iter: int = 100): out = [[] for _ in range(n)] for u, v in edges: out[u].append(v) rank = [1.0 / n] * n for _ in range(max_iter): new_rank = [(1.0 - d) / n for _ in range(n)] dangling_mass = 0.0 for u in range(n): if len(out[u]) == 0: dangling_mass += rank[u] else: share = rank[u] / len(out[u]) for v in out[u]: new_rank[v] += d * share # Redistribute dangling mass uniformly add_back = d * dangling_mass / n for i in range(n): new_rank[i] += add_back diff = sum(abs(new_rank[i] - rank[i]) for i in range(n)) rank = new_rank if diff \u0026lt; eps: break return rank def personalized_pagerank( n: int, edges: List[Tuple[int, int]], seed: List[float], alpha: float = 0.85, eps: float = 1e-8, max_iter: int = 100, ): out = [[] for _ in range(n)] for u, v in edges: out[u].append(v) pi = seed[:] # warm start with seed for _ in range(max_iter): new_pi = [(1.0 - alpha) * seed[i] for i in range(n)] dangling_mass = 0.0 for u in range(n): if len(out[u]) == 0: dangling_mass += pi[u] else: share = pi[u] / len(out[u]) for v in out[u]: new_pi[v] += alpha * share # Inject dangling mass back into seed distribution (more faithful to PPR semantics) for i in range(n): new_pi[i] += alpha * dangling_mass * seed[i] diff = sum(abs(new_pi[i] - pi[i]) for i in range(n)) pi = new_pi if diff \u0026lt; eps: break return pi if __name__ == \u0026#34;__main__\u0026#34;: n = 5 edges = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 2)] pr = pagerank(n, edges) print(\u0026#34;PR:\u0026#34;, [round(x, 6) for x in pr]) seed = [0.0] * n seed[2] = 1.0 ppr = personalized_pagerank(n, edges, seed) print(\u0026#34;PPR(seed=2):\u0026#34;, [round(x, 6) for x in ppr]) Run:\npython3 pagerank_demo.py E — Engineering (Engineering Applications) Scenario 1: Candidate Re-ranking in Recommendation Systems (Python) Background: recall returns 1k candidates; they need graph-structure-aware re-ranking.\nWhy this fits: PPR amplifies graph neighborhoods that are more relevant to the current user.\ndef rerank_by_score(candidates, score): return sorted(candidates, key=lambda x: score.get(x, 0.0), reverse=True) print(rerank_by_score([3, 1, 2], {1: 0.12, 2: 0.35, 3: 0.2})) Scenario 2: Influence Analysis (Go) Background: estimate node influence in social or knowledge propagation graphs.\nWhy this fits: PageRank captures the cascading value of being referenced by important nodes.\npackage main import \u0026#34;fmt\u0026#34; func topK(nodes []int, score map[int]float64, k int) []int { for i := 0; i \u0026lt; len(nodes); i++ { for j := i + 1; j \u0026lt; len(nodes); j++ { if score[nodes[j]] \u0026gt; score[nodes[i]] { nodes[i], nodes[j] = nodes[j], nodes[i] } } } if k \u0026gt; len(nodes) { k = len(nodes) } return nodes[:k] } func main() { nodes := []int{1, 2, 3, 4} score := map[int]float64{1: 0.08, 2: 0.31, 3: 0.12, 4: 0.22} fmt.Println(topK(nodes, score, 2)) } Scenario 3: Incremental Update Pipeline (JavaScript) Background: edges are added/removed daily, so full recomputation every time is too expensive.\nWhy this fits: warm start from old rank plus local updates can significantly cut latency.\nfunction warmStartUpdate(prevRank, deltaEdgesCount) { const factor = Math.max(0.9, 1 - deltaEdgesCount * 0.001); return prevRank.map((x) =\u0026gt; x * factor); } console.log(warmStartUpdate([0.2, 0.3, 0.5], 12)); R — Reflection (Reflection and Deeper Analysis) Complexity Analysis Single-iteration complexity: O(E) Total complexity: O(T * E) (T is iteration count) Space complexity: O(V + E) (adjacency list + rank vectors) Alternatives and Tradeoffs Method Advantages Limitations Indegree ranking Fast to compute Ignores source quality, noisy PageRank Globally stable, interpretable No personalization bias PPR Strong personalization Need per-seed computation, expensive at scale Sampled random walk Parallelizable, flexible approximation Harder variance and stability control Why This Plan Is Most Practical for Engineering Iterative model is simple and easy to batch and monitor Sparse matrix / adjacency lists naturally fit large graphs Warm start and incremental updates support online latency constraints Explanation and Principles (Why This Works) PageRank turns graph structure into a probability-flow conservation problem:\nEach node distributes its current score along outgoing edges Target nodes absorb quality from upstream nodes Damping guarantees traversability and convergence PPR adds a \u0026ldquo;return-to-seed\u0026rdquo; bias to this framework, binding ranking results to user/query context.\nFrequently Asked Questions and Notes Why can convergence be slow?\nalpha may be too high, graph diameter may be large, or dangling nodes may be many; lower alpha, improve preprocessing, and use warm start.\nHow should dangling nodes be handled?\nCommon choices are uniform redistribution of dangling mass, or reinjection into seed distribution for PPR.\nIs online PPR too expensive?\nIt typically needs caching, batched seeds, approximate indexes, or offline precomputation.\nWhen do incremental updates stop working well?\nIf graph structure is heavily reshuffled (large-scale edge rewrites), local correction error accumulates and periodic full recomputation is needed.\nBest Practices and Recommendations Use sparse storage (CSR/CSC or adjacency lists) as default Monitor iteration with residual, max-iteration hit rate, and top-k stability together In online systems, prefer warm start first, then gate incremental vs full by change-size threshold Use PageRank as coarse ranking feature and PPR as personalized weighting feature S — Summary (Summary) Core Takeaways Connected components answer \u0026ldquo;how to split,\u0026rdquo; while PageRank/PPR answer \u0026ldquo;who matters inside each part.\u0026rdquo; PageRank is global structural scoring; PPR is seed-oriented personalized scoring. Production rollout requires all three together: iterative computation, sparse implementation, and incremental update mechanism. For topological propagation tasks, graph databases naturally outperform pure relational join-centric views. To make systems usable online, govern convergence error, compute cost, and update frequency together. Recommended Further Reading Brin, Page. The Anatomy of a Large-Scale Hypertextual Web Search Engine Andersen et al. Local graph partitioning using PageRank vectors Neo4j GDS docs: PageRank / Personalized PageRank Closing / Conclusion PageRank/PPR is not an \u0026ldquo;old algorithm\u0026rdquo; - it is a foundational capability layer in graph computing systems.\nOnly when it is combined with connected components, SCC, and partitioning strategy do you get a complete graph-database engineering loop.\nMetadata Reading time: 15-20 minutes Tags: PageRank, PPR, Graph Database, Recommendation Systems, Incremental Updates SEO keywords: PageRank, Personalized PageRank, sparse matrix, incremental updates Meta description: From PR to PPR, a systematic guide to graph importance propagation and engineering optimization (iteration, sparsity, incrementality). Call To Action (CTA) A practical next step is to do two things:\nRun one full PageRank over your business graph and record top-k stability. Run one incremental update experiment on daily edge changes and compare full vs incremental error/latency. If you want, I can continue with an engineering comparison of HITS / SALSA versus PageRank.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) def pagerank(n, edges, d=0.85, iters=50): out = [[] for _ in range(n)] for u, v in edges: out[u].append(v) r = [1.0 / n] * n for _ in range(iters): nr = [(1 - d) / n] * n dangling = 0.0 for u in range(n): if not out[u]: dangling += r[u] continue share = r[u] / len(out[u]) for v in out[u]: nr[v] += d * share add = d * dangling / n for i in range(n): nr[i] += add r = nr return r #include \u0026lt;stdio.h\u0026gt; void pagerank_demo() { // Minimal demo: production should use CSR/CSC storage double rank[3] = {1.0/3, 1.0/3, 1.0/3}; for (int t = 0; t \u0026lt; 5; ++t) { // Detail omitted: this only demonstrates the iteration framework printf(\u0026#34;iter %d: %.6f %.6f %.6f\\n\u0026#34;, t, rank[0], rank[1], rank[2]); } } int main() { pagerank_demo(); return 0; } #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; vector\u0026lt;double\u0026gt; pagerank(int n, const vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;\u0026amp; edges, double d=0.85, int iters=50) { vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; out(n); for (auto [u,v] : edges) out[u].push_back(v); vector\u0026lt;double\u0026gt; r(n, 1.0 / n); for (int t = 0; t \u0026lt; iters; ++t) { vector\u0026lt;double\u0026gt; nr(n, (1 - d) / n); double dangling = 0.0; for (int u = 0; u \u0026lt; n; ++u) { if (out[u].empty()) { dangling += r[u]; } else { double share = r[u] / out[u].size(); for (int v : out[u]) nr[v] += d * share; } } double add = d * dangling / n; for (int i = 0; i \u0026lt; n; ++i) nr[i] += add; r.swap(nr); } return r; } int main() { vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt; edges{{0,1},{1,2},{2,0},{2,3}}; auto r = pagerank(4, edges); for (double x : r) cout \u0026lt;\u0026lt; fixed \u0026lt;\u0026lt; setprecision(6) \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } package main import \u0026#34;fmt\u0026#34; func pagerank(n int, edges [][2]int, d float64, iters int) []float64 { out := make([][]int, n) for _, e := range edges { u, v := e[0], e[1] out[u] = append(out[u], v) } r := make([]float64, n) for i := range r { r[i] = 1.0 / float64(n) } for t := 0; t \u0026lt; iters; t++ { nr := make([]float64, n) for i := range nr { nr[i] = (1.0 - d) / float64(n) } dangling := 0.0 for u := 0; u \u0026lt; n; u++ { if len(out[u]) == 0 { dangling += r[u] continue } share := r[u] / float64(len(out[u])) for _, v := range out[u] { nr[v] += d * share } } add := d * dangling / float64(n) for i := range nr { nr[i] += add } r = nr } return r } func main() { edges := [][2]int{{0, 1}, {1, 2}, {2, 0}, {2, 3}} fmt.Println(pagerank(4, edges, 0.85, 50)) } fn pagerank(n: usize, edges: \u0026amp;[(usize, usize)], d: f64, iters: usize) -\u0026gt; Vec\u0026lt;f64\u0026gt; { let mut out = vec![Vec::\u0026lt;usize\u0026gt;::new(); n]; for \u0026amp;(u, v) in edges { out[u].push(v); } let mut r = vec![1.0 / n as f64; n]; for _ in 0..iters { let mut nr = vec![(1.0 - d) / n as f64; n]; let mut dangling = 0.0; for u in 0..n { if out[u].is_empty() { dangling += r[u]; } else { let share = r[u] / out[u].len() as f64; for \u0026amp;v in \u0026amp;out[u] { nr[v] += d * share; } } } let add = d * dangling / n as f64; for x in \u0026amp;mut nr { *x += add; } r = nr; } r } fn main() { let edges = vec![(0, 1), (1, 2), (2, 0), (2, 3)]; let r = pagerank(4, \u0026amp;edges, 0.85, 50); println!(\u0026#34;{:?}\u0026#34;, r); } function pagerank(n, edges, d = 0.85, iters = 50) { const out = Array.from({ length: n }, () =\u0026gt; []); for (const [u, v] of edges) out[u].push(v); let rank = Array(n).fill(1 / n); for (let t = 0; t \u0026lt; iters; t += 1) { const next = Array(n).fill((1 - d) / n); let dangling = 0; for (let u = 0; u \u0026lt; n; u += 1) { if (out[u].length === 0) { dangling += rank[u]; } else { const share = rank[u] / out[u].length; for (const v of out[u]) next[v] += d * share; } } const add = (d * dangling) / n; for (let i = 0; i \u0026lt; n; i += 1) next[i] += add; rank = next; } return rank; } console.log(pagerank(4, [[0, 1], [1, 2], [2, 0], [2, 3]])); ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/60-pagerank-and-personalized-pagerank/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\u003cbr\u003e\nConnectivity tells you how a graph is partitioned, while PageRank tells you who matters inside each component. This is one of the core advantages of graph databases over relational databases: not only linking data, but propagating structural importance. This article follows ACERS to explain PageRank / PPR principles and production implementation.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEstimated reading time\u003c/strong\u003e: 15-20 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003ePageRank\u003c/code\u003e, \u003ccode\u003ePPR\u003c/code\u003e, \u003ccode\u003eGraph Database\u003c/code\u003e, \u003ccode\u003eSparse Matrix\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: PageRank, Personalized PageRank, sparse matrix, incremental updates, graph database\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: From classic PageRank to Personalized PageRank, covering iterative computation, sparse-matrix optimization, and incremental update strategy, with runnable multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-audience\"\u003eTarget Audience\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eEngineers building ranking, recommendation, or influence analysis on graph databases\u003c/li\u003e\n\u003cli\u003eDevelopers who already know BFS/DFS/connected components and want to level up to graph scoring\u003c/li\u003e\n\u003cli\u003eAlgorithm engineers focused on iteration performance and update latency on large online graphs\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eYou may already have split graphs into connected components and SCCs, but production systems still face a harder question:\u003c/p\u003e","title":"PageRank / Personalized PageRank: Node Importance and Incremental Updates in Graph Databases - ACERS Analysis"},{"content":" Subtitle / Abstract\nThe hard part of graph querying is not \u0026ldquo;whether you can find a path\u0026rdquo;. It is whether you can find it reliably within latency and memory budgets. This article breaks reachability into three layers: online BFS + hop limit, offline closure (usually not fully materialized), and indexed queries (2-hop / reach index), then gives a directly usable engineering decision template.\nEstimated reading time: 12-16 minutes Tags: k-hop, Reachability, BFS, Bitmap Index SEO keywords: k-hop, Reachability, Transitive Closure, 2-hop labeling, reach index Meta description: From online BFS to indexed reachability queries: hop limits, closure cost tradeoffs, and 2-hop/bitmap index selection. Target Audience Engineers working on graph databases, risk-control graphs, dependency analysis, or call-chain troubleshooting Developers who need to turn \u0026ldquo;path existence\u0026rdquo; from interview logic into production capability System designers facing the three-way tension of high query volume, large graphs, and frequent updates Background / Motivation Reachability queries are a core graph-system capability, but production systems face three practical tensions:\nQueries must be fast: typically synchronous inside API paths (millisecond-level) Graphs are large: from millions to hundreds of millions of nodes/edges Updates are frequent: index maintenance cost cannot grow without bound So you should not focus on one algorithm only. You need layered strategies by scenario:\nOnline low-latency: BFS + hop limit + early stop Offline exactness: transitive closure (usually not fully materialized) Query-heavy workloads: bitmap indexes, 2-hop labeling, reach indexes Core Concepts Concept Definition Key Cost Reachability Whether a path u -\u0026gt; v exists Query latency k-hop query Reachable set with path length \u0026lt;= k Frontier expansion size Transitive Closure Full pairwise reachability matrix Precompute and storage cost 2-hop Labeling Reachability decision via hub labels Label build and maintenance complexity Reach Index Family of query-oriented reachability indexes Index size and update cost A — Algorithm (Problem and Algorithm) Problem Restatement (Engineering Abstraction) Given a directed graph G=(V,E), support two query types:\nreachable(u, v): decide whether u can reach v k_hop(u, k): return nodes reachable from u within k hops Constraints:\nQueries must support early stop (target hit, hop limit reached, budget reached) No recursion (deep-graph risk); use iterative implementations Optional: introduce indexes to accelerate high-frequency queries Input / Output Name Type Description graph List[List[int]] Adjacency list, node IDs are 0..n-1 u, v int Source and target k int Max hops Return 1 bool Reachable or not Return 2 Set[int] k-hop neighborhood Example 1: k-hop graph = [ [1,2], # 0 [3], # 1 [3,4], # 2 [5], # 3 [], # 4 [] # 5 ] query: k_hop(0, 2) result: {0,1,2,3,4} Example 2: Reachability query: reachable(0, 5) result: true query: reachable(4, 5) result: false Reasoning Path (From Naive to Engineering-Ready) Naive Option 1: Full-graph BFS for Every Query Correct but not economical Too much repeated computation under high query frequency Naive Option 2: Full Transitive Closure (TC) Query can be O(1) But build and storage are usually too heavy (especially large graph + frequent updates) Key Observations Most online queries need only local scope (k-hop) or early-stop hits Not every graph is worth full closure materialization Indexing should follow the query/update ratio, not blind theoretical optimality Method Selection Prioritize online queries: BFS + hop limit + early stop Static graph with high query density: consider reach indexes (2-hop/bitmap) Dynamic graph with frequent updates: favor lightweight indexes + online search hybrid C — Concepts (Core Ideas) 1) BFS + Hop Limit For k-hop, BFS is the natural model because BFS layers are hop counts.\nState definition: (node, depth)\nPruning rules:\ndepth == k: do not expand neighbors further node == target: return true immediately for reachability visited_budget hits cap: return partial result or degrade 2) Reachability and Transitive Closure Transitive closure can be viewed as a boolean reachability matrix R:\nR[u][v] = 1 iff u reaches v Advantage: extremely fast queries.\nCost: expensive build, large storage, expensive updates.\nEngineering conclusion: usually do not fully materialize unless the graph is relatively static and query volume is high enough to amortize the cost.\n3) Bitmap Indexes / 2-hop Labeling / Reach Indexes 2-hop labeling decision form (directed reachability):\nFor each node x, maintain L_out(x) and L_in(x) u reaches v iff L_out(u) ∩ L_in(v) != ∅ (plus reflexive rules) Pros: very fast queries.\nChallenges: label construction and incremental maintenance are complex, and label size is highly graph-structure dependent.\nCommon engineering compromises:\nBitmap reach index (compressed storage) Hierarchical indexes + online BFS verification Landmark/Bloom prefilter + exact search fallback 4) Minimal Hand-Worked 2-hop Labeling Example Consider the directed graph:\n0 -\u0026gt; 1 -\u0026gt; 3 \\\\ ^ -\u0026gt; 2 ----| A simplified label set (for demonstration):\nL_out(0) = {1,2,3} L_out(1) = {3} L_out(2) = {3} L_in(3) = {0,1,2} For reachable(0,3), check only:\nL_out(0) ∩ L_in(3) = {1,2,3} ∩ {0,1,2} = {1,2} != ∅ So you can return reachable without expanding the full online frontier.\nThis is why 2-hop is common in read-heavy, write-light workloads: move query cost into offline preprocessing.\nPractical Guide / Steps Quantify workload first: QPS, P99, graph scale, update frequency Build a baseline: iterative BFS + hop limit + early stop Add indexes only after load testing: prioritize bitmap index or lightweight reach index If index hits fail, fall back to online BFS In strict-correctness scenarios, Bloom can only prefilter, never decide alone Runnable Python example (python3 reachability_demo.py):\nfrom collections import deque from typing import List, Set def bfs_k_hop(graph: List[List[int]], s: int, k: int) -\u0026gt; Set[int]: vis = bytearray(len(graph)) vis[s] = 1 q = deque([(s, 0)]) out = {s} while q: u, d = q.popleft() if d == k: continue for v in graph[u]: if not vis[v]: vis[v] = 1 out.add(v) q.append((v, d + 1)) return out def reachable_bfs(graph: List[List[int]], s: int, t: int, hop_limit: int | None = None) -\u0026gt; bool: vis = bytearray(len(graph)) vis[s] = 1 q = deque([(s, 0)]) while q: u, d = q.popleft() if u == t: return True if hop_limit is not None and d == hop_limit: continue for v in graph[u]: if not vis[v]: vis[v] = 1 q.append((v, d + 1)) return False def transitive_closure_small(graph: List[List[int]]) -\u0026gt; List[int]: \u0026#34;\u0026#34;\u0026#34;Small-graph demo: one bitset row per node (Python int).\u0026#34;\u0026#34;\u0026#34; n = len(graph) rows = [0] * n for u in range(n): rows[u] |= 1 \u0026lt;\u0026lt; u for v in graph[u]: rows[u] |= 1 \u0026lt;\u0026lt; v # Warshall-bitset: if u reaches k, then u also reaches all nodes that k reaches for k in range(n): mk = 1 \u0026lt;\u0026lt; k rk = rows[k] for u in range(n): if rows[u] \u0026amp; mk: rows[u] |= rk return rows def reachable_by_tc(rows: List[int], u: int, v: int) -\u0026gt; bool: return ((rows[u] \u0026gt;\u0026gt; v) \u0026amp; 1) == 1 if __name__ == \u0026#34;__main__\u0026#34;: g = [[1, 2], [3], [3, 4], [5], [], []] print(\u0026#34;k\u0026lt;=2 from 0:\u0026#34;, sorted(bfs_k_hop(g, 0, 2))) print(\u0026#34;reachable 0-\u0026gt;5:\u0026#34;, reachable_bfs(g, 0, 5)) print(\u0026#34;reachable 4-\u0026gt;5:\u0026#34;, reachable_bfs(g, 4, 5)) tc = transitive_closure_small(g) print(\u0026#34;tc 0-\u0026gt;5:\u0026#34;, reachable_by_tc(tc, 0, 5)) E — Engineering (Applications) Scenario 1: k-hop Expansion on Risk-Control Graphs (Python) Background: Expand from risky seed accounts to accounts within k hops for real-time blocking.\nWhy this fits: BFS layer semantics align naturally with hop rules and budget control.\nfrom collections import deque def risk_expand(graph, seeds, k): vis = set(seeds) q = deque((s, 0) for s in seeds) while q: u, d = q.popleft() if d == k: continue for v in graph[u]: if v not in vis: vis.add(v) q.append((v, d + 1)) return vis Scenario 2: Fast Service-Call Reachability Checks (Go) Background: During incident debugging, determine whether service A can reach service B via call chains.\nWhy this fits: Reachability can stop immediately on hit, which works well for online diagnostics.\npackage main import \u0026#34;fmt\u0026#34; func Reachable(graph [][]int, s, t int) bool { vis := make([]bool, len(graph)) q := []int{s} vis[s] = true for head := 0; head \u0026lt; len(q); head++ { u := q[head] if u == t { return true } for _, v := range graph[u] { if !vis[v] { vis[v] = true q = append(q, v) } } } return false } func main() { g := [][]int{{1, 2}, {3}, {3, 4}, {5}, {}, {}} fmt.Println(Reachable(g, 0, 5)) } Scenario 3: Bitmap Index for Static Dependency Graphs (C++) Background: Build/compile dependency graphs update infrequently, but dependency-existence checks are very frequent.\nWhy this fits: Build bitmap closure once, then answer queries with O(1) bit checks.\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; std::vector\u0026lt;unsigned long long\u0026gt; closure6(const std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; g) { int n = (int)g.size(); std::vector\u0026lt;unsigned long long\u0026gt; row(n, 0); for (int u = 0; u \u0026lt; n; ++u) { row[u] |= 1ULL \u0026lt;\u0026lt; u; for (int v : g[u]) row[u] |= 1ULL \u0026lt;\u0026lt; v; } for (int k = 0; k \u0026lt; n; ++k) { unsigned long long mk = 1ULL \u0026lt;\u0026lt; k; for (int u = 0; u \u0026lt; n; ++u) { if (row[u] \u0026amp; mk) row[u] |= row[k]; } } return row; } int main() { std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; g = {{1,2},{3},{3,4},{5},{},{}}; auto r = closure6(g); std::cout \u0026lt;\u0026lt; (((r[0] \u0026gt;\u0026gt; 5) \u0026amp; 1ULL) ? \u0026#34;reachable\u0026#34; : \u0026#34;not\u0026#34;) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } R — Reflection (Deeper Analysis) Complexity Analysis Let the actually touched subgraph during query be V' nodes and E' edges:\nOnline BFS query: O(V' + E') k-hop query: worst-case still O(V'+E'), but usually much smaller due to k Full closure: BFS from every node: O(n*(n+m)) Boolean-matrix / bitset optimization still has high precompute and storage cost Alternatives and Tradeoffs Approach Query Build Update Best Fit BFS per query Medium None None Frequent updates, low/medium query volume Full closure Very fast Very high Very high Static small/medium graph, high query density 2-hop / reach index Fast Medium-high Medium-high Query-heavy workloads tolerant of offline build Lightweight index + BFS fallback Fast (avg) Medium Medium Common compromise for most online systems Common Mistakes Blindly materializing full closure, making build/storage uncontrollable Using Bloom alone for strict-correctness reachability decisions No hop/budget limits, causing long-tail latency explosions online Why This Is Engineering-Feasible BFS + hop limit gives a low-complexity, low-maintenance baseline Indexes are introduced gradually based on query density, avoiding over-design \u0026ldquo;Index hit + search fallback\u0026rdquo; balances latency and correctness FAQs and Notes Is Reachability the same as shortest path?\nNo. Reachability only asks whether a path exists, not the minimum distance.\nShould Transitive Closure never be computed?\nNot true. It is valuable on static graphs with high query density; most dynamic online graphs just cannot maintain full closure economically.\nIs 2-hop labeling always better than BFS?\nNo. It is query-friendly but heavier to build/maintain, so it fits read-heavy, write-light scenarios.\nBest Practices and Recommendations Ship an observable BFS baseline first (with hop limits, budgets, timeouts) Use real traffic profiles to decide whether a reach index is worth introducing Prioritize maintainability in index design before theoretical optimality Keep a downgrade path: fallback to BFS when indexes degrade or fail S — Summary (Wrap-up) Core Takeaways Reachability is an \u0026ldquo;algorithm + system constraints\u0026rdquo; problem, not a single-optimal-algorithm problem For k-hop, prefer BFS + hop limit + early stop Transitive Closure can make queries fast, but usually should not be fully materialized, especially on dynamic graphs 2-hop labeling / reach indexes are best for read-heavy, write-light workloads The most stable production pattern is usually \u0026ldquo;lightweight index + online BFS fallback\u0026rdquo; Recommended Further Reading LeetCode 1971 (Find if Path Exists in Graph) LeetCode 847 (Shortest Path Visiting All Nodes, state-space search extension) Graph database query-optimization docs (Neo4j / JanusGraph neighborhood query strategies) Classic reachability-index papers (2-hop labeling / GRAIL) Metadata Reading time: 12-16 minutes Tags: Reachability, k-hop, BFS, 2-hop labeling SEO keywords: Reachability, k-hop, Transitive Closure, 2-hop labeling, reach index Meta description: Engineering reachability query strategy: BFS+hop limits, closure tradeoffs, bitmap/2-hop indexing, and online fallback. Call to Action (CTA) Two practical next steps:\nAdd hop_limit and visit_budget parameters to your current reachability API Run an A/B load test on real traffic: \u0026ldquo;BFS per query\u0026rdquo; vs \u0026ldquo;lightweight index + fallback\u0026rdquo; If you want, I can write the next post as well: \u0026ldquo;Reachability Index Implementation Guide: when to pick 2-hop, when to pick GRAIL, and when to stick with BFS.\u0026rdquo;\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) from collections import deque def reachable(graph, s, t): vis = [False] * len(graph) q = deque([s]) vis[s] = True while q: u = q.popleft() if u == t: return True for v in graph[u]: if not vis[v]: vis[v] = True q.append(v) return False def k_hop(graph, s, k): vis = [False] * len(graph) q = deque([(s, 0)]) vis[s] = True out = {s} while q: u, d = q.popleft() if d == k: continue for v in graph[u]: if not vis[v]: vis[v] = True out.add(v) q.append((v, d + 1)) return out #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #define N 6 bool reachable(int g[N][N], int s, int t) { int q[128], head = 0, tail = 0; bool vis[N] = {0}; q[tail++] = s; vis[s] = true; while (head \u0026lt; tail) { int u = q[head++]; if (u == t) return true; for (int v = 0; v \u0026lt; N; ++v) { if (g[u][v] \u0026amp;\u0026amp; !vis[v]) { vis[v] = true; q[tail++] = v; } } } return false; } int main(void) { int g[N][N] = {0}; g[0][1] = g[0][2] = 1; g[1][3] = 1; g[2][3] = g[2][4] = 1; g[3][5] = 1; printf(\u0026#34;%d\\n\u0026#34;, reachable(g, 0, 5)); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;queue\u0026gt; #include \u0026lt;vector\u0026gt; bool reachable(const std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; g, int s, int t) { std::vector\u0026lt;char\u0026gt; vis(g.size(), 0); std::queue\u0026lt;int\u0026gt; q; vis[s] = 1; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); if (u == t) return true; for (int v : g[u]) { if (!vis[v]) { vis[v] = 1; q.push(v); } } } return false; } int main() { std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; g = {{1,2},{3},{3,4},{5},{},{}}; std::cout \u0026lt;\u0026lt; reachable(g, 0, 5) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } package main import \u0026#34;fmt\u0026#34; func reachable(graph [][]int, s, t int) bool { vis := make([]bool, len(graph)) q := []int{s} vis[s] = true for head := 0; head \u0026lt; len(q); head++ { u := q[head] if u == t { return true } for _, v := range graph[u] { if !vis[v] { vis[v] = true q = append(q, v) } } } return false } func main() { g := [][]int{{1, 2}, {3}, {3, 4}, {5}, {}, {}} fmt.Println(reachable(g, 0, 5)) } use std::collections::VecDeque; fn reachable(graph: \u0026amp;Vec\u0026lt;Vec\u0026lt;usize\u0026gt;\u0026gt;, s: usize, t: usize) -\u0026gt; bool { let mut vis = vec![false; graph.len()]; let mut q = VecDeque::new(); vis[s] = true; q.push_back(s); while let Some(u) = q.pop_front() { if u == t { return true; } for \u0026amp;v in \u0026amp;graph[u] { if !vis[v] { vis[v] = true; q.push_back(v); } } } false } fn main() { let g = vec![vec![1, 2], vec![3], vec![3, 4], vec![5], vec![], vec![]]; println!(\u0026#34;{}\u0026#34;, reachable(\u0026amp;g, 0, 5)); } function reachable(graph, s, t) { const vis = Array(graph.length).fill(false); const q = [s]; let head = 0; vis[s] = true; while (head \u0026lt; q.length) { const u = q[head++]; if (u === t) return true; for (const v of graph[u]) { if (!vis[v]) { vis[v] = true; q.push(v); } } } return false; } const g = [[1, 2], [3], [3, 4], [5], [], []]; console.log(reachable(g, 0, 5)); ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/30-k-hop-reachability-and-reach-index/","summary":"This article walks through k-hop and reachability queries in practice: BFS+hop limits, transitive-closure tradeoffs, and engineering rollout paths for bitmap indexes and 2-hop labeling.","title":"k-hop and Reachability Queries: BFS Limits, Reachability Indexes, and 2-hop Labeling ACERS Analysis"},{"content":" Subtitle / Abstract\nComponents are foundational for graph algorithms: undirected graphs ask \u0026ldquo;are nodes connected,\u0026rdquo; while directed graphs ask \u0026ldquo;are nodes mutually reachable.\u0026rdquo; Following the ACERS template, this article moves from naive methods to Tarjan / Kosaraju, then shows production graph-database use cases with runnable multi-language code.\nEstimated reading time: 14-18 minutes Tags: Graph Theory, Connected Components, SCC, Tarjan SEO keywords: Connected Components, SCC, Tarjan, Kosaraju, graph database Meta description: From undirected connected components to directed SCCs, with clear Tarjan/Kosaraju mechanics, complexity, and production rollout guidance. Target Audience Learners who need BFS/DFS to become second nature Engineers doing subgraph analysis and partition planning in graph-database systems Intermediate developers who want one unified framework for \u0026ldquo;undirected CC + directed SCC\u0026rdquo; Background / Motivation In production, you quickly hit three types of questions:\nDo these nodes naturally split into disconnected groups? (undirected connected components) Which nodes form mutually reachable strong cycles? (directed SCC) How can a large graph be split into subgraphs that are more parallelizable, cache-friendly, and shard-friendly? If you only know BFS/DFS but not the \u0026ldquo;component view,\u0026rdquo; you end up repeating reachability queries with high cost and weak maintainability.\nThe value of component algorithms is: one full-graph scan turns many local queries into O(1) component-ID comparisons.\nCore Concepts Connected Components (CC): maximal node sets in an undirected graph where any two nodes are reachable Strongly Connected Components (SCC): maximal node sets in a directed graph where any two nodes are mutually reachable Condensation DAG: DAG obtained by contracting each SCC into a single node Tarjan core state: dfn[u] (timestamp), low[u] (minimum reachable timestamp), stack and in_stack Kosaraju core flow: DFS finish-order on original graph + second DFS on reversed graph A — Algorithm (Problem and Algorithm) Problem Restatement (Engineering Formulation) Given a graph G=(V,E):\nIf G is undirected, output all Connected Components; If G is directed, output all Strongly Connected Components. Also return:\nThe number of components The component ID of every node Input / Output Name Type Description n int Number of nodes (0..n-1) edges List[(u,v)] Edge list directed bool Directed or not Return (k, comp_id[]) k is component count; comp_id[i] is node i\u0026rsquo;s component ID Example 1 (Undirected CC) n = 7 edges = [(0,1),(1,2),(3,4),(5,6)] Connected components: {0,1,2}, {3,4}, {5,6} k = 3 Example 2 (Directed SCC) n = 6 edges = [(0,1),(1,2),(2,0),(2,3),(3,4),(4,3),(4,5)] Strongly connected components: {0,1,2}, {3,4}, {5} k = 3 Reasoning Path (From Naive to Optimal) Naive Approach Run one reachability search (BFS/DFS) from every node Then merge or cross-compare the resulting sets Problems:\nTime complexity inflates to O(V*(V+E)) Repeated scanning over the same edges hurts cache locality and throughput Key Observations Undirected graph: from one unvisited node, a single BFS/DFS can consume one full connected component. Directed graph: one-way reachability is not enough; you need equivalence classes of mutual reachability (SCC). Method Selection Undirected graph: iterative BFS/DFS + visited (most robust) Directed graph: Tarjan (single DFS pass, more common in production) Kosaraju: very intuitive implementation, useful for cross-checking and education C — Concepts (Core Ideas) Method Categories Graph traversal: BFS / DFS Component decomposition: Connected Components / SCC Condensation modeling: SCC -\u0026gt; DAG Tarjan Invariants Maintain during DFS:\ndfn[u]: timestamp when node u is first visited low[u]: smallest reachable dfn from u via tree edges + back edges When dfn[u] == low[u], u is the root of one SCC. Pop until u to materialize that SCC.\nEssence of Kosaraju Record postorder by DFS finish time on the original graph Build the reversed graph Run DFS on the reversed graph in reverse postorder; each DFS yields one SCC Why Tarjan Is Common in Engineering One DFS pass for SCC decomposition (no explicit reverse-graph build required) Smaller constant factors and more direct memory behavior Easier to combine with online stats (for example SCC size thresholds) Practical Guide / Steps Undirected Connected Components (Iterative) Build adjacency list Start one stack/queue traversal from every unvisited node Assign comp_id during traversal Optional early stop: If only checking whether two nodes are in the same component, stop once confirmed If only building a k-hop subgraph, limit traversal depth Directed SCC (Tarjan) Maintain global timestamp time Push node on DFS entry and initialize dfn/low Recurse into unvisited neighbors; for stack neighbors, update low When dfn==low, pop a full SCC Engineering Choice for visited bitmap: exact, predictable, good for fixed ID spaces bloom filter: memory-saving but with false positives; suitable for approximate dedup, not strict-correctness traversal Runnable Example (Python) from collections import deque from typing import List, Tuple def connected_components_undirected(n: int, edges: List[Tuple[int, int]]) -\u0026gt; Tuple[int, List[int]]: graph = [[] for _ in range(n)] for u, v in edges: graph[u].append(v) graph[v].append(u) comp = [-1] * n cid = 0 for start in range(n): if comp[start] != -1: continue queue = deque([start]) comp[start] = cid while queue: u = queue.popleft() for v in graph[u]: if comp[v] == -1: comp[v] = cid queue.append(v) cid += 1 return cid, comp def scc_tarjan(n: int, edges: List[Tuple[int, int]]) -\u0026gt; Tuple[int, List[int]]: graph = [[] for _ in range(n)] for u, v in edges: graph[u].append(v) dfn = [-1] * n low = [0] * n in_stack = [False] * n stack = [] comp = [-1] * n time = 0 scc_id = 0 def dfs(u: int) -\u0026gt; None: nonlocal time, scc_id dfn[u] = low[u] = time time += 1 stack.append(u) in_stack[u] = True for v in graph[u]: if dfn[v] == -1: dfs(v) low[u] = min(low[u], low[v]) elif in_stack[v]: low[u] = min(low[u], dfn[v]) if dfn[u] == low[u]: while True: x = stack.pop() in_stack[x] = False comp[x] = scc_id if x == u: break scc_id += 1 for i in range(n): if dfn[i] == -1: dfs(i) return scc_id, comp if __name__ == \u0026#34;__main__\u0026#34;: n1 = 7 e1 = [(0, 1), (1, 2), (3, 4), (5, 6)] k1, c1 = connected_components_undirected(n1, e1) print(\u0026#34;Undirected CC count:\u0026#34;, k1, \u0026#34;comp:\u0026#34;, c1) n2 = 6 e2 = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 3), (4, 5)] k2, c2 = scc_tarjan(n2, e2) print(\u0026#34;Directed SCC count:\u0026#34;, k2, \u0026#34;comp:\u0026#34;, c2) Run:\npython3 connected_components_demo.py E — Engineering (Applications) Scenario 1: Community Pre-grouping in Graph Databases (Python) Background: Before community analysis on a user-relationship graph, first remove isolated disconnected blocks.\nWhy this fits: Running CC first directly narrows the scope for later algorithms (for example Louvain).\ndef group_by_component(node_ids, comp_ids): groups = {} for node, cid in zip(node_ids, comp_ids): groups.setdefault(cid, []).append(node) return groups Scenario 2: Subgraph Splitting for Parallel Task Dispatch (Go) Background: Split offline graph-compute tasks by component across workers to reduce cross-worker communication.\nWhy this fits: Components are naturally independent, so tasks parallelize without cross-dependencies.\npackage main import \u0026#34;fmt\u0026#34; func bucketByComp(comp []int) map[int][]int { b := map[int][]int{} for node, cid := range comp { b[cid] = append(b[cid], node) } return b } func main() { comp := []int{0, 0, 1, 1, 2} fmt.Println(bucketByComp(comp)) } Scenario 3: Partition Hints from Component IDs (JavaScript) Background: In online graph services, you want highly coupled nodes placed on the same shard as much as possible.\nWhy this fits: SCC/CC IDs are strong signals that reduce cross-shard edge ratio.\nfunction assignShardByComp(compIds, shardCount) { return compIds.map((cid) =\u0026gt; cid % shardCount); } console.log(assignShardByComp([0, 0, 1, 1, 2, 2], 2)); R — Reflection (Deeper Analysis) Complexity Analysis Undirected CC (BFS/DFS): O(V+E), space O(V) Tarjan SCC: O(V+E), space O(V) Kosaraju SCC: O(V+E), space O(V+E) (includes reverse graph) Alternatives and Tradeoffs Method Graph Type Time Complexity Pros Limits BFS/DFS components Undirected O(V+E) Intuitive, stable Cannot handle SCC Tarjan Directed O(V+E) Single pass, production-friendly Harder than plain BFS to implement Kosaraju Directed O(V+E) Clear mental model Needs reverse graph and two DFS passes Union-Find Static undirected connectivity approx O(E α(V)) Quick to implement Not suitable for SCC Why Tarjan Is More Engineering-Feasible Better fit for online pipelines: one pass can emit SCC IDs directly No reverse-graph build required, reducing extra memory and data movement Easy to attach additional metrics: SCC size, out-edge count, inter-SCC edge ratio Explanation and Principles (Why This Works) The essence of CC is \u0026ldquo;undirected reachability equivalence classes\u0026rdquo;; one traversal can fully cover one class. The essence of SCC is \u0026ldquo;directed mutual-reachability equivalence classes\u0026rdquo;; Tarjan uses dfn/low + stack to identify cycle roots online. After mapping nodes to comp_id, many queries are reduced in dimension: \u0026ldquo;Are these in the same group?\u0026rdquo; =\u0026gt; comp_id[u] == comp_id[v] \u0026ldquo;Partition hint?\u0026rdquo; =\u0026gt; hash(comp_id) FAQs and Notes Can Tarjan be used to compute SCC on undirected graphs?\nYes, but unnecessary; direct CC is simpler for undirected graphs.\nMust Tarjan be recursive?\nNo. It can be converted to an explicit-stack iterative version, but implementation complexity is higher.\nCan Bloom filter replace visited?\nNot for strict-correctness scenarios; false positives can skip nodes that should be traversed.\nWhy is my SCC ordering different?\nAs long as SCC partitioning is correct, numbering order can vary with traversal order.\nBest Practices and Recommendations Normalize node IDs to 0..n-1 before graph algorithms to avoid mapping bugs Prefer iterative BFS/DFS for undirected CC to avoid deep-recursion stack risk For large directed graphs, prioritize Tarjan; add Kosaraju when teaching or cross-validating Persist comp_id in production and reuse it for query, cache, and sharding decisions S — Summary (Wrap-up) Core Takeaways Components are the first \u0026ldquo;dimensionality reduction layer\u0026rdquo; in graph computing; one computation supports many query types. Undirected CC and directed SCC are different problems and cannot be mixed. Tarjan identifies SCC online with dfn/low in O(V+E), which is why it is production-common. Kosaraju is great for understanding and cross-validation; Tarjan is usually better for production rollout. In graph databases, comp_id can directly support coarse community grouping, subgraph splitting, and partition hints. Recommended Further Reading Tarjan, R. (1972). Depth-first search and linear graph algorithms. CLRS graph chapters (SCC, topological sorting) Neo4j Graph Data Science docs: Connected Components / SCC Closing Note If you already know BFS/DFS, the next required step is component thinking.\nIn engineering, the real value is not \u0026ldquo;can traverse,\u0026rdquo; but turning traversal outputs into stable, reusable structured labels (comp_id).\nMetadata Reading time: 14-18 minutes Tags: Graph Theory, Connected Components, SCC, Tarjan, Graph Database SEO keywords: Connected Components, SCC, Tarjan, Kosaraju, Graph Partitioning Meta description: A systematic guide to undirected CC and directed SCC, focused on Tarjan/Kosaraju and graph-database engineering rollout. Call to Action (CTA) Two immediate actions:\nRun CC / SCC on your production graph and output a histogram of comp_id distribution. Measure cross-component edge ratio and evaluate partitioning or subgraph-level parallelization. If you want, I can continue with \u0026ldquo;3) Shortest Paths (Dijkstra / A* / Multi-source BFS)\u0026rdquo; in the same ACERS style.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) from collections import deque def connected_components_undirected(n, edges): g = [[] for _ in range(n)] for u, v in edges: g[u].append(v) g[v].append(u) comp = [-1] * n cid = 0 for s in range(n): if comp[s] != -1: continue q = deque([s]) comp[s] = cid while q: u = q.popleft() for v in g[u]: if comp[v] == -1: comp[v] = cid q.append(v) cid += 1 return cid, comp #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; typedef struct { int* data; int size; int cap; } Vec; void push(Vec* v, int x) { if (v-\u0026gt;size == v-\u0026gt;cap) { v-\u0026gt;cap = v-\u0026gt;cap ? v-\u0026gt;cap * 2 : 4; v-\u0026gt;data = (int*)realloc(v-\u0026gt;data, sizeof(int) * v-\u0026gt;cap); } v-\u0026gt;data[v-\u0026gt;size++] = x; } int main(void) { int n = 5; int comp[5] = {-1, -1, -1, -1, -1}; // Demo placeholder: in real systems, build adjacency by edges and run BFS/DFS. comp[0] = comp[1] = 0; comp[2] = comp[3] = 1; comp[4] = 2; for (int i = 0; i \u0026lt; n; ++i) printf(\u0026#34;node %d -\u0026gt; comp %d\\n\u0026#34;, i, comp[i]); return 0; } #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; pair\u0026lt;int, vector\u0026lt;int\u0026gt;\u0026gt; connectedComponentsUndirected(int n, const vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;\u0026amp; edges) { vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; g(n); for (auto [u,v] : edges) { g[u].push_back(v); g[v].push_back(u); } vector\u0026lt;int\u0026gt; comp(n, -1); int cid = 0; queue\u0026lt;int\u0026gt; q; for (int s = 0; s \u0026lt; n; ++s) { if (comp[s] != -1) continue; comp[s] = cid; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); for (int v : g[u]) { if (comp[v] == -1) { comp[v] = cid; q.push(v); } } } cid++; } return {cid, comp}; } int main() { vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt; edges = {{0,1},{1,2},{3,4}}; auto [k, comp] = connectedComponentsUndirected(5, edges); cout \u0026lt;\u0026lt; \u0026#34;k=\u0026#34; \u0026lt;\u0026lt; k \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; for (int i = 0; i \u0026lt; (int)comp.size(); ++i) cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34;:\u0026#34; \u0026lt;\u0026lt; comp[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } package main import \u0026#34;fmt\u0026#34; func connectedComponentsUndirected(n int, edges [][2]int) (int, []int) { g := make([][]int, n) for _, e := range edges { u, v := e[0], e[1] g[u] = append(g[u], v) g[v] = append(g[v], u) } comp := make([]int, n) for i := range comp { comp[i] = -1 } cid := 0 q := make([]int, 0) for s := 0; s \u0026lt; n; s++ { if comp[s] != -1 { continue } comp[s] = cid q = append(q, s) for len(q) \u0026gt; 0 { u := q[0] q = q[1:] for _, v := range g[u] { if comp[v] == -1 { comp[v] = cid q = append(q, v) } } } cid++ } return cid, comp } func main() { edges := [][2]int{{0, 1}, {1, 2}, {3, 4}} k, comp := connectedComponentsUndirected(5, edges) fmt.Println(k, comp) } use std::collections::VecDeque; fn connected_components_undirected(n: usize, edges: \u0026amp;[(usize, usize)]) -\u0026gt; (usize, Vec\u0026lt;i32\u0026gt;) { let mut g = vec![vec![]; n]; for \u0026amp;(u, v) in edges { g[u].push(v); g[v].push(u); } let mut comp = vec![-1; n]; let mut cid: i32 = 0; for s in 0..n { if comp[s] != -1 { continue; } let mut q = VecDeque::new(); comp[s] = cid; q.push_back(s); while let Some(u) = q.pop_front() { for \u0026amp;v in \u0026amp;g[u] { if comp[v] == -1 { comp[v] = cid; q.push_back(v); } } } cid += 1; } (cid as usize, comp) } fn main() { let edges = vec![(0, 1), (1, 2), (3, 4)]; let (k, comp) = connected_components_undirected(5, \u0026amp;edges); println!(\u0026#34;{} {:?}\u0026#34;, k, comp); } function connectedComponentsUndirected(n, edges) { const g = Array.from({ length: n }, () =\u0026gt; []); for (const [u, v] of edges) { g[u].push(v); g[v].push(u); } const comp = Array(n).fill(-1); let cid = 0; for (let s = 0; s \u0026lt; n; s += 1) { if (comp[s] !== -1) continue; const queue = [s]; comp[s] = cid; while (queue.length) { const u = queue.shift(); for (const v of g[u]) { if (comp[v] === -1) { comp[v] = cid; queue.push(v); } } } cid += 1; } return [cid, comp]; } console.log(connectedComponentsUndirected(5, [[0, 1], [1, 2], [3, 4]])); ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/40-connected-components-and-scc-tarjan-kosaraju/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\u003cbr\u003e\nComponents are foundational for graph algorithms: undirected graphs ask \u0026ldquo;are nodes connected,\u0026rdquo; while directed graphs ask \u0026ldquo;are nodes mutually reachable.\u0026rdquo; Following the ACERS template, this article moves from naive methods to Tarjan / Kosaraju, then shows production graph-database use cases with runnable multi-language code.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEstimated reading time\u003c/strong\u003e: 14-18 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eGraph Theory\u003c/code\u003e, \u003ccode\u003eConnected Components\u003c/code\u003e, \u003ccode\u003eSCC\u003c/code\u003e, \u003ccode\u003eTarjan\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Connected Components, SCC, Tarjan, Kosaraju, graph database\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: From undirected connected components to directed SCCs, with clear Tarjan/Kosaraju mechanics, complexity, and production rollout guidance.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-audience\"\u003eTarget Audience\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLearners who need BFS/DFS to become second nature\u003c/li\u003e\n\u003cli\u003eEngineers doing subgraph analysis and partition planning in graph-database systems\u003c/li\u003e\n\u003cli\u003eIntermediate developers who want one unified framework for \u0026ldquo;undirected CC + directed SCC\u0026rdquo;\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eIn production, you quickly hit three types of questions:\u003c/p\u003e","title":"Connected Components and Strongly Connected Components: Tarjan / Kosaraju ACERS Engineering Analysis"},{"content":" Subtitle / Abstract\nShortest path is not one question. It is an engineering skill set: choose the right algorithm by graph conditions. This ACERS article breaks down BFS (unweighted) / Dijkstra (non-negative weights) / A (heuristic)* and gives optimization templates you actually use in relationship graphs, recommendation paths, and path explanations.\nEstimated reading time: 14-18 minutes Tags: Graph Theory, shortest path, BFS, Dijkstra, A* SEO keywords: shortest path, BFS, Dijkstra, A*, bidirectional search, multi-source BFS Meta description: Engineering guide to the shortest-path core trio: algorithm boundaries, complexity, runnable code, optimization strategies, and practical scenarios. Target Audience Learners reinforcing graph fundamentals who want reusable engineering templates Backend/algorithm engineers working on social links, recommendation paths, or graph-query explanations Developers who know BFS, Dijkstra, and A* by name but still struggle with robust selection and optimization Background / Motivation Shortest-path problems are common in:\nMinimal relationship chains in social networks (how many hops apart) Minimum-cost paths in recommendation systems (multi-objective trade-offs) \u0026ldquo;Why this was recommended\u0026rdquo; path displays in explainability systems The most common production mistake is forcing one algorithm onto every scenario:\nRunning BFS on weighted graphs (wrong result, no explicit error) Running Dijkstra on negative edges (unreliable result) Using A* with a poor heuristic (performance degrades to Dijkstra) In essence, shortest-path solutions should start with graph-condition classification, then algorithm selection.\nCore Concepts Algorithm Suitable Graph Optimality Condition Typical Complexity Keywords BFS Unweighted / equal-weight graph First arrival by layer gives minimum edge count O(V+E) queue, level Dijkstra Non-negative weighted graph Node popped from heap already has optimal distance O((V+E)logV) relaxation, min-heap A* Non-negative weighted graph + heuristic h(n) is admissible (never overestimates) Worst case same as Dijkstra, usually faster f=g+h Key formulas:\nDijkstra relaxation: update when dist[v] \u0026gt; dist[u] + w(u,v) A evaluation function*: f(n) = g(n) + h(n) Where:\ng(n) is known cost from start to n h(n) is heuristic estimated cost from n to target A - Algorithm (Problem and Algorithm) Unified Problem Model Given graph G=(V,E), start node s, and target node t, find both shortest path length and path from s to t.\nThe graph may be unweighted, or weighted with non-negative edge weights.\nInput and Output Name Type Description graph Adjacency list Graph structure; graph[u] is neighbors or (neighbor, weight) s Node ID Start node t Node ID Target node Return Distance + path Return INF/null or empty path when unreachable Example 1 (Unweighted graph) A -\u0026gt; B -\u0026gt; D A -\u0026gt; C -\u0026gt; D minimum edge count from A to D = 2 valid path: A-B-D or A-C-D Example 2 (Non-negative weighted graph) A -\u0026gt; B (2) A -\u0026gt; C (5) B -\u0026gt; C (1) B -\u0026gt; D (4) C -\u0026gt; D (1) minimum cost from A to D = 4 path: A-B-C-D Derivation (From naive to production-ready) Naive idea: enumerate all paths Use DFS to enumerate all s -\u0026gt; t paths, then choose minimum In cyclic graphs, dedup logic is complex and path count can be exponential Conclusion: not practical except on tiny graphs.\nKey Observation 1: if edge weights are equal, layer index is cost Shortest path reduces to \u0026ldquo;fewest edges\u0026rdquo; BFS expands by layers; first reach of target is optimal Key Observation 2: with non-negative weights, greedy shortest-prefix expansion works Dijkstra pops the node with current minimum dist With non-negative weights, popped nodes cannot be improved later Key Observation 3: if you can estimate how far a node is from target, search shrinks A* adds heuristic h(n) on top of Dijkstra Search is guided toward target, reducing irrelevant expansion C - Concepts (Core Ideas) Method Categories BFS: layered traversal + minimum hop count Dijkstra: shortest-path tree + relaxation + min-heap A*: shortest path + heuristic best-first search Relationship Among the Three Dijkstra is A* with h(n)=0 BFS is Dijkstra when all edge weights are 1 A* performance depends heavily on heuristic quality: Too weak: degrades to Dijkstra Too aggressive and overestimating: may lose optimality Engineering Selection Matrix Problem Feature Preferred Algorithm Notes Unweighted graph / minimum hops BFS Relationship chains, k-hop search Minimum cost with non-negative weights Dijkstra Stable default for backend services Non-negative weights + usable heuristic A* Road networks, spatial graphs, explanation paths Negative edges present Bellman-Ford/Johnson Do not use Dijkstra/A* Practical Guide / Steps Step 1: classify graph conditions first Unweighted or equal-weight? Yes -\u0026gt; BFS Any negative edges? Yes -\u0026gt; cannot use Dijkstra/A* Usable heuristic available? Yes -\u0026gt; prefer A* Step 2: unify path reconstruction interface Maintain parent mapping: parent[v] = u, then backtrack from t to s.\nStep 3: implement runnable template (Python) from collections import deque import heapq from math import inf def reconstruct_path(parent, s, t): if t not in parent and s != t: return [] path = [t] while path[-1] != s: path.append(parent[path[-1]]) path.reverse() return path def bfs_shortest_path(graph, s, t, max_depth=None): \u0026#34;\u0026#34;\u0026#34;graph[u] = [v1, v2, ...]\u0026#34;\u0026#34;\u0026#34; q = deque([(s, 0)]) parent = {s: s} visited = {s} while q: u, d = q.popleft() if u == t: return d, reconstruct_path(parent, s, t) if max_depth is not None and d \u0026gt;= max_depth: continue for v in graph.get(u, []): if v not in visited: visited.add(v) parent[v] = u q.append((v, d + 1)) return inf, [] def dijkstra_shortest_path(graph, s, t, max_cost=None): \u0026#34;\u0026#34;\u0026#34;graph[u] = [(v, w), ...], w \u0026gt;= 0\u0026#34;\u0026#34;\u0026#34; dist = {s: 0.0} parent = {s: s} pq = [(0.0, s)] while pq: du, u = heapq.heappop(pq) if du != dist.get(u, inf): continue if max_cost is not None and du \u0026gt; max_cost: continue if u == t: return du, reconstruct_path(parent, s, t) for v, w in graph.get(u, []): nd = du + w if nd \u0026lt; dist.get(v, inf): dist[v] = nd parent[v] = u heapq.heappush(pq, (nd, v)) return inf, [] def astar_shortest_path(graph, s, t, h): \u0026#34;\u0026#34;\u0026#34;h(u) is admissible heuristic estimate from u to t\u0026#34;\u0026#34;\u0026#34; g = {s: 0.0} parent = {s: s} pq = [(h(s), s)] # (f, node) while pq: f, u = heapq.heappop(pq) if u == t: return g[u], reconstruct_path(parent, s, t) for v, w in graph.get(u, []): ng = g[u] + w if ng \u0026lt; g.get(v, inf): g[v] = ng parent[v] = u heapq.heappush(pq, (ng + h(v), v)) return inf, [] if __name__ == \u0026#34;__main__\u0026#34;: unweighted = { \u0026#34;A\u0026#34;: [\u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;], \u0026#34;B\u0026#34;: [\u0026#34;D\u0026#34;], \u0026#34;C\u0026#34;: [\u0026#34;D\u0026#34;], \u0026#34;D\u0026#34;: [], } print(bfs_shortest_path(unweighted, \u0026#34;A\u0026#34;, \u0026#34;D\u0026#34;)) # (2, [\u0026#39;A\u0026#39;, \u0026#39;B\u0026#39;, \u0026#39;D\u0026#39;]) or C path weighted = { \u0026#34;A\u0026#34;: [(\u0026#34;B\u0026#34;, 2), (\u0026#34;C\u0026#34;, 5)], \u0026#34;B\u0026#34;: [(\u0026#34;C\u0026#34;, 1), (\u0026#34;D\u0026#34;, 4)], \u0026#34;C\u0026#34;: [(\u0026#34;D\u0026#34;, 1)], \u0026#34;D\u0026#34;: [], } print(dijkstra_shortest_path(weighted, \u0026#34;A\u0026#34;, \u0026#34;D\u0026#34;)) # (4.0, [\u0026#39;A\u0026#39;, \u0026#39;B\u0026#39;, \u0026#39;C\u0026#39;, \u0026#39;D\u0026#39;]) heuristic = {\u0026#34;A\u0026#34;: 3, \u0026#34;B\u0026#34;: 2, \u0026#34;C\u0026#34;: 1, \u0026#34;D\u0026#34;: 0} print(astar_shortest_path(weighted, \u0026#34;A\u0026#34;, \u0026#34;D\u0026#34;, lambda x: heuristic[x])) E - Engineering (Production Applications) Scenario 1: shortest social link chain (BFS + bidirectional BFS) Background: given user A and user B, find a shortest relationship chain for explainability display.\nWhy it fits: this is an unweighted graph and objective is minimum hops; BFS matches naturally, and bidirectional BFS further reduces expansions.\nfrom collections import deque def bidirectional_bfs(graph, s, t, max_depth=6): if s == t: return 0 qa, qb = deque([s]), deque([t]) da, db = {s: 0}, {t: 0} while qa and qb: # expand smaller frontier first if len(qa) \u0026lt;= len(qb): q, dcur, dother = qa, da, db else: q, dcur, dother = qb, db, da u = q.popleft() if dcur[u] \u0026gt;= max_depth: continue for v in graph.get(u, []): if v in dcur: continue dcur[v] = dcur[u] + 1 if v in dother: return dcur[v] + dother[v] q.append(v) return -1 Scenario 2: recommendation path (Dijkstra) Background: edge weights represent \u0026ldquo;cost\u0026rdquo; (latency, risk, penalty); we need the lowest total cost path.\nWhy it fits: with non-negative weights, Dijkstra is stable and straightforward to service-ify.\npackage main import ( \u0026#34;container/heap\u0026#34; \u0026#34;fmt\u0026#34; ) type Edge struct{ To string; W float64 } type Item struct{ D float64; U string } type PQ []Item func (p PQ) Len() int { return len(p) } func (p PQ) Less(i, j int) bool { return p[i].D \u0026lt; p[j].D } func (p PQ) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func (p *PQ) Push(x interface{}) { *p = append(*p, x.(Item)) } func (p *PQ) Pop() interface{} { old := *p; x := old[len(old)-1]; *p = old[:len(old)-1]; return x } func dijkstra(g map[string][]Edge, s, t string) float64 { const INF = 1e18 dist := map[string]float64{s: 0} pq := \u0026amp;PQ{{0, s}} heap.Init(pq) for pq.Len() \u0026gt; 0 { it := heap.Pop(pq).(Item) if it.D != dist[it.U] { continue } if it.U == t { return it.D } for _, e := range g[it.U] { nd := it.D + e.W if d, ok := dist[e.To]; !ok || nd \u0026lt; d { dist[e.To] = nd heap.Push(pq, Item{nd, e.To}) } } } return INF } func main() { g := map[string][]Edge{ \u0026#34;A\u0026#34;: {{\u0026#34;B\u0026#34;, 2}, {\u0026#34;C\u0026#34;, 5}}, \u0026#34;B\u0026#34;: {{\u0026#34;C\u0026#34;, 1}, {\u0026#34;D\u0026#34;, 4}}, \u0026#34;C\u0026#34;: {{\u0026#34;D\u0026#34;, 1}}, } fmt.Println(dijkstra(g, \u0026#34;A\u0026#34;, \u0026#34;D\u0026#34;)) // 4 } Scenario 3: explanation path in relationship graphs (A* + path pruning) Background: to show users \u0026ldquo;why X was recommended to Y,\u0026rdquo; we need explainable paths with controlled query latency.\nWhy it fits: A* can use domain priors (similarity distance) to cut expansions; combine with maxDepth pruning to control cost.\nfunction astar(graph, s, t, h, maxDepth = 6) { const g = new Map([[s, 0]]); const pq = [[h(s), 0, s]]; // [f, depth, node] while (pq.length) { pq.sort((a, b) =\u0026gt; a[0] - b[0]); const [f, depth, u] = pq.shift(); if (u === t) return g.get(u); if (depth \u0026gt;= maxDepth) continue; for (const [v, w] of (graph.get(u) || [])) { const ng = g.get(u) + w; if (!g.has(v) || ng \u0026lt; g.get(v)) { g.set(v, ng); pq.push([ng + h(v), depth + 1, v]); } } } return Infinity; } Optimization Essentials (Must-know) 1) Multi-source BFS Queue multiple sources at once and run one unified BFS. Useful for tasks like \u0026ldquo;distance to nearest interest point\u0026rdquo; or \u0026ldquo;batch infection radius spread.\u0026rdquo;\nfrom collections import deque def multi_source_bfs(graph, sources): q = deque(sources) dist = {s: 0 for s in sources} while q: u = q.popleft() for v in graph.get(u, []): if v not in dist: dist[v] = dist[u] + 1 q.append(v) return dist 2) Bidirectional BFS / bidirectional Dijkstra Bidirectional BFS: usually cuts search depth significantly on unweighted graphs Bidirectional Dijkstra: can reduce state expansion on non-negative weighted graphs, with higher implementation complexity 3) Path pruning (max_depth / max_cost) In online services, guarantee usable latency first, then optimize coverage:\nBFS: max_depth Dijkstra: max_cost A*: max_depth + heuristic 4) visited bitmap / bloom bitmap: exact and memory-controllable (prefer when node IDs can be mapped to contiguous integers) bloom: more space-efficient but has false positives; suitable for recall-oriented prefiltering, not for strict-optimality main decision chains R - Reflection (Deep Dive) Complexity Comparison Algorithm Time Complexity Space Complexity BFS O(V+E) O(V) Dijkstra (heap) O((V+E)logV) O(V) A* Worst case same as Dijkstra O(V) Alternatives and Trade-offs Approach Conditions Cost When to Choose Bellman-Ford Negative weights allowed O(VE) Must support negative edges Floyd-Warshall All-pairs shortest paths O(V^3) Small offline graph, full-pair queries Core trio in this article High-frequency online queries Low to medium Most online path problems in engineering Common Wrong Approaches Using BFS on weighted graphs Ignoring negative-edge checks and running Dijkstra directly Using an unreasonable heuristic in A*, causing heavy invalid expansion Marking visited too early (can miss better paths in weighted graphs) Why This Set Is the Most Practical in Engineering Covers the most common graph conditions (unweighted + non-negative weights + heuristics) Supports unified interface abstraction so business layers only care about a \u0026ldquo;path query service\u0026rdquo; Composes naturally with bidirectional search and pruning for SLA control Explanation and Principles (Why this works) You can view these three methods as one evolution line:\nBFS: expand by layers for equal edge-cost settings Dijkstra: expand by minimum known true cost for non-negative weighted settings A*: add target-oriented heuristics to Dijkstra to reduce irrelevant expansion The core difference is not coding style, but what determines expansion order:\nBFS uses level Dijkstra uses g A* uses g+h FAQ and Notes What if the graph is disconnected?\nReturn unreachable (INF or empty path). Do not force path backtracking.\nWhen should visited be finalized in Dijkstra?\nRecommended: finalize when the node is popped and confirmed as the current optimal dist.\nHow to choose h(n) in A?*\nRoad networks often use Manhattan/Euclidean distance. Recommendation graphs can use embedding-distance lower bounds. Avoid systematic overestimation.\nWhen should I use bidirectional search?\nWhen both endpoints are known and the graph is large with high branching factor, benefits are usually significant.\nBest Practices and Recommendations Validate graph conditions first (unweighted? negative weights? usable heuristic?) Make path reconstruction, pruning, and logging metrics a shared middleware layer In online services, prioritize tail-latency guarantees before maximum coverage On large graphs, prefer adjacency lists + ID compression + bitmap visited S - Summary Key Takeaways BFS, Dijkstra, and A* are the shortest-path engineering core trio, and the key is condition-based selection Use BFS for unweighted graphs, Dijkstra for non-negative weighted graphs, and A* when reliable heuristics exist Multi-source, bidirectional search, and pruning are not optional polish; they are primary tools for online performance/cost control A* performance ceiling depends on heuristic quality; weak heuristics degrade performance A unified path-service interface significantly lowers switching cost among algorithms Recommended Follow-up Reading LeetCode 127 (Word Ladder, bidirectional BFS) LeetCode 743 (Network Delay Time, Dijkstra) Classic A* paper: Hart, Nilsson, Raphael (1968) Negative-weight scenarios: Bellman-Ford / Johnson Metadata Reading time: 14-18 minutes Tags: Graph Theory, shortest path, BFS, Dijkstra, A*, bidirectional search SEO keywords: shortest path, BFS, Dijkstra, A*, bidirectional BFS, multi-source BFS Meta description: Engineering guide to the shortest-path core trio: algorithm boundaries, complexity, optimization strategies, and runnable code. Call To Action (CTA) Recommended next steps using the same template:\nRefactor your current graph-query API into a pluggable algorithm interface (switchable BFS/Dijkstra/A*) Add online metrics: expanded node count, average path length, and P95 query latency If you want, I can write the next article directly: \u0026ldquo;Engineering shortest paths on negative-weight graphs (Bellman-Ford/Johnson).\u0026rdquo;\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) # Dijkstra (non-negative weights), adjacency list import heapq from math import inf def dijkstra(graph, s, t): dist = {s: 0.0} parent = {s: s} pq = [(0.0, s)] while pq: du, u = heapq.heappop(pq) if du != dist.get(u, inf): continue if u == t: break for v, w in graph.get(u, []): nd = du + w if nd \u0026lt; dist.get(v, inf): dist[v] = nd parent[v] = u heapq.heappush(pq, (nd, v)) if t not in dist: return inf, [] path = [t] while path[-1] != s: path.append(parent[path[-1]]) path.reverse() return dist[t], path // Dijkstra O(V^2) demo for dense/small graphs (non-negative weights) #include \u0026lt;stdio.h\u0026gt; #define N 5 #define INF 1000000000 int main(void) { int g[N][N] = { {0, 2, 5, 0, 0}, {0, 0, 1, 4, 0}, {0, 0, 0, 1, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0} }; int s = 0, t = 3; int dist[N], vis[N] = {0}; for (int i = 0; i \u0026lt; N; i++) dist[i] = INF; dist[s] = 0; for (int i = 0; i \u0026lt; N; i++) { int u = -1; for (int j = 0; j \u0026lt; N; j++) if (!vis[j] \u0026amp;\u0026amp; (u == -1 || dist[j] \u0026lt; dist[u])) u = j; if (u == -1 || dist[u] == INF) break; vis[u] = 1; for (int v = 0; v \u0026lt; N; v++) { if (g[u][v] \u0026gt; 0 \u0026amp;\u0026amp; dist[v] \u0026gt; dist[u] + g[u][v]) dist[v] = dist[u] + g[u][v]; } } if (dist[t] \u0026gt;= INF) printf(\u0026#34;unreachable\\n\u0026#34;); else printf(\u0026#34;dist=%d\\n\u0026#34;, dist[t]); return 0; } #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; pair\u0026lt;long long, vector\u0026lt;int\u0026gt;\u0026gt; dijkstra(int n, vector\u0026lt;vector\u0026lt;pair\u0026lt;int,int\u0026gt;\u0026gt;\u0026gt;\u0026amp; g, int s, int t) { const long long INF = (1LL\u0026lt;\u0026lt;60); vector\u0026lt;long long\u0026gt; dist(n, INF); vector\u0026lt;int\u0026gt; parent(n, -1); priority_queue\u0026lt;pair\u0026lt;long long,int\u0026gt;, vector\u0026lt;pair\u0026lt;long long,int\u0026gt;\u0026gt;, greater\u0026lt;pair\u0026lt;long long,int\u0026gt;\u0026gt;\u0026gt; pq; dist[s] = 0; parent[s] = s; pq.push({0, s}); while (!pq.empty()) { auto [du, u] = pq.top(); pq.pop(); if (du != dist[u]) continue; if (u == t) break; for (auto [v, w] : g[u]) { long long nd = du + w; if (nd \u0026lt; dist[v]) { dist[v] = nd; parent[v] = u; pq.push({nd, v}); } } } if (dist[t] == INF) return {INF, {}}; vector\u0026lt;int\u0026gt; path; for (int x = t; x != s; x = parent[x]) path.push_back(x); path.push_back(s); reverse(path.begin(), path.end()); return {dist[t], path}; } package main import ( \u0026#34;container/heap\u0026#34; \u0026#34;fmt\u0026#34; ) type Edge struct{ To int; W int64 } type Item struct{ D int64; U int } type PQ []Item func (p PQ) Len() int { return len(p) } func (p PQ) Less(i, j int) bool { return p[i].D \u0026lt; p[j].D } func (p PQ) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func (p *PQ) Push(x interface{}) { *p = append(*p, x.(Item)) } func (p *PQ) Pop() interface{} { old := *p; x := old[len(old)-1]; *p = old[:len(old)-1]; return x } func dijkstra(g [][]Edge, s, t int) int64 { const INF int64 = 1\u0026lt;\u0026lt;60 dist := make([]int64, len(g)) for i := range dist { dist[i] = INF } dist[s] = 0 pq := \u0026amp;PQ{{0, s}} heap.Init(pq) for pq.Len() \u0026gt; 0 { it := heap.Pop(pq).(Item) if it.D != dist[it.U] { continue } if it.U == t { return it.D } for _, e := range g[it.U] { nd := it.D + e.W if nd \u0026lt; dist[e.To] { dist[e.To] = nd heap.Push(pq, Item{nd, e.To}) } } } return INF } func main() { g := make([][]Edge, 4) g[0] = []Edge{{1, 2}, {2, 5}} g[1] = []Edge{{2, 1}, {3, 4}} g[2] = []Edge{{3, 1}} fmt.Println(dijkstra(g, 0, 3)) // 4 } use std::cmp::Reverse; use std::collections::BinaryHeap; fn dijkstra(graph: \u0026amp;Vec\u0026lt;Vec\u0026lt;(usize, i64)\u0026gt;\u0026gt;, s: usize, t: usize) -\u0026gt; i64 { let inf = i64::MAX / 4; let mut dist = vec![inf; graph.len()]; let mut pq = BinaryHeap::new(); dist[s] = 0; pq.push((Reverse(0_i64), s)); while let Some((Reverse(du), u)) = pq.pop() { if du != dist[u] { continue; } if u == t { return du; } for \u0026amp;(v, w) in \u0026amp;graph[u] { let nd = du + w; if nd \u0026lt; dist[v] { dist[v] = nd; pq.push((Reverse(nd), v)); } } } inf } fn main() { let g = vec![ vec![(1,2),(2,5)], vec![(2,1),(3,4)], vec![(3,1)], vec![] ]; println!(\u0026#34;{}\u0026#34;, dijkstra(\u0026amp;g, 0, 3)); // 4 } // BFS shortest hops in unweighted graph function bfsShortest(graph, s, t) { const q = [[s, 0]]; const seen = new Set([s]); while (q.length) { const [u, d] = q.shift(); if (u === t) return d; for (const v of (graph.get(u) || [])) { if (!seen.has(v)) { seen.add(v); q.push([v, d + 1]); } } } return Infinity; } const g = new Map([ [\u0026#34;A\u0026#34;, [\u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;]], [\u0026#34;B\u0026#34;, [\u0026#34;D\u0026#34;]], [\u0026#34;C\u0026#34;, [\u0026#34;D\u0026#34;]], [\u0026#34;D\u0026#34;, []], ]); console.log(bfsShortest(g, \u0026#34;A\u0026#34;, \u0026#34;D\u0026#34;)); // 2 ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/20-shortest-path-bfs-dijkstra-astar-acers/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\u003cbr\u003e\nShortest path is not one question. It is an engineering skill set: choose the right algorithm by graph conditions. This ACERS article breaks down \u003cem\u003e\u003cem\u003eBFS (unweighted) / Dijkstra (non-negative weights) / A\u003c/em\u003e (heuristic)\u003c/em\u003e* and gives optimization templates you actually use in relationship graphs, recommendation paths, and path explanations.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEstimated reading time\u003c/strong\u003e: 14-18 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eGraph Theory\u003c/code\u003e, \u003ccode\u003eshortest path\u003c/code\u003e, \u003ccode\u003eBFS\u003c/code\u003e, \u003ccode\u003eDijkstra\u003c/code\u003e, \u003ccode\u003eA*\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: shortest path, BFS, Dijkstra, A*, bidirectional search, multi-source BFS\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Engineering guide to the shortest-path core trio: algorithm boundaries, complexity, runnable code, optimization strategies, and practical scenarios.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-audience\"\u003eTarget Audience\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLearners reinforcing graph fundamentals who want reusable engineering templates\u003c/li\u003e\n\u003cli\u003eBackend/algorithm engineers working on social links, recommendation paths, or graph-query explanations\u003c/li\u003e\n\u003cli\u003eDevelopers who know BFS, Dijkstra, and A* by name but still struggle with robust selection and optimization\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eShortest-path problems are common in:\u003c/p\u003e","title":"Shortest Path Core Trio: BFS, Dijkstra, and A* ACERS Engineering Breakdown"},{"content":" Subtitle / Abstract\nBFS / DFS are not just about \u0026ldquo;being able to code them.\u0026rdquo; You need production-ready behavior, predictable cost, and provable correctness. Following the ACERS structure, this article breaks three common tasks (k-hop query, subgraph extraction, and path existence) into reusable templates: iterative implementation + early stop + visited structure selection.\nEstimated reading time: 12-16 minutes Tags: Graph, BFS, DFS, k-hop, subgraph extraction SEO keywords: BFS, DFS, k-hop query, subgraph extraction, path existence, visited bitmap, bloom filter Meta description: BFS/DFS for engineering scenarios: iterative implementations to avoid stack overflow, early stop to cut search cost, and visited bitmap/bloom to optimize memory and dedup performance. Target Audience Engineers working on graph databases, risk-control relationship graphs, or call-chain analysis Learners who can write \u0026ldquo;problem-solution style BFS/DFS\u0026rdquo; but do not yet have engineering templates Developers who want traversal code that is stable, observable, and extensible Background / Motivation In production systems, BFS/DFS is usually not a one-off offline script. It is part of an online request path:\nk-hop neighborhood queries need latency control Subgraph extraction needs memory and output-size control Path existence checks need fast true/false responses If you stop at textbook recursive templates, you quickly hit issues:\nDeep graphs cause recursive stack overflow No pruning causes unnecessary expansion The wrong visited structure hurts both memory and throughput So this article has one focus: upgrade BFS/DFS to a level where you can use them fluently in production.\nCore Concepts Concept Purpose Engineering Focus BFS (queue) Layer-by-layer expansion, natural support for hop levels Good for k-hop, minimum edge count, layered subgraphs DFS (stack) Deep exploration, efficient for path existence checks Good for fast reachability decisions and depth-based pruning early stop Stop search as soon as conditions are met Controls P99 latency and resource usage visited bitmap Exact dedup with compact memory Requires node ID compression first bloom filter Probabilistic dedup / prefilter Has false positives; cannot be used alone in strict-correctness tasks A - Algorithm (Problem and Algorithm) Problem Restatement (LeetCode-style training problem) Given an unweighted graph G (adjacency list), a start node s, a maximum hop count K, and an optional target node t:\nReturn the set of nodes reachable from s within K hops (k-hop query) Return the subgraph formed by visited nodes and edges (subgraph extraction) Determine whether a path s -\u0026gt; t exists (path existence) Requirements:\nUse iterative BFS/DFS (no recursion) Support early stop (for example: beyond K hops, target hit, or business predicate hit) Maintain visited state to avoid repeated expansion Input and Output Name Type Description graph List[List[int]] Adjacency list, node IDs are 0..n-1 s int Start node K int Maximum hop count (for BFS) t int Target node (for reachability) Return 1 Set[int] Nodes reachable within K hops Return 2 List[Tuple[int,int]] Extracted edge set (optional) Return 3 bool Reachable or not Example 1: k-hop query graph = [ [1,2], # 0 [3], # 1 [3,4], # 2 [5], # 3 [], # 4 [] # 5 ] s = 0, K = 2 output nodes: {0,1,2,3,4} Explanation: within 2 hops, you can reach 0 (0 hop), 1/2 (1 hop), 3/4 (2 hops). Node 5 needs 3 hops.\nExample 2: path existence same graph as above s = 0, t = 5 output: true Derivation (From naive approach to engineering template) Naive version: recursive DFS / BFS without pruning Recursive DFS can hit stack-depth limits on deep graphs BFS without hop limits may scan the full graph Without visited state, expansions can repeat exponentially Key Observations In practice, you usually want not \u0026ldquo;full-graph traversal\u0026rdquo; but the minimal traversal that satisfies business constraints Search order can be templated (queue/BFS, stack/DFS), but pruning must be business-aware visited is not one fixed implementation. You must choose based on graph scale and correctness needs Method Selection k-hop: prefer BFS (naturally layered) Path existence: prefer iterative DFS (stack + early stop) Large graphs: ID compression + bitmap; for high-throughput, weak-consistency dedup, add bloom prefiltering C - Concepts (Core Ideas) Method Categories Graph Traversal Layered Search (BFS) Depth Search (DFS) Pruned Search Engineering Invariants visited[u] = true means node u has been queued/stacked (or consumed, depending on policy) In BFS, (node, depth).depth never exceeds K After an early-stop condition triggers, returned results still satisfy business-defined correctness Early-Stop Design Template hop limit: stop expanding neighbors when depth == K target hit: return immediately when node == t budget control: stop when visited-node count exceeds threshold and return partial results predicate pruning: skip expansion when node attributes do not satisfy business rules visited Structure Selection Structure Correctness Memory Speed Suitable Scenarios HashSet Exact Medium-high Fast Sparse node IDs, dynamic IDs Bitmap Exact Lowest (bit-level) Fast Node IDs can be compressed to contiguous integers Bloom Filter Approximate (false positives) Very low Fast Prefiltering and dedup acceleration (error-tolerant) Key conclusions:\nStrict correctness tasks (for example permission checks, risk-control hits) cannot rely on bloom alone The safest bloom usage is \u0026ldquo;prefilter + exact-structure confirmation\u0026rdquo; Practical Guide / Steps Normalize node IDs first (compress to 0..n-1 when needed) For k-hop, use BFS with depth in queue entries For path existence, use iterative DFS with a stack of pending nodes Apply early-stop checks at the top of each loop Prefer bitmap for visited (when compressible), otherwise HashSet If dedup checks are the throughput bottleneck, add bloom prefiltering Runnable Python example (python3 bfs_dfs_demo.py):\nfrom collections import deque from typing import List, Set class SimpleBloom: \u0026#34;\u0026#34;\u0026#34;Demo Bloom filter: prefilter only, not a standalone correctness guarantee.\u0026#34;\u0026#34;\u0026#34; def __init__(self, m: int = 1 \u0026lt;\u0026lt; 15): self.m = m self.bits = bytearray(m // 8 + 1) def _idx(self, x: int, salt: int) -\u0026gt; int: return hash((x, salt)) \u0026amp; (self.m - 1) def _set(self, i: int) -\u0026gt; None: self.bits[i \u0026gt;\u0026gt; 3] |= 1 \u0026lt;\u0026lt; (i \u0026amp; 7) def _get(self, i: int) -\u0026gt; bool: return (self.bits[i \u0026gt;\u0026gt; 3] \u0026gt;\u0026gt; (i \u0026amp; 7)) \u0026amp; 1 == 1 def add(self, x: int) -\u0026gt; None: for salt in (17, 31, 73): self._set(self._idx(x, salt)) def maybe_contains(self, x: int) -\u0026gt; bool: return all(self._get(self._idx(x, salt)) for salt in (17, 31, 73)) def bfs_k_hop(graph: List[List[int]], s: int, k: int) -\u0026gt; Set[int]: n = len(graph) visited = bytearray(n) # bitmap q = deque([(s, 0)]) visited[s] = 1 result = {s} while q: u, d = q.popleft() if d == k: continue for v in graph[u]: if not visited[v]: visited[v] = 1 result.add(v) q.append((v, d + 1)) return result def dfs_path_exists(graph: List[List[int]], s: int, t: int) -\u0026gt; bool: n = len(graph) visited = bytearray(n) stack = [s] visited[s] = 1 while stack: u = stack.pop() if u == t: # early stop return True for v in graph[u]: if not visited[v]: visited[v] = 1 stack.append(v) return False def bfs_with_bloom_prefilter(graph: List[List[int]], s: int, limit: int = 100000) -\u0026gt; int: \u0026#34;\u0026#34;\u0026#34;Example: bloom reduces set lookups; exact set still guarantees correctness.\u0026#34;\u0026#34;\u0026#34; q = deque([s]) exact = {s} bloom = SimpleBloom() bloom.add(s) visited_count = 0 while q and visited_count \u0026lt; limit: u = q.popleft() visited_count += 1 for v in graph[u]: # bloom says \u0026#34;not seen\u0026#34; =\u0026gt; definitely unseen, enqueue directly if not bloom.maybe_contains(v): bloom.add(v) exact.add(v) q.append(v) continue # bloom says \u0026#34;maybe seen\u0026#34; =\u0026gt; confirm with exact set if v not in exact: exact.add(v) q.append(v) return visited_count if __name__ == \u0026#34;__main__\u0026#34;: graph = [ [1, 2], # 0 [3], # 1 [3, 4], # 2 [5], # 3 [], # 4 [], # 5 ] print(\u0026#34;k-hop\u0026lt;=2:\u0026#34;, sorted(bfs_k_hop(graph, 0, 2))) print(\u0026#34;path 0-\u0026gt;5:\u0026#34;, dfs_path_exists(graph, 0, 5)) print(\u0026#34;bloom+exact visits:\u0026#34;, bfs_with_bloom_prefilter(graph, 0, limit=100)) E - Engineering (Production Applications) Scenario 1: k-hop neighborhood query in graph databases (Python) Background: users provide seed nodes, and the system returns neighborhood nodes within N hops.\nWhy it fits: BFS is naturally layered, and depth directly maps to k-hop business semantics.\nfrom collections import deque def k_hop_nodes(graph, s, k): q = deque([(s, 0)]) vis = {s} out = {s} while q: u, d = q.popleft() if d == k: continue for v in graph[u]: if v not in vis: vis.add(v) out.add(v) q.append((v, d + 1)) return out Scenario 2: call-chain fault tracing (Go) Background: determine whether service A can reach faulty service B in a call graph.\nWhy it fits: iterative DFS with target-hit early stop often returns faster than full-graph scans.\npackage main import \u0026#34;fmt\u0026#34; func pathExists(graph [][]int, s, t int) bool { vis := make([]bool, len(graph)) stack := []int{s} vis[s] = true for len(stack) \u0026gt; 0 { u := stack[len(stack)-1] stack = stack[:len(stack)-1] if u == t { return true } for _, v := range graph[u] { if !vis[v] { vis[v] = true stack = append(stack, v) } } } return false } func main() { g := [][]int{{1, 2}, {3}, {3, 4}, {5}, {}, {}} fmt.Println(pathExists(g, 0, 5)) // true } Scenario 3: online dedup prefiltering in relationship graphs (C++) Background: under high QPS, visited-set lookups become a hotspot.\nWhy it fits: use bloom for fast \u0026ldquo;possibly unseen\u0026rdquo; routing, then confirm with exact bitmap/set to reduce average dedup cost.\n#include \u0026lt;bitset\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;unordered_set\u0026gt; struct Bloom { static const int M = 1 \u0026lt;\u0026lt; 16; std::bitset\u0026lt;M\u0026gt; bits; int h1(int x) const { return (x * 1315423911u) \u0026amp; (M - 1); } int h2(int x) const { return (x * 2654435761u) \u0026amp; (M - 1); } void add(int x) { bits.set(h1(x)); bits.set(h2(x)); } bool maybe(int x) const { return bits.test(h1(x)) \u0026amp;\u0026amp; bits.test(h2(x)); } }; int main() { Bloom b; std::unordered_set\u0026lt;int\u0026gt; exact; for (int x : {1, 2, 3}) { b.add(x); exact.insert(x); } int q = 4; if (!b.maybe(q) || exact.find(q) != exact.end()) { std::cout \u0026lt;\u0026lt; \u0026#34;not visited yet\\n\u0026#34;; } } R - Reflection (Deep Dive) Complexity Analysis Let V' and E' be the node and edge counts of the visited subgraph:\nBFS / DFS time complexity: O(V' + E') Extra space for visited: HashSet: O(V') Bitmap: O(N) bits (N is the full-graph node upper bound) Bloom: O(m) bits (m is bit-array size, tunable approximation) For k-hop tasks, V' and E' are often much smaller than full-graph size, which is where early stop provides most value.\nAlternatives and Trade-offs Approach Pros Cons Best For Recursive DFS Short code Stack risk on deep graphs, weaker controllability Small offline scripts Iterative DFS Controllable, easy to add early stop Manual stack management Path existence / online checks BFS Clear layering, suitable for hop constraints Peak memory may be higher than DFS k-hop / layered retrieval Bidirectional BFS Faster point-to-point path queries Higher implementation complexity Sparse graph, single-source to single-target Common Wrong Approaches Mark visited only at dequeue time: can cause repeated enqueues and queue blowup Use bloom alone as visited: false positives can skip nodes that should be visited No budget limits: online requests can suffer long-tail latency at high-degree nodes Why This Is the Most Practical Engineering Strategy Iterative implementation avoids recursion risks early stop constrains search cost inside business boundaries bitmap/bloom make visited strategy flexible by graph scale FAQ and Notes Which is faster, BFS or DFS?\nThere is no absolute winner. BFS is common for k-hop. For reachability where the target may appear deep, DFS often hits faster.\nCan bloom false positives affect correctness?\nYes. If bloom is used alone for dedup, false positives can skip valid search branches. Strict-correctness tasks must use exact confirmation.\nWhen should visited be marked?\nUsually at enqueue/push time, so the same node is not inserted repeatedly.\nBest Practices and Recommendations Define business stop conditions before writing traversal code Default to iterative versions; use recursion only for small offline tools Prefer bitmap when node IDs can be compressed, balancing speed and memory Use bloom only as a prefilter, not as a standalone correctness guarantee Add visit caps and latency monitoring to traversal to avoid online cascades S - Summary Key Takeaways Engineering-grade BFS/DFS is about iterative containers, clear invariants, and explicit early-stop conditions Prefer BFS for k-hop queries and subgraph extraction; prefer iterative DFS for path existence visited has no universal answer: HashSet, bitmap, and bloom each have boundaries Bloom has false positives; use it as \u0026ldquo;prefilter + exact confirmation,\u0026rdquo; not as a standalone strict decision source Make search budgets (hop count, node budget, time budget) explicit parameters for stable production behavior Recommended Follow-up Reading LeetCode 200 (Number of Islands): graph traversal templates LeetCode 127 (Word Ladder): BFS + pruning Graph500 / graph computing benchmarks: ideas for large-scale traversal performance Classic Bloom-filter papers and engineering parameter sizing (false-positive rate vs bit-array size) Metadata Reading time: 12-16 minutes Tags: Graph, BFS, DFS, k-hop, subgraph extraction SEO keywords: BFS, DFS, k-hop query, path existence, visited bitmap, bloom filter Meta description: Engineering BFS/DFS templates with iterative implementation, early stop, visited bitmap/bloom selection, and runnable multi-language code. Call To Action (CTA) I recommend locking in two actions immediately:\nRefactor one online graph-query API to expose explicit early-stop parameters (hop budget, node budget, time budget) Benchmark HashSet vs bitmap on real data (add bloom prefilter if needed), and record throughput and memory curves If you want, I can write the next post: \u0026ldquo;Union-Find + BFS/DFS selection checklist for graph problems (when to traverse vs when to merge).\u0026rdquo;\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) from collections import deque def bfs_k_hop(graph, s, k): vis = [False] * len(graph) q = deque([(s, 0)]) vis[s] = True out = {s} while q: u, d = q.popleft() if d == k: continue for v in graph[u]: if not vis[v]: vis[v] = True out.add(v) q.append((v, d + 1)) return out def dfs_path_exists(graph, s, t): vis = [False] * len(graph) st = [s] vis[s] = True while st: u = st.pop() if u == t: return True for v in graph[u]: if not vis[v]: vis[v] = True st.append(v) return False if __name__ == \u0026#34;__main__\u0026#34;: g = [[1, 2], [3], [3, 4], [5], [], []] print(sorted(bfs_k_hop(g, 0, 2))) print(dfs_path_exists(g, 0, 5)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; #define N 6 void bfs_k_hop(int g[N][N], int s, int k) { int q[128][2], head = 0, tail = 0; bool vis[N] = {0}; vis[s] = true; q[tail][0] = s; q[tail][1] = 0; tail++; while (head \u0026lt; tail) { int u = q[head][0], d = q[head][1]; head++; if (d == k) continue; for (int v = 0; v \u0026lt; N; ++v) { if (g[u][v] \u0026amp;\u0026amp; !vis[v]) { vis[v] = true; q[tail][0] = v; q[tail][1] = d + 1; tail++; } } } for (int i = 0; i \u0026lt; N; ++i) if (vis[i]) printf(\u0026#34;%d \u0026#34;, i); printf(\u0026#34;\\n\u0026#34;); } bool dfs_path_exists(int g[N][N], int s, int t) { int st[128], top = 0; bool vis[N] = {0}; st[top++] = s; vis[s] = true; while (top) { int u = st[--top]; if (u == t) return true; for (int v = 0; v \u0026lt; N; ++v) { if (g[u][v] \u0026amp;\u0026amp; !vis[v]) { vis[v] = true; st[top++] = v; } } } return false; } int main(void) { int g[N][N] = {0}; g[0][1] = g[0][2] = 1; g[1][3] = 1; g[2][3] = g[2][4] = 1; g[3][5] = 1; bfs_k_hop(g, 0, 2); // 0 1 2 3 4 printf(\u0026#34;%d\\n\u0026#34;, dfs_path_exists(g, 0, 5)); // 1 return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;queue\u0026gt; #include \u0026lt;vector\u0026gt; std::vector\u0026lt;int\u0026gt; bfsKHop(const std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; g, int s, int k) { std::vector\u0026lt;char\u0026gt; vis(g.size(), 0); std::queue\u0026lt;std::pair\u0026lt;int, int\u0026gt;\u0026gt; q; vis[s] = 1; q.push({s, 0}); while (!q.empty()) { auto [u, d] = q.front(); q.pop(); if (d == k) continue; for (int v : g[u]) { if (!vis[v]) { vis[v] = 1; q.push({v, d + 1}); } } } std::vector\u0026lt;int\u0026gt; out; for (int i = 0; i \u0026lt; (int)g.size(); ++i) if (vis[i]) out.push_back(i); return out; } bool dfsPathExists(const std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; g, int s, int t) { std::vector\u0026lt;char\u0026gt; vis(g.size(), 0); std::vector\u0026lt;int\u0026gt; st = {s}; vis[s] = 1; while (!st.empty()) { int u = st.back(); st.pop_back(); if (u == t) return true; for (int v : g[u]) { if (!vis[v]) { vis[v] = 1; st.push_back(v); } } } return false; } int main() { std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; g = {{1,2},{3},{3,4},{5},{},{}}; auto nodes = bfsKHop(g, 0, 2); for (int x : nodes) std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34;; std::cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34; \u0026lt;\u0026lt; dfsPathExists(g, 0, 5) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } package main import \u0026#34;fmt\u0026#34; func bfsKHop(graph [][]int, s, k int) []bool { vis := make([]bool, len(graph)) type Node struct{ u, d int } q := []Node{{s, 0}} vis[s] = true for head := 0; head \u0026lt; len(q); head++ { cur := q[head] if cur.d == k { continue } for _, v := range graph[cur.u] { if !vis[v] { vis[v] = true q = append(q, Node{v, cur.d + 1}) } } } return vis } func dfsPathExists(graph [][]int, s, t int) bool { vis := make([]bool, len(graph)) stack := []int{s} vis[s] = true for len(stack) \u0026gt; 0 { u := stack[len(stack)-1] stack = stack[:len(stack)-1] if u == t { return true } for _, v := range graph[u] { if !vis[v] { vis[v] = true stack = append(stack, v) } } } return false } func main() { g := [][]int{{1, 2}, {3}, {3, 4}, {5}, {}, {}} fmt.Println(bfsKHop(g, 0, 2)) fmt.Println(dfsPathExists(g, 0, 5)) } use std::collections::VecDeque; fn bfs_k_hop(graph: \u0026amp;Vec\u0026lt;Vec\u0026lt;usize\u0026gt;\u0026gt;, s: usize, k: usize) -\u0026gt; Vec\u0026lt;bool\u0026gt; { let mut vis = vec![false; graph.len()]; let mut q: VecDeque\u0026lt;(usize, usize)\u0026gt; = VecDeque::new(); vis[s] = true; q.push_back((s, 0)); while let Some((u, d)) = q.pop_front() { if d == k { continue; } for \u0026amp;v in \u0026amp;graph[u] { if !vis[v] { vis[v] = true; q.push_back((v, d + 1)); } } } vis } fn dfs_path_exists(graph: \u0026amp;Vec\u0026lt;Vec\u0026lt;usize\u0026gt;\u0026gt;, s: usize, t: usize) -\u0026gt; bool { let mut vis = vec![false; graph.len()]; let mut st = vec![s]; vis[s] = true; while let Some(u) = st.pop() { if u == t { return true; } for \u0026amp;v in \u0026amp;graph[u] { if !vis[v] { vis[v] = true; st.push(v); } } } false } fn main() { let graph = vec![vec![1, 2], vec![3], vec![3, 4], vec![5], vec![], vec![]]; println!(\u0026#34;{:?}\u0026#34;, bfs_k_hop(\u0026amp;graph, 0, 2)); println!(\u0026#34;{}\u0026#34;, dfs_path_exists(\u0026amp;graph, 0, 5)); } function bfsKHop(graph, s, k) { const vis = Array(graph.length).fill(false); const q = [[s, 0]]; let head = 0; vis[s] = true; while (head \u0026lt; q.length) { const [u, d] = q[head++]; if (d === k) continue; for (const v of graph[u]) { if (!vis[v]) { vis[v] = true; q.push([v, d + 1]); } } } return vis; } function dfsPathExists(graph, s, t) { const vis = Array(graph.length).fill(false); const st = [s]; vis[s] = true; while (st.length) { const u = st.pop(); if (u === t) return true; for (const v of graph[u]) { if (!vis[v]) { vis[v] = true; st.push(v); } } } return false; } const g = [[1, 2], [3], [3, 4], [5], [], []]; console.log(bfsKHop(g, 0, 2)); console.log(dfsPathExists(g, 0, 5)); ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/algorithm/graph/10-bfs-dfs-k-hop-subgraph-path-existence/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\u003cbr\u003e\nBFS / DFS are not just about \u0026ldquo;being able to code them.\u0026rdquo; You need production-ready behavior, predictable cost, and provable correctness. Following the ACERS structure, this article breaks three common tasks (k-hop query, subgraph extraction, and path existence) into reusable templates: \u003cstrong\u003eiterative implementation + early stop + visited structure selection\u003c/strong\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eEstimated reading time\u003c/strong\u003e: 12-16 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eGraph\u003c/code\u003e, \u003ccode\u003eBFS\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003ek-hop\u003c/code\u003e, \u003ccode\u003esubgraph extraction\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: BFS, DFS, k-hop query, subgraph extraction, path existence, visited bitmap, bloom filter\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: BFS/DFS for engineering scenarios: iterative implementations to avoid stack overflow, early stop to cut search cost, and visited bitmap/bloom to optimize memory and dedup performance.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-audience\"\u003eTarget Audience\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eEngineers working on graph databases, risk-control relationship graphs, or call-chain analysis\u003c/li\u003e\n\u003cli\u003eLearners who can write \u0026ldquo;problem-solution style BFS/DFS\u0026rdquo; but do not yet have engineering templates\u003c/li\u003e\n\u003cli\u003eDevelopers who want traversal code that is stable, observable, and extensible\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eIn production systems, BFS/DFS is usually not a one-off offline script. It is part of an online request path:\u003c/p\u003e","title":"BFS / DFS Engineering Primer: k-hop Queries, Subgraph Extraction, and Path Existence ACERS Breakdown"},{"content":" Subtitle / Summary\nThe key is not comparing values, but comparing node identity (same object / same address). This ACERS guide explains the naive hash approach, the length-alignment approach, and the most practical switch-head two-pointer template, with runnable multi-language implementations under the no-modification and no-cycle constraints.\nReading time: 10-14 min Tags: Hot100, linked list, two pointers SEO keywords: Intersection of Two Linked Lists, switch heads, O(1) space, LeetCode 160 Meta description: Two pointers walk A then B and B then A, guaranteeing meeting at the intersection or both reaching null within m+n steps, with O(m+n) time and O(1) space. A - Algorithm (Problem and Algorithm) Problem Restatement Given heads headA and headB of two singly linked lists, return the node where they intersect. If they do not intersect, return null.\nConstraints:\nThe linked structure has no cycle The original list structure must remain unchanged Input / Output Name Type Description headA ListNode Head of list A headB ListNode Head of list B return ListNode / null Intersection start node (same object), or null Example 1 (Intersecting) A: a1 -\u0026gt; a2 -\u0026gt; c1 -\u0026gt; c2 -\u0026gt; c3 B: b1 -\u0026gt; b2 -\u0026gt; b3 -\u0026gt; c1 -\u0026gt; c2 -\u0026gt; c3 output: c1 (return node reference, not value) Example 2 (No intersection) A: 1 -\u0026gt; 2 -\u0026gt; 3 B: 4 -\u0026gt; 5 output: null Target Readers Hot100 learners who want a reusable linked-list two-pointer template Developers who often confuse \u0026ldquo;same value\u0026rdquo; with \u0026ldquo;same node\u0026rdquo; Engineers working with shared tail structures in chain-like data Background / Motivation This problem looks simple, but it forces you to separate three concepts:\nIntersection means sharing the exact same node object, not equal values You cannot modify structure (no rewriting next, no marking nodes) You still need linear performance The most practical solution is the switch-head two-pointer method. It needs no hash set and no precomputed lengths, yet synchronizes both pointers in at most m+n steps.\nCore Concepts Concept Meaning Note Same node Two pointers reference the same memory object Pointer/reference equality Shared suffix Two lists share all nodes from some node onward After intersection, tails are identical Switch-head two pointers At list end, jump to the other list head Equalizes total traveled distance No-cycle assumption Problem guarantees no cycle in the structure Otherwise cycle handling is required C - Concepts (Core Ideas) How To Build The Solution From Scratch Step 1: Start from the picture, not the code Suppose:\nA: a1 -\u0026gt; a2 -\u0026gt; c1 -\u0026gt; c2 B: b1 ------\u0026gt; c1 -\u0026gt; c2 The answer is node c1. Notice what matters:\nnot equal values not equal positions the same node object in memory So this is a pointer-identity problem, not a value-comparison problem.\nStep 2: Ask what the most direct correct solution would remember The straightforward idea is:\nput every node in A into a hash set walk B the first node already in the set is the intersection This is correct and easy, but it costs O(m) extra space.\nStep 3: Ask what really prevents two pointers from meeting naturally If both lists had the same prefix length before the shared tail, synchronous walking would work immediately. The real obstacle is:\nlen(prefixA) != len(prefixB) So the problem is not how to compare nodes. It is how to make both pointers spend the same total distance before comparing in lockstep.\nStep 4: Turn \u0026ldquo;length alignment\u0026rdquo; into a self-balancing walk One O(1)-space solution is:\ncompute both lengths advance the longer list first then walk together The switch-head trick does the same alignment implicitly:\np = head_a q = head_b while p is not q: p = p.next if p else head_b q = q.next if q else head_a Now:\np walks path A + B q walks path B + A Both combined paths have the same total length.\nStep 5: Why does switching heads remove the offset? Suppose A has a longer private prefix than B. Then p spends that extra distance early, while q spends it later after switching to A.\nAfter both pointers have traversed one full cross-list pass, the length difference is canceled automatically. From that point on, they are aligned relative to the shared tail.\nStep 6: Define the stopping condition precisely The loop stops only when:\nboth pointers hit the same real node, which is the intersection or both become None, which means there is no intersection That second case is important: no extra branch is needed. None is None ends the loop naturally.\nStep 7: Walk one concrete trace Take:\nA: 4 -\u0026gt; 1 -\u0026gt; 8 -\u0026gt; 4 -\u0026gt; 5 B: 5 -\u0026gt; 6 -\u0026gt; 1 -\u0026gt; 8 -\u0026gt; 4 -\u0026gt; 5 At first, p and q are offset because the non-shared prefixes have different lengths. After one pointer finishes its own list and switches heads, the offset is canceled. Then both pointers enter the shared tail together and meet at node 8.\nStep 8: Reduce the method to one sentence LeetCode 160 is \u0026ldquo;make both pointers walk A + B and B + A; equal total distance removes the prefix mismatch automatically.\u0026rdquo;\nAssemble the Full Code from __future__ import annotations class ListNode: def __init__(self, val: int): self.val = val self.next: ListNode | None = None def get_intersection_node(head_a: ListNode | None, head_b: ListNode | None) -\u0026gt; ListNode | None: p, q = head_a, head_b while p is not q: p = p.next if p else head_b q = q.next if q else head_a return p if __name__ == \u0026#34;__main__\u0026#34;: # Build shared tail: c1 -\u0026gt; c2 -\u0026gt; c3 c1 = ListNode(8) c2 = ListNode(4) c3 = ListNode(5) c1.next = c2 c2.next = c3 # A: a1 -\u0026gt; a2 -\u0026gt; c1 a1 = ListNode(4) a2 = ListNode(1) a1.next = a2 a2.next = c1 # B: b1 -\u0026gt; b2 -\u0026gt; b3 -\u0026gt; c1 b1 = ListNode(5) b2 = ListNode(6) b3 = ListNode(1) b1.next = b2 b2.next = b3 b3.next = c1 ans = get_intersection_node(a1, b1) print(ans.val if ans else None) # 8 Reference Answer from __future__ import annotations class ListNode: def __init__(self, x: int): self.val = x self.next: ListNode | None = None def get_intersection_node(head_a: ListNode | None, head_b: ListNode | None) -\u0026gt; ListNode | None: p, q = head_a, head_b while p is not q: p = p.next if p else head_b q = q.next if q else head_a return p Method category Two pointers on linked list Implicit length alignment through \u0026ldquo;walk full A then B\u0026rdquo; Identity equality without structural mutation Why switch-head pointers must meet Let:\na: unique prefix length of A b: unique prefix length of B c: shared suffix length Then:\nLength of A is a + c Length of B is b + c Pointer travel:\npA walks a+c, then b pB walks b+c, then a Both walk a+b+c before entering the same alignment point. So within m+n steps, they either meet at the intersection or both reach null.\nPractice Guide / Steps Initialize pA=headA, pB=headB Loop while pA != pB: pA = pA.next else headB pB = pB.next else headA Return pA (intersection node or null) Runnable Python example (intersection.py):\nfrom __future__ import annotations class ListNode: def __init__(self, val: int): self.val = val self.next: ListNode | None = None def get_intersection_node(head_a: ListNode | None, head_b: ListNode | None) -\u0026gt; ListNode | None: p, q = head_a, head_b while p is not q: p = p.next if p else head_b q = q.next if q else head_a return p if __name__ == \u0026#34;__main__\u0026#34;: # Build shared tail: c1 -\u0026gt; c2 -\u0026gt; c3 c1 = ListNode(8) c2 = ListNode(4) c3 = ListNode(5) c1.next = c2 c2.next = c3 # A: a1 -\u0026gt; a2 -\u0026gt; c1 a1 = ListNode(4) a2 = ListNode(1) a1.next = a2 a2.next = c1 # B: b1 -\u0026gt; b2 -\u0026gt; b3 -\u0026gt; c1 b1 = ListNode(5) b2 = ListNode(6) b3 = ListNode(1) b1.next = b2 b2.next = b3 b3.next = c1 ans = get_intersection_node(a1, b1) print(ans.val if ans else None) # 8 E - Engineering (Real-world Scenarios) Scenario 1: Shared suffix dedup in versioned pipelines (Python) Background: Some experiment/task pipelines are chain nodes, and multiple pipelines can share a common tail. Why it fits: locating the intersection lets you execute shared tail once or cache it.\nclass Step: def __init__(self, name): self.name = name self.next = None def intersection(a, b): p, q = a, b while p is not q: p = p.next if p else b q = q.next if q else a return p if __name__ == \u0026#34;__main__\u0026#34;: common = Step(\u0026#34;train\u0026#34;) common.next = Step(\u0026#34;evaluate\u0026#34;) a = Step(\u0026#34;clean\u0026#34;) a.next = Step(\u0026#34;fe\u0026#34;) a.next.next = common b = Step(\u0026#34;clean_v2\u0026#34;) b.next = common hit = intersection(a, b) print(hit.name if hit else \u0026#34;none\u0026#34;) # train Scenario 2: Safety check to avoid double free (C) Background: In C projects, accidentally shared list tails can cause double free if both lists are freed independently. Why it fits: detect intersection first, then free shared suffix only once.\nstruct Node { int v; struct Node* next; }; struct Node* intersection(struct Node* a, struct Node* b) { struct Node* p = a; struct Node* q = b; while (p != q) { p = p ? p-\u0026gt;next : b; q = q ? q-\u0026gt;next : a; } return p; // may be NULL } Scenario 3: Merge point detection in frontend history branches (JavaScript) Background: Some editors represent operation history as linked nodes; branches can share a common tail after merge/replay. Why it fits: finding intersection gives \u0026ldquo;where shared history starts\u0026rdquo; for UI highlight and merge strategy.\nfunction intersection(headA, headB) { let p = headA; let q = headB; while (p !== q) { p = p ? p.next : headB; q = q ? q.next : headA; } return p; } R - Reflection (Tradeoffs and Deepening) Complexity Time: O(m+n) Space: O(1) Alternatives Method Idea Time Extra space Note Hash set Store nodes of A, scan B O(m+n) O(m) Most direct Length alignment Align by length difference O(m+n) O(1) Needs separate length pass Switch-head two pointers A-\u0026gt;B and B-\u0026gt;A traversal O(m+n) O(1) Cleanest template Common pitfalls Using value equality as intersection: intersection requires node identity equality. Null handling mistakes: loop should end by pointer identity; result can be null. Using template on cyclic lists without checks: this problem guarantees acyclic lists; otherwise loop risk exists. FAQs and Notes Why no infinite loop? Under no-cycle assumption, each pointer walks at most m+n steps before meeting at intersection or both becoming null.\nWhat if headA == headB? They are already equal at start; return immediately.\nCan I mark nodes or modify values? No. Problem requires preserving original structure, and structural mutation is unsafe in shared data.\nBest Practices Memorize the switch-head pattern: p = p ? p-\u0026gt;next : headB, q = q ? q-\u0026gt;next : headA For any \u0026ldquo;shared tail\u0026rdquo; question, first verify whether equality means identity or value If cycles are possible in production data, run cycle detection first S - Summary Key Takeaways Intersection in this problem means same node object, not same value Hash is easy but costs memory; length alignment is O(1) but needs explicit length pass Switch-head two pointers implicitly aligns path lengths, giving O(m+n) time and O(1) space without mutations The no-cycle guarantee is a core precondition for termination and correctness This template transfers to shared-tail / merge-point / common-suffix structures References and Further Reading LeetCode 160. Intersection of Two Linked Lists Classic linked-list pointer patterns: cycle detection, middle node, remove Nth from end Pointer identity concepts in shared mutable structures Meta Reading time: 10-14 min Tags: Hot100, linked list, two pointers, space optimization SEO keywords: Intersection of Two Linked Lists, switch heads, O(1) space, LeetCode 160 Meta description: Two pointers walk A then B and B then A, meeting at intersection or null within m+n steps, with O(m+n) time and O(1) space. Call to Action Use the same thinking on two follow-up problems:\nLinked List Cycle (Floyd) Remove Nth Node From End (fast/slow pointers) If you want, I can also add an advanced follow-up post: how to reason about intersection when cycles may exist.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) from __future__ import annotations class ListNode: def __init__(self, x: int): self.val = x self.next: ListNode | None = None def get_intersection_node(head_a: ListNode | None, head_b: ListNode | None) -\u0026gt; ListNode | None: p, q = head_a, head_b while p is not q: p = p.next if p else head_b q = q.next if q else head_a return p #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; struct ListNode { int val; struct ListNode* next; }; struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB) { struct ListNode* p = headA; struct ListNode* q = headB; while (p != q) { p = p ? p-\u0026gt;next : headB; q = q ? q-\u0026gt;next : headA; } return p; } static struct ListNode* node(int v) { struct ListNode* n = (struct ListNode*)malloc(sizeof(struct ListNode)); n-\u0026gt;val = v; n-\u0026gt;next = NULL; return n; } int main(void) { // shared: c1(8) -\u0026gt; c2(4) -\u0026gt; c3(5) struct ListNode* c1 = node(8); struct ListNode* c2 = node(4); struct ListNode* c3 = node(5); c1-\u0026gt;next = c2; c2-\u0026gt;next = c3; // A: 4 -\u0026gt; 1 -\u0026gt; c1 struct ListNode* a1 = node(4); struct ListNode* a2 = node(1); a1-\u0026gt;next = a2; a2-\u0026gt;next = c1; // B: 5 -\u0026gt; 6 -\u0026gt; 1 -\u0026gt; c1 struct ListNode* b1 = node(5); struct ListNode* b2 = node(6); struct ListNode* b3 = node(1); b1-\u0026gt;next = b2; b2-\u0026gt;next = b3; b3-\u0026gt;next = c1; struct ListNode* ans = getIntersectionNode(a1, b1); if (ans) printf(\u0026#34;%d\\n\u0026#34;, ans-\u0026gt;val); else printf(\u0026#34;null\\n\u0026#34;); // In real code, free nodes carefully: shared suffix should be freed once. return 0; } #include \u0026lt;iostream\u0026gt; struct ListNode { int val; ListNode* next; explicit ListNode(int x) : val(x), next(nullptr) {} }; ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) { ListNode* p = headA; ListNode* q = headB; while (p != q) { p = p ? p-\u0026gt;next : headB; q = q ? q-\u0026gt;next : headA; } return p; } int main() { // shared: c1 -\u0026gt; c2 -\u0026gt; c3 auto* c1 = new ListNode(8); auto* c2 = new ListNode(4); auto* c3 = new ListNode(5); c1-\u0026gt;next = c2; c2-\u0026gt;next = c3; // A: 4 -\u0026gt; 1 -\u0026gt; c1 auto* a1 = new ListNode(4); auto* a2 = new ListNode(1); a1-\u0026gt;next = a2; a2-\u0026gt;next = c1; // B: 5 -\u0026gt; 6 -\u0026gt; 1 -\u0026gt; c1 auto* b1 = new ListNode(5); auto* b2 = new ListNode(6); auto* b3 = new ListNode(1); b1-\u0026gt;next = b2; b2-\u0026gt;next = b3; b3-\u0026gt;next = c1; ListNode* ans = getIntersectionNode(a1, b1); std::cout \u0026lt;\u0026lt; (ans ? std::to_string(ans-\u0026gt;val) : std::string(\u0026#34;null\u0026#34;)) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // Demo only: free omitted. return 0; } package main import \u0026#34;fmt\u0026#34; type ListNode struct { Val int Next *ListNode } func getIntersectionNode(headA, headB *ListNode) *ListNode { p, q := headA, headB for p != q { if p == nil { p = headB } else { p = p.Next } if q == nil { q = headA } else { q = q.Next } } return p } func main() { // shared: c1(8) -\u0026gt; c2(4) -\u0026gt; c3(5) c3 := \u0026amp;ListNode{Val: 5} c2 := \u0026amp;ListNode{Val: 4, Next: c3} c1 := \u0026amp;ListNode{Val: 8, Next: c2} // A: 4 -\u0026gt; 1 -\u0026gt; c1 a := \u0026amp;ListNode{Val: 4, Next: \u0026amp;ListNode{Val: 1, Next: c1}} // B: 5 -\u0026gt; 6 -\u0026gt; 1 -\u0026gt; c1 b := \u0026amp;ListNode{Val: 5, Next: \u0026amp;ListNode{Val: 6, Next: \u0026amp;ListNode{Val: 1, Next: c1}}} ans := getIntersectionNode(a, b) if ans != nil { fmt.Println(ans.Val) } else { fmt.Println(\u0026#34;null\u0026#34;) } } use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct ListNode { val: i32, next: Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;ListNode\u0026gt;\u0026gt;\u0026gt;, } fn node(val: i32) -\u0026gt; Rc\u0026lt;RefCell\u0026lt;ListNode\u0026gt;\u0026gt; { Rc::new(RefCell::new(ListNode { val, next: None })) } fn same(a: \u0026amp;Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;ListNode\u0026gt;\u0026gt;\u0026gt;, b: \u0026amp;Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;ListNode\u0026gt;\u0026gt;\u0026gt;) -\u0026gt; bool { match (a, b) { (Some(x), Some(y)) =\u0026gt; Rc::ptr_eq(x, y), (None, None) =\u0026gt; true, _ =\u0026gt; false, } } fn get_intersection_node( head_a: Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;ListNode\u0026gt;\u0026gt;\u0026gt;, head_b: Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;ListNode\u0026gt;\u0026gt;\u0026gt;, ) -\u0026gt; Option\u0026lt;Rc\u0026lt;RefCell\u0026lt;ListNode\u0026gt;\u0026gt;\u0026gt; { let mut p = head_a.clone(); let mut q = head_b.clone(); while !same(\u0026amp;p, \u0026amp;q) { p = if let Some(n) = p { n.borrow().next.clone() } else { head_b.clone() }; q = if let Some(n) = q { n.borrow().next.clone() } else { head_a.clone() }; } p } fn main() { // shared: c1(8) -\u0026gt; c2(4) -\u0026gt; c3(5) let c1 = node(8); let c2 = node(4); let c3 = node(5); c1.borrow_mut().next = Some(c2.clone()); c2.borrow_mut().next = Some(c3.clone()); // A: 4 -\u0026gt; 1 -\u0026gt; c1 let a1 = node(4); let a2 = node(1); a1.borrow_mut().next = Some(a2.clone()); a2.borrow_mut().next = Some(c1.clone()); // B: 5 -\u0026gt; 6 -\u0026gt; 1 -\u0026gt; c1 let b1 = node(5); let b2 = node(6); let b3 = node(1); b1.borrow_mut().next = Some(b2.clone()); b2.borrow_mut().next = Some(b3.clone()); b3.borrow_mut().next = Some(c1.clone()); let ans = get_intersection_node(Some(a1), Some(b1)); match ans { Some(n) =\u0026gt; println!(\u0026#34;{}\u0026#34;, n.borrow().val), None =\u0026gt; println!(\u0026#34;null\u0026#34;), } } class ListNode { constructor(val) { this.val = val; this.next = null; } } function getIntersectionNode(headA, headB) { let p = headA; let q = headB; while (p !== q) { p = p ? p.next : headB; q = q ? q.next : headA; } return p; } // demo const c1 = new ListNode(8); const c2 = new ListNode(4); const c3 = new ListNode(5); c1.next = c2; c2.next = c3; const a1 = new ListNode(4); const a2 = new ListNode(1); a1.next = a2; a2.next = c1; const b1 = new ListNode(5); const b2 = new ListNode(6); const b3 = new ListNode(1); b1.next = b2; b2.next = b3; b3.next = c1; const ans = getIntersectionNode(a1, b1); console.log(ans ? ans.val : null); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/160-intersection-of-two-linked-lists/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThe key is not comparing values, but comparing node identity (same object / same address). This ACERS guide explains the naive hash approach, the length-alignment approach, and the most practical switch-head two-pointer template, with runnable multi-language implementations under the no-modification and no-cycle constraints.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10-14 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003elinked list\u003c/code\u003e, \u003ccode\u003etwo pointers\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Intersection of Two Linked Lists, switch heads, O(1) space, LeetCode 160\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Two pointers walk A then B and B then A, guaranteeing meeting at the intersection or both reaching null within m+n steps, with O(m+n) time and O(1) space.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a---algorithm-problem-and-algorithm\"\u003eA - Algorithm (Problem and Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven heads \u003ccode\u003eheadA\u003c/code\u003e and \u003ccode\u003eheadB\u003c/code\u003e of two singly linked lists, return the node where they intersect.\nIf they do not intersect, return \u003ccode\u003enull\u003c/code\u003e.\u003c/p\u003e","title":"Hot100: Intersection of Two Linked Lists Two-Pointer Switch-Head O(1) Space ACERS Guide"},{"content":" Subtitle / Summary\nThe constraint “the path can start anywhere, but must go downward” makes root-to-leaf DP insufficient. This ACERS guide explains prefix sums on trees: convert any downward path into a difference of two prefix sums, maintain a frequency hash map during one DFS, and finish in O(n).\nReading time: 12–15 min Tags: binary tree, prefix sum, DFS, hash map SEO keywords: Path Sum III, tree prefix sum, prefix-sum hash, LeetCode 437 Meta description: Count downward paths whose sum equals targetSum in O(n) via prefix sum + hash map, with derivation, tradeoffs, and multi-language implementations. A — Algorithm (Problem \u0026amp; Algorithm) Problem Restatement Given the root root of a binary tree and an integer targetSum, return the number of downward paths whose node values sum to targetSum. The path does not need to start at the root or end at a leaf, but it must go downward (parent → child).\nInput / Output Name Type Description root TreeNode root of the binary tree targetSum int target path sum return int number of valid downward paths Example 1 10 / \\ 5 -3 / \\ \\ 3 2 11 / \\ \\ 3 -2 1 targetSum = 8 output: 3 explain: 5-\u0026gt;3, 5-\u0026gt;2-\u0026gt;1, -3-\u0026gt;11 Example 2 1 / \\ 2 3 targetSum = 3 output: 2 explain: 1-\u0026gt;2, 3 Target Readers LeetCode learners who want a reusable “tree + hash map” template People who tend to write O(n²) when the path does not have to start at the root Engineers working with hierarchical data (call traces, org trees) who need “downward segment” statistics Background / Motivation Many “tree path” problems hide a trap: you naturally assume paths start at the root, or end at leaves — but this problem allows the path to start and end at any nodes, as long as the direction is downward (parent → child).\nThat means:\nMaintaining only a “root-to-current” DP state is not enough Enumerating all start nodes degrades to O(n²) in a skewed tree Sliding window does not apply (node values can be negative, so there is no monotonicity) The key skill worth internalizing is:\nTurn “any downward path” into “the difference of two prefix sums on the same DFS path”.\nOnce you own this model, a lot of tree counting problems collapse into the familiar recipe: prefix sum + frequency map.\nCore Concepts Downward path: can only go from parent to child (no backtracking, no cross-branch jumps) Prefix sum: the sum along the path from the root to the current node Difference counting: if curSum - prevSum = target, then prevSum = curSum - target Path-local hash map: the map must represent prefix sums on the current DFS stack; you must undo it on backtracking C — Concepts (Core Ideas) Derivation: from O(n²) enumeration to O(n) prefix sum Naive approach: start DFS from every node\nFor each node start, count downward paths starting at start with sum targetSum. This can degrade to O(n²) on a chain-like tree and repeats work.\nKey observation: any downward path is a contiguous segment of a root-to-current path\nDuring a DFS, we are always standing on one root → current path (the recursion stack). If we define:\ncurSum: prefix sum from root to current node prevSum: prefix sum from root to some ancestor node Then the sum of the downward path “(ancestor’s child) → current” equals:\ncurSum - prevSum To make it equal targetSum, we need:\nprevSum = curSum - targetSum Method choice: frequency map of prefix sums on the current DFS path\nWhen we visit a node: compute curSum add cnt[curSum - targetSum] to the answer (all paths ending at this node) increment cnt[curSum] and recurse to children decrement cnt[curSum] on backtracking (do not leak into sibling branches) Method Category Prefix sum on tree DFS + frequency hash map Backtracking to maintain path-local state Key Invariant (the thing that makes it correct) When processing node x, the map cnt contains prefix sum counts for the path from the root to x’s parent only. That’s why cnt[curSum - targetSum] exactly means “how many ancestors produce a valid downward segment ending at x”.\nInitialization cnt[0] = 1 matters: we treat the “empty prefix” as occurring once, so when curSum == targetSum (a path starting at the root), it is counted.\nPractice Guide / Steps Run DFS with arguments: node, current prefix sum curSum On entry: update curSum += node.val Add cnt[curSum - targetSum] to the answer Record cnt[curSum] += 1 Recurse to left/right and accumulate counts Backtrack: cnt[curSum] -= 1 Return the accumulated answer Runnable Python example (save as path_sum_iii.py):\nfrom typing import Dict, Optional class TreeNode: def __init__(self, val: int = 0, left: Optional[\u0026#34;TreeNode\u0026#34;] = None, right: Optional[\u0026#34;TreeNode\u0026#34;] = None): self.val = val self.left = left self.right = right def path_sum(root: Optional[TreeNode], target_sum: int) -\u0026gt; int: cnt: Dict[int, int] = {0: 1} def dfs(node: Optional[TreeNode], cur: int) -\u0026gt; int: if node is None: return 0 cur += node.val ans = cnt.get(cur - target_sum, 0) cnt[cur] = cnt.get(cur, 0) + 1 ans += dfs(node.left, cur) ans += dfs(node.right, cur) cnt[cur] -= 1 return ans return dfs(root, 0) if __name__ == \u0026#34;__main__\u0026#34;: # Example 2 root = TreeNode(1, TreeNode(2), TreeNode(3)) print(path_sum(root, 3)) # 2 E — Engineering (Real-world Scenarios) The transferable value of this problem is: counting “downward contiguous segments” in hierarchical data.\nIf your data can be modeled as a parent→child tree and each node has an additive value, you can apply the same template.\nScenario 1: Trace tree — count “downward segments with exact total cost” (Go) Background: a request trace forms a tree of spans; each span has a cost (latency) or a score.\nWhy it fits: you may want to count how many downward sub-chains sum to a threshold (feature construction, pattern detection).\npackage main import \u0026#34;fmt\u0026#34; type Span struct { Cost int64 Next []*Span } func countPaths(root *Span, target int64) int64 { cnt := map[int64]int64{0: 1} var dfs func(*Span, int64) int64 dfs = func(node *Span, cur int64) int64 { if node == nil { return 0 } cur += node.Cost ans := cnt[cur-target] cnt[cur]++ for _, ch := range node.Next { ans += dfs(ch, cur) } cnt[cur]-- return ans } return dfs(root, 0) } func main() { root := \u0026amp;Span{Cost: 1, Next: []*Span{{Cost: 2}, {Cost: 3}}} fmt.Println(countPaths(root, 3)) // 2: 1-\u0026gt;2, 3 } Scenario 2: Org tree / directory tree — count “budget segments” (Python) Background: an org tree where each node carries a budget delta or cost.\nWhy it fits: count downward segments whose total equals a target (compliance rules, feature engineering).\nfrom collections import defaultdict class Node: def __init__(self, v, children=None): self.v = v self.children = children or [] def count_paths(root, target): cnt = defaultdict(int) cnt[0] = 1 def dfs(node, cur): if node is None: return 0 cur += node.v ans = cnt[cur - target] cnt[cur] += 1 for ch in node.children: ans += dfs(ch, cur) cnt[cur] -= 1 return ans return dfs(root, 0) if __name__ == \u0026#34;__main__\u0026#34;: root = Node(1, [Node(2), Node(3)]) print(count_paths(root, 3)) Scenario 3: Frontend component tree — count “downward weight segments” (JavaScript) Background: component/menu trees where each node has a weight (exposure score, risk score, cost score).\nWhy it fits: count how many downward segments match a target sum for debugging or rule matching.\nfunction Node(v, children = []) { this.v = v; this.children = children; } function countPaths(root, target) { const cnt = new Map(); cnt.set(0, 1); function dfs(node, cur) { if (!node) return 0; cur += node.v; const need = cur - target; let ans = cnt.get(need) || 0; cnt.set(cur, (cnt.get(cur) || 0) + 1); for (const ch of node.children) ans += dfs(ch, cur); cnt.set(cur, cnt.get(cur) - 1); return ans; } return dfs(root, 0); } const root = new Node(1, [new Node(2), new Node(3)]); console.log(countPaths(root, 3)); R — Reflection (Tradeoffs \u0026amp; Deeper Notes) Complexity Time: O(n)\nEach node is visited once, and hash operations are O(1) amortized. Space: O(h) ~ O(n)\nThe map stores prefix sums along the current root-to-node path; in the worst case, a skewed tree has height h = n. Alternatives Comparison Method Idea Complexity Issue DFS from every node enumerate start nodes worst O(n²) TLE on skewed trees root-to-leaf DP only classic Path Sum DP O(n) violates “start anywhere / end anywhere” prefix sum + hash map difference counting O(n) must backtrack correctly Common Pitfalls (high-frequency mistakes) Forgetting cnt[curSum] -= 1 on backtracking: you mix prefix sums from sibling branches and count invalid cross-branch paths. Trying sliding window: node values can be negative, so the window has no monotonic property. Counting only paths starting at the root: you will miss paths starting in the middle. Overflow in prefix sums: in production, prefer int64/long long for prefix sums. Explanation / Why it works This is essentially the “tree version of LeetCode 560 (Subarray Sum Equals K)”:\nIn arrays: subarray sum = difference of two prefix sums In trees: downward path sum = difference of two prefix sums on the same DFS stack path The only extra requirement is handling branching: the hash map must represent only the current path, so we must do “enter +1, exit -1” to keep the counting scope correct.\nFAQs and Notes Why cnt[0] = 1?\nIt counts paths that start at the root: if curSum == targetSum, then curSum - targetSum == 0, which matches the “empty prefix”.\nCan I write it as iterative DFS?\nYes, but you must model backtracking explicitly (push enter/exit events). Recursion is simpler; if stack depth is a concern, use an explicit stack.\nDoes the path need to end at a leaf?\nNo. We count paths ending at any node, so we add cnt[cur-target] at every node.\nBest Practices Use int64/long long for prefix sums to avoid overflow Keep the meaning of cnt crystal clear: it is path-local (current DFS stack only) Keep a fixed update order: count first → cnt[cur]++ → recurse → cnt[cur]-- Hand-simulate 2–3 steps on a tiny tree to verify backtracking restores the state S — Summary Key Takeaways For “start anywhere, end anywhere, but downward only” tree path counting, think “prefix sum on tree” first Any downward path sum can be written as a difference of two prefix sums on the same DFS path A frequency map of prefix sums lets you count all paths ending at the current node online in O(1) amortized Backtracking the frequency map is the correctness linchpin (prevents cross-branch pollution) Conclusion The key in LeetCode 437 is not “DFS itself”, but modeling it as difference counting on tree prefix sums. Once you see it this way, the solution becomes short, fast, and reusable.\nReferences and Further Reading LeetCode 437. Path Sum III LeetCode 560. Subarray Sum Equals K (same idea on arrays) LeetCode 112/113. Path Sum / Path Sum II (different constraints; good for comparison) A standard DFS backtracking pattern: mutate state on entry, undo on exit Meta Reading time: 12–15 min Tags: binary tree, prefix sum, DFS, LeetCode 437 SEO keywords: Path Sum III, tree prefix sum, prefix-sum hash, LeetCode 437 Meta description: Prefix sum + hash map to count downward paths with sum targetSum, with derivation and multi-language implementations. Call to Action If you want to solidify this template, do these two next:\nLeetCode 560 (array prefix-sum difference counting) LeetCode 112/113 (Path Sum variants to see how constraints change the solution) If you want an ACERS-style write-up of 560 as a “prefix sum template”, tell me.\nMulti-language Reference Implementations (Python / C / C++ / Go / Rust / JS) from typing import Optional, Dict class TreeNode: def __init__(self, val: int = 0, left: Optional[\u0026#34;TreeNode\u0026#34;] = None, right: Optional[\u0026#34;TreeNode\u0026#34;] = None): self.val = val self.left = left self.right = right def pathSum(root: Optional[TreeNode], targetSum: int) -\u0026gt; int: cnt: Dict[int, int] = {0: 1} def dfs(node: Optional[TreeNode], cur: int) -\u0026gt; int: if node is None: return 0 cur += node.val ans = cnt.get(cur - targetSum, 0) cnt[cur] = cnt.get(cur, 0) + 1 ans += dfs(node.left, cur) ans += dfs(node.right, cur) cnt[cur] -= 1 return ans return dfs(root, 0) if __name__ == \u0026#34;__main__\u0026#34;: # Example 1 root = TreeNode( 10, TreeNode( 5, TreeNode(3, TreeNode(3), TreeNode(-2)), TreeNode(2, None, TreeNode(1)), ), TreeNode(-3, None, TreeNode(11)), ) print(pathSum(root, 8)) # 3 #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; typedef long long i64; typedef struct TreeNode { int val; struct TreeNode* left; struct TreeNode* right; } TreeNode; static int count_nodes(const TreeNode* root) { if (!root) return 0; return 1 + count_nodes(root-\u0026gt;left) + count_nodes(root-\u0026gt;right); } static uint64_t mix64(uint64_t x) { x += 0x9e3779b97f4a7c15ULL; x = (x ^ (x \u0026gt;\u0026gt; 30)) * 0xbf58476d1ce4e5b9ULL; x = (x ^ (x \u0026gt;\u0026gt; 27)) * 0x94d049bb133111ebULL; return x ^ (x \u0026gt;\u0026gt; 31); } typedef struct { i64* keys; int* vals; unsigned char* used; size_t cap; } Map; static Map map_new(size_t cap) { Map m; m.cap = cap; m.keys = (i64*)calloc(cap, sizeof(i64)); m.vals = (int*)calloc(cap, sizeof(int)); m.used = (unsigned char*)calloc(cap, sizeof(unsigned char)); return m; } static void map_free(Map* m) { free(m-\u0026gt;keys); free(m-\u0026gt;vals); free(m-\u0026gt;used); } static int map_get(const Map* m, i64 key) { size_t mask = m-\u0026gt;cap - 1; size_t i = (size_t)mix64((uint64_t)key) \u0026amp; mask; while (m-\u0026gt;used[i]) { if (m-\u0026gt;keys[i] == key) return m-\u0026gt;vals[i]; i = (i + 1) \u0026amp; mask; } return 0; } static void map_add(Map* m, i64 key, int delta) { size_t mask = m-\u0026gt;cap - 1; size_t i = (size_t)mix64((uint64_t)key) \u0026amp; mask; while (m-\u0026gt;used[i]) { if (m-\u0026gt;keys[i] == key) { m-\u0026gt;vals[i] += delta; return; } i = (i + 1) \u0026amp; mask; } m-\u0026gt;used[i] = 1; m-\u0026gt;keys[i] = key; m-\u0026gt;vals[i] = delta; } static int dfs(TreeNode* node, i64 cur, i64 target, Map* cnt) { if (!node) return 0; cur += (i64)node-\u0026gt;val; int ans = map_get(cnt, cur - target); map_add(cnt, cur, 1); ans += dfs(node-\u0026gt;left, cur, target, cnt); ans += dfs(node-\u0026gt;right, cur, target, cnt); map_add(cnt, cur, -1); return ans; } static int pathSum(TreeNode* root, int targetSum) { int n = count_nodes(root); size_t cap = 1; while (cap \u0026lt; (size_t)(n * 4 + 8)) cap \u0026lt;\u0026lt;= 1; /* keep load factor low */ Map cnt = map_new(cap); map_add(\u0026amp;cnt, 0, 1); int ans = dfs(root, 0, (i64)targetSum, \u0026amp;cnt); map_free(\u0026amp;cnt); return ans; } static TreeNode* node(int v, TreeNode* l, TreeNode* r) { TreeNode* n = (TreeNode*)malloc(sizeof(TreeNode)); n-\u0026gt;val = v; n-\u0026gt;left = l; n-\u0026gt;right = r; return n; } static void free_tree(TreeNode* root) { if (!root) return; free_tree(root-\u0026gt;left); free_tree(root-\u0026gt;right); free(root); } int main(void) { /* Example 1 */ TreeNode* root = node(10, node(5, node(3, node(3, NULL, NULL), node(-2, NULL, NULL)), node(2, NULL, node(1, NULL, NULL))), node(-3, NULL, node(11, NULL, NULL))); printf(\u0026#34;%d\\n\u0026#34;, pathSum(root, 8)); /* 3 */ free_tree(root); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;unordered_map\u0026gt; struct TreeNode { int val; TreeNode* left; TreeNode* right; explicit TreeNode(int v) : val(v), left(nullptr), right(nullptr) {} }; static int dfs(TreeNode* node, long long cur, long long target, std::unordered_map\u0026lt;long long, int\u0026gt;\u0026amp; cnt) { if (!node) return 0; cur += node-\u0026gt;val; int ans = 0; auto it = cnt.find(cur - target); if (it != cnt.end()) ans += it-\u0026gt;second; cnt[cur] += 1; ans += dfs(node-\u0026gt;left, cur, target, cnt); ans += dfs(node-\u0026gt;right, cur, target, cnt); cnt[cur] -= 1; return ans; } int pathSum(TreeNode* root, int targetSum) { std::unordered_map\u0026lt;long long, int\u0026gt; cnt; cnt[0] = 1; return dfs(root, 0, targetSum, cnt); } int main() { // Example 1 auto* root = new TreeNode(10); root-\u0026gt;left = new TreeNode(5); root-\u0026gt;right = new TreeNode(-3); root-\u0026gt;left-\u0026gt;left = new TreeNode(3); root-\u0026gt;left-\u0026gt;right = new TreeNode(2); root-\u0026gt;right-\u0026gt;right = new TreeNode(11); root-\u0026gt;left-\u0026gt;left-\u0026gt;left = new TreeNode(3); root-\u0026gt;left-\u0026gt;left-\u0026gt;right = new TreeNode(-2); root-\u0026gt;left-\u0026gt;right-\u0026gt;right = new TreeNode(1); std::cout \u0026lt;\u0026lt; pathSum(root, 8) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // 3 // Omit delete in this demo. In production, release memory properly. return 0; } package main import \u0026#34;fmt\u0026#34; type TreeNode struct { Val int64 Left *TreeNode Right *TreeNode } func pathSum(root *TreeNode, targetSum int64) int64 { cnt := map[int64]int64{0: 1} var dfs func(*TreeNode, int64) int64 dfs = func(node *TreeNode, cur int64) int64 { if node == nil { return 0 } cur += node.Val ans := cnt[cur-targetSum] cnt[cur]++ ans += dfs(node.Left, cur) ans += dfs(node.Right, cur) cnt[cur]-- return ans } return dfs(root, 0) } func main() { // Example 1 root := \u0026amp;TreeNode{Val: 10} root.Left = \u0026amp;TreeNode{Val: 5} root.Right = \u0026amp;TreeNode{Val: -3} root.Left.Left = \u0026amp;TreeNode{Val: 3} root.Left.Right = \u0026amp;TreeNode{Val: 2} root.Right.Right = \u0026amp;TreeNode{Val: 11} root.Left.Left.Left = \u0026amp;TreeNode{Val: 3} root.Left.Left.Right = \u0026amp;TreeNode{Val: -2} root.Left.Right.Right = \u0026amp;TreeNode{Val: 1} fmt.Println(pathSum(root, 8)) // 3 } use std::collections::HashMap; #[derive(Debug)] struct TreeNode { val: i64, left: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, right: Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, } impl TreeNode { fn new(val: i64) -\u0026gt; Self { TreeNode { val, left: None, right: None } } } fn dfs(node: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, cur: i64, target: i64, cnt: \u0026amp;mut HashMap\u0026lt;i64, i32\u0026gt;) -\u0026gt; i32 { let Some(n) = node.as_ref() else { return 0 }; let cur = cur + n.val; let mut ans = *cnt.get(\u0026amp;(cur - target)).unwrap_or(\u0026amp;0); *cnt.entry(cur).or_insert(0) += 1; ans += dfs(\u0026amp;n.left, cur, target, cnt); ans += dfs(\u0026amp;n.right, cur, target, cnt); if let Some(v) = cnt.get_mut(\u0026amp;cur) { *v -= 1; } ans } fn path_sum(root: \u0026amp;Option\u0026lt;Box\u0026lt;TreeNode\u0026gt;\u0026gt;, target: i64) -\u0026gt; i32 { let mut cnt: HashMap\u0026lt;i64, i32\u0026gt; = HashMap::new(); cnt.insert(0, 1); dfs(root, 0, target, \u0026amp;mut cnt) } fn main() { // Example 1 let mut root = Box::new(TreeNode::new(10)); root.left = Some(Box::new(TreeNode::new(5))); root.right = Some(Box::new(TreeNode::new(-3))); { let left = root.left.as_mut().unwrap(); left.left = Some(Box::new(TreeNode::new(3))); left.right = Some(Box::new(TreeNode::new(2))); let ll = left.left.as_mut().unwrap(); ll.left = Some(Box::new(TreeNode::new(3))); ll.right = Some(Box::new(TreeNode::new(-2))); let lr = left.right.as_mut().unwrap(); lr.right = Some(Box::new(TreeNode::new(1))); } { let right = root.right.as_mut().unwrap(); right.right = Some(Box::new(TreeNode::new(11))); } let root = Some(root); println!(\u0026#34;{}\u0026#34;, path_sum(\u0026amp;root, 8)); // 3 } function TreeNode(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } function pathSum(root, targetSum) { const cnt = new Map(); cnt.set(0, 1); function dfs(node, cur) { if (!node) return 0; cur += node.val; let ans = cnt.get(cur - targetSum) || 0; cnt.set(cur, (cnt.get(cur) || 0) + 1); ans += dfs(node.left, cur); ans += dfs(node.right, cur); cnt.set(cur, cnt.get(cur) - 1); return ans; } return dfs(root, 0); } // Example 1 const root = new TreeNode( 10, new TreeNode(5, new TreeNode(3, new TreeNode(3), new TreeNode(-2)), new TreeNode(2, null, new TreeNode(1))), new TreeNode(-3, null, new TreeNode(11)), ); console.log(pathSum(root, 8)); // 3 ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/437-path-sum-iii/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nThe constraint “the path can start anywhere, but must go downward” makes root-to-leaf DP insufficient. This ACERS guide explains \u003cstrong\u003eprefix sums on trees\u003c/strong\u003e: convert any downward path into a difference of two prefix sums, maintain a frequency hash map during one DFS, and finish in O(n).\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12–15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003ebinary tree\u003c/code\u003e, \u003ccode\u003eprefix sum\u003c/code\u003e, \u003ccode\u003eDFS\u003c/code\u003e, \u003ccode\u003ehash map\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Path Sum III, tree prefix sum, prefix-sum hash, LeetCode 437\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Count downward paths whose sum equals targetSum in O(n) via prefix sum + hash map, with derivation, tradeoffs, and multi-language implementations.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm-problem--algorithm\"\u003eA — Algorithm (Problem \u0026amp; Algorithm)\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven the root \u003ccode\u003eroot\u003c/code\u003e of a binary tree and an integer \u003ccode\u003etargetSum\u003c/code\u003e, return the number of \u003cstrong\u003edownward paths\u003c/strong\u003e whose node values sum to \u003ccode\u003etargetSum\u003c/code\u003e.\nThe path does not need to start at the root or end at a leaf, but it must go downward (parent → child).\u003c/p\u003e","title":"Path Sum III: Prefix Sum + Hash Map Counting Downward Paths (LeetCode 437) ACERS Guide"},{"content":" Subtitle / Summary\nSpiral traversal looks like “just printing in a fancy order”, but the real difficulty is getting boundaries and invariants right. This ACERS guide gives a reusable boundary-shrinking template and runnable multi-language solutions.\nReading time: 12–15 min Tags: Hot100, matrix, simulation, boundary shrinking SEO keywords: Hot100, Spiral Matrix, clockwise traversal, boundary shrinking, LeetCode 54 Meta description: O(mn) spiral order traversal using boundary shrinking, with pitfalls, engineering scenarios, and runnable code. A — Algorithm Problem Restatement Given an m × n matrix matrix, return all elements in clockwise spiral order.\nInput / Output Name Type Description matrix int[][] an m × n matrix return int[] elements in clockwise spiral order Example 1 matrix = [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] output: [1, 2, 3, 6, 9, 8, 7, 4, 5] Example 2 matrix = [ [ 1, 2, 3, 4], [ 5, 6, 7, 8], [ 9, 10, 11, 12] ] output: [1, 2, 3, 4, 8, 12, 11, 10, 9, 5, 6, 7] Target Readers Hot100 learners who want a reliable “matrix simulation” template Intermediate engineers who often get boundary cases wrong Anyone working with grids (visualization, raster data, path generation) Background / Motivation Matrix problems are notorious for being “easy to code, hard to get 100% correct”.\nOne extra loop or one missed boundary check can break single-row/single-column cases or cause duplicated output.\nSpiral Matrix is a great training problem because it forces you to make the loop invariant explicit:\nWhat region is still unvisited? How do we shrink it safely after finishing an edge? If you can express that invariant clearly, the code becomes short and robust.\nCore Concepts Boundaries: top / bottom / left / right define the current unvisited rectangle Layer: each iteration peels one outer “ring” (top row, right col, bottom row, left col) Shrink: after finishing an edge, move the boundary inward (top++, right--, bottom--, left++) Loop invariant: the unvisited region is always top..bottom × left..right C — Concepts Thought Process: from visited to boundary shrinking Naive: direction array + visited\nStart at (0,0), move right/down/left/up, rotate when hitting a boundary or visited cell.\nPros: intuitive Cons: needs m×n extra space; more branching and easier to get wrong Key observation: spiral traversal is “peel the onion”\nEach layer is exactly four edges: top row, right column, bottom row, left column.\nChosen method: boundary shrinking (O(1) extra space)\nMaintain top, bottom, left, right. Traverse edges, then shrink the boundary.\nMethod Type Matrix simulation Boundary shrinking Loop invariant + boundary conditions The key invariant (the reason it’s correct) At the start of each loop, the unvisited region is a rectangle:\nrows: top .. bottom cols: left .. right After finishing an edge, we shrink the corresponding boundary by 1:\ntop row done → top += 1 right col done → right -= 1 bottom row done → bottom -= 1 (only if top \u0026lt;= bottom) left col done → left += 1 (only if left \u0026lt;= right) Those two conditional checks are what prevent duplicates when only one row/column remains.\nPractical Steps Handle empty matrix: return [] Initialize boundaries: top=0, bottom=m-1, left=0, right=n-1 While top \u0026lt;= bottom and left \u0026lt;= right: Traverse top row (left → right), top++ Traverse right col (top → bottom), right-- If top \u0026lt;= bottom, traverse bottom row (right → left), bottom-- If left \u0026lt;= right, traverse left col (bottom → top), left++ Return the result Runnable Python example (save as spiral_matrix.py):\nfrom typing import List def spiral_order(matrix: List[List[int]]) -\u0026gt; List[int]: if not matrix or not matrix[0]: return [] m, n = len(matrix), len(matrix[0]) top, bottom, left, right = 0, m - 1, 0, n - 1 res: List[int] = [] while top \u0026lt;= bottom and left \u0026lt;= right: for j in range(left, right + 1): res.append(matrix[top][j]) top += 1 for i in range(top, bottom + 1): res.append(matrix[i][right]) right -= 1 if top \u0026lt;= bottom: for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom -= 1 if left \u0026lt;= right: for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left += 1 return res if __name__ == \u0026#34;__main__\u0026#34;: print(spiral_order([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) print(spiral_order([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])) E — Engineering The “engineering value” of this problem is that it’s a reusable grid path generator.\nYou can replace “append value” with “emit coordinates”, and map coordinates to any domain object (pixels, tiles, warehouse bins, table cells).\nScenario 1: spiral feature extraction for image/raster patches (Python) Background: flatten a small patch (e.g., 7×7, 11×11) into a 1D feature vector.\nWhy it fits: spiral order preserves “outer-to-inner” structure in the sequence.\nfrom typing import List def spiral_order(matrix: List[List[int]]) -\u0026gt; List[int]: if not matrix or not matrix[0]: return [] m, n = len(matrix), len(matrix[0]) top, bottom, left, right = 0, m - 1, 0, n - 1 res: List[int] = [] while top \u0026lt;= bottom and left \u0026lt;= right: for j in range(left, right + 1): res.append(matrix[top][j]) top += 1 for i in range(top, bottom + 1): res.append(matrix[i][right]) right -= 1 if top \u0026lt;= bottom: for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom -= 1 if left \u0026lt;= right: for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left += 1 return res print(spiral_order([[0, 0, 1], [0, 1, 1], [1, 1, 1]])) Scenario 2: backend service streaming a grid “layer by layer” (Go) Background: maps/tiles/seat grids often want progressive loading: outer ring first for faster “first screen”.\nWhy it fits: boundary shrinking naturally yields a layer-by-layer traversal.\npackage main import \u0026#34;fmt\u0026#34; func spiralOrder(matrix [][]int) []int { if len(matrix) == 0 || len(matrix[0]) == 0 { return []int{} } m, n := len(matrix), len(matrix[0]) top, bottom, left, right := 0, m-1, 0, n-1 res := make([]int, 0, m*n) for top \u0026lt;= bottom \u0026amp;\u0026amp; left \u0026lt;= right { for j := left; j \u0026lt;= right; j++ { res = append(res, matrix[top][j]) } top++ for i := top; i \u0026lt;= bottom; i++ { res = append(res, matrix[i][right]) } right-- if top \u0026lt;= bottom { for j := right; j \u0026gt;= left; j-- { res = append(res, matrix[bottom][j]) } bottom-- } if left \u0026lt;= right { for i := bottom; i \u0026gt;= top; i-- { res = append(res, matrix[i][left]) } left++ } } return res } func main() { grid := [][]int{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}} fmt.Println(spiralOrder(grid)) } Scenario 3: spiral scan path for robots / automation (C) Background: coverage scanning in a discrete grid (inspection, cleaning, sampling).\nWhy it fits: O(1) state; no visited buffer needed (good for embedded).\n#include \u0026lt;stdio.h\u0026gt; static void spiral_path(int m, int n) { int top = 0, bottom = m - 1, left = 0, right = n - 1; while (top \u0026lt;= bottom \u0026amp;\u0026amp; left \u0026lt;= right) { for (int j = left; j \u0026lt;= right; ++j) printf(\u0026#34;(%d,%d) \u0026#34;, top, j); ++top; for (int i = top; i \u0026lt;= bottom; ++i) printf(\u0026#34;(%d,%d) \u0026#34;, i, right); --right; if (top \u0026lt;= bottom) { for (int j = right; j \u0026gt;= left; --j) printf(\u0026#34;(%d,%d) \u0026#34;, bottom, j); --bottom; } if (left \u0026lt;= right) { for (int i = bottom; i \u0026gt;= top; --i) printf(\u0026#34;(%d,%d) \u0026#34;, i, left); ++left; } } printf(\u0026#34;\\\\n\u0026#34;); } int main(void) { spiral_path(3, 4); return 0; } Scenario 4: spiral highlight animation in a grid UI (JavaScript) Background: highlight cells in a spiral order for tutorials/animations on a grid.\nWhy it fits: you only need the traversal order as a frame sequence.\nfunction spiralOrder(matrix) { if (!matrix.length || !matrix[0].length) return []; let top = 0, bottom = matrix.length - 1; let left = 0, right = matrix[0].length - 1; const res = []; while (top \u0026lt;= bottom \u0026amp;\u0026amp; left \u0026lt;= right) { for (let j = left; j \u0026lt;= right; j++) res.push(matrix[top][j]); top++; for (let i = top; i \u0026lt;= bottom; i++) res.push(matrix[i][right]); right--; if (top \u0026lt;= bottom) { for (let j = right; j \u0026gt;= left; j--) res.push(matrix[bottom][j]); bottom--; } if (left \u0026lt;= right) { for (let i = bottom; i \u0026gt;= top; i--) res.push(matrix[i][left]); left++; } } return res; } console.log(spiralOrder([[1, 2, 3], [4, 5, 6], [7, 8, 9]])); R — Reflection Complexity Time: O(mn) (each element is output exactly once) Space: O(1) extra (excluding the output array); visited approach uses O(mn) Alternatives Method Idea Extra Space Typical issues visited + direction turns “walk and turn” O(mn) more conditionals, easy to get off-by-one recursion / per-layer slicing “peel layers” depends may introduce slicing copies or recursion overhead boundary shrinking (this post) traverse 4 edges + shrink O(1) must handle single-row/single-col carefully Why boundary shrinking is engineering-friendly Minimal state: four integers describe the progress No extra matrix: good for memory-constrained environments Easy to batch: each layer is naturally a “batch” for progressive output Explanation / Why it works Think of the unvisited part as a shrinking rectangle:\ntop edge: output row top, then top++ right edge: output col right, then right-- bottom edge: only if top \u0026lt;= bottom, output row bottom, then bottom-- left edge: only if left \u0026lt;= right, output col left, then left++ Those two checks are critical: when only one row or one column is left, you must skip the opposite edges to avoid duplicates.\nCommon Pitfalls and Notes Why do we need if top \u0026lt;= bottom and if left \u0026lt;= right?\nThey handle the “single remaining row/column” cases and prevent duplicates.\nShould we handle empty inputs?\nLeetCode usually guarantees m,n \u0026gt;= 1, but production code should return [] for empty input.\nWhat about jagged arrays (rows with different lengths)?\nThe problem assumes a rectangular matrix. Validate/normalize in real systems.\nHow to output coordinates instead of values?\nReplace res.append(matrix[i][j]) with emitting (i, j) (or pushing into a queue/channel).\nBest Practices Write the invariant first: unvisited region is top..bottom × left..right Shrink boundaries immediately after finishing an edge Keep the two safety checks for bottom/left edges For streaming output, replace “append” with “yield/send” S — Summary Key Takeaways Spiral traversal is “peel layers”, not “random turns” top/bottom/left/right boundaries give O(1) extra space The two checks (top\u0026lt;=bottom, left\u0026lt;=right) are the correctness key The same template works as a generic grid path generator Conclusion The goal is not “pass the sample”, but “never break on single-row/single-col edge cases”.\nOnce you master boundary shrinking, many matrix simulation problems become straightforward.\nReferences \u0026amp; Further Reading LeetCode 54. Spiral Matrix LeetCode 59. Spiral Matrix II (generate spiral matrix) LeetCode 885. Spiral Matrix III (expanding spiral path) Meta Reading time: 12–15 min Tags: Hot100, matrix, simulation, boundary shrinking, LeetCode 54 SEO keywords: Hot100, Spiral Matrix, boundary shrinking, O(mn), LeetCode 54 Meta description: O(mn) spiral traversal using boundary shrinking, with pitfalls and multi-language code. Call to Action Turn the boundary-shrinking logic into your personal “matrix simulation template”.\nNext time you see a spiral/layered traversal, copy the template and only swap the “output action”.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) from typing import List def spiral_order(matrix: List[List[int]]) -\u0026gt; List[int]: if not matrix or not matrix[0]: return [] m, n = len(matrix), len(matrix[0]) top, bottom, left, right = 0, m - 1, 0, n - 1 res: List[int] = [] while top \u0026lt;= bottom and left \u0026lt;= right: for j in range(left, right + 1): res.append(matrix[top][j]) top += 1 for i in range(top, bottom + 1): res.append(matrix[i][right]) right -= 1 if top \u0026lt;= bottom: for j in range(right, left - 1, -1): res.append(matrix[bottom][j]) bottom -= 1 if left \u0026lt;= right: for i in range(bottom, top - 1, -1): res.append(matrix[i][left]) left += 1 return res if __name__ == \u0026#34;__main__\u0026#34;: print(spiral_order([[1, 2, 3], [4, 5, 6], [7, 8, 9]])) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; int* spiral_order(int** matrix, int m, int n, int* returnSize) { if (m \u0026lt;= 0 || n \u0026lt;= 0) { *returnSize = 0; return NULL; } int total = m * n; int* res = (int*)malloc((size_t)total * sizeof(int)); int idx = 0; int top = 0, bottom = m - 1, left = 0, right = n - 1; while (top \u0026lt;= bottom \u0026amp;\u0026amp; left \u0026lt;= right) { for (int j = left; j \u0026lt;= right; ++j) res[idx++] = matrix[top][j]; ++top; for (int i = top; i \u0026lt;= bottom; ++i) res[idx++] = matrix[i][right]; --right; if (top \u0026lt;= bottom) { for (int j = right; j \u0026gt;= left; --j) res[idx++] = matrix[bottom][j]; --bottom; } if (left \u0026lt;= right) { for (int i = bottom; i \u0026gt;= top; --i) res[idx++] = matrix[i][left]; ++left; } } *returnSize = idx; return res; } int main(void) { int a0[] = {1, 2, 3, 4}; int a1[] = {5, 6, 7, 8}; int a2[] = {9, 10, 11, 12}; int* matrix[] = {a0, a1, a2}; int returnSize = 0; int* res = spiral_order(matrix, 3, 4, \u0026amp;returnSize); for (int i = 0; i \u0026lt; returnSize; ++i) { if (i) printf(\u0026#34;, \u0026#34;); printf(\u0026#34;%d\u0026#34;, res[i]); } printf(\u0026#34;\\\\n\u0026#34;); free(res); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; std::vector\u0026lt;int\u0026gt; spiralOrder(const std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt;\u0026amp; matrix) { if (matrix.empty() || matrix[0].empty()) return {}; int m = (int)matrix.size(); int n = (int)matrix[0].size(); int top = 0, bottom = m - 1, left = 0, right = n - 1; std::vector\u0026lt;int\u0026gt; res; res.reserve((size_t)m * (size_t)n); while (top \u0026lt;= bottom \u0026amp;\u0026amp; left \u0026lt;= right) { for (int j = left; j \u0026lt;= right; ++j) res.push_back(matrix[top][j]); ++top; for (int i = top; i \u0026lt;= bottom; ++i) res.push_back(matrix[i][right]); --right; if (top \u0026lt;= bottom) { for (int j = right; j \u0026gt;= left; --j) res.push_back(matrix[bottom][j]); --bottom; } if (left \u0026lt;= right) { for (int i = bottom; i \u0026gt;= top; --i) res.push_back(matrix[i][left]); ++left; } } return res; } int main() { std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; m = {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; auto res = spiralOrder(m); for (size_t i = 0; i \u0026lt; res.size(); ++i) { if (i) std::cout \u0026lt;\u0026lt; \u0026#34;, \u0026#34;; std::cout \u0026lt;\u0026lt; res[i]; } std::cout \u0026lt;\u0026lt; \\\u0026#34;\\\\n\\\u0026#34;; return 0; } package main import \\\u0026#34;fmt\\\u0026#34; func spiralOrder(matrix [][]int) []int { if len(matrix) == 0 || len(matrix[0]) == 0 { return []int{} } m, n := len(matrix), len(matrix[0]) top, bottom, left, right := 0, m-1, 0, n-1 res := make([]int, 0, m*n) for top \u0026lt;= bottom \u0026amp;\u0026amp; left \u0026lt;= right { for j := left; j \u0026lt;= right; j++ { res = append(res, matrix[top][j]) } top++ for i := top; i \u0026lt;= bottom; i++ { res = append(res, matrix[i][right]) } right-- if top \u0026lt;= bottom { for j := right; j \u0026gt;= left; j-- { res = append(res, matrix[bottom][j]) } bottom-- } if left \u0026lt;= right { for i := bottom; i \u0026gt;= top; i-- { res = append(res, matrix[i][left]) } left++ } } return res } func main() { grid := [][]int{{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}} fmt.Println(spiralOrder(grid)) } fn spiral_order(matrix: \u0026amp;[Vec\u0026lt;i32\u0026gt;]) -\u0026gt; Vec\u0026lt;i32\u0026gt; { if matrix.is_empty() || matrix[0].is_empty() { return vec![]; } let m = matrix.len() as i32; let n = matrix[0].len() as i32; let (mut top, mut bottom, mut left, mut right) = (0i32, m - 1, 0i32, n - 1); let mut res: Vec\u0026lt;i32\u0026gt; = Vec::with_capacity((m * n) as usize); while top \u0026lt;= bottom \u0026amp;\u0026amp; left \u0026lt;= right { for j in left..=right { res.push(matrix[top as usize][j as usize]); } top += 1; for i in top..=bottom { res.push(matrix[i as usize][right as usize]); } right -= 1; if top \u0026lt;= bottom { for j in (left..=right).rev() { res.push(matrix[bottom as usize][j as usize]); } bottom -= 1; } if left \u0026lt;= right { for i in (top..=bottom).rev() { res.push(matrix[i as usize][left as usize]); } left += 1; } } res } fn main() { let matrix = vec![vec![1, 2, 3, 4], vec![5, 6, 7, 8], vec![9, 10, 11, 12]]; println!(\\\u0026#34;{:?}\\\u0026#34;, spiral_order(\u0026amp;matrix)); } function spiralOrder(matrix) { if (!matrix.length || !matrix[0].length) return []; let top = 0, bottom = matrix.length - 1; let left = 0, right = matrix[0].length - 1; const res = []; while (top \u0026lt;= bottom \u0026amp;\u0026amp; left \u0026lt;= right) { for (let j = left; j \u0026lt;= right; j++) res.push(matrix[top][j]); top++; for (let i = top; i \u0026lt;= bottom; i++) res.push(matrix[i][right]); right--; if (top \u0026lt;= bottom) { for (let j = right; j \u0026gt;= left; j--) res.push(matrix[bottom][j]); bottom--; } if (left \u0026lt;= right) { for (let i = bottom; i \u0026gt;= top; i--) res.push(matrix[i][left]); left++; } } return res; } console.log(spiralOrder([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/54-spiral-matrix/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nSpiral traversal looks like “just printing in a fancy order”, but the real difficulty is getting boundaries and invariants right. This ACERS guide gives a reusable boundary-shrinking template and runnable multi-language solutions.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 12–15 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003ematrix\u003c/code\u003e, \u003ccode\u003esimulation\u003c/code\u003e, \u003ccode\u003eboundary shrinking\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Hot100, Spiral Matrix, clockwise traversal, boundary shrinking, LeetCode 54\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: O(mn) spiral order traversal using boundary shrinking, with pitfalls, engineering scenarios, and runnable code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven an \u003ccode\u003em × n\u003c/code\u003e matrix \u003ccode\u003ematrix\u003c/code\u003e, return all elements in \u003cstrong\u003eclockwise spiral order\u003c/strong\u003e.\u003c/p\u003e","title":"Hot100: Spiral Matrix (Boundary Shrinking Simulation ACERS Guide)"},{"content":" Subtitle / Abstract Not a horizontal list. We use two core ideas: dependency path length and resource complexity. Treat \u0026ldquo;path length\u0026rdquo; as how far information can travel, and \u0026ldquo;resource complexity\u0026rdquo; as the hard constraint on trainability. Once you understand both, you can judge when CNN/RNN/LSTM/Transformer fits best and make measurable tradeoffs.\nEstimated reading time: Approx. 27 min Tags: cnn, rnn, lstm, transformer SEO keywords: CNN, RNN, LSTM, Transformer Meta description: Compare CNN, RNN, LSTM, and Transformer via path length and resource complexity. Target readers Beginners who want a fast comparison of major neural architectures Practitioners who must choose a model for production Developers working on sequence modeling or multimodal systems Background / Motivation Choosing a model answers two questions:\nHow far and how long can information travel in a sequence (dependency path length) Can compute and memory budgets support it (resource complexity) This article stays on these two axes and avoids a wide, shallow overview.\nA concrete example: when n = 1024, an RNN needs 1024 sequential steps for one forward pass. A Transformer can achieve global interaction within 6 to 12 layers, but the attention matrix has n^2 = 1,048,576 elements. These two hard facts almost decide the outcome: you are blocked by path length or by memory/throughput. Ignore either axis and you will pay with accuracy or cost.\nFast mastery map (60-120s) Problem shape: images/grids -\u0026gt; CNN; sequences -\u0026gt; RNN/LSTM/Transformer. Core difference: RNN/LSTM path length grows with n; Transformer path length is near 1 but cost grows as n^2. When to use/avoid: n \u0026lt;= 256 and low compute -\u0026gt; LSTM/RNN; n \u0026gt;= 512 and need parallelism -\u0026gt; Transformer; pure vision -\u0026gt; CNN. Complexity keywords: CNN O(HWk^2); RNN O(n d^2) serial; LSTM O(4 n d^2); Transformer O(n^2 d). Common traps: ignore n^2 memory, misjudge dependency range, mismatch masks or shapes. Master-level mental model Core abstraction: treat sequence modeling as information routing on a computation graph. Path length decides reachability; resource complexity decides feasibility. Problem family: local connections (CNN), chain propagation (RNN/LSTM), global similarity aggregation (Transformer) are different graphs with different shortest paths. Isomorphic template: information routing = aggregate(neighbors). RNN uses linear neighbors, CNN uses fixed-radius neighbors, attention uses all-to-all neighbors. Key invariant: if shortest path L grows with n, long dependencies are hard to learn; if interactions are n^2, memory and time costs are unavoidable. Core concepts and terms (only two deep dives) Dependency path length and parallelism: decides whether long dependencies can be modeled. Resource complexity (time/memory) vs n: decides whether the model can be trained or deployed. Key terms (used throughout):\nPath length L: shortest number of edges from position i to j in the computation graph. Sequential steps S: number of ordered steps in a forward pass. RNN has S ~= n, CNN/Transformer has S ~= number of layers. Receptive field R: span covered by CNN in input space, R = 1 + (k - 1) * L (no dilation). Sequence length n / hidden size d: dominant variables in complexity. These four values are enough to write the core formulas for path length and resource complexity.\nA directly usable estimate: If each layer only connects neighbors within radius r, then spanning distance d needs L \u0026gt;= ceil(d / r). Example: r = 2, d = 256 -\u0026gt; L \u0026gt;= 128, which is expensive in depth and gradients.\nProblem abstraction (inputs/outputs) Image input: X in R^{B x C x H x W}, output is classification/detection logits. Sequence input: X in R^{B x n x d}, output is per-step prediction or a sequence representation. Optimization target: maximize accuracy and throughput within compute/memory budget while meeting latency. Typical engineering ranges:\nSequence length: n in [128, 8192], and n \u0026gt;= 1024 is \u0026ldquo;long sequence\u0026rdquo;. Memory budget: 16 to 80 GB per GPU; n \u0026gt;= 4096 often triggers OOM with full attention. Latency target: online inference often wants P95 \u0026lt; 200 ms, amplifying serial bottlenecks. Feasibility and lower bound intuition Path length lower bound: if each layer only connects neighbors within radius r (RNN has r = 1, CNN has r = (k - 1)/2), then spanning distance d needs L \u0026gt;= ceil(d / r) layers. Example: 1D CNN with k = 3, r = 1, covering d = 512 requires L \u0026gt;= 512 layers. Even with k = 5 (r = 2), you still need L \u0026gt;= 256. Depth cost remains huge.\nAttention lower bound: full attention computes similarity for any i, j, which implies at least Omega(n^2) interactions or memory reads. Unless you drop interactions (window, sparse, approximate), this upper bound is unavoidable.\nA common compromise is downsample then attend: If you reduce n from 2048 to 1024, attention cost drops to 1/4, but each token covers more information, effectively changing the \u0026ldquo;path length\u0026rdquo;. You always trade between the two axes: compress length or pay quadratic cost.\nNaive baselines and bottlenecks Baseline 1: RNN on long sequences When n = 1024, you need 1024 sequential steps; GPU utilization is low. Backprop must keep all intermediate states, so training time rises sharply. Baseline 2: shallow CNN for long dependencies With k = 3 and L = 8, receptive field is only R = 17, which is almost blind for n = 512 tasks. Stack more layers to expand R and parameters and training time explode. Even if per-step compute is cheap, sequential steps decide latency: If one step is 0.3 ms, n = 512 RNN forward is about 154 ms. Transformer has only as many steps as layers (for example 6 layers ~ 1.8 ms). This is why baselines are usable but hard to scale.\nKey observation Dependency is not the time order itself, but the strength of relationships between positions. If all positions can \u0026ldquo;see\u0026rdquo; each other in one layer, path length drops from O(n) to O(1). The cost is interaction count jumping from O(n) to O(n^2), i.e., resource complexity.\nDeep concept 1: dependency path length and parallelism (PDKH) 1) Restate the problem (Polya) If information from position i must affect position j, it must travel along the computation graph. The longer the path, the more gradients decay and the slower training becomes.\nTreat each layer-position as a node, and each valid connection as an edge. Path length L is the shortest path length. Short paths mean fast aggregation; long paths mean repeated transforms before information arrives. This is why path length nearly decides whether long dependencies can be learned.\n2) Minimal example (Bentley) Let sequence length n = 6, and position 1 must influence position 6:\nRNN/LSTM: must pass step by step, path length = 5. CNN (k = 3, L = 2): receptive field is 1 + (k - 1)L = 5, still cannot reach position 6. You need L = 3 layers to cover the full range. Transformer: any position can attend within the same layer, path length = 1. Path length and parallelism comparison Structure Path length L (dependency distance d) Parallelism Notes RNN L = d Low Serial dependency, hard to parallelize LSTM L = d Low Gates mitigate gradient decay CNN L \u0026gt;= ceil((d - 1)/(k - 1)) Medium-High Depends on depth and kernel width Transformer L = 1 High Global attention in parallel Sequential step examples (S) Assume n = 1024 for a single forward pass:\nRNN/LSTM: 1024 ordered steps, S ~= 1024. Transformer (6 layers): 6 ordered steps, S ~= 6. CNN (20 layers): 20 ordered steps, S ~= 20. This explains why RNN throughput is low on GPU: not slow ops, but too many serial steps.\nA rough estimate: if each step is ~0.2 ms, S = 1024 gives ~205 ms per forward pass; S = 6 gives ~1.2 ms (ignoring communication and memory bottlenecks).\nWorked example: how many CNN layers for long dependencies? To cover dependency distance d = 512 with k = 3, L \u0026gt;= (d - 1)/(k - 1) = 255.5, so at least 256 layers. This is why CNNs are often replaced by attention for long sequences.\nMicro-trace: n = 4 dependency propagation Sequence [x1, x2, x3, x4], make x1 influence x4:\nRNN: x1 -\u0026gt; h2 -\u0026gt; h3 -\u0026gt; h4, path length = 3. CNN (k = 3, L = 2): layer 1 lets x1 affect {x1, x2}, layer 2 reaches x3 but not x4. Transformer: x1 directly participates in x4 attention, path length = 1. This tiny example shows path length differences exist even at the smallest scale.\n3) Invariants / contracts (Dijkstra/Hoare) To stably capture dependencies of distance d, the graph must provide paths with length L \u0026lt;= d. When L grows with n, long-dependency training becomes substantially harder.\nGradient decay intuition RNN gradients are products of Jacobians: d h_t / d h_{t-k} = product_{i=t-k+1}^{t} J_i. When k is large, the product quickly shrinks or explodes. This is the root of long dependency difficulty. LSTM uses the cell state as a more direct path, but the length is still O(n).\nA numeric intuition: if the average spectral radius is ~0.9, then after 100 steps the gradient magnitude is about 0.9^100 ~= 0.000026. Even at 0.99, 0.99^100 ~= 0.366, still decaying. So the longer the path, the more you rely on gates or residuals to keep training stable.\nDependency span examples (why long dependencies are hard) Copy task: sequence length n = 512, output the first token at the end. RNN/LSTM must carry information for 511 steps. Transformer can connect start and end in one attention pass.\nBracket matching: matching outer parentheses often spans the entire sequence. Such tasks are extremely sensitive to path length and often favor Transformers.\nEstimating dependency span Text: measure dependency distances within a sentence (often \u0026lt; 128). If cross-paragraph dependencies are common, spans can reach 512 or more. Time series: use autocorrelation length as \u0026ldquo;effective memory\u0026rdquo;. Video/visual sequences: span is driven by object trajectories across frames. Practically, use P90 as a \u0026ldquo;safe span\u0026rdquo;: If 90% of dependencies are below 256, CNN/LSTM is often enough. If P90 exceeds 512, Transformer advantages are usually stable.\nOnce you estimate a typical span d, model selection has a direction.\n4) Formalization (Knuth) RNN/LSTM: path length L = |i - j|. 1D CNN: receptive field R = 1 + (k - 1)L, so covering distance d needs L \u0026gt;= (d - 1)/(k - 1). Transformer: one layer connects any positions, L = 1. Parallelism can be seen as \u0026ldquo;how many sequential steps are required\u0026rdquo;: RNN/LSTM need n steps; CNN/Transformer mainly depend on depth. This directly explains why Transformers have high training throughput.\n5) Correctness sketch (Dijkstra/Hoare) RNN state only moves from t-1 to t, so crossing distance d needs d steps. CNN expands receptive field by (k - 1) per layer, so L layers give R = 1 + (k - 1)L. Transformer attention builds global dependencies, so path length is 1. Structure-by-structure deepening (path length view) CNN: Receptive field grows linearly. For k = 3, it goes 3, 5, 7, 9\u0026hellip; At L = 6, R = 13; at L = 20, R = 41. This shows why CNNs need extreme depth for long dependencies.\nWith dilation, the formula becomes R = 1 + (k - 1) * sum d_l. Example: 4 layers with d_l = [1, 2, 4, 8] gives R = 1 + 2 * (1 + 2 + 4 + 8) = 31. This is larger but still far from n = 512. It improves path length but does not change the linear-growth nature.\nRNN: Path length equals time steps. For n = 512, the furthest dependency needs 511 state transfers. Even if per-step compute is cheap, long chains amplify gradient decay.\nLSTM: Gating stabilizes \u0026ldquo;effective memory\u0026rdquo;, but path length is still O(n). In practice, tricks like setting forget bias b_f = 1 extend memory but do not change the order of growth.\nTransformer: Path length is 1, turning long dependencies into global parallel matrix ops. The cost is higher memory and compute (see concept 2).\nHow LSTM gating extends \u0026ldquo;effective memory\u0026rdquo; LSTM centers on cell state c_t with three gates: f_t = sigma(W_f [x_t, h_{t-1}]) (forget gate) i_t = sigma(W_i [x_t, h_{t-1}]) (input gate) o_t = sigma(W_o [x_t, h_{t-1}]) (output gate) c_t = f_t * c_{t-1} + i_t * tanh(W_c [x_t, h_{t-1}])\nHere * is elementwise multiplication. When f_t is near 1, c_t preserves information longer. This explains why LSTM handles medium-long sequences better than vanilla RNN, but it does not change the fact that path length grows with n.\nIf the mean f_t is ~0.95, the memory factor after 200 steps is 0.95^200 ~= 0.00034. Even at 0.99, after 200 steps it is 0.99^200 ~= 0.133. Gates extend \u0026ldquo;effective path length\u0026rdquo; but cannot change the order.\nTransformer \u0026ldquo;short path\u0026rdquo; still needs order signals Attention is permutation-invariant; without positional encoding, Transformer treats the sequence as a set. So path length = 1 does not automatically solve order. Positional encoding is required.\nA common sinusoidal form: PE(pos, 2i) = sin(pos / 10000^(2i/d_model)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))\nFrom a graph view, attention matrix A = softmax(QK^T) is a weighted fully connected graph. Each row sums to 1, output is a convex combination of values. So a single attention layer can route information between any positions, which is why path length collapses to 1.\nWorked example: CNN receptive field growth table (k = 3) Layers L Receptive field R 1 3 2 5 3 7 4 9 8 17 16 33 def receptive_field(k, layers): return 1 + (k - 1) * layers for L in [1, 2, 4, 8, 16]: print(L, receptive_field(3, L)) 6) Thresholds and scale (Knuth) When dependency span \u0026gt; 256, RNN/LSTM often struggle. When span \u0026gt; 512, Transformer advantages become clear. But this also introduces n^2 cost (see concept 2). These thresholds are empirical, not theoretical limits. In speech and short text (n ~ 128-256), LSTM can still be stable. In long documents and code (n \u0026gt;= 512), path length dominates, and if you also need high throughput, attention parallelism becomes valuable.\n7) Counterexamples / failure modes (Bentley/Sedgewick) If the task is local dependency (for example n \u0026lt;= 128 short text classification), Transformer can overfit due to excessive global modeling. In that case LSTM or 1D CNN is often more stable.\nExample: in a sentiment task with n = 64 and only tens of thousands of samples, Transformer capacity can be too high, so path-length advantage does not translate to accuracy.\n8) Engineering reality (Knuth) Shorter path is not always better: Transformer needs positional encoding to express order; RNN/LSTM can still keep stable memory for n = 200-500.\nCommon \u0026ldquo;mitigations\u0026rdquo;:\nCNN uses residual or pyramid structures to extend receptive field. RNN/LSTM uses truncated BPTT to control training cost. Transformer uses relative positional encoding to strengthen locality. All of these shorten the effective path without changing the main structure. Truncated BPTT impact: if you truncate backprop to 256 steps, you effectively cap dependency span at 256. This is fine for speech or short text, but hurts long-doc summarization or code understanding. So truncation length is your \u0026ldquo;engineering path length budget\u0026rdquo;.\nDeep concept 2: resource complexity as n grows (PDKH) 1) Restate the problem (Polya) When n grows, can the model still be trained and deployed? This is decided by time and memory complexity.\nSplit resource complexity into three dimensions:\nCompute (FLOPs): determines training/inference speed. Memory footprint: decides OOM risk. Memory bandwidth: determines whether throughput is limited by reads/writes. Transformer is often not compute-bound; it is memory-bandwidth bound. 2) Minimal example (Bentley) Let n = 2048, d_model = 512, h = 8:\nAttention matrix has n^2 = 4,194,304 elements. One head in FP16 is about 8 MB; 8 heads about 64 MB. Training also needs activations and gradients, often 3 to 5 times peak. Resource estimate (attention weights) If batch is B, heads h, dtype FP16 (2 bytes): memory ~= B * h * n^2 * 2 bytes. Example: B = 4, h = 8, n = 2048: 4 * 8 * 2048^2 * 2 ~= 512 MB (attention weights only).\nThis scales brutally:\nB doubles -\u0026gt; memory doubles. n doubles -\u0026gt; memory becomes 4x. h doubles -\u0026gt; memory doubles. So increasing n from 2k to 4k is often more dangerous than increasing layers. A more practical estimate is to solve for max n: n_max ~= sqrt(memory_budget / (B * h * 2 bytes)). If memory budget is 8 GB, B = 2, h = 8, then n_max ~= sqrt(8 GB / 32 bytes) ~= 16k. But with 4x to 8x peak overhead, real n is usually 3x to 4x smaller.\nn and memory scale (single head FP16) n n^2 elements Approx memory 512 262,144 ~0.5 MB 1024 1,048,576 ~2 MB 2048 4,194,304 ~8 MB 4096 16,777,216 ~32 MB 8192 67,108,864 ~128 MB You must also consider memory bandwidth: At n = 2048, one head\u0026rsquo;s weights are ~8 MB; with 12 layers that is ~96 MB of reads/writes. Training also reads/writes gradients and activations, so bandwidth pressure grows. This is why FlashAttention speeds up by reducing reads/writes.\ndef attn_memory_mb(n, h=8, batch=4, bytes_per_elem=2): return batch * h * n * n * bytes_per_elem / (1024 ** 2) for n in [512, 1024, 2048, 4096]: print(n, f\u0026#34;{attn_memory_mb(n):.1f} MB\u0026#34;) 3) Invariants / contracts (Dijkstra/Hoare) With full attention, you must compute n^2 interactions explicitly or implicitly. Without approximation, this cost cannot be avoided.\n4) Formalization (Knuth) CNN: O(HWk^2) (or O(n k d^2) for sequences) RNN: O(n d^2) (serial) LSTM: O(4 n d^2) Transformer: O(n^2 d) + O(n d^2) (FFN) Rough compute estimate (n = 1024, d = 512) RNN: per-step d^2, total 1024 * 512^2 ~= 268M MACs. LSTM: about 4x, ~1.07B MACs. Transformer attention: n^2 * d_k. If d_k = 64, 1024^2 * 64 ~= 67M MACs, but FFN adds 2 * n * d * d_ff (d_ff = 2048 gives ~2.1B). Conclusion: Transformer bottlenecks are often FFN and attention memory, not pure FLOPs.\nThe dominance boundary is: n^2 d (attention) vs 2 n d d_ff (FFN). Simplify to n \u0026gt; 2 d_ff for attention to dominate. If d_ff = 2048, attention dominates only when n \u0026gt; 4096. This explains why FFN is the bottleneck at moderate length and attention dominates at very long length.\n5) Correctness sketch (Dijkstra/Hoare) Transformer computes QK^T for all token pairs, so time and memory must scale with n^2.\n6) Thresholds and scale (Knuth) n \u0026lt;= 2048: full attention is usually feasible. 2048 \u0026lt; n \u0026lt;= 8192: use FlashAttention or block attention. n \u0026gt; 8192: require sparse/linear attention or retrieval. A practical upper bound: Single GPU 24 GB, B = 2, h = 8, attention weights at n = 4096 are ~512 MB. With activations and optimizer states, you often approach 16 to 24 GB. So n = 4k is already a warning line for single-GPU training.\n7) Counterexamples / failure modes (Bentley/Sedgewick) On a 16 GB single GPU, forcing n = 8k full attention will often OOM or require tiny batch sizes, lowering efficiency.\n8) Engineering reality (Knuth) Common fixes: FlashAttention, block attention, KV cache, gradient checkpointing. These trade engineering complexity for trainability and throughput.\nTraining vs inference complexity Training: full attention needs n^2 matrix, high memory and compute. Inference (autoregressive): with KV cache, each step interacts with history, per-step complexity is ~O(n), and memory is more manageable. This is why Transformer can be barely workable in inference, but needs strong compute and memory optimization during training.\nKV cache memory estimate: memory ~= B * h * n * d_k * 2 bytes. If B = 1, h = 8, n = 4096, d_k = 64, memory is ~4 MB. But if B = 8 or n = 16k, memory grows linearly and must be planned.\nWorked example: n = 1024 vs n = 4096 For single head FP16:\nn = 1024, attention weights ~2 MB. n = 4096, ~32 MB, 16x larger. If B = 4, h = 8, n = 4096, attention weights alone exceed 1 GB, not counting gradients or activations. So \u0026ldquo;double length\u0026rdquo; is not linear cost, it is geometric.\nComplexity and scale summary (two axes together) Structure Path length L Sequential steps S Time complexity (dominant) Memory complexity (dominant) CNN ~(d/(k-1)) ~layers O(n k d^2) or O(HWk^2) O(n d) RNN d ~n O(n d^2) O(n d) LSTM d ~n O(4 n d^2) O(n d) Transformer 1 ~layers O(n^2 d) + O(n d^2) O(n^2) + O(n d) This table puts path length and resource complexity on one plane: short-path structures (Transformer) are resource-heavy; resource-stable structures (RNN/LSTM) suffer on path length.\nRemember sequential steps S are a hard ceiling. Even with more machines, S is hard to parallelize away. For n = 1024, RNN needs 1024 ordered steps; multi-GPU only increases batch, not reduces steps.\nConstant factors and engineering reality (related to the two axes) Operator granularity: RNN has many small matmuls, GPU utilization is low; Transformer has fewer large matmuls, but bandwidth is the bottleneck. Precision and memory: FP16/BF16 halves attention and activation memory, but does not change path length or dependency span. Residuals and caching: residuals shorten effective path but increase activation storage; short-path models rely more on cache and bandwidth. Worked example (trace): same task, path and cost Toy task: n = 8, make token 1 influence token 8. Compare path and cost:\nRNN/LSTM Path length L = 7, must pass 7 state transfers. Sequential steps S = 8, cannot be parallelized.\nCNN (k = 3) Receptive field R = 1 + 2L. L = 1 -\u0026gt; R = 3, L = 2 -\u0026gt; R = 5, L = 3 -\u0026gt; R = 7, L = 4 -\u0026gt; R = 9. Only L \u0026gt;= 4 covers x1 -\u0026gt; x8.\nTransformer (1 layer) Path length L = 1, x1 can directly influence x8. But attention matrix has n^2 = 64 elements per head. This is tiny at n = 8, but jumps to 4,194,304 at n = 2048.\nThis shows: path advantage exists at small scale; resource disadvantage explodes at large scale.\nPractical guide / steps (selection workflow) Estimate dependency span: for text, use dependency or sentence span; frequent cross-paragraph links suggest d \u0026gt;= 512. Estimate sequence length n: use P50/P90/Max because n determines n^2 cost. Check budget: use B * h * n^2 * 2 bytes to estimate attention memory and leave 4x to 8x headroom. Check parallelism needs: if online inference needs P95 \u0026lt; 200 ms, serial models are likely out. Run a light baseline: small CNN/LSTM to verify learnability and set a minimum accuracy bar. Upgrade structure: if d is large and budget allows, move to Transformer; if budget is tight, consider local attention or hybrid models. You can compress this into two must-answer questions:\nDependency span: is the furthest dependency d clearly \u0026gt; 256? Budget: can memory handle B * h * n^2 attention? If both are clear, selection is usually on track.\nA simplified 2x2 decision matrix:\nsmall d + small budget -\u0026gt; CNN/LSTM small d + large budget -\u0026gt; small Transformer or CNN large d + small budget -\u0026gt; local attention or hybrid large d + large budget -\u0026gt; full Transformer If d is unclear, train a small attention model and inspect attention span distribution first.\nSelection guide Dependency span threshold: if d \u0026lt;= 128, CNN or small RNN is often enough; d \u0026gt;= 512 favors Transformer. Sequence length threshold: n \u0026lt;= 256 makes full attention cheap; n \u0026gt;= 2048 requires memory planning. Memory budget threshold: on a 24 GB GPU, B = 2, h = 8, n = 4096 attention weights are ~512 MB. Add activations and optimizer state and you can hit 16 to 24 GB quickly. Implementation complexity tolerance: if your team cannot optimize kernels, use mature implementations (standard Transformer + FlashAttention). Runnable example (minimal contrast) The code below only contrasts structure-level behavior, not training or loss. It helps you see CNN local aggregation, LSTM sequential state, and Transformer global interaction. Run it to observe output shapes.\nimport torch import torch.nn as nn # CNN cnn = nn.Sequential( nn.Conv2d(3, 16, 3, padding=1), nn.ReLU(), nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(16, 10), ) img = torch.randn(2, 3, 32, 32) print(\u0026#34;cnn:\u0026#34;, cnn(img).shape) # LSTM lstm = nn.LSTM(input_size=16, hidden_size=32, batch_first=True) seq = torch.randn(2, 5, 16) out, _ = lstm(seq) print(\u0026#34;lstm:\u0026#34;, out.shape) # Transformer encoder = nn.TransformerEncoder( nn.TransformerEncoderLayer(d_model=32, nhead=4, batch_first=True), num_layers=2, ) seq = torch.randn(2, 6, 32) print(\u0026#34;transformer:\u0026#34;, encoder(seq).shape) Explanation and principles (two axes) Dependency path: Transformer is shortest, RNN/LSTM is longest, CNN depends on depth and kernel size. Resource cost: Transformer is the most expensive (n^2), RNN/LSTM cost is linear but serial. Other differences (gates, positional encoding) are reinforcement for these two axes.\nIf you place models on a 2D chart:\nX-axis = path length (shorter to the left) Y-axis = resource complexity (lower down) RNN/LSTM sit lower but to the right, Transformer sits higher but to the left, and CNN position varies with kernel size and depth. The two axes are not independent: reduce n and you lower cost but increase each token\u0026rsquo;s semantic coverage; increase layers and you shorten path but raise compute and training difficulty. Real-world solutions often mix compression, local modules, and a small amount of global attention.\nEngineering scenarios (only 3, tied to two axes) Short text classification (n \u0026lt;= 128): small dependency span -\u0026gt; LSTM or 1D CNN is usually enough. With n \u0026lt;= 128, attention matrix is only ~16k elements, so Transformer advantage is limited. Long document summarization (n \u0026gt;= 1024): large dependency span -\u0026gt; Transformer, but you must manage n^2 cost. At n = 2048, attention weights are 4.2M elements, often requiring FlashAttention or block strategies. Streaming speech recognition: low latency requirement -\u0026gt; CNN + LSTM hybrid is more stable. Serial steps hurt real-time latency, so local CNN compresses first and LSTM preserves mid-range dependencies. Alternatives and tradeoffs (only around the two axes) Full attention vs local attention: Full attention is O(n^2), local window attention is O(n w). If n = 2048, w = 256, cost is ~8x lower, but path length grows to about n/w ~= 8. If dependency span d = 2048 and window w = 256, you need at least L \u0026gt;= ceil(d / w) = 8 layers for global reach. You trade lower memory for deeper paths and harder training. Deeper CNN vs adding attention: With k = 3, CNN needs 256 layers to cover d = 512. Attention can reduce path length to 1 but adds n^2 memory cost. RNN/LSTM vs Transformer: The former has linear resources but long path; the latter has short path but quadratic resources. When n is small and d is not large, RNN/LSTM can have better cost-performance. Increase kernel size vs increase depth: Larger k shortens required depth but compute grows as O(k d^2). More layers keeps small kernels but still increases path length and training difficulty. Skill ladder Master local structures: understand CNN receptive field and path length. Master chain propagation: understand RNN/LSTM state transfer and gradient decay. Master global routing: understand Transformer global interaction and n^2 cost. Extend in practice: for very long n or tight budget, try local attention or hybrid models. Common questions and notes Underestimating n^2 memory leads to failed training. Missing positional encoding makes Transformer unable to represent order. Large LSTM hidden size overfits on small data. Shallow CNNs \u0026ldquo;cannot see\u0026rdquo; global dependencies, so performance stalls. Truncated BPTT too short caps dependencies at 128/256 and hurts long-range tasks. Doubling n without more data raises overfitting risk and memory cost sharply. Looking only at parameter count often underestimates real memory use. Using average n for budgeting is risky: if P90 doubles, attention memory is 4x. Excessive padding wastes attention on useless tokens; use length bucketing. Best practices and recommendations Treat dependency span as the first decision axis. Treat memory/throughput budget as the second axis. Validate learnability with a light baseline, then upgrade. If span is large but budget is small, prefer sparse/block attention or hybrids. For long sequences, compute n percentiles before selecting full attention. Tune n and h first; they have the largest effect on memory and throughput. Track peak memory during training, not just parameter count. For ultra-long sequences, try chunking/downsampling and check accuracy. Log P90 n, peak memory, and throughput in training runs. Summary / Conclusion CNN suits local patterns and vision grids. RNN/LSTM suits short to mid sequences and low compute. Transformer excels at long dependencies and parallel training but has n^2 cost. Model selection hinges on two axes: dependency path length and resource complexity. When d \u0026gt; 512, path length often dominates; when n \u0026gt; 2048, memory dominates. If budget is limited, shorten n or use local attention before deepening the model. References and further reading https://arxiv.org/abs/1409.2329 https://arxiv.org/abs/1706.03762 https://arxiv.org/abs/2010.11929 Call to Action (CTA) Run the same dataset with an LSTM and a Transformer, compare dependency span and memory cost, and write down your conclusion.\n","permalink":"https://jeanphilogong.github.io/jeanblog/ai/architecture/cnn-rnn-lstm-transformer-comparison/","summary":"Using dependency path length and resource complexity, this article compares CNN, RNN, LSTM, and Transformer, and provides runnable examples plus a selection workflow.","title":"CNN, RNN, LSTM, and Transformer: Differences and When to Use Each"},{"content":"Problem Requirement You are given n non-negative integers in height. Each integer is the height of a bar with width 1, and all bars are adjacent from left to right.\nAfter rain, taller bars on both sides may hold water above shorter bars. Return the total amount of water trapped by the entire elevation map.\nLeetCode expects this interface:\nclass Solution: def trap(self, height: List[int]) -\u0026gt; int: ... Example 1 Input: height = [0,1,0,2,1,0,1,3,2,1,2,1] Output: 6 Example 2 Input: height = [4,2,0,3,2,5] Output: 9 Constraints n == len(height) 1 \u0026lt;= n \u0026lt;= 2 * 10^4 0 \u0026lt;= height[i] \u0026lt;= 10^5 Step 1: First Answer How Much Water One Position Holds Do not calculate the whole elevation map yet. Focus on one position:\nheight = [3,0,2] ^ i = 1 The bar at index 1 has height 0. There is a bar of height 3 on its left and a bar of height 2 on its right.\nIf we look only at the left side, it seems that the water could rise to height 3. But the right wall has height 2, so any water above 2 would spill over that side. The highest possible water level is therefore:\nmin(highest bar on the left, highest bar on the right) = min(3, 2) = 2 The water above this position is:\nwater level - current bar height = 2 - 0 = 2 The current baseline is:\nFind walls on both sides of the current position, then determine how high the water can remain.\nBut \u0026ldquo;look at both walls\u0026rdquo; is not executable enough. If one side contains several bars, we need the highest boundary that side can provide. If we use only the taller side, water may still spill over the shorter side.\nFor one index i, add one executable rule:\nFind left_highest in 0..i. Find right_highest in i..n-1. Let the shorter boundary determine water_level. Use water_level - height[i] as the water above this position. Both ranges include i. This guarantees that neither highest value is lower than height[i], so the result cannot become negative.\nWrite this local rule as the first runnable version:\nfrom typing import List def trapped_at(height: List[int], i: int) -\u0026gt; int: left_highest = max(height[: i + 1]) right_highest = max(height[i:]) water_level = min(left_highest, right_highest) return water_level - height[i] Check it against the valley above:\nassert trapped_at([3, 0, 2], 1) == 2 The two endpoints do not have complete boundaries on both sides, so neither traps water:\nassert trapped_at([3, 0, 2], 0) == 0 assert trapped_at([3, 0, 2], 2) == 0 Check another valley with equal-height boundaries:\nassert trapped_at([2, 1, 2], 1) == 1 Now this version can:\ncalculate the trapped water above any one index explain why the shorter of the two highest boundaries determines the water level keep the result non-negative by including the current position in both ranges It still lacks:\nthe total trapped water for the entire elevation map reuse across positions that repeatedly scan the same left and right ranges Step 2: Add the Water From Every Position The current version can answer:\nHow much water is trapped above index i? But the problem asks for the total across the entire elevation map. Calling trapped_at once misses every other position.\nThe current baseline is the local function from the previous step:\ndef trapped_at(height: List[int], i: int) -\u0026gt; int: left_highest = max(height[: i + 1]) right_highest = max(height[i:]) water_level = min(left_highest, right_highest) return water_level - height[i] Its local answer is correct. It only lacks a completion rule: process every index once and add each local contribution to total.\nAfter the previous version, add:\ndef trap_by_scanning(height: List[int]) -\u0026gt; int: total = 0 for i in range(len(height)): total += trapped_at(height, i) return total For [3,0,2], the three contributions are:\ni trapped_at(height, i) Running total 0 0 0 1 2 2 2 0 2 Now check complete inputs:\nassert trap_by_scanning([3, 0, 2]) == 2 assert trap_by_scanning([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]) == 6 assert trap_by_scanning([4, 2, 0, 3, 2, 5]) == 9 assert trap_by_scanning([1]) == 0 Now this version can:\ncalculate the total trapped water for the entire elevation map make every position contribute its local amount exactly once handle a single bar and inputs without a valley It still lacks:\nreuse when different indices search for the same highest boundaries efficient execution at the largest input size trapped_at examines O(n) elements for one position, and the outer loop processes n positions, so the worst-case time is O(n^2). Python slicing also creates O(n) temporary space. Replacing slices with explicit loops would remove that temporary allocation but would not remove the repeated O(n^2) scanning.\nStep 3: Store Boundaries That Were Already Found Consider:\nheight = [4,2,0,3,2,5] To find the highest left boundary for index 1, we inspect [4,2]. For index 2, we inspect [4,2,0], repeating all the work for [4,2].\nThe right side has the same problem. The input may contain 2 * 10^4 positions, so the O(n^2) baseline repeatedly answers the same prefix and suffix questions.\nThe current baseline is:\nfor i in range(len(height)): total += trapped_at(height, i) Its local formula is correct. The break is that every call searches for the boundaries again.\nReplace those searches with two reusable arrays:\nleft_highest[i]: the highest bar in 0..i right_highest[i]: the highest bar in i..n-1 Each new position only needs the result already stored beside it:\nleft_highest[i] = max(left_highest[i - 1], height[i]) right_highest[i] = max(right_highest[i + 1], height[i]) These names now participate directly in updates and in the final water calculation:\ndef trap_with_boundaries(height: List[int]) -\u0026gt; int: n = len(height) left_highest = [0] * n left_highest[0] = height[0] for i in range(1, n): left_highest[i] = max(left_highest[i - 1], height[i]) right_highest = [0] * n right_highest[n - 1] = height[n - 1] for i in range(n - 2, -1, -1): right_highest[i] = max(right_highest[i + 1], height[i]) total = 0 for i in range(n): water_level = min(left_highest[i], right_highest[i]) total += water_level - height[i] return total The two construction loops maintain these invariants:\nAfter writing left_highest[i], it equals the maximum of height[0..i].\nAfter writing right_highest[i], it equals the maximum of height[i..n-1].\nThe final loop therefore uses the same two real boundaries verified in Step 1, without searching for them again.\nCheck the middle version:\nassert trap_with_boundaries([3, 0, 2]) == 2 assert trap_with_boundaries([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]) == 6 assert trap_with_boundaries([4, 2, 0, 3, 2, 5]) == 9 assert trap_with_boundaries([5, 4, 3, 2, 1]) == 0 assert trap_with_boundaries([2, 2, 2]) == 0 Now this version can:\ncalculate the total in O(n) time reuse every highest prefix and suffix boundary preserve the local water formula for every position It still lacks:\na way to avoid storing two arrays of length n O(1) extra space The three linear loops take O(n) total time, and the two boundary arrays use O(n) extra space.\nStep 4: Settle Only the Side Whose Boundary Is Known The boundary-array version has reduced the time to O(n), but it stores two values for every index:\nleft_highest[0..n-1] right_highest[0..n-1] Once the final loop processes a position, it never uses that position again. The new pressure is:\nCan we keep only the two boundaries needed now instead of storing every boundary?\nThe current baseline depends on:\nwater[i] = min(left_highest[i], right_highest[i]) - height[i] Before removing the arrays, we must know which side already has a settled lower boundary.\nNow introduce four pieces of state:\nleft and right: the two ends of the unsettled interval left_highest: the highest bar seen from the beginning through left right_highest: the highest bar seen from right through the end First update both highest values. If:\nleft_highest \u0026lt;= right_highest then the true highest value to the right of left is at least right_highest, so it is also at least left_highest. The lower boundary for left is therefore known to be left_highest, and its water is:\nleft_highest - height[left] Conversely, if left_highest \u0026gt; right_highest, the lower boundary for right is known to be right_highest, and its water is:\nright_highest - height[right] Settle one side per iteration and move that pointer. Replace the boundary-array version with the final LeetCode implementation:\nfrom typing import List class Solution: def trap(self, height: List[int]) -\u0026gt; int: left = 0 right = len(height) - 1 left_highest = 0 right_highest = 0 total = 0 while left \u0026lt;= right: left_highest = max(left_highest, height[left]) right_highest = max(right_highest, height[right]) if left_highest \u0026lt;= right_highest: total += left_highest - height[left] left += 1 else: total += right_highest - height[right] right -= 1 return total Use [5,0,1,0,2] to check the easy-to-miss right-side branch:\nleft right left_highest right_highest Settled this round Added water total 0 4 5 2 Right index 4 0 0 0 3 5 2 Right index 3 2 2 0 2 5 2 Right index 2 1 3 0 1 5 2 Right index 1 2 5 0 0 5 5 Left index 0 0 5 Final checks:\nsolution = Solution() assert solution.trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]) == 6 assert solution.trap([4, 2, 0, 3, 2, 5]) == 9 assert solution.trap([5, 0, 1, 0, 2]) == 5 assert solution.trap([3, 0, 2]) == 2 assert solution.trap([5, 4, 3, 2, 1]) == 0 assert solution.trap([2, 2, 2]) == 0 assert solution.trap([1]) == 0 The loop invariant is:\nAt the start of each iteration, every index outside left..right has been settled exactly once.\nAfter updating the two highest values:\nleft_highest = max(height[0..left]) right_highest = max(height[right..n-1]) If left_highest \u0026lt;= right_highest, the true right boundary cannot be lower than left_highest, so settling left is correct. Otherwise, the true left boundary cannot be lower than right_highest, so settling right is correct. Exactly one pointer moves each round, carrying the invariant into the next iteration. When left \u0026gt; right, every index has been settled once, so total is the complete answer. This version now satisfies the problem requirements:\nTime: O(n). One pointer moves each round, and every index is settled once. Extra space: O(1). Only the pointers, highest boundaries, and total are stored. The input array is not modified. Two-Pointer Common Mistakes 1. Calculating Water Before Updating the Current Boundaries The current bar is also a candidate for the highest boundary on its side. First execute:\nleft_highest = max(left_highest, height[left]) right_highest = max(right_highest, height[right]) Only then is the local water calculation guaranteed to be non-negative.\n2. Replacing the Comparison With the Current Bar Heights Comparing height[left] and height[right] can support another correct implementation, but that version uses a different movement rule and proof. This tutorial settles a side from the maintained left_highest and right_highest; do not replace only the condition while keeping the original proof.\n3. Moving Both Pointers in One Iteration Only the side with the lower known boundary is safe to settle. Moving both pointers skips a position on the other side whose lower boundary may still be unknown.\n4. Excluding the Current Position From a Boundary Range If a highest value excludes the current bar, water_level - height[i] may become negative. All three versions include the current position in both highest values, so they do not need an extra max(0, ...).\n5. Changing left \u0026lt;= right to left \u0026lt; right Without Updating the Proof Other loop boundaries can support correct implementations, but this tutorial\u0026rsquo;s invariant says that every index is settled exactly once. left \u0026lt;= right explicitly processes the meeting position. Changing the loop condition also requires a different termination argument.\nAlternative: Settle Valleys With a Monotonic Stack The two-pointer solution settles one position as soon as its lower boundary is known. A monotonic stack uses a different unit of work:\nKeep valley bottoms unresolved until a higher right wall arrives, then settle the horizontal water layer closed by that wall.\nThis is not a further optimization of the two-pointer version. Both approaches run in O(n) time. The stack version is useful because it connects this problem to the same unresolved-candidate model used by Daily Temperatures and Largest Rectangle in Histogram.\nKeep Unresolved Indices in Non-Increasing Height Order Scan bars from left to right and store their indices in stack. The heights at those indices remain non-increasing from bottom to top:\nheight[stack[0]] \u0026gt;= height[stack[1]] \u0026gt;= ... If the current bar is not taller than the stack top, it cannot close a valley above that top. Push its index and continue.\nWhen the current bar is taller than the stack top, the top can act as a valley bottom whose right wall has just appeared:\nbottom = stack.pop() After the pop:\nthe current index right is the right wall the new stack top is the left wall bottom is the lower level between those walls If the stack becomes empty, no left wall exists, so that bottom cannot trap water.\nOtherwise, calculate the newly closed horizontal layer:\nwidth = right - left - 1 bounded_height = min(height[left], height[right]) - height[bottom] water = width * bounded_height Continue popping while the current bar is taller than the new stack top. One right wall may settle several layers of the same valley.\nTrace One Layer at a Time Use:\nheight = [4,2,0,3] Before index 3, the stack contains [0,1,2], with heights [4,2,0]. The current height is 3.\nFirst pop index 2, whose height is 0:\nleft = 1 right = 3 width = 3 - 1 - 1 = 1 bounded_height = min(2, 3) - 0 = 2 added water = 1 * 2 = 2 The current bar is still taller than the new top at index 1, so pop again:\nleft = 0 right = 3 width = 3 - 0 - 1 = 2 bounded_height = min(4, 3) - 2 = 1 added water = 2 * 1 = 2 The two pops calculate different vertical layers, so they do not count the same water twice. The total is 4.\nComplete Monotonic-Stack Implementation from typing import List class Solution: def trap(self, height: List[int]) -\u0026gt; int: total = 0 stack = [] for right, right_height in enumerate(height): while stack and right_height \u0026gt; height[stack[-1]]: bottom = stack.pop() if not stack: break left = stack[-1] width = right - left - 1 bounded_height = min(height[left], right_height) - height[bottom] total += width * bounded_height stack.append(right) return total Why Each Pop Is Correct Before processing right, every index in the stack is still missing a higher right wall. Heights are non-increasing from bottom to top.\nWhen height[right] \u0026gt; height[bottom]:\nright is the first processed position that can close water above bottom; otherwise bottom would have been popped earlier the new stack top is the nearest remaining left boundary both boundaries are higher than or equal to the level being added right - left - 1 covers exactly the positions between the two walls The pop calculates only the layer above height[bottom] and below the shorter wall. If another pop follows, it starts from a higher bottom level, so the layers remain disjoint.\nAfter all lower tops are popped, the current height is less than or equal to the stack-top height. Pushing right restores the non-increasing invariant.\nChecks solution = Solution() assert solution.trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]) == 6 assert solution.trap([4, 2, 0, 3, 2, 5]) == 9 assert solution.trap([4, 2, 0, 3]) == 4 assert solution.trap([3, 0, 2]) == 2 assert solution.trap([5, 4, 3, 2, 1]) == 0 assert solution.trap([2, 2, 2]) == 0 assert solution.trap([1]) == 0 Complexity Every index is pushed once and popped at most once. All iterations of the nested while therefore total O(n):\nTime: O(n). Extra space: O(n), for the index stack. The two-pointer version remains preferable when O(1) extra space is the main goal. The stack version is preferable when the learning goal is to recognize unresolved candidates and settle a full interval when its future boundary arrives.\nConnection to Other Monotonic-Stack Problems The stack mechanism is shared, but each problem assigns a different meaning to a pop:\nProblem Stack order What a pop settles 739 Daily Temperatures Non-increasing temperatures Waiting distance to the first warmer day 503 Next Greater Element II Non-increasing values Next greater value in a circular array 84 Largest Rectangle in Histogram Non-decreasing heights Maximal width for one limiting height 42 Trapping Rain Water Non-increasing heights Water layer closed by left and right walls Summary The derivation is:\ncalculate the water above one position -\u0026gt; scan for every position to get an O(n^2) correct solution -\u0026gt; store every highest prefix and suffix for O(n) time and O(n) space -\u0026gt; prove that the side with the lower known boundary can be settled now -\u0026gt; move one pointer per round for O(n) time and O(1) extra space The alternative monotonic-stack branch is:\nkeep unresolved valley indices in non-increasing height order -\u0026gt; let a higher right wall pop one valley bottom -\u0026gt; combine the new stack top and current index as two boundaries -\u0026gt; calculate one horizontal water layer -\u0026gt; push and pop every index at most once The essential part of the two-pointer solution is explaining why the lower known boundary settles one side. The essential part of the stack solution is explaining why one pop has both boundaries needed to settle a disjoint water layer. The two approaches share the same physical boundary model, but they organize the computation differently.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/42-trapping-rain-water/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given \u003ccode\u003en\u003c/code\u003e non-negative integers in \u003ccode\u003eheight\u003c/code\u003e. Each integer is the height of a bar with width \u003ccode\u003e1\u003c/code\u003e, and all bars are adjacent from left to right.\u003c/p\u003e\n\u003cp\u003eAfter rain, taller bars on both sides may hold water above shorter bars. Return the total amount of water trapped by the entire elevation map.\u003c/p\u003e\n\u003cp\u003eLeetCode expects this interface:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eSolution\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003etrap\u003c/span\u003e(self, height: List[int]) \u003cspan style=\"color:#f92672\"\u003e-\u0026gt;\u003c/span\u003e int:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e...\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"example-1\"\u003eExample 1\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: height = [0,1,0,2,1,0,1,3,2,1,2,1]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 6\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"example-2\"\u003eExample 2\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eInput: height = [4,2,0,3,2,5]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eOutput: 9\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"constraints\"\u003eConstraints\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-text\" data-lang=\"text\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003en == len(height)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e1 \u0026lt;= n \u0026lt;= 2 * 10^4\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e0 \u0026lt;= height[i] \u0026lt;= 10^5\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"step-1-first-answer-how-much-water-one-position-holds\"\u003eStep 1: First Answer How Much Water One Position Holds\u003c/h2\u003e\n\u003cp\u003eDo not calculate the whole elevation map yet. Focus on one position:\u003c/p\u003e","title":"LeetCode 42: How Much Rain Water Can an Elevation Map Hold?"},{"content":" Subtitle / Summary\nMaximum Subarray is the classic 1D DP / greedy template. This ACERS guide explains Kadane\u0026rsquo;s idea, engineering use cases, and runnable multi-language solutions.\nReading time: 10–12 min Tags: Hot100, dynamic programming, greedy SEO keywords: Maximum Subarray, Kadane, dynamic programming, O(n), Hot100 Meta description: Kadane O(n) maximum subarray sum with engineering scenarios and multi-language code. A — Algorithm Problem Restatement Given an integer array nums, find the contiguous subarray with the largest sum (must contain at least one element) and return the sum.\nInput / Output Name Type Description nums int[] integer array return int maximum subarray sum Example 1 (official) nums = [-2,1,-3,4,-1,2,1,-5,4] output = 6 explanation: subarray [4,-1,2,1] has sum 6 Example 2 (official) nums = [1] output = 1 Target Readers Hot100 learners building stable templates Engineers analyzing peak segments in time series Anyone who wants a clean O(n) solution Background / Motivation Maximum subarray sum appears in P\u0026amp;L streaks, KPI lift windows, anomaly bursts, and throughput gains.\nThe naive O(n^2) enumeration does not scale. Kadane\u0026rsquo;s algorithm solves it in one pass.\nCore Concepts Subarray: contiguous, non-empty segment State: dp[i] = best sum ending at index i Kadane: if the running sum is negative, drop it and restart C — Concepts Key Formula Let dp[i] be the maximum subarray sum ending at i:\ndp[i] = max(nums[i], dp[i-1] + nums[i]) answer = max(dp[i]) Method Type 1D DP Greedy restart when prefix is negative Intuition If the best sum ending at i-1 is negative, extending it only makes the sum worse.\nSo we restart at i.\nPractical Steps Initialize cur = nums[0], best = nums[0] Scan from index 1: cur = max(nums[i], cur + nums[i]) best = max(best, cur) Return best Runnable Python example (save as maximum_subarray.py):\ndef max_subarray(nums): cur = best = nums[0] for x in nums[1:]: cur = max(x, cur + x) best = max(best, cur) return best if __name__ == \u0026#34;__main__\u0026#34;: print(max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4])) print(max_subarray([1])) E — Engineering Scenario 1: Profit streak detection (Python, data analysis) Background: daily profit deltas, find the best contiguous streak.\nWhy it fits: Kadane yields the peak gain window in O(n).\ndef best_profit_streak(deltas): cur = best = deltas[0] for x in deltas[1:]: cur = max(x, cur + x) best = max(best, cur) return best print(best_profit_streak([3, -2, 5, -1, 2, -4, 6])) Scenario 2: High-performance metric bursts (C++) Background: find the strongest contiguous spike in CPU delta metrics.\nWhy it fits: O(n) scan is cache-friendly and fast.\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; int maxBurst(const std::vector\u0026lt;int\u0026gt;\u0026amp; deltas) { int cur = deltas[0]; int best = deltas[0]; for (size_t i = 1; i \u0026lt; deltas.size(); ++i) { cur = std::max(deltas[i], cur + deltas[i]); best = std::max(best, cur); } return best; } int main() { std::cout \u0026lt;\u0026lt; maxBurst({3, -2, 5, -1, 2}) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } Scenario 3: Backend throughput improvement window (Go) Background: QPS deltas over time, find the best continuous improvement.\nWhy it fits: Kadane works in a streaming update loop.\npackage main import \u0026#34;fmt\u0026#34; func maxIncrease(deltas []int) int { cur := deltas[0] best := deltas[0] for i := 1; i \u0026lt; len(deltas); i++ { if cur+deltas[i] \u0026gt; deltas[i] { cur += deltas[i] } else { cur = deltas[i] } if cur \u0026gt; best { best = cur } } return best } func main() { fmt.Println(maxIncrease([]int{3, -2, 5, -1, 2})) } Scenario 4: Frontend engagement lift (JavaScript) Background: analyze consecutive engagement deltas to find best campaign window.\nWhy it fits: linear scan in browser or Node.js.\nfunction maxSubArray(nums) { let cur = nums[0]; let best = nums[0]; for (let i = 1; i \u0026lt; nums.length; i++) { cur = Math.max(nums[i], cur + nums[i]); best = Math.max(best, cur); } return best; } console.log(maxSubArray([3, -2, 5, -1, 2])); R — Reflection Complexity Time: O(n) Space: O(1) Alternatives Method Idea Complexity Drawbacks Brute force enumerate all subarrays O(n^2) too slow Prefix sums compute each interval sum O(n^2) still slow Divide \u0026amp; conquer split and merge O(n log n) more complex Kadane 1D DP O(n) simplest and optimal Why This Is Optimal One pass, constant memory Clear correctness argument Easy to embed in streaming pipelines Explanation \u0026amp; Rationale Kadane\u0026rsquo;s algorithm keeps the best sum ending at the current index.\nWhen the running sum turns negative, it cannot help any future subarray, so we restart.\nThis gives the optimal sum with linear complexity.\nFAQs / Pitfalls What if all numbers are negative?\nStill works: the answer is the least negative single element.\nAre empty subarrays allowed?\nNo, the problem requires at least one element.\nDo we need the interval indices?\nIf needed, track start/end when updating cur.\nBest Practices Use two variables (cur, best) instead of full DP arrays Convert complex signals into delta arrays before applying Kadane If you need indices, store a candidate start pointer S — Summary Key Takeaways Maximum Subarray is a classic 1D DP template Kadane drops negative prefixes for optimality O(n) time and O(1) space scale well Works for all-negative arrays without special cases Common in profit, throughput, and metric burst analysis Conclusion Kadane is a must-know pattern for contiguous optimum problems.\nOnce mastered, you can apply it to many real-world sequences.\nReferences \u0026amp; Further Reading LeetCode 53. Maximum Subarray Standard DP textbooks (maximum subarray sum) CLRS discussion of divide-and-conquer vs Kadane Meta Reading time: 10–12 min Tags: Hot100, dynamic programming, greedy, subarray SEO keywords: Maximum Subarray, Kadane, O(n), Hot100 Meta description: Kadane O(n) maximum subarray sum with engineering scenarios. Call to Action If you are working through Hot100, turn Kadane into a reusable template in your toolbox.\nShare your engineering adaptations in the comments.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) def max_subarray(nums): cur = best = nums[0] for x in nums[1:]: cur = max(x, cur + x) best = max(best, cur) return best if __name__ == \u0026#34;__main__\u0026#34;: print(max_subarray([-2, 1, -3, 4, -1, 2, 1, -5, 4])) #include \u0026lt;stdio.h\u0026gt; int max_subarray(const int *nums, int n) { int cur = nums[0]; int best = nums[0]; for (int i = 1; i \u0026lt; n; ++i) { int with_cur = cur + nums[i]; cur = nums[i] \u0026gt; with_cur ? nums[i] : with_cur; if (cur \u0026gt; best) best = cur; } return best; } int main(void) { int nums[] = {-2, 1, -3, 4, -1, 2, 1, -5, 4}; printf(\u0026#34;%d\\n\u0026#34;, max_subarray(nums, 9)); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; int maxSubArray(const std::vector\u0026lt;int\u0026gt;\u0026amp; nums) { int cur = nums[0]; int best = nums[0]; for (size_t i = 1; i \u0026lt; nums.size(); ++i) { cur = std::max(nums[i], cur + nums[i]); best = std::max(best, cur); } return best; } int main() { std::vector\u0026lt;int\u0026gt; nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4}; std::cout \u0026lt;\u0026lt; maxSubArray(nums) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } package main import \u0026#34;fmt\u0026#34; func maxSubArray(nums []int) int { cur := nums[0] best := nums[0] for i := 1; i \u0026lt; len(nums); i++ { if cur+nums[i] \u0026gt; nums[i] { cur += nums[i] } else { cur = nums[i] } if cur \u0026gt; best { best = cur } } return best } func main() { nums := []int{-2, 1, -3, 4, -1, 2, 1, -5, 4} fmt.Println(maxSubArray(nums)) } fn max_subarray(nums: \u0026amp;[i32]) -\u0026gt; i32 { let mut cur = nums[0]; let mut best = nums[0]; for \u0026amp;x in \u0026amp;nums[1..] { let with_cur = cur + x; cur = if x \u0026gt; with_cur { x } else { with_cur }; if cur \u0026gt; best { best = cur; } } best } fn main() { let nums = vec![-2, 1, -3, 4, -1, 2, 1, -5, 4]; println!(\u0026#34;{}\u0026#34;, max_subarray(\u0026amp;nums)); } function maxSubArray(nums) { let cur = nums[0]; let best = nums[0]; for (let i = 1; i \u0026lt; nums.length; i++) { cur = Math.max(nums[i], cur + nums[i]); best = Math.max(best, cur); } return best; } console.log(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4])); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/hot100/53-maximum-subarray/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nMaximum Subarray is the classic 1D DP / greedy template. This ACERS guide explains Kadane\u0026rsquo;s idea, engineering use cases, and runnable multi-language solutions.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10–12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003eHot100\u003c/code\u003e, \u003ccode\u003edynamic programming\u003c/code\u003e, \u003ccode\u003egreedy\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Maximum Subarray, Kadane, dynamic programming, O(n), Hot100\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Kadane O(n) maximum subarray sum with engineering scenarios and multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven an integer array \u003ccode\u003enums\u003c/code\u003e, find the contiguous subarray with the largest sum (must contain at least one element) and return the sum.\u003c/p\u003e","title":"Hot100: Maximum Subarray (Kadane O(n) ACERS Guide)"},{"content":" Subtitle / Summary\nA classic event-spacing validation model. This ACERS guide explains the one-pass logic, engineering use cases, and runnable multi-language solutions.\nReading time: 10–12 min Tags: array, two pointers, event spacing SEO keywords: LeetCode 1437, event spacing, O(n) Meta description: One-pass validation for minimum spacing between 1s, with engineering use cases and multi-language code. A — Algorithm Problem Restatement Given an integer array nums and integer k, return true if every pair of 1s is at least k apart; otherwise return false.\nInput / Output Name Type Description nums int[] binary array with 0/1 k int required minimum spacing return bool whether the spacing rule holds Example 1 nums = [1,0,0,0,1,0,0,1], k = 2 output = true Example 2 nums = [1,0,1], k = 2 output = false Target Readers LeetCode learners building stable templates Engineers working on monitoring / risk control / behavior analytics Developers who need spacing or rate-limit validations Background / Motivation Many systems require events to be spaced apart: login failures, alarms, sensitive actions, API calls, etc.\nThis problem maps directly to event spacing validation.\nA one-pass, O(1)-memory solution is ideal for real-time systems.\nCore Concepts Event spacing: at least k zeros between two 1s Online validation: only the last event index is needed Boundary handling: initialize last = -k-1 to avoid special cases C — Concepts Key Observation Track the index of the last seen 1 (last) On each new 1, if i - last \u0026lt;= k → spacing violated Method Type One-pass scan Event spacing validation Greedy with a last pointer Formula Spacing requirement:\n(j - i - 1) \u0026gt;= k ⇔ (j - i) \u0026gt; k So the violation check is:\nif i - last \u0026lt;= k: return false Practical Steps Set last = -k - 1 (so the first 1 always passes) Scan from left to right When seeing 1, check distance to last If too close, return false Otherwise update last and continue Runnable Python example (save as k_length_apart.py):\ndef k_length_apart(nums, k): last = -k - 1 for i, x in enumerate(nums): if x == 1: if i - last \u0026lt;= k: return False last = i return True if __name__ == \u0026#34;__main__\u0026#34;: print(k_length_apart([1, 0, 0, 0, 1, 0, 0, 1], 2)) # True print(k_length_apart([1, 0, 1], 2)) # False E — Engineering Scenario 1: Risk control for login failures (Python) Background: repeated login failures too close together suggest brute force.\nWhy it fits: only the last failure index is required.\ndef check_login_spacing(events, k): last = -k - 1 for i, x in enumerate(events): if x != 1: continue if i - last \u0026lt;= k: return False last = i return True Scenario 2: Monitoring error density (Go) Background: errors shouldn’t occur too frequently in a time window.\nWhy it fits: O(1) memory, stream-friendly.\npackage main import \u0026#34;fmt\u0026#34; func okSpacing(log []int, k int) bool { last := -k - 1 for i, x := range log { if x == 1 { if i-last \u0026lt;= k { return false } last = i } } return true } func main() { fmt.Println(okSpacing([]int{1, 0, 0, 1}, 2)) } Scenario 3: Debounce in embedded systems (C) Background: sensor triggers must be spaced to avoid bouncing.\nWhy it fits: minimal state, fast checks.\n#include \u0026lt;stdio.h\u0026gt; int k_length_apart(const int *a, int n, int k) { int last = -k - 1; for (int i = 0; i \u0026lt; n; ++i) { if (a[i] == 1) { if (i - last \u0026lt;= k) return 0; last = i; } } return 1; } int main(void) { int a[] = {1,0,0,1}; printf(\u0026#34;%d\\n\u0026#34;, k_length_apart(a, 4, 2)); return 0; } Scenario 4: Frontend click throttling (JavaScript) Background: avoid bursts of high-value actions.\nWhy it fits: same spacing model on a click sequence.\nfunction okSpacing(events, k) { let last = -k - 1; for (let i = 0; i \u0026lt; events.length; i++) { if (events[i] === 1) { if (i - last \u0026lt;= k) return false; last = i; } } return true; } console.log(okSpacing([1, 0, 0, 1], 2)); R — Reflection Complexity Time: O(n) Space: O(1) Alternatives Method Idea Complexity Drawbacks Store all 1 indices then validate gaps O(n) extra memory Double loop compare all pairs O(n^2) too slow One-pass keep last index O(n) simplest Why This Is Best Minimal state Works in streaming systems Straightforward correctness Explanation \u0026amp; Rationale Keeping only the last 1 is enough because the constraint is local to consecutive 1s.\nInitializing last = -k-1 creates a “virtual 1” so the first real 1 always passes.\nAny time i - last \u0026lt;= k, the rule is violated.\nFAQs / Pitfalls Why i - last \u0026lt;= k?\nThe requirement (i - last - 1) \u0026gt;= k rearranges to i - last \u0026gt; k.\nIs k = 0 valid?\nYes. It means adjacent 1s are allowed.\nIs the array required to be 0/1?\nThe problem is binary, but the model can be generalized if you define “event” as 1.\nBest Practices Use last = -k-1 to avoid special cases Wrap the logic as a reusable spacing validator Combine with rate-limit checks if needed S — Summary Key Takeaways The task is an event-spacing validation problem Only the last event index is needed Initialization trick simplifies boundary handling One-pass scan gives O(n)/O(1) Useful in risk control, monitoring, and throttling Conclusion This is a simple but powerful template that maps directly to production systems.\nTurn it into a reusable utility and you’ll use it again and again.\nReferences \u0026amp; Further Reading LeetCode 1437. Check If All 1\u0026rsquo;s Are at Least Length K Places Away Rate limiting / debounce / throttle docs Event stream processing basics Meta Reading time: 10–12 min Tags: array, event spacing, monitoring, risk control SEO keywords: LeetCode 1437, event spacing, O(n) Meta description: One-pass minimum spacing validation with engineering use cases. Call to Action If you’re building monitoring or risk-control systems, add this “event spacing” template to your toolkit.\nShare your real-world adaptations in the comments.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) def k_length_apart(nums, k): last = -k - 1 for i, x in enumerate(nums): if x == 1: if i - last \u0026lt;= k: return False last = i return True if __name__ == \u0026#34;__main__\u0026#34;: print(k_length_apart([1, 0, 0, 0, 1, 0, 0, 1], 2)) #include \u0026lt;stdio.h\u0026gt; int k_length_apart(const int *a, int n, int k) { int last = -k - 1; for (int i = 0; i \u0026lt; n; ++i) { if (a[i] == 1) { if (i - last \u0026lt;= k) return 0; last = i; } } return 1; } int main(void) { int a[] = {1,0,0,1}; printf(\u0026#34;%d\\n\u0026#34;, k_length_apart(a, 4, 2)); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; bool kLengthApart(const std::vector\u0026lt;int\u0026gt;\u0026amp; nums, int k) { int last = -k - 1; for (int i = 0; i \u0026lt; (int)nums.size(); ++i) { if (nums[i] == 1) { if (i - last \u0026lt;= k) return false; last = i; } } return true; } int main() { std::cout \u0026lt;\u0026lt; std::boolalpha \u0026lt;\u0026lt; kLengthApart({1,0,0,1}, 2) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } package main import \u0026#34;fmt\u0026#34; func kLengthApart(nums []int, k int) bool { last := -k - 1 for i, x := range nums { if x == 1 { if i-last \u0026lt;= k { return false } last = i } } return true } func main() { fmt.Println(kLengthApart([]int{1, 0, 0, 1}, 2)) } fn k_length_apart(nums: \u0026amp;[i32], k: i32) -\u0026gt; bool { let mut last = -k - 1; for (i, \u0026amp;x) in nums.iter().enumerate() { if x == 1 { let i = i as i32; if i - last \u0026lt;= k { return false; } last = i; } } true } fn main() { let nums = vec![1, 0, 0, 1]; println!(\u0026#34;{}\u0026#34;, k_length_apart(\u0026amp;nums, 2)); } function kLengthApart(nums, k) { let last = -k - 1; for (let i = 0; i \u0026lt; nums.length; i++) { if (nums[i] === 1) { if (i - last \u0026lt;= k) return false; last = i; } } return true; } console.log(kLengthApart([1, 0, 0, 1], 2)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/1437-check-if-all-ones-are-at-least-k-places-away/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nA classic event-spacing validation model. This ACERS guide explains the one-pass logic, engineering use cases, and runnable multi-language solutions.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10–12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003earray\u003c/code\u003e, \u003ccode\u003etwo pointers\u003c/code\u003e, \u003ccode\u003eevent spacing\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: LeetCode 1437, event spacing, O(n)\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: One-pass validation for minimum spacing between 1s, with engineering use cases and multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven an integer array \u003ccode\u003enums\u003c/code\u003e and integer \u003ccode\u003ek\u003c/code\u003e, return \u003ccode\u003etrue\u003c/code\u003e if every pair of \u003ccode\u003e1\u003c/code\u003es is at least \u003ccode\u003ek\u003c/code\u003e apart; otherwise return \u003ccode\u003efalse\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 1437: Check If All 1's Are at Least K Apart (ACERS Guide)"},{"content":" Subtitle / Summary\nA classic bit-manipulation template: determine if a number is a power of two in O(1). This ACERS guide covers the core insight, practical uses, and runnable multi-language implementations.\nReading time: 8–12 min Tags: bit manipulation, binary, math SEO keywords: Power of Two, bit manipulation, binary, O(1), LeetCode 231 Meta description: O(1) power-of-two check using bit tricks, with engineering scenarios and multi-language code. A — Algorithm Problem Restatement Given an integer n, determine whether it is a power of two.\nReturn true if it is; otherwise, return false.\nInput / Output Name Type Description n int input integer return bool whether n is a power of two Example 1 Input: n = 1 Output: true Explanation: 2^0 = 1 Example 2 Input: n = 12 Output: false Explanation: 12 in binary is 1100, which has multiple 1s Target Readers LeetCode learners building a bit-manipulation toolkit Backend / systems engineers who need alignment or capacity checks Anyone who wants stable O(1) integer tests Background / Motivation Power-of-two checks show up everywhere: hash table capacities, memory alignment, sharding, FFT window sizes.\nLooping or using floating-point logs is slower and prone to corner-case bugs.\nThe bitwise method is fast, simple, and reliable.\nCore Concepts Binary form: a power of two has exactly one 1 in its binary representation Bitwise AND: n \u0026amp; (n - 1) clears the lowest set bit Positive-only: n must be greater than 0 C — Concepts Core Insight A power of two has a single 1 bit:\n1 = 0001 2 = 0010 4 = 0100 8 = 1000 If n has exactly one 1, then:\nn = 1000...000 n - 1 = 0111...111 n \u0026amp; (n - 1) = 0 Therefore:\nn is power of two ⟺ n \u0026gt; 0 and (n \u0026amp; (n - 1)) == 0 Method Type Bit manipulation (bit hacks) Constant-time numeric test Practical Steps Reject non-positive values: n \u0026lt;= 0 → false Compute (n \u0026amp; (n - 1)) If the result is 0, return true Runnable Python example (save as power_of_two.py and run python3 power_of_two.py):\ndef is_power_of_two(n: int) -\u0026gt; bool: return n \u0026gt; 0 and (n \u0026amp; (n - 1)) == 0 if __name__ == \u0026#34;__main__\u0026#34;: print(is_power_of_two(1)) # True print(is_power_of_two(12)) # False E — Engineering Scenario 1: Data analysis / signal processing window size (Python) Background: FFT and some convolution routines require power-of-two sizes.\nWhy it fits: one-line validation avoids runtime failures.\ndef is_power_of_two(n: int) -\u0026gt; bool: return n \u0026gt; 0 and (n \u0026amp; (n - 1)) == 0 window = 1024 if not is_power_of_two(window): raise ValueError(\u0026#34;window must be power of two\u0026#34;) print(\u0026#34;ok\u0026#34;) Scenario 2: Memory alignment in allocators (C) Background: memory allocators often align blocks to powers of two.\nWhy it fits: the check is constant-time and branch-light.\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; int is_pow2(uint32_t x) { return x \u0026gt; 0 \u0026amp;\u0026amp; (x \u0026amp; (x - 1)) == 0; } int main(void) { printf(\u0026#34;%d\\n\u0026#34;, is_pow2(64)); printf(\u0026#34;%d\\n\u0026#34;, is_pow2(48)); return 0; } Scenario 3: Backend sharding / worker count validation (Go) Background: shard counts are often powers of two to enable idx \u0026amp; (n-1) routing.\nWhy it fits: avoids modulo cost and keeps mapping uniform.\npackage main import \u0026#34;fmt\u0026#34; func isPowerOfTwo(n int) bool { return n \u0026gt; 0 \u0026amp;\u0026amp; (n\u0026amp;(n-1)) == 0 } func main() { shards := 16 if !isPowerOfTwo(shards) { panic(\u0026#34;shards must be power of two\u0026#34;) } fmt.Println(\u0026#34;ok\u0026#34;) } R — Reflection Complexity Time: O(1) Space: O(1) Alternative Approaches Method Idea Complexity Drawbacks Divide by 2 loop keep dividing while even O(log n) slower, more code Popcount == 1 count 1-bits O(1) library/intrinsic dependency log2 check check integer log varies floating-point precision Bit trick n \u0026amp; (n - 1) O(1) simplest and robust Why This Is Best in Practice No loops, no divisions, no floating point Stable across languages and integer sizes Matches real-world systems requirements Explanation \u0026amp; Rationale A power of two has a single set bit. Subtracting 1 flips that bit to 0 and turns all lower bits to 1.\nSo only a number with one set bit will satisfy n \u0026amp; (n - 1) == 0.\nWe must additionally check n \u0026gt; 0 to rule out 0 and negatives.\nFAQs / Pitfalls Is n = 0 a power of two?\nNo. Always guard with n \u0026gt; 0.\nWhat about negative numbers?\nThey are not powers of two in this context. The sign bit in two\u0026rsquo;s complement breaks the single-bit property.\nIs log2(n) safe?\nNot reliably—floating-point precision can misclassify large values.\nBest Practices Always include the n \u0026gt; 0 check Encapsulate the logic into a small utility function If you need the nearest power of two, build a separate helper instead of overloading this function S — Summary Key Takeaways A power of two has exactly one 1 bit n \u0026amp; (n - 1) removes the lowest set bit n \u0026gt; 0 is required to exclude 0 and negatives The bit trick is O(1), concise, and reliable Widely used in hashing, alignment, and sharding Conclusion This is a core bit-manipulation template worth memorizing. It shows up repeatedly in systems code and performance-critical logic.\nReferences \u0026amp; Further Reading LeetCode 231. Power of Two LeetCode 191. Number of 1 Bits LeetCode 342. Power of Four Hacker\u0026rsquo;s Delight (bit tricks) Computer Systems: A Programmer\u0026rsquo;s Perspective (binary operations) Meta Reading time: 8–12 min Tags: bit manipulation, binary, math, LeetCode 231 SEO keywords: Power of Two, bit manipulation, binary, O(1) Meta description: O(1) power-of-two check with bit tricks and engineering applications. Call to Action Try converting a few related problems (power of four, count of 1 bits) into your own ACERS templates.\nShare your variants or engineering use cases in the comments.\nMulti-language Implementations (Python / C / C++ / Go / Rust / JS) def is_power_of_two(n: int) -\u0026gt; bool: return n \u0026gt; 0 and (n \u0026amp; (n - 1)) == 0 if __name__ == \u0026#34;__main__\u0026#34;: print(is_power_of_two(1)) # True print(is_power_of_two(12)) # False #include \u0026lt;stdio.h\u0026gt; int is_power_of_two(int n) { return n \u0026gt; 0 \u0026amp;\u0026amp; (n \u0026amp; (n - 1)) == 0; } int main(void) { printf(\u0026#34;%d\\n\u0026#34;, is_power_of_two(1)); printf(\u0026#34;%d\\n\u0026#34;, is_power_of_two(12)); return 0; } #include \u0026lt;iostream\u0026gt; bool isPowerOfTwo(int n) { return n \u0026gt; 0 \u0026amp;\u0026amp; (n \u0026amp; (n - 1)) == 0; } int main() { std::cout \u0026lt;\u0026lt; std::boolalpha \u0026lt;\u0026lt; isPowerOfTwo(1) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; std::cout \u0026lt;\u0026lt; std::boolalpha \u0026lt;\u0026lt; isPowerOfTwo(12) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } package main import \u0026#34;fmt\u0026#34; func isPowerOfTwo(n int) bool { return n \u0026gt; 0 \u0026amp;\u0026amp; (n\u0026amp;(n-1)) == 0 } func main() { fmt.Println(isPowerOfTwo(1)) fmt.Println(isPowerOfTwo(12)) } fn is_power_of_two(n: i32) -\u0026gt; bool { n \u0026gt; 0 \u0026amp;\u0026amp; (n \u0026amp; (n - 1)) == 0 } fn main() { println!(\u0026#34;{}\u0026#34;, is_power_of_two(1)); println!(\u0026#34;{}\u0026#34;, is_power_of_two(12)); } function isPowerOfTwo(n) { return n \u0026gt; 0 \u0026amp;\u0026amp; (n \u0026amp; (n - 1)) === 0; } console.log(isPowerOfTwo(1)); console.log(isPowerOfTwo(12)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/231-power-of-two/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nA classic bit-manipulation template: determine if a number is a power of two in O(1). This ACERS guide covers the core insight, practical uses, and runnable multi-language implementations.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 8–12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003ebit manipulation\u003c/code\u003e, \u003ccode\u003ebinary\u003c/code\u003e, \u003ccode\u003emath\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Power of Two, bit manipulation, binary, O(1), LeetCode 231\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: O(1) power-of-two check using bit tricks, with engineering scenarios and multi-language code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven an integer \u003ccode\u003en\u003c/code\u003e, determine whether it is a power of two.\u003cbr\u003e\nReturn \u003ccode\u003etrue\u003c/code\u003e if it is; otherwise, return \u003ccode\u003efalse\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 231: Power of Two (Bit Trick O(1) ACERS Guide)"},{"content":" Subtitle / Summary\nA standard fixed-window counting problem. This ACERS guide explains the sliding-window model, engineering use cases, and runnable multi-language solutions.\nReading time: 10–12 min Tags: sliding window, string, fixed window SEO keywords: Maximum Number of Vowels, Sliding Window, Fixed Window Meta description: Fixed-window sliding count for maximum vowels with engineering applications. A — Algorithm Problem Restatement Given a string s and an integer k, return the maximum number of vowels in any substring of length k.\nInput / Output Name Type Description s string lowercase letters k int window length return int maximum vowels in any length-k window Example 1 s = \u0026#34;abciiidef\u0026#34;, k = 3 output = 3 Example 2 s = \u0026#34;aeiou\u0026#34;, k = 2 output = 2 Target Readers LeetCode learners who want stable templates Engineers working on windowed metrics Anyone building real-time counters Background / Motivation Many engineering tasks ask: “What is the maximum count in any fixed-length window?”\nRecomputing every window is O(nk). Sliding window updates in O(1) per step, giving O(n).\nCore Concepts Fixed sliding window: length k, move right one step each time Incremental update: add incoming item, remove outgoing item Condition counting: count only items matching a predicate C — Concepts Method Type Fixed sliding window + predicate counting.\nKey Model Maintain cnt for the current window:\ncnt = cnt + isVowel(s[i]) - isVowel(s[i-k]) Each step updates in O(1).\nPractical Steps Count vowels in the first window Set ans = cnt Slide: add s[i], remove s[i-k] Update ans with max Return ans Runnable Example (Python) def max_vowels(s: str, k: int) -\u0026gt; int: vowels = set(\u0026#34;aeiou\u0026#34;) cnt = sum(1 for c in s[:k] if c in vowels) ans = cnt for i in range(k, len(s)): if s[i] in vowels: cnt += 1 if s[i - k] in vowels: cnt -= 1 if cnt \u0026gt; ans: ans = cnt return ans if __name__ == \u0026#34;__main__\u0026#34;: print(max_vowels(\u0026#34;abciiidef\u0026#34;, 3)) Run:\npython3 demo.py Explanation \u0026amp; Trade-offs Because the window size is fixed, each move only changes two characters.\nThis makes O(1) updates possible and avoids O(nk) recomputation.\nE — Engineering Scenario 1: Error Peak per Window (Go) Background: Compute the maximum errors in any k-minute window.\nWhy: Same fixed-window counting model.\npackage main import \u0026#34;fmt\u0026#34; func maxErrors(flags []int, k int) int { cnt, ans := 0, 0 for i, x := range flags { if x == 1 { cnt++ } if i \u0026gt;= k \u0026amp;\u0026amp; flags[i-k] == 1 { cnt-- } if i \u0026gt;= k-1 \u0026amp;\u0026amp; cnt \u0026gt; ans { ans = cnt } } return ans } func main() { fmt.Println(maxErrors([]int{0, 1, 1, 0, 1, 0, 1}, 3)) } Scenario 2: Text Feature Peak (Python) Background: Count maximum keyword occurrences in any k-length window.\nWhy: Predicate can be replaced with any condition.\ndef max_keyword(text, k, keywords): s = list(text) cnt = sum(1 for c in s[:k] if c in keywords) ans = cnt for i in range(k, len(s)): if s[i] in keywords: cnt += 1 if s[i - k] in keywords: cnt -= 1 ans = max(ans, cnt) return ans print(max_keyword(\u0026#34;happyxxsadxxhappy\u0026#34;, 5, set(\u0026#34;hs\u0026#34;))) Scenario 3: Frontend Live Highlight (JavaScript) Background: Highlight max sensitive chars in the latest k input.\nWhy: O(1) update per keystroke.\nfunction maxFlag(chars, k, flagSet) { let cnt = 0; for (let i = 0; i \u0026lt; Math.min(k, chars.length); i += 1) { if (flagSet.has(chars[i])) cnt += 1; } let ans = cnt; for (let i = k; i \u0026lt; chars.length; i += 1) { if (flagSet.has(chars[i])) cnt += 1; if (flagSet.has(chars[i - k])) cnt -= 1; ans = Math.max(ans, cnt); } return ans; } console.log(maxFlag(\u0026#34;abciiidef\u0026#34;, 3, new Set([\u0026#34;a\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;i\u0026#34;, \u0026#34;o\u0026#34;, \u0026#34;u\u0026#34;]))); R — Reflection Complexity Time: O(n) Space: O(1) Alternatives Method Time Space Notes Brute force O(nk) O(1) Too slow for large data Prefix sum O(n) O(n) Extra memory Sliding window O(n) O(1) Best balance Common Pitfalls Updating result before the window is formed Forgetting to remove the outgoing element Inconsistent vowel predicate Why this is optimal You must inspect each character at least once.\nSliding window achieves that lower bound with O(1) updates.\nS — Summary Fixed-window counting is a reusable template Sliding window reduces O(nk) to O(n) The predicate can represent any engineering condition Ideal for streaming or online stats Further Reading LeetCode 1456 Sliding Window Pattern Prefix sum vs window Conclusion This problem is less about vowels and more about a fixed-window counting model.\nOnce memorized, it translates directly to monitoring and analytics.\nReferences https://leetcode.com/problems/maximum-number-of-vowels-in-a-substring-of-given-length/ https://en.wikipedia.org/wiki/Sliding_window_protocol Metadata Reading time: 10–12 min Tags: sliding window, string, fixed window SEO: Maximum Number of Vowels, Sliding Window Meta description: O(n) fixed-window max vowels with engineering use cases. Call to Action Try rewriting one of your monitoring metrics as a fixed-window count.\nIf you want, share your use case and I can help translate it.\nMulti-language Reference (Python / C / C++ / Go / Rust / JS) def max_vowels(s: str, k: int) -\u0026gt; int: vowels = set(\u0026#34;aeiou\u0026#34;) cnt = sum(1 for c in s[:k] if c in vowels) ans = cnt for i in range(k, len(s)): if s[i] in vowels: cnt += 1 if s[i - k] in vowels: cnt -= 1 ans = max(ans, cnt) return ans if __name__ == \u0026#34;__main__\u0026#34;: print(max_vowels(\u0026#34;abciiidef\u0026#34;, 3)) #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; static int is_vowel(char c) { return c == \u0026#39;a\u0026#39; || c == \u0026#39;e\u0026#39; || c == \u0026#39;i\u0026#39; || c == \u0026#39;o\u0026#39; || c == \u0026#39;u\u0026#39;; } int max_vowels(const char *s, int k) { int cnt = 0; int ans = 0; int n = (int)strlen(s); for (int i = 0; i \u0026lt; n; ++i) { if (is_vowel(s[i])) cnt++; if (i \u0026gt;= k \u0026amp;\u0026amp; is_vowel(s[i - k])) cnt--; if (i \u0026gt;= k - 1 \u0026amp;\u0026amp; cnt \u0026gt; ans) ans = cnt; } return ans; } int main(void) { printf(\u0026#34;%d\\n\u0026#34;, max_vowels(\u0026#34;abciiidef\u0026#34;, 3)); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;string\u0026gt; static bool isVowel(char c) { return c == \u0026#39;a\u0026#39; || c == \u0026#39;e\u0026#39; || c == \u0026#39;i\u0026#39; || c == \u0026#39;o\u0026#39; || c == \u0026#39;u\u0026#39;; } int maxVowels(const std::string \u0026amp;s, int k) { int cnt = 0, ans = 0; for (int i = 0; i \u0026lt; (int)s.size(); ++i) { if (isVowel(s[i])) cnt++; if (i \u0026gt;= k \u0026amp;\u0026amp; isVowel(s[i - k])) cnt--; if (i \u0026gt;= k - 1 \u0026amp;\u0026amp; cnt \u0026gt; ans) ans = cnt; } return ans; } int main() { std::cout \u0026lt;\u0026lt; maxVowels(\u0026#34;abciiidef\u0026#34;, 3) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } package main import \u0026#34;fmt\u0026#34; func isVowel(c byte) bool { return c == \u0026#39;a\u0026#39; || c == \u0026#39;e\u0026#39; || c == \u0026#39;i\u0026#39; || c == \u0026#39;o\u0026#39; || c == \u0026#39;u\u0026#39; } func maxVowels(s string, k int) int { cnt, ans := 0, 0 for i := 0; i \u0026lt; len(s); i++ { if isVowel(s[i]) { cnt++ } if i \u0026gt;= k \u0026amp;\u0026amp; isVowel(s[i-k]) { cnt-- } if i \u0026gt;= k-1 \u0026amp;\u0026amp; cnt \u0026gt; ans { ans = cnt } } return ans } func main() { fmt.Println(maxVowels(\u0026#34;abciiidef\u0026#34;, 3)) } fn is_vowel(c: u8) -\u0026gt; bool { c == b\u0026#39;a\u0026#39; || c == b\u0026#39;e\u0026#39; || c == b\u0026#39;i\u0026#39; || c == b\u0026#39;o\u0026#39; || c == b\u0026#39;u\u0026#39; } fn max_vowels(s: \u0026amp;str, k: usize) -\u0026gt; i32 { let bytes = s.as_bytes(); let mut cnt: i32 = 0; let mut ans: i32 = 0; for i in 0..bytes.len() { if is_vowel(bytes[i]) { cnt += 1; } if i \u0026gt;= k \u0026amp;\u0026amp; is_vowel(bytes[i - k]) { cnt -= 1; } if i + 1 \u0026gt;= k \u0026amp;\u0026amp; cnt \u0026gt; ans { ans = cnt; } } ans } fn main() { println!(\u0026#34;{}\u0026#34;, max_vowels(\u0026#34;abciiidef\u0026#34;, 3)); } function maxVowels(s, k) { const isVowel = (c) =\u0026gt; \u0026#34;aeiou\u0026#34;.includes(c); let cnt = 0; let ans = 0; for (let i = 0; i \u0026lt; s.length; i += 1) { if (isVowel(s[i])) cnt += 1; if (i \u0026gt;= k \u0026amp;\u0026amp; isVowel(s[i - k])) cnt -= 1; if (i \u0026gt;= k - 1 \u0026amp;\u0026amp; cnt \u0026gt; ans) ans = cnt; } return ans; } console.log(maxVowels(\u0026#34;abciiidef\u0026#34;, 3)); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/1456-maximum-number-of-vowels-in-a-substring-of-given-length/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Summary\u003c/strong\u003e\u003cbr\u003e\nA standard fixed-window counting problem. This ACERS guide explains the sliding-window model, engineering use cases, and runnable multi-language solutions.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 10–12 min\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003esliding window\u003c/code\u003e, \u003ccode\u003estring\u003c/code\u003e, \u003ccode\u003efixed window\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Maximum Number of Vowels, Sliding Window, Fixed Window\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Fixed-window sliding count for maximum vowels with engineering applications.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"a--algorithm\"\u003eA — Algorithm\u003c/h2\u003e\n\u003ch3 id=\"problem-restatement\"\u003eProblem Restatement\u003c/h3\u003e\n\u003cp\u003eGiven a string \u003ccode\u003es\u003c/code\u003e and an integer \u003ccode\u003ek\u003c/code\u003e, return the maximum number of vowels in any substring of length \u003ccode\u003ek\u003c/code\u003e.\u003c/p\u003e","title":"LeetCode 1456: Maximum Number of Vowels in a Substring of Given Length (ACERS Guide)"},{"content":"Problem Requirement You are given an integer array nums and an integer k. A window of exactly k contiguous elements starts at the left edge of nums and moves one position to the right at a time. Return the maximum value from every window, in the same left-to-right order as the windows.\nThe elements in a window must be contiguous. Their original order and positions do not change, and adjacent windows can overlap. Values do not need to be unique and may be negative.\nLeetCode Contract LeetCode calls maxSlidingWindow(nums, k). The method receives the integer array and the valid window size, then returns an integer array containing one maximum for each complete window. The input is guaranteed to satisfy the constraints below.\nExamples Example 1:\nInput: nums = [1,3,-1,-3,5,3,6,7], k = 3 Output: [3,3,5,5,6,7] Example 2:\nInput: nums = [1], k = 1 Output: [1] Constraints 1 \u0026lt;= nums.length \u0026lt;= 10^5 -10^4 \u0026lt;= nums[i] \u0026lt;= 10^4 1 \u0026lt;= k \u0026lt;= nums.length Step 1: When Is a Window Complete? For Example 1, which indices form the window when its right edge reaches index 2? What changes when that edge reaches index 3?\nThe current baseline says only that a size-k window moves right. This breaks when we try to enumerate the outputs: it does not yet specify the window\u0026rsquo;s exact left edge or the first position at which a full window exists.\nUse zero-based indices and call the current ending index right. If the window begins at left and contains exactly k elements, then:\nright - left + 1 = k left = right - k + 1 The window is complete only when left \u0026gt;= 0, which is equivalent to:\nright \u0026gt;= k - 1 Before that condition is true, there are fewer than k elements and no output position exists. Once it is true, the window is nums[left:right + 1]. During a full left-to-right pass, that window contributes output position left.\nCheck this boundary rule against the entire first example, where k = 3:\nright left = right - k + 1 Complete? Window indices Window values Output position Maximum 0 -2 No - - - - 1 -1 No - - - - 2 0 Yes 0..2 [1,3,-1] 0 3 3 1 Yes 1..3 [3,-1,-3] 1 3 4 2 Yes 2..4 [-1,-3,5] 2 5 5 3 Yes 3..5 [-3,5,3] 3 5 6 4 Yes 4..6 [5,3,6] 4 6 7 5 Yes 5..7 [3,6,7] 5 7 The first output appears at right = 2 = k - 1. For an array of length n, the legal left edges are 0 through n - k, so the output count is:\n(n - k) - 0 + 1 = n - k + 1 Here, n = 8 and k = 3, so there are 8 - 3 + 1 = 6 outputs. That matches both the six complete rows in the table and the six values in the expected output.\nCheckpoint 1 Freeze: The reader can identify every complete window and its output position.\nIt still lacks: No runnable algorithm calculates all maxima.\nStep 2: Scan Every Complete Window Step 1 can identify every complete window, but it still leaves the maxima in the example table as manual results. For instance, when right = 2, the boundary rule gives left = 0 and the slice nums[0:3], but no executable rule computes 3 or places it in the returned list.\nThe current baseline is the exact boundary model from Step 1. This breaks when we need to produce the full answer: knowing every legal left and right does not calculate or collect any maximum.\nMake one change: turn that boundary model into a complete scan function. For each legal left, derive the inclusive right, scan exactly nums[left:right + 1], and append its maximum to answer.\ndef max_sliding_window_scan(nums: list[int], k: int) -\u0026gt; list[int]: answer = [] for left in range(len(nums) - k + 1): right = left + k - 1 answer.append(max(nums[left:right + 1])) return answer # Official examples assert max_sliding_window_scan([1, 3, -1, -3, 5, 3, 6, 7], 3) == [ 3, 3, 5, 5, 6, 7 ] assert max_sliding_window_scan([1], 1) == [1] # Boundary cases assert max_sliding_window_scan([4, -2, 7], 1) == [4, -2, 7] assert max_sliding_window_scan([4, -2, 7], 3) == [7] assert max_sliding_window_scan([9, 7, 5, 3, 1], 3) == [9, 7, 5] Check this change by running the block. The assertions cover both official examples, k = 1, k = n, and decreasing input. They finish without output when every returned list is correct.\nThere are n - k + 1 windows. Each slice contains k values, and both building that slice and finding its maximum take O(k) time. The total time is therefore O((n - k + 1)k), commonly written as O(nk). The returned list holds n - k + 1 values, and the temporary slice uses O(k) extra space.\nCheckpoint 2 Freeze: The reader can compute every window maximum correctly with the complete O(nk) scan baseline.\nIt still lacks: Overlapping windows repeatedly scan up to k values from scratch instead of reusing work from the previous window.\nStep 3: Which Stored Position Just Expired? The scan baseline is correct, but it rebuilds a slice for every window and then discards that window\u0026rsquo;s membership. To carry the membership forward, we must know exactly which old element leaves when left moves right.\nThis becomes ambiguous if we store only values. For nums = [2,2,1] and k = 2, the first complete window contains a 2 from index 0 and another 2 from index 1. When the next window starts at index 1, index 0 leaves but index 1 stays. The value 2 alone does not identify those two occurrences.\nThe current baseline is max_sliding_window_scan, which knows left and right but keeps no state between windows. This breaks when we try to update membership from one window to the next: expiration is about a position, and the baseline has not retained any position to remove.\nMake one change: use an ordinary collections.deque named candidates and store indices in arrival order. Append each right index. Once left is known, remove front indices while candidates[0] \u0026lt; left, because those positions are outside the current window. An index equal to left must stay.\nThis version deliberately finds each maximum by scanning the values at all current candidate indices:\nfrom collections import deque def max_sliding_window_candidates(nums: list[int], k: int) -\u0026gt; list[int]: answer = [] candidates = deque() for right in range(len(nums)): candidates.append(right) left = right - k + 1 while candidates and candidates[0] \u0026lt; left: candidates.popleft() if left \u0026gt;= 0: answer.append(max(nums[index] for index in candidates)) return answer cases = [ ([1, 3, -1, -3], 3), ([2, 2, 1], 2), ([1, 3, -1, -3, 5, 3, 6, 7], 3), ([1], 1), ([4, -2, 7], 1), ([4, -2, 7], 3), ([9, 7, 5, 3, 1], 3), ] for nums, k in cases: assert max_sliding_window_candidates(nums, k) == max_sliding_window_scan( nums, k ) Check expiration with nums = [1,3,-1,-3] and k = 3:\nright left After append Expired indices candidates after eviction Current values Output 0 -2 [0] - [0] [1] - 1 -1 [0,1] - [0,1] [1,3] - 2 0 [0,1,2] - [0,1,2] [1,3,-1] 3 3 1 [0,1,2,3] 0 [1,2,3] [3,-1,-3] 3 At right = 3, the new left edge is 1. The test 0 \u0026lt; 1 removes index 0 from the front, while indices 1, 2, and 3 remain as the exact current window.\nNow check duplicate identity with nums = [2,2,1] and k = 2:\nright left After append Expired indices candidates after eviction Current values Output 0 -1 [0] - [0] [2] - 1 0 [0,1] - [0,1] [2,2] 2 2 1 [0,1,2] 0 [1,2] [2,1] 2 The two equal values remain distinguishable at indices 0 and 1. When the window moves, the expiration rule removes index 0 specifically and keeps the equal value at index 1. The executable checks compare this membership version with the complete Task 2 scan for both traces, the official input, and the earlier boundary cases.\nEach index is appended once and removed from the front at most once, with each operation taking constant time. However, every complete window still scans all k current candidate values to call max, so the total time is O((n - k + 1)k + n), commonly written as O(nk). The deque uses O(k) auxiliary space, apart from the returned answer.\nCheckpoint 3 Freeze: The reader can maintain exactly the indices in the current window and distinguish equal values by position.\nIt still lacks: Finding each maximum still scans all current candidates, which takes O(k) time per complete window.\nStep 4: Which Candidates Can Never Become Maximum? The ordinary membership deque is correct, but a complete window can still contain k candidate indices. Calling max() across those candidates repeats an O(k) scan for every output.\nConsider an earlier candidate index i and the new index right, where:\ni \u0026lt; right nums[i] \u0026lt;= nums[right] The later value makes the earlier value permanently useless:\nAs long as a future window still contains i, it also contains right because right is later. nums[right] is at least as large as nums[i], so i cannot provide a better maximum. Index i expires before right, so there is no later window in which i remains after right has left. Call i a dominated candidate. The current baseline keeps dominated indices and scans them again. This breaks the attempt to make maximum lookup constant time.\nThe deque already stores indices in increasing arrival order, so compare the new value with the tail. In the previous version, replace the per-window max() scan by removing every smaller-or-equal tail before appending right:\nwhile candidates and nums[candidates[-1]] \u0026lt;= nums[right]: candidates.pop() If the tail is removed, compare the same new value with the next tail. When the loop stops, either the deque is empty or its tail value is greater than nums[right]. Appending right therefore leaves candidate values in strictly decreasing order from front to back. The maximum is now nums[candidates[0]].\nThis is the first point at which the deque is monotonic: its indices increase, while their values strictly decrease.\nFinal LeetCode Implementation Connect the new rule to the membership version. For each right, expire old front indices, remove dominated tail indices, append right, and emit the front value once the first window is complete.\nfrom collections import deque class Solution: def maxSlidingWindow(self, nums: list[int], k: int) -\u0026gt; list[int]: answer = [] candidates = deque() for right in range(len(nums)): left = right - k + 1 while candidates and candidates[0] \u0026lt; left: candidates.popleft() while candidates and nums[candidates[-1]] \u0026lt;= nums[right]: candidates.pop() candidates.append(right) if right \u0026gt;= k - 1: answer.append(nums[candidates[0]]) return answer Official-Example Trace For nums = [1,3,-1,-3,5,3,6,7] and k = 3, each deque entry below is written as index:value. Tail removals are listed in the order they occur.\nright left Expired front Removed tails Deque after append Output 0 -2 - - [0:1] - 1 -1 - 0:1 [1:3] - 2 0 - - [1:3, 2:-1] 3 3 1 - - [1:3, 2:-1, 3:-3] 3 4 2 1:3 3:-3, 2:-1 [4:5] 5 5 3 - - [4:5, 5:3] 5 6 4 - 5:3, 4:5 [6:6] 6 7 5 - 6:6 [7:7] 7 The outputs are [3,3,5,5,6,7]. At right = 4, front expiration first removes index 1, then the new value 5 removes both smaller tail values. This row exercises both removal directions in one iteration.\nEqual-Value Policy The \u0026lt;= comparison removes an earlier equal value and keeps the later equal value. For [2,2,1] with k = 2, processing index 1 removes index 0 before appending index 1. Both values could produce the same maximum, but index 1 expires later, so it is never worse for any current or future window.\nUsing \u0026lt; instead would also produce correct maxima, but it would retain equal values and make the deque values non-increasing rather than strictly decreasing. This implementation deliberately uses \u0026lt;= so one representative of an equal maximum remains: the latest one.\nLoop Invariant and Correctness After each right is processed and appended:\nCandidate indices are strictly increasing and no candidate is left of the current left boundary. Candidate values are strictly decreasing from front to back. Every processed, unexpired index omitted from the deque is dominated by a later deque index with a greater-or-equal value. The expiration loop preserves the first property because expired indices can only occur at the front. The tail loop preserves the third property because each removed index is replaced by the later right, which has a greater-or-equal value. Popping continues until appending right preserves the second property.\nFor a complete window, the front candidate is at least as large as every other stored candidate by the second property. Every omitted window index is no larger than a later stored candidate by the third property. Therefore the front value is the maximum of the entire current window, so every appended output is correct.\nComplexity Each of the n indices is appended exactly once. An index can then be removed once: either from the tail when a later value dominates it or from the front when it expires. Across the whole run, the two while loops therefore perform at most n successful removals, not k removals per window. The outer loop and all failed stopping comparisons add only constant work per index, giving amortized O(n) time.\nAfter expiration, the deque contains only indices from the current window, so it holds at most k indices. Auxiliary space is O(k), excluding the returned array of n - k + 1 maxima.\nExecutable Checks Fixed assertions cover the official examples, both window-size extremes, decreasing input, repeated tail removal, and equal values:\nsolution = Solution() assert solution.maxSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3) == [ 3, 3, 5, 5, 6, 7 ] assert solution.maxSlidingWindow([1], 1) == [1] assert solution.maxSlidingWindow([4, -2, 7], 1) == [4, -2, 7] assert solution.maxSlidingWindow([4, -2, 7], 3) == [7] assert solution.maxSlidingWindow([9, 7, 5, 3, 1], 3) == [9, 7, 5] assert solution.maxSlidingWindow([1, 2, 3, 4, 5], 3) == [3, 4, 5] assert solution.maxSlidingWindow([2, 2, 1], 2) == [2, 2] The optimized method can also be checked against the Task 2 scan on generated valid inputs. A fixed seed makes a failure reproducible, and the copy check verifies that the method does not mutate nums:\nfrom random import Random rng = Random(239) for _ in range(2000): n = rng.randint(1, 30) nums = [rng.randint(-20, 20) for _ in range(n)] k = rng.randint(1, n) original = nums.copy() actual = solution.maxSlidingWindow(nums, k) assert nums == original assert actual == max_sliding_window_scan(nums, k) Common Mistakes Storing values instead of indices makes front expiration ambiguous when values repeat. Expiring candidates[0] \u0026lt;= left incorrectly removes the index at the current left boundary. Only indices \u0026lt; left are outside the window. Appending right before the tail loop compares the new index with itself. Remove dominated old tails first. Using \u0026lt; in the tail loop while claiming values are strictly decreasing leaves equal values in the deque. That policy is correct, but the claimed invariant is not. Calling max() over the deque after maintaining decreasing order restores the O(k) lookup that this step removed. Read the maximum from the front. Derivation Summary left = right - k + 1 identifies the current window, and output begins at right = k - 1. Scanning every complete window gives a correct O(nk) baseline. Storing indices in a deque makes expiration exact, even for equal values. A later greater-or-equal value permanently dominates an earlier one, so dominated tails can be removed. The remaining values are strictly decreasing, making the unexpired front the window maximum in amortized O(n) time and O(k) auxiliary space. Checkpoint 4 Freeze: The reader can solve LeetCode 239 through the required maxSlidingWindow contract in amortized O(n) time and O(k) auxiliary space.\nIt still lacks: Nothing required by the problem. Only independent full-draft review remains.\n","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/239-sliding-window-maximum/","summary":"\u003ch2 id=\"problem-requirement\"\u003eProblem Requirement\u003c/h2\u003e\n\u003cp\u003eYou are given an integer array \u003ccode\u003enums\u003c/code\u003e and an integer \u003ccode\u003ek\u003c/code\u003e. A window of exactly\n\u003ccode\u003ek\u003c/code\u003e contiguous elements starts at the left edge of \u003ccode\u003enums\u003c/code\u003e and moves one\nposition to the right at a time. Return the maximum value from every window, in\nthe same left-to-right order as the windows.\u003c/p\u003e\n\u003cp\u003eThe elements in a window must be contiguous. Their original order and\npositions do not change, and adjacent windows can overlap. Values do not need\nto be unique and may be negative.\u003c/p\u003e","title":"LeetCode 239: Sliding Window Maximum"},{"content":"What Is size_t? Why C++ Loops Prefer size_t Over int Subtitle / Abstract When you iterate containers with a for loop, size_t is often safer and closer to the intended meaning than int. This post uses the ACERS structure to explain what size_t is, why it is used, the common pitfalls, and practical patterns for production C++.\nMeta Reading time: 8-10 minutes Tags: C++, size_t, type system, loops, STL SEO keywords: size_t usage, size_t vs int, C++ loop initialization, size_t underflow Meta description: Explain size_t and why loops often use it, with safe patterns and engineering scenarios. Target readers C++ beginners who are new to size_t, sizeof, and container size() return types Mid-level engineers who have seen -Wsign-compare warnings or unsigned underflow bugs Engineers writing cross-platform or high-performance C++ Background / Motivation In C++ code, you often see loops like:\nfor (size_t i = 0; i \u0026lt; vec.size(); ++i) { ... } Common questions:\nWhy not use the more \u0026ldquo;obvious\u0026rdquo; int? What exactly is size_t, and why is it unsigned? Where are the pitfalls? This article answers those questions.\nA - Algorithm (Problem and Approach) The question Why use size_t for loop indices and sizes instead of int in C++?\nThis is fundamentally about type semantics and API consistency:\nsize_t is the standard type for object sizes and indices int is a signed counter with different semantics Basic example 1: container size and index #include \u0026lt;vector\u0026gt; std::vector\u0026lt;int\u0026gt; v{1, 2, 3}; for (std::size_t i = 0; i \u0026lt; v.size(); ++i) { // i matches v.size() type; no signed/unsigned warning } Basic example 2: unsigned underflow #include \u0026lt;cstddef\u0026gt; std::size_t n = 0; std::size_t x = n - 1; // not -1, but a very large positive number Concept sketch:\nsize_t (unsigned) : 0 ---------------------\u0026gt; SIZE_MAX int (signed) : -2^(N-1) ---- 0 ---- 2^(N-1)-1 Key point: size_t cannot represent negative numbers; subtraction can wrap to a huge value.\nC - Concepts (Core Ideas) What is size_t? size_t is an unsigned integer type that can represent the size of any object. sizeof returns size_t. On 64-bit systems it is typically 64-bit; on 32-bit systems it is typically 32-bit. #include \u0026lt;cstddef\u0026gt; std::size_t n = sizeof(int); What category does this belong to? Type semantics: use types to express \u0026ldquo;size/index\u0026rdquo; API consistency: matches container size() signatures Portability: guaranteed to represent any object size Key model sizeof(T) -\u0026gt; size_t Range: 0 \u0026lt;= size_t \u0026lt;= SIZE_MAX SIZE_MAX = 2^N - 1 (N is the bit width) Practical steps (with commands) Include the header: #include \u0026lt;cstddef\u0026gt; for std::size_t. Align with API: use std::size_t or container::size_type for sizes/indices. Cache bounds: store n = v.size() to avoid repeated calls and unsigned pitfalls. Avoid unsigned underflow: do not write v.size() - 1 on possibly empty containers. Reverse iteration: use for (size_t i = n; i-- \u0026gt; 0;) or std::ssize. Enable warnings: -Wsign-compare to surface issues early. # g++ example g++ -std=c++20 -Wall -Wextra -Wsign-compare main.cpp -o demo ./demo Runnable example: safe size_t loops #include \u0026lt;cstddef\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;utility\u0026gt; #include \u0026lt;vector\u0026gt; int main() { std::vector\u0026lt;int\u0026gt; a{5, 2, 4, 6, 1}; for (std::size_t i = 0; i + 1 \u0026lt; a.size(); ++i) { bool swapped = false; std::size_t n = a.size() - i; for (std::size_t j = 0; j + 1 \u0026lt; n; ++j) { if (a[j] \u0026gt; a[j + 1]) { std::swap(a[j], a[j + 1]); swapped = true; } } if (!swapped) break; } for (int x : a) std::cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#39; \u0026#39;; std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; // Safe reverse iteration for (std::size_t i = a.size(); i-- \u0026gt; 0; ) { std::cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } std::cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } Why size_t is the better fit Clearer semantics: size_t means \u0026ldquo;size/length\u0026rdquo;, int means \u0026ldquo;signed count\u0026rdquo;. Larger range: on 64-bit systems, int is usually 32-bit and may overflow on huge containers. API matching: vector::size() and string::size() return size_t. Fewer implicit conversions: mixing int and size_t triggers -Wsign-compare and can break logic. E - Engineering (Real-world Usage) Below are three real engineering scenarios with background, rationale, and runnable examples.\nScenario 1: Large-scale batch processing (C++) Background: At billion-scale data, container sizes can exceed 2^31. Why it fits: size_t can represent the range and aligns with STL.\n#include \u0026lt;cstddef\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; int main() { std::vector\u0026lt;int\u0026gt; data(5, 1); std::size_t sum = 0; for (std::size_t i = 0; i \u0026lt; data.size(); ++i) { sum += static_cast\u0026lt;std::size_t\u0026gt;(data[i]); } std::cout \u0026lt;\u0026lt; sum \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } Scenario 2: Memory allocation and buffers (C) Background: C APIs like malloc and memcpy use size_t for byte counts. Why it fits: consistent across platforms and safe for large allocations.\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; int main(void) { size_t n = 5; int *p = (int*)malloc(n * sizeof(int)); if (!p) return 1; for (size_t i = 0; i \u0026lt; n; ++i) p[i] = (int)i; for (size_t i = 0; i \u0026lt; n; ++i) printf(\u0026#34;%d \u0026#34;, p[i]); printf(\u0026#34;\\n\u0026#34;); free(p); return 0; } Scenario 3: Cross-platform library APIs (C++) Background: API functions take buffer length parameters. Why it fits: size_t is the universal size type for callers on different platforms.\n#include \u0026lt;cstddef\u0026gt; #include \u0026lt;cstdint\u0026gt; #include \u0026lt;iostream\u0026gt; std::uint8_t checksum(const std::uint8_t* buf, std::size_t len) { std::uint8_t acc = 0; for (std::size_t i = 0; i \u0026lt; len; ++i) { acc ^= buf[i]; } return acc; } int main() { std::uint8_t payload[] = {1, 2, 3, 4}; std::cout \u0026lt;\u0026lt; static_cast\u0026lt;int\u0026gt;(checksum(payload, sizeof(payload))) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } R - Reflection (Deep Dive) Time and space complexity The loop examples are typically O(n) time O(1) extra space This is independent of int vs size_t; the difference is correctness and maintainability.\nAlternative approaches Option Pros Cons Use cases int index Simple Small range, signed/unsigned mismatch Small data, teaching examples size_t index Large range, API match Unsigned underflow risk Most size/index cases std::ssize Signed, safe reverse Requires C++20 When negative values are meaningful Iterators/range for Safest No index When you do not need indices Why this approach is most practical\nsize_t is the standard size type with best compatibility. Safe patterns avoid underflow pitfalls. Aligns naturally with STL APIs and avoids warnings. Common questions and pitfalls Is size_t always 64-bit? No, it depends on platform width. Is auto i = 0 OK? It deduces int, not size_t. Why is v.size() - 1 dangerous? Underflows on empty containers. Why is for (size_t i = n - 1; i \u0026gt;= 0; --i) wrong? i \u0026gt;= 0 is always true for unsigned. Does int avoid underflow? It avoids unsigned underflow but introduces range and conversion risks. Best practices Prefer std::size_t or container::size_type for sizes and indices. Cache n = v.size() to avoid repeated calls and reduce risk. For reverse loops use for (size_t i = n; i-- \u0026gt; 0;) or std::ssize. Use range-for if you do not need indices. Enable -Wsign-compare to surface bugs early. S - Summary Key takeaways size_t is the standard type for object size and index; sizeof returns it. It matches vector::size() and avoids signed/unsigned mismatch. Its range is larger than int on 64-bit systems. Unsigned subtraction can underflow; write conditions to avoid it. Reverse iteration has safe patterns; do not use i \u0026gt;= 0 with unsigned. References and further reading C++ reference: std::size_t: https://en.cppreference.com/w/cpp/types/size_t C++ reference: std::ssize: https://en.cppreference.com/w/cpp/iterator/ssize ISO C standard: size_t: https://en.cppreference.com/w/c/types/size_t Conclusion size_t is not a mysterious type. It is the standard way C/C++ expresses sizes and indices. If you avoid unsigned underflow and use safe loop conditions, it is more robust and more consistent than int. Consider enabling -Wsign-compare and cleaning up mixed-sign usage in your codebase.\nCall to Action (CTA) Search your codebase for places where size() is mixed with int, switch to size_t, and run tests. If you have hit a bug related to this, share the case and learnings.\n","permalink":"https://jeanphilogong.github.io/jeanblog/dev/c++/size_t-why-not-int-loop/","summary":"\u003ch1 id=\"what-is-size_\"\u003e\u003cstrong\u003eWhat Is size_t? Why C++ Loops Prefer size_t Over int\u003c/strong\u003e\u003c/h1\u003e\n\u003ch3 id=\"subtitle--abstract\"\u003eSubtitle / Abstract\u003c/h3\u003e\n\u003cp\u003eWhen you iterate containers with a \u003ccode\u003efor\u003c/code\u003e loop, \u003ccode\u003esize_t\u003c/code\u003e is often safer and closer to the intended meaning than \u003ccode\u003eint\u003c/code\u003e. This post uses the ACERS structure to explain what \u003ccode\u003esize_t\u003c/code\u003e is, why it is used, the common pitfalls, and practical patterns for production C++.\u003c/p\u003e\n\u003ch3 id=\"meta\"\u003eMeta\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eReading time: 8-10 minutes\u003c/li\u003e\n\u003cli\u003eTags: C++, size_t, type system, loops, STL\u003c/li\u003e\n\u003cli\u003eSEO keywords: size_t usage, size_t vs int, C++ loop initialization, size_t underflow\u003c/li\u003e\n\u003cli\u003eMeta description: Explain size_t and why loops often use it, with safe patterns and engineering scenarios.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"target-readers\"\u003eTarget readers\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003eC++ beginners who are new to \u003ccode\u003esize_t\u003c/code\u003e, \u003ccode\u003esizeof\u003c/code\u003e, and container \u003ccode\u003esize()\u003c/code\u003e return types\u003c/li\u003e\n\u003cli\u003eMid-level engineers who have seen \u003ccode\u003e-Wsign-compare\u003c/code\u003e warnings or unsigned underflow bugs\u003c/li\u003e\n\u003cli\u003eEngineers writing cross-platform or high-performance C++\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h3\u003e\n\u003cp\u003eIn C++ code, you often see loops like:\u003c/p\u003e","title":"What Is size_t? Why C++ Loops Prefer size_t Over int"},{"content":" Subtitle / Abstract A basic counting problem: use frequency + combinations to drop O(n^2) to O(n). Includes engineering use cases and portable implementations.\nReading time: 8-10 minutes Tags: hash-table, counting, array SEO keywords: Good Pairs, hash map, frequency Meta description: Hash counting solution for Good Pairs with complexity and code. Target readers Beginners learning hash tables and counting Engineers who want to map interview patterns to real stats tasks Interview prep for basic counting models Background / Motivation Counting equal pairs is a classic problem. A double loop is O(n^2). With frequency counting, you can solve it in linear time and scale to large data.\nA - Algorithm (Problem and approach) Problem Given an integer array nums, a pair (i, j) is a good pair if nums[i] == nums[j] and i \u0026lt; j. Return the number of good pairs.\nInput/Output Name Type Description nums int[] integer array return int number of good pairs Examples nums output notes [1, 2, 3, 1, 1, 3] 4 (0,3) (0,4) (3,4) (2,5) [1, 1, 1, 1] 6 C(4,2) = 6 [1, 2, 3] 0 no duplicates Simple intuition:\nValue 1 appears 3 times -\u0026gt; C(3,2)=3 Value 3 appears 2 times -\u0026gt; C(2,2)=1 Total = 4 C - Concepts (Core ideas) Frequency count: count occurrences of each value Combinations: if value appears c times, pairs = c*(c-1)/2 Hash table: O(1) average update Key formula:\nFor each value v with count c: Pairs = c * (c - 1) / 2 One-pass model:\nans += count[nums[i]] count[nums[i]] += 1 Practical steps Initialize count and ans = 0 For each element x, add count[x] to ans Increment count[x] E - Engineering (Real-world usage) Scenario 1: Data quality scoring (Python) Duplicate-pair score for a column:\ndef duplicate_pair_score(values): count = {} score = 0 for v in values: score += count.get(v, 0) count[v] = count.get(v, 0) + 1 return score print(duplicate_pair_score([\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;A\u0026#34;, \u0026#34;C\u0026#34;, \u0026#34;A\u0026#34;])) Scenario 2: Batch task dedup weight (Go) package main import \u0026#34;fmt\u0026#34; func goodPairs(ids []int) int { count := map[int]int{} ans := 0 for _, id := range ids { ans += count[id] count[id]++ } return ans } func main() { fmt.Println(goodPairs([]int{7, 7, 8, 9, 7})) } Scenario 3: Frontend duplicate warning (JS) function goodPairs(items) { const count = new Map(); let ans = 0; for (const x of items) { ans += count.get(x) || 0; count.set(x, (count.get(x) || 0) + 1); } return ans; } console.log(goodPairs([\u0026#34;u1\u0026#34;, \u0026#34;u2\u0026#34;, \u0026#34;u1\u0026#34;, \u0026#34;u1\u0026#34;])); R - Reflection Complexity Time: O(n) Space: O(n) Alternatives Approach Time Space Notes double loop O(n^2) O(1) simple but slow sort and group O(n log n) O(1) changes order hash count (one pass) O(n) O(n) fastest in practice Pitfalls Add count[x] before incrementing to avoid self-pairing Use 64-bit integers if counts are large Pre-size hash map if possible S - Summary Good pairs equal combinations of equal values Hash counting drops O(n^2) to O(n) One-pass counting is clean and safe This model transfers to dedup stats, quality scoring, and logs Conclusion Good pairs are a deceptively simple counting problem. Once you master hash counting, many similar tasks become trivial.\nReferences https://leetcode.com/problems/number-of-good-pairs/ https://en.wikipedia.org/wiki/Combination https://docs.python.org/3/library/stdtypes.html#mapping-types-dict https://en.cppreference.com/w/cpp/container/unordered_map https://doc.rust-lang.org/std/collections/struct.HashMap.html Call to Action (CTA) Use this counting model as a base and adapt it to three-sum variants or grouped stats. Share your approach in comments.\nMulti-language reference implementations from typing import List def num_identical_pairs(nums: List[int]) -\u0026gt; int: count = {} ans = 0 for x in nums: ans += count.get(x, 0) count[x] = count.get(x, 0) + 1 return ans if __name__ == \u0026#34;__main__\u0026#34;: print(num_identical_pairs([1, 2, 3, 1, 1, 3])) #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; typedef struct { int key; int count; int used; } Entry; static unsigned hash_int(int key) { return (uint32_t)key * 2654435761u; } static int find_slot(Entry *table, int cap, int key, int *found) { unsigned mask = (unsigned)cap - 1u; unsigned idx = hash_int(key) \u0026amp; mask; while (table[idx].used \u0026amp;\u0026amp; table[idx].key != key) { idx = (idx + 1u) \u0026amp; mask; } *found = table[idx].used \u0026amp;\u0026amp; table[idx].key == key; return (int)idx; } long long num_identical_pairs(const int *nums, int n) { int cap = 1; while (cap \u0026lt; n * 2) cap \u0026lt;\u0026lt;= 1; if (cap \u0026lt; 2) cap = 2; Entry *table = (Entry *)calloc((size_t)cap, sizeof(Entry)); if (!table) return 0; long long ans = 0; for (int i = 0; i \u0026lt; n; ++i) { int found = 0; int pos = find_slot(table, cap, nums[i], \u0026amp;found); if (found) { ans += table[pos].count; table[pos].count += 1; } else { table[pos].used = 1; table[pos].key = nums[i]; table[pos].count = 1; } } free(table); return ans; } int main(void) { int nums[] = {1, 2, 3, 1, 1, 3}; printf(\u0026#34;%lld\\n\u0026#34;, num_identical_pairs(nums, 6)); return 0; } #include \u0026lt;iostream\u0026gt; #include \u0026lt;unordered_map\u0026gt; #include \u0026lt;vector\u0026gt; long long num_identical_pairs(const std::vector\u0026lt;int\u0026gt; \u0026amp;nums) { std::unordered_map\u0026lt;int, long long\u0026gt; count; long long ans = 0; for (int x : nums) { ans += count[x]; count[x] += 1; } return ans; } int main() { std::vector\u0026lt;int\u0026gt; nums{1, 2, 3, 1, 1, 3}; std::cout \u0026lt;\u0026lt; num_identical_pairs(nums) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 0; } package main import \u0026#34;fmt\u0026#34; func numIdenticalPairs(nums []int) int64 { count := map[int]int64{} var ans int64 = 0 for _, x := range nums { ans += count[x] count[x]++ } return ans } func main() { fmt.Println(numIdenticalPairs([]int{1, 2, 3, 1, 1, 3})) } use std::collections::HashMap; fn num_identical_pairs(nums: \u0026amp;[i32]) -\u0026gt; i64 { let mut count: HashMap\u0026lt;i32, i64\u0026gt; = HashMap::new(); let mut ans: i64 = 0; for \u0026amp;x in nums { let c = *count.get(\u0026amp;x).unwrap_or(\u0026amp;0); ans += c; count.insert(x, c + 1); } ans } fn main() { let nums = vec![1, 2, 3, 1, 1, 3]; println!(\u0026#34;{}\u0026#34;, num_identical_pairs(\u0026amp;nums)); } function numIdenticalPairs(nums) { const count = new Map(); let ans = 0; for (const x of nums) { ans += count.get(x) || 0; count.set(x, (count.get(x) || 0) + 1); } return ans; } console.log(numIdenticalPairs([1, 2, 3, 1, 1, 3])); ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/1512-number-of-good-pairs/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\nA basic counting problem: use frequency + combinations to drop O(n^2) to O(n). Includes engineering use cases and portable implementations.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 8-10 minutes\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTags\u003c/strong\u003e: \u003ccode\u003ehash-table\u003c/code\u003e, \u003ccode\u003ecounting\u003c/code\u003e, \u003ccode\u003earray\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eSEO keywords\u003c/strong\u003e: Good Pairs, hash map, frequency\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMeta description\u003c/strong\u003e: Hash counting solution for Good Pairs with complexity and code.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eBeginners learning hash tables and counting\u003c/li\u003e\n\u003cli\u003eEngineers who want to map interview patterns to real stats tasks\u003c/li\u003e\n\u003cli\u003eInterview prep for basic counting models\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eCounting equal pairs is a classic problem. A double loop is O(n^2). With frequency counting, you can solve it in linear time and scale to large data.\u003c/p\u003e","title":"LeetCode 1512: Number of Good Pairs (Hash Counting ACERS Guide)"},{"content":"XOR and RC4: From Principles to Go Practice (with Safer Alternatives) Subtitle / Abstract Use minimal math to explain XOR and RC4, provide runnable Go examples, and clarify why RC4 is considered insecure with recommended alternatives.\nTarget readers Backend engineers reading legacy RC4 code Beginners who confuse encoding and encryption Intermediate developers building a stream-cipher mental model Background / Motivation Many systems still contain RC4 or custom decryption logic. Common mistakes include treating Base64 as encryption and ignoring integrity checks. Understanding XOR and RC4 helps you evaluate security correctly and avoid copying outdated designs into new systems.\nCore concepts XOR: bitwise operation, reversible Stream cipher: XOR a pseudorandom keystream with plaintext bytes RC4: classic stream cipher, no longer recommended Base64: encoding, not encryption Integrity: encryption alone does not prevent tampering Practical steps Receive a Base64 string (often RC4 output) Decode Base64 to raw bytes Initialize RC4 with a shared key XOR keystream with bytes Convert output to UTF-8 text if it is textual Runnable example (Go) package main import ( \u0026#34;crypto/rc4\u0026#34; \u0026#34;encoding/base64\u0026#34; \u0026#34;fmt\u0026#34; ) func rc4XOR(key string, data []byte) ([]byte, error) { c, err := rc4.NewCipher([]byte(key)) if err != nil { return nil, err } out := make([]byte, len(data)) c.XORKeyStream(out, data) return out, nil } func encryptToBase64RC4(key, plaintext string) (string, error) { out, err := rc4XOR(key, []byte(plaintext)) if err != nil { return \u0026#34;\u0026#34;, err } return base64.StdEncoding.EncodeToString(out), nil } func decryptBase64RC4(key, encoded string) (string, error) { raw, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return \u0026#34;\u0026#34;, err } out, err := rc4XOR(key, raw) if err != nil { return \u0026#34;\u0026#34;, err } return string(out), nil } func main() { key := \u0026#34;demo-key\u0026#34; plaintext := \u0026#34;hello rc4\u0026#34; enc, _ := encryptToBase64RC4(key, plaintext) dec, _ := decryptBase64RC4(key, enc) fmt.Println(enc) fmt.Println(dec) } Run:\ngo run rc4_demo.go Explanation XOR is reversible because a XOR b XOR b = a. RC4 generates a pseudorandom keystream and XORs it with data byte by byte. Since encryption and decryption use the same keystream, keystream reuse or bias can leak information.\nCommon pitfalls Base64 is encoding, not encryption RC4 has known biases and is deprecated Encryption alone does not provide integrity; use MAC or AEAD Reusing keys can reveal plaintext Best practices Use AES-GCM or ChaCha20-Poly1305 for new systems Migrate legacy RC4 systems as soon as possible Consider confidentiality and integrity together Conclusion XOR is the core operation behind stream ciphers. RC4 is easy to understand but unsafe; it is suitable for reading legacy code, not new design. Modern systems should use AEAD algorithms instead.\nReferences https://www.rfc-editor.org/rfc/rfc6229 https://www.rfc-editor.org/rfc/rfc7465 https://en.wikipedia.org/wiki/RC4 https://pkg.go.dev/crypto/rc4 Meta Reading time: 8 minutes Tags: go, security, crypto, rc4, xor SEO keywords: XOR, RC4, stream cipher, Go, encryption, Base64 Meta description: Explain XOR and RC4 with runnable Go examples and why RC4 is no longer secure. Call to Action (CTA) After running the demo, replace RC4 with AES-GCM and document the differences and migration cost for your team.\n","permalink":"https://jeanphilogong.github.io/jeanblog/dev/go/xor-rec4-primer/","summary":"\u003ch1 id=\"xor-and-rc4-from-principles-to-go-practice-with-safer-alternatives\"\u003eXOR and RC4: From Principles to Go Practice (with Safer Alternatives)\u003c/h1\u003e\n\u003ch2 id=\"subtitle--abstract\"\u003eSubtitle / Abstract\u003c/h2\u003e\n\u003cp\u003eUse minimal math to explain XOR and RC4, provide runnable Go examples, and clarify why RC4 is considered insecure with recommended alternatives.\u003c/p\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eBackend engineers reading legacy RC4 code\u003c/li\u003e\n\u003cli\u003eBeginners who confuse encoding and encryption\u003c/li\u003e\n\u003cli\u003eIntermediate developers building a stream-cipher mental model\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eMany systems still contain RC4 or custom decryption logic. Common mistakes include treating Base64 as encryption and ignoring integrity checks. Understanding XOR and RC4 helps you evaluate security correctly and avoid copying outdated designs into new systems.\u003c/p\u003e","title":"XOR and RC4: From Principles to Go Practice (with Safer Alternatives)"},{"content":" Final post of the series: put all algorithms into one decision framework so you can quickly choose what to use, why it fits, and how to validate in real projects.\nTarget Readers Engineers who need to justify sort choices in projects/interviews/presentations. People who want a practical cheat sheet using size, distribution, stability, and memory. Background and Motivation Too many algorithms lead to \u0026ldquo;default quicksort\u0026rdquo; or \u0026ldquo;blind stability\u0026rdquo; without a framework. This post offers an actionable selection table + test checklist covering built-ins, non-comparison, external sort, and hybrids. A - Algorithm (Theme and Quick Reference) Core question: Choose a sorting strategy under different constraints.\nQuick reference (priority suggestions)\nStable + nearly sorted: TimSort / merge (Python/Java default). In-place + worst-case bound: Introsort (C++ std::sort idea) / heap sort. Known range/digits: counting/bucket/radix. Small/near-sorted: insertion; also as a hybrid subroutine. External sort (beyond RAM): chunk + multi-way merge (stable). Teaching/demo: bubble/selection/insertion to show stability and swap cost. C - Concepts (Core Dimensions) Dimension Focus Algorithms Time complexity average/worst quick/Introsort/heap/merge/TimSort/non-comparison Space in-place vs O(n) quick/heap/Introsort in-place; merge/TimSort/counting/radix need extra space Stability preserve relative order merge/TimSort/insertion/counting/radix; quick/heap/selection/shell are unstable Data characteristics size/order/range near-sorted -\u0026gt; TimSort/insertion; known range -\u0026gt; counting/radix; large random -\u0026gt; Introsort/quick Environment memory/external storage memory tight -\u0026gt; in-place; beyond RAM -\u0026gt; external merge E - Engineering Scenarios Scenario 1: API pagination sort (Go) Need: mid-size, no stability, memory tight. Choice: sort.Slice (Introsort idea), insertion for small segments. Validate: reverse and heavy-duplicate cases, check for degeneration and time. Scenario 2: Log batch processing (Python) Need: stable, near-sorted (by time bucket). Choice: built-in TimSort. Validate: local inversions; verify stability preserves order. Scenario 3: Large file sorting (C++) Need: data exceeds RAM, stable. Choice: external sort (chunk sort + k-way merge). Validate: chunk size vs I/O; min-heap merge; ensure stable merge. Scenario 4: Known-range integer batches (Go) Need: small range, speed. Choice: counting or radix; if range large but digits limited, use radix. Validate: estimate k vs n; stress test extremes. Scenario 5: Frontend stable table sort (JavaScript) Need: stable multi-key order. Choice: browser built-in (usually stable) or custom stable merge/TimSort; if unsure, map index to preserve stability. R - Reflection Time/space trade-off: in-place but unstable (quick/heap/Introsort) vs stable with extra memory (merge/TimSort/counting/radix). Worst-case guarantees: Introsort/heap/merge have bounds; quicksort needs anti-degeneration strategy; TimSort worst-case is still O(n log n). Data characteristics: known range/digits make non-comparison a big win; near-sorted favors TimSort/insertion. External sorting: I/O dominated; focus on chunk size, merge fan-in, and temp files. S - Summary Ask four questions first: size/distribution? stability? memory/external? range/digits? Built-in sorts are often enough: Python/Java stable TimSort; C++/Go Introsort-like unstable; customize only when needed. Non-comparison sorts are powerful under bounded range/digits; external sort handles beyond-RAM data. Hybrid strategies are the norm: insertion for small segments, heap fallback on depth, run detection + merge. Practice Guide / Steps Write a selection table: scenario -\u0026gt; requirements -\u0026gt; choice -\u0026gt; rationale. Benchmark on six datasets: random, reverse, nearly sorted, heavy duplicates, range-bounded, beyond-RAM. Add monitoring: sort time, comparisons (if measurable), memory; for external sorts track I/O. Require a \u0026ldquo;sort algorithm + rationale\u0026rdquo; field in PRs or design docs. Common Pitfalls and Notes Ignoring stability when business depends on relative order; use stable sort or index mapping. Underestimating memory: counting/radix can explode; external sort needs temp storage planning. Pivot degeneration: custom quicksort needs random/median-of-three + insertion threshold + tail recursion. Using quicksort on nearly sorted data: TimSort/insertion may be faster. Runnable Example: Simple Selection Function (Python) def choose_sort(stable: bool, n: int, range_known=False, near_sorted=False): if range_known: return \u0026#34;counting/radix\u0026#34; if stable else \u0026#34;counting/radix\u0026#34; if stable: if n \u0026gt; 5e5: return \u0026#34;merge/timsort\u0026#34; return \u0026#34;timsort\u0026#34; if near_sorted and n \u0026lt; 1e4: return \u0026#34;insertion\u0026#34; if n \u0026gt; 1e6: return \u0026#34;introsort/heap\u0026#34; return \u0026#34;introsort/quicksort\u0026#34; print(choose_sort(stable=True, n=10000, range_known=False, near_sorted=True)) References and Further Reading The previous 7 posts in this series: O(n^2) baselines, shell, merge, quick, heap, non-comparison, TimSort/Introsort. CLRS sorting chapters; Bentley \u0026amp; McIlroy \u0026ldquo;Engineering a Sort Function\u0026rdquo;. Meta Reading time: approx. 12 min SEO keywords: sorting selection, stable sort, external sort, TimSort, Introsort Meta description: sorting series finale with decision tables by scale/distribution/stability/memory plus testing guidance for real projects. Call to Action (CTA) Fill out a \u0026ldquo;sorting selection table\u0026rdquo; for your project with scenario/requirements/algorithm/rationale. Run benchmarks on six data distributions and record time/memory to validate your choice. If you need external sorting, build a chunk + merge PoC and measure I/O and storage costs. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/sorting/9.sorting-series-selection-guide/","summary":"Practical selection guide: decision tables by scale/distribution/stability/memory, engineering scenarios, test checklist, and common pitfalls to apply the series.","title":"Sorting Series (Final): Practical Selection - Choose by Scale, Stability, Memory, Distribution"},{"content":" Core idea: even with AI, you should be able to implement critical paths offline. AI accelerates; it does not replace thinking. This post combines learning science and practical workflows with a self-checklist.\nTarget readers Mid to senior engineers and tech leads who want AI speed without losing control. Team leads adopting AI-assisted coding or documentation. Engineers who already work with Git, tests, and code reviews. Background and motivation Pain points: Copy-pasting model output without understanding leads to fragile code and hard debugging. Over-reliance on prompts reduces independent implementation ability. Architecture and security decisions get driven by the model instead of the engineer. Goals: Implement critical paths from scratch without AI when needed. Use AI for validation and refactoring, not for blind generation. Build a \u0026ldquo;think first, verify later\u0026rdquo; workflow. Core concepts Feynman technique: if you can explain it simply, you understand it. Deliberate practice: target weak points with feedback and challenge. Retrieval practice: recall and derive before checking answers. Red/blue mode with AI: human writes first (blue), AI critiques (red). Replaceability: can you replace the model and still ship the feature? Practical steps Write a human plan first, then ask AI Sketch interfaces, flow, and edge cases before prompting. Limit copy/paste; hand-type key logic Routes, migrations, permissions should be typed by you; AI can review. Side-by-side comparison Left: your solution, right: AI suggestions. Keep only what you can explain. Retrieval practice loop Implement without AI, then compare with AI, mark blind spots, rewrite once. Feynman output Summarize in 3-5 sentences; if you cannot, study again. Runnable micro-exercise Implement a unique function that preserves order:\ndef unique_keep_order(items): seen = set() result = [] for x in items: if x in seen: continue seen.add(x) result.append(x) return result assert unique_keep_order([1, 2, 2, 3]) == [1, 2, 3] Exercise flow:\nRound 1: no AI, implement and test; note gaps. Round 2: compare with AI, check edge cases (e.g., unhashable items). Round 3: explain complexity and limits to a teammate or in a recording. Explanation Why limit copy/paste? It skips the recall-derive-verify loop and makes understanding shallow. Hand-typing exposes gaps in API knowledge and naming. Trade-offs Fully manual: safest but slow; use for security-critical modules. AI review: faster but needs human design and merge. AI scaffolding: good for kickstart, but requires tests and refactoring. Common questions How to avoid prompt dependence? Write pseudocode and tests first, then ask AI. What if time is tight? Ask AI for checklists or tests; you implement the core. How to prove you are not being driven? Document your decisions and reasons. Security/compliance: never paste secrets; use local or private models if needed. Best practices Weekly: rewrite a core path without AI (auth, billing, migrations). Add PR template fields: what decisions were made by humans vs AI. Use TDD: write tests first, ask AI for edge-case tests only. Keep the \u0026ldquo;explainability\u0026rdquo; rule: if you cannot explain it in 3 sentences, rework. Track blind spots and practice deliberately. Conclusion AI is a multiplier, not a driver. Keep replaceability and explanation as your safety belt. Use Feynman + deliberate practice + retrieval practice to lock in understanding. References Richard Feynman, \u0026ldquo;The Feynman Technique\u0026rdquo; Anders Ericsson, \u0026ldquo;Peak\u0026rdquo; Roediger \u0026amp; Karpicke, \u0026ldquo;Test-Enhanced Learning\u0026rdquo; Thoughtworks Technology Radar (AI-assisted coding) Meta Reading time: about 9 minutes Tags: AI assistant, engineering practice, learning methods SEO keywords: AI dependence, engineering autonomy, deliberate practice, Feynman learning, AI code review Updated: 2025-11-14 Call to Action (CTA) Pick one critical module, hand-write it, then use AI to review and record the diff. Add an \u0026ldquo;AI assistance scope\u0026rdquo; field to your PR template. Share your \u0026ldquo;no-AI rewrite\u0026rdquo; experiences and learnings. ","permalink":"https://jeanphilogong.github.io/jeanblog/thoughts/thoughts/ai-usage-self-control/","summary":"How to avoid copy-paste dependence when using AI for coding: Feynman technique, deliberate practice, retrieval practice, and a practical self-check workflow.","title":"Do Not Let AI Drive You: Keep the Ability to Build Independently"},{"content":" A practical emmet-vim handbook for developers who live in Vim/Neovim but feel HTML/CSS is slow: fast install, must-know shortcuts, minimal runnable examples, and a validation/troubleshooting checklist.\nReader profile and prerequisites Frontend or full-stack engineers using Vim/Neovim for UI work. Comfortable with basic HTML/CSS and editing ~/.vimrc or init.lua. Suggested environment: Vim 8.2+ with +python3 or Neovim 0.7+; Git installed; Homebrew/Apt available. Background and problem Scenario: typing \u0026lt;div class=\u0026quot;card\u0026quot;\u0026gt;\u0026lt;img ...\u0026gt; by hand is slow and error-prone. Pain points: Repetitive HTML/CSS blocks break flow. Managing tag closures and nesting is easy to mess up. VS Code has Emmet built in; Vim lacks comparable speed without a plugin. Goal: expand a full structure in a few keystrokes; example input ul.list\u0026gt;li.item$*3\u0026gt;a{click} should expand correctly, with reliable shortcuts and configurable behavior. Core concepts Abbreviation: ul\u0026gt;li*3 expands to a full tag tree with one shortcut. Trigger key: emmet-vim default is \u0026lt;C-y\u0026gt;, (Ctrl+y then comma); \u0026lt;C-y\u0026gt;d balances/wraps tags. Context aware: in CSS, m10-20 expands to margin: 10px 20px;; in HTML, it builds tags. Numbering with $: li.item$*3 creates item1/2/3; ${} supports placeholders. Environment and dependencies Vim 8.2+ with :echo has('python3') returning 1, or Neovim 0.7+. Python 3.8+ (python3 --version) used by the Emmet engine. Any plugin manager: vim-plug, dein, lazy.nvim, packer.nvim. Optional: Node 18+ for other Emmet CLI tools (not required for emmet-vim). Typical install (vim-plug): \u0026#34; ~/.vimrc or init.vim call plug#begin(\u0026#39;~/.vim/plugged\u0026#39;) Plug \u0026#39;mattn/emmet-vim\u0026#39; call plug#end() let g:user_emmet_leader_key=\u0026#39;,\u0026#39; \u0026#34; customize leader; default is \u0026lt;C-y\u0026gt; Run :PlugInstall in Vim after setup.\nPractical steps (copy-ready) 1) Verify Python support :echo has(\u0026#39;python3\u0026#39;) Expected output is 1. If not, install a Vim build with Python3 or configure Neovim provider.\n2) Configure basic key bindings \u0026#34; Make Emmet trigger shorter: use comma as leader let g:user_emmet_leader_key=\u0026#39;,\u0026#39; \u0026#34; Enable in HTML/CSS/JSX let g:user_emmet_settings = { \\ \u0026#39;javascript.jsx\u0026#39; : { \\ \u0026#39;extends\u0026#39; : \u0026#39;html\u0026#39; \\ } \\} Expected: in HTML/JSX, type an abbreviation and press ,+, or ,+; (same as \u0026lt;C-y\u0026gt;,).\n3) HTML list example Input:\nul.list\u0026gt;li.item$*3\u0026gt;a{click me} Press ,+, to expand:\n\u0026lt;ul class=\u0026#34;list\u0026#34;\u0026gt; \u0026lt;li class=\u0026#34;item1\u0026#34;\u0026gt;\u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;click me\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li class=\u0026#34;item2\u0026#34;\u0026gt;\u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;click me\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li class=\u0026#34;item3\u0026#34;\u0026gt;\u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;click me\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; 4) Wrap or rebalance tags Select text, type ul\u0026gt;li*, press ,+w (Wrap with abbreviation) to wrap it in a list. On a tag, press ,+d to balance select the parent and quickly rearrange. 5) CSS abbreviations Input: p10-20 bgc#0f172a c#e2e8f0 then trigger:\npadding: 10px 20px; background-color: #0f172a; color: #e2e8f0; 6) JSX/TSX usage Extend javascriptreact / typescriptreact in g:user_emmet_settings. In JSX, input Button.primary\u0026gt;{Submit} then trigger: \u0026lt;Button className=\u0026#34;primary\u0026#34;\u0026gt;Submit\u0026lt;/Button\u0026gt; Make sure filetype is javascriptreact/typescriptreact.\nMore frequent snippets (ready to paste) 1) Semantic page shell + top nav Input:\nheader.site\u0026gt;div.container\u0026gt;h1.logo{Brand}+nav\u0026gt;ul\u0026gt;li*3\u0026gt;a{Nav $}+button.btn.primary{Sign up} Output:\n\u0026lt;header class=\u0026#34;site\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;container\u0026#34;\u0026gt; \u0026lt;h1 class=\u0026#34;logo\u0026#34;\u0026gt;Brand\u0026lt;/h1\u0026gt; \u0026lt;nav\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;Nav 1\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;Nav 2\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;Nav 3\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/nav\u0026gt; \u0026lt;button class=\u0026#34;btn primary\u0026#34;\u0026gt;Sign up\u0026lt;/button\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/header\u0026gt; 2) Form with labels and submit Input:\nform#contact\u0026gt;label[for=name]{Name}+input#name[type=text required placeholder=Your name]+label[for=email]{Email}+input#email[type=email required placeholder=hi@example.com]+button.btn[type=submit]{Send} Output:\n\u0026lt;form id=\u0026#34;contact\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;name\u0026#34;\u0026gt;Name\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;name\u0026#34; type=\u0026#34;text\u0026#34; required placeholder=\u0026#34;Your name\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;email\u0026#34;\u0026gt;Email\u0026lt;/label\u0026gt; \u0026lt;input id=\u0026#34;email\u0026#34; type=\u0026#34;email\u0026#34; required placeholder=\u0026#34;hi@example.com\u0026#34;\u0026gt; \u0026lt;button class=\u0026#34;btn\u0026#34; type=\u0026#34;submit\u0026#34;\u0026gt;Send\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; 3) Card grid (blog/product list) Input:\nsection.blog\u0026gt;h2{Latest Posts}+div.grid\u0026gt;article.card$*3\u0026gt;img[alt=thumb$ src=/img/thumb$.jpg]+h3{Post $}+p{Short teaser}+a.read[href=/post$]{Read more} Output (excerpt):\n\u0026lt;section class=\u0026#34;blog\u0026#34;\u0026gt; \u0026lt;h2\u0026gt;Latest Posts\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;grid\u0026#34;\u0026gt; \u0026lt;article class=\u0026#34;card1\u0026#34;\u0026gt; \u0026lt;img alt=\u0026#34;thumb1\u0026#34; src=\u0026#34;/img/thumb1.jpg\u0026#34;\u0026gt; \u0026lt;h3\u0026gt;Post 1\u0026lt;/h3\u0026gt; \u0026lt;p\u0026gt;Short teaser\u0026lt;/p\u0026gt; \u0026lt;a class=\u0026#34;read\u0026#34; href=\u0026#34;/post1\u0026#34;\u0026gt;Read more\u0026lt;/a\u0026gt; \u0026lt;/article\u0026gt; ... \u0026lt;/div\u0026gt; \u0026lt;/section\u0026gt; 4) Table with auto numbering Input:\ntable.table\u0026gt;thead\u0026gt;tr\u0026gt;th*3{Col $}+tbody\u0026gt;tr*3\u0026gt;td{Row $ Col 1}+td{Row $ Col 2}+td{Row $ Col 3} Output:\n\u0026lt;table class=\u0026#34;table\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;Col 1\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Col 2\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;Col 3\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;Row 1 Col 1\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Row 1 Col 2\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Row 1 Col 3\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;Row 2 Col 1\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Row 2 Col 2\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Row 2 Col 3\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;Row 3 Col 1\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Row 3 Col 2\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;Row 3 Col 3\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; 5) JSX/TSX component snippet Input:\nCard\u0026gt;Image[src=/hero.png alt=Hero aria-label=Hero]+h3{Landing}+p{Faster HTML}+Button.primary{Get started} Expanded in React/TSX:\n\u0026lt;Card\u0026gt; \u0026lt;Image src=\u0026#34;/hero.png\u0026#34; alt=\u0026#34;Hero\u0026#34; aria-label=\u0026#34;Hero\u0026#34; /\u0026gt; \u0026lt;h3\u0026gt;Landing\u0026lt;/h3\u0026gt; \u0026lt;p\u0026gt;Faster HTML\u0026lt;/p\u0026gt; \u0026lt;Button className=\u0026#34;primary\u0026#34;\u0026gt;Get started\u0026lt;/Button\u0026gt; \u0026lt;/Card\u0026gt; 6) CSS quick combo (Emmet CSS syntax) Input:\nd:f ai:c jc:sb g:16 p:16 m:0 bdrs:12px bgc:#0f172a c:#e2e8f0 Output:\ndisplay: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px; margin: 0; border-radius: 12px; background-color: #0f172a; color: #e2e8f0; 7) Wrap with abbreviation examples Select lines \u0026ldquo;Item A\u0026rdquo; and \u0026ldquo;Item B\u0026rdquo;, type ul.list\u0026gt;li*, press ,+w: \u0026lt;ul class=\u0026#34;list\u0026#34;\u0026gt; \u0026lt;li\u0026gt;Item A\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;Item B\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; Great for converting raw text to a list or card container. Minimal runnable demo (local) Create demo.html: \u0026lt;!doctype html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt;\u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt;\u0026lt;title\u0026gt;Emmet Demo\u0026lt;/title\u0026gt;\u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- type emmet abbreviations here and trigger expand --\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Open in Vim, enter section.hero\u0026gt;h1{Hello}+p{Speed up with emmet-vim}+ul.features\u0026gt;li.feature$*3 inside \u0026lt;body\u0026gt;. Trigger expansion, save with :w, open in browser to see the result. Trade-offs and choices emmet-vim in Vim vs Emmet via LSP/completion: the former is dependency-free and instant; the latter may need Node or a server but integrates with completions. Trigger key: default \u0026lt;C-y\u0026gt; avoids conflicts but is two keystrokes; , or \u0026lt;C-e\u0026gt; is faster but can conflict with other plugins. Formatting: emmet-vim does not format; run Prettier/ESLint on output if needed. Common pitfalls and FAQ Not working: has('python3') is 0, wrong filetype, or :PlugInstall not run. JSX expands with HTML attrs: ensure javascript.jsx/javascriptreact extends html; set filetype if needed. Key conflicts: check mappings with :verbose imap , , and rebind. Multi-cursor: emmet-vim does not support it natively; use vim-visual-multi and trigger after inserting abbreviations. Performance: large expansions can be slow; use on component snippets instead of massive trees. Test and validation checklist :echo has('python3') == 1. In HTML buffer, type div#app\u0026gt;header\u0026gt;h1{Hi}+nav\u0026gt;ul\u0026gt;li*3\u0026gt;a{link$} and expand correctly. In CSS buffer, m10-20 and bgc#333 expand to valid declarations. In JSX buffer, Card\u0026gt;Button.primary{Go} expands to \u0026lt;Card\u0026gt;\u0026lt;Button className=\u0026quot;primary\u0026quot;\u0026gt;Go\u0026lt;/Button\u0026gt;\u0026lt;/Card\u0026gt;. No errors in :messages; trigger key not overridden. Performance and accessibility Prefer semantic tags (header/nav/main/section) for screen readers and SEO. Always add alt for images: img[alt=avatar src=/avatar.png]. Add aria-label placeholders for icon-only buttons. Emmet does not impact performance metrics directly, but avoid unnecessary nesting. Best practices Configure g:user_emmet_settings explicitly per filetype for HTML/JSX/TSX consistency. Customize the leader (e.g., ,) and sync it across machines via dotfiles. Combine with formatters (Prettier/StyLua/ESLint) to normalize style on save. Write a skeleton abbreviation first, then add classes and attributes. Remember $ for numbering and {} for text content. Summary and next steps You now have: install steps, key bindings, HTML/CSS/JSX examples, validation checklist, and troubleshooting. Next steps: Create team snippets as Emmet custom snippets. Combine Emmet with UltiSnips/LuaSnip for composite templates. Integrate with LSP/formatter to build a consistent save workflow. References and links Emmet docs: https://docs.emmet.io/ emmet-vim repo: https://github.com/mattn/emmet-vim Vim Python3 provider: https://github.com/neovim/neovim/wiki/FAQ#python-support Meta Estimated reading: 11 minutes; for Vim/Neovim frontend engineers. Tags: vim, neovim, emmet, frontend, productivity; category: frontend. SEO keywords: emmet-vim, Vim Emmet, HTML CSS autocompletion. Updated: 2025-11-14. CTA Create a local demo.html and expand a few abbreviations yourself. If you hit key conflicts or new scenarios, open an issue or comment. If this helped, star mattn/emmet-vim to support the author. ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/frontend/emmet-vim-guide/","summary":"Practical Emmet notes for Vim/Neovim users: install, key mappings, runnable examples, validation checklist, and common pitfalls to 3x your page and component speed.","title":"Emmet-Vim Speed Guide: Write HTML/CSS with Abbreviations"},{"content":" Sorting series post 8 explains two engineering hybrid sorts: TimSort (stable, run detection + merge + insertion) used by Python/Java, and Introsort (quick + heap + insertion, unstable) behind C++ std::sort.\nTarget Readers Anyone who wants to understand built-in sort behavior, stability, and degeneration protection. Engineers who need a hybrid strategy balancing average performance and worst-case bounds. People preparing interviews or talks on TimSort/Introsort. Background and Motivation Pure quicksort can degrade; pure merge needs O(n) memory and does not fully exploit near-sortedness. Hybrid sorting combines strengths: TimSort uses natural runs and stable merge; Introsort falls back to heap when recursion depth is too large and uses insertion on small segments. A - Algorithm TimSort core flow (stable)\nScan array to detect monotonic runs (increasing/decreasing; reverse decreasing runs). Extend short runs to minrun and use insertion to finish them. Merge runs following stack rules with stable merge; near-sorted data yields long runs and fewer merges. Introsort core flow (unstable)\nStart with quicksort (random/median-of-three). When recursion depth exceeds a threshold (~2*log n), switch to heap sort to avoid O(n^2). Use insertion for segments smaller than a threshold (e.g., 16/24). C - Concepts Algorithm Stable Avg Time Worst Time Space Key Points TimSort Yes O(n log n) O(n log n) O(n) run detection + stable merge + insertion Introsort No O(n log n) O(n log n) O(1) quick start + depth fallback to heap + insertion run: a monotonic contiguous subsequence; more runs means more merges. minrun: lower bound on run length (typically 32~64); short runs are extended with insertion. Depth threshold: Introsort uses 2*floor(log2 n) as a cutoff for heap fallback. E - Engineering Scenario 1: Python/Java default sort (TimSort idea) Background: stable sort with excellent performance on near-sorted data.\n# Simplified TimSort skeleton (idea only, not full merge rules) MINRUN = 32 def insertion(a, l, r): for i in range(l+1, r+1): key=a[i]; j=i-1 while j\u0026gt;=l and a[j]\u0026gt;key: a[j+1]=a[j]; j-=1 a[j+1]=key def timsort(a): n=len(a) # 1) detect runs + extend to MINRUN runs=[]; i=0 while i\u0026lt;n: j=i+1 while j\u0026lt;n and a[j]\u0026gt;=a[j-1]: j+=1 # simplified: only increasing runs l,r=i,j-1 if r-l+1 \u0026lt; MINRUN: end=min(n-1,l+MINRUN-1) insertion(a,l,end) r=end runs.append((l,r)) i=r+1 # 2) simplified merge: left to right import heapq while len(runs)\u0026gt;1: l1,r1 = runs.pop(0) l2,r2 = runs.pop(0) merge(a,l1,r1,l2,r2) runs.insert(0,(l1,r2)) return a def merge(a,l1,r1,l2,r2): buf = a[l1:r2+1] i=0; j=l2-l1; k=l1 while i\u0026lt;=r1-l1 and j\u0026lt;=r2-l1: if buf[i] \u0026lt;= buf[j]: a[k]=buf[i]; i+=1 else: a[k]=buf[j]; j+=1 k+=1 while i\u0026lt;=r1-l1: a[k]=buf[i]; i+=1; k+=1 while j\u0026lt;=r2-l1: a[k]=buf[j]; j+=1; k+=1 arr=[5,2,3,1,4] print(timsort(arr)) Scenario 2: C++ std::sort idea (Introsort) Background: low constants, in-place, worst-case bounded.\n#include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; void insertion(vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int r){ for(int i=l+1;i\u0026lt;=r;++i){int key=a[i], j=i-1; while(j\u0026gt;=l \u0026amp;\u0026amp; a[j]\u0026gt;key){a[j+1]=a[j]; j--;} a[j+1]=key;} } int partition_mid(vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int r){ int m=l+(r-l)/2; if(a[m]\u0026lt;a[l]) swap(a[m],a[l]); if(a[r]\u0026lt;a[l]) swap(a[r],a[l]); if(a[r]\u0026lt;a[m]) swap(a[r],a[m]); int pivot=a[m]; int i=l-1,j=r+1; while(true){ do{i++;}while(a[i]\u0026lt;pivot); do{j--;}while(a[j]\u0026gt;pivot); if(i\u0026gt;=j) return j; swap(a[i],a[j]); } } void heapsort(vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int r){ make_heap(a.begin()+l, a.begin()+r+1); sort_heap(a.begin()+l, a.begin()+r+1); } void introsort(vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int r, int depth){ while(r-l+1 \u0026gt; 16){ if(depth==0){ heapsort(a,l,r); return; } int p = partition_mid(a,l,r); if(p-l \u0026lt; r-p){ introsort(a,l,p,depth-1); l=p+1; } else { introsort(a,p+1,r,depth-1); r=p; } } insertion(a,l,r); } int main(){ vector\u0026lt;int\u0026gt; a={5,2,3,1,4,9,8,7,6}; int depth = 2*log(a.size()); introsort(a,0,a.size()-1,depth); for(int x:a) cout\u0026lt;\u0026lt;x\u0026lt;\u0026lt;\u0026#34; \u0026#34;; } Scenario 3: Go/JavaScript hybrid sorting Go: built-in sort is similar to Introsort (quick + heap + insertion); check source for details. JS: use TimSort implementations for stability; otherwise mimic Introsort for in-place speed. R - Reflection Complexity: TimSort: worst O(n log n), faster on partially sorted data (long runs, fewer merges), space O(n). Introsort: worst O(n log n), average like quicksort, space O(1) ignoring recursion. Stability: TimSort is stable; Introsort is not. Trade-offs: Near-sorted + stable: TimSort (Python/Java default). Memory tight + low constants: Introsort (C++ std::sort). External sorting: TimSort/merge; with data larger than RAM, use multi-way merge. Why it works: hybrid strategies absorb strengths and avoid degeneration paths. S - Summary TimSort uses run detection + stable merge + insertion; excellent on near-sorted data and stable; default in Python/Java. Introsort starts with quicksort, falls back to heap at depth limit, uses insertion at the end; unstable but in-place and fast; used by C++ std::sort. Selection: stable + near-sorted -\u0026gt; TimSort; in-place + worst-case bound -\u0026gt; Introsort; external -\u0026gt; merge/TimSort; known range -\u0026gt; non-comparison. Understanding built-ins helps performance tuning and interviews. Practice Guide / Steps Decide on stability, memory budget, and degree of order. For TimSort: Implement run detection and reverse decreasing runs. Set minrun (32~64) and fill short runs with insertion. Implement stable merge and follow run-stack rules. For Introsort: Depth limit = 2*floor(log2 n); fallback to heap when exceeded. Use insertion for small segments; pivot random or median-of-three. Benchmark with random, nearly sorted, reverse, heavy duplicates. Common Pitfalls and Notes TimSort merge rules are complex; avoid unbalanced run stacks; keep stability. Introsort heap fallback must pass correct subranges; Hoare partition boundaries matter. Small-segment thresholds should be tuned (commonly 16~32). Runnable Example: Mini Introsort in JavaScript function insertion(a,l,r){ for(let i=l+1;i\u0026lt;=r;i++){ const key=a[i]; let j=i-1; while(j\u0026gt;=l \u0026amp;\u0026amp; a[j]\u0026gt;key){ a[j+1]=a[j]; j--; } a[j+1]=key; } } function partition(a,l,r){ const m=l+((r-l)\u0026gt;\u0026gt;1); if(a[m]\u0026lt;a[l]) [a[m],a[l]]=[a[l],a[m]]; if(a[r]\u0026lt;a[l]) [a[r],a[l]]=[a[l],a[r]]; if(a[r]\u0026lt;a[m]) [a[r],a[m]]=[a[m],a[r]]; const pivot=a[m]; let i=l-1,j=r+1; while(true){ do{i++;}while(a[i]\u0026lt;pivot); do{j--;}while(a[j]\u0026gt;pivot); if(i\u0026gt;=j) return j; [a[i],a[j]]=[a[j],a[i]]; } } function heapify(a,n,i,l){ while(true){ let largest=i, left=2*(i-l)+1+l, right=left+1; if(left\u0026lt;n \u0026amp;\u0026amp; a[left]\u0026gt;a[largest]) largest=left; if(right\u0026lt;n \u0026amp;\u0026amp; a[right]\u0026gt;a[largest]) largest=right; if(largest===i) break; [a[i],a[largest]]=[a[largest],a[i]]; i=largest; } } function heapsort(a,l,r){ const n=r+1; for(let i=Math.floor((l+r)/2); i\u0026gt;=l; i--) heapify(a,n,i,l); for(let end=r; end\u0026gt;l; end--){ [a[l],a[end]]=[a[end],a[l]]; heapify(a,end,l,l); } } function introsort(a,l=0,r=a.length-1,depth=2*Math.floor(Math.log2(a.length||1))){ while(r-l+1\u0026gt;16){ if(depth===0){ heapsort(a,l,r); return a; } const p=partition(a,l,r); if(p-l \u0026lt; r-p){ introsort(a,l,p,depth-1); l=p+1; } else { introsort(a,p+1,r,depth-1); r=p; } } insertion(a,l,r); return a; } console.log(introsort([5,2,3,1,4,9,8,7,6])); References and Further Reading Tim Peters, \u0026ldquo;Timsort\u0026rdquo; design notes (CPython source) Java Arrays.sort (object version) implementation Musser, \u0026ldquo;Introspective Sorting and Selection Algorithms\u0026rdquo; (1997) Bentley \u0026amp; McIlroy, \u0026ldquo;Engineering a Sort Function\u0026rdquo; (1993) Meta Reading time: approx. 16 min SEO keywords: TimSort, Introsort, std::sort, stable sort, hybrid sort Meta description: sorting series (8) explaining TimSort and Introsort strategies, stability, trade-offs, and engineering usage. Call to Action (CTA) Benchmark your dataset: built-in sort vs your hybrid implementation. If you need stability and near-sorted performance, try TimSort ideas; for in-place and worst-case bounds, try Introsort. Follow the series finale: the selection guide. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/sorting/8.sorting-series-timsort-introsort/","summary":"Explain Python/Java TimSort and C++ std::sort Introsort: triggers, stability, complexity, trade-offs, with skeletons and selection guidance.","title":"Sorting Series (8): TimSort and Introsort - Engineering Patterns Behind Built-in Sorts"},{"content":" Sorting series post 7 focuses on non-comparison sorting: when key range or digit length is bounded, complexity can drop to O(n+k), but space, stability, and feasibility must be balanced.\nTarget Readers Engineers sorting integer keys with known range/digits. Learners seeking lower-than-n-log-n sorting for large batches. People comparing standard library comparison sorts vs non-comparison. Background and Motivation Comparison sorts have a lower bound of Omega(n log n); non-comparison sorts bypass that by using range/digit information. Cost: extra space and restricted applicability; implementation must handle stability and memory carefully. A - Algorithm Covered algorithms: counting sort, bucket sort, radix sort (LSD).\nBasic examples\nCounting: [4, 2, 2, 8, 3], range 0..9, count -\u0026gt; prefix sums -\u0026gt; stable fill. Radix: group by ones/tens/hundreds digits, stable per digit. C - Concepts Algorithm Idea Time Space Stable Counting frequency + prefix sums O(n+k) O(k+n) Can be stable Bucket split by ranges, sort within buckets expected O(n+k) O(n+k) depends on inner sort Radix stable per-digit sort in multiple passes O(d*(n+b)) O(n+b) Yes if each pass is stable k: range size; d: number of digits; b: base (number of buckets). Stability: counting can be stable; radix requires stable passes; bucket depends on inner sort. E - Engineering Scenario 1: Small-range integer sort (Python counting) def counting_sort(a, max_val): cnt = [0]*(max_val+1) for x in a: cnt[x]+=1 # prefix sums for i in range(1, len(cnt)): cnt[i]+=cnt[i-1] out=[0]*len(a) for x in reversed(a): cnt[x]-=1 out[cnt[x]] = x return out print(counting_sort([4,2,2,8,3], 9)) Scenario 2: Bucket sort for known float distribution (JavaScript) Background: floats uniformly distributed in [0,1).\nfunction bucketSort(arr, buckets=10){ const B=Array.from({length:buckets},()=\u0026gt;[]); for(const x of arr){ const idx = Math.min(buckets-1, Math.floor(x*buckets)); B[idx].push(x); } for(const b of B) b.sort((a,b)=\u0026gt;a-b); return B.flat(); } console.log(bucketSort([0.78,0.17,0.39,0.26,0.72,0.94,0.21,0.12,0.23,0.68])); Scenario 3: Radix sort for large integers (Go, LSD) package main import \u0026#34;fmt\u0026#34; func radixLSD(a []int) { maxv := 0 for _,v := range a { if v\u0026gt;maxv { maxv=v } } exp := 1 buf := make([]int, len(a)) for maxv/exp \u0026gt; 0 { cnt := make([]int, 10) for _,v := range a { digit := (v/exp)%10; cnt[digit]++ } for i:=1;i\u0026lt;10;i++ { cnt[i]+=cnt[i-1] } for i:=len(a)-1;i\u0026gt;=0;i-- { d := (a[i]/exp)%10 cnt[d]-- buf[cnt[d]] = a[i] } copy(a, buf) exp *= 10 } } func main(){ a:=[]int{170,45,75,90,802,24,2,66}; radixLSD(a); fmt.Println(a) } Scenario 4: C++ counting sort (small range) #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; vector\u0026lt;int\u0026gt; counting_sort(const vector\u0026lt;int\u0026gt;\u0026amp; a, int maxv){ vector\u0026lt;int\u0026gt; cnt(maxv+1), out(a.size()); for(int x:a) cnt[x]++; for(int i=1;i\u0026lt;=maxv;i++) cnt[i]+=cnt[i-1]; for(int i=(int)a.size()-1;i\u0026gt;=0;i--){ int x=a[i]; cnt[x]--; out[cnt[x]]=x; } return out; } Scenario 5: Rust radix sort (LSD) pub fn radix_lsd(a: \u0026amp;mut [u32]) { let mut maxv = *a.iter().max().unwrap(); let mut exp = 1u32; let n = a.len(); let mut buf = vec![0u32; n]; while maxv/exp \u0026gt; 0 { let mut cnt = [0usize; 10]; for \u0026amp;v in a.iter() { cnt[((v/exp)%10) as usize] += 1; } for i in 1..10 { cnt[i] += cnt[i-1]; } for \u0026amp;v in a.iter().rev() { let d = ((v/exp)%10) as usize; cnt[d] -= 1; buf[cnt[d]] = v; } a.copy_from_slice(\u0026amp;buf); exp *= 10; } } R - Reflection Complexity and prerequisites: Counting: O(n+k), k is range size; if k \u0026raquo; n, not worth it. Bucket: expected O(n+k) depends on distribution; worst-case can degrade. Radix: O(d*(n+b)), d digits, base b; each pass must be stable. Trade-offs: Memory: counting/bucket needs O(k) or O(n+k); large ranges are infeasible. Stability: counting and radix can be stable; bucket depends on inner sort. Data type: best for integers or keys mappable to integers (dates, IPs, fixed-length strings). Why it works: With bounded range/digits, non-comparison sorts break the n log n lower bound. Great for logs, bucketed stats, and bulk integer sorting. S - Summary Non-comparison sorts require known range/digits/distribution and can reach O(n+k). Counting is simple and stable for small ranges; radix works for multi-digit integers; bucket depends on distribution. Core risks: memory blow-up, distribution mismatch, missing stability. Selection: small range -\u0026gt; counting; moderate digits + stable -\u0026gt; radix; uniform floats -\u0026gt; bucket; otherwise compare-based. Practice Guide / Steps Estimate range/digits: if k is close to or larger than n, be cautious. Decide stability: radix needs stable per-digit sort; bucket may need stable inner sort. Control memory: counting array length = max-min+1; radix buffers at least O(n). Test with random, all equal, huge range, skewed distribution. Common Pitfalls and Notes Counting sort with negatives needs offset (or split positive/negative). Radix loses stability if any digit pass is unstable. Bucket sort degrades on skewed distributions; increase bucket count or sort large buckets with other methods. Large memory usage calls for fallback to comparison sort or chunking. Runnable Example: Counting with negatives (Python) def counting_sort_with_neg(a): mn, mx = min(a), max(a) offset = -mn cnt = [0]*(mx - mn + 1) for x in a: cnt[x+offset]+=1 for i in range(1,len(cnt)): cnt[i]+=cnt[i-1] out=[0]*len(a) for x in reversed(a): cnt[x+offset]-=1 out[cnt[x+offset]] = x return out print(counting_sort_with_neg([3,-1,2,-1,0])) References and Further Reading CLRS non-comparison sorting chapters Donald Knuth, \u0026ldquo;The Art of Computer Programming, Vol. 3\u0026rdquo; (Sorting and Searching) Lower bounds and word-RAM model discussions on integer sorting Meta Reading time: approx. 15 min SEO keywords: counting sort, bucket sort, radix sort, non-comparison, O(n+k) Meta description: sorting series (7) explaining prerequisites, complexity, and engineering trade-offs for counting/bucket/radix with multilingual examples. Call to Action (CTA) Estimate range/digits of your dataset and implement a counting or radix sort benchmark. If distribution is skewed, tune bucket counts or use a hybrid inside large buckets. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/sorting/7.sorting-series-non-comparison/","summary":"Explain prerequisites, complexity, implementation details, and pitfalls of non-comparison sorts with multilingual examples for counting, bucket, and radix.","title":"Sorting Series (7): Non-Comparison Sorting - Counting, Bucket, Radix and the Range/Digit Tradeoff"},{"content":" For frontend developers with 1-2 years of experience who want a fast, state-driven button in Svelte. Covers state colors, disabled and loading states, accessibility (ARIA), testing, pitfalls, and runnable examples.\nTarget readers and prerequisites Frontend engineers familiar with JS/TS and new to Svelte or already using it. Developers who need a unified button style, state, and interaction in a project. Requirements: Node 18+, Svelte 5, package manager (npm/pnpm), can run npm create svelte@latest. Background / Motivation Buttons are high-frequency interactions, but style, state, and accessibility are often ignored. Dynamic class names without null protection lead to undefined or broken styles. Accessibility (keyboard and ARIA) plus loading/disabled states are product-grade requirements. Consistency needs a centralized state-to-style mapping to avoid magic strings everywhere. Core concepts State mapping: map business states to class strings via a function, not nested ternaries in templates. Optional chaining (?.) and nullish coalescing (??): safely read backend fields and provide defaults. ARIA and keyboard access: aria-busy, aria-disabled, role, tabindex help screen readers and keyboard users. Visual hierarchy: primary, secondary, ghost buttons. Environment and dependencies Node 18+, Svelte 5 UI utility classes: examples use Tailwind (replace with any styling system) Recommended commands: npm create svelte@latest demo-buttons cd demo-buttons npm install Practical steps 1) Centralize state-to-style mapping // statusTone.ts export function statusTone(status?: string) { if (status === \u0026#39;succeeded\u0026#39; || status === \u0026#39;completed\u0026#39;) { return \u0026#39;bg-emerald-600 hover:bg-emerald-700 text-white border border-emerald-600\u0026#39;; } if (status === \u0026#39;failed\u0026#39;) { return \u0026#39;bg-rose-600 hover:bg-rose-700 text-white border border-rose-600\u0026#39;; } if (status === \u0026#39;processing\u0026#39; || status === \u0026#39;pending\u0026#39;) { return \u0026#39;bg-amber-500 hover:bg-amber-600 text-white border border-amber-500\u0026#39;; } return \u0026#39;bg-slate-200 text-slate-700 border border-slate-300\u0026#39;; } Why: keep status-to-class logic centralized and maintainable; supports both completed and succeeded.\n2) Safe values inside a Svelte component \u0026lt;script lang=\u0026#34;ts\u0026#34;\u0026gt; import { statusTone } from \u0026#39;./statusTone\u0026#39;; export let status: string | undefined; export let loading = false; export let label = \u0026#39;Submit\u0026#39;; \u0026lt;/script\u0026gt; \u0026lt;button class={`inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold transition ${statusTone(status)}`} aria-busy={loading} aria-disabled={loading} disabled={loading} \u0026gt; {#if loading} \u0026lt;span class=\u0026#34;h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; {/if} {label ?? \u0026#39;Submit\u0026#39;} \u0026lt;/button\u0026gt; Notes:\nlabel ?? 'Submit' provides a default label safely. aria-busy, aria-disabled, and disabled stay in sync. 3) Optional chaining and nullish coalescing example {#if detailStatus?.status ?? record.status} \u0026lt;span class=\u0026#34;text-xs text-slate-500\u0026#34;\u0026gt; Current status: {detailStatus?.status ?? record.status ?? \u0026#39;pending\u0026#39;} \u0026lt;/span\u0026gt; {/if} ?. avoids errors if detailStatus is undefined, ?? falls back to a default.\n4) Keyboard and screen reader support For non-\u0026lt;button\u0026gt; elements, add: role=\u0026quot;button\u0026quot;, tabindex=\u0026quot;0\u0026quot;, aria-label=\u0026quot;...\u0026quot;. Handle on:keydown for Enter or Space. Sync loading/disabled state with aria-busy and aria-disabled. 5) Common variants Primary: main action, high-contrast or brand color. Secondary: dark or outline style for secondary actions. Ghost: transparent background with border. Icon button: add aria-label for screen readers. 6) Skeleton loading / disabled strategy Loading: show spinner, block double-submit; use disabled and aria-busy. Disabled: for permission/quota conditions, use weaker style like opacity-60 cursor-not-allowed. 7) Events and error handling Wrap click: set loading optimistically, run async work, reset in finally. On error: show toast, and color with statusTone('failed') if needed. Runnable snippet \u0026lt;script lang=\u0026#34;ts\u0026#34;\u0026gt; import { statusTone } from \u0026#39;./statusTone\u0026#39;; let status: \u0026#39;pending\u0026#39; | \u0026#39;processing\u0026#39; | \u0026#39;succeeded\u0026#39; | \u0026#39;failed\u0026#39; = \u0026#39;pending\u0026#39;; let loading = false; async function simulate() { loading = true; status = \u0026#39;processing\u0026#39;; await new Promise((r) =\u0026gt; setTimeout(r, 1200)); status = Math.random() \u0026gt; 0.5 ? \u0026#39;succeeded\u0026#39; : \u0026#39;failed\u0026#39;; loading = false; } \u0026lt;/script\u0026gt; \u0026lt;div class=\u0026#34;space-y-3\u0026#34;\u0026gt; \u0026lt;button class={`inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold transition ${statusTone(status)}`} aria-busy={loading} aria-disabled={loading} disabled={loading} on:click={simulate} \u0026gt; {#if loading} \u0026lt;span class=\u0026#34;h-3 w-3 animate-spin rounded-full border-2 border-white border-t-transparent\u0026#34;\u0026gt;\u0026lt;/span\u0026gt; {/if} {status === \u0026#39;pending\u0026#39; ? \u0026#39;Start\u0026#39; : status === \u0026#39;processing\u0026#39; ? \u0026#39;Processing...\u0026#39; : status === \u0026#39;succeeded\u0026#39; ? \u0026#39;Done\u0026#39; : \u0026#39;Retry\u0026#39;} \u0026lt;/button\u0026gt; \u0026lt;p class=\u0026#34;text-sm text-slate-600\u0026#34;\u0026gt;Current status: {status}\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; Run and verify:\nnpm run dev # Page shows the button; click it to see Processing... then success or failure color Common questions and notes Inconsistent status values: backend may return succeeded/completed; handle both. Long class strings: you can use clsx or classnames, but keep mapping logic centralized. Accessibility gaps: custom elements need role/tabindex/aria-label; loading needs aria-busy. Disabled styles: add opacity-60 cursor-not-allowed for clarity. Default text: use ?? instead of || to avoid empty string issues. Testing checklist Unit: statusTone returns expected classes for each state. Component: when loading, button.disabled === true and aria-busy=\u0026quot;true\u0026quot; exists. Accessibility: Tab focuses, Enter/Space triggers; aria-label present for icon buttons. Visual: contrast ratio \u0026gt;= 4.5:1 for text on backgrounds. Best practices Split mapping, structure, and a11y: function (state-\u0026gt;class) + template + accessibility helpers. Define the state machine before styling; avoid scattered magic strings. Default to accessibility: keyboard, screen reader, and synchronized disabled/busy states. Provide a runnable example for team reuse. Summary / Next steps The key is \u0026ldquo;state mapping + safe values + a11y sync\u0026rdquo;. statusTone centralizes styles, ?. and ?? make data safe, ARIA makes it production ready. Next: align with your design system (colors/sizes/icons), publish a Button component, and add Playwright a11y checks. References Svelte docs: events and accessibility MDN: Optional chaining, Nullish coalescing WAI-ARIA Authoring Practices: Button Call to Action (CTA) Copy the example into your component library and replace colors/states. Audit existing buttons for missing aria-* and disabled styles. ","permalink":"https://jeanphilogong.github.io/jeanblog/dev/frontend/svelte-button-config-guide/","summary":"Build reusable buttons in Svelte: dynamic classes, optional chaining and nullish coalescing, safe defaults, state-driven styles, accessibility, testing, and common pitfalls.","title":"Svelte Button Configuration Guide: States, Styles, and Accessibility"},{"content":" Sorting series post 6 focuses on heap sort: in-place O(n log n), unstable, slightly higher constants but worst-case guaranteed. It is also the foundation of streaming top-k.\nTarget Readers Engineers who need in-place sorting with worst-case O(n log n) guarantees. Learners connecting priority queues, top-k, and heap sort. People comparing quick/merge/heap selection. Background and Motivation Heap sort builds a heap and repeatedly extracts the max; best/avg/worst are all O(n log n). Pros: in-place, worst-case safe. Cons: unstable, cache-unfriendly, higher constants than quicksort. Shares structure with priority queues and streaming top-k, giving strong engineering value. A - Algorithm Steps\nBuild a max-heap (bottom-up O(n)). Repeatedly swap root with tail, shrink heap, sift down to restore heap (O(log n) each). Basic example Array [4, 10, 3, 5, 1]:\nAfter heapify: [10, 5, 3, 4, 1]. Swap root and tail -\u0026gt; [1,5,3,4,10], sift down -\u0026gt; [5,4,3,1,10]. Repeat until sorted. C - Concepts Concept Description Heap property Parent \u0026gt;= children (max-heap), children of i are 2i+1 and 2i+2. Heapify Sift down from last non-leaf to root, O(n). Sift down Swap node downward to restore heap, O(log n). Stability Unstable; swaps can reorder equals. Space In-place O(1) extra space. Complexity\nTime: heapify O(n) + n times sift O(log n) =\u0026gt; O(n log n); worst-case same. Space: O(1); recursion uses O(log n) stack, iterative uses O(1). E - Engineering Scenario 1: In-place backend sorting (C) Background: need in-place and worst-case guarantees.\nvoid heapify(int *a, int n, int i){ while(1){ int l=2*i+1, r=2*i+2, largest=i; if(l\u0026lt;n \u0026amp;\u0026amp; a[l]\u0026gt;a[largest]) largest=l; if(r\u0026lt;n \u0026amp;\u0026amp; a[r]\u0026gt;a[largest]) largest=r; if(largest==i) break; int t=a[i]; a[i]=a[largest]; a[largest]=t; i=largest; } } void heap_sort(int *a, int n){ for(int i=n/2-1;i\u0026gt;=0;i--) heapify(a,n,i); for(int end=n-1; end\u0026gt;0; end--){ int t=a[0]; a[0]=a[end]; a[end]=t; heapify(a,end,0); } } Scenario 2: Streaming top-k (Python, min-heap) Background: maintain top k values in a stream.\nimport heapq def topk(stream, k): h=[] for x in stream: if len(h)\u0026lt;k: heapq.heappush(h, x) else: if x\u0026gt;h[0]: heapq.heapreplace(h, x) return sorted(h, reverse=True) print(topk([5,1,9,3,12,4], 3)) # [12,9,5] Scenario 3: Go priority queue + sorting Background: use container/heap to build a heap-based sort.\npackage main import ( \u0026#34;container/heap\u0026#34; \u0026#34;fmt\u0026#34; ) type IntHeap []int func (h IntHeap) Len() int { return len(h) } func (h IntHeap) Less(i, j int) bool { return h[i] \u0026lt; h[j] } func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } func (h *IntHeap) Push(x interface{}) { *h = append(*h, x.(int)) } func (h *IntHeap) Pop() interface{} { old := *h; n := len(old); x := old[n-1]; *h = old[:n-1]; return x } func heapSort(a []int) []int { h := IntHeap(a) heap.Init(\u0026amp;h) res := make([]int, 0, len(a)) for h.Len()\u0026gt;0 { res = append(res, heap.Pop(\u0026amp;h).(int)) } return res // ascending } func main(){ fmt.Println(heapSort([]int{4,10,3,5,1})) } Scenario 4: Rust in-place heap sort pub fn heap_sort(a: \u0026amp;mut [i32]) { let n = a.len(); // build max-heap for i in (0..n/2).rev() { sift_down(a, i, n); } for end in (1..n).rev() { a.swap(0, end); sift_down(a, 0, end); } } fn sift_down(a: \u0026amp;mut [i32], mut i: usize, n: usize) { loop { let l = 2*i+1; let r = l+1; let mut largest = i; if l \u0026lt; n \u0026amp;\u0026amp; a[l] \u0026gt; a[largest] { largest = l; } if r \u0026lt; n \u0026amp;\u0026amp; a[r] \u0026gt; a[largest] { largest = r; } if largest == i { break; } a.swap(i, largest); i = largest; } } Scenario 5: JavaScript concise version function heapify(a, n, i){ while(true){ let l=2*i+1, r=2*i+2, largest=i; if(l\u0026lt;n \u0026amp;\u0026amp; a[l]\u0026gt;a[largest]) largest=l; if(r\u0026lt;n \u0026amp;\u0026amp; a[r]\u0026gt;a[largest]) largest=r; if(largest===i) break; [a[i],a[largest]]=[a[largest],a[i]]; i=largest; } } function heapSort(a){ const n=a.length; for(let i=Math.floor(n/2)-1;i\u0026gt;=0;i--) heapify(a,n,i); for(let end=n-1;end\u0026gt;0;end--){ [a[0],a[end]]=[a[end],a[0]]; heapify(a,end,0); } return a; } console.log(heapSort([4,10,3,5,1])); R - Reflection Complexity: time O(n log n) in worst/avg/best; space O(1). Alternatives: Need stability -\u0026gt; merge/TimSort. Better constants/cache -\u0026gt; quicksort often faster. Known range -\u0026gt; counting/bucket/radix. Why it works: Worst-case guarantees, suitable when degradation is unacceptable. In-place, good for memory-constrained environments. Shares structure with priority queues and streaming top-k. S - Summary Heap sort: in-place, unstable, worst-case O(n log n), higher constants than quicksort. Engineering: heaps are common for top-k/streaming; full heap sort is less visible in libraries but available (C++ make_heap/sort_heap). If you need stability or near-sorted optimization, use merge/TimSort; for low constants, use quick/Introsort; heap sort shines when you need in-place and worst-case guarantees. Heapify bottom-up is O(n); iterative sift-down avoids recursion. Practice Guide / Steps Implement heapify (bottom-up) and sift-down (iterative); verify child index math. If only top-k is required, maintain a min-heap of size k, space O(k). Benchmark random, reversed, heavy duplicates; record swap counts and time to observe cache effects. For stability, add original index as a secondary key (higher constant). Common Pitfalls and Notes Child index math is easy to get wrong: 2i+1, 2i+2; continue sifting after swap. Recursive sift-down uses O(log n) stack; iterative avoids stack risk. Heap sort is unstable; equal elements may reorder. Runnable Example: Minimal Python def heap_sort(a): n=len(a) def sift(i, size): while True: l,r=2*i+1,2*i+2; largest=i if l\u0026lt;size and a[l]\u0026gt;a[largest]: largest=l if r\u0026lt;size and a[r]\u0026gt;a[largest]: largest=r if largest==i: break a[i],a[largest]=a[largest],a[i]; i=largest for i in range(n//2-1,-1,-1): sift(i,n) for end in range(n-1,0,-1): a[0],a[end]=a[end],a[0] sift(0,end) return a print(heap_sort([4,10,3,5,1])) References and Further Reading CLRS \u0026ldquo;Introduction to Algorithms\u0026rdquo; heap sort C++ std::make_heap / std::sort_heap implementation William Cochran, \u0026ldquo;Heaps and Priority Queues\u0026rdquo; notes Meta Reading time: approx. 14 min SEO keywords: heap sort, in-place sorting, top-k, priority queue Meta description: sorting series (6) explaining heap sort, heapify/sift, complexity, and engineering trade-offs with multilingual examples. Call to Action (CTA) Compare quicksort vs heap sort on the same dataset and observe cache effects. If you need top-k, implement a min-heap and benchmark it. Follow the series: non-comparison sorting, TimSort/Introsort, selection guide. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/sorting/6.sorting-series-heap-sort/","summary":"Explain heap sort principles, complexity, and engineering scenarios; compare with quick/merge; include multilingual implementations and top-k examples.","title":"Sorting Series (6): Heap Sort - In-place O(n log n) with Worst-Case Guarantees"},{"content":" Sorting series post 5 focuses on quicksort: average O(n log n), in-place, low constant factors, but it needs pivot strategy and tail recursion optimization to avoid worst-case O(n^2) and deep stacks. This is the ACERS view from theory to engineering practice.\nTarget Readers Developers who want a production-ready quicksort. Readers curious about pivot choice, duplicates handling, tail-recursion/hybrid strategies. People who want to understand std::sort / Introsort design motivation. Background and Motivation Quicksort is often the default because it is in-place, cache-friendly, and fast, but worst-case O(n^2) and duplicates can hurt. Engineering practice uses random pivot, median-of-three, three-way partition, tail recursion, and insertion for small segments. A - Algorithm Theme: Achieve average O(n log n) with in-place and low constants, while mitigating degeneration.\nBasic example Array [3, 5, 2, 2, 8], pivot = 3:\nAfter partition -\u0026gt; [2,2,3,5,8], left \u0026lt; 3, right \u0026gt;= 3. Recurse on left and right segments. C - Concepts Key Concept Description Pivot selection Random, median-of-three (first/mid/last), or median-of-five to reduce degeneration. Partition strategy Lomuto (single side) is simple but swaps more; Hoare (two-side) swaps less; three-way partition handles duplicates. Duplicates Three-way partition (\u0026lt;, =, \u0026gt;) prevents degeneration with many equal keys. Tail recursion Always recurse on smaller side and iterate on larger side to keep stack O(log n). Hybrid strategy Use insertion for small segments; fall back to heap sort when recursion depth is too high (Introsort). Complexity\nAverage time O(n log n), worst-case O(n^2) with extreme pivots. Space: average recursion stack O(log n), worst-case O(n); tail recursion reduces risk. Unstable, in-place. E - Engineering Scenario 1: General backend sorting (Go) Background: dataset ~1e5, random distribution. Why: Go sort.Slice uses quick/heap hybrid; example includes insertion for small segments.\npackage main import \u0026#34;fmt\u0026#34; func insertion(a []int, l, r int) { for i := l+1; i \u0026lt;= r; i++ { key := a[i]; j := i-1 for j \u0026gt;= l \u0026amp;\u0026amp; a[j] \u0026gt; key { a[j+1]=a[j]; j-- } a[j+1]=key } } func partition(a []int, l, r int) int { pivot := a[(l+r)\u0026gt;\u0026gt;1] i, j := l, r for i \u0026lt;= j { for a[i] \u0026lt; pivot { i++ } for a[j] \u0026gt; pivot { j-- } if i \u0026lt;= j { a[i], a[j] = a[j], a[i]; i++; j-- } } return i } func quick(a []int, l, r int) { for r-l+1 \u0026gt; 16 { p := partition(a, l, r) if p-l \u0026lt; r-p { quick(a, l, p-1); l = p } else { quick(a, p, r); r = p-1 } } insertion(a, l, r) } func main(){ arr := []int{3,5,2,2,8,1,7} quick(arr,0,len(arr)-1) fmt.Println(arr) } Scenario 2: Many duplicates (Python three-way) Background: many equal values (e.g., bucketed IDs), two-way partition degrades. Why: three-way partition handles equal keys in one pass.\ndef quick3(a, l=0, r=None): if r is None: r = len(a)-1 while l \u0026lt; r: if r - l + 1 \u0026lt;= 16: for i in range(l+1, r+1): key=a[i]; j=i-1 while j\u0026gt;=l and a[j]\u0026gt;key: a[j+1]=a[j]; j-=1 a[j+1]=key return pivot = a[(l+r)//2] lt, i, gt = l, l, r while i \u0026lt;= gt: if a[i] \u0026lt; pivot: a[lt], a[i] = a[i], a[lt]; lt+=1; i+=1 elif a[i] \u0026gt; pivot: a[i], a[gt] = a[gt], a[i]; gt-=1 else: i+=1 if lt-l \u0026lt; r-gt: quick3(a, l, lt-1); l = gt+1 else: quick3(a, gt+1, r); r = lt-1 return a arr=[3,5,2,2,8,1,7,2,2] quick3(arr) print(arr) Scenario 3: C++ performance-sensitive partition (Hoare + median-of-three) Background: performance-sensitive, need fewer swaps and more robust pivot.\n#include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; int median3(vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int r){ int m = l + (r-l)/2; if(a[m] \u0026lt; a[l]) swap(a[m], a[l]); if(a[r] \u0026lt; a[l]) swap(a[r], a[l]); if(a[m] \u0026lt; a[r]) swap(a[m], a[r]); // a[r] = median return a[r]; } int partition(vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int r){ int pivot = median3(a,l,r); int i=l-1, j=r; while(true){ do{ i++; } while(a[i] \u0026lt; pivot); do{ j--; } while(a[j] \u0026gt; pivot); if(i\u0026gt;=j) break; swap(a[i], a[j]); } swap(a[i], a[r]); return i; } void quick(vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int r){ while(l \u0026lt; r){ if(r-l+1 \u0026lt;= 16){ for(int i=l+1;i\u0026lt;=r;++i){int key=a[i], j=i-1; while(j\u0026gt;=l \u0026amp;\u0026amp; a[j]\u0026gt;key){a[j+1]=a[j]; j--;} a[j+1]=key;} return; } int p = partition(a,l,r); if(p-l \u0026lt; r-p){ quick(a,l,p-1); l=p+1; } else{ quick(a,p+1,r); r=p-1; } } } Scenario 4: JavaScript small arrays with median-of-three Background: mid-size arrays, use median-of-three + insertion threshold.\nfunction insertion(a,l,r){ for(let i=l+1;i\u0026lt;=r;i++){ const key=a[i]; let j=i-1; while(j\u0026gt;=l \u0026amp;\u0026amp; a[j]\u0026gt;key){ a[j+1]=a[j]; j--; } a[j+1]=key; } } function partition(a,l,r){ const m = l + ((r-l)\u0026gt;\u0026gt;1); if(a[m]\u0026lt;a[l]) [a[m],a[l]]=[a[l],a[m]]; if(a[r]\u0026lt;a[l]) [a[r],a[l]]=[a[l],a[r]]; if(a[r]\u0026lt;a[m]) [a[r],a[m]]=[a[m],a[r]]; const pivot = a[r]; let i=l-1; for(let j=l;j\u0026lt;r;j++) if(a[j]\u0026lt;=pivot){ i++; [a[i],a[j]]=[a[j],a[i]]; } [a[i+1],a[r]]=[a[r],a[i+1]]; return i+1; } function quick(a,l=0,r=a.length-1){ while(l\u0026lt;r){ if(r-l+1\u0026lt;=16){ insertion(a,l,r); return; } const p=partition(a,l,r); if(p-l \u0026lt; r-p){ quick(a,l,p-1); l=p+1; } else{ quick(a,p+1,r); r=p-1; } } return a; } console.log(quick([3,5,2,2,8,1,7])); R - Reflection Complexity: average O(n log n), worst O(n^2); stack depth O(log n) avg; tail recursion + insertion threshold keep it bounded. Alternatives: Need stability or guaranteed upper bound -\u0026gt; merge/heap/TimSort. Known range -\u0026gt; counting/bucket/radix. Standard libraries: C++ std::sort uses Introsort (quick + heap + insertion); Python/Java use TimSort (stable). Why this works: Random/median-of-three reduces degeneration. Three-way partition handles duplicates. Tail recursion + insertion threshold reduces stack depth and constants. S - Summary Quicksort strengths: in-place, low constants, cache-friendly, average O(n log n). Risks: extreme pivot -\u0026gt; O(n^2); duplicates -\u0026gt; degeneration; unstable. Robust strategy: random/median-of-three pivot, three-way partition, insertion for small segments, tail recursion; use Introsort ideas when needed. Selection: stability/external -\u0026gt; merge/TimSort; memory tight + random -\u0026gt; quick/Introsort; duplicates -\u0026gt; three-way. Practice Guide / Steps Pivot: random or median-of-three; median-of-five for extra robustness. Duplicates: use three-way partition; otherwise two-way is fine. Set small segment threshold (e.g., 16/24) and insert; set depth limit to fall back to heap (Introsort). Test sets: random, reverse, all equal, many duplicates, large arrays. Common Pitfalls and Notes Lomuto partition swaps more; Hoare partition returns an index that changes recursion boundaries. Deep recursion can overflow stack: use tail recursion or iterative loops. Not handling duplicates causes degeneration; three-way is critical. Fixed first-element pivot degenerates on sorted data. Runnable Examples: Minimal Multilang Python (random pivot + three-way) import random def quick3(a, l=0, r=None): if r is None: r = len(a)-1 while l \u0026lt; r: if r-l+1 \u0026lt;= 16: for i in range(l+1, r+1): key=a[i]; j=i-1 while j\u0026gt;=l and a[j]\u0026gt;key: a[j+1]=a[j]; j-=1 a[j+1]=key return a pivot_i = random.randint(l, r) a[l], a[pivot_i] = a[pivot_i], a[l] pivot = a[l] lt, i, gt = l, l+1, r while i \u0026lt;= gt: if a[i] \u0026lt; pivot: a[lt], a[i] = a[i], a[lt]; lt+=1; i+=1 elif a[i] \u0026gt; pivot: a[i], a[gt] = a[gt], a[i]; gt-=1 else: i+=1 if lt-l \u0026lt; r-gt: quick3(a, l, lt-1); l = gt+1 else: quick3(a, gt+1, r); r = lt-1 return a arr=[3,5,2,2,8,1,7,2,2] quick3(arr); print(arr) C (Hoare partition + insertion threshold) #include \u0026lt;stdlib.h\u0026gt; void insertion(int *a,int l,int r){ for(int i=l+1;i\u0026lt;=r;i++){ int key=a[i], j=i-1; while(j\u0026gt;=l \u0026amp;\u0026amp; a[j]\u0026gt;key){ a[j+1]=a[j]; j--; } a[j+1]=key; } } int partition(int *a,int l,int r){ int pivot=a[(l+r)/2]; int i=l-1, j=r+1; while(1){ do{ i++; } while(a[i]\u0026lt;pivot); do{ j--; } while(a[j]\u0026gt;pivot); if(i\u0026gt;=j) return j; int t=a[i]; a[i]=a[j]; a[j]=t; } } void quick(int *a,int l,int r){ while(l\u0026lt;r){ if(r-l+1\u0026lt;=16){ insertion(a,l,r); return; } int p=partition(a,l,r); if(p-l \u0026lt; r-p){ quick(a,l,p); l=p+1; } else{ quick(a,p+1,r); r=p; } } } C++ (median-of-three + Hoare) int partition(vector\u0026lt;int\u0026gt;\u0026amp; a,int l,int r){ int m=l+(r-l)/2; if(a[m]\u0026lt;a[l]) swap(a[m],a[l]); if(a[r]\u0026lt;a[l]) swap(a[r],a[l]); if(a[r]\u0026lt;a[m]) swap(a[r],a[m]); int pivot=a[m]; int i=l-1,j=r+1; while(true){ do{i++;}while(a[i]\u0026lt;pivot); do{j--;}while(a[j]\u0026gt;pivot); if(i\u0026gt;=j) return j; swap(a[i],a[j]); } } Go (two-way, simplified) func Quick(a []int, l, r int){ for l\u0026lt;r { if r-l+1 \u0026lt;= 16 { insertion(a,l,r); return } p := partition(a,l,r) if p-l \u0026lt; r-p { Quick(a,l,p-1); l=p } else { Quick(a,p,r); r=p-1 } } } Rust (three-way) pub fn quick3(a: \u0026amp;mut [i32]) { fn insertion(a: \u0026amp;mut [i32]) { for i in 1..a.len() { let key=a[i]; let mut j=i as i32-1; while j\u0026gt;=0 \u0026amp;\u0026amp; a[j as usize]\u0026gt;key { a[(j+1) as usize]=a[j as usize]; j-=1; } a[(j+1) as usize]=key; } } fn sort(a: \u0026amp;mut [i32]) { let n=a.len(); if n\u0026lt;=16 { insertion(a); return; } let pivot=a[n/2]; let (mut lt, mut i, mut gt) = (0,0,n-1); while i\u0026lt;=gt { if a[i]\u0026lt;pivot { a.swap(lt,i); lt+=1; i+=1; } else if a[i]\u0026gt;pivot { a.swap(i,gt); if gt==0 {break;} gt-=1; } else { i+=1; } } sort(\u0026amp;mut a[..lt]); sort(\u0026amp;mut a[gt+1..]); } if !a.is_empty() { sort(a); } } JavaScript (median-of-three + insertion) function insertion(a,l,r){ for(let i=l+1;i\u0026lt;=r;i++){ const key=a[i]; let j=i-1; while(j\u0026gt;=l \u0026amp;\u0026amp; a[j]\u0026gt;key){ a[j+1]=a[j]; j--; } a[j+1]=key; } } function quick(a,l=0,r=a.length-1){ while(l\u0026lt;r){ if(r-l+1\u0026lt;=16){ insertion(a,l,r); return a; } const m=l+((r-l)\u0026gt;\u0026gt;1); if(a[m]\u0026lt;a[l]) [a[m],a[l]]=[a[l],a[m]]; if(a[r]\u0026lt;a[l]) [a[r],a[l]]=[a[l],a[r]]; if(a[r]\u0026lt;a[m]) [a[r],a[m]]=[a[m],a[r]]; const pivot=a[m]; let i=l, j=r; while(i\u0026lt;=j){ while(a[i]\u0026lt;pivot) i++; while(a[j]\u0026gt;pivot) j--; if(i\u0026lt;=j){ [a[i],a[j]]=[a[j],a[i]]; i++; j--; } } if(j-l \u0026lt; r-i){ quick(a,l,j); l=i; } else { quick(a,i,r); r=j; } } return a; } console.log(quick([3,5,2,2,8,1,7])); Best Practices Default to language built-in sort; if custom, include random/median-of-three, three-way for duplicates, insertion threshold, tail recursion. Use merge/TimSort for stability; consider Introsort for strict upper bounds. Benchmark on random, reverse, all equal, many duplicates, and large arrays. Conclusion Quicksort is fast and in-place but must use pivot strategy and three-way partition to avoid degeneration. Tail recursion + insertion thresholds are standard engineering practice; fall back to heap if depth grows (Introsort). Selection rules: stability/external -\u0026gt; merge/TimSort; memory-tight random -\u0026gt; quick/Introsort; many duplicates -\u0026gt; three-way. References and Further Reading Hoare, \u0026ldquo;Quicksort\u0026rdquo; (1961) Bentley \u0026amp; McIlroy, \u0026ldquo;Engineering a Sort Function\u0026rdquo; (1993) C++ std::sort and std::stable_sort implementation notes Meta Reading time: approx. 16 min SEO keywords: quicksort, pivot selection, three-way partition, tail recursion, Introsort Meta description: sorting series (5) explaining pivot strategies, duplicate handling, tail recursion, and hybrid optimizations with multilingual implementations. Call to Action (CTA) Benchmark random vs fixed pivots on your real data; compare performance. Add \u0026ldquo;insertion threshold + tail recursion\u0026rdquo; to your implementation and measure stack depth and time. Follow the series: heap sort, non-comparison, TimSort/Introsort, selection guide. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/sorting/5.sorting-series-quick-sort/","summary":"Comprehensive quicksort guide: pivot selection, three-way partitioning, tail recursion optimization, hybrid sorting practices, with multilingual implementations and engineering guidance.","title":"Sorting Series (5): Quick Sort - Pivot Strategy, Tail Recursion, Engineering Practice"},{"content":"LeetCode 1: Two Sum Summary Find two indices such that nums[i] + nums[j] = target. Use a hash map for O(n) time.\nApproach Iterate and store value -\u0026gt; index. For each number x, check if target - x exists.\nComplexity Time: O(n) Space: O(n) Python reference implementation def two_sum(nums, target): seen = {} for i, x in enumerate(nums): y = target - x if y in seen: return [seen[y], i] seen[x] = i ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/1-two-sum/","summary":"\u003ch1 id=\"leetcode-1-two-sum\"\u003eLeetCode 1: Two Sum\u003c/h1\u003e\n\u003ch2 id=\"summary\"\u003eSummary\u003c/h2\u003e\n\u003cp\u003eFind two indices such that \u003ccode\u003enums[i] + nums[j] = target\u003c/code\u003e. Use a hash map for O(n) time.\u003c/p\u003e\n\u003ch2 id=\"approach\"\u003eApproach\u003c/h2\u003e\n\u003cp\u003eIterate and store \u003ccode\u003evalue -\u0026gt; index\u003c/code\u003e. For each number \u003ccode\u003ex\u003c/code\u003e, check if \u003ccode\u003etarget - x\u003c/code\u003e exists.\u003c/p\u003e\n\u003ch2 id=\"complexity\"\u003eComplexity\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eTime: O(n)\u003c/li\u003e\n\u003cli\u003eSpace: O(n)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"python-reference-implementation\"\u003ePython reference implementation\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003etwo_sum\u003c/span\u003e(nums, target):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    seen \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e {}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e i, x \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e enumerate(nums):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        y \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e target \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e x\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e y \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e seen:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e [seen[y], i]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        seen[x] \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e i\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"LeetCode 1: Two Sum (Hash Map ACERS Summary)"},{"content":"LeetCode 2300: Successful Pairs of Spells and Potions Summary For each spell, count how many potions make spell * potion \u0026gt;= success. Sort potions and binary search the threshold.\nApproach Sort potions. For each spell, compute need = ceil(success / spell). Use binary search to find the first potion \u0026gt;= need. Complexity Time: O(n log n) Space: O(1) extra (or O(n) if sorting a copy) Python reference implementation import bisect import math def successful_pairs(spells, potions, success): potions = sorted(potions) n = len(potions) res = [] for s in spells: need = (success + s - 1) // s idx = bisect.bisect_left(potions, need) res.append(n - idx) return res ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/2300-successful-pairs-of-spells-and-potions/","summary":"\u003ch1 id=\"leetcode-2300-successful-pairs-of-spells-and-potions\"\u003eLeetCode 2300: Successful Pairs of Spells and Potions\u003c/h1\u003e\n\u003ch2 id=\"summary\"\u003eSummary\u003c/h2\u003e\n\u003cp\u003eFor each spell, count how many potions make \u003ccode\u003espell * potion \u0026gt;= success\u003c/code\u003e. Sort potions and binary search the threshold.\u003c/p\u003e\n\u003ch2 id=\"approach\"\u003eApproach\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eSort potions.\u003c/li\u003e\n\u003cli\u003eFor each spell, compute \u003ccode\u003eneed = ceil(success / spell)\u003c/code\u003e.\u003c/li\u003e\n\u003cli\u003eUse binary search to find the first potion \u0026gt;= need.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"complexity\"\u003eComplexity\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eTime: O(n log n)\u003c/li\u003e\n\u003cli\u003eSpace: O(1) extra (or O(n) if sorting a copy)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"python-reference-implementation\"\u003ePython reference implementation\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e bisect\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e math\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esuccessful_pairs\u003c/span\u003e(spells, potions, success):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    potions \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e sorted(potions)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    n \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e len(potions)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    res \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e []\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e s \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e spells:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        need \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e (success \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e s \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e) \u003cspan style=\"color:#f92672\"\u003e//\u003c/span\u003e s\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        idx \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e bisect\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003ebisect_left(potions, need)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        res\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003eappend(n \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e idx)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e res\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"LeetCode 2300: Successful Pairs of Spells and Potions"},{"content":"LeetCode 2379: Minimum Recolors to Get K Consecutive Black Blocks Summary Given a string of \u0026lsquo;B\u0026rsquo; and \u0026lsquo;W\u0026rsquo;, find the minimum recolors to make a substring of length k all black.\nApproach Use a sliding window of length k and count the number of whites in the window. The minimum whites across all windows is the answer.\nComplexity Time: O(n) Space: O(1) Python reference implementation def minimum_recolors(blocks, k): whites = sum(1 for c in blocks[:k] if c == \u0026#39;W\u0026#39;) ans = whites for i in range(k, len(blocks)): if blocks[i-k] == \u0026#39;W\u0026#39;: whites -= 1 if blocks[i] == \u0026#39;W\u0026#39;: whites += 1 ans = min(ans, whites) return ans ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/2379-minimum-recolors-to-get-k-consecutive-black-blocks/","summary":"\u003ch1 id=\"leetcode-2379-minimum-recolors-to-get-k-consecutive-black-blocks\"\u003eLeetCode 2379: Minimum Recolors to Get K Consecutive Black Blocks\u003c/h1\u003e\n\u003ch2 id=\"summary\"\u003eSummary\u003c/h2\u003e\n\u003cp\u003eGiven a string of \u0026lsquo;B\u0026rsquo; and \u0026lsquo;W\u0026rsquo;, find the minimum recolors to make a substring of length \u003ccode\u003ek\u003c/code\u003e all black.\u003c/p\u003e\n\u003ch2 id=\"approach\"\u003eApproach\u003c/h2\u003e\n\u003cp\u003eUse a sliding window of length \u003ccode\u003ek\u003c/code\u003e and count the number of whites in the window. The minimum whites across all windows is the answer.\u003c/p\u003e\n\u003ch2 id=\"complexity\"\u003eComplexity\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eTime: O(n)\u003c/li\u003e\n\u003cli\u003eSpace: O(1)\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"python-reference-implementation\"\u003ePython reference implementation\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eminimum_recolors\u003c/span\u003e(blocks, k):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    whites \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e sum(\u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e c \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e blocks[:k] \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e c \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;W\u0026#39;\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ans \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e whites\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e i \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e range(k, len(blocks)):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e blocks[i\u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003ek] \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;W\u0026#39;\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            whites \u003cspan style=\"color:#f92672\"\u003e-=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e blocks[i] \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#39;W\u0026#39;\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            whites \u003cspan style=\"color:#f92672\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ans \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e min(ans, whites)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e ans\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"LeetCode 2379: Minimum Recolors to Get K Consecutive Black Blocks"},{"content":"LeetCode 2841: Maximum Sum of Almost Unique Subarray Summary Given an array, window size k, and threshold m, find the maximum sum of any length-k subarray that contains at least m distinct elements.\nApproach Use a sliding window with a frequency map, track window sum and number of distinct values.\nComplexity Time: O(n) Space: O(n) for frequency map Python reference implementation def max_sum_almost_unique(nums, m, k): from collections import defaultdict count = defaultdict(int) distinct = 0 window_sum = 0 ans = 0 for i, x in enumerate(nums): window_sum += x if count[x] == 0: distinct += 1 count[x] += 1 if i \u0026gt;= k: y = nums[i - k] window_sum -= y count[y] -= 1 if count[y] == 0: distinct -= 1 if i \u0026gt;= k - 1 and distinct \u0026gt;= m: ans = max(ans, window_sum) return ans ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/leetcode/2841-maximum-sum-of-almost-unique-subarray/","summary":"\u003ch1 id=\"leetcode-2841-maximum-sum-of-almost-unique-subarray\"\u003eLeetCode 2841: Maximum Sum of Almost Unique Subarray\u003c/h1\u003e\n\u003ch2 id=\"summary\"\u003eSummary\u003c/h2\u003e\n\u003cp\u003eGiven an array, window size \u003ccode\u003ek\u003c/code\u003e, and threshold \u003ccode\u003em\u003c/code\u003e, find the maximum sum of any length-\u003ccode\u003ek\u003c/code\u003e subarray that contains at least \u003ccode\u003em\u003c/code\u003e distinct elements.\u003c/p\u003e\n\u003ch2 id=\"approach\"\u003eApproach\u003c/h2\u003e\n\u003cp\u003eUse a sliding window with a frequency map, track window sum and number of distinct values.\u003c/p\u003e\n\u003ch2 id=\"complexity\"\u003eComplexity\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eTime: O(n)\u003c/li\u003e\n\u003cli\u003eSpace: O(n) for frequency map\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"python-reference-implementation\"\u003ePython reference implementation\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edef\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emax_sum_almost_unique\u003c/span\u003e(nums, m, k):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003efrom\u003c/span\u003e collections \u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e defaultdict\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    count \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e defaultdict(int)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    distinct \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    window_sum \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    ans \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003efor\u003c/span\u003e i, x \u003cspan style=\"color:#f92672\"\u003ein\u003c/span\u003e enumerate(nums):\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        window_sum \u003cspan style=\"color:#f92672\"\u003e+=\u003c/span\u003e x\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e count[x] \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            distinct \u003cspan style=\"color:#f92672\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        count[x] \u003cspan style=\"color:#f92672\"\u003e+=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e i \u003cspan style=\"color:#f92672\"\u003e\u0026gt;=\u003c/span\u003e k:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            y \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e nums[i \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e k]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            window_sum \u003cspan style=\"color:#f92672\"\u003e-=\u003c/span\u003e y\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            count[y] \u003cspan style=\"color:#f92672\"\u003e-=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e count[y] \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                distinct \u003cspan style=\"color:#f92672\"\u003e-=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e i \u003cspan style=\"color:#f92672\"\u003e\u0026gt;=\u003c/span\u003e k \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003eand\u003c/span\u003e distinct \u003cspan style=\"color:#f92672\"\u003e\u0026gt;=\u003c/span\u003e m:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            ans \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e max(ans, window_sum)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e ans\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"LeetCode 2841: Maximum Sum of Almost Unique Subarray"},{"content":" Sorting series post 4 focuses on merge sort: classic divide and conquer, stable, O(n log n) time, with the cost of O(n) extra space. It is the foundation for external sorting and many stable library sorts.\nTarget Readers Engineers who need stability and can afford O(n) extra space. Learners building divide-and-conquer foundations for quicksort/TimSort. People working with huge files or streams and want external merge knowledge. Background and Motivation Merge sort is O(n log n) on all inputs, unaffected by pivot degeneration. The trade-off is O(n) extra space; in-place variants are complex and costly. External sorting (data larger than RAM) uses \u0026ldquo;chunk sort + k-way merge\u0026rdquo; based on merge ideas. A - Algorithm Problem: Sort a comparable sequence, stable, in O(n log n).\nSteps (top-down)\nSplit: recursively divide the array into halves. Conquer: sort left and right halves. Merge: merge two sorted halves using a buffer. Basic example Array [5,2,4,6,1,3]:\nSplit into [5,2,4] and [6,1,3], then split further. Merge [2,4,5] and [1,3,6] -\u0026gt; [1,2,3,4,5,6] (stable preserves order). C - Concepts Key Concept Description Divide and conquer Split into subproblems, solve and merge. Stability When equal, take from left first to preserve relative order. Space Typical implementation uses O(n) buffer; bottom-up still needs buffer. Variants Bottom-up merge, block merge, external k-way merge. Complexity\nTime: T(n) = 2T(n/2) + O(n) =\u0026gt; O(n log n) (best/avg/worst). Space: O(n) buffer (external sort depends on block size). E - Engineering Scenario 1: Stable multi-key sorting (Python) Background: sort logs by date then user_id, need stability. Why: Python\u0026rsquo;s list.sort / sorted use TimSort, which is stable and adaptive, so equal keys keep their original order. Why this example: it shows a real-world multi-key sort where stability matters, and demonstrates that you can rely on the built-in sort instead of hand-writing a merge routine.\nfrom operator import itemgetter logs = [(\u0026#34;2025-11-21\u0026#34;, \u0026#34;u2\u0026#34;), (\u0026#34;2025-11-21\u0026#34;, \u0026#34;u1\u0026#34;), (\u0026#34;2025-11-20\u0026#34;, \u0026#34;u3\u0026#34;)] logs.sort(key=itemgetter(0,1)) print(logs) Scenario 2: External sorting for large files (C++) Background: sort a 10GB integer file with 512MB RAM. Why: chunk sort + k-way merge keeps memory bounded; add a global sequence id to preserve stability for equal keys. Approach: read fixed-size chunks, sort in memory, spill to temp files, then k-way merge with a min-heap. Notes: input is one integer per line; output is sorted integers; chunk_items controls memory use.\n#include \u0026lt;algorithm\u0026gt; // std::sort #include \u0026lt;cstdint\u0026gt; // uint64_t fixed-width integer #include \u0026lt;cstdio\u0026gt; // std::remove for deleting temp files #include \u0026lt;fstream\u0026gt; // std::ifstream/std::ofstream #include \u0026lt;iostream\u0026gt; // std::cerr #include \u0026lt;queue\u0026gt; // std::priority_queue #include \u0026lt;string\u0026gt; // std::string, std::to_string #include \u0026lt;vector\u0026gt; // std::vector // A simple aggregate type. In C++, \u0026#34;struct\u0026#34; defaults to public members. // \u0026#34;uint64_t\u0026#34; is a fixed-width unsigned integer used for a stable tie-breaker. struct Record { int value; uint64_t seq; }; // Each heap entry remembers which temp file it came from. // \u0026#34;size_t\u0026#34; is an unsigned type for sizes/indices. struct HeapItem { Record rec; size_t file_index; }; // Comparator functor for std::priority_queue (a max-heap by default). // By reversing the comparison, we effectively get a min-heap by (value, seq). struct HeapCmp { bool operator()(const HeapItem\u0026amp; a, const HeapItem\u0026amp; b) const { if (a.rec.value != b.rec.value) return a.rec.value \u0026gt; b.rec.value; return a.rec.seq \u0026gt; b.rec.seq; } }; // \u0026#34;static\u0026#34; gives internal linkage (file-local). The \u0026#34;\u0026gt;\u0026gt;\u0026#34; operator reads tokens. // It returns false when the stream fails (EOF or invalid format). static bool read_record(std::ifstream\u0026amp; in, Record\u0026amp; out) { return static_cast\u0026lt;bool\u0026gt;(in \u0026gt;\u0026gt; out.value \u0026gt;\u0026gt; out.seq); } int main(int argc, char** argv) { // argc is the count of arguments; argv is the array of C-strings. if (argc \u0026lt; 3) { std::cerr \u0026lt;\u0026lt; \u0026#34;Usage: \u0026#34; \u0026lt;\u0026lt; argv[0] \u0026lt;\u0026lt; \u0026#34; \u0026lt;input\u0026gt; \u0026lt;output\u0026gt; [chunk_items]\\n\u0026#34;; std::cerr \u0026lt;\u0026lt; \u0026#34;Input format: one integer per line.\\n\u0026#34;; return 1; } std::string input_path = argv[1]; std::string output_path = argv[2]; size_t chunk_items = 1000000; // Default chunk size (items, not bytes). if (argc \u0026gt;= 4) { // std::stoull converts a string to unsigned long long. chunk_items = static_cast\u0026lt;size_t\u0026gt;(std::stoull(argv[3])); if (chunk_items == 0) { std::cerr \u0026lt;\u0026lt; \u0026#34;chunk_items must be \u0026gt; 0\\n\u0026#34;; return 1; } } std::ifstream in(input_path); if (!in) { std::cerr \u0026lt;\u0026lt; \u0026#34;Failed to open input: \u0026#34; \u0026lt;\u0026lt; input_path \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 1; } std::vector\u0026lt;std::string\u0026gt; temp_files; std::vector\u0026lt;Record\u0026gt; buffer; buffer.reserve(chunk_items); uint64_t seq = 0; // Monotonic sequence to preserve stability across chunks. while (true) { buffer.clear(); int v; // Read up to chunk_items values; \u0026#34;\u0026amp;\u0026amp;\u0026#34; short-circuits on EOF. while (buffer.size() \u0026lt; chunk_items \u0026amp;\u0026amp; (in \u0026gt;\u0026gt; v)) { buffer.push_back(Record{v, seq++}); // Aggregate initialization. } if (buffer.empty()) break; // std::sort uses the comparator lambda: \u0026#34;[]\u0026#34; means no captures. std::sort(buffer.begin(), buffer.end(), [](const Record\u0026amp; a, const Record\u0026amp; b) { if (a.value != b.value) return a.value \u0026lt; b.value; return a.seq \u0026lt; b.seq; }); std::string tmp_name = \u0026#34;chunk_\u0026#34; + std::to_string(temp_files.size()) + \u0026#34;.tmp\u0026#34;; std::ofstream out(tmp_name); if (!out) { std::cerr \u0026lt;\u0026lt; \u0026#34;Failed to create temp file: \u0026#34; \u0026lt;\u0026lt; tmp_name \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 1; } for (const auto\u0026amp; rec : buffer) { out \u0026lt;\u0026lt; rec.value \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; rec.seq \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } temp_files.push_back(tmp_name); if (!in) break; } // Handle empty input: just create an empty output file. if (temp_files.empty()) { std::ofstream out(output_path); return 0; } std::vector\u0026lt;std::ifstream\u0026gt; inputs; inputs.reserve(temp_files.size()); for (const auto\u0026amp; name : temp_files) { // emplace_back constructs the ifstream in-place. inputs.emplace_back(name); if (!inputs.back()) { std::cerr \u0026lt;\u0026lt; \u0026#34;Failed to open temp file: \u0026#34; \u0026lt;\u0026lt; name \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 1; } } // priority_queue\u0026lt;value, container, comparator\u0026gt; std::priority_queue\u0026lt;HeapItem, std::vector\u0026lt;HeapItem\u0026gt;, HeapCmp\u0026gt; heap; for (size_t i = 0; i \u0026lt; inputs.size(); ++i) { Record rec; if (read_record(inputs[i], rec)) { heap.push(HeapItem{rec, i}); } } std::ofstream out(output_path); if (!out) { std::cerr \u0026lt;\u0026lt; \u0026#34;Failed to open output: \u0026#34; \u0026lt;\u0026lt; output_path \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return 1; } // k-way merge: always take the smallest (value, seq) from the heap. while (!heap.empty()) { HeapItem top = heap.top(); heap.pop(); out \u0026lt;\u0026lt; top.rec.value \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; Record next; if (read_record(inputs[top.file_index], next)) { heap.push(HeapItem{next, top.file_index}); } } // Clean up temp files using std::remove from \u0026lt;cstdio\u0026gt;. for (const auto\u0026amp; name : temp_files) { std::remove(name.c_str()); } return 0; } Example (simulate external sorting with tiny memory): Use a small chunk_items so the program must spill to temp files and perform a k-way merge, just like the 10GB/512MB scenario.\nInput (numbers.txt):\n5 1 5 2 2 3 Compile:\ng++ -O2 -std=c++17 external_sort.cpp -o external_sort Run (set chunk_items=3 to force multiple chunks):\n./external_sort numbers.txt sorted.txt 3 Output (sorted.txt):\n1 2 2 3 5 Scenario 3: Frontend stable sorting (JavaScript) Background: table must keep original order for equal keys. Why: modern browser sort is mostly stable; if unsure, use a stable merge.\nfunction mergeSort(arr){ if(arr.length\u0026lt;=1) return arr; const mid = arr.length\u0026gt;\u0026gt;1; const left = mergeSort(arr.slice(0,mid)); const right = mergeSort(arr.slice(mid)); const res=[]; let i=0,j=0; while(i\u0026lt;left.length \u0026amp;\u0026amp; j\u0026lt;right.length){ if(left[i].key \u0026lt;= right[j].key) res.push(left[i++]); else res.push(right[j++]); } return res.concat(left.slice(i)).concat(right.slice(j)); } console.log(mergeSort([{key:1},{key:1},{key:0}])); Scenario 4: Stable backend sorting (Go) Background: stable sort by multiple fields. Why: sort.SliceStable is merge-based and stable.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sort\u0026#34; ) type Item struct{ Date string; User string } func main(){ items := []Item{{\u0026#34;2025-11-21\u0026#34;,\u0026#34;u2\u0026#34;},{\u0026#34;2025-11-21\u0026#34;,\u0026#34;u1\u0026#34;},{\u0026#34;2025-11-20\u0026#34;,\u0026#34;u3\u0026#34;}} sort.SliceStable(items, func(i, j int) bool { if items[i].Date == items[j].Date { return items[i].User \u0026lt; items[j].User } return items[i].Date \u0026lt; items[j].Date }) fmt.Println(items) } R - Reflection Complexity analysis: time O(n log n), space O(n); external sort cost dominated by I/O and block size. Alternatives: vs quicksort: quick is in-place and small-constant but unstable and can degrade; merge is stable and bounded. vs heap sort: heap is in-place and unstable with poor cache behavior; merge is better for stability/external. vs TimSort: TimSort is faster on nearly sorted data and stable but more complex; merge is its base. Why it is preferred: stable, predictable O(n log n), and the default for external sorting. S - Summary Merge sort provides stable and predictable O(n log n) with O(n) extra space. External sorting, stable multi-key sorting, and many stable libraries rely on merge ideas. Bottom-up merge avoids recursion depth but still needs buffers. If input is nearly sorted and you want more speed, consider TimSort; if memory is tight and stability is not needed, use quick/heap. Evaluate stability needs, memory budget, data size, and I/O cost. Practice Guide / Steps Decide stability and memory budget: if O(n) buffer is ok, use merge/stable libraries; otherwise quick/heap. Choose implementation: top-down is simplest; bottom-up avoids deep recursion. Ensure stability in merge: when equal, pick from left. Edge tests: empty, single, all equal, reverse, heavy duplicates. Runnable Examples: Multilingual Implementations Python (top-down) def merge_sort(a): if len(a) \u0026lt;= 1: return a mid = len(a)//2 left = merge_sort(a[:mid]) right = merge_sort(a[mid:]) i=j=0; res=[] while i \u0026lt; len(left) and j \u0026lt; len(right): if left[i] \u0026lt;= right[j]: res.append(left[i]); i+=1 else: res.append(right[j]); j+=1 res.extend(left[i:]); res.extend(right[j:]) return res print(merge_sort([5,2,4,6,1,3])) C (bottom-up) #include \u0026lt;stdlib.h\u0026gt; void merge(int *a, int *buf, int l, int m, int r){ int i=l, j=m, k=l; while(i\u0026lt;m \u0026amp;\u0026amp; j\u0026lt;r){ if(a[i] \u0026lt;= a[j]) buf[k++] = a[i++]; else buf[k++] = a[j++]; } while(i\u0026lt;m) buf[k++] = a[i++]; while(j\u0026lt;r) buf[k++] = a[j++]; for(int t=l; t\u0026lt;r; ++t) a[t]=buf[t]; } void merge_sort(int *a, int n){ int *buf = malloc(sizeof(int)*n); for(int width=1; width\u0026lt;n; width*=2){ for(int i=0; i\u0026lt;n; i+=2*width){ int l=i, m=i+width\u0026lt; n? i+width: n, r=i+2*width\u0026lt; n? i+2*width: n; merge(a, buf, l, m, r); } } free(buf); } C++ (top-down) void merge(vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int m, int r, vector\u0026lt;int\u0026gt;\u0026amp; buf){ int i=l,j=m,k=l; while(i\u0026lt;m \u0026amp;\u0026amp; j\u0026lt;r){ if(a[i]\u0026lt;=a[j]) buf[k++]=a[i++]; else buf[k++]=a[j++]; } while(i\u0026lt;m) buf[k++]=a[i++]; while(j\u0026lt;r) buf[k++]=a[j++]; for(int t=l;t\u0026lt;r;++t) a[t]=buf[t]; } void merge_sort(vector\u0026lt;int\u0026gt;\u0026amp; a, int l, int r, vector\u0026lt;int\u0026gt;\u0026amp; buf){ if(r-l\u0026lt;=1) return; int m = l + (r-l)/2; merge_sort(a,l,m,buf); merge_sort(a,m,r,buf); merge(a,l,m,r,buf); } Go (top-down) func mergeSort(a []int) []int { if len(a) \u0026lt;= 1 { return a } mid := len(a)/2 left := mergeSort(a[:mid]) right := mergeSort(a[mid:]) res := make([]int, 0, len(a)) i, j := 0, 0 for i \u0026lt; len(left) \u0026amp;\u0026amp; j \u0026lt; len(right) { if left[i] \u0026lt;= right[j] { res = append(res, left[i]); i++ } else { res = append(res, right[j]); j++ } } res = append(res, left[i:]...) res = append(res, right[j:]...) return res } Rust (top-down with buffer) fn merge_sort(a: \u0026amp;mut [i32]) { let n = a.len(); if n \u0026lt;= 1 { return; } let mid = n/2; merge_sort(\u0026amp;mut a[..mid]); merge_sort(\u0026amp;mut a[mid..]); let mut buf = a.to_vec(); merge(\u0026amp;a[..mid], \u0026amp;a[mid..], \u0026amp;mut buf[..]); a.copy_from_slice(\u0026amp;buf); } fn merge(left: \u0026amp;[i32], right: \u0026amp;[i32], out: \u0026amp;mut [i32]) { let (mut i, mut j, mut k) = (0,0,0); while i \u0026lt; left.len() \u0026amp;\u0026amp; j \u0026lt; right.len() { if left[i] \u0026lt;= right[j] { out[k]=left[i]; i+=1; } else { out[k]=right[j]; j+=1; } k+=1; } if i \u0026lt; left.len() { out[k..k+left.len()-i].copy_from_slice(\u0026amp;left[i..]); } if j \u0026lt; right.len() { out[k..k+right.len()-j].copy_from_slice(\u0026amp;right[j..]); } } JavaScript (top-down) function mergeSort(a){ if(a.length\u0026lt;=1) return a; const mid = a.length\u0026gt;\u0026gt;1; const left = mergeSort(a.slice(0,mid)); const right = mergeSort(a.slice(mid)); const res=[]; let i=0,j=0; while(i\u0026lt;left.length \u0026amp;\u0026amp; j\u0026lt;right.length){ if(left[i] \u0026lt;= right[j]) res.push(left[i++]); else res.push(right[j++]); } return res.concat(left.slice(i)).concat(right.slice(j)); } console.log(mergeSort([5,2,4,6,1,3])); Common Pitfalls and Notes Recursion depth: for large n use bottom-up iteration or stack tuning. Space usage: evaluate O(n) buffer; external sort requires careful block sizing and merge fan-in. Stability: when equal, take left to preserve order. Performance: use double-buffering or alternating buffers to reduce copies. Best Practices Use stable library sort when available (Python/Java object sort, Go SliceStable). Factor out a merge helper to enforce stability; use bottom-up for large arrays. External sort: control chunk size, use priority queue for k-way merge, batch I/O. For nearly sorted data, consider TimSort; merge is its foundation. Conclusion Merge sort is stable and predictably O(n log n), ideal for stable multi-key sorting and external sorting. Extra space is the main trade-off; in-place variants are complex and uncommon. Bottom-up merge avoids recursion; external merge is essential for huge data. References and Further Reading CLRS \u0026ldquo;Introduction to Algorithms\u0026rdquo; merge sort TimSort paper and CPython/Java sources (run merging) PostgreSQL tuplesort external sorting implementation Meta Reading time: approx. 15 min SEO keywords: merge sort, stable sort, external sorting, divide and conquer Meta description: sorting series (4) explaining merge sort stability, space trade-offs, external sorting, and multilingual implementations. Call to Action (CTA) Benchmark built-in stable sort vs your merge implementation on your dataset. If sorting large files, prototype chunk + k-way merge and measure I/O cost. Follow the series: quicksort, heap sort, non-comparison, TimSort/Introsort, selection guide. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/sorting/4.sorting-series-merge-sort/","summary":"Systematic explanation of merge sort principles, stability, space trade-offs, and engineering scenarios with Python/C/C++/Go/Rust/JS implementations and external sorting guidance.","title":"Sorting Series (4): Merge Sort - Stable Divide and Conquer and External Sorting"},{"content":" Sorting series post 3 focuses on Shell sort: grouped insertion with decreasing gaps reduces worst-case O(n^2) toward around O(n log^2 n), making it a key step in understanding \u0026ldquo;local order to global order.\u0026rdquo;\nTarget Readers Learners who know insertion sort and want its higher-level optimization. Engineers needing in-place sorting for mid-size data. People explaining the impact of gap sequences in talks or courses. Background and Motivation Insertion sort is fast when nearly sorted but remains O(n^2) on random arrays. Shell sort uses grouping + shrinking gaps so elements move toward their approximate positions early. Gap sequence choice directly affects performance and complexity. A - Algorithm Problem: Sort a length-n comparable sequence in-place.\nCore steps (gap starts at n/2)\nChoose initial gap; split array into gap-based subsequences. Run insertion sort on each subsequence (step size = gap). Decrease gap and repeat until gap = 1 (equivalent to insertion). Basic example Array [9, 8, 3, 7, 5, 6, 4, 1], gap sequence 4 -\u0026gt; 2 -\u0026gt; 1:\ngap=4: subsequences (0,4),(1,5),(2,6),(3,7), insert sort each to roughly position elements. gap=2: finer groups and more insertion. gap=1: final insertion pass for full order. C - Concepts Key Concept Description Gap sequence Common choices: n/2, Knuth (1,4,13,40,\u0026hellip;), Sedgewick, etc. Determines comparison bounds. Grouped insertion Insertion sort on gap-separated subsequences, moves distant elements early. In-place Uses constant extra space. Stability Classic Shell sort is not stable (cross-gap swaps can reorder equals). Complexity range\nWorst-case depends on gap sequence; simple n/2 can still be O(n^2). Good sequences (e.g., Sedgewick) achieve O(n^(4/3)) or O(n log^2 n), often ~O(n^{1.2-1.3}) in practice. Space: O(1). E - Engineering Scenario 1: Mid-size, memory-sensitive sort (C) Background: embedded/backend arrays (1e4~1e5), need in-place with low memory. Why: Shell sort is in-place with low constants; often better than pure insertion; more stable performance than quick/heap on some distributions.\nvoid shell_sort(int *a, int n) { // Knuth sequence: 1,4,13,40,... until \u0026lt; n/3 int gap = 1; while (gap \u0026lt; n/3) gap = gap * 3 + 1; for (; gap \u0026gt;= 1; gap /= 3) { for (int i = gap; i \u0026lt; n; ++i) { int temp = a[i], j = i; while (j \u0026gt;= gap \u0026amp;\u0026amp; a[j-gap] \u0026gt; temp) { a[j] = a[j-gap]; j -= gap; } a[j] = temp; } } } Scenario 2: Nearly sorted business lists (Python) Background: list appends a few elements; total size \u0026lt;= 1e5. Why: gentle gap sequence moves distant elements into place, then gap=1 finishes with insertion.\ndef shell_sort(arr): n = len(arr) gap = 1 while gap \u0026lt; n // 3: gap = 3 * gap + 1 # Knuth while gap \u0026gt;= 1: for i in range(gap, n): temp = arr[i] j = i while j \u0026gt;= gap and arr[j - gap] \u0026gt; temp: arr[j] = arr[j - gap] j -= gap arr[j] = temp gap //= 3 return arr data = [9,8,3,7,5,6,4,1] print(shell_sort(data)) Scenario 3: Go backend batch sorting Background: per-request sorting length 1e3~1e4; in-place to reduce GC. Why: custom Shell sort as an alternative to reduce allocations.\npackage main import \u0026#34;fmt\u0026#34; func shellSort(a []int) { gap := 1 for gap \u0026lt; len(a)/3 { gap = gap*3 + 1 } for gap \u0026gt;= 1 { for i := gap; i \u0026lt; len(a); i++ { tmp, j := a[i], i for j \u0026gt;= gap \u0026amp;\u0026amp; a[j-gap] \u0026gt; tmp { a[j] = a[j-gap] j -= gap } a[j] = tmp } gap /= 3 } } func main(){ arr := []int{9,8,3,7,5,6,4,1}; shellSort(arr); fmt.Println(arr) } Scenario 4: Frontend large array but low memory (JavaScript) Background: browser handling thousands of records, avoid allocations. Why: in-place and short implementation; Knuth sequence works well.\nfunction shellSort(a){ let gap = 1; while (gap \u0026lt; a.length/3) gap = gap*3 + 1; while (gap \u0026gt;= 1){ for (let i = gap; i \u0026lt; a.length; i++){ const tmp = a[i]; let j = i; while (j \u0026gt;= gap \u0026amp;\u0026amp; a[j-gap] \u0026gt; tmp){ a[j] = a[j-gap]; j -= gap; } a[j] = tmp; } gap = Math.floor(gap/3); } return a; } console.log(shellSort([9,8,3,7,5,6,4,1])); R - Reflection Complexity: Time depends on gap sequence. Knuth works well in practice but can still be O(n^2) worst-case. Sedgewick improves to O(n^(4/3)) bounds. Space: O(1). Alternatives: vs insertion: Shell reduces long-distance moves; gap=1 returns to insertion. vs quick/heap: Shell is more cache-friendly but lacks strict O(n log n) bounds. vs merge: merge is stable but needs O(n) extra space; Shell is in-place but unstable. Why it works: Large gaps quickly move elements toward their approximate positions, reducing later insertion cost. Gap selection is critical; too large yields little benefit, too small leaves too many inversions. S - Summary Shell sort = grouped insertion + decreasing gaps. In-place but unstable; performance hinges on gap sequence. Knuth sequence is a practical default; Sedgewick/Pratt can improve theoretical bounds. Best for mid-size arrays when in-place is required and stability is not. In hybrid strategies, Shell can replace insertion for small segments as a middle layer. Benchmark using real data distribution; theory alone is not enough. Practice Guide / Steps Choose gap: Knuth by default; use Sedgewick if you want better upper bounds. Switching rule: after gap=1, finish with insertion; in hybrids, use Shell below a threshold. Test sets: random, nearly sorted, reversed, heavy duplicates. Record metrics: comparisons/moves, time, cache hits (perf/pprof). Runnable Examples: Multilingual Implementations Python def shell_sort(a): n=len(a); gap=1 while gap \u0026lt; n//3: gap = 3*gap + 1 while gap\u0026gt;=1: for i in range(gap,n): tmp=a[i]; j=i while j\u0026gt;=gap and a[j-gap]\u0026gt;tmp: a[j]=a[j-gap]; j-=gap a[j]=tmp gap//=3 return a print(shell_sort([9,8,3,7,5,6,4,1])) C void shell_sort(int *a, int n){ int gap=1; while(gap \u0026lt; n/3) gap = gap*3 + 1; for(; gap\u0026gt;=1; gap/=3){ for(int i=gap;i\u0026lt;n;i++){ int tmp=a[i], j=i; while(j\u0026gt;=gap \u0026amp;\u0026amp; a[j-gap]\u0026gt;tmp){ a[j]=a[j-gap]; j-=gap; } a[j]=tmp; } } } C++ void shell(vector\u0026lt;int\u0026gt;\u0026amp; a){ int n=a.size(), gap=1; while(gap\u0026lt;n/3) gap=gap*3+1; for(; gap\u0026gt;=1; gap/=3){ for(int i=gap;i\u0026lt;n;i++){ int tmp=a[i], j=i; while(j\u0026gt;=gap \u0026amp;\u0026amp; a[j-gap]\u0026gt;tmp){ a[j]=a[j-gap]; j-=gap; } a[j]=tmp; } } } Go func ShellSort(a []int) { gap := 1 for gap \u0026lt; len(a)/3 { gap = gap*3 + 1 } for gap \u0026gt;= 1 { for i := gap; i \u0026lt; len(a); i++ { tmp, j := a[i], i for j \u0026gt;= gap \u0026amp;\u0026amp; a[j-gap] \u0026gt; tmp { a[j] = a[j-gap] j -= gap } a[j] = tmp } gap /= 3 } } Rust pub fn shell_sort(a: \u0026amp;mut [i32]) { let mut gap = 1usize; while gap \u0026lt; a.len()/3 { gap = gap*3 + 1; } while gap \u0026gt;= 1 { for i in gap..a.len() { let tmp = a[i]; let mut j = i; while j \u0026gt;= gap \u0026amp;\u0026amp; a[j-gap] \u0026gt; tmp { a[j] = a[j-gap]; j -= gap; } a[j] = tmp; } if gap == 1 { break; } gap /= 3; } } JavaScript function shellSort(a){ let gap=1; while(gap \u0026lt; a.length/3) gap = gap*3 + 1; while(gap\u0026gt;=1){ for(let i=gap;i\u0026lt;a.length;i++){ const tmp=a[i]; let j=i; while(j\u0026gt;=gap \u0026amp;\u0026amp; a[j-gap]\u0026gt;tmp){ a[j]=a[j-gap]; j-=gap; } a[j]=tmp; } gap=Math.floor(gap/3); } return a; } Common Pitfalls and Notes Stability: Shell sort is unstable; if stability is required, choose merge/TimSort. Gap choice: simple n/2 often degenerates; use at least Knuth or Sedgewick. Size: for tiny arrays use insertion; for huge arrays evaluate O(n log n) alternatives. Benchmark: gap sequences behave differently across data distributions; measure on your data. Best Practices Default to Knuth sequence for simplicity and performance; use Sedgewick/Pratt for better bounds. In hybrids, replace the \u0026ldquo;small segment threshold\u0026rdquo; with Shell and measure whether it beats insertion. For teaching, visualize gap=4/2/1 passes to illustrate grouped insertion. Count comparisons/moves to evaluate different gap sequences. Conclusion Shell sort uses grouped insertion to reduce long-distance inversions; in-place but unstable. Knuth is practical; for stability or strict bounds use merge/TimSort/heap. As a hybrid layer, Shell can bridge insertion and quick/heap performance. References and Further Reading D. L. Shell, \u0026ldquo;A High-Speed Sorting Procedure\u0026rdquo; (1959) Robert Sedgewick, \u0026ldquo;Analysis of Shellsort and Related Algorithms\u0026rdquo; (1986) CLRS discussion of Shell sort Meta Reading time: approx. 15 min SEO keywords: Shell sort, gap sequence, in-place sorting, unstable sorting Meta description: sorting series (3) explaining gap sequences, complexity, and engineering usage of Shell sort with multilingual implementations. Call to Action (CTA) Compare Knuth vs Sedgewick sequences on your dataset and record timing differences. Replace small-segment insertion in your quicksort with Shell sort and measure the impact. Follow the series: merge, quick, heap, non-comparison, TimSort/Introsort, selection guide. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/sorting/3.sorting-series-shell-sort/","summary":"Explain Shell sort principles, gap strategies, and engineering usage with scenarios and Python/C/C++/Go/Rust/JS implementations.","title":"Sorting Series (3): Shell Sort - From Insertion to Gap-Based Efficiency"},{"content":" This is the second post in the sorting series, focusing on three O(n^2) baseline algorithms: bubble, selection, and insertion. They are simple and foundational, useful for understanding higher-level sorts and still valuable on small or nearly sorted data.\nTarget Readers Practice and teaching: need to master baseline sorts and explain them. Engineers: small-scale, embedded, or code-size-sensitive scenarios. Learners: want to understand stability, in-place behavior, and complexity sources. Background and Motivation Pain points: O(n^2) sorts are often ignored, but they represent the three core ideas: swap, select, insert. On small or nearly sorted data, insertion sort can outperform O(n log n) algorithms. We need a side-by-side comparison of stability, swaps/moves, and engineering use. A - Algorithm Theme: Compare bubble (swap-driven), selection (min selection), and insertion (prefix insertion) with a basic example.\nExample array: [5, 2, 4, 6, 1]\nBubble: adjacent swaps bubble the max to the end; repeat n passes. Selection: pick min each pass and swap into place; at most n swaps. Insertion: maintain a sorted prefix and insert current element; efficient when nearly sorted. Illustration (first two insertion passes):\nPass 1: |5| 2 4 6 1 -\u0026gt; 2 5 4 6 1 Pass 2: 2 |5| 4 6 1 -\u0026gt; 2 4 5 6 1 C - Concepts Algorithm Idea Stable In-place Comparisons (avg) Swaps/Moves Bubble Adjacent swaps Yes Yes O(n^2) O(n^2) swaps Selection Select min each pass No Yes O(n^2) O(n) swaps Insertion Insert into sorted prefix Yes Yes O(n^2) O(n^2) moves; O(n) when nearly sorted Where they fit\nBubble: teaching, stability requirement, tiny arrays. Selection: high swap cost (large objects), comparisons are acceptable. Insertion: small arrays, nearly sorted; also used as a subroutine in TimSort/Shell. E - Engineering Scenario 1: Embedded firmware small arrays (C) Background: microcontroller sorting at most tens of integers. Why: short code, in-place, no extra memory; selection sort has few swaps.\n// Selection sort, in-place O(1) space void selection_sort(int *a, int n) { for (int i = 0; i \u0026lt; n - 1; ++i) { int min_i = i; for (int j = i + 1; j \u0026lt; n; ++j) if (a[j] \u0026lt; a[min_i]) min_i = j; if (min_i != i) { int tmp = a[i]; a[i] = a[min_i]; a[min_i] = tmp; } } } Scenario 2: Nearly sorted small lists (Python) Background: UI list receives a few new items; data is mostly ordered. Why: insertion sort runs close to O(n) when inversions are small.\ndef insertion_sort(arr): for i in range(1, len(arr)): key = arr[i] j = i - 1 while j \u0026gt;= 0 and arr[j] \u0026gt; key: arr[j + 1] = arr[j] j -= 1 arr[j + 1] = key return arr data = [1, 2, 3, 5, 4] print(insertion_sort(data)) Scenario 3: Teaching visualization (JavaScript) Background: show swap vs insert in a classroom or demo. Why: bubble sort is stable and intuitive; JS is short and visual.\nfunction bubbleSort(arr) { const a = [...arr]; for (let i = 0; i \u0026lt; a.length; i++) { let swapped = false; for (let j = 0; j \u0026lt; a.length - i - 1; j++) { if (a[j] \u0026gt; a[j + 1]) { [a[j], a[j + 1]] = [a[j + 1], a[j]]; swapped = true; } } if (!swapped) break; // small optimization } return a; } console.log(bubbleSort([5, 2, 4, 6, 1])); Scenario 4: Small batch sorting (Go) Background: request payload size \u0026lt; 64, prefer smaller constants. Why: Go sort switches to insertion for small segments; show a minimal implementation.\npackage main import \u0026#34;fmt\u0026#34; func insertionSort(a []int) { for i := 1; i \u0026lt; len(a); i++ { key := a[i] j := i - 1 for j \u0026gt;= 0 \u0026amp;\u0026amp; a[j] \u0026gt; key { a[j+1] = a[j] j-- } a[j+1] = key } } func main() { arr := []int{5, 2, 4, 6, 1} insertionSort(arr) fmt.Println(arr) } R - Reflection Complexity: all three have O(n^2) time in worst/avg, O(1) space. Stability: bubble and insertion are stable; selection is not (swap can break order). Alternatives: Small arrays: insertion beats bubble/selection; also used as fallback in TimSort/Introsort. Large arrays: switch to O(n log n) (quick/merge/heap) or non-comparison. Why keep them: Teaching value: clear view of comparisons, swaps, and moves. Engineering value: tiny input, nearly sorted, code size constraints, or as hybrid submodules. S - Summary Bubble/selection/insertion represent swap/select/insert ideas and are core to understanding sorting. Stability: bubble and insertion are stable; selection is not but uses few swaps. On small or nearly sorted data, insertion often beats O(n log n) algorithms. Modern sort implementations use hybrids: large segments use quick/heap/merge, small segments fall back to insertion. Choose based on size, degree of order, stability, and swap cost. Practice Guide / Steps If n \u0026lt; 64 and nearly sorted, prefer insertion. Need stability and visualization: bubble with early-exit optimization. Swap cost is high: selection reduces swaps. Use insertion as a subroutine in quick/merge for small segments. Runnable Examples (Multilang Baselines) Python - Insertion def insertion_sort(a): for i in range(1, len(a)): key = a[i]; j = i - 1 while j \u0026gt;= 0 and a[j] \u0026gt; key: a[j+1] = a[j]; j -= 1 a[j+1] = key return a print(insertion_sort([5,2,4,6,1])) C - Selection void selection_sort(int *a, int n) { for (int i = 0; i \u0026lt; n - 1; ++i) { int min_i = i; for (int j = i + 1; j \u0026lt; n; ++j) if (a[j] \u0026lt; a[min_i]) min_i = j; if (min_i != i) { int t=a[i]; a[i]=a[min_i]; a[min_i]=t; } } } C++ - Bubble #include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; void bubble(vector\u0026lt;int\u0026gt;\u0026amp; a){ for(size_t i=0;i\u0026lt;a.size();++i){ bool swapped=false; for(size_t j=0;j+1\u0026lt;a.size()-i;++j){ if(a[j]\u0026gt;a[j+1]){swap(a[j],a[j+1]);swapped=true;} } if(!swapped) break; } } int main(){vector\u0026lt;int\u0026gt; a={5,2,4,6,1}; bubble(a); for(int x:a) cout\u0026lt;\u0026lt;x\u0026lt;\u0026lt;\u0026#34; \u0026#34;;} Go - Insertion func insertion(a []int){ for i:=1;i\u0026lt;len(a);i++{ key:=a[i]; j:=i-1 for j\u0026gt;=0 \u0026amp;\u0026amp; a[j]\u0026gt;key { a[j+1]=a[j]; j-- } a[j+1]=key } } Rust - Insertion fn insertion_sort(a: \u0026amp;mut [i32]) { for i in 1..a.len() { let key = a[i]; let mut j = i as i32 - 1; while j \u0026gt;= 0 \u0026amp;\u0026amp; a[j as usize] \u0026gt; key { a[(j+1) as usize] = a[j as usize]; j -= 1; } a[(j+1) as usize] = key; } } fn main(){ let mut v = vec![5,2,4,6,1]; insertion_sort(\u0026amp;mut v); println!(\u0026#34;{:?}\u0026#34;, v); } JavaScript - Bubble function bubbleSort(a){ for(let i=0;i\u0026lt;a.length;i++){ let swapped=false; for(let j=0;j\u0026lt;a.length-i-1;j++){ if(a[j]\u0026gt;a[j+1]){[a[j],a[j+1]]=[a[j+1],a[j]];swapped=true;} } if(!swapped) break; } return a; } console.log(bubbleSort([5,2,4,6,1])); Explanation and Trade-offs Bubble vs Selection: bubble is stable but swaps more; selection swaps less but is unstable. Insertion vs Bubble: insertion does fewer moves; nearly sorted arrays can drop to O(n). Hybrid strategy: quick/heap for large segments, insertion for small segments. Common Pitfalls and Notes Bubble without early exit wastes O(n^2) on already sorted input. Selection with large structs: fewer swaps but each swap is costly; for stability use index arrays. Insertion degrades on large arrays; best for small or nearly sorted segments. Best Practices Keep a comparison table for stability, swaps/moves, and constants. Write an insertion helper and reuse it in custom quick/merge sorts. Test with sorted, reversed, many duplicates, and nearly sorted to observe early-exit behavior. Conclusion The O(n^2) trio is the foundation and a key component of hybrid sorting. For nearly sorted or small inputs, insertion remains a strong choice. Stable needs -\u0026gt; bubble or insertion; swap cost sensitive -\u0026gt; selection or stable variants. References and Further Reading CLRS chapters on insertion/bubble/selection sort CPython TimSort implementation notes (insertion thresholds) Intel/AMD notes on cache effects for small array sorting Meta Reading time: approx. 14 min SEO keywords: bubble sort, selection sort, insertion sort, O(n^2) sorting, stability Meta description: sorting series (2) comparing bubble/selection/insertion principles, stability, scenarios, and multilingual implementations for small or nearly sorted data. Call to Action (CTA) Benchmark a small dataset (e.g., 50 log rows) with all three algorithms. Add a \u0026ldquo;\u0026lt;= 32 switch to insertion\u0026rdquo; optimization in your quick/merge and measure the gain. Follow the series: shell, merge, quick, heap, non-comparison, TimSort/Introsort, selection guide. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/sorting/2.sorting-series-on2-baseline/","summary":"Systematic ACERS explanation of bubble/selection/insertion sorts: principles, stability, scenarios, and multilingual implementations with selection guidance.","title":"Sorting Series (2): Bubble, Selection, Insertion - Three O(n^2) Baselines"},{"content":" For readers preparing a systematic sorting series: this is the preface. We first build a selection map with the ACERS framework so you can quickly decide when to use quicksort, merge, heap, counting/radix, TimSort, Introsort, and more.\nTarget Readers Practice and study: want a structured overview to write a sorting series. Backend/data engineers: care about memory, stability, and parallel scenarios. Teaching and sharing: need a reusable framework and examples. Background and Motivation Pain points: too many sorting algorithms with similar names; stability/complexity are easy to mix up; engineering requires cache behavior, external sort, and language defaults. Goal: provide a \u0026ldquo;selection cheat sheet + scenarios + code skeletons\u0026rdquo; so the rest of the series has consistent structure and language. A - Algorithm Theme: How to choose a sorting algorithm based on input size, data distribution, and stability requirements.\nBasic examples\nExample 1: small array (\u0026lt;= 30) and nearly sorted -\u0026gt; insertion sort. Example 2: mid-size random array (~1e4) -\u0026gt; quicksort or Introsort. Example 3: huge integer keys with narrow range (\u0026lt;= 1e6) -\u0026gt; counting or bucket sort. Simple input/output\nInput: an array/slice of comparable elements Output: array/slice sorted in non-decreasing order C - Concepts Algorithm Avg Time Space Stable In-place Notes Bubble/Selection/Insertion O(n^2) O(1) Bubble/Insertion yes Yes/Yes/Yes baseline/teaching Shell between O(n^2) and O(n log n) O(1) No Yes gap sequence matters Merge O(n log n) O(n) Yes No good for external sort Quick O(n log n) avg; worst O(n^2) O(log n) No Yes pivot choice is key Heap O(n log n) O(1) No Yes good for streaming top-k Counting/Bucket/Radix O(n + k) O(n + k) Counting/Radix stable No/depends known range/digits required TimSort O(n log n) O(n) Yes No default in Python/Java Introsort O(n log n) O(1) No Yes C++ std::sort Categories\nDivide and conquer: merge, quick. Heap-based: heap sort. Increment-based: shell. Non-comparison: counting, bucket, radix. Engineering hybrids: TimSort (insertion + merge), Introsort (quick + heap + insertion). E - Engineering Scenario 1: Batch analytics (Python) Background: process 1e6 log rows, stable sort by timestamp to keep original order for ties. Why: Python built-in sort uses TimSort, stable and fast on partially sorted data.\nfrom operator import itemgetter logs = [ (\u0026#34;2025-11-01T10:00:00\u0026#34;, \u0026#34;user1\u0026#34;, 3), (\u0026#34;2025-11-01T10:00:00\u0026#34;, \u0026#34;user2\u0026#34;, 1), (\u0026#34;2025-11-01T10:00:01\u0026#34;, \u0026#34;user3\u0026#34;, 2), ] # Sort by timestamp ascending; stable keeps original order for ties logs.sort(key=itemgetter(0)) print(logs) Scenario 2: Backend pagination sort (Go) Background: sort items by price ascending and sales descending; dataset \u0026lt; 1e5. Why: sort.Slice is in-place and flexible; standard library is sufficient here.\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;sort\u0026#34; ) type Item struct { Price int; Sales int } func main() { items := []Item{{100, 50}, {80, 200}, {100, 120}} sort.Slice(items, func(i, j int) bool { if items[i].Price == items[j].Price { return items[i].Sales \u0026gt; items[j].Sales // sales desc } return items[i].Price \u0026lt; items[j].Price }) fmt.Println(items) } Scenario 3: Memory-limited offline sort (C++, external merge) Background: sort a 10GB integer file with only 512MB RAM. Why: external sort requires chunking + multi-way merge; stable and memory-bound.\n#include \u0026lt;bits/stdc++.h\u0026gt; using namespace std; int main() { vector\u0026lt;int\u0026gt; buf; buf.reserve(1 \u0026lt;\u0026lt; 20); // ~1M ints vector\u0026lt;string\u0026gt; tmpFiles; int x; int chunk = 0; while (cin \u0026gt;\u0026gt; x) { buf.push_back(x); if (buf.size() == buf.capacity()) { sort(buf.begin(), buf.end()); string name = \u0026#34;chunk\u0026#34; + to_string(chunk++) + \u0026#34;.tmp\u0026#34;; ofstream out(name); for (int v : buf) out \u0026lt;\u0026lt; v \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; tmpFiles.push_back(name); buf.clear(); } } // Omit the final chunk and k-way merge; show the idea cerr \u0026lt;\u0026lt; \u0026#34;chunks: \u0026#34; \u0026lt;\u0026lt; tmpFiles.size() \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } Scenario 4: Frontend table sorting (JavaScript) Background: sort by multiple columns and keep order for equal keys (stable). Why: modern browsers usually have stable Array.prototype.sort; if unsure, map indices and sort.\nconst rows = [ { price: 100, sales: 50 }, { price: 100, sales: 120 }, { price: 80, sales: 200 }, ]; rows .map((row, idx) =\u0026gt; ({ ...row, idx })) .sort((a, b) =\u0026gt; a.price - b.price || a.idx - b.idx) .forEach(r =\u0026gt; console.log(r)); R - Reflection Complexity and space: O(n log n) workhorses: merge (stable, not in-place), quick (in-place, worst-case), heap (in-place, cache-unfriendly). O(n + k) non-comparison: counting/bucket/radix, only when range/digits are bounded. O(n^2) baseline: bubble/selection/insertion, mostly for teaching or tiny arrays. Alternatives: External sort vs in-memory: if data exceeds RAM, chunk + merge is mandatory. TimSort vs plain merge: TimSort is faster on partially sorted data and is the engineering default. Introsort vs plain quicksort: depth fallback to heap avoids worst-case O(n^2). Why this selection is reasonable: Stability first: merge/TimSort/counting/radix. Memory first: quick/heap/Introsort (in-place). Range known: counting/bucket/radix. Huge data: external merge with multi-way streaming. S - Summary Selection depends on data size, distribution, stability needs, and memory/external constraints. Default to the language built-in sort (often TimSort/Introsort); customize only for special needs. Non-comparison sorting can drop complexity to O(n + k) when range/digits are bounded. External sorting is essential for data larger than memory; core idea is chunking + multi-way merge. Choose criteria first (time, space, stability), then the algorithm. Practice Guide / Steps Step 1: Evaluate size and distribution (random/near-sorted/heavy duplicates). Step 2: Decide stability requirement and memory budget. Step 3: Use the table to pick a baseline; for Python/Java, prefer built-in stable sort. Step 4: Write three boundary tests: all equal, reverse, nearly sorted. Step 5: Benchmark large data and record time/memory. Runnable Example (Quick Benchmark, Python) import random, time def bench(n=100000): arr = [random.randint(0, 1000000) for _ in range(n)] t0 = time.time(); sorted(arr); t1 = time.time() print(f\u0026#34;n={n}, timsort time={t1 - t0:.3f}s\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: bench(200000) Common Pitfalls and Notes Forgetting comparator logic in Array.sort / sort.Slice may break stability or ordering. Fixed pivot in quicksort degenerates on sorted input; use random or median-of-three. Counting/bucket sort can blow memory if range is large; estimate min/max first. External sort with too many temp files requires k-way merge or staged merging. Best Practices Use standard library sorts in production unless you have explicit constraints. Write comparator + tests before implementing the sort; verify stability if needed. Sample large data to judge whether bucket/radix is appropriate or external sort is needed. Require a \u0026ldquo;sorting algorithm + rationale\u0026rdquo; note in PRs for review. Conclusion This preface provides the ACERS selection map for the series. Next steps: expand by category (O(n^2) baseline, shell, merge, quick, heap, non-comparison, TimSort, Introsort, selection playbook). References and Further Reading CLRS \u0026ldquo;Introduction to Algorithms\u0026rdquo; sorting chapters TimSort paper and CPython listobject.c C++ std::sort / std::stable_sort implementation notes PostgreSQL external sort (tuplesort) Meta Reading time: approx. 12 min SEO keywords: sorting selection, algorithm stability, TimSort, Introsort, external sort Meta description: sorting series preface using ACERS to summarize complexity, stability, and engineering scenarios with runnable examples and a selection checklist. Call to Action (CTA) Write a \u0026ldquo;sorting selection checklist\u0026rdquo; for your project with size/distribution/stability notes. Run the Python benchmark above with your real data distribution. Follow the series (quicksort, merge, heap, non-comparison, TimSort/Introsort, selection playbook) and replicate it with the ACERS template. ","permalink":"https://jeanphilogong.github.io/jeanblog/alg/sorting/1.sorting-series-preface/","summary":"Use the ACERS template to map common sorting algorithms by scenario, complexity, stability, and engineering usage, with runnable examples and a selection checklist.","title":"Sorting Series (1): How to Choose an Algorithm - Time, Space, Stability, Scenarios"},{"content":"UFW + CrowdSec: Stop Malicious Port Scans Subtitle / Abstract: How do you protect exposed server ports? This guide shows how to move past Fail2ban regex hell and build a stable, automated, intelligent port-scan defense system.\nTarget readers Developers using FRP or reverse tunnels Operators of cloud servers (Tencent, Alibaba, AWS, etc.) Linux users who want to stop port scans and SSH brute force People using Fail2ban who want a modern alternative Anyone improving personal server security Background / Motivation: Why you need port-scan defense When you run FRP (frps + frpc) or expose multiple ports, you will often see:\nMassive scans: repeated SYN probes Malicious connection attempts SSH password brute force Automated scans of 6001-6010, 7000, 22, 8080, etc. Traditional approaches have weaknesses:\nUFW only blocks passively Fail2ban is regex-heavy, error-prone, and lacks behavior analysis FRPS logs are hard to match in Fail2ban Attacks still consume frps/sshd resources and can cause slowdowns We need a modern system: no regex, auto detection, intelligent IP banning.\nCore concepts FRP (frps / frpc): reverse tunnel tool, often exposes many TCP ports (e.g., 6001-6010) UFW: Ubuntu firewall, but not intelligent Fail2ban: log-matching ban tool that requires regex CrowdSec (recommended): modern open-source IPS that detects port scans and brute force with behavior analysis and low resource usage Practical guide: Auto-block port scans with CrowdSec (Ubuntu/Debian) 1) Install CrowdSec curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash sudo apt install crowdsec -y 2) Install firewall bouncer (iptables/UFW) sudo apt install crowdsec-firewall-bouncer-iptables CrowdSec will manage blocking automatically.\n3) What it detects out of the box TCP port scans FRP brute-force attempts SSH brute force High-rate connections (DoS-like) Suspicious sequences (behavior analysis) No extra rules needed for 6001-6010 and other ports.\n4) View banned IPs sudo cscli decisions list Example:\nID Scope Value Reason Duration 1 Ip 195.24.237.176 portscan 4h 2 Ip 213.199.63.251 ssh-bf 24h 5) Manual ban (optional) sudo cscli decisions add --ip 195.24.237.176 6) Dashboard (optional) sudo apt install crowdsec-lapi Why CrowdSec \u0026gt; Fail2ban Feature Fail2ban CrowdSec Port-scan detection No Yes (auto) FRP log support Regex heavy No log match needed Config complexity High Low Performance Medium Very low Extensibility Weak Modular + behavior analysis Visualization None Dashboard Resource usage Medium RAM \u0026lt; 20MB CrowdSec is a modern replacement for Fail2ban with lower overhead and stronger detection.\nFail2ban pitfalls (why it fails in FRP scenarios) FRPS logs are complex; IP fields shift and are inconsistent Regex must be perfect; a small mistake matches nothing Logs include colons, brackets, and ports, which break patterns Host IP may be internal (e.g., 10.5.100.2), causing mismatched source IPs UFW log formats vary; Fail2ban cannot extract IP reliably Encoding issues can lead to \u0026ldquo;No failure-id group\u0026rdquo; Risks and notes Blocking can briefly impact FRP or SSH; always keep a backup access method (cloud console). CrowdSec may false-positive crawlers. Whitelist trusted IPs: sudo cscli machines list sudo cscli decisions delete --ip \u0026lt;trusted-ip\u0026gt; FRP often hides real client IPs in logs; CrowdSec works at the kernel level, so it still sees the source. Best practices Replace Fail2ban with CrowdSec (strongly recommended) Close unused FRP ports, use strong tokens and encryption Use SSH keys only, disable password auth Keep UFW default deny incoming Check bans regularly: cscli decisions list Consider Cloudflare Tunnel as an alternative to FRP Summary This guide covered:\nHow to detect and block port scans Why Fail2ban regex often fails Why FRP logs are a poor fit for Fail2ban How CrowdSec provides automated, low-maintenance protection Final solution: UFW + CrowdSec = stable, automated, low-maintenance server defense.\nReferences CrowdSec docs: https://doc.crowdsec.net CrowdSec bouncer: https://github.com/crowdsecurity/cs-firewall-bouncer Fail2ban docs: https://fail2ban.readthedocs.io FRP: https://github.com/fatedier/frp UFW docs: https://wiki.ubuntu.com/UFW ","permalink":"https://jeanphilogong.github.io/jeanblog/linux/linux/ufw-crowdsec-portscan/","summary":"\u003ch1 id=\"ufw--crowdsec-stop-malicious-port-scans\"\u003eUFW + CrowdSec: Stop Malicious Port Scans\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract:\u003c/strong\u003e How do you protect exposed server ports? This guide shows how to move past Fail2ban regex hell and build a stable, automated, intelligent port-scan defense system.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eDevelopers using FRP or reverse tunnels\u003c/li\u003e\n\u003cli\u003eOperators of cloud servers (Tencent, Alibaba, AWS, etc.)\u003c/li\u003e\n\u003cli\u003eLinux users who want to stop port scans and SSH brute force\u003c/li\u003e\n\u003cli\u003ePeople using Fail2ban who want a modern alternative\u003c/li\u003e\n\u003cli\u003eAnyone improving personal server security\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"background--motivation-why-you-need-port-scan-defense\"\u003eBackground / Motivation: Why you need port-scan defense\u003c/h2\u003e\n\u003cp\u003eWhen you run FRP (frps + frpc) or expose multiple ports, you will often see:\u003c/p\u003e","title":"UFW + CrowdSec: Stop Malicious Port Scans (From Fail2ban Pain to a Modern Solution)"},{"content":"WireGuard Full Guide: Build a Secure High-Speed Private Network (VPN Tutorial) Subtitle / Abstract: A beginner-to-intermediate WireGuard VPN guide. Learn to build a fast, secure private network and enforce a zero-exposure model where services are only reachable through VPN.\nTarget readers People who want to hide server or PC ports behind a VPN Users who want to reduce scanning and brute force risk Anyone building a private LAN or remote access to home Linux/Windows users, developers, and ops beginners Background and motivation: Why WireGuard? If you expose ports to the public internet (SSH, databases, admin panels), you will face:\nconstant scans brute force attempts automated probes potential intrusion risk OpenVPN is mature but heavy, slower, and complex to configure.\nWireGuard is built for modern security:\nsmall, secure, fast (next-gen VPN) codebase \u0026lt; 4000 lines (OpenVPN is 400k+) easy config low latency, high throughput great for private networks and remote work This guide helps you build a private network that is invisible to the public internet.\nCore concepts What is WireGuard? WireGuard is a modern, minimal VPN protocol in the Linux kernel, using modern crypto (ChaCha20, Curve25519, etc.).\nHighlights:\nvery fast simple config files strong security by default stable roaming (mobile network switching works) Terminology Term Meaning Interface WireGuard virtual interface, e.g., wg0 Peer A node (client/server) PrivateKey private key (keep secret) PublicKey public key (identity to peers) AllowedIPs IP ranges allowed for a peer WireGuard is peer-to-peer and does not need a certificate system like OpenVPN.\nWireGuard vs OpenVPN Item WireGuard OpenVPN Performance very fast (kernel) slower (user space) Config complexity minimal complex Security modern by default configurable but easy to misconfigure Stability high average Roaming excellent weak Code size ~4000 lines ~400k lines One-line summary: if you want speed, simplicity, and stability, choose WireGuard.\nPractical setup: Build WireGuard on a server Example uses Ubuntu/Debian.\n1. Install WireGuard sudo apt update sudo apt install wireguard -y 2. Generate server keys wg genkey | tee server_private.key | wg pubkey \u0026gt; server_public.key 3. Create server config /etc/wireguard/wg0.conf [Interface] Address = 10.8.0.1/24 ListenPort = 51820 PrivateKey = \u0026lt;server_private_key\u0026gt; # client peers will be added below 4. Start WireGuard sudo wg-quick up wg0 Enable on boot:\nsudo systemctl enable wg-quick@wg0 Create a mobile client (Peer) 1. Generate client keys wg genkey | tee phone_private.key | wg pubkey \u0026gt; phone_public.key 2. Add peer on the server Edit /etc/wireguard/wg0.conf:\n[Peer] PublicKey = \u0026lt;phone_public_key\u0026gt; AllowedIPs = 10.8.0.2/32 Restart WireGuard:\nsudo wg-quick down wg0 sudo wg-quick up wg0 3. Create client config (phone) phone.conf:\n[Interface] PrivateKey = \u0026lt;phone_private_key\u0026gt; Address = 10.8.0.2/32 DNS = 1.1.1.1 [Peer] PublicKey = \u0026lt;server_public_key\u0026gt; Endpoint = \u0026lt;your-public-ip-or-domain\u0026gt;:51820 AllowedIPs = 0.0.0.0/0 PersistentKeepalive = 25 Import on mobile via QR code Install:\nAndroid: WireGuard (Google Play) iOS: WireGuard (App Store) Generate QR code:\nqrencode -t ansiutf8 \u0026lt; phone.conf Scan in the WireGuard app.\nAfter connecting, your phone gets:\nInternal IP: 10.8.0.2 You can access:\nYour server: 10.8.0.1 Examples:\nSSH: ssh user@10.8.0.1 RDP: 10.8.0.1 Web: http://10.8.0.1:xxxx Why this works 1. Peer-to-peer design No certificates, no TLS, no expiry issues.\n2. Keys are identity Each device has a key pair as its identity.\n3. Kernel implementation WireGuard runs in the kernel crypto subsystem for high efficiency.\n4. Designed for modern networks Seamless roaming between 4G and Wi-Fi on mobile.\nCommon pitfalls Error 1: port 51820/udp not open You must open:\nUDP 51820 Error 2: wrong AllowedIPs If you set:\nAllowedIPs = 0.0.0.0/0 all phone traffic goes through VPN.\nTo access only the LAN:\nAllowedIPs = 10.8.0.0/24 Error 3: IP forwarding disabled echo \u0026#34;net.ipv4.ip_forward=1\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p Best practices Generate a unique key pair for each device Do not share config files Use a fixed server IP (or DDNS) Restrict non-VPN traffic via UFW Bind backend services to internal IP only Example: SSH listens only on\nListenAddress 10.8.0.1 Summary WireGuard is ideal for:\nhome or work private networks hiding server ports secure remote access building a private LAN This guide covers principles, install, config, mobile access, and best practices. You can now:\ndeploy WireGuard quickly on any server access your private network securely avoid public port exposure and scanning If you need:\nDocker-based WireGuard Windows as the server multi-user management advanced routing Let me know and I can extend the series.\nReferences WireGuard docs: https://www.wireguard.com/ Linux man pages: man wg, man wg-quick WireGuard paper: https://www.wireguard.com/papers/wireguard.pdf Meta (SEO) Keywords: WireGuard tutorial, VPN private network, self-hosted VPN, server security, WireGuard vs OpenVPN Reading time: 8-12 minutes Tags: VPN, Linux, security, private network, tutorial Meta description: A comprehensive WireGuard VPN tutorial for building a fast and secure private network, with setup and mobile access. Call to Action (CTA) If this helped, star it, ask questions, or tell me your WireGuard scenario. I can help you tailor the config and extend the series.\n","permalink":"https://jeanphilogong.github.io/jeanblog/linux/linux/wireguard-vpn-neiwang/","summary":"\u003ch1 id=\"wireguard-full-guide-build-a-secure-high-speed-private-network-vpn-tutorial\"\u003eWireGuard Full Guide: Build a Secure High-Speed Private Network (VPN Tutorial)\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract:\u003c/strong\u003e\nA beginner-to-intermediate WireGuard VPN guide. Learn to build a fast, secure private network and enforce a zero-exposure model where services are only reachable through VPN.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003ePeople who want to hide server or PC ports behind a VPN\u003c/li\u003e\n\u003cli\u003eUsers who want to reduce scanning and brute force risk\u003c/li\u003e\n\u003cli\u003eAnyone building a private LAN or remote access to home\u003c/li\u003e\n\u003cli\u003eLinux/Windows users, developers, and ops beginners\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"background-and-motivation-why-wireguard\"\u003eBackground and motivation: Why WireGuard?\u003c/h2\u003e\n\u003cp\u003eIf you expose ports to the public internet (SSH, databases, admin panels), you will face:\u003c/p\u003e","title":"WireGuard Full Guide: Build a Secure High-Speed Private Network (VPN Tutorial)"},{"content":"Build a Hugo Blog with GitHub Pages in 10 Minutes Subtitle / Abstract This guide takes you from zero to a deployed Hugo blog on GitHub Pages with GitHub Actions. It is beginner-friendly and explains the key moving parts.\nTarget readers Hugo beginners Developers who want a quick technical blog Users of GitHub Pages and GitHub Actions Anyone who wants free static hosting Background / Motivation Common pain points when publishing a blog:\nmanual uploads scattered deployment steps confusing GitHub Pages setup theme assets failing to build The combo Hugo + GitHub Pages + GitHub Actions solves these:\nHugo is fast Pages is free Actions deploys on every push Core concepts Hugo: static site generator GitHub Pages: free static hosting GitHub Actions: CI pipeline to build and deploy PaperMod: popular Hugo theme Steps: from local to online Step 1: Create a Hugo site hugo new site myblog cd myblog git init Add PaperMod:\ngit submodule add https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod Set config.toml:\nbaseURL = \u0026#34;https://\u0026lt;your-username\u0026gt;.github.io/\u0026lt;repo\u0026gt;/\u0026#34; languageCode = \u0026#34;en-us\u0026#34; title = \u0026#34;My Blog\u0026#34; theme = \u0026#34;PaperMod\u0026#34; Step 2: Push to GitHub git remote add origin git@github.com:\u0026lt;your-username\u0026gt;/\u0026lt;repo\u0026gt;.git git add . git commit -m \u0026#34;init blog\u0026#34; git push -u origin main Step 3: Enable GitHub Pages In your repo:\nSettings -\u0026gt; Pages Build and deployment -\u0026gt; Source = GitHub Actions If the repo is private, set it public or enable Pages for private repositories (paid). Otherwise you may see 404.\nStep 4: Add GitHub Actions workflow Create .github/workflows/hugo.yml:\nname: Deploy Hugo site to Pages on: push: branches: [\u0026#34;main\u0026#34;] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: true - uses: peaceiris/actions-hugo@v2 with: hugo-version: \u0026#34;0.120.4\u0026#34; - name: Build run: hugo --minify - name: Upload uses: actions/upload-pages-artifact@v2 with: path: ./public deploy: needs: build runs-on: ubuntu-latest permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2 Step 5: Create a post and publish hugo new posts/hello-world.md Edit front matter, set draft: false, write your content, and push.\nCommon issues draft: true means the post will not show incorrect baseURL causes broken links missing submodules in Actions -\u0026gt; theme missing Pages source not set to Actions Summary Hugo builds fast static pages GitHub Actions automates build and deploy GitHub Pages hosts for free Once set, you only need to write Markdown and push.\n","permalink":"https://jeanphilogong.github.io/jeanblog/thoughts/thoughts/how-to-build-a-blog-system/","summary":"\u003ch1 id=\"build-a-hugo-blog-with-github-pages-in-10-minutes\"\u003eBuild a Hugo Blog with GitHub Pages in 10 Minutes\u003c/h1\u003e\n\u003ch2 id=\"subtitle--abstract\"\u003eSubtitle / Abstract\u003c/h2\u003e\n\u003cp\u003eThis guide takes you from zero to a deployed Hugo blog on GitHub Pages with GitHub Actions. It is beginner-friendly and explains the key moving parts.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHugo beginners\u003c/li\u003e\n\u003cli\u003eDevelopers who want a quick technical blog\u003c/li\u003e\n\u003cli\u003eUsers of GitHub Pages and GitHub Actions\u003c/li\u003e\n\u003cli\u003eAnyone who wants free static hosting\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eCommon pain points when publishing a blog:\u003c/p\u003e","title":"How to Build a Blog System"},{"content":"How to Publish with Hugo: From Markdown to Online Blog Subtitle / Abstract This guide explains how to create, manage, and publish Hugo posts: front matter, drafts, images, directory structure, local preview, and deployment.\nTarget readers Hugo beginners Developers building a technical blog with Hugo Writers using Markdown + static sites Users of PaperMod, DoIt, and similar themes Background / Motivation After setting up a Hugo site, common questions include:\nWhere should posts go? How should front matter be written? Where do images live? Why does the post show locally but not online? How do drafts and publish dates work? How do posts appear on the homepage? This guide provides practical steps and best practices for the full publishing flow.\nCore concepts 1) Content directory Hugo posts live under content/:\ncontent/ posts/ my-first-post.md 2) Front matter Top metadata controls title, date, draft, tags, etc.\n--- title: \u0026#34;My Title\u0026#34; date: 2024-08-26 draft: false tags: [\u0026#34;hugo\u0026#34;, \u0026#34;blog\u0026#34;] --- 3) Draft Drafts are not built. Use hugo server -D to preview drafts.\n4) Section content/posts/* maps to the /posts/ section.\nSteps Step 1: Create a new post hugo new posts/how-to-publish.md This creates:\ncontent/posts/how-to-publish.md Default content:\n--- title: \u0026#34;How to Publish\u0026#34; date: 2024-08-26T10:00:00+08:00 draft: true --- Step 2: Edit front matter A typical PaperMod-friendly front matter:\n--- title: \u0026#34;How to Publish with Hugo\u0026#34; date: 2024-08-26T10:00:00+08:00 draft: false tags: [\u0026#34;hugo\u0026#34;, \u0026#34;blog\u0026#34;, \u0026#34;static-site\u0026#34;] categories: [\u0026#34;tutorial\u0026#34;] summary: \u0026#34;A complete guide from writing to publishing.\u0026#34; cover: image: \u0026#34;/images/hugo-cover.png\u0026#34; alt: \u0026#34;Hugo cover\u0026#34; caption: \u0026#34;Hugo blog cover\u0026#34; --- Step 3: Write content Use Markdown and keep headings consistent. Add code blocks where needed.\nStep 4: Add images Common options:\nstatic/images/... and reference as /images/... Page bundles: content/posts/my-post/index.md with images in the same folder Step 5: Preview locally hugo server -D Open http://localhost:1313.\nStep 6: Build and deploy hugo Static output goes to public/. Deploy via GitHub Pages, Netlify, or your server.\nCommon pitfalls draft: true prevents publishing Wrong folder (e.g., outside content/) Missing or incorrect baseURL Images referenced with wrong paths Theme config not loaded Summary Create posts with hugo new Write front matter carefully Preview with hugo server -D Build with hugo and deploy If you want a complete deployment pipeline with GitHub Actions, see the next guide.\n","permalink":"https://jeanphilogong.github.io/jeanblog/thoughts/thoughts/how-to-publish-by-hugo/","summary":"\u003ch1 id=\"how-to-publish-with-hugo-from-markdown-to-online-blog\"\u003eHow to Publish with Hugo: From Markdown to Online Blog\u003c/h1\u003e\n\u003ch2 id=\"subtitle--abstract\"\u003eSubtitle / Abstract\u003c/h2\u003e\n\u003cp\u003eThis guide explains how to create, manage, and publish Hugo posts: front matter, drafts, images, directory structure, local preview, and deployment.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eHugo beginners\u003c/li\u003e\n\u003cli\u003eDevelopers building a technical blog with Hugo\u003c/li\u003e\n\u003cli\u003eWriters using Markdown + static sites\u003c/li\u003e\n\u003cli\u003eUsers of PaperMod, DoIt, and similar themes\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eAfter setting up a Hugo site, common questions include:\u003c/p\u003e","title":"How to Publish with Hugo"},{"content":"Title (accurate and keyword-rich) Write Clear Requirements with Issue Templates: A Complete Guide to GitHub Issue Forms\nSubtitle / Abstract This post teaches you how to configure GitHub Issue templates for feature requests and bug reports, including folder structure, YAML forms, Markdown templates, and common pitfalls. It is ideal for teams that want clearer requirements and less back-and-forth.\nTarget readers This article is for:\nBackend/frontend/full-stack engineers who create Issues regularly Leads/TLs/architects who want standardized requirement intake Mid-level developers familiar with GitHub but new to Issue templates Beginners can follow, but basic GitHub knowledge is assumed.\nBackground / Motivation: Why templates? Without templates, you often hear:\n\u0026ldquo;What is the background?\u0026rdquo; \u0026ldquo;Which modules are affected?\u0026rdquo; \u0026ldquo;What is the acceptance criteria?\u0026rdquo; \u0026ldquo;How high is the priority?\u0026rdquo; A one-line Issue like:\n\u0026ldquo;Add export feature\u0026rdquo;\nwill confuse everyone.\nLong-term pain points:\nHigh communication cost: details must be asked repeatedly Information asymmetry: the requester knows, but the Issue does not Hard to plan: no priority or acceptance criteria Hard to trace: months later, nobody knows the intent GitHub Issue Templates are structured questions that guide good input:\nenforce or guide required fields auto-label and auto-prefix titles support form UI validation Goal: make every new Issue understandable at a glance.\nCore concepts 1. Issue Template A preset format shown when creating a new Issue Can be Markdown text or YAML form 2. Markdown template Old and simple Essentially a prefilled Markdown file Path: .github/ISSUE_TEMPLATE/xxx.md or .github/ISSUE_TEMPLATE.md 3. YAML Issue form New and recommended Form UI with inputs and dropdowns Submissions are converted to Markdown in the Issue body Path: .github/ISSUE_TEMPLATE/xxx.yml 4. config.yml Path: .github/ISSUE_TEMPLATE/config.yml Controls: Whether blank Issues are allowed Which templates are displayed Practice guide / Steps overview Create .github/ISSUE_TEMPLATE Create a Feature template (YAML form) (Optional) Create a Bug template Configure config.yml for blank Issue behavior Commit and push to GitHub Verify in the GitHub UI Step 1: Create the template folder mkdir -p .github/ISSUE_TEMPLATE Structure:\nyour-repo/ .github/ ISSUE_TEMPLATE/ # yml / md files go here src/ ... Step 2: Feature request template (YAML form) Create .github/ISSUE_TEMPLATE/feature-request.yml:\nname: \u0026#34;Feature Request\u0026#34; description: \u0026#34;Use for new features or requirement changes\u0026#34; title: \u0026#34;[Feature] \u0026#34; labels: - \u0026#34;feature\u0026#34; - \u0026#34;enhancement\u0026#34; body: - type: markdown attributes: value: | Thanks for submitting a feature request. Please be as clear as possible for evaluation and planning. - type: input id: module attributes: label: Affected module description: Service or module involved (API, crawler, UI, etc.) placeholder: e.g. export API / attachment viewer validations: required: true - type: textarea id: background attributes: label: Background / scenario description: Why do we need this? What problem are we solving? placeholder: | Describe business context, roles, usage scenario, pain points... validations: required: true - type: textarea id: description attributes: label: Requirement description description: Describe desired behavior from the user perspective. placeholder: | 1. Add ... on page ... 2. When user does ..., the system should ... 3. Edge cases to support: ... validations: required: true - type: textarea id: acceptance_criteria attributes: label: Acceptance criteria description: What counts as \u0026#34;done\u0026#34;? Helps testing and review. placeholder: | - [ ] Scenario 1: ... - [ ] Scenario 2: ... - [ ] Performance / security requirements: ... validations: required: true - type: dropdown id: priority attributes: label: Priority description: For planning and ordering options: - P0 (must be done this iteration) - P1 (high) - P2 (normal) - P3 (low) default: 2 validations: required: false - type: textarea id: extra attributes: label: Extra info description: Related APIs, docs, designs, screenshots, linked issues placeholder: | - API docs: - Design / prototype: - Related Issue / ticket: validations: required: false Effect:\nA \u0026ldquo;Feature Request\u0026rdquo; option appears on Issue creation The form replaces plain text Labels feature and enhancement are added Title is prefixed with [Feature] Key fields are required Step 3 (Optional): Bug report template Create .github/ISSUE_TEMPLATE/bug-report.yml:\nname: \u0026#34;Bug Report\u0026#34; description: \u0026#34;Use for bugs and exceptions\u0026#34; title: \u0026#34;[Bug] \u0026#34; labels: - \u0026#34;bug\u0026#34; body: - type: textarea id: summary attributes: label: Summary placeholder: Briefly describe the problem validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce placeholder: | 1. Open ... 2. Click ... 3. See ... validations: required: true - type: textarea id: expected attributes: label: Expected result validations: required: true - type: textarea id: actual attributes: label: Actual result validations: required: true - type: textarea id: extra attributes: label: Extra info description: Logs, screenshots, environment details validations: required: false Now your team can separate feature requests from bugs clearly.\nStep 4: Configure config.yml Create .github/ISSUE_TEMPLATE/config.yml:\nblank_issues_enabled: false # disallow blank Issues, force templates contact_links: - name: Internal request system url: https://example.com/your-internal-system about: If this is a formal request, create it in the internal system first. If you do not have an internal system, remove contact_links or replace with your wiki.\nblank_issues_enabled: false forces template usage and prevents empty Issues.\nStep 5: Commit and push git add .github/ISSUE_TEMPLATE/* git commit -m \u0026#34;chore: add GitHub issue templates for feature and bug\u0026#34; git push Templates take effect on the default branch (usually main or master).\nStep 6: Verify in GitHub UI Open your repo Click Issues Click New issue You should see template choices such as:\nFeature Request Bug Report (Optional) Open a blank issue If blank_issues_enabled: false, the blank option disappears.\nMinimal runnable example If you only need a minimal Feature template, do this:\n1) Create folder:\nmkdir -p .github/ISSUE_TEMPLATE 2) Create .github/ISSUE_TEMPLATE/feature-request.yml:\nname: \u0026#34;Feature Request\u0026#34; description: \u0026#34;Use for new features or requirement changes\u0026#34; title: \u0026#34;[Feature] \u0026#34; labels: [\u0026#34;feature\u0026#34;] body: - type: textarea id: background attributes: label: Background / scenario placeholder: Briefly describe why this is needed validations: required: true - type: textarea id: description attributes: label: Requirement description placeholder: | What should the system do? How will users use it? validations: required: true - type: textarea id: acceptance attributes: label: Acceptance criteria placeholder: | - [ ] Scenario 1: ... - [ ] Scenario 2: ... validations: required: true Then:\ngit add .github/ISSUE_TEMPLATE/feature-request.yml git commit -m \u0026#34;add minimal feature request issue template\u0026#34; git push Why YAML forms instead of Markdown? Benefits of YAML forms Required field validation for background/requirements/acceptance Friendly UI for non-technical teammates Clear structure for reading and automation Auto labels and title prefixes Markdown template pros and cons Markdown templates are fine, but:\nPros: simple and compatible good for technical teams Cons: cannot enforce required fields UI is less friendly for product/ops roles If you want team standards and clarity, YAML forms are better. For small personal projects, Markdown is enough.\nCommon issues and notes 1. Template not working? Check:\ncorrect path: .github/ISSUE_TEMPLATE/xxx.yml or .github/ISSUE_TEMPLATE/xxx.md default branch: template must be on main/master case sensitivity: ISSUE_TEMPLATE must match exactly 2. Template updated but UI unchanged? browser cache: refresh or use private mode confirm you pushed to GitHub for forks, templates are per-repo and do not inherit upstream 3. Org-level templates? You can configure templates in an org-wide .github repo. Repos without templates will use the org default.\n4. YAML errors? YAML is sensitive to indentation and spaces GitHub may ignore or error on malformed YAML Use editor validation (VS Code is great) Best practices Start simple: one feature template first Require core fields: background, description, acceptance Standardize title prefixes: [Feature], [Bug] Auto-label to reduce manual maintenance Keep forms short; balance clarity and friction Review after 1-2 months and refine fields Conclusion This guide covers:\nIssue template concepts (YAML forms vs Markdown) A full Feature template plus optional Bug template The end-to-end workflow: create -\u0026gt; write -\u0026gt; push -\u0026gt; verify Why YAML forms are recommended If you apply these steps, requirement quality will improve immediately.\nReferences and further reading GitHub Docs: Issue and pull request templates Keywords: github issue template yaml github issue forms github .github/ISSUE_TEMPLATE examples Meta Reading time: 8-12 minutes Tags: GitHub, collaboration, Issue templates, team standards, requirements SEO keywords: GitHub Issue template GitHub Issue Template config YAML Issue Form tutorial Feature request template Meta description: A complete guide to configuring GitHub Issue templates (YAML forms and Bug templates) with best practices and pitfalls. Call to Action (CTA) If you finished reading, do this now:\nPick a GitHub repo you use often Create .github/ISSUE_TEMPLATE/feature-request.yml Push and open a test Issue to see the effect ","permalink":"https://jeanphilogong.github.io/jeanblog/notes/git-notes/write-clear-issues-from-zero-to-template/","summary":"\u003ch2 id=\"title-accurate-and-keyword-rich\"\u003eTitle (accurate and keyword-rich)\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003eWrite Clear Requirements with Issue Templates: A Complete Guide to GitHub Issue Forms\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"subtitle--abstract\"\u003eSubtitle / Abstract\u003c/h2\u003e\n\u003cp\u003eThis post teaches you how to configure GitHub Issue templates for feature requests and bug reports, including folder structure, YAML forms, Markdown templates, and common pitfalls. It is ideal for teams that want clearer requirements and less back-and-forth.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cp\u003eThis article is for:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eBackend/frontend/full-stack engineers who create Issues regularly\u003c/li\u003e\n\u003cli\u003eLeads/TLs/architects who want standardized requirement intake\u003c/li\u003e\n\u003cli\u003eMid-level developers familiar with GitHub but new to Issue templates\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eBeginners can follow, but basic GitHub knowledge is assumed.\u003c/p\u003e","title":"Write Clear Issues with Templates: From Zero to GitHub Issue Forms"},{"content":"Title How to Write a Qualified API Document: From Swagger to Modern OpenAPI\nSubtitle / Abstract Want developers to actually enjoy using your API? This article covers the structure, examples, and best practices of high-quality API documentation based on Swagger/OpenAPI (originally by Tony Tam).\nTarget readers Beginners who want a standard API doc structure Mid-level developers improving maintainability Architects and leads defining API standards Background / Motivation Common problems in API docs:\ninconsistent format out-of-date content not usable for automation or testing Tony Tam introduced Swagger (renamed to OpenAPI) in 2010 to solve this. It is now the de facto standard for REST API docs, used by Google, Amazon, Stripe, and more.\nCore concepts Concept Description API doc Technical specification of how to call an API and interpret requests/responses Swagger/OpenAPI Standard to define, generate, and test REST APIs Endpoint A concrete path like /users/{id} Schema Field structure for requests and responses Practical steps Define a clear structure\nOverview Authentication Endpoints Schemas Errors and examples Use OpenAPI (YAML is recommended)\nRecommended tools\nEditors: Swagger Editor, Stoplight Studio, VS Code + YAML Docs: Swagger UI / ReDoc Auto-gen: Springdoc, FastAPI, NestJS Runnable example (OpenAPI) openapi: 3.0.0 info: title: User Management API version: 1.0.0 description: APIs for user management. servers: - url: https://api.example.com/v1 paths: /users/{id}: get: summary: Get user by ID parameters: - name: id in: path required: true description: User ID schema: type: string responses: \u0026#39;200\u0026#39;: description: OK content: application/json: schema: $ref: \u0026#39;#/components/schemas/User\u0026#39; \u0026#39;404\u0026#39;: description: User not found components: schemas: User: type: object properties: id: type: string description: Unique user id name: type: string description: User name email: type: string description: Email address You can import this into Swagger Editor for visualization and testing.\nExplanation Why OpenAPI?\nStandardized: avoid custom formats Automated: generate SDKs, tests, mocks Interactive: Swagger UI allows live testing Alternatives:\nRAML (MuleSoft) API Blueprint (documentation-focused) OpenAPI wins because of its tooling ecosystem.\nCommon pitfalls Problem Cause Fix Docs and code drift manual updates auto-generate from code (FastAPI, Springdoc) Schema too complex deep nesting split models with $ref Missing fields in examples no mock testing use mock server to validate Best practices Version your API paths (e.g., /v1/) Standardize error format ({code, message, data}) Keep docs in sync with code Add real examples Validate OpenAPI in CI Summary Great API docs are not just documentation but a collaboration bridge. Swagger/OpenAPI is about making APIs machine-readable and human-usable. With the right structure and tools, your APIs become easier to maintain and test.\nReferences OpenAPI Specification Swagger Editor ReDoc Microsoft API Design Guidelines Meta Reading time: 7 minutes Tags: API docs, Swagger, OpenAPI, standards, Tony Tam SEO keywords: API documentation standard, Swagger tutorial, OpenAPI example, RESTful design Meta description: Based on Swagger/OpenAPI, this guide explains API doc structure, examples, and best practices. Call to Action (CTA) Try it now:\nOpen Swagger Editor and paste the YAML above Or follow this series on API design; next post: auto-generate SDKs with OpenAPI ","permalink":"https://jeanphilogong.github.io/jeanblog/thoughts/thoughts/api-standards/","summary":"\u003ch1 id=\"title\"\u003eTitle\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eHow to Write a Qualified API Document: From Swagger to Modern OpenAPI\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"subtitle--abstract\"\u003eSubtitle / Abstract\u003c/h2\u003e\n\u003cp\u003eWant developers to actually enjoy using your API? This article covers the structure, examples, and best practices of high-quality API documentation based on Swagger/OpenAPI (originally by Tony Tam).\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eBeginners who want a standard API doc structure\u003c/li\u003e\n\u003cli\u003eMid-level developers improving maintainability\u003c/li\u003e\n\u003cli\u003eArchitects and leads defining API standards\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eCommon problems in API docs:\u003c/p\u003e","title":"API Standards"},{"content":"For a system, a single thread should be a single assistant. We should provide each user with one assistant and optimize that assistant.\nProviding many parallel threads per user is too expensive and unnecessary.\n","permalink":"https://jeanphilogong.github.io/jeanblog/thoughts/thoughts/thoughts-on-ai-systems/","summary":"\u003cp\u003eFor a system, a single thread should be a single assistant. We should provide each user with one assistant and optimize that assistant.\u003c/p\u003e\n\u003cp\u003eProviding many parallel threads per user is too expensive and unnecessary.\u003c/p\u003e","title":"Thoughts on AI Systems"},{"content":"Run Gitea Locally: Your Private GitHub (with Existing Repo Import) Subtitle / Abstract: This guide walks you through installing the lightweight Git server Gitea on your local machine. No root required, no system pollution. Manage, browse, and push projects like GitHub, and import existing repos.\nTarget readers: Personal developers, indie engineers, and small team leads with basic Git knowledge.\nBackground / Motivation Many developers want:\nto host code inside a company or LAN to avoid cloud platforms (GitHub/Gitee) to have a web UI, pull requests, and code browsing GitLab is heavy (often multiple GB of RAM). Gitea is:\nlightweight a single binary supports PR, Wiki, Issues, CI/CD In minutes, you get a private \u0026ldquo;mini GitHub\u0026rdquo;.\nCore concepts Term Description GitLab most powerful open-source Git platform, heavy resource usage Gitea lightweight self-hosted Git service with GitHub-like UI Bare repo repo with history only, no working tree Pull Request merge request from one branch to another SQLite default lightweight database for Gitea Setup steps 1) Prepare environment Supported OS: Linux / macOS / Windows Recommended: RAM \u0026gt;= 512MB, disk \u0026gt;= 1GB\n2) Create directory and download mkdir -p ~/gitea cd ~/gitea wget -O gitea https://dl.gitea.io/gitea/1.22.0/gitea-1.22.0-linux-amd64 chmod +x gitea 3) Start Gitea ./gitea web --port 3000 Open: http://localhost:3000\n4) Install wizard Fill in:\nDB type: SQLite3 Repo root: /home/\u0026lt;username\u0026gt;/gitea/repos Base URL: http://localhost:3000 Create admin account Runnable example: push an existing repo Assume your local project is /home/gong/projects/scrapy:\nCreate a repo named scrapy in Gitea In your project directory: cd ~/projects/scrapy git remote set-url origin http://localhost:3000/JeanphiloGong/scrapy.git git push -u origin --all git push -u origin --tags Refresh the web UI to see full history.\nRegister as a system service 1) Prerequisites Assume Gitea is installed at:\n/home/gong/gitea Binary path:\n/home/gong/gitea/gitea Run as user gong and do not use root.\n2) Create systemd service Create /etc/systemd/system/gitea.service:\n[Unit] Description=Gitea (Self-hosted Git Service) After=network.target [Service] # User and group User=gong Group=gong # Working directory WorkingDirectory=/home/gong/gitea # Start command ExecStart=/home/gong/gitea/gitea web --config /home/gong/gitea/custom/conf/app.ini # Restart policy Restart=always RestartSec=10s # Environment (optional) Environment=USER=gong HOME=/home/gong GITEA_WORK_DIR=/home/gong/gitea # Security PrivateTmp=true ProtectSystem=full NoNewPrivileges=true [Install] WantedBy=multi-user.target Notes:\nWorkingDirectory is the Gitea directory ExecStart defines the launch command Restart=always ensures auto-restart 3) Load and enable # Reload systemd sudo systemctl daemon-reload # Enable auto-start sudo systemctl enable gitea # Start service sudo systemctl start gitea # Check status sudo systemctl status gitea Expected:\nActive: active (running) 4) View logs Real-time logs:\nsudo journalctl -u gitea -f History logs:\nsudo journalctl -u gitea --since \u0026#34;1 hour ago\u0026#34; Explanation Gitea is a Go-based self-hosted Git service. It manages local Git repos (e.g., ~/gitea/repos) and exposes GitHub-like operations via HTTP/SSH.\nCompared to git init --bare (just storage), Gitea adds web UI, users, PRs, and Wiki.\nCommon issues Issue Cause Fix Port 3000 in use Another service uses it Run ./gitea web --port 8080 Permission errors Gitea runs as current user Check repo directory permissions Push fails Repo init conflict Do not select \u0026ldquo;Initialize with README\u0026rdquo; Push slow/timeouts Using HTTP not SSH Configure SSH keys for faster pushes Best practices SQLite is enough for personal or small teams\nRun in background: nohup ./gitea web \u0026amp;\nBackup regularly:\n~/gitea/repos/ ~/gitea/data/gitea.db ~/gitea/custom/conf/app.ini If the team grows, move to a server or Docker\nSummary You have:\nDeployed Gitea locally Avoided port conflicts Pushed existing repos to Gitea Gained a web UI, PRs, and history You now have your own private \u0026ldquo;GitHub\u0026rdquo;.\nReferences Gitea docs Gitea downloads Pro Git book Forgejo Meta Reading time: 8 minutes Tags: Git, Gitea, self-hosted, DevOps, version-control SEO keywords: local Gitea install, self-hosted Git server, private GitHub, import local repo Meta description: Set up a lightweight local Git server with Gitea, including PRs, web UI, and repo management. Call to Action (CTA) Try it now:\nRun the install commands Visit http://localhost:3000 Create your first repo Push a project If you want automation and backup scripts, leave a comment and I will share a follow-up.\n","permalink":"https://jeanphilogong.github.io/jeanblog/notes/git-notes/configure-gitea/","summary":"\u003ch1 id=\"run-gitea-locally-your-private-github-with-existing-repo-import\"\u003eRun Gitea Locally: Your Private GitHub (with Existing Repo Import)\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract:\u003c/strong\u003e\nThis guide walks you through installing the lightweight Git server Gitea on your local machine. No root required, no system pollution. Manage, browse, and push projects like GitHub, and import existing repos.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTarget readers:\u003c/strong\u003e\nPersonal developers, indie engineers, and small team leads with basic Git knowledge.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h2\u003e\n\u003cp\u003eMany developers want:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eto host code inside a company or LAN\u003c/li\u003e\n\u003cli\u003eto avoid cloud platforms (GitHub/Gitee)\u003c/li\u003e\n\u003cli\u003eto have a web UI, pull requests, and code browsing\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eGitLab is heavy (often multiple GB of RAM). Gitea is:\u003c/p\u003e","title":"How to Set Up Gitea"},{"content":"Title From feat to fix: master Git commit conventions for collaboration and automation\nSubtitle / Abstract A practical guide to Conventional Commits. Learn commit types (feat:, fix:), write clean messages, and enable automatic changelogs and releases.\nTarget readers Beginners: new to Git, want better commit habits. Mid-level devs: want commits friendly to team and CI. Leads/architects: want a consistent team standard. Background / Motivation Most commit messages look like:\n\u0026ldquo;update code\u0026rdquo; \u0026ldquo;fix bug\u0026rdquo; \u0026ldquo;some changes\u0026rdquo;\nThey are readable short-term but useless long-term. As teams grow, it becomes hard to track intent or automate releases.\nConventional Commits provides a simple, unified format so commits are readable, traceable, and automatable.\nCore concept Conventional Commits define a commit message structure:\n\u0026lt;type\u0026gt;(\u0026lt;scope\u0026gt;): \u0026lt;subject\u0026gt; \u0026lt;body\u0026gt; \u0026lt;footer\u0026gt; type: commit type, e.g. feat, fix, docs scope: optional area, e.g. ui, api subject: short description (\u0026lt;= 50 chars) body: details (optional) footer: metadata (e.g., BREAKING CHANGE) Practical steps Set Git editor to Neovim (optional) git config --global core.editor \u0026#34;nvim\u0026#34; Write a standard commit message git commit -m \u0026#34;feat(lsp): support new nvim-lspconfig API\u0026#34; Structured commit example feat(lsp): update LSP config for new nvim-lspconfig - remove old lspconfig[server].setup - use new function call lspconfig(server, {...}) Enforce with tooling (optional) npm install -g commitlint @commitlint/config-conventional Create .commitlintrc.js:\nmodule.exports = { extends: [\u0026#34;@commitlint/config-conventional\u0026#34;] }; Runnable examples # new feature git commit -m \u0026#34;feat(auth): add two-factor login\u0026#34; # bug fix git commit -m \u0026#34;fix(ui): fix text invisibility in dark mode\u0026#34; # docs git commit -m \u0026#34;docs(readme): add usage notes\u0026#34; # refactor git commit -m \u0026#34;refactor(api): optimize auth logic\u0026#34; # performance git commit -m \u0026#34;perf(db): improve query cache\u0026#34; Explanation This standard comes from the Angular commit message format and became the Conventional Commits spec.\nBenefits:\nClear structure: see type and scope at a glance Machine-readable: auto changelog generation Easy integration: with semantic-release Alternatives:\nGitmoji (emoji commits) Semantic Versioning (release versioning) FAQ Question Answer Can I mix English and Chinese? Yes, but keep it consistent. Prefer English in the subject. What if one commit covers multiple types? Split into multiple commits. What if I cannot write a long message? At least explain \u0026ldquo;why\u0026rdquo; in one line. Is scope required? Optional, but recommended. Best practices One commit does one thing Subject in lowercase, no period First line \u0026lt;= 50 characters Blank line after subject, details start on line 3 Start with a verb (add, fix, update) Conclusion Commit conventions are a small investment with big returns: better history, smoother collaboration, and automation.\nReferences Conventional Commits Angular Commit Message Guidelines semantic-release Gitmoji Meta Reading time: about 6 minutes Tags: Git, standards, Conventional Commits, collaboration SEO keywords: Git commit conventions, Conventional Commits, feat fix refactor, best practices Meta description: A practical guide to writing clean commit messages with feat: and fix: and enabling automation. Call to Action (CTA) Try this in your next commit:\ngit commit -m \u0026#34;feat: first commit with conventional commits\u0026#34; Share your team conventions and lessons learned in the comments.\n","permalink":"https://jeanphilogong.github.io/jeanblog/notes/git-notes/git-commit-conventions-team-efficiency/","summary":"\u003ch3 id=\"title\"\u003eTitle\u003c/h3\u003e\n\u003cp\u003eFrom \u003ccode\u003efeat\u003c/code\u003e to \u003ccode\u003efix\u003c/code\u003e: master Git commit conventions for collaboration and automation\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"subtitle--abstract\"\u003eSubtitle / Abstract\u003c/h3\u003e\n\u003cp\u003eA practical guide to Conventional Commits. Learn commit types (\u003ccode\u003efeat:\u003c/code\u003e, \u003ccode\u003efix:\u003c/code\u003e), write clean messages, and enable automatic changelogs and releases.\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"target-readers\"\u003eTarget readers\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBeginners\u003c/strong\u003e: new to Git, want better commit habits.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMid-level devs\u003c/strong\u003e: want commits friendly to team and CI.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLeads/architects\u003c/strong\u003e: want a consistent team standard.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"background--motivation\"\u003eBackground / Motivation\u003c/h3\u003e\n\u003cp\u003eMost commit messages look like:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;update code\u0026rdquo;\n\u0026ldquo;fix bug\u0026rdquo;\n\u0026ldquo;some changes\u0026rdquo;\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Conventional Commits: Make Team Collaboration and Automation Efficient"},{"content":"Bengio-style ML Task Specification: From Research to Engineering Subtitle: How to write a reproducible, explainable, and comparable fine-tuning task document based on Yoshua Bengio\u0026rsquo;s methodology.\nReading time: 10 minutes Tags: ML documentation, fine-tuning, technical standards, deep learning practice Audience: mid to senior ML engineers, researchers, technical writers\n1. Why do we need this document? In ML projects, teams often run fine-tuning experiments. Months later, nobody can reproduce results or explain why a learning rate or LoRA layer was chosen.\nYoshua Bengio (one of the deep learning pioneers) proposed the idea that an ML task document must allow others to fully reproduce results and understand the design rationale. This became the Bengio-style ML project report structure, used by Google Research, Meta AI, OpenAI, and others.\n2. Core ideas of the Bengio template Item Description Source Yoshua Bengio, \u0026ldquo;Deep Learning Research Practice Notes\u0026rdquo; Goal Ensure ML experiments are reproducible, understandable, and comparable Use cases Fine-tuning, comparison studies, research reports, internal docs Benefits Clear structure, unified format, easy to convert into papers or internal whitepapers 3. Standard structure (nine sections) 1) Title page Document title (e.g., \u0026ldquo;Design and Implementation of Four Fine-Tuning Tasks\u0026rdquo;) Author, date, version Project or organization name 2) Abstract Briefly describe goals, model direction, and expected outcomes.\nExample:\nThis document describes the design, experiment plan, and evaluation for fine-tuning four architectures, comparing performance on a specific dataset.\n3) Background and motivation Explain:\ncurrent system limitations why fine-tuning is needed related papers and existing results scientific or business motivation Example: \u0026ldquo;Current LMs generalize poorly in low-resource domains, so we propose parameter-efficient fine-tuning on multilingual data.\u0026rdquo;\n4) Problem definition Define inputs/outputs, task type, and metrics:\nTask type: classification / generation / regression I/O format: text -\u0026gt; label or text -\u0026gt; text Metrics: accuracy, F1, BLEU, loss Constraints: compute budget, time, data privacy 5) Models and approach For each model, record:\narchitecture (Llama-3, Phi-3, Gemma, etc.) fine-tuning method (Full FT, LoRA, Adapter, QLoRA) key hyperparameters (batch size, epochs, LR) Model Method Dataset Epochs Learning Rate Model A LoRA Dataset X 5 3e-5 Model B Full Dataset X 3 2e-5 Model C Adapter Dataset Y 10 1e-4 Model D QLoRA Dataset Z 4 1e-5 6) Experimental setup Environment (GPU type, framework, version) Data split (train/val/test) Random seeds and reproducibility controls Logging tools (e.g., Weights \u0026amp; Biases) 7) Results and analysis Include:\nmetric tables and plots (accuracy, loss curves) model size vs performance trade-offs unexpected results and explanations Tip: include TensorBoard or matplotlib plots to show convergence trends.\n8) Conclusion and future work Which model performed best? Why (architecture, optimization)? Future directions (multi-task learning, quantization) 9) Appendix and references additional logs and code paths cited papers and open-source repos 4. Best practices Ensure reproducibility (version lock + seeds) Record motivation and assumptions per model Use tables/plots for comparability Use structured headings for team sharing and future papers 5. Summary The Bengio-style ML document is not just a format, it is a research culture. It makes collaboration transparent and results verifiable.\nReferences Yoshua Bengio, Deep Learning Research Practice Notes OpenAI Technical Reports (fine-tuning guides) Google Research: Effective ML Experiment Documentation Meta AI: Reproducibility Checklist for ML Models Call to Action Try writing your next fine-tuning report using this template. Use it as a team standard and improve reproducibility.\n","permalink":"https://jeanphilogong.github.io/jeanblog/thoughts/thoughts/how-to-write-a-perfect-ml-document/","summary":"\u003ch1 id=\"bengio-style-ml-task-specification-from-research-to-engineering\"\u003eBengio-style ML Task Specification: From Research to Engineering\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle:\u003c/strong\u003e\nHow to write a reproducible, explainable, and comparable fine-tuning task document based on Yoshua Bengio\u0026rsquo;s methodology.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReading time:\u003c/strong\u003e 10 minutes\n\u003cstrong\u003eTags:\u003c/strong\u003e ML documentation, fine-tuning, technical standards, deep learning practice\n\u003cstrong\u003eAudience:\u003c/strong\u003e mid to senior ML engineers, researchers, technical writers\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-why-do-we-need-this-document\"\u003e1. Why do we need this document?\u003c/h2\u003e\n\u003cp\u003eIn ML projects, teams often run fine-tuning experiments. Months later, nobody can reproduce results or explain why a learning rate or LoRA layer was chosen.\u003c/p\u003e","title":"How to Write a Perfect Machine Learning Document"},{"content":"Ping Works but SSH Fails: A Real Case of SSH vs VNC Subtitle: From connection refusal to protocol identification: understand TCP, SSH, and VNC Reading time: 7 minutes Tags: network troubleshooting, SSH, VNC, Linux, remote access SEO keywords: SSH connection failed, kex_exchange_identification, VNC port 5905, RFB 003.008, SSH vs VNC\nTarget readers Linux users, developers, and server admins Engineers learning systematic network troubleshooting Readers interested in SSH/VNC protocol behavior Background and motivation Have you seen this?\n\u0026ldquo;The server can be pinged, but SSH does not connect.\u0026rdquo;\nThis is common on hosts running multiple services (SSH, VNC, HTTP). This article walks through a real case: from \u0026ldquo;SSH failed\u0026rdquo; to finding out the port was actually VNC.\nSymptoms Command:\nssh chenhm@101.6.142.82 -p 5905 Output:\nkex_exchange_identification: Connection closed by remote host Connection closed by 101.6.142.82 port 5905 Ping test:\nping 101.6.142.82 It succeeds with no packet loss.\nSo we know:\nHost is online Network is reachable SSH handshake failed Core concepts Concept Meaning Ping ICMP test for connectivity only TCP Transport protocol that builds connections SSH Application protocol on top of TCP for secure login VNC / RFB Remote desktop protocol (Remote Frame Buffer) In short: Ping OK != SSH OK because they are different layers.\nTroubleshooting steps Step 1. Test TCP connectivity telnet 101.6.142.82 5905 Output:\nTrying 101.6.142.82... Connected to 101.6.142.82. Escape character is \u0026#39;^]\u0026#39;. RFB 003.008 Key clue: RFB 003.008 is the VNC handshake string (Remote Frame Buffer v3.8).\nThis means:\nPort 5905 is open It is running VNC, not SSH Why this happens After TCP connects, SSH sends a greeting like SSH-2.0-OpenSSH_8.x. A VNC server replies with RFB 003.008 instead. Protocol mismatch causes the SSH client to close, resulting in kex_exchange_identification.\nVerification Check the process on the port sudo ss -tlnp | grep 5905 Possible output:\nLISTEN 0 5 0.0.0.0:5905 ... /usr/bin/Xvnc Check SSH port sudo grep ^Port /etc/ssh/sshd_config If it returns Port 22, SSH is still on the default port.\nCorrect connection If you want the GUI Use a VNC client:\nvncviewer 101.6.142.82:5905 Or tools like:\nRealVNC TigerVNC TightVNC If you want the terminal Use SSH on the correct port:\nssh chenhm@101.6.142.82 -p 22 Common issues and fixes Problem Cause Fix Connection closed by remote host Protocol mismatch (SSH to VNC) Use the correct protocol SSH fails on all ports SSH service not running sudo systemctl start sshd VNC refused Firewall blocked firewall-cmd --add-port=5905/tcp --permanent SSH disconnected fail2ban ban check /var/log/auth.log Best practices Separate port and protocol: port number alone does not identify service type. Use telnet or nc to read protocol banners. Check logs: journalctl -u ssh, /var/log/auth.log. Define a clear port map for multi-service hosts: SSH -\u0026gt; 22 VNC -\u0026gt; 5900+ HTTP -\u0026gt; 80/8080 HTTPS -\u0026gt; 443 Summary This case shows:\nHow to separate network, transport, and application layer issues How to identify protocol banners (RFB vs SSH) How to find the real service on a port One-line conclusion:\nSSH is fine; you connected to the wrong service.\nReferences OpenSSH Manual TigerVNC GitHub [Linux man pages: ssh, telnet, ss, netstat] RFC 6143: The Remote Framebuffer Protocol (RFB) Call to Action Try this on your own server:\nnc \u0026lt;server_ip\u0026gt; \u0026lt;port\u0026gt; Check the banner you get back. You may discover more \u0026ldquo;hidden services\u0026rdquo;.\n","permalink":"https://jeanphilogong.github.io/jeanblog/linux/linux/ping-works-ssh-fails-fake-ssh-true-vnc/","summary":"\u003ch1 id=\"ping-works-but-ssh-fails-a-real-case-of-ssh-vs-vnc\"\u003ePing Works but SSH Fails: A Real Case of SSH vs VNC\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle:\u003c/strong\u003e From connection refusal to protocol identification: understand TCP, SSH, and VNC\n\u003cstrong\u003eReading time:\u003c/strong\u003e 7 minutes\n\u003cstrong\u003eTags:\u003c/strong\u003e network troubleshooting, SSH, VNC, Linux, remote access\n\u003cstrong\u003eSEO keywords:\u003c/strong\u003e SSH connection failed, kex_exchange_identification, VNC port 5905, RFB 003.008, SSH vs VNC\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eLinux users, developers, and server admins\u003c/li\u003e\n\u003cli\u003eEngineers learning systematic network troubleshooting\u003c/li\u003e\n\u003cli\u003eReaders interested in SSH/VNC protocol behavior\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"background-and-motivation\"\u003eBackground and motivation\u003c/h2\u003e\n\u003cp\u003eHave you seen this?\u003c/p\u003e","title":"Ping Works but SSH Fails: A Real Case of SSH vs VNC"},{"content":"Below is a full draft based on your SSH startup and debugging process. It is ready for publication on a technical blog.\nRun SSH Without sudo on Linux (User-Level sshd Guide) Subtitle / Abstract: When you have no root access in a lab or restricted server environment, how do you start SSH and access your account remotely? This guide shows how to run sshd in your user directory, enable key login, and connect remotely.\nReading time: 10 minutes Target readers: intermediate Linux users, researchers, server users, DevOps learners Tags: SSH, sshd, Linux, remote access, non-root, system config SEO keywords: SSH without root, user-level sshd, openssh config, unprivileged ports, remote login failed\nBackground and motivation Many research servers and shared hosts do not grant sudo. But we still need:\nremote login file upload/download access from another machine By default, sshd requires root because it binds port 22 and reads system auth info. However, you can run a user-level SSH service in your home directory without changing system config.\nCore concepts Term Meaning sshd SSH server daemon that accepts connections user-space sshd sshd started by a normal user, no root privileges HostKey key pair used to encrypt SSH connections AuthorizedKeys list of public keys allowed to log in /etc/shadow password hash file; non-root cannot read Step-by-step: Start user-level SSH Step 1: Prepare config Create directory:\nmkdir -p ~/.ssh Create config ~/.ssh/ssh_config:\nPort 2222 ListenAddress 0.0.0.0 HostKey /home/\u0026lt;username\u0026gt;/.ssh/ssh_host_ed25519_key AuthorizedKeysFile /home/\u0026lt;username\u0026gt;/.ssh/authorized_keys PasswordAuthentication yes PubkeyAuthentication yes ChallengeResponseAuthentication no PidFile /home/\u0026lt;username\u0026gt;/.ssh/sshd.pid Note: do not use ~ in paths; OpenSSH will not expand it.\nStep 2: Generate host keys ssh-keygen -t ed25519 -f ~/.ssh/ssh_host_ed25519_key -N \u0026#34;\u0026#34; chmod 600 ~/.ssh/ssh_host_ed25519_key Step 3: Start user-level sshd /usr/bin/sshd -d -f ~/.ssh/ssh_config If you see:\nServer listening on 0.0.0.0 port 2222. then it is running. Test locally:\nssh -p 2222 \u0026lt;username\u0026gt;@localhost Explanation Why use port 2222? Ports \u0026lt; 1024 are privileged and require root. Use 2222 or 8022 instead.\nWhy \u0026ldquo;Could not get shadow information\u0026rdquo;? Non-root users cannot read /etc/shadow, so password auth fails. Use public keys instead.\nUse SSH key login (recommended) Generate local key (no email comment):\nssh-keygen -t ed25519 -C \u0026#34;\u0026#34; -f ~/.ssh/id_ed25519_noemail Add to authorized keys:\ncat ~/.ssh/id_ed25519_noemail.pub \u0026gt;\u0026gt; ~/.ssh/authorized_keys chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys Test login:\nssh -i ~/.ssh/id_ed25519_noemail -p 2222 \u0026lt;username\u0026gt;@localhost Allow remote access Ensure sshd listens on all addresses\nss -tlnp | grep 2222 If output is 127.0.0.1:2222, it is local only. Set:\nListenAddress 0.0.0.0 and restart sshd.\nFirewall and NAT\nIf external access shows \u0026ldquo;Connection refused\u0026rdquo;, firewall or NAT is blocking. If localhost works but public IP fails, open the port or configure forwarding. Run sshd in background\nnohup /usr/bin/sshd -f ~/.ssh/ssh_config -E ~/.ssh/sshd.log \u0026amp; tail -f ~/.ssh/sshd.log Common issues Issue Cause Fix Permission denied (password) cannot read /etc/shadow use key auth Address already in use port in use kill old process or change port Bind to port failed tried port 22 use port \u0026gt; 1024 Connection refused firewall / NAT block check listen address and policies Could not load host key HostKey path wrong use absolute path and chmod 600 Best practices Use ed25519 keys (secure and fast). In non-root environments, use key-only auth. Keep ~/.ssh at 700 and authorized_keys at 600. Do not expose your home directory or host keys. If remote access is needed, ensure ListenAddress 0.0.0.0 and open ports. Summary This guide shows how to:\nStart SSH without sudo Enable key auth to avoid /etc/shadow Support both local and remote login Debug common errors like \u0026ldquo;Connection refused\u0026rdquo; You can now run your own SSH service under a normal account.\nReferences OpenSSH manual man sshd_config RFC 4251: The Secure Shell Protocol Architecture Linux file permissions Call to Action (CTA) Try starting your own user-level sshd using the steps above. Save and share this guide for restricted environments. Share your SSH deployment pitfalls and fixes. Do you want a Markdown version with syntax highlighting ready for publishing?\n","permalink":"https://jeanphilogong.github.io/jeanblog/linux/linux/enable-ssh-without-sudo/","summary":"\u003cp\u003eBelow is a full draft based on your SSH startup and debugging process. It is ready for publication on a technical blog.\u003c/p\u003e\n\u003chr\u003e\n\u003ch1 id=\"run-ssh-without-sudo-on-linux-user-level-sshd-guide\"\u003eRun SSH Without sudo on Linux (User-Level sshd Guide)\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract:\u003c/strong\u003e\nWhen you have no root access in a lab or restricted server environment, how do you start SSH and access your account remotely? This guide shows how to run \u003ccode\u003esshd\u003c/code\u003e in your user directory, enable key login, and connect remotely.\u003c/p\u003e","title":"Run SSH Without sudo: User-Level sshd on Linux"},{"content":"Title: Run sshd Without sudo: Troubleshooting, nohup, and systemd (User-Level SSH)\nSubtitle / Abstract: How to run OpenSSH as a normal user, solve common errors like \u0026ldquo;connection refused\u0026rdquo;, \u0026ldquo;password auth failed\u0026rdquo;, and start-limit-hit, and keep sshd alive using nohup or systemd.\nTarget readers: Intermediate Linux users, researchers on shared servers, and anyone who needs SSH without root.\n1. Background / Motivation In some lab or shared environments, regular users do not have sudo. The default sshd service cannot be started. If you need to:\nremote into your Linux host use VS Code Remote or SCP but cannot change system config then you must run sshd in user space. This introduces issues: port conflicts, firewall rules, auth failures, and start-limit-hit.\n2. Core concepts Term Meaning sshd OpenSSH daemon that handles SSH logins user-level sshd sshd started by a normal user, no root privileges authorized_keys list of allowed public keys nohup run a process detached from the terminal systemd \u0026ndash;user user-level systemd instance for services start-limit-hit systemd pauses restarts after frequent failures 3. Full setup steps 1) Generate and configure SSH keys ssh-keygen -t ed25519 -C \u0026#34;\u0026#34; -f ~/.ssh/id_ed25519_noemail cat ~/.ssh/id_ed25519_noemail.pub \u0026gt;\u0026gt; ~/.ssh/authorized_keys chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys Ensure ~/.ssh/authorized_keys permissions are correct.\n2) Create a user-level sshd config ~/.ssh/ssh_config_pub\nPort 2223 ListenAddress 0.0.0.0 HostKey /home/chenhm/.ssh/ssh_host_ed25519_key AuthorizedKeysFile /home/chenhm/.ssh/authorized_keys PasswordAuthentication no PubkeyAuthentication yes PidFile /home/chenhm/.ssh/sshd_pub.pid LogLevel INFO SyslogFacility AUTH Generate host key:\nssh-keygen -t ed25519 -f ~/.ssh/ssh_host_ed25519_key -N \u0026#34;\u0026#34; 3) Start in debug mode /usr/bin/sshd -d -f ~/.ssh/ssh_config_pub If you see:\nServer listening on 0.0.0.0 port 2223\nthen it is running.\n4. Two ways to keep it running Option A: nohup (simplest) nohup /usr/bin/sshd -f ~/.ssh/ssh_config_pub -E ~/.ssh/sshd_pub.log \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 \u0026amp; Runs after terminal closes\nCheck process:\nps -ef | grep \u0026#34;sshd -f\u0026#34; Check logs:\ntail -f ~/.ssh/sshd_pub.log Stop:\npkill -f \u0026#34;sshd -f /home/chenhm/.ssh/ssh_config_pub\u0026#34; Pros: no dependencies, works instantly. Cons: does not auto-start after reboot.\nOption B: systemd user service (auto-restart/auto-start) 1) Create the unit file ~/.config/systemd/user/sshd-user.service\n[Unit] Description=User-level SSH server [Service] Type=forking ExecStart=/usr/bin/sshd -f /home/chenhm/.ssh/ssh_config_pub -E /home/chenhm/.ssh/sshd_pub.log PIDFile=/home/chenhm/.ssh/sshd_pub.pid Restart=on-failure RestartSec=5 [Install] WantedBy=default.target 2) Enable and start systemctl --user daemon-reload systemctl --user enable sshd-user systemctl --user start sshd-user 3) Verify systemctl --user status sshd-user ss -tlnp | grep sshd You should see Active: active (running) and 0.0.0.0:2223.\n5. Troubleshooting table Error Cause Fix Connection refused sshd not listening on public interface or firewall blocked set ListenAddress 0.0.0.0, check ss -tlnp Permission denied (password) no access to /etc/shadow use public key auth Bind to port ... failed: Address already in use port already used by old sshd pkill -f \u0026quot;sshd -f\u0026quot; start-limit-hit systemd sees frequent crashes set Type=forking and PIDFile= No logs wrong path or permission use -E ~/.ssh/sshd.log 6. Why this works User-level sshd does not need root because it binds to ports \u0026gt;= 1024. Public key auth avoids /etc/shadow access. Type=forking lets systemd track the daemon correctly. PIDFile helps systemd manage the process. 7. Notes Port \u0026gt; 1024: non-root cannot bind to privileged ports. Firewall: must allow your chosen port. Permissions: ~/.ssh must be 700 and authorized_keys must be 600. Multiple instances: use separate PidFile and log paths. Auto-start: systemctl --user enable sshd-user. 8. Best practices Use nohup for testing or temporary runs. Use systemd \u0026ndash;user for stable long-term service. Expose only key-based auth on public interfaces. Separate internal vs external ports. Use @reboot cron as fallback if systemd is unavailable. 9. Conclusion This guide showed how to deploy SSH without sudo:\nGenerate keys and enable key auth Create user-level sshd config Validate with nohup, then stabilize with systemd Fix start-limit-hit, port conflicts, and auth failures You get:\nMultiple ports and instances Auto-restart Auto-start Secure remote access References OpenSSH manual systemd user services OpenSSH key management Meta\nReading time: about 10 minutes Tags: SSH, Linux, systemd, nohup, no-sudo SEO keywords: no-sudo sshd systemd user OpenSSH start-limit-hit Meta description: A complete guide to running OpenSSH without sudo and fixing common startup errors. Call to Action (CTA) Try running a user-level sshd on your lab server. If this helps, share your setup and lessons learned.\n","permalink":"https://jeanphilogong.github.io/jeanblog/linux/linux/fix-sshsystem-process-start-failure/","summary":"\u003cp\u003e\u003cstrong\u003eTitle:\u003c/strong\u003e\nRun sshd Without sudo: Troubleshooting, nohup, and systemd (User-Level SSH)\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract:\u003c/strong\u003e\nHow to run OpenSSH as a normal user, solve common errors like \u0026ldquo;connection refused\u0026rdquo;, \u0026ldquo;password auth failed\u0026rdquo;, and \u003ccode\u003estart-limit-hit\u003c/code\u003e, and keep sshd alive using nohup or systemd.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eTarget readers:\u003c/strong\u003e\nIntermediate Linux users, researchers on shared servers, and anyone who needs SSH without root.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-background--motivation\"\u003e1. Background / Motivation\u003c/h2\u003e\n\u003cp\u003eIn some lab or shared environments, regular users do not have sudo. The default sshd service cannot be started. If you need to:\u003c/p\u003e","title":"Run sshd Without sudo: Troubleshooting and Persistent User-Level SSH"},{"content":"Auto-start frp on Ubuntu: A Complete Guide Subtitle / Abstract Use systemd to run frp (Fast Reverse Proxy) as a managed service for stable, secure, and monitored auto-start on boot.\nReading time: 8 minutes Tags: frp, intranet tunneling, systemd, auto-start, Linux, Ubuntu SEO keywords: frp auto start, Ubuntu frp config, frpc systemd, frps service, intranet tunneling Meta description: Step-by-step systemd setup for frp (frpc/frps) with config templates and troubleshooting.\nTarget readers Developers deploying frps on cloud servers Intermediate Linux users building stable home/office tunnels DevOps and self-hosting enthusiasts Background and motivation Many developers use frp to expose internal services (SSH, web, NAS) to the internet. The problem is that running ./frpc -c frpc.ini manually is inconvenient and unreliable after reboot.\nWe want auto-start on boot + auto-restart on failure + centralized logs, which is exactly what systemd provides.\nCore concepts frps / frpc: server and client binaries for frp systemd: service manager for modern Linux unit file: configuration for service startup, dependencies, and restart policy Step-by-step setup 1) Install and place files sudo mv frpc /usr/local/bin/ sudo chmod +x /usr/local/bin/frpc sudo mkdir -p /etc/frp sudo mv frpc.ini /etc/frp/frpc.ini For the server side, replace frpc with frps and frpc.ini with frps.ini.\n2) (Optional) Create a dedicated user sudo useradd --system --no-create-home --shell /sbin/nologin frp sudo chown -R frp:frp /etc/frp 3) Create a systemd unit Create /etc/systemd/system/frpc.service:\n[Unit] Description=frp client service After=network-online.target Wants=network-online.target [Service] Type=simple User=frp Group=frp ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.ini Restart=on-failure RestartSec=5 LimitNOFILE=65536 [Install] WantedBy=multi-user.target 4) Start and enable sudo systemctl daemon-reload sudo systemctl start frpc sudo systemctl enable frpc 5) Check status and logs sudo systemctl status frpc sudo journalctl -u frpc -f Logs are centralized in the systemd journal for easier troubleshooting.\nHow it works WantedBy=multi-user.target ensures auto-start during boot. After=network-online.target starts only after the network is ready. Restart=on-failure auto-restarts frpc on unexpected exit. Compared to @reboot cron, systemd gives better dependency control, restart policy, and unified logs.\nCommon issues and fixes Issue Cause Fix Service fails to start Config file permission issue Ensure /etc/frp/frpc.ini is readable by user frp Network not ready Missing systemd dependencies Enable systemd-networkd-wait-online.service frp cannot connect Firewall or security group blocks Open TCP/UDP ports Service not auto-starting enable not run sudo systemctl enable frpc Best practices Run with non-root user for safety. Ship logs to ELK/Promtail if needed. Enable token auth or TLS in frp configs. For multiple frpc instances, use frpc@name.service templates. Summary You learned how to:\nInstall and configure frp Create a systemd service Enable auto-start and auto-restart Understand common pitfalls Once you understand systemd, you can manage any custom daemon the same way.\nReferences frp docs: https://github.com/fatedier/frp systemd.service: https://www.freedesktop.org/software/systemd/man/systemd.service.html Ubuntu Server Guide - systemd: https://ubuntu.com/server/docs/service-systemd Call to Action Copy the unit file to your server and run sudo systemctl enable --now frpc. If it works, share what you are exposing via frp. You can also publish a template or script for others.\n","permalink":"https://jeanphilogong.github.io/jeanblog/linux/linux/frp-auto-start-on-ubuntu/","summary":"\u003ch1 id=\"auto-start-frp-on-ubuntu-a-complete-guide\"\u003eAuto-start frp on Ubuntu: A Complete Guide\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eSubtitle / Abstract\u003c/strong\u003e\nUse systemd to run frp (Fast Reverse Proxy) as a managed service for stable, secure, and monitored auto-start on boot.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eReading time\u003c/strong\u003e: 8 minutes\n\u003cstrong\u003eTags\u003c/strong\u003e: frp, intranet tunneling, systemd, auto-start, Linux, Ubuntu\n\u003cstrong\u003eSEO keywords\u003c/strong\u003e: frp auto start, Ubuntu frp config, frpc systemd, frps service, intranet tunneling\n\u003cstrong\u003eMeta description\u003c/strong\u003e: Step-by-step systemd setup for frp (frpc/frps) with config templates and troubleshooting.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"target-readers\"\u003eTarget readers\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eDevelopers deploying frps on cloud servers\u003c/li\u003e\n\u003cli\u003eIntermediate Linux users building stable home/office tunnels\u003c/li\u003e\n\u003cli\u003eDevOps and self-hosting enthusiasts\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"background-and-motivation\"\u003eBackground and motivation\u003c/h2\u003e\n\u003cp\u003eMany developers use \u003cstrong\u003efrp\u003c/strong\u003e to expose internal services (SSH, web, NAS) to the internet. The problem is that running \u003ccode\u003e./frpc -c frpc.ini\u003c/code\u003e manually is inconvenient and unreliable after reboot.\u003c/p\u003e","title":"Auto-start frp on Ubuntu with systemd"},{"content":"Windows + WSL2 Port Forwarding Guide (Access Flask 5000) Prerequisites You are using WSL2 (Ubuntu or another Linux distro) The Windows host can access the LAN (Wi-Fi or Ethernet) A Flask service is running inside WSL2 and listening on: app.run(host=\u0026#34;0.0.0.0\u0026#34;, port=5000) host=\u0026quot;0.0.0.0\u0026quot; is required; otherwise external access will fail.\nStep 1: Check the WSL2 IP In WSL2:\nip addr show eth0 You should see something like:\ninet 172.26.209.37/20 Record the IP after inet (here: 172.26.209.37). This is the WSL2 internal IP.\nStep 2: Open PowerShell (Admin) Press Win + X and select Windows PowerShell (Admin) Confirm admin privileges if prompted by UAC Step 3: Add Port Forwarding In PowerShell, forward Windows port 5000 to WSL2:\n# Forward Windows port 5000 to WSL2 port 5000 netsh interface portproxy add v4tov4 listenport=5000 listenaddress=0.0.0.0 connectport=5000 connectaddress=172.26.209.37 # Allow LAN access through the firewall netsh advfirewall firewall add rule name=\u0026#34;WSL Flask 5000\u0026#34; dir=in action=allow protocol=TCP localport=5000 listenaddress=0.0.0.0 listens on all Windows interfaces connectaddress=172.26.209.37 is the WSL2 internal IP The firewall rule allows LAN devices to access Windows port 5000 Step 4: Test the Forwarding On the Windows machine: curl http://localhost:5000 # or curl http://192.168.1.227:5000 From another LAN device: http://\u0026lt;Windows-LAN-IP\u0026gt;:5000 Example:\nhttp://192.168.1.227:5000 Step 5 (Optional): Auto-update Script WSL2 IP can change after reboot. You can create a PowerShell script wsl_port_forward.ps1 to update rules:\n# Get current WSL IP $wsl_ip = wsl hostname -I | ForEach-Object { $_.Split(\u0026#34; \u0026#34;)[0] } Write-Host \u0026#34;Detected WSL IP: $wsl_ip\u0026#34; # Remove old rule netsh interface portproxy delete v4tov4 listenport=5000 listenaddress=0.0.0.0 # Add new rule netsh interface portproxy add v4tov4 listenport=5000 listenaddress=0.0.0.0 connectport=5000 connectaddress=$wsl_ip # Allow firewall netsh advfirewall firewall add rule name=\u0026#34;WSL Flask 5000\u0026#34; dir=in action=allow protocol=TCP localport=5000 Run this script before starting WSL each time It detects the current WSL IP and updates the forwarding rule Step 6: Notes Flask must listen on 0.0.0.0, otherwise only local access works Ensure Windows Firewall allows TCP port 5000 If LAN devices still cannot access: Check router policies for blocked LAN ports Verify Windows firewall rules WSL2 uses NAT; LAN devices cannot reach the WSL IP directly. Use Windows IP + port forwarding Summary WSL2 networking is isolated by default; LAN cannot access WSL directly Windows port forwarding + firewall rules enable LAN access to WSL services An auto-update script can handle WSL IP changes after reboot ","permalink":"https://jeanphilogong.github.io/jeanblog/linux/linux/wsl-intranet-not-shared-with-windows/","summary":"\u003ch1 id=\"windows--wsl2-port-forwarding-guide-access-flask-5000\"\u003eWindows + WSL2 Port Forwarding Guide (Access Flask 5000)\u003c/h1\u003e\n\u003ch2 id=\"prerequisites\"\u003ePrerequisites\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eYou are using \u003cstrong\u003eWSL2\u003c/strong\u003e (Ubuntu or another Linux distro)\u003c/li\u003e\n\u003cli\u003eThe Windows host can access the LAN (Wi-Fi or Ethernet)\u003c/li\u003e\n\u003cli\u003eA Flask service is running inside WSL2 and listening on:\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-python\" data-lang=\"python\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eapp\u003cspan style=\"color:#f92672\"\u003e.\u003c/span\u003erun(host\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;0.0.0.0\u0026#34;\u003c/span\u003e, port\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e5000\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e\u003ccode\u003ehost=\u0026quot;0.0.0.0\u0026quot;\u003c/code\u003e is required; otherwise external access will fail.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"step-1-check-the-wsl2-ip\"\u003eStep 1: Check the WSL2 IP\u003c/h2\u003e\n\u003cp\u003eIn WSL2:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eip addr show eth0\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eYou should see something like:\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003einet 172.26.209.37/20\n\u003c/code\u003e\u003c/pre\u003e\u003cblockquote\u003e\n\u003cp\u003eRecord the IP after \u003ccode\u003einet\u003c/code\u003e (here: \u003ccode\u003e172.26.209.37\u003c/code\u003e). This is the WSL2 internal IP.\u003c/p\u003e\n\u003c/blockquote\u003e","title":"Expose WSL2 Services to the LAN via Windows Port Forwarding"},{"content":"Load Testing APIs with wrk (Detailed Guide) This article explains how to use wrk on Ubuntu to stress-test backend APIs (Flask, FastAPI, Spring Boot, etc.) and interpret the results.\n1. What is wrk? wrk is a modern, high-performance HTTP benchmarking tool written in C. Key features:\nHigh concurrency: thousands of concurrent connections Multi-threaded: uses multiple CPU cores Lua scripting: for custom headers, bodies, tokens Faster than Apache Benchmark (ab): lighter and more stable 2. Install wrk On Ubuntu/Debian:\nsudo apt update sudo apt install wrk -y Verify:\nwrk --version Expected output:\nwrk 4.2.0 [epoll] 3. Quick start Suppose your service is at:\nhttp://192.168.1.224:5000/api/tenders Run:\nwrk -t4 -c100 -d30s http://192.168.1.224:5000/api/tenders Parameters Flag Meaning -t4 4 threads (use multi-core CPU) -c100 100 concurrent connections -d30s 30 seconds duration last arg target URL 4. Sample output explained Example output:\nRunning 30s test @ http://192.168.1.224:5000/api/tenders 4 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.12s 248.83ms 1.99s 85.59% Req/Sec 22.88 14.29 90.00 77.73% 2452 requests in 30.09s, 27.02MB read Socket errors: connect 0, read 0, write 0, timeout 2 Requests/sec: 81.49 Transfer/sec: 0.90MB Metrics Metric Meaning Example Notes Latency Average response time 1.12s Slow if \u0026gt; 1s Req/Sec Requests per thread per second 22.88 Depends on thread count Requests/sec Total QPS 81.49 Throughput Transfer/sec Data per second 0.90MB Bandwidth usage Timeouts Timed-out requests 2 Indicates delays In general:\nExcellent: latency \u0026lt; 200ms OK: 200-800ms Slow: \u0026gt; 1s 5. Tips to improve concurrency 1) Use a production server (Flask example) Do not use app.run(). Use Gunicorn:\npip install gunicorn gunicorn -w 4 -b 0.0.0.0:5000 run:app -w 4: 4 worker processes (recommended 2 * CPU + 1) Improves concurrency and stability 2) Increase async throughput (I/O-bound APIs) gunicorn -w 4 -k gevent -b 0.0.0.0:5000 run:app -k gevent uses async workers to handle many waiting requests.\n3) Reduce response size Large responses consume network bandwidth. Suggestions:\nReturn only required fields Enable gzip (Nginx or Flask plugins) 6. Advanced: Lua scripts for custom requests Lua scripts can do:\nCustom headers and tokens POST JSON bodies Randomized parameters Example post.lua:\nwrk.method = \u0026#34;POST\u0026#34; wrk.body = \u0026#39;{\u0026#34;keyword\u0026#34;:\u0026#34;test\u0026#34;}\u0026#39; wrk.headers[\u0026#34;Content-Type\u0026#34;] = \u0026#34;application/json\u0026#34; Run:\nwrk -t4 -c100 -d30s -s post.lua http://127.0.0.1:5000/api/search ","permalink":"https://jeanphilogong.github.io/jeanblog/linux/linux/wrk-load-testing-guide/","summary":"\u003ch1 id=\"load-testing-apis-with-wrk-detailed-guide\"\u003eLoad Testing APIs with wrk (Detailed Guide)\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003eThis article explains how to use \u003ccode\u003ewrk\u003c/code\u003e on Ubuntu to stress-test backend APIs (Flask, FastAPI, Spring Boot, etc.) and interpret the results.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-what-is-wrk\"\u003e1. What is wrk?\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/wg/wrk\"\u003e\u003ccode\u003ewrk\u003c/code\u003e\u003c/a\u003e is a modern, high-performance HTTP benchmarking tool written in C. Key features:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eHigh concurrency\u003c/strong\u003e: thousands of concurrent connections\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMulti-threaded\u003c/strong\u003e: uses multiple CPU cores\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLua scripting\u003c/strong\u003e: for custom headers, bodies, tokens\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eFaster than Apache Benchmark (ab)\u003c/strong\u003e: lighter and more stable\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-install-wrk\"\u003e2. Install wrk\u003c/h2\u003e\n\u003cp\u003eOn Ubuntu/Debian:\u003c/p\u003e","title":"How to Use wrk for Load Testing"},{"content":"Access a Git Bare Repo on Windows WSL2 from the LAN In development, you often need to share Git repositories across multiple machines. If you use WSL2 on Windows and want other LAN machines to access a Git bare repo inside WSL2, this guide walks you through the setup.\n1. Create a Git bare repo in WSL2 In WSL2, go to the target directory:\ngit init --bare my_project.git my_project.git is a bare repo with no working tree, only Git data. A bare repo behaves like a remote and can be cloned and pushed. 2. Enable SSH in WSL2 Other machines will access via SSH.\nInstall SSH server: sudo apt update sudo apt install openssh-server -y Start SSH: sudo service ssh start Check status: sudo service ssh status Default port is 22; you can change it in /etc/ssh/sshd_config. 3. Get the WSL2 IP In WSL2:\nip addr Find the inet under eth0, for example:\ninet 172.25.190.21/20 Note: WSL2 IP can change after reboot.\n4. Configure Windows Firewall Allow SSH port through firewall:\nWindows Firewall -\u0026gt; Advanced settings -\u0026gt; Inbound rules -\u0026gt; New rule Rule type: Port -\u0026gt; TCP -\u0026gt; Port 22 (or custom like 2222) Allow connection -\u0026gt; Apply to Domain/Private/Public Name the rule and finish 5. Recommended: Windows port forwarding Because WSL2 IP changes, use Windows port forwarding:\nOpen PowerShell (Admin): netsh interface portproxy add v4tov4 listenport=2222 listenaddress=0.0.0.0 connectport=22 connectaddress=\u0026lt;WSL_IP\u0026gt; From another LAN machine, access via Windows IP + 2222: git clone ssh://user@WINDOWS_IP:2222/home/user/my_project.git user is your WSL2 username WINDOWS_IP is the Windows host LAN IP 6. Clone, push, pull from another machine Clone:\ngit clone ssh://user@WINDOWS_IP:2222/home/user/my_project.git Commit and push:\ngit add . git commit -m \u0026#34;update\u0026#34; git push origin main # or master Pull updates:\ngit pull origin main 7. Summary WSL2 has a virtual network; its IP may change on each boot. Port forwarding + firewall rules are the most reliable solution. A bare repo inside WSL2 works like a remote for LAN access. With these steps, multiple LAN machines can access a WSL2 Git repo for easy collaboration.\n","permalink":"https://jeanphilogong.github.io/jeanblog/notes/git-notes/lan-git-bare-repo/","summary":"\u003ch1 id=\"access-a-git-bare-repo-on-windows-wsl2-from-the-lan\"\u003eAccess a Git Bare Repo on Windows WSL2 from the LAN\u003c/h1\u003e\n\u003cp\u003eIn development, you often need to share Git repositories across multiple machines. If you use WSL2 on Windows and want other LAN machines to access a Git bare repo inside WSL2, this guide walks you through the setup.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-create-a-git-bare-repo-in-wsl2\"\u003e1. Create a Git bare repo in WSL2\u003c/h2\u003e\n\u003cp\u003eIn WSL2, go to the target directory:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit init --bare my_project.git\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e\u003ccode\u003emy_project.git\u003c/code\u003e is a bare repo with no working tree, only Git data.\u003c/li\u003e\n\u003cli\u003eA bare repo behaves like a remote and can be cloned and pushed.\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-enable-ssh-in-wsl2\"\u003e2. Enable SSH in WSL2\u003c/h2\u003e\n\u003cp\u003eOther machines will access via SSH.\u003c/p\u003e","title":"LAN Git Bare on WSL2"},{"content":"Simplified Git Branch Workflow (Solo / Small Team) This workflow is a simplified version of Git Flow. It is suitable for personal projects or small teams: structured but not heavy.\n1. Main branch (long-lived) main Always stable and release-ready Production deployments come from here For small teams, main is usually enough; no need for develop.\n2. Feature development (feature branch) Naming: feature/\u0026lt;feature-name\u0026gt; Purpose: build new features, then merge back to main Examples:\nfeature/login-api feature/user-profile Flow:\n# create feature branch from main git checkout -b feature/login-api main # merge back to main when done git checkout main git merge feature/login-api git branch -d feature/login-api 3. Bug fixes (bugfix branch) Naming: bugfix/\u0026lt;issue-name\u0026gt; Purpose: fix bugs in dev/test Example:\nbugfix/fix-login-redirect Same flow as feature branch, merge back to main.\n4. Hotfixes (hotfix branch) Naming: hotfix/\u0026lt;issue-name\u0026gt; Purpose: urgent production fixes Example:\nhotfix/security-patch Flow:\ngit checkout -b hotfix/security-patch main # fix, commit git checkout main git merge hotfix/security-patch git branch -d hotfix/security-patch 5. Releases (tags) Use Git tags for release versions No dedicated release branch required Example:\ngit tag v1.0.0 git push origin v1.0.0 Minimal recommended rules Permanent branch: main Temporary branches: feature/..., bugfix/..., hotfix/... Use tags for releases, no separate release branch Branch lifecycle diagram gitGraph commit id: \u0026#34;Init main\u0026#34; branch feature/login-api commit id: \u0026#34;Build login API\u0026#34; checkout main merge feature/login-api id: \u0026#34;Merge feature\u0026#34; branch bugfix/fix-redirect commit id: \u0026#34;Fix login redirect\u0026#34; checkout main merge bugfix/fix-redirect id: \u0026#34;Merge bugfix\u0026#34; branch hotfix/security-patch commit id: \u0026#34;Emergency security patch\u0026#34; checkout main merge hotfix/security-patch id: \u0026#34;Merge hotfix\u0026#34; commit id: \u0026#34;Tag v1.0.0\u0026#34; ","permalink":"https://jeanphilogong.github.io/jeanblog/notes/git-notes/git-branching-workflow/","summary":"\u003ch1 id=\"simplified-git-branch-workflow-solo--small-team\"\u003eSimplified Git Branch Workflow (Solo / Small Team)\u003c/h1\u003e\n\u003cp\u003eThis workflow is a simplified version of Git Flow. It is suitable for personal projects or small teams: structured but not heavy.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-main-branch-long-lived\"\u003e1. Main branch (long-lived)\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ccode\u003emain\u003c/code\u003e\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003eAlways stable and release-ready\u003c/li\u003e\n\u003cli\u003eProduction deployments come from here\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003eFor small teams, \u003ccode\u003emain\u003c/code\u003e is usually enough; no need for \u003ccode\u003edevelop\u003c/code\u003e.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-feature-development-feature-branch\"\u003e2. Feature development (feature branch)\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eNaming: \u003ccode\u003efeature/\u0026lt;feature-name\u0026gt;\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003ePurpose: build new features, then merge back to \u003ccode\u003emain\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eExamples:\u003c/p\u003e","title":"Git Branch Workflow for Small Teams"},{"content":"Use a Local Git Bare Repo to Separate Dev and Test Environments In full-stack work, a common problem is how to isolate dev and test environments. Many people host on GitHub or GitLab, but private projects may not be suitable for public hosting.\nGit is distributed. You can set up a local bare repo as a remote to move code from dev -\u0026gt; test in one machine.\nWhat is a bare repository? A normal repo (git init) has a working tree + .git metadata and can be edited directly. A bare repo (git init --bare) has only Git data, no working tree. It is usually used as a remote. In short:\nDev repo: where you write code Bare repo: remote sync point with full history Test repo: clone from bare repo to simulate deployment Step 1: Create the bare repo Create a bare repo under a local directory (e.g., ~/.repos):\nmkdir -p ~/.repos cd ~/.repos git init --bare scrapy.git Now ~/.repos/scrapy.git is your local remote.\nStep 2: Add the local remote in your dev repo Assume your dev repo is ~/scrapy:\ncd ~/scrapy git remote add local ~/.repos/scrapy.git Check:\ngit remote -v Expected:\nlocal /home/gong/.repos/scrapy.git (fetch) local /home/gong/.repos/scrapy.git (push) Step 3: Push to the local remote Push main:\ngit push local main Your bare repo now contains all commits.\nStep 4: Clone in the test environment Assume your test environment is ~/test-env:\ncd ~/test-env git clone ~/.repos/scrapy.git You now have a clean copy for testing without affecting dev.\nNote on HEAD warning Sometimes you see:\nwarning: remote HEAD refers to nonexistent ref, unable to checkout This happens because a newly created bare repo has no default HEAD. Set it:\ncd ~/.repos/scrapy.git git symbolic-ref HEAD refs/heads/main Then clone again.\nStep 5: Sync workflow In dev (~/scrapy):\ngit add . git commit -m \u0026#34;feat: finish feature\u0026#34; git push local main In test (~/test-env/scrapy):\ngit pull Now you can sync dev -\u0026gt; test easily on one machine.\nSummary If you cannot push to GitHub/GitLab, a local bare repo can separate dev and test:\nno external platform required dev and test isolated full Git history preserved If the project grows, consider a private Git service (Gitea/GitLab CE) or Docker deployment.\n","permalink":"https://jeanphilogong.github.io/jeanblog/notes/git-notes/git-bare-repo-dev-test-isolation/","summary":"\u003ch1 id=\"use-a-local-git-bare-repo-to-separate-dev-and-test-environments\"\u003eUse a Local Git Bare Repo to Separate Dev and Test Environments\u003c/h1\u003e\n\u003cp\u003eIn full-stack work, a common problem is \u003cstrong\u003ehow to isolate dev and test environments\u003c/strong\u003e. Many people host on GitHub or GitLab, but private projects may not be suitable for public hosting.\u003c/p\u003e\n\u003cp\u003eGit is distributed. You can set up a \u003cstrong\u003elocal bare repo\u003c/strong\u003e as a remote to move code from \u003cstrong\u003edev -\u0026gt; test\u003c/strong\u003e in one machine.\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"what-is-a-bare-repository\"\u003eWhat is a bare repository?\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eA normal repo (\u003ccode\u003egit init\u003c/code\u003e) has a \u003cstrong\u003eworking tree + .git metadata\u003c/strong\u003e and can be edited directly.\u003c/li\u003e\n\u003cli\u003eA bare repo (\u003ccode\u003egit init --bare\u003c/code\u003e) has only Git data, no working tree. It is usually used as a \u003cstrong\u003eremote\u003c/strong\u003e.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIn short:\u003c/p\u003e","title":"Use a Local Git Bare Repo to Separate Dev and Test Environments"},{"content":"Introduction For TypeScript files with the .ts extension, we cannot run them directly. We need to transpile TypeScript to JavaScript and then run the JavaScript output.\nThere are two common approaches: upload .ts to the server and compile via CI, or transpile locally and upload the .js build to production. If you want to run and test locally during development, you can use ts-node, but the project still needs a build step for production.\n","permalink":"https://jeanphilogong.github.io/jeanblog/dev/frontend/typescript-setup-guide/","summary":"\u003ch1 id=\"introduction\"\u003eIntroduction\u003c/h1\u003e\n\u003cp\u003eFor TypeScript files with the \u003ccode\u003e.ts\u003c/code\u003e extension, we cannot run them directly. We need to transpile TypeScript to JavaScript and then run the JavaScript output.\u003c/p\u003e\n\u003cp\u003eThere are two common approaches: upload \u003ccode\u003e.ts\u003c/code\u003e to the server and compile via CI, or transpile locally and upload the \u003ccode\u003e.js\u003c/code\u003e build to production. If you want to run and test locally during development, you can use \u003ccode\u003ets-node\u003c/code\u003e, but the project still needs a build step for production.\u003c/p\u003e","title":"How to Use and Configure a TypeScript Environment"},{"content":"Introduction I want to build an AI system that supports tree-shaped or graph-shaped Q\u0026amp;A, instead of a traditional single-thread chat flow.\nExploration Open-source framework research flowise ","permalink":"https://jeanphilogong.github.io/jeanblog/thoughts/thoughts/ai-assistant-frontend-rebuild-ideas/","summary":"\u003ch1 id=\"introduction\"\u003eIntroduction\u003c/h1\u003e\n\u003cp\u003eI want to build an AI system that supports tree-shaped or graph-shaped Q\u0026amp;A, instead of a traditional single-thread chat flow.\u003c/p\u003e\n\u003ch1 id=\"exploration\"\u003eExploration\u003c/h1\u003e\n\u003ch2 id=\"open-source-framework-research\"\u003eOpen-source framework research\u003c/h2\u003e\n\u003ch3 id=\"flowise\"\u003eflowise\u003c/h3\u003e","title":"A New Frontend Idea for AI Assistants"},{"content":"Introduction Mermaid is a framework for creating diagrams using code. This post shows how to install the tooling on your server and render Mermaid code into images.\nSteps Install the renderer Run:\nnpm install -g @mermaid-js/mermaid-cli Note: the CLI requires npm version \u0026gt;= 20. It is recommended to manage npm versions with nvm.\nIf you do not have nvm, install it with:\ncurl -o https://raw.githubusercontent.com/nvm-sh/nvim/v0.39.4/install.sh | bash Restart your shell, then run:\nnvm install 20 nvm use 20 nvm alias default 20 Verify:\nnode -v npm -v Render a diagram Put your Mermaid code in a file ending with .mmd, then run:\nmmdc -i diagrams/example.mmd -o images/example.svg ","permalink":"https://jeanphilogong.github.io/jeanblog/linux/linux/create-and-edit-mermaid-diagrams/","summary":"\u003ch1 id=\"introduction\"\u003eIntroduction\u003c/h1\u003e\n\u003cp\u003eMermaid is a framework for creating diagrams using code. This post shows how to install the tooling on your server and render Mermaid code into images.\u003c/p\u003e\n\u003ch1 id=\"steps\"\u003eSteps\u003c/h1\u003e\n\u003ch2 id=\"install-the-renderer\"\u003eInstall the renderer\u003c/h2\u003e\n\u003cp\u003eRun:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003enpm install -g @mermaid-js/mermaid-cli\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNote: the CLI requires npm version \u0026gt;= 20. It is recommended to manage npm versions with nvm.\u003c/p\u003e\n\u003cp\u003eIf you do not have nvm, install it with:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003ecurl -o https://raw.githubusercontent.com/nvm-sh/nvim/v0.39.4/install.sh | bash\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRestart your shell, then run:\u003c/p\u003e","title":"How to Create and Edit Mermaid Diagrams"},{"content":"How to Truly Master a Paper Conclusion To truly master a paper, reading once is not enough. You need to decompose, verify, and reconstruct it, and then express the key points in your own words or implementation. The goal: explain the core contribution in 5 minutes, derive key formulas by hand, and reproduce a core experiment.\nPrinciples and background A paper is a compressed expression of a problem. It omits background, intuition, failed attempts, and many details. Mastery requires \u0026ldquo;decompressing\u0026rdquo; that information into your own knowledge network: assumptions, derivations, engineering steps, and the limits of the results. Only then can you judge when to use it and when not to.\nSteps Do not treat a paper as authority. Treat it as a claim you can test. Break the claims into verifiable assertions and test them. Mastery is not memorizing text, but turning it into a tool you can use. Real understanding requires action: derive, implement, compare, explain.\nPreparation and pre-read (30-60 minutes)\nRead title, abstract, conclusion, figures (skip details). Capture what problem it solves and what results it claims. Scan intro and contributions; list 3 key claims. Check references to see if you need to read prerequisites. Deep read (2-6 hours)\nRead methods/theory carefully. Hand-derive key formulas. Create a symbol table; write pseudocode for algorithms. Mark unclear points and create a question list. Decompose and reconstruct (half day to days)\nBreak the paper into: problem, assumptions, method, theorems, experiments, conclusions, limits. Write 2-3 sentences for each section in your own words. Implement a minimal runnable version of the algorithm. Implement and reproduce (hours to days)\nFocus on the part that best reflects the contribution. Debug on small synthetic data, then match paper settings. Suggested environments: Python + Jupyter/Colab, or C++/Rust for systems/perf. Common libraries: numpy/pandas/matplotlib/scikit-learn/torch/tensorflow. Map paper symbols to code variables in comments/docstrings. Plot and compare\nReproduce key plots (loss curves, error tables). If exact numbers are hard, verify trends. Add assertions/unit tests to confirm theory on synthetic cases. Digest and output\nWrite a one-page cheatsheet or short blog; aim for a 5-minute explanation. Create Anki cards for assumptions, theorem conditions, derivation steps. Explain to someone else or write a report. Tools (practical)\nReferences: Zotero / Mendeley Notes: Obsidian / Notion / org-mode Code and experiments: Git + Jupyter/Colab + Docker Text tools: pdftotext, pdfgrep, grep, ripgrep Common mistakes Mistake: only read, never do (no derivation or implementation). Fix: force yourself to implement or write pseudocode and derive key steps. Mistake: ignore assumptions and boundaries. Fix: list all assumptions and test violations. Mistake: equate code with the paper. Fix: read author code and compare with the paper; record differences. Mistake: chase exact numeric reproduction too early. Fix: verify trends first, then refine details. Mistake: accept formulas without checking steps. Fix: derive line by line and track missing lemmas. Verification checklist Explain the core contribution, use cases, and limits in 5 minutes. Derive key formulas or rewrite the proof by hand. Implement a minimal working example that matches paper trends. Answer: what assumptions are critical, and what failure modes exist? Apply the idea to a slightly different problem and observe results. ","permalink":"https://jeanphilogong.github.io/jeanblog/thoughts/thoughts/mastering-paper/","summary":"\u003ch1 id=\"how-to-truly-master-a-paper\"\u003eHow to Truly Master a Paper\u003c/h1\u003e\n\u003ch1 id=\"conclusion\"\u003eConclusion\u003c/h1\u003e\n\u003cp\u003eTo truly master a paper, reading once is not enough. You need to decompose, verify, and reconstruct it, and then express the key points in your own words or implementation. The goal: explain the core contribution in 5 minutes, derive key formulas by hand, and reproduce a core experiment.\u003c/p\u003e\n\u003ch1 id=\"principles-and-background\"\u003ePrinciples and background\u003c/h1\u003e\n\u003cp\u003eA paper is a compressed expression of a problem. It omits background, intuition, failed attempts, and many details. Mastery requires \u0026ldquo;decompressing\u0026rdquo; that information into your own knowledge network: assumptions, derivations, engineering steps, and the limits of the results. Only then can you judge when to use it and when not to.\u003c/p\u003e","title":"Mastering a Paper"},{"content":"What problem does this paper solve, and what are the results? We know AI systems are expanding and can solve general tasks. But many AI agent applications today target small tasks. NVIDIA argues that small language models (SLMs) are capable, more suitable, and cheaper, and should be a main direction for future agents.\nThe paper discusses:\nWhat tasks current SLMs can handle Where general language ability matters The limits of SLMs as agents Conclusion: moving from LLMs to SLMs has advantages in both capability and cost.\n","permalink":"https://jeanphilogong.github.io/jeanblog/thoughts/thoughts/reading-nvidia-small-models-paper/","summary":"\u003ch1 id=\"what-problem-does-this-paper-solve-and-what-are-the-results\"\u003eWhat problem does this paper solve, and what are the results?\u003c/h1\u003e\n\u003cp\u003eWe know AI systems are expanding and can solve general tasks. But many AI agent applications today target small tasks. NVIDIA argues that small language models (SLMs) are capable, more suitable, and cheaper, and should be a main direction for future agents.\u003c/p\u003e\n\u003cp\u003eThe paper discusses:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eWhat tasks current SLMs can handle\u003c/li\u003e\n\u003cli\u003eWhere general language ability matters\u003c/li\u003e\n\u003cli\u003eThe limits of SLMs as agents\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eConclusion: moving from LLMs to SLMs has advantages in both capability and cost.\u003c/p\u003e","title":"Reading an NVIDIA Paper on Small Language Models"}]